Skip to main content

sup_xml_core/xpath/exslt/
common.rs

1//! EXSLT common family — http://exslt.org/common
2//!
3//! Two functions:
4//!
5//! | Function            | Status      | Notes                                       |
6//! |---------------------|-------------|---------------------------------------------|
7//! | `exsl:node-set`     | implemented | nodeset → identity; scalar → 1-text-node    |
8//! | `exsl:object-type`  | implemented | XPath value-type discriminant string         |
9//!
10//! `exsl:node-set`'s motivating use-case is unwrapping XSLT
11//! result-tree-fragment variables (`<xsl:variable name="x"><foo/>
12//! </xsl:variable>`) so they can be traversed.  That XSLT-specific
13//! materialisation is intercepted by the XSLT engine's bindings
14//! before reaching this fallback dispatcher; the path here covers
15//! the bare-XPath and "already a node-set" cases.
16
17use crate::error::{ErrorDomain, ErrorLevel, XmlError};
18use crate::xpath::eval::{Value, value_to_string};
19use crate::xpath::index::DocIndexLike;
20
21use super::Result;
22
23fn err(msg: impl Into<String>) -> XmlError {
24    XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, msg)
25}
26
27pub fn dispatch<I: DocIndexLike>(
28    name: &str, args: Vec<Value>, idx: &I,
29) -> Option<Result<Value>> {
30    match name {
31        "node-set"    => Some(node_set_fn(&args, idx)),
32        "object-type" => Some(object_type_fn(&args, idx)),
33        _ => None,
34    }
35}
36
37/// `exsl:node-set(value) → node-set`.
38///
39/// - If `value` is already a node-set, return it unchanged.
40/// - For a scalar (string / number / boolean), allocate a single
41///   synthetic text node holding the string-value and return a
42///   one-element node-set.  This matches libexslt's behaviour for
43///   non-RTF inputs.
44/// - Foreign node-sets pass through (they're already navigable).
45///
46/// The interesting XSLT case — `exsl:node-set($rtf-variable)` — is
47/// handled inside the XSLT engine's bindings before reaching this
48/// dispatcher.  By the time control lands here, the argument has
49/// already been resolved to a `Value`; there's no way to recover
50/// the RTF structure from a stringified value.
51fn node_set_fn<I: DocIndexLike>(args: &[Value], idx: &I) -> Result<Value> {
52    if args.len() != 1 {
53        return Err(err("exsl:node-set takes 1 argument"));
54    }
55    match &args[0] {
56        Value::NodeSet(_) | Value::ForeignNodeSet(_) => Ok(args[0].clone()),
57        scalar => {
58            let s = value_to_string(scalar, idx);
59            let ids = idx.allocate_rtf_text_nodes(vec![s]).ok_or_else(|| err(
60                "exsl:node-set: this XPath context does not support RTF allocation",
61            ))?;
62            Ok(Value::NodeSet(ids))
63        }
64    }
65}
66
67/// `exsl:object-type(value) → string` — discriminate XPath value
68/// types.  Used by defensive library stylesheets that branch on
69/// argument type.  Returns one of:
70///
71/// * `"node-set"` — `Value::NodeSet` / `Value::ForeignNodeSet`
72/// * `"string"`   — `Value::String`
73/// * `"number"`   — `Value::Number`
74/// * `"boolean"`  — `Value::Boolean`
75///
76/// (No `"RTF"` value because by the time the function sees its
77/// argument, an RTF has already been coerced — the XSLT engine
78/// hands stringified RTFs through as `Value::String`.)
79fn object_type_fn<I: DocIndexLike>(args: &[Value], _idx: &I) -> Result<Value> {
80    if args.len() != 1 {
81        return Err(err("exsl:object-type takes 1 argument"));
82    }
83    let label = match &args[0] {
84        Value::NodeSet(_) | Value::ForeignNodeSet(_) => "node-set",
85        Value::String(_)  => "string",
86        Value::Number(_)  => "number",
87        Value::Boolean(_) => "boolean",
88        Value::Typed(t)   => {
89            // Map the typed atomic back to one of EXSL's four
90            // coarse labels.  Numeric kinds → "number"; boolean
91            // → "boolean"; everything else (date / duration /
92            // string / etc.) → "string".
93            if t.numeric.is_some() { "number" }
94            else if t.boolean.is_some() { "boolean" }
95            else { "string" }
96        }
97        // EXSLT predates atomic sequences; label them as the
98        // generic "node-set" since that's what callers expect to
99        // iterate.
100        Value::Sequence(_) | Value::IntRange { .. } => "node-set",
101        Value::Map(_)     => "map",
102        Value::Array(_)   => "array",
103        Value::Function(_) => "function",
104    };
105    Ok(Value::String(label.to_string()))
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::xpath::eval::Numeric;
112    use crate::xpath::XPathContext;
113    use crate::{parse_str, ParseOptions};
114
115    fn doc() -> sup_xml_tree::dom::Document {
116        parse_str("<r><a/><b/></r>", &ParseOptions::default()).unwrap()
117    }
118
119    #[test]
120    fn node_set_passes_nodeset_through() {
121        let d = doc();
122        let ctx = XPathContext::new(&d);
123        let ns = ctx.eval("/r/*").unwrap();
124        let original_len = match &ns { Value::NodeSet(n) => n.len(), _ => panic!() };
125        let r = dispatch("node-set", vec![ns], &ctx.index).unwrap().unwrap();
126        match r {
127            Value::NodeSet(n) => assert_eq!(n.len(), original_len),
128            _ => panic!("expected node-set"),
129        }
130    }
131
132    #[test]
133    fn node_set_wraps_string_in_single_text_node() {
134        let d = doc();
135        let ctx = XPathContext::new(&d);
136        let r = dispatch("node-set",
137            vec![Value::String("hello".into())], &ctx.index).unwrap().unwrap();
138        let ns = match r { Value::NodeSet(ns) => ns, _ => panic!() };
139        assert_eq!(ns.len(), 1);
140        assert_eq!(ctx.index.string_value(ns[0]), "hello");
141    }
142
143    #[test]
144    fn node_set_wraps_number_in_text_node_with_xpath_string_form() {
145        let d = doc();
146        let ctx = XPathContext::new(&d);
147        let r = dispatch("node-set",
148            vec![Value::Number(Numeric::Double(42.0))], &ctx.index).unwrap().unwrap();
149        let ns = match r { Value::NodeSet(ns) => ns, _ => panic!() };
150        assert_eq!(ctx.index.string_value(ns[0]), "42");
151    }
152
153    #[test]
154    fn object_type_returns_correct_label_per_variant() {
155        let d = doc();
156        let ctx = XPathContext::new(&d);
157        let cases: &[(Value, &str)] = &[
158            (Value::String("x".into()),  "string"),
159            (Value::Number(Numeric::Double(1.0)),         "number"),
160            (Value::Boolean(true),       "boolean"),
161            (Value::NodeSet(vec![]),     "node-set"),
162        ];
163        for (arg, expected) in cases {
164            let r = dispatch("object-type", vec![arg.clone()], &ctx.index)
165                .unwrap().unwrap();
166            assert!(matches!(r, Value::String(ref s) if s == expected),
167                "got {r:?} for arg {arg:?}");
168        }
169    }
170
171    #[test]
172    fn unknown_function_returns_none() {
173        let d = doc();
174        let ctx = XPathContext::new(&d);
175        assert!(dispatch("nonsense", vec![], &ctx.index).is_none());
176    }
177
178    // ── dyn:evaluate end-to-end (lives in eval, not dispatch) ──
179
180    #[test]
181    fn dyn_evaluate_runs_string_as_xpath_against_context() {
182        use crate::xpath::XPathBindingsBuilder;
183        let d = parse_str(
184            "<r><a>alpha</a><a>beta</a><a>gamma</a></r>",
185            &ParseOptions::default(),
186        ).unwrap();
187        let ctx = crate::xpath::XPathContext::new(&d);
188        let mut bind = XPathBindingsBuilder::new();
189        bind.namespace("dyn", "http://exslt.org/dynamic");
190        let v = ctx.eval_with("dyn:evaluate('count(/r/a)')", 0, &bind).unwrap();
191        assert_eq!(crate::xpath::eval::value_to_number(&v, &ctx.index), 3.0);
192    }
193
194    #[test]
195    fn dyn_evaluate_invalid_expression_yields_empty_nodeset() {
196        use crate::xpath::XPathBindingsBuilder;
197        let d = doc();
198        let ctx = crate::xpath::XPathContext::new(&d);
199        let mut bind = XPathBindingsBuilder::new();
200        bind.namespace("dyn", "http://exslt.org/dynamic");
201        let v = ctx.eval_with("dyn:evaluate('not a valid xpath /// ')", 0, &bind).unwrap();
202        assert!(matches!(v, Value::NodeSet(ref ns) if ns.is_empty()));
203    }
204}