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    #[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    // Setup
82    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    // Tokenizing
214    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    // Parsing
220    let blocks = group_lines_to_blocks(tokenized_lines);
221    let parsed_elements = parse_blocks(&blocks);
222
223    // HTML Generation
224    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}