rustdoc_json_to_markdown/
converter.rs

1//! Markdown converter for rustdoc JSON data.
2
3use anyhow::Result;
4use rustdoc_types::{Crate, Item, ItemEnum, Visibility};
5
6/// Convert a rustdoc Crate to markdown format.
7pub fn convert_to_markdown(crate_data: &Crate, include_private: bool) -> Result<String> {
8    let mut output = String::new();
9
10    let root_item = crate_data.index.get(&crate_data.root)
11        .ok_or_else(|| anyhow::anyhow!("Root item not found in index"))?;
12
13    let crate_name = root_item.name.as_deref().unwrap_or("unknown");
14    output.push_str(&format!("# {}\n\n", crate_name));
15
16    if let Some(docs) = &root_item.docs {
17        output.push_str(&format!("{}\n\n", docs));
18    }
19
20    output.push_str("## Table of Contents\n\n");
21
22    let mut items: Vec<_> = crate_data.index.iter().collect();
23    items.sort_by(|a, b| {
24        let name_a = a.1.name.as_deref().unwrap_or("");
25        let name_b = b.1.name.as_deref().unwrap_or("");
26        name_a.cmp(name_b)
27    });
28
29    let mut toc_entries = Vec::new();
30    let mut content_sections = Vec::new();
31
32    for (id, item) in &items {
33        if *id == &crate_data.root {
34            continue;
35        }
36
37        if !include_private && !is_public(item) {
38            continue;
39        }
40
41        if let Some(section) = format_item(*id, item, crate_data) {
42            if let Some(name) = &item.name {
43                let anchor = name.to_lowercase().replace("::", "-");
44                toc_entries.push(format!("- [{}](#{})", name, anchor));
45                content_sections.push(section);
46            }
47        }
48    }
49
50    output.push_str(&toc_entries.join("\n"));
51    output.push_str("\n\n---\n\n");
52    output.push_str(&content_sections.join("\n\n"));
53
54    Ok(output)
55}
56
57fn is_public(item: &Item) -> bool {
58    matches!(item.visibility, Visibility::Public)
59}
60
61fn format_item(item_id: &rustdoc_types::Id, item: &Item, crate_data: &Crate) -> Option<String> {
62    let name = item.name.as_ref()?;
63    let mut output = String::new();
64
65    match &item.inner {
66        ItemEnum::Struct(s) => {
67            output.push_str(&format!("## {}\n\n", name));
68            output.push_str("**Type:** Struct\n\n");
69
70            if let Some(docs) = &item.docs {
71                output.push_str(&format!("{}\n\n", docs));
72            }
73
74            if !s.generics.params.is_empty() {
75                output.push_str("**Generic Parameters:**\n");
76                for param in &s.generics.params {
77                    output.push_str(&format!("- {}\n", format_generic_param(param)));
78                }
79                output.push_str("\n");
80            }
81
82            match &s.kind {
83                rustdoc_types::StructKind::Plain { fields, .. } => {
84                    if !fields.is_empty() {
85                        output.push_str("**Fields:**\n\n");
86                        output.push_str("| Name | Type | Description |\n");
87                        output.push_str("|------|------|-------------|\n");
88                        for field_id in fields {
89                            if let Some(field) = crate_data.index.get(field_id) {
90                                if let Some(field_name) = &field.name {
91                                    let field_type = if let ItemEnum::StructField(ty) = &field.inner {
92                                        format_type(ty)
93                                    } else {
94                                        "?".to_string()
95                                    };
96                                    let field_doc = if let Some(docs) = &field.docs {
97                                        docs.lines().next().unwrap_or("").to_string()
98                                    } else {
99                                        "".to_string()
100                                    };
101                                    output.push_str(&format!("| `{}` | `{}` | {} |\n",
102                                        field_name, field_type, field_doc));
103                                }
104                            }
105                        }
106                        output.push_str("\n");
107                    }
108                }
109                rustdoc_types::StructKind::Tuple(fields) => {
110                    output.push_str(&format!("**Tuple Struct** with {} field(s)\n\n", fields.len()));
111                }
112                rustdoc_types::StructKind::Unit => {
113                    output.push_str("**Unit Struct**\n\n");
114                }
115            }
116
117            let (inherent_impls, trait_impls) = collect_impls_for_type(item_id, crate_data);
118
119            if !inherent_impls.is_empty() {
120                output.push_str("**Methods:**\n\n");
121                for impl_block in inherent_impls {
122                    output.push_str(&format_impl_methods(impl_block, crate_data));
123                }
124                output.push_str("\n");
125            }
126
127            if !trait_impls.is_empty() {
128                let user_impls: Vec<_> = trait_impls.iter()
129                    .filter(|impl_block| {
130                        !impl_block.is_synthetic && impl_block.blanket_impl.is_none()
131                    })
132                    .collect();
133
134                if !user_impls.is_empty() {
135                    output.push_str("**Trait Implementations:**\n\n");
136                    for impl_block in user_impls {
137                        if let Some(trait_ref) = &impl_block.trait_ {
138                            output.push_str(&format!("- **{}**\n", trait_ref.path));
139                            let methods = format_impl_methods(impl_block, crate_data);
140                            if !methods.is_empty() {
141                                for line in methods.lines() {
142                                    output.push_str(&format!("  {}\n", line));
143                                }
144                            }
145                        }
146                    }
147                    output.push_str("\n");
148                }
149            }
150        }
151        ItemEnum::Enum(e) => {
152            output.push_str(&format!("## {}\n\n", name));
153            output.push_str("**Type:** Enum\n\n");
154
155            if let Some(docs) = &item.docs {
156                output.push_str(&format!("{}\n\n", docs));
157            }
158
159            if !e.generics.params.is_empty() {
160                output.push_str("**Generic Parameters:**\n");
161                for param in &e.generics.params {
162                    output.push_str(&format!("- {}\n", format_generic_param(param)));
163                }
164                output.push_str("\n");
165            }
166
167            if !e.variants.is_empty() {
168                output.push_str("**Variants:**\n\n");
169                output.push_str("| Variant | Kind | Description |\n");
170                output.push_str("|---------|------|-------------|\n");
171                for variant_id in &e.variants {
172                    if let Some(variant) = crate_data.index.get(variant_id) {
173                        if let Some(variant_name) = &variant.name {
174                            let variant_kind = if let ItemEnum::Variant(v) = &variant.inner {
175                                match &v.kind {
176                                    rustdoc_types::VariantKind::Plain => "Unit".to_string(),
177                                    rustdoc_types::VariantKind::Tuple(fields) => {
178                                        let types: Vec<_> = fields.iter().map(|field_id| {
179                                            if let Some(id) = field_id {
180                                                if let Some(field_item) = crate_data.index.get(id) {
181                                                    if let ItemEnum::StructField(ty) = &field_item.inner {
182                                                        return format_type(ty);
183                                                    }
184                                                }
185                                            }
186                                            "?".to_string()
187                                        }).collect();
188                                        format!("Tuple({})", types.join(", "))
189                                    },
190                                    rustdoc_types::VariantKind::Struct { fields, .. } => {
191                                        format!("Struct ({} fields)", fields.len())
192                                    }
193                                }
194                            } else {
195                                "?".to_string()
196                            };
197                            let variant_doc = if let Some(docs) = &variant.docs {
198                                docs.lines().next().unwrap_or("").to_string()
199                            } else {
200                                "".to_string()
201                            };
202                            output.push_str(&format!("| `{}` | {} | {} |\n",
203                                variant_name, variant_kind, variant_doc));
204                        }
205                    }
206                }
207                output.push_str("\n");
208            }
209
210            let (inherent_impls, trait_impls) = collect_impls_for_type(item_id, crate_data);
211
212            if !inherent_impls.is_empty() {
213                output.push_str("**Methods:**\n\n");
214                for impl_block in inherent_impls {
215                    output.push_str(&format_impl_methods(impl_block, crate_data));
216                }
217                output.push_str("\n");
218            }
219
220            if !trait_impls.is_empty() {
221                let user_impls: Vec<_> = trait_impls.iter()
222                    .filter(|impl_block| {
223                        !impl_block.is_synthetic && impl_block.blanket_impl.is_none()
224                    })
225                    .collect();
226
227                if !user_impls.is_empty() {
228                    output.push_str("**Trait Implementations:**\n\n");
229                    for impl_block in user_impls {
230                        if let Some(trait_ref) = &impl_block.trait_ {
231                            output.push_str(&format!("- **{}**\n", trait_ref.path));
232                            let methods = format_impl_methods(impl_block, crate_data);
233                            if !methods.is_empty() {
234                                for line in methods.lines() {
235                                    output.push_str(&format!("  {}\n", line));
236                                }
237                            }
238                        }
239                    }
240                    output.push_str("\n");
241                }
242            }
243        }
244        ItemEnum::Function(f) => {
245            output.push_str(&format!("## {}\n\n", name));
246            output.push_str("**Type:** Function\n\n");
247
248            if let Some(docs) = &item.docs {
249                output.push_str(&format!("{}\n\n", docs));
250            }
251
252            output.push_str("```rust\n");
253            output.push_str(&format!("fn {}", name));
254
255            if !f.generics.params.is_empty() {
256                output.push_str("<");
257                let params: Vec<String> = f.generics.params.iter()
258                    .map(format_generic_param)
259                    .collect();
260                output.push_str(&params.join(", "));
261                output.push_str(">");
262            }
263
264            output.push_str("(");
265            let inputs: Vec<String> = f.sig.inputs.iter()
266                .map(|(name, ty)| format!("{}: {}", name, format_type(ty)))
267                .collect();
268            output.push_str(&inputs.join(", "));
269            output.push_str(")");
270
271            if let Some(output_type) = &f.sig.output {
272                output.push_str(&format!(" -> {}", format_type(output_type)));
273            }
274
275            output.push_str("\n```\n\n");
276        }
277        ItemEnum::Trait(t) => {
278            output.push_str(&format!("## {}\n\n", name));
279            output.push_str("**Type:** Trait\n\n");
280
281            if let Some(docs) = &item.docs {
282                output.push_str(&format!("{}\n\n", docs));
283            }
284
285            if !t.items.is_empty() {
286                output.push_str("**Methods:**\n\n");
287                for method_id in &t.items {
288                    if let Some(method) = crate_data.index.get(method_id) {
289                        if let Some(method_name) = &method.name {
290                            output.push_str(&format!("- `{}`", method_name));
291                            if let Some(method_docs) = &method.docs {
292                                output.push_str(&format!(": {}", method_docs.lines().next().unwrap_or("")));
293                            }
294                            output.push_str("\n");
295                        }
296                    }
297                }
298                output.push_str("\n");
299            }
300        }
301        ItemEnum::Module(_) => {
302            output.push_str(&format!("## Module: {}\n\n", name));
303
304            if let Some(docs) = &item.docs {
305                output.push_str(&format!("{}\n\n", docs));
306            }
307        }
308        ItemEnum::Constant { .. } => {
309            output.push_str(&format!("## {}\n\n", name));
310            output.push_str("**Type:** Constant\n\n");
311
312            if let Some(docs) = &item.docs {
313                output.push_str(&format!("{}\n\n", docs));
314            }
315        }
316        ItemEnum::TypeAlias(_) => {
317            output.push_str(&format!("## {}\n\n", name));
318            output.push_str("**Type:** Type Alias\n\n");
319
320            if let Some(docs) = &item.docs {
321                output.push_str(&format!("{}\n\n", docs));
322            }
323        }
324        _ => {
325            return None;
326        }
327    }
328
329    Some(output)
330}
331
332fn format_generic_param(param: &rustdoc_types::GenericParamDef) -> String {
333    match &param.kind {
334        rustdoc_types::GenericParamDefKind::Lifetime { .. } => {
335            format!("'{}", param.name)
336        }
337        rustdoc_types::GenericParamDefKind::Type { .. } => {
338            param.name.clone()
339        }
340        rustdoc_types::GenericParamDefKind::Const { .. } => {
341            format!("const {}", param.name)
342        }
343    }
344}
345
346fn collect_impls_for_type<'a>(type_id: &rustdoc_types::Id, crate_data: &'a Crate) -> (Vec<&'a rustdoc_types::Impl>, Vec<&'a rustdoc_types::Impl>) {
347    use rustdoc_types::Type;
348
349    let mut inherent_impls = Vec::new();
350    let mut trait_impls = Vec::new();
351
352    for (_id, item) in &crate_data.index {
353        if let ItemEnum::Impl(impl_block) = &item.inner {
354            let matches = match &impl_block.for_ {
355                Type::ResolvedPath(path) => path.id == *type_id,
356                _ => false,
357            };
358
359            if matches {
360                if impl_block.trait_.is_some() {
361                    trait_impls.push(impl_block);
362                } else {
363                    inherent_impls.push(impl_block);
364                }
365            }
366        }
367    }
368
369    (inherent_impls, trait_impls)
370}
371
372fn format_impl_methods(impl_block: &rustdoc_types::Impl, crate_data: &Crate) -> String {
373    let mut output = String::new();
374
375    for method_id in &impl_block.items {
376        if let Some(method) = crate_data.index.get(method_id) {
377            if let ItemEnum::Function(f) = &method.inner {
378                if let Some(method_name) = &method.name {
379                    let sig = format_function_signature(method_name, f);
380                    let doc = if let Some(docs) = &method.docs {
381                        docs.lines().next().unwrap_or("")
382                    } else {
383                        ""
384                    };
385                    output.push_str(&format!("- `{}` - {}\n", sig, doc));
386                }
387            }
388        }
389    }
390
391    output
392}
393
394fn format_function_signature(name: &str, f: &rustdoc_types::Function) -> String {
395    let mut sig = format!("fn {}", name);
396
397    if !f.generics.params.is_empty() {
398        sig.push('<');
399        let params: Vec<String> = f.generics.params.iter()
400            .map(format_generic_param)
401            .collect();
402        sig.push_str(&params.join(", "));
403        sig.push('>');
404    }
405
406    sig.push('(');
407    let inputs: Vec<String> = f.sig.inputs.iter()
408        .map(|(name, ty)| format!("{}: {}", name, format_type(ty)))
409        .collect();
410    sig.push_str(&inputs.join(", "));
411    sig.push(')');
412
413    if let Some(output_type) = &f.sig.output {
414        sig.push_str(&format!(" -> {}", format_type(output_type)));
415    }
416
417    sig
418}
419
420fn format_type(ty: &rustdoc_types::Type) -> String {
421    use rustdoc_types::Type;
422    match ty {
423        Type::ResolvedPath(path) => path.path.clone(),
424        Type::DynTrait(dt) => {
425            if let Some(first) = dt.traits.first() {
426                format!("dyn {}", first.trait_.path)
427            } else {
428                "dyn Trait".to_string()
429            }
430        }
431        Type::Generic(name) => name.clone(),
432        Type::Primitive(name) => name.clone(),
433        Type::FunctionPointer(_) => "fn(...)".to_string(),
434        Type::Tuple(types) => {
435            let formatted: Vec<_> = types.iter().map(format_type).collect();
436            format!("({})", formatted.join(", "))
437        }
438        Type::Slice(inner) => format!("[{}]", format_type(inner)),
439        Type::Array { type_, len } => format!("[{}; {}]", format_type(type_), len),
440        Type::Pat { type_, .. } => format_type(type_),
441        Type::ImplTrait(_bounds) => "impl Trait".to_string(),
442        Type::Infer => "_".to_string(),
443        Type::RawPointer { is_mutable, type_ } => {
444            if *is_mutable {
445                format!("*mut {}", format_type(type_))
446            } else {
447                format!("*const {}", format_type(type_))
448            }
449        }
450        Type::BorrowedRef { lifetime, is_mutable, type_ } => {
451            let lifetime_str = lifetime.as_deref().unwrap_or("'_");
452            if *is_mutable {
453                format!("&{} mut {}", lifetime_str, format_type(type_))
454            } else {
455                format!("&{} {}", lifetime_str, format_type(type_))
456            }
457        }
458        Type::QualifiedPath { name, self_type, trait_, .. } => {
459            if let Some(trait_) = trait_ {
460                format!("<{} as {}>::{}", format_type(self_type), trait_.path, name)
461            } else {
462                format!("{}::{}", format_type(self_type), name)
463            }
464        }
465    }
466}