cargo_doc_docusaurus/
converter.rs

1//! Markdown converter for rustdoc JSON data.
2
3use anyhow::Result;
4use rustdoc_types::{Crate, Id, Item, ItemEnum, Visibility};
5use std::cell::RefCell;
6use std::collections::HashMap;
7
8thread_local! {
9    /// Thread-local storage for the base path to use in generated links
10    static BASE_PATH: RefCell<String> = const { RefCell::new(String::new()) };
11    /// Thread-local storage for workspace crate names
12    static WORKSPACE_CRATES: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
13    /// Thread-local storage for the sidebar root link URL
14    static SIDEBAR_ROOT_LINK: RefCell<Option<String>> = const { RefCell::new(None) };
15}
16
17/// Represents the multi-file markdown output
18pub struct MarkdownOutput {
19  /// Crate name
20  pub crate_name: String,
21  /// Map of relative file path -> content
22  pub files: HashMap<String, String>,
23  /// Sidebar configuration (optional, for Docusaurus)
24  pub sidebar: Option<String>,
25}
26
27/// Represents a sidebar item for Docusaurus
28#[derive(Debug, Clone)]
29enum SidebarItem {
30  /// A document reference with optional label
31  Doc {
32    id: String,
33    label: Option<String>,
34    custom_props: Option<String>, // Can be either className or customProps JSON
35  },
36  /// A link item (for dynamic sidebars)
37  Link {
38    href: String,
39    label: String,
40    custom_props: Option<String>,
41  },
42  /// A category with sub-items
43  Category {
44    label: String,
45    items: Vec<SidebarItem>,
46    collapsed: bool,
47    link: Option<String>, // Optional link to make category clickable
48  },
49}
50
51/// Convert a rustdoc Crate to multi-file markdown format.
52pub fn convert_to_markdown_multifile(
53  crate_data: &Crate,
54  include_private: bool,
55  base_path: &str,
56  workspace_crates: &[String],
57  sidebarconfig_collapsed: bool,
58  sidebar_root_link: Option<&str>,
59) -> Result<MarkdownOutput> {
60  // Set the base path, workspace crates, and sidebar root link for this conversion in thread-local storage
61  BASE_PATH.with(|bp| *bp.borrow_mut() = base_path.to_string());
62  WORKSPACE_CRATES.with(|wc| *wc.borrow_mut() = workspace_crates.to_vec());
63  SIDEBAR_ROOT_LINK.with(|srl| *srl.borrow_mut() = sidebar_root_link.map(|s| s.to_string()));
64
65  let root_item = crate_data
66    .index
67    .get(&crate_data.root)
68    .ok_or_else(|| anyhow::anyhow!("Root item not found in index"))?;
69
70  let crate_name = root_item.name.as_deref().unwrap_or("unknown");
71
72  // Build a map of item_id -> full_path using the paths data
73  let item_paths = build_path_map(crate_data);
74
75  // Group items by module (no longer duplicating re-exports)
76  let mut modules = group_by_module(crate_data, &item_paths, include_private);
77
78  // Build a map of re-exported modules (module_path -> list of re-exported submodule paths)
79  let reexported_modules = build_reexported_modules(crate_data, &item_paths, include_private);
80
81  let mut files = HashMap::new();
82
83  // Check if we have items in the root crate
84  let root_module_key = crate_name.to_string();
85  let has_root_items = modules.contains_key(&root_module_key);
86
87  // First, ensure ALL modules (including empty ones) are in the modules map
88  // This is crucial for generating index.md files and sidebar entries for parent modules
89  // that only contain submodules (prevents Docusaurus referencing missing doc ids).
90  for (_id, item) in &crate_data.index {
91    if let ItemEnum::Module(_) = &item.inner {
92      if let Some(path) = item_paths.get(_id) {
93        let module_path = path.join("::");
94        // Ensure this module exists in the map (even if empty)
95        modules.entry(module_path).or_default();
96      }
97    }
98  }
99
100  // Build module hierarchy to determine which modules have submodules
101  let module_hierarchy = build_module_hierarchy(&modules, crate_name);
102
103  // Ensure parent modules from hierarchy are present in `modules` so we
104  // generate index pages and matching sidebar keys for parent modules that
105  // only contain submodules (prevents Docusaurus referencing missing doc ids).
106  // This is a minimal change: it inserts empty item lists for parents that
107  // don't already exist so `generate_module_overview` will still create
108  // a corresponding `index.md` file.
109  for parent in module_hierarchy.keys() {
110    modules.entry(parent.clone()).or_default();
111  }
112
113  // Generate index.md - either with crate overview or with root module content
114  if has_root_items {
115    // If there are items in the root module, combine crate overview with root content
116    let root_items = &modules[&root_module_key];
117    let index_content = generate_combined_crate_and_root_content(
118      crate_name,
119      root_item,
120      crate_data,
121      &modules,
122      root_items,
123      &module_hierarchy,
124      &reexported_modules,
125    );
126    files.insert("index.md".to_string(), index_content);
127  } else {
128    // Just crate overview if no root items
129    let index_content = generate_crate_index(crate_name, root_item, &modules);
130    files.insert("index.md".to_string(), index_content);
131  }
132
133  // Generate overview files and individual pages for each module
134  for (module_name, items) in &modules {
135    // Skip the root module as it's already handled in index.md
136    if module_name == &root_module_key {
137      // Generate individual pages for root-level items
138      generate_individual_pages(
139        items,
140        "",
141        &mut files,
142        crate_data,
143        &item_paths,
144        crate_name,
145        crate_name,
146        include_private,
147      );
148      continue;
149    }
150
151    let module_filename = module_name
152      .strip_prefix(&format!("{}::", crate_name))
153      .unwrap_or(module_name)
154      .replace("::", "/");
155
156    // Always generate module overview (even if items are re-exported)
157    // This ensures all modules are navigable
158    let overview_path = format!("{}/index.md", module_filename);
159
160    // Generate module overview page (index-style)
161    let module_overview = generate_module_overview(
162      module_name,
163      items, // Use direct items only, not all recursive items
164      crate_data,
165      &item_paths,
166      crate_name,
167      &module_hierarchy,
168    );
169    files.insert(overview_path.clone(), module_overview);
170
171    // Always generate individual pages for items
172    // All modules use subdirectories, so items go in the module directory
173    let item_prefix = if module_filename.is_empty() {
174      String::new()
175    } else {
176      format!("{}/", module_filename)
177    };
178    generate_individual_pages(
179      items,
180      &item_prefix,
181      &mut files,
182      crate_data,
183      &item_paths,
184      crate_name,
185      module_name,
186      include_private,
187    );
188  }
189
190  // Generate sidebar structure with sidebars for each module
191  let sidebar = generate_all_sidebars(
192    crate_name,
193    &modules,
194    &item_paths,
195    crate_data,
196    sidebarconfig_collapsed,
197  );
198
199  Ok(MarkdownOutput {
200    crate_name: crate_name.to_string(),
201    files,
202    sidebar: Some(sidebar),
203  })
204}
205
206/// Convert a rustdoc Crate to markdown format (legacy single-file).
207pub fn convert_to_markdown(crate_data: &Crate, include_private: bool) -> Result<String> {
208  let mut output = String::new();
209
210  let root_item = crate_data
211    .index
212    .get(&crate_data.root)
213    .ok_or_else(|| anyhow::anyhow!("Root item not found in index"))?;
214
215  let crate_name = root_item.name.as_deref().unwrap_or("unknown");
216  output.push_str(&format!("# {}\n\n", crate_name));
217
218  if let Some(docs) = &root_item.docs {
219    output.push_str(&format!("{}\n\n", docs));
220  }
221
222  // Build a map of item_id -> full_path using the paths data
223  let item_paths = build_path_map(crate_data);
224
225  // Group items by module
226  let modules = group_by_module(crate_data, &item_paths, include_private);
227
228  // Generate hierarchical ToC
229  output.push_str("## Table of Contents\n\n");
230  output.push_str(&generate_toc(&modules, crate_name));
231  output.push_str("\n\n---\n\n");
232
233  // Generate content organized by module
234  output.push_str(&generate_content(
235    &modules,
236    crate_data,
237    &item_paths,
238    include_private,
239  ));
240
241  Ok(output)
242}
243
244fn build_path_map(crate_data: &Crate) -> HashMap<Id, Vec<String>> {
245  crate_data
246    .paths
247    .iter()
248    .map(|(id, summary)| (*id, summary.path.clone()))
249    .collect()
250}
251
252fn build_module_hierarchy(
253  modules: &HashMap<String, Vec<(Id, Item)>>,
254  crate_name: &str,
255) -> HashMap<String, Vec<String>> {
256  let mut hierarchy: HashMap<String, Vec<String>> = HashMap::new();
257
258  for module_name in modules.keys() {
259    // Skip the root crate module itself
260    if module_name == crate_name {
261      continue;
262    }
263
264    // Extract the relative module path
265    let relative_path = module_name
266      .strip_prefix(&format!("{}::", crate_name))
267      .unwrap_or(module_name);
268
269    // Split into components
270    let components: Vec<&str> = relative_path.split("::").collect();
271
272    // Handle top-level modules (direct children of crate root)
273    if components.len() == 1 {
274      hierarchy
275        .entry(crate_name.to_string())
276        .or_default()
277        .push(module_name.clone());
278    }
279
280    // For each component, check if it's a parent of this module
281    for i in 0..components.len() {
282      let parent_path = if i == 0 {
283        format!("{}::{}", crate_name, components[0])
284      } else {
285        let parent_components = &components[0..=i];
286        format!("{}::{}", crate_name, parent_components.join("::"))
287      };
288
289      // If this is not the full path, it's a parent
290      if parent_path != *module_name && components.len() > i + 1 {
291        let child_path = if i + 1 < components.len() - 1 {
292          let child_components = &components[0..=i + 1];
293          format!("{}::{}", crate_name, child_components.join("::"))
294        } else {
295          module_name.clone()
296        };
297
298        hierarchy.entry(parent_path).or_default().push(child_path);
299      }
300    }
301  }
302
303  // Deduplicate children
304  for children in hierarchy.values_mut() {
305    children.sort();
306    children.dedup();
307  }
308
309  hierarchy
310}
311
312/// Build a map of re-exported modules
313/// Returns: parent_module_path -> list of (child_module_name, child_module_full_path)
314fn build_reexported_modules(
315  crate_data: &Crate,
316  item_paths: &HashMap<Id, Vec<String>>,
317  include_private: bool,
318) -> HashMap<String, Vec<(String, String)>> {
319  let mut reexports: HashMap<String, Vec<(String, String)>> = HashMap::new();
320
321  // Iterate through all modules to find their Use items
322  for (module_id, module_item) in &crate_data.index {
323    if let ItemEnum::Module(module_data) = &module_item.inner {
324      // Get the module path
325      let module_path = if let Some(path) = item_paths.get(module_id) {
326        path.join("::")
327      } else {
328        continue;
329      };
330
331      // Process all items in this module to find re-exports
332      for item_id in &module_data.items {
333        if let Some(item) = crate_data.index.get(item_id) {
334          if let ItemEnum::Use(import) = &item.inner {
335            // Only process public re-exports
336            if !include_private && !is_public(item) {
337              continue;
338            }
339
340            // Try to find the imported item
341            if let Some(imported_id) = &import.id {
342              if let Some(imported_item) = crate_data.index.get(imported_id) {
343                // Check if this is a glob import
344                if import.is_glob {
345                  // Glob re-export - find all public submodules
346                  if let ItemEnum::Module(source_module_data) = &imported_item.inner {
347                    for source_item_id in &source_module_data.items {
348                      if let Some(source_item) = crate_data.index.get(source_item_id) {
349                        // Only process modules
350                        if let ItemEnum::Module(_) = &source_item.inner {
351                          // Only add public modules
352                          if !include_private && !is_public(source_item) {
353                            continue;
354                          }
355
356                          if let Some(source_item_name) = &source_item.name {
357                            // Get the full path of the source module
358                            if let Some(source_path) = item_paths.get(source_item_id) {
359                              let source_full_path = source_path.join("::");
360                              reexports
361                                .entry(module_path.clone())
362                                .or_default()
363                                .push((source_item_name.clone(), source_full_path));
364                            }
365                          }
366                        }
367                      }
368                    }
369                  }
370                } else {
371                  // Single module re-export
372                  if let ItemEnum::Module(_) = &imported_item.inner {
373                    if let Some(imported_name) = &imported_item.name {
374                      if let Some(imported_path) = item_paths.get(imported_id) {
375                        let imported_full_path = imported_path.join("::");
376                        reexports
377                          .entry(module_path.clone())
378                          .or_default()
379                          .push((imported_name.clone(), imported_full_path));
380                      }
381                    }
382                  }
383                }
384              }
385            }
386          }
387        }
388      }
389    }
390  }
391
392  // Deduplicate
393  for list in reexports.values_mut() {
394    list.sort();
395    list.dedup();
396  }
397
398  reexports
399}
400
401/// Check if all items in a module are re-exported in its parent module
402fn group_by_module(
403  crate_data: &Crate,
404  item_paths: &HashMap<Id, Vec<String>>,
405  include_private: bool,
406) -> HashMap<String, Vec<(Id, Item)>> {
407  let mut modules: HashMap<String, Vec<(Id, Item)>> = HashMap::new();
408
409  for (id, item) in &crate_data.index {
410    if id == &crate_data.root {
411      continue;
412    }
413
414    if !include_private && !is_public(item) {
415      continue;
416    }
417
418    // Skip if we can't format this item type
419    if !can_format_item(item) {
420      continue;
421    }
422
423    // Get the module path (all elements except the last one)
424    let module_path = if let Some(path) = item_paths.get(id) {
425      if path.len() > 1 {
426        // Item is in a submodule
427        path[..path.len() - 1].join("::")
428      } else if path.len() == 1 {
429        // Item is at the root of the crate - use crate name as the module path
430        path[0].clone()
431      } else {
432        continue; // Skip items with empty path
433      }
434    } else {
435      continue; // Skip items without path info
436    };
437
438    modules
439      .entry(module_path)
440      .or_default()
441      .push((*id, item.clone()));
442  }
443
444  // Process re-exports (ItemEnum::Use)
445  // For glob re-exports (pub use module::*), we generate duplicate files like rustdoc does
446  // For simple re-exports, we only show the link in the Re-exports section
447  for (module_id, module_item) in &crate_data.index {
448    if let ItemEnum::Module(module_data) = &module_item.inner {
449      // Get the module path
450      let module_path = if let Some(path) = item_paths.get(module_id) {
451        path.join("::")
452      } else {
453        continue;
454      };
455
456      // Process all items in this module
457      for item_id in &module_data.items {
458        if let Some(item) = crate_data.index.get(item_id) {
459          // Check if this item is a re-export
460          if let ItemEnum::Use(import) = &item.inner {
461            // Only process public re-exports
462            if !include_private && !is_public(item) {
463              continue;
464            }
465
466            // Always add the Use item itself for the Re-exports section
467            modules
468              .entry(module_path.clone())
469              .or_default()
470              .push((*item_id, item.clone()));
471
472            // For glob re-exports (pub use module::*), also add all re-exported items
473            // This matches rustdoc's behavior of generating duplicate documentation
474            if import.is_glob {
475              if let Some(imported_id) = &import.id {
476                // Prevent self-referential re-exports (e.g., pub use self::*)
477                if imported_id == module_id {
478                  continue;
479                }
480
481                // Resolve the re-export chain to find the final item
482                let mut visited = std::collections::HashSet::new();
483                if let Some((resolved_id, imported_item)) =
484                  resolve_reexport_chain(imported_id, crate_data, 0, &mut visited)
485                {
486                  if let ItemEnum::Module(imported_module_data) = &imported_item.inner {
487                    // Get the imported module path to check for circular references
488                    let imported_module_path = item_paths.get(&resolved_id).map(|p| p.join("::"));
489
490                    // Skip if the imported module is a parent of the current module
491                    // (prevents infinite loops with circular re-exports)
492                    if let Some(imported_path) = &imported_module_path {
493                      if module_path.starts_with(&format!("{}::", imported_path)) {
494                        continue;
495                      }
496                    }
497
498                    // Add all items from the imported module
499                    for imported_item_id in &imported_module_data.items {
500                      if let Some(imported_item) = crate_data.index.get(imported_item_id) {
501                        // Skip if not public (unless include_private is true)
502                        if !include_private && !is_public(imported_item) {
503                          continue;
504                        }
505
506                        // Skip if we can't format this item type
507                        if !can_format_item(imported_item) {
508                          continue;
509                        }
510
511                        // Skip Use items within the glob to avoid nested re-exports
512                        if matches!(imported_item.inner, ItemEnum::Use(_)) {
513                          continue;
514                        }
515
516                        // Skip Module items to avoid duplicating module definitions
517                        if matches!(imported_item.inner, ItemEnum::Module(_)) {
518                          continue;
519                        }
520
521                        // Add the imported item to this module
522                        modules
523                          .entry(module_path.clone())
524                          .or_default()
525                          .push((*imported_item_id, imported_item.clone()));
526                      }
527                    }
528                  }
529                }
530              }
531            }
532          }
533        }
534      }
535    }
536  }
537
538  // Sort items within each module by name and remove duplicates
539  for items in modules.values_mut() {
540    items.sort_by(|a, b| {
541      let name_a = a.1.name.as_deref().unwrap_or("");
542      let name_b = b.1.name.as_deref().unwrap_or("");
543      name_a.cmp(name_b)
544    });
545    // Remove duplicates (same ID)
546    items.dedup_by(|a, b| a.0 == b.0);
547  }
548
549  modules
550}
551
552fn can_format_item(item: &Item) -> bool {
553  matches!(
554    item.inner,
555    ItemEnum::Struct(_)
556      | ItemEnum::Enum(_)
557      | ItemEnum::Function(_)
558      | ItemEnum::Trait(_)
559      | ItemEnum::Module(_)
560      | ItemEnum::Constant { .. }
561      | ItemEnum::TypeAlias(_)
562  )
563}
564
565/// Get the rustdoc-style prefix for an item type (e.g., "fn.", "struct.", etc.)
566fn get_item_prefix(item: &Item) -> &'static str {
567  match &item.inner {
568    ItemEnum::Function(_) => "fn.",
569    ItemEnum::Struct(_) => "struct.",
570    ItemEnum::Enum(_) => "enum.",
571    ItemEnum::Trait(_) => "trait.",
572    ItemEnum::Constant { .. } => "constant.",
573    ItemEnum::TypeAlias(_) => "type.",
574    ItemEnum::Module(_) => "", // Modules don't get a prefix
575    _ => "",
576  }
577}
578
579fn get_item_type_label(item: &Item) -> &'static str {
580  match &item.inner {
581    ItemEnum::Function(_) => "Function",
582    ItemEnum::Struct(_) => "Struct",
583    ItemEnum::Enum(_) => "Enum",
584    ItemEnum::Trait(_) => "Trait",
585    ItemEnum::Constant { .. } => "Constant",
586    ItemEnum::TypeAlias(_) => "Type",
587    ItemEnum::Module(_) => "Module",
588    _ => "",
589  }
590}
591
592fn generate_toc(modules: &HashMap<String, Vec<(Id, Item)>>, crate_name: &str) -> String {
593  let mut toc = String::new();
594
595  // Sort modules alphabetically
596  let mut module_names: Vec<_> = modules.keys().collect();
597  module_names.sort();
598
599  for module_name in module_names {
600    let items = &modules[module_name];
601
602    // Get the last component of the module path for display
603    let display_name = module_name
604      .strip_prefix(&format!("{}::", crate_name))
605      .unwrap_or(module_name);
606
607    toc.push_str(&format!("- **{}**\n", display_name));
608
609    for (_id, item) in items {
610      if let Some(name) = &item.name {
611        let full_path = format!("{}::{}", module_name, name);
612        let anchor = full_path.to_lowercase().replace("::", "-");
613        toc.push_str(&format!("  - [{}](#{})\n", name, anchor));
614      }
615    }
616  }
617
618  toc
619}
620
621fn generate_content(
622  modules: &HashMap<String, Vec<(Id, Item)>>,
623  crate_data: &Crate,
624  item_paths: &HashMap<Id, Vec<String>>,
625  include_private: bool,
626) -> String {
627  let mut output = String::new();
628
629  // Sort modules alphabetically
630  let mut module_names: Vec<_> = modules.keys().collect();
631  module_names.sort();
632
633  for module_name in module_names {
634    let items = &modules[module_name];
635
636    // Module header
637    output.push_str(&format!("# Module: `{}`\n\n", module_name));
638
639    // Generate content for each item in the module
640    for (id, item) in items {
641      if let Some(section) =
642        format_item_with_path(id, item, crate_data, item_paths, include_private)
643      {
644        output.push_str(&section);
645        output.push_str("\n\n");
646      }
647    }
648
649    output.push_str("---\n\n");
650  }
651
652  output
653}
654
655fn format_item_with_path(
656  item_id: &Id,
657  item: &Item,
658  crate_data: &Crate,
659  item_paths: &HashMap<Id, Vec<String>>,
660  include_private: bool,
661) -> Option<String> {
662  let full_path = item_paths.get(item_id)?;
663  let full_name = full_path.join("::");
664
665  let mut output = format_item(item_id, item, crate_data, include_private)?;
666
667  // Replace the simple name header with the full path
668  if let Some(name) = &item.name {
669    let old_header = format!("## {}\n\n", name);
670    let new_header = format!("## {}\n\n", full_name);
671    output = output.replace(&old_header, &new_header);
672  }
673
674  Some(output)
675}
676
677fn is_public(item: &Item) -> bool {
678  matches!(item.visibility, Visibility::Public)
679}
680
681/// Resolve a chain of re-exports to find the final item
682/// Returns (final_id, final_item) if successful, None if the chain is circular or too deep
683fn resolve_reexport_chain<'a>(
684  item_id: &Id,
685  crate_data: &'a Crate,
686  depth: usize,
687  visited: &mut std::collections::HashSet<Id>,
688) -> Option<(Id, &'a Item)> {
689  const MAX_DEPTH: usize = 10;
690
691  if depth > MAX_DEPTH {
692    return None;
693  }
694
695  if !visited.insert(*item_id) {
696    // Circular reference detected
697    return None;
698  }
699
700  if let Some(item) = crate_data.index.get(item_id) {
701    if let ItemEnum::Use(import) = &item.inner {
702      // This is a re-export, follow the chain
703      if let Some(imported_id) = &import.id {
704        return resolve_reexport_chain(imported_id, crate_data, depth + 1, visited);
705      }
706    }
707    // Not a re-export, return the item
708    Some((*item_id, item))
709  } else {
710    None
711  }
712}
713
714/// Get the visibility indicator for an item (e.g., "🔒" for restricted visibility)
715fn get_visibility_indicator(item: &Item) -> &'static str {
716  match &item.visibility {
717    Visibility::Public => "",
718    _ => " 🔒", // Lock emoji for crate/restricted visibility
719  }
720}
721
722/// Format a struct definition with links extracted
723#[allow(clippy::single_char_add_str, clippy::manual_flatten)]
724fn format_struct_definition_with_links(
725  name: &str,
726  s: &rustdoc_types::Struct,
727  item: &Item,
728  crate_data: &Crate,
729  include_private: bool,
730) -> (String, Vec<(String, String)>) {
731  let mut code = String::new();
732  let mut all_links = Vec::new();
733
734  // Add visibility and struct keyword
735  let visibility = match &item.visibility {
736    rustdoc_types::Visibility::Public => "pub ",
737    _ => "",
738  };
739
740  code.push_str(&format!("{}struct {}", visibility, name));
741
742  // Add generic parameters
743  let non_synthetic_params: Vec<_> = s
744    .generics
745    .params
746    .iter()
747    .filter(|p| {
748      !matches!(&p.kind, rustdoc_types::GenericParamDefKind::Lifetime { .. })
749        || !is_synthetic_lifetime(&p.name)
750    })
751    .collect();
752
753  if !non_synthetic_params.is_empty() {
754    code.push('<');
755    let params: Vec<String> = non_synthetic_params
756      .iter()
757      .map(|p| p.name.clone())
758      .collect();
759    code.push_str(&params.join(", "));
760    code.push('>');
761  }
762
763  // Add struct body based on kind
764  match &s.kind {
765    rustdoc_types::StructKind::Plain { fields, .. } => {
766      if fields.is_empty() {
767        #[allow(clippy::single_char_add_str)]
768        code.push_str(";");
769      } else {
770        #[allow(clippy::single_char_add_str)]
771        code.push_str(" {");
772        for field_id in fields {
773          if let Some(field) = crate_data.index.get(field_id) {
774            if let Some(field_name) = &field.name {
775              if let ItemEnum::StructField(ty) = &field.inner {
776                // Show field visibility based on include_private flag
777                let field_visibility = if include_private {
778                  match &field.visibility {
779                    rustdoc_types::Visibility::Public => "pub ",
780                    rustdoc_types::Visibility::Crate => "pub(crate) ",
781                    rustdoc_types::Visibility::Restricted { .. } => "",
782                    rustdoc_types::Visibility::Default => "",
783                  }
784                } else {
785                  match &field.visibility {
786                    rustdoc_types::Visibility::Public => "pub ",
787                    _ => continue,
788                  }
789                };
790
791                let (field_type, links) = format_type_with_links(ty, crate_data, Some(item));
792                all_links.extend(links);
793                code.push_str(&format!(
794                  "\n    {}{}: {},",
795                  field_visibility, field_name, field_type
796                ));
797              }
798            }
799          }
800        }
801        code.push_str("\n}");
802      }
803    }
804    rustdoc_types::StructKind::Tuple(fields) => {
805      code.push('(');
806      let mut visible_fields = Vec::new();
807      for field_id in fields {
808        if let Some(id) = field_id {
809          if let Some(field) = crate_data.index.get(id) {
810            if let ItemEnum::StructField(ty) = &field.inner {
811              if include_private {
812                let field_visibility = match &field.visibility {
813                  rustdoc_types::Visibility::Public => "pub ",
814                  rustdoc_types::Visibility::Crate => "pub(crate) ",
815                  rustdoc_types::Visibility::Restricted { .. } => "",
816                  rustdoc_types::Visibility::Default => "",
817                };
818                let (field_type, links) = format_type_with_links(ty, crate_data, Some(item));
819                all_links.extend(links);
820                if field_visibility.is_empty() {
821                  visible_fields.push(field_type);
822                } else {
823                  visible_fields.push(format!("{}{}", field_visibility, field_type));
824                }
825              } else {
826                match &field.visibility {
827                  rustdoc_types::Visibility::Public => {
828                    let (field_type, links) = format_type_with_links(ty, crate_data, Some(item));
829                    all_links.extend(links);
830                    visible_fields.push(format!("pub {}", field_type));
831                  }
832                  _ => continue,
833                }
834              }
835            }
836          }
837        }
838      }
839      code.push_str(&visible_fields.join(", "));
840      code.push_str(");");
841    }
842    rustdoc_types::StructKind::Unit => {
843      code.push_str(";");
844    }
845  }
846
847  (code, all_links)
848}
849
850/// Format an enum definition with links extracted
851#[allow(clippy::manual_flatten)]
852fn format_enum_definition_with_links(
853  name: &str,
854  e: &rustdoc_types::Enum,
855  item: &Item,
856  crate_data: &Crate,
857) -> (String, Vec<(String, String)>) {
858  let mut code = String::new();
859  let mut all_links = Vec::new();
860
861  // Add visibility and enum keyword
862  let visibility = match &item.visibility {
863    rustdoc_types::Visibility::Public => "pub ",
864    _ => "",
865  };
866
867  code.push_str(&format!("{}enum {}", visibility, name));
868
869  // Add generic parameters
870  let non_synthetic_params: Vec<_> = e
871    .generics
872    .params
873    .iter()
874    .filter(|p| {
875      !matches!(&p.kind, rustdoc_types::GenericParamDefKind::Lifetime { .. })
876        || !is_synthetic_lifetime(&p.name)
877    })
878    .collect();
879
880  if !non_synthetic_params.is_empty() {
881    code.push('<');
882    let params: Vec<String> = non_synthetic_params
883      .iter()
884      .map(|p| p.name.clone())
885      .collect();
886    code.push_str(&params.join(", "));
887    code.push('>');
888  }
889
890  code.push_str(" {");
891
892  // Add variants with their fields
893  for variant_id in &e.variants {
894    if let Some(variant) = crate_data.index.get(variant_id) {
895      if let Some(variant_name) = &variant.name {
896        code.push_str(&format!("\n    {}", variant_name));
897
898        // Check if the variant has fields
899        if let ItemEnum::Variant(variant_inner) = &variant.inner {
900          match &variant_inner.kind {
901            rustdoc_types::VariantKind::Plain => {
902              // Unit variant, no fields
903            }
904            rustdoc_types::VariantKind::Tuple(field_ids) => {
905              // Tuple variant with fields: Message(Type1, Type2)
906              code.push('(');
907              let mut field_types = Vec::new();
908              for field_id in field_ids {
909                if let Some(id) = field_id {
910                  if let Some(field_item) = crate_data.index.get(id) {
911                    if let ItemEnum::StructField(ty) = &field_item.inner {
912                      let (type_str, links) = format_type_with_links(ty, crate_data, Some(item));
913                      field_types.push(type_str);
914                      all_links.extend(links);
915                    }
916                  }
917                }
918              }
919              code.push_str(&field_types.join(", "));
920              code.push(')');
921            }
922            rustdoc_types::VariantKind::Struct {
923              fields,
924              has_stripped_fields: _,
925            } => {
926              // Struct variant with named fields: Message { field1: Type1, field2: Type2 }
927              code.push_str(" { ");
928              let mut field_strs = Vec::new();
929              for field_id in fields {
930                if let Some(field_item) = crate_data.index.get(field_id) {
931                  if let Some(field_name) = &field_item.name {
932                    if let ItemEnum::StructField(ty) = &field_item.inner {
933                      let (type_str, links) = format_type_with_links(ty, crate_data, Some(item));
934                      field_strs.push(format!("{}: {}", field_name, type_str));
935                      all_links.extend(links);
936                    }
937                  }
938                }
939              }
940              code.push_str(&field_strs.join(", "));
941              code.push_str(" }");
942            }
943          }
944        }
945        code.push(',');
946      }
947    }
948  }
949
950  code.push_str("\n}");
951
952  (code, all_links)
953}
954
955/// Format a function definition with links extracted
956#[allow(clippy::format_in_format_args)]
957fn format_function_definition_with_links(
958  name: &str,
959  f: &rustdoc_types::Function,
960  item: &Item,
961  crate_data: &Crate,
962) -> (String, Vec<(String, String)>) {
963  let mut code = String::new();
964  let mut all_links = Vec::new();
965
966  // Collect generic parameters
967  let generic_params: Vec<String> = if !f.generics.params.is_empty() {
968    f.generics.params.iter().map(format_generic_param).collect()
969  } else {
970    Vec::new()
971  };
972
973  // Collect function inputs
974  let mut inputs = Vec::new();
975  for (param_name, ty) in &f.sig.inputs {
976    let (type_str, links) = format_type_with_links(ty, crate_data, Some(item));
977    all_links.extend(links);
978    inputs.push(format!("{}: {}", param_name, type_str));
979  }
980
981  // Format on multiple lines if signature is too long (> 80 chars) or has many parameters (> 3)
982  let single_line = format!(
983    "fn {}{}",
984    if !generic_params.is_empty() {
985      format!("{}<{}>", name, generic_params.join(", "))
986    } else {
987      name.to_string()
988    },
989    format!("({})", inputs.join(", "))
990  );
991
992  if inputs.len() > 3 || single_line.len() > 80 {
993    // Multi-line format
994    code.push_str(&format!("fn {}", name));
995    if !generic_params.is_empty() {
996      code.push('<');
997      code.push_str(&generic_params.join(", "));
998      code.push('>');
999    }
1000    code.push_str("(\n");
1001    for (i, input) in inputs.iter().enumerate() {
1002      code.push_str(&format!("    {}", input));
1003      if i < inputs.len() - 1 {
1004        code.push(',');
1005      }
1006      code.push('\n');
1007    }
1008    code.push(')');
1009  } else {
1010    // Single line format
1011    code.push_str(&format!("fn {}", name));
1012    if !generic_params.is_empty() {
1013      code.push('<');
1014      code.push_str(&generic_params.join(", "));
1015      code.push('>');
1016    }
1017    code.push('(');
1018    code.push_str(&inputs.join(", "));
1019    code.push(')');
1020  }
1021
1022  if let Some(output_type) = &f.sig.output {
1023    let (type_str, links) = format_type_with_links(output_type, crate_data, Some(item));
1024    all_links.extend(links);
1025    code.push_str(&format!(" -> {}", type_str));
1026  }
1027
1028  (code, all_links)
1029}
1030
1031#[allow(clippy::single_char_add_str)]
1032fn format_item(
1033  item_id: &rustdoc_types::Id,
1034  item: &Item,
1035  crate_data: &Crate,
1036  include_private: bool,
1037) -> Option<String> {
1038  let name = item.name.as_ref()?;
1039  let mut output = String::new();
1040
1041  match &item.inner {
1042    ItemEnum::Struct(s) => {
1043      // Format struct definition with links
1044      let (code, links) =
1045        format_struct_definition_with_links(name, s, item, crate_data, include_private);
1046      let links_json = format_links_as_json(&links);
1047      output.push_str(&format!(
1048        "<RustCode code={{`{}`}} links={{{}}} />\n\n",
1049        code, links_json
1050      ));
1051
1052      if let Some(docs) = &item.docs {
1053        output.push_str(&format!("{}\n\n", sanitize_docs_for_mdx(docs)));
1054      }
1055
1056      let non_synthetic_params: Vec<_> = s
1057        .generics
1058        .params
1059        .iter()
1060        .filter(|p| {
1061          !matches!(&p.kind, rustdoc_types::GenericParamDefKind::Lifetime { .. })
1062            || !is_synthetic_lifetime(&p.name)
1063        })
1064        .collect();
1065
1066      if !non_synthetic_params.is_empty() {
1067        output.push_str("### Generic Parameters\n\n");
1068        for param in non_synthetic_params {
1069          output.push_str(&format!("- {}\n", format_generic_param(param)));
1070        }
1071        output.push('\n');
1072      }
1073
1074      match &s.kind {
1075        rustdoc_types::StructKind::Plain { fields, .. } => {
1076          if !fields.is_empty() {
1077            // Filter fields based on include_private flag
1078            let visible_fields: Vec<_> = if include_private {
1079              fields.iter().collect()
1080            } else {
1081              fields
1082                .iter()
1083                .filter(|&field_id| {
1084                  if let Some(field) = crate_data.index.get(field_id) {
1085                    is_public(field)
1086                  } else {
1087                    false
1088                  }
1089                })
1090                .collect()
1091            };
1092
1093            if !visible_fields.is_empty() {
1094              output.push_str("### Fields\n\n");
1095              for field_id in visible_fields {
1096                if let Some(field) = crate_data.index.get(field_id) {
1097                  if let Some(field_name) = &field.name {
1098                    let (type_str, type_links) = if let ItemEnum::StructField(ty) = &field.inner {
1099                      format_type_with_links(ty, crate_data, Some(item))
1100                    } else {
1101                      ("?".to_string(), Vec::new())
1102                    };
1103
1104                    let field_sig = format!("{}: {}", field_name, type_str);
1105                    let links_json = format_links_as_json(&type_links);
1106                    output.push_str(&format!(
1107                      "<RustCode inline code={{`{}`}} links={{{}}} />\n\n",
1108                      field_sig, links_json
1109                    ));
1110
1111                    if let Some(docs) = &field.docs {
1112                      let first_line = docs.lines().next().unwrap_or("").trim();
1113                      if !first_line.is_empty() {
1114                        output.push_str(&format!(
1115                          "<div className=\"rust-field-doc\">{}</div>\n\n",
1116                          first_line
1117                        ));
1118                      }
1119                    }
1120                  }
1121                }
1122              }
1123              output.push_str("\n");
1124            }
1125          }
1126        }
1127        rustdoc_types::StructKind::Tuple(fields) => {
1128          let types: Vec<String> = fields
1129            .iter()
1130            .filter_map(|field_id| {
1131              field_id.and_then(|id| {
1132                crate_data.index.get(&id).map(|field| {
1133                  if let ItemEnum::StructField(ty) = &field.inner {
1134                    format_type(ty, crate_data)
1135                  } else {
1136                    "?".to_string()
1137                  }
1138                })
1139              })
1140            })
1141            .collect();
1142          output.push_str(&format!("**Tuple Struct**: `({})`\n\n", types.join(", ")));
1143        }
1144        rustdoc_types::StructKind::Unit => {
1145          output.push_str("**Unit Struct**\n\n");
1146        }
1147      }
1148
1149      let (inherent_impls, trait_impls) = collect_impls_for_type(item_id, crate_data);
1150
1151      if !inherent_impls.is_empty() {
1152        output.push_str("### Methods\n\n");
1153        for impl_block in inherent_impls {
1154          let methods = format_impl_methods(impl_block, crate_data, Some(item));
1155          for (sig, links, doc) in methods {
1156            let links_json = format_links_as_json(&links);
1157            output.push_str(&format!(
1158              "<RustCode inline code={{`{}`}} links={{{}}} />\n\n",
1159              sig, links_json
1160            ));
1161            if let Some(doc) = doc {
1162              output.push_str(&format!("{}\n\n", doc));
1163            }
1164            output.push_str("---\n\n");
1165          }
1166        }
1167      }
1168
1169      if !trait_impls.is_empty() {
1170        let user_impls: Vec<_> = trait_impls
1171          .iter()
1172          .filter(|impl_block| !impl_block.is_synthetic && impl_block.blanket_impl.is_none())
1173          .collect();
1174
1175        if !user_impls.is_empty() {
1176          let mut derives = Vec::new();
1177          let mut trait_with_methods = Vec::new();
1178
1179          for impl_block in user_impls {
1180            if let Some(trait_ref) = &impl_block.trait_ {
1181              let methods = format_impl_methods(impl_block, crate_data, Some(item));
1182              if methods.is_empty() {
1183                derives.push(trait_ref.path.as_str());
1184              } else {
1185                trait_with_methods.push((trait_ref, methods));
1186              }
1187            }
1188          }
1189
1190          let public_derives: Vec<_> = derives
1191            .into_iter()
1192            .filter(|t| !is_compiler_internal_trait(t))
1193            .collect();
1194
1195          if !public_derives.is_empty() {
1196            output.push_str("**Traits:** ");
1197            output.push_str(&public_derives.join(", "));
1198            output.push_str("\n\n");
1199          }
1200
1201          if !trait_with_methods.is_empty() {
1202            output.push_str("### Trait Implementations\n\n");
1203
1204            // Sort trait implementations alphabetically by trait path
1205            let mut sorted_trait_with_methods = trait_with_methods;
1206            sorted_trait_with_methods.sort_by(|a, b| a.0.path.cmp(&b.0.path));
1207
1208            for (trait_ref, methods) in sorted_trait_with_methods {
1209              output.push_str(&format!("#### {}\n\n", trait_ref.path));
1210              for (sig, links, doc) in methods {
1211                let links_json = format_links_as_json(&links);
1212                output.push_str(&format!(
1213                  "<RustCode inline code={{`{}`}} links={{{}}} />\n\n",
1214                  sig, links_json
1215                ));
1216                if let Some(doc) = doc {
1217                  output.push_str(&format!("{}\n\n", doc));
1218                }
1219                output.push_str("---\n\n");
1220              }
1221            }
1222          }
1223        }
1224      }
1225    }
1226    ItemEnum::Enum(e) => {
1227      // Format enum definition with links
1228      let (code, links) = format_enum_definition_with_links(name, e, item, crate_data);
1229      let links_json = format_links_as_json(&links);
1230      output.push_str(&format!(
1231        "<RustCode code={{`{}`}} links={{{}}} />\n\n",
1232        code, links_json
1233      ));
1234
1235      if let Some(docs) = &item.docs {
1236        output.push_str(&format!("{}\n\n", sanitize_docs_for_mdx(docs)));
1237      }
1238
1239      let non_synthetic_params: Vec<_> = e
1240        .generics
1241        .params
1242        .iter()
1243        .filter(|p| {
1244          !matches!(&p.kind, rustdoc_types::GenericParamDefKind::Lifetime { .. })
1245            || !is_synthetic_lifetime(&p.name)
1246        })
1247        .collect();
1248
1249      if !non_synthetic_params.is_empty() {
1250        output.push_str("### Generic Parameters\n\n");
1251        for param in non_synthetic_params {
1252          output.push_str(&format!("- {}\n", format_generic_param(param)));
1253        }
1254        output.push('\n');
1255      }
1256
1257      if !e.variants.is_empty() {
1258        output.push_str("### Variants\n\n");
1259        for variant_id in &e.variants {
1260          if let Some(variant) = crate_data.index.get(variant_id) {
1261            if let Some(variant_name) = &variant.name {
1262              let variant_kind = if let ItemEnum::Variant(v) = &variant.inner {
1263                match &v.kind {
1264                  rustdoc_types::VariantKind::Plain => None,
1265                  rustdoc_types::VariantKind::Tuple(fields) => {
1266                    let types: Vec<_> = fields
1267                      .iter()
1268                      .map(|field_id| {
1269                        if let Some(id) = field_id {
1270                          if let Some(field_item) = crate_data.index.get(id) {
1271                            if let ItemEnum::StructField(ty) = &field_item.inner {
1272                              return format_type_plain(ty, crate_data);
1273                            }
1274                          }
1275                        }
1276                        "?".to_string()
1277                      })
1278                      .collect();
1279                    Some(format!("({})", types.join(", ")))
1280                  }
1281                  rustdoc_types::VariantKind::Struct { fields, .. } => {
1282                    let field_list: Vec<String> = fields
1283                      .iter()
1284                      .filter_map(|field_id| {
1285                        crate_data.index.get(field_id).and_then(|f| {
1286                          f.name.as_ref().map(|name| {
1287                            let field_type = if let ItemEnum::StructField(ty) = &f.inner {
1288                              format_type_plain(ty, crate_data)
1289                            } else {
1290                              "?".to_string()
1291                            };
1292                            format!("{}: {}", name, field_type)
1293                          })
1294                        })
1295                      })
1296                      .collect();
1297                    Some(format!("{{ {} }}", field_list.join(", ")))
1298                  }
1299                }
1300              } else {
1301                None
1302              };
1303
1304              output.push_str("- `");
1305              output.push_str(variant_name);
1306              if let Some(kind) = variant_kind {
1307                output.push_str(&kind);
1308              }
1309              output.push('`');
1310
1311              if let Some(docs) = &variant.docs {
1312                let first_line = docs.lines().next().unwrap_or("").trim();
1313                if !first_line.is_empty() {
1314                  output.push_str(&format!(" - {}", first_line));
1315                }
1316              }
1317              output.push('\n');
1318            }
1319          }
1320        }
1321        output.push('\n');
1322      }
1323
1324      let (inherent_impls, trait_impls) = collect_impls_for_type(item_id, crate_data);
1325
1326      if !inherent_impls.is_empty() {
1327        output.push_str("### Methods\n\n");
1328        for impl_block in inherent_impls {
1329          let methods = format_impl_methods(impl_block, crate_data, Some(item));
1330          for (sig, links, doc) in methods {
1331            let links_json = format_links_as_json(&links);
1332            output.push_str(&format!(
1333              "<RustCode inline code={{`{}`}} links={{{}}} />\n\n",
1334              sig, links_json
1335            ));
1336            if let Some(doc) = doc {
1337              output.push_str(&format!("{}\n\n", doc));
1338            }
1339            output.push_str("---\n\n");
1340          }
1341        }
1342      }
1343
1344      if !trait_impls.is_empty() {
1345        let user_impls: Vec<_> = trait_impls
1346          .iter()
1347          .filter(|impl_block| !impl_block.is_synthetic && impl_block.blanket_impl.is_none())
1348          .collect();
1349
1350        if !user_impls.is_empty() {
1351          let mut derives = Vec::new();
1352          let mut trait_with_methods = Vec::new();
1353
1354          for impl_block in user_impls {
1355            if let Some(trait_ref) = &impl_block.trait_ {
1356              let methods = format_impl_methods(impl_block, crate_data, Some(item));
1357              if methods.is_empty() {
1358                derives.push(trait_ref.path.as_str());
1359              } else {
1360                trait_with_methods.push((trait_ref, methods));
1361              }
1362            }
1363          }
1364
1365          let public_derives: Vec<_> = derives
1366            .into_iter()
1367            .filter(|t| !is_compiler_internal_trait(t))
1368            .collect();
1369
1370          if !public_derives.is_empty() {
1371            output.push_str("**Traits:** ");
1372            output.push_str(&public_derives.join(", "));
1373            output.push_str("\n\n");
1374          }
1375
1376          if !trait_with_methods.is_empty() {
1377            output.push_str("### Trait Implementations\n\n");
1378
1379            // Sort trait implementations alphabetically by trait path
1380            let mut sorted_trait_with_methods = trait_with_methods;
1381            sorted_trait_with_methods.sort_by(|a, b| a.0.path.cmp(&b.0.path));
1382
1383            for (trait_ref, methods) in sorted_trait_with_methods {
1384              output.push_str(&format!("#### {}\n\n", trait_ref.path));
1385              for (sig, links, doc) in methods {
1386                let links_json = format_links_as_json(&links);
1387                output.push_str(&format!(
1388                  "<RustCode inline code={{`{}`}} links={{{}}} />\n\n",
1389                  sig, links_json
1390                ));
1391                if let Some(doc) = doc {
1392                  output.push_str(&format!("{}\n\n", doc));
1393                }
1394                output.push_str("---\n\n");
1395              }
1396            }
1397          }
1398        }
1399      }
1400    }
1401    ItemEnum::Function(f) => {
1402      output.push_str("*Function*\n\n");
1403
1404      if let Some(docs) = &item.docs {
1405        output.push_str(&format!("{}\n\n", sanitize_docs_for_mdx(docs)));
1406      }
1407
1408      // Format function definition with links
1409      let (code, links) = format_function_definition_with_links(name, f, item, crate_data);
1410      let links_json = format_links_as_json(&links);
1411      output.push_str(&format!(
1412        "<RustCode code={{`{}`}} links={{{}}} />\n\n",
1413        code, links_json
1414      ));
1415    }
1416    ItemEnum::Trait(t) => {
1417      // Add code signature like rustdoc
1418      output.push_str("```rust\n");
1419
1420      // Add visibility and trait keyword
1421      let visibility = match &item.visibility {
1422        rustdoc_types::Visibility::Public => "pub ",
1423        _ => "",
1424      };
1425
1426      output.push_str(&format!("{}trait {}", visibility, name));
1427
1428      // Show simplified trait signature
1429      output.push_str(" { /* ... */ }\n");
1430      output.push_str("```\n\n");
1431
1432      if let Some(docs) = &item.docs {
1433        output.push_str(&format!("{}\n\n", sanitize_docs_for_mdx(docs)));
1434      }
1435
1436      if !t.items.is_empty() {
1437        output.push_str("### Methods\n\n");
1438        for method_id in &t.items {
1439          if let Some(method) = crate_data.index.get(method_id) {
1440            if let Some(method_name) = &method.name {
1441              output.push_str(&format!("- `{}`", method_name));
1442              if let Some(method_docs) = &method.docs {
1443                output.push_str(&format!(": {}", method_docs.lines().next().unwrap_or("")));
1444              }
1445              output.push('\n');
1446            }
1447          }
1448        }
1449        output.push('\n');
1450      }
1451    }
1452    ItemEnum::Module(_) => {
1453      output.push_str(&format!("## Module: {}\n\n", name));
1454
1455      if let Some(docs) = &item.docs {
1456        output.push_str(&format!("{}\n\n", sanitize_docs_for_mdx(docs)));
1457      }
1458    }
1459    ItemEnum::Constant { .. } => {
1460      output.push_str(&format!("## {}\n\n", name));
1461      output.push_str("*Constant*\n\n");
1462
1463      if let Some(docs) = &item.docs {
1464        output.push_str(&format!("{}\n\n", sanitize_docs_for_mdx(docs)));
1465      }
1466    }
1467    ItemEnum::TypeAlias(ta) => {
1468      output.push_str(&format!("## {}\n\n", name));
1469      output.push_str(&format!(
1470        "*Type Alias*: `{}`\n\n",
1471        format_type(&ta.type_, crate_data)
1472      ));
1473
1474      if let Some(docs) = &item.docs {
1475        output.push_str(&format!("{}\n\n", sanitize_docs_for_mdx(docs)));
1476      }
1477    }
1478    _ => {
1479      return None;
1480    }
1481  }
1482
1483  Some(output)
1484}
1485
1486fn format_generic_param(param: &rustdoc_types::GenericParamDef) -> String {
1487  match &param.kind {
1488    rustdoc_types::GenericParamDefKind::Lifetime { .. } => {
1489      // Lifetime names already include the ' prefix in rustdoc JSON
1490      param.name.clone()
1491    }
1492    rustdoc_types::GenericParamDefKind::Type { .. } => param.name.clone(),
1493    rustdoc_types::GenericParamDefKind::Const { .. } => {
1494      format!("const {}", param.name)
1495    }
1496  }
1497}
1498
1499fn is_synthetic_lifetime(name: &str) -> bool {
1500  // Filter compiler-generated synthetic lifetimes
1501  name == "'_"
1502    || name.starts_with("'_") && name[2..].chars().all(|c| c.is_ascii_digit())
1503    || name.starts_with("'life") && name[5..].chars().all(|c| c.is_ascii_digit())
1504    || name == "'async_trait"
1505}
1506
1507fn is_compiler_internal_trait(trait_name: &str) -> bool {
1508  matches!(
1509    trait_name,
1510    "StructuralPartialEq" | "StructuralEq" | "Freeze" | "Unpin" | "RefUnwindSafe" | "UnwindSafe"
1511  )
1512}
1513
1514fn collect_impls_for_type<'a>(
1515  type_id: &rustdoc_types::Id,
1516  crate_data: &'a Crate,
1517) -> (Vec<&'a rustdoc_types::Impl>, Vec<&'a rustdoc_types::Impl>) {
1518  use rustdoc_types::Type;
1519
1520  let mut inherent_impls = Vec::new();
1521  let mut trait_impls = Vec::new();
1522
1523  for item in crate_data.index.values() {
1524    if let ItemEnum::Impl(impl_block) = &item.inner {
1525      let matches = match &impl_block.for_ {
1526        Type::ResolvedPath(path) => path.id == *type_id,
1527        _ => false,
1528      };
1529
1530      if matches {
1531        if impl_block.trait_.is_some() {
1532          trait_impls.push(impl_block);
1533        } else {
1534          inherent_impls.push(impl_block);
1535        }
1536      }
1537    }
1538  }
1539
1540  (inherent_impls, trait_impls)
1541}
1542
1543#[allow(clippy::type_complexity)]
1544fn format_impl_methods(
1545  impl_block: &rustdoc_types::Impl,
1546  crate_data: &Crate,
1547  parent_item: Option<&Item>,
1548) -> Vec<(String, Vec<(String, String)>, Option<String>)> {
1549  let mut methods = Vec::new();
1550
1551  for method_id in &impl_block.items {
1552    if let Some(method) = crate_data.index.get(method_id) {
1553      if let ItemEnum::Function(f) = &method.inner {
1554        if let Some(method_name) = &method.name {
1555          let (sig, links) =
1556            format_function_signature_with_links(method_name, f, crate_data, parent_item);
1557          let doc = method.docs.as_ref().and_then(|d| {
1558            let first_line = d.lines().next().unwrap_or("").trim();
1559            if !first_line.is_empty() {
1560              Some(first_line.to_string())
1561            } else {
1562              None
1563            }
1564          });
1565          methods.push((sig, links, doc));
1566        }
1567      }
1568    }
1569  }
1570
1571  methods
1572}
1573
1574#[allow(clippy::format_in_format_args)]
1575fn format_function_signature_with_links(
1576  name: &str,
1577  f: &rustdoc_types::Function,
1578  crate_data: &Crate,
1579  current_item: Option<&Item>,
1580) -> (String, Vec<(String, String)>) {
1581  let mut sig = format!("fn {}", name);
1582  let mut links = Vec::new();
1583
1584  let non_synthetic_params: Vec<String> = f
1585    .generics
1586    .params
1587    .iter()
1588    .filter(|p| {
1589      !matches!(&p.kind, rustdoc_types::GenericParamDefKind::Lifetime { .. })
1590        || !is_synthetic_lifetime(&p.name)
1591    })
1592    .map(format_generic_param)
1593    .collect();
1594
1595  if !non_synthetic_params.is_empty() {
1596    sig.push('<');
1597    sig.push_str(&non_synthetic_params.join(", "));
1598    sig.push('>');
1599  }
1600
1601  sig.push('(');
1602  let mut inputs = Vec::new();
1603  for (param_name, ty) in &f.sig.inputs {
1604    let (type_str, type_links) = format_type_with_links(ty, crate_data, current_item);
1605    links.extend(type_links);
1606    inputs.push(format!("{}: {}", param_name, type_str));
1607  }
1608
1609  // Format on multiple lines if signature is too long (> 80 chars) or has many parameters (> 3)
1610  let single_line = format!(
1611    "fn {}{}",
1612    if !non_synthetic_params.is_empty() {
1613      format!("{}<{}>", name, non_synthetic_params.join(", "))
1614    } else {
1615      name.to_string()
1616    },
1617    format!("({})", inputs.join(", "))
1618  );
1619
1620  if inputs.len() > 3 || single_line.len() > 80 {
1621    // Multi-line format
1622    sig = format!("fn {}", name);
1623    if !non_synthetic_params.is_empty() {
1624      sig.push('<');
1625      sig.push_str(&non_synthetic_params.join(", "));
1626      sig.push('>');
1627    }
1628    sig.push_str("(\n");
1629    for (i, input) in inputs.iter().enumerate() {
1630      sig.push_str(&format!("    {}", input));
1631      if i < inputs.len() - 1 {
1632        sig.push(',');
1633      }
1634      sig.push('\n');
1635    }
1636    sig.push(')');
1637  } else {
1638    // Single line format
1639    sig.push_str(&inputs.join(", "));
1640    sig.push(')');
1641  }
1642
1643  if let Some(output_type) = &f.sig.output {
1644    let (type_str, type_links) = format_type_with_links(output_type, crate_data, current_item);
1645    links.extend(type_links);
1646    sig.push_str(&format!(" -> {}", type_str));
1647  }
1648
1649  (sig, links)
1650}
1651
1652fn format_type(ty: &rustdoc_types::Type, crate_data: &Crate) -> String {
1653  format_type_depth(ty, crate_data, 0)
1654}
1655
1656fn format_type_depth(ty: &rustdoc_types::Type, crate_data: &Crate, depth: usize) -> String {
1657  const MAX_DEPTH: usize = 50;
1658
1659  if depth > MAX_DEPTH {
1660    return "...".to_string();
1661  }
1662
1663  use rustdoc_types::Type;
1664  match ty {
1665    Type::ResolvedPath(path) => {
1666      let short_name = get_short_type_name(&path.path);
1667      let link = Some(path.id)
1668        .as_ref()
1669        .and_then(|id| generate_type_link(&path.path, id, crate_data, None));
1670      let mut result = if let Some(link) = link {
1671        format!("[{}]({})", short_name, link)
1672      } else {
1673        short_name
1674      };
1675      if let Some(args) = &path.args {
1676        result.push_str(&format_generic_args(args, crate_data));
1677      }
1678      result
1679    }
1680    Type::DynTrait(dt) => {
1681      if let Some(first) = dt.traits.first() {
1682        let short_name = get_short_type_name(&first.trait_.path);
1683        let link = generate_type_link(&first.trait_.path, &first.trait_.id, crate_data, None);
1684        if let Some(link) = link {
1685          format!("dyn [{}]({})", short_name, link)
1686        } else {
1687          format!("dyn {}", short_name)
1688        }
1689      } else {
1690        "dyn Trait".to_string()
1691      }
1692    }
1693    Type::Generic(name) => name.clone(),
1694    Type::Primitive(name) => name.clone(),
1695    Type::FunctionPointer(_) => "fn(...)".to_string(),
1696    Type::Tuple(types) => {
1697      let formatted: Vec<_> = types
1698        .iter()
1699        .map(|t| format_type_depth(t, crate_data, depth + 1))
1700        .collect();
1701      format!("({})", formatted.join(", "))
1702    }
1703    Type::Slice(inner) => format!("[{}]", format_type_depth(inner, crate_data, depth + 1)),
1704    Type::Array { type_, len } => format!(
1705      "[{}; {}]",
1706      format_type_depth(type_, crate_data, depth + 1),
1707      len
1708    ),
1709    Type::Pat { type_, .. } => format_type_depth(type_, crate_data, depth + 1),
1710    Type::ImplTrait(_bounds) => "impl Trait".to_string(),
1711    Type::Infer => "_".to_string(),
1712    Type::RawPointer { is_mutable, type_ } => {
1713      if *is_mutable {
1714        format!("*mut {}", format_type_depth(type_, crate_data, depth + 1))
1715      } else {
1716        format!("*const {}", format_type_depth(type_, crate_data, depth + 1))
1717      }
1718    }
1719    Type::BorrowedRef {
1720      lifetime,
1721      is_mutable,
1722      type_,
1723    } => {
1724      let lifetime_str = lifetime.as_deref().unwrap_or("");
1725      let space = if lifetime_str.is_empty() { "" } else { " " };
1726      if *is_mutable {
1727        format!(
1728          "&{}{} mut {}",
1729          lifetime_str,
1730          space,
1731          format_type_depth(type_, crate_data, depth + 1)
1732        )
1733      } else {
1734        format!(
1735          "&{}{}{}",
1736          lifetime_str,
1737          space,
1738          format_type_depth(type_, crate_data, depth + 1)
1739        )
1740      }
1741    }
1742    Type::QualifiedPath {
1743      name,
1744      self_type,
1745      trait_,
1746      ..
1747    } => {
1748      if let Some(trait_) = trait_ {
1749        let trait_short = get_short_type_name(&trait_.path);
1750        let trait_link = generate_type_link(&trait_.path, &trait_.id, crate_data, None);
1751        let trait_part = if let Some(link) = trait_link {
1752          format!("[{}]({})", trait_short, link)
1753        } else {
1754          trait_short
1755        };
1756        format!(
1757          "<{} as {}>::{}",
1758          format_type_depth(self_type, crate_data, depth + 1),
1759          trait_part,
1760          name
1761        )
1762      } else {
1763        format!(
1764          "{}::{}",
1765          format_type_depth(self_type, crate_data, depth + 1),
1766          name
1767        )
1768      }
1769    }
1770  }
1771}
1772
1773/// Format a type without links (for use in code blocks)
1774fn format_type_plain(ty: &rustdoc_types::Type, crate_data: &Crate) -> String {
1775  use rustdoc_types::Type;
1776  match ty {
1777    Type::ResolvedPath(path) => {
1778      let short_name = get_short_type_name(&path.path);
1779      let mut result = short_name;
1780      if let Some(args) = &path.args {
1781        result.push_str(&format_generic_args_plain(args, crate_data));
1782      }
1783      result
1784    }
1785    Type::DynTrait(dt) => {
1786      if let Some(first) = dt.traits.first() {
1787        let short_name = get_short_type_name(&first.trait_.path);
1788        format!("dyn {}", short_name)
1789      } else {
1790        "dyn Trait".to_string()
1791      }
1792    }
1793    Type::Generic(name) => name.clone(),
1794    Type::Primitive(name) => name.clone(),
1795    Type::FunctionPointer(_) => "fn(...)".to_string(),
1796    Type::Tuple(types) => {
1797      let formatted: Vec<_> = types
1798        .iter()
1799        .map(|t| format_type_plain(t, crate_data))
1800        .collect();
1801      format!("({})", formatted.join(", "))
1802    }
1803    Type::Slice(inner) => format!("[{}]", format_type_plain(inner, crate_data)),
1804    Type::Array { type_, len } => format!("[{}; {}]", format_type_plain(type_, crate_data), len),
1805    Type::Pat { type_, .. } => format_type_plain(type_, crate_data),
1806    Type::ImplTrait(_bounds) => "impl Trait".to_string(),
1807    Type::Infer => "_".to_string(),
1808    Type::RawPointer { is_mutable, type_ } => {
1809      if *is_mutable {
1810        format!("*mut {}", format_type_plain(type_, crate_data))
1811      } else {
1812        format!("*const {}", format_type_plain(type_, crate_data))
1813      }
1814    }
1815    Type::BorrowedRef {
1816      lifetime,
1817      is_mutable,
1818      type_,
1819    } => {
1820      let lifetime_str = lifetime.as_deref().unwrap_or("");
1821      let space = if lifetime_str.is_empty() { "" } else { " " };
1822      if *is_mutable {
1823        format!(
1824          "&{}{} mut {}",
1825          lifetime_str,
1826          space,
1827          format_type_plain(type_, crate_data)
1828        )
1829      } else {
1830        format!(
1831          "&{}{}{}",
1832          lifetime_str,
1833          space,
1834          format_type_plain(type_, crate_data)
1835        )
1836      }
1837    }
1838    Type::QualifiedPath {
1839      name,
1840      self_type,
1841      trait_,
1842      ..
1843    } => {
1844      if let Some(trait_) = trait_ {
1845        let trait_short = get_short_type_name(&trait_.path);
1846        format!(
1847          "<{} as {}>::{}",
1848          format_type_plain(self_type, crate_data),
1849          trait_short,
1850          name
1851        )
1852      } else {
1853        format!("{}::{}", format_type_plain(self_type, crate_data), name)
1854      }
1855    }
1856  }
1857}
1858
1859fn format_generic_args_plain(args: &rustdoc_types::GenericArgs, crate_data: &Crate) -> String {
1860  use rustdoc_types::{GenericArg, GenericArgs};
1861  match args {
1862    GenericArgs::AngleBracketed { args, .. } => {
1863      if args.is_empty() {
1864        String::new()
1865      } else {
1866        let formatted: Vec<String> = args
1867          .iter()
1868          .filter_map(|arg| match arg {
1869            GenericArg::Lifetime(lt) if lt != "'_" => Some(lt.clone()),
1870            GenericArg::Lifetime(_) => None,
1871            GenericArg::Type(ty) => Some(format_type_plain(ty, crate_data)),
1872            GenericArg::Const(c) => Some(c.expr.clone()),
1873            GenericArg::Infer => Some("_".to_string()),
1874          })
1875          .collect();
1876        if formatted.is_empty() {
1877          String::new()
1878        } else {
1879          format!("<{}>", formatted.join(", "))
1880        }
1881      }
1882    }
1883    GenericArgs::Parenthesized { inputs, output } => {
1884      let inputs_str: Vec<_> = inputs
1885        .iter()
1886        .map(|t| format_type_plain(t, crate_data))
1887        .collect();
1888      let mut result = format!("({})", inputs_str.join(", "));
1889      if let Some(output) = output {
1890        result.push_str(&format!(" -> {}", format_type_plain(output, crate_data)));
1891      }
1892      result
1893    }
1894    GenericArgs::ReturnTypeNotation => "(..)".to_string(),
1895  }
1896}
1897
1898fn get_short_type_name(full_path: &str) -> String {
1899  full_path
1900    .split("::")
1901    .last()
1902    .unwrap_or(full_path)
1903    .to_string()
1904}
1905
1906fn format_links_as_json(links: &[(String, String)]) -> String {
1907  if links.is_empty() {
1908    return "[]".to_string();
1909  }
1910
1911  let items: Vec<String> = links
1912    .iter()
1913    .map(|(text, href)| {
1914      // Escape quotes in text and href
1915      let text_escaped = text.replace('\\', "\\\\").replace('"', "\\\"");
1916      let href_escaped = href.replace('\\', "\\\\").replace('"', "\\\"");
1917      // Use quoted keys for MDX/JSX compatibility
1918      format!(
1919        r#"{{"text": "{}", "href": "{}"}}"#,
1920        text_escaped, href_escaped
1921      )
1922    })
1923    .collect();
1924
1925  format!("[{}]", items.join(", "))
1926}
1927
1928/// Sanitize documentation comments for MDX compatibility
1929///
1930/// MDX is stricter than regular markdown about HTML tags. This function ensures
1931/// that HTML blocks (like <details>) are properly separated from text paragraphs
1932/// with blank lines.
1933fn sanitize_docs_for_mdx(docs: &str) -> String {
1934  let lines: Vec<&str> = docs.lines().collect();
1935  let mut result: Vec<String> = Vec::new();
1936  let mut i = 0;
1937
1938  while i < lines.len() {
1939    let current_line = lines[i];
1940    let trimmed = current_line.trim();
1941
1942    // Check if this line starts with an HTML opening tag
1943    if trimmed.starts_with('<') && !trimmed.starts_with("</") {
1944      // Extract tag name (e.g., "details" from "<details>")
1945      if let Some(tag_end) = trimmed.find(|c: char| ['>', ' '].contains(&c)) {
1946        let tag_name = &trimmed[1..tag_end];
1947
1948        // Only process block-level HTML tags
1949        if matches!(
1950          tag_name,
1951          "details" | "summary" | "div" | "table" | "pre" | "blockquote"
1952        ) {
1953          // Ensure blank line before the HTML block
1954          if !result.is_empty() && !result.last().unwrap().is_empty() {
1955            result.push(String::new());
1956          }
1957
1958          // Split multiple HTML tags on the same line (e.g., "<details><summary>")
1959          // MDX requires each tag to be on its own line
1960          let mut current_line_content = trimmed.to_string();
1961          while !current_line_content.is_empty() {
1962            if let Some(tag_start) = current_line_content.find('<') {
1963              // Add any content before the tag
1964              if tag_start > 0 {
1965                result.push(current_line_content[..tag_start].to_string());
1966              }
1967
1968              // Find the end of this tag
1969              if let Some(tag_end) = current_line_content[tag_start..].find('>') {
1970                let tag_end_abs = tag_start + tag_end + 1;
1971                result.push(current_line_content[tag_start..tag_end_abs].to_string());
1972                current_line_content = current_line_content[tag_end_abs..].to_string();
1973              } else {
1974                // Malformed tag, just add the rest
1975                result.push(current_line_content.clone());
1976                break;
1977              }
1978            } else {
1979              // No more tags, add remaining content if any
1980              if !current_line_content.trim().is_empty() {
1981                result.push(current_line_content.clone());
1982              }
1983              break;
1984            }
1985          }
1986
1987          // Continue adding lines until we find the closing tag
1988          i += 1;
1989          while i < lines.len() {
1990            let next_line = lines[i];
1991            let next_trimmed = next_line.trim();
1992
1993            // Check if we found the closing tag
1994            if next_trimmed.contains(&format!("</{}>", tag_name)) {
1995              // Split this line too in case it has multiple tags
1996              let mut current_line_content = next_trimmed.to_string();
1997              while !current_line_content.is_empty() {
1998                if let Some(tag_start) = current_line_content.find('<') {
1999                  if tag_start > 0 {
2000                    result.push(current_line_content[..tag_start].to_string());
2001                  }
2002                  if let Some(tag_end) = current_line_content[tag_start..].find('>') {
2003                    let tag_end_abs = tag_start + tag_end + 1;
2004                    result.push(current_line_content[tag_start..tag_end_abs].to_string());
2005                    current_line_content = current_line_content[tag_end_abs..].to_string();
2006                  } else {
2007                    result.push(current_line_content.clone());
2008                    break;
2009                  }
2010                } else {
2011                  if !current_line_content.trim().is_empty() {
2012                    result.push(current_line_content.clone());
2013                  }
2014                  break;
2015                }
2016              }
2017              i += 1;
2018              break;
2019            } else {
2020              // Trim HTML lines to avoid indentation issues with MDX
2021              result.push(next_trimmed.to_string());
2022            }
2023            i += 1;
2024          }
2025
2026          // Ensure blank line after the HTML block
2027          if i < lines.len() && !lines[i].trim().is_empty() {
2028            result.push(String::new());
2029          }
2030          continue;
2031        }
2032      }
2033    }
2034
2035    // Preserve original line (don't trim it)
2036    result.push(current_line.to_string());
2037    i += 1;
2038  }
2039
2040  result.join("\n")
2041}
2042
2043fn generate_type_link(
2044  full_path: &str,
2045  item_id: &Id,
2046  crate_data: &Crate,
2047  current_item: Option<&Item>,
2048) -> Option<String> {
2049  generate_type_link_depth(full_path, item_id, crate_data, current_item, 0)
2050}
2051
2052#[allow(clippy::bind_instead_of_map)]
2053fn generate_type_link_depth(
2054  full_path: &str,
2055  item_id: &Id,
2056  crate_data: &Crate,
2057  current_item: Option<&Item>,
2058  depth: usize,
2059) -> Option<String> {
2060  const MAX_DEPTH: usize = 10;
2061
2062  if depth >= MAX_DEPTH {
2063    return None;
2064  }
2065
2066  // Always prefer the path from crate_data.paths when available,
2067  // as it contains the most accurate full path information
2068  // BUT: if depth > 0, we're in a recursive call and should trust the provided full_path
2069  let full_path = if depth == 0 {
2070    if let Some(path_info) = crate_data.paths.get(item_id) {
2071      path_info.path.join("::")
2072    } else if full_path.starts_with("$crate") {
2073      // Fallback for $crate placeholder
2074      full_path.replace("$crate", "unknown")
2075    } else {
2076      full_path.to_string()
2077    }
2078  } else {
2079    full_path.to_string()
2080  };
2081  let full_path = full_path.as_str();
2082
2083  // Check if this is a local or external type by looking at crate_id in paths
2084  let is_local = if let Some(path_info) = crate_data.paths.get(item_id) {
2085    path_info.crate_id == 0
2086  } else {
2087    // If not in paths, check if it's in the current crate's index
2088    crate_data.index.contains_key(item_id)
2089  };
2090
2091  // Handle local types (from the current crate)
2092  if is_local {
2093    if let Some(item) = crate_data.index.get(item_id) {
2094      // Local type - we need to build the full path from the item's location
2095      // Get the crate name
2096      let _crate_name = crate_data.index.get(&crate_data.root)?.name.as_ref()?;
2097
2098      // Get the item prefix (struct., enum., trait., etc.)
2099      let prefix = get_item_prefix(item);
2100
2101      // Extract module path for the target item
2102      // First try from crate_data.paths as it's more reliable
2103      let target_module_path = if let Some(path_info) = crate_data.paths.get(item_id) {
2104        // Get the full path from paths (e.g., ["test_crate", "patterns", "Builder"])
2105        let path_components: Vec<&str> = path_info.path.iter().map(|s| s.as_str()).collect();
2106        if path_components.len() > 2 {
2107          // Skip crate name and item name, join the middle parts
2108          // e.g., ["test_crate", "patterns", "Builder"] -> "patterns"
2109          Some(path_components[1..path_components.len() - 1].join("/"))
2110        } else {
2111          // Root module (only crate name and item name)
2112          Some("".to_string())
2113        }
2114      } else if let Some(span) = &item.span {
2115        // Fallback to span if paths is not available
2116        let span_filename = &span.filename;
2117        if let Some(filename_str) = span_filename.to_str() {
2118          if let Some(src_idx) = filename_str.rfind("/src/") {
2119            let after_src = &filename_str[src_idx + 5..];
2120            if let Some(rs_idx) = after_src.rfind(".rs") {
2121              let module_path = &after_src[..rs_idx];
2122              if module_path == "lib" || module_path == "main" {
2123                Some("".to_string())
2124              } else {
2125                Some(module_path.to_string())
2126              }
2127            } else {
2128              None
2129            }
2130          } else {
2131            None
2132          }
2133        } else {
2134          None
2135        }
2136      } else {
2137        None
2138      };
2139
2140      // Extract module path for the current item (if provided)
2141      let _current_module_path = if let Some(current) = current_item {
2142        // First try to get it from span
2143        if let Some(span) = &current.span {
2144          let span_filename = &span.filename;
2145          if let Some(filename_str) = span_filename.to_str() {
2146            if let Some(src_idx) = filename_str.rfind("/src/") {
2147              let after_src = &filename_str[src_idx + 5..];
2148              if let Some(rs_idx) = after_src.rfind(".rs") {
2149                let module_path = &after_src[..rs_idx];
2150                if module_path == "lib" || module_path == "main" {
2151                  Some("".to_string())
2152                } else {
2153                  Some(module_path.to_string())
2154                }
2155              } else {
2156                None
2157              }
2158            } else {
2159              None
2160            }
2161          } else {
2162            None
2163          }
2164        } else {
2165          // If no span (e.g., re-export), try to infer from the item's path in paths
2166          // Get the item's id and look it up in paths
2167          crate_data.paths.get(&current.id).map(|path_info| {
2168            let full_path: Vec<&str> = path_info.path.iter().map(|s| s.as_str()).collect();
2169            if full_path.len() > 2 {
2170              // Skip crate name and item name, join the middle parts
2171              full_path[1..full_path.len() - 1].join("/")
2172            } else {
2173              // Root module
2174              String::new()
2175            }
2176          })
2177        }
2178      } else {
2179        None
2180      };
2181
2182      // The full_path might be just the type name or include module path
2183      let path_segments: Vec<&str> = full_path.split("::").collect();
2184      let type_name = path_segments.last().unwrap_or(&full_path);
2185
2186      // Generate absolute link from crate root
2187      // This works for both original files and re-exports without any path calculations
2188      if let Some(target_path) = target_module_path {
2189        let crate_name = path_segments.first().unwrap_or(&"");
2190
2191        let base = BASE_PATH.with(|bp| bp.borrow().clone());
2192        let base_prefix = if base.is_empty() { String::new() } else { base };
2193
2194        if target_path.is_empty() {
2195          // Target is in root module: /base_path/crate_name/struct.TypeName
2196          return Some(format!(
2197            "{}/{}/{}{}",
2198            base_prefix, crate_name, prefix, type_name
2199          ));
2200        } else {
2201          // Target is in a nested module: /base_path/crate_name/module/path/struct.TypeName
2202          return Some(format!(
2203            "{}/{}/{}/{}{}",
2204            base_prefix, crate_name, target_path, prefix, type_name
2205          ));
2206        }
2207      } else {
2208        // Fallback: use crate root path
2209        let crate_name = path_segments.first().unwrap_or(&"");
2210        let base = BASE_PATH.with(|bp| bp.borrow().clone());
2211        let base_prefix = if base.is_empty() { String::new() } else { base };
2212        return Some(format!(
2213          "{}/{}/{}{}",
2214          base_prefix, crate_name, prefix, type_name
2215        ));
2216      }
2217    } // end if let Some(item)
2218  } // end if is_local
2219
2220  // External type - check if it's std/core/alloc first, then try docs.rs
2221  let path_parts: Vec<&str> = full_path.split("::").collect();
2222
2223  if path_parts.len() >= 2 {
2224    let crate_name = path_parts[0];
2225
2226    // Check if it's a standard library crate (std, core, alloc)
2227    if crate_name == "std" || crate_name == "core" || crate_name == "alloc" {
2228      // Build doc.rust-lang.org URL
2229      // e.g., std::sync::Arc -> https://doc.rust-lang.org/std/sync/struct.Arc.html
2230      // e.g., core::fmt::Formatter -> https://doc.rust-lang.org/core/fmt/struct.Formatter.html
2231
2232      let type_name = path_parts.last()?;
2233
2234      // Special handling for type aliases that should redirect to their canonical type
2235      // Only redirect specific known aliases, not all instances of these types
2236      if full_path == "core::fmt::Result" || full_path == "std::fmt::Result" {
2237        // core::fmt::Result is an alias for Result<(), fmt::Error>
2238        // Better to link to the generic Result documentation
2239        return Some("https://doc.rust-lang.org/std/result/enum.Result.html".to_string());
2240      }
2241
2242      // For core types, prefer linking to std documentation when available
2243      // (std is re-exported and more familiar to users)
2244      if crate_name == "core" {
2245        match full_path {
2246          "core::result::Result" => {
2247            return Some("https://doc.rust-lang.org/std/result/enum.Result.html".to_string());
2248          }
2249          "core::option::Option" => {
2250            return Some("https://doc.rust-lang.org/std/option/enum.Option.html".to_string());
2251          }
2252          _ => {}
2253        }
2254      }
2255
2256      // Filter out internal implementation modules
2257      let mut module_parts: Vec<&str> = path_parts[1..path_parts.len() - 1].to_vec();
2258      let internal_modules = ["bounded", "unbounded", "inner", "private", "imp"];
2259      module_parts.retain(|part| !internal_modules.contains(part));
2260      let module_path = module_parts.join("/");
2261
2262      // Try to guess the item type from common patterns
2263      let item_type =
2264        if type_name.ends_with("Error") || *type_name == "Option" || *type_name == "Result" {
2265          "enum"
2266        } else {
2267          "struct" // Default to struct for most std types
2268        };
2269
2270      return Some(format!(
2271        "https://doc.rust-lang.org/{}/{}/{}.{}.html",
2272        crate_name, module_path, item_type, type_name
2273      ));
2274    }
2275
2276    // External crate - use crate_id to get the REAL crate name
2277    // (not the first path segment which might be a module name)
2278    let real_crate_name = if let Some(path_info) = crate_data.paths.get(item_id) {
2279      if path_info.crate_id != 0 {
2280        // It's from an external crate - look up the real name
2281        crate_data
2282          .external_crates
2283          .get(&path_info.crate_id)
2284          .map(|c| c.name.as_str())
2285          .unwrap_or(crate_name)
2286      } else {
2287        // It's from the current crate
2288        crate_name
2289      }
2290    } else {
2291      crate_name
2292    };
2293
2294    // Check if this external crate is part of the workspace
2295    // If so, generate an internal link instead of docs.rs
2296    // Note: Normalize both names by replacing hyphens with underscores
2297    // because crate names in Cargo.toml use hyphens but rustdoc uses underscores
2298    let normalized_crate_name = real_crate_name.replace('-', "_");
2299    let is_workspace_crate = WORKSPACE_CRATES.with(|wc| {
2300      wc.borrow().iter().any(|c| {
2301        let normalized_c = c.replace('-', "_");
2302        normalized_c == normalized_crate_name
2303      })
2304    });
2305
2306    if is_workspace_crate {
2307      // Generate internal link for workspace crate
2308      // Get the item prefix
2309      let prefix = crate_data
2310        .paths
2311        .get(item_id)
2312        .map(|p| match p.kind {
2313          rustdoc_types::ItemKind::Struct => "struct.",
2314          rustdoc_types::ItemKind::Enum => "enum.",
2315          rustdoc_types::ItemKind::Trait => "trait.",
2316          rustdoc_types::ItemKind::Function => "fn.",
2317          rustdoc_types::ItemKind::TypeAlias => "type.",
2318          rustdoc_types::ItemKind::Constant => "constant.",
2319          _ => "struct.",
2320        })
2321        .unwrap_or("struct.");
2322
2323      let type_name = path_parts.last()?;
2324
2325      // Get module path
2326      let mut module_parts: Vec<&str> = path_parts[1..path_parts.len() - 1].to_vec();
2327      let internal_modules = ["bounded", "unbounded", "inner", "private", "imp"];
2328      module_parts.retain(|part| !internal_modules.contains(part));
2329      let module_path = module_parts.join("/");
2330
2331      let base = BASE_PATH.with(|bp| bp.borrow().clone());
2332      let base_prefix = if base.is_empty() { String::new() } else { base };
2333
2334      if module_path.is_empty() {
2335        // Top-level type: /base_path/crate_name/struct.TypeName
2336        return Some(format!(
2337          "{}/{}/{}{}",
2338          base_prefix, real_crate_name, prefix, type_name
2339        ));
2340      } else {
2341        // Nested module: /base_path/crate_name/module/path/struct.TypeName
2342        return Some(format!(
2343          "{}/{}/{}/{}{}",
2344          base_prefix, real_crate_name, module_path, prefix, type_name
2345        ));
2346      }
2347    }
2348
2349    // Not a workspace crate - generate docs.rs link
2350    // Try to get the item kind from paths to generate correct URL
2351    let item_kind = crate_data
2352      .paths
2353      .get(item_id)
2354      .map(|p| &p.kind)
2355      .and_then(|k| match k {
2356        rustdoc_types::ItemKind::Struct => Some("struct"),
2357        rustdoc_types::ItemKind::Enum => Some("enum"),
2358        rustdoc_types::ItemKind::Trait => Some("trait"),
2359        rustdoc_types::ItemKind::Function => Some("fn"),
2360        rustdoc_types::ItemKind::TypeAlias => Some("type"),
2361        rustdoc_types::ItemKind::Constant => Some("constant"),
2362        _ => Some("struct"), // Default
2363      })
2364      .unwrap_or("struct");
2365
2366    let type_name = path_parts.last()?;
2367
2368    // Get module path, filtering out common internal implementation modules
2369    let mut module_parts: Vec<&str> = path_parts[1..path_parts.len() - 1].to_vec();
2370
2371    // Remove common internal module names that are typically not in public re-exports
2372    // These are often implementation details that docs.rs doesn't expose in URLs
2373    let internal_modules = ["bounded", "unbounded", "inner", "private", "imp"];
2374    module_parts.retain(|part| !internal_modules.contains(part));
2375
2376    let module_path = module_parts.join("/");
2377
2378    // Format: https://docs.rs/crate_name/latest/crate_name/module/path/struct.TypeName.html
2379    if module_path.is_empty() {
2380      // Top-level type in crate
2381      return Some(format!(
2382        "https://docs.rs/{}/latest/{}/{}.{}.html",
2383        real_crate_name, real_crate_name, item_kind, type_name
2384      ));
2385    } else {
2386      return Some(format!(
2387        "https://docs.rs/{}/latest/{}/{}/{}.{}.html",
2388        real_crate_name, real_crate_name, module_path, item_kind, type_name
2389      ));
2390    }
2391  }
2392
2393  // Single-segment path - try to find in paths first (could be from any crate)
2394  if path_parts.len() == 1 {
2395    let type_name = path_parts[0];
2396
2397    // Check if we have this type in paths (could be external or std)
2398    if let Some(path_info) = crate_data.paths.get(item_id) {
2399      let full_path_from_paths = path_info.path.join("::");
2400      // Only recurse if the resolved path is different from the input path
2401      if full_path_from_paths != full_path {
2402        // Recursively call with the full path
2403        return generate_type_link_depth(
2404          &full_path_from_paths,
2405          item_id,
2406          crate_data,
2407          current_item,
2408          depth + 1,
2409        );
2410      }
2411    }
2412
2413    // Fallback: common std library types (for backward compatibility)
2414    match type_name {
2415      "String" => {
2416        return Some("https://doc.rust-lang.org/std/string/struct.String.html".to_string());
2417      }
2418      "Vec" => return Some("https://doc.rust-lang.org/std/vec/struct.Vec.html".to_string()),
2419      "Option" => return Some("https://doc.rust-lang.org/std/option/enum.Option.html".to_string()),
2420      "Result" => return Some("https://doc.rust-lang.org/std/result/enum.Result.html".to_string()),
2421      "Box" => return Some("https://doc.rust-lang.org/std/boxed/struct.Box.html".to_string()),
2422      "Rc" => return Some("https://doc.rust-lang.org/std/rc/struct.Rc.html".to_string()),
2423      "Arc" => return Some("https://doc.rust-lang.org/std/sync/struct.Arc.html".to_string()),
2424      "HashMap" => {
2425        return Some("https://doc.rust-lang.org/std/collections/struct.HashMap.html".to_string());
2426      }
2427      "HashSet" => {
2428        return Some("https://doc.rust-lang.org/std/collections/struct.HashSet.html".to_string());
2429      }
2430      "BTreeMap" => {
2431        return Some("https://doc.rust-lang.org/std/collections/struct.BTreeMap.html".to_string());
2432      }
2433      "BTreeSet" => {
2434        return Some("https://doc.rust-lang.org/std/collections/struct.BTreeSet.html".to_string());
2435      }
2436      "Mutex" => return Some("https://doc.rust-lang.org/std/sync/struct.Mutex.html".to_string()),
2437      "RwLock" => return Some("https://doc.rust-lang.org/std/sync/struct.RwLock.html".to_string()),
2438      "Cell" => return Some("https://doc.rust-lang.org/std/cell/struct.Cell.html".to_string()),
2439      "RefCell" => {
2440        return Some("https://doc.rust-lang.org/std/cell/struct.RefCell.html".to_string());
2441      }
2442      "Path" => return Some("https://doc.rust-lang.org/std/path/struct.Path.html".to_string()),
2443      "PathBuf" => {
2444        return Some("https://doc.rust-lang.org/std/path/struct.PathBuf.html".to_string());
2445      }
2446      _ => {}
2447    }
2448  }
2449
2450  None
2451}
2452
2453fn format_type_with_links(
2454  ty: &rustdoc_types::Type,
2455  crate_data: &Crate,
2456  current_item: Option<&Item>,
2457) -> (String, Vec<(String, String)>) {
2458  format_type_with_links_depth(ty, crate_data, current_item, 0)
2459}
2460
2461fn format_type_with_links_depth(
2462  ty: &rustdoc_types::Type,
2463  crate_data: &Crate,
2464  current_item: Option<&Item>,
2465  depth: usize,
2466) -> (String, Vec<(String, String)>) {
2467  const MAX_DEPTH: usize = 50;
2468
2469  if depth > MAX_DEPTH {
2470    return ("...".to_string(), Vec::new());
2471  }
2472
2473  use rustdoc_types::Type;
2474  let mut links = Vec::new();
2475
2476  let type_str = match ty {
2477    Type::ResolvedPath(path) => {
2478      let short_name = get_short_type_name(&path.path);
2479      if let Some(link) = Some(path.id)
2480        .as_ref()
2481        .and_then(|id| generate_type_link(&path.path, id, crate_data, current_item))
2482      {
2483        links.push((short_name.clone(), link));
2484      }
2485      let mut result = short_name;
2486      if let Some(args) = &path.args {
2487        let (args_str, args_links) = format_generic_args_with_links(args, crate_data, current_item);
2488        links.extend(args_links);
2489        result.push_str(&args_str);
2490      }
2491      result
2492    }
2493    Type::DynTrait(dt) => {
2494      if let Some(first) = dt.traits.first() {
2495        let short_name = get_short_type_name(&first.trait_.path);
2496        if let Some(link) = generate_type_link(
2497          &first.trait_.path,
2498          &first.trait_.id,
2499          crate_data,
2500          current_item,
2501        ) {
2502          links.push((short_name.clone(), link));
2503        }
2504        format!("dyn {}", short_name)
2505      } else {
2506        "dyn Trait".to_string()
2507      }
2508    }
2509    Type::Generic(name) => name.clone(),
2510    Type::Primitive(name) => name.clone(),
2511    Type::FunctionPointer(_) => "fn(...)".to_string(),
2512    Type::Tuple(types) => {
2513      let mut parts = Vec::new();
2514      for t in types {
2515        let (type_str, type_links) =
2516          format_type_with_links_depth(t, crate_data, current_item, depth + 1);
2517        links.extend(type_links);
2518        parts.push(type_str);
2519      }
2520      format!("({})", parts.join(", "))
2521    }
2522    Type::Slice(inner) => {
2523      let (inner_str, inner_links) =
2524        format_type_with_links_depth(inner, crate_data, current_item, depth + 1);
2525      links.extend(inner_links);
2526      format!("[{}]", inner_str)
2527    }
2528    Type::Array { type_, len } => {
2529      let (type_str, type_links) =
2530        format_type_with_links_depth(type_, crate_data, current_item, depth + 1);
2531      links.extend(type_links);
2532      format!("[{}; {}]", type_str, len)
2533    }
2534    Type::Pat { type_, .. } => {
2535      let (type_str, type_links) =
2536        format_type_with_links_depth(type_, crate_data, current_item, depth + 1);
2537      links.extend(type_links);
2538      type_str
2539    }
2540    Type::ImplTrait(bounds) => {
2541      // Extract links from trait bounds in impl Trait
2542      for bound in bounds {
2543        if let rustdoc_types::GenericBound::TraitBound { trait_, .. } = bound {
2544          let short_name = get_short_type_name(&trait_.path);
2545          if let Some(link) = generate_type_link(&trait_.path, &trait_.id, crate_data, current_item)
2546          {
2547            links.push((short_name, link));
2548          }
2549          // Also extract links from generic arguments (e.g., Into<T>)
2550          if let Some(args) = &trait_.args {
2551            let (_, args_links) = format_generic_args_with_links(args, crate_data, current_item);
2552            links.extend(args_links);
2553          }
2554        }
2555      }
2556      "impl Trait".to_string()
2557    }
2558    Type::Infer => "_".to_string(),
2559    Type::RawPointer { is_mutable, type_ } => {
2560      let (type_str, type_links) =
2561        format_type_with_links_depth(type_, crate_data, current_item, depth + 1);
2562      links.extend(type_links);
2563      if *is_mutable {
2564        format!("*mut {}", type_str)
2565      } else {
2566        format!("*const {}", type_str)
2567      }
2568    }
2569    Type::BorrowedRef {
2570      lifetime,
2571      is_mutable,
2572      type_,
2573    } => {
2574      let (type_str, type_links) =
2575        format_type_with_links_depth(type_, crate_data, current_item, depth + 1);
2576      links.extend(type_links);
2577      let lifetime_str = lifetime.as_deref().unwrap_or("");
2578      let space = if lifetime_str.is_empty() { "" } else { " " };
2579      if *is_mutable {
2580        format!("&{}{} mut {}", lifetime_str, space, type_str)
2581      } else {
2582        format!("&{}{}{}", lifetime_str, space, type_str)
2583      }
2584    }
2585    Type::QualifiedPath {
2586      name,
2587      self_type,
2588      trait_,
2589      ..
2590    } => {
2591      let (self_str, self_links) =
2592        format_type_with_links_depth(self_type, crate_data, current_item, depth + 1);
2593      links.extend(self_links);
2594      if let Some(trait_) = trait_ {
2595        let trait_short = get_short_type_name(&trait_.path);
2596        if let Some(link) = generate_type_link(&trait_.path, &trait_.id, crate_data, current_item) {
2597          links.push((trait_short.clone(), link));
2598        }
2599        format!("<{} as {}>::{}", self_str, trait_short, name)
2600      } else {
2601        format!("{}::{}", self_str, name)
2602      }
2603    }
2604  };
2605
2606  (type_str, links)
2607}
2608
2609fn format_generic_args_with_links(
2610  args: &rustdoc_types::GenericArgs,
2611  crate_data: &Crate,
2612  current_item: Option<&Item>,
2613) -> (String, Vec<(String, String)>) {
2614  use rustdoc_types::{GenericArg, GenericArgs};
2615  let mut links = Vec::new();
2616
2617  let args_str = match args {
2618    GenericArgs::AngleBracketed { args, .. } => {
2619      if args.is_empty() {
2620        String::new()
2621      } else {
2622        let mut formatted = Vec::new();
2623        for arg in args {
2624          match arg {
2625            GenericArg::Type(ty) => {
2626              let (type_str, type_links) = format_type_with_links(ty, crate_data, current_item);
2627              links.extend(type_links);
2628              formatted.push(type_str);
2629            }
2630            GenericArg::Lifetime(lt) => {
2631              if !is_synthetic_lifetime(lt) {
2632                formatted.push(lt.clone());
2633              }
2634            }
2635            _ => {}
2636          }
2637        }
2638        if formatted.is_empty() {
2639          String::new()
2640        } else {
2641          format!("<{}>", formatted.join(", "))
2642        }
2643      }
2644    }
2645    GenericArgs::Parenthesized { inputs, output } => {
2646      let mut inputs_parts = Vec::new();
2647      for input in inputs {
2648        let (type_str, type_links) = format_type_with_links(input, crate_data, current_item);
2649        links.extend(type_links);
2650        inputs_parts.push(type_str);
2651      }
2652      if let Some(out) = output {
2653        let (out_str, out_links) = format_type_with_links(out, crate_data, current_item);
2654        links.extend(out_links);
2655        format!("({}) -> {}", inputs_parts.join(", "), out_str)
2656      } else {
2657        format!("({})", inputs_parts.join(", "))
2658      }
2659    }
2660    GenericArgs::ReturnTypeNotation => String::new(),
2661  };
2662
2663  (args_str, links)
2664}
2665
2666fn format_generic_args(args: &rustdoc_types::GenericArgs, crate_data: &Crate) -> String {
2667  use rustdoc_types::{GenericArg, GenericArgs};
2668  match args {
2669    GenericArgs::AngleBracketed { args, .. } => {
2670      if args.is_empty() {
2671        String::new()
2672      } else {
2673        let formatted: Vec<String> = args
2674          .iter()
2675          .filter_map(|arg| match arg {
2676            GenericArg::Lifetime(lt) if lt != "'_" => Some(lt.clone()),
2677            GenericArg::Lifetime(_) => None,
2678            GenericArg::Type(ty) => Some(format_type(ty, crate_data)),
2679            GenericArg::Const(c) => Some(c.expr.clone()),
2680            GenericArg::Infer => Some("_".to_string()),
2681          })
2682          .collect();
2683        if formatted.is_empty() {
2684          String::new()
2685        } else {
2686          format!("<{}>", formatted.join(", "))
2687        }
2688      }
2689    }
2690    GenericArgs::Parenthesized { inputs, output } => {
2691      let inputs_str: Vec<_> = inputs.iter().map(|t| format_type(t, crate_data)).collect();
2692      let mut result = format!("({})", inputs_str.join(", "));
2693      if let Some(output) = output {
2694        result.push_str(&format!(" -> {}", format_type(output, crate_data)));
2695      }
2696      result
2697    }
2698    GenericArgs::ReturnTypeNotation => "(..)".to_string(),
2699  }
2700}
2701
2702fn generate_crate_index(
2703  crate_name: &str,
2704  root_item: &Item,
2705  modules: &HashMap<String, Vec<(Id, Item)>>,
2706) -> String {
2707  let mut output = String::new();
2708
2709  // Import RustCode component for inline code rendering
2710  output.push_str("import RustCode from '@site/src/components/RustCode';\n");
2711  output.push_str("import Link from '@docusaurus/Link';\n\n");
2712
2713  output.push_str(&format!("# {}\n\n", crate_name));
2714
2715  if let Some(docs) = &root_item.docs {
2716    output.push_str(&format!("{}\n\n", docs));
2717  }
2718
2719  // Module listing with summary
2720  output.push_str("## Modules\n\n");
2721
2722  let mut module_names: Vec<_> = modules.keys().collect();
2723  module_names.sort();
2724
2725  for module_name in module_names {
2726    let items = &modules[module_name];
2727
2728    let display_name = module_name
2729      .strip_prefix(&format!("{}::", crate_name))
2730      .unwrap_or(module_name);
2731
2732    // For Docusaurus structure: submodules get index.md in their directory
2733    let module_file = if display_name.contains("::") {
2734      format!("{}/", display_name.replace("::", "/"))
2735    } else {
2736      format!("{}.md", display_name)
2737    };
2738
2739    // Count item types
2740    let mut counts = HashMap::new();
2741    for (_id, item) in items {
2742      let type_name = match &item.inner {
2743        ItemEnum::Struct(_) => "structs",
2744        ItemEnum::Enum(_) => "enums",
2745        ItemEnum::Function(_) => "functions",
2746        ItemEnum::Trait(_) => "traits",
2747        ItemEnum::Constant { .. } => "constants",
2748        ItemEnum::TypeAlias(_) => "type aliases",
2749        ItemEnum::Module(_) => "modules",
2750        _ => continue,
2751      };
2752      *counts.entry(type_name).or_insert(0) += 1;
2753    }
2754
2755    output.push_str(&format!("### [`{}`]({})\n\n", display_name, module_file));
2756
2757    if !counts.is_empty() {
2758      let mut summary: Vec<String> = counts
2759        .iter()
2760        .map(|(name, count)| format!("{} {}", count, name))
2761        .collect();
2762      summary.sort();
2763      output.push_str(&format!("*{}*\n\n", summary.join(", ")));
2764    }
2765  }
2766
2767  output
2768}
2769
2770fn generate_combined_crate_and_root_content(
2771  crate_name: &str,
2772  root_item: &Item,
2773  _crate_data: &Crate,
2774  _modules: &HashMap<String, Vec<(Id, Item)>>,
2775  root_items: &[(Id, Item)],
2776  module_hierarchy: &HashMap<String, Vec<String>>,
2777  reexported_modules: &HashMap<String, Vec<(String, String)>>,
2778) -> String {
2779  let mut output = String::new();
2780
2781  // Calculate sidebar key for the crate
2782  let base_path = BASE_PATH.with(|bp| bp.borrow().clone());
2783  let base_path_for_sidebar = base_path
2784    .strip_prefix("/docs/")
2785    .or_else(|| base_path.strip_prefix("/docs"))
2786    .or_else(|| base_path.strip_prefix("/"))
2787    .unwrap_or(&base_path);
2788  let sidebar_key = format!("{}/{}", base_path_for_sidebar, crate_name).replace("/", "_");
2789
2790  // Add frontmatter with displayed_sidebar
2791  output.push_str("---\n");
2792  output.push_str(&format!("title: {}\n", crate_name));
2793  output.push_str(&format!("displayed_sidebar: '{}'\n", sidebar_key));
2794  output.push_str("---\n\n");
2795
2796  // Import RustCode component for inline code rendering
2797  output.push_str("import RustCode from '@site/src/components/RustCode';\n");
2798  output.push_str("import Link from '@docusaurus/Link';\n\n");
2799
2800  output.push_str(&format!("# Crate {}\n\n", crate_name));
2801
2802  if let Some(docs) = &root_item.docs {
2803    output.push_str(&format!("{}\n\n", docs));
2804  }
2805
2806  // If we have root-level items, show them first
2807  if !root_items.is_empty() {
2808    // Separate re-exports (Use items) from regular items
2809    let mut re_exports = Vec::new();
2810    let mut regular_items = Vec::new();
2811
2812    for (id, item) in root_items {
2813      if matches!(&item.inner, ItemEnum::Use(_)) {
2814        re_exports.push((id, item));
2815      } else {
2816        regular_items.push((id, item));
2817      }
2818    }
2819
2820    // Show Re-exports section first (if any)
2821    // Only show re-exports where the source module/item is public (rustdoc behavior)
2822    if !re_exports.is_empty() {
2823      let mut public_re_exports = Vec::new();
2824
2825      for (id, item) in &re_exports {
2826        if let ItemEnum::Use(use_item) = &item.inner {
2827          // Check if the source is public
2828          let is_source_public = if let Some(import_id) = &use_item.id {
2829            if let Some(imported_item) = _crate_data.index.get(import_id) {
2830              is_public(imported_item)
2831            } else {
2832              // Not found in crate index - external dependency, assume public
2833              true
2834            }
2835          } else {
2836            // No import ID - assume public
2837            true
2838          };
2839
2840          if is_source_public {
2841            public_re_exports.push((id, item, use_item));
2842          }
2843        }
2844      }
2845
2846      if !public_re_exports.is_empty() {
2847        output.push_str("## Re-exports\n\n");
2848
2849        for (_id, _item, use_item) in &public_re_exports {
2850          // Use the full source path (e.g., "patterns::Builder")
2851          let source_path = &use_item.source;
2852
2853          // Build code string for RustCode component
2854          let code_str = if use_item.is_glob {
2855            format!("pub use {}::*;", source_path)
2856          } else {
2857            format!("pub use {};", source_path)
2858          };
2859
2860          // Extract the final component of the path for linking
2861          // e.g., "generated::MessageRole" -> "MessageRole"
2862          let type_name = source_path.split("::").last().unwrap_or(source_path);
2863
2864          // Try to find link to the re-exported item using absolute links
2865          let links: Vec<(String, String)> = if let Some(import_id) = &use_item.id {
2866            if let Some(link) = generate_type_link(source_path, import_id, _crate_data, None) {
2867              vec![(type_name.to_string(), link)]
2868            } else {
2869              // External dependency - no link
2870              vec![]
2871            }
2872          } else {
2873            vec![]
2874          };
2875
2876          let links_json = format_links_as_json(&links);
2877
2878          // Use RustCode inline component for consistent formatting
2879          output.push_str(&format!(
2880            "<RustCode inline code={{`{}`}} links={{{}}} />\n\n",
2881            code_str, links_json
2882          ));
2883        }
2884      }
2885    }
2886
2887    let mut by_type: HashMap<&str, Vec<&Item>> = HashMap::new();
2888    for (_id, item) in &regular_items {
2889      let type_name = match &item.inner {
2890        ItemEnum::Struct(_) => "Structs",
2891        ItemEnum::Enum(_) => "Enums",
2892        ItemEnum::Function(_) => "Functions",
2893        ItemEnum::Trait(_) => "Traits",
2894        ItemEnum::Constant { .. } => "Constants",
2895        ItemEnum::TypeAlias(_) => "Type Aliases",
2896        ItemEnum::Module(_) => continue, // Skip module items, use hierarchy instead
2897        ItemEnum::Use(_) => continue,    // Use items are handled separately in Re-exports section
2898        _ => continue,
2899      };
2900      by_type.entry(type_name).or_default().push(item);
2901    }
2902
2903    let type_order = [
2904      "Modules",
2905      "Structs",
2906      "Enums",
2907      "Functions",
2908      "Traits",
2909      "Constants",
2910      "Type Aliases",
2911    ];
2912    for type_name in &type_order {
2913      // Special handling for Modules - use hierarchy to show top-level modules
2914      if *type_name == "Modules" {
2915        let mut all_modules: Vec<(String, String)> = Vec::new();
2916
2917        // Add modules from hierarchy (direct submodules)
2918        if let Some(top_level_modules) = module_hierarchy.get(crate_name) {
2919          for module_path in top_level_modules {
2920            let module_name = module_path.split("::").last().unwrap_or(module_path);
2921            all_modules.push((module_name.to_string(), module_path.clone()));
2922          }
2923        }
2924
2925        // Add re-exported modules
2926        if let Some(reexported) = reexported_modules.get(crate_name) {
2927          for (module_name, module_path) in reexported {
2928            // Extract just the module name (last component)
2929            let short_name = module_path.split("::").last().unwrap_or(module_name);
2930            all_modules.push((short_name.to_string(), module_path.clone()));
2931          }
2932        }
2933
2934        // Sort and deduplicate by module_name only (not the full path)
2935        // This prevents showing "app" twice when there's both "app" and "app::app"
2936        all_modules.sort();
2937        let mut seen_names = std::collections::HashSet::new();
2938        all_modules.retain(|(module_name, _)| seen_names.insert(module_name.clone()));
2939
2940        if !all_modules.is_empty() {
2941          output.push_str(&format!("## {}\n\n", type_name));
2942          for (module_name, module_path) in all_modules {
2943            // For re-exported modules, link to their original location
2944            let link_path = module_path
2945              .strip_prefix(&format!("{}::", crate_name))
2946              .unwrap_or(&module_path)
2947              .replace("::", "/");
2948
2949            // Try to get documentation from root_items
2950            let doc_line = root_items
2951              .iter()
2952              .find(|(_, item)| {
2953                if let Some(item_name) = &item.name {
2954                  item_name == &module_name && matches!(&item.inner, ItemEnum::Module(_))
2955                } else {
2956                  false
2957                }
2958              })
2959              .and_then(|(_, item)| item.docs.as_ref())
2960              .and_then(|docs| docs.lines().next())
2961              .filter(|line| !line.is_empty());
2962
2963            // Only add " — " if there's documentation
2964            if let Some(doc_text) = doc_line {
2965              output.push_str(&format!(
2966                "<div><Link to=\"{}/\" className=\"rust-mod\">{}</Link> — {}</div>\n\n",
2967                link_path, module_name, doc_text
2968              ));
2969            } else {
2970              output.push_str(&format!(
2971                "<div><Link to=\"{}/\" className=\"rust-mod\">{}</Link></div>\n\n",
2972                link_path, module_name
2973              ));
2974            }
2975          }
2976        }
2977        continue;
2978      }
2979
2980      if let Some(items_of_type) = by_type.get(type_name) {
2981        output.push_str(&format!("## {}\n\n", type_name));
2982
2983        // Determine CSS class based on type
2984        let css_class = match *type_name {
2985          "Structs" | "Enums" => "rust-struct",
2986          "Traits" => "rust-trait",
2987          "Functions" => "rust-fn",
2988          "Constants" => "rust-constant",
2989          "Type Aliases" => "rust-type",
2990          _ => "rust-item",
2991        };
2992
2993        for item in items_of_type {
2994          if let Some(name) = &item.name {
2995            // Other items link to their individual pages with rustdoc-style prefix
2996            let prefix = get_item_prefix(item);
2997            let link = format!("{}{}", prefix, name);
2998            let visibility_indicator = get_visibility_indicator(item);
2999
3000            output.push_str("<div>");
3001            output.push_str(&format!(
3002              "<Link to=\"{}\" className=\"{}\">{}</Link> {}",
3003              link, css_class, name, visibility_indicator
3004            ));
3005            if let Some(docs) = &item.docs {
3006              let sanitized = sanitize_docs_for_mdx(docs);
3007              if let Some(first_line) = sanitized.lines().next() {
3008                if !first_line.is_empty() {
3009                  output.push_str(&format!(" — {}", first_line));
3010                }
3011              }
3012            }
3013            output.push_str("</div>\n\n");
3014          }
3015        }
3016      }
3017    }
3018  }
3019
3020  output
3021}
3022
3023#[allow(clippy::too_many_arguments)]
3024fn generate_individual_pages(
3025  items: &[(Id, Item)],
3026  path_prefix: &str,
3027  files: &mut HashMap<String, String>,
3028  _crate_data: &Crate,
3029  item_paths: &HashMap<Id, Vec<String>>,
3030  _crate_name: &str,
3031  _module_name: &str,
3032  include_private: bool,
3033) {
3034  for (id, item) in items {
3035    // Skip Use items (re-exports) - they're only shown in the module overview
3036    // The actual items are documented in their original modules
3037    if matches!(&item.inner, ItemEnum::Use(_)) {
3038      continue;
3039    }
3040
3041    if let Some(name) = &item.name {
3042      // Skip module items as they get their own overview pages
3043      if matches!(&item.inner, ItemEnum::Module(_)) {
3044        continue;
3045      }
3046
3047      // Use rustdoc-style prefix for item filename (e.g., "fn.send_message.md")
3048      let item_prefix = get_item_prefix(item);
3049      let file_path = format!("{}{}{}.md", path_prefix, item_prefix, name);
3050
3051      if let Some(mut content) =
3052        format_item_with_path(id, item, _crate_data, item_paths, include_private)
3053      {
3054        // Add frontmatter for Docusaurus navigation with type label and sidebar
3055        let type_label = get_item_type_label(item);
3056        let title = if type_label.is_empty() {
3057          name.to_string()
3058        } else {
3059          format!("{} {}", type_label, name)
3060        };
3061
3062        // Calculate sidebar key from module path (same as module overview)
3063        let base_path = BASE_PATH.with(|bp| bp.borrow().clone());
3064        let base_path_for_sidebar = base_path
3065          .strip_prefix("/docs/")
3066          .or_else(|| base_path.strip_prefix("/docs"))
3067          .or_else(|| base_path.strip_prefix("/"))
3068          .unwrap_or(&base_path);
3069        let sidebar_key = if _module_name == _crate_name {
3070          // For items in the crate root, use "_items" suffix
3071          // to match the sidebar generated for leaf items of the crate
3072          format!("{}/{}_items", base_path_for_sidebar, _crate_name).replace("/", "_")
3073        } else {
3074          let module_path = _module_name.replace("::", "/");
3075          format!("{}/{}", base_path_for_sidebar, module_path).replace("/", "_")
3076        };
3077
3078        let frontmatter = format!(
3079          "---\ntitle: \"{}\"\ndisplayed_sidebar: '{}'\n---\n\nimport RustCode from '@site/src/components/RustCode';\nimport Link from '@docusaurus/Link';\n\n",
3080          title, sidebar_key
3081        );
3082
3083        // Add breadcrumb path (like rustdoc does for all items)
3084        // For re-exported items (duplicates), use the current module path + item name
3085        // For original items, use their full path from item_paths
3086        let breadcrumb = if _module_name == _crate_name {
3087          // Root module - just crate::ItemName
3088          format!("**{}::{}**\n\n", _module_name, name)
3089        } else {
3090          // Check if this is the original location or a re-export
3091          let original_path = item_paths.get(id).map(|p| p.join("::"));
3092          let expected_path = format!("{}::{}", _module_name, name);
3093
3094          // If the original path matches the expected path, it's the original item
3095          // Otherwise, it's a re-exported duplicate - use the current module path
3096          if original_path.as_deref() == Some(expected_path.as_str()) {
3097            format!("**{}**\n\n", expected_path)
3098          } else {
3099            // Re-exported item - use current module path
3100            format!("**{}**\n\n", expected_path)
3101          }
3102        };
3103
3104        content = format!("{}{}{}", frontmatter, breadcrumb, content);
3105        files.insert(file_path, content);
3106      }
3107    }
3108  }
3109}
3110
3111#[allow(clippy::same_item_push)]
3112fn generate_module_overview(
3113  module_name: &str,
3114  items: &[(Id, Item)],
3115  _crate_data: &Crate,
3116  _item_paths: &HashMap<Id, Vec<String>>,
3117  crate_name: &str,
3118  module_hierarchy: &HashMap<String, Vec<String>>,
3119) -> String {
3120  let mut output = String::new();
3121
3122  let display_name = module_name
3123    .strip_prefix(&format!("{}::", crate_name))
3124    .unwrap_or(module_name);
3125
3126  // Get just the last component of the module name (rustdoc style)
3127  let short_name = display_name.split("::").last().unwrap_or(display_name);
3128
3129  // Calculate sidebar key from module path
3130  let base_path = BASE_PATH.with(|bp| bp.borrow().clone());
3131  let base_path_for_sidebar = base_path
3132    .strip_prefix("/docs/")
3133    .or_else(|| base_path.strip_prefix("/docs"))
3134    .or_else(|| base_path.strip_prefix("/"))
3135    .unwrap_or(&base_path);
3136
3137  // For module overview pages, use the PARENT module's sidebar
3138  // This way the module page shows "In <parent>" with siblings
3139  let sidebar_module = if module_name == crate_name {
3140    // Root module uses its own sidebar
3141    crate_name.to_string()
3142  } else if module_name.contains("::") {
3143    // Sub-module uses parent's sidebar
3144    module_name.rsplit_once("::").unwrap().0.to_string()
3145  } else {
3146    // Top-level module (crate_name::module) uses crate's sidebar
3147    crate_name.to_string()
3148  };
3149
3150  let sidebar_key = if sidebar_module == crate_name {
3151    // If this module's parent is the crate root, use the "_modules" variant
3152    // which shows "In <crate>" with crate's modules, not "Crates"
3153    if module_name == crate_name {
3154      // This IS the crate root page itself - use the regular sidebar
3155      format!("{}/{}", base_path_for_sidebar, crate_name).replace("/", "_")
3156    } else {
3157      // This is a child of the crate root - use the "_modules" variant
3158      format!("{}/{}_modules", base_path_for_sidebar, crate_name).replace("/", "_")
3159    }
3160  } else {
3161    // This module's parent is another module (not the crate)
3162    // Use the parent's "_children" sidebar which shows the parent's contents
3163    let module_path = sidebar_module.replace("::", "/");
3164    format!("{}/{}_children", base_path_for_sidebar, module_path).replace("/", "_")
3165  };
3166
3167  // Add FrontMatter for Docusaurus with the module name as title and sidebar
3168  output.push_str("---\n");
3169  output.push_str(&format!("title: {}\n", short_name));
3170  output.push_str(&format!("sidebar_label: {}\n", short_name));
3171  output.push_str(&format!("displayed_sidebar: '{}'\n", sidebar_key));
3172  output.push_str("---\n\n");
3173
3174  // Import RustCode component
3175  output.push_str("import RustCode from '@site/src/components/RustCode';\n");
3176  output.push_str("import Link from '@docusaurus/Link';\n\n");
3177
3178  // Breadcrumb with :: separator (rustdoc style)
3179  let breadcrumb = module_name;
3180  output.push_str(&format!("**{}**\n\n", breadcrumb));
3181
3182  output.push_str(&format!("# Module {}\n\n", short_name));
3183
3184  // Module documentation (if any module item exists)
3185  for (_id, item) in items {
3186    if matches!(&item.inner, ItemEnum::Module(_)) {
3187      if let Some(docs) = &item.docs {
3188        output.push_str(&format!("{}\n\n", sanitize_docs_for_mdx(docs)));
3189      }
3190      break;
3191    }
3192  }
3193
3194  // Separate re-exports (Use items) from regular items
3195  let mut re_exports = Vec::new();
3196  let mut regular_items = Vec::new();
3197
3198  for (id, item) in items {
3199    if matches!(&item.inner, ItemEnum::Use(_)) {
3200      re_exports.push((id, item));
3201    } else {
3202      regular_items.push((id, item));
3203    }
3204  }
3205
3206  // Show Re-exports section first (if any)
3207  // Only show re-exports where the source module/item is public (rustdoc behavior)
3208  if !re_exports.is_empty() {
3209    let mut public_re_exports = Vec::new();
3210
3211    for (id, item) in &re_exports {
3212      if let ItemEnum::Use(use_item) = &item.inner {
3213        // Check if the source is public
3214        let is_source_public = if let Some(import_id) = &use_item.id {
3215          if let Some(imported_item) = _crate_data.index.get(import_id) {
3216            is_public(imported_item)
3217          } else {
3218            // External dependency - always show
3219            true
3220          }
3221        } else {
3222          // No import ID - assume public
3223          true
3224        };
3225
3226        if is_source_public {
3227          public_re_exports.push((id, item, use_item));
3228        }
3229      }
3230    }
3231
3232    if !public_re_exports.is_empty() {
3233      output.push_str("## Re-exports\n\n");
3234
3235      for (_id, _item, use_item) in &public_re_exports {
3236        // Use the full source path for proper linking
3237        let source_path = &use_item.source;
3238
3239        // Build code string for RustCode component
3240        let code_str = if use_item.is_glob {
3241          format!("pub use {}::*;", source_path)
3242        } else {
3243          format!("pub use {};", source_path)
3244        };
3245
3246        // Extract the final component of the path for text matching
3247        // e.g., "patterns::Builder" -> "Builder"
3248        let type_name = source_path.split("::").last().unwrap_or(source_path);
3249
3250        // Try to find link to the re-exported item using absolute links
3251        let links: Vec<(String, String)> = if let Some(import_id) = &use_item.id {
3252          if let Some(link) = generate_type_link(source_path, import_id, _crate_data, None) {
3253            vec![(type_name.to_string(), link)]
3254          } else {
3255            // External dependency - no link
3256            vec![]
3257          }
3258        } else {
3259          vec![]
3260        };
3261
3262        let links_json = format_links_as_json(&links);
3263
3264        // Use RustCode inline component for consistent formatting
3265        output.push_str(&format!(
3266          "<RustCode inline code={{`{}`}} links={{{}}} />\n\n",
3267          code_str, links_json
3268        ));
3269      }
3270    }
3271  }
3272
3273  // Table of contents for this module (rustdoc style overview)
3274  let mut by_type: HashMap<&str, Vec<(&Id, &Item)>> = HashMap::new();
3275  for (id, item) in &regular_items {
3276    let type_name = match &item.inner {
3277      ItemEnum::Struct(_) => "Structs",
3278      ItemEnum::Enum(_) => "Enums",
3279      ItemEnum::Function(_) => "Functions",
3280      ItemEnum::Trait(_) => "Traits",
3281      ItemEnum::Constant { .. } => "Constants",
3282      ItemEnum::TypeAlias(_) => "Type Aliases",
3283      ItemEnum::Module(_) => continue, // Skip modules from items, we'll use hierarchy instead
3284      ItemEnum::Use(_) => continue,    // Use items are handled separately in Re-exports section
3285      _ => continue,
3286    };
3287    by_type.entry(type_name).or_default().push((id, item));
3288  }
3289
3290  let type_order = [
3291    "Modules",
3292    "Structs",
3293    "Enums",
3294    "Functions",
3295    "Traits",
3296    "Constants",
3297    "Type Aliases",
3298  ];
3299  for type_name in &type_order {
3300    // Special handling for Modules - use hierarchy instead of items
3301    if *type_name == "Modules" {
3302      if let Some(submodules) = module_hierarchy.get(module_name) {
3303        if !submodules.is_empty() {
3304          // Collect all submodules
3305          let mut valid_submodules = Vec::new();
3306          for submodule_path in submodules {
3307            let submodule_name = submodule_path.split("::").last().unwrap_or(submodule_path);
3308            valid_submodules.push((submodule_path, submodule_name));
3309          }
3310
3311          // Only show Modules section if there are valid submodules
3312          if !valid_submodules.is_empty() {
3313            output.push_str(&format!("## {}\n\n", type_name));
3314            for (submodule_path, submodule_name) in valid_submodules {
3315              // Try to get the module item from the crate index
3316              let module_item = _crate_data.index.iter().find(|(_, item)| {
3317                if let Some(item_name) = &item.name {
3318                  // Match by path to handle re-exported modules
3319                  item_name == submodule_name
3320                    && matches!(&item.inner, ItemEnum::Module(_))
3321                    && submodule_path.ends_with(&format!("::{}", submodule_name))
3322                } else {
3323                  false
3324                }
3325              });
3326
3327              let visibility_indicator = module_item
3328                .map(|(_, item)| get_visibility_indicator(item))
3329                .unwrap_or("");
3330
3331              let doc_line = module_item
3332                .and_then(|(_, item)| item.docs.as_ref())
3333                .and_then(|docs| docs.lines().next())
3334                .filter(|line| !line.is_empty());
3335
3336              // Only add " — " if there's documentation
3337              if let Some(doc_text) = doc_line {
3338                output.push_str(&format!(
3339                  "<div><Link to=\"{}/\" className=\"rust-mod\">{}</Link> {} — {}</div>\n\n",
3340                  submodule_name, submodule_name, visibility_indicator, doc_text
3341                ));
3342              } else {
3343                output.push_str(&format!(
3344                  "<div><Link to=\"{}/\" className=\"rust-mod\">{}</Link> {}</div>\n\n",
3345                  submodule_name, submodule_name, visibility_indicator
3346                ));
3347              }
3348            }
3349          }
3350        }
3351      }
3352      continue;
3353    }
3354
3355    if let Some(items_of_type) = by_type.get(type_name) {
3356      output.push_str(&format!("## {}\n\n", type_name));
3357
3358      // Determine CSS class based on type
3359      let css_class = match *type_name {
3360        "Modules" => "rust-mod",
3361        "Structs" | "Enums" => "rust-struct",
3362        "Traits" => "rust-trait",
3363        "Functions" => "rust-fn",
3364        "Constants" => "rust-constant",
3365        "Type Aliases" => "rust-type",
3366        _ => "rust-item",
3367      };
3368
3369      for (id, item) in items_of_type {
3370        // For Use items, get the name from the use.name field
3371        let item_name: Option<&String> = if let ItemEnum::Use(use_item) = &item.inner {
3372          Some(&use_item.name)
3373        } else {
3374          item.name.as_ref()
3375        };
3376
3377        if let Some(name) = item_name {
3378          // Special handling for Use items (external re-exports)
3379          if let ItemEnum::Use(_) = &item.inner {
3380            // For external re-exports, just show the name without a link
3381            // (we don't have the full item definition to create a proper page)
3382            // or we could link to the external documentation if available
3383            let prefix = "struct."; // Default to struct prefix
3384            output.push_str(&format!("[{}]({}{})\n", name, prefix, name));
3385            continue;
3386          }
3387
3388          // Determine the correct link path
3389          let link = if let Some(item_path) = _item_paths.get(id) {
3390            // Get the module part of the item path (all except last element)
3391            let item_module_path = if item_path.len() > 1 {
3392              &item_path[..item_path.len() - 1]
3393            } else {
3394              item_path.as_slice()
3395            };
3396            let item_module = item_module_path.join("::");
3397
3398            // Check if this item is defined directly in the current module
3399            if item_module == module_name {
3400              // Item is defined directly in this module - use simple link with prefix
3401              let prefix = get_item_prefix(item);
3402              format!("{}{}", prefix, name)
3403            } else {
3404              // Item is in a submodule or re-exported from elsewhere - calculate relative path
3405              let current_module_parts: Vec<&str> = module_name.split("::").collect();
3406              let item_module_parts = item_module_path;
3407
3408              // Calculate relative path
3409              let mut relative_parts = Vec::new();
3410
3411              // Go up to common ancestor
3412              let common_prefix_len = current_module_parts
3413                .iter()
3414                .zip(item_module_parts.iter())
3415                .take_while(|(a, b)| a == b)
3416                .count();
3417
3418              // Add ".." for each level up
3419              for _ in 0..(current_module_parts.len() - common_prefix_len) {
3420                relative_parts.push("..");
3421              }
3422
3423              // Add path down to item
3424              for part in &item_module_parts[common_prefix_len..] {
3425                relative_parts.push(part);
3426              }
3427
3428              let prefix = get_item_prefix(item);
3429              let mut path = relative_parts.join("/");
3430              if !path.is_empty() {
3431                path.push('/');
3432              }
3433              path.push_str(&format!("{}{}", prefix, name));
3434              path
3435            }
3436          } else {
3437            // Fallback if no path info
3438            let prefix = get_item_prefix(item);
3439            format!("{}{}", prefix, name)
3440          };
3441
3442          let visibility_indicator = get_visibility_indicator(item);
3443
3444          output.push_str("<div>");
3445          output.push_str(&format!(
3446            "<Link to=\"{}\" className=\"{}\">{}</Link> {}",
3447            link, css_class, name, visibility_indicator
3448          ));
3449          if let Some(docs) = &item.docs {
3450            let sanitized = sanitize_docs_for_mdx(docs);
3451            if let Some(first_line) = sanitized.lines().next() {
3452              if !first_line.is_empty() {
3453                output.push_str(&format!(" — {}", first_line));
3454              }
3455            }
3456          }
3457          output.push_str("</div>\n\n");
3458        }
3459      }
3460    }
3461  }
3462
3463  output
3464}
3465
3466/// Generate sidebar structure for Docusaurus
3467/// This generates multiple sidebars - one for each module that has content
3468fn generate_all_sidebars(
3469  crate_name: &str,
3470  modules: &HashMap<String, Vec<(Id, Item)>>,
3471  _item_paths: &HashMap<Id, Vec<String>>,
3472  crate_data: &Crate,
3473  sidebarconfig_collapsed: bool,
3474) -> String {
3475  let mut all_sidebars = HashMap::new();
3476
3477  // Get the base_path from thread-local storage
3478  let base_path = BASE_PATH.with(|bp| bp.borrow().clone());
3479
3480  // For Docusaurus sidebar, paths must be relative to the docs/ folder
3481  let sidebar_prefix = if base_path == "/docs" || base_path == "docs" {
3482    ""
3483  } else if base_path.starts_with("/docs/") {
3484    base_path.strip_prefix("/docs/").unwrap()
3485  } else if base_path.starts_with("docs/") {
3486    base_path.strip_prefix("docs/").unwrap()
3487  } else {
3488    &base_path
3489  };
3490
3491  // Generate TWO sidebars for the root crate:
3492  // 1. With is_root=true (shows "Crates" section) - used by the crate's own page
3493  let root_sidebar_for_crate = generate_sidebar_for_module(
3494    crate_name,
3495    crate_name,
3496    modules,
3497    crate_data,
3498    sidebar_prefix,
3499    sidebarconfig_collapsed,
3500    true, // is_root - shows "Crates" section
3501    &crate_data.crate_version,
3502    false, // show_all_parent_items - false for modules
3503  );
3504
3505  let root_path = if sidebar_prefix.is_empty() {
3506    crate_name.to_string()
3507  } else {
3508    format!("{}/{}", sidebar_prefix, crate_name)
3509  };
3510  all_sidebars.insert(root_path.clone(), root_sidebar_for_crate);
3511
3512  // 2. With is_root=false (shows crate's modules) - used by the crate's child modules
3513  let root_sidebar_for_modules = generate_sidebar_for_module(
3514    crate_name,
3515    crate_name,
3516    modules,
3517    crate_data,
3518    sidebar_prefix,
3519    sidebarconfig_collapsed,
3520    false, // is_root=false - shows "In <parent>" with crate's modules
3521    &crate_data.crate_version,
3522    false, // show_all_parent_items - false for modules
3523  );
3524
3525  // Use a different key for this sidebar (add "_modules" suffix)
3526  let root_path_for_modules = format!("{}_modules", root_path);
3527  all_sidebars.insert(root_path_for_modules, root_sidebar_for_modules);
3528
3529  // Generate sidebar for each submodule (for dynamic sidebar when entering modules)
3530  eprintln!("[DEBUG] Total modules to process: {}", modules.keys().len());
3531  for module_key in modules.keys() {
3532    eprintln!("[DEBUG] Processing module: {}", module_key);
3533    if module_key == crate_name {
3534      eprintln!("[DEBUG] Skipping root crate: {}", crate_name);
3535      continue; // Skip root, already handled
3536    }
3537
3538    let sidebar = generate_sidebar_for_module(
3539      crate_name,
3540      module_key,
3541      modules,
3542      crate_data,
3543      sidebar_prefix,
3544      sidebarconfig_collapsed,
3545      false, // not root
3546      &crate_data.crate_version,
3547      false, // show_all_parent_items - false for modules
3548    );
3549
3550    // Convert module_key from Rust path (::) to file path (/)
3551    let module_path_normalized = module_key.replace("::", "/");
3552    let module_path = if sidebar_prefix.is_empty() {
3553      module_path_normalized.clone()
3554    } else {
3555      format!("{}/{}", sidebar_prefix, module_path_normalized)
3556    };
3557    all_sidebars.insert(module_path.clone(), sidebar);
3558
3559    // Check if this module has sub-modules (direct children) or items
3560    // Generate a _children sidebar if the module has submodules or items (structs, enums, etc.)
3561    let has_submodules_or_items = modules.keys().any(|key| {
3562      if let Some(stripped) = key.strip_prefix(&format!("{}::", module_key)) {
3563        !stripped.contains("::")
3564      } else {
3565        false
3566      }
3567    }) || modules
3568      .get(module_key)
3569      .map(|items| {
3570        items
3571          .iter()
3572          .any(|(_, item)| !matches!(&item.inner, ItemEnum::Module(_) | ItemEnum::Use(_)))
3573      })
3574      .unwrap_or(false);
3575
3576    eprintln!(
3577      "[DEBUG] Module '{}' has_submodules_or_items: {}",
3578      module_key, has_submodules_or_items
3579    );
3580
3581    // If this module has sub-modules or items, generate an additional sidebar for them
3582    if has_submodules_or_items {
3583      let submodule_sidebar = generate_sidebar_for_module(
3584        crate_name,
3585        module_key, // Use this module as the "parent"
3586        modules,
3587        crate_data,
3588        sidebar_prefix,
3589        sidebarconfig_collapsed,
3590        false,
3591        &crate_data.crate_version,
3592        true, // show_all_parent_items = true to show THIS module's contents
3593      );
3594
3595      // Use "_children" suffix to distinguish from the module's own sidebar
3596      let submodule_sidebar_key = format!("{}_children", module_path.replace("/", "_"));
3597      all_sidebars.insert(submodule_sidebar_key, submodule_sidebar);
3598    }
3599  }
3600
3601  // Generate sidebar for each leaf item (struct, enum, trait, fn, etc.)
3602  // Each item gets its own sidebar showing "In <parent_module>" with all parent items
3603  // BUT: all items in the same module share the same sidebar!
3604  // So we generate one sidebar per module (not per item) and use the module path as key
3605  let mut processed_modules = std::collections::HashSet::new();
3606
3607  for (module_key, items) in modules {
3608    // Skip if we already processed this module
3609    if processed_modules.contains(module_key) {
3610      continue;
3611    }
3612
3613    // Check if this module has any non-module items
3614    let has_leaf_items = items
3615      .iter()
3616      .any(|(_, item)| !matches!(&item.inner, ItemEnum::Module(_) | ItemEnum::Use(_)));
3617
3618    if !has_leaf_items {
3619      continue; // No leaf items, skip
3620    }
3621
3622    processed_modules.insert(module_key.clone());
3623
3624    // Generate sidebar for this module (to be used by all leaf items in it)
3625    let parent_module = module_key;
3626
3627    eprintln!(
3628      "[DEBUG] Generating leaf items sidebar for module_key: {}",
3629      module_key
3630    );
3631
3632    let item_sidebar = generate_sidebar_for_module(
3633      crate_name,
3634      parent_module,
3635      modules,
3636      crate_data,
3637      sidebar_prefix,
3638      sidebarconfig_collapsed,
3639      false, // is_root = false - leaf items always show "In <module>", never "Crates"
3640      &crate_data.crate_version,
3641      true, // show_all_parent_items - true for leaf items (struct, enum, etc.)
3642    );
3643
3644    // The sidebar key is the module path (not the item path!)
3645    // This matches what's written in the frontmatter of item files
3646    let parent_module_path = parent_module.replace("::", "/");
3647
3648    let sidebar_key = if sidebar_prefix.is_empty() {
3649      parent_module_path.clone()
3650    } else {
3651      format!("{}/{}", sidebar_prefix, parent_module_path)
3652    };
3653
3654    // If this is for leaf items of the crate root, add "_items" suffix
3655    // to avoid collision with the crate's own sidebar (which shows "Crates")
3656    let sidebar_key = if parent_module == crate_name {
3657      format!("{}_items", sidebar_key.replace("/", "_"))
3658    } else {
3659      sidebar_key
3660    };
3661
3662    all_sidebars.insert(sidebar_key, item_sidebar);
3663  }
3664
3665  // Convert to TypeScript with multiple sidebars
3666  sidebars_to_js(&all_sidebars, sidebarconfig_collapsed)
3667}
3668
3669/// Generate sidebar for a specific module
3670#[allow(clippy::too_many_arguments)]
3671fn generate_sidebar_for_module(
3672  _crate_name: &str, // Prefixed with _ to avoid unused warning
3673  module_key: &str,
3674  modules: &HashMap<String, Vec<(Id, Item)>>,
3675  _crate_data: &Crate, // Prefixed with _ to avoid unused warning
3676  sidebar_prefix: &str,
3677  _sidebarconfig_collapsed: bool, // Prefixed with _ to avoid unused warning
3678  is_root: bool,
3679  crate_version: &Option<String>,
3680  show_all_parent_items: bool, // New parameter: if true, show all items in parent module (for leaf items)
3681) -> Vec<SidebarItem> {
3682  let module_items = modules.get(module_key).cloned().unwrap_or_default();
3683
3684  // Convert module_key from :: to / for doc IDs
3685  let _module_path = module_key.replace("::", "/"); // Prefixed with _ to avoid unused warning
3686
3687  let mut sidebar_items = Vec::new();
3688
3689  // Add "Go back" link and crate title for root crates, or just crate title for modules
3690  if is_root {
3691    // For root crate: use the configured sidebar_root_link if available
3692    let sidebar_root_link = SIDEBAR_ROOT_LINK.with(|srl| srl.borrow().clone());
3693
3694    if let Some(link) = sidebar_root_link {
3695      sidebar_items.push(SidebarItem::Link {
3696        href: link,
3697        label: "← Go back".to_string(),
3698        custom_props: Some("rust-sidebar-back-link".to_string()),
3699      });
3700    }
3701
3702    // Add crate title with version for root crates
3703    // The title itself is clickable and links to the crate index
3704    let crate_root_path = if sidebar_prefix.is_empty() {
3705      format!("{}/index", _crate_name)
3706    } else {
3707      format!("{}/{}/index", sidebar_prefix, _crate_name)
3708    };
3709
3710    // Use customProps to pass crate name and version to a custom sidebar component
3711    sidebar_items.push(SidebarItem::Doc {
3712      id: crate_root_path,
3713      label: Some(_crate_name.to_string()), // Fallback label
3714      custom_props: Some(format!(
3715        "{{ rustCrateTitle: true, crateName: '{}', version: '{}' }}",
3716        _crate_name,
3717        crate_version.as_deref().unwrap_or("")
3718      )),
3719    });
3720
3721    // For root crate, the title is already clickable, so we don't add a separate Overview
3722  } else {
3723    // For submodules: show crate name with version (rustdoc style)
3724    // This links to the crate root
3725    let crate_root_path = if sidebar_prefix.is_empty() {
3726      format!("{}/index", _crate_name)
3727    } else {
3728      format!("{}/{}/index", sidebar_prefix, _crate_name)
3729    };
3730
3731    // Use customProps to pass crate name and version to a custom sidebar component
3732    sidebar_items.push(SidebarItem::Doc {
3733      id: crate_root_path,
3734      label: Some(_crate_name.to_string()), // Fallback label
3735      custom_props: Some(format!(
3736        "{{ rustCrateTitle: true, crateName: '{}', version: '{}' }}",
3737        _crate_name,
3738        crate_version.as_deref().unwrap_or("")
3739      )),
3740    });
3741
3742    // Module title commented out - the overview is already on the right side
3743    // We don't need a separate "Overview" link in the sidebar
3744    /*
3745    // Add Overview link to the submodule's index
3746    // Use customProps to render it as a module title (similar to crate title but without version)
3747    let module_index_path = if sidebar_prefix.is_empty() {
3748        format!("{}/index", module_path)
3749    } else {
3750        format!("{}/{}/index", sidebar_prefix, module_path)
3751    };
3752
3753    let module_display_name = module_key.split("::").last().unwrap_or(module_key);
3754
3755    sidebar_items.push(SidebarItem::Doc {
3756        id: module_index_path,
3757        label: Some(module_display_name.to_string()),
3758        custom_props: Some(format!(
3759            "{{ rustModuleTitle: true, moduleName: '{}' }}",
3760            module_display_name
3761        )),
3762    });
3763    */
3764  }
3765
3766  // Categorize items by type
3767  let mut by_type: HashMap<&str, Vec<&Item>> = HashMap::new();
3768
3769  for (_, item) in &module_items {
3770    if matches!(&item.inner, ItemEnum::Use(_)) {
3771      continue;
3772    }
3773
3774    let type_name = match &item.inner {
3775      ItemEnum::Module(_) => "Modules",
3776      ItemEnum::Struct(_) | ItemEnum::StructField(_) => "Structs",
3777      ItemEnum::Enum(_) | ItemEnum::Variant(_) => "Enums",
3778      ItemEnum::Function(_) => "Functions",
3779      ItemEnum::Trait(_) => "Traits",
3780      ItemEnum::Constant { .. } => "Constants",
3781      ItemEnum::TypeAlias(_) => "Type Aliases",
3782      ItemEnum::Macro(_) => "Macros",
3783      ItemEnum::ProcMacro(_) => "Proc Macros",
3784      ItemEnum::Static { .. } => "Statics",
3785      _ => continue,
3786    };
3787
3788    by_type.entry(type_name).or_default().push(item);
3789  }
3790
3791  // Add "In <parent>" section for ALL modules and crates (rustdoc style)
3792  // - For crate root (is_root = true): show workspace sibling crates
3793  // - For modules: show "In <parent>" with parent's content
3794  // - For leaf items: show "In <module>" with module's content
3795
3796  // Determine which module's items to show based on show_all_parent_items and is_root:
3797  let (parent_module, siblings_label) = if show_all_parent_items {
3798    // For leaf items: show all items from the current module (not parent)
3799    eprintln!("[DEBUG] Leaf item sidebar for module_key: {}", module_key);
3800    (Some(module_key), format!("In {}", module_key))
3801  } else if is_root {
3802    // For root crate with is_root=true: show ONLY workspace crates, not the crate's modules
3803    // The workspace crates section is added separately below
3804    eprintln!(
3805      "[DEBUG] Root crate sidebar (is_root=true) for module_key: {}",
3806      module_key
3807    );
3808    (None, String::new()) // Don't collect any modules, only show "Crates" section
3809  } else if module_key == _crate_name {
3810    // For root crate with is_root=false: show crate's own modules
3811    // This is used by the crate's child modules to navigate
3812    eprintln!(
3813      "[DEBUG] Root crate sidebar (is_root=false) for module_key: {}",
3814      module_key
3815    );
3816    (Some(module_key), format!("In {}", _crate_name))
3817  } else if module_key.contains("::") {
3818    // For modules: has parent module - show siblings
3819    let parent = module_key.rsplit_once("::").unwrap().0;
3820    eprintln!(
3821      "[DEBUG] Module sidebar for module_key: {}, parent: {}",
3822      module_key, parent
3823    );
3824    (Some(parent), format!("In {}", parent))
3825  } else {
3826    // For top-level modules: show siblings in crate
3827    eprintln!(
3828      "[DEBUG] Top-level module sidebar for module_key: {}",
3829      module_key
3830    );
3831    (None, format!("In crate {}", _crate_name))
3832  };
3833
3834  // Rustdoc-style: Group parent items by type (Modules, Structs, Enums, etc.)
3835  // Use the same type_order as before
3836  let type_order = vec![
3837    "Modules",
3838    "Macros",
3839    "Structs",
3840    "Enums",
3841    "Traits",
3842    "Functions",
3843    "Type Aliases",
3844    "Constants",
3845    "Statics",
3846    "Primitives",
3847  ];
3848
3849  // Group items by type using HashMap
3850  use std::collections::HashMap;
3851  let mut items_by_type: HashMap<&str, Vec<SidebarItem>> = HashMap::new();
3852
3853  // For both modules and leaf items, we need to add child modules
3854  // - For modules: children of the parent module (siblings of current module)
3855  // - For leaf items: children of the current module (submodules)
3856  let child_modules: Vec<&String> = modules
3857    .keys()
3858    .filter(|key| {
3859      if let Some(target_module) = parent_module {
3860        // Check if this is a direct child of target_module
3861        // A direct child has the form: target_module::child_name (one more :: than target)
3862        let target_prefix = format!("{}::", target_module);
3863        if key.starts_with(&target_prefix) {
3864          // Count :: in both strings to ensure it's a direct child, not a grandchild
3865          let target_colons = target_module.matches("::").count();
3866          let key_colons = key.matches("::").count();
3867          key_colons == target_colons + 1
3868        } else {
3869          false
3870        }
3871      } else {
3872        // Top-level modules of the crate (children of crate root)
3873        !key.contains("::") && *key != _crate_name
3874      }
3875    })
3876    .collect();
3877
3878  for child_key in child_modules {
3879    let child_name = child_key.split("::").last().unwrap_or(child_key);
3880    let child_path = child_key.replace("::", "/");
3881    let child_doc_id = if sidebar_prefix.is_empty() {
3882      format!("{}/index", child_path)
3883    } else {
3884      format!("{}/{}/index", sidebar_prefix, child_path)
3885    };
3886
3887    let label = child_name.to_string();
3888
3889    items_by_type
3890      .entry("Modules")
3891      .or_default()
3892      .push(SidebarItem::Doc {
3893        id: child_doc_id,
3894        label: Some(label),
3895        custom_props: Some("rust-mod".to_string()),
3896      });
3897  }
3898
3899  // Add all other items (structs, enums, functions, etc.) from parent_module
3900  let parent_items_source = if let Some(parent_key) = parent_module {
3901    modules.get(parent_key)
3902  } else {
3903    modules.get(_crate_name)
3904  };
3905
3906  if let Some(parent_module_items) = parent_items_source {
3907    for (_item_id, item) in parent_module_items {
3908      if let Some(item_name) = &item.name {
3909        // Skip modules (already added above)
3910        if matches!(&item.inner, ItemEnum::Module(_)) {
3911          continue;
3912        }
3913
3914        let prefix = get_item_prefix(item);
3915        let parent_path = if let Some(pk) = parent_module {
3916          pk.replace("::", "/")
3917        } else {
3918          _crate_name.to_string()
3919        };
3920
3921        let item_doc_id = if sidebar_prefix.is_empty() {
3922          format!("{}/{}{}", parent_path, prefix, item_name)
3923        } else {
3924          format!("{}/{}/{}{}", sidebar_prefix, parent_path, prefix, item_name)
3925        };
3926
3927        // Determine CSS class and type category based on item type
3928        let (class_name, type_category) = if prefix.starts_with("struct.") {
3929          ("rust-struct", "Structs")
3930        } else if prefix.starts_with("enum.") {
3931          ("rust-struct", "Enums")
3932        } else if prefix.starts_with("trait.") {
3933          ("rust-trait", "Traits")
3934        } else if prefix.starts_with("fn.") {
3935          ("rust-fn", "Functions")
3936        } else if prefix.starts_with("constant.") {
3937          ("rust-constant", "Constants")
3938        } else if prefix.starts_with("type.") {
3939          ("rust-type", "Type Aliases")
3940        } else if prefix.starts_with("macro.") {
3941          ("rust-macro", "Macros")
3942        } else if prefix.starts_with("static.") {
3943          ("rust-static", "Statics")
3944        } else {
3945          ("rust-item", "Primitives")
3946        };
3947
3948        items_by_type
3949          .entry(type_category)
3950          .or_default()
3951          .push(SidebarItem::Doc {
3952            id: item_doc_id,
3953            label: Some(item_name.clone()),
3954            custom_props: Some(class_name.to_string()),
3955          });
3956      }
3957    }
3958  } // Close if let Some(parent_module_items)
3959
3960  // Create categories for each type that has items
3961  let mut parent_section_items = Vec::new();
3962  for type_name in type_order {
3963    if let Some(items) = items_by_type.get(type_name) {
3964      if !items.is_empty() {
3965        parent_section_items.push(SidebarItem::Category {
3966          label: type_name.to_string(),
3967          items: items.clone(),
3968          collapsed: false, // Will be rendered as collapsible: false
3969          link: None,
3970        });
3971      }
3972    }
3973  }
3974
3975  // Generate link to parent module
3976  let parent_link = if let Some(parent_key) = parent_module {
3977    let parent_path = parent_key.replace("::", "/");
3978    if sidebar_prefix.is_empty() {
3979      Some(format!("{}/index", parent_path))
3980    } else {
3981      Some(format!("{}/{}/index", sidebar_prefix, parent_path))
3982    }
3983  } else {
3984    // Parent is crate root
3985    if sidebar_prefix.is_empty() {
3986      Some(format!("{}/index", _crate_name))
3987    } else {
3988      Some(format!("{}/{}/index", sidebar_prefix, _crate_name))
3989    }
3990  };
3991
3992  // Add "In <parent>" section in these cases:
3993  // - For leaf items (show_all_parent_items=true): always wrap in "In <module>"
3994  // - For sub-modules where parent is NOT the crate: wrap in "In <parent>"
3995  // - For modules where parent IS the crate: DON'T wrap (rustdoc behavior without TOC)
3996  // Root crates (is_root=true) will show "Crates" section instead (added below)
3997  let should_wrap_in_category = !is_root
3998    && !parent_section_items.is_empty()
3999    && (show_all_parent_items || parent_module != Some(_crate_name));
4000
4001  if should_wrap_in_category {
4002    sidebar_items.push(SidebarItem::Category {
4003      label: siblings_label,
4004      items: parent_section_items,
4005      collapsed: false, // Keep open like rustdoc
4006      link: parent_link,
4007    });
4008  } else if !is_root && !parent_section_items.is_empty() {
4009    // For modules whose parent is the crate: add categories directly without wrapper
4010    sidebar_items.extend(parent_section_items);
4011  }
4012
4013  // For root crates: add "Crates" section with workspace sibling crates
4014  if is_root {
4015    let workspace_crates = WORKSPACE_CRATES.with(|wc| wc.borrow().clone());
4016
4017    if workspace_crates.len() > 1 {
4018      let mut crate_items = Vec::new();
4019
4020      for crate_name in &workspace_crates {
4021        // Normalize crate name: replace hyphens with underscores for file paths
4022        let normalized_crate_name = crate_name.replace("-", "_");
4023
4024        let crate_doc_id = if sidebar_prefix.is_empty() {
4025          format!("{}/index", normalized_crate_name)
4026        } else {
4027          format!("{}/{}/index", sidebar_prefix, normalized_crate_name)
4028        };
4029
4030        let label = crate_name.to_string();
4031
4032        crate_items.push(SidebarItem::Doc {
4033          id: crate_doc_id,
4034          label: Some(label),
4035          custom_props: Some("rust-mod".to_string()),
4036        });
4037      }
4038
4039      // Sort crate items by label (alphabetically)
4040      crate_items.sort_by(|a, b| {
4041        let label_a = match a {
4042          SidebarItem::Doc { label, .. } => label.as_deref().unwrap_or(""),
4043          SidebarItem::Link { label, .. } => label.as_str(),
4044          SidebarItem::Category { label, .. } => label.as_str(),
4045        };
4046        let label_b = match b {
4047          SidebarItem::Doc { label, .. } => label.as_deref().unwrap_or(""),
4048          SidebarItem::Link { label, .. } => label.as_str(),
4049          SidebarItem::Category { label, .. } => label.as_str(),
4050        };
4051        label_a.cmp(label_b)
4052      });
4053
4054      sidebar_items.push(SidebarItem::Category {
4055        label: "Crates".to_string(),
4056        items: crate_items,
4057        collapsed: false,
4058        link: None,
4059      });
4060    }
4061  }
4062
4063  sidebar_items
4064}
4065
4066/// Convert multiple sidebars to TypeScript code
4067fn sidebars_to_js(all_sidebars: &HashMap<String, Vec<SidebarItem>>, _collapsed: bool) -> String {
4068  let mut output = String::new();
4069
4070  output.push_str("// This file is auto-generated by cargo-doc-md\n");
4071  output.push_str("// Do not edit manually - this file will be regenerated\n\n");
4072  output.push_str("import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';\n\n");
4073  output.push_str("// Rust API documentation sidebars\n");
4074  output.push_str("// Each module has its own sidebar for better navigation\n");
4075  output.push_str("// Import this in your docusaurus.config.ts:\n");
4076  output.push_str("// import { rustSidebars } from './sidebars-rust';\n");
4077  output.push_str("//\n");
4078  output.push_str("// Then configure in docs plugin:\n");
4079  output.push_str("// docs: {\n");
4080  output.push_str("//   sidebarPath: './sidebars.ts',\n");
4081  output
4082    .push_str("//   async sidebarItemsGenerator({ defaultSidebarItemsGenerator, ...args }) {\n");
4083  output.push_str("//     const items = await defaultSidebarItemsGenerator(args);\n");
4084  output.push_str("//     const docPath = args.item.id;\n");
4085  output.push_str("//     // Use module-specific sidebar if available\n");
4086  output.push_str("//     for (const [path, sidebar] of Object.entries(rustSidebars)) {\n");
4087  output.push_str("//       if (docPath.startsWith(path + '/')) {\n");
4088  output.push_str("//         return sidebar;\n");
4089  output.push_str("//       }\n");
4090  output.push_str("//     }\n");
4091  output.push_str("//     return items;\n");
4092  output.push_str("//   },\n");
4093  output.push_str("// }\n\n");
4094
4095  output.push_str("export const rustSidebars: Record<string, any[]> = {\n");
4096
4097  // Sort by path for consistent output
4098  let mut sorted_paths: Vec<_> = all_sidebars.keys().cloned().collect();
4099  sorted_paths.sort();
4100
4101  let first_path = sorted_paths.first().cloned();
4102
4103  for path in &sorted_paths {
4104    let items = &all_sidebars[path];
4105    // Convert path with slashes and dots to valid sidebar key (replace / and . with _)
4106    let sidebar_key = path.replace("/", "_").replace(".", "_");
4107    output.push_str(&format!("  '{}': [\n", sidebar_key));
4108    for item in items {
4109      output.push_str(&format_sidebar_item(item, 2));
4110    }
4111    output.push_str("  ],\n");
4112  }
4113
4114  output.push_str("};\n\n");
4115
4116  // NOTE: rootRustSidebar is generated during merge in writer.rs
4117  // to include all crates from the workspace
4118
4119  // Also export the main sidebar for backward compatibility
4120  if let Some(first_path) = first_path {
4121    let first_sidebar_key = first_path.replace("/", "_").replace(".", "_");
4122    output.push_str("// Main API documentation sidebar (for backward compatibility)\n");
4123    output.push_str("export const rustApiDocumentation = rustSidebars['");
4124    output.push_str(&first_sidebar_key);
4125    output.push_str("'];\n\n");
4126    output.push_str("// Or use as a single category:\n");
4127    output.push_str("export const rustApiCategory = {\n");
4128    output.push_str("  type: 'category' as const,\n");
4129    output.push_str("  label: 'API Documentation',\n");
4130    output.push_str("  collapsed: false,\n");
4131    output.push_str("  items: rustApiDocumentation,\n");
4132    output.push_str("};\n");
4133  }
4134
4135  output
4136}
4137
4138/// Format a single sidebar item with proper indentation
4139fn format_sidebar_item(item: &SidebarItem, indent: usize) -> String {
4140  let indent_str = "  ".repeat(indent);
4141
4142  match item {
4143    SidebarItem::Doc {
4144      id,
4145      label,
4146      custom_props,
4147    } => {
4148      // Remove .md extension if present and convert to doc ID
4149      let doc_id = id.trim_end_matches(".md").replace(".md", "");
4150
4151      // If we have a label or customProps, create an object with type, id, label, and optional className/customProps
4152      if label.is_some() || custom_props.is_some() {
4153        let mut output = format!("{}{{ type: 'doc', id: '{}'", indent_str, doc_id);
4154
4155        if let Some(label_text) = label {
4156          output.push_str(&format!(", label: '{}'", label_text));
4157        }
4158
4159        // Determine if custom_props is className or customProps based on format
4160        if let Some(props) = custom_props {
4161          if props.starts_with('{') {
4162            // It's customProps JSON object
4163            output.push_str(&format!(", customProps: {}", props));
4164          } else {
4165            // It's a className string
4166            output.push_str(&format!(", className: '{}'", props));
4167          }
4168        }
4169
4170        output.push_str(" },\n");
4171        output
4172      } else {
4173        // Just a string reference (Docusaurus will infer the label)
4174        format!("{}'{doc_id}',\n", indent_str)
4175      }
4176    }
4177    SidebarItem::Link {
4178      href,
4179      label,
4180      custom_props,
4181    } => {
4182      // Generate a link item with href
4183      let mut output = format!(
4184        "{}{{ type: 'link', href: '{}', label: '{}'",
4185        indent_str, href, label
4186      );
4187      if let Some(props) = custom_props {
4188        if props.starts_with('{') {
4189          output.push_str(&format!(", customProps: {}", props));
4190        } else {
4191          output.push_str(&format!(", className: '{}'", props));
4192        }
4193      }
4194      output.push_str(" },\n");
4195      output
4196    }
4197    SidebarItem::Category {
4198      label,
4199      items,
4200      collapsed,
4201      link,
4202    } => {
4203      let mut output = String::new();
4204      output.push_str(&format!("{}{{\n", indent_str));
4205      output.push_str(&format!("{}  type: 'category',\n", indent_str));
4206      output.push_str(&format!("{}  label: '{}',\n", indent_str, label));
4207
4208      // Add link if present (makes the category clickable)
4209      if let Some(link_path) = link {
4210        let doc_id = link_path.trim_end_matches(".md").replace(".md", "");
4211        output.push_str(&format!("{}  link: {{\n", indent_str));
4212        output.push_str(&format!("{}    type: 'doc',\n", indent_str));
4213        output.push_str(&format!("{}    id: '{}',\n", indent_str, doc_id));
4214        output.push_str(&format!("{}  }},\n", indent_str));
4215      }
4216
4217      // Nested categories (indent > 0) are not collapsible (rustdoc style)
4218      // Top-level categories use the collapsed parameter
4219      if indent > 0 {
4220        output.push_str(&format!("{}  collapsible: false,\n", indent_str));
4221      } else {
4222        output.push_str(&format!("{}  collapsed: {},\n", indent_str, collapsed));
4223      }
4224
4225      output.push_str(&format!("{}  items: [\n", indent_str));
4226
4227      for sub_item in items {
4228        output.push_str(&format_sidebar_item(sub_item, indent + 2));
4229      }
4230
4231      output.push_str(&format!("{}  ],\n", indent_str));
4232      output.push_str(&format!("{}}},\n", indent_str));
4233      output
4234    }
4235  }
4236}
4237
4238#[cfg(test)]
4239mod tests {
4240  use super::*;
4241
4242  #[test]
4243  fn test_sanitize_docs_for_mdx_inline_html() {
4244    // Test case: HTML tag inline with text (the problematic case)
4245    let input = "Identifies the sender of the message.\n<details><summary>JSON schema</summary>\n\n```json\n{\n  \"type\": \"string\"\n}\n```\n\n</details>";
4246    let result = sanitize_docs_for_mdx(input);
4247
4248    // Should have a blank line before <details>
4249    assert!(
4250      result.contains("message.\n\n<details>"),
4251      "Expected blank line before <details>, got:\n{}",
4252      result
4253    );
4254  }
4255
4256  #[test]
4257  fn test_sanitize_docs_for_mdx_already_separated() {
4258    // Test case: HTML already properly separated
4259    let input = "Some text.\n\n<details><summary>Info</summary>\nContent\n</details>\n\nMore text.";
4260    let result = sanitize_docs_for_mdx(input);
4261
4262    // Should preserve the existing separation
4263    assert!(
4264      result.contains("text.\n\n<details>"),
4265      "Should preserve existing blank lines"
4266    );
4267  }
4268
4269  #[test]
4270  fn test_sanitize_docs_for_mdx_no_html() {
4271    // Test case: No HTML tags
4272    let input = "Just some regular markdown text.\nWith multiple lines.";
4273    let result = sanitize_docs_for_mdx(input);
4274
4275    // Should return unchanged
4276    assert_eq!(result, input, "Plain text should be unchanged");
4277  }
4278
4279  #[test]
4280  fn test_sanitize_docs_for_mdx_inline_html_tags() {
4281    // Test case: Inline HTML like <code> should not be affected
4282    let input = "Use the `<code>` tag for inline code.";
4283    let result = sanitize_docs_for_mdx(input);
4284
4285    // Should return unchanged (code is not a block-level tag)
4286    assert_eq!(result, input, "Inline HTML should be unchanged");
4287  }
4288}