commit
2d1c954322
34
.dockerignore
Normal file
34
.dockerignore
Normal 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
1
.gitignore
vendored
|
@ -27,3 +27,4 @@ stage
|
|||
shell.nix
|
||||
# vim temporary files
|
||||
**/.*.sw*
|
||||
.swp
|
||||
|
|
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -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
|
||||
|
|
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -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 aren’t 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
|
||||
|
||||
|
|
|
@ -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
1089
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
11
Cargo.toml
11
Cargo.toml
|
@ -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",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
|
|
16
components/config/src/config/languages.rs
Normal file
16
components/config/src/config/languages.rs
Normal 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>;
|
16
components/config/src/config/link_checker.rs
Normal file
16
components/config/src/config/link_checker.rs
Normal 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() }
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
27
components/config/src/config/search.rs
Normal file
27
components/config/src/config/search.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
11
components/config/src/config/slugify.rs
Normal file
11
components/config/src/config/slugify.rs
Normal 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,
|
||||
}
|
35
components/config/src/config/taxonomies.rs
Normal file
35
components/config/src/config/taxonomies.rs
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -8,4 +8,4 @@ edition = "2018"
|
|||
tera = "1"
|
||||
toml = "0.5"
|
||||
image = "0.23"
|
||||
syntect = "4.1"
|
||||
syntect = "4.4"
|
||||
|
|
|
@ -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()) }
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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#"
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
|
|
|
@ -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(§ion.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),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: §ion.pages,
|
||||
all_pages: Cow::from(§ion.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(§ion, &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(§ion, &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(§ion, &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(§ion, &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(§ion, &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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,4 +16,4 @@ default-features = false
|
|||
features = ["blocking", "rustls-tls"]
|
||||
|
||||
[dev-dependencies]
|
||||
mockito = "0.25"
|
||||
mockito = "0.27"
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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"
|
|
@ -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, it’s rendering the wrong
|
||||
// content into the root feed, and it’s 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(¤t, &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]);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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(
|
||||
§ion.permalink,
|
||||
&[
|
||||
§ion.meta.title.clone().unwrap_or_default(),
|
||||
&AMMONIA.clean(§ion.content).to_string(),
|
||||
],
|
||||
&fill_index(config, §ion.meta.title, §ion.meta.description, §ion.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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
79
components/site/src/feed.rs
Normal file
79
components/site/src/feed.rs
Normal 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
172
components/site/src/link_checking.rs
Normal file
172
components/site/src/link_checking.rs
Normal 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 })
|
||||
}
|
73
components/site/src/sass.rs
Normal file
73
components/site/src/sass.rs
Normal 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)
|
||||
}
|
|
@ -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
105
components/site/src/tpls.rs
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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", ¤t_path("/posts/")));
|
||||
assert!(file_contains!(public, "posts/page/2/index.html", ¤t_path("/posts/page/2/")));
|
||||
assert!(file_contains!(public, "posts/python/index.html", ¤t_path("/posts/python/")));
|
||||
assert!(file_contains!(
|
||||
public,
|
||||
"posts/tutorials/index.html",
|
||||
¤t_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", ¤t_path("/")));
|
||||
assert!(file_contains!(public, "page/2/index.html", ¤t_path("/page/2/")));
|
||||
assert!(file_contains!(public, "paginated/index.html", ¤t_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", ¤t_path("/tags/")));
|
||||
assert!(file_contains!(public, "tags/a/index.html", ¤t_path("/tags/a/")));
|
||||
assert!(file_contains!(public, "tags/a/page/2/index.html", ¤t_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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
@ -37,9 +43,24 @@ That's it. Zola will now recognise this template as a shortcode named `youtube`
|
|||
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
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
+++
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
21
docs/content/documentation/themes/extending-a-theme.md
Normal file
21
docs/content/documentation/themes/extending-a-theme.md
Normal 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' %}`.
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
15
src/cli.rs
15
src/cli.rs
|
@ -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")
|
||||
|
|
|
@ -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
333
src/cmd/serve.rs
333
src/cmd/serve.rs
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
1
sublime/syntaxes/GDScript-sublime
Submodule
1
sublime/syntaxes/GDScript-sublime
Submodule
|
@ -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.
1
sublime/syntaxes/sublime-glsl
Submodule
1
sublime/syntaxes/sublime-glsl
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 4cd4acfffc7f2ab4f154b6ebfbbe0bb71825eb89
|
663
sublime/themes/OneHalfDark.tmTheme
Normal file
663
sublime/themes/OneHalfDark.tmTheme
Normal 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 <sp@sonpham.me></string>
|
||||
<key>comment</key>
|
||||
<string>A dark iTerm 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 (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>
|
663
sublime/themes/OneHalfLight.tmTheme
Normal file
663
sublime/themes/OneHalfLight.tmTheme
Normal 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 <sp@sonpham.me></string>
|
||||
<key>comment</key>
|
||||
<string>A light iTerm 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 (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.
160
sublime/themes/green.tmTheme
Normal file
160
sublime/themes/green.tmTheme
Normal 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>
|
726
sublime/themes/railsbase16-green-screen-dark.tmTheme
Normal file
726
sublime/themes/railsbase16-green-screen-dark.tmTheme
Normal 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://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>
|
|
@ -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
Loading…
Reference in a new issue