1pub mod breadcrumbs;
41mod capture;
42mod context;
43pub mod doc_links;
44mod flat;
45pub mod impl_category;
46pub mod impls;
47mod items;
48pub mod module;
49mod nested;
50pub mod quick_ref;
51pub mod render_shared;
52pub mod toc;
53
54pub use breadcrumbs::BreadcrumbGenerator;
55pub use capture::MarkdownCapture;
56pub mod config;
57pub use config::{RenderConfig, SourceConfig};
58pub use context::{GeneratorContext, ItemAccess, ItemFilter, LinkResolver, RenderContext};
59pub use doc_links::{DocLinkProcessor, DocLinkUtils};
60use flat::FlatGenerator;
61use fs_err as fs;
62pub use impl_category::ImplCategory;
63use indicatif::{ProgressBar, ProgressStyle};
64pub use module::ModuleRenderer;
65use nested::NestedGenerator;
66pub use quick_ref::{QuickRefEntry, QuickRefGenerator, extract_summary};
67use rustdoc_types::{Crate, Item, ItemEnum};
68pub use toc::{TocEntry, TocGenerator};
69use tracing::{debug, info, instrument};
70
71use crate::error::Error;
72use crate::{Args, CliOutputFormat};
73
74pub struct Generator<'a> {
87 ctx: GeneratorContext<'a>,
89
90 args: &'a Args,
92
93 root_item: &'a Item,
95}
96
97impl<'a> Generator<'a> {
98 pub fn new(krate: &'a Crate, args: &'a Args, config: RenderConfig) -> Result<Self, Error> {
115 let root_item = krate
116 .index
117 .get(&krate.root)
118 .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
119
120 let ctx = GeneratorContext::new(krate, args, config);
121
122 Ok(Self {
123 ctx,
124 args,
125 root_item,
126 })
127 }
128
129 #[instrument(skip(self), fields(
141 crate_name = %self.ctx.crate_name(),
142 format = ?self.args.format,
143 output = %self.args.output.display()
144 ))]
145 pub fn generate(&self) -> Result<(), Error> {
146 info!("Starting single-crate documentation generation");
147
148 fs::create_dir_all(&self.args.output).map_err(Error::CreateDir)?;
150 debug!(path = %self.args.output.display(), "Created output directory");
151
152 let total_modules = self.ctx.count_modules(self.root_item) + 1;
154 debug!(total_modules, "Counted modules for progress tracking");
155 let progress = Self::create_progress_bar(total_modules)?;
156
157 match self.args.format {
159 CliOutputFormat::Flat => {
160 debug!("Using flat output format");
161 let generator = FlatGenerator::new(&self.ctx, &self.args.output, &progress);
162 generator.generate(self.root_item)?;
163 },
164 CliOutputFormat::Nested => {
165 debug!("Using nested output format");
166 let generator = NestedGenerator::new(&self.ctx, &self.args.output, &progress);
167 generator.generate(self.root_item)?;
168 },
169 }
170
171 progress.finish_with_message("done");
172 info!("Single-crate documentation generation complete");
173 Ok(())
174 }
175
176 fn create_progress_bar(total: usize) -> Result<ProgressBar, Error> {
182 let progress = ProgressBar::new(total as u64);
183 let style = ProgressStyle::with_template(
184 "{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} modules",
185 )
186 .map_err(Error::ProgressBarTemplate)?
187 .progress_chars("=>-");
188 progress.set_style(style);
189 Ok(progress)
190 }
191
192 pub fn generate_to_capture(
212 krate: &Crate,
213 format: CliOutputFormat,
214 include_private: bool,
215 ) -> Result<MarkdownCapture, Error> {
216 let args = Args {
218 path: None,
219 dir: None,
220 mdbook: false,
221 search_index: false,
222 primary_crate: None,
223 output: std::path::PathBuf::new(),
224 format,
225 exclude_private: !include_private,
226 include_blanket_impls: false,
227 };
228
229 let root_item = krate
230 .index
231 .get(&krate.root)
232 .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
233
234 let ctx = GeneratorContext::new(krate, &args, RenderConfig::default());
235 let mut capture = MarkdownCapture::new();
236
237 match format {
238 CliOutputFormat::Flat => {
239 Self::generate_flat_to_capture(&ctx, root_item, &mut capture)?;
240 },
241 CliOutputFormat::Nested => {
242 Self::generate_nested_to_capture(&ctx, root_item, "", &mut capture)?;
243 },
244 }
245
246 Ok(capture)
247 }
248
249 pub fn generate_to_capture_with_config(
269 krate: &Crate,
270 format: CliOutputFormat,
271 include_private: bool,
272 config: RenderConfig,
273 ) -> Result<MarkdownCapture, Error> {
274 let args = Args {
276 path: None,
277 dir: None,
278 mdbook: false,
279 search_index: false,
280 primary_crate: None,
281 output: std::path::PathBuf::new(),
282 format,
283 exclude_private: !include_private,
284 include_blanket_impls: false,
285 };
286
287 let root_item = krate
288 .index
289 .get(&krate.root)
290 .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
291
292 let ctx = GeneratorContext::new(krate, &args, config);
293 let mut capture = MarkdownCapture::new();
294
295 match format {
296 CliOutputFormat::Flat => {
297 Self::generate_flat_to_capture(&ctx, root_item, &mut capture)?;
298 },
299 CliOutputFormat::Nested => {
300 Self::generate_nested_to_capture(&ctx, root_item, "", &mut capture)?;
301 },
302 }
303
304 Ok(capture)
305 }
306
307 fn generate_flat_to_capture(
309 ctx: &GeneratorContext,
310 root: &Item,
311 capture: &mut MarkdownCapture,
312 ) -> Result<(), Error> {
313 let renderer = module::ModuleRenderer::new(ctx, "index.md", true);
315 capture.insert("index.md".to_string(), renderer.render(root));
316
317 if let ItemEnum::Module(module) = &root.inner {
319 for item_id in &module.items {
320 if let Some(item) = ctx.krate.index.get(item_id)
321 && let ItemEnum::Module(_) = &item.inner
322 && ctx.should_include_item(item)
323 {
324 Self::generate_flat_recursive_capture(ctx, item, "", capture)?;
325 }
326 }
327 }
328
329 Ok(())
330 }
331
332 fn generate_flat_recursive_capture(
334 ctx: &GeneratorContext,
335 item: &Item,
336 prefix: &str,
337 capture: &mut MarkdownCapture,
338 ) -> Result<(), Error> {
339 let name = item.name.as_deref().unwrap_or("unnamed");
340 let current_file = if prefix.is_empty() {
341 format!("{name}.md")
342 } else {
343 format!("{prefix}__{name}.md")
344 };
345
346 let renderer = module::ModuleRenderer::new(ctx, ¤t_file, false);
347 let content = renderer.render(item);
348 capture.insert(current_file, content);
349
350 let new_prefix = if prefix.is_empty() {
351 name.to_string()
352 } else {
353 format!("{prefix}__{name}")
354 };
355
356 if let ItemEnum::Module(module) = &item.inner {
357 for sub_id in &module.items {
358 if let Some(sub_item) = ctx.krate.index.get(sub_id)
359 && let ItemEnum::Module(_) = &sub_item.inner
360 && ctx.should_include_item(sub_item)
361 {
362 Self::generate_flat_recursive_capture(ctx, sub_item, &new_prefix, capture)?;
363 }
364 }
365 }
366
367 Ok(())
368 }
369
370 fn generate_nested_to_capture(
372 ctx: &GeneratorContext,
373 root: &Item,
374 path_prefix: &str,
375 capture: &mut MarkdownCapture,
376 ) -> Result<(), Error> {
377 let name = root.name.as_deref().unwrap_or("unnamed");
378 let is_root = path_prefix.is_empty()
379 && name
380 == ctx.krate.index[&ctx.krate.root]
381 .name
382 .as_deref()
383 .unwrap_or("");
384
385 let current_file = if path_prefix.is_empty() {
386 if is_root {
387 "index.md".to_string()
388 } else {
389 format!("{name}/index.md")
390 }
391 } else {
392 format!("{path_prefix}/{name}/index.md")
393 };
394
395 let renderer = module::ModuleRenderer::new(ctx, ¤t_file, is_root);
396 capture.insert(current_file.clone(), renderer.render(root));
397
398 let new_prefix = if path_prefix.is_empty() {
399 if is_root {
400 String::new()
401 } else {
402 name.to_string()
403 }
404 } else {
405 format!("{path_prefix}/{name}")
406 };
407
408 if let ItemEnum::Module(module) = &root.inner {
409 for sub_id in &module.items {
410 if let Some(sub_item) = ctx.krate.index.get(sub_id)
411 && let ItemEnum::Module(_) = &sub_item.inner
412 && ctx.should_include_item(sub_item)
413 {
414 Self::generate_nested_to_capture(ctx, sub_item, &new_prefix, capture)?;
415 }
416 }
417 }
418
419 Ok(())
420 }
421
422 pub fn run(krate: &'a Crate, args: &'a Args) -> Result<(), Error> {
442 let generator = Self::new(krate, args, RenderConfig::default())?;
443 generator.generate()
444 }
445}