#![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 <source> <destination>
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<SystemTime>) {
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()
}