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