Commit Graph

1463 Commits

Author SHA1 Message Date
Andreas Kling
007dc28d16 LibWeb: Move shadow root style invalidation into the helper
Element::set_shadow_root directly selected the style invalidation reason
used when a shadow root changes. Move that mapping into
CSS::Invalidation::ElementStateInvalidator.

Element still owns the shadow-root state transition and the required
layout-tree invalidation. CSS invalidation now owns the style dirtiness
for that state change.
2026-04-29 15:47:23 +02:00
Andreas Kling
b0bb2bd0a8 LibWeb: Move active state invalidation into the helper
Element::set_being_activated directly selected the style invalidation
reason used for :active changes. Move that mapping into
CSS::Invalidation::ElementStateInvalidator.

The element continues to own its activation state. CSS invalidation now
owns the style invalidation work associated with changing that state.
2026-04-29 15:47:23 +02:00
Andreas Kling
79c32f88d2 LibWeb: Move pending :has() invalidation into the helper
Document.cpp still flushed pending :has() invalidation by walking the
document and shadow-root style scopes directly. Move that CSS-specific
flush into CSS::Invalidation::HasMutationInvalidator.

Document continues to own the flag that says a :has() flush is needed.
The helper now owns the style-scope work needed to invalidate elements
affected by pending :has() mutations.
2026-04-29 15:47:23 +02:00
Andreas Kling
b8c2469566 LibWeb: Move media query style invalidation into a helper
Document.cpp still handled CSS fallout from stylesheet media query match
changes directly. Move active stylesheet evaluation, rule-cache
invalidation, shadow-root fallout, and slot propagation into the CSS
invalidation helper.

Document continues to decide when media queries should be evaluated.
The helper now owns the style invalidation consequences when stylesheet
media queries change match state.
2026-04-29 15:47:23 +02:00
Andreas Kling
0a938fdd51 LibWeb: Move slot style propagation into the helper
Document.cpp still knew how style changes on a slot propagate to
assigned light-DOM nodes. Move that flat-tree inheritance invalidation
into CSS::Invalidation::SlotInvalidator.

The style update walk continues to decide when an element's style
changed. The helper now owns the ::slotted() consequence of dirtying
assigned slottables for a changed slot.
2026-04-29 15:47:23 +02:00
Andreas Kling
98b13da3b4 LibWeb: Move adopted stylesheet invalidation into a helper
AdoptedStyleSheets.cpp still handled CSS-side fallout from adopted sheet
list mutations directly. Move sheet attachment and media-query setup to
a CSS invalidation helper, along with rule-cache invalidation.

The observable array callbacks continue to validate JS values and
same-document construction rules. The helper now owns the CSS work for
when a constructed stylesheet becomes visible to, or is removed from, a
Document or ShadowRoot.
2026-04-29 15:47:23 +02:00
Andreas Kling
c191d51af1 LibWeb: Move slotted style invalidation into a helper
Slottable.cpp still handled the style invalidation fallout from slot
assignment changes directly. Move that ::slotted()-related policy into
CSS::Invalidation::SlotInvalidator.

Slot assignment remains DOM bookkeeping. The helper now owns the
choice to dirty element slottables when they gain or lose assignment
to a slot.
2026-04-29 15:47:23 +02:00
Andreas Kling
bc0059cfd5 LibWeb: Move text directionality invalidation into the helper
CharacterData.cpp still handled the style invalidation fallout from text
mutations under dir=auto ancestors directly. Move that behavior into the
CSS language invalidation helper.

CharacterData continues to own text replacement and layout text updates.
The helper now owns the inherited :dir() restyle and :has(:dir(...))
ancestor scheduling that can follow from text content changes.
2026-04-29 15:47:23 +02:00
Andreas Kling
1ba3ec6ae7 LibWeb: Move part style invalidation into a helper
Element.cpp still handled the style invalidation fallout from part and
exportparts attribute changes directly. Move that ::part-related policy
into CSS::Invalidation::PartInvalidator.

Element continues to update the DOM token state for part attributes. The
helper now owns the style dirtiness for elements targeted through ::part
and for shadow-tree descendants exposed through exportparts.
2026-04-29 15:47:23 +02:00
Andreas Kling
b0effb3167 LibWeb: Move language style invalidation into a helper
Element.cpp still handled the CSS invalidation fallout from dir and lang
attribute changes directly. Move the descendant style dirtiness and
:has(:dir/:lang) ancestor scheduling into CSS::Invalidation.

Element continues to parse and store the DOM-facing attribute state. The
new helper owns the inherited style invalidation behavior that follows
from language and directionality changes.
2026-04-29 15:47:23 +02:00
Andreas Kling
3d8fa1ed26 LibWeb: Move node style invalidation into a helper
Node.cpp still contained the CSS policy for full style invalidation and
property-based invalidation plans. Move that logic into
CSS::Invalidation::NodeInvalidator.

Node remains the public DOM entry point for callers that need to
invalidate style. The helper now owns the :has() metadata probing,
style-scope plan lookup, and subtree/sibling invalidation scheduling.
2026-04-29 15:47:23 +02:00
Andreas Kling
61a18d91d6 LibWeb: Move pseudo-class state invalidation into a helper
Document.cpp contained the CSS rule-cache matching used to decide which
elements need style updates when hover, focus, or target state changes.
Move that logic into CSS::Invalidation::PseudoClassInvalidator.

Document still owns the current state slots and chooses when a state
transition happens. The helper now owns the selector matching and
recursive invalidation pass for those pseudo-class transitions.
2026-04-29 15:47:23 +02:00
Andreas Kling
6069bcdcc7 LibWeb: Move StyleInvalidator into CSS invalidation
StyleInvalidator applies CSS invalidation plans and matches selector
features while walking DOM nodes. Move the class from DOM into the
CSS::Invalidation namespace alongside the other invalidation helpers.

Document still owns the invalidator and DOM nodes still expose the state
that gets marked, but the policy for applying invalidation plans now has
a home with the rest of the CSS invalidation code.
2026-04-29 15:47:23 +02:00
Andreas Kling
4b3abc6958 LibWeb: Move invalidation set matching into a helper
Element.cpp still contained the CSS logic for deciding whether an
invalidation set references features present on an element. Move that
matcher into CSS::Invalidation::InvalidationSetMatcher.

The helper uses Element's public API for classes, id, attributes,
pseudo-class state, and removed-attribute tracking. This keeps Element
focused on DOM state while CSS::Invalidation owns selector feature
matching.
2026-04-29 15:47:23 +02:00
Andreas Kling
d7f5939e46 LibWeb: Move custom element state invalidation into a helper
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.
2026-04-29 15:47:23 +02:00
Andreas Kling
eeab3671c2 LibWeb: Move attribute style invalidation into a helper
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.
2026-04-29 15:47:23 +02:00
Andreas Kling
85e33738f5 LibWeb: Move :has() element invalidation into the helper
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.
2026-04-29 15:47:23 +02:00
Andreas Kling
95eb41092c LibWeb: Move structural mutation invalidation into a helper
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.
2026-04-29 15:47:23 +02:00
Andreas Kling
e4e3c46837 LibWeb: Move :has() mutation scheduling into a helper
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.
2026-04-29 15:47:23 +02:00
Andreas Kling
ea64c5e147 LibWeb: Move :has() mutation checks into a helper
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.
2026-04-29 15:47:23 +02:00
Andreas Kling
e9ed125bbe LibWeb: Don't fan sibling structural invalidation into descendants
When an insert or remove flips a sibling's match for :first-child,
:last-child, :nth-child, :nth-of-type, or a + / ~ combinator, only the
sibling's own match changes. Descendants only need rechecking when a
selector ties them to the sibling's position.

invalidate_structurally_affected_siblings used to mark the affected
sibling's entire subtree, so a sibling with N descendants turned a
match-state flip into N+1 element recomputes. Reuse the per-element
flags introduced for the same-parent-move optimization: when neither
affected_by_structural_pseudo_class_in_non_subject_position nor
affected_by_sibling_combinator_in_non_subject_position is set, mark
only the sibling root, and let the existing inherited-update path
reach descendants if the sibling's own style changes.

Rebaseline the impacted tests. styleInvalidations gains one per
affected sibling because set_needs_style_update increments the counter
(entire-subtree marks did not), but elementStyleRecomputations falls
accordingly.
2026-04-29 04:53:14 +02:00
Tim Ledbetter
e495db44d5 LibWeb: Notify only affected layout nodes when a CSS image loads
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.
2026-04-29 04:33:35 +02:00
Aliaksandr Kalenik
4762c4fa5c LibWeb: Add incremental HTML parsing
Introduce IncrementalDocumentParser, which streams the response body
through a TextCodec::StreamingDecoder into the HTMLTokenizer one chunk
at a time. The tokenizer pauses when it runs out of input and resumes
once the next chunk is appended; when the body closes we close the
tokenizer's input stream so it can finish the parse.

DocumentLoading routes HTML responses through the new parser instead of
buffering the full body before handing it to HTMLParser.
2026-04-29 04:12:44 +02:00
Aliaksandr Kalenik
f499edefae LibWeb: Track whether HTMLParser is script-created
Add a ScriptCreatedParser flag plumbed through HTMLParser's constructor
and create_for_scripting(). Only document.open()'s parser sets it to
Yes. Document::close() step 3 now checks is_script_created() so it
correctly skips parsers that weren't created via document.open(),
matching the spec.

Previously the check was just `if (!m_parser)`, which incorrectly let
document.close() insert an EOF into a network-driven parser. The bug
was mostly latent because the network parser used to finish quickly,
but it matters once the network parser stays alive for the duration of
a streamed parse.
2026-04-29 04:12:44 +02:00
Andreas Kling
f10f651e49 LibWeb: Don't treat first media-query evaluation as a flip
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).
2026-04-28 19:06:29 +02:00
Andreas Kling
ce5d0bdfc7 LibWeb: Narrow :has descendant invalidation fanout
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().
2026-04-28 15:34:49 +02:00
Andreas Kling
356a369aa6 LibWeb: Avoid descendant recomputes for same-parent moves
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.
2026-04-28 15:34:49 +02:00
Andreas Kling
11d9d4a7a5 LibWeb: Match attribute invalidation by presence and recent removal
Element::includes_properties_from_invalidation_set() previously
short-circuited and returned true for the id and class attributes,
because the parsed id and class-name state was treated as the source of
truth and the attribute presence check could disagree with it. That
shortcut over-invalidated for any [class] or [id] selector even when
neither attribute had ever been touched on the element.

Drop the special case and answer attribute presence queries the same
way for every attribute: the element either currently has the attribute,
or has had it removed earlier in this style update batch.

To handle the just-removed case, track the set of attribute names that
were removed since the last invalidation pass on a new per-Element
Vector<FlyString, 1> m_removed_attributes_for_style_invalidation. The
StyleInvalidator clears the vector for each element it visits during
perform_pending_style_invalidations, so it only ever holds names from
the current batch.

Rebaseline the affected attribute-presence tests; counters drop because
elements that never had id/class no longer match [class] / [id]
invalidations.
2026-04-28 15:34:49 +02:00
Andreas Kling
9fae2bcff9 LibWeb: Avoid unrelated structural :has invalidation
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.
2026-04-28 15:34:49 +02:00
Andreas Kling
eed76b3619 LibWeb: Track rule scope outside MatchingRule
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.
2026-04-28 13:07:52 +02:00
Andreas Kling
118802b3f0 LibWeb: Scope media rule cache invalidation
Invalidate only the style scope whose media rules changed instead
of throwing away every shadow root rule cache whenever any active
stylesheet changes media query match state. Shadow-root stylesheet
changes still dirty the host side because :host and ::slotted
selectors can affect nodes outside the shadow tree.

When scoped invalidation leaves dirty descendants in a shadow root,
preserve the host ancestor chain so the document style update walk
reaches them before forced layout.

Add coverage that a matching media rule introduced in one shadow tree
does not broadly invalidate a page full of unrelated shadow roots,
and that a dirty shadow root is updated before layout is forced.
2026-04-28 09:49:50 +02:00
Andreas Kling
97b6ea8bbe LibWeb: Honor skipped children in shadow-including traversal
When a shadow-including traversal callback returns
SkipChildrenAndContinue for the inclusive node, do not continue
into that node's shadow-including descendants.
2026-04-27 16:22:11 +02:00
Aliaksandr Kalenik
53fa1b19f1 LibWeb: Make external SVG script fetches async
Replace the spin_until in SVGScriptElement::process_the_script_element
with an async fetch that mirrors HTMLScriptElement's mark_as_ready
pattern. External SVG scripts now fetch and execute asynchronously,
matching Chromium's behavior.

For HTML-embedded SVG scripts, the parser pauses via the existing
schedule_resume_check infrastructure, extended to support SVG scripts
through a new pending_parsing_blocking_svg_script slot on Document.
For top-level XML/SVG documents, scripts execute when their fetch
completes; the load event is delayed via DocumentLoadEventDelayer which
the existing XMLDocumentBuilder::document_end already waits on.
2026-04-27 03:04:07 +02:00
Aliaksandr Kalenik
057fb2b0c5 LibWeb: Run unloading cleanup steps during document unload
The HTML unload algorithm runs the old document's unloading document
cleanup steps before deciding whether the document can be destroyed.
Document::unload() still had that step as a FIXME, so cross-spec
cleanup could be skipped when test-web reused a view for navigation.

The flake depended on test order. The fullscreen sibling WPT requests
fullscreen for one element, then for its sibling, and finishes with that
second element still being the document's fullscreen element. test-web
then navigates the same view to the next test. Since unload skipped the
cleanup hook, the old document kept its fullscreen/top-layer state past
that document boundary.

form-reset-callback.html is deterministic when run by itself. The first
two subtests call the [CEReactions] form.reset() API and observe the
callback synchronously. The failing subtest calls resetButton.click(),
which reaches form reset through input activation behavior and expects
the formResetCallback reaction to run at the next microtask checkpoint.
With the reused view still polluted by the previous fullscreen document,
that checkpoint could run without observing the callback, so only that
subtest failed.

Run the unloading cleanup steps during unload so navigating away from a
fullscreen document clears that state before the next document starts.
2026-04-27 00:44:42 +02:00
Aliaksandr Kalenik
70ac025eff LibWeb: Implement the speculative HTML parser
When the HTML parser blocks on a synchronous external script, run a
separate tokenizer over the unparsed input and issue speculative fetches
for the resources it finds (script src, link rel=stylesheet|preload, img
src), with <base href> tracking and template/foreign-content skipping.

Also fills in the previously-stubbed "consume a preloaded resource"
algorithm and the document's "map of preloaded resources", so that
<link rel="preload"> followed by a matching consumer deduplicates to
a single fetch.
2026-04-26 18:48:29 +02:00
Tim Ledbetter
6bb037aec7 LibWeb: Skip backward sibling invalidation when no child needs it
We now track when a parent has a child affected by a backward structural
pseudo-class. These are selectors whose match result for an element can
depend on siblings after that element, such as `:last-child`,
`:only-child`, `:last-of-type`, `:only-of-type`, `:nth-last-child`, and
`:nth-last-of-type`.

When inserting or removing a node, previous siblings only need style
invalidation if one of them was matched against such a selector. Use the
parent-level flag to skip the previous-sibling walk when no child under
that parent can be affected.

This saves a lot of invalidation work on sites that insert a lot of
nodes into the DOM via JS.
2026-04-26 16:14:43 +02:00
Tim Ledbetter
08dd93a8e0 LibWeb: Count previous-sibling visits during style invalidation
Add a `previousSiblingInvalidationWalkVisits` counter that increments
once per element examined during the previous-sibling walk in
`invalidate_style` on `NodeInsertBefore` and `NodeRemove`. This can be
expensive and the next commit introduces an optimization that prevents
this work being done unnecessarily
2026-04-26 16:14:43 +02:00
Aliaksandr Kalenik
b1ccab81ad LibWeb: Replace spin_until in HTMLParser::handle_text with async resume
Spinning a nested event loop to wait for a parser-blocking script blocks
the calling thread, can deadlock, and creates reentrancy hazards. Switch
to an event-driven pause/resume model, mirroring the prior
HTMLParserEndState refactor (df96b69e7a).

Three WPT document.write tests flip from Fail to Pass and are
rebaselined: all write an external script via document.write() followed
by inline content. With spin_until, control did not return to the caller
of document.write() between writing the script and observing its effects
so the test's order assertions saw a different sequence than the spec
mandates.
2026-04-26 10:44:45 +02:00
Andreas Kling
89e5deb7b9 LibWeb: Centralize style scope lookup for invalidation
Several invalidation paths need to consider not only a node's own root
scope, but also shadow scopes that can observe the node through :host(),
::slotted(), or ::part() selectors. Each caller open-coded that
traversal, which made the dir/lang and dir=auto fixes carry the same
shadow-boundary logic in multiple places.

Add Node helpers for resolving a node's style scope and for visiting
every style scope that may observe that node. Use them from the
property, child-list, dir/lang, and dir=auto invalidation paths, and
share the same style-scope lookup with DOM::AbstractElement and
Layout::NodeWithStyle.
2026-04-26 10:40:58 +02:00
Andreas Kling
2ff002b0bb LibWeb: Invalidate dir=auto descendant styles on text mutation
When the text content under a dir=auto ancestor changes, the
ancestor's effective directionality can flip and inherits down to
every descendant. The previous commit only marked the ancestor
itself dirty, so descendants whose :dir()-dependent style depended
on the ancestor's direction were left rendered against the old
value.

Walk the ancestor's shadow-including inclusive subtree and mark
every element for style update so the new direction propagates
through the cascade.
2026-04-26 10:40:58 +02:00
Andreas Kling
ecd5c7d82e LibWeb: Recompute dir=auto ancestor style on character data mutation
dir=auto resolves an element's effective directionality from its text
content, so any ancestor with dir=auto can flip its :dir() match
when descendant text changes. CharacterData::replace_data updated the
text node and its layout but never propagated this to the
direction-dependent ancestor styles or to :has(:dir(...)) ancestors,
leaving those rules stuck on the previous direction.

Walk up parent_element from the changed text node and, for each
dir=auto ancestor, mark it for style update and schedule the :has()
walk on every reachable scope so :has(:dir(...)) above can also
re-evaluate.
2026-04-26 10:40:58 +02:00
Andreas Kling
5ff832081d LibWeb: Make :dir() and :lang() invalidation cross shadow boundaries
Three related fixes around how dir and lang attribute changes flow
through shadow boundaries:

* Element::lang() looked up the host through parent_element(), which
  is null for elements directly inside a shadow root. The shadow
  root -> host fallback in step 3 therefore never fired and
  shadow-tree elements never inherited their host document's
  language. Walk through parent() instead so the shadow root case is
  detected.

* The dir/lang attribute_changed handlers each marked descendants
  for style update, but neither one descended through shadow
  boundaries via for_each_shadow_including_inclusive_descendant the
  same way (and only one cleared the cached language). Merge both
  handlers so dir and lang share the same shadow-including descendant
  walk.

* :has(:dir(...)) and :has(:lang(...)) on ancestors aren't keyed on
  any property the regular invalidation plan tracks, so the :has()
  ancestor walk has to be scheduled explicitly. Schedule it on the
  document scope and on every shadow scope reachable from this
  element via parent_or_shadow_host.
2026-04-26 10:40:58 +02:00
Andreas Kling
43f2021484 LibWeb: Recompute shadow part style when exportparts changes
When a shadow host's exportparts attribute changes, elements with
part tokens inside its shadow tree may newly become or stop being
targets of ::part() rules in the outer scope. Mark every such
descendant for style update so the new exposure is reflected.
2026-04-26 10:40:58 +02:00
Andreas Kling
48c4a254a6 LibWeb: Recompute style when an element's part attribute changes
::part(...) rules in the outer scope target shadow descendants by
their part-name tokens. When an element's part attribute changed,
m_parts and the IDL part list were updated but the element itself
was never marked for style update, so previously-matched ::part()
rules kept applying or new matches stayed unevaluated.
2026-04-26 10:40:58 +02:00
Andreas Kling
996080eeea LibWeb: Recompute style for slottables when slot assignment changes
assign_slottables only updated each slottable's m_assigned_slot
pointer. The slottable's match against ::slotted(...) rules is
determined by whether it's currently assigned to a slot, so gaining
or losing an assignment must trigger a fresh style computation.
Without that, mutations like changing a slot's name or a slottable's
slot attribute left the slottable rendered against stale ::slotted()
rules.

Mark the slottable element for style update both when it loses an
assignment and when it gains one.
2026-04-26 10:40:58 +02:00
Andreas Kling
7d6f77bdf3 LibWeb: Recompute style for descendants when dir/lang changes
The dir and lang attributes inherit, so descendants' :dir() and
:lang() matches and any direction-dependent layout/text depend on
ancestor values. Element::attribute_changed updated the m_dir field
and refreshed cached language values, but never marked descendants
for style recomputation, so existing styled descendants kept their
old :dir()/:lang() results.

Mark every descendant for style update when dir or lang changes.
2026-04-26 10:40:58 +02:00
Andreas Kling
3cbe6bc89a LibWeb: Schedule attribute :has() walks on outer scopes too
The property-based Node::invalidate_style only added inner shadow
scopes (the element's own shadow and enclosing hosts' shadows). When
the mutated element lived inside a shadow tree, mutations could not
reach ::part(...:has(...)) rules in the outer document or outer
shadow root because those scopes were never collected.

Walk parent_or_shadow_host and add the document scope (or each
crossed shadow root scope) when the ancestor's root differs from
this element's root, alongside the inner shadow scopes already
collected.
2026-04-26 10:40:58 +02:00
Andreas Kling
60113a3be2 LibWeb: Schedule attribute :has() walks on enclosing shadow scopes
The property-based Node::invalidate_style only collected the element's
own shadow scope (when the element itself was a shadow host), not the
shadow scopes of enclosing hosts. So a class or attribute change on a
light-DOM descendant of a shadow host could not flip
:host(...:has(.descendant)) rules in that host's shadow root.

Walk parent_or_shadow_host from the mutated element and add every
enclosing shadow root's StyleScope to the set we check :has()
metadata against, schedule walks on, and run invalidation plans
through.
2026-04-26 10:40:58 +02:00
Andreas Kling
bc4ff9038a LibWeb: Schedule :has() walks on enclosing shadow scopes for inserts
Light-DOM mutations under a shadow host can flip
:host(...:has(...)) rules that live entirely in the host's shadow
root. Node::invalidate_style only scheduled the :has() walk on the
root scope of the mutated node, so those shadow-side rules never
re-evaluated when their light-DOM input changed.

Walk up parent_or_shadow_host from the mutation parent and schedule
the :has() walk on every enclosing shadow root's StyleScope that has
:has() rules.
2026-04-26 10:40:58 +02:00
Andreas Kling
b8573eba6f LibWeb: Use document scope for invalidation in user-agent shadow trees
SVG <use> shadow trees opt into the host document's stylesheets via
ShadowRoot::uses_document_style_sheets. Style computation already
honors this in AbstractElement::style_scope(), but
Node::invalidate_style picked the shadow root's own StyleScope based
solely on root().is_shadow_root(), so mutations inside such trees
never consulted document :has() metadata or invalidation plans.

Match the existing style-computation scope selection so invalidation
runs against the document StyleScope when the root is a shadow root
that uses document stylesheets.
2026-04-26 10:40:58 +02:00