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            path: None,
219            dir: None,
220            mdbook: false,
221            search_index: false,
222            primary_crate: None,
223            output: std::path::PathBuf::new(),
224            format,
225            exclude_private: !include_private,
226            include_blanket_impls: false,
227        };
228
229        let root_item = krate
230            .index
231            .get(&krate.root)
232            .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
233
234        let ctx = GeneratorContext::new(krate, &args, RenderConfig::default());
235        let mut capture = MarkdownCapture::new();
236
237        match format {
238            CliOutputFormat::Flat => {
239                Self::generate_flat_to_capture(&ctx, root_item, &mut capture)?;
240            },
241            CliOutputFormat::Nested => {
242                Self::generate_nested_to_capture(&ctx, root_item, "", &mut capture)?;
243            },
244        }
245
246        Ok(capture)
247    }
248
249    /// Generate markdown to an in-memory capture with custom configuration.
250    ///
251    /// This variant allows specifying a custom [`RenderConfig`] for testing
252    /// different rendering options like `hide_trivial_derives`.
253    ///
254    /// # Arguments
255    ///
256    /// * `krate` - The parsed rustdoc JSON crate
257    /// * `format` - Output format (Flat or Nested)
258    /// * `include_private` - Whether to include private items
259    /// * `config` - Custom rendering configuration
260    ///
261    /// # Returns
262    ///
263    /// A `MarkdownCapture` containing all generated markdown files.
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if the root item cannot be found in the crate index.
268    pub fn generate_to_capture_with_config(
269        krate: &Crate,
270        format: CliOutputFormat,
271        include_private: bool,
272        config: RenderConfig,
273    ) -> Result<MarkdownCapture, Error> {
274        // Create a mock Args for the context
275        let args = Args {
276            path: None,
277            dir: None,
278            mdbook: false,
279            search_index: false,
280            primary_crate: None,
281            output: std::path::PathBuf::new(),
282            format,
283            exclude_private: !include_private,
284            include_blanket_impls: false,
285        };
286
287        let root_item = krate
288            .index
289            .get(&krate.root)
290            .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
291
292        let ctx = GeneratorContext::new(krate, &args, config);
293        let mut capture = MarkdownCapture::new();
294
295        match format {
296            CliOutputFormat::Flat => {
297                Self::generate_flat_to_capture(&ctx, root_item, &mut capture)?;
298            },
299            CliOutputFormat::Nested => {
300                Self::generate_nested_to_capture(&ctx, root_item, "", &mut capture)?;
301            },
302        }
303
304        Ok(capture)
305    }
306
307    /// Generate flat structure to capture.
308    fn generate_flat_to_capture(
309        ctx: &GeneratorContext,
310        root: &Item,
311        capture: &mut MarkdownCapture,
312    ) -> Result<(), Error> {
313        // Generate root module
314        let renderer = module::ModuleRenderer::new(ctx, "index.md", true);
315        capture.insert("index.md".to_string(), renderer.render(root));
316
317        // Generate submodules
318        if let ItemEnum::Module(module) = &root.inner {
319            for item_id in &module.items {
320                if let Some(item) = ctx.krate.index.get(item_id)
321                    && let ItemEnum::Module(_) = &item.inner
322                    && ctx.should_include_item(item)
323                {
324                    Self::generate_flat_recursive_capture(ctx, item, "", capture)?;
325                }
326            }
327        }
328
329        Ok(())
330    }
331
332    /// Recursive flat generation to capture.
333    fn generate_flat_recursive_capture(
334        ctx: &GeneratorContext,
335        item: &Item,
336        prefix: &str,
337        capture: &mut MarkdownCapture,
338    ) -> Result<(), Error> {
339        let name = item.name.as_deref().unwrap_or("unnamed");
340        let current_file = if prefix.is_empty() {
341            format!("{name}.md")
342        } else {
343            format!("{prefix}__{name}.md")
344        };
345
346        let renderer = module::ModuleRenderer::new(ctx, &current_file, false);
347        let content = renderer.render(item);
348        capture.insert(current_file, content);
349
350        let new_prefix = if prefix.is_empty() {
351            name.to_string()
352        } else {
353            format!("{prefix}__{name}")
354        };
355
356        if let ItemEnum::Module(module) = &item.inner {
357            for sub_id in &module.items {
358                if let Some(sub_item) = ctx.krate.index.get(sub_id)
359                    && let ItemEnum::Module(_) = &sub_item.inner
360                    && ctx.should_include_item(sub_item)
361                {
362                    Self::generate_flat_recursive_capture(ctx, sub_item, &new_prefix, capture)?;
363                }
364            }
365        }
366
367        Ok(())
368    }
369
370    /// Generate nested structure to capture.
371    fn generate_nested_to_capture(
372        ctx: &GeneratorContext,
373        root: &Item,
374        path_prefix: &str,
375        capture: &mut MarkdownCapture,
376    ) -> Result<(), Error> {
377        let name = root.name.as_deref().unwrap_or("unnamed");
378        let is_root = path_prefix.is_empty()
379            && name
380                == ctx.krate.index[&ctx.krate.root]
381                    .name
382                    .as_deref()
383                    .unwrap_or("");
384
385        let current_file = if path_prefix.is_empty() {
386            if is_root {
387                "index.md".to_string()
388            } else {
389                format!("{name}/index.md")
390            }
391        } else {
392            format!("{path_prefix}/{name}/index.md")
393        };
394
395        let renderer = module::ModuleRenderer::new(ctx, &current_file, is_root);
396        capture.insert(current_file.clone(), renderer.render(root));
397
398        let new_prefix = if path_prefix.is_empty() {
399            if is_root {
400                String::new()
401            } else {
402                name.to_string()
403            }
404        } else {
405            format!("{path_prefix}/{name}")
406        };
407
408        if let ItemEnum::Module(module) = &root.inner {
409            for sub_id in &module.items {
410                if let Some(sub_item) = ctx.krate.index.get(sub_id)
411                    && let ItemEnum::Module(_) = &sub_item.inner
412                    && ctx.should_include_item(sub_item)
413                {
414                    Self::generate_nested_to_capture(ctx, sub_item, &new_prefix, capture)?;
415                }
416            }
417        }
418
419        Ok(())
420    }
421
422    /// Convenience method to generate documentation in one call.
423    ///
424    /// Creates a `Generator` and runs it immediately. For more control
425    /// over the generation process, use `new()` and `generate()` separately.
426    ///
427    /// Uses default `RenderConfig`. For custom configuration, use `new()` directly.
428    ///
429    /// # Arguments
430    ///
431    /// * `krate` - The parsed rustdoc JSON crate
432    /// * `args` - CLI arguments containing output path, format, and options
433    ///
434    /// # Returns
435    ///
436    /// `Ok(())` on success, or an error if any file operation fails.
437    ///
438    /// # Errors
439    ///
440    /// Returns an error if the root item cannot be found or if file operations fail.
441    pub fn run(krate: &'a Crate, args: &'a Args) -> Result<(), Error> {
442        let generator = Self::new(krate, args, RenderConfig::default())?;
443        generator.generate()
444    }
445}