#![forbid(unsafe_code)]
pub mod ast;
pub mod context;
pub mod eval;
pub mod exslt;
pub mod pattern;
pub mod rtf;
mod index;
mod lexer;
mod parser;
mod bindings_builder;
pub use bindings_builder::XPathBindingsBuilder;
pub use ast::{Axis, Expr, LocationPath, NodeTest, Step};
pub use ast::{FunctionSig, ItemType, Occurrence, SequenceType};
pub use parser::parse_sequence_type_str;
pub use context::{DocIndex, INodeKind, is_synthetic_id};
pub use eval::Value as XPathValue;
pub use eval::compile_xpath_2_0_regex;
pub use index::{DocIndexLike, NodeId, XPathNodeKind};
use crate::error::Result;
use sup_xml_tree::dom::Document as ArenaDocument;
#[derive(Debug, Clone)]
pub struct XPathOptions {
pub libxml2_compatible: bool,
pub xpath_2_0: bool,
pub max_eval_steps: u64,
}
impl Default for XPathOptions {
fn default() -> Self {
Self {
libxml2_compatible: false,
xpath_2_0: false,
max_eval_steps: eval::DEFAULT_MAX_EVAL_STEPS,
}
}
}
pub fn parse_xpath(src: &str) -> Result<Expr> {
parse_xpath_with(src, &XPathOptions::default())
}
pub fn parse_xpath_with(src: &str, opts: &XPathOptions) -> Result<Expr> {
let allow_exponent = opts.libxml2_compatible || opts.xpath_2_0;
let (tokens, spans) = lexer::tokenize_with(src, allow_exponent)?;
let mut p = parser::Parser::new_with_spans(tokens, spans, src);
p.set_xpath_2_0(opts.xpath_2_0);
let expr = p.parse_expr()?;
p.expect_eof()?;
let depth = ast::max_predicate_nesting(&expr);
if depth > MAX_PREDICATE_NESTING_DEPTH {
use crate::error::{ErrorDomain, ErrorLevel, XmlError};
return Err(XmlError::new(
ErrorDomain::XPath,
ErrorLevel::Error,
format!(
"XPath predicate nesting depth ({depth}) exceeds limit \
({MAX_PREDICATE_NESTING_DEPTH}); evaluator complexity is \
O(N^k) in document size N and predicate-nesting depth k"
),
));
}
Ok(expr)
}
pub const MAX_PREDICATE_NESTING_DEPTH: u32 = 8;
pub struct XPathContext<'doc> {
pub index: DocIndex<'doc>,
doc: &'doc ArenaDocument,
options: XPathOptions,
}
impl<'doc> XPathContext<'doc> {
pub fn new(doc: &'doc ArenaDocument) -> Self {
Self::new_with(doc, XPathOptions::default())
}
pub fn new_with(doc: &'doc ArenaDocument, options: XPathOptions) -> Self {
Self { index: DocIndex::build(doc), doc, options }
}
pub fn eval(&self, src: &str) -> Result<XPathValue> {
self.eval_with(src, 0, &eval::NoBindings)
}
pub fn eval_at(&self, src: &str, context_node: NodeId) -> Result<XPathValue> {
self.eval_with(src, context_node, &eval::NoBindings)
}
pub fn eval_with(
&self,
src: &str,
context_node: NodeId,
bindings: &dyn eval::XPathBindings,
) -> Result<XPathValue> {
let expr = parse_xpath_with(src, &self.options)?;
eval::validate_prefixes(&expr, bindings)?;
eval::set_eval_budget(self.options.max_eval_steps);
eval::reset_eval_budget();
eval::refresh_stable_now();
let static_ctx = eval::StaticContext {
xpath_2_0: self.options.xpath_2_0,
xpath_3_0: false,
libxml2_compatible: self.options.libxml2_compatible,
current_node: Some(context_node),
};
eval::eval_expr(
&expr,
&eval::EvalCtx {
context_node, pos: 1, size: 1, bindings,
static_ctx: &static_ctx,
},
&self.index,
)
}
pub fn id_for_element(&self, ptr: *const sup_xml_tree::dom::Node<'_>) -> Option<NodeId> {
use crate::xpath::context::INodeKind;
if ptr.is_null() { return Some(0); }
let target = ptr as *const ();
for (i, n) in self.index.nodes.iter().enumerate() {
let matches = match n.kind {
INodeKind::Element(p) | INodeKind::Text(p) | INodeKind::Comment(p)
| INodeKind::CData(p) | INodeKind::PI(p)
=> (p as *const _ as *const ()) == target,
INodeKind::Attribute(a)
=> (a as *const _ as *const ()) == target,
_ => false,
};
if matches { return Some(i); }
}
None
}
pub fn eval_bool(&self, src: &str) -> Result<bool> {
Ok(eval::value_to_bool(&self.eval(src)?, &self.index))
}
pub fn eval_str(&self, src: &str) -> Result<String> {
Ok(eval::value_to_string(&self.eval(src)?, &self.index))
}
pub fn eval_num(&self, src: &str) -> Result<f64> {
Ok(eval::value_to_number(&self.eval(src)?, &self.index))
}
pub fn eval_strings(&self, src: &str) -> Result<Vec<String>> {
let v = self.eval(src)?;
Ok(match v {
XPathValue::NodeSet(ns) => ns.iter().map(|&id| self.index.string_value(id)).collect(),
other => vec![eval::value_to_string(&other, &self.index)],
})
}
pub fn eval_count(&self, src: &str) -> Result<usize> {
let v = self.eval(src)?;
Ok(match v { XPathValue::NodeSet(ns) => ns.len(), _ => 0 })
}
pub fn eval_node_xml(&self, src: &str) -> Result<Vec<String>> {
use crate::serializer::{serialize_node_to_string, serialize_with, SerializeOptions};
use crate::xpath::context::INodeKind;
let v = self.eval(src)?;
let ns = match v {
XPathValue::NodeSet(ns) => ns,
other => return Ok(vec![eval::value_to_string(&other, &self.index)]),
};
let opts = SerializeOptions::default();
let mut out = Vec::with_capacity(ns.len());
for id in ns {
let node = &self.index.nodes[id];
let serialized = match &node.kind {
INodeKind::Element(n) | INodeKind::Text(n)
| INodeKind::Comment(n) | INodeKind::CData(n)
| INodeKind::PI(n) => serialize_node_to_string(n, &opts),
INodeKind::Attribute(a) => {
let mut s = String::with_capacity(a.name().len() + a.value().len() + 3);
s.push_str(a.name());
s.push_str("=\"");
for ch in a.value().chars() {
match ch {
'&' => s.push_str("&"),
'<' => s.push_str("<"),
'"' => s.push_str("""),
c => s.push(c),
}
}
s.push('"');
s
}
INodeKind::Namespace { prefix: Some(p), uri } =>
format!("xmlns:{}=\"{}\"", p, uri),
INodeKind::Namespace { prefix: None, uri } =>
format!("xmlns=\"{}\"", uri),
INodeKind::Document => serialize_with(self.doc, &opts),
};
out.push(serialized);
}
Ok(out)
}
}
pub fn xpath_eval(doc: &ArenaDocument, src: &str) -> Result<XPathValue> {
XPathContext::new(doc).eval(src)
}
pub fn xpath_bool(doc: &ArenaDocument, src: &str) -> Result<bool> {
XPathContext::new(doc).eval_bool(src)
}
pub fn xpath_str(doc: &ArenaDocument, src: &str) -> Result<String> {
XPathContext::new(doc).eval_str(src)
}
pub fn xpath_num(doc: &ArenaDocument, src: &str) -> Result<f64> {
XPathContext::new(doc).eval_num(src)
}
pub fn xpath_strings(doc: &ArenaDocument, src: &str) -> Result<Vec<String>> {
XPathContext::new(doc).eval_strings(src)
}
pub fn xpath_count(doc: &ArenaDocument, src: &str) -> Result<usize> {
XPathContext::new(doc).eval_count(src)
}
#[cfg(test)]
mod arena_tests {
use super::*;
use crate::{parse_str, ParseOptions};
fn parse(xml: &str) -> ArenaDocument {
parse_str(xml, &ParseOptions::default()).expect("parse")
}
fn parse_ns(xml: &str) -> ArenaDocument {
let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
parse_str(xml, &opts).expect("parse")
}
#[test]
fn absolute_path() {
let doc = parse(r#"<catalog><book id="1"/><book id="2"/></catalog>"#);
assert_eq!(xpath_count(&doc, "/catalog/book").unwrap(), 2);
assert_eq!(xpath_count(&doc, "/catalog").unwrap(), 1);
}
#[test]
fn wildcard_children() {
let doc = parse("<r><a/><b/><c/></r>");
assert_eq!(xpath_count(&doc, "/r/*").unwrap(), 3);
}
#[test]
fn attribute_predicate() {
let doc = parse(r#"<r><book id="1"/><book id="2"/><book id="1"/></r>"#);
assert_eq!(xpath_count(&doc, "/r/book[@id='1']").unwrap(), 2);
assert!(xpath_bool(&doc, "/r/book[@id='1']").unwrap());
}
#[test]
fn current_in_nested_predicate_is_fixed_to_the_expression_context() {
let doc = parse("<s><phase id='m'><active pat='p1'/></phase>\
<p id='p1'/><p id='p2'/></s>");
let ctx = XPathContext::new(&doc);
let node_of = |q: &str| match ctx.eval(q).unwrap() {
XPathValue::NodeSet(ns) => ns[0],
other => panic!("expected node-set, got {other:?}"),
};
let expr = "../phase[@id='m']/active[@pat=current()/@id]";
let hit = ctx.eval_at(expr, node_of("//p[@id='p1']")).unwrap();
assert!(matches!(hit, XPathValue::NodeSet(ref ns) if ns.len() == 1),
"current() in the nested predicate must resolve to p1: {hit:?}");
let miss = ctx.eval_at(expr, node_of("//p[@id='p2']")).unwrap();
assert!(matches!(miss, XPathValue::NodeSet(ref ns) if ns.is_empty()),
"expected no match from p2: {miss:?}");
}
#[test]
fn attribute_value_extraction() {
let doc = parse(r#"<r id="42"/>"#);
assert_eq!(xpath_str(&doc, "/r/@id").unwrap(), "42");
}
#[test]
fn descendant_or_self() {
let doc = parse("<r><a><b><c/></b></a></r>");
assert_eq!(xpath_count(&doc, "//c").unwrap(), 1);
assert_eq!(xpath_count(&doc, "//*").unwrap(), 4); }
#[test]
fn string_value_extraction() {
let doc = parse("<r><title>Hello</title></r>");
assert_eq!(xpath_str(&doc, "/r/title").unwrap(), "Hello");
assert_eq!(xpath_str(&doc, "string(/r/title)").unwrap(), "Hello");
}
#[test]
fn string_concat_children() {
let doc = parse("<r>foo<b>bar</b>baz</r>");
assert_eq!(xpath_str(&doc, "string(/r)").unwrap(), "foobarbaz");
}
#[test]
fn name_functions() {
let doc = parse("<r><a/></r>");
assert_eq!(xpath_str(&doc, "name(/r)").unwrap(), "r");
assert_eq!(xpath_str(&doc, "name(/r/a)").unwrap(), "a");
}
#[test]
fn substring_basic() {
let doc = parse("<r/>");
assert_eq!(xpath_str(&doc, r#"substring("12345", 2, 3)"#).unwrap(), "234");
assert_eq!(xpath_str(&doc, r#"substring("12345", 2)"#).unwrap(), "2345");
assert_eq!(xpath_str(&doc, r#"substring("12345", 1.5, 2.6)"#).unwrap(), "234");
assert_eq!(xpath_str(&doc, r#"substring("12345", 0, 3)"#).unwrap(), "12");
}
#[test]
fn substring_negative_start() {
let doc = parse("<r/>");
assert_eq!(xpath_str(&doc, r#"substring("12345", -3, 5)"#).unwrap(), "1");
assert_eq!(xpath_str(&doc, r#"substring("abc", 10, 5)"#).unwrap(), "");
assert_eq!(xpath_str(&doc, r#"substring("abc", 1, 0)"#).unwrap(), "");
}
#[test]
fn substring_infinite_start_does_not_panic() {
let doc = parse("<r/>");
assert_eq!(xpath_str(&doc, r#"substring("hello", 1 div 0, 5)"#).unwrap(), "");
assert_eq!(xpath_str(&doc, r#"substring("hello", -1 div 0, 5)"#).unwrap(), "");
}
#[test]
fn substring_infinite_length() {
let doc = parse("<r/>");
assert_eq!(
xpath_str(&doc, r#"substring("12345", -42, 1 div 0)"#).unwrap(),
"12345",
);
assert_eq!(xpath_str(&doc, r#"substring("abc", 1, -1 div 0)"#).unwrap(), "");
}
#[test]
fn substring_nan_yields_empty() {
let doc = parse("<r/>");
assert_eq!(xpath_str(&doc, r#"substring("hello", 0 div 0, 5)"#).unwrap(), "");
assert_eq!(xpath_str(&doc, r#"substring("hello", 1, 0 div 0)"#).unwrap(), "");
assert_eq!(
xpath_str(&doc, r#"substring("hello", -1 div 0, 1 div 0)"#).unwrap(),
"",
);
}
#[test]
fn count_function() {
let doc = parse("<r><i/><i/><i/></r>");
assert_eq!(xpath_num(&doc, "count(/r/i)").unwrap(), 3.0);
}
#[test]
fn arithmetic() {
let doc = parse("<r/>");
assert_eq!(xpath_num(&doc, "2 + 3 * 4").unwrap(), 14.0);
assert_eq!(xpath_num(&doc, "10 div 4").unwrap(), 2.5);
}
#[test]
fn position_predicate() {
let doc = parse("<r><i>1</i><i>2</i><i>3</i></r>");
assert_eq!(xpath_str(&doc, "/r/i[1]").unwrap(), "1");
assert_eq!(xpath_str(&doc, "/r/i[2]").unwrap(), "2");
assert_eq!(xpath_str(&doc, "/r/i[last()]").unwrap(), "3");
}
#[test]
fn numeric_predicate_on_text() {
let doc = parse("<r><i>5</i><i>15</i><i>25</i></r>");
assert_eq!(xpath_count(&doc, "/r/i[. > 10]").unwrap(), 2);
}
struct TestNs(&'static [(&'static str, &'static str)]);
impl eval::XPathBindings for TestNs {
fn resolve_prefix(&self, prefix: &str) -> Option<String> {
self.0.iter().find(|(p, _)| *p == prefix).map(|(_, u)| (*u).to_string())
}
}
#[test]
fn prefixed_element_addressable_by_qname() {
let doc = parse_ns(r#"<r xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>X</dc:title></r>"#);
let ctx = XPathContext::new(&doc);
let bind = TestNs(&[("dc", "http://purl.org/dc/elements/1.1/")]);
let v = ctx.eval_with("namespace-uri(/r/dc:title)", 0, &bind).unwrap();
assert_eq!(eval::value_to_string(&v, &ctx.index),
"http://purl.org/dc/elements/1.1/");
let v = ctx.eval_with("local-name(/r/dc:title)", 0, &bind).unwrap();
assert_eq!(eval::value_to_string(&v, &ctx.index), "title");
}
#[test]
fn undefined_prefix_in_qname_errors() {
let doc = parse_ns(r#"<r xmlns:fa="urn:x"><fa:d/></r>"#);
let err = xpath_str(&doc, "/fa:d").unwrap_err();
assert!(
err.message.contains("Undefined namespace prefix"),
"expected undefined-prefix error, got: {}", err.message,
);
}
#[test]
fn undefined_prefix_in_prefix_wildcard_errors() {
let doc = parse_ns(r#"<r xmlns:fa="urn:x"><fa:d/></r>"#);
let err = xpath_str(&doc, "/fa:*").unwrap_err();
assert!(
err.message.contains("Undefined namespace prefix"),
"expected undefined-prefix error, got: {}", err.message,
);
}
#[test]
fn undefined_prefix_in_predicate_errors() {
let doc = parse_ns("<r><a/></r>");
let err = xpath_str(&doc, "/r/a[fa:flag]").unwrap_err();
assert!(
err.message.contains("Undefined namespace prefix"),
"expected undefined-prefix error, got: {}", err.message,
);
}
#[test]
fn namespace_axis_implicit_xml_prefix() {
let doc = parse_ns("<r/>");
assert_eq!(xpath_count(&doc, "/r/namespace::*").unwrap(), 1);
assert_eq!(
xpath_str(&doc, "string(/r/namespace::*[1])").unwrap(),
"http://www.w3.org/XML/1998/namespace",
);
}
#[test]
fn namespace_axis_one_declared_plus_xml() {
let doc = parse_ns(
r#"<r xmlns:dc="http://purl.org/dc/elements/1.1/"><c/></r>"#,
);
assert_eq!(xpath_count(&doc, "/r/namespace::*").unwrap(), 2);
assert_eq!(xpath_count(&doc, "/r/c/namespace::*").unwrap(), 2);
}
#[test]
fn namespace_axis_name_test_selects_by_prefix() {
let doc = parse_ns(
r#"<r xmlns:dc="http://purl.org/dc/elements/1.1/"/>"#,
);
assert_eq!(
xpath_str(&doc, "string(/r/namespace::dc)").unwrap(),
"http://purl.org/dc/elements/1.1/",
);
assert_eq!(
xpath_str(&doc, "local-name(/r/namespace::dc)").unwrap(),
"dc",
);
}
#[test]
fn namespace_axis_default_namespace_has_empty_prefix() {
let doc = parse_ns(r#"<r xmlns="http://example.com/ns"/>"#);
let ctx = XPathContext::new(&doc);
let bind = TestNs(&[("x", "http://example.com/ns")]);
let v = ctx.eval_with("count(/x:r/namespace::*)", 0, &bind).unwrap();
assert_eq!(eval::value_to_number(&v, &ctx.index), 2.0);
let v = ctx
.eval_with("string(/x:r/namespace::*[local-name()=''])", 0, &bind)
.unwrap();
assert_eq!(eval::value_to_string(&v, &ctx.index), "http://example.com/ns");
}
#[test]
fn namespace_axis_inherits_from_ancestor() {
let doc = parse_ns(
r#"<r xmlns:a="urn:a"><mid xmlns:b="urn:b"><leaf/></mid></r>"#,
);
assert_eq!(xpath_count(&doc, "/r/mid/leaf/namespace::*").unwrap(), 3);
assert_eq!(
xpath_str(&doc, "string(/r/mid/leaf/namespace::a)").unwrap(),
"urn:a",
);
assert_eq!(
xpath_str(&doc, "string(/r/mid/leaf/namespace::b)").unwrap(),
"urn:b",
);
}
#[test]
fn namespace_axis_shadows_ancestor() {
let doc = parse_ns(
r#"<r xmlns:p="urn:outer"><c xmlns:p="urn:inner"/></r>"#,
);
assert_eq!(
xpath_str(&doc, "string(/r/namespace::p)").unwrap(),
"urn:outer",
);
assert_eq!(
xpath_str(&doc, "string(/r/c/namespace::p)").unwrap(),
"urn:inner",
);
assert_eq!(xpath_count(&doc, "/r/c/namespace::*").unwrap(), 2);
}
#[test]
fn namespace_axis_default_undeclaration() {
let doc = parse_ns(
r#"<r xmlns="urn:outer"><c xmlns=""/></r>"#,
);
let ctx = XPathContext::new(&doc);
let bind = TestNs(&[("x", "urn:outer")]);
let r_count = ctx.eval_with("count(/x:r/namespace::*)", 0, &bind).unwrap();
assert_eq!(eval::value_to_number(&r_count, &ctx.index), 2.0);
let c_count = ctx.eval_with("count(/x:r/c/namespace::*)", 0, &bind).unwrap();
assert_eq!(eval::value_to_number(&c_count, &ctx.index), 1.0);
}
const EXSLT_NS: &[(&str, &str)] = &[
("math", "http://exslt.org/math"),
("date", "http://exslt.org/dates-and-times"),
("str", "http://exslt.org/strings"),
("set", "http://exslt.org/sets"),
];
#[test]
fn exslt_math_max_via_xpath() {
let doc = parse("<r><i>3</i><i>7</i><i>1</i><i>5</i></r>");
let ctx = XPathContext::new(&doc);
let v = ctx.eval_with("math:max(/r/i)", 0, &TestNs(EXSLT_NS)).unwrap();
assert_eq!(eval::value_to_number(&v, &ctx.index), 7.0);
}
#[test]
fn exslt_str_padding_via_xpath() {
let doc = parse("<r/>");
let ctx = XPathContext::new(&doc);
let v = ctx.eval_with("str:padding(5, '*')", 0, &TestNs(EXSLT_NS)).unwrap();
assert_eq!(eval::value_to_string(&v, &ctx.index), "*****");
}
#[test]
fn exslt_set_distinct_via_xpath() {
let doc = parse("<r><i>x</i><i>y</i><i>x</i><i>y</i><i>z</i></r>");
let ctx = XPathContext::new(&doc);
let v = ctx.eval_with("count(set:distinct(/r/i))", 0, &TestNs(EXSLT_NS)).unwrap();
assert_eq!(eval::value_to_number(&v, &ctx.index), 3.0);
}
#[test]
fn exslt_date_year_via_xpath() {
let doc = parse("<r/>");
let ctx = XPathContext::new(&doc);
let v = ctx.eval_with(
"date:year('2024-07-04T12:00:00Z')", 0, &TestNs(EXSLT_NS),
).unwrap();
assert_eq!(eval::value_to_number(&v, &ctx.index), 2024.0);
}
#[test]
fn exslt_prefix_is_user_choice() {
let doc = parse("<r><i>5</i><i>3</i></r>");
let ctx = XPathContext::new(&doc);
let bind = TestNs(&[("MATHS", "http://exslt.org/math")]);
let v = ctx.eval_with("MATHS:min(/r/i)", 0, &bind).unwrap();
assert_eq!(eval::value_to_number(&v, &ctx.index), 3.0);
}
#[test]
fn unknown_exslt_function_in_registered_ns_errors() {
let doc = parse("<r/>");
let ctx = XPathContext::new(&doc);
let err = ctx.eval_with(
"math:does-not-exist()", 0, &TestNs(EXSLT_NS),
).unwrap_err();
assert!(
err.message.contains("Unregistered XPath function"),
"unexpected error: {}", err.message,
);
}
#[test]
fn union_dedups() {
let doc = parse("<r><a/><b/></r>");
assert_eq!(xpath_count(&doc, "/r/a | /r/b | /r/a").unwrap(), 2);
}
#[test]
fn boolean_and_or() {
let doc = parse("<r><a/><b/></r>");
assert!(xpath_bool(&doc, "/r/a or /r/missing").unwrap());
assert!(!xpath_bool(&doc, "/r/a and /r/missing").unwrap());
}
#[test]
fn context_amortises_index_build() {
let doc = parse("<r><book/><book/><book/></r>");
let ctx = XPathContext::new(&doc);
assert_eq!(ctx.eval_count("/r/book").unwrap(), 3);
assert_eq!(ctx.eval_count("//book").unwrap(), 3);
assert!(ctx.eval_bool("/r").unwrap());
}
fn eval31(src: &str) -> String {
let doc = parse("<r/>");
let opts = XPathOptions { xpath_2_0: true, ..XPathOptions::default() };
let ctx = XPathContext::new_with(&doc, opts);
let bind = TestNs(&[
("map", "http://www.w3.org/2005/xpath-functions/map"),
("array", "http://www.w3.org/2005/xpath-functions/array"),
("math", "http://www.w3.org/2005/xpath-functions/math"),
]);
eval::value_to_string(&ctx.eval_with(src, 0, &bind).expect("eval"), &ctx.index)
}
#[test]
fn inline_function_and_dynamic_call() {
assert_eq!(eval31("let $f := function($x) { $x * 2 } return $f(21)"), "42");
}
#[test]
fn inline_function_captures_closure() {
assert_eq!(
eval31("let $n := 10 return (function($x) { $x + $n })(5)"),
"15");
}
#[test]
fn let_single_and_chained_bindings() {
assert_eq!(eval31("let $x := 40 return $x + 2"), "42");
assert_eq!(eval31("let $x := 3, $y := $x * 4 return $x + $y"), "15");
}
#[test]
fn let_binds_whole_sequence() {
assert_eq!(eval31("let $s := (1, 2, 3, 4) return count($s)"), "4");
assert_eq!(eval31("let $s := 1 to 5 return sum($s)"), "15");
}
#[test]
fn let_nested_in_for() {
assert_eq!(
eval31("string-join(for $i in 1 to 3 return \
let $sq := $i * $i return string($sq), ' ')"),
"1 4 9");
}
#[test]
fn parse_json_objects_arrays_scalars() {
assert_eq!(eval31("parse-json('[1,2,3]')?2"), "2");
assert_eq!(eval31("parse-json('{\"a\":10,\"b\":20}')?b"), "20");
assert_eq!(eval31("parse-json('{\"a\":[5,6,7]}')?a?3"), "7");
assert_eq!(eval31("parse-json('\"hi\\u0041\"')"), "hiA");
assert_eq!(eval31("parse-json('true')"), "true");
assert_eq!(eval31("count(parse-json('null'))"), "0");
assert_eq!(eval31("parse-json('{\"x\":null}')?x => count()"), "0");
}
#[test]
fn parse_json_duplicate_key_policies() {
assert_eq!(eval31("parse-json('{\"k\":1,\"k\":2}')?k"), "1");
assert_eq!(
eval31("parse-json('{\"k\":1,\"k\":2}', map{'duplicates':'use-last'})?k"),
"2");
}
#[test]
fn xml_to_json_round_trips_vocabulary() {
let doc = parse_ns(concat!(
r#"<map xmlns="http://www.w3.org/2005/xpath-functions">"#,
r#"<string key="a">hi</string>"#,
r#"<number key="n">42</number>"#,
r#"<boolean key="b">true</boolean>"#,
r#"<array key="xs"><number>1</number><number>2</number></array>"#,
r#"<null key="z"/>"#,
r#"</map>"#));
let opts = XPathOptions { xpath_2_0: true, ..XPathOptions::default() };
let ctx = XPathContext::new_with(&doc, opts);
let got = ctx.eval_str("xml-to-json(/)").unwrap();
assert_eq!(got, r#"{"a":"hi","n":42,"b":true,"xs":[1,2],"z":null}"#);
}
#[test]
fn let_rejected_under_xpath_1_0() {
let doc = parse("<r/>");
let ctx = XPathContext::new(&doc); assert!(
ctx.eval("let $x := 1 return $x").is_err(),
"`let` must not parse under XPath 1.0",
);
assert_eq!(eval31("let $x := 1 return $x"), "1");
}
#[test]
fn named_function_reference_via_for_each() {
assert_eq!(
eval31("string-join(for-each(('a','bb','ccc'), string-length#1), ',')"),
"1,2,3");
}
#[test]
fn fn_for_each_and_filter() {
assert_eq!(
eval31("string-join(for-each(1 to 4, function($x){ $x * $x }), ' ')"),
"1 4 9 16");
assert_eq!(
eval31("string-join(filter(1 to 6, function($x){ $x mod 2 = 0 }), ' ')"),
"2 4 6");
}
#[test]
fn fn_fold_left_right() {
assert_eq!(
eval31("fold-left(1 to 5, 0, function($a,$b){ $a + $b })"),
"15");
assert_eq!(
eval31("fold-right(('a','b','c'), '', function($x,$a){ concat($x,$a) })"),
"abc");
}
#[test]
fn map_for_each_higher_order() {
assert_eq!(
eval31("sum(map:for-each(map{'a':1,'b':2,'c':3}, function($k,$v){ $v }))"),
"6");
}
#[test]
fn array_for_each_and_fold() {
assert_eq!(
eval31("array:fold-left([1,2,3,4], 0, function($a,$b){ $a + $b })"),
"10");
assert_eq!(
eval31("array:size(array:for-each([1,2,3], function($x){ $x * $x }))"),
"3");
}
#[test]
fn fn_apply_and_arity() {
assert_eq!(eval31("function-arity(function($a,$b){ $a }) "), "2");
assert_eq!(eval31("apply(concat#3, ['a','b','c'])"), "abc");
}
#[test]
fn array_put_insert_remove() {
assert_eq!(eval31("array:size(array:put([1,2,3], 2, 9))"), "3");
assert_eq!(eval31("array:get(array:put([1,2,3], 2, 9), 2)"), "9");
assert_eq!(eval31("array:size(array:insert-before([1,2,3], 2, 9))"), "4");
assert_eq!(eval31("array:get(array:insert-before([1,2,3], 2, 9), 2)"), "9");
assert_eq!(eval31("array:get(array:insert-before([1,2,3], 4, 9), 4)"), "9");
assert_eq!(eval31("array:size(array:remove([1,2,3,4], (2,4)))"), "2");
assert_eq!(eval31("array:get(array:remove([1,2,3,4], (2,4)), 2)"), "3");
assert_eq!(eval31("array:size(array:remove([1,2,3], 2))"), "2");
}
}