Skip to main content

nargo_document/generator/
mod.rs

1pub 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/// 文档生成器核心组件(可克隆)
14#[derive(Clone)]
15struct CoreGenerator {
16    /// 配置信息
17    config: Config,
18    /// Markdown 渲染器
19    markdown_renderer: MarkdownRenderer,
20    /// HTML 生成器
21    html_generator: HtmlGenerator,
22    /// PDF 生成器
23    pdf_generator: PdfGenerator,
24    /// 静态资源处理器
25    static_processor: StaticProcessor,
26}
27
28/// 文档生成器,负责将 Markdown 文件转换为多种格式的文档
29pub struct Generator {
30    /// 核心组件
31    core: CoreGenerator,
32    /// 插件注册表
33    plugin_registry: PluginRegistry,
34}
35
36impl Generator {
37    /// 创建新的文档生成器
38    ///
39    /// # 参数
40    /// * `config` - 文档配置信息
41    ///
42    /// # 返回值
43    /// 返回一个新的文档生成器实例
44    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    /// 获取插件注册表的可变引用
49    ///
50    /// # 返回值
51    /// 返回插件注册表的可变引用
52    pub fn plugin_registry_mut(&mut self) -> &mut PluginRegistry {
53        &mut self.plugin_registry
54    }
55
56    /// 克隆核心组件
57    fn clone_core(&self) -> CoreGenerator {
58        self.core.clone()
59    }
60
61    /// 生成文档
62    ///
63    /// # 参数
64    /// * `input_dir` - 输入目录,包含 Markdown 文件
65    /// * `output_dir` - 输出目录,用于存放生成的文档
66    ///
67    /// # 返回值
68    /// 返回 Result,成功时为 (),失败时为错误信息
69    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        // 1. 创建输出目录
73        fs::create_dir_all(output_dir)?;
74
75        // 2. 初始化所有插件
76        self.plugin_registry.setup_all(None);
77        println!("Initialized {} plugins", self.plugin_registry.plugin_count());
78
79        // 3. 扫描 Markdown 文件
80        let markdown_files = self.find_markdown_files(input_dir)?;
81        println!("Found {} Markdown files", markdown_files.len());
82
83        // 4. 顺序处理每个 Markdown 文件
84        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        // 5. 处理静态资源
94        self.core.static_processor.copy_static_files(&input_dir, &output_dir)?;
95
96        // 6. 生成索引文件
97        self.generate_index(&output_dir)?;
98
99        Ok(())
100    }
101
102    /// 查找所有 Markdown 文件
103    ///
104    /// # 参数
105    /// * `dir` - 要搜索的目录
106    ///
107    /// # 返回值
108    /// 返回包含所有 Markdown 文件路径的 Vec
109    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    /// 递归扫描目录,查找 Markdown 文件
116    ///
117    /// # 参数
118    /// * `path` - 当前扫描的路径
119    /// * `files` - 用于存储找到的 Markdown 文件路径
120    ///
121    /// # 返回值
122    /// 返回 Result,成功时为 (),失败时为错误信息
123    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    /// 处理单个 Markdown 文件
142    ///
143    /// # 参数
144    /// * `file` - Markdown 文件路径
145    /// * `input_dir` - 输入目录
146    /// * `output_dir` - 输出目录
147    ///
148    /// # 返回值
149    /// 返回 Result,成功时为 (),失败时为错误信息
150    fn process_markdown_file(&self, file: &str, input_dir: &str, output_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
151        // 读取 Markdown 文件
152        let content = fs::read_to_string(file)?;
153
154        // 创建插件上下文
155        let context = PluginContext::from_content(content.clone(), file.to_string());
156
157        // 调用 before_render 钩子
158        let before_context = self.plugin_registry.before_render_all(context);
159
160        // 渲染 Markdown 为 HTML
161        let markdown_html = self.core.markdown_renderer.render(&before_context.content)?;
162
163        // 生成完整的 HTML 页面
164        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        // 创建 after_render 上下文
168        let after_context = PluginContext::new(full_html.clone(), before_context.frontmatter.clone(), file.to_string());
169
170        // 调用 after_render 钩子
171        let final_context = self.plugin_registry.after_render_all(after_context);
172
173        // 计算输出路径
174        let relative_path = Path::new(file).strip_prefix(input_dir)?;
175
176        // 生成 HTML 文件
177        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        // 生成 PDF 文件
185        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    /// 从 Markdown 内容中提取标题
196    ///
197    /// # 参数
198    /// * `content` - Markdown 内容
199    ///
200    /// # 返回值
201    /// 返回提取的标题,如果没有标题则返回 None
202    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    /// 生成索引文件
212    ///
213    /// # 参数
214    /// * `output_dir` - 输出目录
215    ///
216    /// # 返回值
217    /// 返回 Result,成功时为 (),失败时为错误信息
218    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        // 生成 HTML 索引文件
241        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        // 生成 PDF 索引文件
246        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}