use indextree::{Arena, NodeId};
use serde::Serialize;
#[derive(Debug, Clone)]
pub struct Document {
pub content: String,
pub headings: Vec<Heading>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Heading {
pub level: usize,
pub text: String,
#[serde(skip_serializing)]
pub offset: usize,
}
#[derive(Debug, Clone)]
pub struct HeadingNode {
pub heading: Heading,
pub children: Vec<HeadingNode>,
}
impl Document {
pub fn new(content: String, headings: Vec<Heading>) -> Self {
Self { content, headings }
}
pub fn build_tree(&self) -> Vec<HeadingNode> {
let mut arena = Arena::new();
let mut stack: Vec<(usize, NodeId)> = Vec::new();
let mut roots = Vec::new();
for heading in &self.headings {
let node_id = arena.new_node(heading.clone());
while let Some(&(parent_level, _)) = stack.last() {
if parent_level < heading.level {
break;
}
stack.pop();
}
if let Some(&(_, parent_id)) = stack.last() {
parent_id.append(node_id, &mut arena);
} else {
roots.push(node_id);
}
stack.push((heading.level, node_id));
}
roots
.into_iter()
.map(|root_id| build_heading_node(root_id, &arena))
.collect()
}
pub fn headings_at_level(&self, level: usize) -> Vec<&Heading> {
self.headings.iter().filter(|h| h.level == level).collect()
}
pub fn find_heading(&self, text: &str) -> Option<&Heading> {
let search = text.to_lowercase();
self.headings
.iter()
.find(|h| h.text.to_lowercase() == search)
}
pub fn filter_headings(&self, filter: &str) -> Vec<&Heading> {
let search = filter.to_lowercase();
self.headings
.iter()
.filter(|h| h.text.to_lowercase().contains(&search))
.collect()
}
pub fn extract_section(&self, heading_text: &str) -> Option<String> {
let heading_idx = self
.headings
.iter()
.position(|h| h.text.to_lowercase() == heading_text.to_lowercase())?;
let heading = &self.headings[heading_idx];
let start = heading.offset;
let after_heading = &self.content[start..];
let content_start = after_heading
.find('\n')
.map(|i| start + i + 1)
.unwrap_or(start);
let end = self
.headings
.iter()
.skip(heading_idx + 1)
.find(|h| h.level <= heading.level)
.map(|h| h.offset)
.unwrap_or(self.content.len());
Some(self.content[content_start..end].trim().to_string())
}
}
fn build_heading_node(node_id: NodeId, arena: &Arena<Heading>) -> HeadingNode {
let heading = arena[node_id].get().clone();
let children = node_id
.children(arena)
.map(|child_id| build_heading_node(child_id, arena))
.collect();
HeadingNode { heading, children }
}
impl HeadingNode {
pub fn render_box_tree(&self, prefix: &str, is_last: bool) -> String {
self.render_box_tree_styled(prefix, is_last, false)
}
pub fn render_box_tree_styled(&self, prefix: &str, is_last: bool, compact: bool) -> String {
let mut result = String::new();
let (connector, space, continuation) = if compact {
if is_last {
("└──", "", " ")
} else {
("├──", "", "│ ")
}
} else {
if is_last {
("└─ ", "", " ")
} else {
("├─ ", "", "│ ")
}
};
let marker = "#".repeat(self.heading.level);
result.push_str(&format!(
"{}{}{}{} {}\n",
prefix, connector, space, marker, self.heading.text
));
let child_prefix = format!("{}{}", prefix, continuation);
for (i, child) in self.children.iter().enumerate() {
let is_last_child = i == self.children.len() - 1;
result.push_str(&child.render_box_tree_styled(&child_prefix, is_last_child, compact));
}
result
}
}