1use crate::types::value::XmlValue;
7use crate::xpath::error::XPathError;
8use crate::xpath::iterator::XmlItem;
9use crate::xpath::string_ops;
10use crate::xpath::DomNavigator;
11
12use super::{
13 atomize_to_double, atomize_to_string, atomize_to_string_opt, atomize_to_string_required,
14 atomize_to_string_strict, atomize_to_string_strict_opt, XPathValue,
15};
16use crate::xpath::context::DynamicContext;
17
18const DEFAULT_COLLATION: &str = "http://www.w3.org/2005/xpath-functions/collation/codepoint";
20
21fn validate_collation(collation: Option<&str>) -> Result<(), XPathError> {
24 match collation {
25 None => Ok(()),
26 Some(c) if c.is_empty() || c == DEFAULT_COLLATION => Ok(()),
27 Some(c) => Err(XPathError::unknown_collation(c)),
28 }
29}
30
31pub fn concat<N: DomNavigator>(
39 _context: &mut DynamicContext<'_, N>,
40 args: Vec<XPathValue<N>>,
41) -> Result<XPathValue<N>, XPathError> {
42 if args.len() < 2 {
43 return Err(XPathError::wrong_number_of_arguments(
44 "concat",
45 2,
46 args.len(),
47 ));
48 }
49
50 let strings: Result<Vec<String>, _> = args.into_iter().map(atomize_to_string).collect();
51 let strings = strings?;
52 let refs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect();
53 let result = string_ops::concat(&refs);
54 Ok(XPathValue::string(result))
55}
56
57pub fn string_join<N: DomNavigator>(
61 _context: &mut DynamicContext<'_, N>,
62 mut args: Vec<XPathValue<N>>,
63) -> Result<XPathValue<N>, XPathError> {
64 if args.len() != 2 {
65 return Err(XPathError::wrong_number_of_arguments(
66 "string-join",
67 2,
68 args.len(),
69 ));
70 }
71
72 let separator = atomize_to_string_required(args.pop().unwrap())?;
73 let sequence = args.pop().unwrap();
74
75 let strings: Result<Vec<String>, XPathError> = sequence
77 .into_vec()
78 .into_iter()
79 .map(|item| match item {
80 XmlItem::Atomic(v) => Ok(v.to_string_value()),
81 XmlItem::Node(n) => Ok(n.value()),
82 })
83 .collect();
84 let strings = strings?;
85 let refs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect();
86 let result = string_ops::string_join(&refs, &separator);
87 Ok(XPathValue::string(result))
88}
89
90pub fn substring<N: DomNavigator>(
95 _context: &mut DynamicContext<'_, N>,
96 mut args: Vec<XPathValue<N>>,
97) -> Result<XPathValue<N>, XPathError> {
98 if args.len() < 2 || args.len() > 3 {
99 return Err(XPathError::wrong_number_of_arguments(
100 "substring",
101 2,
102 args.len(),
103 ));
104 }
105
106 let source = atomize_to_string(args.remove(0))?;
107 let start = atomize_to_double(args.remove(0))?;
108 let length = if !args.is_empty() {
109 Some(atomize_to_double(args.remove(0))?)
110 } else {
111 None
112 };
113
114 let result = string_ops::substring(&source, start, length);
115 Ok(XPathValue::string(result))
116}
117
118pub fn string_length<N: DomNavigator>(
123 context: &mut DynamicContext<'_, N>,
124 args: Vec<XPathValue<N>>,
125) -> Result<XPathValue<N>, XPathError> {
126 if args.len() > 1 {
127 return Err(XPathError::wrong_number_of_arguments(
128 "string-length",
129 1,
130 args.len(),
131 ));
132 }
133
134 let source = if args.is_empty() {
135 match &context.context_item {
137 Some(item) => match item {
138 XmlItem::Atomic(v) => v.to_string_value(),
139 XmlItem::Node(n) => n.value(),
140 },
141 None => {
142 return Err(XPathError::XPDY0002 {
143 message: "Context item is absent".to_string(),
144 })
145 }
146 }
147 } else {
148 atomize_to_string(args.into_iter().next().unwrap())?
149 };
150
151 let len = string_ops::string_length(&source);
152 Ok(XPathValue::integer(len as i64))
153}
154
155pub fn normalize_space<N: DomNavigator>(
160 context: &mut DynamicContext<'_, N>,
161 args: Vec<XPathValue<N>>,
162) -> Result<XPathValue<N>, XPathError> {
163 if args.len() > 1 {
164 return Err(XPathError::wrong_number_of_arguments(
165 "normalize-space",
166 1,
167 args.len(),
168 ));
169 }
170
171 let source = if args.is_empty() {
172 match &context.context_item {
174 Some(item) => match item {
175 XmlItem::Atomic(v) => v.to_string_value(),
176 XmlItem::Node(n) => n.value(),
177 },
178 None => {
179 return Err(XPathError::XPDY0002 {
180 message: "Context item is absent".to_string(),
181 })
182 }
183 }
184 } else {
185 atomize_to_string(args.into_iter().next().unwrap())?
186 };
187
188 let result = string_ops::normalize_space(&source);
189 Ok(XPathValue::string(result))
190}
191
192pub fn normalize_unicode<N: DomNavigator>(
197 _context: &mut DynamicContext<'_, N>,
198 mut args: Vec<XPathValue<N>>,
199) -> Result<XPathValue<N>, XPathError> {
200 if args.is_empty() || args.len() > 2 {
201 return Err(XPathError::wrong_number_of_arguments(
202 "normalize-unicode",
203 1,
204 args.len(),
205 ));
206 }
207
208 let source = atomize_to_string_strict(args.remove(0))?;
209
210 let form = if !args.is_empty() {
211 let form_str = atomize_to_string_required(args.remove(0))?;
212 let trimmed = form_str.trim();
213 if trimmed.is_empty() {
214 None
215 } else {
216 match string_ops::UnicodeNormalizationForm::parse(trimmed) {
217 Some(f) => Some(f),
218 None => {
219 return Err(XPathError::FOCH0003 {
220 normalization_form: form_str,
221 })
222 }
223 }
224 }
225 } else {
226 Some(string_ops::UnicodeNormalizationForm::NFC)
228 };
229
230 #[cfg(feature = "unicode-normalization")]
231 let result = string_ops::normalize_unicode(&source, form);
232
233 #[cfg(not(feature = "unicode-normalization"))]
234 let result = string_ops::normalize_unicode(&source, form)?;
235
236 Ok(XPathValue::string(result))
237}
238
239pub fn upper_case<N: DomNavigator>(
243 _context: &mut DynamicContext<'_, N>,
244 mut args: Vec<XPathValue<N>>,
245) -> Result<XPathValue<N>, XPathError> {
246 if args.len() != 1 {
247 return Err(XPathError::wrong_number_of_arguments(
248 "upper-case",
249 1,
250 args.len(),
251 ));
252 }
253
254 let source = atomize_to_string(args.remove(0))?;
255 let result = string_ops::upper_case(&source);
256 Ok(XPathValue::string(result))
257}
258
259pub fn lower_case<N: DomNavigator>(
263 _context: &mut DynamicContext<'_, N>,
264 mut args: Vec<XPathValue<N>>,
265) -> Result<XPathValue<N>, XPathError> {
266 if args.len() != 1 {
267 return Err(XPathError::wrong_number_of_arguments(
268 "lower-case",
269 1,
270 args.len(),
271 ));
272 }
273
274 let source = atomize_to_string(args.remove(0))?;
275 let result = string_ops::lower_case(&source);
276 Ok(XPathValue::string(result))
277}
278
279pub fn translate<N: DomNavigator>(
283 _context: &mut DynamicContext<'_, N>,
284 mut args: Vec<XPathValue<N>>,
285) -> Result<XPathValue<N>, XPathError> {
286 if args.len() != 3 {
287 return Err(XPathError::wrong_number_of_arguments(
288 "translate",
289 3,
290 args.len(),
291 ));
292 }
293
294 let source = atomize_to_string_strict(args.remove(0))?;
295 let map_string = atomize_to_string_strict(args.remove(0))?;
296 let trans_string = atomize_to_string_strict(args.remove(0))?;
297
298 let result = string_ops::translate(&source, &map_string, &trans_string);
299 Ok(XPathValue::string(result))
300}
301
302pub fn encode_for_uri<N: DomNavigator>(
306 _context: &mut DynamicContext<'_, N>,
307 mut args: Vec<XPathValue<N>>,
308) -> Result<XPathValue<N>, XPathError> {
309 if args.len() != 1 {
310 return Err(XPathError::wrong_number_of_arguments(
311 "encode-for-uri",
312 1,
313 args.len(),
314 ));
315 }
316
317 let source = atomize_to_string_strict(args.remove(0))?;
318 let result = string_ops::encode_for_uri(&source);
319 Ok(XPathValue::string(result))
320}
321
322pub fn iri_to_uri<N: DomNavigator>(
326 _context: &mut DynamicContext<'_, N>,
327 mut args: Vec<XPathValue<N>>,
328) -> Result<XPathValue<N>, XPathError> {
329 if args.len() != 1 {
330 return Err(XPathError::wrong_number_of_arguments(
331 "iri-to-uri",
332 1,
333 args.len(),
334 ));
335 }
336
337 let source = atomize_to_string_strict(args.remove(0))?;
338 let result = string_ops::iri_to_uri(&source);
339 Ok(XPathValue::string(result))
340}
341
342pub fn escape_html_uri<N: DomNavigator>(
346 _context: &mut DynamicContext<'_, N>,
347 mut args: Vec<XPathValue<N>>,
348) -> Result<XPathValue<N>, XPathError> {
349 if args.len() != 1 {
350 return Err(XPathError::wrong_number_of_arguments(
351 "escape-html-uri",
352 1,
353 args.len(),
354 ));
355 }
356
357 let source = atomize_to_string_strict(args.remove(0))?;
358 let result = string_ops::escape_html_uri(&source);
359 Ok(XPathValue::string(result))
360}
361
362pub fn contains<N: DomNavigator>(
367 _context: &mut DynamicContext<'_, N>,
368 mut args: Vec<XPathValue<N>>,
369) -> Result<XPathValue<N>, XPathError> {
370 if args.len() < 2 || args.len() > 3 {
371 return Err(XPathError::wrong_number_of_arguments(
372 "contains",
373 2,
374 args.len(),
375 ));
376 }
377
378 if args.len() == 3 {
379 let _collation = atomize_to_string_required(args.pop().unwrap())?;
380 }
381 let source = atomize_to_string(args.remove(0))?;
382 let substring = atomize_to_string(args.remove(0))?;
383 let result = string_ops::contains(&source, &substring);
386 Ok(XPathValue::boolean(result))
387}
388
389pub fn starts_with<N: DomNavigator>(
394 _context: &mut DynamicContext<'_, N>,
395 mut args: Vec<XPathValue<N>>,
396) -> Result<XPathValue<N>, XPathError> {
397 if args.len() < 2 || args.len() > 3 {
398 return Err(XPathError::wrong_number_of_arguments(
399 "starts-with",
400 2,
401 args.len(),
402 ));
403 }
404
405 if args.len() == 3 {
406 let _collation = atomize_to_string_required(args.pop().unwrap())?;
407 }
408 let source = atomize_to_string(args.remove(0))?;
409 let prefix = atomize_to_string(args.remove(0))?;
410 let result = string_ops::starts_with(&source, &prefix);
413 Ok(XPathValue::boolean(result))
414}
415
416pub fn ends_with<N: DomNavigator>(
421 _context: &mut DynamicContext<'_, N>,
422 mut args: Vec<XPathValue<N>>,
423) -> Result<XPathValue<N>, XPathError> {
424 if args.len() < 2 || args.len() > 3 {
425 return Err(XPathError::wrong_number_of_arguments(
426 "ends-with",
427 2,
428 args.len(),
429 ));
430 }
431
432 if args.len() == 3 {
433 let _collation = atomize_to_string_required(args.pop().unwrap())?;
434 }
435 let source = atomize_to_string(args.remove(0))?;
436 let suffix = atomize_to_string(args.remove(0))?;
437 let result = string_ops::ends_with(&source, &suffix);
440 Ok(XPathValue::boolean(result))
441}
442
443pub fn substring_before<N: DomNavigator>(
448 _context: &mut DynamicContext<'_, N>,
449 mut args: Vec<XPathValue<N>>,
450) -> Result<XPathValue<N>, XPathError> {
451 if args.len() < 2 || args.len() > 3 {
452 return Err(XPathError::wrong_number_of_arguments(
453 "substring-before",
454 2,
455 args.len(),
456 ));
457 }
458
459 if args.len() == 3 {
460 let _collation = atomize_to_string_required(args.pop().unwrap())?;
461 }
462 let source = atomize_to_string(args.remove(0))?;
463 let pattern = atomize_to_string(args.remove(0))?;
464 let result = string_ops::substring_before(&source, &pattern);
467 Ok(XPathValue::string(result))
468}
469
470pub fn substring_after<N: DomNavigator>(
475 _context: &mut DynamicContext<'_, N>,
476 mut args: Vec<XPathValue<N>>,
477) -> Result<XPathValue<N>, XPathError> {
478 if args.len() < 2 || args.len() > 3 {
479 return Err(XPathError::wrong_number_of_arguments(
480 "substring-after",
481 2,
482 args.len(),
483 ));
484 }
485
486 if args.len() == 3 {
487 let _collation = atomize_to_string_required(args.pop().unwrap())?;
488 }
489 let source = atomize_to_string(args.remove(0))?;
490 let pattern = atomize_to_string(args.remove(0))?;
491 let result = string_ops::substring_after(&source, &pattern);
494 Ok(XPathValue::string(result))
495}
496
497pub fn string_to_codepoints<N: DomNavigator>(
501 _context: &mut DynamicContext<'_, N>,
502 mut args: Vec<XPathValue<N>>,
503) -> Result<XPathValue<N>, XPathError> {
504 if args.len() != 1 {
505 return Err(XPathError::wrong_number_of_arguments(
506 "string-to-codepoints",
507 1,
508 args.len(),
509 ));
510 }
511
512 let source = atomize_to_string_strict_opt(args.remove(0))?;
513
514 match source {
515 None => Ok(XPathValue::empty()),
516 Some(ref s) if s.is_empty() => Ok(XPathValue::empty()),
517 Some(s) => {
518 let codepoints = string_ops::string_to_codepoints(&s);
519 let items: Vec<XmlItem<N>> = codepoints
520 .into_iter()
521 .map(|cp| XmlItem::Atomic(XmlValue::integer(cp.into())))
522 .collect();
523 Ok(XPathValue::from_sequence(items))
524 }
525 }
526}
527
528pub fn codepoints_to_string<N: DomNavigator>(
533 _context: &mut DynamicContext<'_, N>,
534 mut args: Vec<XPathValue<N>>,
535) -> Result<XPathValue<N>, XPathError> {
536 if args.len() != 1 {
537 return Err(XPathError::wrong_number_of_arguments(
538 "codepoints-to-string",
539 1,
540 args.len(),
541 ));
542 }
543
544 let sequence = args.remove(0);
545 let items = sequence.into_vec();
546
547 if items.is_empty() {
548 return Ok(XPathValue::string(""));
549 }
550
551 let mut codepoints = Vec::with_capacity(items.len());
552 for item in items {
553 match item {
554 XmlItem::Atomic(v) => {
555 let cp = atomize_to_codepoint(&v)?;
556 codepoints.push(cp);
557 }
558 XmlItem::Node(_) => {
559 return Err(XPathError::XPTY0004 {
560 expected: "xs:integer".to_string(),
561 found: "node()".to_string(),
562 });
563 }
564 }
565 }
566
567 match string_ops::codepoints_to_string(&codepoints) {
568 Some(s) => Ok(XPathValue::string(s)),
569 None => Err(XPathError::FOCH0001 {
570 codepoint: "invalid".to_string(),
571 }),
572 }
573}
574
575fn is_valid_xml_char(cp: u32) -> bool {
579 matches!(cp,
580 0x9 | 0xA | 0xD |
581 0x20..=0xD7FF |
582 0xE000..=0xFFFD |
583 0x10000..=0x10FFFF
584 )
585}
586
587fn atomize_to_codepoint(value: &XmlValue) -> Result<u32, XPathError> {
591 let cp;
592
593 if let Some(i) = value.as_integer() {
595 cp = i.try_into().map_err(|_| XPathError::FOCH0001 {
596 codepoint: i.to_string(),
597 })?;
598 } else if value.type_code.is_numeric() {
599 if let Some(d) = value.as_double() {
600 if d.is_nan() || d.is_infinite() || d.fract() != 0.0 {
602 return Err(XPathError::FORG0001 {
603 value: d.to_string(),
604 target_type: "xs:integer".to_string(),
605 });
606 }
607 if d < 0.0 || d > u32::MAX as f64 {
609 return Err(XPathError::FOCH0001 {
610 codepoint: d.to_string(),
611 });
612 }
613 cp = d as u32;
614 } else {
615 return Err(XPathError::XPTY0004 {
616 expected: "xs:integer".to_string(),
617 found: format!("{:?}", value.type_code),
618 });
619 }
620 } else {
621 return Err(XPathError::XPTY0004 {
622 expected: "xs:integer".to_string(),
623 found: format!("{:?}", value.type_code),
624 });
625 }
626
627 if !is_valid_xml_char(cp) {
629 return Err(XPathError::FOCH0001 {
630 codepoint: cp.to_string(),
631 });
632 }
633
634 Ok(cp)
635}
636
637pub fn compare<N: DomNavigator>(
642 _context: &mut DynamicContext<'_, N>,
643 mut args: Vec<XPathValue<N>>,
644) -> Result<XPathValue<N>, XPathError> {
645 if args.len() < 2 || args.len() > 3 {
646 return Err(XPathError::wrong_number_of_arguments(
647 "compare",
648 2,
649 args.len(),
650 ));
651 }
652
653 if args.len() == 3 {
655 let collation = atomize_to_string_required(args.pop().unwrap())?;
656 validate_collation(Some(&collation))?;
657 }
658 let s1 = atomize_to_string_opt(args.remove(0))?;
659 let s2 = atomize_to_string_opt(args.remove(0))?;
660
661 match (s1, s2) {
662 (Some(a), Some(b)) => {
663 let result = string_ops::compare(&a, &b);
664 Ok(XPathValue::integer(result as i64))
665 }
666 _ => Ok(XPathValue::empty()),
667 }
668}
669
670pub fn codepoint_equal<N: DomNavigator>(
674 _context: &mut DynamicContext<'_, N>,
675 mut args: Vec<XPathValue<N>>,
676) -> Result<XPathValue<N>, XPathError> {
677 if args.len() != 2 {
678 return Err(XPathError::wrong_number_of_arguments(
679 "codepoint-equal",
680 2,
681 args.len(),
682 ));
683 }
684
685 let s1 = atomize_to_string_strict_opt(args.remove(0))?;
686 let s2 = atomize_to_string_strict_opt(args.remove(0))?;
687
688 match (s1, s2) {
689 (Some(a), Some(b)) => {
690 let result = string_ops::codepoint_equal(&a, &b);
691 Ok(XPathValue::boolean(result))
692 }
693 _ => Ok(XPathValue::empty()),
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700 use crate::namespace::table::NameTable;
701 use crate::xpath::context::XPathContext;
702 use crate::xpath::RoXmlNavigator;
703
704 fn make_context() -> (NameTable, DynamicContext<'static, RoXmlNavigator<'static>>) {
705 let table = Box::leak(Box::new(NameTable::new()));
706 let static_ctx = Box::leak(Box::new(XPathContext::new(table)));
707 let dyn_ctx = DynamicContext::new(static_ctx, 0);
708 (NameTable::new(), dyn_ctx)
709 }
710
711 #[test]
712 fn test_concat() {
713 let (_, mut ctx) = make_context();
714 let args = vec![
715 XPathValue::string("Hello"),
716 XPathValue::string(", "),
717 XPathValue::string("World!"),
718 ];
719 let result = concat(&mut ctx, args).unwrap();
720 match result {
721 XPathValue::Item(XmlItem::Atomic(v)) => {
722 assert_eq!(v.as_string().unwrap(), "Hello, World!");
723 }
724 _ => panic!("Expected string"),
725 }
726 }
727
728 #[test]
729 fn test_string_join() {
730 let (_, mut ctx) = make_context();
731 let seq = XPathValue::from_sequence(vec![
732 XmlItem::Atomic(XmlValue::string("a")),
733 XmlItem::Atomic(XmlValue::string("b")),
734 XmlItem::Atomic(XmlValue::string("c")),
735 ]);
736 let args = vec![seq, XPathValue::string("-")];
737 let result = string_join(&mut ctx, args).unwrap();
738 match result {
739 XPathValue::Item(XmlItem::Atomic(v)) => {
740 assert_eq!(v.as_string().unwrap(), "a-b-c");
741 }
742 _ => panic!("Expected string"),
743 }
744 }
745
746 #[test]
747 fn test_substring() {
748 let (_, mut ctx) = make_context();
749 let args = vec![
750 XPathValue::string("hello"),
751 XPathValue::double(2.0),
752 XPathValue::double(3.0),
753 ];
754 let result = substring(&mut ctx, args).unwrap();
755 match result {
756 XPathValue::Item(XmlItem::Atomic(v)) => {
757 assert_eq!(v.as_string().unwrap(), "ell");
758 }
759 _ => panic!("Expected string"),
760 }
761 }
762
763 #[test]
764 fn test_string_length() {
765 let (_, mut ctx) = make_context();
766 let args = vec![XPathValue::string("hello")];
767 let result = string_length(&mut ctx, args).unwrap();
768 match result {
769 XPathValue::Item(XmlItem::Atomic(v)) => {
770 assert_eq!(*v.as_integer().unwrap(), 5.into());
771 }
772 _ => panic!("Expected integer"),
773 }
774 }
775
776 #[test]
777 fn test_upper_lower_case() {
778 let (_, mut ctx) = make_context();
779
780 let args = vec![XPathValue::string("Hello")];
781 let result = upper_case(&mut ctx, args).unwrap();
782 match result {
783 XPathValue::Item(XmlItem::Atomic(v)) => {
784 assert_eq!(v.as_string().unwrap(), "HELLO");
785 }
786 _ => panic!("Expected string"),
787 }
788
789 let args = vec![XPathValue::string("Hello")];
790 let result = lower_case(&mut ctx, args).unwrap();
791 match result {
792 XPathValue::Item(XmlItem::Atomic(v)) => {
793 assert_eq!(v.as_string().unwrap(), "hello");
794 }
795 _ => panic!("Expected string"),
796 }
797 }
798
799 #[test]
800 fn test_contains() {
801 let (_, mut ctx) = make_context();
802 let args = vec![
803 XPathValue::string("hello world"),
804 XPathValue::string("world"),
805 ];
806 let result = contains(&mut ctx, args).unwrap();
807 match result {
808 XPathValue::Item(XmlItem::Atomic(v)) => {
809 assert!(v.as_boolean().unwrap());
810 }
811 _ => panic!("Expected boolean"),
812 }
813 }
814
815 #[test]
816 fn test_starts_ends_with() {
817 let (_, mut ctx) = make_context();
818
819 let args = vec![XPathValue::string("hello"), XPathValue::string("he")];
820 let result = starts_with(&mut ctx, args).unwrap();
821 match result {
822 XPathValue::Item(XmlItem::Atomic(v)) => {
823 assert!(v.as_boolean().unwrap());
824 }
825 _ => panic!("Expected boolean"),
826 }
827
828 let args = vec![XPathValue::string("hello"), XPathValue::string("lo")];
829 let result = ends_with(&mut ctx, args).unwrap();
830 match result {
831 XPathValue::Item(XmlItem::Atomic(v)) => {
832 assert!(v.as_boolean().unwrap());
833 }
834 _ => panic!("Expected boolean"),
835 }
836 }
837
838 #[test]
839 fn test_encode_for_uri() {
840 let (_, mut ctx) = make_context();
841 let args = vec![XPathValue::string("hello world")];
842 let result = encode_for_uri(&mut ctx, args).unwrap();
843 match result {
844 XPathValue::Item(XmlItem::Atomic(v)) => {
845 assert_eq!(v.as_string().unwrap(), "hello%20world");
846 }
847 _ => panic!("Expected string"),
848 }
849 }
850
851 #[test]
852 fn test_string_to_codepoints() {
853 let (_, mut ctx) = make_context();
854 let args = vec![XPathValue::string("ABC")];
855 let result = string_to_codepoints(&mut ctx, args).unwrap();
856 let items = result.into_vec();
857 assert_eq!(items.len(), 3);
858 match &items[0] {
859 XmlItem::Atomic(v) => assert_eq!(*v.as_integer().unwrap(), 65.into()),
860 _ => panic!("Expected integer"),
861 }
862 }
863
864 #[test]
865 fn test_codepoints_to_string() {
866 let (_, mut ctx) = make_context();
867 let seq = XPathValue::from_sequence(vec![
868 XmlItem::Atomic(XmlValue::integer(65.into())),
869 XmlItem::Atomic(XmlValue::integer(66.into())),
870 XmlItem::Atomic(XmlValue::integer(67.into())),
871 ]);
872 let args = vec![seq];
873 let result = codepoints_to_string(&mut ctx, args).unwrap();
874 match result {
875 XPathValue::Item(XmlItem::Atomic(v)) => {
876 assert_eq!(v.as_string().unwrap(), "ABC");
877 }
878 _ => panic!("Expected string"),
879 }
880 }
881
882 #[test]
883 fn test_codepoints_to_string_from_doubles() {
884 let (_, mut ctx) = make_context();
885 let seq = XPathValue::from_sequence(vec![
887 XmlItem::Atomic(XmlValue::double(65.0)),
888 XmlItem::Atomic(XmlValue::double(66.0)),
889 XmlItem::Atomic(XmlValue::double(67.0)),
890 ]);
891 let args = vec![seq];
892 let result = codepoints_to_string(&mut ctx, args).unwrap();
893 match result {
894 XPathValue::Item(XmlItem::Atomic(v)) => {
895 assert_eq!(v.as_string().unwrap(), "ABC");
896 }
897 _ => panic!("Expected string"),
898 }
899 }
900
901 #[test]
902 fn test_codepoints_to_string_fractional_double_fails() {
903 let (_, mut ctx) = make_context();
904 let seq = XPathValue::from_sequence(vec![XmlItem::Atomic(XmlValue::double(65.5))]);
906 let args = vec![seq];
907 let result = codepoints_to_string(&mut ctx, args);
908 assert!(result.is_err());
909 }
910
911 #[test]
912 fn test_codepoints_to_string_empty() {
913 let (_, mut ctx) = make_context();
914 let seq = XPathValue::<RoXmlNavigator>::Empty;
915 let args = vec![seq];
916 let result = codepoints_to_string(&mut ctx, args).unwrap();
917 match result {
918 XPathValue::Item(XmlItem::Atomic(v)) => {
919 assert_eq!(v.as_string().unwrap(), "");
920 }
921 _ => panic!("Expected empty string"),
922 }
923 }
924
925 #[test]
926 fn test_compare() {
927 let (_, mut ctx) = make_context();
928 let args = vec![XPathValue::string("abc"), XPathValue::string("abd")];
929 let result = compare(&mut ctx, args).unwrap();
930 match result {
931 XPathValue::Item(XmlItem::Atomic(v)) => {
932 assert_eq!(*v.as_integer().unwrap(), (-1).into());
933 }
934 _ => panic!("Expected integer"),
935 }
936 }
937
938 #[test]
939 fn test_codepoint_equal() {
940 let (_, mut ctx) = make_context();
941 let args = vec![XPathValue::string("abc"), XPathValue::string("abc")];
942 let result = codepoint_equal(&mut ctx, args).unwrap();
943 match result {
944 XPathValue::Item(XmlItem::Atomic(v)) => {
945 assert!(v.as_boolean().unwrap());
946 }
947 _ => panic!("Expected boolean"),
948 }
949
950 let args = vec![XPathValue::string("abc"), XPathValue::string("ABC")];
951 let result = codepoint_equal(&mut ctx, args).unwrap();
952 match result {
953 XPathValue::Item(XmlItem::Atomic(v)) => {
954 assert!(!v.as_boolean().unwrap());
955 }
956 _ => panic!("Expected boolean"),
957 }
958 }
959
960 #[test]
961 fn test_compare_with_default_collation() {
962 let (_, mut ctx) = make_context();
963 let args = vec![
965 XPathValue::string("abc"),
966 XPathValue::string("abd"),
967 XPathValue::string(DEFAULT_COLLATION),
968 ];
969 let result = compare(&mut ctx, args).unwrap();
970 match result {
971 XPathValue::Item(XmlItem::Atomic(v)) => {
972 assert_eq!(*v.as_integer().unwrap(), (-1).into());
973 }
974 _ => panic!("Expected integer"),
975 }
976 }
977
978 #[test]
979 fn test_compare_with_invalid_collation() {
980 let (_, mut ctx) = make_context();
981 let args = vec![
983 XPathValue::string("abc"),
984 XPathValue::string("abd"),
985 XPathValue::string("http://example.com/custom-collation"),
986 ];
987 let result = compare(&mut ctx, args);
988 assert!(matches!(result, Err(XPathError::FOCH0002 { .. })));
989 }
990}