1use std::collections::HashSet;
8use std::fmt::Write;
9
10use rustdoc_types::{Id, Item, ItemEnum};
11
12use crate::generator::context::RenderContext;
13use crate::generator::items::ItemRenderer;
14use crate::generator::quick_ref::{QuickRefEntry, QuickRefGenerator, extract_summary};
15use crate::generator::toc::{TocEntry, TocGenerator};
16use crate::linker::AnchorUtils;
17
18pub struct ModuleRenderer<'a> {
29 ctx: &'a dyn RenderContext,
31
32 current_file: &'a str,
34
35 is_root: bool,
37}
38
39impl<'a> ModuleRenderer<'a> {
40 pub fn new(ctx: &'a dyn RenderContext, current_file: &'a str, is_root: bool) -> Self {
48 Self {
49 ctx,
50 current_file,
51 is_root,
52 }
53 }
54
55 fn process_docs(&self, item: &Item) -> Option<String> {
60 self.ctx.process_docs(item, self.current_file)
61 }
62
63 #[must_use]
87 pub fn render(&self, item: &Item) -> String {
88 let mut md = String::new();
89
90 let name = item.name.as_deref().unwrap_or("crate");
91
92 if self.is_root {
94 _ = write!(md, "# Crate `{name}`\n\n");
95 if let Some(version) = self.ctx.crate_version() {
96 _ = write!(md, "**Version:** {version}\n\n");
97 }
98 } else {
99 _ = write!(md, "# Module `{name}`\n\n");
100 }
101
102 if let Some(docs) = self.process_docs(item) {
104 _ = write!(md, "{}", &docs);
105 _ = write!(md, "\n\n");
106 }
107
108 if let ItemEnum::Module(module) = &item.inner {
110 let categorized = self.categorize_items(&module.items);
111 let config = self.ctx.render_config();
112
113 let toc_gen = TocGenerator::new(config.toc_threshold);
115 let toc_entries = Self::build_toc_entries(&categorized);
116 if let Some(toc) = toc_gen.generate(&toc_entries) {
117 _ = write!(md, "{}", &toc);
118 }
119
120 if config.quick_reference {
122 let quick_ref_entries = self.build_quick_ref_entries(&categorized);
123 if !quick_ref_entries.is_empty() {
124 let quick_ref_gen = QuickRefGenerator::new();
125 _ = write!(md, "{}", &quick_ref_gen.generate(&quick_ref_entries));
126 }
127 }
128
129 self.render_all_sections(&mut md, &categorized);
130 }
131
132 md
133 }
134
135 fn categorize_items(&self, item_ids: &'a [Id]) -> CategorizedItems<'a> {
145 let mut items = CategorizedItems::default();
146 let mut seen_items: HashSet<&Id> = HashSet::new();
147
148 for item_id in item_ids {
149 if !seen_items.insert(item_id) {
151 continue;
152 }
153
154 if let Some(child) = self.ctx.get_item(item_id) {
155 if !self.ctx.should_include_item(child) {
157 continue;
158 }
159
160 match &child.inner {
161 ItemEnum::Module(_) => items.modules.push((item_id, child)),
163
164 ItemEnum::Struct(_) => items.structs.push((item_id, child)),
166 ItemEnum::Enum(_) => items.enums.push((item_id, child)),
167 ItemEnum::Union(_) => items.unions.push((item_id, child)),
168 ItemEnum::TypeAlias(_) => items.type_aliases.push(child),
169
170 ItemEnum::Trait(_) => items.traits.push((item_id, child)),
172 ItemEnum::Function(_) => items.functions.push(child),
173 ItemEnum::Constant { .. } => items.constants.push(child),
174 ItemEnum::Static(_) => items.statics.push(child),
175 ItemEnum::Macro(_) => items.macros.push(child),
176
177 ItemEnum::Use(use_item) => {
179 if use_item.is_glob {
181 self.expand_glob_reexport(&mut items, use_item, &mut seen_items);
183 } else if let Some(target_id) = &use_item.id
184 && let Some(target_item) = self.ctx.get_item(target_id)
185 {
186 match &target_item.inner {
188 ItemEnum::Module(_) => items.modules.push((item_id, child)),
189
190 ItemEnum::Struct(_) => items.structs.push((item_id, child)),
191 ItemEnum::Enum(_) => items.enums.push((item_id, child)),
192 ItemEnum::Union(_) => items.unions.push((item_id, child)),
193 ItemEnum::TypeAlias(_) => items.type_aliases.push(child),
194
195 ItemEnum::Trait(_) => items.traits.push((item_id, child)),
196 ItemEnum::Function(_) => items.functions.push(child),
197 ItemEnum::Constant { .. } => items.constants.push(child),
198 ItemEnum::Static(_) => items.statics.push(child),
199 ItemEnum::Macro(_) => items.macros.push(child),
200 _ => {},
201 }
202 }
203 },
204
205 _ => {},
206 }
207 }
208 }
209
210 items.sort();
213
214 items
215 }
216
217 fn expand_glob_reexport(
219 &self,
220 items: &mut CategorizedItems<'a>,
221 use_item: &rustdoc_types::Use,
222 seen_items: &mut HashSet<&'a Id>,
223 ) {
224 let Some(target_id) = &use_item.id else {
226 return;
227 };
228
229 let Some(target_module) = self.ctx.get_item(target_id) else {
231 return;
232 };
233
234 let ItemEnum::Module(module) = &target_module.inner else {
236 return;
237 };
238
239 for child_id in &module.items {
241 if !seen_items.insert(child_id) {
243 continue;
244 }
245
246 let Some(child) = self.ctx.get_item(child_id) else {
247 continue;
248 };
249
250 if !self.ctx.should_include_item(child) {
252 continue;
253 }
254
255 match &child.inner {
257 ItemEnum::Module(_) => items.modules.push((child_id, child)),
258
259 ItemEnum::Struct(_) => items.structs.push((child_id, child)),
260 ItemEnum::Enum(_) => items.enums.push((child_id, child)),
261 ItemEnum::Union(_) => items.unions.push((child_id, child)),
262 ItemEnum::TypeAlias(_) => items.type_aliases.push(child),
263
264 ItemEnum::Trait(_) => items.traits.push((child_id, child)),
265 ItemEnum::Function(_) => items.functions.push(child),
266 ItemEnum::Constant { .. } => items.constants.push(child),
267 ItemEnum::Static(_) => items.statics.push(child),
268 ItemEnum::Macro(_) => items.macros.push(child),
269
270 _ => {},
271 }
272 }
273 }
274
275 fn render_all_sections(&self, md: &mut String, items: &CategorizedItems) {
289 let mut has_content = if items.modules.is_empty() {
293 false
294 } else {
295 self.render_modules_section(md, &items.modules);
296 true
297 };
298
299 if items.has_types() {
301 if has_content {
302 _ = writeln!(md, "\n---\n");
303 }
304
305 self.render_types_section(md, items);
306 has_content = true;
307 }
308
309 if !items.traits.is_empty() {
311 if has_content {
312 _ = writeln!(md, "\n---\n");
313 }
314
315 self.render_traits_section(md, &items.traits);
316 has_content = true;
317 }
318
319 if !items.functions.is_empty() {
321 if has_content {
322 _ = writeln!(md, "\n---\n");
323 }
324
325 self.render_functions_section(md, &items.functions);
326 has_content = true;
327 }
328
329 if !items.constants.is_empty() {
331 if has_content {
332 _ = writeln!(md, "\n---\n");
333 }
334
335 self.render_constants_section(md, &items.constants);
336 has_content = true;
337 }
338
339 if !items.statics.is_empty() {
341 if has_content {
342 _ = writeln!(md, "\n---\n");
343 }
344
345 self.render_statics_section(md, &items.statics);
346 has_content = true;
347 }
348
349 if !items.macros.is_empty() {
351 if has_content {
352 _ = writeln!(md, "\n---\n");
353 }
354
355 self.render_macros_section(md, &items.macros);
356 }
357 }
358
359 fn render_types_section(&self, md: &mut String, items: &CategorizedItems) {
380 _ = write!(md, "## Types\n\n");
381
382 let renderer = ItemRenderer::new(self.ctx, self.current_file);
383
384 for (item_id, struct_item) in &items.structs {
386 renderer.render_struct(md, **item_id, struct_item);
387 }
388
389 for (item_id, enum_item) in &items.enums {
391 renderer.render_enum(md, **item_id, enum_item);
392 }
393
394 for (item_id, union_item) in &items.unions {
396 renderer.render_union(md, **item_id, union_item);
397 }
398
399 for alias_item in &items.type_aliases {
401 renderer.render_type_alias(md, alias_item);
402 }
403 }
404
405 fn render_statics_section(&self, md: &mut String, statics: &[&Item]) {
407 if statics.is_empty() {
408 return;
409 }
410
411 _ = write!(md, "## Statics\n\n");
412
413 let renderer = ItemRenderer::new(self.ctx, self.current_file);
414
415 for static_item in statics {
416 renderer.render_static(md, static_item);
417 }
418 }
419
420 fn build_toc_entries(items: &CategorizedItems) -> Vec<TocEntry> {
431 let mut entries = Vec::new();
432
433 #[expect(clippy::items_after_statements, reason = "Helper function definition")]
435 fn item_entries(items: &[(&Id, &Item)]) -> Vec<TocEntry> {
436 items
437 .iter()
438 .filter_map(|(_, item)| {
439 let name = item.name.as_deref()?;
440 Some(TocEntry::new(
441 format!("`{name}`"),
442 AnchorUtils::slugify_anchor(name),
443 ))
444 })
445 .collect()
446 }
447
448 #[expect(clippy::items_after_statements, reason = "Helper function definition")]
450 fn simple_item_entries(items: &[&Item]) -> Vec<TocEntry> {
451 items
452 .iter()
453 .filter_map(|item| {
454 let name = item.name.as_deref()?;
455 Some(TocEntry::new(
456 format!("`{name}`"),
457 AnchorUtils::slugify_anchor(name),
458 ))
459 })
460 .collect()
461 }
462
463 if !items.modules.is_empty() {
465 entries.push(TocEntry::with_children(
466 "Modules",
467 "modules",
468 item_entries(&items.modules),
469 ));
470 }
471
472 if items.has_types() {
474 let mut type_children = Vec::new();
476
477 type_children.extend(item_entries(&items.structs));
479
480 type_children.extend(item_entries(&items.enums));
482
483 type_children.extend(item_entries(&items.unions));
485
486 type_children.extend(simple_item_entries(&items.type_aliases));
488
489 entries.push(TocEntry::with_children("Types", "types", type_children));
490 }
491
492 if !items.traits.is_empty() {
494 entries.push(TocEntry::with_children(
495 "Traits",
496 "traits",
497 item_entries(&items.traits),
498 ));
499 }
500
501 if !items.functions.is_empty() {
503 entries.push(TocEntry::with_children(
504 "Functions",
505 "functions",
506 simple_item_entries(&items.functions),
507 ));
508 }
509
510 if !items.constants.is_empty() {
512 entries.push(TocEntry::with_children(
513 "Constants",
514 "constants",
515 simple_item_entries(&items.constants),
516 ));
517 }
518
519 if !items.statics.is_empty() {
521 entries.push(TocEntry::with_children(
522 "Statics",
523 "statics",
524 simple_item_entries(&items.statics),
525 ));
526 }
527
528 if !items.macros.is_empty() {
530 entries.push(TocEntry::with_children(
531 "Macros",
532 "macros",
533 simple_item_entries(&items.macros),
534 ));
535 }
536
537 entries
538 }
539
540 fn build_quick_ref_entries(&self, items: &CategorizedItems) -> Vec<QuickRefEntry> {
546 let mut entries = Vec::new();
547
548 for (id, item) in &items.modules {
550 if let Some(name) = item.name.as_deref() {
551 entries.push(QuickRefEntry::new(
552 name,
553 "mod",
554 AnchorUtils::slugify_anchor(name),
555 self.get_item_summary(item, **id),
556 ));
557 }
558 }
559
560 for (id, item) in &items.structs {
561 if let Some(name) = item.name.as_deref() {
562 entries.push(QuickRefEntry::new(
563 name,
564 "struct",
565 AnchorUtils::slugify_anchor(name),
566 self.get_item_summary(item, **id),
567 ));
568 }
569 }
570
571 for (id, item) in &items.enums {
572 if let Some(name) = item.name.as_deref() {
573 entries.push(QuickRefEntry::new(
574 name,
575 "enum",
576 AnchorUtils::slugify_anchor(name),
577 self.get_item_summary(item, **id),
578 ));
579 }
580 }
581
582 for (id, item) in &items.traits {
583 if let Some(name) = item.name.as_deref() {
584 entries.push(QuickRefEntry::new(
585 name,
586 "trait",
587 AnchorUtils::slugify_anchor(name),
588 self.get_item_summary(item, **id),
589 ));
590 }
591 }
592
593 for item in &items.functions {
596 if let Some(name) = item.name.as_deref() {
597 entries.push(QuickRefEntry::new(
598 name,
599 "fn",
600 AnchorUtils::slugify_anchor(name),
601 extract_summary(item.docs.as_deref()),
602 ));
603 }
604 }
605
606 for item in &items.macros {
607 if let Some(name) = item.name.as_deref() {
608 entries.push(QuickRefEntry::new(
609 name,
610 "macro",
611 AnchorUtils::slugify_anchor(name),
612 extract_summary(item.docs.as_deref()),
613 ));
614 }
615 }
616
617 for item in &items.constants {
618 if let Some(name) = item.name.as_deref() {
619 entries.push(QuickRefEntry::new(
620 name,
621 "const",
622 AnchorUtils::slugify_anchor(name),
623 extract_summary(item.docs.as_deref()),
624 ));
625 }
626 }
627
628 for item in &items.type_aliases {
629 if let Some(name) = item.name.as_deref() {
630 entries.push(QuickRefEntry::new(
631 name,
632 "type",
633 AnchorUtils::slugify_anchor(name),
634 extract_summary(item.docs.as_deref()),
635 ));
636 }
637 }
638
639 for (id, item) in &items.unions {
641 if let Some(name) = item.name.as_deref() {
642 entries.push(QuickRefEntry::new(
643 name,
644 "union",
645 AnchorUtils::slugify_anchor(name),
646 self.get_item_summary(item, **id),
647 ));
648 }
649 }
650
651 for item in &items.statics {
653 if let Some(name) = item.name.as_deref() {
654 entries.push(QuickRefEntry::new(
655 name,
656 "static",
657 AnchorUtils::slugify_anchor(name),
658 extract_summary(item.docs.as_deref()),
659 ));
660 }
661 }
662
663 entries
664 }
665
666 fn get_item_summary(&self, item: &Item, item_id: Id) -> String {
671 let summary = extract_summary(item.docs.as_deref());
673 if !summary.is_empty() {
674 return summary;
675 }
676
677 if let ItemEnum::Use(use_item) = &item.inner
679 && let Some(target_id) = &use_item.id
680 && let Some(target_item) = self.ctx.krate().index.get(target_id)
681 {
682 let target_summary = extract_summary(target_item.docs.as_deref());
683 if !target_summary.is_empty() {
684 return target_summary;
685 }
686 }
687
688 if let Some(target_item) = self.ctx.krate().index.get(&item_id)
690 && target_item.id != item.id
691 {
692 let target_summary = extract_summary(target_item.docs.as_deref());
693 if !target_summary.is_empty() {
694 return target_summary;
695 }
696 }
697
698 String::new()
699 }
700
701 fn render_modules_section(&self, md: &mut String, modules: &[(&Id, &Item)]) {
703 if modules.is_empty() {
704 return;
705 }
706
707 _ = writeln!(md, "## Modules\n");
708
709 for (module_id, module_item) in modules {
710 let module_name = module_item.name.as_deref().unwrap_or("unnamed");
711
712 if let Some(link) = self.ctx.create_link(**module_id, self.current_file) {
713 _ = write!(md, "- {link}");
714 } else {
715 _ = write!(md, "- **`{module_name}`**");
716 }
717
718 let summary = self.get_module_summary(module_item, **module_id);
720
721 if !summary.is_empty() {
722 _ = write!(md, " — {summary}");
723 }
724
725 _ = writeln!(md);
726 }
727
728 md.push('\n');
729 }
730
731 fn get_module_summary(&self, item: &Item, item_id: Id) -> String {
733 if let Some(docs) = &item.docs
735 && let Some(first_line) = docs.lines().next()
736 && !first_line.trim().is_empty()
737 {
738 return first_line.to_string();
739 }
740
741 if let ItemEnum::Use(use_item) = &item.inner
743 && let Some(target_id) = &use_item.id
744 && let Some(target_item) = self.ctx.krate().index.get(target_id)
745 && let Some(docs) = &target_item.docs
746 && let Some(first_line) = docs.lines().next()
747 {
748 return first_line.to_string();
749 }
750
751 if let Some(target_item) = self.ctx.krate().index.get(&item_id)
753 && target_item.id != item.id
754 && let Some(docs) = &target_item.docs
755 && let Some(first_line) = docs.lines().next()
756 {
757 return first_line.to_string();
758 }
759
760 String::new()
761 }
762
763 fn render_traits_section(&self, md: &mut String, traits: &[(&Id, &Item)]) {
765 if traits.is_empty() {
766 return;
767 }
768
769 _ = writeln!(md, "## Traits\n");
770
771 let renderer = ItemRenderer::new(self.ctx, self.current_file);
772
773 for (item_id, trait_item) in traits {
774 renderer.render_trait(md, **item_id, trait_item);
775 }
776 }
777
778 fn render_functions_section(&self, md: &mut String, functions: &[&Item]) {
780 if functions.is_empty() {
781 return;
782 }
783
784 _ = writeln!(md, "## Functions\n");
785
786 let renderer = ItemRenderer::new(self.ctx, self.current_file);
787
788 for func_item in functions {
789 renderer.render_function(md, func_item);
790 }
791 }
792
793 fn render_macros_section(&self, md: &mut String, macros: &[&Item]) {
795 if macros.is_empty() {
796 return;
797 }
798
799 _ = writeln!(md, "## Macros\n");
800
801 let renderer = ItemRenderer::new(self.ctx, self.current_file);
802
803 for macro_item in macros {
804 renderer.render_macro(md, macro_item);
805 }
806 }
807
808 fn render_constants_section(&self, md: &mut String, constants: &[&Item]) {
810 if constants.is_empty() {
811 return;
812 }
813
814 _ = writeln!(md, "## Constants\n");
815
816 let renderer = ItemRenderer::new(self.ctx, self.current_file);
817
818 for const_item in constants {
819 renderer.render_constant(md, const_item);
820 }
821 }
822}
823
824#[derive(Default)]
837struct CategorizedItems<'a> {
838 modules: Vec<(&'a Id, &'a Item)>,
841
842 structs: Vec<(&'a Id, &'a Item)>,
846
847 enums: Vec<(&'a Id, &'a Item)>,
849
850 unions: Vec<(&'a Id, &'a Item)>,
852
853 type_aliases: Vec<&'a Item>,
855
856 traits: Vec<(&'a Id, &'a Item)>,
859
860 functions: Vec<&'a Item>,
862
863 constants: Vec<&'a Item>,
865
866 statics: Vec<&'a Item>,
868
869 macros: Vec<&'a Item>,
871}
872
873impl CategorizedItems<'_> {
874 pub const fn has_types(&self) -> bool {
878 !self.structs.is_empty()
879 || !self.enums.is_empty()
880 || !self.unions.is_empty()
881 || !self.type_aliases.is_empty()
882 }
883
884 fn sort(&mut self) {
889 fn item_name(item: &Item) -> &str {
891 item.name.as_deref().unwrap_or("")
892 }
893
894 self.modules
896 .sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
897 self.structs
898 .sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
899 self.enums
900 .sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
901 self.unions
902 .sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
903 self.traits
904 .sort_by(|a, b| item_name(a.1).cmp(item_name(b.1)));
905
906 self.type_aliases
908 .sort_by(|a, b| item_name(a).cmp(item_name(b)));
909 self.functions
910 .sort_by(|a, b| item_name(a).cmp(item_name(b)));
911 self.constants
912 .sort_by(|a, b| item_name(a).cmp(item_name(b)));
913 self.statics.sort_by(|a, b| item_name(a).cmp(item_name(b)));
914 self.macros.sort_by(|a, b| item_name(a).cmp(item_name(b)));
915 }
916}