From 7e496878e5c40ccfb64939a0915eaa0104182897 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 14 Mar 2017 21:25:45 +0900 Subject: [PATCH] Sections Parse _index.md files as sections and render them --- .gitignore | 3 +- Cargo.lock | 118 +++--- Cargo.toml | 17 +- src/cmd/build.rs | 4 +- src/cmd/serve.rs | 3 +- src/config.rs | 4 +- src/errors.rs | 3 +- src/front_matter.rs | 174 ++------- src/lib.rs | 8 +- src/page.rs | 347 ++++-------------- src/section.rs | 101 +++++ src/site.rs | 142 +++++-- src/utils.rs | 49 +++ test_site/config.toml | 7 + test_site/content/posts/_index.md | 4 + test_site/content/posts/fixed-slug.md | 7 + test_site/content/posts/fixed-url.md | 7 + test_site/content/posts/no-section/simple.md | 6 + test_site/content/posts/python.md | 6 + test_site/content/posts/simple.md | 6 + test_site/content/posts/tutorials/_index.md | 4 + .../content/posts/tutorials/devops/_index.md | 4 + .../content/posts/tutorials/devops/docker.md | 6 + .../content/posts/tutorials/devops/nix.md | 6 + .../posts/tutorials/programming/_index.md | 4 + .../posts/tutorials/programming/python.md | 6 + .../posts/tutorials/programming/rust.md | 6 + test_site/content/posts/with-assets/index.md | 7 + test_site/content/posts/with-assets/with.js | 0 test_site/static/scripts/hello.js | 0 test_site/static/site.css | 3 + test_site/templates/categories.html | 3 + test_site/templates/category.html | 8 + test_site/templates/index.html | 27 ++ test_site/templates/page.html | 5 + test_site/templates/section.html | 10 + test_site/templates/tag.html | 7 + test_site/templates/tags.html | 3 + tests/front_matter.rs | 197 ++++++++++ tests/page.rs | 249 +++++++++++++ tests/site.rs | 176 +++++++++ 41 files changed, 1208 insertions(+), 539 deletions(-) create mode 100644 src/section.rs create mode 100644 test_site/config.toml create mode 100644 test_site/content/posts/_index.md create mode 100644 test_site/content/posts/fixed-slug.md create mode 100644 test_site/content/posts/fixed-url.md create mode 100644 test_site/content/posts/no-section/simple.md create mode 100644 test_site/content/posts/python.md create mode 100644 test_site/content/posts/simple.md create mode 100644 test_site/content/posts/tutorials/_index.md create mode 100644 test_site/content/posts/tutorials/devops/_index.md create mode 100644 test_site/content/posts/tutorials/devops/docker.md create mode 100644 test_site/content/posts/tutorials/devops/nix.md create mode 100644 test_site/content/posts/tutorials/programming/_index.md create mode 100644 test_site/content/posts/tutorials/programming/python.md create mode 100644 test_site/content/posts/tutorials/programming/rust.md create mode 100644 test_site/content/posts/with-assets/index.md create mode 100644 test_site/content/posts/with-assets/with.js create mode 100644 test_site/static/scripts/hello.js create mode 100644 test_site/static/site.css create mode 100644 test_site/templates/categories.html create mode 100644 test_site/templates/category.html create mode 100644 test_site/templates/index.html create mode 100644 test_site/templates/page.html create mode 100644 test_site/templates/section.html create mode 100644 test_site/templates/tag.html create mode 100644 test_site/templates/tags.html create mode 100644 tests/front_matter.rs create mode 100644 tests/page.rs create mode 100644 tests/site.rs diff --git a/.gitignore b/.gitignore index 99fac5f..b4f2bd6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ target .idea/ -site -theme +test_site/public diff --git a/Cargo.lock b/Cargo.lock index 44a8b3e..b76548a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,14 +3,14 @@ name = "gutenberg" version = "0.0.1" dependencies = [ "chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.20.5 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.21.1 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "mount 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.0 (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.10 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", @@ -18,7 +18,8 @@ dependencies = [ "slug 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "staticfile 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "syntect 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tera 0.8.0 (git+https://github.com/Keats/tera?branch=reload)", + "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "tera 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "ws 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -26,7 +27,7 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -37,6 +38,16 @@ name = "ansi_term" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "atty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "backtrace" version = "0.3.0" @@ -56,7 +67,7 @@ name = "backtrace-sys" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "gcc 0.3.43 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -67,7 +78,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -76,11 +87,6 @@ name = "bitflags" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "bitflags" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "bitflags" version = "0.7.0" @@ -131,17 +137,17 @@ dependencies = [ [[package]] name = "clap" -version = "2.20.5" +version = "2.21.1" 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)", - "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "term_size 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "vec_map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -149,7 +155,7 @@ name = "cmake" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "gcc 0.3.43 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -157,7 +163,7 @@ name = "conduit-mime-types" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -233,7 +239,7 @@ dependencies = [ [[package]] name = "gcc" -version = "0.3.43" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -253,7 +259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "humansize" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -264,9 +270,9 @@ dependencies = [ "httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "mime 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -293,6 +299,15 @@ dependencies = [ "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "iovec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "iron" version = "0.5.1" @@ -364,7 +379,7 @@ dependencies = [ [[package]] name = "mime" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -375,7 +390,7 @@ name = "miniz-sys" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "gcc 0.3.43 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -397,9 +412,10 @@ dependencies = [ [[package]] name = "mio" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ + "iovec 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", @@ -563,7 +579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", "xml-rs 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -578,10 +594,10 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.0.8" +version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -608,7 +624,7 @@ name = "regex" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "aho-corasick 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -627,7 +643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "rustc-serialize" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -766,20 +782,28 @@ dependencies = [ "onig 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "plist 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "yaml-rust 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tempdir" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "tera" -version = "0.8.0" -source = "git+https://github.com/Keats/tera?branch=reload#ee038a6f3519ac35e1878ca9b29ec739a8f84a15" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", - "humansize 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "humansize 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "pest 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -927,7 +951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "vec_map" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -972,7 +996,7 @@ dependencies = [ "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "mio 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", "sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1002,13 +1026,13 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] -"checksum aho-corasick 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0638fd549427caa90c499814196d1b9e3725eb4d15d7339d6de073a680ed0ca2" +"checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699" "checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6" +"checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159" "checksum backtrace 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f551bc2ddd53aea015d453ef0b635af89444afa5ed2405dd0b2062ad5d600d80" "checksum backtrace-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d192fd129132fbc97497c1f2ec2c2c5174e376b95f535199ef4fe0a293d33842" "checksum bincode 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "55eb0b7fd108527b0c77860f75eca70214e11a8b4c6ef05148c54c05a25d48ad" "checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3" -"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.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e1ab483fc81a8143faa7203c4a3c02888ebd1a782e37e41fa34753ba9a162" "checksum byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" @@ -1017,7 +1041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c" "checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00" "checksum chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "158b0bd7d75cbb6bf9c25967a48a2e9f77da95876b858eadfabaa99cd069de6e" -"checksum clap 2.20.5 (registry+https://github.com/rust-lang/crates.io-index)" = "7db281b0520e97fbd15cd615dcd8f8bcad0c26f5f7d5effe705f090f39e9a758" +"checksum clap 2.21.1 (registry+https://github.com/rust-lang/crates.io-index)" = "74a80f603221c9cd9aa27a28f52af452850051598537bb6b359c38a7d61e5cda" "checksum cmake 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "e1acc68a3f714627af38f9f5d09706a28584ba60dfe2cca68f40bf779f941b25" "checksum conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "95ca30253581af809925ef68c2641cc140d6183f43e12e0af4992d53768bd7b8" "checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850" @@ -1029,14 +1053,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344" "checksum fsevent 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "dfe593ebcfc76884138b25426999890b10da8e6a46d01b499d7c54c604672c38" "checksum fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1a772d36c338d07a032d5375a36f15f9a7043bf0cb8ce7cee658e037c6032874" -"checksum gcc 0.3.43 (registry+https://github.com/rust-lang/crates.io-index)" = "c07c758b972368e703a562686adb39125707cc1ef3399da8c019fc6c2498a75d" +"checksum gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)" = "40899336fb50db0c78710f53e87afc54d8c7266fb76262fecc78ca1a7f09deae" "checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" "checksum httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e7a63e511f9edffbab707141fbb8707d1a3098615fb2adbd5769cdfcc9b17d" -"checksum humansize 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b963e0c0a5149e12a9cab4d889404e4935e3484db7c4d9681e8bbdbcb9dfd80" +"checksum humansize 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "92d211e6e70b05749dce515b47684f29a3c8c38bbbb21c50b30aff9eca1b0bd3" "checksum hyper 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)" = "43a15e3273b2133aaac0150478ab443fb89f15c3de41d8d93d8f3bb14bf560f6" "checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11" "checksum inotify 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887fcc180136e77a85e6a6128579a719027b1bab9b1c38ea4444244fe262c20c" +"checksum iovec 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "29d062ee61fccdf25be172e70f34c9f6efc597e1fb8f6526e8437b2046ab26be" "checksum iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2440ae846e7a8c7f9b401db8f6e31b4ea5e7d3688b91761337da7e054520c75b" "checksum itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "eb2f404fbc66fd9aac13e998248505e7ecb2ad8e44ab6388684c5fb11c6c251c" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" @@ -1047,10 +1072,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5141eca02775a762cc6cd564d8d2c50f67c0ea3a372cbf1c51592b3e029e10ad" "checksum matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efd7622e3022e1a6eaa602c4cea8912254e5582c9c692e9167714182244801b1" "checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4" -"checksum mime 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5c93a4bd787ddc6e7833c519b73a50883deb5863d76d9b71eb8216fb7f94e66" +"checksum mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5514f038123342d01ee5f95129e4ef1e0470c93bc29edf058a46f9ee3ba6737e" "checksum miniz-sys 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "28eaee17666671fa872e567547e8428e83308ebe5808cdf6a0e28397dbe2c726" "checksum mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a637d1ca14eacae06296a008fa7ad955347e34efcb5891cfd8ba05491a37907e" -"checksum mio 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "eecdbdd49a849336e77b453f021c89972a2cfb5b51931a0026ae0ac4602de681" +"checksum mio 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "aa30e3753079b08ce3d75cf3b44783e36fe0e1f64065f65c1d894d1688fb2580" "checksum miow 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3e690c5df6b2f60acd45d56378981e827ff8295562fc8d34f573deb267a59cd1" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" "checksum modifier 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "41f5c9112cb662acd3b204077e0de5bc66305fa8df65c8019d5adb10e9ab6e58" @@ -1069,14 +1094,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903" "checksum plist 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6ab9bef2781bcdac1baf3e29eb297344cd24263e22fd9436d3a21215b7d8aa" "checksum plugin 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1a6a0dc3910bc8db877ffed8e457763b317cf880df4ae19109b9f77d277cf6e0" -"checksum pulldown-cmark 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1058d7bb927ca067656537eec4e02c2b4b70eaaa129664c5b90c111e20326f41" +"checksum pulldown-cmark 0.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "b0b0f7b64fd9ff618da552df85b0d356a1487e5ef41df8b5727b0f73bd1215a1" "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" "checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d" "checksum redox_syscall 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "8dd35cc9a8bdec562c757e3d43c1526b5c6d2653e23e2315065bc25556550753" "checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01" "checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457" "checksum rustc-demangle 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3058a43ada2c2d0b92b3ae38007a2d0fa5e9db971be260e0171408a4ff471c95" -"checksum rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "237546c689f20bb44980270c73c3b9edd0891c1be49cc1274406134a66d3957b" +"checksum rustc-serialize 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "684ce48436d6465300c9ea783b6b14c4361d6b8dcbb1375b486a69cc19e2dfb0" "checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" "checksum same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d931a44fdaa43b8637009e7632a02adc4f2b2e0733c08caa4cf00e8da4a117a7" "checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" @@ -1095,7 +1120,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum syn 0.11.9 (registry+https://github.com/rust-lang/crates.io-index)" = "480c834701caba3548aa991e54677281be3a5414a9d09ddbdf4ed74a569a9d19" "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" "checksum syntect 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6728e7e9bbd971751d17d39b0e38e3558c10b9fb32125441bb17c434a2754e7c" -"checksum tera 0.8.0 (git+https://github.com/Keats/tera?branch=reload)" = "" +"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" +"checksum tera 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6f2ff83a1773a0482ddc961d0030b514f1848f592ae9612afb241e5eb455df75" "checksum term_size 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "07b6c1ac5b3fffd75073276bca1ceed01f67a28537097a2a9539e116e50fb21a" "checksum thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4437c97558c70d129e40629a5b385b3fb1ffac301e63941335e4d354081ec14a" "checksum thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c85048c6260d17cf486ceae3282d9fb6b90be220bf5b28c400f5485ffc29f0c7" @@ -1115,7 +1141,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum unsafe-any 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b351086021ebc264aea3ab4f94d61d889d98e5e9ec2d985d993f50133537fd3a" "checksum url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f5ba8a749fb4479b043733416c244fa9d1d3af3d7c23804944651c8a448cb87e" "checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" -"checksum vec_map 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cac5efe5cb0fa14ec2f84f83c701c562ee63f6dcc680861b21d65c682adfb05f" +"checksum vec_map 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8cdc8b93bd0198ed872357fb2e667f7125646b1762f16d60b2c96350d361897" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum walkdir 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c66c0b9792f0a765345452775f3adbd28dde9d33f30d13e5dcc5ae17cf6f3780" "checksum walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "bb08f9e670fab86099470b97cd2b252d6527f0b3cc1401acdb595ffc9dd288ff" diff --git a/Cargo.toml b/Cargo.toml index 10d6f95..5d74f60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,9 @@ homepage = "https://github.com/Keats/gutenberg" repository = "https://github.com/Keats/gutenberg" keywords = ["static", "site", "generator", "blog"] +[[bin]] +name = "gutenberg" + [dependencies] error-chain = "0.10" clap = "2.19" @@ -20,11 +23,13 @@ glob = "0.2" serde = "0.9" serde_json = "0.9" serde_derive = "0.9" -tera = { git = "https://github.com/Keats/tera", branch = "reload" } -# tera = "0.8" +# tera = { path = "../tera" } +# tera = { git = "https://github.com/Keats/tera", branch = "reload" } +tera = "0.8" slug = "0.1" syntect = "1" chrono = "0.3" +toml = { version = "0.3", default-features = false, features = ["serde"]} # Below is for the serve cmd staticfile = "0.4" @@ -33,9 +38,5 @@ mount = "0.3" notify = "4" ws = "0.6" -[dependencies.toml] -version = "0.3" -default-features = false -features = ["serde"] - - +[dev-dependencies] +tempdir = "0.3" diff --git a/src/cmd/build.rs b/src/cmd/build.rs index a20459c..ec58119 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -1,7 +1,9 @@ +use std::env; + use gutenberg::errors::Result; use gutenberg::Site; pub fn build() -> Result<()> { - Site::new(false)?.build() + Site::new(env::current_dir().unwrap())?.build() } diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index 30a621e..0c83639 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -61,7 +61,8 @@ fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &st pub fn serve(interface: &str, port: &str) -> Result<()> { println!("Building site..."); let start = Instant::now(); - let mut site = Site::new(true)?; + let mut site = Site::new(env::current_dir().unwrap())?; + site.enable_live_reload(); site.build()?; report_elapsed_time(start); diff --git a/src/config.rs b/src/config.rs index c2c108d..76000f3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -81,8 +81,8 @@ impl Default for Config { /// Get and parse the config. /// If it doesn't succeed, exit -pub fn get_config() -> Config { - match Config::from_file("config.toml") { +pub fn get_config(path: &Path) -> Config { + match Config::from_file(path.join("config.toml")) { Ok(c) => c, Err(e) => { println!("Failed to load config.toml"); diff --git a/src/errors.rs b/src/errors.rs index 5ff689c..589c5fc 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,8 +2,7 @@ use tera; use toml; error_chain! { - errors { - } + errors {} links { Tera(tera::Error, tera::ErrorKind); diff --git a/src/front_matter.rs b/src/front_matter.rs index b94827e..c9d7732 100644 --- a/src/front_matter.rs +++ b/src/front_matter.rs @@ -1,12 +1,18 @@ use std::collections::HashMap; - +use std::path::Path; use toml; use tera::Value; use chrono::prelude::*; +use regex::Regex; + +use errors::{Result, ResultExt}; -use errors::{Result}; + +lazy_static! { + static ref PAGE_RE: Regex = Regex::new(r"^\n?\+\+\+\n((?s).*(?-s))\+\+\+\n?((?s).*(?-s))$").unwrap(); +} /// The front matter of every page @@ -85,157 +91,23 @@ impl FrontMatter { } -#[cfg(test)] -mod tests { - use super::{FrontMatter}; - use tera::to_value; - - - #[test] - fn test_can_parse_a_valid_front_matter() { - let content = r#" -title = "Hello" -description = "hey there""#; - let res = FrontMatter::parse(content); - println!("{:?}", res); - assert!(res.is_ok()); - let res = res.unwrap(); - assert_eq!(res.title, "Hello".to_string()); - assert_eq!(res.description, "hey there".to_string()); - } - - #[test] - fn test_can_parse_tags() { - let content = r#" -title = "Hello" -description = "hey there" -slug = "hello-world" -tags = ["rust", "html"]"#; - let res = FrontMatter::parse(content); - assert!(res.is_ok()); - let res = res.unwrap(); - - assert_eq!(res.title, "Hello".to_string()); - assert_eq!(res.slug.unwrap(), "hello-world".to_string()); - assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]); - } - - #[test] - fn test_can_parse_extra_attributes_in_frontmatter() { - let content = r#" -title = "Hello" -description = "hey there" -slug = "hello-world" - -[extra] -language = "en" -authors = ["Bob", "Alice"]"#; - let res = FrontMatter::parse(content); - assert!(res.is_ok()); - let res = res.unwrap(); - - assert_eq!(res.title, "Hello".to_string()); - assert_eq!(res.slug.unwrap(), "hello-world".to_string()); - let extra = res.extra.unwrap(); - assert_eq!(extra.get("language").unwrap(), &to_value("en").unwrap()); - assert_eq!( - extra.get("authors").unwrap(), - &to_value(["Bob".to_string(), "Alice".to_string()]).unwrap() - ); +/// Split a file between the front matter and its content +/// It will parse the front matter as well and returns any error encountered +/// TODO: add tests +pub fn split_content(file_path: &Path, content: &str) -> Result<(FrontMatter, String)> { + if !PAGE_RE.is_match(content) { + bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy()); } - #[test] - fn test_is_ok_with_url_instead_of_slug() { - let content = r#" -title = "Hello" -description = "hey there" -url = "hello-world""#; - let res = FrontMatter::parse(content); - assert!(res.is_ok()); - let res = res.unwrap(); - assert!(res.slug.is_none()); - assert_eq!(res.url.unwrap(), "hello-world".to_string()); - } + // 2. extract the front matter and the content + let caps = PAGE_RE.captures(content).unwrap(); + // caps[0] is the full match + let front_matter = &caps[1]; + let content = &caps[2]; - #[test] - fn test_errors_with_empty_front_matter() { - let content = r#" "#; - let res = FrontMatter::parse(content); - assert!(res.is_err()); - } - - #[test] - fn test_errors_with_invalid_front_matter() { - let content = r#"title = 1\n"#; - let res = FrontMatter::parse(content); - assert!(res.is_err()); - } + // 3. create our page, parse front matter and assign all of that + let meta = FrontMatter::parse(front_matter) + .chain_err(|| format!("Error when parsing front matter of file `{}`", file_path.to_string_lossy()))?; - #[test] - fn test_errors_with_missing_required_value_front_matter() { - let content = r#"title = """#; - let res = FrontMatter::parse(content); - assert!(res.is_err()); - } - - #[test] - fn test_errors_on_non_string_tag() { - let content = r#" -title = "Hello" -description = "hey there" -slug = "hello-world" -tags = ["rust", 1]"#; - let res = FrontMatter::parse(content); - assert!(res.is_err()); - } - - #[test] - fn test_errors_on_present_but_empty_slug() { - let content = r#" -title = "Hello" -description = "hey there" -slug = """#; - let res = FrontMatter::parse(content); - assert!(res.is_err()); - } - - #[test] - fn test_errors_on_present_but_empty_url() { - let content = r#" -title = "Hello" -description = "hey there" -url = """#; - let res = FrontMatter::parse(content); - assert!(res.is_err()); - } - - #[test] - fn test_parse_date_yyyy_mm_dd() { - let content = r#" -title = "Hello" -description = "hey there" -date = "2016-10-10""#; - let res = FrontMatter::parse(content).unwrap(); - assert!(res.parse_date().is_some()); - } - - #[test] - fn test_parse_date_rfc3339() { - let content = r#" -title = "Hello" -description = "hey there" -date = "2002-10-02T15:00:00Z""#; - let res = FrontMatter::parse(content).unwrap(); - assert!(res.parse_date().is_some()); - } - - #[test] - fn test_cant_parse_random_date_format() { - let content = r#" -title = "Hello" -description = "hey there" -date = "2002/10/12""#; - let res = FrontMatter::parse(content).unwrap(); - assert!(res.parse_date().is_none()); - } + Ok((meta, content.to_string())) } diff --git a/src/lib.rs b/src/lib.rs index 81d1df9..3c5fe81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ extern crate glob; extern crate syntect; extern crate slug; extern crate chrono; +#[cfg(test)] +extern crate tempdir; mod utils; mod config; @@ -22,9 +24,11 @@ mod page; mod front_matter; mod site; mod markdown; +mod section; pub use site::Site; pub use config::Config; -pub use front_matter::FrontMatter; -pub use page::Page; +pub use front_matter::{FrontMatter, split_content}; +pub use page::{Page}; +pub use section::{Section}; pub use utils::create_file; diff --git a/src/page.rs b/src/page.rs index 44d1fb4..6b2e69c 100644 --- a/src/page.rs +++ b/src/page.rs @@ -1,32 +1,28 @@ /// A page, can be a blog post or a basic page use std::cmp::Ordering; -use std::fs::{File, read_dir}; -use std::io::prelude::*; +use std::fs::{read_dir}; use std::path::{Path, PathBuf}; use std::result::Result as StdResult; -use regex::Regex; use tera::{Tera, Context}; use serde::ser::{SerializeStruct, self}; use slug::slugify; use errors::{Result, ResultExt}; use config::Config; -use front_matter::{FrontMatter}; +use front_matter::{FrontMatter, split_content}; use markdown::markdown_to_html; +use utils::{read_file, find_content_components}; -lazy_static! { - static ref PAGE_RE: Regex = Regex::new(r"^\n?\+\+\+\n((?s).*(?-s))\+\+\+\n((?s).*(?-s))$").unwrap(); -} /// Looks into the current folder for the path and see if there's anything that is not a .md /// file. Those will be copied next to the rendered .html file fn find_related_assets(path: &Path) -> Vec { let mut assets = vec![]; - for entry in read_dir(path.parent().unwrap()).unwrap().filter_map(|e| e.ok()) { + for entry in read_dir(path).unwrap().filter_map(|e| e.ok()) { let entry_path = entry.path(); if entry_path.is_file() { match entry_path.extension() { @@ -43,24 +39,22 @@ fn find_related_assets(path: &Path) -> Vec { } -#[derive(Clone, Debug, PartialEq, Deserialize)] +#[derive(Clone, Debug, PartialEq)] pub struct Page { - /// .md filepath, excluding the content/ bit - #[serde(skip_serializing)] - pub filepath: String, + /// The .md path + pub file_path: PathBuf, + /// The parent directory of the file. Is actually the grand parent directory + /// if it's an asset folder + pub parent_path: PathBuf, /// The name of the .md file - #[serde(skip_serializing)] - pub filename: String, - /// The directories above our .md file are called sections - /// for example a file at content/kb/solutions/blabla.md will have 2 sections: + pub file_name: String, + /// The directories above our .md file + /// for example a file at content/kb/solutions/blabla.md will have 2 components: /// `kb` and `solutions` - #[serde(skip_serializing)] - pub sections: Vec, + pub components: Vec, /// The actual content of the page, in markdown - #[serde(skip_serializing)] pub raw_content: String, /// All the non-md files we found next to the .md file - #[serde(skip_serializing)] pub assets: Vec, /// The HTML rendered of the page pub content: String, @@ -89,9 +83,10 @@ pub struct Page { impl Page { pub fn new(meta: FrontMatter) -> Page { Page { - filepath: "".to_string(), - filename: "".to_string(), - sections: vec![], + file_path: PathBuf::new(), + parent_path: PathBuf::new(), + file_name: "".to_string(), + components: vec![], raw_content: "".to_string(), assets: vec![], content: "".to_string(), @@ -118,25 +113,13 @@ 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 parse(filepath: &str, content: &str, config: &Config) -> Result { + pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result { // 1. separate front matter from content - if !PAGE_RE.is_match(content) { - bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", filepath); - } - - // 2. extract the front matter and the content - let caps = PAGE_RE.captures(content).unwrap(); - // caps[0] is the full match - let front_matter = &caps[1]; - let content = &caps[2]; - - // 3. create our page, parse front matter and assign all of that - let meta = FrontMatter::parse(front_matter) - .chain_err(|| format!("Error when parsing front matter of file `{}`", filepath))?; - + let (meta, content) = split_content(file_path, content)?; let mut page = Page::new(meta); - page.filepath = filepath.to_string(); - page.raw_content = content.to_string(); + page.file_path = file_path.to_path_buf(); + page.parent_path = page.file_path.parent().unwrap().to_path_buf(); + page.raw_content = content; // We try to be smart about highlighting code as it can be time-consuming // If the global config disables it, then we do nothing. However, @@ -158,33 +141,38 @@ impl Page { } } - let path = Path::new(filepath); - page.filename = path.file_stem().expect("Couldn't get filename").to_string_lossy().to_string(); + let path = Path::new(file_path); + page.file_name = path.file_stem().unwrap().to_string_lossy().to_string(); + page.slug = { if let Some(ref slug) = page.meta.slug { slug.trim().to_string() } else { - slugify(page.filename.clone()) + slugify(page.file_name.clone()) } }; - // 4. Find sections // Pages with custom urls exists outside of sections if let Some(ref u) = page.meta.url { page.url = u.trim().to_string(); } else { - // find out if we have sections - for section in path.parent().unwrap().components() { - page.sections.push(section.as_ref().to_string_lossy().to_string()); - } - - if !page.sections.is_empty() { - page.url = format!("{}/{}", page.sections.join("/"), page.slug); + page.components = find_content_components(&page.file_path); + if !page.components.is_empty() { + // If we have a folder with an asset, don't consider it as a component + if page.file_name == "index" { + page.components.pop(); + // also set parent_path to grandparent instead + page.parent_path = page.parent_path.parent().unwrap().to_path_buf(); + } + + // Don't add a trailing slash to sections + page.url = format!("{}/{}", page.components.join("/"), page.slug); } else { page.url = page.slug.clone(); } } + page.permalink = if config.base_url.ends_with('/') { format!("{}{}", config.base_url, page.url) } else { @@ -197,15 +185,14 @@ impl Page { /// Read and parse a .md file into a Page struct pub fn from_file>(path: P, config: &Config) -> Result { let path = path.as_ref(); + let content = read_file(path)?; + let mut page = Page::parse(path, &content, config)?; + page.assets = find_related_assets(&path.parent().unwrap()); - let mut content = String::new(); - File::open(path) - .chain_err(|| format!("Failed to open '{:?}'", path.display()))? - .read_to_string(&mut content)?; + if !page.assets.is_empty() && page.file_name != "index" { + bail!("Page `{}` has assets but is not named index.md", path.display()); + } - // Remove the content string from name - let mut page = Page::parse(&path.strip_prefix("content").unwrap().to_string_lossy(), &content, config)?; - page.assets = find_related_assets(&path); Ok(page) } @@ -223,7 +210,7 @@ impl Page { context.add("page", self); tera.render(&tpl_name, &context) - .chain_err(|| format!("Failed to render page '{}'", self.filename)) + .chain_err(|| format!("Failed to render page '{}'", self.file_name)) } } @@ -275,235 +262,25 @@ impl PartialOrd for Page { #[cfg(test)] mod tests { - use super::{Page}; - use config::Config; - - - #[test] - fn test_can_parse_a_valid_page() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -slug = "hello-world" -+++ -Hello world"#; - let res = Page::parse("post.md", content, &Config::default()); - assert!(res.is_ok()); - let page = res.unwrap(); - - assert_eq!(page.meta.title, "Hello".to_string()); - assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string()); - assert_eq!(page.raw_content, "Hello world".to_string()); - assert_eq!(page.content, "

Hello world

\n".to_string()); - } + use tempdir::TempDir; - #[test] - fn test_can_find_one_parent_directory() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -slug = "hello-world" -+++ -Hello world"#; - let res = Page::parse("posts/intro.md", content, &Config::default()); - assert!(res.is_ok()); - let page = res.unwrap(); - assert_eq!(page.sections, vec!["posts".to_string()]); - } + use std::fs::File; - #[test] - fn test_can_find_multiple_parent_directories() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -slug = "hello-world" -+++ -Hello world"#; - let res = Page::parse("posts/intro/start.md", content, &Config::default()); - assert!(res.is_ok()); - let page = res.unwrap(); - assert_eq!(page.sections, vec!["posts".to_string(), "intro".to_string()]); - } + use super::{find_related_assets}; #[test] - fn test_can_make_url_from_sections_and_slug() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -slug = "hello-world" -+++ -Hello world"#; - let mut conf = Config::default(); - conf.base_url = "http://hello.com/".to_string(); - let res = Page::parse("posts/intro/start.md", content, &conf); - assert!(res.is_ok()); - let page = res.unwrap(); - assert_eq!(page.url, "posts/intro/hello-world"); - assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world"); - } - - #[test] - fn test_can_make_permalink_with_non_trailing_slash_base_url() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -slug = "hello-world" -+++ -Hello world"#; - let mut conf = Config::default(); - conf.base_url = "http://hello.com".to_string(); - let res = Page::parse("posts/intro/start.md", content, &conf); - assert!(res.is_ok()); - let page = res.unwrap(); - assert_eq!(page.url, "posts/intro/hello-world"); - assert_eq!(page.permalink, format!("{}{}", conf.base_url, "/posts/intro/hello-world")); - } - - #[test] - fn test_can_make_url_from_slug_only() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -slug = "hello-world" -+++ -Hello world"#; - let res = Page::parse("start.md", content, &Config::default()); - assert!(res.is_ok()); - let page = res.unwrap(); - assert_eq!(page.url, "hello-world"); - assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "hello-world")); - } - - #[test] - fn test_errors_on_invalid_front_matter_format() { - let content = r#" -title = "Hello" -description = "hey there" -slug = "hello-world" -+++ -Hello world"#; - let res = Page::parse("start.md", content, &Config::default()); - assert!(res.is_err()); - } - - #[test] - fn test_can_make_slug_from_non_slug_filename() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -+++ -Hello world"#; - let res = Page::parse("file with space.md", content, &Config::default()); - assert!(res.is_ok()); - let page = res.unwrap(); - assert_eq!(page.slug, "file-with-space"); - assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space")); - } - - #[test] - fn test_trim_slug_if_needed() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -+++ -Hello world"#; - let res = Page::parse(" file with space.md", content, &Config::default()); - assert!(res.is_ok()); - let page = res.unwrap(); - assert_eq!(page.slug, "file-with-space"); - assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space")); - } - - #[test] - fn test_reading_analytics_short() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -+++ -Hello world"#; - let res = Page::parse("file with space.md", content, &Config::default()); - assert!(res.is_ok()); - let page = res.unwrap(); - let (word_count, reading_time) = page.get_reading_analytics(); - assert_eq!(word_count, 2); - assert_eq!(reading_time, 0); - } - - #[test] - fn test_reading_analytics_long() { - let mut content = r#" -+++ -title = "Hello" -description = "hey there" -+++ -Hello world"#.to_string(); - for _ in 0..1000 { - content.push_str(" Hello world"); - } - let res = Page::parse("hello.md", &content, &Config::default()); - assert!(res.is_ok()); - let page = res.unwrap(); - let (word_count, reading_time) = page.get_reading_analytics(); - assert_eq!(word_count, 2002); - assert_eq!(reading_time, 10); - } - - #[test] - fn test_automatic_summary_is_empty_string() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -+++ -Hello world"#.to_string(); - let res = Page::parse("hello.md", &content, &Config::default()); - assert!(res.is_ok()); - let page = res.unwrap(); - assert_eq!(page.summary, ""); - } - - #[test] - fn test_can_specify_summary() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -+++ -Hello world - -"#.to_string(); - let res = Page::parse("hello.md", &content, &Config::default()); - assert!(res.is_ok()); - let page = res.unwrap(); - assert_eq!(page.summary, "

Hello world

\n"); - } - - #[test] - fn test_can_auto_detect_when_highlighting_needed() { - let content = r#" -+++ -title = "Hello" -description = "hey there" -+++ -``` -Hey there -``` -"#.to_string(); - let mut config = Config::default(); - config.highlight_code = Some(true); - let res = Page::parse("hello.md", &content, &config); - assert!(res.is_ok()); - let page = res.unwrap(); - assert!(page.content.starts_with(", + /// The relative URL of the page + pub url: String, + /// The full URL for that page + pub permalink: String, + /// The front matter meta-data + pub meta: FrontMatter, + /// All direct pages of that section + pub pages: Vec, + /// All direct subsections + pub subsections: Vec
, +} + +impl Section { + pub fn new(file_path: &Path, meta: FrontMatter) -> Section { + Section { + file_path: file_path.to_path_buf(), + parent_path: file_path.parent().unwrap().to_path_buf(), + components: vec![], + url: "".to_string(), + permalink: "".to_string(), + meta: meta, + pages: vec![], + subsections: vec![], + } + } + + pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result
{ + let (meta, _) = split_content(file_path, content)?; + let mut section = Section::new(file_path, meta); + section.components = find_content_components(§ion.file_path); + section.url = section.components.join("/"); + section.permalink = section.components.join("/"); + + section.permalink = if config.base_url.ends_with('/') { + format!("{}{}", config.base_url, section.url) + } else { + format!("{}/{}", config.base_url, section.url) + }; + + Ok(section) + } + + /// Read and parse a .md file into a Page struct + pub fn from_file>(path: P, config: &Config) -> Result
{ + let path = path.as_ref(); + let content = read_file(path)?; + + Section::parse(path, &content, config) + } + + /// Renders the page using the default layout, unless specified in front-matter + pub fn render_html(&self, tera: &Tera, config: &Config) -> Result { + let tpl_name = match self.meta.template { + Some(ref l) => l.to_string(), + None => "section.html".to_string() + }; + + // TODO: create a helper to create context to ensure all contexts + // have the same names + let mut context = Context::new(); + context.add("config", config); + context.add("section", self); + + tera.render(&tpl_name, &context) + .chain_err(|| format!("Failed to render section '{}'", self.file_path.display())) + } +} + +impl ser::Serialize for Section { + fn serialize(&self, serializer: S) -> StdResult where S: ser::Serializer { + let mut state = serializer.serialize_struct("section", 6)?; + state.serialize_field("title", &self.meta.title)?; + state.serialize_field("description", &self.meta.description)?; + state.serialize_field("url", &format!("/{}", self.url))?; + state.serialize_field("permalink", &self.permalink)?; + state.serialize_field("pages", &self.pages)?; + state.serialize_field("subsections", &self.subsections)?; + state.end() + } +} diff --git a/src/site.rs b/src/site.rs index 3bd3e62..2e9bd17 100644 --- a/src/site.rs +++ b/src/site.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::iter::FromIterator; use std::fs::{remove_dir_all, copy, remove_file}; -use std::path::Path; +use std::path::{Path, PathBuf}; use glob::glob; use tera::{Tera, Context}; @@ -10,8 +10,9 @@ use walkdir::WalkDir; use errors::{Result, ResultExt}; use config::{Config, get_config}; -use page::Page; +use page::{Page}; use utils::{create_file, create_directory}; +use section::{Section}; lazy_static! { @@ -50,53 +51,98 @@ impl ListItem { } } - #[derive(Debug)] pub struct Site { - config: Config, - pages: HashMap, - sections: HashMap>, - templates: Tera, + pub base_path: PathBuf, + pub config: Config, + pub pages: HashMap, + pub sections: BTreeMap, + pub templates: Tera, live_reload: bool, + output_path: PathBuf, } impl Site { - pub fn new(livereload: bool) -> Result { - let mut tera = Tera::new("templates/**/*") - .chain_err(|| "Error parsing templates")?; + /// Parse a site at the given path. Defaults to the current dir + /// Passing in a path is only used in tests + pub fn new>(path: P) -> Result { + let path = path.as_ref(); + + let tpl_glob = format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*"); + let mut tera = Tera::new(&tpl_glob).chain_err(|| "Error parsing templates")?; tera.extend(&GUTENBERG_TERA)?; let mut site = Site { - config: get_config(), + base_path: path.to_path_buf(), + config: get_config(&path), pages: HashMap::new(), - sections: HashMap::new(), + sections: BTreeMap::new(), templates: tera, - live_reload: livereload, + live_reload: false, + output_path: PathBuf::from("public"), }; site.parse_site()?; Ok(site) } + /// What the function name says + pub fn enable_live_reload(&mut self) { + self.live_reload = true; + } + + /// Used by tests to change the output path to a tmp dir + #[doc(hidden)] + pub fn set_output_path>(&mut self, path: P) { + self.output_path = path.as_ref().to_path_buf(); + } + /// Reads all .md files in the `content` directory and create pages /// out of them fn parse_site(&mut self) -> Result<()> { - // First step: do all the articles and group article by sections - // hardcoded pattern so can't error - for entry in glob("content/**/*.md").unwrap().filter_map(|e| e.ok()) { - let page = Page::from_file(&entry.as_path(), &self.config)?; + let path = self.base_path.to_string_lossy().replace("\\", "/"); + let content_glob = format!("{}/{}", path, "content/**/*.md"); + + // parent_dir -> Section + let mut sections = BTreeMap::new(); - for section in &page.sections { - self.sections.entry(section.clone()).or_insert_with(|| vec![]).push(page.slug.clone()); + // Glob is giving us the result order so _index will show up first + // for each directory + for entry in glob(&content_glob).unwrap().filter_map(|e| e.ok()) { + let path = entry.as_path(); + + if path.file_name().unwrap() == "_index.md" { + let section = Section::from_file(&path, &self.config)?; + sections.insert(section.parent_path.clone(), section); + } else { + let page = Page::from_file(&path, &self.config)?; + if sections.contains_key(&page.parent_path) { + sections.get_mut(&page.parent_path).unwrap().pages.push(page.clone()); + } + self.pages.insert(page.file_path.clone(), page); } + } + + // Find out the direct subsections of each subsection if there are some + let mut grandparent_paths = HashMap::new(); + for section in sections.values() { + let grand_parent = section.parent_path.parent().unwrap().to_path_buf(); + grandparent_paths.entry(grand_parent).or_insert_with(|| vec![]).push(section.clone()); + } - self.pages.insert(page.slug.clone(), page); + for (parent_path, section) in sections.iter_mut() { + match grandparent_paths.get(parent_path) { + Some(paths) => section.subsections.extend(paths.clone()), + None => continue, + }; } + self.sections = sections; + Ok(()) } - // Inject live reload script tag if in live reload mode + /// Inject live reload script tag if in live reload mode fn inject_livereload(&self, html: String) -> String { if self.live_reload { return html.replace( @@ -108,11 +154,10 @@ impl Site { html } - /// Copy the content of the `static` folder into the `public` folder /// - /// TODO: only copy one file if possible because that would be a waster - /// to do re-copy the whole thing + /// TODO: only copy one file if possible because that would be a waste + /// to do re-copy the whole thing. Benchmark first to see if it's a big difference pub fn copy_static_directory(&self) -> Result<()> { let from = Path::new("static"); let target = Path::new("public"); @@ -160,7 +205,7 @@ impl Site { } pub fn build_pages(&self) -> Result<()> { - let public = Path::new("public"); + let public = self.output_path.clone(); if !public.exists() { create_directory(&public)?; } @@ -168,40 +213,39 @@ impl Site { let mut pages = vec![]; let mut category_pages: HashMap> = HashMap::new(); let mut tag_pages: HashMap> = HashMap::new(); + // First we render the pages themselves for page in self.pages.values() { // Copy the nesting of the content directory if we have sections for that page let mut current_path = public.to_path_buf(); - // This loop happens when the page doesn't have a set URL - for section in &page.sections { - current_path.push(section); + for component in page.url.split("/") { + current_path.push(component); if !current_path.exists() { create_directory(¤t_path)?; } } - // if we have a url already set, use that as base - if let Some(ref url) = page.meta.url { - current_path.push(url); - } - // Make sure the folder exists create_directory(¤t_path)?; + // Finally, create a index.html file there with the page rendered let output = page.render_html(&self.templates, &self.config)?; create_file(current_path.join("index.html"), &self.inject_livereload(output))?; + // Copy any asset we found previously into the same directory as the index.html for asset in &page.assets { let asset_path = asset.as_path(); copy(&asset_path, ¤t_path.join(asset_path.file_name().unwrap()))?; } + pages.push(page); if let Some(ref category) = page.meta.category { category_pages.entry(category.to_string()).or_insert_with(|| vec![]).push(page); } + if let Some(ref tags) = page.meta.tags { for tag in tags { tag_pages.entry(tag.to_string()).or_insert_with(|| vec![]).push(page); @@ -232,6 +276,7 @@ impl Site { if self.config.generate_rss.unwrap() { self.render_rss_feed()?; } + self.render_sections()?; self.copy_static_directory() } @@ -247,7 +292,7 @@ impl Site { ("tags", "tags.html", "tag.html", "tag") }; - let public = Path::new("public"); + let public = self.output_path.clone(); let mut output_path = public.to_path_buf(); output_path.push(name); create_directory(&output_path)?; @@ -292,8 +337,7 @@ impl Site { context.add("pages", &self.pages.values().collect::>()); let sitemap = self.templates.render("sitemap.xml", &context)?; - let public = Path::new("public"); - create_file(public.join("sitemap.xml"), &sitemap)?; + create_file(self.output_path.join("sitemap.xml"), &sitemap)?; Ok(()) } @@ -309,6 +353,7 @@ impl Site { if pages.is_empty() { return Ok(()); } + pages.sort_by(|a, b| a.partial_cmp(b).unwrap()); context.add("pages", &pages); context.add("last_build_date", &pages[0].meta.date); @@ -323,8 +368,27 @@ impl Site { let sitemap = self.templates.render("rss.xml", &context)?; - let public = Path::new("public"); - create_file(public.join("rss.xml"), &sitemap)?; + create_file(self.output_path.join("rss.xml"), &sitemap)?; + + Ok(()) + } + + fn render_sections(&self) -> Result<()> { + let public = self.output_path.clone(); + + for section in self.sections.values() { + let mut output_path = public.to_path_buf(); + for component in §ion.components { + output_path.push(component); + + if !output_path.exists() { + create_directory(&output_path)?; + } + } + + let output = section.render_html(&self.templates, &self.config)?; + create_file(output_path.join("index.html"), &self.inject_livereload(output))?; + } Ok(()) } diff --git a/src/utils.rs b/src/utils.rs index d028072..3068112 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -21,3 +21,52 @@ pub fn create_directory>(path: P) -> Result<()> { } Ok(()) } + + +/// Return the content of a file, with error handling added +pub fn read_file>(path: P) -> Result { + let path = path.as_ref(); + + let mut content = String::new(); + File::open(path) + .chain_err(|| format!("Failed to open '{:?}'", path.display()))? + .read_to_string(&mut content)?; + + Ok(content) +} + + +/// Takes a full path to a .md and returns only the components after the `content` directory +/// Will not return the filename as last component +pub fn find_content_components>(path: P) -> Vec { + let path = path.as_ref(); + let mut is_in_content = false; + let mut components = vec![]; + + for section in path.parent().unwrap().components() { + let component = section.as_ref().to_string_lossy(); + + if is_in_content { + components.push(component.to_string()); + continue; + } + + if component == "content" { + is_in_content = true; + } + } + + components +} + + +#[cfg(test)] +mod tests { + use super::{find_content_components}; + + #[test] + fn test_find_content_components() { + let res = find_content_components("/home/vincent/code/site/content/posts/tutorials/python.md"); + assert_eq!(res, ["posts".to_string(), "tutorials".to_string()]); + } +} diff --git a/test_site/config.toml b/test_site/config.toml new file mode 100644 index 0000000..ba0629c --- /dev/null +++ b/test_site/config.toml @@ -0,0 +1,7 @@ +title = "My site" +base_url = "https://replace-this-with-your-url.com" +highlight_code = true + + +[extra.author] +name = "Vincent Prouillet" diff --git a/test_site/content/posts/_index.md b/test_site/content/posts/_index.md new file mode 100644 index 0000000..93ed252 --- /dev/null +++ b/test_site/content/posts/_index.md @@ -0,0 +1,4 @@ ++++ +title = "Posts" +description = "" ++++ diff --git a/test_site/content/posts/fixed-slug.md b/test_site/content/posts/fixed-slug.md new file mode 100644 index 0000000..3eff97d --- /dev/null +++ b/test_site/content/posts/fixed-slug.md @@ -0,0 +1,7 @@ ++++ +title = "Fixed slug" +description = "" +slug = "something-else" ++++ + +A simple page with a slug defined diff --git a/test_site/content/posts/fixed-url.md b/test_site/content/posts/fixed-url.md new file mode 100644 index 0000000..e51171e --- /dev/null +++ b/test_site/content/posts/fixed-url.md @@ -0,0 +1,7 @@ ++++ +title = "Fixed URL" +description = "" +url = "a-fixed-url" ++++ + +A simple page with fixed url diff --git a/test_site/content/posts/no-section/simple.md b/test_site/content/posts/no-section/simple.md new file mode 100644 index 0000000..43bfcc6 --- /dev/null +++ b/test_site/content/posts/no-section/simple.md @@ -0,0 +1,6 @@ ++++ +title = "Simple" +description = "" ++++ + +A simple page diff --git a/test_site/content/posts/python.md b/test_site/content/posts/python.md new file mode 100644 index 0000000..ae5f25f --- /dev/null +++ b/test_site/content/posts/python.md @@ -0,0 +1,6 @@ ++++ +title = "Python in posts" +description = "" ++++ + +Same filename but different path diff --git a/test_site/content/posts/simple.md b/test_site/content/posts/simple.md new file mode 100644 index 0000000..43bfcc6 --- /dev/null +++ b/test_site/content/posts/simple.md @@ -0,0 +1,6 @@ ++++ +title = "Simple" +description = "" ++++ + +A simple page diff --git a/test_site/content/posts/tutorials/_index.md b/test_site/content/posts/tutorials/_index.md new file mode 100644 index 0000000..049aa01 --- /dev/null +++ b/test_site/content/posts/tutorials/_index.md @@ -0,0 +1,4 @@ ++++ +title = "Tutorials" +description = "" ++++ diff --git a/test_site/content/posts/tutorials/devops/_index.md b/test_site/content/posts/tutorials/devops/_index.md new file mode 100644 index 0000000..b4f4ab5 --- /dev/null +++ b/test_site/content/posts/tutorials/devops/_index.md @@ -0,0 +1,4 @@ ++++ +title = "DevOps" +description = "" ++++ diff --git a/test_site/content/posts/tutorials/devops/docker.md b/test_site/content/posts/tutorials/devops/docker.md new file mode 100644 index 0000000..eb0073a --- /dev/null +++ b/test_site/content/posts/tutorials/devops/docker.md @@ -0,0 +1,6 @@ ++++ +title = "Docker" +description = "" ++++ + +A simple page diff --git a/test_site/content/posts/tutorials/devops/nix.md b/test_site/content/posts/tutorials/devops/nix.md new file mode 100644 index 0000000..f12ccb6 --- /dev/null +++ b/test_site/content/posts/tutorials/devops/nix.md @@ -0,0 +1,6 @@ ++++ +title = "Nix" +description = "" ++++ + +A simple page diff --git a/test_site/content/posts/tutorials/programming/_index.md b/test_site/content/posts/tutorials/programming/_index.md new file mode 100644 index 0000000..d46aac2 --- /dev/null +++ b/test_site/content/posts/tutorials/programming/_index.md @@ -0,0 +1,4 @@ ++++ +title = "Programming" +description = "" ++++ diff --git a/test_site/content/posts/tutorials/programming/python.md b/test_site/content/posts/tutorials/programming/python.md new file mode 100644 index 0000000..8a0a68d --- /dev/null +++ b/test_site/content/posts/tutorials/programming/python.md @@ -0,0 +1,6 @@ ++++ +title = "Python tutorial" +description = "" ++++ + +A simple page diff --git a/test_site/content/posts/tutorials/programming/rust.md b/test_site/content/posts/tutorials/programming/rust.md new file mode 100644 index 0000000..b217323 --- /dev/null +++ b/test_site/content/posts/tutorials/programming/rust.md @@ -0,0 +1,6 @@ ++++ +title = "Rust" +description = "" ++++ + +A simple page diff --git a/test_site/content/posts/with-assets/index.md b/test_site/content/posts/with-assets/index.md new file mode 100644 index 0000000..2000dfd --- /dev/null +++ b/test_site/content/posts/with-assets/index.md @@ -0,0 +1,7 @@ ++++ +title = "With assets" +description = "hey there" +slug = "with-assets" ++++ + +Hello world diff --git a/test_site/content/posts/with-assets/with.js b/test_site/content/posts/with-assets/with.js new file mode 100644 index 0000000..e69de29 diff --git a/test_site/static/scripts/hello.js b/test_site/static/scripts/hello.js new file mode 100644 index 0000000..e69de29 diff --git a/test_site/static/site.css b/test_site/static/site.css new file mode 100644 index 0000000..b05faf8 --- /dev/null +++ b/test_site/static/site.css @@ -0,0 +1,3 @@ +body { + color: red; +} diff --git a/test_site/templates/categories.html b/test_site/templates/categories.html new file mode 100644 index 0000000..9ac110e --- /dev/null +++ b/test_site/templates/categories.html @@ -0,0 +1,3 @@ +{% for category in categories %} + {{ category.name }} {{ category.slug }} {{ category.count }} +{% endfor %} diff --git a/test_site/templates/category.html b/test_site/templates/category.html new file mode 100644 index 0000000..70079cb --- /dev/null +++ b/test_site/templates/category.html @@ -0,0 +1,8 @@ +Category: {{ category }} + + +{% for page in pages %} + +{% endfor %} diff --git a/test_site/templates/index.html b/test_site/templates/index.html new file mode 100644 index 0000000..417d614 --- /dev/null +++ b/test_site/templates/index.html @@ -0,0 +1,27 @@ + + + + + + + + + + + {{ config.title }} + + + +
+ {% block content %} +
+ {% for page in pages %} + + {% endfor %} +
+ {% endblock content %} +
+ + diff --git a/test_site/templates/page.html b/test_site/templates/page.html new file mode 100644 index 0000000..ff68f1f --- /dev/null +++ b/test_site/templates/page.html @@ -0,0 +1,5 @@ +{% extends "index.html" %} + +{% block content %} + {{ page.content | safe }} +{% endblock content %} diff --git a/test_site/templates/section.html b/test_site/templates/section.html new file mode 100644 index 0000000..cf4b1d1 --- /dev/null +++ b/test_site/templates/section.html @@ -0,0 +1,10 @@ +{% extends "index.html" %} + +{% block content %} + {% for page in section.pages %} + {{page.title}} + {% endfor %} + {% for subsection in section.subsections %} + {{subsection.title}} + {% endfor %} +{% endblock content %} diff --git a/test_site/templates/tag.html b/test_site/templates/tag.html new file mode 100644 index 0000000..62f6c1a --- /dev/null +++ b/test_site/templates/tag.html @@ -0,0 +1,7 @@ +Tag: {{ tag }} + +{% for page in pages %} + +{% endfor %} diff --git a/test_site/templates/tags.html b/test_site/templates/tags.html new file mode 100644 index 0000000..8a0b254 --- /dev/null +++ b/test_site/templates/tags.html @@ -0,0 +1,3 @@ +{% for tag in tags %} + {{ tag.name }} {{ tag.slug }} {{ tag.count }} +{% endfor %} diff --git a/tests/front_matter.rs b/tests/front_matter.rs new file mode 100644 index 0000000..14291c6 --- /dev/null +++ b/tests/front_matter.rs @@ -0,0 +1,197 @@ +extern crate gutenberg; +extern crate tera; + +use std::path::Path; + +use gutenberg::{FrontMatter, split_content}; +use tera::to_value; + + +#[test] +fn test_can_parse_a_valid_front_matter() { + let content = r#" +title = "Hello" +description = "hey there""#; + let res = FrontMatter::parse(content); + println!("{:?}", res); + assert!(res.is_ok()); + let res = res.unwrap(); + assert_eq!(res.title, "Hello".to_string()); + assert_eq!(res.description, "hey there".to_string()); +} + +#[test] +fn test_can_parse_tags() { + let content = r#" +title = "Hello" +description = "hey there" +slug = "hello-world" +tags = ["rust", "html"]"#; + let res = FrontMatter::parse(content); + assert!(res.is_ok()); + let res = res.unwrap(); + + assert_eq!(res.title, "Hello".to_string()); + assert_eq!(res.slug.unwrap(), "hello-world".to_string()); + assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]); +} + +#[test] +fn test_can_parse_extra_attributes_in_frontmatter() { + let content = r#" +title = "Hello" +description = "hey there" +slug = "hello-world" + +[extra] +language = "en" +authors = ["Bob", "Alice"]"#; + let res = FrontMatter::parse(content); + assert!(res.is_ok()); + let res = res.unwrap(); + + assert_eq!(res.title, "Hello".to_string()); + assert_eq!(res.slug.unwrap(), "hello-world".to_string()); + let extra = res.extra.unwrap(); + assert_eq!(extra.get("language").unwrap(), &to_value("en").unwrap()); + assert_eq!( + extra.get("authors").unwrap(), + &to_value(["Bob".to_string(), "Alice".to_string()]).unwrap() + ); +} + +#[test] +fn test_is_ok_with_url_instead_of_slug() { + let content = r#" +title = "Hello" +description = "hey there" +url = "hello-world""#; + let res = FrontMatter::parse(content); + assert!(res.is_ok()); + let res = res.unwrap(); + assert!(res.slug.is_none()); + assert_eq!(res.url.unwrap(), "hello-world".to_string()); +} + +#[test] +fn test_errors_with_empty_front_matter() { + let content = r#" "#; + let res = FrontMatter::parse(content); + assert!(res.is_err()); +} + +#[test] +fn test_errors_with_invalid_front_matter() { + let content = r#"title = 1\n"#; + let res = FrontMatter::parse(content); + assert!(res.is_err()); +} + +#[test] +fn test_errors_with_missing_required_value_front_matter() { + let content = r#"title = """#; + let res = FrontMatter::parse(content); + assert!(res.is_err()); +} + +#[test] +fn test_errors_on_non_string_tag() { + let content = r#" +title = "Hello" +description = "hey there" +slug = "hello-world" +tags = ["rust", 1]"#; + let res = FrontMatter::parse(content); + assert!(res.is_err()); +} + +#[test] +fn test_errors_on_present_but_empty_slug() { + let content = r#" +title = "Hello" +description = "hey there" +slug = """#; + let res = FrontMatter::parse(content); + assert!(res.is_err()); +} + +#[test] +fn test_errors_on_present_but_empty_url() { + let content = r#" +title = "Hello" +description = "hey there" +url = """#; + let res = FrontMatter::parse(content); + assert!(res.is_err()); +} + +#[test] +fn test_parse_date_yyyy_mm_dd() { + let content = r#" +title = "Hello" +description = "hey there" +date = "2016-10-10""#; + let res = FrontMatter::parse(content).unwrap(); + assert!(res.parse_date().is_some()); +} + +#[test] +fn test_parse_date_rfc3339() { + let content = r#" +title = "Hello" +description = "hey there" +date = "2002-10-02T15:00:00Z""#; + let res = FrontMatter::parse(content).unwrap(); + assert!(res.parse_date().is_some()); +} + +#[test] +fn test_cant_parse_random_date_format() { + let content = r#" +title = "Hello" +description = "hey there" +date = "2002/10/12""#; + let res = FrontMatter::parse(content).unwrap(); + assert!(res.parse_date().is_none()); +} + + +#[test] +fn test_can_split_content_valid() { + let content = r#" ++++ +title = "Title" +description = "hey there" +date = "2002/10/12" ++++ +Hello +"#; + let (front_matter, content) = split_content(Path::new(""), content).unwrap(); + assert_eq!(content, "Hello\n"); + assert_eq!(front_matter.title, "Title"); +} + +#[test] +fn test_can_split_content_with_only_frontmatter_valid() { + let content = r#" ++++ +title = "Title" +description = "hey there" +date = "2002/10/12" ++++"#; + let (front_matter, content) = split_content(Path::new(""), content).unwrap(); + assert_eq!(content, ""); + assert_eq!(front_matter.title, "Title"); +} + +#[test] +fn test_error_if_cannot_locate_frontmatter() { + let content = r#" ++++ +title = "Title" +description = "hey there" +date = "2002/10/12" +"#; + let res = split_content(Path::new(""), content); + assert!(res.is_err()); +} diff --git a/tests/page.rs b/tests/page.rs new file mode 100644 index 0000000..fc8dacc --- /dev/null +++ b/tests/page.rs @@ -0,0 +1,249 @@ +extern crate gutenberg; +extern crate tempdir; + +use tempdir::TempDir; + +use std::fs::File; +use std::path::Path; + +use gutenberg::{Page, Config}; + + +#[test] +fn test_can_parse_a_valid_page() { + let content = r#" ++++ +title = "Hello" +description = "hey there" +slug = "hello-world" ++++ +Hello world"#; + let res = Page::parse(Path::new("post.md"), content, &Config::default()); + assert!(res.is_ok()); + let page = res.unwrap(); + + assert_eq!(page.meta.title, "Hello".to_string()); + assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string()); + assert_eq!(page.raw_content, "Hello world".to_string()); + assert_eq!(page.content, "

Hello world

\n".to_string()); +} + +#[test] +fn test_can_find_one_parent_directory() { + let content = r#" ++++ +title = "Hello" +description = "hey there" +slug = "hello-world" ++++ +Hello world"#; + let res = Page::parse(Path::new("content/posts/intro.md"), content, &Config::default()); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.components, vec!["posts".to_string()]); +} + +#[test] +fn test_can_find_multiple_parent_directories() { + let content = r#" ++++ +title = "Hello" +description = "hey there" +slug = "hello-world" ++++ +Hello world"#; + let res = Page::parse(Path::new("content/posts/intro/start.md"), content, &Config::default()); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.components, vec!["posts".to_string(), "intro".to_string()]); +} + +#[test] +fn test_can_make_url_from_sections_and_slug() { + let content = r#" ++++ +title = "Hello" +description = "hey there" +slug = "hello-world" ++++ +Hello world"#; + let mut conf = Config::default(); + conf.base_url = "http://hello.com/".to_string(); + let res = Page::parse(Path::new("content/posts/intro/start.md"), content, &conf); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.url, "posts/intro/hello-world"); + assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world"); +} + +#[test] +fn test_can_make_permalink_with_non_trailing_slash_base_url() { + let content = r#" ++++ +title = "Hello" +description = "hey there" +slug = "hello-world" ++++ +Hello world"#; + let mut conf = Config::default(); + conf.base_url = "http://hello.com".to_string(); + let res = Page::parse(Path::new("content/posts/intro/hello-world.md"), content, &conf); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.url, "posts/intro/hello-world"); + assert_eq!(page.permalink, format!("{}{}", conf.base_url, "/posts/intro/hello-world")); +} + +#[test] +fn test_can_make_url_from_slug_only() { + let content = r#" ++++ +title = "Hello" +description = "hey there" +slug = "hello-world" ++++ +Hello world"#; + let res = Page::parse(Path::new("start.md"), content, &Config::default()); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.url, "hello-world"); + assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "hello-world")); +} + +#[test] +fn test_errors_on_invalid_front_matter_format() { + let content = r#" +title = "Hello" +description = "hey there" +slug = "hello-world" ++++ +Hello world"#; + let res = Page::parse(Path::new("start.md"), content, &Config::default()); + assert!(res.is_err()); +} + +#[test] +fn test_can_make_slug_from_non_slug_filename() { + let content = r#" ++++ +title = "Hello" +description = "hey there" ++++ +Hello world"#; + let res = Page::parse(Path::new("file with space.md"), content, &Config::default()); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.slug, "file-with-space"); + assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space")); +} + +#[test] +fn test_trim_slug_if_needed() { + let content = r#" ++++ +title = "Hello" +description = "hey there" ++++ +Hello world"#; + let res = Page::parse(Path::new(" file with space.md"), content, &Config::default()); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.slug, "file-with-space"); + assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space")); +} + +#[test] +fn test_reading_analytics_short() { + let content = r#" ++++ +title = "Hello" +description = "hey there" ++++ +Hello world"#; + let res = Page::parse(Path::new("hello.md"), content, &Config::default()); + assert!(res.is_ok()); + let page = res.unwrap(); + let (word_count, reading_time) = page.get_reading_analytics(); + assert_eq!(word_count, 2); + assert_eq!(reading_time, 0); +} + +#[test] +fn test_reading_analytics_long() { + let mut content = r#" ++++ +title = "Hello" +description = "hey there" ++++ +Hello world"#.to_string(); + for _ in 0..1000 { + content.push_str(" Hello world"); + } + let res = Page::parse(Path::new("hello.md"), &content, &Config::default()); + assert!(res.is_ok()); + let page = res.unwrap(); + let (word_count, reading_time) = page.get_reading_analytics(); + assert_eq!(word_count, 2002); + assert_eq!(reading_time, 10); +} + +#[test] +fn test_automatic_summary_is_empty_string() { + let content = r#" ++++ +title = "Hello" +description = "hey there" ++++ +Hello world"#.to_string(); + let res = Page::parse(Path::new("hello.md"), &content, &Config::default()); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.summary, ""); +} + +#[test] +fn test_can_specify_summary() { + let content = r#" ++++ +title = "Hello" +description = "hey there" ++++ +Hello world + +"#.to_string(); + let res = Page::parse(Path::new("hello.md"), &content, &Config::default()); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.summary, "

Hello world

\n"); +} + +#[test] +fn test_can_auto_detect_when_highlighting_needed() { + let content = r#" ++++ +title = "Hello" +description = "hey there" ++++ +``` +Hey there +``` +"#.to_string(); + let mut config = Config::default(); + config.highlight_code = Some(true); + let res = Page::parse(Path::new("hello.md"), &content, &config); + assert!(res.is_ok()); + let page = res.unwrap(); + assert!(page.content.starts_with(" { + { + let mut path = $root.clone(); + for component in $path.split("/") { + path = path.join(component); + } + Path::new(&path).exists() + } + } +} + +macro_rules! file_contains { + ($root: expr, $path: expr, $text: expr) => { + { + let mut path = $root.clone(); + for component in $path.split("/") { + path = path.join(component); + } + let mut file = File::open(&path).unwrap(); + let mut s = String::new(); + file.read_to_string(&mut s).unwrap(); + + s.contains($text) + } + } +} + +#[test] +fn test_can_build_site_without_live_reload() { + let mut path = env::current_dir().unwrap().to_path_buf(); + path.push("test_site"); + let mut site = Site::new(&path).unwrap(); + let tmp_dir = TempDir::new("example").expect("create temp dir"); + let public = &tmp_dir.path().join("public"); + site.set_output_path(&public); + site.build().unwrap(); + + assert!(Path::new(&public).exists()); + + assert!(file_exists!(public, "index.html")); + assert!(file_exists!(public, "sitemap.xml")); + assert!(file_exists!(public, "a-fixed-url/index.html")); + + assert!(file_exists!(public, "posts/python/index.html")); + assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html")); + assert!(file_exists!(public, "posts/with-assets/index.html")); + + // Sections + assert!(file_exists!(public, "posts/index.html")); + assert!(file_exists!(public, "posts/tutorials/index.html")); + assert!(file_exists!(public, "posts/tutorials/devops/index.html")); + assert!(file_exists!(public, "posts/tutorials/programming/index.html")); + // TODO: add assertion for syntax highlighting + + // No tags or categories + assert_eq!(file_exists!(public, "categories/index.html"), false); + assert_eq!(file_exists!(public, "tags/index.html"), false); + + // no live reload code + assert_eq!(file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10"), false); +} + +#[test] +fn test_can_build_site_with_live_reload() { + let mut path = env::current_dir().unwrap().to_path_buf(); + path.push("test_site"); + let mut site = Site::new(&path).unwrap(); + let tmp_dir = TempDir::new("example").expect("create temp dir"); + let public = &tmp_dir.path().join("public"); + site.set_output_path(&public); + site.enable_live_reload(); + site.build().unwrap(); + + assert!(Path::new(&public).exists()); + + assert!(file_exists!(public, "index.html")); + assert!(file_exists!(public, "sitemap.xml")); + assert!(file_exists!(public, "a-fixed-url/index.html")); + + assert!(file_exists!(public, "posts/python/index.html")); + assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html")); + assert!(file_exists!(public, "posts/with-assets/index.html")); + + // Sections + assert!(file_exists!(public, "posts/index.html")); + assert!(file_exists!(public, "posts/tutorials/index.html")); + assert!(file_exists!(public, "posts/tutorials/devops/index.html")); + assert!(file_exists!(public, "posts/tutorials/programming/index.html")); + // TODO: add assertion for syntax highlighting + + // No tags or categories + assert_eq!(file_exists!(public, "categories/index.html"), false); + assert_eq!(file_exists!(public, "tags/index.html"), false); + + // no live reload code + assert!(file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10")); +} + +#[test] +fn test_can_build_site_with_categories() { + let mut path = env::current_dir().unwrap().to_path_buf(); + path.push("test_site"); + let mut site = Site::new(&path).unwrap(); + let tmp_dir = TempDir::new("example").expect("create temp dir"); + site.set_output_path(&tmp_dir); + site.build().unwrap(); +} + +#[test] +fn test_can_build_site_with_tags() { + let mut path = env::current_dir().unwrap().to_path_buf(); + path.push("test_site"); + let mut site = Site::new(&path).unwrap(); + let tmp_dir = TempDir::new("example").expect("create temp dir"); + site.set_output_path(&tmp_dir); + site.build().unwrap(); +}