123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- use chrono::NaiveDate;
- use pulldown_cmark::{html, Parser};
- use regex::Regex;
-
- use std::fs;
- use std::path;
-
- use config::Config;
-
- #[derive(Debug, PartialEq)]
- 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<NaiveDate>,
- }
-
- impl Entry {
- pub fn render(&self, template: &str, config: &Config) -> String {
- let parser = Parser::new(&self.body);
- let mut html_buf = String::new();
- html::push_html(&mut html_buf, parser);
- let formatted_date = match self.date {
- Some(date) => date.format("%Y-%m-%d").to_string(),
- None => String::from(""),
- };
- template
- .replace(
- "{{ page_title }}",
- &format!("{} | {}", &self.title, &config.site_name),
- )
- .replace("{{ title }}", &self.title)
- .replace("{{ slug }}", &self.slug)
- .replace("{{ body }}", &html_buf.to_string())
- .replace("{{ date }}", &formatted_date)
- }
- }
-
- pub fn read_entry_dir(cwd: &path::Path) -> Vec<fs::DirEntry> {
- match fs::read_dir(cwd) {
- Ok(entries) => entries.into_iter().map(|entry| entry.unwrap()).collect(),
- Err(err) => panic!(err),
- }
- }
-
- pub fn parse_entry(kind: EntryKind, contents: &str, path: &path::Path) -> Entry {
- lazy_static! {
- static ref RE_WITH_DATE: Regex =
- Regex::new(r"^# (?P<title>.*) \| (?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"]
- .to_string();
-
- if kind == EntryKind::Post {
- let captures = &RE_WITH_DATE
- .captures(&contents)
- .expect("Couldn't parse post");
- let title = captures["title"].to_string();
- let body = captures["body"].to_string();
- let date = Some(
- NaiveDate::parse_from_str(&captures["date"], "%Y-%m-%d").expect("Couldn't parse date"),
- );
- Entry {
- kind,
- title,
- body,
- slug,
- date,
- }
- } else {
- let captures = &RE_WITHOUT_DATE
- .captures(&contents)
- .expect("Couldn't parse page");
- let title = captures["title"].to_string();
- let body = captures["body"].to_string();
- Entry {
- kind,
- title,
- body,
- slug,
- date: None,
- }
- }
- }
-
- pub fn write_entry(
- cwd: &path::Path,
- 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::Path,
- 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 chrono::NaiveDate;
- use uuid::Uuid;
-
- use std::{env, fs, path};
-
- use super::*;
-
- #[test]
- fn test_render_post() {
- let config = Config {
- site_name: String::from("Test Site"),
- url: String::from("testsite.com"),
- description: String::from("recent posts from testsite.com"),
- };
-
- 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></head><body><article><h1>{{ title }}</h1><div>{{ body }}</div></article></body></html>", &config
- )
- .replace("\n", "");
-
- assert_eq!(
- "<html><head><title>hello world | Test Site</title></head><body><article><h1>hello world</h1><div><p>lorem ipsum dolor sit amet</p></div></article></body></html>",
- &output,
- );
- }
-
- #[test]
- fn test_render_post_listing() {
- let config = Config {
- site_name: String::from("Test Site"),
- url: String::from("testsite.com"),
- description: String::from("recent posts from testsite.com"),
- };
-
- 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(
- "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>",
- "<ul>{{ post_listing }}</ul>",
- "<li><a href=\"/{{ slug }}\">{{ title }}</a></li>",
- &posts,
- &config,
- )
- .replace("\n", "");
-
- assert_eq!(
- "<html><head><title>Test Site</title></head><body><ul><li><a href=\"/first-post\">First post</a></li><li><a href=\"/second-post\">Second post</a></li><li><a href=\"/third-post\">Third post</a></li></ul></body></html>",
- &output,
- );
- }
-
- #[test]
- fn test_write_entry() {
- let temp_dir = env::temp_dir();
- let project_dir = temp_dir.join(&Uuid::new_v4().to_string());
- fs::create_dir(&project_dir).unwrap();
-
- fs::create_dir(project_dir.join("public")).unwrap();
- fs::create_dir(project_dir.join("public").join("posts")).unwrap();
-
- let layout =
- "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>";
- let post_template = "<article><h1>{{ title }}</h1><div>{{ body }}</div></article>";
- 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(),
- url: String::from("testsite.com"),
- description: String::from("recent posts from testsite.com"),
- };
-
- write_entry(&project_dir, &layout, &post_template, &post, &config);
-
- let content = fs::read_to_string(
- project_dir
- .join("public")
- .join("posts")
- .join("hello-world")
- .join("index.html"),
- )
- .unwrap();
-
- assert_eq!(
- "<html><head><title>Hello world | Test Site</title></head><body><article><h1>Hello world</h1><div><p>Lorem ipsum dolor sit amet</p></div></article></body></html>",
- content.replace("\n", "")
- );
-
- fs::remove_dir_all(temp_dir.join(&project_dir)).unwrap();
- }
-
- #[test]
- fn test_write_post_listing() {
- let temp_dir = env::temp_dir();
- let project_dir = temp_dir.join(&Uuid::new_v4().to_string());
- fs::create_dir(&project_dir).unwrap();
- env::set_current_dir(&project_dir).unwrap();
-
- let cwd = env::current_dir().unwrap();
- fs::create_dir(cwd.join("public")).unwrap();
-
- let layout =
- "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>";
- let post_listing_template = "<ul>{{ post_listing }}</ul>";
- let post_item_template = "<li><a href=\"/{{ slug }}\">{{ title }}</a></li>";
- 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(),
- url: String::from("testsite.com"),
- description: String::from("recent posts from testsite.com"),
- };
- write_entry_listing(
- &cwd,
- &layout,
- &post_listing_template,
- &post_item_template,
- &posts,
- &config,
- );
-
- assert_eq!(
- "<html><head><title>Test Site</title></head><body><ul><li><a href\
- =\"/first-post\">First post</a></li><li><a href=\"/second-post\">\
- Second post</a></li><li><a href=\"/third-post\">Third post</a></li\
- ></ul></body></html>",
- fs::read_to_string(&cwd.join("public").join("index.html"))
- .unwrap()
- .replace("\n", ""),
- );
-
- fs::remove_dir_all(temp_dir.join(&project_dir)).unwrap();
- }
-
- #[test]
- fn test_parse_post_entry() {
- let post = parse_entry(
- EntryKind::Post,
- "# Test Title | 2000-01-01\n\nThis is the body",
- path::PathBuf::from("posts/one.md").as_path(),
- );
-
- assert_eq!(post.kind, EntryKind::Post);
- assert_eq!(post.slug, "one");
- assert_eq!(post.title, "Test Title");
- assert_eq!(post.body, "This is the body");
- assert_eq!(post.date, Some(NaiveDate::from_ymd(2000, 1, 1)));
- }
-
- #[test]
- fn test_parse_page_entry() {
- let post = parse_entry(
- EntryKind::Page,
- "# Test Title\n\nThis is the body",
- path::PathBuf::from("pages/one.md").as_path(),
- );
-
- assert_eq!(post.kind, EntryKind::Page);
- assert_eq!(post.slug, "one");
- assert_eq!(post.title, "Test Title");
- assert_eq!(post.body, "This is the body");
- assert_eq!(post.date, None);
- }
-
- #[test]
- fn test_display_entry_date() {
- let post = Entry {
- kind: EntryKind::Post,
- title: String::from("Test Title"),
- slug: String::from("one"),
- body: String::from("This is the body"),
- date: Some(NaiveDate::from_ymd(2000, 1, 1)),
- };
-
- let config = Config {
- site_name: String::from("Test Site"),
- url: String::from("testsite.com"),
- description: String::from("recent posts from testsite.com"),
- };
-
- assert_eq!("2000-01-01", post.render("{{ date }}", &config));
- }
- }
|