Skip to main content

rusty_xml_xpath/
lib.rs

1//! XPath 1.0 compile + eval matching libxml2 `xpath.h` (M4).
2
3#![forbid(unsafe_code)]
4
5use rusty_xml_tree::{NodeId, NodeKind, XmlDoc};
6use std::cmp::Ordering;
7
8/// libxml2 `xmlXPathObjectType`.
9pub const XPATH_UNDEFINED: i32 = 0;
10pub const XPATH_NODESET: i32 = 1;
11pub const XPATH_BOOLEAN: i32 = 2;
12pub const XPATH_NUMBER: i32 = 3;
13pub const XPATH_STRING: i32 = 4;
14
15#[derive(Clone, Debug)]
16pub enum XPathObject {
17    Undefined,
18    NodeSet(Vec<NodeId>),
19    Boolean(bool),
20    Number(f64),
21    String(String),
22}
23
24impl XPathObject {
25    pub fn object_type(&self) -> i32 {
26        match self {
27            XPathObject::Undefined => XPATH_UNDEFINED,
28            XPathObject::NodeSet(_) => XPATH_NODESET,
29            XPathObject::Boolean(_) => XPATH_BOOLEAN,
30            XPathObject::Number(_) => XPATH_NUMBER,
31            XPathObject::String(_) => XPATH_STRING,
32        }
33    }
34}
35
36pub struct XmlXPathContext<'a> {
37    pub doc: &'a XmlDoc,
38    pub node: NodeId,
39    pub position: usize,
40    pub size: usize,
41    ns: Vec<(String, String)>,
42}
43
44impl<'a> XmlXPathContext<'a> {
45    /// `xmlXPathNewContext`.
46    #[doc(alias = "xmlXPathNewContext")]
47    pub fn xml_xpath_new_context(doc: &'a XmlDoc) -> Self {
48        Self {
49            node: doc.xml_doc_get_root_element().unwrap_or(NodeId::DOCUMENT),
50            doc,
51            position: 1,
52            size: 1,
53            ns: Vec::new(),
54        }
55    }
56
57    /// `xmlXPathSetContextNode`.
58    #[doc(alias = "xmlXPathSetContextNode")]
59    pub fn xml_xpath_set_context_node(&mut self, node: NodeId) {
60        self.node = node;
61    }
62
63    pub fn register_ns(&mut self, prefix: &str, href: &str) {
64        self.ns.push((prefix.to_string(), href.to_string()));
65    }
66}
67
68/// `xmlXPathIsNaN`.
69#[doc(alias = "xmlXPathIsNaN")]
70pub fn xml_xpath_is_nan(v: f64) -> bool {
71    v.is_nan()
72}
73
74/// `xmlXPathIsInf`.
75#[doc(alias = "xmlXPathIsInf")]
76pub fn xml_xpath_is_inf(v: f64) -> i32 {
77    if v.is_infinite() {
78        if v.is_sign_positive() { 1 } else { -1 }
79    } else {
80        0
81    }
82}
83
84/// `xmlXPathOrderDocElems` — walk document order (preorder).
85#[doc(alias = "xmlXPathOrderDocElems")]
86pub fn xml_xpath_order_doc_elems(doc: &XmlDoc) -> Vec<NodeId> {
87    let mut out = Vec::new();
88    fn walk(doc: &XmlDoc, id: NodeId, out: &mut Vec<NodeId>) {
89        out.push(id);
90        let mut a = doc.first_attr(id);
91        while let Some(x) = a {
92            out.push(x);
93            a = doc.next_sibling(x);
94        }
95        let mut c = doc.first_child(id);
96        while let Some(x) = c {
97            walk(doc, x, out);
98            c = doc.next_sibling(x);
99        }
100    }
101    walk(doc, NodeId::DOCUMENT, &mut out);
102    out
103}
104
105/// `xmlXPathCmpNodes`.
106#[doc(alias = "xmlXPathCmpNodes")]
107pub fn xml_xpath_cmp_nodes(doc: &XmlDoc, a: NodeId, b: NodeId) -> i32 {
108    let order = xml_xpath_order_doc_elems(doc);
109    let ia = order.iter().position(|n| *n == a);
110    let ib = order.iter().position(|n| *n == b);
111    match (ia, ib) {
112        (Some(x), Some(y)) => x.cmp(&y) as i32,
113        _ => a.0.cmp(&b.0) as i32,
114    }
115}
116
117/// `xmlXPathEval` / `xmlXPathEvalExpression`.
118#[doc(alias = "xmlXPathEval")]
119pub fn xml_xpath_eval(expr: &str, ctx: &XmlXPathContext<'_>) -> Result<XPathObject, String> {
120    let mut p = Parser {
121        src: expr.trim(),
122        pos: 0,
123        depth: 0,
124    };
125    let ast = p.parse_expr()?;
126    p.skip_ws();
127    if p.pos < p.src.len() && !p.src[p.pos..].chars().all(|c| c.is_whitespace()) {
128        return Err(format!("trailing junk in XPath: {}", &p.src[p.pos..]));
129    }
130    eval(&ast, ctx)
131}
132
133/// `xmlXPathCastToBoolean`.
134#[doc(alias = "xmlXPathCastToBoolean")]
135pub fn xml_xpath_cast_to_boolean(o: &XPathObject) -> bool {
136    match o {
137        XPathObject::Boolean(b) => *b,
138        XPathObject::Number(n) => *n != 0.0 && !n.is_nan(),
139        XPathObject::String(s) => !s.is_empty(),
140        XPathObject::NodeSet(v) => !v.is_empty(),
141        XPathObject::Undefined => false,
142    }
143}
144
145/// `xmlXPathCastToNumber`.
146#[doc(alias = "xmlXPathCastToNumber")]
147pub fn xml_xpath_cast_to_number(o: &XPathObject, ctx: &XmlXPathContext<'_>) -> f64 {
148    match o {
149        XPathObject::Number(n) => *n,
150        XPathObject::Boolean(b) => {
151            if *b { 1.0 } else { 0.0 }
152        }
153        XPathObject::String(s) => xpath_number(s),
154        XPathObject::NodeSet(v) => xml_xpath_cast_to_number(&XPathObject::String(string_val(ctx, v)), ctx),
155        XPathObject::Undefined => f64::NAN,
156    }
157}
158
159/// `xmlXPathCastToString`.
160#[doc(alias = "xmlXPathCastToString")]
161pub fn xml_xpath_cast_to_string(o: &XPathObject, ctx: &XmlXPathContext<'_>) -> String {
162    match o {
163        XPathObject::String(s) => s.clone(),
164        XPathObject::Boolean(b) => if *b { "true".into() } else { "false".into() },
165        XPathObject::Number(n) => number_to_string(*n),
166        XPathObject::NodeSet(v) => string_val(ctx, v),
167        XPathObject::Undefined => String::new(),
168    }
169}
170
171fn number_to_string(n: f64) -> String {
172    if n.is_nan() {
173        return "NaN".into();
174    }
175    if n.is_infinite() {
176        return if n.is_sign_positive() {
177            "Infinity".into()
178        } else {
179            "-Infinity".into()
180        };
181    }
182    if n == 0.0 {
183        return "0".into();
184    }
185    if n > i32::MIN as f64 && n < i32::MAX as f64 && n == (n as i32) as f64 {
186        return format!("{}", n as i32);
187    }
188    let abs = n.abs();
189    if abs > 1e9 || abs < 1e-5 {
190        let s = format!("{n:.14e}");
191        let Some(idx) = s.find('e') else { return s };
192        let mant = &s[..idx];
193        let es = &s[idx..];
194        let mut mant = mant.to_string();
195        if mant.contains('.') {
196            while mant.ends_with('0') {
197                mant.pop();
198            }
199            if mant.ends_with('.') {
200                mant.pop();
201            }
202        }
203        let es = if es.starts_with("e-") || es.starts_with("e+") {
204            es.to_string()
205        } else {
206            format!("e+{}", &es[1..])
207        };
208        format!("{mant}{es}")
209    } else {
210        let mut s = format!("{n}");
211        if let Some(rest) = s.strip_prefix("-") {
212            if rest.starts_with('.') {
213                s = format!("-0{rest}");
214            }
215        } else if s.starts_with('.') {
216            s = format!("0{s}");
217        }
218        s
219    }
220}
221
222fn dump_number(n: f64) -> String {
223    if n.is_nan() {
224        return "NaN".into();
225    }
226    if n.is_infinite() {
227        return if n.is_sign_positive() {
228            "Infinity".into()
229        } else {
230            "-Infinity".into()
231        };
232    }
233    if n == 0.0 {
234        return "0".into();
235    }
236    // libxml2 debug dump uses `%0g` (6 significant digits, scientific outside [1e-4, 1e6)).
237    let exp = n.abs().log10().floor() as i32;
238    if exp < -4 || exp >= 6 {
239        let s = format!("{n:.5e}");
240        let Some(idx) = s.find('e') else { return s };
241        let mant = &s[..idx];
242        let es = &s[idx..];
243        let mut mant = mant.to_string();
244        if mant.contains('.') {
245            while mant.ends_with('0') {
246                mant.pop();
247            }
248            if mant.ends_with('.') {
249                mant.pop();
250            }
251        }
252        let es = if es.starts_with("e-") || es.starts_with("e+") {
253            es.to_string()
254        } else {
255            format!("e+{}", &es[1..])
256        };
257        format!("{mant}{es}")
258    } else {
259        format!("{n}")
260    }
261}
262
263fn xpath_number(s: &str) -> f64 {
264    let t = s.trim();
265    if t.is_empty() {
266        return f64::NAN;
267    }
268    t.parse::<f64>().unwrap_or(f64::NAN)
269}
270
271fn string_val(ctx: &XmlXPathContext<'_>, nodes: &[NodeId]) -> String {
272    let Some(&id) = nodes.first() else { return String::new() };
273    ctx.doc.xml_node_get_content(id)
274}
275
276/// Dump matching libxml2 `xmlXPathDebugDumpObject` used by `xmllint --xpath` / testXPath.
277pub fn xml_xpath_debug_dump(obj: &XPathObject, ctx: &XmlXPathContext<'_>) -> String {
278    match obj {
279        XPathObject::Undefined => "Object is empty (NULL)\n".into(),
280        XPathObject::Number(n) => format!("Object is a number : {}\n", dump_number(*n)),
281        XPathObject::Boolean(b) => format!(
282            "Object is a Boolean : {}\n",
283            if *b { "true" } else { "false" }
284        ),
285        XPathObject::String(s) => format!("Object is a string : {}\n", debug_string(s)),
286        XPathObject::NodeSet(v) => {
287            let mut s = format!("Object is a Node Set :\nSet contains {} nodes:\n", v.len());
288            for (i, id) in v.iter().enumerate() {
289                s.push_str(&format!("{}", i + 1));
290                dump_one(ctx.doc, *id, 1, &mut s);
291            }
292            s
293        }
294    }
295}
296
297fn debug_string(s: &str) -> String {
298    let mut out = String::new();
299    for (i, c) in s.chars().enumerate() {
300        if i >= 40 {
301            out.push_str("...");
302            break;
303        }
304        if c.is_whitespace() {
305            out.push(' ');
306        } else if (c as u32) >= 0x80 {
307            out.push_str(&format!("#{:X}", c as u32));
308        } else {
309            out.push(c);
310        }
311    }
312    out
313}
314
315fn spaces(depth: i32) -> String {
316    "  ".repeat(depth.max(0) as usize)
317}
318
319fn dump_one(doc: &XmlDoc, id: NodeId, depth: i32, out: &mut String) {
320    match doc.kind(id) {
321        NodeKind::Document | NodeKind::HtmlDocument => {
322            out.push_str(&spaces(depth));
323            out.push_str(" /\n");
324        }
325        NodeKind::Element => {
326            out.push_str(&spaces(depth));
327            out.push_str("ELEMENT ");
328            out.push_str(&doc.qname(id));
329            out.push('\n');
330            for (pre, href) in doc.ns_defs(id) {
331                out.push_str(&spaces(depth + 1));
332                match pre {
333                    Some(p) => out.push_str(&format!("namespace {p} href={href}\n")),
334                    None => out.push_str(&format!("default namespace href={href}\n")),
335                }
336            }
337            let mut a = doc.first_attr(id);
338            while let Some(x) = a {
339                dump_attr(doc, x, depth + 1, out);
340                a = doc.next_sibling(x);
341            }
342        }
343        NodeKind::Attribute => dump_attr(doc, id, depth, out),
344        NodeKind::Text => {
345            out.push_str(&spaces(depth));
346            out.push_str("TEXT\n");
347            out.push_str(&spaces(depth + 1));
348            out.push_str("content=");
349            out.push_str(&debug_string(doc.content(id)));
350            out.push('\n');
351        }
352        NodeKind::CData => {
353            out.push_str(&spaces(depth));
354            out.push_str("CDATA_SECTION\n");
355        }
356        NodeKind::Comment => {
357            out.push_str(&spaces(depth));
358            out.push_str("COMMENT\n");
359        }
360        NodeKind::Pi => {
361            out.push_str(&spaces(depth));
362            out.push_str(&format!("PI {}\n", doc.name(id)));
363        }
364        _ => {
365            out.push_str(&spaces(depth));
366            out.push_str(&format!("{:?}\n", doc.kind(id)));
367        }
368    }
369}
370
371fn dump_attr(doc: &XmlDoc, id: NodeId, depth: i32, out: &mut String) {
372    out.push_str(&spaces(depth));
373    out.push_str("ATTRIBUTE ");
374    out.push_str(doc.name(id));
375    out.push('\n');
376    out.push_str(&spaces(depth + 1));
377    out.push_str("TEXT\n");
378    out.push_str(&spaces(depth + 2));
379    out.push_str("content=");
380    out.push_str(&debug_string(doc.content(id)));
381    out.push('\n');
382}
383
384/// `xmllint --xpath` scalar printer (`%0g` / true / false / string).
385pub fn xml_xpath_print_lint(obj: &XPathObject) -> Option<String> {
386    match obj {
387        XPathObject::Undefined => Some(String::new()),
388        XPathObject::Number(n) => Some(format!("{}\n", dump_number(*n))),
389        XPathObject::Boolean(b) => Some(format!("{}\n", if *b { "true" } else { "false" })),
390        XPathObject::String(s) => Some(format!("{s}\n")),
391        XPathObject::NodeSet(_) => None,
392    }
393}
394#[doc(alias = "xmlXPathCompile")]
395pub fn xml_xpath_compile(expr: &str) -> Result<String, String> {
396    let mut p = Parser {
397        src: expr.trim(),
398        pos: 0,
399        depth: 0,
400    };
401    let _ = p.parse_expr()?;
402    Ok(expr.to_string())
403}
404
405/// `xmlXPathCompiledEval`.
406#[doc(alias = "xmlXPathCompiledEval")]
407pub fn xml_xpath_compiled_eval(
408    expr: &str,
409    ctx: &XmlXPathContext<'_>,
410) -> Result<XPathObject, String> {
411    xml_xpath_eval(expr, ctx)
412}
413
414/* ---------------- parser / AST ---------------- */
415
416#[derive(Clone, Debug)]
417enum Expr {
418    Or(Box<Expr>, Box<Expr>),
419    And(Box<Expr>, Box<Expr>),
420    Eq(Box<Expr>, Box<Expr>, bool),
421    Rel(Box<Expr>, Box<Expr>, Ordering, bool),
422    Add(Box<Expr>, Box<Expr>, bool),
423    Mul(Box<Expr>, Box<Expr>, char),
424    Neg(Box<Expr>),
425    Union(Box<Expr>, Box<Expr>),
426    Path(PathExpr),
427    Steps { base: Box<Expr>, steps: Vec<Step> },
428    Filter(Box<Expr>, Vec<Expr>),
429    Literal(String),
430    Number(f64),
431    Var(String),
432    Fun { name: String, args: Vec<Expr> },
433}
434
435#[derive(Clone, Debug)]
436struct PathExpr {
437    abs: bool,
438    steps: Vec<Step>,
439}
440
441#[derive(Clone, Debug)]
442struct Step {
443    axis: Axis,
444    test: NodeTest,
445    preds: Vec<Expr>,
446}
447
448#[derive(Clone, Copy, Debug, PartialEq, Eq)]
449enum Axis {
450    Child,
451    Descendant,
452    Parent,
453    Ancestor,
454    FollowingSibling,
455    PrecedingSibling,
456    Following,
457    Preceding,
458    Attribute,
459    Namespace,
460    SelfAxis,
461    DescendantOrSelf,
462    AncestorOrSelf,
463}
464
465#[derive(Clone, Debug)]
466enum NodeTest {
467    Star,
468    Name(String, Option<String>),
469    Node,
470    Text,
471    Comment,
472    Pi(Option<String>),
473}
474
475/// Nesting limit for a compiled expression.
476///
477/// The recursive-descent parser had no bound, so `((((...1...))))` or a chain
478/// of `//*|//*|...` around a thousand deep overflowed the stack. That is worse
479/// than a panic: a stack overflow ABORTS THE PROCESS and `catch_unwind` cannot
480/// recover from it. The nesting also builds a `Box` chain whose recursive
481/// `Drop` overflows on its own, so bounding the parse bounds both.
482///
483/// The limit is 64, not something larger, because one level of XPath nesting
484/// costs about THIRTEEN stack frames as it descends the precedence chain
485/// (expr -> or -> and -> eq -> rel -> add -> mul -> unary -> union -> path ->
486/// relative -> step -> preds -> expr). A limit of 256 was measured to overflow
487/// at 250 before the guard could fire. Real expressions nest a handful deep.
488const MAX_XPATH_DEPTH: u32 = 64;
489
490struct Parser<'a> {
491    src: &'a str,
492    pos: usize,
493    depth: u32,
494}
495
496impl<'a> Parser<'a> {
497    fn enter(&mut self) -> Result<(), String> {
498        self.depth += 1;
499        if self.depth > MAX_XPATH_DEPTH {
500            return Err("expression nested too deeply".into());
501        }
502        Ok(())
503    }
504    fn leave(&mut self) {
505        self.depth = self.depth.saturating_sub(1);
506    }
507    fn skip_ws(&mut self) {
508        while let Some(c) = self.src[self.pos..].chars().next() {
509            if c.is_whitespace() {
510                self.pos += c.len_utf8();
511            } else {
512                break;
513            }
514        }
515    }
516    fn peek(&self) -> Option<char> {
517        self.src[self.pos..].chars().next()
518    }
519    fn starts(&self, s: &str) -> bool {
520        self.src[self.pos..].starts_with(s)
521    }
522    fn bump(&mut self, n: usize) {
523        self.pos += n;
524    }
525    fn parse_expr(&mut self) -> Result<Expr, String> {
526        self.enter()?;
527        let r = self.parse_or();
528        self.leave();
529        r
530    }
531    fn parse_or(&mut self) -> Result<Expr, String> {
532        let mut e = self.parse_and()?;
533        loop {
534            self.skip_ws();
535            if self.keyword("or") {
536                let r = self.parse_and()?;
537                e = Expr::Or(Box::new(e), Box::new(r));
538            } else {
539                break;
540            }
541        }
542        Ok(e)
543    }
544    fn parse_and(&mut self) -> Result<Expr, String> {
545        let mut e = self.parse_eq()?;
546        loop {
547            self.skip_ws();
548            if self.keyword("and") {
549                let r = self.parse_eq()?;
550                e = Expr::And(Box::new(e), Box::new(r));
551            } else {
552                break;
553            }
554        }
555        Ok(e)
556    }
557    fn parse_eq(&mut self) -> Result<Expr, String> {
558        let mut e = self.parse_rel()?;
559        loop {
560            self.skip_ws();
561            if self.starts("=") {
562                self.bump(1);
563                let r = self.parse_rel()?;
564                e = Expr::Eq(Box::new(e), Box::new(r), true);
565            } else if self.starts("!=") {
566                self.bump(2);
567                let r = self.parse_rel()?;
568                e = Expr::Eq(Box::new(e), Box::new(r), false);
569            } else {
570                break;
571            }
572        }
573        Ok(e)
574    }
575    fn parse_rel(&mut self) -> Result<Expr, String> {
576        let mut e = self.parse_add()?;
577        loop {
578            self.skip_ws();
579            if self.starts("<=") {
580                self.bump(2);
581                let r = self.parse_add()?;
582                e = Expr::Rel(Box::new(e), Box::new(r), Ordering::Less, true);
583            } else if self.starts(">=") {
584                self.bump(2);
585                let r = self.parse_add()?;
586                e = Expr::Rel(Box::new(e), Box::new(r), Ordering::Greater, true);
587            } else if self.starts("<") {
588                self.bump(1);
589                let r = self.parse_add()?;
590                e = Expr::Rel(Box::new(e), Box::new(r), Ordering::Less, false);
591            } else if self.starts(">") {
592                self.bump(1);
593                let r = self.parse_add()?;
594                e = Expr::Rel(Box::new(e), Box::new(r), Ordering::Greater, false);
595            } else {
596                break;
597            }
598        }
599        Ok(e)
600    }
601    fn parse_add(&mut self) -> Result<Expr, String> {
602        let mut e = self.parse_mul()?;
603        loop {
604            self.skip_ws();
605            if self.starts("+") {
606                self.bump(1);
607                let r = self.parse_mul()?;
608                e = Expr::Add(Box::new(e), Box::new(r), true);
609            } else if self.starts("-") && !self.is_name_start_after_minus() {
610                self.bump(1);
611                let r = self.parse_mul()?;
612                e = Expr::Add(Box::new(e), Box::new(r), false);
613            } else {
614                break;
615            }
616        }
617        Ok(e)
618    }
619    fn is_name_start_after_minus(&self) -> bool {
620        false
621    }
622    fn parse_mul(&mut self) -> Result<Expr, String> {
623        let mut e = self.parse_unary()?;
624        loop {
625            self.skip_ws();
626            if self.starts("*") && !self.looks_like_nametest_star() {
627                self.bump(1);
628                let r = self.parse_unary()?;
629                e = Expr::Mul(Box::new(e), Box::new(r), '*');
630            } else if self.keyword("div") {
631                let r = self.parse_unary()?;
632                e = Expr::Mul(Box::new(e), Box::new(r), 'd');
633            } else if self.keyword("mod") {
634                let r = self.parse_unary()?;
635                e = Expr::Mul(Box::new(e), Box::new(r), 'm');
636            } else {
637                break;
638            }
639        }
640        Ok(e)
641    }
642    fn looks_like_nametest_star(&self) -> bool {
643        false
644    }
645    fn parse_unary(&mut self) -> Result<Expr, String> {
646        self.skip_ws();
647        if self.starts("-") {
648            self.bump(1);
649            self.enter()?;
650            let inner = self.parse_unary();
651            self.leave();
652            Ok(Expr::Neg(Box::new(inner?)))
653        } else {
654            self.parse_union()
655        }
656    }
657    fn parse_union(&mut self) -> Result<Expr, String> {
658        let mut e = self.parse_path()?;
659        // The loop is iterative but each step adds a Box level, so the tree it
660        // builds is as deep as the chain is long -- and dropping it recurses.
661        let mut arms = 0u32;
662        loop {
663            self.skip_ws();
664            if self.starts("|") {
665                self.bump(1);
666                arms += 1;
667                if arms > MAX_XPATH_DEPTH {
668                    return Err("expression nested too deeply".into());
669                }
670                let r = self.parse_path()?;
671                e = Expr::Union(Box::new(e), Box::new(r));
672            } else {
673                break;
674            }
675        }
676        Ok(e)
677    }
678    fn parse_path(&mut self) -> Result<Expr, String> {
679        self.skip_ws();
680        if self.looks_like_primary() {
681            return self.parse_filter_or_primary();
682        }
683        let abs = if self.starts("//") {
684            self.bump(2);
685            let mut steps = vec![Step {
686                axis: Axis::DescendantOrSelf,
687                test: NodeTest::Node,
688                preds: vec![],
689            }];
690            steps.extend(self.parse_relative()?);
691            return Ok(Expr::Path(PathExpr { abs: true, steps }));
692        } else if self.starts("/") {
693            self.bump(1);
694            true
695        } else {
696            false
697        };
698        let steps = self.parse_relative()?;
699        if abs || !steps.is_empty() {
700            Ok(Expr::Path(PathExpr { abs, steps }))
701        } else {
702            self.parse_filter_or_primary()
703        }
704    }
705    fn looks_like_primary(&self) -> bool {
706        let s = self.src[self.pos..].trim_start();
707        match s.chars().next() {
708            Some('(' | '$' | '\'' | '"') => true,
709            Some(d) if d.is_ascii_digit() => true,
710            Some('.') => s[1..].chars().next().map(|x| x.is_ascii_digit()).unwrap_or(false),
711            Some(c) if c.is_ascii_alphabetic() || c == '_' => {
712                let i = s
713                    .find(|ch: char| !(ch.is_ascii_alphanumeric() || "-._:".contains(ch)))
714                    .unwrap_or(s.len());
715                s[i..].trim_start().starts_with('(')
716            }
717            _ => false,
718        }
719    }
720    fn parse_relative(&mut self) -> Result<Vec<Step>, String> {
721        let mut steps = Vec::new();
722        self.skip_ws();
723        if self.peek().is_none() || matches!(self.peek(), Some(')' | ',' | '|' | ']')) {
724            return Ok(steps);
725        }
726        if self.starts("/") && !self.starts("//") {
727            return Ok(steps);
728        }
729        steps.push(self.parse_step()?);
730        loop {
731            self.skip_ws();
732            if self.starts("//") {
733                self.bump(2);
734                steps.push(Step {
735                    axis: Axis::DescendantOrSelf,
736                    test: NodeTest::Node,
737                    preds: vec![],
738                });
739                steps.push(self.parse_step()?);
740            } else if self.starts("/") {
741                self.bump(1);
742                steps.push(self.parse_step()?);
743            } else {
744                break;
745            }
746        }
747        Ok(steps)
748    }
749    fn parse_step(&mut self) -> Result<Step, String> {
750        self.skip_ws();
751        if self.starts("..") {
752            self.bump(2);
753            return Ok(Step {
754                axis: Axis::Parent,
755                test: NodeTest::Node,
756                preds: self.parse_preds()?,
757            });
758        }
759        if self.starts(".") {
760            self.bump(1);
761            return Ok(Step {
762                axis: Axis::SelfAxis,
763                test: NodeTest::Node,
764                preds: self.parse_preds()?,
765            });
766        }
767        if self.starts("@") {
768            self.bump(1);
769            let test = self.parse_node_test()?;
770            return Ok(Step {
771                axis: Axis::Attribute,
772                test,
773                preds: self.parse_preds()?,
774            });
775        }
776        let save = self.pos;
777        if let Some(axis) = self.try_axis() {
778            let test = self.parse_node_test()?;
779            return Ok(Step {
780                axis,
781                test,
782                preds: self.parse_preds()?,
783            });
784        }
785        self.pos = save;
786        let test = self.parse_node_test()?;
787        Ok(Step {
788            axis: Axis::Child,
789            test,
790            preds: self.parse_preds()?,
791        })
792    }
793    fn try_axis(&mut self) -> Option<Axis> {
794        self.skip_ws();
795        let names = [
796            ("descendant-or-self", Axis::DescendantOrSelf),
797            ("following-sibling", Axis::FollowingSibling),
798            ("preceding-sibling", Axis::PrecedingSibling),
799            ("ancestor-or-self", Axis::AncestorOrSelf),
800            ("descendant", Axis::Descendant),
801            ("attribute", Axis::Attribute),
802            ("following", Axis::Following),
803            ("namespace", Axis::Namespace),
804            ("preceding", Axis::Preceding),
805            ("ancestor", Axis::Ancestor),
806            ("parent", Axis::Parent),
807            ("child", Axis::Child),
808            ("self", Axis::SelfAxis),
809        ];
810        for (n, ax) in names {
811            if self.src[self.pos..].starts_with(n) {
812                let after = self.pos + n.len();
813                if self.src[after..].starts_with("::") {
814                    self.pos = after + 2;
815                    return Some(ax);
816                }
817            }
818        }
819        None
820    }
821    fn parse_node_test(&mut self) -> Result<NodeTest, String> {
822        self.skip_ws();
823        if self.starts("*") {
824            self.bump(1);
825            return Ok(NodeTest::Star);
826        }
827        if self.fn_test("node") {
828            return Ok(NodeTest::Node);
829        }
830        if self.fn_test("text") {
831            return Ok(NodeTest::Text);
832        }
833        if self.fn_test("comment") {
834            return Ok(NodeTest::Comment);
835        }
836        if self.src[self.pos..].starts_with("processing-instruction") {
837            self.pos += "processing-instruction".len();
838            self.skip_ws();
839            if !self.starts("(") {
840                return Err("expected (".into());
841            }
842            self.bump(1);
843            self.skip_ws();
844            let lit = if self.starts("'") || self.starts("\"") {
845                Some(self.parse_literal_raw()?)
846            } else {
847                None
848            };
849            self.skip_ws();
850            if self.starts(")") {
851                self.bump(1);
852            }
853            return Ok(NodeTest::Pi(lit));
854        }
855        let name = self.parse_qname()?;
856        let (prefix, local) = split_qname(&name);
857        Ok(NodeTest::Name(local, prefix))
858    }
859    fn fn_test(&mut self, n: &str) -> bool {
860        if self.src[self.pos..].starts_with(n) {
861            let after = self.pos + n.len();
862            let rest = &self.src[after..];
863            let trimmed = rest.trim_start();
864            if trimmed.starts_with("()") || trimmed.starts_with('(') {
865                // node() 
866                if let Some(idx) = rest.find(')') {
867                    self.pos = after + idx + 1;
868                    return true;
869                }
870            }
871        }
872        false
873    }
874    fn parse_preds(&mut self) -> Result<Vec<Expr>, String> {
875        let mut v = Vec::new();
876        loop {
877            self.skip_ws();
878            if self.starts("[") {
879                self.bump(1);
880                v.push(self.parse_expr()?);
881                self.skip_ws();
882                if self.starts("]") {
883                    self.bump(1);
884                } else {
885                    return Err("expected ]".into());
886                }
887            } else {
888                break;
889            }
890        }
891        Ok(v)
892    }
893    fn parse_filter_or_primary(&mut self) -> Result<Expr, String> {
894        let mut e = self.parse_primary()?;
895        let preds = self.parse_preds()?;
896        if !preds.is_empty() {
897            e = Expr::Filter(Box::new(e), preds);
898        }
899        self.skip_ws();
900        if self.starts("/") {
901            let mut steps = Vec::new();
902            if self.starts("//") {
903                self.bump(2);
904                steps.push(Step {
905                    axis: Axis::DescendantOrSelf,
906                    test: NodeTest::Node,
907                    preds: vec![],
908                });
909            } else {
910                self.bump(1);
911            }
912            steps.extend(self.parse_relative()?);
913            return Ok(Expr::Steps {
914                base: Box::new(e),
915                steps,
916            });
917        }
918        Ok(e)
919    }
920    fn parse_primary(&mut self) -> Result<Expr, String> {
921        self.skip_ws();
922        if self.starts("(") {
923            self.bump(1);
924            let e = self.parse_expr()?;
925            self.skip_ws();
926            if self.starts(")") {
927                self.bump(1);
928            }
929            return Ok(e);
930        }
931        if self.starts("$") {
932            self.bump(1);
933            return Ok(Expr::Var(self.parse_qname()?));
934        }
935        if self.starts("'") || self.starts("\"") {
936            return Ok(Expr::Literal(self.parse_literal_raw()?));
937        }
938        if self.peek().map(|c| c.is_ascii_digit() || c == '.').unwrap_or(false) {
939            return Ok(Expr::Number(self.parse_number()?));
940        }
941        let name = self.parse_qname()?;
942        self.skip_ws();
943        if self.starts("(") {
944            self.bump(1);
945            let mut args = Vec::new();
946            self.skip_ws();
947            if !self.starts(")") {
948                args.push(self.parse_expr()?);
949                loop {
950                    self.skip_ws();
951                    if self.starts(",") {
952                        self.bump(1);
953                        args.push(self.parse_expr()?);
954                    } else {
955                        break;
956                    }
957                }
958            }
959            self.skip_ws();
960            if self.starts(")") {
961                self.bump(1);
962            }
963            return Ok(Expr::Fun { name, args });
964        }
965        // name as child nametest path
966        let (prefix, local) = split_qname(&name);
967        Ok(Expr::Path(PathExpr {
968            abs: false,
969            steps: vec![Step {
970                axis: Axis::Child,
971                test: NodeTest::Name(local, prefix),
972                preds: vec![],
973            }],
974        }))
975    }
976    fn parse_literal_raw(&mut self) -> Result<String, String> {
977        let q = self.peek().ok_or("literal")?;
978        self.bump(1);
979        if let Some(end) = self.src[self.pos..].find(q) {
980            let s = self.src[self.pos..self.pos + end].to_string();
981            self.pos += end + 1;
982            Ok(s)
983        } else {
984            Err("unterminated string".into())
985        }
986    }
987    fn parse_number(&mut self) -> Result<f64, String> {
988        let start = self.pos;
989        while self.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
990            self.bump(1);
991        }
992        if self.peek() == Some('.') {
993            self.bump(1);
994            while self.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
995                self.bump(1);
996            }
997        }
998        if matches!(self.peek(), Some('e' | 'E')) {
999            self.bump(1);
1000            if matches!(self.peek(), Some('+' | '-')) {
1001                self.bump(1);
1002            }
1003            while self.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
1004                self.bump(1);
1005            }
1006        }
1007        let s = &self.src[start..self.pos];
1008        Ok(s.parse::<f64>().unwrap_or(f64::NAN))
1009    }
1010    fn parse_qname(&mut self) -> Result<String, String> {
1011        self.skip_ws();
1012        let start = self.pos;
1013        let first = self.peek().ok_or_else(|| format!("expected name at {}", &self.src[self.pos..]))?;
1014        if !(first.is_ascii_alphabetic() || first == '_' || first == ':') {
1015            return Err(format!("expected name at {}", &self.src[self.pos..]));
1016        }
1017        self.bump(first.len_utf8());
1018        while let Some(c) = self.peek() {
1019            if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':' {
1020                self.bump(c.len_utf8());
1021            } else {
1022                break;
1023            }
1024        }
1025        Ok(self.src[start..self.pos].to_string())
1026    }
1027    fn keyword(&mut self, kw: &str) -> bool {
1028        self.skip_ws();
1029        if self.src[self.pos..].starts_with(kw) {
1030            let after = self.pos + kw.len();
1031            let next = self.src[after..].chars().next();
1032            if next.map(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-').unwrap_or(false) {
1033                return false;
1034            }
1035            self.pos = after;
1036            true
1037        } else {
1038            false
1039        }
1040    }
1041}
1042
1043fn split_qname(n: &str) -> (Option<String>, String) {
1044    if let Some((p, l)) = n.split_once(':') {
1045        (Some(p.to_string()), l.to_string())
1046    } else {
1047        (None, n.to_string())
1048    }
1049}
1050
1051/* ---------------- eval ---------------- */
1052
1053fn eval(expr: &Expr, ctx: &XmlXPathContext<'_>) -> Result<XPathObject, String> {
1054    match expr {
1055        Expr::Or(a, b) => {
1056            let x = xml_xpath_cast_to_boolean(&eval(a, ctx)?);
1057            if x {
1058                Ok(XPathObject::Boolean(true))
1059            } else {
1060                Ok(XPathObject::Boolean(xml_xpath_cast_to_boolean(&eval(b, ctx)?)))
1061            }
1062        }
1063        Expr::And(a, b) => {
1064            let x = xml_xpath_cast_to_boolean(&eval(a, ctx)?);
1065            if !x {
1066                Ok(XPathObject::Boolean(false))
1067            } else {
1068                Ok(XPathObject::Boolean(xml_xpath_cast_to_boolean(&eval(b, ctx)?)))
1069            }
1070        }
1071        Expr::Eq(a, b, eq) => {
1072            let l = eval(a, ctx)?;
1073            let r = eval(b, ctx)?;
1074            Ok(XPathObject::Boolean(compare_eq(&l, &r, ctx) == *eq))
1075        }
1076        Expr::Rel(a, b, ord, or_eq) => {
1077            let ln = xml_xpath_cast_to_number(&eval(a, ctx)?, ctx);
1078            let rn = xml_xpath_cast_to_number(&eval(b, ctx)?, ctx);
1079            let c = ln.partial_cmp(&rn);
1080            let ok = match c {
1081                Some(o) if o == *ord => true,
1082                Some(Ordering::Equal) if *or_eq => true,
1083                _ => false,
1084            };
1085            Ok(XPathObject::Boolean(ok))
1086        }
1087        Expr::Add(a, b, plus) => {
1088            let ln = xml_xpath_cast_to_number(&eval(a, ctx)?, ctx);
1089            let rn = xml_xpath_cast_to_number(&eval(b, ctx)?, ctx);
1090            Ok(XPathObject::Number(if *plus { ln + rn } else { ln - rn }))
1091        }
1092        Expr::Mul(a, b, op) => {
1093            let ln = xml_xpath_cast_to_number(&eval(a, ctx)?, ctx);
1094            let rn = xml_xpath_cast_to_number(&eval(b, ctx)?, ctx);
1095            Ok(XPathObject::Number(match op {
1096                '*' => ln * rn,
1097                'd' => ln / rn,
1098                _ => ln % rn,
1099            }))
1100        }
1101        Expr::Neg(a) => Ok(XPathObject::Number(-xml_xpath_cast_to_number(&eval(a, ctx)?, ctx))),
1102        Expr::Union(a, b) => {
1103            let mut ns = nodeset(eval(a, ctx)?);
1104            ns.extend(nodeset(eval(b, ctx)?));
1105            Ok(XPathObject::NodeSet(sort_unique(ctx.doc, ns)))
1106        }
1107        Expr::Path(p) => eval_path(p, ctx),
1108        Expr::Steps { base, steps } => {
1109            let mut nodes = nodeset(eval(base, ctx)?);
1110            for step in steps {
1111                let mut next = Vec::new();
1112                for n in nodes {
1113                    next.extend(axis_nodes(ctx.doc, n, step.axis, &step.test, &ctx.ns));
1114                }
1115                nodes = filter_preds(ctx, sort_unique(ctx.doc, next), &step.preds)?;
1116            }
1117            Ok(XPathObject::NodeSet(nodes))
1118        }
1119        Expr::Filter(e, preds) => {
1120            let o = eval(e, ctx)?;
1121            let ns = nodeset(o);
1122            Ok(XPathObject::NodeSet(filter_preds(ctx, ns, preds)?))
1123        }
1124        Expr::Literal(s) => Ok(XPathObject::String(s.clone())),
1125        Expr::Number(n) => Ok(XPathObject::Number(*n)),
1126        Expr::Var(_) => Err("variables not bound".into()),
1127        Expr::Fun { name, args } => eval_fun(name, args, ctx),
1128    }
1129}
1130
1131fn compare_eq(l: &XPathObject, r: &XPathObject, ctx: &XmlXPathContext<'_>) -> bool {
1132    match (l, r) {
1133        (XPathObject::Boolean(_), _) | (_, XPathObject::Boolean(_)) => {
1134            xml_xpath_cast_to_boolean(l) == xml_xpath_cast_to_boolean(r)
1135        }
1136        (XPathObject::Number(_), _) | (_, XPathObject::Number(_)) => {
1137            xml_xpath_cast_to_number(l, ctx) == xml_xpath_cast_to_number(r, ctx)
1138        }
1139        _ => xml_xpath_cast_to_string(l, ctx) == xml_xpath_cast_to_string(r, ctx),
1140    }
1141}
1142
1143fn nodeset(o: XPathObject) -> Vec<NodeId> {
1144    match o {
1145        XPathObject::NodeSet(v) => v,
1146        _ => vec![],
1147    }
1148}
1149
1150fn sort_unique(doc: &XmlDoc, mut v: Vec<NodeId>) -> Vec<NodeId> {
1151    let order = xml_xpath_order_doc_elems(doc);
1152    v.sort_by_key(|n| order.iter().position(|x| x == n).unwrap_or(usize::MAX));
1153    v.dedup();
1154    v
1155}
1156
1157fn eval_path(path: &PathExpr, ctx: &XmlXPathContext<'_>) -> Result<XPathObject, String> {
1158    let mut nodes = if path.abs {
1159        vec![NodeId::DOCUMENT]
1160    } else {
1161        vec![ctx.node]
1162    };
1163    if path.steps.is_empty() && path.abs {
1164        return Ok(XPathObject::NodeSet(nodes));
1165    }
1166    for step in &path.steps {
1167        let mut next = Vec::new();
1168        for n in nodes {
1169            next.extend(axis_nodes(ctx.doc, n, step.axis, &step.test, &ctx.ns));
1170        }
1171        nodes = filter_preds(ctx, sort_unique(ctx.doc, next), &step.preds)?;
1172    }
1173    Ok(XPathObject::NodeSet(nodes))
1174}
1175
1176fn filter_preds(
1177    ctx: &XmlXPathContext<'_>,
1178    nodes: Vec<NodeId>,
1179    preds: &[Expr],
1180) -> Result<Vec<NodeId>, String> {
1181    let mut cur = nodes;
1182    for pred in preds {
1183        let size = cur.len();
1184        let mut kept = Vec::new();
1185        for (i, n) in cur.iter().enumerate() {
1186            let c2 = XmlXPathContext {
1187                doc: ctx.doc,
1188                node: *n,
1189                position: i + 1,
1190                size,
1191                ns: ctx.ns.clone(),
1192            };
1193            let v = eval(pred, &c2)?;
1194            let pass = match v {
1195                XPathObject::Number(num) => (num as usize) == c2.position,
1196                other => xml_xpath_cast_to_boolean(&other),
1197            };
1198            if pass {
1199                kept.push(*n);
1200            }
1201        }
1202        cur = kept;
1203    }
1204    Ok(cur)
1205}
1206
1207fn axis_nodes(
1208    doc: &XmlDoc,
1209    n: NodeId,
1210    axis: Axis,
1211    test: &NodeTest,
1212    ns: &[(String, String)],
1213) -> Vec<NodeId> {
1214    let mut raw = Vec::new();
1215    match axis {
1216        Axis::SelfAxis => raw.push(n),
1217        Axis::Child => {
1218            let mut c = doc.first_child(n);
1219            while let Some(x) = c {
1220                raw.push(x);
1221                c = doc.next_sibling(x);
1222            }
1223        }
1224        Axis::Attribute => {
1225            let mut a = doc.first_attr(n);
1226            while let Some(x) = a {
1227                raw.push(x);
1228                a = doc.next_sibling(x);
1229            }
1230        }
1231        Axis::Parent => {
1232            if let Some(p) = doc.parent(n) {
1233                raw.push(p);
1234            }
1235        }
1236        Axis::Ancestor => {
1237            let mut p = doc.parent(n);
1238            while let Some(x) = p {
1239                raw.push(x);
1240                p = doc.parent(x);
1241            }
1242        }
1243        Axis::AncestorOrSelf => {
1244            raw.push(n);
1245            let mut p = doc.parent(n);
1246            while let Some(x) = p {
1247                raw.push(x);
1248                p = doc.parent(x);
1249            }
1250        }
1251        Axis::Descendant => collect_desc(doc, n, &mut raw, false),
1252        Axis::DescendantOrSelf => collect_desc(doc, n, &mut raw, true),
1253        Axis::FollowingSibling => {
1254            let mut s = doc.next_sibling(n);
1255            while let Some(x) = s {
1256                raw.push(x);
1257                s = doc.next_sibling(x);
1258            }
1259        }
1260        Axis::PrecedingSibling => {
1261            let mut s = doc.prev_sibling(n);
1262            let mut v = Vec::new();
1263            while let Some(x) = s {
1264                v.push(x);
1265                s = doc.prev_sibling(x);
1266            }
1267            v.reverse();
1268            raw.extend(v);
1269        }
1270        Axis::Following => {
1271            let mut cur = n;
1272            loop {
1273                if let Some(s) = doc.next_sibling(cur) {
1274                    collect_desc(doc, s, &mut raw, true);
1275                    let mut t = doc.next_sibling(s);
1276                    while let Some(x) = t {
1277                        collect_desc(doc, x, &mut raw, true);
1278                        t = doc.next_sibling(x);
1279                    }
1280                    cur = s;
1281                    // climb for more following
1282                    if let Some(p) = doc.parent(cur) {
1283                        cur = p;
1284                        continue;
1285                    }
1286                } else if let Some(p) = doc.parent(cur) {
1287                    cur = p;
1288                    continue;
1289                }
1290                break;
1291            }
1292        }
1293        Axis::Preceding => {
1294            // nodes before n in document order, excluding ancestors
1295            let order = xml_xpath_order_doc_elems(doc);
1296            let mut ancestors = Vec::new();
1297            let mut p = Some(n);
1298            while let Some(x) = p {
1299                ancestors.push(x);
1300                p = doc.parent(x);
1301            }
1302            if let Some(idx) = order.iter().position(|x| *x == n) {
1303                for &id in &order[..idx] {
1304                    if !ancestors.contains(&id) {
1305                        raw.push(id);
1306                    }
1307                }
1308            }
1309        }
1310        Axis::Namespace => {
1311            for (pre, _) in doc.ns_defs(n) {
1312                let dummy = n; // namespace nodes not first-class; skip
1313                let _ = (pre, dummy);
1314            }
1315        }
1316    }
1317    raw.into_iter()
1318        .filter(|id| node_test(doc, *id, test, ns, axis))
1319        .collect()
1320}
1321
1322fn collect_desc(doc: &XmlDoc, n: NodeId, out: &mut Vec<NodeId>, include_self: bool) {
1323    if include_self {
1324        out.push(n);
1325    }
1326    let mut c = doc.first_child(n);
1327    while let Some(x) = c {
1328        collect_desc(doc, x, out, true);
1329        c = doc.next_sibling(x);
1330    }
1331}
1332
1333fn node_test(
1334    doc: &XmlDoc,
1335    id: NodeId,
1336    test: &NodeTest,
1337    ns: &[(String, String)],
1338    axis: Axis,
1339) -> bool {
1340    match test {
1341        NodeTest::Node => true,
1342        NodeTest::Star => {
1343            if axis == Axis::Attribute {
1344                doc.kind(id) == NodeKind::Attribute
1345            } else {
1346                doc.kind(id) == NodeKind::Element
1347            }
1348        }
1349        NodeTest::Text => doc.kind(id) == NodeKind::Text || doc.kind(id) == NodeKind::CData,
1350        NodeTest::Comment => doc.kind(id) == NodeKind::Comment,
1351        NodeTest::Pi(t) => {
1352            doc.kind(id) == NodeKind::Pi && t.as_deref().map(|x| x == doc.name(id)).unwrap_or(true)
1353        }
1354        NodeTest::Name(local, prefix) => {
1355            let kind_ok = if axis == Axis::Attribute {
1356                doc.kind(id) == NodeKind::Attribute
1357            } else {
1358                doc.kind(id) == NodeKind::Element
1359            };
1360            if !kind_ok || doc.name(id) != local {
1361                return false;
1362            }
1363            if let Some(p) = prefix {
1364                let href = ns.iter().find(|(a, _)| a == p).map(|(_, h)| h.as_str());
1365                match href {
1366                    Some(h) => doc.ns_uri(id) == Some(h),
1367                    None => doc.prefix(id) == Some(p.as_str()),
1368                }
1369            } else {
1370                true
1371            }
1372        }
1373    }
1374}
1375
1376fn eval_fun(name: &str, args: &[Expr], ctx: &XmlXPathContext<'_>) -> Result<XPathObject, String> {
1377    let ev = |i: usize| eval(&args[i], ctx);
1378    let local = name.rsplit(':').next().unwrap_or(name);
1379    match local {
1380        "true" => Ok(XPathObject::Boolean(true)),
1381        "false" => Ok(XPathObject::Boolean(false)),
1382        "not" => Ok(XPathObject::Boolean(!xml_xpath_cast_to_boolean(&ev(0)?))),
1383        "boolean" => Ok(XPathObject::Boolean(xml_xpath_cast_to_boolean(&ev(0)?))),
1384        "number" => {
1385            let o = if args.is_empty() {
1386                XPathObject::NodeSet(vec![ctx.node])
1387            } else {
1388                ev(0)?
1389            };
1390            Ok(XPathObject::Number(xml_xpath_cast_to_number(&o, ctx)))
1391        }
1392        "string" => {
1393            let o = if args.is_empty() {
1394                XPathObject::NodeSet(vec![ctx.node])
1395            } else {
1396                ev(0)?
1397            };
1398            Ok(XPathObject::String(xml_xpath_cast_to_string(&o, ctx)))
1399        }
1400        "last" => Ok(XPathObject::Number(ctx.size as f64)),
1401        "position" => Ok(XPathObject::Number(ctx.position as f64)),
1402        "count" => Ok(XPathObject::Number(nodeset(ev(0)?).len() as f64)),
1403        "local-name" => {
1404            let ns = if args.is_empty() {
1405                vec![ctx.node]
1406            } else {
1407                nodeset(ev(0)?)
1408            };
1409            Ok(XPathObject::String(
1410                ns.first().map(|id| ctx.doc.name(*id).to_string()).unwrap_or_default(),
1411            ))
1412        }
1413        "name" => {
1414            let ns = if args.is_empty() {
1415                vec![ctx.node]
1416            } else {
1417                nodeset(ev(0)?)
1418            };
1419            Ok(XPathObject::String(
1420                ns.first().map(|id| ctx.doc.qname(*id)).unwrap_or_default(),
1421            ))
1422        }
1423        "namespace-uri" => {
1424            let ns = if args.is_empty() {
1425                vec![ctx.node]
1426            } else {
1427                nodeset(ev(0)?)
1428            };
1429            Ok(XPathObject::String(
1430                ns.first()
1431                    .and_then(|id| ctx.doc.ns_uri(*id).map(str::to_string))
1432                    .unwrap_or_default(),
1433            ))
1434        }
1435        "concat" => {
1436            let mut s = String::new();
1437            for a in args {
1438                s.push_str(&xml_xpath_cast_to_string(&eval(a, ctx)?, ctx));
1439            }
1440            Ok(XPathObject::String(s))
1441        }
1442        "starts-with" => {
1443            let a = xml_xpath_cast_to_string(&ev(0)?, ctx);
1444            let b = xml_xpath_cast_to_string(&ev(1)?, ctx);
1445            Ok(XPathObject::Boolean(a.starts_with(&b)))
1446        }
1447        "contains" => {
1448            let a = xml_xpath_cast_to_string(&ev(0)?, ctx);
1449            let b = xml_xpath_cast_to_string(&ev(1)?, ctx);
1450            Ok(XPathObject::Boolean(a.contains(&b)))
1451        }
1452        "substring-before" => {
1453            let a = xml_xpath_cast_to_string(&ev(0)?, ctx);
1454            let b = xml_xpath_cast_to_string(&ev(1)?, ctx);
1455            Ok(XPathObject::String(
1456                a.split_once(&b).map(|(x, _)| x.to_string()).unwrap_or_default(),
1457            ))
1458        }
1459        "substring-after" => {
1460            let a = xml_xpath_cast_to_string(&ev(0)?, ctx);
1461            let b = xml_xpath_cast_to_string(&ev(1)?, ctx);
1462            Ok(XPathObject::String(
1463                a.split_once(&b).map(|(_, x)| x.to_string()).unwrap_or_default(),
1464            ))
1465        }
1466        "substring" => {
1467            let s = xml_xpath_cast_to_string(&ev(0)?, ctx);
1468            let start = xpath_round(xml_xpath_cast_to_number(&ev(1)?, ctx));
1469            let end = if args.len() > 2 {
1470                start + xpath_round(xml_xpath_cast_to_number(&ev(2)?, ctx))
1471            } else {
1472                f64::INFINITY
1473            };
1474            let out: String = s
1475                .chars()
1476                .enumerate()
1477                .filter(|(i, _)| {
1478                    let pos = (*i as f64) + 1.0;
1479                    pos >= start && pos < end
1480                })
1481                .map(|(_, c)| c)
1482                .collect();
1483            Ok(XPathObject::String(out))
1484        }
1485        "string-length" => {
1486            let s = if args.is_empty() {
1487                ctx.doc.xml_node_get_content(ctx.node)
1488            } else {
1489                xml_xpath_cast_to_string(&ev(0)?, ctx)
1490            };
1491            Ok(XPathObject::Number(s.chars().count() as f64))
1492        }
1493        "normalize-space" => {
1494            let s = if args.is_empty() {
1495                ctx.doc.xml_node_get_content(ctx.node)
1496            } else {
1497                xml_xpath_cast_to_string(&ev(0)?, ctx)
1498            };
1499            Ok(XPathObject::String(
1500                s.split_whitespace().collect::<Vec<_>>().join(" "),
1501            ))
1502        }
1503        "translate" => {
1504            let s = xml_xpath_cast_to_string(&ev(0)?, ctx);
1505            let from: Vec<char> = xml_xpath_cast_to_string(&ev(1)?, ctx).chars().collect();
1506            let to: Vec<char> = xml_xpath_cast_to_string(&ev(2)?, ctx).chars().collect();
1507            let out: String = s
1508                .chars()
1509                .filter_map(|c| {
1510                    if let Some(i) = from.iter().position(|x| *x == c) {
1511                        to.get(i).copied()
1512                    } else {
1513                        Some(c)
1514                    }
1515                })
1516                .collect();
1517            Ok(XPathObject::String(out))
1518        }
1519        "floor" => Ok(XPathObject::Number(
1520            xml_xpath_cast_to_number(&ev(0)?, ctx).floor(),
1521        )),
1522        "ceiling" => Ok(XPathObject::Number(
1523            xml_xpath_cast_to_number(&ev(0)?, ctx).ceil(),
1524        )),
1525        "round" => {
1526            let n = xml_xpath_cast_to_number(&ev(0)?, ctx);
1527            Ok(XPathObject::Number(xpath_round(n)))
1528        }
1529        "sum" => {
1530            let ns = nodeset(ev(0)?);
1531            let mut t = 0.0;
1532            for id in ns {
1533                t += xpath_number(&ctx.doc.xml_node_get_content(id));
1534            }
1535            Ok(XPathObject::Number(t))
1536        }
1537        "id" => {
1538            let ids = xml_xpath_cast_to_string(&ev(0)?, ctx);
1539            let mut found = Vec::new();
1540            let order = xml_xpath_order_doc_elems(ctx.doc);
1541            for tok in ids.split_whitespace() {
1542                for id in &order {
1543                    if ctx.doc.kind(*id) == NodeKind::Element
1544                        && ctx.doc.xml_get_prop(*id, "id").as_deref() == Some(tok)
1545                    {
1546                        found.push(*id);
1547                    }
1548                }
1549            }
1550            Ok(XPathObject::NodeSet(sort_unique(ctx.doc, found)))
1551        }
1552        "lang" => {
1553            let want = xml_xpath_cast_to_string(&ev(0)?, ctx).to_ascii_lowercase();
1554            let mut cur = Some(ctx.node);
1555            let mut lang = None;
1556            while let Some(id) = cur {
1557                if let Some(l) = ctx.doc.xml_get_prop(id, "lang") {
1558                    lang = Some(l);
1559                    break;
1560                }
1561                cur = ctx.doc.parent(id);
1562            }
1563            let ok = lang
1564                .map(|l| {
1565                    let l = l.to_ascii_lowercase();
1566                    l == want || l.starts_with(&format!("{want}-"))
1567                })
1568                .unwrap_or(false);
1569            Ok(XPathObject::Boolean(ok))
1570        }
1571        _ => Err(format!("unknown function {name}")),
1572    }
1573}
1574
1575fn xpath_round(n: f64) -> f64 {
1576    if n.is_nan() || n.is_infinite() {
1577        return n;
1578    }
1579    if n == 0.0 {
1580        return n;
1581    }
1582    // XPath round: floor(n+0.5) except negative half toward +inf
1583    if n >= 0.0 {
1584        (n + 0.5).floor()
1585    } else {
1586        let f = (n.abs() + 0.5).floor();
1587        if (n.abs() - n.abs().floor()) == 0.5 {
1588            -n.abs().floor()
1589        } else {
1590            -f
1591        }
1592    }
1593}