diff --git a/lib/parser.js b/lib/parser.js index 2ef8bd87e5356ad7132e9b6ba7d49c06da63ab5c..2871049e6a3cffe60eadbd98653cf6280106bde0 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1638,13 +1638,20 @@ module.exports = class Parser { selectorParts() { var tok - , arr = []; + , arr = [] + , parens = 0; // Selector candidates, // stitched together to // form a selector. - while (tok = this.selectorToken()) { + // Commas inside functions belong to this selector, not the group. + while (tok = this.selectorToken() || (parens && this.accept(','))) { debug.selector('%s', tok); + if ('function' == tok.type || '(' == tok.type) { + ++parens; + } else if (')' == tok.type && parens) { + --parens; + } // Selector component switch (tok.type) { case '{': diff --git a/lib/utils.js b/lib/utils.js index 88e5c3993d464e9b2be1c262d115572c6c9de7de..03516414fd9c5a631f758cb0d87d4f0addad982a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -426,6 +426,60 @@ exports.uniq = function(arr){ return ret; }; +/** + * Split a selector list without splitting function arguments or attributes. + * + * @param {String} str + * @return {Array} + * @api private + */ + +exports.splitSelectors = function(str){ + var selectors = [] + , start = 0 + , parens = 0 + , brackets = 0 + , quote; + + for (var i = 0; i < str.length; ++i) { + var c = str[i]; + if ('\\' == c) { + ++i; + continue; + } + if (quote) { + if (c == quote) quote = null; + continue; + } + if ('"' == c || "'" == c) { + quote = c; + continue; + } + if ('/' == c && '*' == str[i + 1]) { + var end = str.indexOf('*/', i + 2); + if (-1 == end) break; + i = end + 1; + continue; + } + if ('[' == c) { + ++brackets; + } else if (']' == c) { + if (brackets) --brackets; + } else if (!brackets) { + if ('(' == c) { + ++parens; + } else if (')' == c) { + if (parens) --parens; + } else if (',' == c && !parens) { + selectors.push(str.slice(start, i)); + start = i + 1; + } + } + } + selectors.push(str.slice(start)); + return selectors; +}; + /** * Compile selector strings in `arr` from the bottom-up * to produce the selector combinations. For example diff --git a/lib/visitor/normalizer.js b/lib/visitor/normalizer.js index d6d97654f7a4dc9e91db8782a6aba42528283f43..2e614f9feb5c952e37241627e9db6285352765b6 100644 --- a/lib/visitor/normalizer.js +++ b/lib/visitor/normalizer.js @@ -237,29 +237,34 @@ module.exports = class Normalizer extends Visitor { visitGroup(group) { var stack = this.stack , map = this.map - , parts; + , normalized = []; - // normalize interpolated selectors with comma - group.nodes.forEach(function (selector, i) { - if (!~selector.val.indexOf(',')) return; - if (~selector.val.indexOf('\\,')) { + // Normalize interpolated lists without overwriting subsequent selectors. + group.nodes.forEach(function (selector) { + if (!~selector.val.indexOf(',')) { + normalized.push(selector); + return; + } + var parts = utils.splitSelectors(selector.val); + if (1 == parts.length) { selector.val = selector.val.replace(/\\,/g, ','); + normalized.push(selector); return; } - parts = selector.val.split(','); var root = '/' == selector.val.charAt(0) , part, s; for (var k = 0, len = parts.length; k < len; ++k) { - part = parts[k].trim(); + part = parts[k].trim().replace(/\\,/g, ','); if (root && k > 0 && !~part.indexOf('&')) { part = '/' + part; } s = new nodes.Selector([new nodes.Literal(part)]); s.val = part; s.block = group.block; - group.nodes[i++] = s; + normalized.push(s); } }); + group.nodes = normalized; stack.push(group.nodes); var selectors = utils.compileSelectors(stack, true); diff --git a/test/cases/selectors.pseudo.css b/test/cases/selectors.pseudo.css index 387e67088e631bc281e700856f85f4b4a154b5ea..629025368d70a53c5d167eaf67879d3be6cfc2ae 100644 --- a/test/cases/selectors.pseudo.css +++ b/test/cases/selectors.pseudo.css @@ -94,15 +94,11 @@ label input:blank, label span { color: #014; } -:is(header, -main, -footer) p:hover { +:is(header, main, footer) p:hover { color: #f00; cursor: pointer; } -:where(header, -main, -footer) p:hover { +:where(header, main, footer) p:hover { color: #f00; cursor: pointer; } diff --git a/test/cases/regression.2750.compress.css b/test/cases/regression.2750.compress.css new file mode 100644 --- /dev/null +++ b/test/cases/regression.2750.compress.css @@ -0,0 +1,1 @@ +.scope :is(.a, .b) .item,.scope :where(.c, .d) .item{color:#f00} diff --git a/test/cases/regression.2750.compress.styl b/test/cases/regression.2750.compress.styl new file mode 100644 --- /dev/null +++ b/test/cases/regression.2750.compress.styl @@ -0,0 +1,6 @@ +// Compression must not move selector fragments across functional arguments. + +.scope + :is(.a, .b), :where(.c, .d) + .item + color red diff --git a/test/cases/regression.2750.css b/test/cases/regression.2750.css new file mode 100644 --- /dev/null +++ b/test/cases/regression.2750.css @@ -0,0 +1,45 @@ +:is(.a, .b) .item { + color: #f00; +} +.scope :is(.a, .b) .item { + color: #f00; +} +.menu-item:is(:hover, :focus) { + color: #f00; +} +.parent:is(.a, .b) .item { + color: #f00; +} +:where(.a, .b) .item { + color: #f00; +} +:not(.disabled, :has(> .icon, + .sibling)) .item { + color: #f00; +} +:has(> .a, + .b) .item { + color: #f00; +} +:nth-child(2n + 1 of .a, .b) .item { + color: #f00; +} +.first .item, +:is(.second, .third) .item, +.last .item { + color: #f00; +} +[data-value="a,b"] .item { + color: #f00; +} +:is([data-value="),x"], :not(.c, .d)) .item { + color: #f00; +} +:is(.multiline-a,.multiline-b) .item { + color: #f00; +} +.extend-target:is(.a, .b), +.extended { + color: #f00; +} +:is(.escaped-a,.escaped-b) .item { + color: #f00; +} diff --git a/test/cases/regression.2750.interpolation.css b/test/cases/regression.2750.interpolation.css new file mode 100644 --- /dev/null +++ b/test/cases/regression.2750.interpolation.css @@ -0,0 +1,44 @@ +:is(.a, .b) .item { + color: #f00; +} +.wrapper :not(.a, :has(> .b, + .c)) .item, +.wrapper .fallback .item { + color: #f00; +} +.before .item, +:is(.a, .b) .item, +.fallback .item, +.after .item { + color: #f00; +} +.attributes [data-value="a,b"] .item, +.attributes [data-other=",)"] .item { + color: #f00; +} +.quoted [data-value="a\",b"] .item, +.quoted .fallback .item { + color: #f00; +} +:is(.a, .b) .item, +.fallback .item { + color: #f00; +} +.legacy :is(.a, .b) .item, +.legacy .fallback .item { + color: #f00; +} +.bif:is(.a, .b) .item { + color: #f00; +} +.single-quoted [data-value='a,b'] .item, +.single-quoted .fallback .item { + color: #f00; +} +.escaped-parens .bracket\(x .item, +.escaped-parens .bracket\)x .item { + color: #f00; +} +.comments :is(.a, .b /* (, */) .item, +.comments .fallback .item { + color: #f00; +} diff --git a/test/cases/regression.2750.interpolation.styl b/test/cases/regression.2750.interpolation.styl new file mode 100644 --- /dev/null +++ b/test/cases/regression.2750.interpolation.styl @@ -0,0 +1,55 @@ +// Preserve functional and attribute commas when expanding interpolated lists. + +selector = ':is(.a, .b)' +{selector} + .item + color red + +.wrapper + {':not(.a, :has(> .b, + .c)), .fallback'} + .item + color red + +.before, {':is(.a, .b), .fallback'}, .after + .item + color red + +.attributes + {'[data-value="a,b"], [data-other=",)"]'} + .item + color red + +.quoted + {'[data-value="a\",b"], .fallback'} + .item + color red + +.root + /{':is(.a, .b), .fallback'} + .item + color red + +.legacy + {':is(.a\, .b), .fallback'} + .item + color red + +.bif + /{selector('&:is(.a, .b)')} + .item + color red + +.single-quoted + {"[data-value='a,b'], .fallback"} + .item + color red + +.escaped-parens + {'.bracket\(x, .bracket\)x'} + .item + color red + +.comments + {':is(.a, .b /* (, */), .fallback'} + .item + color red diff --git a/test/cases/regression.2750.styl b/test/cases/regression.2750.styl new file mode 100644 --- /dev/null +++ b/test/cases/regression.2750.styl @@ -0,0 +1,63 @@ +// Keep commas inside functional selectors out of the outer selector list. + +:is(.a, .b) + .item + color red + +.scope + :is(.a, .b) + .item + color red + +.menu-item:is(:hover, :focus) + color red + +.parent + &:is(.a, .b) + .item + color red + +:where(.a, .b) + .item + color red + +:not(.disabled, :has(> .icon, + .sibling)) + .item + color red + +:has(> .a, + .b) + .item + color red + +:nth-child(2n + 1 of .a, .b) + .item + color red + +.first, :is(.second, .third), .last + .item + color red + +[data-value="a,b"] + .item + color red + +:is([data-value="),x"], :not(.c, .d)) + .item + color red + +:is( + .multiline-a, + .multiline-b +) + .item + color red + +.extend-target:is(.a, .b) + color red + +.extended + @extend .extend-target:is(.a, .b) + +:is(.escaped-a\\\, .escaped-b) + .item + color red