Skip to content

✨ feat(dom): add Shadow DOM tree model#573

Merged
gaborbernat merged 1 commit into
tox-dev:mainfrom
gaborbernat:feat/553-shadow-dom
Jul 7, 2026
Merged

✨ feat(dom): add Shadow DOM tree model#573
gaborbernat merged 1 commit into
tox-dev:mainfrom
gaborbernat:feat/553-shadow-dom

Conversation

@gaborbernat

Copy link
Copy Markdown
Member

turbohtml modeled the light DOM but had no way to represent a shadow tree, so code ported from a browser or from jsdom lost attachShadow, <slot> composition, and the flattened tree it relied on. 🌳 This adds the DOM Living Standard shadow-tree model as the foundation the later declarative-shadow-DOM work (#549) builds on.

Element.attach_shadow attaches an open or closed ShadowRoot and records the host-to-root link in a per-tree table. A shadow root reuses the existing document-fragment node, flagged so node_wrap types it as ShadowRoot, and it is held off the light tree: it stays outside the host's children, so the existing queries, traversal, and serialization skip it without a special case, and you reach it only through Element.shadow_root (which reads None for a closed root) or the reference attach_shadow returned. Slot assignment runs in C from the spec's find-a-slot and find-slotables walks, computed on demand rather than cached, so assigned_nodes, assigned_elements, and assigned_slot reflect the tree as it stands each time you ask. assigned_nodes(flatten=True) falls back to a slot's own children and expands nested shadow slots, and Node.flattened_children returns the composed tree with every slot replaced by what it received. Every algorithm runs under the per-tree critical section.

Because a shadow root sits beside the host instead of inside it, cloning or serializing the host does not carry its shadow tree, matching the encapsulation the spec describes. The jsdom migration guide gains a Shadow DOM section mapping attachShadow and assignedNodes to their turbohtml spellings.

closes #553

@gaborbernat gaborbernat added the enhancement New feature or request label Jul 7, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 7, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 9.45%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 74 untouched benchmarks
🆕 1 new benchmark
⏩ 18 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
test_feature[text-content] 2 ms 1.8 ms +9.45%
🆕 test_feature[shadow] N/A 598.1 µs N/A

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing gaborbernat:feat/553-shadow-dom (d650b46) with main (88400b9)

Open in CodSpeed

Footnotes

  1. 18 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Model the DOM Living Standard shadow tree: attach_shadow attaches an open or
closed ShadowRoot to a host element, and the host-to-root link is recorded in a
per-tree table so the shadow root stays off the light tree -- it is never a
child of the host, so existing queries, traversal, and serialization skip it for
free and reach it only through Element.shadow_root or the returned reference.

Slots assign the host's children by name in the C core, computed on demand from
the spec's find-a-slot and find-slotables walks so an assignment always reflects
the current tree: assigned_nodes and assigned_elements read a slot's assignment
(with a flatten option that falls back to a slot's own children and expands
nested shadow slots), assigned_slot gives the slot a child landed in, and
flattened_children returns the composed tree with every slot replaced by what it
received. A shadow root reuses the document-fragment node with a tag_flags bit so
node_wrap types it as ShadowRoot; the algorithms run under the per-tree critical
section. This is the foundation a later declarative-shadow-DOM change builds on.

closes tox-dev#553
@gaborbernat gaborbernat force-pushed the feat/553-shadow-dom branch from 4991bb8 to d650b46 Compare July 7, 2026 10:16
@gaborbernat gaborbernat merged commit 32c1c44 into tox-dev:main Jul 7, 2026
48 checks passed
gaborbernat added a commit that referenced this pull request Jul 7, 2026
…577)

* ✨ feat(parser): attach declarative shadow roots

A <template shadowrootmode="open|closed"> now attaches a shadow root to
its parent instead of building a template content fragment, per the
WHATWG tree-construction steps. The C tree builder creates the template
only on the stack of open elements, attaches a shadow root to the
adjusted current node (reusing the #573 shadow primitives), and
redirects the template's content into it, so the template never joins
the light tree. shadowrootdelegatesfocus and shadowrootclonable set the
matching flags, read through the new ShadowRoot.delegates_focus and
ShadowRoot.clonable.

The behavior follows the per-document "allow declarative shadow roots"
flag: parse() honors it by default (a browser navigation) and
parse_fragment() does not (an innerHTML assignment), each overridable
with allow_declarative_shadow_roots. A parent that is not a valid shadow
host, or already hosts a shadow root, keeps an ordinary template.

* 📊 feat(bench): time declarative shadow parsing

Add a parse-shadow operation that parses a document of web-component
cards whose <template shadowrootmode> attaches a shadow root to each
host, gated in CodSpeed over a large inline synthetic corpus. Classify
it as a whole-document op in pgo_corpus so the PGO training build feeds
it real pages instead of raising KeyError.

* 📝 docs(shadow): document declarative shadow roots

Cover the parser feature across the four Diátaxis dimensions (the
tokenizing tutorial, the shadow-dom how-to and explanation, the parsing
reference), extend the parse5 migration guide, and add the towncrier
fragment. Tests exercise open and closed modes, the delegatesfocus and
clonable flags, nesting, host validity, the already-hosted fallback, the
fragment opt-in, and that a plain template still builds a content
fragment.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
@gaborbernat gaborbernat deleted the feat/553-shadow-dom branch July 10, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DOM: Shadow DOM tree model (attachShadow/ShadowRoot/slots)

1 participant