@@ -1,5 +1,15 @@ | |||||
# Changelog | # 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) | ## 0.0.5 (2017-05-15) | ||||
- Fix XML templates overriding and reloading | - Fix XML templates overriding and reloading | ||||
@@ -4,7 +4,7 @@ version = "0.0.5" | |||||
dependencies = [ | dependencies = [ | ||||
"base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"kernel32-sys 0.2.2 (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)", | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
] | ] | ||||
[[package]] | [[package]] | ||||
name = "backtrace" | name = "backtrace" | ||||
version = "0.3.0" | |||||
version = "0.3.2" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"backtrace-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"gcc 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", | "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]] | [[package]] | ||||
@@ -155,7 +155,7 @@ dependencies = [ | |||||
[[package]] | [[package]] | ||||
name = "clap" | name = "clap" | ||||
version = "2.24.1" | |||||
version = "2.24.2" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", | "ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
@@ -163,9 +163,9 @@ dependencies = [ | |||||
"bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "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)", | "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]] | [[package]] | ||||
@@ -212,7 +212,7 @@ name = "error-chain" | |||||
version = "0.10.0" | version = "0.10.0" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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]] | [[package]] | ||||
@@ -220,7 +220,7 @@ name = "filetime" | |||||
version = "0.1.10" | version = "0.1.10" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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]] | [[package]] | ||||
@@ -228,7 +228,7 @@ name = "flate2" | |||||
version = "0.2.19" | version = "0.2.19" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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)", | "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 = [ | dependencies = [ | ||||
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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]] | [[package]] | ||||
@@ -252,7 +252,7 @@ name = "fsevent-sys" | |||||
version = "0.1.6" | version = "0.1.6" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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]] | [[package]] | ||||
@@ -301,11 +301,11 @@ dependencies = [ | |||||
[[package]] | [[package]] | ||||
name = "idna" | name = "idna" | ||||
version = "0.1.1" | |||||
version = "0.1.2" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"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)", | "unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
] | ] | ||||
@@ -314,7 +314,7 @@ name = "inotify" | |||||
version = "0.3.0" | version = "0.3.0" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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]] | [[package]] | ||||
@@ -322,7 +322,7 @@ name = "iovec" | |||||
version = "0.1.0" | version = "0.1.0" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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)", | "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]] | [[package]] | ||||
name = "libc" | name = "libc" | ||||
version = "0.2.22" | |||||
version = "0.2.23" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
[[package]] | [[package]] | ||||
@@ -392,7 +392,7 @@ name = "memchr" | |||||
version = "1.0.1" | version = "1.0.1" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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]] | [[package]] | ||||
@@ -409,7 +409,7 @@ version = "0.1.9" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"gcc 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", | "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]] | [[package]] | ||||
@@ -418,7 +418,7 @@ version = "0.5.1" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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 = [ | dependencies = [ | ||||
"cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "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)", | "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" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", | "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]] | [[package]] | ||||
@@ -512,7 +512,7 @@ dependencies = [ | |||||
"fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "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)", | "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)", | "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)", | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
@@ -555,7 +555,7 @@ name = "num_cpus" | |||||
version = "1.4.0" | version = "1.4.0" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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]] | [[package]] | ||||
@@ -565,7 +565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
dependencies = [ | dependencies = [ | ||||
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "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" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"cmake 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
] | ] | ||||
@@ -628,7 +628,7 @@ name = "rand" | |||||
version = "0.3.15" | version = "0.3.15" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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]] | [[package]] | ||||
@@ -638,19 +638,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
[[package]] | [[package]] | ||||
name = "regex" | name = "regex" | ||||
version = "0.2.1" | |||||
version = "0.2.2" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", | "aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
] | ] | ||||
[[package]] | [[package]] | ||||
name = "regex-syntax" | name = "regex-syntax" | ||||
version = "0.4.0" | |||||
version = "0.4.1" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
[[package]] | [[package]] | ||||
@@ -702,12 +702,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
[[package]] | [[package]] | ||||
name = "serde" | name = "serde" | ||||
version = "1.0.5" | |||||
version = "1.0.7" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
[[package]] | [[package]] | ||||
name = "serde_derive" | name = "serde_derive" | ||||
version = "1.0.5" | |||||
version = "1.0.7" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "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)", | "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]] | [[package]] | ||||
@@ -804,7 +804,7 @@ dependencies = [ | |||||
"lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "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)", | "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)", | "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)", | "yaml-rust 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
@@ -820,7 +820,7 @@ dependencies = [ | |||||
[[package]] | [[package]] | ||||
name = "tera" | name = "tera" | ||||
version = "0.10.5" | |||||
version = "0.10.6" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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)", | "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" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"kernel32-sys 0.2.2 (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)", | "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" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"kernel32-sys 0.2.2 (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)", | |||||
] | ] | ||||
[[package]] | [[package]] | ||||
@@ -887,7 +887,7 @@ version = "0.1.37" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"kernel32-sys 0.2.2 (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)", | |||||
"redox_syscall 0.1.17 (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)", | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
] | ] | ||||
@@ -897,7 +897,7 @@ name = "toml" | |||||
version = "0.4.1" | version = "0.4.1" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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]] | [[package]] | ||||
@@ -928,7 +928,7 @@ dependencies = [ | |||||
[[package]] | [[package]] | ||||
name = "unicode-bidi" | name = "unicode-bidi" | ||||
version = "0.2.5" | |||||
version = "0.3.1" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
@@ -941,7 +941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
[[package]] | [[package]] | ||||
name = "unicode-segmentation" | name = "unicode-segmentation" | ||||
version = "1.1.0" | |||||
version = "1.2.0" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
[[package]] | [[package]] | ||||
@@ -980,7 +980,7 @@ name = "url" | |||||
version = "1.4.0" | version = "1.4.0" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | 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)", | "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]] | [[package]] | ||||
name = "vec_map" | name = "vec_map" | ||||
version = "0.7.0" | |||||
version = "0.8.0" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
[[package]] | [[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 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 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 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 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 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" | "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 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.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 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 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 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" | "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 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 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 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 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 iovec 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "29d062ee61fccdf25be172e70f34c9f6efc597e1fb8f6526e8437b2046ab26be" | ||||
"checksum iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2440ae846e7a8c7f9b401db8f6e31b4ea5e7d3688b91761337da7e054520c75b" | "checksum 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 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 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 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 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 matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efd7622e3022e1a6eaa602c4cea8912254e5582c9c692e9167714182244801b1" | ||||
"checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4" | "checksum 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 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 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 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-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-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" | "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 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.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 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_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 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" | "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 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 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 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 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-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" | "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 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 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 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-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-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 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" | "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 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 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 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 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 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" | "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)`. | For example, linking to a file located at `content/pages/about.md` would be `[my link](./pages/about.md)`. | ||||
### Anchors | ### 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 | ### Shortcodes | ||||
Gutenberg uses markdown for content but sometimes you want to insert some HTML, for example for a YouTube video. | 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 | // Better error message than the rust default | ||||
if path.exists() && path.is_dir() { | if path.exists() && path.is_dir() { | ||||
@@ -26,7 +26,7 @@ pub fn create_new_project<P: AsRef<Path>>(name: P) -> Result<()> { | |||||
// main folder | // main folder | ||||
create_dir(path)?; | create_dir(path)?; | ||||
create_file(path.join("config.toml"), CONFIG.trim_left())?; | |||||
create_file(&path.join("config.toml"), CONFIG.trim_left())?; | |||||
// content folder | // content folder | ||||
create_dir(path.join("content"))?; | create_dir(path.join("content"))?; | ||||
@@ -23,6 +23,10 @@ enum ChangeKind { | |||||
StaticFiles, | 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"); | 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<()> { | pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> { | ||||
let start = Instant::now(); | let start = Instant::now(); | ||||
let mut site = Site::new(env::current_dir().unwrap(), config_file)?; | 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); | mount.mount("/livereload.js", livereload_handler); | ||||
// Starts with a _ to not trigger the unused lint | // Starts with a _ to not trigger the unused lint | ||||
// we need to assign to a variable otherwise it will block | // 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 | // The websocket for livereload | ||||
let ws_server = WebSocket::new(|output: Sender| { | 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::*; | use notify::DebouncedEvent::*; | ||||
loop { | loop { | ||||
// See https://github.com/spf13/hugo/blob/master/commands/hugo.go | |||||
// for a more complete version of that | |||||
match rx.recv() { | match rx.recv() { | ||||
Ok(event) => { | Ok(event) => { | ||||
match 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 | /// Returns whether the path we received corresponds to a temp file created | ||||
/// by an editor or the OS | /// by an editor or the OS | ||||
fn is_temp_file(path: &Path) -> bool { | 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 | /// Detect what changed from the given path so we have an idea what needs | ||||
/// to be reloaded | /// to be reloaded | ||||
fn detect_change_kind(pwd: &str, path: &Path) -> (ChangeKind, String) { | 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}; | use super::{is_temp_file, detect_change_kind, ChangeKind}; | ||||
#[test] | #[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.swp"), | ||||
Path::new("hello.swx"), | Path::new("hello.swx"), | ||||
Path::new(".DS_STORE"), | Path::new(".DS_STORE"), | ||||
@@ -231,14 +230,14 @@ mod tests { | |||||
Path::new("#hello.html"), | Path::new("#hello.html"), | ||||
]; | ]; | ||||
for t in testcases { | |||||
for t in test_cases { | |||||
assert!(is_temp_file(&t)); | assert!(is_temp_file(&t)); | ||||
} | } | ||||
} | } | ||||
#[test] | #[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()), | (ChangeKind::Templates, "/templates/hello.html".to_string()), | ||||
"/home/vincent/site", Path::new("/home/vincent/site/templates/hello.html") | "/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)); | 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 | /// Display a warning in the console if there are ignored pages in the site | ||||
pub fn warn_about_ignored_pages(site: &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() { | if !ignored_pages.is_empty() { | ||||
warn(&format!( | warn(&format!( | ||||
"{} page(s) ignored (missing date or order in a sorted section):", | "{} page(s) ignored (missing date or order in a sorted section):", | ||||
ignored_pages.len() | ignored_pages.len() | ||||
)); | )); | ||||
for path in site.get_ignored_pages() { | |||||
for path in ignored_pages { | |||||
warn(&format!("- {}", path.display())); | warn(&format!("- {}", path.display())); | ||||
} | } | ||||
} | } | ||||
@@ -62,9 +66,11 @@ pub fn report_elapsed_time(instant: Instant) { | |||||
/// Display an error message and the actual error(s) | /// Display an error message and the actual error(s) | ||||
pub fn unravel_errors(message: &str, error: &Error) { | pub fn unravel_errors(message: &str, error: &Error) { | ||||
if !message.is_empty() { | |||||
self::error(message); | 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) { | match cmd::serve(interface, port, config_file) { | ||||
Ok(()) => (), | Ok(()) => (), | ||||
Err(e) => { | Err(e) => { | ||||
console::unravel_errors("Failed to build the site", &e); | |||||
console::unravel_errors("", &e); | |||||
::std::process::exit(1); | ::std::process::exit(1); | ||||
}, | }, | ||||
}; | }; | ||||
@@ -1,8 +1,21 @@ | |||||
use std::path::Path; | use std::path::Path; | ||||
use gutenberg::{Site, SectionFrontMatter, PageFrontMatter}; | |||||
use gutenberg::{Site, Page, Section, SectionFrontMatter, PageFrontMatter}; | |||||
use gutenberg::errors::Result; | 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)] | #[derive(Debug, Clone, Copy, PartialEq)] | ||||
enum PageChangesNeeded { | enum PageChangesNeeded { | ||||
/// Editing `tags` | /// Editing `tags` | ||||
@@ -22,7 +35,7 @@ enum SectionChangesNeeded { | |||||
Sort, | Sort, | ||||
/// Editing `title`, `description`, `extra`, `template` or setting `render` to true | /// Editing `title`, `description`, `extra`, `template` or setting `render` to true | ||||
Render, | Render, | ||||
/// Editing `paginate_by` or `paginate_path` | |||||
/// Editing `paginate_by`, `paginate_path` or `insert_anchor` | |||||
RenderWithPages, | RenderWithPages, | ||||
/// Setting `render` to false | /// Setting `render` to false | ||||
Delete, | Delete, | ||||
@@ -43,7 +56,9 @@ fn find_section_front_matter_changes(current: &SectionFrontMatter, other: &Secti | |||||
return changes_needed; | 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); | changes_needed.push(SectionChangesNeeded::RenderWithPages); | ||||
// Nothing else we can do | // Nothing else we can do | ||||
return changes_needed; | 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: | // A section was deleted, many things can be impacted: | ||||
// - the pages of the section are becoming orphans | // - the pages of the section are becoming orphans | ||||
// - any page that was referencing the section (index, etc) | // - 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 | // Remove the link to it and the section itself from the Site | ||||
site.permalinks.remove(&relative_path); | site.permalinks.remove(&relative_path); | ||||
site.sections.remove(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: | // A page was deleted, many things can be impacted: | ||||
// - the section the page is in | // - the section the page is in | ||||
// - any page that was referencing the section (index, etc) | // - 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); | site.permalinks.remove(&relative_path); | ||||
if let Some(p) = site.pages.remove(path) { | if let Some(p) = site.pages.remove(path) { | ||||
if p.meta.has_tags() || p.meta.category.is_some() { | if p.meta.has_tags() || p.meta.category.is_some() { | ||||
site.populate_tags_and_categories(); | site.populate_tags_and_categories(); | ||||
} | } | ||||
if site.find_parent_section(&p).is_some() { | |||||
if find_parent_section(site, &p).is_some() { | |||||
site.populate_sections(); | 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 | // Deletion is something that doesn't happen all the time so we | ||||
// don't need to optimise it too much | // don't need to optimise it too much | ||||
return site.build(); | return site.build(); | ||||
@@ -140,6 +157,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { | |||||
return Ok(()); | return Ok(()); | ||||
}, | }, | ||||
None => { | None => { | ||||
site.register_get_url_fn(); | |||||
// New section, only render that one | // New section, only render that one | ||||
site.populate_sections(); | site.populate_sections(); | ||||
return site.render_section(&site.sections[path], true); | 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 | // A page was edited | ||||
match site.add_page(path, true)? { | match site.add_page(path, true)? { | ||||
Some(prev) => { | Some(prev) => { | ||||
site.register_get_url_fn(); | |||||
// Updating a page | // Updating a page | ||||
let current = site.pages[path].clone(); | let current = site.pages[path].clone(); | ||||
// Front matter didn't change, only content did | // 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()?; | site.render_categories()?; | ||||
}, | }, | ||||
PageChangesNeeded::Sort => { | 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 | None => continue // Do nothing if it's an orphan page | ||||
}; | }; | ||||
site.populate_sections(); | site.populate_sections(); | ||||
@@ -188,6 +207,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { | |||||
}, | }, | ||||
None => { | None => { | ||||
site.register_get_url_fn(); | |||||
// It's a new page! | // It's a new page! | ||||
site.populate_sections(); | site.populate_sections(); | ||||
site.populate_tags_and_categories(); | site.populate_tags_and_categories(); | ||||
@@ -6,7 +6,7 @@ use std::collections::HashMap; | |||||
use toml::{Value as Toml, self}; | use toml::{Value as Toml, self}; | ||||
use errors::{Result, ResultExt}; | use errors::{Result, ResultExt}; | ||||
use markdown::SETUP; | |||||
use rendering::highlighting::THEME_SET; | |||||
#[derive(Debug, PartialEq, Serialize, Deserialize)] | #[derive(Debug, PartialEq, Serialize, Deserialize)] | ||||
@@ -24,8 +24,10 @@ pub struct Config { | |||||
pub description: Option<String>, | pub description: Option<String>, | ||||
/// The language used in the site. Defaults to "en" | /// The language used in the site. Defaults to "en" | ||||
pub language_code: Option<String>, | pub language_code: Option<String>, | ||||
/// Whether to generate RSS, defaults to false | |||||
/// Whether to generate RSS. Defaults to false | |||||
pub generate_rss: Option<bool>, | 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 | /// Whether to generate tags and individual tag pages if some pages have them. Defaults to true | ||||
pub generate_tags_pages: Option<bool>, | pub generate_tags_pages: Option<bool>, | ||||
/// Whether to generate categories and individual tag categories if some pages have them. Defaults to true | /// 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.language_code, "en".to_string()); | ||||
set_default!(config.highlight_code, false); | set_default!(config.highlight_code, false); | ||||
set_default!(config.generate_rss, 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_tags_pages, false); | ||||
set_default!(config.generate_categories_pages, false); | set_default!(config.generate_categories_pages, false); | ||||
set_default!(config.insert_anchor_links, false); | set_default!(config.insert_anchor_links, false); | ||||
match config.highlight_theme { | match config.highlight_theme { | ||||
Some(ref t) => { | Some(ref t) => { | ||||
if !SETUP.theme_set.themes.contains_key(t) { | |||||
if !THEME_SET.themes.contains_key(t) { | |||||
bail!("Theme {} not available", 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 | /// Makes a url, taking into account that the base url might have a trailing slash | ||||
pub fn make_permalink(&self, path: &str) -> String { | 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) | format!("{}{}", self.base_url, path) | ||||
} else { | } else { | ||||
format!("{}/{}", self.base_url, path) | format!("{}/{}", self.base_url, path) | ||||
@@ -95,8 +100,9 @@ impl Config { | |||||
} | } | ||||
} | } | ||||
/// Exists only for testing purposes | |||||
#[doc(hidden)] | |||||
impl Default for Config { | impl Default for Config { | ||||
/// Exists for testing purposes | |||||
fn default() -> Config { | fn default() -> Config { | ||||
Config { | Config { | ||||
title: "".to_string(), | title: "".to_string(), | ||||
@@ -106,6 +112,7 @@ impl Default for Config { | |||||
description: None, | description: None, | ||||
language_code: Some("en".to_string()), | language_code: Some("en".to_string()), | ||||
generate_rss: Some(false), | generate_rss: Some(false), | ||||
rss_limit: Some(10000), | |||||
generate_tags_pages: Some(true), | generate_tags_pages: Some(true), | ||||
generate_categories_pages: Some(true), | generate_categories_pages: Some(true), | ||||
insert_anchor_links: Some(false), | insert_anchor_links: Some(false), | ||||
@@ -181,4 +188,17 @@ hello = "world" | |||||
assert_eq!(config.unwrap().extra.unwrap().get("hello").unwrap().as_str().unwrap(), "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 page; | ||||
mod pagination; | mod pagination; | ||||
mod section; | mod section; | ||||
mod sorting; | mod sorting; | ||||
mod utils; | mod utils; | ||||
mod file_info; | |||||
mod taxonomies; | |||||
pub use self::page::{Page}; | pub use self::page::{Page}; | ||||
pub use self::section::{Section}; | pub use self::section::{Section}; | ||||
pub use self::pagination::{Paginator, Pager}; | pub use self::pagination::{Paginator, Pager}; | ||||
pub use self::sorting::{SortBy, sort_pages, populate_previous_and_next_pages}; | 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 std::result::Result as StdResult; | ||||
use tera::{Tera, Context}; | |||||
use tera::{Tera, Context as TeraContext}; | |||||
use serde::ser::{SerializeStruct, self}; | use serde::ser::{SerializeStruct, self}; | ||||
use slug::slugify; | use slug::slugify; | ||||
use errors::{Result, ResultExt}; | use errors::{Result, ResultExt}; | ||||
use config::Config; | 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::utils::{find_related_assets, get_reading_analytics}; | ||||
use content::file_info::FileInfo; | |||||
#[derive(Clone, Debug, PartialEq)] | #[derive(Clone, Debug, PartialEq)] | ||||
pub struct Page { | pub struct Page { | ||||
/// All info about the actual file | |||||
pub file: FileInfo, | |||||
/// The front matter meta-data | /// The front matter meta-data | ||||
pub meta: PageFrontMatter, | 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 | /// The actual content of the page, in markdown | ||||
pub raw_content: String, | pub raw_content: String, | ||||
/// All the non-md files we found next to the .md file | /// All the non-md files we found next to the .md file | ||||
pub assets: Vec<PathBuf>, | pub assets: Vec<PathBuf>, | ||||
/// The HTML rendered of the page | /// The HTML rendered of the page | ||||
pub content: String, | pub content: String, | ||||
/// The slug of that page. | /// The slug of that page. | ||||
/// First tries to find the slug in the meta and defaults to filename otherwise | /// First tries to find the slug in the meta and defaults to filename otherwise | ||||
pub slug: String, | 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 | /// When <!-- more --> is found in the text, will take the content up to that part | ||||
/// as summary | /// as summary | ||||
pub summary: Option<String>, | pub summary: Option<String>, | ||||
/// The previous page, by whatever sorting is used for the index/section | /// The previous page, by whatever sorting is used for the index/section | ||||
pub previous: Option<Box<Page>>, | pub previous: Option<Box<Page>>, | ||||
/// The next page, by whatever sorting is used for the index/section | /// The next page, by whatever sorting is used for the index/section | ||||
@@ -61,14 +49,12 @@ pub struct Page { | |||||
impl 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 { | Page { | ||||
file: FileInfo::new_page(file_path), | |||||
meta: meta, | meta: meta, | ||||
file_path: PathBuf::new(), | |||||
relative_path: String::new(), | |||||
parent_path: PathBuf::new(), | |||||
file_name: "".to_string(), | |||||
components: vec![], | |||||
raw_content: "".to_string(), | raw_content: "".to_string(), | ||||
assets: vec![], | assets: vec![], | ||||
content: "".to_string(), | content: "".to_string(), | ||||
@@ -85,49 +71,26 @@ impl Page { | |||||
/// Files without front matter or with invalid front matter are considered | /// Files without front matter or with invalid front matter are considered | ||||
/// erroneous | /// erroneous | ||||
pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Page> { | 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 (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; | page.raw_content = content; | ||||
let path = Path::new(file_path); | |||||
page.file_name = path.file_stem().unwrap().to_string_lossy().to_string(); | |||||
page.slug = { | page.slug = { | ||||
if let Some(ref slug) = page.meta.slug { | if let Some(ref slug) = page.meta.slug { | ||||
slug.trim().to_string() | slug.trim().to_string() | ||||
} else { | } 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 { | if let Some(ref u) = page.meta.url { | ||||
page.path = u.trim().to_string(); | 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); | page.permalink = config.make_permalink(&page.path); | ||||
Ok(page) | Ok(page) | ||||
@@ -140,7 +103,7 @@ impl Page { | |||||
let mut page = Page::parse(path, &content, config)?; | let mut page = Page::parse(path, &content, config)?; | ||||
page.assets = find_related_assets(path.parent().unwrap()); | 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); | 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 | /// We need access to all pages url to render links relative to content | ||||
/// so that can't happen at the same time as parsing | /// 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 -->") { | if self.raw_content.contains("<!-- more -->") { | ||||
self.summary = Some({ | self.summary = Some({ | ||||
let summary = self.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[0]; | 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() | None => "page.html".to_string() | ||||
}; | }; | ||||
let mut context = Context::new(); | |||||
let mut context = TeraContext::new(); | |||||
context.add("config", config); | context.add("config", config); | ||||
context.add("page", self); | context.add("page", self); | ||||
context.add("current_url", &self.permalink); | context.add("current_url", &self.permalink); | ||||
context.add("current_path", &self.path); | context.add("current_path", &self.path); | ||||
tera.render(&tpl_name, &context) | 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 { | impl Default for Page { | ||||
fn default() -> Page { | fn default() -> Page { | ||||
Page { | Page { | ||||
file: FileInfo::default(), | |||||
meta: PageFrontMatter::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(), | raw_content: "".to_string(), | ||||
assets: vec![], | assets: vec![], | ||||
content: "".to_string(), | content: "".to_string(), | ||||
@@ -205,7 +164,7 @@ impl Default for Page { | |||||
impl ser::Serialize for Page { | impl ser::Serialize for Page { | ||||
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer { | 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("content", &self.content)?; | ||||
state.serialize_field("title", &self.meta.title)?; | state.serialize_field("title", &self.meta.title)?; | ||||
state.serialize_field("description", &self.meta.description)?; | 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("permalink", &self.permalink)?; | ||||
state.serialize_field("summary", &self.summary)?; | state.serialize_field("summary", &self.summary)?; | ||||
state.serialize_field("tags", &self.meta.tags)?; | state.serialize_field("tags", &self.meta.tags)?; | ||||
state.serialize_field("draft", &self.meta.draft)?; | |||||
state.serialize_field("category", &self.meta.category)?; | state.serialize_field("category", &self.meta.category)?; | ||||
state.serialize_field("extra", &self.meta.extra)?; | state.serialize_field("extra", &self.meta.extra)?; | ||||
let (word_count, reading_time) = get_reading_analytics(&self.raw_content); | let (word_count, reading_time) = get_reading_analytics(&self.raw_content); | ||||
@@ -226,3 +184,142 @@ impl ser::Serialize for Page { | |||||
state.end() | 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) | 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 { | if !is_index { | ||||
s.path = "posts".to_string(); | s.path = "posts".to_string(); | ||||
s.permalink = "https://vincent.is/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 { | } else { | ||||
s.permalink = "https://vincent.is".to_string(); | s.permalink = "https://vincent.is".to_string(); | ||||
} | } | ||||
@@ -2,29 +2,25 @@ use std::collections::HashMap; | |||||
use std::path::{Path, PathBuf}; | use std::path::{Path, PathBuf}; | ||||
use std::result::Result as StdResult; | use std::result::Result as StdResult; | ||||
use tera::{Tera, Context}; | |||||
use tera::{Tera, Context as TeraContext}; | |||||
use serde::ser::{SerializeStruct, self}; | use serde::ser::{SerializeStruct, self}; | ||||
use config::Config; | use config::Config; | ||||
use front_matter::{SectionFrontMatter, split_section_content}; | use front_matter::{SectionFrontMatter, split_section_content}; | ||||
use errors::{Result, ResultExt}; | 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::Page; | ||||
use content::file_info::FileInfo; | |||||
#[derive(Clone, Debug, PartialEq)] | #[derive(Clone, Debug, PartialEq)] | ||||
pub struct Section { | pub struct Section { | ||||
/// All info about the actual file | |||||
pub file: FileInfo, | |||||
/// The front matter meta-data | /// The front matter meta-data | ||||
pub meta: SectionFrontMatter, | 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 | /// The URL path of the page | ||||
pub path: String, | pub path: String, | ||||
/// The full URL for that page | /// The full URL for that page | ||||
@@ -46,11 +42,8 @@ impl Section { | |||||
let file_path = file_path.as_ref(); | let file_path = file_path.as_ref(); | ||||
Section { | Section { | ||||
file: FileInfo::new_section(file_path), | |||||
meta: meta, | 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(), | path: "".to_string(), | ||||
permalink: "".to_string(), | permalink: "".to_string(), | ||||
raw_content: "".to_string(), | raw_content: "".to_string(), | ||||
@@ -65,16 +58,8 @@ impl Section { | |||||
let (meta, content) = split_section_content(file_path, content)?; | let (meta, content) = split_section_content(file_path, content)?; | ||||
let mut section = Section::new(file_path, meta); | let mut section = Section::new(file_path, meta); | ||||
section.raw_content = content.clone(); | 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); | 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) | Ok(section) | ||||
} | } | ||||
@@ -101,7 +86,8 @@ impl Section { | |||||
/// We need access to all pages url to render links relative to content | /// We need access to all pages url to render links relative to content | ||||
/// so that can't happen at the same time as parsing | /// 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<()> { | 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(()) | Ok(()) | ||||
} | } | ||||
@@ -109,7 +95,7 @@ impl Section { | |||||
pub fn render_html(&self, sections: HashMap<String, Section>, tera: &Tera, config: &Config) -> Result<String> { | pub fn render_html(&self, sections: HashMap<String, Section>, tera: &Tera, config: &Config) -> Result<String> { | ||||
let tpl_name = self.get_template_name(); | let tpl_name = self.get_template_name(); | ||||
let mut context = Context::new(); | |||||
let mut context = TeraContext::new(); | |||||
context.add("config", config); | context.add("config", config); | ||||
context.add("section", self); | context.add("section", self); | ||||
context.add("current_url", &self.permalink); | context.add("current_url", &self.permalink); | ||||
@@ -119,46 +105,36 @@ impl Section { | |||||
} | } | ||||
tera.render(&tpl_name, &context) | 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? | /// Is this the index section? | ||||
pub fn is_index(&self) -> bool { | 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> { | pub fn all_pages_path(&self) -> Vec<PathBuf> { | ||||
let mut paths = vec![]; | 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 | paths | ||||
} | } | ||||
/// Whether the page given belongs to that section | /// 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 { | impl ser::Serialize for Section { | ||||
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer { | 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("content", &self.content)?; | ||||
state.serialize_field("permalink", &self.permalink)?; | |||||
state.serialize_field("title", &self.meta.title)?; | state.serialize_field("title", &self.meta.title)?; | ||||
state.serialize_field("description", &self.meta.description)?; | state.serialize_field("description", &self.meta.description)?; | ||||
state.serialize_field("extra", &self.meta.extra)?; | |||||
state.serialize_field("path", &format!("/{}", self.path))?; | state.serialize_field("path", &format!("/{}", self.path))?; | ||||
state.serialize_field("permalink", &self.permalink)?; | state.serialize_field("permalink", &self.permalink)?; | ||||
state.serialize_field("pages", &self.pages)?; | 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 { | 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 { | fn default() -> Section { | ||||
Section { | Section { | ||||
file: FileInfo::default(), | |||||
meta: SectionFrontMatter::default(), | meta: SectionFrontMatter::default(), | ||||
file_path: PathBuf::new(), | |||||
relative_path: "".to_string(), | |||||
parent_path: PathBuf::new(), | |||||
components: vec![], | |||||
path: "".to_string(), | path: "".to_string(), | ||||
permalink: "".to_string(), | permalink: "".to_string(), | ||||
raw_content: "".to_string(), | raw_content: "".to_string(), | ||||
@@ -59,12 +59,20 @@ pub fn populate_previous_and_next_pages(input: &[Page]) -> Vec<Page> { | |||||
if i > 0 { | if i > 0 { | ||||
let next = &pages[i - 1]; | 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 { | if i < input.len() - 1 { | ||||
let previous = &pages[i + 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); | res.push(new_page); | ||||
} | } | ||||
@@ -81,13 +89,13 @@ mod tests { | |||||
fn create_page_with_date(date: &str) -> Page { | fn create_page_with_date(date: &str) -> Page { | ||||
let mut front_matter = PageFrontMatter::default(); | let mut front_matter = PageFrontMatter::default(); | ||||
front_matter.date = Some(date.to_string()); | 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 { | fn create_page_with_order(order: usize) -> Page { | ||||
let mut front_matter = PageFrontMatter::default(); | let mut front_matter = PageFrontMatter::default(); | ||||
front_matter.order = Some(order); | front_matter.order = Some(order); | ||||
Page::new(front_matter) | |||||
Page::new("content/hello.md", front_matter) | |||||
} | } | ||||
#[test] | #[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)) | (word_count, (word_count / 200)) | ||||
} | } | ||||
#[cfg(test)] | #[cfg(test)] | ||||
mod tests { | mod tests { | ||||
use std::fs::File; | use std::fs::File; | ||||
@@ -8,7 +8,7 @@ mod page; | |||||
mod section; | mod section; | ||||
pub use self::page::PageFrontMatter; | pub use self::page::PageFrontMatter; | ||||
pub use self::section::{SectionFrontMatter}; | |||||
pub use self::section::{SectionFrontMatter, InsertAnchor}; | |||||
lazy_static! { | lazy_static! { | ||||
static ref PAGE_RE: Regex = Regex::new(r"^[[:space:]]*\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap(); | 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>, | pub url: Option<String>, | ||||
/// Tags, not to be confused with categories | /// Tags, not to be confused with categories | ||||
pub tags: Option<Vec<String>>, | 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 | /// Only one category allowed. Can't be an empty string if present | ||||
pub category: Option<String>, | pub category: Option<String>, | ||||
/// Integer to use to order content. Lowest is at the bottom, highest first | /// Integer to use to order content. Lowest is at the bottom, highest first | ||||
@@ -100,7 +98,6 @@ impl Default for PageFrontMatter { | |||||
slug: None, | slug: None, | ||||
url: None, | url: None, | ||||
tags: None, | tags: None, | ||||
draft: None, | |||||
category: None, | category: None, | ||||
order: None, | order: None, | ||||
template: None, | template: None, | ||||
@@ -9,6 +9,14 @@ use content::SortBy; | |||||
static DEFAULT_PAGINATE_PATH: &'static str = "page"; | 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 | /// The front matter of every section | ||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||||
pub struct SectionFrontMatter { | 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`. | /// Path to be used by pagination: the page number will be appended after it. Defaults to `page`. | ||||
#[serde(skip_serializing)] | #[serde(skip_serializing)] | ||||
pub paginate_path: Option<String>, | 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`. | /// Whether to render that section or not. Defaults to `true`. | ||||
/// Useful when the section is only there to organize things but is not meant | /// 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 | /// to be used directly, like a posts section in a personal site | ||||
@@ -56,6 +68,10 @@ impl SectionFrontMatter { | |||||
f.sort_by = Some(SortBy::None); | f.sort_by = Some(SortBy::None); | ||||
} | } | ||||
if f.insert_anchor.is_none() { | |||||
f.insert_anchor = Some(InsertAnchor::None); | |||||
} | |||||
Ok(f) | Ok(f) | ||||
} | } | ||||
@@ -87,6 +103,7 @@ impl Default for SectionFrontMatter { | |||||
paginate_by: None, | paginate_by: None, | ||||
paginate_path: Some(DEFAULT_PAGINATE_PATH.to_string()), | paginate_path: Some(DEFAULT_PAGINATE_PATH.to_string()), | ||||
render: Some(true), | render: Some(true), | ||||
insert_anchor: Some(InsertAnchor::None), | |||||
extra: 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)] | #[cfg(test)] | ||||
extern crate tempdir; | extern crate tempdir; | ||||
mod utils; | |||||
mod fs; | |||||
mod config; | mod config; | ||||
pub mod errors; | pub mod errors; | ||||
mod front_matter; | mod front_matter; | ||||
mod content; | mod content; | ||||
mod site; | mod site; | ||||
mod markdown; | |||||
mod rendering; | |||||
// Filters, Global Fns and default instance of Tera | // Filters, Global Fns and default instance of Tera | ||||
mod templates; | mod templates; | ||||
pub use site::{Site}; | pub use site::{Site}; | ||||
pub use config::{Config, get_config}; | 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 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::borrow::Cow::Owned; | ||||
use std::collections::HashMap; | |||||
use pulldown_cmark as cmark; | use pulldown_cmark as cmark; | ||||
use self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES}; | 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::dumps::from_binary; | ||||
use syntect::easy::HighlightLines; | use syntect::easy::HighlightLines; | ||||
use syntect::parsing::SyntaxSet; | use syntect::parsing::SyntaxSet; | ||||
use syntect::highlighting::ThemeSet; | |||||
use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground}; | 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 | // We need to put those in a struct to impl Send and sync | ||||
pub struct Setup { | pub struct Setup { | ||||
pub syntax_set: SyntaxSet, | pub syntax_set: SyntaxSet, | ||||
pub theme_set: ThemeSet, | |||||
} | } | ||||
unsafe impl Send for Setup {} | unsafe impl Send for Setup {} | ||||
@@ -29,87 +29,25 @@ lazy_static!{ | |||||
static ref SHORTCODE_RE: Regex = Regex::new(r#"\{(?:%|\{)\s+([[:alnum:]]+?)\(([[:alnum:]]+?="?.+?"?)\)\s+(?:%|\})\}"#).unwrap(); | static ref SHORTCODE_RE: Regex = Regex::new(r#"\{(?:%|\{)\s+([[:alnum:]]+?)\(([[:alnum:]]+?="?.+?"?)\)\s+(?:%|\})\}"#).unwrap(); | ||||
pub static ref SETUP: Setup = Setup { | pub static ref SETUP: Setup = Setup { | ||||
syntax_set: { | 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.link_syntaxes(); | ||||
ps | 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 | // 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 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 | // 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 | // to be highlighted. It could potentially have false positive if the content | ||||
// has ``` in it but that seems kind of unlikely | // 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("```") | content.contains("```") | ||||
} else { | } else { | ||||
false | false | ||||
}; | }; | ||||
let highlight_theme = config.highlight_theme.clone().unwrap(); | |||||
// Set while parsing | // Set while parsing | ||||
let mut error = None; | let mut error = None; | ||||
let mut highlighter: Option<HighlightLines> = 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) { | if shortcode_block.is_none() && text.starts_with("{{") && text.ends_with("}}") && SHORTCODE_RE.is_match(&text) { | ||||
let (name, args) = parse_shortcode(&text); | let (name, args) = parse_shortcode(&text); | ||||
added_shortcode = true; | 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))), | Ok(s) => return Event::Html(Owned(format!("</p>{}", s))), | ||||
Err(e) => { | Err(e) => { | ||||
error = Some(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 let Some(ref mut shortcode) = shortcode_block { | ||||
if text.trim() == "{% end %}" { | if text.trim() == "{% end %}" { | ||||
added_shortcode = true; | added_shortcode = true; | ||||
match shortcode.render(tera) { | |||||
match shortcode.render(context.tera) { | |||||
Ok(s) => return Event::Html(Owned(format!("</p>{}", s))), | Ok(s) => return Event::Html(Owned(format!("</p>{}", s))), | ||||
Err(e) => { | Err(e) => { | ||||
error = Some(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); | let id = find_anchor(&anchors, slugify(&text), 0); | ||||
anchors.push(id.clone()); | 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 { | } else { | ||||
String::new() | String::new() | ||||
}; | }; | ||||
header_already_inserted = true; | 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 | // Business as usual | ||||
@@ -232,7 +175,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter | |||||
if !should_highlight { | if !should_highlight { | ||||
return Event::Html(Owned("<pre><code>".to_owned())); | 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 | let syntax = info | ||||
.split(' ') | .split(' ') | ||||
.next() | .next() | ||||
@@ -257,21 +200,11 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter | |||||
return Event::Html(Owned("".to_owned())); | return Event::Html(Owned("".to_owned())); | ||||
} | } | ||||
if link.starts_with("./") { | 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())); | return Event::Start(Tag::Link(Owned(url), title.clone())); | ||||
}, | }, | ||||
None => { | |||||
Err(_) => { | |||||
error = Some(format!("Relative link {} not found.", link).into()); | error = Some(format!("Relative link {} not found.", link).into()); | ||||
return Event::Html(Owned("".to_string())); | return Event::Html(Owned("".to_string())); | ||||
} | } | ||||
@@ -340,46 +273,33 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter | |||||
mod tests { | mod tests { | ||||
use std::collections::HashMap; | use std::collections::HashMap; | ||||
use templates::GUTENBERG_TERA; | |||||
use tera::Tera; | use tera::Tera; | ||||
use config::Config; | 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] | #[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"); | assert_eq!(res, "<p>hello</p>\n"); | ||||
} | } | ||||
#[test] | #[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!( | assert_eq!( | ||||
res, | res, | ||||
"<pre><code>$ gutenberg server\n</code></pre>\n" | "<pre><code>$ gutenberg server\n</code></pre>\n" | ||||
@@ -387,8 +307,12 @@ mod tests { | |||||
} | } | ||||
#[test] | #[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!( | assert_eq!( | ||||
res, | 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>" | "<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] | #[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!( | assert_eq!( | ||||
res, | 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>" | "<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] | #[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 | // defaults to plain text | ||||
assert_eq!( | assert_eq!( | ||||
res, | res, | ||||
@@ -415,18 +347,24 @@ mod tests { | |||||
} | } | ||||
#[test] | #[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#" | let res = markdown_to_html(r#" | ||||
Hello | Hello | ||||
{{ youtube(id="ub36ffWAqgQ") }} | {{ youtube(id="ub36ffWAqgQ") }} | ||||
"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap(); | |||||
"#, &context).unwrap(); | |||||
assert!(res.contains("<p>Hello</p>\n<div >")); | 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""#)); | ||||
} | } | ||||
#[test] | #[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#" | let res = markdown_to_html(r#" | ||||
Hello | Hello | ||||
@@ -438,7 +376,7 @@ Hello | |||||
{{ gist(url="https://gist.github.com/Keats/32d26f699dcc13ebd41b") }} | {{ 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("<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""#)); | ||||
assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#)); | assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#)); | ||||
@@ -446,40 +384,52 @@ Hello | |||||
} | } | ||||
#[test] | #[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"); | assert_eq!(res, "<p><code>{{ youtube(id="w7Ft2ymGmfc") }}</code></p>\n"); | ||||
} | } | ||||
#[test] | #[test] | ||||
fn test_markdown_to_html_shortcode_with_body() { | |||||
fn can_render_shortcode_with_body() { | |||||
let mut tera = Tera::default(); | let mut tera = Tera::default(); | ||||
tera.extend(&GUTENBERG_TERA).unwrap(); | tera.extend(&GUTENBERG_TERA).unwrap(); | ||||
tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author}}</blockquote>").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#" | let res = markdown_to_html(r#" | ||||
Hello | Hello | ||||
{% quote(author="Keats") %} | {% quote(author="Keats") %} | ||||
A quote | A quote | ||||
{% end %} | {% end %} | ||||
"#, &HashMap::new(), &tera, &Config::default()).unwrap(); | |||||
"#, &context).unwrap(); | |||||
assert_eq!(res, "<p>Hello\n</p><blockquote>A quote - Keats</blockquote>"); | assert_eq!(res, "<p>Hello\n</p><blockquote>A quote - Keats</blockquote>"); | ||||
} | } | ||||
#[test] | #[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()); | assert!(res.is_err()); | ||||
} | } | ||||
#[test] | #[test] | ||||
fn test_markdown_to_html_relative_link_exists() { | |||||
fn can_make_valid_relative_link() { | |||||
let mut permalinks = HashMap::new(); | let mut permalinks = HashMap::new(); | ||||
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); | 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( | let res = markdown_to_html( | ||||
r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#, | r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#, | ||||
&permalinks, | |||||
&GUTENBERG_TERA, | |||||
&Config::default() | |||||
&context | |||||
).unwrap(); | ).unwrap(); | ||||
assert!( | assert!( | ||||
@@ -488,15 +438,13 @@ A quote | |||||
} | } | ||||
#[test] | #[test] | ||||
fn test_markdown_to_html_relative_links_with_anchors() { | |||||
fn can_make_relative_links_with_anchors() { | |||||
let mut permalinks = HashMap::new(); | let mut permalinks = HashMap::new(); | ||||
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string()); | 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!( | assert!( | ||||
res.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#) | res.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#) | ||||
@@ -504,67 +452,94 @@ A quote | |||||
} | } | ||||
#[test] | #[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()); | assert!(res.is_err()); | ||||
} | } | ||||
#[test] | #[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"); | assert_eq!(res, "<h1 id=\"hello\">Hello</h1>\n"); | ||||
} | } | ||||
#[test] | #[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"); | assert_eq!(res, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n"); | ||||
} | } | ||||
#[test] | #[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!( | assert_eq!( | ||||
res, | 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 | // See https://github.com/Keats/gutenberg/issues/42 | ||||
#[test] | #[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!( | assert_eq!( | ||||
res, | 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 | // See https://github.com/Keats/gutenberg/issues/53 | ||||
#[test] | #[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!( | assert_eq!( | ||||
res, | 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] | #[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!( | assert_eq!( | ||||
res, | 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::fs::{remove_dir_all, copy, create_dir_all}; | ||||
use std::path::{Path, PathBuf}; | use std::path::{Path, PathBuf}; | ||||
use glob::glob; | use glob::glob; | ||||
use tera::{Tera, Context}; | use tera::{Tera, Context}; | ||||
use slug::slugify; | |||||
use walkdir::WalkDir; | use walkdir::WalkDir; | ||||
use errors::{Result, ResultExt}; | use errors::{Result, ResultExt}; | ||||
use config::{Config, get_config}; | 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 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)] | #[derive(Debug)] | ||||
pub struct Site { | pub struct Site { | ||||
/// The base path of the gutenberg site | |||||
pub base_path: PathBuf, | pub base_path: PathBuf, | ||||
/// The parsed config for the site | |||||
pub config: Config, | pub config: Config, | ||||
pub pages: HashMap<PathBuf, Page>, | pub pages: HashMap<PathBuf, Page>, | ||||
pub sections: HashMap<PathBuf, Section>, | pub sections: HashMap<PathBuf, Section>, | ||||
@@ -49,8 +26,8 @@ pub struct Site { | |||||
live_reload: bool, | live_reload: bool, | ||||
output_path: PathBuf, | output_path: PathBuf, | ||||
static_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 | /// 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 | /// We need that if there are relative links in the content that need to be resolved | ||||
pub permalinks: HashMap<String, String>, | pub permalinks: HashMap<String, String>, | ||||
@@ -75,8 +52,8 @@ impl Site { | |||||
live_reload: false, | live_reload: false, | ||||
output_path: path.join("public"), | output_path: path.join("public"), | ||||
static_path: path.join("static"), | static_path: path.join("static"), | ||||
tags: HashMap::new(), | |||||
categories: HashMap::new(), | |||||
tags: None, | |||||
categories: None, | |||||
permalinks: HashMap::new(), | permalinks: HashMap::new(), | ||||
}; | }; | ||||
@@ -88,15 +65,6 @@ impl Site { | |||||
self.live_reload = true; | 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 | /// Get all the orphan (== without section) pages in the site | ||||
pub fn get_all_orphan_pages(&self) -> Vec<&Page> { | pub fn get_all_orphan_pages(&self) -> Vec<&Page> { | ||||
let mut pages_in_sections = vec![]; | let mut pages_in_sections = vec![]; | ||||
@@ -107,7 +75,7 @@ impl Site { | |||||
} | } | ||||
for page in self.pages.values() { | 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); | orphans.push(page); | ||||
} | } | ||||
} | } | ||||
@@ -115,17 +83,6 @@ impl Site { | |||||
orphans | 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 | /// Used by tests to change the output path to a tmp dir | ||||
#[doc(hidden)] | #[doc(hidden)] | ||||
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) { | pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) { | ||||
@@ -146,8 +103,8 @@ impl Site { | |||||
self.add_page(path, false)?; | 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"); | let index_path = self.base_path.join("content").join("_index.md"); | ||||
if !self.sections.contains_key(&index_path) { | if !self.sections.contains_key(&index_path) { | ||||
let mut index_section = Section::default(); | let mut index_section = Section::default(); | ||||
@@ -155,9 +112,16 @@ impl Site { | |||||
self.sections.insert(index_path, index_section); | 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 | // TODO: make that parallel | ||||
for page in self.pages.values_mut() { | 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 | // TODO: make that parallel | ||||
for section in self.sections.values_mut() { | for section in self.sections.values_mut() { | ||||
@@ -168,22 +132,30 @@ impl Site { | |||||
self.populate_tags_and_categories(); | 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_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(()) | 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 | /// Add a page to the site | ||||
/// The `render` parameter is used in the serve command, when rebuilding a page. | /// The `render` parameter is used in the serve command, when rebuilding a page. | ||||
/// If `true`, it will also render the markdown for that page | /// If `true`, it will also render the markdown for that page | ||||
/// Returns the previous page struct if there was one | /// Returns the previous page struct if there was one | ||||
pub fn add_page(&mut self, path: &Path, render: bool) -> Result<Option<Page>> { | pub fn add_page(&mut self, path: &Path, render: bool) -> Result<Option<Page>> { | ||||
let page = Page::from_file(&path, &self.config)?; | 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 { | 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(); | 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) | Ok(prev) | ||||
@@ -192,11 +164,11 @@ impl Site { | |||||
/// Add a section to the site | /// Add a section to the site | ||||
/// The `render` parameter is used in the serve command, when rebuilding a page. | /// The `render` parameter is used in the serve command, when rebuilding a page. | ||||
/// If `true`, it will also render the markdown for that 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>> { | pub fn add_section(&mut self, path: &Path, render: bool) -> Result<Option<Section>> { | ||||
let section = Section::from_file(path, &self.config)?; | 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 { | if render { | ||||
let mut section = self.sections.get_mut(path).unwrap(); | let mut section = self.sections.get_mut(path).unwrap(); | ||||
@@ -206,12 +178,21 @@ impl Site { | |||||
Ok(prev) | 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 | /// Find out the direct subsections of each subsection if there are some | ||||
/// as well as the pages for each section | /// as well as the pages for each section | ||||
pub fn populate_sections(&mut self) { | pub fn populate_sections(&mut self) { | ||||
let mut grandparent_paths = HashMap::new(); | let mut grandparent_paths = HashMap::new(); | ||||
for section in self.sections.values_mut() { | 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()); | 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` | // 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() { | 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() { | 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()), | Some(paths) => section.subsections.extend(paths.clone()), | ||||
None => continue, | 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) { | 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 | 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. | /// Copy static file to public directory. | ||||
pub fn copy_static_file<P: AsRef<Path>>(&self, path: P) -> Result<()> { | pub fn copy_static_file<P: AsRef<Path>>(&self, path: P) -> Result<()> { | ||||
let relative_path = path.as_ref().strip_prefix(&self.static_path).unwrap(); | let relative_path = path.as_ref().strip_prefix(&self.static_path).unwrap(); | ||||
@@ -332,7 +305,7 @@ impl Site { | |||||
/// Renders a single content page | /// Renders a single content page | ||||
pub fn render_page(&self, page: &Page) -> Result<()> { | 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 | // Copy the nesting of the content directory if we have sections for that page | ||||
let mut current_path = self.output_path.to_path_buf(); | 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 | // Finally, create a index.html file there with the page rendered | ||||
let output = page.render_html(&self.tera, &self.config)?; | 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 | // Copy any asset we found previously into the same directory as the index.html | ||||
for asset in &page.assets { | for asset in &page.assets { | ||||
@@ -361,7 +334,7 @@ impl Site { | |||||
Ok(()) | Ok(()) | ||||
} | } | ||||
/// Builds the site to the `public` directory after deleting it | |||||
/// Deletes the `public` directory and builds the site | |||||
pub fn build(&self) -> Result<()> { | pub fn build(&self) -> Result<()> { | ||||
self.clean()?; | self.clean()?; | ||||
self.render_sections()?; | self.render_sections()?; | ||||
@@ -381,98 +354,45 @@ impl Site { | |||||
/// Renders robots.txt | /// Renders robots.txt | ||||
pub fn render_robots(&self) -> Result<()> { | pub fn render_robots(&self) -> Result<()> { | ||||
self.ensure_public_directory_exists()?; | |||||
ensure_directory_exists(&self.output_path)?; | |||||
create_file( | create_file( | ||||
self.output_path.join("robots.txt"), | |||||
&self.output_path.join("robots.txt"), | |||||
&self.tera.render("robots.txt", &Context::new())? | &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<()> { | 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<()> { | 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_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( | create_file( | ||||
output_path.join(&slug).join("index.html"), | |||||
&output_path.join(&item.slug).join("index.html"), | |||||
&self.inject_livereload(single_output) | &self.inject_livereload(single_output) | ||||
)?; | )?; | ||||
} | } | ||||
@@ -482,28 +402,31 @@ impl Site { | |||||
/// What it says on the tin | /// What it says on the tin | ||||
pub fn render_sitemap(&self) -> Result<()> { | pub fn render_sitemap(&self) -> Result<()> { | ||||
self.ensure_public_directory_exists()?; | |||||
ensure_directory_exists(&self.output_path)?; | |||||
let mut context = Context::new(); | let mut context = Context::new(); | ||||
context.add("pages", &self.pages.values().collect::<Vec<&Page>>()); | context.add("pages", &self.pages.values().collect::<Vec<&Page>>()); | ||||
context.add("sections", &self.sections.values().collect::<Vec<&Section>>()); | context.add("sections", &self.sections.values().collect::<Vec<&Section>>()); | ||||
let mut categories = vec![]; | 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( | categories.push( | ||||
self.config.make_permalink(&format!("categories/{}", slugify(category))) | |||||
self.config.make_permalink(&format!("{}/{}", &name, item.slug)) | |||||
); | ); | ||||
} | } | ||||
} | } | ||||
context.add("categories", &categories); | context.add("categories", &categories); | ||||
let mut tags = vec![]; | 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( | 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)?; | 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(()) | Ok(()) | ||||
} | } | ||||
pub fn render_rss_feed(&self) -> Result<()> { | pub fn render_rss_feed(&self) -> Result<()> { | ||||
self.ensure_public_directory_exists()?; | |||||
ensure_directory_exists(&self.output_path)?; | |||||
let mut context = Context::new(); | let mut context = Context::new(); | ||||
let pages = self.pages.values() | let pages = self.pages.values() | ||||
.filter(|p| p.meta.date.is_some()) | .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() | .cloned() | ||||
.collect::<Vec<Page>>(); | .collect::<Vec<Page>>(); | ||||
@@ -544,7 +467,7 @@ impl Site { | |||||
let sitemap = self.tera.render("rss.xml", &context)?; | 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(()) | Ok(()) | ||||
} | } | ||||
@@ -554,17 +477,17 @@ impl Site { | |||||
fn get_sections_map(&self) -> HashMap<String, Section> { | fn get_sections_map(&self) -> HashMap<String, Section> { | ||||
self.sections | self.sections | ||||
.values() | .values() | ||||
.map(|s| (s.components.join("/"), s.clone())) | |||||
.map(|s| (s.file.components.join("/"), s.clone())) | |||||
.collect() | .collect() | ||||
} | } | ||||
/// Renders a single section | /// Renders a single section | ||||
pub fn render_section(&self, section: &Section, render_pages: bool) -> Result<()> { | 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 public = self.output_path.clone(); | ||||
let mut output_path = public.to_path_buf(); | let mut output_path = public.to_path_buf(); | ||||
for component in §ion.components { | |||||
for component in §ion.file.components { | |||||
output_path.push(component); | output_path.push(component); | ||||
if !output_path.exists() { | if !output_path.exists() { | ||||
@@ -590,7 +513,7 @@ impl Site { | |||||
&self.tera, | &self.tera, | ||||
&self.config, | &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(()) | Ok(()) | ||||
@@ -610,7 +533,7 @@ impl Site { | |||||
/// Renders all pages that do not belong to any sections | /// Renders all pages that do not belong to any sections | ||||
pub fn render_orphan_pages(&self) -> Result<()> { | 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() { | for page in self.get_all_orphan_pages() { | ||||
self.render_page(page)?; | self.render_page(page)?; | ||||
@@ -621,7 +544,7 @@ impl Site { | |||||
/// Renders a list of pages when the section/index is wanting pagination. | /// Renders a list of pages when the section/index is wanting pagination. | ||||
fn render_paginated(&self, output_path: &Path, section: &Section) -> Result<()> { | 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 { | let paginate_path = match section.meta.paginate_path { | ||||
Some(ref s) => s.clone(), | Some(ref s) => s.clone(), | ||||
@@ -636,13 +559,63 @@ impl Site { | |||||
create_directory(&page_path)?; | create_directory(&page_path)?; | ||||
let output = paginator.render_pager(pager, self)?; | let output = paginator.render_pager(pager, self)?; | ||||
if i > 0 { | 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 { | } 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(()) | 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}; | use super::{markdown, base64_decode, base64_encode}; | ||||
#[test] | #[test] | ||||
fn test_markdown() { | |||||
fn markdown_filter() { | |||||
let result = markdown(to_value(&"# Hey").unwrap(), HashMap::new()); | let result = markdown(to_value(&"# Hey").unwrap(), HashMap::new()); | ||||
assert!(result.is_ok()); | assert!(result.is_ok()); | ||||
assert_eq!(result.unwrap(), to_value(&"<h1>Hey</h1>\n").unwrap()); | assert_eq!(result.unwrap(), to_value(&"<h1>Hey</h1>\n").unwrap()); | ||||
} | } | ||||
#[test] | #[test] | ||||
fn test_base64_encode() { | |||||
fn base64_encode_filter() { | |||||
// from https://tools.ietf.org/html/rfc4648#section-10 | // from https://tools.ietf.org/html/rfc4648#section-10 | ||||
let tests = vec![ | let tests = vec![ | ||||
("", ""), | ("", ""), | ||||
@@ -73,7 +73,7 @@ mod tests { | |||||
#[test] | #[test] | ||||
fn test_base64_decode() { | |||||
fn base64_decode_filter() { | |||||
let tests = vec![ | let tests = vec![ | ||||
("", ""), | ("", ""), | ||||
("Zg==", "f"), | ("Zg==", "f"), | ||||
@@ -3,13 +3,14 @@ use std::path::{PathBuf}; | |||||
use tera::{GlobalFn, Value, from_value, to_value, Result}; | 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 { | pub fn make_get_page(all_pages: &HashMap<PathBuf, Page>) -> GlobalFn { | ||||
let mut pages = HashMap::new(); | let mut pages = HashMap::new(); | ||||
for page in all_pages.values() { | 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> { | 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: | file_extensions: | ||||
- j2 | - j2 | ||||
- jinja2 | - jinja2 | ||||
scope: source.jinja2 | |||||
scope: text.html.jinja2 | |||||
contexts: | contexts: | ||||
main: | main: | ||||
- include: scope:text.html.basic | |||||
- match: '({%)\s*(raw)\s*(%})' | - match: '({%)\s*(raw)\s*(%})' | ||||
captures: | captures: | ||||
1: entity.other.jinja2.delimiter.tag | 1: entity.other.jinja2.delimiter.tag |
@@ -2,4 +2,5 @@ | |||||
title = "Posts" | title = "Posts" | ||||
paginate_by = 2 | paginate_by = 2 | ||||
template = "section_paginated.html" | template = "section_paginated.html" | ||||
insert_anchor = "left" | |||||
+++ | +++ |
@@ -8,3 +8,5 @@ date = "2017-01-01" | |||||
A simple page with a slug defined | A simple page with a slug defined | ||||
# Title | # Title | ||||
Hey |
@@ -1,3 +1,3 @@ | |||||
{% for category in categories %} | {% for category in categories %} | ||||
{{ category.name }} {{ category.slug }} {{ category.count }} | |||||
{{ category.name }} {{ category.slug }} {{ category.pages | length }} | |||||
{% endfor %} | {% endfor %} |
@@ -1,7 +1,7 @@ | |||||
Category: {{ category }} | |||||
Category: {{ category.name }} | |||||
{% for page in pages %} | |||||
{% for page in category.pages %} | |||||
<article> | <article> | ||||
<h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3> | <h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3> | ||||
</article> | </article> | ||||
@@ -1,6 +1,6 @@ | |||||
Tag: {{ tag }} | |||||
Tag: {{ tag.name }} | |||||
{% for page in pages %} | |||||
{% for page in tag.pages %} | |||||
<article> | <article> | ||||
<h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3> | <h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3> | ||||
</article> | </article> | ||||
@@ -1,3 +1,3 @@ | |||||
{% for tag in tags %} | {% for tag in tags %} | ||||
{{ tag.name }} {{ tag.slug }} {{ tag.count }} | |||||
{{ tag.name }} {{ tag.slug }} {{ tag.pages | length }} | |||||
{% endfor %} | {% 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] | #[test] | ||||
fn test_can_parse_site() { | |||||
fn can_parse_site() { | |||||
let mut path = env::current_dir().unwrap().to_path_buf(); | let mut path = env::current_dir().unwrap().to_path_buf(); | ||||
path.push("test_site"); | path.push("test_site"); | ||||
let mut site = Site::new(&path, "config.toml").unwrap(); | 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 | // Make sure we remove all the pwd + content from the sections | ||||
let basic = &site.pages[&posts_path.join("simple.md")]; | 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 | // Make sure the page with a url doesn't have any sections | ||||
let url_post = &site.pages[&posts_path.join("fixed-url.md")]; | 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 | // 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")]; | 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 | // That we have the right number of sections | ||||
assert_eq!(site.sections.len(), 6); | assert_eq!(site.sections.len(), 6); | ||||
@@ -89,7 +89,7 @@ macro_rules! file_contains { | |||||
} | } | ||||
#[test] | #[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(); | let mut path = env::current_dir().unwrap().to_path_buf(); | ||||
path.push("test_site"); | path.push("test_site"); | ||||
let mut site = Site::new(&path, "config.toml").unwrap(); | let mut site = Site::new(&path, "config.toml").unwrap(); | ||||
@@ -131,7 +131,7 @@ fn test_can_build_site_without_live_reload() { | |||||
} | } | ||||
#[test] | #[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(); | let mut path = env::current_dir().unwrap().to_path_buf(); | ||||
path.push("test_site"); | path.push("test_site"); | ||||
let mut site = Site::new(&path, "config.toml").unwrap(); | let mut site = Site::new(&path, "config.toml").unwrap(); | ||||
@@ -169,7 +169,7 @@ fn test_can_build_site_with_live_reload() { | |||||
} | } | ||||
#[test] | #[test] | ||||
fn test_can_build_site_with_categories() { | |||||
fn can_build_site_with_categories() { | |||||
let mut path = env::current_dir().unwrap().to_path_buf(); | let mut path = env::current_dir().unwrap().to_path_buf(); | ||||
path.push("test_site"); | path.push("test_site"); | ||||
let mut site = Site::new(&path, "config.toml").unwrap(); | let mut site = Site::new(&path, "config.toml").unwrap(); | ||||
@@ -190,7 +190,7 @@ fn test_can_build_site_with_categories() { | |||||
site.build().unwrap(); | site.build().unwrap(); | ||||
assert!(Path::new(&public).exists()); | 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, "index.html")); | ||||
assert!(file_exists!(public, "sitemap.xml")); | assert!(file_exists!(public, "sitemap.xml")); | ||||
@@ -221,7 +221,7 @@ fn test_can_build_site_with_categories() { | |||||
} | } | ||||
#[test] | #[test] | ||||
fn test_can_build_site_with_tags() { | |||||
fn can_build_site_with_tags() { | |||||
let mut path = env::current_dir().unwrap().to_path_buf(); | let mut path = env::current_dir().unwrap().to_path_buf(); | ||||
path.push("test_site"); | path.push("test_site"); | ||||
let mut site = Site::new(&path, "config.toml").unwrap(); | let mut site = Site::new(&path, "config.toml").unwrap(); | ||||
@@ -243,7 +243,7 @@ fn test_can_build_site_with_tags() { | |||||
site.build().unwrap(); | site.build().unwrap(); | ||||
assert!(Path::new(&public).exists()); | 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, "index.html")); | ||||
assert!(file_exists!(public, "sitemap.xml")); | assert!(file_exists!(public, "sitemap.xml")); | ||||
@@ -273,11 +273,10 @@ fn test_can_build_site_with_tags() { | |||||
} | } | ||||
#[test] | #[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(); | let mut path = env::current_dir().unwrap().to_path_buf(); | ||||
path.push("test_site"); | path.push("test_site"); | ||||
let mut site = Site::new(&path, "config.toml").unwrap(); | let mut site = Site::new(&path, "config.toml").unwrap(); | ||||
site.config.insert_anchor_links = Some(true); | |||||
site.load().unwrap(); | site.load().unwrap(); | ||||
let tmp_dir = TempDir::new("example").expect("create temp dir"); | let tmp_dir = TempDir::new("example").expect("create temp dir"); | ||||
let public = &tmp_dir.path().join("public"); | 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()); | assert!(Path::new(&public).exists()); | ||||
// anchor link inserted | // 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] | #[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(); | let mut path = env::current_dir().unwrap().to_path_buf(); | ||||
path.push("test_site"); | path.push("test_site"); | ||||
let mut site = Site::new(&path, "config.toml").unwrap(); | let mut site = Site::new(&path, "config.toml").unwrap(); | ||||
@@ -349,7 +348,7 @@ fn test_can_build_site_with_pagination_for_section() { | |||||
} | } | ||||
#[test] | #[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(); | let mut path = env::current_dir().unwrap().to_path_buf(); | ||||
path.push("test_site"); | path.push("test_site"); | ||||
let mut site = Site::new(&path, "config.toml").unwrap(); | 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!(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_prev"), false); | ||||
assert_eq!(file_contains!(public, "index.html", "has_next"), false); | assert_eq!(file_contains!(public, "index.html", "has_next"), false); | ||||
} | } |