rustdoc_json_to_markdown/
converter.rs1use anyhow::Result;
4use rustdoc_types::{Crate, Item, ItemEnum, Visibility};
5
6pub 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(¶ms.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 ¶m.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(¶ms.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}