dom-tree-rs
A tiny, zero-dependency, forgiving HTML parser. Turn messy real-world HTML into a clean tree (and JSON) — in the browser, on the edge, or anywhere you don't want to pull in a 100 KB+ parser. It never panics on bad input.
let dom = parse;
let items: = dom.find_by_tag.map.collect;
assert_eq!; // note the missing </li> — recovered
println!; // self-describing JSON tree, zero deps
What it is not: a spec-compliant WHATWG parser (use
html5ever) or a raw-throughput record (usetl). It's the smallest way to turn arbitrary HTML into a usable tree.
Why
Most Rust HTML parsers are either spec-grade and heavy (html5ever, scraper,
lol_html — 5–11 dependencies, hundreds of KB of WASM) or fast and low-level
(tl). dom-tree-rs fills a specific gap:
- Zero dependencies in the default build. Your
cargo treestays clean. - WASM-first: a ~19 KB-gzipped module that parses HTML to JSON in any JS runtime (browser / Deno / Cloudflare Workers / Node).
- Built-in JSON output — no
serderequired (it's an optional feature). - Forgiving: missing end tags, void elements, mis-nesting, stray tags,
unquoted/boolean attributes, comments, doctypes,
<script>/<style>, entities — all handled, and it never panics. no_std+alloccompatible.- Ergonomic tree:
parent/children/descendants/ancestors,find_by_tag/id/class, and optional CSS-selector querying.
How it compares
| dom-tree-rs | tl | scraper | html5ever | lol_html | |
|---|---|---|---|---|---|
| Default dependencies | 0 | 0 | 5+ | 10+ | 11+ |
| WASM + npm package | ✅ | ✅ | ❌ | ❌ | ❌ |
| Built-in JSON output | ✅ | ❌ | ❌ | ❌ | ❌ |
| CSS-selector querying | subset¹ | limited | ✅ | ❌ | ✅ |
| Streaming tokenizer | ✅ | ❌ | ❌ | partial | ✅ |
no_std |
✅ | ❌ | ❌ | ❌ | ❌ |
| Never panics on input | ✅ | ✅ | ✅ | ✅ | ✅ |
| WHATWG spec-compliant | ❌ | ❌ | ✅ | ✅ | ❌ |
¹ behind the query feature.
Performance
Parsing is fast for an owned tree. On a synthetic article workload (Apple
M-series, single core, --release):
| input | dom-tree-rs | tl | html5ever† |
|---|---|---|---|
| ~1 KB | ~256 MiB/s | ~308 MiB/s | — |
| ~50 KB | ~250 MiB/s | ~297 MiB/s | — |
| ~1 MB | ~248 MiB/s | ~257 MiB/s | — |
We land within ~5–15 % of tl (a SIMD speed specialist) and roughly on par on
large documents, while staying zero-dependency and producing an owned tree.
tl will usually win on raw throughput — if speed is your only goal, use
tl. † html5ever benches around ~50 MB/s in the community
rust-html-parser-benchmark; we don't include it in our harness to keep the
benchmark crate dependency-light. Run the numbers yourself:
&&
Install
[]
= "0.2"
The library is imported as domtree:
use parse;
Usage
Traverse
let dom = parse;
let h1 = dom.find_by_tag.next.unwrap;
assert_eq!;
for a in dom.find_by_tag
// arena-backed: parent / siblings / ancestors are O(1) per step
let link = dom.find_by_tag.next.unwrap;
assert_eq!;
Query with CSS selectors (query feature)
= { = "0.2", = ["query"] }
#
Supported: type / * / #id / .class / [attr] / [attr=value],
descendant ( ) and child (>) combinators, and , selector lists.
JSON
let dom = parse;
let json = dom.to_json;
// {"type":"document","errors":0,"children":[
// {"type":"element","tag":"p","attrs":{},"children":[
// {"type":"text","value":"hi"}]}]}
With the serde feature, the tree also implements serde::Serialize (same JSON
shape).
WebAssembly
See dom-tree-wasm/ — parse HTML to JSON from JavaScript with
a ~19 KB-gzipped module and zero JS dependencies.
import init from "dom-tree-wasm";
await ;
const doc = JSON.;
What it handles
Void elements (<br>, <img>, …), self-closing tags, comments, doctypes,
CDATA, raw text (<script>/<style>), escapable raw text
(<title>/<textarea>), boolean and unquoted attributes,
hyphenated/namespaced/custom tags, the common HTML entities (named + numeric),
and implicit closing for the everyday cases (<li>, <p>, table cells/rows,
<dt>/<dd>, <option>).
What it deliberately does not do
It is not a full WHATWG tree builder: no adoption-agency algorithm, no
automatic <html>/<head>/<body> insertion, no foster parenting, and only a
curated subset (~130) of named entities. Everything it recovers from is recorded
in Dom::errors.
If you need byte-exact browser parsing, use html5ever.
Features
| feature | default | what it adds |
|---|---|---|
json |
✅ | the zero-dependency Dom::to_json serializer |
serde |
serde::Serialize for the tree (adds the serde dep) |
|
query |
the CSS-subset selector engine, Dom::select |
|
std |
std-only conveniences; the crate is no_std + alloc otherwise |
MSRV
Rust 1.70.
Quality
Property-tested for "never panics" + tree invariants (proptest, thousands of
cases per run), snapshot-tested against a corpus of messy HTML, and CI runs
fmt / clippy / a no_std build / a wasm size gate / cargo-fuzz.
License
MIT.