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, &current_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, &current_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}