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    /// Whether this attribute node is of type `ID` (§5.3, §4.1's `id()`) —
81    /// i.e. whether XPath's `id()` function should be able to find this
82    /// attribute's owner element by this attribute's string-value. The
83    /// XPath 1.0 data model derives this from a DTD or schema; most callers
84    /// have no such information, so the default is `false` everywhere
85    /// (`id()` then always returns an empty node-set, never an error — the
86    /// function itself is always implemented). Callers that do have ID
87    /// information (DTD validation, `xml:id`, a known convention) should
88    /// override this on their `Attribute`-kind nodes. Meaningless (and
89    /// never called by this crate) on non-`Attribute` nodes.
90    fn is_id_attribute(self) -> bool {
91        false
92    }
93}
94
95/// An XPath 1.0 document: gives access to the root node.
96pub trait Document {
97    type N<'a>: Node<'a>
98    where
99        Self: 'a;
100
101    fn root(&self) -> Self::N<'_>;
102}
103
104/// A minimal in-memory reference tree, used only to prove `Node`/`Document`
105/// are implementable and to drive the axis tests in `axes.rs`. Not a
106/// production tree type, not part of the public API.
107#[cfg(test)]
108pub(crate) mod fixture {
109    use super::{Document, ExpandedName, Node, NodeKind};
110    use std::cmp::Ordering;
111
112    #[derive(Debug)]
113    struct NodeData {
114        kind: NodeKind,
115        name: Option<ExpandedName>,
116        text: Option<String>,
117        parent: Option<usize>,
118        children: Vec<usize>,
119        attributes: Vec<usize>,
120        namespaces: Vec<usize>,
121    }
122
123    impl NodeData {
124        fn new(kind: NodeKind, parent: Option<usize>) -> Self {
125            NodeData {
126                kind,
127                name: None,
128                text: None,
129                parent,
130                children: Vec::new(),
131                attributes: Vec::new(),
132                namespaces: Vec::new(),
133            }
134        }
135    }
136
137    #[derive(Debug)]
138    struct Arena {
139        nodes: Vec<NodeData>,
140    }
141
142    /// Builds a fixture tree. Calls must add an element's namespaces, then
143    /// its attributes, then its children (each fully built before moving
144    /// to the next sibling) so that arena indices come out in true
145    /// document order — `document_order()` is then just an index compare.
146    struct Builder {
147        nodes: Vec<NodeData>,
148    }
149
150    impl Builder {
151        fn push(&mut self, data: NodeData) -> usize {
152            let idx = self.nodes.len();
153            self.nodes.push(data);
154            idx
155        }
156
157        fn new_root(&mut self) -> usize {
158            self.push(NodeData::new(NodeKind::Root, None))
159        }
160
161        fn add_element(&mut self, parent: usize, local: &str) -> usize {
162            let mut data = NodeData::new(NodeKind::Element, Some(parent));
163            data.name = Some(ExpandedName {
164                namespace_uri: None,
165                local_name: local.to_string(),
166            });
167            let idx = self.push(data);
168            self.nodes[parent].children.push(idx);
169            idx
170        }
171
172        fn add_namespace(&mut self, owner: usize, prefix: &str, uri: &str) -> usize {
173            let mut data = NodeData::new(NodeKind::Namespace, Some(owner));
174            data.name = Some(ExpandedName {
175                namespace_uri: None,
176                local_name: prefix.to_string(),
177            });
178            data.text = Some(uri.to_string());
179            let idx = self.push(data);
180            self.nodes[owner].namespaces.push(idx);
181            idx
182        }
183
184        fn add_attribute(&mut self, owner: usize, local: &str, value: &str) -> usize {
185            let mut data = NodeData::new(NodeKind::Attribute, Some(owner));
186            data.name = Some(ExpandedName {
187                namespace_uri: None,
188                local_name: local.to_string(),
189            });
190            data.text = Some(value.to_string());
191            let idx = self.push(data);
192            self.nodes[owner].attributes.push(idx);
193            idx
194        }
195
196        fn add_text(&mut self, parent: usize, text: &str) -> usize {
197            let mut data = NodeData::new(NodeKind::Text, Some(parent));
198            data.text = Some(text.to_string());
199            let idx = self.push(data);
200            self.nodes[parent].children.push(idx);
201            idx
202        }
203
204        fn add_comment(&mut self, parent: usize, text: &str) -> usize {
205            let mut data = NodeData::new(NodeKind::Comment, Some(parent));
206            data.text = Some(text.to_string());
207            let idx = self.push(data);
208            self.nodes[parent].children.push(idx);
209            idx
210        }
211
212        fn add_pi(&mut self, parent: usize, target: &str, content: &str) -> usize {
213            let mut data = NodeData::new(NodeKind::ProcessingInstruction, Some(parent));
214            data.name = Some(ExpandedName {
215                namespace_uri: None,
216                local_name: target.to_string(),
217            });
218            data.text = Some(content.to_string());
219            let idx = self.push(data);
220            self.nodes[parent].children.push(idx);
221            idx
222        }
223    }
224
225    pub(crate) struct TestDoc {
226        arena: Arena,
227    }
228
229    impl TestDoc {
230        pub(crate) fn node(&self, idx: usize) -> TestNode<'_> {
231            TestNode {
232                arena: &self.arena,
233                idx,
234            }
235        }
236    }
237
238    impl Document for TestDoc {
239        type N<'a> = TestNode<'a>;
240
241        fn root(&self) -> Self::N<'_> {
242            self.node(0)
243        }
244    }
245
246    #[derive(Clone, Copy, Debug)]
247    pub(crate) struct TestNode<'a> {
248        arena: &'a Arena,
249        idx: usize,
250    }
251
252    impl<'a> PartialEq for TestNode<'a> {
253        fn eq(&self, other: &Self) -> bool {
254            std::ptr::eq(self.arena, other.arena) && self.idx == other.idx
255        }
256    }
257
258    impl<'a> Eq for TestNode<'a> {}
259
260    impl<'a> Node<'a> for TestNode<'a> {
261        fn kind(self) -> NodeKind {
262            self.arena.nodes[self.idx].kind
263        }
264
265        fn parent(self) -> Option<Self> {
266            self.arena.nodes[self.idx].parent.map(|p| TestNode {
267                arena: self.arena,
268                idx: p,
269            })
270        }
271
272        fn children(self) -> impl Iterator<Item = Self> + 'a {
273            let arena = self.arena;
274            self.arena.nodes[self.idx]
275                .children
276                .clone()
277                .into_iter()
278                .map(move |i| TestNode { arena, idx: i })
279        }
280
281        fn attributes(self) -> impl Iterator<Item = Self> + 'a {
282            let arena = self.arena;
283            self.arena.nodes[self.idx]
284                .attributes
285                .clone()
286                .into_iter()
287                .map(move |i| TestNode { arena, idx: i })
288        }
289
290        fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
291            let arena = self.arena;
292            self.arena.nodes[self.idx]
293                .namespaces
294                .clone()
295                .into_iter()
296                .map(move |i| TestNode { arena, idx: i })
297        }
298
299        fn expanded_name(self) -> Option<ExpandedName> {
300            self.arena.nodes[self.idx].name.clone()
301        }
302
303        fn string_value(self) -> String {
304            let data = &self.arena.nodes[self.idx];
305            match data.kind {
306                NodeKind::Root | NodeKind::Element => {
307                    let mut out = String::new();
308                    collect_text(self.arena, self.idx, &mut out);
309                    out
310                }
311                NodeKind::Attribute
312                | NodeKind::Namespace
313                | NodeKind::ProcessingInstruction
314                | NodeKind::Comment
315                | NodeKind::Text => data.text.clone().unwrap_or_default(),
316            }
317        }
318
319        fn document_order(self, other: Self) -> Ordering {
320            self.idx.cmp(&other.idx)
321        }
322    }
323
324    fn collect_text(arena: &Arena, idx: usize, out: &mut String) {
325        for &child in &arena.nodes[idx].children {
326            match arena.nodes[child].kind {
327                NodeKind::Text => out.push_str(arena.nodes[child].text.as_deref().unwrap_or("")),
328                NodeKind::Element => collect_text(arena, child, out),
329                _ => {}
330            }
331        }
332    }
333
334    /// Named handles into the fixture tree built by [`build`]:
335    ///
336    /// ```text
337    /// Root
338    ///  └─ html (ns: xml=...; attrs: lang="en", id="root-el")
339    ///      └─ body (ns: x=...; attrs: class="main")
340    ///          ├─ text0 "Hello "
341    ///          ├─ span (attrs: data-x="1")
342    ///          │   └─ text1 "World"
343    ///          ├─ comment "note"
344    ///          ├─ pi proc "data"
345    ///          └─ text2 "!"
346    /// ```
347    ///
348    /// `span`'s `data-x` attribute exists specifically as a trap for
349    /// `following`/`preceding`: it sits in document order strictly between
350    /// `span` and `text1`, so it is neither an ancestor nor a descendant
351    /// of `text0`/`text2` — an implementation that excludes attribute/
352    /// namespace nodes from those axes only by filtering out descendants/
353    /// ancestors (instead of also filtering by node kind) would wrongly
354    /// let it leak into `following(text0)` or `preceding(text2)`.
355    pub(crate) struct Fixture {
356        pub doc: TestDoc,
357        pub html: usize,
358        pub html_ns: usize,
359        pub html_lang: usize,
360        pub html_id: usize,
361        pub body: usize,
362        pub body_ns: usize,
363        pub body_class: usize,
364        pub text0: usize,
365        pub span: usize,
366        pub span_attr: usize,
367        pub text1: usize,
368        pub comment: usize,
369        pub pi: usize,
370        pub text2: usize,
371    }
372
373    pub(crate) fn build() -> Fixture {
374        let mut b = Builder { nodes: Vec::new() };
375        let root = b.new_root();
376        let html = b.add_element(root, "html");
377        let html_ns = b.add_namespace(html, "xml", "http://www.w3.org/XML/1998/namespace");
378        let html_lang = b.add_attribute(html, "lang", "en");
379        let html_id = b.add_attribute(html, "id", "root-el");
380        let body = b.add_element(html, "body");
381        let body_ns = b.add_namespace(body, "x", "urn:x");
382        let body_class = b.add_attribute(body, "class", "main");
383        let text0 = b.add_text(body, "Hello ");
384        let span = b.add_element(body, "span");
385        let span_attr = b.add_attribute(span, "data-x", "1");
386        let text1 = b.add_text(span, "World");
387        let comment = b.add_comment(body, "note");
388        let pi = b.add_pi(body, "proc", "data");
389        let text2 = b.add_text(body, "!");
390        Fixture {
391            doc: TestDoc {
392                arena: Arena { nodes: b.nodes },
393            },
394            html,
395            html_ns,
396            html_lang,
397            html_id,
398            body,
399            body_ns,
400            body_class,
401            text0,
402            span,
403            span_attr,
404            text1,
405            comment,
406            pi,
407            text2,
408        }
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::fixture::build;
415    use super::{Node, NodeKind};
416
417    #[test]
418    fn string_value_text() {
419        let f = build();
420        assert_eq!(f.doc.node(f.text0).string_value(), "Hello ");
421    }
422
423    #[test]
424    fn string_value_comment() {
425        let f = build();
426        assert_eq!(f.doc.node(f.comment).string_value(), "note");
427    }
428
429    #[test]
430    fn string_value_processing_instruction() {
431        let f = build();
432        assert_eq!(f.doc.node(f.pi).string_value(), "data");
433    }
434
435    #[test]
436    fn string_value_attribute() {
437        let f = build();
438        assert_eq!(f.doc.node(f.html_lang).string_value(), "en");
439    }
440
441    #[test]
442    fn string_value_namespace() {
443        let f = build();
444        assert_eq!(
445            f.doc.node(f.html_ns).string_value(),
446            "http://www.w3.org/XML/1998/namespace"
447        );
448    }
449
450    #[test]
451    fn string_value_element_concatenates_nested_descendant_text_in_document_order() {
452        let f = build();
453        // span is nested one level deeper than text0/text2 — string-value
454        // must still concatenate across depths, in document order.
455        assert_eq!(f.doc.node(f.span).string_value(), "World");
456        assert_eq!(f.doc.node(f.body).string_value(), "Hello World!");
457        assert_eq!(f.doc.node(f.html).string_value(), "Hello World!");
458    }
459
460    #[test]
461    fn string_value_root_concatenates_all_descendant_text() {
462        let f = build();
463        assert_eq!(f.doc.node(0).string_value(), "Hello World!");
464    }
465
466    #[test]
467    fn expanded_name_per_kind() {
468        let f = build();
469        assert!(f.doc.node(0).expanded_name().is_none()); // Root
470        assert_eq!(
471            f.doc.node(f.html).expanded_name().unwrap().local_name,
472            "html"
473        );
474        assert_eq!(
475            f.doc.node(f.html_lang).expanded_name().unwrap().local_name,
476            "lang"
477        );
478        assert_eq!(
479            f.doc.node(f.html_ns).expanded_name().unwrap().local_name,
480            "xml"
481        );
482        assert_eq!(f.doc.node(f.pi).expanded_name().unwrap().local_name, "proc");
483        assert!(f.doc.node(f.comment).expanded_name().is_none());
484        assert!(f.doc.node(f.text0).expanded_name().is_none());
485    }
486
487    #[test]
488    fn kind_per_node() {
489        let f = build();
490        assert_eq!(f.doc.node(0).kind(), NodeKind::Root);
491        assert_eq!(f.doc.node(f.html).kind(), NodeKind::Element);
492        assert_eq!(f.doc.node(f.html_lang).kind(), NodeKind::Attribute);
493        assert_eq!(f.doc.node(f.html_ns).kind(), NodeKind::Namespace);
494        assert_eq!(f.doc.node(f.pi).kind(), NodeKind::ProcessingInstruction);
495        assert_eq!(f.doc.node(f.comment).kind(), NodeKind::Comment);
496        assert_eq!(f.doc.node(f.text0).kind(), NodeKind::Text);
497    }
498
499    #[test]
500    fn document_order_matches_tree_structure() {
501        let f = build();
502        use std::cmp::Ordering;
503        let root = f.doc.node(0);
504        let html = f.doc.node(f.html);
505        let body = f.doc.node(f.body);
506        let text0 = f.doc.node(f.text0);
507        assert_eq!(root.document_order(html), Ordering::Less);
508        assert_eq!(html.document_order(body), Ordering::Less);
509        assert_eq!(body.document_order(text0), Ordering::Less);
510        assert_eq!(html.document_order(html), Ordering::Equal);
511    }
512}