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