diff --git a/.eslintrc b/.eslintrc index ffd436e..4d1236d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,6 +6,6 @@ "node": true }, "rules": { - "quotes": [2, "single"] + "quotes": [2, "single", { "avoidEscape": true }] } } diff --git a/index.js b/index.js index e772160..0e8ee03 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,14 @@ 'use strict'; const postcss = require('postcss'); -const Tokenizer = require('css-selector-tokenizer'); +const selectorParser = require('postcss-selector-parser'); const valueParser = require('postcss-value-parser'); +const isSpacing = node => node.type === 'combinator' && node.value === ' '; + function normalizeNodeArray(nodes) { const array = []; + nodes.forEach(function(x) { if (Array.isArray(x)) { normalizeNodeArray(x).forEach(function(item) { @@ -16,150 +19,206 @@ function normalizeNodeArray(nodes) { } }); - if (array.length > 0 && array[array.length - 1].type === 'spacing') { + if (array.length > 0 && isSpacing(array[array.length - 1])) { array.pop(); } return array; } -function localizeNode(node, context) { - if (context.ignoreNextSpacing && node.type !== 'spacing') { - throw new Error('Missing whitespace after :' + context.ignoreNextSpacing); - } - if (context.enforceNoSpacing && node.type === 'spacing') { - throw new Error('Missing whitespace before :' + context.enforceNoSpacing); - } +function localizeNode(rule, mode, options) { + const isScopePseudo = node => + node.value === ':local' || node.value === ':global'; - let newNodes; - switch (node.type) { - case 'selectors': - let resultingGlobal; - context.hasPureGlobals = false; - newNodes = node.nodes.map(function(n) { - const nContext = { - global: context.global, - lastWasSpacing: true, - hasLocals: false, - explicit: false - }; - n = localizeNode(n, nContext); - if (typeof resultingGlobal === 'undefined') { - resultingGlobal = nContext.global; - } else if (resultingGlobal !== nContext.global) { - throw new Error( - 'Inconsistent rule global/local result in rule "' + - Tokenizer.stringify(node) + - '" (multiple selectors must result in the same mode for the rule)' - ); - } - if (!nContext.hasLocals) { - context.hasPureGlobals = true; - } - return n; - }); - context.global = resultingGlobal; - node = Object.create(node); - node.nodes = normalizeNodeArray(newNodes); - break; + const transform = (node, context) => { + if (context.ignoreNextSpacing && !isSpacing(node)) { + throw new Error('Missing whitespace after ' + context.ignoreNextSpacing); + } + if (context.enforceNoSpacing && isSpacing(node)) { + throw new Error('Missing whitespace before ' + context.enforceNoSpacing); + } - case 'selector': - newNodes = node.nodes.map(function(n) { - return localizeNode(n, context); - }); - node = Object.create(node); - node.nodes = normalizeNodeArray(newNodes); - break; + let newNodes; + switch (node.type) { + case 'root': { + let resultingGlobal; + context.hasPureGlobals = false; + newNodes = node.nodes.map(function(n) { + const nContext = { + global: context.global, + lastWasSpacing: true, + hasLocals: false, + explicit: false, + }; + n = transform(n, nContext); + if (typeof resultingGlobal === 'undefined') { + resultingGlobal = nContext.global; + } else if (resultingGlobal !== nContext.global) { + throw new Error( + 'Inconsistent rule global/local result in rule "' + + node + + '" (multiple selectors must result in the same mode for the rule)' + ); + } + if (!nContext.hasLocals) { + context.hasPureGlobals = true; + } + return n; + }); + context.global = resultingGlobal; + + node.nodes = normalizeNodeArray(newNodes); + break; + } + case 'selector': { + newNodes = node.map(childNode => transform(childNode, context)); - case 'spacing': - if (context.ignoreNextSpacing) { - context.ignoreNextSpacing = false; - context.lastWasSpacing = false; - context.enforceNoSpacing = false; - return null; + node = node.clone(); + node.nodes = normalizeNodeArray(newNodes); + break; } - context.lastWasSpacing = true; - return node; - - case 'pseudo-class': - if (node.name === 'local' || node.name === 'global') { - if (context.inside) { - throw new Error( - 'A :' + - node.name + - ' is not allowed inside of a :' + - context.inside + - '(...)' - ); + case 'combinator': { + if (isSpacing(node)) { + if (context.ignoreNextSpacing) { + context.ignoreNextSpacing = false; + context.lastWasSpacing = false; + context.enforceNoSpacing = false; + return null; + } + context.lastWasSpacing = true; + return node; } - context.ignoreNextSpacing = context.lastWasSpacing ? node.name : false; - context.enforceNoSpacing = context.lastWasSpacing ? false : node.name; - context.global = node.name === 'global'; - context.explicit = true; - return null; + break; } - break; + case 'pseudo': { + let childContext; + const isNested = !!node.length; + const isScoped = isScopePseudo(node); + + // :local(.foo) + if (isNested) { + if (isScoped) { + if (context.inside) { + throw new Error( + `A ${node.value} is not allowed inside of a ${ + context.inside + }(...)` + ); + } + + childContext = { + global: node.value === ':global', + inside: node.value, + hasLocals: false, + explicit: true, + }; + + newNodes = node + .map(childNode => transform(childNode, childContext)) + .reduce((acc, next) => acc.concat(next.nodes), []); + + if (newNodes.length) { + const { before, after } = node.spaces; + + const first = newNodes[0]; + const last = newNodes[newNodes.length - 1]; + + first.spaces = { before, after: first.spaces.after }; + last.spaces = { before: last.spaces.before, after }; + } + + node = newNodes; + + break; + } else { + childContext = { + global: context.global, + inside: context.inside, + lastWasSpacing: true, + hasLocals: false, + explicit: context.explicit, + }; + newNodes = node.map(childNode => + transform(childNode, childContext) + ); - case 'nested-pseudo-class': - let subContext; - if (node.name === 'local' || node.name === 'global') { - if (context.inside) { - throw new Error( - 'A :' + - node.name + - '(...) is not allowed inside of a :' + - context.inside + - '(...)' - ); + node = node.clone(); + node.nodes = normalizeNodeArray(newNodes); + + if (childContext.hasLocals) { + context.hasLocals = true; + } + } + break; + + //:local .foo .bar + } else if (isScoped) { + if (context.inside) { + throw new Error( + `A ${node.value} is not allowed inside of a ${ + context.inside + }(...)` + ); + } + + const next = node.parent; + const addBackSpacing = !!node.spaces.before; + + context.ignoreNextSpacing = context.lastWasSpacing + ? node.value + : false; + + context.enforceNoSpacing = context.lastWasSpacing + ? false + : node.value; + + context.global = node.value === ':global'; + context.explicit = true; + + // because this node has spacing that is lost when we remove it + // we make up for it by adding an extra combinator in since adding + // spacing on the parent selector doesn't work + return addBackSpacing + ? selectorParser.combinator({ value: ' ' }) + : null; } - subContext = { - global: node.name === 'global', - inside: node.name, - hasLocals: false, - explicit: true - }; - node = node.nodes.map(function(n) { - return localizeNode(n, subContext); - }); - // don't leak spacing - node[0].before = undefined; - node[node.length - 1].after = undefined; - } else { - subContext = { - global: context.global, - inside: context.inside, - lastWasSpacing: true, - hasLocals: false, - explicit: context.explicit - }; - newNodes = node.nodes.map(function(n) { - return localizeNode(n, subContext); - }); - node = Object.create(node); - node.nodes = normalizeNodeArray(newNodes); + break; } - if (subContext.hasLocals) { - context.hasLocals = true; - } - break; + case 'id': + case 'class': { + if (!context.global) { + const innerNode = node.clone(); + innerNode.spaces = { before: '', after: '' }; + + node = selectorParser.pseudo({ + value: ':local', + nodes: [innerNode], + spaces: node.spaces, + }); + + context.hasLocals = true; + } - case 'id': - case 'class': - if (!context.global) { - node = { - type: 'nested-pseudo-class', - name: 'local', - nodes: [node] - }; - context.hasLocals = true; + break; } - break; - } + } - // reset context - context.lastWasSpacing = false; - context.ignoreNextSpacing = false; - context.enforceNoSpacing = false; - return node; + context.lastWasSpacing = false; + context.ignoreNextSpacing = false; + context.enforceNoSpacing = false; + return node; + }; + + const rootContext = { + global: mode === 'global', + hasPureGlobals: false, + }; + + const selector = selectorParser(root => { + transform(root, rootContext); + }).processSync(rule, { updateSelector: false, lossless: true }); + + rootContext.selector = selector; + return rootContext; } function localizeDeclNode(node, context) { @@ -172,22 +231,29 @@ function localizeDeclNode(node, context) { break; case 'function': - if (context.options && context.options.rewriteUrl && node.value.toLowerCase() === 'url') { - node.nodes.map((nestedNode) => { + if ( + context.options && + context.options.rewriteUrl && + node.value.toLowerCase() === 'url' + ) { + node.nodes.map(nestedNode => { if (nestedNode.type !== 'string' && nestedNode.type !== 'word') { return; } - let newUrl = context.options.rewriteUrl(context.global, nestedNode.value); + let newUrl = context.options.rewriteUrl( + context.global, + nestedNode.value + ); switch (nestedNode.type) { case 'string': - if (nestedNode.quote === '\'') { - newUrl = newUrl.replace(/(\\)/g, '\\$1').replace(/'/g, '\\\'') + if (nestedNode.quote === "'") { + newUrl = newUrl.replace(/(\\)/g, '\\$1').replace(/'/g, "\\'"); } if (nestedNode.quote === '"') { - newUrl = newUrl.replace(/(\\)/g, '\\$1').replace(/"/g, '\\"') + newUrl = newUrl.replace(/(\\)/g, '\\$1').replace(/"/g, '\\"'); } break; @@ -206,8 +272,11 @@ function localizeDeclNode(node, context) { function isWordAFunctionArgument(wordNode, functionNode) { return functionNode - ? functionNode.nodes.some(functionNodeChild => functionNodeChild.sourceIndex === wordNode.sourceIndex) - : false + ? functionNode.nodes.some( + functionNodeChild => + functionNodeChild.sourceIndex === wordNode.sourceIndex + ) + : false; } function localizeAnimationShorthandDeclValues(decl, context) { @@ -245,13 +314,13 @@ function localizeAnimationShorthandDeclValues(decl, context) { '$step-start': 1, $initial: Infinity, $inherit: Infinity, - $unset: Infinity + $unset: Infinity, }; const didParseAnimationName = false; let parsedAnimationKeywords = {}; let stepsFunctionNode = null; - const valueNodes = valueParser(decl.value).walk((node) => { + const valueNodes = valueParser(decl.value).walk(node => { /* If div-token appeared (represents as comma ','), a possibility of an animation-keywords should be reflesh. */ if (node.type === 'div') { parsedAnimationKeywords = {}; @@ -284,7 +353,7 @@ function localizeAnimationShorthandDeclValues(decl, context) { const subContext = { options: context.options, global: context.global, - localizeNextItem: shouldParseAnimationName && !context.global + localizeNextItem: shouldParseAnimationName && !context.global, }; return localizeDeclNode(node, subContext); }); @@ -298,7 +367,7 @@ function localizeDeclValues(localize, decl, context) { const subContext = { options: context.options, global: context.global, - localizeNextItem: localize && !context.global + localizeNextItem: localize && !context.global, }; nodes[index] = localizeDeclNode(node, subContext); }); @@ -367,7 +436,7 @@ module.exports = postcss.plugin('postcss-modules-local-by-default', function( atrule.walkDecls(function(decl) { localizeDecl(decl, { options: options, - global: globalKeyframes + global: globalKeyframes, }); }); } else if (atrule.nodes) { @@ -375,7 +444,7 @@ module.exports = postcss.plugin('postcss-modules-local-by-default', function( if (decl.type === 'decl') { localizeDecl(decl, { options: options, - global: globalMode + global: globalMode, }); } }); @@ -389,33 +458,24 @@ module.exports = postcss.plugin('postcss-modules-local-by-default', function( // ignore keyframe rules return; } - const selector = Tokenizer.parse(rule.selector); - const context = { - options: options, - global: globalMode, - hasPureGlobals: false - }; - let newSelector; - try { - newSelector = localizeNode(selector, context); - } catch (e) { - throw rule.error(e.message); - } + + const context = localizeNode(rule, options.mode); + + context.options = options; + if (pureMode && context.hasPureGlobals) { throw rule.error( 'Selector "' + - Tokenizer.stringify(selector) + + rule.selector + '" is not pure ' + '(pure selectors must contain at least one local class or id)' ); } + rule.selector = context.selector; // Less-syntax mixins parse as rules with no nodes if (rule.nodes) { - rule.nodes.forEach(function(decl) { - localizeDecl(decl, context); - }); + rule.nodes.forEach(decl => localizeDecl(decl, context)); } - rule.selector = Tokenizer.stringify(newSelector); }); }; }); diff --git a/package.json b/package.json index 35da4b5..7ae0368 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,15 @@ "type": "git", "url": "https://github.com/css-modules/postcss-modules-local-by-default.git" }, + "prettier": { + "semi": true, + "singleQuote": true, + "trailingComma": "es5" + }, "dependencies": { "css-selector-tokenizer": "^0.7.0", "postcss": "^7.0.6", + "postcss-selector-parser": "^5.0.0", "postcss-value-parser": "^3.3.1" }, "devDependencies": { diff --git a/test.js b/test.js index d83090d..53123cb 100644 --- a/test.js +++ b/test.js @@ -9,194 +9,200 @@ const tests = [ { should: 'scope selectors', input: '.foobar {}', - expected: ':local(.foobar) {}' + expected: ':local(.foobar) {}', }, { should: 'scope ids', input: '#foobar {}', - expected: ':local(#foobar) {}' + expected: ':local(#foobar) {}', }, { should: 'scope multiple selectors', input: '.foo, .baz {}', - expected: ':local(.foo), :local(.baz) {}' + expected: ':local(.foo), :local(.baz) {}', }, { should: 'scope sibling selectors', input: '.foo ~ .baz {}', - expected: ':local(.foo) ~ :local(.baz) {}' + expected: ':local(.foo) ~ :local(.baz) {}', }, { should: 'scope psuedo elements', input: '.foo:after {}', - expected: ':local(.foo):after {}' + expected: ':local(.foo):after {}', }, { should: 'scope media queries', input: '@media only screen { .foo {} }', - expected: '@media only screen { :local(.foo) {} }' + expected: '@media only screen { :local(.foo) {} }', }, { should: 'allow narrow global selectors', input: ':global(.foo .bar) {}', - expected: '.foo .bar {}' + expected: '.foo .bar {}', }, { should: 'allow narrow local selectors', input: ':local(.foo .bar) {}', - expected: ':local(.foo) :local(.bar) {}' + expected: ':local(.foo) :local(.bar) {}', }, { should: 'allow broad global selectors', input: ':global .foo .bar {}', - expected: '.foo .bar {}' + expected: '.foo .bar {}', }, { should: 'allow broad local selectors', input: ':local .foo .bar {}', - expected: ':local(.foo) :local(.bar) {}' + expected: ':local(.foo) :local(.bar) {}', }, { should: 'allow multiple narrow global selectors', input: ':global(.foo), :global(.bar) {}', - expected: '.foo, .bar {}' + expected: '.foo, .bar {}', }, { should: 'allow multiple broad global selectors', input: ':global .foo, :global .bar {}', - expected: '.foo, .bar {}' + expected: '.foo, .bar {}', }, { should: 'allow multiple broad local selectors', input: ':local .foo, :local .bar {}', - expected: ':local(.foo), :local(.bar) {}' + expected: ':local(.foo), :local(.bar) {}', }, { should: 'allow narrow global selectors nested inside local styles', input: '.foo :global(.foo .bar) {}', - expected: ':local(.foo) .foo .bar {}' + expected: ':local(.foo) .foo .bar {}', }, { should: 'allow broad global selectors nested inside local styles', input: '.foo :global .foo .bar {}', - expected: ':local(.foo) .foo .bar {}' + expected: ':local(.foo) .foo .bar {}', }, { should: 'allow parentheses inside narrow global selectors', input: '.foo :global(.foo:not(.bar)) {}', - expected: ':local(.foo) .foo:not(.bar) {}' + expected: ':local(.foo) .foo:not(.bar) {}', }, { should: 'allow parentheses inside narrow local selectors', input: '.foo :local(.foo:not(.bar)) {}', - expected: ':local(.foo) :local(.foo):not(:local(.bar)) {}' + expected: ':local(.foo) :local(.foo):not(:local(.bar)) {}', }, { should: 'allow narrow global selectors appended to local styles', input: '.foo:global(.foo.bar) {}', - expected: ':local(.foo).foo.bar {}' + expected: ':local(.foo).foo.bar {}', }, { should: 'ignore selectors that are already local', input: ':local(.foobar) {}', - expected: ':local(.foobar) {}' + expected: ':local(.foobar) {}', }, { should: 'ignore nested selectors that are already local', input: ':local(.foo) :local(.bar) {}', - expected: ':local(.foo) :local(.bar) {}' + expected: ':local(.foo) :local(.bar) {}', }, { should: 'ignore multiple selectors that are already local', input: ':local(.foo), :local(.bar) {}', - expected: ':local(.foo), :local(.bar) {}' + expected: ':local(.foo), :local(.bar) {}', }, { should: 'ignore sibling selectors that are already local', input: ':local(.foo) ~ :local(.bar) {}', - expected: ':local(.foo) ~ :local(.bar) {}' + expected: ':local(.foo) ~ :local(.bar) {}', }, { should: 'ignore psuedo elements that are already local', input: ':local(.foo):after {}', - expected: ':local(.foo):after {}' + expected: ':local(.foo):after {}', + }, + { + should: 'trim whitespace after empty broad selector', + input: '.bar :global :global {}', + expected: ':local(.bar) {}', }, { should: 'broad global should be limited to selector', input: ':global .foo, .bar :global, .foobar :global {}', - expected: '.foo, :local(.bar), :local(.foobar) {}' + expected: '.foo, :local(.bar), :local(.foobar) {}', }, { should: 'broad global should be limited to nested selector', input: '.foo:not(:global .bar).foobar {}', - expected: ':local(.foo):not(.bar):local(.foobar) {}' + expected: ':local(.foo):not(.bar):local(.foobar) {}', }, { should: 'broad global and local should allow switching', input: '.foo :global .bar :local .foobar :local .barfoo {}', - expected: ':local(.foo) .bar :local(.foobar) :local(.barfoo) {}' + expected: ':local(.foo) .bar :local(.foobar) :local(.barfoo) {}', }, { should: 'localize a single animation-name', input: '.foo { animation-name: bar; }', - expected: ':local(.foo) { animation-name: :local(bar); }' + expected: ':local(.foo) { animation-name: :local(bar); }', }, { should: 'not localize a single animation-delay', input: '.foo { animation-delay: 1s; }', - expected: ':local(.foo) { animation-delay: 1s; }' + expected: ':local(.foo) { animation-delay: 1s; }', }, { should: 'localize multiple animation-names', input: '.foo { animation-name: bar, foobar; }', - expected: ':local(.foo) { animation-name: :local(bar), :local(foobar); }' + expected: ':local(.foo) { animation-name: :local(bar), :local(foobar); }', }, { should: 'localize animation', input: '.foo { animation: bar 5s, foobar; }', - expected: ':local(.foo) { animation: :local(bar) 5s, :local(foobar); }' + expected: ':local(.foo) { animation: :local(bar) 5s, :local(foobar); }', }, { should: 'localize animation with vendor prefix', input: '.foo { -webkit-animation: bar; animation: bar; }', expected: - ':local(.foo) { -webkit-animation: :local(bar); animation: :local(bar); }' + ':local(.foo) { -webkit-animation: :local(bar); animation: :local(bar); }', }, { should: 'not localize other rules', input: '.foo { content: "animation: bar;" }', - expected: ':local(.foo) { content: "animation: bar;" }' + expected: ':local(.foo) { content: "animation: bar;" }', }, { should: 'not localize global rules', input: ':global .foo { animation: foo; animation-name: bar; }', - expected: '.foo { animation: foo; animation-name: bar; }' + expected: '.foo { animation: foo; animation-name: bar; }', }, { should: 'handle a complex animation rule', input: '.foo { animation: foo, bar 5s linear 2s infinite alternate, barfoo 1s; }', expected: - ':local(.foo) { animation: :local(foo), :local(bar) 5s linear 2s infinite alternate, :local(barfoo) 1s; }' + ':local(.foo) { animation: :local(foo), :local(bar) 5s linear 2s infinite alternate, :local(barfoo) 1s; }', }, { should: 'handle animations where the first value is not the animation name', input: '.foo { animation: 1s foo; }', - expected: ':local(.foo) { animation: 1s :local(foo); }' + expected: ':local(.foo) { animation: 1s :local(foo); }', }, { should: 'handle animations where the first value is not the animation name whilst also using keywords', input: '.foo { animation: 1s normal ease-out infinite foo; }', expected: - ':local(.foo) { animation: 1s normal ease-out infinite :local(foo); }' + ':local(.foo) { animation: 1s normal ease-out infinite :local(foo); }', }, { should: 'not treat animation curve as identifier of animation name even if it separated by comma', - input: '.foo { animation: slide-right 300ms forwards ease-out, fade-in 300ms forwards ease-out; }', + input: + '.foo { animation: slide-right 300ms forwards ease-out, fade-in 300ms forwards ease-out; }', expected: - ':local(.foo) { animation: :local(slide-right) 300ms forwards ease-out, :local(fade-in) 300ms forwards ease-out; }' + ':local(.foo) { animation: :local(slide-right) 300ms forwards ease-out, :local(fade-in) 300ms forwards ease-out; }', }, { should: @@ -207,48 +213,47 @@ const tests = [ '.foo { animation: spin 1s steps(12, END) infinite; }', '.foo { animation: spin 1s steps(12, START) infinite; }', ].join('\n'), - expected: - [ - ':local(.foo) { animation: :local(spin) 1s steps(12, end) infinite; }', - ':local(.foo) { animation: :local(spin) 1s STEPS(12, start) infinite; }', - ':local(.foo) { animation: :local(spin) 1s steps(12, END) infinite; }', - ':local(.foo) { animation: :local(spin) 1s steps(12, START) infinite; }' - ].join('\n'), + expected: [ + ':local(.foo) { animation: :local(spin) 1s steps(12, end) infinite; }', + ':local(.foo) { animation: :local(spin) 1s STEPS(12, start) infinite; }', + ':local(.foo) { animation: :local(spin) 1s steps(12, END) infinite; }', + ':local(.foo) { animation: :local(spin) 1s steps(12, START) infinite; }', + ].join('\n'), }, { should: 'handle animations with custom timing functions', input: '.foo { animation: 1s normal cubic-bezier(0.25, 0.5, 0.5. 0.75) foo; }', expected: - ':local(.foo) { animation: 1s normal cubic-bezier(0.25, 0.5, 0.5. 0.75) :local(foo); }' + ':local(.foo) { animation: 1s normal cubic-bezier(0.25, 0.5, 0.5. 0.75) :local(foo); }', }, { should: 'handle animations whose names are keywords', input: '.foo { animation: 1s infinite infinite; }', - expected: ':local(.foo) { animation: 1s infinite :local(infinite); }' + expected: ':local(.foo) { animation: 1s infinite :local(infinite); }', }, { should: 'handle not localize an animation shorthand value of "inherit"', input: '.foo { animation: inherit; }', - expected: ':local(.foo) { animation: inherit; }' + expected: ':local(.foo) { animation: inherit; }', }, { should: 'handle "constructor" as animation name', input: '.foo { animation: constructor constructor; }', expected: - ':local(.foo) { animation: :local(constructor) :local(constructor); }' + ':local(.foo) { animation: :local(constructor) :local(constructor); }', }, { should: 'default to global when mode provided', input: '.foo {}', options: { mode: 'global' }, - expected: '.foo {}' + expected: '.foo {}', }, { should: 'default to local when mode provided', input: '.foo {}', options: { mode: 'local' }, - expected: ':local(.foo) {}' + expected: ':local(.foo) {}', }, { should: 'use correct spacing', @@ -262,7 +267,7 @@ const tests = [ ':local(.a).b {}', ':local( .a ).b {}', ':local(.a) .b {}', - ':local( .a ) .b {}' + ':local( .a ) .b {}', ].join('\n'), options: { mode: 'global' }, expected: [ @@ -275,147 +280,151 @@ const tests = [ ':local(.a).b {}', ':local(.a).b {}', ':local(.a) .b {}', - ':local(.a) .b {}' - ].join('\n') + ':local(.a) .b {}', + ].join('\n'), }, { should: 'localize keyframes', input: '@keyframes foo { from { color: red; } to { color: blue; } }', expected: - '@keyframes :local(foo) { from { color: red; } to { color: blue; } }' + '@keyframes :local(foo) { from { color: red; } to { color: blue; } }', }, { should: 'localize keyframes in global default mode', input: '@keyframes foo {}', options: { mode: 'global' }, - expected: '@keyframes foo {}' + expected: '@keyframes foo {}', }, { should: 'localize explicit keyframes', input: '@keyframes :local(foo) { 0% { color: red; } 33.3% { color: yellow; } 100% { color: blue; } } @-webkit-keyframes :global(bar) { from { color: red; } to { color: blue; } }', expected: - '@keyframes :local(foo) { 0% { color: red; } 33.3% { color: yellow; } 100% { color: blue; } } @-webkit-keyframes bar { from { color: red; } to { color: blue; } }' + '@keyframes :local(foo) { 0% { color: red; } 33.3% { color: yellow; } 100% { color: blue; } } @-webkit-keyframes bar { from { color: red; } to { color: blue; } }', }, { should: 'ignore :export statements', input: ':export { foo: __foo; }', - expected: ':export { foo: __foo; }' + expected: ':export { foo: __foo; }', }, { should: 'ignore :import statemtents', input: ':import("~/lol.css") { foo: __foo; }', - expected: ':import("~/lol.css") { foo: __foo; }' + expected: ':import("~/lol.css") { foo: __foo; }', + }, + { + should: 'incorrectly handle nested selectors', + input: '.bar:not(:global .foo, .baz) {}', + expected: ':local(.bar):not(.foo, .baz) {}', }, { should: 'compile in pure mode', input: ':global(.foo).bar, [type="radio"] ~ .label, :not(.foo), #bar {}', options: { mode: 'pure' }, expected: - '.foo:local(.bar), [type="radio"] ~ :local(.label), :not(:local(.foo)), :local(#bar) {}' + '.foo:local(.bar), [type="radio"] ~ :local(.label), :not(:local(.foo)), :local(#bar) {}', }, { should: 'compile explict global element', input: ':global(input) {}', - expected: 'input {}' + expected: 'input {}', }, { should: 'compile explict global attribute', input: ':global([type="radio"]), :not(:global [type="radio"]) {}', - expected: '[type="radio"], :not([type="radio"]) {}' + expected: '[type="radio"], :not([type="radio"]) {}', }, - { should: 'throw on invalid mode', input: '', options: { mode: '???' }, - error: /"global", "local" or "pure"/ + error: /"global", "local" or "pure"/, }, { should: 'throw on inconsistent selector result', input: ':global .foo, .bar {}', - error: /Inconsistent/ + error: /Inconsistent/, }, { should: 'throw on nested :locals', input: ':local(:local(.foo)) {}', - error: /is not allowed inside/ + error: /is not allowed inside/, }, { should: 'throw on nested :globals', input: ':global(:global(.foo)) {}', - error: /is not allowed inside/ + error: /is not allowed inside/, }, { should: 'throw on nested mixed', input: ':local(:global(.foo)) {}', - error: /is not allowed inside/ + error: /is not allowed inside/, }, { should: 'throw on nested broad :local', input: ':global(:local .foo) {}', - error: /is not allowed inside/ + error: /is not allowed inside/, }, { should: 'throw on incorrect spacing with broad :global', input: '.foo :global.bar {}', - error: /Missing whitespace after :global/ + error: /Missing whitespace after :global/, }, { should: 'throw on incorrect spacing with broad :local', input: '.foo:local .bar {}', - error: /Missing whitespace before :local/ + error: /Missing whitespace before :local/, }, { should: 'throw on not pure selector (global class)', input: ':global(.foo) {}', options: { mode: 'pure' }, - error: /":global\(\.foo\)" is not pure/ + error: /":global\(\.foo\)" is not pure/, }, { should: 'throw on not pure selector (with multiple 1)', input: '.foo, :global(.bar) {}', options: { mode: 'pure' }, - error: /".foo, :global\(\.bar\)" is not pure/ + error: /".foo, :global\(\.bar\)" is not pure/, }, { should: 'throw on not pure selector (with multiple 2)', input: ':global(.bar), .foo {}', options: { mode: 'pure' }, - error: /":global\(\.bar\), .foo" is not pure/ + error: /":global\(\.bar\), .foo" is not pure/, }, { should: 'throw on not pure selector (element)', input: 'input {}', options: { mode: 'pure' }, - error: /"input" is not pure/ + error: /"input" is not pure/, }, { should: 'throw on not pure selector (attribute)', input: '[type="radio"] {}', options: { mode: 'pure' }, - error: /"\[type="radio"\]" is not pure/ + error: /"\[type="radio"\]" is not pure/, }, { should: 'throw on not pure keyframes', input: '@keyframes :global(foo) {}', options: { mode: 'pure' }, - error: /@keyframes :global\(\.\.\.\) is not allowed in pure mode/ + error: /@keyframes :global\(\.\.\.\) is not allowed in pure mode/, }, { should: 'pass through global element', input: 'input {}', - expected: 'input {}' + expected: 'input {}', }, { should: 'localise class and pass through element', input: '.foo input {}', - expected: ':local(.foo) input {}' + expected: ':local(.foo) input {}', }, { should: 'pass through attribute selector', input: '[type="radio"] {}', - expected: '[type="radio"] {}' + expected: '[type="radio"] {}', }, { should: 'not modify urls without option', @@ -426,7 +435,7 @@ const tests = [ expected: ':local(.a) { background: url(./image.png); }\n' + '.b { background: url(image.png); }\n' + - ':local(.c) { background: url("./image.png"); }' + ':local(.c) { background: url("./image.png"); }', }, { should: 'rewrite url in local block', @@ -434,7 +443,7 @@ const tests = [ '.a { background: url(./image.png); }\n' + ':global .b { background: url(image.png); }\n' + '.c { background: url("./image.png"); }\n' + - '.c { background: url(\'./image.png\'); }\n' + + ".c { background: url('./image.png'); }\n" + '.d { background: -webkit-image-set(url("./image.png") 1x, url("./image2x.png") 2x); }\n' + '@font-face { src: url("./font.woff"); }\n' + '@-webkit-font-face { src: url("./font.woff"); }\n' + @@ -446,7 +455,7 @@ const tests = [ rewriteUrl: function(global, url) { const mode = global ? 'global' : 'local'; return '(' + mode + ')' + url + '"' + mode + '"'; - } + }, }, expected: ':local(.a) { background: url((local\\)./image.png\\"local\\"); }\n' + @@ -464,7 +473,7 @@ const tests = [ { should: 'not crash on atrule without nodes', input: '@charset "utf-8";', - expected: '@charset "utf-8";' + expected: '@charset "utf-8";', }, { should: 'not crash on a rule without nodes', @@ -476,32 +485,32 @@ const tests = [ return root; })(), // postcss-less's stringify would honor `ruleWithoutBody` and omit the trailing `{}` - expected: ':local(.a) {\n :local(.b) {}\n}' + expected: ':local(.a) {\n :local(.b) {}\n}', }, { should: 'not break unicode characters', input: '.a { content: "\\2193" }', - expected: ':local(.a) { content: "\\2193" }' + expected: ':local(.a) { content: "\\2193" }', }, { should: 'not break unicode characters', input: '.a { content: "\\2193\\2193" }', - expected: ':local(.a) { content: "\\2193\\2193" }' + expected: ':local(.a) { content: "\\2193\\2193" }', }, { should: 'not break unicode characters', input: '.a { content: "\\2193 \\2193" }', - expected: ':local(.a) { content: "\\2193 \\2193" }' + expected: ':local(.a) { content: "\\2193 \\2193" }', }, { should: 'not break unicode characters', input: '.a { content: "\\2193\\2193\\2193" }', - expected: ':local(.a) { content: "\\2193\\2193\\2193" }' + expected: ':local(.a) { content: "\\2193\\2193\\2193" }', }, { should: 'not break unicode characters', input: '.a { content: "\\2193 \\2193 \\2193" }', - expected: ':local(.a) { content: "\\2193 \\2193 \\2193" }' + expected: ':local(.a) { content: "\\2193 \\2193 \\2193" }', }, ]; diff --git a/yarn.lock b/yarn.lock index d093fff..618ad53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -537,6 +537,11 @@ cssesc@^0.1.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" integrity sha1-yBSQPkViM3GgR3tAEJqq++6t27Q= +cssesc@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" + integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== + ctype@0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/ctype/-/ctype-0.5.3.tgz#82c18c2461f74114ef16c135224ad0b9144ca12f" @@ -1259,6 +1264,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -2084,6 +2094,15 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +postcss-selector-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" + integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== + dependencies: + cssesc "^2.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + postcss-value-parser@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" @@ -2814,6 +2833,11 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^0.4.3" +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"