use crate::xml::xpath::context::XPathContext;
use crate::xml::xpath::types::{node_string_value, string_to_number, NodeSet, XPathValue};
use std::collections::HashMap;
pub type XPathFunction = fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>;
pub fn core_functions() -> HashMap<String, XPathFunction> {
let mut funcs: HashMap<String, XPathFunction> = HashMap::new();
funcs.insert("last".into(), fn_last);
funcs.insert("position".into(), fn_position);
funcs.insert("count".into(), fn_count);
funcs.insert("id".into(), fn_id);
funcs.insert("local-name".into(), fn_local_name);
funcs.insert("namespace-uri".into(), fn_namespace_uri);
funcs.insert("name".into(), fn_name);
funcs.insert("string".into(), fn_string);
funcs.insert("concat".into(), fn_concat);
funcs.insert("starts-with".into(), fn_starts_with);
funcs.insert("contains".into(), fn_contains);
funcs.insert("substring-before".into(), fn_substring_before);
funcs.insert("substring-after".into(), fn_substring_after);
funcs.insert("substring".into(), fn_substring);
funcs.insert("string-length".into(), fn_string_length);
funcs.insert("normalize-space".into(), fn_normalize_space);
funcs.insert("translate".into(), fn_translate);
funcs.insert("boolean".into(), fn_boolean);
funcs.insert("not".into(), fn_not);
funcs.insert("true".into(), fn_true);
funcs.insert("false".into(), fn_false);
funcs.insert("lang".into(), fn_lang);
funcs.insert("number".into(), fn_number);
funcs.insert("sum".into(), fn_sum);
funcs.insert("floor".into(), fn_floor);
funcs.insert("ceiling".into(), fn_ceiling);
funcs.insert("round".into(), fn_round);
funcs
}
fn get_string_arg(args: &[XPathValue], index: usize) -> String {
if index < args.len() {
args[index].as_string()
} else {
String::new()
}
}
fn get_number_arg(args: &[XPathValue], index: usize) -> f64 {
if index < args.len() {
args[index].as_number()
} else {
f64::NAN
}
}
fn get_boolean_arg(args: &[XPathValue], index: usize) -> bool {
if index < args.len() {
args[index].as_boolean()
} else {
false
}
}
fn get_node_set_arg(args: &[XPathValue], index: usize) -> NodeSet {
if index < args.len() {
match &args[index] {
XPathValue::NodeSet(ns) => ns.clone(),
_ => NodeSet::new(),
}
} else {
NodeSet::new()
}
}
fn get_first_node(
ctx: &XPathContext,
args: &[XPathValue],
index: usize,
) -> Option<*mut crate::abi::structs::_xmlNode> {
if index < args.len() {
match &args[index] {
XPathValue::NodeSet(ns) => ns.first(),
_ => None,
}
} else {
Some(ctx.context_node)
}
}
fn fn_last(ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::Number(ctx.last() as f64))
}
fn fn_position(ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::Number(ctx.position() as f64))
}
fn fn_count(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let ns = get_node_set_arg(args, 0);
Ok(XPathValue::Number(ns.len() as f64))
}
fn fn_id(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::NodeSet(NodeSet::new()))
}
fn fn_local_name(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let node = get_first_node(ctx, args, 0);
if let Some(node) = node {
unsafe {
let name = crate::xml::string::xmlstr_to_string((*node).name);
if let Some(pos) = name.find(':') {
Ok(XPathValue::String(name[pos + 1..].to_string()))
} else {
Ok(XPathValue::String(name))
}
}
} else {
Ok(XPathValue::String(String::new()))
}
}
fn fn_namespace_uri(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let node = get_first_node(ctx, args, 0);
if let Some(node) = node {
unsafe {
if let Some(ns) = (*node).ns.as_ref() {
let uri = crate::xml::string::xmlstr_to_string(ns.href);
Ok(XPathValue::String(uri))
} else {
Ok(XPathValue::String(String::new()))
}
}
} else {
Ok(XPathValue::String(String::new()))
}
}
fn fn_name(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let node = get_first_node(ctx, args, 0);
if let Some(node) = node {
unsafe {
let name = crate::xml::string::xmlstr_to_string((*node).name);
Ok(XPathValue::String(name))
}
} else {
Ok(XPathValue::String(String::new()))
}
}
fn fn_string(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
if args.is_empty() {
Ok(XPathValue::String(node_string_value(ctx.context_node)))
} else {
Ok(XPathValue::String(args[0].as_string()))
}
}
fn fn_concat(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let mut result = String::new();
for arg in args {
result.push_str(&arg.as_string());
}
Ok(XPathValue::String(result))
}
fn fn_starts_with(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let s1 = get_string_arg(args, 0);
let s2 = get_string_arg(args, 1);
Ok(XPathValue::Boolean(s1.starts_with(&s2)))
}
fn fn_contains(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let s1 = get_string_arg(args, 0);
let s2 = get_string_arg(args, 1);
Ok(XPathValue::Boolean(s1.contains(&s2)))
}
fn fn_substring_before(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let s1 = get_string_arg(args, 0);
let s2 = get_string_arg(args, 1);
if let Some(pos) = s1.find(&s2) {
Ok(XPathValue::String(s1[..pos].to_string()))
} else {
Ok(XPathValue::String(String::new()))
}
}
fn fn_substring_after(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let s1 = get_string_arg(args, 0);
let s2 = get_string_arg(args, 1);
if let Some(pos) = s1.find(&s2) {
Ok(XPathValue::String(s1[pos + s2.len()..].to_string()))
} else {
Ok(XPathValue::String(String::new()))
}
}
fn fn_substring(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let s = get_string_arg(args, 0);
let start = get_number_arg(args, 1);
let has_length = args.len() >= 3;
let length = if has_length {
get_number_arg(args, 2)
} else {
f64::MAX
};
let start_rounded = start.round() as isize;
let length_rounded = length.round() as isize;
let start_index = if start_rounded < 1 {
0
} else {
(start_rounded - 1) as usize
};
let length = if length_rounded < 0 {
0
} else {
length_rounded as usize
};
if start_index >= s.len() || length == 0 {
Ok(XPathValue::String(String::new()))
} else {
let end = std::cmp::min(start_index + length, s.len());
Ok(XPathValue::String(s[start_index..end].to_string()))
}
}
fn fn_string_length(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let s = if args.is_empty() {
node_string_value(ctx.context_node)
} else {
get_string_arg(args, 0)
};
Ok(XPathValue::Number(s.len() as f64))
}
fn fn_normalize_space(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let s = if args.is_empty() {
node_string_value(ctx.context_node)
} else {
get_string_arg(args, 0)
};
let normalized: Vec<&str> = s.split_whitespace().collect();
Ok(XPathValue::String(normalized.join(" ")))
}
fn fn_translate(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let s = get_string_arg(args, 0);
let from = get_string_arg(args, 1);
let to = get_string_arg(args, 2);
let result: String = s
.chars()
.map(|c| {
if let Some(pos) = from.find(c) {
if pos < to.len() {
to.chars().nth(pos).unwrap_or(c)
} else {
'\0' }
} else {
c
}
})
.filter(|&c| c != '\0')
.collect();
Ok(XPathValue::String(result))
}
fn fn_boolean(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::Boolean(get_boolean_arg(args, 0)))
}
fn fn_not(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::Boolean(!get_boolean_arg(args, 0)))
}
fn fn_true(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::Boolean(true))
}
fn fn_false(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
Ok(XPathValue::Boolean(false))
}
fn fn_lang(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let lang = get_string_arg(args, 0);
let mut node = ctx.context_node;
unsafe {
while !node.is_null() {
let mut prop = (*node).properties;
while !prop.is_null() {
let attr_name = crate::xml::string::xmlstr_to_string((*prop).name);
if attr_name == "lang" || attr_name == "xml:lang" {
if !(*prop).children.is_null() {
let attr_val =
crate::xml::string::xmlstr_to_string((*(*prop).children).content);
if attr_val.to_lowercase() == lang.to_lowercase()
|| attr_val
.to_lowercase()
.starts_with(&format!("{}-", lang.to_lowercase()))
{
return Ok(XPathValue::Boolean(true));
}
}
}
prop = (*prop).next;
}
node = (*node).parent;
}
}
Ok(XPathValue::Boolean(false))
}
fn fn_number(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
if args.is_empty() {
Ok(XPathValue::Number(string_to_number(&node_string_value(
ctx.context_node,
))))
} else {
Ok(XPathValue::Number(get_number_arg(args, 0)))
}
}
fn fn_sum(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let ns = get_node_set_arg(args, 0);
let mut total = 0.0;
for node in ns.iter() {
let s = node_string_value(node);
total += string_to_number(&s);
}
Ok(XPathValue::Number(total))
}
fn fn_floor(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let n = get_number_arg(args, 0);
Ok(XPathValue::Number(n.floor()))
}
fn fn_ceiling(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let n = get_number_arg(args, 0);
Ok(XPathValue::Number(n.ceil()))
}
fn fn_round(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
let n = get_number_arg(args, 0);
if n.is_nan() || n.is_infinite() || n == 0.0 {
return Ok(XPathValue::Number(n));
}
let rust_rounded = n.round();
let result = if n.is_sign_negative() && (n - rust_rounded).abs() == 0.5 {
rust_rounded + 1.0
} else {
rust_rounded
};
Ok(XPathValue::Number(result))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_true_false() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
assert_eq!(fn_true(&mut ctx, &[]).unwrap().as_boolean(), true);
assert_eq!(fn_false(&mut ctx, &[]).unwrap().as_boolean(), false);
}
#[test]
fn test_boolean_conversion() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
assert_eq!(
fn_boolean(&mut ctx, &[XPathValue::Boolean(true)])
.unwrap()
.as_boolean(),
true
);
assert_eq!(
fn_boolean(&mut ctx, &[XPathValue::Boolean(false)])
.unwrap()
.as_boolean(),
false
);
}
#[test]
fn test_not() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
assert_eq!(
fn_not(&mut ctx, &[XPathValue::Boolean(true)])
.unwrap()
.as_boolean(),
false
);
assert_eq!(
fn_not(&mut ctx, &[XPathValue::Boolean(false)])
.unwrap()
.as_boolean(),
true
);
}
#[test]
fn test_number_round() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
assert_eq!(
fn_floor(&mut ctx, &[XPathValue::Number(3.7)])
.unwrap()
.as_number(),
3.0
);
assert_eq!(
fn_ceiling(&mut ctx, &[XPathValue::Number(3.2)])
.unwrap()
.as_number(),
4.0
);
assert_eq!(
fn_round(&mut ctx, &[XPathValue::Number(3.5)])
.unwrap()
.as_number(),
4.0
);
assert_eq!(
fn_round(&mut ctx, &[XPathValue::Number(-3.5)])
.unwrap()
.as_number(),
-3.0
);
}
#[test]
fn test_string_functions() {
let mut ctx = XPathContext::new(std::ptr::null_mut());
assert_eq!(
fn_concat(
&mut ctx,
&[
XPathValue::String("a".into()),
XPathValue::String("b".into()),
XPathValue::String("c".into())
]
)
.unwrap()
.as_string(),
"abc"
);
assert_eq!(
fn_starts_with(
&mut ctx,
&[
XPathValue::String("hello".into()),
XPathValue::String("he".into())
]
)
.unwrap()
.as_boolean(),
true
);
assert_eq!(
fn_starts_with(
&mut ctx,
&[
XPathValue::String("hello".into()),
XPathValue::String("x".into())
]
)
.unwrap()
.as_boolean(),
false
);
assert_eq!(
fn_contains(
&mut ctx,
&[
XPathValue::String("hello".into()),
XPathValue::String("ell".into())
]
)
.unwrap()
.as_boolean(),
true
);
assert_eq!(
fn_string_length(&mut ctx, &[XPathValue::String("hello".into())])
.unwrap()
.as_number(),
5.0
);
}
#[test]
fn test_core_functions_registered() {
let funcs = core_functions();
assert!(funcs.contains_key("last"));
assert!(funcs.contains_key("position"));
assert!(funcs.contains_key("count"));
assert!(funcs.contains_key("string"));
assert!(funcs.contains_key("concat"));
assert!(funcs.contains_key("boolean"));
assert!(funcs.contains_key("not"));
assert!(funcs.contains_key("number"));
assert!(funcs.contains_key("sum"));
assert!(funcs.contains_key("floor"));
assert!(funcs.contains_key("ceiling"));
assert!(funcs.contains_key("round"));
assert!(funcs.contains_key("name"));
assert!(funcs.contains_key("local-name"));
assert_eq!(funcs.len(), 27);
assert!(funcs.contains_key("namespace-uri"));
}
}