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 #[arg(
52 short = 'O',
53 long,
54 help = "Open the generated index.html in the default web browser."
55 )]
56 open: bool,
57 #[arg(short, long, default_value = "", num_args = 0.., help = "Exclude files or directories from the input directory. Can be specified multiple times, or as a space-separated list.")]
58 exclude: Vec<String>,
59}
60
61fn main() -> Result<(), Error> {
62 match run() {
63 Ok(_) => {
64 info!("Static site generation completed successfully.");
65 Ok(())
66 }
67 Err(e) => {
68 error!("An error occurred: {e}");
69 std::process::exit(1);
70 }
71 }
72}
73
74fn run() -> Result<(), Error> {
75 let cli = Cli::parse();
76 let input_dir = &cli.input_dir;
77 let config_path = &cli.config;
78 let run_recursively = &cli.recursive;
79 let num_threads = cli.num_threads;
80
81 let env = if cli.verbose {
83 Env::default().default_filter_or("info")
84 } else {
85 Env::default().default_filter_or("warn")
86 };
87 env_logger::Builder::from_env(env).init();
88
89 init_config(config_path)?;
90 let config = CONFIG.get().unwrap();
91 let file_contents = read_input_dir(input_dir, run_recursively, &cli.exclude)?;
92 let mut file_names: Vec<String> = Vec::with_capacity(file_contents.len());
93
94 let thread_pool = ThreadPool::build(num_threads).map_err(|e| {
95 error!("Failed to create thread pool: {e}");
96 e
97 })?;
98 let cli = Arc::new(cli);
99
100 for (file_path, file_content) in file_contents {
101 info!("Generating HTML for file: {}", file_path);
102
103 file_names.push(file_path.clone());
104
105 thread_pool
106 .execute({
107 let cli = Arc::clone(&cli);
108 move || {
109 generate_static_site(cli, &file_path, &file_content).unwrap_or_else(|e| {
110 error!("Failed to generate HTML for {file_path}: {e}");
111 });
112 }
113 })
114 .map_err(|e| {
115 error!("Failed to execute job in thread pool: {e}");
116 e
117 })?;
118 }
119
120 thread_pool
121 .execute({
122 let cli = Arc::clone(&cli);
123 move || {
124 let index_html = generate_index(&file_names);
125 write_html_to_file(&index_html, &cli.output_dir, "index.html").unwrap_or_else(
126 |e| {
127 error!("Failed to write index.html: {e}");
128 },
129 );
130 }
131 })
132 .map_err(|e| {
133 error!("Failed to execute job in thread pool for index generation: {e}");
134 e
135 })?;
136
137 let css_file = &config.html.css_file;
138 if css_file != "default" && !css_file.is_empty() {
139 info!("Using custom CSS file: {}", css_file);
140 thread_pool
141 .execute({
142 let cli = Arc::clone(&cli);
143 move || {
144 copy_css_to_output_dir(css_file, &cli.output_dir).unwrap_or_else(|e| {
145 error!("Failed to copy CSS file: {e}");
146 });
147 }
148 })
149 .map_err(|e| {
150 error!("Failed to execute job in thread pool for copying CSS file: {e}");
151 e
152 })?;
153 } else {
154 info!("Using default CSS file.");
155
156 thread_pool
157 .execute({
158 let cli = Arc::clone(&cli);
159 move || {
160 write_default_css_file(&cli.output_dir).unwrap_or_else(|e| {
161 error!("Failed to write default CSS file: {e}");
162 });
163 }
164 })
165 .map_err(|e| {
166 error!("Failed to execute job in thread pool for using default CSS: {e}");
167 e
168 })?;
169 }
170
171 let favicon_path = &config.html.favicon_file;
172 if !favicon_path.is_empty() {
173 info!("Copying favicon from: {}", favicon_path);
174 thread_pool
175 .execute({
176 let cli = Arc::clone(&cli);
177 move || {
178 copy_favicon_to_output_dir(favicon_path, &cli.output_dir).unwrap_or_else(|e| {
179 error!("Failed to copy favicon: {e}");
180 });
181 }
182 })
183 .map_err(|e| {
184 error!("Failed to execute job in thread pool for favicon copy: {e}");
185 e
186 })?;
187 } else {
188 info!("No favicon specified in config.");
189 }
190
191 thread_pool.join_all();
192
193 if cli.open {
194 let index_path = Path::new(&cli.output_dir).join("index.html");
195 if index_path.exists() {
196 if let Err(e) = webbrowser::open(&index_path.to_string_lossy()) {
197 error!("Failed to open index.html in browser: {e}");
198 } else {
199 info!("Opened index.html in browser.");
200 }
201 } else {
202 error!(
203 "index.html does not exist at path: {}",
204 index_path.display()
205 );
206 }
207 }
208
209 Ok(())
210}
211
212fn generate_static_site(cli: Arc<Cli>, file_path: &str, file_contents: &str) -> Result<(), Error> {
213 let mut tokenized_lines: Vec<Vec<Token>> = Vec::new();
215 for line in file_contents.split('\n') {
216 tokenized_lines.push(tokenize(line));
217 }
218
219 let blocks = group_lines_to_blocks(tokenized_lines);
221 let parsed_elements = parse_blocks(&blocks);
222
223 let generated_html = generate_html(
225 file_path,
226 &parsed_elements,
227 &cli.output_dir,
228 &cli.input_dir,
229 file_path,
230 );
231
232 let html_relative_path = if file_path.ends_with(".md") {
233 file_path.trim_end_matches(".md").to_string() + ".html"
234 } else {
235 file_path.to_string() + ".html"
236 };
237
238 let output_path = Path::new(&cli.output_dir).join(&html_relative_path);
239 if let Some(parent) = output_path.parent() {
240 std::fs::create_dir_all(parent)?;
241 }
242
243 write_html_to_file(&generated_html, &cli.output_dir, &html_relative_path)?;
244
245 Ok(())
246}