cargo_docs_md/generator/
context.rs

1//! Shared context for documentation generation.
2//!
3//! This module provides the [`GeneratorContext`] struct which holds all shared
4//! state needed during markdown generation, including the crate data, lookup
5//! maps, and configuration options.
6//!
7//! # Trait Hierarchy
8//!
9//! The rendering context is split into focused traits for better abstraction:
10//!
11//! - [`ItemAccess`] - Core data access (crate, items, impls)
12//! - [`ItemFilter`] - Visibility and filtering logic
13//! - [`LinkResolver`] - Link creation and documentation processing
14//! - [`RenderContext`] - Combined super-trait for convenience
15//!
16//! This allows components to depend only on the traits they need, improving
17//! testability and reducing coupling.
18
19use std::collections::HashMap;
20use std::path::Path;
21
22use rustdoc_types::{Crate, Id, Impl, Item, ItemEnum, Visibility};
23
24use crate::Args;
25use crate::generator::config::RenderConfig;
26use crate::generator::doc_links::{DocLinkProcessor, DocLinkUtils};
27use crate::generator::render_shared::SourcePathConfig;
28use crate::linker::LinkRegistry;
29
30// =============================================================================
31// Focused Traits
32// =============================================================================
33
34/// Core data access for crate documentation.
35///
36/// Provides read-only access to the crate structure, items, and impl blocks.
37pub trait ItemAccess {
38    /// Get the crate being documented.
39    fn krate(&self) -> &Crate;
40
41    /// Get the crate name.
42    fn crate_name(&self) -> &str;
43
44    /// Get an item by its ID.
45    fn get_item(&self, id: &Id) -> Option<&Item>;
46
47    /// Get impl blocks for a type.
48    fn get_impls(&self, id: &Id) -> Option<&[&Impl]>;
49
50    /// Get the crate version for display in headers.
51    fn crate_version(&self) -> Option<&str>;
52
53    /// Get the rendering configuration.
54    fn render_config(&self) -> &RenderConfig;
55
56    /// Get source path config for a specific file.
57    ///
58    /// Returns `None` if source locations are disabled or no source dir configured.
59    /// The returned config has the correct depth for the given file path.
60    fn source_path_config_for_file(&self, _current_file: &str) -> Option<SourcePathConfig> {
61        None
62    }
63}
64
65/// Item visibility and filtering logic.
66///
67/// Determines which items should be included in the generated documentation.
68pub trait ItemFilter {
69    /// Check if an item should be included based on visibility.
70    fn should_include_item(&self, item: &Item) -> bool;
71
72    /// Whether private items should be included.
73    fn include_private(&self) -> bool;
74
75    /// Whether blanket trait implementations should be included.
76    ///
77    /// When `false` (default), impls like `From`, `Into`, `Any`, `Borrow` are filtered.
78    fn include_blanket_impls(&self) -> bool;
79}
80
81/// Link creation and documentation processing.
82///
83/// Handles intra-doc link resolution and markdown link generation.
84pub trait LinkResolver {
85    /// Get the link registry for single-crate mode.
86    ///
87    /// Returns `None` in multi-crate mode where `UnifiedLinkRegistry` is used instead.
88    fn link_registry(&self) -> Option<&LinkRegistry>;
89
90    /// Process documentation string with intra-doc link resolution.
91    ///
92    /// Transforms `` [`Type`] `` style links in doc comments into proper
93    /// markdown links. Also strips duplicate titles and reference definitions.
94    ///
95    /// # Arguments
96    ///
97    /// * `item` - The item whose docs to process (provides docs and links map)
98    /// * `current_file` - Path of the current file (for relative link calculation)
99    fn process_docs(&self, item: &Item, current_file: &str) -> Option<String>;
100
101    /// Create a markdown link to an item.
102    ///
103    /// # Arguments
104    ///
105    /// * `id` - The item ID to link to
106    /// * `current_file` - Path of the current file (for relative link calculation)
107    ///
108    /// # Returns
109    ///
110    /// A markdown link like `[`Name`](path/to/item.md)`, or `None` if the item
111    /// cannot be linked.
112    fn create_link(&self, id: Id, current_file: &str) -> Option<String>;
113}
114
115// =============================================================================
116// Combined Trait
117// =============================================================================
118
119/// Combined rendering context trait.
120///
121/// This trait combines [`ItemAccess`], [`ItemFilter`], and [`LinkResolver`]
122/// for components that need full access to the rendering context.
123///
124/// Most renderers should use this trait for convenience, but components
125/// with limited requirements can depend on individual sub-traits.
126pub trait RenderContext: ItemAccess + ItemFilter + LinkResolver {}
127
128/// Shared context containing all data needed for documentation generation.
129///
130/// This struct is passed to all rendering components and provides:
131/// - Access to the parsed crate data
132/// - Impl block lookup for rendering implementations
133/// - Link registry for cross-references
134/// - CLI configuration options
135pub struct GeneratorContext<'a> {
136    /// The parsed rustdoc JSON crate.
137    pub krate: &'a Crate,
138
139    /// The crate name (extracted from root module).
140    crate_name: String,
141
142    /// Maps type IDs to all impl blocks for that type.
143    ///
144    /// Used for rendering "Implementations" and "Trait Implementations" sections.
145    pub impl_map: HashMap<Id, Vec<&'a Impl>>,
146
147    /// Registry for creating cross-reference links between items.
148    pub link_registry: LinkRegistry,
149
150    /// CLI arguments containing output path, format, and options.
151    pub args: &'a Args,
152
153    /// Rendering configuration options.
154    pub config: RenderConfig,
155
156    /// Pre-built index mapping item names to their IDs for fast lookup.
157    ///
158    /// Built once at construction time from `krate.paths` and shared across
159    /// all `DocLinkProcessor` instances for efficiency.
160    path_name_index: HashMap<&'a str, Vec<Id>>,
161
162    /// Base source path configuration for transforming cargo registry paths.
163    ///
164    /// `None` if source locations are disabled or no `.source_*` dir detected.
165    /// The `depth` field is set to 0; use `source_path_config_for_file()` to
166    /// get a config with the correct depth for a specific file.
167    source_path_config: Option<SourcePathConfig>,
168}
169
170impl<'a> GeneratorContext<'a> {
171    /// Create a new generator context from crate data and CLI arguments.
172    ///
173    /// Builds the path map, impl map, and link registry needed for generation.
174    ///
175    /// # Arguments
176    ///
177    /// * `krate` - The parsed rustdoc JSON crate
178    /// * `args` - CLI arguments containing output path, format, and options
179    /// * `config` - Rendering configuration options
180    #[must_use]
181    pub fn new(krate: &'a Crate, args: &'a Args, config: RenderConfig) -> Self {
182        use crate::CliOutputFormat;
183
184        // Extract crate name from root module
185        let crate_name = krate
186            .index
187            .get(&krate.root)
188            .and_then(|item| item.name.clone())
189            .unwrap_or_else(|| "unnamed".to_string());
190
191        let impl_map = Self::build_impl_map(krate);
192        let is_flat = matches!(args.format, CliOutputFormat::Flat);
193        let link_registry = LinkRegistry::build(krate, is_flat, !args.exclude_private);
194        let path_name_index = Self::build_path_name_index(krate);
195
196        // Build source path config if source_locations is enabled and we have a source_dir
197        let source_path_config = if config.include_source.source_locations {
198            config
199                .include_source
200                .source_dir
201                .as_ref()
202                .map(|dir| SourcePathConfig::new(dir, ""))
203        } else {
204            None
205        };
206
207        Self {
208            krate,
209            crate_name,
210            impl_map,
211            link_registry,
212            args,
213            config,
214            path_name_index,
215            source_path_config,
216        }
217    }
218
219    /// Set the source directory for path transformation.
220    ///
221    /// This can be called after construction if a `.source_*` directory
222    /// is detected or specified via CLI. Only has effect if `source_locations`
223    /// is enabled in the config.
224    pub fn set_source_dir(&mut self, source_dir: &Path) {
225        if self.config.include_source.source_locations {
226            self.source_path_config = Some(SourcePathConfig::new(source_dir, ""));
227        }
228    }
229
230    /// Build a map from type ID to all impl blocks for that type.
231    ///
232    /// This enables rendering the "Implementations" and "Trait Implementations"
233    /// sections for structs, enums, and other types.
234    ///
235    /// Uses the `impls` field on Struct/Enum/Union items directly rather than
236    /// scanning all items and checking the `for_` field. This provides clearer
237    /// semantics and leverages `rustdoc_types` structured data.
238    fn build_impl_map(krate: &'a Crate) -> HashMap<Id, Vec<&'a Impl>> {
239        let mut map: HashMap<Id, Vec<&'a Impl>> = HashMap::new();
240
241        // Iterate over all types that can have impl blocks and collect their impls
242        for (type_id, item) in &krate.index {
243            let impl_ids: &[Id] = match &item.inner {
244                ItemEnum::Struct(s) => &s.impls,
245
246                ItemEnum::Enum(e) => &e.impls,
247
248                ItemEnum::Union(u) => &u.impls,
249
250                _ => continue,
251            };
252
253            // Look up each impl block and add to the map
254            for impl_id in impl_ids {
255                if let Some(impl_item) = krate.index.get(impl_id)
256                    && let ItemEnum::Impl(impl_block) = &impl_item.inner
257                {
258                    map.entry(*type_id).or_default().push(impl_block);
259                }
260            }
261        }
262
263        // Sort impl blocks within each type for deterministic output
264        for impls in map.values_mut() {
265            impls.sort_by(|a, b| Self::impl_sort_key(a).cmp(&Self::impl_sort_key(b)));
266        }
267
268        map
269    }
270
271    /// Generate a sort key for an impl block.
272    ///
273    /// Inherent impls (no trait) sort before trait impls.
274    /// Trait impls are sorted by trait name.
275    fn impl_sort_key(impl_block: &Impl) -> (u8, String) {
276        impl_block
277            .trait_
278            .as_ref()
279            .map_or_else(|| (0, String::new()), |path| (1, path.path.clone()))
280    }
281
282    /// Check if an item should be included based on visibility settings.
283    ///
284    /// By default, all items are included. If `--exclude-private`
285    /// is set, only public items are included.
286    ///
287    /// # Visibility Levels
288    ///
289    /// - `Public` - Always included
290    /// - `Crate`, `Restricted`, `Default` - Included by default, excluded with `--exclude-private`
291    #[must_use]
292    pub const fn should_include_item(&self, item: &Item) -> bool {
293        match &item.visibility {
294            Visibility::Public => true,
295            _ => !self.args.exclude_private,
296        }
297    }
298
299    /// Count the total number of modules that will be generated.
300    ///
301    /// Used to initialize the progress bar with the correct total.
302    /// Respects the `--exclude-private` flag when counting.
303    #[must_use]
304    pub fn count_modules(&self, item: &Item) -> usize {
305        let mut count = 0;
306
307        if let ItemEnum::Module(module) = &item.inner {
308            for item_id in &module.items {
309                if let Some(child) = self.krate.index.get(item_id)
310                    && let ItemEnum::Module(_) = &child.inner
311                    && self.should_include_item(child)
312                {
313                    count += 1;
314                    count += self.count_modules(child);
315                }
316            }
317        }
318
319        count
320    }
321
322    /// Build an index mapping item names to their IDs for fast lookup.
323    ///
324    /// This index is built once at context construction time and shared
325    /// across all `DocLinkProcessor` instances, eliminating redundant
326    /// index building for each item with documentation.
327    fn build_path_name_index(krate: &'a Crate) -> HashMap<&'a str, Vec<Id>> {
328        let mut index: HashMap<&'a str, Vec<Id>> = HashMap::new();
329
330        for (id, path_info) in &krate.paths {
331            if let Some(name) = path_info.path.last() {
332                index.entry(name.as_str()).or_default().push(*id);
333            }
334        }
335
336        // Sort each Vec by full path for deterministic resolution order
337        // Using direct Vec<String> comparison (lexicographic) instead of joining
338        for ids in index.values_mut() {
339            ids.sort_by(|a, b| {
340                let path_a = krate.paths.get(a).map(|p| &p.path);
341                let path_b = krate.paths.get(b).map(|p| &p.path);
342
343                path_a.cmp(&path_b)
344            });
345        }
346
347        index
348    }
349}
350
351impl ItemAccess for GeneratorContext<'_> {
352    fn krate(&self) -> &Crate {
353        self.krate
354    }
355
356    fn crate_name(&self) -> &str {
357        &self.crate_name
358    }
359
360    fn get_item(&self, id: &Id) -> Option<&Item> {
361        self.krate.index.get(id)
362    }
363
364    fn get_impls(&self, id: &Id) -> Option<&[&Impl]> {
365        self.impl_map.get(id).map(Vec::as_slice)
366    }
367
368    fn crate_version(&self) -> Option<&str> {
369        self.krate.crate_version.as_deref()
370    }
371
372    fn render_config(&self) -> &RenderConfig {
373        &self.config
374    }
375
376    fn source_path_config_for_file(&self, current_file: &str) -> Option<SourcePathConfig> {
377        self.source_path_config
378            .as_ref()
379            .map(|base| base.with_depth(current_file))
380    }
381}
382
383impl ItemFilter for GeneratorContext<'_> {
384    fn should_include_item(&self, item: &Item) -> bool {
385        match &item.visibility {
386            Visibility::Public => true,
387            _ => !self.args.exclude_private,
388        }
389    }
390
391    fn include_private(&self) -> bool {
392        !self.args.exclude_private
393    }
394
395    fn include_blanket_impls(&self) -> bool {
396        self.args.include_blanket_impls
397    }
398}
399
400impl LinkResolver for GeneratorContext<'_> {
401    fn link_registry(&self) -> Option<&LinkRegistry> {
402        Some(&self.link_registry)
403    }
404
405    fn process_docs(&self, item: &Item, current_file: &str) -> Option<String> {
406        let docs = item.docs.as_ref()?;
407        let name = item.name.as_deref().unwrap_or("");
408
409        // Strip duplicate title if docs start with "# name"
410        let docs = DocLinkUtils::strip_duplicate_title(docs, name);
411
412        // Use pre-built index for efficiency (avoids rebuilding for each item)
413        let processor = DocLinkProcessor::with_index(
414            self.krate,
415            &self.link_registry,
416            current_file,
417            &self.path_name_index,
418        );
419        Some(processor.process(docs, &item.links))
420    }
421
422    fn create_link(&self, id: Id, current_file: &str) -> Option<String> {
423        self.link_registry.create_link(id, current_file)
424    }
425}
426
427// Blanket implementation: any type that implements all three sub-traits
428// automatically implements RenderContext
429impl<T: ItemAccess + ItemFilter + LinkResolver> RenderContext for T {}