cargo_docs_md/multi_crate/
summary.rs

1//! mdBook SUMMARY.md generator.
2//!
3//! This module provides [`SummaryGenerator`] which creates a SUMMARY.md file
4//! compatible with mdBook for multi-crate documentation.
5
6use std::fmt::Write;
7use std::path::Path;
8
9use fs_err as FsErr;
10use rustdoc_types::{ItemEnum, Visibility};
11
12use crate::error::Error;
13use crate::multi_crate::CrateCollection;
14
15/// Generates mdBook-compatible SUMMARY.md file.
16///
17/// Creates a table of contents linking all crates and their modules,
18/// allowing the documentation to be built as an mdBook site.
19///
20/// # Output Format
21///
22/// ```markdown
23/// # Summary
24///
25/// - [tracing](tracing/index.md)
26///   - [span](tracing/span/index.md)
27///   - [field](tracing/field/index.md)
28/// - [tracing_core](tracing_core/index.md)
29///   - [subscriber](tracing_core/subscriber/index.md)
30/// ```
31pub struct SummaryGenerator<'a> {
32    /// Collection of crates to document.
33    crates: &'a CrateCollection,
34
35    /// Output directory for SUMMARY.md.
36    output_dir: &'a Path,
37
38    /// Whether to include private items.
39    include_private: bool,
40}
41
42impl<'a> SummaryGenerator<'a> {
43    /// Create a new summary generator.
44    ///
45    /// # Arguments
46    ///
47    /// * `crates` - Collection of parsed crates
48    /// * `output_dir` - Directory to write SUMMARY.md
49    /// * `include_private` - Whether to include private modules
50    #[must_use]
51    pub const fn new(
52        crates: &'a CrateCollection,
53        output_dir: &'a Path,
54        include_private: bool,
55    ) -> Self {
56        Self {
57            crates,
58            output_dir,
59            include_private,
60        }
61    }
62
63    /// Generate the SUMMARY.md file.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if the file cannot be written.
68    pub fn generate(&self) -> Result<(), Error> {
69        let mut content = String::from("# Summary\n\n");
70
71        for (crate_name, krate) in self.crates.iter() {
72            // Add crate entry
73            _ = writeln!(content, "- [{crate_name}]({crate_name}/index.md)");
74
75            // Add module entries
76            if let Some(root) = krate.index.get(&krate.root)
77                && let ItemEnum::Module(module) = &root.inner
78            {
79                self.add_modules(&mut content, krate, &module.items, crate_name, 1);
80            }
81        }
82
83        let summary_path = self.output_dir.join("SUMMARY.md");
84        FsErr::write(&summary_path, content).map_err(Error::FileWrite)?;
85
86        Ok(())
87    }
88
89    /// Add module entries recursively.
90    fn add_modules(
91        &self,
92        content: &mut String,
93        krate: &rustdoc_types::Crate,
94        items: &[rustdoc_types::Id],
95        path_prefix: &str,
96        indent: usize,
97    ) {
98        // Collect and sort modules alphabetically, filtering by visibility
99        let mut modules: Vec<_> = items
100            .iter()
101            .filter_map(|id| krate.index.get(id))
102            .filter(|item| matches!(&item.inner, ItemEnum::Module(_)))
103            .filter(|item| {
104                // Include item if include_private is true OR item is public
105                self.include_private || matches!(item.visibility, Visibility::Public)
106            })
107            .collect();
108
109        modules.sort_by_key(|item| item.name.as_deref().unwrap_or(""));
110
111        for item in modules {
112            let name = item.name.as_deref().unwrap_or("unnamed");
113            let indent_str = "  ".repeat(indent);
114            let link_path = format!("{path_prefix}/{name}/index.md");
115
116            _ = writeln!(content, "{indent_str}- [{name}]({link_path})");
117
118            // Recurse into child modules
119            if let ItemEnum::Module(module) = &item.inner {
120                let child_prefix = format!("{path_prefix}/{name}");
121
122                self.add_modules(content, krate, &module.items, &child_prefix, indent + 1);
123            }
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_summary_generation() {
134        // Basic test that the generator can be constructed
135        let crates = CrateCollection::new();
136        let temp_dir = std::env::temp_dir();
137        let generator = SummaryGenerator::new(&crates, &temp_dir, false);
138
139        // Empty crates should produce minimal output
140        assert!(generator.crates.is_empty());
141    }
142
143    #[test]
144    fn test_summary_respects_include_private() {
145        // Verify the flag is stored correctly
146        let crates = CrateCollection::new();
147        let temp_dir = std::env::temp_dir();
148
149        let public_only = SummaryGenerator::new(&crates, &temp_dir, false);
150        assert!(!public_only.include_private);
151
152        let with_private = SummaryGenerator::new(&crates, &temp_dir, true);
153        assert!(with_private.include_private);
154    }
155}