use serde::{Deserialize, Serialize};
use super::node::NodeId;
use super::node::TreeNode;
use super::tree::DocumentTree;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TocNode {
pub title: String,
pub node_id: Option<String>,
pub depth: usize,
pub page_range: Option<(usize, usize)>,
pub summary: Option<String>,
pub children: Vec<TocNode>,
}
impl TocNode {
pub fn new(title: impl Into<String>, depth: usize) -> Self {
Self {
title: title.into(),
node_id: None,
depth,
page_range: None,
summary: None,
children: Vec::new(),
}
}
pub fn with_node_id(mut self, id: impl Into<String>) -> Self {
self.node_id = Some(id.into());
self
}
pub fn with_page_range(mut self, start: usize, end: usize) -> Self {
self.page_range = Some((start, end));
self
}
pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
self.summary = Some(summary.into());
self
}
pub fn add_child(&mut self, child: TocNode) {
self.children.push(child);
}
pub fn count_nodes(&self) -> usize {
1 + self.children.iter().map(|c| c.count_nodes()).sum::<usize>()
}
pub fn count_leaves(&self) -> usize {
if self.children.is_empty() {
1
} else {
self.children.iter().map(|c| c.count_leaves()).sum()
}
}
pub fn max_depth(&self) -> usize {
if self.children.is_empty() {
self.depth
} else {
self.children
.iter()
.map(|c| c.max_depth())
.max()
.unwrap_or(self.depth)
}
}
}
#[derive(Debug, Clone)]
pub struct TocConfig {
pub max_depth: Option<usize>,
pub include_summaries: bool,
pub include_pages: bool,
pub min_content_length: usize,
}
impl Default for TocConfig {
fn default() -> Self {
Self {
max_depth: None,
include_summaries: true,
include_pages: true,
min_content_length: 0,
}
}
}
impl TocConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_depth(mut self, depth: usize) -> Self {
self.max_depth = Some(depth);
self
}
pub fn with_summaries(mut self, include: bool) -> Self {
self.include_summaries = include;
self
}
}
#[derive(Clone)]
pub struct TocView {
config: TocConfig,
}
impl TocView {
pub fn new() -> Self {
Self {
config: TocConfig::default(),
}
}
pub fn with_config(config: TocConfig) -> Self {
Self { config }
}
pub fn generate(&self, tree: &DocumentTree) -> TocNode {
self.build_toc_node(tree, tree.root(), 0)
}
pub fn generate_from(&self, tree: &DocumentTree, start: NodeId) -> TocNode {
let depth = tree.get(start).map_or(0, |n| n.depth);
self.build_toc_node(tree, start, depth)
}
fn build_toc_node(&self, tree: &DocumentTree, node_id: NodeId, depth: usize) -> TocNode {
let node = match tree.get(node_id) {
Some(n) => n,
None => return TocNode::new("Unknown", depth),
};
if let Some(max) = self.config.max_depth {
if depth > max {
return TocNode::new("...", depth - 1);
}
}
if node.content.len() < self.config.min_content_length && tree.children(node_id).is_empty()
{
return TocNode::new(node.title.clone(), depth);
}
let mut toc_node =
TocNode::new(&node.title, depth).with_node_id(node.node_id.clone().unwrap_or_default());
if self.config.include_pages {
if let (Some(start), Some(end)) = (node.start_page, node.end_page) {
toc_node = toc_node.with_page_range(start, end);
}
}
if self.config.include_summaries && !node.summary.is_empty() {
toc_node = toc_node.with_summary(&node.summary);
}
for child_id in tree.children(node_id) {
let child_toc = self.build_toc_node(tree, child_id, depth + 1);
toc_node.add_child(child_toc);
}
toc_node
}
pub fn generate_flat(&self, tree: &DocumentTree) -> Vec<TocEntry> {
let mut entries = Vec::new();
self.collect_flat_entries(tree, tree.root(), &mut entries);
entries
}
fn collect_flat_entries(
&self,
tree: &DocumentTree,
node_id: NodeId,
entries: &mut Vec<TocEntry>,
) {
if let Some(node) = tree.get(node_id) {
entries.push(TocEntry {
title: node.title.clone(),
node_id: node.node_id.clone(),
depth: node.depth,
page_range: node.start_page.zip(node.end_page),
});
for child_id in tree.children(node_id) {
self.collect_flat_entries(tree, child_id, entries);
}
}
}
pub fn generate_filtered<F>(&self, tree: &DocumentTree, filter: F) -> Vec<TocNode>
where
F: Fn(&TreeNode) -> bool,
{
let mut result = Vec::new();
self.collect_filtered(tree, tree.root(), &filter, &mut result);
result
}
fn collect_filtered<F>(
&self,
tree: &DocumentTree,
node_id: NodeId,
filter: &F,
result: &mut Vec<TocNode>,
) where
F: Fn(&TreeNode) -> bool,
{
if let Some(node) = tree.get(node_id) {
if filter(node) {
let toc_node = self.build_toc_node(tree, node_id, node.depth);
result.push(toc_node);
}
for child_id in tree.children(node_id) {
self.collect_filtered(tree, child_id, filter, result);
}
}
}
pub fn format_markdown(&self, toc: &TocNode) -> String {
let mut output = String::new();
self.write_markdown(toc, &mut output, 0);
output
}
fn write_markdown(&self, toc: &TocNode, output: &mut String, level: usize) {
let indent = " ".repeat(level);
let bullet = if level == 0 { "-" } else { "-" };
output.push_str(&format!("{}{} {}\n", indent, bullet, toc.title));
if let Some(ref summary) = toc.summary {
output.push_str(&format!("{} > {}\n", indent, summary));
}
for child in &toc.children {
self.write_markdown(child, output, level + 1);
}
}
pub fn format_json(&self, toc: &TocNode) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(toc)
}
}
impl Default for TocView {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TocEntry {
pub title: String,
pub node_id: Option<String>,
pub depth: usize,
pub page_range: Option<(usize, usize)>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_toc_node_creation() {
let mut root = TocNode::new("Root", 0);
let child = TocNode::new("Child", 1)
.with_node_id("node-1")
.with_summary("A child node");
root.add_child(child);
assert_eq!(root.count_nodes(), 2);
assert_eq!(root.count_leaves(), 1);
assert_eq!(root.max_depth(), 1);
}
#[test]
fn test_toc_config() {
let config = TocConfig::new().with_max_depth(3).with_summaries(false);
assert_eq!(config.max_depth, Some(3));
assert!(!config.include_summaries);
}
}