use std::{borrow::Cow, collections::HashMap, iter::repeat};
use crate::{
XMLVersion,
tree::convert::NodeKind,
xpath::{XPathContext, XPathError, XPathNodeSet, XPathObject},
};
type XPathFunction = fn(&mut XPathContext, usize) -> Result<XPathObject, XPathError>;
pub(super) struct FunctionLibrary {
map: HashMap<Cow<'static, str>, XPathFunction>,
}
impl FunctionLibrary {
pub(super) fn get(&self, name: &str) -> Result<XPathFunction, XPathError> {
self.map
.get(name)
.copied()
.ok_or(XPathError::UnresolvableFunctionName)
}
}
impl Default for FunctionLibrary {
fn default() -> Self {
Self {
map: HashMap::from([
(Cow::Borrowed("last"), last as _),
(Cow::Borrowed("position"), position as _),
(Cow::Borrowed("count"), count as _),
(Cow::Borrowed("id"), id as _),
(Cow::Borrowed("local-name"), local_name as _),
(Cow::Borrowed("namespace-uri"), namespace_uri as _),
(Cow::Borrowed("name"), name as _),
(Cow::Borrowed("string"), string as _),
(Cow::Borrowed("concat"), concat as _),
(Cow::Borrowed("starts-with"), starts_with as _),
(Cow::Borrowed("contains"), contains as _),
(Cow::Borrowed("substring-before"), substring_before as _),
(Cow::Borrowed("substring-after"), substring_after as _),
(Cow::Borrowed("substring"), substring as _),
(Cow::Borrowed("string-length"), string_length as _),
(Cow::Borrowed("normalize-space"), normalize_space as _),
(Cow::Borrowed("translate"), translate as _),
(Cow::Borrowed("boolean"), boolean as _),
(Cow::Borrowed("not"), not as _),
(Cow::Borrowed("true"), r#true as _),
(Cow::Borrowed("false"), r#false as _),
(Cow::Borrowed("lang"), lang as _),
(Cow::Borrowed("number"), number as _),
(Cow::Borrowed("sum"), sum as _),
(Cow::Borrowed("floor"), floor as _),
(Cow::Borrowed("ceiling"), ceiling as _),
(Cow::Borrowed("round"), round as _),
]),
}
}
}
fn last(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 0 {
return Err(XPathError::IncorrectNumberOfArgument);
}
Ok(context.size.into())
}
fn position(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 0 {
return Err(XPathError::IncorrectNumberOfArgument);
}
Ok(context.position.into())
}
fn count(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let XPathObject::NodeSet(node_set) = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
else {
return Err(XPathError::IncorrectArgumentType);
};
Ok(node_set.len().into())
}
fn id(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let object = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?;
let mut node_set = XPathNodeSet::default();
let document = context.node.clone().unwrap().owner_document();
match object {
XPathObject::NodeSet(ns) => {
for node in ns.iter() {
let string = node.xpath_string_value();
for id in string
.split(|c: char| XMLVersion::default().is_whitespace(c))
.filter(|s| !s.is_empty())
{
if let Some(elem) = document.get_element_by_id(id) {
node_set.push(elem);
}
}
}
}
object => {
let string = object.as_string()?;
for id in string
.split(|c: char| XMLVersion::default().is_whitespace(c))
.filter(|s| !s.is_empty())
{
if let Some(elem) = document.get_element_by_id(id) {
node_set.push(elem);
}
}
}
}
node_set.sort();
Ok(node_set.into())
}
fn local_name(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args > 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let node = if num_args == 1 {
context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_nodeset()?
.first()
.cloned()
} else {
context.node.clone()
};
let Some(node) = node else {
return Ok("".into());
};
match node.downcast() {
NodeKind::Element(element) => Ok(element.local_name().into()),
NodeKind::Attribute(attribute) => Ok(attribute.local_name().into()),
NodeKind::Namespace(namespace) => {
Ok(namespace.prefix().as_deref().unwrap_or_default().into())
}
NodeKind::ProcessingInstruction(pi) => Ok(pi.target().into()),
_ => Ok("".into()),
}
}
fn namespace_uri(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args > 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let node = if num_args == 1 {
context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_nodeset()?
.first()
.cloned()
} else {
context.node.clone()
};
let Some(node) = node else {
return Ok("".into());
};
match node.downcast() {
NodeKind::Element(element) => Ok(element
.namespace_name()
.as_deref()
.unwrap_or_default()
.into()),
NodeKind::Attribute(attribute) => Ok(attribute
.namespace_name()
.as_deref()
.unwrap_or_default()
.into()),
_ => Ok("".into()),
}
}
fn name(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args > 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let node = if num_args == 1 {
let nodeset = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_nodeset()?;
nodeset.first().cloned()
} else {
context.node.clone()
};
let Some(node) = node else {
return Ok("".into());
};
match node.downcast() {
NodeKind::Element(element) => Ok(element.name().into()),
NodeKind::Attribute(attribute) => Ok(attribute.name().into()),
NodeKind::Namespace(namespace) => {
Ok(namespace.prefix().as_deref().unwrap_or_default().into())
}
NodeKind::ProcessingInstruction(pi) => Ok(pi.target().into()),
_ => Ok("".into()),
}
}
fn string(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args > 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
if num_args == 1 {
context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.cast_to_string()
} else {
Ok(context
.node
.as_ref()
.map(|node| node.xpath_string_value())
.unwrap_or_default()
.into())
}
}
fn concat(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args < 2 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let mut buf = vec![];
for _ in 0..num_args {
let string = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
buf.push(string);
}
Ok(buf.into_iter().rev().collect::<String>().into())
}
fn starts_with(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 2 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let second = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
let first = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
Ok(first.starts_with(second.as_ref()).into())
}
fn contains(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 2 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let second = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
let first = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
Ok(first.contains(second.as_ref()).into())
}
fn substring_before(
context: &mut XPathContext,
num_args: usize,
) -> Result<XPathObject, XPathError> {
if num_args != 2 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let second = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
let first = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
Ok(first
.split_once(second.as_ref())
.map(|ret| ret.0)
.unwrap_or("")
.into())
}
fn substring_after(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 2 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let second = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
let first = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
Ok(first
.split_once(second.as_ref())
.map(|ret| ret.1)
.unwrap_or("")
.into())
}
fn substring(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 2 && num_args != 3 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let third = if num_args == 3 {
context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_number()?
} else {
f64::INFINITY
};
let second = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_number()?;
let first = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
if second.is_nan() || second.is_infinite() || third.is_nan() || third.is_sign_negative() {
return Ok("".into());
}
let start = second.round();
if start > first.len() as f64 {
return Ok("".into());
}
let end = (start + third.round()).max(1.0);
let start = start.max(1.0);
let length = (end - start).min(first.len() as f64) as usize;
Ok(first
.chars()
.skip(start as usize - 1)
.take(length)
.collect::<Box<str>>()
.into())
}
fn string_length(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args > 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
if num_args == 1 {
let string = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
Ok(string.chars().count().into())
} else {
Ok(context
.node
.as_ref()
.map(|node| node.xpath_string_value().chars().count())
.unwrap_or_default()
.into())
}
}
fn normalize_space(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args > 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let string = if num_args == 1 {
context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?
} else {
context
.node
.as_ref()
.map(|node| node.xpath_string_value())
.unwrap_or_default()
.into_boxed_str()
};
Ok(string
.split(|c| XMLVersion::default().is_whitespace(c))
.filter(|c| !c.is_empty())
.enumerate()
.fold(
String::new(),
|s, (i, v)| {
if i > 0 { s + " " + v } else { s + v }
},
)
.into())
}
fn translate(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 3 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let third = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
let second = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
let first = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
let mut replacement = second
.chars()
.zip(third.chars().map(Some).chain(repeat(None)))
.collect::<Vec<_>>();
replacement.sort_by_key(|v| v.0);
replacement.dedup_by_key(|v| v.0);
let ret = first
.chars()
.filter_map(|c| {
replacement
.binary_search_by_key(&c, |v| v.0)
.map(|pos| replacement[pos].1)
.unwrap_or(Some(c))
})
.collect::<Box<str>>();
Ok(ret.into())
}
fn boolean(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)
.and_then(|object| object.cast_to_boolean())
}
fn not(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let boolean = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_boolean()?;
Ok((!boolean).into())
}
fn r#true(_context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 0 {
return Err(XPathError::IncorrectNumberOfArgument);
}
Ok(true.into())
}
fn r#false(_context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 0 {
return Err(XPathError::IncorrectNumberOfArgument);
}
Ok(false.into())
}
fn lang(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let string = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_string()?;
let mut node = context.node.clone();
let mut ret = false;
while let Some(now) = node {
if let Some(elem) = now.as_element()
&& let Some(lang) = elem.language()
{
if let Some((lang, _)) = lang.split_once('-') {
ret = lang.eq_ignore_ascii_case(&string);
} else {
ret = lang.eq_ignore_ascii_case(&string);
}
break;
}
node = now.parent_node().map(From::from);
}
Ok(ret.into())
}
fn number(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args > 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
if num_args == 1 {
context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.cast_to_number()
} else {
string(context, 0).and_then(|obj| obj.cast_to_number())
}
}
fn sum(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let node_set = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_nodeset()
.map_err(|_| XPathError::IncorrectArgumentType)?;
let mut ret = 0.0;
for node in &node_set {
ret += XPathObject::String(node.xpath_string_value().into()).as_number()?;
}
Ok(ret.into())
}
fn floor(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let number = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_number()?;
Ok(number.floor().into())
}
fn ceiling(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let number = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_number()?;
Ok(number.ceil().into())
}
fn xpath_round(x: f64) -> f64 {
let y = x.floor();
if x == y {
y
} else {
(2. * x - y).floor().copysign(x)
}
}
fn round(context: &mut XPathContext, num_args: usize) -> Result<XPathObject, XPathError> {
if num_args != 1 {
return Err(XPathError::IncorrectNumberOfArgument);
}
let x = context
.pop_object()
.ok_or(XPathError::IncorrectNumberOfArgument)?
.as_number()?;
Ok(xpath_round(x).into())
}