use std::collections::{BTreeSet, HashMap};
use std::sync::Mutex;
use super::value::Value;
use super::version::XPathVersion;
use crate::xml::{Document, NodeId};
#[derive(Debug, Default)]
pub struct Documents {
loaded: HashMap<String, NodeId>,
missing: Mutex<BTreeSet<String>>,
}
impl Documents {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, uri: impl Into<String>, root: NodeId) {
self.loaded.insert(uri.into(), root);
}
#[must_use]
pub fn lookup(&self, uri: &str) -> Option<NodeId> {
if let Some(root) = self.loaded.get(uri) {
return Some(*root);
}
self.record_missing(uri);
None
}
fn record_missing(&self, uri: &str) {
if let Ok(mut missing) = self.missing.lock() {
missing.insert(uri.to_string());
}
}
#[must_use]
pub fn missing(&self) -> Vec<String> {
self.missing
.lock()
.map(|missing| missing.iter().cloned().collect())
.unwrap_or_default()
}
pub fn clear_missing(&self) {
if let Ok(mut missing) = self.missing.lock() {
missing.clear();
}
}
#[must_use]
pub fn len(&self) -> usize {
self.loaded.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.loaded.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct Namespaces {
bindings: HashMap<String, String>,
}
impl Namespaces {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, prefix: impl Into<String>, uri: impl Into<String>) {
self.bindings.insert(prefix.into(), uri.into());
}
#[must_use]
pub fn resolve(&self, prefix: &str) -> Option<&str> {
if prefix == "xml" {
return Some(crate::xml::XML_NAMESPACE);
}
self.bindings.get(prefix).map(String::as_str)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.bindings.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bindings.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct Variables {
entries: Vec<(String, Value)>,
}
impl Variables {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn bind(&mut self, name: impl Into<String>, value: Value) {
self.entries.push((name.into(), value));
}
#[must_use]
pub fn lookup(&self, name: &str) -> Option<&Value> {
self.entries
.iter()
.rev()
.find(|(n, _)| n == name)
.map(|(_, v)| v)
}
#[must_use]
pub fn mark(&self) -> usize {
self.entries.len()
}
pub fn truncate(&mut self, mark: usize) {
self.entries.truncate(mark);
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.entries.iter().map(|(n, _)| n.as_str())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub struct EvalContext<'a> {
pub document: &'a Document,
pub node: NodeId,
pub position: usize,
pub size: usize,
pub variables: &'a Variables,
pub namespaces: &'a Namespaces,
pub current: NodeId,
pub version: XPathVersion,
pub current_time: Option<f64>,
pub implicit_timezone: i32,
pub documents: Option<&'a Documents>,
}
impl<'a> EvalContext<'a> {
#[must_use]
pub fn new(
document: &'a Document,
node: NodeId,
variables: &'a Variables,
namespaces: &'a Namespaces,
) -> Self {
Self {
document,
node,
position: 1,
size: 1,
variables,
namespaces,
current: node,
version: XPathVersion::V1,
current_time: None,
implicit_timezone: 0,
documents: None,
}
}
#[must_use]
pub fn with_implicit_timezone(mut self, minutes: i32) -> Self {
self.implicit_timezone = minutes;
self
}
#[must_use]
pub fn with_current_time(mut self, seconds: f64) -> Self {
self.current_time = Some(seconds);
self
}
#[must_use]
pub fn with_version(mut self, version: XPathVersion) -> Self {
self.version = version;
self
}
#[must_use]
pub fn with_documents(mut self, documents: &'a Documents) -> Self {
self.documents = Some(documents);
self
}
#[must_use]
pub fn focus(&self, node: NodeId, position: usize, size: usize) -> Self {
Self {
node,
position,
size,
..*self
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inner_binding_shadows_outer() {
let mut vars = Variables::new();
vars.bind("x", Value::Number(1.0));
vars.bind("x", Value::Number(2.0));
assert_eq!(vars.lookup("x"), Some(&Value::Number(2.0)));
}
#[test]
fn truncate_restores_the_outer_scope() {
let mut vars = Variables::new();
vars.bind("x", Value::Number(1.0));
let mark = vars.mark();
vars.bind("x", Value::Number(2.0));
vars.truncate(mark);
assert_eq!(vars.lookup("x"), Some(&Value::Number(1.0)));
}
#[test]
fn unbound_lookup_returns_none() {
assert!(Variables::new().lookup("x").is_none());
}
#[test]
fn xml_prefix_is_bound_without_declaration() {
assert_eq!(
Namespaces::new().resolve("xml"),
Some(crate::xml::XML_NAMESPACE)
);
}
}