`attribute()` handled `$` in the value position, and treated it as the suffix
match operator when followed by `=`, but had no branch for a `$` that belongs to
the attribute name. Those fell through to the `tokens.caret` case, which only
acts when the next token is `=`, so the character was silently discarded:
parser().astSync("[#{$attr}]").toString() // "[#{attr}]"
parser().astSync("[$attr]").toString() // "[$attr]" -> "[attr]"
Preprocessors reach this through interpolation, which is how both reports arrived.
The value position already worked, so `[lang=#{$locale}]` round-tripped while
`[#{$attr}]` did not.
The new branch mirrors the existing value-position handling and is guarded on the
next token not being `=`, which keeps the suffix match operator intact. That guard
is what allows a name and an operator to coexist:
[$attr$="x"] attribute "$attr", operator "$=", value "x"
Compared against main across 4,800 generated attribute selectors: 991 newly
round-trip, 0 regressions, and none that previously parsed now throw. A trailing
`$` such as `[a$]` also stops raising a raw TypeError as a side effect, since the
new branch returns before the unguarded lookahead is reached.
Fixes postcss#211
Fixes postcss#306
Fixes #211. Fixes #306.
Both reports are the same defect seen through different syntax.
The bug
The
$is dropped when it belongs to the attribute name. The value position already worked, which is why[lang=#{$locale}]round-trips in the existing tests while[#{$attr}]does not.Cause
attribute()has a branch for$in the value position, and thetokens.caretcase immediately below turns$into the suffix match operator when the next token is=. Nothing handled a$that is part of the attribute name, so it fell through totokens.caret, failed the=check, and was discarded without being recorded anywhere.The new branch mirrors the value-position handling, and is guarded on the next token not being
=.Why the guard matters
Without it,
$=stops parsing as an operator. With it, a name and an operator can coexist:[a$=x]and[data-weird-attr$="Something=weird"]are unaffected, and both remain covered by the existing tests.Verification
Compared against
mainacross 4,800 generated attribute selectors, varying the name, namespace, operator, value, quoting, insensitivity flag and padding:TypeErrorThe
TypeErrorreduction is a side effect rather than the aim: a trailing$such as[a$]now returns before reaching the unguardednext[TOKEN.TYPE]lookahead intokens.caret. That lookahead is the subject of #334 and is not otherwise touched here.npm testpasses, includingoxlint, the type check and the coverage thresholds: 94.99% lines, 95.41% branches, 97.71% functions against gates of 94/94/96. Test count 789 to 799.Tests
Added to
nonstandard.mjs, next to the existingsass escapescases, since these arrive through preprocessor interpolation: interpolated attribute name, bare$name, interpolation on both sides, a$name combined with$=, and a namespaced$name.Not addressed
[#{$attr} i]still drops the insensitivity flag, both before and after this change. The flag survives only when an operator is present, which is a separate defect and left alone.