#html #parser


Fast HTML parser written in pure Rust

21 releases (6 breaking)

Uses new Rust 2021

0.7.5 Apr 18, 2022
0.7.4 Mar 20, 2022
0.7.1 Feb 28, 2022
0.4.4 Dec 27, 2021
0.2.0 Mar 27, 2021

#73 in Parser implementations

Download history 49/week @ 2022-01-29 65/week @ 2022-02-05 77/week @ 2022-02-12 38/week @ 2022-02-19 291/week @ 2022-02-26 113/week @ 2022-03-05 84/week @ 2022-03-12 152/week @ 2022-03-19 26/week @ 2022-03-26 45/week @ 2022-04-02 68/week @ 2022-04-09 67/week @ 2022-04-16 84/week @ 2022-04-23 97/week @ 2022-04-30 332/week @ 2022-05-07 435/week @ 2022-05-14

954 downloads per month
Used in less than 6 crates

MIT license

3.5K SLoC


tl is a fast HTML parser written in pure Rust.

This crate (currently) does not strictly follow the full specification of the HTML standard, however this usually is not a problem for most use cases. This crate generally attempts to support most "sane" HTML. Not being limited by a specification allows for more optimization opportunities. If you need a parser that can (very quickly) parse the typical HTML document and you need a simple API to work with the DOM, give this crate a try.

If you need a parser that closely follows the standard, consider using html5ever, lol-html, or html5gum.


Add tl to your dependencies.

tl = "0.7.5"
# or, with explicit SIMD support
# (requires a nightly compiler!)
tl = { version = "0.7.5", features = ["simd"] }

The main function is tl::parse(). It accepts an HTML source code string and parses it. It is important to note that tl currently silently ignores tags that are invalid, sort of like browsers do. Sometimes, this means that large chunks of the HTML document do not appear in the resulting tree.

let dom = tl::parse(r#"<p id="text">Hello</p>"#, tl::ParserOptions::default()).unwrap();
let parser = dom.parser();
let element = dom.get_element_by_id("text")
  .expect("Failed to find element")

assert_eq!(element.inner_text(parser), "Hello");


Finding a tag using the query selector API
let dom = tl::parse(r#"<div><img src="cool-image.png" /></div>"#, tl::ParserOptions::default()).unwrap();
let img = dom.query_selector("img[src]").unwrap().next();
Iterating over the subnodes of an HTML document
let dom = tl::parse(r#"<div><img src="cool-image.png" /></div>"#, tl::ParserOptions::default()).unwrap();
let img = dom.nodes()
  .find(|node| {
    node.as_tag().map_or(false, |tag| tag.name() == "img")
Mutating the `href` attribute of an anchor tag:

In a real world scenario, you would want to handle errors properly instead of unwrapping.

let input = r#"<div><a href="/about">About</a></div>"#;
let mut dom = tl::parse(input, tl::ParserOptions::default())
  .expect("HTML string too long");
let anchor = dom.query_selector("a[href]")
  .expect("Failed to parse query selector")
  .expect("Failed to find anchor tag");

let parser_mut = dom.parser_mut();

let anchor = anchor.get_mut(parser_mut)
  .expect("Failed to resolve node")
  .expect("Failed to cast Node to HTMLTag");

let attributes = anchor.attributes_mut();

  .expect("Attribute not found or malformed")

assert_eq!(attributes.get("href").flatten(), Some(&"http://localhost/about".into()));

SIMD-accelerated parsing

This crate has utility functions used by the parser which make use of SIMD (e.g. finding a specific byte by looking at the next 16 bytes at once, instead of going through the string one by one). These are disabled by default and must be enabled explicitly by passing the simd feature flag due to the unstable feature portable_simd. This requires a nightly compiler!

If the simd feature is not enabled, it will fall back to stable alternatives that don't explicitly use SIMD intrinsics, but are still decently well optimized, using techniques such as manual loop unrolling to remove boundary checks and other branches by a factor of 16, which also helps LLVM further optimize the code and potentially generate SIMD instructions by itself.


Results for parsing a ~320KB HTML document. Benchmarked using criterion.

Note: Some HTML parsers listed closely follow the specification while others don't, which greatly impacts performance as the specification limits what can and can't be done. Comparing the performance of a parser that doesn't follow the specification to one that does isn't fair and doesn't yield meaningful results, but it can be interesting to see what the theoretical difference is.

              time            thrpt             follows spec
tl¹           629.78 us       496.65 MiB/s      ❌
lol_html      788.91 us       396.47 MiB/s      ✅
htmlstream    2.2786 ms       137.48 MiB/s      ❌
html5ever     6.2233 ms       50.276 MiB/s      ✅

¹ - simd feature enabled


No runtime deps