tl
Fast, zero-copy HTML parser for Rust with a query-selector API
Repository Health
Technical Analysis
tl is a fast, zero-copy HTML parser written in pure Rust, built for use cases where raw parsing throughput matters more than strict HTML-standard compliance. Rather than implementing the full WHATWG HTML parsing algorithm, tl focuses on handling the “sane” HTML that real-world documents contain, which opens up optimization opportunities not available to fully spec-compliant parsers like html5ever.
The crate parses an HTML string into a VDom that can be traversed node-by-node or queried with a CSS-style query_selector API (e.g. img[src], a[href]), and supports mutating attributes directly on parsed nodes. An optional simd feature (requiring nightly Rust) accelerates byte-scanning operations used during tokenization via portable_simd, while the stable build falls back to manually unrolled loops that still let LLVM auto-vectorize much of the hot path. tl is commonly reached for in scraping tools, static-site link/asset extraction, and other performance-sensitive HTML-processing tasks.
What You Get
- A
tl::parse()entry point returning aVDomwith parser/parser_mut handles for read and mutate access - A CSS-style
query_selectorAPI for finding elements by tag, attribute, or id - Zero-copy parsing that borrows from the input string instead of allocating new strings for text/attribute values
- Direct attribute mutation on parsed tags (e.g. rewriting
href/srcvalues in place) - An optional nightly-only
simdfeature for accelerated byte scanning during tokenization - A stable-Rust fallback path using manually unrolled loops when SIMD is unavailable
Common Use Cases
- Web scraping tools that need to extract links, images, or text from HTML at high throughput
- Static site or link-checker tooling that walks HTML documents to find and validate
href/srcattributes - Rewriting or sanitizing HTML fragments by mutating attributes on matched elements
- Performance-sensitive services that parse large volumes of HTML and can tolerate lenient, non-spec-strict parsing
Under The Hood
Architecture - tl parses input in two conceptual stages: a byte-level scanner (src/stream.rs, src/bytes.rs) that walks the input string looking for tag/attribute boundaries, and a parser layer (src/parser/) that builds a flat node arena exposed through VDom (src/vdom.rs). Rather than a classic tree of owned nodes, elements reference into this arena via handles resolved through a Parser/ParserMut context, which is what allows the zero-copy design — text and attribute values borrow directly from the original input string instead of being copied. A separate queryselector/ module implements CSS-like selector parsing and matching (tag name, attribute presence/value, id) against the arena. An inline module and simd module provide the SIMD-accelerated and stable-fallback byte-scanning primitives used by the tokenizer.
Tech Stack - Pure Rust (edition 2021) with zero runtime dependencies in the default build — the crate deliberately keeps its dependency tree empty, relying only on criterion as a dev-dependency for benchmarking. The optional simd feature uses the unstable portable_simd API, which requires a nightly compiler; the stable build instead uses manual loop unrolling to approximate similar throughput without unstable features. An internal __INTERNALS_DO_NOT_USE feature flag exposes normally-private internals for the crate’s own fuzz and benchmark harnesses.
Code Quality - Tests live inline in src/tests.rs (780 lines) rather than a separate tests/ directory, covering parsing edge cases, query-selector matching, and mutation behavior; there is also a fuzz/ directory wired up for fuzz-testing the parser against malformed input, which matters given the parser’s lenient, non-spec-following design. The codebase is compact (roughly 1,600 lines excluding tests/fuzzing) and organized into clearly separated stream/parser/queryselector/simd modules, though public API documentation is comment-based and lighter than the README’s example-driven style.
API Design - The primary entry point, tl::parse(input, ParserOptions::default()), returns a Result<VDom, ParseError> and pairs with a small number of follow-on calls (dom.parser(), dom.query_selector(), dom.get_element_by_id(), dom.nodes()) that cover the common read paths in just a few lines, as shown directly in the README. Mutation requires explicitly obtaining a parser_mut() handle and casting nodes via as_tag_mut(), which is slightly more ceremony than the read path but keeps the borrow-checker-friendly zero-copy design honest about when mutable access is being taken.
Used by 2 apps in this directory
Anarlog
Note Taking · AI Assistants · Productivity
Anarlog is an open-source, local-first AI meeting notetaker that records, transcribes, and summarizes meetings entirely on your device — no cloud lock-in, no mandatory account, and every note saved as a plain markdown file you own forever.
Trieve
AI Development · Search · Developer Tools
All-in-one self-hostable platform for hybrid search, RAG, recommendations, and analytics built on Rust and Qdrant.