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;
20
21use rustdoc_types::{Crate, Id, Impl, Item, ItemEnum, Visibility};
22
23use crate::Args;
24use crate::generator::doc_links::{DocLinkProcessor, strip_duplicate_title};
25use crate::linker::LinkRegistry;
26
27// =============================================================================
28// Focused Traits
29// =============================================================================
30
31/// Core data access for crate documentation.
32///
33/// Provides read-only access to the crate structure, items, and impl blocks.
34pub trait ItemAccess {
35 /// Get the crate being documented.
36 fn krate(&self) -> &Crate;
37
38 /// Get the crate name.
39 fn crate_name(&self) -> &str;
40
41 /// Get an item by its ID.
42 fn get_item(&self, id: &Id) -> Option<&Item>;
43
44 /// Get impl blocks for a type.
45 fn get_impls(&self, id: &Id) -> Option<&[&Impl]>;
46
47 /// Get the crate version for display in headers.
48 fn crate_version(&self) -> Option<&str>;
49}
50
51/// Item visibility and filtering logic.
52///
53/// Determines which items should be included in the generated documentation.
54pub trait ItemFilter {
55 /// Check if an item should be included based on visibility.
56 fn should_include_item(&self, item: &Item) -> bool;
57
58 /// Whether private items should be included.
59 fn include_private(&self) -> bool;
60
61 /// Whether blanket trait implementations should be included.
62 ///
63 /// When `false` (default), impls like `From`, `Into`, `Any`, `Borrow` are filtered.
64 fn include_blanket_impls(&self) -> bool;
65}
66
67/// Link creation and documentation processing.
68///
69/// Handles intra-doc link resolution and markdown link generation.
70pub trait LinkResolver {
71 /// Get the link registry for single-crate mode.
72 ///
73 /// Returns `None` in multi-crate mode where `UnifiedLinkRegistry` is used instead.
74 fn link_registry(&self) -> Option<&LinkRegistry>;
75
76 /// Process documentation string with intra-doc link resolution.
77 ///
78 /// Transforms `` [`Type`] `` style links in doc comments into proper
79 /// markdown links. Also strips duplicate titles and reference definitions.
80 ///
81 /// # Arguments
82 ///
83 /// * `item` - The item whose docs to process (provides docs and links map)
84 /// * `current_file` - Path of the current file (for relative link calculation)
85 fn process_docs(&self, item: &Item, current_file: &str) -> Option<String>;
86
87 /// Create a markdown link to an item.
88 ///
89 /// # Arguments
90 ///
91 /// * `id` - The item ID to link to
92 /// * `current_file` - Path of the current file (for relative link calculation)
93 ///
94 /// # Returns
95 ///
96 /// A markdown link like `[`Name`](path/to/item.md)`, or `None` if the item
97 /// cannot be linked.
98 fn create_link(&self, id: Id, current_file: &str) -> Option<String>;
99}
100
101// =============================================================================
102// Combined Trait
103// =============================================================================
104
105/// Combined rendering context trait.
106///
107/// This trait combines [`ItemAccess`], [`ItemFilter`], and [`LinkResolver`]
108/// for components that need full access to the rendering context.
109///
110/// Most renderers should use this trait for convenience, but components
111/// with limited requirements can depend on individual sub-traits.
112pub trait RenderContext: ItemAccess + ItemFilter + LinkResolver {}
113
114/// Shared context containing all data needed for documentation generation.
115///
116/// This struct is passed to all rendering components and provides:
117/// - Access to the parsed crate data
118/// - Impl block lookup for rendering implementations
119/// - Link registry for cross-references
120/// - CLI configuration options
121pub struct GeneratorContext<'a> {
122 /// The parsed rustdoc JSON crate.
123 pub krate: &'a Crate,
124
125 /// The crate name (extracted from root module).
126 crate_name: String,
127
128 /// Maps type IDs to all impl blocks for that type.
129 ///
130 /// Used for rendering "Implementations" and "Trait Implementations" sections.
131 pub impl_map: HashMap<Id, Vec<&'a Impl>>,
132
133 /// Registry for creating cross-reference links between items.
134 pub link_registry: LinkRegistry,
135
136 /// CLI arguments containing output path, format, and options.
137 pub args: &'a Args,
138}
139
140impl<'a> GeneratorContext<'a> {
141 /// Create a new generator context from crate data and CLI arguments.
142 ///
143 /// Builds the path map, impl map, and link registry needed for generation.
144 ///
145 /// # Arguments
146 ///
147 /// * `krate` - The parsed rustdoc JSON crate
148 /// * `args` - CLI arguments containing output path, format, and options
149 #[must_use]
150 pub fn new(krate: &'a Crate, args: &'a Args) -> Self {
151 use crate::CliOutputFormat;
152
153 // Extract crate name from root module
154 let crate_name = krate
155 .index
156 .get(&krate.root)
157 .and_then(|item| item.name.clone())
158 .unwrap_or_else(|| "unnamed".to_string());
159
160 let impl_map = Self::build_impl_map(krate);
161 let is_flat = matches!(args.format, CliOutputFormat::Flat);
162 let link_registry = LinkRegistry::build(krate, is_flat, !args.exclude_private);
163
164 Self {
165 krate,
166 crate_name,
167 impl_map,
168 link_registry,
169 args,
170 }
171 }
172
173 /// Build a map from type ID to all impl blocks for that type.
174 ///
175 /// This enables rendering the "Implementations" and "Trait Implementations"
176 /// sections for structs, enums, and other types.
177 ///
178 /// Rustdoc JSON stores impl blocks as separate items in the index.
179 /// Each impl has a `for_` field indicating what type it implements for.
180 /// We walk all items, find impls, and group them by their target type ID.
181 fn build_impl_map(krate: &'a Crate) -> HashMap<Id, Vec<&'a Impl>> {
182 let mut map: HashMap<Id, Vec<&'a Impl>> = HashMap::new();
183
184 for item in krate.index.values() {
185 if let ItemEnum::Impl(impl_block) = &item.inner
186 && let Some(type_id) = Self::get_type_id(&impl_block.for_)
187 {
188 map.entry(type_id).or_default().push(impl_block);
189 }
190 }
191
192 // Sort impl blocks within each type for deterministic output
193 for impls in map.values_mut() {
194 impls.sort_by(|a, b| Self::impl_sort_key(a).cmp(&Self::impl_sort_key(b)));
195 }
196
197 map
198 }
199
200 /// Generate a sort key for an impl block.
201 ///
202 /// Inherent impls (no trait) sort before trait impls.
203 /// Trait impls are sorted by trait name.
204 fn impl_sort_key(impl_block: &Impl) -> (u8, String) {
205 match &impl_block.trait_ {
206 None => (0, String::new()), // Inherent impls first
207 Some(path) => (1, path.path.clone()), // Then trait impls by name
208 }
209 }
210
211 /// Extract the item ID from a Type if it's a resolved path.
212 ///
213 /// Only `ResolvedPath` types (named types like `Vec`, `String`, `MyStruct`)
214 /// have associated IDs. Other types (primitives, references, etc.) return None.
215 const fn get_type_id(ty: &rustdoc_types::Type) -> Option<Id> {
216 match ty {
217 rustdoc_types::Type::ResolvedPath(path) => Some(path.id),
218 _ => None,
219 }
220 }
221
222 /// Check if an item should be included based on visibility settings.
223 ///
224 /// By default, all items are included. If `--exclude-private`
225 /// is set, only public items are included.
226 ///
227 /// # Visibility Levels
228 ///
229 /// - `Public` - Always included
230 /// - `Crate`, `Restricted`, `Default` - Included by default, excluded with `--exclude-private`
231 #[must_use]
232 pub const fn should_include_item(&self, item: &Item) -> bool {
233 match &item.visibility {
234 Visibility::Public => true,
235 _ => !self.args.exclude_private,
236 }
237 }
238
239 /// Count the total number of modules that will be generated.
240 ///
241 /// Used to initialize the progress bar with the correct total.
242 /// Respects the `--exclude-private` flag when counting.
243 #[must_use]
244 pub fn count_modules(&self, item: &Item) -> usize {
245 let mut count = 0;
246
247 if let ItemEnum::Module(module) = &item.inner {
248 for item_id in &module.items {
249 if let Some(child) = self.krate.index.get(item_id)
250 && let ItemEnum::Module(_) = &child.inner
251 && self.should_include_item(child)
252 {
253 count += 1;
254 count += self.count_modules(child);
255 }
256 }
257 }
258
259 count
260 }
261}
262
263impl ItemAccess for GeneratorContext<'_> {
264 fn krate(&self) -> &Crate {
265 self.krate
266 }
267
268 fn crate_name(&self) -> &str {
269 &self.crate_name
270 }
271
272 fn get_item(&self, id: &Id) -> Option<&Item> {
273 self.krate.index.get(id)
274 }
275
276 fn get_impls(&self, id: &Id) -> Option<&[&Impl]> {
277 self.impl_map.get(id).map(Vec::as_slice)
278 }
279
280 fn crate_version(&self) -> Option<&str> {
281 self.krate.crate_version.as_deref()
282 }
283}
284
285impl ItemFilter for GeneratorContext<'_> {
286 fn should_include_item(&self, item: &Item) -> bool {
287 match &item.visibility {
288 Visibility::Public => true,
289 _ => !self.args.exclude_private,
290 }
291 }
292
293 fn include_private(&self) -> bool {
294 !self.args.exclude_private
295 }
296
297 fn include_blanket_impls(&self) -> bool {
298 self.args.include_blanket_impls
299 }
300}
301
302impl LinkResolver for GeneratorContext<'_> {
303 fn link_registry(&self) -> Option<&LinkRegistry> {
304 Some(&self.link_registry)
305 }
306
307 fn process_docs(&self, item: &Item, current_file: &str) -> Option<String> {
308 let docs = item.docs.as_ref()?;
309 let name = item.name.as_deref().unwrap_or("");
310
311 // Strip duplicate title if docs start with "# name"
312 let docs = strip_duplicate_title(docs, name);
313
314 let processor = DocLinkProcessor::new(self.krate, &self.link_registry, current_file);
315 Some(processor.process(docs, &item.links))
316 }
317
318 fn create_link(&self, id: Id, current_file: &str) -> Option<String> {
319 self.link_registry.create_link(id, current_file)
320 }
321}
322
323// Blanket implementation: any type that implements all three sub-traits
324// automatically implements RenderContext
325impl<T: ItemAccess + ItemFilter + LinkResolver> RenderContext for T {}