cargo_docs_md/generator/
toc.rs

1//! Table of Contents generation for module documentation.
2//!
3//! This module provides [`TocGenerator`] which generates a markdown table of contents
4//! for modules that exceed a configurable item threshold. The TOC provides navigation
5//! to major sections (Types, Traits, Functions, etc.) with nested links to individual items.
6//!
7//! # Example Output
8//!
9//! ```markdown
10//! ## Contents
11//!
12//! - [Structs](#structs)
13//!   - [`Parser`](#parser)
14//!   - [`Config`](#config)
15//! - [Enums](#enums)
16//!   - [`Error`](#error)
17//! - [Functions](#functions)
18//! ```
19
20use std::fmt::Write;
21
22/// An entry in the table of contents.
23///
24/// Each entry represents either a section heading (like "Structs") or an
25/// individual item (like a specific struct name). Entries can have children
26/// for nested navigation.
27#[derive(Debug, Clone)]
28pub struct TocEntry {
29    /// Display title for this entry.
30    pub title: String,
31
32    /// Anchor link target (without the `#` prefix).
33    pub anchor: String,
34
35    /// Child entries for nested navigation.
36    pub children: Vec<Self>,
37}
38
39impl TocEntry {
40    /// Create a new TOC entry.
41    ///
42    /// # Arguments
43    ///
44    /// * `title` - Display title for the entry
45    /// * `anchor` - Anchor link target (without `#`)
46    #[must_use]
47    pub fn new(title: impl Into<String>, anchor: impl Into<String>) -> Self {
48        Self {
49            title: title.into(),
50            anchor: anchor.into(),
51            children: Vec::new(),
52        }
53    }
54
55    /// Create a new TOC entry with children.
56    ///
57    /// # Arguments
58    ///
59    /// * `title` - Display title for the entry
60    /// * `anchor` - Anchor link target (without `#`)
61    /// * `children` - Child entries for nested items
62    #[must_use]
63    pub fn with_children(
64        title: impl Into<String>,
65        anchor: impl Into<String>,
66        children: Vec<Self>,
67    ) -> Self {
68        Self {
69            title: title.into(),
70            anchor: anchor.into(),
71            children,
72        }
73    }
74
75    /// Count total items in this entry and all descendants.
76    #[must_use]
77    pub fn count(&self) -> usize {
78        1 + self.children.iter().map(Self::count).sum::<usize>()
79    }
80}
81
82/// Generator for markdown table of contents.
83///
84/// The generator only produces output when the total number of items
85/// exceeds the configured threshold. This prevents cluttering small
86/// modules with unnecessary navigation.
87#[derive(Debug, Clone)]
88pub struct TocGenerator {
89    /// Minimum items required to generate a TOC.
90    threshold: usize,
91}
92
93impl TocGenerator {
94    /// Create a new TOC generator with the given threshold.
95    ///
96    /// # Arguments
97    ///
98    /// * `threshold` - Minimum number of items required to generate a TOC
99    #[must_use]
100    pub const fn new(threshold: usize) -> Self {
101        Self { threshold }
102    }
103
104    /// Generate a markdown table of contents from the given entries.
105    ///
106    /// Returns `None` if the total item count is below the threshold.
107    ///
108    /// # Arguments
109    ///
110    /// * `entries` - Top-level TOC entries (typically section headings)
111    ///
112    /// # Returns
113    ///
114    /// A formatted markdown string with the TOC, or `None` if below threshold.
115    #[must_use]
116    pub fn generate(&self, entries: &[TocEntry]) -> Option<String> {
117        // Calculate total items
118        let total: usize = entries.iter().map(TocEntry::count).sum();
119
120        if total < self.threshold {
121            return None;
122        }
123
124        let mut md = String::new();
125        _ = write!(md, "## Contents\n\n");
126
127        for entry in entries {
128            Self::render_entry(&mut md, entry, 0);
129        }
130
131        md.push('\n');
132        Some(md)
133    }
134
135    /// Render a single TOC entry with proper indentation.
136    fn render_entry(md: &mut String, entry: &TocEntry, depth: usize) {
137        let indent = "  ".repeat(depth);
138        _ = writeln!(md, "{indent}- [{}](#{})", entry.title, entry.anchor);
139
140        for child in &entry.children {
141            Self::render_entry(md, child, depth + 1);
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn toc_entry_new() {
152        let entry = TocEntry::new("Structs", "structs");
153        assert_eq!(entry.title, "Structs");
154        assert_eq!(entry.anchor, "structs");
155        assert!(entry.children.is_empty());
156    }
157
158    #[test]
159    fn toc_entry_with_children() {
160        let children = vec![
161            TocEntry::new("`Parser`", "parser"),
162            TocEntry::new("`Config`", "config"),
163        ];
164        let entry = TocEntry::with_children("Structs", "structs", children);
165
166        assert_eq!(entry.title, "Structs");
167        assert_eq!(entry.children.len(), 2);
168        assert_eq!(entry.children[0].title, "`Parser`");
169    }
170
171    #[test]
172    fn toc_entry_count() {
173        let entry = TocEntry::new("Single", "single");
174        assert_eq!(entry.count(), 1);
175
176        let with_children = TocEntry::with_children(
177            "Parent",
178            "parent",
179            vec![
180                TocEntry::new("Child1", "child1"),
181                TocEntry::new("Child2", "child2"),
182            ],
183        );
184        assert_eq!(with_children.count(), 3);
185    }
186
187    #[test]
188    fn toc_generator_below_threshold() {
189        let generator = TocGenerator::new(10);
190        let entries = vec![
191            TocEntry::new("Structs", "structs"),
192            TocEntry::new("Enums", "enums"),
193        ];
194
195        assert!(generator.generate(&entries).is_none());
196    }
197
198    #[test]
199    fn toc_generator_at_threshold() {
200        let generator = TocGenerator::new(3);
201        let entries = vec![
202            TocEntry::new("Structs", "structs"),
203            TocEntry::new("Enums", "enums"),
204            TocEntry::new("Functions", "functions"),
205        ];
206
207        let result = generator.generate(&entries);
208        assert!(result.is_some());
209    }
210
211    #[test]
212    fn toc_generator_output_format() {
213        let generator = TocGenerator::new(1);
214        let entries = vec![TocEntry::with_children(
215            "Structs",
216            "structs",
217            vec![
218                TocEntry::new("`Parser`", "parser"),
219                TocEntry::new("`Config`", "config"),
220            ],
221        )];
222
223        let result = generator.generate(&entries).unwrap();
224
225        assert!(result.contains("## Contents"));
226        assert!(result.contains("- [Structs](#structs)"));
227        assert!(result.contains("  - [`Parser`](#parser)"));
228        assert!(result.contains("  - [`Config`](#config)"));
229    }
230
231    #[test]
232    fn toc_generator_nested_indentation() {
233        let generator = TocGenerator::new(1);
234        let entries = vec![TocEntry::with_children(
235            "Level0",
236            "l0",
237            vec![TocEntry::with_children(
238                "Level1",
239                "l1",
240                vec![TocEntry::new("Level2", "l2")],
241            )],
242        )];
243
244        let result = generator.generate(&entries).unwrap();
245
246        assert!(result.contains("- [Level0](#l0)"));
247        assert!(result.contains("  - [Level1](#l1)"));
248        assert!(result.contains("    - [Level2](#l2)"));
249    }
250}