Skip to main content

xpath_eval/
document.rs

1//! The XPath 1.0 data model (§5): node kinds, expanded names, and the
2//! `Node`/`Document` traits a caller implements over their own tree.
3//!
4//! This module only defines the data model — the 13 axes derived on top of
5//! it live in `axes.rs`, not here (see `plan/03-document-trait.md`).
6
7use std::cmp::Ordering;
8
9/// An XPath 1.0 node type (§5).
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum NodeKind {
12    Root,
13    Element,
14    Attribute,
15    Namespace,
16    ProcessingInstruction,
17    Comment,
18    Text,
19}
20
21/// An expanded name (namespace URI + local name), §5.2/§5.3/§5.4/§5.5.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ExpandedName {
24    pub namespace_uri: Option<String>,
25    pub local_name: String,
26}
27
28/// A node in the caller's tree, viewed as an XPath 1.0 data-model node.
29///
30/// `Copy` because axis iterators (`axes.rs`) produce many short-lived
31/// handles — modeled after `roxmltree::Node<'a>`, not a `NodeId`/arena
32/// scheme.
33///
34/// Methods take `self` by value (not `&self`) despite the `Copy` bound —
35/// deliberately, so the returned `impl Iterator<Item = Self> + 'a` only
36/// captures `'a` and not an additional borrow of a locally-owned handle
37/// (Rust's return-position-impl-trait-in-traits rules otherwise implicitly
38/// capture the `&self` lifetime too, which conflicts with callers that
39/// build axis iterators from an owned, short-lived `N`).
40pub trait Node<'a>: Copy + Eq + 'a {
41    /// The node's type (§5).
42    fn kind(self) -> NodeKind;
43
44    /// The node's parent, if any. Root nodes have no parent. Attribute and
45    /// namespace nodes have a parent (their owner element) despite not
46    /// being among that element's `children()`.
47    fn parent(self) -> Option<Self>;
48
49    /// The node's children. Non-empty only for `Root`/`Element` (§5.1/§5.2)
50    /// — attribute and namespace nodes are never children of anything.
51    fn children(self) -> impl Iterator<Item = Self> + 'a;
52
53    /// The element's attribute nodes. Non-empty only for `Element` (§5.3).
54    /// Not part of `children()` — see the document-order rule below.
55    fn attributes(self) -> impl Iterator<Item = Self> + 'a;
56
57    /// The element's namespace nodes. Non-empty only for `Element` (§5.4).
58    /// Not part of `children()`.
59    fn namespaces(self) -> impl Iterator<Item = Self> + 'a;
60
61    /// The node's expanded name. `None` for `Root`/`Comment`/`Text`, which
62    /// have no name; `Some` for `Element`/`Attribute`/`Namespace`/
63    /// `ProcessingInstruction` per the §5 table.
64    fn expanded_name(self) -> Option<ExpandedName>;
65
66    /// The node's string-value, computed per node kind per the §5 table
67    /// (e.g. Element/Root: concatenation of the string-values of all
68    /// descendant text nodes, in document order).
69    fn string_value(self) -> String;
70
71    /// Total order per document order (§5): an element occurs before its
72    /// namespace nodes, which occur before its attribute nodes, which
73    /// occur before its children, each subtree recursively in document
74    /// order. (Relative order among an element's own namespace nodes, and
75    /// among its own attribute nodes, is implementation-defined — it only
76    /// needs to be *some* stable order.) Must be consistent with
77    /// `children()`/`attributes()`/`namespaces()`.
78    fn document_order(self, other: Self) -> Ordering;
79}
80
81/// An XPath 1.0 document: gives access to the root node.
82pub trait Document {
83    type N<'a>: Node<'a>
84    where
85        Self: 'a;
86
87    fn root(&self) -> Self::N<'_>;
88}
89
90/// A minimal in-memory reference tree, used only to prove `Node`/`Document`
91/// are implementable and to drive the axis tests in `axes.rs`. Not a
92/// production tree type, not part of the public API.
93#[cfg(test)]
94pub(crate) mod fixture {
95    use super::{Document, ExpandedName, Node, NodeKind};
96    use std::cmp::Ordering;
97
98    #[derive(Debug)]
99    struct NodeData {
100        kind: NodeKind,
101        name: Option<ExpandedName>,
102        text: Option<String>,
103        parent: Option<usize>,
104        children: Vec<usize>,
105        attributes: Vec<usize>,
106        namespaces: Vec<usize>,
107    }
108
109    impl NodeData {
110        fn new(kind: NodeKind, parent: Option<usize>) -> Self {
111            NodeData {
112                kind,
113                name: None,
114                text: None,
115                parent,
116                children: Vec::new(),
117                attributes: Vec::new(),
118                namespaces: Vec::new(),
119            }
120        }
121    }
122
123    #[derive(Debug)]
124    struct Arena {
125        nodes: Vec<NodeData>,
126    }
127
128    /// Builds a fixture tree. Calls must add an element's namespaces, then
129    /// its attributes, then its children (each fully built before moving
130    /// to the next sibling) so that arena indices come out in true
131    /// document order — `document_order()` is then just an index compare.
132    struct Builder {
133        nodes: Vec<NodeData>,
134    }
135
136    impl Builder {
137        fn push(&mut self, data: NodeData) -> usize {
138            let idx = self.nodes.len();
139            self.nodes.push(data);
140            idx
141        }
142
143        fn new_root(&mut self) -> usize {
144            self.push(NodeData::new(NodeKind::Root, None))
145        }
146
147        fn add_element(&mut self, parent: usize, local: &str) -> usize {
148            let mut data = NodeData::new(NodeKind::Element, Some(parent));
149            data.name = Some(ExpandedName {
150                namespace_uri: None,
151                local_name: local.to_string(),
152            });
153            let idx = self.push(data);
154            self.nodes[parent].children.push(idx);
155            idx
156        }
157
158        fn add_namespace(&mut self, owner: usize, prefix: &str, uri: &str) -> usize {
159            let mut data = NodeData::new(NodeKind::Namespace, Some(owner));
160            data.name = Some(ExpandedName {
161                namespace_uri: None,
162                local_name: prefix.to_string(),
163            });
164            data.text = Some(uri.to_string());
165            let idx = self.push(data);
166            self.nodes[owner].namespaces.push(idx);
167            idx
168        }
169
170        fn add_attribute(&mut self, owner: usize, local: &str, value: &str) -> usize {
171            let mut data = NodeData::new(NodeKind::Attribute, Some(owner));
172            data.name = Some(ExpandedName {
173                namespace_uri: None,
174                local_name: local.to_string(),
175            });
176            data.text = Some(value.to_string());
177            let idx = self.push(data);
178            self.nodes[owner].attributes.push(idx);
179            idx
180        }
181
182        fn add_text(&mut self, parent: usize, text: &str) -> usize {
183            let mut data = NodeData::new(NodeKind::Text, Some(parent));
184            data.text = Some(text.to_string());
185            let idx = self.push(data);
186            self.nodes[parent].children.push(idx);
187            idx
188        }
189
190        fn add_comment(&mut self, parent: usize, text: &str) -> usize {
191            let mut data = NodeData::new(NodeKind::Comment, Some(parent));
192            data.text = Some(text.to_string());
193            let idx = self.push(data);
194            self.nodes[parent].children.push(idx);
195            idx
196        }
197
198        fn add_pi(&mut self, parent: usize, target: &str, content: &str) -> usize {
199            let mut data = NodeData::new(NodeKind::ProcessingInstruction, Some(parent));
200            data.name = Some(ExpandedName {
201                namespace_uri: None,
202                local_name: target.to_string(),
203            });
204            data.text = Some(content.to_string());
205            let idx = self.push(data);
206            self.nodes[parent].children.push(idx);
207            idx
208        }
209    }
210
211    pub(crate) struct TestDoc {
212        arena: Arena,
213    }
214
215    impl TestDoc {
216        pub(crate) fn node(&self, idx: usize) -> TestNode<'_> {
217            TestNode {
218                arena: &self.arena,
219                idx,
220            }
221        }
222    }
223
224    impl Document for TestDoc {
225        type N<'a> = TestNode<'a>;
226
227        fn root(&self) -> Self::N<'_> {
228            self.node(0)
229        }
230    }
231
232    #[derive(Clone, Copy, Debug)]
233    pub(crate) struct TestNode<'a> {
234        arena: &'a Arena,
235        idx: usize,
236    }
237
238    impl<'a> PartialEq for TestNode<'a> {
239        fn eq(&self, other: &Self) -> bool {
240            std::ptr::eq(self.arena, other.arena) && self.idx == other.idx
241        }
242    }
243
244    impl<'a> Eq for TestNode<'a> {}
245
246    impl<'a> Node<'a> for TestNode<'a> {
247        fn kind(self) -> NodeKind {
248            self.arena.nodes[self.idx].kind
249        }
250
251        fn parent(self) -> Option<Self> {
252            self.arena.nodes[self.idx].parent.map(|p| TestNode {
253                arena: self.arena,
254                idx: p,
255            })
256        }
257
258        fn children(self) -> impl Iterator<Item = Self> + 'a {
259            let arena = self.arena;
260            self.arena.nodes[self.idx]
261                .children
262                .clone()
263                .into_iter()
264                .map(move |i| TestNode { arena, idx: i })
265        }
266
267        fn attributes(self) -> impl Iterator<Item = Self> + 'a {
268            let arena = self.arena;
269            self.arena.nodes[self.idx]
270                .attributes
271                .clone()
272                .into_iter()
273                .map(move |i| TestNode { arena, idx: i })
274        }
275
276        fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
277            let arena = self.arena;
278            self.arena.nodes[self.idx]
279                .namespaces
280                .clone()
281                .into_iter()
282                .map(move |i| TestNode { arena, idx: i })
283        }
284
285        fn expanded_name(self) -> Option<ExpandedName> {
286            self.arena.nodes[self.idx].name.clone()
287        }
288
289        fn string_value(self) -> String {
290            let data = &self.arena.nodes[self.idx];
291            match data.kind {
292                NodeKind::Root | NodeKind::Element => {
293                    let mut out = String::new();
294                    collect_text(self.arena, self.idx, &mut out);
295                    out
296                }
297                NodeKind::Attribute
298                | NodeKind::Namespace
299                | NodeKind::ProcessingInstruction
300                | NodeKind::Comment
301                | NodeKind::Text => data.text.clone().unwrap_or_default(),
302            }
303        }
304
305        fn document_order(self, other: Self) -> Ordering {
306            self.idx.cmp(&other.idx)
307        }
308    }
309
310    fn collect_text(arena: &Arena, idx: usize, out: &mut String) {
311        for &child in &arena.nodes[idx].children {
312            match arena.nodes[child].kind {
313                NodeKind::Text => out.push_str(arena.nodes[child].text.as_deref().unwrap_or("")),
314                NodeKind::Element => collect_text(arena, child, out),
315                _ => {}
316            }
317        }
318    }
319
320    /// Named handles into the fixture tree built by [`build`]:
321    ///
322    /// ```text
323    /// Root
324    ///  └─ html (ns: xml=...; attrs: lang="en", id="root-el")
325    ///      └─ body (ns: x=...; attrs: class="main")
326    ///          ├─ text0 "Hello "
327    ///          ├─ span (attrs: data-x="1")
328    ///          │   └─ text1 "World"
329    ///          ├─ comment "note"
330    ///          ├─ pi proc "data"
331    ///          └─ text2 "!"
332    /// ```
333    ///
334    /// `span`'s `data-x` attribute exists specifically as a trap for
335    /// `following`/`preceding`: it sits in document order strictly between
336    /// `span` and `text1`, so it is neither an ancestor nor a descendant
337    /// of `text0`/`text2` — an implementation that excludes attribute/
338    /// namespace nodes from those axes only by filtering out descendants/
339    /// ancestors (instead of also filtering by node kind) would wrongly
340    /// let it leak into `following(text0)` or `preceding(text2)`.
341    pub(crate) struct Fixture {
342        pub doc: TestDoc,
343        pub html: usize,
344        pub html_ns: usize,
345        pub html_lang: usize,
346        pub html_id: usize,
347        pub body: usize,
348        pub body_ns: usize,
349        pub body_class: usize,
350        pub text0: usize,
351        pub span: usize,
352        pub span_attr: usize,
353        pub text1: usize,
354        pub comment: usize,
355        pub pi: usize,
356        pub text2: usize,
357    }
358
359    pub(crate) fn build() -> Fixture {
360        let mut b = Builder { nodes: Vec::new() };
361        let root = b.new_root();
362        let html = b.add_element(root, "html");
363        let html_ns = b.add_namespace(html, "xml", "http://www.w3.org/XML/1998/namespace");
364        let html_lang = b.add_attribute(html, "lang", "en");
365        let html_id = b.add_attribute(html, "id", "root-el");
366        let body = b.add_element(html, "body");
367        let body_ns = b.add_namespace(body, "x", "urn:x");
368        let body_class = b.add_attribute(body, "class", "main");
369        let text0 = b.add_text(body, "Hello ");
370        let span = b.add_element(body, "span");
371        let span_attr = b.add_attribute(span, "data-x", "1");
372        let text1 = b.add_text(span, "World");
373        let comment = b.add_comment(body, "note");
374        let pi = b.add_pi(body, "proc", "data");
375        let text2 = b.add_text(body, "!");
376        Fixture {
377            doc: TestDoc {
378                arena: Arena { nodes: b.nodes },
379            },
380            html,
381            html_ns,
382            html_lang,
383            html_id,
384            body,
385            body_ns,
386            body_class,
387            text0,
388            span,
389            span_attr,
390            text1,
391            comment,
392            pi,
393            text2,
394        }
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::fixture::build;
401    use super::{Node, NodeKind};
402
403    #[test]
404    fn string_value_text() {
405        let f = build();
406        assert_eq!(f.doc.node(f.text0).string_value(), "Hello ");
407    }
408
409    #[test]
410    fn string_value_comment() {
411        let f = build();
412        assert_eq!(f.doc.node(f.comment).string_value(), "note");
413    }
414
415    #[test]
416    fn string_value_processing_instruction() {
417        let f = build();
418        assert_eq!(f.doc.node(f.pi).string_value(), "data");
419    }
420
421    #[test]
422    fn string_value_attribute() {
423        let f = build();
424        assert_eq!(f.doc.node(f.html_lang).string_value(), "en");
425    }
426
427    #[test]
428    fn string_value_namespace() {
429        let f = build();
430        assert_eq!(
431            f.doc.node(f.html_ns).string_value(),
432            "http://www.w3.org/XML/1998/namespace"
433        );
434    }
435
436    #[test]
437    fn string_value_element_concatenates_nested_descendant_text_in_document_order() {
438        let f = build();
439        // span is nested one level deeper than text0/text2 — string-value
440        // must still concatenate across depths, in document order.
441        assert_eq!(f.doc.node(f.span).string_value(), "World");
442        assert_eq!(f.doc.node(f.body).string_value(), "Hello World!");
443        assert_eq!(f.doc.node(f.html).string_value(), "Hello World!");
444    }
445
446    #[test]
447    fn string_value_root_concatenates_all_descendant_text() {
448        let f = build();
449        assert_eq!(f.doc.node(0).string_value(), "Hello World!");
450    }
451
452    #[test]
453    fn expanded_name_per_kind() {
454        let f = build();
455        assert!(f.doc.node(0).expanded_name().is_none()); // Root
456        assert_eq!(
457            f.doc.node(f.html).expanded_name().unwrap().local_name,
458            "html"
459        );
460        assert_eq!(
461            f.doc.node(f.html_lang).expanded_name().unwrap().local_name,
462            "lang"
463        );
464        assert_eq!(
465            f.doc.node(f.html_ns).expanded_name().unwrap().local_name,
466            "xml"
467        );
468        assert_eq!(f.doc.node(f.pi).expanded_name().unwrap().local_name, "proc");
469        assert!(f.doc.node(f.comment).expanded_name().is_none());
470        assert!(f.doc.node(f.text0).expanded_name().is_none());
471    }
472
473    #[test]
474    fn kind_per_node() {
475        let f = build();
476        assert_eq!(f.doc.node(0).kind(), NodeKind::Root);
477        assert_eq!(f.doc.node(f.html).kind(), NodeKind::Element);
478        assert_eq!(f.doc.node(f.html_lang).kind(), NodeKind::Attribute);
479        assert_eq!(f.doc.node(f.html_ns).kind(), NodeKind::Namespace);
480        assert_eq!(f.doc.node(f.pi).kind(), NodeKind::ProcessingInstruction);
481        assert_eq!(f.doc.node(f.comment).kind(), NodeKind::Comment);
482        assert_eq!(f.doc.node(f.text0).kind(), NodeKind::Text);
483    }
484
485    #[test]
486    fn document_order_matches_tree_structure() {
487        let f = build();
488        use std::cmp::Ordering;
489        let root = f.doc.node(0);
490        let html = f.doc.node(f.html);
491        let body = f.doc.node(f.body);
492        let text0 = f.doc.node(f.text0);
493        assert_eq!(root.document_order(html), Ordering::Less);
494        assert_eq!(html.document_order(body), Ordering::Less);
495        assert_eq!(body.document_order(text0), Ordering::Less);
496        assert_eq!(html.document_order(html), Ordering::Equal);
497    }
498}