Skip to main content

rdom_core/
text.rs

1//! `textContent` — getter concatenates descendant Text data; setter
2//! replaces all children with a single Text node.
3//!
4//! Spec: <https://dom.spec.whatwg.org/#dom-node-textcontent>
5
6use crate::dom::Dom;
7use crate::error::Result;
8use crate::node::NodeData;
9use crate::node_id::NodeId;
10
11impl<Ext> Dom<Ext> {
12    /// Concatenate the string content of `id` and all its descendants.
13    ///
14    /// - Text nodes: own `data`.
15    /// - Element / Fragment: recursive concat of descendants.
16    /// - Comment: empty string (matches spec — comments are **not**
17    ///   included in textContent).
18    pub fn text_content(&self, id: NodeId) -> String {
19        let mut out = String::new();
20        self.collect_text(id, &mut out);
21        out
22    }
23
24    /// The text nodes' data in tree order (an iterative walk, so any
25    /// depth is safe); comments contribute nothing.
26    fn collect_text(&self, id: NodeId, out: &mut String) {
27        self.walk_subtree(id, &mut |_, data| {
28            if let NodeData::Text { data } = data {
29                out.push_str(data);
30            }
31        });
32    }
33}
34
35impl<Ext: Default> Dom<Ext> {
36    /// Replace all children of `id` with a single Text node containing `text`.
37    ///
38    /// Matches `Node.textContent` setter semantics: any existing children
39    /// are detached + dropped; if `text` is empty the node has no
40    /// children; otherwise it has exactly one Text child.
41    ///
42    /// Element-only operation fails with `WrongNodeType` on Text/Comment
43    /// (use `set_node_value` on those).
44    pub fn set_text_content(&mut self, id: NodeId, text: &str) -> Result<()> {
45        use crate::error::DomError;
46        use crate::node::NodeType;
47
48        match &self.node_or_err(id)?.data {
49            NodeData::Element { .. } | NodeData::Fragment => {}
50            NodeData::Text { .. } => {
51                return Err(DomError::WrongNodeType {
52                    expected: "Element or Fragment",
53                    got: NodeType::Text,
54                });
55            }
56            NodeData::Comment { .. } => {
57                return Err(DomError::WrongNodeType {
58                    expected: "Element or Fragment",
59                    got: NodeType::Comment,
60                });
61            }
62        }
63
64        // Drop existing children entirely so we don't leak orphan nodes.
65        let existing: Vec<NodeId> = {
66            let mut out = Vec::new();
67            let mut c = self.get_node(id).and_then(|n| n.first_child);
68            while let Some(cid) = c {
69                out.push(cid);
70                c = self.get_node(cid).and_then(|n| n.next_sibling);
71            }
72            out
73        };
74        for cid in existing {
75            self.drop_subtree(cid)?;
76        }
77
78        if !text.is_empty() {
79            let t = self.create_text_node(text);
80            self.append_child(id, t)?;
81        }
82        Ok(())
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use crate::Dom;
89
90    #[test]
91    fn text_node_returns_own_data() {
92        let mut dom: Dom = Dom::new();
93        let t = dom.create_text_node("hello");
94        assert_eq!(dom.text_content(t), "hello");
95    }
96
97    #[test]
98    fn element_concatenates_descendant_text() {
99        let mut dom: Dom = Dom::new();
100        let div = dom.create_element("div");
101        let a = dom.create_text_node("hello ");
102        let span = dom.create_element("span");
103        let b = dom.create_text_node("world");
104        dom.append_child(span, b).unwrap();
105        dom.append_child(div, a).unwrap();
106        dom.append_child(div, span).unwrap();
107        assert_eq!(dom.text_content(div), "hello world");
108    }
109
110    #[test]
111    fn comment_children_not_included() {
112        let mut dom: Dom = Dom::new();
113        let div = dom.create_element("div");
114        let a = dom.create_text_node("before ");
115        let c = dom.create_comment(" skip me ");
116        let b = dom.create_text_node("after");
117        dom.append_child(div, a).unwrap();
118        dom.append_child(div, c).unwrap();
119        dom.append_child(div, b).unwrap();
120        assert_eq!(dom.text_content(div), "before after");
121    }
122
123    #[test]
124    fn empty_element_empty_text_content() {
125        let mut dom: Dom = Dom::new();
126        let div = dom.create_element("div");
127        assert_eq!(dom.text_content(div), "");
128    }
129
130    #[test]
131    fn set_text_content_replaces_children() {
132        let mut dom: Dom = Dom::new();
133        let div = dom.create_element("div");
134        let old_text = dom.create_text_node("old");
135        let old_span = dom.create_element("span");
136        dom.append_child(div, old_text).unwrap();
137        dom.append_child(div, old_span).unwrap();
138
139        dom.set_text_content(div, "new content").unwrap();
140
141        assert_eq!(dom.node(div).child_element_count(), 0);
142        assert_eq!(
143            dom.node(div).first_child().unwrap().node_value(),
144            Some("new content")
145        );
146        assert_eq!(dom.text_content(div), "new content");
147    }
148
149    #[test]
150    fn set_text_content_empty_clears_children() {
151        let mut dom: Dom = Dom::new();
152        let div = dom.create_element("div");
153        let t = dom.create_text_node("existing");
154        dom.append_child(div, t).unwrap();
155
156        dom.set_text_content(div, "").unwrap();
157
158        assert!(!dom.node(div).has_child_nodes());
159    }
160
161    #[test]
162    fn set_text_content_on_text_errors() {
163        let mut dom: Dom = Dom::new();
164        let t = dom.create_text_node("hi");
165        assert!(dom.set_text_content(t, "bye").is_err());
166    }
167
168    #[test]
169    fn set_text_content_on_fragment_works() {
170        let mut dom: Dom = Dom::new();
171        let frag = dom.create_document_fragment();
172        let old = dom.create_element("span");
173        dom.append_child(frag, old).unwrap();
174
175        dom.set_text_content(frag, "flat text").unwrap();
176        assert_eq!(dom.text_content(frag), "flat text");
177    }
178}