Skip to main content

blitz_dom_api/
document.rs

1//! Document-level operations: node creation and lookup.
2//!
3//! Upstream: `blitz-script/src/dom/document.rs`. See MAPPING.md.
4
5use blitz_dom::{BaseDocument, LocalName, Namespace, NodeId, QualName};
6
7use crate::Result;
8use crate::error::DomError;
9
10/// An HTML-namespaced qualified name.
11pub(crate) fn qual_name(local: &str) -> QualName {
12    QualName::new(None, markup5ever::ns!(html), LocalName::from(local))
13}
14
15/// A qualified name in an explicit namespace.
16pub(crate) fn qual_name_ns(local: &str, ns: &str) -> QualName {
17    QualName::new(None, Namespace::from(ns), LocalName::from(local))
18}
19
20// === Node creation ===
21
22/// `document.createElement(tag)`.
23///
24/// The tag is ASCII-lowercased, as the HTML parser would have done. The
25/// element is created detached; nothing about layout changes until it is
26/// inserted, which is why `blitz-script` deliberately does not mark layout
27/// dirty here either.
28pub fn create_element(doc: &mut BaseDocument, tag: &str) -> Result<NodeId> {
29    let tag = tag.to_ascii_lowercase();
30    Ok(doc.mutate().create_element(qual_name(&tag), Vec::new()))
31}
32
33/// `document.createElementNS(ns, tag)`.
34///
35/// Argument order follows the DOM method, namespace first. The tag is *not*
36/// lowercased: SVG and MathML names are case-sensitive.
37pub fn create_element_ns(doc: &mut BaseDocument, ns: &str, tag: &str) -> Result<NodeId> {
38    Ok(doc
39        .mutate()
40        .create_element(qual_name_ns(tag, ns), Vec::new()))
41}
42
43/// `document.createTextNode(text)`.
44pub fn create_text_node(doc: &mut BaseDocument, text: &str) -> Result<NodeId> {
45    Ok(doc.mutate().create_text_node(text))
46}
47
48/// `document.createComment(text)`.
49pub fn create_comment(doc: &mut BaseDocument, text: &str) -> Result<NodeId> {
50    Ok(doc.mutate().create_comment_node(text))
51}
52
53/// `document.importNode(node, deep)`.
54///
55/// Every script-visible node belongs to the one document, so importing is the
56/// same structural operation as cloning. It stays a distinct entry point
57/// because frameworks take this path specifically for template roots.
58pub fn import_node(doc: &mut BaseDocument, node: NodeId, deep: bool) -> Result<NodeId> {
59    crate::node::clone_node(doc, node, deep)
60}
61
62// === Lookup ===
63
64/// `document.documentElement`.
65pub fn document_element(doc: &BaseDocument) -> Result<Option<NodeId>> {
66    Ok(doc.try_root_element().map(|root| root.id))
67}
68
69/// `document.body`.
70pub fn body(doc: &BaseDocument) -> Result<Option<NodeId>> {
71    Ok(find_tag(doc, markup5ever::local_name!("body")))
72}
73
74/// `document.head`.
75pub fn head(doc: &BaseDocument) -> Result<Option<NodeId>> {
76    Ok(find_tag(doc, markup5ever::local_name!("head")))
77}
78
79/// Find the first element with the given tag name.
80///
81/// Three stages, in this order, copied from upstream: the root element itself,
82/// then its immediate children, then a full pre-order walk. The first two are
83/// the shape a parsed document actually has; the walk is the fallback for a
84/// tree script assembled by hand.
85fn find_tag(doc: &BaseDocument, tag: LocalName) -> Option<NodeId> {
86    let root = doc.try_root_element()?;
87    if root.data.is_element_with_tag_name(&tag) {
88        return Some(root.id);
89    }
90    root.children
91        .iter()
92        .copied()
93        .find(|child_id| {
94            doc.get_node(*child_id)
95                .is_some_and(|child| child.data.is_element_with_tag_name(&tag))
96        })
97        .or_else(|| {
98            let mut stack = vec![doc.root_node().id];
99            while let Some(node_id) = stack.pop() {
100                let node = doc.get_node(node_id)?;
101                if node.data.is_element_with_tag_name(&tag) {
102                    return Some(node_id);
103                }
104                stack.extend(node.children.iter().rev().copied());
105            }
106            None
107        })
108}
109
110/// `document.getElementById(id)`.
111pub fn get_element_by_id(doc: &BaseDocument, id: &str) -> Result<Option<NodeId>> {
112    Ok(doc.get_element_by_id(id))
113}
114
115/// `document.querySelector(selector)`.
116///
117/// Deliberate difference: an unparseable selector is an error here.
118/// `blitz-script` maps it to `null`; its reparented body is
119/// `query_selector(...).ok().flatten()`.
120pub fn query_selector(doc: &BaseDocument, selector: &str) -> Result<Option<NodeId>> {
121    doc.query_selector(selector)
122        .map_err(|_| DomError::InvalidSelector(selector.to_owned()))
123}
124
125/// `document.querySelectorAll(selector)`, in tree order.
126///
127/// Same deliberate difference as [`query_selector`]; upstream's fallback is
128/// `.unwrap_or_default()`.
129pub fn query_selector_all(doc: &BaseDocument, selector: &str) -> Result<Vec<NodeId>> {
130    doc.query_selector_all(selector)
131        .map(|matches| matches.into_iter().collect())
132        .map_err(|_| DomError::InvalidSelector(selector.to_owned()))
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::element;
139    use crate::node;
140    use crate::test_support::{bare, skeleton};
141
142    #[test]
143    fn create_element_lowercases_the_tag() {
144        let mut doc = bare();
145        let id = create_element(&mut doc, "DIV").unwrap();
146        assert_eq!(element::tag_name(&doc, id).unwrap(), "DIV");
147        assert_eq!(
148            doc.get_node(id)
149                .and_then(|n| n.element_data())
150                .map(|e| e.name.local.to_string()),
151            Some("div".to_string())
152        );
153    }
154
155    #[test]
156    fn create_element_ns_keeps_the_namespace_and_the_case() {
157        let mut doc = bare();
158        let id =
159            create_element_ns(&mut doc, "http://www.w3.org/2000/svg", "linearGradient").unwrap();
160        let element = doc.get_node(id).unwrap().element_data().unwrap();
161        assert_eq!(element.name.local.to_string(), "linearGradient");
162        assert_eq!(element.name.ns.to_string(), "http://www.w3.org/2000/svg");
163    }
164
165    #[test]
166    fn create_text_node_carries_its_text() {
167        let mut doc = bare();
168        let id = create_text_node(&mut doc, "hello").unwrap();
169        assert_eq!(node::text_content(&doc, id).unwrap(), "hello");
170    }
171
172    #[test]
173    fn create_comment_makes_a_comment_node() {
174        let mut doc = bare();
175        let id = create_comment(&mut doc, "note").unwrap();
176        assert!(matches!(
177            doc.get_node(id).map(|n| &n.data),
178            Some(blitz_dom::NodeData::Comment { .. })
179        ));
180    }
181
182    #[test]
183    fn import_node_copies_the_subtree_when_deep() {
184        let (mut doc, _html, _head, body) = skeleton();
185        let outer = create_element(&mut doc, "div").unwrap();
186        let inner = create_element(&mut doc, "span").unwrap();
187        node::append_child(&mut doc, outer, inner).unwrap();
188        node::append_child(&mut doc, body, outer).unwrap();
189
190        let copy = import_node(&mut doc, outer, true).unwrap();
191        assert_ne!(copy, outer);
192        assert_eq!(node::child_nodes(&doc, copy).unwrap().len(), 1);
193
194        let shallow = import_node(&mut doc, outer, false).unwrap();
195        assert!(node::child_nodes(&doc, shallow).unwrap().is_empty());
196    }
197
198    #[test]
199    fn document_element_is_the_root_element() {
200        let (doc, html, _head, _body) = skeleton();
201        assert_eq!(document_element(&doc).unwrap(), Some(html));
202        assert_eq!(document_element(&bare()).unwrap(), None);
203    }
204
205    #[test]
206    fn body_and_head_are_found_among_the_root_children() {
207        let (doc, _html, head_id, body_id) = skeleton();
208        assert_eq!(body(&doc).unwrap(), Some(body_id));
209        assert_eq!(head(&doc).unwrap(), Some(head_id));
210    }
211
212    /// The third stage of the search: a body nested deeper than the root's own
213    /// children still resolves.
214    #[test]
215    fn body_falls_back_to_a_full_tree_walk() {
216        let (mut doc, html, _head, _body) = skeleton();
217        let wrapper = create_element(&mut doc, "div").unwrap();
218        let nested = create_element(&mut doc, "body").unwrap();
219        node::append_child(&mut doc, wrapper, nested).unwrap();
220        node::append_child(&mut doc, html, wrapper).unwrap();
221
222        // The skeleton's own body is still a direct child, so it wins.
223        assert_ne!(body(&doc).unwrap(), Some(nested));
224
225        // With no direct child body, the walk finds the nested one.
226        let (mut doc, html, _head, body_id) = skeleton();
227        node::remove_child(&mut doc, html, body_id).unwrap();
228        let wrapper = create_element(&mut doc, "div").unwrap();
229        let nested = create_element(&mut doc, "body").unwrap();
230        node::append_child(&mut doc, wrapper, nested).unwrap();
231        node::append_child(&mut doc, html, wrapper).unwrap();
232        assert_eq!(body(&doc).unwrap(), Some(nested));
233    }
234
235    #[test]
236    fn get_element_by_id_finds_an_attached_element() {
237        let (mut doc, _html, _head, body_id) = skeleton();
238        let target = create_element(&mut doc, "div").unwrap();
239        element::set_attribute(&mut doc, target, "id", "target").unwrap();
240        node::append_child(&mut doc, body_id, target).unwrap();
241        assert_eq!(get_element_by_id(&doc, "target").unwrap(), Some(target));
242        assert_eq!(get_element_by_id(&doc, "absent").unwrap(), None);
243    }
244
245    #[test]
246    fn query_selector_returns_the_first_match() {
247        let (mut doc, _html, _head, body_id) = skeleton();
248        let first = create_element(&mut doc, "p").unwrap();
249        let second = create_element(&mut doc, "p").unwrap();
250        node::append_child(&mut doc, body_id, first).unwrap();
251        node::append_child(&mut doc, body_id, second).unwrap();
252        assert_eq!(query_selector(&doc, "p").unwrap(), Some(first));
253    }
254
255    #[test]
256    fn query_selector_all_returns_every_match_in_tree_order() {
257        let (mut doc, _html, _head, body_id) = skeleton();
258        let first = create_element(&mut doc, "p").unwrap();
259        let second = create_element(&mut doc, "p").unwrap();
260        node::append_child(&mut doc, body_id, first).unwrap();
261        node::append_child(&mut doc, body_id, second).unwrap();
262        assert_eq!(query_selector_all(&doc, "p").unwrap(), vec![first, second]);
263    }
264
265    #[test]
266    fn an_unparseable_selector_is_an_error_not_an_empty_result() {
267        let (doc, _html, _head, _body) = skeleton();
268        assert!(matches!(
269            query_selector(&doc, "!!!"),
270            Err(DomError::InvalidSelector(_))
271        ));
272        assert!(matches!(
273            query_selector_all(&doc, "!!!"),
274            Err(DomError::InvalidSelector(_))
275        ));
276    }
277}