cargo_docs_md/generator/mod.rs
1//! Markdown documentation generator for rustdoc JSON.
2//!
3//! This is the core module that transforms rustdoc JSON data into markdown files.
4//! It handles the complete generation pipeline: traversing modules, rendering
5//! different item types, and creating cross-reference links.
6//!
7//! # Architecture
8//!
9//! The generation process follows these steps:
10//!
11//! 1. **Setup**: Create output directory, build path and impl maps
12//! 2. **Link Registry**: Build a registry mapping item IDs to file paths
13//! 3. **Generation**: Recursively traverse modules and write markdown files
14//!
15//! # Module Structure
16//!
17//! - [`context`] - Shared state for generation (crate data, maps, config)
18//! - [`module`] - Module-level markdown rendering
19//! - [`items`] - Individual item rendering (structs, enums, traits, etc.)
20//! - [`impls`] - Implementation block rendering
21//! - [`flat`] - Flat output format generator
22//! - [`nested`] - Nested output format generator
23//!
24//! # Output Formats
25//!
26//! Two output formats are supported:
27//!
28//! - **Flat**: All files in one directory (`module.md`, `parent__child.md`)
29//! - **Nested**: Directory hierarchy (`module/index.md`, `parent/child/index.md`)
30//!
31//! # Usage
32//!
33//! ```ignore
34//! use docs_md::generator::Generator;
35//!
36//! let generator = Generator::new(&krate, &args)?;
37//! generator.generate()?;
38//! ```
39
40pub mod breadcrumbs;
41mod capture;
42mod context;
43pub mod doc_links;
44mod flat;
45pub mod impl_category;
46pub mod impls;
47mod items;
48pub mod module;
49mod nested;
50pub mod quick_ref;
51pub mod render_shared;
52pub mod toc;
53
54pub use breadcrumbs::BreadcrumbGenerator;
55pub use capture::MarkdownCapture;
56pub mod config;
57pub use config::{RenderConfig, SourceConfig};
58pub use context::{GeneratorContext, ItemAccess, ItemFilter, LinkResolver, RenderContext};
59pub use doc_links::{DocLinkProcessor, DocLinkUtils};
60use flat::FlatGenerator;
61use fs_err as fs;
62pub use impl_category::ImplCategory;
63use indicatif::{ProgressBar, ProgressStyle};
64pub use module::ModuleRenderer;
65use nested::NestedGenerator;
66pub use quick_ref::{QuickRefEntry, QuickRefGenerator, extract_summary};
67use rustdoc_types::{Crate, Item, ItemEnum};
68pub use toc::{TocEntry, TocGenerator};
69use tracing::{debug, info, instrument};
70
71use crate::error::Error;
72use crate::{Args, CliOutputFormat};
73
74/// Main documentation generator.
75///
76/// This struct orchestrates the entire documentation generation process,
77/// coordinating between the context, format-specific generators, and
78/// progress reporting.
79///
80/// # Example
81///
82/// ```ignore
83/// let generator = Generator::new(&krate, &args)?;
84/// generator.generate()?;
85/// ```
86pub struct Generator<'a> {
87 /// Shared context containing crate data, maps, and configuration.
88 ctx: GeneratorContext<'a>,
89
90 /// CLI arguments containing output path and format options.
91 args: &'a Args,
92
93 /// The root module item of the crate.
94 root_item: &'a Item,
95}
96
97impl<'a> Generator<'a> {
98 /// Create a new generator for the given crate and arguments.
99 ///
100 /// This initializes the shared context including:
101 /// - Path map (item ID → module path)
102 /// - Impl map (type ID → impl blocks)
103 /// - Link registry for cross-references
104 ///
105 /// # Arguments
106 ///
107 /// * `krate` - The parsed rustdoc JSON crate
108 /// * `args` - CLI arguments containing output path, format, and options
109 /// * `config` - Rendering configuration options
110 ///
111 /// # Errors
112 ///
113 /// Returns an error if the root item cannot be found in the crate index.
114 pub fn new(krate: &'a Crate, args: &'a Args, config: RenderConfig) -> Result<Self, Error> {
115 let root_item = krate
116 .index
117 .get(&krate.root)
118 .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
119
120 let ctx = GeneratorContext::new(krate, args, config);
121
122 Ok(Self {
123 ctx,
124 args,
125 root_item,
126 })
127 }
128
129 /// Generate markdown documentation.
130 ///
131 /// This is the main entry point for documentation generation. It:
132 ///
133 /// 1. Creates the output directory
134 /// 2. Sets up a progress bar
135 /// 3. Dispatches to the format-specific generator (flat or nested)
136 ///
137 /// # Errors
138 ///
139 /// Returns an error if any file operation fails.
140 #[instrument(skip(self), fields(
141 crate_name = %self.ctx.crate_name(),
142 format = ?self.args.format,
143 output = %self.args.output.display()
144 ))]
145 pub fn generate(&self) -> Result<(), Error> {
146 info!("Starting single-crate documentation generation");
147
148 // Ensure the output directory exists
149 fs::create_dir_all(&self.args.output).map_err(Error::CreateDir)?;
150 debug!(path = %self.args.output.display(), "Created output directory");
151
152 // Set up progress bar
153 let total_modules = self.ctx.count_modules(self.root_item) + 1;
154 debug!(total_modules, "Counted modules for progress tracking");
155 let progress = Self::create_progress_bar(total_modules)?;
156
157 // Dispatch to format-specific generator
158 match self.args.format {
159 CliOutputFormat::Flat => {
160 debug!("Using flat output format");
161 let generator = FlatGenerator::new(&self.ctx, &self.args.output, &progress);
162 generator.generate(self.root_item)?;
163 },
164 CliOutputFormat::Nested => {
165 debug!("Using nested output format");
166 let generator = NestedGenerator::new(&self.ctx, &self.args.output, &progress);
167 generator.generate(self.root_item)?;
168 },
169 }
170
171 progress.finish_with_message("done");
172 info!("Single-crate documentation generation complete");
173 Ok(())
174 }
175
176 /// Create a progress bar for user feedback.
177 ///
178 /// # Errors
179 ///
180 /// Returns an error if the progress bar template is invalid.
181 fn create_progress_bar(total: usize) -> Result<ProgressBar, Error> {
182 let progress = ProgressBar::new(total as u64);
183 let style = ProgressStyle::with_template(
184 "{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} modules",
185 )
186 .map_err(Error::ProgressBarTemplate)?
187 .progress_chars("=>-");
188 progress.set_style(style);
189 Ok(progress)
190 }
191
192 /// Generate documentation to memory instead of disk.
193 ///
194 /// This function mirrors `generate()` but captures all output in a
195 /// `MarkdownCapture` struct instead of writing to the filesystem.
196 /// Useful for testing and programmatic access to generated docs.
197 ///
198 /// # Arguments
199 ///
200 /// * `krate` - The parsed rustdoc JSON crate
201 /// * `format` - Output format (Flat or Nested)
202 /// * `include_private` - Whether to include private items
203 ///
204 /// # Returns
205 ///
206 /// A `MarkdownCapture` containing all generated markdown files.
207 ///
208 /// # Errors
209 ///
210 /// Returns an error if the root item cannot be found in the crate index.
211 pub fn generate_to_capture(
212 krate: &Crate,
213 format: CliOutputFormat,
214 include_private: bool,
215 ) -> Result<MarkdownCapture, Error> {
216 // Create a mock Args for the context
217 let args = Args {
218 format,
219 exclude_private: !include_private,
220 ..Args::default()
221 };
222
223 let root_item = krate
224 .index
225 .get(&krate.root)
226 .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
227
228 let ctx = GeneratorContext::new(krate, &args, RenderConfig::default());
229 let mut capture = MarkdownCapture::new();
230
231 match format {
232 CliOutputFormat::Flat => {
233 Self::generate_flat_to_capture(&ctx, root_item, &mut capture)?;
234 },
235 CliOutputFormat::Nested => {
236 Self::generate_nested_to_capture(&ctx, root_item, "", &mut capture)?;
237 },
238 }
239
240 Ok(capture)
241 }
242
243 /// Generate markdown to an in-memory capture with custom configuration.
244 ///
245 /// This variant allows specifying a custom [`RenderConfig`] for testing
246 /// different rendering options like `hide_trivial_derives`.
247 ///
248 /// # Arguments
249 ///
250 /// * `krate` - The parsed rustdoc JSON crate
251 /// * `format` - Output format (Flat or Nested)
252 /// * `include_private` - Whether to include private items
253 /// * `config` - Custom rendering configuration
254 ///
255 /// # Returns
256 ///
257 /// A `MarkdownCapture` containing all generated markdown files.
258 ///
259 /// # Errors
260 ///
261 /// Returns an error if the root item cannot be found in the crate index.
262 pub fn generate_to_capture_with_config(
263 krate: &Crate,
264 format: CliOutputFormat,
265 include_private: bool,
266 config: RenderConfig,
267 ) -> Result<MarkdownCapture, Error> {
268 // Create a mock Args for the context
269 let args = Args {
270 format,
271 exclude_private: !include_private,
272 ..Args::default()
273 };
274
275 let root_item = krate
276 .index
277 .get(&krate.root)
278 .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
279
280 let ctx = GeneratorContext::new(krate, &args, config);
281 let mut capture = MarkdownCapture::new();
282
283 match format {
284 CliOutputFormat::Flat => {
285 Self::generate_flat_to_capture(&ctx, root_item, &mut capture)?;
286 },
287 CliOutputFormat::Nested => {
288 Self::generate_nested_to_capture(&ctx, root_item, "", &mut capture)?;
289 },
290 }
291
292 Ok(capture)
293 }
294
295 /// Generate flat structure to capture.
296 fn generate_flat_to_capture(
297 ctx: &GeneratorContext,
298 root: &Item,
299 capture: &mut MarkdownCapture,
300 ) -> Result<(), Error> {
301 // Generate root module
302 let renderer = module::ModuleRenderer::new(ctx, "index.md", true);
303 capture.insert("index.md".to_string(), renderer.render(root));
304
305 // Generate submodules
306 if let ItemEnum::Module(module) = &root.inner {
307 for item_id in &module.items {
308 if let Some(item) = ctx.krate.index.get(item_id)
309 && let ItemEnum::Module(_) = &item.inner
310 && ctx.should_include_item(item)
311 {
312 Self::generate_flat_recursive_capture(ctx, item, "", capture)?;
313 }
314 }
315 }
316
317 Ok(())
318 }
319
320 /// Recursive flat generation to capture.
321 fn generate_flat_recursive_capture(
322 ctx: &GeneratorContext,
323 item: &Item,
324 prefix: &str,
325 capture: &mut MarkdownCapture,
326 ) -> Result<(), Error> {
327 let name = item.name.as_deref().unwrap_or("unnamed");
328 let current_file = if prefix.is_empty() {
329 format!("{name}.md")
330 } else {
331 format!("{prefix}__{name}.md")
332 };
333
334 let renderer = module::ModuleRenderer::new(ctx, ¤t_file, false);
335 let content = renderer.render(item);
336 capture.insert(current_file, content);
337
338 let new_prefix = if prefix.is_empty() {
339 name.to_string()
340 } else {
341 format!("{prefix}__{name}")
342 };
343
344 if let ItemEnum::Module(module) = &item.inner {
345 for sub_id in &module.items {
346 if let Some(sub_item) = ctx.krate.index.get(sub_id)
347 && let ItemEnum::Module(_) = &sub_item.inner
348 && ctx.should_include_item(sub_item)
349 {
350 Self::generate_flat_recursive_capture(ctx, sub_item, &new_prefix, capture)?;
351 }
352 }
353 }
354
355 Ok(())
356 }
357
358 /// Generate nested structure to capture.
359 fn generate_nested_to_capture(
360 ctx: &GeneratorContext,
361 root: &Item,
362 path_prefix: &str,
363 capture: &mut MarkdownCapture,
364 ) -> Result<(), Error> {
365 let name = root.name.as_deref().unwrap_or("unnamed");
366 let is_root = path_prefix.is_empty()
367 && name
368 == ctx.krate.index[&ctx.krate.root]
369 .name
370 .as_deref()
371 .unwrap_or("");
372
373 let current_file = if path_prefix.is_empty() {
374 if is_root {
375 "index.md".to_string()
376 } else {
377 format!("{name}/index.md")
378 }
379 } else {
380 format!("{path_prefix}/{name}/index.md")
381 };
382
383 let renderer = module::ModuleRenderer::new(ctx, ¤t_file, is_root);
384 capture.insert(current_file.clone(), renderer.render(root));
385
386 let new_prefix = if path_prefix.is_empty() {
387 if is_root {
388 String::new()
389 } else {
390 name.to_string()
391 }
392 } else {
393 format!("{path_prefix}/{name}")
394 };
395
396 if let ItemEnum::Module(module) = &root.inner {
397 for sub_id in &module.items {
398 if let Some(sub_item) = ctx.krate.index.get(sub_id)
399 && let ItemEnum::Module(_) = &sub_item.inner
400 && ctx.should_include_item(sub_item)
401 {
402 Self::generate_nested_to_capture(ctx, sub_item, &new_prefix, capture)?;
403 }
404 }
405 }
406
407 Ok(())
408 }
409
410 /// Convenience method to generate documentation in one call.
411 ///
412 /// Creates a `Generator` and runs it immediately. For more control
413 /// over the generation process, use `new()` and `generate()` separately.
414 ///
415 /// Uses default `RenderConfig`. For custom configuration, use `new()` directly.
416 ///
417 /// # Arguments
418 ///
419 /// * `krate` - The parsed rustdoc JSON crate
420 /// * `args` - CLI arguments containing output path, format, and options
421 ///
422 /// # Returns
423 ///
424 /// `Ok(())` on success, or an error if any file operation fails.
425 ///
426 /// # Errors
427 ///
428 /// Returns an error if the root item cannot be found or if file operations fail.
429 pub fn run(krate: &'a Crate, args: &'a Args) -> Result<(), Error> {
430 let generator = Self::new(krate, args, RenderConfig::default())?;
431 generator.generate()
432 }
433}