nargo_document/generator/
mod.rs1pub mod html;
2pub mod markdown;
3pub mod pdf;
4pub mod static_;
5
6use crate::{
7 config::Config,
8 generator::{html::HtmlGenerator, markdown::MarkdownRenderer, pdf::PdfGenerator, static_::StaticProcessor},
9 plugin::{PluginContext, PluginRegistry},
10};
11use std::{fs, path::Path};
12
13#[derive(Clone)]
15struct CoreGenerator {
16 config: Config,
18 markdown_renderer: MarkdownRenderer,
20 html_generator: HtmlGenerator,
22 pdf_generator: PdfGenerator,
24 static_processor: StaticProcessor,
26}
27
28pub struct Generator {
30 core: CoreGenerator,
32 plugin_registry: PluginRegistry,
34}
35
36impl Generator {
37 pub fn new(config: Config) -> Self {
45 Self { core: CoreGenerator { config: config.clone(), markdown_renderer: MarkdownRenderer::new(), html_generator: HtmlGenerator::with_config(config), pdf_generator: PdfGenerator::new(), static_processor: StaticProcessor::new() }, plugin_registry: PluginRegistry::new() }
46 }
47
48 pub fn plugin_registry_mut(&mut self) -> &mut PluginRegistry {
53 &mut self.plugin_registry
54 }
55
56 fn clone_core(&self) -> CoreGenerator {
58 self.core.clone()
59 }
60
61 pub fn generate(&mut self, input_dir: &str, output_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
70 println!("Generating documentation from {} to {}", input_dir, output_dir);
71
72 fs::create_dir_all(output_dir)?;
74
75 self.plugin_registry.setup_all(None);
77 println!("Initialized {} plugins", self.plugin_registry.plugin_count());
78
79 let markdown_files = self.find_markdown_files(input_dir)?;
81 println!("Found {} Markdown files", markdown_files.len());
82
83 let input_dir = input_dir.to_string();
85 let output_dir = output_dir.to_string();
86
87 for file in markdown_files.iter() {
88 if let Err(e) = self.process_markdown_file(file, &input_dir, &output_dir) {
89 eprintln!("Error processing file {}: {}", file, e);
90 }
91 }
92
93 self.core.static_processor.copy_static_files(&input_dir, &output_dir)?;
95
96 self.generate_index(&output_dir)?;
98
99 Ok(())
100 }
101
102 fn find_markdown_files(&self, dir: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
110 let mut files = Vec::new();
111 self.scan_dir(Path::new(dir), &mut files)?;
112 Ok(files)
113 }
114
115 fn scan_dir(&self, path: &Path, files: &mut Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
124 if path.is_dir() {
125 for entry in fs::read_dir(path)? {
126 let entry = entry?;
127 let entry_path = entry.path();
128 if entry_path.is_dir() {
129 self.scan_dir(&entry_path, files)?;
130 }
131 else if let Some(ext) = entry_path.extension() {
132 if ext == "md" || ext == "markdown" {
133 files.push(entry_path.to_string_lossy().to_string());
134 }
135 }
136 }
137 }
138 Ok(())
139 }
140
141 fn process_markdown_file(&self, file: &str, input_dir: &str, output_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
151 let content = fs::read_to_string(file)?;
153
154 let context = PluginContext::from_content(content.clone(), file.to_string());
156
157 let before_context = self.plugin_registry.before_render_all(context);
159
160 let markdown_html = self.core.markdown_renderer.render(&before_context.content)?;
162
163 let title = self.extract_title(&before_context.content).unwrap_or_else(|| "Documentation".to_string());
165 let full_html = self.core.html_generator.generate(&markdown_html, &title);
166
167 let after_context = PluginContext::new(full_html.clone(), before_context.frontmatter.clone(), file.to_string());
169
170 let final_context = self.plugin_registry.after_render_all(after_context);
172
173 let relative_path = Path::new(file).strip_prefix(input_dir)?;
175
176 let html_path = Path::new(output_dir).join(relative_path).with_extension("html");
178 if let Some(parent) = html_path.parent() {
179 fs::create_dir_all(parent)?;
180 }
181 fs::write(&html_path, &final_context.content)?;
182 println!("Generated: {}", html_path.display());
183
184 let pdf_path = Path::new(output_dir).join(relative_path).with_extension("pdf");
186 if let Some(parent) = pdf_path.parent() {
187 fs::create_dir_all(parent)?;
188 }
189 self.core.pdf_generator.generate(&final_context.content, &pdf_path)?;
190 println!("Generated: {}", pdf_path.display());
191
192 Ok(())
193 }
194
195 fn extract_title(&self, content: &str) -> Option<String> {
203 for line in content.lines() {
204 if line.starts_with("# ") {
205 return Some(line[2..].trim().to_string());
206 }
207 }
208 None
209 }
210
211 fn generate_index(&self, output_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
219 let index_content = r#"
220# HXO Documentation
221
222Welcome to HXO Documentation!
223
224## Getting Started
225
226- [Installation](installation.html)
227- [Quick Start](quick-start.html)
228- [Configuration](configuration.html)
229
230## API Reference
231
232- [Core Concepts](core-concepts.html)
233- [CLI Commands](cli-commands.html)
234- [API Documentation](api-documentation.html)
235"#;
236
237 let markdown_html = self.core.markdown_renderer.render(index_content)?;
238 let full_html = self.core.html_generator.generate(&markdown_html, "HXO Documentation");
239
240 let index_path = Path::new(output_dir).join("index.html");
242 fs::write(&index_path, &full_html)?;
243 println!("Generated: {}", index_path.display());
244
245 let pdf_path = Path::new(output_dir).join("index.pdf");
247 self.core.pdf_generator.generate(&full_html, &pdf_path)?;
248 println!("Generated: {}", pdf_path.display());
249
250 Ok(())
251 }
252}