use std::fmt;
pub type NodeId = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TextStyle {
pub bold: bool,
pub italic: bool,
pub code: bool,
pub strikethrough: bool,
pub underline: bool,
pub highlight: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
Left,
Center,
Right,
}
#[derive(Debug, Clone, PartialEq)]
pub enum NodeType {
Document,
Paragraph,
Heading {
level: u8,
}, CodeBlock {
language: Option<String>,
fence_info: String,
},
BlockQuote,
List {
ordered: bool,
start: u64,
},
ListItem {
checkbox: Option<bool>,
},
ThematicBreak,
Text {
content: String,
style: TextStyle,
},
SoftBreak,
HardBreak,
Link {
destination: String,
title: Option<String>,
},
Image {
destination: String,
title: Option<String>,
},
Code {
content: String,
},
WikiLink {
destination: String,
}, Table,
TableHead,
TableRow,
TableCell {
alignment: Option<Alignment>,
},
}
impl NodeType {
pub fn is_block(&self) -> bool {
matches!(
self,
NodeType::Document
| NodeType::Paragraph
| NodeType::Heading { .. }
| NodeType::CodeBlock { .. }
| NodeType::BlockQuote
| NodeType::List { .. }
| NodeType::ListItem { .. }
| NodeType::ThematicBreak
| NodeType::Table
| NodeType::TableHead
| NodeType::TableRow
)
}
pub fn is_inline(&self) -> bool {
matches!(
self,
NodeType::Text { .. }
| NodeType::SoftBreak
| NodeType::HardBreak
| NodeType::Link { .. }
| NodeType::Image { .. }
| NodeType::Code { .. }
| NodeType::WikiLink { .. }
)
}
pub fn can_have_children(&self) -> bool {
!matches!(
self,
NodeType::Text { .. }
| NodeType::SoftBreak
| NodeType::HardBreak
| NodeType::Code { .. }
| NodeType::ThematicBreak
)
}
}
#[derive(Debug, Clone)]
pub struct ComputedStyle {
pub font_face: u8, pub font_size: u8, pub color: u32, pub bgcolor: Option<u32>, pub underline: bool,
pub line_height: f32, }
impl Default for ComputedStyle {
fn default() -> Self {
ComputedStyle {
font_face: 0,
font_size: 14,
color: 0x000000FF,
bgcolor: None,
underline: false,
line_height: 1.2,
}
}
}
#[derive(Debug, Clone)]
pub struct ASTNode {
pub id: NodeId,
pub node_type: NodeType,
pub char_start: usize,
pub char_end: usize,
pub children: Vec<ASTNode>,
pub parent_id: Option<NodeId>,
pub style: ComputedStyle,
pub dirty: bool, }
impl ASTNode {
pub fn new(id: NodeId, node_type: NodeType, char_start: usize, char_end: usize) -> Self {
let style = Self::compute_style_for_type(&node_type);
ASTNode {
id,
node_type,
char_start,
char_end,
children: Vec::new(),
parent_id: None,
style,
dirty: true,
}
}
fn compute_style_for_type(node_type: &NodeType) -> ComputedStyle {
let mut style = ComputedStyle::default();
match node_type {
NodeType::Heading { level } => {
style.font_face = 1; style.font_size = match level {
1 => 20,
2 => 18,
3 => 16,
_ => 14,
};
}
NodeType::Code { .. } | NodeType::CodeBlock { .. } => {
style.font_face = 4; style.color = 0x006400FF; style.bgcolor = Some(0xF5F5F5FF); }
NodeType::Link { .. } | NodeType::WikiLink { .. } => {
style.color = 0x0000FFFF; style.underline = true;
}
NodeType::BlockQuote => {
style.font_face = 3; style.color = 0x640000FF; }
NodeType::Text {
style: text_style, ..
} => {
if text_style.bold && text_style.italic {
style.font_face = 3; } else if text_style.bold {
style.font_face = 1; } else if text_style.italic {
style.font_face = 2; }
if text_style.code {
style.font_face = 4; style.color = 0x006400FF; }
}
_ => {}
}
style
}
pub fn add_child(&mut self, mut child: ASTNode) {
child.parent_id = Some(self.id);
self.children.push(child);
}
pub fn text_content(&self) -> Option<&str> {
match &self.node_type {
NodeType::Text { content, .. } => Some(content),
NodeType::Code { content } => Some(content),
_ => None,
}
}
pub fn flatten_text(&self) -> String {
let mut result = String::new();
self.flatten_text_recursive(&mut result);
result
}
fn flatten_text_recursive(&self, buffer: &mut String) {
match &self.node_type {
NodeType::Text { content, .. } => {
buffer.push_str(content);
}
NodeType::Code { content } => {
buffer.push_str(content);
}
NodeType::SoftBreak => {
buffer.push(' ');
}
NodeType::HardBreak => {
buffer.push('\n');
}
_ => {
for child in &self.children {
child.flatten_text_recursive(buffer);
}
}
}
}
pub fn contains_position(&self, pos: usize) -> bool {
pos >= self.char_start && pos < self.char_end
}
pub fn adjust_positions(&mut self, from_pos: usize, delta: isize) {
if self.char_start >= from_pos {
self.char_start = (self.char_start as isize + delta).max(0) as usize;
}
if self.char_end >= from_pos {
self.char_end = (self.char_end as isize + delta).max(0) as usize;
}
for child in &mut self.children {
child.adjust_positions(from_pos, delta);
}
}
pub fn mark_dirty(&mut self) {
self.dirty = true;
}
}
impl fmt::Display for ASTNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_recursive(f, 0)
}
}
impl ASTNode {
fn fmt_recursive(&self, f: &mut fmt::Formatter<'_>, indent: usize) -> fmt::Result {
let indent_str = " ".repeat(indent);
write!(f, "{}", indent_str)?;
match &self.node_type {
NodeType::Document => writeln!(f, "Document [{}-{}]", self.char_start, self.char_end)?,
NodeType::Paragraph => {
writeln!(f, "Paragraph [{}-{}]", self.char_start, self.char_end)?
}
NodeType::Heading { level } => writeln!(
f,
"Heading(h{}) [{}-{}]",
level, self.char_start, self.char_end
)?,
NodeType::Text { content, style } => writeln!(
f,
"Text({:?}): {:?} [{}-{}]",
style, content, self.char_start, self.char_end
)?,
NodeType::Link { destination, .. } => writeln!(
f,
"Link -> {:?} [{}-{}]",
destination, self.char_start, self.char_end
)?,
NodeType::CodeBlock { language, .. } => writeln!(
f,
"CodeBlock({:?}) [{}-{}]",
language, self.char_start, self.char_end
)?,
NodeType::List { ordered, .. } => writeln!(
f,
"List({}) [{}-{}]",
if *ordered { "ordered" } else { "unordered" },
self.char_start,
self.char_end
)?,
_ => writeln!(
f,
"{:?} [{}-{}]",
self.node_type, self.char_start, self.char_end
)?,
}
for child in &self.children {
child.fmt_recursive(f, indent + 1)?;
}
Ok(())
}
}
pub struct Document {
pub root: ASTNode,
pub source: String,
next_id: NodeId,
}
impl Document {
pub fn new() -> Self {
let root = ASTNode::new(0, NodeType::Document, 0, 0);
Document {
root,
source: String::new(),
next_id: 1,
}
}
pub fn next_id(&mut self) -> NodeId {
let id = self.next_id;
self.next_id += 1;
id
}
pub fn find_node(&self, id: NodeId) -> Option<&ASTNode> {
Self::find_node_recursive(&self.root, id)
}
fn find_node_recursive(node: &ASTNode, id: NodeId) -> Option<&ASTNode> {
if node.id == id {
return Some(node);
}
for child in &node.children {
if let Some(found) = Self::find_node_recursive(child, id) {
return Some(found);
}
}
None
}
pub fn find_node_at_position(&self, pos: usize) -> Option<&ASTNode> {
Self::find_node_at_position_recursive(&self.root, pos)
}
fn find_node_at_position_recursive(node: &ASTNode, pos: usize) -> Option<&ASTNode> {
if !node.contains_position(pos) {
return None;
}
for child in &node.children {
if let Some(found) = Self::find_node_at_position_recursive(child, pos) {
return Some(found);
}
}
Some(node)
}
pub fn to_text(&self) -> String {
self.root.flatten_text()
}
pub fn update_source(&mut self, new_source: String, changed_start: usize, changed_end: usize) {
let old_len = self.source.len();
self.source = new_source;
let new_len = self.source.len();
let delta = new_len as isize - old_len as isize;
self.root.adjust_positions(changed_end, delta);
self.mark_region_dirty(changed_start, changed_end);
}
fn mark_region_dirty(&mut self, start: usize, end: usize) {
Self::mark_node_dirty_recursive(&mut self.root, start, end);
}
fn mark_node_dirty_recursive(node: &mut ASTNode, start: usize, end: usize) {
if node.char_end >= start && node.char_start <= end {
node.mark_dirty();
for child in &mut node.children {
Self::mark_node_dirty_recursive(child, start, end);
}
}
}
}
impl Default for Document {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for Document {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Document (source: {} bytes)", self.source.len())?;
write!(f, "{}", self.root)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_creation() {
let node = ASTNode::new(0, NodeType::Paragraph, 0, 10);
assert_eq!(node.id, 0);
assert_eq!(node.char_start, 0);
assert_eq!(node.char_end, 10);
assert!(node.dirty);
}
#[test]
fn test_add_child() {
let mut parent = ASTNode::new(0, NodeType::Paragraph, 0, 10);
let child = ASTNode::new(
1,
NodeType::Text {
content: "hello".to_string(),
style: TextStyle::default(),
},
0,
5,
);
parent.add_child(child);
assert_eq!(parent.children.len(), 1);
assert_eq!(parent.children[0].parent_id, Some(0));
}
#[test]
fn test_flatten_text() {
let mut para = ASTNode::new(0, NodeType::Paragraph, 0, 15);
para.add_child(ASTNode::new(
1,
NodeType::Text {
content: "hello".to_string(),
style: TextStyle::default(),
},
0,
5,
));
para.add_child(ASTNode::new(
2,
NodeType::Text {
content: " world".to_string(),
style: TextStyle::default(),
},
5,
11,
));
assert_eq!(para.flatten_text(), "hello world");
}
#[test]
fn test_contains_position() {
let node = ASTNode::new(0, NodeType::Paragraph, 10, 20);
assert!(!node.contains_position(5));
assert!(node.contains_position(10));
assert!(node.contains_position(15));
assert!(!node.contains_position(20));
}
#[test]
fn test_adjust_positions() {
let mut node = ASTNode::new(0, NodeType::Paragraph, 10, 20);
let child = ASTNode::new(
1,
NodeType::Text {
content: "test".to_string(),
style: TextStyle::default(),
},
10,
14,
);
node.add_child(child);
node.adjust_positions(5, 3);
assert_eq!(node.char_start, 13);
assert_eq!(node.char_end, 23);
assert_eq!(node.children[0].char_start, 13);
assert_eq!(node.children[0].char_end, 17);
}
#[test]
fn test_document_find_node_at_position() {
let mut doc = Document::new();
doc.root.char_end = 10;
let mut para = ASTNode::new(doc.next_id(), NodeType::Paragraph, 0, 10);
para.add_child(ASTNode::new(
doc.next_id(),
NodeType::Text {
content: "hello".to_string(),
style: TextStyle::default(),
},
0,
5,
));
doc.root.add_child(para);
let node = doc.find_node_at_position(3);
assert!(node.is_some());
assert!(matches!(node.unwrap().node_type, NodeType::Text { .. }));
}
}