Browse Source

Merge pull request #678 from getzola/next

0.8.0
index-subcmd
Vincent Prouillet GitHub 4 years ago
parent
commit
e6902264ef
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 2399 additions and 1092 deletions
  1. +1
    -1
      .travis.yml
  2. +14
    -0
      CHANGELOG.md
  3. +831
    -692
      Cargo.lock
  4. +7
    -5
      Cargo.toml
  5. +14
    -0
      Dockerfile
  6. +1
    -1
      README.md
  7. +1
    -1
      appveyor.yml
  8. +17
    -1
      completions/_zola
  9. +9
    -1
      completions/_zola.ps1
  10. +43
    -25
      completions/zola.bash
  11. +4
    -1
      completions/zola.fish
  12. +1
    -1
      components/config/Cargo.toml
  13. +2
    -2
      components/errors/Cargo.toml
  14. +2
    -2
      components/front_matter/Cargo.toml
  15. +5
    -0
      components/front_matter/src/section.rs
  16. +1
    -1
      components/imageproc/Cargo.toml
  17. +7
    -3
      components/imageproc/src/lib.rs
  18. +1
    -1
      components/library/Cargo.toml
  19. +91
    -0
      components/library/src/content/mod.rs
  20. +69
    -7
      components/library/src/content/page.rs
  21. +19
    -0
      components/library/src/content/section.rs
  22. +2
    -2
      components/rendering/Cargo.toml
  23. +60
    -21
      components/rendering/src/markdown.rs
  24. +66
    -58
      components/rendering/tests/markdown.rs
  25. +3
    -2
      components/site/Cargo.toml
  26. +191
    -37
      components/site/src/lib.rs
  27. +15
    -17
      components/site/src/sitemap.rs
  28. +10
    -9
      components/site/tests/site.rs
  29. +7
    -7
      components/site/tests/site_i18n.rs
  30. +4
    -3
      components/templates/Cargo.toml
  31. +5
    -4
      components/templates/src/builtins/rss.xml
  32. +2
    -1
      components/templates/src/builtins/sitemap.xml
  33. +1
    -0
      components/templates/src/builtins/split_sitemap_index.xml
  34. +3
    -12
      components/templates/src/global_fns/load_data.rs
  35. +36
    -3
      components/templates/src/global_fns/mod.rs
  36. +1
    -1
      components/templates/src/lib.rs
  37. +1
    -1
      components/utils/Cargo.toml
  38. +2
    -2
      components/utils/src/fs.rs
  39. +1
    -4
      components/utils/src/net.rs
  40. +40
    -14
      components/utils/src/site.rs
  41. +4
    -4
      components/utils/src/templates.rs
  42. +11
    -5
      docs/content/documentation/content/image-processing/index.md
  43. +17
    -9
      docs/content/documentation/content/linking.md
  44. +4
    -4
      docs/content/documentation/content/overview.md
  45. +2
    -2
      docs/content/documentation/content/page.md
  46. +6
    -2
      docs/content/documentation/content/section.md
  47. +2
    -1
      docs/content/documentation/content/syntax-highlighting.md
  48. +1
    -1
      docs/content/documentation/content/table-of-contents.md
  49. +1
    -1
      docs/content/documentation/content/taxonomies.md
  50. +1
    -1
      docs/content/documentation/deployment/github-pages.md
  51. +1
    -1
      docs/content/documentation/deployment/gitlab-pages.md
  52. +7
    -18
      docs/content/documentation/deployment/netlify.md
  53. +5
    -0
      docs/content/documentation/getting-started/cli-usage.md
  54. +0
    -6
      docs/content/documentation/getting-started/configuration.md
  55. +6
    -6
      docs/content/documentation/getting-started/directory-structure.md
  56. +15
    -5
      docs/content/documentation/templates/overview.md
  57. +1
    -1
      docs/content/documentation/templates/pagination.md
  58. +1
    -1
      docs/content/documentation/templates/rss.md
  59. +1
    -1
      docs/content/documentation/templates/taxonomies.md
  60. +1
    -1
      docs/content/documentation/themes/creating-a-theme.md
  61. +3
    -3
      docs/content/documentation/themes/installing-and-using-themes.md
  62. +1
    -1
      docs/content/documentation/themes/overview.md
  63. +6
    -6
      docs/templates/index.html
  64. +3
    -3
      netlify.toml
  65. +2
    -2
      snapcraft.yaml
  66. +2
    -0
      src/cli.rs
  67. +24
    -0
      src/cmd/check.rs
  68. +2
    -0
      src/cmd/mod.rs
  69. +47
    -59
      src/cmd/serve.rs
  70. +1
    -1
      src/console.rs
  71. +20
    -3
      src/main.rs
  72. BIN
      sublime_syntaxes/newlines.packdump
  73. +609
    -0
      sublime_syntaxes/nix.sublime-syntax
  74. +1
    -0
      test_site/content/posts/_index.md
  75. +1
    -1
      test_site/content/posts/draft.md

+ 1
- 1
.travis.yml View File

@@ -16,7 +16,7 @@ matrix:

# The earliest stable Rust version that works
- env: TARGET=x86_64-unknown-linux-gnu
rust: 1.31.0
rust: 1.34.0


before_install: set -e


+ 14
- 0
CHANGELOG.md View File

@@ -1,5 +1,19 @@
# Changelog

## 0.8.0 (2019-06-22)

### Breaking

- Allow specifying heading IDs. It is a breaking change in the unlikely case you are using `{#..}` in your heading
- Internal links are now starting by `@/` rather than `./` to avoid confusion with relative links

### Other

- Fix image processing not happening if called from the template
- Add a `zola check` command to that validates the site and checks all external links
- Sections can have `aliases` as well
- Anchors in internal links are now checked for existence

## 0.7.0 (2019-04-28)

### Breaking


+ 831
- 692
Cargo.lock
File diff suppressed because it is too large
View File


+ 7
- 5
Cargo.toml View File

@@ -1,6 +1,6 @@
[package]
name = "zola"
version = "0.7.0"
version = "0.8.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
license = "MIT"
readme = "README.md"
@@ -26,9 +26,10 @@ termcolor = "1.0.4"
# Used in init to ensure the url given as base_url is a valid one
url = "1.5"
# Below is for the serve cmd
actix-web = { version = "0.7", default-features = false, features = [] }
actix-files = "0.1"
actix-web = { version = "1.0", default-features = false, features = [] }
notify = "4"
ws = "0.7"
ws = "0.8"
ctrlc = "3"

site = { path = "components/site" }
@@ -53,5 +54,6 @@ members = [
"components/library",
]

#[profile.release]
#debug = true
[profile.release]
lto = true
codegen-units = 1

+ 14
- 0
Dockerfile View File

@@ -0,0 +1,14 @@
FROM bitnami/minideb AS builder
RUN install_packages python-pip curl tar python-setuptools rsync binutils
RUN pip install dockerize
RUN mkdir -p /workdir
WORKDIR /workdir
ENV DOCKER_TAG v0.7.0
RUN curl -L https://github.com/getzola/zola/releases/download/$DOCKER_TAG/zola-$DOCKER_TAG-x86_64-unknown-linux-gnu.tar.gz | tar xz
RUN mv zola /usr/bin
RUN dockerize -n -o /workdir /usr/bin/zola


FROM scratch
COPY --from=builder /workdir .
ENTRYPOINT [ "/usr/bin/zola" ]

+ 1
- 1
README.md View File

@@ -32,7 +32,7 @@ in the `docs/content` folder of the repository and the community can use [its fo
| Search | ![yes](./is-yes.svg) | ![no](./is-no.svg) | ![no](./is-no.svg) | ![yes](./is-yes.svg) |
| Data files | ![yes](./is-yes.svg) | ![yes](./is-yes.svg) | ![yes](./is-yes.svg) | ![no](./is-no.svg) |
| LiveReload | ![yes](./is-yes.svg) | ![no](./is-no.svg) | ![yes](./is-yes.svg) | ![yes](./is-yes.svg) |
| Netlify support | ![ehh](./is-ehh.svg) | ![no](./is-no.svg) | ![yes](./is-yes.svg) | ![no](./is-no.svg) |
| Netlify support | ![yes](./is-yes.svg) | ![no](./is-no.svg) | ![yes](./is-yes.svg) | ![no](./is-no.svg) |
| Breadcrumbs | ![yes](./is-yes.svg) | ![no](./is-no.svg) | ![no](./is-no.svg) | ![yes](./is-yes.svg) |
| Custom output formats | ![no](./is-no.svg) | ![no](./is-no.svg) | ![yes](./is-yes.svg) | ![no](./is-no.svg) |



+ 1
- 1
appveyor.yml View File

@@ -10,7 +10,7 @@ environment:

matrix:
- target: x86_64-pc-windows-msvc
RUST_VERSION: 1.31.0
RUST_VERSION: 1.34.0
- target: x86_64-pc-windows-msvc
RUST_VERSION: stable



+ 17
- 1
completions/_zola View File

@@ -68,6 +68,14 @@ _arguments "${_arguments_options[@]}" \
'--version[Prints version information]' \
&& ret=0
;;
(check)
_arguments "${_arguments_options[@]}" \
'-h[Prints help information]' \
'--help[Prints help information]' \
'-V[Prints version information]' \
'--version[Prints version information]' \
&& ret=0
;;
(help)
_arguments "${_arguments_options[@]}" \
'-h[Prints help information]' \
@@ -85,8 +93,9 @@ esac
_zola_commands() {
local commands; commands=(
"init:Create a new Zola project" \
"build:Builds the site" \
"build:Deletes the output directory if there is one and builds the site" \
"serve:Serve the site. Rebuild and reload on change automatically" \
"check:Try building the project without rendering it. Checks links" \
"help:Prints this message or the help of the given subcommand(s)" \
)
_describe -t commands 'zola commands' commands "$@"
@@ -98,6 +107,13 @@ _zola__build_commands() {
)
_describe -t commands 'zola build commands' commands "$@"
}
(( $+functions[_zola__check_commands] )) ||
_zola__check_commands() {
local commands; commands=(
)
_describe -t commands 'zola check commands' commands "$@"
}
(( $+functions[_zola__help_commands] )) ||
_zola__help_commands() {
local commands; commands=(


+ 9
- 1
completions/_zola.ps1 View File

@@ -27,8 +27,9 @@ Register-ArgumentCompleter -Native -CommandName 'zola' -ScriptBlock {
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information')
[CompletionResult]::new('init', 'init', [CompletionResultType]::ParameterValue, 'Create a new Zola project')
[CompletionResult]::new('build', 'build', [CompletionResultType]::ParameterValue, 'Builds the site')
[CompletionResult]::new('build', 'build', [CompletionResultType]::ParameterValue, 'Deletes the output directory if there is one and builds the site')
[CompletionResult]::new('serve', 'serve', [CompletionResultType]::ParameterValue, 'Serve the site. Rebuild and reload on change automatically')
[CompletionResult]::new('check', 'check', [CompletionResultType]::ParameterValue, 'Try building the project without rendering it. Checks links')
[CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Prints this message or the help of the given subcommand(s)')
break
}
@@ -66,6 +67,13 @@ Register-ArgumentCompleter -Native -CommandName 'zola' -ScriptBlock {
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information')
break
}
'zola;check' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Prints version information')
break
}
'zola;help' {
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Prints help information')


+ 43
- 25
completions/zola.bash View File

@@ -16,6 +16,9 @@ _zola() {
build)
cmd+="__build"
;;
check)
cmd+="__check"
;;
help)
cmd+="__help"
;;
@@ -32,64 +35,79 @@ _zola() {

case "${cmd}" in
zola)
opts=" -h -V -c --help --version --config init build serve help"
opts=" -h -V -c --help --version --config init build serve check help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--config)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-c)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zola__build)
opts=" -h -V -u -o --help --version --base-url --output-dir "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--base-url)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-u)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--output-dir)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-o)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zola__check)
opts=" -h -V --help --version "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zola__help)
opts=" -h -V --help --version "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
@@ -98,13 +116,13 @@ _zola() {
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zola__init)
opts=" -h -V --help --version <name> "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
@@ -113,54 +131,54 @@ _zola() {
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
zola__serve)
opts=" -h -V -i -p -o -u --watch-only --help --version --interface --port --output-dir --base-url "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--interface)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-i)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--port)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-p)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--output-dir)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-o)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--base-url)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-u)
COMPREPLY=($(compgen -f ${cur}))
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
esac


+ 4
- 1
completions/zola.fish View File

@@ -2,8 +2,9 @@ complete -c zola -n "__fish_use_subcommand" -s c -l config -d 'Path to a config
complete -c zola -n "__fish_use_subcommand" -s h -l help -d 'Prints help information'
complete -c zola -n "__fish_use_subcommand" -s V -l version -d 'Prints version information'
complete -c zola -n "__fish_use_subcommand" -f -a "init" -d 'Create a new Zola project'
complete -c zola -n "__fish_use_subcommand" -f -a "build" -d 'Builds the site'
complete -c zola -n "__fish_use_subcommand" -f -a "build" -d 'Deletes the output directory if there is one and builds the site'
complete -c zola -n "__fish_use_subcommand" -f -a "serve" -d 'Serve the site. Rebuild and reload on change automatically'
complete -c zola -n "__fish_use_subcommand" -f -a "check" -d 'Try building the project without rendering it. Checks links'
complete -c zola -n "__fish_use_subcommand" -f -a "help" -d 'Prints this message or the help of the given subcommand(s)'
complete -c zola -n "__fish_seen_subcommand_from init" -s h -l help -d 'Prints help information'
complete -c zola -n "__fish_seen_subcommand_from init" -s V -l version -d 'Prints version information'
@@ -18,5 +19,7 @@ complete -c zola -n "__fish_seen_subcommand_from serve" -s u -l base-url -d 'Cha
complete -c zola -n "__fish_seen_subcommand_from serve" -l watch-only -d 'Do not start a server, just re-build project on changes'
complete -c zola -n "__fish_seen_subcommand_from serve" -s h -l help -d 'Prints help information'
complete -c zola -n "__fish_seen_subcommand_from serve" -s V -l version -d 'Prints version information'
complete -c zola -n "__fish_seen_subcommand_from check" -s h -l help -d 'Prints help information'
complete -c zola -n "__fish_seen_subcommand_from check" -s V -l version -d 'Prints version information'
complete -c zola -n "__fish_seen_subcommand_from help" -s h -l help -d 'Prints help information'
complete -c zola -n "__fish_seen_subcommand_from help" -s V -l version -d 'Prints version information'

+ 1
- 1
components/config/Cargo.toml View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]

[dependencies]
toml = "0.4"
toml = "0.5"
serde = "1"
serde_derive = "1"
chrono = "0.4"


+ 2
- 2
components/errors/Cargo.toml View File

@@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]

[dependencies]
tera = "1.0.0-alpha.3"
toml = "0.4"
tera = "1.0.0-beta.10"
toml = "0.5"
image = "0.21"
syntect = "3"

+ 2
- 2
components/front_matter/Cargo.toml View File

@@ -4,11 +4,11 @@ version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]

[dependencies]
tera = "1.0.0-alpha.3"
tera = "1.0.0-beta.10"
chrono = "0.4"
serde = "1"
serde_derive = "1"
toml = "0.4"
toml = "0.5"
regex = "1"
lazy_static = "1"



+ 5
- 0
components/front_matter/src/section.rs View File

@@ -58,6 +58,10 @@ pub struct SectionFrontMatter {
/// Optional template for all pages in this section (including the pages of children section)
#[serde(skip_serializing)]
pub page_template: Option<String>,
/// All aliases for that page. Zola will create HTML templates that will
/// redirect to this
#[serde(skip_serializing)]
pub aliases: Vec<String>,
/// Any extra parameter present in the front matter
pub extra: HashMap<String, Value>,
}
@@ -97,6 +101,7 @@ impl Default for SectionFrontMatter {
in_search_index: true,
transparent: false,
page_template: None,
aliases: Vec::new(),
extra: HashMap::new(),
}
}


+ 1
- 1
components/imageproc/Cargo.toml View File

@@ -6,7 +6,7 @@ authors = ["Vojtěch Král <vojtech@kral.hk>"]
[dependencies]
lazy_static = "1"
regex = "1.0"
tera = "1.0.0-alpha.3"
tera = "1.0.0-beta.10"
image = "0.21"
rayon = "1"



+ 7
- 3
components/imageproc/src/lib.rs View File

@@ -65,12 +65,16 @@ impl ResizeOp {
}
"fit_height" => {
if height.is_none() {
return Err("op=\"fit_height\" requires a `height` argument".to_string().into());
return Err("op=\"fit_height\" requires a `height` argument"
.to_string()
.into());
}
}
"scale" | "fit" | "fill" => {
if width.is_none() || height.is_none() {
return Err(format!("op={} requires a `width` and `height` argument", op).into());
return Err(
format!("op={} requires a `width` and `height` argument", op).into()
);
}
}
_ => return Err(format!("Invalid image resize operation: {}", op).into()),
@@ -168,7 +172,7 @@ impl Format {
pub fn is_lossy<P: AsRef<Path>>(p: P) -> Option<bool> {
p.as_ref()
.extension()
.and_then(|s| s.to_str())
.and_then(std::ffi::OsStr::to_str)
.map(|ext| match ext.to_lowercase().as_str() {
"jpg" | "jpeg" => Some(true),
"png" => Some(false),


+ 1
- 1
components/library/Cargo.toml View File

@@ -7,7 +7,7 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
slotmap = "0.2"
rayon = "1"
chrono = { version = "0.4", features = ["serde"] }
tera = "1.0.0-alpha.3"
tera = "1.0.0-beta.10"
serde = "1"
serde_derive = "1"
slug = "0.1"


+ 91
- 0
components/library/src/content/mod.rs View File

@@ -7,3 +7,94 @@ pub use self::file_info::FileInfo;
pub use self::page::Page;
pub use self::section::Section;
pub use self::ser::{SerializingPage, SerializingSection};

use rendering::Header;

pub fn has_anchor(headings: &[Header], anchor: &str) -> bool {
for heading in headings {
if heading.id == anchor {
return true;
}
if has_anchor(&heading.children, anchor) {
return true;
}
}

false
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn can_find_anchor_at_root() {
let input = vec![
Header {
level: 1,
id: "1".to_string(),
permalink: String::new(),
title: String::new(),
children: vec![],
},
Header {
level: 2,
id: "1-1".to_string(),
permalink: String::new(),
title: String::new(),
children: vec![],
},
Header {
level: 3,
id: "1-1-1".to_string(),
permalink: String::new(),
title: String::new(),
children: vec![],
},
Header {
level: 2,
id: "1-2".to_string(),
permalink: String::new(),
title: String::new(),
children: vec![],
},
];

assert!(has_anchor(&input, "1-2"));
}

#[test]
fn can_find_anchor_in_children() {
let input = vec![Header {
level: 1,
id: "1".to_string(),
permalink: String::new(),
title: String::new(),
children: vec![
Header {
level: 2,
id: "1-1".to_string(),
permalink: String::new(),
title: String::new(),
children: vec![],
},
Header {
level: 3,
id: "1-1-1".to_string(),
permalink: String::new(),
title: String::new(),
children: vec![],
},
Header {
level: 2,
id: "1-2".to_string(),
permalink: String::new(),
title: String::new(),
children: vec![],
},
],
}];

assert!(has_anchor(&input, "1-2"));
}
}

+ 69
- 7
components/library/src/content/page.rs View File

@@ -17,6 +17,7 @@ use utils::site::get_reading_analytics;
use utils::templates::render_template;

use content::file_info::FileInfo;
use content::has_anchor;
use content::ser::SerializingPage;

lazy_static! {
@@ -76,6 +77,13 @@ pub struct Page {
pub lang: String,
/// Contains all the translated version of that page
pub translations: Vec<Key>,
/// Contains the internal links that have an anchor: we can only check the anchor
/// after all pages have been built and their ToC compiled. The page itself should exist otherwise
/// it would have errored before getting there
/// (path to markdown, anchor value)
pub internal_links_with_anchors: Vec<(String, String)>,
/// Contains the external links that need to be checked
pub external_links: Vec<String>,
}

impl Page {
@@ -104,6 +112,8 @@ impl Page {
reading_time: None,
lang: String::new(),
translations: Vec::new(),
internal_links_with_anchors: Vec::new(),
external_links: Vec::new(),
}
}

@@ -185,6 +195,7 @@ impl Page {

page.path = path;
}

if !page.path.ends_with('/') {
page.path = format!("{}/", page.path);
}
@@ -233,7 +244,7 @@ impl Page {
page.assets = assets;
}

page.serialized_assets = page.serialize_assets();
page.serialized_assets = page.serialize_assets(&base_path);
} else {
page.assets = vec![];
}
@@ -262,6 +273,8 @@ impl Page {
self.summary = res.summary_len.map(|l| res.body[0..l].to_owned());
self.content = res.body;
self.toc = res.toc;
self.external_links = res.external_links;
self.internal_links_with_anchors = res.internal_links_with_anchors;

Ok(())
}
@@ -287,15 +300,31 @@ impl Page {
}

/// Creates a vectors of asset URLs.
fn serialize_assets(&self) -> Vec<String> {
fn serialize_assets(&self, base_path: &PathBuf) -> Vec<String> {
self.assets
.iter()
.filter_map(|asset| asset.file_name())
.filter_map(|filename| filename.to_str())
.map(|filename| self.path.clone() + filename)
.map(|filename| {
let mut path = self.file.path.clone();
// Popping the index.md from the path since file.parent would be one level too high
// for our need here
path.pop();
path.push(filename);
path = path
.strip_prefix(&base_path.join("content"))
.expect("Should be able to stripe prefix")
.to_path_buf();
path
})
.map(|path| path.to_string_lossy().to_string())
.collect()
}

pub fn has_anchor(&self, anchor: &str) -> bool {
has_anchor(&self.toc, anchor)
}

pub fn to_serialized<'a>(&'a self, library: &'a Library) -> SerializingPage<'a> {
SerializingPage::from_page(self, library)
}
@@ -329,6 +358,8 @@ impl Default for Page {
reading_time: None,
lang: String::new(),
translations: Vec::new(),
internal_links_with_anchors: Vec::new(),
external_links: Vec::new(),
}
}
}
@@ -507,7 +538,7 @@ Hello world
let res = Page::from_file(
nested_path.join("index.md").as_path(),
&Config::default(),
&PathBuf::new(),
&path.to_path_buf(),
);
assert!(res.is_ok());
let page = res.unwrap();
@@ -534,7 +565,7 @@ Hello world
let res = Page::from_file(
nested_path.join("index.md").as_path(),
&Config::default(),
&PathBuf::new(),
&path.to_path_buf(),
);
assert!(res.is_ok());
let page = res.unwrap();
@@ -544,6 +575,36 @@ Hello world
assert_eq!(page.permalink, "http://a-website.com/posts/hey/");
}

// https://github.com/getzola/zola/issues/674
#[test]
fn page_with_assets_uses_filepath_for_assets() {
let tmp_dir = tempdir().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("with_assets");
create_dir(&nested_path).expect("create nested temp dir");
let mut f = File::create(nested_path.join("index.md")).unwrap();
f.write_all(b"+++\n+++\n").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::from_file(
nested_path.join("index.md").as_path(),
&Config::default(),
&path.to_path_buf(),
);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.file.parent, path.join("content").join("posts"));
assert_eq!(page.assets.len(), 3);
assert_eq!(page.serialized_assets.len(), 3);
// We should not get with-assets since that's the slugified version
assert!(page.serialized_assets[0].contains("with_assets"));
assert_eq!(page.permalink, "http://a-website.com/posts/with-assets/");
}

// https://github.com/getzola/zola/issues/607
#[test]
fn page_with_assets_and_date_in_folder_name() {
@@ -562,7 +623,7 @@ Hello world
let res = Page::from_file(
nested_path.join("index.md").as_path(),
&Config::default(),
&PathBuf::new(),
&path.to_path_buf(),
);
assert!(res.is_ok());
let page = res.unwrap();
@@ -592,7 +653,8 @@ Hello world
let mut config = Config::default();
config.ignored_content_globset = Some(gsb.build().unwrap());

let res = Page::from_file(nested_path.join("index.md").as_path(), &config, &PathBuf::new());
let res =
Page::from_file(nested_path.join("index.md").as_path(), &config, &path.to_path_buf());

assert!(res.is_ok());
let page = res.unwrap();


+ 19
- 0
components/library/src/content/section.rs View File

@@ -13,6 +13,7 @@ use utils::site::get_reading_analytics;
use utils::templates::render_template;

use content::file_info::FileInfo;
use content::has_anchor;
use content::ser::SerializingSection;
use library::Library;

@@ -56,6 +57,13 @@ pub struct Section {
pub lang: String,
/// Contains all the translated version of that section
pub translations: Vec<Key>,
/// Contains the internal links that have an anchor: we can only check the anchor
/// after all pages have been built and their ToC compiled. The page itself should exist otherwise
/// it would have errored before getting there
/// (path to markdown, anchor value)
pub internal_links_with_anchors: Vec<(String, String)>,
/// Contains the external links that need to be checked
pub external_links: Vec<String>,
}

impl Section {
@@ -85,6 +93,8 @@ impl Section {
reading_time: None,
lang: String::new(),
translations: Vec::new(),
internal_links_with_anchors: Vec::new(),
external_links: Vec::new(),
}
}

@@ -189,6 +199,9 @@ impl Section {
})?;
self.content = res.body;
self.toc = res.toc;
self.external_links = res.external_links;
self.internal_links_with_anchors = res.internal_links_with_anchors;

Ok(())
}

@@ -224,6 +237,10 @@ impl Section {
.collect()
}

pub fn has_anchor(&self, anchor: &str) -> bool {
has_anchor(&self.toc, anchor)
}

pub fn to_serialized<'a>(&'a self, library: &'a Library) -> SerializingSection<'a> {
SerializingSection::from_section(self, library)
}
@@ -255,6 +272,8 @@ impl Default for Section {
word_count: None,
lang: String::new(),
translations: Vec::new(),
internal_links_with_anchors: Vec::new(),
external_links: Vec::new(),
}
}
}


+ 2
- 2
components/rendering/Cargo.toml View File

@@ -4,9 +4,9 @@ version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]

[dependencies]
tera = { version = "1.0.0-alpha.3", features = ["preserve_order"] }
tera = { version = "1.0.0-beta.10", features = ["preserve_order"] }
syntect = "3"
pulldown-cmark = "0.4"
pulldown-cmark = "0.5"
slug = "0.1"
serde = "1"
serde_derive = "1"


+ 60
- 21
components/rendering/src/markdown.rs View File

@@ -9,7 +9,6 @@ use config::highlighting::{get_highlighter, SYNTAX_SET, THEME_SET};
use context::RenderContext;
use errors::{Error, Result};
use front_matter::InsertAnchor;
use link_checker::check_url;
use table_of_contents::{make_table_of_contents, Header};
use utils::site::resolve_internal_link;
use utils::vec::InsertMany;
@@ -25,6 +24,8 @@ pub struct Rendered {
pub body: String,
pub summary_len: Option<usize>,
pub toc: Vec<Header>,
pub internal_links_with_anchors: Vec<(String, String)>,
pub external_links: Vec<String>,
}

// tracks a header in a slice of pulldown-cmark events
@@ -33,11 +34,12 @@ struct HeaderRef {
start_idx: usize,
end_idx: usize,
level: i32,
id: Option<String>,
}

impl HeaderRef {
fn new(start: usize, level: i32) -> HeaderRef {
HeaderRef { start_idx: start, end_idx: 0, level }
HeaderRef { start_idx: start, end_idx: 0, level, id: None }
}
}

@@ -65,34 +67,39 @@ fn is_colocated_asset_link(link: &str) -> bool {
&& !link.starts_with("mailto:")
}

fn fix_link(link_type: LinkType, link: &str, context: &RenderContext) -> Result<String> {
fn fix_link(
link_type: LinkType,
link: &str,
context: &RenderContext,
internal_links_with_anchors: &mut Vec<(String, String)>,
external_links: &mut Vec<String>,
) -> Result<String> {
if link_type == LinkType::Email {
return Ok(link.to_string());
}
// A few situations here:
// - it could be a relative link (starting with `./`)
// - it could be a relative link (starting with `@/`)
// - it could be a link to a co-located asset
// - it could be a normal link
let result = if link.starts_with("./") {
let result = if link.starts_with("@/") {
match resolve_internal_link(&link, context.permalinks) {
Ok(url) => url,
Ok(resolved) => {
if resolved.anchor.is_some() {
internal_links_with_anchors
.push((resolved.md_path.unwrap(), resolved.anchor.unwrap()));
}
resolved.permalink
}
Err(_) => {
return Err(format!("Relative link {} not found.", link).into());
}
}
} else if is_colocated_asset_link(&link) {
format!("{}{}", context.current_page_permalink, link)
} else if context.config.check_external_links
&& !link.starts_with('#')
&& !link.starts_with("mailto:")
{
let res = check_url(&link);
if res.is_valid() {
link.to_string()
} else {
return Err(format!("Link {} is not valid: {}", link, res.message()).into());
}
} else {
if !link.starts_with('#') && !link.starts_with("mailto:") {
external_links.push(link.to_owned());
}
link.to_string()
};
Ok(result)
@@ -103,8 +110,9 @@ fn get_text(parser_slice: &[Event]) -> String {
let mut title = String::new();

for event in parser_slice.iter() {
if let Event::Text(text) = event {
title += text;
match event {
Event::Text(text) | Event::Code(text) => title += text,
_ => continue,
}
}

@@ -141,6 +149,8 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render

let mut inserted_anchors: Vec<String> = vec![];
let mut headers: Vec<Header> = vec![];
let mut internal_links_with_anchors = Vec::new();
let mut external_links = Vec::new();

let mut opts = Options::empty();
let mut has_summary = false;
@@ -206,7 +216,13 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
Event::Start(Tag::Image(link_type, src, title))
}
Event::Start(Tag::Link(link_type, link, title)) => {
let fixed_link = match fix_link(link_type, &link, context) {
let fixed_link = match fix_link(
link_type,
&link,
context,
&mut internal_links_with_anchors,
&mut external_links,
) {
Ok(fixed_link) => fixed_link,
Err(err) => {
error = Some(err);
@@ -225,15 +241,36 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
})
.collect::<Vec<_>>(); // We need to collect the events to make a second pass

let header_refs = get_header_refs(&events);
let mut header_refs = get_header_refs(&events);

let mut anchors_to_insert = vec![];

// First header pass: look for a manually-specified IDs, e.g. `# Heading text {#hash}`
// (This is a separate first pass so that auto IDs can avoid collisions with manual IDs.)
for header_ref in header_refs.iter_mut() {
let end_idx = header_ref.end_idx;
if let Event::Text(ref mut text) = events[end_idx - 1] {
if text.as_bytes().last() == Some(&b'}') {
if let Some(mut i) = text.find("{#") {
let id = text[i + 2..text.len() - 1].to_owned();
inserted_anchors.push(id.clone());
while i > 0 && text.as_bytes()[i - 1] == b' ' {
i -= 1;
}
header_ref.id = Some(id);
*text = text[..i].to_owned().into();
}
}
}
}

// Second header pass: auto-generate remaining IDs, and emit HTML
for header_ref in header_refs {
let start_idx = header_ref.start_idx;
let end_idx = header_ref.end_idx;
let title = get_text(&events[start_idx + 1..end_idx]);
let id = find_anchor(&inserted_anchors, slugify(&title), 0);
let id =
header_ref.id.unwrap_or_else(|| find_anchor(&inserted_anchors, slugify(&title), 0));
inserted_anchors.push(id.clone());

// insert `id` to the tag
@@ -280,6 +317,8 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
summary_len: if has_summary { html.find(CONTINUE_READING) } else { None },
body: html,
toc: make_table_of_contents(headers),
internal_links_with_anchors,
external_links,
})
}
}

+ 66
- 58
components/rendering/tests/markdown.rs View File

@@ -299,7 +299,7 @@ fn can_make_valid_relative_link() {
let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
let res = render_content(
r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#,
r#"[rel link](@/pages/about.md), [abs link](https://vincent.is/about)"#,
&context,
)
.unwrap();
@@ -316,7 +316,7 @@ fn can_make_relative_links_with_anchors() {
let tera_ctx = Tera::default();
let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
let res = render_content(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap();
let res = render_content(r#"[rel link](@/pages/about.md#cv)"#, &context).unwrap();

assert!(res.body.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#));
}
@@ -327,7 +327,7 @@ fn errors_relative_link_inexistant() {
let permalinks_ctx = HashMap::new();
let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("[rel link](./pages/about.md)", &context);
let res = render_content("[rel link](@/pages/about.md)", &context);
assert!(res.is_err());
}

@@ -351,6 +351,56 @@ fn can_add_id_to_headers_same_slug() {
assert_eq!(res.body, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n");
}

#[test]
fn can_handle_manual_ids_on_headers() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
// Tested things: manual IDs; whitespace flexibility; that automatic IDs avoid collision with
// manual IDs; that duplicates are in fact permitted among manual IDs; that any non-plain-text
// in the middle of `{#…}` will disrupt it from being acknowledged as a manual ID (that last
// one could reasonably be considered a bug rather than a feature, but test it either way); one
// workaround for the improbable case where you actually want `{#…}` at the end of a header.
let res = render_content(
"\
# Hello\n\
# Hello{#hello}\n\
# Hello {#hello}\n\
# Hello {#Something_else} \n\
# Workaround for literal {#…&#125;\n\
# Hello\n\
# Auto {#*matic*}",
&context,
)
.unwrap();
assert_eq!(
res.body,
"\
<h1 id=\"hello-1\">Hello</h1>\n\
<h1 id=\"hello\">Hello</h1>\n\
<h1 id=\"hello\">Hello</h1>\n\
<h1 id=\"Something_else\">Hello</h1>\n\
<h1 id=\"workaround-for-literal\">Workaround for literal {#…}</h1>\n\
<h1 id=\"hello-2\">Hello</h1>\n\
<h1 id=\"auto-matic\">Auto {#<em>matic</em>}</h1>\n\
"
);
}

#[test]
fn blank_headers() {
let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new();
let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# \n#\n# {#hmm} \n# {#}", &context).unwrap();
assert_eq!(
res.body,
"<h1 id=\"-1\"></h1>\n<h1 id=\"-2\"></h1>\n<h1 id=\"hmm\"></h1>\n<h1 id=\"\"></h1>\n"
);
}

#[test]
fn can_insert_anchor_left() {
let permalinks_ctx = HashMap::new();
@@ -583,7 +633,7 @@ fn can_make_valid_relative_link_in_header() {
let tera_ctx = Tera::default();
let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
let res = render_content(r#" # [rel link](./pages/about.md)"#, &context).unwrap();
let res = render_content(r#" # [rel link](@/pages/about.md)"#, &context).unwrap();

assert_eq!(
res.body,
@@ -657,44 +707,9 @@ Some text
}

#[test]
fn can_validate_valid_external_links() {
fn correctly_captures_external_links() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.check_external_links = true;
let context = RenderContext::new(
&ZOLA_TERA,
&config,
"https://vincent.is/about/",
&permalinks_ctx,
InsertAnchor::None,
);
let res = render_content("[a link](http://google.com)", &context).unwrap();
assert_eq!(res.body, "<p><a href=\"http://google.com\">a link</a></p>\n");
}

#[test]
fn can_show_error_message_for_invalid_external_links() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.check_external_links = true;
let context = RenderContext::new(
&ZOLA_TERA,
&config,
"https://vincent.is/about/",
&permalinks_ctx,
InsertAnchor::None,
);
let res = render_content("[a link](http://google.comy)", &context);
assert!(res.is_err());
let err = res.unwrap_err();
assert!(format!("{}", err).contains("Link http://google.comy is not valid"));
}

#[test]
fn doesnt_try_to_validate_email_links_mailto() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.check_external_links = true;
let config = Config::default();
let context = RenderContext::new(
&ZOLA_TERA,
&config,
@@ -702,24 +717,17 @@ fn doesnt_try_to_validate_email_links_mailto() {
&permalinks_ctx,
InsertAnchor::None,
);
let res = render_content("Email: [foo@bar.baz](mailto:foo@bar.baz)", &context).unwrap();
assert_eq!(res.body, "<p>Email: <a href=\"mailto:foo@bar.baz\">foo@bar.baz</a></p>\n");
}

#[test]
fn doesnt_try_to_validate_email_links_angled_brackets() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.check_external_links = true;
let context = RenderContext::new(
&ZOLA_TERA,
&config,
"https://vincent.is/about/",
&permalinks_ctx,
InsertAnchor::None,
let content = "
[a link](http://google.com)
[a link](http://google.comy)
Email: [foo@bar.baz](mailto:foo@bar.baz)
Email: <foo@bar.baz>
";
let res = render_content(content, &context).unwrap();
assert_eq!(
res.external_links,
&["http://google.com".to_owned(), "http://google.comy".to_owned()]
);
let res = render_content("Email: <foo@bar.baz>", &context).unwrap();
assert_eq!(res.body, "<p>Email: <a href=\"mailto:foo@bar.baz\">foo@bar.baz</a></p>\n");
}

#[test]


+ 3
- 2
components/site/Cargo.toml View File

@@ -4,8 +4,8 @@ version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]

[dependencies]
tera = "1.0.0-alpha.3"
glob = "0.2"
tera = "1.0.0-beta.10"
glob = "0.3"
rayon = "1"
serde = "1"
serde_derive = "1"
@@ -19,6 +19,7 @@ front_matter = { path = "../front_matter" }
search = { path = "../search" }
imageproc = { path = "../imageproc" }
library = { path = "../library" }
link_checker = { path = "../link_checker" }

[dev-dependencies]
tempfile = "3"

+ 191
- 37
components/site/src/lib.rs View File

@@ -12,6 +12,7 @@ extern crate config;
extern crate front_matter;
extern crate imageproc;
extern crate library;
extern crate link_checker;
extern crate search;
extern crate templates;
extern crate utils;
@@ -19,10 +20,9 @@ extern crate utils;
#[cfg(test)]
extern crate tempfile;


mod sitemap;

use std::collections::{HashMap};
use std::collections::HashMap;
use std::fs::{copy, create_dir_all, remove_dir_all};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};
@@ -33,11 +33,12 @@ use sass_rs::{compile_file, Options as SassOptions, OutputStyle};
use tera::{Context, Tera};

use config::{get_config, Config};
use errors::{Error, Result};
use errors::{Error, ErrorKind, Result};
use front_matter::InsertAnchor;
use library::{
find_taxonomies, sort_actual_pages_by_date, Library, Page, Paginator, Section, Taxonomy,
};
use link_checker::check_url;
use templates::{global_fns, render_redirect_template, ZOLA_TERA};
use utils::fs::{copy_directory, create_directory, create_file, ensure_directory_exists};
use utils::net::get_available_port;
@@ -243,9 +244,148 @@ impl Site {
self.render_markdown()?;
self.register_tera_global_fns();

// Needs to be done after rendering markdown as we only get the anchors at that point
self.check_internal_links_with_anchors()?;

if self.config.check_external_links {
self.check_external_links()?;
}

Ok(())
}

/// Very similar to check_external_links but can't be merged as far as I can see since we always
/// want to check the internal links but only the external in zola check :/
pub fn check_internal_links_with_anchors(&self) -> Result<()> {
let library = self.library.write().expect("Get lock for check_internal_links_with_anchors");
let page_links = library
.pages()
.values()
.map(|p| {
let path = &p.file.path;
p.internal_links_with_anchors.iter().map(move |l| (path.clone(), l))
})
.flatten();
let section_links = library
.sections()
.values()
.map(|p| {
let path = &p.file.path;
p.internal_links_with_anchors.iter().map(move |l| (path.clone(), l))
})
.flatten();
let all_links = page_links.chain(section_links).collect::<Vec<_>>();
let mut full_path = self.base_path.clone();
full_path.push("content");

let errors: Vec<_> = all_links
.iter()
.filter_map(|(page_path, (md_path, anchor))| {
// There are a few `expect` here since the presence of the .md file will
// already have been checked in the markdown rendering
let mut p = full_path.clone();
for part in md_path.split('/') {
p.push(part);
}
if md_path.contains("_index.md") {
let section = library
.get_section(&p)
.expect("Couldn't find section in check_internal_links_with_anchors");
if section.has_anchor(&anchor) {
None
} else {
Some((page_path, md_path, anchor))
}
} else {
let page = library
.get_page(&p)
.expect("Couldn't find section in check_internal_links_with_anchors");
if page.has_anchor(&anchor) {
None
} else {
Some((page_path, md_path, anchor))
}
}
})
.collect();
if errors.is_empty() {
return Ok(());
}
let msg = errors
.into_iter()
.map(|(page_path, md_path, anchor)| {
format!(
"The anchor in the link `@/{}#{}` in {} does not exist.",
md_path,
anchor,
page_path.to_string_lossy(),
)
})
.collect::<Vec<_>>()
.join("\n");
Err(Error { kind: ErrorKind::Msg(msg.into()), source: None })
}

pub fn check_external_links(&self) -> Result<()> {
let library = self.library.write().expect("Get lock for check_external_links");
let page_links = library
.pages()
.values()
.map(|p| {
let path = &p.file.path;
p.external_links.iter().map(move |l| (path.clone(), l))
})
.flatten();
let section_links = library
.sections()
.values()
.map(|p| {
let path = &p.file.path;
p.external_links.iter().map(move |l| (path.clone(), l))
})
.flatten();
let all_links = page_links.chain(section_links).collect::<Vec<_>>();

// create thread pool with lots of threads so we can fetch
// (almost) all pages simultaneously
let threads = std::cmp::min(all_links.len(), 32);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.map_err(|e| Error { kind: ErrorKind::Msg(e.to_string().into()), source: None })?;

let errors: Vec<_> = pool.install(|| {
all_links
.par_iter()
.filter_map(|(page_path, link)| {
let res = check_url(&link);
if res.is_valid() {
None
} else {
Some((page_path, link, res))
}
})
.collect()
});

if errors.is_empty() {
return Ok(());
}
let msg = errors
.into_iter()
.map(|(page_path, link, check_res)| {
format!(
"Dead link in {} to {}: {}",
page_path.to_string_lossy(),
link,
check_res.message()
)
})
.collect::<Vec<_>>()
.join("\n");
Err(Error { kind: ErrorKind::Msg(msg.into()), source: None })
}

/// Insert a default index section for each language if necessary so we don't need to create
/// a _index.md to render the index page at the root of the site
pub fn create_default_index_sections(&mut self) -> Result<()> {
@@ -338,6 +478,10 @@ impl Site {
"resize_image",
global_fns::ResizeImage::new(self.imageproc.clone()),
);
self.tera.register_function(
"get_image_metadata",
global_fns::GetImageMeta::new(self.content_path.clone()),
);
self.tera.register_function("load_data", global_fns::LoadData::new(self.base_path.clone()));
self.tera.register_function("trans", global_fns::Trans::new(self.config.clone()));
self.tera.register_function(
@@ -537,9 +681,6 @@ impl Site {
self.compile_sass(&self.base_path)?;
}

self.process_images()?;
self.copy_static_directories()?;

if self.config.build_search_index {
self.build_search_index()?;
}
@@ -577,6 +718,11 @@ impl Site {
self.render_404()?;
self.render_robots()?;
self.render_taxonomies()?;
// We process images at the end as we might have picked up images to process from markdown
// or from templates
self.process_images()?;
// Processed images will be in static so the last step is to copy it
self.copy_static_directories()?;

Ok(())
}
@@ -662,35 +808,46 @@ impl Site {
Ok(compiled_paths)
}

fn render_alias(&self, alias: &str, permalink: &str) -> Result<()> {
let mut output_path = self.output_path.to_path_buf();
let mut split = alias.split('/').collect::<Vec<_>>();

// If the alias ends with an html file name, use that instead of mapping
// as a path containing an `index.html`
let page_name = match split.pop() {
Some(part) if part.ends_with(".html") => part,
Some(part) => {
split.push(part);
"index.html"
}
None => "index.html",
};

for component in split {
output_path.push(&component);

if !output_path.exists() {
create_directory(&output_path)?;
}
}

create_file(
&output_path.join(page_name),
&render_redirect_template(&permalink, &self.tera)?,
)
}

pub fn render_aliases(&self) -> Result<()> {
ensure_directory_exists(&self.output_path)?;
for (_, page) in self.library.read().unwrap().pages() {
let library = self.library.read().unwrap();
for (_, page) in library.pages() {
for alias in &page.meta.aliases {
let mut output_path = self.output_path.to_path_buf();
let mut split = alias.split('/').collect::<Vec<_>>();

// If the alias ends with an html file name, use that instead of mapping
// as a path containing an `index.html`
let page_name = match split.pop() {
Some(part) if part.ends_with(".html") => part,
Some(part) => {
split.push(part);
"index.html"
}
None => "index.html",
};

for component in split {
output_path.push(&component);

if !output_path.exists() {
create_directory(&output_path)?;
}
}
create_file(
&output_path.join(page_name),
&render_redirect_template(&page.permalink, &self.tera)?,
)?;
self.render_alias(&alias, &page.permalink)?;
}
}
for (_, section) in library.sections() {
for alias in &section.meta.aliases {
self.render_alias(&alias, &section.permalink)?;
}
}
Ok(())
@@ -778,11 +935,8 @@ impl Site {

let library = self.library.read().unwrap();
let all_sitemap_entries = {
let mut all_sitemap_entries = sitemap::find_entries(
&library,
&self.taxonomies[..],
&self.config,
);
let mut all_sitemap_entries =
sitemap::find_entries(&library, &self.taxonomies[..], &self.config);
all_sitemap_entries.sort();
all_sitemap_entries
};


+ 15
- 17
components/site/src/sitemap.rs View File

@@ -1,11 +1,11 @@
use std::borrow::Cow;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::collections::{HashSet};

use tera::{Map, Value};
use config::{Config};
use config::Config;
use library::{Library, Taxonomy};
use std::cmp::Ordering;
use tera::{Map, Value};

/// The sitemap only needs links, potentially date and extra for pages in case of updates
/// for examples so we trim down all entries to only that
@@ -54,7 +54,11 @@ impl<'a> Ord for SitemapEntry<'a> {

/// Finds out all the links to put in a sitemap from the pages/sections/taxonomies
/// There are no duplicate permalinks in the output vec
pub fn find_entries<'a>(library: &'a Library, taxonomies: &'a [Taxonomy], config: &'a Config) -> Vec<SitemapEntry<'a>> {
pub fn find_entries<'a>(
library: &'a Library,
taxonomies: &'a [Taxonomy],
config: &'a Config,
) -> Vec<SitemapEntry<'a>> {
let pages = library
.pages_values()
.iter()
@@ -77,20 +81,14 @@ pub fn find_entries<'a>(library: &'a Library, taxonomies: &'a [Taxonomy], config
.map(|s| SitemapEntry::new(Cow::Borrowed(&s.permalink), None))
.collect::<Vec<_>>();

for section in library
.sections_values()
.iter()
.filter(|s| s.meta.paginate_by.is_some())
{
let number_pagers = (section.pages.len() as f64
/ section.meta.paginate_by.unwrap() as f64)
.ceil() as isize;
for i in 1..=number_pagers {
let permalink =
format!("{}{}/{}/", section.permalink, section.meta.paginate_path, i);
sections.push(SitemapEntry::new(Cow::Owned(permalink), None))
}
for section in library.sections_values().iter().filter(|s| s.meta.paginate_by.is_some()) {
let number_pagers =
(section.pages.len() as f64 / section.meta.paginate_by.unwrap() as f64).ceil() as isize;
for i in 1..=number_pagers {
let permalink = format!("{}{}/{}/", section.permalink, section.meta.paginate_path, i);
sections.push(SitemapEntry::new(Cow::Owned(permalink), None))
}
}

let mut taxonomies_entries = vec![];
for taxonomy in taxonomies {


+ 10
- 9
components/site/tests/site.rs View File

@@ -127,6 +127,7 @@ fn can_build_site_without_live_reload() {
// aliases work
assert!(file_exists!(public, "an-old-url/old-page/index.html"));
assert!(file_contains!(public, "an-old-url/old-page/index.html", "something-else"));
assert!(file_contains!(public, "another-old-url/index.html", "posts/"));

// html aliases work
assert!(file_exists!(public, "an-old-url/an-old-alias.html"));
@@ -166,12 +167,12 @@ fn can_build_site_without_live_reload() {
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/posts/simple/</loc>"
"<loc>https%3A//replace-this-with-your-url.com/posts/simple/</loc>"
));
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/posts/</loc>"
"<loc>https%3A//replace-this-with-your-url.com/posts/</loc>"
));
// Drafts are not in the sitemap
assert!(!file_contains!(public, "sitemap.xml", "draft"));
@@ -278,7 +279,7 @@ fn can_build_site_with_taxonomies() {
assert!(file_contains!(
public,
"categories/a/rss.xml",
"https://replace-this-with-your-url.com/categories/a/rss.xml"
"https%3A//replace-this-with-your-url.com/categories/a/rss.xml"
));
// Extending from a theme works
assert!(file_contains!(public, "categories/a/index.html", "EXTENDED"));
@@ -289,12 +290,12 @@ fn can_build_site_with_taxonomies() {
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/categories/</loc>"
"<loc>https%3A//replace-this-with-your-url.com/categories/</loc>"
));
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/categories/a/</loc>"
"<loc>https%3A//replace-this-with-your-url.com/categories/a/</loc>"
));
}

@@ -423,7 +424,7 @@ fn can_build_site_with_pagination_for_section() {
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/posts/page/4/</loc>"
"<loc>https%3A//replace-this-with-your-url.com/posts/page/4/</loc>"
));
}

@@ -476,7 +477,7 @@ fn can_build_site_with_pagination_for_index() {
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/page/1/</loc>"
"<loc>https%3A//replace-this-with-your-url.com/page/1/</loc>"
))
}

@@ -557,7 +558,7 @@ fn can_build_site_with_pagination_for_taxonomy() {
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/tags/a/page/6/</loc>"
"<loc>https%3A//replace-this-with-your-url.com/tags/a/page/6/</loc>"
))
}

@@ -641,7 +642,7 @@ fn can_apply_page_templates() {
assert_eq!(child.meta.title, Some("Local section override".into()));
}

// https://github.com/getzola/zola/issues/571
// https%3A//github.com/getzola/zola/issues/571
#[test]
fn can_build_site_custom_builtins_from_theme() {
let (_, _tmp_dir, public) = build_site("test_site");


+ 7
- 7
components/site/tests/site_i18n.rs View File

@@ -112,17 +112,17 @@ fn can_build_multilingual_site() {

// sitemap contains all languages
assert!(file_exists!(public, "sitemap.xml"));
assert!(file_contains!(public, "sitemap.xml", "https://example.com/blog/something-else/"));
assert!(file_contains!(public, "sitemap.xml", "https://example.com/fr/blog/something-else/"));
assert!(file_contains!(public, "sitemap.xml", "https://example.com/it/blog/something-else/"));
assert!(file_contains!(public, "sitemap.xml", "https%3A//example.com/blog/something-else/"));
assert!(file_contains!(public, "sitemap.xml", "https%3A//example.com/fr/blog/something-else/"));
assert!(file_contains!(public, "sitemap.xml", "https%3A//example.com/it/blog/something-else/"));

// one rss per language
assert!(file_exists!(public, "rss.xml"));
assert!(file_contains!(public, "rss.xml", "https://example.com/blog/something-else/"));
assert!(!file_contains!(public, "rss.xml", "https://example.com/fr/blog/something-else/"));
assert!(file_contains!(public, "rss.xml", "https%3A//example.com/blog/something-else/"));
assert!(!file_contains!(public, "rss.xml", "https%3A//example.com/fr/blog/something-else/"));
assert!(file_exists!(public, "fr/rss.xml"));
assert!(!file_contains!(public, "fr/rss.xml", "https://example.com/blog/something-else/"));
assert!(file_contains!(public, "fr/rss.xml", "https://example.com/fr/blog/something-else/"));
assert!(!file_contains!(public, "fr/rss.xml", "https%3A//example.com/blog/something-else/"));
assert!(file_contains!(public, "fr/rss.xml", "https%3A//example.com/fr/blog/something-else/"));
// Italian doesn't have RSS enabled
assert!(!file_exists!(public, "it/rss.xml"));



+ 4
- 3
components/templates/Cargo.toml View File

@@ -4,12 +4,13 @@ version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]

[dependencies]
tera = "1.0.0-alpha.3"
tera = "1.0.0-beta.10"
base64 = "0.10"
lazy_static = "1"
pulldown-cmark = "0.2"
toml = "0.4"
pulldown-cmark = "0.5"
toml = "0.5"
csv = "1"
image = "0.21"
serde_json = "1.0"
reqwest = "0.9"
url = "1.5"


+ 5
- 4
components/templates/src/builtins/rss.xml View File

@@ -1,18 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>{{ config.title }}</title>
<link>{{ config.base_url | safe }}</link>
<link>{{ config.base_url | urlencode | safe }}</link>
<description>{{ config.description }}</description>
<generator>Zola</generator>
<language>{{ config.default_language }}</language>
<atom:link href="{{ feed_url | safe }}" rel="self" type="application/rss+xml"/>
<atom:link href="{{ feed_url | safe | urlencode | safe }}" rel="self" type="application/rss+xml"/>
<lastBuildDate>{{ last_build_date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
{% for page in pages %}
<item>
<title>{{ page.title }}</title>
<pubDate>{{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
<link>{{ page.permalink | safe }}</link>
<guid>{{ page.permalink | safe }}</guid>
<link>{{ page.permalink | urlencode | safe }}</link>
<guid>{{ page.permalink | urlencode | safe }}</guid>
<description>{% if page.summary %}{{ page.summary }}{% else %}{{ page.content }}{% endif %}</description>
</item>
{% endfor %}


+ 2
- 1
components/templates/src/builtins/sitemap.xml View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
{% for sitemap_entry in entries %}
<url>
<loc>{{ sitemap_entry.permalink | safe }}</loc>
<loc>{{ sitemap_entry.permalink | urlencode | safe }}</loc>
{% if sitemap_entry.date %}
<lastmod>{{ sitemap_entry.date }}</lastmod>
{% endif %}


+ 1
- 0
components/templates/src/builtins/split_sitemap_index.xml View File

@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="https://www.sitemaps.org/schemas/sitemap/0.9/siteindex.xsd">
{% for sitemap in sitemaps %}
<sitemap>


+ 3
- 12
components/templates/src/global_fns/load_data.rs View File

@@ -445,10 +445,7 @@ mod tests {
args.insert("path".to_string(), to_value("test.css").unwrap());
let result = static_fn.call(&args.clone()).unwrap();

assert_eq!(
result,
".hello {}\n",
);
assert_eq!(result, ".hello {}\n",);
}

#[test]
@@ -459,10 +456,7 @@ mod tests {
args.insert("format".to_string(), to_value("plain").unwrap());
let result = static_fn.call(&args.clone()).unwrap();

assert_eq!(
result,
"Number,Title\n1,Gutenberg\n2,Printing",
);
assert_eq!(result, "Number,Title\n1,Gutenberg\n2,Printing",);
}

#[test]
@@ -473,10 +467,7 @@ mod tests {
args.insert("format".to_string(), to_value("plain").unwrap());
let result = static_fn.call(&args.clone()).unwrap();

assert_eq!(
result,
".hello {}\n",
);
assert_eq!(result, ".hello {}\n",);
}

#[test]


+ 36
- 3
components/templates/src/global_fns/mod.rs View File

@@ -2,9 +2,11 @@ use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock};

use tera::{from_value, to_value, Function as TeraFn, Result, Value};
use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value};

use config::Config;
use image;
use image::GenericImageView;
use library::{Library, Taxonomy};
use utils::site::resolve_internal_link;

@@ -60,9 +62,9 @@ impl TeraFn for GetUrl {
args.get("path"),
"`get_url` requires a `path` argument with a string value"
);
if path.starts_with("./") {
if path.starts_with("@/") {
match resolve_internal_link(&path, &self.permalinks) {
Ok(url) => Ok(to_value(url).unwrap()),
Ok(resolved) => Ok(to_value(resolved.permalink).unwrap()),
Err(_) => {
Err(format!("Could not resolve URL for link `{}` not found.", path).into())
}
@@ -140,6 +142,37 @@ impl TeraFn for ResizeImage {
}
}

#[derive(Debug)]
pub struct GetImageMeta {
content_path: PathBuf,
}

impl GetImageMeta {
pub fn new(content_path: PathBuf) -> Self {
Self { content_path }
}
}

impl TeraFn for GetImageMeta {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
let path = required_arg!(
String,
args.get("path"),
"`get_image_meta` requires a `path` argument with a string value"
);
let src_path = self.content_path.join(&path);
if !src_path.exists() {
return Err(format!("`get_image_meta`: Cannot find path: {}", path).into());
}
let img = image::open(&src_path)
.map_err(|e| Error::chain(format!("Failed to process image: {}", path), e))?;
let mut map = tera::Map::new();
map.insert(String::from("height"), Value::Number(tera::Number::from(img.height())));
map.insert(String::from("width"), Value::Number(tera::Number::from(img.width())));
Ok(Value::Object(map))
}
}

#[derive(Debug)]
pub struct GetTaxonomyUrl {
taxonomies: HashMap<String, HashMap<String, String>>,


+ 1
- 1
components/templates/src/lib.rs View File

@@ -4,10 +4,10 @@ extern crate lazy_static;
extern crate tera;
extern crate base64;
extern crate csv;
extern crate image;
extern crate pulldown_cmark;
extern crate reqwest;
extern crate url;

#[cfg(test)]
#[macro_use]
extern crate serde_json;


+ 1
- 1
components/utils/Cargo.toml View File

@@ -5,7 +5,7 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]

[dependencies]
errors = { path = "../errors" }
tera = "1.0.0-alpha.3"
tera = "1.0.0-beta.10"
unicode-segmentation = "1.2"
walkdir = "2"
toml = "0.4"


+ 2
- 2
components/utils/src/fs.rs View File

@@ -77,7 +77,7 @@ pub fn read_file_with_error(path: &Path, message: &str) -> Result<String> {
pub fn find_related_assets(path: &Path) -> Vec<PathBuf> {
let mut assets = vec![];

for entry in read_dir(path).unwrap().filter_map(|e| e.ok()) {
for entry in read_dir(path).unwrap().filter_map(std::result::Result::ok) {
let entry_path = entry.path();
if entry_path.is_file() {
match entry_path.extension() {
@@ -108,7 +108,7 @@ pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf) -> Result<()>
}

pub fn copy_directory(src: &PathBuf, dest: &PathBuf) -> Result<()> {
for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) {
for entry in WalkDir::new(src).into_iter().filter_map(std::result::Result::ok) {
let relative_path = entry.path().strip_prefix(src).unwrap();
let target_path = dest.join(relative_path);



+ 1
- 4
components/utils/src/net.rs View File

@@ -5,8 +5,5 @@ pub fn get_available_port(avoid: u16) -> Option<u16> {
}

pub fn port_is_available(port: u16) -> bool {
match TcpListener::bind(("127.0.0.1", port)) {
Ok(_) => true,
Err(_) => false,
}
TcpListener::bind(("127.0.0.1", port)).is_ok()
}

+ 40
- 14
components/utils/src/site.rs View File

@@ -9,22 +9,39 @@ pub fn get_reading_analytics(content: &str) -> (usize, usize) {

// https://help.medium.com/hc/en-us/articles/214991667-Read-time
// 275 seems a bit too high though
(word_count, (word_count / 200))
(word_count, ((word_count + 199) / 200))
}

/// 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> {
#[derive(Debug, PartialEq, Clone)]
pub struct ResolvedInternalLink {
pub permalink: String,
// The 2 fields below are only set when there is an anchor
// as we will need that to check if it exists after the markdown rendering is done
pub md_path: Option<String>,
pub anchor: Option<String>,
}

/// Resolves an internal link (of the `@/posts/something.md#hey` sort) to its absolute link and
/// returns the path + anchor as well
pub fn resolve_internal_link(
link: &str,
permalinks: &HashMap<String, String>,
) -> Result<ResolvedInternalLink> {
// First we remove the ./ since that's zola specific
let clean_link = link.replacen("./", "", 1);
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]))
Ok(ResolvedInternalLink {
permalink: format!("{}#{}", p, parts[1]),
md_path: Some(parts[0].to_string()),
anchor: Some(parts[1].to_string()),
})
} else {
Ok(p.to_string())
Ok(ResolvedInternalLink { permalink: p.to_string(), md_path: None, anchor: None })
}
}
None => bail!(format!("Relative link {} not found.", link)),
@@ -41,37 +58,46 @@ mod tests {
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");
let res = resolve_internal_link("@/pages/about.md", &permalinks).unwrap();
assert_eq!(res.permalink, "https://vincent.is/about");
}

#[test]
fn can_resolve_valid_root_internal_link() {
let mut permalinks = HashMap::new();
permalinks.insert("about.md".to_string(), "https://vincent.is/about".to_string());
let res = resolve_internal_link("./about.md", &permalinks).unwrap();
assert_eq!(res, "https://vincent.is/about");
let res = resolve_internal_link("@/about.md", &permalinks).unwrap();
assert_eq!(res.permalink, "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");
let res = resolve_internal_link("@/pages/about.md#hello", &permalinks).unwrap();
assert_eq!(res.permalink, "https://vincent.is/about#hello");
assert_eq!(res.md_path, Some("pages/about.md".to_string()));
assert_eq!(res.anchor, Some("hello".to_string()));
}

#[test]
fn errors_resolve_inexistant_internal_link() {
let res = resolve_internal_link("./pages/about.md#hello", &HashMap::new());
let res = resolve_internal_link("@/pages/about.md#hello", &HashMap::new());
assert!(res.is_err());
}

#[test]
fn reading_analytics_empty_text() {
let (word_count, reading_time) = get_reading_analytics(" ");
assert_eq!(word_count, 0);
assert_eq!(reading_time, 0);
}

#[test]
fn reading_analytics_short_text() {
let (word_count, reading_time) = get_reading_analytics("Hello World");
assert_eq!(word_count, 2);
assert_eq!(reading_time, 0);
assert_eq!(reading_time, 1);
}

#[test]


+ 4
- 4
components/utils/src/templates.rs View File

@@ -11,7 +11,7 @@ macro_rules! render_default_tpl {
let mut context = Context::new();
context.insert("filename", $filename);
context.insert("url", $url);
Tera::one_off(DEFAULT_TPL, context, true).map_err(|e| e.into())
Tera::one_off(DEFAULT_TPL, context, true).map_err(std::convert::Into::into)
}};
}

@@ -27,21 +27,21 @@ pub fn render_template(
) -> Result<String> {
// check if it is in the templates
if tera.templates.contains_key(name) {
return tera.render(name, context).map_err(|e| e.into());
return tera.render(name, context).map_err(std::convert::Into::into);
}

// check if it is part of a theme
if let Some(ref t) = *theme {
let theme_template_name = format!("{}/templates/{}", t, name);
if tera.templates.contains_key(&theme_template_name) {
return tera.render(&theme_template_name, context).map_err(|e| e.into());
return tera.render(&theme_template_name, context).map_err(std::convert::Into::into);
}
}

// check if it is part of ZOLA_TERA defaults
let default_name = format!("__zola_builtins/{}", name);
if tera.templates.contains_key(&default_name) {
return tera.render(&default_name, context).map_err(|e| e.into());
return tera.render(&default_name, context).map_err(std::convert::Into::into);
}

// maybe it's a default one?


+ 11
- 5
docs/content/documentation/content/image-processing/index.md View File

@@ -14,7 +14,7 @@ resize_image(path, width, height, op, quality)

### Arguments

- `path`: The path to the source image relative to the `content` directory in the [directory structure](./documentation/getting-started/directory-structure.md).
- `path`: The path to the source image relative to the `content` directory in the [directory structure](@/documentation/getting-started/directory-structure.md).
- `width` and `height`: The dimensions in pixels of the resized image. Usage depends on the `op` argument.
- `op` (_optional_): Resize operation. This can be one of:
- `"scale"`
@@ -97,8 +97,8 @@ The source for all examples is this 300 Ă— 380 pixels image:

## Using `resize_image` in markdown via shortcodes

`resize_image` is a built-in Tera global function (see the [Templates](./documentation/templates/_index.md) chapter),
but it can be used in markdown, too, using [Shortcodes](./documentation/content/shortcodes.md).
`resize_image` is a built-in Tera global function (see the [Templates](@/documentation/templates/_index.md) chapter),
but it can be used in markdown, too, using [Shortcodes](@/documentation/content/shortcodes.md).

The examples above were generated using a shortcode file named `resize_image.html` with this content:

@@ -110,9 +110,9 @@ The examples above were generated using a shortcode file named `resize_image.htm

The `resize_image()` can be used multiple times and/or in loops. It is designed to handle this efficiently.

This can be used along with `assets` [page metadata](./documentation/templates/pages-sections.md) to create picture galleries.
This can be used along with `assets` [page metadata](@/documentation/templates/pages-sections.md) to create picture galleries.
The `assets` variable holds paths to all assets in the directory of a page with resources
(see [assets colocation](./documentation/content/overview.md#assets-colocation)): if you have files other than images you
(see [assets colocation](@/documentation/content/overview.md#assets-colocation)): if you have files other than images you
will need to filter them out in the loop first like in the example below.

This can be used in shortcodes. For example, we can create a very simple html-only clickable
@@ -145,3 +145,9 @@ Here is the result:
<small>
Image attribution: Public domain, except: _06-example.jpg_: Willi Heidelbach, _07-example.jpg_: Daniel Ullrich.
</small>


## Get image size

Sometimes when building a gallery it is useful to know the dimensions of each asset. You can get this information with
[get_image_metadata](./documentation/templates/overview.md#get-image-metadata)

+ 17
- 9
docs/content/documentation/content/linking.md View File

@@ -3,9 +3,9 @@ title = "Internal links & deep linking"
weight = 50
+++

## Header id and anchor insertion
While rendering the markdown content, a unique id will automatically be assigned to each header. This id is created
by converting the header text to a [slug](https://en.wikipedia.org/wiki/Semantic_URL#Slug), appending numbers at the end
## Heading id and anchor insertion
While rendering the markdown content, a unique id will automatically be assigned to each heading. This id is created
by converting the heading text to a [slug](https://en.wikipedia.org/wiki/Semantic_URL#Slug), appending numbers at the end
if the slug already exists for that article. For example:

```md
@@ -16,21 +16,29 @@ if the slug already exists for that article. For example:
## Example code <- example-code-1
```

You can also manually specify an id with a `{#…}` suffix on the heading line:

```md
# Something manual! {#manual}
```

This is useful for making deep links robust, either proactively (so that you can later change the text of a heading without breaking links to it) or retroactively (keeping the slug of the old header text, when changing the text). It can also be useful for migration of existing sites with different header id schemes, so that you can keep deep links working.

## Anchor insertion
It is possible to have Zola automatically insert anchor links next to the header, as you can see on the site you are currently
It is possible to have Zola automatically insert anchor links next to the heading, as you can see on the site you are currently
reading if you hover a title.

This option is set at the section level: the `insert_anchor_links` variable on the
[Section front-matter page](./documentation/content/section.md#front-matter).
[Section front-matter page](@/documentation/content/section.md#front-matter).

The default template is very basic and will need CSS tweaks in your project to look decent.
If you want to change the anchor template, it can easily be overwritten by
creating a `anchor-link.html` file in the `templates` directory.

## Internal links
Linking to other pages and their headers is so common that Zola adds a
special syntax to Markdown links to handle them: start the link with `./` and point to the `.md` file you want
Linking to other pages and their headings is so common that Zola adds a
special syntax to Markdown links to handle them: start the link with `@/` and point to the `.md` file you want
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)`.
You can still link to a header directly: `[my link](./pages/about.md#example)` will work as expected.
For example, linking to a file located at `content/pages/about.md` would be `[my link](@/pages/about.md)`.
You can still link to an anchor directly: `[my link](@/pages/about.md#example)` will work as expected.

+ 4
- 4
docs/content/documentation/content/overview.md View File

@@ -5,8 +5,8 @@ weight = 10


Zola uses the folder structure to determine the site structure.
Each folder in the `content` directory represents a [section](./documentation/content/section.md)
that contains [pages](./documentation/content/page.md): your `.md` files.
Each folder in the `content` directory represents a [section](@/documentation/content/section.md)
that contains [pages](@/documentation/content/page.md): your `.md` files.

```bash
.
@@ -24,7 +24,7 @@ that contains [pages](./documentation/content/page.md): your `.md` files.
```

Each page path (the part after the `base_url`, for example `blog/cli-usage/`) can be customised by changing the `path` or `slug`
attribute of the [page front-matter](./documentation/content/page.md#front-matter).
attribute of the [page front-matter](@/documentation/content/page.md#front-matter).

You might have noticed a file named `_index.md` in the example above.
This file is used to store both metadata and content of the section itself and is not considered a page.
@@ -70,7 +70,7 @@ By default, this page will get the folder name as its slug. So its permalink wou
### Excluding files from assets

It is possible to ignore selected asset files using the
[ignored_content](./documentation/getting-started/configuration.md) setting in the config file.
[ignored_content](@/documentation/getting-started/configuration.md) setting in the config file.
For example, say you have an Excel spreadsheet from which you are taking several screenshots and
then linking to those image files on your website. For maintainability purposes, you want to keep
the spreadsheet in the same folder as the markdown, but you don't want to copy the spreadsheet to


+ 2
- 2
docs/content/documentation/content/page.md View File

@@ -25,7 +25,7 @@ character in a filename on Windows.
As you can see, creating an `about.md` file is exactly equivalent to creating an
`about/index.md` file. The only difference between the two methods is that creating
the `about` folder allows you to use asset colocation, as discussed in the
[Overview](./documentation/content/overview.md#assets-colocation) section of this documentation.
[Overview](@/documentation/content/overview.md#assets-colocation) section of this documentation.

## Front-matter

@@ -100,7 +100,7 @@ paragraph of each page in a list for example.
To do so, add <code>&lt;!-- more --&gt;</code> in your content at the point
where you want the summary to end and the content up to that point will be also
available separately in the
[template](./documentation/templates/pages-sections.md#page-variables).
[template](@/documentation/templates/pages-sections.md#page-variables).

An anchor link to this position named `continue-reading` is created, wrapped in a paragraph
with a `zola-continue-reading` id, so you can link directly to it if needed for example:


+ 6
- 2
docs/content/documentation/content/section.md View File

@@ -14,7 +14,7 @@ not have any content or metadata. If you would like to add content or metadata,
`_index.md` file at the root of the `content` folder and edit it just as you would edit any other
`_index.md` file; your `index.html` template will then have access to that content and metadata.

Any non-Markdown file in the section folder is added to the `assets` collection of the section, as explained in the [Content Overview](./documentation/content/overview.md#assets-colocation). These files are then available from the Markdown using relative links.
Any non-Markdown file in the section folder is added to the `assets` collection of the section, as explained in the [Content Overview](@/documentation/content/overview.md#assets-colocation). These files are then available from the Markdown using relative links.

## Front-matter

@@ -89,6 +89,10 @@ redirect_to = ""
# sections for each year under a posts section.
transparent = false

# Use aliases if you are moving content but want to redirect previous URLs to the
# current one. This takes an array of path, not URLs.
aliases = []

# Your own data
[extra]
+++
@@ -101,7 +105,7 @@ Keep in mind that any configuration apply only to the direct pages, not to the s
## Pagination

To enable pagination for a section's pages, simply set `paginate_by` to a positive number and it will automatically
paginate by this much. See [pagination template documentation](./documentation/templates/pagination.md) for more information
paginate by this much. See [pagination template documentation](@/documentation/templates/pagination.md) for more information
on what will be available in the template.

You can also change the pagination path (the word displayed while paginated in the URL, like `page/1`)


+ 2
- 1
docs/content/documentation/content/syntax-highlighting.md View File

@@ -4,7 +4,7 @@ weight = 80
+++

Zola comes with built-in syntax highlighting but you first
need to enable it in the [configuration](./documentation/getting-started/configuration.md).
need to enable it in the [configuration](@/documentation/getting-started/configuration.md).

Once this is done, Zola will automatically highlight all code blocks
in your content. A code block in Markdown looks like the following:
@@ -120,6 +120,7 @@ Here is a full list of the supported languages and the short names you can use:
- TypeScript -> ["ts"]
- TypeScriptReact -> ["tsx"]
- VimL -> ["vim"]
- Nix -> ["nix"]
- TOML -> ["toml", "tml", "Cargo.lock", "Gopkg.lock", "Pipfile"]
```



+ 1
- 1
docs/content/documentation/content/table-of-contents.md View File

@@ -6,7 +6,7 @@ weight = 60
Each page/section will automatically generate a table of contents for itself based on the headers present.

It is available in the template through the `toc` variable.
You can view the [template variables](./documentation/templates/pages-sections.md#table-of-contents)
You can view the [template variables](@/documentation/templates/pages-sections.md#table-of-contents)
documentation for information on its structure.

Here is an example of using that field to render a 2-level table of contents:


+ 1
- 1
docs/content/documentation/content/taxonomies.md View File

@@ -5,7 +5,7 @@ weight = 90

Zola has built-in support for taxonomies.

The first step is to define the taxonomies in your [config.toml](./documentation/getting-started/configuration.md).
The first step is to define the taxonomies in your [config.toml](@/documentation/getting-started/configuration.md).

A taxonomy has 5 variables:



+ 1
- 1
docs/content/documentation/deployment/github-pages.md View File

@@ -51,7 +51,7 @@ language: minimal
before_script:
# Download and unzip the zola executable
# Replace the version numbers in the URL by the version you want to use
- curl -s -L https://github.com/getzola/zola/releases/download/v0.7.0/zola-v0.7.0-x86_64-unknown-linux-gnu.tar.gz | sudo tar xvzf - -C /usr/local/bin
- curl -s -L https://github.com/getzola/zola/releases/download/v0.8.0/zola-v0.8.0-x86_64-unknown-linux-gnu.tar.gz | sudo tar xvzf - -C /usr/local/bin

script:
- zola build


+ 1
- 1
docs/content/documentation/deployment/gitlab-pages.md View File

@@ -41,7 +41,7 @@ variables:
# This variable will ensure that the CI runner pulls in your theme from the submodule
GIT_SUBMODULE_STRATEGY: recursive
# Specify the zola version you want to use here
ZOLA_VERSION: "v0.7.0"
ZOLA_VERSION: "v0.8.0"

pages:
script:


+ 7
- 18
docs/content/documentation/deployment/netlify.md View File

@@ -8,20 +8,16 @@ with no effort. This very site is hosted by Netlify and automatically deployed o

If you don't have an account with Netlify, you can [sign up](https://app.netlify.com) for one.

Netlify has built-in support for the older version (named Gutenberg) but not for Zola yet.

## Automatic Deploys (*Gutenberg only*)

> Automatic deploys based on environment variable are not available for Zola yet,
> only for version <= 0.4.2 when it was called Gutenberg
## Automatic Deploys

Once you are in the admin interface, you can add a site from a Git provider (GitHub, GitLab or Bitbucket). At the end
of this process, you can select the deploy settings for the project:

- build command: `GUTENBERG_VERSION=0.4.2 gutenberg build` (replace the version number in the variable by the version you want to use)
- build command: `ZOLA_VERSION=0.8.0 zola build` (replace the version number in the variable by the version you want to use)
- publish directory: the path to where the `public` directory is

With this setup, your site should be automatically deployed on every commit on master. For `GUTENBERG_VERSION`, you may
With this setup, your site should be automatically deployed on every commit on master. For `ZOLA_VERSION`, you may
use any of the tagged `release` versions in the GitHub repository — Netlify will automatically fetch the tagged version
and use it to build your site.

@@ -32,15 +28,15 @@ the admin interface.

```toml
[build]
# assuming the Gutenberg site is in a docs folder, if it isn't you don't need
# assuming the Zola site is in a docs folder, if it isn't you don't need
# to have a `base` variable but you do need the `publish` and `command`
base = "docs"
publish = "docs/public"
command = "gutenberg build"
command = "zola build"

[build.environment]
# Set the version name that you want to use and Netlify will automatically use it
GUTENBERG_VERSION = "0.5.0"
ZOLA_VERSION = "0.8.0"

# The magic for deploying previews of branches
# We need to override the base url with whatever url Netlify assigns to our
@@ -48,14 +44,7 @@ GUTENBERG_VERSION = "0.5.0"
# `$DEPLOY_PRIME_URL`.

[context.deploy-preview]
command = "gutenberg build --base-url $DEPLOY_PRIME_URL"
```

## Automatic deploys for Zola
Since Netlify doesn't support Zola currently, you will need to download the archive directly from GitHub, replacing the version in the URL with the one you want:

```
command = "curl -sL https://github.com/getzola/zola/releases/download/v0.7.0/zola-v0.7.0-x86_64-unknown-linux-gnu.tar.gz | tar zxv && ./zola build"
command = "zola build --base-url $DEPLOY_PRIME_URL"
```

## Manual Deploys


+ 5
- 0
docs/content/documentation/getting-started/cli-usage.md View File

@@ -83,6 +83,11 @@ You can also point to another config file than `config.toml` like so - the posit
$ zola --config config.staging.toml serve
```

### check

The check subcommand will try to build all pages just like the build command would, but without writing any of the
results to disk. Additionally, it always checks external links regardless of the site configuration.

## Colored output

Any of the three commands will emit colored output if your terminal supports it.


+ 0
- 6
docs/content/documentation/getting-started/configuration.md View File

@@ -67,12 +67,6 @@ compile_sass = false
# content for the `default_language`
build_search_index = false

# Go through every links in all content and check if the links are valid
# If a link is invalid (404, 500, etc), the build will error.
# Link checking can take a very long time if you have many links so this should
# only enabled once in a while to catch any dead links.
check_external_links = false

# A list of glob patterns specifying asset files to ignore when
# processing the content directory.
# Defaults to none, which means all asset files are copied over to the public folder.


+ 6
- 6
docs/content/documentation/getting-started/directory-structure.md View File

@@ -22,14 +22,14 @@ Here's a high level overview of each of these folders and `config.toml`.

## `config.toml`
A mandatory configuration file of Zola in TOML format.
It is explained in details in the [Configuration page](./documentation/getting-started/configuration.md).
It is explained in details in the [Configuration page](@/documentation/getting-started/configuration.md).

## `content`
Where all your markup content lies: this will be mostly comprised of `.md` files.
Each folder in the `content` directory represents a [section](./documentation/content/section.md)
that contains [pages](./documentation/content/page.md) : your `.md` files.
Each folder in the `content` directory represents a [section](@/documentation/content/section.md)
that contains [pages](@/documentation/content/page.md) : your `.md` files.

To learn more, read [the content overview](./documentation/content/overview.md).
To learn more, read [the content overview](@/documentation/content/overview.md).

## `sass`
Contains the [Sass](http://sass-lang.com) files to be compiled. Non-Sass files will be ignored.
@@ -41,9 +41,9 @@ Contains any kind of files. All the files/folders in the `static` folder will be

## `templates`
Contains all the [Tera](https://tera.netlify.com) templates that will be used to render this site.
Have a look at the [Templates](./documentation/templates/_index.md) to learn more about default templates
Have a look at the [Templates](@/documentation/templates/_index.md) to learn more about default templates
and available variables.

## `themes`
Contains themes that can be used for that site. If you are not planning to use themes, leave this folder empty.
If you want to learn about themes, head to the [themes documentation](./documentation/themes/_index.md).
If you want to learn about themes, head to the [themes documentation](@/documentation/themes/_index.md).

+ 15
- 5
docs/content/documentation/templates/overview.md View File

@@ -15,11 +15,13 @@ to print the whole context.

A few variables are available on all templates minus RSS and sitemap:

- `config`: the [configuration](./documentation/getting-started/configuration.md) without any modifications
- `config`: the [configuration](@/documentation/getting-started/configuration.md) without any modifications
- `current_path`: the path (full URL without the `base_url`) of the current page, never starting with a `/`
- `current_url`: the full URL for that page
- `lang`: the language for that page, `null` if the page/section doesn't have a language set

The 404 template does not get `current_path` and `current_url` as it cannot know it.

## Standard Templates
By default, Zola will look for three templates: `index.html`, which is applied
to the site homepage; `section.html`, which is applied to all sections (any HTML
@@ -103,11 +105,11 @@ If you only need the metadata of the section, you can pass `metadata_only=true`

### ` get_url`
Gets the permalink for the given path.
If the path starts with `./`, it will be understood as an internal
link like the ones used in markdown.
If the path starts with `@/`, it will be understood as an internal
link like the ones used in markdown, starting from the root `content` directory.

```jinja2
{% set url = get_url(path="./blog/_index.md") %}
{% set url = get_url(path="@/blog/_index.md") %}
```

This can also be used to get the permalinks for static assets for example if
@@ -128,6 +130,14 @@ In the case of non-internal links, you can also add a cachebust of the format `?
by passing `cachebust=true` to the `get_url` function.


### `get_image_metadata`
Gets metadata for an image. Today the only supported keys are `width` and `height`.

```jinja2
{% set meta = get_image_metadata(path="...") %}
Our image is {{ meta.width }}x{{ meta.height }}
```

### `get_taxonomy_url`
Gets the permalink for the taxonomy item found.

@@ -228,4 +238,4 @@ Gets the translation of the given `key`, for the `default_language` or the `lang

### `resize_image`
Resizes an image file.
Pease refer to [_Content / Image Processing_](./documentation/content/image-processing/index.md) for complete documentation.
Pease refer to [_Content / Image Processing_](@/documentation/content/image-processing/index.md) for complete documentation.

+ 1
- 1
docs/content/documentation/templates/pagination.md View File

@@ -45,4 +45,4 @@ A paginated taxonomy gets two variables aside from the `paginator` variable:
- a `taxonomy` variable of type `TaxonomyConfig`
- a `term` variable of type `TaxonomyTerm`.

See the [taxonomies page](./documentation/templates/taxonomies.md) for a detailed version of the types.
See the [taxonomies page](@/documentation/templates/taxonomies.md) for a detailed version of the types.

+ 1
- 1
docs/content/documentation/templates/rss.md View File

@@ -13,5 +13,5 @@ directory or, if one does not exist, will use the use the built-in rss template.
The RSS template gets two variables in addition of the config:

- `last_build_date`: the date of the latest post
- `pages`: see [the page variables](./documentation/templates/pages-sections.md#page-variables) for
- `pages`: see [the page variables](@/documentation/templates/pages-sections.md#page-variables) for
a detailed description of what this contains

+ 1
- 1
docs/content/documentation/templates/taxonomies.md View File

@@ -60,5 +60,5 @@ current_path: String;
term: TaxonomyTerm;
```

A paginated taxonomy term will also get a `paginator` variable, see the [pagination page](./documentation/templates/pagination.md)
A paginated taxonomy term will also get a `paginator` variable, see the [pagination page](@/documentation/templates/pagination.md)
for more details on that.

+ 1
- 1
docs/content/documentation/themes/creating-a-theme.md View File

@@ -58,7 +58,7 @@ Theme templates should use [macros](https://tera.netlify.com/docs/templates/#mac

## Submitting a theme to the gallery

If you want your theme to be featured in the [themes](./themes/_index.md) section
If you want your theme to be featured in the [themes](@/themes/_index.md) section
of this site, the theme will require two more things:

- `screenshot.png`: a screenshot of the theme in action with a max size of around 2000x1000


+ 3
- 3
docs/content/documentation/themes/installing-and-using-themes.md View File

@@ -18,13 +18,13 @@ Cloning the repository using Git or another VCS will allow you to easily
update it but you can also simply download the files manually and paste
them in a folder.

You can find a list of themes [on this very website](./themes/_index.md).
You can find a list of themes [on this very website](@/themes/_index.md).

## Using a theme

Now that you have the theme in your `themes` directory, you only need to tell
Zola to use it to get started by setting the `theme` variable of the
[configuration file](./documentation/getting-started/configuration.md). The theme
[configuration file](@/documentation/getting-started/configuration.md). The theme
name has to be name of the directory you cloned the theme in.
For example, if you cloned a theme in `themes/simple-blog`, the theme name to use
in the configuration file is `simple-blog`.
@@ -52,7 +52,7 @@ Some custom data
```

Most themes will also provide some variables that are meant to be overriden: this happens in the `extra` section
of the [configuration file](./documentation/getting-started/configuration.md).
of the [configuration file](@/documentation/getting-started/configuration.md).
Let's say a theme uses a `show_twitter` variable and sets it to `false` by default. If you want to set it to `true`,
you can update your `config.toml` like so:



+ 1
- 1
docs/content/documentation/themes/overview.md View File

@@ -8,5 +8,5 @@ but still easy to update if needed.

All themes can use the full power of Zola, from shortcodes to Sass compilation.

A list of themes is available [on this very website](./themes/_index.md).
A list of themes is available [on this very website](@/themes/_index.md).


+ 6
- 6
docs/templates/index.html View File

@@ -15,8 +15,8 @@
<header>
<nav class="{% block extra_nav_class %}container{% endblock extra_nav_class %}">
<a class="header__logo white" href="{{ config.base_url }}">Zola</a>
<a class="white" href="{{ get_url(path="./documentation/_index.md") }}" class="nav-link">Docs</a>
<a class="white" href="{{ get_url(path="./themes/_index.md") }}" class="nav-link">Themes</a>
<a class="white" href="{{ get_url(path="@/documentation/_index.md") }}" class="nav-link">Docs</a>
<a class="white" href="{{ get_url(path="@/themes/_index.md") }}" class="nav-link">Themes</a>
<a class="white" href="https://zola.discourse.group/" class="nav-link">Forum</a>
<a class="white" href="https://github.com/getzola/zola" class="nav-link">GitHub</a>

@@ -37,7 +37,7 @@
<p class="hero__tagline">
Forget dependencies. Everything you need in one binary.
</p>
<a href="{{ get_url(path="./documentation/_index.md") }}" class="button">Get started</a>
<a href="{{ get_url(path="@/documentation/_index.md") }}" class="button">Get started</a>
</div>

<div class="inverted-colours selling-points">
@@ -73,7 +73,7 @@
<p>
From the CLI to the template engine, everything is designed to be intuitive.
Don't take my words for it though, look at the
<a href="{{ get_url(path="./documentation/_index.md") }}">documentation</a> and see for yourself.
<a href="{{ get_url(path="@/documentation/_index.md") }}">documentation</a> and see for yourself.
</p>
</div>

@@ -88,8 +88,8 @@
<div class="selling-point">
<h2>Augmented Markdown</h2>
<p>
Zola comes with <a href="{{ get_url(path="./documentation/content/shortcodes.md") }}">shortcodes</a> and
<a href="{{ get_url(path="./documentation/content/linking.md") }}">internal links</a>
Zola comes with <a href="{{ get_url(path="@/documentation/content/shortcodes.md") }}">shortcodes</a> and
<a href="{{ get_url(path="@/documentation/content/linking.md") }}">internal links</a>
to make it easier to write your content.
</p>
</div>


+ 3
- 3
netlify.toml View File

@@ -1,10 +1,10 @@
[build]
base = "docs"
publish = "docs/public"
command = "curl -sL https://github.com/getzola/zola/releases/download/v0.5.0/zola-v0.5.0-x86_64-unknown-linux-gnu.tar.gz | tar zxv && ./zola build"
command = "zola build"

[build.environment]
ZOLA_VERSION = "0.5.0"
ZOLA_VERSION = "0.7.0"

[context.deploy-preview]
command = "curl -sL https://github.com/getzola/zola/releases/download/v0.5.0/zola-v0.5.0-x86_64-unknown-linux-gnu.tar.gz | tar zxv && ./zola build --base-url $DEPLOY_PRIME_URL"
command = "zola build --base-url $DEPLOY_PRIME_URL"

+ 2
- 2
snapcraft.yaml View File

@@ -1,5 +1,5 @@
name: zola
version: 0.7.0
version: 0.8.0
summary: A fast static site generator in a single binary with everything built-in.
description: |
A fast static site generator in a single binary with everything built-in.
@@ -21,7 +21,7 @@ parts:
zola:
source-type: git
source: https://github.com/getzola/zola.git
source-tag: v0.7.0
source-tag: v0.8.0
plugin: rust
rust-channel: stable
build-packages:


+ 2
- 0
src/cli.rs View File

@@ -67,5 +67,7 @@ pub fn build_cli() -> App<'static, 'static> {
.takes_value(false)
.help("Do not start a server, just re-build project on changes")
]),
SubCommand::with_name("check")
.about("Try building the project without rendering it. Checks links")
])
}

+ 24
- 0
src/cmd/check.rs View File

@@ -0,0 +1,24 @@
use std::env;
use std::path::PathBuf;

use errors::Result;
use site::Site;

use console;

pub fn check(config_file: &str, base_path: Option<&str>, base_url: Option<&str>) -> Result<()> {
let bp = base_path.map(PathBuf::from).unwrap_or(env::current_dir().unwrap());
let mut site = Site::new(bp, config_file)?;
// Force the checking of external links
site.config.check_external_links = true;
// Disable syntax highlighting since the results won't be used
// and this operation can be expensive.
site.config.highlight_code = false;
if let Some(b) = base_url {
site.set_base_url(b.to_string());
}
site.load()?;
console::notify_site_size(&site);
console::warn_about_ignored_pages(&site);
Ok(())
}

+ 2
- 0
src/cmd/mod.rs View File

@@ -1,7 +1,9 @@
mod build;
mod check;
mod init;
mod serve;

pub use self::build::build;
pub use self::check::check;
pub use self::init::create_new_project;
pub use self::serve::serve;

+ 47
- 59
src/cmd/serve.rs View File

@@ -23,14 +23,15 @@

use std::env;
use std::fs::{read_dir, remove_dir_all, File};
use std::io::{self, Read};
use std::io::Read;
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
use std::sync::mpsc::channel;
use std::thread;
use std::time::{Duration, Instant};

use actix_web::middleware::{Middleware, Response, Started};
use actix_web::{self, fs, http, server, App, HttpRequest, HttpResponse, Responder};
use actix_files as fs;
use actix_web::middleware::errhandlers::{ErrorHandlerResponse, ErrorHandlers};
use actix_web::{dev, http, web, App, HttpResponse, HttpServer};
use chrono::prelude::*;
use ctrlc;
use notify::{watcher, RecursiveMode, Watcher};
@@ -58,35 +59,35 @@ enum ChangeKind {
// errors
const LIVE_RELOAD: &str = include_str!("livereload.js");

struct NotFoundHandler {
rendered_template: PathBuf,
struct ErrorFilePaths {
not_found: PathBuf,
}

impl<S> Middleware<S> for NotFoundHandler {
fn start(&self, _req: &HttpRequest<S>) -> actix_web::Result<Started> {
Ok(Started::Done)
}
fn not_found<B>(
res: dev::ServiceResponse<B>
) -> std::result::Result<ErrorHandlerResponse<B>, actix_web::Error> {
let buf: Vec<u8> = {
let error_files: &ErrorFilePaths = res.request().app_data().unwrap();

fn response(
&self,
_req: &HttpRequest<S>,
mut resp: HttpResponse,
) -> actix_web::Result<Response> {
if http::StatusCode::NOT_FOUND == resp.status() {
let mut fh = File::open(&self.rendered_template)?;
let mut buf: Vec<u8> = vec![];
let _ = fh.read_to_end(&mut buf)?;
resp.replace_body(buf);
resp.headers_mut().insert(
http::header::CONTENT_TYPE,
http::header::HeaderValue::from_static("text/html"),
);
}
Ok(Response::Done(resp))
}
let mut fh = File::open(&error_files.not_found)?;
let mut buf: Vec<u8> = vec![];
let _ = fh.read_to_end(&mut buf)?;
buf
};

let new_resp = HttpResponse::build(http::StatusCode::NOT_FOUND)
.header(
http::header::CONTENT_TYPE,
http::header::HeaderValue::from_static("text/html"),
)
.body(buf);

Ok(ErrorHandlerResponse::Response(
res.into_response(new_resp.into_body()),
))
}

fn livereload_handler(_: &HttpRequest) -> HttpResponse {
fn livereload_handler() -> HttpResponse {
HttpResponse::Ok().content_type("text/javascript").body(LIVE_RELOAD)
}

@@ -141,27 +142,6 @@ fn create_new_site(
Ok((site, address))
}

/// Attempt to render `index.html` when a directory is requested.
///
/// The default "batteries included" mechanisms for actix to handle directory
/// listings rely on redirection which behaves oddly (the location headers
/// seem to use relative paths for some reason).
/// They also mean that the address in the browser will include the
/// `index.html` on a successful redirect (rare), which is unsightly.
///
/// Rather than deal with all of that, we can hijack a hook for presenting a
/// custom directory listing response and serve it up using their
/// `NamedFile` responder.
fn handle_directory<'a, 'b>(
dir: &'a fs::Directory,
req: &'b HttpRequest,
) -> io::Result<HttpResponse> {
let mut path = PathBuf::from(&dir.base);
path.push(&dir.path);
path.push("index.html");
fs::NamedFile::open(path)?.respond_to(req)
}

pub fn serve(
interface: &str,
port: u16,
@@ -211,24 +191,30 @@ pub fn serve(
let static_root = output_path.clone();
let broadcaster = if !watch_only {
thread::spawn(move || {
let s = server::new(move || {
let s = HttpServer::new(move || {
let error_handlers = ErrorHandlers::new()
.handler(http::StatusCode::NOT_FOUND, not_found);

App::new()
.middleware(NotFoundHandler { rendered_template: static_root.join("404.html") })
.resource(r"/livereload.js", |r| r.f(livereload_handler))
.data(ErrorFilePaths {
not_found: static_root.join("404.html"),
})
.wrap(error_handlers)
.route(
"/livereload.js",
web::get().to(livereload_handler)
)
// Start a webserver that serves the `output_dir` directory
.handler(
r"/",
fs::StaticFiles::new(&static_root)
.unwrap()
.show_files_listing()
.files_listing_renderer(handle_directory),
.service(
fs::Files::new("/", &static_root)
.index_file("index.html"),
)
})
.bind(&address)
.expect("Can't start the webserver")
.shutdown_timeout(20);
println!("Web server is available at http://{}\n", &address);
s.run();
s.run()
});
// The websocket for livereload
let ws_server = WebSocket::new(|output: Sender| {
@@ -390,7 +376,9 @@ pub fn serve(
}
console::report_elapsed_time(start);
}
Create(path) | Write(path) | Remove(path) => {
// Intellij does weird things on edit, chmod is there to count those changes
// https://github.com/passcod/notify/issues/150#issuecomment-494912080
Create(path) | Write(path) | Remove(path) | Chmod(path) => {
if is_temp_file(&path) || path.is_dir() {
continue;
}


+ 1
- 1
src/console.rs View File

@@ -53,7 +53,7 @@ pub fn notify_site_size(site: &Site) {
"-> Creating {} pages ({} orphan), {} sections, and processing {} images",
library.pages().len(),
site.get_number_orphan_pages(),
library.sections().len() - 1, // -1 since we do not the index as a section
library.sections().len() - 1, // -1 since we do not count the index as a section there
site.num_img_ops(),
);
}


+ 20
- 3
src/main.rs View File

@@ -1,3 +1,4 @@
extern crate actix_files;
extern crate actix_web;
extern crate atty;
#[macro_use]
@@ -63,13 +64,15 @@ fn main() {
::std::process::exit(1);
}
};
let watch_only = matches.is_present("watch_only");

// Default one
if port != 1111 && !port_is_available(port) {
if port != 1111 && !watch_only && !port_is_available(port) {
console::error("The requested port is not available");
::std::process::exit(1);
}

if !port_is_available(port) {
if !watch_only && !port_is_available(port) {
port = if let Some(p) = get_available_port(1111) {
p
} else {
@@ -77,7 +80,6 @@ fn main() {
::std::process::exit(1);
}
}
let watch_only = matches.is_present("watch_only");
let output_dir = matches.value_of("output_dir").unwrap();
let base_url = matches.value_of("base_url").unwrap();
console::info("Building site...");
@@ -89,6 +91,21 @@ fn main() {
}
};
}
("check", Some(matches)) => {
console::info("Checking site...");
let start = Instant::now();
match cmd::check(
config_file,
matches.value_of("base_path"),
matches.value_of("base_url"),
) {
Ok(()) => console::report_elapsed_time(start),
Err(e) => {
console::unravel_errors("Failed to check the site", &e);
::std::process::exit(1);
}
};
}
_ => unreachable!(),
}
}

BIN
sublime_syntaxes/newlines.packdump View File


+ 609
- 0
sublime_syntaxes/nix.sublime-syntax View File

@@ -0,0 +1,609 @@
%YAML 1.2
---
# http://www.sublimetext.com/docs/3/syntax.html
name: Nix
file_extensions:
- nix
scope: source.nix
contexts:
main:
- include: expression
comment:
- match: '/\*([^*]|\*[^\/])*'
push:
- meta_scope: comment.block.nix
- match: \*\/
pop: true
- include: comment-remark
- match: '\#'
push:
- meta_scope: comment.line.number-sign.nix
- match: $
pop: true
- include: comment-remark
attribute-bind:
- include: attribute-name
- include: attribute-bind-from-equals
attribute-bind-from-equals:
- match: \=
captures:
0: keyword.operator.bind.nix
push:
- match: \;
captures:
0: punctuation.terminator.bind.nix
pop: true
- include: expression
attribute-inherit:
- match: \binherit\b
captures:
0: keyword.other.inherit.nix
push:
- match: \;
captures:
0: punctuation.terminator.inherit.nix
pop: true
- match: \(
captures:
0: punctuation.section.function.arguments.nix
push:
- match: (?=\;)
pop: true
- match: \)
captures:
0: punctuation.section.function.arguments.nix
push:
- match: (?=\;)
pop: true
- include: bad-reserved
- include: attribute-name-single
- include: others
- include: expression
- match: '(?=[a-zA-Z\_])'
push:
- match: (?=\;)
pop: true
- include: bad-reserved
- include: attribute-name-single
- include: others
- include: others
attribute-name:
- match: '\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*'
scope: entity.other.attribute-name.multipart.nix
- match: \.
- include: string-quoted
- include: interpolation
attribute-name-single:
- match: '\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*'
scope: entity.other.attribute-name.single.nix
attrset-contents:
- include: attribute-inherit
- include: bad-reserved
- include: attribute-bind
- include: others
attrset-definition:
- match: '(?=\{)'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- match: '(\{)'
captures:
0: punctuation.definition.attrset.nix
push:
- match: '(\})'
captures:
0: punctuation.definition.attrset.nix
pop: true
- include: attrset-contents
- match: '(?<=\})'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression-cont
attrset-definition-brace-opened:
- match: '(?<=\})'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression-cont
- match: (?=.?)
push:
- match: '\}'
captures:
0: punctuation.definition.attrset.nix
pop: true
- include: attrset-contents
attrset-for-sure:
- match: (?=\brec\b)
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- match: \brec\b
captures:
0: keyword.other.nix
push:
- match: '(?=\{)'
pop: true
- include: others
- include: attrset-definition
- include: others
- match: '(?=\{\s*(\}|[^,?]*(=|;)))'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: attrset-definition
- include: others
attrset-or-function:
- match: '\{'
captures:
0: punctuation.definition.attrset-or-function.nix
push:
- match: '(?=([\])};]|\b(else|then)\b))'
pop: true
- match: '(?=(\s*\}|\"|\binherit\b|\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*(\s*\.|\s*=[^=])|\$\{[a-zA-z0-9\_\''\-]+\}(\s*\.|\s*=[^=])))'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: attrset-definition-brace-opened
- match: '(?=(\.\.\.|\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*\s*[,?]))'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: function-definition-brace-opened
- include: bad-reserved
- match: '\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*'
captures:
0: variable.parameter.function.maybe.nix
push:
- match: '(?=([\])};]|\b(else|then)\b))'
pop: true
- match: (?=\.)
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: attrset-definition-brace-opened
- match: \s*(\,)
captures:
1: keyword.operator.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: function-definition-brace-opened
- match: (?=\=)
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: attribute-bind-from-equals
- include: attrset-definition-brace-opened
- match: (?=\?)
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: function-parameter-default
- match: \,
captures:
0: keyword.operator.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: function-definition-brace-opened
- include: others
- include: others
bad-reserved:
- match: '(?<![\w''-])(if|then|else|assert|with|let|in|rec|inherit)(?![\w''-])'
scope: invalid.illegal.reserved.nix
comment-remark:
- match: (TODO|FIXME|BUG|\!\!\!):?
captures:
1: markup.bold.comment.nix
constants:
- match: \b(builtins|true|false|null)\b
captures:
0: constant.language.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression-cont
- match: \b(scopedImport|import|isNull|abort|throw|baseNameOf|dirOf|removeAttrs|map|toString|derivationStrict|derivation)\b
captures:
0: support.function.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression-cont
- match: '\b[0-9]+\b'
captures:
0: constant.numeric.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression-cont
expression:
- include: parens-and-cont
- include: list-and-cont
- include: string
- include: interpolation
- include: with-assert
- include: function-for-sure
- include: attrset-for-sure
- include: attrset-or-function
- include: let
- include: if
- include: operator-unary
- include: constants
- include: bad-reserved
- include: parameter-name-and-cont
- include: others
expression-cont:
- match: (?=.?)
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: parens
- include: list
- include: string
- include: interpolation
- include: function-for-sure
- include: attrset-for-sure
- include: attrset-or-function
- match: '(\bor\b|\.|==|!=|!|\<\=|\<|\>\=|\>|&&|\|\||-\>|//|\?|\+\+|-|\*|/(?=([^*]|$))|\+)'
scope: keyword.operator.nix
- include: constants
- include: bad-reserved
- include: parameter-name
- include: others
function-body:
- match: '(@\s*([a-zA-Z\_][a-zA-Z0-9\_\''\-]*)\s*)?(\:)'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression
function-body-from-colon:
- match: (\:)
captures:
0: punctuation.definition.function.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression
function-contents:
- include: bad-reserved
- include: function-parameter
- include: others
function-definition:
- match: (?=.?)
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: function-body-from-colon
- match: (?=.?)
push:
- match: (?=\:)
pop: true
- match: '(\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*)'
captures:
0: variable.parameter.function.4.nix
push:
- match: (?=\:)
pop: true
- match: \@
push:
- match: (?=\:)
pop: true
- include: function-header-until-colon-no-arg
- include: others
- include: others
- match: '(?=\{)'
push:
- match: (?=\:)
pop: true
- include: function-header-until-colon-with-arg
- include: others
function-definition-brace-opened:
- match: (?=.?)
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: function-body-from-colon
- match: (?=.?)
push:
- match: (?=\:)
pop: true
- include: function-header-close-brace-with-arg
- match: (?=.?)
push:
- match: '(?=\})'
pop: true
- include: function-contents
- include: others
function-for-sure:
- match: '(?=(\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*\s*[:@]|\{[^}]*\}\s*:|\{[^#}"''/=]*[,\?]))'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: function-definition
function-header-close-brace-no-arg:
- match: '\}'
captures:
0: punctuation.definition.entity.function.nix
push:
- match: (?=\:)
pop: true
- include: others
function-header-close-brace-with-arg:
- match: '\}'
captures:
0: punctuation.definition.entity.function.nix
push:
- match: (?=\:)
pop: true
- include: function-header-terminal-arg
- include: others
function-header-open-brace:
- match: '\{'
captures:
0: punctuation.definition.entity.function.2.nix
push:
- match: '(?=\})'
pop: true
- include: function-contents
function-header-terminal-arg:
- match: (?=@)
push:
- match: (?=\:)
pop: true
- match: \@
push:
- match: (?=\:)
pop: true
- match: '(\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*)'
push:
- meta_scope: variable.parameter.function.3.nix
- match: (?=\:)
pop: true
- include: others
- include: others
function-header-until-colon-no-arg:
- match: '(?=\{)'
push:
- match: (?=\:)
pop: true
- include: function-header-open-brace
- include: function-header-close-brace-no-arg
function-header-until-colon-with-arg:
- match: '(?=\{)'
push:
- match: (?=\:)
pop: true
- include: function-header-open-brace
- include: function-header-close-brace-with-arg
function-parameter:
- match: (\.\.\.)
push:
- meta_scope: keyword.operator.nix
- match: '(,|(?=\}))'
pop: true
- include: others
- match: '\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*'
captures:
0: variable.parameter.function.1.nix
push:
- match: '(,|(?=\}))'
captures:
0: keyword.operator.nix
pop: true
- include: whitespace
- include: comment
- include: function-parameter-default
- include: expression
- include: others
function-parameter-default:
- match: \?
captures:
0: keyword.operator.nix
push:
- match: "(?=[,}])"
pop: true
- include: expression
if:
- match: (?=\bif\b)
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- match: \bif\b
captures:
0: keyword.other.nix
push:
- match: \bth(?=en\b)
captures:
0: keyword.other.nix
pop: true
- include: expression
- match: (?<=th)en\b
captures:
0: keyword.other.nix
push:
- match: \bel(?=se\b)
captures:
0: keyword.other.nix
pop: true
- include: expression
- match: (?<=el)se\b
captures:
0: keyword.other.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
captures:
0: keyword.other.nix
pop: true
- include: expression
illegal:
- match: .
scope: invalid.illegal
interpolation:
- match: '\$\{'
captures:
0: punctuation.section.embedded.begin.nix
push:
- meta_scope: markup.italic
- match: '\}'
captures:
0: punctuation.section.embedded.end.nix
pop: true
- include: expression
let:
- match: (?=\blet\b)
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- match: \blet\b
captures:
0: keyword.other.nix
push:
- match: '(?=([\])};,]|\b(in|else|then)\b))'
pop: true
- match: '(?=\{)'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- match: '\{'
push:
- match: '\}'
pop: true
- include: attrset-contents
- match: '(^|(?<=\}))'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression-cont
- include: others
- include: attrset-contents
- include: others
- match: \bin\b
captures:
0: keyword.other.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression
list:
- match: '\['
captures:
0: punctuation.definition.list.nix
push:
- match: '\]'
captures:
0: punctuation.definition.list.nix
pop: true
- include: expression
list-and-cont:
- match: '(?=\[)'
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: list
- include: expression-cont
operator-unary:
- match: (!|-)
scope: keyword.operator.unary.nix
others:
- include: whitespace
- include: comment
- include: illegal
parameter-name:
- match: '\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*'
captures:
0: variable.parameter.name.nix
parameter-name-and-cont:
- match: '\b[a-zA-Z\_][a-zA-Z0-9\_\''\-]*'
captures:
0: variable.parameter.name.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression-cont
parens:
- match: \(
captures:
0: punctuation.definition.expression.nix
push:
- match: \)
captures:
0: punctuation.definition.expression.nix
pop: true
- include: expression
parens-and-cont:
- match: (?=\()
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: parens
- include: expression-cont
string:
- match: (?=\'\')
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- match: \'\'
captures:
0: punctuation.definition.string.other.start.nix
push:
- meta_scope: string.quoted.other.nix
- match: \'\'(?!\$|\'|\\.)
captures:
0: punctuation.definition.string.other.end.nix
pop: true
- match: \'\'(\$|\'|\\.)
scope: constant.character.escape.nix
- include: interpolation
- include: expression-cont
- match: (?=\")
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: string-quoted
- include: expression-cont
- match: '(~?[a-zA-Z0-9\.\_\-\+]*(\/[a-zA-Z0-9\.\_\-\+]+)+)'
captures:
0: string.unquoted.path.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression-cont
- match: '(\<[a-zA-Z0-9\.\_\-\+]+(\/[a-zA-Z0-9\.\_\-\+]+)*\>)'
captures:
0: string.unquoted.spath.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression-cont
- match: '([a-zA-Z][a-zA-Z0-9\+\-\.]*\:[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~\*\'']+)'
captures:
0: string.unquoted.url.nix
push:
- match: '(?=([\])};,]|\b(else|then)\b))'
pop: true
- include: expression-cont
string-quoted:
- match: \"
captures:
0: punctuation.definition.string.double.start.nix
push:
- meta_scope: string.quoted.double.nix
- match: \"
captures:
0: punctuation.definition.string.double.end.nix
pop: true
- match: \\.
scope: constant.character.escape.nix
- include: interpolation
whitespace:
- match: \s+
with-assert:
- match: '(?<![\w''-])(with|assert)(?![\w''-])'
captures:
0: keyword.other.nix
push:
- match: \;
pop: true
- include: expression

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

@@ -4,4 +4,5 @@ paginate_by = 2
template = "section_paginated.html"
insert_anchor_links = "left"
sort_by = "date"
aliases = ["another-old-url/index.html"]
+++

+ 1
- 1
test_site/content/posts/draft.md View File

@@ -7,4 +7,4 @@ date = 2016-03-01

{{ theme_shortcode() }}

Link to [root](./hello.md).
Link to [root](@/hello.md).

Loading…
Cancel
Save