1use crate::ids::NameId;
16use crate::namespace::qname::QualifiedName;
17use crate::namespace::table::NameTable;
18use crate::types::value::{XmlAtomicValue, XmlValue, XmlValueKind};
19use crate::types::XmlTypeCode;
20use crate::xpath::context::DynamicContext;
21use crate::xpath::error::XPathError;
22use crate::xpath::iterator::XmlItem;
23use crate::xpath::{DomNavigator, DomNodeType};
24
25use super::{atomize_to_string_opt, materialize, XPathValue};
26
27pub fn name<N: DomNavigator>(
36 context: &mut DynamicContext<'_, N>,
37 args: Vec<XPathValue<N>>,
38) -> Result<XPathValue<N>, XPathError> {
39 if args.len() > 1 {
40 return Err(XPathError::wrong_number_of_arguments("name", 1, args.len()));
41 }
42
43 let node = get_node_arg(context, args)?;
44
45 match node {
46 None => Ok(XPathValue::string("")),
47 Some(nav) => Ok(XPathValue::string(nav.name().to_string())),
48 }
49}
50
51pub fn local_name<N: DomNavigator>(
60 context: &mut DynamicContext<'_, N>,
61 args: Vec<XPathValue<N>>,
62) -> Result<XPathValue<N>, XPathError> {
63 if args.len() > 1 {
64 return Err(XPathError::wrong_number_of_arguments(
65 "local-name",
66 1,
67 args.len(),
68 ));
69 }
70
71 let node = get_node_arg(context, args)?;
72
73 match node {
74 None => Ok(XPathValue::string("")),
75 Some(nav) => Ok(XPathValue::string(nav.local_name().to_string())),
76 }
77}
78
79pub fn namespace_uri<N: DomNavigator>(
88 context: &mut DynamicContext<'_, N>,
89 args: Vec<XPathValue<N>>,
90) -> Result<XPathValue<N>, XPathError> {
91 if args.len() > 1 {
92 return Err(XPathError::wrong_number_of_arguments(
93 "namespace-uri",
94 1,
95 args.len(),
96 ));
97 }
98
99 let node = get_node_arg(context, args)?;
100
101 match node {
102 None => Ok(XPathValue::from_atomic(any_uri(""))),
103 Some(nav) => Ok(XPathValue::from_atomic(any_uri(nav.namespace_uri()))),
104 }
105}
106
107pub fn node_name<N: DomNavigator>(
117 context: &mut DynamicContext<'_, N>,
118 args: Vec<XPathValue<N>>,
119) -> Result<XPathValue<N>, XPathError> {
120 if args.len() > 1 {
121 return Err(XPathError::wrong_number_of_arguments(
122 "node-name",
123 1,
124 args.len(),
125 ));
126 }
127
128 let node = get_node_arg(context, args)?;
129
130 match node {
131 None => Ok(XPathValue::Empty),
132 Some(nav) => {
133 let names = context.static_context.names;
134 match nav.node_type() {
135 DomNodeType::Element | DomNodeType::Attribute => {
136 let local_name = get_or_empty_id(names, nav.local_name());
137 let namespace_uri = get_opt_id(names, nav.namespace_uri());
138 let prefix = get_opt_id(names, nav.prefix());
139 let qname = QualifiedName::new(namespace_uri, local_name, prefix);
140 Ok(XPathValue::from_atomic(XmlValue::new(
141 XmlTypeCode::QName,
142 XmlValueKind::Atomic(XmlAtomicValue::QName(qname)),
143 )))
144 }
145 DomNodeType::ProcessingInstruction => {
146 let local_name = get_or_empty_id(names, nav.name());
148 let qname = QualifiedName::new(None, local_name, None);
149 Ok(XPathValue::from_atomic(XmlValue::new(
150 XmlTypeCode::QName,
151 XmlValueKind::Atomic(XmlAtomicValue::QName(qname)),
152 )))
153 }
154 DomNodeType::Namespace => {
155 let local_name = get_or_empty_id(names, nav.local_name());
157 let qname = QualifiedName::new(None, local_name, None);
158 Ok(XPathValue::from_atomic(XmlValue::new(
159 XmlTypeCode::QName,
160 XmlValueKind::Atomic(XmlAtomicValue::QName(qname)),
161 )))
162 }
163 _ => Ok(XPathValue::Empty),
165 }
166 }
167 }
168}
169
170pub fn nilled<N: DomNavigator>(
179 context: &mut DynamicContext<'_, N>,
180 args: Vec<XPathValue<N>>,
181) -> Result<XPathValue<N>, XPathError> {
182 if args.len() > 1 {
183 return Err(XPathError::wrong_number_of_arguments(
184 "nilled",
185 1,
186 args.len(),
187 ));
188 }
189
190 let node = get_node_arg(context, args)?;
191 let node = match node {
192 None => return Ok(XPathValue::Empty),
193 Some(n) => n,
194 };
195
196 match node.node_type() {
197 DomNodeType::Element => {
198 let mut nav = node.clone();
200 if nav.move_to_first_attribute() {
201 loop {
202 if nav.local_name() == "nil"
203 && nav.namespace_uri() == "http://www.w3.org/2001/XMLSchema-instance"
204 {
205 let value = nav.value();
206 let is_nilled = value == "true" || value == "1";
207 return Ok(XPathValue::boolean(is_nilled));
208 }
209 if !nav.move_to_next_attribute() {
210 break;
211 }
212 }
213 }
214 Ok(XPathValue::boolean(false))
216 }
217 _ => Ok(XPathValue::Empty),
218 }
219}
220
221pub fn base_uri<N: DomNavigator>(
230 context: &mut DynamicContext<'_, N>,
231 args: Vec<XPathValue<N>>,
232) -> Result<XPathValue<N>, XPathError> {
233 if args.len() > 1 {
234 return Err(XPathError::wrong_number_of_arguments(
235 "base-uri",
236 1,
237 args.len(),
238 ));
239 }
240
241 let node = get_node_arg(context, args)?;
242
243 match node {
244 None => Ok(XPathValue::Empty),
245 Some(nav) => {
246 let uri = compute_base_uri(&nav, context.base_uri.as_deref());
247 match uri {
248 Some(u) if !u.is_empty() => Ok(XPathValue::from_atomic(any_uri(u))),
249 _ => Ok(XPathValue::Empty),
250 }
251 }
252 }
253}
254
255pub fn document_uri<N: DomNavigator>(
263 context: &mut DynamicContext<'_, N>,
264 args: Vec<XPathValue<N>>,
265) -> Result<XPathValue<N>, XPathError> {
266 if args.len() > 1 {
267 return Err(XPathError::wrong_number_of_arguments(
268 "document-uri",
269 1,
270 args.len(),
271 ));
272 }
273
274 let node = get_node_arg(context, args)?;
275 let node = match node {
276 None => return Ok(XPathValue::Empty),
277 Some(n) => n,
278 };
279
280 match node.node_type() {
281 DomNodeType::Root => {
282 let uri = node.base_uri();
283 if uri.is_empty() {
284 Ok(XPathValue::Empty)
285 } else {
286 Ok(XPathValue::from_atomic(any_uri(uri)))
287 }
288 }
289 _ => Ok(XPathValue::Empty),
290 }
291}
292
293pub fn lang<N: DomNavigator>(
302 context: &mut DynamicContext<'_, N>,
303 mut args: Vec<XPathValue<N>>,
304) -> Result<XPathValue<N>, XPathError> {
305 if args.is_empty() || args.len() > 2 {
306 return Err(XPathError::wrong_number_of_arguments("lang", 1, args.len()));
307 }
308
309 let test_lang = atomize_to_string_opt(args.remove(0))?.unwrap_or_default();
310
311 let node = if args.is_empty() {
312 match &context.context_item {
314 Some(XmlItem::Node(n)) => n.clone(),
315 Some(XmlItem::Atomic(_)) => {
316 return Err(XPathError::XPTY0004 {
317 expected: "node()".to_string(),
318 found: "atomic value".to_string(),
319 });
320 }
321 None => {
322 return Err(XPathError::XPDY0002 {
323 message: "Context item is absent".to_string(),
324 });
325 }
326 }
327 } else {
328 let node_arg = args.remove(0);
329 let items = materialize(node_arg);
330 if items.is_empty() {
331 return Ok(XPathValue::boolean(false));
332 }
333 match &items[0] {
334 XmlItem::Node(n) => n.clone(),
335 XmlItem::Atomic(_) => {
336 return Err(XPathError::XPTY0004 {
337 expected: "node()".to_string(),
338 found: "atomic value".to_string(),
339 });
340 }
341 }
342 };
343
344 let node_lang = find_xml_lang(&node);
346
347 let result = match node_lang {
348 Some(lang) => lang_matches(&lang, &test_lang),
349 None => false,
350 };
351
352 Ok(XPathValue::boolean(result))
353}
354
355pub fn root<N: DomNavigator>(
363 context: &mut DynamicContext<'_, N>,
364 args: Vec<XPathValue<N>>,
365) -> Result<XPathValue<N>, XPathError> {
366 if args.len() > 1 {
367 return Err(XPathError::wrong_number_of_arguments("root", 1, args.len()));
368 }
369
370 let node = get_node_arg(context, args)?;
371
372 match node {
373 None => Ok(XPathValue::Empty),
374 Some(mut nav) => {
375 nav.move_to_root();
376 Ok(XPathValue::from_node(nav))
377 }
378 }
379}
380
381pub fn id<N: DomNavigator>(
398 context: &mut DynamicContext<'_, N>,
399 args: Vec<XPathValue<N>>,
400) -> Result<XPathValue<N>, XPathError> {
401 if args.is_empty() || args.len() > 2 {
402 return Err(XPathError::wrong_number_of_arguments("id", 1, args.len()));
403 }
404
405 let mut args = args;
406
407 let ref_node = if args.len() == 2 {
409 let node_arg = args.remove(1);
410 let items = materialize(node_arg);
411 if items.len() != 1 {
412 return Err(XPathError::XPTY0004 {
413 expected: "node()".to_string(),
414 found: if items.is_empty() {
415 "empty-sequence()".to_string()
416 } else {
417 format!("sequence of {} items", items.len())
418 },
419 });
420 }
421 match items.into_iter().next().unwrap() {
422 XmlItem::Node(n) => n,
423 XmlItem::Atomic(_) => {
424 return Err(XPathError::XPTY0004 {
425 expected: "node()".to_string(),
426 found: "atomic value".to_string(),
427 });
428 }
429 }
430 } else {
431 match &context.context_item {
432 Some(XmlItem::Node(n)) => n.clone(),
433 Some(XmlItem::Atomic(_)) => {
434 return Err(XPathError::XPTY0004 {
435 expected: "node()".to_string(),
436 found: "atomic value".to_string(),
437 });
438 }
439 None => {
440 return Err(XPathError::XPDY0002 {
441 message: "Context item is absent".to_string(),
442 });
443 }
444 }
445 };
446
447 let mut root_nav = ref_node;
449 root_nav.move_to_root();
450
451 let id_arg = args.into_iter().next().unwrap();
453 let tokens = collect_id_tokens(id_arg);
454
455 let mut result_nodes: Vec<N> = Vec::new();
457 for token in &tokens {
458 if let Some(found) = root_nav.find_element_by_id(token)? {
459 let already_present = result_nodes.iter().any(|n| n.is_same_position(&found));
461 if !already_present {
462 result_nodes.push(found);
463 }
464 }
465 }
466
467 result_nodes.sort_by(|a, b| {
469 use crate::xpath::node_ops::compare_document_order;
470 compare_document_order(a, b)
471 });
472
473 let items: Vec<XmlItem<N>> = result_nodes.into_iter().map(XmlItem::Node).collect();
475 Ok(XPathValue::from_sequence(items))
476}
477
478fn collect_id_tokens<N: DomNavigator>(value: XPathValue<N>) -> Vec<String> {
483 let mut tokens = Vec::new();
484 match value {
485 XPathValue::Empty => {}
486 XPathValue::Item(item) => {
487 let s = item_string_value(item);
488 for token in s.split_whitespace() {
489 tokens.push(token.to_string());
490 }
491 }
492 XPathValue::Sequence(items) => {
493 for item in items {
494 let s = item_string_value(item);
495 for token in s.split_whitespace() {
496 tokens.push(token.to_string());
497 }
498 }
499 }
500 }
501 tokens
502}
503
504fn item_string_value<N: DomNavigator>(item: XmlItem<N>) -> String {
506 match item {
507 XmlItem::Node(nav) => nav.value(),
508 XmlItem::Atomic(v) => crate::xpath::atomize::string_value(&v),
509 }
510}
511
512fn any_uri(s: impl Into<String>) -> XmlValue {
518 XmlValue::new(
519 XmlTypeCode::AnyUri,
520 XmlValueKind::Atomic(XmlAtomicValue::AnyUri(s.into())),
521 )
522}
523
524fn get_or_empty_id(names: &NameTable, s: &str) -> NameId {
529 if s.is_empty() {
530 NameId(0)
531 } else {
532 names.add(s)
533 }
534}
535
536fn get_opt_id(names: &NameTable, s: &str) -> Option<NameId> {
541 if s.is_empty() {
542 None
543 } else {
544 Some(names.add(s))
545 }
546}
547
548fn get_node_arg<N: DomNavigator>(
551 context: &DynamicContext<'_, N>,
552 args: Vec<XPathValue<N>>,
553) -> Result<Option<N>, XPathError> {
554 if args.is_empty() {
555 match &context.context_item {
557 Some(XmlItem::Node(n)) => Ok(Some(n.clone())),
558 Some(XmlItem::Atomic(_)) => {
559 Ok(None)
561 }
562 None => Err(XPathError::XPDY0002 {
563 message: "Context item is absent".to_string(),
564 }),
565 }
566 } else {
567 let items = materialize(args.into_iter().next().unwrap());
568 if items.is_empty() {
569 return Ok(None);
570 }
571 match &items[0] {
572 XmlItem::Node(n) => Ok(Some(n.clone())),
573 XmlItem::Atomic(_) => {
574 Ok(None)
576 }
577 }
578 }
579}
580
581fn compute_base_uri<N: DomNavigator>(node: &N, static_base_uri: Option<&str>) -> Option<String> {
588 let mut xml_bases: Vec<String> = Vec::new();
589 let mut nav = node.clone();
590
591 match nav.node_type() {
593 DomNodeType::Text
594 | DomNodeType::Whitespace
595 | DomNodeType::SignificantWhitespace
596 | DomNodeType::Comment
597 | DomNodeType::ProcessingInstruction => {
598 if !nav.move_to_parent() {
599 return None;
600 }
601 }
602 _ => {}
603 }
604
605 loop {
607 if nav.node_type() == DomNodeType::Element {
608 if let Some(xml_base) = get_xml_base_attr(&nav) {
609 xml_bases.push(xml_base);
610 }
611 }
612
613 if nav.node_type() == DomNodeType::Root {
614 let doc_base = nav.base_uri();
616 if !doc_base.is_empty() {
617 xml_bases.push(doc_base.to_string());
618 }
619 break;
620 }
621
622 if !nav.move_to_parent() {
623 let doc_base = nav.base_uri();
631 if !doc_base.is_empty() {
632 xml_bases.push(doc_base.to_string());
633 }
634 break;
635 }
636 }
637
638 let mut base = static_base_uri.map(|s| s.to_string());
640
641 for uri in xml_bases.into_iter().rev() {
643 base = Some(resolve_uri(&uri, base.as_deref()));
644 }
645
646 base
647}
648
649fn get_xml_base_attr<N: DomNavigator>(nav: &N) -> Option<String> {
651 let mut attr_nav = nav.clone();
652 if attr_nav.move_to_first_attribute() {
653 loop {
654 if attr_nav.local_name() == "base"
655 && attr_nav.namespace_uri() == "http://www.w3.org/XML/1998/namespace"
656 {
657 return Some(attr_nav.value());
658 }
659 if !attr_nav.move_to_next_attribute() {
660 break;
661 }
662 }
663 }
664 None
665}
666
667fn resolve_uri(uri: &str, base: Option<&str>) -> String {
673 if uri.contains("://") || uri.starts_with("file:") {
675 return uri.to_string();
676 }
677
678 match base {
679 None => uri.to_string(),
680 Some(base_uri) => {
681 if uri.is_empty() {
682 return base_uri.to_string();
683 }
684
685 if uri.starts_with('/') {
687 if let Some(scheme_end) = base_uri.find("://") {
689 if let Some(path_start) = base_uri[scheme_end + 3..].find('/') {
690 let host_end = scheme_end + 3 + path_start;
691 return format!("{}{}", &base_uri[..host_end], uri);
692 }
693 }
694 uri.to_string()
695 } else {
696 if let Some(last_slash) = base_uri.rfind('/') {
698 format!("{}/{}", &base_uri[..last_slash], uri)
699 } else {
700 uri.to_string()
701 }
702 }
703 }
704 }
705}
706
707fn find_xml_lang<N: DomNavigator>(node: &N) -> Option<String> {
709 let mut nav = node.clone();
710
711 loop {
712 if nav.node_type() == DomNodeType::Element {
714 let mut attr_nav = nav.clone();
715 if attr_nav.move_to_first_attribute() {
716 loop {
717 if attr_nav.local_name() == "lang"
718 && attr_nav.namespace_uri() == "http://www.w3.org/XML/1998/namespace"
719 {
720 return Some(attr_nav.value());
721 }
722 if !attr_nav.move_to_next_attribute() {
723 break;
724 }
725 }
726 }
727 }
728
729 if !nav.move_to_parent() {
731 break;
732 }
733 }
734
735 None
736}
737
738fn lang_matches(lang: &str, test_lang: &str) -> bool {
743 let lang_lower = lang.to_lowercase();
744 let test_lower = test_lang.to_lowercase();
745
746 if lang_lower == test_lower {
747 return true;
748 }
749
750 if lang_lower.starts_with(&test_lower) {
752 let remainder = &lang_lower[test_lower.len()..];
753 if remainder.starts_with('-') {
754 return true;
755 }
756 }
757
758 false
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn test_lang_matches_exact() {
767 assert!(lang_matches("en", "en"));
768 assert!(lang_matches("EN", "en"));
769 assert!(lang_matches("en", "EN"));
770 }
771
772 #[test]
773 fn test_lang_matches_subtag() {
774 assert!(lang_matches("en-US", "en"));
775 assert!(lang_matches("en-GB", "en"));
776 assert!(lang_matches("zh-Hans-CN", "zh"));
777 }
778
779 #[test]
780 fn test_lang_matches_no_match() {
781 assert!(!lang_matches("de", "en"));
782 assert!(!lang_matches("english", "en"));
783 assert!(!lang_matches("en", "en-US"));
784 }
785
786 #[test]
787 fn test_any_uri_creation() {
788 let uri = any_uri("http://example.com");
789 assert_eq!(uri.type_code, XmlTypeCode::AnyUri);
790 }
791
792 #[test]
793 fn test_lang_matches_empty_testlang() {
794 assert!(!lang_matches("en", ""));
796 assert!(!lang_matches("en-US", ""));
797 }
798
799 #[test]
800 fn test_resolve_uri_absolute() {
801 assert_eq!(
803 resolve_uri("http://example.com/path", Some("http://other.com/")),
804 "http://example.com/path"
805 );
806 }
807
808 #[test]
809 fn test_resolve_uri_relative() {
810 assert_eq!(
812 resolve_uri("file.xml", Some("http://example.com/dir/base.xml")),
813 "http://example.com/dir/file.xml"
814 );
815 }
816
817 #[test]
818 fn test_resolve_uri_absolute_path() {
819 assert_eq!(
821 resolve_uri(
822 "/absolute/path.xml",
823 Some("http://example.com/dir/base.xml")
824 ),
825 "http://example.com/absolute/path.xml"
826 );
827 }
828
829 #[test]
830 fn test_resolve_uri_no_base() {
831 assert_eq!(resolve_uri("relative.xml", None), "relative.xml");
833 }
834
835 #[test]
836 fn test_resolve_uri_empty() {
837 assert_eq!(
839 resolve_uri("", Some("http://example.com/base.xml")),
840 "http://example.com/base.xml"
841 );
842 }
843
844 #[test]
849 fn test_collect_id_tokens_single_string() {
850 use crate::xpath::RoXmlNavigator;
851 let value: super::super::XPathValue<RoXmlNavigator<'static>> =
852 super::super::XPathValue::string("abc");
853 let tokens = collect_id_tokens(value);
854 assert_eq!(tokens, vec!["abc"]);
855 }
856
857 #[test]
858 fn test_collect_id_tokens_multi_whitespace() {
859 use crate::xpath::RoXmlNavigator;
860 let value: super::super::XPathValue<RoXmlNavigator<'static>> =
861 super::super::XPathValue::string(" foo bar baz ");
862 let tokens = collect_id_tokens(value);
863 assert_eq!(tokens, vec!["foo", "bar", "baz"]);
864 }
865
866 #[test]
867 fn test_collect_id_tokens_empty() {
868 use crate::xpath::RoXmlNavigator;
869 let value: super::super::XPathValue<RoXmlNavigator<'static>> =
870 super::super::XPathValue::Empty;
871 let tokens = collect_id_tokens(value);
872 assert!(tokens.is_empty());
873 }
874
875 #[test]
876 fn test_collect_id_tokens_sequence() {
877 use crate::types::value::XmlValue;
878 use crate::xpath::iterator::XmlItem;
879 use crate::xpath::RoXmlNavigator;
880
881 let items = vec![
882 XmlItem::Atomic(XmlValue::string("a1 a2")),
883 XmlItem::Atomic(XmlValue::string("b1")),
884 ];
885 let value: super::super::XPathValue<RoXmlNavigator<'static>> =
886 super::super::XPathValue::from_sequence(items);
887 let tokens = collect_id_tokens(value);
888 assert_eq!(tokens, vec!["a1", "a2", "b1"]);
889 }
890
891 #[test]
892 fn test_fn_id_empty_without_dtd() {
893 use crate::namespace::table::NameTable;
896 use crate::xpath::context::{DynamicContext, XPathContext};
897 use crate::xpath::RoXmlNavigator;
898
899 let doc = roxmltree::Document::parse("<root><a id='x'/></root>").unwrap();
900 let nav = RoXmlNavigator::new(&doc);
901
902 let names = NameTable::new();
903 let static_ctx = XPathContext::new(&names);
904 let mut dyn_ctx = DynamicContext::new(&static_ctx, 0);
905 dyn_ctx.context_item = Some(XmlItem::Node(nav));
906
907 let args = vec![super::super::XPathValue::string("x")];
908 let result = id(&mut dyn_ctx, args).unwrap();
909
910 assert!(
912 matches!(result, super::super::XPathValue::Empty),
913 "Expected empty sequence from fn:id without DTD"
914 );
915 }
916}