Skip to content

Coherent story for HTML-setting methods #11669

@noamr

Description

@noamr

[Note: OP edited after some discussion]

What is the issue with the HTML Standard?

With the introduction of setHTML{Unsafe}, we have multiple ways of applying an HTML string into an existing document:

  • setHTML
  • setHTMLUnsafe
  • innerHTML and outerHTML setters
  • createContextualFragment
  • insertAdjacentHTML
  • Detached document streaming (document.write() into an inactive document and inserting the root node the active one)

With #2142 and #11542, we are about to introduce even more methods that insert HTML asynchronously using a stream.
Some of these techniques support different levels of sanitation and slightly different behaviors.

So this is a good time to have a wider overview of these methods, and come up with a consistent structure for their API.

Specifically, the following variants apply when inserting HTML:

API shape

-Treat unsafe/safe stream/set and replaceChildren/replaceWith/append/prepend/before/after as permutations that are best exposed as different methods (226 = 20 methods). We can tweak this of course

dictionary UnsafeHTMLSetterOptions  {
  (Sanitizer or SanitizerConfig)? sanitizer = null;
  boolean? runScripts = false;
};

dictionary SafeHTMLSetterOptions {
  // The user of this dictionary must ensure a sanitizer is provided.
  (Sanitizer or SanitizerConfig) sanitizer;
};

[Exposed=Window]
interface ParentNode {
  void setHTML((DOMString or TrustedHTML) html, SafeHTMLSetterOptions options);
  void setHTMLUnsafe((DOMString or TrustedHTML) html, optional UnsafeHTMLSetterOptions options = {});
  void beforeHTML((DOMString or TrustedHTML) html, SafeHTMLSetterOptions options);
  void beforeHTMLUnsafe((DOMString or TrustedHTML) html, optional UnsafeHTMLSetterOptions options = {});
  void afterHTML((DOMString or TrustedHTML) html, SafeHTMLSetterOptions options);
  void afterHTMLUnsafe((DOMString or TrustedHTML) html, optional UnsafeHTMLSetterOptions options = {});
  void appendHTML((DOMString or TrustedHTML) html, SafeHTMLSetterOptions options);
  void appendHTMLUnsafe((DOMString or TrustedHTML) html, optional UnsafeHTMLSetterOptions options = {});
  void prependHTML((DOMString or TrustedHTML) html, SafeHTMLSetterOptions options);
  void prependHTMLUnsafe((DOMString or TrustedHTML) html, optional UnsafeHTMLSetterOptions options = {});
  void replaceWithHTML((DOMString or TrustedHTML) html, SafeHTMLSetterOptions options);
  void replaceWithHTMLUnsafe((DOMString or TrustedHTML) html, optional UnsafeHTMLSetterOptions options = {});
  WritableStream streamHTML(SafeHTMLSetterOptions options);
  WritableStream streamHTMLUnsafe(optional UnsafeHTMLSetterOptions options = {});
  WritableStream streamBeforeHTML(SafeHTMLSetterOptions options);
  WritableStream streamBeforeHTMLUnsafe(optional UnsafeHTMLSetterOptions options = {});
  WritableStream streamAfterHTML(SafeHTMLSetterOptions options);
  WritableStream streamAfterHTMLUnsafe(optional UnsafeHTMLSetterOptions options = {});
  WritableStream streamAppendHTML(SafeHTMLSetterOptions options);
  WritableStream streamAppendHTMLUnsafe(optional UnsafeHTMLSetterOptions options = {});
  WritableStream streamPrependHTML(SafeHTMLSetterOptions options);
  WritableStream streamPrependHTMLUnsafe(optional UnsafeHTMLSetterOptions options = {});
  WritableStream streamReplaceWithHTML(SafeHTMLSetterOptions options);
  WritableStream streamReplaceWithHTMLUnsafe(optional UnsafeHTMLSetterOptions options = {});
};

Alternative: 12 methods, with insertion mode (taken from #10122)

enum HTMLInsertionPoint { "before", "after", "start", "end" };

// Hypothetical interface where these methods would live,
// for example, on Element or ShadowRoot.
[Exposed=Window]
interface ParentNode {

  void setHTML((DOMString or TrustedHTML) html, SafeHTMLSetterOptions options);
  void setHTMLUnsafe((DOMString or TrustedHTML) html, optional UnsafeHTMLSetterOptions options = {});
  void insertHTML((DOMString or TrustedHTML) html, HTMLInsertionPoint insertionPoint, SafeHTMLSetterOptions options);
  void insertHTMLUnsafe((DOMString or TrustedHTML) html, HTMLInsertionPoint insertionPoint, optional UnsafeHTMLSetterOptions options = {});
  void replaceWithHTML((DOMString or TrustedHTML) html, SafeHTMLSetterOptions options);
  void replaceWithHTMLUnsafe((DOMString or TrustedHTML) html, optional UnsafeHTMLSetterOptions options = {});
  WritableStream streamHTML(SafeHTMLSetterOptions options);
  WritableStream streamHTMLUnsafe(optional UnsafeHTMLSetterOptions options = {});
  WritableStream streamInsertHTML(HTMLInsertionPoint insertionPoint, SafeHTMLSetterOptions options);
  WritableStream streamInsertHTMLUnsafe(HTMLInsertionPoint insertionPoint, optional UnsafeHTMLSetterOptions options = {});
  WritableStream streamReplaceWithHTML(SafeHTMLSetterOptions options);
  WritableStream streamReplaceWithHTMLUnsafe(optional UnsafeHTMLSetterOptions options = {});
}};

Alternative: pass ReadableStream instead of returning a WritableStream:

[Exposed=Window]
interface ParentNode {
  Promise<void> streamHTML((ReadableStream or TrustedReadableStream), SafeHTMLSetterOptions options);
  // ...
};

This is simpler from a trusted types perspective, as the stream has to be trusted as a whole rather than the chunks.
OTOH the API feels a bit less "streamy" than returning a Writable.

Other notes

For the interleaved case, as well as for trusted types, suggesting to do that via transform streams:

The aforementioned issues include more details about these, but they are designed in a way that shouldn't add more complexity to the normal case.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions