1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
//! # dom-tree-rs
//!
//! A tiny, **zero-dependency**, forgiving HTML parser. It turns messy
//! real-world HTML into a clean DOM tree (and JSON) and **never panics** on bad
//! input — ideal for the browser, the edge, or anywhere you don't want to pull
//! in a 100 KB+ parser.
//!
//! It is *not* a spec-compliant WHATWG parser (see
//! [`html5ever`](https://crates.io/crates/html5ever) for that) and *not* a
//! speed record (see [`tl`](https://crates.io/crates/tl)). It is the smallest
//! way to get a usable tree out of arbitrary HTML.
//!
//! ## Quick start
//!
//! ```
//! let dom = domtree::parse(r#"<ul id="list"><li class="item">Item 1</li></ul>"#);
//!
//! let li = dom.find_by_tag("li").next().unwrap();
//! assert_eq!(li.attr("class"), Some("item"));
//! assert_eq!(li.text_content(), "Item 1");
//!
//! // The first list element:
//! let ul = dom.root().unwrap();
//! assert_eq!(ul.tag_name(), Some("ul"));
//! assert_eq!(ul.children().count(), 1);
//! # #[cfg(feature = "json")]
//! # assert!(dom.to_json().contains("\"tag\":\"li\""));
//! ```
//!
//! ## What it handles
//!
//! Void elements (`<br>`, `<img>`, …), self-closing tags, comments, doctypes,
//! CDATA, raw text (`<script>`/`<style>`), boolean and unquoted attributes,
//! hyphenated/custom tags, common HTML entities, and the everyday "forgot a
//! closing tag" cases (`<li>`, `<p>`, table cells, …) via implicit closing.
//!
//! ## What it deliberately does *not* do
//!
//! Full WHATWG tree construction: no adoption-agency algorithm, no automatic
//! `<html>`/`<head>`/`<body>` insertion, no foster parenting. Anything it has
//! to recover from is recorded in [`Dom::errors`].
//!
//! No unbounded search for the element an end tag closes: it must be among
//! the 256 innermost open elements or the 8 outermost (where `</body>` and
//! `</html>` find theirs). An end tag for an element open only in between is
//! ignored as stray ([`ErrorKind::StrayEndTag`]) and the element stays open.
//! Real documents almost never nest that deep, and the bound keeps every end
//! tag O(1) on hostile input.
//!
//! ## Getting content out
//!
//! - [`Dom::to_text`] — readable plain text: no `<script>`/`<style>`/`<head>`,
//! whitespace collapsed, a line per block, a blank line between paragraphs.
//! - [`Dom::to_markdown`] — CommonMark + GFM (headings, lists, links, images,
//! code blocks, tables, quotes): for LLM prompts, RAG indexes and notes.
//! - [`Dom::to_html`], [`NodeRef::inner_html`], [`NodeRef::outer_html`] — HTML.
//! - [`Dom::to_markdown_chunks`] — that Markdown split at headings into
//! chunks under a size budget, each with its heading path: for RAG indexes.
//!
//! ```
//! let dom = domtree::parse(
//! "<article><h2>News</h2><p>It <em>works</em>!</p><script>track()</script></article>",
//! );
//! assert_eq!(dom.to_text(), "News\n\nIt works!");
//! # #[cfg(feature = "markdown")]
//! assert_eq!(dom.to_markdown(), "## News\n\nIt *works*!");
//! ```
//!
//! ## Editing and source positions
//!
//! A [`Dom`] doubles as a small HTML rewriter: [`Dom::remove`],
//! [`Dom::set_text`], [`Dom::set_attr`], [`Dom::rename`], [`Dom::wrap`],
//! [`Dom::unwrap`], parsed-fragment insertion ([`Dom::append_html`] and
//! friends) and node moves ([`Dom::append_child`], [`Dom::insert_before`]).
//! Parsed nodes also remember where they came from:
//! [`NodeRef::source_range`] plus [`line_col`].
//!
//! ```
//! let src = "<p>Hello <b>world</b></p>";
//! let mut dom = domtree::parse(src);
//! let b = dom.find_by_tag("b").next().unwrap();
//! assert_eq!(b.source_range(), Some(9..21));
//! assert_eq!(domtree::line_col(src, 9), (1, 10));
//!
//! let b = b.id();
//! dom.rename(b, "strong");
//! dom.insert_html_after(b, "!");
//! assert_eq!(dom.to_html(), "<p>Hello <strong>world</strong>!</p>");
//! ```
//!
//! ## Structure and data
//!
//! - [`Dom::main_content`] — the article / post / docs body without the
//! navigation, sidebars, comments and footer ("reader mode").
//! - [`Dom::links`] / [`Dom::resources`] — every link (with its text) and
//! every embedded URL, resolved against the page URL and `<base href>`
//! ([`resolve_url`], [`resolve_srcset`]: RFC 3986, zero-dependency).
//! - [`Dom::metadata`] — title, description, canonical URL, language,
//! OpenGraph / Twitter-card fields, JSON-LD blocks and feeds.
//! - [`NodeRef::table_rows`] — a `<table>` as rows of cell text with
//! `colspan`/`rowspan` expanded; [`to_csv`] formats them.
//!
//! ```
//! let dom = domtree::parse(r#"<html lang="en"><head><title>Release notes</title></head>
//! <body><nav><a href="/">Home</a></nav>
//! <article><h1>Version 2</h1>
//! <p>This release adds link extraction, page metadata, tables as data and a
//! reader mode, all without a single new dependency.</p>
//! <p>See the <a href="changelog.html">full changelog</a> for details.</p>
//! </article></body></html>"#);
//!
//! assert_eq!(dom.metadata().title.as_deref(), Some("Release notes"));
//! let article = dom.main_content().unwrap();
//! assert_eq!(article.tag_name(), Some("article"));
//! let links = dom.links(Some("https://example.com/news/"));
//! assert_eq!(links[1].url, "https://example.com/news/changelog.html");
//! assert_eq!(links[1].text, "full changelog");
//! ```
//!
//! ## Features
//!
//! - `json` *(default)* — the zero-dependency [`Dom::to_json`] serializer.
//! - `markdown` *(default)* — the zero-dependency [`Dom::to_markdown`] and
//! [`Dom::to_markdown_chunks`].
//! - `serde` — optional `serde::Serialize` for the tree, [`Metadata`] and
//! [`Chunk`] (adds the `serde` dep).
//! - `query` — a zero-dependency CSS selector engine ([`Dom::select`], [`Selector`]).
//! - `entities-full` — decode all 2,125 WHATWG named character references
//! instead of the curated ~180 (a sorted static table, about 70 KB).
//! - `simd` — scan text and attribute values with
//! [`memchr`](https://crates.io/crates/memchr) (SIMD, `no_std`) instead of
//! the built-in word-at-a-time search. Adds that one dependency; tokens and
//! trees are identical either way. Worth it only for very text-heavy input.
//! - `std` — adds `std`-only conveniences; the crate is `no_std + alloc` by default.
extern crate alloc;
extern crate std;
pub use ;
pub use ;
pub use ;
pub use ;
pub use line_col;
pub use ;
pub use Tokenizer;
pub use ;
pub use ;
pub use Chunk;
pub use ;
/// Parse a full HTML document into a best-effort tree.
///
/// Never panics. Any recovery the parser performed is recorded in
/// [`Dom::errors`].
///
/// ```
/// let dom = domtree::parse("<p>hello<p>world");
/// assert_eq!(dom.find_by_tag("p").count(), 2); // implicit </p>
/// ```
/// Parse an HTML fragment.
///
/// Uses the same engine as [`parse`]; the name simply documents intent (a
/// fragment commonly has several top-level nodes).
/// Create a streaming [`Tokenizer`] over `html` without building a tree.
///
/// ```
/// use domtree::Token;
/// let kinds: usize = domtree::tokenize("<b>hi</b>").count();
/// assert_eq!(kinds, 3); // <b>, "hi", </b>
/// ```
/// Stream a document as [`Event`]s — start and end of elements, text,
/// comments, doctypes — without building a tree.
///
/// Unlike [`tokenize`], the events come **after recovery**, from the same
/// tree builder [`parse`] uses: omitted end tags are implied (`<li>a<li>b`
/// ends the first `<li>`), void and self-closing elements end right away,
/// stray end tags are dropped, mismatched ones close what they skip, and
/// everything still open ends at the end of the input. So every `Start` has
/// exactly one matching `End`, and replaying the events builds exactly the
/// tree `parse` returns. Text and attribute values are entity-decoded, and
/// each event comes with its byte range in `html` (see [`Events`]).
///
/// Memory use is proportional to the nesting depth, not the document size —
/// except for the recovery diagnostics, which are kept for
/// [`Events::errors`] (16 bytes each; messy markup has about one per
/// implicitly closed element). It is not faster than [`parse`]: each start
/// tag with attributes gets its own `Vec`, where the tree shares one arena.
///
/// ```
/// use domtree::Event;
///
/// let html = "<ul><li>Tom & Jerry<li>Spike<br></ul>";
/// let mut depth = 0;
/// let mut items = Vec::new();
/// for (event, range) in domtree::events(html) {
/// match event {
/// Event::Start { name, .. } => {
/// depth += 1;
/// if name == "li" {
/// items.push(String::new());
/// }
/// }
/// Event::End { name, implied } => {
/// depth -= 1;
/// if name == "li" {
/// // Both `</li>` were omitted: the first is closed by the
/// // second `<li>`, the second by `</ul>`.
/// assert!(implied);
/// assert!(range.is_empty());
/// }
/// }
/// Event::Text(text) => {
/// if let Some(item) = items.last_mut() {
/// item.push_str(&text);
/// }
/// }
/// _ => {}
/// }
/// }
/// assert_eq!(depth, 0);
/// assert_eq!(items, ["Tom & Jerry", "Spike"]);
/// ```