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 full_method_docs: bool,
843 ) where
844 F: Fn(&Item) -> Option<String>,
845 L: Fn(rustdoc_types::Id) -> Option<String>,
846 {
847 for item_id in &impl_block.items {
848 if let Some(item) = krate.index.get(item_id) {
849 let name = item.name.as_deref().unwrap_or("_");
850
851 match &item.inner {
852 ItemEnum::Function(f) => {
853 Self::render_impl_function(md, name, f, *type_renderer, parent_type_name, impl_ctx);
854
855 if let Some(link_creator) = create_type_link {
857 Self::render_function_type_links_inline(
858 md,
859 f,
860 *type_renderer,
861 link_creator,
862 );
863 }
864
865 if let Some(pf) = process_docs
867 && let Some(docs) = pf(item)
868 {
869 let summary = Self::extract_method_summary(&docs, full_method_docs);
870 if !summary.is_empty() {
871 for line in summary.lines() {
873 _ = write!(md, "\n\n {line}");
874 }
875 }
876 }
877
878 _ = writeln!(md, "\n");
879 },
880
881 ItemEnum::AssocConst { type_, .. } => {
882 if let Some(type_name) = parent_type_name {
884 let anchor = AnchorUtils::impl_item_anchor(
885 type_name,
886 name,
887 AssocItemKind::Const,
888 impl_ctx,
889 );
890 _ = writeln!(
891 md,
892 "- <span id=\"{anchor}\"></span>`const {name}: {}`\n",
893 type_renderer.render_type(type_)
894 );
895 } else {
896 _ = writeln!(
897 md,
898 "- `const {name}: {}`\n",
899 type_renderer.render_type(type_)
900 );
901 }
902 },
903
904 ItemEnum::AssocType { type_, .. } => {
905 let anchor_prefix = parent_type_name
908 .map(|tn| {
909 format!(
910 "<span id=\"{}\"></span>",
911 AnchorUtils::impl_item_anchor(
912 tn,
913 name,
914 AssocItemKind::Type,
915 impl_ctx
916 )
917 )
918 })
919 .unwrap_or_default();
920
921 if let Some(ty) = type_ {
922 _ = writeln!(
923 md,
924 "- {anchor_prefix}`type {name} = {}`\n",
925 type_renderer.render_type(ty)
926 );
927 } else {
928 _ = writeln!(md, "- {anchor_prefix}`type {name}`\n");
929 }
930 },
931
932 _ => {},
933 }
934 }
935 }
936 }
937
938 fn extract_method_summary(docs: &str, full_method_docs: bool) -> String {
948 if full_method_docs {
950 return docs.to_string();
951 }
952
953 if docs.contains("```") {
956 return docs.to_string();
957 }
958
959 let first_paragraph: String = docs
962 .lines()
963 .take_while(|line| !line.trim().is_empty())
964 .collect::<Vec<_>>()
965 .join("\n");
966
967 first_paragraph
968 }
969
970 fn render_function_type_links_inline<L>(
975 md: &mut String,
976 f: &rustdoc_types::Function,
977 type_renderer: TypeRenderer,
978 create_link: &L,
979 ) where
980 L: Fn(rustdoc_types::Id) -> Option<String>,
981 {
982 use std::collections::HashSet;
983
984 let mut all_types = Vec::new();
985
986 for (_, ty) in &f.sig.inputs {
988 all_types.extend(type_renderer.collect_linkable_types(ty));
989 }
990
991 if let Some(output) = &f.sig.output {
993 all_types.extend(type_renderer.collect_linkable_types(output));
994 }
995
996 let mut seen = HashSet::new();
998 let unique_types: Vec<_> = all_types
999 .into_iter()
1000 .filter(|(name, _)| seen.insert(name.clone()))
1001 .collect();
1002
1003 let links: Vec<String> = unique_types
1005 .iter()
1006 .filter_map(|(_, id)| create_link(*id))
1007 .collect();
1008
1009 if !links.is_empty() {
1011 _ = write!(md, " — {}", links.join(", "));
1012 }
1013 }
1014
1015 fn render_impl_function(
1022 md: &mut String,
1023 name: &str,
1024 f: &rustdoc_types::Function,
1025 type_renderer: TypeRenderer,
1026 parent_type_name: Option<&str>,
1027 impl_ctx: ImplContext<'_>,
1028 ) {
1029 let generics = type_renderer.render_generics(&f.generics.params);
1030
1031 let params: Vec<String> = f
1032 .sig
1033 .inputs
1034 .iter()
1035 .map(|(param_name, ty)| {
1036 let raw = format!("{param_name}: {}", type_renderer.render_type(ty));
1037
1038 RendererUtils::sanitize_self_param(&raw).into_owned()
1039 })
1040 .collect();
1041
1042 let ret = f
1043 .sig
1044 .output
1045 .as_ref()
1046 .map(|ty| format!(" -> {}", type_renderer.render_type(ty)))
1047 .unwrap_or_default();
1048
1049 let is_async = if f.header.is_async { "async " } else { "" };
1050 let is_const = if f.header.is_const { "const " } else { "" };
1051 let is_unsafe = if f.header.is_unsafe { "unsafe " } else { "" };
1052
1053 let anchor_span = parent_type_name
1056 .map(|tn| {
1057 format!(
1058 "<span id=\"{}\"></span>",
1059 AnchorUtils::impl_item_anchor(tn, name, AssocItemKind::Method, impl_ctx)
1060 )
1061 })
1062 .unwrap_or_default();
1063
1064 _ = write!(
1065 md,
1066 "- {anchor_span}`{}{}{}fn {}{}({}){}`",
1067 is_const,
1068 is_async,
1069 is_unsafe,
1070 name,
1071 generics,
1072 params.join(", "),
1073 ret
1074 );
1075 }
1076
1077 pub fn append_docs(md: &mut String, docs: Option<String>) {
1081 if let Some(docs) = docs {
1082 _ = write!(md, "{}", &docs);
1083 _ = writeln!(md, "\n");
1084 }
1085 }
1086
1087 #[must_use]
1106 pub fn render_collapsible_start(summary: &str) -> String {
1107 format!("<details>\n<summary>{summary}</summary>\n\n")
1108 }
1109
1110 #[must_use]
1122 pub const fn render_collapsible_end() -> &'static str {
1123 "\n</details>\n\n"
1124 }
1125
1126 #[must_use]
1130 pub fn impl_sort_key(impl_block: &Impl, type_renderer: &TypeRenderer) -> String {
1131 let trait_name = impl_block
1132 .trait_
1133 .as_ref()
1134 .map(|t| t.path.clone())
1135 .unwrap_or_default();
1136 let for_type = type_renderer.render_type(&impl_block.for_);
1137 let generics = type_renderer.render_generics(&impl_block.generics.params);
1138
1139 format!("{trait_name}{generics}::{for_type}")
1140 }
1141
1142 #[must_use]
1169 pub fn render_source_location(
1170 span: Option<&Span>,
1171 source_path_config: Option<&SourcePathConfig>,
1172 ) -> String {
1173 let Some(span) = span else {
1174 return String::new();
1175 };
1176
1177 let (start_line, _) = span.begin;
1178 let (end_line, _) = span.end;
1179
1180 let line_ref = if start_line == end_line {
1182 format!("{start_line}")
1183 } else {
1184 format!("{start_line}-{end_line}")
1185 };
1186
1187 if let Some(config) = source_path_config
1189 && let Some(relative_path) =
1190 RendererUtils::transform_cargo_path(&span.filename, &config.source_dir_name)
1191 {
1192 let prefix = "../".repeat(config.depth + 1);
1195
1196 let fragment = if start_line == end_line {
1198 format!("#L{start_line}")
1199 } else {
1200 format!("#L{start_line}-L{end_line}")
1201 };
1202
1203 let display_path = relative_path
1205 .strip_prefix(&config.source_dir_name)
1206 .map_or(relative_path.as_str(), |p| p.trim_start_matches('/'));
1207
1208 return format!(
1209 "*Defined in [`{display_path}:{line_ref}`]({prefix}{relative_path}{fragment})*\n\n"
1210 );
1211 }
1212
1213 let filename = span.filename.display();
1215 format!("*Defined in `{filename}:{line_ref}`*\n\n")
1216 }
1217
1218 pub fn render_union_definition(
1231 md: &mut String,
1232 name: &str,
1233 u: &rustdoc_types::Union,
1234 krate: &Crate,
1235 type_renderer: &TypeRenderer,
1236 ) {
1237 let generics = type_renderer.render_generics(&u.generics.params);
1238 let where_clause = type_renderer.render_where_clause(&u.generics.where_predicates);
1239
1240 _ = writeln!(md, "### `{name}{generics}`\n");
1241
1242 _ = writeln!(md, "```rust");
1243 _ = writeln!(md, "union {name}{generics}{where_clause} {{");
1244
1245 for field_id in &u.fields {
1246 if let Some(field) = krate.index.get(field_id) {
1247 let field_name = field.name.as_deref().unwrap_or("_");
1248
1249 if let ItemEnum::StructField(ty) = &field.inner {
1250 let vis = match &field.visibility {
1251 Visibility::Public => "pub ",
1252 _ => "",
1253 };
1254
1255 _ = writeln!(
1256 md,
1257 " {}{}: {},",
1258 vis,
1259 field_name,
1260 type_renderer.render_type(ty)
1261 );
1262 }
1263 }
1264 }
1265
1266 if u.has_stripped_fields {
1267 _ = writeln!(md, " // some fields omitted");
1268 }
1269
1270 _ = writeln!(md, "}}\n```\n");
1271 }
1272
1273 pub fn render_union_fields<F>(
1286 md: &mut String,
1287 fields: &[Id],
1288 krate: &Crate,
1289 type_renderer: &TypeRenderer,
1290 process_docs: F,
1291 ) where
1292 F: Fn(&Item) -> Option<String>,
1293 {
1294 let has_documented_fields = fields
1296 .iter()
1297 .any(|id| krate.index.get(id).is_some_and(|item| item.docs.is_some()));
1298
1299 if !has_documented_fields {
1300 return;
1301 }
1302
1303 _ = write!(md, "#### Fields\n\n");
1304
1305 for field_id in fields {
1306 let Some(field) = krate.index.get(field_id) else {
1307 continue;
1308 };
1309
1310 let field_name = field.name.as_deref().unwrap_or("_");
1311
1312 if let ItemEnum::StructField(ty) = &field.inner {
1313 let type_str = type_renderer.render_type(ty);
1314 _ = writeln!(md, "- **`{field_name}`**: `{type_str}`");
1315
1316 if let Some(docs) = process_docs(field) {
1317 for line in docs.lines() {
1319 if line.is_empty() {
1320 md.push('\n');
1321 } else {
1322 _ = writeln!(md, " {line}");
1323 }
1324 }
1325
1326 _ = writeln!(md);
1327 }
1328 }
1329 }
1330 }
1331
1332 pub fn render_static_definition(
1344 md: &mut String,
1345 name: &str,
1346 s: &rustdoc_types::Static,
1347 type_renderer: &TypeRenderer,
1348 ) {
1349 _ = write!(md, "### `{name}`\n\n");
1350
1351 _ = writeln!(md, "```rust");
1352
1353 let mut decl = String::new();
1355
1356 if s.is_unsafe {
1358 _ = write!(decl, "unsafe ");
1359 }
1360
1361 _ = write!(decl, "static ");
1362
1363 if s.is_mutable {
1365 _ = write!(decl, "mut ");
1366 }
1367
1368 _ = write!(decl, "{name}: {}", type_renderer.render_type(&s.type_));
1370
1371 if !s.expr.is_empty() {
1373 _ = write!(decl, " = {}", s.expr);
1374 }
1375
1376 _ = write!(decl, ";");
1377
1378 _ = writeln!(md, "{decl}");
1379 _ = write!(md, "```\n\n");
1380 }
1381}
1382pub trait DocsProcessor {
1386 fn process_item_docs(&self, item: &Item) -> Option<String>;
1388}
1389
1390impl<T: RenderContext + ?Sized> DocsProcessor for (&T, &str) {
1391 fn process_item_docs(&self, item: &Item) -> Option<String> {
1392 self.0.process_docs(item, self.1)
1393 }
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398 use super::*;
1399
1400 mod sanitize_path_tests {
1401 use super::*;
1402
1403 #[test]
1404 fn removes_crate_prefix() {
1405 assert_eq!(
1406 RendererUtils::sanitize_path("$crate::clone::Clone"),
1407 "clone::Clone"
1408 );
1409 }
1410
1411 #[test]
1412 fn removes_multiple_crate_prefixes() {
1413 assert_eq!(
1414 RendererUtils::sanitize_path("$crate::foo::$crate::bar::Baz"),
1415 "foo::bar::Baz"
1416 );
1417 }
1418
1419 #[test]
1420 fn preserves_normal_paths() {
1421 assert_eq!(
1422 RendererUtils::sanitize_path("std::fmt::Debug"),
1423 "std::fmt::Debug"
1424 );
1425 }
1426
1427 #[test]
1428 fn preserves_simple_names() {
1429 assert_eq!(RendererUtils::sanitize_path("Clone"), "Clone");
1430 }
1431
1432 #[test]
1433 fn handles_empty_string() {
1434 assert_eq!(RendererUtils::sanitize_path(""), "");
1435 }
1436
1437 #[test]
1438 fn returns_borrowed_when_no_change() {
1439 let result = RendererUtils::sanitize_path("std::fmt::Debug");
1440 assert!(matches!(result, Cow::Borrowed(_)));
1441 }
1442
1443 #[test]
1444 fn returns_owned_when_changed() {
1445 let result = RendererUtils::sanitize_path("$crate::Clone");
1446 assert!(matches!(result, Cow::Owned(_)));
1447 }
1448 }
1449
1450 mod sanitize_self_param_tests {
1451 use super::*;
1452
1453 #[test]
1454 fn converts_ref_self() {
1455 assert_eq!(RendererUtils::sanitize_self_param("self: &Self"), "&self");
1456 }
1457
1458 #[test]
1459 fn converts_mut_ref_self() {
1460 assert_eq!(
1461 RendererUtils::sanitize_self_param("self: &mut Self"),
1462 "&mut self"
1463 );
1464 }
1465
1466 #[test]
1467 fn converts_owned_self() {
1468 assert_eq!(RendererUtils::sanitize_self_param("self: Self"), "self");
1469 }
1470
1471 #[test]
1472 fn preserves_regular_params() {
1473 assert_eq!(RendererUtils::sanitize_self_param("x: i32"), "x: i32");
1474 }
1475
1476 #[test]
1477 fn preserves_complex_types() {
1478 assert_eq!(
1479 RendererUtils::sanitize_self_param("callback: impl Fn()"),
1480 "callback: impl Fn()"
1481 );
1482 }
1483
1484 #[test]
1485 fn returns_borrowed_for_all_cases() {
1486 assert!(matches!(
1488 RendererUtils::sanitize_self_param("self: &Self"),
1489 Cow::Borrowed(_)
1490 ));
1491 assert!(matches!(
1492 RendererUtils::sanitize_self_param("self: &mut Self"),
1493 Cow::Borrowed(_)
1494 ));
1495 assert!(matches!(
1496 RendererUtils::sanitize_self_param("self: Self"),
1497 Cow::Borrowed(_)
1498 ));
1499 assert!(matches!(
1500 RendererUtils::sanitize_self_param("x: i32"),
1501 Cow::Borrowed(_)
1502 ));
1503 }
1504 }
1505
1506 mod collapsible_tests {
1507 use super::RendererInternals;
1508
1509 #[test]
1510 fn start_contains_details_tag() {
1511 let result = RendererInternals::render_collapsible_start("Test Summary");
1512 assert!(result.contains("<details>"));
1513 }
1514
1515 #[test]
1516 fn start_contains_summary_with_text() {
1517 let result =
1518 RendererInternals::render_collapsible_start("Derived Traits (9 implementations)");
1519 assert!(result.contains("<summary>Derived Traits (9 implementations)</summary>"));
1520 }
1521
1522 #[test]
1523 fn start_has_proper_formatting() {
1524 let result = RendererInternals::render_collapsible_start("Test");
1525 assert_eq!(result, "<details>\n<summary>Test</summary>\n\n");
1526 }
1527
1528 #[test]
1529 fn end_closes_details_tag() {
1530 let result = RendererInternals::render_collapsible_end();
1531 assert!(result.contains("</details>"));
1532 }
1533
1534 #[test]
1535 fn end_has_proper_formatting() {
1536 assert_eq!(
1537 RendererInternals::render_collapsible_end(),
1538 "\n</details>\n\n"
1539 );
1540 }
1541
1542 #[test]
1543 fn start_and_end_pair_correctly() {
1544 let start = RendererInternals::render_collapsible_start("Content");
1545 let end = RendererInternals::render_collapsible_end();
1546 let full = format!("{start}Some markdown content here{end}");
1547
1548 assert!(full.starts_with("<details>"));
1549 assert!(full.ends_with("</details>\n\n"));
1550 assert!(full.contains("<summary>Content</summary>"));
1551 }
1552 }
1553
1554 mod source_location_tests {
1555 use std::path::PathBuf;
1556
1557 use super::*;
1558
1559 #[test]
1560 fn transform_cargo_path_extracts_crate_relative() {
1561 let path = PathBuf::from(
1562 "/home/user/.cargo/registry/src/index.crates.io-xxx/serde-1.0.228/src/lib.rs",
1563 );
1564 let result = RendererUtils::transform_cargo_path(&path, ".source_12345");
1565 assert_eq!(
1566 result,
1567 Some(".source_12345/serde-1.0.228/src/lib.rs".to_string())
1568 );
1569 }
1570
1571 #[test]
1572 fn transform_cargo_path_handles_nested_paths() {
1573 let path = PathBuf::from(
1574 "/home/user/.cargo/registry/src/index.crates.io-abc/tokio-1.0.0/src/runtime/mod.rs",
1575 );
1576 let result = RendererUtils::transform_cargo_path(&path, ".source_99999");
1577 assert_eq!(
1578 result,
1579 Some(".source_99999/tokio-1.0.0/src/runtime/mod.rs".to_string())
1580 );
1581 }
1582
1583 #[test]
1584 fn transform_cargo_path_returns_none_for_non_cargo_path() {
1585 let path = PathBuf::from("/usr/local/src/myproject/lib.rs");
1586 let result = RendererUtils::transform_cargo_path(&path, ".source_12345");
1587 assert_eq!(result, None);
1588 }
1589
1590 #[test]
1591 fn transform_cargo_path_returns_none_for_local_path() {
1592 let path = PathBuf::from("src/lib.rs");
1593 let result = RendererUtils::transform_cargo_path(&path, ".source_12345");
1594 assert_eq!(result, None);
1595 }
1596
1597 #[test]
1598 fn source_path_config_calculates_depth() {
1599 let source_dir = PathBuf::from("/project/.source_12345");
1600
1601 let config = SourcePathConfig::new(&source_dir, "index.md");
1602 assert_eq!(config.depth, 0);
1603
1604 let config = SourcePathConfig::new(&source_dir, "serde/index.md");
1605 assert_eq!(config.depth, 1);
1606
1607 let config = SourcePathConfig::new(&source_dir, "serde/de/visitor/index.md");
1608 assert_eq!(config.depth, 3);
1609 }
1610
1611 #[test]
1612 fn source_path_config_extracts_dir_name() {
1613 let source_dir = PathBuf::from("/project/.source_1733660400");
1614 let config = SourcePathConfig::new(&source_dir, "index.md");
1615 assert_eq!(config.source_dir_name, ".source_1733660400");
1616 }
1617
1618 #[test]
1619 fn source_path_config_with_depth_preserves_name() {
1620 let source_dir = PathBuf::from("/project/.source_12345");
1621 let base_config = SourcePathConfig::new(&source_dir, "");
1622 let file_config = base_config.with_depth("crate/module/index.md");
1623
1624 assert_eq!(file_config.source_dir_name, ".source_12345");
1625 assert_eq!(file_config.depth, 2);
1626 }
1627
1628 #[test]
1629 fn render_source_location_without_config_shows_absolute_path() {
1630 let span = rustdoc_types::Span {
1631 filename: PathBuf::from(
1632 "/home/user/.cargo/registry/src/index/serde-1.0/src/lib.rs",
1633 ),
1634 begin: (10, 0),
1635 end: (25, 0),
1636 };
1637 let result = RendererInternals::render_source_location(Some(&span), None);
1638 assert!(result.contains("/home/user/.cargo/registry/src/index/serde-1.0/src/lib.rs"));
1639 assert!(result.contains("10-25"));
1640 assert!(!result.contains('['));
1642 }
1643
1644 #[test]
1645 fn render_source_location_with_config_creates_link() {
1646 let span = rustdoc_types::Span {
1647 filename: PathBuf::from(
1648 "/home/user/.cargo/registry/src/index.crates.io-xxx/serde-1.0.228/src/lib.rs",
1649 ),
1650 begin: (10, 0),
1651 end: (25, 0),
1652 };
1653 let config = SourcePathConfig {
1654 source_dir_name: ".source_12345".to_string(),
1655 depth: 1, };
1657 let result = RendererInternals::render_source_location(Some(&span), Some(&config));
1658
1659 assert!(result.contains('['));
1661 assert!(result.contains("]("));
1662 assert!(result.contains("../../.source_12345/serde-1.0.228/src/lib.rs"));
1664 assert!(result.contains("#L10-L25"));
1666 assert!(result.contains("[`serde-1.0.228/src/lib.rs:10-25`]"));
1668 }
1669
1670 #[test]
1671 fn render_source_location_single_line() {
1672 let span = rustdoc_types::Span {
1673 filename: PathBuf::from(
1674 "/home/user/.cargo/registry/src/index.crates.io-xxx/foo-1.0.0/src/lib.rs",
1675 ),
1676 begin: (42, 0),
1677 end: (42, 0),
1678 };
1679 let config = SourcePathConfig {
1680 source_dir_name: ".source_99999".to_string(),
1681 depth: 0,
1682 };
1683 let result = RendererInternals::render_source_location(Some(&span), Some(&config));
1684
1685 assert!(result.contains(":42`]"));
1687 assert!(result.contains("#L42)"));
1688 assert!(!result.contains("-L"));
1690 }
1691
1692 #[test]
1693 fn render_source_location_none_span_returns_empty() {
1694 let config = SourcePathConfig {
1695 source_dir_name: ".source_12345".to_string(),
1696 depth: 0,
1697 };
1698 let result = RendererInternals::render_source_location(None, Some(&config));
1699 assert!(result.is_empty());
1700 }
1701 }
1702
1703 mod categorized_trait_items_tests {
1704 use std::collections::HashMap;
1705
1706 use rustdoc_types::{Abi, Crate, Function, FunctionHeader, FunctionSignature, Target};
1707
1708 use super::*;
1709
1710 fn make_test_crate(items: Vec<(Id, Item)>) -> Crate {
1711 let mut index: HashMap<Id, Item> = HashMap::new();
1712 for (id, item) in items {
1713 index.insert(id, item);
1714 }
1715
1716 Crate {
1717 root: Id(0),
1718 crate_version: None,
1719 includes_private: false,
1720 index,
1721 paths: HashMap::new(),
1722 external_crates: HashMap::new(),
1723 format_version: 0,
1724 target: Target {
1725 triple: String::new(),
1726 target_features: vec![],
1727 },
1728 }
1729 }
1730
1731 fn make_function_item(name: &str, has_body: bool) -> Item {
1732 Item {
1733 id: Id(0),
1734 crate_id: 0,
1735 name: Some(name.to_string()),
1736 attrs: vec![],
1737 visibility: Visibility::Public,
1738 inner: ItemEnum::Function(Function {
1739 sig: FunctionSignature {
1740 inputs: vec![],
1741 output: None,
1742 is_c_variadic: false,
1743 },
1744 generics: rustdoc_types::Generics {
1745 params: vec![],
1746 where_predicates: vec![],
1747 },
1748 header: FunctionHeader {
1749 is_const: false,
1750 is_async: false,
1751 is_unsafe: false,
1752 abi: Abi::Rust,
1753 },
1754 has_body,
1755 }),
1756 deprecation: None,
1757 docs: None,
1758 span: None,
1759 links: HashMap::new(),
1760 }
1761 }
1762
1763 fn make_assoc_type_item(name: &str) -> Item {
1764 Item {
1765 id: Id(0),
1766 crate_id: 0,
1767 name: Some(name.to_string()),
1768 attrs: vec![],
1769 visibility: Visibility::Public,
1770 inner: ItemEnum::AssocType {
1771 generics: rustdoc_types::Generics {
1772 params: vec![],
1773 where_predicates: vec![],
1774 },
1775 bounds: vec![],
1776 type_: None,
1777 },
1778 deprecation: None,
1779 docs: None,
1780 span: None,
1781 links: HashMap::new(),
1782 }
1783 }
1784
1785 fn make_assoc_const_item(name: &str) -> Item {
1786 Item {
1787 id: Id(0),
1788 crate_id: 0,
1789 name: Some(name.to_string()),
1790 attrs: vec![],
1791 visibility: Visibility::Public,
1792 inner: ItemEnum::AssocConst {
1793 type_: rustdoc_types::Type::Primitive("i32".to_string()),
1794 value: Some("42".to_string()),
1795 },
1796 deprecation: None,
1797 docs: None,
1798 span: None,
1799 links: HashMap::new(),
1800 }
1801 }
1802
1803 #[test]
1804 fn empty_trait_items() {
1805 let krate = make_test_crate(vec![]);
1806 let result = CategorizedTraitItems::categorize_trait_items(&[], &krate);
1807
1808 assert!(result.required_methods.is_empty());
1809 assert!(result.provided_methods.is_empty());
1810 assert!(result.associated_types.is_empty());
1811 assert!(result.associated_consts.is_empty());
1812 }
1813
1814 #[test]
1815 fn categorizes_required_method() {
1816 let id = Id(1);
1817 let item = make_function_item("required_fn", false);
1818 let krate = make_test_crate(vec![(id, item)]);
1819
1820 let result = CategorizedTraitItems::categorize_trait_items(&[id], &krate);
1821
1822 assert_eq!(result.required_methods.len(), 1);
1823 assert_eq!(
1824 result.required_methods[0].name.as_deref(),
1825 Some("required_fn")
1826 );
1827 assert!(result.provided_methods.is_empty());
1828 }
1829
1830 #[test]
1831 fn categorizes_provided_method() {
1832 let id = Id(1);
1833 let item = make_function_item("provided_fn", true);
1834 let krate = make_test_crate(vec![(id, item)]);
1835
1836 let result = CategorizedTraitItems::categorize_trait_items(&[id], &krate);
1837
1838 assert!(result.required_methods.is_empty());
1839 assert_eq!(result.provided_methods.len(), 1);
1840 assert_eq!(
1841 result.provided_methods[0].name.as_deref(),
1842 Some("provided_fn")
1843 );
1844 }
1845
1846 #[test]
1847 fn categorizes_associated_type() {
1848 let id = Id(1);
1849 let item = make_assoc_type_item("Item");
1850 let krate = make_test_crate(vec![(id, item)]);
1851
1852 let result = CategorizedTraitItems::categorize_trait_items(&[id], &krate);
1853
1854 assert_eq!(result.associated_types.len(), 1);
1855 assert_eq!(result.associated_types[0].name.as_deref(), Some("Item"));
1856 }
1857
1858 #[test]
1859 fn categorizes_associated_const() {
1860 let id = Id(1);
1861 let item = make_assoc_const_item("CONST");
1862 let krate = make_test_crate(vec![(id, item)]);
1863
1864 let result = CategorizedTraitItems::categorize_trait_items(&[id], &krate);
1865
1866 assert_eq!(result.associated_consts.len(), 1);
1867 assert_eq!(result.associated_consts[0].name.as_deref(), Some("CONST"));
1868 }
1869
1870 #[test]
1871 fn categorizes_mixed_items() {
1872 let req_id = Id(1);
1873 let prov_id = Id(2);
1874 let type_id = Id(3);
1875 let const_id = Id(4);
1876
1877 let krate = make_test_crate(vec![
1878 (req_id, make_function_item("req", false)),
1879 (prov_id, make_function_item("prov", true)),
1880 (type_id, make_assoc_type_item("Output")),
1881 (const_id, make_assoc_const_item("MAX")),
1882 ]);
1883
1884 let result = CategorizedTraitItems::categorize_trait_items(
1885 &[req_id, prov_id, type_id, const_id],
1886 &krate,
1887 );
1888
1889 assert_eq!(result.required_methods.len(), 1);
1890 assert_eq!(result.provided_methods.len(), 1);
1891 assert_eq!(result.associated_types.len(), 1);
1892 assert_eq!(result.associated_consts.len(), 1);
1893 }
1894
1895 #[test]
1896 fn skips_missing_items() {
1897 let existing_id = Id(1);
1898 let missing_id = Id(99);
1899 let krate = make_test_crate(vec![(existing_id, make_function_item("fn", false))]);
1900
1901 let result =
1902 CategorizedTraitItems::categorize_trait_items(&[existing_id, missing_id], &krate);
1903
1904 assert_eq!(result.required_methods.len(), 1);
1906 }
1907 }
1908}