pub mod breadcrumbs;
mod capture;
mod context;
pub mod doc_links;
mod flat;
pub mod impl_category;
pub mod impls;
mod items;
pub mod module;
mod nested;
pub mod quick_ref;
pub mod render_shared;
pub mod toc;
pub use breadcrumbs::BreadcrumbGenerator;
pub use capture::MarkdownCapture;
pub mod config;
pub use config::{RenderConfig, SourceConfig};
pub use context::{GeneratorContext, ItemAccess, ItemFilter, LinkResolver, RenderContext};
pub use doc_links::{DocLinkProcessor, DocLinkUtils};
use flat::FlatGenerator;
use fs_err as fs;
pub use impl_category::ImplCategory;
use indicatif::{ProgressBar, ProgressStyle};
pub use module::ModuleRenderer;
use nested::NestedGenerator;
pub use quick_ref::{QuickRefEntry, QuickRefGenerator, extract_summary};
use rustdoc_types::{Crate, Item, ItemEnum};
pub use toc::{TocEntry, TocGenerator};
use tracing::{debug, info, instrument};
use crate::error::Error;
use crate::{Args, CliOutputFormat};
pub struct Generator<'a> {
ctx: GeneratorContext<'a>,
args: &'a Args,
root_item: &'a Item,
}
impl<'a> Generator<'a> {
pub fn new(krate: &'a Crate, args: &'a Args, config: RenderConfig) -> Result<Self, Error> {
let root_item = krate
.index
.get(&krate.root)
.ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
let ctx = GeneratorContext::new(krate, args, config);
Ok(Self {
ctx,
args,
root_item,
})
}
#[instrument(skip(self), fields(
crate_name = %self.ctx.crate_name(),
format = ?self.args.format,
output = %self.args.output.display()
))]
pub fn generate(&self) -> Result<(), Error> {
info!("Starting single-crate documentation generation");
fs::create_dir_all(&self.args.output).map_err(Error::CreateDir)?;
debug!(path = %self.args.output.display(), "Created output directory");
let total_modules = self.ctx.count_modules(self.root_item) + 1;
debug!(total_modules, "Counted modules for progress tracking");
let progress = Self::create_progress_bar(total_modules)?;
match self.args.format {
CliOutputFormat::Flat => {
debug!("Using flat output format");
let generator = FlatGenerator::new(&self.ctx, &self.args.output, &progress);
generator.generate(self.root_item)?;
},
CliOutputFormat::Nested => {
debug!("Using nested output format");
let generator = NestedGenerator::new(&self.ctx, &self.args.output, &progress);
generator.generate(self.root_item)?;
},
}
progress.finish_with_message("done");
info!("Single-crate documentation generation complete");
Ok(())
}
fn create_progress_bar(total: usize) -> Result<ProgressBar, Error> {
let progress = ProgressBar::new(total as u64);
let style = ProgressStyle::with_template(
"{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} modules",
)
.map_err(Error::ProgressBarTemplate)?
.progress_chars("=>-");
progress.set_style(style);
Ok(progress)
}
pub fn generate_to_capture(
krate: &Crate,
format: CliOutputFormat,
include_private: bool,
) -> Result<MarkdownCapture, Error> {
let args = Args {
format,
exclude_private: !include_private,
..Args::default()
};
let root_item = krate
.index
.get(&krate.root)
.ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
let ctx = GeneratorContext::new(krate, &args, RenderConfig::default());
let mut capture = MarkdownCapture::new();
match format {
CliOutputFormat::Flat => {
Self::generate_flat_to_capture(&ctx, root_item, &mut capture)?;
},
CliOutputFormat::Nested => {
Self::generate_nested_to_capture(&ctx, root_item, "", &mut capture)?;
},
}
Ok(capture)
}
pub fn generate_to_capture_with_config(
krate: &Crate,
format: CliOutputFormat,
include_private: bool,
config: RenderConfig,
) -> Result<MarkdownCapture, Error> {
let args = Args {
format,
exclude_private: !include_private,
..Args::default()
};
let root_item = krate
.index
.get(&krate.root)
.ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
let ctx = GeneratorContext::new(krate, &args, config);
let mut capture = MarkdownCapture::new();
match format {
CliOutputFormat::Flat => {
Self::generate_flat_to_capture(&ctx, root_item, &mut capture)?;
},
CliOutputFormat::Nested => {
Self::generate_nested_to_capture(&ctx, root_item, "", &mut capture)?;
},
}
Ok(capture)
}
fn generate_flat_to_capture(
ctx: &GeneratorContext,
root: &Item,
capture: &mut MarkdownCapture,
) -> Result<(), Error> {
let renderer = module::ModuleRenderer::new(ctx, "index.md", true);
capture.insert("index.md".to_string(), renderer.render(root));
if let ItemEnum::Module(module) = &root.inner {
for item_id in &module.items {
if let Some(item) = ctx.krate.index.get(item_id)
&& let ItemEnum::Module(_) = &item.inner
&& ctx.should_include_item(item)
{
Self::generate_flat_recursive_capture(ctx, item, "", capture)?;
}
}
}
Ok(())
}
fn generate_flat_recursive_capture(
ctx: &GeneratorContext,
item: &Item,
prefix: &str,
capture: &mut MarkdownCapture,
) -> Result<(), Error> {
let name = item.name.as_deref().unwrap_or("unnamed");
let current_file = if prefix.is_empty() {
format!("{name}.md")
} else {
format!("{prefix}__{name}.md")
};
let renderer = module::ModuleRenderer::new(ctx, ¤t_file, false);
let content = renderer.render(item);
capture.insert(current_file, content);
let new_prefix = if prefix.is_empty() {
name.to_string()
} else {
format!("{prefix}__{name}")
};
if let ItemEnum::Module(module) = &item.inner {
for sub_id in &module.items {
if let Some(sub_item) = ctx.krate.index.get(sub_id)
&& let ItemEnum::Module(_) = &sub_item.inner
&& ctx.should_include_item(sub_item)
{
Self::generate_flat_recursive_capture(ctx, sub_item, &new_prefix, capture)?;
}
}
}
Ok(())
}
fn generate_nested_to_capture(
ctx: &GeneratorContext,
root: &Item,
path_prefix: &str,
capture: &mut MarkdownCapture,
) -> Result<(), Error> {
let name = root.name.as_deref().unwrap_or("unnamed");
let is_root = path_prefix.is_empty()
&& name
== ctx.krate.index[&ctx.krate.root]
.name
.as_deref()
.unwrap_or("");
let current_file = if path_prefix.is_empty() {
if is_root {
"index.md".to_string()
} else {
format!("{name}/index.md")
}
} else {
format!("{path_prefix}/{name}/index.md")
};
let renderer = module::ModuleRenderer::new(ctx, ¤t_file, is_root);
capture.insert(current_file.clone(), renderer.render(root));
let new_prefix = if path_prefix.is_empty() {
if is_root {
String::new()
} else {
name.to_string()
}
} else {
format!("{path_prefix}/{name}")
};
if let ItemEnum::Module(module) = &root.inner {
for sub_id in &module.items {
if let Some(sub_item) = ctx.krate.index.get(sub_id)
&& let ItemEnum::Module(_) = &sub_item.inner
&& ctx.should_include_item(sub_item)
{
Self::generate_nested_to_capture(ctx, sub_item, &new_prefix, capture)?;
}
}
}
Ok(())
}
pub fn run(krate: &'a Crate, args: &'a Args) -> Result<(), Error> {
let generator = Self::new(krate, args, RenderConfig::default())?;
generator.generate()
}
}