Browse Source

Merge pull request #68 from Keats/moar

v0.0.6
index-subcmd
Vincent Prouillet GitHub 7 years ago
parent
commit
5559dff996
44 changed files with 1212 additions and 2116 deletions
  1. +10
    -0
      CHANGELOG.md
  2. +63
    -63
      Cargo.lock
  3. +7
    -4
      README.md
  4. +3
    -3
      src/bin/cmd/init.rs
  5. +1
    -1156
      src/bin/cmd/livereload.js
  6. +12
    -13
      src/bin/cmd/serve.rs
  7. +12
    -6
      src/bin/console.rs
  8. +1
    -1
      src/bin/gutenberg.rs
  9. +28
    -8
      src/bin/rebuild.rs
  10. +25
    -5
      src/config.rs
  11. +116
    -0
      src/content/file_info.rs
  12. +3
    -3
      src/content/mod.rs
  13. +168
    -71
      src/content/page.rs
  14. +2
    -2
      src/content/pagination.rs
  15. +24
    -51
      src/content/section.rs
  16. +12
    -4
      src/content/sorting.rs
  17. +135
    -0
      src/content/taxonomies.rs
  18. +0
    -1
      src/content/utils.rs
  19. +1
    -1
      src/front_matter/mod.rs
  20. +0
    -3
      src/front_matter/page.rs
  21. +17
    -0
      src/front_matter/section.rs
  22. +40
    -0
      src/fs.rs
  23. +4
    -5
      src/lib.rs
  24. +33
    -0
      src/rendering/context.rs
  25. +6
    -0
      src/rendering/highlighting.rs
  26. +152
    -177
      src/rendering/markdown.rs
  27. +4
    -0
      src/rendering/mod.rs
  28. +101
    -0
      src/rendering/short_code.rs
  29. +164
    -191
      src/site.rs
  30. +1
    -1
      src/templates/builtins/anchor-link.html
  31. +3
    -3
      src/templates/filters.rs
  32. +40
    -2
      src/templates/global_fns.rs
  33. +0
    -69
      src/utils.rs
  34. +2
    -1
      sublime_syntaxes/Jinja2.sublime-syntax
  35. BIN
      sublime_syntaxes/newlines.packdump
  36. BIN
      sublime_syntaxes/nonewlines.packdump
  37. +1
    -0
      test_site/content/posts/_index.md
  38. +2
    -0
      test_site/content/posts/fixed-slug.md
  39. +1
    -1
      test_site/templates/categories.html
  40. +2
    -2
      test_site/templates/category.html
  41. +2
    -2
      test_site/templates/tag.html
  42. +1
    -1
      test_site/templates/tags.html
  43. +0
    -251
      tests/page.rs
  44. +13
    -15
      tests/site.rs

+ 10
- 0
CHANGELOG.md View File

@@ -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


+ 63
- 63
Cargo.lock View File

@@ -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"


+ 7
- 4
README.md View File

@@ -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.


+ 3
- 3
src/bin/cmd/init.rs View File

@@ -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"))?;


+ 1
- 1156
src/bin/cmd/livereload.js
File diff suppressed because it is too large
View File


+ 12
- 13
src/bin/cmd/serve.rs View File

@@ -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));
} }
} }


+ 12
- 6
src/bin/console.rs View File

@@ -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));
}
} }

+ 1
- 1
src/bin/gutenberg.rs View File

@@ -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);
}, },
}; };


+ 28
- 8
src/bin/rebuild.rs View File

@@ -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();


+ 25
- 5
src/config.rs View File

@@ -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");
}
} }

+ 116
- 0
src/content/file_info.rs View File

@@ -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()]);
}
}

+ 3
- 3
src/content/mod.rs View File

@@ -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};



+ 168
- 71
src/content/page.rs View File

@@ -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());
}
}

+ 2
- 2
src/content/pagination.rs View File

@@ -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();
} }


+ 24
- 51
src/content/section.rs View File

@@ -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(&section.file_path);
section.path = section.components.join("/");
section.path = section.file.components.join("/");
section.permalink = config.make_permalink(&section.path); section.permalink = config.make_permalink(&section.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(),


+ 12
- 4
src/content/sorting.rs View File

@@ -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]


+ 135
- 0
src/content/taxonomies.rs View File

@@ -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))
}
}

+ 0
- 1
src/content/utils.rs View File

@@ -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;


+ 1
- 1
src/front_matter/mod.rs View 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();


+ 0
- 3
src/front_matter/page.rs View File

@@ -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,


+ 17
- 0
src/front_matter/section.rs View File

@@ -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,
} }
} }


+ 40
- 0
src/fs.rs View File

@@ -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)
}

+ 4
- 5
src/lib.rs View File

@@ -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};

+ 33
- 0
src/rendering/context.rs View 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
}
}

+ 6
- 0
src/rendering/highlighting.rs View File

@@ -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"));
}

src/markdown.rs → src/rendering/markdown.rs View File

@@ -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=&quot;w7Ft2ymGmfc&quot;) }}</code></p>\n"); assert_eq!(res, "<p><code>{{ youtube(id=&quot;w7Ft2ymGmfc&quot;) }}</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"
); );
} }
} }

+ 4
- 0
src/rendering/mod.rs View File

@@ -0,0 +1,4 @@
pub mod highlighting;
pub mod markdown;
pub mod short_code;
pub mod context;

+ 101
- 0
src/rendering/short_code.rs View File

@@ -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");
}

}

+ 164
- 191
src/site.rs View File

@@ -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(&section.parent_path) {
match grandparent_paths.get(&section.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(&current_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 &section.components {
for component in &section.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(&section.permalink, &self.tera)?)?;
create_file(&output_path.join("index.html"), &self.inject_livereload(output))?;
create_file(&page_path.join("index.html"), &render_redirect_template(&section.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
src/templates/builtins/anchor-link.html View File

@@ -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>

+ 3
- 3
src/templates/filters.rs View File

@@ -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"),


+ 40
- 2
src/templates/global_fns.rs View File

@@ -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()),
}
})
}

+ 0
- 69
src/utils.rs View File

@@ -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()]);
}
}

sublime_syntaxes/Jinj2.sublime-syntax → sublime_syntaxes/Jinja2.sublime-syntax View File

@@ -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

BIN
sublime_syntaxes/newlines.packdump View File


BIN
sublime_syntaxes/nonewlines.packdump View File


+ 1
- 0
test_site/content/posts/_index.md View File

@@ -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"
+++ +++

+ 2
- 0
test_site/content/posts/fixed-slug.md View File

@@ -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
- 1
test_site/templates/categories.html View File

@@ -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 %}

+ 2
- 2
test_site/templates/category.html View File

@@ -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>


+ 2
- 2
test_site/templates/tag.html View File

@@ -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
- 1
test_site/templates/tags.html View File

@@ -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 %}

+ 0
- 251
tests/page.rs View File

@@ -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());
}

+ 13
- 15
tests/site.rs View File

@@ -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);

} }

Loading…
Cancel
Save