Skip to main content

sup_xml_core/xpath/
index.rs

1//! Tree-agnostic abstraction over [`super::context::DocIndex`] (legacy tree)
2//! and [`super::context::DocIndex`] (arena tree).
3//!
4//! XPath evaluation in [`super::eval`] is generic over this trait so we have
5//! a single evaluator that works against both tree representations.  The
6//! public entry points ([`super::XPathContext`] and
7//! [`super::XPathContext`]) remain separate parallel types — only the
8//! evaluator internals are shared.
9
10use std::ops::Range;
11
12/// Index into a flat per-document node table.  Zero is the synthetic Document
13/// node; every other index points to the corresponding entry in the per-tree
14/// implementation's `nodes` vector.
15pub type NodeId = usize;
16
17/// Simple, tree-agnostic enum.  Used by eval for node-test matching.  The
18/// per-tree `INodeKind` / `INodeKind` types map onto this via
19/// [`DocIndexLike::kind`].
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum XPathNodeKind {
22    Document,
23    Element,
24    Attribute,
25    Text,
26    Comment,
27    CData,
28    PI,
29    /// Synthetic namespace node — one per in-scope namespace
30    /// binding for an element, materialized during indexing.
31    /// XPath 1.0 §5.4.
32    Namespace,
33}
34
35/// Methods the XPath evaluator needs from any backing tree representation.
36///
37/// Both [`super::context::DocIndex`] and [`super::context::DocIndex`]
38/// implement this trait so eval can run against either.
39pub trait DocIndexLike {
40    /// Graft a parsed [`sup_xml_tree::dom::Document`] into the index
41    /// at runtime, returning the node id of its synthetic document
42    /// root.  Concrete `DocIndex` implementations use the same
43    /// append-only RTF storage that XSLT result-tree fragments use,
44    /// so the call needs only `&self`; stub implementations (test
45    /// shims) return `None` to fall back to the legacy
46    /// "URI not pre-loaded" error path in `document_fn`.
47    fn graft_dynamic_document(
48        &self,
49        _doc: &sup_xml_tree::dom::Document,
50    ) -> Option<NodeId> {
51        None
52    }
53
54    /// Children in document order (content children only — attributes are
55    /// addressed separately via [`attr_range`](Self::attr_range)).
56    fn children(&self, id: NodeId) -> &[NodeId];
57
58    /// Parent node id, or `None` for the synthetic Document node (id 0).
59    fn parent(&self, id: NodeId) -> Option<NodeId>;
60
61    /// Attribute-node id range for an element (empty range for non-elements).
62    fn attr_range(&self, id: NodeId) -> Range<NodeId>;
63
64    /// Namespace-node id range for an element — the synthetic
65    /// namespace nodes that XPath's `namespace::` axis traverses.
66    /// One per distinct in-scope prefix (own + inherited, with
67    /// closer declarations shadowing) plus the implicit `xml`
68    /// prefix.  Empty for non-elements.  Default impl returns an
69    /// empty range; the arena index materialises these eagerly
70    /// during build.
71    fn ns_range(&self, _id: NodeId) -> Range<NodeId> { 0..0 }
72
73    /// Tree-agnostic node-kind discriminant.
74    fn kind(&self, id: NodeId) -> XPathNodeKind;
75
76    /// PI target — only meaningful for [`XPathNodeKind::PI`]; returns `""`
77    /// for other kinds.
78    fn pi_target(&self, id: NodeId) -> &str;
79
80    /// XPath 1.0 § 5 string value of the node.
81    fn string_value(&self, id: NodeId) -> String;
82
83    /// Qualified name (`prefix:local`) — element / attribute / PI; `""` otherwise.
84    fn node_name(&self, id: NodeId) -> &str;
85
86    /// Local part of the QName (everything after the colon, or the whole name).
87    fn local_name(&self, id: NodeId) -> &str;
88
89    /// Namespace URI bound to the element/attribute's prefix; `""` otherwise.
90    fn namespace_uri(&self, id: NodeId) -> &str;
91
92    /// Namespace prefix declared on this element/attribute (e.g. `"dc"`),
93    /// or `None` when the node has no namespace or is the default
94    /// namespace.  Default impl returns `None`; types that carry
95    /// namespace info (the arena index) override.
96    fn namespace_prefix(&self, _id: NodeId) -> Option<&str> { None }
97
98    /// Convenience: `kind(id) == Element`.
99    fn is_element(&self, id: NodeId) -> bool {
100        matches!(self.kind(id), XPathNodeKind::Element)
101    }
102
103    /// True iff `attr_id` is a DTD-declared ID-type attribute or
104    /// matches the `xml:id` convention.  Default returns `true` for
105    /// any attribute whose local name is `id` (libxml2's de facto
106    /// behaviour); indexes built from documents with DTDs override
107    /// to consult the parsed `<!ATTLIST … ID>` declarations.
108    fn is_id_attribute(&self, attr_id: NodeId) -> bool {
109        if !matches!(self.kind(attr_id), XPathNodeKind::Attribute) { return false; }
110        let n = self.node_name(attr_id);
111        n == "xml:id" || self.local_name(attr_id) == "id"
112    }
113
114    /// True iff `attr_id` is a DTD-declared `IDREF`/`IDREFS`-type
115    /// attribute.  Unlike ID there is no DTD-less convention, so the
116    /// default returns `false`; indexes built from documents with DTDs
117    /// override to consult the parsed `<!ATTLIST … IDREF>` declarations.
118    /// Consulted by XPath 2.0 §14.5.5's `idref()`.
119    fn is_idref_attribute(&self, _attr_id: NodeId) -> bool { false }
120
121    /// Allocate one synthetic text node per supplied string and
122    /// return their `NodeId`s.  Used by EXSLT functions
123    /// (`str:tokenize`, `str:split`, `regexp:match`) that need to
124    /// return a node-set of computed strings without an XSLT-engine
125    /// RTF arena to host them.  Default returns `None` — the index
126    /// implementation doesn't support runtime allocation.  Indexes
127    /// that do (the arena `DocIndex`) override to return the new
128    /// IDs in document order.
129    fn allocate_rtf_text_nodes(&self, _values: Vec<String>) -> Option<Vec<NodeId>> { None }
130
131    /// Start an append-only RTF subtree the caller can build into via
132    /// the returned [`RtfBuilder`].  The builder's
133    /// `add_document` / `add_element` / `add_text` / `add_attribute`
134    /// API populates a fresh tree; callers hand it back to
135    /// [`finish_rtf`](Self::finish_rtf) to publish the resulting
136    /// `RtfIndex` into the arena.  Default returns `None` — indexes
137    /// without arena storage (test shims, foreign-doc wrappers) can
138    /// signal "no RTF construction support" here.  The concrete arena
139    /// `DocIndex` overrides to return a real builder.
140    fn rtf_builder(&self) -> Option<super::rtf::RtfBuilder> { None }
141
142    /// Publish a populated [`RtfBuilder`] into the arena, returning
143    /// the global id of the synthetic document root.  Mirrors
144    /// [`rtf_builder`](Self::rtf_builder); default returns `None`.
145    fn finish_rtf(&self, _builder: super::rtf::RtfBuilder) -> Option<NodeId> { None }
146
147    /// Schema-aware: governing type `(ns, local)` of a constructed RTF
148    /// node built with a `type=` / `xsl:type=` annotation, if any.
149    /// Default `None` — indexes without RTF type tracking report every
150    /// constructed node as untyped.
151    fn rtf_node_type(&self, _id: NodeId) -> Option<(String, String)> { None }
152
153    /// True when `id` is a synthetic RTF doc-wrap that holds the
154    /// items of a sequence-typed XSLT binding rather than a real
155    /// XML / XSLT document tree.  Used by XSLT's XTDE1270 / XTDE1370
156    /// / XTDE1380 to refuse the wrap as a document root when the
157    /// spec asks for the "root of the tree containing the context
158    /// node".  Default `false` — index shims without sequence-typed
159    /// binding support never produce such wraps.
160    fn is_synthetic_wrap(&self, _id: NodeId) -> bool { false }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    /// Minimal `DocIndexLike` that overrides only the required methods,
168    /// so the trait's default impls (`ns_range`, `namespace_prefix`,
169    /// `is_element`) are the ones under test.
170    ///
171    /// Layout: id 0 is the synthetic Document, id 1 is an Element child,
172    /// id 2 is a Text child of the element.
173    struct StubIndex;
174
175    impl DocIndexLike for StubIndex {
176        fn children(&self, id: NodeId) -> &[NodeId] {
177            match id {
178                0 => &[1],
179                1 => &[2],
180                _ => &[],
181            }
182        }
183        fn parent(&self, id: NodeId) -> Option<NodeId> {
184            match id {
185                0 => None,
186                1 => Some(0),
187                2 => Some(1),
188                _ => None,
189            }
190        }
191        fn attr_range(&self, _id: NodeId) -> Range<NodeId> { 0..0 }
192        fn kind(&self, id: NodeId) -> XPathNodeKind {
193            match id {
194                0 => XPathNodeKind::Document,
195                1 => XPathNodeKind::Element,
196                2 => XPathNodeKind::Text,
197                _ => XPathNodeKind::Text,
198            }
199        }
200        fn pi_target(&self, _id: NodeId) -> &str { "" }
201        fn string_value(&self, _id: NodeId) -> String { String::new() }
202        fn node_name(&self, _id: NodeId) -> &str { "" }
203        fn local_name(&self, _id: NodeId) -> &str { "" }
204        fn namespace_uri(&self, _id: NodeId) -> &str { "" }
205        // ns_range, namespace_prefix, is_element intentionally NOT
206        // overridden — the trait defaults are under test.
207    }
208
209    #[test]
210    fn default_ns_range_is_empty() {
211        let idx = StubIndex;
212        // The Element node (id 1) has no namespace info in the stub, so
213        // the default should return an empty range regardless of id.
214        for id in 0..=2 {
215            let r = idx.ns_range(id);
216            assert_eq!(r, 0..0, "ns_range({id}) should default to empty");
217            assert!(r.is_empty());
218        }
219    }
220
221    #[test]
222    fn default_namespace_prefix_is_none() {
223        let idx = StubIndex;
224        for id in 0..=2 {
225            assert_eq!(idx.namespace_prefix(id), None);
226        }
227    }
228
229    #[test]
230    fn default_is_element_matches_kind() {
231        let idx = StubIndex;
232        assert!(!idx.is_element(0));         // Document
233        assert!( idx.is_element(1));         // Element
234        assert!(!idx.is_element(2));         // Text
235    }
236
237    #[test]
238    fn stub_required_methods_smoke() {
239        // The required methods on StubIndex are needed for the impl to
240        // exist, but the defaults are what's under test in this module.
241        // Touch each required method once so coverage isn't pulled down
242        // by the test-only stub.
243        let idx = StubIndex;
244        assert_eq!(idx.children(0), &[1]);
245        assert_eq!(idx.children(1), &[2]);
246        assert!(idx.children(2).is_empty());
247        assert_eq!(idx.parent(0), None);
248        assert_eq!(idx.parent(1), Some(0));
249        assert_eq!(idx.parent(2), Some(1));
250        assert_eq!(idx.parent(99), None);
251        assert!(idx.attr_range(1).is_empty());
252        assert_eq!(idx.kind(0), XPathNodeKind::Document);
253        assert_eq!(idx.kind(1), XPathNodeKind::Element);
254        assert_eq!(idx.kind(2), XPathNodeKind::Text);
255        assert_eq!(idx.kind(99), XPathNodeKind::Text);
256        assert_eq!(idx.pi_target(0), "");
257        assert_eq!(idx.string_value(0), "");
258        assert_eq!(idx.node_name(0), "");
259        assert_eq!(idx.local_name(0), "");
260        assert_eq!(idx.namespace_uri(0), "");
261    }
262
263    #[test]
264    fn xpath_node_kind_traits() {
265        // Exercise the derived impls on XPathNodeKind so they aren't
266        // listed as uncovered functions.
267        let k = XPathNodeKind::Element;
268        let copy = k;                                  // Copy
269        let clone = k.clone();                         // Clone
270        assert_eq!(k, copy);                           // PartialEq / Eq
271        assert_eq!(k, clone);
272        assert_ne!(k, XPathNodeKind::Text);
273        // Debug
274        let s = format!("{:?}", k);
275        assert!(s.contains("Element"));
276        // All variants distinct.
277        let variants = [
278            XPathNodeKind::Document,
279            XPathNodeKind::Element,
280            XPathNodeKind::Attribute,
281            XPathNodeKind::Text,
282            XPathNodeKind::Comment,
283            XPathNodeKind::CData,
284            XPathNodeKind::PI,
285            XPathNodeKind::Namespace,
286        ];
287        for (i, a) in variants.iter().enumerate() {
288            for (j, b) in variants.iter().enumerate() {
289                assert_eq!(i == j, a == b);
290            }
291        }
292    }
293}