Skip to main content

sup_xml_core/xpath/
context.rs

1//! Arena-tree counterpart to [`super::context::DocIndex`].
2//!
3//! Builds the same flat `Vec<INode>` shape with `NodeId` indices, but walks
4//! the arena tree ([`sup_xml_tree::dom::Document`]) via its linked-list
5//! children/attributes API instead of the legacy `Vec<Node>` fields.  Both
6//! types implement [`super::index::DocIndexLike`], so the XPath evaluator
7//! in [`super::eval`] works against either with no per-tree-type code.
8
9use sup_xml_tree::dom::{Attribute as ArenaAttr, Document as ArenaDoc, Node as ArenaNode, NodeKind as ArenaKind};
10
11use super::index::{DocIndexLike, NodeId, XPathNodeKind};
12
13/// Per-node entry in the flat index.
14pub struct INode<'doc> {
15    pub kind: INodeKind<'doc>,
16    pub parent: Option<NodeId>,
17    /// IDs of attribute nodes (elements only).  Half-open range.
18    pub attr_start: usize,
19    pub attr_end:   usize,
20    /// IDs of synthetic namespace nodes (elements only).  Half-open
21    /// range.  Empty when this element has no in-scope namespace
22    /// bindings — which, given the implicit `xml` prefix, only
23    /// applies to non-elements.
24    pub ns_start: usize,
25    pub ns_end:   usize,
26    /// IDs of content children (non-attribute).
27    pub content_children: Vec<NodeId>,
28}
29
30/// Discriminant for [`INode::kind`].  The Element/Attribute/etc. variants
31/// carry the original arena reference for cheap accessor methods.
32pub enum INodeKind<'doc> {
33    Document,
34    Element(&'doc ArenaNode<'doc>),
35    Attribute(&'doc ArenaAttr<'doc>),
36    Text(&'doc ArenaNode<'doc>),
37    Comment(&'doc ArenaNode<'doc>),
38    CData(&'doc ArenaNode<'doc>),
39    PI(&'doc ArenaNode<'doc>),
40    /// Synthetic namespace node — materialised during indexing
41    /// from each element's in-scope namespace bindings.  `parent`
42    /// is the element this binding is observed on; `prefix` is
43    /// `None` for the default namespace (`xmlns="…"`); `uri` is
44    /// the namespace URI.  See XPath 1.0 §5.4.
45    Namespace { prefix: Option<&'doc str>, uri: &'doc str },
46}
47
48/// The implicit `xml` prefix every element carries per the XML
49/// Namespaces recommendation.  Stored as `&'static str` so it
50/// reborrows into any `&'doc str` slot.
51const XML_NS_PREFIX: &str = "xml";
52const XML_NS_URI:    &str = "http://www.w3.org/XML/1998/namespace";
53
54pub struct DocIndex<'doc> {
55    pub nodes: Vec<INode<'doc>>,
56    /// DTD-declared ID-attribute map (element-name → list of
57    /// attribute-names typed as `ID`).  Snapshotted from the source
58    /// [`ArenaDoc`] at index-build time so `id()` can consult it
59    /// without owning a Document reference.  Empty when the doc had
60    /// no DTD ID-typed attributes.
61    id_attrs: std::sync::Arc<std::collections::HashMap<String, Vec<String>>>,
62    /// DTD-declared IDREF/IDREFS-attribute map (element-name → list of
63    /// attribute-names typed as `IDREF`/`IDREFS`).  Snapshotted like
64    /// [`id_attrs`](Self::id_attrs) so `idref()` (XPath 2.0 §14.5.5)
65    /// can find the referencing attributes.  Empty when the doc had no
66    /// DTD IDREF-typed attributes.
67    idref_attrs: std::sync::Arc<std::collections::HashMap<String, Vec<String>>>,
68    /// Append-only store of EXSLT result-tree-fragment text nodes
69    /// allocated at runtime by `str:tokenize` / `str:split` /
70    /// `regexp:match`.  Each entry holds the text content; the
71    /// corresponding `NodeId` is `SYNTHETIC_TEXT_BASE | index`.
72    /// Interior-mutable so allocation can happen through the
73    /// `&DocIndex` references the evaluator holds.
74    synthetic: std::cell::RefCell<Vec<String>>,
75    /// Result-tree-fragment store for XSLT body-form `xsl:variable`
76    /// bindings — see [`super::rtf`] for the full design.  Held as
77    /// a [`elsa::FrozenVec`] of `Box<RtfIndex>` so the XSLT engine
78    /// can `push_rtf` through `&DocIndex` while every `&RtfIndex`
79    /// (and the `&[NodeId]` slices it hands out) stays stable for
80    /// the rest of the evaluation.  Each entry is built complete-
81    /// then-frozen; no in-place mutation after registration.
82    rtfs: elsa::FrozenVec<Box<super::rtf::RtfIndex>>,
83    /// Synthetic RTF document-roots that wrap a sequence-typed XSLT
84    /// binding (XSLT 2.0 §5.7.2 / §9.3).  Per spec, items in a
85    /// sequence-typed `as="element()" / element()* / text()*`
86    /// variable are parentless; the engine stores them as children
87    /// of a doc-root for storage convenience.  XTDE1270 / XTDE1370
88    /// / XTDE1380 ("the root of the tree containing the context
89    /// node is not a document") must therefore see THROUGH these
90    /// wraps and refuse them as document roots.  Interior-mutable so
91    /// the XSLT engine can mark wraps via the `&DocIndex` it holds.
92    synthetic_wraps: std::cell::RefCell<std::collections::HashSet<NodeId>>,
93    /// Schema-aware: governing type `(ns, local)` for RTF (constructed-
94    /// tree) nodes built with a `type=` / `xsl:type=` annotation.  Keyed
95    /// by the node's globally-encoded RTF id.  Populated by
96    /// [`finish_rtf`](Self::finish_rtf) draining the builder's
97    /// `typed_nodes`; read back via [`rtf_node_type`](Self::rtf_node_type)
98    /// so `data()` / `instance of` see a constructed node's declared
99    /// type.  Empty unless schema-aware construction occurred.
100    rtf_node_types: std::cell::RefCell<std::collections::HashMap<NodeId, Box<(String, String)>>>,
101}
102
103/// Top-bit-set IDs are EXSLT-synthesised text nodes.  The remaining
104/// bits index into `DocIndex::synthetic`.  Using the top bit lets
105/// every `DocIndexLike` method dispatch in a single mask test, and
106/// guarantees synthetic IDs sort after every real index entry — so
107/// document order between real and synthetic node-sets is total.
108pub const SYNTHETIC_TEXT_BASE: NodeId = 1_usize << (usize::BITS - 1);
109
110
111impl<'doc> DocIndex<'doc> {
112    /// Build a flat index over `doc`.  O(n) in the number of nodes.
113    pub fn build(doc: &'doc ArenaDoc) -> Self {
114        let mut idx = Self {
115            nodes: Vec::new(),
116            id_attrs: doc.id_attributes().clone(),
117            idref_attrs: doc.idref_attributes().clone(),
118            synthetic: std::cell::RefCell::new(Vec::new()),
119            rtfs:      elsa::FrozenVec::new(),
120            synthetic_wraps: std::cell::RefCell::new(std::collections::HashSet::new()),
121            rtf_node_types:  std::cell::RefCell::new(std::collections::HashMap::new()),
122        };
123        // Node 0: synthetic Document.
124        idx.nodes.push(INode {
125            kind: INodeKind::Document,
126            parent: None,
127            attr_start: 0, attr_end: 0,
128            ns_start:   0, ns_end:   0,
129            content_children: Vec::new(),
130        });
131        // Walk the document-level chain (prolog comments/PIs, root,
132        // then epilogue comments/PIs) so XPath sees them as children
133        // of the document node per XPath 1.0 §5.1.
134        let mut top: Vec<NodeId> = Vec::new();
135        let mut cur: Option<&'doc ArenaNode<'doc>> = Some(doc.first_sibling());
136        while let Some(n) = cur {
137            top.extend(idx.add_node(n, 0));
138            cur = n.next_sibling.get();
139        }
140        idx.nodes[0].content_children = top;
141        idx
142    }
143
144    /// Append another document's tree to this index, returning the
145    /// NodeId of the new synthetic Document node.  Used to make
146    /// runtime-loaded documents (e.g. via the XSLT `document()`
147    /// function or XInclude resolution) addressable through the
148    /// same XPath index as the primary source.
149    ///
150    /// The added doc must outlive `'doc` — typically achieved by
151    /// owning it in a `Vec<Box<Document>>` that lives at least as
152    /// long as the index.
153    pub fn add_document(&mut self, doc: &'doc ArenaDoc) -> NodeId {
154        let doc_id = self.nodes.len();
155        self.nodes.push(INode {
156            kind: INodeKind::Document,
157            parent: None,
158            attr_start: 0, attr_end: 0,
159            ns_start:   0, ns_end:   0,
160            content_children: Vec::new(),
161        });
162        let mut top: Vec<NodeId> = Vec::new();
163        let mut cur: Option<&'doc ArenaNode<'doc>> = Some(doc.first_sibling());
164        while let Some(n) = cur {
165            top.extend(self.add_node(n, doc_id));
166            cur = n.next_sibling.get();
167        }
168        self.nodes[doc_id].content_children = top;
169        // Merge the grafted doc's DTD-declared ID-attribute typing
170        // into ours.  Same element name in two docs is rare in
171        // practice; on collision we extend the existing list so
172        // either DTD's typing keeps working.
173        if !doc.id_attributes().is_empty() {
174            let mut merged = (*self.id_attrs).clone();
175            for (elem, ids) in doc.id_attributes().iter() {
176                merged.entry(elem.clone())
177                    .or_insert_with(Vec::new)
178                    .extend(ids.iter().cloned());
179            }
180            self.id_attrs = std::sync::Arc::new(merged);
181        }
182        if !doc.idref_attributes().is_empty() {
183            let mut merged = (*self.idref_attrs).clone();
184            for (elem, refs) in doc.idref_attributes().iter() {
185                merged.entry(elem.clone())
186                    .or_insert_with(Vec::new)
187                    .extend(refs.iter().cloned());
188            }
189            self.idref_attrs = std::sync::Arc::new(merged);
190        }
191        doc_id
192    }
193
194    fn add_node(&mut self, node: &'doc ArenaNode<'doc>, parent: NodeId) -> Vec<NodeId> {
195        match node.kind {
196            ArenaKind::Element => {
197                let id = self.nodes.len();
198                self.nodes.push(INode {
199                    kind: INodeKind::Element(node),
200                    parent: Some(parent),
201                    attr_start: 0, attr_end: 0,
202                    ns_start:   0, ns_end:   0,
203                    content_children: Vec::new(),
204                });
205                // Materialise namespace nodes (XPath 1.0 §5.4) *before* the
206                // attributes: the data model places an element's namespace
207                // nodes ahead of its attribute nodes in document order, and
208                // NodeId order is the document order that dedup_sort()
209                // relies on (so e.g. `namespace::* | attribute::*` sorts
210                // correctly).  Both stay adjacent to the element, before its
211                // content children, so that assumption holds.
212                let ns_start = self.nodes.len();
213                for (prefix, uri) in collect_in_scope_namespaces(node) {
214                    self.nodes.push(INode {
215                        kind: INodeKind::Namespace { prefix, uri },
216                        parent: Some(id),
217                        attr_start: 0, attr_end: 0,
218                        ns_start:   0, ns_end:   0,
219                        content_children: Vec::new(),
220                    });
221                }
222                let ns_end = self.nodes.len();
223                self.nodes[id].ns_start = ns_start;
224                self.nodes[id].ns_end   = ns_end;
225
226                let attr_start = self.nodes.len();
227                for attr in node.attributes() {
228                    // Namespace declarations sometimes land in the
229                    // attribute list (parser-path-dependent); they
230                    // live on the namespace axis, not the attribute
231                    // axis, so skip them here.  XPath 1.0 §5.3 says
232                    // namespace declarations are not attributes.
233                    let name = attr.name();
234                    if name == "xmlns" || name.starts_with("xmlns:") { continue; }
235                    self.nodes.push(INode {
236                        kind: INodeKind::Attribute(attr),
237                        parent: Some(id),
238                        attr_start: 0, attr_end: 0,
239                        ns_start:   0, ns_end:   0,
240                        content_children: Vec::new(),
241                    });
242                }
243                let attr_end = self.nodes.len();
244                self.nodes[id].attr_start = attr_start;
245                self.nodes[id].attr_end   = attr_end;
246
247                let mut children = Vec::new();
248                for child in node.children() {
249                    children.extend(self.add_node(child, id));
250                }
251                self.nodes[id].content_children = children;
252                vec![id]
253            }
254            ArenaKind::Text | ArenaKind::Comment | ArenaKind::CData | ArenaKind::Pi => {
255                let id = self.nodes.len();
256                let kind = match node.kind {
257                    ArenaKind::Text    => INodeKind::Text(node),
258                    ArenaKind::Comment => INodeKind::Comment(node),
259                    ArenaKind::CData   => INodeKind::CData(node),
260                    ArenaKind::Pi      => INodeKind::PI(node),
261                    _                  => unreachable!(),
262                };
263                self.nodes.push(INode {
264                    kind,
265                    parent: Some(parent),
266                    attr_start: 0, attr_end: 0,
267                    ns_start:   0, ns_end:   0,
268                    content_children: Vec::new(),
269                });
270                vec![id]
271            }
272            // c-abi-only discriminant; never appears on a real Node.
273            ArenaKind::Attribute => unreachable!("Attribute kind never appears on a Node"),
274            ArenaKind::Document  => unreachable!("Document kind never appears on a Node"),
275            // Entity references — XPath data model doesn't expose
276            // them; they're either expanded earlier in the parser
277            // or appear as opaque placeholders we can skip.
278            ArenaKind::EntityRef => vec![],
279            // DocumentFragment is a compat-shim transient — produced
280            // by `xmlNewDocFragment` and grafted into a real tree
281            // before any XPath context is built over it.  If one
282            // shows up here, treat it as a skip-and-emit-nothing
283            // node (same as we do for EntityRef).
284            ArenaKind::DocumentFragment => vec![],
285            // The DTD internal subset is not part of the XPath data
286            // model — skip the node and its declaration body.
287            ArenaKind::DtdDecl => vec![],
288            ArenaKind::Dtd => vec![],
289        }
290    }
291
292    /// XPath 1.0 § 5 string value of a node.
293    pub fn string_value(&self, id: NodeId) -> String {
294        if let Some((rtf, local)) = self.rtf_at(id) {
295            return rtf.string_value(local);
296        }
297        if is_synthetic(id) {
298            let i = id & !SYNTHETIC_TEXT_BASE;
299            return self.synthetic.borrow().get(i).cloned().unwrap_or_default();
300        }
301        match &self.nodes[id].kind {
302            INodeKind::Document | INodeKind::Element(_) => {
303                let mut s = String::new();
304                self.concat_text(id, &mut s);
305                s
306            }
307            INodeKind::Attribute(a) => a.value().to_string(),
308            INodeKind::Text(n) | INodeKind::CData(n) => n.content().to_string(),
309            INodeKind::Comment(n)                          => n.content().to_string(),
310            INodeKind::PI(n)                               => n.content().to_string(),
311            // Namespace node's string value is its URI (XPath §5.4).
312            INodeKind::Namespace { uri, .. }               => (*uri).to_string(),
313        }
314    }
315
316    fn concat_text(&self, id: NodeId, out: &mut String) {
317        for &child in &self.nodes[id].content_children {
318            match &self.nodes[child].kind {
319                INodeKind::Text(n) | INodeKind::CData(n) => out.push_str(n.content()),
320                INodeKind::Element(_) => self.concat_text(child, out),
321                _ => {}
322            }
323        }
324    }
325
326    pub fn node_name(&self, id: NodeId) -> &str {
327        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.node_name(local); }
328        if is_synthetic(id) { return ""; }
329        match &self.nodes[id].kind {
330            INodeKind::Element(n) => n.name(),
331            INodeKind::Attribute(a) => a.name(),
332            INodeKind::PI(n) => n.name(),
333            // Namespace node's "name" is its prefix, or "" for the
334            // default namespace binding (XPath §5.4).
335            INodeKind::Namespace { prefix, .. } => prefix.unwrap_or(""),
336            _ => "",
337        }
338    }
339
340    pub fn local_name(&self, id: NodeId) -> &str {
341        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.local_name(local); }
342        if is_synthetic(id) { return ""; }
343        // Namespace nodes don't carry a colon-prefixed form; the
344        // local name equals the binding's prefix (empty for default).
345        if let INodeKind::Namespace { prefix, .. } = &self.nodes[id].kind {
346            return prefix.unwrap_or("");
347        }
348        let full = self.node_name(id);
349        match full.split_once(':') {
350            Some((_, local)) => local,
351            None => full,
352        }
353    }
354
355    pub fn namespace_uri(&self, id: NodeId) -> &str {
356        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.namespace_uri(local); }
357        if is_synthetic(id) { return ""; }
358        match &self.nodes[id].kind {
359            INodeKind::Element(n)   => n.namespace.get().map(|ns| ns.href()).unwrap_or(""),
360            INodeKind::Attribute(a) => a.namespace.get().map(|ns| ns.href()).unwrap_or(""),
361            // Per XPath §5.4, the expanded-name of a namespace node
362            // has a null namespace URI — distinct from the URI the
363            // namespace node *binds*, which is its string value.
364            _ => "",
365        }
366    }
367
368    pub fn namespace_prefix(&self, id: NodeId) -> Option<&str> {
369        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.namespace_prefix(local); }
370        if is_synthetic(id) { return None; }
371        match &self.nodes[id].kind {
372            INodeKind::Element(n)   => n.namespace.get().and_then(|ns| ns.prefix()),
373            INodeKind::Attribute(a) => a.namespace.get().and_then(|ns| ns.prefix()),
374            _ => None,
375        }
376    }
377}
378
379/// Collect the in-scope namespace bindings for an element, in
380/// XPath 1.0 § 5.4 semantics:
381///
382/// * Walk the ancestor-or-self chain, accumulating `xmlns:p` and
383///   `xmlns` declarations.  Closer declarations shadow further ones.
384/// * Bindings declared with an empty URI (`xmlns:p=""`, `xmlns=""`)
385///   are *undeclarations* — they remove the prefix from the
386///   in-scope set rather than appearing as a namespace node.
387/// * Always append the implicit `xml` prefix (the XML Namespaces
388///   recommendation declares it on every element), unless the user
389///   somehow shadowed it.
390///
391/// Returns `(prefix, uri)` pairs.  Order matters only for stable
392/// document-order reporting on `namespace::*`; we use declaration
393/// order: own bindings first, then each ancestor's, with shadowing.
394fn collect_in_scope_namespaces<'doc>(
395    el: &'doc ArenaNode<'doc>,
396) -> Vec<(Option<&'doc str>, &'doc str)> {
397    // (prefix, uri) — prefix=None means default namespace.
398    let mut out: Vec<(Option<&'doc str>, &'doc str)> = Vec::new();
399    let mut seen_default = false;
400    let mut seen_xml     = false;
401
402    let mut cur: Option<&'doc ArenaNode<'doc>> = Some(el);
403    while let Some(n) = cur {
404        for (prefix, uri) in n.ns_declarations() {
405            let already = out.iter().any(|(p, _)| *p == prefix)
406                || (prefix.is_none() && seen_default);
407            if already {
408                // Shadowed by a closer declaration; skip.
409                continue;
410            }
411            if prefix.is_none() {
412                seen_default = true;
413            }
414            if uri.is_empty() {
415                // Undeclaration — record that we've seen this
416                // prefix so further (ancestor) declarations don't
417                // re-introduce it, but don't emit a namespace node.
418                continue;
419            }
420            if prefix == Some(XML_NS_PREFIX) {
421                seen_xml = true;
422            }
423            out.push((prefix, uri));
424        }
425        cur = n.parent.get();
426    }
427
428    if !seen_xml {
429        out.push((Some(XML_NS_PREFIX), XML_NS_URI));
430    }
431    out
432}
433
434// ── EXSLT RTF allocator ─────────────────────────────────────────────────────
435
436impl<'doc> DocIndex<'doc> {
437    /// Allocate one synthetic text node per supplied string and
438    /// return their `NodeId`s in document order.  Used by EXSLT
439    /// functions like `str:tokenize`, `str:split`, and
440    /// `regexp:match` that need to materialise a node-set of
441    /// strings without an XSLT-engine RTF arena to host them.
442    ///
443    /// The nodes are top-level — they have no parent and no
444    /// document — but every `DocIndexLike` accessor handles them
445    /// correctly so callers can iterate the result with
446    /// `xsl:for-each`, take their string-value, count them, etc.
447    /// Their IDs sort after every real node-set node, so unions
448    /// stay totally ordered.
449    pub fn allocate_rtf_text_nodes_inherent(&self, values: Vec<String>) -> Vec<NodeId> {
450        let mut store = self.synthetic.borrow_mut();
451        let start = store.len();
452        store.extend(values);
453        (start..store.len())
454            .map(|i| SYNTHETIC_TEXT_BASE | i)
455            .collect()
456    }
457
458}
459
460/// True iff `id`'s top bit is set — i.e. it points into the EXSLT
461/// synthetic-text store rather than the main `nodes` table.  Inlined
462/// at every `DocIndexLike` accessor to short-circuit before the real
463/// table lookup.
464#[inline(always)]
465pub fn is_synthetic_id(id: NodeId) -> bool { is_synthetic(id) }
466
467pub(crate) fn is_synthetic(id: NodeId) -> bool {
468    id & SYNTHETIC_TEXT_BASE != 0
469}
470
471// ── RTF (result-tree-fragment) hosting ─────────────────────────────
472
473impl<'doc> DocIndex<'doc> {
474    /// Begin building a new result-tree fragment.  Returns an
475    /// [`RtfBuilder`](super::rtf::RtfBuilder) that knows its
476    /// eventual host-vector slot — callers populate it via
477    /// `add_document` / `add_element` / etc., then hand it back to
478    /// [`finish_rtf`](Self::finish_rtf).
479    pub fn start_rtf(&self) -> super::rtf::RtfBuilder {
480        let slot = self.rtfs.len();
481        super::rtf::RtfBuilder::new(slot)
482    }
483
484    /// Mark `root` (an RTF document-root NodeId) as the synthetic
485    /// wrap of a sequence-typed XSLT binding.  See
486    /// [`synthetic_wraps`](Self::synthetic_wraps) for the rationale.
487    pub fn mark_synthetic_wrap(&self, root: NodeId) {
488        self.synthetic_wraps.borrow_mut().insert(root);
489    }
490
491    /// True when `id` is a synthetic doc-wrap created for
492    /// sequence-typed binding storage rather than a real source /
493    /// XSLT document tree root.  Used by XTDE1270 / XTDE1370 /
494    /// XTDE1380 to refuse the wrap as a document for the purposes
495    /// of `fn:key` / `fn:unparsed-entity-uri` and friends.
496    pub fn is_synthetic_wrap(&self, id: NodeId) -> bool {
497        self.synthetic_wraps.borrow().contains(&id)
498    }
499
500    /// Freeze a populated builder into the host vector.  Returns
501    /// the globally-encoded id of the RTF's document node so the
502    /// caller can bind a variable directly to it.
503    pub fn finish_rtf(&self, mut builder: super::rtf::RtfBuilder) -> NodeId {
504        let host_index = builder.host_index;
505        // Drain any schema-type annotations the builder collected for
506        // constructed nodes into the index's PSVI table (the encoded
507        // ids are already global, so they're valid keys regardless of
508        // when the RTF slot is pushed below).
509        if !builder.typed_nodes.is_empty() {
510            let mut tbl = self.rtf_node_types.borrow_mut();
511            for (id, ty) in builder.typed_nodes.drain(..) {
512                tbl.insert(id, ty);
513            }
514        }
515        let rtf = builder.build();
516        // FrozenVec::push takes shared &self and returns &T with
517        // a lifetime tied to the FrozenVec.  Boxes stay put; only
518        // the outer vector's tail grows.
519        self.rtfs.push(Box::new(rtf));
520        super::rtf::encode_rtf_id(host_index, 0)
521    }
522
523    /// Governing type `(ns, local)` recorded for a constructed RTF node,
524    /// if it was built with a `type=` / `xsl:type=` annotation.
525    pub fn rtf_node_type(&self, id: NodeId) -> Option<(String, String)> {
526        self.rtf_node_types.borrow().get(&id).map(|b| (**b).clone())
527    }
528
529    /// Graft a fully-parsed [`sup_xml_tree::dom::Document`] into the
530    /// index at runtime, returning the node id of its synthetic
531    /// document root.  Used by XSLT 2.0 `doc()` / XSLT 1.0
532    /// `document()` to resolve URIs computed at apply time (rather
533    /// than statically discovered at compile time).
534    ///
535    /// The grafted doc reuses the [`super::rtf`] storage: every
536    /// node's data is copied into the RTF's append-only `nodes`
537    /// table so the caller can drop the source [`Document`] right
538    /// after the call.  XPath operations on the returned node id
539    /// dispatch through the same RTF accessors that XSLT-built
540    /// result-tree fragments use.  Bytes-for-bytes structural
541    /// fidelity is preserved (PIs, comments, attributes,
542    /// namespace declarations); doc-level XSD typing and unparsed-
543    /// entity metadata are not — the spec leaves those undefined
544    /// for `doc()`-loaded resources.
545    pub fn graft_dynamic_document(
546        &self,
547        doc: &sup_xml_tree::dom::Document,
548    ) -> NodeId {
549        let mut builder = self.start_rtf();
550        let root = builder.add_document();
551        // Walk the document's top-level chain.  `first_sibling`
552        // points at the first prolog comment / PI / element; the
553        // chain ends when `next_sibling` is `None`.
554        let mut cur = Some(doc.first_sibling());
555        while let Some(n) = cur {
556            graft_node(&mut builder, root, n);
557            cur = n.next_sibling.get();
558        }
559        self.finish_rtf(builder)
560    }
561}
562
563/// Recursive helper for [`DocIndex::graft_dynamic_document`].
564/// Translates one `Node` (and its descendants) into the
565/// corresponding `RtfBuilder` calls, preserving structural shape.
566fn graft_node(
567    builder: &mut super::rtf::RtfBuilder,
568    parent:  NodeId,
569    node:    &sup_xml_tree::dom::Node<'_>,
570) {
571    use sup_xml_tree::dom::NodeKind;
572    match node.kind {
573        NodeKind::Element => {
574            let name   = node.name();
575            let prefix = name.rfind(':').map(|i| &name[..i]);
576            let uri    = node.namespace.get().map(|ns| ns.href()).unwrap_or("");
577            let elem   = builder.add_element(parent, name, uri, prefix);
578            // Attributes first so the attribute-axis enumeration
579            // reflects source order.
580            let mut attrs = node.attributes().peekable();
581            if attrs.peek().is_some() {
582                builder.start_attrs(elem);
583                for a in attrs {
584                    let an      = a.name();
585                    let aprefix = an.rfind(':').map(|i| &an[..i]);
586                    let auri    = a.namespace.get().map(|ns| ns.href()).unwrap_or("");
587                    builder.add_attribute(elem, an, auri, aprefix, a.value());
588                }
589            }
590            for child in node.children() {
591                graft_node(builder, elem, child);
592            }
593        }
594        NodeKind::Text | NodeKind::CData => {
595            builder.add_text(parent, node.content());
596        }
597        NodeKind::Comment => {
598            builder.add_comment(parent, node.content());
599        }
600        NodeKind::Pi => {
601            builder.add_pi(parent, node.name(), node.content());
602        }
603        // Document / Attribute / EntityRef / etc. don't appear as
604        // children of a Document or Element at this level in the
605        // arena (Document is the top, Attribute hangs off an
606        // element's attr chain).  Skip silently.
607        _ => {}
608    }
609}
610
611// ── DocIndexLike (arena tree) ───────────────────────────────────────────────
612
613impl<'doc> DocIndex<'doc> {
614    /// Resolve an RTF-marked id to its host [`super::rtf::RtfIndex`] +
615    /// local node id.  Inlined into the dispatch hot path on every
616    /// accessor so the FrozenVec deref + decode cost is one cmov on
617    /// real-source-tree IDs (the marker bit is zero) and a single
618    /// shift + index on RTF IDs.
619    #[inline(always)]
620    fn rtf_at(&self, id: NodeId) -> Option<(&super::rtf::RtfIndex, NodeId)> {
621        if !super::rtf::is_rtf_id(id) { return None; }
622        let (host_i, local) = super::rtf::decode_rtf_id(id);
623        self.rtfs.get(host_i).map(|r| (r, local))
624    }
625}
626
627impl<'doc> DocIndexLike for DocIndex<'doc> {
628    fn graft_dynamic_document(
629        &self,
630        doc: &sup_xml_tree::dom::Document,
631    ) -> Option<NodeId> {
632        Some(DocIndex::graft_dynamic_document(self, doc))
633    }
634    fn children(&self, id: NodeId) -> &[NodeId] {
635        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.children(local); }
636        if is_synthetic(id) { return &[]; }
637        &self.nodes[id].content_children
638    }
639    fn parent(&self, id: NodeId) -> Option<NodeId> {
640        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.parent(local); }
641        if is_synthetic(id) { return None; }
642        self.nodes[id].parent
643    }
644    fn attr_range(&self, id: NodeId) -> std::ops::Range<NodeId> {
645        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.attr_range(local); }
646        if is_synthetic(id) { return 0..0; }
647        self.nodes[id].attr_start..self.nodes[id].attr_end
648    }
649    fn ns_range(&self, id: NodeId) -> std::ops::Range<NodeId> {
650        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.ns_range(local); }
651        if is_synthetic(id) { return 0..0; }
652        self.nodes[id].ns_start..self.nodes[id].ns_end
653    }
654    fn kind(&self, id: NodeId) -> XPathNodeKind {
655        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.kind(local); }
656        if is_synthetic(id) { return XPathNodeKind::Text; }
657        match self.nodes[id].kind {
658            INodeKind::Document        => XPathNodeKind::Document,
659            INodeKind::Element(_)      => XPathNodeKind::Element,
660            INodeKind::Attribute(_)    => XPathNodeKind::Attribute,
661            INodeKind::Text(_)         => XPathNodeKind::Text,
662            INodeKind::Comment(_)      => XPathNodeKind::Comment,
663            INodeKind::CData(_)        => XPathNodeKind::CData,
664            INodeKind::PI(_)           => XPathNodeKind::PI,
665            INodeKind::Namespace { .. } => XPathNodeKind::Namespace,
666        }
667    }
668    fn pi_target(&self, id: NodeId) -> &str {
669        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.pi_target(local); }
670        if is_synthetic(id) { return ""; }
671        match &self.nodes[id].kind {
672            INodeKind::PI(n) => n.name(),
673            _ => "",
674        }
675    }
676    fn string_value(&self, id: NodeId) -> String {
677        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.string_value(local); }
678        if is_synthetic(id) {
679            let i = id & !SYNTHETIC_TEXT_BASE;
680            return self.synthetic.borrow().get(i).cloned().unwrap_or_default();
681        }
682        DocIndex::string_value(self, id)
683    }
684    fn node_name(&self, id: NodeId) -> &str {
685        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.node_name(local); }
686        if is_synthetic(id) { return ""; }
687        DocIndex::node_name(self, id)
688    }
689    fn local_name(&self, id: NodeId) -> &str {
690        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.local_name(local); }
691        if is_synthetic(id) { return ""; }
692        DocIndex::local_name(self, id)
693    }
694    fn namespace_prefix(&self, id: NodeId) -> Option<&str> {
695        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.namespace_prefix(local); }
696        if is_synthetic(id) { return None; }
697        DocIndex::namespace_prefix(self, id)
698    }
699    fn namespace_uri(&self, id: NodeId) -> &str {
700        if let Some((rtf, local)) = self.rtf_at(id) { return rtf.namespace_uri(local); }
701        if is_synthetic(id) { return ""; }
702        DocIndex::namespace_uri(self, id)
703    }
704    fn is_element(&self, id: NodeId) -> bool {
705        if let Some((rtf, local)) = self.rtf_at(id) {
706            return matches!(rtf.kind(local), XPathNodeKind::Element);
707        }
708        if is_synthetic(id) { return false; }
709        matches!(self.nodes[id].kind, INodeKind::Element(_))
710    }
711    fn allocate_rtf_text_nodes(&self, values: Vec<String>) -> Option<Vec<NodeId>> {
712        Some(DocIndex::allocate_rtf_text_nodes_inherent(self, values))
713    }
714    fn rtf_builder(&self) -> Option<super::rtf::RtfBuilder> {
715        Some(DocIndex::start_rtf(self))
716    }
717    fn finish_rtf(&self, builder: super::rtf::RtfBuilder) -> Option<NodeId> {
718        Some(DocIndex::finish_rtf(self, builder))
719    }
720    fn rtf_node_type(&self, id: NodeId) -> Option<(String, String)> {
721        DocIndex::rtf_node_type(self, id)
722    }
723    fn is_synthetic_wrap(&self, id: NodeId) -> bool {
724        DocIndex::is_synthetic_wrap(self, id)
725    }
726    fn is_id_attribute(&self, attr_id: NodeId) -> bool {
727        if super::rtf::is_rtf_id(attr_id) { return false; }
728        if is_synthetic(attr_id) { return false; }
729        let INodeKind::Attribute(_) = self.nodes[attr_id].kind else { return false; };
730        let local = self.local_name(attr_id);
731        // Default convention always honoured: any `xml:id` and any
732        // attribute literally named `id` count, matching libxml2 in
733        // DTD-less documents.
734        if self.node_name(attr_id) == "xml:id" || local == "id" {
735            return true;
736        }
737        // DTD-typed override: consult the parsed `<!ATTLIST … ID>`
738        // declarations on this attribute's owner element.
739        if self.id_attrs.is_empty() { return false; }
740        let Some(owner) = self.parent(attr_id) else { return false; };
741        let owner_name = self.node_name(owner);
742        let attr_name = self.node_name(attr_id);
743        self.id_attrs.get(owner_name)
744            .is_some_and(|ids| ids.iter().any(|n| n == attr_name || n == local))
745    }
746    fn is_idref_attribute(&self, attr_id: NodeId) -> bool {
747        if super::rtf::is_rtf_id(attr_id) { return false; }
748        if is_synthetic(attr_id) { return false; }
749        let INodeKind::Attribute(_) = self.nodes[attr_id].kind else { return false; };
750        // IDREF typing only comes from the DTD — there is no DTD-less
751        // convention (unlike `id`/`xml:id` for ID attributes).
752        if self.idref_attrs.is_empty() { return false; }
753        let Some(owner) = self.parent(attr_id) else { return false; };
754        let owner_name = self.node_name(owner);
755        let attr_name = self.node_name(attr_id);
756        let local = self.local_name(attr_id);
757        self.idref_attrs.get(owner_name)
758            .is_some_and(|refs| refs.iter().any(|n| n == attr_name || n == local))
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765    use crate::xpath::index::DocIndexLike;
766    use crate::{parse_str, ParseOptions};
767
768    fn idx_of(xml: &str) -> (sup_xml_tree::dom::Document, ()) {
769        // Namespace-aware parsing: these tests probe the XPath data
770        // model's namespace nodes, which only get populated when
771        // the parser runs the xmlns-resolution pass.  Under the
772        // `c-abi` feature, xmlns declarations live on the
773        // element's `ns_def` chain — populated only on this path
774        // — and `ns_declarations()` walks that chain.  Without
775        // `namespace_aware`, ns_def stays empty and every
776        // namespace-node assertion below sees just the implicit
777        // `xml` binding.
778        let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
779        let doc = parse_str(xml, &opts).unwrap();
780        (doc, ())
781    }
782
783    /// Find the first node matching `kind` via the index by scanning
784    /// NodeIds.  Returns the id.
785    fn find_kind(idx: &DocIndex<'_>, want: XPathNodeKind) -> NodeId {
786        for id in 0..idx.nodes.len() {
787            if idx.kind(id) == want { return id; }
788        }
789        panic!("no node of kind {want:?}");
790    }
791
792    // ── PI nodes ────────────────────────────────────────────────
793
794    #[test]
795    fn pi_node_indexing_and_accessors() {
796        let (doc, _) = idx_of(r#"<r><?xml-stylesheet href="s.xsl"?></r>"#);
797        let idx = DocIndex::build(&doc);
798        let pi_id = find_kind(&idx, XPathNodeKind::PI);
799        assert_eq!(idx.node_name(pi_id),   "xml-stylesheet");
800        assert_eq!(idx.local_name(pi_id),  "xml-stylesheet");
801        assert_eq!(idx.namespace_uri(pi_id), "");
802        assert_eq!(idx.namespace_prefix(pi_id), None);
803        // PI string-value is its data part (content after target).
804        assert_eq!(idx.string_value(pi_id), r#"href="s.xsl""#);
805        assert_eq!(<DocIndex<'_> as DocIndexLike>::pi_target(&idx, pi_id), "xml-stylesheet");
806    }
807
808    #[test]
809    fn pi_target_returns_empty_for_non_pi_nodes() {
810        let (doc, _) = idx_of("<r/>");
811        let idx = DocIndex::build(&doc);
812        let elem_id = find_kind(&idx, XPathNodeKind::Element);
813        assert_eq!(<DocIndex<'_> as DocIndexLike>::pi_target(&idx, elem_id), "");
814    }
815
816    // ── CDATA nodes ─────────────────────────────────────────────
817
818    #[test]
819    fn cdata_node_indexed_and_kinded() {
820        let (doc, _) = idx_of("<r><![CDATA[raw <data>]]></r>");
821        let idx = DocIndex::build(&doc);
822        let cd_id = find_kind(&idx, XPathNodeKind::CData);
823        // CData string-value is the raw text content.
824        assert_eq!(idx.string_value(cd_id), "raw <data>");
825        // CData kind via DocIndexLike trait dispatch.
826        assert_eq!(idx.kind(cd_id), XPathNodeKind::CData);
827    }
828
829    // ── Comment nodes ───────────────────────────────────────────
830
831    #[test]
832    fn comment_string_value() {
833        let (doc, _) = idx_of("<r><!-- hello --></r>");
834        let idx = DocIndex::build(&doc);
835        let c_id = find_kind(&idx, XPathNodeKind::Comment);
836        assert_eq!(idx.string_value(c_id), " hello ");
837    }
838
839    // ── Attribute with namespace ────────────────────────────────
840
841    #[test]
842    fn attribute_string_value_returns_attr_value() {
843        // Exercises the INodeKind::Attribute(a) arm of string_value
844        // and namespace_uri's unwrap_or("") path.
845        let (doc, _) = idx_of(r#"<r id="x"/>"#);
846        let idx = DocIndex::build(&doc);
847        let attr_id = find_kind(&idx, XPathNodeKind::Attribute);
848        assert_eq!(idx.string_value(attr_id), "x");
849        // namespace_uri's match arm fires (returns "" via unwrap_or).
850        assert_eq!(idx.namespace_uri(attr_id), "");
851    }
852
853    #[test]
854    fn unprefixed_attribute_has_no_namespace_uri_or_prefix() {
855        let (doc, _) = idx_of(r#"<r id="x"/>"#);
856        let idx = DocIndex::build(&doc);
857        let attr_id = find_kind(&idx, XPathNodeKind::Attribute);
858        assert_eq!(idx.namespace_uri(attr_id), "");
859        assert_eq!(idx.namespace_prefix(attr_id), None);
860    }
861
862    // ── Non-element / non-attribute nodes have no NS ────────────
863
864    #[test]
865    fn text_node_has_no_namespace() {
866        let (doc, _) = idx_of("<r>hello</r>");
867        let idx = DocIndex::build(&doc);
868        let txt_id = find_kind(&idx, XPathNodeKind::Text);
869        assert_eq!(idx.namespace_uri(txt_id), "");
870        assert_eq!(idx.namespace_prefix(txt_id), None);
871    }
872
873    // ── Explicit xmlns:xml declaration ──────────────────────────
874
875    #[test]
876    fn explicit_xmlns_xml_declaration_is_honored() {
877        // When the user explicitly declares xmlns:xml on an element,
878        // we must NOT also append the implicit one (seen_xml = true).
879        let (doc, _) = idx_of(
880            r#"<r xmlns:xml="http://www.w3.org/XML/1998/namespace"/>"#,
881        );
882        let idx = DocIndex::build(&doc);
883        let r_id = find_kind(&idx, XPathNodeKind::Element);
884        let ns_range = idx.ns_range(r_id);
885        let xml_count = ns_range
886            .filter(|&nid| idx.node_name(nid) == "xml")
887            .count();
888        assert_eq!(xml_count, 1, "xml prefix should appear exactly once");
889    }
890
891    // ── Namespace nodes ─────────────────────────────────────────
892
893    #[test]
894    fn namespace_node_local_and_node_name_match_prefix() {
895        let (doc, _) = idx_of(r#"<r xmlns:ns="urn:n"/>"#);
896        let idx = DocIndex::build(&doc);
897        let r_id = find_kind(&idx, XPathNodeKind::Element);
898        let ns_range = idx.ns_range(r_id);
899        // Should have at least the 'ns' binding plus the implicit 'xml'.
900        let mut found_ns = false;
901        for nid in ns_range {
902            if idx.node_name(nid) == "ns" {
903                found_ns = true;
904                assert_eq!(idx.local_name(nid), "ns");
905                assert_eq!(idx.string_value(nid), "urn:n");
906                assert_eq!(idx.namespace_uri(nid), "");
907            }
908        }
909        assert!(found_ns, "ns binding not found among namespace nodes");
910    }
911
912    #[test]
913    fn default_namespace_node_has_empty_name() {
914        let (doc, _) = idx_of(r#"<r xmlns="urn:default"/>"#);
915        let idx = DocIndex::build(&doc);
916        let r_id = find_kind(&idx, XPathNodeKind::Element);
917        let ns_range = idx.ns_range(r_id);
918        let mut found = false;
919        for nid in ns_range {
920            // Default namespace: prefix=None → node_name() returns "".
921            if idx.string_value(nid) == "urn:default" {
922                found = true;
923                assert_eq!(idx.node_name(nid), "");
924                assert_eq!(idx.local_name(nid), "");
925            }
926        }
927        assert!(found);
928    }
929
930    #[test]
931    fn xmlns_undeclaration_removes_default() {
932        // xmlns="" on a child element undoes the parent's default.
933        let (doc, _) = idx_of(
934            r#"<r xmlns="urn:default"><c xmlns=""/></r>"#,
935        );
936        let idx = DocIndex::build(&doc);
937        // Walk to the inner element.
938        let inner = (0..idx.nodes.len())
939            .find(|&id|
940                idx.kind(id) == XPathNodeKind::Element
941                && idx.local_name(id) == "c"
942            )
943            .expect("c element");
944        let ns_range = idx.ns_range(inner);
945        let default_count = ns_range
946            .filter(|&nid| idx.node_name(nid) == "")
947            .count();
948        // The default namespace should be undeclared on <c/>, so no
949        // node for it.
950        assert_eq!(default_count, 0);
951    }
952
953    // ── DocIndexLike trait wrappers ─────────────────────────────
954
955    #[test]
956    fn doc_index_like_thunks_forward_correctly() {
957        let (doc, _) = idx_of(r#"<r xmlns:ns="urn:n" id="x"><a/></r>"#);
958        let idx = DocIndex::build(&doc);
959        // Trait-method dispatch covers the thunk lines in the impl.
960        let r_id = find_kind(&idx, XPathNodeKind::Element);
961        let attr_id = find_kind(&idx, XPathNodeKind::Attribute);
962        // The implicit `xml` plus `ns` give the element 2 namespace nodes.
963        assert!(<DocIndex<'_> as DocIndexLike>::ns_range(&idx, r_id).len() >= 2);
964        assert!(<DocIndex<'_> as DocIndexLike>::is_element(&idx, r_id));
965        assert!(!<DocIndex<'_> as DocIndexLike>::is_element(&idx, attr_id));
966        assert_eq!(<DocIndex<'_> as DocIndexLike>::namespace_prefix(&idx, attr_id), None);
967        // The thunk for string_value.
968        assert!(!<DocIndex<'_> as DocIndexLike>::string_value(&idx, r_id).is_empty()
969                || true); // smoke
970    }
971}