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;
14use crate::generator::quick_ref::{QuickRefEntry, QuickRefGenerator, extract_summary};
15use crate::generator::toc::{TocEntry, TocGenerator};
16use crate::linker::AnchorUtils;
17
18/// Renders a module to markdown.
19///
20/// This struct handles the complete rendering of a module's documentation page,
21/// including:
22/// - Title (Crate or Module heading)
23/// - Module-level documentation
24/// - Sections for each item type (Modules, Structs, Enums, etc.)
25///
26/// The renderer is generic over [`RenderContext`], allowing it to work with
27/// both single-crate (`GeneratorContext`) and multi-crate (`SingleCrateView`) modes.
28pub struct ModuleRenderer<'a> {
29    /// Reference to the render context (either single-crate or multi-crate).
30    ctx: &'a dyn RenderContext,
31
32    /// Path of the current file being generated (for relative link calculation).
33    current_file: &'a str,
34
35    /// Whether this is the crate root module.
36    is_root: bool,
37}
38
39impl<'a> ModuleRenderer<'a> {
40    /// Create a new module renderer.
41    ///
42    /// # Arguments
43    ///
44    /// * `ctx` - Render context (implements `RenderContext` trait)
45    /// * `current_file` - Path of this file (for relative link calculation)
46    /// * `is_root` - True if this is the crate root module
47    pub fn new(ctx: &'a dyn RenderContext, current_file: &'a str, is_root: bool) -> Self {
48        Self {
49            ctx,
50            current_file,
51            is_root,
52        }
53    }
54
55    /// Process documentation string to resolve intra-doc links.
56    ///
57    /// Delegates to the render context's `process_docs` method, which handles
58    /// both single-crate and multi-crate link resolution.
59    fn process_docs(&self, item: &Item) -> Option<String> {
60        self.ctx.process_docs(item, self.current_file)
61    }
62
63    /// Generate the complete markdown content for a module.
64    ///
65    /// # Output Structure
66    ///
67    /// ```markdown
68    /// # Crate `name` (or Module `name`)
69    ///
70    /// [module documentation]
71    ///
72    /// ## Contents (if items exceed threshold)
73    /// - [Structs](#structs)
74    ///   - [`Parser`](#parser)
75    ///
76    /// ## Modules
77    /// - [submodule](link) - first line of docs
78    ///
79    /// ## Structs
80    /// ### `StructName`
81    /// [struct definition and docs]
82    ///
83    /// ## Enums
84    /// ...
85    /// ```
86    #[must_use]
87    pub fn render(&self, item: &Item) -> String {
88        let mut md = String::new();
89
90        let name = item.name.as_deref().unwrap_or("crate");
91
92        // === Title Section ===
93        if self.is_root {
94            _ = write!(md, "# Crate `{name}`\n\n");
95            if let Some(version) = self.ctx.crate_version() {
96                _ = write!(md, "**Version:** {version}\n\n");
97            }
98        } else {
99            _ = write!(md, "# Module `{name}`\n\n");
100        }
101
102        // === Documentation Section ===
103        if let Some(docs) = self.process_docs(item) {
104            _ = write!(md, "{}", &docs);
105            _ = write!(md, "\n\n");
106        }
107
108        // === Module Contents ===
109        if let ItemEnum::Module(module) = &item.inner {
110            let categorized = self.categorize_items(&module.items);
111            let config = self.ctx.render_config();
112
113            // === Table of Contents (if above threshold) ===
114            let toc_gen = TocGenerator::new(config.toc_threshold);
115            let toc_entries = Self::build_toc_entries(&categorized);
116            if let Some(toc) = toc_gen.generate(&toc_entries) {
117                _ = write!(md, "{}", &toc);
118            }
119
120            // === Quick Reference (if enabled) ===
121            if config.quick_reference {
122                let quick_ref_entries = self.build_quick_ref_entries(&categorized);
123                if !quick_ref_entries.is_empty() {
124                    let quick_ref_gen = QuickRefGenerator::new();
125                    _ = write!(md, "{}", &quick_ref_gen.generate(&quick_ref_entries));
126                }
127            }
128
129            self.render_all_sections(&mut md, &categorized);
130        }
131
132        md
133    }
134
135    /// Categorize module items by type for organized rendering.
136    ///
137    /// Items are categorized into groups for structured documentation.
138    /// - Modules (for navigation)
139    /// - Types (structs, enums, unions, type aliases)
140    /// - Traits
141    /// - Functions
142    /// - Constants and statics
143    /// - Macros
144    fn categorize_items(&self, item_ids: &'a [Id]) -> CategorizedItems<'a> {
145        let mut items = CategorizedItems::default();
146        let mut seen_items: HashSet<&Id> = HashSet::new();
147
148        for item_id in item_ids {
149            // Skip: if alredy processed (from glob expansion)
150            if !seen_items.insert(item_id) {
151                continue;
152            }
153
154            if let Some(child) = self.ctx.get_item(item_id) {
155                // Skip: This item should not be included.
156                if !self.ctx.should_include_item(child) {
157                    continue;
158                }
159
160                match &child.inner {
161                    // Navigation
162                    ItemEnum::Module(_) => items.modules.push((item_id, child)),
163
164                    // Types section
165                    ItemEnum::Struct(_) => items.structs.push((item_id, child)),
166                    ItemEnum::Enum(_) => items.enums.push((item_id, child)),
167                    ItemEnum::Union(_) => items.unions.push((item_id, child)),
168                    ItemEnum::TypeAlias(_) => items.type_aliases.push(child),
169
170                    // Other sections
171                    ItemEnum::Trait(_) => items.traits.push((item_id, child)),
172                    ItemEnum::Function(_) => items.functions.push(child),
173                    ItemEnum::Constant { .. } => items.constants.push(child),
174                    ItemEnum::Static(_) => items.statics.push(child),
175                    ItemEnum::Macro(_) => items.macros.push(child),
176
177                    // Handle re-exports
178                    ItemEnum::Use(use_item) => {
179                        // If its a glob re-export
180                        if use_item.is_glob {
181                            // Glob re-export: expand target module's items
182                            self.expand_glob_reexport(&mut items, use_item, &mut seen_items);
183                        } else if let Some(target_id) = &use_item.id
184                            && let Some(target_item) = self.ctx.get_item(target_id)
185                        {
186                            // Specific re-export: categorize by target type
187                            match &target_item.inner {
188                                ItemEnum::Module(_) => items.modules.push((item_id, child)),
189
190                                ItemEnum::Struct(_) => items.structs.push((item_id, child)),
191                                ItemEnum::Enum(_) => items.enums.push((item_id, child)),
192                                ItemEnum::Union(_) => items.unions.push((item_id, child)),
193                                ItemEnum::TypeAlias(_) => items.type_aliases.push(child),
194
195                                ItemEnum::Trait(_) => items.traits.push((item_id, child)),
196                                ItemEnum::Function(_) => items.functions.push(child),
197                                ItemEnum::Constant { .. } => items.constants.push(child),
198                                ItemEnum::Static(_) => items.statics.push(child),
199                                ItemEnum::Macro(_) => items.macros.push(child),
200                                _ => {},
201                            }
202                        }
203                    },
204
205                    _ => {},
206                }
207            }
208        }
209
210        // Sort all categories for deterministic output.
211        // TODO: See if we can move to an ordered data structure by insertion.
212        items.sort();
213
214        items
215    }
216
217    /// Expand a glob re-export by adding all public items from the target module.
218    fn expand_glob_reexport(
219        &self,
220        items: &mut CategorizedItems<'a>,
221        use_item: &rustdoc_types::Use,
222        seen_items: &mut HashSet<&'a Id>,
223    ) {
224        // Get target module ID
225        let Some(target_id) = &use_item.id else {
226            return;
227        };
228
229        // Look up target module
230        let Some(target_module) = self.ctx.get_item(target_id) else {
231            return;
232        };
233
234        // Must be a module
235        let ItemEnum::Module(module) = &target_module.inner else {
236            return;
237        };
238
239        // Add each public item from the target module
240        for child_id in &module.items {
241            // Skip if already seen (handles explicit + glob overlap)
242            if !seen_items.insert(child_id) {
243                continue;
244            }
245
246            let Some(child) = self.ctx.get_item(child_id) else {
247                continue;
248            };
249
250            // Respect visibility settings
251            if !self.ctx.should_include_item(child) {
252                continue;
253            }
254
255            // Categorize based on item type
256            match &child.inner {
257                ItemEnum::Module(_) => items.modules.push((child_id, child)),
258
259                ItemEnum::Struct(_) => items.structs.push((child_id, child)),
260                ItemEnum::Enum(_) => items.enums.push((child_id, child)),
261                ItemEnum::Union(_) => items.unions.push((child_id, child)),
262                ItemEnum::TypeAlias(_) => items.type_aliases.push(child),
263
264                ItemEnum::Trait(_) => items.traits.push((child_id, child)),
265                ItemEnum::Function(_) => items.functions.push(child),
266                ItemEnum::Constant { .. } => items.constants.push(child),
267                ItemEnum::Static(_) => items.statics.push(child),
268                ItemEnum::Macro(_) => items.macros.push(child),
269
270                _ => {},
271            }
272        }
273    }
274
275    /// Render all item sections with horizontal rule separators.
276    ///
277    /// Sections are rendered in this order:
278    /// 1. Modules (navigation, no separator before)
279    /// 2. Types (structs, enums, unions, type aliases)
280    /// 3. Traits
281    /// 4. Functions
282    /// 5. Constants
283    /// 6. Statics
284    /// 7. Macros
285    ///
286    /// Horizontal rules (`---`) are added between major sections for
287    /// visual separation in the rendered output.
288    fn render_all_sections(&self, md: &mut String, items: &CategorizedItems) {
289        // Track if we've rendered any content (for separator logic)
290        // === Modules Section (navigation) ===
291        // No separator before modules - they come first
292        let mut has_content = if items.modules.is_empty() {
293            false
294        } else {
295            self.render_modules_section(md, &items.modules);
296            true
297        };
298
299        // === Types Section (structs, enums, unions, type aliases) ===
300        if items.has_types() {
301            if has_content {
302                _ = writeln!(md, "\n---\n");
303            }
304
305            self.render_types_section(md, items);
306            has_content = true;
307        }
308
309        // === Traits Section ===
310        if !items.traits.is_empty() {
311            if has_content {
312                _ = writeln!(md, "\n---\n");
313            }
314
315            self.render_traits_section(md, &items.traits);
316            has_content = true;
317        }
318
319        // === Functions Section ===
320        if !items.functions.is_empty() {
321            if has_content {
322                _ = writeln!(md, "\n---\n");
323            }
324
325            self.render_functions_section(md, &items.functions);
326            has_content = true;
327        }
328
329        // === Constants Section ===
330        if !items.constants.is_empty() {
331            if has_content {
332                _ = writeln!(md, "\n---\n");
333            }
334
335            self.render_constants_section(md, &items.constants);
336            has_content = true;
337        }
338
339        // === Statics Section ===
340        if !items.statics.is_empty() {
341            if has_content {
342                _ = writeln!(md, "\n---\n");
343            }
344
345            self.render_statics_section(md, &items.statics);
346            has_content = true;
347        }
348
349        // === Macros Section ===
350        if !items.macros.is_empty() {
351            if has_content {
352                _ = writeln!(md, "\n---\n");
353            }
354
355            self.render_macros_section(md, &items.macros);
356        }
357    }
358
359    /// Render the Types section (structs, enums, unions, type aliases).
360    ///
361    /// All type definitions are grouped under a single "Types" heading,
362    /// with each item type rendered in subsections:
363    ///
364    /// ```markdown
365    /// ## Types
366    ///
367    /// ### `MyStruct`
368    /// [struct definition]
369    ///
370    /// ### `MyEnum`
371    /// [enum definition]
372    ///
373    /// ### `MyUnion`
374    /// [union definition]
375    ///
376    /// ### `MyAlias`
377    /// [type alias definition]
378    /// ```
379    fn render_types_section(&self, md: &mut String, items: &CategorizedItems) {
380        _ = write!(md, "## Types\n\n");
381
382        let renderer = ItemRenderer::new(self.ctx, self.current_file);
383
384        // Render structs
385        for (item_id, struct_item) in &items.structs {
386            renderer.render_struct(md, **item_id, struct_item);
387        }
388
389        // Render enums
390        for (item_id, enum_item) in &items.enums {
391            renderer.render_enum(md, **item_id, enum_item);
392        }
393
394        // Render unions
395        for (item_id, union_item) in &items.unions {
396            renderer.render_union(md, **item_id, union_item);
397        }
398
399        // Render type aliases
400        for alias_item in &items.type_aliases {
401            renderer.render_type_alias(md, alias_item);
402        }
403    }
404
405    /// Render the Statics section.
406    fn render_statics_section(&self, md: &mut String, statics: &[&Item]) {
407        if statics.is_empty() {
408            return;
409        }
410
411        _ = write!(md, "## Statics\n\n");
412
413        let renderer = ItemRenderer::new(self.ctx, self.current_file);
414
415        for static_item in statics {
416            renderer.render_static(md, static_item);
417        }
418    }
419
420    /// Build TOC entries from categorized items.
421    ///
422    /// Creates a hierarchical structure for the table of contents:
423    /// - Modules section
424    /// - Types section (with children: structs, enums, unions, type aliases)
425    /// - Traits section
426    /// - Functions section
427    /// - Constants section
428    /// - Statics section
429    /// - Macros section
430    fn build_toc_entries(items: &CategorizedItems) -> Vec<TocEntry> {
431        let mut entries = Vec::new();
432
433        // Helper to create item entries for items with IDs
434        #[expect(clippy::items_after_statements, reason = "Helper function definition")]
435        fn item_entries(items: &[(&Id, &Item)]) -> Vec<TocEntry> {
436            items
437                .iter()
438                .filter_map(|(_, item)| {
439                    let name = item.name.as_deref()?;
440                    Some(TocEntry::new(
441                        format!("`{name}`"),
442                        AnchorUtils::slugify_anchor(name),
443                    ))
444                })
445                .collect()
446        }
447
448        // Helper for items without IDs
449        #[expect(clippy::items_after_statements, reason = "Helper function definition")]
450        fn simple_item_entries(items: &[&Item]) -> Vec<TocEntry> {
451            items
452                .iter()
453                .filter_map(|item| {
454                    let name = item.name.as_deref()?;
455                    Some(TocEntry::new(
456                        format!("`{name}`"),
457                        AnchorUtils::slugify_anchor(name),
458                    ))
459                })
460                .collect()
461        }
462
463        // === Modules ===
464        if !items.modules.is_empty() {
465            entries.push(TocEntry::with_children(
466                "Modules",
467                "modules",
468                item_entries(&items.modules),
469            ));
470        }
471
472        // === Types (combined section) ===
473        if items.has_types() {
474            // Collect all type items as children
475            let mut type_children = Vec::new();
476
477            // Add structs
478            type_children.extend(item_entries(&items.structs));
479
480            // Add enums
481            type_children.extend(item_entries(&items.enums));
482
483            // Add unions
484            type_children.extend(item_entries(&items.unions));
485
486            // Add type aliases
487            type_children.extend(simple_item_entries(&items.type_aliases));
488
489            entries.push(TocEntry::with_children("Types", "types", type_children));
490        }
491
492        // === Traits ===
493        if !items.traits.is_empty() {
494            entries.push(TocEntry::with_children(
495                "Traits",
496                "traits",
497                item_entries(&items.traits),
498            ));
499        }
500
501        // === Functions ===
502        if !items.functions.is_empty() {
503            entries.push(TocEntry::with_children(
504                "Functions",
505                "functions",
506                simple_item_entries(&items.functions),
507            ));
508        }
509
510        // === Constants ===
511        if !items.constants.is_empty() {
512            entries.push(TocEntry::with_children(
513                "Constants",
514                "constants",
515                simple_item_entries(&items.constants),
516            ));
517        }
518
519        // === Statics ===
520        if !items.statics.is_empty() {
521            entries.push(TocEntry::with_children(
522                "Statics",
523                "statics",
524                simple_item_entries(&items.statics),
525            ));
526        }
527
528        // === Macros ===
529        if !items.macros.is_empty() {
530            entries.push(TocEntry::with_children(
531                "Macros",
532                "macros",
533                simple_item_entries(&items.macros),
534            ));
535        }
536
537        entries
538    }
539
540    /// Build quick reference entries from categorized items.
541    ///
542    /// Creates a flat list of entries for the quick reference table,
543    /// including all item types with their names, kinds, and summaries.
544    /// For re-exports, uses the target item's docs when the re-export lacks its own.
545    fn build_quick_ref_entries(&self, items: &CategorizedItems) -> Vec<QuickRefEntry> {
546        let mut entries = Vec::new();
547
548        // Add entries from items with IDs (supports re-export doc fallback)
549        for (id, item) in &items.modules {
550            if let Some(name) = item.name.as_deref() {
551                entries.push(QuickRefEntry::new(
552                    name,
553                    "mod",
554                    AnchorUtils::slugify_anchor(name),
555                    self.get_item_summary(item, **id),
556                ));
557            }
558        }
559
560        for (id, item) in &items.structs {
561            if let Some(name) = item.name.as_deref() {
562                entries.push(QuickRefEntry::new(
563                    name,
564                    "struct",
565                    AnchorUtils::slugify_anchor(name),
566                    self.get_item_summary(item, **id),
567                ));
568            }
569        }
570
571        for (id, item) in &items.enums {
572            if let Some(name) = item.name.as_deref() {
573                entries.push(QuickRefEntry::new(
574                    name,
575                    "enum",
576                    AnchorUtils::slugify_anchor(name),
577                    self.get_item_summary(item, **id),
578                ));
579            }
580        }
581
582        for (id, item) in &items.traits {
583            if let Some(name) = item.name.as_deref() {
584                entries.push(QuickRefEntry::new(
585                    name,
586                    "trait",
587                    AnchorUtils::slugify_anchor(name),
588                    self.get_item_summary(item, **id),
589                ));
590            }
591        }
592
593        // Simple entries (functions, macros, constants, type aliases)
594        // These don't have IDs in categorization, so use direct docs only
595        for item in &items.functions {
596            if let Some(name) = item.name.as_deref() {
597                entries.push(QuickRefEntry::new(
598                    name,
599                    "fn",
600                    AnchorUtils::slugify_anchor(name),
601                    extract_summary(item.docs.as_deref()),
602                ));
603            }
604        }
605
606        for item in &items.macros {
607            if let Some(name) = item.name.as_deref() {
608                entries.push(QuickRefEntry::new(
609                    name,
610                    "macro",
611                    AnchorUtils::slugify_anchor(name),
612                    extract_summary(item.docs.as_deref()),
613                ));
614            }
615        }
616
617        for item in &items.constants {
618            if let Some(name) = item.name.as_deref() {
619                entries.push(QuickRefEntry::new(
620                    name,
621                    "const",
622                    AnchorUtils::slugify_anchor(name),
623                    extract_summary(item.docs.as_deref()),
624                ));
625            }
626        }
627
628        for item in &items.type_aliases {
629            if let Some(name) = item.name.as_deref() {
630                entries.push(QuickRefEntry::new(
631                    name,
632                    "type",
633                    AnchorUtils::slugify_anchor(name),
634                    extract_summary(item.docs.as_deref()),
635                ));
636            }
637        }
638
639        // Add unions (new)
640        for (id, item) in &items.unions {
641            if let Some(name) = item.name.as_deref() {
642                entries.push(QuickRefEntry::new(
643                    name,
644                    "union",
645                    AnchorUtils::slugify_anchor(name),
646                    self.get_item_summary(item, **id),
647                ));
648            }
649        }
650
651        // Add statics (new)
652        for item in &items.statics {
653            if let Some(name) = item.name.as_deref() {
654                entries.push(QuickRefEntry::new(
655                    name,
656                    "static",
657                    AnchorUtils::slugify_anchor(name),
658                    extract_summary(item.docs.as_deref()),
659                ));
660            }
661        }
662
663        entries
664    }
665
666    /// Get summary for an item, with fallback for re-exports.
667    ///
668    /// For re-exports (`ItemEnum::Use`), if the item has no docs, falls back
669    /// to the target item's documentation.
670    fn get_item_summary(&self, item: &Item, item_id: Id) -> String {
671        // First try the item's own docs
672        let summary = extract_summary(item.docs.as_deref());
673        if !summary.is_empty() {
674            return summary;
675        }
676
677        // For re-exports, try to get the target's docs
678        if let ItemEnum::Use(use_item) = &item.inner
679            && let Some(target_id) = &use_item.id
680            && let Some(target_item) = self.ctx.krate().index.get(target_id)
681        {
682            let target_summary = extract_summary(target_item.docs.as_deref());
683            if !target_summary.is_empty() {
684                return target_summary;
685            }
686        }
687
688        // Also check if item_id points to a different item (for non-Use re-exports)
689        if let Some(target_item) = self.ctx.krate().index.get(&item_id)
690            && target_item.id != item.id
691        {
692            let target_summary = extract_summary(target_item.docs.as_deref());
693            if !target_summary.is_empty() {
694                return target_summary;
695            }
696        }
697
698        String::new()
699    }
700
701    /// Render the Modules section with links to submodules.
702    fn render_modules_section(&self, md: &mut String, modules: &[(&Id, &Item)]) {
703        if modules.is_empty() {
704            return;
705        }
706
707        _ = writeln!(md, "## Modules\n");
708
709        for (module_id, module_item) in modules {
710            let module_name = module_item.name.as_deref().unwrap_or("unnamed");
711
712            if let Some(link) = self.ctx.create_link(**module_id, self.current_file) {
713                _ = write!(md, "- {link}");
714            } else {
715                _ = write!(md, "- **`{module_name}`**");
716            }
717
718            // Get summary: try item's own docs, then fall back to target's docs for re-exports
719            let summary = self.get_module_summary(module_item, **module_id);
720
721            if !summary.is_empty() {
722                _ = write!(md, " — {summary}");
723            }
724
725            _ = writeln!(md);
726        }
727
728        md.push('\n');
729    }
730
731    /// Get summary for a module, with fallback for re-exports.
732    fn get_module_summary(&self, item: &Item, item_id: Id) -> String {
733        // First try the item's own docs
734        if let Some(docs) = &item.docs
735            && let Some(first_line) = docs.lines().next()
736            && !first_line.trim().is_empty()
737        {
738            return first_line.to_string();
739        }
740
741        // For re-exports, try to get the target's docs
742        if let ItemEnum::Use(use_item) = &item.inner
743            && let Some(target_id) = &use_item.id
744            && let Some(target_item) = self.ctx.krate().index.get(target_id)
745            && let Some(docs) = &target_item.docs
746            && let Some(first_line) = docs.lines().next()
747        {
748            return first_line.to_string();
749        }
750
751        // Also check if item_id points to a different item (for non-Use re-exports)
752        if let Some(target_item) = self.ctx.krate().index.get(&item_id)
753            && target_item.id != item.id
754            && let Some(docs) = &target_item.docs
755            && let Some(first_line) = docs.lines().next()
756        {
757            return first_line.to_string();
758        }
759
760        String::new()
761    }
762
763    /// Render the Traits section.
764    fn render_traits_section(&self, md: &mut String, traits: &[(&Id, &Item)]) {
765        if traits.is_empty() {
766            return;
767        }
768
769        _ = writeln!(md, "## Traits\n");
770
771        let renderer = ItemRenderer::new(self.ctx, self.current_file);
772
773        for (item_id, trait_item) in traits {
774            renderer.render_trait(md, **item_id, trait_item);
775        }
776    }
777
778    /// Render the Functions section.
779    fn render_functions_section(&self, md: &mut String, functions: &[&Item]) {
780        if functions.is_empty() {
781            return;
782        }
783
784        _ = writeln!(md, "## Functions\n");
785
786        let renderer = ItemRenderer::new(self.ctx, self.current_file);
787
788        for func_item in functions {
789            renderer.render_function(md, func_item);
790        }
791    }
792
793    /// Render the Macros section.
794    fn render_macros_section(&self, md: &mut String, macros: &[&Item]) {
795        if macros.is_empty() {
796            return;
797        }
798
799        _ = writeln!(md, "## Macros\n");
800
801        let renderer = ItemRenderer::new(self.ctx, self.current_file);
802
803        for macro_item in macros {
804            renderer.render_macro(md, macro_item);
805        }
806    }
807
808    /// Render the Constants section.
809    fn render_constants_section(&self, md: &mut String, constants: &[&Item]) {
810        if constants.is_empty() {
811            return;
812        }
813
814        _ = writeln!(md, "## Constants\n");
815
816        let renderer = ItemRenderer::new(self.ctx, self.current_file);
817
818        for const_item in constants {
819            renderer.render_constant(md, const_item);
820        }
821    }
822}
823
824/// Items categorized by type for organized rendering.
825///
826/// Items are sorted into buckets by their type so they can be rendered
827/// in consistent sections. The structure groups related items:
828///
829/// - **Types**: Structs, enums, unions, and type aliases
830/// - **Traits**: Trait definitions
831/// - **Functions**: Standalone functions
832/// - **Constants**: Constants and statics
833/// - **Macros**: Macro definitions
834///
835/// This organization improves navigation by grouping related items together.
836#[derive(Default)]
837struct CategorizedItems<'a> {
838    /// Child modules (need ID for linking).
839    /// Rendered first for navigation purposes.
840    modules: Vec<(&'a Id, &'a Item)>,
841
842    // === Types Section ===
843    // These are grouped under a single "Types" heading in the output.
844    /// Struct definitions (need ID for impl lookup).
845    structs: Vec<(&'a Id, &'a Item)>,
846
847    /// Enum definitions (need ID for impl lookup).
848    enums: Vec<(&'a Id, &'a Item)>,
849
850    /// Union definitions (need ID for impl lookup).
851    unions: Vec<(&'a Id, &'a Item)>,
852
853    /// Type alias definitions.
854    type_aliases: Vec<&'a Item>,
855
856    // === Other Sections ===
857    /// Trait definitions (need ID for impl lookup).
858    traits: Vec<(&'a Id, &'a Item)>,
859
860    /// Standalone functions.
861    functions: Vec<&'a Item>,
862
863    /// Constants.
864    constants: Vec<&'a Item>,
865
866    /// Static variables.
867    statics: Vec<&'a Item>,
868
869    /// Macro definitions.
870    macros: Vec<&'a Item>,
871}
872
873impl CategorizedItems<'_> {
874    /// Check if the Types section has any items.
875    ///
876    /// Returns true if there are any structs, enums, unions, or type aliases.
877    pub const fn has_types(&self) -> bool {
878        !self.structs.is_empty()
879            || !self.enums.is_empty()
880            || !self.unions.is_empty()
881            || !self.type_aliases.is_empty()
882    }
883
884    /// Sort all item categories alphabetically by name for deterministic output.
885    ///
886    /// This ensures consistent ordering regardless of `HashMap` iteration order
887    /// in the rustdoc JSON index.
888    fn sort(&mut self) {
889        // Helper to get item name for sorting
890        fn item_name(item: &Item) -> &str {
891            item.name.as_deref().unwrap_or("")
892        }
893
894        // Sort items with IDs by name
895        self.modules
896            .sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
897        self.structs
898            .sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
899        self.enums
900            .sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
901        self.unions
902            .sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
903        self.traits
904            .sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
905
906        // Sort items without IDs by name
907        self.type_aliases
908            .sort_by(|a, b| item_name(a).cmp(item_name(b)));
909        self.functions
910            .sort_by(|a, b| item_name(a).cmp(item_name(b)));
911        self.constants
912            .sort_by(|a, b| item_name(a).cmp(item_name(b)));
913        self.statics.sort_by(|a, b| item_name(a).cmp(item_name(b)));
914        self.macros.sort_by(|a, b| item_name(a).cmp(item_name(b)));
915    }
916}