cargo_docs_md/multi_crate/
generator.rs

1//! Multi-crate documentation generator.
2//!
3//! This module provides [`MultiCrateGenerator`] which orchestrates
4//! documentation generation across multiple crates with cross-crate linking.
5
6use std::collections::{HashMap, HashSet};
7use std::fmt::Write;
8use std::path::Path;
9use std::sync::Arc;
10
11use fs_err as fs;
12use indicatif::{ProgressBar, ProgressStyle};
13use rayon::prelude::*;
14use rustdoc_types::{Id, Item, ItemEnum, StructKind};
15use tracing::{debug, info, info_span, instrument};
16
17use crate::Args;
18use crate::error::Error;
19use crate::generator::breadcrumbs::BreadcrumbGenerator;
20use crate::generator::impls::is_blanket_impl;
21use crate::generator::render_shared::{
22    append_docs, impl_sort_key, render_constant_definition, render_enum_definition,
23    render_enum_variants_docs, render_function_definition, render_impl_items, render_macro_heading,
24    render_struct_definition, render_struct_fields, render_trait_definition, render_trait_item,
25    render_type_alias_definition,
26};
27use crate::generator::{ItemFilter, LinkResolver};
28use crate::multi_crate::context::SingleCrateView;
29use crate::multi_crate::search::SearchIndexGenerator;
30use crate::multi_crate::summary::SummaryGenerator;
31use crate::multi_crate::{CrateCollection, MultiCrateContext};
32use crate::types::TypeRenderer;
33
34/// Generator for multi-crate documentation.
35///
36/// Produces a directory structure with one subdirectory per crate,
37/// each containing nested markdown files with cross-crate linking.
38///
39/// # Output Structure
40///
41/// ```text
42/// output/
43/// ├── tracing/
44/// │   ├── index.md
45/// │   └── span/
46/// │       └── index.md
47/// ├── tracing_core/
48/// │   ├── index.md
49/// │   └── subscriber/
50/// │       └── index.md
51/// └── SUMMARY.md        # If --mdbook enabled
52/// ```
53pub struct MultiCrateGenerator<'a> {
54    /// Multi-crate context with unified registry.
55    ctx: MultiCrateContext<'a>,
56
57    /// CLI arguments.
58    args: &'a Args,
59}
60
61impl<'a> MultiCrateGenerator<'a> {
62    /// Create a new multi-crate generator.
63    ///
64    /// # Arguments
65    ///
66    /// * `crates` - Collection of parsed crates
67    /// * `args` - CLI arguments
68    #[must_use]
69    pub fn new(crates: &'a CrateCollection, args: &'a Args) -> Self {
70        let ctx = MultiCrateContext::new(crates, args);
71        Self { ctx, args }
72    }
73
74    /// Generate documentation for all crates.
75    ///
76    /// Creates the output directory structure, generates docs for each crate
77    /// in parallel using rayon, and optionally generates SUMMARY.md for
78    /// mdBook compatibility.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if any file operation fails.
83    #[instrument(skip(self), fields(
84        crate_count = self.ctx.crates().names().len(),
85        output = %self.args.output.display(),
86        mdbook = self.args.mdbook,
87        search_index = self.args.search_index
88    ))]
89    pub fn generate(&self) -> Result<(), Error> {
90        info!("Starting multi-crate documentation generation");
91
92        // Create output directory
93        fs::create_dir_all(&self.args.output).map_err(Error::CreateDir)?;
94        debug!(path = %self.args.output.display(), "Created output directory");
95
96        // Pre-create crate directories to avoid race conditions in parallel generation
97        for crate_name in self.ctx.crates().names() {
98            let crate_dir = self.args.output.join(crate_name);
99            fs::create_dir_all(&crate_dir).map_err(Error::CreateDir)?;
100        }
101
102        // Count total modules across all crates for progress bar
103        let total_modules: usize = self
104            .ctx
105            .crates()
106            .iter()
107            .filter_map(|(name, _)| self.ctx.single_crate_view(name))
108            .map(|view| view.count_modules() + 1)
109            .sum();
110
111        debug!(total_modules, "Total modules to generate");
112        let progress = Arc::new(Self::create_progress_bar(total_modules)?);
113
114        // Generate crates in parallel
115        self.ctx
116            .crates()
117            .names()
118            .par_iter()
119            .try_for_each(|crate_name| {
120                let span = info_span!("generate_crate", crate_name);
121                let _guard = span.enter();
122
123                let view = self
124                    .ctx
125                    .single_crate_view(crate_name)
126                    .ok_or_else(|| Error::ItemNotFound((*crate_name).clone()))?;
127
128                self.generate_crate(&view, &progress)
129            })?;
130
131        // Generate SUMMARY.md if requested (sequential - single file)
132        if self.args.mdbook {
133            info!("Generating SUMMARY.md for mdBook");
134            progress.set_message("Generating SUMMARY.md...");
135            let summary_gen = SummaryGenerator::new(
136                self.ctx.crates(),
137                &self.args.output,
138                !self.args.exclude_private,
139            );
140            summary_gen.generate()?;
141        }
142
143        // Generate search index if requested (sequential - single file)
144        if self.args.search_index {
145            info!("Generating search_index.json");
146            progress.set_message("Generating search_index.json...");
147
148            // Collect the IDs of all rendered items to filter the search index
149            let rendered_items = self.collect_rendered_items();
150
151            let search_gen = SearchIndexGenerator::new(
152                self.ctx.crates(),
153                !self.args.exclude_private,
154                rendered_items,
155            );
156            search_gen
157                .write(&self.args.output)
158                .map_err(Error::FileWrite)?;
159        }
160
161        progress.finish_with_message("Done!");
162        info!("Multi-crate documentation generation complete");
163        Ok(())
164    }
165
166    /// Collect the IDs of all items that would be rendered.
167    ///
168    /// This walks the module tree for each crate using the same visibility
169    /// rules as rendering, collecting the IDs of items that will have
170    /// documentation generated for them.
171    fn collect_rendered_items(&self) -> HashMap<String, HashSet<Id>> {
172        let mut result = HashMap::new();
173
174        for crate_name in self.ctx.crates().names() {
175            if let Some(view) = self.ctx.single_crate_view(crate_name) {
176                let mut ids = HashSet::new();
177                Self::collect_crate_items(&view, &mut ids);
178                result.insert(crate_name.clone(), ids);
179            }
180        }
181
182        result
183    }
184
185    /// Collect rendered item IDs for a single crate.
186    fn collect_crate_items(view: &SingleCrateView, ids: &mut HashSet<Id>) {
187        let krate = view.krate();
188
189        // Get root item
190        let Some(root_item) = krate.index.get(&krate.root) else {
191            return;
192        };
193
194        // Collect root module items
195        Self::collect_module_items(view, root_item, ids);
196    }
197
198    /// Recursively collect rendered item IDs from a module.
199    fn collect_module_items(view: &SingleCrateView, item: &Item, ids: &mut HashSet<Id>) {
200        let krate = view.krate();
201
202        if let ItemEnum::Module(module) = &item.inner {
203            for item_id in &module.items {
204                if let Some(child) = krate.index.get(item_id) {
205                    if !view.should_include_item(child) {
206                        continue;
207                    }
208
209                    match &child.inner {
210                        // Documentable items - add their IDs
211                        ItemEnum::Struct(_)
212                        | ItemEnum::Enum(_)
213                        | ItemEnum::Trait(_)
214                        | ItemEnum::Function(_)
215                        | ItemEnum::TypeAlias(_)
216                        | ItemEnum::Constant { .. }
217                        | ItemEnum::Macro(_) => {
218                            ids.insert(*item_id);
219                        },
220
221                        // Modules - add ID and recurse
222                        ItemEnum::Module(_) => {
223                            ids.insert(*item_id);
224                            Self::collect_module_items(view, child, ids);
225                        },
226
227                        // Re-exports - add the Use item ID (not the target)
228                        ItemEnum::Use(use_item) if !use_item.is_glob => {
229                            // Verify target exists (same logic as rendering)
230                            let target_exists =
231                                use_item.id.as_ref().is_some_and(|target_id| {
232                                    krate.index.contains_key(target_id)
233                                        || view.lookup_item_across_crates(target_id).is_some()
234                                }) || view.resolve_external_path(&use_item.source).is_some();
235
236                            if target_exists {
237                                ids.insert(*item_id);
238                            }
239                        },
240
241                        _ => {},
242                    }
243                }
244            }
245        }
246    }
247
248    /// Generate documentation for a single crate.
249    #[instrument(skip(self, view, progress), fields(crate_name = %view.crate_name()))]
250    fn generate_crate(
251        &self,
252        view: &SingleCrateView,
253        progress: &Arc<ProgressBar>,
254    ) -> Result<(), Error> {
255        debug!("Starting crate generation");
256
257        let crate_name = view.crate_name();
258        let crate_dir = self.args.output.join(crate_name);
259
260        // Crate directory already created in generate() to avoid race conditions
261
262        // Get root item
263        let root_item = view
264            .krate()
265            .index
266            .get(&view.krate().root)
267            .ok_or_else(|| Error::ItemNotFound(view.krate().root.0.to_string()))?;
268
269        // Generate root index.md
270        let file_path = format!("{crate_name}/index.md");
271        let renderer = MultiCrateModuleRenderer::new(view, &file_path, true);
272        let content = renderer.render(root_item);
273
274        let index_path = crate_dir.join("index.md");
275        fs::write(&index_path, content).map_err(Error::FileWrite)?;
276        progress.inc(1);
277
278        // Generate submodules
279        if let ItemEnum::Module(module) = &root_item.inner {
280            for item_id in &module.items {
281                if let Some(item) = view.krate().index.get(item_id)
282                    && let ItemEnum::Module(_) = &item.inner
283                    && view.should_include_item(item)
284                {
285                    Self::generate_module(view, item, &crate_dir, vec![], &Arc::clone(progress))?;
286                }
287            }
288        }
289
290        debug!("Crate generation complete");
291        Ok(())
292    }
293
294    /// Generate a module directory with index.md and child modules.
295    fn generate_module(
296        view: &SingleCrateView,
297        item: &Item,
298        parent_dir: &Path,
299        module_path: Vec<String>,
300        progress: &Arc<ProgressBar>,
301    ) -> Result<(), Error> {
302        let name = item.name.as_deref().unwrap_or("unnamed");
303
304        // Create module directory
305        let module_dir = parent_dir.join(name);
306        fs::create_dir_all(&module_dir).map_err(Error::CreateDir)?;
307
308        // Build module path for file and breadcrumbs
309        let mut current_path = module_path;
310        current_path.push(name.to_string());
311
312        // File path relative to output root (includes crate name)
313        let file_path = format!("{}/{}/index.md", view.crate_name(), current_path.join("/"));
314
315        // Generate breadcrumbs
316        let breadcrumb_gen = BreadcrumbGenerator::new(&current_path, view.crate_name());
317        let breadcrumbs = breadcrumb_gen.generate();
318
319        // Generate module content
320        let renderer = MultiCrateModuleRenderer::new(view, &file_path, false);
321        let module_content = renderer.render(item);
322
323        // Combine breadcrumbs + content
324        let content = format!("{breadcrumbs}{module_content}");
325
326        // Write index.md
327        let file_path_on_disk = module_dir.join("index.md");
328        fs::write(&file_path_on_disk, content).map_err(Error::FileWrite)?;
329        progress.inc(1);
330
331        // Recurse into child modules
332        if let ItemEnum::Module(module) = &item.inner {
333            for sub_id in &module.items {
334                if let Some(sub_item) = view.krate().index.get(sub_id)
335                    && let ItemEnum::Module(_) = &sub_item.inner
336                    && view.should_include_item(sub_item)
337                {
338                    Self::generate_module(
339                        view,
340                        sub_item,
341                        &module_dir,
342                        current_path.clone(),
343                        &Arc::clone(progress),
344                    )?;
345                }
346            }
347        }
348
349        Ok(())
350    }
351
352    /// Create a progress bar.
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if the progress bar template is invalid.
357    fn create_progress_bar(total: usize) -> Result<ProgressBar, Error> {
358        let progress = ProgressBar::new(total as u64);
359
360        let style =
361            ProgressStyle::with_template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
362                .map_err(Error::ProgressBarTemplate)?
363                .progress_chars("=>-");
364
365        progress.set_style(style);
366        Ok(progress)
367    }
368}
369
370/// Module renderer for multi-crate context.
371///
372/// Wraps the standard module rendering with multi-crate link resolution.
373///
374/// This renderer handles special cases that aren't covered by the standard
375/// `ModuleRenderer`, particularly re-exports (`pub use`) which need to
376/// resolve items across crate boundaries.
377struct MultiCrateModuleRenderer<'a> {
378    /// Single-crate view for this crate (implements `RenderContext`).
379    view: &'a SingleCrateView<'a>,
380
381    /// Current file path for link resolution.
382    file_path: &'a str,
383
384    /// Whether this is the crate root.
385    is_root: bool,
386
387    /// Cached type renderer to avoid repeated construction.
388    type_renderer: TypeRenderer<'a>,
389}
390
391impl<'a> MultiCrateModuleRenderer<'a> {
392    /// Create a new multi-crate module renderer.
393    const fn new(view: &'a SingleCrateView<'a>, file_path: &'a str, is_root: bool) -> Self {
394        Self {
395            view,
396            file_path,
397            is_root,
398            type_renderer: TypeRenderer::new(view.krate()),
399        }
400    }
401
402    /// Render a module item to markdown.
403    fn render(&self, item: &Item) -> String {
404        let mut md = String::new();
405
406        // Module title
407        let name = item.name.as_deref().unwrap_or("unnamed");
408        if self.is_root {
409            _ = writeln!(md, "# Crate `{name}`\n");
410        } else {
411            _ = writeln!(md, "# Module `{name}`\n");
412        }
413
414        // Module documentation - use RenderContext trait method
415        if let Some(docs) = self.view.process_docs(item, self.file_path) {
416            _ = writeln!(md, "{docs}\n");
417        }
418
419        // Module contents
420        if let ItemEnum::Module(module) = &item.inner {
421            self.render_module_contents(&mut md, module, item);
422        }
423
424        md
425    }
426
427    /// Render module contents (items, types, functions, etc.).
428    fn render_module_contents(
429        &self,
430        md: &mut String,
431        module: &rustdoc_types::Module,
432        _parent: &Item,
433    ) {
434        let krate = self.view.krate();
435        let mut seen_items: HashSet<Id> = HashSet::new();
436
437        // Collect items by category (with IDs for impl block rendering)
438        let mut modules: Vec<&Item> = Vec::new();
439        let mut structs: Vec<(&Id, &Item)> = Vec::new();
440        let mut enums: Vec<(&Id, &Item)> = Vec::new();
441        let mut traits: Vec<&Item> = Vec::new();
442        let mut functions: Vec<&Item> = Vec::new();
443        let mut types: Vec<&Item> = Vec::new();
444        let mut constants: Vec<&Item> = Vec::new();
445        let mut macros: Vec<&Item> = Vec::new();
446
447        for item_id in &module.items {
448            // Skip if already processed (from glob expansion)
449            if !seen_items.insert(*item_id) {
450                continue;
451            }
452
453            if let Some(item) = krate.index.get(item_id) {
454                if !self.view.should_include_item(item) {
455                    continue;
456                }
457
458                match &item.inner {
459                    ItemEnum::Module(_) => modules.push(item),
460
461                    ItemEnum::Struct(_) => structs.push((item_id, item)),
462
463                    ItemEnum::Enum(_) => enums.push((item_id, item)),
464
465                    ItemEnum::Trait(_) => traits.push(item),
466
467                    ItemEnum::Function(_) => functions.push(item),
468
469                    ItemEnum::TypeAlias(_) => types.push(item),
470
471                    ItemEnum::Constant { .. } => constants.push(item),
472
473                    ItemEnum::Macro(_) => macros.push(item),
474
475                    // Handle re-exports
476                    ItemEnum::Use(use_item) => {
477                        if use_item.is_glob {
478                            // Glob re-export: expand target module's items
479                            self.expand_glob_reexport(
480                                &mut modules,
481                                &mut structs,
482                                &mut enums,
483                                &mut traits,
484                                &mut functions,
485                                &mut types,
486                                &mut constants,
487                                &mut macros,
488                                use_item,
489                                &mut seen_items,
490                            );
491                        } else {
492                            // Specific re-export: resolve target and categorize
493                            let target_item = use_item.id.as_ref().map_or_else(
494                                || {
495                                    // No ID (external re-export) - resolve by path
496                                    self.view
497                                        .resolve_external_path(&use_item.source)
498                                        .map(|(_, item, _)| item)
499                                },
500                                |target_id| {
501                                    // Has ID - try local crate first, then search all crates
502                                    krate.index.get(target_id).or_else(|| {
503                                        self.view
504                                            .lookup_item_across_crates(target_id)
505                                            .map(|(_, item)| item)
506                                    })
507                                },
508                            );
509
510                            if let Some(target_item) = target_item {
511                                match &target_item.inner {
512                                    ItemEnum::Module(_) => modules.push(item),
513
514                                    ItemEnum::Struct(_) => structs.push((item_id, item)),
515
516                                    ItemEnum::Enum(_) => enums.push((item_id, item)),
517
518                                    ItemEnum::Trait(_) => traits.push(item),
519
520                                    ItemEnum::Function(_) => functions.push(item),
521
522                                    ItemEnum::TypeAlias(_) => types.push(item),
523
524                                    ItemEnum::Constant { .. } => constants.push(item),
525
526                                    ItemEnum::Macro(_) => macros.push(item),
527
528                                    _ => {},
529                                }
530                            }
531                        }
532                    },
533                    _ => {},
534                }
535            }
536        }
537
538        // Check if crate/module is empty
539        let is_empty = modules.is_empty()
540            && structs.is_empty()
541            && enums.is_empty()
542            && traits.is_empty()
543            && functions.is_empty()
544            && types.is_empty()
545            && constants.is_empty()
546            && macros.is_empty();
547
548        if is_empty && self.is_root {
549            // Empty crate - likely a proc-macro or re-export crate
550            let crate_name = self.view.crate_name();
551            if crate_name.ends_with("_derive") || crate_name.ends_with("-derive") {
552                // Derive macro crate - try to link to parent crate
553                let parent_crate = crate_name
554                    .strip_suffix("_derive")
555                    .or_else(|| crate_name.strip_suffix("-derive"))
556                    .unwrap_or(crate_name);
557
558                _ = writeln!(md, "## Overview\n");
559                _ = writeln!(
560                    md,
561                    "This is a **procedural macro crate** that provides derive macros."
562                );
563                _ = writeln!(md);
564                _ = writeln!(
565                    md,
566                    "The macros from this crate are typically re-exported from the parent crate \
567                     [`{parent_crate}`](../{parent_crate}/index.md) for convenience. \
568                     You should generally depend on the parent crate rather than this one directly."
569                );
570                _ = writeln!(md);
571                _ = writeln!(md, "### Usage\n");
572                _ = writeln!(md, "```toml");
573                _ = writeln!(md, "[dependencies]");
574                _ = writeln!(
575                    md,
576                    "{parent_crate} = {{ version = \"*\", features = [\"derive\"] }}"
577                );
578                _ = writeln!(md, "```");
579            } else if crate_name.ends_with("_impl") || crate_name.ends_with("-impl") {
580                let parent_crate = crate_name
581                    .strip_suffix("_impl")
582                    .or_else(|| crate_name.strip_suffix("-impl"))
583                    .unwrap_or(crate_name);
584
585                _ = writeln!(md, "## Overview\n");
586                _ = writeln!(
587                    md,
588                    "This is an **implementation detail crate** with no public API."
589                );
590                _ = writeln!(md);
591                _ = writeln!(
592                    md,
593                    "The functionality from this crate is re-exported through \
594                     [`{parent_crate}`](../{parent_crate}/index.md). \
595                     You should depend on the parent crate instead."
596                );
597            } else {
598                _ = writeln!(md, "*This crate has no public items to document.*");
599            }
600            return;
601        }
602
603        // Render sections with full detail
604        Self::render_modules_section(md, &modules);
605        self.render_structs_section(md, &structs);
606        self.render_enums_section(md, &enums);
607        self.render_traits_section(md, &traits);
608        self.render_functions_section(md, &functions);
609        self.render_type_aliases_section(md, &types);
610        self.render_constants_section(md, &constants);
611        self.render_macros_section(md, &macros);
612    }
613
614    /// Render modules section (links to subdirectories).
615    fn render_modules_section(md: &mut String, modules: &[&Item]) {
616        if modules.is_empty() {
617            return;
618        }
619
620        _ = writeln!(md, "## Modules\n");
621
622        for item in modules {
623            let (name, summary) = Self::get_item_name_and_summary(item);
624            _ = writeln!(md, "- [`{name}`]({name}/index.md) - {summary}");
625        }
626
627        _ = writeln!(md);
628    }
629
630    /// Render structs section with full detail.
631    fn render_structs_section(&self, md: &mut String, structs: &[(&Id, &Item)]) {
632        if structs.is_empty() {
633            return;
634        }
635
636        _ = writeln!(md, "## Structs\n");
637
638        for (item_id, item) in structs {
639            self.render_struct(md, **item_id, item);
640        }
641    }
642
643    /// Render enums section with full detail.
644    fn render_enums_section(&self, md: &mut String, enums: &[(&Id, &Item)]) {
645        if enums.is_empty() {
646            return;
647        }
648
649        _ = writeln!(md, "## Enums\n");
650
651        for (item_id, item) in enums {
652            self.render_enum(md, **item_id, item);
653        }
654    }
655
656    /// Render traits section with full detail.
657    fn render_traits_section(&self, md: &mut String, traits: &[&Item]) {
658        if traits.is_empty() {
659            return;
660        }
661
662        _ = writeln!(md, "## Traits\n");
663
664        for item in traits {
665            self.render_trait(md, item);
666        }
667    }
668
669    /// Render functions section with full detail.
670    fn render_functions_section(&self, md: &mut String, functions: &[&Item]) {
671        if functions.is_empty() {
672            return;
673        }
674
675        _ = writeln!(md, "## Functions\n");
676
677        for item in functions {
678            self.render_function(md, item);
679        }
680    }
681
682    /// Render type aliases section with full detail.
683    fn render_type_aliases_section(&self, md: &mut String, types: &[&Item]) {
684        if types.is_empty() {
685            return;
686        }
687
688        _ = writeln!(md, "## Type Aliases\n");
689
690        for item in types {
691            self.render_type_alias(md, item);
692        }
693    }
694
695    /// Render constants section with full detail.
696    fn render_constants_section(&self, md: &mut String, constants: &[&Item]) {
697        if constants.is_empty() {
698            return;
699        }
700
701        _ = writeln!(md, "## Constants\n");
702
703        for item in constants {
704            self.render_constant(md, item);
705        }
706    }
707
708    /// Render macros section with full detail.
709    fn render_macros_section(&self, md: &mut String, macros: &[&Item]) {
710        if macros.is_empty() {
711            return;
712        }
713
714        _ = writeln!(md, "## Macros\n");
715
716        for item in macros {
717            self.render_macro(md, item);
718        }
719    }
720
721    /// Get name and summary for an item, handling re-exports.
722    fn get_item_name_and_summary(item: &Item) -> (String, String) {
723        if let ItemEnum::Use(use_item) = &item.inner {
724            // For re-exports, the name is always from the Use item
725            let name = use_item.name.clone();
726            let docs = item.docs.as_deref().unwrap_or("");
727            let summary = docs.lines().next().unwrap_or("").to_string();
728            (name, summary)
729        } else {
730            let name = item.name.clone().unwrap_or_else(|| "unnamed".to_string());
731            let docs = item.docs.as_deref().unwrap_or("");
732            let summary = docs.lines().next().unwrap_or("").to_string();
733            (name, summary)
734        }
735    }
736
737    /// Render a struct definition to markdown.
738    fn render_struct(&self, md: &mut String, item_id: Id, item: &Item) {
739        let current_krate = self.view.krate();
740
741        // Handle re-exports: use the target item for rendering
742        // source_crate_name is set when the target is from another crate
743        let (name, actual_item, actual_id, source_crate_name): (&str, &Item, Id, Option<&str>) =
744            if let ItemEnum::Use(use_item) = &item.inner {
745                let name = use_item.name.as_str();
746
747                if let Some(ref target_id) = use_item.id {
748                    // Has ID - try local crate first, then search all crates
749                    if let Some(target) = current_krate.index.get(target_id) {
750                        // Found in local crate
751                        (name, target, *target_id, None)
752                    } else if let Some((src_crate, target)) =
753                        self.view.lookup_item_across_crates(target_id)
754                    {
755                        // Found in another crate - capture the source crate name
756                        let is_external = src_crate != self.view.crate_name();
757                        (
758                            name,
759                            target,
760                            *target_id,
761                            if is_external { Some(src_crate) } else { None },
762                        )
763                    } else {
764                        return;
765                    }
766                } else {
767                    // No ID - try to resolve by path (external re-export)
768                    if let Some((src_crate, target, target_id)) =
769                        self.view.resolve_external_path(&use_item.source)
770                    {
771                        (name, target, target_id, Some(src_crate))
772                    } else {
773                        return;
774                    }
775                }
776            } else {
777                (
778                    item.name.as_deref().unwrap_or("unnamed"),
779                    item,
780                    item_id,
781                    None,
782                )
783            };
784
785        if let ItemEnum::Struct(s) = &actual_item.inner {
786            // Get the appropriate crate context for rendering
787            // Use source crate for field lookups if this is a cross-crate re-export
788            let render_krate = source_crate_name
789                .and_then(|name| self.view.get_crate(name))
790                .unwrap_or(current_krate);
791
792            // Create TypeRenderer for the appropriate crate
793            let type_renderer = if source_crate_name.is_some() {
794                TypeRenderer::new(render_krate)
795            } else {
796                // Use cached renderer for local items
797                self.type_renderer
798            };
799
800            // Struct definition (heading + code block)
801            render_struct_definition(md, name, s, render_krate, &type_renderer);
802
803            // Add re-export annotation for external re-exports
804            if let Some(src_crate) = source_crate_name {
805                _ = writeln!(md, "*Re-exported from `{src_crate}`*\n");
806            }
807
808            // Documentation
809            append_docs(md, self.view.process_docs(actual_item, self.file_path));
810
811            // Fields documentation - use source crate for field type lookups
812            if let StructKind::Plain { fields, .. } = &s.kind {
813                render_struct_fields(md, fields, render_krate, &type_renderer, |field| {
814                    self.view.process_docs(field, self.file_path)
815                });
816            }
817
818            // Impl blocks - pass source crate for cross-crate re-exports
819            self.render_impl_blocks(md, actual_id, source_crate_name);
820        }
821    }
822
823    /// Render an enum definition to markdown.
824    fn render_enum(&self, md: &mut String, item_id: Id, item: &Item) {
825        let current_krate = self.view.krate();
826
827        // Handle re-exports: use the target item for rendering
828        // source_crate_name is set when the target is from another crate
829        let (name, actual_item, actual_id, source_crate_name): (&str, &Item, Id, Option<&str>) =
830            if let ItemEnum::Use(use_item) = &item.inner {
831                let name = use_item.name.as_str();
832
833                if let Some(ref target_id) = use_item.id {
834                    // Has ID - try local crate first, then search all crates
835                    if let Some(target) = current_krate.index.get(target_id) {
836                        // Found in local crate
837                        (name, target, *target_id, None)
838                    } else if let Some((src_crate, target)) =
839                        self.view.lookup_item_across_crates(target_id)
840                    {
841                        // Found in another crate - capture the source crate name
842                        let is_external = src_crate != self.view.crate_name();
843                        (
844                            name,
845                            target,
846                            *target_id,
847                            if is_external { Some(src_crate) } else { None },
848                        )
849                    } else {
850                        return;
851                    }
852                } else {
853                    // No ID - try to resolve by path (external re-export)
854                    if let Some((src_crate, target, target_id)) =
855                        self.view.resolve_external_path(&use_item.source)
856                    {
857                        (name, target, target_id, Some(src_crate))
858                    } else {
859                        return;
860                    }
861                }
862            } else {
863                (
864                    item.name.as_deref().unwrap_or("unnamed"),
865                    item,
866                    item_id,
867                    None,
868                )
869            };
870
871        if let ItemEnum::Enum(e) = &actual_item.inner {
872            // Get the appropriate crate context for rendering
873            // Use source crate for variant lookups if this is a cross-crate re-export
874            let render_krate = source_crate_name
875                .and_then(|name| self.view.get_crate(name))
876                .unwrap_or(current_krate);
877
878            // Create TypeRenderer for the appropriate crate
879            let type_renderer = if source_crate_name.is_some() {
880                TypeRenderer::new(render_krate)
881            } else {
882                // Use cached renderer for local items
883                self.type_renderer
884            };
885
886            // Enum definition (heading + code block with variants)
887            render_enum_definition(md, name, e, render_krate, &type_renderer);
888
889            // Add re-export annotation for external re-exports
890            if let Some(src_crate) = source_crate_name {
891                _ = writeln!(md, "*Re-exported from `{src_crate}`*\n");
892            }
893
894            // Documentation
895            append_docs(md, self.view.process_docs(actual_item, self.file_path));
896
897            // Variants documentation - use source crate for variant type lookups
898            render_enum_variants_docs(md, &e.variants, render_krate, |variant| {
899                self.view.process_docs(variant, self.file_path)
900            });
901
902            // Impl blocks - pass source crate for cross-crate re-exports
903            self.render_impl_blocks(md, actual_id, source_crate_name);
904        }
905    }
906
907    /// Render a trait definition to markdown.
908    fn render_trait(&self, md: &mut String, item: &Item) {
909        let krate = self.view.krate();
910        let name = item.name.as_deref().unwrap_or("unnamed");
911
912        if let ItemEnum::Trait(t) = &item.inner {
913            // Trait definition (heading + code block)
914            render_trait_definition(md, name, t, &self.type_renderer);
915
916            // Documentation
917            append_docs(md, self.view.process_docs(item, self.file_path));
918
919            // Required methods section
920            if !t.items.is_empty() {
921                md.push_str("#### Required Methods\n\n");
922
923                for method_id in &t.items {
924                    if let Some(method) = krate.index.get(method_id) {
925                        render_trait_item(md, method, &self.type_renderer, |m| {
926                            self.view.process_docs(m, self.file_path)
927                        });
928                    }
929                }
930            }
931        }
932    }
933
934    /// Render a function definition to markdown.
935    fn render_function(&self, md: &mut String, item: &Item) {
936        let name = item.name.as_deref().unwrap_or("unnamed");
937
938        if let ItemEnum::Function(f) = &item.inner {
939            render_function_definition(md, name, f, &self.type_renderer);
940        }
941
942        append_docs(md, self.view.process_docs(item, self.file_path));
943    }
944
945    /// Render a constant definition to markdown.
946    fn render_constant(&self, md: &mut String, item: &Item) {
947        let name = item.name.as_deref().unwrap_or("unnamed");
948
949        if let ItemEnum::Constant { type_, const_ } = &item.inner {
950            render_constant_definition(md, name, type_, const_, &self.type_renderer);
951        }
952
953        append_docs(md, self.view.process_docs(item, self.file_path));
954    }
955
956    /// Render a type alias to markdown.
957    fn render_type_alias(&self, md: &mut String, item: &Item) {
958        let name = item.name.as_deref().unwrap_or("unnamed");
959
960        if let ItemEnum::TypeAlias(ta) = &item.inner {
961            render_type_alias_definition(md, name, ta, &self.type_renderer);
962        }
963
964        append_docs(md, self.view.process_docs(item, self.file_path));
965    }
966
967    /// Render a macro to markdown.
968    fn render_macro(&self, md: &mut String, item: &Item) {
969        let name = item.name.as_deref().unwrap_or("unnamed");
970        render_macro_heading(md, name);
971        append_docs(md, self.view.process_docs(item, self.file_path));
972    }
973
974    /// Expand a glob re-export into the category vectors.
975    #[allow(clippy::too_many_arguments)]
976    fn expand_glob_reexport<'b>(
977        &self,
978        modules: &mut Vec<&'b Item>,
979        structs: &mut Vec<(&'b Id, &'b Item)>,
980        enums: &mut Vec<(&'b Id, &'b Item)>,
981        traits: &mut Vec<&'b Item>,
982        functions: &mut Vec<&'b Item>,
983        types: &mut Vec<&'b Item>,
984        constants: &mut Vec<&'b Item>,
985        macros: &mut Vec<&'b Item>,
986        use_item: &rustdoc_types::Use,
987        seen_items: &mut HashSet<Id>,
988    ) where
989        'a: 'b,
990    {
991        let krate = self.view.krate();
992
993        let Some(target_id) = &use_item.id else {
994            return;
995        };
996        let Some(target_module) = krate.index.get(target_id) else {
997            return;
998        };
999        let ItemEnum::Module(module) = &target_module.inner else {
1000            return;
1001        };
1002
1003        for child_id in &module.items {
1004            if !seen_items.insert(*child_id) {
1005                continue; // Already processed
1006            }
1007
1008            let Some(child) = krate.index.get(child_id) else {
1009                continue;
1010            };
1011
1012            if !self.view.should_include_item(child) {
1013                continue;
1014            }
1015
1016            match &child.inner {
1017                ItemEnum::Module(_) => modules.push(child),
1018                ItemEnum::Struct(_) => structs.push((child_id, child)),
1019                ItemEnum::Enum(_) => enums.push((child_id, child)),
1020                ItemEnum::Trait(_) => traits.push(child),
1021                ItemEnum::Function(_) => functions.push(child),
1022                ItemEnum::TypeAlias(_) => types.push(child),
1023                ItemEnum::Constant { .. } => constants.push(child),
1024                ItemEnum::Macro(_) => macros.push(child),
1025                _ => {},
1026            }
1027        }
1028    }
1029
1030    /// Render impl blocks for a type, including cross-crate impls.
1031    ///
1032    /// # Arguments
1033    ///
1034    /// * `md` - The markdown output buffer
1035    /// * `item_id` - The ID of the item to render impl blocks for
1036    /// * `source_crate_name` - Optional source crate name for cross-crate re-exports.
1037    ///   When provided, impls are looked up from the source crate.
1038    fn render_impl_blocks(&self, md: &mut String, item_id: Id, source_crate_name: Option<&str>) {
1039        let current_krate = self.view.krate();
1040
1041        // Get the appropriate crate for impl lookups
1042        let render_krate = source_crate_name
1043            .and_then(|name| self.view.get_crate(name))
1044            .unwrap_or(current_krate);
1045
1046        // Create TypeRenderer for the appropriate crate
1047        let type_renderer = if source_crate_name.is_some() {
1048            TypeRenderer::new(render_krate)
1049        } else {
1050            self.type_renderer
1051        };
1052
1053        // Get impls - for cross-crate items, look up from source crate
1054        let impls = if source_crate_name.is_some() {
1055            // For cross-crate re-exports, get impls from the source crate
1056            self.view.get_impls_from_crate(item_id, render_krate)
1057        } else {
1058            // For local items, use the normal lookup
1059            self.view.get_all_impls(item_id)
1060        };
1061
1062        if impls.is_empty() {
1063            return;
1064        }
1065
1066        // Partition into inherent vs trait impls
1067        let (inherent, trait_impls): (Vec<&rustdoc_types::Impl>, Vec<&rustdoc_types::Impl>) =
1068            impls.into_iter().partition(|i| i.trait_.is_none());
1069
1070        // Filter out synthetic impls
1071        let inherent: Vec<_> = inherent.into_iter().filter(|i| !i.is_synthetic).collect();
1072
1073        // Filter out synthetic impls and optionally blanket impls
1074        // Respect the --include-blanket-impls flag
1075        let include_blanket = self.view.include_blanket_impls();
1076        let mut trait_impls: Vec<_> = trait_impls
1077            .into_iter()
1078            .filter(|i| !i.is_synthetic)
1079            .filter(|i| include_blanket || !is_blanket_impl(i))
1080            .collect();
1081
1082        // Sort trait impls by trait name + generics for deterministic output
1083        trait_impls.sort_by(|a, b| {
1084            let key_a = impl_sort_key(a, &type_renderer);
1085            let key_b = impl_sort_key(b, &type_renderer);
1086            key_a.cmp(&key_b)
1087        });
1088
1089        // Deduplicate trait impls with same key (can happen with cross-crate impls)
1090        trait_impls
1091            .dedup_by(|a, b| impl_sort_key(a, &type_renderer) == impl_sort_key(b, &type_renderer));
1092
1093        // Render inherent implementations
1094        if !inherent.is_empty() {
1095            _ = write!(md, "#### Implementations\n\n");
1096
1097            for impl_block in inherent {
1098                render_impl_items(
1099                    md,
1100                    impl_block,
1101                    render_krate,
1102                    &type_renderer,
1103                    &None::<fn(&Item) -> Option<String>>,
1104                    &Some(|id: rustdoc_types::Id| {
1105                        LinkResolver::create_link(self.view, id, self.file_path)
1106                    }),
1107                );
1108            }
1109        }
1110
1111        // Render trait implementations
1112        if !trait_impls.is_empty() {
1113            _ = write!(md, "#### Trait Implementations\n\n");
1114
1115            for impl_block in trait_impls {
1116                if let Some(trait_path) = &impl_block.trait_ {
1117                    // Build trait name with generic args
1118                    let trait_name = trait_path
1119                        .path
1120                        .split("::")
1121                        .last()
1122                        .unwrap_or(&trait_path.path);
1123
1124                    let generics = type_renderer.render_generics(&impl_block.generics.params);
1125                    let for_type = type_renderer.render_type(&impl_block.for_);
1126
1127                    // Include unsafe/negative markers like single-crate mode
1128                    let unsafe_str = if impl_block.is_unsafe { "unsafe " } else { "" };
1129                    let negative_str = if impl_block.is_negative { "!" } else { "" };
1130
1131                    _ = writeln!(
1132                        md,
1133                        "##### `{unsafe_str}impl{generics} {negative_str}{trait_name} for {for_type}`\n"
1134                    );
1135                }
1136
1137                render_impl_items(
1138                    md,
1139                    impl_block,
1140                    render_krate,
1141                    &type_renderer,
1142                    &None::<fn(&Item) -> Option<String>>,
1143                    &Some(|id: rustdoc_types::Id| {
1144                        LinkResolver::create_link(self.view, id, self.file_path)
1145                    }),
1146                );
1147            }
1148        }
1149    }
1150}