rustdoc_json_to_markdown/
converter.rs

1//! Markdown converter for rustdoc JSON data.
2
3use anyhow::Result;
4use rustdoc_types::{Crate, Item, ItemEnum, Visibility, Id};
5use std::collections::HashMap;
6
7/// Convert a rustdoc Crate to markdown format.
8pub 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    // Build a map of item_id -> full_path using the paths data
22    let item_paths = build_path_map(crate_data);
23
24    // Group items by module
25    let modules = group_by_module(crate_data, &item_paths, include_private);
26
27    // Generate hierarchical ToC
28    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    // Generate content organized by module
33    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        // Skip if we can't format this item type
63        if !can_format_item(item) {
64            continue;
65        }
66
67        // Get the module path (all elements except the last one)
68        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; // Skip root-level items without module
73            }
74        } else {
75            continue; // Skip items without path info
76        };
77
78        modules.entry(module_path)
79            .or_insert_with(Vec::new)
80            .push((id.clone(), item.clone()));
81    }
82
83    // Sort items within each module by name
84    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    // Sort modules alphabetically
108    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        // Get the last component of the module path for display
115        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    // Sort modules alphabetically
140    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        // Module header
147        output.push_str(&format!("# Module: `{}`\n\n", module_name));
148
149        // Generate content for each item in the module
150        for (id, item) in items {
151            if let Some(section) = format_item_with_path(id, item, crate_data, item_paths) {
152                output.push_str(&section);
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    // Replace the simple name header with the full path
175    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(&params.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 &param.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(&params.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}