feat: auto-escape {/} inside <code> elements to render code samples literally (v3)#7539
Draft
janechu wants to merge 1 commit into
Draft
Conversation
5 tasks
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-element`):
* Curly braces (`{` / `}`) inside every `<code>` element (text and
attribute values of any descendant) are replaced with their HTML
numeric character references (`{` / `}`) 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 `<` / `>` 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-element declarative fixture builder injecting `<f-template>`
declarations).
Fixes #7520.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1e4a3a0 to
c15e214
Compare
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.
Pull Request
📖 Description
Companion PR to #7538 (against
main) — ports the same auto-escape feature for<code>samples to the v3 declarative-element source layout.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_elementsinmicrosoft-fast-build) and the client-side<f-template>parser (escapeBracesInCodeElementsin@microsoft/fast-element):{/}) in text content and attribute values of any descendant of<code>are replaced with{/}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.innerHTMLdecodes 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.<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 normalrender_*pipeline (for example, the declarative fixture builder injecting<f-template>declarations after fast-build runs).This is a feature change.
🎫 Issues
{/}inside<code>elements to render code samples literally #7538👩💻 Reviewer Notes
The behaviour is intentionally scoped to the tag name
codeonly (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 withf-are not escaped.The escape pass is idempotent — pre-existing
</{/>/}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_samplesWASM export is consumed bypackages/fast-element/scripts/declarative/build-fixtures.js, which injects<f-template>declarations from each fixture'stemplates.htmlinto the renderedindex.htmlafterfast-buildruns. 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.The Rust crate (
crates/microsoft-fast-build) is shared verbatim with PR #7538 againstmain. The JavaScript-side changes are localised to the v3 declarative source files:packages/fast-element/src/declarative/utilities.ts— JSDoc updates oncodeElementNameandescapeBracesInCodeElementsexplaining the server/client split (no functional change to the helper itself).packages/fast-element/test/declarative/fixtures/scenarios/code-sample/— fixture + spec.packages/fast-element/docs/declarative-html.md+declarative-design.md— narrative docs.📑 Test Plan
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).test/declarative/fixtures/scenarios/code-sample/code-sample.spec.tscovering:<code>still resolve.<code>is preserved literally.<f-when>inside<code>renders as text and does not activate the directive.<F-When>is also escaped (DOM round-trip safety).<f-repeat>inside<code>renders as text and does not activate the directive.<code>is preserved literally.<button>inside<code>renders as a live DOM element with literal{{...}}text in its content and attributes.<my-demo-widget>) inside<code>renders as a live DOM element with literal{{...}}attributes.cargo test) and all 185 v3 declarative Playwright tests on chromium; fast-build node tests (30/30).✅ Checklist
General
$ npm run change