Skip to content

Add streaming HTML insertion methods to Element, ShadowRoot, and ChildNode - #12758

Open
noamr wants to merge 4 commits into
noamr/streaming-sanitizerfrom
noamr/stream-html
Open

noamr wants to merge 4 commits into
noamr/streaming-sanitizerfrom
noamr/stream-html

Conversation

@noamr

@noamr noamr commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Add streaming HTML insertion methods to Element, ShadowRoot, and ChildNode

Introduce safe and unsafe streaming HTML parsing and insertion methods:

  • Element & ShadowRoot: streamHTML(), streamHTMLUnsafe(), streamAppendHTML(), streamAppendHTMLUnsafe(), streamPrependHTML(), and streamPrependHTMLUnsafe()
  • ChildNode: streamBeforeHTML(), streamBeforeHTMLUnsafe(), streamAfterHTML(), streamAfterHTMLUnsafe(), streamReplaceWithHTML(), and streamReplaceWithHTMLUnsafe()

This allows developers to parse and stream HTML directly into specific positions of the DOM tree using WritableStream.

  • Stream Semantics:
    • writeAlgorithm: converts chunks to DOMString, revalidates that referenceChild (if present) is still a child of target (aborting the parser and rejecting with HierarchyRequestError if displaced), feeds the chunk to the parser, and resolves when characters and parser-blocking scripts have executed.
    • closeAlgorithm: revalidates referenceChild, appends an explicit EOF to the parser's input stream to finish parsing, and executes deferred and module scripts.
    • abortAlgorithm: aborts the underlying HTML parser.
  • Script Execution Model:
    Streaming fragment parsers can execute scripts when runScripts is true:
    • The parser is given a script target document. Script elements created by the parser receive an intended document set to this document.
    • For safe streams or when runScripts is false (default), the script target document is the temporary inert document, preventing script execution.
    • When runScripts is true, the script target document is the context element's node document.
    • Safe streaming into a <script> element throws a NotSupportedError DOMException.
  • Atomic Validation & DOM Mutation Ordering:
    • HTMLTemplateElement insertion targets are redirected to their content DocumentFragment before any mutation.
    • For ChildNode methods, disconnected nodes are validated via parent for HTML streaming (throwing HierarchyRequestError if no suitable parent exists).
    • Synchronous option validation (Trusted Types and SanitizerConfig) occurs before the stream is returned and prior to any destructive DOM mutations (replace all with null for streamHTML or removal of the node for streamReplaceWithHTML).
  • Trusted Types Integration:
    Unsafe streaming methods accept (SetHTMLUnsafeOptions or TrustedHTMLParserOptions) using the Stream sink type.

Closes #2142

(https://github.com/whatwg/meta/blob/main/COMMITTING.md) to use.

(See WHATWG Working Mode: Changes for more details.)


/dynamic-markup-insertion.html ( diff )
/index.html ( diff )
/infrastructure.html ( diff )
/parsing.html ( diff )
/references.html ( diff )
/scripting.html ( diff )
/xhtml.html ( diff )

@noamr noamr changed the title Add streaming HTML insertion methods Streaming HTML insertion methods Aug 5, 2026
@noamr noamr changed the title Streaming HTML insertion methods Stream HTML into element or ShadowRoot Aug 5, 2026
@noamr
noamr force-pushed the noamr/stream-html branch from df24ea2 to e416732 Compare August 5, 2026 09:32
@noamr noamr closed this Aug 5, 2026
@noamr noamr reopened this Aug 5, 2026
@noamr
noamr force-pushed the noamr/stream-html branch from e416732 to 5106e9d Compare August 6, 2026 16:23
@noamr
noamr force-pushed the noamr/stream-html branch from deb55bb to e97c374 Compare August 6, 2026 19:46
@noamr
noamr force-pushed the noamr/stream-html branch from d5a18a7 to b19b385 Compare August 7, 2026 20:17
Comment thread source
Comment thread source Outdated
Comment thread source Outdated
Comment thread source Outdated
@noamr
noamr force-pushed the noamr/stream-html branch from d87ab8d to 4880d38 Compare August 11, 2026 15:49
@noamr
noamr force-pushed the noamr/stream-html branch from 2de75ec to ce9f49b Compare August 11, 2026 20:18
noamr added a commit to web-platform-tests/wpt that referenced this pull request Aug 12, 2026
See whatwg/html#12758

Note that tests that assert how this works with <template for>
are still tentative as that PR is still not landed.

@zcorpan zcorpan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM % spaces

Comment thread source Outdated
Comment thread source Outdated
@noamr
noamr force-pushed the noamr/stream-html branch from 421aa3f to 043dcdf Compare August 12, 2026 13:02
@noamr
noamr force-pushed the noamr/stream-html branch from 043dcdf to 286c499 Compare August 12, 2026 13:25
@noamr
noamr force-pushed the noamr/stream-html branch from 286c499 to 146266b Compare August 12, 2026 16:08
@zcorpan zcorpan mentioned this pull request Aug 14, 2026
5 tasks done
@noamr
noamr force-pushed the noamr/stream-html branch from 146266b to 6e5c251 Compare August 14, 2026 08:56
@noamr noamr closed this Aug 14, 2026
@noamr noamr reopened this Aug 14, 2026
@noamr
noamr force-pushed the noamr/stream-html branch from 8376fee to 82e9336 Compare August 28, 2026 12:34
@noamr
noamr force-pushed the noamr/stream-html branch 3 times, most recently from 8b318d5 to 2a95737 Compare August 28, 2026 14:27
@noamr
noamr force-pushed the noamr/stream-html branch from 2a95737 to 67cae0b Compare September 1, 2026 15:11
@noamr
noamr force-pushed the noamr/stream-html branch 2 times, most recently from b3835c2 to a7f0a86 Compare September 1, 2026 17:07
@noamr
noamr force-pushed the noamr/stream-html branch from a7f0a86 to 9b016ad Compare September 1, 2026 20:38
@noamr
noamr force-pushed the noamr/stream-html branch from 9b016ad to b61ef7d Compare September 1, 2026 20:44
@noamr
noamr force-pushed the noamr/stream-html branch from b61ef7d to 8ae6011 Compare September 2, 2026 10:26
@noamr
noamr force-pushed the noamr/stream-html branch from 8ae6011 to f4db75c Compare September 2, 2026 14:58
@zcorpan

zcorpan commented Sep 16, 2026

Copy link
Copy Markdown
Member

I squashed this and rebased it on #12756. Two conflicts, both from my "Don't look up custom element definitions for sanitized-away elements" commit (fab5f2b) and this PR independently factoring sanitize into an element-name algorithm. Resolved as follows:

  • Kept this PR's "sanitizer action for element name" and dropped my "get the sanitizer action for an element name" and the sanitize wrapper, since this PR removes that algorithm and moves the sanitize dfn to the intro prose.
  • In "create an element for a token", merged this PR's steps (switch document to the template contents owner for dropped elements, sanitize the is attribute) with my step that skips the custom element definition lookup for dropped elements. Both needed the action, so I compute it once into sanitizerAction and use it in both places.
  • My Noah's Ark note linked to "sanitize attributes", which this PR removes. I repointed it to "create an element for the token" and reworded the rationale, since removed attributes are now never appended to the element rather than removed afterwards.

Otherwise the diff is unchanged from before the rebase.

Comment thread source
noamr added 4 commits October 1, 2026 11:10
…d handling

Net changes since the previously pushed version:

Stream semantics
* write() and close() no longer wait for scripts. write() resolves once the chunk has been parsed
  as far as possible. If the parser is blocked on a parser-blocking script, input written in the
  meantime accumulates and is parsed after that script runs. close() appends an explicit EOF and
  resolves. Deferred and module scripts then run in later tasks, one per task.
* The streaming parser is now a script-created parser. Otherwise it would reach EOF at the end of
  every chunk, rather than at the explicit EOF that close() appends.
* Removed the "reference child displaced" flag. A displaced reference child is only checked when
  writing a chunk; close() doesn't check it. If the reference child is moved while a chunk is being
  parsed (by a script or a custom element reaction), the nodes that would have been inserted before
  it are dropped, and the next write() rejects with "HierarchyRequestError".
* Aborting the parser marks the script elements on the stack of open elements as already started,
  so a partially parsed script never runs. It also unblocks rendering on pending and deferred
  scripts. Removed the unused AbortSignal abort reason term.

Fragment parser script execution
* Fragment parsers block on scripts through the existing pending parsing-blocking script and the
  parser pause flag. When the script is ready, a task runs the main parser's steps for executing
  it, then resumes the parser.
* Added "run a fragment parser", and "execute the deferred scripts", which is the deferred-script
  loop from "the end", now shared with fragment parsers.
* "The end" (stop parsing): fragment parsers pop the stack of open elements, so open elements such
  as object, textarea and media elements are processed. They then queue a task to execute the
  deferred scripts and return. There is no event loop spin, and no DOMContentLoaded or load on the
  inert document.
* Push an element queue for fragment parsers too, so that when streaming into a connected node,
  upgrades and lifecycle callbacks run before the next streamed script.
* SVG script elements are not processed when the fragment parser's script target document is its
  own inert document, i.e., when it does not run scripts. This mirrors HTML scripts being marked as
  already started.
* "Parser scripting disabled" is described as an initial value that initializing a fragment parser
  overrides.

Element creation
* When creating an element, an intended parent that is in the insertion target redirection map is
  replaced by its redirect target. When streaming, elements whose intended parent is the root html
  element, or an element that the sanitizer replaces with its children, are therefore created in
  the target's node document. Before, they were created in the parser's inert document and adopted
  on insertion. They are now created the same way as their descendants, which are created after
  their parent has been inserted into the target's node document.

API surface
* Streaming into an HTML or SVG script element throws "NotSupportedError". This applies to the
  unsafe methods as well as the safe ones.
* The domintro now says that streamHTML*() and streamReplaceWithHTML*() remove the existing content
  when they are called. The intro describes the corrected script timing.

Editorial
* Corrected the "stop parsing" fragment-case note, and listed all the Streams editors in [STREAMS].
* Infra wording ("of", "return"), inlined refChild, and wrapping fixes.

The change that keeps declarative shadow roots off elements that are replaced with their children
is sanitizer-only, so it moved to the streaming-sanitizer branch (#12756).
…agency reactions, abort and render-blocking, stream realm, force async note, Trusted Types domintros
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

A way to stream content into an element

2 participants