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        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                        // Add type links if link creator is provided
856                        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                        // Extract and render method documentation
866                        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                                // Indent the summary for proper markdown rendering under the list item
872                                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                        // Add anchor for associated constants if parent type is known
883                        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                        // Add anchor for associated types if parent type is known
906                        // Use impl_item_anchor to include trait name for uniqueness
907                        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    /// Extract method documentation summary for impl blocks.
939    ///
940    /// The extraction strategy is:
941    /// 1. If `full_method_docs` is true, return the entire documentation
942    /// 2. If the docs contain code examples (triple-backtick blocks), return full docs to preserve them
943    /// 3. Otherwise, extract just the first paragraph (lines until first blank line)
944    ///
945    /// This ensures important code examples are never lost while keeping summaries
946    /// concise for methods without examples.
947    fn extract_method_summary(docs: &str, full_method_docs: bool) -> String {
948        // If full docs requested, return everything
949        if full_method_docs {
950            return docs.to_string();
951        }
952
953        // Auto-expand if docs contain code examples (``` blocks)
954        // This preserves important usage examples that would otherwise be truncated
955        if docs.contains("```") {
956            return docs.to_string();
957        }
958
959        // Extract first paragraph: lines until first blank line
960        // This captures the essential description without examples
961        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    /// Render type links for a function signature inline (for impl methods).
971    ///
972    /// This is a helper that collects types from function signatures and
973    /// creates links for resolvable types, outputting them on the same line.
974    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        // Collect from parameters
987        for (_, ty) in &f.sig.inputs {
988            all_types.extend(type_renderer.collect_linkable_types(ty));
989        }
990
991        // Collect from return type
992        if let Some(output) = &f.sig.output {
993            all_types.extend(type_renderer.collect_linkable_types(output));
994        }
995
996        // Deduplicate by name (keep first occurrence)
997        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        // Create links for resolvable types
1004        let links: Vec<String> = unique_types
1005            .iter()
1006            .filter_map(|(_, id)| create_link(*id))
1007            .collect();
1008
1009        // Output inline if we have links
1010        if !links.is_empty() {
1011            _ = write!(md, " — {}", links.join(", "));
1012        }
1013    }
1014
1015    /// Render a function signature within an impl block.
1016    ///
1017    /// Renders as a bullet point with the full signature including modifiers.
1018    /// If `parent_type_name` is provided, includes a hidden anchor for deep linking.
1019    /// The `impl_ctx` parameter ensures unique anchors when the same method name
1020    /// appears in multiple trait implementations (e.g., `fmt` in Debug and Display).
1021    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        // Add anchor for deep linking if parent type is known
1054        // Use impl_item_anchor to include trait name for unique anchors
1055        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    /// Append processed documentation to markdown.
1078    ///
1079    /// Helper function to add documentation with consistent formatting.
1080    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    /// Render the opening of a collapsible `<details>` block with a summary.
1088    ///
1089    /// Produces HTML that creates a collapsible section in markdown. Use with
1090    /// [`render_collapsible_end`] to close the block.
1091    ///
1092    /// # Arguments
1093    ///
1094    /// * `summary` - The text to display in the summary line (clickable header)
1095    ///
1096    /// # Example
1097    ///
1098    /// ```
1099    /// use cargo_docs_md::generator::render_shared::RendererInternals;
1100    ///
1101    /// let start = RendererInternals::render_collapsible_start("Derived Traits (9 implementations)");
1102    /// assert!(start.contains("<details>"));
1103    /// assert!(start.contains("<summary>Derived Traits (9 implementations)</summary>"));
1104    /// ```
1105    #[must_use]
1106    pub fn render_collapsible_start(summary: &str) -> String {
1107        format!("<details>\n<summary>{summary}</summary>\n\n")
1108    }
1109
1110    /// Render the closing of a collapsible `<details>` block.
1111    ///
1112    /// Returns a static string to close a block opened with [`render_collapsible_start`].
1113    ///
1114    /// # Example
1115    ///
1116    /// ```
1117    /// use cargo_docs_md::generator::render_shared::RendererInternals;
1118    ///
1119    /// assert_eq!(RendererInternals::render_collapsible_end(), "\n</details>\n\n");
1120    /// ```
1121    #[must_use]
1122    pub const fn render_collapsible_end() -> &'static str {
1123        "\n</details>\n\n"
1124    }
1125
1126    /// Generate a sort key for an impl block for deterministic ordering.
1127    ///
1128    /// Combines trait name, generic params, and for-type to create a unique key.
1129    #[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    /// Render a source location reference for an item.
1143    ///
1144    /// Produces a small italicized line showing the source file and line range.
1145    /// If `source_path_config` is provided, generates a clickable markdown link
1146    /// relative to the current file's location.
1147    ///
1148    /// # Arguments
1149    ///
1150    /// * `span` - The source span from the item
1151    /// * `source_path_config` - Optional configuration for path transformation
1152    ///
1153    /// # Returns
1154    ///
1155    /// A formatted markdown string with the source location, or empty string if span is None.
1156    ///
1157    /// # Example Output (without config)
1158    ///
1159    /// ```text
1160    /// *Defined in `/home/user/.cargo/registry/src/.../serde-1.0.228/src/lib.rs:10-25`*
1161    /// ```
1162    ///
1163    /// # Example Output (with config, depth=2)
1164    ///
1165    /// ```text
1166    /// *Defined in [`serde-1.0.228/src/lib.rs:10-25`](../../.source_xxx/serde-1.0.228/src/lib.rs#L10-L25)*
1167    /// ```
1168    #[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        // Format line reference for display
1181        let line_ref = if start_line == end_line {
1182            format!("{start_line}")
1183        } else {
1184            format!("{start_line}-{end_line}")
1185        };
1186
1187        // Try to transform the path if config is provided
1188        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            // Build the prefix of "../" based on depth
1193            // +1 to exit generated_docs/ directory
1194            let prefix = "../".repeat(config.depth + 1);
1195
1196            // GitHub-style line fragment
1197            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            // Display path without the .source_xxx prefix for cleaner look
1204            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        // Fallback: just display the path as-is (no link)
1214        let filename = span.filename.display();
1215        format!("*Defined in `{filename}:{line_ref}`*\n\n")
1216    }
1217
1218    /// Render a union definition code block to markdown.
1219    ///
1220    /// Produces a heading with the union name and generics, followed by a Rust
1221    /// code block showing the union definition with all fields.
1222    ///
1223    /// # Arguments
1224    ///
1225    /// * `md` - Output markdown string
1226    /// * `name` - The union name (may differ from item.name for re-exports)
1227    /// * `u` - The union data from rustdoc
1228    /// * `krate` - The crate containing field definitions
1229    /// * `type_renderer` - Type renderer for generics and field types
1230    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    /// Render union fields documentation.
1274    ///
1275    /// Creates a "Fields" section with each field's name, type, and documentation.
1276    /// Only renders if at least one field has documentation.
1277    ///
1278    /// # Arguments
1279    ///
1280    /// * `md` - Output markdown string
1281    /// * `fields` - Field IDs from the union
1282    /// * `krate` - The crate containing field definitions
1283    /// * `type_renderer` - Type renderer for field types
1284    /// * `process_docs` - Callback to process documentation strings
1285    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        // Check if any fields have documentation
1295        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                    // Indent documentation under the field
1318                    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    /// Render a static definition code block to markdown.
1333    ///
1334    /// Produces a heading with the static name, followed by a Rust
1335    /// code block showing the static definition.
1336    ///
1337    /// # Arguments
1338    ///
1339    /// * `md` - Output markdown string
1340    /// * `name` - The static name (may differ from item.name for re-exports)
1341    /// * `s` - The static data from rustdoc
1342    /// * `type_renderer` - Type renderer for the static's type
1343    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        // Build the static declaration with modifiers
1354        let mut decl = String::new();
1355
1356        // Check for unsafe (extern block statics)
1357        if s.is_unsafe {
1358            _ = write!(decl, "unsafe ");
1359        }
1360
1361        _ = write!(decl, "static ");
1362
1363        // Check for mutable
1364        if s.is_mutable {
1365            _ = write!(decl, "mut ");
1366        }
1367
1368        // Add name and type
1369        _ = write!(decl, "{name}: {}", type_renderer.render_type(&s.type_));
1370
1371        // Add initializer expression if not empty
1372        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}
1382/// Check if a render context can resolve documentation.
1383///
1384/// This trait provides a unified way to process docs from different contexts.
1385pub trait DocsProcessor {
1386    /// Process documentation for an item, resolving intra-doc links.
1387    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            // All return values should be borrowed (no allocation)
1487            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            // Should not have a link (no [ or ])
1641            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, // e.g., "serde/index.md"
1656            };
1657            let result = RendererInternals::render_source_location(Some(&span), Some(&config));
1658
1659            // Should have markdown link
1660            assert!(result.contains('['));
1661            assert!(result.contains("]("));
1662            // Should have relative prefix (depth=1 + 1 for generated_docs = ../..)
1663            assert!(result.contains("../../.source_12345/serde-1.0.228/src/lib.rs"));
1664            // Should have line fragment
1665            assert!(result.contains("#L10-L25"));
1666            // Display path should NOT have .source prefix
1667            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            // Single line should show just one line number
1686            assert!(result.contains(":42`]"));
1687            assert!(result.contains("#L42)"));
1688            // Should NOT have range format
1689            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            // Should have one item, missing ID is skipped
1905            assert_eq!(result.required_methods.len(), 1);
1906        }
1907    }
1908}