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}