fix(parser): preserve devs <template> wrapper#307
Merged
Conversation
`process_component_template` used to extract only the inner content of a
developer-authored `<template>` wrapper and then re-wrap it with
framework-controlled attributes (or drop the wrapper entirely in
--dom=light). The dev's attributes — `foo="bar"`, `shadowrootmode="closed"`,
signal fragments like `foo="{{x}}"`, and any custom data-*/aria-* — were
silently discarded. The framework was overriding developer intent.
The new contract treats the dev's wrapper as the source of truth:
* Dev supplied `<template …>`: preserved verbatim in BOTH --dom=light and
--dom=shadow. The only modification is stripping runtime-only attrs
(`@event`, `:bind`, `?cond`), since those are protocol metadata and never
appear in HTML output. If the active CSS strategy emits a snippet, it is
spliced immediately inside the dev's opening tag so styles still apply.
`adopted_specifier` is intentionally ignored — when the dev manages the
wrapper, they manage CSS adoption.
* Dev omitted `<template>`:
- --dom=shadow wraps with `<template shadowrootmode="open">` (plus the
optional `shadowrootadoptedstylesheets="<tag>"` for CSS modules).
- --dom=light emits the content as-is (with the CSS snippet prepended,
if any).
Performance (mission-critical per the framework's perf rules):
* Zero recursion, zero regex.
* New fast-path byte scan `tag_scan::opening_tag_has_runtime_prefix` lets
`strip_runtime_attrs_from_template` skip the tree-sitter parse entirely
when no `@`/`:`/`?` attributes exist on the opening tag — the common case
for most components. The scan is quote-aware and bounded to the opening
tag (stops at the first unquoted `>`).
* Output buffers are pre-sized with `String::with_capacity` using exact
length math; the intermediate `inner` String allocation was removed in
favour of slicing.
* No new clones, no `Cow` indirection at the public boundary.
Consolidation:
The codebase had four near-duplicate quote-aware tag scanners
(`find_tag_end` in plugin/webui.rs, `find_tag_close` in plugin/fast_v2.rs,
`find_tag_close` in plugin/fast_v3.rs, and the two helpers introduced for
this fix). They have been unified into `crates/webui-parser/src/tag_scan.rs`
with one canonical `find_tag_close` plus the new
`opening_tag_has_runtime_prefix` fast-path helper.
Latent bug fixed by the consolidation: the old `find_tag_close` in
plugin/fast_v2.rs and plugin/fast_v3.rs only tracked double quotes. An
input like `<a x='a>b'>` would return the `>` at position 7 instead of 10.
The shared `find_tag_close` tracks both `"` and `'`, so FAST v2/v3 now
silently inherit the fix without any call-site changes.
Tests:
* 8 new inline unit tests in webui-parser/src/lib.rs cover all four
cells of the {dev-template, dom-strategy} matrix plus signal-fragment
attrs, multiple attrs, and dev-supplied `shadowrootmode="closed"`.
* 10 new unit tests in webui-parser/src/tag_scan.rs cover both helpers
(single/double quoted `>`, unterminated tags, prefix-in-tag-name
ignored, quoted prefix chars ignored, stop-at-unquoted-`>` semantics).
* Three existing tests that asserted the old destructive behaviour
(`test_component_no_double_wrap_template`,
`test_component_styled_no_double_wrap`,
`test_component_strip_runtime_attrs`) have been updated to assert the
new correct contract — they previously enforced the bug.
* Duplicate `find_tag_close_skips_quoted_gt` tests in fast_v2 and fast_v3
removed; the canonical tests now live in tag_scan.rs.
`cargo xtask check` passes (license-headers, fmt, clippy, proto-drift,
deny, test, build, wasm, examples, bench, docs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
mcritzjam
approved these changes
May 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
process_component_templateused to extract only the inner content of a developer-authored<template>wrapper and then re-wrap it with framework-controlled attributes (or drop the wrapper entirely in --dom=light). The dev's attributes —foo="bar",shadowrootmode="closed", signal fragments likefoo="{{x}}", and any custom data-/aria- — were silently discarded. The framework was overriding developer intent.The new contract treats the dev's wrapper as the source of truth:
<template …>: preserved verbatim in BOTH --dom=light and --dom=shadow. The only modification is stripping runtime-only attrs (@event,:bind,?cond), since those are protocol metadata and never appear in HTML output. If the active CSS strategy emits a snippet, it is spliced immediately inside the dev's opening tag so styles still apply.adopted_specifieris intentionally ignored — when the dev manages the wrapper, they manage CSS adoption.<template>:<template shadowrootmode="open">(plus the optionalshadowrootadoptedstylesheets="<tag>"for CSS modules).Consolidation:
The codebase had four near-duplicate quote-aware tag scanners (
find_tag_endin plugin/webui.rs,find_tag_closein plugin/fast_v2.rs,find_tag_closein plugin/fast_v3.rs, and the two helpers introduced for this fix). They have been unified intocrates/webui-parser/src/tag_scan.rswith one canonicalfind_tag_closeplus the newopening_tag_has_runtime_prefixfast-path helper.Tests:
shadowrootmode="closed".>, unterminated tags, prefix-in-tag-name ignored, quoted prefix chars ignored, stop-at-unquoted->semantics).test_component_no_double_wrap_template,test_component_styled_no_double_wrap,test_component_strip_runtime_attrs) have been updated to assert the new correct contract — they previously enforced the bug.find_tag_close_skips_quoted_gttests in fast_v2 and fast_v3 removed; the canonical tests now live in tag_scan.rs.cargo xtask checkpasses (license-headers, fmt, clippy, proto-drift, deny, test, build, wasm, examples, bench, docs).