@@ -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 | |||
@@ -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 | |||
@@ -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 |
@@ -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" ] |
@@ -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) | | |||
@@ -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 | |||
@@ -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=( | |||
@@ -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') | |||
@@ -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 | |||
@@ -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' |
@@ -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" | |||
@@ -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" |
@@ -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" | |||
@@ -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(), | |||
} | |||
} | |||
@@ -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" | |||
@@ -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), | |||
@@ -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" | |||
@@ -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")); | |||
} | |||
} |
@@ -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(); | |||
@@ -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(), | |||
} | |||
} | |||
} | |||
@@ -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" | |||
@@ -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, | |||
}) | |||
} | |||
} |
@@ -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 {#…}\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] | |||
@@ -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" |
@@ -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 §ion.meta.aliases { | |||
self.render_alias(&alias, §ion.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 | |||
}; | |||
@@ -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 { | |||
@@ -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"); | |||
@@ -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,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" | |||
@@ -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 %} | |||
@@ -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,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> | |||
@@ -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] | |||
@@ -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>>, | |||
@@ -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; | |||
@@ -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" | |||
@@ -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); | |||
@@ -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() | |||
} |
@@ -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] | |||
@@ -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? | |||
@@ -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) |
@@ -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. |
@@ -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 | |||
@@ -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><!-- more --></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: | |||
@@ -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`) | |||
@@ -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"] | |||
``` | |||
@@ -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: | |||
@@ -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: | |||
@@ -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 | |||
@@ -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: | |||
@@ -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 | |||
@@ -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. | |||
@@ -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. | |||
@@ -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,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. |
@@ -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. |
@@ -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 |
@@ -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. |
@@ -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 | |||
@@ -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: | |||
@@ -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). | |||
@@ -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> | |||
@@ -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" |
@@ -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: | |||
@@ -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") | |||
]) | |||
} |
@@ -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(()) | |||
} |
@@ -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; |
@@ -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; | |||
} | |||
@@ -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(), | |||
); | |||
} | |||
@@ -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!(), | |||
} | |||
} |
@@ -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 |
@@ -4,4 +4,5 @@ paginate_by = 2 | |||
template = "section_paginated.html" | |||
insert_anchor_links = "left" | |||
sort_by = "date" | |||
aliases = ["another-old-url/index.html"] | |||
+++ |
@@ -7,4 +7,4 @@ date = 2016-03-01 | |||
{{ theme_shortcode() }} | |||
Link to [root](./hello.md). | |||
Link to [root](@/hello.md). |