cargo_docs_md/
lib.rs

1//! docs-md library interface for testing and reuse.
2//!
3//! This module exposes the core functionality of docs-md as a library,
4//! allowing integration tests and external tools to use the markdown
5//! generation capabilities programmatically.
6
7#![deny(missing_docs)]
8#![warn(clippy::pedantic)]
9#![warn(clippy::nursery)]
10
11use std::path::PathBuf;
12
13use clap::{Parser, Subcommand, ValueEnum};
14
15pub mod error;
16pub mod generator;
17pub mod linker;
18#[cfg(feature = "trace")]
19pub mod logger;
20pub mod multi_crate;
21pub mod parser;
22pub mod types;
23
24pub use crate::generator::{Generator, MarkdownCapture};
25pub use crate::linker::{LinkRegistry, slugify_anchor};
26#[cfg(feature = "trace")]
27use crate::logger::LogLevel;
28pub use crate::multi_crate::{
29    CrateCollection, MultiCrateContext, MultiCrateGenerator, MultiCrateParser, SearchIndex,
30    SearchIndexGenerator, UnifiedLinkRegistry,
31};
32
33/// Output format for the generated markdown documentation.
34///
35/// Controls how module files are organized in the output directory.
36#[derive(Debug, Clone, Copy, Default, ValueEnum)]
37pub enum OutputFormat {
38    /// Flat structure: all files in one directory.
39    ///
40    /// Module hierarchy is encoded in filenames using double underscores.
41    /// Example: `parent__child__grandchild.md`
42    #[default]
43    Flat,
44
45    /// Nested structure: directories mirror module hierarchy.
46    ///
47    /// Each module gets its own directory with an `index.md` file.
48    /// Example: `parent/child/grandchild/index.md`
49    Nested,
50}
51
52/// Cargo wrapper for subcommand invocation.
53///
54/// When invoked as `cargo docs-md`, cargo passes "docs-md" as the first argument.
55/// This wrapper handles that by making `docs-md` a subcommand that contains the real CLI.
56#[derive(Parser, Debug)]
57#[command(name = "cargo", bin_name = "cargo")]
58pub enum Cargo {
59    /// Generate per-module markdown from rustdoc JSON
60    #[command(name = "docs-md")]
61    DocsMd(Cli),
62}
63
64/// Top-level CLI for docs-md.
65#[derive(Parser, Debug)]
66#[command(
67    author,
68    version,
69    about = "Generate per-module markdown from rustdoc JSON",
70    args_conflicts_with_subcommands = true
71)]
72pub struct Cli {
73    #[command(subcommand)]
74    /// Subcommand to run
75    pub command: Option<Command>,
76
77    #[command(flatten)]
78    /// Generation options (used when no subcommand is specified)
79    pub args: GenerateArgs,
80
81    /// Logging verbosity level
82    ///
83    /// Controls the amount of diagnostic output. Use for debugging link
84    /// resolution issues or understanding the generation process.
85    #[cfg(feature = "trace")]
86    #[arg(long, value_enum, default_value = "off")]
87    pub log_level: LogLevel,
88
89    /// Enable logging to a file instead of stderr
90    ///
91    /// When set, logs are written to this file path instead of stderr.
92    /// Useful for capturing debug output without cluttering terminal.
93    #[cfg(feature = "trace")]
94    #[arg(long)]
95    pub log_file: Option<PathBuf>,
96}
97
98/// Available subcommands
99#[derive(Subcommand, Debug)]
100pub enum Command {
101    /// Build rustdoc JSON and generate markdown in one step.
102    ///
103    /// This runs `cargo +nightly doc` with JSON output, then generates
104    /// markdown documentation from the result. Requires nightly toolchain.
105    ///
106    /// Example: `cargo docs-md docs --primary-crate my_crate`
107    Docs(DocsArgs),
108}
109
110/// Arguments for the `docs` subcommand (build + generate).
111#[derive(Parser, Debug)]
112#[expect(
113    clippy::struct_excessive_bools,
114    reason = "Hm.. Cache lining optimization? Seems unnecessary for a CLI args struct."
115)]
116pub struct DocsArgs {
117    /// Output directory for generated markdown files.
118    ///
119    /// Defaults to `generated_docs/` in the current directory.
120    #[arg(short, long, default_value = "generated_docs")]
121    pub output: PathBuf,
122
123    /// Output format (flat or nested).
124    #[arg(short, long, value_enum, default_value_t = CliOutputFormat::Nested)]
125    pub format: CliOutputFormat,
126
127    /// Primary crate name for preferential link resolution.
128    ///
129    /// If not specified, attempts to detect from Cargo.toml.
130    #[arg(long)]
131    pub primary_crate: Option<String>,
132
133    /// Exclude private (non-public) items from the output.
134    ///
135    /// By default, all items are documented including private ones.
136    /// Enable this to only include public items.
137    #[arg(long, default_value_t = false)]
138    pub exclude_private: bool,
139
140    /// Include blanket trait implementations in the output.
141    #[arg(long, default_value_t = false)]
142    pub include_blanket_impls: bool,
143
144    /// Skip generating mdBook SUMMARY.md file.
145    #[arg(long, default_value_t = false)]
146    pub no_mdbook: bool,
147
148    /// Skip generating `search_index.json` file.
149    #[arg(long, default_value_t = false)]
150    pub no_search_index: bool,
151
152    /// Run cargo clean before building (full rebuild).
153    #[arg(long, default_value_t = false)]
154    pub clean: bool,
155
156    /// Additional arguments to pass to cargo doc.
157    ///
158    /// Example: `docs-md docs -- --all-features`
159    #[arg(last = true)]
160    pub cargo_args: Vec<String>,
161}
162
163/// Command-line arguments for direct generation (no subcommand).
164///
165/// The tool accepts input from two mutually exclusive sources:
166/// 1. A local rustdoc JSON file (`--path`)
167/// 2. A directory of rustdoc JSON files (`--dir`)
168#[derive(Parser, Debug, Default)]
169#[expect(
170    clippy::struct_excessive_bools,
171    reason = "Hm.. Cache lining optimization? Seems unnecessary for a CLI args struct."
172)]
173pub struct GenerateArgs {
174    /// Path to a local rustdoc JSON file.
175    ///
176    /// Generate this file with: `cargo doc --output-format json`
177    /// The JSON file will be in `target/doc/{crate_name}.json`
178    ///
179    /// Mutually exclusive with `--dir`.
180    #[arg(
181        short,
182        long,
183        required_unless_present_any = ["dir"],
184        conflicts_with = "dir"
185    )]
186    pub path: Option<PathBuf>,
187
188    /// Directory containing multiple rustdoc JSON files.
189    ///
190    /// Use this for multi-crate documentation generation. The tool will
191    /// scan the directory for all `*.json` files (rustdoc format) and
192    /// generate documentation for each crate with cross-crate linking.
193    ///
194    /// Generate JSON files with:
195    /// `RUSTDOCFLAGS='-Z unstable-options --output-format json' cargo +nightly doc`
196    ///
197    /// Mutually exclusive with `--path`.
198    #[arg(
199        long,
200        required_unless_present_any = ["path"],
201        conflicts_with = "path"
202    )]
203    pub dir: Option<PathBuf>,
204
205    /// Generate mdBook-compatible SUMMARY.md file.
206    ///
207    /// Only valid with `--dir` for multi-crate documentation.
208    /// Creates a `SUMMARY.md` file in the output directory that can be
209    /// used as the entry point for an mdBook documentation site.
210    #[arg(long, requires = "dir", default_value_t = false)]
211    pub mdbook: bool,
212
213    /// Generate `search_index.json` for client-side search.
214    ///
215    /// Only valid with `--dir` for multi-crate documentation.
216    /// Creates a `search_index.json` file containing all documented items,
217    /// which can be used with client-side search libraries like Fuse.js,
218    /// Lunr.js, or `FlexSearch`.
219    #[arg(long, requires = "dir", default_value_t = false)]
220    pub search_index: bool,
221
222    /// Primary crate name for preferential link resolution.
223    ///
224    /// When specified with `--dir`, links to items in this crate take
225    /// precedence over items with the same name in dependencies.
226    /// This helps resolve ambiguous links like `exit` to the intended
227    /// crate rather than `std::process::exit`.
228    #[arg(long, requires = "dir")]
229    pub primary_crate: Option<String>,
230
231    /// Output directory for generated markdown files.
232    ///
233    /// The directory will be created if it doesn't exist.
234    /// Defaults to `generated_docs/` in the current directory.
235    #[arg(short, long, default_value = "generated_docs")]
236    pub output: PathBuf,
237
238    /// Output format (flat or nested).
239    ///
240    /// - `flat`: All files in one directory
241    /// - `nested`: Directory hierarchy mirroring modules (default)
242    #[arg(short, long, value_enum, default_value_t = CliOutputFormat::Nested)]
243    pub format: CliOutputFormat,
244
245    /// Exclude private (non-public) items from the output.
246    ///
247    /// By default, all items are documented including `pub(crate)`,
248    /// `pub(super)`, and private items. Enable this to only include
249    /// public items.
250    #[arg(long, default_value_t = false)]
251    pub exclude_private: bool,
252
253    /// Include blanket trait implementations in the output.
254    ///
255    /// By default, blanket impls like `From`, `Into`, `TryFrom`, `TryInto`,
256    /// `Any`, `Borrow`, `BorrowMut`, and `ToOwned` are filtered out to reduce
257    /// noise. Enable this to include them in the documentation.
258    #[arg(long, default_value_t = false)]
259    pub include_blanket_impls: bool,
260}
261
262/// Backwards-compatible type alias for existing code.
263pub type Args = GenerateArgs;
264
265/// CLI-compatible output format enum (for clap `ValueEnum` derive).
266#[derive(Clone, Copy, Debug, Default, ValueEnum)]
267pub enum CliOutputFormat {
268    /// Flat structure with double-underscore separators in filenames.
269    #[default]
270    Flat,
271
272    /// Nested directory structure mirroring the module hierarchy.
273    Nested,
274}
275
276impl From<CliOutputFormat> for OutputFormat {
277    fn from(cli: CliOutputFormat) -> Self {
278        match cli {
279            CliOutputFormat::Flat => Self::Flat,
280
281            CliOutputFormat::Nested => Self::Nested,
282        }
283    }
284}