Separate front matter parsing from the page

This commit is contained in:
Vincent Prouillet 2016-12-11 15:05:03 +09:00
parent 4ae84e468b
commit 3cd5da2128
10 changed files with 392 additions and 229 deletions

77
Cargo.lock generated
View file

@ -2,12 +2,16 @@
name = "gutenberg" name = "gutenberg"
version = "0.1.0" version = "0.1.0"
dependencies = [ 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)", "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)", "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)", "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)", "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)", "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)", "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)", "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)", "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]] [[package]]
name = "clap" name = "clap"
version = "2.19.1" version = "2.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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)", "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)", "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)", "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" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "regex" name = "regex"
version = "0.1.80" version = "0.1.80"
@ -243,7 +252,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "rustc-serialize" name = "rustc-serialize"
version = "0.3.21" version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
@ -259,9 +268,35 @@ name = "serde"
version = "0.8.19" version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "serde_json" name = "serde_json"
version = "0.8.3" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "dtoa 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -283,10 +318,19 @@ name = "strsim"
version = "0.5.2" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "tera" name = "tera"
version = "0.4.1" 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 = [ dependencies = [
"error-chain 0.7.1 (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)", "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)", "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)", "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 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)", "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)", "url 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -332,7 +376,7 @@ name = "toml"
version = "0.1.30" version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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]] [[package]]
@ -366,6 +410,11 @@ name = "unicode-width"
version = "0.1.3" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "unidecode" name = "unidecode"
version = "0.2.0" 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.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 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 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 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 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" "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 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 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 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 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 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-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 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 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 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 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 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 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" "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-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-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-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 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 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" "checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f"

View file

@ -16,6 +16,10 @@ walkdir = "1"
pulldown-cmark = "0" pulldown-cmark = "0"
regex = "0.1" regex = "0.1"
lazy_static = "0.2" 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" } tera = { git = "https://github.com/Keats/tera.git", branch = "v0.5" }
clippy = {version = "~0.0.103", optional = true} clippy = {version = "~0.0.103", optional = true}

35
README.md Normal file
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

View file

@ -1,12 +1,28 @@
use glob::glob;
use tera::Tera;
use config:: Config; use config:: Config;
use errors::{Result}; use errors::{Result, ResultExt};
use page::Page;
use tera::Tera;
pub fn build(config: Config) -> Result<()> { 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(()) Ok(())
} }

View file

@ -3,7 +3,7 @@ use std::io::prelude::*;
use std::fs::{create_dir, File}; use std::fs::{create_dir, File};
use std::path::Path; use std::path::Path;
use errors::{Result, ErrorKind}; use errors::Result;
const CONFIG: &'static str = r#" const CONFIG: &'static str = r#"

View file

@ -5,13 +5,14 @@ use std::path::Path;
use toml::Parser; use toml::Parser;
use errors::{Result, ErrorKind}; use errors::{Result, ErrorKind, ResultExt};
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub title: String, pub title: String,
pub base_url: String, pub base_url: String,
pub theme: String,
pub favicon: Option<String>, pub favicon: Option<String>,
} }
@ -21,6 +22,7 @@ impl Default for Config {
Config { Config {
title: "".to_string(), title: "".to_string(),
base_url: "".to_string(), base_url: "".to_string(),
theme: "".to_string(),
favicon: None, favicon: None,
} }
@ -55,12 +57,15 @@ impl Config {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Config> { pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Config> {
let mut content = String::new(); 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) Config::from_str(&content)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{Config}; use super::{Config};

View file

@ -1,14 +1,16 @@
use tera;
error_chain! { error_chain! {
links {
Tera(tera::Error, tera::ErrorKind);
}
foreign_links { foreign_links {
Io(::std::io::Error); Io(::std::io::Error);
} }
errors { errors {
InvalidFrontMatter(name: String) {
description("frontmatter is invalid")
display("Front Matter of file '{}' is missing or is invalid", name)
}
InvalidConfig { InvalidConfig {
description("invalid config") description("invalid config")
display("The config.toml is invalid or is using the wrong type for an argument") display("The config.toml is invalid or is using the wrong type for an argument")

185
src/front_matter.rs Normal file
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());
}
}

View file

@ -1,19 +1,24 @@
// `error_chain!` can recurse deeply #![feature(proc_macro)]
#![recursion_limit = "1024"]
#[macro_use] extern crate clap; #[macro_use] extern crate clap;
#[macro_use] extern crate error_chain; #[macro_use] extern crate error_chain;
#[macro_use] extern crate lazy_static; #[macro_use] extern crate lazy_static;
#[macro_use] extern crate serde_derive;
extern crate toml; extern crate toml;
extern crate walkdir; extern crate walkdir;
extern crate pulldown_cmark; extern crate pulldown_cmark;
extern crate regex; extern crate regex;
extern crate tera; extern crate tera;
extern crate glob;
mod config; mod config;
mod errors; mod errors;
mod cmd; mod cmd;
mod page; mod page;
mod front_matter;
use config::Config; use config::Config;
@ -58,13 +63,13 @@ fn main() {
}, },
}; };
}, },
("build", None) => { ("build", Some(_)) => {
match cmd::build(get_config()) { match cmd::build(get_config()) {
Ok(()) => { Ok(()) => {
println!("Project built"); println!("Project built");
}, },
Err(e) => { Err(e) => {
println!("Error: {}", e); println!("Error: {}", e.iter().nth(1).unwrap().description());
::std::process::exit(1); ::std::process::exit(1);
}, },
}; };

View file

@ -1,76 +1,68 @@
/// A page, can be a blog post or a basic page /// 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::default::Default;
use std::fs::File;
use std::io::prelude::*;
// use pulldown_cmark as cmark; // use pulldown_cmark as cmark;
use regex::Regex; use regex::Regex;
use toml::{Parser, Value as TomlValue}; use tera::{Tera, Value, Context};
use tera::{Tera, Value, to_value, Context};
use errors::{Result}; use errors::{Result, ResultExt};
use errors::ErrorKind::InvalidFrontMatter;
use config::Config; use config::Config;
use front_matter::parse_front_matter;
lazy_static! { lazy_static! {
static ref DELIM_RE: Regex = Regex::new(r"\+\+\+\s*\r?\n").unwrap(); 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> of the page
title: String, pub title: String,
// the url the page appears at (slug form) // The page slug
url: String, pub slug: String,
// the actual content of the page // the actual content of the page
content: String, pub content: String,
// tags, not to be confused with categories // tags, not to be confused with categories
tags: Vec<String>, pub tags: Vec<String>,
// whether this page should be public or not // whether this page should be public or not
is_draft: bool, pub is_draft: bool,
// any extra parameter present in the front matter // any extra parameter present in the front matter
// it will be passed to the template context // 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 // only one category allowed
category: Option<String>, pub category: Option<String>,
// optional date if we want to order pages (ie blog post) // 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 // 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 that appears when linked, e.g. on twitter
description: Option<String>, pub description: Option<String>,
} }
impl Default for Page { impl Default for Page {
fn default() -> Page { fn default() -> Page {
Page { Page {
filepath: "".to_string(),
title: "".to_string(), title: "".to_string(),
url: "".to_string(), slug: "".to_string(),
content: "".to_string(), content: "".to_string(),
tags: vec![], tags: vec![],
is_draft: false, is_draft: false,
extra: HashMap::new(), extra: HashMap::new(),
url: None,
category: None, category: None,
date: None, date: None,
layout: None, layout: None,
@ -84,119 +76,65 @@ impl Page {
// Parse a page given the content of the .md file // Parse a page given the content of the .md file
// Files without front matter or with invalid front matter are considered // Files without front matter or with invalid front matter are considered
// erroneous // 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 // 1. separate front matter from content
if !DELIM_RE.is_match(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 // 2. extract the front matter and the content
let splits: Vec<&str> = DELIM_RE.splitn(content, 2).collect(); let splits: Vec<&str> = DELIM_RE.splitn(content, 2).collect();
let front_matter = splits[0]; let front_matter = splits[0];
if front_matter.trim() == "" {
return Err(InvalidFrontMatter(filename.to_string()).into());
}
let content = splits[1]; let content = splits[1];
// 2. create our page, parse front matter and assign all of that // 2. create our page, parse front matter and assign all of that
let mut page = Page::default(); let mut page = Page::default();
page.filepath = filepath.to_string();
page.content = content.to_string(); page.content = content.to_string();
parse_front_matter(front_matter, &mut page)
// Keeps track of required fields: title, url .chain_err(|| format!("Error when parsing front matter of file `{}`", filepath))?;
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());
}
Ok(page) 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)] #[cfg(test)]
mod tests { mod tests {
use super::{Page}; use super::{Page};
use tera::to_value;
#[test] #[test]
fn test_can_parse_a_valid_page() { fn test_can_parse_a_valid_page() {
let content = r#" let content = r#"
title = "Hello" title = "Hello"
url = "hello-world" slug = "hello-world"
+++ +++
Hello world"#; Hello world"#;
let res = Page::from_str("", content); let res = Page::from_str("", content);
@ -204,90 +142,8 @@ Hello world"#;
let page = res.unwrap(); let page = res.unwrap();
assert_eq!(page.title, "Hello".to_string()); 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()); 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());
}
} }