ultrahtml

repository·main·Indexed 20 days ago

https://github.com/natemoo-re/ultrahtml

A lightweight (1.75kB), zero-dependency HTML-like parser and transformer compatible with any JavaScript runtime. It supports HTML, Astro, Vue, and Svelte syntaxes, providing a fault-tolerant parser, AST walk and transform utilities, built-in sanitization, and a hyperscript-style h() helper. Features include a tagged template utility for ergonomic HTML construction, CSS inlining and scoping transformers, and CSS selector support via querySelector and querySelectorAll.

Tokens
4.8K
Snippets
18
Records
19
Agent score
69%

What's inside ultrahtml

  1. Overview of ultrahtml

    main

    ultrahtml is a tiny (1.75kB), zero-dependency library designed for enhancing and parsing HTML-like syntax. It is compatible with any JavaScript runtime and supports HTML, Astro, Vue, Svelte, and other HTML-like syntaxes.

    Key features include:

    • A fault-tolerant parser.
    • AST walk and transform utilities.
    • Built-in sanitization.
    • A html template utility.
    • querySelector and querySelectorAll support via ultrahtml/selector.
  2. Serialize an AST to a string with render

    main

    The render function takes an AST and serializes it back into an HTML string. This is an async operation.

    import { parse, render } from "ultrahtml";
    
    const ast = parse(`<h1>Hello world!</h1>`);
    const output = await render(ast);
  3. Traverse the AST with walk and walkSync

    main

    The walk function allows you to traverse the Abstract Syntax Tree (AST) to scan for text, elements, components, or perform validations.

    • walk: An async function that must be awaited. Use this if your tree contains async components.
    • walkSync: A synchronous version of walk. Use this only when it is guaranteed that there are no async components in the tree.

    Both functions accept an AST and a callback function that receives a node.

    import { parse, walk, ELEMENT_NODE } from "ultrahtml";
    
    const ast = parse(`<h1>Hello world!</h1>`);
    await walk(ast, async (node) => {
      if (node.type === ELEMENT_NODE && node.name === "script") {
        throw new Error("Found a script!");
      }
    });
  4. Modify markup with transform and transformSync

    main

    The transform function provides a way to modify markup by applying a sequence of transformers. You can use built-in transformers like swap and sanitize, or write your own.

    • transform: An async function for applying transformers.
    • transformSync: A synchronous version of transform. Use this only when it is guaranteed that there are no async functions in the transformers.

    It is often used in conjunction with the html template utility to define custom component transformations.

    import { transform, html } from "ultrahtml";
    import swap from "ultrahtml/transformers/swap";
    import sanitize from "ultrahtml/transformers/sanitize";
    
    const output = await transform(`<h1>Hello world!</h1>`, [
      swap({
        h1: "h2",
        h3: (props, children) => html`<h2 class="ultra">${children}</h2>`,
      }),
      sanitize({ allowElements: ["h1", "h2", "h3"] }),
    ]);
    
    console.log(output); // <h2>Hello world!</h2>
  5. Configure `InlineOptions` for the inline transformer

    main

    When calling inline(opts), you can provide an options object to control how styles are applied and how media queries are evaluated.

    Options

    OptionTypeDescription
    useObjectSyntaxbooleanIf true, the style attribute on nodes will be emitted as an object instead of a CSS string. Defaults to false.
    envPartial<Environment> & { width: number; height: number }An environment object used to evaluate @media queries. It must include width and height.

    Environment Resolution

    The env object automatically resolves several dimension-related properties if they are not explicitly provided:

    • dppx: Device pixel ratio (defaults to 1).
    • widthPx: Set to width if not provided.
    • heightPx: Set to height if not provided.
    • deviceWidthPx: Set to width * dppx if not provided.
    • deviceHeightPx: Set to height * dppx if not provided.
    import inline from './src/transformers/inline.js';
    
    const transformer = inline({
      useObjectSyntax: true,
      env: {
        width: 1920,
        height: 1080,
        dppx: 2
      }
    });
    
    const transformedDoc = transformer(doc);
    // Nodes will have node.attributes.style as an object: { color: 'red' }
  6. Configure ScopeOptions for the scope transformer

    main

    When initializing the scope transformer, you can provide a ScopeOptions object to customize how scoping is applied.

    OptionTypeDescription
    hashstring (optional)A custom string to use as the scope identifier. If not provided, a shorthash of the rendered document is generated automatically.
    attributestring (optional)The name of the attribute used to store the scope ID on elements (e.g., data-scope). If provided, the transformer will only process <style> tags that possess this attribute.
    export interface ScopeOptions {
    	hash?: string;
    	attribute?: string;
    }
  7. Configure the sanitize transformer

    main

    The ultrahtml/transformers/sanitize transformer implements an API based on a proposed HTML Sanitizer API. It allows you to control which elements, attributes, components, and comments are retained or removed.

    Use these options to define your sanitization policy:

    | Option | Type | Default | Description |
    | -------------------------- | ------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `allowElements` | `string[]` | `undefined` | An array of strings indicating elements that the sanitizer should not remove. All elements not in the array will be dropped. |
    | `blockElements` | `string[]` | `undefined` | An array of strings indicating elements that the sanitizer should remove, but keep their child elements. |
    | `unblockElements` | `string[]` | `undefined` | An array of strings indicating elements that the sanitizer should not remove. All elements not in the array will be removed, but keep their child content. |
    | `dropElements` | `string[]` | `["script"]` | An array of strings indicating elements (including nested elements) that the sanitizer should remove. |
    | `allowAttributes` | `Record<string, string[]>` | `undefined` | An object where each key is the attribute name and the value is an Array of allowed tag names. Matching attributes will not be removed. All attributes that are not in the array will be dropped. |
    | `dropAttributes` | `Record<string, string[]>` | `undefined` | An object where each key is the attribute name and the value is an Array of dropped tag names. Matching attributes will be removed. |
    | `allowComponents` | `boolean` | `false` | A boolean value set to false (default) to remove components and their children. If set to true, components will be subject to built-in and custom configuration checks (and will be retained or dropped based on those checks). |
    | `allowCustomElements` | `boolean` | `false` | A boolean value set to false (default) to remove custom elements and their children. If set to true, custom elements will be subject to built-in and custom configuration checks (and will be retained or dropped based on those checks). |
    | `allowComments` | `boolean` | `false` | A boolean value set to false (default) to remove HTML comments. Set to true in order to keep comments. |
  8. Transform HTML using `transform()` and `transformSync()`

    main

    The transform functions provide a high-level pipeline: they parse the input (string or Node), apply a sequence of Transformer functions to the AST, and then render the resulting AST back to a string. This is useful for building HTML manipulation pipelines (e.g., minification, sanitization, or component injection).

    import { transform, transformSync, Transformer } from 'ultrahtml';
    
    const myTransformer: Transformer = async (node) => {
      // logic to modify the node
      return node;
    };
    
    const result = await transform('<div></div>', [myTransformer]);
    const resultSync = transformSync('<div></div>', [(node) => node]);
  9. Create virtual nodes with `h()`

    main

    The h function is a hyperscript-style helper for creating ElementNode objects manually. It allows you to construct an AST without parsing a string.

    • type: The tag name (string) or a function (which triggers a custom RenderFn).
    • props: An optional object containing attributes.
    • ...children: Any number of child nodes or strings (which are automatically converted to TextNode).
    import { h } from 'ultrahtml';
    
    const vnode = h('div', { class: 'container' }, 
      h('h1', null, 'Title'),
      'Just some text'
    );
  10. Query multiple nodes with querySelectorAll()

    main

    Use querySelectorAll(node, selector) to find all Node elements that match the provided CSS selector within the given node tree. It returns an array of matching Node objects.

    import { querySelectorAll } from 'ultrahtml/selector';
    
    const allMatches = querySelectorAll(rootNode, 'div > p');
  11. Render an AST to an HTML string with `render()`

    main

    The render function (async) and renderSync function (synchronous) convert an AST (Node) back into an HTML string. It handles element tags, attributes, text content, comments, and doctypes. It also supports custom rendering logic if a RenderFn is attached to an ElementNode.

    import { render, renderSync, parse } from 'ultrahtml';
    
    const ast = parse('<div><p>Hello</p></div>');
    
    // Async rendering
    const htmlAsync = await render(ast);
    
    // Synchronous rendering
    const htmlSync = renderSync(ast);
  12. Query nodes with querySelector()

    main

    Use querySelector(node, selector) to find the first Node that matches the provided CSS selector within the given node tree. If no match is found, it returns undefined (or throws if the selector is invalid).

    import { querySelector } from 'ultrahtml/selector';
    
    const firstMatch = querySelector(rootNode, '.my-class');