* maybe_slugify() only does simple sanitation if config.slugify is false * slugify is disabled by default, turn on for backwards-compatibility * First docs changes for optional slugification * Remove # from slugs but not & * Add/fix tests for utf8 slugs * Fix test sites for i18n slugs * fix templates tests for i18n slugs * Rename slugify setting to slugify_paths * Default slugify_paths * Update documentation for slugify_paths * quasi_slugify removes ?, /, # and newlines * Remove forbidden NTFS chars in quasi_slugify() * Slugification forbidden chars can be configured * Remove trailing dot/space in quasi_slugify * Fix NTFS path sanitation * Revert configurable slugification charset * Remove \r for windows newlines and \t tabulations in quasi_slugify() * Update docs for output paths * Replace slugify with slugify_paths * Fix test * Default to not slugifying * Move slugs utils to utils crate * Use slugify_paths for anchors as wellindex-subcmd
@@ -25,3 +25,5 @@ stage | |||||
# nixos dependencies snippet | # nixos dependencies snippet | ||||
shell.nix | shell.nix | ||||
# vim temporary files | |||||
**/.*.sw* |
@@ -5,6 +5,8 @@ | |||||
### Breaking | ### Breaking | ||||
- Remove `toc` variable in section/page context and pass it to `page.toc` and `section.toc` instead so they are | - Remove `toc` variable in section/page context and pass it to `page.toc` and `section.toc` instead so they are | ||||
accessible everywhere | accessible everywhere | ||||
- [Slugification](https://en.wikipedia.org/wiki/Slug_(web_publishing)#Slug) of page paths is now optional. By default, every path will be slugified as it is happening right now. | |||||
To keep non-ASCII characters, set `slugify_paths = true` in your config. | |||||
### Other | ### Other | ||||
- Add zenburn syntax highlighting theme | - Add zenburn syntax highlighting theme | ||||
@@ -344,10 +344,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
[[package]] | [[package]] | ||||
name = "bincode" | name = "bincode" | ||||
version = "1.2.0" | |||||
version = "1.2.1" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||||
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", | "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
] | ] | ||||
@@ -1141,7 +1140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
dependencies = [ | dependencies = [ | ||||
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", | "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"gif 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", | "gif 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"jpeg-decoder 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", | |||||
"jpeg-decoder 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", | |||||
"num-iter 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", | "num-iter 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", | "num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", | "num-traits 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
@@ -1223,7 +1222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||
[[package]] | [[package]] | ||||
name = "jpeg-decoder" | name = "jpeg-decoder" | ||||
version = "0.1.16" | |||||
version = "0.1.18" | |||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", | "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
@@ -1275,7 +1274,6 @@ dependencies = [ | |||||
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"serde_derive 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | "serde_derive 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"slotmap 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", | "slotmap 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"slug 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | |||||
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", | "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"tera 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | "tera 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", | "toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
@@ -2099,7 +2097,6 @@ dependencies = [ | |||||
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", | "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"serde_derive 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | "serde_derive 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"slug 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | |||||
"syntect 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)", | "syntect 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"templates 0.1.0", | "templates 0.1.0", | ||||
"tera 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | "tera 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
@@ -2520,7 +2517,7 @@ name = "syntect" | |||||
version = "3.2.0" | version = "3.2.0" | ||||
source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
dependencies = [ | dependencies = [ | ||||
"bincode 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||||
"bincode 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", | "flate2 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", | "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
@@ -3009,6 +3006,7 @@ version = "0.1.0" | |||||
dependencies = [ | dependencies = [ | ||||
"errors 0.1.0", | "errors 0.1.0", | ||||
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"slug 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", | |||||
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", | "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"tera 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | "tera 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
"toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", | "toml 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", | ||||
@@ -3259,7 +3257,7 @@ dependencies = [ | |||||
"checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" | "checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" | ||||
"checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" | "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" | ||||
"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" | "checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" | ||||
"checksum bincode 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8ab639324e3ee8774d296864fbc0dbbb256cf1a41c490b94cba90c082915f92" | |||||
"checksum bincode 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf" | |||||
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" | "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" | ||||
"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" | "checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" | ||||
"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" | "checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" | ||||
@@ -3351,7 +3349,7 @@ dependencies = [ | |||||
"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" | "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" | ||||
"checksum ipconfig 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa79fa216fbe60834a9c0737d7fcd30425b32d1c58854663e24d4c4b328ed83f" | "checksum ipconfig 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa79fa216fbe60834a9c0737d7fcd30425b32d1c58854663e24d4c4b328ed83f" | ||||
"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" | "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" | ||||
"checksum jpeg-decoder 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "c1aae18ffeeae409c6622c3b6a7ee49792a7e5a062eea1b135fbb74e301792ba" | |||||
"checksum jpeg-decoder 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "0256f0aec7352539102a9efbcb75543227b7ab1117e0f95450023af730128451" | |||||
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" | ||||
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" | "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" | ||||
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" | "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" | ||||
@@ -130,6 +130,8 @@ pub struct Config { | |||||
/// key into different language. | /// key into different language. | ||||
translations: HashMap<String, TranslateTerm>, | translations: HashMap<String, TranslateTerm>, | ||||
/// Whether to slugify page and taxonomy URLs (disable for UTF-8 URLs) | |||||
pub slugify_paths: bool, | |||||
/// 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, | ||||
/// Which themes to use for code highlighting. See Readme for supported themes | /// Which themes to use for code highlighting. See Readme for supported themes | ||||
@@ -354,6 +356,7 @@ impl Default for Config { | |||||
title: None, | title: None, | ||||
description: None, | description: None, | ||||
theme: None, | theme: None, | ||||
slugify_paths: true, | |||||
highlight_code: false, | highlight_code: false, | ||||
highlight_theme: "base16-ocean-dark".to_string(), | highlight_theme: "base16-ocean-dark".to_string(), | ||||
default_language: "en".to_string(), | default_language: "en".to_string(), | ||||
@@ -10,7 +10,6 @@ chrono = { version = "0.4", features = ["serde"] } | |||||
tera = "1" | tera = "1" | ||||
serde = "1" | serde = "1" | ||||
serde_derive = "1" | serde_derive = "1" | ||||
slug = "0.1" | |||||
regex = "1" | regex = "1" | ||||
lazy_static = "1" | lazy_static = "1" | ||||
@@ -4,7 +4,6 @@ use std::path::{Path, PathBuf}; | |||||
use regex::Regex; | use regex::Regex; | ||||
use slotmap::DefaultKey; | use slotmap::DefaultKey; | ||||
use slug::slugify; | |||||
use tera::{Context as TeraContext, Tera}; | use tera::{Context as TeraContext, Tera}; | ||||
use config::Config; | use config::Config; | ||||
@@ -19,6 +18,7 @@ use utils::templates::render_template; | |||||
use content::file_info::FileInfo; | use content::file_info::FileInfo; | ||||
use content::has_anchor; | use content::has_anchor; | ||||
use content::ser::SerializingPage; | use content::ser::SerializingPage; | ||||
use utils::slugs::maybe_slugify_paths; | |||||
lazy_static! { | lazy_static! { | ||||
// Based on https://regex101.com/r/H2n38Z/1/tests | // Based on https://regex101.com/r/H2n38Z/1/tests | ||||
@@ -160,21 +160,21 @@ impl Page { | |||||
page.slug = { | page.slug = { | ||||
if let Some(ref slug) = page.meta.slug { | if let Some(ref slug) = page.meta.slug { | ||||
slugify(&slug.trim()) | |||||
maybe_slugify_paths(&slug.trim(), config.slugify_paths) | |||||
} 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 { | ||||
slugify(&slug) | |||||
maybe_slugify_paths(&slug, config.slugify_paths) | |||||
} else { | } else { | ||||
slugify(parent.file_name().unwrap().to_str().unwrap()) | |||||
maybe_slugify_paths(parent.file_name().unwrap().to_str().unwrap(), config.slugify_paths) | |||||
} | } | ||||
} else { | } else { | ||||
slugify(&page.file.name) | |||||
maybe_slugify_paths(&page.file.name, config.slugify_paths) | |||||
} | } | ||||
} else if let Some(slug) = slug_from_dated_filename { | } else if let Some(slug) = slug_from_dated_filename { | ||||
slugify(&slug) | |||||
maybe_slugify_paths(&slug, config.slugify_paths) | |||||
} else { | } else { | ||||
slugify(&page.file.name) | |||||
maybe_slugify_paths(&page.file.name, config.slugify_paths) | |||||
} | } | ||||
}; | }; | ||||
@@ -443,7 +443,8 @@ Hello world"#; | |||||
slug = "hello-&-world" | slug = "hello-&-world" | ||||
+++ | +++ | ||||
Hello world"#; | Hello world"#; | ||||
let config = Config::default(); | |||||
let mut config = Config::default(); | |||||
config.slugify_paths = true; | |||||
let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); | let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); | ||||
assert!(res.is_ok()); | assert!(res.is_ok()); | ||||
let page = res.unwrap(); | let page = res.unwrap(); | ||||
@@ -452,6 +453,23 @@ 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_utf8_slug_frontmatter() { | |||||
let content = r#" | |||||
+++ | |||||
slug = "日本" | |||||
+++ | |||||
Hello world"#; | |||||
let mut config = Config::default(); | |||||
config.slugify_paths = false; | |||||
let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); | |||||
assert!(res.is_ok()); | |||||
let page = res.unwrap(); | |||||
assert_eq!(page.path, "日本/"); | |||||
assert_eq!(page.components, vec!["日本"]); | |||||
assert_eq!(page.permalink, config.make_permalink("日本")); | |||||
} | |||||
#[test] | #[test] | ||||
fn can_make_url_from_path() { | fn can_make_url_from_path() { | ||||
let content = r#" | let content = r#" | ||||
@@ -508,7 +526,8 @@ Hello world"#; | |||||
#[test] | #[test] | ||||
fn can_make_slug_from_non_slug_filename() { | fn can_make_slug_from_non_slug_filename() { | ||||
let config = Config::default(); | |||||
let mut config = Config::default(); | |||||
config.slugify_paths = true; | |||||
let res = | let res = | ||||
Page::parse(Path::new(" file with space.md"), "+++\n+++", &config, &PathBuf::new()); | Page::parse(Path::new(" file with space.md"), "+++\n+++", &config, &PathBuf::new()); | ||||
assert!(res.is_ok()); | assert!(res.is_ok()); | ||||
@@ -517,6 +536,17 @@ Hello world"#; | |||||
assert_eq!(page.permalink, config.make_permalink(&page.slug)); | assert_eq!(page.permalink, config.make_permalink(&page.slug)); | ||||
} | } | ||||
#[test] | |||||
fn can_make_path_from_utf8_filename() { | |||||
let mut config = Config::default(); | |||||
config.slugify_paths = false; | |||||
let res = Page::parse(Path::new("日本.md"), "+++\n++++", &config, &PathBuf::new()); | |||||
assert!(res.is_ok()); | |||||
let page = res.unwrap(); | |||||
assert_eq!(page.slug, "日本"); | |||||
assert_eq!(page.permalink, config.make_permalink(&page.slug)); | |||||
} | |||||
#[test] | #[test] | ||||
fn can_specify_summary() { | fn can_specify_summary() { | ||||
let config = Config::default(); | let config = Config::default(); | ||||
@@ -1,5 +1,4 @@ | |||||
extern crate serde; | extern crate serde; | ||||
extern crate slug; | |||||
extern crate tera; | extern crate tera; | ||||
#[macro_use] | #[macro_use] | ||||
extern crate serde_derive; | extern crate serde_derive; | ||||
@@ -1,7 +1,6 @@ | |||||
use std::collections::HashMap; | use std::collections::HashMap; | ||||
use slotmap::DefaultKey; | use slotmap::DefaultKey; | ||||
use slug::slugify; | |||||
use tera::{Context, Tera}; | use tera::{Context, Tera}; | ||||
use config::{Config, Taxonomy as TaxonomyConfig}; | use config::{Config, Taxonomy as TaxonomyConfig}; | ||||
@@ -10,6 +9,7 @@ use utils::templates::render_template; | |||||
use content::SerializingPage; | use content::SerializingPage; | ||||
use library::Library; | use library::Library; | ||||
use utils::slugs::maybe_slugify_paths; | |||||
use sorting::sort_pages_by_date; | use sorting::sort_pages_by_date; | ||||
#[derive(Debug, Clone, PartialEq, Serialize)] | #[derive(Debug, Clone, PartialEq, Serialize)] | ||||
@@ -69,7 +69,7 @@ impl TaxonomyItem { | |||||
}) | }) | ||||
.collect(); | .collect(); | ||||
let (mut pages, ignored_pages) = sort_pages_by_date(data); | let (mut pages, ignored_pages) = sort_pages_by_date(data); | ||||
let slug = slugify(name); | |||||
let slug = maybe_slugify_paths(name, config.slugify_paths); | |||||
let permalink = if taxonomy.lang != config.default_language { | let permalink = if taxonomy.lang != config.default_language { | ||||
config.make_permalink(&format!("/{}/{}/{}", taxonomy.lang, taxonomy.name, slug)) | config.make_permalink(&format!("/{}/{}/{}", taxonomy.lang, taxonomy.name, slug)) | ||||
} else { | } else { | ||||
@@ -169,7 +169,6 @@ 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); | ||||
@@ -331,6 +330,101 @@ mod tests { | |||||
assert_eq!(categories.items[1].pages.len(), 1); | assert_eq!(categories.items[1].pages.len(), 1); | ||||
} | } | ||||
#[test] | |||||
fn can_make_slugified_taxonomies() { | |||||
let mut config = Config::default(); | |||||
let mut library = Library::new(2, 0, false); | |||||
config.taxonomies = vec![ | |||||
TaxonomyConfig { | |||||
name: "categories".to_string(), | |||||
lang: config.default_language.clone(), | |||||
..TaxonomyConfig::default() | |||||
}, | |||||
TaxonomyConfig { | |||||
name: "tags".to_string(), | |||||
lang: config.default_language.clone(), | |||||
..TaxonomyConfig::default() | |||||
}, | |||||
TaxonomyConfig { | |||||
name: "authors".to_string(), | |||||
lang: config.default_language.clone(), | |||||
..TaxonomyConfig::default() | |||||
}, | |||||
]; | |||||
let mut page1 = Page::default(); | |||||
let mut taxo_page1 = HashMap::new(); | |||||
taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]); | |||||
taxo_page1.insert("categories".to_string(), vec!["Programming tutorials".to_string()]); | |||||
page1.meta.taxonomies = taxo_page1; | |||||
page1.lang = config.default_language.clone(); | |||||
library.insert_page(page1); | |||||
let mut page2 = Page::default(); | |||||
let mut taxo_page2 = HashMap::new(); | |||||
taxo_page2.insert("tags".to_string(), vec!["rust".to_string(), "js".to_string()]); | |||||
taxo_page2.insert("categories".to_string(), vec!["Other".to_string()]); | |||||
page2.meta.taxonomies = taxo_page2; | |||||
page2.lang = config.default_language.clone(); | |||||
library.insert_page(page2); | |||||
let mut page3 = Page::default(); | |||||
let mut taxo_page3 = HashMap::new(); | |||||
taxo_page3.insert("tags".to_string(), vec!["js".to_string()]); | |||||
taxo_page3.insert("authors".to_string(), vec!["Vincent Prouillet".to_string()]); | |||||
page3.meta.taxonomies = taxo_page3; | |||||
page3.lang = config.default_language.clone(); | |||||
library.insert_page(page3); | |||||
let taxonomies = find_taxonomies(&config, &library).unwrap(); | |||||
let (tags, categories, authors) = { | |||||
let mut t = None; | |||||
let mut c = None; | |||||
let mut a = None; | |||||
for x in taxonomies { | |||||
match x.kind.name.as_ref() { | |||||
"tags" => t = Some(x), | |||||
"categories" => c = Some(x), | |||||
"authors" => a = Some(x), | |||||
_ => unreachable!(), | |||||
} | |||||
} | |||||
(t.unwrap(), c.unwrap(), a.unwrap()) | |||||
}; | |||||
assert_eq!(tags.items.len(), 3); | |||||
assert_eq!(categories.items.len(), 2); | |||||
assert_eq!(authors.items.len(), 1); | |||||
assert_eq!(tags.items[0].name, "db"); | |||||
assert_eq!(tags.items[0].slug, "db"); | |||||
assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/"); | |||||
assert_eq!(tags.items[0].pages.len(), 1); | |||||
assert_eq!(tags.items[1].name, "js"); | |||||
assert_eq!(tags.items[1].slug, "js"); | |||||
assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/js/"); | |||||
assert_eq!(tags.items[1].pages.len(), 2); | |||||
assert_eq!(tags.items[2].name, "rust"); | |||||
assert_eq!(tags.items[2].slug, "rust"); | |||||
assert_eq!(tags.items[2].permalink, "http://a-website.com/tags/rust/"); | |||||
assert_eq!(tags.items[2].pages.len(), 2); | |||||
assert_eq!(categories.items[0].name, "Other"); | |||||
assert_eq!(categories.items[0].slug, "other"); | |||||
assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/other/"); | |||||
assert_eq!(categories.items[0].pages.len(), 1); | |||||
assert_eq!(categories.items[1].name, "Programming tutorials"); | |||||
assert_eq!(categories.items[1].slug, "programming-tutorials"); | |||||
assert_eq!( | |||||
categories.items[1].permalink, | |||||
"http://a-website.com/categories/programming-tutorials/" | |||||
); | |||||
assert_eq!(categories.items[1].pages.len(), 1); | |||||
} | |||||
#[test] | #[test] | ||||
fn errors_on_unknown_taxonomy() { | fn errors_on_unknown_taxonomy() { | ||||
let mut config = Config::default(); | let mut config = Config::default(); | ||||
@@ -466,4 +560,155 @@ mod tests { | |||||
); | ); | ||||
assert_eq!(categories.items[1].pages.len(), 1); | assert_eq!(categories.items[1].pages.len(), 1); | ||||
} | } | ||||
#[test] | |||||
fn can_make_utf8_taxonomies() { | |||||
let mut config = Config::default(); | |||||
config.slugify_paths = false; | |||||
config.languages.push(Language { | |||||
rss: false, | |||||
code: "fr".to_string(), | |||||
..Language::default() | |||||
}); | |||||
let mut library = Library::new(2, 0, true); | |||||
config.taxonomies = vec![TaxonomyConfig { | |||||
name: "catégories".to_string(), | |||||
lang: "fr".to_string(), | |||||
..TaxonomyConfig::default() | |||||
}]; | |||||
let mut page = Page::default(); | |||||
page.lang = "fr".to_string(); | |||||
let mut taxo_page = HashMap::new(); | |||||
taxo_page.insert("catégories".to_string(), vec!["Écologie".to_string()]); | |||||
page.meta.taxonomies = taxo_page; | |||||
library.insert_page(page); | |||||
let taxonomies = find_taxonomies(&config, &library).unwrap(); | |||||
let categories = &taxonomies[0]; | |||||
assert_eq!(categories.items.len(), 1); | |||||
assert_eq!(categories.items[0].name, "Écologie"); | |||||
assert_eq!( | |||||
categories.items[0].permalink, | |||||
"http://a-website.com/fr/catégories/Écologie/" | |||||
); | |||||
assert_eq!(categories.items[0].pages.len(), 1); | |||||
} | |||||
#[test] | |||||
fn can_make_slugified_taxonomies_in_multiple_languages() { | |||||
let mut config = Config::default(); | |||||
config.slugify_paths = true; | |||||
config.languages.push(Language { | |||||
rss: false, | |||||
code: "fr".to_string(), | |||||
..Language::default() | |||||
}); | |||||
let mut library = Library::new(2, 0, true); | |||||
config.taxonomies = vec![ | |||||
TaxonomyConfig { | |||||
name: "categories".to_string(), | |||||
lang: config.default_language.clone(), | |||||
..TaxonomyConfig::default() | |||||
}, | |||||
TaxonomyConfig { | |||||
name: "tags".to_string(), | |||||
lang: config.default_language.clone(), | |||||
..TaxonomyConfig::default() | |||||
}, | |||||
TaxonomyConfig { | |||||
name: "auteurs".to_string(), | |||||
lang: "fr".to_string(), | |||||
..TaxonomyConfig::default() | |||||
}, | |||||
TaxonomyConfig { | |||||
name: "tags".to_string(), | |||||
lang: "fr".to_string(), | |||||
..TaxonomyConfig::default() | |||||
}, | |||||
]; | |||||
let mut page1 = Page::default(); | |||||
let mut taxo_page1 = HashMap::new(); | |||||
taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]); | |||||
taxo_page1.insert("categories".to_string(), vec!["Programming tutorials".to_string()]); | |||||
page1.meta.taxonomies = taxo_page1; | |||||
page1.lang = config.default_language.clone(); | |||||
library.insert_page(page1); | |||||
let mut page2 = Page::default(); | |||||
let mut taxo_page2 = HashMap::new(); | |||||
taxo_page2.insert("tags".to_string(), vec!["rust".to_string()]); | |||||
taxo_page2.insert("categories".to_string(), vec!["Other".to_string()]); | |||||
page2.meta.taxonomies = taxo_page2; | |||||
page2.lang = config.default_language.clone(); | |||||
library.insert_page(page2); | |||||
let mut page3 = Page::default(); | |||||
page3.lang = "fr".to_string(); | |||||
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()]); | |||||
page3.meta.taxonomies = taxo_page3; | |||||
library.insert_page(page3); | |||||
let taxonomies = find_taxonomies(&config, &library).unwrap(); | |||||
let (tags, categories, authors) = { | |||||
let mut t = None; | |||||
let mut c = None; | |||||
let mut a = None; | |||||
for x in taxonomies { | |||||
match x.kind.name.as_ref() { | |||||
"tags" => { | |||||
if x.kind.lang == "en" { | |||||
t = Some(x) | |||||
} | |||||
} | |||||
"categories" => c = Some(x), | |||||
"auteurs" => a = Some(x), | |||||
_ => unreachable!(), | |||||
} | |||||
} | |||||
(t.unwrap(), c.unwrap(), a.unwrap()) | |||||
}; | |||||
assert_eq!(tags.items.len(), 2); | |||||
assert_eq!(categories.items.len(), 2); | |||||
assert_eq!(authors.items.len(), 1); | |||||
assert_eq!(tags.items[0].name, "db"); | |||||
assert_eq!(tags.items[0].slug, "db"); | |||||
assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/"); | |||||
assert_eq!(tags.items[0].pages.len(), 1); | |||||
assert_eq!(tags.items[1].name, "rust"); | |||||
assert_eq!(tags.items[1].slug, "rust"); | |||||
assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/rust/"); | |||||
assert_eq!(tags.items[1].pages.len(), 2); | |||||
assert_eq!(authors.items[0].name, "Vincent Prouillet"); | |||||
assert_eq!(authors.items[0].slug, "vincent-prouillet"); | |||||
assert_eq!( | |||||
authors.items[0].permalink, | |||||
"http://a-website.com/fr/auteurs/vincent-prouillet/" | |||||
); | |||||
assert_eq!(authors.items[0].pages.len(), 1); | |||||
assert_eq!(categories.items[0].name, "Other"); | |||||
assert_eq!(categories.items[0].slug, "other"); | |||||
assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/other/"); | |||||
assert_eq!(categories.items[0].pages.len(), 1); | |||||
assert_eq!(categories.items[1].name, "Programming tutorials"); | |||||
assert_eq!(categories.items[1].slug, "programming-tutorials"); | |||||
assert_eq!( | |||||
categories.items[1].permalink, | |||||
"http://a-website.com/categories/programming-tutorials/" | |||||
); | |||||
assert_eq!(categories.items[1].pages.len(), 1); | |||||
} | |||||
} | } |
@@ -7,7 +7,6 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | |||||
tera = { version = "1", features = ["preserve_order"] } | tera = { version = "1", features = ["preserve_order"] } | ||||
syntect = "=3.2.0" | syntect = "=3.2.0" | ||||
pulldown-cmark = "0.6" | pulldown-cmark = "0.6" | ||||
slug = "0.1" | |||||
serde = "1" | serde = "1" | ||||
serde_derive = "1" | serde_derive = "1" | ||||
pest = "2" | pest = "2" | ||||
@@ -1,5 +1,4 @@ | |||||
extern crate pulldown_cmark; | extern crate pulldown_cmark; | ||||
extern crate slug; | |||||
extern crate syntect; | extern crate syntect; | ||||
extern crate tera; | extern crate tera; | ||||
#[macro_use] | #[macro_use] | ||||
@@ -1,6 +1,5 @@ | |||||
use pulldown_cmark as cmark; | use pulldown_cmark as cmark; | ||||
use regex::Regex; | use regex::Regex; | ||||
use slug::slugify; | |||||
use syntect::easy::HighlightLines; | use syntect::easy::HighlightLines; | ||||
use syntect::html::{ | use syntect::html::{ | ||||
start_highlighted_html_snippet, styled_line_to_highlighted_html, IncludeBackground, | start_highlighted_html_snippet, styled_line_to_highlighted_html, IncludeBackground, | ||||
@@ -13,6 +12,7 @@ use front_matter::InsertAnchor; | |||||
use table_of_contents::{make_table_of_contents, Heading}; | 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; | ||||
use utils::slugs::maybe_slugify_anchors; | |||||
use self::cmark::{Event, LinkType, Options, Parser, Tag}; | use self::cmark::{Event, LinkType, Options, Parser, Tag}; | ||||
@@ -298,7 +298,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render | |||||
let title = get_text(&events[start_idx + 1..end_idx]); | let title = get_text(&events[start_idx + 1..end_idx]); | ||||
let id = heading_ref | let id = heading_ref | ||||
.id | .id | ||||
.unwrap_or_else(|| find_anchor(&inserted_anchors, slugify(&title), 0)); | |||||
.unwrap_or_else(|| find_anchor(&inserted_anchors, maybe_slugify_anchors(&title, context.config.slugify_paths), 0)); | |||||
inserted_anchors.push(id.clone()); | inserted_anchors.push(id.clone()); | ||||
// insert `id` to the tag | // insert `id` to the tag | ||||
@@ -351,6 +351,17 @@ fn can_add_id_to_headings_same_slug() { | |||||
assert_eq!(res.body, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n"); | assert_eq!(res.body, "<h1 id=\"hello\">Hello</h1>\n<h1 id=\"hello-1\">Hello</h1>\n"); | ||||
} | } | ||||
#[test] | |||||
fn can_add_non_slug_id_to_headings() { | |||||
let tera_ctx = Tera::default(); | |||||
let permalinks_ctx = HashMap::new(); | |||||
let mut config = Config::default(); | |||||
config.slugify_paths = false; | |||||
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); | |||||
let res = render_content(r#"# L'Ă©cologie et vous"#, &context).unwrap(); | |||||
assert_eq!(res.body, "<h1 id=\"L'Ă©cologie_et_vous\">L'Ă©cologie et vous</h1>\n"); | |||||
} | |||||
#[test] | #[test] | ||||
fn can_handle_manual_ids_on_headings() { | fn can_handle_manual_ids_on_headings() { | ||||
let tera_ctx = Tera::default(); | let tera_ctx = Tera::default(); | ||||
@@ -389,7 +389,8 @@ mod tests { | |||||
#[test] | #[test] | ||||
fn can_get_taxonomy() { | fn can_get_taxonomy() { | ||||
let config = Config::default(); | |||||
let mut config = Config::default(); | |||||
config.slugify_paths = true; | |||||
let taxo_config = TaxonomyConfig { | let taxo_config = TaxonomyConfig { | ||||
name: "tags".to_string(), | name: "tags".to_string(), | ||||
lang: config.default_language.clone(), | lang: config.default_language.clone(), | ||||
@@ -466,7 +467,8 @@ mod tests { | |||||
#[test] | #[test] | ||||
fn can_get_taxonomy_url() { | fn can_get_taxonomy_url() { | ||||
let config = Config::default(); | |||||
let mut config = Config::default(); | |||||
config.slugify_paths = true; | |||||
let taxo_config = TaxonomyConfig { | let taxo_config = TaxonomyConfig { | ||||
name: "tags".to_string(), | name: "tags".to_string(), | ||||
lang: config.default_language.clone(), | lang: config.default_language.clone(), | ||||
@@ -10,6 +10,7 @@ unicode-segmentation = "1.2" | |||||
walkdir = "2" | walkdir = "2" | ||||
toml = "0.5" | toml = "0.5" | ||||
serde = "1" | serde = "1" | ||||
slug = "0.1" | |||||
[dev-dependencies] | [dev-dependencies] | ||||
tempfile = "3" | tempfile = "3" |
@@ -8,6 +8,7 @@ extern crate tera; | |||||
extern crate toml; | extern crate toml; | ||||
extern crate unicode_segmentation; | extern crate unicode_segmentation; | ||||
extern crate walkdir; | extern crate walkdir; | ||||
extern crate slug; | |||||
pub mod de; | pub mod de; | ||||
pub mod fs; | pub mod fs; | ||||
@@ -15,3 +16,4 @@ pub mod net; | |||||
pub mod site; | pub mod site; | ||||
pub mod templates; | pub mod templates; | ||||
pub mod vec; | pub mod vec; | ||||
pub mod slugs; |
@@ -0,0 +1,107 @@ | |||||
fn strip_chars(s: &str, chars: &str) -> String { | |||||
let mut sanitized_string = s.to_string(); | |||||
sanitized_string.retain( |c| !chars.contains(c)); | |||||
sanitized_string | |||||
} | |||||
fn strip_invalid_paths_chars(s: &str) -> String { | |||||
// NTFS forbidden characters : https://gist.github.com/doctaphred/d01d05291546186941e1b7ddc02034d3 | |||||
// Also we need to trim . from the end of filename | |||||
let trimmed = s.trim_end_matches(|c| c == ' ' || c == '.'); | |||||
let cleaned = trimmed.replace(" ", "_"); | |||||
// And () [] since they are not allowed in markdown links | |||||
strip_chars(&cleaned, "<>:/|?*#()[]\n\"\\\r\t") | |||||
} | |||||
fn strip_invalid_anchors_chars(s: &str) -> String { | |||||
// spaces are not valid in markdown links | |||||
let cleaned = s.replace(" ", "_"); | |||||
// https://tools.ietf.org/html/rfc3986#section-3.5 | |||||
strip_chars(&cleaned, "\"#%<>[\\]()^`{|}") | |||||
} | |||||
pub fn maybe_slugify_paths(s: &str, slugify: bool) -> String { | |||||
if slugify { | |||||
// ASCII slugification | |||||
slug::slugify(s) | |||||
} | |||||
else { | |||||
// Only remove forbidden characters | |||||
strip_invalid_paths_chars(s) | |||||
} | |||||
} | |||||
pub fn maybe_slugify_anchors(s: &str, slugify: bool) -> String { | |||||
if slugify { | |||||
// ASCII slugification | |||||
slug::slugify(s) | |||||
} | |||||
else { | |||||
// Only remove forbidden characters | |||||
strip_invalid_anchors_chars(s) | |||||
} | |||||
} | |||||
#[cfg(test)] | |||||
mod tests { | |||||
use super::*; | |||||
#[test] | |||||
fn strip_invalid_paths_chars_works() { | |||||
let tests = vec![ | |||||
// no newlines | |||||
("test\ntest", "testtest"), | |||||
// no whitespaces | |||||
("test ", "test"), | |||||
("t est ", "t_est"), | |||||
// invalid NTFS | |||||
("test .", "test"), | |||||
("test. ", "test"), | |||||
("test#test/test?test", "testtesttesttest"), | |||||
// Invalid CommonMark chars in links | |||||
("test (hey)", "test_hey"), | |||||
("test (hey", "test_hey"), | |||||
("test hey)", "test_hey"), | |||||
("test [hey]", "test_hey"), | |||||
("test [hey", "test_hey"), | |||||
("test hey]", "test_hey"), | |||||
// UTF-8 | |||||
("日本", "日本"), | |||||
]; | |||||
for (input, expected) in tests { | |||||
assert_eq!(strip_invalid_paths_chars(&input), expected); | |||||
} | |||||
} | |||||
#[test] | |||||
fn strip_invalid_anchors_chars_works() { | |||||
let tests = vec![ | |||||
("日本", "日本"), | |||||
// Some invalid chars get removed | |||||
("test#", "test"), | |||||
("test<", "test"), | |||||
("test%", "test"), | |||||
("test^", "test"), | |||||
("test{", "test"), | |||||
("test|", "test"), | |||||
("test(", "test"), | |||||
// Spaces are replaced by `_` | |||||
("test hey", "test_hey"), | |||||
]; | |||||
for (input, expected) in tests { | |||||
assert_eq!(strip_invalid_anchors_chars(&input), expected); | |||||
} | |||||
} | |||||
#[test] | |||||
fn maybe_slugify_paths_enabled() { | |||||
assert_eq!(maybe_slugify_paths("héhé", true), "hehe"); | |||||
} | |||||
#[test] | |||||
fn maybe_slugify_paths_disabled() { | |||||
assert_eq!(maybe_slugify_paths("héhé", false), "héhé"); | |||||
} | |||||
} |
@@ -4,9 +4,11 @@ weight = 50 | |||||
+++ | +++ | ||||
## Heading id and anchor insertion | ## Heading id and anchor insertion | ||||
While rendering the Markdown content, a unique id will automatically be assigned to each heading. This id is created | |||||
by converting the heading text to a [slug](https://en.wikipedia.org/wiki/Semantic_URL#Slug), and appending numbers at | |||||
the end if the slug already exists for that article. For example: | |||||
While rendering the Markdown content, a unique id will automatically be assigned to each heading. | |||||
This id is created by converting the heading text to a [slug](https://en.wikipedia.org/wiki/Semantic_URL#Slug) if `slugify_paths` is enabled. | |||||
if `slugify_paths` is disabled, whitespaces are replaced by `_` and the following characters are stripped: `#`, `%`, `<`, `>`, `[`, `]`, `(`, `)`, \`, `^`, `{`, `|`, `}`. | |||||
A number is appended at the end if the slug already exists for that article | |||||
For example: | |||||
```md | ```md | ||||
# Something exciting! <- something-exciting | # Something exciting! <- something-exciting | ||||
@@ -27,6 +27,49 @@ As you can see, creating an `about.md` file is equivalent to creating an | |||||
the `about` directory allows you to use asset co-location, as discussed in the | the `about` directory allows you to use asset co-location, as discussed in the | ||||
[overview](@/documentation/content/overview.md#asset-colocation) section. | [overview](@/documentation/content/overview.md#asset-colocation) section. | ||||
## Output paths | |||||
For any page within your content folder, its output path will be defined by either: | |||||
- its `slug` frontmatter key | |||||
- its filename | |||||
Either way, these proposed path will be sanitized before being used. | |||||
If `slugify_paths` is enabled in the site's config - the default - paths are [slugified](https://en.wikipedia.org/wiki/Clean_URL#Slug). | |||||
Otherwise, a simpler sanitation is performed, outputting only valid NTFS paths. | |||||
The following characters are removed: `<`, `>`, `:`, `/`, `|`, `?`, `*`, `#`, `\\`, `(`, `)`, `[`, `]` as well as newlines and tabulations. | |||||
Additionally, trailing whitespace and dots are removed and whitespaces are replaced by `_`. | |||||
**NOTE:** To produce URLs containing non-English characters (UTF8), `slugify_paths` needs to be set to `false`. | |||||
### Path from frontmatter | |||||
The output path for the page will first be read from the `slug` key in the page's frontmatter. | |||||
**Example:** (file `content/zines/mlf-kurdistan.md`) | |||||
``` | |||||
+++ | |||||
title = "Le mouvement des Femmes Libres, à la tête de la libération kurde" | |||||
slug = "femmes-libres-libération-kurde" | |||||
+++ | |||||
This is my article. | |||||
``` | |||||
This frontmatter will output the article to `[base_url]/zines/femmes-libres-libération-kurde` with `slugify_paths` disabled, and to `[base_url]/zines/femmes-libres-liberation-kurde` with `slugify_enabled` enabled. | |||||
### Path from filename | |||||
When the article's output path is not specified in the frontmatter, it is extracted from the file's path in the content folder. Consider a file `content/foo/bar/thing.md`. The output path is constructed: | |||||
- if the filename is `index.md`, its parent folder name (`bar`) is used as output path | |||||
- otherwise, the output path is extracted from `thing`Â (the filename without the `.md` extension) | |||||
If the path found starts with a datetime string (`YYYY-mm-dd` or [a RFC3339 datetime](https://www.ietf.org/rfc/rfc3339.txt)) followed by an underscore (`_`) or a dash (`-`), this date is removed from the output path and will be used as the page date (unless already set in the front-matter). Note that the full RFC3339 datetime contains colons, which is not a valid character in a filename on Windows. | |||||
The output path extracted from the file path is then slugified or not depending on the `slugify_paths` config, as explained previously. | |||||
**Example:** The file `content/blog/2018-10-10-hello-world.md` will generated a page available at will be available at `[base_url]/hello-world`. | |||||
## Front matter | ## Front matter | ||||
The TOML front matter is a set of metadata embedded in a file at the beginning of the file enclosed | The TOML front matter is a set of metadata embedded in a file at the beginning of the file enclosed | ||||
@@ -5,7 +5,7 @@ weight = 90 | |||||
Zola has built-in support for taxonomies. | Zola has built-in support for taxonomies. | ||||
The first step is to define the taxonomies in your [config.toml](@/documentation/getting-started/configuration.md). | |||||
## Configuration | |||||
A taxonomy has five variables: | A taxonomy has five variables: | ||||
@@ -16,21 +16,48 @@ For example the default would be page/1. | |||||
- `rss`: if set to `true`, an RSS feed will be generated for each term. | - `rss`: if set to `true`, an RSS feed will be generated for each term. | ||||
- `lang`: only set this if you are making a multilingual site and want to indicate which language this taxonomy is for | - `lang`: only set this if you are making a multilingual site and want to indicate which language this taxonomy is for | ||||
Once this is done, you can then set taxonomies in your content and Zola will pick | |||||
them up: | |||||
**Example 1:** (one language) | |||||
```toml | |||||
taxonomies = [ name = "categories", rss = true ] | |||||
``` | |||||
**Example 2:** (multilingual site) | |||||
```toml | |||||
taxonomies = [ | |||||
{name = "tags", lang = "fr"}, | |||||
{name = "tags", lang = "eo"}, | |||||
{name = "tags", lang = "en"}, | |||||
] | |||||
``` | |||||
## Using taxonomies | |||||
Once the configuration is done, you can then set taxonomies in your content and Zola will pick them up: | |||||
**Example:** | |||||
```toml | ```toml | ||||
+++ | +++ | ||||
... | |||||
title = "Writing a static-site generator in Rust" | |||||
date = 2019-08-15 | |||||
[taxonomies] | [taxonomies] | ||||
tags = ["rust", "web"] | tags = ["rust", "web"] | ||||
categories = ["programming"] | categories = ["programming"] | ||||
+++ | +++ | ||||
``` | ``` | ||||
The taxonomy pages are available at the following paths: | |||||
## Output paths | |||||
In a similar manner to how section and pages calculate their output path: | |||||
- the taxonomy name is never slugified | |||||
- the taxonomy entry (eg. as specific tag) is slugified when `slugify_paths` is enabled in the configuration | |||||
The taxonomy pages are then available at the following paths: | |||||
```plain | ```plain | ||||
$BASE_URL/$NAME/ | |||||
$BASE_URL/$NAME/$SLUG | |||||
$BASE_URL/$NAME/ (taxonomy) | |||||
$BASE_URL/$NAME/$SLUG (taxonomy entry) | |||||
``` | ``` | ||||
@@ -27,6 +27,10 @@ default_language = "en" | |||||
# The site theme to use. | # The site theme to use. | ||||
theme = "" | theme = "" | ||||
# Slugify paths for compatibility with ASCII-only URLs produced by Zola < 0.9 | |||||
# Enabling this setting removes non-English (UTF8) characters in URLs | |||||
slugify_paths = false | |||||
# When set to "true", all code blocks are highlighted. | # When set to "true", all code blocks are highlighted. | ||||
highlight_code = false | highlight_code = false | ||||
@@ -4,6 +4,7 @@ highlight_code = true | |||||
compile_sass = true | compile_sass = true | ||||
generate_rss = true | generate_rss = true | ||||
theme = "sample" | theme = "sample" | ||||
slugify_paths = true | |||||
taxonomies = [ | taxonomies = [ | ||||
{name = "categories", rss = true}, | {name = "categories", rss = true}, | ||||