#![feature(path_add_extension)] mod collect_files; pub use collect_files::*; mod generate_html; pub use generate_html::*; use markdown::*; use vagabond::*; use std::collections::HashSet; use std::time::SystemTime; use log::{info, warn, error, fatal}; use switchboard::{Switchboard, SwitchQuery}; fn print_help() -> ! { eprintln!("\ Usage: toaster Generate a website from a structured directory of markdown files. Arguments: source Source directory with markdown files destination Path to output directory Switches: --delete Delete the destination directory first if it exists --html Generate HTML output --version, -v Print information as each file is parsed --version Print the program version and exit --help, -h Print help "); std::process::exit(0); } fn print_version() -> ! { let version = env!("CARGO_PKG_VERSION"); eprintln!("toaster, version {version}"); eprintln!("written by ben bridle"); std::process::exit(0); } fn main() { let mut args = Switchboard::from_env(); if args.named("help").short('h').as_bool() { print_help(); } if args.named("version").as_bool() { print_version(); } if args.named("verbose").short('v').as_bool() { log::set_log_level(log::LogLevel::Info); } let source = args.positional("source").as_path(); let destination = args.positional("destination").as_path(); let delete_existing = args.named("delete").as_bool(); let export_html = args.named("html").as_bool(); let source = match source.canonicalize() { Ok(source) => source, Err(err) => fatal!("{source:?}: {err}"), }; let website = Website::from_path(&source); // Check for duplicate output paths for pages. let mut destinations: HashSet<&str> = HashSet::new(); let mut duplicates: HashSet<&str> = HashSet::new(); for page in &website.pages { if !destinations.insert(&page.full_url) { duplicates.insert(&page.full_url); }; } if !duplicates.is_empty() { for destination in duplicates { warn!("Multiple pages have the output path {destination:?}"); for page in &website.pages { if page.full_url == destination { eprintln!(":: {:?}", page.source_path); } } } } let mut destination = destination; destination.push(make_url_safe(&website.name)); if delete_existing && Entry::from_path(&destination).is_ok() { info!("Deleting existing destination directory {destination:?}"); remove(&destination).unwrap_or_else(|_| error!("Failed to delete existing destination directory {destination:?}")); } for page in &website.pages { let mut destination = destination.clone(); destination.push(&page.full_url); // Convert document to different formats. if export_html { let html = generate_html(&page.document, page, &website); write_file(&html, &destination, "html", page.last_modified); } // Copy original markdown file. destination.add_extension("md"); info!("Copying original markdown file to {destination:?}"); copy(&page.source_path, &destination).unwrap_or_else(|_| error!("Failed to copy original markdown file {:?} to {:?}", page.source_path, destination)); } for static_file in &website.static_files { let mut destination = destination.clone(); destination.push(&static_file.full_url); info!("Copying static file to {destination:?}"); make_parent_directory(&destination).unwrap(); copy(&static_file.source_path, &destination).unwrap_or_else(|_| error!("Failed to copy static file {:?} to {:?}", static_file.source_path, destination)); } // NOTE: Static dir contents are copied as part of all static files. for redirect in &website.redirects { let mut destination = destination.clone(); destination.push(&redirect.full_url); let path = &redirect.redirect; if export_html { if !path.contains("://") { if let Some(path) = website.has_page(redirect, &path, "html") { write_file(&generate_html_redirect(&path), &destination, "html", redirect.last_modified); } else { warn!("Redirect {:?} links to nonexistent page {path:?}", redirect.name); } } else { write_file(&generate_html_redirect(&path), &destination, "html", redirect.last_modified); } } } } pub fn write_file(text: &str, destination: &PathBuf, ext: &str, last_modified: Option) { let mut destination = destination.clone(); destination.add_extension(ext); info!("Generating {destination:?}"); make_parent_directory(&destination).unwrap_or_else(|_| error!("Failed to create parent directories for {destination:?}")); write_to_file(&destination, text).unwrap_or_else(|_| error!("Failed to write generated {ext} file to {destination:?}")); // Set the last-modified time of the new file to the time provided. if let Some(time) = last_modified { if let Ok(dest) = std::fs::File::open(&destination) { let _ = dest.set_modified(time); } } } pub fn make_url_safe(text: &str) -> String { text.to_ascii_lowercase().chars().filter_map(|c| if c.is_alphanumeric() || "-_~.+/#".contains(c) { Some(c) } else if c == ' ' { Some('-') } else { None } ) .collect() }