
@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 & 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:
| Function | Description |
|---|---|
parse(source: string): Document | Parses an HTML string into a document and recovers malformed markup using htmlparser2-style rules. |
stringify(target: Node | Document): string | Serializes 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
| Property | Description |
|---|---|
type | Always "document". |
source | Original HTML string, unchanged by mutable edits. |
start | Always 0. |
end | Original source length. |
children | Top-level nodes in source order. |
text | Decoded 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
| Property | Description |
|---|---|
type | Always "element". |
name | Element tag name, generally lowercase; case-sensitive SVG names are preserved. |
start | Offset where the opening tag starts. |
end | Offset after the element’s end in the original source. |
parent | Parent Element or Document; undefined when detached in mutable mode. |
children | Child nodes in order. |
document | Owning Document. |
attributes | Frozen object with lowercase names and decoded attribute values. |
text | Decoded text of all descendants. |
html | Inner 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:
| Property | Description |
|---|---|
type | "text", "comment", or "directive". |
value | Decoded text, comment body, or directive body. Text in raw-text elements and CDATA is literal. |
start | Start offset in original source. |
end | End offset in original source. |
parent | Containing Element or Document; undefined after detachment in mutable mode. |
document | Owning Document. |
CSS Selector Reference
The selector methods accept:
- Type selectors,
*,#id, and.class. - Attribute presence and operators
=,~=,|=,^=,$=, and*=, withiandsflags. - 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
- Parse Markdown, HTML, and CSS with very-small-parser Library
- Extract Clean Web Content with Defuddle.js
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.







