Skip to content

fix(parser): preserve devs <template> wrapper#307

Merged
mcritzjam merged 1 commit into
mainfrom
mmansour/preserve-dev-template-wrapper
May 19, 2026
Merged

fix(parser): preserve devs <template> wrapper#307
mcritzjam merged 1 commit into
mainfrom
mmansour/preserve-dev-template-wrapper

Conversation

@mohamedmansour
Copy link
Copy Markdown
Contributor

@mohamedmansour mohamedmansour commented May 19, 2026

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).

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.

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).

`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>
@mohamedmansour mohamedmansour changed the title fix(parser): preserve developer-authored <template> wrapper verbatim fix(parser): preserve developer-authored <template> wrapper May 19, 2026
@mohamedmansour mohamedmansour changed the title fix(parser): preserve developer-authored <template> wrapper fix(parser): preserve devs <template> wrapper May 19, 2026
@mcritzjam mcritzjam merged commit 68c4bd9 into main May 19, 2026
21 checks passed
@mcritzjam mcritzjam deleted the mmansour/preserve-dev-template-wrapper branch May 19, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants