#![feature(path_add_extension)] mod collect_files; mod config; mod generate_html; mod generate_rss; mod string_utils; pub use collect_files::*; pub use config::*; pub use generate_html::*; pub use generate_rss::*; pub use string_utils::*; use markdown::*; use vagabond::*; use std::collections::HashSet; use std::time::SystemTime; use log::{info, warn, error, fatal}; use switchboard::*; fn main() { let mut args = Switchboard::from_env(); // Informational switches. args.named("help").short('h'); args.named("version"); args.named("verbose").short('v'); if args.get("help").as_bool() { print_help(); std::process::exit(0); } if args.get("version").as_bool() { let version = env!("CARGO_PKG_VERSION"); eprintln!("toaster, version {version}"); eprintln!("written by ben bridle"); std::process::exit(0); } if args.get("verbose").as_bool() { log::set_log_level(log::LogLevel::Info); } // Functional switches. args.positional("source"); args.positional("destination"); args.named("delete"); args.named("html"); args.named("use-symlinks"); args.raise_errors(); let source = args.get("source").as_path(); let destination = args.get("destination").as_path(); let delete_existing = args.get("delete").as_bool(); let export_html = args.get("html").as_bool(); let use_symlinks = args.get("use-symlinks").as_bool(); #[cfg(not(target_family = "unix"))] if use_symlinks { fatal!("Symbolic links are only supported on Linux"); } // Parse entire website directory. 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 urls: HashSet<&str> = HashSet::new(); let mut duplicates: HashSet<&str> = HashSet::new(); for page in &website.pages { if !urls.insert(&page.url) { duplicates.insert(&page.url); }; } for static_file in &website.static_files { if !urls.insert(&static_file.url) { duplicates.insert(&static_file.url); }; } for redirect in &website.redirects { if !urls.insert(&redirect.url) { duplicates.insert(&redirect.url); }; } if !duplicates.is_empty() { for destination in duplicates { warn!("Multiple files, pages, or redirects have the output path {destination:?}"); for page in &website.pages { if page.url == destination { eprintln!(":: {:?}", page.source_path); } } for static_file in &website.static_files { if static_file.url == destination { eprintln!(":: {:?}", static_file.source_path); } } for redirect in &website.redirects { if redirect.url == destination { eprintln!(":: {:?}", redirect.source_path); } } } } let mut destination = destination; destination.push(&website.name.slug()); 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.url); // Convert document to different formats. if export_html { let html = generate_html(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.url); if use_symlinks { #[cfg(target_family = "unix")] { info!("Linking static file to {destination:?}"); make_parent_directory(&destination).unwrap(); let _ = remove(&destination); std::os::unix::fs::symlink(&static_file.source_path, &destination).unwrap_or_else(|_| error!("Failed to link static file {:?} to {:?}", static_file.source_path, destination)); } } else { info!("Copying static file to {destination:?}"); 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.url); let path = &redirect.target; if export_html { if !path.contains("://") { if let Some(path) = website.has_page(redirect, &path, "html") { write_file(&generate_html_redirect(&path, &website), &destination, "html", redirect.last_modified); } else { warn!("Redirect {:?} links to nonexistent page {path:?}", redirect.name); } } else { write_file(&generate_html_redirect(&path, &website), &destination, "html", redirect.last_modified); } } } for feed in &website.feeds { let mut destination = destination.clone(); destination.push(&feed.url); write_file(&generate_rss(feed, &website), &destination, "rss", feed.last_modified); } } 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 "); } 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); } } }