use std::path::PathBuf; use chrono::NaiveDateTime; use lazy_static::lazy_static; use pulldown_cmark::{html, Parser}; use regex::Regex; use serde::{Deserialize, Deserializer, Serializer}; use serde_derive::{Deserialize, Serialize}; #[derive(Clone, Default, Deserialize, Serialize)] #[serde(default)] pub struct Page { pub permalink: String, pub title: String, pub author: String, pub description: String, pub draft: bool, #[serde(deserialize_with = "parse_datetime", serialize_with = "date_to_string")] pub date: Option, #[serde(skip_deserializing)] pub content: String, #[serde(skip_deserializing)] pub prev: Option>, #[serde(skip_deserializing)] pub next: Option>, #[serde(skip)] pub input: PathBuf, #[serde(skip)] pub output: Option, } impl Page { pub fn from(path: PathBuf, content: &[u8]) -> Page { let content = String::from_utf8_lossy(&content); if !FRONTMATTER_REGEX.is_match(&content) { return Page { content: markdown_to_html(&content), input: path, ..Default::default() }; } let captures = FRONTMATTER_REGEX.captures(&content).unwrap(); let mut page: Page = match toml::from_str(&captures[1]) { Ok(page) => page, Err(e) => { warn!("failed to parse frontmatter in `{}`: {}", path.display(), e); Page::default() } }; if page.permalink == "" { page.permalink = "/".to_string() + &path.with_extension("").to_string_lossy(); } else { page.output = Some(PathBuf::from(page.permalink.trim_start_matches('/'))); } page.content = markdown_to_html(&captures[2]); page.input = path; page } } lazy_static! { static ref FRONTMATTER_REGEX: Regex = Regex::new(r"^[[:space:]]*\-\-\-\r?\n((?s).*?(?-s))\-\-\-\r?\n?((?s).*(?-s))$").unwrap(); } fn markdown_to_html(content: &str) -> String { let mut result = String::new(); html::push_html(&mut result, Parser::new(content)); result } fn parse_datetime<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let date = if let Ok(date) = toml::value::Datetime::deserialize(deserializer) { let date = date.to_string(); if date.contains('T') { chrono::DateTime::parse_from_rfc3339(&date) .ok() .map(|date| date.naive_local()) } else { chrono::NaiveDate::parse_from_str(&date, "%Y-%m-%d") .ok() .map(|date| date.and_hms(0, 0, 0)) } } else { None }; Ok(date) } fn date_to_string(date: &Option, serializer: S) -> Result where S: Serializer, { if let Some(date) = date { serializer.serialize_some(&date.format("%Y-%m-%dT%H:%M:%S").to_string()) } else { serializer.serialize_none() } }