pub mod html;
pub mod markdown;
pub mod pdf;
pub mod static_;
use crate::{
config::Config,
generator::{html::HtmlGenerator, markdown::MarkdownRenderer, pdf::PdfGenerator, static_::StaticProcessor},
plugin::{PluginContext, PluginRegistry},
};
use std::{fs, path::Path};
#[derive(Clone)]
struct CoreGenerator {
config: Config,
markdown_renderer: MarkdownRenderer,
html_generator: HtmlGenerator,
pdf_generator: PdfGenerator,
static_processor: StaticProcessor,
}
pub struct Generator {
core: CoreGenerator,
plugin_registry: PluginRegistry,
}
impl Generator {
pub fn new(config: Config) -> Self {
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() }
}
pub fn plugin_registry_mut(&mut self) -> &mut PluginRegistry {
&mut self.plugin_registry
}
fn clone_core(&self) -> CoreGenerator {
self.core.clone()
}
pub fn generate(&mut self, input_dir: &str, output_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
println!("Generating documentation from {} to {}", input_dir, output_dir);
fs::create_dir_all(output_dir)?;
self.plugin_registry.setup_all(None);
println!("Initialized {} plugins", self.plugin_registry.plugin_count());
let markdown_files = self.find_markdown_files(input_dir)?;
println!("Found {} Markdown files", markdown_files.len());
let input_dir = input_dir.to_string();
let output_dir = output_dir.to_string();
for file in markdown_files.iter() {
if let Err(e) = self.process_markdown_file(file, &input_dir, &output_dir) {
eprintln!("Error processing file {}: {}", file, e);
}
}
self.core.static_processor.copy_static_files(&input_dir, &output_dir)?;
self.generate_index(&output_dir)?;
Ok(())
}
fn find_markdown_files(&self, dir: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut files = Vec::new();
self.scan_dir(Path::new(dir), &mut files)?;
Ok(files)
}
fn scan_dir(&self, path: &Path, files: &mut Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
if path.is_dir() {
for entry in fs::read_dir(path)? {
let entry = entry?;
let entry_path = entry.path();
if entry_path.is_dir() {
self.scan_dir(&entry_path, files)?;
}
else if let Some(ext) = entry_path.extension() {
if ext == "md" || ext == "markdown" {
files.push(entry_path.to_string_lossy().to_string());
}
}
}
}
Ok(())
}
fn process_markdown_file(&self, file: &str, input_dir: &str, output_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
let content = fs::read_to_string(file)?;
let context = PluginContext::from_content(content.clone(), file.to_string());
let before_context = self.plugin_registry.before_render_all(context);
let markdown_html = self.core.markdown_renderer.render(&before_context.content)?;
let title = self.extract_title(&before_context.content).unwrap_or_else(|| "Documentation".to_string());
let full_html = self.core.html_generator.generate(&markdown_html, &title);
let after_context = PluginContext::new(full_html.clone(), before_context.frontmatter.clone(), file.to_string());
let final_context = self.plugin_registry.after_render_all(after_context);
let relative_path = Path::new(file).strip_prefix(input_dir)?;
let html_path = Path::new(output_dir).join(relative_path).with_extension("html");
if let Some(parent) = html_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&html_path, &final_context.content)?;
println!("Generated: {}", html_path.display());
let pdf_path = Path::new(output_dir).join(relative_path).with_extension("pdf");
if let Some(parent) = pdf_path.parent() {
fs::create_dir_all(parent)?;
}
self.core.pdf_generator.generate(&final_context.content, &pdf_path)?;
println!("Generated: {}", pdf_path.display());
Ok(())
}
fn extract_title(&self, content: &str) -> Option<String> {
for line in content.lines() {
if line.starts_with("# ") {
return Some(line[2..].trim().to_string());
}
}
None
}
fn generate_index(&self, output_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
let index_content = r#"
# HXO Documentation
Welcome to HXO Documentation!
## Getting Started
- [Installation](installation.html)
- [Quick Start](quick-start.html)
- [Configuration](configuration.html)
## API Reference
- [Core Concepts](core-concepts.html)
- [CLI Commands](cli-commands.html)
- [API Documentation](api-documentation.html)
"#;
let markdown_html = self.core.markdown_renderer.render(index_content)?;
let full_html = self.core.html_generator.generate(&markdown_html, "HXO Documentation");
let index_path = Path::new(output_dir).join("index.html");
fs::write(&index_path, &full_html)?;
println!("Generated: {}", index_path.display());
let pdf_path = Path::new(output_dir).join("index.pdf");
self.core.pdf_generator.generate(&full_html, &pdf_path)?;
println!("Generated: {}", pdf_path.display());
Ok(())
}
}