Build a per-anchor Bloom-style filter for :has() argument matching
after an anchor sees a second check for the same traversal scope. The
filter stores salted tag, id, class, and attribute-name hashes from the
child or descendant scope and rejects arguments whose required
identifiers are absent.
This avoids repeatedly walking the same subtree for unrelated :has()
arguments while preserving the single-check case. More complex
direct-child arguments use the descendant scope so hashes from later
descendant compounds cannot cause false rejections.
Keep the filter conservative for quirks-mode class selectors and for
sibling-combinator relative selectors during invalidation metadata
collection. Text tests cover cache-primed misses for both cases.
Track the synthetic pseudo-elements that matched while computing an
originating element's normal style. Store the transient match set as a
bitfield, then copy those bits into ComputedProperties. Use them during
style invalidation to skip pseudo style recomputation when neither the
old nor new originating style matched pseudo rules and no pseudo style
already exists.
This shaves roughly 500 ms off loading the Ladybird GitHub repository.
Materialize synthetic pseudo styles on demand for CSSOM reads so
getComputedStyle(element, "::before") still computes skipped styles when
script asks for them. Add coverage for a universal pseudo selector, and
update style invalidation counter expectations for the reduced work.
Previously, and according to the spec, `a::part(foo)::before` would be a
single CompoundSelector, even though it matches against 3 different
targets. This meant some awkward swapping of targets in the middle of
matching, and in particular it made `::part()` and `::slotted()` quite
hacky, requiring them to track extra data on the MatchContext to then
use later. This was scattered around and difficult to follow.
Partly inspired by Gecko, this commit instead introduces an invisible
PseudoElement combinator. After parsing a selector, we find any
CompoundSelectors that contain a pseudo-element and split them up, so
that each CompoundSelector only has a single target in the end. Where
the pseudo-element was at the start of a CompoundSelector, we insert an
invisible universal selector before it to represent its originating
element.
So now, a CompoundSelector deals with one target, and switching targets
is done at the combinator.
The one inconsistency is that we match the target of ::slotted()
and ::part() in pseudo_element_transition_target(), instead of before
then when processing the SimpleSelector. This is to avoid repeating the
same computations twice.
No outward-facing behaviour changes, though the invalidation metrics
have changed.
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.
...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.
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
This adds visit_edges(Cell::Visitor&) methods to various helper structs
that contain GC pointers, and makes sure they are called from owning
GC-heap-allocated objects as needed.
These were found by our Clang plugin after expanding its capabilities.
The added rules will be enforced by CI going forward.
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.
Implements `::slotted()` to enough extent we could pass the imported WPT
test and make substantial layout correctness improvement on
https://www.rottentomatoes.com/
We achieve this by keeping track of all checked pseudo class selectors
in the SelectorEngine code. We also give StyleComputer per-pseudo-class
rule caches.
The upcoming generated types will match those for pseudo-classes: A
PseudoElementSelector type, that then holds a PseudoElement enum
defining what it is. That enum will be at the top level in the Web::CSS
namespace.
In order to keep the diffs clearer, this commit renames and moves the
types, and then a following one will replace the handwritten enum with
a generated one.
Before this change, checking if fast selector matching could be used was
only enabled in style recalculation and hover invalidation. With this
change it's enabled for all callers of SelectorEngine::matches() by
default. This way APIs like `Element.matches()` and `querySelector()`
could take advantage of this optimization.
Prior to this change, we invalidated all elements in the document if it
used any selectors with :has(). This change aims to improve that by
applying a combination of techniques:
- Collect metadata for each element if it was matched against a selector
with :has() in the subject position. This is needed to invalidate all
elements that could be affected by selectors like `div:has(.a:empty)`
because they are not covered by the invalidation sets.
- Use invalidation sets to invalidate elements that are affected by
selectors with :has() in a non-subject position.
Selectors like `.a:has(.b) + .c` still cause whole-document invalidation
because invalidation sets cover only descendants, not siblings. As a
result, there is no performance improvement on github.com due to this
limitation. However, youtube.com and discord.com benefit from this
change.
Instead of recalculating styles for all nodes in the common ancestor of
the new and old hovered nodes' subtrees, this change introduces the
following approach:
- While calculating ComputedProperties, a flag is saved if any rule
applied to an element is affected by the hover state during the
execution of SelectorEngine::matches().
- When the hovered element changes, styles are marked for recalculation
only if the flag saved in ComputedProperties indicates that the
element could be affected by the hover state.
Resulting in a massive rename across almost everywhere! Alongside the
namespace change, we now have the following names:
* JS::NonnullGCPtr -> GC::Ref
* JS::GCPtr -> GC::Ptr
* JS::HeapFunction -> GC::Function
* JS::CellImpl -> GC::Cell
* JS::Handle -> GC::Root
LibWeb keeps growing and the Web namespace is filling up fast.
Let's put DOM stuff into Web::DOM, just like we already started doing
with SVG stuff in Web::SVG.