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 impls;
46mod items;
47pub mod module;
48mod nested;
49pub mod render_shared;
50
51pub use breadcrumbs::BreadcrumbGenerator;
52pub use capture::MarkdownCapture;
53pub use context::{GeneratorContext, ItemAccess, ItemFilter, LinkResolver, RenderContext};
54pub use doc_links::{
55    DocLinkProcessor, convert_html_links, convert_path_reference_links, strip_duplicate_title,
56    strip_reference_definitions,
57};
58use flat::FlatGenerator;
59use fs_err as fs;
60use indicatif::{ProgressBar, ProgressStyle};
61pub use module::ModuleRenderer;
62use nested::NestedGenerator;
63use rustdoc_types::{Crate, Item, ItemEnum};
64use tracing::{debug, info, instrument};
65
66use crate::error::Error;
67use crate::{Args, CliOutputFormat};
68
69/// Main documentation generator.
70///
71/// This struct orchestrates the entire documentation generation process,
72/// coordinating between the context, format-specific generators, and
73/// progress reporting.
74///
75/// # Example
76///
77/// ```ignore
78/// let generator = Generator::new(&krate, &args)?;
79/// generator.generate()?;
80/// ```
81pub struct Generator<'a> {
82    /// Shared context containing crate data, maps, and configuration.
83    ctx: GeneratorContext<'a>,
84
85    /// CLI arguments containing output path and format options.
86    args: &'a Args,
87
88    /// The root module item of the crate.
89    root_item: &'a Item,
90}
91
92impl<'a> Generator<'a> {
93    /// Create a new generator for the given crate and arguments.
94    ///
95    /// This initializes the shared context including:
96    /// - Path map (item ID → module path)
97    /// - Impl map (type ID → impl blocks)
98    /// - Link registry for cross-references
99    ///
100    /// # Arguments
101    ///
102    /// * `krate` - The parsed rustdoc JSON crate
103    /// * `args` - CLI arguments containing output path, format, and options
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if the root item cannot be found in the crate index.
108    pub fn new(krate: &'a Crate, args: &'a Args) -> Result<Self, Error> {
109        let root_item = krate
110            .index
111            .get(&krate.root)
112            .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
113
114        let ctx = GeneratorContext::new(krate, args);
115
116        Ok(Self {
117            ctx,
118            args,
119            root_item,
120        })
121    }
122
123    /// Generate markdown documentation.
124    ///
125    /// This is the main entry point for documentation generation. It:
126    ///
127    /// 1. Creates the output directory
128    /// 2. Sets up a progress bar
129    /// 3. Dispatches to the format-specific generator (flat or nested)
130    ///
131    /// # Errors
132    ///
133    /// Returns an error if any file operation fails.
134    #[instrument(skip(self), fields(
135        crate_name = %self.ctx.crate_name(),
136        format = ?self.args.format,
137        output = %self.args.output.display()
138    ))]
139    pub fn generate(&self) -> Result<(), Error> {
140        info!("Starting single-crate documentation generation");
141
142        // Ensure the output directory exists
143        fs::create_dir_all(&self.args.output).map_err(Error::CreateDir)?;
144        debug!(path = %self.args.output.display(), "Created output directory");
145
146        // Set up progress bar
147        let total_modules = self.ctx.count_modules(self.root_item) + 1;
148        debug!(total_modules, "Counted modules for progress tracking");
149        let progress = Self::create_progress_bar(total_modules)?;
150
151        // Dispatch to format-specific generator
152        match self.args.format {
153            CliOutputFormat::Flat => {
154                debug!("Using flat output format");
155                let generator = FlatGenerator::new(&self.ctx, &self.args.output, &progress);
156                generator.generate(self.root_item)?;
157            },
158            CliOutputFormat::Nested => {
159                debug!("Using nested output format");
160                let generator = NestedGenerator::new(&self.ctx, &self.args.output, &progress);
161                generator.generate(self.root_item)?;
162            },
163        }
164
165        progress.finish_with_message("done");
166        info!("Single-crate documentation generation complete");
167        Ok(())
168    }
169
170    /// Create a progress bar for user feedback.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the progress bar template is invalid.
175    fn create_progress_bar(total: usize) -> Result<ProgressBar, Error> {
176        let progress = ProgressBar::new(total as u64);
177        let style = ProgressStyle::with_template(
178            "{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} modules",
179        )
180        .map_err(Error::ProgressBarTemplate)?
181        .progress_chars("=>-");
182        progress.set_style(style);
183        Ok(progress)
184    }
185
186    /// Generate documentation to memory instead of disk.
187    ///
188    /// This function mirrors `generate()` but captures all output in a
189    /// `MarkdownCapture` struct instead of writing to the filesystem.
190    /// Useful for testing and programmatic access to generated docs.
191    ///
192    /// # Arguments
193    ///
194    /// * `krate` - The parsed rustdoc JSON crate
195    /// * `format` - Output format (Flat or Nested)
196    /// * `include_private` - Whether to include private items
197    ///
198    /// # Returns
199    ///
200    /// A `MarkdownCapture` containing all generated markdown files.
201    ///
202    /// # Errors
203    ///
204    /// Returns an error if the root item cannot be found in the crate index.
205    pub fn generate_to_capture(
206        krate: &Crate,
207        format: CliOutputFormat,
208        include_private: bool,
209    ) -> Result<MarkdownCapture, Error> {
210        // Create a mock Args for the context
211        let args = Args {
212            path: None,
213            dir: None,
214            mdbook: false,
215            search_index: false,
216            primary_crate: None,
217            output: std::path::PathBuf::new(),
218            format,
219            exclude_private: !include_private,
220            include_blanket_impls: false,
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);
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 flat structure to capture.
244    fn generate_flat_to_capture(
245        ctx: &GeneratorContext,
246        root: &Item,
247        capture: &mut MarkdownCapture,
248    ) -> Result<(), Error> {
249        // Generate root module
250        let renderer = module::ModuleRenderer::new(ctx, "index.md", true);
251        capture.insert("index.md".to_string(), renderer.render(root));
252
253        // Generate submodules
254        if let ItemEnum::Module(module) = &root.inner {
255            for item_id in &module.items {
256                if let Some(item) = ctx.krate.index.get(item_id)
257                    && let ItemEnum::Module(_) = &item.inner
258                    && ctx.should_include_item(item)
259                {
260                    Self::generate_flat_recursive_capture(ctx, item, "", capture)?;
261                }
262            }
263        }
264
265        Ok(())
266    }
267
268    /// Recursive flat generation to capture.
269    fn generate_flat_recursive_capture(
270        ctx: &GeneratorContext,
271        item: &Item,
272        prefix: &str,
273        capture: &mut MarkdownCapture,
274    ) -> Result<(), Error> {
275        let name = item.name.as_deref().unwrap_or("unnamed");
276        let current_file = if prefix.is_empty() {
277            format!("{name}.md")
278        } else {
279            format!("{prefix}__{name}.md")
280        };
281
282        let renderer = module::ModuleRenderer::new(ctx, &current_file, false);
283        let content = renderer.render(item);
284        capture.insert(current_file, content);
285
286        let new_prefix = if prefix.is_empty() {
287            name.to_string()
288        } else {
289            format!("{prefix}__{name}")
290        };
291
292        if let ItemEnum::Module(module) = &item.inner {
293            for sub_id in &module.items {
294                if let Some(sub_item) = ctx.krate.index.get(sub_id)
295                    && let ItemEnum::Module(_) = &sub_item.inner
296                    && ctx.should_include_item(sub_item)
297                {
298                    Self::generate_flat_recursive_capture(ctx, sub_item, &new_prefix, capture)?;
299                }
300            }
301        }
302
303        Ok(())
304    }
305
306    /// Generate nested structure to capture.
307    fn generate_nested_to_capture(
308        ctx: &GeneratorContext,
309        root: &Item,
310        path_prefix: &str,
311        capture: &mut MarkdownCapture,
312    ) -> Result<(), Error> {
313        let name = root.name.as_deref().unwrap_or("unnamed");
314        let is_root = path_prefix.is_empty()
315            && name
316                == ctx.krate.index[&ctx.krate.root]
317                    .name
318                    .as_deref()
319                    .unwrap_or("");
320
321        let current_file = if path_prefix.is_empty() {
322            if is_root {
323                "index.md".to_string()
324            } else {
325                format!("{name}/index.md")
326            }
327        } else {
328            format!("{path_prefix}/{name}/index.md")
329        };
330
331        let renderer = module::ModuleRenderer::new(ctx, &current_file, is_root);
332        capture.insert(current_file.clone(), renderer.render(root));
333
334        let new_prefix = if path_prefix.is_empty() {
335            if is_root {
336                String::new()
337            } else {
338                name.to_string()
339            }
340        } else {
341            format!("{path_prefix}/{name}")
342        };
343
344        if let ItemEnum::Module(module) = &root.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_nested_to_capture(ctx, sub_item, &new_prefix, capture)?;
351                }
352            }
353        }
354
355        Ok(())
356    }
357
358    /// Convenience method to generate documentation in one call.
359    ///
360    /// Creates a `Generator` and runs it immediately. For more control
361    /// over the generation process, use `new()` and `generate()` separately.
362    ///
363    /// # Arguments
364    ///
365    /// * `krate` - The parsed rustdoc JSON crate
366    /// * `args` - CLI arguments containing output path, format, and options
367    ///
368    /// # Returns
369    ///
370    /// `Ok(())` on success, or an error if any file operation fails.
371    ///
372    /// # Errors
373    ///
374    /// Returns an error if the root item cannot be found or if file operations fail.
375    pub fn run(krate: &'a Crate, args: &'a Args) -> Result<(), Error> {
376        let generator = Self::new(krate, args)?;
377        generator.generate()
378    }
379}