cargo_docs_md/generator/
render_shared.rs

1//! Shared rendering functions for documentation generation.
2//!
3//! This module contains standalone rendering functions that can be used by both
4//! single-crate ([`ItemRenderer`](super::ItemRenderer)) and multi-crate
5//! ([`MultiCrateModuleRenderer`](crate::multi_crate::generator)) renderers.
6//!
7//! These functions handle the core markdown generation logic without being tied
8//! to a specific rendering context, avoiding code duplication between the two modes.
9
10use 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// =============================================================================
21// Source Location Rendering
22// =============================================================================
23
24/// Information needed to transform source paths to relative links.
25///
26/// When generating source location references, this config enables transforming
27/// absolute cargo registry paths to relative links pointing to the local
28/// `.source_{timestamp}` directory.
29#[derive(Debug, Clone)]
30pub struct SourcePathConfig {
31    /// The `.source_{timestamp}` directory name (e.g., `.source_1733660400`).
32    pub source_dir_name: String,
33
34    /// Depth of the current markdown file from `generated_docs/`.
35    /// Used to calculate the correct number of `../` prefixes.
36    pub depth: usize,
37}
38
39impl SourcePathConfig {
40    /// Create a new source path config.
41    ///
42    /// # Arguments
43    ///
44    /// * `source_dir` - Full path to the `.source_*` directory
45    /// * `current_file` - Path of the current markdown file relative to output dir
46    #[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        // Count depth: number of '/' in current_file path
55        // e.g., "serde/de/index.md" has depth 2
56        let depth = current_file.matches('/').count();
57
58        Self {
59            source_dir_name,
60            depth,
61        }
62    }
63
64    /// Create a config with a specific depth (for file-specific configs).
65    #[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/// Categorized trait items for structured rendering.
75#[derive(Default)]
76pub struct CategorizedTraitItems<'a> {
77    /// Required methods (no default body)
78    pub required_methods: Vec<&'a Item>,
79
80    /// Provided methods (have default body)
81    pub provided_methods: Vec<&'a Item>,
82
83    /// Associated types
84    pub associated_types: Vec<&'a Item>,
85
86    /// Associated constants
87    pub associated_consts: Vec<&'a Item>,
88}
89
90impl<'a> CategorizedTraitItems<'a> {
91    /// Categorize trait items into required/provided methods, types and constants.
92    #[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
126/// Unit struct to organize path related utility functions related to renderer functions.
127pub struct RendererUtils;
128
129impl RendererUtils {
130    /// Sanitize trait paths by removing macro artifacts.
131    ///
132    /// Rustdoc JSON can contain `$crate::` prefixes from macro expansions
133    /// which leak implementation details into documentation. This function
134    /// removes these artifacts for cleaner output.
135    ///
136    /// Uses `Cow<str>` to avoid allocation when no changes are needed.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use cargo_docs_md::generator::render_shared::RendererUtils;
142    ///
143    /// assert_eq!(RendererUtils::sanitize_path("$crate::clone::Clone"), "clone::Clone");
144    /// assert_eq!(RendererUtils::sanitize_path("std::fmt::Debug"), "std::fmt::Debug");
145    /// ```
146    #[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    /// Sanitize self parameter in function signatures.
156    ///
157    /// Converts verbose self type annotations to idiomatic Rust syntax:
158    /// - `self: &Self` → `&self`
159    /// - `self: &mut Self` → `&mut self`
160    /// - `self: Self` → `self`
161    ///
162    /// Uses `Cow<str>` to avoid allocation when no changes are needed.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use cargo_docs_md::generator::render_shared::RendererUtils;
168    ///
169    /// assert_eq!(RendererUtils::sanitize_self_param("self: &Self"), "&self");
170    /// assert_eq!(RendererUtils::sanitize_self_param("self: &mut Self"), "&mut self");
171    /// assert_eq!(RendererUtils::sanitize_self_param("self: Self"), "self");
172    /// assert_eq!(RendererUtils::sanitize_self_param("x: i32"), "x: i32");
173    /// ```
174    #[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    /// Write tuple field types directly to buffer, comma-separated.
188    ///
189    /// Avoids intermediate `Vec` allocation by writing directly to the output buffer.
190    /// Handles `Option<Id>` fields from rustdoc's representation of tuple structs/variants
191    /// (where `None` indicates a private field).
192    ///
193    /// # Arguments
194    ///
195    /// * `out` - Output buffer to write to
196    /// * `fields` - Slice of optional field IDs from rustdoc
197    /// * `krate` - Crate containing field definitions
198    /// * `type_renderer` - Type renderer for field types
199    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! is infallible for String
215                _ = write!(out, "{}", type_renderer.render_type(ty));
216                first = false;
217            }
218        }
219    }
220
221    /// Transform an absolute cargo registry path to a relative `.source_*` path.
222    ///
223    /// Converts paths like:
224    /// `/home/user/.cargo/registry/src/index.crates.io-xxx/serde-1.0.228/src/lib.rs`
225    ///
226    /// To:
227    /// `.source_1733660400/serde-1.0.228/src/lib.rs`
228    ///
229    /// Returns `None` if the path doesn't match the expected cargo registry pattern.
230    #[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        // Look for the pattern: .cargo/registry/src/{index}/
235        // The crate directory follows the index directory
236        if let Some(registry_idx) = path_str.find(".cargo/registry/src/") {
237            // Find the index directory end (e.g., "index.crates.io-xxx/")
238            let after_registry = &path_str[registry_idx + ".cargo/registry/src/".len()..];
239
240            // Skip the index directory name (find the next '/')
241            if let Some(slash_idx) = after_registry.find('/') {
242                // Everything after the index directory is the crate path
243                // e.g., "serde-1.0.228/src/lib.rs"
244                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
253/// Unit struct to organize trait related functions.
254pub struct TraitRenderer;
255
256impl TraitRenderer {
257    /// Write trait bounds with `: ` prefix directly to buffer.
258    ///
259    /// Avoids intermediate `Vec` allocation for trait supertrait bounds.
260    /// Writes nothing if bounds are empty.
261    ///
262    /// # Arguments
263    ///
264    /// * `out` - Output buffer to write to
265    /// * `bounds` - Slice of generic bounds from the trait
266    /// * `type_renderer` - Type renderer for bounds (passed by value as it's Copy)
267    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            // Skip empty rendered bounds
282            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    /// Render a trait definition code block to markdown.
296    ///
297    /// Produces a heading with the trait name and generics, followed by a Rust
298    /// code block showing the trait signature with supertraits.
299    ///
300    /// # Arguments
301    ///
302    /// * `md` - Output markdown string
303    /// * `name` - The trait name
304    /// * `t` - The trait data from rustdoc
305    /// * `type_renderer` - Type renderer for generics and bounds
306    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    /// Render a single trait item (method, associated type, or constant).
327    ///
328    /// Each item is rendered as a bullet point with its signature in backticks.
329    /// For methods, the first line of documentation is included.
330    ///
331    /// # Arguments
332    ///
333    /// * `md` - Output markdown string
334    /// * `item` - The trait item (function, assoc type, or assoc const)
335    /// * `type_renderer` - Type renderer for types
336    /// * `process_docs` - Closure to process documentation with intra-doc link resolution
337    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
416/// Unit struct containing renderer functions.
417/// Helpful because free functions are annoying.
418pub struct RendererInternals;
419
420impl RendererInternals {
421    /// Render a struct definition code block to markdown.
422    ///
423    /// Produces a heading with the struct name and generics, followed by a Rust
424    /// code block showing the struct definition.
425    ///
426    /// # Arguments
427    ///
428    /// * `md` - Output markdown string
429    /// * `name` - The struct name (may differ from item.name for re-exports)
430    /// * `s` - The struct data from rustdoc
431    /// * `krate` - The crate containing field definitions
432    /// * `type_renderer` - Type renderer for generics and field types
433    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    /// Render documented struct fields to markdown.
496    ///
497    /// Produces a "Fields" section with each documented field as a bullet point
498    /// showing the field name, type, and documentation.
499    ///
500    /// # Arguments
501    ///
502    /// * `md` - Output markdown string
503    /// * `fields` - Field IDs from the struct
504    /// * `krate` - Crate containing field definitions
505    /// * `type_renderer` - Type renderer for field types
506    /// * `process_docs` - Closure to process documentation with intra-doc link resolution
507    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    /// Render an enum definition code block to markdown.
547    ///
548    /// Produces a heading with the enum name and generics, followed by a Rust
549    /// code block showing the enum definition with all variants.
550    ///
551    /// # Arguments
552    ///
553    /// * `md` - Output markdown string
554    /// * `name` - The enum name (may differ from item.name for re-exports)
555    /// * `e` - The enum data from rustdoc
556    /// * `krate` - The crate containing variant definitions
557    /// * `type_renderer` - Type renderer for generics and variant types
558    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    /// Render a single enum variant within the definition code block.
584    ///
585    /// Handles all three variant kinds: plain, tuple, and struct variants.
586    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    /// Render documented enum variants to markdown.
631    ///
632    /// Produces a "Variants" section with each documented variant as a bullet point.
633    ///
634    /// # Arguments
635    ///
636    /// * `md` - Output markdown string
637    /// * `variants` - Variant IDs from the enum
638    /// * `krate` - Crate containing variant definitions
639    /// * `process_docs` - Closure to process documentation with intra-doc link resolution
640    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    /// Render a function definition to markdown.
671    ///
672    /// Produces a heading with the function name, followed by a Rust code block
673    /// showing the full signature with modifiers (const, async, unsafe).
674    ///
675    /// # Arguments
676    ///
677    /// * `md` - Output markdown string
678    /// * `name` - The function name
679    /// * `f` - The function data from rustdoc
680    /// * `type_renderer` - Type renderer for parameter and return types
681    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    /// Render a constant definition to markdown.
732    ///
733    /// Produces a heading with the constant name, followed by a Rust code block
734    /// showing `const NAME: Type = value;`.
735    ///
736    /// # Arguments
737    ///
738    /// * `md` - Output markdown string
739    /// * `name` - The constant name
740    /// * `type_` - The constant's type
741    /// * `const_` - The constant data including value
742    /// * `type_renderer` - Type renderer for the type
743    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    /// Render a type alias definition to markdown.
770    ///
771    /// Produces a heading with the alias name and generics, followed by a Rust
772    /// code block showing `type Name<T> = TargetType;`.
773    ///
774    /// # Arguments
775    ///
776    /// * `md` - Output markdown string
777    /// * `name` - The type alias name
778    /// * `ta` - The type alias data from rustdoc
779    /// * `type_renderer` - Type renderer for generics and the aliased type
780    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    /// Render a macro definition to markdown.
802    ///
803    /// Produces a heading with the macro name and `!` suffix.
804    /// Note: We don't show macro rules since rustdoc JSON doesn't provide them.
805    ///
806    /// # Arguments
807    ///
808    /// * `md` - Output markdown string
809    /// * `name` - The macro name
810    pub fn render_macro_heading(md: &mut String, name: &str) {
811        _ = write!(md, "### `{name}!`\n\n");
812    }
813
814    /// Render the items within an impl block.
815    ///
816    /// This renders all methods, associated constants, and associated types
817    /// within an impl block as bullet points.
818    ///
819    /// # Arguments
820    ///
821    /// * `md` - Output markdown string
822    /// * `impl_block` - The impl block to render items from
823    /// * `krate` - The crate containing item definitions
824    /// * `type_renderer` - Type renderer for types
825    /// * `process_docs` - Optional closure to process documentation
826    /// * `create_type_link` - Optional closure to create links for types `(id -> Option<markdown_link>)`
827    /// * `parent_type_name` - Optional type name for generating method anchors
828    /// * `impl_ctx` - Context for anchor generation (inherent vs trait impl)
829    #[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                        // Add type links if link creator is provided
855                        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                        // First line of docs as summary (with blank line before)
865                        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                        // Add anchor for associated constants if parent type is known
877                        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                        // Add anchor for associated types if parent type is known
900                        // Use impl_item_anchor to include trait name for uniqueness
901                        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    /// Render type links for a function signature inline (for impl methods).
933    ///
934    /// This is a helper that collects types from function signatures and
935    /// creates links for resolvable types, outputting them on the same line.
936    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        // Collect from parameters
949        for (_, ty) in &f.sig.inputs {
950            all_types.extend(type_renderer.collect_linkable_types(ty));
951        }
952
953        // Collect from return type
954        if let Some(output) = &f.sig.output {
955            all_types.extend(type_renderer.collect_linkable_types(output));
956        }
957
958        // Deduplicate by name (keep first occurrence)
959        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        // Create links for resolvable types
966        let links: Vec<String> = unique_types
967            .iter()
968            .filter_map(|(_, id)| create_link(*id))
969            .collect();
970
971        // Output inline if we have links
972        if !links.is_empty() {
973            _ = write!(md, " — {}", links.join(", "));
974        }
975    }
976
977    /// Render a function signature within an impl block.
978    ///
979    /// Renders as a bullet point with the full signature including modifiers.
980    /// If `parent_type_name` is provided, includes a hidden anchor for deep linking.
981    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        // Add anchor for deep linking if parent type is known
1013        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    /// Append processed documentation to markdown.
1036    ///
1037    /// Helper function to add documentation with consistent formatting.
1038    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    /// Render the opening of a collapsible `<details>` block with a summary.
1046    ///
1047    /// Produces HTML that creates a collapsible section in markdown. Use with
1048    /// [`render_collapsible_end`] to close the block.
1049    ///
1050    /// # Arguments
1051    ///
1052    /// * `summary` - The text to display in the summary line (clickable header)
1053    ///
1054    /// # Example
1055    ///
1056    /// ```
1057    /// use cargo_docs_md::generator::render_shared::RendererInternals;
1058    ///
1059    /// let start = RendererInternals::render_collapsible_start("Derived Traits (9 implementations)");
1060    /// assert!(start.contains("<details>"));
1061    /// assert!(start.contains("<summary>Derived Traits (9 implementations)</summary>"));
1062    /// ```
1063    #[must_use]
1064    pub fn render_collapsible_start(summary: &str) -> String {
1065        format!("<details>\n<summary>{summary}</summary>\n\n")
1066    }
1067
1068    /// Render the closing of a collapsible `<details>` block.
1069    ///
1070    /// Returns a static string to close a block opened with [`render_collapsible_start`].
1071    ///
1072    /// # Example
1073    ///
1074    /// ```
1075    /// use cargo_docs_md::generator::render_shared::RendererInternals;
1076    ///
1077    /// assert_eq!(RendererInternals::render_collapsible_end(), "\n</details>\n\n");
1078    /// ```
1079    #[must_use]
1080    pub const fn render_collapsible_end() -> &'static str {
1081        "\n</details>\n\n"
1082    }
1083
1084    /// Generate a sort key for an impl block for deterministic ordering.
1085    ///
1086    /// Combines trait name, generic params, and for-type to create a unique key.
1087    #[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    /// Render a source location reference for an item.
1101    ///
1102    /// Produces a small italicized line showing the source file and line range.
1103    /// If `source_path_config` is provided, generates a clickable markdown link
1104    /// relative to the current file's location.
1105    ///
1106    /// # Arguments
1107    ///
1108    /// * `span` - The source span from the item
1109    /// * `source_path_config` - Optional configuration for path transformation
1110    ///
1111    /// # Returns
1112    ///
1113    /// A formatted markdown string with the source location, or empty string if span is None.
1114    ///
1115    /// # Example Output (without config)
1116    ///
1117    /// ```text
1118    /// *Defined in `/home/user/.cargo/registry/src/.../serde-1.0.228/src/lib.rs:10-25`*
1119    /// ```
1120    ///
1121    /// # Example Output (with config, depth=2)
1122    ///
1123    /// ```text
1124    /// *Defined in [`serde-1.0.228/src/lib.rs:10-25`](../../.source_xxx/serde-1.0.228/src/lib.rs#L10-L25)*
1125    /// ```
1126    #[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        // Format line reference for display
1139        let line_ref = if start_line == end_line {
1140            format!("{start_line}")
1141        } else {
1142            format!("{start_line}-{end_line}")
1143        };
1144
1145        // Try to transform the path if config is provided
1146        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            // Build the prefix of "../" based on depth
1151            // +1 to exit generated_docs/ directory
1152            let prefix = "../".repeat(config.depth + 1);
1153
1154            // GitHub-style line fragment
1155            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            // Display path without the .source_xxx prefix for cleaner look
1162            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        // Fallback: just display the path as-is (no link)
1172        let filename = span.filename.display();
1173        format!("*Defined in `{filename}:{line_ref}`*\n\n")
1174    }
1175
1176    /// Render a union definition code block to markdown.
1177    ///
1178    /// Produces a heading with the union name and generics, followed by a Rust
1179    /// code block showing the union definition with all fields.
1180    ///
1181    /// # Arguments
1182    ///
1183    /// * `md` - Output markdown string
1184    /// * `name` - The union name (may differ from item.name for re-exports)
1185    /// * `u` - The union data from rustdoc
1186    /// * `krate` - The crate containing field definitions
1187    /// * `type_renderer` - Type renderer for generics and field types
1188    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    /// Render union fields documentation.
1232    ///
1233    /// Creates a "Fields" section with each field's name, type, and documentation.
1234    /// Only renders if at least one field has documentation.
1235    ///
1236    /// # Arguments
1237    ///
1238    /// * `md` - Output markdown string
1239    /// * `fields` - Field IDs from the union
1240    /// * `krate` - The crate containing field definitions
1241    /// * `type_renderer` - Type renderer for field types
1242    /// * `process_docs` - Callback to process documentation strings
1243    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        // Check if any fields have documentation
1253        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                    // Indent documentation under the field
1276                    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    /// Render a static definition code block to markdown.
1291    ///
1292    /// Produces a heading with the static name, followed by a Rust
1293    /// code block showing the static definition.
1294    ///
1295    /// # Arguments
1296    ///
1297    /// * `md` - Output markdown string
1298    /// * `name` - The static name (may differ from item.name for re-exports)
1299    /// * `s` - The static data from rustdoc
1300    /// * `type_renderer` - Type renderer for the static's type
1301    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        // Build the static declaration with modifiers
1312        let mut decl = String::new();
1313
1314        // Check for unsafe (extern block statics)
1315        if s.is_unsafe {
1316            _ = write!(decl, "unsafe ");
1317        }
1318
1319        _ = write!(decl, "static ");
1320
1321        // Check for mutable
1322        if s.is_mutable {
1323            _ = write!(decl, "mut ");
1324        }
1325
1326        // Add name and type
1327        _ = write!(decl, "{name}: {}", type_renderer.render_type(&s.type_));
1328
1329        // Add initializer expression if not empty
1330        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}
1340/// Check if a render context can resolve documentation.
1341///
1342/// This trait provides a unified way to process docs from different contexts.
1343pub trait DocsProcessor {
1344    /// Process documentation for an item, resolving intra-doc links.
1345    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            // All return values should be borrowed (no allocation)
1445            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            // Should not have a link (no [ or ])
1599            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, // e.g., "serde/index.md"
1614            };
1615            let result = RendererInternals::render_source_location(Some(&span), Some(&config));
1616
1617            // Should have markdown link
1618            assert!(result.contains('['));
1619            assert!(result.contains("]("));
1620            // Should have relative prefix (depth=1 + 1 for generated_docs = ../..)
1621            assert!(result.contains("../../.source_12345/serde-1.0.228/src/lib.rs"));
1622            // Should have line fragment
1623            assert!(result.contains("#L10-L25"));
1624            // Display path should NOT have .source prefix
1625            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            // Single line should show just one line number
1644            assert!(result.contains(":42`]"));
1645            assert!(result.contains("#L42)"));
1646            // Should NOT have range format
1647            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            // Should have one item, missing ID is skipped
1863            assert_eq!(result.required_methods.len(), 1);
1864        }
1865    }
1866}