mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Transform deprecated shadow DOM selectors in StyleManager
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user