diff --git a/package.json b/package.json index 63b00ee8f..3a33472f3 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "nslog": "^3", "oniguruma": "6.1.0", "pathwatcher": "~6.5", - "postcss-selector-parser": "^2.2.1", + "postcss": "5.2.4", + "postcss-selector-parser": "2.2.1", "property-accessors": "^1.1.3", "random-words": "0.0.1", "resolve": "^1.1.6", diff --git a/spec/style-manager-spec.js b/spec/style-manager-spec.js index 19451ffea..65fc24e9a 100644 --- a/spec/style-manager-spec.js +++ b/spec/style-manager-spec.js @@ -1,39 +1,107 @@ const StyleManager = require('../src/style-manager') describe('StyleManager', () => { - let [manager, addEvents, removeEvents, updateEvents] = [] + let [styleManager, addEvents, removeEvents, updateEvents] = [] beforeEach(() => { - manager = new StyleManager({configDirPath: atom.getConfigDirPath()}) + styleManager = new StyleManager({configDirPath: atom.getConfigDirPath()}) addEvents = [] removeEvents = [] updateEvents = [] - manager.onDidAddStyleElement((event) => { addEvents.push(event) }) - manager.onDidRemoveStyleElement((event) => { removeEvents.push(event) }) - manager.onDidUpdateStyleElement((event) => { updateEvents.push(event) }) + styleManager.onDidAddStyleElement((event) => { addEvents.push(event) }) + styleManager.onDidRemoveStyleElement((event) => { removeEvents.push(event) }) + styleManager.onDidUpdateStyleElement((event) => { updateEvents.push(event) }) }) describe('::addStyleSheet(source, params)', () => { it('adds a stylesheet based on the given source and returns a disposable allowing it to be removed', () => { - const disposable = manager.addStyleSheet('a {color: red}') + const disposable = styleManager.addStyleSheet('a {color: red}') expect(addEvents.length).toBe(1) expect(addEvents[0].textContent).toBe('a {color: red}') - const styleElements = manager.getStyleElements() + const styleElements = styleManager.getStyleElements() expect(styleElements.length).toBe(1) expect(styleElements[0].textContent).toBe('a {color: red}') disposable.dispose() expect(removeEvents.length).toBe(1) expect(removeEvents[0].textContent).toBe('a {color: red}') - expect(manager.getStyleElements().length).toBe(0) + expect(styleManager.getStyleElements().length).toBe(0) + }) + + describe('atom-text-editor shadow DOM selectors upgrades', () => { + beforeEach(() => { + // attach styles element to the DOM to parse CSS rules + styleManager.onDidAddStyleElement((styleElement) => { jasmine.attachToDOM(styleElement) }) + }) + + it('removes the ::shadow pseudo-element from atom-text-editor selectors', () => { + styleManager.addStyleSheet(` + atom-text-editor::shadow .class-1, atom-text-editor::shadow .class-2 { color: red } + atom-text-editor::shadow > .class-3 { color: yellow } + atom-text-editor .class-4 { color: blue } + another-element::shadow .class-5 { color: white } + `) + expect(Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map((r) => r.selectorText)).toEqual([ + 'atom-text-editor .class-1, atom-text-editor .class-2', + 'atom-text-editor > .class-3', + 'atom-text-editor .class-4', + 'another-element::shadow .class-5' + ]) + } + ) + + describe('when a selector targets the atom-text-editor shadow DOM', () => { + it('prepends "--syntax" to class selectors matching a grammar scope name and not already starting with "syntax--"', () => { + styleManager.addStyleSheet(` + .class-1 { color: red } + .source > .js, .source.coffee { color: green } + .syntax--source { color: gray } + #id-1 { color: blue } + `, {context: 'atom-text-editor'}) + expect(Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map((r) => r.selectorText)).toEqual([ + '.class-1', + '.syntax--source > .syntax--js, .syntax--source.syntax--coffee', + '.syntax--source', + '#id-1' + ]) + + styleManager.addStyleSheet(` + .source > .js, .source.coffee { color: green } + atom-text-editor::shadow .source > .js { color: yellow } + atom-text-editor .source > .js { color: red } + `) + expect(Array.from(styleManager.getStyleElements()[1].sheet.cssRules).map((r) => r.selectorText)).toEqual([ + '.source > .js, .source.coffee', + 'atom-text-editor .syntax--source > .syntax--js', + 'atom-text-editor .source > .js' + ]) + }) + }) + + it('replaces ":host" with "atom-text-editor" only when the context of a stylesheet is "atom-text-editor"', () => { + styleManager.addStyleSheet(':host .class-1, :host .class-2 { color: red; }') + expect(Array.from(styleManager.getStyleElements()[0].sheet.cssRules).map((r) => r.selectorText)).toEqual([ + ':host .class-1, :host .class-2' + ]) + global.debug = true + styleManager.addStyleSheet(':host .class-1, :host .class-2 { color: red; }', {context: 'atom-text-editor'}) + global.debug = false + expect(Array.from(styleManager.getStyleElements()[1].sheet.cssRules).map((r) => r.selectorText)).toEqual([ + 'atom-text-editor .class-1, atom-text-editor .class-2' + ]) + }) + + it('does not throw exceptions on rules with no selectors', () => { + styleManager.addStyleSheet('@media screen {font-size: 10px}', {context: 'atom-text-editor'}) + }) }) describe('when a sourcePath parameter is specified', () => { it('ensures a maximum of one style element for the given source path, updating a previous if it exists', () => { - const disposable1 = manager.addStyleSheet('a {color: red}', {sourcePath: '/foo/bar'}) + const disposable1 = styleManager.addStyleSheet('a {color: red}', {sourcePath: '/foo/bar'}) expect(addEvents.length).toBe(1) expect(addEvents[0].getAttribute('source-path')).toBe('/foo/bar') - const disposable2 = manager.addStyleSheet('a {color: blue}', {sourcePath: '/foo/bar'}) + const disposable2 = styleManager.addStyleSheet('a {color: blue}', {sourcePath: '/foo/bar'}) expect(addEvents.length).toBe(1) expect(updateEvents.length).toBe(1) expect(updateEvents[0].getAttribute('source-path')).toBe('/foo/bar') @@ -41,7 +109,7 @@ describe('StyleManager', () => { disposable2.dispose() addEvents = [] - manager.addStyleSheet('a {color: yellow}', {sourcePath: '/foo/bar'}) + styleManager.addStyleSheet('a {color: yellow}', {sourcePath: '/foo/bar'}) expect(addEvents.length).toBe(1) expect(addEvents[0].getAttribute('source-path')).toBe('/foo/bar') expect(addEvents[0].textContent).toBe('a {color: yellow}') @@ -50,11 +118,11 @@ describe('StyleManager', () => { describe('when a priority parameter is specified', () => { it('inserts the style sheet based on the priority', () => { - manager.addStyleSheet('a {color: red}', {priority: 1}) - manager.addStyleSheet('a {color: blue}', {priority: 0}) - manager.addStyleSheet('a {color: green}', {priority: 2}) - manager.addStyleSheet('a {color: yellow}', {priority: 1}) - expect(manager.getStyleElements().map((elt) => elt.textContent)).toEqual([ + styleManager.addStyleSheet('a {color: red}', {priority: 1}) + styleManager.addStyleSheet('a {color: blue}', {priority: 0}) + styleManager.addStyleSheet('a {color: green}', {priority: 2}) + styleManager.addStyleSheet('a {color: yellow}', {priority: 1}) + expect(styleManager.getStyleElements().map((elt) => elt.textContent)).toEqual([ 'a {color: blue}', 'a {color: red}', 'a {color: yellow}', diff --git a/spec/styles-element-spec.coffee b/spec/styles-element-spec.coffee index b29979524..0889b0d43 100644 --- a/spec/styles-element-spec.coffee +++ b/spec/styles-element-spec.coffee @@ -77,71 +77,3 @@ describe "StylesElement", -> expect(element.children.length).toBe 2 expect(element.children[0].textContent).toBe "a {color: red;}" expect(element.children[1].textContent).toBe "a {color: blue;}" - - describe "atom-text-editor shadow DOM selector upgrades", -> - beforeEach -> - spyOn(console, 'warn') - - it "removes the ::shadow pseudo-element from atom-text-editor selectors", -> - atom.styles.addStyleSheet(""" - atom-text-editor::shadow .class-1, atom-text-editor::shadow .class-2 { color: red; } - atom-text-editor::shadow > .class-3 { color: yellow; } - atom-text-editor .class-4 { color: blue; } - another-element::shadow .class-5 { color: white; } - """) - expect(Array.from(element.lastChild.sheet.cssRules).map((r) -> r.selectorText)).toEqual([ - 'atom-text-editor .class-1, atom-text-editor .class-2', - 'atom-text-editor > .class-3', - 'atom-text-editor .class-4', - 'another-element::shadow .class-5' - ]) - expect(console.warn).toHaveBeenCalled() - - describe "when the context of a style sheet is 'atom-text-editor'", -> - it "prepends `--syntax` to selectors not contained in atom-text-editor or matching a spatial decoration", -> - atom.styles.addStyleSheet(""" - .class-1 { color: red; } - .class-2 > .class-3, .class-4.class-5 { color: green; } - .class-6 atom-text-editor .class-7 { color: yellow; } - atom-text-editor .class-8, .class-9 { color: blue; } - atom-text-editor .indent-guide, atom-text-editor .leading-whitespace { background: white; } - .syntax--class-10 { color: gray; } - :host .class-11 { color: purple; } - #id-1 { color: gray; } - """, {context: 'atom-text-editor'}) - expect(Array.from(element.lastChild.sheet.cssRules).map((r) -> r.selectorText)).toEqual([ - '.syntax--class-1', - '.syntax--class-2 > .syntax--class-3, .syntax--class-4.syntax--class-5', - '.class-6 atom-text-editor .class-7', - 'atom-text-editor .class-8, .syntax--class-9', - 'atom-text-editor .syntax--indent-guide, atom-text-editor .syntax--leading-whitespace', - '.syntax--class-10', - 'atom-text-editor .class-11', - '#id-1' - ]) - expect(console.warn).toHaveBeenCalled() - - describe "when the context of a style sheet is not 'atom-text-editor'", -> - it "never prepends class names with `--syntax`", -> - atom.styles.addStyleSheet(""" - .class-1 { color: red; } - .class-2 > .class-3, .class-4.class-5 { color: green; } - .class-6 atom-text-editor .class-7 { color: yellow; } - atom-text-editor .class-8, .class-9 { color: blue; } - atom-text-editor .indent-guide, atom-text-editor .leading-whitespace { background: white; } - #id-1 { color: gray; } - """) - expect(Array.from(element.lastChild.sheet.cssRules).map((r) -> r.selectorText)).toEqual([ - '.class-1' - '.class-2 > .class-3, .class-4.class-5' - '.class-6 atom-text-editor .class-7' - 'atom-text-editor .class-8, .class-9' - 'atom-text-editor .indent-guide, atom-text-editor .leading-whitespace' - '#id-1' - ]) - expect(console.warn).not.toHaveBeenCalled() - - it "does not throw exceptions on rules with no selectors", -> - atom.styles.addStyleSheet """ - @media screen {font-size: 10px;} - """, context: 'atom-text-editor' diff --git a/src/deprecated-syntax-selectors.js b/src/deprecated-syntax-selectors.js index 8b85956ca..8b798bd4c 100644 --- a/src/deprecated-syntax-selectors.js +++ b/src/deprecated-syntax-selectors.js @@ -863,7 +863,7 @@ module.exports = new Set([ 'trace', 'trace-argument', 'trace-object', 'traceback', 'tracing', 'track_processing', 'trader', 'tradersk', 'trail', 'trailing', 'trailing-array-separator', 'trailing-dictionary-separator', 'trailing-match', - 'trailing-whitespace', 'trait', 'traits', 'traits-keyword', 'transaction', + 'trait', 'traits', 'traits-keyword', 'transaction', 'transcendental', 'transcludeblock', 'transcludeinline', 'transclusion', 'transform', 'transformation', 'transient', 'transition', 'transitionable-property-value', 'translation', 'transmission-filter', diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index c7e3bf367..c1cb2ba64 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -202,8 +202,8 @@ class LinesTileComponent if @presenter.isCloseTagCode(tagCode) openScopeNode = openScopeNode.parentElement else if @presenter.isOpenTagCode(tagCode) - scopes = @presenter.tagForCode(tagCode).replace(/\s+/g, '.').split('.').map((scope) -> "syntax--#{scope}") - newScopeNode = @domElementPool.buildElement("span", scopes.join(' ')) + scope = @presenter.tagForCode(tagCode) + newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' ')) openScopeNode.appendChild(newScopeNode) openScopeNode = newScopeNode else @@ -219,7 +219,7 @@ class LinesTileComponent if lineText.endsWith(@presenter.displayLayer.foldCharacter) # Insert a zero-width non-breaking whitespace, so that - # LinesYardstick can take the syntax--fold-marker::after pseudo-element + # LinesYardstick can take the fold-marker::after pseudo-element # into account during measurements when such marker is the last # character on the line. textNode = @domElementPool.buildText(ZERO_WIDTH_NBSP) diff --git a/src/style-manager.js b/src/style-manager.js index ae06f9060..a294d1d15 100644 --- a/src/style-manager.js +++ b/src/style-manager.js @@ -1,7 +1,10 @@ +const {Emitter, Disposable} = require('event-kit') const fs = require('fs-plus') const path = require('path') -const {Emitter, Disposable} = require('event-kit') +const postcss = require('postcss') +const selectorParser = require('postcss-selector-parser') const StylesElement = require('./styles-element') +const DEPRECATED_SYNTAX_SELECTORS = require('./deprecated-syntax-selectors') // Extended: A singleton instance of this class available via `atom.styles`, // which you can use to globally query and observe the set of active style @@ -122,7 +125,7 @@ module.exports = class StyleManager { } } - styleElement.textContent = source + styleElement.textContent = transformDeprecatedShadowDOMSelectors(source, params.context) if (updated) { this.emitter.emit('did-update-style-element', styleElement) } else { @@ -205,3 +208,37 @@ module.exports = class StyleManager { } } } + +function transformDeprecatedShadowDOMSelectors (css, context) { + const root = postcss.parse(css) + root.walkRules((rule) => { + rule.selector = selectorParser((selectors) => { + selectors.each((selector) => { + const firstNode = selector.nodes[0] + if (context === 'atom-text-editor' && firstNode.type === 'pseudo' && firstNode.value === ':host') { + const atomTextEditorElementNode = selectorParser.tag({value: 'atom-text-editor'}) + firstNode.replaceWith(atomTextEditorElementNode) + } + + let targetsAtomTextEditorShadow = context === 'atom-text-editor' + let previousNode + selector.each((node) => { + if (targetsAtomTextEditorShadow && node.type === 'class') { + if (DEPRECATED_SYNTAX_SELECTORS.has(node.value) && !node.value.startsWith('syntax--')) { + node.value = `syntax--${node.value}` + } + } else if (previousNode) { + const currentNodeIsShadowPseudoClass = node.type === 'pseudo' && node.value === '::shadow' + const previousNodeIsAtomTextEditor = previousNode.type === 'tag' && previousNode.value === 'atom-text-editor' + if (previousNodeIsAtomTextEditor && currentNodeIsShadowPseudoClass) { + selector.removeChild(node) + targetsAtomTextEditorShadow = true + } + } + previousNode = node + }) + }) + }).process(rule.selector).result + }) + return root.toString() +} diff --git a/src/styles-element.coffee b/src/styles-element.coffee index e82775c92..2c53300c2 100644 --- a/src/styles-element.coffee +++ b/src/styles-element.coffee @@ -1,9 +1,4 @@ {Emitter, CompositeDisposable} = require 'event-kit' -selectorProcessor = require 'postcss-selector-parser' -SPATIAL_DECORATIONS = new Set([ - 'invisible-character', 'hard-tab', 'leading-whitespace', - 'trailing-whitespace', 'eol', 'indent-guide', 'fold-marker' -]) class StylesElement extends HTMLElement subscriptions: null @@ -24,9 +19,6 @@ class StylesElement extends HTMLElement @styleElementClonesByOriginalElement = new WeakMap attachedCallback: -> - for styleElement in @children - @upgradeDeprecatedSelectors(styleElement) - @context = @getAttribute('context') ? undefined detachedCallback: -> @@ -68,7 +60,6 @@ class StylesElement extends HTMLElement break @insertBefore(styleElementClone, insertBefore) - @upgradeDeprecatedSelectors(styleElementClone) @emitter.emit 'did-add-style-element', styleElementClone styleElementRemoved: (styleElement) -> @@ -88,52 +79,4 @@ class StylesElement extends HTMLElement styleElementMatchesContext: (styleElement) -> not @context? or styleElement.context is @context - upgradeDeprecatedSelectors: (styleElement) -> - return unless styleElement.sheet? - - transformDeprecatedShadowSelectors = (selectors) -> - selectors.each (selector) -> - isSyntaxSelector = not selector.some((node) -> - (node.type is 'tag' and node.value is 'atom-text-editor') or - (node.type is 'class' and node.value is 'region') or - (node.type is 'class' and node.value is 'wrap-guide') or - (node.type is 'class' and /spell-check/.test(node.value)) - ) - previousNode = null - selector.each (node) -> - isShadowPseudoClass = node.type is 'pseudo' and node.value is '::shadow' - isHostPseudoClass = node.type is 'pseudo' and node.value is ':host' - if isHostPseudoClass and not previousNode? - newNode = selectorProcessor.tag({value: 'atom-text-editor'}) - node.replaceWith(newNode) - previousNode = newNode - else if isShadowPseudoClass and previousNode?.type is 'tag' and previousNode?.value is 'atom-text-editor' - selector.removeChild(node) - else - if styleElement.context is 'atom-text-editor' and node.type is 'class' - if (isSyntaxSelector and not node.value.startsWith('syntax--')) or SPATIAL_DECORATIONS.has(node.value) - node.value = 'syntax--' + node.value - previousNode = node - - upgradedSelectors = [] - for rule in styleElement.sheet.cssRules when rule.selectorText? - inputSelector = rule.selectorText - outputSelector = rule.selectorText - outputSelector = selectorProcessor(transformDeprecatedShadowSelectors).process(outputSelector).result - if inputSelector isnt outputSelector - rule.selectorText = outputSelector - upgradedSelectors.push({inputSelector, outputSelector}) - - if upgradedSelectors.length > 0 - upgradedSelectorsText = upgradedSelectors.map(({inputSelector, outputSelector}) -> "`#{inputSelector}` => `#{outputSelector}`").join('\n') - console.warn(""" - Shadow DOM for `atom-text-editor` elements has been removed. This means - should stop using :host and ::shadow pseudo-selectors, and prepend all - your syntax selectors with `syntax--`. To prevent breakage with existing - stylesheets, we have automatically upgraded the following selectors in - `#{styleElement.sourcePath}`: - - #{upgradedSelectorsText} - """) - module.exports = StylesElement = document.registerElement 'atom-styles', prototype: StylesElement.prototype diff --git a/static/text-editor-light.less b/static/text-editor-light.less index 1f16ec71e..193749d51 100644 --- a/static/text-editor-light.less +++ b/static/text-editor-light.less @@ -122,12 +122,12 @@ atom-text-editor { .line { white-space: pre; - &.cursor-line .syntax--fold-marker::after { + &.cursor-line .fold-marker::after { opacity: 1; } } - .syntax--fold-marker { + .fold-marker { cursor: default; &::after { @@ -143,12 +143,12 @@ atom-text-editor { color: @text-color-subtle; } - .syntax--invisible-character { + .invisible-character { font-weight: normal !important; font-style: normal !important; } - .syntax--indent-guide { + .indent-guide { display: inline-block; box-shadow: inset 1px 0; }