@@ -16,7 +16,7 @@ matrix: | |||||
# The earliest stable Rust version that works | # The earliest stable Rust version that works | ||||
- env: TARGET=x86_64-unknown-linux-gnu | - env: TARGET=x86_64-unknown-linux-gnu | ||||
rust: 1.29.0 | |||||
rust: 1.30.0 | |||||
before_install: set -e | before_install: set -e | ||||
@@ -1,5 +1,14 @@ | |||||
# Changelog | # Changelog | ||||
## 0.5.1 (2018-12-14) | |||||
- Fix deleting markdown file in `zola serve` | |||||
- Fix pagination for taxonomies being broken and add missing documentation for it | |||||
- Add missing pager pages from the sitemap | |||||
- Allow and parse full RFC339 datetimes in filenames | |||||
- Live reload is now enabled for the 404 page on serve | |||||
## 0.5.0 (2018-11-17) | ## 0.5.0 (2018-11-17) | ||||
### Breaking | ### Breaking | ||||
@@ -1,6 +1,6 @@ | |||||
[package] | [package] | ||||
name = "zola" | name = "zola" | ||||
version = "0.5.0" | |||||
version = "0.5.1" | |||||
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"] | ||||
license = "MIT" | license = "MIT" | ||||
readme = "README.md" | readme = "README.md" | ||||
@@ -37,6 +37,14 @@ impl Taxonomy { | |||||
false | false | ||||
} | } | ||||
} | } | ||||
pub fn paginate_path(&self) -> &str { | |||||
if let Some(ref path) = self.paginate_path { | |||||
path | |||||
} else { | |||||
"page" | |||||
} | |||||
} | |||||
} | } | ||||
impl Default for Taxonomy { | impl Default for Taxonomy { | ||||
@@ -20,8 +20,11 @@ use content::file_info::FileInfo; | |||||
use content::ser::SerializingPage; | use content::ser::SerializingPage; | ||||
lazy_static! { | lazy_static! { | ||||
// Check whether a string starts with yyyy-mm-dd{-,_} | |||||
static ref DATE_IN_FILENAME: Regex = Regex::new(r"^^([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))(_|-)").unwrap(); | |||||
// Based on https://regex101.com/r/H2n38Z/1/tests | |||||
// A regex parsing RFC3339 date followed by {_,-}, some characters and ended by .md | |||||
static ref RFC3339_DATE: Regex = Regex::new( | |||||
r"^(?P<datetime>(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])(T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9])))?)(_|-)(?P<slug>.+$)" | |||||
).unwrap(); | |||||
} | } | ||||
#[derive(Clone, Debug, PartialEq)] | #[derive(Clone, Debug, PartialEq)] | ||||
@@ -113,11 +116,11 @@ impl Page { | |||||
page.word_count = Some(word_count); | page.word_count = Some(word_count); | ||||
page.reading_time = Some(reading_time); | page.reading_time = Some(reading_time); | ||||
let mut has_date_in_name = false; | |||||
if DATE_IN_FILENAME.is_match(&page.file.name) { | |||||
has_date_in_name = true; | |||||
let mut slug_from_dated_filename = None; | |||||
if let Some(ref caps) = RFC3339_DATE.captures(&page.file.name.replace(".md", "")) { | |||||
slug_from_dated_filename = Some(caps.name("slug").unwrap().as_str().to_string()); | |||||
if page.meta.date.is_none() { | if page.meta.date.is_none() { | ||||
page.meta.date = Some(page.file.name[..10].to_string()); | |||||
page.meta.date = Some(caps.name("datetime").unwrap().as_str().to_string()); | |||||
page.meta.date_to_datetime(); | page.meta.date_to_datetime(); | ||||
} | } | ||||
} | } | ||||
@@ -132,9 +135,8 @@ impl Page { | |||||
slugify(&page.file.name) | slugify(&page.file.name) | ||||
} | } | ||||
} else { | } else { | ||||
if has_date_in_name { | |||||
// skip the date + the {_,-} | |||||
slugify(&page.file.name[11..]) | |||||
if let Some(slug) = slug_from_dated_filename { | |||||
slugify(&slug) | |||||
} else { | } else { | ||||
slugify(&page.file.name) | slugify(&page.file.name) | ||||
} | } | ||||
@@ -507,7 +509,7 @@ Hello world | |||||
} | } | ||||
#[test] | #[test] | ||||
fn can_get_date_from_filename() { | |||||
fn can_get_date_from_short_date_in_filename() { | |||||
let config = Config::default(); | let config = Config::default(); | ||||
let content = r#" | let content = r#" | ||||
+++ | +++ | ||||
@@ -523,6 +525,23 @@ Hello world | |||||
assert_eq!(page.slug, "hello"); | assert_eq!(page.slug, "hello"); | ||||
} | } | ||||
#[test] | |||||
fn can_get_date_from_full_rfc3339_date_in_filename() { | |||||
let config = Config::default(); | |||||
let content = r#" | |||||
+++ | |||||
+++ | |||||
Hello world | |||||
<!-- more -->"# | |||||
.to_string(); | |||||
let res = Page::parse(Path::new("2018-10-02T15:00:00Z-hello.md"), &content, &config); | |||||
assert!(res.is_ok()); | |||||
let page = res.unwrap(); | |||||
assert_eq!(page.meta.date, Some("2018-10-02T15:00:00Z".to_string())); | |||||
assert_eq!(page.slug, "hello"); | |||||
} | |||||
#[test] | #[test] | ||||
fn frontmatter_date_override_filename_date() { | fn frontmatter_date_override_filename_date() { | ||||
let config = Config::default(); | let config = Config::default(); | ||||
@@ -80,7 +80,7 @@ impl Section { | |||||
pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Section> { | pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Section> { | ||||
let (meta, content) = split_section_content(file_path, content)?; | let (meta, content) = split_section_content(file_path, content)?; | ||||
let mut section = Section::new(file_path, meta); | let mut section = Section::new(file_path, meta); | ||||
section.raw_content = content.clone(); | |||||
section.raw_content = content; | |||||
let (word_count, reading_time) = get_reading_analytics(§ion.raw_content); | let (word_count, reading_time) = get_reading_analytics(§ion.raw_content); | ||||
section.word_count = Some(word_count); | section.word_count = Some(word_count); | ||||
section.reading_time = Some(reading_time); | section.reading_time = Some(reading_time); | ||||
@@ -14,7 +14,7 @@ use taxonomies::{Taxonomy, TaxonomyItem}; | |||||
#[derive(Clone, Debug, PartialEq)] | #[derive(Clone, Debug, PartialEq)] | ||||
enum PaginationRoot<'a> { | enum PaginationRoot<'a> { | ||||
Section(&'a Section), | Section(&'a Section), | ||||
Taxonomy(&'a Taxonomy), | |||||
Taxonomy(&'a Taxonomy, &'a TaxonomyItem), | |||||
} | } | ||||
/// A list of all the pages in the paginator with their index and links | /// A list of all the pages in the paginator with their index and links | ||||
@@ -93,14 +93,14 @@ impl<'a> Paginator<'a> { | |||||
all_pages: &item.pages, | all_pages: &item.pages, | ||||
pagers: Vec::with_capacity(item.pages.len() / paginate_by), | pagers: Vec::with_capacity(item.pages.len() / paginate_by), | ||||
paginate_by, | paginate_by, | ||||
root: PaginationRoot::Taxonomy(taxonomy), | |||||
root: PaginationRoot::Taxonomy(taxonomy, item), | |||||
permalink: item.permalink.clone(), | permalink: item.permalink.clone(), | ||||
path: format!("{}/{}", taxonomy.kind.name, item.slug), | path: format!("{}/{}", taxonomy.kind.name, item.slug), | ||||
paginate_path: taxonomy | paginate_path: taxonomy | ||||
.kind | .kind | ||||
.paginate_path | .paginate_path | ||||
.clone() | .clone() | ||||
.unwrap_or_else(|| "pages".to_string()), | |||||
.unwrap_or_else(|| "page".to_string()), | |||||
is_index: false, | is_index: false, | ||||
template: format!("{}/single.html", taxonomy.kind.name), | template: format!("{}/single.html", taxonomy.kind.name), | ||||
}; | }; | ||||
@@ -212,8 +212,9 @@ impl<'a> Paginator<'a> { | |||||
context | context | ||||
.insert("section", &SerializingSection::from_section_basic(s, Some(library))); | .insert("section", &SerializingSection::from_section_basic(s, Some(library))); | ||||
} | } | ||||
PaginationRoot::Taxonomy(t) => { | |||||
PaginationRoot::Taxonomy(t, item) => { | |||||
context.insert("taxonomy", &t.kind); | context.insert("taxonomy", &t.kind); | ||||
context.insert("term", &item.serialize(library)); | |||||
} | } | ||||
}; | }; | ||||
context.insert("current_url", &pager.permalink); | context.insert("current_url", &pager.permalink); | ||||
@@ -349,7 +350,7 @@ mod tests { | |||||
assert_eq!(paginator.pagers[1].index, 2); | assert_eq!(paginator.pagers[1].index, 2); | ||||
assert_eq!(paginator.pagers[1].pages.len(), 1); | assert_eq!(paginator.pagers[1].pages.len(), 1); | ||||
assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/tags/something/pages/2/"); | |||||
assert_eq!(paginator.pagers[1].path, "tags/something/pages/2/"); | |||||
assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/tags/something/page/2/"); | |||||
assert_eq!(paginator.pagers[1].path, "tags/something/page/2/"); | |||||
} | } | ||||
} | } |
@@ -13,7 +13,7 @@ use library::Library; | |||||
use sorting::sort_pages_by_date; | use sorting::sort_pages_by_date; | ||||
#[derive(Debug, Clone, PartialEq, Serialize)] | #[derive(Debug, Clone, PartialEq, Serialize)] | ||||
struct SerializedTaxonomyItem<'a> { | |||||
pub struct SerializedTaxonomyItem<'a> { | |||||
name: &'a str, | name: &'a str, | ||||
slug: &'a str, | slug: &'a str, | ||||
permalink: &'a str, | permalink: &'a str, | ||||
@@ -71,6 +71,10 @@ impl TaxonomyItem { | |||||
TaxonomyItem { name: name.to_string(), permalink, slug, pages } | TaxonomyItem { name: name.to_string(), permalink, slug, pages } | ||||
} | } | ||||
pub fn serialize<'a>(&'a self, library: &'a Library) -> SerializedTaxonomyItem<'a> { | |||||
SerializedTaxonomyItem::from_item(self, library) | |||||
} | |||||
} | } | ||||
#[derive(Debug, Clone, PartialEq, Serialize)] | #[derive(Debug, Clone, PartialEq, Serialize)] | ||||
@@ -311,7 +311,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { | |||||
if is_md { | if is_md { | ||||
// only delete if it was able to be added in the first place | // only delete if it was able to be added in the first place | ||||
if !index.exists() && !path.exists() { | if !index.exists() && !path.exists() { | ||||
delete_element(site, path, is_section)?; | |||||
return delete_element(site, path, is_section); | |||||
} | } | ||||
// Added another .md in a assets directory | // Added another .md in a assets directory | ||||
@@ -352,6 +352,7 @@ pub fn after_template_change(site: &mut Site, path: &Path) -> Result<()> { | |||||
site.render_orphan_pages() | site.render_orphan_pages() | ||||
} | } | ||||
"section.html" => site.render_sections(), | "section.html" => site.render_sections(), | ||||
"404.html" => site.render_404(), | |||||
// Either the index or some unknown template changed | // Either the index or some unknown template changed | ||||
// We can't really know what this change affects so rebuild all | // We can't really know what this change affects so rebuild all | ||||
// the things | // the things | ||||
@@ -228,9 +228,22 @@ fn can_rebuild_after_renaming_section_folder() { | |||||
fn can_rebuild_after_renaming_non_md_asset_in_colocated_folder() { | fn can_rebuild_after_renaming_non_md_asset_in_colocated_folder() { | ||||
let tmp_dir = tempdir().expect("create temp dir"); | let tmp_dir = tempdir().expect("create temp dir"); | ||||
let (site_path, mut site) = load_and_build_site!(tmp_dir); | let (site_path, mut site) = load_and_build_site!(tmp_dir); | ||||
let (old_path, new_path) = rename!(site_path, "content/posts/with-assets/zola.png", "gutenberg.png"); | |||||
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 | // 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); | let res = after_content_rename(&mut site, &old_path, &new_path); | ||||
assert!(res.is_ok()); | 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); | |||||
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()); | |||||
} |
@@ -31,7 +31,7 @@ pub struct Rendered { | |||||
// means we will have example, example-1, example-2 etc | // means we will have example, example-1, example-2 etc | ||||
fn find_anchor(anchors: &[String], name: String, level: u8) -> String { | fn find_anchor(anchors: &[String], name: String, level: u8) -> String { | ||||
if level == 0 && !anchors.contains(&name) { | if level == 0 && !anchors.contains(&name) { | ||||
return name.to_string(); | |||||
return name; | |||||
} | } | ||||
let new_anchor = format!("{}-{}", name, level + 1); | let new_anchor = format!("{}-{}", name, level + 1); | ||||
@@ -1,7 +1,7 @@ | |||||
use pest::iterators::Pair; | use pest::iterators::Pair; | ||||
use pest::Parser; | use pest::Parser; | ||||
use tera::{to_value, Context, Map, Value}; | |||||
use regex::Regex; | use regex::Regex; | ||||
use tera::{to_value, Context, Map, Value}; | |||||
use context::RenderContext; | use context::RenderContext; | ||||
use errors::{Result, ResultExt}; | use errors::{Result, ResultExt}; | ||||
@@ -20,9 +20,9 @@ lazy_static! { | |||||
fn replace_string_markers(input: &str) -> String { | fn replace_string_markers(input: &str) -> String { | ||||
match input.chars().next().unwrap() { | match input.chars().next().unwrap() { | ||||
'"' => input.replace('"', "").to_string(), | |||||
'\'' => input.replace('\'', "").to_string(), | |||||
'`' => input.replace('`', "").to_string(), | |||||
'"' => input.replace('"', ""), | |||||
'\'' => input.replace('\'', ""), | |||||
'`' => input.replace('`', ""), | |||||
_ => unreachable!("How did you even get there"), | _ => unreachable!("How did you even get there"), | ||||
} | } | ||||
} | } | ||||
@@ -627,10 +627,8 @@ impl Site { | |||||
ensure_directory_exists(&self.output_path)?; | ensure_directory_exists(&self.output_path)?; | ||||
let mut context = Context::new(); | let mut context = Context::new(); | ||||
context.insert("config", &self.config); | context.insert("config", &self.config); | ||||
create_file( | |||||
&self.output_path.join("404.html"), | |||||
&render_template("404.html", &self.tera, &context, &self.config.theme)?, | |||||
) | |||||
let output = render_template("404.html", &self.tera, &context, &self.config.theme)?; | |||||
create_file(&self.output_path.join("404.html"), &self.inject_livereload(output)) | |||||
} | } | ||||
/// Renders robots.txt | /// Renders robots.txt | ||||
@@ -646,7 +644,6 @@ impl Site { | |||||
/// Renders all taxonomies with at least one non-draft post | /// Renders all taxonomies with at least one non-draft post | ||||
pub fn render_taxonomies(&self) -> Result<()> { | pub fn render_taxonomies(&self) -> Result<()> { | ||||
// TODO: make parallel? | |||||
for taxonomy in &self.taxonomies { | for taxonomy in &self.taxonomies { | ||||
self.render_taxonomy(taxonomy)?; | self.render_taxonomy(taxonomy)?; | ||||
} | } | ||||
@@ -669,24 +666,26 @@ impl Site { | |||||
.items | .items | ||||
.par_iter() | .par_iter() | ||||
.map(|item| { | .map(|item| { | ||||
if taxonomy.kind.rss { | |||||
self.render_rss_feed( | |||||
item.pages.iter().map(|p| self.library.get_page_by_key(*p)).collect(), | |||||
Some(&PathBuf::from(format!("{}/{}", taxonomy.kind.name, item.slug))), | |||||
)?; | |||||
} | |||||
let path = output_path.join(&item.slug); | |||||
if taxonomy.kind.is_paginated() { | if taxonomy.kind.is_paginated() { | ||||
self.render_paginated( | self.render_paginated( | ||||
&output_path, | |||||
&path, | |||||
&Paginator::from_taxonomy(&taxonomy, item, &self.library), | &Paginator::from_taxonomy(&taxonomy, item, &self.library), | ||||
) | |||||
)?; | |||||
} else { | } else { | ||||
let single_output = | let single_output = | ||||
taxonomy.render_term(item, &self.tera, &self.config, &self.library)?; | taxonomy.render_term(item, &self.tera, &self.config, &self.library)?; | ||||
let path = output_path.join(&item.slug); | |||||
create_directory(&path)?; | create_directory(&path)?; | ||||
create_file(&path.join("index.html"), &self.inject_livereload(single_output)) | |||||
create_file(&path.join("index.html"), &self.inject_livereload(single_output))?; | |||||
} | |||||
if taxonomy.kind.rss { | |||||
self.render_rss_feed( | |||||
item.pages.iter().map(|p| self.library.get_page_by_key(*p)).collect(), | |||||
Some(&PathBuf::from(format!("{}/{}", taxonomy.kind.name, item.slug))), | |||||
) | |||||
} else { | |||||
Ok(()) | |||||
} | } | ||||
}) | }) | ||||
.collect::<Result<()>>() | .collect::<Result<()>>() | ||||
@@ -720,6 +719,18 @@ impl Site { | |||||
.iter() | .iter() | ||||
.map(|s| SitemapEntry::new(s.permalink.clone(), None)) | .map(|s| SitemapEntry::new(s.permalink.clone(), None)) | ||||
.collect::<Vec<_>>(); | .collect::<Vec<_>>(); | ||||
for section in | |||||
self.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 + 1 { | |||||
let permalink = | |||||
format!("{}{}/{}/", section.permalink, section.meta.paginate_path, i); | |||||
sections.push(SitemapEntry::new(permalink, None)) | |||||
} | |||||
} | |||||
sections.sort_by(|a, b| a.permalink.cmp(&b.permalink)); | sections.sort_by(|a, b| a.permalink.cmp(&b.permalink)); | ||||
context.insert("sections", §ions); | context.insert("sections", §ions); | ||||
@@ -733,12 +744,29 @@ impl Site { | |||||
self.config.make_permalink(&format!("{}/{}", &name, item.slug)), | self.config.make_permalink(&format!("{}/{}", &name, item.slug)), | ||||
None, | None, | ||||
)); | )); | ||||
if taxonomy.kind.is_paginated() { | |||||
let number_pagers = (item.pages.len() as f64 | |||||
/ taxonomy.kind.paginate_by.unwrap() as f64) | |||||
.ceil() as isize; | |||||
for i in 1..number_pagers + 1 { | |||||
let permalink = self.config.make_permalink(&format!( | |||||
"{}/{}/{}/{}", | |||||
name, | |||||
item.slug, | |||||
taxonomy.kind.paginate_path(), | |||||
i | |||||
)); | |||||
terms.push(SitemapEntry::new(permalink, None)) | |||||
} | |||||
} | |||||
} | } | ||||
terms.sort_by(|a, b| a.permalink.cmp(&b.permalink)); | terms.sort_by(|a, b| a.permalink.cmp(&b.permalink)); | ||||
taxonomies.push(terms); | taxonomies.push(terms); | ||||
} | } | ||||
context.insert("taxonomies", &taxonomies); | |||||
context.insert("taxonomies", &taxonomies); | |||||
context.insert("config", &self.config); | context.insert("config", &self.config); | ||||
let sitemap = &render_template("sitemap.xml", &self.tera, &context, &self.config.theme)?; | let sitemap = &render_template("sitemap.xml", &self.tera, &context, &self.config.theme)?; | ||||
@@ -771,7 +799,7 @@ impl Site { | |||||
pages.par_sort_unstable_by(sort_actual_pages_by_date); | pages.par_sort_unstable_by(sort_actual_pages_by_date); | ||||
context.insert("last_build_date", &pages[0].meta.date.clone().map(|d| d.to_string())); | |||||
context.insert("last_build_date", &pages[0].meta.date.clone()); | |||||
// limit to the last n elements if the limit is set; otherwise use all. | // limit to the last n elements if the limit is set; otherwise use all. | ||||
let num_entries = self.config.rss_limit.unwrap_or(pages.len()); | let num_entries = self.config.rss_limit.unwrap_or(pages.len()); | ||||
let p = pages | let p = pages | ||||
@@ -794,7 +822,7 @@ impl Site { | |||||
let feed = &render_template("rss.xml", &self.tera, &context, &self.config.theme)?; | let feed = &render_template("rss.xml", &self.tera, &context, &self.config.theme)?; | ||||
if let Some(ref base) = base_path { | if let Some(ref base) = base_path { | ||||
let mut output_path = self.output_path.clone().to_path_buf(); | |||||
let mut output_path = self.output_path.clone(); | |||||
for component in base.components() { | for component in base.components() { | ||||
output_path.push(component); | output_path.push(component); | ||||
if !output_path.exists() { | if !output_path.exists() { | ||||
@@ -805,16 +833,13 @@ impl Site { | |||||
} else { | } else { | ||||
create_file(&self.output_path.join("rss.xml"), feed)?; | create_file(&self.output_path.join("rss.xml"), feed)?; | ||||
} | } | ||||
Ok(()) | Ok(()) | ||||
} | } | ||||
/// Renders a single section | /// Renders a single section | ||||
pub fn render_section(&self, section: &Section, render_pages: bool) -> Result<()> { | pub fn render_section(&self, section: &Section, render_pages: bool) -> Result<()> { | ||||
ensure_directory_exists(&self.output_path)?; | ensure_directory_exists(&self.output_path)?; | ||||
let public = self.output_path.clone(); | |||||
let mut output_path = public.to_path_buf(); | |||||
let mut output_path = self.output_path.clone(); | |||||
for component in §ion.file.components { | for component in §ion.file.components { | ||||
output_path.push(component); | output_path.push(component); | ||||
@@ -1,3 +1,4 @@ | |||||
extern crate config; | |||||
extern crate site; | extern crate site; | ||||
extern crate tempfile; | extern crate tempfile; | ||||
@@ -7,6 +8,7 @@ use std::fs::File; | |||||
use std::io::prelude::*; | use std::io::prelude::*; | ||||
use std::path::Path; | use std::path::Path; | ||||
use config::Taxonomy; | |||||
use site::Site; | use site::Site; | ||||
use tempfile::tempdir; | use tempfile::tempdir; | ||||
@@ -465,6 +467,13 @@ fn can_build_site_with_pagination_for_section() { | |||||
"posts/page/4/index.html", | "posts/page/4/index.html", | ||||
"Last: https://replace-this-with-your-url.com/posts/page/5/" | "Last: https://replace-this-with-your-url.com/posts/page/5/" | ||||
)); | )); | ||||
// sitemap contains the pager pages | |||||
assert!(file_contains!( | |||||
public, | |||||
"sitemap.xml", | |||||
"<loc>https://replace-this-with-your-url.com/posts/page/4/</loc>" | |||||
)); | |||||
} | } | ||||
#[test] | #[test] | ||||
@@ -510,6 +519,93 @@ fn can_build_site_with_pagination_for_index() { | |||||
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/")); | ||||
assert_eq!(file_contains!(public, "index.html", "has_prev"), false); | 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"), false); | ||||
// sitemap contains the pager pages | |||||
assert!(file_contains!( | |||||
public, | |||||
"sitemap.xml", | |||||
"<loc>https://replace-this-with-your-url.com/page/1/</loc>" | |||||
)) | |||||
} | |||||
#[test] | |||||
fn can_build_site_with_pagination_for_taxonomy() { | |||||
let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); | |||||
path.push("test_site"); | |||||
let mut site = Site::new(&path, "config.toml").unwrap(); | |||||
site.config.taxonomies.push(Taxonomy { | |||||
name: "tags".to_string(), | |||||
paginate_by: Some(2), | |||||
paginate_path: None, | |||||
rss: true, | |||||
}); | |||||
site.load().unwrap(); | |||||
for (i, (_, page)) in site.library.pages_mut().iter_mut().enumerate() { | |||||
page.meta.taxonomies = { | |||||
let mut taxonomies = HashMap::new(); | |||||
taxonomies | |||||
.insert("tags".to_string(), vec![if i % 2 == 0 { "A" } else { "B" }.to_string()]); | |||||
taxonomies | |||||
}; | |||||
} | |||||
site.populate_taxonomies().unwrap(); | |||||
let tmp_dir = tempdir().expect("create temp dir"); | |||||
let public = &tmp_dir.path().join("public"); | |||||
site.set_output_path(&public); | |||||
site.build().unwrap(); | |||||
assert!(Path::new(&public).exists()); | |||||
assert!(file_exists!(public, "index.html")); | |||||
assert!(file_exists!(public, "sitemap.xml")); | |||||
assert!(file_exists!(public, "robots.txt")); | |||||
assert!(file_exists!(public, "a-fixed-url/index.html")); | |||||
assert!(file_exists!(public, "posts/python/index.html")); | |||||
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html")); | |||||
assert!(file_exists!(public, "posts/with-assets/index.html")); | |||||
// Tags | |||||
assert!(file_exists!(public, "tags/index.html")); | |||||
// With RSS | |||||
assert!(file_exists!(public, "tags/a/rss.xml")); | |||||
assert!(file_exists!(public, "tags/b/rss.xml")); | |||||
// And pagination! | |||||
assert!(file_exists!(public, "tags/a/page/1/index.html")); | |||||
assert!(file_exists!(public, "tags/b/page/1/index.html")); | |||||
assert!(file_exists!(public, "tags/a/page/2/index.html")); | |||||
assert!(file_exists!(public, "tags/b/page/2/index.html")); | |||||
// should redirect to posts/ | |||||
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/\"" | |||||
)); | |||||
assert!(file_contains!(public, "tags/a/index.html", "Num pagers: 6")); | |||||
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")); | |||||
assert!(file_contains!(public, "tags/a/index.html", "has_next")); | |||||
assert!(file_contains!( | |||||
public, | |||||
"tags/a/index.html", | |||||
"First: https://replace-this-with-your-url.com/tags/a/" | |||||
)); | |||||
assert!(file_contains!( | |||||
public, | |||||
"tags/a/index.html", | |||||
"Last: https://replace-this-with-your-url.com/tags/a/page/6/" | |||||
)); | |||||
assert_eq!(file_contains!(public, "tags/a/index.html", "has_prev"), false); | |||||
// sitemap contains the pager pages | |||||
assert!(file_contains!( | |||||
public, | |||||
"sitemap.xml", | |||||
"<loc>https://replace-this-with-your-url.com/tags/a/page/6/</loc>" | |||||
)) | |||||
} | } | ||||
#[test] | #[test] | ||||
@@ -417,11 +417,11 @@ mod tests { | |||||
assert_eq!( | assert_eq!( | ||||
result, | result, | ||||
json!({ | json!({ | ||||
"category": { | |||||
"date": "1979-05-27T07:32:00Z", | |||||
"key": "value" | |||||
}, | |||||
}) | |||||
"category": { | |||||
"date": "1979-05-27T07:32:00Z", | |||||
"key": "value" | |||||
}, | |||||
}) | |||||
); | ); | ||||
} | } | ||||
@@ -438,12 +438,12 @@ mod tests { | |||||
assert_eq!( | assert_eq!( | ||||
result, | result, | ||||
json!({ | json!({ | ||||
"headers": ["Number", "Title"], | |||||
"records": [ | |||||
["1", "Gutenberg"], | |||||
["2", "Printing"] | |||||
], | |||||
}) | |||||
"headers": ["Number", "Title"], | |||||
"records": [ | |||||
["1", "Gutenberg"], | |||||
["2", "Printing"] | |||||
], | |||||
}) | |||||
) | ) | ||||
} | } | ||||
@@ -460,12 +460,12 @@ mod tests { | |||||
assert_eq!( | assert_eq!( | ||||
result, | result, | ||||
json!({ | json!({ | ||||
"key": "value", | |||||
"array": [1, 2, 3], | |||||
"subpackage": { | |||||
"subkey": 5 | |||||
} | |||||
}) | |||||
"key": "value", | |||||
"array": [1, 2, 3], | |||||
"subpackage": { | |||||
"subkey": 5 | |||||
} | |||||
}) | |||||
) | ) | ||||
} | } | ||||
} | } |
@@ -144,7 +144,7 @@ pub fn make_get_taxonomy(all_taxonomies: &[Taxonomy], library: &Library) -> Glob | |||||
None => { | None => { | ||||
return Err( | return Err( | ||||
format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into() | format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into() | ||||
) | |||||
); | |||||
} | } | ||||
}; | }; | ||||
@@ -180,12 +180,12 @@ pub fn make_get_taxonomy_url(all_taxonomies: &[Taxonomy]) -> GlobalFn { | |||||
"`get_taxonomy_url` received an unknown taxonomy as kind: {}", | "`get_taxonomy_url` received an unknown taxonomy as kind: {}", | ||||
kind | kind | ||||
) | ) | ||||
.into()) | |||||
.into()); | |||||
} | } | ||||
}; | }; | ||||
if let Some(ref permalink) = container.get(&name) { | |||||
return Ok(to_value(permalink.clone()).unwrap()); | |||||
if let Some(permalink) = container.get(&name) { | |||||
return Ok(to_value(permalink).unwrap()); | |||||
} | } | ||||
Err(format!("`get_taxonomy_url`: couldn't find `{}` in `{}` taxonomy", name, kind).into()) | Err(format!("`get_taxonomy_url`: couldn't find `{}` in `{}` taxonomy", name, kind).into()) | ||||
@@ -226,7 +226,7 @@ pub fn make_resize_image(imageproc: Arc<Mutex<imageproc::Processor>>) -> GlobalF | |||||
return Err(format!("`resize_image`: Cannot find path: {}", path).into()); | return Err(format!("`resize_image`: Cannot find path: {}", path).into()); | ||||
} | } | ||||
let imageop = imageproc::ImageOp::from_args(path.clone(), &op, width, height, quality) | |||||
let imageop = imageproc::ImageOp::from_args(path, &op, width, height, quality) | |||||
.map_err(|e| format!("`resize_image`: {}", e))?; | .map_err(|e| format!("`resize_image`: {}", e))?; | ||||
let url = imageproc.insert(imageop); | let url = imageproc.insert(imageop); | ||||
@@ -16,7 +16,7 @@ create a **page** at `[base_url]/about`). | |||||
If the file is given any name *other* than `index.md` or `_index.md`, then it will | If the file is given any name *other* than `index.md` or `_index.md`, then it will | ||||
create a page with that name (without the `.md`). So naming a file in the root of your | create a page with that name (without the `.md`). So naming a file in the root of your | ||||
content directory `about.md` would also create a page at `[base_url]/about`. | content directory `about.md` would also create a page at `[base_url]/about`. | ||||
Another exception to that rule is that a filename starting with a YYYY-mm-dd date followed by | |||||
Another exception to that rule is that a filename starting with a datetime (YYYY-mm-dd or [a RFC3339 datetime](https://www.ietf.org/rfc/rfc3339.txt)) followed by | |||||
an underscore (`_`) or a dash (`-`) will use that date as the page date, unless already set | an underscore (`_`) or a dash (`-`) will use that date as the page date, unless already set | ||||
in the front-matter. The page name will be anything after `_`/`-` so a filename like `2018-10-10-hello-world.md` will | in the front-matter. The page name will be anything after `_`/`-` so a filename like `2018-10-10-hello-world.md` will | ||||
be available at `[base_url]/hello-world` | be available at `[base_url]/hello-world` | ||||
@@ -26,6 +26,7 @@ Here is a full list of the supported languages and the short names you can use: | |||||
- Plain Text -> ["txt"] | - Plain Text -> ["txt"] | ||||
- Assembly x86 (NASM) -> ["asm", "inc", "nasm"] | - Assembly x86 (NASM) -> ["asm", "inc", "nasm"] | ||||
- Crystal -> ["cr"] | - Crystal -> ["cr"] | ||||
- Dart -> ["dart"] | |||||
- Elixir -> ["ex", "exs"] | - Elixir -> ["ex", "exs"] | ||||
- fsharp -> ["fs"] | - fsharp -> ["fs"] | ||||
- Handlebars -> ["handlebars", "handlebars.html", "hbr", "hbrs", "hbs", "hdbs", "hjs", "mu", "mustache", "rac", "stache", "template", "tmpl"] | - Handlebars -> ["handlebars", "handlebars.html", "hbr", "hbrs", "hbs", "hdbs", "hjs", "mu", "mustache", "rac", "stache", "template", "tmpl"] | ||||
@@ -14,7 +14,9 @@ Zola is available on [Brew](https://brew.sh): | |||||
$ brew install zola | $ brew install zola | ||||
``` | ``` | ||||
## Linux | |||||
## From source | |||||
To build it from source, you will need to have Git, [Rust (at least 1.30) and Cargo](https://www.rust-lang.org/) | |||||
installed. You will also need additional dependencies to compile [libsass](https://github.com/sass/libsass): | |||||
### Arch Linux | ### Arch Linux | ||||
@@ -7,10 +7,9 @@ Two things can get paginated: a section or a taxonomy term. | |||||
A paginated section gets the same `section` variable as a normal | A paginated section gets the same `section` variable as a normal | ||||
[section page](./documentation/templates/pages-sections.md#section-variables) minus its pages | [section page](./documentation/templates/pages-sections.md#section-variables) minus its pages | ||||
while a paginated taxonomy gets the a `taxonomy` variable of type `TaxonomyConfig`, equivalent | |||||
to the taxonomy definition in the `config.toml`. | |||||
while | |||||
In addition, a paginated page gets a `paginator` variable of the `Pager` type: | |||||
Both get a paginated page gets a `paginator` variable of the `Pager` type: | |||||
```ts | ```ts | ||||
// How many items per page | // How many items per page | ||||
@@ -33,3 +32,17 @@ pages: Array<Page>; | |||||
// Which page are we on | // Which page are we on | ||||
current_index: Number; | current_index: Number; | ||||
``` | ``` | ||||
## Section | |||||
A paginated section gets the same `section` variable as a normal | |||||
[section page](./documentation/templates/pages-sections.md#section-variables) minus its pages. | |||||
## Taxonomy term | |||||
A paginated taxonomy gets two variables: | |||||
- a `taxonomy` variable of type `TaxonomyConfig` | |||||
- a `term` variable of type `TaxonomyTerm`. | |||||
See the [taxonomies page](./documentation/templates/taxonomies.md) for a detailed version of the types. |
@@ -17,10 +17,22 @@ permalink: String; | |||||
pages: Array<Page>; | pages: Array<Page>; | ||||
``` | ``` | ||||
## Non-paginated taxonomies | |||||
If a taxonomy is not paginated, the templates get the following variables: | |||||
and a `TaxonomyConfig`: | |||||
```ts | |||||
name: String, | |||||
slug: String, | |||||
paginate_by: Number?; | |||||
paginate_path: String?; | |||||
rss: Bool; | |||||
``` | |||||
``` | |||||
### Taxonomy list (`list.html`) | |||||
This template is never paginated and therefore get the following variables in all cases. | |||||
### Single term (`single.html`) | |||||
```ts | ```ts | ||||
// The site config | // The site config | ||||
config: Config; | config: Config; | ||||
@@ -30,11 +42,12 @@ taxonomy: TaxonomyConfig; | |||||
current_url: String; | current_url: String; | ||||
// The current path for that page | // The current path for that page | ||||
current_path: String; | current_path: String; | ||||
// The current term being rendered | |||||
term: TaxonomyTerm; | |||||
// All terms for that taxonomy | |||||
terms: Array<TaxonomyTerm>; | |||||
``` | ``` | ||||
### Taxonomy list (`list.html`) | |||||
### Single term (`single.html`) | |||||
```ts | ```ts | ||||
// The site config | // The site config | ||||
config: Config; | config: Config; | ||||
@@ -44,8 +57,9 @@ taxonomy: TaxonomyConfig; | |||||
current_url: String; | current_url: String; | ||||
// The current path for that page | // The current path for that page | ||||
current_path: String; | current_path: String; | ||||
// All terms for that taxonomy | |||||
terms: Array<TaxonomyTerm>; | |||||
// The current term being rendered | |||||
term: TaxonomyTerm; | |||||
``` | ``` | ||||
## Paginated taxonomies | |||||
A paginated taxonomy term will also get a `paginator` variable, see the [pagination page](./documentation/templates/pagination.md) | |||||
for more details on that. |
@@ -12,6 +12,7 @@ apps: | |||||
zola: | zola: | ||||
command: zola | command: zola | ||||
plugs: | plugs: | ||||
- home | |||||
- network | - network | ||||
- network-bind | - network-bind | ||||
@@ -0,0 +1,202 @@ | |||||
%YAML 1.2 | |||||
--- | |||||
# http://www.sublimetext.com/docs/3/syntax.html | |||||
name: Dart | |||||
file_extensions: | |||||
- dart | |||||
scope: source.dart | |||||
contexts: | |||||
main: | |||||
- match: ^(#!.*)$ | |||||
scope: meta.preprocessor.script.dart | |||||
- match: ^\w*\b(library|import|part of|part|export)\b | |||||
captures: | |||||
0: keyword.other.import.dart | |||||
push: | |||||
- meta_scope: meta.declaration.dart | |||||
- match: ; | |||||
captures: | |||||
0: punctuation.terminator.dart | |||||
pop: true | |||||
- include: strings | |||||
- include: comments | |||||
- match: \b(as|show|hide)\b | |||||
scope: keyword.other.import.dart | |||||
- include: comments | |||||
- include: punctuation | |||||
- include: annotations | |||||
- include: keywords | |||||
- include: constants-and-special-vars | |||||
- include: strings | |||||
annotations: | |||||
- match: "@[a-zA-Z]+" | |||||
scope: storage.type.annotation.dart | |||||
comments: | |||||
- match: /\*\*/ | |||||
scope: comment.block.empty.dart | |||||
captures: | |||||
0: punctuation.definition.comment.dart | |||||
- include: comments-doc-oldschool | |||||
- include: comments-doc | |||||
- include: comments-inline | |||||
comments-doc: | |||||
- match: /// | |||||
scope: comment.block.documentation.dart | |||||
comments-doc-oldschool: | |||||
- match: /\*\* | |||||
push: | |||||
- meta_scope: comment.block.documentation.dart | |||||
- match: \*/ | |||||
pop: true | |||||
- include: dartdoc | |||||
comments-inline: | |||||
- match: /\* | |||||
push: | |||||
- meta_scope: comment.block.dart | |||||
- match: \*/ | |||||
pop: true | |||||
- match: ((//).*)$ | |||||
captures: | |||||
1: comment.line.double-slash.dart | |||||
constants-and-special-vars: | |||||
- match: (?<!\$)\b(true|false|null)\b(?!\$) | |||||
scope: constant.language.dart | |||||
- match: (?<!\$)\b(this|super)\b(?!\$) | |||||
scope: variable.language.dart | |||||
- match: '(?<!\$)\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)\b(?!\$)' | |||||
scope: constant.numeric.dart | |||||
- match: "(?<![a-zA-Z0-9_$])[_$]*[A-Z][a-zA-Z0-9_$]*" | |||||
scope: support.class.dart | |||||
- match: '([_$]*[a-z][a-zA-Z0-9_$]*)(\(|\s+=>)' | |||||
captures: | |||||
1: entity.name.function.dart | |||||
dartdoc: | |||||
- match: '(\[.*?\])' | |||||
captures: | |||||
0: variable.name.source.dart | |||||
- match: " .*" | |||||
captures: | |||||
0: variable.name.source.dart | |||||
- match: "```.*?$" | |||||
push: | |||||
- meta_content_scope: variable.other.source.dart | |||||
- match: "```" | |||||
pop: true | |||||
- match: (`.*?`) | |||||
captures: | |||||
0: variable.other.source.dart | |||||
- match: (`.*?`) | |||||
captures: | |||||
0: variable.other.source.dart | |||||
- match: (\* (( ).*))$ | |||||
captures: | |||||
2: variable.other.source.dart | |||||
- match: (\* .*)$ | |||||
keywords: | |||||
- match: (?<!\$)\bas\b(?!\$) | |||||
scope: keyword.cast.dart | |||||
- match: (?<!\$)\b(try|on|catch|finally|throw|rethrow)\b(?!\$) | |||||
scope: keyword.control.catch-exception.dart | |||||
- match: (?<!\$)\b(break|case|continue|default|do|else|for|if|in|return|switch|while)\b(?!\$) | |||||
scope: keyword.control.dart | |||||
- match: (?<!\$)\b(sync(\*)?|async(\*)?|await|yield(\*)?)\b(?!\$) | |||||
scope: keyword.control.dart | |||||
- match: (?<!\$)\bassert\b(?!\$) | |||||
scope: keyword.control.dart | |||||
- match: (?<!\$)\b(new)\b(?!\$) | |||||
scope: keyword.control.new.dart | |||||
- match: (?<!\$)\b(abstract|class|enum|extends|external|factory|implements|get|mixin|native|operator|set|typedef|with)\b(?!\$) | |||||
scope: keyword.declaration.dart | |||||
- match: (?<!\$)\b(is\!?)\b(?!\$) | |||||
scope: keyword.operator.dart | |||||
- match: '\?|:' | |||||
scope: keyword.operator.ternary.dart | |||||
- match: (<<|>>>?|~|\^|\||&) | |||||
scope: keyword.operator.bitwise.dart | |||||
- match: ((&|\^|\||<<|>>>?)=) | |||||
scope: keyword.operator.assignment.bitwise.dart | |||||
- match: (=>) | |||||
scope: keyword.operator.closure.dart | |||||
- match: (==|!=|<=?|>=?) | |||||
scope: keyword.operator.comparison.dart | |||||
- match: '(([+*/%-]|\~)=)' | |||||
scope: keyword.operator.assignment.arithmetic.dart | |||||
- match: (=) | |||||
scope: keyword.operator.assignment.dart | |||||
- match: (\-\-|\+\+) | |||||
scope: keyword.operator.increment-decrement.dart | |||||
- match: (\-|\+|\*|\/|\~\/|%) | |||||
scope: keyword.operator.arithmetic.dart | |||||
- match: (!|&&|\|\|) | |||||
scope: keyword.operator.logical.dart | |||||
- match: (?<!\$)\b(static|final|const)\b(?!\$) | |||||
scope: storage.modifier.dart | |||||
- match: (?<!\$)\b(?:void|bool|num|int|double|dynamic|var)\b(?!\$) | |||||
scope: storage.type.primitive.dart | |||||
punctuation: | |||||
- match: "," | |||||
scope: punctuation.comma.dart | |||||
- match: ; | |||||
scope: punctuation.terminator.dart | |||||
- match: \. | |||||
scope: punctuation.dot.dart | |||||
string-interp: | |||||
- match: '\$((\w+)|\{([^{}]+)\})' | |||||
captures: | |||||
2: variable.parameter.dart | |||||
3: variable.parameter.dart | |||||
- match: \\. | |||||
scope: constant.character.escape.dart | |||||
strings: | |||||
- match: (?<!r)""" | |||||
push: | |||||
- meta_scope: string.interpolated.triple.double.dart | |||||
- match: '"""(?!")' | |||||
pop: true | |||||
- include: string-interp | |||||
- match: (?<!r)''' | |||||
push: | |||||
- meta_scope: string.interpolated.triple.single.dart | |||||
- match: "'''(?!')" | |||||
pop: true | |||||
- include: string-interp | |||||
- match: r""" | |||||
push: | |||||
- meta_scope: string.quoted.triple.double.dart | |||||
- match: '"""(?!")' | |||||
pop: true | |||||
- match: r''' | |||||
push: | |||||
- meta_scope: string.quoted.triple.single.dart | |||||
- match: "'''(?!')" | |||||
pop: true | |||||
- match: (?<!\|r)" | |||||
push: | |||||
- meta_scope: string.interpolated.double.dart | |||||
- match: '"' | |||||
pop: true | |||||
- match: \n | |||||
scope: invalid.string.newline | |||||
- include: string-interp | |||||
- match: r" | |||||
push: | |||||
- meta_scope: string.quoted.double.dart | |||||
- match: '"' | |||||
pop: true | |||||
- match: \n | |||||
scope: invalid.string.newline | |||||
- match: (?<!\|r)' | |||||
push: | |||||
- meta_scope: string.interpolated.single.dart | |||||
- match: "'" | |||||
pop: true | |||||
- match: \n | |||||
scope: invalid.string.newline | |||||
- include: string-interp | |||||
- match: r' | |||||
push: | |||||
- meta_scope: string.quoted.single.dart | |||||
- match: "'" | |||||
pop: true | |||||
- match: \n | |||||
scope: invalid.string.newline |
@@ -1,7 +1,21 @@ | |||||
Tag: {{ term.name }} | |||||
{% if not paginator %} | |||||
Tag: {{ term.name }} | |||||
{% for page in term.pages %} | |||||
<article> | |||||
<h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3> | |||||
</article> | |||||
{% endfor %} | |||||
{% for page in term.pages %} | |||||
<article> | |||||
<h3 class="post__title"><a href="{{ page.permalink | safe }}">{{ page.title | safe }}</a></h3> | |||||
</article> | |||||
{% endfor %} | |||||
{% else %} | |||||
Tag: {{ term.name }} | |||||
{% for page in paginator.pages %} | |||||
{{page.title|safe}} | |||||
{% endfor %} | |||||
Num pagers: {{ paginator.number_pagers }} | |||||
Page size: {{ paginator.paginate_by }} | |||||
Current index: {{ paginator.current_index }} | |||||
First: {{ paginator.first | safe }} | |||||
Last: {{ paginator.last | safe }} | |||||
{% if paginator.previous %}has_prev{% endif%} | |||||
{% if paginator.next %}has_next{% endif%} | |||||
{% endif %} |