SVG / MathML streaming with <template for>

Streaming SVG or MathML children with <template for> fails: the insert is HTML-namespaced, so nothing paints. Stream the whole <svg> or <math> instead — or parse that tree in a <div hidden> and swap it in with JS, like React/Sold/Preact SSR.

Limit

DPU completion inside <svg>

<template for> inserts an HTML-namespaced <circle> into an existing <svg>. It is not an SVGElement, so it never paints. React-style JS replaces a placeholder with a complete <svg> parsed in a hidden div, so it does.

Limit: the DPU frame stays empty. The streamed circle is HTML, not SVG. The React column paints.

Streamed (DPU)
Loading…
<svg>
  <?start name="svg-slot">
  <text>Loading…</text>
  <?end>
</svg>
<template for="svg-slot">
  <circle cx="80" cy="55" r="24" fill="tomato"/>
</template>
JS replacement
Loading…
<div id="B">Loading…</div>
<div hidden id="S">
  <svg>
    <circle cx="80" cy="55" r="24" fill="tomato"/>
  </svg>
</div>
<script>
  B.replaceWith(...S.childNodes);
  S.remove();
</script>
Expected result
Works

Stream the whole <svg>

Replace the pending range with a complete <svg> tree. The HTML parser puts it in the SVG namespace, so the circle paints. React-style JS does the same swap from a hidden div.

Expected: DPU, JS replacement, and the static result all paint a tomato circle.

Streamed (DPU)
Loading…
<?start name="svg-whole">
Loading…
<?end>
<template for="svg-whole">
  <svg width="160" height="100" viewBox="0 0 160 100">
    <circle cx="80" cy="55" r="24" fill="tomato"/>
  </svg>
</template>
JS replacement
Loading…
<div id="B">Loading…</div>
<div hidden id="S">
  <svg>
    <circle cx="80" cy="55" r="24" fill="tomato"/>
  </svg>
</div>
<script>
  B.replaceWith(...S.childNodes);
  S.remove();
</script>
Expected result
Limit

DPU completion inside <math>

Same limit as SVG: <template for> inserts HTML-namespaced <mi> / <mo> / <mn> into an existing <math>. They are not MathML, so they typeset as plain text (or not at all). React-style JS replaces a placeholder with a complete <math> parsed in a hidden div, so it typesets.

Limit: the DPU frame has no italic variables or stacked exponents. The streamed tokens are HTML, not MathML. The React column typesets.

Streamed (DPU)
Loading…
<math>
  <?start name="math-slot">
  <mtext>Loading…</mtext>
  <?end>
</math>
<template for="math-slot">
  <msup><mi>a</mi><mn>2</mn></msup>
  <mo>+</mo>
  <msup><mi>b</mi><mn>2</mn></msup>
  <mo>=</mo>
  <msup><mi>c</mi><mn>2</mn></msup>
</template>
JS replacement
Loading…
<div id="B">Loading…</div>
<div hidden id="S">
  <math display="block">
    <msup><mi>a</mi><mn>2</mn></msup>
    <mo>+</mo>
    <msup><mi>b</mi><mn>2</mn></msup>
    <mo>=</mo>
    <msup><mi>c</mi><mn>2</mn></msup>
  </math>
</div>
<script>
  B.replaceWith(...S.childNodes);
  S.remove();
</script>
Expected result
a2 + b2 = c2
Works

Stream the whole <math>

Replace the pending range with a complete <math> tree. The HTML parser puts the tokens in the MathML namespace, so they typeset. React-style JS does the same swap from a hidden div.

Expected: DPU, JS replacement, and the static result all typeset italic variables and stacked exponents.

Streamed (DPU)
Loading…
<?start name="math-whole">
Loading…
<?end>
<template for="math-whole">
  <math display="block">
    <msup><mi>a</mi><mn>2</mn></msup>
    <mo>+</mo>
    <msup><mi>b</mi><mn>2</mn></msup>
    <mo>=</mo>
    <msup><mi>c</mi><mn>2</mn></msup>
  </math>
</template>
JS replacement
Loading…
<div id="B">Loading…</div>
<div hidden id="S">
  <math display="block">
    <msup><mi>a</mi><mn>2</mn></msup>
    <mo>+</mo>
    <msup><mi>b</mi><mn>2</mn></msup>
    <mo>=</mo>
    <msup><mi>c</mi><mn>2</mn></msup>
  </math>
</div>
<script>
  B.replaceWith(...S.childNodes);
  S.remove();
</script>
Expected result
a2 + b2 = c2