Skip to content

✨ feat(parser): declarative Shadow DOM (<template shadowrootmode>)#577

Merged
gaborbernat merged 4 commits into
tox-dev:mainfrom
gaborbernat:feat/549-declarative-shadow
Jul 7, 2026
Merged

✨ feat(parser): declarative Shadow DOM (<template shadowrootmode>)#577
gaborbernat merged 4 commits into
tox-dev:mainfrom
gaborbernat:feat/549-declarative-shadow

Conversation

@gaborbernat

Copy link
Copy Markdown
Member

Servers ship shadow trees inline with a <template shadowrootmode>, and a browser attaches them during parsing so the encapsulation is in place before any script runs. turbohtml parsed such a template as an ordinary template element, leaving the shadow tree unbuilt and the markup misrepresented. This wires declarative shadow DOM into the C tree builder so parse reproduces what a navigating browser produces, closes #549.

The tree builder follows the WHATWG tree-construction steps for the template start tag directly. 🌱 When a <template> carries a shadowrootmode of open or closed and its parent is a valid shadow host, it attaches a shadow root to that parent, reusing the #573 Shadow DOM primitives (attach_shadow, the off-tree shadow node, the per-tree host table) rather than a new tree model, and redirects the template's content into the shadow root. The template element is created only on the stack of open elements, so it never joins the light tree and disappears once its content is parsed. shadowrootdelegatesfocus and shadowrootclonable set flags surfaced as ShadowRoot.delegates_focus and ShadowRoot.clonable. Two guards keep the transform faithful to the DOM: the host must be a valid shadow host name (a flow-content element or a hyphenated custom-element name), and a parent that already hosts a shadow root keeps its first one.

The feature honors the spec's per-document allow declarative shadow roots flag. Whole-document parse sets it on, matching a browser navigation, while parse_fragment leaves it off, matching an innerHTML assignment; the new allow_declarative_shadow_roots argument on each flips that default, turning a fragment into the setHTMLUnsafe case. 📦 This changes the default document parse: a stray <template shadowrootmode> now leaves the light tree, so pass allow_declarative_shadow_roots=False to keep the old plain-template behavior.

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 tox-dev#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.
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.
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.
@gaborbernat gaborbernat added documentation Improvements or additions to documentation enhancement New feature or request labels Jul 7, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 7, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 78 untouched benchmarks
🆕 1 new benchmark
⏩ 18 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
🆕 test_feature[parse-shadow] N/A 3.7 ms N/A

Comparing gaborbernat:feat/549-declarative-shadow (7eee217) with main (0fe506c)

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.

@gaborbernat gaborbernat merged commit d72a418 into tox-dev:main Jul 7, 2026
48 checks passed
@gaborbernat gaborbernat deleted the feat/549-declarative-shadow 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

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parser: declarative Shadow DOM (<template shadowrootmode>)

1 participant