@@ -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 |
@@ -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 | ||||
@@ -1,7 +1,7 @@ | |||||
[package] | [package] | ||||
name = "zola" | name = "zola" | ||||
version = "0.8.0" | |||||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||||
version = "0.9.0" | |||||
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" } | ||||
@@ -56,4 +57,4 @@ members = [ | |||||
[profile.release] | [profile.release] | ||||
lto = true | lto = true | ||||
codegen-units = 1 | |||||
codegen-units = 1 |
@@ -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 | ||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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]' \ | ||||
@@ -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') | ||||
@@ -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 | ||||
@@ -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' | ||||
@@ -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" } |
@@ -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 | |||||
pub check_external_links: bool, | |||||
/// The mode Zola is currently being ran on. Some logging/feature can differ depending on the | |||||
/// 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] | |||||
fn can_use_language_configuration() { | |||||
let config = r#" | |||||
const CONFIG_TRANSLATION: &str = 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); | |||||
assert!(config.is_ok()); | |||||
let translations = config.unwrap().translations; | |||||
assert_eq!(translations["fr"]["title"].as_str().unwrap(), "Un titre"); | |||||
assert_eq!(translations["en"]["title"].as_str().unwrap(), "A title"); | |||||
#[test] | |||||
fn can_use_present_translation() { | |||||
let config = Config::parse(CONFIG_TRANSLATION).unwrap(); | |||||
assert_eq!(config.get_translation("fr", "title").unwrap(), "Un titre"); | |||||
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] | ||||
@@ -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" | |||||
syntect = "3" | |||||
image = "0.22" | |||||
syntect = "=3.2.0" |
@@ -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 { | |||||
ErrorKind::Tera(ref err) => source = err.source(), | |||||
_ => (), | |||||
}; | |||||
if let ErrorKind::Tera(ref err) = self.kind { | |||||
source = err.source(); | |||||
} | |||||
} | } | ||||
source | source | ||||
@@ -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 | ||||
@@ -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)] | ||||
@@ -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" } | ||||
@@ -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 | |||||
/// height preserving aspect ratio. | |||||
/// If the image is larger than the specified width or height, scales the image such | |||||
/// 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) => { | ||||
@@ -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" |
@@ -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( | |||||
&file_path.strip_prefix(base_path).expect("Strip base path prefix for page"), | |||||
); | |||||
let mut components = | |||||
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( | |||||
&file_path.strip_prefix(base_path).expect("Strip base path prefix for section"), | |||||
); | |||||
let components = | |||||
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(), | ||||
@@ -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(), | ||||
@@ -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>, | |||||
/// Toc made from the headers of the markdown file | |||||
pub toc: Vec<Header>, | |||||
pub heavier: Option<DefaultKey>, | |||||
/// Toc made from the headings of the markdown file | |||||
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(&slug) | |||||
} else { | |||||
slugify(&page.file.name) | |||||
} | |||||
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" | ||||
@@ -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>, | |||||
/// Toc made from the headers of the markdown file | |||||
pub toc: Vec<Header>, | |||||
pub subsections: Vec<DefaultKey>, | |||||
/// Toc made from the headings of the markdown file | |||||
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/"); | |||||
} | |||||
} | } |
@@ -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 { | |||||
section | |||||
let mut ancestors = vec![]; | |||||
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() | |||||
} else { | |||||
vec![] | |||||
}; | |||||
let translations = if let Some(ref lib) = library { | |||||
TranslatedContent::find_all_sections(section, lib) | |||||
} else { | |||||
vec![] | |||||
}; | |||||
.collect(); | |||||
translations = TranslatedContent::find_all_sections(section, lib); | |||||
subsections = | |||||
section.subsections.iter().map(|k| lib.get_section_path_by_key(*k)).collect(); | |||||
} | |||||
SerializingSection { | SerializingSection { | ||||
relative_path: §ion.file.relative, | relative_path: §ion.file.relative, | ||||
@@ -287,7 +285,7 @@ impl<'a> SerializingSection<'a> { | |||||
assets: §ion.serialized_assets, | assets: §ion.serialized_assets, | ||||
lang: §ion.lang, | lang: §ion.lang, | ||||
pages: vec![], | pages: vec![], | ||||
subsections: vec![], | |||||
subsections, | |||||
translations, | translations, | ||||
} | } | ||||
} | } | ||||
@@ -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( | |||||
sorted | |||||
.iter() | |||||
.map(|k| { | |||||
if let Some(page) = self.pages.get(*k) { | |||||
(k, page.is_draft()) | |||||
} else { | |||||
unreachable!("Sorting got an unknown page") | |||||
} | |||||
}) | |||||
.collect(), | |||||
); | |||||
let with_siblings = find_siblings(&sorted); | |||||
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() | ||||
} | } | ||||
@@ -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/"); | ||||
} | } | ||||
@@ -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: Vec<(&Key, bool)>) -> Vec<(Key, Option<Key>, Option<Key>)> { | |||||
pub fn find_siblings(sorted: &[DefaultKey]) -> Vec<(DefaultKey, Option<DefaultKey>, Option<DefaultKey>)> { | |||||
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() { | |||||
if *is_draft { | |||||
res.push((**key, None, None)); | |||||
continue; | |||||
} | |||||
let mut with_siblings = (**key, None, None); | |||||
for (i, key) in sorted.iter().enumerate() { | |||||
let mut with_siblings = (*key, None, None); | |||||
if i > 0 { | if i > 0 { | ||||
let mut j = i; | |||||
loop { | |||||
if j == 0 { | |||||
break; | |||||
} | |||||
j -= 1; | |||||
if sorted[j].1 { | |||||
continue; | |||||
} | |||||
// lighter / later | |||||
with_siblings.1 = Some(*sorted[j].0); | |||||
break; | |||||
} | |||||
// lighter / later | |||||
with_siblings.1 = Some(sorted[i - 1]); | |||||
} | } | ||||
if i < length - 1 { | if i < length - 1 { | ||||
let mut j = i; | |||||
loop { | |||||
if j == length - 1 { | |||||
break; | |||||
} | |||||
j += 1; | |||||
if sorted[j].1 { | |||||
continue; | |||||
} | |||||
// heavier/earlier | |||||
with_siblings.2 = Some(*sorted[j].0); | |||||
break; | |||||
} | |||||
// heavier/earlier | |||||
with_siblings.2 = Some(sorted[i + 1]); | |||||
} | } | ||||
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 = | |||||
vec![(&key1, page1.is_draft()), (&key2, page2.is_draft()), (&key3, page3.is_draft())]; | |||||
let input = vec![key1, key2, key3]; | |||||
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)); | ||||
@@ -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) { | |||||
if taxonomies_def[name].lang != page.lang { | |||||
bail!( | |||||
"Page `{}` has taxonomy `{}` which is not available in that language", | |||||
page.file.path.display(), | |||||
name | |||||
); | |||||
} | |||||
all_taxonomies.entry(name).or_insert_with(HashMap::new); | |||||
let taxo_key = format!("{}-{}", name, page.lang); | |||||
if taxonomies_def.contains_key(&taxo_key) { | |||||
all_taxonomies.entry(taxo_key.clone()).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" | |||||
); | |||||
} | |||||
} | } |
@@ -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" } |
@@ -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); | |||||
} | |||||
} | } |
@@ -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() | ||||
} | } | ||||
@@ -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" | |||||
pulldown-cmark = "0.5" | |||||
syntect = "=3.2.0" | |||||
pulldown-cmark = "0.6" | |||||
slug = "0.1" | slug = "0.1" | ||||
serde = "1" | serde = "1" | ||||
serde_derive = "1" | serde_derive = "1" | ||||
@@ -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 | ||||
@@ -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 { | |||||
fn new(start: usize, level: i32) -> HeaderRef { | |||||
HeaderRef { start_idx: start, end_idx: 0, level, id: None } | |||||
impl HeadingRef { | |||||
fn new(start: usize, level: u32) -> HeadingRef { | |||||
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> { | |||||
let mut header_refs = vec![]; | |||||
fn get_heading_refs(events: &[Event]) -> Vec<HeadingRef> { | |||||
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)) => { | |||||
header_refs.push(HeaderRef::new(i, *level)); | |||||
Event::Start(Tag::Heading(level)) => { | |||||
heading_refs.push(HeadingRef::new(i, *level)); | |||||
} | } | ||||
Event::End(Tag::Header(_)) => { | |||||
let msg = "Header end before start?"; | |||||
header_refs.last_mut().expect(msg).end_idx = i; | |||||
Event::End(Tag::Heading(_)) => { | |||||
let msg = "Heading end before start?"; | |||||
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() { | |||||
let end_idx = header_ref.end_idx; | |||||
for heading_ref in heading_refs.iter_mut() { | |||||
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 | |||||
for header_ref in header_refs { | |||||
let start_idx = header_ref.start_idx; | |||||
let end_idx = header_ref.end_idx; | |||||
// Second heading pass: auto-generate remaining IDs, and emit HTML | |||||
for heading_ref in heading_refs { | |||||
let start_idx = heading_ref.start_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() }; | |||||
headers.push(h); | |||||
let h = Heading { level: heading_ref.level, id, permalink, title, children: Vec::new() }; | |||||
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, | ||||
}) | }) | ||||
@@ -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 { | |||||
pub fn new(level: i32) -> Header { | |||||
Header { | |||||
impl Heading { | |||||
pub fn new(level: u32) -> Heading { | |||||
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 | |||||
/// representing the hierarchy | |||||
pub fn make_table_of_contents(headers: Vec<Header>) -> Vec<Header> { | |||||
let mut toc = vec![]; | |||||
'parent: for header in headers { | |||||
if toc.is_empty() { | |||||
toc.push(header); | |||||
continue; | |||||
// 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 | |||||
} | } | ||||
// 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; | |||||
} | |||||
Some(parent) => { | |||||
if heading.level <= parent.level { | |||||
// Heading is same level or higher so we don't insert here | |||||
return false; | |||||
} | } | ||||
if header.level > h.level { | |||||
h.children.push(header); | |||||
continue 'parent; | |||||
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 | |||||
} | } | ||||
} | |||||
} | |||||
// Nop, just insert it | |||||
toc.push(header) | |||||
/// Converts the flat temp headings into a nested set of headings | |||||
/// representing the hierarchy | |||||
pub fn make_table_of_contents(headings: Vec<Heading>) -> Vec<Heading> { | |||||
let mut toc = vec![]; | |||||
for heading in headings { | |||||
// First heading or we try to insert the current heading in a previous one | |||||
if toc.is_empty() || !insert_into_parent(toc.iter_mut().last(), &heading) { | |||||
toc.push(heading); | |||||
} | |||||
} | } | ||||
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), | |||||
Header::new(2), | |||||
Header::new(2), | |||||
Header::new(3), | |||||
Header::new(2), | |||||
Header::new(1), | |||||
Header::new(2), | |||||
Header::new(3), | |||||
Header::new(3), | |||||
Heading::new(1), | |||||
Heading::new(2), | |||||
Heading::new(2), | |||||
Heading::new(3), | |||||
Heading::new(2), | |||||
Heading::new(1), | |||||
Heading::new(2), | |||||
Heading::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), | |||||
Header::new(2), | |||||
Header::new(2), | |||||
Header::new(3), | |||||
Header::new(2), | |||||
Header::new(1), | |||||
Header::new(4), | |||||
Heading::new(3), | |||||
Heading::new(2), | |||||
Heading::new(2), | |||||
Heading::new(3), | |||||
Heading::new(2), | |||||
Heading::new(1), | |||||
Heading::new(4), | |||||
]; | ]; | ||||
let toc = make_table_of_contents(input); | let toc = make_table_of_contents(input); | ||||
println!("{:#?}", toc); | println!("{:#?}", toc); | ||||
@@ -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=\"world\">World<a class=\"zola-anchor\" href=\"#world\" aria-label=\"Anchor link for: world\">đź”—</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></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].title, "header with code"); | |||||
assert_eq!(toc[0].id, "heading-with-code"); | |||||
assert_eq!(toc[0].title, "heading with code"); | |||||
assert_eq!(toc[1].id, "anchor-in-header"); | |||||
assert_eq!(toc[1].title, "anchor in header"); | |||||
assert_eq!(toc[1].id, "anchor-in-heading"); | |||||
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"); | |||||
} |
@@ -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" } | ||||
@@ -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 §ion.pages { | for key in §ion.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; | ||||
} | } | ||||
@@ -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 | |||||
.into_iter() | |||||
.filter(|p| p.meta.date.is_some() && !p.is_draft()) | |||||
.collect::<Vec<_>>(); | |||||
let mut pages = all_pages.into_iter().filter(|p| p.meta.date.is_some()).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() { | ||||
@@ -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()), | ||||
@@ -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) | |||||
assert_eq!(library.pages().len(), 22); | |||||
// Correct number of pages (sections do not count as pages, draft are ignored) | |||||
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")); | |||||
} |
@@ -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%3A//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/blog/something-else/")); | |||||
assert!(file_contains!(public, "sitemap.xml", "https://example.com/fr/blog/something-else/")); | |||||
assert!(file_contains!(public, "sitemap.xml", "https://example.com/it/blog/something-else/")); | |||||
// 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%3A//example.com/fr/blog/something-else/")); | |||||
assert!(file_contains!(public, "rss.xml", "https://example.com/blog/something-else/")); | |||||
assert!(!file_contains!(public, "rss.xml", "https://example.com/fr/blog/something-else/")); | |||||
assert!(file_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%3A//example.com/fr/blog/something-else/")); | |||||
assert!(!file_contains!(public, "fr/rss.xml", "https://example.com/blog/something-else/")); | |||||
assert!(file_contains!(public, "fr/rss.xml", "https://example.com/fr/blog/something-else/")); | |||||
// 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")); | |||||
} | } |
@@ -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" } | ||||
@@ -1 +1 @@ | |||||
<a class="zola-anchor" href="#{{ id }}" aria-label="Anchor link for: {{ id }}">đź”—</a> | |||||
<a class="zola-anchor" href="#{{ id }}" aria-label="Anchor link for: {{ id }}">đź”—</a> |
@@ -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> | |||||
<guid>{{ page.permalink | urlencode | safe }}</guid> | |||||
<link>{{ page.permalink | escape_xml | safe }}</link> | |||||
<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 %} | ||||
@@ -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 %} | ||||
@@ -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> | ||||
@@ -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()) | ||||
} | } | ||||
@@ -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] | ||||
@@ -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_FMT: &'static str = "auto"; | |||||
static DEFAULT_OP: &str = "fill"; | |||||
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 static_fn = GetTaxonomy::new(taxonomies.clone(), library.clone()); | |||||
let taxonomies = vec![tags.clone(), tags_fr.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 static_fn = GetTaxonomyUrl::new(&taxonomies); | |||||
let taxonomies = vec![tags.clone(), tags_fr.clone()]; | |||||
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() { | |||||
let trans_config = r#" | |||||
const TRANS_CONFIG: &str = 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)); | |||||
} | |||||
} | } |
@@ -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] | ||||
@@ -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)); | ||||
} | } | ||||
_ => { | _ => { | ||||
@@ -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(()) | ||||
@@ -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); | ||||
@@ -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" |
@@ -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) |
@@ -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 | ||||
@@ -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 | ||||
] | ] | ||||
``` | ``` | ||||
@@ -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 | ||||
@@ -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. | ||||
@@ -28,8 +28,7 @@ categories = ["programming"] | |||||
+++ | +++ | ||||
``` | ``` | ||||
The taxonomy pages will only be created if at least one non-draft page is found and | |||||
are available at the following paths: | |||||
The taxonomy pages are available at the following paths: | |||||
```plain | ```plain | ||||
$BASE_URL/$NAME/ | $BASE_URL/$NAME/ | ||||
@@ -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`. | |||||
That branch name can also be manually changed in the settings of a repository. | |||||
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. | |||||
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 && | |||||
git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages | |||||
ghp-import -n public -b master && | |||||
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` | ||||
@@ -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: | ||||
@@ -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 | ||||
@@ -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 | |||||
zola. | |||||
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. | |||||
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 | ||||
@@ -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). |
@@ -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. | ||||
@@ -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 | |||||
[section page](@/documentation/templates/pages-sections.md#section-variables) minus its pages | |||||
while both a paginated taxonomy page and a paginated section page gets a | |||||
`paginator` variable of the `Pager` type: | |||||
Both kinds get a `paginator` variable of the `Pager` type, on top of the common variables mentioned in the | |||||
[overview page](@/documentation/templates/overview.md): | |||||
```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 | ||||
@@ -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 |
@@ -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) | ||||
@@ -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 = "liquid_think" | twitter = "liquid_think" | ||||
@@ -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. | |||||
@@ -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" | ||||
@@ -116,4 +116,4 @@ katex_enable = true | |||||
katex_auto_render = true | katex_auto_render = true | ||||
``` | ``` | ||||
@@ -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" | ||||
@@ -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 | |||||
@@ -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 | |||||
@@ -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 | ||||
@@ -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> | ||||
@@ -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> | ||||
  | |||||
{%- endif %} | {%- endif %} | ||||
{%- endfor %} | {%- endfor %} |
@@ -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: | ||||
@@ -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"), | |||||
]) | |||||
]) | ]) | ||||
} | } |
@@ -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); | ||||
@@ -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<()> { | |||||
let bp = base_path.map(PathBuf::from).unwrap_or(env::current_dir().unwrap()); | |||||
pub fn check( | |||||
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; | |||||
// Disable syntax highlighting since the results won't be used | |||||
// and this operation can be expensive. | |||||
site.config.highlight_code = false; | |||||
site.config.enable_check_mode(); | |||||
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(()) | ||||
} | } |
@@ -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() { | |||||
bail!("Folder `{}` already exists", path.to_string_lossy().to_string()); | |||||
if path.exists() && !is_directory_quasi_empty(&path)? { | |||||
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); | |||||
} | |||||
} |
@@ -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 | |||||
// 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 | |||||
// This is dist/livereload.min.js from the LiveReload.js v3.0.0 release | |||||
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( | |||||
http::header::CONTENT_TYPE, | |||||
http::header::HeaderValue::from_static("text/html"), | |||||
) | |||||
.header(http::header::CONTENT_TYPE, http::header::HeaderValue::from_static("text/html")) | |||||
.body(buf); | .body(buf); | ||||
Ok(ErrorHandlerResponse::Response( | |||||
res.into_response(new_resp.into_body()), | |||||
)) | |||||
Ok(ErrorHandlerResponse::Response(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() | |||||
.handler(http::StatusCode::NOT_FOUND, not_found); | |||||
let error_handlers = | |||||
ErrorHandlers::new().handler(http::StatusCode::NOT_FOUND, not_found); | |||||
App::new() | App::new() | ||||
.data(ErrorFilePaths { | |||||
not_found: static_root.join("404.html"), | |||||
}) | |||||
.data(ErrorFilePaths { not_found: static_root.join("404.html") }) | |||||
.wrap(error_handlers) | .wrap(error_handlers) | ||||
.route( | |||||
"/livereload.js", | |||||
web::get().to(livereload_handler) | |||||
) | |||||
.route("/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( | |||||
fs::Files::new("/", &static_root) | |||||
.index_file("index.html"), | |||||
) | |||||
.service(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"), | ||||
@@ -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(); | ||||
@@ -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) => { | ||||
@@ -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://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> |
@@ -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> |