tl

Fast, zero-copy HTML parser for Rust with a query-selector API

Library
Cargo
v0.7.8
405stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
33/100Needs Attention
Development Activity0
Maintenance0
Community44
Maturity60
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
68/100Good
Architecture72
Code Quality68
Innovation70
Learning Curve60

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 a VDom with parser/parser_mut handles for read and mutate access
  • A CSS-style query_selector API 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/src values in place)
  • An optional nightly-only simd feature 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/src attributes
  • 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.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search