cargo_docs_md/multi_crate/
context.rs

1//! Multi-crate generation context.
2//!
3//! This module provides [`MultiCrateContext`] which holds shared state
4//! during multi-crate documentation generation, and [`SingleCrateView`]
5//! which provides a single-crate interface for existing rendering code.
6
7use std::collections::HashMap;
8use std::fmt::Write;
9use std::path::Path;
10use std::sync::LazyLock;
11
12use regex::Regex;
13use rustdoc_types::{Crate, Id, Impl, Item, ItemEnum, Visibility};
14use tracing::{debug, instrument, trace};
15
16use crate::Args;
17use crate::generator::config::RenderConfig;
18use crate::generator::doc_links::DocLinkUtils;
19use crate::generator::render_shared::SourcePathConfig;
20use crate::generator::{ItemAccess, ItemFilter, LinkResolver};
21use crate::linker::{AnchorUtils, LinkRegistry};
22use crate::multi_crate::{CrateCollection, UnifiedLinkRegistry};
23use crate::utils::PathUtils;
24
25/// Regex for backtick code links: [`Name`] not followed by ( or [
26static BACKTICK_LINK_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[`([^`]+)`\]").unwrap());
27
28/// Regex for plain links [name] where name is `snake_case`
29static PLAIN_LINK_RE: LazyLock<Regex> =
30    LazyLock::new(|| Regex::new(r"\[([a-z][a-z0-9_]*)\]").unwrap());
31
32/// Shared context for multi-crate documentation generation.
33///
34/// Holds references to all crates, the unified link registry, and
35/// CLI configuration. Used by [`MultiCrateGenerator`] to coordinate
36/// generation across crates.
37///
38/// [`MultiCrateGenerator`]: crate::multi_crate::MultiCrateGenerator
39pub struct MultiCrateContext<'a> {
40    /// All crates being documented.
41    crates: &'a CrateCollection,
42
43    /// Unified link registry for cross-crate resolution.
44    registry: UnifiedLinkRegistry,
45
46    /// CLI arguments.
47    args: &'a Args,
48
49    /// Rendering configuration options.
50    config: RenderConfig,
51
52    /// Pre-computed cross-crate impl blocks.
53    ///
54    /// Maps target crate name -> type name -> impl blocks from other crates.
55    /// This is computed once during construction rather than per-view.
56    cross_crate_impls: HashMap<String, HashMap<String, Vec<&'a Impl>>>,
57
58    /// Base source path configuration for transforming cargo registry paths.
59    ///
60    /// `None` if source locations are disabled or no `.source_*` dir detected.
61    source_path_config: Option<SourcePathConfig>,
62}
63
64impl<'a> MultiCrateContext<'a> {
65    /// Create a new multi-crate context.
66    ///
67    /// Builds the unified link registry and pre-computes cross-crate impls.
68    ///
69    /// # Arguments
70    ///
71    /// * `crates` - Collection of parsed crates
72    /// * `args` - CLI arguments
73    /// * `config` - Rendering configuration options
74    #[must_use]
75    #[instrument(skip(crates, args, config), fields(crate_count = crates.names().len()))]
76    pub fn new(crates: &'a CrateCollection, args: &'a Args, config: RenderConfig) -> Self {
77        debug!("Creating multi-crate context");
78
79        let primary = args.primary_crate.as_deref();
80        let registry = UnifiedLinkRegistry::build(crates, primary);
81
82        // Pre-compute cross-crate impls for all crates
83        debug!("Building cross-crate impl map");
84        let cross_crate_impls = Self::build_cross_crate_impls(crates);
85
86        // Build source path config if source_locations is enabled and we have a source_dir
87        let source_path_config = if config.include_source.source_locations {
88            config
89                .include_source
90                .source_dir
91                .as_ref()
92                .map(|dir| SourcePathConfig::new(dir, ""))
93        } else {
94            None
95        };
96
97        debug!(
98            cross_crate_impl_count = cross_crate_impls.values().map(HashMap::len).sum::<usize>(),
99            "Multi-crate context created"
100        );
101
102        Self {
103            crates,
104            registry,
105            args,
106            config,
107            cross_crate_impls,
108            source_path_config,
109        }
110    }
111
112    /// Set the source directory for path transformation.
113    ///
114    /// This can be called after construction if a `.source_*` directory
115    /// is detected or specified via CLI. Only has effect if `source_locations`
116    /// is enabled in the config.
117    pub fn set_source_dir(&mut self, source_dir: &Path) {
118        if self.config.include_source.source_locations {
119            self.source_path_config = Some(SourcePathConfig::new(source_dir, ""));
120        }
121    }
122
123    /// Get source path config for a specific file.
124    ///
125    /// Returns `None` if source locations are disabled or no source dir configured.
126    #[must_use]
127    pub fn source_path_config_for_file(&self, current_file: &str) -> Option<SourcePathConfig> {
128        self.source_path_config
129            .as_ref()
130            .map(|base| base.with_depth(current_file))
131    }
132
133    /// Build the cross-crate impl map for all crates.
134    ///
135    /// Scans all crates once and groups impl blocks by their target crate
136    /// and type name. This avoids O(n*m) scanning per view creation.
137    fn build_cross_crate_impls(
138        crates: &'a CrateCollection,
139    ) -> HashMap<String, HashMap<String, Vec<&'a Impl>>> {
140        let mut result: HashMap<String, HashMap<String, Vec<&'a Impl>>> = HashMap::new();
141
142        // Initialize empty maps for all crates
143        for crate_name in crates.names() {
144            result.insert(crate_name.clone(), HashMap::new());
145        }
146
147        // Scan all crates for impl blocks
148        for (source_crate, krate) in crates.iter() {
149            for item in krate.index.values() {
150                if let ItemEnum::Impl(impl_block) = &item.inner {
151                    // Skip synthetic impls
152                    if impl_block.is_synthetic {
153                        continue;
154                    }
155
156                    // Get the target type path
157                    if let Some(type_path) = Self::get_impl_target_path(impl_block)
158                        && let Some(target_crate) = type_path.split("::").next()
159                    {
160                        // Skip if targeting same crate (not cross-crate)
161                        if target_crate == source_crate {
162                            continue;
163                        }
164
165                        // Only add if target crate is in our collection
166                        if let Some(type_map) = result.get_mut(target_crate) {
167                            // Extract the type name (last segment)
168                            let type_name = type_path
169                                .split("::")
170                                .last()
171                                .unwrap_or(&type_path)
172                                .to_string();
173
174                            type_map.entry(type_name).or_default().push(impl_block);
175                        }
176                    }
177                }
178            }
179        }
180
181        result
182    }
183
184    /// Get the crate collection.
185    #[must_use]
186    pub const fn crates(&self) -> &CrateCollection {
187        self.crates
188    }
189
190    /// Get the unified link registry.
191    #[must_use]
192    pub const fn registry(&self) -> &UnifiedLinkRegistry {
193        &self.registry
194    }
195
196    /// Get CLI arguments.
197    #[must_use]
198    pub const fn args(&self) -> &Args {
199        self.args
200    }
201
202    /// Create a single-crate view for rendering one crate.
203    ///
204    /// This bridges multi-crate mode to existing single-crate rendering
205    /// code by providing a compatible interface that uses the unified
206    /// registry for cross-crate link resolution.
207    #[must_use]
208    pub fn single_crate_view(&'a self, crate_name: &str) -> Option<SingleCrateView<'a>> {
209        // Use get_with_name to get the crate name with the collection's lifetime
210        let (name, krate) = self.crates.get_with_name(crate_name)?;
211
212        Some(SingleCrateView::new(
213            name,
214            krate,
215            &self.registry,
216            self.args,
217            self,
218        ))
219    }
220
221    /// Find an item across all crates by ID.
222    ///
223    /// Searches through all crates in the collection to find an item with
224    /// the given ID. This is useful for resolving re-exports that point to
225    /// items in external crates.
226    ///
227    /// # Returns
228    ///
229    /// A tuple of `(crate_name, item)` if found, or `None` if the item
230    /// doesn't exist in any crate.
231    #[must_use]
232    pub fn find_item(&self, id: &Id) -> Option<(&str, &Item)> {
233        for (crate_name, krate) in self.crates.iter() {
234            if let Some(item) = krate.index.get(id) {
235                return Some((crate_name, item));
236            }
237        }
238        None
239    }
240
241    /// Get pre-computed cross-crate impl blocks for a target crate.
242    ///
243    /// Returns a map from type name to impl blocks from other crates.
244    /// This data is pre-computed during context construction for efficiency.
245    ///
246    /// # Returns
247    ///
248    /// Reference to the type-name -> impl-blocks map, or `None` if the
249    /// crate is not in the collection.
250    #[must_use]
251    pub fn get_cross_crate_impls(
252        &self,
253        target_crate: &str,
254    ) -> Option<&HashMap<String, Vec<&'a Impl>>> {
255        self.cross_crate_impls.get(target_crate)
256    }
257
258    /// Get the target type path for an impl block.
259    fn get_impl_target_path(impl_block: &Impl) -> Option<String> {
260        use rustdoc_types::Type;
261
262        match &impl_block.for_ {
263            Type::ResolvedPath(path) => Some(path.path.clone()),
264            _ => None,
265        }
266    }
267}
268
269/// View of a single crate within multi-crate context.
270///
271/// Provides an interface similar to [`GeneratorContext`] but uses
272/// [`UnifiedLinkRegistry`] for cross-crate link resolution. This
273/// allows existing rendering code to work with minimal changes.
274///
275/// [`GeneratorContext`]: crate::generator::GeneratorContext
276pub struct SingleCrateView<'a> {
277    /// Name of this crate (borrowed from the context).
278    crate_name: &'a str,
279
280    /// The crate being rendered.
281    krate: &'a Crate,
282
283    /// Unified registry for link resolution.
284    registry: &'a UnifiedLinkRegistry,
285
286    /// CLI arguments.
287    args: &'a Args,
288
289    /// Reference to the parent multi-crate context for cross-crate lookups.
290    ctx: &'a MultiCrateContext<'a>,
291
292    /// Map from type ID to impl blocks (local crate only).
293    impl_map: HashMap<Id, Vec<&'a Impl>>,
294
295    /// Reference to pre-computed cross-crate impl blocks from context.
296    /// Maps type name to impl blocks from other crates.
297    cross_crate_impls: Option<&'a HashMap<String, Vec<&'a Impl>>>,
298
299    /// Map from type name to type ID for cross-crate impl lookup.
300    type_name_to_id: HashMap<String, Id>,
301}
302
303impl<'a> SingleCrateView<'a> {
304    /// Create a new single-crate view.
305    fn new(
306        crate_name: &'a str,
307        krate: &'a Crate,
308        registry: &'a UnifiedLinkRegistry,
309        args: &'a Args,
310        ctx: &'a MultiCrateContext<'a>,
311    ) -> Self {
312        // Get reference to pre-computed cross-crate impls
313        let cross_crate_impls = ctx.get_cross_crate_impls(crate_name);
314
315        let mut view = Self {
316            crate_name,
317            krate,
318            registry,
319            args,
320            ctx,
321            impl_map: HashMap::new(),
322            cross_crate_impls,
323            type_name_to_id: HashMap::new(),
324        };
325
326        view.build_impl_map();
327        view.build_type_name_map();
328
329        view
330    }
331
332    /// Build the impl map for all types.
333    ///
334    /// Uses the `impls` field on Struct/Enum/Union items directly rather than
335    /// scanning all items and checking the `for_` field. This provides clearer
336    /// semantics and leverages `rustdoc_types` structured data.
337    fn build_impl_map(&mut self) {
338        self.impl_map.clear();
339
340        // Iterate over all types that can have impl blocks and collect their impls
341        for (type_id, item) in &self.krate.index {
342            let impl_ids: &[Id] = match &item.inner {
343                ItemEnum::Struct(s) => &s.impls,
344                ItemEnum::Enum(e) => &e.impls,
345                ItemEnum::Union(u) => &u.impls,
346                _ => continue,
347            };
348
349            // Look up each impl block and add to the map
350            for impl_id in impl_ids {
351                if let Some(impl_item) = self.krate.index.get(impl_id)
352                    && let ItemEnum::Impl(impl_block) = &impl_item.inner
353                {
354                    self.impl_map.entry(*type_id).or_default().push(impl_block);
355                }
356            }
357        }
358
359        // Sort impl blocks for deterministic output
360        for impls in self.impl_map.values_mut() {
361            impls.sort_by_key(|i| Self::impl_sort_key(i));
362            // Deduplicate impls with the same sort key
363            impls.dedup_by(|a, b| Self::impl_sort_key(a) == Self::impl_sort_key(b));
364        }
365    }
366
367    /// Build a map from type name to type ID.
368    ///
369    /// This is used to look up cross-crate impls by type name.
370    fn build_type_name_map(&mut self) {
371        self.type_name_to_id.clear();
372
373        for (id, item) in &self.krate.index {
374            if let Some(name) = &item.name {
375                // Only include types that can have impls
376                match &item.inner {
377                    ItemEnum::Struct(_) | ItemEnum::Enum(_) | ItemEnum::Union(_) => {
378                        self.type_name_to_id.insert(name.clone(), *id);
379                    },
380                    _ => {},
381                }
382            }
383        }
384    }
385
386    /// Generate a sort key for impl blocks.
387    fn impl_sort_key(impl_block: &Impl) -> (u8, String) {
388        // Extract trait name from the path (last segment)
389        let trait_name: String = impl_block
390            .trait_
391            .as_ref()
392            .map_or("", |p| PathUtils::short_name(&p.path))
393            .to_string();
394
395        let priority = if impl_block.trait_.is_none() {
396            0 // Inherent impls first
397        } else if trait_name.starts_with("From") || trait_name.starts_with("Into") {
398            1 // Conversion traits
399        } else if trait_name.starts_with("De") || trait_name.starts_with("Se") {
400            3 // Serde traits last
401        } else {
402            2 // Other traits
403        };
404
405        (priority, trait_name)
406    }
407
408    /// Get the crate name.
409    #[must_use]
410    pub const fn crate_name(&self) -> &str {
411        self.crate_name
412    }
413
414    /// Get the crate being rendered.
415    #[must_use]
416    pub const fn krate(&self) -> &Crate {
417        self.krate
418    }
419
420    /// Get the unified registry.
421    #[must_use]
422    pub const fn registry(&self) -> &UnifiedLinkRegistry {
423        self.registry
424    }
425
426    /// Get CLI arguments.
427    #[must_use]
428    pub const fn args(&self) -> &Args {
429        self.args
430    }
431
432    /// Get impl blocks for a type (local crate only).
433    #[must_use]
434    pub fn get_impls(&self, id: Id) -> Option<&Vec<&'a Impl>> {
435        self.impl_map.get(&id)
436    }
437
438    /// Get all impl blocks for a type, including cross-crate impls.
439    ///
440    /// This method merges local impls (from this crate) with impls from
441    /// other crates that implement traits for this type.
442    #[must_use]
443    pub fn get_all_impls(&self, id: Id) -> Vec<&'a Impl> {
444        let mut result = Vec::new();
445
446        // Add local impls
447        if let Some(local_impls) = self.impl_map.get(&id) {
448            result.extend(local_impls.iter().copied());
449        }
450
451        // Add cross-crate impls by looking up the type name
452        if let Some(item) = self.krate.index.get(&id)
453            && let Some(type_name) = &item.name
454            && let Some(cross_crate_map) = self.cross_crate_impls
455            && let Some(external_impls) = cross_crate_map.get(type_name)
456        {
457            result.extend(external_impls.iter().copied());
458        }
459
460        result
461    }
462
463    /// Get impl blocks for a type from a specific crate.
464    ///
465    /// This is used for cross-crate re-exports where we need to look up
466    /// impl blocks from the source crate rather than the current crate.
467    ///
468    /// # Arguments
469    ///
470    /// * `id` - The ID of the type to get impls for
471    /// * `source_krate` - The crate to look up impls from
472    ///
473    /// # Returns
474    ///
475    /// A vector of impl blocks found in the source crate for the given type ID.
476    #[must_use]
477    pub fn get_impls_from_crate(&self, id: Id, source_krate: &'a Crate) -> Vec<&'a Impl> {
478        let mut result = Vec::new();
479
480        // Scan the source crate for impl blocks targeting this ID
481        for item in source_krate.index.values() {
482            if let ItemEnum::Impl(impl_block) = &item.inner
483                && let Some(target_id) = Self::get_impl_target_id_from_type(&impl_block.for_)
484                && target_id == id
485            {
486                result.push(impl_block);
487            }
488        }
489
490        // Also include cross-crate impls if this is our current crate
491        if std::ptr::eq(source_krate, self.krate)
492            && let Some(item) = self.krate.index.get(&id)
493            && let Some(type_name) = &item.name
494            && let Some(cross_crate_map) = self.cross_crate_impls
495            && let Some(external_impls) = cross_crate_map.get(type_name)
496        {
497            result.extend(external_impls.iter().copied());
498        }
499
500        result
501    }
502
503    /// Extract the target ID from a Type (for impl block matching).
504    const fn get_impl_target_id_from_type(ty: &rustdoc_types::Type) -> Option<Id> {
505        use rustdoc_types::Type;
506
507        match ty {
508            Type::ResolvedPath(path) => Some(path.id),
509            _ => None,
510        }
511    }
512
513    /// Check if an item should be included based on visibility.
514    #[must_use]
515    pub const fn should_include_item(&self, item: &rustdoc_types::Item) -> bool {
516        if self.args.exclude_private {
517            return matches!(item.visibility, Visibility::Public);
518        }
519
520        true
521    }
522
523    /// Count modules for progress reporting.
524    #[must_use]
525    pub fn count_modules(&self) -> usize {
526        self.krate
527            .index
528            .values()
529            .filter(|item| matches!(&item.inner, ItemEnum::Module(_)))
530            .count()
531    }
532
533    /// Create a markdown link using the unified registry.
534    #[must_use]
535    pub fn create_link(&self, to_crate: &str, to_id: Id, from_path: &str) -> Option<String> {
536        self.registry
537            .create_link(self.crate_name, from_path, to_crate, to_id)
538    }
539
540    /// Resolve a name to a crate and ID.
541    #[must_use]
542    pub fn resolve_name(&self, name: &str) -> Option<(String, Id)> {
543        self.registry
544            .resolve_name(name, self.crate_name)
545            .map(|(s, id)| (s.to_string(), id))
546    }
547
548    /// Look up an item across all crates by ID.
549    ///
550    /// This is useful for resolving re-exports that point to items in
551    /// external crates. First checks the local crate, then searches
552    /// all other crates in the collection.
553    ///
554    /// # Returns
555    ///
556    /// A tuple of `(crate_name, item)` if found, or `None` if the item
557    /// doesn't exist in any crate.
558    #[must_use]
559    #[instrument(skip(self), fields(crate_name = %self.crate_name), level = "trace")]
560    pub fn lookup_item_across_crates(&self, id: &Id) -> Option<(&str, &Item)> {
561        // First check local crate (fast path)
562        if let Some(item) = self.krate.index.get(id) {
563            trace!(found_in = "local", "Item found in local crate");
564            return Some((self.crate_name, item));
565        }
566
567        // Fall back to searching all crates
568        trace!("Item not in local crate, searching all crates");
569        let result = self.ctx.find_item(id);
570
571        if let Some((crate_name, _)) = &result {
572            debug!(found_in = %crate_name, "Item found in external crate");
573        } else {
574            trace!("Item not found in any crate");
575        }
576
577        result
578    }
579
580    /// Get a crate by name from the collection.
581    ///
582    /// This is useful for getting the source crate context when rendering
583    /// re-exported items from other crates.
584    ///
585    /// # Returns
586    ///
587    /// The crate if found, or `None` if no crate with that name exists.
588    #[must_use]
589    pub fn get_crate(&self, name: &str) -> Option<&Crate> {
590        self.ctx.crates.get(name)
591    }
592
593    /// Resolve a path like `regex_automata::Regex` to an item.
594    ///
595    /// This is used for external re-exports where `use_item.id` is `None`
596    /// but the source path is available.
597    ///
598    /// # Returns
599    ///
600    /// A tuple of `(source_crate, item, item_id)` if found.
601    #[must_use]
602    pub fn resolve_external_path(&self, path: &str) -> Option<(&str, &Item, Id)> {
603        let (source_crate, id) = self.registry.resolve_path(path)?;
604        let (crate_name, item) = self.ctx.find_item(&id)?;
605
606        // Verify the crate matches
607        if crate_name == source_crate {
608            Some((crate_name, item, id))
609        } else {
610            None
611        }
612    }
613
614    /// Process backtick links like `[`Span`]` to markdown links.
615    #[tracing::instrument(skip(self, docs, item_links), level = "trace", fields(file = %current_file))]
616    fn process_backtick_links(
617        &self,
618        docs: &str,
619        item_links: &HashMap<String, Id>,
620        current_file: &str,
621    ) -> String {
622        let mut result = String::with_capacity(docs.len());
623        let mut last_end = 0;
624        let mut resolved_count = 0;
625        let mut unresolved_count = 0;
626
627        for caps in BACKTICK_LINK_RE.captures_iter(docs) {
628            let full_match = caps.get(0).unwrap();
629            let match_start = full_match.start();
630            let match_end = full_match.end();
631
632            // Check if followed by ( or [ (already a link)
633            let next_char = docs[match_end..].chars().next();
634            if matches!(next_char, Some('(' | '[')) {
635                tracing::trace!(
636                    link = %full_match.as_str(),
637                    "Skipping - already has link target"
638                );
639                continue;
640            }
641
642            _ = write!(result, "{}", &docs[last_end..match_start]);
643            last_end = match_end;
644
645            let link_text = &caps[1];
646
647            // The item_links keys may have backticks (e.g., "`Visit`") or not ("Visit")
648            // Try the backtick-wrapped version first since that's what rustdoc typically uses
649            let backtick_key = format!("`{link_text}`");
650
651            // Try to resolve the link (try backtick version first, then plain)
652            if let Some(resolved) = self
653                .resolve_link(&backtick_key, item_links, current_file)
654                .or_else(|| self.resolve_link(link_text, item_links, current_file))
655            {
656                tracing::trace!(
657                    link_text = %link_text,
658                    resolved = %resolved,
659                    "Resolved backtick link"
660                );
661
662                resolved_count += 1;
663                _ = write!(result, "{}", &resolved);
664            } else {
665                tracing::trace!(
666                    link_text = %link_text,
667                    "Could not resolve backtick link, keeping as inline code"
668                );
669                unresolved_count += 1;
670
671                // Couldn't resolve - convert to plain inline code
672                _ = write!(result, "`{link_text}`");
673            }
674        }
675
676        _ = write!(result, "{}", &docs[last_end..]);
677
678        if resolved_count > 0 || unresolved_count > 0 {
679            tracing::trace!(
680                resolved = resolved_count,
681                unresolved = unresolved_count,
682                "Finished processing backtick links"
683            );
684        }
685
686        result
687    }
688
689    /// Process plain links like `[enter]` to markdown links.
690    ///
691    /// Uses the registry to resolve links to proper paths. If the item exists
692    /// in the registry, creates a link to its location. If on the current page
693    /// and has a heading anchor, uses an anchor link.
694    ///
695    /// Skips matches that are:
696    /// - Inside inline code (backticks)
697    /// - Already markdown links (followed by `(` or `[`)
698    fn process_plain_links(&self, docs: &str, current_file: &str) -> String {
699        let mut result = String::with_capacity(docs.len());
700        let mut last_end = 0;
701
702        for caps in PLAIN_LINK_RE.captures_iter(docs) {
703            let full_match = caps.get(0).unwrap();
704            let match_start = full_match.start();
705            let match_end = full_match.end();
706
707            // Check if followed by ( or [ (already a link)
708            let next_char = docs[match_end..].chars().next();
709
710            if matches!(next_char, Some('(' | '[')) {
711                continue;
712            }
713
714            // Check if inside inline code (count backticks before match)
715            let before = &docs[..match_start];
716            let backtick_count = before.chars().filter(|&c| c == '`').count();
717
718            if backtick_count % 2 == 1 {
719                // Odd number of backticks means we're inside inline code
720                continue;
721            }
722
723            _ = write!(result, "{}", &docs[last_end..match_start]);
724            last_end = match_end;
725
726            let link_text = &caps[1];
727
728            // Try to resolve via registry
729            if let Some(link) = self.resolve_plain_link(link_text, current_file) {
730                _ = write!(result, "{link}");
731            } else {
732                // Unresolved - keep as plain text
733                _ = write!(result, "[{link_text}]");
734            }
735        }
736
737        _ = write!(result, "{}", &docs[last_end..]);
738        result
739    }
740
741    /// Resolve a plain link `[name]` to a markdown link.
742    ///
743    /// Returns `Some(markdown_link)` if the item can be resolved,
744    /// `None` if it should remain as plain text.
745    #[expect(clippy::similar_names)]
746    #[tracing::instrument(skip(self), level = "trace")]
747    fn resolve_plain_link(&self, link_text: &str, current_file: &str) -> Option<String> {
748        // Try to find the item in the registry
749        let (resolved_crate, id) = self.registry.resolve_name(link_text, self.crate_name)?;
750
751        tracing::trace!(
752            resolved_crate = %resolved_crate,
753            id = ?id,
754            "Found item in registry"
755        );
756
757        // Check if this is an external re-export and try to follow it
758        let (target_crate, target_id) = self
759            .registry
760            .resolve_reexport(&resolved_crate, id)
761            .unwrap_or_else(|| (resolved_crate.clone(), id));
762
763        let followed_reexport = target_crate != resolved_crate || target_id != id;
764        if followed_reexport {
765            tracing::trace!(
766                original_crate = %resolved_crate,
767                original_id = ?id,
768                target_crate = %target_crate,
769                target_id = ?target_id,
770                "Followed re-export chain to original item"
771            );
772        }
773
774        // Get the crate data for the target (might be different from current crate)
775        let target_krate = self.ctx.crates.get(&target_crate)?;
776
777        // Get the item's path info from the target crate
778        let path_info = target_krate.paths.get(&target_id)?;
779
780        // Get the file path for this item
781        let target_path = self.registry.get_path(&target_crate, target_id)?;
782
783        // Strip crate prefix from current_file for comparison
784        let current_local = Self::strip_crate_prefix(current_file);
785
786        // Check if same file (accounting for cross-crate)
787        let is_same_file = target_crate == self.crate_name && target_path == current_local;
788
789        if is_same_file {
790            // Item is on the current page
791            if AnchorUtils::item_has_anchor(path_info.kind) {
792                // Has a heading - create anchor link
793                let anchor = AnchorUtils::slugify_anchor(link_text);
794                tracing::trace!(
795                    anchor = %anchor,
796                    kind = ?path_info.kind,
797                    "Creating same-page anchor link"
798                );
799                Some(format!("[{link_text}](#{anchor})"))
800            } else {
801                // No heading - link to page without anchor
802                tracing::trace!(
803                    kind = ?path_info.kind,
804                    "Item on same page but no heading - linking to page"
805                );
806                Some(format!("[{link_text}]()"))
807            }
808        } else {
809            // Item is in a different file (possibly different crate)
810            tracing::trace!(
811                target_crate = %target_crate,
812                target_path = %target_path,
813                "Creating cross-file link"
814            );
815            let relative =
816                self.build_markdown_link(current_file, &target_crate, target_path, link_text, None);
817            Some(relative)
818        }
819    }
820
821    /// Resolve a link text to a markdown link using the registry.
822    ///
823    /// This function attempts to convert rustdoc link syntax into valid markdown
824    /// links that work in the generated documentation.
825    ///
826    /// # Arguments
827    /// * `link_text` - The raw link target from rustdoc (e.g., "`crate::config::ConfigBuilder::method`")
828    /// * `item_links` - Map of link texts to Item IDs from rustdoc's `links` field
829    /// * `current_file` - The markdown file being generated (e.g., "ureq/index.md")
830    ///
831    /// # Returns
832    /// * `Some(markdown_link)` - A formatted markdown link like `[`text`](path.md#anchor)`
833    /// * `None` - If the link cannot be resolved (will be rendered as inline code)
834    ///
835    /// # Examples
836    ///
837    /// ```text
838    /// Input:  link_text = "crate::config::ConfigBuilder::http_status_as_error"
839    ///         current_file = "ureq/index.md"
840    /// Output: Some("[`crate::config::ConfigBuilder::http_status_as_error`](config/index.md#http_status_as_error)")
841    ///
842    /// Input:  link_text = "ConfigBuilder"
843    ///         current_file = "ureq/agent/index.md"
844    /// Output: Some("[`ConfigBuilder`](../config/index.md#configbuilder)")
845    ///
846    /// Input:  link_text = "std::io::Error"  (external crate, not in registry)
847    ///         current_file = "ureq/index.md"
848    /// Output: None  (rendered as `std::io::Error` inline code)
849    /// ```
850    #[instrument(skip(self, item_links), fields(crate_name = %self.crate_name))]
851    fn resolve_link(
852        &self,
853        link_text: &str,
854        item_links: &HashMap<String, Id>,
855        current_file: &str,
856    ) -> Option<String> {
857        // ─────────────────────────────────────────────────────────────────────
858        // Strategy 1: Try the item's links map (most accurate)
859        // ─────────────────────────────────────────────────────────────────────
860        // Rustdoc provides a `links` map on each item that maps link text to
861        // the resolved Item ID. This is the most reliable source because rustdoc
862        // has already done the name resolution.
863        //
864        // Example item_links map:
865        //   {
866        //     "ConfigBuilder" => Id(123),
867        //     "crate::config::ConfigBuilder" => Id(123),
868        //     "Agent" => Id(456)
869        //   }
870        tracing::trace!(
871            strategy = "item_links",
872            "Attempting resolution via item links map"
873        );
874        if let Some(id) = item_links.get(link_text) {
875            // We have an ID! Now convert it to a markdown path.
876            // Example: Id(123) → "config/index.md" → "[`ConfigBuilder`](config/index.md)"
877            tracing::debug!(strategy = "item_links", ?id, "Found ID in item links");
878
879            // Strip backticks from display name if present (rustdoc uses `Name` as keys)
880            let display_name = link_text.trim_matches('`');
881            if let Some(link) = self.build_link_to_id(*id, current_file, display_name, None) {
882                tracing::debug!(strategy = "item_links", link = %link, "Successfully resolved");
883
884                return Some(link);
885            }
886
887            tracing::trace!(strategy = "item_links", "ID Found but couldn't build link");
888        }
889
890        // ─────────────────────────────────────────────────────────────────────
891        // Strategy 2: Try resolving by name in the registry
892        // ─────────────────────────────────────────────────────────────────────
893        // If the item_links map didn't have this link (can happen with re-exports
894        // or manually written links), try looking up the name directly in our
895        // cross-crate registry.
896        //
897        // Example:
898        //   link_text = "Agent"
899        //   registry.resolve_name("Agent", "ureq") → Some(("ureq", Id(456)))
900        tracing::trace!(
901            strategy = "registry_name",
902            "Attempting resolution via registry name lookup"
903        );
904        if let Some((resolved_crate, id)) = self.registry.resolve_name(link_text, self.crate_name) {
905            // Only use this if:
906            // 1. Same crate (internal link), OR
907            // 2. Explicitly looks like an external reference (contains "::")
908            //
909            // This prevents accidental cross-crate linking for common names like "Error"
910            if (resolved_crate == self.crate_name || Self::looks_like_external_reference(link_text))
911                // Use build_link_to_id to follow re-exports to the original definition
912                && let Some(link) = self.build_link_to_id(id, current_file, link_text, None)
913            {
914                return Some(link);
915            }
916        }
917
918        // ─────────────────────────────────────────────────────────────────────
919        // Strategy 3: Try crate:: prefixed paths
920        // ─────────────────────────────────────────────────────────────────────
921        // Handle explicit crate-relative paths like "crate::config::ConfigBuilder::method"
922        // These are common in rustdoc comments and need special parsing.
923        //
924        // Example:
925        //   link_text = "crate::config::ConfigBuilder::http_status_as_error"
926        //   → strip prefix → "config::ConfigBuilder::http_status_as_error"
927        //   → resolve_crate_path() handles the rest
928        if let Some(path_without_crate) = link_text.strip_prefix("crate::")
929            && let Some(link) = self.resolve_crate_path(path_without_crate, link_text, current_file)
930        {
931            return Some(link);
932        }
933
934        // ─────────────────────────────────────────────────────────────────────
935        // Give up on qualified paths we can't resolve
936        // ─────────────────────────────────────────────────────────────────────
937        // If it has "::" and we still haven't resolved it, it's probably an
938        // external crate we don't have (like std, serde, tokio, etc.)
939        // Return None so it renders as inline code: `std::io::Error`
940        if link_text.contains("::") {
941            return None;
942        }
943
944        // ─────────────────────────────────────────────────────────────────────
945        // Fallback: anchor on current page (only if item has a heading)
946        // ─────────────────────────────────────────────────────────────────────
947        // For simple names without ::, check if the item exists and has a heading.
948        // Only structs, enums, traits, functions, etc. get headings.
949        // Methods, fields, and variants don't have headings (they're bullet points).
950        if let Some((_, id)) = self.registry.resolve_name(link_text, self.crate_name)
951            && let Some(path_info) = self.krate.paths.get(&id)
952        {
953            if AnchorUtils::item_has_anchor(path_info.kind) {
954                return Some(format!(
955                    "[`{link_text}`](#{})",
956                    AnchorUtils::slugify_anchor(link_text)
957                ));
958            }
959
960            // Item exists but no anchor - link to page without anchor
961            return Some(format!("[`{link_text}`]()"));
962        }
963
964        // Unknown item - return None (renders as inline code)
965        None
966    }
967
968    /// Build a link to an item by ID.
969    ///
970    /// This is the simplest path when we already have a resolved Item ID from
971    /// rustdoc's links map. We just need to look up the file path in our registry.
972    ///
973    /// # Arguments
974    /// * `id` - The rustdoc Item ID to link to
975    /// * `current_file` - Source file for relative path computation
976    /// * `display_name` - Text to show in the link
977    /// * `anchor` - Optional anchor (e.g., method name)
978    ///
979    /// # Example Transformation
980    ///
981    /// ```text
982    /// Input:
983    ///   id = Id(123)  (rustdoc's internal ID for ConfigBuilder)
984    ///   current_file = "ureq/agent/index.md"
985    ///   display_name = "ConfigBuilder"
986    ///   anchor = None
987    ///
988    /// Step 1: Look up ID in registry
989    ///   registry.get_path("ureq", Id(123)) → Some("config/index.md")
990    ///
991    /// Step 2: Build markdown link
992    ///   build_markdown_link("ureq/agent/index.md", "ureq", "config/index.md", "ConfigBuilder", None)
993    ///   → "[`ConfigBuilder`](../config/index.md)"
994    ///
995    /// Output: Some("[`ConfigBuilder`](../config/index.md)")
996    /// ```
997    #[expect(clippy::too_many_lines)]
998    #[tracing::instrument(skip(self), level = "trace")]
999    fn build_link_to_id(
1000        &self,
1001        id: Id,
1002        current_file: &str,
1003        display_name: &str,
1004        anchor: Option<&str>,
1005    ) -> Option<String> {
1006        // First: Check if this is a re-export and follow to the original definition
1007        // Re-exports don't have headings - we need to link to where the item is defined
1008        //
1009        // Method 1: Check our re_export_sources registry
1010        if let Some((original_crate, original_id)) =
1011            self.registry.resolve_reexport(self.crate_name, id)
1012        {
1013            tracing::trace!(
1014                original_crate = %original_crate,
1015                original_id = ?original_id,
1016                "Following re-export via registry to original definition"
1017            );
1018
1019            if let Some(target_path) = self.registry.get_path(&original_crate, original_id) {
1020                return Some(self.build_markdown_link(
1021                    current_file,
1022                    &original_crate,
1023                    target_path,
1024                    display_name,
1025                    anchor,
1026                ));
1027            }
1028        }
1029
1030        // Method 2: Check if the item itself is a Use item in the index
1031        if let Some(item) = self.krate.index.get(&id)
1032            && let ItemEnum::Use(use_item) = &item.inner
1033        {
1034            tracing::trace!(
1035                source = %use_item.source,
1036                target_id = ?use_item.id,
1037                "Found Use item in index"
1038            );
1039
1040            // Method 2a: If the Use item has a target ID, look up via paths
1041            // This handles cases where source is relative (e.g., "self::event::Event")
1042            // but the ID points to the actual item in another crate
1043            if let Some(target_id) = use_item.id
1044                && let Some(path_info) = self.krate.paths.get(&target_id)
1045                && let Some(external_crate) = path_info.path.first()
1046            {
1047                tracing::trace!(
1048                    external_crate = %external_crate,
1049                    path = ?path_info.path,
1050                    "Following Use item target ID to external crate"
1051                );
1052
1053                // Try to find the item in the external crate by name
1054                let item_name = path_info.path.last().unwrap_or(&path_info.path[0]);
1055
1056                if let Some((resolved_crate, resolved_id)) =
1057                    self.registry.resolve_name(item_name, external_crate)
1058                    && let Some(target_path) = self.registry.get_path(&resolved_crate, resolved_id)
1059                {
1060                    return Some(self.build_markdown_link(
1061                        current_file,
1062                        &resolved_crate,
1063                        target_path,
1064                        display_name,
1065                        anchor,
1066                    ));
1067                }
1068            }
1069
1070            // Method 2b: Try to resolve the source path directly
1071            if !use_item.source.is_empty()
1072                && let Some((original_crate, original_id)) =
1073                    self.registry.resolve_path(&use_item.source)
1074                && let Some(target_path) = self.registry.get_path(&original_crate, original_id)
1075            {
1076                return Some(self.build_markdown_link(
1077                    current_file,
1078                    &original_crate,
1079                    target_path,
1080                    display_name,
1081                    anchor,
1082                ));
1083            }
1084        }
1085
1086        // Strategy 1: Try to find the ID in the current crate
1087        if let Some(target_path) = self.registry.get_path(self.crate_name, id) {
1088            tracing::trace!(
1089                strategy = "current_crate",
1090                crate_name = %self.crate_name,
1091                target_path = %target_path,
1092                "Found ID in current crate registry"
1093            );
1094            return Some(self.build_markdown_link(
1095                current_file,
1096                self.crate_name,
1097                target_path,
1098                display_name,
1099                anchor,
1100            ));
1101        }
1102
1103        tracing::trace!(
1104            strategy = "current_crate",
1105            crate_name = %self.crate_name,
1106            "ID not found in current crate, checking paths for external reference"
1107        );
1108
1109        // Strategy 2: ID not in current crate - check if it's an external item via paths
1110        // The paths map can contain IDs from other crates (for re-exports/cross-refs)
1111        if let Some(path_info) = self.krate.paths.get(&id) {
1112            // path_info.path is like ["tracing_core", "field", "Visit"]
1113            // First element is the crate name
1114            let path_str = path_info.path.join("::");
1115            tracing::trace!(
1116                strategy = "external_paths",
1117                path = %path_str,
1118                kind = ?path_info.kind,
1119                "Found path info for external item"
1120            );
1121
1122            if let Some(external_crate) = path_info.path.first() {
1123                // Strategy 2a: Try direct ID lookup in external crate
1124                if let Some(target_path) = self.registry.get_path(external_crate, id) {
1125                    tracing::trace!(
1126                        strategy = "external_direct_id",
1127                        external_crate = %external_crate,
1128                        target_path = %target_path,
1129                        "Found external item by direct ID lookup"
1130                    );
1131
1132                    return Some(self.build_markdown_link(
1133                        current_file,
1134                        external_crate,
1135                        target_path,
1136                        display_name,
1137                        anchor,
1138                    ));
1139                }
1140
1141                // Strategy 2b: External crate uses different ID - try name-based lookup
1142                // This handles cross-crate references where IDs are crate-local
1143                let item_name = path_info.path.last()?;
1144                tracing::trace!(
1145                    strategy = "external_name_lookup",
1146                    external_crate = %external_crate,
1147                    item_name = %item_name,
1148                    "Attempting name-based lookup in external crate"
1149                );
1150
1151                if let Some((resolved_crate, resolved_id)) =
1152                    self.registry.resolve_name(item_name, external_crate)
1153                {
1154                    tracing::trace!(
1155                        strategy = "external_name_lookup",
1156                        resolved_crate = %resolved_crate,
1157                        resolved_id = ?resolved_id,
1158                        "Name resolved to crate and ID"
1159                    );
1160
1161                    if let Some(target_path) = self.registry.get_path(&resolved_crate, resolved_id)
1162                    {
1163                        tracing::debug!(
1164                            strategy = "external_name_lookup",
1165                            resolved_crate = %resolved_crate,
1166                            target_path = %target_path,
1167                            "Successfully resolved external item"
1168                        );
1169                        return Some(self.build_markdown_link(
1170                            current_file,
1171                            &resolved_crate,
1172                            target_path,
1173                            display_name,
1174                            anchor,
1175                        ));
1176                    }
1177
1178                    tracing::trace!(
1179                        strategy = "external_name_lookup",
1180                        resolved_crate = %resolved_crate,
1181                        resolved_id = ?resolved_id,
1182                        "Name resolved but no path found in registry"
1183                    );
1184                } else {
1185                    tracing::trace!(
1186                        strategy = "external_name_lookup",
1187                        external_crate = %external_crate,
1188                        item_name = %item_name,
1189                        "Name not found in external crate registry"
1190                    );
1191                }
1192            }
1193        } else {
1194            tracing::trace!(strategy = "external_paths", "No path info found for ID");
1195        }
1196
1197        tracing::trace!("All strategies exhausted, returning None");
1198        None
1199    }
1200
1201    /// Resolve `crate::path::Item` or `crate::path::Item::method` patterns.
1202    ///
1203    /// This handles the common rustdoc pattern where docs reference items using
1204    /// crate-relative paths. The tricky part is distinguishing between:
1205    /// - `crate::module::Type` (link to Type, no anchor)
1206    /// - `crate::module::Type::method` (link to Type with #method anchor)
1207    /// - `crate::module::Type::Variant` (link to Type with #Variant anchor)
1208    ///
1209    /// # Arguments
1210    /// * `path_without_crate` - The path after stripping "`crate::`" prefix
1211    /// * `display_name` - Full original text for display (includes "`crate::`")
1212    /// * `current_file` - Source file for relative path computation
1213    ///
1214    /// # Example Transformation
1215    ///
1216    /// ```text
1217    /// Input:
1218    ///   path_without_crate = "config::ConfigBuilder::http_status_as_error"
1219    ///   display_name = "crate::config::ConfigBuilder::http_status_as_error"
1220    ///   current_file = "ureq/index.md"
1221    ///
1222    /// Step 1: Split into type path and anchor
1223    ///   split_type_and_anchor("config::ConfigBuilder::http_status_as_error")
1224    ///   → ("config::ConfigBuilder", Some("http_status_as_error"))
1225    ///   (lowercase "http_status_as_error" indicates a method)
1226    ///
1227    /// Step 2: Extract the type name (last segment of type path)
1228    ///   "config::ConfigBuilder".rsplit("::").next() → "ConfigBuilder"
1229    ///
1230    /// Step 3: Resolve type name in registry
1231    ///   registry.resolve_name("ConfigBuilder", "ureq") → Some(("ureq", Id(123)))
1232    ///   registry.get_path("ureq", Id(123)) → Some("config/index.md")
1233    ///
1234    /// Step 4: Build markdown link with anchor
1235    ///   build_markdown_link("ureq/index.md", "ureq", "config/index.md",
1236    ///                       "crate::config::ConfigBuilder::http_status_as_error",
1237    ///                       Some("http_status_as_error"))
1238    ///   → "[`crate::config::ConfigBuilder::http_status_as_error`](config/index.md#http_status_as_error)"
1239    ///
1240    /// Output: Some("[`crate::config::ConfigBuilder::http_status_as_error`](config/index.md#http_status_as_error)")
1241    /// ```
1242    fn resolve_crate_path(
1243        &self,
1244        path_without_crate: &str,
1245        display_name: &str,
1246        current_file: &str,
1247    ) -> Option<String> {
1248        // Step 1: Separate the type path from any method/variant anchor
1249        // "config::ConfigBuilder::method" → ("config::ConfigBuilder", Some("method"))
1250        let (type_path, anchor) = Self::split_type_and_anchor(path_without_crate);
1251
1252        // Step 2: Get just the type name (we'll search for this in the registry)
1253        // "config::ConfigBuilder" → "ConfigBuilder"
1254        let type_name = PathUtils::short_name(type_path);
1255
1256        // Step 3: Look up the type in our cross-crate registry
1257        // This finds which crate owns "ConfigBuilder" and what file it's in
1258        let (resolved_crate, id) = self.registry.resolve_name(type_name, self.crate_name)?;
1259        let target_path = self.registry.get_path(&resolved_crate, id)?;
1260
1261        // Step 4: Build the final markdown link
1262        Some(self.build_markdown_link(
1263            current_file,
1264            &resolved_crate,
1265            target_path,
1266            display_name,
1267            anchor,
1268        ))
1269    }
1270
1271    /// Split `config::ConfigBuilder::method` into (`config::ConfigBuilder`, Some("method")).
1272    ///
1273    /// Detects methods (lowercase) and enum variants (`Type::Variant` pattern).
1274    ///
1275    /// # Detection Rules
1276    ///
1277    /// 1. **Methods/fields**: Last segment starts with lowercase
1278    ///    - `Type::method` → (Type, method)
1279    ///    - `mod::Type::field_name` → (`mod::Type`, `field_name`)
1280    ///
1281    /// 2. **Enum variants**: Two consecutive uppercase segments
1282    ///    - `Option::Some` → (Option, Some)
1283    ///    - `mod::Error::IoError` → (`mod::Error`, `IoError`)
1284    ///
1285    /// 3. **Nested types**: Uppercase but no uppercase predecessor
1286    ///    - `mod::OuterType::InnerType` → (`mod::OuterType::InnerType`, None)
1287    ///
1288    /// # Examples
1289    ///
1290    /// ```text
1291    /// "ConfigBuilder::http_status_as_error"
1292    ///   Last segment "http_status_as_error" starts lowercase → method
1293    ///   → ("ConfigBuilder", Some("http_status_as_error"))
1294    ///
1295    /// "config::ConfigBuilder::new"
1296    ///   Last segment "new" starts lowercase → method
1297    ///   → ("config::ConfigBuilder", Some("new"))
1298    ///
1299    /// "Option::Some"
1300    ///   "Option" uppercase, "Some" uppercase → enum variant
1301    ///   → ("Option", Some("Some"))
1302    ///
1303    /// "error::Error::Io"
1304    ///   "Error" uppercase, "Io" uppercase → enum variant
1305    ///   → ("error::Error", Some("Io"))
1306    ///
1307    /// "config::ConfigBuilder"
1308    ///   "config" lowercase, "ConfigBuilder" uppercase → not a variant
1309    ///   → ("config::ConfigBuilder", None)
1310    ///
1311    /// "Vec"
1312    ///   No "::" separator
1313    ///   → ("Vec", None)
1314    /// ```
1315    fn split_type_and_anchor(path: &str) -> (&str, Option<&str>) {
1316        // Find the last "::" separator
1317        // "config::ConfigBuilder::method" → sep_pos = 21 (before "method")
1318        let Some(sep_pos) = path.rfind("::") else {
1319            // No separator, just a simple name like "Vec"
1320            return (path, None);
1321        };
1322
1323        // Split into: rest = "config::ConfigBuilder", last = "method"
1324        let last = &path[sep_pos + 2..]; // Skip the "::"
1325        let rest = &path[..sep_pos];
1326
1327        // ─────────────────────────────────────────────────────────────────────
1328        // Rule 1: Lowercase last segment = method/field
1329        // ─────────────────────────────────────────────────────────────────────
1330        // Methods and fields in Rust are snake_case by convention
1331        if last.starts_with(|c: char| c.is_lowercase()) {
1332            return (rest, Some(last));
1333        }
1334
1335        // ─────────────────────────────────────────────────────────────────────
1336        // Rule 2: Check for enum variant (Type::Variant pattern)
1337        // ─────────────────────────────────────────────────────────────────────
1338        // Both the type and variant are uppercase (PascalCase)
1339
1340        // Check if there's another "::" before this one
1341        // "error::Error::Io" → prev_sep at position of "Error", prev = "Error"
1342        if let Some(prev_sep) = rest.rfind("::") {
1343            let prev = &rest[prev_sep + 2..]; // The segment before "last"
1344
1345            // Both uppercase = likely Type::Variant
1346            // "Error" uppercase + "Io" uppercase → enum variant
1347            if prev.starts_with(|c: char| c.is_uppercase())
1348                && last.starts_with(|c: char| c.is_uppercase())
1349            {
1350                return (rest, Some(last));
1351            }
1352        } else if rest.starts_with(|c: char| c.is_uppercase())
1353            && last.starts_with(|c: char| c.is_uppercase())
1354        {
1355            // Simple case: "Option::Some" with no module prefix
1356            // "Option" uppercase + "Some" uppercase → enum variant
1357            return (rest, Some(last));
1358        }
1359
1360        // ─────────────────────────────────────────────────────────────────────
1361        // No anchor detected
1362        // ─────────────────────────────────────────────────────────────────────
1363        // This is something like "mod::Type" where Type is not a variant
1364        (path, None)
1365    }
1366
1367    /// Build a markdown link, handling same-crate and cross-crate cases.
1368    ///
1369    /// This is the core function that computes relative paths between markdown
1370    /// files and formats the final link.
1371    ///
1372    /// # Arguments
1373    /// * `current_file` - The file we're generating (e.g., "ureq/agent/index.md")
1374    /// * `target_crate` - The crate containing the target item
1375    /// * `target_path` - Path to target within its crate (e.g., "config/index.md")
1376    /// * `display_name` - Text to show in the link
1377    /// * `anchor` - Optional anchor suffix (e.g., "`method_name`")
1378    ///
1379    /// # Path Computation Examples
1380    ///
1381    /// ## Same Crate Examples
1382    ///
1383    /// ```text
1384    /// Example 1: Link from index to nested module
1385    ///    current_file = "ureq/index.md"
1386    ///    target_crate = "ureq"
1387    ///    target_path = "config/index.md"
1388    ///
1389    ///    Step 1: Strip crate prefix from current
1390    ///      "ureq/index.md" -> "index.md"
1391    ///
1392    ///    Step 2: Compute relative path
1393    ///      from "index.md" to "config/index.md"
1394    ///      -> "config/index.md"
1395    ///
1396    ///    Output: "[`display`](config/index.md)"
1397    ///
1398    /// Example 2: Link from nested to sibling module
1399    ///    current_file = "ureq/agent/index.md"
1400    ///    target_crate = "ureq"
1401    ///    target_path = "config/index.md"
1402    ///
1403    ///    Step 1: Strip crate prefix
1404    ///      "ureq/agent/index.md" -> "agent/index.md"
1405    ///
1406    ///    Step 2: Compute relative path
1407    ///      from "agent/index.md" to "config/index.md"
1408    ///      -> "config/index.md"
1409    ///
1410    ///    Output: "[`display`][../config/index.md]"
1411    ///
1412    /// ## Cross-Crate Examples
1413    ///
1414    /// ```text
1415    /// Example 3: Link from one crate to another
1416    ///   current_file = "ureq/agent/index.md"
1417    ///   target_crate = "http"
1418    ///   target_path  = "status/index.md"
1419    ///
1420    ///   Step 1: Strip crate prefix
1421    ///     "ureq/agent/index.md" → "agent/index.md"
1422    ///
1423    ///   Step 2: Count depth (number of '/' in local path)
1424    ///     "agent/index.md" has 1 slash → depth = 1
1425    ///
1426    ///   Step 3: Build cross-crate path
1427    ///     Go up (depth + 1) levels: "../" * 2 = "../../"
1428    ///     Then into target crate: "../../http/status/index.md"
1429    ///
1430    ///   Output: "[`display`](../../http/status/index.md)"
1431    ///
1432    /// Example 4: Cross-crate from root
1433    ///   current_file = "ureq/index.md"
1434    ///   target_crate = "http"
1435    ///   target_path  = "index.md"
1436    ///
1437    ///   depth = 0 (no slashes in "index.md")
1438    ///   prefix = "../" * 1 = "../"
1439    ///
1440    ///   Output: "[`display`](../http/index.md)"
1441    /// ```
1442    fn build_markdown_link(
1443        &self,
1444        current_file: &str,
1445        target_crate: &str,
1446        target_path: &str,
1447        display_name: &str,
1448        anchor: Option<&str>,
1449    ) -> String {
1450        use crate::linker::LinkRegistry;
1451
1452        // ------------------------------------------------------------------------
1453        //  Step 1: Get the crate-local portion of the current path
1454        // ------------------------------------------------------------------------
1455        // "ureq/agent/index.md" -> "agent/index.md"
1456        // This is needed because target_path doesn't include the crate prefix
1457        let current_local = Self::strip_crate_prefix(current_file);
1458
1459        // ------------------------------------------------------------------------
1460        //  Step 2: Compute the file path portion of the link
1461        // ------------------------------------------------------------------------
1462        let file_link = if target_crate == self.crate_name {
1463            // ====================================================================
1464            //  SAME CRATE: Use relative path within the crate
1465            // ====================================================================
1466            if current_local == target_path {
1467                // Same file, we only need an anchor, no file path.
1468                // Example: linking to a method on the same page
1469                String::new()
1470            } else {
1471                // Different file in same crate - compute relative path
1472                // "agent/index.md" -> "config/index.md" = "../config/index.md"
1473                LinkRegistry::compute_relative_path(current_local, target_path)
1474            }
1475        } else {
1476            // ================================================================
1477            // CROSS-CRATE: Navigate up to docs root, then into target crate
1478            // ================================================================
1479            Self::compute_cross_crate_path(current_local, target_crate, target_path)
1480        };
1481
1482        // ─────────────────────────────────────────────────────────────────────
1483        // Step 3: Build the anchor suffix
1484        // ─────────────────────────────────────────────────────────────────────
1485        // Convert anchor to slug format (lowercase, hyphens for special chars)
1486        // "http_status_as_error" → "#http_status_as_error"
1487        let anchor_suffix = anchor.map_or_else(String::new, |a| {
1488            format!("#{}", AnchorUtils::slugify_anchor(a))
1489        });
1490
1491        // ─────────────────────────────────────────────────────────────────────
1492        // Step 4: Assemble the final markdown link
1493        // ─────────────────────────────────────────────────────────────────────
1494        if file_link.is_empty() {
1495            // Same file - we need an anchor (either explicit or from display name)
1496            // If no explicit anchor was provided, use the display name as anchor
1497            let anchor = if anchor.is_some() {
1498                anchor_suffix
1499            } else {
1500                // Turn display name into anchor: "ConfigBuilder" → "#configbuilder"
1501                format!("#{}", AnchorUtils::slugify_anchor(display_name))
1502            };
1503            format!("[`{display_name}`]({anchor})")
1504        } else {
1505            // Different file - include file path and optional anchor
1506            format!("[`{display_name}`]({file_link}{anchor_suffix})")
1507        }
1508    }
1509
1510    /// Compute a relative path for cross-crate linking.
1511    ///
1512    /// Given the local portion of the current file path (without crate prefix),
1513    /// computes the `../` prefix needed to navigate to another crate's file.
1514    ///
1515    /// # Arguments
1516    /// * `current_local` - Current file path within crate (e.g., "agent/index.md")
1517    /// * `target_crate` - Name of the target crate
1518    /// * `target_path` - Path within target crate (e.g., "status/index.md")
1519    ///
1520    /// # Examples
1521    ///
1522    /// ```text
1523    /// // From root of one crate to another
1524    /// compute_cross_crate_path("index.md", "http", "index.md")
1525    ///   → "../http/index.md"
1526    ///
1527    /// // From nested module to another crate
1528    /// compute_cross_crate_path("agent/index.md", "http", "status/index.md")
1529    ///   → "../../http/status/index.md"
1530    ///
1531    /// // From deeply nested to another crate root
1532    /// compute_cross_crate_path("a/b/c/index.md", "other", "index.md")
1533    ///   → "../../../../other/index.md"
1534    /// ```
1535    fn compute_cross_crate_path(
1536        current_local: &str,
1537        target_crate: &str,
1538        target_path: &str,
1539    ) -> String {
1540        // Count depth: number of '/' in current path
1541        // "agent/index.md" has 1 slash → depth = 1
1542        let depth = current_local.matches('/').count();
1543
1544        // We need to go up:
1545        // - `depth` levels to get to crate root
1546        // - +1 more level to get to docs root (above all crates)
1547        let prefix = "../".repeat(depth + 1);
1548
1549        // Then descend into the target crate
1550        format!("{prefix}{target_crate}/{target_path}")
1551    }
1552
1553    /// Strip the crate prefix from a file path.
1554    ///
1555    /// File paths in our system includes the crate name as the first directory.
1556    /// This helper removes it to get the crate-local path.
1557    ///
1558    /// # Examples
1559    ///
1560    /// ```text
1561    /// "ureq/config/index.md" -> "config/index.md"
1562    /// "ureq/index.md"        -> "index.md"
1563    /// "http/status/index.md" -> "status/index.md"
1564    /// "simple.md"            -> "simple.md" (no slash returns as is)
1565    /// ```
1566    #[inline]
1567    fn strip_crate_prefix(path: &str) -> &str {
1568        // Find the first '/' which seperates crate name from the rest
1569        // "ureq/config/index.md"
1570        //      ^ position = 4
1571        //
1572        // Then return everything after it: "config/index.md"
1573        path.find('/').map_or(path, |i| &path[(i + 1)..])
1574    }
1575
1576    /// Check if a link text looks like an intentional external crate reference.
1577    ///
1578    /// Simple names like "Wide", "Error", "Default" are often meant to be
1579    /// local anchors or type aliases, not cross-crate links.
1580    fn looks_like_external_reference(link_text: &str) -> bool {
1581        // Contains :: - explicit path reference
1582        if link_text.contains("::") {
1583            return true;
1584        }
1585
1586        // Known external crate names or patterns
1587        let external_patterns = ["std::", "core::", "alloc::", "_crate", "_derive", "_impl"];
1588
1589        for pattern in external_patterns {
1590            if link_text.contains(pattern) {
1591                return true;
1592            }
1593        }
1594
1595        // Single PascalCase words are usually local items, not external
1596        // (External items would be referenced with full paths)
1597        false
1598    }
1599}
1600
1601impl ItemAccess for SingleCrateView<'_> {
1602    fn krate(&self) -> &Crate {
1603        self.krate
1604    }
1605
1606    fn crate_name(&self) -> &str {
1607        self.crate_name
1608    }
1609
1610    fn get_item(&self, id: &Id) -> Option<&Item> {
1611        self.krate.index.get(id)
1612    }
1613
1614    fn get_impls(&self, id: &Id) -> Option<&[&Impl]> {
1615        self.impl_map.get(id).map(Vec::as_slice)
1616    }
1617
1618    fn crate_version(&self) -> Option<&str> {
1619        self.krate.crate_version.as_deref()
1620    }
1621
1622    fn render_config(&self) -> &RenderConfig {
1623        &self.ctx.config
1624    }
1625
1626    fn source_path_config_for_file(&self, current_file: &str) -> Option<SourcePathConfig> {
1627        self.ctx.source_path_config_for_file(current_file)
1628    }
1629}
1630
1631impl ItemFilter for SingleCrateView<'_> {
1632    fn should_include_item(&self, item: &Item) -> bool {
1633        match &item.visibility {
1634            Visibility::Public => true,
1635            _ => !self.args.exclude_private,
1636        }
1637    }
1638
1639    fn include_private(&self) -> bool {
1640        !self.args.exclude_private
1641    }
1642
1643    fn include_blanket_impls(&self) -> bool {
1644        self.args.include_blanket_impls
1645    }
1646}
1647
1648impl LinkResolver for SingleCrateView<'_> {
1649    fn link_registry(&self) -> Option<&LinkRegistry> {
1650        // Multi-crate mode uses UnifiedLinkRegistry instead
1651        None
1652    }
1653
1654    fn process_docs(&self, item: &Item, current_file: &str) -> Option<String> {
1655        let docs = item.docs.as_ref()?;
1656        let name = item.name.as_deref().unwrap_or("");
1657
1658        // Strip duplicate title if docs start with "# name"
1659        let docs = DocLinkUtils::strip_duplicate_title(docs, name);
1660
1661        // Strip reference definitions first to prevent mangled output
1662        let stripped = DocLinkUtils::strip_reference_definitions(docs);
1663
1664        // Unhide rustdoc hidden lines and add `rust` to bare code fences
1665        let unhidden = DocLinkUtils::unhide_code_lines(&stripped);
1666
1667        // Convert HTML and path reference links
1668        let html_processed = DocLinkUtils::convert_html_links(&unhidden);
1669        let path_processed = DocLinkUtils::convert_path_reference_links(&html_processed);
1670
1671        // Process backtick links [`Name`]
1672        let backtick_processed =
1673            self.process_backtick_links(&path_processed, &item.links, current_file);
1674
1675        // Process plain links [name]
1676        let plain_processed = self.process_plain_links(&backtick_processed, current_file);
1677
1678        Some(plain_processed)
1679    }
1680
1681    fn create_link(&self, id: Id, current_file: &str) -> Option<String> {
1682        use crate::linker::LinkRegistry;
1683
1684        // Look up path in the unified registry (crate-local, no prefix)
1685        let target_path = self.registry.get_path(self.crate_name, id)?;
1686
1687        // Get the item name for display
1688        let display_name = self
1689            .registry
1690            .get_name(self.crate_name, id)
1691            .map_or("item", |s| s.as_str());
1692
1693        // Strip crate prefix from current_file to get crate-local path
1694        // "crate_name/module/index.md" -> "module/index.md"
1695        let current_local = Self::strip_crate_prefix(current_file);
1696
1697        // Compute relative path using the same logic as build_markdown_link
1698        let relative_path = if current_local == target_path.as_str() {
1699            // Same file - just use anchor
1700            format!("#{}", AnchorUtils::slugify_anchor(display_name))
1701        } else {
1702            // Different file - compute relative path within crate and append anchor
1703            let path = LinkRegistry::compute_relative_path(current_local, target_path);
1704            format!("{}#{}", path, AnchorUtils::slugify_anchor(display_name))
1705        };
1706
1707        Some(format!("[`{display_name}`]({relative_path})"))
1708    }
1709}
1710
1711// SingleCrateView automatically implements RenderContext via blanket impl
1712
1713#[cfg(test)]
1714mod tests {
1715    use super::*;
1716
1717    // =========================================================================
1718    // Tests for split_type_and_anchor
1719    // =========================================================================
1720
1721    mod split_type_and_anchor {
1722        use super::*;
1723
1724        #[test]
1725        fn simple_type_no_anchor() {
1726            assert_eq!(SingleCrateView::split_type_and_anchor("Vec"), ("Vec", None));
1727        }
1728
1729        #[test]
1730        fn module_path_no_anchor() {
1731            // Module prefix + type = no anchor (lowercase then uppercase)
1732            assert_eq!(
1733                SingleCrateView::split_type_and_anchor("config::ConfigBuilder"),
1734                ("config::ConfigBuilder", None)
1735            );
1736        }
1737
1738        #[test]
1739        fn type_with_method() {
1740            // Type::method - last segment lowercase = method anchor
1741            assert_eq!(
1742                SingleCrateView::split_type_and_anchor("Type::method"),
1743                ("Type", Some("method"))
1744            );
1745        }
1746
1747        #[test]
1748        fn type_with_snake_case_method() {
1749            assert_eq!(
1750                SingleCrateView::split_type_and_anchor("ConfigBuilder::http_status_as_error"),
1751                ("ConfigBuilder", Some("http_status_as_error"))
1752            );
1753        }
1754
1755        #[test]
1756        fn module_type_method() {
1757            // Full path with method
1758            assert_eq!(
1759                SingleCrateView::split_type_and_anchor("config::ConfigBuilder::new"),
1760                ("config::ConfigBuilder", Some("new"))
1761            );
1762        }
1763
1764        #[test]
1765        fn enum_variant_simple() {
1766            // Both uppercase = enum variant
1767            assert_eq!(
1768                SingleCrateView::split_type_and_anchor("Option::Some"),
1769                ("Option", Some("Some"))
1770            );
1771        }
1772
1773        #[test]
1774        fn enum_variant_with_module() {
1775            // Module + Type::Variant
1776            assert_eq!(
1777                SingleCrateView::split_type_and_anchor("error::Error::Io"),
1778                ("error::Error", Some("Io"))
1779            );
1780        }
1781
1782        #[test]
1783        fn result_variant() {
1784            assert_eq!(
1785                SingleCrateView::split_type_and_anchor("Result::Ok"),
1786                ("Result", Some("Ok"))
1787            );
1788        }
1789
1790        #[test]
1791        fn nested_modules_with_type() {
1792            // Deep nesting ending in type (no anchor)
1793            assert_eq!(
1794                SingleCrateView::split_type_and_anchor("a::b::c::Type"),
1795                ("a::b::c::Type", None)
1796            );
1797        }
1798
1799        #[test]
1800        fn nested_modules_with_method() {
1801            // Deep nesting ending in method
1802            assert_eq!(
1803                SingleCrateView::split_type_and_anchor("a::b::Type::method"),
1804                ("a::b::Type", Some("method"))
1805            );
1806        }
1807
1808        #[test]
1809        fn associated_type_treated_as_variant() {
1810            // Iterator::Item - both uppercase, treated as variant (acceptable)
1811            assert_eq!(
1812                SingleCrateView::split_type_and_anchor("Iterator::Item"),
1813                ("Iterator", Some("Item"))
1814            );
1815        }
1816
1817        #[test]
1818        fn const_associated_item() {
1819            // Type::CONST - uppercase const, treated as variant
1820            assert_eq!(
1821                SingleCrateView::split_type_and_anchor("Type::MAX"),
1822                ("Type", Some("MAX"))
1823            );
1824        }
1825    }
1826
1827    // =========================================================================
1828    // Tests for strip_crate_prefix
1829    // =========================================================================
1830
1831    mod strip_crate_prefix {
1832        use super::*;
1833
1834        #[test]
1835        fn strips_crate_from_nested_path() {
1836            assert_eq!(
1837                SingleCrateView::strip_crate_prefix("ureq/config/index.md"),
1838                "config/index.md"
1839            );
1840        }
1841
1842        #[test]
1843        fn strips_crate_from_root() {
1844            assert_eq!(
1845                SingleCrateView::strip_crate_prefix("ureq/index.md"),
1846                "index.md"
1847            );
1848        }
1849
1850        #[test]
1851        fn strips_crate_from_deep_path() {
1852            assert_eq!(
1853                SingleCrateView::strip_crate_prefix("http/uri/authority/index.md"),
1854                "uri/authority/index.md"
1855            );
1856        }
1857
1858        #[test]
1859        fn no_slash_returns_as_is() {
1860            assert_eq!(
1861                SingleCrateView::strip_crate_prefix("simple.md"),
1862                "simple.md"
1863            );
1864        }
1865    }
1866
1867    // =========================================================================
1868    // Tests for looks_like_external_reference
1869    // =========================================================================
1870
1871    mod looks_like_external_reference {
1872        use super::*;
1873
1874        #[test]
1875        fn qualified_path_is_external() {
1876            assert!(SingleCrateView::looks_like_external_reference(
1877                "std::io::Error"
1878            ));
1879        }
1880
1881        #[test]
1882        fn crate_path_is_external() {
1883            assert!(SingleCrateView::looks_like_external_reference(
1884                "regex::Regex"
1885            ));
1886        }
1887
1888        #[test]
1889        fn std_prefix_is_external() {
1890            assert!(SingleCrateView::looks_like_external_reference(
1891                "std::vec::Vec"
1892            ));
1893        }
1894
1895        #[test]
1896        fn core_prefix_is_external() {
1897            assert!(SingleCrateView::looks_like_external_reference(
1898                "core::mem::drop"
1899            ));
1900        }
1901
1902        #[test]
1903        fn alloc_prefix_is_external() {
1904            assert!(SingleCrateView::looks_like_external_reference(
1905                "alloc::string::String"
1906            ));
1907        }
1908
1909        #[test]
1910        fn simple_name_not_external() {
1911            assert!(!SingleCrateView::looks_like_external_reference("Error"));
1912        }
1913
1914        #[test]
1915        fn pascal_case_not_external() {
1916            assert!(!SingleCrateView::looks_like_external_reference(
1917                "ConfigBuilder"
1918            ));
1919        }
1920
1921        #[test]
1922        fn derive_suffix_is_external() {
1923            assert!(SingleCrateView::looks_like_external_reference(
1924                "serde_derive"
1925            ));
1926        }
1927    }
1928
1929    // =========================================================================
1930    // Tests for compute_cross_crate_path (relative path computation)
1931    // =========================================================================
1932
1933    mod compute_cross_crate_path {
1934        use super::*;
1935
1936        #[test]
1937        fn from_root_to_root() {
1938            // From crate root (index.md) to another crate's root
1939            assert_eq!(
1940                SingleCrateView::compute_cross_crate_path("index.md", "http", "index.md"),
1941                "../http/index.md"
1942            );
1943        }
1944
1945        #[test]
1946        fn from_root_to_nested() {
1947            // From crate root to nested module in another crate
1948            assert_eq!(
1949                SingleCrateView::compute_cross_crate_path("index.md", "http", "status/index.md"),
1950                "../http/status/index.md"
1951            );
1952        }
1953
1954        #[test]
1955        fn from_nested_to_root() {
1956            // From nested module to another crate's root
1957            // depth = 1 (one '/'), needs "../" * 2 = "../../"
1958            assert_eq!(
1959                SingleCrateView::compute_cross_crate_path("agent/index.md", "http", "index.md"),
1960                "../../http/index.md"
1961            );
1962        }
1963
1964        #[test]
1965        fn from_nested_to_nested() {
1966            // From nested module to nested module in another crate
1967            assert_eq!(
1968                SingleCrateView::compute_cross_crate_path(
1969                    "agent/index.md",
1970                    "http",
1971                    "status/index.md"
1972                ),
1973                "../../http/status/index.md"
1974            );
1975        }
1976
1977        #[test]
1978        fn from_deeply_nested() {
1979            // From deeply nested (3 levels) to another crate
1980            // depth = 3, needs "../" * 4 = "../../../../"
1981            assert_eq!(
1982                SingleCrateView::compute_cross_crate_path("a/b/c/index.md", "other", "index.md"),
1983                "../../../../other/index.md"
1984            );
1985        }
1986
1987        #[test]
1988        fn to_deeply_nested() {
1989            // From root to deeply nested in another crate
1990            assert_eq!(
1991                SingleCrateView::compute_cross_crate_path("index.md", "target", "x/y/z/index.md"),
1992                "../target/x/y/z/index.md"
1993            );
1994        }
1995
1996        #[test]
1997        fn both_deeply_nested() {
1998            // Both source and target are deeply nested
1999            assert_eq!(
2000                SingleCrateView::compute_cross_crate_path("a/b/index.md", "target", "x/y/index.md"),
2001                "../../../target/x/y/index.md"
2002            );
2003        }
2004    }
2005
2006    // Note: process_plain_links tests removed - function is now registry-aware
2007    // and requires a full SingleCrateView context. Behavior is tested via
2008    // integration tests.
2009}