Skip to content

Fix parsing of pseudo-elements and pseudo-classes after ::details-content - #1308

Open
rorshopping wants to merge 3 commits into
parcel-bundler:masterfrom
rorshopping:fix/details-content-before
Open

rorshopping wants to merge 3 commits into
parcel-bundler:masterfrom
rorshopping:fix/details-content-before

Conversation

@rorshopping

@rorshopping rorshopping commented Aug 12, 2026 •

Copy link
Copy Markdown

Fixes #1244

Problem

Selectors chaining pseudo-elements off ::details-content fail to parse:

::details-content::before { color: red }   /* error: Invalid state */
details::details-content::after { content: '' }

Root cause

::details-content is an element-backed pseudo-element: it behaves like a type selector, so other pseudo-elements and pseudo-classes may follow it. The parser did not model this distinction, and treated every known pseudo-element the same way as ::before (which cannot be followed by further pseudo-elements).

Changes

The relationship between pseudo-elements is now modeled comprehensively, mirroring the current upstream selectors parser in servo/stylo (where Firefox implements ::details-content) and the Chromium change that allowed almost all pseudo-elements after ::part() (crrev.com/c/5823774):

  • Rename the parsing state to AFTER_NON_ELEMENT_BACKED_PSEUDO, add AFTER_BEFORE_OR_AFTER_PSEUDO, and add PseudoElement::{parses_as_element_backed, is_before_or_after, valid_after_before_or_after} trait methods, matching upstream naming.
  • Element-backed pseudo-elements now parse exactly like ::part(): all pseudo-classes (including functional ones like :lang()) and all other pseudo-elements may follow them, and they may also follow ::part() itself, e.g. ::part(foo)::details-content::before. ::part(), ::slotted(), combinators and type/class/id selectors remain errors after them.
  • ::before/::after accept a single following pseudo-element, restricted to a valid_after_before_or_after() allowlist that currently only contains ::marker (per css-pseudo-4), so ::before::marker and ::details-content::before::marker parse while e.g. ::before::before, ::marker::marker and ::before::details-content are still errors.
  • lightningcss also marks ::picker() as element-backed, matching Gecko (stylo's pseudo_elements.toml). The view-transition pseudo-elements are deliberately left out: css-view-transitions only allows :only-child after them.
  • A bare element-backed pseudo-element now also sets the HAS_PSEUDO selector flag, so such selectors are excluded from the :is() lowering in rules/mod.rs (where pseudo-elements are not allowed).

Now all of these parse:

::details-content::before { color: red }
::details-content::after { content: '' }
::details-content::marker { color: red }
::details-content::before::marker { color: red }
details::details-content::before { color: red }
details::details-content:lang(en)::before { content: '' }
::part(foo)::details-content::before { color: red }
::part(foo)::marker { color: red }
::picker(select)::before { color: red }

while these remain errors (matching browsers):

::details-content div { color: red }          /* error */
::details-content.foo { color: red }          /* error */
::details-content::part(x) { color: red }     /* error */
::details-content::slotted(div) { color: red } /* error */
::details-content::before::after { color: red } /* error */
::before::before { color: red }               /* error */
::marker::marker { color: red }               /* error */
::view-transition-group(foo)::before { color: red } /* error */

Tests

  • selectors: test_pseudo_on_element_backed_pseudo, plus new test_pseudo_duplicate_before_after_or_marker, test_pseudo_after_before_or_after and test_pseudo_after_part (mirroring upstream), and test_parsing now covers the chains above.
  • lightningcss: minify tests for ::details-content::before/after/marker, ::before::marker, ::details-content::before::marker, details::details-content::before, ::part(foo)::details-content::before and ::picker(select)::before, plus error tests for ::details-content::part(foo), ::details-content::slotted(div), ::details-content::before::before, ::details-content::before::selection, ::before::details-content, ::marker::marker and ::part(foo)::part(bar).
  • Also fixes the pre-existing test_parsing failures in the selectors crate (the DummyParser was missing ::target-text, ::search-text, :current, ::highlight(), ::picker(), ::picker-icon, ::checkmark, ::grammar-error and ::spelling-error), so that suite is fully green.

Element-backed pseudo-elements such as ::details-content behave like a
type selector, and can be followed by other pseudo-elements and
pseudo-classes:

  ::details-content::before { color: red }
  details::details-content::after { content: '' }

This matches the css-pseudo-4 spec section on element-backed
pseudo-elements and the behavior of browsers (Firefox, Chrome).

Also allow functional pseudo-classes such as :lang() and :dir() after
::part() and element-backed pseudo-elements, matching upstream selectors
(Firefox bug 1960561).
Comment thread src/lib.rs Outdated
// https://github.com/parcel-bundler/lightningcss/issues/1244
minify_test("::details-content::before {color:red}", "::details-content:before{color:red}");
minify_test("::details-content::after {color:red}", "::details-content:after{color:red}");
minify_test("::details-content::marker {color:red}", "::details-content::marker{color:red}");

@yisibl yisibl Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about ::details-content::before::marker and ::part(foo)::details-content::before?
See https://chromium-review.googlesource.com/c/chromium/src/+/5823774

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this requires a comprehensive consideration of the relationship with other pseudo-elements.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — I've reworked this to comprehensively model the relationships between pseudo-elements, mirroring the current upstream selectors parser in servo/stylo (where Firefox implements ::details-content, and where the :lang()-after-::part() fix landed) together with the rules from the Chromium CL you linked.

Both cases now parse:

  • ::details-content::before::marker — ::before/::after now accept a single following pseudo-element, restricted to a new valid_after_before_or_after() allowlist that currently only contains ::marker (css-pseudo-4, and upstream stylo does the same). ::before::marker, ::after::marker and ::details-content::before::marker parse; ::before::before, ::marker::marker, ::before::details-content etc. remain errors.
  • ::part(foo)::details-content::before — element-backed pseudo-elements are now parsed exactly like ::part() (upstream folds both into one state): they may follow ::part() itself, and accept any pseudo-element/pseudo-class after them, matching the WPT tests from the CL (::part(mypart)::details-content, ::part(mypart)::marker, …).

The resulting matrix:

after allowed rejected
::part(), element-backed (::details-content, ::picker()) pseudo-classes incl. functional (:lang(), :dir()), any pseudo-element incl. element-backed ones ::part(), ::slotted(), combinators, type/class/id/attribute selectors
::before, ::after stateful pseudo-classes (:hover…), ::marker only any other pseudo-element, functional pseudo-classes
other pseudo-elements (e.g. ::marker, ::selection) stateful pseudo-classes any pseudo-element

Other fixes that fell out of this:

  • lightningcss now also marks ::picker() as element-backed, matching Gecko (pseudo_elements.toml). The view-transition pseudo-elements are deliberately excluded: css-view-transitions only allows :only-child after them (Gecko's toml does parse them as element-backed via a default, but that conflicts with the spec's :only-child-only rule, so I kept them non-element-backed).
  • A bare element-backed pseudo-element now sets the HAS_PSEUDO flag, so e.g. ::details-content is no longer eligible for the :is() lowering in rules/mod.rs (:is(::details-content) would be invalid).
  • Fixed the pre-existing test_parsing failures in the selectors crate (DummyParser was missing ::target-text, ::search-text, ::highlight(), ::picker() etc.), so that suite is fully green now.

One deliberate deviation from upstream stylo: after ::marker, the AFTER_BEFORE_OR_AFTER_PSEUDO state is cleared again, so ::before::marker::marker is rejected — upstream keeps the state set and would accept it, which looks like an oversight given their own test_pseudo_duplicate_before_after_or_marker.

Port the pseudo-element relationship model from the upstream servo/stylo
selectors parser:

- Rename AFTER_PSEUDO_ELEMENT to AFTER_NON_ELEMENT_BACKED_PSEUDO and add
  AFTER_BEFORE_OR_AFTER_PSEUDO, matching upstream naming.
- Element-backed pseudo-elements now parse exactly like ::part(): all
  pseudo-classes and pseudo-elements may follow them, and they may also
  follow ::part() itself, e.g. ::part(foo)::details-content::before.
- ::before/::after accept a single following pseudo-element restricted to
  the new valid_after_before_or_after() allowlist, currently only
  ::marker, so ::before::marker and ::details-content::before::marker
  parse while duplicates like ::before::before remain errors.
- Mark ::picker() as element-backed in lightningcss as well, matching
  Gecko. View-transition pseudo-elements stay non-element-backed since
  the spec only allows :only-child after them.
- A bare element-backed pseudo-element now sets the HAS_PSEUDO flag so
  such selectors are excluded from :is() lowering.
- Implement the pseudo-classes/pseudo-elements missing from the selectors
  crate DummyParser to fix the pre-existing test_parsing failures.
@codspeed

codspeed Bot commented Sep 17, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 8 untouched benchmarks


Comparing rorshopping:fix/details-content-before (87c5e92) with master (c6a0c3c)

Open in CodSpeed

rustc 1.96 stopped passing --allow-undefined to wasm-ld
(https://blog.rust-lang.org/2026/04/.../wasm-undefined-symbols), so the
undefined N-API import symbols from napi-sys are now a link error. Pass
the flag explicitly for wasm32-unknown-unknown; the symbols are resolved
by the host at runtime, as before.
@rorshopping

Copy link
Copy Markdown
Author

The failing test-wasm check is unrelated to the changes in this PR — it fails on other recent branches as well (e.g. @mischnic's browserslist branch, Sept 18), and I could reproduce it from this branch on a clean checkout with rustc 1.96.0 and 1.98.1 alike.

Root cause: rustc 1.96.0 stopped passing --allow-undefined to wasm-ld (see the Rust blog post on WebAssembly changes). napi-sys declares the N-API functions as undefined extern "C" symbols that the host (Node) provides at runtime, so yarn wasm:build-release now fails linking lightningcss_node with rust-lld: error: undefined symbol: napi_get_cb_info etc. CI resolves dtolnay/rust-toolchain@stable to the latest stable (1.98.1), which overrides the rust-toolchain.toml pin via RUSTUP_TOOLCHAIN, so the job started failing even though nothing in the repo changed.

Fix (0e2d60d): pass -C link-arg=--allow-undefined explicitly for wasm32-unknown-unknown in the existing .cargo/config.toml rustflags, restoring exactly the flag rustc used to pass. Verified locally on both 1.96.0 and 1.98.1: the module links, exports napi_register_module_v1/register_module, and its 46 napi_* imports are again resolved by the host at runtime. This is also needed on master, since the same failure affects every branch built with a current toolchain.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Selecting ::details-content::before pseudo element errors

2 participants