1use std::borrow::Cow;
11use std::fmt::Write;
12use std::path::Path;
13
14use rustdoc_types::{Crate, Id, Impl, Item, ItemEnum, Span, StructKind, VariantKind, Visibility};
15
16use crate::generator::context::RenderContext;
17use crate::linker::{AnchorUtils, AssocItemKind, ImplContext};
18use crate::types::TypeRenderer;
19
20#[derive(Debug, Clone)]
30pub struct SourcePathConfig {
31 pub source_dir_name: String,
33
34 pub depth: usize,
37}
38
39impl SourcePathConfig {
40 #[must_use]
47 pub fn new(source_dir: &Path, current_file: &str) -> Self {
48 let source_dir_name = source_dir
49 .file_name()
50 .and_then(|n| n.to_str())
51 .unwrap_or(".source")
52 .to_string();
53
54 let depth = current_file.matches('/').count();
57
58 Self {
59 source_dir_name,
60 depth,
61 }
62 }
63
64 #[must_use]
66 pub fn with_depth(&self, current_file: &str) -> Self {
67 Self {
68 source_dir_name: self.source_dir_name.clone(),
69 depth: current_file.matches('/').count(),
70 }
71 }
72}
73
74#[derive(Default)]
76pub struct CategorizedTraitItems<'a> {
77 pub required_methods: Vec<&'a Item>,
79
80 pub provided_methods: Vec<&'a Item>,
82
83 pub associated_types: Vec<&'a Item>,
85
86 pub associated_consts: Vec<&'a Item>,
88}
89
90impl<'a> CategorizedTraitItems<'a> {
91 #[must_use]
93 pub fn categorize_trait_items(trait_items: &[Id], krate: &'a Crate) -> Self {
94 let mut result = CategorizedTraitItems::default();
95
96 for item_id in trait_items {
97 let Some(item) = krate.index.get(item_id) else {
98 continue;
99 };
100
101 match &item.inner {
102 ItemEnum::Function(f) => {
103 if f.has_body {
104 result.provided_methods.push(item);
105 } else {
106 result.required_methods.push(item);
107 }
108 },
109
110 ItemEnum::AssocType { .. } => {
111 result.associated_types.push(item);
112 },
113
114 ItemEnum::AssocConst { .. } => {
115 result.associated_consts.push(item);
116 },
117
118 _ => {},
119 }
120 }
121
122 result
123 }
124}
125
126pub struct RendererUtils;
128
129impl RendererUtils {
130 #[must_use]
147 pub fn sanitize_path(path: &str) -> Cow<'_, str> {
148 if path.contains("$crate::") {
149 Cow::Owned(path.replace("$crate::", ""))
150 } else {
151 Cow::Borrowed(path)
152 }
153 }
154
155 #[must_use]
175 pub fn sanitize_self_param(param: &str) -> Cow<'_, str> {
176 match param {
177 "self: &Self" => Cow::Borrowed("&self"),
178
179 "self: &mut Self" => Cow::Borrowed("&mut self"),
180
181 "self: Self" => Cow::Borrowed("self"),
182
183 _ => Cow::Borrowed(param),
184 }
185 }
186
187 pub fn write_tuple_fields(
200 out: &mut String,
201 fields: &[Option<Id>],
202 krate: &Crate,
203 type_renderer: &TypeRenderer,
204 ) {
205 let mut first = true;
206 for id in fields.iter().filter_map(|id| id.as_ref()) {
207 if let Some(item) = krate.index.get(id)
208 && let ItemEnum::StructField(ty) = &item.inner
209 {
210 if !first {
211 _ = write!(out, ", ");
212 }
213
214 _ = write!(out, "{}", type_renderer.render_type(ty));
216 first = false;
217 }
218 }
219 }
220
221 #[must_use]
231 pub fn transform_cargo_path(absolute_path: &Path, source_dir_name: &str) -> Option<String> {
232 let path_str = absolute_path.to_str()?;
233
234 if let Some(registry_idx) = path_str.find(".cargo/registry/src/") {
237 let after_registry = &path_str[registry_idx + ".cargo/registry/src/".len()..];
239
240 if let Some(slash_idx) = after_registry.find('/') {
242 let crate_relative = &after_registry[slash_idx + 1..];
245 return Some(format!("{source_dir_name}/{crate_relative}"));
246 }
247 }
248
249 None
250 }
251}
252
253pub struct TraitRenderer;
255
256impl TraitRenderer {
257 fn write_trait_bounds(
268 out: &mut String,
269 bounds: &[rustdoc_types::GenericBound],
270 type_renderer: TypeRenderer,
271 ) {
272 if bounds.is_empty() {
273 return;
274 }
275
276 _ = write!(out, ": ");
277 let mut first = true;
278
279 for bound in bounds {
280 let rendered = type_renderer.render_generic_bound(bound);
281 if rendered.is_empty() {
283 continue;
284 }
285
286 if !first {
287 _ = write!(out, " + ");
288 }
289
290 _ = write!(out, "{}", &rendered);
291 first = false;
292 }
293 }
294
295 pub fn render_trait_definition(
307 md: &mut String,
308 name: &str,
309 t: &rustdoc_types::Trait,
310 type_renderer: &TypeRenderer,
311 ) {
312 let generics = type_renderer.render_generics(&t.generics.params);
313 let where_clause = type_renderer.render_where_clause(&t.generics.where_predicates);
314
315 _ = writeln!(md, "### `{name}{generics}`\n");
316
317 _ = writeln!(md, "```rust");
318
319 _ = write!(md, "trait {name}{generics}");
320 Self::write_trait_bounds(md, &t.bounds, *type_renderer);
321 _ = writeln!(md, "{where_clause} {{ ... }}");
322
323 _ = writeln!(md, "```\n");
324 }
325
326 pub fn render_trait_item<F>(
338 md: &mut String,
339 item: &Item,
340 type_renderer: &TypeRenderer,
341 process_docs: F,
342 ) where
343 F: Fn(&Item) -> Option<String>,
344 {
345 let name = item.name.as_deref().unwrap_or("_");
346
347 match &item.inner {
348 ItemEnum::Function(f) => {
349 let generics = type_renderer.render_generics(&f.generics.params);
350
351 let params: Vec<String> = f
352 .sig
353 .inputs
354 .iter()
355 .map(|(param_name, ty)| {
356 let raw = format!("{param_name}: {}", type_renderer.render_type(ty));
357
358 RendererUtils::sanitize_self_param(&raw).into_owned()
359 })
360 .collect();
361
362 let ret = f
363 .sig
364 .output
365 .as_ref()
366 .map(|ty| format!(" -> {}", type_renderer.render_type(ty)))
367 .unwrap_or_default();
368
369 _ = write!(
370 md,
371 "- `fn {}{}({}){}`",
372 name,
373 generics,
374 params.join(", "),
375 ret
376 );
377
378 if let Some(docs) = process_docs(item)
379 && let Some(first_line) = docs.lines().next()
380 {
381 _ = write!(md, "\n\n {first_line}");
382 }
383
384 _ = write!(md, "\n\n");
385 },
386
387 ItemEnum::AssocType { bounds, type_, .. } => {
388 let bounds_str = if bounds.is_empty() {
389 String::new()
390 } else {
391 format!(": {}", bounds.len())
392 };
393 let default_str = type_
394 .as_ref()
395 .map(|ty| format!(" = {}", type_renderer.render_type(ty)))
396 .unwrap_or_default();
397
398 _ = write!(md, "- `type {name}{bounds_str}{default_str}`\n\n");
399 },
400
401 ItemEnum::AssocConst { type_, .. } => {
402 _ = write!(
403 md,
404 "- `const {name}: {}`\n\n",
405 type_renderer.render_type(type_)
406 );
407 },
408
409 _ => {
410 _ = write!(md, "- `{name}`\n\n");
411 },
412 }
413 }
414}
415
416pub struct RendererInternals;
419
420impl RendererInternals {
421 pub fn render_struct_definition(
434 md: &mut String,
435 name: &str,
436 s: &rustdoc_types::Struct,
437 krate: &Crate,
438 type_renderer: &TypeRenderer,
439 ) {
440 let generics = type_renderer.render_generics(&s.generics.params);
441 let where_clause = type_renderer.render_where_clause(&s.generics.where_predicates);
442
443 _ = write!(md, "### `{name}{generics}`\n\n");
444
445 _ = writeln!(md, "```rust");
446 match &s.kind {
447 StructKind::Unit => {
448 _ = writeln!(md, "struct {name}{generics}{where_clause};");
449 },
450
451 StructKind::Tuple(fields) => {
452 _ = write!(md, "struct {name}{generics}(");
453 RendererUtils::write_tuple_fields(md, fields, krate, type_renderer);
454 _ = writeln!(md, "){where_clause};");
455 },
456
457 StructKind::Plain {
458 fields,
459 has_stripped_fields,
460 } => {
461 _ = writeln!(md, "struct {name}{generics}{where_clause} {{");
462
463 for field_id in fields {
464 if let Some(field) = krate.index.get(field_id) {
465 let field_name = field.name.as_deref().unwrap_or("_");
466
467 if let ItemEnum::StructField(ty) = &field.inner {
468 let vis = match &field.visibility {
469 Visibility::Public => "pub ",
470 _ => "",
471 };
472
473 _ = writeln!(
474 md,
475 " {}{}: {},",
476 vis,
477 field_name,
478 type_renderer.render_type(ty)
479 );
480 }
481 }
482 }
483
484 if *has_stripped_fields {
485 _ = writeln!(md, " // [REDACTED: Private Fields]");
486 }
487
488 _ = writeln!(md, "}}");
489 },
490 }
491
492 _ = writeln!(md, "```\n");
493 }
494
495 pub fn render_struct_fields<F>(
508 md: &mut String,
509 fields: &[Id],
510 krate: &Crate,
511 type_renderer: &TypeRenderer,
512 process_docs: F,
513 ) where
514 F: Fn(&Item) -> Option<String>,
515 {
516 let documented_fields: Vec<_> = fields
517 .iter()
518 .filter_map(|id| krate.index.get(id))
519 .filter(|f| f.docs.is_some())
520 .collect();
521
522 if !documented_fields.is_empty() {
523 _ = writeln!(md, "#### Fields\n");
524
525 for field in documented_fields {
526 let field_name = field.name.as_deref().unwrap_or("_");
527
528 if let ItemEnum::StructField(ty) = &field.inner {
529 _ = write!(
530 md,
531 "- **`{}`**: `{}`",
532 field_name,
533 type_renderer.render_type(ty)
534 );
535
536 if let Some(docs) = process_docs(field) {
537 _ = write!(md, "\n\n {}", docs.replace('\n', "\n "));
538 }
539
540 _ = writeln!(md, "\n");
541 }
542 }
543 }
544 }
545
546 pub fn render_enum_definition(
559 md: &mut String,
560 name: &str,
561 e: &rustdoc_types::Enum,
562 krate: &Crate,
563 type_renderer: &TypeRenderer,
564 ) {
565 let generics = type_renderer.render_generics(&e.generics.params);
566 let where_clause = type_renderer.render_where_clause(&e.generics.where_predicates);
567
568 _ = write!(md, "### `{name}{generics}`\n\n");
569
570 _ = writeln!(md, "```rust");
571 _ = writeln!(md, "enum {name}{generics}{where_clause} {{");
572
573 for variant_id in &e.variants {
574 if let Some(variant) = krate.index.get(variant_id) {
575 Self::render_enum_variant(md, variant, krate, type_renderer);
576 }
577 }
578
579 _ = writeln!(md, "}}");
580 _ = writeln!(md, "```\n");
581 }
582
583 pub fn render_enum_variant(
587 md: &mut String,
588 variant: &Item,
589 krate: &Crate,
590 type_renderer: &TypeRenderer,
591 ) {
592 let variant_name = variant.name.as_deref().unwrap_or("_");
593
594 if let ItemEnum::Variant(v) = &variant.inner {
595 match &v.kind {
596 VariantKind::Plain => {
597 _ = writeln!(md, " {variant_name},");
598 },
599
600 VariantKind::Tuple(fields) => {
601 _ = write!(md, " {variant_name}(");
602 RendererUtils::write_tuple_fields(md, fields, krate, type_renderer);
603 _ = writeln!(md, "),");
604 },
605
606 VariantKind::Struct { fields, .. } => {
607 _ = writeln!(md, " {variant_name} {{");
608
609 for field_id in fields {
610 if let Some(field) = krate.index.get(field_id) {
611 let field_name = field.name.as_deref().unwrap_or("_");
612
613 if let ItemEnum::StructField(ty) = &field.inner {
614 _ = writeln!(
615 md,
616 " {}: {},",
617 field_name,
618 type_renderer.render_type(ty)
619 );
620 }
621 }
622 }
623
624 _ = writeln!(md, " }},");
625 },
626 }
627 }
628 }
629
630 pub fn render_enum_variants_docs<F>(
641 md: &mut String,
642 variants: &[Id],
643 krate: &Crate,
644 process_docs: F,
645 ) where
646 F: Fn(&Item) -> Option<String>,
647 {
648 let documented_variants: Vec<_> = variants
649 .iter()
650 .filter_map(|id| krate.index.get(id))
651 .filter(|v| v.docs.is_some())
652 .collect();
653
654 if !documented_variants.is_empty() {
655 _ = writeln!(md, "#### Variants\n");
656
657 for variant in documented_variants {
658 let variant_name = variant.name.as_deref().unwrap_or("_");
659 _ = write!(md, "- **`{variant_name}`**");
660
661 if let Some(docs) = process_docs(variant) {
662 _ = write!(md, "\n\n {}", docs.replace('\n', "\n "));
663 }
664
665 _ = writeln!(md, "\n");
666 }
667 }
668 }
669
670 pub fn render_function_definition(
682 md: &mut String,
683 name: &str,
684 f: &rustdoc_types::Function,
685 type_renderer: &TypeRenderer,
686 ) {
687 let generics = type_renderer.render_generics(&f.generics.params);
688 let where_clause = type_renderer.render_where_clause(&f.generics.where_predicates);
689
690 let params: Vec<String> = f
691 .sig
692 .inputs
693 .iter()
694 .map(|(param_name, ty)| {
695 let raw = format!("{param_name}: {}", type_renderer.render_type(ty));
696
697 RendererUtils::sanitize_self_param(&raw).into_owned()
698 })
699 .collect();
700
701 let ret = f
702 .sig
703 .output
704 .as_ref()
705 .map(|ty| format!(" -> {}", type_renderer.render_type(ty)))
706 .unwrap_or_default();
707
708 let is_async = if f.header.is_async { "async " } else { "" };
709 let is_const = if f.header.is_const { "const " } else { "" };
710 let is_unsafe = if f.header.is_unsafe { "unsafe " } else { "" };
711
712 _ = writeln!(md, "### `{name}`\n");
713 _ = writeln!(md, "```rust");
714
715 _ = writeln!(
716 md,
717 "{}{}{}fn {}{}({}){}{}",
718 is_const,
719 is_async,
720 is_unsafe,
721 name,
722 generics,
723 params.join(", "),
724 ret,
725 where_clause
726 );
727
728 _ = writeln!(md, "```\n");
729 }
730
731 pub fn render_constant_definition(
744 md: &mut String,
745 name: &str,
746 type_: &rustdoc_types::Type,
747 const_: &rustdoc_types::Constant,
748 type_renderer: &TypeRenderer,
749 ) {
750 _ = writeln!(md, "### `{name}`");
751
752 _ = writeln!(md, "```rust");
753
754 let value = const_
755 .value
756 .as_ref()
757 .map(|v| format!(" = {v}"))
758 .unwrap_or_default();
759
760 _ = writeln!(
761 md,
762 "const {name}: {}{value};",
763 type_renderer.render_type(type_)
764 );
765
766 _ = writeln!(md, "```\n");
767 }
768
769 pub fn render_type_alias_definition(
781 md: &mut String,
782 name: &str,
783 ta: &rustdoc_types::TypeAlias,
784 type_renderer: &TypeRenderer,
785 ) {
786 let generics = type_renderer.render_generics(&ta.generics.params);
787 let where_clause = type_renderer.render_where_clause(&ta.generics.where_predicates);
788
789 _ = write!(md, "### `{name}{generics}`\n\n");
790 _ = writeln!(md, "```rust");
791
792 _ = writeln!(
793 md,
794 "type {name}{generics}{where_clause} = {};",
795 type_renderer.render_type(&ta.type_)
796 );
797
798 _ = writeln!(md, "```\n");
799 }
800
801 pub fn render_macro_heading(md: &mut String, name: &str) {
811 _ = write!(md, "### `{name}!`\n\n");
812 }
813
814 #[expect(
830 clippy::too_many_arguments,
831 reason = "Internal helper with documented params"
832 )]
833 pub fn render_impl_items<F, L>(
834 md: &mut String,
835 impl_block: &Impl,
836 krate: &Crate,
837 type_renderer: &TypeRenderer,
838 process_docs: &Option<F>,
839 create_type_link: &Option<L>,
840 parent_type_name: Option<&str>,
841 impl_ctx: ImplContext<'_>,
842 ) where
843 F: Fn(&Item) -> Option<String>,
844 L: Fn(rustdoc_types::Id) -> Option<String>,
845 {
846 for item_id in &impl_block.items {
847 if let Some(item) = krate.index.get(item_id) {
848 let name = item.name.as_deref().unwrap_or("_");
849
850 match &item.inner {
851 ItemEnum::Function(f) => {
852 Self::render_impl_function(md, name, f, *type_renderer, parent_type_name);
853
854 if let Some(link_creator) = create_type_link {
856 Self::render_function_type_links_inline(
857 md,
858 f,
859 *type_renderer,
860 link_creator,
861 );
862 }
863
864 if let Some(pf) = process_docs
866 && let Some(docs) = pf(item)
867 && let Some(first_line) = docs.lines().next()
868 {
869 _ = write!(md, "\n\n {first_line}");
870 }
871
872 _ = writeln!(md, "\n");
873 },
874
875 ItemEnum::AssocConst { type_, .. } => {
876 if let Some(type_name) = parent_type_name {
878 let anchor = AnchorUtils::impl_item_anchor(
879 type_name,
880 name,
881 AssocItemKind::Const,
882 impl_ctx,
883 );
884 _ = writeln!(
885 md,
886 "- <span id=\"{anchor}\"></span>`const {name}: {}`\n",
887 type_renderer.render_type(type_)
888 );
889 } else {
890 _ = writeln!(
891 md,
892 "- `const {name}: {}`\n",
893 type_renderer.render_type(type_)
894 );
895 }
896 },
897
898 ItemEnum::AssocType { type_, .. } => {
899 let anchor_prefix = parent_type_name
902 .map(|tn| {
903 format!(
904 "<span id=\"{}\"></span>",
905 AnchorUtils::impl_item_anchor(
906 tn,
907 name,
908 AssocItemKind::Type,
909 impl_ctx
910 )
911 )
912 })
913 .unwrap_or_default();
914
915 if let Some(ty) = type_ {
916 _ = writeln!(
917 md,
918 "- {anchor_prefix}`type {name} = {}`\n",
919 type_renderer.render_type(ty)
920 );
921 } else {
922 _ = writeln!(md, "- {anchor_prefix}`type {name}`\n");
923 }
924 },
925
926 _ => {},
927 }
928 }
929 }
930 }
931
932 fn render_function_type_links_inline<L>(
937 md: &mut String,
938 f: &rustdoc_types::Function,
939 type_renderer: TypeRenderer,
940 create_link: &L,
941 ) where
942 L: Fn(rustdoc_types::Id) -> Option<String>,
943 {
944 use std::collections::HashSet;
945
946 let mut all_types = Vec::new();
947
948 for (_, ty) in &f.sig.inputs {
950 all_types.extend(type_renderer.collect_linkable_types(ty));
951 }
952
953 if let Some(output) = &f.sig.output {
955 all_types.extend(type_renderer.collect_linkable_types(output));
956 }
957
958 let mut seen = HashSet::new();
960 let unique_types: Vec<_> = all_types
961 .into_iter()
962 .filter(|(name, _)| seen.insert(name.clone()))
963 .collect();
964
965 let links: Vec<String> = unique_types
967 .iter()
968 .filter_map(|(_, id)| create_link(*id))
969 .collect();
970
971 if !links.is_empty() {
973 _ = write!(md, " — {}", links.join(", "));
974 }
975 }
976
977 fn render_impl_function(
982 md: &mut String,
983 name: &str,
984 f: &rustdoc_types::Function,
985 type_renderer: TypeRenderer,
986 parent_type_name: Option<&str>,
987 ) {
988 let generics = type_renderer.render_generics(&f.generics.params);
989
990 let params: Vec<String> = f
991 .sig
992 .inputs
993 .iter()
994 .map(|(param_name, ty)| {
995 let raw = format!("{param_name}: {}", type_renderer.render_type(ty));
996
997 RendererUtils::sanitize_self_param(&raw).into_owned()
998 })
999 .collect();
1000
1001 let ret = f
1002 .sig
1003 .output
1004 .as_ref()
1005 .map(|ty| format!(" -> {}", type_renderer.render_type(ty)))
1006 .unwrap_or_default();
1007
1008 let is_async = if f.header.is_async { "async " } else { "" };
1009 let is_const = if f.header.is_const { "const " } else { "" };
1010 let is_unsafe = if f.header.is_unsafe { "unsafe " } else { "" };
1011
1012 let anchor_span = parent_type_name
1014 .map(|tn| {
1015 format!(
1016 "<span id=\"{}\"></span>",
1017 AnchorUtils::method_anchor(tn, name)
1018 )
1019 })
1020 .unwrap_or_default();
1021
1022 _ = write!(
1023 md,
1024 "- {anchor_span}`{}{}{}fn {}{}({}){}`",
1025 is_const,
1026 is_async,
1027 is_unsafe,
1028 name,
1029 generics,
1030 params.join(", "),
1031 ret
1032 );
1033 }
1034
1035 pub fn append_docs(md: &mut String, docs: Option<String>) {
1039 if let Some(docs) = docs {
1040 _ = write!(md, "{}", &docs);
1041 _ = writeln!(md, "\n");
1042 }
1043 }
1044
1045 #[must_use]
1064 pub fn render_collapsible_start(summary: &str) -> String {
1065 format!("<details>\n<summary>{summary}</summary>\n\n")
1066 }
1067
1068 #[must_use]
1080 pub const fn render_collapsible_end() -> &'static str {
1081 "\n</details>\n\n"
1082 }
1083
1084 #[must_use]
1088 pub fn impl_sort_key(impl_block: &Impl, type_renderer: &TypeRenderer) -> String {
1089 let trait_name = impl_block
1090 .trait_
1091 .as_ref()
1092 .map(|t| t.path.clone())
1093 .unwrap_or_default();
1094 let for_type = type_renderer.render_type(&impl_block.for_);
1095 let generics = type_renderer.render_generics(&impl_block.generics.params);
1096
1097 format!("{trait_name}{generics}::{for_type}")
1098 }
1099
1100 #[must_use]
1127 pub fn render_source_location(
1128 span: Option<&Span>,
1129 source_path_config: Option<&SourcePathConfig>,
1130 ) -> String {
1131 let Some(span) = span else {
1132 return String::new();
1133 };
1134
1135 let (start_line, _) = span.begin;
1136 let (end_line, _) = span.end;
1137
1138 let line_ref = if start_line == end_line {
1140 format!("{start_line}")
1141 } else {
1142 format!("{start_line}-{end_line}")
1143 };
1144
1145 if let Some(config) = source_path_config
1147 && let Some(relative_path) =
1148 RendererUtils::transform_cargo_path(&span.filename, &config.source_dir_name)
1149 {
1150 let prefix = "../".repeat(config.depth + 1);
1153
1154 let fragment = if start_line == end_line {
1156 format!("#L{start_line}")
1157 } else {
1158 format!("#L{start_line}-L{end_line}")
1159 };
1160
1161 let display_path = relative_path
1163 .strip_prefix(&config.source_dir_name)
1164 .map_or(relative_path.as_str(), |p| p.trim_start_matches('/'));
1165
1166 return format!(
1167 "*Defined in [`{display_path}:{line_ref}`]({prefix}{relative_path}{fragment})*\n\n"
1168 );
1169 }
1170
1171 let filename = span.filename.display();
1173 format!("*Defined in `{filename}:{line_ref}`*\n\n")
1174 }
1175
1176 pub fn render_union_definition(
1189 md: &mut String,
1190 name: &str,
1191 u: &rustdoc_types::Union,
1192 krate: &Crate,
1193 type_renderer: &TypeRenderer,
1194 ) {
1195 let generics = type_renderer.render_generics(&u.generics.params);
1196 let where_clause = type_renderer.render_where_clause(&u.generics.where_predicates);
1197
1198 _ = writeln!(md, "### `{name}{generics}`\n");
1199
1200 _ = writeln!(md, "```rust");
1201 _ = writeln!(md, "union {name}{generics}{where_clause} {{");
1202
1203 for field_id in &u.fields {
1204 if let Some(field) = krate.index.get(field_id) {
1205 let field_name = field.name.as_deref().unwrap_or("_");
1206
1207 if let ItemEnum::StructField(ty) = &field.inner {
1208 let vis = match &field.visibility {
1209 Visibility::Public => "pub ",
1210 _ => "",
1211 };
1212
1213 _ = writeln!(
1214 md,
1215 " {}{}: {},",
1216 vis,
1217 field_name,
1218 type_renderer.render_type(ty)
1219 );
1220 }
1221 }
1222 }
1223
1224 if u.has_stripped_fields {
1225 _ = writeln!(md, " // some fields omitted");
1226 }
1227
1228 _ = writeln!(md, "}}\n```\n");
1229 }
1230
1231 pub fn render_union_fields<F>(
1244 md: &mut String,
1245 fields: &[Id],
1246 krate: &Crate,
1247 type_renderer: &TypeRenderer,
1248 process_docs: F,
1249 ) where
1250 F: Fn(&Item) -> Option<String>,
1251 {
1252 let has_documented_fields = fields
1254 .iter()
1255 .any(|id| krate.index.get(id).is_some_and(|item| item.docs.is_some()));
1256
1257 if !has_documented_fields {
1258 return;
1259 }
1260
1261 _ = write!(md, "#### Fields\n\n");
1262
1263 for field_id in fields {
1264 let Some(field) = krate.index.get(field_id) else {
1265 continue;
1266 };
1267
1268 let field_name = field.name.as_deref().unwrap_or("_");
1269
1270 if let ItemEnum::StructField(ty) = &field.inner {
1271 let type_str = type_renderer.render_type(ty);
1272 _ = writeln!(md, "- **`{field_name}`**: `{type_str}`");
1273
1274 if let Some(docs) = process_docs(field) {
1275 for line in docs.lines() {
1277 if line.is_empty() {
1278 md.push('\n');
1279 } else {
1280 _ = writeln!(md, " {line}");
1281 }
1282 }
1283
1284 _ = writeln!(md);
1285 }
1286 }
1287 }
1288 }
1289
1290 pub fn render_static_definition(
1302 md: &mut String,
1303 name: &str,
1304 s: &rustdoc_types::Static,
1305 type_renderer: &TypeRenderer,
1306 ) {
1307 _ = write!(md, "### `{name}`\n\n");
1308
1309 _ = writeln!(md, "```rust");
1310
1311 let mut decl = String::new();
1313
1314 if s.is_unsafe {
1316 _ = write!(decl, "unsafe ");
1317 }
1318
1319 _ = write!(decl, "static ");
1320
1321 if s.is_mutable {
1323 _ = write!(decl, "mut ");
1324 }
1325
1326 _ = write!(decl, "{name}: {}", type_renderer.render_type(&s.type_));
1328
1329 if !s.expr.is_empty() {
1331 _ = write!(decl, " = {}", s.expr);
1332 }
1333
1334 _ = write!(decl, ";");
1335
1336 _ = writeln!(md, "{decl}");
1337 _ = write!(md, "```\n\n");
1338 }
1339}
1340pub trait DocsProcessor {
1344 fn process_item_docs(&self, item: &Item) -> Option<String>;
1346}
1347
1348impl<T: RenderContext + ?Sized> DocsProcessor for (&T, &str) {
1349 fn process_item_docs(&self, item: &Item) -> Option<String> {
1350 self.0.process_docs(item, self.1)
1351 }
1352}
1353
1354#[cfg(test)]
1355mod tests {
1356 use super::*;
1357
1358 mod sanitize_path_tests {
1359 use super::*;
1360
1361 #[test]
1362 fn removes_crate_prefix() {
1363 assert_eq!(
1364 RendererUtils::sanitize_path("$crate::clone::Clone"),
1365 "clone::Clone"
1366 );
1367 }
1368
1369 #[test]
1370 fn removes_multiple_crate_prefixes() {
1371 assert_eq!(
1372 RendererUtils::sanitize_path("$crate::foo::$crate::bar::Baz"),
1373 "foo::bar::Baz"
1374 );
1375 }
1376
1377 #[test]
1378 fn preserves_normal_paths() {
1379 assert_eq!(
1380 RendererUtils::sanitize_path("std::fmt::Debug"),
1381 "std::fmt::Debug"
1382 );
1383 }
1384
1385 #[test]
1386 fn preserves_simple_names() {
1387 assert_eq!(RendererUtils::sanitize_path("Clone"), "Clone");
1388 }
1389
1390 #[test]
1391 fn handles_empty_string() {
1392 assert_eq!(RendererUtils::sanitize_path(""), "");
1393 }
1394
1395 #[test]
1396 fn returns_borrowed_when_no_change() {
1397 let result = RendererUtils::sanitize_path("std::fmt::Debug");
1398 assert!(matches!(result, Cow::Borrowed(_)));
1399 }
1400
1401 #[test]
1402 fn returns_owned_when_changed() {
1403 let result = RendererUtils::sanitize_path("$crate::Clone");
1404 assert!(matches!(result, Cow::Owned(_)));
1405 }
1406 }
1407
1408 mod sanitize_self_param_tests {
1409 use super::*;
1410
1411 #[test]
1412 fn converts_ref_self() {
1413 assert_eq!(RendererUtils::sanitize_self_param("self: &Self"), "&self");
1414 }
1415
1416 #[test]
1417 fn converts_mut_ref_self() {
1418 assert_eq!(
1419 RendererUtils::sanitize_self_param("self: &mut Self"),
1420 "&mut self"
1421 );
1422 }
1423
1424 #[test]
1425 fn converts_owned_self() {
1426 assert_eq!(RendererUtils::sanitize_self_param("self: Self"), "self");
1427 }
1428
1429 #[test]
1430 fn preserves_regular_params() {
1431 assert_eq!(RendererUtils::sanitize_self_param("x: i32"), "x: i32");
1432 }
1433
1434 #[test]
1435 fn preserves_complex_types() {
1436 assert_eq!(
1437 RendererUtils::sanitize_self_param("callback: impl Fn()"),
1438 "callback: impl Fn()"
1439 );
1440 }
1441
1442 #[test]
1443 fn returns_borrowed_for_all_cases() {
1444 assert!(matches!(
1446 RendererUtils::sanitize_self_param("self: &Self"),
1447 Cow::Borrowed(_)
1448 ));
1449 assert!(matches!(
1450 RendererUtils::sanitize_self_param("self: &mut Self"),
1451 Cow::Borrowed(_)
1452 ));
1453 assert!(matches!(
1454 RendererUtils::sanitize_self_param("self: Self"),
1455 Cow::Borrowed(_)
1456 ));
1457 assert!(matches!(
1458 RendererUtils::sanitize_self_param("x: i32"),
1459 Cow::Borrowed(_)
1460 ));
1461 }
1462 }
1463
1464 mod collapsible_tests {
1465 use super::RendererInternals;
1466
1467 #[test]
1468 fn start_contains_details_tag() {
1469 let result = RendererInternals::render_collapsible_start("Test Summary");
1470 assert!(result.contains("<details>"));
1471 }
1472
1473 #[test]
1474 fn start_contains_summary_with_text() {
1475 let result =
1476 RendererInternals::render_collapsible_start("Derived Traits (9 implementations)");
1477 assert!(result.contains("<summary>Derived Traits (9 implementations)</summary>"));
1478 }
1479
1480 #[test]
1481 fn start_has_proper_formatting() {
1482 let result = RendererInternals::render_collapsible_start("Test");
1483 assert_eq!(result, "<details>\n<summary>Test</summary>\n\n");
1484 }
1485
1486 #[test]
1487 fn end_closes_details_tag() {
1488 let result = RendererInternals::render_collapsible_end();
1489 assert!(result.contains("</details>"));
1490 }
1491
1492 #[test]
1493 fn end_has_proper_formatting() {
1494 assert_eq!(
1495 RendererInternals::render_collapsible_end(),
1496 "\n</details>\n\n"
1497 );
1498 }
1499
1500 #[test]
1501 fn start_and_end_pair_correctly() {
1502 let start = RendererInternals::render_collapsible_start("Content");
1503 let end = RendererInternals::render_collapsible_end();
1504 let full = format!("{start}Some markdown content here{end}");
1505
1506 assert!(full.starts_with("<details>"));
1507 assert!(full.ends_with("</details>\n\n"));
1508 assert!(full.contains("<summary>Content</summary>"));
1509 }
1510 }
1511
1512 mod source_location_tests {
1513 use std::path::PathBuf;
1514
1515 use super::*;
1516
1517 #[test]
1518 fn transform_cargo_path_extracts_crate_relative() {
1519 let path = PathBuf::from(
1520 "/home/user/.cargo/registry/src/index.crates.io-xxx/serde-1.0.228/src/lib.rs",
1521 );
1522 let result = RendererUtils::transform_cargo_path(&path, ".source_12345");
1523 assert_eq!(
1524 result,
1525 Some(".source_12345/serde-1.0.228/src/lib.rs".to_string())
1526 );
1527 }
1528
1529 #[test]
1530 fn transform_cargo_path_handles_nested_paths() {
1531 let path = PathBuf::from(
1532 "/home/user/.cargo/registry/src/index.crates.io-abc/tokio-1.0.0/src/runtime/mod.rs",
1533 );
1534 let result = RendererUtils::transform_cargo_path(&path, ".source_99999");
1535 assert_eq!(
1536 result,
1537 Some(".source_99999/tokio-1.0.0/src/runtime/mod.rs".to_string())
1538 );
1539 }
1540
1541 #[test]
1542 fn transform_cargo_path_returns_none_for_non_cargo_path() {
1543 let path = PathBuf::from("/usr/local/src/myproject/lib.rs");
1544 let result = RendererUtils::transform_cargo_path(&path, ".source_12345");
1545 assert_eq!(result, None);
1546 }
1547
1548 #[test]
1549 fn transform_cargo_path_returns_none_for_local_path() {
1550 let path = PathBuf::from("src/lib.rs");
1551 let result = RendererUtils::transform_cargo_path(&path, ".source_12345");
1552 assert_eq!(result, None);
1553 }
1554
1555 #[test]
1556 fn source_path_config_calculates_depth() {
1557 let source_dir = PathBuf::from("/project/.source_12345");
1558
1559 let config = SourcePathConfig::new(&source_dir, "index.md");
1560 assert_eq!(config.depth, 0);
1561
1562 let config = SourcePathConfig::new(&source_dir, "serde/index.md");
1563 assert_eq!(config.depth, 1);
1564
1565 let config = SourcePathConfig::new(&source_dir, "serde/de/visitor/index.md");
1566 assert_eq!(config.depth, 3);
1567 }
1568
1569 #[test]
1570 fn source_path_config_extracts_dir_name() {
1571 let source_dir = PathBuf::from("/project/.source_1733660400");
1572 let config = SourcePathConfig::new(&source_dir, "index.md");
1573 assert_eq!(config.source_dir_name, ".source_1733660400");
1574 }
1575
1576 #[test]
1577 fn source_path_config_with_depth_preserves_name() {
1578 let source_dir = PathBuf::from("/project/.source_12345");
1579 let base_config = SourcePathConfig::new(&source_dir, "");
1580 let file_config = base_config.with_depth("crate/module/index.md");
1581
1582 assert_eq!(file_config.source_dir_name, ".source_12345");
1583 assert_eq!(file_config.depth, 2);
1584 }
1585
1586 #[test]
1587 fn render_source_location_without_config_shows_absolute_path() {
1588 let span = rustdoc_types::Span {
1589 filename: PathBuf::from(
1590 "/home/user/.cargo/registry/src/index/serde-1.0/src/lib.rs",
1591 ),
1592 begin: (10, 0),
1593 end: (25, 0),
1594 };
1595 let result = RendererInternals::render_source_location(Some(&span), None);
1596 assert!(result.contains("/home/user/.cargo/registry/src/index/serde-1.0/src/lib.rs"));
1597 assert!(result.contains("10-25"));
1598 assert!(!result.contains('['));
1600 }
1601
1602 #[test]
1603 fn render_source_location_with_config_creates_link() {
1604 let span = rustdoc_types::Span {
1605 filename: PathBuf::from(
1606 "/home/user/.cargo/registry/src/index.crates.io-xxx/serde-1.0.228/src/lib.rs",
1607 ),
1608 begin: (10, 0),
1609 end: (25, 0),
1610 };
1611 let config = SourcePathConfig {
1612 source_dir_name: ".source_12345".to_string(),
1613 depth: 1, };
1615 let result = RendererInternals::render_source_location(Some(&span), Some(&config));
1616
1617 assert!(result.contains('['));
1619 assert!(result.contains("]("));
1620 assert!(result.contains("../../.source_12345/serde-1.0.228/src/lib.rs"));
1622 assert!(result.contains("#L10-L25"));
1624 assert!(result.contains("[`serde-1.0.228/src/lib.rs:10-25`]"));
1626 }
1627
1628 #[test]
1629 fn render_source_location_single_line() {
1630 let span = rustdoc_types::Span {
1631 filename: PathBuf::from(
1632 "/home/user/.cargo/registry/src/index.crates.io-xxx/foo-1.0.0/src/lib.rs",
1633 ),
1634 begin: (42, 0),
1635 end: (42, 0),
1636 };
1637 let config = SourcePathConfig {
1638 source_dir_name: ".source_99999".to_string(),
1639 depth: 0,
1640 };
1641 let result = RendererInternals::render_source_location(Some(&span), Some(&config));
1642
1643 assert!(result.contains(":42`]"));
1645 assert!(result.contains("#L42)"));
1646 assert!(!result.contains("-L"));
1648 }
1649
1650 #[test]
1651 fn render_source_location_none_span_returns_empty() {
1652 let config = SourcePathConfig {
1653 source_dir_name: ".source_12345".to_string(),
1654 depth: 0,
1655 };
1656 let result = RendererInternals::render_source_location(None, Some(&config));
1657 assert!(result.is_empty());
1658 }
1659 }
1660
1661 mod categorized_trait_items_tests {
1662 use std::collections::HashMap;
1663
1664 use rustdoc_types::{Abi, Crate, Function, FunctionHeader, FunctionSignature, Target};
1665
1666 use super::*;
1667
1668 fn make_test_crate(items: Vec<(Id, Item)>) -> Crate {
1669 let mut index: HashMap<Id, Item> = HashMap::new();
1670 for (id, item) in items {
1671 index.insert(id, item);
1672 }
1673
1674 Crate {
1675 root: Id(0),
1676 crate_version: None,
1677 includes_private: false,
1678 index,
1679 paths: HashMap::new(),
1680 external_crates: HashMap::new(),
1681 format_version: 0,
1682 target: Target {
1683 triple: String::new(),
1684 target_features: vec![],
1685 },
1686 }
1687 }
1688
1689 fn make_function_item(name: &str, has_body: bool) -> Item {
1690 Item {
1691 id: Id(0),
1692 crate_id: 0,
1693 name: Some(name.to_string()),
1694 attrs: vec![],
1695 visibility: Visibility::Public,
1696 inner: ItemEnum::Function(Function {
1697 sig: FunctionSignature {
1698 inputs: vec![],
1699 output: None,
1700 is_c_variadic: false,
1701 },
1702 generics: rustdoc_types::Generics {
1703 params: vec![],
1704 where_predicates: vec![],
1705 },
1706 header: FunctionHeader {
1707 is_const: false,
1708 is_async: false,
1709 is_unsafe: false,
1710 abi: Abi::Rust,
1711 },
1712 has_body,
1713 }),
1714 deprecation: None,
1715 docs: None,
1716 span: None,
1717 links: HashMap::new(),
1718 }
1719 }
1720
1721 fn make_assoc_type_item(name: &str) -> Item {
1722 Item {
1723 id: Id(0),
1724 crate_id: 0,
1725 name: Some(name.to_string()),
1726 attrs: vec![],
1727 visibility: Visibility::Public,
1728 inner: ItemEnum::AssocType {
1729 generics: rustdoc_types::Generics {
1730 params: vec![],
1731 where_predicates: vec![],
1732 },
1733 bounds: vec![],
1734 type_: None,
1735 },
1736 deprecation: None,
1737 docs: None,
1738 span: None,
1739 links: HashMap::new(),
1740 }
1741 }
1742
1743 fn make_assoc_const_item(name: &str) -> Item {
1744 Item {
1745 id: Id(0),
1746 crate_id: 0,
1747 name: Some(name.to_string()),
1748 attrs: vec![],
1749 visibility: Visibility::Public,
1750 inner: ItemEnum::AssocConst {
1751 type_: rustdoc_types::Type::Primitive("i32".to_string()),
1752 value: Some("42".to_string()),
1753 },
1754 deprecation: None,
1755 docs: None,
1756 span: None,
1757 links: HashMap::new(),
1758 }
1759 }
1760
1761 #[test]
1762 fn empty_trait_items() {
1763 let krate = make_test_crate(vec![]);
1764 let result = CategorizedTraitItems::categorize_trait_items(&[], &krate);
1765
1766 assert!(result.required_methods.is_empty());
1767 assert!(result.provided_methods.is_empty());
1768 assert!(result.associated_types.is_empty());
1769 assert!(result.associated_consts.is_empty());
1770 }
1771
1772 #[test]
1773 fn categorizes_required_method() {
1774 let id = Id(1);
1775 let item = make_function_item("required_fn", false);
1776 let krate = make_test_crate(vec![(id, item)]);
1777
1778 let result = CategorizedTraitItems::categorize_trait_items(&[id], &krate);
1779
1780 assert_eq!(result.required_methods.len(), 1);
1781 assert_eq!(
1782 result.required_methods[0].name.as_deref(),
1783 Some("required_fn")
1784 );
1785 assert!(result.provided_methods.is_empty());
1786 }
1787
1788 #[test]
1789 fn categorizes_provided_method() {
1790 let id = Id(1);
1791 let item = make_function_item("provided_fn", true);
1792 let krate = make_test_crate(vec![(id, item)]);
1793
1794 let result = CategorizedTraitItems::categorize_trait_items(&[id], &krate);
1795
1796 assert!(result.required_methods.is_empty());
1797 assert_eq!(result.provided_methods.len(), 1);
1798 assert_eq!(
1799 result.provided_methods[0].name.as_deref(),
1800 Some("provided_fn")
1801 );
1802 }
1803
1804 #[test]
1805 fn categorizes_associated_type() {
1806 let id = Id(1);
1807 let item = make_assoc_type_item("Item");
1808 let krate = make_test_crate(vec![(id, item)]);
1809
1810 let result = CategorizedTraitItems::categorize_trait_items(&[id], &krate);
1811
1812 assert_eq!(result.associated_types.len(), 1);
1813 assert_eq!(result.associated_types[0].name.as_deref(), Some("Item"));
1814 }
1815
1816 #[test]
1817 fn categorizes_associated_const() {
1818 let id = Id(1);
1819 let item = make_assoc_const_item("CONST");
1820 let krate = make_test_crate(vec![(id, item)]);
1821
1822 let result = CategorizedTraitItems::categorize_trait_items(&[id], &krate);
1823
1824 assert_eq!(result.associated_consts.len(), 1);
1825 assert_eq!(result.associated_consts[0].name.as_deref(), Some("CONST"));
1826 }
1827
1828 #[test]
1829 fn categorizes_mixed_items() {
1830 let req_id = Id(1);
1831 let prov_id = Id(2);
1832 let type_id = Id(3);
1833 let const_id = Id(4);
1834
1835 let krate = make_test_crate(vec![
1836 (req_id, make_function_item("req", false)),
1837 (prov_id, make_function_item("prov", true)),
1838 (type_id, make_assoc_type_item("Output")),
1839 (const_id, make_assoc_const_item("MAX")),
1840 ]);
1841
1842 let result = CategorizedTraitItems::categorize_trait_items(
1843 &[req_id, prov_id, type_id, const_id],
1844 &krate,
1845 );
1846
1847 assert_eq!(result.required_methods.len(), 1);
1848 assert_eq!(result.provided_methods.len(), 1);
1849 assert_eq!(result.associated_types.len(), 1);
1850 assert_eq!(result.associated_consts.len(), 1);
1851 }
1852
1853 #[test]
1854 fn skips_missing_items() {
1855 let existing_id = Id(1);
1856 let missing_id = Id(99);
1857 let krate = make_test_crate(vec![(existing_id, make_function_item("fn", false))]);
1858
1859 let result =
1860 CategorizedTraitItems::categorize_trait_items(&[existing_id, missing_id], &krate);
1861
1862 assert_eq!(result.required_methods.len(), 1);
1864 }
1865 }
1866}