From f7e11759e98eaa9d9616bc94920648639628fd04 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Mon, 18 Feb 2019 09:16:51 -0500 Subject: [PATCH 1/5] WIP --- package.json | 1 + yarn.lock | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/package.json b/package.json index 35da4b5..d2e30da 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "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/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" From 2e8728079e78ad5b9b25a8ed51c3ec51235f14cc Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Mon, 18 Feb 2019 14:21:49 -0500 Subject: [PATCH 2/5] WIP --- .eslintrc | 2 +- index.js | 356 +++++++++++-------- package.json | 5 + test.js | 988 ++++++++++++++++++++++++++------------------------- 4 files changed, 708 insertions(+), 643 deletions(-) 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..6ff2133 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 IS_GLOBAL = Symbol('is global'); + function normalizeNodeArray(nodes) { const array = []; + nodes.forEach(function(x) { if (Array.isArray(x)) { normalizeNodeArray(x).forEach(function(item) { @@ -22,144 +25,196 @@ function normalizeNodeArray(nodes) { 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 checkForInconsistentRule(node, current, context) { + if (context.isGlobal !== context.lastIsGlobal) + throw new Error( + 'Inconsistent rule global/local result in rule "' + + String(node) + + '" (multiple selectors must result in the same mode for the rule)' + ); +} - 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; +function trimSelectors(selector) { + let last; + while ( + (last = selector.last) && + (last.type === 'combinator' && last.value === ' ') + ) { + last.remove(); + } +} - case 'selector': - newNodes = node.nodes.map(function(n) { - return localizeNode(n, context); - }); - node = Object.create(node); - node.nodes = normalizeNodeArray(newNodes); - break; +function localizeNodez(rule, mode, options) { + console.log(mode); + const isScopePseudo = node => + node.value === ':local' || node.value === ':global'; + + const transform = (node, context) => { + switch (node.type) { + case 'root': { + const childContext = { ...context, hasLocals: false }; + let overallIsGlobal; + + node.each(childNode => { + // isGlobal should not carry over across selectors: + // `:global .foo, .bar -> .foo, :local(.bar)` + childContext.isGlobal = context.isGlobal; + + transform(childNode, childContext); + + if (overallIsGlobal == null) { + overallIsGlobal = childContext.isGlobal; + } else { + if (overallIsGlobal !== childContext.isGlobal) + throw new Error( + 'Inconsistent rule global/local result in rule "' + + String(node) + + '" (multiple selectors must result in the same mode for the rule)' + ); + } + console.log(childContext.hasLocals); + if (childContext.hasLocals) { + context.hasPureGlobals = false; + } + }); - case 'spacing': - if (context.ignoreNextSpacing) { - context.ignoreNextSpacing = false; - context.lastWasSpacing = false; - context.enforceNoSpacing = false; - return null; + break; } - context.lastWasSpacing = true; - return node; + case 'selector': { + // const childContext = { ...context, hasLocals: false }; - 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 + - '(...)' - ); + node.each(childNode => transform(childNode, context)); + + // context.isGlobal = childContext.isGlobal; + + trimSelectors(node); + + break; + } + case 'combinator': { + if ( + node.value === ' ' && + (context.shouldTrimTrainingWhitespace || !node.next()) + ) { + //console.log('COMBIN', node.spaces); + context.shouldTrimTrainingWhitespace = false; + node.remove(); } - 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': { + if (!isScopePseudo(node)) { + // const childContext = { + // ...context, + // hasLocals: false, + // }; + node.each(childNode => transform(childNode, context)); + + // if (childContext.hasLocals) context.hasLocals = true; + break; + } - 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 + - '(...)' + `A ${node.value} is not allowed inside of a ${context.inside}(...)` ); } - 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, + + const isGlobal = node.value === ':global'; + + const isNested = !!node.length; + if (!isNested) { + context.isGlobal = isGlobal; + context.shouldTrimTrainingWhitespace = !node.spaces.before; + // console.log(node.spaces); + node.remove(); + return null; + } + + const childContext = { + ...context, + isGlobal, + inside: node.value, hasLocals: false, - explicit: context.explicit }; - newNodes = node.nodes.map(function(n) { - return localizeNode(n, subContext); + + // The nodes of a psuedo will be Selectors, which we want to flatten + // into the parent + const nodes = node + .clone() + .map(childNode => transform(childNode, childContext)) + .reduce( + (acc, next) => + acc.concat(next.type === 'selector' ? next.nodes : next), + [] + ); + + // console.log('asfasfasf', nodes); + + if (nodes.length) { + const { before, after } = node.spaces; + + const first = nodes[0]; + const last = nodes[node.length - 1]; + + first.spaces = { before, after: first.spaces.after }; + last.spaces = { before: last.spaces.before, after }; + } + nodes.forEach(childNode => { + node.parent.insertBefore(node, childNode); }); - node = Object.create(node); - node.nodes = normalizeNodeArray(newNodes); + node.remove(); + // const parent = node.parent; + // node.replaceWith(nodes[0].nodes); + // console.log('asfasfasf', nodes, String(parent.parent)); + return nodes; } - if (subContext.hasLocals) { - context.hasLocals = true; - } - break; + case 'id': + case 'class': { + const spaces = { ...node.spaces }; + + if (context.shouldTrimTrainingWhitespace) { + // console.log('HEREEEEEE', spaces); + spaces.before = ''; + context.shouldTrimTrainingWhitespace = false; + } - case 'id': - case 'class': - if (!context.global) { - node = { - type: 'nested-pseudo-class', - name: 'local', - nodes: [node] - }; - context.hasLocals = true; + if (!context.isGlobal) { + // console.log('REPLCE', node.spaces); + const innerNode = node.clone(); + innerNode.spaces = { before: '', after: '' }; + + // console.log(node); + node.replaceWith( + selectorParser.pseudo({ + value: ':local', + nodes: [innerNode], + spaces, + }) + ); + console.log('HERE'); + context.hasLocals = true; + } else { + //node.spaces = spaces; + } + break; } - break; - } + } + return node; + }; - // reset context - context.lastWasSpacing = false; - context.ignoreNextSpacing = false; - context.enforceNoSpacing = false; - return node; + const isGlobal = mode === 'global'; + const rootContext = { + isGlobal, + hasPureGlobals: true, + }; + + const updatedRule = selectorParser(root => { + transform(root, rootContext); + }).processSync(rule, { updateSelector: true, lossless: true }); + + // console.log('HERE', rule.selector); + return rootContext; } function localizeDeclNode(node, context) { @@ -172,22 +227,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.isGlobal, + 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 +268,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 +310,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 = {}; @@ -283,8 +348,8 @@ function localizeAnimationShorthandDeclValues(decl, context) { const subContext = { options: context.options, - global: context.global, - localizeNextItem: shouldParseAnimationName && !context.global + isGlobal: context.isGlobal, + localizeNextItem: shouldParseAnimationName && !context.isGlobal, }; return localizeDeclNode(node, subContext); }); @@ -297,8 +362,8 @@ function localizeDeclValues(localize, decl, context) { valueNodes.walk((node, index, nodes) => { const subContext = { options: context.options, - global: context.global, - localizeNextItem: localize && !context.global + isGlobal: context.isGlobal, + localizeNextItem: localize && !context.isGlobal, }; nodes[index] = localizeDeclNode(node, subContext); }); @@ -367,7 +432,7 @@ module.exports = postcss.plugin('postcss-modules-local-by-default', function( atrule.walkDecls(function(decl) { localizeDecl(decl, { options: options, - global: globalKeyframes + isGlobal: globalKeyframes, }); }); } else if (atrule.nodes) { @@ -375,7 +440,7 @@ module.exports = postcss.plugin('postcss-modules-local-by-default', function( if (decl.type === 'decl') { localizeDecl(decl, { options: options, - global: globalMode + isGlobal: globalMode, }); } }); @@ -389,22 +454,13 @@ 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 = localizeNodez(rule, options.mode, 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)' ); @@ -412,10 +468,10 @@ module.exports = postcss.plugin('postcss-modules-local-by-default', function( // Less-syntax mixins parse as rules with no nodes if (rule.nodes) { rule.nodes.forEach(function(decl) { + console.log(context); localizeDecl(decl, context); }); } - rule.selector = Tokenizer.stringify(newSelector); }); }; }); diff --git a/package.json b/package.json index d2e30da..7ae0368 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,11 @@ "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", diff --git a/test.js b/test.js index d83090d..c39a7fe 100644 --- a/test.js +++ b/test.js @@ -6,503 +6,507 @@ const plugin = require('./'); const name = require('./package.json').name; const tests = [ - { - should: 'scope selectors', - input: '.foobar {}', - expected: ':local(.foobar) {}' - }, - { - should: 'scope ids', - input: '#foobar {}', - expected: ':local(#foobar) {}' - }, - { - should: 'scope multiple selectors', - input: '.foo, .baz {}', - expected: ':local(.foo), :local(.baz) {}' - }, - { - should: 'scope sibling selectors', - input: '.foo ~ .baz {}', - expected: ':local(.foo) ~ :local(.baz) {}' - }, - { - should: 'scope psuedo elements', - input: '.foo:after {}', - expected: ':local(.foo):after {}' - }, - { - should: 'scope media queries', - input: '@media only screen { .foo {} }', - expected: '@media only screen { :local(.foo) {} }' - }, - { - should: 'allow narrow global selectors', - input: ':global(.foo .bar) {}', - expected: '.foo .bar {}' - }, - { - should: 'allow narrow local selectors', - input: ':local(.foo .bar) {}', - expected: ':local(.foo) :local(.bar) {}' - }, - { - should: 'allow broad global selectors', - input: ':global .foo .bar {}', - expected: '.foo .bar {}' - }, - { - should: 'allow broad local selectors', - input: ':local .foo .bar {}', - expected: ':local(.foo) :local(.bar) {}' - }, - { - should: 'allow multiple narrow global selectors', - input: ':global(.foo), :global(.bar) {}', - expected: '.foo, .bar {}' - }, - { - should: 'allow multiple broad global selectors', - input: ':global .foo, :global .bar {}', - expected: '.foo, .bar {}' - }, - { - should: 'allow multiple broad local selectors', - input: ':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 {}' - }, - { - should: 'allow broad global selectors nested inside local styles', - input: '.foo :global .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) {}' - }, - { - should: 'allow parentheses inside narrow local selectors', - input: '.foo :local(.foo:not(.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 {}' - }, - { - should: 'ignore selectors that are already local', - input: ':local(.foobar) {}', - expected: ':local(.foobar) {}' - }, - { - should: 'ignore nested selectors that are already local', - input: ':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) {}' - }, - { - should: 'ignore sibling selectors that are already local', - input: ':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 {}' - }, - { - should: 'broad global should be limited to selector', - input: ':global .foo, .bar :global, .foobar :global {}', - 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) {}' - }, - { - should: 'broad global and local should allow switching', - input: '.foo :global .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); }' - }, - { - should: 'not localize a single animation-delay', - input: '.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); }' - }, - { - should: 'localize animation', - input: '.foo { animation: bar 5s, 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); }' - }, - { - should: 'not localize other rules', - input: '.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; }' - }, - { - 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; }' - }, - { - should: 'handle animations where the first value is not the animation name', - input: '.foo { animation: 1s 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); }' - }, - { - 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; }', - expected: - ':local(.foo) { animation: :local(slide-right) 300ms forwards ease-out, :local(fade-in) 300ms forwards ease-out; }' - }, - { - should: - 'not treat "start" and "end" keywords in steps() function as identifiers', - input: [ - '.foo { animation: spin 1s steps(12, end) infinite; }', - '.foo { animation: spin 1s STEPS(12, start) infinite; }', - '.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'), - }, - { - 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); }' - }, - { - should: 'handle animations whose names are keywords', - input: '.foo { animation: 1s infinite 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; }' - }, - { - should: 'handle "constructor" as animation name', - input: '.foo { animation: constructor constructor; }', - expected: - ':local(.foo) { animation: :local(constructor) :local(constructor); }' - }, - { - should: 'default to global when mode provided', - input: '.foo {}', - options: { mode: 'global' }, - expected: '.foo {}' - }, - { - should: 'default to local when mode provided', - input: '.foo {}', - options: { mode: 'local' }, - expected: ':local(.foo) {}' - }, - { - should: 'use correct spacing', - input: [ - '.a :local .b {}', - '.a:local.b {}', - '.a:local(.b) {}', - '.a:local( .b ) {}', - '.a :local(.b) {}', - '.a :local( .b ) {}', - ':local(.a).b {}', - ':local( .a ).b {}', - ':local(.a) .b {}', - ':local( .a ) .b {}' - ].join('\n'), - options: { mode: 'global' }, - expected: [ - '.a :local(.b) {}', - '.a:local(.b) {}', - '.a:local(.b) {}', - '.a:local(.b) {}', - '.a :local(.b) {}', - '.a :local(.b) {}', - ':local(.a).b {}', - ':local(.a).b {}', - ':local(.a) .b {}', - ':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; } }' - }, - { - should: 'localize keyframes in global default mode', - input: '@keyframes foo {}', - options: { mode: 'global' }, - 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; } }' - }, - { - should: 'ignore :export statements', - input: ':export { foo: __foo; }', - expected: ':export { foo: __foo; }' - }, - { - should: 'ignore :import statemtents', - input: ':import("~/lol.css") { foo: __foo; }', - expected: ':import("~/lol.css") { foo: __foo; }' - }, + // { + // should: 'scope selectors', + // input: '.foobar {}', + // expected: ':local(.foobar) {}', + // }, + // { + // should: 'scope ids', + // input: '#foobar {}', + // expected: ':local(#foobar) {}', + // }, + // { + // should: 'scope multiple selectors', + // input: '.foo, .baz {}', + // expected: ':local(.foo), :local(.baz) {}', + // }, + // { + // should: 'scope sibling selectors', + // input: '.foo ~ .baz {}', + // expected: ':local(.foo) ~ :local(.baz) {}', + // }, + // { + // should: 'scope psuedo elements', + // input: '.foo:after {}', + // expected: ':local(.foo):after {}', + // }, + // { + // should: 'scope media queries', + // input: '@media only screen { .foo {} }', + // expected: '@media only screen { :local(.foo) {} }', + // }, + // { + // should: 'allow narrow global selectors', + // input: ':global(.foo .bar) {}', + // expected: '.foo .bar {}', + // }, + // { + // should: 'allow narrow local selectors', + // input: ':local(.foo .bar) {}', + // expected: ':local(.foo) :local(.bar) {}', + // }, + // { + // should: 'allow broad global selectors', + // input: ':global .foo .bar {}', + // expected: '.foo .bar {}', + // }, + // { + // should: 'allow broad local selectors', + // input: ':local .foo .bar {}', + // expected: ':local(.foo) :local(.bar) {}', + // }, + // { + // should: 'allow multiple narrow global selectors', + // input: ':global(.foo), :global(.bar) {}', + // expected: '.foo, .bar {}', + // }, + // { + // should: 'allow multiple broad global selectors', + // input: ':global .foo, :global .bar {}', + // expected: '.foo, .bar {}', + // }, + // { + // should: 'allow multiple broad local selectors', + // input: ':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 {}', + // }, + // { + // should: 'allow broad global selectors nested inside local styles', + // input: '.foo :global .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) {}', + // }, + // { + // should: 'allow parentheses inside narrow local selectors', + // input: '.foo :local(.foo:not(.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 {}', + // }, + // { + // should: 'ignore selectors that are already local', + // input: ':local(.foobar) {}', + // expected: ':local(.foobar) {}', + // }, + // { + // should: 'ignore nested selectors that are already local', + // input: ':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) {}' + // }, + // { + // should: 'ignore sibling selectors that are already local', + // input: ':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 {}' + // }, + // { + // 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) {}', + // }, + // { + // should: 'broad global should be limited to nested selector', + // input: '.foo:not(:global .bar).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) {}', + // }, + // { + // should: 'localize a single animation-name', + // input: '.foo { animation-name: 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; }' + // }, + // { + // should: 'localize multiple animation-names', + // input: '.foo { animation-name: bar, 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); }' + // }, + // { + // should: 'localize animation with vendor prefix', + // input: '.foo { -webkit-animation: bar; animation: bar; }', + // expected: + // ':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;" }' + // }, + // { + // should: 'not localize global rules', + // input: ':global .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; }' + // }, + // { + // should: 'handle animations where the first value is not the animation name', + // input: '.foo { animation: 1s 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); }' + // }, + // { + // 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; }', + // expected: + // ':local(.foo) { animation: :local(slide-right) 300ms forwards ease-out, :local(fade-in) 300ms forwards ease-out; }' + // }, + // { + // should: + // 'not treat "start" and "end" keywords in steps() function as identifiers', + // input: [ + // '.foo { animation: spin 1s steps(12, end) infinite; }', + // '.foo { animation: spin 1s STEPS(12, start) infinite; }', + // '.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'), + // }, + // { + // 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); }' + // }, + // { + // should: 'handle animations whose names are keywords', + // input: '.foo { animation: 1s infinite 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; }' + // }, + // { + // should: 'handle "constructor" as animation name', + // input: '.foo { animation: constructor constructor; }', + // expected: + // ':local(.foo) { animation: :local(constructor) :local(constructor); }' + // }, + // { + // should: 'default to global when mode provided', + // input: '.foo {}', + // options: { mode: 'global' }, + // expected: '.foo {}', + // }, + // { + // should: 'default to local when mode provided', + // input: '.foo {}', + // options: { mode: 'local' }, + // expected: ':local(.foo) {}', + // }, + // { + // should: 'use correct spacing', + // input: [ + // '.a :local .b {}', + // '.a:local.b {}', + // '.a:local(.b) {}', + // '.a:local( .b ) {}', + // '.a :local(.b) {}', + // '.a :local( .b ) {}', + // ':local(.a).b {}', + // ':local( .a ).b {}', + // ':local(.a) .b {}', + // ':local( .a ) .b {}', + // ].join('\n'), + // options: { mode: 'global' }, + // expected: [ + // '.a :local(.b) {}', + // '.a:local(.b) {}', + // '.a:local(.b) {}', + // '.a:local(.b) {}', + // '.a :local(.b) {}', + // '.a :local(.b) {}', + // ':local(.a).b {}', + // ':local(.a).b {}', + // ':local(.a) .b {}', + // ':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; } }', + // }, + // { + // should: 'localize keyframes in global default mode', + // input: '@keyframes foo {}', + // options: { mode: 'global' }, + // 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; } }' + // }, + // { + // should: 'ignore :export statements', + // input: ':export { foo: __foo; }', + // expected: ':export { foo: __foo; }' + // }, + // { + // should: 'ignore :import statemtents', + // input: ':import("~/lol.css") { foo: __foo; }', + // expected: ':import("~/lol.css") { foo: __foo; }' + // }, { 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) {}' - }, - { - should: 'compile explict global element', - input: ':global(input) {}', - expected: 'input {}' - }, - { - should: 'compile explict global attribute', - input: ':global([type="radio"]), :not(:global [type="radio"]) {}', - expected: '[type="radio"], :not([type="radio"]) {}' - }, - - { - should: 'throw on invalid mode', - input: '', - options: { mode: '???' }, - error: /"global", "local" or "pure"/ - }, - { - should: 'throw on inconsistent selector result', - input: ':global .foo, .bar {}', - error: /Inconsistent/ - }, - { - should: 'throw on nested :locals', - input: ':local(:local(.foo)) {}', - error: /is not allowed inside/ - }, - { - should: 'throw on nested :globals', - input: ':global(:global(.foo)) {}', - error: /is not allowed inside/ - }, - { - should: 'throw on nested mixed', - input: ':local(:global(.foo)) {}', - error: /is not allowed inside/ - }, - { - should: 'throw on nested broad :local', - input: ':global(:local .foo) {}', - error: /is not allowed inside/ - }, - { - should: 'throw on incorrect spacing with broad :global', - input: '.foo :global.bar {}', - error: /Missing whitespace after :global/ - }, - { - should: 'throw on incorrect spacing with broad :local', - input: '.foo:local .bar {}', - 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/ - }, - { - should: 'throw on not pure selector (with multiple 1)', - input: '.foo, :global(.bar) {}', - options: { mode: '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/ - }, - { - should: 'throw on not pure selector (element)', - input: 'input {}', - options: { mode: '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/ - }, - { - should: 'throw on not pure keyframes', - input: '@keyframes :global(foo) {}', - options: { mode: 'pure' }, - error: /@keyframes :global\(\.\.\.\) is not allowed in pure mode/ - }, - { - should: 'pass through global element', - input: 'input {}', - expected: 'input {}' - }, - { - should: 'localise class and pass through element', - input: '.foo input {}', - expected: ':local(.foo) input {}' - }, - { - should: 'pass through attribute selector', - input: '[type="radio"] {}', - expected: '[type="radio"] {}' - }, - { - should: 'not modify urls without option', - input: - '.a { background: url(./image.png); }\n' + - ':global .b { background: url(image.png); }\n' + - '.c { background: url("./image.png"); }', - expected: - ':local(.a) { background: url(./image.png); }\n' + - '.b { background: url(image.png); }\n' + - ':local(.c) { background: url("./image.png"); }' - }, - { - should: 'rewrite url in local block', - input: - '.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' + - '.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' + - '@media screen { .a { src: url("./image.png"); } }\n' + - '@keyframes :global(ani1) { 0% { src: url("image.png"); } }\n' + - '@keyframes ani2 { 0% { src: url("./image.png"); } }\n' + - 'foo { background: end-with-url(something); }', - options: { - rewriteUrl: function(global, url) { - const mode = global ? 'global' : 'local'; - return '(' + mode + ')' + url + '"' + mode + '"'; - } - }, - expected: - ':local(.a) { background: url((local\\)./image.png\\"local\\"); }\n' + - '.b { background: url((global\\)image.png\\"global\\"); }\n' + - ':local(.c) { background: url("(local)./image.png\\"local\\""); }\n' + - ':local(.c) { background: url(\'(local)./image.png"local"\'); }\n' + - ':local(.d) { background: -webkit-image-set(url("(local)./image.png\\"local\\"") 1x, url("(local)./image2x.png\\"local\\"") 2x); }\n' + - '@font-face { src: url("(local)./font.woff\\"local\\""); }\n' + - '@-webkit-font-face { src: url("(local)./font.woff\\"local\\""); }\n' + - '@media screen { :local(.a) { src: url("(local)./image.png\\"local\\""); } }\n' + - '@keyframes ani1 { 0% { src: url("(global)image.png\\"global\\""); } }\n' + - '@keyframes :local(ani2) { 0% { src: url("(local)./image.png\\"local\\""); } }\n' + - 'foo { background: end-with-url(something); }', - }, - { - should: 'not crash on atrule without nodes', - input: '@charset "utf-8";', - expected: '@charset "utf-8";' - }, - { - should: 'not crash on a rule without nodes', - input: (function() { - const inner = postcss.rule({ selector: '.b', ruleWithoutBody: true }); - const outer = postcss.rule({ selector: '.a' }).push(inner); - const root = postcss.root().push(outer); - inner.nodes = undefined; - return root; - })(), - // postcss-less's stringify would honor `ruleWithoutBody` and omit the trailing `{}` - expected: ':local(.a) {\n :local(.b) {}\n}' - }, - { - should: 'not break unicode characters', - input: '.a { content: "\\2193" }', - expected: ':local(.a) { content: "\\2193" }' - }, - { - should: 'not break unicode characters', - input: '.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" }' - }, - { - should: 'not break unicode characters', - input: '.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" }' - }, + '.foo:local(.bar), [type="radio"] ~ :local(.label), :not(:local(.foo)), :local(#bar) {}', + }, + // { + // should: 'compile explict global element', + // input: ':global(input) {}', + // expected: 'input {}' + // }, + // { + // should: 'compile explict global attribute', + // input: ':global([type="radio"]), :not(:global [type="radio"]) {}', + // expected: '[type="radio"], :not([type="radio"]) {}' + // }, + // { + // should: 'throw on invalid mode', + // input: '', + // options: { mode: '???' }, + // error: /"global", "local" or "pure"/, + // }, + // { + // should: 'throw on inconsistent selector result', + // input: ':global .foo, .bar {}', + // error: /Inconsistent/, + // }, + // { + // should: 'throw on nested :locals', + // input: ':local(:local(.foo)) {}', + // error: /is not allowed inside/ + // }, + // { + // should: 'throw on nested :globals', + // input: ':global(:global(.foo)) {}', + // error: /is not allowed inside/ + // }, + // { + // should: 'throw on nested mixed', + // input: ':local(:global(.foo)) {}', + // error: /is not allowed inside/ + // }, + // { + // should: 'throw on nested broad :local', + // input: ':global(:local .foo) {}', + // error: /is not allowed inside/ + // }, + // { + // should: 'throw on incorrect spacing with broad :global', + // input: '.foo :global.bar {}', + // error: /Missing whitespace after :global/ + // }, + // { + // should: 'throw on incorrect spacing with broad :local', + // input: '.foo:local .bar {}', + // 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/ + // }, + // { + // should: 'throw on not pure selector (with multiple 1)', + // input: '.foo, :global(.bar) {}', + // options: { mode: '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/ + // }, + // { + // should: 'throw on not pure selector (element)', + // input: 'input {}', + // options: { mode: '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/ + // }, + // { + // should: 'throw on not pure keyframes', + // input: '@keyframes :global(foo) {}', + // options: { mode: 'pure' }, + // error: /@keyframes :global\(\.\.\.\) is not allowed in pure mode/ + // }, + // { + // should: 'pass through global element', + // input: 'input {}', + // expected: 'input {}' + // }, + // { + // should: 'localise class and pass through element', + // input: '.foo input {}', + // expected: ':local(.foo) input {}' + // }, + // { + // should: 'pass through attribute selector', + // input: '[type="radio"] {}', + // expected: '[type="radio"] {}' + // }, + // { + // should: 'not modify urls without option', + // input: + // '.a { background: url(./image.png); }\n' + + // ':global .b { background: url(image.png); }\n' + + // '.c { background: url("./image.png"); }', + // expected: + // ':local(.a) { background: url(./image.png); }\n' + + // '.b { background: url(image.png); }\n' + + // ':local(.c) { background: url("./image.png"); }' + // }, + // { + // should: 'rewrite url in local block', + // input: + // '.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' + + // '.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' + + // '@media screen { .a { src: url("./image.png"); } }\n' + + // '@keyframes :global(ani1) { 0% { src: url("image.png"); } }\n' + + // '@keyframes ani2 { 0% { src: url("./image.png"); } }\n' + + // 'foo { background: end-with-url(something); }', + // options: { + // rewriteUrl: function(global, url) { + // const mode = global ? 'global' : 'local'; + // return '(' + mode + ')' + url + '"' + mode + '"'; + // } + // }, + // expected: + // ':local(.a) { background: url((local\\)./image.png\\"local\\"); }\n' + + // '.b { background: url((global\\)image.png\\"global\\"); }\n' + + // ':local(.c) { background: url("(local)./image.png\\"local\\""); }\n' + + // ':local(.c) { background: url(\'(local)./image.png"local"\'); }\n' + + // ':local(.d) { background: -webkit-image-set(url("(local)./image.png\\"local\\"") 1x, url("(local)./image2x.png\\"local\\"") 2x); }\n' + + // '@font-face { src: url("(local)./font.woff\\"local\\""); }\n' + + // '@-webkit-font-face { src: url("(local)./font.woff\\"local\\""); }\n' + + // '@media screen { :local(.a) { src: url("(local)./image.png\\"local\\""); } }\n' + + // '@keyframes ani1 { 0% { src: url("(global)image.png\\"global\\""); } }\n' + + // '@keyframes :local(ani2) { 0% { src: url("(local)./image.png\\"local\\""); } }\n' + + // 'foo { background: end-with-url(something); }', + // }, + // { + // should: 'not crash on atrule without nodes', + // input: '@charset "utf-8";', + // expected: '@charset "utf-8";' + // }, + // { + // should: 'not crash on a rule without nodes', + // input: (function() { + // const inner = postcss.rule({ selector: '.b', ruleWithoutBody: true }); + // const outer = postcss.rule({ selector: '.a' }).push(inner); + // const root = postcss.root().push(outer); + // inner.nodes = undefined; + // return root; + // })(), + // // postcss-less's stringify would honor `ruleWithoutBody` and omit the trailing `{}` + // expected: ':local(.a) {\n :local(.b) {}\n}' + // }, + // { + // should: 'not break unicode characters', + // input: '.a { content: "\\2193" }', + // expected: ':local(.a) { content: "\\2193" }' + // }, + // { + // should: 'not break unicode characters', + // input: '.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" }' + // }, + // { + // should: 'not break unicode characters', + // input: '.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" }' + // }, ]; function process(css, options) { From 555dc1a4b5cd9ce10ea61397042d3819206b65f3 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Mon, 18 Feb 2019 14:56:30 -0500 Subject: [PATCH 3/5] WIP --- index.js | 39 ++++++++++++++------------- test.js | 81 ++++++++++++++++++++++++++++++-------------------------- 2 files changed, 63 insertions(+), 57 deletions(-) diff --git a/index.js b/index.js index 6ff2133..daece4a 100644 --- a/index.js +++ b/index.js @@ -45,23 +45,26 @@ function trimSelectors(selector) { } function localizeNodez(rule, mode, options) { - console.log(mode); + // console.log(mode); const isScopePseudo = node => node.value === ':local' || node.value === ':global'; const transform = (node, context) => { switch (node.type) { case 'root': { - const childContext = { ...context, hasLocals: false }; + const childContext = { ...context }; let overallIsGlobal; node.each(childNode => { - // isGlobal should not carry over across selectors: + // isGlobal and hasLocals should not carry over across selectors: // `:global .foo, .bar -> .foo, :local(.bar)` childContext.isGlobal = context.isGlobal; + childContext.hasLocals = false; transform(childNode, childContext); + console.log(overallIsGlobal, childContext); + if (overallIsGlobal == null) { overallIsGlobal = childContext.isGlobal; } else { @@ -72,7 +75,7 @@ function localizeNodez(rule, mode, options) { '" (multiple selectors must result in the same mode for the rule)' ); } - console.log(childContext.hasLocals); + if (childContext.hasLocals) { context.hasPureGlobals = false; } @@ -81,12 +84,8 @@ function localizeNodez(rule, mode, options) { break; } case 'selector': { - // const childContext = { ...context, hasLocals: false }; - node.each(childNode => transform(childNode, context)); - // context.isGlobal = childContext.isGlobal; - trimSelectors(node); break; @@ -104,13 +103,11 @@ function localizeNodez(rule, mode, options) { } case 'pseudo': { if (!isScopePseudo(node)) { - // const childContext = { - // ...context, - // hasLocals: false, - // }; + // This needs to not update `isGlobal` for tests to pass + // the behavior seems _wrong_ tho. + const childContext = { ...context }; + console.log('PSEUEDO', node.value, context); node.each(childNode => transform(childNode, context)); - - // if (childContext.hasLocals) context.hasLocals = true; break; } @@ -120,11 +117,11 @@ function localizeNodez(rule, mode, options) { ); } - const isGlobal = node.value === ':global'; - const isNested = !!node.length; + const isGlobal = node.value === ':global'; if (!isNested) { context.isGlobal = isGlobal; + context.shouldTrimTrainingWhitespace = !node.spaces.before; // console.log(node.spaces); node.remove(); @@ -148,6 +145,10 @@ function localizeNodez(rule, mode, options) { acc.concat(next.type === 'selector' ? next.nodes : next), [] ); + // console.log(context); + if (childContext.hasLocals) { + context.hasLocals = true; + } // console.log('asfasfasf', nodes); @@ -160,6 +161,7 @@ function localizeNodez(rule, mode, options) { first.spaces = { before, after: first.spaces.after }; last.spaces = { before: last.spaces.before, after }; } + nodes.forEach(childNode => { node.parent.insertBefore(node, childNode); }); @@ -192,11 +194,10 @@ function localizeNodez(rule, mode, options) { spaces, }) ); - console.log('HERE'); + // console.log('HERE'); context.hasLocals = true; - } else { - //node.spaces = spaces; } + break; } } diff --git a/test.js b/test.js index c39a7fe..454cd0d 100644 --- a/test.js +++ b/test.js @@ -312,23 +312,28 @@ const tests = [ // input: ':import("~/lol.css") { foo: __foo; }', // expected: ':import("~/lol.css") { foo: __foo; }' // }, - { - 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) {}', - }, // { - // should: 'compile explict global element', - // input: ':global(input) {}', - // expected: 'input {}' + // 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) {}', // }, // { - // should: 'compile explict global attribute', - // input: ':global([type="radio"]), :not(:global [type="radio"]) {}', - // expected: '[type="radio"], :not([type="radio"]) {}' + // should: 'compile explict global element', + // input: ':global(input) {}', + // expected: 'input {}', // }, + { + should: 'compile explict global attribute', + input: ':global([type="radio"]), :not(:global [type="radio"]) {}', + expected: '[type="radio"], :not([type="radio"]) {}', + }, // { // should: 'throw on invalid mode', // input: '', @@ -343,83 +348,83 @@ const tests = [ // { // 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', @@ -430,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', @@ -438,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' + @@ -450,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' + @@ -468,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', @@ -480,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" }', // }, ]; From dd5bcf6a0b97666c329f57d56e19a8a9a4377f20 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Mon, 18 Feb 2019 15:46:25 -0500 Subject: [PATCH 4/5] WIP --- index.js | 277 ++++++++++++++++++++++++++++++------------------------- test.js | 20 ++-- 2 files changed, 160 insertions(+), 137 deletions(-) diff --git a/index.js b/index.js index daece4a..04a2420 100644 --- a/index.js +++ b/index.js @@ -19,20 +19,21 @@ 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 checkForInconsistentRule(node, current, context) { - if (context.isGlobal !== context.lastIsGlobal) + if (context.global !== context.lastIsGlobal) throw new Error( 'Inconsistent rule global/local result in rule "' + String(node) + '" (multiple selectors must result in the same mode for the rule)' ); } +const isSpacing = node => node.type === 'combinator' && node.value === ' '; function trimSelectors(selector) { let last; @@ -45,155 +46,174 @@ function trimSelectors(selector) { } function localizeNodez(rule, mode, options) { - // console.log(mode); const isScopePseudo = node => node.value === ':local' || node.value === ':global'; 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); + } + + let newNodes; switch (node.type) { case 'root': { - const childContext = { ...context }; - let overallIsGlobal; - - node.each(childNode => { - // isGlobal and hasLocals should not carry over across selectors: - // `:global .foo, .bar -> .foo, :local(.bar)` - childContext.isGlobal = context.isGlobal; - childContext.hasLocals = false; - - transform(childNode, childContext); - - console.log(overallIsGlobal, childContext); - - if (overallIsGlobal == null) { - overallIsGlobal = childContext.isGlobal; - } else { - if (overallIsGlobal !== childContext.isGlobal) - throw new Error( - 'Inconsistent rule global/local result in rule "' + - String(node) + - '" (multiple selectors must result in the same mode for the rule)' - ); + 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 (childContext.hasLocals) { - context.hasPureGlobals = false; + if (!nContext.hasLocals) { + context.hasPureGlobals = true; } + return n; }); + context.global = resultingGlobal; + node.nodes = normalizeNodeArray(newNodes); + // console.log(node.nodes); break; } case 'selector': { - node.each(childNode => transform(childNode, context)); - - trimSelectors(node); + newNodes = node.map(childNode => transform(childNode, context)); + node = node.clone(); + node.nodes = normalizeNodeArray(newNodes); + console.log('SECLE', node.toString()); break; } case 'combinator': { - if ( - node.value === ' ' && - (context.shouldTrimTrainingWhitespace || !node.next()) - ) { - //console.log('COMBIN', node.spaces); - context.shouldTrimTrainingWhitespace = false; - node.remove(); + if (!isSpacing(node)) break; + + if (context.ignoreNextSpacing) { + context.ignoreNextSpacing = false; + context.lastWasSpacing = false; + context.enforceNoSpacing = false; + return null; } + context.lastWasSpacing = true; break; } case 'pseudo': { - if (!isScopePseudo(node)) { - // This needs to not update `isGlobal` for tests to pass - // the behavior seems _wrong_ tho. - const childContext = { ...context }; - console.log('PSEUEDO', node.value, context); - node.each(childNode => transform(childNode, context)); - break; - } - - if (context.inside) { - throw new Error( - `A ${node.value} is not allowed inside of a ${context.inside}(...)` - ); - } - + let childContext; const isNested = !!node.length; - const isGlobal = node.value === ':global'; - if (!isNested) { - context.isGlobal = isGlobal; + const isScoped = isScopePseudo(node); - context.shouldTrimTrainingWhitespace = !node.spaces.before; - // console.log(node.spaces); - node.remove(); - return null; - } - - const childContext = { - ...context, - isGlobal, - inside: node.value, - hasLocals: false, - }; - - // The nodes of a psuedo will be Selectors, which we want to flatten - // into the parent - const nodes = node - .clone() - .map(childNode => transform(childNode, childContext)) - .reduce( - (acc, next) => - acc.concat(next.type === 'selector' ? next.nodes : next), - [] - ); - // console.log(context); - if (childContext.hasLocals) { - context.hasLocals = true; - } + // :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, + }; + // console.log('PSUDI', node.nodes); + + 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 }; + } + console.log('PSUDI', node); + node = newNodes; + + // // don't leak spacing + // node[0].spaces.before = ''; + // node[node.length - 1].spaces.after = ''; + break; + } else { + childContext = { + global: context.global, + inside: context.inside, + lastWasSpacing: true, + hasLocals: false, + explicit: context.explicit, + }; + newNodes = node.map(childNode => + transform(childNode, childContext) + ); - // console.log('asfasfasf', nodes); + node = node.clone(); + node.nodes = normalizeNodeArray(newNodes); - if (nodes.length) { - const { before, after } = node.spaces; + if (childContext.hasLocals) { + context.hasLocals = true; + } + } + break; - const first = nodes[0]; - const last = nodes[node.length - 1]; + //:local .foo .bar + } else if (isScoped) { + if (context.inside) { + throw new Error( + `A ${node.value} is not allowed inside of a ${ + context.inside + }(...)` + ); + } - first.spaces = { before, after: first.spaces.after }; - last.spaces = { before: last.spaces.before, after }; + const next = node.next(); + console.log('SPACESS', next, node.spaces); + if (next) next.spaces = node.spaces; + + context.ignoreNextSpacing = context.lastWasSpacing + ? node.value + : false; + context.enforceNoSpacing = context.lastWasSpacing + ? false + : node.value; + context.global = node.value === ':global'; + context.explicit = true; + return null; } - - nodes.forEach(childNode => { - node.parent.insertBefore(node, childNode); - }); - node.remove(); - // const parent = node.parent; - // node.replaceWith(nodes[0].nodes); - // console.log('asfasfasf', nodes, String(parent.parent)); - return nodes; + break; } case 'id': case 'class': { - const spaces = { ...node.spaces }; - - if (context.shouldTrimTrainingWhitespace) { - // console.log('HEREEEEEE', spaces); - spaces.before = ''; - context.shouldTrimTrainingWhitespace = false; - } - - if (!context.isGlobal) { - // console.log('REPLCE', node.spaces); + if (!context.global) { + console.log('REPLCE', node.spaces); const innerNode = node.clone(); + console.log(innerNode); innerNode.spaces = { before: '', after: '' }; - // console.log(node); - node.replaceWith( - selectorParser.pseudo({ - value: ':local', - nodes: [innerNode], - spaces, - }) - ); + node = selectorParser.pseudo({ + value: ':local', + nodes: [innerNode], + spaces: node.spaces, + }); // console.log('HERE'); context.hasLocals = true; } @@ -201,13 +221,16 @@ function localizeNodez(rule, mode, options) { break; } } + + context.lastWasSpacing = false; + context.ignoreNextSpacing = false; + context.enforceNoSpacing = false; return node; }; - const isGlobal = mode === 'global'; const rootContext = { - isGlobal, - hasPureGlobals: true, + global: mode === 'global', + hasPureGlobals: false, }; const updatedRule = selectorParser(root => { @@ -239,7 +262,7 @@ function localizeDeclNode(node, context) { } let newUrl = context.options.rewriteUrl( - context.isGlobal, + context.global, nestedNode.value ); @@ -349,8 +372,8 @@ function localizeAnimationShorthandDeclValues(decl, context) { const subContext = { options: context.options, - isGlobal: context.isGlobal, - localizeNextItem: shouldParseAnimationName && !context.isGlobal, + global: context.global, + localizeNextItem: shouldParseAnimationName && !context.global, }; return localizeDeclNode(node, subContext); }); @@ -363,8 +386,8 @@ function localizeDeclValues(localize, decl, context) { valueNodes.walk((node, index, nodes) => { const subContext = { options: context.options, - isGlobal: context.isGlobal, - localizeNextItem: localize && !context.isGlobal, + global: context.global, + localizeNextItem: localize && !context.global, }; nodes[index] = localizeDeclNode(node, subContext); }); @@ -433,7 +456,7 @@ module.exports = postcss.plugin('postcss-modules-local-by-default', function( atrule.walkDecls(function(decl) { localizeDecl(decl, { options: options, - isGlobal: globalKeyframes, + global: globalKeyframes, }); }); } else if (atrule.nodes) { @@ -441,7 +464,7 @@ module.exports = postcss.plugin('postcss-modules-local-by-default', function( if (decl.type === 'decl') { localizeDecl(decl, { options: options, - isGlobal: globalMode, + global: globalMode, }); } }); diff --git a/test.js b/test.js index 454cd0d..b6bfe20 100644 --- a/test.js +++ b/test.js @@ -66,11 +66,11 @@ const tests = [ // input: ':global .foo, :global .bar {}', // expected: '.foo, .bar {}', // }, - // { - // should: 'allow multiple broad local selectors', - // input: ':local .foo, :local .bar {}', - // expected: ':local(.foo), :local(.bar) {}', - // }, + { + should: 'allow multiple broad local selectors', + input: ':local .foo, :local .bar {}', + expected: ':local(.foo), :local(.bar) {}', + }, // { // should: 'allow narrow global selectors nested inside local styles', // input: '.foo :global(.foo .bar) {}', @@ -329,11 +329,11 @@ const tests = [ // input: ':global(input) {}', // expected: 'input {}', // }, - { - should: 'compile explict global attribute', - input: ':global([type="radio"]), :not(:global [type="radio"]) {}', - expected: '[type="radio"], :not([type="radio"]) {}', - }, + // { + // should: 'compile explict global attribute', + // input: ':global([type="radio"]), :not(:global [type="radio"]) {}', + // expected: '[type="radio"], :not([type="radio"]) {}', + // }, // { // should: 'throw on invalid mode', // input: '', From dab44267547433f2d52d5d8e07ff40fb9482ad04 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Mon, 18 Feb 2019 16:43:00 -0500 Subject: [PATCH 5/5] chore: migrate off of css-selector-tokenizer --- index.js | 84 ++--- test.js | 1002 +++++++++++++++++++++++++++--------------------------- 2 files changed, 533 insertions(+), 553 deletions(-) diff --git a/index.js b/index.js index 04a2420..0e8ee03 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ const postcss = require('postcss'); const selectorParser = require('postcss-selector-parser'); const valueParser = require('postcss-value-parser'); -const IS_GLOBAL = Symbol('is global'); +const isSpacing = node => node.type === 'combinator' && node.value === ' '; function normalizeNodeArray(nodes) { const array = []; @@ -25,27 +25,7 @@ function normalizeNodeArray(nodes) { return array; } -function checkForInconsistentRule(node, current, context) { - if (context.global !== context.lastIsGlobal) - throw new Error( - 'Inconsistent rule global/local result in rule "' + - String(node) + - '" (multiple selectors must result in the same mode for the rule)' - ); -} -const isSpacing = node => node.type === 'combinator' && node.value === ' '; - -function trimSelectors(selector) { - let last; - while ( - (last = selector.last) && - (last.type === 'combinator' && last.value === ' ') - ) { - last.remove(); - } -} - -function localizeNodez(rule, mode, options) { +function localizeNode(rule, mode, options) { const isScopePseudo = node => node.value === ':local' || node.value === ':global'; @@ -87,7 +67,6 @@ function localizeNodez(rule, mode, options) { context.global = resultingGlobal; node.nodes = normalizeNodeArray(newNodes); - // console.log(node.nodes); break; } case 'selector': { @@ -95,19 +74,19 @@ function localizeNodez(rule, mode, options) { node = node.clone(); node.nodes = normalizeNodeArray(newNodes); - console.log('SECLE', node.toString()); break; } case 'combinator': { - if (!isSpacing(node)) break; - - if (context.ignoreNextSpacing) { - context.ignoreNextSpacing = false; - context.lastWasSpacing = false; - context.enforceNoSpacing = false; - return null; + if (isSpacing(node)) { + if (context.ignoreNextSpacing) { + context.ignoreNextSpacing = false; + context.lastWasSpacing = false; + context.enforceNoSpacing = false; + return null; + } + context.lastWasSpacing = true; + return node; } - context.lastWasSpacing = true; break; } case 'pseudo': { @@ -132,7 +111,6 @@ function localizeNodez(rule, mode, options) { hasLocals: false, explicit: true, }; - // console.log('PSUDI', node.nodes); newNodes = node .map(childNode => transform(childNode, childContext)) @@ -147,12 +125,9 @@ function localizeNodez(rule, mode, options) { first.spaces = { before, after: first.spaces.after }; last.spaces = { before: last.spaces.before, after }; } - console.log('PSUDI', node); + node = newNodes; - // // don't leak spacing - // node[0].spaces.before = ''; - // node[node.length - 1].spaces.after = ''; break; } else { childContext = { @@ -185,28 +160,33 @@ function localizeNodez(rule, mode, options) { ); } - const next = node.next(); - console.log('SPACESS', next, node.spaces); - if (next) next.spaces = node.spaces; + 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; - return null; + + // 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; } break; } case 'id': case 'class': { if (!context.global) { - console.log('REPLCE', node.spaces); const innerNode = node.clone(); - console.log(innerNode); innerNode.spaces = { before: '', after: '' }; node = selectorParser.pseudo({ @@ -214,7 +194,7 @@ function localizeNodez(rule, mode, options) { nodes: [innerNode], spaces: node.spaces, }); - // console.log('HERE'); + context.hasLocals = true; } @@ -233,11 +213,11 @@ function localizeNodez(rule, mode, options) { hasPureGlobals: false, }; - const updatedRule = selectorParser(root => { + const selector = selectorParser(root => { transform(root, rootContext); - }).processSync(rule, { updateSelector: true, lossless: true }); + }).processSync(rule, { updateSelector: false, lossless: true }); - // console.log('HERE', rule.selector); + rootContext.selector = selector; return rootContext; } @@ -479,7 +459,9 @@ module.exports = postcss.plugin('postcss-modules-local-by-default', function( return; } - const context = localizeNodez(rule, options.mode, options); + const context = localizeNode(rule, options.mode); + + context.options = options; if (pureMode && context.hasPureGlobals) { throw rule.error( @@ -489,12 +471,10 @@ module.exports = postcss.plugin('postcss-modules-local-by-default', function( '(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) { - console.log(context); - localizeDecl(decl, context); - }); + rule.nodes.forEach(decl => localizeDecl(decl, context)); } }); }; diff --git a/test.js b/test.js index b6bfe20..53123cb 100644 --- a/test.js +++ b/test.js @@ -6,512 +6,512 @@ const plugin = require('./'); const name = require('./package.json').name; const tests = [ - // { - // should: 'scope selectors', - // input: '.foobar {}', - // expected: ':local(.foobar) {}', - // }, - // { - // should: 'scope ids', - // input: '#foobar {}', - // expected: ':local(#foobar) {}', - // }, - // { - // should: 'scope multiple selectors', - // input: '.foo, .baz {}', - // expected: ':local(.foo), :local(.baz) {}', - // }, - // { - // should: 'scope sibling selectors', - // input: '.foo ~ .baz {}', - // expected: ':local(.foo) ~ :local(.baz) {}', - // }, - // { - // should: 'scope psuedo elements', - // input: '.foo:after {}', - // expected: ':local(.foo):after {}', - // }, - // { - // should: 'scope media queries', - // input: '@media only screen { .foo {} }', - // expected: '@media only screen { :local(.foo) {} }', - // }, - // { - // should: 'allow narrow global selectors', - // input: ':global(.foo .bar) {}', - // expected: '.foo .bar {}', - // }, - // { - // should: 'allow narrow local selectors', - // input: ':local(.foo .bar) {}', - // expected: ':local(.foo) :local(.bar) {}', - // }, - // { - // should: 'allow broad global selectors', - // input: ':global .foo .bar {}', - // expected: '.foo .bar {}', - // }, - // { - // should: 'allow broad local selectors', - // input: ':local .foo .bar {}', - // expected: ':local(.foo) :local(.bar) {}', - // }, - // { - // should: 'allow multiple narrow global selectors', - // input: ':global(.foo), :global(.bar) {}', - // expected: '.foo, .bar {}', - // }, - // { - // should: 'allow multiple broad global selectors', - // input: ':global .foo, :global .bar {}', - // expected: '.foo, .bar {}', - // }, + { + should: 'scope selectors', + input: '.foobar {}', + expected: ':local(.foobar) {}', + }, + { + should: 'scope ids', + input: '#foobar {}', + expected: ':local(#foobar) {}', + }, + { + should: 'scope multiple selectors', + input: '.foo, .baz {}', + expected: ':local(.foo), :local(.baz) {}', + }, + { + should: 'scope sibling selectors', + input: '.foo ~ .baz {}', + expected: ':local(.foo) ~ :local(.baz) {}', + }, + { + should: 'scope psuedo elements', + input: '.foo:after {}', + expected: ':local(.foo):after {}', + }, + { + should: 'scope media queries', + input: '@media only screen { .foo {} }', + expected: '@media only screen { :local(.foo) {} }', + }, + { + should: 'allow narrow global selectors', + input: ':global(.foo .bar) {}', + expected: '.foo .bar {}', + }, + { + should: 'allow narrow local selectors', + input: ':local(.foo .bar) {}', + expected: ':local(.foo) :local(.bar) {}', + }, + { + should: 'allow broad global selectors', + input: ':global .foo .bar {}', + expected: '.foo .bar {}', + }, + { + should: 'allow broad local selectors', + input: ':local .foo .bar {}', + expected: ':local(.foo) :local(.bar) {}', + }, + { + should: 'allow multiple narrow global selectors', + input: ':global(.foo), :global(.bar) {}', + expected: '.foo, .bar {}', + }, + { + should: 'allow multiple broad global selectors', + input: ':global .foo, :global .bar {}', + expected: '.foo, .bar {}', + }, { should: 'allow multiple broad local selectors', input: ':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 {}', - // }, - // { - // should: 'allow broad global selectors nested inside local styles', - // input: '.foo :global .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) {}', - // }, - // { - // should: 'allow parentheses inside narrow local selectors', - // input: '.foo :local(.foo:not(.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 {}', - // }, - // { - // should: 'ignore selectors that are already local', - // input: ':local(.foobar) {}', - // expected: ':local(.foobar) {}', - // }, - // { - // should: 'ignore nested selectors that are already local', - // input: ':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) {}' - // }, - // { - // should: 'ignore sibling selectors that are already local', - // input: ':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 {}' - // }, - // { - // 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) {}', - // }, - // { - // should: 'broad global should be limited to nested selector', - // input: '.foo:not(:global .bar).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) {}', - // }, - // { - // should: 'localize a single animation-name', - // input: '.foo { animation-name: 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; }' - // }, - // { - // should: 'localize multiple animation-names', - // input: '.foo { animation-name: bar, 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); }' - // }, - // { - // should: 'localize animation with vendor prefix', - // input: '.foo { -webkit-animation: bar; animation: bar; }', - // expected: - // ':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;" }' - // }, - // { - // should: 'not localize global rules', - // input: ':global .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; }' - // }, - // { - // should: 'handle animations where the first value is not the animation name', - // input: '.foo { animation: 1s 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); }' - // }, - // { - // 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; }', - // expected: - // ':local(.foo) { animation: :local(slide-right) 300ms forwards ease-out, :local(fade-in) 300ms forwards ease-out; }' - // }, - // { - // should: - // 'not treat "start" and "end" keywords in steps() function as identifiers', - // input: [ - // '.foo { animation: spin 1s steps(12, end) infinite; }', - // '.foo { animation: spin 1s STEPS(12, start) infinite; }', - // '.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'), - // }, - // { - // 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); }' - // }, - // { - // should: 'handle animations whose names are keywords', - // input: '.foo { animation: 1s infinite 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; }' - // }, - // { - // should: 'handle "constructor" as animation name', - // input: '.foo { animation: constructor constructor; }', - // expected: - // ':local(.foo) { animation: :local(constructor) :local(constructor); }' - // }, - // { - // should: 'default to global when mode provided', - // input: '.foo {}', - // options: { mode: 'global' }, - // expected: '.foo {}', - // }, - // { - // should: 'default to local when mode provided', - // input: '.foo {}', - // options: { mode: 'local' }, - // expected: ':local(.foo) {}', - // }, - // { - // should: 'use correct spacing', - // input: [ - // '.a :local .b {}', - // '.a:local.b {}', - // '.a:local(.b) {}', - // '.a:local( .b ) {}', - // '.a :local(.b) {}', - // '.a :local( .b ) {}', - // ':local(.a).b {}', - // ':local( .a ).b {}', - // ':local(.a) .b {}', - // ':local( .a ) .b {}', - // ].join('\n'), - // options: { mode: 'global' }, - // expected: [ - // '.a :local(.b) {}', - // '.a:local(.b) {}', - // '.a:local(.b) {}', - // '.a:local(.b) {}', - // '.a :local(.b) {}', - // '.a :local(.b) {}', - // ':local(.a).b {}', - // ':local(.a).b {}', - // ':local(.a) .b {}', - // ':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; } }', - // }, - // { - // should: 'localize keyframes in global default mode', - // input: '@keyframes foo {}', - // options: { mode: 'global' }, - // 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; } }' - // }, - // { - // should: 'ignore :export statements', - // input: ':export { foo: __foo; }', - // expected: ':export { foo: __foo; }' - // }, - // { - // should: 'ignore :import statemtents', - // input: ':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) {}', - // }, - // { - // should: 'compile explict global element', - // input: ':global(input) {}', - // expected: 'input {}', - // }, - // { - // should: 'compile explict global attribute', - // input: ':global([type="radio"]), :not(:global [type="radio"]) {}', - // expected: '[type="radio"], :not([type="radio"]) {}', - // }, - // { - // should: 'throw on invalid mode', - // input: '', - // options: { mode: '???' }, - // error: /"global", "local" or "pure"/, - // }, - // { - // should: 'throw on inconsistent selector result', - // input: ':global .foo, .bar {}', - // error: /Inconsistent/, - // }, - // { - // should: 'throw on nested :locals', - // input: ':local(:local(.foo)) {}', - // error: /is not allowed inside/, - // }, - // { - // should: 'throw on nested :globals', - // input: ':global(:global(.foo)) {}', - // error: /is not allowed inside/, - // }, - // { - // should: 'throw on nested mixed', - // input: ':local(:global(.foo)) {}', - // error: /is not allowed inside/, - // }, - // { - // should: 'throw on nested broad :local', - // input: ':global(:local .foo) {}', - // error: /is not allowed inside/, - // }, - // { - // should: 'throw on incorrect spacing with broad :global', - // input: '.foo :global.bar {}', - // error: /Missing whitespace after :global/, - // }, - // { - // should: 'throw on incorrect spacing with broad :local', - // input: '.foo:local .bar {}', - // 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/, - // }, - // { - // should: 'throw on not pure selector (with multiple 1)', - // input: '.foo, :global(.bar) {}', - // options: { mode: '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/, - // }, - // { - // should: 'throw on not pure selector (element)', - // input: 'input {}', - // options: { mode: '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/, - // }, - // { - // should: 'throw on not pure keyframes', - // input: '@keyframes :global(foo) {}', - // options: { mode: 'pure' }, - // error: /@keyframes :global\(\.\.\.\) is not allowed in pure mode/, - // }, - // { - // should: 'pass through global element', - // input: 'input {}', - // expected: 'input {}', - // }, - // { - // should: 'localise class and pass through element', - // input: '.foo input {}', - // expected: ':local(.foo) input {}', - // }, - // { - // should: 'pass through attribute selector', - // input: '[type="radio"] {}', - // expected: '[type="radio"] {}', - // }, - // { - // should: 'not modify urls without option', - // input: - // '.a { background: url(./image.png); }\n' + - // ':global .b { background: url(image.png); }\n' + - // '.c { background: url("./image.png"); }', - // expected: - // ':local(.a) { background: url(./image.png); }\n' + - // '.b { background: url(image.png); }\n' + - // ':local(.c) { background: url("./image.png"); }', - // }, - // { - // should: 'rewrite url in local block', - // input: - // '.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" + - // '.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' + - // '@media screen { .a { src: url("./image.png"); } }\n' + - // '@keyframes :global(ani1) { 0% { src: url("image.png"); } }\n' + - // '@keyframes ani2 { 0% { src: url("./image.png"); } }\n' + - // 'foo { background: end-with-url(something); }', - // options: { - // rewriteUrl: function(global, url) { - // const mode = global ? 'global' : 'local'; - // return '(' + mode + ')' + url + '"' + mode + '"'; - // }, - // }, - // expected: - // ':local(.a) { background: url((local\\)./image.png\\"local\\"); }\n' + - // '.b { background: url((global\\)image.png\\"global\\"); }\n' + - // ':local(.c) { background: url("(local)./image.png\\"local\\""); }\n' + - // ':local(.c) { background: url(\'(local)./image.png"local"\'); }\n' + - // ':local(.d) { background: -webkit-image-set(url("(local)./image.png\\"local\\"") 1x, url("(local)./image2x.png\\"local\\"") 2x); }\n' + - // '@font-face { src: url("(local)./font.woff\\"local\\""); }\n' + - // '@-webkit-font-face { src: url("(local)./font.woff\\"local\\""); }\n' + - // '@media screen { :local(.a) { src: url("(local)./image.png\\"local\\""); } }\n' + - // '@keyframes ani1 { 0% { src: url("(global)image.png\\"global\\""); } }\n' + - // '@keyframes :local(ani2) { 0% { src: url("(local)./image.png\\"local\\""); } }\n' + - // 'foo { background: end-with-url(something); }', - // }, - // { - // should: 'not crash on atrule without nodes', - // input: '@charset "utf-8";', - // expected: '@charset "utf-8";', - // }, - // { - // should: 'not crash on a rule without nodes', - // input: (function() { - // const inner = postcss.rule({ selector: '.b', ruleWithoutBody: true }); - // const outer = postcss.rule({ selector: '.a' }).push(inner); - // const root = postcss.root().push(outer); - // inner.nodes = undefined; - // return root; - // })(), - // // postcss-less's stringify would honor `ruleWithoutBody` and omit the trailing `{}` - // expected: ':local(.a) {\n :local(.b) {}\n}', - // }, - // { - // should: 'not break unicode characters', - // input: '.a { content: "\\2193" }', - // expected: ':local(.a) { content: "\\2193" }', - // }, - // { - // should: 'not break unicode characters', - // input: '.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" }', - // }, - // { - // should: 'not break unicode characters', - // input: '.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" }', - // }, + { + should: 'allow narrow global selectors nested inside local styles', + input: '.foo :global(.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 {}', + }, + { + should: 'allow parentheses inside narrow global selectors', + input: '.foo :global(.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)) {}', + }, + { + should: 'allow narrow global selectors appended to local styles', + input: '.foo:global(.foo.bar) {}', + expected: ':local(.foo).foo.bar {}', + }, + { + should: 'ignore selectors that are already local', + input: ':local(.foobar) {}', + expected: ':local(.foobar) {}', + }, + { + should: 'ignore nested selectors that are already local', + input: ':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) {}', + }, + { + should: 'ignore sibling selectors that are already local', + input: ':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 {}', + }, + { + 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) {}', + }, + { + should: 'broad global should be limited to nested selector', + input: '.foo:not(:global .bar).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) {}', + }, + { + should: 'localize a single animation-name', + input: '.foo { animation-name: 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; }', + }, + { + should: 'localize multiple animation-names', + input: '.foo { animation-name: bar, 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); }', + }, + { + should: 'localize animation with vendor prefix', + input: '.foo { -webkit-animation: bar; animation: bar; }', + expected: + ':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;" }', + }, + { + should: 'not localize global rules', + input: ':global .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; }', + }, + { + should: 'handle animations where the first value is not the animation name', + input: '.foo { animation: 1s 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); }', + }, + { + 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; }', + expected: + ':local(.foo) { animation: :local(slide-right) 300ms forwards ease-out, :local(fade-in) 300ms forwards ease-out; }', + }, + { + should: + 'not treat "start" and "end" keywords in steps() function as identifiers', + input: [ + '.foo { animation: spin 1s steps(12, end) infinite; }', + '.foo { animation: spin 1s STEPS(12, start) infinite; }', + '.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'), + }, + { + 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); }', + }, + { + should: 'handle animations whose names are keywords', + input: '.foo { animation: 1s infinite 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; }', + }, + { + should: 'handle "constructor" as animation name', + input: '.foo { animation: constructor constructor; }', + expected: + ':local(.foo) { animation: :local(constructor) :local(constructor); }', + }, + { + should: 'default to global when mode provided', + input: '.foo {}', + options: { mode: 'global' }, + expected: '.foo {}', + }, + { + should: 'default to local when mode provided', + input: '.foo {}', + options: { mode: 'local' }, + expected: ':local(.foo) {}', + }, + { + should: 'use correct spacing', + input: [ + '.a :local .b {}', + '.a:local.b {}', + '.a:local(.b) {}', + '.a:local( .b ) {}', + '.a :local(.b) {}', + '.a :local( .b ) {}', + ':local(.a).b {}', + ':local( .a ).b {}', + ':local(.a) .b {}', + ':local( .a ) .b {}', + ].join('\n'), + options: { mode: 'global' }, + expected: [ + '.a :local(.b) {}', + '.a:local(.b) {}', + '.a:local(.b) {}', + '.a:local(.b) {}', + '.a :local(.b) {}', + '.a :local(.b) {}', + ':local(.a).b {}', + ':local(.a).b {}', + ':local(.a) .b {}', + ':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; } }', + }, + { + should: 'localize keyframes in global default mode', + input: '@keyframes foo {}', + options: { mode: 'global' }, + 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; } }', + }, + { + should: 'ignore :export statements', + input: ':export { foo: __foo; }', + expected: ':export { foo: __foo; }', + }, + { + should: 'ignore :import statemtents', + input: ':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) {}', + }, + { + should: 'compile explict global element', + input: ':global(input) {}', + expected: 'input {}', + }, + { + should: 'compile explict global attribute', + input: ':global([type="radio"]), :not(:global [type="radio"]) {}', + expected: '[type="radio"], :not([type="radio"]) {}', + }, + { + should: 'throw on invalid mode', + input: '', + options: { mode: '???' }, + error: /"global", "local" or "pure"/, + }, + { + should: 'throw on inconsistent selector result', + input: ':global .foo, .bar {}', + error: /Inconsistent/, + }, + { + should: 'throw on nested :locals', + input: ':local(:local(.foo)) {}', + error: /is not allowed inside/, + }, + { + should: 'throw on nested :globals', + input: ':global(:global(.foo)) {}', + error: /is not allowed inside/, + }, + { + should: 'throw on nested mixed', + input: ':local(:global(.foo)) {}', + error: /is not allowed inside/, + }, + { + should: 'throw on nested broad :local', + input: ':global(:local .foo) {}', + error: /is not allowed inside/, + }, + { + should: 'throw on incorrect spacing with broad :global', + input: '.foo :global.bar {}', + error: /Missing whitespace after :global/, + }, + { + should: 'throw on incorrect spacing with broad :local', + input: '.foo:local .bar {}', + 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/, + }, + { + should: 'throw on not pure selector (with multiple 1)', + input: '.foo, :global(.bar) {}', + options: { mode: '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/, + }, + { + should: 'throw on not pure selector (element)', + input: 'input {}', + options: { mode: '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/, + }, + { + should: 'throw on not pure keyframes', + input: '@keyframes :global(foo) {}', + options: { mode: 'pure' }, + error: /@keyframes :global\(\.\.\.\) is not allowed in pure mode/, + }, + { + should: 'pass through global element', + input: 'input {}', + expected: 'input {}', + }, + { + should: 'localise class and pass through element', + input: '.foo input {}', + expected: ':local(.foo) input {}', + }, + { + should: 'pass through attribute selector', + input: '[type="radio"] {}', + expected: '[type="radio"] {}', + }, + { + should: 'not modify urls without option', + input: + '.a { background: url(./image.png); }\n' + + ':global .b { background: url(image.png); }\n' + + '.c { background: url("./image.png"); }', + expected: + ':local(.a) { background: url(./image.png); }\n' + + '.b { background: url(image.png); }\n' + + ':local(.c) { background: url("./image.png"); }', + }, + { + should: 'rewrite url in local block', + input: + '.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" + + '.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' + + '@media screen { .a { src: url("./image.png"); } }\n' + + '@keyframes :global(ani1) { 0% { src: url("image.png"); } }\n' + + '@keyframes ani2 { 0% { src: url("./image.png"); } }\n' + + 'foo { background: end-with-url(something); }', + options: { + rewriteUrl: function(global, url) { + const mode = global ? 'global' : 'local'; + return '(' + mode + ')' + url + '"' + mode + '"'; + }, + }, + expected: + ':local(.a) { background: url((local\\)./image.png\\"local\\"); }\n' + + '.b { background: url((global\\)image.png\\"global\\"); }\n' + + ':local(.c) { background: url("(local)./image.png\\"local\\""); }\n' + + ':local(.c) { background: url(\'(local)./image.png"local"\'); }\n' + + ':local(.d) { background: -webkit-image-set(url("(local)./image.png\\"local\\"") 1x, url("(local)./image2x.png\\"local\\"") 2x); }\n' + + '@font-face { src: url("(local)./font.woff\\"local\\""); }\n' + + '@-webkit-font-face { src: url("(local)./font.woff\\"local\\""); }\n' + + '@media screen { :local(.a) { src: url("(local)./image.png\\"local\\""); } }\n' + + '@keyframes ani1 { 0% { src: url("(global)image.png\\"global\\""); } }\n' + + '@keyframes :local(ani2) { 0% { src: url("(local)./image.png\\"local\\""); } }\n' + + 'foo { background: end-with-url(something); }', + }, + { + should: 'not crash on atrule without nodes', + input: '@charset "utf-8";', + expected: '@charset "utf-8";', + }, + { + should: 'not crash on a rule without nodes', + input: (function() { + const inner = postcss.rule({ selector: '.b', ruleWithoutBody: true }); + const outer = postcss.rule({ selector: '.a' }).push(inner); + const root = postcss.root().push(outer); + inner.nodes = undefined; + return root; + })(), + // postcss-less's stringify would honor `ruleWithoutBody` and omit the trailing `{}` + expected: ':local(.a) {\n :local(.b) {}\n}', + }, + { + should: 'not break unicode characters', + input: '.a { content: "\\2193" }', + expected: ':local(.a) { content: "\\2193" }', + }, + { + should: 'not break unicode characters', + input: '.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" }', + }, + { + should: 'not break unicode characters', + input: '.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" }', + }, ]; function process(css, options) {