Merge pull request #804 from getzola/next

v0.9.0
This commit is contained in:
Vincent Prouillet 2019-09-28 10:59:38 -07:00 committed by GitHub
commit 37ad5dccc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 4160 additions and 1770 deletions

View file

@ -1,60 +0,0 @@
dist: trusty
language: rust
services: docker
env:
global:
- CRATE_NAME=zola
matrix:
include:
# Linux
- env: TARGET=x86_64-unknown-linux-gnu
# OSX
- env: TARGET=x86_64-apple-darwin
os: osx
# The earliest stable Rust version that works
- env: TARGET=x86_64-unknown-linux-gnu
rust: 1.34.0
before_install: set -e
install:
- sh ci/install.sh
- source ~/.cargo/env || true
script:
- bash ci/script.sh
after_script: set +e
before_deploy:
- sh ci/before_deploy.sh
deploy:
api_key:
secure: "nksXOY7p8vAWDpItN9Tyx+0CmOPMj/iAgH+iT512URpgJG/i+ziUWDEYpQO4PfZMJUDUa1tnSZ31O4MIe2Sgfj6DHR1zK+LKeLaZxuxxJUSMXSAkbIXcjFlOPKQBPnMZVVcDaHMxz18jiRpElDR2k0PIEtspW2rDsrr+7mzmQn7pan60k77tU3RG3K7fYgMmNjVv64XqMBSCS3fpqiroIz7rVL1HZ3sCoTNnxDM8nXo/8gTjlVowTvUTsVyHRgtDRJdlPuI0yf4oJmvQPX74P2OkQmOVpGxeJ/gSTJ1xWxYfMgyvNaiO9NKF+fUfxvHR/V58CfBHPdJkcnThV5KIPjE5mHZfSTFf5cG6gJtnVhvhQV7vBhIRI/iCt55SPCXse1HWzTY1GxE5oXw2VzUt/kzD2pFf8rtf64JURgGolenYv3aw+ps1MGUwUjl8CF31XBSiASVwpif7kd9P3bafg6pGUytfjgpV/wJJc8OpO8IGwTSNe4r0wtcFb92stxta4NKC3L4F0w/juaK+0+Mjt4SCyh6rRzpHQu9TJKniskp7/URp5KhMFAo66sFpgSYVa23OTkYmjtB8IqlJzmpuDSs/WSAVA8InSgHDaQeBd0UEbNaWU1+avtAGBtb8+rZnbw7ikPF0j2pHImD5ZjHp7+jt/hpcwqrOkBuB5CSeBKs="
file_glob: true
file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.*
on:
condition: $TRAVIS_RUST_VERSION = stable
tags: true
provider: releases
skip_cleanup: true
cache: cargo
before_cache:
# Travis can't cache files that are not readable by "others"
- chmod -R a+r $HOME/.cargo
branches:
only:
# release tags
- /^v\d+\.\d+\.\d+.*$/
- master
- next
notifications:
email: false

View file

@ -1,5 +1,29 @@
# Changelog # Changelog
## 0.9.0 (2019-09-28)
### Breaking
- Add `--drafts` flag to `build`, `serve` and `check` to load drafts. Drafts are never loaded by default anymore
- Using `fit` in `resize_image` on an image smaller than the given height/width is now a no-op and will not upscale images anymore
### Other
- Add `--open` flag to open server URL in default browser
- Fix sitemaps namespace & do not urlencode URLs
- Update livereload
- Add `hard_link_static` config option to hard link things in the static directory instead of copying
- Add warning for old style internal links since they would still function silently
- Print some counts when running `zola check`
- Re-render all pages/sections when `anchor-link.html` is changed
- Taxonomies can now have the same name in multiple languages
- `zola init` can now be create sites inside the current directory
- Fix table of contents generation for deep heading levels
- Add `lang` in all templates context except sitemap, robots
- Add `lang` parameter to `get_taxonomy` and `get_taxonomy_url`
- Rebuild whole site on changes in `themes` changes
- Add one-dark syntax highlighting theme
- Process images on changes in `zola serve` if needed after change
## 0.8.0 (2019-06-22) ## 0.8.0 (2019-06-22)
### Breaking ### Breaking

1987
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package] [package]
name = "zola" name = "zola"
version = "0.8.0" version = "0.9.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] authors = ["Vincent Prouillet <hello@vincentprouillet.com>"]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
description = "A fast static site generator with everything built-in" description = "A fast static site generator with everything built-in"
@ -21,16 +21,17 @@ atty = "0.2.11"
clap = "2" clap = "2"
chrono = "0.4" chrono = "0.4"
lazy_static = "1.1.0" lazy_static = "1.1.0"
toml = "0.4"
termcolor = "1.0.4" termcolor = "1.0.4"
# Used in init to ensure the url given as base_url is a valid one # Used in init to ensure the url given as base_url is a valid one
url = "1.5" url = "2"
# Below is for the serve cmd # Below is for the serve cmd
actix-files = "0.1" actix-files = "0.1"
actix-web = { version = "1.0", default-features = false, features = [] } actix-web = { version = "1.0", default-features = false, features = [] }
notify = "4" notify = "4"
ws = "0.8" ws = "0.9"
ctrlc = "3" ctrlc = "3"
open = "1.2"
globset = "0.4"
site = { path = "components/site" } site = { path = "components/site" }
errors = { path = "components/errors" } errors = { path = "components/errors" }

View file

@ -3,7 +3,6 @@ RUN install_packages python-pip curl tar python-setuptools rsync binutils
RUN pip install dockerize RUN pip install dockerize
RUN mkdir -p /workdir RUN mkdir -p /workdir
WORKDIR /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 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 mv zola /usr/bin
RUN dockerize -n -o /workdir /usr/bin/zola RUN dockerize -n -o /workdir /usr/bin/zola

View file

@ -1,57 +0,0 @@
# Based on the "trust" template v0.1.1
# https://github.com/japaric/trust/tree/v0.1.1
os: Visual Studio 2017
environment:
global:
RUST_VERSION: stable
CRATE_NAME: zola
matrix:
- target: x86_64-pc-windows-msvc
RUST_VERSION: 1.34.0
- target: x86_64-pc-windows-msvc
RUST_VERSION: stable
install:
- call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" x86_amd64
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
- rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_VERSION%
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustc -Vv
- cargo -V
test_script:
# we don't run the "test phase" when doing deploys
- if [%APPVEYOR_REPO_TAG%]==[false] (
cargo test --all --target %TARGET%
)
before_deploy:
- cargo rustc --target %TARGET% --release --bin zola -- -C lto
- ps: ci\before_deploy.ps1
deploy:
artifact: /.*\.zip/
auth_token:
secure: i64eFOHoySQryE3M9pr2JGRukAK3LGltOsUxeFHwilS+3O6/6828A4NUmI0FW4zN
description: ''
on:
RUST_VERSION: stable
appveyor_repo_tag: true
provider: GitHub
cache:
- C:\Users\appveyor\.cargo\registry
- target
branches:
only:
# Release tags
- /^v\d+\.\d+\.\d+.*$/
- master
- next
# disable automatic builds
build: false

133
azure-pipelines.yml Normal file
View file

@ -0,0 +1,133 @@
trigger:
branches:
include: ['*']
tags:
include: ['*']
stages:
- stage: Tests
jobs:
- job:
strategy:
matrix:
windows-stable:
imageName: 'vs2017-win2016'
rustup_toolchain: stable
mac-stable:
imageName: 'macos-10.14'
rustup_toolchain: stable
linux-stable:
imageName: 'ubuntu-16.04'
rustup_toolchain: stable
linux-1.35:
imageName: 'ubuntu-16.04'
rustup_toolchain: 1.35.0
pool:
vmImage: $(imageName)
steps:
- script: |
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUSTUP_TOOLCHAIN
echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin"
displayName: Install rust
condition: ne( variables['Agent.OS'], 'Windows_NT' )
- script: |
curl -sSf -o rustup-init.exe https://win.rustup.rs
rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN%
echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin"
displayName: Windows install rust
condition: eq( variables['Agent.OS'], 'Windows_NT' )
- script: cargo build --all
displayName: Cargo build
- script: cargo test --all
displayName: Cargo test
- stage: Release
dependsOn: Tests
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/')
jobs:
- job:
strategy:
matrix:
windows-stable:
imageName: 'vs2017-win2016'
rustup_toolchain: stable
target: 'x86_64-pc-windows-msvc'
mac-stable:
imageName: 'macos-10.14'
rustup_toolchain: stable
target: 'x86_64-apple-darwin'
linux-stable:
imageName: 'ubuntu-16.04'
rustup_toolchain: stable
target: 'x86_64-unknown-linux-gnu'
pool:
vmImage: $(imageName)
steps:
- script: |
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUSTUP_TOOLCHAIN
echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin"
displayName: Install rust
condition: ne( variables['Agent.OS'], 'Windows_NT' )
- script: |
curl -sSf -o rustup-init.exe https://win.rustup.rs
rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN%
echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin"
displayName: Windows install rust
condition: eq( variables['Agent.OS'], 'Windows_NT' )
- script: |
rustup target add $TARGET
cargo build --release --target $TARGET
condition: ne( variables['Agent.OS'], 'Windows_NT' )
displayName: Build
- script: |
rustup target add %TARGET%
cargo build --release --target %TARGET%
condition: eq( variables['Agent.OS'], 'Windows_NT' )
displayName: Build on Windows
- task: CopyFiles@2
displayName: Copy assets
condition: ne( variables['Agent.OS'], 'Windows_NT' )
inputs:
sourceFolder: '$(Build.SourcesDirectory)/target/$(TARGET)/release'
contents: zola
targetFolder: '$(Build.BinariesDirectory)/'
- task: CopyFiles@2
displayName: Copy assets on Windows
condition: eq( variables['Agent.OS'], 'Windows_NT' )
inputs:
sourceFolder: '$(Build.SourcesDirectory)/target/$(TARGET)/release'
contents: zola.exe
targetFolder: '$(Build.BinariesDirectory)/'
- task: ArchiveFiles@2
displayName: Gather assets
condition: ne( variables['Agent.OS'], 'Windows_NT' )
inputs:
rootFolderOrFile: '$(Build.BinariesDirectory)/zola'
archiveType: 'tar'
tarCompression: 'gz'
archiveFile: '$(Build.ArtifactStagingDirectory)/zola-$(Build.SourceBranchName)-$(TARGET).tar.gz'
- task: ArchiveFiles@2
displayName: Gather assets
condition: eq( variables['Agent.OS'], 'Windows_NT' )
inputs:
rootFolderOrFile: '$(Build.BinariesDirectory)/zola.exe'
archiveType: 'tar'
tarCompression: 'gz'
archiveFile: '$(Build.ArtifactStagingDirectory)/zola-$(Build.SourceBranchName)-$(TARGET).tar.gz'
- task: GithubRelease@0
inputs:
gitHubConnection: 'zola'
repositoryName: 'keats/azure-pipelines-test'
action: 'edit'
target: '$(build.sourceVersion)'
tagSource: 'manual'
tag: '$(Build.SourceBranchName)'
assets: '$(Build.ArtifactStagingDirectory)/zola-$(Build.SourceBranchName)-$(TARGET).tar.gz'
title: '$(Build.SourceBranchName)'
assetUploadMode: 'replace'
addChangeLog: true

View file

@ -1,22 +0,0 @@
# This script takes care of packaging the build artifacts that will go in the
# release zipfile
$SRC_DIR = $PWD.Path
$STAGE = [System.Guid]::NewGuid().ToString()
Set-Location $ENV:Temp
New-Item -Type Directory -Name $STAGE
Set-Location $STAGE
$ZIP = "$SRC_DIR\$($Env:CRATE_NAME)-$($Env:APPVEYOR_REPO_TAG_NAME)-$($Env:TARGET).zip"
Copy-Item "$SRC_DIR\target\$($Env:TARGET)\release\zola.exe" '.\'
7z a "$ZIP" *
Push-AppveyorArtifact "$ZIP"
Remove-Item *.* -Force
Set-Location ..
Remove-Item $STAGE
Set-Location $SRC_DIR

View file

@ -1,31 +0,0 @@
# This script takes care of building your crate and packaging it for release
set -ex
main() {
local src=$(pwd) \
stage=
case $TRAVIS_OS_NAME in
linux)
stage=$(mktemp -d)
;;
osx)
stage=$(mktemp -d -t tmp)
;;
esac
test -f Cargo.lock || cargo generate-lockfile
cross rustc --bin zola --target $TARGET --release -- -C lto
cp target/$TARGET/release/zola $stage/
cd $stage
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
cd $src
rm -rf $stage
}
main

View file

@ -1,31 +0,0 @@
set -ex
main() {
curl https://sh.rustup.rs -sSf | \
sh -s -- -y --default-toolchain $TRAVIS_RUST_VERSION
local target=
if [ $TRAVIS_OS_NAME = linux ]; then
target=x86_64-unknown-linux-gnu
sort=sort
else
target=x86_64-apple-darwin
sort=gsort # for `sort --sort-version`, from brew's coreutils.
fi
# This fetches latest stable release
local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \
| cut -d/ -f3 \
| grep -E '^v[0-9.]+$' \
| $sort --version-sort \
| tail -n1)
echo cross version: $tag
curl -LSfs https://japaric.github.io/trust/install.sh | \
sh -s -- \
--force \
--git japaric/cross \
--tag $tag \
--target $target
}
main

View file

@ -1,17 +0,0 @@
# This script takes care of testing your crate
set -ex
# TODO This is the "test phase", tweak it as you see fit
main() {
if [ ! -z $DISABLE_TESTS ]; then
return
fi
cross test --all --target $TARGET
}
# we don't run the "test phase" when doing deploys
if [ -z $TRAVIS_TAG ]; then
main
fi

View file

@ -36,7 +36,7 @@ _arguments "${_arguments_options[@]}" \
'--help[Prints help information]' \ '--help[Prints help information]' \
'-V[Prints version information]' \ '-V[Prints version information]' \
'--version[Prints version information]' \ '--version[Prints version information]' \
':name -- Name of the project. Will create a new directory with that name in the current directory:_files' \ '::name -- Name of the project. Will create a new directory with that name in the current directory:_files' \
&& ret=0 && ret=0
;; ;;
(build) (build)
@ -45,6 +45,7 @@ _arguments "${_arguments_options[@]}" \
'--base-url=[Force the base URL to be that value (default to the one in config.toml)]' \ '--base-url=[Force the base URL to be that value (default to the one in config.toml)]' \
'-o+[Outputs the generated site in the given path]' \ '-o+[Outputs the generated site in the given path]' \
'--output-dir=[Outputs the generated site in the given path]' \ '--output-dir=[Outputs the generated site in the given path]' \
'--drafts[Include drafts when loading the site]' \
'-h[Prints help information]' \ '-h[Prints help information]' \
'--help[Prints help information]' \ '--help[Prints help information]' \
'-V[Prints version information]' \ '-V[Prints version information]' \
@ -62,6 +63,9 @@ _arguments "${_arguments_options[@]}" \
'-u+[Changes the base_url]' \ '-u+[Changes the base_url]' \
'--base-url=[Changes the base_url]' \ '--base-url=[Changes the base_url]' \
'--watch-only[Do not start a server, just re-build project on changes]' \ '--watch-only[Do not start a server, just re-build project on changes]' \
'--drafts[Include drafts when loading the site]' \
'-O[Open site in the default browser]' \
'--open[Open site in the default browser]' \
'-h[Prints help information]' \ '-h[Prints help information]' \
'--help[Prints help information]' \ '--help[Prints help information]' \
'-V[Prints version information]' \ '-V[Prints version information]' \
@ -70,6 +74,7 @@ _arguments "${_arguments_options[@]}" \
;; ;;
(check) (check)
_arguments "${_arguments_options[@]}" \ _arguments "${_arguments_options[@]}" \
'--drafts[Include drafts when loading the site]' \
'-h[Prints help information]' \ '-h[Prints help information]' \
'--help[Prints help information]' \ '--help[Prints help information]' \
'-V[Prints version information]' \ '-V[Prints version information]' \

View file

@ -45,6 +45,7 @@ Register-ArgumentCompleter -Native -CommandName 'zola' -ScriptBlock {
[CompletionResult]::new('--base-url', 'base-url', [CompletionResultType]::ParameterName, 'Force the base URL to be that value (default to the one in config.toml)') [CompletionResult]::new('--base-url', 'base-url', [CompletionResultType]::ParameterName, 'Force the base URL to be that value (default to the one in config.toml)')
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Outputs the generated site in the given path') [CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Outputs the generated site in the given path')
[CompletionResult]::new('--output-dir', 'output-dir', [CompletionResultType]::ParameterName, 'Outputs the generated site in the given path') [CompletionResult]::new('--output-dir', 'output-dir', [CompletionResultType]::ParameterName, 'Outputs the generated site in the given path')
[CompletionResult]::new('--drafts', 'drafts', [CompletionResultType]::ParameterName, 'Include drafts when loading the site')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information')
[CompletionResult]::new('--help', 'help', [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('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information')
@ -61,6 +62,9 @@ Register-ArgumentCompleter -Native -CommandName 'zola' -ScriptBlock {
[CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'Changes the base_url') [CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'Changes the base_url')
[CompletionResult]::new('--base-url', 'base-url', [CompletionResultType]::ParameterName, 'Changes the base_url') [CompletionResult]::new('--base-url', 'base-url', [CompletionResultType]::ParameterName, 'Changes the base_url')
[CompletionResult]::new('--watch-only', 'watch-only', [CompletionResultType]::ParameterName, 'Do not start a server, just re-build project on changes') [CompletionResult]::new('--watch-only', 'watch-only', [CompletionResultType]::ParameterName, 'Do not start a server, just re-build project on changes')
[CompletionResult]::new('--drafts', 'drafts', [CompletionResultType]::ParameterName, 'Include drafts when loading the site')
[CompletionResult]::new('-O', 'O', [CompletionResultType]::ParameterName, 'Open site in the default browser')
[CompletionResult]::new('--open', 'open', [CompletionResultType]::ParameterName, 'Open site in the default browser')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information')
[CompletionResult]::new('--help', 'help', [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('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information')
@ -68,6 +72,7 @@ Register-ArgumentCompleter -Native -CommandName 'zola' -ScriptBlock {
break break
} }
'zola;check' { 'zola;check' {
[CompletionResult]::new('--drafts', 'drafts', [CompletionResultType]::ParameterName, 'Include drafts when loading the site')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Prints help information')
[CompletionResult]::new('--help', 'help', [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('-V', 'V', [CompletionResultType]::ParameterName, 'Prints version information')

View file

@ -59,7 +59,7 @@ _zola() {
;; ;;
zola__build) zola__build)
opts=" -h -V -u -o --help --version --base-url --output-dir " opts=" -h -V -u -o --drafts --help --version --base-url --output-dir "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -90,7 +90,7 @@ _zola() {
return 0 return 0
;; ;;
zola__check) zola__check)
opts=" -h -V --help --version " opts=" -h -V --drafts --help --version "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -135,7 +135,7 @@ _zola() {
return 0 return 0
;; ;;
zola__serve) zola__serve)
opts=" -h -V -i -p -o -u --watch-only --help --version --interface --port --output-dir --base-url " opts=" -O -h -V -i -p -o -u --watch-only --drafts --open --help --version --interface --port --output-dir --base-url "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0

View file

@ -10,6 +10,7 @@ complete -c zola -n "__fish_seen_subcommand_from init" -s h -l help -d 'Prints h
complete -c zola -n "__fish_seen_subcommand_from init" -s V -l version -d 'Prints version information' complete -c zola -n "__fish_seen_subcommand_from init" -s V -l version -d 'Prints version information'
complete -c zola -n "__fish_seen_subcommand_from build" -s u -l base-url -d 'Force the base URL to be that value (default to the one in config.toml)' complete -c zola -n "__fish_seen_subcommand_from build" -s u -l base-url -d 'Force the base URL to be that value (default to the one in config.toml)'
complete -c zola -n "__fish_seen_subcommand_from build" -s o -l output-dir -d 'Outputs the generated site in the given path' complete -c zola -n "__fish_seen_subcommand_from build" -s o -l output-dir -d 'Outputs the generated site in the given path'
complete -c zola -n "__fish_seen_subcommand_from build" -l drafts -d 'Include drafts when loading the site'
complete -c zola -n "__fish_seen_subcommand_from build" -s h -l help -d 'Prints help information' complete -c zola -n "__fish_seen_subcommand_from build" -s h -l help -d 'Prints help information'
complete -c zola -n "__fish_seen_subcommand_from build" -s V -l version -d 'Prints version information' complete -c zola -n "__fish_seen_subcommand_from build" -s V -l version -d 'Prints version information'
complete -c zola -n "__fish_seen_subcommand_from serve" -s i -l interface -d 'Interface to bind on' complete -c zola -n "__fish_seen_subcommand_from serve" -s i -l interface -d 'Interface to bind on'
@ -17,8 +18,11 @@ complete -c zola -n "__fish_seen_subcommand_from serve" -s p -l port -d 'Which p
complete -c zola -n "__fish_seen_subcommand_from serve" -s o -l output-dir -d 'Outputs the generated site in the given path' complete -c zola -n "__fish_seen_subcommand_from serve" -s o -l output-dir -d 'Outputs the generated site in the given path'
complete -c zola -n "__fish_seen_subcommand_from serve" -s u -l base-url -d 'Changes the base_url' complete -c zola -n "__fish_seen_subcommand_from serve" -s u -l base-url -d 'Changes the base_url'
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" -l watch-only -d 'Do not start a server, just re-build project on changes'
complete -c zola -n "__fish_seen_subcommand_from serve" -l drafts -d 'Include drafts when loading the site'
complete -c zola -n "__fish_seen_subcommand_from serve" -s O -l open -d 'Open site in the default browser'
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 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 serve" -s V -l version -d 'Prints version information'
complete -c zola -n "__fish_seen_subcommand_from check" -l drafts -d 'Include drafts when loading the site'
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 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 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 h -l help -d 'Prints help information'

View file

@ -10,7 +10,7 @@ serde_derive = "1"
chrono = "0.4" chrono = "0.4"
globset = "0.4" globset = "0.4"
lazy_static = "1" lazy_static = "1"
syntect = "3" syntect = "=3.2.0"
errors = { path = "../errors" } errors = { path = "../errors" }
utils = { path = "../utils" } utils = { path = "../utils" }

View file

@ -8,12 +8,20 @@ use toml;
use toml::Value as Toml; use toml::Value as Toml;
use errors::Result; use errors::Result;
use errors::Error;
use highlighting::THEME_SET; use highlighting::THEME_SET;
use theme::Theme; use theme::Theme;
use utils::fs::read_file_with_error; use utils::fs::read_file_with_error;
// We want a default base url for tests // We want a default base url for tests
static DEFAULT_BASE_URL: &'static str = "http://a-website.com"; static DEFAULT_BASE_URL: &str = "http://a-website.com";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Mode {
Build,
Serve,
Check,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
@ -22,11 +30,13 @@ pub struct Language {
pub code: String, pub code: String,
/// Whether to generate a RSS feed for that language, defaults to `false` /// Whether to generate a RSS feed for that language, defaults to `false`
pub rss: bool, pub rss: bool,
/// Whether to generate search index for that language, defaults to `false`
pub search: bool,
} }
impl Default for Language { impl Default for Language {
fn default() -> Language { fn default() -> Language {
Language { code: String::new(), rss: false } Language { code: String::new(), rss: false, search: false }
} }
} }
@ -76,6 +86,8 @@ impl Default for Taxonomy {
} }
} }
type TranslateTerm = HashMap<String, String>;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct Config { pub struct Config {
@ -93,8 +105,15 @@ pub struct Config {
pub default_language: String, pub default_language: String,
/// The list of supported languages outside of the default one /// The list of supported languages outside of the default one
pub languages: Vec<Language>, pub languages: Vec<Language>,
/// Languages list and translated strings /// Languages list and translated strings
pub translations: HashMap<String, Toml>, ///
/// The `String` key of `HashMap` is a language name, the value should be toml crate `Table`
/// with String key representing term and value another `String` representing its translation.
///
/// The attribute is intentionally not public, use `get_translation()` method for translating
/// key into different language.
translations: HashMap<String, TranslateTerm>,
/// Whether to highlight all code blocks found in markdown files. Defaults to false /// Whether to highlight all code blocks found in markdown files. Defaults to false
pub highlight_code: bool, pub highlight_code: bool,
@ -106,6 +125,8 @@ pub struct Config {
pub generate_rss: bool, pub generate_rss: bool,
/// The number of articles to include in the RSS feed. Defaults to including all items. /// The number of articles to include in the RSS feed. Defaults to including all items.
pub rss_limit: Option<usize>, pub rss_limit: Option<usize>,
/// If set, files from static/ will be hardlinked instead of copied to the output dir.
pub hard_link_static: bool,
pub taxonomies: Vec<Taxonomy>, pub taxonomies: Vec<Taxonomy>,
@ -120,8 +141,10 @@ pub struct Config {
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are needed #[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are needed
pub ignored_content_globset: Option<GlobSet>, pub ignored_content_globset: Option<GlobSet>,
/// Whether to check all external links for validity /// The mode Zola is currently being ran on. Some logging/feature can differ depending on the
pub check_external_links: bool, /// command being used.
#[serde(skip_serializing)]
pub mode: Mode,
/// A list of directories to search for additional `.sublime-syntax` files in. /// A list of directories to search for additional `.sublime-syntax` files in.
pub extra_syntaxes: Vec<String>, pub extra_syntaxes: Vec<String>,
@ -265,6 +288,39 @@ impl Config {
pub fn languages_codes(&self) -> Vec<&str> { pub fn languages_codes(&self) -> Vec<&str> {
self.languages.iter().map(|l| l.code.as_ref()).collect() self.languages.iter().map(|l| l.code.as_ref()).collect()
} }
pub fn is_in_build_mode(&self) -> bool {
self.mode == Mode::Build
}
pub fn is_in_serve_mode(&self) -> bool {
self.mode == Mode::Serve
}
pub fn is_in_check_mode(&self) -> bool {
self.mode == Mode::Check
}
pub fn enable_serve_mode(&mut self) {
self.mode = Mode::Serve;
}
pub fn enable_check_mode(&mut self) {
self.mode = Mode::Check;
// Disable syntax highlighting since the results won't be used
// and this operation can be expensive.
self.highlight_code = false;
}
pub fn get_translation<S: AsRef<str>>(&self, lang: S, key: S) -> Result<String> {
let terms = self.translations.get(lang.as_ref()).ok_or_else(|| {
Error::msg(format!("Translation for language '{}' is missing", lang.as_ref()))
})?;
terms.get(key.as_ref()).ok_or_else(|| {
Error::msg(format!("Translation key '{}' for language '{}' is missing", key.as_ref(), lang.as_ref()))
}).map(|term| term.to_string())
}
} }
impl Default for Config { impl Default for Config {
@ -280,9 +336,10 @@ impl Default for Config {
languages: Vec::new(), languages: Vec::new(),
generate_rss: false, generate_rss: false,
rss_limit: None, rss_limit: None,
hard_link_static: false,
taxonomies: Vec::new(), taxonomies: Vec::new(),
compile_sass: false, compile_sass: false,
check_external_links: false, mode: Mode::Build,
build_search_index: false, build_search_index: false,
ignored_content: Vec::new(), ignored_content: Vec::new(),
ignored_content_globset: None, ignored_content_globset: None,
@ -412,9 +469,7 @@ a_value = 10
assert_eq!(extra["a_value"].as_integer().unwrap(), 10); assert_eq!(extra["a_value"].as_integer().unwrap(), 10);
} }
#[test] const CONFIG_TRANSLATION: &str = r#"
fn can_use_language_configuration() {
let config = r#"
base_url = "https://remplace-par-ton-url.fr" base_url = "https://remplace-par-ton-url.fr"
default_language = "fr" default_language = "fr"
@ -424,14 +479,29 @@ title = "Un titre"
[translations.en] [translations.en]
title = "A title" title = "A title"
"#; "#;
let config = Config::parse(config); #[test]
assert!(config.is_ok()); fn can_use_present_translation() {
let translations = config.unwrap().translations; let config = Config::parse(CONFIG_TRANSLATION).unwrap();
assert_eq!(translations["fr"]["title"].as_str().unwrap(), "Un titre"); assert_eq!(config.get_translation("fr", "title").unwrap(), "Un titre");
assert_eq!(translations["en"]["title"].as_str().unwrap(), "A title"); assert_eq!(config.get_translation("en", "title").unwrap(), "A title");
}
#[test]
fn error_on_absent_translation_lang() {
let config = Config::parse(CONFIG_TRANSLATION).unwrap();
let error = config.get_translation("absent", "key").unwrap_err();
assert_eq!("Translation for language 'absent' is missing", format!("{}", error));
}
#[test]
fn error_on_absent_translation_key() {
let config = Config::parse(CONFIG_TRANSLATION).unwrap();
let error = config.get_translation("en", "absent").unwrap_err();
assert_eq!("Translation key 'absent' for language 'en' is missing", format!("{}", error));
} }
#[test] #[test]

View file

@ -6,5 +6,5 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
[dependencies] [dependencies]
tera = "1.0.0-beta.10" tera = "1.0.0-beta.10"
toml = "0.5" toml = "0.5"
image = "0.21" image = "0.22"
syntect = "3" syntect = "=3.2.0"

View file

@ -31,10 +31,9 @@ impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> { fn source(&self) -> Option<&(dyn StdError + 'static)> {
let mut source = self.source.as_ref().map(|c| &**c); let mut source = self.source.as_ref().map(|c| &**c);
if source.is_none() { if source.is_none() {
match self.kind { if let ErrorKind::Tera(ref err) = self.kind {
ErrorKind::Tera(ref err) => source = err.source(), source = err.source();
_ => (), }
};
} }
source source

View file

@ -24,7 +24,7 @@ pub struct PageFrontMatter {
/// The converted date into a (year, month, day) tuple /// The converted date into a (year, month, day) tuple
#[serde(default, skip_deserializing)] #[serde(default, skip_deserializing)]
pub datetime_tuple: Option<(i32, u32, u32)>, pub datetime_tuple: Option<(i32, u32, u32)>,
/// Whether this page is a draft and should be ignored for pagination etc /// Whether this page is a draft
pub draft: bool, pub draft: bool,
/// The page slug. Will be used instead of the filename if present /// The page slug. Will be used instead of the filename if present
/// Can't be an empty string if present /// Can't be an empty string if present

View file

@ -7,7 +7,7 @@ use errors::Result;
use super::{InsertAnchor, SortBy}; use super::{InsertAnchor, SortBy};
static DEFAULT_PAGINATE_PATH: &'static str = "page"; static DEFAULT_PAGINATE_PATH: &str = "page";
/// The front matter of every section /// The front matter of every section
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View file

@ -7,7 +7,7 @@ authors = ["Vojtěch Král <vojtech@kral.hk>"]
lazy_static = "1" lazy_static = "1"
regex = "1.0" regex = "1.0"
tera = "1.0.0-beta.10" tera = "1.0.0-beta.10"
image = "0.21" image = "0.22"
rayon = "1" rayon = "1"
errors = { path = "../errors" } errors = { path = "../errors" }

View file

@ -23,7 +23,7 @@ use regex::Regex;
use errors::{Error, Result}; use errors::{Error, Result};
use utils::fs as ufs; use utils::fs as ufs;
static RESIZED_SUBDIR: &'static str = "processed_images"; static RESIZED_SUBDIR: &str = "processed_images";
lazy_static! { lazy_static! {
pub static ref RESIZED_FILENAME: Regex = pub static ref RESIZED_FILENAME: Regex =
@ -41,8 +41,8 @@ pub enum ResizeOp {
/// Scales the image to a specified height with width computed such /// Scales the image to a specified height with width computed such
/// that aspect ratio is preserved /// that aspect ratio is preserved
FitHeight(u32), FitHeight(u32),
/// Scales the image such that it fits within the specified width and /// If the image is larger than the specified width or height, scales the image such
/// height preserving aspect ratio. /// that it fits within the specified width and height preserving aspect ratio.
/// Either dimension may end up being smaller, but never larger than specified. /// Either dimension may end up being smaller, but never larger than specified.
Fit(u32, u32), Fit(u32, u32),
/// Scales the image such that it fills the specified width and height. /// Scales the image such that it fills the specified width and height.
@ -129,6 +129,7 @@ impl From<ResizeOp> for u8 {
} }
} }
#[allow(clippy::derive_hash_xor_eq)]
impl Hash for ResizeOp { impl Hash for ResizeOp {
fn hash<H: Hasher>(&self, hasher: &mut H) { fn hash<H: Hasher>(&self, hasher: &mut H) {
hasher.write_u8(u8::from(*self)); hasher.write_u8(u8::from(*self));
@ -194,6 +195,7 @@ impl Format {
} }
} }
#[allow(clippy::derive_hash_xor_eq)]
impl Hash for Format { impl Hash for Format {
fn hash<H: Hasher>(&self, hasher: &mut H) { fn hash<H: Hasher>(&self, hasher: &mut H) {
use Format::*; use Format::*;
@ -264,7 +266,13 @@ impl ImageOp {
Scale(w, h) => img.resize_exact(w, h, RESIZE_FILTER), Scale(w, h) => img.resize_exact(w, h, RESIZE_FILTER),
FitWidth(w) => img.resize(w, u32::max_value(), RESIZE_FILTER), FitWidth(w) => img.resize(w, u32::max_value(), RESIZE_FILTER),
FitHeight(h) => img.resize(u32::max_value(), h, RESIZE_FILTER), FitHeight(h) => img.resize(u32::max_value(), h, RESIZE_FILTER),
Fit(w, h) => img.resize(w, h, RESIZE_FILTER), Fit(w, h) => {
if img_w > w || img_h > h {
img.resize(w, h, RESIZE_FILTER)
} else {
img
}
},
Fill(w, h) => { Fill(w, h) => {
let factor_w = img_w as f32 / w as f32; let factor_w = img_w as f32 / w as f32;
let factor_h = img_h as f32 / h as f32; let factor_h = img_h as f32 / h as f32;
@ -300,7 +308,7 @@ impl ImageOp {
match self.format { match self.format {
Format::Png => { Format::Png => {
let mut enc = PNGEncoder::new(&mut f); let enc = PNGEncoder::new(&mut f);
enc.encode(&img.raw_pixels(), img_w, img_h, img.color())?; enc.encode(&img.raw_pixels(), img_w, img_h, img.color())?;
} }
Format::Jpeg(q) => { Format::Jpeg(q) => {

View file

@ -4,7 +4,7 @@ version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
[dependencies] [dependencies]
slotmap = "0.2" slotmap = "0.4"
rayon = "1" rayon = "1"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
tera = "1.0.0-beta.10" tera = "1.0.0-beta.10"
@ -22,5 +22,5 @@ errors = { path = "../errors" }
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"
toml = "0.4" toml = "0.5"
globset = "0.4" globset = "0.4"

View file

@ -56,9 +56,8 @@ impl FileInfo {
let file_path = path.to_path_buf(); let file_path = path.to_path_buf();
let mut parent = file_path.parent().expect("Get parent of page").to_path_buf(); let mut parent = file_path.parent().expect("Get parent of page").to_path_buf();
let name = path.file_stem().unwrap().to_string_lossy().to_string(); let name = path.file_stem().unwrap().to_string_lossy().to_string();
let mut components = find_content_components( let mut components =
&file_path.strip_prefix(base_path).expect("Strip base path prefix for page"), find_content_components(&file_path.strip_prefix(base_path).unwrap_or(&file_path));
);
let relative = if !components.is_empty() { let relative = if !components.is_empty() {
format!("{}/{}.md", components.join("/"), name) format!("{}/{}.md", components.join("/"), name)
} else { } else {
@ -91,9 +90,8 @@ impl FileInfo {
let file_path = path.to_path_buf(); let file_path = path.to_path_buf();
let parent = path.parent().expect("Get parent of section").to_path_buf(); let parent = path.parent().expect("Get parent of section").to_path_buf();
let name = path.file_stem().unwrap().to_string_lossy().to_string(); let name = path.file_stem().unwrap().to_string_lossy().to_string();
let components = find_content_components( let components =
&file_path.strip_prefix(base_path).expect("Strip base path prefix for section"), find_content_components(&file_path.strip_prefix(base_path).unwrap_or(&file_path));
);
let relative = if !components.is_empty() { let relative = if !components.is_empty() {
format!("{}/{}.md", components.join("/"), name) format!("{}/{}.md", components.join("/"), name)
} else { } else {
@ -196,7 +194,7 @@ mod tests {
#[test] #[test]
fn can_find_valid_language_in_page() { fn can_find_valid_language_in_page() {
let mut config = Config::default(); let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false }); config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
let mut file = FileInfo::new_page( let mut file = FileInfo::new_page(
&Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md"), &Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md"),
&PathBuf::new(), &PathBuf::new(),
@ -209,7 +207,7 @@ mod tests {
#[test] #[test]
fn can_find_valid_language_in_page_with_assets() { fn can_find_valid_language_in_page_with_assets() {
let mut config = Config::default(); let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false }); config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
let mut file = FileInfo::new_page( let mut file = FileInfo::new_page(
&Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.fr.md"), &Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.fr.md"),
&PathBuf::new(), &PathBuf::new(),
@ -235,7 +233,7 @@ mod tests {
#[test] #[test]
fn errors_on_unknown_language_in_page_with_i18n_on() { fn errors_on_unknown_language_in_page_with_i18n_on() {
let mut config = Config::default(); let mut config = Config::default();
config.languages.push(Language { code: String::from("it"), rss: false }); config.languages.push(Language { code: String::from("it"), rss: false, search: false });
let mut file = FileInfo::new_page( let mut file = FileInfo::new_page(
&Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md"), &Path::new("/home/vincent/code/site/content/posts/tutorials/python.fr.md"),
&PathBuf::new(), &PathBuf::new(),
@ -247,7 +245,7 @@ mod tests {
#[test] #[test]
fn can_find_valid_language_in_section() { fn can_find_valid_language_in_section() {
let mut config = Config::default(); let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false }); config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
let mut file = FileInfo::new_section( let mut file = FileInfo::new_section(
&Path::new("/home/vincent/code/site/content/posts/tutorials/_index.fr.md"), &Path::new("/home/vincent/code/site/content/posts/tutorials/_index.fr.md"),
&PathBuf::new(), &PathBuf::new(),

View file

@ -8,9 +8,9 @@ pub use self::page::Page;
pub use self::section::Section; pub use self::section::Section;
pub use self::ser::{SerializingPage, SerializingSection}; pub use self::ser::{SerializingPage, SerializingSection};
use rendering::Header; use rendering::Heading;
pub fn has_anchor(headings: &[Header], anchor: &str) -> bool { pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool {
for heading in headings { for heading in headings {
if heading.id == anchor { if heading.id == anchor {
return true; return true;
@ -30,28 +30,28 @@ mod tests {
#[test] #[test]
fn can_find_anchor_at_root() { fn can_find_anchor_at_root() {
let input = vec![ let input = vec![
Header { Heading {
level: 1, level: 1,
id: "1".to_string(), id: "1".to_string(),
permalink: String::new(), permalink: String::new(),
title: String::new(), title: String::new(),
children: vec![], children: vec![],
}, },
Header { Heading {
level: 2, level: 2,
id: "1-1".to_string(), id: "1-1".to_string(),
permalink: String::new(), permalink: String::new(),
title: String::new(), title: String::new(),
children: vec![], children: vec![],
}, },
Header { Heading {
level: 3, level: 3,
id: "1-1-1".to_string(), id: "1-1-1".to_string(),
permalink: String::new(), permalink: String::new(),
title: String::new(), title: String::new(),
children: vec![], children: vec![],
}, },
Header { Heading {
level: 2, level: 2,
id: "1-2".to_string(), id: "1-2".to_string(),
permalink: String::new(), permalink: String::new(),
@ -65,27 +65,27 @@ mod tests {
#[test] #[test]
fn can_find_anchor_in_children() { fn can_find_anchor_in_children() {
let input = vec![Header { let input = vec![Heading {
level: 1, level: 1,
id: "1".to_string(), id: "1".to_string(),
permalink: String::new(), permalink: String::new(),
title: String::new(), title: String::new(),
children: vec![ children: vec![
Header { Heading {
level: 2, level: 2,
id: "1-1".to_string(), id: "1-1".to_string(),
permalink: String::new(), permalink: String::new(),
title: String::new(), title: String::new(),
children: vec![], children: vec![],
}, },
Header { Heading {
level: 3, level: 3,
id: "1-1-1".to_string(), id: "1-1-1".to_string(),
permalink: String::new(), permalink: String::new(),
title: String::new(), title: String::new(),
children: vec![], children: vec![],
}, },
Header { Heading {
level: 2, level: 2,
id: "1-2".to_string(), id: "1-2".to_string(),
permalink: String::new(), permalink: String::new(),

View file

@ -3,7 +3,7 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use regex::Regex; use regex::Regex;
use slotmap::Key; use slotmap::DefaultKey;
use slug::slugify; use slug::slugify;
use tera::{Context as TeraContext, Tera}; use tera::{Context as TeraContext, Tera};
@ -11,7 +11,7 @@ use config::Config;
use errors::{Error, Result}; use errors::{Error, Result};
use front_matter::{split_page_content, InsertAnchor, PageFrontMatter}; use front_matter::{split_page_content, InsertAnchor, PageFrontMatter};
use library::Library; use library::Library;
use rendering::{render_content, Header, RenderContext}; use rendering::{render_content, Heading, RenderContext};
use utils::fs::{find_related_assets, read_file}; use utils::fs::{find_related_assets, read_file};
use utils::site::get_reading_analytics; use utils::site::get_reading_analytics;
use utils::templates::render_template; use utils::templates::render_template;
@ -35,7 +35,7 @@ pub struct Page {
/// The front matter meta-data /// The front matter meta-data
pub meta: PageFrontMatter, pub meta: PageFrontMatter,
/// The list of parent sections /// The list of parent sections
pub ancestors: Vec<Key>, pub ancestors: Vec<DefaultKey>,
/// The actual content of the page, in markdown /// The actual content of the page, in markdown
pub raw_content: String, pub raw_content: String,
/// All the non-md files we found next to the .md file /// All the non-md files we found next to the .md file
@ -58,15 +58,15 @@ pub struct Page {
/// as summary /// as summary
pub summary: Option<String>, pub summary: Option<String>,
/// The earlier page, for pages sorted by date /// The earlier page, for pages sorted by date
pub earlier: Option<Key>, pub earlier: Option<DefaultKey>,
/// The later page, for pages sorted by date /// The later page, for pages sorted by date
pub later: Option<Key>, pub later: Option<DefaultKey>,
/// The lighter page, for pages sorted by weight /// The lighter page, for pages sorted by weight
pub lighter: Option<Key>, pub lighter: Option<DefaultKey>,
/// The heavier page, for pages sorted by weight /// The heavier page, for pages sorted by weight
pub heavier: Option<Key>, pub heavier: Option<DefaultKey>,
/// Toc made from the headers of the markdown file /// Toc made from the headings of the markdown file
pub toc: Vec<Header>, pub toc: Vec<Heading>,
/// How many words in the raw content /// How many words in the raw content
pub word_count: Option<usize>, pub word_count: Option<usize>,
/// How long would it take to read the raw content. /// How long would it take to read the raw content.
@ -76,7 +76,7 @@ pub struct Page {
/// Corresponds to the lang in the {slug}.{lang}.md file scheme /// Corresponds to the lang in the {slug}.{lang}.md file scheme
pub lang: String, pub lang: String,
/// Contains all the translated version of that page /// Contains all the translated version of that page
pub translations: Vec<Key>, pub translations: Vec<DefaultKey>,
/// Contains the internal links that have an anchor: we can only check the anchor /// 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 /// after all pages have been built and their ToC compiled. The page itself should exist otherwise
/// it would have errored before getting there /// it would have errored before getting there
@ -160,7 +160,7 @@ impl Page {
page.slug = { page.slug = {
if let Some(ref slug) = page.meta.slug { if let Some(ref slug) = page.meta.slug {
slug.trim().to_string() slugify(&slug.trim())
} else if page.file.name == "index" { } else if page.file.name == "index" {
if let Some(parent) = page.file.path.parent() { if let Some(parent) = page.file.path.parent() {
if let Some(slug) = slug_from_dated_filename { if let Some(slug) = slug_from_dated_filename {
@ -171,12 +171,10 @@ impl Page {
} else { } else {
slugify(&page.file.name) slugify(&page.file.name)
} }
} else if let Some(slug) = slug_from_dated_filename {
slugify(&slug)
} else { } else {
if let Some(slug) = slug_from_dated_filename { slugify(&page.file.name)
slugify(&slug)
} else {
slugify(&page.file.name)
}
} }
}; };
@ -439,6 +437,22 @@ Hello world"#;
assert_eq!(page.permalink, config.make_permalink("hello-world")); assert_eq!(page.permalink, config.make_permalink("hello-world"));
} }
#[test]
fn can_make_url_from_slug_only_with_no_special_chars() {
let content = r#"
+++
slug = "hello-&-world"
+++
Hello world"#;
let config = Config::default();
let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.path, "hello-world/");
assert_eq!(page.components, vec!["hello-world"]);
assert_eq!(page.permalink, config.make_permalink("hello-world"));
}
#[test] #[test]
fn can_make_url_from_path() { fn can_make_url_from_path() {
let content = r#" let content = r#"
@ -722,7 +736,7 @@ Hello world
#[test] #[test]
fn can_specify_language_in_filename() { fn can_specify_language_in_filename() {
let mut config = Config::default(); let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false }); config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
let content = r#" let content = r#"
+++ +++
+++ +++
@ -739,7 +753,7 @@ Bonjour le monde"#
#[test] #[test]
fn can_specify_language_in_filename_with_date() { fn can_specify_language_in_filename_with_date() {
let mut config = Config::default(); let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false }); config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
let content = r#" let content = r#"
+++ +++
+++ +++
@ -758,7 +772,7 @@ Bonjour le monde"#
#[test] #[test]
fn i18n_frontmatter_path_overrides_default_permalink() { fn i18n_frontmatter_path_overrides_default_permalink() {
let mut config = Config::default(); let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false }); config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
let content = r#" let content = r#"
+++ +++
path = "bonjour" path = "bonjour"

View file

@ -1,13 +1,13 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use slotmap::Key; use slotmap::DefaultKey;
use tera::{Context as TeraContext, Tera}; use tera::{Context as TeraContext, Tera};
use config::Config; use config::Config;
use errors::{Error, Result}; use errors::{Error, Result};
use front_matter::{split_section_content, SectionFrontMatter}; use front_matter::{split_section_content, SectionFrontMatter};
use rendering::{render_content, Header, RenderContext}; use rendering::{render_content, Heading, RenderContext};
use utils::fs::{find_related_assets, read_file}; use utils::fs::{find_related_assets, read_file};
use utils::site::get_reading_analytics; use utils::site::get_reading_analytics;
use utils::templates::render_template; use utils::templates::render_template;
@ -38,15 +38,15 @@ pub struct Section {
/// All the non-md files we found next to the .md file as string for use in templates /// All the non-md files we found next to the .md file as string for use in templates
pub serialized_assets: Vec<String>, pub serialized_assets: Vec<String>,
/// All direct pages of that section /// All direct pages of that section
pub pages: Vec<Key>, pub pages: Vec<DefaultKey>,
/// All pages that cannot be sorted in this section /// All pages that cannot be sorted in this section
pub ignored_pages: Vec<Key>, pub ignored_pages: Vec<DefaultKey>,
/// The list of parent sections /// The list of parent sections
pub ancestors: Vec<Key>, pub ancestors: Vec<DefaultKey>,
/// All direct subsections /// All direct subsections
pub subsections: Vec<Key>, pub subsections: Vec<DefaultKey>,
/// Toc made from the headers of the markdown file /// Toc made from the headings of the markdown file
pub toc: Vec<Header>, pub toc: Vec<Heading>,
/// How many words in the raw content /// How many words in the raw content
pub word_count: Option<usize>, pub word_count: Option<usize>,
/// How long would it take to read the raw content. /// How long would it take to read the raw content.
@ -56,7 +56,7 @@ pub struct Section {
/// Corresponds to the lang in the _index.{lang}.md file scheme /// Corresponds to the lang in the _index.{lang}.md file scheme
pub lang: String, pub lang: String,
/// Contains all the translated version of that section /// Contains all the translated version of that section
pub translations: Vec<Key>, pub translations: Vec<DefaultKey>,
/// Contains the internal links that have an anchor: we can only check the anchor /// 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 /// after all pages have been built and their ToC compiled. The page itself should exist otherwise
/// it would have errored before getting there /// it would have errored before getting there
@ -113,7 +113,11 @@ impl Section {
section.reading_time = Some(reading_time); section.reading_time = Some(reading_time);
let path = section.file.components.join("/"); let path = section.file.components.join("/");
if section.lang != config.default_language { if section.lang != config.default_language {
section.path = format!("{}/{}", section.lang, path); if path.is_empty() {
section.path = format!("{}/", section.lang);
} else {
section.path = format!("{}/{}/", section.lang, path);
}
} else { } else {
section.path = format!("{}/", path); section.path = format!("{}/", path);
} }
@ -346,7 +350,7 @@ mod tests {
#[test] #[test]
fn can_specify_language_in_filename() { fn can_specify_language_in_filename() {
let mut config = Config::default(); let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false }); config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
let content = r#" let content = r#"
+++ +++
+++ +++
@ -368,7 +372,7 @@ Bonjour le monde"#
#[test] #[test]
fn can_make_links_to_translated_sections_without_double_trailing_slash() { fn can_make_links_to_translated_sections_without_double_trailing_slash() {
let mut config = Config::default(); let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false }); config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
let content = r#" let content = r#"
+++ +++
+++ +++
@ -381,4 +385,25 @@ Bonjour le monde"#
assert_eq!(section.lang, "fr".to_string()); assert_eq!(section.lang, "fr".to_string());
assert_eq!(section.permalink, "http://a-website.com/fr/"); assert_eq!(section.permalink, "http://a-website.com/fr/");
} }
#[test]
fn can_make_links_to_translated_subsections_with_trailing_slash() {
let mut config = Config::default();
config.languages.push(Language { code: String::from("fr"), rss: false, search: false });
let content = r#"
+++
+++
Bonjour le monde"#
.to_string();
let res = Section::parse(
Path::new("content/subcontent/_index.fr.md"),
&content,
&config,
&PathBuf::new(),
);
assert!(res.is_ok());
let section = res.unwrap();
assert_eq!(section.lang, "fr".to_string());
assert_eq!(section.permalink, "http://a-website.com/fr/subcontent/");
}
} }

View file

@ -254,23 +254,21 @@ impl<'a> SerializingSection<'a> {
} }
} }
/// Same as from_section but doesn't fetch pages and sections /// Same as from_section but doesn't fetch pages
pub fn from_section_basic(section: &'a Section, library: Option<&'a Library>) -> Self { pub fn from_section_basic(section: &'a Section, library: Option<&'a Library>) -> Self {
let ancestors = if let Some(ref lib) = library { let mut ancestors = vec![];
section let mut translations = vec![];
let mut subsections = vec![];
if let Some(ref lib) = library {
ancestors = section
.ancestors .ancestors
.iter() .iter()
.map(|k| lib.get_section_by_key(*k).file.relative.clone()) .map(|k| lib.get_section_by_key(*k).file.relative.clone())
.collect() .collect();
} else { translations = TranslatedContent::find_all_sections(section, lib);
vec![] subsections =
}; section.subsections.iter().map(|k| lib.get_section_path_by_key(*k)).collect();
}
let translations = if let Some(ref lib) = library {
TranslatedContent::find_all_sections(section, lib)
} else {
vec![]
};
SerializingSection { SerializingSection {
relative_path: &section.file.relative, relative_path: &section.file.relative,
@ -287,7 +285,7 @@ impl<'a> SerializingSection<'a> {
assets: &section.serialized_assets, assets: &section.serialized_assets,
lang: &section.lang, lang: &section.lang,
pages: vec![], pages: vec![],
subsections: vec![], subsections,
translations, translations,
} }
} }

View file

@ -1,7 +1,7 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use slotmap::{DenseSlotMap, Key}; use slotmap::{DenseSlotMap, DefaultKey};
use front_matter::SortBy; use front_matter::SortBy;
@ -19,13 +19,13 @@ use sorting::{find_siblings, sort_pages_by_date, sort_pages_by_weight};
#[derive(Debug)] #[derive(Debug)]
pub struct Library { pub struct Library {
/// All the pages of the site /// All the pages of the site
pages: DenseSlotMap<Page>, pages: DenseSlotMap<DefaultKey, Page>,
/// All the sections of the site /// All the sections of the site
sections: DenseSlotMap<Section>, sections: DenseSlotMap<DefaultKey, Section>,
/// A mapping path -> key for pages so we can easily get their key /// A mapping path -> key for pages so we can easily get their key
pub paths_to_pages: HashMap<PathBuf, Key>, pub paths_to_pages: HashMap<PathBuf, DefaultKey>,
/// A mapping path -> key for sections so we can easily get their key /// A mapping path -> key for sections so we can easily get their key
pub paths_to_sections: HashMap<PathBuf, Key>, pub paths_to_sections: HashMap<PathBuf, DefaultKey>,
/// Whether we need to look for translations /// Whether we need to look for translations
is_multilingual: bool, is_multilingual: bool,
} }
@ -42,7 +42,7 @@ impl Library {
} }
/// Add a section and return its Key /// Add a section and return its Key
pub fn insert_section(&mut self, section: Section) -> Key { pub fn insert_section(&mut self, section: Section) -> DefaultKey {
let path = section.file.path.clone(); let path = section.file.path.clone();
let key = self.sections.insert(section); let key = self.sections.insert(section);
self.paths_to_sections.insert(path, key); self.paths_to_sections.insert(path, key);
@ -50,18 +50,18 @@ impl Library {
} }
/// Add a page and return its Key /// Add a page and return its Key
pub fn insert_page(&mut self, page: Page) -> Key { pub fn insert_page(&mut self, page: Page) -> DefaultKey {
let path = page.file.path.clone(); let path = page.file.path.clone();
let key = self.pages.insert(page); let key = self.pages.insert(page);
self.paths_to_pages.insert(path, key); self.paths_to_pages.insert(path, key);
key key
} }
pub fn pages(&self) -> &DenseSlotMap<Page> { pub fn pages(&self) -> &DenseSlotMap<DefaultKey, Page> {
&self.pages &self.pages
} }
pub fn pages_mut(&mut self) -> &mut DenseSlotMap<Page> { pub fn pages_mut(&mut self) -> &mut DenseSlotMap<DefaultKey, Page> {
&mut self.pages &mut self.pages
} }
@ -69,11 +69,11 @@ impl Library {
self.pages.values().collect::<Vec<_>>() self.pages.values().collect::<Vec<_>>()
} }
pub fn sections(&self) -> &DenseSlotMap<Section> { pub fn sections(&self) -> &DenseSlotMap<DefaultKey, Section> {
&self.sections &self.sections
} }
pub fn sections_mut(&mut self) -> &mut DenseSlotMap<Section> { pub fn sections_mut(&mut self) -> &mut DenseSlotMap<DefaultKey, Section> {
&mut self.sections &mut self.sections
} }
@ -139,7 +139,7 @@ impl Library {
let parent_is_transparent; let parent_is_transparent;
// We need to get a reference to a section later so keep the scope of borrowing small // We need to get a reference to a section later so keep the scope of borrowing small
{ {
let mut section = self.sections.get_mut(*section_key).unwrap(); let section = self.sections.get_mut(*section_key).unwrap();
section.pages.push(key); section.pages.push(key);
parent_is_transparent = section.meta.transparent; parent_is_transparent = section.meta.transparent;
} }
@ -236,18 +236,7 @@ impl Library {
for (key, (sorted, cannot_be_sorted, sort_by)) in updates { for (key, (sorted, cannot_be_sorted, sort_by)) in updates {
// Find sibling between sorted pages first // Find sibling between sorted pages first
let with_siblings = find_siblings( let with_siblings = find_siblings(&sorted);
sorted
.iter()
.map(|k| {
if let Some(page) = self.pages.get(*k) {
(k, page.is_draft())
} else {
unreachable!("Sorting got an unknown page")
}
})
.collect(),
);
for (k2, val1, val2) in with_siblings { for (k2, val1, val2) in with_siblings {
if let Some(page) = self.pages.get_mut(k2) { if let Some(page) = self.pages.get_mut(k2) {
@ -347,7 +336,7 @@ impl Library {
} }
/// Only used in tests /// Only used in tests
pub fn get_section_key<P: AsRef<Path>>(&self, path: P) -> Option<&Key> { pub fn get_section_key<P: AsRef<Path>>(&self, path: P) -> Option<&DefaultKey> {
self.paths_to_sections.get(path.as_ref()) self.paths_to_sections.get(path.as_ref())
} }
@ -360,15 +349,15 @@ impl Library {
.get_mut(self.paths_to_sections.get(path.as_ref()).cloned().unwrap_or_default()) .get_mut(self.paths_to_sections.get(path.as_ref()).cloned().unwrap_or_default())
} }
pub fn get_section_by_key(&self, key: Key) -> &Section { pub fn get_section_by_key(&self, key: DefaultKey) -> &Section {
self.sections.get(key).unwrap() self.sections.get(key).unwrap()
} }
pub fn get_section_mut_by_key(&mut self, key: Key) -> &mut Section { pub fn get_section_mut_by_key(&mut self, key: DefaultKey) -> &mut Section {
self.sections.get_mut(key).unwrap() self.sections.get_mut(key).unwrap()
} }
pub fn get_section_path_by_key(&self, key: Key) -> &str { pub fn get_section_path_by_key(&self, key: DefaultKey) -> &str {
&self.get_section_by_key(key).file.relative &self.get_section_by_key(key).file.relative
} }
@ -376,11 +365,11 @@ impl Library {
self.pages.get(self.paths_to_pages.get(path.as_ref()).cloned().unwrap_or_default()) self.pages.get(self.paths_to_pages.get(path.as_ref()).cloned().unwrap_or_default())
} }
pub fn get_page_by_key(&self, key: Key) -> &Page { pub fn get_page_by_key(&self, key: DefaultKey) -> &Page {
self.pages.get(key).unwrap() self.pages.get(key).unwrap()
} }
pub fn get_page_mut_by_key(&mut self, key: Key) -> &mut Page { pub fn get_page_mut_by_key(&mut self, key: DefaultKey) -> &mut Page {
self.pages.get_mut(key).unwrap() self.pages.get_mut(key).unwrap()
} }

View file

@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use slotmap::Key; use slotmap::DefaultKey;
use tera::{to_value, Context, Tera, Value}; use tera::{to_value, Context, Tera, Value};
use config::Config; use config::Config;
@ -44,7 +44,7 @@ impl<'a> Pager<'a> {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Paginator<'a> { pub struct Paginator<'a> {
/// All pages in the section/taxonomy /// All pages in the section/taxonomy
all_pages: &'a [Key], all_pages: &'a [DefaultKey],
/// Pages split in chunks of `paginate_by` /// Pages split in chunks of `paginate_by`
pub pagers: Vec<Pager<'a>>, pub pagers: Vec<Pager<'a>>,
/// How many content pages on a paginated page at max /// How many content pages on a paginated page at max
@ -117,9 +117,6 @@ impl<'a> Paginator<'a> {
for key in self.all_pages { for key in self.all_pages {
let page = library.get_page_by_key(*key); let page = library.get_page_by_key(*key);
if page.is_draft() {
continue;
}
current_page.push(page.to_serialized_basic(library)); current_page.push(page.to_serialized_basic(library));
if current_page.len() == self.paginate_by { if current_page.len() == self.paginate_by {
@ -211,10 +208,12 @@ impl<'a> Paginator<'a> {
PaginationRoot::Section(s) => { PaginationRoot::Section(s) => {
context context
.insert("section", &SerializingSection::from_section_basic(s, Some(library))); .insert("section", &SerializingSection::from_section_basic(s, Some(library)));
context.insert("lang", &s.lang);
} }
PaginationRoot::Taxonomy(t, item) => { PaginationRoot::Taxonomy(t, item) => {
context.insert("taxonomy", &t.kind); context.insert("taxonomy", &t.kind);
context.insert("term", &item.serialize(library)); context.insert("term", &item.serialize(library));
context.insert("lang", &t.kind.lang);
} }
}; };
context.insert("current_url", &pager.permalink); context.insert("current_url", &pager.permalink);
@ -281,7 +280,7 @@ mod tests {
assert_eq!(paginator.pagers[0].path, "posts/"); assert_eq!(paginator.pagers[0].path, "posts/");
assert_eq!(paginator.pagers[1].index, 2); assert_eq!(paginator.pagers[1].index, 2);
assert_eq!(paginator.pagers[1].pages.len(), 1); assert_eq!(paginator.pagers[1].pages.len(), 2);
assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/page/2/"); assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/page/2/");
assert_eq!(paginator.pagers[1].path, "posts/page/2/"); assert_eq!(paginator.pagers[1].path, "posts/page/2/");
} }
@ -298,7 +297,7 @@ mod tests {
assert_eq!(paginator.pagers[0].path, ""); assert_eq!(paginator.pagers[0].path, "");
assert_eq!(paginator.pagers[1].index, 2); assert_eq!(paginator.pagers[1].index, 2);
assert_eq!(paginator.pagers[1].pages.len(), 1); assert_eq!(paginator.pagers[1].pages.len(), 2);
assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/page/2/"); assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/page/2/");
assert_eq!(paginator.pagers[1].path, "page/2/"); assert_eq!(paginator.pagers[1].path, "page/2/");
} }
@ -350,7 +349,7 @@ mod tests {
assert_eq!(paginator.pagers[0].path, "tags/something"); assert_eq!(paginator.pagers[0].path, "tags/something");
assert_eq!(paginator.pagers[1].index, 2); assert_eq!(paginator.pagers[1].index, 2);
assert_eq!(paginator.pagers[1].pages.len(), 1); assert_eq!(paginator.pagers[1].pages.len(), 2);
assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/tags/something/page/2/"); assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/tags/something/page/2/");
assert_eq!(paginator.pagers[1].path, "tags/something/page/2/"); assert_eq!(paginator.pagers[1].path, "tags/something/page/2/");
} }

View file

@ -2,12 +2,13 @@ use std::cmp::Ordering;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use rayon::prelude::*; use rayon::prelude::*;
use slotmap::Key; use slotmap::DefaultKey;
use content::Page; use content::Page;
/// Used by the RSS feed /// Used by the RSS feed
/// There to not have to import sorting stuff in the site crate /// There to not have to import sorting stuff in the site crate
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn sort_actual_pages_by_date(a: &&Page, b: &&Page) -> Ordering { pub fn sort_actual_pages_by_date(a: &&Page, b: &&Page) -> Ordering {
let ord = b.meta.datetime.unwrap().cmp(&a.meta.datetime.unwrap()); let ord = b.meta.datetime.unwrap().cmp(&a.meta.datetime.unwrap());
if ord == Ordering::Equal { if ord == Ordering::Equal {
@ -20,7 +21,7 @@ pub fn sort_actual_pages_by_date(a: &&Page, b: &&Page) -> Ordering {
/// Takes a list of (page key, date, permalink) and sort them by dates if possible /// Takes a list of (page key, date, permalink) and sort them by dates if possible
/// Pages without date will be put in the unsortable bucket /// Pages without date will be put in the unsortable bucket
/// The permalink is used to break ties /// The permalink is used to break ties
pub fn sort_pages_by_date(pages: Vec<(&Key, Option<NaiveDateTime>, &str)>) -> (Vec<Key>, Vec<Key>) { pub fn sort_pages_by_date(pages: Vec<(&DefaultKey, Option<NaiveDateTime>, &str)>) -> (Vec<DefaultKey>, Vec<DefaultKey>) {
let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) = let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) =
pages.into_par_iter().partition(|page| page.1.is_some()); pages.into_par_iter().partition(|page| page.1.is_some());
@ -39,7 +40,7 @@ pub fn sort_pages_by_date(pages: Vec<(&Key, Option<NaiveDateTime>, &str)>) -> (V
/// Takes a list of (page key, weight, permalink) and sort them by weight if possible /// Takes a list of (page key, weight, permalink) and sort them by weight if possible
/// Pages without weight will be put in the unsortable bucket /// Pages without weight will be put in the unsortable bucket
/// The permalink is used to break ties /// The permalink is used to break ties
pub fn sort_pages_by_weight(pages: Vec<(&Key, Option<usize>, &str)>) -> (Vec<Key>, Vec<Key>) { pub fn sort_pages_by_weight(pages: Vec<(&DefaultKey, Option<usize>, &str)>) -> (Vec<DefaultKey>, Vec<DefaultKey>) {
let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) = let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) =
pages.into_par_iter().partition(|page| page.1.is_some()); pages.into_par_iter().partition(|page| page.1.is_some());
@ -56,53 +57,21 @@ pub fn sort_pages_by_weight(pages: Vec<(&Key, Option<usize>, &str)>) -> (Vec<Key
} }
/// Find the lighter/heavier and earlier/later pages for all pages having a date/weight /// Find the lighter/heavier and earlier/later pages for all pages having a date/weight
/// and that are not drafts. pub fn find_siblings(sorted: &[DefaultKey]) -> Vec<(DefaultKey, Option<DefaultKey>, Option<DefaultKey>)> {
pub fn find_siblings(sorted: Vec<(&Key, bool)>) -> Vec<(Key, Option<Key>, Option<Key>)> {
let mut res = Vec::with_capacity(sorted.len()); let mut res = Vec::with_capacity(sorted.len());
let length = sorted.len(); let length = sorted.len();
for (i, (key, is_draft)) in sorted.iter().enumerate() { for (i, key) in sorted.iter().enumerate() {
if *is_draft { let mut with_siblings = (*key, None, None);
res.push((**key, None, None));
continue;
}
let mut with_siblings = (**key, None, None);
if i > 0 { if i > 0 {
let mut j = i; // lighter / later
loop { with_siblings.1 = Some(sorted[i - 1]);
if j == 0 {
break;
}
j -= 1;
if sorted[j].1 {
continue;
}
// lighter / later
with_siblings.1 = Some(*sorted[j].0);
break;
}
} }
if i < length - 1 { if i < length - 1 {
let mut j = i; // heavier/earlier
loop { with_siblings.2 = Some(sorted[i + 1]);
if j == length - 1 {
break;
}
j += 1;
if sorted[j].1 {
continue;
}
// heavier/earlier
with_siblings.2 = Some(*sorted[j].0);
break;
}
} }
res.push(with_siblings); res.push(with_siblings);
} }
@ -207,10 +176,9 @@ mod tests {
let page3 = create_page_with_weight(3); let page3 = create_page_with_weight(3);
let key3 = dense.insert(page3.clone()); let key3 = dense.insert(page3.clone());
let input = let input = vec![key1, key2, key3];
vec![(&key1, page1.is_draft()), (&key2, page2.is_draft()), (&key3, page3.is_draft())];
let pages = find_siblings(input); let pages = find_siblings(&input);
assert_eq!(pages[0].1, None); assert_eq!(pages[0].1, None);
assert_eq!(pages[0].2, Some(key2)); assert_eq!(pages[0].2, Some(key2));

View file

@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use slotmap::Key; use slotmap::DefaultKey;
use slug::slugify; use slug::slugify;
use tera::{Context, Tera}; use tera::{Context, Tera};
@ -44,7 +44,7 @@ pub struct TaxonomyItem {
pub name: String, pub name: String,
pub slug: String, pub slug: String,
pub permalink: String, pub permalink: String,
pub pages: Vec<Key>, pub pages: Vec<DefaultKey>,
} }
impl TaxonomyItem { impl TaxonomyItem {
@ -52,7 +52,7 @@ impl TaxonomyItem {
name: &str, name: &str,
taxonomy: &TaxonomyConfig, taxonomy: &TaxonomyConfig,
config: &Config, config: &Config,
keys: Vec<Key>, keys: Vec<DefaultKey>,
library: &Library, library: &Library,
) -> Self { ) -> Self {
// Taxonomy are almost always used for blogs so we filter by dates // Taxonomy are almost always used for blogs so we filter by dates
@ -113,7 +113,7 @@ impl Taxonomy {
fn new( fn new(
kind: TaxonomyConfig, kind: TaxonomyConfig,
config: &Config, config: &Config,
items: HashMap<String, Vec<Key>>, items: HashMap<String, Vec<DefaultKey>>,
library: &Library, library: &Library,
) -> Taxonomy { ) -> Taxonomy {
let mut sorted_items = vec![]; let mut sorted_items = vec![];
@ -142,6 +142,7 @@ impl Taxonomy {
) -> Result<String> { ) -> Result<String> {
let mut context = Context::new(); let mut context = Context::new();
context.insert("config", config); context.insert("config", config);
context.insert("lang", &self.kind.lang);
context.insert("term", &SerializedTaxonomyItem::from_item(item, library)); context.insert("term", &SerializedTaxonomyItem::from_item(item, library));
context.insert("taxonomy", &self.kind); context.insert("taxonomy", &self.kind);
context.insert( context.insert(
@ -168,6 +169,7 @@ impl Taxonomy {
self.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect(); self.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect();
context.insert("terms", &terms); context.insert("terms", &terms);
context.insert("taxonomy", &self.kind); context.insert("taxonomy", &self.kind);
context.insert("lang", &self.kind.lang);
context.insert("current_url", &config.make_permalink(&self.kind.name)); context.insert("current_url", &config.make_permalink(&self.kind.name));
context.insert("current_path", &self.kind.name); context.insert("current_path", &self.kind.name);
@ -186,33 +188,21 @@ pub fn find_taxonomies(config: &Config, library: &Library) -> Result<Vec<Taxonom
let taxonomies_def = { let taxonomies_def = {
let mut m = HashMap::new(); let mut m = HashMap::new();
for t in &config.taxonomies { for t in &config.taxonomies {
m.insert(t.name.clone(), t); m.insert(format!("{}-{}", t.name, t.lang), t);
} }
m m
}; };
let mut all_taxonomies = HashMap::new(); let mut all_taxonomies = HashMap::new();
for (key, page) in library.pages() { for (key, page) in library.pages() {
// Draft are not part of taxonomies
if page.is_draft() {
continue;
}
for (name, val) in &page.meta.taxonomies { for (name, val) in &page.meta.taxonomies {
if taxonomies_def.contains_key(name) { let taxo_key = format!("{}-{}", name, page.lang);
if taxonomies_def[name].lang != page.lang { if taxonomies_def.contains_key(&taxo_key) {
bail!( all_taxonomies.entry(taxo_key.clone()).or_insert_with(HashMap::new);
"Page `{}` has taxonomy `{}` which is not available in that language",
page.file.path.display(),
name
);
}
all_taxonomies.entry(name).or_insert_with(HashMap::new);
for v in val { for v in val {
all_taxonomies all_taxonomies
.get_mut(name) .get_mut(&taxo_key)
.unwrap() .unwrap()
.entry(v.to_string()) .entry(v.to_string())
.or_insert_with(|| vec![]) .or_insert_with(|| vec![])
@ -231,7 +221,7 @@ pub fn find_taxonomies(config: &Config, library: &Library) -> Result<Vec<Taxonom
let mut taxonomies = vec![]; let mut taxonomies = vec![];
for (name, taxo) in all_taxonomies { for (name, taxo) in all_taxonomies {
taxonomies.push(Taxonomy::new(taxonomies_def[name].clone(), config, taxo, library)); taxonomies.push(Taxonomy::new(taxonomies_def[&name].clone(), config, taxo, library));
} }
Ok(taxonomies) Ok(taxonomies)
@ -371,7 +361,7 @@ mod tests {
#[test] #[test]
fn can_make_taxonomies_in_multiple_languages() { fn can_make_taxonomies_in_multiple_languages() {
let mut config = Config::default(); let mut config = Config::default();
config.languages.push(Language { rss: false, code: "fr".to_string() }); config.languages.push(Language { rss: false, code: "fr".to_string(), search: false });
let mut library = Library::new(2, 0, true); let mut library = Library::new(2, 0, true);
config.taxonomies = vec![ config.taxonomies = vec![
@ -390,6 +380,11 @@ mod tests {
lang: "fr".to_string(), lang: "fr".to_string(),
..TaxonomyConfig::default() ..TaxonomyConfig::default()
}, },
TaxonomyConfig {
name: "tags".to_string(),
lang: "fr".to_string(),
..TaxonomyConfig::default()
},
]; ];
let mut page1 = Page::default(); let mut page1 = Page::default();
@ -411,6 +406,7 @@ mod tests {
let mut page3 = Page::default(); let mut page3 = Page::default();
page3.lang = "fr".to_string(); page3.lang = "fr".to_string();
let mut taxo_page3 = HashMap::new(); let mut taxo_page3 = HashMap::new();
taxo_page3.insert("tags".to_string(), vec!["rust".to_string()]);
taxo_page3.insert("auteurs".to_string(), vec!["Vincent Prouillet".to_string()]); taxo_page3.insert("auteurs".to_string(), vec!["Vincent Prouillet".to_string()]);
page3.meta.taxonomies = taxo_page3; page3.meta.taxonomies = taxo_page3;
library.insert_page(page3); library.insert_page(page3);
@ -422,7 +418,11 @@ mod tests {
let mut a = None; let mut a = None;
for x in taxonomies { for x in taxonomies {
match x.kind.name.as_ref() { match x.kind.name.as_ref() {
"tags" => t = Some(x), "tags" => {
if x.kind.lang == "en" {
t = Some(x)
}
}
"categories" => c = Some(x), "categories" => c = Some(x),
"auteurs" => a = Some(x), "auteurs" => a = Some(x),
_ => unreachable!(), _ => unreachable!(),
@ -466,30 +466,4 @@ mod tests {
); );
assert_eq!(categories.items[1].pages.len(), 1); assert_eq!(categories.items[1].pages.len(), 1);
} }
#[test]
fn errors_on_taxonomy_of_different_language() {
let mut config = Config::default();
config.languages.push(Language { rss: false, code: "fr".to_string() });
let mut library = Library::new(2, 0, false);
config.taxonomies =
vec![TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }];
let mut page1 = Page::default();
page1.lang = "fr".to_string();
let mut taxo_page1 = HashMap::new();
taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
page1.meta.taxonomies = taxo_page1;
library.insert_page(page1);
let taxonomies = find_taxonomies(&config, &library);
assert!(taxonomies.is_err());
let err = taxonomies.unwrap_err();
// no path as this is created by Default
assert_eq!(
format!("{}", err),
"Page `` has taxonomy `tags` which is not available in that language"
);
}
} }

View file

@ -6,3 +6,5 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
[dependencies] [dependencies]
reqwest = "0.9" reqwest = "0.9"
lazy_static = "1" lazy_static = "1"
errors = { path = "../errors" }

View file

@ -2,8 +2,13 @@ extern crate reqwest;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
extern crate errors;
use reqwest::header::{HeaderMap, ACCEPT}; use reqwest::header::{HeaderMap, ACCEPT};
use reqwest::StatusCode; use reqwest::StatusCode;
use errors::Result;
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
@ -62,6 +67,12 @@ pub fn check_url(url: &str) -> LinkResult {
// Need to actually do the link checking // Need to actually do the link checking
let res = match client.get(url).headers(headers).send() { let res = match client.get(url).headers(headers).send() {
Ok(ref mut response) if has_anchor(url) => {
match check_page_for_anchor(url, response.text()) {
Ok(_) => LinkResult { code: Some(response.status()), error: None },
Err(e) => LinkResult { code: None, error: Some(e.to_string()) },
}
}
Ok(response) => LinkResult { code: Some(response.status()), error: None }, Ok(response) => LinkResult { code: Some(response.status()), error: None },
Err(e) => LinkResult { code: None, error: Some(e.description().to_string()) }, Err(e) => LinkResult { code: None, error: Some(e.description().to_string()) },
}; };
@ -70,9 +81,37 @@ pub fn check_url(url: &str) -> LinkResult {
res res
} }
fn has_anchor(url: &str) -> bool {
match url.find('#') {
Some(index) => match url.get(index..=index + 1) {
Some("#/") | Some("#!") | None => false,
Some(_) => true,
},
None => false,
}
}
fn check_page_for_anchor(url: &str, body: reqwest::Result<String>) -> Result<()> {
let body = body.unwrap();
let index = url.find('#').unwrap();
let anchor = url.get(index + 1..).unwrap();
let checks: [String; 4] = [
format!(" id='{}'", anchor),
format!(r#" id="{}""#, anchor),
format!(" name='{}'", anchor),
format!(r#" name="{}""#, anchor),
];
if checks.iter().any(|check| body[..].contains(&check[..])) {
Ok(())
} else {
Err(errors::Error::from(format!("Anchor `#{}` not found on page", anchor)))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{check_url, LINKS}; use super::{check_page_for_anchor, check_url, has_anchor, LINKS};
#[test] #[test]
fn can_validate_ok_links() { fn can_validate_ok_links() {
@ -91,4 +130,64 @@ mod tests {
assert!(res.code.is_none()); assert!(res.code.is_none());
assert!(res.error.is_some()); assert!(res.error.is_some());
} }
#[test]
fn can_validate_anchors() {
let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect";
let body = "<body><h3 id='method.collect'>collect</h3></body>".to_string();
let res = check_page_for_anchor(url, Ok(body));
assert!(res.is_ok());
}
#[test]
fn can_validate_anchors_with_other_quotes() {
let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect";
let body = r#"<body><h3 id="method.collect">collect</h3></body>"#.to_string();
let res = check_page_for_anchor(url, Ok(body));
assert!(res.is_ok());
}
#[test]
fn can_validate_anchors_with_name_attr() {
let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect";
let body = r#"<body><h3 name="method.collect">collect</h3></body>"#.to_string();
let res = check_page_for_anchor(url, Ok(body));
assert!(res.is_ok());
}
#[test]
fn can_fail_when_anchor_not_found() {
let url = "https://doc.rust-lang.org/std/iter/trait.Iterator.html#me";
let body = "<body><h3 id='method.collect'>collect</h3></body>".to_string();
let res = check_page_for_anchor(url, Ok(body));
assert!(res.is_err());
}
#[test]
fn can_check_url_for_anchor() {
let url = "https://doc.rust-lang.org/std/index.html#the-rust-standard-library";
let res = has_anchor(url);
assert_eq!(res, true);
}
#[test]
fn will_return_false_when_no_anchor() {
let url = "https://doc.rust-lang.org/std/index.html";
let res = has_anchor(url);
assert_eq!(res, false);
}
#[test]
fn will_return_false_when_has_router_url() {
let url = "https://doc.rust-lang.org/#/std";
let res = has_anchor(url);
assert_eq!(res, false);
}
#[test]
fn will_return_false_when_has_router_url_alt() {
let url = "https://doc.rust-lang.org/#!/std";
let res = has_anchor(url);
assert_eq!(res, false);
}
} }

View file

@ -137,6 +137,7 @@ fn handle_section_editing(site: &mut Site, path: &Path) -> Result<()> {
// Updating a section // Updating a section
Some(prev) => { Some(prev) => {
site.populate_sections(); site.populate_sections();
site.process_images()?;
{ {
let library = site.library.read().unwrap(); let library = site.library.read().unwrap();
@ -177,6 +178,7 @@ fn handle_section_editing(site: &mut Site, path: &Path) -> Result<()> {
// New section, only render that one // New section, only render that one
None => { None => {
site.populate_sections(); site.populate_sections();
site.process_images()?;
site.register_tera_global_fns(); site.register_tera_global_fns();
site.render_section(&site.library.read().unwrap().get_section(&pathbuf).unwrap(), true) site.render_section(&site.library.read().unwrap().get_section(&pathbuf).unwrap(), true)
} }
@ -201,6 +203,7 @@ fn handle_page_editing(site: &mut Site, path: &Path) -> Result<()> {
site.populate_sections(); site.populate_sections();
site.populate_taxonomies()?; site.populate_taxonomies()?;
site.register_tera_global_fns(); site.register_tera_global_fns();
site.process_images()?;
{ {
let library = site.library.read().unwrap(); let library = site.library.read().unwrap();
@ -249,6 +252,7 @@ fn handle_page_editing(site: &mut Site, path: &Path) -> Result<()> {
site.populate_taxonomies()?; site.populate_taxonomies()?;
site.register_early_global_fns(); site.register_early_global_fns();
site.register_tera_global_fns(); site.register_tera_global_fns();
site.process_images()?;
// No need to optimise that yet, we can revisit if it becomes an issue // No need to optimise that yet, we can revisit if it becomes an issue
site.build() site.build()
} }
@ -306,12 +310,41 @@ pub fn after_content_rename(site: &mut Site, old: &Path, new: &Path) -> Result<(
old.to_path_buf() old.to_path_buf()
}; };
site.library.write().unwrap().remove_page(&old_path); site.library.write().unwrap().remove_page(&old_path);
handle_page_editing(site, &new_path)
let ignored_content_globset = site.config.ignored_content_globset.clone();
let is_ignored_file = match ignored_content_globset {
Some(gs) => gs.is_match(new),
None => false,
};
if !is_ignored_file {
return handle_page_editing(site, &new_path);
}
Ok(())
}
fn is_section(path: &str, languages_codes: &[&str]) -> bool {
if path == "_index.md" {
return true;
}
for language_code in languages_codes {
let lang_section_string = format!("_index.{}.md", language_code);
if path == lang_section_string {
return true;
}
}
return false;
} }
/// What happens when a section or a page is created/edited /// What happens when a section or a page is created/edited
pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
let is_section = path.file_name().unwrap() == "_index.md"; let is_section = {
let languages_codes = site.config.languages_codes();
is_section(path.file_name().unwrap().to_str().unwrap(), &languages_codes)
};
let is_md = path.extension().unwrap() == "md"; let is_md = path.extension().unwrap() == "md";
let index = path.parent().unwrap().join("index.md"); let index = path.parent().unwrap().join("index.md");
@ -384,14 +417,19 @@ pub fn after_template_change(site: &mut Site, path: &Path) -> Result<()> {
_ => { _ => {
// If we are updating a shortcode, re-render the markdown of all pages/site // If we are updating a shortcode, re-render the markdown of all pages/site
// because we have no clue which one needs rebuilding // because we have no clue which one needs rebuilding
// Same for the anchor-link template
// TODO: look if there the shortcode is used in the markdown instead of re-rendering // TODO: look if there the shortcode is used in the markdown instead of re-rendering
// everything // everything
if path.components().any(|x| x == Component::Normal("shortcodes".as_ref())) { if filename == "anchor-link.html"
|| path.components().any(|x| x == Component::Normal("shortcodes".as_ref()))
{
println!("Rendering markdown");
site.render_markdown()?; site.render_markdown()?;
} }
site.populate_sections(); site.populate_sections();
site.populate_taxonomies()?; site.populate_taxonomies()?;
site.render_sections()?; site.render_sections()?;
site.process_images()?;
site.render_orphan_pages()?; site.render_orphan_pages()?;
site.render_taxonomies() site.render_taxonomies()
} }

View file

@ -5,8 +5,8 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
[dependencies] [dependencies]
tera = { version = "1.0.0-beta.10", features = ["preserve_order"] } tera = { version = "1.0.0-beta.10", features = ["preserve_order"] }
syntect = "3" syntect = "=3.2.0"
pulldown-cmark = "0.5" pulldown-cmark = "0.6"
slug = "0.1" slug = "0.1"
serde = "1" serde = "1"
serde_derive = "1" serde_derive = "1"

View file

@ -32,7 +32,7 @@ use errors::Result;
pub use context::RenderContext; pub use context::RenderContext;
use markdown::markdown_to_html; use markdown::markdown_to_html;
pub use shortcode::render_shortcodes; pub use shortcode::render_shortcodes;
pub use table_of_contents::Header; pub use table_of_contents::Heading;
pub fn render_content(content: &str, context: &RenderContext) -> Result<markdown::Rendered> { pub fn render_content(content: &str, context: &RenderContext) -> Result<markdown::Rendered> {
// Don't do shortcodes if there is nothing like a shortcode in the content // Don't do shortcodes if there is nothing like a shortcode in the content

View file

@ -9,7 +9,7 @@ use config::highlighting::{get_highlighter, SYNTAX_SET, THEME_SET};
use context::RenderContext; use context::RenderContext;
use errors::{Error, Result}; use errors::{Error, Result};
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
use table_of_contents::{make_table_of_contents, Header}; use table_of_contents::{make_table_of_contents, Heading};
use utils::site::resolve_internal_link; use utils::site::resolve_internal_link;
use utils::vec::InsertMany; use utils::vec::InsertMany;
@ -23,23 +23,23 @@ const ANCHOR_LINK_TEMPLATE: &str = "anchor-link.html";
pub struct Rendered { pub struct Rendered {
pub body: String, pub body: String,
pub summary_len: Option<usize>, pub summary_len: Option<usize>,
pub toc: Vec<Header>, pub toc: Vec<Heading>,
pub internal_links_with_anchors: Vec<(String, String)>, pub internal_links_with_anchors: Vec<(String, String)>,
pub external_links: Vec<String>, pub external_links: Vec<String>,
} }
// tracks a header in a slice of pulldown-cmark events // tracks a heading in a slice of pulldown-cmark events
#[derive(Debug)] #[derive(Debug)]
struct HeaderRef { struct HeadingRef {
start_idx: usize, start_idx: usize,
end_idx: usize, end_idx: usize,
level: i32, level: u32,
id: Option<String>, id: Option<String>,
} }
impl HeaderRef { impl HeadingRef {
fn new(start: usize, level: i32) -> HeaderRef { fn new(start: usize, level: u32) -> HeadingRef {
HeaderRef { start_idx: start, end_idx: 0, level, id: None } HeadingRef { start_idx: start, end_idx: 0, level, id: None }
} }
} }
@ -77,6 +77,12 @@ fn fix_link(
if link_type == LinkType::Email { if link_type == LinkType::Email {
return Ok(link.to_string()); return Ok(link.to_string());
} }
// TODO: remove me in a few versions when people have upgraded
if link.starts_with("./") && link.contains(".md") {
println!("It looks like the link `{}` is using the previous syntax for internal links: start with @/ instead", link);
}
// A few situations here: // 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 link to a co-located asset
@ -119,23 +125,23 @@ fn get_text(parser_slice: &[Event]) -> String {
title title
} }
fn get_header_refs(events: &[Event]) -> Vec<HeaderRef> { fn get_heading_refs(events: &[Event]) -> Vec<HeadingRef> {
let mut header_refs = vec![]; let mut heading_refs = vec![];
for (i, event) in events.iter().enumerate() { for (i, event) in events.iter().enumerate() {
match event { match event {
Event::Start(Tag::Header(level)) => { Event::Start(Tag::Heading(level)) => {
header_refs.push(HeaderRef::new(i, *level)); heading_refs.push(HeadingRef::new(i, *level));
} }
Event::End(Tag::Header(_)) => { Event::End(Tag::Heading(_)) => {
let msg = "Header end before start?"; let msg = "Heading end before start?";
header_refs.last_mut().expect(msg).end_idx = i; heading_refs.last_mut().expect(msg).end_idx = i;
} }
_ => (), _ => (),
} }
} }
header_refs heading_refs
} }
pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Rendered> { pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Rendered> {
@ -148,7 +154,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
let mut highlighter: Option<(HighlightLines, bool)> = None; let mut highlighter: Option<(HighlightLines, bool)> = None;
let mut inserted_anchors: Vec<String> = vec![]; let mut inserted_anchors: Vec<String> = vec![];
let mut headers: Vec<Header> = vec![]; let mut headings: Vec<Heading> = vec![];
let mut internal_links_with_anchors = Vec::new(); let mut internal_links_with_anchors = Vec::new();
let mut external_links = Vec::new(); let mut external_links = Vec::new();
@ -241,14 +247,14 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
}) })
.collect::<Vec<_>>(); // We need to collect the events to make a second pass .collect::<Vec<_>>(); // We need to collect the events to make a second pass
let mut header_refs = get_header_refs(&events); let mut heading_refs = get_heading_refs(&events);
let mut anchors_to_insert = vec![]; let mut anchors_to_insert = vec![];
// First header pass: look for a manually-specified IDs, e.g. `# Heading text {#hash}` // First heading 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.) // (This is a separate first pass so that auto IDs can avoid collisions with manual IDs.)
for header_ref in header_refs.iter_mut() { for heading_ref in heading_refs.iter_mut() {
let end_idx = header_ref.end_idx; let end_idx = heading_ref.end_idx;
if let Event::Text(ref mut text) = events[end_idx - 1] { if let Event::Text(ref mut text) = events[end_idx - 1] {
if text.as_bytes().last() == Some(&b'}') { if text.as_bytes().last() == Some(&b'}') {
if let Some(mut i) = text.find("{#") { if let Some(mut i) = text.find("{#") {
@ -257,24 +263,24 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
while i > 0 && text.as_bytes()[i - 1] == b' ' { while i > 0 && text.as_bytes()[i - 1] == b' ' {
i -= 1; i -= 1;
} }
header_ref.id = Some(id); heading_ref.id = Some(id);
*text = text[..i].to_owned().into(); *text = text[..i].to_owned().into();
} }
} }
} }
} }
// Second header pass: auto-generate remaining IDs, and emit HTML // Second heading pass: auto-generate remaining IDs, and emit HTML
for header_ref in header_refs { for heading_ref in heading_refs {
let start_idx = header_ref.start_idx; let start_idx = heading_ref.start_idx;
let end_idx = header_ref.end_idx; let end_idx = heading_ref.end_idx;
let title = get_text(&events[start_idx + 1..end_idx]); let title = get_text(&events[start_idx + 1..end_idx]);
let id = let id =
header_ref.id.unwrap_or_else(|| find_anchor(&inserted_anchors, slugify(&title), 0)); heading_ref.id.unwrap_or_else(|| find_anchor(&inserted_anchors, slugify(&title), 0));
inserted_anchors.push(id.clone()); inserted_anchors.push(id.clone());
// insert `id` to the tag // insert `id` to the tag
let html = format!("<h{lvl} id=\"{id}\">", lvl = header_ref.level, id = id); let html = format!("<h{lvl} id=\"{id}\">", lvl = heading_ref.level, id = id);
events[start_idx] = Event::Html(html.into()); events[start_idx] = Event::Html(html.into());
// generate anchors and places to insert them // generate anchors and places to insert them
@ -297,10 +303,10 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
anchors_to_insert.push((anchor_idx, Event::Html(anchor_link.into()))); anchors_to_insert.push((anchor_idx, Event::Html(anchor_link.into())));
} }
// record header to make table of contents // record heading to make table of contents
let permalink = format!("{}#{}", context.current_page_permalink, id); let permalink = format!("{}#{}", context.current_page_permalink, id);
let h = Header { level: header_ref.level, id, permalink, title, children: Vec::new() }; let h = Heading { level: heading_ref.level, id, permalink, title, children: Vec::new() };
headers.push(h); headings.push(h);
} }
if context.insert_anchor != InsertAnchor::None { if context.insert_anchor != InsertAnchor::None {
@ -311,12 +317,12 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
} }
if let Some(e) = error { if let Some(e) = error {
return Err(e); Err(e)
} else { } else {
Ok(Rendered { Ok(Rendered {
summary_len: if has_summary { html.find(CONTINUE_READING) } else { None }, summary_len: if has_summary { html.find(CONTINUE_READING) } else { None },
body: html, body: html,
toc: make_table_of_contents(headers), toc: make_table_of_contents(headings),
internal_links_with_anchors, internal_links_with_anchors,
external_links, external_links,
}) })

View file

@ -1,17 +1,17 @@
/// Populated while receiving events from the markdown parser /// Populated while receiving events from the markdown parser
#[derive(Debug, PartialEq, Clone, Serialize)] #[derive(Debug, PartialEq, Clone, Serialize)]
pub struct Header { pub struct Heading {
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub level: i32, pub level: u32,
pub id: String, pub id: String,
pub permalink: String, pub permalink: String,
pub title: String, pub title: String,
pub children: Vec<Header>, pub children: Vec<Heading>,
} }
impl Header { impl Heading {
pub fn new(level: i32) -> Header { pub fn new(level: u32) -> Heading {
Header { Heading {
level, level,
id: String::new(), id: String::new(),
permalink: String::new(), permalink: String::new(),
@ -21,39 +21,49 @@ impl Header {
} }
} }
impl Default for Header { impl Default for Heading {
fn default() -> Self { fn default() -> Self {
Header::new(0) Heading::new(0)
} }
} }
/// Converts the flat temp headers into a nested set of headers // Takes a potential (mutable) parent and a heading to try and insert into
// Returns true when it performed the insertion, false otherwise
fn insert_into_parent(potential_parent: Option<&mut Heading>, heading: &Heading) -> bool {
match potential_parent {
None => {
// No potential parent to insert into so it needs to be insert higher
false
}
Some(parent) => {
if heading.level <= parent.level {
// Heading is same level or higher so we don't insert here
return false;
}
if heading.level + 1 == parent.level {
// We have a direct child of the parent
parent.children.push(heading.clone());
return true;
}
// We need to go deeper
if !insert_into_parent(parent.children.iter_mut().last(), heading) {
// No, we need to insert it here
parent.children.push(heading.clone());
}
true
}
}
}
/// Converts the flat temp headings into a nested set of headings
/// representing the hierarchy /// representing the hierarchy
pub fn make_table_of_contents(headers: Vec<Header>) -> Vec<Header> { pub fn make_table_of_contents(headings: Vec<Heading>) -> Vec<Heading> {
let mut toc = vec![]; let mut toc = vec![];
'parent: for header in headers { for heading in headings {
if toc.is_empty() { // First heading or we try to insert the current heading in a previous one
toc.push(header); if toc.is_empty() || !insert_into_parent(toc.iter_mut().last(), &heading) {
continue; toc.push(heading);
} }
// See if we have to insert as a child of a previous header
for h in toc.iter_mut().rev() {
// Look in its children first
for child in h.children.iter_mut().rev() {
if header.level > child.level {
child.children.push(header);
continue 'parent;
}
}
if header.level > h.level {
h.children.push(header);
continue 'parent;
}
}
// Nop, just insert it
toc.push(header)
} }
toc toc
@ -65,7 +75,7 @@ mod tests {
#[test] #[test]
fn can_make_basic_toc() { fn can_make_basic_toc() {
let input = vec![Header::new(1), Header::new(1), Header::new(1)]; let input = vec![Heading::new(1), Heading::new(1), Heading::new(1)];
let toc = make_table_of_contents(input); let toc = make_table_of_contents(input);
assert_eq!(toc.len(), 3); assert_eq!(toc.len(), 3);
} }
@ -73,15 +83,15 @@ mod tests {
#[test] #[test]
fn can_make_more_complex_toc() { fn can_make_more_complex_toc() {
let input = vec![ let input = vec![
Header::new(1), Heading::new(1),
Header::new(2), Heading::new(2),
Header::new(2), Heading::new(2),
Header::new(3), Heading::new(3),
Header::new(2), Heading::new(2),
Header::new(1), Heading::new(1),
Header::new(2), Heading::new(2),
Header::new(3), Heading::new(3),
Header::new(3), Heading::new(3),
]; ];
let toc = make_table_of_contents(input); let toc = make_table_of_contents(input);
assert_eq!(toc.len(), 2); assert_eq!(toc.len(), 2);
@ -91,16 +101,59 @@ mod tests {
assert_eq!(toc[1].children[0].children.len(), 2); assert_eq!(toc[1].children[0].children.len(), 2);
} }
#[test]
fn can_make_deep_toc() {
let input = vec![
Heading::new(1),
Heading::new(2),
Heading::new(3),
Heading::new(4),
Heading::new(5),
Heading::new(4),
];
let toc = make_table_of_contents(input);
assert_eq!(toc.len(), 1);
assert_eq!(toc[0].children.len(), 1);
assert_eq!(toc[0].children[0].children.len(), 1);
assert_eq!(toc[0].children[0].children[0].children.len(), 2);
assert_eq!(toc[0].children[0].children[0].children[0].children.len(), 1);
}
#[test]
fn can_make_deep_messy_toc() {
let input = vec![
Heading::new(2), // toc[0]
Heading::new(3),
Heading::new(4),
Heading::new(5),
Heading::new(4),
Heading::new(2), // toc[1]
Heading::new(1), // toc[2]
Heading::new(2),
Heading::new(3),
Heading::new(4),
];
let toc = make_table_of_contents(input);
assert_eq!(toc.len(), 3);
assert_eq!(toc[0].children.len(), 1);
assert_eq!(toc[0].children[0].children.len(), 2);
assert_eq!(toc[0].children[0].children[0].children.len(), 1);
assert_eq!(toc[1].children.len(), 0);
assert_eq!(toc[2].children.len(), 1);
assert_eq!(toc[2].children[0].children.len(), 1);
assert_eq!(toc[2].children[0].children[0].children.len(), 1);
}
#[test] #[test]
fn can_make_messy_toc() { fn can_make_messy_toc() {
let input = vec![ let input = vec![
Header::new(3), Heading::new(3),
Header::new(2), Heading::new(2),
Header::new(2), Heading::new(2),
Header::new(3), Heading::new(3),
Header::new(2), Heading::new(2),
Header::new(1), Heading::new(1),
Header::new(4), Heading::new(4),
]; ];
let toc = make_table_of_contents(input); let toc = make_table_of_contents(input);
println!("{:#?}", toc); println!("{:#?}", toc);

View file

@ -332,7 +332,7 @@ fn errors_relative_link_inexistant() {
} }
#[test] #[test]
fn can_add_id_to_headers() { fn can_add_id_to_headings() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
@ -342,7 +342,7 @@ fn can_add_id_to_headers() {
} }
#[test] #[test]
fn can_add_id_to_headers_same_slug() { fn can_add_id_to_headings_same_slug() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
@ -352,7 +352,7 @@ fn can_add_id_to_headers_same_slug() {
} }
#[test] #[test]
fn can_handle_manual_ids_on_headers() { fn can_handle_manual_ids_on_headings() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
@ -361,7 +361,7 @@ fn can_handle_manual_ids_on_headers() {
// manual IDs; that duplicates are in fact permitted among manual IDs; that any non-plain-text // 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 // 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 // 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. // workaround for the improbable case where you actually want `{#…}` at the end of a heading.
let res = render_content( let res = render_content(
"\ "\
# Hello\n\ # Hello\n\
@ -389,7 +389,7 @@ fn can_handle_manual_ids_on_headers() {
} }
#[test] #[test]
fn blank_headers() { fn blank_headings() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
@ -409,7 +409,7 @@ fn can_insert_anchor_left() {
let res = render_content("# Hello", &context).unwrap(); let res = render_content("# Hello", &context).unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
"<h1 id=\"hello\"><a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello</h1>\n" "<h1 id=\"hello\"><a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>Hello</h1>\n"
); );
} }
@ -421,20 +421,20 @@ fn can_insert_anchor_right() {
let res = render_content("# Hello", &context).unwrap(); let res = render_content("# Hello", &context).unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
"<h1 id=\"hello\">Hello<a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\n</h1>\n" "<h1 id=\"hello\">Hello<a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a></h1>\n"
); );
} }
#[test] #[test]
fn can_insert_anchor_for_multi_header() { fn can_insert_anchor_for_multi_heading() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::Right); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::Right);
let res = render_content("# Hello\n# World", &context).unwrap(); let res = render_content("# Hello\n# World", &context).unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
"<h1 id=\"hello\">Hello<a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\n</h1>\n\ "<h1 id=\"hello\">Hello<a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a></h1>\n\
<h1 id=\"world\">World<a class=\"zola-anchor\" href=\"#world\" aria-label=\"Anchor link for: world\">🔗</a>\n</h1>\n" <h1 id=\"world\">World<a class=\"zola-anchor\" href=\"#world\" aria-label=\"Anchor link for: world\">🔗</a></h1>\n"
); );
} }
@ -447,7 +447,7 @@ fn can_insert_anchor_with_exclamation_mark() {
let res = render_content("# Hello!", &context).unwrap(); let res = render_content("# Hello!", &context).unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
"<h1 id=\"hello\"><a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello!</h1>\n" "<h1 id=\"hello\"><a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>Hello!</h1>\n"
); );
} }
@ -460,7 +460,7 @@ fn can_insert_anchor_with_link() {
let res = render_content("## [Rust](https://rust-lang.org)", &context).unwrap(); let res = render_content("## [Rust](https://rust-lang.org)", &context).unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
"<h2 id=\"rust\"><a class=\"zola-anchor\" href=\"#rust\" aria-label=\"Anchor link for: rust\">🔗</a>\n<a href=\"https://rust-lang.org\">Rust</a></h2>\n" "<h2 id=\"rust\"><a class=\"zola-anchor\" href=\"#rust\" aria-label=\"Anchor link for: rust\">🔗</a><a href=\"https://rust-lang.org\">Rust</a></h2>\n"
); );
} }
@ -472,7 +472,7 @@ fn can_insert_anchor_with_other_special_chars() {
let res = render_content("# Hello*_()", &context).unwrap(); let res = render_content("# Hello*_()", &context).unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
"<h1 id=\"hello\"><a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>\nHello*_()</h1>\n" "<h1 id=\"hello\"><a class=\"zola-anchor\" href=\"#hello\" aria-label=\"Anchor link for: hello\">🔗</a>Hello*_()</h1>\n"
); );
} }
@ -490,11 +490,11 @@ fn can_make_toc() {
let res = render_content( let res = render_content(
r#" r#"
# Header 1 # Heading 1
## Header 2 ## Heading 2
## Another Header 2 ## Another Heading 2
### Last one ### Last one
"#, "#,
@ -522,9 +522,9 @@ fn can_ignore_tags_in_toc() {
let res = render_content( let res = render_content(
r#" r#"
## header with `code` ## heading with `code`
## [anchor](https://duckduckgo.com/) in header ## [anchor](https://duckduckgo.com/) in heading
## **bold** and *italics* ## **bold** and *italics*
"#, "#,
@ -534,11 +534,11 @@ fn can_ignore_tags_in_toc() {
let toc = res.toc; let toc = res.toc;
assert_eq!(toc[0].id, "header-with-code"); assert_eq!(toc[0].id, "heading-with-code");
assert_eq!(toc[0].title, "header with code"); assert_eq!(toc[0].title, "heading with code");
assert_eq!(toc[1].id, "anchor-in-header"); assert_eq!(toc[1].id, "anchor-in-heading");
assert_eq!(toc[1].title, "anchor in header"); assert_eq!(toc[1].title, "anchor in heading");
assert_eq!(toc[2].id, "bold-and-italics"); assert_eq!(toc[2].id, "bold-and-italics");
assert_eq!(toc[2].title, "bold and italics"); assert_eq!(toc[2].title, "bold and italics");
@ -564,7 +564,7 @@ fn can_understand_backtick_in_paragraphs() {
// https://github.com/Keats/gutenberg/issues/297 // https://github.com/Keats/gutenberg/issues/297
#[test] #[test]
fn can_understand_links_in_header() { fn can_understand_links_in_heading() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
@ -573,7 +573,7 @@ fn can_understand_links_in_header() {
} }
#[test] #[test]
fn can_understand_link_with_title_in_header() { fn can_understand_link_with_title_in_heading() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
@ -586,7 +586,7 @@ fn can_understand_link_with_title_in_header() {
} }
#[test] #[test]
fn can_understand_emphasis_in_header() { fn can_understand_emphasis_in_heading() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
@ -595,7 +595,7 @@ fn can_understand_emphasis_in_header() {
} }
#[test] #[test]
fn can_understand_strong_in_header() { fn can_understand_strong_in_heading() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
@ -604,7 +604,7 @@ fn can_understand_strong_in_header() {
} }
#[test] #[test]
fn can_understand_code_in_header() { fn can_understand_code_in_heading() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
@ -614,7 +614,7 @@ fn can_understand_code_in_header() {
// See https://github.com/getzola/zola/issues/569 // See https://github.com/getzola/zola/issues/569
#[test] #[test]
fn can_understand_footnote_in_header() { fn can_understand_footnote_in_heading() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
@ -627,7 +627,7 @@ fn can_understand_footnote_in_header() {
} }
#[test] #[test]
fn can_make_valid_relative_link_in_header() { fn can_make_valid_relative_link_in_heading() {
let mut permalinks = HashMap::new(); let mut permalinks = HashMap::new();
permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about/".to_string()); permalinks.insert("pages/about.md".to_string(), "https://vincent.is/about/".to_string());
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
@ -819,3 +819,14 @@ fn doesnt_try_to_highlight_content_from_shortcode() {
// let res = render_content(markdown_string, &context).unwrap(); // let res = render_content(markdown_string, &context).unwrap();
// assert_eq!(res.body, expected); // assert_eq!(res.body, expected);
//} //}
// https://github.com/getzola/zola/issues/747
#[test]
fn leaves_custom_url_scheme_untouched() {
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("[foo@bar.tld](xmpp:foo@bar.tld)", &context).unwrap();
assert_eq!(res.body, "<p><a href=\"xmpp:foo@bar.tld\">foo@bar.tld</a></p>\n");
}

View file

@ -5,7 +5,7 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
[dependencies] [dependencies]
elasticlunr-rs = "2" elasticlunr-rs = "2"
ammonia = "2" ammonia = "3"
lazy_static = "1" lazy_static = "1"
errors = { path = "../errors" } errors = { path = "../errors" }

View file

@ -48,7 +48,9 @@ pub fn build_index(lang: &str, library: &Library) -> Result<String> {
let mut index = Index::with_language(language, &["title", "body"]); let mut index = Index::with_language(language, &["title", "body"]);
for section in library.sections_values() { for section in library.sections_values() {
add_section_to_index(&mut index, section, library); if section.lang == lang {
add_section_to_index(&mut index, section, library);
}
} }
Ok(index.to_json()) Ok(index.to_json())
@ -72,7 +74,7 @@ fn add_section_to_index(index: &mut Index, section: &Section, library: &Library)
for key in &section.pages { for key in &section.pages {
let page = library.get_page_by_key(*key); let page = library.get_page_by_key(*key);
if !page.meta.in_search_index || page.meta.draft { if !page.meta.in_search_index {
continue; continue;
} }

View file

@ -63,6 +63,8 @@ pub struct Site {
pub permalinks: HashMap<String, String>, pub permalinks: HashMap<String, String>,
/// Contains all pages and sections of the site /// Contains all pages and sections of the site
pub library: Arc<RwLock<Library>>, pub library: Arc<RwLock<Library>>,
/// Whether to load draft pages
include_drafts: bool,
} }
impl Site { impl Site {
@ -131,6 +133,7 @@ impl Site {
static_path, static_path,
taxonomies: Vec::new(), taxonomies: Vec::new(),
permalinks: HashMap::new(), permalinks: HashMap::new(),
include_drafts: false,
// We will allocate it properly later on // We will allocate it properly later on
library: Arc::new(RwLock::new(Library::new(0, 0, false))), library: Arc::new(RwLock::new(Library::new(0, 0, false))),
}; };
@ -138,6 +141,12 @@ impl Site {
Ok(site) Ok(site)
} }
/// Set the site to load the drafts.
/// Needs to be called before loading it
pub fn include_drafts(&mut self) {
self.include_drafts = true;
}
/// The index sections are ALWAYS at those paths /// The index sections are ALWAYS at those paths
/// There are one index section for the basic language + 1 per language /// There are one index section for the basic language + 1 per language
fn index_section_paths(&self) -> Vec<(PathBuf, Option<String>)> { fn index_section_paths(&self) -> Vec<(PathBuf, Option<String>)> {
@ -210,6 +219,10 @@ impl Site {
page_entries page_entries
.into_par_iter() .into_par_iter()
.filter(|entry| match &config.ignored_content_globset {
Some(gs) => !gs.is_match(entry.as_path()),
None => true,
})
.map(|entry| { .map(|entry| {
let path = entry.as_path(); let path = entry.as_path();
Page::from_file(path, config, &self.base_path) Page::from_file(path, config, &self.base_path)
@ -229,6 +242,10 @@ impl Site {
let mut pages_insert_anchors = HashMap::new(); let mut pages_insert_anchors = HashMap::new();
for page in pages { for page in pages {
let p = page?; let p = page?;
// Should draft pages be ignored?
if p.meta.draft && !self.include_drafts {
continue;
}
pages_insert_anchors.insert( pages_insert_anchors.insert(
p.file.path.clone(), p.file.path.clone(),
self.find_parent_section_insert_anchor(&p.file.parent.clone(), &p.lang), self.find_parent_section_insert_anchor(&p.file.parent.clone(), &p.lang),
@ -247,7 +264,7 @@ impl Site {
// Needs to be done after rendering markdown as we only get the anchors at that point // Needs to be done after rendering markdown as we only get the anchors at that point
self.check_internal_links_with_anchors()?; self.check_internal_links_with_anchors()?;
if self.config.check_external_links { if self.config.is_in_check_mode() {
self.check_external_links()?; self.check_external_links()?;
} }
@ -275,6 +292,15 @@ impl Site {
}) })
.flatten(); .flatten();
let all_links = page_links.chain(section_links).collect::<Vec<_>>(); let all_links = page_links.chain(section_links).collect::<Vec<_>>();
if self.config.is_in_check_mode() {
println!("Checking {} internal link(s) with an anchor.", all_links.len());
}
if all_links.is_empty() {
return Ok(());
}
let mut full_path = self.base_path.clone(); let mut full_path = self.base_path.clone();
full_path.push("content"); full_path.push("content");
@ -308,9 +334,19 @@ impl Site {
} }
}) })
.collect(); .collect();
if self.config.is_in_check_mode() {
println!(
"> Checked {} internal link(s) with an anchor: {} error(s) found.",
all_links.len(),
errors.len()
);
}
if errors.is_empty() { if errors.is_empty() {
return Ok(()); return Ok(());
} }
let msg = errors let msg = errors
.into_iter() .into_iter()
.map(|(page_path, md_path, anchor)| { .map(|(page_path, md_path, anchor)| {
@ -323,7 +359,7 @@ impl Site {
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
Err(Error { kind: ErrorKind::Msg(msg.into()), source: None }) Err(Error { kind: ErrorKind::Msg(msg), source: None })
} }
pub fn check_external_links(&self) -> Result<()> { pub fn check_external_links(&self) -> Result<()> {
@ -345,6 +381,11 @@ impl Site {
}) })
.flatten(); .flatten();
let all_links = page_links.chain(section_links).collect::<Vec<_>>(); let all_links = page_links.chain(section_links).collect::<Vec<_>>();
println!("Checking {} external link(s).", all_links.len());
if all_links.is_empty() {
return Ok(());
}
// create thread pool with lots of threads so we can fetch // create thread pool with lots of threads so we can fetch
// (almost) all pages simultaneously // (almost) all pages simultaneously
@ -352,7 +393,7 @@ impl Site {
let pool = rayon::ThreadPoolBuilder::new() let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads) .num_threads(threads)
.build() .build()
.map_err(|e| Error { kind: ErrorKind::Msg(e.to_string().into()), source: None })?; .map_err(|e| Error { kind: ErrorKind::Msg(e.to_string()), source: None })?;
let errors: Vec<_> = pool.install(|| { let errors: Vec<_> = pool.install(|| {
all_links all_links
@ -368,9 +409,16 @@ impl Site {
.collect() .collect()
}); });
println!(
"> Checked {} external link(s): {} error(s) found.",
all_links.len(),
errors.len()
);
if errors.is_empty() { if errors.is_empty() {
return Ok(()); return Ok(());
} }
let msg = errors let msg = errors
.into_iter() .into_iter()
.map(|(page_path, link, check_res)| { .map(|(page_path, link, check_res)| {
@ -383,7 +431,7 @@ impl Site {
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
Err(Error { kind: ErrorKind::Msg(msg.into()), source: None }) Err(Error { kind: ErrorKind::Msg(msg), source: None })
} }
/// Insert a default index section for each language if necessary so we don't need to create /// Insert a default index section for each language if necessary so we don't need to create
@ -486,7 +534,7 @@ impl Site {
self.tera.register_function("trans", global_fns::Trans::new(self.config.clone())); self.tera.register_function("trans", global_fns::Trans::new(self.config.clone()));
self.tera.register_function( self.tera.register_function(
"get_taxonomy_url", "get_taxonomy_url",
global_fns::GetTaxonomyUrl::new(&self.taxonomies), global_fns::GetTaxonomyUrl::new(&self.config.default_language, &self.taxonomies),
); );
} }
@ -501,7 +549,11 @@ impl Site {
); );
self.tera.register_function( self.tera.register_function(
"get_taxonomy", "get_taxonomy",
global_fns::GetTaxonomy::new(self.taxonomies.clone(), self.library.clone()), global_fns::GetTaxonomy::new(
&self.config.default_language,
self.taxonomies.clone(),
self.library.clone(),
),
); );
} }
@ -597,11 +649,12 @@ impl Site {
copy_directory( copy_directory(
&self.base_path.join("themes").join(theme).join("static"), &self.base_path.join("themes").join(theme).join("static"),
&self.output_path, &self.output_path,
false,
)?; )?;
} }
// We're fine with missing static folders // We're fine with missing static folders
if self.static_path.exists() { if self.static_path.exists() {
copy_directory(&self.static_path, &self.output_path)?; copy_directory(&self.static_path, &self.output_path, self.config.hard_link_static)?;
} }
Ok(()) Ok(())
@ -698,7 +751,7 @@ impl Site {
.pages_values() .pages_values()
.iter() .iter()
.filter(|p| p.lang == self.config.default_language) .filter(|p| p.lang == self.config.default_language)
.map(|p| *p) .cloned()
.collect() .collect()
} else { } else {
library.pages_values() library.pages_values()
@ -711,7 +764,7 @@ impl Site {
continue; continue;
} }
let pages = let pages =
library.pages_values().iter().filter(|p| p.lang == lang.code).map(|p| *p).collect(); library.pages_values().iter().filter(|p| p.lang == lang.code).cloned().collect();
self.render_rss_feed(pages, Some(&PathBuf::from(lang.code.clone())))?; self.render_rss_feed(pages, Some(&PathBuf::from(lang.code.clone())))?;
} }
@ -728,6 +781,7 @@ impl Site {
} }
pub fn build_search_index(&self) -> Result<()> { pub fn build_search_index(&self) -> Result<()> {
ensure_directory_exists(&self.output_path)?;
// index first // index first
create_file( create_file(
&self.output_path.join(&format!("search_index.{}.js", self.config.default_language)), &self.output_path.join(&format!("search_index.{}.js", self.config.default_language)),
@ -737,6 +791,18 @@ impl Site {
), ),
)?; )?;
for language in &self.config.languages {
if language.code != self.config.default_language && language.search {
create_file(
&self.output_path.join(&format!("search_index.{}.js", &language.code)),
&format!(
"window.searchIndex = {};",
search::build_index(&language.code, &self.library.read().unwrap())?
),
)?;
}
}
// then elasticlunr.min.js // then elasticlunr.min.js
create_file(&self.output_path.join("elasticlunr.min.js"), search::ELASTICLUNR_JS)?; create_file(&self.output_path.join("elasticlunr.min.js"), search::ELASTICLUNR_JS)?;
@ -873,7 +939,7 @@ impl Site {
) )
} }
/// Renders all taxonomies with at least one non-draft post /// Renders all taxonomies
pub fn render_taxonomies(&self) -> Result<()> { pub fn render_taxonomies(&self) -> Result<()> {
for taxonomy in &self.taxonomies { for taxonomy in &self.taxonomies {
self.render_taxonomy(taxonomy)?; self.render_taxonomy(taxonomy)?;
@ -990,10 +1056,7 @@ impl Site {
ensure_directory_exists(&self.output_path)?; ensure_directory_exists(&self.output_path)?;
let mut context = Context::new(); let mut context = Context::new();
let mut pages = all_pages let mut pages = all_pages.into_iter().filter(|p| p.meta.date.is_some()).collect::<Vec<_>>();
.into_iter()
.filter(|p| p.meta.date.is_some() && !p.is_draft())
.collect::<Vec<_>>();
// Don't generate a RSS feed if none of the pages has a date // Don't generate a RSS feed if none of the pages has a date
if pages.is_empty() { if pages.is_empty() {

View file

@ -62,7 +62,6 @@ pub fn find_entries<'a>(
let pages = library let pages = library
.pages_values() .pages_values()
.iter() .iter()
.filter(|p| !p.is_draft())
.map(|p| { .map(|p| {
let date = match p.meta.date { let date = match p.meta.date {
Some(ref d) => Some(d.to_string()), Some(ref d) => Some(d.to_string()),

View file

@ -18,8 +18,8 @@ fn can_parse_site() {
site.load().unwrap(); site.load().unwrap();
let library = site.library.read().unwrap(); let library = site.library.read().unwrap();
// Correct number of pages (sections do not count as pages) // Correct number of pages (sections do not count as pages, draft are ignored)
assert_eq!(library.pages().len(), 22); assert_eq!(library.pages().len(), 21);
let posts_path = path.join("content").join("posts"); let posts_path = path.join("content").join("posts");
// Make sure the page with a url doesn't have any sections // Make sure the page with a url doesn't have any sections
@ -42,7 +42,7 @@ fn can_parse_site() {
let posts_section = library.get_section(&posts_path.join("_index.md")).unwrap(); let posts_section = library.get_section(&posts_path.join("_index.md")).unwrap();
assert_eq!(posts_section.subsections.len(), 2); assert_eq!(posts_section.subsections.len(), 2);
assert_eq!(posts_section.pages.len(), 10); assert_eq!(posts_section.pages.len(), 9); // 10 with 1 draft == 9
assert_eq!( assert_eq!(
posts_section.ancestors, posts_section.ancestors,
vec![*library.get_section_key(&index_section.file.path).unwrap()] vec![*library.get_section_key(&index_section.file.path).unwrap()]
@ -167,12 +167,12 @@ fn can_build_site_without_live_reload() {
assert!(file_contains!( assert!(file_contains!(
public, public,
"sitemap.xml", "sitemap.xml",
"<loc>https%3A//replace-this-with-your-url.com/posts/simple/</loc>" "<loc>https://replace-this-with-your-url.com/posts/simple/</loc>"
)); ));
assert!(file_contains!( assert!(file_contains!(
public, public,
"sitemap.xml", "sitemap.xml",
"<loc>https%3A//replace-this-with-your-url.com/posts/</loc>" "<loc>https://replace-this-with-your-url.com/posts/</loc>"
)); ));
// Drafts are not in the sitemap // Drafts are not in the sitemap
assert!(!file_contains!(public, "sitemap.xml", "draft")); assert!(!file_contains!(public, "sitemap.xml", "draft"));
@ -189,9 +189,10 @@ fn can_build_site_without_live_reload() {
} }
#[test] #[test]
fn can_build_site_with_live_reload() { fn can_build_site_with_live_reload_and_drafts() {
let (_, _tmp_dir, public) = build_site_with_setup("test_site", |mut site| { let (_, _tmp_dir, public) = build_site_with_setup("test_site", |mut site| {
site.enable_live_reload(1000); site.enable_live_reload(1000);
site.include_drafts();
(site, true) (site, true)
}); });
@ -229,7 +230,10 @@ fn can_build_site_with_live_reload() {
"posts/python/index.html", "posts/python/index.html",
r#"<a name="continue-reading"></a>"# r#"<a name="continue-reading"></a>"#
)); ));
assert!(file_contains!(public, "posts/draft/index.html", r#"THEME_SHORTCODE"#));
// Drafts are included
assert!(file_exists!(public, "posts/draft/index.html"));
assert!(file_contains!(public, "sitemap.xml", "draft"));
} }
#[test] #[test]
@ -279,7 +283,7 @@ fn can_build_site_with_taxonomies() {
assert!(file_contains!( assert!(file_contains!(
public, public,
"categories/a/rss.xml", "categories/a/rss.xml",
"https%3A//replace-this-with-your-url.com/categories/a/rss.xml" "https://replace-this-with-your-url.com/categories/a/rss.xml"
)); ));
// Extending from a theme works // Extending from a theme works
assert!(file_contains!(public, "categories/a/index.html", "EXTENDED")); assert!(file_contains!(public, "categories/a/index.html", "EXTENDED"));
@ -290,12 +294,12 @@ fn can_build_site_with_taxonomies() {
assert!(file_contains!( assert!(file_contains!(
public, public,
"sitemap.xml", "sitemap.xml",
"<loc>https%3A//replace-this-with-your-url.com/categories/</loc>" "<loc>https://replace-this-with-your-url.com/categories/</loc>"
)); ));
assert!(file_contains!( assert!(file_contains!(
public, public,
"sitemap.xml", "sitemap.xml",
"<loc>https%3A//replace-this-with-your-url.com/categories/a/</loc>" "<loc>https://replace-this-with-your-url.com/categories/a/</loc>"
)); ));
} }
@ -424,7 +428,7 @@ fn can_build_site_with_pagination_for_section() {
assert!(file_contains!( assert!(file_contains!(
public, public,
"sitemap.xml", "sitemap.xml",
"<loc>https%3A//replace-this-with-your-url.com/posts/page/4/</loc>" "<loc>https://replace-this-with-your-url.com/posts/page/4/</loc>"
)); ));
} }
@ -477,7 +481,7 @@ fn can_build_site_with_pagination_for_index() {
assert!(file_contains!( assert!(file_contains!(
public, public,
"sitemap.xml", "sitemap.xml",
"<loc>https%3A//replace-this-with-your-url.com/page/1/</loc>" "<loc>https://replace-this-with-your-url.com/page/1/</loc>"
)) ))
} }
@ -558,7 +562,7 @@ fn can_build_site_with_pagination_for_taxonomy() {
assert!(file_contains!( assert!(file_contains!(
public, public,
"sitemap.xml", "sitemap.xml",
"<loc>https%3A//replace-this-with-your-url.com/tags/a/page/6/</loc>" "<loc>https://replace-this-with-your-url.com/tags/a/page/6/</loc>"
)) ))
} }
@ -642,7 +646,7 @@ fn can_apply_page_templates() {
assert_eq!(child.meta.title, Some("Local section override".into())); assert_eq!(child.meta.title, Some("Local section override".into()));
} }
// https%3A//github.com/getzola/zola/issues/571 // https://github.com/getzola/zola/issues/571
#[test] #[test]
fn can_build_site_custom_builtins_from_theme() { fn can_build_site_custom_builtins_from_theme() {
let (_, _tmp_dir, public) = build_site("test_site"); let (_, _tmp_dir, public) = build_site("test_site");
@ -652,3 +656,9 @@ fn can_build_site_custom_builtins_from_theme() {
assert!(file_exists!(public, "404.html")); assert!(file_exists!(public, "404.html"));
assert!(file_contains!(public, "404.html", "Oops")); assert!(file_contains!(public, "404.html", "Oops"));
} }
#[test]
fn can_ignore_markdown_content() {
let (_, _tmp_dir, public) = build_site("test_site");
assert!(!file_exists!(public, "posts/ignored/index.html"));
}

View file

@ -112,30 +112,45 @@ fn can_build_multilingual_site() {
// sitemap contains all languages // sitemap contains all languages
assert!(file_exists!(public, "sitemap.xml")); assert!(file_exists!(public, "sitemap.xml"));
assert!(file_contains!(public, "sitemap.xml", "https%3A//example.com/blog/something-else/")); assert!(file_contains!(public, "sitemap.xml", "https://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://example.com/fr/blog/something-else/"));
assert!(file_contains!(public, "sitemap.xml", "https%3A//example.com/it/blog/something-else/")); assert!(file_contains!(public, "sitemap.xml", "https://example.com/it/blog/something-else/"));
// one rss per language // one rss per language
assert!(file_exists!(public, "rss.xml")); assert!(file_exists!(public, "rss.xml"));
assert!(file_contains!(public, "rss.xml", "https%3A//example.com/blog/something-else/")); assert!(file_contains!(public, "rss.xml", "https://example.com/blog/something-else/"));
assert!(!file_contains!(public, "rss.xml", "https%3A//example.com/fr/blog/something-else/")); assert!(!file_contains!(public, "rss.xml", "https://example.com/fr/blog/something-else/"));
assert!(file_exists!(public, "fr/rss.xml")); assert!(file_exists!(public, "fr/rss.xml"));
assert!(!file_contains!(public, "fr/rss.xml", "https%3A//example.com/blog/something-else/")); assert!(!file_contains!(public, "fr/rss.xml", "https://example.com/blog/something-else/"));
assert!(file_contains!(public, "fr/rss.xml", "https%3A//example.com/fr/blog/something-else/")); assert!(file_contains!(public, "fr/rss.xml", "https://example.com/fr/blog/something-else/"));
// Italian doesn't have RSS enabled // Italian doesn't have RSS enabled
assert!(!file_exists!(public, "it/rss.xml")); assert!(!file_exists!(public, "it/rss.xml"));
// Taxonomies are per-language // Taxonomies are per-language
// English
assert!(file_exists!(public, "authors/index.html")); assert!(file_exists!(public, "authors/index.html"));
assert!(file_contains!(public, "authors/index.html", "Queen")); assert!(file_contains!(public, "authors/index.html", "Queen"));
assert!(!file_contains!(public, "authors/index.html", "Vincent")); assert!(!file_contains!(public, "authors/index.html", "Vincent"));
assert!(!file_exists!(public, "auteurs/index.html")); assert!(!file_exists!(public, "auteurs/index.html"));
assert!(file_exists!(public, "authors/queen-elizabeth/rss.xml")); assert!(file_exists!(public, "authors/queen-elizabeth/rss.xml"));
assert!(file_exists!(public, "tags/index.html"));
assert!(file_contains!(public, "tags/index.html", "hello"));
assert!(!file_contains!(public, "tags/index.html", "bonjour"));
// French
assert!(!file_exists!(public, "fr/authors/index.html")); assert!(!file_exists!(public, "fr/authors/index.html"));
assert!(file_exists!(public, "fr/auteurs/index.html")); assert!(file_exists!(public, "fr/auteurs/index.html"));
assert!(!file_contains!(public, "fr/auteurs/index.html", "Queen")); assert!(!file_contains!(public, "fr/auteurs/index.html", "Queen"));
assert!(file_contains!(public, "fr/auteurs/index.html", "Vincent")); assert!(file_contains!(public, "fr/auteurs/index.html", "Vincent"));
assert!(!file_exists!(public, "fr/auteurs/vincent-prouillet/rss.xml")); assert!(!file_exists!(public, "fr/auteurs/vincent-prouillet/rss.xml"));
assert!(file_exists!(public, "fr/tags/index.html"));
assert!(file_contains!(public, "fr/tags/index.html", "bonjour"));
assert!(!file_contains!(public, "fr/tags/index.html", "hello"));
// one lang index per language
assert!(file_exists!(public, "search_index.en.js"));
assert!(file_exists!(public, "search_index.it.js"));
assert!(!file_exists!(public, "search_index.fr.js"));
} }

View file

@ -7,13 +7,13 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
tera = "1.0.0-beta.10" tera = "1.0.0-beta.10"
base64 = "0.10" base64 = "0.10"
lazy_static = "1" lazy_static = "1"
pulldown-cmark = "0.5" pulldown-cmark = "0.6"
toml = "0.5" toml = "0.5"
csv = "1" csv = "1"
image = "0.21" image = "0.22"
serde_json = "1.0" serde_json = "1.0"
reqwest = "0.9" reqwest = "0.9"
url = "1.5" url = "2"
errors = { path = "../errors" } errors = { path = "../errors" }
utils = { path = "../utils" } utils = { path = "../utils" }

View file

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

View file

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

View file

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

View file

@ -1,10 +1,14 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::BuildHasher;
use base64::{decode, encode}; use base64::{decode, encode};
use pulldown_cmark as cmark; use pulldown_cmark as cmark;
use tera::{to_value, Result as TeraResult, Value}; use tera::{to_value, Result as TeraResult, Value};
pub fn markdown(value: &Value, args: &HashMap<String, Value>) -> TeraResult<Value> { pub fn markdown<S: BuildHasher>(
value: &Value,
args: &HashMap<String, Value, S>,
) -> TeraResult<Value> {
let s = try_get_value!("markdown", "value", String, value); let s = try_get_value!("markdown", "value", String, value);
let inline = match args.get("inline") { let inline = match args.get("inline") {
Some(val) => try_get_value!("markdown", "inline", bool, val), Some(val) => try_get_value!("markdown", "inline", bool, val),
@ -30,12 +34,18 @@ pub fn markdown(value: &Value, args: &HashMap<String, Value>) -> TeraResult<Valu
Ok(to_value(&html).unwrap()) Ok(to_value(&html).unwrap())
} }
pub fn base64_encode(value: &Value, _: &HashMap<String, Value>) -> TeraResult<Value> { pub fn base64_encode<S: BuildHasher>(
value: &Value,
_: &HashMap<String, Value, S>,
) -> TeraResult<Value> {
let s = try_get_value!("base64_encode", "value", String, value); let s = try_get_value!("base64_encode", "value", String, value);
Ok(to_value(&encode(s.as_bytes())).unwrap()) Ok(to_value(&encode(s.as_bytes())).unwrap())
} }
pub fn base64_decode(value: &Value, _: &HashMap<String, Value>) -> TeraResult<Value> { pub fn base64_decode<S: BuildHasher>(
value: &Value,
_: &HashMap<String, Value, S>,
) -> TeraResult<Value> {
let s = try_get_value!("base64_decode", "value", String, value); let s = try_get_value!("base64_decode", "value", String, value);
Ok(to_value(&String::from_utf8(decode(s.as_bytes()).unwrap()).unwrap()).unwrap()) Ok(to_value(&String::from_utf8(decode(s.as_bytes()).unwrap()).unwrap()).unwrap())
} }

View file

@ -445,7 +445,11 @@ mod tests {
args.insert("path".to_string(), to_value("test.css").unwrap()); args.insert("path".to_string(), to_value("test.css").unwrap());
let result = static_fn.call(&args.clone()).unwrap(); let result = static_fn.call(&args.clone()).unwrap();
assert_eq!(result, ".hello {}\n",); if cfg!(windows) {
assert_eq!(result, ".hello {}\r\n",);
} else {
assert_eq!(result, ".hello {}\n",);
};
} }
#[test] #[test]
@ -456,7 +460,11 @@ mod tests {
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
let result = static_fn.call(&args.clone()).unwrap(); let result = static_fn.call(&args.clone()).unwrap();
assert_eq!(result, "Number,Title\n1,Gutenberg\n2,Printing",); if cfg!(windows) {
assert_eq!(result, "Number,Title\r\n1,Gutenberg\r\n2,Printing",);
} else {
assert_eq!(result, "Number,Title\n1,Gutenberg\n2,Printing",);
};
} }
#[test] #[test]
@ -467,7 +475,11 @@ mod tests {
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
let result = static_fn.call(&args.clone()).unwrap(); let result = static_fn.call(&args.clone()).unwrap();
assert_eq!(result, ".hello {}\n",); if cfg!(windows) {
assert_eq!(result, ".hello {}\r\n",);
} else {
assert_eq!(result, ".hello {}\n",);
};
} }
#[test] #[test]

View file

@ -33,8 +33,12 @@ impl TeraFn for Trans {
let key = required_arg!(String, args.get("key"), "`trans` requires a `key` argument."); let key = required_arg!(String, args.get("key"), "`trans` requires a `key` argument.");
let lang = optional_arg!(String, args.get("lang"), "`trans`: `lang` must be a string.") let lang = optional_arg!(String, args.get("lang"), "`trans`: `lang` must be a string.")
.unwrap_or_else(|| self.config.default_language.clone()); .unwrap_or_else(|| self.config.default_language.clone());
let translations = &self.config.translations[lang.as_str()];
Ok(to_value(&translations[key.as_str()]).unwrap()) let term = self.config.get_translation(lang, key).map_err(|e| {
Error::chain("Failed to retreive term translation", e)
})?;
Ok(to_value(term).unwrap())
} }
} }
@ -94,8 +98,8 @@ impl ResizeImage {
} }
} }
static DEFAULT_OP: &'static str = "fill"; static DEFAULT_OP: &str = "fill";
static DEFAULT_FMT: &'static str = "auto"; static DEFAULT_FMT: &str = "auto";
const DEFAULT_Q: u8 = 75; const DEFAULT_Q: u8 = 75;
impl TeraFn for ResizeImage { impl TeraFn for ResizeImage {
@ -176,18 +180,19 @@ impl TeraFn for GetImageMeta {
#[derive(Debug)] #[derive(Debug)]
pub struct GetTaxonomyUrl { pub struct GetTaxonomyUrl {
taxonomies: HashMap<String, HashMap<String, String>>, taxonomies: HashMap<String, HashMap<String, String>>,
default_lang: String,
} }
impl GetTaxonomyUrl { impl GetTaxonomyUrl {
pub fn new(all_taxonomies: &[Taxonomy]) -> Self { pub fn new(default_lang: &str, all_taxonomies: &[Taxonomy]) -> Self {
let mut taxonomies = HashMap::new(); let mut taxonomies = HashMap::new();
for taxonomy in all_taxonomies { for taxo in all_taxonomies {
let mut items = HashMap::new(); let mut items = HashMap::new();
for item in &taxonomy.items { for item in &taxo.items {
items.insert(item.name.clone(), item.permalink.clone()); items.insert(item.name.clone(), item.permalink.clone());
} }
taxonomies.insert(taxonomy.kind.name.clone(), items); taxonomies.insert(format!("{}-{}", taxo.kind.name, taxo.kind.lang), items);
} }
Self { taxonomies } Self { taxonomies, default_lang: default_lang.to_string() }
} }
} }
impl TeraFn for GetTaxonomyUrl { impl TeraFn for GetTaxonomyUrl {
@ -202,7 +207,11 @@ impl TeraFn for GetTaxonomyUrl {
args.get("name"), args.get("name"),
"`get_taxonomy_url` requires a `name` argument with a string value" "`get_taxonomy_url` requires a `name` argument with a string value"
); );
let container = match self.taxonomies.get(&kind) { let lang =
optional_arg!(String, args.get("lang"), "`get_taxonomy`: `lang` must be a string")
.unwrap_or_else(|| self.default_lang.clone());
let container = match self.taxonomies.get(&format!("{}-{}", kind, lang)) {
Some(c) => c, Some(c) => c,
None => { None => {
return Err(format!( return Err(format!(
@ -289,14 +298,19 @@ impl TeraFn for GetSection {
pub struct GetTaxonomy { pub struct GetTaxonomy {
library: Arc<RwLock<Library>>, library: Arc<RwLock<Library>>,
taxonomies: HashMap<String, Taxonomy>, taxonomies: HashMap<String, Taxonomy>,
default_lang: String,
} }
impl GetTaxonomy { impl GetTaxonomy {
pub fn new(all_taxonomies: Vec<Taxonomy>, library: Arc<RwLock<Library>>) -> Self { pub fn new(
default_lang: &str,
all_taxonomies: Vec<Taxonomy>,
library: Arc<RwLock<Library>>,
) -> Self {
let mut taxonomies = HashMap::new(); let mut taxonomies = HashMap::new();
for taxo in all_taxonomies { for taxo in all_taxonomies {
taxonomies.insert(taxo.kind.name.clone(), taxo); taxonomies.insert(format!("{}-{}", taxo.kind.name, taxo.kind.lang), taxo);
} }
Self { taxonomies, library } Self { taxonomies, library, default_lang: default_lang.to_string() }
} }
} }
impl TeraFn for GetTaxonomy { impl TeraFn for GetTaxonomy {
@ -307,7 +321,11 @@ impl TeraFn for GetTaxonomy {
"`get_taxonomy` requires a `kind` argument with a string value" "`get_taxonomy` requires a `kind` argument with a string value"
); );
match self.taxonomies.get(&kind) { let lang =
optional_arg!(String, args.get("lang"), "`get_taxonomy`: `lang` must be a string")
.unwrap_or_else(|| self.default_lang.clone());
match self.taxonomies.get(&format!("{}-{}", kind, lang)) {
Some(t) => Ok(to_value(t.to_serialized(&self.library.read().unwrap())).unwrap()), Some(t) => Ok(to_value(t.to_serialized(&self.library.read().unwrap())).unwrap()),
None => { None => {
Err(format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into()) Err(format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into())
@ -376,6 +394,11 @@ mod tests {
lang: config.default_language.clone(), lang: config.default_language.clone(),
..TaxonomyConfig::default() ..TaxonomyConfig::default()
}; };
let taxo_config_fr = TaxonomyConfig {
name: "tags".to_string(),
lang: "fr".to_string(),
..TaxonomyConfig::default()
};
let library = Arc::new(RwLock::new(Library::new(0, 0, false))); let library = Arc::new(RwLock::new(Library::new(0, 0, false)));
let tag = TaxonomyItem::new( let tag = TaxonomyItem::new(
"Programming", "Programming",
@ -384,10 +407,19 @@ mod tests {
vec![], vec![],
&library.read().unwrap(), &library.read().unwrap(),
); );
let tag_fr = TaxonomyItem::new(
"Programmation",
&taxo_config_fr,
&config,
vec![],
&library.read().unwrap(),
);
let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
let tags_fr = Taxonomy { kind: taxo_config_fr, items: vec![tag_fr] };
let taxonomies = vec![tags.clone()]; let taxonomies = vec![tags.clone(), tags_fr.clone()];
let static_fn = GetTaxonomy::new(taxonomies.clone(), library.clone()); let static_fn =
GetTaxonomy::new(&config.default_language, taxonomies.clone(), library.clone());
// can find it correctly // can find it correctly
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap()); args.insert("kind".to_string(), to_value("tags").unwrap());
@ -412,6 +444,19 @@ mod tests {
res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["pages"], res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["pages"],
Value::Array(vec![]) Value::Array(vec![])
); );
// Works with other languages as well
let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap());
args.insert("lang".to_string(), to_value("fr").unwrap());
let res = static_fn.call(&args).unwrap();
let res_obj = res.as_object().unwrap();
assert_eq!(res_obj["kind"], to_value(tags_fr.kind).unwrap());
assert_eq!(res_obj["items"].clone().as_array().unwrap().len(), 1);
assert_eq!(
res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["name"],
Value::String("Programmation".to_string())
);
// and errors if it can't find it // and errors if it can't find it
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("something-else").unwrap()); args.insert("kind".to_string(), to_value("something-else").unwrap());
@ -426,12 +471,19 @@ mod tests {
lang: config.default_language.clone(), lang: config.default_language.clone(),
..TaxonomyConfig::default() ..TaxonomyConfig::default()
}; };
let taxo_config_fr = TaxonomyConfig {
name: "tags".to_string(),
lang: "fr".to_string(),
..TaxonomyConfig::default()
};
let library = Library::new(0, 0, false); let library = Library::new(0, 0, false);
let tag = TaxonomyItem::new("Programming", &taxo_config, &config, vec![], &library); let tag = TaxonomyItem::new("Programming", &taxo_config, &config, vec![], &library);
let tag_fr = TaxonomyItem::new("Programmation", &taxo_config_fr, &config, vec![], &library);
let tags = Taxonomy { kind: taxo_config, items: vec![tag] }; let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
let tags_fr = Taxonomy { kind: taxo_config_fr, items: vec![tag_fr] };
let taxonomies = vec![tags.clone()]; let taxonomies = vec![tags.clone(), tags_fr.clone()];
let static_fn = GetTaxonomyUrl::new(&taxonomies); let static_fn = GetTaxonomyUrl::new(&config.default_language, &taxonomies);
// can find it correctly // can find it correctly
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap()); args.insert("kind".to_string(), to_value("tags").unwrap());
@ -440,6 +492,16 @@ mod tests {
static_fn.call(&args).unwrap(), static_fn.call(&args).unwrap(),
to_value("http://a-website.com/tags/programming/").unwrap() to_value("http://a-website.com/tags/programming/").unwrap()
); );
// works with other languages
let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap());
args.insert("name".to_string(), to_value("Programmation").unwrap());
args.insert("lang".to_string(), to_value("fr").unwrap());
assert_eq!(
static_fn.call(&args).unwrap(),
to_value("http://a-website.com/fr/tags/programmation/").unwrap()
);
// and errors if it can't find it // and errors if it can't find it
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap()); args.insert("kind".to_string(), to_value("tags").unwrap());
@ -447,9 +509,8 @@ mod tests {
assert!(static_fn.call(&args).is_err()); assert!(static_fn.call(&args).is_err());
} }
#[test]
fn can_translate_a_string() { const TRANS_CONFIG: &str = r#"
let trans_config = r#"
base_url = "https://remplace-par-ton-url.fr" base_url = "https://remplace-par-ton-url.fr"
default_language = "fr" default_language = "fr"
@ -459,10 +520,11 @@ title = "Un titre"
[translations.en] [translations.en]
title = "A title" title = "A title"
"#; "#;
let config = Config::parse(trans_config).unwrap(); #[test]
fn can_translate_a_string() {
let config = Config::parse(TRANS_CONFIG).unwrap();
let static_fn = Trans::new(config); let static_fn = Trans::new(config);
let mut args = HashMap::new(); let mut args = HashMap::new();
@ -475,4 +537,26 @@ title = "A title"
args.insert("lang".to_string(), to_value("fr").unwrap()); args.insert("lang".to_string(), to_value("fr").unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "Un titre"); assert_eq!(static_fn.call(&args).unwrap(), "Un titre");
} }
#[test]
fn error_on_absent_translation_lang() {
let mut args = HashMap::new();
args.insert("lang".to_string(), to_value("absent").unwrap());
args.insert("key".to_string(), to_value("title").unwrap());
let config = Config::parse(TRANS_CONFIG).unwrap();
let error = Trans::new(config).call(&args).unwrap_err();
assert_eq!("Failed to retreive term translation", format!("{}", error));
}
#[test]
fn error_on_absent_translation_key() {
let mut args = HashMap::new();
args.insert("lang".to_string(), to_value("en").unwrap());
args.insert("key".to_string(), to_value("absent").unwrap());
let config = Config::parse(TRANS_CONFIG).unwrap();
let error = Trans::new(config).call(&args).unwrap_err();
assert_eq!("Failed to retreive term translation", format!("{}", error));
}
} }

View file

@ -8,7 +8,7 @@ errors = { path = "../errors" }
tera = "1.0.0-beta.10" tera = "1.0.0-beta.10"
unicode-segmentation = "1.2" unicode-segmentation = "1.2"
walkdir = "2" walkdir = "2"
toml = "0.4" toml = "0.5"
serde = "1" serde = "1"
[dev-dependencies] [dev-dependencies]

View file

@ -40,7 +40,7 @@ pub fn fix_toml_dates(table: Map<String, Value>) -> Value {
for (key, value) in table { for (key, value) in table {
match value { match value {
Value::Object(mut o) => { Value::Object(o) => {
new.insert(key, convert_toml_date(o)); new.insert(key, convert_toml_date(o));
} }
_ => { _ => {

View file

@ -95,7 +95,7 @@ pub fn find_related_assets(path: &Path) -> Vec<PathBuf> {
/// Copy a file but takes into account where to start the copy as /// Copy a file but takes into account where to start the copy as
/// there might be folders we need to create on the way /// there might be folders we need to create on the way
pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf) -> Result<()> { pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf, hard_link: bool) -> Result<()> {
let relative_path = src.strip_prefix(base_path).unwrap(); let relative_path = src.strip_prefix(base_path).unwrap();
let target_path = dest.join(relative_path); let target_path = dest.join(relative_path);
@ -103,11 +103,15 @@ pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf) -> Result<()>
create_dir_all(parent_directory)?; create_dir_all(parent_directory)?;
} }
copy(src, target_path)?; if hard_link {
std::fs::hard_link(src, target_path)?
} else {
copy(src, target_path)?;
}
Ok(()) Ok(())
} }
pub fn copy_directory(src: &PathBuf, dest: &PathBuf) -> Result<()> { pub fn copy_directory(src: &PathBuf, dest: &PathBuf, hard_link: bool) -> Result<()> {
for entry in WalkDir::new(src).into_iter().filter_map(std::result::Result::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 relative_path = entry.path().strip_prefix(src).unwrap();
let target_path = dest.join(relative_path); let target_path = dest.join(relative_path);
@ -117,7 +121,7 @@ pub fn copy_directory(src: &PathBuf, dest: &PathBuf) -> Result<()> {
create_directory(&target_path)?; create_directory(&target_path)?;
} }
} else { } else {
copy_file(entry.path(), dest, src)?; copy_file(entry.path(), dest, src, hard_link)?;
} }
} }
Ok(()) Ok(())

View file

@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::BuildHasher;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use errors::Result; use errors::Result;
@ -23,9 +24,9 @@ pub struct ResolvedInternalLink {
/// Resolves an internal link (of the `@/posts/something.md#hey` sort) to its absolute link and /// Resolves an internal link (of the `@/posts/something.md#hey` sort) to its absolute link and
/// returns the path + anchor as well /// returns the path + anchor as well
pub fn resolve_internal_link( pub fn resolve_internal_link<S: BuildHasher>(
link: &str, link: &str,
permalinks: &HashMap<String, String>, permalinks: &HashMap<String, String, S>,
) -> Result<ResolvedInternalLink> { ) -> Result<ResolvedInternalLink> {
// First we remove the ./ since that's zola specific // First we remove the ./ since that's zola specific
let clean_link = link.replacen("@/", "", 1); let clean_link = link.replacen("@/", "", 1);

View file

@ -7,7 +7,6 @@ highlight_code = true
insert_anchor_links = true insert_anchor_links = true
highlight_theme = "kronuz" highlight_theme = "kronuz"
build_search_index = true build_search_index = true
# check_external_links = true
[extra] [extra]
author = "Vincent Prouillet" author = "Vincent Prouillet"

View file

@ -9,7 +9,7 @@ which is available in template code as well as in shortcodes.
The function usage is as follows: The function usage is as follows:
```jinja2 ```jinja2
resize_image(path, width, height, op, quality) resize_image(path, width, height, op, format, quality)
``` ```
### Arguments ### Arguments
@ -78,10 +78,16 @@ The source for all examples is this 300 × 380 pixels image:
{{ resize_image(path="documentation/content/image-processing/01-zola.png", width=0, height=150, op="fit_height") }} {{ resize_image(path="documentation/content/image-processing/01-zola.png", width=0, height=150, op="fit_height") }}
### **`"fit"`** ### **`"fit"`**
Like `"fit_width"` and `"fit_height"` combined. Like `"fit_width"` and `"fit_height"` combined, but only resize if the image is bigger than any of the specified dimensions.
This mode is handy, if e.g. images are automatically shrinked to certain sizes in a shortcode for mobile optimization.
Resizes the image such that the result fits within `width` and `height` preserving aspect ratio. This means that both width or height Resizes the image such that the result fits within `width` and `height` preserving aspect ratio. This means that both width or height
will be at max `width` and `height`, respectively, but possibly one of them smaller so as to preserve the aspect ratio. will be at max `width` and `height`, respectively, but possibly one of them smaller so as to preserve the aspect ratio.
`resize_image(..., width=5000, height=5000, op="fit")`
{{ resize_image(path="documentation/content/image-processing/01-zola.png", width=5000, height=5000, op="fit") }}
`resize_image(..., width=150, height=150, op="fit")` `resize_image(..., width=150, height=150, op="fit")`
{{ resize_image(path="documentation/content/image-processing/01-zola.png", width=150, height=150, op="fit") }} {{ resize_image(path="documentation/content/image-processing/01-zola.png", width=150, height=150, op="fit") }}
@ -150,4 +156,4 @@ Here is the result:
## Get image size ## Get image size
Sometimes when building a gallery it is useful to know the dimensions of each asset. You can get this information with 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) [get_image_metadata](@/documentation/templates/overview.md#get-image-metadata)

View file

@ -33,7 +33,7 @@ This option is set at the section level: the `insert_anchor_links` variable on t
The default template is very basic and will need CSS tweaks in your project to look decent. 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 If you want to change the anchor template, it can easily be overwritten by
creating a `anchor-link.html` file in the `templates` directory. creating a `anchor-link.html` file in the `templates` directory which gets an `id` variable.
## Internal links ## Internal links
Linking to other pages and their headings is so common that Zola adds a Linking to other pages and their headings is so common that Zola adds a

View file

@ -12,6 +12,7 @@ to your `config.toml`. For example:
```toml ```toml
languages = [ languages = [
{code = "fr", rss = true}, # there will be a RSS feed for French content {code = "fr", rss = true}, # there will be a RSS feed for French content
{code = "fr", search = true}, # there will be a Search Index for French content
{code = "it"}, # there won't be a RSS feed for Italian content {code = "it"}, # there won't be a RSS feed for Italian content
] ]
``` ```

View file

@ -37,8 +37,7 @@ While none of the front-matter variables are mandatory, the opening and closing
Here is an example page with all the variables available. The values provided below are the default Here is an example page with all the variables available. The values provided below are the default
values. values.
```md ```toml
+++
title = "" title = ""
description = "" description = ""
@ -55,7 +54,7 @@ date =
# will not be rendered. # will not be rendered.
weight = 0 weight = 0
# A draft page will not be present in prev/next pagination # A draft page is only loaded if the `--drafts` flag is passed to `zola build`, `zola serve` or `zola check`
draft = false draft = false
# If filled, it will use that slug instead of the filename to make up the URL # If filled, it will use that slug instead of the filename to make up the URL
@ -87,9 +86,6 @@ template = "page.html"
# Your own data # Your own data
[extra] [extra]
+++
Some content
``` ```
## Summary ## Summary

View file

@ -33,8 +33,7 @@ Here is an example `_index.md` with all the variables available. The values pro
default values. default values.
```md ```toml
+++
title = "" title = ""
description = "" description = ""
@ -95,9 +94,6 @@ aliases = []
# Your own data # Your own data
[extra] [extra]
+++
Some content
``` ```
Keep in mind that any configuration apply only to the direct pages, not to the subsections' pages. Keep in mind that any configuration apply only to the direct pages, not to the subsections' pages.

View file

@ -28,8 +28,7 @@ categories = ["programming"]
+++ +++
``` ```
The taxonomy pages will only be created if at least one non-draft page is found and The taxonomy pages are available at the following paths:
are available at the following paths:
```plain ```plain
$BASE_URL/$NAME/ $BASE_URL/$NAME/

View file

@ -4,8 +4,7 @@ weight = 30
+++ +++
By default, GitHub Pages uses Jekyll (A ruby based static site generator), By default, GitHub Pages uses Jekyll (A ruby based static site generator),
but you can use whatever you want provided you have an `index.html` file in the root of a branch called `gh-pages`. but you can also publish any generated files provided you have an `index.html` file in the root of a branch called `gh-pages` or `master`, in addition you can also publish from a `docs` directory in your repository. That branch name can also be manually changed in the settings of a repository. **However** this only applies to publishing in a custom domain, i.e. if you want to publish to a GitHub provided web service under the `github.io` domain, you can **only** use the `master` branch of your repository as explained [here](https://help.github.com/en/articles/configuring-a-publishing-source-for-github-pages), so we will focus on the method which will work regardless of the domain.
That branch name can also be manually changed in the settings of a repository.
We can use any CI server to build and deploy our site. For example: We can use any CI server to build and deploy our site. For example:
@ -45,13 +44,15 @@ Make sure "Display value in build log" is off, and then click add. Now Travis ha
We're almost done. We just need some scripts in a .travis.yml file to tell Travis what to do. We're almost done. We just need some scripts in a .travis.yml file to tell Travis what to do.
**NOTE**: The script below assumes that we're taking the code from the `code` branch and will generate the HTML to be published in the `master` branch of the same repository. You're free to use any other branch for the Markdown files but if you want to use `<username>.github.io` or `<org>.github.io`, the destination branch **MUST** be `master`.
```yaml ```yaml
language: minimal language: minimal
before_script: before_script:
# Download and unzip the zola executable # Download and unzip the zola executable
# Replace the version numbers in the URL by the version you want to use # 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.8.0/zola-v0.8.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.9.0/zola-v0.9.0-x86_64-unknown-linux-gnu.tar.gz | sudo tar xvzf - -C /usr/local/bin
script: script:
- zola build - zola build
@ -59,12 +60,12 @@ script:
# If you are using a different folder than `public` for the output directory, you will # If you are using a different folder than `public` for the output directory, you will
# need to change the `zola` command and the `ghp-import` path # need to change the `zola` command and the `ghp-import` path
after_success: | after_success: |
[ $TRAVIS_BRANCH = master ] && [ $TRAVIS_BRANCH = code ] &&
[ $TRAVIS_PULL_REQUEST = false ] && [ $TRAVIS_PULL_REQUEST = false ] &&
zola build && zola build &&
sudo pip install ghp-import && sudo pip install ghp-import &&
ghp-import -n public && ghp-import -n public -b master &&
git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git master
``` ```
If your site is using a custom domain, you will need to mention it in the `ghp-import` command: `ghp-import -c vaporsoft.net -n public` If your site is using a custom domain, you will need to mention it in the `ghp-import` command: `ghp-import -c vaporsoft.net -n public`

View file

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

View file

@ -14,8 +14,10 @@ If you don't have an account with Netlify, you can [sign up](https://app.netlify
Once you are in the admin interface, you can add a site from a Git provider (GitHub, GitLab or Bitbucket). At the end 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: of this process, you can select the deploy settings for the project:
- build command: `ZOLA_VERSION=0.8.0 zola build` (replace the version number in the variable by the version you want to use) - build command: `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 - publish directory: the path to where the `public` directory is
- image selection: `Ubuntu Xenial 16.04 (default)`
- Environment variables: `ZOLA_VERSION` with for example `0.8.0` as value
With this setup, your site should be automatically deployed on every commit on master. For `ZOLA_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 use any of the tagged `release` versions in the GitHub repository — Netlify will automatically fetch the tagged version
@ -36,7 +38,7 @@ command = "zola build"
[build.environment] [build.environment]
# Set the version name that you want to use and Netlify will automatically use it # Set the version name that you want to use and Netlify will automatically use it
ZOLA_VERSION = "0.8.0" ZOLA_VERSION = "0.9.0"
# The magic for deploying previews of branches # The magic for deploying previews of branches
# We need to override the base url with whatever url Netlify assigns to our # We need to override the base url with whatever url Netlify assigns to our

View file

@ -3,21 +3,29 @@ title = "CLI usage"
weight = 2 weight = 2
+++ +++
Zola only has 3 commands: init, build and serve. Zola only has 4 commands: `init`, `build`, `serve` and `check`.
You can view the help of the whole program by running `zola --help` and You can view the help of the whole program by running `zola --help` and
the command help by running `zola <cmd> --help`. the command help by running `zola <cmd> --help`.
## init ## init
Creates the directory structure used by Zola at the given directory. Creates the directory structure used by Zola at the given directory after asking a few basic configuration questions.
Any choices made during those prompts can easily be changed by modifying the `config.toml`.
```bash ```bash
$ zola init my_site $ zola init my_site
$ zola init
``` ```
will create a new folder named `my_site` and the files/folders needed by If the `my_site` folder already exists, Zola will only populate it if it does not contain non-hidden files (dotfiles are ignored). If no `my_site` argument is passed, Zola will try to populate the current directory.
zola.
You can initialize a git repository and a Zola site directly from within a new folder:
```bash
$ git init
$ zola init
```
## build ## build
@ -48,6 +56,8 @@ You can also point to another config file than `config.toml` like so - the posit
$ zola --config config.staging.toml build $ zola --config config.staging.toml build
``` ```
By defaults, drafts are not loaded. If you wish to include them, pass the `--drafts` flag.
## serve ## serve
This will build and serve the site using a local server. You can also specify This will build and serve the site using a local server. You can also specify
@ -56,6 +66,9 @@ the interface/port combination to use if you want something different than the d
You can also specify different addresses for the interface and base_url using `-u`/`--base-url`, for example You can also specify different addresses for the interface and base_url using `-u`/`--base-url`, for example
if you are running zola in a Docker container. if you are running zola in a Docker container.
Use the `--open` flag to automatically open the locally hosted instance in your
web browser.
In the event you don't want zola to run a local webserver, you can use the `--watch-only` flag. In the event you don't want zola to run a local webserver, you can use the `--watch-only` flag.
Before starting, it will delete the public directory to ensure it starts from a clean slate. Before starting, it will delete the public directory to ensure it starts from a clean slate.
@ -68,6 +81,7 @@ $ zola serve --interface 0.0.0.0 --port 2000
$ zola serve --interface 0.0.0.0 --base-url 127.0.0.1 $ zola serve --interface 0.0.0.0 --base-url 127.0.0.1
$ zola serve --interface 0.0.0.0 --port 2000 --output-dir www/public $ zola serve --interface 0.0.0.0 --port 2000 --output-dir www/public
$ zola serve --watch-only $ zola serve --watch-only
$ zola serve --open
``` ```
The serve command will watch all your content and will provide live reload, without The serve command will watch all your content and will provide live reload, without
@ -83,10 +97,15 @@ You can also point to another config file than `config.toml` like so - the posit
$ zola --config config.staging.toml serve $ zola --config config.staging.toml serve
``` ```
By defaults, drafts are not loaded. If you wish to include them, pass the `--drafts` flag.
### check ### check
The check subcommand will try to build all pages just like the build command would, but without writing any of the 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. results to disk. Additionally, it will also check all external links present in Markdown files by trying to fetch
them (links present in the template files will not be checked).
By defaults, drafts are not loaded. If you wish to include them, pass the `--drafts` flag.
## Colored output ## Colored output

View file

@ -41,10 +41,17 @@ generate_rss = false
# not set (the default). # not set (the default).
# rss_limit = 20 # rss_limit = 20
# Whether to copy or hardlink files in static/ directory. Useful for sites
# whose static files are large. Note that for this to work, both static/ and
# output directory need to be on the same filesystem. Also, theme's static/
# files are always copies, regardles of this setting. False by default.
# hard_link_static = false
# The taxonomies to be rendered for that site and their configuration # The taxonomies to be rendered for that site and their configuration
# Example: # Example:
# taxonomies = [ # taxonomies = [
# {name = "tags", rss = true}, # each tag will have its own RSS feed # {name = "tags", rss = true}, # each tag will have its own RSS feed
# {name = "tags", lang = "fr"}, # you can have taxonomies with the same name in multiple languages
# {name = "categories", paginate_by = 5}, # 5 items per page for a term # {name = "categories", paginate_by = 5}, # 5 items per page for a term
# {name = "authors"}, # Basic definition: no RSS or pagination # {name = "authors"}, # Basic definition: no RSS or pagination
# ] # ]
@ -55,6 +62,7 @@ taxonomies = []
# Example: # Example:
# languages = [ # languages = [
# {code = "fr", rss = true}, # there will be a RSS feed for French content # {code = "fr", rss = true}, # there will be a RSS feed for French content
# {code = "fr", search = true}, # there will be a Search Index for French content
# {code = "it"}, # there won't be a RSS feed for Italian content # {code = "it"}, # there won't be a RSS feed for Italian content
# ] # ]
# #
@ -121,6 +129,8 @@ Zola currently has the following highlight themes available:
- [ayu-light](https://github.com/dempfi/ayu) - [ayu-light](https://github.com/dempfi/ayu)
- [ayu-dark](https://github.com/dempfi/ayu) - [ayu-dark](https://github.com/dempfi/ayu)
- [ayu-mirage](https://github.com/dempfi/ayu) - [ayu-mirage](https://github.com/dempfi/ayu)
- [Tomorrow](https://tmtheme-editor.herokuapp.com/#!/editor/theme/Tomorrow)
- [one-dark](https://github.com/andresmichel/one-dark-theme)
Zola uses the Sublime Text themes, making it very easy to add more. Zola uses the Sublime Text themes, making it very easy to add more.
If you want a theme not on that list, please open an issue or a pull request on the [Zola repo](https://github.com/getzola/zola). If you want a theme not on that list, please open an issue or a pull request on the [Zola repo](https://github.com/getzola/zola).

View file

@ -38,6 +38,8 @@ The directory structure of the `sass` folder will be preserved when copying over
## `static` ## `static`
Contains any kind of files. All the files/folders in the `static` folder will be copied as-is in the output directory. Contains any kind of files. All the files/folders in the `static` folder will be copied as-is in the output directory.
If your static files are large you can configure Zola to [hard link](https://en.wikipedia.org/wiki/Hard_link) them
instead of copying by setting `hard_link_static = true` in the config file.
## `templates` ## `templates`
Contains all the [Tera](https://tera.netlify.com) templates that will be used to render this site. Contains all the [Tera](https://tera.netlify.com) templates that will be used to render this site.

View file

@ -5,33 +5,34 @@ weight = 30
Two things can get paginated: a section and a taxonomy term. Two things can get paginated: a section and a taxonomy term.
A paginated section gets the same `section` variable as a normal Both kinds get a `paginator` variable of the `Pager` type, on top of the common variables mentioned in the
[section page](@/documentation/templates/pages-sections.md#section-variables) minus its pages [overview page](@/documentation/templates/overview.md):
while both a paginated taxonomy page and a paginated section page gets a
`paginator` variable of the `Pager` type:
```ts ```ts
// How many items per page // How many items per pager
paginate_by: Number; paginate_by: Number;
// The base URL for the pagination: section permalink + pagination path // The base URL for the pagination: section permalink + pagination path
// You can concatenate an integer with that to get a link to a given pagination page. // You can concatenate an integer with that to get a link to a given pagination pager.
base_url: String; base_url: String;
// How many pagers in this paginator // How many pagers in total
number_pagers: Number; number_pagers: Number;
// Permalink to the first page // Permalink to the first pager
first: String; first: String;
// Permalink to the last page // Permalink to the last pager
last: String; last: String;
// Permalink to the previous page, if there is one // Permalink to the previous pager, if there is one
previous: String?; previous: String?;
// Permalink to the next page, if there is one // Permalink to the next pager, if there is one
next: String?; next: String?;
// All pages for the current page // All pages for the current pager
pages: Array<Page>; pages: Array<Page>;
// Which page are we on // Which pager are we on
current_index: Number; current_index: Number;
``` ```
A pager is a page of the pagination: if you have 100 pages and are paginating 10 by 10, you will have 10 pagers containing
each 10 pages.
## Section ## Section
A paginated section gets the same `section` variable as a normal A paginated section gets the same `section` variable as a normal

View file

@ -8,10 +8,11 @@ generate an `rss.xml` page for the site, which will live at `base_url/rss.xml`.
generate the `rss.xml` page, Zola will look for a `rss.xml` file in the `templates` generate the `rss.xml` page, Zola will look for a `rss.xml` file in the `templates`
directory or, if one does not exist, will use the use the built-in rss template. directory or, if one does not exist, will use the use the built-in rss template.
**Only pages with a date and that are not draft will be available.** **Only pages with a date will be available.**
The RSS template gets two variables in addition of the config: The RSS template gets three variables in addition of the config:
- `feed_url`: the full url to that specific feed
- `last_build_date`: the date of the latest post - `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 a detailed description of what this contains

View file

@ -43,6 +43,8 @@ current_url: String;
current_path: String; current_path: String;
// All terms for that taxonomy // All terms for that taxonomy
terms: Array<TaxonomyTerm>; terms: Array<TaxonomyTerm>;
// The lang of the current page
lang: String;
``` ```
@ -58,6 +60,8 @@ current_url: String;
current_path: String; current_path: String;
// The current term being rendered // The current term being rendered
term: TaxonomyTerm; term: TaxonomyTerm;
// The lang of the current page
lang: String;
``` ```
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)

View file

@ -6,7 +6,7 @@ template = "theme.html"
date = 2018-09-03T02:13:01-04:00 date = 2018-09-03T02:13:01-04:00
[extra] [extra]
created = 2019-04-06T11:27:43+02:00 created = 2019-07-12T23:49:55+02:00
updated = 2018-09-03T02:13:01-04:00 updated = 2018-09-03T02:13:01-04:00
repository = "https://github.com/InsidiousMind/Ergo" repository = "https://github.com/InsidiousMind/Ergo"
homepage = "https://github.com/InsidiousMind/Ergo" homepage = "https://github.com/InsidiousMind/Ergo"
@ -23,7 +23,7 @@ homepage = "https://code.liquidthink.net"
![Ergo Screenshot](https://i.imgur.com/l182IYg.jpg) ![Ergo Screenshot](https://i.imgur.com/l182IYg.jpg)
A light, simple & beautiful Gutenberg theme made with a focus on writing. Inspired by sbvtle and Pixyll. A light, simple & beautiful Zola theme made with a focus on writing. Inspired by sbvtle and Pixyll.
Like both those web designs, Ergo is a theme that emphasizes content, but still tries to be stylish. Frankly, the design is Like both those web designs, Ergo is a theme that emphasizes content, but still tries to be stylish. Frankly, the design is
most like sbvtle (http://sbvtle.com) but without the clever svbtle Engine, Javascript, community or kudos button (kudos is on the list of additions, though! But then i'll have to use JS...) most like sbvtle (http://sbvtle.com) but without the clever svbtle Engine, Javascript, community or kudos button (kudos is on the list of additions, though! But then i'll have to use JS...)
@ -37,17 +37,17 @@ Here's a timelapse:
## Installation ## Installation
Get [Gutenberg](https://www.getgutenberg.io/) and/or follow their guide on [installing a theme](https://www.getgutenberg.io/documentation/themes/installing-and-using-themes/). Get [Zola](https://www.getzola.org/) and/or follow their guide on [installing a theme](https://www.getzola.org/documentation/themes/installing-and-using-themes/).
Make sure to add `theme = "ergo"` to your `config.toml` Make sure to add `theme = "ergo"` to your `config.toml`
#### Check gutenberg version (only 0.4.1+) #### Check zola version (only 0.4.1+)
Just to double-check to make sure you have the right version. It is not supported to use this theme with a version under 0.4.1. Just to double-check to make sure you have the right version. It is not supported to use this theme with a version under 0.4.1.
### how to serve ### how to serve
go into your sites directory, and type `gutenberg serve`. You should see your new site at `localhost:1111`. go into your sites directory, and type `zola serve`. You should see your new site at `localhost:1111`.
### Deployment to Github Pages or Netlify ### Deployment to Github Pages or Netlify
[Gutenberg](https://www.getgutenberg.io) already has great documentation for deploying to [Netlify](https://www.getgutenberg.io/documentation/deployment/netlify/) or [Github Pages](https://www.getgutenberg.io/documentation/deployment/github-pages/). I won't bore you with a regurgitated explanation. [Zola](https://www.getzola.org) already has great documentation for deploying to [Netlify](https://www.getzola.org/documentation/deployment/netlify/) or [Github Pages](https://www.getzola.org/documentation/deployment/github-pages/). I won't bore you with a regurgitated explanation.
### Customizing the Theme ### Customizing the Theme
All colors used on the site are from `sass/colors.scss`. There's only about 5-6 colors total. All colors used on the site are from `sass/colors.scss`. There's only about 5-6 colors total.
@ -63,7 +63,7 @@ profile = 'profile.svg'
website = "code.liquidthink.net" website = "code.liquidthink.net"
# github # github
github = "InsidiousMind" # case does not matter github = "Insipx" # case does not matter
# twitter # twitter
twitter = "liquid_think" twitter = "liquid_think"
# email # email

View file

@ -0,0 +1,240 @@
+++
title = "Zulma"
description = "A zola theme based off bulma.css"
template = "theme.html"
date = 2019-05-12T22:44:07+01:00
[extra]
created = 2019-07-12T23:55:11+02:00
updated = 2019-05-12T22:44:07+01:00
repository = "https://github.com/Worble/Zulma"
homepage = "https://github.com/Worble/Zulma"
minimum_version = "0.6.0"
license = "MIT"
demo = "https://festive-morse-47d46c.netlify.com/"
[extra.author]
name = "Worble"
homepage = ""
+++
# Zulma
A Bulma theme for Zola. See a live preview [here](https://festive-morse-47d46c.netlify.com/)
![Zulma Screenshot](/screenshot.png)
## Contents
- [Zulma](#zulma)
- [Contents](#contents)
- [Installation](#installation)
- [Javascript](#javascript)
- [Sources](#sources)
- [Building](#building)
- [Options](#options)
- [Pagination](#pagination)
- [Taxonomies](#taxonomies)
- [Menu Links](#menu-links)
- [Brand](#brand)
- [Search](#search)
- [Title](#title)
- [Theming](#theming)
- [Original](#original)
- [Known Bugs](#known-bugs)
## Installation
First download this theme to your `themes` directory:
```bash
cd themes
git clone https://github.com/Worble/Zulma
```
and then enable it in your `config.toml`:
```toml
theme = "zulma"
```
That's it! No more configuration should be required, however it might look a little basic. Head to the [Options](#options) section to see what you can set for more customizability.
## Javascript
### Sources
All the source javascript files live in `javascript/src`. Following is a list of the javascript files, their purpose, and their sources. All files are prefixed with `zulma_` to avoid any name clashes.
- `zulma_search.js` - Used when a user types into the search box on the navbar (if enabled). Taken from [Zola's site](https://github.com/getzola/zola/blob/6100a43/docs/static/search.js).
- `zulma_navbar.js` - Used for the mobile navbar toggle. Taken from the [bulma template](https://github.com/dansup/bulma-templates/blob/6263eb7/js/bulma.js) at Bulmaswatch
- `zulma_switchcss.js` - Used for swapping themes (if enabled).
### Building
The javascript files are transpiled by babel, minified by webpack, sourcemaps are generated and then everything placed in `static/js`. The repo already contains the transpiled and minified files along with their corrosponding sourcemaps so you don't need to do anything to use these. If you would prefer to build it yourself, feel free to inspect the js files and then run the build process yourself (please ensure that you have [node, npm](https://nodejs.org/en/) and optionally [yarn](https://yarnpkg.com/lang/en/) installed):
```bash
cd javascript
yarn
yarn webpack
```
## Options
### Pagination
Zulma makes no assumptions about your project. You can freely paginate your content folder or your taxonomies and it will adapt accordingly. For example, editing or creating section (`content/_index.md`) and setting pagination:
```toml
paginate_by = 5
```
This is handled internally, no input is needed from the user.
### Taxonomies
Zulma has 3 taxonomies already set internally: `tags`, `cateogories` and `authors`. Setting of any these three in your config.toml like so:
```toml
taxonomies = [
{name = "categories"},
{name = "tags", paginate_by = 5, rss = true},
{name = "authors", rss = true},
]
```
and setting any of them in a content file:
```toml
[taxonomies]
categories = ["Hello world"]
tags = ["rust", "ssg", "other", "test"]
authors = ["Joe Bloggs"]
```
will cause that metadata to appear on the post, either on the header for the name, or at the bottom for tags and categories, and enable those pages.
Making your own taxonomies is also designed to be as easy as possible. First, add it to your cargo.toml
```toml
taxonomies = [
{name = "links"},
]
```
and make the corrosponding folder in your templates, in this case: `templates\links`, and the necessary files: `templates\links\list.html` and `templates\links\single.html`
And then for each, just inherit the taxonomy master page for that page. Before rendering the content block, you may optionally set a variable called `title` for the hero to display on that page, otherwise it will use the default for that taxonomy.
In `single.html`:
```jinja
{%/* extends "Zulma/templates/taxonomy_single.html" */%}
```
In `list.html`:
```jinja
{%/* extends "Zulma/templates/taxonomy_list.html" */%}
{%/* block content */%}
{%/* set title = "These are all the Links"*/%}
{{/* super() */}}
{%/* endblock content */%}
```
### Menu Links
In extra, setting `zulma_menu` with a list of items will cause them to render to the top menu bar. It has two paramers, `url` and `name`. These _must_ be set. If you put \$BASE_URL in a url, it will automatically be replaced by the actual site URL. This is the easiest way to allow users to navigate to your taxonomies:
```toml
[extra]
zulma_menu = [
{url = "$BASE_URL/categories", name = "Categories"},
{url = "$BASE_URL/tags", name = "Tags"},
{url = "$BASE_URL/authors", name = "Authors"}
]
```
On mobile, a dropdown burger is rendered using javascript. If the page detects javascript is disabled on the clients machine, it will gracefully degrade to always showing the menu (which isn't pretty, but keeps the site functional).
### Brand
In extra, setting `zulma_brand` will cause a brand image to display in the upper left of the top menu bar. This link will always lead back to the homepage. It has two parameters, `image`(optional) and `text`(required). `image` will set the brand to an image at the location specified, and `text` will provide the alt text for this image. If you put \$BASE_URL in a url, it will automatically be replaced by the actual site URL. If `image` is not set, the brand will simply be the text specified.
```toml
[extra]
zulma_brand = {image = "$BASE_URL/images/bulma.png", text = "Home"}
```
### Search
Zulma provides search built in. So long as `build_search_index` is set to `true` in `config.toml` then a search input will appear on the top navigation bar. This requires javascript to be enabled to function; if the page detects javascript is disabled on the clients machine, it will hide itself.
The search is shamefully stolen from [Zola's site](https://github.com/getzola/zola/blob/master/docs/static/search.js). Thanks, Vincent!
### Title
In extra, setting `zulma_title` will set a hero banner on the index page to appear with that title inside.
```toml
[extra]
zulma_title = "Blog"
```
If you want to get fancy with it, you can set an image behind using sass like so:
```scss
.index .hero-body {
background-image: url(https://upload.wikimedia.org/wikipedia/commons/thumb/f/f6/Plum_trees_Kitano_Tenmangu.jpg/1200px-Plum_trees_Kitano_Tenmangu.jpg);
background-position: center;
background-size: cover;
background-repeat: no-repeat;
background-color: rgba(0, 0, 0, 0.6);
background-blend-mode: overlay;
}
```
This will set the image behind the hero, and darken it so the main text can still be easily read.
### Theming
In extra, setting `zulma_theme` to a valid value will change the current colour scheme to that one. All themes were taken from [Bulmaswatch](https://jenil.github.io/bulmaswatch/). Valid theme values are:
- default
- darkly
- flatly
- pulse
- simplex
- lux
- slate
- solar
- superhero
All valid themes can also be found under the `extra.zulma_themes` variable in the `theme.toml`. Choosing no theme will set default as the theme. Setting an invalid theme value will cause the site to render improperly.
```toml
[extra]
zulma_theme = "darkly"
```
Additionally, in extra, you can also set the `zulma_allow_theme_selection` boolean. Setting this to `true` will allow a menu in the footer to allow users to select their own theme. This option will store their theme choice in their localstorage and apply it on every page, assuming `zulma_allow_theme_selection` is still true. This requires javascript to be enabled to function; if the page detects javascript is disabled on the clients machine, it will hide itself.
Each theme contains the entirety of Bulma, and will weigh in at ~180kb. If you're running on a server severely limited on space, then I'd recommend you delete each theme you're not using, either from the source or from `/public`. Obviously, doing this will cause `zulma_allow_theme_selection` to work improperly, so make sure you either override `extra.zulma_themes` in `config.toml` to only show themes you have left or to not enable this option at all.
```toml
[extra]
zulma_allow_theme_selection = true
```
## Original
This template is based on the [blog template](https://dansup.github.io/bulma-templates/templates/blog.html) over at [Free Bulma Templates](https://dansup.github.io/bulma-templates/). All themes were taken from [Bulmaswatch](https://jenil.github.io/bulmaswatch/). The code behind from originally adapted from the [after-dark](https://github.com/getzola/after-dark/blob/master/README.md) zola template.
## Known Bugs
- If user theme swapping is enabled and the user selects a theme different to the default, a slight delay will be introduced in page rendering as the css gets swapped out and in by the javascript. This is particularly pronounced when using the dark theme, since it will flash white before going back to black. This is better than the alternative flashes of unstyled content or old theme, but still annoying. I don't know any way around this, but with browser caching it should be fast enough to not cause serious issues.

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -6,7 +6,7 @@ template = "theme.html"
date = 2018-01-25T18:44:44+01:00 date = 2018-01-25T18:44:44+01:00
[extra] [extra]
created = 2019-04-06T11:27:43+02:00 created = 2019-07-12T23:49:55+02:00
updated = 2018-01-25T18:44:44+01:00 updated = 2018-01-25T18:44:44+01:00
repository = "https://github.com/getzola/even" repository = "https://github.com/getzola/even"
homepage = "https://github.com/getzola/even" homepage = "https://github.com/getzola/even"

View file

@ -6,7 +6,7 @@ template = "theme.html"
date = 2018-01-21T04:35:36-05:00 date = 2018-01-21T04:35:36-05:00
[extra] [extra]
created = 2019-04-06T11:27:43+02:00 created = 2019-07-12T23:49:55+02:00
updated = 2018-01-21T04:35:36-05:00 updated = 2018-01-21T04:35:36-05:00
repository = "https://github.com/piedoom/feather" repository = "https://github.com/piedoom/feather"
homepage = "https://github.com/piedoom/feather" homepage = "https://github.com/piedoom/feather"

View file

@ -0,0 +1,136 @@
+++
title = "hallo"
description = "A single-page theme to introduce yourself."
template = "theme.html"
date = 2019-06-05T15:08:48+02:00
[extra]
created = 2019-07-12T23:55:11+02:00
updated = 2019-06-05T15:08:48+02:00
repository = "https://github.com/flyingP0tat0/zola-hallo.git"
homepage = "https://github.com/janbaudisch/zola-hallo"
minimum_version = "0.4.0"
license = "MIT"
demo = "https://zola-hallo.janbaudisch.dev"
[extra.author]
name = "Jan Baudisch"
homepage = "https://janbaudisch.dev"
+++
[![Build Status][build-img]][build-url]
[![Demo][demo-img]][demo-url]
# Hallo
> A single-page theme to introduce yourself.
>
> [Zola][zola] port of [hallo-hugo][hallo-hugo].
![Screenshot](screenshot.png)
## Original
This is a port of the original [hallo-hugo][hallo-hugo] theme for Hugo ([License][upstream-license]).
## Installation
The easiest way to install this theme is to either clone it ...
```
git clone https://github.com/janbaudisch/zola-hallo.git themes/hallo
```
... or to use it as a submodule.
```
git submodule add https://github.com/janbaudisch/zola-hallo.git themes/hallo
```
Either way, you will have to enable the theme in your `config.toml`.
```toml
theme = "hallo"
```
### Introduction
The introduction text is included from `templates/partials/introduction.html`.
You will need to create this file and fill it with content.
## Options
See [`config.toml`][config] for an example configuration.
### Author
The given name will be used for the 'I am ...' text.
Default: `Hallo`
```toml
[extra.author]
name = "Hallo"
```
### Greeting
The string will be used as a greeting.
Default: `Hello!`
```toml
[extra]
greeting = "Hello!"
```
### `iam`
This variable defines the `I am` text, which you may want to swap out for another language.
Default: `I am`
```toml
[extra]
iam = "I am"
```
### Links
Links show up below the introduction. They are styled with [Font Awesome][fontawesome], you may optionally choose the iconset (default is [brands][fontawesome-brands]).
```toml
[extra]
links = [
{ title = "E-Mail", url = "mailto:mail@example.org", iconset = "fas", icon = "envelope" },
{ title = "GitHub", url = "https://github.com", icon = "github" },
{ title = "Twitter", url = "https://twitter.com", icon = "twitter" }
]
```
### Theme
Change the colors used.
```toml
[extra.theme]
background = "#6FCDBD"
foreground = "#FFF" # text and portrait border
hover = "#333" # link hover
```
[build-img]: https://travis-ci.com/janbaudisch/zola-hallo.svg?branch=master
[build-url]: https://travis-ci.com/janbaudisch/zola-hallo
[demo-img]: https://img.shields.io/badge/demo-live-green.svg
[demo-url]: https://zola-hallo.janbaudisch.dev
[zola]: https://www.getzola.org
[hallo-hugo]: https://github.com/EmielH/hallo-hugo
[fontawesome]: https://fontawesome.com
[fontawesome-brands]: https://fontawesome.com/icons?d=gallery&s=brands&m=free
[upstream-license]: https://github.com/janbaudisch/zola-hallo/blob/master/upstream/LICENSE
[config]: https://github.com/janbaudisch/zola-hallo/blob/master/config.toml

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -0,0 +1,168 @@
+++
title = "sam"
description = "A Simple and Minimalist theme with a focus on typography and content."
template = "theme.html"
date = 2019-07-02T17:55:24+02:00
[extra]
created = 2019-07-02T17:55:24+02:00
updated = 2019-07-02T17:55:24+02:00
repository = "https://github.com/janbaudisch/zola-sam.git"
homepage = "https://github.com/janbaudisch/zola-sam"
minimum_version = "0.4.0"
license = "AGPL-3.0"
demo = "https://zola-sam.janbaudisch.dev"
[extra.author]
name = "Jan Baudisch"
homepage = "https://janbaudisch.dev"
+++
[![Build Status][build-img]][build-url]
[![Demo][demo-img]][demo-url]
# Sam
> A Simple and Minimalist theme with a focus on typography and content.
>
> [Zola][zola] port of [hugo-theme-sam][hugo-sam].
![Screenshot](screenshot.png)
## Original
This is a port of the original [hugo-theme-sam][hugo-sam] theme for Hugo ([License][upstream-license]).
See [`upstream`][upstream] for source code take from there.
## Installation
The easiest way to install this theme is to either clone it ...
```
git clone https://github.com/janbaudisch/zola-sam.git themes/sam
```
... or to use it as a submodule.
```
git submodule add https://github.com/janbaudisch/zola-sam.git themes/sam
```
Either way, you will have to enable the theme in your `config.toml`.
```toml
theme = "sam"
```
## Options
See [`config.toml`][config] for an example configuration.
### Menu
The menu on the index page is created as follows: If the `sam_menu` variable is set, it gets used.
```toml
[extra]
sam_menu = [
{ text = "posts", link = "/posts" },
{ text = "about", link = "/about" },
{ text = "github", link = "https://github.com" }
]
```
If it is not set, all sections under `content` will get linked.
#### Bottom menu
This variable decides wether the menu - as mentioned above - will also be displayed at the bottom of pages.
Default: `false`
```toml
[extra]
sam_bottom_menu = true
```
### `home`
Sets the name for all links referring to the home page in the menus and the 404 page.
Default: `home`
```toml
[extra]
home = "home"
```
### Date format
Specifies how to display dates. The format is described [here][date-format-docs].
Default: `%a %b %e, %Y`
```toml
[extra]
date_format = "%a %b %e, %Y"
```
### Word count and reading time
You can enable or disable word count and reading time for posts across the whole site:
Default: `true` (both)
```toml
[extra]
show_word_count = true
show_reading_time = true
```
If enabled, you can opt-out per page via front-matter:
Default: `false` (both)
```
+++
[extra]
hide_word_count = true
hide_reading_time = true
+++
```
### Disable page header
If you want to disable the complete header of a page (for example a page which is explicitly not a post), you can do so via front-matter:
Default: `false`
```
+++
[extra]
no_header = true
+++
```
### Footer
To place some text at the end of pages, set the following:
```toml
[extra.sam_footer]
text = "Some footer text."
```
[build-img]: https://travis-ci.com/janbaudisch/zola-sam.svg?branch=master
[build-url]: https://travis-ci.com/janbaudisch/zola-sam
[demo-img]: https://img.shields.io/badge/demo-live-green.svg
[demo-url]: https://zola-sam.janbaudisch.dev
[zola]: https://getzola.org
[hugo-sam]: https://github.com/victoriadotdev/hugo-theme-sam
[upstream]: https://github.com/janbaudisch/zola-sam/blob/master/upstream
[upstream-license]: https://github.com/janbaudisch/zola-sam/blob/master/upstream/LICENSE
[config]: https://github.com/janbaudisch/zola-sam/blob/master/config.toml
[date-format-docs]: https://docs.rs/chrono/latest/chrono/format/strftime/index.html

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -6,7 +6,7 @@ template = "theme.html"
date = 2019-01-27T19:57:59+01:00 date = 2019-01-27T19:57:59+01:00
[extra] [extra]
created = 2019-04-06T11:27:43+02:00 created = 2019-07-12T23:49:55+02:00
updated = 2019-01-27T19:57:59+01:00 updated = 2019-01-27T19:57:59+01:00
repository = "https://github.com/waynee95/zola-theme-hikari" repository = "https://github.com/waynee95/zola-theme-hikari"
homepage = "https://github.com/waynee95/zola-theme-hikari" homepage = "https://github.com/waynee95/zola-theme-hikari"
@ -21,13 +21,13 @@ homepage = "https://waynee95.me"
# hikari # hikari
> this is a port of the [hikari theme](https://github.com/mx3m/hikari-for-jekyll) for Zola > this is a port of the [hikari theme](https://github.com/mx3m/hikari-for-jekyll) for [Zola](https://www.getzola.org/)
Hikari is a simple theme perfect for dev-savvy bloggers. Hikari is a simple theme perfect for dev-savvy bloggers.
![screenshot](screenshot.png) ![screenshot](screenshot.png)
[View demo](https://waynee95.me/zola-theme-hikari) [View demo](https://waynee95.github.io/zola-theme-hikari/)
## Installation ## Installation

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View file

@ -99,7 +99,7 @@
{% endblock content %} {% endblock content %}
</div> </div>
<footer> <footer>
©2017-2018<a class="white" href="http://vincentprouillet.com">Vincent Prouillet</a> and <a class="white" href="https://github.com/getzola/zola/graphs/contributors">contributors</a> ©2017-2019<a class="white" href="http://vincentprouillet.com">Vincent Prouillet</a> and <a class="white" href="https://github.com/getzola/zola/graphs/contributors">contributors</a>
</footer> </footer>
<script type="text/javascript" src="{{ get_url(path="elasticlunr.min.js") }}"></script> <script type="text/javascript" src="{{ get_url(path="elasticlunr.min.js") }}"></script>

View file

@ -3,6 +3,5 @@
<a href="{{ get_url(path=asset) | safe }}" target="_blank"> <a href="{{ get_url(path=asset) | safe }}" target="_blank">
<img src="{{ resize_image(path=asset, width=240, height=180, op="fill", format="auto") | safe }}" /> <img src="{{ resize_image(path=asset, width=240, height=180, op="fill", format="auto") | safe }}" />
</a> </a>
&ensp;
{%- endif %} {%- endif %}
{%- endfor %} {%- endfor %}

View file

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

View file

@ -19,7 +19,7 @@ pub fn build_cli() -> App<'static, 'static> {
.about("Create a new Zola project") .about("Create a new Zola project")
.arg( .arg(
Arg::with_name("name") Arg::with_name("name")
.required(true) .default_value(".")
.help("Name of the project. Will create a new directory with that name in the current directory") .help("Name of the project. Will create a new directory with that name in the current directory")
), ),
SubCommand::with_name("build") SubCommand::with_name("build")
@ -36,6 +36,10 @@ pub fn build_cli() -> App<'static, 'static> {
.default_value("public") .default_value("public")
.takes_value(true) .takes_value(true)
.help("Outputs the generated site in the given path"), .help("Outputs the generated site in the given path"),
Arg::with_name("drafts")
.long("drafts")
.takes_value(false)
.help("Include drafts when loading the site"),
]), ]),
SubCommand::with_name("serve") SubCommand::with_name("serve")
.about("Serve the site. Rebuild and reload on change automatically") .about("Serve the site. Rebuild and reload on change automatically")
@ -65,9 +69,24 @@ pub fn build_cli() -> App<'static, 'static> {
Arg::with_name("watch_only") Arg::with_name("watch_only")
.long("watch-only") .long("watch-only")
.takes_value(false) .takes_value(false)
.help("Do not start a server, just re-build project on changes") .help("Do not start a server, just re-build project on changes"),
Arg::with_name("drafts")
.long("drafts")
.takes_value(false)
.help("Include drafts when loading the site"),
Arg::with_name("open")
.short("O")
.long("open")
.takes_value(false)
.help("Open site in the default browser"),
]), ]),
SubCommand::with_name("check") SubCommand::with_name("check")
.about("Try building the project without rendering it. Checks links") .about("Try building the project without rendering it. Checks links")
.args(&[
Arg::with_name("drafts")
.long("drafts")
.takes_value(false)
.help("Include drafts when loading the site"),
])
]) ])
} }

View file

@ -5,12 +5,20 @@ use site::Site;
use console; use console;
pub fn build(config_file: &str, base_url: Option<&str>, output_dir: &str) -> Result<()> { pub fn build(
config_file: &str,
base_url: Option<&str>,
output_dir: &str,
include_drafts: bool,
) -> Result<()> {
let mut site = Site::new(env::current_dir().unwrap(), config_file)?; let mut site = Site::new(env::current_dir().unwrap(), config_file)?;
site.set_output_path(output_dir); site.set_output_path(output_dir);
if let Some(b) = base_url { if let Some(b) = base_url {
site.set_base_url(b.to_string()); site.set_base_url(b.to_string());
} }
if include_drafts {
site.include_drafts();
}
site.load()?; site.load()?;
console::notify_site_size(&site); console::notify_site_size(&site);
console::warn_about_ignored_pages(&site); console::warn_about_ignored_pages(&site);

View file

@ -6,19 +6,24 @@ use site::Site;
use console; use console;
pub fn check(config_file: &str, base_path: Option<&str>, base_url: Option<&str>) -> Result<()> { pub fn check(
let bp = base_path.map(PathBuf::from).unwrap_or(env::current_dir().unwrap()); config_file: &str,
base_path: Option<&str>,
base_url: Option<&str>,
include_drafts: bool,
) -> Result<()> {
let bp = base_path.map(PathBuf::from).unwrap_or_else(|| env::current_dir().unwrap());
let mut site = Site::new(bp, config_file)?; let mut site = Site::new(bp, config_file)?;
// Force the checking of external links // Force the checking of external links
site.config.check_external_links = true; site.config.enable_check_mode();
// 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 { if let Some(b) = base_url {
site.set_base_url(b.to_string()); site.set_base_url(b.to_string());
} }
if include_drafts {
site.include_drafts();
}
site.load()?; site.load()?;
console::notify_site_size(&site); console::check_site_summary(&site);
console::warn_about_ignored_pages(&site); console::warn_about_ignored_pages(&site);
Ok(()) Ok(())
} }

View file

@ -25,15 +25,54 @@ build_search_index = %SEARCH%
# Put all your custom variables here # Put all your custom variables here
"#; "#;
// Given a path, return true if it is a directory and it doesn't have any
// non-hidden files, otherwise return false (path is assumed to exist)
pub fn is_directory_quasi_empty(path: &Path) -> Result<bool> {
if path.is_dir() {
let mut entries = match path.read_dir() {
Ok(entries) => entries,
Err(e) => {
bail!(
"Could not read `{}` because of error: {}",
path.to_string_lossy().to_string(),
e
);
}
};
// If any entry raises an error or isn't hidden (i.e. starts with `.`), we raise an error
if entries.any(|x| match x {
Ok(file) => !file
.file_name()
.to_str()
.expect("Could not convert filename to &str")
.starts_with('.'),
Err(_) => true,
}) {
return Ok(false);
}
return Ok(true);
}
Ok(false)
}
pub fn create_new_project(name: &str) -> Result<()> { pub fn create_new_project(name: &str) -> Result<()> {
let path = Path::new(name); let path = Path::new(name);
// Better error message than the rust default // Better error message than the rust default
if path.exists() && path.is_dir() { if path.exists() && !is_directory_quasi_empty(&path)? {
bail!("Folder `{}` already exists", path.to_string_lossy().to_string()); if name == "." {
bail!("The current directory is not an empty folder (hidden files are ignored).");
} else {
bail!(
"`{}` is not an empty folder (hidden files are ignored).",
path.to_string_lossy().to_string()
)
}
} }
create_dir(path)?;
console::info("Welcome to Zola!"); console::info("Welcome to Zola!");
console::info("Please answer a few questions to get started quickly.");
console::info("Any choices made can be changed by modifying the `config.toml` file later.");
let base_url = ask_url("> What is the URL of your site?", "https://example.com")?; let base_url = ask_url("> What is the URL of your site?", "https://example.com")?;
let compile_sass = ask_bool("> Do you want to enable Sass compilation?", true)?; let compile_sass = ask_bool("> Do you want to enable Sass compilation?", true)?;
@ -47,6 +86,7 @@ pub fn create_new_project(name: &str) -> Result<()> {
.replace("%SEARCH%", &format!("{}", search)) .replace("%SEARCH%", &format!("{}", search))
.replace("%HIGHLIGHT%", &format!("{}", highlight)); .replace("%HIGHLIGHT%", &format!("{}", highlight));
create_dir(path)?;
create_file(&path.join("config.toml"), &config)?; create_file(&path.join("config.toml"), &config)?;
create_dir(path.join("content"))?; create_dir(path.join("content"))?;
@ -66,3 +106,60 @@ pub fn create_new_project(name: &str) -> Result<()> {
println!("Visit https://www.getzola.org for the full documentation."); println!("Visit https://www.getzola.org for the full documentation.");
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
use std::env::temp_dir;
use std::fs::{create_dir, remove_dir, remove_dir_all};
#[test]
fn init_empty_directory() {
let mut dir = temp_dir();
dir.push("test_empty_dir");
if dir.exists() {
remove_dir_all(&dir).expect("Could not free test directory");
}
create_dir(&dir).expect("Could not create test directory");
let allowed = is_directory_quasi_empty(&dir)
.expect("An error happened reading the directory's contents");
remove_dir(&dir).unwrap();
assert_eq!(true, allowed);
}
#[test]
fn init_non_empty_directory() {
let mut dir = temp_dir();
dir.push("test_non_empty_dir");
if dir.exists() {
remove_dir_all(&dir).expect("Could not free test directory");
}
create_dir(&dir).expect("Could not create test directory");
let mut content = dir.clone();
content.push("content");
create_dir(&content).unwrap();
let allowed = is_directory_quasi_empty(&dir)
.expect("An error happened reading the directory's contents");
remove_dir(&content).unwrap();
remove_dir(&dir).unwrap();
assert_eq!(false, allowed);
}
#[test]
fn init_quasi_empty_directory() {
let mut dir = temp_dir();
dir.push("test_quasi_empty_dir");
if dir.exists() {
remove_dir_all(&dir).expect("Could not free test directory");
}
create_dir(&dir).expect("Could not create test directory");
let mut git = dir.clone();
git.push(".git");
create_dir(&git).unwrap();
let allowed = is_directory_quasi_empty(&dir)
.expect("An error happened reading the directory's contents");
remove_dir(&git).unwrap();
remove_dir(&dir).unwrap();
assert_eq!(true, allowed);
}
}

File diff suppressed because one or more lines are too long

View file

@ -21,6 +21,8 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
extern crate globset;
use std::env; use std::env;
use std::fs::{read_dir, remove_dir_all, File}; use std::fs::{read_dir, remove_dir_all, File};
use std::io::Read; use std::io::Read;
@ -37,26 +39,26 @@ use ctrlc;
use notify::{watcher, RecursiveMode, Watcher}; use notify::{watcher, RecursiveMode, Watcher};
use ws::{Message, Sender, WebSocket}; use ws::{Message, Sender, WebSocket};
use cmd::serve::globset::GlobSet;
use errors::{Error as ZolaError, Result}; use errors::{Error as ZolaError, Result};
use site::Site; use site::Site;
use utils::fs::copy_file; use utils::fs::copy_file;
use console; use console;
use open;
use rebuild; use rebuild;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum ChangeKind { enum ChangeKind {
Content, Content,
Templates, Templates,
Themes,
StaticFiles, StaticFiles,
Sass, Sass,
Config, Config,
} }
// Uglified using uglifyjs // This is dist/livereload.min.js from the LiveReload.js v3.0.0 release
// Also, commenting out the lines 330-340 (containing `e instanceof ProtocolError`) was needed
// as it seems their build didn't work well and didn't include ProtocolError so it would error on
// errors
const LIVE_RELOAD: &str = include_str!("livereload.js"); const LIVE_RELOAD: &str = include_str!("livereload.js");
struct ErrorFilePaths { struct ErrorFilePaths {
@ -64,7 +66,7 @@ struct ErrorFilePaths {
} }
fn not_found<B>( fn not_found<B>(
res: dev::ServiceResponse<B> res: dev::ServiceResponse<B>,
) -> std::result::Result<ErrorHandlerResponse<B>, actix_web::Error> { ) -> std::result::Result<ErrorHandlerResponse<B>, actix_web::Error> {
let buf: Vec<u8> = { let buf: Vec<u8> = {
let error_files: &ErrorFilePaths = res.request().app_data().unwrap(); let error_files: &ErrorFilePaths = res.request().app_data().unwrap();
@ -76,15 +78,10 @@ fn not_found<B>(
}; };
let new_resp = HttpResponse::build(http::StatusCode::NOT_FOUND) let new_resp = HttpResponse::build(http::StatusCode::NOT_FOUND)
.header( .header(http::header::CONTENT_TYPE, http::header::HeaderValue::from_static("text/html"))
http::header::CONTENT_TYPE,
http::header::HeaderValue::from_static("text/html"),
)
.body(buf); .body(buf);
Ok(ErrorHandlerResponse::Response( Ok(ErrorHandlerResponse::Response(res.into_response(new_resp.into_body())))
res.into_response(new_resp.into_body()),
))
} }
fn livereload_handler() -> HttpResponse { fn livereload_handler() -> HttpResponse {
@ -121,6 +118,7 @@ fn create_new_site(
output_dir: &str, output_dir: &str,
base_url: &str, base_url: &str,
config_file: &str, config_file: &str,
include_drafts: bool,
) -> Result<(Site, String)> { ) -> Result<(Site, String)> {
let mut site = Site::new(env::current_dir().unwrap(), config_file)?; let mut site = Site::new(env::current_dir().unwrap(), config_file)?;
@ -132,8 +130,12 @@ fn create_new_site(
format!("http://{}", base_address) format!("http://{}", base_address)
}; };
site.config.enable_serve_mode();
site.set_base_url(base_url); site.set_base_url(base_url);
site.set_output_path(output_dir); site.set_output_path(output_dir);
if include_drafts {
site.include_drafts();
}
site.load()?; site.load()?;
site.enable_live_reload(port); site.enable_live_reload(port);
console::notify_site_size(&site); console::notify_site_size(&site);
@ -149,14 +151,18 @@ pub fn serve(
base_url: &str, base_url: &str,
config_file: &str, config_file: &str,
watch_only: bool, watch_only: bool,
open: bool,
include_drafts: bool,
) -> Result<()> { ) -> Result<()> {
let start = Instant::now(); let start = Instant::now();
let (mut site, address) = create_new_site(interface, port, output_dir, base_url, config_file)?; let (mut site, address) =
create_new_site(interface, port, output_dir, base_url, config_file, include_drafts)?;
console::report_elapsed_time(start); console::report_elapsed_time(start);
// Setup watchers // Setup watchers
let mut watching_static = false; let mut watching_static = false;
let mut watching_templates = false; let mut watching_templates = false;
let mut watching_themes = false;
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap();
watcher watcher
@ -180,6 +186,13 @@ pub fn serve(
.map_err(|e| ZolaError::chain("Can't watch the `templates` folder.", e))?; .map_err(|e| ZolaError::chain("Can't watch the `templates` folder.", e))?;
} }
if Path::new("themes").exists() {
watching_themes = true;
watcher
.watch("themes/", RecursiveMode::Recursive)
.map_err(|e| ZolaError::chain("Can't watch the `themes` folder.", e))?;
}
// Sass support is optional so don't make it an error to no have a sass folder // Sass support is optional so don't make it an error to no have a sass folder
let _ = watcher.watch("sass/", RecursiveMode::Recursive); let _ = watcher.watch("sass/", RecursiveMode::Recursive);
@ -192,28 +205,25 @@ pub fn serve(
let broadcaster = if !watch_only { let broadcaster = if !watch_only {
thread::spawn(move || { thread::spawn(move || {
let s = HttpServer::new(move || { let s = HttpServer::new(move || {
let error_handlers = ErrorHandlers::new() let error_handlers =
.handler(http::StatusCode::NOT_FOUND, not_found); ErrorHandlers::new().handler(http::StatusCode::NOT_FOUND, not_found);
App::new() App::new()
.data(ErrorFilePaths { .data(ErrorFilePaths { not_found: static_root.join("404.html") })
not_found: static_root.join("404.html"),
})
.wrap(error_handlers) .wrap(error_handlers)
.route( .route("/livereload.js", web::get().to(livereload_handler))
"/livereload.js",
web::get().to(livereload_handler)
)
// Start a webserver that serves the `output_dir` directory // Start a webserver that serves the `output_dir` directory
.service( .service(fs::Files::new("/", &static_root).index_file("index.html"))
fs::Files::new("/", &static_root)
.index_file("index.html"),
)
}) })
.bind(&address) .bind(&address)
.expect("Can't start the webserver") .expect("Can't start the webserver")
.shutdown_timeout(20); .shutdown_timeout(20);
println!("Web server is available at http://{}\n", &address); println!("Web server is available at http://{}\n", &address);
if open {
if let Err(err) = open::that(format!("http://{}", &address)) {
eprintln!("Failed to open URL in your browser: {}", err);
}
}
s.run() s.run()
}); });
// The websocket for livereload // The websocket for livereload
@ -253,6 +263,9 @@ pub fn serve(
if watching_templates { if watching_templates {
watchers.push("templates"); watchers.push("templates");
} }
if watching_themes {
watchers.push("themes");
}
if site.config.compile_sass { if site.config.compile_sass {
watchers.push("sass"); watchers.push("sass");
} }
@ -267,7 +280,7 @@ pub fn serve(
println!("Press Ctrl+C to stop\n"); println!("Press Ctrl+C to stop\n");
// Delete the output folder on ctrl+C // Delete the output folder on ctrl+C
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
remove_dir_all(&output_path).expect("Failed to delete output directory"); let _ = remove_dir_all(&output_path);
::std::process::exit(0); ::std::process::exit(0);
}) })
.expect("Error setting Ctrl-C handler"); .expect("Error setting Ctrl-C handler");
@ -321,7 +334,12 @@ pub fn serve(
} else { } else {
rebuild_done_handling( rebuild_done_handling(
&broadcaster, &broadcaster,
copy_file(&path, &site.output_path, &site.static_path), copy_file(
&path,
&site.output_path,
&site.static_path,
site.config.hard_link_static,
),
&partial_path.to_string_lossy(), &partial_path.to_string_lossy(),
); );
} }
@ -348,6 +366,7 @@ pub fn serve(
); );
let start = Instant::now(); let start = Instant::now();
match change_kind { match change_kind {
ChangeKind::Content => { ChangeKind::Content => {
console::info(&format!("-> Content renamed {}", path.display())); console::info(&format!("-> Content renamed {}", path.display()));
@ -361,6 +380,22 @@ pub fn serve(
ChangeKind::Templates => reload_templates(&mut site, &path), ChangeKind::Templates => reload_templates(&mut site, &path),
ChangeKind::StaticFiles => copy_static(&site, &path, &partial_path), ChangeKind::StaticFiles => copy_static(&site, &path, &partial_path),
ChangeKind::Sass => reload_sass(&site, &path, &partial_path), ChangeKind::Sass => reload_sass(&site, &path, &partial_path),
ChangeKind::Themes => {
console::info(
"-> Themes changed. The whole site will be reloaded.",
);
site = create_new_site(
interface,
port,
output_dir,
base_url,
config_file,
include_drafts,
)
.unwrap()
.0;
rebuild_done_handling(&broadcaster, Ok(()), "/x.js");
}
ChangeKind::Config => { ChangeKind::Config => {
console::info("-> Config changed. The whole site will be reloaded. The browser needs to be refreshed to make the changes visible."); console::info("-> Config changed. The whole site will be reloaded. The browser needs to be refreshed to make the changes visible.");
site = create_new_site( site = create_new_site(
@ -369,6 +404,7 @@ pub fn serve(
output_dir, output_dir,
base_url, base_url,
config_file, config_file,
include_drafts,
) )
.unwrap() .unwrap()
.0; .0;
@ -379,6 +415,9 @@ pub fn serve(
// Intellij does weird things on edit, chmod is there to count those changes // Intellij does weird things on edit, chmod is there to count those changes
// https://github.com/passcod/notify/issues/150#issuecomment-494912080 // https://github.com/passcod/notify/issues/150#issuecomment-494912080
Create(path) | Write(path) | Remove(path) | Chmod(path) => { Create(path) | Write(path) | Remove(path) | Chmod(path) => {
if is_ignored_file(&site.config.ignored_content_globset, &path) {
continue;
}
if is_temp_file(&path) || path.is_dir() { if is_temp_file(&path) || path.is_dir() {
continue; continue;
} }
@ -402,6 +441,22 @@ pub fn serve(
(ChangeKind::Templates, _) => reload_templates(&mut site, &path), (ChangeKind::Templates, _) => reload_templates(&mut site, &path),
(ChangeKind::StaticFiles, p) => copy_static(&site, &path, &p), (ChangeKind::StaticFiles, p) => copy_static(&site, &path, &p),
(ChangeKind::Sass, p) => reload_sass(&site, &path, &p), (ChangeKind::Sass, p) => reload_sass(&site, &path, &p),
(ChangeKind::Themes, _) => {
console::info(
"-> Themes changed. The whole site will be reloaded.",
);
site = create_new_site(
interface,
port,
output_dir,
base_url,
config_file,
include_drafts,
)
.unwrap()
.0;
rebuild_done_handling(&broadcaster, Ok(()), "/x.js");
}
(ChangeKind::Config, _) => { (ChangeKind::Config, _) => {
console::info("-> Config changed. The whole site will be reloaded. The browser needs to be refreshed to make the changes visible."); console::info("-> Config changed. The whole site will be reloaded. The browser needs to be refreshed to make the changes visible.");
site = create_new_site( site = create_new_site(
@ -410,6 +465,7 @@ pub fn serve(
output_dir, output_dir,
base_url, base_url,
config_file, config_file,
include_drafts,
) )
.unwrap() .unwrap()
.0; .0;
@ -425,6 +481,13 @@ pub fn serve(
} }
} }
fn is_ignored_file(ignored_content_globset: &Option<GlobSet>, path: &Path) -> bool {
match ignored_content_globset {
Some(gs) => gs.is_match(path),
None => false,
}
}
/// Returns whether the path we received corresponds to a temp file created /// Returns whether the path we received corresponds to a temp file created
/// by an editor or the OS /// by an editor or the OS
fn is_temp_file(path: &Path) -> bool { fn is_temp_file(path: &Path) -> bool {
@ -460,6 +523,8 @@ fn detect_change_kind(pwd: &Path, path: &Path) -> (ChangeKind, PathBuf) {
let change_kind = if partial_path.starts_with("/templates") { let change_kind = if partial_path.starts_with("/templates") {
ChangeKind::Templates ChangeKind::Templates
} else if partial_path.starts_with("/themes") {
ChangeKind::Themes
} else if partial_path.starts_with("/content") { } else if partial_path.starts_with("/content") {
ChangeKind::Content ChangeKind::Content
} else if partial_path.starts_with("/static") { } else if partial_path.starts_with("/static") {
@ -516,6 +581,11 @@ mod tests {
Path::new("/home/vincent/site"), Path::new("/home/vincent/site"),
Path::new("/home/vincent/site/templates/hello.html"), Path::new("/home/vincent/site/templates/hello.html"),
), ),
(
(ChangeKind::Themes, PathBuf::from("/themes/hello.html")),
Path::new("/home/vincent/site"),
Path::new("/home/vincent/site/themes/hello.html"),
),
( (
(ChangeKind::StaticFiles, PathBuf::from("/static/site.css")), (ChangeKind::StaticFiles, PathBuf::from("/static/site.css")),
Path::new("/home/vincent/site"), Path::new("/home/vincent/site"),

View file

@ -46,7 +46,7 @@ fn colorize(message: &str, color: &ColorSpec) {
stdout.set_color(&ColorSpec::new()).unwrap(); stdout.set_color(&ColorSpec::new()).unwrap();
} }
/// Display in the console the number of pages/sections in the site /// Display in the console the number of pages/sections in the site, and number of images to process
pub fn notify_site_size(site: &Site) { pub fn notify_site_size(site: &Site) {
let library = site.library.read().unwrap(); let library = site.library.read().unwrap();
println!( println!(
@ -58,6 +58,22 @@ pub fn notify_site_size(site: &Site) {
); );
} }
/// Display in the console only the number of pages/sections in the site
pub fn check_site_summary(site: &Site) {
let library = site.library.read().unwrap();
let orphans = library.get_all_orphan_pages();
println!(
"-> Site content: {} pages ({} orphan), {} sections",
library.pages().len(),
orphans.len(),
library.sections().len() - 1, // -1 since we do not count the index as a section there
);
for orphan in orphans {
warn(&format!("Orphan page found: {}", orphan.path));
}
}
/// Display a warning in the console if there are ignored pages in the site /// Display a warning in the console if there are ignored pages in the site
pub fn warn_about_ignored_pages(site: &Site) { pub fn warn_about_ignored_pages(site: &Site) {
let library = site.library.read().unwrap(); let library = site.library.read().unwrap();

View file

@ -16,6 +16,7 @@ extern crate site;
#[macro_use] #[macro_use]
extern crate errors; extern crate errors;
extern crate front_matter; extern crate front_matter;
extern crate open;
extern crate rebuild; extern crate rebuild;
extern crate utils; extern crate utils;
@ -47,7 +48,12 @@ fn main() {
console::info("Building site..."); console::info("Building site...");
let start = Instant::now(); let start = Instant::now();
let output_dir = matches.value_of("output_dir").unwrap(); let output_dir = matches.value_of("output_dir").unwrap();
match cmd::build(config_file, matches.value_of("base_url"), output_dir) { match cmd::build(
config_file,
matches.value_of("base_url"),
output_dir,
matches.is_present("drafts"),
) {
Ok(()) => console::report_elapsed_time(start), Ok(()) => console::report_elapsed_time(start),
Err(e) => { Err(e) => {
console::unravel_errors("Failed to build the site", &e); console::unravel_errors("Failed to build the site", &e);
@ -65,6 +71,8 @@ fn main() {
} }
}; };
let watch_only = matches.is_present("watch_only"); let watch_only = matches.is_present("watch_only");
let open = matches.is_present("open");
let include_drafts = matches.is_present("drafts");
// Default one // Default one
if port != 1111 && !watch_only && !port_is_available(port) { if port != 1111 && !watch_only && !port_is_available(port) {
@ -72,7 +80,7 @@ fn main() {
::std::process::exit(1); ::std::process::exit(1);
} }
if !watch_only && !port_is_available(port) { if !watch_only && !port_is_available(port) {
port = if let Some(p) = get_available_port(1111) { port = if let Some(p) = get_available_port(1111) {
p p
} else { } else {
@ -83,7 +91,16 @@ fn main() {
let output_dir = matches.value_of("output_dir").unwrap(); let output_dir = matches.value_of("output_dir").unwrap();
let base_url = matches.value_of("base_url").unwrap(); let base_url = matches.value_of("base_url").unwrap();
console::info("Building site..."); console::info("Building site...");
match cmd::serve(interface, port, output_dir, base_url, config_file, watch_only) { match cmd::serve(
interface,
port,
output_dir,
base_url,
config_file,
watch_only,
open,
include_drafts,
) {
Ok(()) => (), Ok(()) => (),
Err(e) => { Err(e) => {
console::unravel_errors("", &e); console::unravel_errors("", &e);
@ -98,6 +115,7 @@ fn main() {
config_file, config_file,
matches.value_of("base_path"), matches.value_of("base_path"),
matches.value_of("base_url"), matches.value_of("base_url"),
matches.is_present("drafts"),
) { ) {
Ok(()) => console::report_elapsed_time(start), Ok(()) => console::report_elapsed_time(start),
Err(e) => { Err(e) => {

View file

@ -0,0 +1,253 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!-- Generated by: TmTheme-Editor -->
<!-- ============================================ -->
<!-- app: http://tmtheme-editor.herokuapp.com -->
<!-- code: https://github.com/aziz/tmTheme-Editor -->
<plist version="1.0">
<dict>
<key>comment</key>
<string>http:&#x2f;&#x2f;chriskempson.com</string>
<key>name</key>
<string>Tomorrow</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#FFFFFF</string>
<key>caret</key>
<string>#AEAFAD</string>
<key>foreground</key>
<string>#4D4D4C</string>
<key>invisibles</key>
<string>#D1D1D1</string>
<key>lineHighlight</key>
<string>#EFEFEF</string>
<key>selection</key>
<string>#D6D6D6</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comment</string>
<key>scope</key>
<string>comment, string.quoted.double.block.python</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#999999</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Foreground</string>
<key>scope</key>
<string>keyword.operator.class, constant.other, source.php.embedded.line</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#666969</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variable, String Link, Regular Expression, Tag Name</string>
<key>scope</key>
<string>variable, support.other.variable, string.other.link, string.regexp, entity.name.tag, entity.other.attribute-name, meta.tag, declaration.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#C82829</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Number, Constant, Function Argument, Tag Attribute, Embedded</string>
<key>scope</key>
<string>constant.numeric, constant.language, support.constant, constant.character, variable.parameter, punctuation.section.embedded, keyword.other.unit</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#F5871F</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Class, Support</string>
<key>scope</key>
<string>entity.name.class, entity.name.type.class, support.type, support.class</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#C99E00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String, Symbols, Inherited Class, Markup Heading</string>
<key>scope</key>
<string>string, constant.other.symbol, entity.other.inherited-class, markup.heading</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#718C00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Operator, Misc</string>
<key>scope</key>
<string>keyword.operator, constant.other.color</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#3E999F</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function, Special Method, Block Level</string>
<key>scope</key>
<string>entity.name.function, meta.function-call, support.function, keyword.other.special-method, meta.block-level</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#4271AE</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword, Storage</string>
<key>scope</key>
<string>keyword, storage, storage.type</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#8959A8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#C82829</string>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#FFFFFF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Separator</string>
<key>scope</key>
<string>meta.separator</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#4271AE</string>
<key>foreground</key>
<string>#FFFFFF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Deprecated</string>
<key>scope</key>
<string>invalid.deprecated</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#8959A8</string>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#FFFFFF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Diff foreground</string>
<key>scope</key>
<string>markup.inserted.diff, markup.deleted.diff, meta.diff.header.to-file, meta.diff.header.from-file</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#FFFFFF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Diff insertion</string>
<key>scope</key>
<string>markup.inserted.diff, meta.diff.header.to-file</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#718c00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Diff deletion</string>
<key>scope</key>
<string>markup.deleted.diff, meta.diff.header.from-file</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#c82829</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Diff header</string>
<key>scope</key>
<string>meta.diff.header.from-file, meta.diff.header.to-file</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#FFFFFF</string>
<key>background</key>
<string>#4271ae</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Diff range</string>
<key>scope</key>
<string>meta.diff.range</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#3e999f</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>82CCD69C-F1B1-4529-B39E-780F91F07604</string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>semanticClass</key>
<string>theme.light.tomorrow</string>
</dict>
</plist>

Binary file not shown.

View file

@ -0,0 +1,876 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>author</key>
<string>IceTimux / Andres Michel</string>
<key>name</key>
<string>One Dark</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#6c7079</string>
<key>background</key>
<string>#2B303B</string>
<key>invisibles</key>
<string>#747369</string>
<key>caret</key>
<string>#528bff</string>
<key>lineHighlight</key>
<string>#8cc2fc0b</string>
<key>bracketContentsOptions</key>
<string>underline</string>
<key>bracketContentsForeground</key>
<string>#528bff</string>
<key>bracketsOptions</key>
<string>underline</string>
<key>bracketsForeground</key>
<string>#528bff</string>
<key>tagsOptions</key>
<string>underline</string>
<key>tagsForeground</key>
<string>#528bff</string>
<key>findHighlight</key>
<string>#314365</string>
<key>findHighlightForeground</key>
<string>#528bff</string>
<key>gutter</key>
<string>#2B303B</string>
<key>gutterForeground</key>
<string>#636d8388</string>
<key>selection</key>
<string>#bbccf51b</string>
<key>selectionBorder</key>
<string>#bbccf51b</string>
<key>inactiveSelection</key>
<string>#bbccf51b</string>
<key>guide</key>
<string>#464c55</string>
<key>activeGuide</key>
<string>#464c55</string>
<key>stackGuide</key>
<string>#464c55</string>
<key>highlight</key>
<string>#528bff80</string>
<key>highlightForeground</key>
<string>#528bff</string>
<key>shadow</key>
<string>#2B303B</string>
<key>shadowWidth</key>
<string>1</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Text and source</string>
<key>scope</key>
<string>text, source</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#abb2bf</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Text</string>
<key>scope</key>
<string>variable.parameter.function</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comments</string>
<key>scope</key>
<string>comment, punctuation.definition.comment</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#5f697a</string>
<key>fontStyle</key>
<string> italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Delimiters</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Operators</string>
<key>scope</key>
<string>keyword.operator</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keywords</string>
<key>scope</key>
<string>keyword</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#cd74e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variables</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#eb6772</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Functions</string>
<key>scope</key>
<string>entity.name.function, meta.require, support.function.any-method</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#5cb3fa</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Classes</string>
<key>scope</key>
<string>support.class, entity.name.class, entity.name.type.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#f0c678</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Classes</string>
<key>scope</key>
<string>meta.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Methods</string>
<key>scope</key>
<string>keyword.other.special-method</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#5cb3fa</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#cd74e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support</string>
<key>scope</key>
<string>support.function</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#5ebfcc</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Strings, Inherited Class</string>
<key>scope</key>
<string>string, constant.other.symbol, entity.other.inherited-class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#9acc76</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Integers</string>
<key>scope</key>
<string>constant.numeric</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Floats</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Boolean</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constants</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Tags</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#eb6772</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Attributes</string>
<key>scope</key>
<string>entity.other.attribute-name</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Attribute IDs</string>
<key>scope</key>
<string>entity.other.attribute-name.id, punctuation.definition.entity</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Selector</string>
<key>scope</key>
<string>meta.selector</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#cd74e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Values</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Headings</string>
<key>scope</key>
<string>markup.heading punctuation.definition.heading, entity.name.section</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#5cb3fa</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Units</string>
<key>scope</key>
<string>keyword.other.unit</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Bold</string>
<key>scope</key>
<string>markup.bold, punctuation.definition.bold</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#f0c678</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Italic</string>
<key>scope</key>
<string>markup.italic, punctuation.definition.italic</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#cd74e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Code</string>
<key>scope</key>
<string>markup.raw.inline</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#9acc76</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Link Text</string>
<key>scope</key>
<string>string.other.link, punctuation.definition.string.end.markdown</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#eb6772</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Link Url</string>
<key>scope</key>
<string>meta.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Lists</string>
<key>scope</key>
<string>markup.list</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#eb6772</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Quotes</string>
<key>scope</key>
<string>markup.quote</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Separator</string>
<key>scope</key>
<string>meta.separator</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#515151</string>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Inserted</string>
<key>scope</key>
<string>markup.inserted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#9acc76</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Deleted</string>
<key>scope</key>
<string>markup.deleted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#eb6772</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Changed</string>
<key>scope</key>
<string>markup.changed</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#cd74e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Colors</string>
<key>scope</key>
<string>constant.other.color</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#5ebfcc</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Regular Expressions</string>
<key>scope</key>
<string>string.regexp</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#5ebfcc</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Escape Characters</string>
<key>scope</key>
<string>constant.character.escape</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#5ebfcc</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Embedded</string>
<key>scope</key>
<string>punctuation.section.embedded, variable.interpolation</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c94e42</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Illegal</string>
<key>scope</key>
<string>invalid.illegal</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#ffffff</string>
<key>background</key>
<string>#e05252</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Broken</string>
<key>scope</key>
<string>invalid.broken</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#f99157</string>
<key>foreground</key>
<string>#2d2d2d</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Deprecated</string>
<key>scope</key>
<string>invalid.deprecated</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#d27b53</string>
<key>foreground</key>
<string>#2c323d</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Unimplemented</string>
<key>scope</key>
<string>invalid.unimplemented</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#747369</string>
<key>foreground</key>
<string>#2c323d</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Json key</string>
<key>scope</key>
<string>source.json meta.structure.dictionary.json string.quoted.double.json</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#eb6772</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Json value</string>
<key>scope</key>
<string>source.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#9acc76</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>json sub key</string>
<key>scope</key>
<string>source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json string.quoted.double.json</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#eb6772</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Json sub value</string>
<key>scope</key>
<string>source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#9acc76</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>laravel blade tag</string>
<key>scope</key>
<string>text.html.laravel-blade source.php.embedded.line.html entity.name.tag.laravel-blade</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#cd74e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>laravel blade @</string>
<key>scope</key>
<string>text.html.laravel-blade source.php.embedded.line.html support.constant.laravel-blade</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#cd74e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>python function parameter</string>
<key>scope</key>
<string>source.python meta.function.python meta.function.parameters.python variable.parameter.function.python</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>python meta function</string>
<key>scope</key>
<string>source.python meta.function-call.python support.type.python</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#5ebfcc</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>python logical keyword</string>
<key>scope</key>
<string>source.python keyword.operator.logical.python</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#cd74e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>python class ( begin</string>
<key>scope</key>
<string>source.python meta.class.python punctuation.definition.inheritance.begin.python</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#f0c678</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>python class ) end</string>
<key>scope</key>
<string>source.python meta.class.python punctuation.definition.inheritance.end.python</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#f0c678</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>python function call parameter name</string>
<key>scope</key>
<string>source.python meta.function-call.python meta.function-call.arguments.python variable.parameter.function.python</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>php fcuntion constants</string>
<key>scope</key>
<string>text.html.basic source.php.embedded.block.html support.constant.std.php</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>php namespace name</string>
<key>scope</key>
<string>text.html.basic source.php.embedded.block.html meta.namespace.php entity.name.type.namespace.php</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#f0c678</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>javascript meta constant</string>
<key>scope</key>
<string>source.js meta.function.js support.constant.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>php namespace in top</string>
<key>scope</key>
<string>text.html.basic` source.php.embedded.block.html constant.other.php</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#cd74e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>php namespace name in top</string>
<key>scope</key>
<string>text.html.basic source.php.embedded.block.html support.other.namespace.php</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#db9d63</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>latex label names</string>
<key>scope</key>
<string>text.tex.latex meta.function.environment.math.latex string.other.math.block.environment.latex meta.definition.label.latex variable.parameter.definition.label.latex</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>latex italic emph</string>
<key>scope</key>
<string>text.tex.latex meta.function.emph.latex markup.italic.emph.latex</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string> italic</string>
<key>foreground</key>
<string>#cd74e8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>subl_new js vars</string>
<key>scope</key>
<string>source.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>new_subl js $</string>
<key>scope</key>
<string>source.js meta.function-call.with-arguments.js variable.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>new_subl js call method</string>
<key>scope</key>
<string>source.js meta.group.braces.round meta.group.braces.curly meta.function-call.method.without-arguments.js variable.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>new_subl e js</string>
<key>scope</key>
<string>source.js meta.group.braces.round meta.group.braces.curly variable.other.object.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>new_subl js key</string>
<key>scope</key>
<string>source.js meta.group.braces.round meta.group.braces.curly constant.other.object.key.js string.unquoted.label.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>new_subl obejct key</string>
<key>scope</key>
<string>source.js meta.group.braces.round meta.group.braces.curly constant.other.object.key.js punctuation.separator.key-value.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>new_subl js method with args</string>
<key>scope</key>
<string>source.js meta.group.braces.round meta.group.braces.curly meta.function-call.method.with-arguments.js variable.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>new_subl js variable function</string>
<key>scope</key>
<string>source.js meta.function-call.method.with-arguments.js variable.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>new_subl variabel function method</string>
<key>scope</key>
<string>source.js meta.function-call.method.without-arguments.js variable.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#adb7c9</string>
</dict>
</dict>
</array>
</dict>
</plist>

View file

@ -11,5 +11,7 @@ taxonomies = [
extra_syntaxes = ["syntaxes"] extra_syntaxes = ["syntaxes"]
ignored_content = ["*/ignored.md"]
[extra.author] [extra.author]
name = "Vincent Prouillet" name = "Vincent Prouillet"

Some files were not shown because too many files have changed in this diff Show more