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