The parser already put template children in a separate contents document
per the HTML spec (tree_sink.rs), but nothing exposed that node, so
`get content()` fabricated an empty DocumentFragment and the parsed
markup was unreachable:
document.body.innerHTML = '<template id=t><p>a</p></template>';
t.content.childNodes.length // 0 (Chrome: 1)
t.innerHTML // "" (Chrome: '<p>a</p>')
Every <template>-driven UI — custom elements cloning
`template.content`, lit, Vue runtime templates, htmx fragments, Alpine
x-if/x-for — stamped nothing, with no error raised.
Three layers were involved:
- `DomTree::template_contents` returns the contents node, allocating one
on demand so templates built via `createElement` work too. Exposed as
the `template_contents` op; `get content()` wraps it as a
DocumentFragment cached by node id, so `.content` keeps a stable
identity across reads.
- `import_node_from` cloned the element data verbatim, carrying the
*source* tree's contents NodeId into the destination tree, where it
indexed whatever unrelated node occupied that slot. That is why a
template injected via innerHTML resolved `.content` to a bogus node
rather than merely an empty one. The contents are now imported into a
fresh node and the reference remapped, queued onto the existing
worklist so nested templates stay iterative.
- The serializer emitted a template's own (always empty) children, so
contents vanished from outerHTML/innerHTML and `cloneNode(true)` —
which round-trips through outer_html — produced an empty template.
`content_source` resolves a template to its contents document for both
inner_html and outer_html.
With serialization fixed on the Rust side, the `set innerHTML` template
branch that routed to `.content` now has a matching getter for free, so
no template special case is needed on the JS hot path.