From 02a4f79ff08136b6edf593195908946d9b85aa5f Mon Sep 17 00:00:00 2001 From: Rizwan Saleem Date: Sun, 8 Nov 2026 22:20:44 +0000 Subject: [PATCH] fix: do not reuse a pseudo's name as the namespace of a following `|` `:hover|a` printed as `:hoverhover|a`. The pseudo consumed the word `hover`, and then `namespace()` looked back at that same token and used it again as the prefix of `|a`. Each parse and print added another copy. When the `|` is reached from the main loop, the token before it has already been consumed, so only a skipped `&` can still be a prefix. The `|` now starts an empty namespace there, the way it already does after `]` in `[x]|a`. --- src/__tests__/namespaces.mjs | 20 ++++++++++++++++++++ src/parser.js | 14 ++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/__tests__/namespaces.mjs b/src/__tests__/namespaces.mjs index 7ce8bcb..79c24e0 100644 --- a/src/__tests__/namespaces.mjs +++ b/src/__tests__/namespaces.mjs @@ -201,3 +201,23 @@ throws("lone pipe symbol with trailing spaces", "| "); throws("lone pipe symbol with surrounding spaces", " | "); throws("trailing pipe symbol with a namespace", "foo| "); throws("trailing pipe symbol with any namespace", "*| "); + +test("`|` after a pseudo-class does not reuse its name as a namespace", "a:hover|b", (t, tree) => { + t.deepEqual(tree.nodes[0].nodes.length, 3); + t.deepEqual(tree.nodes[0].nodes[1].type, "pseudo"); + t.deepEqual(tree.nodes[0].nodes[1].value, ":hover"); + t.deepEqual(tree.nodes[0].nodes[2].type, "tag"); + t.deepEqual(tree.nodes[0].nodes[2].namespace, true); + t.deepEqual(tree.nodes[0].nodes[2].value, "b"); +}); + +test( + "`|*` after a pseudo-element does not reuse its name as a namespace", + "::before|*", + (t, tree) => { + t.deepEqual(tree.nodes[0].nodes.length, 2); + t.deepEqual(tree.nodes[0].nodes[0].value, "::before"); + t.deepEqual(tree.nodes[0].nodes[1].type, "universal"); + t.deepEqual(tree.nodes[0].nodes[1].namespace, true); + }, +); diff --git a/src/parser.js b/src/parser.js index 24d35be..7ba508b 100644 --- a/src/parser.js +++ b/src/parser.js @@ -649,7 +649,10 @@ export default class Parser { combinator() { if (this.content() === "|") { - return this.namespace(); + // A word or `*` right before this `|` was already consumed by whatever + // came before, such as the name of a pseudo in `:hover|a`, so it cannot be + // the prefix. Only a skipped `&` can still be one. + return this.namespace(false); } // We need to decide between a space that's a descendant combinator and meaningless whitespace at the end of a selector. let nextSigTokenPos = this.locateNextMeaningfulToken(this.position); @@ -831,15 +834,18 @@ export default class Parser { return this.error(`Unexpected '|'.`, this.currToken[TOKEN.START_POS]); } - namespace() { + /** + * @param {boolean} [prefixed] whether the token before the `|` is still + * unconsumed, as it is when `word()` or `universal()` sees the `|` next. + */ + namespace(prefixed = true) { const prev = this.prevToken; // Only treat the previous token as a namespace prefix when it can actually // be one (a type/word or the universal `*`). A comment, comma, combinator // or whitespace before `|` means an empty namespace, not a prefix. const before = prev && - (prev[TOKEN.TYPE] === tokens.word || - prev[TOKEN.TYPE] === tokens.asterisk || + ((prefixed && (prev[TOKEN.TYPE] === tokens.word || prev[TOKEN.TYPE] === tokens.asterisk)) || prev[TOKEN.TYPE] === tokens.ampersand) ? this.content(prev) : true;