cargo_docs_md/generator/
module.rs

1//! Module markdown rendering for documentation generation.
2//!
3//! This module provides the [`ModuleRenderer`] struct which handles rendering
4//! a Rust module's documentation to markdown format, including all its items
5//! organized by type.
6
7use std::collections::HashSet;
8use std::fmt::Write;
9
10use rustdoc_types::{Id, Item, ItemEnum};
11
12use crate::generator::context::RenderContext;
13use crate::generator::items::ItemRenderer;
14
15/// Renders a module to markdown.
16///
17/// This struct handles the complete rendering of a module's documentation page,
18/// including:
19/// - Title (Crate or Module heading)
20/// - Module-level documentation
21/// - Sections for each item type (Modules, Structs, Enums, etc.)
22///
23/// The renderer is generic over [`RenderContext`], allowing it to work with
24/// both single-crate (`GeneratorContext`) and multi-crate (`SingleCrateView`) modes.
25pub struct ModuleRenderer<'a> {
26    /// Reference to the render context (either single-crate or multi-crate).
27    ctx: &'a dyn RenderContext,
28
29    /// Path of the current file being generated (for relative link calculation).
30    current_file: &'a str,
31
32    /// Whether this is the crate root module.
33    is_root: bool,
34}
35
36impl<'a> ModuleRenderer<'a> {
37    /// Create a new module renderer.
38    ///
39    /// # Arguments
40    ///
41    /// * `ctx` - Render context (implements `RenderContext` trait)
42    /// * `current_file` - Path of this file (for relative link calculation)
43    /// * `is_root` - True if this is the crate root module
44    pub fn new(ctx: &'a dyn RenderContext, current_file: &'a str, is_root: bool) -> Self {
45        Self {
46            ctx,
47            current_file,
48            is_root,
49        }
50    }
51
52    /// Process documentation string to resolve intra-doc links.
53    ///
54    /// Delegates to the render context's `process_docs` method, which handles
55    /// both single-crate and multi-crate link resolution.
56    fn process_docs(&self, item: &Item) -> Option<String> {
57        self.ctx.process_docs(item, self.current_file)
58    }
59
60    /// Generate the complete markdown content for a module.
61    ///
62    /// # Output Structure
63    ///
64    /// ```markdown
65    /// # Crate `name` (or Module `name`)
66    ///
67    /// [module documentation]
68    ///
69    /// ## Modules
70    /// - [submodule](link) - first line of docs
71    ///
72    /// ## Structs
73    /// ### `StructName`
74    /// [struct definition and docs]
75    ///
76    /// ## Enums
77    /// ...
78    /// ```
79    #[must_use]
80    pub fn render(&self, item: &Item) -> String {
81        let mut md = String::new();
82
83        let name = item.name.as_deref().unwrap_or("crate");
84
85        // === Title Section ===
86        if self.is_root {
87            _ = write!(md, "# Crate `{name}`\n\n");
88            if let Some(version) = self.ctx.crate_version() {
89                _ = write!(md, "**Version:** {version}\n\n");
90            }
91        } else {
92            _ = write!(md, "# Module `{name}`\n\n");
93        }
94
95        // === Documentation Section ===
96        if let Some(docs) = self.process_docs(item) {
97            md.push_str(&docs);
98            md.push_str("\n\n");
99        }
100
101        // === Module Contents ===
102        if let ItemEnum::Module(module) = &item.inner {
103            let categorized = self.categorize_items(&module.items);
104            self.render_all_sections(&mut md, &categorized);
105        }
106
107        md
108    }
109
110    /// Categorize module items by type for organized rendering.
111    fn categorize_items(&self, item_ids: &'a [Id]) -> CategorizedItems<'a> {
112        let mut items = CategorizedItems::default();
113        let mut seen_items: HashSet<&Id> = HashSet::new();
114
115        for item_id in item_ids {
116            // Skip if already processed (from glob expansion)
117            if !seen_items.insert(item_id) {
118                continue;
119            }
120
121            if let Some(child) = self.ctx.get_item(item_id) {
122                if !self.ctx.should_include_item(child) {
123                    continue;
124                }
125
126                match &child.inner {
127                    ItemEnum::Module(_) => items.modules.push((item_id, child)),
128                    ItemEnum::Struct(_) => items.structs.push((item_id, child)),
129                    ItemEnum::Enum(_) => items.enums.push((item_id, child)),
130                    ItemEnum::Trait(_) => items.traits.push((item_id, child)),
131                    ItemEnum::Function(_) => items.functions.push(child),
132                    ItemEnum::Macro(_) => items.macros.push(child),
133                    ItemEnum::Constant { .. } => items.constants.push(child),
134                    ItemEnum::TypeAlias(_) => items.type_aliases.push(child),
135
136                    // Handle re-exports
137                    ItemEnum::Use(use_item) => {
138                        if use_item.is_glob {
139                            // Glob re-export: expand target module's items
140                            self.expand_glob_reexport(&mut items, use_item, &mut seen_items);
141                        } else if let Some(target_id) = &use_item.id
142                            && let Some(target_item) = self.ctx.get_item(target_id)
143                        {
144                            // Specific re-export: categorize by target type
145                            match &target_item.inner {
146                                ItemEnum::Module(_) => items.modules.push((item_id, child)),
147                                ItemEnum::Struct(_) => items.structs.push((item_id, child)),
148                                ItemEnum::Enum(_) => items.enums.push((item_id, child)),
149                                ItemEnum::Trait(_) => items.traits.push((item_id, child)),
150                                ItemEnum::Function(_) => items.functions.push(child),
151                                ItemEnum::Macro(_) => items.macros.push(child),
152                                ItemEnum::Constant { .. } => items.constants.push(child),
153                                ItemEnum::TypeAlias(_) => items.type_aliases.push(child),
154                                _ => {},
155                            }
156                        }
157                    },
158                    _ => {},
159                }
160            }
161        }
162
163        // Sort all categories for deterministic output
164        items.sort();
165
166        items
167    }
168
169    /// Expand a glob re-export by adding all public items from the target module.
170    fn expand_glob_reexport(
171        &self,
172        items: &mut CategorizedItems<'a>,
173        use_item: &rustdoc_types::Use,
174        seen_items: &mut HashSet<&'a Id>,
175    ) {
176        // Get target module ID
177        let Some(target_id) = &use_item.id else { return };
178
179        // Look up target module
180        let Some(target_module) = self.ctx.get_item(target_id) else { return };
181
182        // Must be a module
183        let ItemEnum::Module(module) = &target_module.inner else { return };
184
185        // Add each public item from the target module
186        for child_id in &module.items {
187            // Skip if already seen (handles explicit + glob overlap)
188            if !seen_items.insert(child_id) {
189                continue;
190            }
191
192            let Some(child) = self.ctx.get_item(child_id) else { continue };
193
194            // Respect visibility settings
195            if !self.ctx.should_include_item(child) {
196                continue;
197            }
198
199            // Categorize based on item type
200            match &child.inner {
201                ItemEnum::Module(_) => items.modules.push((child_id, child)),
202                ItemEnum::Struct(_) => items.structs.push((child_id, child)),
203                ItemEnum::Enum(_) => items.enums.push((child_id, child)),
204                ItemEnum::Trait(_) => items.traits.push((child_id, child)),
205                ItemEnum::Function(_) => items.functions.push(child),
206                ItemEnum::Macro(_) => items.macros.push(child),
207                ItemEnum::Constant { .. } => items.constants.push(child),
208                ItemEnum::TypeAlias(_) => items.type_aliases.push(child),
209                _ => {},
210            }
211        }
212    }
213
214    /// Render all item sections in the standard order.
215    fn render_all_sections(&self, md: &mut String, items: &CategorizedItems) {
216        self.render_modules_section(md, &items.modules);
217        self.render_structs_section(md, &items.structs);
218        self.render_enums_section(md, &items.enums);
219        self.render_traits_section(md, &items.traits);
220        self.render_functions_section(md, &items.functions);
221        self.render_macros_section(md, &items.macros);
222        self.render_constants_section(md, &items.constants);
223        self.render_type_aliases_section(md, &items.type_aliases);
224    }
225
226    /// Render the Modules section with links to submodules.
227    fn render_modules_section(&self, md: &mut String, modules: &[(&Id, &Item)]) {
228        if modules.is_empty() {
229            return;
230        }
231
232        md.push_str("## Modules\n\n");
233        for (module_id, module_item) in modules {
234            let module_name = module_item.name.as_deref().unwrap_or("unnamed");
235
236            if let Some(link) = self.ctx.create_link(**module_id, self.current_file) {
237                _ = write!(md, "- {link}");
238            } else {
239                _ = write!(md, "- **`{module_name}`**");
240            }
241
242            if let Some(docs) = &module_item.docs
243                && let Some(first_line) = docs.lines().next()
244            {
245                _ = write!(md, " - {first_line}");
246            }
247            md.push('\n');
248        }
249        md.push('\n');
250    }
251
252    /// Render the Structs section.
253    fn render_structs_section(&self, md: &mut String, structs: &[(&Id, &Item)]) {
254        if structs.is_empty() {
255            return;
256        }
257
258        md.push_str("## Structs\n\n");
259        let renderer = ItemRenderer::new(self.ctx, self.current_file);
260        for (item_id, struct_item) in structs {
261            renderer.render_struct(md, **item_id, struct_item);
262        }
263    }
264
265    /// Render the Enums section.
266    fn render_enums_section(&self, md: &mut String, enums: &[(&Id, &Item)]) {
267        if enums.is_empty() {
268            return;
269        }
270
271        md.push_str("## Enums\n\n");
272        let renderer = ItemRenderer::new(self.ctx, self.current_file);
273        for (item_id, enum_item) in enums {
274            renderer.render_enum(md, **item_id, enum_item);
275        }
276    }
277
278    /// Render the Traits section.
279    fn render_traits_section(&self, md: &mut String, traits: &[(&Id, &Item)]) {
280        if traits.is_empty() {
281            return;
282        }
283
284        md.push_str("## Traits\n\n");
285        let renderer = ItemRenderer::new(self.ctx, self.current_file);
286        for (_item_id, trait_item) in traits {
287            renderer.render_trait(md, trait_item);
288        }
289    }
290
291    /// Render the Functions section.
292    fn render_functions_section(&self, md: &mut String, functions: &[&Item]) {
293        if functions.is_empty() {
294            return;
295        }
296
297        md.push_str("## Functions\n\n");
298        let renderer = ItemRenderer::new(self.ctx, self.current_file);
299        for func_item in functions {
300            renderer.render_function(md, func_item);
301        }
302    }
303
304    /// Render the Macros section.
305    fn render_macros_section(&self, md: &mut String, macros: &[&Item]) {
306        if macros.is_empty() {
307            return;
308        }
309
310        md.push_str("## Macros\n\n");
311        let renderer = ItemRenderer::new(self.ctx, self.current_file);
312        for macro_item in macros {
313            renderer.render_macro(md, macro_item);
314        }
315    }
316
317    /// Render the Constants section.
318    fn render_constants_section(&self, md: &mut String, constants: &[&Item]) {
319        if constants.is_empty() {
320            return;
321        }
322
323        md.push_str("## Constants\n\n");
324        let renderer = ItemRenderer::new(self.ctx, self.current_file);
325        for const_item in constants {
326            renderer.render_constant(md, const_item);
327        }
328    }
329
330    /// Render the Type Aliases section.
331    fn render_type_aliases_section(&self, md: &mut String, type_aliases: &[&Item]) {
332        if type_aliases.is_empty() {
333            return;
334        }
335
336        md.push_str("## Type Aliases\n\n");
337        let renderer = ItemRenderer::new(self.ctx, self.current_file);
338        for alias_item in type_aliases {
339            renderer.render_type_alias(md, alias_item);
340        }
341    }
342}
343
344/// Items categorized by type for organized rendering.
345///
346/// Items are sorted into buckets by their type so they can be rendered
347/// in consistent sections.
348#[derive(Default)]
349struct CategorizedItems<'a> {
350    /// Child modules (need ID for linking).
351    modules: Vec<(&'a Id, &'a Item)>,
352
353    /// Struct definitions (need ID for impl lookup).
354    structs: Vec<(&'a Id, &'a Item)>,
355
356    /// Enum definitions (need ID for impl lookup).
357    enums: Vec<(&'a Id, &'a Item)>,
358
359    /// Trait definitions (need ID for impl lookup).
360    traits: Vec<(&'a Id, &'a Item)>,
361
362    /// Standalone functions.
363    functions: Vec<&'a Item>,
364
365    /// Macro definitions.
366    macros: Vec<&'a Item>,
367
368    /// Constants and statics.
369    constants: Vec<&'a Item>,
370
371    /// Type alias definitions.
372    type_aliases: Vec<&'a Item>,
373}
374
375impl<'a> CategorizedItems<'a> {
376    /// Sort all item categories alphabetically by name for deterministic output.
377    ///
378    /// This ensures consistent ordering regardless of HashMap iteration order
379    /// in the rustdoc JSON index.
380    fn sort(&mut self) {
381        // Helper to get item name for sorting
382        fn item_name(item: &Item) -> &str {
383            item.name.as_deref().unwrap_or("")
384        }
385
386        // Sort items with IDs by name
387        self.modules.sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
388        self.structs.sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
389        self.enums.sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
390        self.traits.sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
391
392        // Sort items without IDs by name
393        self.functions.sort_by(|a, b| item_name(a).cmp(item_name(b)));
394        self.macros.sort_by(|a, b| item_name(a).cmp(item_name(b)));
395        self.constants.sort_by(|a, b| item_name(a).cmp(item_name(b)));
396        self.type_aliases.sort_by(|a, b| item_name(a).cmp(item_name(b)));
397    }
398}