From e47a15c804b8c5a6c4de9b549616f78a9964675e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= Date: Thu, 4 Sep 2025 09:36:43 +0200 Subject: [PATCH 1/4] Add patchUnsafe() methods Depends on https://github.com/w3c/trusted-types/pull/597 for Trusted Types integration. --- source | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/source b/source index 631f655f890..23ce72afcb2 100644 --- a/source +++ b/source @@ -123478,7 +123478,7 @@ document.body.appendChild(frame)

DOM parsing and serialization APIs

-
partial interface Element {
+  
partial interface Element {
   [CEReactions] undefined setHTMLUnsafe((TrustedHTML or DOMString) html);
   DOMString getHTML(optional GetHTMLOptions options = {});
 
@@ -123487,7 +123487,7 @@ document.body.appendChild(frame)
[CEReactions] undefined insertAdjacentHTML(DOMString position, (TrustedHTML or DOMString) string); }; -partial interface ShadowRoot { +partial interface ShadowRoot { [CEReactions] undefined setHTMLUnsafe((TrustedHTML or DOMString) html); DOMString getHTML(optional GetHTMLOptions options = {}); @@ -124357,6 +124357,81 @@ interface XMLSerializer { +

Patching

+ +

TODO: introduction, what's all this?

+ +
partial interface Element {
+  WritableStream patchUnsafe(optional PatchUnsafeOptions options = {});
+};
+
+partial interface ShadowRoot {
+  WritableStream patchUnsafe(optional PatchUnsafeOptions options = {});
+};
+
+dictionary PatchUnsafeOptions {
+  TrustedTransformStream trustedTransformStream;
+};
+ +
+ +

Element's patchUnsafe(options) method steps + are:

+ +
    +
  1. Let writable be a new WritableStream.

  2. + +
  3. +

    If options["trustedTransformStream"] exists:

    + +
      +
    1. Set writable to the result of piping writable through options["trustedTransformStream"].

    2. +
    +
  4. + +
  5. +

    Otherwise:

    + +
      +
    1. If there's a default TT policy, use that and wrap writable 👋

    2. +
    +
  6. + +
  7. Create a new parser and do all the actual work 👋

  8. + +
  9. Return writable.

  10. +
+ +
+

Do a thing like this:

+
const policy = trustedTypes.createPolicy("my-policy", {
+  createTransformStream() {
+    return new TransformStream({
+      transform(chunk, controller) {
+        // TODO: some buffering
+        controller.enqueue(sanitize(chunk));
+      }
+    });
+  }
+});
+
+const trustedTransformStream = policy.createTransformStream(input);
+const writable = element.patchUnsafe({ trustedTransformStream });
+const response = await fetch('/fragments/something');
+response.body.pipeTo(writable);
+
+ +

ShadowRoot's patchUnsafe(options) method steps + are:

+ +
    +
  1. TODO
  2. +
+ +
+

Timers

The setTimeout() and Date: Thu, 2 Oct 2025 10:01:19 +0200 Subject: [PATCH 2/4] Flesh out the idea more --- source | 56 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/source b/source index 23ce72afcb2..5973a3646fb 100644 --- a/source +++ b/source @@ -124370,11 +124370,12 @@ partial interface ShadowRoot { }; dictionary PatchUnsafeOptions { - TrustedTransformStream trustedTransformStream; + boolean runScripts = false; };

+

Element's patchUnsafe(options) method steps are:

@@ -124383,25 +124384,58 @@ dictionary PatchUnsafeOptions {
  • Let writable be a new WritableStream.

  • -

    If options["trustedTransformStream"] exists:

    +

    👋 Sketch of the streams setup:

      -
    1. Set writable to the result of piping writable through options["trustedTransformStream"].

    2. +
    3. Incoming chunks must all be of the same type, either strings, bytes, or a trusted types + wrapper.

    4. + +
    5. +

      Some decisions are made based on the first chunk:

      + +
        +
      1. If it's not a trusted types wrapper and there is a default TT policy, create a + transform stream using createTransformStream from the default policy. + Then pipe chunks through that transform stream.

      2. + +
      3. If it's bytes, create an UTF-8 TextDecoderStream and pipe chunks through that.

      4. + +
      5. TODO: which should happen first? should we allow the TT transform stream to take bytes + as input, or guarantee that it's strings by then?

      6. +
      +
    6. + +
    7. As chunks are coming through, before handing them to the parser, check that they're all + of the same type. For trusted types, also check that the chunks are in the same order and not + duplicated/filtered/reordered.

  • -

    Otherwise:

    +

    👋 Sketch of the parser setup:

      -
    1. If there's a default TT policy, use that and wrap writable 👋

    2. +
    3. Let parser be a new fragment parser.

    4. + +
    5. Add this to the stack of open elements.

    6. + +
    7. If options["runScripts"], + don't mark scripts as already executed.

    8. + +
    9. Write chunks into the parser as they are written to writable.

    10. + +
    11. When a template element with a patchfor attribute + comes out of the parser, find the target node by ID and set that as the parser's insertion + point.

    12. + +
    13. The first time an element is patched, replace all children. Subsequent patches to the + same target append children.

  • -
  • Create a new parser and do all the actual work 👋

  • -
  • Return writable.

  • +

    Do a thing like this:

    @@ -124416,12 +124450,13 @@ dictionary PatchUnsafeOptions { } }); -const trustedTransformStream = policy.createTransformStream(input); -const writable = element.patchUnsafe({ trustedTransformStream }); const response = await fetch('/fragments/something'); -response.body.pipeTo(writable);
    +const transform = policy.createTransformStream(); +const writable = element.patchUnsafe(); +await response.body.pipeThrough(transform).pipeTo(writable);
    +

    ShadowRoot's patchUnsafe(options) method steps are:

    @@ -124429,6 +124464,7 @@ response.body.pipeTo(writable);
    1. TODO
    +
    From c9be483df3c8196107074fb053641422f8506f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= Date: Mon, 6 Oct 2025 16:02:36 +0200 Subject: [PATCH 3/4] Rename to streamHTMLUnsafe --- source | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/source b/source index 5973a3646fb..13c9e49b7e3 100644 --- a/source +++ b/source @@ -124362,22 +124362,22 @@ interface XMLSerializer {

    TODO: introduction, what's all this?

    partial interface Element {
    -  WritableStream patchUnsafe(optional PatchUnsafeOptions options = {});
    +  WritableStream streamHTMLUnsafe(optional StreamHTMLUnsafeOptions options = {});
     };
     
     partial interface ShadowRoot {
    -  WritableStream patchUnsafe(optional PatchUnsafeOptions options = {});
    +  WritableStream streamHTMLUnsafe(optional StreamHTMLUnsafeOptions options = {});
     };
     
    -dictionary PatchUnsafeOptions {
    -  boolean runScripts = false;
    +dictionary StreamHTMLUnsafeOptions {
    +  boolean runScripts = false;
     };

    Element's patchUnsafe(options) method steps + data-x="dom-Element-streamHTMLUnsafe">streamHTMLUnsafe(options) method steps are:

      @@ -124419,17 +124419,10 @@ dictionary PatchUnsafeOptions {
    1. Add this to the stack of open elements.

    2. -
    3. If options["runScripts"], +

    4. If options["runScripts"], don't mark scripts as already executed.

    5. Write chunks into the parser as they are written to writable.

    6. - -
    7. When a template element with a patchfor attribute - comes out of the parser, find the target node by ID and set that as the parser's insertion - point.

    8. - -
    9. The first time an element is patched, replace all children. Subsequent patches to the - same target append children.

    @@ -124452,13 +124445,13 @@ dictionary PatchUnsafeOptions { const response = await fetch('/fragments/something'); const transform = policy.createTransformStream(); -const writable = element.patchUnsafe(); +const writable = element.streamHTMLUnsafe(); await response.body.pipeThrough(transform).pipeTo(writable);

    ShadowRoot's patchUnsafe(options) method steps + data-x="dom-ShadowRoot-streamHTMLUnsafe">streamHTMLUnsafe(options) method steps are:

      From cc255eac6ef94321ef17723c1956f45b24f87a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20J=C3=A4genstedt?= Date: Mon, 6 Oct 2025 16:10:03 +0200 Subject: [PATCH 4/4] Remove handling of byte chunks, betting on textStream() --- source | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/source b/source index 13c9e49b7e3..ca6e72728ab 100644 --- a/source +++ b/source @@ -124387,27 +124387,15 @@ dictionary StreamHTMLUnsafeOptions {

      👋 Sketch of the streams setup:

        -
      1. Incoming chunks must all be of the same type, either strings, bytes, or a trusted types - wrapper.

      2. +
      3. Incoming chunks must all be of the same type, either strings or a trusted types + wrapper. Check this on every chunk and treat mixing as an error.

      4. -
      5. -

        Some decisions are made based on the first chunk:

        - -
          -
        1. If it's not a trusted types wrapper and there is a default TT policy, create a - transform stream using createTransformStream from the default policy. - Then pipe chunks through that transform stream.

        2. - -
        3. If it's bytes, create an UTF-8 TextDecoderStream and pipe chunks through that.

        4. - -
        5. TODO: which should happen first? should we allow the TT transform stream to take bytes - as input, or guarantee that it's strings by then?

        6. -
        -
      6. +
      7. If the first chunk is not a trusted types wrapper and there is a default TT policy, + create a transform stream using createTransformStream from the default + policy. Then pipe chunks through that transform stream.

      8. -
      9. As chunks are coming through, before handing them to the parser, check that they're all - of the same type. For trusted types, also check that the chunks are in the same order and not - duplicated/filtered/reordered.

      10. +
      11. For trusted types handled "outside" (not by the internal transform stream) check that + the chunks are in the same order and not duplicated/filtered/reordered.