Browse Source

Separate front matter parsing from the page

index-subcmd
Vincent Prouillet 8 years ago
parent
commit
3cd5da2128
10 changed files with 389 additions and 226 deletions
  1. +66
    -11
      Cargo.lock
  2. +4
    -0
      Cargo.toml
  3. +35
    -0
      README.md
  4. +19
    -3
      src/cmd/build.rs
  5. +1
    -1
      src/cmd/new.rs
  6. +8
    -3
      src/config.rs
  7. +6
    -4
      src/errors.rs
  8. +185
    -0
      src/front_matter.rs
  9. +9
    -4
      src/main.rs
  10. +56
    -200
      src/page.rs

+ 66
- 11
Cargo.lock View File

@@ -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"


+ 4
- 0
Cargo.toml View File

@@ -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}



+ 35
- 0
README.md View File

@@ -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

+ 19
- 3
src/cmd/build.rs View File

@@ -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(())
}

+ 1
- 1
src/cmd/new.rs View File

@@ -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#"


+ 8
- 3
src/config.rs View File

@@ -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};


+ 6
- 4
src/errors.rs View File

@@ -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")


+ 185
- 0
src/front_matter.rs View File

@@ -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());
}
}

+ 9
- 4
src/main.rs View File

@@ -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);
},
};


+ 56
- 200
src/page.rs View File

@@ -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());
}
}

Loading…
Cancel
Save