use std::sync::Arc;
use super::signature::{FunctionArity, FunctionSignature};
use super::{eval_function, FunctionId, XPathValue, FUNCTION_REGISTRY};
use crate::types::sequence::SequenceType;
use crate::xpath::context::DynamicContext;
use crate::xpath::error::XPathError;
use crate::xpath::DomNavigator;
const CUSTOM_HANDLE_BASE: u32 = 0x1000_0000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FunctionHandle(pub(crate) u32);
impl FunctionHandle {
#[inline]
pub fn is_builtin(&self) -> bool {
self.0 < CUSTOM_HANDLE_BASE
}
#[inline]
pub fn is_custom(&self) -> bool {
self.0 >= CUSTOM_HANDLE_BASE
}
#[inline]
pub(crate) fn custom_index(&self) -> Option<usize> {
if self.is_custom() {
Some((self.0 - CUSTOM_HANDLE_BASE) as usize)
} else {
None
}
}
}
impl From<FunctionId> for FunctionHandle {
fn from(id: FunctionId) -> Self {
FunctionHandle(id as u32)
}
}
#[derive(Debug, Clone)]
pub struct DynamicFunctionSignature {
pub namespace: Arc<str>,
pub local_name: Arc<str>,
pub arity: FunctionArity,
pub param_types: Vec<SequenceType>,
pub return_type: SequenceType,
}
impl DynamicFunctionSignature {
pub fn new(
namespace: impl Into<Arc<str>>,
local_name: impl Into<Arc<str>>,
param_types: Vec<SequenceType>,
return_type: SequenceType,
) -> Self {
let arity = FunctionArity::Exact(param_types.len());
Self {
namespace: namespace.into(),
local_name: local_name.into(),
arity,
param_types,
return_type,
}
}
pub fn variadic(
namespace: impl Into<Arc<str>>,
local_name: impl Into<Arc<str>>,
min_args: usize,
param_types: Vec<SequenceType>,
return_type: SequenceType,
) -> Self {
Self {
namespace: namespace.into(),
local_name: local_name.into(),
arity: FunctionArity::Variadic(min_args),
param_types,
return_type,
}
}
pub fn range(
namespace: impl Into<Arc<str>>,
local_name: impl Into<Arc<str>>,
min_args: usize,
max_args: usize,
param_types: Vec<SequenceType>,
return_type: SequenceType,
) -> Self {
Self {
namespace: namespace.into(),
local_name: local_name.into(),
arity: FunctionArity::Range(min_args, max_args),
param_types,
return_type,
}
}
pub fn matches_arity(&self, count: usize) -> bool {
self.arity.matches(count)
}
}
impl From<&FunctionSignature> for DynamicFunctionSignature {
fn from(sig: &FunctionSignature) -> Self {
Self {
namespace: Arc::from(sig.namespace),
local_name: Arc::from(sig.local_name),
arity: sig.arity,
param_types: sig.param_types.clone(),
return_type: sig.return_type.clone(),
}
}
}
pub trait FunctionCatalog: std::fmt::Debug {
fn lookup(&self, namespace: &str, local_name: &str, arity: usize) -> Option<FunctionHandle>;
fn get_signature(&self, handle: FunctionHandle) -> Option<DynamicFunctionSignature>;
}
pub trait FunctionEvaluator<N: DomNavigator> {
fn eval(
&self,
handle: FunctionHandle,
ctx: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BuiltinCatalog;
impl FunctionCatalog for BuiltinCatalog {
fn lookup(&self, namespace: &str, local_name: &str, arity: usize) -> Option<FunctionHandle> {
FUNCTION_REGISTRY
.lookup(namespace, local_name, arity)
.map(|entry| FunctionHandle::from(entry.id))
}
fn get_signature(&self, handle: FunctionHandle) -> Option<DynamicFunctionSignature> {
if !handle.is_builtin() {
return None;
}
FUNCTION_REGISTRY
.by_id(handle_to_function_id(handle).ok()?)
.map(|entry| DynamicFunctionSignature::from(&entry.signature))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BuiltinEvaluator;
impl<N: DomNavigator> FunctionEvaluator<N> for BuiltinEvaluator {
fn eval(
&self,
handle: FunctionHandle,
ctx: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
let id = handle_to_function_id(handle)?;
eval_function(id, ctx, args)
}
}
pub(crate) fn handle_to_function_id(handle: FunctionHandle) -> Result<FunctionId, XPathError> {
if !handle.is_builtin() {
return Err(XPathError::Internal(format!(
"Cannot convert custom handle {:?} to FunctionId",
handle
)));
}
let value = handle.0 as u16;
let entry = FUNCTION_REGISTRY
.by_id_value(value)
.ok_or_else(|| XPathError::Internal(format!("Invalid function handle: {}", value)))?;
Ok(entry.id)
}
use std::collections::HashMap;
pub type CustomFn<N> = Arc<
dyn Fn(&mut DynamicContext<'_, N>, Vec<XPathValue<N>>) -> Result<XPathValue<N>, XPathError>
+ Send
+ Sync,
>;
struct CustomFunctionEntry<N: DomNavigator> {
signature: DynamicFunctionSignature,
implementation: CustomFn<N>,
}
pub struct FunctionSet<N: DomNavigator> {
custom_functions: Vec<CustomFunctionEntry<N>>,
lookup: HashMap<(Arc<str>, Arc<str>, usize), FunctionHandle>,
variadic_lookup: HashMap<(Arc<str>, Arc<str>), (FunctionHandle, usize)>,
}
impl<N: DomNavigator> FunctionSet<N> {
pub fn new() -> Self {
Self {
custom_functions: Vec::new(),
lookup: HashMap::new(),
variadic_lookup: HashMap::new(),
}
}
pub fn with_builtins() -> Self {
Self::new()
}
pub fn register<F>(
&mut self,
signature: DynamicFunctionSignature,
implementation: F,
) -> FunctionHandle
where
F: Fn(&mut DynamicContext<'_, N>, Vec<XPathValue<N>>) -> Result<XPathValue<N>, XPathError>
+ Send
+ Sync
+ 'static,
{
let index = self.custom_functions.len();
let handle = FunctionHandle(CUSTOM_HANDLE_BASE + index as u32);
let ns = signature.namespace.clone();
let local = signature.local_name.clone();
match signature.arity {
FunctionArity::Exact(n) => {
self.lookup.insert((ns.clone(), local.clone(), n), handle);
}
FunctionArity::Range(min, max) => {
for arity in min..=max {
self.lookup
.insert((ns.clone(), local.clone(), arity), handle);
}
}
FunctionArity::Variadic(min) => {
self.variadic_lookup
.insert((ns.clone(), local.clone()), (handle, min));
}
}
self.custom_functions.push(CustomFunctionEntry {
signature,
implementation: Arc::new(implementation),
});
handle
}
pub fn custom_count(&self) -> usize {
self.custom_functions.len()
}
pub fn has_custom_functions(&self) -> bool {
!self.custom_functions.is_empty()
}
}
impl<N: DomNavigator> Default for FunctionSet<N> {
fn default() -> Self {
Self::with_builtins()
}
}
impl<N: DomNavigator> std::fmt::Debug for FunctionSet<N> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FunctionSet")
.field("custom_count", &self.custom_functions.len())
.field("lookup_count", &self.lookup.len())
.finish()
}
}
impl<N: DomNavigator> FunctionCatalog for FunctionSet<N> {
fn lookup(&self, namespace: &str, local_name: &str, arity: usize) -> Option<FunctionHandle> {
let ns: Arc<str> = Arc::from(namespace);
let local: Arc<str> = Arc::from(local_name);
if let Some(&handle) = self.lookup.get(&(ns.clone(), local.clone(), arity)) {
return Some(handle);
}
if let Some(&(handle, min_arity)) = self.variadic_lookup.get(&(ns.clone(), local.clone())) {
if arity >= min_arity {
return Some(handle);
}
}
FUNCTION_REGISTRY
.lookup(namespace, local_name, arity)
.map(|entry| FunctionHandle::from(entry.id))
}
fn get_signature(&self, handle: FunctionHandle) -> Option<DynamicFunctionSignature> {
if let Some(index) = handle.custom_index() {
self.custom_functions
.get(index)
.map(|e| e.signature.clone())
} else {
FUNCTION_REGISTRY
.by_id(handle_to_function_id(handle).ok()?)
.map(|entry| DynamicFunctionSignature::from(&entry.signature))
}
}
}
impl<N: DomNavigator> FunctionEvaluator<N> for FunctionSet<N> {
fn eval(
&self,
handle: FunctionHandle,
ctx: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if let Some(index) = handle.custom_index() {
let entry = self.custom_functions.get(index).ok_or_else(|| {
XPathError::Internal(format!("Invalid custom function handle: {:?}", handle))
})?;
(entry.implementation)(ctx, args)
} else {
let id = handle_to_function_id(handle)?;
eval_function(id, ctx, args)
}
}
}
const XPATH10_FUNCTIONS: &[&str] = &[
"last",
"position",
"count",
"id",
"name",
"local-name",
"namespace-uri",
"lang",
"string",
"concat",
"starts-with",
"contains",
"substring-before",
"substring-after",
"substring",
"string-length",
"normalize-space",
"translate",
"boolean",
"not",
"true",
"false",
"number",
"sum",
"floor",
"ceiling",
"round",
];
#[derive(Debug, Clone, Copy, Default)]
pub struct XPath10Catalog;
impl FunctionCatalog for XPath10Catalog {
fn lookup(&self, namespace: &str, local_name: &str, arity: usize) -> Option<FunctionHandle> {
if namespace.is_empty() {
if XPATH10_FUNCTIONS.contains(&local_name) {
FUNCTION_REGISTRY
.lookup(super::signature::FN_NAMESPACE, local_name, arity)
.map(|entry| FunctionHandle::from(entry.id))
} else {
None
}
} else {
BuiltinCatalog.lookup(namespace, local_name, arity)
}
}
fn get_signature(&self, handle: FunctionHandle) -> Option<DynamicFunctionSignature> {
BuiltinCatalog.get_signature(handle)
}
}
fn wrap_as_double<N: DomNavigator>(result: XPathValue<N>) -> XPathValue<N> {
if let Some(i) = result.as_integer() {
return XPathValue::double(i.to_string().parse::<f64>().unwrap_or(f64::NAN));
}
result
}
#[derive(Debug, Clone, Copy, Default)]
pub struct XPath10Evaluator;
impl<N: DomNavigator> FunctionEvaluator<N> for XPath10Evaluator {
fn eval(
&self,
handle: FunctionHandle,
ctx: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
let id = handle_to_function_id(handle)?;
match id {
FunctionId::String => {
use crate::xpath::atomize;
match args.len() {
0 => {
let item = ctx.require_context_item()?.clone();
let s = match item {
crate::xpath::iterator::XmlItem::Node(nav) => nav.value(),
crate::xpath::iterator::XmlItem::Atomic(v) => atomize::string_value(&v),
};
Ok(XPathValue::string(s))
}
1 => {
let s = atomize::to_string_10(&args[0]);
Ok(XPathValue::string(s))
}
_ => Err(XPathError::wrong_number_of_arguments(
"string",
1,
args.len(),
)),
}
}
FunctionId::Number => {
use crate::xpath::atomize;
match args.len() {
0 => {
let item = ctx.require_context_item()?.clone();
let d = match item {
crate::xpath::iterator::XmlItem::Node(nav) => {
let s = nav.value();
s.trim().parse().unwrap_or(f64::NAN)
}
crate::xpath::iterator::XmlItem::Atomic(v) => atomize::to_number(&v),
};
Ok(XPathValue::double(d))
}
1 => {
let d = atomize::to_number_10(&args[0]);
Ok(XPathValue::double(d))
}
_ => Err(XPathError::wrong_number_of_arguments(
"number",
1,
args.len(),
)),
}
}
FunctionId::Count
| FunctionId::StringLength
| FunctionId::Last
| FunctionId::Position => {
let result = eval_function(id, ctx, args)?;
Ok(wrap_as_double(result))
}
FunctionId::Sum | FunctionId::Floor | FunctionId::Ceiling | FunctionId::Round => {
let result = eval_function(id, ctx, args)?;
Ok(wrap_as_double(result))
}
_ => eval_function(id, ctx, args),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xpath::RoXmlNavigator;
#[test]
fn test_function_handle_from_id() {
let handle = FunctionHandle::from(FunctionId::Count);
assert!(handle.is_builtin());
assert!(!handle.is_custom());
}
#[test]
fn test_custom_handle() {
let handle = FunctionHandle(CUSTOM_HANDLE_BASE);
assert!(!handle.is_builtin());
assert!(handle.is_custom());
assert_eq!(handle.custom_index(), Some(0));
let handle2 = FunctionHandle(CUSTOM_HANDLE_BASE + 5);
assert_eq!(handle2.custom_index(), Some(5));
}
#[test]
fn test_builtin_catalog_lookup() {
let catalog = BuiltinCatalog;
let handle = catalog.lookup("http://www.w3.org/2005/xpath-functions", "count", 1);
assert!(handle.is_some());
assert!(handle.unwrap().is_builtin());
}
#[test]
fn test_builtin_catalog_not_found() {
let catalog = BuiltinCatalog;
let handle = catalog.lookup("http://example.com", "my-func", 1);
assert!(handle.is_none());
}
#[test]
fn test_dynamic_signature_from_static() {
let catalog = BuiltinCatalog;
let handle = catalog
.lookup("http://www.w3.org/2005/xpath-functions", "count", 1)
.unwrap();
let sig = catalog.get_signature(handle);
assert!(sig.is_some());
let sig = sig.unwrap();
assert_eq!(&*sig.local_name, "count");
assert_eq!(sig.arity, FunctionArity::Exact(1));
}
#[test]
fn test_function_set_builtins_accessible() {
let functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
let handle = functions.lookup("http://www.w3.org/2005/xpath-functions", "count", 1);
assert!(handle.is_some());
assert!(handle.unwrap().is_builtin());
}
#[test]
fn test_function_set_register_custom() {
let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
let sig = DynamicFunctionSignature::new(
"http://example.com/ext",
"my-func",
vec![SequenceType::string()],
SequenceType::string(),
);
let handle = functions.register(sig, |_ctx, _args| Ok(XPathValue::string("custom result")));
assert!(handle.is_custom());
assert_eq!(functions.custom_count(), 1);
}
#[test]
fn test_function_set_lookup_custom() {
let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
let sig = DynamicFunctionSignature::new(
"http://example.com/ext",
"my-upper",
vec![SequenceType::string()],
SequenceType::string(),
);
let registered_handle = functions.register(sig, |_ctx, mut args| {
let s = super::super::atomize_to_string(args.remove(0))?;
Ok(XPathValue::string(s.to_uppercase()))
});
let found_handle = functions.lookup("http://example.com/ext", "my-upper", 1);
assert!(found_handle.is_some());
assert_eq!(found_handle.unwrap(), registered_handle);
assert!(found_handle.unwrap().is_custom());
}
#[test]
fn test_function_set_get_custom_signature() {
let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
let sig = DynamicFunctionSignature::new(
"http://example.com/ext",
"test-func",
vec![SequenceType::integer(), SequenceType::integer()],
SequenceType::integer(),
);
let handle = functions.register(sig, |_ctx, _args| Ok(XPathValue::integer(42)));
let retrieved_sig = functions.get_signature(handle);
assert!(retrieved_sig.is_some());
let retrieved_sig = retrieved_sig.unwrap();
assert_eq!(&*retrieved_sig.namespace, "http://example.com/ext");
assert_eq!(&*retrieved_sig.local_name, "test-func");
assert_eq!(retrieved_sig.arity, FunctionArity::Exact(2));
}
#[test]
fn test_function_set_custom_overrides_builtin() {
let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
let sig = DynamicFunctionSignature::new(
"http://www.w3.org/2005/xpath-functions",
"count",
vec![SequenceType::any()],
SequenceType::integer(),
);
let custom_handle = functions.register(sig, |_ctx, _args| {
Ok(XPathValue::integer(999))
});
let found_handle = functions.lookup("http://www.w3.org/2005/xpath-functions", "count", 1);
assert!(found_handle.is_some());
assert_eq!(found_handle.unwrap(), custom_handle);
assert!(found_handle.unwrap().is_custom());
}
#[test]
fn test_function_set_eval_custom() {
use crate::namespace::table::NameTable;
use crate::xpath::context::{DynamicContext, XPathContext};
let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
let sig = DynamicFunctionSignature::new(
"http://example.com/ext",
"double",
vec![SequenceType::double()],
SequenceType::double(),
);
let handle = functions.register(sig, |_ctx, mut args| {
let val = args.remove(0);
let d = val.as_f64().unwrap_or(0.0);
Ok(XPathValue::double(d * 2.0))
});
let names = NameTable::new();
let static_ctx = XPathContext::new(&names);
let mut dyn_ctx: DynamicContext<'_, RoXmlNavigator<'static>> =
DynamicContext::new(&static_ctx, 0);
let args = vec![XPathValue::double(21.0)];
let result = functions.eval(handle, &mut dyn_ctx, args).unwrap();
assert_eq!(result.as_f64(), Some(42.0));
}
#[test]
fn test_function_set_eval_builtin() {
use crate::namespace::table::NameTable;
use crate::types::value::XmlValue;
use crate::xpath::context::{DynamicContext, XPathContext};
use crate::xpath::iterator::XmlItem;
let functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
let handle = functions
.lookup("http://www.w3.org/2005/xpath-functions", "count", 1)
.unwrap();
let names = NameTable::new();
let static_ctx = XPathContext::new(&names);
let mut dyn_ctx: DynamicContext<'_, RoXmlNavigator<'static>> =
DynamicContext::new(&static_ctx, 0);
let items = vec![
XmlItem::Atomic(XmlValue::integer(1.into())),
XmlItem::Atomic(XmlValue::integer(2.into())),
XmlItem::Atomic(XmlValue::integer(3.into())),
];
let args = vec![XPathValue::from_sequence(items)];
let result = functions.eval(handle, &mut dyn_ctx, args).unwrap();
assert_eq!(
result.as_integer().map(|i| i.to_string()),
Some("3".to_string())
);
}
#[test]
fn test_function_set_range_arity() {
let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
let sig = DynamicFunctionSignature::range(
"http://example.com/ext",
"multi",
1,
3,
vec![
SequenceType::string(),
SequenceType::string(),
SequenceType::string(),
],
SequenceType::string(),
);
let handle =
functions.register(sig, |_ctx, args| Ok(XPathValue::integer(args.len() as i64)));
assert_eq!(
functions.lookup("http://example.com/ext", "multi", 1),
Some(handle)
);
assert_eq!(
functions.lookup("http://example.com/ext", "multi", 2),
Some(handle)
);
assert_eq!(
functions.lookup("http://example.com/ext", "multi", 3),
Some(handle)
);
assert!(functions
.lookup("http://example.com/ext", "multi", 0)
.is_none());
assert!(functions
.lookup("http://example.com/ext", "multi", 4)
.is_none());
}
#[test]
fn test_function_set_variadic() {
let mut functions: FunctionSet<RoXmlNavigator<'static>> = FunctionSet::with_builtins();
let sig = DynamicFunctionSignature::variadic(
"http://example.com/ext",
"varargs",
2,
vec![SequenceType::any_atomic()],
SequenceType::integer(),
);
let handle =
functions.register(sig, |_ctx, args| Ok(XPathValue::integer(args.len() as i64)));
assert_eq!(
functions.lookup("http://example.com/ext", "varargs", 2),
Some(handle)
);
assert_eq!(
functions.lookup("http://example.com/ext", "varargs", 5),
Some(handle)
);
assert_eq!(
functions.lookup("http://example.com/ext", "varargs", 100),
Some(handle)
);
assert!(functions
.lookup("http://example.com/ext", "varargs", 0)
.is_none());
assert!(functions
.lookup("http://example.com/ext", "varargs", 1)
.is_none());
}
#[test]
fn test_xpath10_catalog_resolves_core_function() {
let catalog = XPath10Catalog;
let handle = catalog.lookup("", "count", 1);
assert!(handle.is_some());
assert!(handle.unwrap().is_builtin());
}
#[test]
fn test_xpath10_catalog_rejects_non_core_function() {
let catalog = XPath10Catalog;
let handle = catalog.lookup("", "deep-equal", 2);
assert!(handle.is_none());
}
#[test]
fn test_xpath10_catalog_non_empty_ns_delegates() {
let catalog = XPath10Catalog;
let handle = catalog.lookup("http://www.w3.org/2005/xpath-functions", "count", 1);
assert!(handle.is_some());
}
#[test]
fn test_xpath10_catalog_all_core_functions() {
let catalog = XPath10Catalog;
let functions_with_arity: &[(&str, usize)] = &[
("last", 0),
("position", 0),
("count", 1),
("id", 1),
("name", 0),
("local-name", 0),
("namespace-uri", 0),
("lang", 1),
("string", 0),
("concat", 2),
("starts-with", 2),
("contains", 2),
("substring-before", 2),
("substring-after", 2),
("substring", 2),
("string-length", 0),
("normalize-space", 0),
("translate", 3),
("boolean", 1),
("not", 1),
("true", 0),
("false", 0),
("number", 0),
("sum", 1),
("floor", 1),
("ceiling", 1),
("round", 1),
];
for (name, arity) in functions_with_arity {
let handle = catalog.lookup("", name, *arity);
assert!(
handle.is_some(),
"XPath 1.0 function '{}' with arity {} not found",
name,
arity
);
}
}
#[test]
fn test_xpath10_evaluator_count_returns_double() {
use crate::namespace::table::NameTable;
use crate::types::value::XmlValue;
use crate::xpath::context::{DynamicContext, XPathContext};
use crate::xpath::iterator::XmlItem;
let evaluator = XPath10Evaluator;
let names = NameTable::new();
let static_ctx = XPathContext::new(&names);
let mut dyn_ctx: DynamicContext<'_, RoXmlNavigator<'static>> =
DynamicContext::new(&static_ctx, 0);
let items = vec![
XmlItem::Atomic(XmlValue::integer(1.into())),
XmlItem::Atomic(XmlValue::integer(2.into())),
XmlItem::Atomic(XmlValue::integer(3.into())),
];
let args = vec![XPathValue::from_sequence(items)];
let handle = FunctionHandle::from(FunctionId::Count);
let result = evaluator.eval(handle, &mut dyn_ctx, args).unwrap();
assert_eq!(result.as_f64(), Some(3.0));
assert!(result.as_integer().is_none());
}
#[test]
fn test_xpath10_evaluator_string_with_node() {
let evaluator = XPath10Evaluator;
let doc = roxmltree::Document::parse("<r><a>first</a><b>second</b></r>").unwrap();
let mut nav_a = RoXmlNavigator::new(&doc);
nav_a.move_to_first_child(); nav_a.move_to_first_child(); let mut nav_b = nav_a.clone();
nav_b.move_to_next_sibling();
use crate::namespace::table::NameTable;
use crate::xpath::context::{DynamicContext, XPathContext};
use crate::xpath::iterator::XmlItem;
let names = NameTable::new();
let static_ctx = XPathContext::new(&names);
let mut dyn_ctx = DynamicContext::new(&static_ctx, 0);
let node_seq = XPathValue::from_sequence(vec![XmlItem::Node(nav_a), XmlItem::Node(nav_b)]);
let args = vec![node_seq];
let handle = FunctionHandle::from(FunctionId::String);
let result = evaluator.eval(handle, &mut dyn_ctx, args).unwrap();
assert_eq!(result.as_str(), Some("first".to_string()));
}
}