@@ -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)" = "<none>" | |||
"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" | |||
@@ -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} | |||
@@ -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 |
@@ -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<Page> = 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(()) | |||
} |
@@ -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#" | |||
@@ -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<String>, | |||
} | |||
@@ -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<P: AsRef<Path>>(path: P) -> Result<Config> { | |||
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}; | |||
@@ -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") | |||
@@ -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::<Vec<_>>()), | |||
TomlValue::Table(ref table) => { | |||
to_value(&table.into_iter().map(|(k, v)| { | |||
(k, toml_to_tera(v)) | |||
}).collect::<BTreeMap<_, _>>()) | |||
} | |||
} | |||
} | |||
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()); | |||
} | |||
} |
@@ -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); | |||
}, | |||
}; | |||
@@ -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::<Vec<_>>()), | |||
TomlValue::Table(ref table) => { | |||
to_value(&table.into_iter().map(|(k, v)| { | |||
(k, toml_to_tera(v)) | |||
}).collect::<BTreeMap<_,_>>()) | |||
} | |||
} | |||
} | |||
#[derive(Debug, PartialEq, Serialize, Deserialize)] | |||
pub struct Page { | |||
// .md filepath, excluding the content/ bit | |||
pub filepath: String, | |||
#[derive(Debug, PartialEq)] | |||
struct Page { | |||
// <title> 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()); | |||
} | |||
} |