I'm creating this website to share my learnings and thoughts. This is mostly just a test initial post.
This website is generated using a custom static site authoring and publishing tool I wrote.
The generator is about 250 lines of Rust code at the moment, with minimal dependencies—mostly just a Markdown parser/HTML generator and utilities to watch for filesystem events.
I use the publish
operation to generate the HTML files and them serve with a standard web server.
In fact, the code is so small that I'll just paste it here:
use std::{
env,
fs::{self, File},
io::Read,
path::{Path, PathBuf},
};
mod html;
use dunce;
use html::{HTML_STRUCTURE_END, HTML_STRUCTURE_START};
use markdown::{CompileOptions, ParseOptions};
use notify::Watcher;
use pathdiff::diff_paths;
const DOCUMENT_ROOT: &str = "documents";
const OUTPUT_ROOT_AUTHOR: &str = "public_html";
const CSS_PATH: &str = "style.css";
fn main() -> anyhow::Result<()> {
return parse_operations();
}
fn print_help() -> anyhow::Result<()> {
println!("Usage: blog_salt_amphora OPERATION ARGUMENTS");
println!("\nOperations:");
println!(" help");
println!(" author");
println!(" publish [output path root]");
println!();
Ok(())
}
fn parse_operations() -> anyhow::Result<()> {
let args: Vec<String> = env::args().collect();
let args_len = args.len();
if args_len < 2 {
return print_help();
}
let mut selected_operation: Option<ProgramOperation> = None;
let mut selected_arguments: Vec<&str> = Vec::new();
for arg in &args[1..args_len] {
match arg.as_str() {
"author" => selected_operation = Some(ProgramOperation::Author),
"publish" => selected_operation = Some(ProgramOperation::Publish),
"help" | "--help" | "-h" => selected_operation = Some(ProgramOperation::Help),
program_arg => selected_arguments.push(program_arg),
}
}
return match selected_operation {
Some(operation) => match operation {
ProgramOperation::Help => print_help(),
ProgramOperation::Author => operation_author(),
ProgramOperation::Publish => operation_publish(&selected_arguments),
},
None => {
return print_help();
}
};
}
fn operation_author() -> anyhow::Result<()> {
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = notify::RecommendedWatcher::new(tx, notify::Config::default())?;
watcher.watch(Path::new(&DOCUMENT_ROOT), notify::RecursiveMode::Recursive)?;
for res in rx {
handle_watcher_event(res);
}
Ok(())
}
fn operation_publish(selected_arguments: &Vec<&str>) -> anyhow::Result<()> {
if selected_arguments.len() < 1 {
return print_help();
}
let output_root_path = selected_arguments[0];
generate_dir(Path::new(DOCUMENT_ROOT), &output_root_path)?;
Ok(())
}
fn handle_watcher_event(res: anyhow::Result<notify::Event, notify::Error>) {
match res {
Ok(event) => match event.kind {
notify::EventKind::Create(_) | notify::EventKind::Modify(_) => {
if event.paths.len() > 0 {
let path = &event.paths[0];
match generate_output(path, &OUTPUT_ROOT_AUTHOR) {
Ok(_) => {
println!("Processing change in: {}", &path.display());
}
Err(e) => {
eprintln!("Error generating output for {}: {:?}", path.display(), e);
}
}
}
}
notify::EventKind::Remove(_) => {}
_ => {}
},
Err(e) => println!("Watch error: {:?}", e),
}
}
/// Generates the files in a directory
fn generate_dir(dir: &Path, output_root_path: &str) -> anyhow::Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
generate_dir(&path, &output_root_path)?;
} else if path.is_file() {
match generate_output(&path, &output_root_path) {
Ok(_) => {
println!("Generated output for {}", path.display());
}
Err(e) => {
eprintln!("Error generating output for {}: {:?}", path.display(), e);
}
}
}
}
Ok(())
}
fn generate_output(document_path: &PathBuf, output_root_path: &str) -> anyhow::Result<()> {
let css_path = Path::new(&CSS_PATH);
let mut css_file = File::open(&css_path).unwrap();
let mut css_string = String::new();
css_file.read_to_string(&mut css_string).unwrap();
let document_root_path = dunce::canonicalize(PathBuf::from(DOCUMENT_ROOT))?;
match document_path.extension() {
Some(ext) => {
if ext != "md" {
println!("Skipping {:?}", &document_path);
return Ok(());
}
}
None => {
println!("Skipping {:?}", &document_path);
return Ok(());
}
}
let document_path = match dunce::canonicalize(&document_path) {
Ok(pathbuf) => pathbuf,
Err(e) => {
println!(
"Error canonicalizing document path: {:?}, error: {:?}",
&document_path, e
);
return Ok(());
}
};
let mut input_file = File::open(&document_path)?;
let mut buffer = String::new();
input_file.read_to_string(&mut buffer)?;
add_css_to_buffer(&mut buffer, &css_string);
let md_options = markdown::Options {
compile: CompileOptions {
allow_dangerous_html: true,
..CompileOptions::default()
},
parse: ParseOptions::default(),
};
let mut html = markdown::to_html_with_options(&buffer, &md_options).unwrap();
add_html5_structure_to_buffer(&mut html);
let output_path = diff_paths(&document_path, document_root_path).unwrap();
let mut output_path = dunce::canonicalize(PathBuf::from(output_root_path))?.join(output_path);
output_path.set_extension("html");
fs::create_dir_all(&output_path.parent().unwrap().to_path_buf())?;
fs::write(output_path, &html.into_bytes())?;
Ok(())
}
fn add_html5_structure_to_buffer(buffer: &mut String) {
*buffer = String::from(HTML_STRUCTURE_START) + buffer + &String::from(HTML_STRUCTURE_END);
}
fn add_css_to_buffer(buffer: &mut String, css_string: &String) {
*buffer += "\n<style>\n";
*buffer += &css_string;
*buffer += "\n</style>\n";
}
enum ProgramOperation {
Help,
Author,
Publish,
}