cargo-docs-md 0.2.4

Generate per-module markdown documentation from rustdoc JSON output
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
//! Shared context for documentation generation.
//!
//! This module provides the [`GeneratorContext`] struct which holds all shared
//! state needed during markdown generation, including the crate data, lookup
//! maps, and configuration options.
//!
//! # Trait Hierarchy
//!
//! The rendering context is split into focused traits for better abstraction:
//!
//! - [`ItemAccess`] - Core data access (crate, items, impls)
//! - [`ItemFilter`] - Visibility and filtering logic
//! - [`LinkResolver`] - Link creation and documentation processing
//! - [`RenderContext`] - Combined super-trait for convenience
//!
//! This allows components to depend only on the traits they need, improving
//! testability and reducing coupling.

use std::collections::HashMap;
use std::path::Path;

use rustdoc_types::{Crate, Id, Impl, Item, ItemEnum, Visibility};

use crate::Args;
use crate::generator::config::RenderConfig;
use crate::generator::doc_links::{DocLinkProcessor, DocLinkUtils};
use crate::generator::render_shared::SourcePathConfig;
use crate::linker::LinkRegistry;

// =============================================================================
// Focused Traits
// =============================================================================

/// Core data access for crate documentation.
///
/// Provides read-only access to the crate structure, items, and impl blocks.
pub trait ItemAccess {
    /// Get the crate being documented.
    fn krate(&self) -> &Crate;

    /// Get the crate name.
    fn crate_name(&self) -> &str;

    /// Get an item by its ID.
    fn get_item(&self, id: &Id) -> Option<&Item>;

    /// Get impl blocks for a type.
    fn get_impls(&self, id: &Id) -> Option<&[&Impl]>;

    /// Get the crate version for display in headers.
    fn crate_version(&self) -> Option<&str>;

    /// Get the rendering configuration.
    fn render_config(&self) -> &RenderConfig;

    /// Get source path config for a specific file.
    ///
    /// Returns `None` if source locations are disabled or no source dir configured.
    /// The returned config has the correct depth for the given file path.
    fn source_path_config_for_file(&self, _current_file: &str) -> Option<SourcePathConfig> {
        None
    }
}

/// Item visibility and filtering logic.
///
/// Determines which items should be included in the generated documentation.
pub trait ItemFilter {
    /// Check if an item should be included based on visibility.
    fn should_include_item(&self, item: &Item) -> bool;

    /// Whether private items should be included.
    fn include_private(&self) -> bool;

    /// Whether blanket trait implementations should be included.
    ///
    /// When `false` (default), impls like `From`, `Into`, `Any`, `Borrow` are filtered.
    fn include_blanket_impls(&self) -> bool;
}

/// Link creation and documentation processing.
///
/// Handles intra-doc link resolution and markdown link generation.
pub trait LinkResolver {
    /// Get the link registry for single-crate mode.
    ///
    /// Returns `None` in multi-crate mode where `UnifiedLinkRegistry` is used instead.
    fn link_registry(&self) -> Option<&LinkRegistry>;

    /// Process documentation string with intra-doc link resolution.
    ///
    /// Transforms `` [`Type`] `` style links in doc comments into proper
    /// markdown links. Also strips duplicate titles and reference definitions.
    ///
    /// # Arguments
    ///
    /// * `item` - The item whose docs to process (provides docs and links map)
    /// * `current_file` - Path of the current file (for relative link calculation)
    fn process_docs(&self, item: &Item, current_file: &str) -> Option<String>;

    /// Create a markdown link to an item.
    ///
    /// # Arguments
    ///
    /// * `id` - The item ID to link to
    /// * `current_file` - Path of the current file (for relative link calculation)
    ///
    /// # Returns
    ///
    /// A markdown link like `[`Name`](path/to/item.md)`, or `None` if the item
    /// cannot be linked.
    fn create_link(&self, id: Id, current_file: &str) -> Option<String>;
}

// =============================================================================
// Combined Trait
// =============================================================================

/// Combined rendering context trait.
///
/// This trait combines [`ItemAccess`], [`ItemFilter`], and [`LinkResolver`]
/// for components that need full access to the rendering context.
///
/// Most renderers should use this trait for convenience, but components
/// with limited requirements can depend on individual sub-traits.
pub trait RenderContext: ItemAccess + ItemFilter + LinkResolver {}

/// Shared context containing all data needed for documentation generation.
///
/// This struct is passed to all rendering components and provides:
/// - Access to the parsed crate data
/// - Impl block lookup for rendering implementations
/// - Link registry for cross-references
/// - CLI configuration options
pub struct GeneratorContext<'a> {
    /// The parsed rustdoc JSON crate.
    pub krate: &'a Crate,

    /// The crate name (extracted from root module).
    crate_name: String,

    /// Maps type IDs to all impl blocks for that type.
    ///
    /// Used for rendering "Implementations" and "Trait Implementations" sections.
    pub impl_map: HashMap<Id, Vec<&'a Impl>>,

    /// Registry for creating cross-reference links between items.
    pub link_registry: LinkRegistry,

    /// CLI arguments containing output path, format, and options.
    pub args: &'a Args,

    /// Rendering configuration options.
    pub config: RenderConfig,

    /// Pre-built index mapping item names to their IDs for fast lookup.
    ///
    /// Built once at construction time from `krate.paths` and shared across
    /// all `DocLinkProcessor` instances for efficiency.
    path_name_index: HashMap<&'a str, Vec<Id>>,

    /// Base source path configuration for transforming cargo registry paths.
    ///
    /// `None` if source locations are disabled or no `.source_*` dir detected.
    /// The `depth` field is set to 0; use `source_path_config_for_file()` to
    /// get a config with the correct depth for a specific file.
    source_path_config: Option<SourcePathConfig>,
}

impl<'a> GeneratorContext<'a> {
    /// Create a new generator context from crate data and CLI arguments.
    ///
    /// Builds the path map, impl map, and link registry needed for generation.
    ///
    /// # Arguments
    ///
    /// * `krate` - The parsed rustdoc JSON crate
    /// * `args` - CLI arguments containing output path, format, and options
    /// * `config` - Rendering configuration options
    #[must_use]
    pub fn new(krate: &'a Crate, args: &'a Args, config: RenderConfig) -> Self {
        use crate::CliOutputFormat;

        // Extract crate name from root module
        let crate_name = krate
            .index
            .get(&krate.root)
            .and_then(|item| item.name.clone())
            .unwrap_or_else(|| "unnamed".to_string());

        let impl_map = Self::build_impl_map(krate);
        let is_flat = matches!(args.format, CliOutputFormat::Flat);
        let link_registry = LinkRegistry::build(krate, is_flat, !args.exclude_private);
        let path_name_index = Self::build_path_name_index(krate);

        // Build source path config if source_locations is enabled and we have a source_dir
        let source_path_config = if config.include_source.source_locations {
            config
                .include_source
                .source_dir
                .as_ref()
                .map(|dir| SourcePathConfig::new(dir, ""))
        } else {
            None
        };

        Self {
            krate,
            crate_name,
            impl_map,
            link_registry,
            args,
            config,
            path_name_index,
            source_path_config,
        }
    }

    /// Set the source directory for path transformation.
    ///
    /// This can be called after construction if a `.source_*` directory
    /// is detected or specified via CLI. Only has effect if `source_locations`
    /// is enabled in the config.
    pub fn set_source_dir(&mut self, source_dir: &Path) {
        if self.config.include_source.source_locations {
            self.source_path_config = Some(SourcePathConfig::new(source_dir, ""));
        }
    }

    /// Build a map from type ID to all impl blocks for that type.
    ///
    /// This enables rendering the "Implementations" and "Trait Implementations"
    /// sections for structs, enums, and other types.
    ///
    /// Uses the `impls` field on Struct/Enum/Union items directly rather than
    /// scanning all items and checking the `for_` field. This provides clearer
    /// semantics and leverages `rustdoc_types` structured data.
    fn build_impl_map(krate: &'a Crate) -> HashMap<Id, Vec<&'a Impl>> {
        let mut map: HashMap<Id, Vec<&'a Impl>> = HashMap::new();

        // Iterate over all types that can have impl blocks and collect their impls
        for (type_id, item) in &krate.index {
            let impl_ids: &[Id] = match &item.inner {
                ItemEnum::Struct(s) => &s.impls,

                ItemEnum::Enum(e) => &e.impls,

                ItemEnum::Union(u) => &u.impls,

                _ => continue,
            };

            // Look up each impl block and add to the map
            for impl_id in impl_ids {
                if let Some(impl_item) = krate.index.get(impl_id)
                    && let ItemEnum::Impl(impl_block) = &impl_item.inner
                {
                    map.entry(*type_id).or_default().push(impl_block);
                }
            }
        }

        // Sort impl blocks within each type for deterministic output
        for impls in map.values_mut() {
            impls.sort_by(|a, b| Self::impl_sort_key(a).cmp(&Self::impl_sort_key(b)));
        }

        map
    }

    /// Generate a sort key for an impl block.
    ///
    /// Inherent impls (no trait) sort before trait impls.
    /// Trait impls are sorted by trait name.
    fn impl_sort_key(impl_block: &Impl) -> (u8, String) {
        impl_block
            .trait_
            .as_ref()
            .map_or_else(|| (0, String::new()), |path| (1, path.path.clone()))
    }

    /// Check if an item should be included based on visibility settings.
    ///
    /// By default, all items are included. If `--exclude-private`
    /// is set, only public items are included.
    ///
    /// # Visibility Levels
    ///
    /// - `Public` - Always included
    /// - `Crate`, `Restricted`, `Default` - Included by default, excluded with `--exclude-private`
    #[must_use]
    pub const fn should_include_item(&self, item: &Item) -> bool {
        match &item.visibility {
            Visibility::Public => true,
            _ => !self.args.exclude_private,
        }
    }

    /// Count the total number of modules that will be generated.
    ///
    /// Used to initialize the progress bar with the correct total.
    /// Respects the `--exclude-private` flag when counting.
    #[must_use]
    pub fn count_modules(&self, item: &Item) -> usize {
        let mut count = 0;

        if let ItemEnum::Module(module) = &item.inner {
            for item_id in &module.items {
                if let Some(child) = self.krate.index.get(item_id)
                    && let ItemEnum::Module(_) = &child.inner
                    && self.should_include_item(child)
                {
                    count += 1;
                    count += self.count_modules(child);
                }
            }
        }

        count
    }

    /// Build an index mapping item names to their IDs for fast lookup.
    ///
    /// This index is built once at context construction time and shared
    /// across all `DocLinkProcessor` instances, eliminating redundant
    /// index building for each item with documentation.
    fn build_path_name_index(krate: &'a Crate) -> HashMap<&'a str, Vec<Id>> {
        let mut index: HashMap<&'a str, Vec<Id>> = HashMap::new();

        for (id, path_info) in &krate.paths {
            if let Some(name) = path_info.path.last() {
                index.entry(name.as_str()).or_default().push(*id);
            }
        }

        // Sort each Vec by full path for deterministic resolution order
        // Using direct Vec<String> comparison (lexicographic) instead of joining
        for ids in index.values_mut() {
            ids.sort_by(|a, b| {
                let path_a = krate.paths.get(a).map(|p| &p.path);
                let path_b = krate.paths.get(b).map(|p| &p.path);

                path_a.cmp(&path_b)
            });
        }

        index
    }
}

impl ItemAccess for GeneratorContext<'_> {
    fn krate(&self) -> &Crate {
        self.krate
    }

    fn crate_name(&self) -> &str {
        &self.crate_name
    }

    fn get_item(&self, id: &Id) -> Option<&Item> {
        self.krate.index.get(id)
    }

    fn get_impls(&self, id: &Id) -> Option<&[&Impl]> {
        self.impl_map.get(id).map(Vec::as_slice)
    }

    fn crate_version(&self) -> Option<&str> {
        self.krate.crate_version.as_deref()
    }

    fn render_config(&self) -> &RenderConfig {
        &self.config
    }

    fn source_path_config_for_file(&self, current_file: &str) -> Option<SourcePathConfig> {
        self.source_path_config
            .as_ref()
            .map(|base| base.with_depth(current_file))
    }
}

impl ItemFilter for GeneratorContext<'_> {
    fn should_include_item(&self, item: &Item) -> bool {
        match &item.visibility {
            Visibility::Public => true,
            _ => !self.args.exclude_private,
        }
    }

    fn include_private(&self) -> bool {
        !self.args.exclude_private
    }

    fn include_blanket_impls(&self) -> bool {
        self.args.include_blanket_impls
    }
}

impl LinkResolver for GeneratorContext<'_> {
    fn link_registry(&self) -> Option<&LinkRegistry> {
        Some(&self.link_registry)
    }

    fn process_docs(&self, item: &Item, current_file: &str) -> Option<String> {
        let docs = item.docs.as_ref()?;
        let name = item.name.as_deref().unwrap_or("");

        // Strip duplicate title if docs start with "# name"
        let docs = DocLinkUtils::strip_duplicate_title(docs, name);

        // Use pre-built index for efficiency (avoids rebuilding for each item)
        let processor = DocLinkProcessor::with_index(
            self.krate,
            &self.link_registry,
            current_file,
            &self.path_name_index,
        );
        Some(processor.process(docs, &item.links))
    }

    fn create_link(&self, id: Id, current_file: &str) -> Option<String> {
        self.link_registry.create_link(id, current_file)
    }
}

// Blanket implementation: any type that implements all three sub-traits
// automatically implements RenderContext
impl<T: ItemAccess + ItemFilter + LinkResolver> RenderContext for T {}