use std::fmt::Write;
#[derive(Debug, Clone)]
pub struct TocEntry {
pub title: String,
pub anchor: String,
pub children: Vec<Self>,
}
impl TocEntry {
#[must_use]
pub fn new(title: impl Into<String>, anchor: impl Into<String>) -> Self {
Self {
title: title.into(),
anchor: anchor.into(),
children: Vec::new(),
}
}
#[must_use]
pub fn with_children(
title: impl Into<String>,
anchor: impl Into<String>,
children: Vec<Self>,
) -> Self {
Self {
title: title.into(),
anchor: anchor.into(),
children,
}
}
#[must_use]
pub fn count(&self) -> usize {
1 + self.children.iter().map(Self::count).sum::<usize>()
}
}
#[derive(Debug, Clone)]
pub struct TocGenerator {
threshold: usize,
}
impl TocGenerator {
#[must_use]
pub const fn new(threshold: usize) -> Self {
Self { threshold }
}
#[must_use]
pub fn generate(&self, entries: &[TocEntry]) -> Option<String> {
let total: usize = entries.iter().map(TocEntry::count).sum();
if total < self.threshold {
return None;
}
let mut md = String::new();
_ = write!(md, "## Contents\n\n");
for entry in entries {
Self::render_entry(&mut md, entry, 0);
}
md.push('\n');
Some(md)
}
fn render_entry(md: &mut String, entry: &TocEntry, depth: usize) {
let indent = " ".repeat(depth);
_ = writeln!(md, "{indent}- [{}](#{})", entry.title, entry.anchor);
for child in &entry.children {
Self::render_entry(md, child, depth + 1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn toc_entry_new() {
let entry = TocEntry::new("Structs", "structs");
assert_eq!(entry.title, "Structs");
assert_eq!(entry.anchor, "structs");
assert!(entry.children.is_empty());
}
#[test]
fn toc_entry_with_children() {
let children = vec![
TocEntry::new("`Parser`", "parser"),
TocEntry::new("`Config`", "config"),
];
let entry = TocEntry::with_children("Structs", "structs", children);
assert_eq!(entry.title, "Structs");
assert_eq!(entry.children.len(), 2);
assert_eq!(entry.children[0].title, "`Parser`");
}
#[test]
fn toc_entry_count() {
let entry = TocEntry::new("Single", "single");
assert_eq!(entry.count(), 1);
let with_children = TocEntry::with_children(
"Parent",
"parent",
vec![
TocEntry::new("Child1", "child1"),
TocEntry::new("Child2", "child2"),
],
);
assert_eq!(with_children.count(), 3);
}
#[test]
fn toc_generator_below_threshold() {
let generator = TocGenerator::new(10);
let entries = vec![
TocEntry::new("Structs", "structs"),
TocEntry::new("Enums", "enums"),
];
assert!(generator.generate(&entries).is_none());
}
#[test]
fn toc_generator_at_threshold() {
let generator = TocGenerator::new(3);
let entries = vec![
TocEntry::new("Structs", "structs"),
TocEntry::new("Enums", "enums"),
TocEntry::new("Functions", "functions"),
];
let result = generator.generate(&entries);
assert!(result.is_some());
}
#[test]
fn toc_generator_output_format() {
let generator = TocGenerator::new(1);
let entries = vec![TocEntry::with_children(
"Structs",
"structs",
vec![
TocEntry::new("`Parser`", "parser"),
TocEntry::new("`Config`", "config"),
],
)];
let result = generator.generate(&entries).unwrap();
assert!(result.contains("## Contents"));
assert!(result.contains("- [Structs](#structs)"));
assert!(result.contains(" - [`Parser`](#parser)"));
assert!(result.contains(" - [`Config`](#config)"));
}
#[test]
fn toc_generator_nested_indentation() {
let generator = TocGenerator::new(1);
let entries = vec![TocEntry::with_children(
"Level0",
"l0",
vec![TocEntry::with_children(
"Level1",
"l1",
vec![TocEntry::new("Level2", "l2")],
)],
)];
let result = generator.generate(&entries).unwrap();
assert!(result.contains("- [Level0](#l0)"));
assert!(result.contains(" - [Level1](#l1)"));
assert!(result.contains(" - [Level2](#l2)"));
}
}