Mark elements reached by stepping through sibling combinators inside
:has() and use that breadcrumb during generic invalidation walks.
Keep the existing conservative sibling scans for mutations outside
those marked subtrees so nested :is(), :not(), and nesting cases
continue to invalidate correctly.
Also keep :has() eager within compounds that contain ::part(). Those
selectors retarget the remaining simple selectors to the part host, so
deferring :has() there changes which element the pseudo-class runs
against and can make ::part(foo):has(.match) spuriously match.
Add a counter-based sibling-scan test and a regression test covering
the ::part()/ :has() selector orderings.
A DOM mutation under a document that uses any :has() rule currently
walks every ancestor up to the root, invoking invalidate_style_if_
affected_by_has() on each. Most of those ancestors have nothing to
do with :has(), so the work scales linearly with DOM depth.
Introduce an in_has_scope flag on Element, set while evaluating :has()
arguments for invalidation metadata. StyleScope's upward invalidation
walk now terminates at the first element that is neither in :has()
scope nor a :has() anchor, so it only traverses the region where some
:has() rule might actually care about the change.
Keep the existing fast :has() matching paths for normal selector
matching, but bypass them while collecting per-element metadata so the
scope markers still get populated. Node insertion also schedules the
parent for the :has() walk so newly inserted nodes still reach the real
anchor.
The css-has-invalidation suite adds focused coverage for these shapes
and updates the expected counters to reflect the shorter walks.
Teach :has() matching to recognize the common case of a single
descendant compound made only of tag and class selectors. This lets us
stay on the cheap per-element matcher instead of recursing through the
full relative-selector machinery for each candidate descendant.
Keep the optimization limited to that simple selector shape and fall
back to the generic matcher for everything else.
Avoid the generic relative-selector matcher for child-only tag
selectors inside :has(). These selectors can be answered by walking the
anchor's direct children and checking the tag match directly, which
keeps a very hot path cheaper.
Preserve the existing ancestor cache behavior and fall back to the
generic matcher for all other selector shapes.
Introduce a small set of counters on Document that track the work done
while processing :has() invalidation: how often the upward walk runs,
how many elements it visits, how often matches_has_pseudo_class() is
invoked, how well the per-pass result cache performs, and how many
elements transition from clean to needs-style-update.
Expose the counters through internals so tests can assert precise bounds
on the invalidation work triggered by a mutation, which regular
reference tests cannot express.
Add a css-has-invalidation test suite that covers subject-position,
non-subject-position, sibling-combinator, and no-:has() cases. The
baseline tests share a helper script so later coverage can reuse the
same counter-printing path.
The counters are test-only observation; they do not affect style
computation itself.
Pass AbstractElement around as the target for match_compound_selector()
and match_simple_selector().
This has no effect yet, as we currently handle the pseudo-element at the
top layer and then pass down an AbstractElement without it, but it'll
matter once we stop doing that.
Having 3 different functions named `matches()` that work on different
parts of a selector, all of which take a lot of arguments, makes this
harder to reason about than it needs to be. Rename them like so:
- The public "matches an entire selector" function is still matches()
- matches_compound_selector() for compound selectors
- matches_simple_selector() for simple selectors
...instead of separate Element and PseudoElement arguments.
As noted, AbstractElement's constness is weird currently, but that's a
tangent I don't want to go on right now.
This really represents the final pseudo-element, the one that style
actually applies to. We'll want to know that even once we fully support
having multiple pseudo-elements in a selector.
When matching ::part(), only consider shadow trees whose host lies in
the same style scope as the rule. Use MatchContext::rule_shadow_root,
which is already set when collecting ::part() rules, as that scope.
For cross-scope rules, accept a candidate shadow root only if its
host's containing_shadow_root() matches the rule's shadow root: the
rule's scope is the document when rule_shadow_root is null, or that
shadow root when the rule comes from its stylesheet.
The one exception is :host::part(): the rule and the part live in the
same shadow root, so its host is outside the rule's scope and the
comparison would never pass. Allow the check to be skipped for this
case by setting MatchContext::for_host_part_matching when the compound
selector contains :host (directly or inside :is(), which is how CSS
nesting resolves &::part() inside a :host rule).
Run that :host pre-scan only when rule_shadow_root is set. Document
rules never use the same-shadow-root exception; scanning would
otherwise set for_host_part_matching whenever :host appears in the
compound and break cross-scope ::part() matching (e.g. multiple-scopes).
Previously the engine walked all ancestor shadow roots without this
cross-scope check.
The :host::part() pattern allows a shadow DOM's own stylesheet to
style its internal elements that have been exposed via the part
attribute. Previously, ::part() rules were only collected from
ancestor shadow roots (for external part styling), but never from the
element's own containing shadow root.
Fix this by also collecting ::part() rules from the element's own
shadow root in collect_matching_rules(). Additionally, fix the
selector engine's ::part() compound matching to preserve the
shadow_host when the rule originates from the part element's own
shadow root, allowing :host to match correctly in the same compound
selector.
This fixes 2 previously failing WPT tests:
- css/css-shadow-parts/host-part-002.html
- css/css-shadow-parts/host-part-nesting.html
We were conflating elements being the active element and elements being
activated. The :active pseudo class is supposed to be based on whether
an element will have its activation behavior run upon a button being
released.
Store whether an element is being activated as a flag that is set/reset
by EventHandler.
Doing this allows label elements to visually activate their control
without doing a weird paintable hack, so the Labelable classes have
been yeeted.
The :nth-child(), :nth-last-child(), :nth-of-type(), and
:nth-last-of-type() pseudo-classes previously returned false for
elements that have no parent node. This meant that calling
element.matches(":nth-child(1)") on a standalone element created
via document.createElement() would incorrectly return false.
An element without a parent has no siblings, so its index is 1. The
simpler child-indexed pseudo-classes (:first-child, :last-child, etc.)
already handled this case correctly by checking for sibling presence
without requiring a parent. This change brings the nth-* variants in
line with that behavior by guarding the sibling iteration loops on
parent existence rather than returning false early.
Split the structural-change selector metadata into directional bits for
first/last-child and forward/backward positional selectors.
This gives sibling invalidation enough information to distinguish which
side of a mutation can affect an element, instead of treating all
structural selectors as bidirectional.
The selector matching code bypassed the pseudo-element type check for
`::part()` selectors to support compound selectors like
`::part(foo)::before`. This caused bare ::part() declarations to leak
into unrelated pseudo-elements like ::selection.
Fix this by finding any additional pseudo-element beyond ::part() in the
selector and verifying it matches the target.
Remove 11 heavy includes from Document.h that were only needed for
pointer/reference types (already forward-declared in Forward.h), and
extract the nested ViewportClient interface to a standalone header.
This reduces Document.h's recompilation cascade from ~1228 files to
~717 files (42% reduction). Headers like BrowsingContext.h that were
previously transitively included see even larger improvements (from
~1228 down to ~73 dependents).
Previously only input elements were matched. Add placeholder_value()
to HTMLTextAreaElement mirroring the HTMLInputElement API and update
both selector matching code paths to handle textarea.
The :scope pseudo-class inside :has(), :is(), :where(), and :not()
selectors was not receiving the scoping root from outer selector
contexts like Element.closest().
This fix passes the scope parameter through matches_has_pseudo_class(),
matches_relative_selector(), and the :is()/:where()/:not() cases so
that :scope correctly refers to the scoping root element.
This fixes WPT tests for Element.closest() with selector
':has(> :scope)' and for comparing :has(:scope) with :is(:scope)
selectors.
The `:has()` pseudo-class requires traversing descendants (or siblings)
to find matches.
With this change we cache results keyed by `(Selector*, Element*)`
pairs. The cache is stored in `StyleComputer` and cleared at the start
of each style computation pass in `Document::update_style()`.
When `:has()` uses a descendant combinator and we find a match, we also
cache that all ancestors between the matching descendant and the
anchor match. For example with `div:has(.target)`:
```html
<div id="A"> <!-- checking :has(.target) here -->
<div id="B">
<div id="C">
<span class="target"/>
</div>
</div>
</div>
```
When we find `.target` while checking `div#A`, we also cache that
`div#B` and `div#C` match `:has(.target)` since they also contain
`.target`. Later when styling these elements, we get cache hits and skip
traversal.
:heading() now matches based on a computed heading level, which is based
on the level of the tag (h1, h2, etc) and then modified by these two new
attributes.
I'm caching this heading level on HTMLHeadingElement, based on the dom
tree version. That's more invalidation than is actually needed, but it
saves us calculating it over and over when the document hasn't changed.
The failing test cases are:
- Implicit headingreset for modal dialogs which is apparently unspecced
and controversial.
- Not walking the flat tree properly. A flat tree ancestor of a
slot-assigned element is its slot, which is something we don't do
anywhere that I could find. I've made a note to look into this later.
We also don't implement the `ReflectRange` IDL attribute yet, which
means we're not clamping the read value of `headingOffset`.
Corresponds to:
e774e8e318
Per the CSS Namespaces spec, an empty string declared in an
@namespace rule represents no namespace.
Fixes WPT:
- css/css-namespaces/prefix-002.xml
- css/css-namespaces/prefix-003.xml
Implements `::slotted()` to enough extent we could pass the imported WPT
test and make substantial layout correctness improvement on
https://www.rottentomatoes.com/
This reverts e7890429aa and partly reverts
a59c15481f.
The one pseudo-class that accepted multiple of these was :heading(), and
since that got changed to take integers instead, there's no need to keep
this extra complexity (and memory usage) around.
And make it a DOM::Node, not DOM::Element. This makes everything flow
much better, such as spec texts that explicitly mention "focused area"
as the fact that we don't necessarily need to traverse a tree of
elements, since a Node can be focusable as well.
Eventually this will need to be a struct with a separate "focused area"
and "DOM anchor", but this change will make it easier to achieve that.
The upcoming `:heading()` pseudo-class takes multiple comma-separated
An+Bs. Also rename this field as the `:nth-[last-]child()`
pseudo-classes are only a subset of the users.
Which has an optmization if both size of the string being passed
through are FlyStrings, which actually ends up being the case
in some places during selector matching comparing attribute names.
Instead of maintaining more overloads of
Infra::is_ascii_case_insensitive_match, switch
everything over to equals_ignoring_ascii_case instead.
The spec requires us to accept any ident here, not just ltr/rtl, and
also serialize it back out. That means we need to keep the original
string around.
In order to not call keyword_from_string() every time we want to match
a :dir() selector, we still attempt to parse the keyword and keep it
around.
A small behaviour change is that now we'll serialize the ident with its
original casing, instead of always lowercase. Chrome and Firefox
disagree on this, so I think either is fine until that can be
officially decided.
Gets us 2 WPT passes (including 1 from the as-yet-unmerged :dir() test).
Previously we only matched against the first attribute with a given
local name. What we actually want to do is look at each attribute with
that local name in turn and only return false if none of them match.
Also remove a hack for HTML elements in HTML documents, where we would
refuse to match any namespaced attributes. This doesn't seem to be
based on the spec, but we had regressions without it, until now. :^)
Gets us 21 more WPT subtest passes.
The HTML spec gives us a list of HTML attributes that must have their
values compared case-insensitively by default (when the attribute
selector does not specify a case-sensitiveness). However, ifwe have a
namespace, then we are not looking for an HTML attribute, so this
should not apply.
Gets us 8 more WPT subtest passes.
With this, we pass the 8 ref tests in css/selectors/selectors-4/ which
previously failed. This is not technically a full implementation, as we
are supposed to first canonicalize the language range and tag, but that
will require downloading and processing the IANA language subtag
registry:
https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
That's significantly more work, and WPT doesn't seem to test any cases
that require that, so we can leave it for now.