Parse and Edit HTML Strings with @lukeed/html

Category: Javascript | November 2, 2026
Authorlukeed
Last UpdateNovember 2, 2026
LicenseMIT
Tags
Views0 views
Parse and Edit HTML Strings with @lukeed/html

@lukeed/html is a JavaScript HTML parser for querying HTML strings with CSS selectors and locating nodes in the original markup.

It reproduces the unedited source exactly and includes a mutable entry for changing attributes, text, and elements.

Features

  • Attribute selectors, combinators, and structural pseudo-classes.
  • Lazy decoding of text and attribute values.
  • Original source offsets for every parsed node.
  • Line and column lookup for matched elements.
  • Stable node identity across repeated reads.
  • htmlparser2-style recovery from malformed HTML.
  • Source-preserving serialization after targeted edits.

How To Use It

Installation

Install the package with npm.

npm install @lukeed/html

Basic Usage

Import the read-only entry to query HTML and reproduce the original string:

import { parse, stringify } from "@lukeed/html";
const source = '<main><h1>Projects &amp; Notes</h1><a href="/docs">Docs</a></main>';
const doc = parse(source);
console.log(doc.find("h1")?.text); // "Projects & Notes"
console.log(doc.find('a[href="/docs"]')?.getAttr("href")); // "/docs"
console.log(stringify(doc) === source); // true

For browser use, load the ESM build from a CDN:

<script type="module">
  import { parse } from "https://cdn.jsdelivr.net/npm/@lukeed/[email protected]/+esm";
  const doc = parse('<nav><a href="/about">About</a></nav>');
  console.log(doc.find("nav > a")?.getAttr("href")); // "/about"
</script>

Find Markup by Line and Column

Search for images missing an alt attribute and report their positions in the input string:

import { parse, stringify } from "@lukeed/html";
const source = `<article>
  <img src="/hero.jpg">
  <img src="/logo.svg" alt="Site logo">
</article>`;
const doc = parse(source);
for (const img of doc.all("img:not([alt])")) {
  const { line, column } = doc.position(img.start);
  console.log(`${line}:${column}`, stringify(img));
}

Edit HTML with the Mutable Entry

Import @lukeed/html/mutable for edits. Insertion methods accept nodes or strings; strings become text. Parse a fragment before inserting its HTML elements.

import { parse, stringify } from "@lukeed/html/mutable";
const doc = parse("<main><h2>Updates</h2><ul><li>First</li></ul></main>");
const heading = doc.find("h2");
if (heading) heading.name = "h1";
doc.find("li")?.setAttr("class", "featured");
doc.find("ul")?.append(...parse("<li>Second</li>").children);
console.log(stringify(doc));
// <main><h1>Updates</h1><ul><li class="featured">First</li><li>Second</li></ul></main>

API Reference

Module Functions

Both @lukeed/html and @lukeed/html/mutable export parse and stringify:

FunctionDescription
parse(source: string): DocumentParses an HTML string into a document and recovers malformed markup using htmlparser2-style rules.
stringify(target: Node | Document): stringSerializes a node or document. The read-only entry returns its original source slice; the mutable entry rebuilds edited portions with escaped values.

TypeScript Types

Both entries export Document, Element, Text, Comment, Directive, Node (Element | Text | Comment | Directive), Parent (Element | Document), and Position ({ line: number; column: number }).

The mutable entry also exports Content (Node | string) for insertion arguments.

Document Properties

PropertyDescription
typeAlways "document".
sourceOriginal HTML string, unchanged by mutable edits.
startAlways 0.
endOriginal source length.
childrenTop-level nodes in source order.
textDecoded text from all descendant text nodes.

Document Methods

// First matching element, or undefined.
doc.find(selector);
// All matching elements in document order.
doc.all(selector);
// Original source offset to { line, column }, both 1-based.
doc.position(offset);

Element Properties

PropertyDescription
typeAlways "element".
nameElement tag name, generally lowercase; case-sensitive SVG names are preserved.
startOffset where the opening tag starts.
endOffset after the element’s end in the original source.
parentParent Element or Document; undefined when detached in mutable mode.
childrenChild nodes in order.
documentOwning Document.
attributesFrozen object with lowercase names and decoded attribute values.
textDecoded text of all descendants.
htmlInner HTML; writable in mutable mode.

Element Methods

// Attribute value or undefined; name matching is case-insensitive.
element.getAttr(name);
// True when the attribute exists; name matching is case-insensitive.
element.hasAttr(name);
// First matching descendant, or undefined.
element.find(selector);
// Matching descendants in document order.
element.all(selector);
// Whether the element matches the selector.
element.matches(selector);
// Closest matching ancestor, including the element itself, or undefined.
element.closest(selector);

Text, Comment, and Directive Nodes

Repeated reads of a parsed node return the same object, including for === checks. Text, Comment, and Directive expose these properties:

PropertyDescription
type"text", "comment", or "directive".
valueDecoded text, comment body, or directive body. Text in raw-text elements and CDATA is literal.
startStart offset in original source.
endEnd offset in original source.
parentContaining Element or Document; undefined after detachment in mutable mode.
documentOwning Document.

CSS Selector Reference

The selector methods accept:

  • Type selectors, *, #id, and .class.
  • Attribute presence and operators =, ~=, |=, ^=, $=, and *=, with i and s flags.
  • Descendant, child (>), adjacent sibling (+), and general sibling (~) combinators, plus comma-separated selector lists.
  • :root, :empty, :first-child, :last-child, :only-child, :first-of-type, :last-of-type, :only-of-type.
  • :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-of-type(), :not(), :is(), :where().

Mutable Methods and Setters

The mutable entry includes the same query methods and properties, plus these editing operations.

Document

// Insert nodes or text at the end or start of the document.
doc.append(...items);
doc.prepend(...items);

Element

setAttr() accepts a string value, preserves the original spelling of existing attributes, and escapes edited values during serialization. Newly created attributes keep the spelling passed to setAttr(). Invalid attribute or tag names throw SyntaxError. Invalid edits to void or raw-text elements can throw TypeError. The html setter parses markup in the element’s context, including SVG.

// Set an attribute with a string value, or remove it.
element.setAttr(name, value);
element.removeAttr(name);
// Insert nodes or text among the element's children.
element.append(...items);
element.prepend(...items);
// Rename the element or replace its children.
element.name = "section";
element.text = "Updated text";
element.html = "<strong>Updated HTML</strong>";

Element, Text, Comment, and Directive

before(), after(), replace(), append(), and prepend() accept nodes or text strings. Existing nodes move from their previous location, including across documents. clone() retains source offsets. Inserted nodes have undefined offsets when they do not originate from the destination document’s source.

node.before(...items);  // Insert before this node.
node.after(...items);   // Insert after this node.
node.replace(...items); // Replace this node.
node.remove();          // Detach this node.
node.clone();           // Deep copy with no parent, in the same document.

Text, Comment, and Directive

The value setter updates the text, comment body, or declaration body.

node.value = "Updated content";

Alternatives

FAQs

Q: How can I get updated source offsets after editing HTML?
A: Serialize the changed document with stringify(doc) and parse the resulting string again. Offsets from the original document continue to refer to its original source.

Q: Why does a selector throw SyntaxError?
A: The selector is malformed or uses unsupported syntax. Check the pseudo-class and combinator against the selector reference.

Q: Can @lukeed/html sanitize untrusted HTML?
A: No. Its parser uses htmlparser2-style tree construction, which differs from how browsers interpret malformed markup. Sanitize untrusted HTML with a dedicated sanitizer before rendering it.

You Might Be Interested In:


Leave a Reply