Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/__tests__/escapes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
Expand Down
12 changes: 10 additions & 2 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down