From 3cd5da2128def65a304f0637a745b43ed78f5420 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Sun, 11 Dec 2016 15:05:03 +0900 Subject: [PATCH] Separate front matter parsing from the page --- Cargo.lock | 77 +++++++++++-- Cargo.toml | 4 + README.md | 35 ++++++ src/cmd/build.rs | 22 +++- src/cmd/new.rs | 2 +- src/config.rs | 11 +- src/errors.rs | 10 +- src/front_matter.rs | 185 ++++++++++++++++++++++++++++++++ src/main.rs | 13 ++- src/page.rs | 256 ++++++++++---------------------------------- 10 files changed, 389 insertions(+), 226 deletions(-) create mode 100644 README.md create mode 100644 src/front_matter.rs diff --git a/Cargo.lock b/Cargo.lock index abacd9a..2825a1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,16 @@ name = "gutenberg" version = "0.1.0" dependencies = [ - "clap 2.19.1 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.19.2 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "pulldown-cmark 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", "tera 0.4.1 (git+https://github.com/Keats/tera.git?branch=v0.5)", "toml 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -66,7 +70,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "clap" -version = "2.19.1" +version = "2.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -95,7 +99,7 @@ dependencies = [ "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "quine-mc_cluskey 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -219,6 +223,11 @@ name = "quine-mc_cluskey" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "quote" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "regex" version = "0.1.80" @@ -243,7 +252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "rustc-serialize" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -259,9 +268,35 @@ name = "serde" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "serde_codegen" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_codegen_internals 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_codegen_internals" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "syn 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde_codegen 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "serde_json" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -283,10 +318,19 @@ name = "strsim" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "syn" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tera" version = "0.4.1" -source = "git+https://github.com/Keats/tera.git?branch=v0.5#85b6fb3723469cb9ec06e63aa80f48348d4ece73" +source = "git+https://github.com/Keats/tera.git?branch=v0.5#6fc3c61fc58c010abc26f3272badea1b9bc13963" dependencies = [ "error-chain 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", @@ -295,7 +339,7 @@ dependencies = [ "pest 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", "slug 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -332,7 +376,7 @@ name = "toml" version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rustc-serialize 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -366,6 +410,11 @@ name = "unicode-width" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unicode-xid" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unidecode" version = "0.2.0" @@ -417,7 +466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4f67931368edf3a9a51d29886d245f1c3db2f1ef0dcc9e35ff70341b78c10d23" "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" "checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c" -"checksum clap 2.19.1 (registry+https://github.com/rust-lang/crates.io-index)" = "956cee0b2427dd9e71129a509d1ef17a7f5df9f8253924074d7a5d79bc61851e" +"checksum clap 2.19.2 (registry+https://github.com/rust-lang/crates.io-index)" = "305ad043f009db535a110200541d4567b63e172b1fe030313fbb92565da7ed24" "checksum clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)" = "5b4fabf979ddf6419a313c1c0ada4a5b95cfd2049c56e8418d622d27b4b6ff32" "checksum clippy_lints 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)" = "ce96ec05bfe018a0d5d43da115e54850ea2217981ff0f2e462780ab9d594651a" "checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850" @@ -439,15 +488,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum pest 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f6666c81a6359af7a9dbc48f596d6f318a9dbaefdec248581ab836dc0c1f082" "checksum pulldown-cmark 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1058d7bb927ca067656537eec4e02c2b4b70eaaa129664c5b90c111e20326f41" "checksum quine-mc_cluskey 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "07589615d719a60c8dd8a4622e7946465dfef20d1a428f969e3443e7386d5f45" +"checksum quote 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)" = "6732e32663c9c271bfc7c1823486b471f18c47a2dbf87c066897b7b51afc83be" "checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f" "checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" "checksum rustc-demangle 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1430d286cadb237c17c885e25447c982c97113926bb579f4379c0eca8d9586dc" -"checksum rustc-serialize 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)" = "bff9fc1c79f2dec76b253273d07682e94a978bd8f132ded071188122b2af9818" +"checksum rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "237546c689f20bb44980270c73c3b9edd0891c1be49cc1274406134a66d3957b" "checksum semver 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2d5b7638a1f03815d94e88cb3b3c08e87f0db4d683ef499d1836aaf70a45623f" "checksum serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)" = "58a19c0871c298847e6b68318484685cd51fa5478c0c905095647540031356e5" -"checksum serde_json 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1cb6b19e74d9f65b9d03343730b643d729a446b29376785cd65efdff4675e2fc" +"checksum serde_codegen 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)" = "ce29a6ae259579707650ec292199b5fed2c0b8e2a4bdc994452d24d1bcf2242a" +"checksum serde_codegen_internals 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "59933a62554548c690d2673c5164f0c4a46be7c5731edfd94b0ecb1048940732" +"checksum serde_derive 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)" = "a4b541549c4207d3602c9abcc3e31252e91751674264eb85c103bb20197054b4" +"checksum serde_json 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3f7d3c184d35801fb8b32b46a7d58d57dbcc150b0eb2b46a1eb79645e8ecfd5b" "checksum slug 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f5ff4b43cb07b86c5f9236c92714a22cdf9e5a27a7d85e398e2c9403328cb8" "checksum strsim 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "67f84c44fbb2f91db7fef94554e6b2ac05909c9c0b0bc23bb98d3a1aebfe7f7c" +"checksum syn 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94e7d81ecd16d39f16193af05b8d5a0111b9d8d2f3f78f31760f327a247da777" "checksum tera 0.4.1 (git+https://github.com/Keats/tera.git?branch=v0.5)" = "" "checksum term_size 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f7f5f3f71b0040cecc71af239414c23fd3c73570f5ff54cf50e03cef637f2a0" "checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" @@ -458,6 +512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "26643a2f83bac55f1976fb716c10234485f9202dcd65cfbdf9da49867b271172" "checksum unicode-segmentation 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c3bc443ded17b11305ffffe6b37e2076f328a5a8cb6aa877b1b98f77699e98b5" "checksum unicode-width 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2d6722facc10989f63ee0e20a83cd4e1714a9ae11529403ac7e0afd069abc39e" +"checksum unicode-xid 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "36dff09cafb4ec7c8cf0023eb0b686cb6ce65499116a12201c9e11840ca01beb" "checksum unidecode 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d2adb95ee07cd579ed18131f2d9e7a17c25a4b76022935c7f2460d2bfae89fd2" "checksum url 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "48ccf7bd87a81b769cf84ad556e034541fb90e1cd6d4bc375c822ed9500cd9d7" "checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" diff --git a/Cargo.toml b/Cargo.toml index 97322c9..846ba94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,10 @@ walkdir = "1" pulldown-cmark = "0" regex = "0.1" lazy_static = "0.2" +glob = "0.2" +serde = "0.8" +serde_json = "0.8" +serde_derive = "0.8" tera = { git = "https://github.com/Keats/tera.git", branch = "v0.5" } clippy = {version = "~0.0.103", optional = true} diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f59532 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Gutenberg + +## Design + +Can be used for blogs or general static pages + +Commands: + +- new: start a new project -> creates the structure + default config.toml +- build: reads all the .md files and build the site with template +- serve: starts a server and watches/reload the site on change + + +All pages go into the `content` folder. Subfolder represents a list of content, ie + +```bash +├── content +│   ├── posts +│   │   └── intro.md +│   └── some.md +``` + +`some.md` will be accessible at `mywebsite.com/some` and there will be other pages: + +- `mywebsite.com/posts` that will list all the pages contained in the `posts` folder +- `mywebsite.com/posts/intro` + + +### Building the site +Get all .md files in content, remove the `content/` prefix to their path +Split the file between front matter and content +Parse the front matter +markdown -> HTML for the content +TO THINK OF: create list pages for folders, can be done while globbing I guess? +Render templates diff --git a/src/cmd/build.rs b/src/cmd/build.rs index c874dcc..0f7bdeb 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -1,12 +1,28 @@ +use glob::glob; +use tera::Tera; use config:: Config; -use errors::{Result}; - -use tera::Tera; +use errors::{Result, ResultExt}; +use page::Page; pub fn build(config: Config) -> Result<()> { + let tera = Tera::new("layouts/**/*").chain_err(|| "Error parsing templates")?; + let mut pages: Vec = vec![]; + + // hardcoded pattern so can't error + for entry in glob("content/**/*.md").unwrap().filter_map(|e| e.ok()) { + let path = entry.as_path(); + // Remove the content string from name + let filepath = path.to_string_lossy().replace("content/", ""); + pages.push(Page::from_file(&filepath)?); + } + + for page in pages { + let html = page.render_html(&tera, &config) + .chain_err(|| format!("Failed to render '{}'", page.filepath))?; + } Ok(()) } diff --git a/src/cmd/new.rs b/src/cmd/new.rs index 8614bac..8f7edde 100644 --- a/src/cmd/new.rs +++ b/src/cmd/new.rs @@ -3,7 +3,7 @@ use std::io::prelude::*; use std::fs::{create_dir, File}; use std::path::Path; -use errors::{Result, ErrorKind}; +use errors::Result; const CONFIG: &'static str = r#" diff --git a/src/config.rs b/src/config.rs index 7cc0c95..8eb12d5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,13 +5,14 @@ use std::path::Path; use toml::Parser; -use errors::{Result, ErrorKind}; +use errors::{Result, ErrorKind, ResultExt}; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Config { pub title: String, pub base_url: String, + pub theme: String, pub favicon: Option, } @@ -21,6 +22,7 @@ impl Default for Config { Config { title: "".to_string(), base_url: "".to_string(), + theme: "".to_string(), favicon: None, } @@ -55,12 +57,15 @@ impl Config { pub fn from_file>(path: P) -> Result { let mut content = String::new(); - File::open(path)?.read_to_string(&mut content)?; + File::open(path) + .chain_err(|| "Failed to load config.toml. Are you in the right directory?")? + .read_to_string(&mut content)?; Config::from_str(&content) } } + #[cfg(test)] mod tests { use super::{Config}; diff --git a/src/errors.rs b/src/errors.rs index eca6c64..9f98a0e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,14 +1,16 @@ +use tera; + error_chain! { + links { + Tera(tera::Error, tera::ErrorKind); + } + foreign_links { Io(::std::io::Error); } errors { - InvalidFrontMatter(name: String) { - description("frontmatter is invalid") - display("Front Matter of file '{}' is missing or is invalid", name) - } InvalidConfig { description("invalid config") display("The config.toml is invalid or is using the wrong type for an argument") diff --git a/src/front_matter.rs b/src/front_matter.rs new file mode 100644 index 0000000..3f63f67 --- /dev/null +++ b/src/front_matter.rs @@ -0,0 +1,185 @@ +use std::collections::BTreeMap; + + +use toml::{Parser, Value as TomlValue}; +use tera::{Value, to_value}; + + +use errors::{Result}; +use page::Page; + + +// Converts from one value (Toml) to another (Tera) +// Used to fill the Page::extra map +fn toml_to_tera(val: &TomlValue) -> Value { + match *val { + TomlValue::String(ref s) | TomlValue::Datetime(ref s) => to_value(s), + TomlValue::Boolean(ref b) => to_value(b), + TomlValue::Integer(ref n) => to_value(n), + TomlValue::Float(ref n) => to_value(n), + TomlValue::Array(ref arr) => to_value(&arr.into_iter().map(toml_to_tera).collect::>()), + TomlValue::Table(ref table) => { + to_value(&table.into_iter().map(|(k, v)| { + (k, toml_to_tera(v)) + }).collect::>()) + } + } +} + + +pub fn parse_front_matter(front_matter: &str, page: &mut Page) -> Result<()> { + if front_matter.trim() == "" { + bail!("Front matter of file is missing"); + } + + let mut parser = Parser::new(&front_matter); + + if let Some(value) = parser.parse() { + for (key, value) in value.iter() { + match key.as_str() { + "title" | "slug" | "url" | "category" | "layout" | "description" => match *value { + TomlValue::String(ref s) => { + if key == "title" { + page.title = s.to_string(); + } else if key == "slug" { + page.slug = s.to_string(); + } else if key == "url" { + page.url = Some(s.to_string()); + } else if key == "category" { + page.category = Some(s.to_string()); + } else if key == "layout" { + page.layout = Some(s.to_string()); + } else if key == "description" { + page.description = Some(s.to_string()); + } + } + _ => bail!("Field {} should be a string", key) + }, + "draft" => match *value { + TomlValue::Boolean(b) => page.is_draft = b, + _ => bail!("Field {} should be a boolean", key) + }, + "date" => match *value { + TomlValue::Datetime(ref d) => page.date = Some(d.to_string()), + _ => bail!("Field {} should be a date", key) + }, + "tags" => match *value { + TomlValue::Array(ref a) => { + for elem in a { + if key == "tags" { + match *elem { + TomlValue::String(ref s) => page.tags.push(s.to_string()), + _ => bail!("Tag `{}` should be a string") + } + } + } + }, + _ => bail!("Field {} should be an array", key) + }, + // extra fields + _ => { + page.extra.insert(key.to_string(), toml_to_tera(value)); + } + } + } + } else { + bail!("Errors parsing front matter: {:?}", parser.errors); + } + + if page.title == "" || page.slug == "" { + bail!("Front matter is missing required fields (title, slug or both)"); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{parse_front_matter}; + use tera::to_value; + use page::Page; + + + #[test] + fn test_can_parse_a_valid_front_matter() { + let content = r#" +title = "Hello" +slug = "hello-world""#; + let mut page = Page::default(); + let res = parse_front_matter(content, &mut page); + assert!(res.is_ok()); + + assert_eq!(page.title, "Hello".to_string()); + assert_eq!(page.slug, "hello-world".to_string()); + } + + #[test] + fn test_can_parse_tags() { + let content = r#" +title = "Hello" +slug = "hello-world" +tags = ["rust", "html"]"#; + let mut page = Page::default(); + let res = parse_front_matter(content, &mut page); + assert!(res.is_ok()); + + assert_eq!(page.title, "Hello".to_string()); + assert_eq!(page.slug, "hello-world".to_string()); + assert_eq!(page.tags, ["rust".to_string(), "html".to_string()]); + } + + #[test] + fn test_can_parse_extra_attributes_in_frontmatter() { + let content = r#" +title = "Hello" +slug = "hello-world" +language = "en" +authors = ["Bob", "Alice"]"#; + let mut page = Page::default(); + let res = parse_front_matter(content, &mut page); + assert!(res.is_ok()); + + assert_eq!(page.title, "Hello".to_string()); + assert_eq!(page.slug, "hello-world".to_string()); + assert_eq!(page.extra.get("language").unwrap(), &to_value("en")); + assert_eq!( + page.extra.get("authors").unwrap(), + &to_value(["Bob".to_string(), "Alice".to_string()]) + ); + } + + #[test] + fn test_ignores_pages_with_empty_front_matter() { + let content = r#" "#; + let mut page = Page::default(); + let res = parse_front_matter(content, &mut page); + assert!(res.is_err()); + } + + #[test] + fn test_errors_with_invalid_front_matter() { + let content = r#"title = 1\n"#; + let mut page = Page::default(); + let res = parse_front_matter(content, &mut page); + assert!(res.is_err()); + } + + #[test] + fn test_errors_with_missing_required_value_front_matter() { + let content = r#"title = """#; + let mut page = Page::default(); + let res = parse_front_matter(content, &mut page); + assert!(res.is_err()); + } + + #[test] + fn test_errors_on_non_string_tag() { + let content = r#" +title = "Hello" +slug = "hello-world" +tags = ["rust", 1]"#; + let mut page = Page::default(); + let res = parse_front_matter(content, &mut page); + assert!(res.is_err()); + } +} diff --git a/src/main.rs b/src/main.rs index 61119ec..fc6342c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,24 @@ -// `error_chain!` can recurse deeply -#![recursion_limit = "1024"] +#![feature(proc_macro)] + #[macro_use] extern crate clap; #[macro_use] extern crate error_chain; #[macro_use] extern crate lazy_static; +#[macro_use] extern crate serde_derive; extern crate toml; extern crate walkdir; extern crate pulldown_cmark; extern crate regex; extern crate tera; +extern crate glob; + mod config; mod errors; mod cmd; mod page; +mod front_matter; + use config::Config; @@ -58,13 +63,13 @@ fn main() { }, }; }, - ("build", None) => { + ("build", Some(_)) => { match cmd::build(get_config()) { Ok(()) => { println!("Project built"); }, Err(e) => { - println!("Error: {}", e); + println!("Error: {}", e.iter().nth(1).unwrap().description()); ::std::process::exit(1); }, }; diff --git a/src/page.rs b/src/page.rs index 3aedecf..557aacb 100644 --- a/src/page.rs +++ b/src/page.rs @@ -1,76 +1,68 @@ /// A page, can be a blog post or a basic page -use std::collections::{HashMap, BTreeMap}; +use std::collections::HashMap; use std::default::Default; +use std::fs::File; +use std::io::prelude::*; // use pulldown_cmark as cmark; use regex::Regex; -use toml::{Parser, Value as TomlValue}; -use tera::{Tera, Value, to_value, Context}; +use tera::{Tera, Value, Context}; -use errors::{Result}; -use errors::ErrorKind::InvalidFrontMatter; +use errors::{Result, ResultExt}; use config::Config; +use front_matter::parse_front_matter; lazy_static! { static ref DELIM_RE: Regex = Regex::new(r"\+\+\+\s*\r?\n").unwrap(); } -// Converts from one value (Toml) to another (Tera) -// Used to fill the Page::extra map -fn toml_to_tera(val: &TomlValue) -> Value { - match *val { - TomlValue::String(ref s) | TomlValue::Datetime(ref s) => to_value(s), - TomlValue::Boolean(ref b) => to_value(b), - TomlValue::Integer(ref n) => to_value(n), - TomlValue::Float(ref n) => to_value(n), - TomlValue::Array(ref arr) => to_value(&arr.into_iter().map(toml_to_tera).collect::>()), - TomlValue::Table(ref table) => { - to_value(&table.into_iter().map(|(k, v)| { - (k, toml_to_tera(v)) - }).collect::>()) - } - } -} +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Page { + // .md filepath, excluding the content/ bit + pub filepath: String, -#[derive(Debug, PartialEq)] -struct Page { // of the page - title: String, - // the url the page appears at (slug form) - url: String, + pub title: String, + // The page slug + pub slug: String, // the actual content of the page - content: String, + pub content: String, // tags, not to be confused with categories - tags: Vec<String>, + pub tags: Vec<String>, // whether this page should be public or not - is_draft: bool, + pub is_draft: bool, // any extra parameter present in the front matter // it will be passed to the template context - extra: HashMap<String, Value>, + pub extra: HashMap<String, Value>, + // the url the page appears at, overrides the slug if set + pub url: Option<String>, // only one category allowed - category: Option<String>, + pub category: Option<String>, // optional date if we want to order pages (ie blog post) - date: Option<String>, + pub date: Option<String>, // optional layout, if we want to specify which html to render for that page - layout: Option<String>, + pub layout: Option<String>, // description that appears when linked, e.g. on twitter - description: Option<String>, + pub description: Option<String>, } impl Default for Page { fn default() -> Page { Page { + filepath: "".to_string(), + title: "".to_string(), - url: "".to_string(), + slug: "".to_string(), content: "".to_string(), tags: vec![], is_draft: false, extra: HashMap::new(), + url: None, category: None, date: None, layout: None, @@ -84,119 +76,65 @@ impl Page { // Parse a page given the content of the .md file // Files without front matter or with invalid front matter are considered // erroneous - pub fn from_str(filename: &str, content: &str) -> Result<Page> { + pub fn from_str(filepath: &str, content: &str) -> Result<Page> { // 1. separate front matter from content if !DELIM_RE.is_match(content) { - return Err(InvalidFrontMatter(filename.to_string()).into()); + bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", filepath); } // 2. extract the front matter and the content let splits: Vec<&str> = DELIM_RE.splitn(content, 2).collect(); let front_matter = splits[0]; - if front_matter.trim() == "" { - return Err(InvalidFrontMatter(filename.to_string()).into()); - } - let content = splits[1]; // 2. create our page, parse front matter and assign all of that let mut page = Page::default(); + page.filepath = filepath.to_string(); page.content = content.to_string(); + parse_front_matter(front_matter, &mut page) + .chain_err(|| format!("Error when parsing front matter of file `{}`", filepath))?; - // Keeps track of required fields: title, url - let mut num_required_fields = 2; - let mut parser = Parser::new(&front_matter); - - if let Some(value) = parser.parse() { - for (key, value) in value.iter() { - if key == "title" { - page.title = value - .as_str() - .ok_or(InvalidFrontMatter(filename.to_string()))? - .to_string(); - num_required_fields -= 1; - } else if key == "url" { - page.url = value - .as_str() - .ok_or(InvalidFrontMatter(filename.to_string()))? - .to_string(); - num_required_fields -= 1; - } else if key == "draft" { - page.is_draft = value - .as_bool() - .ok_or(InvalidFrontMatter(filename.to_string()))?; - } else if key == "category" { - page.category = Some( - value - .as_str() - .ok_or(InvalidFrontMatter(filename.to_string()))?.to_string() - ); - } else if key == "layout" { - page.layout = Some( - value - .as_str() - .ok_or(InvalidFrontMatter(filename.to_string()))?.to_string() - ); - } else if key == "description" { - page.description = Some( - value - .as_str() - .ok_or(InvalidFrontMatter(filename.to_string()))?.to_string() - ); - } else if key == "date" { - page.date = Some( - value - .as_datetime() - .ok_or(InvalidFrontMatter(filename.to_string()))?.to_string() - ); - } else if key == "tags" { - let toml_tags = value - .as_slice() - .ok_or(InvalidFrontMatter(filename.to_string()))?; + Ok(page) + } - for tag in toml_tags { - page.tags.push( - tag - .as_str() - .ok_or(InvalidFrontMatter(filename.to_string()))? - .to_string() - ); - } - } else { - page.extra.insert(key.to_string(), toml_to_tera(value)); - } - } + pub fn from_file(path: &str) -> Result<Page> { + let mut content = String::new(); + File::open(path) + .chain_err(|| format!("Failed to open '{:?}'", path))? + .read_to_string(&mut content)?; - } else { - // TODO: handle error in parsing TOML - println!("parse errors: {:?}", parser.errors); - } + Page::from_str(path, &content) + } - if num_required_fields > 0 { - println!("Not all required fields"); - return Err(InvalidFrontMatter(filename.to_string()).into()); + fn get_layout_name(&self) -> String { + // TODO: handle themes + match self.layout { + Some(ref l) => l.to_string(), + None => "_default/single.html".to_string() } - - Ok(page) } -// pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> { -// -// } + pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> { + let tpl = self.get_layout_name(); + let mut context = Context::new(); + context.add("site", config); + context.add("page", self); + // println!("{:?}", tera); + tera.render(&tpl, context).chain_err(|| "") + } } #[cfg(test)] mod tests { use super::{Page}; - use tera::to_value; #[test] fn test_can_parse_a_valid_page() { let content = r#" title = "Hello" -url = "hello-world" +slug = "hello-world" +++ Hello world"#; let res = Page::from_str("", content); @@ -204,90 +142,8 @@ Hello world"#; let page = res.unwrap(); assert_eq!(page.title, "Hello".to_string()); - assert_eq!(page.url, "hello-world".to_string()); + assert_eq!(page.slug, "hello-world".to_string()); assert_eq!(page.content, "Hello world".to_string()); } - #[test] - fn test_can_parse_tags() { - let content = r#" -title = "Hello" -url = "hello-world" -tags = ["rust", "html"] -+++ -Hello world"#; - let res = Page::from_str("", content); - assert!(res.is_ok()); - let page = res.unwrap(); - - assert_eq!(page.title, "Hello".to_string()); - assert_eq!(page.url, "hello-world".to_string()); - assert_eq!(page.content, "Hello world".to_string()); - assert_eq!(page.tags, ["rust".to_string(), "html".to_string()]); - } - - #[test] - fn test_can_parse_extra_attributes_in_frontmatter() { - let content = r#" -title = "Hello" -url = "hello-world" -language = "en" -authors = ["Bob", "Alice"] -+++ -Hello world"#; - let res = Page::from_str("", content); - assert!(res.is_ok()); - let page = res.unwrap(); - - assert_eq!(page.title, "Hello".to_string()); - assert_eq!(page.url, "hello-world".to_string()); - assert_eq!(page.extra.get("language").unwrap(), &to_value("en")); - assert_eq!( - page.extra.get("authors").unwrap(), - &to_value(["Bob".to_string(), "Alice".to_string()]) - ); - } - - #[test] - fn test_ignore_pages_with_no_front_matter() { - let content = r#"Hello world"#; - let res = Page::from_str("", content); - assert!(res.is_err()); - } - - #[test] - fn test_ignores_pages_with_empty_front_matter() { - let content = r#"+++\nHello world"#; - let res = Page::from_str("", content); - assert!(res.is_err()); - } - - #[test] - fn test_ignores_pages_with_invalid_front_matter() { - let content = r#"title = 1\n+++\nHello world"#; - let res = Page::from_str("", content); - assert!(res.is_err()); - } - - #[test] - fn test_ignores_pages_with_missing_required_value_front_matter() { - let content = r#" -title = "" -+++ -Hello world"#; - let res = Page::from_str("", content); - assert!(res.is_err()); - } - - #[test] - fn test_errors_on_non_string_tag() { - let content = r#" -title = "Hello" -url = "hello-world" -tags = ["rust", 1] -+++ -Hello world"#; - let res = Page::from_str("", content); - assert!(res.is_err()); - } }