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/// Represents the multi-file markdown output
8pub struct MarkdownOutput {
9    /// Crate name
10    pub crate_name: String,
11    /// Map of relative file path -> content
12    pub files: HashMap<String, String>,
13}
14
15/// Convert a rustdoc Crate to multi-file markdown format.
16pub 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    // Build a map of item_id -> full_path using the paths data
23    let item_paths = build_path_map(crate_data);
24
25    // Group items by module
26    let modules = group_by_module(crate_data, &item_paths, include_private);
27
28    let mut files = HashMap::new();
29
30    // Generate index.md with crate overview and module list
31    let index_content = generate_crate_index(crate_name, root_item, &modules);
32    files.insert("index.md".to_string(), index_content);
33
34    // Generate one file per module
35    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
52/// Convert a rustdoc Crate to markdown format (legacy single-file).
53pub 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    // Build a map of item_id -> full_path using the paths data
67    let item_paths = build_path_map(crate_data);
68
69    // Group items by module
70    let modules = group_by_module(crate_data, &item_paths, include_private);
71
72    // Generate hierarchical ToC
73    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    // Generate content organized by module
78    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        // Skip if we can't format this item type
108        if !can_format_item(item) {
109            continue;
110        }
111
112        // Get the module path (all elements except the last one)
113        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; // Skip root-level items without module
118            }
119        } else {
120            continue; // Skip items without path info
121        };
122
123        modules.entry(module_path)
124            .or_insert_with(Vec::new)
125            .push((id.clone(), item.clone()));
126    }
127
128    // Sort items within each module by name
129    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    // Sort modules alphabetically
153    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        // Get the last component of the module path for display
160        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    // Sort modules alphabetically
185    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        // Module header
192        output.push_str(&format!("# Module: `{}`\n\n", module_name));
193
194        // Generate content for each item in the module
195        for (id, item) in items {
196            if let Some(section) = format_item_with_path(id, item, crate_data, item_paths) {
197                output.push_str(&section);
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    // Replace the simple name header with the full path
220    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(&params.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 &param.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(&params.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    // Module listing with summary
654    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        // Count item types
668        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    // Breadcrumb
709    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    // Table of contents for this module
715    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    // Generate content for each item
757    for (id, item) in items {
758        if let Some(section) = format_item_with_path(id, item, crate_data, item_paths) {
759            output.push_str(&section);
760            output.push_str("\n\n");
761        }
762    }
763
764    output
765}