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