use chrono::NaiveDate; use comrak::{markdown_to_html, ComrakOptions}; use regex::Regex; use std::fs; use std::path; use config::Config; #[derive(Debug)] pub enum EntryKind { Page, Post, } #[derive(Debug)] pub struct Entry { pub kind: EntryKind, pub title: String, pub slug: String, pub body: String, pub date: Option, } impl Entry { fn render(&self, template: &str, config: &Config) -> String { template .replace( "{{ page_title }}", &format!("{} | {}", &self.title, &config.site_name), ) .replace("{{ title }}", &self.title) .replace("{{ slug }}", &self.slug) .replace( "{{ body }}", &markdown_to_html(&self.body, &ComrakOptions::default()), ) } } pub fn read_entry_dir(cwd: &path::PathBuf) -> Vec { match fs::read_dir(cwd) { Ok(entries) => entries.into_iter().map(|entry| entry.unwrap()).collect(), Err(err) => panic!(err), } } pub fn parse_entry(path: path::PathBuf) -> Entry { let contents = fs::read_to_string(&path).expect("Couldn't read post file"); lazy_static! { static ref re_with_date: Regex = Regex::new(r"^# (?P.*) \| (?P<date>\d{4}-\d{2}-\d{2})\n\n(?s)(?P<body>.*)") .unwrap(); static ref re_without_date: Regex = Regex::new(r"^# (?P<title>.*)\n\n(?s)(?P<body>.*)").unwrap(); static ref slug_re: Regex = Regex::new(r"(?P<slug>\S+).md").unwrap(); } let filename = &path.file_name().unwrap().to_str().unwrap(); let slug = &slug_re .captures(filename) .expect("Couldn't parse slug from filename")["slug"]; if let Some(date_string) = &re_with_date.captures(&contents) { let title = &re_with_date .captures(&contents) .expect("Couldn't parse title")["title"]; let body = &re_with_date .captures(&contents) .expect("Couldn't parse title")["body"]; let date = Some( NaiveDate::parse_from_str(&date_string["date"], "%Y-%m-%d") .expect("Couldn't parse date"), ); Entry { kind: EntryKind::Post, title: String::from(title), body: String::from(body), slug: String::from(slug), date: date, } } else { let title = &re_without_date .captures(&contents) .expect("Couldn't parse title")["title"]; let body = &re_without_date .captures(&contents) .expect("Couldn't parse title")["body"]; Entry { kind: EntryKind::Page, title: String::from(title), body: String::from(body), slug: String::from(slug), date: None, } } } pub fn write_entry( cwd: &path::PathBuf, layout: &str, post_template: &str, entry: &Entry, config: &Config, ) { let root_path = match entry.kind { EntryKind::Post => cwd.join("public").join("posts").join(&entry.slug), EntryKind::Page => cwd.join("public").join(&entry.slug), }; match fs::create_dir(&root_path) { Ok(_) => {} Err(err) => match err.kind() { std::io::ErrorKind::AlreadyExists => {} _ => panic!(err), }, } let template = layout.replace("{{ contents }}", &post_template); fs::write( &root_path.join("index.html"), entry.render(&template, &config), ) .expect("Unable to write file"); } pub fn write_entry_listing( cwd: &path::PathBuf, layout: &str, post_listing_template: &str, post_item_template: &str, posts: &Vec<Entry>, config: &Config, ) { fs::write( cwd.join("public").join("index.html"), render_post_listing( layout, post_listing_template, post_item_template, posts, config, ), ) .expect("Unable to write file"); } pub fn render_post_listing( layout: &str, post_listing_template: &str, post_item_template: &str, posts: &Vec<Entry>, config: &Config, ) -> String { layout .replace("{{ page_title }}", &format!("{}", config.site_name)) .replace( "{{ contents }}", &post_listing_template.replace( "{{ post_listing }}", &posts .iter() .map(|post| post.render(&post_item_template, &config)) .collect::<Vec<String>>() .join("\n"), ), ) } #[cfg(test)] mod tests { use super::{render_post_listing, write_entry, write_entry_listing, Config, Entry, EntryKind}; use chrono::NaiveDate; use std::{env, fs}; use uuid::Uuid; #[test] fn test_render_post() { let config = Config { site_name: String::from("Test Site"), }; let post = Entry { title: String::from("hello world"), body: String::from("lorem ipsum dolor sit amet"), slug: String::from("hello-world"), date: Some(NaiveDate::from_ymd(2019, 1, 1)), kind: EntryKind::Post, }; let output = post.render( "<html><head><title>{{ page_title }}

{{ title }}

{{ body }}
", &config ) .replace("\n", ""); assert_eq!( "hello world | Test Site

hello world

lorem ipsum dolor sit amet

", &output, ); } #[test] fn test_render_post_listing() { let config = Config { site_name: String::from("Test Site"), }; let posts = vec![ Entry { title: String::from("First post"), body: String::from("lorem ipsum dolor sit amet"), slug: String::from("first-post"), date: Some(NaiveDate::from_ymd(2019, 1, 1)), kind: EntryKind::Post, }, Entry { title: String::from("Second post"), body: String::from("lorem ipsum dolor sit amet"), slug: String::from("second-post"), date: Some(NaiveDate::from_ymd(2019, 1, 1)), kind: EntryKind::Post, }, Entry { title: String::from("Third post"), body: String::from("lorem ipsum dolor sit amet"), slug: String::from("third-post"), date: Some(NaiveDate::from_ymd(2019, 1, 1)), kind: EntryKind::Post, }, ]; let output = render_post_listing( "{{ page_title }}{{ contents }}", "", "
  • {{ title }}
  • ", &posts, &config, ) .replace("\n", ""); assert_eq!( "Test Site", &output, ); } #[test] fn test_write_entry() { let temp_dir = env::temp_dir(); let working_dir = temp_dir.join(&Uuid::new_v4().to_string()); fs::create_dir(&working_dir).unwrap(); env::set_current_dir(&working_dir).unwrap(); let cwd = env::current_dir().unwrap(); fs::create_dir(cwd.join("public")).unwrap(); fs::create_dir(cwd.join("public").join("posts")).unwrap(); let layout = "{{ page_title }}{{ contents }}"; let post_template = "

    {{ title }}

    {{ body }}
    "; let post = Entry { title: String::from("Hello world"), body: String::from("Lorem ipsum dolor sit amet"), slug: String::from("hello-world"), date: Some(NaiveDate::from_ymd(2019, 1, 1)), kind: EntryKind::Post, }; let config = Config { site_name: "Test Site".to_string(), }; write_entry(&cwd, &layout, &post_template, &post, &config); let content = fs::read_to_string( cwd.join("public") .join("posts") .join("hello-world") .join("index.html"), ) .unwrap(); assert_eq!( "Hello world | Test Site

    Hello world

    Lorem ipsum dolor sit amet

    ", content.replace("\n", "") ); fs::remove_dir_all(temp_dir.join(&working_dir)).unwrap(); } #[test] fn test_write_post_listing() { let temp_dir = env::temp_dir(); let working_dir = temp_dir.join(&Uuid::new_v4().to_string()); fs::create_dir(&working_dir).unwrap(); env::set_current_dir(&working_dir).unwrap(); let cwd = env::current_dir().unwrap(); fs::create_dir(cwd.join("public")).unwrap(); let layout = "{{ page_title }}{{ contents }}"; let post_listing_template = ""; let post_item_template = "
  • {{ title }}
  • "; let posts = vec![ Entry { title: String::from("First post"), body: String::from("lorem ipsum dolor sit amet"), slug: String::from("first-post"), date: Some(NaiveDate::from_ymd(2019, 1, 1)), kind: EntryKind::Post, }, Entry { title: String::from("Second post"), body: String::from("lorem ipsum dolor sit amet"), slug: String::from("second-post"), date: Some(NaiveDate::from_ymd(2019, 1, 1)), kind: EntryKind::Post, }, Entry { title: String::from("Third post"), body: String::from("lorem ipsum dolor sit amet"), slug: String::from("third-post"), date: Some(NaiveDate::from_ymd(2019, 1, 1)), kind: EntryKind::Post, }, ]; let config = Config { site_name: "Test Site".to_string(), }; write_entry_listing( &cwd, &layout, &post_listing_template, &post_item_template, &posts, &config, ); assert_eq!( "Test Site", fs::read_to_string(&cwd.join("public").join("index.html")) .unwrap() .replace("\n", ""), ); fs::remove_dir_all(temp_dir.join(&working_dir)).unwrap(); } }