markrs/
main.rs

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    // Setup
74    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    // Tokenizing
190    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    // Parsing
196    let blocks = group_lines_to_blocks(tokenized_lines);
197    let parsed_elements = parse_blocks(&blocks);
198
199    // HTML Generation
200    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}