#![deny(missing_docs)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
pub mod error;
pub mod generator;
pub mod linker;
#[cfg(feature = "trace")]
pub mod logger;
pub mod multi_crate;
pub mod parser;
#[cfg(feature = "source-parsing")]
pub mod source;
pub mod types;
pub mod utils;
pub use crate::generator::{Generator, MarkdownCapture, RenderConfig, SourceConfig};
pub use crate::linker::{AnchorUtils, LinkRegistry};
#[cfg(feature = "trace")]
use crate::logger::LogLevel;
pub use crate::multi_crate::{
CrateCollection, MultiCrateContext, MultiCrateGenerator, MultiCrateParser, SearchIndex,
SearchIndexGenerator, UnifiedLinkRegistry,
};
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum OutputFormat {
#[default]
Flat,
Nested,
}
#[derive(Parser, Debug)]
#[command(name = "cargo", bin_name = "cargo")]
pub enum Cargo {
#[command(name = "docs-md")]
DocsMd(Cli),
}
#[derive(Parser, Debug)]
#[command(
author,
version,
about = "Generate per-module markdown from rustdoc JSON",
args_conflicts_with_subcommands = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[command(flatten)]
pub args: GenerateArgs,
#[cfg(feature = "trace")]
#[arg(long, value_enum, default_value = "off")]
pub log_level: LogLevel,
#[cfg(feature = "trace")]
#[arg(long)]
pub log_file: Option<PathBuf>,
}
#[derive(Subcommand, Debug)]
pub enum Command {
Docs(DocsArgs),
#[cfg(feature = "source-parsing")]
CollectSources(CollectSourcesArgs),
}
#[derive(Parser, Debug)]
#[expect(
clippy::struct_excessive_bools,
reason = "Hm.. Cache lining optimization? Seems unnecessary for a CLI args struct."
)]
pub struct DocsArgs {
#[arg(short, long, default_value = "generated_docs")]
pub output: PathBuf,
#[arg(short, long, value_enum, default_value_t = CliOutputFormat::Nested)]
pub format: CliOutputFormat,
#[arg(long)]
pub primary_crate: Option<String>,
#[arg(long, default_value_t = false)]
pub exclude_private: bool,
#[arg(long, default_value_t = false)]
pub include_blanket_impls: bool,
#[arg(long, default_value_t = false)]
pub no_mdbook: bool,
#[arg(long, default_value_t = false)]
pub no_search_index: bool,
#[arg(long, default_value_t = false)]
pub clean: bool,
#[arg(long, default_value_t = 10)]
pub toc_threshold: usize,
#[arg(long, default_value_t = false)]
pub no_quick_reference: bool,
#[arg(long, default_value_t = false)]
pub no_group_impls: bool,
#[arg(long, default_value_t = false)]
pub hide_trivial_derives: bool,
#[arg(long, default_value_t = false)]
pub no_method_anchors: bool,
#[arg(long, default_value_t = false)]
pub source_locations: bool,
#[arg(long, default_value_t = false)]
pub full_method_docs: bool,
#[arg(last = true)]
pub cargo_args: Vec<String>,
}
#[cfg(feature = "source-parsing")]
#[derive(Parser, Debug)]
pub struct CollectSourcesArgs {
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub include_dev: bool,
#[arg(long, default_value_t = false)]
pub dry_run: bool,
#[arg(long)]
pub manifest_path: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub minimal_sources: bool,
#[arg(long, default_value_t = false)]
pub no_gitignore: bool,
}
#[derive(Parser, Debug, Default)]
#[expect(
clippy::struct_excessive_bools,
reason = "Hm.. Cache lining optimization? Seems unnecessary for a CLI args struct."
)]
pub struct GenerateArgs {
#[arg(
short,
long,
required_unless_present_any = ["dir"],
conflicts_with = "dir"
)]
pub path: Option<PathBuf>,
#[arg(
long,
required_unless_present_any = ["path"],
conflicts_with = "path"
)]
pub dir: Option<PathBuf>,
#[arg(long, requires = "dir", default_value_t = false)]
pub no_mdbook: bool,
#[arg(long, requires = "dir", default_value_t = false)]
pub no_search_index: bool,
#[arg(long, requires = "dir")]
pub primary_crate: Option<String>,
#[arg(short, long, default_value = "generated_docs")]
pub output: PathBuf,
#[arg(short, long, value_enum, default_value_t = CliOutputFormat::Nested)]
pub format: CliOutputFormat,
#[arg(long, default_value_t = false)]
pub exclude_private: bool,
#[arg(long, default_value_t = false)]
pub include_blanket_impls: bool,
#[arg(long, default_value_t = 10)]
pub toc_threshold: usize,
#[arg(long, default_value_t = false)]
pub no_quick_reference: bool,
#[arg(long, default_value_t = false)]
pub no_group_impls: bool,
#[arg(long, default_value_t = false)]
pub hide_trivial_derives: bool,
#[arg(long, default_value_t = false)]
pub no_method_anchors: bool,
#[arg(long, default_value_t = false)]
pub source_locations: bool,
#[arg(long, default_value_t = false)]
pub full_method_docs: bool,
}
pub type Args = GenerateArgs;
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
pub enum CliOutputFormat {
#[default]
Flat,
Nested,
}
impl From<CliOutputFormat> for OutputFormat {
fn from(cli: CliOutputFormat) -> Self {
match cli {
CliOutputFormat::Flat => Self::Flat,
CliOutputFormat::Nested => Self::Nested,
}
}
}
#[inline(never)]
#[must_use]
pub fn iter_zip(a: Vec<i64>, b: Vec<i64>) -> i64 {
let mut r = 0i64;
assert!(a.len() == b.len());
for (x, y) in a.iter().zip(b.iter()) {
r += x + y;
}
r
}
#[inline(never)]
#[must_use]
pub fn index_loop(a: Vec<i64>, b: Vec<i64>) -> i64 {
let mut r = 0i64;
assert!(a.len() == b.len());
for i in 0..a.len() {
r += a[i] + b[i];
}
r
}
#[inline(never)]
#[must_use]
pub fn index_loop_no_assert(a: Vec<i64>, b: Vec<i64>) -> i64 {
let mut r = 0i64;
for i in 0..a.len() {
r += a[i] + b[i];
}
r
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn test_generate_args_no_mdbook_flag() {
let args = GenerateArgs::try_parse_from(["test", "--dir", "target/doc"]).unwrap();
assert!(!args.no_mdbook, "no_mdbook should default to false");
let args =
GenerateArgs::try_parse_from(["test", "--dir", "target/doc", "--no-mdbook"]).unwrap();
assert!(args.no_mdbook, "no_mdbook should be true when flag is set");
}
#[test]
fn test_generate_args_no_search_index_flag() {
let args = GenerateArgs::try_parse_from(["test", "--dir", "target/doc"]).unwrap();
assert!(
!args.no_search_index,
"no_search_index should default to false"
);
let args =
GenerateArgs::try_parse_from(["test", "--dir", "target/doc", "--no-search-index"])
.unwrap();
assert!(
args.no_search_index,
"no_search_index should be true when flag is set"
);
}
#[test]
fn test_docs_args_no_mdbook_flag() {
let args = DocsArgs::try_parse_from(["test"]).unwrap();
assert!(
!args.no_mdbook,
"no_mdbook should default to false (mdbook enabled)"
);
let args = DocsArgs::try_parse_from(["test", "--no-mdbook"]).unwrap();
assert!(args.no_mdbook, "no_mdbook should be true when flag is set");
}
#[test]
fn test_docs_args_no_search_index_flag() {
let args = DocsArgs::try_parse_from(["test"]).unwrap();
assert!(
!args.no_search_index,
"no_search_index should default to false (search index enabled)"
);
let args = DocsArgs::try_parse_from(["test", "--no-search-index"]).unwrap();
assert!(
args.no_search_index,
"no_search_index should be true when flag is set"
);
}
#[test]
fn test_docs_and_generate_args_consistent_defaults() {
let docs_args = DocsArgs::try_parse_from(["test"]).unwrap();
let generate_args = GenerateArgs::try_parse_from(["test", "--dir", "target/doc"]).unwrap();
assert_eq!(
docs_args.no_mdbook, generate_args.no_mdbook,
"DocsArgs and GenerateArgs should have same default for no_mdbook"
);
assert_eq!(
docs_args.no_search_index, generate_args.no_search_index,
"DocsArgs and GenerateArgs should have same default for no_search_index"
);
}
#[cfg(feature = "source-parsing")]
#[test]
fn test_collect_sources_args_minimal_sources_flag() {
let args = CollectSourcesArgs::try_parse_from(["test"]).unwrap();
assert!(
!args.minimal_sources,
"minimal_sources should default to false (full copy)"
);
let args = CollectSourcesArgs::try_parse_from(["test", "--minimal-sources"]).unwrap();
assert!(
args.minimal_sources,
"minimal_sources should be true when flag is set"
);
}
#[cfg(feature = "source-parsing")]
#[test]
fn test_collect_sources_args_no_gitignore_flag() {
let args = CollectSourcesArgs::try_parse_from(["test"]).unwrap();
assert!(
!args.no_gitignore,
"no_gitignore should default to false (update gitignore)"
);
let args = CollectSourcesArgs::try_parse_from(["test", "--no-gitignore"]).unwrap();
assert!(
args.no_gitignore,
"no_gitignore should be true when flag is set"
);
}
#[test]
fn test_generate_args_requires_path_or_dir() {
let result = GenerateArgs::try_parse_from(["test"]);
assert!(
result.is_err(),
"Should require either --path or --dir"
);
let result = GenerateArgs::try_parse_from(["test", "--path", "file.json"]);
assert!(result.is_ok(), "Should accept --path alone");
let result = GenerateArgs::try_parse_from(["test", "--dir", "target/doc"]);
assert!(result.is_ok(), "Should accept --dir alone");
let result =
GenerateArgs::try_parse_from(["test", "--path", "file.json", "--dir", "target/doc"]);
assert!(
result.is_err(),
"--path and --dir should be mutually exclusive"
);
}
#[test]
fn test_mdbook_search_index_with_dir() {
let result = GenerateArgs::try_parse_from([
"test",
"--dir",
"target/doc",
"--no-mdbook",
"--no-search-index",
]);
assert!(
result.is_ok(),
"--no-mdbook and --no-search-index should work with --dir"
);
let args = result.unwrap();
assert!(args.no_mdbook, "--no-mdbook should be true");
assert!(args.no_search_index, "--no-search-index should be true");
}
#[test]
fn test_generate_args_defaults() {
let args = GenerateArgs::try_parse_from(["test", "--dir", "target/doc"]).unwrap();
assert_eq!(
args.output,
PathBuf::from("generated_docs"),
"output should default to generated_docs"
);
assert!(
matches!(args.format, CliOutputFormat::Nested),
"format should default to Nested"
);
assert!(!args.exclude_private, "exclude_private should default to false");
assert!(
!args.include_blanket_impls,
"include_blanket_impls should default to false"
);
assert_eq!(args.toc_threshold, 10, "toc_threshold should default to 10");
assert!(
!args.no_quick_reference,
"no_quick_reference should default to false"
);
assert!(!args.no_group_impls, "no_group_impls should default to false");
assert!(
!args.hide_trivial_derives,
"hide_trivial_derives should default to false"
);
assert!(
!args.no_method_anchors,
"no_method_anchors should default to false"
);
assert!(
!args.source_locations,
"source_locations should default to false"
);
assert!(
!args.full_method_docs,
"full_method_docs should default to false"
);
}
}