1use crate::xml::xpath::context::{BoxedXPathFunction, XPathContext};
69use crate::xml::xpath::types::{node_string_value, string_to_number, NodeSet, XPathValue};
70use once_cell::sync::Lazy;
71use std::collections::HashMap;
72
73pub type XPathFunction = fn(&mut XPathContext, &[XPathValue]) -> Result<XPathValue, String>;
77
78pub static CORE_FUNCTION_SLICE: &[(&str, XPathFunction)] = &[
85 ("last", fn_last),
87 ("position", fn_position),
88 ("count", fn_count),
89 ("id", fn_id),
90 ("local-name", fn_local_name),
91 ("namespace-uri", fn_namespace_uri),
92 ("name", fn_name),
93 ("string", fn_string),
95 ("concat", fn_concat),
96 ("starts-with", fn_starts_with),
97 ("contains", fn_contains),
98 ("substring-before", fn_substring_before),
99 ("substring-after", fn_substring_after),
100 ("substring", fn_substring),
101 ("string-length", fn_string_length),
102 ("normalize-space", fn_normalize_space),
103 ("translate", fn_translate),
104 ("boolean", fn_boolean),
106 ("not", fn_not),
107 ("true", fn_true),
108 ("false", fn_false),
109 ("lang", fn_lang),
110 ("number", fn_number),
112 ("sum", fn_sum),
113 ("floor", fn_floor),
114 ("ceiling", fn_ceiling),
115 ("round", fn_round),
116];
117
118static CORE_FUNCTION_TABLE: Lazy<HashMap<&'static str, BoxedXPathFunction>> = Lazy::new(|| {
119 CORE_FUNCTION_SLICE
120 .iter()
121 .map(|&(name, f)| (name, Box::new(f) as BoxedXPathFunction))
122 .collect()
123});
124
125pub fn lookup_core_function(name: &str) -> Option<&'static BoxedXPathFunction> {
128 CORE_FUNCTION_TABLE.get(name)
129}
130
131pub fn core_functions() -> HashMap<String, XPathFunction> {
136 CORE_FUNCTION_SLICE
137 .iter()
138 .map(|&(name, f)| (name.to_string(), f))
139 .collect()
140}
141
142fn get_string_arg(args: &[XPathValue], index: usize) -> String {
147 if index < args.len() {
148 args[index].as_string()
149 } else {
150 String::new()
151 }
152}
153
154fn get_number_arg(args: &[XPathValue], index: usize) -> f64 {
155 if index < args.len() {
156 args[index].as_number()
157 } else {
158 f64::NAN
159 }
160}
161
162fn get_boolean_arg(args: &[XPathValue], index: usize) -> bool {
163 if index < args.len() {
164 args[index].as_boolean()
165 } else {
166 false
167 }
168}
169
170fn get_node_set_arg(args: &[XPathValue], index: usize) -> NodeSet {
171 if index < args.len() {
172 match &args[index] {
173 XPathValue::NodeSet(ns) => ns.clone(),
174 _ => NodeSet::new(),
175 }
176 } else {
177 NodeSet::new()
178 }
179}
180
181fn get_first_node(
182 ctx: &XPathContext,
183 args: &[XPathValue],
184 index: usize,
185) -> Option<*mut crate::abi::structs::_xmlNode> {
186 if index < args.len() {
187 match &args[index] {
188 XPathValue::NodeSet(ns) => ns.first(),
189 _ => None,
190 }
191 } else {
192 Some(ctx.context_node)
194 }
195}
196
197const fn fn_last(ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
203 Ok(XPathValue::Number(ctx.last() as f64))
204}
205
206const fn fn_position(ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
208 Ok(XPathValue::Number(ctx.position() as f64))
209}
210
211fn fn_count(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
213 let ns = get_node_set_arg(args, 0);
214 Ok(XPathValue::Number(ns.len() as f64))
215}
216
217const fn fn_id(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
219 Ok(XPathValue::NodeSet(NodeSet::new()))
222}
223
224fn fn_local_name(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
232 let node = get_first_node(ctx, args, 0);
233 if let Some(node) = node {
234 unsafe {
235 let name = crate::xml::string::xmlstr_to_string((*node).name);
236 if let Some(pos) = name.find(':') {
238 Ok(XPathValue::String(name[pos + 1..].to_string()))
239 } else {
240 Ok(XPathValue::String(name))
241 }
242 }
243 } else {
244 Ok(XPathValue::String(String::new()))
245 }
246}
247
248fn fn_namespace_uri(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
257 let node = get_first_node(ctx, args, 0);
258 if let Some(node) = node {
259 unsafe {
260 if let Some(ns) = (*node).ns.as_ref() {
261 let uri = crate::xml::string::xmlstr_to_string(ns.href);
262 Ok(XPathValue::String(uri))
263 } else {
264 Ok(XPathValue::String(String::new()))
265 }
266 }
267 } else {
268 Ok(XPathValue::String(String::new()))
269 }
270}
271
272fn fn_name(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
283 let node = get_first_node(ctx, args, 0);
284 if let Some(node) = node {
285 unsafe {
286 use crate::abi::types::xmlElementType as ET;
287 let t = (*node).type_;
288 let name = crate::xml::string::xmlstr_to_string((*node).name);
289 if (t == ET::XML_ELEMENT_NODE as i32 || t == ET::XML_ATTRIBUTE_NODE as i32)
290 && !name.is_empty()
291 && !(*node).ns.is_null()
292 && !(*(*node).ns).prefix.is_null()
293 {
294 let prefix = crate::xml::string::xmlstr_to_string((*(*node).ns).prefix);
295 Ok(XPathValue::String(format!("{prefix}:{name}")))
296 } else {
297 Ok(XPathValue::String(name))
298 }
299 }
300 } else {
301 Ok(XPathValue::String(String::new()))
302 }
303}
304
305fn fn_string(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
311 if args.is_empty() {
312 Ok(XPathValue::String(node_string_value(ctx.context_node)))
314 } else {
315 Ok(XPathValue::String(args[0].as_string()))
316 }
317}
318
319fn fn_concat(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
321 let mut result = String::new();
322 for arg in args {
323 result.push_str(&arg.as_string());
324 }
325 Ok(XPathValue::String(result))
326}
327
328fn fn_starts_with(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
330 let s1 = get_string_arg(args, 0);
331 let s2 = get_string_arg(args, 1);
332 Ok(XPathValue::Boolean(s1.starts_with(&s2)))
333}
334
335fn fn_contains(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
337 let s1 = get_string_arg(args, 0);
338 let s2 = get_string_arg(args, 1);
339 Ok(XPathValue::Boolean(s1.contains(&s2)))
340}
341
342fn fn_substring_before(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
344 let s1 = get_string_arg(args, 0);
345 let s2 = get_string_arg(args, 1);
346 if let Some(pos) = s1.find(&s2) {
347 Ok(XPathValue::String(s1[..pos].to_string()))
348 } else {
349 Ok(XPathValue::String(String::new()))
350 }
351}
352
353fn fn_substring_after(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
355 let s1 = get_string_arg(args, 0);
356 let s2 = get_string_arg(args, 1);
357 if let Some(pos) = s1.find(&s2) {
358 Ok(XPathValue::String(s1[pos + s2.len()..].to_string()))
359 } else {
360 Ok(XPathValue::String(String::new()))
361 }
362}
363
364fn fn_substring(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
372 let s = get_string_arg(args, 0);
373 let start = get_number_arg(args, 1);
374 let has_length = args.len() >= 3;
375 let length = if has_length {
376 get_number_arg(args, 2)
377 } else {
378 f64::MAX
379 };
380
381 let start_r = start.round();
382 let end_r = start_r + length.round();
383
384 let mut out = String::new();
385 for (i, c) in s.chars().enumerate() {
386 let p = (i + 1) as f64;
387 if p >= start_r && p < end_r {
388 out.push(c);
389 }
390 }
391 Ok(XPathValue::String(out))
392}
393
394fn fn_string_length(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
399 let s = if args.is_empty() {
400 node_string_value(ctx.context_node)
401 } else {
402 get_string_arg(args, 0)
403 };
404 Ok(XPathValue::Number(s.chars().count() as f64))
405}
406
407fn fn_normalize_space(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
409 let s = if args.is_empty() {
410 node_string_value(ctx.context_node)
411 } else {
412 get_string_arg(args, 0)
413 };
414 let normalized: Vec<&str> = s.split_whitespace().collect();
415 Ok(XPathValue::String(normalized.join(" ")))
416}
417
418fn fn_translate(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
420 let s = get_string_arg(args, 0);
421 let from = get_string_arg(args, 1);
422 let to = get_string_arg(args, 2);
423
424 let result: String = s
425 .chars()
426 .map(|c| {
427 if let Some(pos) = from.chars().position(|x| x == c) {
431 let to_chars: Vec<char> = to.chars().collect();
432 if pos < to_chars.len() {
433 to_chars[pos]
434 } else {
435 '\0' }
437 } else {
438 c
439 }
440 })
441 .filter(|&c| c != '\0')
442 .collect();
443
444 Ok(XPathValue::String(result))
445}
446
447fn fn_boolean(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
453 Ok(XPathValue::Boolean(get_boolean_arg(args, 0)))
454}
455
456fn fn_not(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
458 Ok(XPathValue::Boolean(!get_boolean_arg(args, 0)))
459}
460
461const fn fn_true(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
463 Ok(XPathValue::Boolean(true))
464}
465
466const fn fn_false(_ctx: &mut XPathContext, _args: &[XPathValue]) -> Result<XPathValue, String> {
468 Ok(XPathValue::Boolean(false))
469}
470
471fn fn_lang(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
481 let lang = get_string_arg(args, 0);
482 let mut node = ctx.context_node;
483 unsafe {
484 while !node.is_null() {
485 let mut prop = (*node).properties;
486 while !prop.is_null() {
487 let attr_name = crate::xml::string::xmlstr_to_string((*prop).name);
488 if attr_name == "lang" || attr_name == "xml:lang" {
489 if !(*prop).children.is_null() {
491 let attr_val =
492 crate::xml::string::xmlstr_to_string((*(*prop).children).content);
493 if attr_val.to_lowercase() == lang.to_lowercase()
494 || attr_val
495 .to_lowercase()
496 .starts_with(&format!("{}-", lang.to_lowercase()))
497 {
498 return Ok(XPathValue::Boolean(true));
499 }
500 }
501 }
502 prop = (*prop).next;
503 }
504 node = (*node).parent;
505 }
506 }
507 Ok(XPathValue::Boolean(false))
508}
509
510fn fn_number(ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
516 if args.is_empty() {
517 Ok(XPathValue::Number(string_to_number(&node_string_value(
518 ctx.context_node,
519 ))))
520 } else {
521 Ok(XPathValue::Number(get_number_arg(args, 0)))
522 }
523}
524
525fn fn_sum(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
527 let ns = get_node_set_arg(args, 0);
528 let mut total = 0.0;
529 for node in ns.iter() {
530 let s = node_string_value(node);
531 total += string_to_number(&s);
532 }
533 Ok(XPathValue::Number(total))
534}
535
536fn fn_floor(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
538 let n = get_number_arg(args, 0);
539 Ok(XPathValue::Number(n.floor()))
540}
541
542fn fn_ceiling(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
544 let n = get_number_arg(args, 0);
545 Ok(XPathValue::Number(n.ceil()))
546}
547
548fn fn_round(_ctx: &mut XPathContext, args: &[XPathValue]) -> Result<XPathValue, String> {
554 let n = get_number_arg(args, 0);
555 if n.is_nan() || n.is_infinite() || n == 0.0 {
556 return Ok(XPathValue::Number(n));
557 }
558 let rust_rounded = n.round();
562 let result = if n.is_sign_negative() && (n - rust_rounded).abs() == 0.5 {
563 rust_rounded + 1.0
565 } else {
566 rust_rounded
567 };
568 Ok(XPathValue::Number(result))
569}
570
571#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
580 fn test_true_false() {
581 let mut ctx = XPathContext::new(std::ptr::null_mut());
582 assert!(fn_true(&mut ctx, &[]).unwrap().as_boolean());
583 assert!(!fn_false(&mut ctx, &[]).unwrap().as_boolean());
584 }
585
586 #[test]
587 fn test_boolean_conversion() {
588 let mut ctx = XPathContext::new(std::ptr::null_mut());
589 assert!(fn_boolean(&mut ctx, &[XPathValue::Boolean(true)])
590 .unwrap()
591 .as_boolean());
592 assert!(!fn_boolean(&mut ctx, &[XPathValue::Boolean(false)])
593 .unwrap()
594 .as_boolean());
595 }
596
597 #[test]
598 fn test_not() {
599 let mut ctx = XPathContext::new(std::ptr::null_mut());
600 assert!(!fn_not(&mut ctx, &[XPathValue::Boolean(true)])
601 .unwrap()
602 .as_boolean());
603 assert!(fn_not(&mut ctx, &[XPathValue::Boolean(false)])
604 .unwrap()
605 .as_boolean());
606 }
607
608 #[test]
609 fn test_number_round() {
610 let mut ctx = XPathContext::new(std::ptr::null_mut());
611 assert_eq!(
612 fn_floor(&mut ctx, &[XPathValue::Number(3.7)])
613 .unwrap()
614 .as_number(),
615 3.0
616 );
617 assert_eq!(
618 fn_ceiling(&mut ctx, &[XPathValue::Number(3.2)])
619 .unwrap()
620 .as_number(),
621 4.0
622 );
623 assert_eq!(
624 fn_round(&mut ctx, &[XPathValue::Number(3.5)])
625 .unwrap()
626 .as_number(),
627 4.0
628 );
629 assert_eq!(
630 fn_round(&mut ctx, &[XPathValue::Number(-3.5)])
631 .unwrap()
632 .as_number(),
633 -3.0
634 );
635 }
636
637 #[test]
638 fn test_string_functions() {
639 let mut ctx = XPathContext::new(std::ptr::null_mut());
640 assert_eq!(
641 fn_concat(
642 &mut ctx,
643 &[
644 XPathValue::String("a".into()),
645 XPathValue::String("b".into()),
646 XPathValue::String("c".into())
647 ]
648 )
649 .unwrap()
650 .as_string(),
651 "abc"
652 );
653 assert!(fn_starts_with(
654 &mut ctx,
655 &[
656 XPathValue::String("hello".into()),
657 XPathValue::String("he".into())
658 ]
659 )
660 .unwrap()
661 .as_boolean());
662 assert!(!fn_starts_with(
663 &mut ctx,
664 &[
665 XPathValue::String("hello".into()),
666 XPathValue::String("x".into())
667 ]
668 )
669 .unwrap()
670 .as_boolean());
671 assert!(fn_contains(
672 &mut ctx,
673 &[
674 XPathValue::String("hello".into()),
675 XPathValue::String("ell".into())
676 ]
677 )
678 .unwrap()
679 .as_boolean());
680 assert_eq!(
681 fn_string_length(&mut ctx, &[XPathValue::String("hello".into())])
682 .unwrap()
683 .as_number(),
684 5.0
685 );
686 }
687
688 #[test]
689 fn test_core_functions_registered() {
690 let funcs = core_functions();
691 assert!(funcs.contains_key("last"));
692 assert!(funcs.contains_key("position"));
693 assert!(funcs.contains_key("count"));
694 assert!(funcs.contains_key("string"));
695 assert!(funcs.contains_key("concat"));
696 assert!(funcs.contains_key("boolean"));
697 assert!(funcs.contains_key("not"));
698 assert!(funcs.contains_key("number"));
699 assert!(funcs.contains_key("sum"));
700 assert!(funcs.contains_key("floor"));
701 assert!(funcs.contains_key("ceiling"));
702 assert!(funcs.contains_key("round"));
703 assert!(funcs.contains_key("name"));
704 assert!(funcs.contains_key("local-name"));
705 assert_eq!(funcs.len(), 27);
706 assert!(funcs.contains_key("namespace-uri"));
707 }
708}