Skip to main content

dazzle_core/
grove.rs

1//! Grove trait definitions
2//!
3//! This module defines the abstract interface for document trees (groves),
4//! following OpenJade's grove/ architecture.
5//!
6//! The grove model is defined by the DSSSL standard (ISO/IEC 10179:1996) as an
7//! abstract representation of document structure, independent of markup syntax.
8//!
9//! ## Architecture
10//!
11//! Like OpenJade's separation between `grove/` (abstract interface) and `spgrove/`
12//! (OpenSP implementation), Dazzle defines:
13//!
14//! - **This module (`dazzle-core/grove`)**: Abstract traits (`Node`, `NodeList`, `Grove`)
15//! - **`dazzle-grove-libxml2`**: Concrete implementation using libxml2 (XML + DTD)
16//! - **`dazzle-grove-opensp` (future)**: Concrete implementation using OpenSP (full SGML)
17//!
18//! This allows the Scheme interpreter to work with any grove implementation
19//! without coupling to a specific parser.
20//!
21//! ## Key Traits
22//!
23//! - `Node`: A single node in the document tree (element, text, attribute, etc.)
24//! - `NodeList`: An ordered collection of nodes
25//! - `Grove`: The complete document grove (root + global operations)
26
27use std::any::Any;
28use std::fmt::Debug;
29
30/// A node in the document tree
31///
32/// Corresponds to OpenJade's `GroveImpl::NodeImpl` interface.
33///
34/// ## DSSSL Node Properties
35///
36/// DSSSL defines numerous node properties. The core ones are:
37///
38/// - **gi**: Generic identifier (element name)
39/// - **id**: ID attribute value
40/// - **data**: Text content (for text nodes)
41/// - **attributes**: Attribute nodes
42/// - **children**: Child nodes (elements only, not text)
43/// - **parent**: Parent node
44///
45/// See DSSSL spec Section 8 for complete property list.
46pub trait Node: Debug + Any {
47    /// Clone this node into a new Box
48    ///
49    /// This is required because trait objects (Box<dyn Node>) cannot implement Clone directly.
50    /// Each Node implementation must provide its own cloning logic.
51    fn clone_node(&self) -> Box<dyn Node>;
52
53    /// Get a unique node identifier for identity comparison
54    ///
55    /// Returns a value that uniquely identifies this node instance. Two nodes are
56    /// considered identical (not just equal) if they have the same node_ptr value.
57    /// This corresponds to OpenJade's pointer comparison for node identity.
58    ///
59    /// For libxml2 nodes, this is the address of the underlying xmlNode pointer.
60    fn node_ptr(&self) -> usize;
61
62    /// Get the generic identifier (element name)
63    ///
64    /// Returns `None` for non-element nodes.
65    ///
66    /// DSSSL: `gi` property
67    ///
68    /// **Design Note**: Returns owned `String` rather than `&str` because
69    /// XML parsers (libxml2) return owned strings. This avoids lifetime complexity.
70    fn gi(&self) -> Option<String>;
71
72    /// Get the ID attribute value
73    ///
74    /// Returns `None` if node has no ID attribute.
75    ///
76    /// DSSSL: `id` property
77    fn id(&self) -> Option<String>;
78
79    /// Get text content
80    ///
81    /// For text nodes, returns the text. For elements, returns
82    /// concatenated descendant text.
83    ///
84    /// DSSSL: `data` property
85    fn data(&self) -> Option<String>;
86
87    /// Get child nodes
88    ///
89    /// **Important**: In DSSSL, `children` returns only element nodes,
90    /// not text nodes. Text is accessed via `data` property.
91    ///
92    /// DSSSL: `children` property
93    fn children(&self) -> Box<dyn NodeList>;
94
95    /// Get ALL child nodes (including text nodes)
96    ///
97    /// This is an internal method used by `process-children` to iterate over
98    /// all child nodes including text. This is NOT the DSSSL `children` property,
99    /// which returns only element nodes.
100    ///
101    /// **Internal use only**: Used by evaluator's process_children implementation.
102    fn all_children(&self) -> Box<dyn NodeList>;
103
104    /// Get parent node
105    ///
106    /// Returns `None` for the root node.
107    ///
108    /// DSSSL: `parent` property
109    fn parent(&self) -> Option<Box<dyn Node>>;
110
111    /// Get attribute value
112    ///
113    /// Includes DTD default values if defined.
114    ///
115    /// DSSSL: `attribute-string` primitive
116    fn attribute_string(&self, name: &str) -> Option<String>;
117
118    /// Check if this is an element node
119    fn is_element(&self) -> bool;
120
121    /// Check if this is a text node
122    fn is_text(&self) -> bool;
123
124    /// Check node equality (same node in document tree)
125    ///
126    /// Two node references are equal if they refer to the same
127    /// node in the document, not just structurally similar nodes.
128    fn node_eq(&self, other: &dyn Node) -> bool;
129
130    /// Get a unique identifier for this node
131    ///
132    /// Returns a value that uniquely identifies this node within its document.
133    /// Two nodes with the same `node_id` are the same node.
134    fn node_id(&self) -> usize;
135}
136
137/// An ordered collection of nodes
138///
139/// Corresponds to OpenJade's `GroveImpl::NodeListImpl` interface.
140///
141/// ## Lazy Evaluation
142///
143/// Like OpenJade, node lists should support lazy evaluation - they don't
144/// need to materialize all nodes immediately. Operations like `first()`
145/// and `rest()` can be implemented efficiently as iterators.
146///
147/// DSSSL node lists are immutable and functional (cons-list style).
148pub trait NodeList: Debug {
149    /// Check if the node list is empty
150    ///
151    /// DSSSL: `node-list-empty?`
152    fn is_empty(&self) -> bool;
153
154    /// Get the first node
155    ///
156    /// Returns `None` if the list is empty.
157    ///
158    /// DSSSL: `node-list-first`
159    fn first(&self) -> Option<Box<dyn Node>>;
160
161    /// Get the rest of the node list (all but first)
162    ///
163    /// Returns an empty node list if this list has 0 or 1 elements.
164    ///
165    /// DSSSL: `node-list-rest`
166    fn rest(&self) -> Box<dyn NodeList>;
167
168    /// Get the length of the node list
169    ///
170    /// DSSSL: `node-list-length`
171    fn length(&self) -> usize;
172
173    /// Get node at index
174    ///
175    /// Returns `None` if index is out of bounds.
176    ///
177    /// DSSSL: `node-list-ref`
178    fn get(&self, index: usize) -> Option<Box<dyn Node>>;
179}
180
181/// The complete document grove
182///
183/// Corresponds to OpenJade's `GroveImpl::Grove` interface.
184///
185/// A grove holds the entire document tree and provides global operations
186/// like `element-with-id`.
187pub trait Grove: Debug {
188    /// Get the root node
189    ///
190    /// DSSSL: `grove-root` (or implicit root access)
191    fn root(&self) -> Box<dyn Node>;
192
193    /// Find element by ID
194    ///
195    /// Returns `None` if no element with the given ID exists.
196    ///
197    /// DSSSL: `element-with-id`
198    fn element_with_id(&self, id: &str) -> Option<Box<dyn Node>>;
199}
200
201/// An empty node list implementation
202///
203/// This is a concrete implementation of NodeList that represents
204/// an empty list. It's used by primitives like `empty-node-list`.
205#[derive(Debug, Clone)]
206pub struct EmptyNodeList;
207
208impl EmptyNodeList {
209    pub fn new() -> Self {
210        EmptyNodeList
211    }
212}
213
214impl Default for EmptyNodeList {
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220impl NodeList for EmptyNodeList {
221    fn is_empty(&self) -> bool {
222        true
223    }
224
225    fn first(&self) -> Option<Box<dyn Node>> {
226        None
227    }
228
229    fn rest(&self) -> Box<dyn NodeList> {
230        Box::new(EmptyNodeList::new())
231    }
232
233    fn length(&self) -> usize {
234        0
235    }
236
237    fn get(&self, _index: usize) -> Option<Box<dyn Node>> {
238        None
239    }
240}
241
242/// A node list backed by a vector with shared ownership
243///
244/// Used for creating filtered node lists (e.g., from select-elements).
245/// Uses Rc to share the vector without cloning nodes.
246#[derive(Debug)]
247pub struct VecNodeList {
248    nodes: std::rc::Rc<Vec<Box<dyn Node>>>,
249    offset: usize,
250}
251
252impl VecNodeList {
253    pub fn new(nodes: Vec<Box<dyn Node>>) -> Self {
254        VecNodeList {
255            nodes: std::rc::Rc::new(nodes),
256            offset: 0,
257        }
258    }
259
260    fn from_rc(nodes: std::rc::Rc<Vec<Box<dyn Node>>>, offset: usize) -> Self {
261        VecNodeList { nodes, offset }
262    }
263}
264
265impl NodeList for VecNodeList {
266    fn is_empty(&self) -> bool {
267        self.offset >= self.nodes.len()
268    }
269
270    fn first(&self) -> Option<Box<dyn Node>> {
271        self.nodes.get(self.offset).map(|n| n.clone_node())
272    }
273
274    fn rest(&self) -> Box<dyn NodeList> {
275        if self.offset + 1 >= self.nodes.len() {
276            Box::new(EmptyNodeList::new())
277        } else {
278            Box::new(VecNodeList::from_rc(self.nodes.clone(), self.offset + 1))
279        }
280    }
281
282    fn length(&self) -> usize {
283        self.nodes.len().saturating_sub(self.offset)
284    }
285
286    fn get(&self, index: usize) -> Option<Box<dyn Node>> {
287        self.nodes.get(self.offset + index).map(|n| n.clone_node())
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_traits_defined() {
297        // This test just ensures the traits compile
298        // Actual tests will be in dazzle-grove-libxml2
299    }
300
301    #[test]
302    fn test_empty_node_list() {
303        let empty = EmptyNodeList::new();
304        assert!(empty.is_empty());
305        assert_eq!(empty.length(), 0);
306        assert!(empty.first().is_none());
307        assert!(empty.get(0).is_none());
308        assert!(empty.rest().is_empty());
309    }
310}