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

## 0.0.6 (unreleased)

- Fix missing serialized data for sections
- Change the single item template context for categories/tags
- Add a `get_url` and a `get_section` global Tera function
- Add a config option to control how many articles to show in RSS feed
- Move `insert_anchor_links` from config to being a section option and it can
now be insert left or right


## 0.0.5 (2017-05-15)

- Fix XML templates overriding and reloading


+ 63
- 63
Cargo.lock View File

@@ -4,7 +4,7 @@ version = "0.0.5"
dependencies = [
"base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.24.1 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)",
"error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -12,14 +12,14 @@ dependencies = [
"mount 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"notify 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"pulldown-cmark 0.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"slug 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"staticfile 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"syntect 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"tera 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)",
"tera 0.10.6 (registry+https://github.com/rust-lang/crates.io-index)",
"term-painter 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -45,20 +45,20 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "backtrace"
version = "0.3.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-demangle 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -69,7 +69,7 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"gcc 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -155,7 +155,7 @@ dependencies = [

[[package]]
name = "clap"
version = "2.24.1"
version = "2.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -163,9 +163,9 @@ dependencies = [
"bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"vec_map 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -212,7 +212,7 @@ name = "error-chain"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -220,7 +220,7 @@ name = "filetime"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -228,7 +228,7 @@ name = "flate2"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"miniz-sys 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
]

@@ -244,7 +244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -252,7 +252,7 @@ name = "fsevent-sys"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -301,11 +301,11 @@ dependencies = [

[[package]]
name = "idna"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-bidi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-bidi 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]

@@ -314,7 +314,7 @@ name = "inotify"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -322,7 +322,7 @@ name = "iovec"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]

@@ -374,7 +374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "libc"
version = "0.2.22"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
@@ -392,7 +392,7 @@ name = "memchr"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -409,7 +409,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"gcc 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -418,7 +418,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"miow 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"net2 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -436,7 +436,7 @@ dependencies = [
"iovec 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"net2 0.2.29 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -487,7 +487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -498,7 +498,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -512,7 +512,7 @@ dependencies = [
"fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"inotify 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -555,7 +555,7 @@ name = "num_cpus"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -565,7 +565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"onig_sys 61.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

@@ -575,7 +575,7 @@ version = "61.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cmake 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]

@@ -628,7 +628,7 @@ name = "rand"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -638,19 +638,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "regex"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "regex-syntax"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
@@ -702,12 +702,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "serde"
version = "1.0.5"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "serde_derive"
version = "1.0.5"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -732,7 +732,7 @@ dependencies = [
"dtoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -804,7 +804,7 @@ dependencies = [
"lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"onig 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"plist 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"yaml-rust 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -820,7 +820,7 @@ dependencies = [

[[package]]
name = "tera"
version = "0.10.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -829,8 +829,8 @@ dependencies = [
"humansize 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"pest 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"slug 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -859,7 +859,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]

@@ -869,7 +869,7 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -887,7 +887,7 @@ version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -897,7 +897,7 @@ name = "toml"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
@@ -928,7 +928,7 @@ dependencies = [

[[package]]
name = "unicode-bidi"
version = "0.2.5"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -941,7 +941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "unicode-segmentation"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
@@ -980,7 +980,7 @@ name = "url"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"idna 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"idna 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]

@@ -991,7 +991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "vec_map"
version = "0.7.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
@@ -1070,7 +1070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699"
"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
"checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159"
"checksum backtrace 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f551bc2ddd53aea015d453ef0b635af89444afa5ed2405dd0b2062ad5d600d80"
"checksum backtrace 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "72f9b4182546f4b04ebc4ab7f84948953a118bd6021a1b6a6c909e3e94f6be76"
"checksum backtrace-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d192fd129132fbc97497c1f2ec2c2c5174e376b95f535199ef4fe0a293d33842"
"checksum base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557"
"checksum bincode 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "55eb0b7fd108527b0c77860f75eca70214e11a8b4c6ef05148c54c05a25d48ad"
@@ -1084,7 +1084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c"
"checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00"
"checksum chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d9123be86fd2a8f627836c235ecdf331fdd067ecf7ac05aa1a68fbcf2429f056"
"checksum clap 2.24.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7541069be0b8aec41030802abe8b5cdef0490070afaa55418adea93b1e431e0"
"checksum clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b8f69e518f967224e628896b54e41ff6acfb4dcfefc5076325c36525dac900f"
"checksum cmake 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)" = "92278eb79412c8f75cfc89e707a1bb3a6490b68f7f2e78d15c774f30fe701122"
"checksum conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "95ca30253581af809925ef68c2641cc140d6183f43e12e0af4992d53768bd7b8"
"checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850"
@@ -1102,7 +1102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum httparse 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77f756bed9ee3a83ce98774f4155b42a31b787029013f3a7d83eca714e500e21"
"checksum humansize 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "92d211e6e70b05749dce515b47684f29a3c8c38bbbb21c50b30aff9eca1b0bd3"
"checksum hyper 0.10.10 (registry+https://github.com/rust-lang/crates.io-index)" = "36e108e0b1fa2d17491cbaac4bc460dc0956029d10ccf83c913dd0e5db3e7f07"
"checksum idna 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6ac85ec3f80c8e4e99d9325521337e14ec7555c458a14e377d189659a427f375"
"checksum idna 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2233d4940b1f19f0418c158509cd7396b8d70a5db5705ce410914dc8fa603b37"
"checksum inotify 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887fcc180136e77a85e6a6128579a719027b1bab9b1c38ea4444244fe262c20c"
"checksum iovec 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "29d062ee61fccdf25be172e70f34c9f6efc597e1fb8f6526e8437b2046ab26be"
"checksum iron 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2440ae846e7a8c7f9b401db8f6e31b4ea5e7d3688b91761337da7e054520c75b"
@@ -1111,7 +1111,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
"checksum lazy_static 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3b37545ab726dd833ec6420aaba8231c5b320814b9029ad585555d2a03e94fbf"
"checksum lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce12306c4739d86ee97c23139f3a34ddf0387bbf181bc7929d287025a8c3ef6b"
"checksum libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)" = "babb8281da88cba992fa1f4ddec7d63ed96280a1a53ec9b919fd37b53d71e502"
"checksum libc 0.2.23 (registry+https://github.com/rust-lang/crates.io-index)" = "e7eb6b826bfc1fdea7935d46556250d1799b7fe2d9f7951071f4291710665e3e"
"checksum log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5141eca02775a762cc6cd564d8d2c50f67c0ea3a372cbf1c51592b3e029e10ad"
"checksum matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efd7622e3022e1a6eaa602c4cea8912254e5582c9c692e9167714182244801b1"
"checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4"
@@ -1141,8 +1141,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d"
"checksum redox_syscall 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "29dbdfd4b9df8ab31dec47c6087b7b13cbf4a776f335e4de8efba8288dda075b"
"checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01"
"checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457"
"checksum regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1731164734096285ec2a5ec7fea5248ae2f5485b3feeb0115af4fda2183b2d1b"
"checksum regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad890a5eef7953f55427c50575c680c42841653abd2b028b68cd223d157f62db"
"checksum rustc-demangle 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3058a43ada2c2d0b92b3ae38007a2d0fa5e9db971be260e0171408a4ff471c95"
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084"
@@ -1151,8 +1151,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum sequence_trie 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c915714ca833b1d4d6b8f6a9d72a3ff632fe45b40a8d184ef79c81bec6327eed"
"checksum serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8"
"checksum serde 0.9.15 (registry+https://github.com/rust-lang/crates.io-index)" = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af"
"checksum serde 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e101024c846392aadc80d5d452f2ff011f9bff1a0441151f8575e8a23488ef95"
"checksum serde_derive 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "7c85839e9a00a5b0c7bddb1e44b8c3907c7aba5fe234c7ec5ccef1c188eb41d9"
"checksum serde 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c0c3d79316a6051231925504f6ef893d45088e8823c77a8331a3dcf427ee9087"
"checksum serde_derive 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0019cd5b9f0529a1a0e145a912e9a2d60c325c58f7f260fc36c71976e9d76aee"
"checksum serde_derive_internals 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "021c338d22c7e30f957a6ab7e388cb6098499dda9fd4ba1661ee074ca7a180d1"
"checksum serde_json 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "48b04779552e92037212c3615370f6bd57a40ebba7f20e554ff9f55e41a69a7b"
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c"
@@ -1165,7 +1165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
"checksum syntect 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24204b1f4bdd49f84e5f4b219d0bf1dc45ac2fd7fc46320ab6627b537d6d4b69"
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
"checksum tera 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)" = "5ce5ea7e2239a92d2bb662b8a337d8a3c45b9e6d630d113b0ca18dd6e64fb05d"
"checksum tera 0.10.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c931ade2857155d5e55115375d4d2b8a441536e2b9e44643a8b67e235e09030"
"checksum term 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d168af3930b369cfe245132550579d47dfd873d69470755a19c2c6568dbbd989"
"checksum term-painter 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ab900bf2f05175932b13d4fc12f8ff09ef777715b04998791ab2c930841e496b"
"checksum term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2b6b55df3198cc93372e85dd2ed817f0e38ce8cc0f22eb32391bfad9c4bf209"
@@ -1177,9 +1177,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887"
"checksum typemap 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6"
"checksum unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a5906ca2b98c799f4b1ab4557b76367ebd6ae5ef14930ec841c74aed5f3764"
"checksum unicode-bidi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d3a078ebdd62c0e71a709c3d53d2af693fe09fe93fbff8344aebe289b78f9032"
"checksum unicode-bidi 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c44d4e7ce691e2538b886bf33669fd6da1653a12d741b9390f351955c0949c03"
"checksum unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e28fa37426fceeb5cf8f41ee273faa7c82c47dc8fba5853402841e665fcd86ff"
"checksum unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18127285758f0e2c6cf325bb3f3d138a12fee27de4f23e146cd6a179f26c2cf3"
"checksum unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a8083c594e02b8ae1654ae26f0ade5158b119bd88ad0e8227a5d8fcd72407946"
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
"checksum unidecode 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d2adb95ee07cd579ed18131f2d9e7a17c25a4b76022935c7f2460d2bfae89fd2"
@@ -1187,7 +1187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum unsafe-any 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b351086021ebc264aea3ab4f94d61d889d98e5e9ec2d985d993f50133537fd3a"
"checksum url 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f5ba8a749fb4479b043733416c244fa9d1d3af3d7c23804944651c8a448cb87e"
"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122"
"checksum vec_map 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8cdc8b93bd0198ed872357fb2e667f7125646b1762f16d60b2c96350d361897"
"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c"
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
"checksum walkdir 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c66c0b9792f0a765345452775f3adbd28dde9d33f30d13e5dcc5ae17cf6f3780"
"checksum walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "bb08f9e670fab86099470b97cd2b252d6527f0b3cc1401acdb595ffc9dd288ff"


+ 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)`.

### Anchors
Headers get an automatic id from their content in order to be able to add deep links. By default no links are actually created but
the `insert_anchor_links` option in `config.toml` can be set to `true` to link tags. The default template is very ugly and will need
CSS tweaks in your projet to look decent. The default template can also be easily overwritten by creating a `anchor-link.html` file in
the `templates` directory.
Headers get an automatic id from their content in order to be able to add deep links.
You can also choose, at the section level, whether to automatically insert an anchor link next to it. It is turned off by default
but can be turned on by setting `insert_anchor = "left"` or `insert_anchor = "right"` in the `_index.md` file. `left` will insert
the anchor link before the title text and right will insert it after.

The default template is very basic and will need CSS tweaks in your projet to look decent.
It can easily be overwritten by creating a `anchor-link.html` file in the `templates` directory.

### Shortcodes
Gutenberg uses markdown for content but sometimes you want to insert some HTML, for example for a YouTube video.


+ 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
if path.exists() && path.is_dir() {
@@ -26,7 +26,7 @@ pub fn create_new_project<P: AsRef<Path>>(name: P) -> Result<()> {

// main folder
create_dir(path)?;
create_file(path.join("config.toml"), CONFIG.trim_left())?;
create_file(&path.join("config.toml"), CONFIG.trim_left())?;

// content folder
create_dir(path.join("content"))?;


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

// Uglified using uglifyjs
// Also, commenting out the lines 330-340 (containing `e instanceof ProtocolError`) was needed
// as it seems their build didn't work well and didn't include ProtocolError so it would error on
// errors
const LIVE_RELOAD: &'static str = include_str!("livereload.js");


@@ -49,8 +53,6 @@ fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &st
}
}


// Most of it taken from mdbook
pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> {
let start = Instant::now();
let mut site = Site::new(env::current_dir().unwrap(), config_file)?;
@@ -88,7 +90,8 @@ pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> {
mount.mount("/livereload.js", livereload_handler);
// Starts with a _ to not trigger the unused lint
// we need to assign to a variable otherwise it will block
let _iron = Iron::new(mount).http(address.as_str()).unwrap();
let _iron = Iron::new(mount).http(address.as_str())
.chain_err(|| "Can't start the webserver")?;

// The websocket for livereload
let ws_server = WebSocket::new(|output: Sender| {
@@ -119,8 +122,6 @@ pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> {
use notify::DebouncedEvent::*;

loop {
// See https://github.com/spf13/hugo/blob/master/commands/hugo.go
// for a more complete version of that
match rx.recv() {
Ok(event) => {
match event {
@@ -162,7 +163,6 @@ pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> {
}
}


/// Returns whether the path we received corresponds to a temp file created
/// by an editor or the OS
fn is_temp_file(path: &Path) -> bool {
@@ -191,7 +191,6 @@ fn is_temp_file(path: &Path) -> bool {
}
}


/// Detect what changed from the given path so we have an idea what needs
/// to be reloaded
fn detect_change_kind(pwd: &str, path: &Path) -> (ChangeKind, String) {
@@ -218,8 +217,8 @@ mod tests {
use super::{is_temp_file, detect_change_kind, ChangeKind};

#[test]
fn test_can_recognize_temp_files() {
let testcases = vec![
fn can_recognize_temp_files() {
let test_cases = vec![
Path::new("hello.swp"),
Path::new("hello.swx"),
Path::new(".DS_STORE"),
@@ -231,14 +230,14 @@ mod tests {
Path::new("#hello.html"),
];

for t in testcases {
for t in test_cases {
assert!(is_temp_file(&t));
}
}

#[test]
fn test_can_detect_kind_of_changes() {
let testcases = vec![
fn can_detect_kind_of_changes() {
let test_cases = vec![
(
(ChangeKind::Templates, "/templates/hello.html".to_string()),
"/home/vincent/site", Path::new("/home/vincent/site/templates/hello.html")
@@ -253,7 +252,7 @@ mod tests {
),
];

for (expected, pwd, path) in testcases {
for (expected, pwd, path) in test_cases {
assert_eq!(expected, detect_change_kind(&pwd, &path));
}
}


+ 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
pub fn warn_about_ignored_pages(site: &Site) {
let ignored_pages = site.get_ignored_pages();
let ignored_pages: Vec<_> = site.sections
.values()
.flat_map(|s| s.ignored_pages.iter().map(|p| p.file.path.clone()))
.collect();

if !ignored_pages.is_empty() {
warn(&format!(
"{} page(s) ignored (missing date or order in a sorted section):",
ignored_pages.len()
));
for path in site.get_ignored_pages() {
for path in ignored_pages {
warn(&format!("- {}", path.display()));
}
}
@@ -62,9 +66,11 @@ pub fn report_elapsed_time(instant: Instant) {

/// Display an error message and the actual error(s)
pub fn unravel_errors(message: &str, error: &Error) {
if !message.is_empty() {
self::error(message);
self::error(&format!("Error: {}", error));
for e in error.iter().skip(1) {
self::error(&format!("Reason: {}", e));
}
}
self::error(&format!("Error: {}", error));
for e in error.iter().skip(1) {
self::error(&format!("Reason: {}", e));
}
}

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

@@ -70,7 +70,7 @@ fn main() {
match cmd::serve(interface, port, config_file) {
Ok(()) => (),
Err(e) => {
console::unravel_errors("Failed to build the site", &e);
console::unravel_errors("", &e);
::std::process::exit(1);
},
};


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

@@ -1,8 +1,21 @@
use std::path::Path;

use gutenberg::{Site, SectionFrontMatter, PageFrontMatter};
use gutenberg::{Site, Page, Section, SectionFrontMatter, PageFrontMatter};
use gutenberg::errors::Result;


/// Finds the section that contains the page given if there is one
pub fn find_parent_section<'a>(site: &'a Site, page: &Page) -> Option<&'a Section> {
for section in site.sections.values() {
if section.is_child_page(&page.file.path) {
return Some(section)
}
}

None
}


#[derive(Debug, Clone, Copy, PartialEq)]
enum PageChangesNeeded {
/// Editing `tags`
@@ -22,7 +35,7 @@ enum SectionChangesNeeded {
Sort,
/// Editing `title`, `description`, `extra`, `template` or setting `render` to true
Render,
/// Editing `paginate_by` or `paginate_path`
/// Editing `paginate_by`, `paginate_path` or `insert_anchor`
RenderWithPages,
/// Setting `render` to false
Delete,
@@ -43,7 +56,9 @@ fn find_section_front_matter_changes(current: &SectionFrontMatter, other: &Secti
return changes_needed;
}

if current.paginate_by != other.paginate_by || current.paginate_path != other.paginate_path {
if current.paginate_by != other.paginate_by
|| current.paginate_path != other.paginate_path
|| current.insert_anchor != other.insert_anchor {
changes_needed.push(SectionChangesNeeded::RenderWithPages);
// Nothing else we can do
return changes_needed;
@@ -85,7 +100,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
// A section was deleted, many things can be impacted:
// - the pages of the section are becoming orphans
// - any page that was referencing the section (index, etc)
let relative_path = site.sections[path].relative_path.clone();
let relative_path = site.sections[path].file.relative.clone();
// Remove the link to it and the section itself from the Site
site.permalinks.remove(&relative_path);
site.sections.remove(path);
@@ -94,18 +109,20 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
// A page was deleted, many things can be impacted:
// - the section the page is in
// - any page that was referencing the section (index, etc)
let relative_path = site.pages[path].relative_path.clone();
let relative_path = site.pages[path].file.relative.clone();
site.permalinks.remove(&relative_path);
if let Some(p) = site.pages.remove(path) {
if p.meta.has_tags() || p.meta.category.is_some() {
site.populate_tags_and_categories();
}

if site.find_parent_section(&p).is_some() {
if find_parent_section(site, &p).is_some() {
site.populate_sections();
}
};
}
// Ensure we have our fn updated so it doesn't contain the permalinks deleted
site.register_get_url_fn();
// Deletion is something that doesn't happen all the time so we
// don't need to optimise it too much
return site.build();
@@ -140,6 +157,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
return Ok(());
},
None => {
site.register_get_url_fn();
// New section, only render that one
site.populate_sections();
return site.render_section(&site.sections[path], true);
@@ -150,6 +168,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
// A page was edited
match site.add_page(path, true)? {
Some(prev) => {
site.register_get_url_fn();
// Updating a page
let current = site.pages[path].clone();
// Front matter didn't change, only content did
@@ -171,8 +190,8 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
site.render_categories()?;
},
PageChangesNeeded::Sort => {
let section_path = match site.find_parent_section(&site.pages[path]) {
Some(s) => s.file_path.clone(),
let section_path = match find_parent_section(site, &site.pages[path]) {
Some(s) => s.file.path.clone(),
None => continue // Do nothing if it's an orphan page
};
site.populate_sections();
@@ -188,6 +207,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {

},
None => {
site.register_get_url_fn();
// It's a new page!
site.populate_sections();
site.populate_tags_and_categories();


+ 25
- 5
src/config.rs View File

@@ -6,7 +6,7 @@ use std::collections::HashMap;
use toml::{Value as Toml, self};

use errors::{Result, ResultExt};
use markdown::SETUP;
use rendering::highlighting::THEME_SET;


#[derive(Debug, PartialEq, Serialize, Deserialize)]
@@ -24,8 +24,10 @@ pub struct Config {
pub description: Option<String>,
/// The language used in the site. Defaults to "en"
pub language_code: Option<String>,
/// Whether to generate RSS, defaults to false
/// Whether to generate RSS. Defaults to false
pub generate_rss: Option<bool>,
/// The number of articles to include in the RSS feed. Defaults to unlimited
pub rss_limit: Option<usize>,
/// Whether to generate tags and individual tag pages if some pages have them. Defaults to true
pub generate_tags_pages: Option<bool>,
/// Whether to generate categories and individual tag categories if some pages have them. Defaults to true
@@ -59,13 +61,14 @@ impl Config {
set_default!(config.language_code, "en".to_string());
set_default!(config.highlight_code, false);
set_default!(config.generate_rss, false);
set_default!(config.rss_limit, <usize>::max_value());
set_default!(config.generate_tags_pages, false);
set_default!(config.generate_categories_pages, false);
set_default!(config.insert_anchor_links, false);

match config.highlight_theme {
Some(ref t) => {
if !SETUP.theme_set.themes.contains_key(t) {
if !THEME_SET.themes.contains_key(t) {
bail!("Theme {} not available", t)
}
},
@@ -87,7 +90,9 @@ impl Config {

/// Makes a url, taking into account that the base url might have a trailing slash
pub fn make_permalink(&self, path: &str) -> String {
if self.base_url.ends_with('/') {
if self.base_url.ends_with('/') && path.starts_with('/') {
format!("{}{}", self.base_url, &path[1..])
} else if self.base_url.ends_with('/') {
format!("{}{}", self.base_url, path)
} else {
format!("{}/{}", self.base_url, path)
@@ -95,8 +100,9 @@ impl Config {
}
}

/// Exists only for testing purposes
#[doc(hidden)]
impl Default for Config {
/// Exists for testing purposes
fn default() -> Config {
Config {
title: "".to_string(),
@@ -106,6 +112,7 @@ impl Default for Config {
description: None,
language_code: Some("en".to_string()),
generate_rss: Some(false),
rss_limit: Some(10000),
generate_tags_pages: Some(true),
generate_categories_pages: Some(true),
insert_anchor_links: Some(false),
@@ -181,4 +188,17 @@ hello = "world"
assert_eq!(config.unwrap().extra.unwrap().get("hello").unwrap().as_str().unwrap(), "world");
}

#[test]
fn can_make_url_with_non_trailing_slash_base_url() {
let mut config = Config::default();
config.base_url = "http://vincent.is".to_string();
assert_eq!(config.make_permalink("hello"), "http://vincent.is/hello");
}

#[test]
fn can_make_url_with_trailing_slash_path() {
let mut config = Config::default();
config.base_url = "http://vincent.is/".to_string();
assert_eq!(config.make_permalink("/hello"), "http://vincent.is/hello");
}
}

+ 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 pagination;
mod section;
mod sorting;
mod utils;
mod file_info;
mod taxonomies;

pub use self::page::{Page};
pub use self::section::{Section};
pub use self::pagination::{Paginator, Pager};
pub use self::sorting::{SortBy, sort_pages, populate_previous_and_next_pages};
pub use self::taxonomies::{Taxonomy, TaxonomyItem};


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

@@ -4,43 +4,32 @@ use std::path::{Path, PathBuf};
use std::result::Result as StdResult;


use tera::{Tera, Context};
use tera::{Tera, Context as TeraContext};
use serde::ser::{SerializeStruct, self};
use slug::slugify;

use errors::{Result, ResultExt};
use config::Config;
use front_matter::{PageFrontMatter, split_page_content};
use markdown::markdown_to_html;
use utils::{read_file, find_content_components};
use front_matter::{PageFrontMatter, InsertAnchor, split_page_content};
use rendering::markdown::markdown_to_html;
use rendering::context::Context;
use fs::{read_file};
use content::utils::{find_related_assets, get_reading_analytics};
use content::file_info::FileInfo;


#[derive(Clone, Debug, PartialEq)]
pub struct Page {
/// All info about the actual file
pub file: FileInfo,
/// The front matter meta-data
pub meta: PageFrontMatter,
/// The .md path
pub file_path: PathBuf,
/// The .md path, starting from the content directory, with / slashes
pub relative_path: String,
/// The parent directory of the file. Is actually the grand parent directory
/// if it's an asset folder
pub parent_path: PathBuf,
/// The name of the .md file
pub file_name: String,
/// The directories above our .md file
/// for example a file at content/kb/solutions/blabla.md will have 2 components:
/// `kb` and `solutions`
pub components: Vec<String>,
/// The actual content of the page, in markdown
pub raw_content: String,
/// All the non-md files we found next to the .md file
pub assets: Vec<PathBuf>,
/// The HTML rendered of the page
pub content: String,

/// The slug of that page.
/// First tries to find the slug in the meta and defaults to filename otherwise
pub slug: String,
@@ -52,7 +41,6 @@ pub struct Page {
/// When <!-- more --> is found in the text, will take the content up to that part
/// as summary
pub summary: Option<String>,

/// The previous page, by whatever sorting is used for the index/section
pub previous: Option<Box<Page>>,
/// The next page, by whatever sorting is used for the index/section
@@ -61,14 +49,12 @@ pub struct Page {


impl Page {
pub fn new(meta: PageFrontMatter) -> Page {
pub fn new<P: AsRef<Path>>(file_path: P, meta: PageFrontMatter) -> Page {
let file_path = file_path.as_ref();

Page {
file: FileInfo::new_page(file_path),
meta: meta,
file_path: PathBuf::new(),
relative_path: String::new(),
parent_path: PathBuf::new(),
file_name: "".to_string(),
components: vec![],
raw_content: "".to_string(),
assets: vec![],
content: "".to_string(),
@@ -85,49 +71,26 @@ impl Page {
/// Files without front matter or with invalid front matter are considered
/// erroneous
pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Page> {
// 1. separate front matter from content
let (meta, content) = split_page_content(file_path, content)?;
let mut page = Page::new(meta);
page.file_path = file_path.to_path_buf();
page.parent_path = page.file_path.parent().unwrap().to_path_buf();
let mut page = Page::new(file_path, meta);
page.raw_content = content;

let path = Path::new(file_path);
page.file_name = path.file_stem().unwrap().to_string_lossy().to_string();

page.slug = {
if let Some(ref slug) = page.meta.slug {
slug.trim().to_string()
} else {
slugify(page.file_name.clone())
slugify(page.file.name.clone())
}
};
page.components = find_content_components(&page.file_path);
page.relative_path = format!("{}/{}.md", page.components.join("/"), page.file_name);

// 4. Find sections
// Pages with custom urls exists outside of sections
let mut path_set = false;
if let Some(ref u) = page.meta.url {
page.path = u.trim().to_string();
path_set = true;
}

if !page.components.is_empty() {
// If we have a folder with an asset, don't consider it as a component
if page.file_name == "index" {
page.components.pop();
// also set parent_path to grandparent instead
page.parent_path = page.parent_path.parent().unwrap().to_path_buf();
}
if !path_set {
// Don't add a trailing slash to sections
page.path = format!("{}/{}", page.components.join("/"), page.slug);
}
} else if !path_set {
page.path = page.slug.clone();
} else {
page.path = if page.file.components.is_empty() {
page.slug.clone()
} else {
format!("{}/{}", page.file.components.join("/"), page.slug)
};
}

page.permalink = config.make_permalink(&page.path);

Ok(page)
@@ -140,7 +103,7 @@ impl Page {
let mut page = Page::parse(path, &content, config)?;
page.assets = find_related_assets(path.parent().unwrap());

if !page.assets.is_empty() && page.file_name != "index" {
if !page.assets.is_empty() && page.file.name != "index" {
bail!("Page `{}` has assets ({:?}) but is not named index.md", path.display(), page.assets);
}

@@ -150,13 +113,13 @@ impl Page {

/// We need access to all pages url to render links relative to content
/// so that can't happen at the same time as parsing
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<()> {
self.content = markdown_to_html(&self.raw_content, permalinks, tera, config)?;
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config, anchor_insert: InsertAnchor) -> Result<()> {
let context = Context::new(tera, config, permalinks, anchor_insert);
self.content = markdown_to_html(&self.raw_content, &context)?;
if self.raw_content.contains("<!-- more -->") {
self.summary = Some({
let summary = self.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[0];
markdown_to_html(summary, permalinks, tera, config)?
markdown_to_html(summary, &context)?
})
}

@@ -170,26 +133,22 @@ impl Page {
None => "page.html".to_string()
};

let mut context = Context::new();
let mut context = TeraContext::new();
context.add("config", config);
context.add("page", self);
context.add("current_url", &self.permalink);
context.add("current_path", &self.path);

tera.render(&tpl_name, &context)
.chain_err(|| format!("Failed to render page '{}'", self.file_path.display()))
.chain_err(|| format!("Failed to render page '{}'", self.file.path.display()))
}
}

impl Default for Page {
fn default() -> Page {
Page {
file: FileInfo::default(),
meta: PageFrontMatter::default(),
file_path: PathBuf::new(),
relative_path: String::new(),
parent_path: PathBuf::new(),
file_name: "".to_string(),
components: vec![],
raw_content: "".to_string(),
assets: vec![],
content: "".to_string(),
@@ -205,7 +164,7 @@ impl Default for Page {

impl ser::Serialize for Page {
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer {
let mut state = serializer.serialize_struct("page", 16)?;
let mut state = serializer.serialize_struct("page", 15)?;
state.serialize_field("content", &self.content)?;
state.serialize_field("title", &self.meta.title)?;
state.serialize_field("description", &self.meta.description)?;
@@ -215,7 +174,6 @@ impl ser::Serialize for Page {
state.serialize_field("permalink", &self.permalink)?;
state.serialize_field("summary", &self.summary)?;
state.serialize_field("tags", &self.meta.tags)?;
state.serialize_field("draft", &self.meta.draft)?;
state.serialize_field("category", &self.meta.category)?;
state.serialize_field("extra", &self.meta.extra)?;
let (word_count, reading_time) = get_reading_analytics(&self.raw_content);
@@ -226,3 +184,142 @@ impl ser::Serialize for Page {
state.end()
}
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::fs::{File, create_dir};
use std::path::Path;

use tera::Tera;
use tempdir::TempDir;

use config::Config;
use super::Page;
use front_matter::InsertAnchor;


#[test]
fn test_can_parse_a_valid_page() {
let content = r#"
+++
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("post.md"), content, &Config::default());
assert!(res.is_ok());
let mut page = res.unwrap();
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default(), InsertAnchor::None).unwrap();

assert_eq!(page.meta.title.unwrap(), "Hello".to_string());
assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string());
assert_eq!(page.raw_content, "Hello world".to_string());
assert_eq!(page.content, "<p>Hello world</p>\n".to_string());
}

#[test]
fn test_can_make_url_from_sections_and_slug() {
let content = r#"
+++
slug = "hello-world"
+++
Hello world"#;
let mut conf = Config::default();
conf.base_url = "http://hello.com/".to_string();
let res = Page::parse(Path::new("content/posts/intro/start.md"), content, &conf);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.path, "posts/intro/hello-world");
assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world");
}

#[test]
fn can_make_url_from_slug_only() {
let content = r#"
+++
slug = "hello-world"
+++
Hello world"#;
let config = Config::default();
let res = Page::parse(Path::new("start.md"), content, &config);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.path, "hello-world");
assert_eq!(page.permalink, config.make_permalink("hello-world"));
}

#[test]
fn errors_on_invalid_front_matter_format() {
// missing starting +++
let content = r#"
title = "Hello"
description = "hey there"
slug = "hello-world"
+++
Hello world"#;
let res = Page::parse(Path::new("start.md"), content, &Config::default());
assert!(res.is_err());
}

#[test]
fn can_make_slug_from_non_slug_filename() {
let config = Config::default();
let res = Page::parse(Path::new(" file with space.md"), "+++\n+++", &config);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.slug, "file-with-space");
assert_eq!(page.permalink, config.make_permalink(&page.slug));
}

#[test]
fn can_specify_summary() {
let config = Config::default();
let content = r#"
+++
+++
Hello world
<!-- more -->"#.to_string();
let res = Page::parse(Path::new("hello.md"), &content, &config);
assert!(res.is_ok());
let mut page = res.unwrap();
page.render_markdown(&HashMap::default(), &Tera::default(), &config, InsertAnchor::None).unwrap();
assert_eq!(page.summary, Some("<p>Hello world</p>\n".to_string()));
}

#[test]
fn page_with_assets_gets_right_parent_path() {
let tmp_dir = TempDir::new("example").expect("create temp dir");
let path = tmp_dir.path();
create_dir(&path.join("content")).expect("create content temp dir");
create_dir(&path.join("content").join("posts")).expect("create posts temp dir");
let nested_path = path.join("content").join("posts").join("assets");
create_dir(&nested_path).expect("create nested temp dir");
File::create(nested_path.join("index.md")).unwrap();
File::create(nested_path.join("example.js")).unwrap();
File::create(nested_path.join("graph.jpg")).unwrap();
File::create(nested_path.join("fail.png")).unwrap();

let res = Page::parse(
nested_path.join("index.md").as_path(),
"+++\nurl=\"hey\"+++\n",
&Config::default()
);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.file.parent, path.join("content").join("posts"));
}

#[test]
fn errors_file_not_named_index_with_assets() {
let tmp_dir = TempDir::new("example").expect("create temp dir");
File::create(tmp_dir.path().join("something.md")).unwrap();
File::create(tmp_dir.path().join("example.js")).unwrap();
File::create(tmp_dir.path().join("graph.jpg")).unwrap();
File::create(tmp_dir.path().join("fail.png")).unwrap();

let page = Page::from_file(tmp_dir.path().join("something.md"), &Config::default());
assert!(page.is_err());
}
}

+ 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)
.chain_err(|| format!("Failed to render pager {} of section '{}'", pager.index, self.section.file_path.display()))
.chain_err(|| format!("Failed to render pager {} of section '{}'", pager.index, self.section.file.path.display()))
}
}

@@ -166,7 +166,7 @@ mod tests {
if !is_index {
s.path = "posts".to_string();
s.permalink = "https://vincent.is/posts".to_string();
s.components = vec!["posts".to_string()];
s.file.components = vec!["posts".to_string()];
} else {
s.permalink = "https://vincent.is".to_string();
}


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

@@ -2,29 +2,25 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;

use tera::{Tera, Context};
use tera::{Tera, Context as TeraContext};
use serde::ser::{SerializeStruct, self};

use config::Config;
use front_matter::{SectionFrontMatter, split_section_content};
use errors::{Result, ResultExt};
use utils::{read_file, find_content_components};
use markdown::markdown_to_html;
use fs::{read_file};
use rendering::markdown::markdown_to_html;
use rendering::context::Context;
use content::Page;
use content::file_info::FileInfo;


#[derive(Clone, Debug, PartialEq)]
pub struct Section {
/// All info about the actual file
pub file: FileInfo,
/// The front matter meta-data
pub meta: SectionFrontMatter,
/// The _index.md full path
pub file_path: PathBuf,
/// The .md path, starting from the content directory, with / slashes
pub relative_path: String,
/// Path of the directory containing the _index.md file
pub parent_path: PathBuf,
/// The folder names from `content` to this section file
pub components: Vec<String>,
/// The URL path of the page
pub path: String,
/// The full URL for that page
@@ -46,11 +42,8 @@ impl Section {
let file_path = file_path.as_ref();

Section {
file: FileInfo::new_section(file_path),
meta: meta,
file_path: file_path.to_path_buf(),
relative_path: "".to_string(),
parent_path: file_path.parent().unwrap().to_path_buf(),
components: vec![],
path: "".to_string(),
permalink: "".to_string(),
raw_content: "".to_string(),
@@ -65,16 +58,8 @@ impl Section {
let (meta, content) = split_section_content(file_path, content)?;
let mut section = Section::new(file_path, meta);
section.raw_content = content.clone();
section.components = find_content_components(&section.file_path);
section.path = section.components.join("/");
section.path = section.file.components.join("/");
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)
}

@@ -101,7 +86,8 @@ impl Section {
/// We need access to all pages url to render links relative to content
/// so that can't happen at the same time as parsing
pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<()> {
self.content = markdown_to_html(&self.raw_content, permalinks, tera, config)?;
let context = Context::new(tera, config, permalinks, self.meta.insert_anchor.unwrap());
self.content = markdown_to_html(&self.raw_content, &context)?;
Ok(())
}

@@ -109,7 +95,7 @@ impl Section {
pub fn render_html(&self, sections: HashMap<String, Section>, tera: &Tera, config: &Config) -> Result<String> {
let tpl_name = self.get_template_name();

let mut context = Context::new();
let mut context = TeraContext::new();
context.add("config", config);
context.add("section", self);
context.add("current_url", &self.permalink);
@@ -119,46 +105,36 @@ impl Section {
}

tera.render(&tpl_name, &context)
.chain_err(|| format!("Failed to render section '{}'", self.file_path.display()))
.chain_err(|| format!("Failed to render section '{}'", self.file.path.display()))
}

/// Is this the index section?
pub fn is_index(&self) -> bool {
self.components.is_empty()
self.file.components.is_empty()
}

/// Returns all the paths for the pages belonging to that section
/// Returns all the paths of the pages belonging to that section
pub fn all_pages_path(&self) -> Vec<PathBuf> {
let mut paths = vec![];
paths.extend(self.pages.iter().map(|p| p.file_path.clone()));
paths.extend(self.ignored_pages.iter().map(|p| p.file_path.clone()));
paths.extend(self.pages.iter().map(|p| p.file.path.clone()));
paths.extend(self.ignored_pages.iter().map(|p| p.file.path.clone()));
paths
}

/// Whether the page given belongs to that section
pub fn is_child_page(&self, page: &Page) -> bool {
for p in &self.pages {
if p.file_path == page.file_path {
return true;
}
}

for p in &self.ignored_pages {
if p.file_path == page.file_path {
return true;
}
}

false
pub fn is_child_page(&self, path: &PathBuf) -> bool {
self.all_pages_path().contains(path)
}
}

impl ser::Serialize for Section {
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer {
let mut state = serializer.serialize_struct("section", 7)?;
let mut state = serializer.serialize_struct("section", 9)?;
state.serialize_field("content", &self.content)?;
state.serialize_field("permalink", &self.permalink)?;
state.serialize_field("title", &self.meta.title)?;
state.serialize_field("description", &self.meta.description)?;
state.serialize_field("extra", &self.meta.extra)?;
state.serialize_field("path", &format!("/{}", self.path))?;
state.serialize_field("permalink", &self.permalink)?;
state.serialize_field("pages", &self.pages)?;
@@ -167,15 +143,12 @@ impl ser::Serialize for Section {
}
}

/// Used to create a default index section if there is no _index.md in the root content directory
impl Default for Section {
/// Used to create a default index section if there is no _index.md in the root content directory
fn default() -> Section {
Section {
file: FileInfo::default(),
meta: SectionFrontMatter::default(),
file_path: PathBuf::new(),
relative_path: "".to_string(),
parent_path: PathBuf::new(),
components: vec![],
path: "".to_string(),
permalink: "".to_string(),
raw_content: "".to_string(),


+ 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 {
let next = &pages[i - 1];
new_page.next = Some(Box::new(next.clone()));
let mut next_page = next.clone();
// Remove prev/next otherwise we serialise the whole thing...
next_page.previous = None;
next_page.next = None;
new_page.next = Some(Box::new(next_page));
}

if i < input.len() - 1 {
let previous = &pages[i + 1];
new_page.previous = Some(Box::new(previous.clone()));
// Remove prev/next otherwise we serialise the whole thing...
let mut previous_page = previous.clone();
previous_page.previous = None;
previous_page.next = None;
new_page.previous = Some(Box::new(previous_page));
}
res.push(new_page);
}
@@ -81,13 +89,13 @@ mod tests {
fn create_page_with_date(date: &str) -> Page {
let mut front_matter = PageFrontMatter::default();
front_matter.date = Some(date.to_string());
Page::new(front_matter)
Page::new("content/hello.md", front_matter)
}

fn create_page_with_order(order: usize) -> Page {
let mut front_matter = PageFrontMatter::default();
front_matter.order = Some(order);
Page::new(front_matter)
Page::new("content/hello.md", front_matter)
}

#[test]


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


#[cfg(test)]
mod tests {
use std::fs::File;


+ 1
- 1
src/front_matter/mod.rs View File

@@ -8,7 +8,7 @@ mod page;
mod section;

pub use self::page::PageFrontMatter;
pub use self::section::{SectionFrontMatter};
pub use self::section::{SectionFrontMatter, InsertAnchor};

lazy_static! {
static ref PAGE_RE: Regex = Regex::new(r"^[[:space:]]*\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap();


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

@@ -24,8 +24,6 @@ pub struct PageFrontMatter {
pub url: Option<String>,
/// Tags, not to be confused with categories
pub tags: Option<Vec<String>>,
/// Whether this page is a draft and should be published or not
pub draft: Option<bool>,
/// Only one category allowed. Can't be an empty string if present
pub category: Option<String>,
/// Integer to use to order content. Lowest is at the bottom, highest first
@@ -100,7 +98,6 @@ impl Default for PageFrontMatter {
slug: None,
url: None,
tags: None,
draft: None,
category: None,
order: None,
template: None,


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

@@ -9,6 +9,14 @@ use content::SortBy;
static DEFAULT_PAGINATE_PATH: &'static str = "page";


#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InsertAnchor {
Left,
Right,
None,
}

/// The front matter of every section
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SectionFrontMatter {
@@ -28,6 +36,10 @@ pub struct SectionFrontMatter {
/// Path to be used by pagination: the page number will be appended after it. Defaults to `page`.
#[serde(skip_serializing)]
pub paginate_path: Option<String>,
/// Whether to insert a link for each header like in Github READMEs. Defaults to false
/// The default template can be overridden by creating a `anchor-link.html` template and CSS will need to be
/// written if you turn that on.
pub insert_anchor: Option<InsertAnchor>,
/// Whether to render that section or not. Defaults to `true`.
/// Useful when the section is only there to organize things but is not meant
/// to be used directly, like a posts section in a personal site
@@ -56,6 +68,10 @@ impl SectionFrontMatter {
f.sort_by = Some(SortBy::None);
}

if f.insert_anchor.is_none() {
f.insert_anchor = Some(InsertAnchor::None);
}

Ok(f)
}

@@ -87,6 +103,7 @@ impl Default for SectionFrontMatter {
paginate_by: None,
paginate_path: Some(DEFAULT_PAGINATE_PATH.to_string()),
render: Some(true),
insert_anchor: Some(InsertAnchor::None),
extra: None,
}
}


+ 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)]
extern crate tempdir;

mod utils;
mod fs;
mod config;
pub mod errors;
mod front_matter;
mod content;
mod site;
mod markdown;
mod rendering;
// Filters, Global Fns and default instance of Tera
mod templates;

pub use site::{Site};
pub use config::{Config, get_config};
pub use front_matter::{PageFrontMatter, SectionFrontMatter, split_page_content, split_section_content};
pub use front_matter::{PageFrontMatter, SectionFrontMatter, InsertAnchor, split_page_content, split_section_content};
pub use content::{Page, Section, SortBy, sort_pages, populate_previous_and_next_pages};
pub use utils::{create_file};
pub use markdown::markdown_to_html;
pub use fs::{create_file};

+ 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::collections::HashMap;

use pulldown_cmark as cmark;
use self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
@@ -8,18 +7,19 @@ use slug::slugify;
use syntect::dumps::from_binary;
use syntect::easy::HighlightLines;
use syntect::parsing::SyntaxSet;
use syntect::highlighting::ThemeSet;
use syntect::html::{start_coloured_html_snippet, styles_to_coloured_html, IncludeBackground};
use tera::{Tera, Context};

use config::Config;
use errors::{Result, ResultExt};
use tera::{Context as TeraContext};

use errors::{Result};
use site::resolve_internal_link;
use front_matter::InsertAnchor;
use rendering::context::Context;
use rendering::highlighting::THEME_SET;
use rendering::short_code::{ShortCode, parse_shortcode, render_simple_shortcode};

// We need to put those in a struct to impl Send and sync
pub struct Setup {
pub syntax_set: SyntaxSet,
pub theme_set: ThemeSet,
}

unsafe impl Send for Setup {}
@@ -29,87 +29,25 @@ lazy_static!{
static ref SHORTCODE_RE: Regex = Regex::new(r#"\{(?:%|\{)\s+([[:alnum:]]+?)\(([[:alnum:]]+?="?.+?"?)\)\s+(?:%|\})\}"#).unwrap();
pub static ref SETUP: Setup = Setup {
syntax_set: {
let mut ps: SyntaxSet = from_binary(include_bytes!("../sublime_syntaxes/newlines.packdump"));
let mut ps: SyntaxSet = from_binary(include_bytes!("../../sublime_syntaxes/newlines.packdump"));
ps.link_syntaxes();
ps
},
theme_set: from_binary(include_bytes!("../sublime_themes/all.themedump"))
};
}

/// A shortcode that has a body
/// Called by having some content like {% ... %} body {% end %}
/// We need the struct to hold the data while we're processing the markdown
#[derive(Debug)]
struct ShortCode {
name: String,
args: HashMap<String, String>,
body: String,
}

impl ShortCode {
pub fn new(name: &str, args: HashMap<String, String>) -> ShortCode {
ShortCode {
name: name.to_string(),
args: args,
body: String::new(),
}
}

pub fn append(&mut self, text: &str) {
self.body.push_str(text)
}

pub fn render(&self, tera: &Tera) -> Result<String> {
let mut context = Context::new();
for (key, value) in &self.args {
context.add(key, value);
}
context.add("body", &self.body);
let tpl_name = format!("shortcodes/{}.html", self.name);
tera.render(&tpl_name, &context)
.chain_err(|| format!("Failed to render {} shortcode", self.name))
}
}

/// Parse a shortcode without a body
fn parse_shortcode(input: &str) -> (String, HashMap<String, String>) {
let mut args = HashMap::new();
let caps = SHORTCODE_RE.captures(input).unwrap();
// caps[0] is the full match
let name = &caps[1];
let arg_list = &caps[2];
for arg in arg_list.split(',') {
let bits = arg.split('=').collect::<Vec<_>>();
args.insert(bits[0].trim().to_string(), bits[1].replace("\"", ""));
}

(name.to_string(), args)
}

/// Renders a shortcode or return an error
fn render_simple_shortcode(tera: &Tera, name: &str, args: &HashMap<String, String>) -> Result<String> {
let mut context = Context::new();
for (key, value) in args.iter() {
context.add(key, value);
}
let tpl_name = format!("shortcodes/{}.html", name);

tera.render(&tpl_name, &context).chain_err(|| format!("Failed to render {} shortcode", name))
}

pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<String> {
pub fn markdown_to_html(content: &str, context: &Context) -> Result<String> {
// We try to be smart about highlighting code as it can be time-consuming
// If the global config disables it, then we do nothing. However,
// if we see a code block in the content, we assume that this page needs
// to be highlighted. It could potentially have false positive if the content
// has ``` in it but that seems kind of unlikely
let should_highlight = if config.highlight_code.unwrap() {
let should_highlight = if context.highlight_code {
content.contains("```")
} else {
false
};
let highlight_theme = config.highlight_theme.clone().unwrap();
// Set while parsing
let mut error = None;
let mut highlighter: Option<HighlightLines> = None;
@@ -167,7 +105,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
if shortcode_block.is_none() && text.starts_with("{{") && text.ends_with("}}") && SHORTCODE_RE.is_match(&text) {
let (name, args) = parse_shortcode(&text);
added_shortcode = true;
match render_simple_shortcode(tera, &name, &args) {
match render_simple_shortcode(context.tera, &name, &args) {
Ok(s) => return Event::Html(Owned(format!("</p>{}", s))),
Err(e) => {
error = Some(e);
@@ -193,7 +131,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
if let Some(ref mut shortcode) = shortcode_block {
if text.trim() == "{% end %}" {
added_shortcode = true;
match shortcode.render(tera) {
match shortcode.render(context.tera) {
Ok(s) => return Event::Html(Owned(format!("</p>{}", s))),
Err(e) => {
error = Some(e);
@@ -213,15 +151,20 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
}
let id = find_anchor(&anchors, slugify(&text), 0);
anchors.push(id.clone());
let anchor_link = if config.insert_anchor_links.unwrap() {
let mut context = Context::new();
context.add("id", &id);
tera.render("anchor-link.html", &context).unwrap()
let anchor_link = if context.should_insert_anchor() {
let mut c = TeraContext::new();
c.add("id", &id);
context.tera.render("anchor-link.html", &c).unwrap()
} else {
String::new()
};
header_already_inserted = true;
return Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, anchor_link, text)));
let event = match context.insert_anchor {
InsertAnchor::Left => Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, anchor_link, text))),
InsertAnchor::Right => Event::Html(Owned(format!(r#"id="{}">{}{}"#, id, text, anchor_link))),
InsertAnchor::None => Event::Html(Owned(format!(r#"id="{}">{}"#, id, text)))
};
return event;
}

// Business as usual
@@ -232,7 +175,7 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
if !should_highlight {
return Event::Html(Owned("<pre><code>".to_owned()));
}
let theme = &SETUP.theme_set.themes[&highlight_theme];
let theme = &THEME_SET.themes[&context.highlight_theme];
let syntax = info
.split(' ')
.next()
@@ -257,21 +200,11 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
return Event::Html(Owned("".to_owned()));
}
if link.starts_with("./") {
// First we remove the ./ since that's gutenberg specific
let clean_link = link.replacen("./", "", 1);
// Then we remove any potential anchor
// parts[0] will be the file path and parts[1] the anchor if present
let parts = clean_link.split('#').collect::<Vec<_>>();
match permalinks.get(parts[0]) {
Some(p) => {
let url = if parts.len() > 1 {
format!("{}#{}", p, parts[1])
} else {
p.to_string()
};
match resolve_internal_link(link, context.permalinks) {
Ok(url) => {
return Event::Start(Tag::Link(Owned(url), title.clone()));
},
None => {
Err(_) => {
error = Some(format!("Relative link {} not found.", link).into());
return Event::Html(Owned("".to_string()));
}
@@ -340,46 +273,33 @@ pub fn markdown_to_html(content: &str, permalinks: &HashMap<String, String>, ter
mod tests {
use std::collections::HashMap;

use templates::GUTENBERG_TERA;
use tera::Tera;

use config::Config;
use super::{markdown_to_html, parse_shortcode};

#[test]
fn test_parse_simple_shortcode_one_arg() {
let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc") }}"#);
assert_eq!(name, "youtube");
assert_eq!(args["id"], "w7Ft2ymGmfc");
}

#[test]
fn test_parse_simple_shortcode_several_arg() {
let (name, args) = parse_shortcode(r#"{{ youtube(id="w7Ft2ymGmfc", autoplay=true) }}"#);
assert_eq!(name, "youtube");
assert_eq!(args["id"], "w7Ft2ymGmfc");
assert_eq!(args["autoplay"], "true");
}
use front_matter::InsertAnchor;
use templates::GUTENBERG_TERA;
use rendering::context::Context;

#[test]
fn test_parse_block_shortcode_several_arg() {
let (name, args) = parse_shortcode(r#"{% youtube(id="w7Ft2ymGmfc", autoplay=true) %}"#);
assert_eq!(name, "youtube");
assert_eq!(args["id"], "w7Ft2ymGmfc");
assert_eq!(args["autoplay"], "true");
}
use super::markdown_to_html;

#[test]
fn test_markdown_to_html_simple() {
let res = markdown_to_html("hello", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
fn can_do_markdown_to_html_simple() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html("hello", &context).unwrap();
assert_eq!(res, "<p>hello</p>\n");
}

#[test]
fn test_markdown_to_html_code_block_highlighting_off() {
let mut config = Config::default();
config.highlight_code = Some(false);
let res = markdown_to_html("```\n$ gutenberg server\n```", &HashMap::new(), &Tera::default(), &config).unwrap();
fn doesnt_highlight_code_block_with_highlighting_off() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let mut context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None);
context.highlight_code = false;
let res = markdown_to_html("```\n$ gutenberg server\n```", &context).unwrap();
assert_eq!(
res,
"<pre><code>$ gutenberg server\n</code></pre>\n"
@@ -387,8 +307,12 @@ mod tests {
}

#[test]
fn test_markdown_to_html_code_block_no_lang() {
let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
fn can_highlight_code_block_no_lang() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html("```\n$ gutenberg server\n$ ping\n```", &context).unwrap();
assert_eq!(
res,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">$ gutenberg server\n</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">$ ping\n</span></pre>"
@@ -396,8 +320,12 @@ mod tests {
}

#[test]
fn test_markdown_to_html_code_block_with_lang() {
let res = markdown_to_html("```python\nlist.append(1)\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
fn can_highlight_code_block_with_lang() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html("```python\nlist.append(1)\n```", &context).unwrap();
assert_eq!(
res,
"<pre style=\"background-color:#2b303b\">\n<span style=\"background-color:#2b303b;color:#c0c5ce;\">list</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">.</span><span style=\"background-color:#2b303b;color:#bf616a;\">append</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">(</span><span style=\"background-color:#2b303b;color:#d08770;\">1</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">)</span><span style=\"background-color:#2b303b;color:#c0c5ce;\">\n</span></pre>"
@@ -405,8 +333,12 @@ mod tests {
}

#[test]
fn test_markdown_to_html_code_block_with_unknown_lang() {
let res = markdown_to_html("```yolo\nlist.append(1)\n```", &HashMap::new(), &Tera::default(), &Config::default()).unwrap();
fn can_higlight_code_block_with_unknown_lang() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html("```yolo\nlist.append(1)\n```", &context).unwrap();
// defaults to plain text
assert_eq!(
res,
@@ -415,18 +347,24 @@ mod tests {
}

#[test]
fn test_markdown_to_html_with_shortcode() {
fn can_render_shortcode() {
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html(r#"
Hello

{{ youtube(id="ub36ffWAqgQ") }}
"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
"#, &context).unwrap();
assert!(res.contains("<p>Hello</p>\n<div >"));
assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
}

#[test]
fn test_markdown_to_html_with_several_shortcode_in_row() {
fn can_render_several_shortcode_in_row() {
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html(r#"
Hello

@@ -438,7 +376,7 @@ Hello

{{ gist(url="https://gist.github.com/Keats/32d26f699dcc13ebd41b") }}

"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
"#, &context).unwrap();
assert!(res.contains("<p>Hello</p>\n<div >"));
assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
assert!(res.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#));
@@ -446,40 +384,52 @@ Hello
}

#[test]
fn test_markdown_to_html_shortcode_in_code_block() {
let res = markdown_to_html(r#"```{{ youtube(id="w7Ft2ymGmfc") }}```"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
fn doesnt_render_shortcode_in_code_block() {
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html(r#"```{{ youtube(id="w7Ft2ymGmfc") }}```"#, &context).unwrap();
assert_eq!(res, "<p><code>{{ youtube(id=&quot;w7Ft2ymGmfc&quot;) }}</code></p>\n");
}

#[test]
fn test_markdown_to_html_shortcode_with_body() {
fn can_render_shortcode_with_body() {
let mut tera = Tera::default();
tera.extend(&GUTENBERG_TERA).unwrap();
tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author}}</blockquote>").unwrap();
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&tera, &config_ctx, &permalinks_ctx, InsertAnchor::None);

let res = markdown_to_html(r#"
Hello
{% quote(author="Keats") %}
A quote
{% end %}
"#, &HashMap::new(), &tera, &Config::default()).unwrap();
"#, &context).unwrap();
assert_eq!(res, "<p>Hello\n</p><blockquote>A quote - Keats</blockquote>");
}

#[test]
fn test_markdown_to_html_unknown_shortcode() {
let res = markdown_to_html("{{ hello(flash=true) }}", &HashMap::new(), &Tera::default(), &Config::default());
fn errors_rendering_unknown_shortcode() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html("{{ hello(flash=true) }}", &context);
assert!(res.is_err());
}

#[test]
fn test_markdown_to_html_relative_link_exists() {
fn can_make_valid_relative_link() {
let mut permalinks = HashMap::new();
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
let tera_ctx = Tera::default();
let config_ctx = Config::default();
let context = Context::new(&tera_ctx, &config_ctx, &permalinks, InsertAnchor::None);
let res = markdown_to_html(
r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#,
&permalinks,
&GUTENBERG_TERA,
&Config::default()
&context
).unwrap();

assert!(
@@ -488,15 +438,13 @@ A quote
}

#[test]
fn test_markdown_to_html_relative_links_with_anchors() {
fn can_make_relative_links_with_anchors() {
let mut permalinks = HashMap::new();
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about".to_string());
let res = markdown_to_html(
r#"[rel link](./pages/about.md#cv)"#,
&permalinks,
&GUTENBERG_TERA,
&Config::default()
).unwrap();
let tera_ctx = Tera::default();
let config_ctx = Config::default();
let context = Context::new(&tera_ctx, &config_ctx, &permalinks, InsertAnchor::None);
let res = markdown_to_html(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap();

assert!(
res.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#)
@@ -504,67 +452,94 @@ A quote
}

#[test]
fn test_markdown_to_html_relative_link_inexistant() {
let res = markdown_to_html("[rel link](./pages/about.md)", &HashMap::new(), &Tera::default(), &Config::default());
fn errors_relative_link_inexistant() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html("[rel link](./pages/about.md)", &context);
assert!(res.is_err());
}

#[test]
fn test_markdown_to_html_add_id_to_headers() {
let res = markdown_to_html(r#"# Hello"#, &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
fn can_add_id_to_headers() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html(r#"# Hello"#, &context).unwrap();
assert_eq!(res, "<h1 id=\"hello\">Hello</h1>\n");
}

#[test]
fn test_markdown_to_html_add_id_to_headers_same_slug() {
let res = markdown_to_html("# Hello\n# Hello", &HashMap::new(), &GUTENBERG_TERA, &Config::default()).unwrap();
fn can_add_id_to_headers_same_slug() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&tera_ctx, &config_ctx, &permalinks_ctx, InsertAnchor::None);
let res = markdown_to_html("# Hello\n# Hello", &context).unwrap();
assert_eq!(res, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n");
}

#[test]
fn test_markdown_to_html_insert_anchor() {
let mut config = Config::default();
config.insert_anchor_links = Some(true);
let res = markdown_to_html("# Hello", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap();
fn can_insert_anchor_left() {
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::Left);
let res = markdown_to_html("# Hello", &context).unwrap();
assert_eq!(
res,
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello</h1>\n"
);
}

#[test]
fn can_insert_anchor_right() {
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::Right);
let res = markdown_to_html("# Hello", &context).unwrap();
assert_eq!(
res,
"<h1 id=\"hello\"><a class=\"anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello</h1>\n"
"<h1 id=\"hello\">Hello<a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\n</h1>\n"
);
}

// See https://github.com/Keats/gutenberg/issues/42
#[test]
fn test_markdown_to_html_insert_anchor_with_exclamation_mark() {
let mut config = Config::default();
config.insert_anchor_links = Some(true);
let res = markdown_to_html("# Hello!", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap();
fn can_insert_anchor_with_exclamation_mark() {
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::Left);
let res = markdown_to_html("# Hello!", &context).unwrap();
assert_eq!(
res,
"<h1 id=\"hello\"><a class=\"anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello!</h1>\n"
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello!</h1>\n"
);
}

// See https://github.com/Keats/gutenberg/issues/53
#[test]
fn test_markdown_to_html_insert_anchor_with_link() {
let mut config = Config::default();
config.insert_anchor_links = Some(true);
let res = markdown_to_html("## [](#xresources)Xresources", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap();
fn can_insert_anchor_with_link() {
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::Left);
let res = markdown_to_html("## [](#xresources)Xresources", &context).unwrap();
assert_eq!(
res,
"<h2 id=\"xresources\"><a class=\"anchor\" href=\"#xresources\" aria-label=\"Anchor link for: xresources\">đź”—</a>\nXresources</h2>\n"
"<h2 id=\"xresources\"><a class=\"gutenberg-anchor\" href=\"#xresources\" aria-label=\"Anchor link for: xresources\">đź”—</a>\nXresources</h2>\n"
);
}


#[test]
fn test_markdown_to_html_insert_anchor_with_other_special_chars() {
let mut config = Config::default();
config.insert_anchor_links = Some(true);
let res = markdown_to_html("# Hello*_()", &HashMap::new(), &GUTENBERG_TERA, &config).unwrap();
fn can_insert_anchor_with_other_special_chars() {
let permalinks_ctx = HashMap::new();
let config_ctx = Config::default();
let context = Context::new(&GUTENBERG_TERA, &config_ctx, &permalinks_ctx, InsertAnchor::Left);
let res = markdown_to_html("# Hello*_()", &context).unwrap();
assert_eq!(
res,
"<h1 id=\"hello\"><a class=\"anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello*_()</h1>\n"
"<h1 id=\"hello\"><a class=\"gutenberg-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">đź”—</a>\nHello*_()</h1>\n"
);
}
}

+ 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::path::{Path, PathBuf};

use glob::glob;
use tera::{Tera, Context};
use slug::slugify;
use walkdir::WalkDir;

use errors::{Result, ResultExt};
use config::{Config, get_config};
use utils::{create_file, create_directory};
use content::{Page, Section, Paginator, SortBy, populate_previous_and_next_pages, sort_pages};
use fs::{create_file, create_directory, ensure_directory_exists};
use content::{Page, Section, Paginator, SortBy, Taxonomy, populate_previous_and_next_pages, sort_pages};
use templates::{GUTENBERG_TERA, global_fns, render_redirect_template};
use front_matter::InsertAnchor;


#[derive(Debug, PartialEq)]
enum RenderList {
Tags,
Categories,
}

/// A tag or category
#[derive(Debug, Serialize, PartialEq)]
struct ListItem {
name: String,
slug: String,
count: usize,
}

impl ListItem {
pub fn new(name: &str, count: usize) -> ListItem {
ListItem {
name: name.to_string(),
slug: slugify(name),
count: count,
}
}
}

#[derive(Debug)]
pub struct Site {
/// The base path of the gutenberg site
pub base_path: PathBuf,
/// The parsed config for the site
pub config: Config,
pub pages: HashMap<PathBuf, Page>,
pub sections: HashMap<PathBuf, Section>,
@@ -49,8 +26,8 @@ pub struct Site {
live_reload: bool,
output_path: PathBuf,
static_path: PathBuf,
pub tags: HashMap<String, Vec<PathBuf>>,
pub categories: HashMap<String, Vec<PathBuf>>,
pub tags: Option<Taxonomy>,
pub categories: Option<Taxonomy>,
/// A map of all .md files (section and pages) and their permalink
/// We need that if there are relative links in the content that need to be resolved
pub permalinks: HashMap<String, String>,
@@ -75,8 +52,8 @@ impl Site {
live_reload: false,
output_path: path.join("public"),
static_path: path.join("static"),
tags: HashMap::new(),
categories: HashMap::new(),
tags: None,
categories: None,
permalinks: HashMap::new(),
};

@@ -88,15 +65,6 @@ impl Site {
self.live_reload = true;
}

/// Gets the path of all ignored pages in the site
/// Used for reporting them in the CLI
pub fn get_ignored_pages(&self) -> Vec<PathBuf> {
self.sections
.values()
.flat_map(|s| s.ignored_pages.iter().map(|p| p.file_path.clone()))
.collect()
}

/// Get all the orphan (== without section) pages in the site
pub fn get_all_orphan_pages(&self) -> Vec<&Page> {
let mut pages_in_sections = vec![];
@@ -107,7 +75,7 @@ impl Site {
}

for page in self.pages.values() {
if !pages_in_sections.contains(&page.file_path) {
if !pages_in_sections.contains(&page.file.path) {
orphans.push(page);
}
}
@@ -115,17 +83,6 @@ impl Site {
orphans
}

/// Finds the section that contains the page given if there is one
pub fn find_parent_section(&self, page: &Page) -> Option<&Section> {
for section in self.sections.values() {
if section.is_child_page(page) {
return Some(section)
}
}

None
}

/// Used by tests to change the output path to a tmp dir
#[doc(hidden)]
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) {
@@ -146,8 +103,8 @@ impl Site {
self.add_page(path, false)?;
}
}
// Insert a default index section so we don't need to create a _index.md to render
// the index page
// Insert a default index section if necessary so we don't need to create
// a _index.md to render the index page
let index_path = self.base_path.join("content").join("_index.md");
if !self.sections.contains_key(&index_path) {
let mut index_section = Section::default();
@@ -155,9 +112,16 @@ impl Site {
self.sections.insert(index_path, index_section);
}

// Silly thing needed to make the borrow checker happy
let mut pages_insert_anchors = HashMap::new();
for page in self.pages.values() {
pages_insert_anchors.insert(page.file.path.clone(), self.find_parent_section_insert_anchor(&page.file.parent.clone()));
}

// TODO: make that parallel
for page in self.pages.values_mut() {
page.render_markdown(&self.permalinks, &self.tera, &self.config)?;
let insert_anchor = pages_insert_anchors[&page.file.path];
page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?;
}
// TODO: make that parallel
for section in self.sections.values_mut() {
@@ -168,22 +132,30 @@ impl Site {
self.populate_tags_and_categories();

self.tera.register_global_function("get_page", global_fns::make_get_page(&self.pages));
self.tera.register_global_function("get_section", global_fns::make_get_section(&self.sections));
self.register_get_url_fn();

Ok(())
}

/// Separate fn as it can be called in the serve command
pub fn register_get_url_fn(&mut self) {
self.tera.register_global_function("get_url", global_fns::make_get_url(self.permalinks.clone()));
}

/// Add a page to the site
/// The `render` parameter is used in the serve command, when rebuilding a page.
/// If `true`, it will also render the markdown for that page
/// Returns the previous page struct if there was one
pub fn add_page(&mut self, path: &Path, render: bool) -> Result<Option<Page>> {
let page = Page::from_file(&path, &self.config)?;
self.permalinks.insert(page.relative_path.clone(), page.permalink.clone());
let prev = self.pages.insert(page.file_path.clone(), page);
self.permalinks.insert(page.file.relative.clone(), page.permalink.clone());
let prev = self.pages.insert(page.file.path.clone(), page);

if render {
let insert_anchor = self.find_parent_section_insert_anchor(&self.pages[path].file.parent);
let mut page = self.pages.get_mut(path).unwrap();
page.render_markdown(&self.permalinks, &self.tera, &self.config)?;
page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?;
}

Ok(prev)
@@ -192,11 +164,11 @@ impl Site {
/// Add a section to the site
/// The `render` parameter is used in the serve command, when rebuilding a page.
/// If `true`, it will also render the markdown for that page
/// Returns the previous page struct if there was one
/// Returns the previous section struct if there was one
pub fn add_section(&mut self, path: &Path, render: bool) -> Result<Option<Section>> {
let section = Section::from_file(path, &self.config)?;
self.permalinks.insert(section.relative_path.clone(), section.permalink.clone());
let prev = self.sections.insert(section.file_path.clone(), section);
self.permalinks.insert(section.file.relative.clone(), section.permalink.clone());
let prev = self.sections.insert(section.file.path.clone(), section);

if render {
let mut section = self.sections.get_mut(path).unwrap();
@@ -206,12 +178,21 @@ impl Site {
Ok(prev)
}

/// Finds the insert_anchor for the parent section of the directory at `path`.
/// Defaults to `AnchorInsert::None` if no parent section found
pub fn find_parent_section_insert_anchor(&self, parent_path: &PathBuf) -> InsertAnchor {
match self.sections.get(&parent_path.join("_index.md")) {
Some(s) => s.meta.insert_anchor.unwrap(),
None => InsertAnchor::None
}
}

/// Find out the direct subsections of each subsection if there are some
/// as well as the pages for each section
pub fn populate_sections(&mut self) {
let mut grandparent_paths = HashMap::new();
for section in self.sections.values_mut() {
if let Some(grand_parent) = section.parent_path.parent() {
if let Some(ref grand_parent) = section.file.grand_parent {
grandparent_paths.entry(grand_parent.to_path_buf()).or_insert_with(|| vec![]).push(section.clone());
}
// Make sure the pages of a section are empty since we can call that many times on `serve`
@@ -220,13 +201,14 @@ impl Site {
}

for page in self.pages.values() {
if self.sections.contains_key(&page.parent_path.join("_index.md")) {
self.sections.get_mut(&page.parent_path.join("_index.md")).unwrap().pages.push(page.clone());
let parent_section_path = page.file.parent.join("_index.md");
if self.sections.contains_key(&parent_section_path) {
self.sections.get_mut(&parent_section_path).unwrap().pages.push(page.clone());
}
}

for section in self.sections.values_mut() {
match grandparent_paths.get(&section.parent_path) {
match grandparent_paths.get(&section.file.parent) {
Some(paths) => section.subsections.extend(paths.clone()),
None => continue,
};
@@ -250,24 +232,23 @@ impl Site {
}
}

/// Separated from `parse` for easier testing
/// Find all the tags and categories if it's asked in the config
pub fn populate_tags_and_categories(&mut self) {
for page in self.pages.values() {
if let Some(ref category) = page.meta.category {
self.categories
.entry(category.to_string())
.or_insert_with(|| vec![])
.push(page.file_path.clone());
}
let generate_tags_pages = self.config.generate_tags_pages.unwrap();
let generate_categories_pages = self.config.generate_categories_pages.unwrap();
if !generate_tags_pages && !generate_categories_pages {
return;
}

if let Some(ref tags) = page.meta.tags {
for tag in tags {
self.tags
.entry(tag.to_string())
.or_insert_with(|| vec![])
.push(page.file_path.clone());
}
}
// TODO: can we pass a reference?
let (tags, categories) = Taxonomy::find_tags_and_categories(
self.pages.values().cloned().collect::<Vec<_>>()
);
if generate_tags_pages {
self.tags = Some(tags);
}
if generate_categories_pages {
self.categories = Some(categories);
}
}

@@ -283,14 +264,6 @@ impl Site {
html
}

fn ensure_public_directory_exists(&self) -> Result<()> {
let public = self.output_path.clone();
if !public.exists() {
create_directory(&public)?;
}
Ok(())
}

/// Copy static file to public directory.
pub fn copy_static_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let relative_path = path.as_ref().strip_prefix(&self.static_path).unwrap();
@@ -332,7 +305,7 @@ impl Site {

/// Renders a single content page
pub fn render_page(&self, page: &Page) -> Result<()> {
self.ensure_public_directory_exists()?;
ensure_directory_exists(&self.output_path)?;

// Copy the nesting of the content directory if we have sections for that page
let mut current_path = self.output_path.to_path_buf();
@@ -350,7 +323,7 @@ impl Site {

// Finally, create a index.html file there with the page rendered
let output = page.render_html(&self.tera, &self.config)?;
create_file(current_path.join("index.html"), &self.inject_livereload(output))?;
create_file(&current_path.join("index.html"), &self.inject_livereload(output))?;

// Copy any asset we found previously into the same directory as the index.html
for asset in &page.assets {
@@ -361,7 +334,7 @@ impl Site {
Ok(())
}

/// Builds the site to the `public` directory after deleting it
/// Deletes the `public` directory and builds the site
pub fn build(&self) -> Result<()> {
self.clean()?;
self.render_sections()?;
@@ -381,98 +354,45 @@ impl Site {

/// Renders robots.txt
pub fn render_robots(&self) -> Result<()> {
self.ensure_public_directory_exists()?;
ensure_directory_exists(&self.output_path)?;
create_file(
self.output_path.join("robots.txt"),
&self.output_path.join("robots.txt"),
&self.tera.render("robots.txt", &Context::new())?
)
}

/// Renders all categories if the config allows it
/// Renders all categories and the single category pages if there are some
pub fn render_categories(&self) -> Result<()> {
if self.config.generate_categories_pages.unwrap() {
self.render_categories_and_tags(RenderList::Categories)
} else {
Ok(())
if let Some(ref categories) = self.categories {
self.render_taxonomy(categories)?;
}

Ok(())
}

/// Renders all tags if the config allows it
/// Renders all tags and the single tag pages if there are some
pub fn render_tags(&self) -> Result<()> {
if self.config.generate_tags_pages.unwrap() {
self.render_categories_and_tags(RenderList::Tags)
} else {
Ok(())
if let Some(ref tags) = self.tags {
self.render_taxonomy(tags)?;
}
}

/// Render the /{categories, list} pages and each individual category/tag page
/// They are the same thing fundamentally, a list of pages with something in common
/// TODO: revisit this function, lots of things have changed since then
fn render_categories_and_tags(&self, kind: RenderList) -> Result<()> {
let items = match kind {
RenderList::Categories => &self.categories,
RenderList::Tags => &self.tags,
};

if items.is_empty() {
return Ok(());
}
Ok(())
}

let (list_tpl_name, single_tpl_name, name, var_name) = if kind == RenderList::Categories {
("categories.html", "category.html", "categories", "category")
} else {
("tags.html", "tag.html", "tags", "tag")
};
self.ensure_public_directory_exists()?;
fn render_taxonomy(&self, taxonomy: &Taxonomy) -> Result<()> {
ensure_directory_exists(&self.output_path)?;

// Create the categories/tags directory first
let public = self.output_path.clone();
let mut output_path = public.to_path_buf();
output_path.push(name);
let output_path = self.output_path.join(&taxonomy.get_list_name());
let list_output = taxonomy.render_list(&self.tera, &self.config)?;
create_directory(&output_path)?;
create_file(&output_path.join("index.html"), &self.inject_livereload(list_output))?;

// Then render the index page for that kind.
// We sort by number of page in that category/tag
let mut sorted_items = vec![];
for (item, count) in Vec::from_iter(items).into_iter().map(|(a, b)| (a, b.len())) {
sorted_items.push(ListItem::new(item, count));
}
sorted_items.sort_by(|a, b| b.count.cmp(&a.count));
let mut context = Context::new();
context.add(name, &sorted_items);
context.add("config", &self.config);
context.add("current_url", &self.config.make_permalink(name));
context.add("current_path", &format!("/{}", name));
// And render it immediately
let list_output = self.tera.render(list_tpl_name, &context)?;
create_file(output_path.join("index.html"), &self.inject_livereload(list_output))?;

// Now, each individual item
for (item_name, pages_paths) in items.iter() {
let pages: Vec<&Page> = self.pages
.iter()
.filter(|&(path, _)| pages_paths.contains(path))
.map(|(_, page)| page)
.collect();
// TODO: how to sort categories and tag content?
// Have a setting in config.toml or a _category.md and _tag.md
// The latter is more in line with the rest of Gutenberg but order ordering
// doesn't really work across sections.

let mut context = Context::new();
let slug = slugify(&item_name);
context.add(var_name, &item_name);
context.add(&format!("{}_slug", var_name), &slug);
context.add("pages", &pages);
context.add("config", &self.config);
context.add("current_url", &self.config.make_permalink(&format!("{}/{}", name, slug)));
context.add("current_path", &format!("/{}/{}", name, slug));
let single_output = self.tera.render(single_tpl_name, &context)?;

create_directory(&output_path.join(&slug))?;
for item in &taxonomy.items {
let single_output = taxonomy.render_single_item(item, &self.tera, &self.config)?;

create_directory(&output_path.join(&item.slug))?;
create_file(
output_path.join(&slug).join("index.html"),
&output_path.join(&item.slug).join("index.html"),
&self.inject_livereload(single_output)
)?;
}
@@ -482,28 +402,31 @@ impl Site {

/// What it says on the tin
pub fn render_sitemap(&self) -> Result<()> {
self.ensure_public_directory_exists()?;
ensure_directory_exists(&self.output_path)?;

let mut context = Context::new();
context.add("pages", &self.pages.values().collect::<Vec<&Page>>());
context.add("sections", &self.sections.values().collect::<Vec<&Section>>());

let mut categories = vec![];
if self.config.generate_categories_pages.unwrap() && !self.categories.is_empty() {
categories.push(self.config.make_permalink("categories"));
for category in self.categories.keys() {
if let Some(ref c) = self.categories {
let name = c.get_list_name();
categories.push(self.config.make_permalink(&name));
for item in &c.items {
categories.push(
self.config.make_permalink(&format!("categories/{}", slugify(category)))
self.config.make_permalink(&format!("{}/{}", &name, item.slug))
);
}
}
context.add("categories", &categories);

let mut tags = vec![];
if self.config.generate_tags_pages.unwrap() && !self.tags.is_empty() {
tags.push(self.config.make_permalink("tags"));
for tag in self.tags.keys() {
if let Some(ref t) = self.tags {
let name = t.get_list_name();
tags.push(self.config.make_permalink(&name));
for item in &t.items {
tags.push(
self.config.make_permalink(&format!("tags/{}", slugify(tag)))
self.config.make_permalink(&format!("{}/{}", &name, item.slug))
);
}
}
@@ -511,18 +434,18 @@ impl Site {

let sitemap = self.tera.render("sitemap.xml", &context)?;

create_file(self.output_path.join("sitemap.xml"), &sitemap)?;
create_file(&self.output_path.join("sitemap.xml"), &sitemap)?;

Ok(())
}

pub fn render_rss_feed(&self) -> Result<()> {
self.ensure_public_directory_exists()?;
ensure_directory_exists(&self.output_path)?;

let mut context = Context::new();
let pages = self.pages.values()
.filter(|p| p.meta.date.is_some())
.take(15) // limit to the last 15 elements
.take(self.config.rss_limit.unwrap()) // limit to the last n elements
.cloned()
.collect::<Vec<Page>>();

@@ -544,7 +467,7 @@ impl Site {

let sitemap = self.tera.render("rss.xml", &context)?;

create_file(self.output_path.join("rss.xml"), &sitemap)?;
create_file(&self.output_path.join("rss.xml"), &sitemap)?;

Ok(())
}
@@ -554,17 +477,17 @@ impl Site {
fn get_sections_map(&self) -> HashMap<String, Section> {
self.sections
.values()
.map(|s| (s.components.join("/"), s.clone()))
.map(|s| (s.file.components.join("/"), s.clone()))
.collect()
}

/// Renders a single section
pub fn render_section(&self, section: &Section, render_pages: bool) -> Result<()> {
self.ensure_public_directory_exists()?;
ensure_directory_exists(&self.output_path)?;
let public = self.output_path.clone();

let mut output_path = public.to_path_buf();
for component in &section.components {
for component in &section.file.components {
output_path.push(component);

if !output_path.exists() {
@@ -590,7 +513,7 @@ impl Site {
&self.tera,
&self.config,
)?;
create_file(output_path.join("index.html"), &self.inject_livereload(output))?;
create_file(&output_path.join("index.html"), &self.inject_livereload(output))?;
}

Ok(())
@@ -610,7 +533,7 @@ impl Site {

/// Renders all pages that do not belong to any sections
pub fn render_orphan_pages(&self) -> Result<()> {
self.ensure_public_directory_exists()?;
ensure_directory_exists(&self.output_path)?;

for page in self.get_all_orphan_pages() {
self.render_page(page)?;
@@ -621,7 +544,7 @@ impl Site {

/// Renders a list of pages when the section/index is wanting pagination.
fn render_paginated(&self, output_path: &Path, section: &Section) -> Result<()> {
self.ensure_public_directory_exists()?;
ensure_directory_exists(&self.output_path)?;

let paginate_path = match section.meta.paginate_path {
Some(ref s) => s.clone(),
@@ -636,13 +559,63 @@ impl Site {
create_directory(&page_path)?;
let output = paginator.render_pager(pager, self)?;
if i > 0 {
create_file(page_path.join("index.html"), &self.inject_livereload(output))?;
create_file(&page_path.join("index.html"), &self.inject_livereload(output))?;
} else {
create_file(output_path.join("index.html"), &self.inject_livereload(output))?;
create_file(page_path.join("index.html"), &render_redirect_template(&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(())
}
}


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

#[test]
fn test_markdown() {
fn markdown_filter() {
let result = markdown(to_value(&"# Hey").unwrap(), HashMap::new());
assert!(result.is_ok());
assert_eq!(result.unwrap(), to_value(&"<h1>Hey</h1>\n").unwrap());
}

#[test]
fn test_base64_encode() {
fn base64_encode_filter() {
// from https://tools.ietf.org/html/rfc4648#section-10
let tests = vec![
("", ""),
@@ -73,7 +73,7 @@ mod tests {


#[test]
fn test_base64_decode() {
fn base64_decode_filter() {
let tests = vec![
("", ""),
("Zg==", "f"),


+ 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 content::Page;
use content::{Page, Section};
use site::resolve_internal_link;


pub fn make_get_page(all_pages: &HashMap<PathBuf, Page>) -> GlobalFn {
let mut pages = HashMap::new();
for page in all_pages.values() {
pages.insert(page.relative_path.clone(), page.clone());
pages.insert(page.file.relative.clone(), page.clone());
}

Box::new(move |args| -> Result<Value> {
@@ -27,3 +28,40 @@ pub fn make_get_page(all_pages: &HashMap<PathBuf, Page>) -> GlobalFn {
}
})
}

pub fn make_get_section(all_sections: &HashMap<PathBuf, Section>) -> GlobalFn {
let mut sections = HashMap::new();
for section in all_sections.values() {
sections.insert(section.file.relative.clone(), section.clone());
}

Box::new(move |args| -> Result<Value> {
match args.get("path") {
Some(val) => match from_value::<String>(val.clone()) {
Ok(v) => {
match sections.get(&v) {
Some(p) => Ok(to_value(p).unwrap()),
None => Err(format!("Section `{}` not found.", v).into())
}
},
Err(_) => Err(format!("`get_section` received path={:?} but it requires a string", val).into()),
},
None => Err("`get_section` requires a `path` argument.".into()),
}
})
}

pub fn make_get_url(permalinks: HashMap<String, String>,) -> GlobalFn {
Box::new(move |args| -> Result<Value> {
match args.get("link") {
Some(val) => match from_value::<String>(val.clone()) {
Ok(v) => match resolve_internal_link(&v, &permalinks) {
Ok(url) => Ok(to_value(url).unwrap()),
Err(_) => Err(format!("Could not resolve URL for link `{}` not found.", v).into())
},
Err(_) => Err(format!("`get_url` received link={:?} but it requires a string", val).into()),
},
None => Err("`get_url` requires a `link` argument.".into()),
}
})
}

+ 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:
- j2
- jinja2
scope: source.jinja2
scope: text.html.jinja2
contexts:
main:
- include: scope:text.html.basic
- match: '({%)\s*(raw)\s*(%})'
captures:
1: entity.other.jinja2.delimiter.tag

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"
paginate_by = 2
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

# Title

Hey

+ 1
- 1
test_site/templates/categories.html View File

@@ -1,3 +1,3 @@
{% for category in categories %}
{{ category.name }} {{ category.slug }} {{ category.count }}
{{ category.name }} {{ category.slug }} {{ category.pages | length }}
{% 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>
<h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3>
</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>
<h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3>
</article>


+ 1
- 1
test_site/templates/tags.html View File

@@ -1,3 +1,3 @@
{% for tag in tags %}
{{ tag.name }} {{ tag.slug }} {{ tag.count }}
{{ tag.name }} {{ tag.slug }} {{ tag.pages | length }}
{% 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]
fn test_can_parse_site() {
fn can_parse_site() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@@ -24,7 +24,7 @@ fn test_can_parse_site() {

// Make sure we remove all the pwd + content from the sections
let basic = &site.pages[&posts_path.join("simple.md")];
assert_eq!(basic.components, vec!["posts".to_string()]);
assert_eq!(basic.file.components, vec!["posts".to_string()]);

// Make sure the page with a url doesn't have any sections
let url_post = &site.pages[&posts_path.join("fixed-url.md")];
@@ -32,7 +32,7 @@ fn test_can_parse_site() {

// Make sure the article in a folder with only asset doesn't get counted as a section
let asset_folder_post = &site.pages[&posts_path.join("with-assets").join("index.md")];
assert_eq!(asset_folder_post.components, vec!["posts".to_string()]);
assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]);

// That we have the right number of sections
assert_eq!(site.sections.len(), 6);
@@ -89,7 +89,7 @@ macro_rules! file_contains {
}

#[test]
fn test_can_build_site_without_live_reload() {
fn can_build_site_without_live_reload() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@@ -131,7 +131,7 @@ fn test_can_build_site_without_live_reload() {
}

#[test]
fn test_can_build_site_with_live_reload() {
fn can_build_site_with_live_reload() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@@ -169,7 +169,7 @@ fn test_can_build_site_with_live_reload() {
}

#[test]
fn test_can_build_site_with_categories() {
fn can_build_site_with_categories() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@@ -190,7 +190,7 @@ fn test_can_build_site_with_categories() {
site.build().unwrap();

assert!(Path::new(&public).exists());
assert_eq!(site.categories.len(), 2);
assert_eq!(site.categories.unwrap().len(), 2);

assert!(file_exists!(public, "index.html"));
assert!(file_exists!(public, "sitemap.xml"));
@@ -221,7 +221,7 @@ fn test_can_build_site_with_categories() {
}

#[test]
fn test_can_build_site_with_tags() {
fn can_build_site_with_tags() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@@ -243,7 +243,7 @@ fn test_can_build_site_with_tags() {
site.build().unwrap();

assert!(Path::new(&public).exists());
assert_eq!(site.tags.len(), 3);
assert_eq!(site.tags.unwrap().len(), 3);

assert!(file_exists!(public, "index.html"));
assert!(file_exists!(public, "sitemap.xml"));
@@ -273,11 +273,10 @@ fn test_can_build_site_with_tags() {
}

#[test]
fn test_can_build_site_and_insert_anchor_links() {
fn can_build_site_and_insert_anchor_links() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
site.config.insert_anchor_links = Some(true);
site.load().unwrap();
let tmp_dir = TempDir::new("example").expect("create temp dir");
let public = &tmp_dir.path().join("public");
@@ -286,11 +285,11 @@ fn test_can_build_site_and_insert_anchor_links() {

assert!(Path::new(&public).exists());
// anchor link inserted
assert!(file_contains!(public, "posts/something-else/index.html", "<h1 id=\"title\"><a class=\"anchor\" href=\"#title\""));
assert!(file_contains!(public, "posts/something-else/index.html", "<h1 id=\"title\"><a class=\"gutenberg-anchor\" href=\"#title\""));
}

#[test]
fn test_can_build_site_with_pagination_for_section() {
fn can_build_site_with_pagination_for_section() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@@ -349,7 +348,7 @@ fn test_can_build_site_with_pagination_for_section() {
}

#[test]
fn test_can_build_site_with_pagination_for_index() {
fn can_build_site_with_pagination_for_index() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@@ -391,5 +390,4 @@ fn test_can_build_site_with_pagination_for_index() {
assert!(file_contains!(public, "index.html", "Last: https://replace-this-with-your-url.com/"));
assert_eq!(file_contains!(public, "index.html", "has_prev"), false);
assert_eq!(file_contains!(public, "index.html", "has_next"), false);

}

Loading…
Cancel
Save