307 lines
8.8 KiB
Rust
307 lines
8.8 KiB
Rust
//! Document structure and node implementation for the Terrace language.
|
|
|
|
use crate::parser::{LineData, create_line_data, parse_line};
|
|
use crate::readers::Reader;
|
|
use std::collections::VecDeque;
|
|
|
|
/// Represents a single node/line in a Terrace document.
|
|
#[derive(Debug, Clone)]
|
|
pub struct TerraceNode {
|
|
line_data: LineData,
|
|
content: String,
|
|
line_number: usize,
|
|
document: *const TerraceDocument, // Raw pointer to avoid circular reference
|
|
}
|
|
|
|
impl TerraceNode {
|
|
/// Create a new TerraceNode.
|
|
fn new(line_data: LineData, content: String, line_number: usize, document: *const TerraceDocument) -> Self {
|
|
Self {
|
|
line_data,
|
|
content,
|
|
line_number,
|
|
document,
|
|
}
|
|
}
|
|
|
|
/// Get the head of the node (the first word before any space).
|
|
pub fn head(&self) -> &str {
|
|
&self.content[self.line_data.offset_head..self.line_data.offset_tail]
|
|
}
|
|
|
|
/// Get the tail of the node (everything after the first space).
|
|
pub fn tail(&self) -> &str {
|
|
if self.line_data.offset_tail + 1 >= self.content.len() {
|
|
""
|
|
} else {
|
|
&self.content[self.line_data.offset_tail + 1..]
|
|
}
|
|
}
|
|
|
|
/// Get the content of the node (everything after the indentation).
|
|
pub fn content(&self) -> &str {
|
|
&self.content[self.line_data.offset_head..]
|
|
}
|
|
|
|
/// Get the indentation level of the node.
|
|
pub fn level(&self) -> usize {
|
|
self.line_data.level
|
|
}
|
|
|
|
/// Get the line number of the node.
|
|
pub fn line_number(&self) -> usize {
|
|
self.line_number
|
|
}
|
|
|
|
/// Check if the node's head matches the given value.
|
|
pub fn is(&self, value: &str) -> bool {
|
|
self.head() == value
|
|
}
|
|
|
|
/// Check if the node is empty (contains only whitespace).
|
|
pub fn is_empty(&self) -> bool {
|
|
self.content.trim().is_empty()
|
|
}
|
|
|
|
/// Get the raw content starting from the given offset.
|
|
pub fn raw(&self, offset: Option<usize>) -> &str {
|
|
&self.content[offset.unwrap_or(0)..]
|
|
}
|
|
|
|
/// Get an iterator over the children of this node.
|
|
pub fn children(&self) -> TerraceNodeChildrenIterator {
|
|
TerraceNodeChildrenIterator::new(self.document, self.level())
|
|
}
|
|
|
|
/// Get an iterator over the siblings of this node.
|
|
pub fn siblings(&self) -> TerraceNodeSiblingsIterator {
|
|
TerraceNodeSiblingsIterator::new(self.document, self.level())
|
|
}
|
|
}
|
|
|
|
/// Iterator for children of a TerraceNode.
|
|
pub struct TerraceNodeChildrenIterator {
|
|
document: *const TerraceDocument,
|
|
parent_level: usize,
|
|
}
|
|
|
|
impl TerraceNodeChildrenIterator {
|
|
fn new(document: *const TerraceDocument, parent_level: usize) -> Self {
|
|
Self {
|
|
document,
|
|
parent_level,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Iterator for TerraceNodeChildrenIterator {
|
|
type Item = TerraceNode;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
// This is a simplified implementation - in a real async context,
|
|
// we'd need to handle the async nature properly
|
|
// For now, this is a placeholder that would need to be implemented
|
|
// with proper async iteration
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Iterator for siblings of a TerraceNode.
|
|
pub struct TerraceNodeSiblingsIterator {
|
|
document: *const TerraceDocument,
|
|
current_level: usize,
|
|
}
|
|
|
|
impl TerraceNodeSiblingsIterator {
|
|
fn new(document: *const TerraceDocument, current_level: usize) -> Self {
|
|
Self {
|
|
document,
|
|
current_level,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Iterator for TerraceNodeSiblingsIterator {
|
|
type Item = TerraceNode;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
// This is a simplified implementation - in a real async context,
|
|
// we'd need to handle the async nature properly
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Main document iterator for Terrace documents.
|
|
pub struct TerraceDocument {
|
|
reader: Box<dyn Reader + Send + Sync>,
|
|
indent: char,
|
|
line_data: LineData,
|
|
current_line_number: usize,
|
|
pushed_back_nodes: VecDeque<TerraceNode>,
|
|
is_exhausted: bool,
|
|
}
|
|
|
|
impl TerraceDocument {
|
|
/// Create a new TerraceDocument with the given reader.
|
|
///
|
|
/// # Arguments
|
|
/// * `reader` - The reader to read lines from
|
|
/// * `indent` - The character used for indentation (default: space)
|
|
pub fn new<R: Reader + Send + Sync + 'static>(reader: R, indent: char) -> Self {
|
|
Self {
|
|
reader: Box::new(reader),
|
|
indent,
|
|
line_data: create_line_data(indent),
|
|
current_line_number: 0,
|
|
pushed_back_nodes: VecDeque::new(),
|
|
is_exhausted: false,
|
|
}
|
|
}
|
|
|
|
/// Create a new TerraceDocument with default space indentation.
|
|
pub fn with_reader<R: Reader + Send + Sync + 'static>(reader: R) -> Self {
|
|
Self::new(reader, ' ')
|
|
}
|
|
|
|
/// Get the next node from the document.
|
|
pub async fn next(&mut self) -> Option<TerraceNode> {
|
|
// Check for pushed back nodes first (LIFO order)
|
|
if let Some(node) = self.pushed_back_nodes.pop_back() {
|
|
return Some(node);
|
|
}
|
|
|
|
// If we've exhausted the reader, return None
|
|
if self.is_exhausted {
|
|
return None;
|
|
}
|
|
|
|
let line = match self.reader.read_line().await {
|
|
Ok(Some(line)) => line,
|
|
Ok(None) => {
|
|
self.is_exhausted = true;
|
|
return None;
|
|
}
|
|
Err(_) => return None, // In real implementation, should handle errors properly
|
|
};
|
|
|
|
self.current_line_number += 1;
|
|
parse_line(&line, &mut self.line_data);
|
|
|
|
Some(TerraceNode::new(
|
|
self.line_data.clone(),
|
|
line,
|
|
self.current_line_number,
|
|
self as *const Self,
|
|
))
|
|
}
|
|
|
|
/// Push a node back to be returned on the next call to next().
|
|
fn push_back(&mut self, node: TerraceNode) {
|
|
self.pushed_back_nodes.push_back(node);
|
|
}
|
|
|
|
/// Collect all nodes into a vector.
|
|
pub async fn collect(mut self) -> Vec<TerraceNode> {
|
|
let mut nodes = Vec::new();
|
|
while let Some(node) = self.next().await {
|
|
nodes.push(node);
|
|
}
|
|
nodes
|
|
}
|
|
|
|
/// Filter nodes based on a predicate.
|
|
pub async fn filter<F>(mut self, mut predicate: F) -> Vec<TerraceNode>
|
|
where
|
|
F: FnMut(&TerraceNode) -> bool,
|
|
{
|
|
let mut results = Vec::new();
|
|
while let Some(node) = self.next().await {
|
|
if predicate(&node) {
|
|
results.push(node);
|
|
}
|
|
}
|
|
results
|
|
}
|
|
|
|
/// Find the first node that matches the predicate.
|
|
pub async fn find<F>(mut self, mut predicate: F) -> Option<TerraceNode>
|
|
where
|
|
F: FnMut(&TerraceNode) -> bool,
|
|
{
|
|
while let Some(node) = self.next().await {
|
|
if predicate(&node) {
|
|
return Some(node);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Map nodes using a mapper function.
|
|
pub async fn map<F, T>(mut self, mut mapper: F) -> Vec<T>
|
|
where
|
|
F: FnMut(TerraceNode) -> T,
|
|
{
|
|
let mut results = Vec::new();
|
|
while let Some(node) = self.next().await {
|
|
results.push(mapper(node));
|
|
}
|
|
results
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::readers::StringReader;
|
|
|
|
#[tokio::test]
|
|
async fn test_document_iteration() {
|
|
let content = "hello\n world\n world\nhello again\n terrace";
|
|
let reader = StringReader::new(content);
|
|
let mut doc = TerraceDocument::with_reader(reader);
|
|
|
|
let nodes = doc.collect().await;
|
|
assert_eq!(nodes.len(), 5);
|
|
|
|
assert_eq!(nodes[0].head(), "hello");
|
|
assert_eq!(nodes[0].level(), 0);
|
|
|
|
assert_eq!(nodes[1].head(), "world");
|
|
assert_eq!(nodes[1].level(), 1);
|
|
|
|
assert_eq!(nodes[2].head(), "world");
|
|
assert_eq!(nodes[2].level(), 1);
|
|
|
|
assert_eq!(nodes[3].head(), "hello");
|
|
assert_eq!(nodes[3].level(), 0);
|
|
|
|
assert_eq!(nodes[4].head(), "terrace");
|
|
assert_eq!(nodes[4].level(), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_node_properties() {
|
|
let content = " config database localhost";
|
|
let reader = StringReader::new(content);
|
|
let mut doc = TerraceDocument::with_reader(reader);
|
|
|
|
let node = doc.next().await.unwrap();
|
|
assert_eq!(node.level(), 2);
|
|
assert_eq!(node.head(), "config");
|
|
assert_eq!(node.tail(), "database localhost");
|
|
assert_eq!(node.content(), "config database localhost");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_filter_nodes() {
|
|
let content = "config\n database\n server\n database";
|
|
let reader = StringReader::new(content);
|
|
let doc = TerraceDocument::with_reader(reader);
|
|
|
|
let filtered = doc.filter(|node| node.head() == "database").await;
|
|
assert_eq!(filtered.len(), 2);
|
|
assert_eq!(filtered[0].level(), 1);
|
|
assert_eq!(filtered[1].level(), 1);
|
|
}
|
|
}
|