1use anyhow::Result;
4use rustdoc_types::{Crate, Item, ItemEnum, Visibility, Id};
5use std::collections::HashMap;
6
7pub struct MarkdownOutput {
9 pub crate_name: String,
11 pub files: HashMap<String, String>,
13}
14
15pub fn convert_to_markdown_multifile(crate_data: &Crate, include_private: bool) -> Result<MarkdownOutput> {
17 let root_item = crate_data.index.get(&crate_data.root)
18 .ok_or_else(|| anyhow::anyhow!("Root item not found in index"))?;
19
20 let crate_name = root_item.name.as_deref().unwrap_or("unknown");
21
22 let item_paths = build_path_map(crate_data);
24
25 let modules = group_by_module(crate_data, &item_paths, include_private);
27
28 let mut files = HashMap::new();
29
30 let index_content = generate_crate_index(crate_name, root_item, &modules);
32 files.insert("index.md".to_string(), index_content);
33
34 for (module_name, items) in &modules {
36 let module_filename = module_name
37 .strip_prefix(&format!("{}::", crate_name))
38 .unwrap_or(module_name)
39 .replace("::", "/");
40
41 let file_path = format!("{}.md", module_filename);
42 let module_content = generate_module_file(module_name, items, crate_data, &item_paths, crate_name);
43 files.insert(file_path, module_content);
44 }
45
46 Ok(MarkdownOutput {
47 crate_name: crate_name.to_string(),
48 files,
49 })
50}
51
52pub fn convert_to_markdown(crate_data: &Crate, include_private: bool) -> Result<String> {
54 let mut output = String::new();
55
56 let root_item = crate_data.index.get(&crate_data.root)
57 .ok_or_else(|| anyhow::anyhow!("Root item not found in index"))?;
58
59 let crate_name = root_item.name.as_deref().unwrap_or("unknown");
60 output.push_str(&format!("# {}\n\n", crate_name));
61
62 if let Some(docs) = &root_item.docs {
63 output.push_str(&format!("{}\n\n", docs));
64 }
65
66 let item_paths = build_path_map(crate_data);
68
69 let modules = group_by_module(crate_data, &item_paths, include_private);
71
72 output.push_str("## Table of Contents\n\n");
74 output.push_str(&generate_toc(&modules, crate_name));
75 output.push_str("\n\n---\n\n");
76
77 output.push_str(&generate_content(&modules, crate_data, &item_paths));
79
80 Ok(output)
81}
82
83fn build_path_map(crate_data: &Crate) -> HashMap<Id, Vec<String>> {
84 crate_data.paths.iter()
85 .map(|(id, summary)| {
86 (id.clone(), summary.path.clone())
87 })
88 .collect()
89}
90
91fn group_by_module(
92 crate_data: &Crate,
93 item_paths: &HashMap<Id, Vec<String>>,
94 include_private: bool,
95) -> HashMap<String, Vec<(Id, Item)>> {
96 let mut modules: HashMap<String, Vec<(Id, Item)>> = HashMap::new();
97
98 for (id, item) in &crate_data.index {
99 if id == &crate_data.root {
100 continue;
101 }
102
103 if !include_private && !is_public(item) {
104 continue;
105 }
106
107 if !can_format_item(item) {
109 continue;
110 }
111
112 let module_path = if let Some(path) = item_paths.get(id) {
114 if path.len() > 1 {
115 path[..path.len()-1].join("::")
116 } else {
117 continue; }
119 } else {
120 continue; };
122
123 modules.entry(module_path)
124 .or_insert_with(Vec::new)
125 .push((id.clone(), item.clone()));
126 }
127
128 for items in modules.values_mut() {
130 items.sort_by(|a, b| {
131 let name_a = a.1.name.as_deref().unwrap_or("");
132 let name_b = b.1.name.as_deref().unwrap_or("");
133 name_a.cmp(name_b)
134 });
135 }
136
137 modules
138}
139
140fn can_format_item(item: &Item) -> bool {
141 matches!(
142 item.inner,
143 ItemEnum::Struct(_) | ItemEnum::Enum(_) | ItemEnum::Function(_) |
144 ItemEnum::Trait(_) | ItemEnum::Module(_) | ItemEnum::Constant { .. } |
145 ItemEnum::TypeAlias(_)
146 )
147}
148
149fn generate_toc(modules: &HashMap<String, Vec<(Id, Item)>>, crate_name: &str) -> String {
150 let mut toc = String::new();
151
152 let mut module_names: Vec<_> = modules.keys().collect();
154 module_names.sort();
155
156 for module_name in module_names {
157 let items = &modules[module_name];
158
159 let display_name = module_name.strip_prefix(&format!("{}::", crate_name))
161 .unwrap_or(module_name);
162
163 toc.push_str(&format!("- **{}**\n", display_name));
164
165 for (_id, item) in items {
166 if let Some(name) = &item.name {
167 let full_path = format!("{}::{}", module_name, name);
168 let anchor = full_path.to_lowercase().replace("::", "-");
169 toc.push_str(&format!(" - [{}](#{})\n", name, anchor));
170 }
171 }
172 }
173
174 toc
175}
176
177fn generate_content(
178 modules: &HashMap<String, Vec<(Id, Item)>>,
179 crate_data: &Crate,
180 item_paths: &HashMap<Id, Vec<String>>,
181) -> String {
182 let mut output = String::new();
183
184 let mut module_names: Vec<_> = modules.keys().collect();
186 module_names.sort();
187
188 for module_name in module_names {
189 let items = &modules[module_name];
190
191 output.push_str(&format!("# Module: `{}`\n\n", module_name));
193
194 for (id, item) in items {
196 if let Some(section) = format_item_with_path(id, item, crate_data, item_paths) {
197 output.push_str(§ion);
198 output.push_str("\n\n");
199 }
200 }
201
202 output.push_str("---\n\n");
203 }
204
205 output
206}
207
208fn format_item_with_path(
209 item_id: &Id,
210 item: &Item,
211 crate_data: &Crate,
212 item_paths: &HashMap<Id, Vec<String>>,
213) -> Option<String> {
214 let full_path = item_paths.get(item_id)?;
215 let full_name = full_path.join("::");
216
217 let mut output = format_item(item_id, item, crate_data)?;
218
219 if let Some(name) = &item.name {
221 let old_header = format!("## {}\n\n", name);
222 let new_header = format!("## {}\n\n", full_name);
223 output = output.replace(&old_header, &new_header);
224 }
225
226 Some(output)
227}
228
229fn is_public(item: &Item) -> bool {
230 matches!(item.visibility, Visibility::Public)
231}
232
233fn format_item(item_id: &rustdoc_types::Id, item: &Item, crate_data: &Crate) -> Option<String> {
234 let name = item.name.as_ref()?;
235 let mut output = String::new();
236
237 match &item.inner {
238 ItemEnum::Struct(s) => {
239 output.push_str(&format!("## {}\n\n", name));
240 output.push_str("**Type:** Struct\n\n");
241
242 if let Some(docs) = &item.docs {
243 output.push_str(&format!("{}\n\n", docs));
244 }
245
246 if !s.generics.params.is_empty() {
247 output.push_str("**Generic Parameters:**\n");
248 for param in &s.generics.params {
249 output.push_str(&format!("- {}\n", format_generic_param(param)));
250 }
251 output.push_str("\n");
252 }
253
254 match &s.kind {
255 rustdoc_types::StructKind::Plain { fields, .. } => {
256 if !fields.is_empty() {
257 output.push_str("**Fields:**\n\n");
258 output.push_str("| Name | Type | Description |\n");
259 output.push_str("|------|------|-------------|\n");
260 for field_id in fields {
261 if let Some(field) = crate_data.index.get(field_id) {
262 if let Some(field_name) = &field.name {
263 let field_type = if let ItemEnum::StructField(ty) = &field.inner {
264 format_type(ty)
265 } else {
266 "?".to_string()
267 };
268 let field_doc = if let Some(docs) = &field.docs {
269 docs.lines().next().unwrap_or("").to_string()
270 } else {
271 "".to_string()
272 };
273 output.push_str(&format!("| `{}` | `{}` | {} |\n",
274 field_name, field_type, field_doc));
275 }
276 }
277 }
278 output.push_str("\n");
279 }
280 }
281 rustdoc_types::StructKind::Tuple(fields) => {
282 output.push_str(&format!("**Tuple Struct** with {} field(s)\n\n", fields.len()));
283 }
284 rustdoc_types::StructKind::Unit => {
285 output.push_str("**Unit Struct**\n\n");
286 }
287 }
288
289 let (inherent_impls, trait_impls) = collect_impls_for_type(item_id, crate_data);
290
291 if !inherent_impls.is_empty() {
292 output.push_str("**Methods:**\n\n");
293 for impl_block in inherent_impls {
294 output.push_str(&format_impl_methods(impl_block, crate_data));
295 }
296 output.push_str("\n");
297 }
298
299 if !trait_impls.is_empty() {
300 let user_impls: Vec<_> = trait_impls.iter()
301 .filter(|impl_block| {
302 !impl_block.is_synthetic && impl_block.blanket_impl.is_none()
303 })
304 .collect();
305
306 if !user_impls.is_empty() {
307 output.push_str("**Trait Implementations:**\n\n");
308 for impl_block in user_impls {
309 if let Some(trait_ref) = &impl_block.trait_ {
310 output.push_str(&format!("- **{}**\n", trait_ref.path));
311 let methods = format_impl_methods(impl_block, crate_data);
312 if !methods.is_empty() {
313 for line in methods.lines() {
314 output.push_str(&format!(" {}\n", line));
315 }
316 }
317 }
318 }
319 output.push_str("\n");
320 }
321 }
322 }
323 ItemEnum::Enum(e) => {
324 output.push_str(&format!("## {}\n\n", name));
325 output.push_str("**Type:** Enum\n\n");
326
327 if let Some(docs) = &item.docs {
328 output.push_str(&format!("{}\n\n", docs));
329 }
330
331 if !e.generics.params.is_empty() {
332 output.push_str("**Generic Parameters:**\n");
333 for param in &e.generics.params {
334 output.push_str(&format!("- {}\n", format_generic_param(param)));
335 }
336 output.push_str("\n");
337 }
338
339 if !e.variants.is_empty() {
340 output.push_str("**Variants:**\n\n");
341 output.push_str("| Variant | Kind | Description |\n");
342 output.push_str("|---------|------|-------------|\n");
343 for variant_id in &e.variants {
344 if let Some(variant) = crate_data.index.get(variant_id) {
345 if let Some(variant_name) = &variant.name {
346 let variant_kind = if let ItemEnum::Variant(v) = &variant.inner {
347 match &v.kind {
348 rustdoc_types::VariantKind::Plain => "Unit".to_string(),
349 rustdoc_types::VariantKind::Tuple(fields) => {
350 let types: Vec<_> = fields.iter().map(|field_id| {
351 if let Some(id) = field_id {
352 if let Some(field_item) = crate_data.index.get(id) {
353 if let ItemEnum::StructField(ty) = &field_item.inner {
354 return format_type(ty);
355 }
356 }
357 }
358 "?".to_string()
359 }).collect();
360 format!("Tuple({})", types.join(", "))
361 },
362 rustdoc_types::VariantKind::Struct { fields, .. } => {
363 format!("Struct ({} fields)", fields.len())
364 }
365 }
366 } else {
367 "?".to_string()
368 };
369 let variant_doc = if let Some(docs) = &variant.docs {
370 docs.lines().next().unwrap_or("").to_string()
371 } else {
372 "".to_string()
373 };
374 output.push_str(&format!("| `{}` | {} | {} |\n",
375 variant_name, variant_kind, variant_doc));
376 }
377 }
378 }
379 output.push_str("\n");
380 }
381
382 let (inherent_impls, trait_impls) = collect_impls_for_type(item_id, crate_data);
383
384 if !inherent_impls.is_empty() {
385 output.push_str("**Methods:**\n\n");
386 for impl_block in inherent_impls {
387 output.push_str(&format_impl_methods(impl_block, crate_data));
388 }
389 output.push_str("\n");
390 }
391
392 if !trait_impls.is_empty() {
393 let user_impls: Vec<_> = trait_impls.iter()
394 .filter(|impl_block| {
395 !impl_block.is_synthetic && impl_block.blanket_impl.is_none()
396 })
397 .collect();
398
399 if !user_impls.is_empty() {
400 output.push_str("**Trait Implementations:**\n\n");
401 for impl_block in user_impls {
402 if let Some(trait_ref) = &impl_block.trait_ {
403 output.push_str(&format!("- **{}**\n", trait_ref.path));
404 let methods = format_impl_methods(impl_block, crate_data);
405 if !methods.is_empty() {
406 for line in methods.lines() {
407 output.push_str(&format!(" {}\n", line));
408 }
409 }
410 }
411 }
412 output.push_str("\n");
413 }
414 }
415 }
416 ItemEnum::Function(f) => {
417 output.push_str(&format!("## {}\n\n", name));
418 output.push_str("**Type:** Function\n\n");
419
420 if let Some(docs) = &item.docs {
421 output.push_str(&format!("{}\n\n", docs));
422 }
423
424 output.push_str("```rust\n");
425 output.push_str(&format!("fn {}", name));
426
427 if !f.generics.params.is_empty() {
428 output.push_str("<");
429 let params: Vec<String> = f.generics.params.iter()
430 .map(format_generic_param)
431 .collect();
432 output.push_str(¶ms.join(", "));
433 output.push_str(">");
434 }
435
436 output.push_str("(");
437 let inputs: Vec<String> = f.sig.inputs.iter()
438 .map(|(name, ty)| format!("{}: {}", name, format_type(ty)))
439 .collect();
440 output.push_str(&inputs.join(", "));
441 output.push_str(")");
442
443 if let Some(output_type) = &f.sig.output {
444 output.push_str(&format!(" -> {}", format_type(output_type)));
445 }
446
447 output.push_str("\n```\n\n");
448 }
449 ItemEnum::Trait(t) => {
450 output.push_str(&format!("## {}\n\n", name));
451 output.push_str("**Type:** Trait\n\n");
452
453 if let Some(docs) = &item.docs {
454 output.push_str(&format!("{}\n\n", docs));
455 }
456
457 if !t.items.is_empty() {
458 output.push_str("**Methods:**\n\n");
459 for method_id in &t.items {
460 if let Some(method) = crate_data.index.get(method_id) {
461 if let Some(method_name) = &method.name {
462 output.push_str(&format!("- `{}`", method_name));
463 if let Some(method_docs) = &method.docs {
464 output.push_str(&format!(": {}", method_docs.lines().next().unwrap_or("")));
465 }
466 output.push_str("\n");
467 }
468 }
469 }
470 output.push_str("\n");
471 }
472 }
473 ItemEnum::Module(_) => {
474 output.push_str(&format!("## Module: {}\n\n", name));
475
476 if let Some(docs) = &item.docs {
477 output.push_str(&format!("{}\n\n", docs));
478 }
479 }
480 ItemEnum::Constant { .. } => {
481 output.push_str(&format!("## {}\n\n", name));
482 output.push_str("**Type:** Constant\n\n");
483
484 if let Some(docs) = &item.docs {
485 output.push_str(&format!("{}\n\n", docs));
486 }
487 }
488 ItemEnum::TypeAlias(_) => {
489 output.push_str(&format!("## {}\n\n", name));
490 output.push_str("**Type:** Type Alias\n\n");
491
492 if let Some(docs) = &item.docs {
493 output.push_str(&format!("{}\n\n", docs));
494 }
495 }
496 _ => {
497 return None;
498 }
499 }
500
501 Some(output)
502}
503
504fn format_generic_param(param: &rustdoc_types::GenericParamDef) -> String {
505 match ¶m.kind {
506 rustdoc_types::GenericParamDefKind::Lifetime { .. } => {
507 format!("'{}", param.name)
508 }
509 rustdoc_types::GenericParamDefKind::Type { .. } => {
510 param.name.clone()
511 }
512 rustdoc_types::GenericParamDefKind::Const { .. } => {
513 format!("const {}", param.name)
514 }
515 }
516}
517
518fn collect_impls_for_type<'a>(type_id: &rustdoc_types::Id, crate_data: &'a Crate) -> (Vec<&'a rustdoc_types::Impl>, Vec<&'a rustdoc_types::Impl>) {
519 use rustdoc_types::Type;
520
521 let mut inherent_impls = Vec::new();
522 let mut trait_impls = Vec::new();
523
524 for (_id, item) in &crate_data.index {
525 if let ItemEnum::Impl(impl_block) = &item.inner {
526 let matches = match &impl_block.for_ {
527 Type::ResolvedPath(path) => path.id == *type_id,
528 _ => false,
529 };
530
531 if matches {
532 if impl_block.trait_.is_some() {
533 trait_impls.push(impl_block);
534 } else {
535 inherent_impls.push(impl_block);
536 }
537 }
538 }
539 }
540
541 (inherent_impls, trait_impls)
542}
543
544fn format_impl_methods(impl_block: &rustdoc_types::Impl, crate_data: &Crate) -> String {
545 let mut output = String::new();
546
547 for method_id in &impl_block.items {
548 if let Some(method) = crate_data.index.get(method_id) {
549 if let ItemEnum::Function(f) = &method.inner {
550 if let Some(method_name) = &method.name {
551 let sig = format_function_signature(method_name, f);
552 let doc = if let Some(docs) = &method.docs {
553 docs.lines().next().unwrap_or("")
554 } else {
555 ""
556 };
557 output.push_str(&format!("- `{}` - {}\n", sig, doc));
558 }
559 }
560 }
561 }
562
563 output
564}
565
566fn format_function_signature(name: &str, f: &rustdoc_types::Function) -> String {
567 let mut sig = format!("fn {}", name);
568
569 if !f.generics.params.is_empty() {
570 sig.push('<');
571 let params: Vec<String> = f.generics.params.iter()
572 .map(format_generic_param)
573 .collect();
574 sig.push_str(¶ms.join(", "));
575 sig.push('>');
576 }
577
578 sig.push('(');
579 let inputs: Vec<String> = f.sig.inputs.iter()
580 .map(|(name, ty)| format!("{}: {}", name, format_type(ty)))
581 .collect();
582 sig.push_str(&inputs.join(", "));
583 sig.push(')');
584
585 if let Some(output_type) = &f.sig.output {
586 sig.push_str(&format!(" -> {}", format_type(output_type)));
587 }
588
589 sig
590}
591
592fn format_type(ty: &rustdoc_types::Type) -> String {
593 use rustdoc_types::Type;
594 match ty {
595 Type::ResolvedPath(path) => path.path.clone(),
596 Type::DynTrait(dt) => {
597 if let Some(first) = dt.traits.first() {
598 format!("dyn {}", first.trait_.path)
599 } else {
600 "dyn Trait".to_string()
601 }
602 }
603 Type::Generic(name) => name.clone(),
604 Type::Primitive(name) => name.clone(),
605 Type::FunctionPointer(_) => "fn(...)".to_string(),
606 Type::Tuple(types) => {
607 let formatted: Vec<_> = types.iter().map(format_type).collect();
608 format!("({})", formatted.join(", "))
609 }
610 Type::Slice(inner) => format!("[{}]", format_type(inner)),
611 Type::Array { type_, len } => format!("[{}; {}]", format_type(type_), len),
612 Type::Pat { type_, .. } => format_type(type_),
613 Type::ImplTrait(_bounds) => "impl Trait".to_string(),
614 Type::Infer => "_".to_string(),
615 Type::RawPointer { is_mutable, type_ } => {
616 if *is_mutable {
617 format!("*mut {}", format_type(type_))
618 } else {
619 format!("*const {}", format_type(type_))
620 }
621 }
622 Type::BorrowedRef { lifetime, is_mutable, type_ } => {
623 let lifetime_str = lifetime.as_deref().unwrap_or("'_");
624 if *is_mutable {
625 format!("&{} mut {}", lifetime_str, format_type(type_))
626 } else {
627 format!("&{} {}", lifetime_str, format_type(type_))
628 }
629 }
630 Type::QualifiedPath { name, self_type, trait_, .. } => {
631 if let Some(trait_) = trait_ {
632 format!("<{} as {}>::{}", format_type(self_type), trait_.path, name)
633 } else {
634 format!("{}::{}", format_type(self_type), name)
635 }
636 }
637 }
638}
639
640fn generate_crate_index(
641 crate_name: &str,
642 root_item: &Item,
643 modules: &HashMap<String, Vec<(Id, Item)>>,
644) -> String {
645 let mut output = String::new();
646
647 output.push_str(&format!("# {}\n\n", crate_name));
648
649 if let Some(docs) = &root_item.docs {
650 output.push_str(&format!("{}\n\n", docs));
651 }
652
653 output.push_str("## Modules\n\n");
655
656 let mut module_names: Vec<_> = modules.keys().collect();
657 module_names.sort();
658
659 for module_name in module_names {
660 let items = &modules[module_name];
661
662 let display_name = module_name.strip_prefix(&format!("{}::", crate_name))
663 .unwrap_or(module_name);
664
665 let module_file = format!("{}.md", display_name.replace("::", "/"));
666
667 let mut counts = HashMap::new();
669 for (_id, item) in items {
670 let type_name = match &item.inner {
671 ItemEnum::Struct(_) => "structs",
672 ItemEnum::Enum(_) => "enums",
673 ItemEnum::Function(_) => "functions",
674 ItemEnum::Trait(_) => "traits",
675 ItemEnum::Constant { .. } => "constants",
676 ItemEnum::TypeAlias(_) => "type aliases",
677 ItemEnum::Module(_) => "modules",
678 _ => continue,
679 };
680 *counts.entry(type_name).or_insert(0) += 1;
681 }
682
683 output.push_str(&format!("### [`{}`]({})\n\n", display_name, module_file));
684
685 if !counts.is_empty() {
686 let summary: Vec<String> = counts.iter()
687 .map(|(name, count)| format!("{} {}", count, name))
688 .collect();
689 output.push_str(&format!("*{}*\n\n", summary.join(", ")));
690 }
691 }
692
693 output
694}
695
696fn generate_module_file(
697 module_name: &str,
698 items: &[(Id, Item)],
699 crate_data: &Crate,
700 item_paths: &HashMap<Id, Vec<String>>,
701 crate_name: &str,
702) -> String {
703 let mut output = String::new();
704
705 let display_name = module_name.strip_prefix(&format!("{}::", crate_name))
706 .unwrap_or(module_name);
707
708 let breadcrumb = format!("`{}`", module_name.replace("::", " > "));
710 output.push_str(&format!("{}\n\n", breadcrumb));
711
712 output.push_str(&format!("# Module: {}\n\n", display_name));
713
714 output.push_str("## Contents\n\n");
716
717 let mut by_type: HashMap<&str, Vec<&Item>> = HashMap::new();
718 for (_id, item) in items {
719 let type_name = match &item.inner {
720 ItemEnum::Struct(_) => "Structs",
721 ItemEnum::Enum(_) => "Enums",
722 ItemEnum::Function(_) => "Functions",
723 ItemEnum::Trait(_) => "Traits",
724 ItemEnum::Constant { .. } => "Constants",
725 ItemEnum::TypeAlias(_) => "Type Aliases",
726 ItemEnum::Module(_) => "Modules",
727 _ => continue,
728 };
729 by_type.entry(type_name).or_insert_with(Vec::new).push(&item);
730 }
731
732 let type_order = ["Modules", "Structs", "Enums", "Functions", "Traits", "Constants", "Type Aliases"];
733 for type_name in &type_order {
734 if let Some(items_of_type) = by_type.get(type_name) {
735 output.push_str(&format!("**{}**\n\n", type_name));
736 for item in items_of_type {
737 if let Some(name) = &item.name {
738 let anchor = name.to_lowercase();
739 output.push_str(&format!("- [`{}`](#{})", name, anchor));
740 if let Some(docs) = &item.docs {
741 if let Some(first_line) = docs.lines().next() {
742 if !first_line.is_empty() {
743 output.push_str(&format!(" - {}", first_line));
744 }
745 }
746 }
747 output.push_str("\n");
748 }
749 }
750 output.push_str("\n");
751 }
752 }
753
754 output.push_str("---\n\n");
755
756 for (id, item) in items {
758 if let Some(section) = format_item_with_path(id, item, crate_data, item_paths) {
759 output.push_str(§ion);
760 output.push_str("\n\n");
761 }
762 }
763
764 output
765}