Skip to main content

domtree/
lib.rs

1//! # dom-tree-rs
2//!
3//! A tiny, **zero-dependency**, forgiving HTML parser. It turns messy
4//! real-world HTML into a clean DOM tree (and JSON) and **never panics** on bad
5//! input — ideal for the browser, the edge, or anywhere you don't want to pull
6//! in a 100 KB+ parser.
7//!
8//! It is *not* a spec-compliant WHATWG parser (see
9//! [`html5ever`](https://crates.io/crates/html5ever) for that) and *not* a
10//! speed record (see [`tl`](https://crates.io/crates/tl)). It is the smallest
11//! way to get a usable tree out of arbitrary HTML.
12//!
13//! ## Quick start
14//!
15//! ```
16//! let dom = domtree::parse(r#"<ul id="list"><li class="item">Item 1</li></ul>"#);
17//!
18//! let li = dom.find_by_tag("li").next().unwrap();
19//! assert_eq!(li.attr("class"), Some("item"));
20//! assert_eq!(li.text_content(), "Item 1");
21//!
22//! // The first list element:
23//! let ul = dom.root().unwrap();
24//! assert_eq!(ul.tag_name(), Some("ul"));
25//! assert_eq!(ul.children().count(), 1);
26//! # #[cfg(feature = "json")]
27//! # assert!(dom.to_json().contains("\"tag\":\"li\""));
28//! ```
29//!
30//! ## What it handles
31//!
32//! Void elements (`<br>`, `<img>`, …), self-closing tags, comments, doctypes,
33//! CDATA, raw text (`<script>`/`<style>`), boolean and unquoted attributes,
34//! hyphenated/custom tags, common HTML entities, and the everyday "forgot a
35//! closing tag" cases (`<li>`, `<p>`, table cells, …) via implicit closing.
36//!
37//! ## What it deliberately does *not* do
38//!
39//! Full WHATWG tree construction: no adoption-agency algorithm, no automatic
40//! `<html>`/`<head>`/`<body>` insertion, no foster parenting. Anything it has
41//! to recover from is recorded in [`Dom::errors`].
42//!
43//! ## Features
44//!
45//! - `json` *(default)* — the zero-dependency [`Dom::to_json`] serializer.
46//! - `serde` — optional `serde::Serialize` for the tree (adds the `serde` dep).
47//! - `query` — a small CSS-selector engine, [`Dom::select`].
48//! - `std` — adds `std`-only conveniences; the crate is `no_std + alloc` by default.
49
50#![no_std]
51#![forbid(unsafe_code)]
52#![cfg_attr(docsrs, feature(doc_cfg))]
53#![cfg_attr(
54    not(test),
55    deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)
56)]
57#![warn(missing_docs)]
58
59extern crate alloc;
60
61#[cfg(any(feature = "std", test))]
62extern crate std;
63
64mod builder;
65mod entities;
66mod error;
67mod node;
68mod tags;
69mod token;
70mod tokenizer;
71mod tree;
72
73mod serialize;
74
75#[cfg(feature = "query")]
76mod query;
77
78pub use error::{ErrorKind, ParseError};
79pub use node::{NodeId, NodeKind};
80pub use token::{AttrValue, Token};
81pub use tokenizer::Tokenizer;
82pub use tree::{Descendants, Dom, NodeRef};
83
84/// Parse a full HTML document into a best-effort tree.
85///
86/// Never panics. Any recovery the parser performed is recorded in
87/// [`Dom::errors`].
88///
89/// ```
90/// let dom = domtree::parse("<p>hello<p>world");
91/// assert_eq!(dom.find_by_tag("p").count(), 2); // implicit </p>
92/// ```
93pub fn parse(html: &str) -> Dom {
94    builder::parse_document(html)
95}
96
97/// Parse an HTML fragment.
98///
99/// Uses the same engine as [`parse`]; the name simply documents intent (a
100/// fragment commonly has several top-level nodes).
101pub fn parse_fragment(html: &str) -> Dom {
102    builder::parse_document(html)
103}
104
105/// Create a streaming [`Tokenizer`] over `html` without building a tree.
106///
107/// ```
108/// use domtree::Token;
109/// let kinds: usize = domtree::tokenize("<b>hi</b>").count();
110/// assert_eq!(kinds, 3); // <b>, "hi", </b>
111/// ```
112pub fn tokenize(html: &str) -> Tokenizer<'_> {
113    Tokenizer::new(html)
114}