Skip to main content

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 &amp; &lt;improved&gt;</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 &amp; &lt;improved&gt;</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 &lt; 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}