diff --git a/src/index.js b/src/index.js index c7e3ff4..dc8dd2f 100644 --- a/src/index.js +++ b/src/index.js @@ -95,6 +95,62 @@ function unescape(str) { }); } +function isCssIdentChar(char) { + return /[A-Za-z0-9_\\\u0080-\uFFFF-]/.test(char); +} + +// Split @scope prelude on the `to` keyword only, not on the letters "to" +// inside a selector (e.g. .inputInnerButtons, .auto, :local(.to)). +function splitScopeParams(params) { + let depth = 0; + let quote = null; + + for (let i = 0; i < params.length; i++) { + const char = params[i]; + + if (quote) { + if (char === "\\") { + i += 1; + continue; + } + + if (char === quote) { + quote = null; + } + + continue; + } + + if (char === '"' || char === "'") { + quote = char; + continue; + } + + if (char === "(") { + depth += 1; + continue; + } + + if (char === ")") { + if (depth > 0) { + depth -= 1; + } + continue; + } + + if ( + depth === 0 && + params.slice(i, i + 2) === "to" && + (i === 0 || !isCssIdentChar(params[i - 1])) && + (i + 2 >= params.length || !isCssIdentChar(params[i + 2])) + ) { + return [params.slice(0, i), params.slice(i + 2)]; + } + } + + return [params]; +} + const plugin = (options = {}) => { const generateScopedName = (options && options.generateScopedName) || plugin.generateScopedName; @@ -316,8 +372,7 @@ const plugin = (options = {}) => { root.walkAtRules(/scope$/i, (atRule) => { if (atRule.params) { - atRule.params = atRule.params - .split("to") + atRule.params = splitScopeParams(atRule.params) .map((item) => { const selector = item.trim().slice(1, -1).trim(); diff --git a/test/test-cases/at-rule-scope-to-in-selector/expected.css b/test/test-cases/at-rule-scope-to-in-selector/expected.css new file mode 100644 index 0000000..f56696a --- /dev/null +++ b/test/test-cases/at-rule-scope-to-in-selector/expected.css @@ -0,0 +1,23 @@ +@scope (.innerActionsWrapper) to (.inputInnerButtons > * > *) { + --button-variant: transparent; +} + +@scope (._input__auto) to (._input__total) { + color: red; +} + +@scope (._input__innerActionsWrapper) to (._input__inputInnerButtons) { + color: blue; +} + +@scope (._input__to) { + color: green; +} + +:export { + auto: _input__auto; + total: _input__total; + innerActionsWrapper: _input__innerActionsWrapper; + inputInnerButtons: _input__inputInnerButtons; + to: _input__to; +} diff --git a/test/test-cases/at-rule-scope-to-in-selector/source.css b/test/test-cases/at-rule-scope-to-in-selector/source.css new file mode 100644 index 0000000..e355663 --- /dev/null +++ b/test/test-cases/at-rule-scope-to-in-selector/source.css @@ -0,0 +1,15 @@ +@scope (.innerActionsWrapper) to (.inputInnerButtons > * > *) { + --button-variant: transparent; +} + +@scope (:local(.auto)) to (:local(.total)) { + color: red; +} + +@scope (:local(.innerActionsWrapper)) to (:local(.inputInnerButtons)) { + color: blue; +} + +@scope (:local(.to)) { + color: green; +}