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 {}