use crate::ids::NameId;
use crate::namespace::qname::QualifiedName;
use crate::namespace::table::NameTable;
use crate::types::value::{XmlAtomicValue, XmlValue, XmlValueKind};
use crate::types::XmlTypeCode;
use crate::xpath::context::DynamicContext;
use crate::xpath::error::XPathError;
use crate::xpath::iterator::XmlItem;
use crate::xpath::{DomNavigator, DomNodeType};
use super::{atomize_to_string_opt, materialize, XPathValue};
pub fn name<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() > 1 {
return Err(XPathError::wrong_number_of_arguments("name", 1, args.len()));
}
let node = get_node_arg(context, args)?;
match node {
None => Ok(XPathValue::string("")),
Some(nav) => Ok(XPathValue::string(nav.name().to_string())),
}
}
pub fn local_name<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() > 1 {
return Err(XPathError::wrong_number_of_arguments(
"local-name",
1,
args.len(),
));
}
let node = get_node_arg(context, args)?;
match node {
None => Ok(XPathValue::string("")),
Some(nav) => Ok(XPathValue::string(nav.local_name().to_string())),
}
}
pub fn namespace_uri<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() > 1 {
return Err(XPathError::wrong_number_of_arguments(
"namespace-uri",
1,
args.len(),
));
}
let node = get_node_arg(context, args)?;
match node {
None => Ok(XPathValue::from_atomic(any_uri(""))),
Some(nav) => Ok(XPathValue::from_atomic(any_uri(nav.namespace_uri()))),
}
}
pub fn node_name<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() > 1 {
return Err(XPathError::wrong_number_of_arguments(
"node-name",
1,
args.len(),
));
}
let node = get_node_arg(context, args)?;
match node {
None => Ok(XPathValue::Empty),
Some(nav) => {
let names = context.static_context.names;
match nav.node_type() {
DomNodeType::Element | DomNodeType::Attribute => {
let local_name = get_or_empty_id(names, nav.local_name());
let namespace_uri = get_opt_id(names, nav.namespace_uri());
let prefix = get_opt_id(names, nav.prefix());
let qname = QualifiedName::new(namespace_uri, local_name, prefix);
Ok(XPathValue::from_atomic(XmlValue::new(
XmlTypeCode::QName,
XmlValueKind::Atomic(XmlAtomicValue::QName(qname)),
)))
}
DomNodeType::ProcessingInstruction => {
let local_name = get_or_empty_id(names, nav.name());
let qname = QualifiedName::new(None, local_name, None);
Ok(XPathValue::from_atomic(XmlValue::new(
XmlTypeCode::QName,
XmlValueKind::Atomic(XmlAtomicValue::QName(qname)),
)))
}
DomNodeType::Namespace => {
let local_name = get_or_empty_id(names, nav.local_name());
let qname = QualifiedName::new(None, local_name, None);
Ok(XPathValue::from_atomic(XmlValue::new(
XmlTypeCode::QName,
XmlValueKind::Atomic(XmlAtomicValue::QName(qname)),
)))
}
_ => Ok(XPathValue::Empty),
}
}
}
}
pub fn nilled<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() > 1 {
return Err(XPathError::wrong_number_of_arguments(
"nilled",
1,
args.len(),
));
}
let node = get_node_arg(context, args)?;
let node = match node {
None => return Ok(XPathValue::Empty),
Some(n) => n,
};
match node.node_type() {
DomNodeType::Element => {
let mut nav = node.clone();
if nav.move_to_first_attribute() {
loop {
if nav.local_name() == "nil"
&& nav.namespace_uri() == "http://www.w3.org/2001/XMLSchema-instance"
{
let value = nav.value();
let is_nilled = value == "true" || value == "1";
return Ok(XPathValue::boolean(is_nilled));
}
if !nav.move_to_next_attribute() {
break;
}
}
}
Ok(XPathValue::boolean(false))
}
_ => Ok(XPathValue::Empty),
}
}
pub fn base_uri<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() > 1 {
return Err(XPathError::wrong_number_of_arguments(
"base-uri",
1,
args.len(),
));
}
let node = get_node_arg(context, args)?;
match node {
None => Ok(XPathValue::Empty),
Some(nav) => {
let uri = compute_base_uri(&nav, context.base_uri.as_deref());
match uri {
Some(u) if !u.is_empty() => Ok(XPathValue::from_atomic(any_uri(u))),
_ => Ok(XPathValue::Empty),
}
}
}
}
pub fn document_uri<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() > 1 {
return Err(XPathError::wrong_number_of_arguments(
"document-uri",
1,
args.len(),
));
}
let node = get_node_arg(context, args)?;
let node = match node {
None => return Ok(XPathValue::Empty),
Some(n) => n,
};
match node.node_type() {
DomNodeType::Root => {
let uri = node.base_uri();
if uri.is_empty() {
Ok(XPathValue::Empty)
} else {
Ok(XPathValue::from_atomic(any_uri(uri)))
}
}
_ => Ok(XPathValue::Empty),
}
}
pub fn lang<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
mut args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.is_empty() || args.len() > 2 {
return Err(XPathError::wrong_number_of_arguments("lang", 1, args.len()));
}
let test_lang = atomize_to_string_opt(args.remove(0))?.unwrap_or_default();
let node = if args.is_empty() {
match &context.context_item {
Some(XmlItem::Node(n)) => n.clone(),
Some(XmlItem::Atomic(_)) => {
return Err(XPathError::XPTY0004 {
expected: "node()".to_string(),
found: "atomic value".to_string(),
});
}
None => {
return Err(XPathError::XPDY0002 {
message: "Context item is absent".to_string(),
});
}
}
} else {
let node_arg = args.remove(0);
let items = materialize(node_arg);
if items.is_empty() {
return Ok(XPathValue::boolean(false));
}
match &items[0] {
XmlItem::Node(n) => n.clone(),
XmlItem::Atomic(_) => {
return Err(XPathError::XPTY0004 {
expected: "node()".to_string(),
found: "atomic value".to_string(),
});
}
}
};
let node_lang = find_xml_lang(&node);
let result = match node_lang {
Some(lang) => lang_matches(&lang, &test_lang),
None => false,
};
Ok(XPathValue::boolean(result))
}
pub fn root<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.len() > 1 {
return Err(XPathError::wrong_number_of_arguments("root", 1, args.len()));
}
let node = get_node_arg(context, args)?;
match node {
None => Ok(XPathValue::Empty),
Some(mut nav) => {
nav.move_to_root();
Ok(XPathValue::from_node(nav))
}
}
}
pub fn id<N: DomNavigator>(
context: &mut DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<XPathValue<N>, XPathError> {
if args.is_empty() || args.len() > 2 {
return Err(XPathError::wrong_number_of_arguments("id", 1, args.len()));
}
let mut args = args;
let ref_node = if args.len() == 2 {
let node_arg = args.remove(1);
let items = materialize(node_arg);
if items.len() != 1 {
return Err(XPathError::XPTY0004 {
expected: "node()".to_string(),
found: if items.is_empty() {
"empty-sequence()".to_string()
} else {
format!("sequence of {} items", items.len())
},
});
}
match items.into_iter().next().unwrap() {
XmlItem::Node(n) => n,
XmlItem::Atomic(_) => {
return Err(XPathError::XPTY0004 {
expected: "node()".to_string(),
found: "atomic value".to_string(),
});
}
}
} else {
match &context.context_item {
Some(XmlItem::Node(n)) => n.clone(),
Some(XmlItem::Atomic(_)) => {
return Err(XPathError::XPTY0004 {
expected: "node()".to_string(),
found: "atomic value".to_string(),
});
}
None => {
return Err(XPathError::XPDY0002 {
message: "Context item is absent".to_string(),
});
}
}
};
let mut root_nav = ref_node;
root_nav.move_to_root();
let id_arg = args.into_iter().next().unwrap();
let tokens = collect_id_tokens(id_arg);
let mut result_nodes: Vec<N> = Vec::new();
for token in &tokens {
if let Some(found) = root_nav.find_element_by_id(token)? {
let already_present = result_nodes.iter().any(|n| n.is_same_position(&found));
if !already_present {
result_nodes.push(found);
}
}
}
result_nodes.sort_by(|a, b| {
use crate::xpath::node_ops::compare_document_order;
compare_document_order(a, b)
});
let items: Vec<XmlItem<N>> = result_nodes.into_iter().map(XmlItem::Node).collect();
Ok(XPathValue::from_sequence(items))
}
fn collect_id_tokens<N: DomNavigator>(value: XPathValue<N>) -> Vec<String> {
let mut tokens = Vec::new();
match value {
XPathValue::Empty => {}
XPathValue::Item(item) => {
let s = item_string_value(item);
for token in s.split_whitespace() {
tokens.push(token.to_string());
}
}
XPathValue::Sequence(items) => {
for item in items {
let s = item_string_value(item);
for token in s.split_whitespace() {
tokens.push(token.to_string());
}
}
}
}
tokens
}
fn item_string_value<N: DomNavigator>(item: XmlItem<N>) -> String {
match item {
XmlItem::Node(nav) => nav.value(),
XmlItem::Atomic(v) => crate::xpath::atomize::string_value(&v),
}
}
fn any_uri(s: impl Into<String>) -> XmlValue {
XmlValue::new(
XmlTypeCode::AnyUri,
XmlValueKind::Atomic(XmlAtomicValue::AnyUri(s.into())),
)
}
fn get_or_empty_id(names: &NameTable, s: &str) -> NameId {
if s.is_empty() {
NameId(0)
} else {
names.add(s)
}
}
fn get_opt_id(names: &NameTable, s: &str) -> Option<NameId> {
if s.is_empty() {
None
} else {
Some(names.add(s))
}
}
fn get_node_arg<N: DomNavigator>(
context: &DynamicContext<'_, N>,
args: Vec<XPathValue<N>>,
) -> Result<Option<N>, XPathError> {
if args.is_empty() {
match &context.context_item {
Some(XmlItem::Node(n)) => Ok(Some(n.clone())),
Some(XmlItem::Atomic(_)) => {
Ok(None)
}
None => Err(XPathError::XPDY0002 {
message: "Context item is absent".to_string(),
}),
}
} else {
let items = materialize(args.into_iter().next().unwrap());
if items.is_empty() {
return Ok(None);
}
match &items[0] {
XmlItem::Node(n) => Ok(Some(n.clone())),
XmlItem::Atomic(_) => {
Ok(None)
}
}
}
}
fn compute_base_uri<N: DomNavigator>(node: &N, static_base_uri: Option<&str>) -> Option<String> {
let mut xml_bases: Vec<String> = Vec::new();
let mut nav = node.clone();
match nav.node_type() {
DomNodeType::Text
| DomNodeType::Whitespace
| DomNodeType::SignificantWhitespace
| DomNodeType::Comment
| DomNodeType::ProcessingInstruction => {
if !nav.move_to_parent() {
return None;
}
}
_ => {}
}
loop {
if nav.node_type() == DomNodeType::Element {
if let Some(xml_base) = get_xml_base_attr(&nav) {
xml_bases.push(xml_base);
}
}
if nav.node_type() == DomNodeType::Root {
let doc_base = nav.base_uri();
if !doc_base.is_empty() {
xml_bases.push(doc_base.to_string());
}
break;
}
if !nav.move_to_parent() {
let doc_base = nav.base_uri();
if !doc_base.is_empty() {
xml_bases.push(doc_base.to_string());
}
break;
}
}
let mut base = static_base_uri.map(|s| s.to_string());
for uri in xml_bases.into_iter().rev() {
base = Some(resolve_uri(&uri, base.as_deref()));
}
base
}
fn get_xml_base_attr<N: DomNavigator>(nav: &N) -> Option<String> {
let mut attr_nav = nav.clone();
if attr_nav.move_to_first_attribute() {
loop {
if attr_nav.local_name() == "base"
&& attr_nav.namespace_uri() == "http://www.w3.org/XML/1998/namespace"
{
return Some(attr_nav.value());
}
if !attr_nav.move_to_next_attribute() {
break;
}
}
}
None
}
fn resolve_uri(uri: &str, base: Option<&str>) -> String {
if uri.contains("://") || uri.starts_with("file:") {
return uri.to_string();
}
match base {
None => uri.to_string(),
Some(base_uri) => {
if uri.is_empty() {
return base_uri.to_string();
}
if uri.starts_with('/') {
if let Some(scheme_end) = base_uri.find("://") {
if let Some(path_start) = base_uri[scheme_end + 3..].find('/') {
let host_end = scheme_end + 3 + path_start;
return format!("{}{}", &base_uri[..host_end], uri);
}
}
uri.to_string()
} else {
if let Some(last_slash) = base_uri.rfind('/') {
format!("{}/{}", &base_uri[..last_slash], uri)
} else {
uri.to_string()
}
}
}
}
}
fn find_xml_lang<N: DomNavigator>(node: &N) -> Option<String> {
let mut nav = node.clone();
loop {
if nav.node_type() == DomNodeType::Element {
let mut attr_nav = nav.clone();
if attr_nav.move_to_first_attribute() {
loop {
if attr_nav.local_name() == "lang"
&& attr_nav.namespace_uri() == "http://www.w3.org/XML/1998/namespace"
{
return Some(attr_nav.value());
}
if !attr_nav.move_to_next_attribute() {
break;
}
}
}
}
if !nav.move_to_parent() {
break;
}
}
None
}
fn lang_matches(lang: &str, test_lang: &str) -> bool {
let lang_lower = lang.to_lowercase();
let test_lower = test_lang.to_lowercase();
if lang_lower == test_lower {
return true;
}
if lang_lower.starts_with(&test_lower) {
let remainder = &lang_lower[test_lower.len()..];
if remainder.starts_with('-') {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lang_matches_exact() {
assert!(lang_matches("en", "en"));
assert!(lang_matches("EN", "en"));
assert!(lang_matches("en", "EN"));
}
#[test]
fn test_lang_matches_subtag() {
assert!(lang_matches("en-US", "en"));
assert!(lang_matches("en-GB", "en"));
assert!(lang_matches("zh-Hans-CN", "zh"));
}
#[test]
fn test_lang_matches_no_match() {
assert!(!lang_matches("de", "en"));
assert!(!lang_matches("english", "en"));
assert!(!lang_matches("en", "en-US"));
}
#[test]
fn test_any_uri_creation() {
let uri = any_uri("http://example.com");
assert_eq!(uri.type_code, XmlTypeCode::AnyUri);
}
#[test]
fn test_lang_matches_empty_testlang() {
assert!(!lang_matches("en", ""));
assert!(!lang_matches("en-US", ""));
}
#[test]
fn test_resolve_uri_absolute() {
assert_eq!(
resolve_uri("http://example.com/path", Some("http://other.com/")),
"http://example.com/path"
);
}
#[test]
fn test_resolve_uri_relative() {
assert_eq!(
resolve_uri("file.xml", Some("http://example.com/dir/base.xml")),
"http://example.com/dir/file.xml"
);
}
#[test]
fn test_resolve_uri_absolute_path() {
assert_eq!(
resolve_uri(
"/absolute/path.xml",
Some("http://example.com/dir/base.xml")
),
"http://example.com/absolute/path.xml"
);
}
#[test]
fn test_resolve_uri_no_base() {
assert_eq!(resolve_uri("relative.xml", None), "relative.xml");
}
#[test]
fn test_resolve_uri_empty() {
assert_eq!(
resolve_uri("", Some("http://example.com/base.xml")),
"http://example.com/base.xml"
);
}
#[test]
fn test_collect_id_tokens_single_string() {
use crate::xpath::RoXmlNavigator;
let value: super::super::XPathValue<RoXmlNavigator<'static>> =
super::super::XPathValue::string("abc");
let tokens = collect_id_tokens(value);
assert_eq!(tokens, vec!["abc"]);
}
#[test]
fn test_collect_id_tokens_multi_whitespace() {
use crate::xpath::RoXmlNavigator;
let value: super::super::XPathValue<RoXmlNavigator<'static>> =
super::super::XPathValue::string(" foo bar baz ");
let tokens = collect_id_tokens(value);
assert_eq!(tokens, vec!["foo", "bar", "baz"]);
}
#[test]
fn test_collect_id_tokens_empty() {
use crate::xpath::RoXmlNavigator;
let value: super::super::XPathValue<RoXmlNavigator<'static>> =
super::super::XPathValue::Empty;
let tokens = collect_id_tokens(value);
assert!(tokens.is_empty());
}
#[test]
fn test_collect_id_tokens_sequence() {
use crate::types::value::XmlValue;
use crate::xpath::iterator::XmlItem;
use crate::xpath::RoXmlNavigator;
let items = vec![
XmlItem::Atomic(XmlValue::string("a1 a2")),
XmlItem::Atomic(XmlValue::string("b1")),
];
let value: super::super::XPathValue<RoXmlNavigator<'static>> =
super::super::XPathValue::from_sequence(items);
let tokens = collect_id_tokens(value);
assert_eq!(tokens, vec!["a1", "a2", "b1"]);
}
#[test]
fn test_fn_id_empty_without_dtd() {
use crate::namespace::table::NameTable;
use crate::xpath::context::{DynamicContext, XPathContext};
use crate::xpath::RoXmlNavigator;
let doc = roxmltree::Document::parse("<root><a id='x'/></root>").unwrap();
let nav = RoXmlNavigator::new(&doc);
let names = NameTable::new();
let static_ctx = XPathContext::new(&names);
let mut dyn_ctx = DynamicContext::new(&static_ctx, 0);
dyn_ctx.context_item = Some(XmlItem::Node(nav));
let args = vec![super::super::XPathValue::string("x")];
let result = id(&mut dyn_ctx, args).unwrap();
assert!(
matches!(result, super::super::XPathValue::Empty),
"Expected empty sequence from fn:id without DTD"
);
}
}