xee_interpreter/xml/
document.rs

1use std::sync::atomic;
2
3use ahash::{HashMap, HashMapExt};
4use iri_string::types::{IriStr, IriString};
5use xot::Xot;
6
7use super::Annotations;
8
9static DOCUMENTS_COUNTER: atomic::AtomicUsize = atomic::AtomicUsize::new(0);
10
11fn get_documents_id() -> usize {
12    DOCUMENTS_COUNTER.fetch_add(1, atomic::Ordering::Relaxed)
13}
14
15/// Something went wrong loading [`Documents`]
16#[derive(Debug)]
17pub enum DocumentsError {
18    /// An attempt as made to add a document with a URI that was already known.
19    DuplicateUri(String),
20    /// An error occurred loading the document XML (using the [`xot`] crate).
21    Parse(xot::ParseError),
22}
23
24impl std::error::Error for DocumentsError {}
25
26impl std::fmt::Display for DocumentsError {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            DocumentsError::DuplicateUri(uri) => write!(f, "Duplicate URI: {}", uri),
30            DocumentsError::Parse(e) => write!(f, "Parse error: {}", e),
31        }
32    }
33}
34
35impl From<xot::ParseError> for DocumentsError {
36    fn from(e: xot::ParseError) -> Self {
37        DocumentsError::Parse(e)
38    }
39}
40
41#[derive(Debug, Clone)]
42pub struct Document {
43    pub(crate) uri: Option<IriString>,
44    root: xot::Node,
45}
46
47impl Document {
48    /// The document root node
49    pub fn root(&self) -> xot::Node {
50        self.root
51    }
52
53    pub(crate) fn cleanup(&self, xot: &mut Xot) {
54        xot.remove(self.root).unwrap();
55    }
56}
57
58/// A collection of XML documents as can be used by XPath and XSLT.
59///
60/// This collection can be prepared before any XPath or XSLT processing begins.
61///
62/// Alternatively this collection can be added to incrementally during
63/// processing using the `fn:doc` function for instance. Once a document under
64/// a URL is present, it cannot be changed anymore.
65///
66/// The `fn:parse-xml` and `fn:parse-xml-fragment` functions can be used to
67/// create new documents from strings without URLs.
68#[derive(Debug, Clone)]
69pub struct Documents {
70    id: usize,
71    annotations: Annotations,
72    documents: Vec<Document>,
73    by_uri: HashMap<IriString, DocumentHandle>,
74    uri_by_document_node: HashMap<xot::Node, IriString>,
75}
76
77/// A handle to a document.
78///
79/// This is an identifier into a [`Documents`] collection. You can
80/// freely copy it.
81#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
82pub struct DocumentHandle {
83    pub(crate) documents_id: usize,
84    pub(crate) id: usize,
85}
86
87impl Documents {
88    /// Create a new empty collection of documents.
89    pub fn new() -> Self {
90        Self {
91            id: get_documents_id(),
92            annotations: Annotations::new(),
93            documents: Vec::new(),
94            by_uri: HashMap::new(),
95            uri_by_document_node: HashMap::new(),
96        }
97    }
98
99    /// Clean up all documents.
100    pub fn cleanup(&mut self, xot: &mut Xot) {
101        for document in &self.documents {
102            document.cleanup(xot);
103        }
104        self.annotations.clear();
105        self.documents.clear();
106        self.by_uri.clear();
107    }
108
109    /// Add a string as an XML document. It can be designated with a URI.
110    pub fn add_string(
111        &mut self,
112        xot: &mut Xot,
113        uri: Option<&IriStr>,
114        xml: &str,
115    ) -> Result<DocumentHandle, DocumentsError> {
116        let root = xot.parse(xml)?;
117        self.add_root(xot, uri, root)
118    }
119
120    /// Add a string as an XML fragment.
121    pub fn add_fragment_string(
122        &mut self,
123        xot: &mut Xot,
124        xml: &str,
125    ) -> Result<DocumentHandle, DocumentsError> {
126        let root = xot.parse_fragment(xml)?;
127        self.add_root(xot, None, root)
128    }
129
130    /// Add a root node of an XML document. Designate it with a URI.
131    pub fn add_root(
132        &mut self,
133        xot: &Xot,
134        uri: Option<&IriStr>,
135        root: xot::Node,
136    ) -> Result<DocumentHandle, DocumentsError> {
137        if let Some(uri) = uri {
138            if self.by_uri.contains_key(uri) {
139                // duplicate URI is an error
140                return Err(DocumentsError::DuplicateUri(uri.as_str().to_string()));
141            }
142        }
143
144        let id = self.documents.len();
145        let handle = DocumentHandle {
146            documents_id: self.id,
147            id,
148        };
149        self.documents.push(Document {
150            uri: uri.map(|uri| uri.to_owned()),
151            root,
152        });
153        if let Some(uri) = uri {
154            self.by_uri.insert(uri.to_owned(), handle);
155            self.uri_by_document_node.insert(root, uri.to_owned());
156        }
157        self.annotations.add(xot, root);
158
159        Ok(handle)
160    }
161
162    /// Obtain a document by handle
163    pub fn get_by_handle(&self, handle: DocumentHandle) -> Option<&Document> {
164        // only works if the handle is from this collection
165        if handle.documents_id != self.id {
166            return None;
167        }
168        self.documents.get(handle.id)
169    }
170
171    /// Obtain document node by handle
172    pub fn get_node_by_handle(&self, handle: DocumentHandle) -> Option<xot::Node> {
173        Some(self.get_by_handle(handle)?.root)
174    }
175
176    /// Obtain a document by URI
177    ///
178    /// It's only possible to obtain a document by URI if it was added with a URI.
179    pub fn get_by_uri(&self, uri: &IriStr) -> Option<&Document> {
180        let handle = self.by_uri.get(uri)?;
181        self.get_by_handle(*handle)
182    }
183
184    /// Obtain document node by URI
185    pub fn get_node_by_uri(&self, uri: &IriStr) -> Option<xot::Node> {
186        Some(self.get_by_uri(uri)?.root)
187    }
188
189    /// Obtain document URI by document node.
190    ///
191    /// This only returns a URI if the document was added with a URI.
192    pub fn get_uri_by_document_node(&self, node: xot::Node) -> Option<IriString> {
193        self.uri_by_document_node.get(&node).cloned()
194    }
195
196    /// How many documents are stored.
197    pub fn len(&self) -> usize {
198        self.documents.len()
199    }
200
201    /// Is the collection empty?
202    pub fn is_empty(&self) -> bool {
203        self.documents.is_empty()
204    }
205
206    /// Get the annotations object
207    pub(crate) fn annotations(&self) -> &Annotations {
208        &self.annotations
209    }
210}
211
212impl Default for Documents {
213    fn default() -> Self {
214        Self::new()
215    }
216}