diff --git a/src/__tests__/escapes.mjs b/src/__tests__/escapes.mjs index 946176b..f1a8ba0 100644 --- a/src/__tests__/escapes.mjs +++ b/src/__tests__/escapes.mjs @@ -12,6 +12,32 @@ test("escaped semicolon in id", "#\\;", (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].type, "id"); }); +for (const [delimiter, type] of [ + [".", "class"], + ["#", "id"], +]) { + test(`escaped backslash before the ${type} delimiter`, `.foo\\\\${delimiter}bar`, (t, tree) => { + t.deepEqual( + tree.first.nodes.map(({ type, value }) => [type, value]), + [ + ["class", "foo\\"], + [type, "bar"], + ], + ); + }); + + test( + `escaped ${type} delimiter after an escaped backslash`, + `.foo\\\\\\${delimiter}bar`, + (t, tree) => { + t.deepEqual( + tree.first.nodes.map(({ type, value }) => [type, value]), + [["class", `foo\\${delimiter}bar`]], + ); + }, + ); +} + // This is a side-effect of allowing media queries to be parsed. Not sure it shouldn't just be an error. test("bare parens capture contents as a string", "(h1)", (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].value, "(h1)"); diff --git a/src/parser.js b/src/parser.js index 22d5e69..3042082 100644 --- a/src/parser.js +++ b/src/parser.js @@ -105,6 +105,14 @@ function indexesOf(array, item) { return indexes; } +function isEscaped(word, index) { + let escapeStart = index; + while (word[escapeStart - 1] === "\\") { + escapeStart--; + } + return (index - escapeStart) % 2 === 1; +} + function uniqs() { // A Set keeps the first-occurrence order the previous filter/indexOf pass // produced, without its quadratic cost on long index lists. @@ -1000,12 +1008,12 @@ export default class Parser { } const hasClass = indexesOf(word, ".").filter((i) => { // Allow escaped dot within class name - const escapedDot = word[i - 1] === "\\"; + const escapedDot = isEscaped(word, i); // Allow decimal numbers percent in @keyframes const isKeyframesPercent = /^\d+\.\d+%$/.test(word); return !escapedDot && !isKeyframesPercent; }); - let hasId = indexesOf(word, "#").filter((i) => word[i - 1] !== "\\"); + let hasId = indexesOf(word, "#").filter((i) => !isEscaped(word, i)); // Eliminate Sass interpolations from the list of id indexes const interpolations = indexesOf(word, "#{"); if (interpolations.length) {