Skip to main content

sup_xml_core/xpath/exslt/
mod.rs

1//! EXSLT extension function families — the math, date, str, and
2//! set namespaces every real-world XSLT stylesheet (and many bare
3//! XPath consumers) reach for.
4//!
5//! EXSLT lives *inside* the XPath crate, not inside XSLT, because
6//! these are XPath function libraries — they plug into the XPath
7//! function dispatcher and are useful even when no stylesheet is
8//! involved (e.g. `etree.XPath("math:max(...)")`).
9//!
10//! Each family exposes a `dispatch(name, args, idx)` function that
11//! returns `Some(result)` if the function name belongs to the
12//! family, or `None` otherwise — letting the XPath evaluator chain
13//! through user bindings → EXSLT families → built-ins.
14//!
15//! Spec references:
16//! - https://exslt.org/math/
17//! - https://exslt.org/date/
18//! - https://exslt.org/str/
19//! - https://exslt.org/set/
20
21use crate::error::XmlError;
22use crate::xpath::eval::{Numeric, Value};
23use crate::xpath::index::DocIndexLike;
24
25pub mod math;
26pub mod date;
27pub mod str;
28pub mod sets;
29pub mod regexp;
30pub mod common;
31
32type Result<T> = std::result::Result<T, XmlError>;
33
34// ── namespace URIs ─────────────────────────────────────────────────
35
36pub const MATH_NS:    &str = "http://exslt.org/math";
37pub const DATE_NS:    &str = "http://exslt.org/dates-and-times";
38pub const STR_NS:     &str = "http://exslt.org/strings";
39pub const SET_NS:     &str = "http://exslt.org/sets";
40pub const REGEXP_NS:  &str = "http://exslt.org/regular-expressions";
41pub const COMMON_NS:  &str = "http://exslt.org/common";
42pub const DYN_NS:     &str = "http://exslt.org/dynamic";
43/// XPath 3.0 math namespace — distinct from EXSLT math.  Hosts
44/// `math:pi()`, `math:sin()`, `math:pow()`, etc. per XPath 3.0
45/// §4.8.  XSLT 3.0 §3.6 pre-binds this prefix.
46pub const XPATH_MATH_NS: &str = "http://www.w3.org/2005/xpath-functions/math";
47
48/// Dispatch an XPath function call to the matching EXSLT family.
49/// Returns `Some(result)` when `ns_uri` matches one of the EXSLT
50/// namespace URIs *and* the family recognises `name`; otherwise
51/// `None`, so the caller can fall through to its own table or
52/// surface "unregistered function."
53///
54/// Generic over the doc index because some EXSLT functions (e.g.
55/// `set:distinct`, `str:tokenize` when given a nodeset) need to
56/// read string-values of nodes.
57pub fn dispatch<I: DocIndexLike>(
58    ns_uri: &str,
59    name: &str,
60    args: Vec<Value>,
61    idx: &I,
62) -> Option<Result<Value>> {
63    match ns_uri {
64        MATH_NS        => math::dispatch(name, args, idx),
65        DATE_NS        => date::dispatch(name, args, idx),
66        STR_NS         => self::str::dispatch(name, args, idx),
67        SET_NS         => sets::dispatch(name, args, idx),
68        REGEXP_NS      => regexp::dispatch(name, args, idx),
69        COMMON_NS      => common::dispatch(name, args, idx),
70        XPATH_MATH_NS  => xpath_math_dispatch(name, args, idx),
71        // DYN_NS (`dyn:evaluate`) is intercepted upstream in the XPath
72        // evaluator because it needs the full `EvalCtx` to re-enter the
73        // parser and evaluator at runtime — passing only `args` and
74        // `idx` here is insufficient.
75        _         => None,
76    }
77}
78
79/// XPath 3.0 §4.8 math functions.  Names differ slightly from
80/// EXSLT math (`power` → `pow`, plus `pi`, `exp10`, `log10`).
81/// All operate on `xs:double` per the spec; we round-trip
82/// through f64 since the value model already treats numbers as
83/// double-precision.
84fn xpath_math_dispatch<I: DocIndexLike>(
85    name: &str, args: Vec<Value>, idx: &I,
86) -> Option<Result<Value>> {
87    use crate::error::{ErrorDomain, ErrorLevel};
88    let err = |msg: &str| XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, msg.to_string());
89    fn num<I: DocIndexLike>(v: &Value, idx: &I) -> f64 {
90        crate::xpath::eval::value_to_number(v, idx)
91    }
92    let r = match name {
93        "pi"   => {
94            if !args.is_empty() { return Some(Err(err("math:pi() takes no arguments"))); }
95            Ok(Value::Number(Numeric::Double(std::f64::consts::PI)))
96        }
97        "exp"   | "exp10" | "log"   | "log10"
98        | "sqrt"  | "sin"   | "cos"   | "tan"
99        | "asin"  | "acos"  | "atan" => {
100            if args.len() != 1 {
101                return Some(Err(err(
102                    &format!("math:{name}() takes 1 argument"))));
103            }
104            let n = num(&args[0], idx);
105            let r = match name {
106                "exp"   => n.exp(),
107                "exp10" => 10_f64.powf(n),
108                "log"   => n.ln(),
109                "log10" => n.log10(),
110                "sqrt"  => n.sqrt(),
111                "sin"   => n.sin(),
112                "cos"   => n.cos(),
113                "tan"   => n.tan(),
114                "asin"  => n.asin(),
115                "acos"  => n.acos(),
116                "atan"  => n.atan(),
117                _       => unreachable!(),
118            };
119            Ok(Value::Number(Numeric::Double(r)))
120        }
121        "pow" => {
122            if args.len() != 2 {
123                return Some(Err(err("math:pow() takes 2 arguments")));
124            }
125            let x = num(&args[0], idx);
126            let y = num(&args[1], idx);
127            Ok(Value::Number(Numeric::Double(x.powf(y))))
128        }
129        "atan2" => {
130            if args.len() != 2 {
131                return Some(Err(err("math:atan2() takes 2 arguments")));
132            }
133            let y = num(&args[0], idx);
134            let x = num(&args[1], idx);
135            Ok(Value::Number(Numeric::Double(y.atan2(x))))
136        }
137        _ => return None,
138    };
139    Some(r)
140}