Skip to main content

blitz_dom_api/
node.rs

1//! Node operations: tree structure, tree mutation and text content.
2//!
3//! Upstream: `blitz-script/src/dom/node.rs`. See MAPPING.md.
4
5use std::fmt;
6
7use blitz_dom::node::NodeData;
8use blitz_dom::{BaseDocument, NodeId};
9
10use crate::Result;
11use crate::error::DomError;
12
13/// The bit flags `compare_document_position` returns.
14pub mod document_position {
15    /// The two nodes are in different trees.
16    pub const DISCONNECTED: u16 = 0x01;
17    /// The other node precedes this one.
18    pub const PRECEDING: u16 = 0x02;
19    /// The other node follows this one.
20    pub const FOLLOWING: u16 = 0x04;
21    /// The other node is an ancestor of this one.
22    pub const CONTAINS: u16 = 0x08;
23    /// The other node is a descendant of this one.
24    pub const CONTAINED_BY: u16 = 0x10;
25    /// The relative order of two disconnected nodes is arbitrary but stable.
26    pub const IMPLEMENTATION_SPECIFIC: u16 = 0x20;
27}
28
29// === Read-only tree structure ===
30
31/// `node.parentNode` (and `parentElement`, which upstream aliases to it).
32pub fn parent_node(doc: &BaseDocument, node: NodeId) -> Result<Option<NodeId>> {
33    Ok(doc.get_node(node).and_then(|node| node.parent))
34}
35
36/// `node.childNodes`, as a snapshot rather than a live list.
37///
38/// A live `NodeList` would have to hold the document, which the borrow
39/// discipline forbids. Every caller upstream materialises the list anyway.
40pub fn child_nodes(doc: &BaseDocument, node: NodeId) -> Result<Vec<NodeId>> {
41    Ok(doc
42        .get_node(node)
43        .map(|node| node.children.to_vec())
44        .unwrap_or_default())
45}
46
47/// `node.firstChild`.
48pub fn first_child(doc: &BaseDocument, node: NodeId) -> Result<Option<NodeId>> {
49    Ok(doc
50        .get_node(node)
51        .and_then(|node| node.children.first().copied()))
52}
53
54/// `node.previousSibling`.
55pub fn previous_sibling(doc: &BaseDocument, node: NodeId) -> Result<Option<NodeId>> {
56    Ok(sibling(doc, node, -1))
57}
58
59/// `node.nextSibling`.
60pub fn next_sibling(doc: &BaseDocument, node: NodeId) -> Result<Option<NodeId>> {
61    Ok(sibling(doc, node, 1))
62}
63
64fn sibling(doc: &BaseDocument, node_id: NodeId, offset: isize) -> Option<NodeId> {
65    let node = doc.get_node(node_id)?;
66    let parent = doc.get_node(node.parent?)?;
67    let index = parent.index_of_child(node_id)?;
68    let sibling_index = index.checked_add_signed(offset)?;
69    parent.children.get(sibling_index).copied()
70}
71
72/// `node.hasChildNodes()`.
73pub fn has_child_nodes(doc: &BaseDocument, node: NodeId) -> Result<bool> {
74    Ok(doc
75        .get_node(node)
76        .is_some_and(|node| !node.children.is_empty()))
77}
78
79/// `node.contains(other)`.
80///
81/// True when `other` is `node` or a descendant of it, matching the DOM's
82/// inclusive definition.
83pub fn contains(doc: &BaseDocument, node: NodeId, other: NodeId) -> Result<bool> {
84    let mut current = other;
85    loop {
86        if current == node {
87            return Ok(true);
88        }
89        match doc.get_node(current).and_then(|node| node.parent) {
90            Some(parent_id) => current = parent_id,
91            None => return Ok(false),
92        }
93    }
94}
95
96/// `node.compareDocumentPosition(other)`.
97///
98/// Deliberate difference: upstream asserts the common-ancestor invariants with
99/// `expect` and panics on a malformed tree. Here they are
100/// [`DomError::TreeInvariant`], because a facade must not take the process
101/// down over document state.
102pub fn compare_document_position(doc: &BaseDocument, node: NodeId, other: NodeId) -> Result<u16> {
103    use document_position::*;
104
105    if node == other {
106        return Ok(0);
107    }
108
109    let path = |mut id: NodeId| {
110        let mut path = Vec::new();
111        loop {
112            path.push(id);
113            let Some(parent) = doc.get_node(id).and_then(|node| node.parent) else {
114                break;
115            };
116            id = parent;
117        }
118        path.reverse();
119        path
120    };
121    let node_path = path(node);
122    let other_path = path(other);
123    if node_path.first() != other_path.first() {
124        let order = if node < other { FOLLOWING } else { PRECEDING };
125        return Ok(DISCONNECTED | IMPLEMENTATION_SPECIFIC | order);
126    }
127
128    let common = node_path
129        .iter()
130        .zip(&other_path)
131        .take_while(|(left, right)| left == right)
132        .count();
133    if common == node_path.len() {
134        return Ok(FOLLOWING | CONTAINED_BY);
135    }
136    if common == other_path.len() {
137        return Ok(PRECEDING | CONTAINS);
138    }
139    let parent_id = node_path[common - 1];
140    let parent = doc
141        .get_node(parent_id)
142        .ok_or(DomError::TreeInvariant("common ancestor is missing"))?;
143    let node_index = parent
144        .children
145        .iter()
146        .position(|child| *child == node_path[common])
147        .ok_or(DomError::TreeInvariant("node missing from common ancestor"))?;
148    let other_index = parent
149        .children
150        .iter()
151        .position(|child| *child == other_path[common])
152        .ok_or(DomError::TreeInvariant(
153            "other node missing from common ancestor",
154        ))?;
155    Ok(if node_index < other_index {
156        FOLLOWING
157    } else {
158        PRECEDING
159    })
160}
161
162// === Text content ===
163
164/// `node.textContent`, concatenated over the subtree.
165///
166/// Owned, per the crate's borrow discipline. [`text_content_into`] is the
167/// variant that writes into a caller's buffer instead.
168pub fn text_content(doc: &BaseDocument, node: NodeId) -> Result<String> {
169    Ok(doc
170        .get_node(node)
171        .map(|node| node.text_content())
172        .unwrap_or_default())
173}
174
175/// A [`fmt::Write`] sink that only measures.
176struct Counting(usize);
177
178impl fmt::Write for Counting {
179    fn write_str(&mut self, s: &str) -> fmt::Result {
180        self.0 += s.len();
181        Ok(())
182    }
183}
184
185/// A [`fmt::Write`] sink that fills a caller's slice.
186///
187/// Only ever handed a slice already known to be large enough, so the debug
188/// assertion below is a check on this crate rather than on its caller.
189struct Filling<'out> {
190    out: &'out mut [u8],
191    at: usize,
192}
193
194impl fmt::Write for Filling<'_> {
195    fn write_str(&mut self, s: &str) -> fmt::Result {
196        let end = self.at + s.len();
197        debug_assert!(end <= self.out.len(), "measured length was short");
198        self.out[self.at..end].copy_from_slice(s.as_bytes());
199        self.at = end;
200        Ok(())
201    }
202}
203
204/// The byte length of `node.textContent`, without building it.
205///
206/// One traversal, no allocation. Useful on its own for sizing a buffer, and it
207/// is the first half of [`text_content_into`].
208pub fn text_content_len(doc: &BaseDocument, node: NodeId) -> Result<usize> {
209    let mut counting = Counting(0);
210    if let Some(node) = doc.get_node(node) {
211        node.write_text_content(&mut counting);
212    }
213    Ok(counting.0)
214}
215
216/// `node.textContent`, written into a caller-supplied buffer.
217///
218/// Returns the **full** byte length, always. The bytes were written to
219/// `out[..len]` if and only if `len <= out.len()`; a value that does not fit
220/// leaves `out` untouched rather than truncated. Same contract as
221/// [`element::get_attribute_into`](crate::element::get_attribute_into).
222///
223/// # What this trades, which is not what the attribute reader trades
224///
225/// An attribute's value exists in the document as contiguous bytes, so reading
226/// it into a buffer is one `memcpy` and the owning reader's `String` was pure
227/// overhead. **`textContent` has no such bytes.** It is a concatenation over a
228/// subtree, so it does not exist anywhere until something builds it, and the
229/// only question is *where*.
230///
231/// So this does not remove a copy, it removes an **allocation**: the bytes land
232/// in the caller's buffer instead of in a `String` that is then copied there.
233/// The price is a second traversal — one pass to measure, one to fill — because
234/// the "nothing is written unless it all fits" guarantee cannot be honoured by
235/// a single streaming pass that discovers the overflow halfway through.
236///
237/// Whether one allocation is worth one extra pointer-chasing walk of a subtree
238/// is a timing question, and this crate deliberately measures bytes rather than
239/// time. What is *not* a timing question: the allocation is gone, and a caller
240/// that already knows the length can skip the measuring pass by calling
241/// [`text_content_len`] itself.
242pub fn text_content_into(doc: &BaseDocument, node: NodeId, out: &mut [u8]) -> Result<usize> {
243    let len = text_content_len(doc, node)?;
244    if len > out.len() {
245        return Ok(len);
246    }
247    if let Some(node) = doc.get_node(node) {
248        let mut filling = Filling { out, at: 0 };
249        node.write_text_content(&mut filling);
250    }
251    Ok(len)
252}
253
254/// `node.textContent = text`.
255///
256/// On a text or comment node this rewrites the node's own text. On anything
257/// else it replaces the children with a single text node, or with nothing when
258/// the text is empty.
259///
260/// Existing children are *detached*, not dropped, so that wrappers a runtime
261/// still holds for them stay valid.
262pub fn set_text_content(doc: &mut BaseDocument, node: NodeId, text: &str) -> Result<()> {
263    let is_text_like = matches!(
264        doc.get_node(node).map(|node| &node.data),
265        Some(NodeData::Text(_)) | Some(NodeData::Comment { .. })
266    );
267
268    let mut mutr = doc.mutate();
269    if is_text_like {
270        mutr.set_node_text(node, text);
271    } else {
272        for child_id in mutr.child_ids(node) {
273            mutr.remove_node(child_id);
274        }
275        if !text.is_empty() {
276            let text_id = mutr.create_text_node(text);
277            mutr.append_children(node, &[text_id]);
278        }
279    }
280    Ok(())
281}
282
283// === Tree mutation ===
284
285/// `parent.appendChild(child)`, returning the child.
286///
287/// The child is detached from any current parent first, which is what makes
288/// "move to the end of the same parent" behave.
289///
290/// A runtime that supports custom elements must run its upgrade step after
291/// this call: insertion is when `connectedCallback` is due, and constructing a
292/// guest object is not something this crate can do.
293pub fn append_child(doc: &mut BaseDocument, parent: NodeId, child: NodeId) -> Result<NodeId> {
294    let mut mutr = doc.mutate();
295    if mutr.node_has_parent(child) {
296        mutr.remove_node(child);
297    }
298    mutr.append_children(parent, &[child]);
299    Ok(child)
300}
301
302/// `parent.insertBefore(new_node, reference)`, returning the inserted node.
303///
304/// `reference` of `None` is the DOM's null reference node and means append.
305/// Inserting a node before itself is a no-op.
306pub fn insert_before(
307    doc: &mut BaseDocument,
308    parent: NodeId,
309    new_node: NodeId,
310    reference: Option<NodeId>,
311) -> Result<NodeId> {
312    if reference == Some(new_node) {
313        return Ok(new_node);
314    }
315
316    let mut mutr = doc.mutate();
317    if mutr.node_has_parent(new_node) {
318        mutr.remove_node(new_node);
319    }
320    match reference {
321        Some(reference) if mutr.node_has_parent(reference) => {
322            mutr.insert_nodes_before(reference, &[new_node]);
323        }
324        _ => mutr.append_children(parent, &[new_node]),
325    }
326    Ok(new_node)
327}
328
329/// `parent.removeChild(child)`, returning the child.
330///
331/// Copied behaviour worth knowing: `parent` is not checked. Upstream removes
332/// the child from wherever it actually is rather than raising the DOM's
333/// `NotFoundError`, and the node is detached rather than dropped so that
334/// wrappers stay valid.
335pub fn remove_child(doc: &mut BaseDocument, parent: NodeId, child: NodeId) -> Result<NodeId> {
336    let _ = parent;
337    doc.mutate().remove_node(child);
338    Ok(child)
339}
340
341/// `parent.replaceChild(new_node, old_node)`, returning the *replaced* node.
342///
343/// Argument order follows the DOM method: new first, old second. The return is
344/// `old_node`, which is what the DOM specifies and what upstream returns.
345pub fn replace_child(
346    doc: &mut BaseDocument,
347    parent: NodeId,
348    new_node: NodeId,
349    old_node: NodeId,
350) -> Result<NodeId> {
351    let _ = parent;
352    if new_node != old_node {
353        let mut mutr = doc.mutate();
354        if mutr.node_has_parent(new_node) {
355            mutr.remove_node(new_node);
356        }
357        mutr.insert_nodes_before(old_node, &[new_node]);
358        mutr.remove_node(old_node);
359    }
360    Ok(old_node)
361}
362
363/// `node.cloneNode(deep)`.
364///
365/// A shallow clone copies an element's name and attributes, a text node's
366/// content, or a comment's contents. Anything else clones to an empty comment,
367/// which is upstream's placeholder for a node kind it cannot reproduce.
368pub fn clone_node(doc: &mut BaseDocument, node: NodeId, deep: bool) -> Result<NodeId> {
369    enum CloneSrc {
370        Element(blitz_dom::QualName, Vec<blitz_dom::Attribute>),
371        Text(String),
372        Comment(String),
373        Other,
374    }
375
376    if deep {
377        return Ok(doc.mutate().deep_clone_node(node));
378    }
379
380    let src = match doc.get_node(node).map(|node| &node.data) {
381        Some(NodeData::Element(data)) => {
382            CloneSrc::Element(data.name.clone(), data.attrs().to_vec())
383        }
384        Some(NodeData::Text(data)) => CloneSrc::Text(data.content.clone()),
385        Some(NodeData::Comment { contents }) => CloneSrc::Comment(contents.clone()),
386        _ => CloneSrc::Other,
387    };
388    let mut mutr = doc.mutate();
389    Ok(match src {
390        CloneSrc::Element(name, attrs) => mutr.create_element(name, attrs),
391        CloneSrc::Text(content) => mutr.create_text_node(&content),
392        CloneSrc::Comment(contents) => mutr.create_comment_node(&contents),
393        CloneSrc::Other => mutr.create_comment_node(""),
394    })
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400    use crate::document;
401    use crate::element;
402    use crate::test_support::skeleton;
403
404    fn div(doc: &mut BaseDocument) -> NodeId {
405        document::create_element(doc, "div").unwrap()
406    }
407
408    #[test]
409    fn append_child_attaches_and_returns_the_child() {
410        let (mut doc, _html, _head, body) = skeleton();
411        let child = div(&mut doc);
412        assert_eq!(append_child(&mut doc, body, child).unwrap(), child);
413        assert_eq!(parent_node(&doc, child).unwrap(), Some(body));
414    }
415
416    /// The detach-first rule: appending an attached node moves it rather than
417    /// giving it a second parent.
418    #[test]
419    fn append_child_moves_an_already_attached_node() {
420        let (mut doc, _html, _head, body) = skeleton();
421        let first = div(&mut doc);
422        let second = div(&mut doc);
423        let child = div(&mut doc);
424        append_child(&mut doc, body, first).unwrap();
425        append_child(&mut doc, body, second).unwrap();
426        append_child(&mut doc, first, child).unwrap();
427        append_child(&mut doc, second, child).unwrap();
428
429        assert_eq!(parent_node(&doc, child).unwrap(), Some(second));
430        assert!(child_nodes(&doc, first).unwrap().is_empty());
431    }
432
433    #[test]
434    fn insert_before_places_the_node_ahead_of_the_reference() {
435        let (mut doc, _html, _head, body) = skeleton();
436        let first = div(&mut doc);
437        let second = div(&mut doc);
438        append_child(&mut doc, body, first).unwrap();
439        insert_before(&mut doc, body, second, Some(first)).unwrap();
440        assert_eq!(child_nodes(&doc, body).unwrap(), vec![second, first]);
441    }
442
443    #[test]
444    fn insert_before_with_no_reference_appends() {
445        let (mut doc, _html, _head, body) = skeleton();
446        let first = div(&mut doc);
447        let second = div(&mut doc);
448        append_child(&mut doc, body, first).unwrap();
449        insert_before(&mut doc, body, second, None).unwrap();
450        assert_eq!(child_nodes(&doc, body).unwrap(), vec![first, second]);
451    }
452
453    #[test]
454    fn insert_before_itself_is_a_no_op() {
455        let (mut doc, _html, _head, body) = skeleton();
456        let only = div(&mut doc);
457        append_child(&mut doc, body, only).unwrap();
458        insert_before(&mut doc, body, only, Some(only)).unwrap();
459        assert_eq!(child_nodes(&doc, body).unwrap(), vec![only]);
460    }
461
462    #[test]
463    fn remove_child_detaches_and_returns_the_child() {
464        let (mut doc, _html, _head, body) = skeleton();
465        let child = div(&mut doc);
466        append_child(&mut doc, body, child).unwrap();
467        assert_eq!(remove_child(&mut doc, body, child).unwrap(), child);
468        assert_eq!(parent_node(&doc, child).unwrap(), None);
469        // Detached, not dropped: the node is still addressable.
470        assert!(doc.get_node(child).is_some());
471    }
472
473    #[test]
474    fn replace_child_swaps_in_place_and_returns_the_old_node() {
475        let (mut doc, _html, _head, body) = skeleton();
476        let before = div(&mut doc);
477        let old = div(&mut doc);
478        let after = div(&mut doc);
479        let new = div(&mut doc);
480        for id in [before, old, after] {
481            append_child(&mut doc, body, id).unwrap();
482        }
483        assert_eq!(replace_child(&mut doc, body, new, old).unwrap(), old);
484        assert_eq!(child_nodes(&doc, body).unwrap(), vec![before, new, after]);
485        assert_eq!(parent_node(&doc, old).unwrap(), None);
486    }
487
488    #[test]
489    fn first_child_and_siblings_walk_the_child_list() {
490        let (mut doc, _html, _head, body) = skeleton();
491        let a = div(&mut doc);
492        let b = div(&mut doc);
493        let c = div(&mut doc);
494        for id in [a, b, c] {
495            append_child(&mut doc, body, id).unwrap();
496        }
497        assert_eq!(first_child(&doc, body).unwrap(), Some(a));
498        assert_eq!(next_sibling(&doc, a).unwrap(), Some(b));
499        assert_eq!(next_sibling(&doc, c).unwrap(), None);
500        assert_eq!(previous_sibling(&doc, b).unwrap(), Some(a));
501        assert_eq!(previous_sibling(&doc, a).unwrap(), None);
502    }
503
504    #[test]
505    fn parent_node_is_none_for_a_detached_node() {
506        let (mut doc, _html, _head, _body) = skeleton();
507        let orphan = div(&mut doc);
508        assert_eq!(parent_node(&doc, orphan).unwrap(), None);
509    }
510
511    #[test]
512    fn child_nodes_lists_every_child_including_text() {
513        let (mut doc, _html, _head, body) = skeleton();
514        let element = div(&mut doc);
515        let text = document::create_text_node(&mut doc, "x").unwrap();
516        append_child(&mut doc, body, element).unwrap();
517        append_child(&mut doc, body, text).unwrap();
518        assert_eq!(child_nodes(&doc, body).unwrap(), vec![element, text]);
519    }
520
521    #[test]
522    fn has_child_nodes_tracks_the_child_list() {
523        let (mut doc, _html, _head, body) = skeleton();
524        assert!(!has_child_nodes(&doc, body).unwrap());
525        let child = div(&mut doc);
526        append_child(&mut doc, body, child).unwrap();
527        assert!(has_child_nodes(&doc, body).unwrap());
528    }
529
530    #[test]
531    fn contains_is_inclusive_and_transitive() {
532        let (mut doc, _html, _head, body) = skeleton();
533        let outer = div(&mut doc);
534        let inner = div(&mut doc);
535        let detached = div(&mut doc);
536        append_child(&mut doc, body, outer).unwrap();
537        append_child(&mut doc, outer, inner).unwrap();
538
539        assert!(contains(&doc, outer, outer).unwrap());
540        assert!(contains(&doc, outer, inner).unwrap());
541        assert!(contains(&doc, body, inner).unwrap());
542        assert!(!contains(&doc, inner, outer).unwrap());
543        assert!(!contains(&doc, outer, detached).unwrap());
544    }
545
546    #[test]
547    fn clone_node_copies_attributes_and_honours_deep() {
548        let (mut doc, _html, _head, body) = skeleton();
549        let outer = div(&mut doc);
550        let inner = div(&mut doc);
551        element::set_attribute(&mut doc, outer, "class", "panel").unwrap();
552        append_child(&mut doc, outer, inner).unwrap();
553        append_child(&mut doc, body, outer).unwrap();
554
555        let shallow = clone_node(&mut doc, outer, false).unwrap();
556        assert_eq!(
557            element::get_attribute(&doc, shallow, "class").unwrap(),
558            Some("panel".to_string())
559        );
560        assert!(child_nodes(&doc, shallow).unwrap().is_empty());
561
562        let deep = clone_node(&mut doc, outer, true).unwrap();
563        assert_eq!(child_nodes(&doc, deep).unwrap().len(), 1);
564    }
565
566    #[test]
567    fn clone_node_copies_text_and_comment_contents() {
568        let (mut doc, _html, _head, _body) = skeleton();
569        let text = document::create_text_node(&mut doc, "hello").unwrap();
570        let comment = document::create_comment(&mut doc, "note").unwrap();
571        let text_copy = clone_node(&mut doc, text, false).unwrap();
572        let comment_copy = clone_node(&mut doc, comment, false).unwrap();
573        assert_eq!(text_content(&doc, text_copy).unwrap(), "hello");
574        assert!(matches!(
575            doc.get_node(comment_copy).map(|n| &n.data),
576            Some(NodeData::Comment { contents }) if contents == "note"
577        ));
578    }
579
580    #[test]
581    fn text_content_concatenates_the_subtree() {
582        let (mut doc, _html, _head, body) = skeleton();
583        let outer = div(&mut doc);
584        let inner = div(&mut doc);
585        let a = document::create_text_node(&mut doc, "one ").unwrap();
586        let b = document::create_text_node(&mut doc, "two").unwrap();
587        append_child(&mut doc, outer, a).unwrap();
588        append_child(&mut doc, inner, b).unwrap();
589        append_child(&mut doc, outer, inner).unwrap();
590        append_child(&mut doc, body, outer).unwrap();
591        assert_eq!(text_content(&doc, outer).unwrap(), "one two");
592    }
593
594    /// The buffer variants agree with the owning one over the same subtree.
595    ///
596    /// All three go through `blitz-dom`'s single `write_text_content`
597    /// traversal, which is why this is an equivalence and not three
598    /// independently-maintained expectations. A private copy of that walk in
599    /// this crate is exactly what this would stop catching.
600    #[test]
601    fn text_content_into_agrees_with_text_content() {
602        let (mut doc, _html, _head, body) = skeleton();
603        let outer = div(&mut doc);
604        let inner = div(&mut doc);
605        let a = document::create_text_node(&mut doc, "one ").unwrap();
606        let b = document::create_text_node(&mut doc, "two").unwrap();
607        append_child(&mut doc, outer, a).unwrap();
608        append_child(&mut doc, inner, b).unwrap();
609        append_child(&mut doc, outer, inner).unwrap();
610        append_child(&mut doc, body, outer).unwrap();
611
612        let owned = text_content(&doc, outer).unwrap();
613        assert_eq!(text_content_len(&doc, outer).unwrap(), owned.len());
614
615        let mut buf = [0u8; 32];
616        assert_eq!(
617            text_content_into(&doc, outer, &mut buf).unwrap(),
618            owned.len()
619        );
620        assert_eq!(&buf[..owned.len()], owned.as_bytes());
621
622        // An empty subtree is length zero, not an error and not absent: every
623        // node has text content. This is why the reader has no `ABSENT` case.
624        let empty = div(&mut doc);
625        append_child(&mut doc, body, empty).unwrap();
626        assert_eq!(text_content_into(&doc, empty, &mut buf).unwrap(), 0);
627    }
628
629    /// The `snprintf` contract, across a concatenation rather than a single
630    /// stored value: the length is reported and the buffer is left untouched.
631    ///
632    /// The measure-then-fill split exists for exactly this case. A single
633    /// streaming pass would have written `one ` before discovering that `two`
634    /// did not fit, and the guarantee the ABI states would be false.
635    #[test]
636    fn text_content_into_writes_nothing_when_it_does_not_fit() {
637        let (mut doc, _html, _head, body) = skeleton();
638        let outer = div(&mut doc);
639        let a = document::create_text_node(&mut doc, "one ").unwrap();
640        let b = document::create_text_node(&mut doc, "two").unwrap();
641        append_child(&mut doc, outer, a).unwrap();
642        append_child(&mut doc, outer, b).unwrap();
643        append_child(&mut doc, body, outer).unwrap();
644
645        let mut buf = [b'.'; 5];
646        assert_eq!(text_content_into(&doc, outer, &mut buf).unwrap(), 7);
647        assert_eq!(
648            &buf, b".....",
649            "a partial write would have left `one ` here"
650        );
651    }
652
653    #[test]
654    fn set_text_content_replaces_children_with_one_text_node() {
655        let (mut doc, _html, _head, body) = skeleton();
656        let outer = div(&mut doc);
657        let old_child = div(&mut doc);
658        append_child(&mut doc, outer, old_child).unwrap();
659        append_child(&mut doc, body, outer).unwrap();
660
661        set_text_content(&mut doc, outer, "replaced").unwrap();
662        assert_eq!(child_nodes(&doc, outer).unwrap().len(), 1);
663        assert_eq!(text_content(&doc, outer).unwrap(), "replaced");
664        // Detached, not dropped.
665        assert!(doc.get_node(old_child).is_some());
666        assert_eq!(parent_node(&doc, old_child).unwrap(), None);
667    }
668
669    #[test]
670    fn set_text_content_empty_leaves_no_children() {
671        let (mut doc, _html, _head, body) = skeleton();
672        let outer = div(&mut doc);
673        append_child(&mut doc, body, outer).unwrap();
674        set_text_content(&mut doc, outer, "x").unwrap();
675        set_text_content(&mut doc, outer, "").unwrap();
676        assert!(child_nodes(&doc, outer).unwrap().is_empty());
677    }
678
679    #[test]
680    fn set_text_content_on_a_text_node_rewrites_it_in_place() {
681        let (mut doc, _html, _head, _body) = skeleton();
682        let text = document::create_text_node(&mut doc, "before").unwrap();
683        set_text_content(&mut doc, text, "after").unwrap();
684        assert_eq!(text_content(&doc, text).unwrap(), "after");
685        assert!(child_nodes(&doc, text).unwrap().is_empty());
686    }
687
688    #[test]
689    fn compare_document_position_reports_order_and_containment() {
690        use document_position::*;
691        let (mut doc, _html, _head, body) = skeleton();
692        let first = div(&mut doc);
693        let second = div(&mut doc);
694        let nested = div(&mut doc);
695        append_child(&mut doc, body, first).unwrap();
696        append_child(&mut doc, body, second).unwrap();
697        append_child(&mut doc, first, nested).unwrap();
698
699        assert_eq!(compare_document_position(&doc, first, first).unwrap(), 0);
700        assert_eq!(
701            compare_document_position(&doc, first, second).unwrap(),
702            FOLLOWING
703        );
704        assert_eq!(
705            compare_document_position(&doc, second, first).unwrap(),
706            PRECEDING
707        );
708        assert_eq!(
709            compare_document_position(&doc, first, nested).unwrap(),
710            FOLLOWING | CONTAINED_BY
711        );
712        assert_eq!(
713            compare_document_position(&doc, nested, first).unwrap(),
714            PRECEDING | CONTAINS
715        );
716    }
717
718    #[test]
719    fn compare_document_position_flags_disconnected_nodes() {
720        use document_position::*;
721        let (mut doc, _html, _head, body) = skeleton();
722        let attached = div(&mut doc);
723        let detached = div(&mut doc);
724        append_child(&mut doc, body, attached).unwrap();
725        let result = compare_document_position(&doc, attached, detached).unwrap();
726        assert_eq!(result & DISCONNECTED, DISCONNECTED);
727        assert_eq!(result & IMPLEMENTATION_SPECIFIC, IMPLEMENTATION_SPECIFIC);
728        assert_ne!(result & (PRECEDING | FOLLOWING), 0);
729    }
730}