@@ -1,5 +1,15 @@ | |||
# Changelog | |||
## 0.0.6 (unreleased) | |||
- Fix missing serialized data for sections | |||
- Change the single item template context for categories/tags | |||
- Add a `get_url` and a `get_section` global Tera function | |||
- Add a config option to control how many articles to show in RSS feed | |||
- Move `insert_anchor_links` from config to being a section option and it can | |||
now be insert left or right | |||
## 0.0.5 (2017-05-15) | |||
- Fix XML templates overriding and reloading | |||
@@ -4,7 +4,7 @@ version = "0.0.5" | |||
dependencies = [ | |||
"base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"clap 2.24.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"clap 2.24.2 (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)", | |||
@@ -12,14 +12,14 @@ dependencies = [ | |||
"mount 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"notify 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"pulldown-cmark 0.0.14 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"serde 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"serde_derive 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"serde 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"serde_derive 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"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.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"tera 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"tera 0.10.6 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"term-painter 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"toml 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -45,20 +45,20 @@ 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.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (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" | |||
version = "0.3.2" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"backtrace-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"rustc-demangle 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -69,7 +69,7 @@ version = "0.1.10" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"gcc 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -155,7 +155,7 @@ dependencies = [ | |||
[[package]] | |||
name = "clap" | |||
version = "2.24.1" | |||
version = "2.24.2" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -163,9 +163,9 @@ dependencies = [ | |||
"bitflags 0.8.2 (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.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"unicode-segmentation 1.2.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.7.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -212,7 +212,7 @@ name = "error-chain" | |||
version = "0.10.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"backtrace 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"backtrace 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -220,7 +220,7 @@ name = "filetime" | |||
version = "0.1.10" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -228,7 +228,7 @@ name = "flate2" | |||
version = "0.2.19" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"miniz-sys 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -244,7 +244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -252,7 +252,7 @@ name = "fsevent-sys" | |||
version = "0.1.6" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -301,11 +301,11 @@ dependencies = [ | |||
[[package]] | |||
name = "idna" | |||
version = "0.1.1" | |||
version = "0.1.2" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"unicode-bidi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"unicode-bidi 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -314,7 +314,7 @@ name = "inotify" | |||
version = "0.3.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -322,7 +322,7 @@ name = "iovec" | |||
version = "0.1.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -374,7 +374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "libc" | |||
version = "0.2.22" | |||
version = "0.2.23" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
@@ -392,7 +392,7 @@ name = "memchr" | |||
version = "1.0.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -409,7 +409,7 @@ version = "0.1.9" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"gcc 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -418,7 +418,7 @@ version = "0.5.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"miow 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"net2 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -436,7 +436,7 @@ 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.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"net2 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -487,7 +487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"cfg-if 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)", | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -498,7 +498,7 @@ version = "0.5.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -512,7 +512,7 @@ dependencies = [ | |||
"fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"inotify 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"walkdir 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -555,7 +555,7 @@ name = "num_cpus" | |||
version = "1.4.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -565,7 +565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"onig_sys 61.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -575,7 +575,7 @@ version = "61.3.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"cmake 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -628,7 +628,7 @@ name = "rand" | |||
version = "0.3.15" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -638,19 +638,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "regex" | |||
version = "0.2.1" | |||
version = "0.2.2" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"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)", | |||
"regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
name = "regex-syntax" | |||
version = "0.4.0" | |||
version = "0.4.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
@@ -702,12 +702,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "serde" | |||
version = "1.0.5" | |||
version = "1.0.7" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "serde_derive" | |||
version = "1.0.5" | |||
version = "1.0.7" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -732,7 +732,7 @@ dependencies = [ | |||
"dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"serde 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"serde 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -804,7 +804,7 @@ dependencies = [ | |||
"lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"onig 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"plist 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"rustc-serialize 0.3.24 (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)", | |||
@@ -820,7 +820,7 @@ dependencies = [ | |||
[[package]] | |||
name = "tera" | |||
version = "0.10.5" | |||
version = "0.10.6" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -829,8 +829,8 @@ dependencies = [ | |||
"humansize 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"lazy_static 0.2.8 (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)", | |||
"serde 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"serde 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"serde_json 1.0.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.4.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -859,7 +859,7 @@ version = "0.3.0" | |||
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.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -869,7 +869,7 @@ version = "3.1.0" | |||
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.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -887,7 +887,7 @@ version = "0.1.37" | |||
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.22 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"redox_syscall 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -897,7 +897,7 @@ name = "toml" | |||
version = "0.4.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"serde 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"serde 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
[[package]] | |||
@@ -928,7 +928,7 @@ dependencies = [ | |||
[[package]] | |||
name = "unicode-bidi" | |||
version = "0.2.5" | |||
version = "0.3.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -941,7 +941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "unicode-segmentation" | |||
version = "1.1.0" | |||
version = "1.2.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
@@ -980,7 +980,7 @@ name = "url" | |||
version = "1.4.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"idna 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"idna 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -991,7 +991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "vec_map" | |||
version = "0.7.0" | |||
version = "0.8.0" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
@@ -1070,7 +1070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
"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 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "72f9b4182546f4b04ebc4ab7f84948953a118bd6021a1b6a6c909e3e94f6be76" | |||
"checksum backtrace-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d192fd129132fbc97497c1f2ec2c2c5174e376b95f535199ef4fe0a293d33842" | |||
"checksum base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557" | |||
"checksum bincode 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "55eb0b7fd108527b0c77860f75eca70214e11a8b4c6ef05148c54c05a25d48ad" | |||
@@ -1084,7 +1084,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.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d9123be86fd2a8f627836c235ecdf331fdd067ecf7ac05aa1a68fbcf2429f056" | |||
"checksum clap 2.24.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7541069be0b8aec41030802abe8b5cdef0490070afaa55418adea93b1e431e0" | |||
"checksum clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b8f69e518f967224e628896b54e41ff6acfb4dcfefc5076325c36525dac900f" | |||
"checksum cmake 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)" = "92278eb79412c8f75cfc89e707a1bb3a6490b68f7f2e78d15c774f30fe701122" | |||
"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" | |||
@@ -1102,7 +1102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
"checksum httparse 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77f756bed9ee3a83ce98774f4155b42a31b787029013f3a7d83eca714e500e21" | |||
"checksum humansize 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "92d211e6e70b05749dce515b47684f29a3c8c38bbbb21c50b30aff9eca1b0bd3" | |||
"checksum hyper 0.10.10 (registry+https://github.com/rust-lang/crates.io-index)" = "36e108e0b1fa2d17491cbaac4bc460dc0956029d10ccf83c913dd0e5db3e7f07" | |||
"checksum idna 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6ac85ec3f80c8e4e99d9325521337e14ec7555c458a14e377d189659a427f375" | |||
"checksum idna 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2233d4940b1f19f0418c158509cd7396b8d70a5db5705ce410914dc8fa603b37" | |||
"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" | |||
@@ -1111,7 +1111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" | |||
"checksum lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3b37545ab726dd833ec6420aaba8231c5b320814b9029ad585555d2a03e94fbf" | |||
"checksum lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce12306c4739d86ee97c23139f3a34ddf0387bbf181bc7929d287025a8c3ef6b" | |||
"checksum libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)" = "babb8281da88cba992fa1f4ddec7d63ed96280a1a53ec9b919fd37b53d71e502" | |||
"checksum libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)" = "e7eb6b826bfc1fdea7935d46556250d1799b7fe2d9f7951071f4291710665e3e" | |||
"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" | |||
@@ -1141,8 +1141,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
"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.17 (registry+https://github.com/rust-lang/crates.io-index)" = "29dbdfd4b9df8ab31dec47c6087b7b13cbf4a776f335e4de8efba8288dda075b" | |||
"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 regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1731164734096285ec2a5ec7fea5248ae2f5485b3feeb0115af4fda2183b2d1b" | |||
"checksum regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad890a5eef7953f55427c50575c680c42841653abd2b028b68cd223d157f62db" | |||
"checksum rustc-demangle 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3058a43ada2c2d0b92b3ae38007a2d0fa5e9db971be260e0171408a4ff471c95" | |||
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" | |||
"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" | |||
@@ -1151,8 +1151,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
"checksum sequence_trie 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c915714ca833b1d4d6b8f6a9d72a3ff632fe45b40a8d184ef79c81bec6327eed" | |||
"checksum serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" | |||
"checksum serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)" = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" | |||
"checksum serde 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e101024c846392aadc80d5d452f2ff011f9bff1a0441151f8575e8a23488ef95" | |||
"checksum serde_derive 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "7c85839e9a00a5b0c7bddb1e44b8c3907c7aba5fe234c7ec5ccef1c188eb41d9" | |||
"checksum serde 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c0c3d79316a6051231925504f6ef893d45088e8823c77a8331a3dcf427ee9087" | |||
"checksum serde_derive 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0019cd5b9f0529a1a0e145a912e9a2d60c325c58f7f260fc36c71976e9d76aee" | |||
"checksum serde_derive_internals 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "021c338d22c7e30f957a6ab7e388cb6098499dda9fd4ba1661ee074ca7a180d1" | |||
"checksum serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "48b04779552e92037212c3615370f6bd57a40ebba7f20e554ff9f55e41a69a7b" | |||
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" | |||
@@ -1165,7 +1165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" | |||
"checksum syntect 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24204b1f4bdd49f84e5f4b219d0bf1dc45ac2fd7fc46320ab6627b537d6d4b69" | |||
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" | |||
"checksum tera 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)" = "5ce5ea7e2239a92d2bb662b8a337d8a3c45b9e6d630d113b0ca18dd6e64fb05d" | |||
"checksum tera 0.10.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c931ade2857155d5e55115375d4d2b8a441536e2b9e44643a8b67e235e09030" | |||
"checksum term 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d168af3930b369cfe245132550579d47dfd873d69470755a19c2c6568dbbd989" | |||
"checksum term-painter 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ab900bf2f05175932b13d4fc12f8ff09ef777715b04998791ab2c930841e496b" | |||
"checksum term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2b6b55df3198cc93372e85dd2ed817f0e38ce8cc0f22eb32391bfad9c4bf209" | |||
@@ -1177,9 +1177,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
"checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" | |||
"checksum typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" | |||
"checksum unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a5906ca2b98c799f4b1ab4557b76367ebd6ae5ef14930ec841c74aed5f3764" | |||
"checksum unicode-bidi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d3a078ebdd62c0e71a709c3d53d2af693fe09fe93fbff8344aebe289b78f9032" | |||
"checksum unicode-bidi 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c44d4e7ce691e2538b886bf33669fd6da1653a12d741b9390f351955c0949c03" | |||
"checksum unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e28fa37426fceeb5cf8f41ee273faa7c82c47dc8fba5853402841e665fcd86ff" | |||
"checksum unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18127285758f0e2c6cf325bb3f3d138a12fee27de4f23e146cd6a179f26c2cf3" | |||
"checksum unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a8083c594e02b8ae1654ae26f0ade5158b119bd88ad0e8227a5d8fcd72407946" | |||
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" | |||
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" | |||
"checksum unidecode 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d2adb95ee07cd579ed18131f2d9e7a17c25a4b76022935c7f2460d2bfae89fd2" | |||
@@ -1187,7 +1187,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.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8cdc8b93bd0198ed872357fb2e667f7125646b1762f16d60b2c96350d361897" | |||
"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" | |||
"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" | |||
@@ -169,10 +169,13 @@ to link to. The path to the file starts from the `content` directory. | |||
For example, linking to a file located at `content/pages/about.md` would be `[my link](./pages/about.md)`. | |||
### Anchors | |||
Headers get an automatic id from their content in order to be able to add deep links. By default no links are actually created but | |||
the `insert_anchor_links` option in `config.toml` can be set to `true` to link tags. The default template is very ugly and will need | |||
CSS tweaks in your projet to look decent. The default template can also be easily overwritten by creating a `anchor-link.html` file in | |||
the `templates` directory. | |||
Headers get an automatic id from their content in order to be able to add deep links. | |||
You can also choose, at the section level, whether to automatically insert an anchor link next to it. It is turned off by default | |||
but can be turned on by setting `insert_anchor = "left"` or `insert_anchor = "right"` in the `_index.md` file. `left` will insert | |||
the anchor link before the title text and right will insert it after. | |||
The default template is very basic and will need CSS tweaks in your projet to look decent. | |||
It can easily be overwritten by creating a `anchor-link.html` file in the `templates` directory. | |||
### Shortcodes | |||
Gutenberg uses markdown for content but sometimes you want to insert some HTML, for example for a YouTube video. | |||
@@ -16,8 +16,8 @@ base_url = "https://example.com" | |||
"#; | |||
pub fn create_new_project<P: AsRef<Path>>(name: P) -> Result<()> { | |||
let path = name.as_ref(); | |||
pub fn create_new_project(name: &str) -> Result<()> { | |||
let path = Path::new(name); | |||
// Better error message than the rust default | |||
if path.exists() && path.is_dir() { | |||
@@ -26,7 +26,7 @@ pub fn create_new_project<P: AsRef<Path>>(name: P) -> Result<()> { | |||
// main folder | |||
create_dir(path)?; | |||
create_file(path.join("config.toml"), CONFIG.trim_left())?; | |||
create_file(&path.join("config.toml"), CONFIG.trim_left())?; | |||
// content folder | |||
create_dir(path.join("content"))?; | |||
@@ -23,6 +23,10 @@ enum ChangeKind { | |||
StaticFiles, | |||
} | |||
// Uglified using uglifyjs | |||
// Also, commenting out the lines 330-340 (containing `e instanceof ProtocolError`) was needed | |||
// as it seems their build didn't work well and didn't include ProtocolError so it would error on | |||
// errors | |||
const LIVE_RELOAD: &'static str = include_str!("livereload.js"); | |||
@@ -49,8 +53,6 @@ fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &st | |||
} | |||
} | |||
// Most of it taken from mdbook | |||
pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> { | |||
let start = Instant::now(); | |||
let mut site = Site::new(env::current_dir().unwrap(), config_file)?; | |||
@@ -88,7 +90,8 @@ pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> { | |||
mount.mount("/livereload.js", livereload_handler); | |||
// Starts with a _ to not trigger the unused lint | |||
// we need to assign to a variable otherwise it will block | |||
let _iron = Iron::new(mount).http(address.as_str()).unwrap(); | |||
let _iron = Iron::new(mount).http(address.as_str()) | |||
.chain_err(|| "Can't start the webserver")?; | |||
// The websocket for livereload | |||
let ws_server = WebSocket::new(|output: Sender| { | |||
@@ -119,8 +122,6 @@ pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> { | |||
use notify::DebouncedEvent::*; | |||
loop { | |||
// See https://github.com/spf13/hugo/blob/master/commands/hugo.go | |||
// for a more complete version of that | |||
match rx.recv() { | |||
Ok(event) => { | |||
match event { | |||
@@ -162,7 +163,6 @@ pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> { | |||
} | |||
} | |||
/// Returns whether the path we received corresponds to a temp file created | |||
/// by an editor or the OS | |||
fn is_temp_file(path: &Path) -> bool { | |||
@@ -191,7 +191,6 @@ fn is_temp_file(path: &Path) -> bool { | |||
} | |||
} | |||
/// Detect what changed from the given path so we have an idea what needs | |||
/// to be reloaded | |||
fn detect_change_kind(pwd: &str, path: &Path) -> (ChangeKind, String) { | |||
@@ -218,8 +217,8 @@ mod tests { | |||
use super::{is_temp_file, detect_change_kind, ChangeKind}; | |||
#[test] | |||
fn test_can_recognize_temp_files() { | |||
let testcases = vec![ | |||
fn can_recognize_temp_files() { | |||
let test_cases = vec![ | |||
Path::new("hello.swp"), | |||
Path::new("hello.swx"), | |||
Path::new(".DS_STORE"), | |||
@@ -231,14 +230,14 @@ mod tests { | |||
Path::new("#hello.html"), | |||
]; | |||
for t in testcases { | |||
for t in test_cases { | |||
assert!(is_temp_file(&t)); | |||
} | |||
} | |||
#[test] | |||
fn test_can_detect_kind_of_changes() { | |||
let testcases = vec![ | |||
fn can_detect_kind_of_changes() { | |||
let test_cases = vec![ | |||
( | |||
(ChangeKind::Templates, "/templates/hello.html".to_string()), | |||
"/home/vincent/site", Path::new("/home/vincent/site/templates/hello.html") | |||
@@ -253,7 +252,7 @@ mod tests { | |||
), | |||
]; | |||
for (expected, pwd, path) in testcases { | |||
for (expected, pwd, path) in test_cases { | |||
assert_eq!(expected, detect_change_kind(&pwd, &path)); | |||
} | |||
} | |||
@@ -36,13 +36,17 @@ pub fn notify_site_size(site: &Site) { | |||
/// Display a warning in the console if there are ignored pages in the site | |||
pub fn warn_about_ignored_pages(site: &Site) { | |||
let ignored_pages = site.get_ignored_pages(); | |||
let ignored_pages: Vec<_> = site.sections | |||
.values() | |||
.flat_map(|s| s.ignored_pages.iter().map(|p| p.file.path.clone())) | |||
.collect(); | |||
if !ignored_pages.is_empty() { | |||
warn(&format!( | |||
"{} page(s) ignored (missing date or order in a sorted section):", | |||
ignored_pages.len() | |||
)); | |||
for path in site.get_ignored_pages() { | |||
for path in ignored_pages { | |||
warn(&format!("- {}", path.display())); | |||
} | |||
} | |||
@@ -62,9 +66,11 @@ pub fn report_elapsed_time(instant: Instant) { | |||
/// Display an error message and the actual error(s) | |||
pub fn unravel_errors(message: &str, error: &Error) { | |||
if !message.is_empty() { | |||
self::error(message); | |||
self::error(&format!("Error: {}", error)); | |||
for e in error.iter().skip(1) { | |||
self::error(&format!("Reason: {}", e)); | |||
} | |||
} | |||
self::error(&format!("Error: {}", error)); | |||
for e in error.iter().skip(1) { | |||
self::error(&format!("Reason: {}", e)); | |||
} | |||
} |
@@ -70,7 +70,7 @@ fn main() { | |||
match cmd::serve(interface, port, config_file) { | |||
Ok(()) => (), | |||
Err(e) => { | |||
console::unravel_errors("Failed to build the site", &e); | |||
console::unravel_errors("", &e); | |||
::std::process::exit(1); | |||
}, | |||
}; | |||
@@ -1,8 +1,21 @@ | |||
use std::path::Path; | |||
use gutenberg::{Site, SectionFrontMatter, PageFrontMatter}; | |||
use gutenberg::{Site, Page, Section, SectionFrontMatter, PageFrontMatter}; | |||
use gutenberg::errors::Result; | |||
/// Finds the section that contains the page given if there is one | |||
pub fn find_parent_section<'a>(site: &'a Site, page: &Page) -> Option<&'a Section> { | |||
for section in site.sections.values() { | |||
if section.is_child_page(&page.file.path) { | |||
return Some(section) | |||
} | |||
} | |||
None | |||
} | |||
#[derive(Debug, Clone, Copy, PartialEq)] | |||
enum PageChangesNeeded { | |||
/// Editing `tags` | |||
@@ -22,7 +35,7 @@ enum SectionChangesNeeded { | |||
Sort, | |||
/// Editing `title`, `description`, `extra`, `template` or setting `render` to true | |||
Render, | |||
/// Editing `paginate_by` or `paginate_path` | |||
/// Editing `paginate_by`, `paginate_path` or `insert_anchor` | |||
RenderWithPages, | |||
/// Setting `render` to false | |||
Delete, | |||
@@ -43,7 +56,9 @@ fn find_section_front_matter_changes(current: &SectionFrontMatter, other: &Secti | |||
return changes_needed; | |||
} | |||
if current.paginate_by != other.paginate_by || current.paginate_path != other.paginate_path { | |||
if current.paginate_by != other.paginate_by | |||
|| current.paginate_path != other.paginate_path | |||
|| current.insert_anchor != other.insert_anchor { | |||
changes_needed.push(SectionChangesNeeded::RenderWithPages); | |||
// Nothing else we can do | |||
return changes_needed; | |||
@@ -85,7 +100,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { | |||
// A section was deleted, many things can be impacted: | |||
// - the pages of the section are becoming orphans | |||
// - any page that was referencing the section (index, etc) | |||
let relative_path = site.sections[path].relative_path.clone(); | |||
let relative_path = site.sections[path].file.relative.clone(); | |||
// Remove the link to it and the section itself from the Site | |||
site.permalinks.remove(&relative_path); | |||
site.sections.remove(path); | |||
@@ -94,18 +109,20 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { | |||
// A page was deleted, many things can be impacted: | |||
// - the section the page is in | |||
// - any page that was referencing the section (index, etc) | |||
let relative_path = site.pages[path].relative_path.clone(); | |||
let relative_path = site.pages[path].file.relative.clone(); | |||
site.permalinks.remove(&relative_path); | |||
if let Some(p) = site.pages.remove(path) { | |||
if p.meta.has_tags() || p.meta.category.is_some() { | |||
site.populate_tags_and_categories(); | |||
} | |||
if site.find_parent_section(&p).is_some() { | |||
if find_parent_section(site, &p).is_some() { | |||
site.populate_sections(); | |||
} | |||
}; | |||
} | |||
// Ensure we have our fn updated so it doesn't contain the permalinks deleted | |||
site.register_get_url_fn(); | |||
// Deletion is something that doesn't happen all the time so we | |||
// don't need to optimise it too much | |||
return site.build(); | |||
@@ -140,6 +157,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { | |||
return Ok(()); | |||
}, | |||
None => { | |||
site.register_get_url_fn(); | |||
// New section, only render that one | |||
site.populate_sections(); | |||
return site.render_section(&site.sections[path], true); | |||
@@ -150,6 +168,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { | |||
// A page was edited | |||
match site.add_page(path, true)? { | |||
Some(prev) => { | |||
site.register_get_url_fn(); | |||
// Updating a page | |||
let current = site.pages[path].clone(); | |||
// Front matter didn't change, only content did | |||
@@ -171,8 +190,8 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { | |||
site.render_categories()?; | |||
}, | |||
PageChangesNeeded::Sort => { | |||
let section_path = match site.find_parent_section(&site.pages[path]) { | |||
Some(s) => s.file_path.clone(), | |||
let section_path = match find_parent_section(site, &site.pages[path]) { | |||
Some(s) => s.file.path.clone(), | |||
None => continue // Do nothing if it's an orphan page | |||
}; | |||
site.populate_sections(); | |||
@@ -188,6 +207,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { | |||
}, | |||
None => { | |||
site.register_get_url_fn(); | |||
// It's a new page! | |||
site.populate_sections(); | |||
site.populate_tags_and_categories(); | |||
@@ -6,7 +6,7 @@ use std::collections::HashMap; | |||
use toml::{Value as Toml, self}; | |||
use errors::{Result, ResultExt}; | |||
use markdown::SETUP; | |||
use rendering::highlighting::THEME_SET; | |||
#[derive(Debug, PartialEq, Serialize, Deserialize)] | |||
@@ -24,8 +24,10 @@ pub struct Config { | |||
pub description: Option<String>, | |||
/// The language used in the site. Defaults to "en" | |||
pub language_code: Option<String>, | |||
/// Whether to generate RSS, defaults to false | |||
/// Whether to generate RSS. Defaults to false | |||
pub generate_rss: Option<bool>, | |||
/// The number of articles to include in the RSS feed. Defaults to unlimited | |||
pub rss_limit: Option<usize>, | |||
/// Whether to generate tags and individual tag pages if some pages have them. Defaults to true | |||
pub generate_tags_pages: Option<bool>, | |||
/// Whether to generate categories and individual tag categories if some pages have them. Defaults to true | |||
@@ -59,13 +61,14 @@ impl Config { | |||
set_default!(config.language_code, "en".to_string()); | |||
set_default!(config.highlight_code, false); | |||
set_default!(config.generate_rss, false); | |||
set_default!(config.rss_limit, <usize>::max_value()); | |||
set_default!(config.generate_tags_pages, false); | |||
set_default!(config.generate_categories_pages, false); | |||
set_default!(config.insert_anchor_links, false); | |||
match config.highlight_theme { | |||
Some(ref t) => { | |||
if !SETUP.theme_set.themes.contains_key(t) { | |||
if !THEME_SET.themes.contains_key(t) { | |||
bail!("Theme {} not available", t) | |||
} | |||
}, | |||
@@ -87,7 +90,9 @@ impl Config { | |||
/// Makes a url, taking into account that the base url might have a trailing slash | |||
pub fn make_permalink(&self, path: &str) -> String { | |||
if self.base_url.ends_with('/') { | |||
if self.base_url.ends_with('/') && path.starts_with('/') { | |||
format!("{}{}", self.base_url, &path[1..]) | |||
} else if self.base_url.ends_with('/') { | |||
format!("{}{}", self.base_url, path) | |||
} else { | |||
format!("{}/{}", self.base_url, path) | |||
@@ -95,8 +100,9 @@ impl Config { | |||
} | |||
} | |||
/// Exists only for testing purposes | |||
#[doc(hidden)] | |||
impl Default for Config { | |||
/// Exists for testing purposes | |||
fn default() -> Config { | |||
Config { | |||
title: "".to_string(), | |||
@@ -106,6 +112,7 @@ impl Default for Config { | |||
description: None, | |||
language_code: Some("en".to_string()), | |||
generate_rss: Some(false), | |||
rss_limit: Some(10000), | |||
generate_tags_pages: Some(true), | |||
generate_categories_pages: Some(true), | |||
insert_anchor_links: Some(false), | |||
@@ -181,4 +188,17 @@ hello = "world" | |||
assert_eq!(config.unwrap().extra.unwrap().get("hello").unwrap().as_str().unwrap(), "world"); | |||
} | |||
#[test] | |||
fn can_make_url_with_non_trailing_slash_base_url() { | |||
let mut config = Config::default(); | |||
config.base_url = "http://vincent.is".to_string(); | |||
assert_eq!(config.make_permalink("hello"), "http://vincent.is/hello"); | |||
} | |||
#[test] | |||
fn can_make_url_with_trailing_slash_path() { | |||
let mut config = Config::default(); | |||
config.base_url = "http://vincent.is/".to_string(); | |||
assert_eq!(config.make_permalink("/hello"), "http://vincent.is/hello"); | |||
} | |||
} |
@@ -0,0 +1,116 @@ | |||
use std::path::{Path, PathBuf}; | |||
/// Takes a full path to a file and returns only the components after the first `content` directory | |||
/// Will not return the filename as last component | |||
pub fn find_content_components<P: AsRef<Path>>(path: P) -> Vec<String> { | |||
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 | |||
} | |||
/// Struct that contains all the information about the actual file | |||
#[derive(Debug, Clone, PartialEq)] | |||
pub struct FileInfo { | |||
/// The full path to the .md file | |||
pub path: PathBuf, | |||
/// The name of the .md file without the extension, always `_index` for sections | |||
pub name: String, | |||
/// The .md path, starting from the content directory, with `/` slashes | |||
pub relative: String, | |||
/// Path of the directory containing the .md file | |||
pub parent: PathBuf, | |||
/// Path of the grand parent directory for that file. Only used in sections to find subsections. | |||
pub grand_parent: Option<PathBuf>, | |||
/// The folder names to this section file, starting from the `content` directory | |||
/// For example a file at content/kb/solutions/blabla.md will have 2 components: | |||
/// `kb` and `solutions` | |||
pub components: Vec<String>, | |||
} | |||
impl FileInfo { | |||
pub fn new_page(path: &Path) -> FileInfo { | |||
let file_path = path.to_path_buf(); | |||
let mut parent = file_path.parent().unwrap().to_path_buf(); | |||
let name = path.file_stem().unwrap().to_string_lossy().to_string(); | |||
let mut components = find_content_components(&file_path); | |||
let relative = format!("{}/{}.md", components.join("/"), name); | |||
// If we have a folder with an asset, don't consider it as a component | |||
if !components.is_empty() && name == "index" { | |||
components.pop(); | |||
// also set parent_path to grandparent instead | |||
parent = parent.parent().unwrap().to_path_buf(); | |||
} | |||
FileInfo { | |||
path: file_path, | |||
// We don't care about grand parent for pages | |||
grand_parent: None, | |||
parent, | |||
name, | |||
components, | |||
relative, | |||
} | |||
} | |||
pub fn new_section(path: &Path) -> FileInfo { | |||
let parent = path.parent().unwrap().to_path_buf(); | |||
let components = find_content_components(path); | |||
let relative = if components.is_empty() { | |||
// the index one | |||
"_index.md".to_string() | |||
} else { | |||
format!("{}/_index.md", components.join("/")) | |||
}; | |||
let grand_parent = parent.parent().map(|p| p.to_path_buf()); | |||
FileInfo { | |||
path: path.to_path_buf(), | |||
parent, | |||
grand_parent, | |||
name: "_index".to_string(), | |||
components, | |||
relative, | |||
} | |||
} | |||
} | |||
#[doc(hidden)] | |||
impl Default for FileInfo { | |||
fn default() -> FileInfo { | |||
FileInfo { | |||
path: PathBuf::new(), | |||
parent: PathBuf::new(), | |||
grand_parent: None, | |||
name: String::new(), | |||
components: vec![], | |||
relative: String::new(), | |||
} | |||
} | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use super::find_content_components; | |||
#[test] | |||
fn can_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()]); | |||
} | |||
} |
@@ -1,14 +1,14 @@ | |||
// TODO: move section/page and maybe pagination in this mod | |||
// Not sure where pagination stands if I add a render mod | |||
mod page; | |||
mod pagination; | |||
mod section; | |||
mod sorting; | |||
mod utils; | |||
mod file_info; | |||
mod taxonomies; | |||
pub use self::page::{Page}; | |||
pub use self::section::{Section}; | |||
pub use self::pagination::{Paginator, Pager}; | |||
pub use self::sorting::{SortBy, sort_pages, populate_previous_and_next_pages}; | |||
pub use self::taxonomies::{Taxonomy, TaxonomyItem}; | |||
@@ -4,43 +4,32 @@ use std::path::{Path, PathBuf}; | |||
use std::result::Result as StdResult; | |||
use tera::{Tera, Context}; | |||
use tera::{Tera, Context as TeraContext}; | |||
use serde::ser::{SerializeStruct, self}; | |||
use slug::slugify; | |||
use errors::{Result, ResultExt}; | |||
use config::Config; | |||
use front_matter::{PageFrontMatter, split_page_content}; | |||
use markdown::markdown_to_html; | |||
use utils::{read_file, find_content_components}; | |||
use front_matter::{PageFrontMatter, InsertAnchor, split_page_content}; | |||
use rendering::markdown::markdown_to_html; | |||
use rendering::context::Context; | |||
use fs::{read_file}; | |||
use content::utils::{find_related_assets, get_reading_analytics}; | |||
use content::file_info::FileInfo; | |||
#[derive(Clone, Debug, PartialEq)] | |||
pub struct Page { | |||
/// All info about the actual file | |||
pub file: FileInfo, | |||
/// The front matter meta-data | |||
pub meta: PageFrontMatter, | |||
/// The .md path | |||
pub file_path: PathBuf, | |||
/// The .md path, starting from the content directory, with / slashes | |||
pub relative_path: String, | |||
/// 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 | |||
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` | |||
pub components: Vec<String>, | |||
/// The actual content of the page, in markdown | |||
pub raw_content: String, | |||
/// All the non-md files we found next to the .md file | |||
pub assets: Vec<PathBuf>, | |||
/// The HTML rendered of the page | |||
pub content: String, | |||
/// The slug of that page. | |||
/// First tries to find the slug in the meta and defaults to filename otherwise | |||
pub slug: String, | |||
@@ -52,7 +41,6 @@ pub struct Page { | |||
/// When <!-- more --> is found in the text, will take the content up to that part | |||
/// as summary | |||
pub summary: Option<String>, | |||
/// The previous page, by whatever sorting is used for the index/section | |||
pub previous: Option<Box<Page>>, | |||
/// The next page, by whatever sorting is used for the index/section | |||
@@ -61,14 +49,12 @@ pub struct Page { | |||
impl Page { | |||
pub fn new(meta: PageFrontMatter) -> Page { | |||
pub fn new<P: AsRef<Path>>(file_path: P, meta: PageFrontMatter) -> Page { | |||
let file_path = file_path.as_ref(); | |||
Page { | |||
file: FileInfo::new_page(file_path), | |||
meta: meta, | |||
file_path: PathBuf::new(), | |||
relative_path: String::new(), | |||
parent_path: PathBuf::new(), | |||
file_name: "".to_string(), | |||
components: vec![], | |||
raw_content: "".to_string(), | |||
assets: vec![], | |||
content: "".to_string(), | |||
@@ -85,49 +71,26 @@ impl Page { | |||
/// Files without front matter or with invalid front matter are considered | |||
/// erroneous | |||
pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Page> { | |||
// 1. separate front matter from content | |||
let (meta, content) = split_page_content(file_path, content)?; | |||
let mut page = Page::new(meta); | |||
page.file_path = file_path.to_path_buf(); | |||
page.parent_path = page.file_path.parent().unwrap().to_path_buf(); | |||
let mut page = Page::new(file_path, meta); | |||
page.raw_content = content; | |||
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.file_name.clone()) | |||
slugify(page.file.name.clone()) | |||
} | |||
}; | |||
page.components = find_content_components(&page.file_path); | |||
page.relative_path = format!("{}/{}.md", page.components.join("/"), page.file_name); | |||
// 4. Find sections | |||
// Pages with custom urls exists outside of sections | |||
let mut path_set = false; | |||
if let Some(ref u) = page.meta.url { | |||
page.path = u.trim().to_string(); | |||
path_set = true; | |||
} | |||
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(); | |||
} | |||
if !path_set { | |||
// Don't add a trailing slash to sections | |||
page.path = format!("{}/{}", page.components.join("/"), page.slug); | |||
} | |||
} else if !path_set { | |||
page.path = page.slug.clone(); | |||
} else { | |||
page.path = if page.file.components.is_empty() { | |||
page.slug.clone() | |||
} else { | |||
format!("{}/{}", page.file.components.join("/"), page.slug) | |||
}; | |||
} | |||
page.permalink = config.make_permalink(&page.path); | |||
Ok(page) | |||
@@ -140,7 +103,7 @@ impl Page { | |||
let mut page = Page::parse(path, &content, config)?; | |||
page.assets = find_related_assets(path.parent().unwrap()); | |||
if !page.assets.is_empty() && page.file_name != "index" { | |||
if !page.assets.is_empty() && page.file.name != "index" { | |||
bail!("Page `{}` has assets ({:?}) but is not named index.md", path.display(), page.assets); | |||
} | |||
@@ -150,13 +113,13 @@ impl Page { | |||
/// We need access to all pages url to render links relative to content | |||
/// so that can't happen at the same time as parsing | |||
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<()> { | |||
self.content = markdown_to_html(&self.raw_content, permalinks, tera, config)?; | |||
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config, anchor_insert: InsertAnchor) -> Result<()> { | |||
let context = Context::new(tera, config, permalinks, anchor_insert); | |||
self.content = markdown_to_html(&self.raw_content, &context)?; | |||
if self.raw_content.contains("<!-- more -->") { | |||
self.summary = Some({ | |||
let summary = self.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[0]; | |||
markdown_to_html(summary, permalinks, tera, config)? | |||
markdown_to_html(summary, &context)? | |||
}) | |||
} | |||
@@ -170,26 +133,22 @@ impl Page { | |||
None => "page.html".to_string() | |||
}; | |||
let mut context = Context::new(); | |||
let mut context = TeraContext::new(); | |||
context.add("config", config); | |||
context.add("page", self); | |||
context.add("current_url", &self.permalink); | |||
context.add("current_path", &self.path); | |||
tera.render(&tpl_name, &context) | |||
.chain_err(|| format!("Failed to render page '{}'", self.file_path.display())) | |||
.chain_err(|| format!("Failed to render page '{}'", self.file.path.display())) | |||
} | |||
} | |||
impl Default for Page { | |||
fn default() -> Page { | |||
Page { | |||
file: FileInfo::default(), | |||
meta: PageFrontMatter::default(), | |||
file_path: PathBuf::new(), | |||
relative_path: String::new(), | |||
parent_path: PathBuf::new(), | |||
file_name: "".to_string(), | |||
components: vec![], | |||
raw_content: "".to_string(), | |||
assets: vec![], | |||
content: "".to_string(), | |||
@@ -205,7 +164,7 @@ impl Default for Page { | |||
impl ser::Serialize for Page { | |||
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer { | |||
let mut state = serializer.serialize_struct("page", 16)?; | |||
let mut state = serializer.serialize_struct("page", 15)?; | |||
state.serialize_field("content", &self.content)?; | |||
state.serialize_field("title", &self.meta.title)?; | |||
state.serialize_field("description", &self.meta.description)?; | |||
@@ -215,7 +174,6 @@ impl ser::Serialize for Page { | |||
state.serialize_field("permalink", &self.permalink)?; | |||
state.serialize_field("summary", &self.summary)?; | |||
state.serialize_field("tags", &self.meta.tags)?; | |||
state.serialize_field("draft", &self.meta.draft)?; | |||
state.serialize_field("category", &self.meta.category)?; | |||
state.serialize_field("extra", &self.meta.extra)?; | |||
let (word_count, reading_time) = get_reading_analytics(&self.raw_content); | |||
@@ -226,3 +184,142 @@ impl ser::Serialize for Page { | |||
state.end() | |||
} | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use std::collections::HashMap; | |||
use std::fs::{File, create_dir}; | |||
use std::path::Path; | |||
use tera::Tera; | |||
use tempdir::TempDir; | |||
use config::Config; | |||
use super::Page; | |||
use front_matter::InsertAnchor; | |||
#[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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default(), InsertAnchor::None).unwrap(); | |||
assert_eq!(page.meta.title.unwrap(), "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, "<p>Hello world</p>\n".to_string()); | |||
} | |||
#[test] | |||
fn test_can_make_url_from_sections_and_slug() { | |||
let content = r#" | |||
+++ | |||
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.path, "posts/intro/hello-world"); | |||
assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world"); | |||
} | |||
#[test] | |||
fn can_make_url_from_slug_only() { | |||
let content = r#" | |||
+++ | |||
slug = "hello-world" | |||
+++ | |||
Hello world"#; | |||
let config = Config::default(); | |||
let res = Page::parse(Path::new("start.md"), content, &config); | |||
assert!(res.is_ok()); | |||
let page = res.unwrap(); | |||
assert_eq!(page.path, "hello-world"); | |||
assert_eq!(page.permalink, config.make_permalink("hello-world")); | |||
} | |||
#[test] | |||
fn errors_on_invalid_front_matter_format() { | |||
// missing starting +++ | |||
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 can_make_slug_from_non_slug_filename() { | |||
let config = Config::default(); | |||
let res = Page::parse(Path::new(" file with space.md"), "+++\n+++", &config); | |||
assert!(res.is_ok()); | |||
let page = res.unwrap(); | |||
assert_eq!(page.slug, "file-with-space"); | |||
assert_eq!(page.permalink, config.make_permalink(&page.slug)); | |||
} | |||
#[test] | |||
fn can_specify_summary() { | |||
let config = Config::default(); | |||
let content = r#" | |||
+++ | |||
+++ | |||
Hello world | |||
<!-- more -->"#.to_string(); | |||
let res = Page::parse(Path::new("hello.md"), &content, &config); | |||
assert!(res.is_ok()); | |||
let mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &config, InsertAnchor::None).unwrap(); | |||
assert_eq!(page.summary, Some("<p>Hello world</p>\n".to_string())); | |||
} | |||
#[test] | |||
fn page_with_assets_gets_right_parent_path() { | |||
let tmp_dir = TempDir::new("example").expect("create temp dir"); | |||
let path = tmp_dir.path(); | |||
create_dir(&path.join("content")).expect("create content temp dir"); | |||
create_dir(&path.join("content").join("posts")).expect("create posts temp dir"); | |||
let nested_path = path.join("content").join("posts").join("assets"); | |||
create_dir(&nested_path).expect("create nested temp dir"); | |||
File::create(nested_path.join("index.md")).unwrap(); | |||
File::create(nested_path.join("example.js")).unwrap(); | |||
File::create(nested_path.join("graph.jpg")).unwrap(); | |||
File::create(nested_path.join("fail.png")).unwrap(); | |||
let res = Page::parse( | |||
nested_path.join("index.md").as_path(), | |||
"+++\nurl=\"hey\"+++\n", | |||
&Config::default() | |||
); | |||
assert!(res.is_ok()); | |||
let page = res.unwrap(); | |||
assert_eq!(page.file.parent, path.join("content").join("posts")); | |||
} | |||
#[test] | |||
fn errors_file_not_named_index_with_assets() { | |||
let tmp_dir = TempDir::new("example").expect("create temp dir"); | |||
File::create(tmp_dir.path().join("something.md")).unwrap(); | |||
File::create(tmp_dir.path().join("example.js")).unwrap(); | |||
File::create(tmp_dir.path().join("graph.jpg")).unwrap(); | |||
File::create(tmp_dir.path().join("fail.png")).unwrap(); | |||
let page = Page::from_file(tmp_dir.path().join("something.md"), &Config::default()); | |||
assert!(page.is_err()); | |||
} | |||
} |
@@ -145,7 +145,7 @@ impl<'a> Paginator<'a> { | |||
} | |||
site.tera.render(&self.section.get_template_name(), &context) | |||
.chain_err(|| format!("Failed to render pager {} of section '{}'", pager.index, self.section.file_path.display())) | |||
.chain_err(|| format!("Failed to render pager {} of section '{}'", pager.index, self.section.file.path.display())) | |||
} | |||
} | |||
@@ -166,7 +166,7 @@ mod tests { | |||
if !is_index { | |||
s.path = "posts".to_string(); | |||
s.permalink = "https://vincent.is/posts".to_string(); | |||
s.components = vec!["posts".to_string()]; | |||
s.file.components = vec!["posts".to_string()]; | |||
} else { | |||
s.permalink = "https://vincent.is".to_string(); | |||
} | |||
@@ -2,29 +2,25 @@ use std::collections::HashMap; | |||
use std::path::{Path, PathBuf}; | |||
use std::result::Result as StdResult; | |||
use tera::{Tera, Context}; | |||
use tera::{Tera, Context as TeraContext}; | |||
use serde::ser::{SerializeStruct, self}; | |||
use config::Config; | |||
use front_matter::{SectionFrontMatter, split_section_content}; | |||
use errors::{Result, ResultExt}; | |||
use utils::{read_file, find_content_components}; | |||
use markdown::markdown_to_html; | |||
use fs::{read_file}; | |||
use rendering::markdown::markdown_to_html; | |||
use rendering::context::Context; | |||
use content::Page; | |||
use content::file_info::FileInfo; | |||
#[derive(Clone, Debug, PartialEq)] | |||
pub struct Section { | |||
/// All info about the actual file | |||
pub file: FileInfo, | |||
/// The front matter meta-data | |||
pub meta: SectionFrontMatter, | |||
/// The _index.md full path | |||
pub file_path: PathBuf, | |||
/// The .md path, starting from the content directory, with / slashes | |||
pub relative_path: String, | |||
/// Path of the directory containing the _index.md file | |||
pub parent_path: PathBuf, | |||
/// The folder names from `content` to this section file | |||
pub components: Vec<String>, | |||
/// The URL path of the page | |||
pub path: String, | |||
/// The full URL for that page | |||
@@ -46,11 +42,8 @@ impl Section { | |||
let file_path = file_path.as_ref(); | |||
Section { | |||
file: FileInfo::new_section(file_path), | |||
meta: meta, | |||
file_path: file_path.to_path_buf(), | |||
relative_path: "".to_string(), | |||
parent_path: file_path.parent().unwrap().to_path_buf(), | |||
components: vec![], | |||
path: "".to_string(), | |||
permalink: "".to_string(), | |||
raw_content: "".to_string(), | |||
@@ -65,16 +58,8 @@ impl Section { | |||
let (meta, content) = split_section_content(file_path, content)?; | |||
let mut section = Section::new(file_path, meta); | |||
section.raw_content = content.clone(); | |||
section.components = find_content_components(§ion.file_path); | |||
section.path = section.components.join("/"); | |||
section.path = section.file.components.join("/"); | |||
section.permalink = config.make_permalink(§ion.path); | |||
if section.components.is_empty() { | |||
// the index one | |||
section.relative_path = "_index.md".to_string(); | |||
} else { | |||
section.relative_path = format!("{}/_index.md", section.components.join("/")); | |||
} | |||
Ok(section) | |||
} | |||
@@ -101,7 +86,8 @@ impl Section { | |||
/// We need access to all pages url to render links relative to content | |||
/// so that can't happen at the same time as parsing | |||
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<()> { | |||
self.content = markdown_to_html(&self.raw_content, permalinks, tera, config)?; | |||
let context = Context::new(tera, config, permalinks, self.meta.insert_anchor.unwrap()); | |||
self.content = markdown_to_html(&self.raw_content, &context)?; | |||
Ok(()) | |||
} | |||
@@ -109,7 +95,7 @@ impl Section { | |||
pub fn render_html(&self, sections: HashMap<String, Section>, tera: &Tera, config: &Config) -> Result<String> { | |||
let tpl_name = self.get_template_name(); | |||
let mut context = Context::new(); | |||
let mut context = TeraContext::new(); | |||
context.add("config", config); | |||
context.add("section", self); | |||
context.add("current_url", &self.permalink); | |||
@@ -119,46 +105,36 @@ impl Section { | |||
} | |||
tera.render(&tpl_name, &context) | |||
.chain_err(|| format!("Failed to render section '{}'", self.file_path.display())) | |||
.chain_err(|| format!("Failed to render section '{}'", self.file.path.display())) | |||
} | |||
/// Is this the index section? | |||
pub fn is_index(&self) -> bool { | |||
self.components.is_empty() | |||
self.file.components.is_empty() | |||
} | |||
/// Returns all the paths for the pages belonging to that section | |||
/// Returns all the paths of the pages belonging to that section | |||
pub fn all_pages_path(&self) -> Vec<PathBuf> { | |||
let mut paths = vec![]; | |||
paths.extend(self.pages.iter().map(|p| p.file_path.clone())); | |||
paths.extend(self.ignored_pages.iter().map(|p| p.file_path.clone())); | |||
paths.extend(self.pages.iter().map(|p| p.file.path.clone())); | |||
paths.extend(self.ignored_pages.iter().map(|p| p.file.path.clone())); | |||
paths | |||
} | |||
/// Whether the page given belongs to that section | |||
pub fn is_child_page(&self, page: &Page) -> bool { | |||
for p in &self.pages { | |||
if p.file_path == page.file_path { | |||
return true; | |||
} | |||
} | |||
for p in &self.ignored_pages { | |||
if p.file_path == page.file_path { | |||
return true; | |||
} | |||
} | |||
false | |||
pub fn is_child_page(&self, path: &PathBuf) -> bool { | |||
self.all_pages_path().contains(path) | |||
} | |||
} | |||
impl ser::Serialize for Section { | |||
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer { | |||
let mut state = serializer.serialize_struct("section", 7)?; | |||
let mut state = serializer.serialize_struct("section", 9)?; | |||
state.serialize_field("content", &self.content)?; | |||
state.serialize_field("permalink", &self.permalink)?; | |||
state.serialize_field("title", &self.meta.title)?; | |||
state.serialize_field("description", &self.meta.description)?; | |||
state.serialize_field("extra", &self.meta.extra)?; | |||
state.serialize_field("path", &format!("/{}", self.path))?; | |||
state.serialize_field("permalink", &self.permalink)?; | |||
state.serialize_field("pages", &self.pages)?; | |||
@@ -167,15 +143,12 @@ impl ser::Serialize for Section { | |||
} | |||
} | |||
/// Used to create a default index section if there is no _index.md in the root content directory | |||
impl Default for Section { | |||
/// Used to create a default index section if there is no _index.md in the root content directory | |||
fn default() -> Section { | |||
Section { | |||
file: FileInfo::default(), | |||
meta: SectionFrontMatter::default(), | |||
file_path: PathBuf::new(), | |||
relative_path: "".to_string(), | |||
parent_path: PathBuf::new(), | |||
components: vec![], | |||
path: "".to_string(), | |||
permalink: "".to_string(), | |||
raw_content: "".to_string(), | |||
@@ -59,12 +59,20 @@ pub fn populate_previous_and_next_pages(input: &[Page]) -> Vec<Page> { | |||
if i > 0 { | |||
let next = &pages[i - 1]; | |||
new_page.next = Some(Box::new(next.clone())); | |||
let mut next_page = next.clone(); | |||
// Remove prev/next otherwise we serialise the whole thing... | |||
next_page.previous = None; | |||
next_page.next = None; | |||
new_page.next = Some(Box::new(next_page)); | |||
} | |||
if i < input.len() - 1 { | |||
let previous = &pages[i + 1]; | |||
new_page.previous = Some(Box::new(previous.clone())); | |||
// Remove prev/next otherwise we serialise the whole thing... | |||
let mut previous_page = previous.clone(); | |||
previous_page.previous = None; | |||
previous_page.next = None; | |||
new_page.previous = Some(Box::new(previous_page)); | |||
} | |||
res.push(new_page); | |||
} | |||
@@ -81,13 +89,13 @@ mod tests { | |||
fn create_page_with_date(date: &str) -> Page { | |||
let mut front_matter = PageFrontMatter::default(); | |||
front_matter.date = Some(date.to_string()); | |||
Page::new(front_matter) | |||
Page::new("content/hello.md", front_matter) | |||
} | |||
fn create_page_with_order(order: usize) -> Page { | |||
let mut front_matter = PageFrontMatter::default(); | |||
front_matter.order = Some(order); | |||
Page::new(front_matter) | |||
Page::new("content/hello.md", front_matter) | |||
} | |||
#[test] | |||
@@ -0,0 +1,135 @@ | |||
use std::collections::HashMap; | |||
use slug::slugify; | |||
use tera::{Context, Tera}; | |||
use config::Config; | |||
use errors::{Result, ResultExt}; | |||
use content::Page; | |||
#[derive(Debug, Copy, Clone, PartialEq)] | |||
pub enum TaxonomyKind { | |||
Tags, | |||
Categories, | |||
} | |||
/// A tag or category | |||
#[derive(Debug, Clone, Serialize, PartialEq)] | |||
pub struct TaxonomyItem { | |||
pub name: String, | |||
pub slug: String, | |||
pub pages: Vec<Page>, | |||
} | |||
impl TaxonomyItem { | |||
pub fn new(name: &str, pages: Vec<Page>) -> TaxonomyItem { | |||
TaxonomyItem { | |||
name: name.to_string(), | |||
slug: slugify(name), | |||
pages, | |||
} | |||
} | |||
} | |||
/// All the tags or categories | |||
#[derive(Debug, Clone, PartialEq)] | |||
pub struct Taxonomy { | |||
pub kind: TaxonomyKind, | |||
// this vec is sorted by the count of item | |||
pub items: Vec<TaxonomyItem>, | |||
} | |||
impl Taxonomy { | |||
// TODO: take a Vec<&'a Page> if it makes a difference in terms of perf for actual sites | |||
pub fn find_tags_and_categories(all_pages: Vec<Page>) -> (Taxonomy, Taxonomy) { | |||
let mut tags = HashMap::new(); | |||
let mut categories = HashMap::new(); | |||
// Find all the tags/categories first | |||
for page in all_pages { | |||
if let Some(ref category) = page.meta.category { | |||
categories | |||
.entry(category.to_string()) | |||
.or_insert_with(|| vec![]) | |||
.push(page.clone()); | |||
} | |||
if let Some(ref t) = page.meta.tags { | |||
for tag in t { | |||
tags | |||
.entry(tag.to_string()) | |||
.or_insert_with(|| vec![]) | |||
.push(page.clone()); | |||
} | |||
} | |||
} | |||
// Then make TaxonomyItem out of them, after sorting it | |||
let tags_taxonomy = Taxonomy::new(TaxonomyKind::Tags, tags); | |||
let categories_taxonomy = Taxonomy::new(TaxonomyKind::Categories, categories); | |||
(tags_taxonomy, categories_taxonomy) | |||
} | |||
fn new(kind: TaxonomyKind, items: HashMap<String, Vec<Page>>) -> Taxonomy { | |||
let mut sorted_items = vec![]; | |||
for (name, pages) in &items { | |||
sorted_items.push( | |||
TaxonomyItem::new(name, pages.clone()) | |||
); | |||
} | |||
sorted_items.sort_by(|a, b| b.pages.len().cmp(&a.pages.len())); | |||
Taxonomy { | |||
kind, | |||
items: sorted_items, | |||
} | |||
} | |||
pub fn len(&self) -> usize { | |||
self.items.len() | |||
} | |||
pub fn get_single_item_name(&self) -> String { | |||
match self.kind { | |||
TaxonomyKind::Tags => "tag".to_string(), | |||
TaxonomyKind::Categories => "category".to_string(), | |||
} | |||
} | |||
pub fn get_list_name(&self) -> String { | |||
match self.kind { | |||
TaxonomyKind::Tags => "tags".to_string(), | |||
TaxonomyKind::Categories => "categories".to_string(), | |||
} | |||
} | |||
pub fn render_single_item(&self, item: &TaxonomyItem, tera: &Tera, config: &Config) -> Result<String> { | |||
let name = self.get_single_item_name(); | |||
let mut context = Context::new(); | |||
context.add("config", config); | |||
// TODO: how to sort categories and tag content? | |||
// Have a setting in config.toml or a _category.md and _tag.md | |||
// The latter is more in line with the rest of Gutenberg but order ordering | |||
// doesn't really work across sections. | |||
context.add(&name, item); | |||
context.add("current_url", &config.make_permalink(&format!("{}/{}", name, item.slug))); | |||
context.add("current_path", &format!("/{}/{}", name, item.slug)); | |||
tera.render(&format!("{}.html", name), &context) | |||
.chain_err(|| format!("Failed to render {} page.", name)) | |||
} | |||
pub fn render_list(&self, tera: &Tera, config: &Config) -> Result<String> { | |||
let name = self.get_list_name(); | |||
let mut context = Context::new(); | |||
context.add("config", config); | |||
context.add(&name, &self.items); | |||
context.add("current_url", &config.make_permalink(&name)); | |||
context.add("current_path", &name); | |||
tera.render(&format!("{}.html", name), &context) | |||
.chain_err(|| format!("Failed to render {} page.", name)) | |||
} | |||
} |
@@ -32,7 +32,6 @@ pub fn get_reading_analytics(content: &str) -> (usize, usize) { | |||
(word_count, (word_count / 200)) | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use std::fs::File; | |||
@@ -8,7 +8,7 @@ mod page; | |||
mod section; | |||
pub use self::page::PageFrontMatter; | |||
pub use self::section::{SectionFrontMatter}; | |||
pub use self::section::{SectionFrontMatter, InsertAnchor}; | |||
lazy_static! { | |||
static ref PAGE_RE: Regex = Regex::new(r"^[[:space:]]*\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap(); | |||
@@ -24,8 +24,6 @@ pub struct PageFrontMatter { | |||
pub url: Option<String>, | |||
/// Tags, not to be confused with categories | |||
pub tags: Option<Vec<String>>, | |||
/// Whether this page is a draft and should be published or not | |||
pub draft: Option<bool>, | |||
/// Only one category allowed. Can't be an empty string if present | |||
pub category: Option<String>, | |||
/// Integer to use to order content. Lowest is at the bottom, highest first | |||
@@ -100,7 +98,6 @@ impl Default for PageFrontMatter { | |||
slug: None, | |||
url: None, | |||
tags: None, | |||
draft: None, | |||
category: None, | |||
order: None, | |||
template: None, | |||
@@ -9,6 +9,14 @@ use content::SortBy; | |||
static DEFAULT_PAGINATE_PATH: &'static str = "page"; | |||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] | |||
#[serde(rename_all = "lowercase")] | |||
pub enum InsertAnchor { | |||
Left, | |||
Right, | |||
None, | |||
} | |||
/// The front matter of every section | |||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | |||
pub struct SectionFrontMatter { | |||
@@ -28,6 +36,10 @@ pub struct SectionFrontMatter { | |||
/// Path to be used by pagination: the page number will be appended after it. Defaults to `page`. | |||
#[serde(skip_serializing)] | |||
pub paginate_path: Option<String>, | |||
/// Whether to insert a link for each header like in Github READMEs. Defaults to false | |||
/// The default template can be overridden by creating a `anchor-link.html` template and CSS will need to be | |||
/// written if you turn that on. | |||
pub insert_anchor: Option<InsertAnchor>, | |||
/// Whether to render that section or not. Defaults to `true`. | |||
/// Useful when the section is only there to organize things but is not meant | |||
/// to be used directly, like a posts section in a personal site | |||
@@ -56,6 +68,10 @@ impl SectionFrontMatter { | |||
f.sort_by = Some(SortBy::None); | |||
} | |||
if f.insert_anchor.is_none() { | |||
f.insert_anchor = Some(InsertAnchor::None); | |||
} | |||
Ok(f) | |||
} | |||
@@ -87,6 +103,7 @@ impl Default for SectionFrontMatter { | |||
paginate_by: None, | |||
paginate_path: Some(DEFAULT_PAGINATE_PATH.to_string()), | |||
render: Some(true), | |||
insert_anchor: Some(InsertAnchor::None), | |||
extra: None, | |||
} | |||
} | |||
@@ -0,0 +1,40 @@ | |||
use std::io::prelude::*; | |||
use std::fs::{File, create_dir}; | |||
use std::path::Path; | |||
use errors::{Result, ResultExt}; | |||
/// Create a file with the content given | |||
pub fn create_file(path: &Path, content: &str) -> Result<()> { | |||
let mut file = File::create(&path)?; | |||
file.write_all(content.as_bytes())?; | |||
Ok(()) | |||
} | |||
/// Create a directory at the given path if it doesn't exist already | |||
pub fn ensure_directory_exists(path: &Path) -> Result<()> { | |||
if !path.exists() { | |||
create_directory(path)?; | |||
} | |||
Ok(()) | |||
} | |||
/// Very similar to `create_dir` from the std except it checks if the folder | |||
/// exists before creating it | |||
pub fn create_directory(path: &Path) -> Result<()> { | |||
if !path.exists() { | |||
create_dir(path) | |||
.chain_err(|| format!("Was not able to create folder {}", path.display()))?; | |||
} | |||
Ok(()) | |||
} | |||
/// Return the content of a file, with error handling added | |||
pub fn read_file(path: &Path) -> Result<String> { | |||
let mut content = String::new(); | |||
File::open(path) | |||
.chain_err(|| format!("Failed to open '{:?}'", path.display()))? | |||
.read_to_string(&mut content)?; | |||
Ok(content) | |||
} |
@@ -19,19 +19,18 @@ extern crate base64; | |||
#[cfg(test)] | |||
extern crate tempdir; | |||
mod utils; | |||
mod fs; | |||
mod config; | |||
pub mod errors; | |||
mod front_matter; | |||
mod content; | |||
mod site; | |||
mod markdown; | |||
mod rendering; | |||
// Filters, Global Fns and default instance of Tera | |||
mod templates; | |||
pub use site::{Site}; | |||
pub use config::{Config, get_config}; | |||
pub use front_matter::{PageFrontMatter, SectionFrontMatter, split_page_content, split_section_content}; | |||
pub use front_matter::{PageFrontMatter, SectionFrontMatter, InsertAnchor, split_page_content, split_section_content}; | |||
pub use content::{Page, Section, SortBy, sort_pages, populate_previous_and_next_pages}; | |||
pub use utils::{create_file}; | |||
pub use markdown::markdown_to_html; | |||
pub use fs::{create_file}; |
@@ -0,0 +1,33 @@ | |||
use std::collections::HashMap; | |||
use tera::Tera; | |||
use config::Config; | |||
use front_matter::InsertAnchor; | |||
/// All the information from the gutenberg site that is needed to render HTML from markdown | |||
#[derive(Debug)] | |||
pub struct Context<'a> { | |||
pub tera: &'a Tera, | |||
pub highlight_code: bool, | |||
pub highlight_theme: String, | |||
pub permalinks: &'a HashMap<String, String>, | |||
pub insert_anchor: InsertAnchor, | |||
} | |||
impl<'a> Context<'a> { | |||
pub fn new(tera: &'a Tera, config: &'a Config, permalinks: &'a HashMap<String, String>, insert_anchor: InsertAnchor) -> Context<'a> { | |||
Context { | |||
tera, | |||
permalinks, | |||
insert_anchor, | |||
highlight_code: config.highlight_code.unwrap(), | |||
highlight_theme: config.highlight_theme.clone().unwrap(), | |||
} | |||
} | |||
pub fn should_insert_anchor(&self) -> bool { | |||
self.insert_anchor != InsertAnchor::None | |||
} | |||
} |
@@ -0,0 +1,6 @@ | |||
use syntect::dumps::from_binary; | |||
use syntect::highlighting::ThemeSet; | |||
lazy_static!{ | |||
pub static ref THEME_SET: ThemeSet = from_binary(include_bytes!("../../sublime_themes/all.themedump")); | |||
} |
@@ -1,5 +1,4 @@ | |||
use std::borrow::Cow::Owned; | |||
use std::collections::HashMap; | |||
use pulldown_cmark as cmark; | |||
use self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES}; | |||
@@ -8,18 +7,19 @@ use slug::slugify; | |||
use syntect::dumps::from_binary; | |||
use syntect::easy::HighlightLines; | |||
use syntect::parsing::SyntaxSet; | |||
use syntect::highlighting::ThemeSet; | |||
use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground}; | |||
use tera::{Tera, Context}; | |||
use config::Config; | |||
use errors::{Result, ResultExt}; | |||
use tera::{Context as TeraContext}; | |||
use errors::{Result}; | |||
use site::resolve_internal_link; | |||
use front_matter::InsertAnchor; | |||
use rendering::context::Context; | |||
use rendering::highlighting::THEME_SET; | |||
use rendering::short_code::{ShortCode, parse_shortcode, render_simple_shortcode}; | |||
// We need to put those in a struct to impl Send and sync | |||
pub struct Setup { | |||
pub syntax_set: SyntaxSet, | |||
pub theme_set: ThemeSet, | |||
} | |||
unsafe impl Send for Setup {} | |||
@@ -29,87 +29,25 @@ lazy_static!{ | |||
static ref SHORTCODE_RE: Regex = Regex::new(r#"\{(?:%|\{)\s+([[:alnum:]]+?)\(([[:alnum:]]+?="?.+?"?)\)\s+(?:%|\})\}"#).unwrap(); | |||
pub static ref SETUP: Setup = Setup { | |||
syntax_set: { | |||
let mut ps: SyntaxSet = from_binary(include_bytes!("../sublime_syntaxes/newlines.packdump")); | |||
let mut ps: SyntaxSet = from_binary(include_bytes!("../../sublime_syntaxes/newlines.packdump")); | |||
ps.link_syntaxes(); | |||
ps | |||
}, | |||
theme_set: from_binary(include_bytes!("../sublime_themes/all.themedump")) | |||
}; | |||
} | |||
/// A shortcode that has a body | |||
/// Called by having some content like {% ... %} body {% end %} | |||
/// We need the struct to hold the data while we're processing the markdown | |||
#[derive(Debug)] | |||
struct ShortCode { | |||
name: String, | |||
args: HashMap<String, String>, | |||
body: String, | |||
} | |||
impl ShortCode { | |||
pub fn new(name: &str, args: HashMap<String, String>) -> ShortCode { | |||
ShortCode { | |||
name: name.to_string(), | |||
args: args, | |||
body: String::new(), | |||
} | |||
} | |||
pub fn append(&mut self, text: &str) { | |||
self.body.push_str(text) | |||
} | |||
pub fn render(&self, tera: &Tera) -> Result<String> { | |||
let mut context = Context::new(); | |||
for (key, value) in &self.args { | |||
context.add(key, value); | |||
} | |||
context.add("body", &self.body); | |||
let tpl_name = format!("shortcodes/{}.html", self.name); | |||
tera.render(&tpl_name, &context) | |||
.chain_err(|| format!("Failed to render {} shortcode", self.name)) | |||
} | |||
} | |||
/// Parse a shortcode without a body | |||
fn parse_shortcode(input: &str) -> (String, HashMap<String, String>) { | |||
let mut args = HashMap::new(); | |||
let caps = SHORTCODE_RE.captures(input).unwrap(); | |||
// caps[0] is the full match | |||
let name = &caps[1]; | |||
let arg_list = &caps[2]; | |||
for arg in arg_list.split(',') { | |||
let bits = arg.split('=').collect::<Vec<_>>(); | |||
args.insert(bits[0].trim().to_string(), bits[1].replace("\"", "")); | |||
} | |||
(name.to_string(), args) | |||
} | |||
/// Renders a shortcode or return an error | |||
fn render_simple_shortcode(tera: &Tera, name: &str, args: &HashMap<String, String>) -> Result<String> { | |||
let mut context = Context::new(); | |||
for (key, value) in args.iter() { | |||
context.add(key, value); | |||
} | |||
let tpl_name = format!("shortcodes/{}.html", name); | |||
tera.render(&tpl_name, &context).chain_err(|| format!("Failed to render {} shortcode", name)) | |||
} | |||
pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<String> { | |||
pub fn markdown_to_html(content: &str, context: &Context) -> Result<String> { | |||
// 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, | |||
// if we see a code block in the content, we assume that this page needs | |||
// to be highlighted. It could potentially have false positive if the content | |||
// has ``` in it but that seems kind of unlikely | |||
let should_highlight = if config.highlight_code.unwrap() { | |||
let should_highlight = if context.highlight_code { | |||
content.contains("```") | |||
} else { | |||
false | |||
}; | |||
let highlight_theme = config.highlight_theme.clone().unwrap(); | |||
// Set while parsing | |||
let mut error = None; | |||
let mut highlighter: Option<HighlightLines> = None; | |||
@@ -167,7 +105,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter | |||
if shortcode_block.is_none() && text.starts_with("{{") && text.ends_with("}}") && SHORTCODE_RE.is_match(&text) { | |||
let (name, args) = parse_shortcode(&text); | |||
added_shortcode = true; | |||
match render_simple_shortcode(tera, &name, &args) { | |||
match render_simple_shortcode(context.tera, &name, &args) { | |||
Ok(s) => return Event::Html(Owned(format!("</p>{}", s))), | |||
Err(e) => { | |||
error = Some(e); | |||
@@ -193,7 +131,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter | |||
if let Some(ref mut shortcode) = shortcode_block { | |||
if text.trim() == "{% end %}" { | |||
added_shortcode = true; | |||
match shortcode.render(tera) { | |||
match shortcode.render(context.tera) { | |||
Ok(s) => return Event::Html(Owned(format!("</p>{}", s))), | |||
Err(e) => { | |||
error = Some(e); | |||
@@ -213,15 +151,20 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter | |||
} | |||
let id = find_anchor(&anchors, slugify(&text), 0); | |||
anchors.push(id.clone()); | |||
let anchor_link = if config.insert_anchor_links.unwrap() { | |||
let mut context = Context::new(); | |||
context.add("id", &id); | |||
tera.render("anchor-link.html", &context).unwrap() | |||
let anchor_link = if context.should_insert_anchor() { | |||
let mut c = TeraContext::new(); | |||
c.add("id", &id); | |||
context.tera.render("anchor-link.html", &c).unwrap() | |||
} else { | |||
String::new() | |||
}; | |||
header_already_inserted = true; | |||
return Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, anchor_link, text))); | |||
let event = match context.insert_anchor { | |||
InsertAnchor::Left => Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, anchor_link, text))), | |||
InsertAnchor::Right => Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, text, anchor_link))), | |||
InsertAnchor::None => Event::Html(Owned(format!(r#"id="{}">{}"#, id, text))) | |||
}; | |||
return event; | |||
} | |||
// Business as usual | |||
@@ -232,7 +175,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter | |||
if !should_highlight { | |||
return Event::Html(Owned("<pre><code>".to_owned())); | |||
} | |||
let theme = &SETUP.theme_set.themes[&highlight_theme]; | |||
let theme = &THEME_SET.themes[&context.highlight_theme]; | |||
let syntax = info | |||
.split(' ') | |||
.next() | |||
@@ -257,21 +200,11 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter | |||
return Event::Html(Owned("".to_owned())); | |||
} | |||
if link.starts_with("./") { | |||
// First we remove the ./ since that's gutenberg specific | |||
let clean_link = link.replacen("./", "", 1); | |||
// Then we remove any potential anchor | |||
// parts[0] will be the file path and parts[1] the anchor if present | |||
let parts = clean_link.split('#').collect::<Vec<_>>(); | |||
match permalinks.get(parts[0]) { | |||
Some(p) => { | |||
let url = if parts.len() > 1 { | |||
format!("{}#{}", p, parts[1]) | |||
} else { | |||
p.to_string() | |||
}; | |||
match resolve_internal_link(link, context.permalinks) { | |||
Ok(url) => { | |||
return Event::Start(Tag::Link(Owned(url), title.clone())); | |||
}, | |||
None => { | |||
Err(_) => { | |||
error = Some(format!("Relative link {} not found.", link).into()); | |||
return Event::Html(Owned("".to_string())); | |||
} | |||
@@ -340,46 +273,33 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter | |||
mod tests { | |||
use std::collections::HashMap; | |||
use templates::GUTENBERG_TERA; | |||
use tera::Tera; | |||
use config::Config; | |||
use super::{markdown_to_html, parse_shortcode}; | |||
#[test] | |||
fn test_parse_simple_shortcode_one_arg() { | |||
let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc") }}"#); | |||
assert_eq!(name, "youtube"); | |||
assert_eq!(args["id"], "w7Ft2ymGmfc"); | |||
} | |||
#[test] | |||
fn test_parse_simple_shortcode_several_arg() { | |||
let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc", autoplay=true) }}"#); | |||
assert_eq!(name, "youtube"); | |||
assert_eq!(args["id"], "w7Ft2ymGmfc"); | |||
assert_eq!(args["autoplay"], "true"); | |||
} | |||
use front_matter::InsertAnchor; | |||
use templates::GUTENBERG_TERA; | |||
use rendering::context::Context; | |||
#[test] | |||
fn test_parse_block_shortcode_several_arg() { | |||
let (name, args) = parse_shortcode(r#"{% youtube(id="w7Ft2ymGmfc", autoplay=true) %}"#); | |||
assert_eq!(name, "youtube"); | |||
assert_eq!(args["id"], "w7Ft2ymGmfc"); | |||
assert_eq!(args["autoplay"], "true"); | |||
} | |||
use super::markdown_to_html; | |||
#[test] | |||
fn test_markdown_to_html_simple() { | |||
let res = markdown_to_html("hello", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); | |||
fn can_do_markdown_to_html_simple() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("hello", &context).unwrap(); | |||
assert_eq!(res, "<p>hello</p>\n"); | |||
} | |||
#[test] | |||
fn test_markdown_to_html_code_block_highlighting_off() { | |||
let mut config = Config::default(); | |||
config.highlight_code = Some(false); | |||
let res = markdown_to_html("```\n$ gutenberg server\n```", &HashMap::new(), &Tera::default(), &config).unwrap(); | |||
fn doesnt_highlight_code_block_with_highlighting_off() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let mut context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
context.highlight_code = false; | |||
let res = markdown_to_html("```\n$ gutenberg server\n```", &context).unwrap(); | |||
assert_eq!( | |||
res, | |||
"<pre><code>$ gutenberg server\n</code></pre>\n" | |||
@@ -387,8 +307,12 @@ mod tests { | |||
} | |||
#[test] | |||
fn test_markdown_to_html_code_block_no_lang() { | |||
let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); | |||
fn can_highlight_code_block_no_lang() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", &context).unwrap(); | |||
assert_eq!( | |||
res, | |||
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">$ gutenberg server\n</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">$ ping\n</span></pre>" | |||
@@ -396,8 +320,12 @@ mod tests { | |||
} | |||
#[test] | |||
fn test_markdown_to_html_code_block_with_lang() { | |||
let res = markdown_to_html("```python\nlist.append(1)\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); | |||
fn can_highlight_code_block_with_lang() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("```python\nlist.append(1)\n```", &context).unwrap(); | |||
assert_eq!( | |||
res, | |||
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">.</span><span style=\"background-color:#2b303b;color:#bf616a;\">append</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">(</span><span style=\"background-color:#2b303b;color:#d08770;\">1</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">)</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">\n</span></pre>" | |||
@@ -405,8 +333,12 @@ mod tests { | |||
} | |||
#[test] | |||
fn test_markdown_to_html_code_block_with_unknown_lang() { | |||
let res = markdown_to_html("```yolo\nlist.append(1)\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap(); | |||
fn can_higlight_code_block_with_unknown_lang() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("```yolo\nlist.append(1)\n```", &context).unwrap(); | |||
// defaults to plain text | |||
assert_eq!( | |||
res, | |||
@@ -415,18 +347,24 @@ mod tests { | |||
} | |||
#[test] | |||
fn test_markdown_to_html_with_shortcode() { | |||
fn can_render_shortcode() { | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html(r#" | |||
Hello | |||
{{ youtube(id="ub36ffWAqgQ") }} | |||
"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap(); | |||
"#, &context).unwrap(); | |||
assert!(res.contains("<p>Hello</p>\n<div >")); | |||
assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#)); | |||
} | |||
#[test] | |||
fn test_markdown_to_html_with_several_shortcode_in_row() { | |||
fn can_render_several_shortcode_in_row() { | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html(r#" | |||
Hello | |||
@@ -438,7 +376,7 @@ Hello | |||
{{ gist(url="https://gist.github.com/Keats/32d26f699dcc13ebd41b") }} | |||
"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap(); | |||
"#, &context).unwrap(); | |||
assert!(res.contains("<p>Hello</p>\n<div >")); | |||
assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#)); | |||
assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#)); | |||
@@ -446,40 +384,52 @@ Hello | |||
} | |||
#[test] | |||
fn test_markdown_to_html_shortcode_in_code_block() { | |||
let res = markdown_to_html(r#"```{{ youtube(id="w7Ft2ymGmfc") }}```"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap(); | |||
fn doesnt_render_shortcode_in_code_block() { | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html(r#"```{{ youtube(id="w7Ft2ymGmfc") }}```"#, &context).unwrap(); | |||
assert_eq!(res, "<p><code>{{ youtube(id="w7Ft2ymGmfc") }}</code></p>\n"); | |||
} | |||
#[test] | |||
fn test_markdown_to_html_shortcode_with_body() { | |||
fn can_render_shortcode_with_body() { | |||
let mut tera = Tera::default(); | |||
tera.extend(&GUTENBERG_TERA).unwrap(); | |||
tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author}}</blockquote>").unwrap(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html(r#" | |||
Hello | |||
{% quote(author="Keats") %} | |||
A quote | |||
{% end %} | |||
"#, &HashMap::new(), &tera, &Config::default()).unwrap(); | |||
"#, &context).unwrap(); | |||
assert_eq!(res, "<p>Hello\n</p><blockquote>A quote - Keats</blockquote>"); | |||
} | |||
#[test] | |||
fn test_markdown_to_html_unknown_shortcode() { | |||
let res = markdown_to_html("{{ hello(flash=true) }}", &HashMap::new(), &Tera::default(), &Config::default()); | |||
fn errors_rendering_unknown_shortcode() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("{{ hello(flash=true) }}", &context); | |||
assert!(res.is_err()); | |||
} | |||
#[test] | |||
fn test_markdown_to_html_relative_link_exists() { | |||
fn can_make_valid_relative_link() { | |||
let mut permalinks = HashMap::new(); | |||
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); | |||
let tera_ctx = Tera::default(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera_ctx, &config_ctx, &permalinks, InsertAnchor::None); | |||
let res = markdown_to_html( | |||
r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#, | |||
&permalinks, | |||
&GUTENBERG_TERA, | |||
&Config::default() | |||
&context | |||
).unwrap(); | |||
assert!( | |||
@@ -488,15 +438,13 @@ A quote | |||
} | |||
#[test] | |||
fn test_markdown_to_html_relative_links_with_anchors() { | |||
fn can_make_relative_links_with_anchors() { | |||
let mut permalinks = HashMap::new(); | |||
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); | |||
let res = markdown_to_html( | |||
r#"[rel link](./pages/about.md#cv)"#, | |||
&permalinks, | |||
&GUTENBERG_TERA, | |||
&Config::default() | |||
).unwrap(); | |||
let tera_ctx = Tera::default(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera_ctx, &config_ctx, &permalinks, InsertAnchor::None); | |||
let res = markdown_to_html(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap(); | |||
assert!( | |||
res.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#) | |||
@@ -504,67 +452,94 @@ A quote | |||
} | |||
#[test] | |||
fn test_markdown_to_html_relative_link_inexistant() { | |||
let res = markdown_to_html("[rel link](./pages/about.md)", &HashMap::new(), &Tera::default(), &Config::default()); | |||
fn errors_relative_link_inexistant() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("[rel link](./pages/about.md)", &context); | |||
assert!(res.is_err()); | |||
} | |||
#[test] | |||
fn test_markdown_to_html_add_id_to_headers() { | |||
let res = markdown_to_html(r#"# Hello"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap(); | |||
fn can_add_id_to_headers() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html(r#"# Hello"#, &context).unwrap(); | |||
assert_eq!(res, "<h1 id=\"hello\">Hello</h1>\n"); | |||
} | |||
#[test] | |||
fn test_markdown_to_html_add_id_to_headers_same_slug() { | |||
let res = markdown_to_html("# Hello\n# Hello", &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap(); | |||
fn can_add_id_to_headers_same_slug() { | |||
let tera_ctx = Tera::default(); | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None); | |||
let res = markdown_to_html("# Hello\n# Hello", &context).unwrap(); | |||
assert_eq!(res, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n"); | |||
} | |||
#[test] | |||
fn test_markdown_to_html_insert_anchor() { | |||
let mut config = Config::default(); | |||
config.insert_anchor_links = Some(true); | |||
let res = markdown_to_html("# Hello", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap(); | |||
fn can_insert_anchor_left() { | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::Left); | |||
let res = markdown_to_html("# Hello", &context).unwrap(); | |||
assert_eq!( | |||
res, | |||
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello</h1>\n" | |||
); | |||
} | |||
#[test] | |||
fn can_insert_anchor_right() { | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::Right); | |||
let res = markdown_to_html("# Hello", &context).unwrap(); | |||
assert_eq!( | |||
res, | |||
"<h1 id=\"hello\"><a class=\"anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello</h1>\n" | |||
"<h1 id=\"hello\">Hello<a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\n</h1>\n" | |||
); | |||
} | |||
// See https://github.com/Keats/gutenberg/issues/42 | |||
#[test] | |||
fn test_markdown_to_html_insert_anchor_with_exclamation_mark() { | |||
let mut config = Config::default(); | |||
config.insert_anchor_links = Some(true); | |||
let res = markdown_to_html("# Hello!", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap(); | |||
fn can_insert_anchor_with_exclamation_mark() { | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::Left); | |||
let res = markdown_to_html("# Hello!", &context).unwrap(); | |||
assert_eq!( | |||
res, | |||
"<h1 id=\"hello\"><a class=\"anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello!</h1>\n" | |||
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello!</h1>\n" | |||
); | |||
} | |||
// See https://github.com/Keats/gutenberg/issues/53 | |||
#[test] | |||
fn test_markdown_to_html_insert_anchor_with_link() { | |||
let mut config = Config::default(); | |||
config.insert_anchor_links = Some(true); | |||
let res = markdown_to_html("## [](#xresources)Xresources", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap(); | |||
fn can_insert_anchor_with_link() { | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::Left); | |||
let res = markdown_to_html("## [](#xresources)Xresources", &context).unwrap(); | |||
assert_eq!( | |||
res, | |||
"<h2 id=\"xresources\"><a class=\"anchor\" href=\"#xresources\" aria-label=\"Anchor link for: xresources\">đź”—</a>\nXresources</h2>\n" | |||
"<h2 id=\"xresources\"><a class=\"gutenberg-anchor\" href=\"#xresources\" aria-label=\"Anchor link for: xresources\">đź”—</a>\nXresources</h2>\n" | |||
); | |||
} | |||
#[test] | |||
fn test_markdown_to_html_insert_anchor_with_other_special_chars() { | |||
let mut config = Config::default(); | |||
config.insert_anchor_links = Some(true); | |||
let res = markdown_to_html("# Hello*_()", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap(); | |||
fn can_insert_anchor_with_other_special_chars() { | |||
let permalinks_ctx = HashMap::new(); | |||
let config_ctx = Config::default(); | |||
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::Left); | |||
let res = markdown_to_html("# Hello*_()", &context).unwrap(); | |||
assert_eq!( | |||
res, | |||
"<h1 id=\"hello\"><a class=\"anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello*_()</h1>\n" | |||
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello*_()</h1>\n" | |||
); | |||
} | |||
} |
@@ -0,0 +1,4 @@ | |||
pub mod highlighting; | |||
pub mod markdown; | |||
pub mod short_code; | |||
pub mod context; |
@@ -0,0 +1,101 @@ | |||
use std::collections::HashMap; | |||
use regex::Regex; | |||
use tera::{Tera, Context}; | |||
use errors::{Result, ResultExt}; | |||
lazy_static!{ | |||
static ref SHORTCODE_RE: Regex = Regex::new(r#"\{(?:%|\{)\s+([[:alnum:]]+?)\(([[:alnum:]]+?="?.+?"?)\)\s+(?:%|\})\}"#).unwrap(); | |||
} | |||
/// A shortcode that has a body | |||
/// Called by having some content like {% ... %} body {% end %} | |||
/// We need the struct to hold the data while we're processing the markdown | |||
#[derive(Debug)] | |||
pub struct ShortCode { | |||
name: String, | |||
args: HashMap<String, String>, | |||
body: String, | |||
} | |||
impl ShortCode { | |||
pub fn new(name: &str, args: HashMap<String, String>) -> ShortCode { | |||
ShortCode { | |||
name: name.to_string(), | |||
args: args, | |||
body: String::new(), | |||
} | |||
} | |||
pub fn append(&mut self, text: &str) { | |||
self.body.push_str(text) | |||
} | |||
pub fn render(&self, tera: &Tera) -> Result<String> { | |||
let mut context = Context::new(); | |||
for (key, value) in &self.args { | |||
context.add(key, value); | |||
} | |||
context.add("body", &self.body); | |||
let tpl_name = format!("shortcodes/{}.html", self.name); | |||
tera.render(&tpl_name, &context) | |||
.chain_err(|| format!("Failed to render {} shortcode", self.name)) | |||
} | |||
} | |||
/// Parse a shortcode without a body | |||
pub fn parse_shortcode(input: &str) -> (String, HashMap<String, String>) { | |||
let mut args = HashMap::new(); | |||
let caps = SHORTCODE_RE.captures(input).unwrap(); | |||
// caps[0] is the full match | |||
let name = &caps[1]; | |||
let arg_list = &caps[2]; | |||
for arg in arg_list.split(',') { | |||
let bits = arg.split('=').collect::<Vec<_>>(); | |||
args.insert(bits[0].trim().to_string(), bits[1].replace("\"", "")); | |||
} | |||
(name.to_string(), args) | |||
} | |||
/// Renders a shortcode or return an error | |||
pub fn render_simple_shortcode(tera: &Tera, name: &str, args: &HashMap<String, String>) -> Result<String> { | |||
let mut context = Context::new(); | |||
for (key, value) in args.iter() { | |||
context.add(key, value); | |||
} | |||
let tpl_name = format!("shortcodes/{}.html", name); | |||
tera.render(&tpl_name, &context).chain_err(|| format!("Failed to render {} shortcode", name)) | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use super::{parse_shortcode}; | |||
#[test] | |||
fn can_parse_simple_shortcode_one_arg() { | |||
let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc") }}"#); | |||
assert_eq!(name, "youtube"); | |||
assert_eq!(args["id"], "w7Ft2ymGmfc"); | |||
} | |||
#[test] | |||
fn can_parse_simple_shortcode_several_arg() { | |||
let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc", autoplay=true) }}"#); | |||
assert_eq!(name, "youtube"); | |||
assert_eq!(args["id"], "w7Ft2ymGmfc"); | |||
assert_eq!(args["autoplay"], "true"); | |||
} | |||
#[test] | |||
fn can_parse_block_shortcode_several_arg() { | |||
let (name, args) = parse_shortcode(r#"{% youtube(id="w7Ft2ymGmfc", autoplay=true) %}"#); | |||
assert_eq!(name, "youtube"); | |||
assert_eq!(args["id"], "w7Ft2ymGmfc"); | |||
assert_eq!(args["autoplay"], "true"); | |||
} | |||
} |
@@ -1,47 +1,24 @@ | |||
use std::collections::{HashMap}; | |||
use std::iter::FromIterator; | |||
use std::collections::HashMap; | |||
use std::fs::{remove_dir_all, copy, create_dir_all}; | |||
use std::path::{Path, PathBuf}; | |||
use glob::glob; | |||
use tera::{Tera, Context}; | |||
use slug::slugify; | |||
use walkdir::WalkDir; | |||
use errors::{Result, ResultExt}; | |||
use config::{Config, get_config}; | |||
use utils::{create_file, create_directory}; | |||
use content::{Page, Section, Paginator, SortBy, populate_previous_and_next_pages, sort_pages}; | |||
use fs::{create_file, create_directory, ensure_directory_exists}; | |||
use content::{Page, Section, Paginator, SortBy, Taxonomy, populate_previous_and_next_pages, sort_pages}; | |||
use templates::{GUTENBERG_TERA, global_fns, render_redirect_template}; | |||
use front_matter::InsertAnchor; | |||
#[derive(Debug, PartialEq)] | |||
enum RenderList { | |||
Tags, | |||
Categories, | |||
} | |||
/// A tag or category | |||
#[derive(Debug, Serialize, PartialEq)] | |||
struct ListItem { | |||
name: String, | |||
slug: String, | |||
count: usize, | |||
} | |||
impl ListItem { | |||
pub fn new(name: &str, count: usize) -> ListItem { | |||
ListItem { | |||
name: name.to_string(), | |||
slug: slugify(name), | |||
count: count, | |||
} | |||
} | |||
} | |||
#[derive(Debug)] | |||
pub struct Site { | |||
/// The base path of the gutenberg site | |||
pub base_path: PathBuf, | |||
/// The parsed config for the site | |||
pub config: Config, | |||
pub pages: HashMap<PathBuf, Page>, | |||
pub sections: HashMap<PathBuf, Section>, | |||
@@ -49,8 +26,8 @@ pub struct Site { | |||
live_reload: bool, | |||
output_path: PathBuf, | |||
static_path: PathBuf, | |||
pub tags: HashMap<String, Vec<PathBuf>>, | |||
pub categories: HashMap<String, Vec<PathBuf>>, | |||
pub tags: Option<Taxonomy>, | |||
pub categories: Option<Taxonomy>, | |||
/// A map of all .md files (section and pages) and their permalink | |||
/// We need that if there are relative links in the content that need to be resolved | |||
pub permalinks: HashMap<String, String>, | |||
@@ -75,8 +52,8 @@ impl Site { | |||
live_reload: false, | |||
output_path: path.join("public"), | |||
static_path: path.join("static"), | |||
tags: HashMap::new(), | |||
categories: HashMap::new(), | |||
tags: None, | |||
categories: None, | |||
permalinks: HashMap::new(), | |||
}; | |||
@@ -88,15 +65,6 @@ impl Site { | |||
self.live_reload = true; | |||
} | |||
/// Gets the path of all ignored pages in the site | |||
/// Used for reporting them in the CLI | |||
pub fn get_ignored_pages(&self) -> Vec<PathBuf> { | |||
self.sections | |||
.values() | |||
.flat_map(|s| s.ignored_pages.iter().map(|p| p.file_path.clone())) | |||
.collect() | |||
} | |||
/// Get all the orphan (== without section) pages in the site | |||
pub fn get_all_orphan_pages(&self) -> Vec<&Page> { | |||
let mut pages_in_sections = vec![]; | |||
@@ -107,7 +75,7 @@ impl Site { | |||
} | |||
for page in self.pages.values() { | |||
if !pages_in_sections.contains(&page.file_path) { | |||
if !pages_in_sections.contains(&page.file.path) { | |||
orphans.push(page); | |||
} | |||
} | |||
@@ -115,17 +83,6 @@ impl Site { | |||
orphans | |||
} | |||
/// Finds the section that contains the page given if there is one | |||
pub fn find_parent_section(&self, page: &Page) -> Option<&Section> { | |||
for section in self.sections.values() { | |||
if section.is_child_page(page) { | |||
return Some(section) | |||
} | |||
} | |||
None | |||
} | |||
/// Used by tests to change the output path to a tmp dir | |||
#[doc(hidden)] | |||
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) { | |||
@@ -146,8 +103,8 @@ impl Site { | |||
self.add_page(path, false)?; | |||
} | |||
} | |||
// Insert a default index section so we don't need to create a _index.md to render | |||
// the index page | |||
// Insert a default index section if necessary so we don't need to create | |||
// a _index.md to render the index page | |||
let index_path = self.base_path.join("content").join("_index.md"); | |||
if !self.sections.contains_key(&index_path) { | |||
let mut index_section = Section::default(); | |||
@@ -155,9 +112,16 @@ impl Site { | |||
self.sections.insert(index_path, index_section); | |||
} | |||
// Silly thing needed to make the borrow checker happy | |||
let mut pages_insert_anchors = HashMap::new(); | |||
for page in self.pages.values() { | |||
pages_insert_anchors.insert(page.file.path.clone(), self.find_parent_section_insert_anchor(&page.file.parent.clone())); | |||
} | |||
// TODO: make that parallel | |||
for page in self.pages.values_mut() { | |||
page.render_markdown(&self.permalinks, &self.tera, &self.config)?; | |||
let insert_anchor = pages_insert_anchors[&page.file.path]; | |||
page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?; | |||
} | |||
// TODO: make that parallel | |||
for section in self.sections.values_mut() { | |||
@@ -168,22 +132,30 @@ impl Site { | |||
self.populate_tags_and_categories(); | |||
self.tera.register_global_function("get_page", global_fns::make_get_page(&self.pages)); | |||
self.tera.register_global_function("get_section", global_fns::make_get_section(&self.sections)); | |||
self.register_get_url_fn(); | |||
Ok(()) | |||
} | |||
/// Separate fn as it can be called in the serve command | |||
pub fn register_get_url_fn(&mut self) { | |||
self.tera.register_global_function("get_url", global_fns::make_get_url(self.permalinks.clone())); | |||
} | |||
/// Add a page to the site | |||
/// The `render` parameter is used in the serve command, when rebuilding a page. | |||
/// If `true`, it will also render the markdown for that page | |||
/// Returns the previous page struct if there was one | |||
pub fn add_page(&mut self, path: &Path, render: bool) -> Result<Option<Page>> { | |||
let page = Page::from_file(&path, &self.config)?; | |||
self.permalinks.insert(page.relative_path.clone(), page.permalink.clone()); | |||
let prev = self.pages.insert(page.file_path.clone(), page); | |||
self.permalinks.insert(page.file.relative.clone(), page.permalink.clone()); | |||
let prev = self.pages.insert(page.file.path.clone(), page); | |||
if render { | |||
let insert_anchor = self.find_parent_section_insert_anchor(&self.pages[path].file.parent); | |||
let mut page = self.pages.get_mut(path).unwrap(); | |||
page.render_markdown(&self.permalinks, &self.tera, &self.config)?; | |||
page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?; | |||
} | |||
Ok(prev) | |||
@@ -192,11 +164,11 @@ impl Site { | |||
/// Add a section to the site | |||
/// The `render` parameter is used in the serve command, when rebuilding a page. | |||
/// If `true`, it will also render the markdown for that page | |||
/// Returns the previous page struct if there was one | |||
/// Returns the previous section struct if there was one | |||
pub fn add_section(&mut self, path: &Path, render: bool) -> Result<Option<Section>> { | |||
let section = Section::from_file(path, &self.config)?; | |||
self.permalinks.insert(section.relative_path.clone(), section.permalink.clone()); | |||
let prev = self.sections.insert(section.file_path.clone(), section); | |||
self.permalinks.insert(section.file.relative.clone(), section.permalink.clone()); | |||
let prev = self.sections.insert(section.file.path.clone(), section); | |||
if render { | |||
let mut section = self.sections.get_mut(path).unwrap(); | |||
@@ -206,12 +178,21 @@ impl Site { | |||
Ok(prev) | |||
} | |||
/// Finds the insert_anchor for the parent section of the directory at `path`. | |||
/// Defaults to `AnchorInsert::None` if no parent section found | |||
pub fn find_parent_section_insert_anchor(&self, parent_path: &PathBuf) -> InsertAnchor { | |||
match self.sections.get(&parent_path.join("_index.md")) { | |||
Some(s) => s.meta.insert_anchor.unwrap(), | |||
None => InsertAnchor::None | |||
} | |||
} | |||
/// Find out the direct subsections of each subsection if there are some | |||
/// as well as the pages for each section | |||
pub fn populate_sections(&mut self) { | |||
let mut grandparent_paths = HashMap::new(); | |||
for section in self.sections.values_mut() { | |||
if let Some(grand_parent) = section.parent_path.parent() { | |||
if let Some(ref grand_parent) = section.file.grand_parent { | |||
grandparent_paths.entry(grand_parent.to_path_buf()).or_insert_with(|| vec![]).push(section.clone()); | |||
} | |||
// Make sure the pages of a section are empty since we can call that many times on `serve` | |||
@@ -220,13 +201,14 @@ impl Site { | |||
} | |||
for page in self.pages.values() { | |||
if self.sections.contains_key(&page.parent_path.join("_index.md")) { | |||
self.sections.get_mut(&page.parent_path.join("_index.md")).unwrap().pages.push(page.clone()); | |||
let parent_section_path = page.file.parent.join("_index.md"); | |||
if self.sections.contains_key(&parent_section_path) { | |||
self.sections.get_mut(&parent_section_path).unwrap().pages.push(page.clone()); | |||
} | |||
} | |||
for section in self.sections.values_mut() { | |||
match grandparent_paths.get(§ion.parent_path) { | |||
match grandparent_paths.get(§ion.file.parent) { | |||
Some(paths) => section.subsections.extend(paths.clone()), | |||
None => continue, | |||
}; | |||
@@ -250,24 +232,23 @@ impl Site { | |||
} | |||
} | |||
/// Separated from `parse` for easier testing | |||
/// Find all the tags and categories if it's asked in the config | |||
pub fn populate_tags_and_categories(&mut self) { | |||
for page in self.pages.values() { | |||
if let Some(ref category) = page.meta.category { | |||
self.categories | |||
.entry(category.to_string()) | |||
.or_insert_with(|| vec![]) | |||
.push(page.file_path.clone()); | |||
} | |||
let generate_tags_pages = self.config.generate_tags_pages.unwrap(); | |||
let generate_categories_pages = self.config.generate_categories_pages.unwrap(); | |||
if !generate_tags_pages && !generate_categories_pages { | |||
return; | |||
} | |||
if let Some(ref tags) = page.meta.tags { | |||
for tag in tags { | |||
self.tags | |||
.entry(tag.to_string()) | |||
.or_insert_with(|| vec![]) | |||
.push(page.file_path.clone()); | |||
} | |||
} | |||
// TODO: can we pass a reference? | |||
let (tags, categories) = Taxonomy::find_tags_and_categories( | |||
self.pages.values().cloned().collect::<Vec<_>>() | |||
); | |||
if generate_tags_pages { | |||
self.tags = Some(tags); | |||
} | |||
if generate_categories_pages { | |||
self.categories = Some(categories); | |||
} | |||
} | |||
@@ -283,14 +264,6 @@ impl Site { | |||
html | |||
} | |||
fn ensure_public_directory_exists(&self) -> Result<()> { | |||
let public = self.output_path.clone(); | |||
if !public.exists() { | |||
create_directory(&public)?; | |||
} | |||
Ok(()) | |||
} | |||
/// Copy static file to public directory. | |||
pub fn copy_static_file<P: AsRef<Path>>(&self, path: P) -> Result<()> { | |||
let relative_path = path.as_ref().strip_prefix(&self.static_path).unwrap(); | |||
@@ -332,7 +305,7 @@ impl Site { | |||
/// Renders a single content page | |||
pub fn render_page(&self, page: &Page) -> Result<()> { | |||
self.ensure_public_directory_exists()?; | |||
ensure_directory_exists(&self.output_path)?; | |||
// Copy the nesting of the content directory if we have sections for that page | |||
let mut current_path = self.output_path.to_path_buf(); | |||
@@ -350,7 +323,7 @@ impl Site { | |||
// Finally, create a index.html file there with the page rendered | |||
let output = page.render_html(&self.tera, &self.config)?; | |||
create_file(current_path.join("index.html"), &self.inject_livereload(output))?; | |||
create_file(¤t_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 { | |||
@@ -361,7 +334,7 @@ impl Site { | |||
Ok(()) | |||
} | |||
/// Builds the site to the `public` directory after deleting it | |||
/// Deletes the `public` directory and builds the site | |||
pub fn build(&self) -> Result<()> { | |||
self.clean()?; | |||
self.render_sections()?; | |||
@@ -381,98 +354,45 @@ impl Site { | |||
/// Renders robots.txt | |||
pub fn render_robots(&self) -> Result<()> { | |||
self.ensure_public_directory_exists()?; | |||
ensure_directory_exists(&self.output_path)?; | |||
create_file( | |||
self.output_path.join("robots.txt"), | |||
&self.output_path.join("robots.txt"), | |||
&self.tera.render("robots.txt", &Context::new())? | |||
) | |||
} | |||
/// Renders all categories if the config allows it | |||
/// Renders all categories and the single category pages if there are some | |||
pub fn render_categories(&self) -> Result<()> { | |||
if self.config.generate_categories_pages.unwrap() { | |||
self.render_categories_and_tags(RenderList::Categories) | |||
} else { | |||
Ok(()) | |||
if let Some(ref categories) = self.categories { | |||
self.render_taxonomy(categories)?; | |||
} | |||
Ok(()) | |||
} | |||
/// Renders all tags if the config allows it | |||
/// Renders all tags and the single tag pages if there are some | |||
pub fn render_tags(&self) -> Result<()> { | |||
if self.config.generate_tags_pages.unwrap() { | |||
self.render_categories_and_tags(RenderList::Tags) | |||
} else { | |||
Ok(()) | |||
if let Some(ref tags) = self.tags { | |||
self.render_taxonomy(tags)?; | |||
} | |||
} | |||
/// Render the /{categories, list} pages and each individual category/tag page | |||
/// They are the same thing fundamentally, a list of pages with something in common | |||
/// TODO: revisit this function, lots of things have changed since then | |||
fn render_categories_and_tags(&self, kind: RenderList) -> Result<()> { | |||
let items = match kind { | |||
RenderList::Categories => &self.categories, | |||
RenderList::Tags => &self.tags, | |||
}; | |||
if items.is_empty() { | |||
return Ok(()); | |||
} | |||
Ok(()) | |||
} | |||
let (list_tpl_name, single_tpl_name, name, var_name) = if kind == RenderList::Categories { | |||
("categories.html", "category.html", "categories", "category") | |||
} else { | |||
("tags.html", "tag.html", "tags", "tag") | |||
}; | |||
self.ensure_public_directory_exists()?; | |||
fn render_taxonomy(&self, taxonomy: &Taxonomy) -> Result<()> { | |||
ensure_directory_exists(&self.output_path)?; | |||
// Create the categories/tags directory first | |||
let public = self.output_path.clone(); | |||
let mut output_path = public.to_path_buf(); | |||
output_path.push(name); | |||
let output_path = self.output_path.join(&taxonomy.get_list_name()); | |||
let list_output = taxonomy.render_list(&self.tera, &self.config)?; | |||
create_directory(&output_path)?; | |||
create_file(&output_path.join("index.html"), &self.inject_livereload(list_output))?; | |||
// Then render the index page for that kind. | |||
// We sort by number of page in that category/tag | |||
let mut sorted_items = vec![]; | |||
for (item, count) in Vec::from_iter(items).into_iter().map(|(a, b)| (a, b.len())) { | |||
sorted_items.push(ListItem::new(item, count)); | |||
} | |||
sorted_items.sort_by(|a, b| b.count.cmp(&a.count)); | |||
let mut context = Context::new(); | |||
context.add(name, &sorted_items); | |||
context.add("config", &self.config); | |||
context.add("current_url", &self.config.make_permalink(name)); | |||
context.add("current_path", &format!("/{}", name)); | |||
// And render it immediately | |||
let list_output = self.tera.render(list_tpl_name, &context)?; | |||
create_file(output_path.join("index.html"), &self.inject_livereload(list_output))?; | |||
// Now, each individual item | |||
for (item_name, pages_paths) in items.iter() { | |||
let pages: Vec<&Page> = self.pages | |||
.iter() | |||
.filter(|&(path, _)| pages_paths.contains(path)) | |||
.map(|(_, page)| page) | |||
.collect(); | |||
// TODO: how to sort categories and tag content? | |||
// Have a setting in config.toml or a _category.md and _tag.md | |||
// The latter is more in line with the rest of Gutenberg but order ordering | |||
// doesn't really work across sections. | |||
let mut context = Context::new(); | |||
let slug = slugify(&item_name); | |||
context.add(var_name, &item_name); | |||
context.add(&format!("{}_slug", var_name), &slug); | |||
context.add("pages", &pages); | |||
context.add("config", &self.config); | |||
context.add("current_url", &self.config.make_permalink(&format!("{}/{}", name, slug))); | |||
context.add("current_path", &format!("/{}/{}", name, slug)); | |||
let single_output = self.tera.render(single_tpl_name, &context)?; | |||
create_directory(&output_path.join(&slug))?; | |||
for item in &taxonomy.items { | |||
let single_output = taxonomy.render_single_item(item, &self.tera, &self.config)?; | |||
create_directory(&output_path.join(&item.slug))?; | |||
create_file( | |||
output_path.join(&slug).join("index.html"), | |||
&output_path.join(&item.slug).join("index.html"), | |||
&self.inject_livereload(single_output) | |||
)?; | |||
} | |||
@@ -482,28 +402,31 @@ impl Site { | |||
/// What it says on the tin | |||
pub fn render_sitemap(&self) -> Result<()> { | |||
self.ensure_public_directory_exists()?; | |||
ensure_directory_exists(&self.output_path)?; | |||
let mut context = Context::new(); | |||
context.add("pages", &self.pages.values().collect::<Vec<&Page>>()); | |||
context.add("sections", &self.sections.values().collect::<Vec<&Section>>()); | |||
let mut categories = vec![]; | |||
if self.config.generate_categories_pages.unwrap() && !self.categories.is_empty() { | |||
categories.push(self.config.make_permalink("categories")); | |||
for category in self.categories.keys() { | |||
if let Some(ref c) = self.categories { | |||
let name = c.get_list_name(); | |||
categories.push(self.config.make_permalink(&name)); | |||
for item in &c.items { | |||
categories.push( | |||
self.config.make_permalink(&format!("categories/{}", slugify(category))) | |||
self.config.make_permalink(&format!("{}/{}", &name, item.slug)) | |||
); | |||
} | |||
} | |||
context.add("categories", &categories); | |||
let mut tags = vec![]; | |||
if self.config.generate_tags_pages.unwrap() && !self.tags.is_empty() { | |||
tags.push(self.config.make_permalink("tags")); | |||
for tag in self.tags.keys() { | |||
if let Some(ref t) = self.tags { | |||
let name = t.get_list_name(); | |||
tags.push(self.config.make_permalink(&name)); | |||
for item in &t.items { | |||
tags.push( | |||
self.config.make_permalink(&format!("tags/{}", slugify(tag))) | |||
self.config.make_permalink(&format!("{}/{}", &name, item.slug)) | |||
); | |||
} | |||
} | |||
@@ -511,18 +434,18 @@ impl Site { | |||
let sitemap = self.tera.render("sitemap.xml", &context)?; | |||
create_file(self.output_path.join("sitemap.xml"), &sitemap)?; | |||
create_file(&self.output_path.join("sitemap.xml"), &sitemap)?; | |||
Ok(()) | |||
} | |||
pub fn render_rss_feed(&self) -> Result<()> { | |||
self.ensure_public_directory_exists()?; | |||
ensure_directory_exists(&self.output_path)?; | |||
let mut context = Context::new(); | |||
let pages = self.pages.values() | |||
.filter(|p| p.meta.date.is_some()) | |||
.take(15) // limit to the last 15 elements | |||
.take(self.config.rss_limit.unwrap()) // limit to the last n elements | |||
.cloned() | |||
.collect::<Vec<Page>>(); | |||
@@ -544,7 +467,7 @@ impl Site { | |||
let sitemap = self.tera.render("rss.xml", &context)?; | |||
create_file(self.output_path.join("rss.xml"), &sitemap)?; | |||
create_file(&self.output_path.join("rss.xml"), &sitemap)?; | |||
Ok(()) | |||
} | |||
@@ -554,17 +477,17 @@ impl Site { | |||
fn get_sections_map(&self) -> HashMap<String, Section> { | |||
self.sections | |||
.values() | |||
.map(|s| (s.components.join("/"), s.clone())) | |||
.map(|s| (s.file.components.join("/"), s.clone())) | |||
.collect() | |||
} | |||
/// Renders a single section | |||
pub fn render_section(&self, section: &Section, render_pages: bool) -> Result<()> { | |||
self.ensure_public_directory_exists()?; | |||
ensure_directory_exists(&self.output_path)?; | |||
let public = self.output_path.clone(); | |||
let mut output_path = public.to_path_buf(); | |||
for component in §ion.components { | |||
for component in §ion.file.components { | |||
output_path.push(component); | |||
if !output_path.exists() { | |||
@@ -590,7 +513,7 @@ impl Site { | |||
&self.tera, | |||
&self.config, | |||
)?; | |||
create_file(output_path.join("index.html"), &self.inject_livereload(output))?; | |||
create_file(&output_path.join("index.html"), &self.inject_livereload(output))?; | |||
} | |||
Ok(()) | |||
@@ -610,7 +533,7 @@ impl Site { | |||
/// Renders all pages that do not belong to any sections | |||
pub fn render_orphan_pages(&self) -> Result<()> { | |||
self.ensure_public_directory_exists()?; | |||
ensure_directory_exists(&self.output_path)?; | |||
for page in self.get_all_orphan_pages() { | |||
self.render_page(page)?; | |||
@@ -621,7 +544,7 @@ impl Site { | |||
/// Renders a list of pages when the section/index is wanting pagination. | |||
fn render_paginated(&self, output_path: &Path, section: &Section) -> Result<()> { | |||
self.ensure_public_directory_exists()?; | |||
ensure_directory_exists(&self.output_path)?; | |||
let paginate_path = match section.meta.paginate_path { | |||
Some(ref s) => s.clone(), | |||
@@ -636,13 +559,63 @@ impl Site { | |||
create_directory(&page_path)?; | |||
let output = paginator.render_pager(pager, self)?; | |||
if i > 0 { | |||
create_file(page_path.join("index.html"), &self.inject_livereload(output))?; | |||
create_file(&page_path.join("index.html"), &self.inject_livereload(output))?; | |||
} else { | |||
create_file(output_path.join("index.html"), &self.inject_livereload(output))?; | |||
create_file(page_path.join("index.html"), &render_redirect_template(§ion.permalink, &self.tera)?)?; | |||
create_file(&output_path.join("index.html"), &self.inject_livereload(output))?; | |||
create_file(&page_path.join("index.html"), &render_redirect_template(§ion.permalink, &self.tera)?)?; | |||
} | |||
} | |||
Ok(()) | |||
} | |||
} | |||
/// Resolves an internal link (of the `./posts/something.md#hey` sort) to its absolute link | |||
pub fn resolve_internal_link(link: &str, permalinks: &HashMap<String, String>) -> Result<String> { | |||
// First we remove the ./ since that's gutenberg specific | |||
let clean_link = link.replacen("./", "", 1); | |||
// Then we remove any potential anchor | |||
// parts[0] will be the file path and parts[1] the anchor if present | |||
let parts = clean_link.split('#').collect::<Vec<_>>(); | |||
match permalinks.get(parts[0]) { | |||
Some(p) => { | |||
if parts.len() > 1 { | |||
Ok(format!("{}#{}", p, parts[1])) | |||
} else { | |||
Ok(p.to_string()) | |||
} | |||
}, | |||
None => bail!(format!("Relative link {} not found.", link)), | |||
} | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use std::collections::HashMap; | |||
use super::resolve_internal_link; | |||
#[test] | |||
fn can_resolve_valid_internal_link() { | |||
let mut permalinks = HashMap::new(); | |||
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); | |||
let res = resolve_internal_link("./pages/about.md", &permalinks).unwrap(); | |||
assert_eq!(res, "https://vincent.is/about"); | |||
} | |||
#[test] | |||
fn can_resolve_internal_links_with_anchors() { | |||
let mut permalinks = HashMap::new(); | |||
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); | |||
let res = resolve_internal_link("./pages/about.md#hello", &permalinks).unwrap(); | |||
assert_eq!(res, "https://vincent.is/about#hello"); | |||
} | |||
#[test] | |||
fn errors_resolve_inexistant_internal_link() { | |||
let res = resolve_internal_link("./pages/about.md#hello", &HashMap::new()); | |||
assert!(res.is_err()); | |||
} | |||
} |
@@ -1 +1 @@ | |||
<a class="anchor" href="#{{ id }}" aria-label="Anchor link for: {{ id }}">đź”—</a> | |||
<a class="gutenberg-anchor" href="#{{ id }}" aria-label="Anchor link for: {{ id }}">đź”—</a> |
@@ -45,14 +45,14 @@ mod tests { | |||
use super::{markdown, base64_decode, base64_encode}; | |||
#[test] | |||
fn test_markdown() { | |||
fn markdown_filter() { | |||
let result = markdown(to_value(&"# Hey").unwrap(), HashMap::new()); | |||
assert!(result.is_ok()); | |||
assert_eq!(result.unwrap(), to_value(&"<h1>Hey</h1>\n").unwrap()); | |||
} | |||
#[test] | |||
fn test_base64_encode() { | |||
fn base64_encode_filter() { | |||
// from https://tools.ietf.org/html/rfc4648#section-10 | |||
let tests = vec![ | |||
("", ""), | |||
@@ -73,7 +73,7 @@ mod tests { | |||
#[test] | |||
fn test_base64_decode() { | |||
fn base64_decode_filter() { | |||
let tests = vec![ | |||
("", ""), | |||
("Zg==", "f"), | |||
@@ -3,13 +3,14 @@ use std::path::{PathBuf}; | |||
use tera::{GlobalFn, Value, from_value, to_value, Result}; | |||
use content::Page; | |||
use content::{Page, Section}; | |||
use site::resolve_internal_link; | |||
pub fn make_get_page(all_pages: &HashMap<PathBuf, Page>) -> GlobalFn { | |||
let mut pages = HashMap::new(); | |||
for page in all_pages.values() { | |||
pages.insert(page.relative_path.clone(), page.clone()); | |||
pages.insert(page.file.relative.clone(), page.clone()); | |||
} | |||
Box::new(move |args| -> Result<Value> { | |||
@@ -27,3 +28,40 @@ pub fn make_get_page(all_pages: &HashMap<PathBuf, Page>) -> GlobalFn { | |||
} | |||
}) | |||
} | |||
pub fn make_get_section(all_sections: &HashMap<PathBuf, Section>) -> GlobalFn { | |||
let mut sections = HashMap::new(); | |||
for section in all_sections.values() { | |||
sections.insert(section.file.relative.clone(), section.clone()); | |||
} | |||
Box::new(move |args| -> Result<Value> { | |||
match args.get("path") { | |||
Some(val) => match from_value::<String>(val.clone()) { | |||
Ok(v) => { | |||
match sections.get(&v) { | |||
Some(p) => Ok(to_value(p).unwrap()), | |||
None => Err(format!("Section `{}` not found.", v).into()) | |||
} | |||
}, | |||
Err(_) => Err(format!("`get_section` received path={:?} but it requires a string", val).into()), | |||
}, | |||
None => Err("`get_section` requires a `path` argument.".into()), | |||
} | |||
}) | |||
} | |||
pub fn make_get_url(permalinks: HashMap<String, String>,) -> GlobalFn { | |||
Box::new(move |args| -> Result<Value> { | |||
match args.get("link") { | |||
Some(val) => match from_value::<String>(val.clone()) { | |||
Ok(v) => match resolve_internal_link(&v, &permalinks) { | |||
Ok(url) => Ok(to_value(url).unwrap()), | |||
Err(_) => Err(format!("Could not resolve URL for link `{}` not found.", v).into()) | |||
}, | |||
Err(_) => Err(format!("`get_url` received link={:?} but it requires a string", val).into()), | |||
}, | |||
None => Err("`get_url` requires a `link` argument.".into()), | |||
} | |||
}) | |||
} |
@@ -1,69 +0,0 @@ | |||
use std::io::prelude::*; | |||
use std::fs::{File, create_dir}; | |||
use std::path::Path; | |||
use errors::{Result, ResultExt}; | |||
pub fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> { | |||
let mut file = File::create(&path)?; | |||
file.write_all(content.as_bytes())?; | |||
Ok(()) | |||
} | |||
/// Very similar to `create_dir` from the std except it checks if the folder | |||
/// exists before creating it | |||
pub fn create_directory<P: AsRef<Path>>(path: P) -> Result<()> { | |||
let path = path.as_ref(); | |||
if !path.exists() { | |||
create_dir(path) | |||
.chain_err(|| format!("Was not able to create folder {}", path.display()))?; | |||
} | |||
Ok(()) | |||
} | |||
/// Return the content of a file, with error handling added | |||
pub fn read_file<P: AsRef<Path>>(path: P) -> Result<String> { | |||
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 first `content` directory | |||
/// Will not return the filename as last component | |||
pub fn find_content_components<P: AsRef<Path>>(path: P) -> Vec<String> { | |||
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()]); | |||
} | |||
} |
@@ -5,9 +5,10 @@ name: Jinja2 | |||
file_extensions: | |||
- j2 | |||
- jinja2 | |||
scope: source.jinja2 | |||
scope: text.html.jinja2 | |||
contexts: | |||
main: | |||
- include: scope:text.html.basic | |||
- match: '({%)\s*(raw)\s*(%})' | |||
captures: | |||
1: entity.other.jinja2.delimiter.tag |
@@ -2,4 +2,5 @@ | |||
title = "Posts" | |||
paginate_by = 2 | |||
template = "section_paginated.html" | |||
insert_anchor = "left" | |||
+++ |
@@ -8,3 +8,5 @@ date = "2017-01-01" | |||
A simple page with a slug defined | |||
# Title | |||
Hey |
@@ -1,3 +1,3 @@ | |||
{% for category in categories %} | |||
{{ category.name }} {{ category.slug }} {{ category.count }} | |||
{{ category.name }} {{ category.slug }} {{ category.pages | length }} | |||
{% endfor %} |
@@ -1,7 +1,7 @@ | |||
Category: {{ category }} | |||
Category: {{ category.name }} | |||
{% for page in pages %} | |||
{% for page in category.pages %} | |||
<article> | |||
<h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3> | |||
</article> | |||
@@ -1,6 +1,6 @@ | |||
Tag: {{ tag }} | |||
Tag: {{ tag.name }} | |||
{% for page in pages %} | |||
{% for page in tag.pages %} | |||
<article> | |||
<h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3> | |||
</article> | |||
@@ -1,3 +1,3 @@ | |||
{% for tag in tags %} | |||
{{ tag.name }} {{ tag.slug }} {{ tag.count }} | |||
{{ tag.name }} {{ tag.slug }} {{ tag.pages | length }} | |||
{% endfor %} |
@@ -1,251 +0,0 @@ | |||
extern crate gutenberg; | |||
extern crate tera; | |||
extern crate tempdir; | |||
use std::collections::HashMap; | |||
use std::fs::{File, create_dir}; | |||
use std::path::Path; | |||
use tempdir::TempDir; | |||
use tera::Tera; | |||
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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); | |||
assert_eq!(page.meta.title.unwrap(), "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, "<p>Hello world</p>\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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); | |||
assert_eq!(page.path, "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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); | |||
assert_eq!(page.path, "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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); | |||
assert_eq!(page.path, "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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); | |||
assert_eq!(page.slug, "file-with-space"); | |||
assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space")); | |||
} | |||
#[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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); | |||
assert_eq!(page.summary, None); | |||
} | |||
#[test] | |||
fn test_can_specify_summary() { | |||
let content = r#" | |||
+++ | |||
title = "Hello" | |||
description = "hey there" | |||
+++ | |||
Hello world | |||
<!-- more --> | |||
"#.to_string(); | |||
let res = Page::parse(Path::new("hello.md"), &content, &Config::default()); | |||
assert!(res.is_ok()); | |||
let mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); | |||
assert_eq!(page.summary, Some("<p>Hello world</p>\n".to_string())); | |||
} | |||
#[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 mut page = res.unwrap(); | |||
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap(); | |||
assert!(page.content.starts_with("<pre")); | |||
} | |||
#[test] | |||
fn test_page_with_assets_gets_right_parent_path() { | |||
let tmp_dir = TempDir::new("example").expect("create temp dir"); | |||
let path = tmp_dir.path(); | |||
create_dir(&path.join("content")).expect("create content temp dir"); | |||
create_dir(&path.join("content").join("posts")).expect("create posts temp dir"); | |||
let nested_path = path.join("content").join("posts").join("assets"); | |||
create_dir(&nested_path).expect("create nested temp dir"); | |||
File::create(nested_path.join("index.md")).unwrap(); | |||
File::create(nested_path.join("example.js")).unwrap(); | |||
File::create(nested_path.join("graph.jpg")).unwrap(); | |||
File::create(nested_path.join("fail.png")).unwrap(); | |||
let res = Page::parse( | |||
nested_path.join("index.md").as_path(), | |||
"+++\nurl=\"hey\"+++\n", | |||
&Config::default() | |||
); | |||
assert!(res.is_ok()); | |||
let page = res.unwrap(); | |||
assert_eq!(page.parent_path, path.join("content").join("posts")); | |||
} | |||
#[test] | |||
fn test_file_not_named_index_with_assets() { | |||
let tmp_dir = TempDir::new("example").expect("create temp dir"); | |||
File::create(tmp_dir.path().join("something.md")).unwrap(); | |||
File::create(tmp_dir.path().join("example.js")).unwrap(); | |||
File::create(tmp_dir.path().join("graph.jpg")).unwrap(); | |||
File::create(tmp_dir.path().join("fail.png")).unwrap(); | |||
let page = Page::from_file(tmp_dir.path().join("something.md"), &Config::default()); | |||
assert!(page.is_err()); | |||
} |
@@ -12,7 +12,7 @@ use gutenberg::{Site}; | |||
#[test] | |||
fn test_can_parse_site() { | |||
fn can_parse_site() { | |||
let mut path = env::current_dir().unwrap().to_path_buf(); | |||
path.push("test_site"); | |||
let mut site = Site::new(&path, "config.toml").unwrap(); | |||
@@ -24,7 +24,7 @@ fn test_can_parse_site() { | |||
// Make sure we remove all the pwd + content from the sections | |||
let basic = &site.pages[&posts_path.join("simple.md")]; | |||
assert_eq!(basic.components, vec!["posts".to_string()]); | |||
assert_eq!(basic.file.components, vec!["posts".to_string()]); | |||
// Make sure the page with a url doesn't have any sections | |||
let url_post = &site.pages[&posts_path.join("fixed-url.md")]; | |||
@@ -32,7 +32,7 @@ fn test_can_parse_site() { | |||
// Make sure the article in a folder with only asset doesn't get counted as a section | |||
let asset_folder_post = &site.pages[&posts_path.join("with-assets").join("index.md")]; | |||
assert_eq!(asset_folder_post.components, vec!["posts".to_string()]); | |||
assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]); | |||
// That we have the right number of sections | |||
assert_eq!(site.sections.len(), 6); | |||
@@ -89,7 +89,7 @@ macro_rules! file_contains { | |||
} | |||
#[test] | |||
fn test_can_build_site_without_live_reload() { | |||
fn 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, "config.toml").unwrap(); | |||
@@ -131,7 +131,7 @@ fn test_can_build_site_without_live_reload() { | |||
} | |||
#[test] | |||
fn test_can_build_site_with_live_reload() { | |||
fn 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, "config.toml").unwrap(); | |||
@@ -169,7 +169,7 @@ fn test_can_build_site_with_live_reload() { | |||
} | |||
#[test] | |||
fn test_can_build_site_with_categories() { | |||
fn 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, "config.toml").unwrap(); | |||
@@ -190,7 +190,7 @@ fn test_can_build_site_with_categories() { | |||
site.build().unwrap(); | |||
assert!(Path::new(&public).exists()); | |||
assert_eq!(site.categories.len(), 2); | |||
assert_eq!(site.categories.unwrap().len(), 2); | |||
assert!(file_exists!(public, "index.html")); | |||
assert!(file_exists!(public, "sitemap.xml")); | |||
@@ -221,7 +221,7 @@ fn test_can_build_site_with_categories() { | |||
} | |||
#[test] | |||
fn test_can_build_site_with_tags() { | |||
fn 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, "config.toml").unwrap(); | |||
@@ -243,7 +243,7 @@ fn test_can_build_site_with_tags() { | |||
site.build().unwrap(); | |||
assert!(Path::new(&public).exists()); | |||
assert_eq!(site.tags.len(), 3); | |||
assert_eq!(site.tags.unwrap().len(), 3); | |||
assert!(file_exists!(public, "index.html")); | |||
assert!(file_exists!(public, "sitemap.xml")); | |||
@@ -273,11 +273,10 @@ fn test_can_build_site_with_tags() { | |||
} | |||
#[test] | |||
fn test_can_build_site_and_insert_anchor_links() { | |||
fn can_build_site_and_insert_anchor_links() { | |||
let mut path = env::current_dir().unwrap().to_path_buf(); | |||
path.push("test_site"); | |||
let mut site = Site::new(&path, "config.toml").unwrap(); | |||
site.config.insert_anchor_links = Some(true); | |||
site.load().unwrap(); | |||
let tmp_dir = TempDir::new("example").expect("create temp dir"); | |||
let public = &tmp_dir.path().join("public"); | |||
@@ -286,11 +285,11 @@ fn test_can_build_site_and_insert_anchor_links() { | |||
assert!(Path::new(&public).exists()); | |||
// anchor link inserted | |||
assert!(file_contains!(public, "posts/something-else/index.html", "<h1 id=\"title\"><a class=\"anchor\" href=\"#title\"")); | |||
assert!(file_contains!(public, "posts/something-else/index.html", "<h1 id=\"title\"><a class=\"gutenberg-anchor\" href=\"#title\"")); | |||
} | |||
#[test] | |||
fn test_can_build_site_with_pagination_for_section() { | |||
fn can_build_site_with_pagination_for_section() { | |||
let mut path = env::current_dir().unwrap().to_path_buf(); | |||
path.push("test_site"); | |||
let mut site = Site::new(&path, "config.toml").unwrap(); | |||
@@ -349,7 +348,7 @@ fn test_can_build_site_with_pagination_for_section() { | |||
} | |||
#[test] | |||
fn test_can_build_site_with_pagination_for_index() { | |||
fn can_build_site_with_pagination_for_index() { | |||
let mut path = env::current_dir().unwrap().to_path_buf(); | |||
path.push("test_site"); | |||
let mut site = Site::new(&path, "config.toml").unwrap(); | |||
@@ -391,5 +390,4 @@ fn test_can_build_site_with_pagination_for_index() { | |||
assert!(file_contains!(public, "index.html", "Last: https://replace-this-with-your-url.com/")); | |||
assert_eq!(file_contains!(public, "index.html", "has_prev"), false); | |||
assert_eq!(file_contains!(public, "index.html", "has_next"), false); | |||
} |