Element.cpp still spelled out the :defined pseudo-class invalidation set
when custom element state changed. Move that selector policy into
CustomElementInvalidator.
This keeps Element responsible for the state transition, while
CSS::Invalidation owns the affected selector feature.
HTMLInputElement had two call sites spelling out the same checked and
unchecked pseudo-class invalidation set. Move that selector policy into
FormControlInvalidator.
This keeps the input element responsible for detecting state changes,
while CSS::Invalidation owns the affected selector features.
HTML and SVG link elements both encoded the same pseudo-class list for
hyperlink state changes. Move that CSS policy into LinkInvalidator and
have both call sites delegate to it.
This keeps element-specific code focused on detecting hyperlink state
changes, while the helper owns the affected selector features.
Element.cpp still encoded the CSS consequences of attribute changes:
class/id invalidation keys, pseudo-class triggers, and shadow-host
stylesheet fallout. Move that policy into AttributeInvalidator.
Element now reports attribute changes to the helper and exposes a small
state hook to remember removed attributes while invalidation is pending.
Element exposed a small method that encoded how :has()-affected elements
are marked dirty. Move that policy into CSS::Invalidation alongside the
rest of the :has() mutation invalidation helpers.
This keeps Element focused on DOM state while preserving the existing
subject and non-subject :has() invalidation behavior.
Node.cpp still contained selector-specific policy for sibling and
same-parent-move structural invalidation. Move that logic into
CSS::Invalidation::StructuralMutationInvalidator so DOM mutation code
can delegate structural selector dependency handling.
This is a behavior-preserving extraction. It keeps the existing
previous-sibling walk guard, sibling-distance checks, shadow-root
marking, and ancestor child-needs-style propagation.
Node.cpp still contained the policy for deciding when a DOM mutation
should schedule pending :has() invalidation work. Move that into
CSS::Invalidation::HasMutationInvalidator, next to the mutation feature
collector it depends on.
This keeps DOM mutation code focused on reporting that a mutation
happened, while CSS invalidation code owns the selector-specific checks
for :has() metadata and sibling-combinator sensitivity.
Node.cpp currently knows too much about selector invalidation metadata
when deciding whether subtree mutations can affect :has() selectors.
Pull that logic into CSS/Invalidation/HasMutationFeatureCollector so DOM
mutation code can ask a focused helper instead of inspecting
StyleInvalidationData directly.
This is a behavior-preserving extraction. It keeps the existing
conservative fallbacks for featureless subtree-sensitive selectors and
still uses the existing element property matching helper for
pseudo-class metadata.
In the future we should switch to using a better file format for this,
i.e. one that supports directly pasting CSS grammar production blocks
(https://drafts.csswg.org/css-values-4/#css-grammar-production-block)
and has support for inline comments, but we use JSON for now for
simplicity's sake.
Every DOM mutation that may affect a :has() selector enqueues an
entry in StyleScope keyed by an ancestor node. The entries were
previously stored in a Vector and linearly scanned on every insert to
deduplicate by node. We now use an OrderedHashMap instead, eliminating
the quadratic deduplication.
Per "find flattened slotables", a <slot> whose root is a shadow root is
recursed through, not appended to the result. ::slotted() in an outer
shadow must therefore not match such an intermediate slot.
Fixes the gallery on Reddit comment pages: a re-slotted <slot> was
picking up `::slotted(:not([slot])) { display: grid }` from the inner
shadow, which made the <ul> size to its content rather than the flex
container, leaving the carousel's "next" button with a 0px translate.
Previously, `Document::notify_css_background_image_loaded()` walked the
entire `PaintableBox` subtree and cleared each box's paintable cache
whenever any CSS image finished loading.
Replace this with per-image observers owned by the layout node. During
`apply_style`, each node registers as an `ImageStyleValue::Client` for
the images its style references. On load, only the affected layout
node's paintables are invalidated.
CSSRuleList::evaluate_media_queries previously compared
CSSMediaRule::condition_matches() (which reads MediaQuery::m_matches,
default false) against the freshly-computed result. A brand-new @media
rule whose condition matches would therefore look like a false->true
flip the very first time it was evaluated, the same shape as the
CSSStyleSheet outer-MediaList bug fixed in the previous commit.
In practice all known paths that introduce a new @media rule
(StyleSheetList::add_sheet, AdoptedStyleSheets on_set,
CSSStyleSheet::invalidate_owners, CSSImportRule::set_style_sheet) call
through CSSStyleSheet::evaluate_media_queries eagerly and absorb the
flip before the next Document::evaluate_media_rules pass, so this
change does not move counters in the existing tests. It does make the
inner-@media handling consistent with the outer one, and protects any
future path (e.g. CSSGroupingRule::insert_rule into a nested rule
list) where a new @media rule might be evaluated for the first time
during the regular media-rule pass.
Track per-rule whether evaluate has been called yet via a sticky
m_did_evaluate flag on CSSMediaRule, and only record a flip on
subsequent evaluations.
CSSStyleSheet::evaluate_media_queries previously flagged "no recorded
result yet" as a match-state change, so every freshly-loaded sheet
fired MediaQueryChangedMatchState on the first pass through
Document::evaluate_media_rules. For sheets added through
adoptedStyleSheets that piled an extra full-document style invalidation
on top of the AdoptedStyleSheetsList one, recomputing every element a
second time for nothing.
Drop the !has_value() leg so the very first evaluation establishes the
baseline silently. The sheet's rules already entered the cascade through
StyleSheetListAddSheet, AdoptedStyleSheetsList, or invalidate_owners,
each of which performs its own targeted invalidation.
Two callers relied on the implicit "first eval forces a refresh"
behavior to handle freshly-mutated state:
- invalidate_owners resets m_did_match, then leans on the next eval to
repopulate it. With the new semantics it must also re-evaluate the
sheet eagerly so MediaList::matches() and inner @media state are
fresh before the next rule cache build reads them.
- The adoptedStyleSheets on_set callback didn't evaluate at all,
relying on Document::evaluate_media_rules to populate
MediaList::m_matches. That worked accidentally because the false
flip retriggered invalidate_rule_cache after the matches had been
populated. Mirror StyleSheetList::add_sheet by evaluating the sheet
at adopt time so the rule cache build sees the correct match state
even if it runs first (e.g. via a :has() invalidation pass).
When a :has() mutation is known to come from a specific subtree, use
that subtree as the mutation root while walking observed ancestors.
Before dirtying an anchor and its non-subject descendants, check whether
any cached :has() rule for that anchor can observe the changed subtree.
This keeps unrelated descendant mutations from invalidating every rule
that merely contains :has().
Moving a node within the same parent changes sibling and positional
relationships, but it does not make every descendant of the moved node
need a fresh computed style. Handle this as a structural mutation at
the old and new sibling edges and dirty only the moved root and the
affected ancestors, instead of marking the entire moved subtree.
Factor the existing previous- and next-sibling structural invalidation
out of Node::invalidate_style() into invalidate_structurally_affected_-
siblings(), and pull the ancestor child-needs-style-update walk into
mark_ancestors_as_having_child_needing_style_update(). The new
invalidate_style_after_same_parent_move() reuses both helpers.
Whether the moved root itself needs its own style recomputed depends
on whether any selector matched against it (or against a descendant)
relied on its position in the sibling list. Track that via two new
sticky bits on Element, set during selector matching:
- m_affected_by_structural_pseudo_class_in_non_subject_position
- m_affected_by_sibling_combinator_in_non_subject_position
Both are write-once (sticky) because matching descendants can set them
while we're not currently re-matching this element's own selectors;
keeping them set is conservative and avoids stale descendant style.
When neither bit is set and the element only carries subject-position
positional/sibling/has() involvement, we just dirty the root and skip
its descendants.
Rebaseline same-parent-move-root-only and the structural-feature filter
counters to reflect the new path. Matching behavior is unchanged.
Track the simple selector features that appear inside :has() arguments
on each StyleScope, then consult that metadata before scheduling an
ancestor walk for a structural mutation. If the mutated subtree has no
tag, id, class, attribute, or pseudo-class feature that any cached
:has() argument cares about, skip the walk entirely.
Stay conservative for featureless-sensitive arguments such as :has(*),
:has(:not(...)), :has(:empty), and child-index pseudos: an unfeatured
node can still start or stop matching there. Track that case via a new
has_selectors_sensitive_to_featureless_subtree_changes flag on
StyleInvalidationData and fall back to the old conservative walk.
Stay conservative for pseudo-classes the subtree filter cannot probe
(:focus, :hover, validation pseudos). Move :default out of the set of
trackable feature pseudo-classes for the same reason; it now triggers
the conservative walk where it previously recorded metadata.
Tag and attribute names are stored lowercased, so for non-HTML elements
(SVG, MathML) treat lowercased matches as scheduling hints only; the
actual :has() match still goes through case-sensitive selector matching.
Test counter expectations are rebaselined to reflect the skipped walks
and reduced recomputations. Matching behavior is unchanged.
Share the style cache for shadow roots whose only active author sheet is
the same constructed stylesheet. Matching already carries the effective
shadow root separately, so the cache can be reused while selectors such
as :host and ::slotted() still evaluate against each consuming shadow
root.
Keep the optimization conservative by falling back to the existing
per-scope cache whenever the shadow root has multiple active sheets, a
non-constructed sheet, or a page user stylesheet. Drop the shared cache
when the stylesheet rules or media query match state change.
Add coverage for two shadow roots adopting the same constructed sheet,
including :host, ::slotted(), and replaceSync() invalidation.
Keep cached MatchingRule entries independent from the shadow root that
owns the rule cache. Thread the effective rule shadow root through style
matching as transient state instead, so a rule cache can later be shared
by multiple scopes without copying every cached rule.
This preserves the existing matching behavior by deriving the effective
rule root from each cache lookup site. Pseudo-class invalidation already
operates on a single style scope, so it no longer needs a per-rule scope
filter.
CSSOM declaration mutations on style rules and nested declarations
do not change selector buckets, layer ordering, or keyframe sets stored
in rule caches. Keep those caches valid and only invalidate affected
styles, while leaving keyframe declaration mutations on the existing
cache-invalidating path.
Add coverage showing a CSSOM style declaration mutation is observed
through an already-built rule cache.
CSSFontFaceRule inherited from CSSStyleSheet::Subresource, which made
each face a critical subresource of its parent stylesheet. New
subresources start in the Unloaded state, and the stylesheet's
loading_state() treats Unloaded as Loading.
When a @font-face declares a unicode-range and no codepoint in that
range is ever rendered, FontComputer only registers the face for
matching and never calls FontFace::load() on it. The face stays
Unloaded, so the parent stylesheet stays stuck reporting Loading,
which keeps HTMLLinkElement's load-event delayer alive and prevents
the document load event from firing. HTMLParserEndState then times
out in phase 2 (WaitingForLoadEventDelay) after 15 seconds.
Decouple @font-face from the stylesheet's loading state. Font loading
remains tracked by FontFaceSet, which is the correct place.
ImageSetStyleValue::set_style_sheet() previously stored the style sheet
on itself but didn't propagate to its candidate images. As a result,
candidates were never registered as pending image resources, so their
fetches didn't start until layout time and didn't delay the document's
load event.
This caused css-image-set-background-type.html to be flaky: under
load, the screenshot could be captured before the selected SVG
candidate had finished decoding, producing an empty box instead of
the expected color.
Propagate set_style_sheet() to each candidate image whose type()
filter does not exclude it, mirroring StyleValueList and
ShorthandStyleValue. The candidates now register themselves as
pending so SharedResourceRequest's load event delayer correctly
delays the load event until decoding completes.
build_invalidation_sets_for_simple_selector ignored ::slotted()
pseudo-element selectors entirely. As a result, the invalidation
plans built for a shadow scope didn't list class, attribute, or
pseudo-class properties referenced inside ::slotted(), so changing
those properties on a slottable couldn't enqueue an invalidation
plan for the rule.
Recurse into ::slotted()'s compound argument and feed each simple
selector through the same invalidation-set builder.
collect_properties_used_in_has only inspected pseudo-class argument
selectors. With ::slotted(.x:has(.descendant)) rules, the property
references inside the :has() argument were therefore never recorded
as :has()-affecting, so attribute and state changes on a slottable's
descendants couldn't enqueue an invalidation plan that covered the
::slotted() rule.
Recurse into the ::slotted() compound argument so the :has() metadata
maps include the properties used inside.
Two related fixes that together let :host(...:has(...)) and
::slotted(.x:has(...)) rules re-evaluate when their light-DOM input
changes:
* StyleScope::collect_selector_insights only recursed into
pseudo-class argument selectors. The :has() inside a
::slotted(...) compound argument was therefore invisible to a
shadow scope's insights, so may_have_has_selectors() reported
false and Node::invalidate_style skipped the :has() walk for that
scope. Walk the ::slotted() argument selector through the same
insight collection.
* Element::invalidate_style_if_affected_by_has only set
needs_style_update for elements in subject position. Inside
::slotted(.x:has(...)) and :host(...:has(...)) the rule's
selector subject is the slot or host, but the styled element is
this element; when the :has() result flips, this element's own
computed style must be recomputed. Mark it dirty in the
non-subject branch as well.
When computing pseudo-element style and no pseudo-element rules match,
StyleComputer was returning early without clearing the cascaded and
custom property data on the AbstractElement. As a result,
getComputedStyle() on the pseudo-element kept exposing values from a
previous matching state.
Clear both before bailing so a transition from matched to unmatched
leaves the pseudo-element in a clean state.
Add an abstract image style value for image-set() and parse both the
standard and -webkit-prefixed spellings through the existing <image>
value path. The parser accepts URL and string image candidates,
optional resolution descriptors, and type() filters.
Track attr-taint through substituted component values so image-set()
candidates using attr()-derived URL-producing tokens are rejected when
resolved for URL-using properties.
Update the relevant WPT baselines now that image-set() parsing is
supported in additional value contexts.
Implement the width filtering step of the font matching algorithm.
Without it, system font providers that group all widths under one
family could return a condensed variant for font-width: normal,
producing visibly narrower text.
FontFace.load() set the face's status to "loading" but never switched
the containing FontFaceSets to "loading" or appended to their
`[[LoadingFonts]]` lists. The load-completion handler then found
`[[LoadingFonts]]` already empty and fired switch-to-loaded after the
first face finished, resolving `document.fonts.ready` while faces in
the same set were potentially still loading.
Previously, we were accidentally creating temporary copies of custom
property maps on both sides of a ternary in `compute_style_impl()`. We
now bind to a static empty sentinel instead so the reference binds
directly to `own_values()` without copying.
Currently there are multiple style values which are essentially the same
thing, a function holding a value, just with different names. This
commit adds a generic style value to replace them with and the following
commits will do so.
HTMLLinkElement::removed_from() used `old_root` to find the
StyleSheetList to remove the link's stylesheet from. That's wrong
when the link element lives inside a shadow tree that is itself
nested within a larger removed subtree: Node::remove() hands every
shadow-including descendant the outer subtree's root as `old_root`,
not the descendant's own containing root. So we'd look in the
document's list while the sheet was actually in the shadow root's
list, failing the did_remove VERIFY in StyleSheetList::remove_sheet.
Fix by using the sheet's own owning-root tracking. A link-owned sheet
always has exactly one owning document or shadow root (only constructed
stylesheets can be adopted, and link sheets are never constructed), so
we can just read that entry.
Also make owning_documents_or_shadow_roots() return by const reference
instead of copying the HashTable on every call, which benefits existing
iterating callers too.
Fixes a crash on https://nytimes.com/.
Avoid broad document invalidation when adding or removing ordinary
document-owned or shadow-owned stylesheets. Reuse the targeted
StyleSheetInvalidation path for style rules, including shadow-host
escapes, pseudo-element-only selectors, and trailing-universal cases.
Keep the broad path for sheet contents whose effects are not captured
by selector invalidation alone, including @property, @font-face,
@font-feature-values, @keyframes, imported sheets, and top-level @layer
blocks. Broad-path shadow-root sheets still reach host-side consumers
through their active-scope effects.
Toggling CSSStyleSheet::disabled previously cleared the cached media
match bits and reloaded fonts, but never informed the owning documents
or shadow roots that style resolution was now stale. Worse, the IDL
binding for the disabled attribute dispatches through a non-virtual
setter on StyleSheet, so any override on CSSStyleSheet was bypassed
entirely.
Make set_disabled() virtual so the CSSStyleSheet override actually runs,
snapshot the pre-mutation shadow-root stylesheet effects before flipping
the flag, and hand them to invalidate_owners() so a disable that strips
the last host-reaching rule still tears down host-side style correctly.