cargo_docs_md/generator/
toc.rs1use std::fmt::Write;
21
22#[derive(Debug, Clone)]
28pub struct TocEntry {
29 pub title: String,
31
32 pub anchor: String,
34
35 pub children: Vec<Self>,
37}
38
39impl TocEntry {
40 #[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 #[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 #[must_use]
77 pub fn count(&self) -> usize {
78 1 + self.children.iter().map(Self::count).sum::<usize>()
79 }
80}
81
82#[derive(Debug, Clone)]
88pub struct TocGenerator {
89 threshold: usize,
91}
92
93impl TocGenerator {
94 #[must_use]
100 pub const fn new(threshold: usize) -> Self {
101 Self { threshold }
102 }
103
104 #[must_use]
116 pub fn generate(&self, entries: &[TocEntry]) -> Option<String> {
117 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 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}