1pub mod breadcrumbs;
41mod capture;
42mod context;
43pub mod doc_links;
44mod flat;
45pub mod impls;
46mod items;
47pub mod module;
48mod nested;
49pub mod render_shared;
50
51pub use breadcrumbs::BreadcrumbGenerator;
52pub use capture::MarkdownCapture;
53pub use context::{GeneratorContext, ItemAccess, ItemFilter, LinkResolver, RenderContext};
54pub use doc_links::{
55 DocLinkProcessor, convert_html_links, convert_path_reference_links, strip_duplicate_title,
56 strip_reference_definitions,
57};
58use flat::FlatGenerator;
59use fs_err as fs;
60use indicatif::{ProgressBar, ProgressStyle};
61pub use module::ModuleRenderer;
62use nested::NestedGenerator;
63use rustdoc_types::{Crate, Item, ItemEnum};
64use tracing::{debug, info, instrument};
65
66use crate::error::Error;
67use crate::{Args, CliOutputFormat};
68
69pub struct Generator<'a> {
82 ctx: GeneratorContext<'a>,
84
85 args: &'a Args,
87
88 root_item: &'a Item,
90}
91
92impl<'a> Generator<'a> {
93 pub fn new(krate: &'a Crate, args: &'a Args) -> Result<Self, Error> {
109 let root_item = krate
110 .index
111 .get(&krate.root)
112 .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
113
114 let ctx = GeneratorContext::new(krate, args);
115
116 Ok(Self {
117 ctx,
118 args,
119 root_item,
120 })
121 }
122
123 #[instrument(skip(self), fields(
135 crate_name = %self.ctx.crate_name(),
136 format = ?self.args.format,
137 output = %self.args.output.display()
138 ))]
139 pub fn generate(&self) -> Result<(), Error> {
140 info!("Starting single-crate documentation generation");
141
142 fs::create_dir_all(&self.args.output).map_err(Error::CreateDir)?;
144 debug!(path = %self.args.output.display(), "Created output directory");
145
146 let total_modules = self.ctx.count_modules(self.root_item) + 1;
148 debug!(total_modules, "Counted modules for progress tracking");
149 let progress = Self::create_progress_bar(total_modules)?;
150
151 match self.args.format {
153 CliOutputFormat::Flat => {
154 debug!("Using flat output format");
155 let generator = FlatGenerator::new(&self.ctx, &self.args.output, &progress);
156 generator.generate(self.root_item)?;
157 },
158 CliOutputFormat::Nested => {
159 debug!("Using nested output format");
160 let generator = NestedGenerator::new(&self.ctx, &self.args.output, &progress);
161 generator.generate(self.root_item)?;
162 },
163 }
164
165 progress.finish_with_message("done");
166 info!("Single-crate documentation generation complete");
167 Ok(())
168 }
169
170 fn create_progress_bar(total: usize) -> Result<ProgressBar, Error> {
176 let progress = ProgressBar::new(total as u64);
177 let style = ProgressStyle::with_template(
178 "{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} modules",
179 )
180 .map_err(Error::ProgressBarTemplate)?
181 .progress_chars("=>-");
182 progress.set_style(style);
183 Ok(progress)
184 }
185
186 pub fn generate_to_capture(
206 krate: &Crate,
207 format: CliOutputFormat,
208 include_private: bool,
209 ) -> Result<MarkdownCapture, Error> {
210 let args = Args {
212 path: None,
213 dir: None,
214 mdbook: false,
215 search_index: false,
216 primary_crate: None,
217 output: std::path::PathBuf::new(),
218 format,
219 exclude_private: !include_private,
220 include_blanket_impls: false,
221 };
222
223 let root_item = krate
224 .index
225 .get(&krate.root)
226 .ok_or_else(|| Error::ItemNotFound(krate.root.0.to_string()))?;
227
228 let ctx = GeneratorContext::new(krate, &args);
229 let mut capture = MarkdownCapture::new();
230
231 match format {
232 CliOutputFormat::Flat => {
233 Self::generate_flat_to_capture(&ctx, root_item, &mut capture)?;
234 },
235 CliOutputFormat::Nested => {
236 Self::generate_nested_to_capture(&ctx, root_item, "", &mut capture)?;
237 },
238 }
239
240 Ok(capture)
241 }
242
243 fn generate_flat_to_capture(
245 ctx: &GeneratorContext,
246 root: &Item,
247 capture: &mut MarkdownCapture,
248 ) -> Result<(), Error> {
249 let renderer = module::ModuleRenderer::new(ctx, "index.md", true);
251 capture.insert("index.md".to_string(), renderer.render(root));
252
253 if let ItemEnum::Module(module) = &root.inner {
255 for item_id in &module.items {
256 if let Some(item) = ctx.krate.index.get(item_id)
257 && let ItemEnum::Module(_) = &item.inner
258 && ctx.should_include_item(item)
259 {
260 Self::generate_flat_recursive_capture(ctx, item, "", capture)?;
261 }
262 }
263 }
264
265 Ok(())
266 }
267
268 fn generate_flat_recursive_capture(
270 ctx: &GeneratorContext,
271 item: &Item,
272 prefix: &str,
273 capture: &mut MarkdownCapture,
274 ) -> Result<(), Error> {
275 let name = item.name.as_deref().unwrap_or("unnamed");
276 let current_file = if prefix.is_empty() {
277 format!("{name}.md")
278 } else {
279 format!("{prefix}__{name}.md")
280 };
281
282 let renderer = module::ModuleRenderer::new(ctx, ¤t_file, false);
283 let content = renderer.render(item);
284 capture.insert(current_file, content);
285
286 let new_prefix = if prefix.is_empty() {
287 name.to_string()
288 } else {
289 format!("{prefix}__{name}")
290 };
291
292 if let ItemEnum::Module(module) = &item.inner {
293 for sub_id in &module.items {
294 if let Some(sub_item) = ctx.krate.index.get(sub_id)
295 && let ItemEnum::Module(_) = &sub_item.inner
296 && ctx.should_include_item(sub_item)
297 {
298 Self::generate_flat_recursive_capture(ctx, sub_item, &new_prefix, capture)?;
299 }
300 }
301 }
302
303 Ok(())
304 }
305
306 fn generate_nested_to_capture(
308 ctx: &GeneratorContext,
309 root: &Item,
310 path_prefix: &str,
311 capture: &mut MarkdownCapture,
312 ) -> Result<(), Error> {
313 let name = root.name.as_deref().unwrap_or("unnamed");
314 let is_root = path_prefix.is_empty()
315 && name
316 == ctx.krate.index[&ctx.krate.root]
317 .name
318 .as_deref()
319 .unwrap_or("");
320
321 let current_file = if path_prefix.is_empty() {
322 if is_root {
323 "index.md".to_string()
324 } else {
325 format!("{name}/index.md")
326 }
327 } else {
328 format!("{path_prefix}/{name}/index.md")
329 };
330
331 let renderer = module::ModuleRenderer::new(ctx, ¤t_file, is_root);
332 capture.insert(current_file.clone(), renderer.render(root));
333
334 let new_prefix = if path_prefix.is_empty() {
335 if is_root {
336 String::new()
337 } else {
338 name.to_string()
339 }
340 } else {
341 format!("{path_prefix}/{name}")
342 };
343
344 if let ItemEnum::Module(module) = &root.inner {
345 for sub_id in &module.items {
346 if let Some(sub_item) = ctx.krate.index.get(sub_id)
347 && let ItemEnum::Module(_) = &sub_item.inner
348 && ctx.should_include_item(sub_item)
349 {
350 Self::generate_nested_to_capture(ctx, sub_item, &new_prefix, capture)?;
351 }
352 }
353 }
354
355 Ok(())
356 }
357
358 pub fn run(krate: &'a Crate, args: &'a Args) -> Result<(), Error> {
376 let generator = Self::new(krate, args)?;
377 generator.generate()
378 }
379}