Skip to content

feat: auto-escape {/} inside <code> elements to render code samples literally#7538

Draft
janechu wants to merge 1 commit into
mainfrom
users/janechu/ensure-code-samples-do-not-get-processed
Draft

feat: auto-escape {/} inside <code> elements to render code samples literally#7538
janechu wants to merge 1 commit into
mainfrom
users/janechu/ensure-code-samples-do-not-get-processed

Conversation

@janechu
Copy link
Copy Markdown
Collaborator

@janechu janechu commented May 28, 2026

Pull Request

📖 Description

FAST's template parser was processing {{...}} text and <f-when> / <f-repeat> directive tags found inside <code> elements meant for code samples, causing hydration mismatches and rendering bindings or activating directives instead of showing the literal characters the author wrote.

This change adds transparent auto-escaping for <code> contents. The escape behaviour is scoped: braces are escaped everywhere inside <code>, but angle brackets are escaped only for FAST directive tags. Real HTML elements (<button>) and custom elements (<my-widget>) inside <code> keep their angle brackets and continue to render as live DOM elements — so authors can mix runnable demo elements with surrounding code text in the same <code> block.

The escape runs as a two-stage pipeline split between the server-side renderer (escape_code_sample_elements in microsoft-fast-build) and the client-side <f-template> parser (escapeBracesInCodeElements in @microsoft/fast-html):

  • Braces ({ / }) in text content and attribute values of any descendant of <code> are replaced with &#123; / &#125; on both server and client, so binding delimiters ({{...}}, {{{...}}}, {...}) inside <code> render literally. The brace half has to run client-side too because the DOM serializer used by .innerHTML decodes the numeric references back to literal braces and does not re-encode them, so the escape has to be re-applied before the binding scan.
  • Angle brackets of FAST directive tags (<f-when>, </f-when>, <f-repeat>, </f-repeat>, case-insensitive — browsers normalise tag names to lowercase, so an unescaped <F-When> would re-activate after the DOM round-trip) are entity-escaped on the server only. The DOM serializer re-encodes < / > in text content automatically, so the client never sees a raw directive tag inside <code>.

A new escape_code_samples(html) WASM export wraps the escape so build-time tooling can apply it to author HTML that is injected into a rendered page outside the normal render_* pipeline (for example, the fast-html fixture builder injecting <f-template> declarations after fast-build runs).

This is a feature change.

🎫 Issues

👩‍💻 Reviewer Notes

The behaviour is intentionally scoped to the tag name code only (matching WebUI exactly). Other code-related tags such as <pre>, <samp>, <kbd> are not affected — authors can wrap them with <code> if they want the same behaviour.

Directive-tag matching is case-insensitive and constrained to the exact FAST directive set (f-when, f-repeat). Tag-name lookalikes such as <f-whenever> and other custom elements that happen to start with f- are not escaped.

The escape pass is idempotent — pre-existing &lt; / &#123; / &gt; / &#125; entities are left alone, so running the pass on already-escaped content is a no-op. Nested <code> elements are handled via depth tracking.

The escape_code_samples WASM export is consumed by packages/fast-html/scripts/build-fixtures.js, which injects <f-template> declarations from each fixture's templates.html into the rendered index.html after fast-build runs. Passing the injected content through the same Rust escape ensures the client-side parser sees identical bytes to what the server-side SSR pipeline produced for the corresponding shadow DOM.

📑 Test Plan

  • 23 Rust unit tests in code_escape::tests (passthrough, scope, brace escape inside <code>, directive-tag angle escape including case-insensitive matching, pre-escaped entities, name-prefix safety, > / < inside attribute values, self-closing tags, real elements / custom elements / mixed content, round-trip / idempotency).
  • 9 Playwright tests in test/fixtures/code-sample/code-sample.spec.ts covering:
    1. Bindings outside <code> still resolve.
    2. Double-brace text inside <code> is preserved literally.
    3. Literal <f-when> inside <code> renders as text and does not activate the directive.
    4. Uppercase <F-When> is also escaped (DOM round-trip safety).
    5. Literal <f-repeat> inside <code> renders as text and does not activate the directive.
    6. Single-brace attribute-binding text inside <code> is preserved literally.
    7. Real <button> inside <code> renders as a live DOM element with literal {{...}} text in its content and attributes.
    8. Custom element (<my-demo-widget>) inside <code> renders as a live DOM element with literal {{...}} attributes.
    9. No hydration errors are emitted with literal binding-like text.
  • All existing tests pass: full Rust suite (cargo test) and all 310 fast-html Playwright tests on chromium; fast-build node tests (30/30).

✅ Checklist

General

  • I have included a change request file using $ npm run change
  • I have added tests for my changes.
  • I have tested my changes.
  • I have updated the project documentation to reflect my changes.
  • I have read the CONTRIBUTING documentation and followed the standards for this project.

⏭ Next Steps

Companion PR against releases/fast-element-v3 (#7539) ports the same change to the v3 declarative-element source layout and updates the v3 declarative docs.

@janechu janechu force-pushed the users/janechu/ensure-code-samples-do-not-get-processed branch from b6c64a5 to c931dd4 Compare May 29, 2026 17:48
@janechu janechu changed the title feat: add f-no-parse opt-out attribute for literal binding syntax feat: auto-escape {/} inside <code> elements to render code samples literally May 29, 2026
@janechu janechu marked this pull request as ready for review May 29, 2026 18:35
@janechu janechu marked this pull request as draft May 29, 2026 18:46
Mirrors the auto-escape behavior of Microsoft WebUI's `webui-press`
markdown renderer so that code samples embedded inside `<code>` blocks
render as literal text instead of being interpreted by the FAST
template parser.

The escape runs as a two-stage pipeline split between the server-side
renderer (`escape_code_sample_elements` in `microsoft-fast-build`) and
the client-side `<f-template>` parser (`escapeBracesInCodeElements` in
`@microsoft/fast-html`):

* Curly braces (`{` / `}`) inside every `<code>` element (text and
  attribute values of any descendant) are replaced with their HTML
  numeric character references (`&#123;` / `&#125;`) on **both**
  server and client, so binding delimiters (`{{...}}`, `{{{...}}}`,
  `{...}`) inside `<code>` render literally. The split is necessary
  because the DOM serializer used by `.innerHTML` decodes the numeric
  references back to literal braces and does not re-encode them, so
  the brace escape has to be re-applied client-side before the binding
  scan runs.

* Angle brackets of FAST directive tags (`<f-when>`, `</f-when>`,
  `<f-repeat>`, `</f-repeat>`, case-insensitive) inside `<code>` are
  entity-escaped to `&lt;` / `&gt;` on the **server only**. The DOM
  serializer re-encodes `<` / `>` in text content automatically, so
  the client never sees a raw directive tag inside `<code>` regardless
  of what the page source contained.

* Real HTML elements (`<button>`) and custom elements (`<my-widget>`)
  inside `<code>` keep their angle brackets and continue to render as
  live DOM elements; only brace-binding syntax in their attribute
  values is neutralised. This lets authors mix interactive demo
  elements with surrounding code text in the same `<code>` block.

A new `escape_code_samples(html)` WASM export wraps the escape so
build-time tooling can apply it to author HTML that is injected into a
rendered page outside the normal `render_*` pipeline (for example, the
fast-html fixture builder injecting `<f-template>` declarations).

Fixes #7520.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu force-pushed the users/janechu/ensure-code-samples-do-not-get-processed branch from c931dd4 to 6f6ad53 Compare May 29, 2026 19:24
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.

fix: DSD hydration fails on literal curly-brace binding text in light DOM

1 participant