domtree/edit.rs
1//! The editing API: rewrite a parsed [`Dom`] in place.
2//!
3//! Nodes are never freed. Removing or replacing a node *detaches* it — its
4//! [`NodeId`] stays valid and its subtree can still be read and serialized —
5//! and created or inserted nodes are appended to the arena. Every structural
6//! edit keeps the parent/child/sibling links consistent (top-level nodes stay
7//! linked as siblings) and keeps the `detached` flag set on exactly the nodes
8//! that are not reachable from the top level, so iteration, selectors and
9//! serializers only ever see the live document.
10//!
11//! **Document order.** The parser only appends, so arena order is document
12//! order and [`Dom::nodes`] (which `find_by_*` and `select` build on) is a plain
13//! scan of the arena. An edit that can place a node out of arena order (an
14//! insertion or a move) sets a flag, and from then on `nodes()` follows the
15//! links from the first root instead — still allocation-free and linear.
16//! Removing, unwrapping, renaming and attribute edits keep the fast path.
17//!
18//! No edit recurses: subtree walks follow the links, so they are as safe on
19//! 100,000-deep trees as the parser is.
20
21use alloc::vec::Vec;
22
23use crate::builder;
24use crate::node::{AttrRange, Node, NodeId, RawKind, Span, SrcRange};
25use crate::tags;
26use crate::tree::Dom;
27
28/// What an edit needs to know about a node's kind.
29#[derive(Clone, Copy, PartialEq, Eq)]
30enum Shape {
31 /// An element that may have children.
32 Container,
33 /// A void element (`<br>`, `<img>`, …): never has children.
34 Void,
35 Text,
36 /// A comment or doctype.
37 Other,
38}
39
40impl Dom {
41 // --- helpers ---------------------------------------------------------
42
43 fn shape(&self, id: NodeId) -> Option<Shape> {
44 Some(match &self.node(id)?.raw {
45 RawKind::Element { name, .. } if tags::is_void(self.span_str(*name)) => Shape::Void,
46 RawKind::Element { .. } => Shape::Container,
47 RawKind::Text(_) => Shape::Text,
48 RawKind::Comment(_) | RawKind::Doctype(_) => Shape::Other,
49 })
50 }
51
52 /// Where `id` sits: `Some(Some(parent))`, `Some(None)` for a top-level
53 /// node, or `None` for a detached node with no parent (removed or created)
54 /// and for unknown ids.
55 fn container(&self, id: NodeId) -> Option<Option<NodeId>> {
56 let n = self.node(id)?;
57 match n.parent {
58 Some(p) => Some(Some(p)),
59 None if !n.detached => Some(None),
60 None => None,
61 }
62 }
63
64 /// Whether `node` is `id` itself or one of its ancestors.
65 fn is_inclusive_ancestor(&self, node: NodeId, id: NodeId) -> bool {
66 let mut cur = Some(id);
67 while let Some(c) = cur {
68 if c == node {
69 return true;
70 }
71 cur = self.node(c).and_then(|n| n.parent);
72 }
73 false
74 }
75
76 /// Flag `root` and its whole subtree as detached (or attached).
77 fn set_detached(&mut self, root: NodeId, detached: bool) {
78 let mut cur = root;
79 loop {
80 let Some(n) = self.node_mut(cur) else { return };
81 n.detached = detached;
82 if let Some(child) = n.first_child {
83 cur = child;
84 continue;
85 }
86 // Leaf: move to the next node of the subtree, climbing as needed.
87 loop {
88 if cur == root {
89 return;
90 }
91 let Some(n) = self.node(cur) else { return };
92 if let Some(sib) = n.next_sibling {
93 cur = sib;
94 break;
95 }
96 match n.parent {
97 Some(p) => cur = p,
98 None => return,
99 }
100 }
101 }
102 }
103
104 /// A new parentless, detached node.
105 fn alloc_detached(&mut self, raw: RawKind) -> NodeId {
106 let id = self.alloc(raw, SrcRange::NONE);
107 if let Some(n) = self.node_mut(id) {
108 n.detached = true;
109 }
110 id
111 }
112
113 /// Move `node` (with its subtree) into `parent` (`None`: the top level),
114 /// before its child `before` (`None`: at the end). Refuses — returning
115 /// `false` — when `parent` is inside `node`, which would make a cycle.
116 fn place(&mut self, node: NodeId, parent: Option<NodeId>, before: Option<NodeId>) -> bool {
117 if self.node(node).is_none() || before == Some(node) {
118 return false;
119 }
120 if parent.is_some_and(|p| self.is_inclusive_ancestor(node, p)) {
121 return false;
122 }
123 let detached = match parent {
124 Some(p) => self.node(p).is_none_or(|n| n.detached),
125 None => false,
126 };
127 self.unlink(node);
128 self.link(parent, before, node);
129 // Subtrees are uniformly flagged, so the root's flag speaks for all.
130 if self.node(node).is_some_and(|n| n.detached != detached) {
131 self.set_detached(node, detached);
132 }
133 self.reordered = true;
134 true
135 }
136
137 /// Parse `html` and insert its top-level nodes into `parent` before
138 /// `before`, in order.
139 ///
140 /// Not one `place` per root: that climbs `parent`'s ancestors for the
141 /// cycle check every time (roots × depth). The grafted roots are fresh
142 /// orphans, so they cannot contain `parent` and need no check, and their
143 /// subtrees are all flagged detached, so only an attached `parent` needs
144 /// them re-flagged.
145 fn insert_fragment(
146 &mut self,
147 parent: Option<NodeId>,
148 before: Option<NodeId>,
149 html: &str,
150 ) -> Vec<NodeId> {
151 let roots = self.graft(&builder::parse_document(html));
152 let attached = match parent {
153 Some(p) => self.node(p).is_some_and(|n| !n.detached),
154 None => true,
155 };
156 for &r in &roots {
157 self.link(parent, before, r);
158 if attached {
159 self.set_detached(r, false);
160 }
161 }
162 if !roots.is_empty() {
163 self.reordered = true;
164 }
165 roots
166 }
167
168 /// Copy every node of the freshly parsed `frag` into this arena —
169 /// detached, with no source range — and return the copies of its
170 /// top-level nodes. `frag` was only ever appended to, so its arenas hold
171 /// exactly its document: they are appended wholesale, and node ids, string
172 /// spans and attribute runs all shift by a constant.
173 fn graft(&mut self, frag: &Dom) -> Vec<NodeId> {
174 let base = self.nodes.len();
175 let shift = |id: Option<NodeId>| id.map(|i| NodeId::from_index(base + i.index()));
176 // `as u32` like `intern`: an arena past 4 GiB is out of reach anyway.
177 let str_base = self.strings.len() as u32;
178 let attr_base = self.attrs.len() as u32;
179 let span = |s: Span| Span {
180 start: s.start.wrapping_add(str_base),
181 len: s.len,
182 };
183 self.strings.push_str(&frag.strings);
184 self.attrs
185 .extend(frag.attrs.iter().map(|&(k, v)| (span(k), span(v))));
186 self.nodes.reserve(frag.nodes.len());
187 for node in &frag.nodes {
188 let raw = match node.raw {
189 RawKind::Element { name, attrs } => RawKind::Element {
190 name: span(name),
191 attrs: AttrRange {
192 start: attrs.start.wrapping_add(attr_base),
193 len: attrs.len,
194 },
195 },
196 RawKind::Text(s) => RawKind::Text(span(s)),
197 RawKind::Comment(s) => RawKind::Comment(span(s)),
198 RawKind::Doctype(s) => RawKind::Doctype(span(s)),
199 };
200 self.nodes.push(Node {
201 parent: shift(node.parent),
202 first_child: shift(node.first_child),
203 last_child: shift(node.last_child),
204 next_sibling: shift(node.next_sibling),
205 prev_sibling: shift(node.prev_sibling),
206 detached: true,
207 src: SrcRange::NONE,
208 raw,
209 });
210 }
211 let roots: Vec<NodeId> = frag.roots().filter_map(|r| shift(Some(r.id()))).collect();
212 // The copies are still linked to each other as top-level siblings;
213 // make each a clean orphan before it is placed.
214 for &r in &roots {
215 if let Some(n) = self.node_mut(r) {
216 n.prev_sibling = None;
217 n.next_sibling = None;
218 }
219 }
220 roots
221 }
222
223 // --- removing and attributes -----------------------------------------
224
225 /// Remove a node (and its subtree) from the document.
226 ///
227 /// The node stays valid — handles to it and its descendants still work and
228 /// can still be serialized, and it can be re-inserted with
229 /// [`Dom::append_child`] or [`Dom::insert_before`] — but it is no longer
230 /// reachable from the document: [`Dom::nodes`], `find_by_*`, `select`, and
231 /// every serializer skip it. Removing an already-removed node is a no-op.
232 ///
233 /// ```
234 /// let mut dom = domtree::parse("<p>Hi <script>track()</script>there</p>");
235 /// let script = dom.find_by_tag("script").next().unwrap().id();
236 /// dom.remove(script);
237 /// assert_eq!(dom.to_html(), "<p>Hi there</p>");
238 /// assert_eq!(dom.find_by_tag("script").count(), 0);
239 /// ```
240 pub fn remove(&mut self, id: NodeId) {
241 let Some(node) = self.node(id) else { return };
242 // Inside an already-removed subtree every descendant is flagged too, so
243 // only the links need fixing (keeps `-r div` on deep nesting linear).
244 let already_detached = node.detached;
245 self.unlink(id);
246 if !already_detached {
247 self.set_detached(id, true);
248 }
249 }
250
251 /// Set attribute `name` (ASCII-lower-cased) to `value` on an element,
252 /// replacing any existing value. Does nothing for non-element nodes, and
253 /// nothing if `name` is not a valid attribute name: empty, or containing
254 /// ASCII whitespace, `"`, `'`, `<`, `>`, `/`, `=` or NUL — names are
255 /// written out as they are, so these would change the markup around them.
256 ///
257 /// ```
258 /// let mut dom = domtree::parse("<a href=/x>x</a>");
259 /// let a = dom.find_by_tag("a").next().unwrap().id();
260 /// dom.set_attr(a, "href", "https://example.com/x");
261 /// dom.set_attr(a, "rel", "nofollow");
262 /// dom.set_attr(a, "x><script>alert(1)</script", "refused");
263 /// assert_eq!(dom.to_html(), r#"<a href="https://example.com/x" rel="nofollow">x</a>"#);
264 /// ```
265 pub fn set_attr(&mut self, id: NodeId, name: &str, value: &str) {
266 let Some(attrs) = self.attr_range(id) else {
267 return;
268 };
269 if !tags::is_valid_attr_name(name) {
270 return;
271 }
272 let name = name.to_ascii_lowercase();
273 let existing = self
274 .attr_slice(attrs)
275 .iter()
276 .position(|&(k, _)| self.span_str(k) == name);
277 let value = self.intern(value);
278 if let Some(i) = existing {
279 if let Some(slot) = self.attrs.get_mut(attrs.start as usize + i) {
280 slot.1 = value;
281 }
282 return;
283 }
284 let key = self.intern(&name);
285 // Appending needs the run to end where the arena does: move it there
286 // unless it already does (it always does for the element parsed or
287 // created last, and after the first move).
288 let start = if attrs.range().end == self.attrs.len() {
289 attrs.start
290 } else {
291 let start = self.attrs.len() as u32;
292 for i in attrs.range() {
293 if let Some(&pair) = self.attrs.get(i) {
294 self.attrs.push(pair);
295 }
296 }
297 self.attr_waste += attrs.len as usize;
298 start
299 };
300 self.attrs.push((key, value));
301 self.set_attr_range(
302 id,
303 AttrRange {
304 start,
305 len: attrs.len + 1,
306 },
307 );
308 self.maybe_compact_attrs();
309 }
310
311 /// Remove attribute `name` (ASCII case-insensitive) from an element, if
312 /// present.
313 ///
314 /// ```
315 /// let mut dom = domtree::parse(r#"<p style="color:red" class=x>hi</p>"#);
316 /// let p = dom.find_by_tag("p").next().unwrap().id();
317 /// dom.remove_attr(p, "style");
318 /// assert_eq!(dom.to_html(), r#"<p class="x">hi</p>"#);
319 /// ```
320 pub fn remove_attr(&mut self, id: NodeId, name: &str) {
321 let Some(attrs) = self.attr_range(id) else {
322 return;
323 };
324 // Pack the attributes that stay at the front of the run, in order.
325 let mut kept = 0;
326 for i in attrs.range() {
327 let Some(&(k, v)) = self.attrs.get(i) else {
328 break;
329 };
330 if self.span_str(k).eq_ignore_ascii_case(name) {
331 continue;
332 }
333 if let Some(slot) = self.attrs.get_mut(attrs.start as usize + kept) {
334 *slot = (k, v);
335 }
336 kept += 1;
337 }
338 let removed = attrs.len as usize - kept;
339 if removed == 0 {
340 return;
341 }
342 if attrs.range().end == self.attrs.len() {
343 self.attrs.truncate(attrs.start as usize + kept);
344 } else {
345 self.attr_waste += removed;
346 }
347 self.set_attr_range(
348 id,
349 AttrRange {
350 start: attrs.start,
351 len: kept as u32,
352 },
353 );
354 self.maybe_compact_attrs();
355 }
356
357 /// The attribute run of element `id` (`None` for other nodes).
358 fn attr_range(&self, id: NodeId) -> Option<AttrRange> {
359 match self.node(id)?.raw {
360 RawKind::Element { attrs, .. } => Some(attrs),
361 _ => None,
362 }
363 }
364
365 fn set_attr_range(&mut self, id: NodeId, range: AttrRange) {
366 if let Some(Node {
367 raw: RawKind::Element { attrs, .. },
368 ..
369 }) = self.node_mut(id)
370 {
371 *attrs = range;
372 }
373 }
374
375 /// Rebuild the attribute arena without the slots edits left behind, once
376 /// they outnumber the live ones plus the nodes. That bounds the arena at
377 /// about twice what the document needs however edits move runs around,
378 /// and the rebuild (linear in nodes + slots) is paid for by the moves that
379 /// made the waste.
380 fn maybe_compact_attrs(&mut self) {
381 let live = self.attrs.len() - self.attr_waste;
382 if self.attr_waste <= live + self.nodes.len() {
383 return;
384 }
385 let mut packed = Vec::with_capacity(live);
386 for node in &mut self.nodes {
387 if let RawKind::Element { attrs, .. } = &mut node.raw {
388 let start = packed.len();
389 packed.extend_from_slice(self.attrs.get(attrs.range()).unwrap_or(&[]));
390 *attrs = AttrRange {
391 start: start as u32,
392 len: (packed.len() - start) as u32,
393 };
394 }
395 }
396 self.attrs = packed;
397 self.attr_waste = 0;
398 }
399
400 // --- text and names --------------------------------------------------
401
402 /// Replace a node's text.
403 ///
404 /// On a text node, replaces its content. On an element, removes all of
405 /// its children and puts `text` in a single new text node (none if `text`
406 /// is empty) — the DOM's `textContent = text`. Does nothing on void
407 /// elements, comments and doctypes.
408 ///
409 /// `text` is literal, never parsed (use [`Dom::append_html`] for markup),
410 /// and [`Dom::to_html`] escapes `&`, `<` and `>` in it — except inside
411 /// `<script>` and `<style>`, whose content HTML has no escapes for. There
412 /// it is written as is, apart from any `</script` (or `</style`) that
413 /// would end the element early, which is written `<\/script` — the same
414 /// string inside a JavaScript or CSS string literal — so the output still
415 /// parses back to a single element.
416 ///
417 /// ```
418 /// let mut dom = domtree::parse("<h1>Old <em>title</em></h1><p>x</p>");
419 /// let h1 = dom.find_by_tag("h1").next().unwrap().id();
420 /// dom.set_text(h1, "New & <improved>");
421 /// assert_eq!(dom.to_html(), "<h1>New & <improved></h1><p>x</p>");
422 ///
423 /// let x = dom.find_by_tag("p").next().unwrap().first_child().unwrap().id();
424 /// dom.set_text(x, "y");
425 /// assert_eq!(dom.to_html(), "<h1>New & <improved></h1><p>y</p>");
426 ///
427 /// let mut dom = domtree::parse("<script>old()</script>");
428 /// let script = dom.root().unwrap().id();
429 /// dom.set_text(script, r#"if (a < b) log("</script>")"#);
430 /// assert_eq!(dom.to_html(), r#"<script>if (a < b) log("<\/script>")</script>"#);
431 /// ```
432 pub fn set_text(&mut self, id: NodeId, text: &str) {
433 match self.shape(id) {
434 Some(Shape::Text) => {
435 let span = self.intern(text);
436 if let Some(n) = self.node_mut(id) {
437 n.raw = RawKind::Text(span);
438 }
439 }
440 Some(Shape::Container) => {
441 while let Some(child) = self.node(id).and_then(|n| n.first_child) {
442 self.remove(child);
443 }
444 if !text.is_empty() {
445 let t = self.create_text(text);
446 self.place(t, Some(id), None);
447 }
448 }
449 _ => {}
450 }
451 }
452
453 /// Change an element's tag name (ASCII-lower-cased). Attributes and
454 /// children are kept. Does nothing for non-elements, and nothing if `tag`
455 /// is not a valid tag name: one that does not start with an ASCII letter,
456 /// or contains ASCII whitespace, `"`, `'`, `<`, `>`, `/`, `=` or NUL.
457 ///
458 /// Renaming to a void element (`br`, `img`, …) keeps any children in the
459 /// tree, but [`Dom::to_html`] — like the HTML syntax — cannot express them.
460 ///
461 /// ```
462 /// let mut dom = domtree::parse("<b class=x>bold</b>");
463 /// let b = dom.root().unwrap().id();
464 /// dom.rename(b, "STRONG");
465 /// dom.rename(b, "b><img src=x onerror=alert(1)"); // refused
466 /// assert_eq!(dom.to_html(), r#"<strong class="x">bold</strong>"#);
467 /// ```
468 pub fn rename(&mut self, id: NodeId, tag: &str) {
469 if !matches!(self.shape(id), Some(Shape::Container | Shape::Void))
470 || !tags::is_valid_tag_name(tag)
471 {
472 return;
473 }
474 let span = self.intern_lower(tag);
475 if let Some(Node {
476 raw: RawKind::Element { name, .. },
477 ..
478 }) = self.node_mut(id)
479 {
480 *name = span;
481 }
482 }
483
484 // --- creating and moving ---------------------------------------------
485
486 /// Create a new, empty element (tag ASCII-lower-cased).
487 ///
488 /// It starts *detached*: not part of the document until you attach it
489 /// with [`Dom::append_child`] or [`Dom::insert_before`]. Created nodes have
490 /// no [`source_range`](crate::NodeRef::source_range).
491 ///
492 /// Returns `None` (and creates nothing) if `tag` is not a valid tag name
493 /// (see [`Dom::rename`]).
494 ///
495 /// ```
496 /// let mut dom = domtree::parse("<ul><li>a</li></ul>");
497 /// let ul = dom.root().unwrap().id();
498 /// let li = dom.create_element("li").unwrap();
499 /// let text = dom.create_text("b");
500 /// dom.append_child(li, text);
501 /// dom.set_attr(li, "class", "new");
502 /// dom.append_child(ul, li);
503 /// assert_eq!(dom.to_html(), r#"<ul><li>a</li><li class="new">b</li></ul>"#);
504 ///
505 /// assert!(dom.create_element("li><img").is_none());
506 /// ```
507 pub fn create_element(&mut self, tag: &str) -> Option<NodeId> {
508 self.new_element(tag)
509 }
510
511 /// A new detached element, or `None` if `tag` is not a valid tag name.
512 fn new_element(&mut self, tag: &str) -> Option<NodeId> {
513 if !tags::is_valid_tag_name(tag) {
514 return None;
515 }
516 let name = self.intern_lower(tag);
517 // An empty run at the end of the arena: the first `set_attr` appends
518 // in place.
519 let attrs = AttrRange {
520 start: self.attrs.len() as u32,
521 len: 0,
522 };
523 Some(self.alloc_detached(RawKind::Element { name, attrs }))
524 }
525
526 /// Create a new, detached text node holding `text` literally (see
527 /// [`Dom::create_element`] for how detached nodes behave).
528 ///
529 /// ```
530 /// let mut dom = domtree::parse("<p></p>");
531 /// let p = dom.root().unwrap().id();
532 /// let t = dom.create_text("1 < 2");
533 /// dom.append_child(p, t);
534 /// assert_eq!(dom.to_html(), "<p>1 < 2</p>");
535 /// ```
536 pub fn create_text(&mut self, text: &str) -> NodeId {
537 let span = self.intern(text);
538 self.alloc_detached(RawKind::Text(span))
539 }
540
541 /// Move `child` (with its subtree) to the end of `parent`'s children.
542 ///
543 /// `child` may be a created node, a removed one, or a node elsewhere in the
544 /// document (it is detached from its old place first). If `parent` is
545 /// itself detached, `child` ends up detached with it. Returns `false` and
546 /// changes nothing if `parent` cannot have children (it is not an element,
547 /// or is a void element) or if `parent` is `child` or inside it — moving a
548 /// node into its own subtree would make a cycle.
549 ///
550 /// ```
551 /// let mut dom = domtree::parse("<div id=a><b>x</b></div><div id=c></div>");
552 /// let b = dom.find_by_tag("b").next().unwrap().id();
553 /// let c = dom.find_by_id("c").unwrap().id();
554 /// assert!(dom.append_child(c, b)); // moves <b> from #a to #c
555 /// assert_eq!(dom.to_html(), r#"<div id="a"></div><div id="c"><b>x</b></div>"#);
556 ///
557 /// assert!(!dom.append_child(b, c)); // #c contains <b>: refused, no cycle
558 /// let x = dom.get(b).unwrap().first_child().unwrap().id();
559 /// assert!(!dom.append_child(x, c)); // text nodes have no children
560 /// ```
561 pub fn append_child(&mut self, parent: NodeId, child: NodeId) -> bool {
562 self.shape(parent) == Some(Shape::Container) && self.place(child, Some(parent), None)
563 }
564
565 /// Move `node` (with its subtree) to just before `reference`, as its
566 /// sibling — at the top level if `reference` is a top-level node.
567 ///
568 /// Like [`Dom::append_child`], `node` may come from anywhere. Returns
569 /// `false` and changes nothing if `reference` has no place to insert at (it
570 /// is detached with no parent), if `node` is `reference`, or if `node`
571 /// contains `reference` (a cycle).
572 ///
573 /// ```
574 /// let mut dom = domtree::parse("<li>b</li><li>c</li><li>a</li>");
575 /// let items: Vec<_> = dom.find_by_tag("li").map(|n| n.id()).collect();
576 /// assert!(dom.insert_before(items[0], items[2]));
577 /// assert_eq!(dom.to_html(), "<li>a</li><li>b</li><li>c</li>");
578 /// ```
579 pub fn insert_before(&mut self, reference: NodeId, node: NodeId) -> bool {
580 if node == reference {
581 return false;
582 }
583 match self.container(reference) {
584 Some(parent) => self.place(node, parent, Some(reference)),
585 None => false,
586 }
587 }
588
589 /// Wrap a node in a new `tag` element, which takes the node's place.
590 /// Returns the wrapper's id, or `None` (changing nothing) if `id` is
591 /// unknown, or `tag` is a void element or not a valid tag name (see
592 /// [`Dom::rename`]). Wrapping a detached node gives a detached wrapper.
593 ///
594 /// ```
595 /// let mut dom = domtree::parse("<p>Hello <b>world</b></p>");
596 /// let b = dom.find_by_tag("b").next().unwrap().id();
597 /// let a = dom.wrap(b, "a").unwrap();
598 /// dom.set_attr(a, "href", "/world");
599 /// assert_eq!(dom.to_html(), r#"<p>Hello <a href="/world"><b>world</b></a></p>"#);
600 /// assert!(dom.wrap(b, "").is_none());
601 /// ```
602 pub fn wrap(&mut self, id: NodeId, tag: &str) -> Option<NodeId> {
603 self.node(id)?;
604 if tags::is_void(&tag.to_ascii_lowercase()) {
605 return None;
606 }
607 let wrapper = self.new_element(tag)?;
608 if let Some(parent) = self.container(id) {
609 self.place(wrapper, parent, Some(id));
610 }
611 self.place(id, Some(wrapper), None);
612 Some(wrapper)
613 }
614
615 /// Replace an element by its children: they move up into its place, in
616 /// order, and the element itself is removed (detached, now childless).
617 /// Returns `false` and changes nothing for non-elements and for detached
618 /// elements with no parent (there is no place to move the children to).
619 ///
620 /// ```
621 /// let mut dom = domtree::parse(r#"<p>a <span class="x">b <i>c</i></span> d</p>"#);
622 /// let span = dom.find_by_tag("span").next().unwrap().id();
623 /// assert!(dom.unwrap(span));
624 /// assert_eq!(dom.to_html(), "<p>a b <i>c</i> d</p>");
625 /// ```
626 pub fn unwrap(&mut self, id: NodeId) -> bool {
627 if !matches!(self.shape(id), Some(Shape::Container | Shape::Void)) {
628 return false;
629 }
630 let Some(parent) = self.container(id) else {
631 return false;
632 };
633 // The children keep their relative order, so if arena order was
634 // document order it still is: `reordered` is left alone.
635 while let Some(child) = self.node(id).and_then(|n| n.first_child) {
636 self.unlink(child);
637 self.link(parent, Some(id), child);
638 }
639 self.remove(id);
640 true
641 }
642
643 // --- parsed fragments ------------------------------------------------
644
645 /// Parse `html` as a fragment and append its nodes to the end of
646 /// `parent`'s children. Returns the ids of the inserted top-level nodes,
647 /// in order — empty if `html` produced no nodes or `parent` cannot have
648 /// children (not an element, or a void element).
649 ///
650 /// The fragment is parsed on its own, with the normal parser, not in the
651 /// context of `parent` (so `<li>` closes `<li>` inside it, but text
652 /// appended to a `<script>` is still parsed as HTML). Its parse errors are
653 /// **not** added to [`Dom::errors`] — those positions refer to the original
654 /// input; check [`crate::parse_fragment`]`(html).errors()` if you need
655 /// them. Inserted nodes have no
656 /// [`source_range`](crate::NodeRef::source_range).
657 ///
658 /// ```
659 /// let mut dom = domtree::parse("<ul><li>a</li></ul>");
660 /// let ul = dom.root().unwrap().id();
661 /// let ids = dom.append_html(ul, "<li>b<li>c");
662 /// assert_eq!(ids.len(), 2);
663 /// assert_eq!(dom.to_html(), "<ul><li>a</li><li>b</li><li>c</li></ul>");
664 /// ```
665 pub fn append_html(&mut self, parent: NodeId, html: &str) -> Vec<NodeId> {
666 if self.shape(parent) != Some(Shape::Container) {
667 return Vec::new();
668 }
669 self.insert_fragment(Some(parent), None, html)
670 }
671
672 /// Parse `html` and insert its nodes at the start of `parent`'s children.
673 /// See [`Dom::append_html`] for how the fragment is parsed and what is
674 /// returned.
675 ///
676 /// ```
677 /// let mut dom = domtree::parse("<p>world</p>");
678 /// let p = dom.root().unwrap().id();
679 /// dom.prepend_html(p, "<b>Hello</b>, ");
680 /// assert_eq!(dom.to_html(), "<p><b>Hello</b>, world</p>");
681 /// ```
682 pub fn prepend_html(&mut self, parent: NodeId, html: &str) -> Vec<NodeId> {
683 if self.shape(parent) != Some(Shape::Container) {
684 return Vec::new();
685 }
686 let first = self.node(parent).and_then(|n| n.first_child);
687 self.insert_fragment(Some(parent), first, html)
688 }
689
690 /// Parse `html` and insert its nodes just before `id`, as its siblings (at
691 /// the top level if `id` is a top-level node). Returns nothing inserted if
692 /// `id` is detached with no parent. See [`Dom::append_html`] for how the
693 /// fragment is parsed.
694 ///
695 /// ```
696 /// let mut dom = domtree::parse("<h1>Title</h1><p>Body</p>");
697 /// let h1 = dom.root().unwrap().id();
698 /// dom.insert_html_before(h1, "<!doctype html>");
699 /// assert_eq!(dom.to_html(), "<!doctype html><h1>Title</h1><p>Body</p>");
700 /// ```
701 pub fn insert_html_before(&mut self, id: NodeId, html: &str) -> Vec<NodeId> {
702 match self.container(id) {
703 Some(parent) => self.insert_fragment(parent, Some(id), html),
704 None => Vec::new(),
705 }
706 }
707
708 /// Parse `html` and insert its nodes just after `id`, as its siblings. See
709 /// [`Dom::insert_html_before`].
710 ///
711 /// ```
712 /// let mut dom = domtree::parse("<p>See <a href=/docs>docs</a>.</p>");
713 /// let a = dom.find_by_tag("a").next().unwrap().id();
714 /// dom.insert_html_after(a, " <sup>[1]</sup>");
715 /// assert_eq!(dom.to_html(), r#"<p>See <a href="/docs">docs</a> <sup>[1]</sup>.</p>"#);
716 /// ```
717 pub fn insert_html_after(&mut self, id: NodeId, html: &str) -> Vec<NodeId> {
718 match self.container(id) {
719 Some(parent) => {
720 let next = self.node(id).and_then(|n| n.next_sibling);
721 self.insert_fragment(parent, next, html)
722 }
723 None => Vec::new(),
724 }
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use alloc::format;
731 use alloc::string::ToString;
732 use alloc::vec::Vec;
733
734 /// Appending to two elements in turn moves one run to the end of the arena
735 /// on every call. Without compaction that would be quadratic (~250k
736 /// slots here); with it the arena stays within about twice what is live.
737 #[test]
738 fn moved_attribute_runs_are_compacted() {
739 let mut dom = crate::parse("<a x=1></a><b y=2 z=3></b><i></i>");
740 let ids: Vec<_> = dom.nodes().map(|n| n.id()).collect();
741 let (a, b) = (ids[0], ids[1]);
742 for i in 0..500 {
743 dom.set_attr(a, &format!("a{i}"), "v");
744 dom.set_attr(b, &format!("b{i}"), &i.to_string());
745 let live = 3 + 2 * (i + 1);
746 assert!(dom.attrs.len() <= 2 * (live + dom.nodes.len()) + 2);
747 }
748 let a_attrs: Vec<_> = dom.get(a).unwrap().attributes().collect();
749 assert_eq!(a_attrs.len(), 501);
750 assert_eq!(a_attrs[0], ("x", "1"));
751 assert_eq!(a_attrs[500], ("a499", "v"));
752 let b = dom.get(b).unwrap();
753 assert_eq!(b.attributes().count(), 502);
754 assert_eq!(b.attr("z"), Some("3"));
755 assert_eq!(b.attr("b250"), Some("250"));
756 assert_eq!(dom.get(ids[2]).unwrap().attributes().count(), 0);
757
758 // Removing from a run that is not at the end leaves waste behind,
759 // and compaction keeps that bounded too.
760 for i in 0..500 {
761 dom.remove_attr(a, &format!("a{i}"));
762 }
763 assert_eq!(
764 dom.get(a).unwrap().attributes().collect::<Vec<_>>(),
765 [("x", "1")]
766 );
767 assert!(dom.attrs.len() <= 2 * (504 + dom.nodes.len()) + 2);
768 assert_eq!(dom.get(ids[1]).unwrap().attr("b499"), Some("499"));
769 }
770}