pub struct Dom { /* private fields */ }Expand description
A parsed HTML document: an arena of nodes, a single string buffer they reference, the top-level (root) nodes, and any recovery diagnostics.
Build one with crate::parse and traverse it with NodeRef handles
from Dom::root, Dom::roots, Dom::get, or the find_by_*
helpers. The editing methods (Dom::remove, Dom::set_text,
Dom::append_html, Dom::wrap, …) rewrite it in place.
Implementations§
Source§impl Dom
impl Dom
Sourcepub fn remove(&mut self, id: NodeId)
pub fn remove(&mut self, id: NodeId)
Remove a node (and its subtree) from the document.
The node stays valid — handles to it and its descendants still work and
can still be serialized, and it can be re-inserted with
Dom::append_child or Dom::insert_before — but it is no longer
reachable from the document: Dom::nodes, find_by_*, select, and
every serializer skip it. Removing an already-removed node is a no-op.
let mut dom = domtree::parse("<p>Hi <script>track()</script>there</p>");
let script = dom.find_by_tag("script").next().unwrap().id();
dom.remove(script);
assert_eq!(dom.to_html(), "<p>Hi there</p>");
assert_eq!(dom.find_by_tag("script").count(), 0);Sourcepub fn set_attr(&mut self, id: NodeId, name: &str, value: &str)
pub fn set_attr(&mut self, id: NodeId, name: &str, value: &str)
Set attribute name (ASCII-lower-cased) to value on an element,
replacing any existing value. Does nothing for non-element nodes, and
nothing if name is not a valid attribute name: empty, or containing
ASCII whitespace, ", ', <, >, /, = or NUL — names are
written out as they are, so these would change the markup around them.
let mut dom = domtree::parse("<a href=/x>x</a>");
let a = dom.find_by_tag("a").next().unwrap().id();
dom.set_attr(a, "href", "https://example.com/x");
dom.set_attr(a, "rel", "nofollow");
dom.set_attr(a, "x><script>alert(1)</script", "refused");
assert_eq!(dom.to_html(), r#"<a href="https://example.com/x" rel="nofollow">x</a>"#);Sourcepub fn remove_attr(&mut self, id: NodeId, name: &str)
pub fn remove_attr(&mut self, id: NodeId, name: &str)
Remove attribute name (ASCII case-insensitive) from an element, if
present.
let mut dom = domtree::parse(r#"<p style="color:red" class=x>hi</p>"#);
let p = dom.find_by_tag("p").next().unwrap().id();
dom.remove_attr(p, "style");
assert_eq!(dom.to_html(), r#"<p class="x">hi</p>"#);Sourcepub fn set_text(&mut self, id: NodeId, text: &str)
pub fn set_text(&mut self, id: NodeId, text: &str)
Replace a node’s text.
On a text node, replaces its content. On an element, removes all of
its children and puts text in a single new text node (none if text
is empty) — the DOM’s textContent = text. Does nothing on void
elements, comments and doctypes.
text is literal, never parsed (use Dom::append_html for markup),
and Dom::to_html escapes &, < and > in it — except inside
<script> and <style>, whose content HTML has no escapes for. There
it is written as is, apart from any </script (or </style) that
would end the element early, which is written <\/script — the same
string inside a JavaScript or CSS string literal — so the output still
parses back to a single element.
let mut dom = domtree::parse("<h1>Old <em>title</em></h1><p>x</p>");
let h1 = dom.find_by_tag("h1").next().unwrap().id();
dom.set_text(h1, "New & <improved>");
assert_eq!(dom.to_html(), "<h1>New & <improved></h1><p>x</p>");
let x = dom.find_by_tag("p").next().unwrap().first_child().unwrap().id();
dom.set_text(x, "y");
assert_eq!(dom.to_html(), "<h1>New & <improved></h1><p>y</p>");
let mut dom = domtree::parse("<script>old()</script>");
let script = dom.root().unwrap().id();
dom.set_text(script, r#"if (a < b) log("</script>")"#);
assert_eq!(dom.to_html(), r#"<script>if (a < b) log("<\/script>")</script>"#);Sourcepub fn rename(&mut self, id: NodeId, tag: &str)
pub fn rename(&mut self, id: NodeId, tag: &str)
Change an element’s tag name (ASCII-lower-cased). Attributes and
children are kept. Does nothing for non-elements, and nothing if tag
is not a valid tag name: one that does not start with an ASCII letter,
or contains ASCII whitespace, ", ', <, >, /, = or NUL.
Renaming to a void element (br, img, …) keeps any children in the
tree, but Dom::to_html — like the HTML syntax — cannot express them.
let mut dom = domtree::parse("<b class=x>bold</b>");
let b = dom.root().unwrap().id();
dom.rename(b, "STRONG");
dom.rename(b, "b><img src=x onerror=alert(1)"); // refused
assert_eq!(dom.to_html(), r#"<strong class="x">bold</strong>"#);Sourcepub fn create_element(&mut self, tag: &str) -> Option<NodeId>
pub fn create_element(&mut self, tag: &str) -> Option<NodeId>
Create a new, empty element (tag ASCII-lower-cased).
It starts detached: not part of the document until you attach it
with Dom::append_child or Dom::insert_before. Created nodes have
no source_range.
Returns None (and creates nothing) if tag is not a valid tag name
(see Dom::rename).
let mut dom = domtree::parse("<ul><li>a</li></ul>");
let ul = dom.root().unwrap().id();
let li = dom.create_element("li").unwrap();
let text = dom.create_text("b");
dom.append_child(li, text);
dom.set_attr(li, "class", "new");
dom.append_child(ul, li);
assert_eq!(dom.to_html(), r#"<ul><li>a</li><li class="new">b</li></ul>"#);
assert!(dom.create_element("li><img").is_none());Sourcepub fn create_text(&mut self, text: &str) -> NodeId
pub fn create_text(&mut self, text: &str) -> NodeId
Create a new, detached text node holding text literally (see
Dom::create_element for how detached nodes behave).
let mut dom = domtree::parse("<p></p>");
let p = dom.root().unwrap().id();
let t = dom.create_text("1 < 2");
dom.append_child(p, t);
assert_eq!(dom.to_html(), "<p>1 < 2</p>");Sourcepub fn append_child(&mut self, parent: NodeId, child: NodeId) -> bool
pub fn append_child(&mut self, parent: NodeId, child: NodeId) -> bool
Move child (with its subtree) to the end of parent’s children.
child may be a created node, a removed one, or a node elsewhere in the
document (it is detached from its old place first). If parent is
itself detached, child ends up detached with it. Returns false and
changes nothing if parent cannot have children (it is not an element,
or is a void element) or if parent is child or inside it — moving a
node into its own subtree would make a cycle.
let mut dom = domtree::parse("<div id=a><b>x</b></div><div id=c></div>");
let b = dom.find_by_tag("b").next().unwrap().id();
let c = dom.find_by_id("c").unwrap().id();
assert!(dom.append_child(c, b)); // moves <b> from #a to #c
assert_eq!(dom.to_html(), r#"<div id="a"></div><div id="c"><b>x</b></div>"#);
assert!(!dom.append_child(b, c)); // #c contains <b>: refused, no cycle
let x = dom.get(b).unwrap().first_child().unwrap().id();
assert!(!dom.append_child(x, c)); // text nodes have no childrenSourcepub fn insert_before(&mut self, reference: NodeId, node: NodeId) -> bool
pub fn insert_before(&mut self, reference: NodeId, node: NodeId) -> bool
Move node (with its subtree) to just before reference, as its
sibling — at the top level if reference is a top-level node.
Like Dom::append_child, node may come from anywhere. Returns
false and changes nothing if reference has no place to insert at (it
is detached with no parent), if node is reference, or if node
contains reference (a cycle).
let mut dom = domtree::parse("<li>b</li><li>c</li><li>a</li>");
let items: Vec<_> = dom.find_by_tag("li").map(|n| n.id()).collect();
assert!(dom.insert_before(items[0], items[2]));
assert_eq!(dom.to_html(), "<li>a</li><li>b</li><li>c</li>");Sourcepub fn wrap(&mut self, id: NodeId, tag: &str) -> Option<NodeId>
pub fn wrap(&mut self, id: NodeId, tag: &str) -> Option<NodeId>
Wrap a node in a new tag element, which takes the node’s place.
Returns the wrapper’s id, or None (changing nothing) if id is
unknown, or tag is a void element or not a valid tag name (see
Dom::rename). Wrapping a detached node gives a detached wrapper.
let mut dom = domtree::parse("<p>Hello <b>world</b></p>");
let b = dom.find_by_tag("b").next().unwrap().id();
let a = dom.wrap(b, "a").unwrap();
dom.set_attr(a, "href", "/world");
assert_eq!(dom.to_html(), r#"<p>Hello <a href="/world"><b>world</b></a></p>"#);
assert!(dom.wrap(b, "").is_none());Sourcepub fn unwrap(&mut self, id: NodeId) -> bool
pub fn unwrap(&mut self, id: NodeId) -> bool
Replace an element by its children: they move up into its place, in
order, and the element itself is removed (detached, now childless).
Returns false and changes nothing for non-elements and for detached
elements with no parent (there is no place to move the children to).
let mut dom = domtree::parse(r#"<p>a <span class="x">b <i>c</i></span> d</p>"#);
let span = dom.find_by_tag("span").next().unwrap().id();
assert!(dom.unwrap(span));
assert_eq!(dom.to_html(), "<p>a b <i>c</i> d</p>");Sourcepub fn append_html(&mut self, parent: NodeId, html: &str) -> Vec<NodeId>
pub fn append_html(&mut self, parent: NodeId, html: &str) -> Vec<NodeId>
Parse html as a fragment and append its nodes to the end of
parent’s children. Returns the ids of the inserted top-level nodes,
in order — empty if html produced no nodes or parent cannot have
children (not an element, or a void element).
The fragment is parsed on its own, with the normal parser, not in the
context of parent (so <li> closes <li> inside it, but text
appended to a <script> is still parsed as HTML). Its parse errors are
not added to Dom::errors — those positions refer to the original
input; check crate::parse_fragment(html).errors() if you need
them. Inserted nodes have no
source_range.
let mut dom = domtree::parse("<ul><li>a</li></ul>");
let ul = dom.root().unwrap().id();
let ids = dom.append_html(ul, "<li>b<li>c");
assert_eq!(ids.len(), 2);
assert_eq!(dom.to_html(), "<ul><li>a</li><li>b</li><li>c</li></ul>");Sourcepub fn prepend_html(&mut self, parent: NodeId, html: &str) -> Vec<NodeId>
pub fn prepend_html(&mut self, parent: NodeId, html: &str) -> Vec<NodeId>
Parse html and insert its nodes at the start of parent’s children.
See Dom::append_html for how the fragment is parsed and what is
returned.
let mut dom = domtree::parse("<p>world</p>");
let p = dom.root().unwrap().id();
dom.prepend_html(p, "<b>Hello</b>, ");
assert_eq!(dom.to_html(), "<p><b>Hello</b>, world</p>");Sourcepub fn insert_html_before(&mut self, id: NodeId, html: &str) -> Vec<NodeId>
pub fn insert_html_before(&mut self, id: NodeId, html: &str) -> Vec<NodeId>
Parse html and insert its nodes just before id, as its siblings (at
the top level if id is a top-level node). Returns nothing inserted if
id is detached with no parent. See Dom::append_html for how the
fragment is parsed.
let mut dom = domtree::parse("<h1>Title</h1><p>Body</p>");
let h1 = dom.root().unwrap().id();
dom.insert_html_before(h1, "<!doctype html>");
assert_eq!(dom.to_html(), "<!doctype html><h1>Title</h1><p>Body</p>");Sourcepub fn insert_html_after(&mut self, id: NodeId, html: &str) -> Vec<NodeId>
pub fn insert_html_after(&mut self, id: NodeId, html: &str) -> Vec<NodeId>
Parse html and insert its nodes just after id, as its siblings. See
Dom::insert_html_before.
let mut dom = domtree::parse("<p>See <a href=/docs>docs</a>.</p>");
let a = dom.find_by_tag("a").next().unwrap().id();
dom.insert_html_after(a, " <sup>[1]</sup>");
assert_eq!(dom.to_html(), r#"<p>See <a href="/docs">docs</a> <sup>[1]</sup>.</p>"#);Source§impl Dom
impl Dom
Sourcepub fn base_url(&self, base: Option<&str>) -> Option<String>
pub fn base_url(&self, base: Option<&str>) -> Option<String>
The URL relative links in this document resolve against.
That is the href of the first <base href> element, resolved against
base (typically the URL the page was fetched from); without a
<base> element, base itself. None when neither is available.
As in a browser, a <base> inside a <template> does not count (a
template’s content is not part of the document), and neither does one
inside <svg> or <math> (it is not an HTML <base> there).
let dom = domtree::parse(r#"<head><base href="/docs/"></head>"#);
assert_eq!(
dom.base_url(Some("https://example.com/index.html")).as_deref(),
Some("https://example.com/docs/"),
);
assert_eq!(dom.base_url(None).as_deref(), Some("/docs/"));
assert_eq!(domtree::parse("<p>x</p>").base_url(None), None);Sourcepub fn links(&self, base: Option<&str>) -> Vec<Link<'_>>
pub fn links(&self, base: Option<&str>) -> Vec<Link<'_>>
Every hyperlink in the document — each <a href> and <area href> —
in document order, with its URL resolved and its readable text.
URLs resolve against Dom::base_url(base), so a <base href> in
the page is honoured. javascript: links are skipped: they run code
rather than navigate. Fragment-only (#top), mailto: and other links
are kept; filter on Link::url as needed. Embedded resources (<img src>, <link href>, <script src>, …) are not links; see
Dom::resources.
Link text comes from one pass over the document; in invalid nested
links (<a><a>…</a></a>), the inner text belongs to the inner link
only.
let dom = domtree::parse(
r#"<nav><a href="/docs/">Docs</a> <a href="javascript:void(0)">Menu</a></nav>
<p>See <a href="../faq.html#top">the
FAQ</a> or <a href="https://example.org"><img src=logo.png alt="Example"></a>.</p>"#,
);
let links = dom.links(Some("https://example.com/en/guide/"));
let found: Vec<_> = links.iter().map(|l| (l.url.as_str(), l.text.as_str())).collect();
assert_eq!(found, [
("https://example.com/docs/", "Docs"),
("https://example.com/en/faq.html#top", "the FAQ"),
("https://example.org", "Example"),
]);
assert_eq!(links[1].href, "../faq.html#top");Sourcepub fn resources(&self, base: Option<&str>) -> Vec<Resource<'_>>
pub fn resources(&self, base: Option<&str>) -> Vec<Resource<'_>>
Every embedded resource URL in the document, in document order:
<link href> (stylesheets, icons, feeds, …), every src (images,
scripts, frames, media, …), each candidate of a srcset, poster and
<object data>, resolved against Dom::base_url(base).
Empty values and javascript: URLs are skipped. For each element the
attributes come in the order href, src, srcset, poster, data;
if an attribute is repeated, the first one counts (as with
NodeRef::attr).
let dom = domtree::parse(
r#"<link rel=stylesheet href=/main.css>
<img src="a.png" srcset="a.png 1x, a@2x.png 2x">
<script src="https://cdn.example.com/x.js"></script>"#,
);
let urls: Vec<_> = dom
.resources(Some("https://example.com/blog/"))
.into_iter()
.map(|r| (r.attr, r.url))
.collect();
assert_eq!(urls, [
("href", "https://example.com/main.css".to_string()),
("src", "https://example.com/blog/a.png".to_string()),
("srcset", "https://example.com/blog/a.png".to_string()),
("srcset", "https://example.com/blog/a@2x.png".to_string()),
("src", "https://cdn.example.com/x.js".to_string()),
]);Source§impl Dom
impl Dom
Sourcepub fn metadata(&self) -> Metadata
pub fn metadata(&self) -> Metadata
The page’s Metadata: title, description, canonical URL, language,
OpenGraph and Twitter-card fields, raw JSON-LD blocks and feed links.
The whole document is searched (not just <head>: this parser does not
move misplaced tags there), in one pass. Names and properties match
ASCII case-insensitively; empty values are ignored.
let dom = domtree::parse(r#"<html lang="en"><head>
<title> Shipping a parser
| Blog </title>
<meta name="description" content="What we learned.">
<meta property="og:image" content="https://example.com/cover.png">
<meta name="twitter:card" content="summary_large_image">
<link rel="canonical" href="https://example.com/p/1">
<link rel="alternate" type="application/rss+xml" href="/feed.xml">
<script type="application/ld+json">{"@type": "BlogPosting"}</script>
</head></html>"#);
let meta = dom.metadata();
assert_eq!(meta.title.as_deref(), Some("Shipping a parser | Blog"));
assert_eq!(meta.description.as_deref(), Some("What we learned."));
assert_eq!(meta.lang.as_deref(), Some("en"));
assert_eq!(meta.canonical.as_deref(), Some("https://example.com/p/1"));
assert_eq!(meta.image.as_deref(), Some("https://example.com/cover.png"));
assert_eq!(meta.twitter, [("twitter:card".to_string(), "summary_large_image".to_string())]);
assert_eq!(meta.json_ld, [r#"{"@type": "BlogPosting"}"#]);
assert_eq!(meta.feeds, ["/feed.xml"]);Source§impl Dom
impl Dom
Sourcepub fn main_content(&self) -> Option<NodeRef<'_>>
pub fn main_content(&self) -> Option<NodeRef<'_>>
The element holding the page’s main content — the article, post or
documentation body without the navigation, sidebars, comments and
footer around it. The “reader mode” step before
NodeRef::to_markdown in an LLM or RAG pipeline.
A small, deterministic heuristic in the spirit of Mozilla’s
Readability. Text is measured in non-whitespace characters of visible
text (no <script>, <style>, hidden, aria-hidden="true" or
display:none). Boilerplate means <nav>, <aside>, a
<header>/<footer> that is not inside an <article>/<main>, an
element with a landmark role such as navigation, complementary
or contentinfo, or one whose class/id has a boilerplate word
(nav, menu, header, footer, sidebar, comment, share,
social, related, ad/ads/advert, promo, cookie,
banner, breadcrumb, …) and no strong content word (article,
content, main, body).
- Semantic markup first. If exactly one outermost
<article>has enough text (200+ characters and at least a fifth of the page, or half the page), at most half of it in links and at most a quarter in boilerplate, and it is not itself inside boilerplate, it is the answer. Otherwise the same test runs on<main>/[role=main]. - Score paragraphs. Each innermost paragraph-like element
(
<p>,<pre>,<td>,<li>,<dd>, or a block with 25+ characters of its own text) with 25+ characters, at most half of them links, and not inside boilerplate scores1 + commas + min(chars / 100, 3). The score goes to its parent in full, to its grandparent halved, and to three more ancestors divided by 6, 9 and 12. - Pick the best container. Each scored ancestor starts from a
base by tag (
div/article/main+5,pre/td/blockquote+3, lists and forms −3, headings −5) plus ±25 each for content / boilerplate words in itsclassand in itsid, adds its paragraph scores, and is scaled by1 − link density. The highest final score wins; ties go to the earlier element. If no paragraph qualified, step 2 is retried with boilerplate allowed. - Widen to the whole article. While the winner’s parent adds no
boilerplate, at most as much text as the winner has, and mostly
non-link text (a title, a byline, the rest of a split body), the
parent becomes the winner — stopping below
<body>.
Returns None when no paragraph qualifies even with boilerplate allowed
(an empty, links-only or garbage page), or when the best “container” is the
document itself (paragraphs at the top level with no common parent
element). Use the whole document then. The chosen element is returned
as is: boilerplate inside it (a share bar in an article) is not
removed.
let dom = domtree::parse(r#"<body>
<nav><a href="/">Home</a> <a href="/blog">Blog</a></nav>
<div class="post">
<h1>Arena trees</h1>
<div class="entry">
<p>An arena stores every node in one vector, and links them by index.</p>
<p>Traversal is cheap, removal is a flag, and nothing is reference-counted.</p>
</div>
</div>
<div id="comments"><p>Great post, thanks for writing it up in so much detail!</p></div>
</body>"#);
let main = dom.main_content().unwrap();
assert_eq!(main.attr("class"), Some("post"));
assert!(main.to_markdown().starts_with("# Arena trees"));
assert!(!main.to_text().contains("Great post"));
assert!(domtree::parse("<nav><a href=/>Home</a></nav>").main_content().is_none());Source§impl Dom
impl Dom
Sourcepub fn errors(&self) -> &[ParseError]
pub fn errors(&self) -> &[ParseError]
Recovery diagnostics collected during parsing (empty for clean input).
Sourcepub fn get(&self, id: NodeId) -> Option<NodeRef<'_>>
pub fn get(&self, id: NodeId) -> Option<NodeRef<'_>>
A handle to a node by id, or None if the id is out of range.
Sourcepub fn root(&self) -> Option<NodeRef<'_>>
pub fn root(&self) -> Option<NodeRef<'_>>
The first top-level node (the document element for a typical page).
Sourcepub fn roots(&self) -> impl Iterator<Item = NodeRef<'_>>
pub fn roots(&self) -> impl Iterator<Item = NodeRef<'_>>
All top-level nodes, in document order.
Sourcepub fn nodes(&self) -> impl Iterator<Item = NodeRef<'_>>
pub fn nodes(&self) -> impl Iterator<Item = NodeRef<'_>>
Every node in the document, in document (pre-order) order. Nodes
removed with Dom::remove (and created nodes not yet attached) are
skipped.
let mut dom = domtree::parse("<p>a</p><p>c</p>");
let c = dom.find_by_tag("p").nth(1).unwrap().id();
dom.insert_html_before(c, "<p>b</p>");
let text: Vec<_> = dom.nodes().filter_map(|n| n.text()).collect();
assert_eq!(text, ["a", "b", "c"]);Sourcepub fn find_by_tag<'a>(
&'a self,
name: &'a str,
) -> impl Iterator<Item = NodeRef<'a>> + 'a
pub fn find_by_tag<'a>( &'a self, name: &'a str, ) -> impl Iterator<Item = NodeRef<'a>> + 'a
All elements whose tag name equals name (ASCII case-insensitive).
Sourcepub fn find_by_id<'a>(&'a self, id: &'a str) -> Option<NodeRef<'a>>
pub fn find_by_id<'a>(&'a self, id: &'a str) -> Option<NodeRef<'a>>
The first element with id="…" equal to id, if any.
Sourcepub fn find_by_class<'a>(
&'a self,
class: &'a str,
) -> impl Iterator<Item = NodeRef<'a>> + 'a
pub fn find_by_class<'a>( &'a self, class: &'a str, ) -> impl Iterator<Item = NodeRef<'a>> + 'a
All elements whose class attribute contains class.
Source§impl Dom
impl Dom
Sourcepub fn to_text(&self) -> String
pub fn to_text(&self) -> String
The document as readable plain text.
Unlike NodeRef::text_content, this is what a reader sees: no
<head>/<script>/<style> content, whitespace collapsed, a new line
for each block element, a blank line between paragraphs, <br> as a line
break, tab-separated table cells and <pre> kept verbatim. The result
has no trailing whitespace on lines, no runs of blank lines, and no
leading or trailing blank lines.
let dom = domtree::parse(
"<head><title>T</title></head><h1>Hello</h1>\
<p>Some <b>bold</b>\n text.<br>Next line.</p><script>x()</script>",
);
assert_eq!(dom.to_text(), "Hello\n\nSome bold text.\nNext line.");Source§impl Dom
impl Dom
Sourcepub fn to_markdown(&self) -> String
Available on crate feature markdown only.
pub fn to_markdown(&self) -> String
markdown only.Convert the document to Markdown (CommonMark + GFM tables and strikethrough) — handy for LLM prompts, RAG indexes and notes.
Headings, paragraphs, emphasis, links, images, code spans, fenced code
blocks (language from class="language-…"), nested lists, block quotes
and tables map to their Markdown forms, and text is escaped so it renders
as itself. Hidden elements (<head>, <script>, …) are skipped as in
Dom::to_text. Blocks are separated by one blank line, <br> becomes
a backslash hard break, and the output has no leading or trailing
newline.
let dom = domtree::parse(
"<h1>Title</h1><p>Some <b>bold</b> and a <a href='/x'>link</a>.</p>\
<ul><li>one</li><li>two</li></ul>",
);
assert_eq!(
dom.to_markdown(),
"# Title\n\nSome **bold** and a [link](/x).\n\n- one\n- two",
);Source§impl Dom
impl Dom
Sourcepub fn to_markdown_chunks(&self, max_chars: usize) -> Vec<Chunk>
Available on crate feature markdown only.
pub fn to_markdown_chunks(&self, max_chars: usize) -> Vec<Chunk>
markdown only.The document’s Markdown (Dom::to_markdown) split into chunks of at
most max_chars characters (Unicode scalar values) for retrieval
(RAG), each carrying its heading path.
- Sections. A chunk never spans two sections: each top-level
heading (
#…######) starts a new chunk, and its plain-text title joins theChunk::heading_path(replacing any heading of the same or a deeper level). A heading directly followed by a deeper one stays in the same chunk, so# Guide/## Install/ text is one chunk. Titles in the path are capped at 256 characters (cut, then…). - Packing. Within a section, whole blocks (paragraphs, lists,
quotes, tables, fenced code) are packed greedily, separated by a
blank line, while the chunk stays within
max_chars. Small sections are not merged with their neighbours. - Oversized blocks. A block that alone exceeds
max_charsis split at line boundaries. A split code block is re-fenced and a split table repeats its header rows in every piece, so each piece is valid Markdown on its own (when the fence or header leaves room for a line; otherwise lines are split plainly).
Every chunk is at most max_chars long, except one made of a single
line that is itself longer — lines are never cut (a long paragraph is
one line). A section heading that cannot share a chunk with the first
block under it becomes a chunk of its own. No content is lost or
reordered: concatenating the chunks gives back the Markdown, plus any
repeated fences and table headers. max_chars == 0 is allowed (every
line becomes its own chunk). Blank chunks are never returned.
Work and output are linear in the size of the Markdown: every chunk holds at least one line of it, and its heading path at most six capped titles.
let dom = domtree::parse(
"<h1>Guide</h1><p>Welcome.</p>\
<h2>Install</h2><p>Run the installer.</p><pre>cargo add dom-tree-rs</pre>\
<h2>Usage</h2><p>Parse, then query.</p>",
);
let chunks = dom.to_markdown_chunks(500);
let parts: Vec<_> = chunks.iter().map(|c| (c.context(), c.markdown.as_str())).collect();
assert_eq!(parts, [
("Guide".to_string(), "# Guide\n\nWelcome."),
("Guide > Install".to_string(), "## Install\n\nRun the installer.\n\n```\ncargo add dom-tree-rs\n```"),
("Guide > Usage".to_string(), "## Usage\n\nParse, then query."),
]);Source§impl Dom
impl Dom
Sourcepub fn to_json(&self) -> String
Available on crate feature json only.
pub fn to_json(&self) -> String
json only.Serialize the document to a compact JSON string.
The shape is self-describing: every node is an object with a "type" of
element / text / comment / doctype, elements carry tag,
attrs (an object) and children (an array), and the whole document is
wrapped in { "type": "document", "errors": N, "children": [...] }.
let dom = domtree::parse("<a href=\"/x\">hi</a>");
let json = dom.to_json();
assert!(json.contains("\"tag\":\"a\""));
assert!(json.contains("\"href\":\"/x\""));
assert!(json.contains("\"value\":\"hi\""));Source§impl Dom
impl Dom
Sourcepub fn select<'a, S: AsSelector + ?Sized>(
&'a self,
selector: &S,
) -> impl Iterator<Item = NodeRef<'a>>
Available on crate feature query only.
pub fn select<'a, S: AsSelector + ?Sized>( &'a self, selector: &S, ) -> impl Iterator<Item = NodeRef<'a>>
query only.Every element matching a CSS selector, in document order.
selector may be a &str, a &String or a pre-parsed &Selector
(see AsSelector). It is parsed once per call; an invalid selector
string matches nothing (use Selector::parse to see the error).
let dom = domtree::parse(
"<div><ul><li class='x'>a</li><li>b</li></ul><ol><li>c</li></ol></div>",
);
assert_eq!(dom.select("ul li.x").count(), 1); // descendant + class
assert_eq!(dom.select("ol > li").count(), 1); // child combinator
assert_eq!(dom.select("ul, ol").count(), 2); // selector list
assert_eq!(dom.select("li:nth-child(2)").count(), 1);
assert_eq!(dom.select("ul:has(.x) + ol").count(), 1);Sourcepub fn select_first<S: AsSelector + ?Sized>(
&self,
selector: &S,
) -> Option<NodeRef<'_>>
Available on crate feature query only.
pub fn select_first<S: AsSelector + ?Sized>( &self, selector: &S, ) -> Option<NodeRef<'_>>
query only.The first element (in document order) matching a CSS selector.
let dom = domtree::parse("<p>a</p><p class='x'>b</p><p class='x'>c</p>");
let p = dom.select_first("p.x").unwrap();
assert_eq!(p.text_content(), "b");
assert!(dom.select_first("table").is_none());