Merge pull request #1038 from getzola/next

Next version
This commit is contained in:
Vincent Prouillet 2020-09-04 23:42:30 +02:00 committed by GitHub
commit 2d1c954322
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
128 changed files with 5376 additions and 2758 deletions

34
.dockerignore Normal file
View File

@ -0,0 +1,34 @@
.dockerignore
.git*
Dockerfile
# From .gitignore
target
.idea/
test_site/public
test_site_i18n/public
docs/public
small-blog
medium-blog
big-blog
huge-blog
extra-huge-blog
small-kb
medium-kb
huge-kb
current.bench
now.bench
*.zst
# snapcraft artifacts
snap/.snapcraft
parts
prime
stage
# nixos dependencies snippet
shell.nix
# vim temporary files
**/.*.sw*

1
.gitignore vendored
View File

@ -27,3 +27,4 @@ stage
shell.nix
# vim temporary files
**/.*.sw*
.swp

6
.gitmodules vendored
View File

@ -61,3 +61,9 @@
[submodule "sublime/syntaxes/vue-syntax-highlight"]
path = sublime/syntaxes/vue-syntax-highlight
url = https://github.com/vuejs/vue-syntax-highlight.git
[submodule "sublime/syntaxes/sublime-glsl"]
path = sublime/syntaxes/sublime-glsl
url = https://github.com/euler0/sublime-glsl.git
[submodule "sublime/syntaxes/GDScript-sublime"]
path = sublime/syntaxes/GDScript-sublime
url = https://github.com/beefsack/GDScript-sublime.git

View File

@ -1,5 +1,27 @@
# Changelog
## 0.12.0 (2020-09-04)
### Breaking
- All paths like `current_path`, `page.path`, `section.path` (except colocated assets) now have a leading `/`
- Search index generation for Chinese and Japanese has been disabled by default as it leads to a big increase in
binary size
### Other
- Add 2 syntax highlighting themes: `green` and `railsbase16-green-screen-dark`
- Enable task lists in Markdown
- Add support for SVG in `get_image_metadata`
- Fix parsing of dates in arrays in `extra`
- Add a `--force` argument to `zola init` to allow creating a Zola site in a non-empty directory
- Make themes more flexible: `include` can now be used
- Make search index generation configurable, see docs for examples
- Fix Sass trying to load folders starting with `_`, causing issues with frameworks
- Update livereload.js version
- Add Markdown-outputting shortcodes
- Taxonomies with the same name but different casing are now merged, eg Author and author
## 0.11.0 (2020-05-25)
### Breaking
@ -8,6 +30,7 @@
- Config value `rss_limit` is renamed to `feed_limit`
- Config value `languages.*.rss` is renamed to `languages.*.feed`
- Config value `generate_rss` is renamed to `generate_feed`
- Taxonomy value `rss` is renamed to `feed`
Users with existing feeds should either set `feed_filename = "rss.xml"` in config.toml to keep things the same, or set up a 3xx redirect from rss.xml to atom.xml so that existing feed consumers arent broken.
@ -23,7 +46,6 @@
- Pass missing `lang` template parameter to taxonomy list template
- Fix default index section not having its path set to '/'
- Change cachebust strategy to use SHA256 instead of timestamp
- Fix
## 0.10.1 (2020-03-12)
@ -48,7 +70,7 @@ accessible everywhere
- Check for path collisions when building the site
- Fix bug in template extension with themes
- Use Rustls instead of openssl
- The continue reading HTML element is now a <span> instead of a <p>
- The continue reading HTML element is now a `<span>` instead of a `<p>`
- Update livereload.js
- Add --root global argument

View File

@ -56,11 +56,23 @@ $ cargo run --example generate_sublime synpack ../../sublime/syntaxes ../../subl
### Adding a theme
A gallery containing lots of themes is located at https://tmtheme-editor.herokuapp.com/#!/editor/theme/Agola%20Dark.
More themes can be easily added to Zola, just make a PR with the wanted theme added in the `sublime_themes` directory
and run the following command from the root of the components/config:
More themes can be easily added to Zola, just make a PR with the wanted theme added in the `sublime_themes` directory.
If you want to test Zola with a new theme, it needs to be built into the syntect file `all.themedump`.
First build the tool to generate the syntect file:
```bash
$ cargo run --example generate_sublime themepack ../../sublime/themes ../../sublime/themes/all.themedump
$ git clone https://github.com/getzola/zola.git && cd zola/components/config
$ cargo build --example generate_sublime
```
copy your theme in `sublime/themes/`, then regenerate the syntect file:
``` bash
$ ./target/debug/examples/generate_sublime themepack sublime/themes/ sublime/themes/all.themedump
```
You should see the list of themes being added.
To test your new theme, rebuild Zola with `cargo build`.

1089
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "zola"
version = "0.11.0"
version = "0.12.0"
authors = ["Vincent Prouillet <hello@vincentprouillet.com>"]
edition = "2018"
license = "MIT"
@ -9,7 +9,8 @@ description = "A fast static site generator with everything built-in"
homepage = "https://www.getzola.org"
repository = "https://github.com/getzola/zola"
keywords = ["static", "site", "generator", "blog"]
# build = "build.rs"
include = ["src/**/*", "LICENSE", "README.md"]
[build-dependencies]
clap = "2"
@ -19,9 +20,9 @@ name = "zola"
[dependencies]
atty = "0.2.11"
clap = "2"
clap = { version = "2", default-features = false }
chrono = "0.4"
lazy_static = "1.1.0"
lazy_static = "1.1"
termcolor = "1.0.4"
# Used in init to ensure the url given as base_url is a valid one
url = "2"
@ -39,14 +40,12 @@ site = { path = "components/site" }
errors = { path = "components/errors" }
front_matter = { path = "components/front_matter" }
utils = { path = "components/utils" }
rebuild = { path = "components/rebuild" }
[workspace]
members = [
"components/config",
"components/errors",
"components/front_matter",
"components/rebuild",
"components/rendering",
"components/site",
"components/templates",

View File

@ -21,7 +21,7 @@ stages:
rustup_toolchain: stable
linux-pinned:
imageName: 'ubuntu-16.04'
rustup_toolchain: 1.41.0
rustup_toolchain: 1.43.0
pool:
vmImage: $(imageName)
steps:
@ -145,4 +145,4 @@ stages:
assets: '$(Build.ArtifactStagingDirectory)/zola-$(Build.SourceBranchName)-$(TARGET).zip'
title: '$(Build.SourceBranchName)'
assetUploadMode: 'replace'
addChangeLog: true
addChangeLog: true

View File

@ -3,6 +3,7 @@ name = "config"
version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
edition = "2018"
include = ["src/**/*"]
[dependencies]
toml = "0.5"

View File

@ -28,7 +28,7 @@ fn main() {
builder.add_plain_text_syntax();
match builder.add_from_folder(package_dir, true) {
Ok(_) => (),
Err(e) => println!("Loading error: {:?}", e)
Err(e) => println!("Loading error: {:?}", e),
};
let ss = builder.build();
dump_to_file(&ss, packpath_newlines).unwrap();

View File

@ -0,0 +1,16 @@
use std::collections::HashMap;
use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Language {
/// The language code
pub code: String,
/// Whether to generate a feed for that language, defaults to `false`
pub feed: bool,
/// Whether to generate search index for that language, defaults to `false`
pub search: bool,
}
pub type TranslateTerm = HashMap<String, String>;

View File

@ -0,0 +1,16 @@
use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct LinkChecker {
/// Skip link checking for these URL prefixes
pub skip_prefixes: Vec<String>,
/// Skip anchor checking for these URL prefixes
pub skip_anchor_prefixes: Vec<String>,
}
impl Default for LinkChecker {
fn default() -> LinkChecker {
LinkChecker { skip_prefixes: Vec::new(), skip_anchor_prefixes: Vec::new() }
}
}

View File

@ -1,18 +1,21 @@
pub mod languages;
pub mod link_checker;
pub mod search;
pub mod slugify;
pub mod taxonomies;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use chrono::Utc;
use globset::{Glob, GlobSet, GlobSetBuilder};
use serde_derive::{Deserialize, Serialize};
use syntect::parsing::{SyntaxSet, SyntaxSetBuilder};
use toml;
use toml::Value as Toml;
use crate::highlighting::THEME_SET;
use crate::theme::Theme;
use errors::{bail, Error, Result};
use utils::fs::read_file_with_error;
use utils::slugs::SlugifyStrategy;
// We want a default base url for tests
static DEFAULT_BASE_URL: &str = "http://a-website.com";
@ -24,104 +27,6 @@ pub enum Mode {
Check,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Slugify {
pub paths: SlugifyStrategy,
pub taxonomies: SlugifyStrategy,
pub anchors: SlugifyStrategy,
}
impl Default for Slugify {
fn default() -> Self {
Slugify {
paths: SlugifyStrategy::On,
taxonomies: SlugifyStrategy::On,
anchors: SlugifyStrategy::On,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Language {
/// The language code
pub code: String,
/// Whether to generate a feed for that language, defaults to `false`
pub feed: bool,
/// Whether to generate search index for that language, defaults to `false`
pub search: bool,
}
impl Default for Language {
fn default() -> Self {
Language { code: String::new(), feed: false, search: false }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Taxonomy {
/// The name used in the URL, usually the plural
pub name: String,
/// If this is set, the list of individual taxonomy term page will be paginated
/// by this much
pub paginate_by: Option<usize>,
pub paginate_path: Option<String>,
/// Whether to generate a feed only for each taxonomy term, defaults to false
pub feed: bool,
/// The language for that taxonomy, only used in multilingual sites.
/// Defaults to the config `default_language` if not set
pub lang: String,
}
impl Taxonomy {
pub fn is_paginated(&self) -> bool {
if let Some(paginate_by) = self.paginate_by {
paginate_by > 0
} else {
false
}
}
pub fn paginate_path(&self) -> &str {
if let Some(ref path) = self.paginate_path {
path
} else {
"page"
}
}
}
impl Default for Taxonomy {
fn default() -> Self {
Taxonomy {
name: String::new(),
paginate_by: None,
paginate_path: None,
feed: false,
lang: String::new(),
}
}
}
type TranslateTerm = HashMap<String, String>;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct LinkChecker {
/// Skip link checking for these URL prefixes
pub skip_prefixes: Vec<String>,
/// Skip anchor checking for these URL prefixes
pub skip_anchor_prefixes: Vec<String>,
}
impl Default for LinkChecker {
fn default() -> LinkChecker {
LinkChecker { skip_prefixes: Vec::new(), skip_anchor_prefixes: Vec::new() }
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
@ -138,7 +43,7 @@ pub struct Config {
/// The language used in the site. Defaults to "en"
pub default_language: String,
/// The list of supported languages outside of the default one
pub languages: Vec<Language>,
pub languages: Vec<languages::Language>,
/// Languages list and translated strings
///
@ -147,7 +52,7 @@ pub struct Config {
///
/// The attribute is intentionally not public, use `get_translation()` method for translating
/// key into different language.
translations: HashMap<String, TranslateTerm>,
translations: HashMap<String, languages::TranslateTerm>,
/// Whether to highlight all code blocks found in markdown files. Defaults to false
pub highlight_code: bool,
@ -165,10 +70,12 @@ pub struct Config {
/// 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<taxonomies::Taxonomy>,
/// Whether to compile the `sass` directory and output the css files into the static folder
pub compile_sass: bool,
/// Whether to minify the html output
pub minify_html: bool,
/// Whether to build the search index for the content
pub build_search_index: bool,
/// A list of file glob patterns to ignore when processing the content folder. Defaults to none.
@ -189,16 +96,16 @@ pub struct Config {
#[serde(skip_serializing, skip_deserializing)] // not a typo, 2 are need
pub extra_syntax_set: Option<SyntaxSet>,
pub link_checker: LinkChecker,
pub link_checker: link_checker::LinkChecker,
/// The setup for which slugification strategies to use for paths, taxonomies and anchors
pub slugify: Slugify,
pub slugify: slugify::Slugify,
/// The search config, telling what to include in the search index
pub search: search::Search,
/// All user params set in [extra] in the config
pub extra: HashMap<String, Toml>,
/// Set automatically when instantiating the config. Used for cachebusting
pub build_timestamp: Option<i64>,
}
impl Config {
@ -222,8 +129,6 @@ impl Config {
bail!("Default language `{}` should not appear both in `config.default_language` and `config.languages`", config.default_language)
}
config.build_timestamp = Some(Utc::now().timestamp());
if !config.ignored_content.is_empty() {
// Convert the file glob strings into a compiled glob set matcher. We want to do this once,
// at program initialization, rather than for every page, for example. We arrange for the
@ -248,6 +153,9 @@ impl Config {
}
}
// TODO: re-enable once it's a bit more tested
config.minify_html = false;
Ok(config)
}
@ -303,19 +211,14 @@ impl Config {
/// Merges the extra data from the theme with the config extra data
fn add_theme_extra(&mut self, theme: &Theme) -> Result<()> {
// 3 pass merging
// 1. save config to preserve user
let original = self.extra.clone();
// 2. inject theme extra values
for (key, val) in &theme.extra {
self.extra.entry(key.to_string()).or_insert_with(|| val.clone());
if !self.extra.contains_key(key) {
// The key is not overriden in site config, insert it
self.extra.insert(key.to_string(), val.clone());
continue;
}
merge(self.extra.get_mut(key).unwrap(), val)?;
}
// 3. overwrite with original config
for (key, val) in &original {
self.extra.entry(key.to_string()).or_insert_with(|| val.clone());
}
Ok(())
}
@ -377,6 +280,34 @@ impl Config {
}
}
// merge TOML data that can be a table, or anything else
pub fn merge(into: &mut Toml, from: &Toml) -> Result<()> {
match (from.is_table(), into.is_table()) {
(false, false) => {
// These are not tables so we have nothing to merge
Ok(())
}
(true, true) => {
// Recursively merge these tables
let into_table = into.as_table_mut().unwrap();
for (key, val) in from.as_table().unwrap() {
if !into_table.contains_key(key) {
// An entry was missing in the first table, insert it
into_table.insert(key.to_string(), val.clone());
continue;
}
// Two entries to compare, recurse
merge(into_table.get_mut(key).unwrap(), val)?;
}
Ok(())
}
_ => {
// Trying to merge a table with something else
Err(Error::msg(&format!("Cannot merge config.toml with theme.toml because the following values have incompatibles types:\n- {}\n - {}", into, from)))
}
}
}
impl Default for Config {
fn default() -> Config {
Config {
@ -394,6 +325,7 @@ impl Default for Config {
hard_link_static: false,
taxonomies: Vec::new(),
compile_sass: false,
minify_html: false,
mode: Mode::Build,
build_search_index: false,
ignored_content: Vec::new(),
@ -401,17 +333,18 @@ impl Default for Config {
translations: HashMap::new(),
extra_syntaxes: Vec::new(),
extra_syntax_set: None,
link_checker: LinkChecker::default(),
slugify: Slugify::default(),
link_checker: link_checker::LinkChecker::default(),
slugify: slugify::Slugify::default(),
search: search::Search::default(),
extra: HashMap::new(),
build_timestamp: Some(1),
}
}
}
#[cfg(test)]
mod tests {
use super::{Config, SlugifyStrategy, Theme};
use super::*;
use utils::slugs::SlugifyStrategy;
#[test]
fn can_import_valid_config() {
@ -512,18 +445,39 @@ base_url = "https://replace-this-with-your-url.com"
[extra]
hello = "world"
[extra.sub]
foo = "bar"
[extra.sub.sub]
foo = "bar"
"#;
let mut config = Config::parse(config_str).unwrap();
let theme_str = r#"
[extra]
hello = "foo"
a_value = 10
[extra.sub]
foo = "default"
truc = "default"
[extra.sub.sub]
foo = "default"
truc = "default"
"#;
let theme = Theme::parse(theme_str).unwrap();
assert!(config.add_theme_extra(&theme).is_ok());
let extra = config.extra;
assert_eq!(extra["hello"].as_str().unwrap(), "world".to_string());
assert_eq!(extra["a_value"].as_integer().unwrap(), 10);
assert_eq!(extra["sub"]["foo"].as_str().unwrap(), "bar".to_string());
assert_eq!(extra["sub"].get("truc").expect("The whole extra.sub table was overriden by theme data, discarding extra.sub.truc").as_str().unwrap(), "default".to_string());
assert_eq!(extra["sub"]["sub"]["foo"].as_str().unwrap(), "bar".to_string());
assert_eq!(
extra["sub"]["sub"]
.get("truc")
.expect("Failed to merge subsubtable extra.sub.sub")
.as_str()
.unwrap(),
"default".to_string()
);
}
const CONFIG_TRANSLATION: &str = r#"
@ -681,4 +635,23 @@ languages = [
let err = config.unwrap_err();
assert_eq!("Default language `fr` should not appear both in `config.default_language` and `config.languages`", format!("{}", err));
}
#[test]
fn cannot_overwrite_theme_mapping_with_invalid_type() {
let config_str = r#"
base_url = "http://localhost:1312"
default_language = "fr"
[extra]
foo = "bar"
"#;
let mut config = Config::parse(config_str).unwrap();
let theme_str = r#"
[extra]
[extra.foo]
bar = "baz"
"#;
let theme = Theme::parse(theme_str).unwrap();
// We expect an error here
assert_eq!(false, config.add_theme_extra(&theme).is_ok());
}
}

View File

@ -0,0 +1,27 @@
use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Search {
/// Include the title of the page in the search index. `true` by default.
pub include_title: bool,
/// Includes the whole content in the search index. Ok for small sites but becomes
/// too big on large sites. `true` by default.
pub include_content: bool,
/// Optionally truncate the content down to `n` chars. This might cut content in a word
pub truncate_content_length: Option<usize>,
/// Includes the description in the search index. When the site becomes too large, you can switch
/// to that instead. `false` by default
pub include_description: bool,
}
impl Default for Search {
fn default() -> Self {
Search {
include_title: true,
include_content: true,
include_description: false,
truncate_content_length: None,
}
}
}

View File

@ -0,0 +1,11 @@
use serde_derive::{Deserialize, Serialize};
use utils::slugs::SlugifyStrategy;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Slugify {
pub paths: SlugifyStrategy,
pub taxonomies: SlugifyStrategy,
pub anchors: SlugifyStrategy,
}

View File

@ -0,0 +1,35 @@
use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Taxonomy {
/// The name used in the URL, usually the plural
pub name: String,
/// If this is set, the list of individual taxonomy term page will be paginated
/// by this much
pub paginate_by: Option<usize>,
pub paginate_path: Option<String>,
/// Whether to generate a feed only for each taxonomy term, defaults to false
pub feed: bool,
/// The language for that taxonomy, only used in multilingual sites.
/// Defaults to the config `default_language` if not set
pub lang: String,
}
impl Taxonomy {
pub fn is_paginated(&self) -> bool {
if let Some(paginate_by) = self.paginate_by {
paginate_by > 0
} else {
false
}
}
pub fn paginate_path(&self) -> &str {
if let Some(ref path) = self.paginate_path {
path
} else {
"page"
}
}
}

View File

@ -1,7 +1,9 @@
mod config;
pub mod highlighting;
mod theme;
pub use crate::config::{Config, Language, LinkChecker, Taxonomy};
pub use crate::config::{
languages::Language, link_checker::LinkChecker, slugify::Slugify, taxonomies::Taxonomy, Config,
};
use std::path::Path;

View File

@ -8,4 +8,4 @@ edition = "2018"
tera = "1"
toml = "0.5"
image = "0.23"
syntect = "4.1"
syntect = "4.4"

View File

@ -17,21 +17,18 @@ pub enum ErrorKind {
pub struct Error {
/// Kind of error
pub kind: ErrorKind,
pub source: Option<Box<dyn StdError>>,
pub source: Option<Box<dyn StdError + Send + Sync>>,
}
unsafe impl Sync for Error {}
unsafe impl Send for Error {}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
let mut source = self.source.as_ref().map(|c| &**c);
if source.is_none() {
if let ErrorKind::Tera(ref err) = self.kind {
source = err.source();
}
match self.source {
Some(ref err) => Some(&**err),
None => match self.kind {
ErrorKind::Tera(ref err) => err.source(),
_ => None,
},
}
source
}
}
@ -55,7 +52,7 @@ impl Error {
}
/// Creates generic error with a cause
pub fn chain(value: impl ToString, source: impl Into<Box<dyn StdError>>) -> Self {
pub fn chain(value: impl ToString, source: impl Into<Box<dyn StdError + Send + Sync>>) -> Self {
Self { kind: ErrorKind::Msg(value.to_string()), source: Some(source.into()) }
}

View File

@ -13,7 +13,7 @@ pub use section::SectionFrontMatter;
lazy_static! {
static ref PAGE_RE: Regex =
Regex::new(r"^[[:space:]]*\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap();
Regex::new(r"^[[:space:]]*\+\+\+(\r?\n(?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap();
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
@ -37,7 +37,7 @@ pub enum InsertAnchor {
/// Split a file between the front matter and its content
/// Will return an error if the front matter wasn't found
fn split_content(file_path: &Path, content: &str) -> Result<(String, String)> {
fn split_content<'c>(file_path: &Path, content: &'c str) -> Result<(&'c str, &'c str)> {
if !PAGE_RE.is_match(content) {
bail!(
"Couldn't find front matter in `{}`. Did you forget to add `+++`?",
@ -50,15 +50,15 @@ fn split_content(file_path: &Path, content: &str) -> Result<(String, String)> {
// caps[0] is the full match
// caps[1] => front matter
// caps[2] => content
Ok((caps[1].to_string(), caps[2].to_string()))
Ok((caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()))
}
/// Split a file between the front matter and its content.
/// Returns a parsed `SectionFrontMatter` and the rest of the content
pub fn split_section_content(
pub fn split_section_content<'c>(
file_path: &Path,
content: &str,
) -> Result<(SectionFrontMatter, String)> {
content: &'c str,
) -> Result<(SectionFrontMatter, &'c str)> {
let (front_matter, content) = split_content(file_path, content)?;
let meta = SectionFrontMatter::parse(&front_matter).map_err(|e| {
Error::chain(
@ -71,7 +71,10 @@ pub fn split_section_content(
/// Split a file between the front matter and its content
/// Returns a parsed `PageFrontMatter` and the rest of the content
pub fn split_page_content(file_path: &Path, content: &str) -> Result<(PageFrontMatter, String)> {
pub fn split_page_content<'c>(
file_path: &Path,
content: &'c str,
) -> Result<(PageFrontMatter, &'c str)> {
let (front_matter, content) = split_content(file_path, content)?;
let meta = PageFrontMatter::parse(&front_matter).map_err(|e| {
Error::chain(

View File

@ -3,7 +3,6 @@ use std::collections::HashMap;
use chrono::prelude::*;
use serde_derive::Deserialize;
use tera::{Map, Value};
use toml;
use errors::{bail, Result};
use utils::de::{fix_toml_dates, from_toml_datetime};
@ -38,8 +37,6 @@ pub struct PageFrontMatter {
/// Can't be an empty string if present
pub path: Option<String>,
pub taxonomies: HashMap<String, Vec<String>>,
/// Integer to use to order content. Lowest is at the bottom, highest first
pub order: Option<usize>,
/// Integer to use to order content. Highest is at the bottom, lowest first
pub weight: Option<usize>,
/// All aliases for that page. Zola will create HTML templates that will
@ -57,6 +54,20 @@ pub struct PageFrontMatter {
pub extra: Map<String, Value>,
}
/// Parse a string for a datetime coming from one of the supported TOML format
/// There are three alternatives:
/// 1. an offset datetime (plain RFC3339)
/// 2. a local datetime (RFC3339 with timezone omitted)
/// 3. a local date (YYYY-MM-DD).
/// This tries each in order.
fn parse_datetime(d: &str) -> Option<NaiveDateTime> {
DateTime::parse_from_rfc3339(d)
.or_else(|_| DateTime::parse_from_rfc3339(format!("{}Z", d).as_ref()))
.map(|s| s.naive_local())
.or_else(|_| NaiveDate::parse_from_str(d, "%Y-%m-%d").map(|s| s.and_hms(0, 0, 0)))
.ok()
}
impl PageFrontMatter {
pub fn parse(toml: &str) -> Result<PageFrontMatter> {
let mut f: PageFrontMatter = match toml::from_str(toml) {
@ -83,31 +94,20 @@ impl PageFrontMatter {
f.date_to_datetime();
if let Some(ref date) = f.date {
if f.datetime.is_none() {
bail!("`date` could not be parsed: {}.", date);
}
}
Ok(f)
}
/// Converts the TOML datetime to a Chrono naive datetime
/// Also grabs the year/month/day tuple that will be used in serialization
pub fn date_to_datetime(&mut self) {
self.datetime = if let Some(ref d) = self.date {
if d.contains('T') {
DateTime::parse_from_rfc3339(&d).ok().map(|s| s.naive_local())
} else {
NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok().map(|s| s.and_hms(0, 0, 0))
}
} else {
None
};
self.datetime_tuple = if let Some(ref dt) = self.datetime {
Some((dt.year(), dt.month(), dt.day()))
} else {
None
};
}
pub fn order(&self) -> usize {
self.order.unwrap()
self.datetime = self.date.as_ref().map(|s| s.as_ref()).and_then(parse_datetime);
self.datetime_tuple = self.datetime.map(|dt| (dt.year(), dt.month(), dt.day()));
}
pub fn weight(&self) -> usize {
@ -128,7 +128,6 @@ impl Default for PageFrontMatter {
slug: None,
path: None,
taxonomies: HashMap::new(),
order: None,
weight: None,
aliases: Vec::new(),
in_search_index: true,
@ -198,7 +197,7 @@ mod tests {
date = 2016-10-10
"#;
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.date.is_some());
assert!(res.datetime.is_some());
}
#[test]
@ -209,7 +208,51 @@ mod tests {
date = 2002-10-02T15:00:00Z
"#;
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.date.is_some());
assert!(res.datetime.is_some());
}
#[test]
fn can_parse_date_rfc3339_without_timezone() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002-10-02T15:00:00
"#;
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.datetime.is_some());
}
#[test]
fn can_parse_date_rfc3339_with_space() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002-10-02 15:00:00+02:00
"#;
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.datetime.is_some());
}
#[test]
fn can_parse_date_rfc3339_with_space_without_timezone() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002-10-02 15:00:00
"#;
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.datetime.is_some());
}
#[test]
fn can_parse_date_rfc3339_with_microseconds() {
let content = r#"
title = "Hello"
description = "hey there"
date = 2002-10-02T15:00:00.123456Z
"#;
let res = PageFrontMatter::parse(content).unwrap();
assert!(res.datetime.is_some());
}
#[test]
@ -270,6 +313,23 @@ mod tests {
assert_eq!(res.unwrap().extra["something"]["some-date"], to_value("2002-14-01").unwrap());
}
#[test]
fn can_parse_fully_nested_dates_in_extra() {
let content = r#"
title = "Hello"
description = "hey there"
[extra]
date_example = 2020-05-04
[[extra.questions]]
date = 2020-05-03
name = "Who is the prime minister of Uganda?""#;
let res = PageFrontMatter::parse(content);
println!("{:?}", res);
assert!(res.is_ok());
assert_eq!(res.unwrap().extra["questions"][0]["date"], to_value("2020-05-03").unwrap());
}
#[test]
fn can_parse_taxonomies() {
let content = r#"

View File

@ -1,6 +1,5 @@
use serde_derive::{Deserialize, Serialize};
use tera::{Map, Value};
use toml;
use super::{InsertAnchor, SortBy};
use errors::{bail, Result};
@ -29,6 +28,9 @@ pub struct SectionFrontMatter {
/// How many pages to be displayed per paginated page. No pagination will happen if this isn't set
#[serde(skip_serializing)]
pub paginate_by: Option<usize>,
/// Whether to reverse the order of the pages before segmenting into pagers
#[serde(skip_serializing)]
pub paginate_reversed: bool,
/// Path to be used by pagination: the page number will be appended after it. Defaults to `page`.
#[serde(skip_serializing)]
pub paginate_path: String,
@ -61,6 +63,9 @@ pub struct SectionFrontMatter {
/// redirect to this
#[serde(skip_serializing)]
pub aliases: Vec<String>,
/// Whether to generate a feed for the current section
#[serde(skip_serializing)]
pub generate_feed: bool,
/// Any extra parameter present in the front matter
pub extra: Map<String, Value>,
}
@ -98,6 +103,7 @@ impl Default for SectionFrontMatter {
weight: 0,
template: None,
paginate_by: None,
paginate_reversed: false,
paginate_path: DEFAULT_PAGINATE_PATH.to_string(),
render: true,
redirect_to: None,
@ -106,6 +112,7 @@ impl Default for SectionFrontMatter {
transparent: false,
page_template: None,
aliases: Vec::new(),
generate_feed: false,
extra: Map::new(),
}
}

View File

@ -27,7 +27,7 @@ pub fn find_content_components<P: AsRef<Path>>(path: P) -> Vec<String> {
}
/// Struct that contains all the information about the actual file
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct FileInfo {
/// The full path to the .md file
pub path: PathBuf,
@ -143,22 +143,6 @@ impl FileInfo {
}
}
#[doc(hidden)]
impl Default for FileInfo {
fn default() -> FileInfo {
FileInfo {
path: PathBuf::new(),
parent: PathBuf::new(),
grand_parent: None,
filename: String::new(),
name: String::new(),
components: vec![],
relative: String::new(),
canonical: PathBuf::new(),
}
}
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};

View File

@ -29,7 +29,7 @@ lazy_static! {
).unwrap();
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Page {
/// All info about the actual file
pub file: FileInfo,
@ -48,7 +48,7 @@ pub struct Page {
/// The slug of that page.
/// First tries to find the slug in the meta and defaults to filename otherwise
pub slug: String,
/// The URL path of the page
/// The URL path of the page, always starting with a slash
pub path: String,
/// The components of the path of the page
pub components: Vec<String>,
@ -91,31 +91,7 @@ impl Page {
pub fn new<P: AsRef<Path>>(file_path: P, meta: PageFrontMatter, base_path: &PathBuf) -> Page {
let file_path = file_path.as_ref();
Page {
file: FileInfo::new_page(file_path, base_path),
meta,
ancestors: vec![],
raw_content: "".to_string(),
assets: vec![],
serialized_assets: vec![],
content: "".to_string(),
slug: "".to_string(),
path: "".to_string(),
components: vec![],
permalink: "".to_string(),
summary: None,
earlier: None,
later: None,
lighter: None,
heavier: None,
toc: vec![],
word_count: None,
reading_time: None,
lang: String::new(),
translations: Vec::new(),
internal_links_with_anchors: Vec::new(),
external_links: Vec::new(),
}
Page { file: FileInfo::new_page(file_path, base_path), meta, ..Self::default() }
}
pub fn is_draft(&self) -> bool {
@ -136,7 +112,7 @@ impl Page {
page.lang = page.file.find_language(config)?;
page.raw_content = content;
page.raw_content = content.to_string();
let (word_count, reading_time) = get_reading_analytics(&page.raw_content);
page.word_count = Some(word_count);
page.reading_time = Some(reading_time);
@ -182,8 +158,14 @@ impl Page {
}
};
if let Some(ref p) = page.meta.path {
page.path = p.trim().trim_start_matches('/').to_string();
page.path = if let Some(ref p) = page.meta.path {
let path = p.trim();
if path.starts_with('/') {
path.into()
} else {
format!("/{}", path)
}
} else {
let mut path = if page.file.components.is_empty() {
page.slug.clone()
@ -195,8 +177,8 @@ impl Page {
path = format!("{}/{}", page.lang, path);
}
page.path = path;
}
format!("/{}", path)
};
if !page.path.ends_with('/') {
page.path = format!("{}/", page.path);
@ -238,7 +220,7 @@ impl Page {
page.assets = assets
.into_iter()
.filter(|path| match path.file_name() {
None => true,
None => false,
Some(file) => !globset.is_match(file),
})
.collect();
@ -335,36 +317,6 @@ impl Page {
}
}
impl Default for Page {
fn default() -> Page {
Page {
file: FileInfo::default(),
meta: PageFrontMatter::default(),
ancestors: vec![],
raw_content: "".to_string(),
assets: vec![],
serialized_assets: vec![],
content: "".to_string(),
slug: "".to_string(),
path: "".to_string(),
components: vec![],
permalink: "".to_string(),
summary: None,
earlier: None,
later: None,
lighter: None,
heavier: None,
toc: vec![],
word_count: None,
reading_time: None,
lang: String::new(),
translations: Vec::new(),
internal_links_with_anchors: Vec::new(),
external_links: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
@ -420,7 +372,7 @@ Hello world"#;
Page::parse(Path::new("content/posts/intro/start.md"), content, &conf, &PathBuf::new());
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.path, "posts/intro/hello-world/");
assert_eq!(page.path, "/posts/intro/hello-world/");
assert_eq!(page.components, vec!["posts", "intro", "hello-world"]);
assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world/");
}
@ -436,7 +388,7 @@ Hello world"#;
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.path, "/hello-world/");
assert_eq!(page.components, vec!["hello-world"]);
assert_eq!(page.permalink, config.make_permalink("hello-world"));
}
@ -453,7 +405,7 @@ Hello world"#;
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.path, "/hello-world/");
assert_eq!(page.components, vec!["hello-world"]);
assert_eq!(page.permalink, config.make_permalink("hello-world"));
}
@ -470,7 +422,7 @@ Hello world"#;
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.path, "/日本/");
assert_eq!(page.components, vec!["日本"]);
assert_eq!(page.permalink, config.make_permalink("日本"));
}
@ -491,7 +443,7 @@ Hello world"#;
);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.path, "hello-world/");
assert_eq!(page.path, "/hello-world/");
assert_eq!(page.components, vec!["hello-world"]);
assert_eq!(page.permalink, config.make_permalink("hello-world"));
}
@ -512,7 +464,7 @@ Hello world"#;
);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.path, "hello-world/");
assert_eq!(page.path, "/hello-world/");
assert_eq!(page.permalink, config.make_permalink("hello-world"));
}

View File

@ -17,13 +17,14 @@ use crate::content::has_anchor;
use crate::content::ser::SerializingSection;
use crate::library::Library;
#[derive(Clone, Debug, PartialEq)]
// Default is used to create a default index section if there is no _index.md in the root content directory
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Section {
/// All info about the actual file
pub file: FileInfo,
/// The front matter meta-data
pub meta: SectionFrontMatter,
/// The URL path of the page
/// The URL path of the page, always starting with a slash
pub path: String,
/// The components for the path of that page
pub components: Vec<String>,
@ -74,28 +75,7 @@ impl Section {
) -> Section {
let file_path = file_path.as_ref();
Section {
file: FileInfo::new_section(file_path, base_path),
meta,
ancestors: vec![],
path: "".to_string(),
components: vec![],
permalink: "".to_string(),
raw_content: "".to_string(),
assets: vec![],
serialized_assets: vec![],
content: "".to_string(),
pages: vec![],
ignored_pages: vec![],
subsections: vec![],
toc: vec![],
word_count: None,
reading_time: None,
lang: String::new(),
translations: Vec::new(),
internal_links_with_anchors: Vec::new(),
external_links: Vec::new(),
}
Section { file: FileInfo::new_section(file_path, base_path), meta, ..Self::default() }
}
pub fn parse(
@ -107,20 +87,22 @@ impl Section {
let (meta, content) = split_section_content(file_path, content)?;
let mut section = Section::new(file_path, meta, base_path);
section.lang = section.file.find_language(config)?;
section.raw_content = content;
section.raw_content = content.to_string();
let (word_count, reading_time) = get_reading_analytics(&section.raw_content);
section.word_count = Some(word_count);
section.reading_time = Some(reading_time);
let path = section.file.components.join("/");
if section.lang != config.default_language {
if path.is_empty() {
section.path = format!("{}/", section.lang);
} else {
section.path = format!("{}/{}/", section.lang, path);
}
let lang_path = if section.lang != config.default_language {
format!("/{}", section.lang)
} else {
section.path = format!("{}/", path);
}
"".into()
};
section.path = if path.is_empty() {
format!("{}/", lang_path)
} else {
format!("{}/{}/", lang_path, path)
};
section.components = section
.path
@ -156,7 +138,7 @@ impl Section {
section.assets = assets
.into_iter()
.filter(|path| match path.file_name() {
None => true,
None => false,
Some(file) => !globset.is_match(file),
})
.collect();
@ -252,32 +234,14 @@ impl Section {
pub fn to_serialized_basic<'a>(&'a self, library: &'a Library) -> SerializingSection<'a> {
SerializingSection::from_section_basic(self, Some(library))
}
}
/// Used to create a default index section if there is no _index.md in the root content directory
impl Default for Section {
fn default() -> Section {
Section {
file: FileInfo::default(),
meta: SectionFrontMatter::default(),
ancestors: vec![],
path: "".to_string(),
components: vec![],
permalink: "".to_string(),
raw_content: "".to_string(),
assets: vec![],
serialized_assets: vec![],
content: "".to_string(),
pages: vec![],
ignored_pages: vec![],
subsections: vec![],
toc: vec![],
reading_time: None,
word_count: None,
lang: String::new(),
translations: Vec::new(),
internal_links_with_anchors: Vec::new(),
external_links: Vec::new(),
pub fn paginate_by(&self) -> Option<usize> {
match self.meta.paginate_by {
None => None,
Some(x) => match x {
0 => None,
_ => Some(x),
},
}
}
}

View File

@ -150,6 +150,11 @@ impl<'a> SerializingPage<'a> {
}
}
/// currently only used in testing
pub fn get_title(&'a self) -> &'a Option<String> {
&self.title
}
/// Same as from_page but does not fill sibling pages
pub fn from_page_basic(page: &'a Page, library: Option<&'a Library>) -> Self {
let mut year = None;

View File

@ -12,6 +12,8 @@ use crate::content::{Section, SerializingPage, SerializingSection};
use crate::library::Library;
use crate::taxonomies::{Taxonomy, TaxonomyItem};
use std::borrow::Cow;
#[derive(Clone, Debug, PartialEq)]
enum PaginationRoot<'a> {
Section(&'a Section),
@ -45,11 +47,13 @@ impl<'a> Pager<'a> {
#[derive(Clone, Debug, PartialEq)]
pub struct Paginator<'a> {
/// All pages in the section/taxonomy
all_pages: &'a [DefaultKey],
all_pages: Cow<'a, [DefaultKey]>,
/// Pages split in chunks of `paginate_by`
pub pagers: Vec<Pager<'a>>,
/// How many content pages on a paginated page at max
paginate_by: usize,
/// whether to reverse before grouping
paginate_reversed: bool,
/// The thing we are creating the paginator for: section or taxonomy
root: PaginationRoot<'a>,
// Those below can be obtained from the root but it would make the code more complex than needed
@ -66,10 +70,12 @@ impl<'a> Paginator<'a> {
/// It will always at least create one pager (the first) even if there are not enough pages to paginate
pub fn from_section(section: &'a Section, library: &'a Library) -> Paginator<'a> {
let paginate_by = section.meta.paginate_by.unwrap();
let paginate_reversed = section.meta.paginate_reversed;
let mut paginator = Paginator {
all_pages: &section.pages,
all_pages: Cow::from(&section.pages[..]),
pagers: Vec::with_capacity(section.pages.len() / paginate_by),
paginate_by,
paginate_reversed,
root: PaginationRoot::Section(section),
permalink: section.permalink.clone(),
path: section.path.clone(),
@ -91,12 +97,13 @@ impl<'a> Paginator<'a> {
) -> Paginator<'a> {
let paginate_by = taxonomy.kind.paginate_by.unwrap();
let mut paginator = Paginator {
all_pages: &item.pages,
all_pages: Cow::Borrowed(&item.pages),
pagers: Vec::with_capacity(item.pages.len() / paginate_by),
paginate_by,
paginate_reversed: false,
root: PaginationRoot::Taxonomy(taxonomy, item),
permalink: item.permalink.clone(),
path: format!("{}/{}", taxonomy.kind.name, item.slug),
path: format!("/{}/{}/", taxonomy.kind.name, item.slug),
paginate_path: taxonomy
.kind
.paginate_path
@ -106,6 +113,7 @@ impl<'a> Paginator<'a> {
template: format!("{}/single.html", taxonomy.kind.name),
};
// taxonomy paginators have no sorting so we won't have to reverse
paginator.fill_pagers(library);
paginator
}
@ -116,8 +124,12 @@ impl<'a> Paginator<'a> {
// the pages in the current pagers
let mut current_page = vec![];
for key in self.all_pages {
let page = library.get_page_by_key(*key);
if self.paginate_reversed {
self.all_pages.to_mut().reverse();
}
for key in self.all_pages.to_mut().iter_mut() {
let page = library.get_page_by_key(key.clone());
current_page.push(page.to_serialized_basic(library));
if current_page.len() == self.paginate_by {
@ -146,7 +158,7 @@ impl<'a> Paginator<'a> {
let permalink = format!("{}{}", self.permalink, page_path);
let pager_path = if self.is_index {
page_path
format!("/{}", page_path)
} else if self.path.ends_with('/') {
format!("{}{}", self.path, page_path)
} else {
@ -246,30 +258,39 @@ mod tests {
use super::Paginator;
fn create_section(is_index: bool) -> Section {
fn create_section(is_index: bool, paginate_reversed: bool) -> Section {
let mut f = SectionFrontMatter::default();
f.paginate_by = Some(2);
f.paginate_path = "page".to_string();
f.paginate_reversed = paginate_reversed;
let mut s = Section::new("content/_index.md", f, &PathBuf::new());
if !is_index {
s.path = "posts/".to_string();
s.path = "/posts/".to_string();
s.permalink = "https://vincent.is/posts/".to_string();
s.file.components = vec!["posts".to_string()];
} else {
s.path = "/".into();
s.permalink = "https://vincent.is/".to_string();
}
s
}
fn create_library(is_index: bool) -> (Section, Library) {
let mut library = Library::new(3, 0, false);
library.insert_page(Page::default());
library.insert_page(Page::default());
library.insert_page(Page::default());
fn create_library(
is_index: bool,
num_pages: usize,
paginate_reversed: bool,
) -> (Section, Library) {
let mut library = Library::new(num_pages, 0, false);
for i in 1..=num_pages {
let mut page = Page::default();
page.meta.title = Some(i.to_string());
library.insert_page(page);
}
let mut draft = Page::default();
draft.meta.draft = true;
library.insert_page(draft);
let mut section = create_section(is_index);
let mut section = create_section(is_index, paginate_reversed);
section.pages = library.pages().keys().collect();
library.insert_section(section.clone());
@ -278,41 +299,88 @@ mod tests {
#[test]
fn test_can_create_paginator() {
let (section, library) = create_library(false);
let (section, library) = create_library(false, 3, false);
let paginator = Paginator::from_section(&section, &library);
assert_eq!(paginator.pagers.len(), 2);
assert_eq!(paginator.pagers[0].index, 1);
assert_eq!(paginator.pagers[0].pages.len(), 2);
assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts/");
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].pages.len(), 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/");
}
#[test]
fn test_can_create_reversed_paginator() {
// 6 pages, 5 normal and 1 draft
let (section, library) = create_library(false, 5, true);
let paginator = Paginator::from_section(&section, &library);
assert_eq!(paginator.pagers.len(), 3);
assert_eq!(paginator.pagers[0].index, 1);
assert_eq!(paginator.pagers[0].pages.len(), 2);
assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts/");
assert_eq!(paginator.pagers[0].path, "/posts/");
assert_eq!(
vec!["".to_string(), "5".to_string()],
paginator.pagers[0]
.pages
.iter()
.map(|p| p.get_title().as_ref().unwrap_or(&"".to_string()).to_string())
.collect::<Vec<String>>()
);
assert_eq!(paginator.pagers[1].index, 2);
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].path, "/posts/page/2/");
assert_eq!(
vec!["4".to_string(), "3".to_string()],
paginator.pagers[1]
.pages
.iter()
.map(|p| p.get_title().as_ref().unwrap_or(&"".to_string()).to_string())
.collect::<Vec<String>>()
);
assert_eq!(paginator.pagers[2].index, 3);
assert_eq!(paginator.pagers[2].pages.len(), 2);
assert_eq!(paginator.pagers[2].permalink, "https://vincent.is/posts/page/3/");
assert_eq!(paginator.pagers[2].path, "/posts/page/3/");
assert_eq!(
vec!["2".to_string(), "1".to_string()],
paginator.pagers[2]
.pages
.iter()
.map(|p| p.get_title().as_ref().unwrap_or(&"".to_string()).to_string())
.collect::<Vec<String>>()
);
}
#[test]
fn test_can_create_paginator_for_index() {
let (section, library) = create_library(true);
let (section, library) = create_library(true, 3, false);
let paginator = Paginator::from_section(&section, &library);
assert_eq!(paginator.pagers.len(), 2);
assert_eq!(paginator.pagers[0].index, 1);
assert_eq!(paginator.pagers[0].pages.len(), 2);
assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/");
assert_eq!(paginator.pagers[0].path, "");
assert_eq!(paginator.pagers[0].path, "/");
assert_eq!(paginator.pagers[1].index, 2);
assert_eq!(paginator.pagers[1].pages.len(), 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/");
}
#[test]
fn test_can_build_paginator_context() {
let (section, library) = create_library(false);
let (section, library) = create_library(false, 3, false);
let paginator = Paginator::from_section(&section, &library);
assert_eq!(paginator.pagers.len(), 2);
@ -336,7 +404,7 @@ mod tests {
#[test]
fn test_can_create_paginator_for_taxonomy() {
let (_, library) = create_library(false);
let (_, library) = create_library(false, 3, false);
let taxonomy_def = TaxonomyConfig {
name: "tags".to_string(),
paginate_by: Some(2),
@ -355,18 +423,18 @@ mod tests {
assert_eq!(paginator.pagers[0].index, 1);
assert_eq!(paginator.pagers[0].pages.len(), 2);
assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/tags/something/");
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].pages.len(), 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/");
}
// https://github.com/getzola/zola/issues/866
#[test]
fn works_with_empty_paginate_path() {
let (mut section, library) = create_library(false);
let (mut section, library) = create_library(false, 3, false);
section.meta.paginate_path = String::new();
let paginator = Paginator::from_section(&section, &library);
assert_eq!(paginator.pagers.len(), 2);
@ -374,12 +442,12 @@ mod tests {
assert_eq!(paginator.pagers[0].index, 1);
assert_eq!(paginator.pagers[0].pages.len(), 2);
assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts/");
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].pages.len(), 2);
assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/2/");
assert_eq!(paginator.pagers[1].path, "posts/2/");
assert_eq!(paginator.pagers[1].path, "/posts/2/");
let context = paginator.build_paginator_context(&paginator.pagers[0]);
assert_eq!(context["base_url"], to_value("https://vincent.is/posts/").unwrap());

View File

@ -1,3 +1,4 @@
use std::cmp::Ordering;
use std::collections::HashMap;
use serde_derive::Serialize;
@ -40,7 +41,7 @@ impl<'a> SerializedTaxonomyItem<'a> {
}
/// A taxonomy with all its pages
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone)]
pub struct TaxonomyItem {
pub name: String,
pub slug: String,
@ -70,22 +71,33 @@ impl TaxonomyItem {
})
.collect();
let (mut pages, ignored_pages) = sort_pages_by_date(data);
let slug = slugify_paths(name, config.slugify.taxonomies);
let item_slug = slugify_paths(name, config.slugify.taxonomies);
let taxo_slug = slugify_paths(&taxonomy.name, config.slugify.taxonomies);
let permalink = if taxonomy.lang != config.default_language {
config.make_permalink(&format!("/{}/{}/{}", taxonomy.lang, taxonomy.name, slug))
config.make_permalink(&format!("/{}/{}/{}", taxonomy.lang, taxo_slug, item_slug))
} else {
config.make_permalink(&format!("/{}/{}", taxonomy.name, slug))
config.make_permalink(&format!("/{}/{}", taxo_slug, item_slug))
};
// We still append pages without dates at the end
pages.extend(ignored_pages);
TaxonomyItem { name: name.to_string(), permalink, slug, pages }
TaxonomyItem { name: name.to_string(), permalink, slug: item_slug, pages }
}
pub fn serialize<'a>(&'a self, library: &'a Library) -> SerializedTaxonomyItem<'a> {
SerializedTaxonomyItem::from_item(self, library)
}
pub fn merge(&mut self, other: Self) {
self.pages.extend(other.pages);
}
}
impl PartialEq for TaxonomyItem {
fn eq(&self, other: &Self) -> bool {
self.permalink == other.permalink
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
@ -121,8 +133,23 @@ impl Taxonomy {
for (name, pages) in items {
sorted_items.push(TaxonomyItem::new(&name, &kind, config, pages, library));
}
sorted_items.sort_by(|a, b| a.name.cmp(&b.name));
//sorted_items.sort_by(|a, b| a.name.cmp(&b.name));
sorted_items.sort_by(|a, b| match a.slug.cmp(&b.slug) {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => a.name.cmp(&b.name),
});
sorted_items.dedup_by(|a, b| {
// custom Eq impl checks for equal permalinks
// here we make sure all pages from a get coppied to b
// before dedup gets rid of it
if a == b {
b.merge(a.to_owned());
true
} else {
false
}
});
Taxonomy { kind, items: sorted_items }
}
@ -150,7 +177,7 @@ impl Taxonomy {
"current_url",
&config.make_permalink(&format!("{}/{}", self.kind.name, item.slug)),
);
context.insert("current_path", &format!("/{}/{}", self.kind.name, item.slug));
context.insert("current_path", &format!("/{}/{}/", self.kind.name, item.slug));
render_template(&format!("{}/single.html", self.kind.name), tera, context, &config.theme)
.map_err(|e| {
@ -172,7 +199,7 @@ impl Taxonomy {
context.insert("lang", &self.kind.lang);
context.insert("taxonomy", &self.kind);
context.insert("current_url", &config.make_permalink(&self.kind.name));
context.insert("current_path", &self.kind.name);
context.insert("current_path", &format!("/{}/", self.kind.name));
render_template(&format!("{}/list.html", self.kind.name), tera, context, &config.theme)
.map_err(|e| {
@ -189,23 +216,25 @@ pub fn find_taxonomies(config: &Config, library: &Library) -> Result<Vec<Taxonom
let taxonomies_def = {
let mut m = HashMap::new();
for t in &config.taxonomies {
m.insert(format!("{}-{}", t.name, t.lang), t);
let slug = slugify_paths(&t.name, config.slugify.taxonomies);
m.insert(format!("{}-{}", slug, t.lang), t);
}
m
};
let mut all_taxonomies = HashMap::new();
for (key, page) in library.pages() {
for (name, val) in &page.meta.taxonomies {
let taxo_key = format!("{}-{}", name, page.lang);
for (name, taxo_term) in &page.meta.taxonomies {
let taxo_slug = slugify_paths(&name, config.slugify.taxonomies);
let taxo_key = format!("{}-{}", &taxo_slug, 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 term in taxo_term {
all_taxonomies
.get_mut(&taxo_key)
.unwrap()
.entry(v.to_string())
.entry(term.to_string())
.or_insert_with(|| vec![])
.push(key);
}
@ -235,7 +264,7 @@ mod tests {
use crate::content::Page;
use crate::library::Library;
use config::{Config, Language, Taxonomy as TaxonomyConfig};
use config::{Config, Language, Slugify, Taxonomy as TaxonomyConfig};
use utils::slugs::SlugifyStrategy;
#[test]
@ -710,4 +739,239 @@ mod tests {
);
assert_eq!(categories.items[1].pages.len(), 1);
}
#[test]
fn taxonomies_are_groupted_by_permalink() {
let mut config = Config::default();
let mut library = Library::new(2, 0, false);
config.taxonomies = vec![
TaxonomyConfig {
name: "test-taxonomy".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
TaxonomyConfig {
name: "test taxonomy".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
TaxonomyConfig {
name: "test-taxonomy ".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
TaxonomyConfig {
name: "Test-Taxonomy ".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
];
let mut page1 = Page::default();
let mut taxo_page1 = HashMap::new();
taxo_page1.insert(
"test-taxonomy".to_string(),
vec!["term one".to_string(), "term two".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(
"test taxonomy".to_string(),
vec!["Term Two".to_string(), "term-one".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("test-taxonomy ".to_string(), vec!["term one ".to_string()]);
page3.meta.taxonomies = taxo_page3;
page3.lang = config.default_language.clone();
library.insert_page(page3);
let mut page4 = Page::default();
let mut taxo_page4 = HashMap::new();
taxo_page4.insert("Test-Taxonomy ".to_string(), vec!["Term-Two ".to_string()]);
page4.meta.taxonomies = taxo_page4;
page4.lang = config.default_language.clone();
library.insert_page(page4);
// taxonomies should all be the same
let taxonomies = find_taxonomies(&config, &library).unwrap();
assert_eq!(taxonomies.len(), 1);
let tax = &taxonomies[0];
// terms should be "term one", "term two"
assert_eq!(tax.items.len(), 2);
let term1 = &tax.items[0];
let term2 = &tax.items[1];
assert_eq!(term1.name, "term one");
assert_eq!(term1.slug, "term-one");
assert_eq!(term1.permalink, "http://a-website.com/test-taxonomy/term-one/");
assert_eq!(term1.pages.len(), 3);
assert_eq!(term2.name, "Term Two");
assert_eq!(term2.slug, "term-two");
assert_eq!(term2.permalink, "http://a-website.com/test-taxonomy/term-two/");
assert_eq!(term2.pages.len(), 3);
}
#[test]
fn taxonomies_with_unic_are_grouped_with_default_slugify_strategy() {
let mut config = Config::default();
let mut library = Library::new(2, 0, false);
config.taxonomies = vec![
TaxonomyConfig {
name: "test-taxonomy".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
TaxonomyConfig {
name: "test taxonomy".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
TaxonomyConfig {
name: "test-taxonomy ".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
TaxonomyConfig {
name: "Test-Taxonomy ".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
];
let mut page1 = Page::default();
let mut taxo_page1 = HashMap::new();
taxo_page1.insert("test-taxonomy".to_string(), vec!["Ecole".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("test taxonomy".to_string(), vec!["École".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("test-taxonomy ".to_string(), vec!["ecole".to_string()]);
page3.meta.taxonomies = taxo_page3;
page3.lang = config.default_language.clone();
library.insert_page(page3);
let mut page4 = Page::default();
let mut taxo_page4 = HashMap::new();
taxo_page4.insert("Test-Taxonomy ".to_string(), vec!["école".to_string()]);
page4.meta.taxonomies = taxo_page4;
page4.lang = config.default_language.clone();
library.insert_page(page4);
// taxonomies should all be the same
let taxonomies = find_taxonomies(&config, &library).unwrap();
assert_eq!(taxonomies.len(), 1);
let tax = &taxonomies[0];
// under the default slugify stratagy all of the provided terms should be the same
assert_eq!(tax.items.len(), 1);
let term1 = &tax.items[0];
assert_eq!(term1.name, "Ecole");
assert_eq!(term1.slug, "ecole");
assert_eq!(term1.permalink, "http://a-website.com/test-taxonomy/ecole/");
assert_eq!(term1.pages.len(), 4);
}
#[test]
fn taxonomies_with_unic_are_not_grouped_with_safe_slugify_strategy() {
let mut config = Config::default();
config.slugify = Slugify {
paths: SlugifyStrategy::Safe,
taxonomies: SlugifyStrategy::Safe,
anchors: SlugifyStrategy::Safe,
};
let mut library = Library::new(2, 0, false);
config.taxonomies = vec![
TaxonomyConfig {
name: "test-taxonomy".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
TaxonomyConfig {
name: "test taxonomy".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
TaxonomyConfig {
name: "test-taxonomy ".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
TaxonomyConfig {
name: "Test-Taxonomy ".to_string(),
lang: config.default_language.clone(),
..TaxonomyConfig::default()
},
];
let mut page1 = Page::default();
let mut taxo_page1 = HashMap::new();
taxo_page1.insert("test-taxonomy".to_string(), vec!["Ecole".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("test-taxonomy".to_string(), vec!["École".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("test-taxonomy".to_string(), vec!["ecole".to_string()]);
page3.meta.taxonomies = taxo_page3;
page3.lang = config.default_language.clone();
library.insert_page(page3);
let mut page4 = Page::default();
let mut taxo_page4 = HashMap::new();
taxo_page4.insert("test-taxonomy".to_string(), vec!["école".to_string()]);
page4.meta.taxonomies = taxo_page4;
page4.lang = config.default_language.clone();
library.insert_page(page4);
// taxonomies should all be the same
let taxonomies = find_taxonomies(&config, &library).unwrap();
let tax = &taxonomies[0];
// if names are different permalinks should also be different so
// the tems are still accessable
for term1 in tax.items.iter() {
for term2 in tax.items.iter() {
assert!(term1.name == term2.name || term1.permalink != term2.permalink);
}
}
// under the safe slugify strategy all terms should be distinct
assert_eq!(tax.items.len(), 4);
}
}

View File

@ -16,4 +16,4 @@ default-features = false
features = ["blocking", "rustls-tls"]
[dev-dependencies]
mockito = "0.25"
mockito = "0.27"

View File

@ -54,7 +54,10 @@ pub fn check_url(url: &str, config: &LinkChecker) -> Result {
let body = {
let mut buf: Vec<u8> = vec![];
response.copy_to(&mut buf).unwrap();
String::from_utf8(buf).unwrap()
match String::from_utf8(buf) {
Ok(s) => s,
Err(_) => return Err("The page didn't return valid UTF-8".to_string()),
}
};
match check_page_for_anchor(url, body) {
@ -101,11 +104,15 @@ fn has_anchor(url: &str) -> bool {
fn check_page_for_anchor(url: &str, body: String) -> errors::Result<()> {
let index = url.find('#').unwrap();
let anchor = url.get(index + 1..).unwrap();
let checks: [String; 8] = [
let checks = [
format!(" id={}", anchor),
format!(" ID={}", anchor),
format!(" id='{}'", anchor),
format!(" ID='{}'", anchor),
format!(r#" id="{}""#, anchor),
format!(r#" ID="{}""#, anchor),
format!(" name={}", anchor),
format!(" NAME={}", anchor),
format!(" name='{}'", anchor),
format!(" NAME='{}'", anchor),
format!(r#" name="{}""#, anchor),
@ -256,7 +263,7 @@ mod tests {
}
#[test]
fn can_validate_anchors() {
fn can_validate_anchors_with_double_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, body);
@ -273,9 +280,17 @@ mod tests {
}
#[test]
fn can_validate_anchors_with_other_quotes() {
fn can_validate_anchors_with_single_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 body = "<body><h3 id='method.collect'>collect</h3></body>".to_string();
let res = check_page_for_anchor(url, body);
assert!(res.is_ok());
}
#[test]
fn can_validate_anchors_without_quotes() {
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, body);
assert!(res.is_ok());
}

View File

@ -1,15 +0,0 @@
[package]
name = "rebuild"
version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
edition = "2018"
[dependencies]
errors = { path = "../errors" }
front_matter = { path = "../front_matter" }
library = { path = "../library" }
site = { path = "../site" }
[dev-dependencies]
tempfile = "3"
fs_extra = "1.1"

View File

@ -1,493 +0,0 @@
use std::path::{Component, Path};
use errors::{bail, Result};
use front_matter::{PageFrontMatter, SectionFrontMatter};
use library::{Page, Section};
use site::Site;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PageChangesNeeded {
/// Editing `taxonomies`
Taxonomies,
/// Editing `date`, `order` or `weight`
Sort,
/// Editing anything causes a re-render of the page
Render,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SectionChangesNeeded {
/// Editing `sort_by`
Sort,
/// Editing `title`, `description`, `extra`, `template` or setting `render` to true
Render,
/// Editing `paginate_by`, `paginate_path` or `insert_anchor_links`
RenderWithPages,
/// Setting `render` to false
Delete,
/// Changing `transparent`
Transparent,
}
/// Evaluates all the params in the front matter that changed so we can do the smallest
/// delta in the serve command
/// Order matters as the actions will be done in insertion order
fn find_section_front_matter_changes(
current: &SectionFrontMatter,
new: &SectionFrontMatter,
) -> Vec<SectionChangesNeeded> {
let mut changes_needed = vec![];
if current.sort_by != new.sort_by {
changes_needed.push(SectionChangesNeeded::Sort);
}
if current.transparent != new.transparent {
changes_needed.push(SectionChangesNeeded::Transparent);
}
// We want to hide the section
// TODO: what to do on redirect_path change?
if current.render && !new.render {
changes_needed.push(SectionChangesNeeded::Delete);
// Nothing else we can do
return changes_needed;
}
if current.paginate_by != new.paginate_by
|| current.paginate_path != new.paginate_path
|| current.insert_anchor_links != new.insert_anchor_links
{
changes_needed.push(SectionChangesNeeded::RenderWithPages);
// Nothing else we can do
return changes_needed;
}
// Any new change will trigger a re-rendering of the section page only
changes_needed.push(SectionChangesNeeded::Render);
changes_needed
}
/// Evaluates all the params in the front matter that changed so we can do the smallest
/// delta in the serve command
/// Order matters as the actions will be done in insertion order
fn find_page_front_matter_changes(
current: &PageFrontMatter,
other: &PageFrontMatter,
) -> Vec<PageChangesNeeded> {
let mut changes_needed = vec![];
if current.taxonomies != other.taxonomies {
changes_needed.push(PageChangesNeeded::Taxonomies);
}
if current.date != other.date || current.order != other.order || current.weight != other.weight
{
changes_needed.push(PageChangesNeeded::Sort);
}
changes_needed.push(PageChangesNeeded::Render);
changes_needed
}
/// Handles a path deletion: could be a page, a section, a folder
fn delete_element(site: &mut Site, path: &Path, is_section: bool) -> Result<()> {
{
let mut library = site.library.write().unwrap();
// Ignore the event if this path was not known
if !library.contains_section(&path.to_path_buf())
&& !library.contains_page(&path.to_path_buf())
{
return Ok(());
}
if is_section {
if let Some(s) = library.remove_section(&path.to_path_buf()) {
site.permalinks.remove(&s.file.relative);
}
} else if let Some(p) = library.remove_page(&path.to_path_buf()) {
site.permalinks.remove(&p.file.relative);
}
}
// We might have delete the root _index.md so ensure we have at least the default one
// before populating
site.create_default_index_sections()?;
site.populate_sections();
site.populate_taxonomies()?;
// Ensure we have our fn updated so it doesn't contain the permalink(s)/section/page deleted
site.register_early_global_fns();
site.register_tera_global_fns();
// Deletion is something that doesn't happen all the time so we
// don't need to optimise it too much
site.build()
}
/// Handles a `_index.md` (a section) being edited in some ways
fn handle_section_editing(site: &mut Site, path: &Path) -> Result<()> {
let section = Section::from_file(path, &site.config, &site.base_path)?;
let pathbuf = path.to_path_buf();
match site.add_section(section, true)? {
// Updating a section
Some(prev) => {
site.populate_sections();
site.process_images()?;
{
let library = site.library.read().unwrap();
if library.get_section(&pathbuf).unwrap().meta == prev.meta {
// Front matter didn't change, only content did
// so we render only the section page, not its pages
return site.render_section(&library.get_section(&pathbuf).unwrap(), false);
}
}
// Front matter changed
let changes = find_section_front_matter_changes(
&site.library.read().unwrap().get_section(&pathbuf).unwrap().meta,
&prev.meta,
);
for change in changes {
// Sort always comes first if present so the rendering will be fine
match change {
SectionChangesNeeded::Sort => {
site.register_tera_global_fns();
}
SectionChangesNeeded::Render => site.render_section(
&site.library.read().unwrap().get_section(&pathbuf).unwrap(),
false,
)?,
SectionChangesNeeded::RenderWithPages => site.render_section(
&site.library.read().unwrap().get_section(&pathbuf).unwrap(),
true,
)?,
// not a common enough operation to make it worth optimizing
SectionChangesNeeded::Delete | SectionChangesNeeded::Transparent => {
site.build()?;
}
};
}
Ok(())
}
// New section, only render that one
None => {
site.populate_sections();
site.process_images()?;
site.register_tera_global_fns();
site.render_section(&site.library.read().unwrap().get_section(&pathbuf).unwrap(), true)
}
}
}
macro_rules! render_parent_sections {
($site: expr, $path: expr) => {
for s in $site.library.read().unwrap().find_parent_sections($path) {
$site.render_section(s, false)?;
}
};
}
/// Handles a page being edited in some ways
fn handle_page_editing(site: &mut Site, path: &Path) -> Result<()> {
let page = Page::from_file(path, &site.config, &site.base_path)?;
let pathbuf = path.to_path_buf();
match site.add_page(page, true)? {
// Updating a page
Some(prev) => {
site.populate_sections();
site.populate_taxonomies()?;
site.register_tera_global_fns();
site.process_images()?;
{
let library = site.library.read().unwrap();
// Front matter didn't change, only content did
if library.get_page(&pathbuf).unwrap().meta == prev.meta {
// Other than the page itself, the summary might be seen
// on a paginated list for a blog for example
if library.get_page(&pathbuf).unwrap().summary.is_some() {
render_parent_sections!(site, path);
}
return site.render_page(&library.get_page(&pathbuf).unwrap());
}
}
// Front matter changed
let changes = find_page_front_matter_changes(
&site.library.read().unwrap().get_page(&pathbuf).unwrap().meta,
&prev.meta,
);
for change in changes {
site.register_tera_global_fns();
// Sort always comes first if present so the rendering will be fine
match change {
PageChangesNeeded::Taxonomies => {
site.populate_taxonomies()?;
site.render_taxonomies()?;
}
PageChangesNeeded::Sort => {
site.render_index()?;
}
PageChangesNeeded::Render => {
render_parent_sections!(site, path);
site.render_page(
&site.library.read().unwrap().get_page(&path.to_path_buf()).unwrap(),
)?;
}
};
}
Ok(())
}
// It's a new page!
None => {
site.populate_sections();
site.populate_taxonomies()?;
site.register_early_global_fns();
site.register_tera_global_fns();
site.process_images()?;
// No need to optimise that yet, we can revisit if it becomes an issue
site.build()
}
}
}
/// What happens when we rename a file/folder in the content directory.
/// Note that this is only called for folders when it isn't empty
pub fn after_content_rename(site: &mut Site, old: &Path, new: &Path) -> Result<()> {
let new_path = if new.is_dir() {
if new.join("_index.md").exists() {
// This is a section keep the dir folder to differentiate from renaming _index.md
// which doesn't do the same thing
new.to_path_buf()
} else if new.join("index.md").exists() {
new.join("index.md")
} else {
bail!("Got unexpected folder {:?} while handling renaming that was not expected", new);
}
} else {
new.to_path_buf()
};
// A section folder has been renamed: just reload the whole site and rebuild it as we
// do not really know what needs to be rendered
if new_path.is_dir() {
site.load()?;
return site.build();
}
// We ignore renames on non-markdown files for now
if let Some(ext) = new_path.extension() {
if ext != "md" {
return Ok(());
}
}
// Renaming a file to _index.md, let the section editing do something and hope for the best
if new_path.file_name().unwrap() == "_index.md" {
// We aren't entirely sure where the original thing was so just try to delete whatever was
// at the old path
{
let mut library = site.library.write().unwrap();
library.remove_page(&old.to_path_buf());
library.remove_section(&old.to_path_buf());
}
return handle_section_editing(site, &new_path);
}
// If it is a page, just delete what was there before and
// fake it's a new page
let old_path = if new_path.file_name().unwrap() == "index.md" {
old.join("index.md")
} else {
old.to_path_buf()
};
site.library.write().unwrap().remove_page(&old_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;
}
}
false
}
/// What happens when a section or a page is created/edited
pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
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 index = path.parent().unwrap().join("index.md");
let mut potential_indices = vec![path.parent().unwrap().join("index.md")];
for language in &site.config.languages {
potential_indices.push(path.parent().unwrap().join(format!("index.{}.md", language.code)));
}
let colocated_index = potential_indices.contains(&path.to_path_buf());
// A few situations can happen:
// 1. Change on .md files
// a. Is there already an `index.md`? Return an error if it's something other than delete
// b. Deleted? remove the element
// c. Edited?
// 1. filename is `_index.md`, this is a section
// 1. it's a page otherwise
// 2. Change on non .md files
// a. Try to find a corresponding `_index.md`
// 1. Nothing? Return Ok
// 2. Something? Update the page
if is_md {
// only delete if it was able to be added in the first place
if !index.exists() && !path.exists() {
return delete_element(site, path, is_section);
}
// Added another .md in a assets directory
if index.exists() && path.exists() && !colocated_index {
bail!(
"Change on {:?} detected but only files named `index.md` with an optional language code are allowed",
path.display()
);
} else if index.exists() && !path.exists() {
// deleted the wrong .md, do nothing
return Ok(());
}
if is_section {
handle_section_editing(site, path)
} else {
handle_page_editing(site, path)
}
} else if index.exists() {
handle_page_editing(site, &index)
} else {
Ok(())
}
}
/// What happens when a template is changed
pub fn after_template_change(site: &mut Site, path: &Path) -> Result<()> {
site.tera.full_reload()?;
let filename = path.file_name().unwrap().to_str().unwrap();
match filename {
"sitemap.xml" => site.render_sitemap(),
filename if filename == site.config.feed_filename => {
// FIXME: this is insufficient; for multilingual sites, its rendering the wrong
// content into the root feed, and its not regenerating any of the other feeds (other
// languages or taxonomies with feed enabled).
site.render_feed(
site.library.read().unwrap().pages_values(),
None,
&site.config.default_language,
None,
)
}
"split_sitemap_index.xml" => site.render_sitemap(),
"robots.txt" => site.render_robots(),
"single.html" | "list.html" => site.render_taxonomies(),
"page.html" => {
site.render_sections()?;
site.render_orphan_pages()
}
"section.html" => site.render_sections(),
"404.html" => site.render_404(),
// Either the index or some unknown template changed
// We can't really know what this change affects so rebuild all
// the things
_ => {
// If we are updating a shortcode, re-render the markdown of all pages/site
// 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
// everything
if filename == "anchor-link.html"
|| path.components().any(|x| x == Component::Normal("shortcodes".as_ref()))
{
site.render_markdown()?;
}
site.populate_sections();
site.populate_taxonomies()?;
site.render_sections()?;
site.process_images()?;
site.render_orphan_pages()?;
site.render_taxonomies()
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::{
find_page_front_matter_changes, find_section_front_matter_changes, PageChangesNeeded,
SectionChangesNeeded,
};
use front_matter::{PageFrontMatter, SectionFrontMatter, SortBy};
#[test]
fn can_find_taxonomy_changes_in_page_frontmatter() {
let mut taxonomies = HashMap::new();
taxonomies.insert("tags".to_string(), vec!["a tag".to_string()]);
let new = PageFrontMatter { taxonomies, ..PageFrontMatter::default() };
let changes = find_page_front_matter_changes(&PageFrontMatter::default(), &new);
assert_eq!(changes, vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Render]);
}
#[test]
fn can_find_multiple_changes_in_page_frontmatter() {
let mut taxonomies = HashMap::new();
taxonomies.insert("categories".to_string(), vec!["a category".to_string()]);
let current = PageFrontMatter { taxonomies, order: Some(1), ..PageFrontMatter::default() };
let changes = find_page_front_matter_changes(&current, &PageFrontMatter::default());
assert_eq!(
changes,
vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Sort, PageChangesNeeded::Render]
);
}
#[test]
fn can_find_sort_changes_in_section_frontmatter() {
let new = SectionFrontMatter { sort_by: SortBy::Date, ..SectionFrontMatter::default() };
let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new);
assert_eq!(changes, vec![SectionChangesNeeded::Sort, SectionChangesNeeded::Render]);
}
#[test]
fn can_find_render_changes_in_section_frontmatter() {
let new = SectionFrontMatter { render: false, ..SectionFrontMatter::default() };
let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new);
assert_eq!(changes, vec![SectionChangesNeeded::Delete]);
}
#[test]
fn can_find_paginate_by_changes_in_section_frontmatter() {
let new = SectionFrontMatter { paginate_by: Some(10), ..SectionFrontMatter::default() };
let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new);
assert_eq!(changes, vec![SectionChangesNeeded::RenderWithPages]);
}
}

View File

@ -1,284 +0,0 @@
use std::env;
use std::fs::{self, File};
use std::io::prelude::*;
use fs_extra::dir;
use site::Site;
use tempfile::tempdir;
use rebuild::{after_content_change, after_content_rename};
// Loads the test_site in a tempdir and build it there
// Returns (site_path_in_tempdir, site)
macro_rules! load_and_build_site {
($tmp_dir: expr, $site: expr) => {{
let mut path =
env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push($site);
let mut options = dir::CopyOptions::new();
options.copy_inside = true;
dir::copy(&path, &$tmp_dir, &options).unwrap();
let site_path = $tmp_dir.path().join($site);
let config_file = site_path.join("config.toml");
let mut site = Site::new(&site_path, &config_file).unwrap();
site.load().unwrap();
let public = &site_path.join("public");
site.set_output_path(&public);
site.build().unwrap();
(site_path, site)
}};
}
/// Replace the file at the path (starting from root) by the given content
/// and return the file path that was modified
macro_rules! edit_file {
($site_path: expr, $path: expr, $content: expr) => {{
let mut t = $site_path.clone();
for c in $path.split('/') {
t.push(c);
}
let mut file = File::create(&t).expect("Could not open/create file");
file.write_all($content).expect("Could not write to the file");
t
}};
}
macro_rules! file_contains {
($site_path: expr, $path: expr, $text: expr) => {{
let mut path = $site_path.clone();
for component in $path.split("/") {
path.push(component);
}
let mut file = File::open(&path).unwrap();
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
println!("{:?} -> {}", path, s);
s.contains($text)
}};
}
/// Rename a file or a folder to the new given name
macro_rules! rename {
($site_path: expr, $path: expr, $new_name: expr) => {{
let mut t = $site_path.clone();
for c in $path.split('/') {
t.push(c);
}
let mut new_path = t.parent().unwrap().to_path_buf();
new_path.push($new_name);
fs::rename(&t, &new_path).unwrap();
println!("Renamed {:?} to {:?}", t, new_path);
(t, new_path)
}};
}
#[test]
fn can_rebuild_after_simple_change_to_page_content() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site");
let file_path = edit_file!(
site_path,
"content/rebuild/first.md",
br#"
+++
title = "first"
weight = 1
date = 2017-01-01
+++
Some content"#
);
let res = after_content_change(&mut site, &file_path);
assert!(res.is_ok());
assert!(file_contains!(site_path, "public/rebuild/first/index.html", "<p>Some content</p>"));
}
#[test]
fn can_rebuild_after_title_change_page_global_func_usage() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site");
let file_path = edit_file!(
site_path,
"content/rebuild/first.md",
br#"
+++
title = "Premier"
weight = 10
date = 2017-01-01
+++
# A title"#
);
let res = after_content_change(&mut site, &file_path);
assert!(res.is_ok());
assert!(file_contains!(site_path, "public/rebuild/index.html", "<h1>Premier</h1>"));
}
#[test]
fn can_rebuild_after_sort_change_in_section() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site");
let file_path = edit_file!(
site_path,
"content/rebuild/_index.md",
br#"
+++
paginate_by = 1
sort_by = "weight"
template = "rebuild.html"
+++
"#
);
let res = after_content_change(&mut site, &file_path);
assert!(res.is_ok());
assert!(file_contains!(
site_path,
"public/rebuild/index.html",
"<h1>first</h1><h1>second</h1>"
));
}
#[test]
fn can_rebuild_after_transparent_change() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site");
let file_path = edit_file!(
site_path,
"content/posts/2018/_index.md",
br#"
+++
transparent = false
render = false
+++
"#
);
// Also remove pagination from posts section so we check whether the transparent page title
// is there or not without dealing with pagination
edit_file!(
site_path,
"content/posts/_index.md",
br#"
+++
template = "section.html"
insert_anchor_links = "left"
+++
"#
);
let res = after_content_change(&mut site, &file_path);
assert!(res.is_ok());
assert!(!file_contains!(site_path, "public/posts/index.html", "A transparent page"));
}
#[test]
fn can_rebuild_after_renaming_page() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site");
let (old_path, new_path) = rename!(site_path, "content/posts/simple.md", "hard.md");
let res = after_content_rename(&mut site, &old_path, &new_path);
println!("{:?}", res);
assert!(res.is_ok());
assert!(file_contains!(site_path, "public/posts/hard/index.html", "A simple page"));
}
// https://github.com/Keats/gutenberg/issues/385
#[test]
fn can_rebuild_after_renaming_colocated_asset_folder() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site");
let (old_path, new_path) =
rename!(site_path, "content/posts/with-assets", "with-assets-updated");
assert!(file_contains!(site_path, "content/posts/with-assets-updated/index.md", "Hello"));
let res = after_content_rename(&mut site, &old_path, &new_path);
println!("{:?}", res);
assert!(res.is_ok());
assert!(file_contains!(
site_path,
"public/posts/with-assets-updated/index.html",
"Hello world"
));
}
// https://github.com/Keats/gutenberg/issues/385
#[test]
fn can_rebuild_after_renaming_section_folder() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site");
let (old_path, new_path) = rename!(site_path, "content/posts", "new-posts");
assert!(file_contains!(site_path, "content/new-posts/simple.md", "simple"));
let res = after_content_rename(&mut site, &old_path, &new_path);
assert!(res.is_ok());
assert!(file_contains!(site_path, "public/new-posts/simple/index.html", "simple"));
}
#[test]
fn can_rebuild_after_renaming_non_md_asset_in_colocated_folder() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site");
let (old_path, new_path) =
rename!(site_path, "content/posts/with-assets/zola.png", "gutenberg.png");
// Testing that we don't try to load some images as markdown or something
let res = after_content_rename(&mut site, &old_path, &new_path);
assert!(res.is_ok());
}
#[test]
fn can_rebuild_after_deleting_file() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site");
let path = site_path.join("content").join("posts").join("fixed-slug.md");
fs::remove_file(&path).unwrap();
let res = after_content_change(&mut site, &path);
println!("{:?}", res);
assert!(res.is_ok());
}
#[test]
fn can_rebuild_after_editing_in_colocated_asset_folder_with_language() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site_i18n");
let file_path = edit_file!(
site_path,
"content/blog/with-assets/index.fr.md",
br#"
+++
date = 2018-11-11
+++
Edite
"#
);
let res = after_content_change(&mut site, &file_path);
println!("{:?}", res);
assert!(res.is_ok());
assert!(file_contains!(site_path, "public/fr/blog/with-assets/index.html", "Edite"));
}
// https://github.com/getzola/zola/issues/620
#[test]
fn can_rebuild_after_renaming_section_and_deleting_file() {
let tmp_dir = tempdir().expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir, "test_site");
let (old_path, new_path) = rename!(site_path, "content/posts/", "post/");
let res = after_content_rename(&mut site, &old_path, &new_path);
assert!(res.is_ok());
let path = site_path.join("content").join("_index.md");
fs::remove_file(&path).unwrap();
let res = after_content_change(&mut site, &path);
println!("{:?}", res);
assert!(res.is_ok());
}

View File

@ -3,11 +3,12 @@ name = "rendering"
version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
edition = "2018"
include = ["src/**/*"]
[dependencies]
tera = { version = "1", features = ["preserve_order"] }
syntect = "4.1"
pulldown-cmark = "0.7"
pulldown-cmark = { version = "0.8", default-features = false }
serde = "1"
serde_derive = "1"
pest = "2"

View File

@ -14,7 +14,9 @@ pub fn render_content(content: &str, context: &RenderContext) -> Result<markdown
// Don't do shortcodes if there is nothing like a shortcode in the content
if content.contains("{{") || content.contains("{%") {
let rendered = render_shortcodes(content, context)?;
return markdown_to_html(&rendered, context);
let mut html = markdown_to_html(&rendered, context)?;
html.body = html.body.replace("<!--\\n-->", "\n");
return Ok(html);
}
markdown_to_html(&content, context)

View File

@ -185,6 +185,7 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
opts.insert(Options::ENABLE_TABLES);
opts.insert(Options::ENABLE_FOOTNOTES);
opts.insert(Options::ENABLE_STRIKETHROUGH);
opts.insert(Options::ENABLE_TASKLISTS);
{
let mut events = Parser::new_ext(content, opts)

View File

@ -17,7 +17,6 @@ const _GRAMMAR: &str = include_str!("content.pest");
pub struct ContentParser;
lazy_static! {
static ref MULTIPLE_NEWLINE_RE: Regex = Regex::new(r"\n\s*\n").unwrap();
static ref OUTER_NEWLINE_RE: Regex = Regex::new(r"^\s*\n|\n\s*$").unwrap();
}
@ -115,19 +114,27 @@ fn render_shortcode(
}
tera_context.extend(context.tera_context.clone());
let template_name = format!("shortcodes/{}.html", name);
let mut template_name = format!("shortcodes/{}.md", name);
if !context.tera.templates.contains_key(&template_name) {
template_name = format!("shortcodes/{}.html", name);
}
let res = utils::templates::render_template(&template_name, &context.tera, tera_context, &None)
.map_err(|e| Error::chain(format!("Failed to render {} shortcode", name), e))?;
// Small hack to avoid having multiple blank lines because of Tera tags for example
// A blank like will cause the markdown parser to think we're out of HTML and start looking
// at indentation, making the output a code block.
let res = MULTIPLE_NEWLINE_RE.replace_all(&res, "\n");
let res = OUTER_NEWLINE_RE.replace_all(&res, "");
Ok(res.to_string())
// A blank line will cause the markdown parser to think we're out of HTML and start looking
// at indentation, making the output a code block. To avoid this, newlines are replaced with
// "<!--\n-->" at this stage, which will be undone after markdown rendering in lib.rs. Since
// that is an HTML comment, it shouldn't be rendered anyway. and not cause problems unless
// someone wants to include that comment in their content. This behaviour is unwanted in when
// rendering markdown shortcodes.
if template_name.ends_with(".html") {
Ok(res.replace('\n', "<!--\\n-->").to_string())
} else {
Ok(res.to_string())
}
}
pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<String> {
@ -413,8 +420,8 @@ Some body {{ hello() }}{%/* end */%}"#,
fn shortcodes_with_body_do_not_eat_newlines() {
let mut tera = Tera::default();
tera.add_raw_template("shortcodes/youtube.html", "{{body | safe}}").unwrap();
let res = render_shortcodes("Body\n {% youtube() %}\nHello \n World{% end %}", &tera);
assert_eq!(res, "Body\n Hello \n World");
let res = render_shortcodes("Body\n {% youtube() %}\nHello \n \n\n World{% end %}", &tera);
assert_eq!(res, "Body\n Hello <!--\\n--> <!--\\n--><!--\\n--> World");
}
#[test]
@ -432,4 +439,16 @@ Some body {{ hello() }}{%/* end */%}"#,
let res = render_shortcodes("\n{{ youtube() }}\n", &tera);
assert_eq!(res, "\n Hello, Zola. \n");
}
#[test]
fn shortcodes_that_emit_markdown() {
let mut tera = Tera::default();
tera.add_raw_template(
"shortcodes/youtube.md",
"{% for i in [1,2,3] %}\n* {{ i }}\n{%- endfor %}",
)
.unwrap();
let res = render_shortcodes("{{ youtube() }}", &tera);
assert_eq!(res, "* 1\n* 2\n* 3");
}
}

View File

@ -1,7 +1,7 @@
use serde_derive::Serialize;
/// Populated while receiving events from the markdown parser
#[derive(Debug, PartialEq, Clone, Serialize)]
#[derive(Debug, Default, PartialEq, Clone, Serialize)]
pub struct Heading {
pub level: u32,
pub id: String,
@ -12,19 +12,7 @@ pub struct Heading {
impl Heading {
pub fn new(level: u32) -> Heading {
Heading {
level,
id: String::new(),
permalink: String::new(),
title: String::new(),
children: Vec::new(),
}
}
}
impl Default for Heading {
fn default() -> Self {
Heading::new(0)
Heading { level, ..Self::default() }
}
}

View File

@ -788,10 +788,7 @@ fn doesnt_try_to_highlight_content_from_shortcode() {
let markdown_string = r#"{{ figure(src="spherecluster.png", caption="Some spheres.") }}"#;
let expected = r#"<figure>
<img src="/images/spherecluster.png" alt="Some spheres." />
<figcaption>Some spheres.</figcaption>
</figure>"#;
let expected = "<figure>\n \n <img src=\"/images/spherecluster.png\" alt=\"Some spheres.\" />\n \n\n <figcaption>Some spheres.</figcaption>\n</figure>";
tera.add_raw_template(&format!("shortcodes/{}.html", "figure"), shortcode).unwrap();
let config = Config::default();
@ -801,6 +798,28 @@ fn doesnt_try_to_highlight_content_from_shortcode() {
assert_eq!(res.body, expected);
}
#[test]
fn can_emit_newlines_and_whitespace_with_shortcode() {
let permalinks_ctx = HashMap::new();
let mut tera = Tera::default();
tera.extend(&ZOLA_TERA).unwrap();
let shortcode = r#"<pre>
{{ body }}
</pre>"#;
let markdown_string = "{% preformatted() %}\nHello\n \n Zola\n \n !\n{% end %}";
let expected = "<pre>\nHello\n \n Zola\n \n !\n</pre>";
tera.add_raw_template(&format!("shortcodes/{}.html", "preformatted"), shortcode).unwrap();
let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(markdown_string, &context).unwrap();
assert_eq!(res.body, expected);
}
// TODO: re-enable once it's fixed in Tera
// https://github.com/Keats/tera/issues/373
//#[test]
@ -885,3 +904,44 @@ fn stops_with_an_error_on_an_empty_link() {
assert!(res.is_err());
assert_eq!(res.unwrap_err().to_string(), expected);
}
#[test]
fn can_passthrough_markdown_from_shortcode() {
let permalinks_ctx = HashMap::new();
let mut tera = Tera::default();
tera.extend(&ZOLA_TERA).unwrap();
let shortcode = r#"{% for line in body | split(pat="\n") %}
> {{ line }}
{%- endfor %}
-- {{ author }}
"#;
let markdown_string = r#"
Hello
{% quote(author="Vincent") %}
# Passing through
*to* **the** document
{% end %}
Bla bla"#;
let expected = r#"<p>Hello</p>
<blockquote>
<h1 id="passing-through">Passing through</h1>
<p><em>to</em> <strong>the</strong> document</p>
</blockquote>
<p>-- Vincent</p>
<p>Bla bla</p>
"#;
tera.add_raw_template(&format!("shortcodes/{}.md", "quote"), shortcode).unwrap();
let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(markdown_string, &context).unwrap();
println!("{:?}", res);
assert_eq!(res.body, expected);
}

View File

@ -5,9 +5,15 @@ authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
edition = "2018"
[dependencies]
elasticlunr-rs = "2"
elasticlunr-rs = {version = "2", default-features = false, features = ["da", "de", "du", "es", "fi", "fr", "it", "pt", "ro", "ru", "sv", "tr"] }
ammonia = "3"
lazy_static = "1"
errors = { path = "../errors" }
library = { path = "../library" }
config = { path = "../config" }
[features]
default = []
indexing-zh = ["elasticlunr-rs/zh"]
indexing-ja = ["elasticlunr-rs/ja"]

View File

@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet};
use elasticlunr::{Index, Language};
use lazy_static::lazy_static;
use config::Config;
use errors::{bail, Result};
use library::{Library, Section};
@ -25,11 +26,61 @@ lazy_static! {
};
}
fn build_fields(config: &Config) -> Vec<String> {
let mut fields = vec![];
if config.search.include_title {
fields.push("title".to_owned());
}
if config.search.include_description {
fields.push("description".to_owned());
}
if config.search.include_content {
fields.push("body".to_owned());
}
fields
}
fn fill_index(
config: &Config,
title: &Option<String>,
description: &Option<String>,
content: &str,
) -> Vec<String> {
let mut row = vec![];
if config.search.include_title {
row.push(title.clone().unwrap_or_default());
}
if config.search.include_description {
row.push(description.clone().unwrap_or_default());
}
if config.search.include_content {
let body = AMMONIA.clean(&content).to_string();
if let Some(truncate_len) = config.search.truncate_content_length {
// Not great for unicode
// TODO: fix it like the truncate in Tera
match body.char_indices().nth(truncate_len) {
None => row.push(body),
Some((idx, _)) => row.push((&body[..idx]).to_string()),
};
} else {
row.push(body);
};
}
row
}
/// Returns the generated JSON index with all the documents of the site added using
/// the language given
/// Errors if the language given is not available in Elasticlunr
/// TODO: is making `in_search_index` apply to subsections of a `false` section useful?
pub fn build_index(lang: &str, library: &Library) -> Result<String> {
pub fn build_index(lang: &str, library: &Library, config: &Config) -> Result<String> {
let language = match Language::from_code(lang) {
Some(l) => l,
None => {
@ -37,18 +88,18 @@ 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, &build_fields(&config));
for section in library.sections_values() {
if section.lang == lang {
add_section_to_index(&mut index, section, library);
add_section_to_index(&mut index, section, library, config);
}
}
Ok(index.to_json())
}
fn add_section_to_index(index: &mut Index, section: &Section, library: &Library) {
fn add_section_to_index(index: &mut Index, section: &Section, library: &Library, config: &Config) {
if !section.meta.in_search_index {
return;
}
@ -57,10 +108,7 @@ fn add_section_to_index(index: &mut Index, section: &Section, library: &Library)
if section.meta.redirect_to.is_none() {
index.add_doc(
&section.permalink,
&[
&section.meta.title.clone().unwrap_or_default(),
&AMMONIA.clean(&section.content).to_string(),
],
&fill_index(config, &section.meta.title, &section.meta.description, &section.content),
);
}
@ -72,10 +120,76 @@ fn add_section_to_index(index: &mut Index, section: &Section, library: &Library)
index.add_doc(
&page.permalink,
&[
&page.meta.title.clone().unwrap_or_default(),
&AMMONIA.clean(&page.content).to_string(),
],
&fill_index(config, &page.meta.title, &page.meta.description, &page.content),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use config::Config;
#[test]
fn can_build_fields() {
let mut config = Config::default();
let fields = build_fields(&config);
assert_eq!(fields, vec!["title", "body"]);
config.search.include_content = false;
config.search.include_description = true;
let fields = build_fields(&config);
assert_eq!(fields, vec!["title", "description"]);
config.search.include_content = true;
let fields = build_fields(&config);
assert_eq!(fields, vec!["title", "description", "body"]);
config.search.include_title = false;
let fields = build_fields(&config);
assert_eq!(fields, vec!["description", "body"]);
}
#[test]
fn can_fill_index_default() {
let config = Config::default();
let title = Some("A title".to_string());
let description = Some("A description".to_string());
let content = "Some content".to_string();
let res = fill_index(&config, &title, &description, &content);
assert_eq!(res.len(), 2);
assert_eq!(res[0], title.unwrap());
assert_eq!(res[1], content);
}
#[test]
fn can_fill_index_description() {
let mut config = Config::default();
config.search.include_description = true;
let title = Some("A title".to_string());
let description = Some("A description".to_string());
let content = "Some content".to_string();
let res = fill_index(&config, &title, &description, &content);
assert_eq!(res.len(), 3);
assert_eq!(res[0], title.unwrap());
assert_eq!(res[1], description.unwrap());
assert_eq!(res[2], content);
}
#[test]
fn can_fill_index_truncated_content() {
let mut config = Config::default();
config.search.truncate_content_length = Some(5);
let title = Some("A title".to_string());
let description = Some("A description".to_string());
let content = "Some content".to_string();
let res = fill_index(&config, &title, &description, &content);
assert_eq!(res.len(), 2);
assert_eq!(res[0], title.unwrap());
assert_eq!(res[1], content[..5]);
}
}

View File

@ -3,14 +3,17 @@ name = "site"
version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
edition = "2018"
include = ["src/**/*"]
[dependencies]
tera = "1"
glob = "0.3"
minify-html = "0.3.6"
rayon = "1"
serde = "1"
serde_derive = "1"
sass-rs = "0.2"
lazy_static = "1.1"
errors = { path = "../errors" }
config = { path = "../config" }

View File

@ -98,7 +98,6 @@ def gen_skeleton(name, is_blog):
shutil.rmtree(name)
os.makedirs(os.path.join(name, "content"))
os.makedirs(os.path.join(name, "static"))
with open(os.path.join(name, "config.toml"), "w") as f:
if is_blog:
@ -128,6 +127,7 @@ name = "Vincent Prouillet"
# Re-use the test templates
shutil.copytree("../../../test_site/templates", os.path.join(name, "templates"))
shutil.copytree("../../../test_site/themes", os.path.join(name, "themes"))
shutil.copytree("../../../test_site/static", os.path.join(name, "static"))
def gen_section(path, num_pages, is_blog):

View File

@ -46,7 +46,7 @@ fn bench_render_feed(b: &mut test::Bencher) {
site.library.read().unwrap().pages_values(),
None,
&site.config.default_language,
None,
|c| c,
)
.unwrap();
});

View File

@ -0,0 +1,79 @@
use std::path::PathBuf;
use rayon::prelude::*;
use serde_derive::Serialize;
use tera::Context;
use crate::Site;
use errors::Result;
use library::{sort_actual_pages_by_date, Page, TaxonomyItem};
use utils::templates::render_template;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SerializedFeedTaxonomyItem<'a> {
name: &'a str,
slug: &'a str,
permalink: &'a str,
}
impl<'a> SerializedFeedTaxonomyItem<'a> {
pub fn from_item(item: &'a TaxonomyItem) -> Self {
SerializedFeedTaxonomyItem {
name: &item.name,
slug: &item.slug,
permalink: &item.permalink,
}
}
}
pub fn render_feed(
site: &Site,
all_pages: Vec<&Page>,
lang: &str,
base_path: Option<&PathBuf>,
additional_context_fn: impl Fn(Context) -> Context,
) -> Result<Option<String>> {
let mut pages = all_pages.into_iter().filter(|p| p.meta.date.is_some()).collect::<Vec<_>>();
// Don't generate a feed if none of the pages has a date
if pages.is_empty() {
return Ok(None);
}
pages.par_sort_unstable_by(sort_actual_pages_by_date);
let mut context = Context::new();
context.insert(
"last_updated",
pages
.iter()
.filter_map(|page| page.meta.updated.as_ref())
.chain(pages[0].meta.date.as_ref())
.max() // I love lexicographically sorted date strings
.unwrap(), // Guaranteed because of pages[0].meta.date
);
let library = site.library.read().unwrap();
// limit to the last n elements if the limit is set; otherwise use all.
let num_entries = site.config.feed_limit.unwrap_or_else(|| pages.len());
let p =
pages.iter().take(num_entries).map(|x| x.to_serialized_basic(&library)).collect::<Vec<_>>();
context.insert("pages", &p);
context.insert("config", &site.config);
context.insert("lang", lang);
let feed_filename = &site.config.feed_filename;
let feed_url = if let Some(ref base) = base_path {
site.config.make_permalink(&base.join(feed_filename).to_string_lossy().replace('\\', "/"))
} else {
site.config.make_permalink(feed_filename)
};
context.insert("feed_url", &feed_url);
context = additional_context_fn(context);
let feed = render_template(feed_filename, &site.tera, context, &site.config.theme)?;
Ok(Some(feed))
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,172 @@
use rayon::prelude::*;
use crate::Site;
use errors::{Error, ErrorKind, Result};
/// Very similar to check_external_links but can't be merged as far as I can see since we always
/// want to check the internal links but only the external in zola check :/
pub fn check_internal_links_with_anchors(site: &Site) -> Result<()> {
let library = site.library.write().expect("Get lock for check_internal_links_with_anchors");
let page_links = library
.pages()
.values()
.map(|p| {
let path = &p.file.path;
p.internal_links_with_anchors.iter().map(move |l| (path.clone(), l))
})
.flatten();
let section_links = library
.sections()
.values()
.map(|p| {
let path = &p.file.path;
p.internal_links_with_anchors.iter().map(move |l| (path.clone(), l))
})
.flatten();
let all_links = page_links.chain(section_links).collect::<Vec<_>>();
if site.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 = site.base_path.clone();
full_path.push("content");
let errors: Vec<_> = all_links
.iter()
.filter_map(|(page_path, (md_path, anchor))| {
// There are a few `expect` here since the presence of the .md file will
// already have been checked in the markdown rendering
let mut p = full_path.clone();
for part in md_path.split('/') {
p.push(part);
}
if md_path.contains("_index.md") {
let section = library
.get_section(&p)
.expect("Couldn't find section in check_internal_links_with_anchors");
if section.has_anchor(&anchor) {
None
} else {
Some((page_path, md_path, anchor))
}
} else {
let page = library
.get_page(&p)
.expect("Couldn't find section in check_internal_links_with_anchors");
if page.has_anchor(&anchor) {
None
} else {
Some((page_path, md_path, anchor))
}
}
})
.collect();
if site.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() {
return Ok(());
}
let msg = errors
.into_iter()
.map(|(page_path, md_path, anchor)| {
format!(
"The anchor in the link `@/{}#{}` in {} does not exist.",
md_path,
anchor,
page_path.to_string_lossy(),
)
})
.collect::<Vec<_>>()
.join("\n");
Err(Error { kind: ErrorKind::Msg(msg), source: None })
}
pub fn check_external_links(site: &Site) -> Result<()> {
let library = site.library.write().expect("Get lock for check_external_links");
let page_links = library
.pages()
.values()
.map(|p| {
let path = &p.file.path;
p.external_links.iter().map(move |l| (path.clone(), l))
})
.flatten();
let section_links = library
.sections()
.values()
.map(|p| {
let path = &p.file.path;
p.external_links.iter().map(move |l| (path.clone(), l))
})
.flatten();
let all_links = page_links.chain(section_links).collect::<Vec<_>>();
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
// (almost) all pages simultaneously
let threads = std::cmp::min(all_links.len(), 32);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.map_err(|e| Error { kind: ErrorKind::Msg(e.to_string()), source: None })?;
let errors: Vec<_> = pool.install(|| {
all_links
.par_iter()
.filter_map(|(page_path, link)| {
if site
.config
.link_checker
.skip_prefixes
.iter()
.any(|prefix| link.starts_with(prefix))
{
return None;
}
let res = link_checker::check_url(&link, &site.config.link_checker);
if link_checker::is_valid(&res) {
None
} else {
Some((page_path, link, res))
}
})
.collect()
});
println!("> Checked {} external link(s): {} error(s) found.", all_links.len(), errors.len());
if errors.is_empty() {
return Ok(());
}
let msg = errors
.into_iter()
.map(|(page_path, link, check_res)| {
format!(
"Dead link in {} to {}: {}",
page_path.to_string_lossy(),
link,
link_checker::message(&check_res)
)
})
.collect::<Vec<_>>()
.join("\n");
Err(Error { kind: ErrorKind::Msg(msg), source: None })
}

View File

@ -0,0 +1,73 @@
use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use glob::glob;
use sass_rs::{compile_file, Options, OutputStyle};
use errors::{bail, Result};
use utils::fs::{create_file, ensure_directory_exists};
pub fn compile_sass(base_path: &Path, output_path: &Path) -> Result<()> {
ensure_directory_exists(&output_path)?;
let sass_path = {
let mut sass_path = PathBuf::from(base_path);
sass_path.push("sass");
sass_path
};
let mut options = Options::default();
options.output_style = OutputStyle::Compressed;
let mut compiled_paths = compile_sass_glob(&sass_path, output_path, "scss", &options)?;
options.indented_syntax = true;
compiled_paths.extend(compile_sass_glob(&sass_path, output_path, "sass", &options)?);
compiled_paths.sort();
for window in compiled_paths.windows(2) {
if window[0].1 == window[1].1 {
bail!(
"SASS path conflict: \"{}\" and \"{}\" both compile to \"{}\"",
window[0].0.display(),
window[1].0.display(),
window[0].1.display(),
);
}
}
Ok(())
}
fn compile_sass_glob(
sass_path: &Path,
output_path: &Path,
extension: &str,
options: &Options,
) -> Result<Vec<(PathBuf, PathBuf)>> {
let glob_string = format!("{}/**/*.{}", sass_path.display(), extension);
let files = glob(&glob_string)
.expect("Invalid glob for sass")
.filter_map(|e| e.ok())
.filter(|entry| {
!entry.as_path().components().any(|c| c.as_os_str().to_string_lossy().starts_with('_'))
})
.collect::<Vec<_>>();
let mut compiled_paths = Vec::new();
for file in files {
let css = compile_file(&file, options.clone())?;
let path_inside_sass = file.strip_prefix(&sass_path).unwrap();
let parent_inside_sass = path_inside_sass.parent();
let css_output_path = output_path.join(path_inside_sass).with_extension("css");
if parent_inside_sass.is_some() {
create_dir_all(&css_output_path.parent().unwrap())?;
}
create_file(&css_output_path, &css)?;
compiled_paths.push((path_inside_sass.to_owned(), css_output_path));
}
Ok(compiled_paths)
}

View File

@ -85,12 +85,14 @@ pub fn find_entries<'a>(
})
.collect::<Vec<_>>();
for section in library.sections_values().iter().filter(|s| s.meta.paginate_by.is_some()) {
let number_pagers =
(section.pages.len() as f64 / section.meta.paginate_by.unwrap() as f64).ceil() as isize;
for i in 1..=number_pagers {
let permalink = format!("{}{}/{}/", section.permalink, section.meta.paginate_path, i);
sections.push(SitemapEntry::new(Cow::Owned(permalink), None))
for section in library.sections_values().iter() {
if let Some(paginate_by) = section.paginate_by() {
let number_pagers = (section.pages.len() as f64 / paginate_by as f64).ceil() as isize;
for i in 1..=number_pagers {
let permalink =
format!("{}{}/{}/", section.permalink, section.meta.paginate_path, i);
sections.push(SitemapEntry::new(Cow::Owned(permalink), None))
}
}
}
@ -138,5 +140,7 @@ pub fn find_entries<'a>(
}
}
all_sitemap_entries.into_iter().collect::<Vec<_>>()
let mut entries = all_sitemap_entries.into_iter().collect::<Vec<_>>();
entries.sort();
entries
}

105
components/site/src/tpls.rs Normal file
View File

@ -0,0 +1,105 @@
use std::path::Path;
use tera::Tera;
use crate::Site;
use config::Config;
use errors::{bail, Error, Result};
use templates::{global_fns, ZOLA_TERA};
use utils::templates::rewrite_theme_paths;
pub fn load_tera(path: &Path, config: &Config) -> Result<Tera> {
let tpl_glob =
format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*.{*ml,md}");
// Only parsing as we might be extending templates from themes and that would error
// as we haven't loaded them yet
let mut tera =
Tera::parse(&tpl_glob).map_err(|e| Error::chain("Error parsing templates", e))?;
if let Some(ref theme) = config.theme {
// Test that the templates folder exist for that theme
let theme_path = path.join("themes").join(&theme);
if !theme_path.join("templates").exists() {
bail!("Theme `{}` is missing a templates folder", theme);
}
let theme_tpl_glob = format!(
"{}/{}",
path.to_string_lossy().replace("\\", "/"),
format!("themes/{}/templates/**/*.{{*ml,md}}", theme)
);
let mut tera_theme = Tera::parse(&theme_tpl_glob)
.map_err(|e| Error::chain("Error parsing templates from themes", e))?;
rewrite_theme_paths(&mut tera_theme, &theme);
if theme_path.join("templates").join("robots.txt").exists() {
tera_theme.add_template_file(theme_path.join("templates").join("robots.txt"), None)?;
}
tera.extend(&tera_theme)?;
}
tera.extend(&ZOLA_TERA)?;
tera.build_inheritance_chains()?;
if path.join("templates").join("robots.txt").exists() {
tera.add_template_file(path.join("templates").join("robots.txt"), Some("robots.txt"))?;
}
Ok(tera)
}
/// Adds global fns that are to be available to shortcodes while rendering markdown
pub fn register_early_global_fns(site: &mut Site) {
site.tera.register_function(
"get_url",
global_fns::GetUrl::new(
site.config.clone(),
site.permalinks.clone(),
vec![site.static_path.clone(), site.output_path.clone(), site.content_path.clone()],
),
);
site.tera
.register_function("resize_image", global_fns::ResizeImage::new(site.imageproc.clone()));
site.tera.register_function(
"get_image_metadata",
global_fns::GetImageMeta::new(site.content_path.clone()),
);
site.tera.register_function("load_data", global_fns::LoadData::new(site.base_path.clone()));
site.tera.register_function("trans", global_fns::Trans::new(site.config.clone()));
site.tera.register_function(
"get_taxonomy_url",
global_fns::GetTaxonomyUrl::new(
&site.config.default_language,
&site.taxonomies,
site.config.slugify.taxonomies,
),
);
site.tera.register_function(
"get_file_hash",
global_fns::GetFileHash::new(vec![
site.static_path.clone(),
site.output_path.clone(),
site.content_path.clone(),
]),
);
}
/// Functions filled once we have parsed all the pages/sections only, so not available in shortcodes
pub fn register_tera_global_fns(site: &mut Site) {
site.tera.register_function(
"get_page",
global_fns::GetPage::new(site.base_path.clone(), site.library.clone()),
);
site.tera.register_function(
"get_section",
global_fns::GetSection::new(site.base_path.clone(), site.library.clone()),
);
site.tera.register_function(
"get_taxonomy",
global_fns::GetTaxonomy::new(
&site.config.default_language,
site.taxonomies.clone(),
site.library.clone(),
),
);
}

View File

@ -19,12 +19,12 @@ fn can_parse_site() {
let library = site.library.read().unwrap();
// Correct number of pages (sections do not count as pages, draft are ignored)
assert_eq!(library.pages().len(), 21);
assert_eq!(library.pages().len(), 32);
let posts_path = path.join("content").join("posts");
// Make sure the page with a url doesn't have any sections
let url_post = library.get_page(&posts_path.join("fixed-url.md")).unwrap();
assert_eq!(url_post.path, "a-fixed-url/");
assert_eq!(url_post.path, "/a-fixed-url/");
// Make sure the article in a folder with only asset doesn't get counted as a section
let asset_folder_post =
@ -32,12 +32,12 @@ fn can_parse_site() {
assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]);
// That we have the right number of sections
assert_eq!(library.sections().len(), 11);
assert_eq!(library.sections().len(), 12);
// And that the sections are correct
let index_section = library.get_section(&path.join("content").join("_index.md")).unwrap();
assert_eq!(index_section.subsections.len(), 4);
assert_eq!(index_section.pages.len(), 1);
assert_eq!(index_section.subsections.len(), 5);
assert_eq!(index_section.pages.len(), 3);
assert!(index_section.ancestors.is_empty());
let posts_section = library.get_section(&posts_path.join("_index.md")).unwrap();
@ -370,7 +370,7 @@ fn can_build_site_with_pagination_for_section() {
assert!(file_contains!(
public,
"posts/page/1/index.html",
"http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/posts/\""
"http-equiv=\"refresh\" content=\"0; url=https://replace-this-with-your-url.com/posts/\""
));
assert!(file_contains!(public, "posts/index.html", "Num pagers: 5"));
assert!(file_contains!(public, "posts/index.html", "Page size: 2"));
@ -446,6 +446,16 @@ fn can_build_site_with_pagination_for_section() {
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/posts/page/4/</loc>"
));
// current_path
assert!(file_contains!(public, "posts/index.html", &current_path("/posts/")));
assert!(file_contains!(public, "posts/page/2/index.html", &current_path("/posts/page/2/")));
assert!(file_contains!(public, "posts/python/index.html", &current_path("/posts/python/")));
assert!(file_contains!(
public,
"posts/tutorials/index.html",
&current_path("/posts/tutorials/")
));
}
#[test]
@ -484,7 +494,7 @@ fn can_build_site_with_pagination_for_index() {
assert!(file_contains!(
public,
"page/1/index.html",
"http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/\""
"http-equiv=\"refresh\" content=\"0; url=https://replace-this-with-your-url.com/\""
));
assert!(file_contains!(public, "page/1/index.html", "<title>Redirect</title>"));
assert!(file_contains!(
@ -492,19 +502,28 @@ fn can_build_site_with_pagination_for_index() {
"page/1/index.html",
"<a href=\"https://replace-this-with-your-url.com/\">Click here</a>"
));
assert!(file_contains!(public, "index.html", "Num pages: 1"));
assert!(file_contains!(public, "index.html", "Num pages: 2"));
assert!(file_contains!(public, "index.html", "Current index: 1"));
assert!(file_contains!(public, "index.html", "First: https://replace-this-with-your-url.com/"));
assert!(file_contains!(public, "index.html", "Last: https://replace-this-with-your-url.com/"));
assert!(file_contains!(
public,
"index.html",
"Last: https://replace-this-with-your-url.com/page/2/"
));
assert_eq!(file_contains!(public, "index.html", "has_prev"), false);
assert_eq!(file_contains!(public, "index.html", "has_next"), false);
assert_eq!(file_contains!(public, "index.html", "has_next"), true);
// sitemap contains the pager pages
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/page/1/</loc>"
))
));
// current_path
assert!(file_contains!(public, "index.html", &current_path("/")));
assert!(file_contains!(public, "page/2/index.html", &current_path("/page/2/")));
assert!(file_contains!(public, "paginated/index.html", &current_path("/paginated/")));
}
#[test]
@ -561,9 +580,9 @@ fn can_build_site_with_pagination_for_taxonomy() {
assert!(file_contains!(
public,
"tags/a/page/1/index.html",
"http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/tags/a/\""
"http-equiv=\"refresh\" content=\"0; url=https://replace-this-with-your-url.com/tags/a/\""
));
assert!(file_contains!(public, "tags/a/index.html", "Num pagers: 6"));
assert!(file_contains!(public, "tags/a/index.html", "Num pagers: 8"));
assert!(file_contains!(public, "tags/a/index.html", "Page size: 2"));
assert!(file_contains!(public, "tags/a/index.html", "Current index: 1"));
assert!(!file_contains!(public, "tags/a/index.html", "has_prev"));
@ -576,7 +595,7 @@ fn can_build_site_with_pagination_for_taxonomy() {
assert!(file_contains!(
public,
"tags/a/index.html",
"Last: https://replace-this-with-your-url.com/tags/a/page/6/"
"Last: https://replace-this-with-your-url.com/tags/a/page/8/"
));
assert_eq!(file_contains!(public, "tags/a/index.html", "has_prev"), false);
@ -584,12 +603,17 @@ fn can_build_site_with_pagination_for_taxonomy() {
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/tags/a/page/6/</loc>"
))
"<loc>https://replace-this-with-your-url.com/tags/a/page/8/</loc>"
));
// current_path
assert!(file_contains!(public, "tags/index.html", &current_path("/tags/")));
assert!(file_contains!(public, "tags/a/index.html", &current_path("/tags/a/")));
assert!(file_contains!(public, "tags/a/page/2/index.html", &current_path("/tags/a/page/2/")));
}
#[test]
fn can_build_feed() {
fn can_build_feeds() {
let (_, _tmp_dir, public) = build_site("test_site");
assert!(&public.exists());
@ -598,6 +622,14 @@ fn can_build_feed() {
assert!(file_contains!(public, "atom.xml", "Extra Syntax"));
// Next is posts/simple.md
assert!(file_contains!(public, "atom.xml", "Simple article with shortcodes"));
// Test section feeds
assert!(file_exists!(public, "posts/tutorials/programming/atom.xml"));
// It contains both sections articles
assert!(file_contains!(public, "posts/tutorials/programming/atom.xml", "Python tutorial"));
assert!(file_contains!(public, "posts/tutorials/programming/atom.xml", "Rust"));
// It doesn't contain articles from other sections
assert!(!file_contains!(public, "posts/tutorials/programming/atom.xml", "Extra Syntax"));
}
#[test]
@ -680,12 +712,47 @@ fn can_build_site_custom_builtins_from_theme() {
assert!(file_contains!(public, "404.html", "Oops"));
}
#[test]
fn can_build_site_with_html_minified() {
let (_, _tmp_dir, public) = build_site_with_setup("test_site", |mut site| {
site.config.minify_html = true;
(site, true)
});
assert!(&public.exists());
assert!(file_exists!(public, "index.html"));
assert!(file_contains!(
public,
"index.html",
"<!DOCTYPE html><html lang=en><head><meta charset=UTF-8>"
));
}
#[test]
fn can_ignore_markdown_content() {
let (_, _tmp_dir, public) = build_site("test_site");
assert!(!file_exists!(public, "posts/ignored/index.html"));
}
#[test]
fn can_cachebust_static_files() {
let (_, _tmp_dir, public) = build_site("test_site");
assert!(file_contains!(public, "index.html",
"<link href=\"https://replace-this-with-your-url.com/site.css?h=83bd983e8899946ee33d0fde18e82b04d7bca1881d10846c769b486640da3de9\" rel=\"stylesheet\">"));
}
#[test]
fn can_get_hash_for_static_files() {
let (_, _tmp_dir, public) = build_site("test_site");
assert!(file_contains!(
public,
"index.html",
"src=\"https://replace-this-with-your-url.com/scripts/hello.js\""
));
assert!(file_contains!(public, "index.html",
"integrity=\"sha384-01422f31eaa721a6c4ac8c6fa09a27dd9259e0dfcf3c7593d7810d912a9de5ca2f582df978537bcd10f76896db61fbb9\""));
}
#[test]
fn check_site() {
let (mut site, _tmp_dir, _public) = build_site("test_site");
@ -699,3 +766,8 @@ fn check_site() {
site.config.enable_check_mode();
site.load().expect("link check test_site");
}
// Follows test_site/themes/sample/templates/current_path.html
fn current_path(path: &str) -> String {
format!("[current_path]({})", path)
}

View File

@ -8,12 +8,12 @@ edition = "2018"
tera = "1"
base64 = "0.12"
lazy_static = "1"
pulldown-cmark = "0.7"
pulldown-cmark = { version = "0.8", default-features = false }
toml = "0.5"
csv = "1"
image = "0.23"
serde_json = "1.0"
sha2 = "0.8"
sha2 = "0.9"
url = "2"
errors = { path = "../errors" }
@ -21,6 +21,7 @@ utils = { path = "../utils" }
library = { path = "../library" }
config = { path = "../config" }
imageproc = { path = "../imageproc" }
svg_metadata = "0.4.1"
[dependencies.reqwest]
version = "0.10"
@ -28,4 +29,4 @@ default-features = false
features = ["blocking", "rustls-tls"]
[dev-dependencies]
mockito = "0.25"
mockito = "0.27"

View File

@ -2,13 +2,20 @@
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ lang }}">
<title>{{ config.title }}
{%- if term %} - {{ term.name }}
{%- elif section.title %} - {{ section.title }}
{%- endif -%}
</title>
{%- if config.description %}
<subtitle>{{ config.description }}</subtitle>
{%- endif %}
<link href="{{ feed_url | safe }}" rel="self" type="application/atom+xml"/>
<link href="{{ config.base_url | safe }}"/>
<link href="
{%- if section -%}
{{ section.permalink | escape_xml | safe }}
{%- else -%}
{{ config.base_url | escape_xml | safe }}
{%- endif -%}
"/>
<generator uri="https://www.getzola.org/">Zola</generator>
<updated>{{ last_updated | date(format="%+") }}</updated>
<id>{{ feed_url | safe }}</id>

View File

@ -1,6 +1,6 @@
<!doctype html>
<meta charset="utf-8">
<link rel="canonical" href="{{ url | safe }}">
<meta http-equiv="refresh" content="0;url={{ url | safe }}">
<meta http-equiv="refresh" content="0; url={{ url | safe }}">
<title>Redirect</title>
<p><a href="{{ url | safe }}">Click here</a> to be redirected.</p>

View File

@ -1,8 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>{{ config.title }}</title>
<link>{{ config.base_url | escape_xml | safe }}</link>
<title>{{ config.title }}
{%- if term %} - {{ term.name }}
{%- elif section.title %} - {{ section.title }}
{%- endif -%}
</title>
<link>{%- if section -%}
{{ section.permalink | escape_xml | safe }}
{%- else -%}
{{ config.base_url | escape_xml | safe }}
{%- endif -%}
</link>
<description>{{ config.description }}</description>
<generator>Zola</generator>
<language>{{ config.default_language }}</language>

View File

@ -19,6 +19,7 @@ pub fn markdown<S: BuildHasher>(
opts.insert(cmark::Options::ENABLE_TABLES);
opts.insert(cmark::Options::ENABLE_FOOTNOTES);
opts.insert(cmark::Options::ENABLE_STRIKETHROUGH);
opts.insert(cmark::Options::ENABLE_TASKLISTS);
let mut html = String::new();
let parser = cmark::Parser::new_ext(&s, opts);

View File

@ -209,11 +209,9 @@ impl TeraFn for LoadData {
.header(header::ACCEPT, file_format.as_accept_header())
.send()
.and_then(|res| res.error_for_status())
.map_err(|e| {
match e.status() {
Some(status) => format!("Failed to request {}: {}", url, status),
None => format!("Could not get response status for url: {}", url),
}
.map_err(|e| match e.status() {
Some(status) => format!("Failed to request {}: {}", url, status),
None => format!("Could not get response status for url: {}", url),
})?;
response
.text()

View File

@ -1,18 +1,18 @@
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock};
use std::{fs, io, result};
use sha2::{Digest, Sha256};
use sha2::{Digest, Sha256, Sha384, Sha512};
use svg_metadata as svg;
use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value};
use config::Config;
use image;
use image::GenericImageView;
use library::{Library, Taxonomy};
use utils::site::resolve_internal_link;
use imageproc;
use utils::slugs::{slugify_paths, SlugifyStrategy};
#[macro_use]
mod macros;
@ -49,16 +49,20 @@ impl TeraFn for Trans {
pub struct GetUrl {
config: Config,
permalinks: HashMap<String, String>,
content_path: PathBuf,
search_paths: Vec<PathBuf>,
}
impl GetUrl {
pub fn new(config: Config, permalinks: HashMap<String, String>, content_path: PathBuf) -> Self {
Self { config, permalinks, content_path }
pub fn new(
config: Config,
permalinks: HashMap<String, String>,
search_paths: Vec<PathBuf>,
) -> Self {
Self { config, permalinks, search_paths }
}
}
fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result<String> {
if lang == &config.default_language {
if lang == config.default_language {
return Ok(path);
}
@ -68,17 +72,48 @@ fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result<Stri
);
}
let mut splitted_path: Vec<String> = path.split(".").map(String::from).collect();
let mut splitted_path: Vec<String> = path.split('.').map(String::from).collect();
let ilast = splitted_path.len() - 1;
splitted_path[ilast] = format!("{}.{}", lang, splitted_path[ilast]);
Ok(splitted_path.join("."))
}
fn compute_file_sha256(path: &PathBuf) -> result::Result<String, io::Error> {
let mut file = fs::File::open(path)?;
fn open_file(search_paths: &[PathBuf], url: &str) -> result::Result<fs::File, io::Error> {
let cleaned_url = url.trim_start_matches("@/").trim_start_matches('/');
for base_path in search_paths {
match fs::File::open(base_path.join(cleaned_url)) {
Ok(f) => return Ok(f),
Err(_) => continue,
};
}
Err(io::Error::from(io::ErrorKind::NotFound))
}
fn compute_file_sha256(mut file: fs::File) -> result::Result<String, io::Error> {
let mut hasher = Sha256::new();
io::copy(&mut file, &mut hasher)?;
Ok(format!("{:x}", hasher.result()))
Ok(format!("{:x}", hasher.finalize()))
}
fn compute_file_sha384(mut file: fs::File) -> result::Result<String, io::Error> {
let mut hasher = Sha384::new();
io::copy(&mut file, &mut hasher)?;
Ok(format!("{:x}", hasher.finalize()))
}
fn compute_file_sha512(mut file: fs::File) -> result::Result<String, io::Error> {
let mut hasher = Sha512::new();
io::copy(&mut file, &mut hasher)?;
Ok(format!("{:x}", hasher.finalize()))
}
fn file_not_found_err(search_paths: &[PathBuf], url: &str) -> Result<Value> {
Err(format!(
"file `{}` not found; searched in{}",
url,
search_paths.iter().fold(String::new(), |acc, arg| acc + " " + arg.to_str().unwrap())
)
.into())
}
impl TeraFn for GetUrl {
@ -120,10 +155,11 @@ impl TeraFn for GetUrl {
}
if cachebust {
let full_path = self.content_path.join(&path);
permalink = match compute_file_sha256(&full_path) {
Ok(digest) => format!("{}?h={}", permalink, digest),
Err(_) => return Err(format!("Could not read file `{}`. Expected location: {}", path, full_path.to_str().unwrap()).into()),
match open_file(&self.search_paths, &path).and_then(compute_file_sha256) {
Ok(hash) => {
permalink = format!("{}?h={}", permalink, hash);
}
Err(_) => return file_not_found_err(&self.search_paths, &path),
};
}
Ok(to_value(permalink).unwrap())
@ -131,6 +167,48 @@ impl TeraFn for GetUrl {
}
}
#[derive(Debug)]
pub struct GetFileHash {
search_paths: Vec<PathBuf>,
}
impl GetFileHash {
pub fn new(search_paths: Vec<PathBuf>) -> Self {
Self { search_paths }
}
}
const DEFAULT_SHA_TYPE: u16 = 384;
impl TeraFn for GetFileHash {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
let path = required_arg!(
String,
args.get("path"),
"`get_file_hash` requires a `path` argument with a string value"
);
let sha_type = optional_arg!(
u16,
args.get("sha_type"),
"`get_file_hash`: `sha_type` must be 256, 384 or 512"
)
.unwrap_or(DEFAULT_SHA_TYPE);
let compute_hash_fn = match sha_type {
256 => compute_file_sha256,
384 => compute_file_sha384,
512 => compute_file_sha512,
_ => return Err("`get_file_hash`: `sha_type` must be 256, 384 or 512".into()),
};
let hash = open_file(&self.search_paths, &path).and_then(compute_hash_fn);
match hash {
Ok(digest) => Ok(to_value(digest).unwrap()),
Err(_) => file_not_found_err(&self.search_paths, &path),
}
}
}
#[derive(Debug)]
pub struct ResizeImage {
imageproc: Arc<Mutex<imageproc::Processor>>,
@ -211,31 +289,49 @@ impl TeraFn for GetImageMeta {
if !src_path.exists() {
return Err(format!("`get_image_metadata`: Cannot find path: {}", path).into());
}
let img = image::open(&src_path)
.map_err(|e| Error::chain(format!("Failed to process image: {}", path), e))?;
let (height, width) = image_dimensions(&src_path)?;
let mut map = tera::Map::new();
map.insert(String::from("height"), Value::Number(tera::Number::from(img.height())));
map.insert(String::from("width"), Value::Number(tera::Number::from(img.width())));
map.insert(String::from("height"), Value::Number(tera::Number::from(height)));
map.insert(String::from("width"), Value::Number(tera::Number::from(width)));
Ok(Value::Object(map))
}
}
// Try to read the image dimensions for a given image
fn image_dimensions(path: &PathBuf) -> Result<(u32, u32)> {
if let Some("svg") = path.extension().and_then(OsStr::to_str) {
let img = svg::Metadata::parse_file(&path)
.map_err(|e| Error::chain(format!("Failed to process SVG: {}", path.display()), e))?;
match (img.height(), img.width(), img.view_box()) {
(Some(h), Some(w), _) => Ok((h as u32, w as u32)),
(_, _, Some(view_box)) => Ok((view_box.height as u32, view_box.width as u32)),
_ => Err("Invalid dimensions: SVG width/height and viewbox not set.".into()),
}
} else {
let img = image::open(&path)
.map_err(|e| Error::chain(format!("Failed to process image: {}", path.display()), e))?;
Ok((img.height(), img.width()))
}
}
#[derive(Debug)]
pub struct GetTaxonomyUrl {
taxonomies: HashMap<String, HashMap<String, String>>,
default_lang: String,
slugify: SlugifyStrategy,
}
impl GetTaxonomyUrl {
pub fn new(default_lang: &str, all_taxonomies: &[Taxonomy]) -> Self {
pub fn new(default_lang: &str, all_taxonomies: &[Taxonomy], slugify: SlugifyStrategy) -> Self {
let mut taxonomies = HashMap::new();
for taxo in all_taxonomies {
let mut items = HashMap::new();
for item in &taxo.items {
items.insert(item.name.clone(), item.permalink.clone());
items.insert(slugify_paths(&item.name.clone(), slugify), item.permalink.clone());
}
taxonomies.insert(format!("{}-{}", taxo.kind.name, taxo.kind.lang), items);
}
Self { taxonomies, default_lang: default_lang.to_string() }
Self { taxonomies, default_lang: default_lang.to_string(), slugify: slugify }
}
}
impl TeraFn for GetTaxonomyUrl {
@ -265,7 +361,7 @@ impl TeraFn for GetTaxonomyUrl {
}
};
if let Some(permalink) = container.get(&name) {
if let Some(permalink) = container.get(&slugify_paths(&name, self.slugify)) {
return Ok(to_value(permalink).unwrap());
}
@ -379,7 +475,7 @@ impl TeraFn for GetTaxonomy {
#[cfg(test)]
mod tests {
use super::{GetTaxonomy, GetTaxonomyUrl, GetUrl, Trans};
use super::{GetFileHash, GetTaxonomy, GetTaxonomyUrl, GetUrl, Trans};
use std::collections::HashMap;
use std::env::temp_dir;
@ -397,20 +493,20 @@ mod tests {
use utils::slugs::SlugifyStrategy;
struct TestContext {
content_path: PathBuf,
static_path: PathBuf,
}
impl TestContext {
fn setup() -> Self {
let dir = temp_dir().join("test_global_fns");
let dir = temp_dir().join("static");
create_directory(&dir).expect("Could not create test directory");
create_file(&dir.join("app.css"), "// Hello world!")
.expect("Could not create test content (app.css)");
Self { content_path: dir }
Self { static_path: dir }
}
}
impl Drop for TestContext {
fn drop(&mut self) {
remove_dir_all(&self.content_path).expect("Could not free test directory");
remove_dir_all(&self.static_path).expect("Could not free test directory");
}
}
@ -421,7 +517,7 @@ mod tests {
#[test]
fn can_add_cachebust_to_url() {
let config = Config::default();
let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone());
let static_fn = GetUrl::new(config, HashMap::new(), vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("cachebust".to_string(), to_value(true).unwrap());
@ -431,7 +527,7 @@ mod tests {
#[test]
fn can_add_trailing_slashes() {
let config = Config::default();
let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone());
let static_fn = GetUrl::new(config, HashMap::new(), vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("trailing_slash".to_string(), to_value(true).unwrap());
@ -441,7 +537,7 @@ mod tests {
#[test]
fn can_add_slashes_and_cachebust() {
let config = Config::default();
let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone());
let static_fn = GetUrl::new(config, HashMap::new(), vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("trailing_slash".to_string(), to_value(true).unwrap());
@ -452,7 +548,7 @@ mod tests {
#[test]
fn can_link_to_some_static_file() {
let config = Config::default();
let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone());
let static_fn = GetUrl::new(config, HashMap::new(), vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/app.css");
@ -557,7 +653,9 @@ mod tests {
let tags_fr = Taxonomy { kind: taxo_config_fr, items: vec![tag_fr] };
let taxonomies = vec![tags.clone(), tags_fr.clone()];
let static_fn = GetTaxonomyUrl::new(&config.default_language, &taxonomies);
let static_fn =
GetTaxonomyUrl::new(&config.default_language, &taxonomies, config.slugify.taxonomies);
// can find it correctly
let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap());
@ -566,6 +664,16 @@ mod tests {
static_fn.call(&args).unwrap(),
to_value("http://a-website.com/tags/programming/").unwrap()
);
// can find it correctly with inconsistent capitalisation
let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap());
args.insert("name".to_string(), to_value("programming").unwrap());
assert_eq!(
static_fn.call(&args).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());
@ -639,7 +747,7 @@ title = "A title"
#[test]
fn error_when_language_not_available() {
let config = Config::parse(TRANS_CONFIG).unwrap();
let static_fn = GetUrl::new(config, HashMap::new(), TEST_CONTEXT.content_path.clone());
let static_fn = GetUrl::new(config, HashMap::new(), vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap());
args.insert("lang".to_string(), to_value("it").unwrap());
@ -662,7 +770,7 @@ title = "A title"
"a_section/a_page.en.md".to_string(),
"https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(),
);
let static_fn = GetUrl::new(config, permalinks, TEST_CONTEXT.content_path.clone());
let static_fn = GetUrl::new(config, permalinks, vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap());
args.insert("lang".to_string(), to_value("fr").unwrap());
@ -684,7 +792,7 @@ title = "A title"
"a_section/a_page.en.md".to_string(),
"https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(),
);
let static_fn = GetUrl::new(config, permalinks, TEST_CONTEXT.content_path.clone());
let static_fn = GetUrl::new(config, permalinks, vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap());
args.insert("lang".to_string(), to_value("en").unwrap());
@ -693,4 +801,47 @@ title = "A title"
"https://remplace-par-ton-url.fr/en/a_section/a_page/"
);
}
#[test]
fn can_get_file_hash_sha256() {
let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("sha_type".to_string(), to_value(256).unwrap());
assert_eq!(
static_fn.call(&args).unwrap(),
"572e691dc68c3fcd653ae463261bdb38f35dc6f01715d9ce68799319dd158840"
);
}
#[test]
fn can_get_file_hash_sha384() {
let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "141c09bd28899773b772bbe064d8b718fa1d6f2852b7eafd5ed6689d26b74883b79e2e814cd69d5b52ab476aa284c414");
}
#[test]
fn can_get_file_hash_sha512() {
let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("sha_type".to_string(), to_value(512).unwrap());
assert_eq!(static_fn.call(&args).unwrap(), "379dfab35123b9159d9e4e92dc90e2be44cf3c2f7f09b2e2df80a1b219b461de3556c93e1a9ceb3008e999e2d6a54b4f1d65ee9be9be63fa45ec88931623372f");
}
#[test]
fn error_when_file_not_found_for_hash() {
let static_fn = GetFileHash::new(vec![TEST_CONTEXT.static_path.clone()]);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("doesnt-exist").unwrap());
assert_eq!(
format!(
"file `doesnt-exist` not found; searched in {}",
TEST_CONTEXT.static_path.to_str().unwrap()
),
format!("{}", static_fn.call(&args).unwrap_err())
);
}
}

View File

@ -3,6 +3,7 @@ name = "utils"
version = "0.1.0"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
edition = "2018"
include = ["src/**/*"]
[dependencies]
tera = "1"
@ -13,7 +14,7 @@ serde = "1"
serde_derive = "1"
slug = "0.1"
percent-encoding = "2"
filetime = "0.2.8"
filetime = "0.2.12"
errors = { path = "../errors" }

View File

@ -1,6 +1,5 @@
use serde::{Deserialize, Deserializer};
use tera::{Map, Value};
use toml;
/// Used as an attribute when we want to convert from TOML to a string date
pub fn from_toml_datetime<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
@ -43,6 +42,16 @@ pub fn fix_toml_dates(table: Map<String, Value>) -> Value {
Value::Object(o) => {
new.insert(key, convert_toml_date(o));
}
Value::Array(arr) => {
let mut new_arr = Vec::with_capacity(arr.len());
for v in arr {
match v {
Value::Object(o) => new_arr.push(fix_toml_dates(o)),
_ => new_arr.push(v),
};
}
new.insert(key, Value::Array(new_arr));
}
_ => {
new.insert(key, value);
}

View File

@ -96,10 +96,6 @@ pub fn find_related_assets(path: &Path) -> Vec<PathBuf> {
/// Copy a file but takes into account where to start the copy as
/// there might be folders we need to create on the way.
/// No copy occurs if all of the following conditions are satisfied:
/// 1. A file with the same name already exists in the dest path.
/// 2. Its modification timestamp is identical to that of the src file.
/// 3. Its filesize is identical to that of the src file.
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 target_path = dest.join(relative_path);
@ -108,21 +104,33 @@ pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf, hard_link: boo
create_dir_all(parent_directory)?;
}
copy_file_if_needed(src, &target_path, hard_link)
}
/// No copy occurs if all of the following conditions are satisfied:
/// 1. A file with the same name already exists in the dest path.
/// 2. Its modification timestamp is identical to that of the src file.
/// 3. Its filesize is identical to that of the src file.
pub fn copy_file_if_needed(src: &Path, dest: &PathBuf, hard_link: bool) -> Result<()> {
if let Some(parent_directory) = dest.parent() {
create_dir_all(parent_directory)?;
}
if hard_link {
std::fs::hard_link(src, target_path)?
std::fs::hard_link(src, dest)?
} else {
let src_metadata = metadata(src)?;
let src_mtime = FileTime::from_last_modification_time(&src_metadata);
if Path::new(&target_path).is_file() {
let target_metadata = metadata(&target_path)?;
if Path::new(&dest).is_file() {
let target_metadata = metadata(&dest)?;
let target_mtime = FileTime::from_last_modification_time(&target_metadata);
if !(src_mtime == target_mtime && src_metadata.len() == target_metadata.len()) {
copy(src, &target_path)?;
set_file_mtime(&target_path, src_mtime)?;
copy(src, &dest)?;
set_file_mtime(&dest, src_mtime)?;
}
} else {
copy(src, &target_path)?;
set_file_mtime(&target_path, src_mtime)?;
copy(src, &dest)?;
set_file_mtime(&dest, src_mtime)?;
}
}
Ok(())

View File

@ -11,6 +11,12 @@ pub enum SlugifyStrategy {
Off,
}
impl Default for SlugifyStrategy {
fn default() -> Self {
SlugifyStrategy::On
}
}
fn strip_chars(s: &str, chars: &str) -> String {
let mut sanitized_string = s.to_string();
sanitized_string.retain(|c| !chars.contains(c));

View File

@ -61,52 +61,21 @@ pub fn render_template(
}
}
/// Rewrites the path from extend/macros of the theme used to ensure
/// that they will point to the right place (theme/templates/...)
/// Include is NOT supported as it would be a pain to add and using blocks
/// or macros is always better anyway for themes
/// This will also rename the shortcodes to NOT have the themes in the path
/// so themes shortcodes can be used.
pub fn rewrite_theme_paths(tera_theme: &mut Tera, site_templates: Vec<&str>, theme: &str) {
let mut shortcodes_to_move = vec![];
let mut templates = HashMap::new();
let old_templates = ::std::mem::replace(&mut tera_theme.templates, HashMap::new());
// We want to match the paths in the templates to the new names
for (key, mut tpl) in old_templates {
tpl.name = format!("{}/templates/{}", theme, tpl.name);
// First the parent if there is one
// If a template with the same name is also in site, assumes it overrides the theme one
// and do not change anything
if let Some(ref p) = tpl.parent.clone() {
if !site_templates.contains(&p.as_ref()) {
tpl.parent = Some(format!("{}/templates/{}", theme, p));
}
}
// Next the macros import
let mut updated = vec![];
for &(ref filename, ref namespace) in &tpl.imported_macro_files {
updated.push((format!("{}/templates/{}", theme, filename), namespace.to_string()));
}
tpl.imported_macro_files = updated;
if tpl.name.starts_with(&format!("{}/templates/shortcodes", theme)) {
let new_name = tpl.name.replace(&format!("{}/templates/", theme), "");
shortcodes_to_move.push((key, new_name.clone()));
tpl.name = new_name;
}
templates.insert(tpl.name.clone(), tpl);
}
tera_theme.templates = templates;
// and then replace shortcodes in the Tera instance using the new names
for (old_name, new_name) in shortcodes_to_move {
let tpl = tera_theme.templates.remove(&old_name).unwrap();
tera_theme.templates.insert(new_name, tpl);
/// Rewrites the path of duplicate templates to include the complete theme path
/// Theme templates will be injected into site templates, with higher priority for site
/// templates. To keep a copy of the template in case it's being extended from a site template
/// of the same name, we reinsert it with the theme path prepended
pub fn rewrite_theme_paths(tera_theme: &mut Tera, theme: &str) {
let theme_basepath = format!("{}/templates/", theme);
let mut new_templates = HashMap::new();
for (key, template) in &tera_theme.templates {
let mut tpl = template.clone();
tpl.name = format!("{}{}", theme_basepath, key);
new_templates.insert(tpl.name.clone(), tpl);
}
// Contrary to tera.extend, hashmap.extend does replace existing keys
// We can safely extend because there's no conflicting paths anymore
tera_theme.templates.extend(new_templates);
}
#[cfg(test)]
@ -117,7 +86,7 @@ mod tests {
#[test]
fn can_rewrite_all_paths_of_theme() {
let mut tera = Tera::parse("test-templates/*.html").unwrap();
rewrite_theme_paths(&mut tera, vec!["base.html"], "hyde");
rewrite_theme_paths(&mut tera, "hyde");
// special case to make the test work: we also rename the files to
// match the imports
for (key, val) in &tera.templates.clone() {
@ -133,7 +102,7 @@ mod tests {
);
assert_eq!(
tera.templates["hyde/templates/child.html"].parent,
Some("hyde/templates/index.html".to_string())
Some("index.html".to_string())
);
}
}

View File

@ -20,6 +20,12 @@ languages = [
If you want to use per-language taxonomies, ensure you set the `lang` field in their
configuration.
Note: By default, Chinese and Japanese search indexing is not included. You can include
the support by building `zola` using `cargo build --features search/indexing-ja search/indexing-zh`.
Please also note that, enabling Chinese indexing will increase the binary size by approximately
5 MB while enabling Japanese indexing will increase the binary size by approximately 70 MB
due to the incredibly large dictionaries.
## Content
Once the languages have been added, you can start to translate your content. Zola
uses the filename to detect the language:

View File

@ -20,3 +20,7 @@ After `zola build` or `zola serve`, you should see two files in your static dire
As each site will be different, Zola makes no assumptions about your search function and doesn't provide
the JavaScript/CSS code to do an actual search and display results. You can look at how this site
implements it to get an idea: [search.js](https://github.com/getzola/zola/tree/master/docs/static/search.js).
## Configuring the search index
In some cases, the default indexing strategy is not suitable. You can customise which fields to include and whether
to truncate the content in the [search configuration](@/documentation/getting-started/configuration.md).

View File

@ -93,6 +93,12 @@ transparent = false
# current one. This takes an array of paths, not URLs.
aliases = []
# If set to "true", a feed file will be generated for this section at the
# section's root path. This is independent of the site-wide variable of the same
# name. The section feed will only include posts from that respective feed, and
# not from any other sections, including sub-sections under that section.
generate_feed = false
# Your own data.
[extra]
```
@ -150,6 +156,7 @@ This will be sort all pages by their `weight` field, from lightest weight
page gets `page.lighter` and `page.heavier` variables that contain the
pages with lighter and heavier weights, respectively.
### Reversed sorting
When iterating through pages, you may wish to use the Tera `reverse` filter,
which reverses the order of the pages. For example, after using the `reverse` filter,
pages sorted by weight will be sorted from lightest (at the top) to heaviest
@ -158,8 +165,10 @@ to newest (at the bottom).
`reverse` has no effect on `page.later`/`page.earlier` or `page.heavier`/`page.lighter`.
If the section is paginated the `paginate_reversed=true` in the front matter of the relevant section should be set instead of using the filter.
## Sorting subsections
Sorting sections is a bit less flexible: sections are always sorted by `weight`,
Sorting sections is a bit less flexible: sections can only be sorted by `weight`,
and do not have variables that point to the heavier/lighter sections.
By default, the lightest (lowest `weight`) subsections will be at

View File

@ -3,14 +3,20 @@ title = "Shortcodes"
weight = 40
+++
Although Markdown is good for writing, it isn't great when you need write inline
HTML to add some styling for example.
To solve this, Zola borrows the concept of [shortcodes](https://codex.wordpress.org/Shortcode_API)
from WordPress.
Zola borrows the concept of [shortcodes](https://codex.wordpress.org/Shortcode_API) from WordPress.
In our case, a shortcode corresponds to a template defined in the `templates/shortcodes` directory or
a built-in one that can be used in a Markdown file. If you want to use something similar to shortcodes in your templates, try [Tera macros](https://tera.netlify.com/docs#macros).
a built-in one that can be used in a Markdown file. If you want to use something similar to shortcodes in your templates,
try [Tera macros](https://tera.netlify.com/docs#macros).
Broadly speaking, Zola's shortcodes cover two distinct use cases:
* Inject more complex HTML: Markdown is good for writing, but it isn't great when you need add inline HTML or styling.
* Ease repetitive data based tasks: when you have [external data](@/documentation/templates/overview.md#load-data) that you
want to display in your page's body.
The latter may also be solved by writing HTML, however Zola allows the use of Markdown based shortcodes which end in `.md`
rather than `.html`. This may be particularly useful if you want to include headings generated by the shortcode in the
[table of contents](@/documentation/content/table-of-contents.md).
## Writing a shortcode
Let's write a shortcode to embed YouTube videos as an example.
@ -34,12 +40,27 @@ are in an `if` statement, they are optional.
That's it. Zola will now recognise this template as a shortcode named `youtube` (the filename minus the `.html` extension).
The Markdown renderer will wrap an inline HTML node such as `<a>` or `<span>` into a paragraph.
The Markdown renderer will wrap an inline HTML node such as `<a>` or `<span>` into a paragraph.
If you want to disable this behaviour, wrap your shortcode in a `<div>`.
Shortcodes are rendered before the Markdown is parsed so they don't have access to the table of contents. Because of that,
you also cannot use the `get_page`/`get_section`/`get_taxonomy` global functions. It might work while running
`zola serve` because it has been loaded but it will fail during `zola build`.
A Markdown based shortcode in turn will be treated as if what it returned was part of the page's body. If we create
`books.md` in `templates/shortcodes` for example:
```jinja2
{% set data = load_data(path=path) -%}
{% for book in data.books %}
### {{ book.title }}
{{ book.description | safe }}
{% endfor %}
```
This will create a shortcode `books` with the argument `path` pointing to a `.toml` file where it loads lists of books with
titles and descriptions. They will flow with the rest of the document in which `books` is called.
Shortcodes are rendered before the page's Markdown is parsed so they don't have access to the page's table of contents.
Because of that, you also cannot use the `get_page`/`get_section`/`get_taxonomy` global functions. It might work while
running `zola serve` because it has been loaded but it will fail during `zola build`.
## Using shortcodes

View File

@ -54,6 +54,7 @@ Here is a full list of supported languages and their short names:
- Fortran (Modern) -> ["F03", "F08", "F90", "F95", "f03", "f08", "f90", "f95"]
- Fortran Namelist -> ["namelist"]
- Friendly Interactive Shell (fish) -> ["fish"]
- GDScript (Godot Engine) -> ["gd"]
- Generic Config -> [".dircolors", ".gitattributes", ".gitignore", ".gitmodules", ".inputrc", "Doxyfile", "cfg", "conf", "config", "dircolors", "gitattributes", "gitignore", "gitmodules", "ini", "inputrc", "mak", "mk", "pro"]
- Git Attributes -> [".gitattributes", "attributes", "gitattributes"]
- Git Commit -> ["COMMIT_EDITMSG", "MERGE_MSG", "TAG_EDITMSG"]
@ -63,6 +64,7 @@ Here is a full list of supported languages and their short names:
- Git Log -> ["gitlog"]
- Git Mailmap -> [".mailmap", "mailmap"]
- Git Rebase Todo -> ["git-rebase-todo"]
- GLSL -> ["comp", "frag", "fs", "fsh", "fshader", "geom", "glsl", "gs", "gsh", "gshader", "tesc", "tese", "vert", "vs", "vsh", "vshader"]
- Go -> ["go"]
- GraphQL -> ["gql", "graphql"]
- Graphviz (DOT) -> ["DOT", "dot", "gv"]

View File

@ -3,7 +3,7 @@ title = "Table of Contents"
weight = 60
+++
Each page/section will automatically generate a table of contents for itself based on the headers present.
Each page/section will automatically generate a table of contents for itself based on the headers generated with markdown.
It is available in the template through the `page.toc` or `section.toc` variable.
You can view the [template variables](@/documentation/templates/pages-sections.md#table-of-contents)

View File

@ -64,4 +64,4 @@ The taxonomy pages are then available at the following paths:
$BASE_URL/$NAME/ (taxonomy)
$BASE_URL/$NAME/$SLUG (taxonomy entry)
```
Note that taxonomies are case insensitive so terms that have the same slug will get merged, e.g. sections and pages containing the tag "example" will be shown in the same taxonomy page as ones containing "Example"

View File

@ -27,7 +27,7 @@ specifying the `ZOLA_VERSION` we want to use to deploy the site.
{
"build": {
"env": {
"ZOLA_VERSION": "0.11.0"
"ZOLA_VERSION": "0.12.0"
}
}
}

View File

@ -2,6 +2,6 @@
title = "Getting Started"
weight = 1
sort_by = "weight"
redirect_to = "documentation/getting-started/installation"
redirect_to = "documentation/getting-started/overview"
insert_anchor_links = "left"
+++

View File

@ -1,6 +1,6 @@
+++
title = "CLI usage"
weight = 2
weight = 15
+++
Zola only has 4 commands: `init`, `build`, `serve` and `check`.
@ -20,6 +20,8 @@ $ zola init
If the `my_site` directory already exists, Zola will only populate it if it contains only hidden files (dotfiles are ignored). If no `my_site` argument is passed, Zola will try to populate the current directory.
In case you want to attempt to populate a non-empty directory and are brave, you can use `zola init --force`. Note that this will _not_ overwrite existing folders or files; in those cases you will get a `File exists (os error 17)` error or similar.
You can initialize a git repository and a Zola site directly from within a new folder:
```bash

View File

@ -15,8 +15,9 @@ Here are the current `config.toml` sections:
1. main (unnamed)
2. link_checker
3. slugify
4. translations
5. extra
4. search
5. translations
6. extra
**Only the `base_url` variable is mandatory**. Everything else is optional. All configuration variables
used by Zola as well as their default values are listed below:
@ -84,10 +85,6 @@ languages = []
# When set to "true", the Sass files in the `sass` directory are compiled.
compile_sass = false
# When set to "true", a search index is built from the pages and section
# content for `default_language`.
build_search_index = false
# A list of glob patterns specifying asset files to ignore when the content
# directory is processed. Defaults to none, which means that all asset files are
# copied over to the `public` directory.
@ -117,6 +114,22 @@ paths = "on"
taxonomies = "on"
anchors = "on"
# When set to "true", a search index is built from the pages and section
# content for `default_language`.
build_search_index = false
[search]
# Whether to include the title of the page/section in the index
include_title = true
# Whether to include the description of the page/section in the index
include_description = false
# Whether to include the rendered content of the page/section in the index
include_content = true
# At which character to truncate the content to. Useful if you have a lot of pages and the index would
# become too big to load on the site. Defaults to not being set.
# truncate_content_length = 100
# Optional translation object. Keys should be language codes.
# Optional translation object. The key if present should be a language code.
# Example:
# default_language = "fr"
@ -175,6 +188,7 @@ Zola currently has the following highlight themes available:
- [nord](https://github.com/crabique/Nord-plist/tree/0d655b23d6b300e691676d9b90a68d92b267f7ec)
- [nyx-bold](https://github.com/GalAster/vscode-theme-nyx)
- [one-dark](https://github.com/andresmichel/one-dark-theme)
- [OneHalf](https://github.com/sonph/onehalf)
- [solarized-dark](https://tmtheme-editor.herokuapp.com/#!/editor/theme/Solarized%20(dark))
- [solarized-light](https://tmtheme-editor.herokuapp.com/#!/editor/theme/Solarized%20(light))
- [subway-madrid](https://github.com/idleberg/Subway.tmTheme)

View File

@ -70,7 +70,7 @@ $ choco install zola
Zola does not work in PowerShell ISE.
## From source
To build Zola from source, you will need to have Git, [Rust (at least 1.36) and Cargo](https://www.rust-lang.org/)
To build Zola from source, you will need to have Git, [Rust (at least 1.43) and Cargo](https://www.rust-lang.org/)
installed. You will also need to meet additional dependencies to compile [libsass](https://github.com/sass/libsass):
- OSX, Linux and other Unix-like operating systems: `make` (`gmake` on BSDs), `g++`, `libssl-dev`

View File

@ -33,6 +33,13 @@ Feeds for taxonomy terms get two more variables, using types from the
- `taxonomy`: of type `TaxonomyConfig`
- `term`: of type `TaxonomyTerm`, but without `term.pages` (use `pages` instead)
You can also enable separate feeds for each section by setting the
`generate_feed` variable to true in the respective section's front matter.
Section feeds will use the same template as indicated in the `config.toml` file.
Section feeds, in addition to the five feed template variables, get the
`section` variable from the [section
template](@/documentation/templates/pages-sections.md).
Enable feed autodiscovery allows feed readers and browsers to notify user about a RSS or Atom feed available on your web site. So it is easier for user to subscribe.
As an example this is how it looks like using [Firefox](https://en.wikipedia.org/wiki/Mozilla_Firefox) [Livemarks](https://addons.mozilla.org/en-US/firefox/addon/livemarks/?src=search) addon.

View File

@ -16,7 +16,7 @@ you can place `{{ __tera_context }}` in the template to print the whole context.
A few variables are available on all templates except feeds and the sitemap:
- `config`: the [configuration](@/documentation/getting-started/configuration.md) without any modifications
- `current_path`: the path (full URL without `base_url`) of the current page, never starting with a `/`
- `current_path`: the path (full URL without `base_url`) of the current page, always starting with a `/`
- `current_url`: the full URL for the current page
- `lang`: the language for the current page
@ -146,8 +146,27 @@ In the case of non-internal links, you can also add a cachebust of the format `?
by passing `cachebust=true` to the `get_url` function.
### 'get_file_hash`
Gets the hash digest for a static file. Supported hashes are SHA-256, SHA-384 (default) and SHA-512. Requires `path`. The `sha_type` key is optional and must be one of 256, 384 or 512.
```jinja2
{{/* get_file_hash(path="js/app.js", sha_type=256) */}}
```
This can be used to implement subresource integrity. Do note that subresource integrity is typically used when using external scripts, which `get_file_hash` does not support.
```jinja2
<script src="{{/* get_url(path="js/app.js") */}}"
integrity="sha384-{{/* get_file_hash(path="js/app.js", sha_type=384) */}}"></script>
```
Whenever hashing files, whether using `get_file_hash` or `get_url(..., cachebust=true)`, the file is searched for in three places: `static/`, `content/` and the output path (so e.g. compiled SASS can be hashed, too.)
### `get_image_metadata`
Gets metadata for an image. Currently, the only supported keys are `width` and `height`.
Gets metadata for an image. This supports common formats like JPEG, PNG, as well as SVG.
Currently, the only supported keys are `width` and `height`.
```jinja2
{% set meta = get_image_metadata(path="...") %}
@ -173,6 +192,15 @@ Gets the whole taxonomy of a specific kind.
{% set categories = get_taxonomy(kind="categories") %}
```
The type of the output is:
```ts
kind: TaxonomyConfig;
items: Array<TaxonomyTerm>;
```
See the [Taxonomies documentation](@/documentation/templates/taxonomies.md) for a full documentation of those types.
### `load_data`
Loads data from a file or URL. Supported file types include *toml*, *json* and *csv*.
Any other file type will be loaded as plain text.

View File

@ -80,7 +80,9 @@ path: String;
components: Array<String>;
permalink: String;
extra: HashMap<String, Any>;
// Pages directly in this section, sorted if asked
// Pages directly in this section. By default, the pages are not sorted. Please set the "sorted_by"
// variable in the _index.md file of the corresponding section to "date" or "weight" for sorting by
// date and weight, respectively.
pages: Array<Page>;
// Direct subsections to this section, sorted by subsections weight
// This only contains the path to use in the `get_section` Tera function to get

View File

@ -51,11 +51,6 @@ theme, with live reload working as expected.
Make sure to commit every directory (including `content`) in order for other people
to be able to build the theme from your repository.
### Caveat
Please note that [include paths](https://tera.netlify.com/docs#include) can only be used in normal templates.
Theme templates should use [macros](https://tera.netlify.com/docs#macros) instead.
## Submitting a theme to the gallery
If you want your theme to be featured in the [themes](@/themes/_index.md) section

View File

@ -0,0 +1,21 @@
+++
title = "Extending a theme"
weight = 30
+++
When your site uses a theme, you can replace parts of it in your site's templates folder. For any given theme template, you can either override a single block in it, or replace the whole template. If a site template and a theme template collide, the site template will be given priority. Whether a theme template collides or not, theme templates remain accessible from any template within `theme_name/templates/`.
## Replacing a template
When a site template and a theme template have the same path, for example `templates/page.html` and `themes/theme_name/templates/page.html`, the site template is the one that will be used. This is how you can replace a whole template for a theme.
## Overriding a block
If you don't want to replace a whole template, but override parts of it, you can [extend the template](https://tera.netlify.app/docs/#inheritance) and redefine some specific blocks. For example, if you want to override the `title` block in your theme's page.html, you can create a page.html file in your site templates with the following content:
```
{% extends "theme_name/templates/page.html" %}
{% block title %}{{ page.title }}{% endblock %}
```
If you extend `page.html` and not `theme_name/templates/page.html` specifically, it will extend the site's page template if it exists, and the theme's page template otherwise. This makes it possible to override your theme's base template(s) from your site templates, as long as the theme templates do not hardcode the theme name in template paths. For instance, children templates in the theme should use `{% extends 'index.html' %}`, not `{% extends 'theme_name/templates/index.html' %}`.

View File

@ -47,19 +47,12 @@
h1, h2, h3, h4, h5, h6 {
.zola-anchor {
font-size: 1.25rem;
visibility: hidden;
margin-left: -2rem;
margin-right: 0.75rem;
text-decoration: none;
border-bottom-color: transparent;
cursor: pointer;
}
&:hover {
.zola-anchor {
visibility: visible;
}
}
}
blockquote {

View File

@ -4,7 +4,7 @@
command = "zola build"
[build.environment]
ZOLA_VERSION = "0.8.0"
ZOLA_VERSION = "0.11.0"
[context.deploy-preview]
command = "zola build --base-url $DEPLOY_PRIME_URL"

View File

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

View File

@ -24,11 +24,15 @@ pub fn build_cli() -> App<'static, 'static> {
.subcommands(vec![
SubCommand::with_name("init")
.about("Create a new Zola project")
.arg(
.args(&[
Arg::with_name("name")
.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"),
Arg::with_name("force")
.short("f")
.takes_value(false)
.help("Force creation of project even if directory is non-empty")
]),
SubCommand::with_name("build")
.about("Deletes the output directory if there is one and builds the site")
.args(&[
@ -86,6 +90,11 @@ pub fn build_cli() -> App<'static, 'static> {
.long("open")
.takes_value(false)
.help("Open site in the default browser"),
Arg::with_name("fast")
.short("f")
.long("fast")
.takes_value(false)
.help("Only rebuild the minimum on change - useful when working on a specific page/section"),
]),
SubCommand::with_name("check")
.about("Try building the project without rendering it. Checks links")

View File

@ -1,5 +1,6 @@
use std::fs::{canonicalize, create_dir};
use std::path::Path;
use std::path::PathBuf;
use errors::{bail, Result};
use utils::fs::create_file;
@ -25,6 +26,15 @@ build_search_index = %SEARCH%
# Put all your custom variables here
"#;
// canonicalize(path) function on windows system returns a path with UNC.
// Example: \\?\C:\Users\VssAdministrator\AppData\Local\Temp\new_project
// More details on Universal Naming Convention (UNC):
// https://en.wikipedia.org/wiki/Path_(computing)#Uniform_Naming_Convention
// So the following const will be used to remove the network part of the UNC to display users a more common
// path on windows systems.
// This is a workaround until this issue https://github.com/rust-lang/rust/issues/42869 was fixed.
const LOCAL_UNC: &str = "\\\\?\\";
// 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> {
@ -56,10 +66,17 @@ pub fn is_directory_quasi_empty(path: &Path) -> Result<bool> {
Ok(false)
}
pub fn create_new_project(name: &str) -> Result<()> {
// Remove the unc part of a windows path
fn strip_unc(path: &PathBuf) -> String {
let path_to_refine = path.to_str().unwrap();
path_to_refine.trim_start_matches(LOCAL_UNC).to_string()
}
pub fn create_new_project(name: &str, force: bool) -> Result<()> {
let path = Path::new(name);
// Better error message than the rust default
if path.exists() && !is_directory_quasi_empty(&path)? {
if path.exists() && !is_directory_quasi_empty(&path)? && !force {
if name == "." {
bail!("The current directory is not an empty folder (hidden files are ignored).");
} else {
@ -89,7 +106,10 @@ pub fn create_new_project(name: &str) -> Result<()> {
populate(&path, compile_sass, &config)?;
println!();
console::success(&format!("Done! Your site was created in {:?}", canonicalize(path).unwrap()));
console::success(&format!(
"Done! Your site was created in {}",
strip_unc(&canonicalize(path).unwrap())
));
println!();
console::info(
"Get started by moving into the directory and using the built-in server: `zola serve`",
@ -119,6 +139,7 @@ mod tests {
use super::*;
use std::env::temp_dir;
use std::fs::{create_dir, remove_dir, remove_dir_all};
use std::path::Path;
#[test]
fn init_empty_directory() {
@ -224,4 +245,47 @@ mod tests {
remove_dir_all(&dir).unwrap();
}
#[test]
fn strip_unc_test() {
let mut dir = temp_dir();
dir.push("new_project");
if dir.exists() {
remove_dir_all(&dir).expect("Could not free test directory");
}
create_dir(&dir).expect("Could not create test directory");
if cfg!(target_os = "windows") {
assert_eq!(
strip_unc(&canonicalize(Path::new(&dir)).unwrap()),
"C:\\Users\\VssAdministrator\\AppData\\Local\\Temp\\new_project"
)
} else {
assert_eq!(
strip_unc(&canonicalize(Path::new(&dir)).unwrap()),
canonicalize(Path::new(&dir)).unwrap().to_str().unwrap().to_string()
);
}
remove_dir_all(&dir).unwrap();
}
// If the following test fails it means that the canonicalize function is fixed and strip_unc
// function/workaround is not anymore required.
// See issue https://github.com/rust-lang/rust/issues/42869 as a reference.
#[test]
#[cfg(target_os = "windows")]
fn strip_unc_required_test() {
let mut dir = temp_dir();
dir.push("new_project");
if dir.exists() {
remove_dir_all(&dir).expect("Could not free test directory");
}
create_dir(&dir).expect("Could not create test directory");
assert_eq!(
canonicalize(Path::new(&dir)).unwrap().to_str().unwrap(),
"\\\\?\\C:\\Users\\VssAdministrator\\AppData\\Local\\Temp\\new_project"
);
remove_dir_all(&dir).unwrap();
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
// Contains an embedded version of livereload-js 3.2.1
// Contains an embedded version of livereload-js 3.2.4
//
// Copyright (c) 2010-2012 Andrey Tarantsov
//
@ -21,9 +21,8 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
use std::env;
use std::fs::{read_dir, remove_dir_all};
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
use std::path::{Path, PathBuf};
use std::sync::mpsc::channel;
use std::thread;
use std::time::{Duration, Instant};
@ -32,21 +31,19 @@ use hyper::header;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, Server, StatusCode};
use hyper_staticfile::ResolveResult;
use tokio::io::AsyncReadExt;
use chrono::prelude::*;
use ctrlc;
use notify::{watcher, RecursiveMode, Watcher};
use ws::{Message, Sender, WebSocket};
use errors::{Error as ZolaError, Result};
use globset::GlobSet;
use site::Site;
use site::sass::compile_sass;
use site::{Site, SITE_CONTENT};
use utils::fs::copy_file;
use crate::console;
use open;
use rebuild;
use std::ffi::OsStr;
#[derive(Debug, PartialEq)]
enum ChangeKind {
@ -58,16 +55,23 @@ enum ChangeKind {
Config,
}
static INTERNAL_SERVER_ERROR_TEXT: &[u8] = b"Internal Server Error";
#[derive(Debug, PartialEq)]
enum WatchMode {
Required,
Optional,
Condition(bool),
}
static METHOD_NOT_ALLOWED_TEXT: &[u8] = b"Method Not Allowed";
static NOT_FOUND_TEXT: &[u8] = b"Not Found";
// This is dist/livereload.min.js from the LiveReload.js v3.1.0 release
// This is dist/livereload.min.js from the LiveReload.js v3.2.4 release
const LIVE_RELOAD: &str = include_str!("livereload.js");
async fn handle_request(req: Request<Body>, root: PathBuf) -> Result<Response<Body>> {
let path = req.uri().path().trim_end_matches('/').trim_start_matches('/');
// livereload.js is served using the LIVE_RELOAD str, not a file
if req.uri().path() == "/livereload.js" {
if path == "livereload.js" {
if req.method() == Method::GET {
return Ok(livereload_js());
} else {
@ -75,11 +79,16 @@ async fn handle_request(req: Request<Body>, root: PathBuf) -> Result<Response<Bo
}
}
if let Some(content) = SITE_CONTENT.read().unwrap().get(path) {
return Ok(in_memory_html(content));
}
let result = hyper_staticfile::resolve(&root, &req).await.unwrap();
match result {
ResolveResult::MethodNotMatched => return Ok(method_not_allowed()),
ResolveResult::NotFound | ResolveResult::UriNotMatched => {
return Ok(not_found(Path::new(&root.join("404.html"))).await)
let content_404 = SITE_CONTENT.read().unwrap().get("404.html").map(|x| x.clone());
return Ok(not_found(content_404));
}
_ => (),
};
@ -95,12 +104,12 @@ fn livereload_js() -> Response<Body> {
.expect("Could not build livereload.js response")
}
fn internal_server_error() -> Response<Body> {
fn in_memory_html(content: &str) -> Response<Body> {
Response::builder()
.header(header::CONTENT_TYPE, "text/plain")
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(INTERNAL_SERVER_ERROR_TEXT.into())
.expect("Could not build Internal Server Error response")
.header(header::CONTENT_TYPE, "text/html")
.status(StatusCode::OK)
.body(content.to_owned().into())
.expect("Could not build HTML response")
}
fn method_not_allowed() -> Response<Body> {
@ -111,21 +120,16 @@ fn method_not_allowed() -> Response<Body> {
.expect("Could not build Method Not Allowed response")
}
async fn not_found(page_path: &Path) -> Response<Body> {
if let Ok(mut file) = tokio::fs::File::open(page_path).await {
let mut buf = Vec::new();
if file.read_to_end(&mut buf).await.is_ok() {
return Response::builder()
.header(header::CONTENT_TYPE, "text/html")
.status(StatusCode::NOT_FOUND)
.body(buf.into())
.expect("Could not build Not Found response");
}
return internal_server_error();
fn not_found(content: Option<String>) -> Response<Body> {
if let Some(body) = content {
return Response::builder()
.header(header::CONTENT_TYPE, "text/html")
.status(StatusCode::NOT_FOUND)
.body(body.into())
.expect("Could not build Not Found response");
}
// Use a plain text response when page_path isn't available
// Use a plain text response when we can't find the body of the 404
Response::builder()
.header(header::CONTENT_TYPE, "text/plain")
.status(StatusCode::NOT_FOUND)
@ -160,30 +164,35 @@ fn rebuild_done_handling(broadcaster: &Option<Sender>, res: Result<()>, reload_p
fn create_new_site(
root_dir: &Path,
interface: &str,
port: u16,
interface_port: u16,
output_dir: &Path,
base_url: &str,
config_file: &Path,
include_drafts: bool,
ws_port: Option<u16>,
) -> Result<(Site, String)> {
let mut site = Site::new(root_dir, config_file)?;
let base_address = format!("{}:{}", base_url, port);
let address = format!("{}:{}", interface, port);
let base_address = format!("{}:{}", base_url, interface_port);
let address = format!("{}:{}", interface, interface_port);
let base_url = if site.config.base_url.ends_with('/') {
format!("http://{}/", base_address)
} else {
format!("http://{}", base_address)
};
site.config.enable_serve_mode();
site.enable_serve_mode();
site.set_base_url(base_url);
site.set_output_path(output_dir);
if include_drafts {
site.include_drafts();
}
site.load()?;
site.enable_live_reload(port);
if let Some(p) = ws_port {
site.enable_live_reload_with_port(p);
} else {
site.enable_live_reload(interface_port);
}
console::notify_site_size(&site);
console::warn_about_ignored_pages(&site);
site.build()?;
@ -193,64 +202,66 @@ fn create_new_site(
pub fn serve(
root_dir: &Path,
interface: &str,
port: u16,
interface_port: u16,
output_dir: &Path,
base_url: &str,
config_file: &Path,
watch_only: bool,
open: bool,
include_drafts: bool,
fast_rebuild: bool,
) -> Result<()> {
let start = Instant::now();
let (mut site, address) = create_new_site(
root_dir,
interface,
port,
interface_port,
output_dir,
base_url,
config_file,
include_drafts,
None,
)?;
console::report_elapsed_time(start);
// An array of (path, bool, bool) where the path should be watched for changes, and the boolean value
// indicates whether this file/folder must exist for zola serve to operate
let watch_this = vec![
("config.toml", WatchMode::Required),
("content", WatchMode::Required),
("sass", WatchMode::Condition(site.config.compile_sass)),
("static", WatchMode::Optional),
("templates", WatchMode::Optional),
("themes", WatchMode::Condition(site.config.theme.is_some())),
];
// Setup watchers
let mut watching_static = false;
let mut watching_templates = false;
let mut watching_themes = false;
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap();
watcher
.watch("content/", RecursiveMode::Recursive)
.map_err(|e| ZolaError::chain("Can't watch the `content` folder. Does it exist?", e))?;
watcher
.watch(config_file, RecursiveMode::Recursive)
.map_err(|e| ZolaError::chain("Can't watch the `config` file. Does it exist?", e))?;
if Path::new("static").exists() {
watching_static = true;
watcher
.watch("static/", RecursiveMode::Recursive)
.map_err(|e| ZolaError::chain("Can't watch the `static` folder.", e))?;
// We watch for changes on the filesystem for every entry in watch_this
// Will fail if either:
// - the path is mandatory but does not exist (eg. config.toml)
// - the path exists but has incorrect permissions
// watchers will contain the paths we're actually watching
let mut watchers = Vec::new();
for (entry, mode) in watch_this {
let watch_path = root_dir.join(entry);
let should_watch = match mode {
WatchMode::Required => true,
WatchMode::Optional => watch_path.exists(),
WatchMode::Condition(b) => b,
};
if should_watch {
watcher
.watch(root_dir.join(entry), RecursiveMode::Recursive)
.map_err(|e| ZolaError::chain(format!("Can't watch `{}` for changes in folder `{}`. Do you have correct permissions?", entry, root_dir.display()), e))?;
watchers.push(entry.to_string());
}
}
if Path::new("templates").exists() {
watching_templates = true;
watcher
.watch("templates/", RecursiveMode::Recursive)
.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
let _ = watcher.watch("sass/", RecursiveMode::Recursive);
let ws_address = format!("{}:{}", interface, site.live_reload.unwrap());
let ws_port = site.live_reload;
let ws_address = format!("{}:{}", interface, ws_port.unwrap());
let output_path = Path::new(output_dir).to_path_buf();
// output path is going to need to be moved later on, so clone it for the
@ -318,28 +329,7 @@ pub fn serve(
None
};
let pwd = env::current_dir().unwrap();
let mut watchers = vec!["content", "config.toml"];
if watching_static {
watchers.push("static");
}
if watching_templates {
watchers.push("templates");
}
if watching_themes {
watchers.push("themes");
}
if site.config.compile_sass {
watchers.push("sass");
}
println!(
"Listening for changes in {}{}{{{}}}",
pwd.display(),
MAIN_SEPARATOR,
watchers.join(", ")
);
println!("Listening for changes in {}{{{}}}", root_dir.display(), watchers.join(", "));
println!("Press Ctrl+C to stop\n");
// Delete the output folder on ctrl+C
@ -354,17 +344,6 @@ pub fn serve(
use notify::DebouncedEvent::*;
let reload_templates = |site: &mut Site, path: &Path| {
let msg = if path.is_dir() {
format!("-> Directory in `templates` folder changed {}", path.display())
} else {
format!("-> Template changed {}", path.display())
};
console::info(&msg);
// Force refresh
rebuild_done_handling(&broadcaster, rebuild::after_template_change(site, &path), "/x.js");
};
let reload_sass = |site: &Site, path: &Path, partial_path: &Path| {
let msg = if path.is_dir() {
format!("-> Directory in `sass` folder changed {}", path.display())
@ -374,11 +353,15 @@ pub fn serve(
console::info(&msg);
rebuild_done_handling(
&broadcaster,
site.compile_sass(&site.base_path),
compile_sass(&site.base_path, &site.output_path),
&partial_path.to_string_lossy(),
);
};
let reload_templates = |site: &mut Site, path: &Path| {
rebuild_done_handling(&broadcaster, site.reload_templates(), &path.to_string_lossy());
};
let copy_static = |site: &Site, path: &Path, partial_path: &Path| {
// Do nothing if the file/dir was deleted
if !path.exists() {
@ -415,11 +398,12 @@ pub fn serve(
let recreate_site = || match create_new_site(
root_dir,
interface,
port,
interface_port,
output_dir,
base_url,
config_file,
include_drafts,
ws_port,
) {
Ok((s, _)) => {
rebuild_done_handling(&broadcaster, Ok(()), "/x.js");
@ -434,13 +418,21 @@ pub fn serve(
loop {
match rx.recv() {
Ok(event) => {
let can_do_fast_reload = match event {
Remove(_) => false,
_ => true,
};
match event {
Rename(old_path, path) => {
if path.is_file() && is_temp_file(&path) {
// Intellij does weird things on edit, chmod is there to count those changes
// https://github.com/passcod/notify/issues/150#issuecomment-494912080
Rename(_, 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() {
continue;
}
let (change_kind, partial_path) = detect_change_kind(&pwd, &path);
// We only care about changes in non-empty folders
if path.is_dir() && is_folder_empty(&path) {
continue;
@ -452,79 +444,82 @@ pub fn serve(
);
let start = Instant::now();
match change_kind {
ChangeKind::Content => {
console::info(&format!("-> Content renamed {}", path.display()));
// Force refresh
rebuild_done_handling(
&broadcaster,
rebuild::after_content_rename(&mut site, &old_path, &path),
"/x.js",
);
}
ChangeKind::Templates => reload_templates(&mut site, &path),
ChangeKind::StaticFiles => copy_static(&site, &path, &partial_path),
ChangeKind::Sass => reload_sass(&site, &path, &partial_path),
ChangeKind::Themes => {
console::info(
"-> Themes changed. The whole site will be reloaded.",
);
if let Some(s) = recreate_site() {
site = s;
}
}
ChangeKind::Config => {
console::info("-> Config changed. The whole site will be reloaded. The browser needs to be refreshed to make the changes visible.");
if let Some(s) = recreate_site() {
site = s;
}
}
}
console::report_elapsed_time(start);
}
// Intellij does weird things on edit, chmod is there to count those changes
// https://github.com/passcod/notify/issues/150#issuecomment-494912080
Create(path) | Write(path) | Remove(path) | Chmod(path) => {
if is_ignored_file(&site.config.ignored_content_globset, &path) {
continue;
}
if is_temp_file(&path) || path.is_dir() {
continue;
}
println!(
"Change detected @ {}",
Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
);
let start = Instant::now();
match detect_change_kind(&pwd, &path) {
match detect_change_kind(&root_dir, &path) {
(ChangeKind::Content, _) => {
console::info(&format!("-> Content changed {}", path.display()));
// Force refresh
rebuild_done_handling(
&broadcaster,
rebuild::after_content_change(&mut site, &path),
"/x.js",
);
if fast_rebuild {
if can_do_fast_reload {
let filename = path
.file_name()
.unwrap_or_else(|| OsStr::new(""))
.to_string_lossy();
let res = if filename == "_index.md" {
site.add_and_render_section(&path)
} else if filename.ends_with(".md") {
site.add_and_render_page(&path)
} else {
// an asset changed? a folder renamed?
// should we make it smarter so it doesn't reload the whole site?
Err("dummy".into())
};
if res.is_err() {
if let Some(s) = recreate_site() {
site = s;
}
} else {
rebuild_done_handling(
&broadcaster,
res,
&path.to_string_lossy(),
);
}
} else {
// Should we be smarter than that? Is it worth it?
if let Some(s) = recreate_site() {
site = s;
}
}
} else {
if let Some(s) = recreate_site() {
site = s;
}
}
}
(ChangeKind::Templates, partial_path) => {
let msg = if path.is_dir() {
format!(
"-> Directory in `templates` folder changed {}",
path.display()
)
} else {
format!("-> Template changed {}", path.display())
};
console::info(&msg);
// A shortcode changed, we need to rebuild everything
if partial_path.starts_with("/templates/shortcodes") {
if let Some(s) = recreate_site() {
site = s;
}
} else {
println!("Reloading only template");
// A normal template changed, no need to re-render Markdown.
reload_templates(&mut site, &path)
}
}
(ChangeKind::Templates, _) => reload_templates(&mut site, &path),
(ChangeKind::StaticFiles, p) => copy_static(&site, &path, &p),
(ChangeKind::Sass, p) => reload_sass(&site, &path, &p),
(ChangeKind::Themes, _) => {
console::info(
"-> Themes changed. The whole site will be reloaded.",
);
console::info("-> Themes changed.");
if let Some(s) = recreate_site() {
site = s;
}
}
(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 browser needs to be refreshed to make the changes visible.");
if let Some(s) = recreate_site() {
site = s;

View File

@ -5,7 +5,6 @@ use std::error::Error as StdError;
use std::io::Write;
use std::time::Instant;
use atty;
use chrono::Duration;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
@ -55,7 +54,7 @@ pub fn notify_site_size(site: &Site) {
println!(
"-> Creating {} pages ({} orphan), {} sections, and processing {} images",
library.pages().len(),
site.get_number_orphan_pages(),
library.get_all_orphan_pages().len(),
library.sections().len() - 1, // -1 since we do not count the index as a section there
site.num_img_ops(),
);

View File

@ -14,7 +14,9 @@ fn main() {
let root_dir = match matches.value_of("root").unwrap() {
"." => env::current_dir().unwrap(),
path => PathBuf::from(path),
path => PathBuf::from(path)
.canonicalize()
.expect(&format!("Cannot find root directory: {}", path)),
};
let config_file = match matches.value_of("config") {
Some(path) => PathBuf::from(path),
@ -23,7 +25,8 @@ fn main() {
match matches.subcommand() {
("init", Some(matches)) => {
match cmd::create_new_project(matches.value_of("name").unwrap()) {
let force = matches.is_present("force");
match cmd::create_new_project(matches.value_of("name").unwrap(), force) {
Ok(()) => (),
Err(e) => {
console::unravel_errors("Failed to create the project", &e);
@ -61,6 +64,7 @@ fn main() {
let watch_only = matches.is_present("watch_only");
let open = matches.is_present("open");
let include_drafts = matches.is_present("drafts");
let fast = matches.is_present("fast");
// Default one
if port != 1111 && !watch_only && !port_is_available(port) {
@ -89,6 +93,7 @@ fn main() {
watch_only,
open,
include_drafts,
fast,
) {
Ok(()) => (),
Err(e) => {

@ -0,0 +1 @@
Subproject commit 96f5dcf29728aa987123321e2544330eed991a3e

@ -1 +1 @@
Subproject commit 3dd952ea771e5bc087a41146941ed36f2051c3c4
Subproject commit 44632e19af8370b39643dd60cd76deb7a13c63ee

@ -1 +1 @@
Subproject commit b98a3f3ccff0134c38544d9bc41caf7f61048cdf
Subproject commit 478d3113b82253c08052421595dbf6372120f80f

Binary file not shown.

@ -0,0 +1 @@
Subproject commit 4cd4acfffc7f2ab4f154b6ebfbbe0bb71825eb89

View File

@ -0,0 +1,663 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Name: One Half Dark
Author: Son A. Pham <sp@sonpham.me>
Url: https://github.com/sonph/onehalf
License: The MIT License (MIT)
A dark Sublime Text color scheme based on Atom's One. See
github.com/sonph/onehalf for installation instructions, a light color
scheme, and versions for other editors/terminals such as Vim or iTerm.
Red: #e06c75
Green: #98c379
Yellow: #e5c07b
Blue: #61afef
Purple: #c678dd
Cyan: #56b6c2
White: #dcdfe4
Black: #282c34
Fg: #dcdfe4
Bg: #282c34
Comment: #5c6370
Gutter foreground: #919baa
Gutter background: #282c34
Selection: #474e5d
-->
<!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>name</key>
<string>OneHalfLight</string>
<key>semanticClass</key>
<string>theme.dark.one_half_dark</string>
<key>uuid</key>
<string></string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>author</key>
<string>Son A. Pham &lt;sp@sonpham.me&gt;</string>
<key>comment</key>
<string>A dark iTerm color scheme based on Atom&apos;s One. See github.com&#x2f;sonph&#x2f;onehalf for installation instructions, a light color scheme, and versions for other editors&#x2f;terminals such as (Neo)Vim and Sublime Text.</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#dcdfe4</string>
<key>background</key>
<string>#282c34</string>
<key>bracketsOptions</key>
<string>underline</string>
<key>caret</key>
<string>#a3b3cc</string>
<key>gutter</key>
<string>#282c34</string>
<key>gutterForeground</key>
<string>#919baa</string>
<key>invisibles</key>
<string>#5c6370</string>
<key>lineHighlight</key>
<string>#313640</string>
<key>selection</key>
<string>#474e5d</string>
<key>selectionBorder</key>
<string>#474e5d</string>
<key>tagsForeground</key>
<string></string>
<key>tagsOptions</key>
<string>stippled_underline</string>
<key>bracketContentsOptions</key>
<string>underline</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comments</string>
<key>scope</key>
<string>comment</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#5c6370</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>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Delimiters</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></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></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keywords</string>
<key>scope</key>
<string>keyword</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c678dd</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variables</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</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>#61afef</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>#e5c07b</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>#e5c07b</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>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c678dd</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>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Strings, Inherited Class</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</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>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Floats</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Boolean</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constants</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>HTML: Tags</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>HTML: Tag attributes</string>
<key>scope</key>
<string>entity.other.attribute-name</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</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>#e5c07b</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>#c678dd</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: 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>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Bold</string>
<key>scope</key>
<string>markup.bold, punctuation.definition.bold</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c678dd</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Italic</string>
<key>scope</key>
<string>markup.italic, punctuation.definition.italic</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c678dd</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Code</string>
<key>scope</key>
<string>markup.raw.inline</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Link Text</string>
<key>scope</key>
<string>string.other.link, punctuation.definition.string.end.markdown</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Link Url</string>
<key>scope</key>
<string>meta.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Lists</string>
<key>scope</key>
<string>markup.list</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Quotes</string>
<key>scope</key>
<string>markup.quote</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Java Source</string>
<key>scope</key>
<string>source.java meta.class.java meta.method.java</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Java Class Body</string>
<key>scope</key>
<string>source.java meta.class.java meta.class.body.java</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Function Arguments</string>
<key>scope</key>
<string>source.js meta.function.js variable.parameter.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: New Variables</string>
<key>scope</key>
<string>source.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Variables</string>
<key>scope</key>
<string>source.js variable.other.object.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Variables in Function Calls</string>
<key>scope</key>
<string>source.js meta.function-call.method.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: New Block Variables</string>
<key>scope</key>
<string>source.js meta.block.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Block Variables</string>
<key>scope</key>
<string>source.js meta.block.js variable.other.object.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Block Variables in Function Calls</string>
<key>scope</key>
<string>source.js meta.block.js meta.function-call.method.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Function Calls</string>
<key>scope</key>
<string>source.js meta.function-call.method.js variable.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Properties</string>
<key>scope</key>
<string>source.js meta.property.object.js entity.name.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Prototypes</string>
<key>scope</key>
<string>source.js support.constant.prototype.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</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></string>
<key>foreground</key>
<string></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>#98c379</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>#e06c75</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>#e5c07b</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>#98c379</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>#56b6c2</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></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Illegal</string>
<key>scope</key>
<string>invalid.illegal</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#e06c75</string>
<key>foreground</key>
<string>#dcdfe4</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>#e5c07b</string>
<key>foreground</key>
<string>#dcdfe4</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>#e5c07b</string>
<key>foreground</key>
<string>#dcdfe4</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>#c678dd</string>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@ -0,0 +1,663 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Name: One Half Light
Author: Son A. Pham <sp@sonpham.me>
Url: https://github.com/sonph/onehalf
License: The MIT License (MIT)
A light Sublime Text color scheme based on Atom's One. See
github.com/sonph/onehalf for installation instructions, a dark color scheme,
and versions for other editors/terminals such as Vim or iTerm.
Red: #e45649
Green: #50a14f
Yellow: #c18401
Blue: #0184bc
Purple: #a626a4
Cyan: #0997b3
White: #fafafa
Black: #383a42
Fg: #383a42
Bg: #fafafa
Comment: #a0a1a7
Gutter foreground: #d4d4d4
Gutter background: #fafafa
Selection: #bfceff
-->
<!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>name</key>
<string>OneHalfLight</string>
<key>semanticClass</key>
<string>theme.light.one_half_light</string>
<key>uuid</key>
<string></string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>author</key>
<string>Son A. Pham &lt;sp@sonpham.me&gt;</string>
<key>comment</key>
<string>A light iTerm color scheme based on Atom&apos;s One. See github.com&#x2f;sonph&#x2f;onehalf for installation instructions, a dark color scheme, and versions for other editors&#x2f;terminals such as (Neo)Vim and Sublime Text.</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#383a42</string>
<key>background</key>
<string>#fafafa</string>
<key>bracketsOptions</key>
<string>underline</string>
<key>caret</key>
<string>#383a42</string>
<key>gutter</key>
<string>#fafafa</string>
<key>gutterForeground</key>
<string>#d4d4d4</string>
<key>invisibles</key>
<string>#a0a1a7</string>
<key>lineHighlight</key>
<string>#f0f0f0</string>
<key>selection</key>
<string>#bfceff</string>
<key>selectionBorder</key>
<string>#bfceff</string>
<key>tagsForeground</key>
<string></string>
<key>tagsOptions</key>
<string>stippled_underline</string>
<key>bracketContentsOptions</key>
<string>underline</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comments</string>
<key>scope</key>
<string>comment</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a0a1a7</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>#383a42</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Delimiters</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></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></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keywords</string>
<key>scope</key>
<string>keyword</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a626a4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variables</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e45649</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>#0184bc</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>#c18401</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>#c18401</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>#0184bc</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a626a4</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>#0184bc</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Strings, Inherited Class</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#50a14f</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>#c18401</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Floats</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c18401</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Boolean</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c18401</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constants</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c18401</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>HTML: Tags</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e45649</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>HTML: Tag attributes</string>
<key>scope</key>
<string>entity.other.attribute-name</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c18401</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>#c18401</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>#a626a4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: 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>#0184bc</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Bold</string>
<key>scope</key>
<string>markup.bold, punctuation.definition.bold</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a626a4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Italic</string>
<key>scope</key>
<string>markup.italic, punctuation.definition.italic</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a626a4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Code</string>
<key>scope</key>
<string>markup.raw.inline</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#50a14f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Link Text</string>
<key>scope</key>
<string>string.other.link, punctuation.definition.string.end.markdown</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Link Url</string>
<key>scope</key>
<string>meta.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#50a14f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Lists</string>
<key>scope</key>
<string>markup.list</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Quotes</string>
<key>scope</key>
<string>markup.quote</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#50a14f</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Java Source</string>
<key>scope</key>
<string>source.java meta.class.java meta.method.java</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#383a42</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Java Class Body</string>
<key>scope</key>
<string>source.java meta.class.java meta.class.body.java</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#383a42</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Function Arguments</string>
<key>scope</key>
<string>source.js meta.function.js variable.parameter.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e45649</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: New Variables</string>
<key>scope</key>
<string>source.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e45649</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Variables</string>
<key>scope</key>
<string>source.js variable.other.object.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#383a42</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Variables in Function Calls</string>
<key>scope</key>
<string>source.js meta.function-call.method.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e45649</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: New Block Variables</string>
<key>scope</key>
<string>source.js meta.block.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e45649</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Block Variables</string>
<key>scope</key>
<string>source.js meta.block.js variable.other.object.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#383a42</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Block Variables in Function Calls</string>
<key>scope</key>
<string>source.js meta.block.js meta.function-call.method.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#383a42</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Function Calls</string>
<key>scope</key>
<string>source.js meta.function-call.method.js variable.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#383a42</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Properties</string>
<key>scope</key>
<string>source.js meta.property.object.js entity.name.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#0184bc</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Prototypes</string>
<key>scope</key>
<string>source.js support.constant.prototype.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#383a42</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></string>
<key>foreground</key>
<string></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>#98c379</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>#e06c75</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>#e5c07b</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>#50a14f</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>#0997b3</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></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Illegal</string>
<key>scope</key>
<string>invalid.illegal</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#e06c75</string>
<key>foreground</key>
<string>#fafafa</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>#e5c07b</string>
<key>foreground</key>
<string>#fafafa</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>#e5c07b</string>
<key>foreground</key>
<string>#fafafa</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>#c678dd</string>
<key>foreground</key>
<string>#fafafa</string>
</dict>
</dict>
</array>
</dict>
</plist>

Binary file not shown.

View File

@ -0,0 +1,160 @@
<?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>name</key>
<string>Green</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#1B1D16</string>
<key>caret</key>
<string>#F8FFE2</string>
<key>foreground</key>
<string>#D7FF68</string>
<key>invisibles</key>
<string>#808080</string>
<key>lineHighlight</key>
<string>#2B321C</string>
<key>selection</key>
<string>#3C4822</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comment</string>
<key>scope</key>
<string>comment</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#738939</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword</string>
<key>scope</key>
<string>keyword, storage</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#9DC443</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Number</string>
<key>scope</key>
<string>constant.numeric</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#E6FFA4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#E6FFA4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#E6FFA4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#990000</string>
<key>foreground</key>
<string>#FFFFFF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Diff Header</string>
<key>scope</key>
<string>meta.diff.header, meta.separator.diff, meta.diff.index, meta.diff.range</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#2F33AB</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.deleted</string>
<key>scope</key>
<string>markup.deleted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#F92672</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.inserted</string>
<key>scope</key>
<string>markup.inserted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#A6E22E</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>diff.changed</string>
<key>scope</key>
<string>markup.changed</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#967EFB</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>15675CF3-9DE0-420B-8863-DDF5AFA1D7CA</string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>semanticClass</key>
<string>theme.dark.green</string>
</dict>
</plist>

View File

@ -0,0 +1,726 @@
<?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>author</key>
<string>Chris Kempson (http:&#x2f;&#x2f;chriskempson.com)</string>
<key>name</key>
<string>RailsBase16 Green Screen Dark</string>
<key>semanticClass</key>
<string>theme.dark.rails_base16_green_screen_dark</string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>gutterSettings</key>
<dict>
<key>background</key>
<string>#003300</string>
<key>divider</key>
<string>#003300</string>
<key>foreground</key>
<string>#007700</string>
<key>selectionBackground</key>
<string>#005500</string>
<key>selectionForeground</key>
<string>#009900</string>
</dict>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#001100</string>
<key>caret</key>
<string>#00bb00</string>
<key>foreground</key>
<string>#00bb00</string>
<key>invisibles</key>
<string>#007700</string>
<key>lineHighlight</key>
<string>#003300</string>
<key>selection</key>
<string>#005500</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Text</string>
<key>scope</key>
<string>variable.parameter.function, meta.function.parameters variable.parameter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00bb00</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>#007700</string>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation</string>
<key>scope</key>
<string>punctuation.definition.variable, punctuation.definition.parameters, punctuation.definition.array, punctuation.definition.constant, punctuation.definition.keyword, punctuation.definition.entity, punctuationctuation.definition.entity.css, punctuation.section.function, punctuation.separator</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00bb00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Delimiters</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00bb00</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>#005500</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keywords</string>
<key>scope</key>
<string>keyword</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00bb00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variables</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</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>#009900</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Functions Invocations (Elixir)</string>
<key>scope</key>
<string>source.elixir entity.name.function.elixir</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00bb00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Classes</string>
<key>scope</key>
<string>support.class, entity.name.class, entity.name.type.class, entity.name.module, entity.name.type.module</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Inherited Class</string>
<key>scope</key>
<string>entity.other.inherited-class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Class and Module Namespace separators</string>
<key>scope</key>
<string>meta.module entity.name.module punctuation.accessor.ruby, meta.class entity.name.class punctuation.accessor.ruby, meta.class entity.other.inherited-class punctuation.accessor.ruby</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00bb00</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>#00ff00</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>#009900</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00bb00</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>#005500</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Builtin Function</string>
<key>scope</key>
<string>support.function.builtin</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00bb00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Strings</string>
<key>scope</key>
<string>string, string source string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00bb00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Symbols</string>
<key>scope</key>
<string>constant.other.symbol</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#009900</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>#009900</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Floats</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#009900</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Boolean</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#009900</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constants</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#009900</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>#007700</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Haml Tags</string>
<key>scope</key>
<string>entity.name.tag.haml</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Haml Class</string>
<key>scope</key>
<string>entity.name.tag.class.haml</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Haml Id</string>
<key>scope</key>
<string>entity.name.tag.id.haml</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#009900</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>#009900</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Attribute IDs</string>
<key>scope</key>
<string>entity.other.attribute-name.id</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#009900</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>#00bb00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Values</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#009900</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>#009900</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>#007700</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>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#007700</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>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#00bb00</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>#00bb00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Link Text</string>
<key>scope</key>
<string>string.other.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</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>#009900</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>#007700</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>#009900</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>#005500</string>
<key>foreground</key>
<string>#00bb00</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>#00bb00</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>#007700</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>#00bb00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Colors</string>
<key>scope</key>
<string>constant.other.color, punctuation.definition.constant.scss </string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#005500</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>#005500</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>#005500</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Embedded</string>
<key>scope</key>
<string>variable.interpolation</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#005500</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid.illegal</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#007700</string>
<key>foreground</key>
<string>#001100</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Embedded Source</string>
<key>scope</key>
<string>string source, text source, source meta.interpolation source</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00bb00</string>
<key>background</key>
<string>#003300</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>ERB tags</string>
<key>scope</key>
<string>text source punctuation.section.embedded, text string source punctuation.section.embedded</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>EEx tags</string>
<key>scope</key>
<string>
text meta.embedded punctuation.section.embedded.begin,
text meta.embedded punctuation.section.embedded.end,
text meta.embedded punctuation.section.embedded.end.elixir source.elixir
</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</string>
<key>background</key>
<string>#003300</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String Interpolation Chars (Ruby)</string>
<key>scope</key>
<string>source.ruby meta.interpolation punctuation.section.interpolation.begin, source.ruby meta.interpolation punctuation.section.interpolation.end</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00ff00</string>
<key>background</key>
<string>#003300</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String Interpolation Chars (Elixir)</string>
<key>scope</key>
<string>source.elixir string punctuation.section.embedded.begin, source.elixir string punctuation.section.embedded.end</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00ff00</string>
<key>background</key>
<string>#003300</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>SCSS variable</string>
<key>scope</key>
<string>variable.scss</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support Constant</string>
<key>scope</key>
<string>support.constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#009900</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Block Variables</string>
<key>scope</key>
<string>variable.other.block, meta.block.parameters variable.parameter.ruby</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#00ff00</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constant Variables</string>
<key>scope</key>
<string>meta.constant entity.name.constant.ruby, variable.other.constant.ruby</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Elixir Protocol Names</string>
<key>scope</key>
<string>entity.name.type.protocol.elixir</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#007700</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>26a61b83-b1a6-495a-9f68-157b17ec3d74</string>
</dict>
</plist>

View File

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

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