Repository navigation
Moving from Lexbor
If you have code on Lexbor's C API, this page shows the Parsec call for each thing you do, in Rust. It covers what bindings use most: parsing a document or a fragment, finding elements with selectors, reading text, serializing, getting and setting attributes, and inserting and removing nodes.
On this page: What changes in how you think about it · The calls side by side · Parsing a document · Attributes · Inserting and removing nodes · Setting text · Fragments · Selectors that behave differently · What isn't there yet
Note
Parsec doesn't have a C API yet, so for now moving means calling Parsec from Rust.
A C API is planned (#20), with prebuilt libraries (#21), and so is a fuller migration guide that covers it (#22). If you're on selectolax, a Python package with a familiar API is planned too (#23).
The Lexbor function names below are from its headers at commit 145872a, and each links to its declaration file.
Not sure where to start? Find what your code does in Lexbor today, and follow it to the page you need:
flowchart TD
start{"What does your code do with Lexbor?"}
start -->|"parses documents or fragments"| parsing["Parsing"]
start -->|"queries with CSS selectors"| selectors["Selectors"]
start -->|"reads text, attributes, children"| tree["The document tree"]
start -->|"inserts, removes, sets attributes"| editing["Editing"]
start -->|"serializes back to HTML"| ser["Serialization"]
start -->|"runs the tokenizer with a callback"| tok["Tokenizer"]
start -->|"shares documents across threads"| threads["Thread safety and memory safety"]
start -->|"is called from C, Python or JavaScript"| later["Not yet: #20, #23, #24"]
Each box is a page in the sidebar: Parsing, Selectors, The document tree, Editing, Serialization, Tokenizer, Thread safety and memory safety. This page has the calls side by side for all of them.
- Nothing to create or destroy. There are no
_create,_initor_destroycalls. ADocumentfrees everything when it's dropped. ASelectorListis an ordinary value you can keep. - Nodes are
NodeIds, not pointers. ANodeIdis au32index into the document. You read and change nodes through the document:doc.text(id), notnode->.... An id can't dangle: a removed node stays in the document until the document goes, so using it later is never a use-after-free. It also means memory for removed nodes isn't given back early. - Errors are
Results, and there are few of them. Parsing doesn't fail on bad markup, only on input over 512 MiB or a budget running out. Edits that would break the tree returnEditError. Most other calls can't fail, and those that do nothing on the wrong kind of node say so. - Strings are
&str. Lengths come with them, and they're borrowed from the document, so they're good while you hold the document and don't edit it. - Input is text. Parsec takes UTF-8 as
&strand doesn't sniff encodings yet (#5).
Every Lexbor call on this page, with its Parsec equivalent
| You do this in Lexbor | In Parsec | Notes |
|---|---|---|
lxb_html_document_create, lxb_html_document_parse(doc, html, len) (document.h) |
parsec::parse(html, scripting) |
Returns the document. parse_with adds a budget and returns Result. |
lxb_html_document_destroy(doc) |
drop the Document
|
|
lxb_html_parser_scripting_set (parser.h) |
the scripting argument, or Options::scripting
|
|
lxb_html_document_parse_chunk_begin and friends |
not yet | #6 |
lxb_html_document_parse_fragment(doc, element, html, len) |
parsec::parse_fragment(html, ns, "div", scripting) |
Takes the context's namespace and name, not an element, and returns a separate document. See Fragments. |
lxb_css_parser_create, lxb_css_selectors_parse(parser, s, len) (css/selectors/selectors.h) |
SelectorList::parse(s) |
Result<SelectorList, SelectorError>. |
lxb_selectors_create, lxb_selectors_find(sel, root, list, cb, ctx) (selectors/selectors.h) |
list.select(&doc, root) |
Returns a Vec<NodeId> in document order instead of calling back. root itself is never included. Stopping at the first match isn't there yet (#18). |
lxb_selectors_match_node(sel, node, list, cb, ctx) |
list.matches(&doc, node, None) |
Returns bool. |
lxb_dom_node_text_content(node, &len) (node.h) |
doc.text(node) |
Returns a new String. |
lxb_dom_node_text_content_set(node, text, len) |
remove the children, then create_text and append
|
See Setting text. |
lxb_html_serialize_tree_str(node, &str) (serialize.h) |
doc.outer_html(node) |
|
lxb_html_serialize_deep_str(node, &str) |
doc.inner_html(node) |
|
lxb_html_serialize_pretty_tree_str |
not yet | #9 |
lxb_dom_element_get_attribute(el, name, len, &vlen) (element.h) |
doc.attrs(el).find(|a| a.1 == name).map(|a| a.2) |
No shortcut method yet. |
lxb_dom_element_has_attribute |
doc.attrs(el).any(|a| a.1 == name) |
|
lxb_dom_element_set_attribute(el, name, len, value, vlen) |
doc.set_attr(el, name, value) |
Lowercases the name, replaces or adds. |
lxb_dom_element_remove_attribute(el, name, len) |
doc.remove_attr(el, name) |
Returns whether there was one. |
lxb_dom_element_first_attribute and iterating |
doc.attrs(el) |
(AttrNs, name, value) in source order. |
lxb_dom_element_local_name(el, &len) |
doc.lower_name(el), or doc.kind(el)
|
kind gives the name with SVG's case. |
lxb_dom_node_first_child, lxb_dom_node_next, lxb_dom_node_parent
|
doc.first_child(n), doc.next_sibling(n), doc.parent(n)
|
Option<NodeId>. Also children(n). |
lxb_dom_document_create_element(doc, name, len, NULL) (document.h) |
doc.create_element(Ns::Html, name) |
Give the namespace. |
lxb_dom_document_create_text_node(doc, text, len) |
doc.create_text(text) |
|
lxb_dom_node_insert_child(to, node) |
doc.append(to, node) |
Result; refuses cycles. |
lxb_dom_node_insert_before(to, node) |
doc.insert_before(parent_of_to, node, Some(to)) |
Lexbor's to is the sibling; Parsec takes the parent and the sibling. |
lxb_dom_node_insert_after(to, node) |
doc.insert_before(parent_of_to, node, doc.next_sibling(to)) |
|
lxb_dom_node_remove(node) |
doc.remove(node) |
|
lxb_dom_node_destroy, lxb_dom_node_destroy_deep
|
doc.remove(node) |
Nothing to free; the memory goes with the document. |
use parsec::css::SelectorList;
// lxb_html_document_create + lxb_html_document_parse
let doc = parsec::parse("<div><p class=a>one</p><p>two</p></div>", false);
// lxb_css_parser_create + lxb_css_selectors_parse + lxb_selectors_find
let list = SelectorList::parse("p.a").unwrap();
for p in list.select(&doc, doc.root()) {
// lxb_dom_node_text_content
assert_eq!(doc.text(p), "one");
// lxb_html_serialize_tree_str
assert_eq!(doc.outer_html(p), r#"<p class="a">one</p>"#);
}
// lxb_html_document_destroy: nothing, doc is dropped here.use parsec::css::SelectorList;
let mut doc = parsec::parse(r#"<a href="/old" target=_blank>link</a>"#, false);
let a = SelectorList::parse("a").unwrap().select(&doc, doc.root())[0];
// lxb_dom_element_get_attribute
let href = doc.attrs(a).find(|a| a.1 == "href").map(|a| a.2.to_string());
assert_eq!(href.as_deref(), Some("/old"));
// lxb_dom_element_has_attribute
assert!(doc.attrs(a).any(|a| a.1 == "target"));
// lxb_dom_element_set_attribute
doc.set_attr(a, "href", "/new");
// lxb_dom_element_remove_attribute
assert!(doc.remove_attr(a, "target"));
assert_eq!(doc.outer_html(a), r#"<a href="/new">link</a>"#);Lexbor's insert_before(to, node) puts node before the sibling to. Parsec's insert_before(parent, node, before) takes the parent too, so you look it up:
use parsec::css::SelectorList;
use parsec::Ns;
let mut doc = parsec::parse("<ul><li id=b>b</li></ul>", false);
let b = SelectorList::parse("#b").unwrap().select(&doc, doc.root())[0];
let ul = doc.parent(b).unwrap();
let new_li = |doc: &mut parsec::Document, text: &str| {
let li = doc.create_element(Ns::Html, "li");
let t = doc.create_text(text);
doc.append(li, t).unwrap();
li
};
// lxb_dom_node_insert_before(b, a): a goes before b.
let a = new_li(&mut doc, "a");
doc.insert_before(ul, a, Some(b)).unwrap();
// lxb_dom_node_insert_after(b, c): c goes after b.
let c = new_li(&mut doc, "c");
doc.insert_before(ul, c, doc.next_sibling(b)).unwrap();
// lxb_dom_node_insert_child(ul, d): d goes last.
let d = new_li(&mut doc, "d");
doc.append(ul, d).unwrap();
// lxb_dom_node_remove(b), or lxb_dom_node_destroy_deep(b).
doc.remove(b);
assert_eq!(doc.inner_html(ul), "<li>a</li><li>c</li><li>d</li>");There's no textContent setter. Remove the children and add one text node:
use parsec::css::SelectorList;
use parsec::{Document, NodeId};
fn set_text(doc: &mut Document, el: NodeId, text: &str) {
let kids: Vec<NodeId> = doc.children(el).collect();
for kid in kids {
doc.remove(kid);
}
let t = doc.create_text(text);
doc.append(el, t).unwrap();
}
let mut doc = parsec::parse("<p>old <b>bold</b></p>", false);
let p = SelectorList::parse("p").unwrap().select(&doc, doc.root())[0];
set_text(&mut doc, p, "new & improved");
assert_eq!(doc.outer_html(p), "<p>new & improved</p>");lxb_html_document_parse_fragment parses into the document you give it, under a context element you made. Parsec's parse_fragment takes the context's namespace and name and returns a document of its own, whose root is a Fragment node holding the result:
use parsec::Ns;
// As if setting innerHTML on a <tbody>.
let frag = parsec::parse_fragment("<tr><td>1<td>2", Ns::Html, "tbody", false);
assert_eq!(frag.inner_html(frag.root()), "<tr><td>1</td><td>2</td></tr>");There's no function to move nodes from one document into another yet. If you need the equivalent of setting innerHTML on an element of an existing document, copy the fragment's nodes across. This helper does it for elements, text, comments and processing instructions, which is everything a fragment parse produces; it recurses once per level, so it's for fragments of ordinary depth.
The import and set_inner_html helpers, about 70 lines
use parsec::css::SelectorList;
use parsec::{AttrNs, Document, Kind, NodeId};
/// Copies `node` and everything under it from `src` into `dst`, detached,
/// and returns the copy.
fn import(dst: &mut Document, src: &Document, node: NodeId) -> Option<NodeId> {
let copy = match src.kind(node) {
Kind::Element { ns, name } => {
let el = dst.create_element(ns, name);
// The stored name for plain attributes, the written one (with
// its prefix) for namespaced ones: what the parser would store.
let attrs: Vec<(AttrNs, String, String)> = src
.attrs(node)
.zip(src.attrs_written(node))
.map(|((ns, stored, value), (_, written, _))| {
let name = if ns == AttrNs::None { stored } else { written };
(ns, name.to_string(), value.to_string())
})
.collect();
let borrowed: Vec<(AttrNs, &str, &str)> =
attrs.iter().map(|(ns, n, v)| (*ns, n.as_str(), v.as_str())).collect();
dst.set_attrs(el, &borrowed);
if let (Some(from), Some(to)) = (src.template_contents(node), dst.template_contents(el)) {
for child in src.children(from).collect::<Vec<_>>() {
if let Some(c) = import(dst, src, child) {
dst.append(to, c).unwrap();
}
}
}
el
}
Kind::Text(t) => dst.create_text(t),
Kind::Comment(c) => dst.create_comment(c),
Kind::ProcessingInstruction { target, data } => dst.create_processing_instruction(target, data),
Kind::Cdata(t) => dst.create_cdata(t),
Kind::Document | Kind::Fragment | Kind::Doctype { .. } => return None,
};
for child in src.children(node).collect::<Vec<_>>() {
if let Some(c) = import(dst, src, child) {
dst.append(copy, c).unwrap();
}
}
Some(copy)
}
/// Replaces `el`'s children with `html`, parsed in `el`'s context.
fn set_inner_html(doc: &mut Document, el: NodeId, html: &str) {
let (ns, name) = match doc.kind(el) {
Kind::Element { ns, name } => (ns, name.to_string()),
_ => return,
};
let frag = parsec::parse_fragment(html, ns, &name, false);
for kid in doc.children(el).collect::<Vec<_>>() {
doc.remove(kid);
}
for kid in frag.children(frag.root()).collect::<Vec<_>>() {
if let Some(copy) = import(doc, &frag, kid) {
doc.append(el, copy).unwrap();
}
}
}
fn main() {
let mut doc = parsec::parse("<table><tbody><tr><td>old</td></tr></tbody></table>", false);
let tbody = SelectorList::parse("tbody").unwrap().select(&doc, doc.root())[0];
set_inner_html(&mut doc, tbody, "<tr><td>a<td>b</tr>");
assert_eq!(doc.inner_html(tbody), "<tr><td>a</td><td>b</td></tr>");
assert_eq!(SelectorList::parse("td").unwrap().select(&doc, doc.root()).len(), 2);
}Most selectors match the same in both. The differences you're likely to meet, all listed on Selectors:
-
:scopematches the element you query from in Parsec. Lexbor parses it and never matches it (lexbor#257). -
:hover,:activeand:focusnever match in Parsec. Lexbor matches them on elements that have an attribute of the same name (selectors.c). - Selectors Lexbor parses but never matches, such as
:lang(),:dir(),:validand pseudo-elements, are errors in Parsec until they're implemented, so a query can't quietly find nothing. -
:lexbor-contains()is Lexbor's own and Parsec refuses it. Filter ondoc.text(id)instead. -
[href]means anhrefin no namespace, as in a browser; use[*|href]to include SVG'sxlink:href. -
selectnever includes the node you run it on. If you used Lexbor's option to match the root too, test the root withmatchesas well.
Each has an issue: chunked parsing (#6), encoding sniffing and decoding (#5, #25), parse errors (#7), pretty serialization (#9), first-match queries (#18), the CSS parser and CSSOM (#28), URL parsing (#26), and the C API itself (#20). Not tracked by any issue today: cloning nodes, importing between documents, and DOM mutation callbacks.
If something you rely on in Lexbor isn't on this page, tell us in Discussions; that's how the guide in #22 gets its list.
- Comparison with Lexbor, for what each library has, module by module.
- Selectors, for the full list of what matches.
- FAQ, for the questions people ask first.
Parsec is Apache-2.0 and not yet released; these pages describe main. Found something wrong here? Open an issue or ask in Discussions.
Repository · Issues · Discussions · Roadmap · Security policy · Licence