1mod config;
2mod error;
3mod html_generator;
4mod io;
5mod lexer;
6mod parser;
7mod thread_pool;
8mod types;
9mod utils;
10
11use clap::{Parser, command};
12use env_logger::Env;
13use log::{error, info};
14use std::path::Path;
15use std::sync::{Arc, OnceLock};
16
17use crate::config::{Config, init_config};
18use crate::error::Error;
19use crate::html_generator::{generate_html, generate_index};
20use crate::io::{
21 copy_css_to_output_dir, copy_favicon_to_output_dir, read_input_dir, write_default_css_file,
22 write_html_to_file,
23};
24use crate::lexer::tokenize;
25use crate::parser::{group_lines_to_blocks, parse_blocks};
26use crate::thread_pool::ThreadPool;
27use crate::types::Token;
28
29static CONFIG: OnceLock<Config> = OnceLock::new();
30
31#[derive(Parser, Debug)]
32#[command(
33 author = "Zackary Liel",
34 version = "1.3.3",
35 about = "A Commonmark compliant markdown parser and static site generator.",
36 override_usage = "markrs [OPTIONS] <INPUT_DIR>"
37)]
38struct Cli {
39 #[arg(value_name = "INPUT_DIR")]
40 input_dir: String,
41 #[arg(short, long, default_value = "")]
42 config: String,
43 #[arg(short, long, default_value = "./output")]
44 output_dir: String,
45 #[arg(short, long, default_value = "false")]
46 recursive: bool,
47 #[arg(short, long, default_value = "false")]
48 verbose: bool,
49 #[arg(short, long, default_value = "4")]
50 num_threads: usize,
51}
52
53fn main() -> Result<(), Error> {
54 match run() {
55 Ok(_) => {
56 info!("Static site generation completed successfully.");
57 Ok(())
58 }
59 Err(e) => {
60 error!("An error occurred: {e}");
61 std::process::exit(1);
62 }
63 }
64}
65
66fn run() -> Result<(), Error> {
67 let cli = Cli::parse();
68 let input_dir = &cli.input_dir;
69 let config_path = &cli.config;
70 let run_recursively = &cli.recursive;
71 let num_threads = cli.num_threads;
72
73 let env = if cli.verbose {
75 Env::default().default_filter_or("info")
76 } else {
77 Env::default().default_filter_or("warn")
78 };
79 env_logger::Builder::from_env(env).init();
80
81 init_config(config_path)?;
82 let config = CONFIG.get().unwrap();
83 let file_contents = read_input_dir(input_dir, run_recursively)?;
84 let mut file_names: Vec<String> = Vec::with_capacity(file_contents.len());
85
86 let thread_pool = ThreadPool::build(num_threads).map_err(|e| {
87 error!("Failed to create thread pool: {e}");
88 e
89 })?;
90 let cli = Arc::new(cli);
91
92 for (file_path, file_content) in file_contents {
93 info!("Generating HTML for file: {}", file_path);
94
95 file_names.push(file_path.clone());
96
97 thread_pool
98 .execute({
99 let cli = Arc::clone(&cli);
100 move || {
101 generate_static_site(cli, &file_path, &file_content).unwrap_or_else(|e| {
102 error!("Failed to generate HTML for {file_path}: {e}");
103 });
104 }
105 })
106 .map_err(|e| {
107 error!("Failed to execute job in thread pool: {e}");
108 e
109 })?;
110 }
111
112 thread_pool
113 .execute({
114 let cli = Arc::clone(&cli);
115 move || {
116 let index_html = generate_index(&file_names);
117 write_html_to_file(&index_html, &cli.output_dir, "index.html").unwrap_or_else(
118 |e| {
119 error!("Failed to write index.html: {e}");
120 },
121 );
122 }
123 })
124 .map_err(|e| {
125 error!("Failed to execute job in thread pool for index generation: {e}");
126 e
127 })?;
128
129 let css_file = &config.html.css_file;
130 if css_file != "default" && !css_file.is_empty() {
131 info!("Using custom CSS file: {}", css_file);
132 thread_pool
133 .execute({
134 let cli = Arc::clone(&cli);
135 move || {
136 copy_css_to_output_dir(css_file, &cli.output_dir).unwrap_or_else(|e| {
137 error!("Failed to copy CSS file: {e}");
138 });
139 }
140 })
141 .map_err(|e| {
142 error!("Failed to execute job in thread pool for copying CSS file: {e}");
143 e
144 })?;
145 } else {
146 info!("Using default CSS file.");
147
148 thread_pool
149 .execute({
150 let cli = Arc::clone(&cli);
151 move || {
152 write_default_css_file(&cli.output_dir).unwrap_or_else(|e| {
153 error!("Failed to write default CSS file: {e}");
154 });
155 }
156 })
157 .map_err(|e| {
158 error!("Failed to execute job in thread pool for using default CSS: {e}");
159 e
160 })?;
161 }
162
163 let favicon_path = &config.html.favicon_file;
164 if !favicon_path.is_empty() {
165 info!("Copying favicon from: {}", favicon_path);
166 thread_pool
167 .execute({
168 let cli = Arc::clone(&cli);
169 move || {
170 copy_favicon_to_output_dir(favicon_path, &cli.output_dir).unwrap_or_else(|e| {
171 error!("Failed to copy favicon: {e}");
172 });
173 }
174 })
175 .map_err(|e| {
176 error!("Failed to execute job in thread pool for favicon copy: {e}");
177 e
178 })?;
179 } else {
180 info!("No favicon specified in config.");
181 }
182
183 thread_pool.join_all();
184
185 Ok(())
186}
187
188fn generate_static_site(cli: Arc<Cli>, file_path: &str, file_contents: &str) -> Result<(), Error> {
189 let mut tokenized_lines: Vec<Vec<Token>> = Vec::new();
191 for line in file_contents.split('\n') {
192 tokenized_lines.push(tokenize(line));
193 }
194
195 let blocks = group_lines_to_blocks(tokenized_lines);
197 let parsed_elements = parse_blocks(&blocks);
198
199 let generated_html = generate_html(
201 file_path,
202 &parsed_elements,
203 &cli.output_dir,
204 &cli.input_dir,
205 file_path,
206 );
207
208 let html_relative_path = if file_path.ends_with(".md") {
209 file_path.trim_end_matches(".md").to_string() + ".html"
210 } else {
211 file_path.to_string() + ".html"
212 };
213
214 let output_path = Path::new(&cli.output_dir).join(&html_relative_path);
215 if let Some(parent) = output_path.parent() {
216 std::fs::create_dir_all(parent)?;
217 }
218
219 write_html_to_file(&generated_html, &cli.output_dir, &html_relative_path)?;
220
221 Ok(())
222}