Skip to content
Open
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e47a15c
Add patchUnsafe() methods
foolip Sep 4, 2025
f1f9151
Flesh out the idea more
foolip Oct 2, 2025
c9be483
Rename to streamHTMLUnsafe
foolip Oct 6, 2025
cc255ea
Remove handling of byte chunks, betting on textStream()
foolip Oct 6, 2025
736f05f
Editorial: link directly to trusted type algorithm
foolip Nov 4, 2025
231fc18
Make streamHTMLUnsafe() throw if CSP enforces TT
foolip Nov 4, 2025
fee279f
Replace handwaving with more realistic steps
foolip Nov 6, 2025
1a1987f
Remove existing nodes and insert new ones
foolip Nov 6, 2025
fdcc163
Remove children when the writer is locked
foolip Nov 6, 2025
0834954
Fold into DOM parsing and serialization APIs
foolip Nov 7, 2025
d14b104
already executed -> already started
foolip Nov 7, 2025
9902b9d
Should we support the XML parser?
foolip Nov 7, 2025
8c01e4d
The encoding confidence is irrelevant
foolip Nov 7, 2025
df6786a
Throw InvalidStateError for XML documents
foolip Nov 20, 2025
5c6fc51
Integrate with Streams in the correct way
foolip Nov 20, 2025
2bd29ed
Define unsafely stream HTML to support ShadowRoot
foolip Nov 20, 2025
b5a6d01
Introduce a streaming HTML fragment parsing algorithm
foolip Nov 20, 2025
46ff84a
Add a domintro section
foolip Nov 20, 2025
e1868c1
Merge remote-tracking branch 'origin/main' into foolip/patching
foolip Nov 20, 2025
49a2412
Fix simpler annevk feedback
foolip Nov 25, 2025
6821d77
Move runScripts to SetHTMLUnsafeOptions
foolip Nov 25, 2025
0094421
Start to "integrate" the parsers in a hacky way
foolip Nov 27, 2025
44d3523
Clean up runScripts slightly
foolip Nov 27, 2025
17e6806
Introduce a mark scripts as already started flag
foolip Nov 27, 2025
e7a2dcb
Adjust insertion location in a the right place
foolip Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 223 additions & 17 deletions source
Original file line number Diff line number Diff line change
Expand Up @@ -4611,6 +4611,17 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
</ul>
</dd>

<dt>Streams</dt>
<dd>
<p>The following terms are defined in <cite>Streams</cite>: <ref>STREAMS</ref></p>

<ul class="brief">
<li><dfn data-x-href="https://streams.spec.whatwg.org/#writablestream"><code>WritableStream</code></dfn></li>
<li><dfn data-x-href="https://streams.spec.whatwg.org/#writablestream-set-up" for="WritableStream">set up</dfn> a newly-<span data-x="new">created-via-Web IDL</span> <code>WritableStream</code></li>
<li><dfn data-x-href="https://streams.spec.whatwg.org/#abort-a-writable-stream" for="WritableStream">abort</dfn> a <code>WritableStream</code></li>
</ul>
</dd>

<dt>Web App Manifest</dt>

<dd>
Expand Down Expand Up @@ -4791,6 +4802,7 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
<li><dfn data-x="tt-trustedscript-data" data-x-href="https://w3c.github.io/trusted-types/dist/spec/#trustedscript-data"><code>data</code></dfn></li>
<li><dfn data-x="tt-trustedscripturl" data-x-href="https://w3c.github.io/trusted-types/dist/spec/#trustedscripturl"><code>TrustedScriptURL</code></dfn></li>
<li><dfn data-x="tt-getcompliantstring" data-x-href="https://w3c.github.io/trusted-types/dist/spec/#get-trusted-type-compliant-string">get trusted type compliant string</dfn></li>
<li><dfn data-x="tt-shouldblock" data-x-href="https://w3c.github.io/trusted-types/dist/spec/#should-sink-type-mismatch-violation-be-blocked-by-content-security-policy">should sink type mismatch violation be blocked by content security policy?</dfn></li>
</ul>
</dd>

Expand Down Expand Up @@ -123517,22 +123529,26 @@ document.body.appendChild(frame)</code></pre>

<h3 id="dom-parsing-and-serialization">DOM parsing and serialization APIs</h3>

<pre><code class="idl">partial interface <span id="Element-partial">Element</span> {
[<span>CEReactions</span>] undefined <span data-x="dom-Element-setHTMLUnsafe">setHTMLUnsafe</span>((<code data-x="tt-trustedhtml">TrustedHTML</code> or DOMString) html);
<pre><code class="idl">partial interface <span>Element</span> {
[<span>CEReactions</span>] undefined <span data-x="dom-Element-setHTMLUnsafe">setHTMLUnsafe</span>((<code data-x="tt-trustedhtml">TrustedHTML</code> or DOMString) html, optional <span>SetHTMLUnsafeOptions</span> options = {});
DOMString <span data-x="dom-Element-getHTML">getHTML</span>(optional <span>GetHTMLOptions</span> options = {});

[<span>CEReactions</span>] attribute (<code data-x="tt-trustedhtml">TrustedHTML</code> or [<span>LegacyNullToEmptyString</span>] DOMString) <span data-x="dom-Element-innerHTML">innerHTML</span>;
[<span>CEReactions</span>] attribute (<code data-x="tt-trustedhtml">TrustedHTML</code> or [<span>LegacyNullToEmptyString</span>] DOMString) <span data-x="dom-Element-outerHTML">outerHTML</span>;
[<span>CEReactions</span>] undefined <span data-x="dom-Element-insertAdjacentHTML">insertAdjacentHTML</span>(DOMString position, (<code data-x="tt-trustedhtml">TrustedHTML</code> or DOMString) string);
};

partial interface <span id="ShadowRoot-partial">ShadowRoot</span> {
[<span>CEReactions</span>] undefined <span data-x="dom-ShadowRoot-setHTMLUnsafe">setHTMLUnsafe</span>((<code data-x="tt-trustedhtml">TrustedHTML</code> or DOMString) html);
partial interface <span>ShadowRoot</span> {
[<span>CEReactions</span>] undefined <span data-x="dom-ShadowRoot-setHTMLUnsafe">setHTMLUnsafe</span>((<code data-x="tt-trustedhtml">TrustedHTML</code> or DOMString) html, optional <span>SetHTMLUnsafeOptions</span> options = {});
DOMString <span data-x="dom-ShadowRoot-getHTML">getHTML</span>(optional <span>GetHTMLOptions</span> options = {});

[<span>CEReactions</span>] attribute (<code data-x="tt-trustedhtml">TrustedHTML</code> or [<span>LegacyNullToEmptyString</span>] DOMString) <span data-x="dom-ShadowRoot-innerHTML">innerHTML</span>;
};

dictionary <dfn dictionary>SetHTMLUnsafeOptions</dfn> {
boolean <dfn dict-member for="SetHTMLUnsafeOptions" data-x="dom-SetHTMLUnsafeOptions-runScripts">runScripts</dfn> = false;
};

dictionary <dfn dictionary>GetHTMLOptions</dfn> {
boolean <dfn dict-member for="GetHTMLOptions" data-x="dom-GetHTMLOptions-serializableShadowRoots">serializableShadowRoots</dfn> = false;
sequence&lt;ShadowRoot> <dfn dict-member for="GetHTMLOptions" data-x="dom-GetHTMLOptions-shadowRoots">shadowRoots</dfn> = [];
Expand Down Expand Up @@ -123710,20 +123726,27 @@ enum <dfn enum>DOMParserSupportedType</dfn> {

<dl class="domintro">
<dt><code data-x=""><var>element</var>.<span subdfn
data-x="dom-Element-setHTMLUnsafe">setHTMLUnsafe</span>(<var>html</var>)</code></dt>
data-x="dom-Element-setHTMLUnsafe">setHTMLUnsafe</span>(<var>html</var>, <var>options</var>)</code></dt>

<dd>
<p>Parses <var>html</var> using the HTML parser, and replaces the children of <var>element</var>
with the result. <var>element</var> provides context for the HTML parser.</p>
with the result. <var>element</var> provides context for the HTML parser. <var>options</var>
can contain the following values:</p>

<ul>
<li><p><code data-x="dom-SetHTMLUnsafeOptions-runScripts">runScripts</code> can be set to true
to run scripts when they are inserted into the document.</p></li>
</ul>
</dd>

<dt><code data-x=""><var>shadowRoot</var>.<span subdfn
data-x="dom-ShadowRoot-setHTMLUnsafe">setHTMLUnsafe</span>(<var>html</var>)</code></dt>
data-x="dom-ShadowRoot-setHTMLUnsafe">setHTMLUnsafe</span>(<var>html</var>, <var>options</var>)</code></dt>

<dd>
<p>Parses <var>html</var> using the HTML parser, and replaces the children of
<var>shadowRoot</var> with the result. <var>shadowRoot</var>'s <span
data-x="concept-DocumentFragment-host">host</span> provides context for the HTML parser.</p>
data-x="concept-DocumentFragment-host">host</span> provides context for the HTML parser.
<var>options</var> has the same values as above.</p>
</dd>

<dt><code data-x=""><var>doc</var> = Document.<span
Expand All @@ -123747,8 +123770,8 @@ enum <dfn enum>DOMParserSupportedType</dfn> {

<div algorithm>
<p><code>Element</code>'s <dfn method for="Element"><code
data-x="dom-Element-setHTMLUnsafe">setHTMLUnsafe(<var>html</var>)</code></dfn> method steps
are:</p>
data-x="dom-Element-setHTMLUnsafe">setHTMLUnsafe(<var>html</var>, <var>options</var>)</code></dfn>
method steps are:</p>

<ol>
<li><p>Let <var>compliantHTML</var> be the result of invoking the <span
Expand All @@ -123760,15 +123783,15 @@ enum <dfn enum>DOMParserSupportedType</dfn> {
<li><p>Let <var>target</var> be <span>this</span>'s <span>template contents</span> if
<span>this</span> is a <code>template</code> element; otherwise <span>this</span>.</p></li>

<li><p><span>Unsafely set HTML</span> given <var>target</var>, <span>this</span>, and
<var>compliantHTML</var>.</p></li>
<li><p><span>Unsafely set HTML</span> given <var>target</var>, <span>this</span>,
<var>compliantHTML</var>, and <var>options</var>.</p></li>
</ol>
</div>

<div algorithm>
<p><code>ShadowRoot</code>'s <dfn method for="ShadowRoot"><code
data-x="dom-ShadowRoot-setHTMLUnsafe">setHTMLUnsafe(<var>html</var>)</code></dfn> method steps
are:</p>
data-x="dom-ShadowRoot-setHTMLUnsafe">setHTMLUnsafe(<var>html</var>,
<var>options</var>)</code></dfn> method steps are:</p>

<ol>
<li><p>Let <var>compliantHTML</var> be the result of invoking the <span
Expand All @@ -123778,14 +123801,15 @@ enum <dfn enum>DOMParserSupportedType</dfn> {
data-x="">script</code>".</p></li>

<li><p><span>Unsafely set HTML</span> given <span>this</span>, <span>this</span>'s <span
data-x="concept-DocumentFragment-host">shadow host</span>, and <var>compliantHTML</var>.</p></li>
data-x="concept-DocumentFragment-host">shadow host</span>, <var>compliantHTML</var>,
and <var>options</var>.</p></li>
</ol>
</div>

<div algorithm>
<p>To <dfn>unsafely set HTML</dfn>, given an <code>Element</code> or <code>DocumentFragment</code>
<var>target</var>, an <code>Element</code> <var>contextElement</var>, and a <span>string</span>
<var>html</var>:</p>
<var>target</var>, an <code>Element</code> <var>contextElement</var>, a <span>string</span>
<var>html</var>, and <var>options</var>:</p>

<ol>
<li><p>Let <var>newChildren</var> be the result of the <span>HTML fragment parsing
Expand All @@ -123797,6 +123821,15 @@ enum <dfn enum>DOMParserSupportedType</dfn> {
<li><p>For each <var>node</var> in <var>newChildren</var>, <span
data-x="concept-node-append">append</span> <var>node</var> to <var>fragment</var>.</p></li>

<li>
<p>If <var>options</var>["<code data-x="dom-SetHTMLUnsafeOptions-runScripts">runScripts</code>"]
is true, set <span>already started</span> to false for all <code>script</code> element
shadow-including descendant of <var>fragment</var>.</p>

<p class="XXX">Do this in the parser by not setting <span>already started</span> to true in the
first place.</p>
</li>

<li><p><span data-x="concept-node-replace-all">Replace all</span> with <var>fragment</var> within
<var>target</var>.</p></li>
</ol>
Expand Down Expand Up @@ -124396,6 +124429,155 @@ interface <dfn interface>XMLSerializer</dfn> {

</div>


<h4>The <code data-x="dom-Element-streamHTMLUnsafe">streamHTMLUnsafe()</code> method</h4>

<dl class="domintro">
<dt><code data-x=""><var>element</var>.<span subdfn
data-x="dom-Element-streamHTMLUnsafe">streamHTMLUnsafe</span>(<var>options</var>)</code></dt>

<dd>
<p>Returns a writable stream that acts as the input stream of an HTML parser. <var>options</var>
can contain the following values:</p>

<ul>
<li><p><code data-x="dom-SetHTMLUnsafeOptions-runScripts">runScripts</code> can be set to true
to run scripts when they are inserted into the document.</p></li>
</ul>

<p>Existing children of <var>element</var> are removed and new nodes are added as they are
produced by the parser.</p>
</dd>

<dt><code data-x=""><var>shadowRoot</var>.<span subdfn
data-x="dom-ShadowRoot-streamHTMLUnsafe">streamHTMLUnsafe</span>(<var>options</var>)</code></dt>

<dd>
<p>Returns a writable stream that acts as the input stream of an HTML parser. <var>options</var>
has the same values as above. Existing children of <var>shadowRoot</var> are removed and new
nodes are added as they are produced by the parser.</p>
</dd>
</dl>

<p class="warning">These methods perform no sanitization to remove potentially-dangerous elements
and attributes like <code>script</code> or <span>event handler content attributes</span>.</p>

<div w-nodev>

<pre><code class="idl">partial interface <span>Element</span> {
WritableStream <span data-x="dom-Element-streamHTMLUnsafe">streamHTMLUnsafe</span>(optional <span>SetHTMLUnsafeOptions</span> options = {});
};

partial interface <span>ShadowRoot</span> {
WritableStream <span data-x="dom-ShadowRoot-streamHTMLUnsafe">streamHTMLUnsafe</span>(optional <span>SetHTMLUnsafeOptions</span> options = {});
};</code></pre>

<div algorithm>
<p><code>Element</code>'s <dfn method for="Element"><code
data-x="dom-Element-streamHTMLUnsafe">streamHTMLUnsafe(<var>options</var>)</code></dfn> method steps
are:</p>

<ol>
<li><p>If <var>this</var>'s <span>node document</span> is an <span data-x="XML documents">XML
document</span>, then throw an <span>"<code>InvalidStateError</code>"</span>
<code>DOMException</code>.</p></li>

<li><p>Let <var>disposition</var> be the result of invoking the <span
data-x="tt-shouldblock">should sink type mismatch violation be blocked by content security
policy?</span> algorithm given <span>this</span>'s <span>relevant global object</span>,
"<code data-x="">Element streamHTMLUnsafe</code>", "<code data-x="">script</code>", and
"".</p></li>

<li><p>If <var>disposition</var> is not "<code data-x="">Allowed</code>", then throw a
<code>TypeError</code> exception.</p></li>

<li><p>Let <var>target</var> be <span>this</span>'s <span>template contents</span> if
<span>this</span> is a <code>template</code> element; otherwise <span>this</span>.</p></li>

<li><p>Return the result of <span>unsafely stream HTML</span> given <var>target</var>,
Copy link
Member

Choose a reason for hiding this comment

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

Use <code> for string contents.

<span>this</span>, and <var>options</var>.</p></li>
</ol>
</div>

<div algorithm>
<p><code>ShadowRoot</code>'s <dfn method for="ShadowRoot"><code
data-x="dom-ShadowRoot-streamHTMLUnsafe">streamHTMLUnsafe(<var>options</var>)</code></dfn> method steps
are:</p>

<ol>
<li><p>If <var>this</var>'s <span>node document</span> is an <span data-x="XML documents">XML
document</span>, then throw an <span>"<code>InvalidStateError</code>"</span>
<code>DOMException</code>.</p></li>

<li><p>Let <var>disposition</var> be the result of invoking the <span
data-x="tt-shouldblock">should sink type mismatch violation be blocked by content security
policy?</span> algorithm given <span>this</span>'s <span>relevant global object</span>,
"<code data-x="">ShadowRoot streamHTMLUnsafe</code>", "<code data-x="">script</code>", and
Copy link
Member

Choose a reason for hiding this comment

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

This flag is currently internal to DOM (and just got renamed).

I also don't see how you are accounting for the suppression by creating your own mutation records. Am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

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

There's actually no good reason to suppress mutation observer here, I tested it for innerHTML in https://software.hixie.ch/utilities/js/live-dom-viewer/?saved=14325 and mutation observer is triggered. I'll remove the flag here.

Copy link
Member

Choose a reason for hiding this comment

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

You probably have to use the "replace all" algorithm though as I doubt removing the nodes one-by-one gives the record we want here.

"".</p></li>
Copy link
Member

Choose a reason for hiding this comment

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

the empty string*


Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<li> <p>Let <var>parser</var> be an <span>HTML parser</span> using the <span>streaming HTML
<li><p>Let <var>parser</var> be an <span>HTML parser</span> using the <span>streaming HTML

<li><p>If <var>disposition</var> is not "<code data-x="">Allowed</code>", then throw a
<code>TypeError</code> exception.</p></li>

<li><p>Return the result of <span>unsafely stream HTML</span> given <span>this</span>,
<span>this</span>'s <span data-x="concept-DocumentFragment-host">shadow host</span>, and
<var>options</var>.</p></li>
</ol>
</div>

<div algorithm>
<p>To <dfn>unsafely stream HTML</dfn>, given an <code>Element</code> or <code>DocumentFragment</code>
Copy link
Member

Choose a reason for hiding this comment

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

then return

<var>target</var>, an <code>Element</code> <var>contextElement</var>, and <var>options</var>:</p>

<ol>
<li><p><span data-x="concept-node-remove">Remove</span> all <var>target</var>'s <span
data-x="concept-tree-child">children</span>, in <span>tree order</span>.</p></li>

<li><p>Let <var>parser</var> be an <span>HTML parser</span> using the <span>streaming HTML
fragment parsing algorithm</span> given <var>target</var>, <var>contextElement</var>,
and <var>options</var>.</p></li>

<li>
<p>Let <var>writeAlgorithm</var> be an algorithm which takes a <var>chunk</var> argument and
runs the following steps:</p>

<ol>
<li><p>Let <var>input</var> be the result of <span
data-x="concept-idl-convert">converting</span> <var>chunk</var> to a <code
data-x="idl-DOMString">DOMString</code>. If this throws an exception, then return <span>a
promise rejected with</span> that exception.</p></li>

<li>Place the <var>input</var> into the <span>input stream</span> of <var>parser</var>. The
encoding <span data-x="concept-encoding-confidence">confidence</span> is
<i>irrelevant</i>.</li>

<li><p>Let <var>parser</var> run until it has consumed all the characters just inserted into
the input stream.</p></li>

<li><p>Return <span>a promise resolved with</span> undefined.<p></li>
</ol>
</li>

<li><p>Let <var>writable</var> be a <span>new</span> <code>WritableStream</code> in
<span data-x="concept-incumbent-realm">incumbent realm</span>.</p></li>

<li><p><span>Set up</span> <var>writable</var> with <var>writeAlgorithm</var>.</p></li>

<li><p>Return <var>writable</var>.</p></li>
</ol>
</div>

</div>

<div class="example">
<p>This is how you would replace the contents of an element with new contents appearing as it
arrives over the network:</p>

<pre><code class="js">const response = await fetch('/fragments/something');
const decoder = new TextDecoderStream();
const writable = element.streamHTMLUnsafe();
await response.body.pipeThrough(decoder).pipeTo(writable);</code></pre>
</div>

<h3 split-filename="timers-and-user-prompts" id="timers">Timers</h3>

<p>The <code data-x="dom-setTimeout">setTimeout()</code> and <code
Expand Down Expand Up @@ -144561,7 +144743,28 @@ console.assert(container.firstChild instanceof SuperP);

</div>

<div algorithm>
<p>The <dfn>streaming HTML fragment parsing algorithm</dfn>, given an <code>Element</code> or
<code>DocumentFragment</code> <var>target</var>, an <code>Element</code>
<var>contextElement</var>, and <var>options</var> is the same as the <span>HTML fragment parsing
Copy link
Member

Choose a reason for hiding this comment

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

Given that a lot of the complexity is here, I was hoping this would have been a bit more flushed out before the stage 2 request. The overall API shape seems fine, but I feel like as we're discussing this feature we keep discovering new issues with the supposedly "straightforward" HTML parser integration and we still don't quite know what that is going to look like.

algorithm</span> with these differences:</p>

<ul>
<li><p>There is no <i>input</i> argument, instead input is placed into the <span>input
stream</span> by the caller.</p></li>

<li><p>If <var>options</var>["<code data-x="dom-SetHTMLUnsafeOptions-runScripts">runScripts</code>"]
is true, <span>already started</span> is set to false for <code>script</code> elements before
they are inserted.</p></li>

<li><p>When the parser would insert a node to the <i>root</i> node, that node is instead appended
to <var>target</var>.</p></li>

<li><p>The algorithm doesn't return anything.</p></li>
</ul>

<p class="XXX">Achieve the above by refactoring instead of enumerating the differences.</p>
</div>

<h3 split-filename="named-characters"><dfn>Named character references</dfn></h3>

Expand Down Expand Up @@ -155692,6 +155895,9 @@ INSERT INTERFACES HERE
<dt id="refsSTORAGE">[STORAGE]</dt>
<dd><cite><a href="https://storage.spec.whatwg.org/">Storage</a></cite>, A. van Kesteren. WHATWG.</dd>

<dt id="refsSTREAMS">[STREAMS]</dt>
<dd><cite><a href="https://streams.spec.whatwg.org/">Streams</a></cite>, A. Rice, D. Denicola, M. Buelens, T. Yoshino. WHATWG.</dd>

<dt id="refsSVG">[SVG]</dt>
<dd><cite><a href="https://svgwg.org/svg2-draft/">Scalable Vector Graphics (SVG) 2</a></cite>, N Andronikos, R. Atanassov, T. Bah, B. Birtles, B. Brinza, C. Concolato, E. Dahlström, C. Lilley, C. McCormack, D. Schepers, R. Schwerdtfeger, D. Storey, S. Takagi, J. Watt. W3C.</dd>

Expand Down