Repository navigation
Editing
Every edit is a method on Document that takes &mut self. You create nodes, which start detached, then put them where you want them; you move and remove nodes; and you set, add, rename and remove attributes. The code is in crates/parsec/src/edit.rs.
On this page: Creating nodes · Moving nodes · Removing nodes · adopt_template_contents · Attributes · rename · EditError · The rules edits keep · The cost of editing
Two things hold for everything on this page:
- Every method that takes a
NodeIdpanics on an id that'sdoc.len()or more. - Edits work on the tree. They don't rerun the parser, so nothing gets fixed up for you: if you put a
<tr>straight into a<div>, that's where it stays, and that's what the serializer writes.
Each of these adds a new node to the arena and returns its id. The node has no parent until you insert it.
| Method | Makes |
|---|---|
create_element(&mut self, ns: Ns, name: &str) -> NodeId |
An element with no attributes. See Names and namespaces. |
create_text(&mut self, text: &str) -> NodeId |
A text node. |
create_comment(&mut self, text: &str) -> NodeId |
A comment. |
create_cdata(&mut self, text: &str) -> NodeId |
A CDATA section. The HTML parser never makes these; they're for XML trees. |
create_processing_instruction(&mut self, target: &str, data: &str) -> NodeId |
A processing instruction, written <?target data?>. |
Nothing is checked. A comment containing --> or a processing instruction containing ?> is created as given, and serializes into markup that won't parse back the same way. The strings are copied into the document.
use parsec::{Document, Kind, Ns};
let mut doc = Document::empty();
let p = doc.create_element(Ns::Html, "P");
let t = doc.create_text("fish & chips");
let c = doc.create_comment(" note ");
assert_eq!(doc.kind(p), Kind::Element { ns: Ns::Html, name: "p" });
assert_eq!(doc.parent(t), None);
doc.append(doc.root(), p).unwrap();
doc.append(p, t).unwrap();
doc.append(p, c).unwrap();
assert_eq!(doc.inner_html(0), "<p>fish & chips<!-- note --></p>");create_element treats the name by namespace:
-
Ns::Html: lowercased, asdocument.createElementdoes."DIV"makesdiv. An HTMLtemplategets its own contents node, as a parsed one does. -
Ns::SvgandNs::MathMl: kept as you wrote it."linearGradient"stayslinearGradient. Write the case the element should serialize with. -
Ns::Xml: kept exactly, prefix and all ("dc:creator"), and never treated as an HTML element whatever the name.
The namespace is what makes an element SVG; the name doesn't. create_element(Ns::Html, "svg") makes an HTML element that happens to be called svg. It serializes the same, but selectors and the tree builder's rules treat it as HTML, and you can't change an element's namespace later. When you build SVG or MathML by hand, give every element in it Ns::Svg or Ns::MathMl.
use parsec::css::SelectorList;
use parsec::Ns;
let mut doc = parsec::parse_fragment("", Ns::Html, "div", false);
let svg = doc.create_element(Ns::Svg, "svg");
let grad = doc.create_element(Ns::Svg, "linearGradient");
doc.set_attr(grad, "gradientUnits", "userSpaceOnUse");
doc.append(svg, grad).unwrap();
doc.append(0, svg).unwrap();
assert_eq!(
doc.inner_html(0),
r#"<svg><linearGradient gradientUnits="userSpaceOnUse"></linearGradient></svg>"#
);
// SVG type selectors keep their case, as in a browser.
assert_eq!(SelectorList::parse("linearGradient").unwrap().select(&doc, 0).len(), 1);
assert!(SelectorList::parse("lineargradient").unwrap().select(&doc, 0).is_empty());pub fn insert_before(&mut self, parent: NodeId, child: NodeId, before: Option<NodeId>)
-> Result<(), EditError>
Moves child into parent, just before before, or at the end when before is None. If child is somewhere else in the tree, it's taken out of there first; nodes move, they're never copied.
It checks, in this order, and changes nothing if a check fails:
-
parentis an element, a document or a fragment. OtherwiseErr(EditError::NotAParent). -
before, if given, is a child ofparent. OtherwiseErr(EditError::NotAChild). -
childisn'tparentor one of its ancestors. OtherwiseErr(EditError::Cycle).
If before is child itself, there's nothing to do and it returns Ok(()).
pub fn append(&mut self, parent: NodeId, child: NodeId) -> Result<(), EditError>
The same as insert_before(parent, child, None): child becomes parent's last child.
use parsec::{EditError, Ns};
let mut doc = parsec::parse_fragment("<ol><li>b</li></ol>", Ns::Html, "div", false);
let ol = doc.first_child(0).unwrap();
let b = doc.first_child(ol).unwrap();
let a = doc.create_element(Ns::Html, "li");
let text = doc.create_text("a");
doc.append(a, text).unwrap();
doc.insert_before(ol, a, Some(b)).unwrap();
assert_eq!(doc.inner_html(0), "<ol><li>a</li><li>b</li></ol>");
// Moving b to the front takes it out of its old place.
doc.insert_before(ol, b, doc.first_child(ol)).unwrap();
assert_eq!(doc.inner_html(0), "<ol><li>b</li><li>a</li></ol>");
// The three refusals.
assert_eq!(doc.append(b, ol), Err(EditError::Cycle));
assert_eq!(doc.append(text, b), Err(EditError::NotAParent));
assert_eq!(doc.insert_before(b, a, Some(ol)), Err(EditError::NotAChild));The DOM refuses more than Parsec does today. insert_before doesn't check what kind of node child is, so it will let you:
- put a second element, or text, directly under a
Documentnode; - put a doctype inside an element;
- move a
DocumentorFragmentnode, including a template's contents node, under an element, as long as that doesn't make a cycle. Even the root can be moved under a detached element.
None of these breaks memory safety or panics, but the tree stops meaning what the HTML Standard says it means: :root stops matching once the root has a parent, for instance.
Warning
Keep to elements, text, comments and processing instructions as children, and leave the root and contents nodes where the parser put them.
pub fn remove(&mut self, id: NodeId)
Takes id out of the tree. It does nothing if id has no parent (the root, or a node that's already detached).
A removed node isn't freed. It stays in the arena with its whole subtree still hanging off it, its id stays valid, and you can insert it again. The memory goes when the Document is dropped.
use parsec::Ns;
let mut doc = parsec::parse_fragment("<p>a</p><p>b <em>c</em></p>", Ns::Html, "div", false);
let second = doc.last_child(0).unwrap();
doc.remove(second);
assert_eq!(doc.inner_html(0), "<p>a</p>");
assert_eq!(doc.parent(second), None);
// The subtree came with it.
assert_eq!(doc.text(second), "b c");
doc.insert_before(0, second, doc.first_child(0)).unwrap();
assert_eq!(doc.inner_html(0), "<p>b <em>c</em></p><p>a</p>");Because ids aren't reused, an id you kept still names the same node after any number of edits. What it can't tell you is whether that node is still in the tree; check parent for that. Handles that know when their node was removed are planned (#12).
pub fn adopt_template_contents(&mut self)
Moves every HTML <template>'s contents in under the template element itself, at the end of its children, and leaves each contents node empty. It covers every template in the arena, including templates nested inside other templates' contents and templates you've detached.
The HTML Standard keeps template contents out of the tree (see The document tree). lxml and Beautiful Soup don't, and scrapers usually want them visible. After this call, children, text and selectors see what was in each template. Serialization doesn't change, because the serializer writes a template's contents and then its children.
use parsec::css::SelectorList;
use parsec::Ns;
let mut doc = parsec::parse_fragment("<template><b>x</b></template>", Ns::Html, "div", false);
let find_b = SelectorList::parse("template > b").unwrap();
assert!(find_b.select(&doc, 0).is_empty());
doc.adopt_template_contents();
assert_eq!(find_b.select(&doc, 0).len(), 1);
assert_eq!(doc.inner_html(0), "<template><b>x</b></template>");pub fn set_attr(&mut self, id: NodeId, name: &str, value: &str)
Sets an attribute, as setAttribute does: if the element already has a no-namespace attribute with that name, its value is replaced; otherwise the attribute is added at the end.
- The name is ASCII-lowercased, unless the element is
Ns::Xml. That's how the parser stores names too, and the serializer puts SVG's and MathML's case back for the names in the spec's table, soset_attr(svg, "viewBox", ..)writesviewBox. A mixed-case SVG name that isn't in the table (fooBar) is written lowercase; useadd_attrif you need it kept. - Only attributes with
AttrNs::Noneare looked at. On an SVG<use>withxlink:href,set_attr(id, "xlink:href", ..)adds a second, no-namespace attribute rather than changing the namespaced one, and the element then serializes with both. To change a namespaced attribute, rebuild the list withset_attrs. - Does nothing if
idisn't an element.
pub fn remove_attr(&mut self, id: NodeId, name: &str) -> bool
Removes the attribute called name and returns true, or returns false if there wasn't one or id isn't an element. The name is lowercased as for set_attr. It compares with the stored name in any namespace, and a namespaced attribute's stored name has its prefix (xlink:href), so remove_attr(id, "xlink:href") removes the XLink one while remove_attr(id, "href") doesn't.
use parsec::Ns;
let mut doc = parsec::parse_fragment(r#"<a href="x">a</a>"#, Ns::Html, "div", false);
let a = doc.first_child(0).unwrap();
doc.set_attr(a, "HREF", "y"); // replaces, name lowercased
doc.set_attr(a, "title", "t"); // adds at the end
assert_eq!(doc.outer_html(a), r#"<a href="y" title="t">a</a>"#);
assert!(doc.remove_attr(a, "href"));
assert!(!doc.remove_attr(a, "href"));
assert_eq!(doc.outer_html(a), r#"<a title="t">a</a>"#);pub fn add_attr(&mut self, id: NodeId, ns: AttrNs, name: &str, value: &str)
Appends an attribute exactly as given, without lowercasing the name and without replacing one of the same name. It's meant for building XML, where you've already checked names. Adding a duplicate is your responsibility, and both copies will be written out. For a namespaced attribute, give the written name with its prefix (xlink:href with AttrNs::XLink); attrs then reports href. Does nothing if id isn't an element.
pub fn set_attrs(&mut self, id: NodeId, attrs: &[(AttrNs, &str, &str)])
Replaces all of the element's attributes with attrs, in that order, names exactly as given. Nothing is lowercased or deduplicated: on an HTML element, pass lowercase names, or selectors and set_attr won't find them. Passing an empty slice removes every attribute. Does nothing if id isn't an element.
pub fn rename_attr(&mut self, id: NodeId, index: usize, ns: AttrNs, name: &str)
Renames the element's indexth attribute (counting from 0, in the order attrs gives) to name, exactly as given, and sets its namespace to ns. Does nothing if there's no such attribute or id isn't an element.
pub fn remove_attr_at(&mut self, id: NodeId, index: usize) -> bool
Removes the element's indexth attribute and returns true, or returns false if there's no such attribute or id isn't an element.
use parsec::{AttrNs, Document, Ns};
let mut doc = Document::empty();
let entry = doc.create_element(Ns::Xml, "dc:Creator");
doc.append(0, entry).unwrap();
doc.add_attr(entry, AttrNs::None, "ID", "up");
doc.add_attr(entry, AttrNs::None, "id", "lo");
doc.add_attr(entry, AttrNs::Host(3), "x:href", "/p");
assert_eq!(doc.outer_html(entry), r#"<dc:Creator ID="up" id="lo" x:href="/p"></dc:Creator>"#);
doc.rename(entry, "Creator");
doc.rename_attr(entry, 2, AttrNs::None, "href");
assert!(doc.remove_attr_at(entry, 0));
assert_eq!(doc.outer_html(entry), r#"<Creator id="lo" href="/p"></Creator>"#);
doc.set_attrs(entry, &[(AttrNs::None, "a", "1"), (AttrNs::None, "b", "2")]);
assert_eq!(doc.html_alone(entry), r#"<Creator a="1" b="2">"#);pub fn rename(&mut self, id: NodeId, name: &str)
Renames an element, keeping its namespace, attributes and children. The name is lowercased for Ns::Html and kept as given for the rest, as in create_element, and the element's tag number follows the new name. Does nothing if id isn't an element.
Renaming doesn't add or remove template contents. A div renamed to template has no contents node; a template renamed to div keeps its contents node (which template_contents still reports) but no longer writes it out.
use parsec::Ns;
let mut doc = parsec::parse_fragment("<b class=x>bold</b>", Ns::Html, "div", false);
let b = doc.first_child(0).unwrap();
doc.rename(b, "STRONG");
assert_eq!(doc.inner_html(0), r#"<strong class="x">bold</strong>"#);
assert_eq!(doc.known_tag(b), parsec::tag_number("strong"));#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditError { Cycle, NotAParent, NotAChild }
Why insert_before or append refused. It implements std::error::Error and Display:
| Variant | Display |
|---|---|
Cycle |
a node can't be inserted into itself or its descendants |
NotAParent |
only an element, a document or a fragment can have children |
NotAChild |
the node to insert before isn't a child of the parent |
- No cycles.
insert_beforeandappendnever make a node its own ancestor. - Template contents stay separate, unless you call
adopt_template_contentsor move nodes in yourself. A template's contents node has no parent. - Shared attributes stay shared only until you change them. When the parser rebuilds formatting elements (the
<b>in<p><b>a</p>tis opened again aroundt), the copies share their original's attribute list. Every attribute edit writes a fresh copy of the list for the one element you changed, so the others are left alone. - Strings don't move. A string an edit adds gets its own allocation, and nothing an edit does moves a string the document already holds. Within Rust's borrow rules you can't observe the difference, but code that keeps raw pointers to strings across edits, as a binding to another language might, can rely on it.
Edits are cheap but not free, and none of them gives memory back:
- Creating a node appends to the arena.
- Inserting, appending and removing relink a handful of indexes, except that
insert_beforewalks up fromparentto the root to check for a cycle, so it costs the depth of the tree. - Every attribute edit copies the element's whole attribute list to the end of the document's attribute vector, and stores the new strings separately. Setting many attributes one at a time on an element with many attributes costs more than one
set_attrscall. - Removed nodes and replaced attribute lists stay allocated until the
Documentis dropped. A long-running process that edits one document heavily will see it grow; parse a fresh one, or serialize and reparse, when that matters.
- The document tree, for reading what you've changed.
- Serialization, for writing it back out.
- Moving from Lexbor, for Lexbor's insert calls next to these.
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