mirror of
https://github.com/getzola/zola
synced 2024-12-12 13:22:29 +00:00
Separate front matter parsing from the page
This commit is contained in:
parent
4ae84e468b
commit
3cd5da2128
10 changed files with 392 additions and 229 deletions
77
Cargo.lock
generated
77
Cargo.lock
generated
|
@ -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}
|
||||
|
||||
|
|
35
README.md
Normal file
35
README.md
Normal 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
|
|
@ -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")
|
||||
|
|
185
src/front_matter.rs
Normal file
185
src/front_matter.rs
Normal 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());
|
||||
}
|
||||
}
|
13
src/main.rs
13
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);
|
||||
},
|
||||
};
|
||||
|
|
262
src/page.rs
262
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::<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();
|
||||
|
||||
// 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()))?;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// TODO: handle error in parsing TOML
|
||||
println!("parse errors: {:?}", parser.errors);
|
||||
}
|
||||
|
||||
if num_required_fields > 0 {
|
||||
println!("Not all required fields");
|
||||
return Err(InvalidFrontMatter(filename.to_string()).into());
|
||||
}
|
||||
parse_front_matter(front_matter, &mut page)
|
||||
.chain_err(|| format!("Error when parsing front matter of file `{}`", filepath))?;
|
||||
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
// pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> {
|
||||
//
|
||||
// }
|
||||
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)?;
|
||||
|
||||
Page::from_str(path, &content)
|
||||
}
|
||||
|
||||
fn get_layout_name(&self) -> String {
|
||||
// TODO: handle themes
|
||||
match self.layout {
|
||||
Some(ref l) => l.to_string(),
|
||||
None => "_default/single.html".to_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…
Reference in a new issue