mod ast;
mod eval;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
mod float;
mod parser;
pub use ast::{Axis, BinaryOp, Expr, NodeTest, Step};
pub use eval::Value;
pub use parser::XPathError;
use crate::tree::{Document, NodeId};
#[derive(Debug, Clone, PartialEq)]
pub struct XPath {
expr: Expr,
}
impl XPath {
pub fn compile(expr: &str) -> Result<Self, XPathError> {
Ok(Self {
expr: parser::compile(expr)?,
})
}
pub fn compile_with_namespaces(
expr: &str,
namespaces: &[(&str, &str)],
) -> Result<Self, XPathError> {
Ok(Self {
expr: parser::compile_with(expr, namespaces)?,
})
}
#[must_use]
pub fn evaluate(&self, doc: &Document) -> Value {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("oxml.xpath.evaluate").entered();
eval::evaluate(doc, &self.expr, doc.root())
}
#[must_use]
pub fn evaluate_from(&self, doc: &Document, context: NodeId) -> Value {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("oxml.xpath.evaluate_from").entered();
eval::evaluate(doc, &self.expr, context)
}
#[must_use]
pub const fn expr(&self) -> &Expr {
&self.expr
}
}
pub trait FromXPath: Sized {
fn from_value(value: &Value, doc: &Document) -> Result<Self, TypeError>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeError {
pub message: String,
}
impl core::fmt::Display for TypeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.message)
}
}
#[cfg(feature = "std")]
impl std::error::Error for TypeError {}
impl FromXPath for String {
fn from_value(value: &Value, doc: &Document) -> Result<Self, TypeError> {
Ok(value.to_str(doc))
}
}
impl FromXPath for f64 {
fn from_value(value: &Value, doc: &Document) -> Result<Self, TypeError> {
let n = value.to_number(doc);
if n.is_nan() {
return Err(TypeError {
message: format!("`{}` is not a number", value.to_str(doc)),
});
}
Ok(n)
}
}
impl FromXPath for i64 {
fn from_value(value: &Value, doc: &Document) -> Result<Self, TypeError> {
let n = f64::from_value(value, doc)?;
#[allow(clippy::float_cmp)]
let fractional = n != float::trunc(n);
if fractional {
return Err(TypeError {
message: format!("`{n}` is not an integer"),
});
}
if n.abs() > 9_007_199_254_740_992.0 {
return Err(TypeError {
message: format!(
"`{n}` is outside the exactly-representable range"
),
});
}
#[allow(clippy::cast_possible_truncation)]
Ok(n as Self)
}
}
impl FromXPath for bool {
fn from_value(value: &Value, doc: &Document) -> Result<Self, TypeError> {
let _ = doc;
Ok(value.to_boolean())
}
}
impl FromXPath for crate::NodeId {
fn from_value(value: &Value, doc: &Document) -> Result<Self, TypeError> {
let _ = doc;
match value.nodes() {
Some([first, ..]) => Ok(*first),
Some([]) => Err(TypeError {
message: String::from("the expression matched no nodes"),
}),
None => Err(TypeError {
message: String::from(
"the expression does not produce a node-set",
),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryError {
Compile(XPathError),
Type(TypeError),
}
impl core::fmt::Display for QueryError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Compile(e) => write!(f, "{e}"),
Self::Type(e) => write!(f, "{e}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for QueryError {}
impl Document {
pub fn xpath_one<T: FromXPath>(&self, expr: &str) -> Result<T, QueryError> {
let compiled = XPath::compile(expr).map_err(QueryError::Compile)?;
let value = compiled.evaluate(self);
T::from_value(&value, self).map_err(QueryError::Type)
}
pub fn xpath_all<T: FromXPath>(
&self,
expr: &str,
) -> Result<Vec<T>, QueryError> {
let compiled = XPath::compile(expr).map_err(QueryError::Compile)?;
let value = compiled.evaluate(self);
let Some(nodes) = value.nodes() else {
return Err(QueryError::Type(TypeError {
message: String::from(
"the expression does not produce a node-set",
),
}));
};
nodes
.iter()
.map(|id| {
let single = Value::NodeSet(alloc::vec![*id]);
T::from_value(&single, self).map_err(QueryError::Type)
})
.collect()
}
}