1use anyhow::Result;
4use rustdoc_types::{Crate, Item, ItemEnum, Visibility, Id};
5use std::collections::HashMap;
6
7pub fn convert_to_markdown(crate_data: &Crate, include_private: bool) -> Result<String> {
9 let mut output = String::new();
10
11 let root_item = crate_data.index.get(&crate_data.root)
12 .ok_or_else(|| anyhow::anyhow!("Root item not found in index"))?;
13
14 let crate_name = root_item.name.as_deref().unwrap_or("unknown");
15 output.push_str(&format!("# {}\n\n", crate_name));
16
17 if let Some(docs) = &root_item.docs {
18 output.push_str(&format!("{}\n\n", docs));
19 }
20
21 let item_paths = build_path_map(crate_data);
23
24 let modules = group_by_module(crate_data, &item_paths, include_private);
26
27 output.push_str("## Table of Contents\n\n");
29 output.push_str(&generate_toc(&modules, crate_name));
30 output.push_str("\n\n---\n\n");
31
32 output.push_str(&generate_content(&modules, crate_data, &item_paths));
34
35 Ok(output)
36}
37
38fn build_path_map(crate_data: &Crate) -> HashMap<Id, Vec<String>> {
39 crate_data.paths.iter()
40 .map(|(id, summary)| {
41 (id.clone(), summary.path.clone())
42 })
43 .collect()
44}
45
46fn group_by_module(
47 crate_data: &Crate,
48 item_paths: &HashMap<Id, Vec<String>>,
49 include_private: bool,
50) -> HashMap<String, Vec<(Id, Item)>> {
51 let mut modules: HashMap<String, Vec<(Id, Item)>> = HashMap::new();
52
53 for (id, item) in &crate_data.index {
54 if id == &crate_data.root {
55 continue;
56 }
57
58 if !include_private && !is_public(item) {
59 continue;
60 }
61
62 if !can_format_item(item) {
64 continue;
65 }
66
67 let module_path = if let Some(path) = item_paths.get(id) {
69 if path.len() > 1 {
70 path[..path.len()-1].join("::")
71 } else {
72 continue; }
74 } else {
75 continue; };
77
78 modules.entry(module_path)
79 .or_insert_with(Vec::new)
80 .push((id.clone(), item.clone()));
81 }
82
83 for items in modules.values_mut() {
85 items.sort_by(|a, b| {
86 let name_a = a.1.name.as_deref().unwrap_or("");
87 let name_b = b.1.name.as_deref().unwrap_or("");
88 name_a.cmp(name_b)
89 });
90 }
91
92 modules
93}
94
95fn can_format_item(item: &Item) -> bool {
96 matches!(
97 item.inner,
98 ItemEnum::Struct(_) | ItemEnum::Enum(_) | ItemEnum::Function(_) |
99 ItemEnum::Trait(_) | ItemEnum::Module(_) | ItemEnum::Constant { .. } |
100 ItemEnum::TypeAlias(_)
101 )
102}
103
104fn generate_toc(modules: &HashMap<String, Vec<(Id, Item)>>, crate_name: &str) -> String {
105 let mut toc = String::new();
106
107 let mut module_names: Vec<_> = modules.keys().collect();
109 module_names.sort();
110
111 for module_name in module_names {
112 let items = &modules[module_name];
113
114 let display_name = module_name.strip_prefix(&format!("{}::", crate_name))
116 .unwrap_or(module_name);
117
118 toc.push_str(&format!("- **{}**\n", display_name));
119
120 for (_id, item) in items {
121 if let Some(name) = &item.name {
122 let full_path = format!("{}::{}", module_name, name);
123 let anchor = full_path.to_lowercase().replace("::", "-");
124 toc.push_str(&format!(" - [{}](#{})\n", name, anchor));
125 }
126 }
127 }
128
129 toc
130}
131
132fn generate_content(
133 modules: &HashMap<String, Vec<(Id, Item)>>,
134 crate_data: &Crate,
135 item_paths: &HashMap<Id, Vec<String>>,
136) -> String {
137 let mut output = String::new();
138
139 let mut module_names: Vec<_> = modules.keys().collect();
141 module_names.sort();
142
143 for module_name in module_names {
144 let items = &modules[module_name];
145
146 output.push_str(&format!("# Module: `{}`\n\n", module_name));
148
149 for (id, item) in items {
151 if let Some(section) = format_item_with_path(id, item, crate_data, item_paths) {
152 output.push_str(§ion);
153 output.push_str("\n\n");
154 }
155 }
156
157 output.push_str("---\n\n");
158 }
159
160 output
161}
162
163fn format_item_with_path(
164 item_id: &Id,
165 item: &Item,
166 crate_data: &Crate,
167 item_paths: &HashMap<Id, Vec<String>>,
168) -> Option<String> {
169 let full_path = item_paths.get(item_id)?;
170 let full_name = full_path.join("::");
171
172 let mut output = format_item(item_id, item, crate_data)?;
173
174 if let Some(name) = &item.name {
176 let old_header = format!("## {}\n\n", name);
177 let new_header = format!("## {}\n\n", full_name);
178 output = output.replace(&old_header, &new_header);
179 }
180
181 Some(output)
182}
183
184fn is_public(item: &Item) -> bool {
185 matches!(item.visibility, Visibility::Public)
186}
187
188fn format_item(item_id: &rustdoc_types::Id, item: &Item, crate_data: &Crate) -> Option<String> {
189 let name = item.name.as_ref()?;
190 let mut output = String::new();
191
192 match &item.inner {
193 ItemEnum::Struct(s) => {
194 output.push_str(&format!("## {}\n\n", name));
195 output.push_str("**Type:** Struct\n\n");
196
197 if let Some(docs) = &item.docs {
198 output.push_str(&format!("{}\n\n", docs));
199 }
200
201 if !s.generics.params.is_empty() {
202 output.push_str("**Generic Parameters:**\n");
203 for param in &s.generics.params {
204 output.push_str(&format!("- {}\n", format_generic_param(param)));
205 }
206 output.push_str("\n");
207 }
208
209 match &s.kind {
210 rustdoc_types::StructKind::Plain { fields, .. } => {
211 if !fields.is_empty() {
212 output.push_str("**Fields:**\n\n");
213 output.push_str("| Name | Type | Description |\n");
214 output.push_str("|------|------|-------------|\n");
215 for field_id in fields {
216 if let Some(field) = crate_data.index.get(field_id) {
217 if let Some(field_name) = &field.name {
218 let field_type = if let ItemEnum::StructField(ty) = &field.inner {
219 format_type(ty)
220 } else {
221 "?".to_string()
222 };
223 let field_doc = if let Some(docs) = &field.docs {
224 docs.lines().next().unwrap_or("").to_string()
225 } else {
226 "".to_string()
227 };
228 output.push_str(&format!("| `{}` | `{}` | {} |\n",
229 field_name, field_type, field_doc));
230 }
231 }
232 }
233 output.push_str("\n");
234 }
235 }
236 rustdoc_types::StructKind::Tuple(fields) => {
237 output.push_str(&format!("**Tuple Struct** with {} field(s)\n\n", fields.len()));
238 }
239 rustdoc_types::StructKind::Unit => {
240 output.push_str("**Unit Struct**\n\n");
241 }
242 }
243
244 let (inherent_impls, trait_impls) = collect_impls_for_type(item_id, crate_data);
245
246 if !inherent_impls.is_empty() {
247 output.push_str("**Methods:**\n\n");
248 for impl_block in inherent_impls {
249 output.push_str(&format_impl_methods(impl_block, crate_data));
250 }
251 output.push_str("\n");
252 }
253
254 if !trait_impls.is_empty() {
255 let user_impls: Vec<_> = trait_impls.iter()
256 .filter(|impl_block| {
257 !impl_block.is_synthetic && impl_block.blanket_impl.is_none()
258 })
259 .collect();
260
261 if !user_impls.is_empty() {
262 output.push_str("**Trait Implementations:**\n\n");
263 for impl_block in user_impls {
264 if let Some(trait_ref) = &impl_block.trait_ {
265 output.push_str(&format!("- **{}**\n", trait_ref.path));
266 let methods = format_impl_methods(impl_block, crate_data);
267 if !methods.is_empty() {
268 for line in methods.lines() {
269 output.push_str(&format!(" {}\n", line));
270 }
271 }
272 }
273 }
274 output.push_str("\n");
275 }
276 }
277 }
278 ItemEnum::Enum(e) => {
279 output.push_str(&format!("## {}\n\n", name));
280 output.push_str("**Type:** Enum\n\n");
281
282 if let Some(docs) = &item.docs {
283 output.push_str(&format!("{}\n\n", docs));
284 }
285
286 if !e.generics.params.is_empty() {
287 output.push_str("**Generic Parameters:**\n");
288 for param in &e.generics.params {
289 output.push_str(&format!("- {}\n", format_generic_param(param)));
290 }
291 output.push_str("\n");
292 }
293
294 if !e.variants.is_empty() {
295 output.push_str("**Variants:**\n\n");
296 output.push_str("| Variant | Kind | Description |\n");
297 output.push_str("|---------|------|-------------|\n");
298 for variant_id in &e.variants {
299 if let Some(variant) = crate_data.index.get(variant_id) {
300 if let Some(variant_name) = &variant.name {
301 let variant_kind = if let ItemEnum::Variant(v) = &variant.inner {
302 match &v.kind {
303 rustdoc_types::VariantKind::Plain => "Unit".to_string(),
304 rustdoc_types::VariantKind::Tuple(fields) => {
305 let types: Vec<_> = fields.iter().map(|field_id| {
306 if let Some(id) = field_id {
307 if let Some(field_item) = crate_data.index.get(id) {
308 if let ItemEnum::StructField(ty) = &field_item.inner {
309 return format_type(ty);
310 }
311 }
312 }
313 "?".to_string()
314 }).collect();
315 format!("Tuple({})", types.join(", "))
316 },
317 rustdoc_types::VariantKind::Struct { fields, .. } => {
318 format!("Struct ({} fields)", fields.len())
319 }
320 }
321 } else {
322 "?".to_string()
323 };
324 let variant_doc = if let Some(docs) = &variant.docs {
325 docs.lines().next().unwrap_or("").to_string()
326 } else {
327 "".to_string()
328 };
329 output.push_str(&format!("| `{}` | {} | {} |\n",
330 variant_name, variant_kind, variant_doc));
331 }
332 }
333 }
334 output.push_str("\n");
335 }
336
337 let (inherent_impls, trait_impls) = collect_impls_for_type(item_id, crate_data);
338
339 if !inherent_impls.is_empty() {
340 output.push_str("**Methods:**\n\n");
341 for impl_block in inherent_impls {
342 output.push_str(&format_impl_methods(impl_block, crate_data));
343 }
344 output.push_str("\n");
345 }
346
347 if !trait_impls.is_empty() {
348 let user_impls: Vec<_> = trait_impls.iter()
349 .filter(|impl_block| {
350 !impl_block.is_synthetic && impl_block.blanket_impl.is_none()
351 })
352 .collect();
353
354 if !user_impls.is_empty() {
355 output.push_str("**Trait Implementations:**\n\n");
356 for impl_block in user_impls {
357 if let Some(trait_ref) = &impl_block.trait_ {
358 output.push_str(&format!("- **{}**\n", trait_ref.path));
359 let methods = format_impl_methods(impl_block, crate_data);
360 if !methods.is_empty() {
361 for line in methods.lines() {
362 output.push_str(&format!(" {}\n", line));
363 }
364 }
365 }
366 }
367 output.push_str("\n");
368 }
369 }
370 }
371 ItemEnum::Function(f) => {
372 output.push_str(&format!("## {}\n\n", name));
373 output.push_str("**Type:** Function\n\n");
374
375 if let Some(docs) = &item.docs {
376 output.push_str(&format!("{}\n\n", docs));
377 }
378
379 output.push_str("```rust\n");
380 output.push_str(&format!("fn {}", name));
381
382 if !f.generics.params.is_empty() {
383 output.push_str("<");
384 let params: Vec<String> = f.generics.params.iter()
385 .map(format_generic_param)
386 .collect();
387 output.push_str(¶ms.join(", "));
388 output.push_str(">");
389 }
390
391 output.push_str("(");
392 let inputs: Vec<String> = f.sig.inputs.iter()
393 .map(|(name, ty)| format!("{}: {}", name, format_type(ty)))
394 .collect();
395 output.push_str(&inputs.join(", "));
396 output.push_str(")");
397
398 if let Some(output_type) = &f.sig.output {
399 output.push_str(&format!(" -> {}", format_type(output_type)));
400 }
401
402 output.push_str("\n```\n\n");
403 }
404 ItemEnum::Trait(t) => {
405 output.push_str(&format!("## {}\n\n", name));
406 output.push_str("**Type:** Trait\n\n");
407
408 if let Some(docs) = &item.docs {
409 output.push_str(&format!("{}\n\n", docs));
410 }
411
412 if !t.items.is_empty() {
413 output.push_str("**Methods:**\n\n");
414 for method_id in &t.items {
415 if let Some(method) = crate_data.index.get(method_id) {
416 if let Some(method_name) = &method.name {
417 output.push_str(&format!("- `{}`", method_name));
418 if let Some(method_docs) = &method.docs {
419 output.push_str(&format!(": {}", method_docs.lines().next().unwrap_or("")));
420 }
421 output.push_str("\n");
422 }
423 }
424 }
425 output.push_str("\n");
426 }
427 }
428 ItemEnum::Module(_) => {
429 output.push_str(&format!("## Module: {}\n\n", name));
430
431 if let Some(docs) = &item.docs {
432 output.push_str(&format!("{}\n\n", docs));
433 }
434 }
435 ItemEnum::Constant { .. } => {
436 output.push_str(&format!("## {}\n\n", name));
437 output.push_str("**Type:** Constant\n\n");
438
439 if let Some(docs) = &item.docs {
440 output.push_str(&format!("{}\n\n", docs));
441 }
442 }
443 ItemEnum::TypeAlias(_) => {
444 output.push_str(&format!("## {}\n\n", name));
445 output.push_str("**Type:** Type Alias\n\n");
446
447 if let Some(docs) = &item.docs {
448 output.push_str(&format!("{}\n\n", docs));
449 }
450 }
451 _ => {
452 return None;
453 }
454 }
455
456 Some(output)
457}
458
459fn format_generic_param(param: &rustdoc_types::GenericParamDef) -> String {
460 match ¶m.kind {
461 rustdoc_types::GenericParamDefKind::Lifetime { .. } => {
462 format!("'{}", param.name)
463 }
464 rustdoc_types::GenericParamDefKind::Type { .. } => {
465 param.name.clone()
466 }
467 rustdoc_types::GenericParamDefKind::Const { .. } => {
468 format!("const {}", param.name)
469 }
470 }
471}
472
473fn collect_impls_for_type<'a>(type_id: &rustdoc_types::Id, crate_data: &'a Crate) -> (Vec<&'a rustdoc_types::Impl>, Vec<&'a rustdoc_types::Impl>) {
474 use rustdoc_types::Type;
475
476 let mut inherent_impls = Vec::new();
477 let mut trait_impls = Vec::new();
478
479 for (_id, item) in &crate_data.index {
480 if let ItemEnum::Impl(impl_block) = &item.inner {
481 let matches = match &impl_block.for_ {
482 Type::ResolvedPath(path) => path.id == *type_id,
483 _ => false,
484 };
485
486 if matches {
487 if impl_block.trait_.is_some() {
488 trait_impls.push(impl_block);
489 } else {
490 inherent_impls.push(impl_block);
491 }
492 }
493 }
494 }
495
496 (inherent_impls, trait_impls)
497}
498
499fn format_impl_methods(impl_block: &rustdoc_types::Impl, crate_data: &Crate) -> String {
500 let mut output = String::new();
501
502 for method_id in &impl_block.items {
503 if let Some(method) = crate_data.index.get(method_id) {
504 if let ItemEnum::Function(f) = &method.inner {
505 if let Some(method_name) = &method.name {
506 let sig = format_function_signature(method_name, f);
507 let doc = if let Some(docs) = &method.docs {
508 docs.lines().next().unwrap_or("")
509 } else {
510 ""
511 };
512 output.push_str(&format!("- `{}` - {}\n", sig, doc));
513 }
514 }
515 }
516 }
517
518 output
519}
520
521fn format_function_signature(name: &str, f: &rustdoc_types::Function) -> String {
522 let mut sig = format!("fn {}", name);
523
524 if !f.generics.params.is_empty() {
525 sig.push('<');
526 let params: Vec<String> = f.generics.params.iter()
527 .map(format_generic_param)
528 .collect();
529 sig.push_str(¶ms.join(", "));
530 sig.push('>');
531 }
532
533 sig.push('(');
534 let inputs: Vec<String> = f.sig.inputs.iter()
535 .map(|(name, ty)| format!("{}: {}", name, format_type(ty)))
536 .collect();
537 sig.push_str(&inputs.join(", "));
538 sig.push(')');
539
540 if let Some(output_type) = &f.sig.output {
541 sig.push_str(&format!(" -> {}", format_type(output_type)));
542 }
543
544 sig
545}
546
547fn format_type(ty: &rustdoc_types::Type) -> String {
548 use rustdoc_types::Type;
549 match ty {
550 Type::ResolvedPath(path) => path.path.clone(),
551 Type::DynTrait(dt) => {
552 if let Some(first) = dt.traits.first() {
553 format!("dyn {}", first.trait_.path)
554 } else {
555 "dyn Trait".to_string()
556 }
557 }
558 Type::Generic(name) => name.clone(),
559 Type::Primitive(name) => name.clone(),
560 Type::FunctionPointer(_) => "fn(...)".to_string(),
561 Type::Tuple(types) => {
562 let formatted: Vec<_> = types.iter().map(format_type).collect();
563 format!("({})", formatted.join(", "))
564 }
565 Type::Slice(inner) => format!("[{}]", format_type(inner)),
566 Type::Array { type_, len } => format!("[{}; {}]", format_type(type_), len),
567 Type::Pat { type_, .. } => format_type(type_),
568 Type::ImplTrait(_bounds) => "impl Trait".to_string(),
569 Type::Infer => "_".to_string(),
570 Type::RawPointer { is_mutable, type_ } => {
571 if *is_mutable {
572 format!("*mut {}", format_type(type_))
573 } else {
574 format!("*const {}", format_type(type_))
575 }
576 }
577 Type::BorrowedRef { lifetime, is_mutable, type_ } => {
578 let lifetime_str = lifetime.as_deref().unwrap_or("'_");
579 if *is_mutable {
580 format!("&{} mut {}", lifetime_str, format_type(type_))
581 } else {
582 format!("&{} {}", lifetime_str, format_type(type_))
583 }
584 }
585 Type::QualifiedPath { name, self_type, trait_, .. } => {
586 if let Some(trait_) = trait_ {
587 format!("<{} as {}>::{}", format_type(self_type), trait_.path, name)
588 } else {
589 format!("{}::{}", format_type(self_type), name)
590 }
591 }
592 }
593}