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