html-rewriter-wasm

repository·master·Indexed 42 days ago

https://github.com/cloudflare/html-rewriter-wasm

A WebAssembly implementation of the Cloudflare Workers HTMLRewriter API using lol-html, designed for high-performance HTML transformation in environments like Miniflare. Version 0.3.0 provides the HTMLRewriter class for parsing and transforming HTML streams via selectors and handlers, including support for Element, Comment, and TextChunk manipulation and ESI tag compatibility.

Tokens
2K
Snippets
4
Records
9
Agent score
3%

What's inside html-rewriter-wasm

  1. Handle constraints when using async handlers

    master

    When using async handlers, the implementation uses WebAssembly Asyncify to unwind the stack. This imposes strict concurrency constraints:

    1. No concurrent writes/ends: You must await every call to .write() and .end() before calling them again. You cannot use Promise.all() on multiple .write() calls.
    2. Register handlers early: You cannot add new handlers via .on() after .write() or .end() has been called.
    3. Single use of .end(): The .end() method can only be called once per instance. Create a new HTMLRewriter instance for every new transformation task.
    // ✅ Correct pattern for async handlers
    const rewriter = new HTMLRewriter(...).on("p", {
      async element(element) {
        await fetch(...);
        element.setInnerContent("new");
      }
    });
    
    await rewriter.write(encoder.encode("<p>1</p>"));
    await rewriter.write(encoder.encode("<p>2</p>"));
    await rewriter.end();
  2. Build the html-rewriter-wasm package

    master

    To build the package, you must first install a specific fork of wasm-pack that supports asyncify_get_state (via Binaryen version 92). Then run the npm build command.

    Prerequisites:

    • mrbbot's fork of wasm-pack installed via cargo.

    Build steps:

    $ cargo install --git https://github.com/mrbbot/wasm-pack
    $ npm run build
  3. Use HTMLRewriter to transform HTML

    master

    The HTMLRewriter class allows you to transform HTML content using handlers. You instantiate it with a callback function that receives chunks of the transformed output. You then register handlers using .on(selector, handler) and process data using .write(encodedChunk) and .end().

    Important: You must call .free() on the instance to release WebAssembly memory once you are finished with the rewriter.

    import { HTMLRewriter } from "html-rewriter-wasm";
    
    const encoder = new TextEncoder();
    const decoder = new TextDecoder();
    
    let output = "";
    const rewriter = new HTMLRewriter((outputChunk) => {
      output += decoder.decode(outputChunk);
    });
    
    rewriter.on("p", {
      element(element) {
        element.setInnerContent("new");
      },
    });
    
    try {
      await rewriter.write(encoder.encode("<p>old</p>"));
      await rewriter.end();
      console.log(output); // <p>new</p>
    } finally {
      rewriter.free(); // Remember to free memory
    }
  4. Enable ESI tag compatibility

    master

    To enable the html_rewriter_treats_esi_include_as_void_tag compatibility flag (which treats <esi:include> as a void tag), pass an options object to the HTMLRewriter constructor with enableEsiTags: true.

    const rewriter = new HTMLRewriter((outputChunk) => { ... }, {
      enableEsiTags: true,
    });
  5. Configure ContentTypeOptions for insertions

    master

    When using methods like before, after, replace, prepend, append, or setInnerContent, you can pass an optional ContentTypeOptions object to specify how the content should be treated.

    export interface ContentTypeOptions {
      html?: boolean;
    }

    Setting html: true tells the rewriter to treat the provided string as HTML rather than plain text.

  6. Manipulate elements with the Element class

    master

    The Element class provides methods to modify HTML elements matched by a selector.

    Attribute Manipulation:

    • getAttribute(name: string): string | null
    • hasAttribute(name: string): boolean
    • setAttribute(name: string, value: string): this
    • removeAttribute(name: string): this
    • attributes: An IterableIterator<[string, string]> of all attributes.

    Content and Structure Manipulation:

    • before(content: string, options?: ContentTypeOptions): Inserts content before the element.
    • after(content: string, options?: ContentTypeOptions): Inserts content after the element.
    • prepend(content: string, options?: ContentTypeOptions): Inserts content at the start of the element.
    • append(content: string, options?: ContentTypeOptions): Inserts content at the end of the element.
    • replace(content: string, options?: ContentTypeOptions): Replaces the element with new content.
    • setInnerContent(content: string, options?: ContentTypeOptions): Replaces the element's inner content.
    • remove(): Removes the element.
    • removeAndKeepContent(): Removes the element but keeps its children.

    Lifecycle and Metadata:

    • tagName: string
    • namespaceURI: string
    • removed: boolean
    • onEndTag(handler: (endTag: EndTag) => void | Promise<void>): void: Registers a callback for when the element's closing tag is encountered.
  7. Use the HTMLRewriter class to transform HTML

    master

    The HTMLRewriter class is the primary entry point for parsing and transforming HTML streams. You initialize it with an outputSink function that receives chunks of the transformed HTML as Uint8Array.

    To perform transformations, use .on(selector, handlers) to target specific elements, or .onDocument(handlers) to target document-level events like doctypes or the end of the document. After setting up handlers, feed HTML chunks into the rewriter using .write(chunk) and finalize the process with .end().

    const output: Uint8Array[] = [];
    const rewriter = new HTMLRewriter((chunk) => {
      output.push(chunk);
    });
    
    rewriter
      .on('a', {
        element(element) {
          element.setAttribute('class', 'transformed-link');
        }
      })
      .onDocument({
        end(docEnd) {
          docEnd.append('<!-- End of Document -->');
        }
      });
    
    const encoder = new TextEncoder();
    await rewriter.write(encoder.encode('<html><body><a href="#">Link</a></body></html>'));
    await rewriter.end();
    
    const result = new TextDecoder().decode(new Uint8Array(await Promise.all(output.map(async (c) => c))));
    // result: <html><body><a class="transformed-link" href="#">Link</a></body></html>
  8. Manipulate comments and text with Comment and TextChunk classes

    master

    The Comment and TextChunk classes allow you to modify non-element nodes.

    Comment Class:

    • text: string: The content of the comment.
    • before(content: string, options?: ContentTypeOptions): this
    • after(content: string, options?: ContentTypeOptions): this
    • replace(content: string, options?: ContentTypeOptions): this
    • remove(): this

    TextChunk Class:

    • text: string: The text content.
    • before(content: string, options?: ContentTypeOptions): this
    • after(content: string, options?: ContentTypeOptions): this
    • replace(content: string, options?: ContentTypeOptions): this
    • remove(): this
    • lastInTextNode: boolean: Indicates if this is the last chunk in a text node.
    • removed: boolean