extern crate clap; // 2.33 use clap::{Arg, App}; extern crate chrono; // 0.4 use chrono::NaiveDate; extern crate pulldown_cmark; // 0.8 use pulldown_cmark::{Parser, Options, html}; use std::io::BufRead; use std::path::{Path, PathBuf}; use std::fs; use std::io; use std::fs::File; use std::io::{Read, BufReader}; use std::cmp::Reverse; struct Post { content: String, path: PathBuf, date: NaiveDate, } // https://stackoverflow.com/a/65192210 fn copy_dir_all(src: impl AsRef, dst: impl AsRef, header: &str, footer: &str) -> io::Result<()> { fs::create_dir_all(&dst)?; for entry in fs::read_dir(src)? { let entry = entry?; let ty = entry.file_type()?; if ty.is_dir() { copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()), header, footer)?; } else { if entry.path().extension().unwrap_or_default() == "html" { let mut templated_file = String::new(); File::open(entry.path()).expect("Failed to open html file.").read_to_string(&mut templated_file).expect("Failed to read html file."); let templated_file = templated_file.replace("%header%", header); let templated_file = templated_file.replace("%footer%", footer); fs::write(dst.as_ref().join(entry.file_name()), templated_file).expect("Failed to write templated HTML file."); } else { fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; } } } Ok(()) } fn main() { let matches = App::new("tiny-blog") .arg(Arg::with_name("input") .short("i") .long("input") .help("Path to directory containing the input files. Expected to contain index.html, header.html, footer.html, post.html, and the directory \"posts\", which contains exclusively markdown files.") .required(true) .takes_value(true)) .arg(Arg::with_name("output") .short("o") .long("output") .help("Path to directory to emit the generated site.") .required(true) .takes_value(true)) .get_matches(); let input_dir = Path::new(matches.value_of("input").unwrap()).canonicalize().expect("Failed to resolve input path."); let output_dir = Path::new(matches.value_of("output").unwrap()); fs::create_dir_all(&output_dir).expect("Failed to create directory."); let output_dir = output_dir.canonicalize().expect("Failed to resolve output path."); let mut header = String::new(); File::open(input_dir.join("header.html")).expect("Failed to open header.html.").read_to_string(&mut header).expect("Failed to read header.html"); let mut footer = String::new(); File::open(input_dir.join("footer.html")).expect("Failed to open footer.html.").read_to_string(&mut footer).expect("Failed to read footer.html"); let mut index_template = String::new(); File::open(input_dir.join("index.html")).expect("Failed to open index.html.").read_to_string(&mut index_template).expect("Failed to read index.html"); let index_template = index_template.replace("%header%", &header); let index_template = index_template.replace("%footer%", &footer); let mut post_template = String::new(); File::open(input_dir.join("post.html")).expect("Failed to open post.html.").read_to_string(&mut post_template).expect("Failed to read post.html"); let post_template = post_template.replace("%header%", &header); let post_template = post_template.replace("%footer%", &footer); for item in input_dir.read_dir().expect("Failed to read input directory.") { let entry = item.expect("Failed to copy file."); let path = entry.path(); let name = path.as_path().file_name().expect("Failed to determine file name.").to_string_lossy(); if name == "index.html" || name == "header.html" || name == "footer.html" || name == "post.html" || name == "posts" { continue; } let ty = entry.file_type().expect("Failed to determine type of file."); if ty.is_dir() { copy_dir_all(entry.path(), output_dir.join(entry.file_name()), &header, &footer).expect("Failed to copy directory."); } else { if entry.path().extension().unwrap_or_default() == "html" { let mut templated_file = String::new(); File::open(entry.path()).expect("Failed to open html file.").read_to_string(&mut templated_file).expect("Failed to read html file."); let templated_file = templated_file.replace("%header%", &header); let templated_file = templated_file.replace("%footer%", &footer); fs::write(output_dir.join(entry.file_name()), templated_file).expect("Failed to write templated HTML file."); } else { fs::copy(entry.path(), output_dir.join(entry.file_name())).expect("Failed to copy file."); } } } fs::create_dir_all(&output_dir.join("posts")).expect("Failed to create directory."); let mut parser_options = Options::empty(); parser_options.insert(Options::ENABLE_TABLES); parser_options.insert(Options::ENABLE_FOOTNOTES); parser_options.insert(Options::ENABLE_STRIKETHROUGH); let mut posts: Vec = Vec::new(); for post in input_dir.join("posts").read_dir().expect("Expected directory \"posts\" in input directory.") { let post = post.unwrap(); let post_path = post.path(); let mut post_lines = BufReader::new(File::open(&post_path).expect("Failed to open post.")).lines(); let post_date = post_lines.next().expect("Post file too short!").unwrap(); let post_date = NaiveDate::parse_from_str(&post_date, "%Y-%m-%d").expect("Failed to parse date of post."); let post_content = post_lines.map(|x| x.unwrap()).collect::>().join("\n"); let parser = Parser::new_ext(&post_content, parser_options); let mut output_html = String::new(); html::push_html(&mut output_html, parser); posts.push(Post { content: output_html, path: post_path.strip_prefix(&input_dir).unwrap().to_path_buf(), date: post_date, }); } let mut table_of_contents: Vec = Vec::new(); posts.sort_by_key(|x| Reverse(x.date)); table_of_contents.push("".to_string()); for post in posts { let mut post_path = PathBuf::from(post.path.as_os_str().to_str().expect("Failed to replace spaces in path").replace(" ", "_")); table_of_contents.push(format!("", post.date, post_path.display(), post.path.file_name().unwrap().to_str().unwrap())); post_path.set_extension("html"); fs::write(output_dir.join(post_path), post_template.replace("%content%", &post.content)).unwrap(); } table_of_contents.push("
{}{}
".to_string()); fs::write(output_dir.join("index.html"), index_template.replace("%toc%", &table_of_contents.join("\n"))).unwrap(); }