This commit is contained in:
Vincent Prouillet 2018-10-31 08:18:57 +01:00
parent 8586bc1838
commit b7ce4e59fb
52 changed files with 1418 additions and 1091 deletions

View file

@ -7,9 +7,9 @@ include!("src/cli.rs");
fn main() { fn main() {
// disabled below as it fails in CI // disabled below as it fails in CI
// let mut app = build_cli(); // let mut app = build_cli();
// app.gen_completions("zola", Shell::Bash, "completions/"); // app.gen_completions("zola", Shell::Bash, "completions/");
// app.gen_completions("zola", Shell::Fish, "completions/"); // app.gen_completions("zola", Shell::Fish, "completions/");
// app.gen_completions("zola", Shell::Zsh, "completions/"); // app.gen_completions("zola", Shell::Zsh, "completions/");
// app.gen_completions("zola", Shell::PowerShell, "completions/"); // app.gen_completions("zola", Shell::PowerShell, "completions/");
} }

View file

@ -3,10 +3,10 @@
//! Although it is a valid example for serializing syntaxes, you probably won't need //! Although it is a valid example for serializing syntaxes, you probably won't need
//! to do this yourself unless you want to cache your own compiled grammars. //! to do this yourself unless you want to cache your own compiled grammars.
extern crate syntect; extern crate syntect;
use syntect::parsing::SyntaxSetBuilder;
use syntect::highlighting::ThemeSet;
use syntect::dumps::*;
use std::env; use std::env;
use syntect::dumps::*;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSetBuilder;
fn usage_and_exit() -> ! { fn usage_and_exit() -> ! {
println!("USAGE: cargo run --example generate_sublime synpack source-dir newlines.packdump nonewlines.packdump\n println!("USAGE: cargo run --example generate_sublime synpack source-dir newlines.packdump nonewlines.packdump\n
@ -32,7 +32,7 @@ fn main() {
println!("- {} -> {:?}", s.name, s.file_extensions); println!("- {} -> {:?}", s.name, s.file_extensions);
} }
} }
}, }
(Some(ref cmd), Some(ref theme_dir), Some(ref packpath)) if cmd == "themepack" => { (Some(ref cmd), Some(ref theme_dir), Some(ref packpath)) if cmd == "themepack" => {
let ts = ThemeSet::load_from_folder(theme_dir).unwrap(); let ts = ThemeSet::load_from_folder(theme_dir).unwrap();
for path in ts.themes.keys() { for path in ts.themes.keys() {

View file

@ -41,12 +41,7 @@ impl Taxonomy {
impl Default for Taxonomy { impl Default for Taxonomy {
fn default() -> Taxonomy { fn default() -> Taxonomy {
Taxonomy { Taxonomy { name: String::new(), paginate_by: None, paginate_path: None, rss: false }
name: String::new(),
paginate_by: None,
paginate_path: None,
rss: false,
}
} }
} }
@ -137,19 +132,12 @@ impl Config {
for pat in &config.ignored_content { for pat in &config.ignored_content {
let glob = match Glob::new(pat) { let glob = match Glob::new(pat) {
Ok(g) => g, Ok(g) => g,
Err(e) => bail!( Err(e) => bail!("Invalid ignored_content glob pattern: {}, error = {}", pat, e),
"Invalid ignored_content glob pattern: {}, error = {}",
pat,
e
),
}; };
glob_set_builder.add(glob); glob_set_builder.add(glob);
} }
config.ignored_content_globset = Some( config.ignored_content_globset =
glob_set_builder Some(glob_set_builder.build().expect("Bad ignored_content in config file."));
.build()
.expect("Bad ignored_content in config file."),
);
} }
Ok(config) Ok(config)
@ -162,10 +150,7 @@ impl Config {
let file_name = path.file_name().unwrap(); let file_name = path.file_name().unwrap();
File::open(path) File::open(path)
.chain_err(|| { .chain_err(|| {
format!( format!("No `{:?}` file found. Are you in the right directory?", file_name)
"No `{:?}` file found. Are you in the right directory?",
file_name
)
})? })?
.read_to_string(&mut content)?; .read_to_string(&mut content)?;
@ -217,16 +202,12 @@ impl Config {
let original = self.extra.clone(); let original = self.extra.clone();
// 2. inject theme extra values // 2. inject theme extra values
for (key, val) in &theme.extra { for (key, val) in &theme.extra {
self.extra self.extra.entry(key.to_string()).or_insert_with(|| val.clone());
.entry(key.to_string())
.or_insert_with(|| val.clone());
} }
// 3. overwrite with original config // 3. overwrite with original config
for (key, val) in &original { for (key, val) in &original {
self.extra self.extra.entry(key.to_string()).or_insert_with(|| val.clone());
.entry(key.to_string())
.or_insert_with(|| val.clone());
} }
Ok(()) Ok(())
@ -316,16 +297,7 @@ hello = "world"
let config = Config::parse(config); let config = Config::parse(config);
assert!(config.is_ok()); assert!(config.is_ok());
assert_eq!( assert_eq!(config.unwrap().extra.get("hello").unwrap().as_str().unwrap(), "world");
config
.unwrap()
.extra
.get("hello")
.unwrap()
.as_str()
.unwrap(),
"world"
);
} }
#[test] #[test]
@ -360,10 +332,7 @@ hello = "world"
fn can_make_url_with_localhost() { fn can_make_url_with_localhost() {
let mut config = Config::default(); let mut config = Config::default();
config.base_url = "http://127.0.0.1:1111".to_string(); config.base_url = "http://127.0.0.1:1111".to_string();
assert_eq!( assert_eq!(config.make_permalink("/tags/rust"), "http://127.0.0.1:1111/tags/rust/");
config.make_permalink("/tags/rust"),
"http://127.0.0.1:1111/tags/rust/"
);
} }
// https://github.com/Keats/gutenberg/issues/486 // https://github.com/Keats/gutenberg/issues/486

View file

@ -1,18 +1,18 @@
use syntect::dumps::from_binary; use syntect::dumps::from_binary;
use syntect::parsing::SyntaxSet;
use syntect::highlighting::ThemeSet;
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
use Config; use Config;
lazy_static! { lazy_static! {
pub static ref SYNTAX_SET: SyntaxSet = { pub static ref SYNTAX_SET: SyntaxSet = {
let ss: SyntaxSet = from_binary(include_bytes!("../../../sublime_syntaxes/newlines.packdump")); let ss: SyntaxSet =
from_binary(include_bytes!("../../../sublime_syntaxes/newlines.packdump"));
ss ss
}; };
pub static ref THEME_SET: ThemeSet =
pub static ref THEME_SET: ThemeSet = from_binary(include_bytes!("../../../sublime_themes/all.themedump")); from_binary(include_bytes!("../../../sublime_themes/all.themedump"));
} }
/// Returns the highlighter and whether it was found in the extra or not /// Returns the highlighter and whether it was found in the extra or not
@ -21,7 +21,8 @@ pub fn get_highlighter<'a>(info: &str, config: &Config) -> (HighlightLines<'a>,
let mut in_extra = false; let mut in_extra = false;
if let Some(ref lang) = info.split(' ').next() { if let Some(ref lang) = info.split(' ').next() {
let syntax = SYNTAX_SET.find_syntax_by_token(lang) let syntax = SYNTAX_SET
.find_syntax_by_token(lang)
.or_else(|| { .or_else(|| {
if let Some(ref extra) = config.extra_syntax_set { if let Some(ref extra) = config.extra_syntax_set {
let s = extra.find_syntax_by_token(lang); let s = extra.find_syntax_by_token(lang);

View file

@ -9,10 +9,9 @@ extern crate globset;
extern crate lazy_static; extern crate lazy_static;
extern crate syntect; extern crate syntect;
mod config; mod config;
mod theme;
pub mod highlighting; pub mod highlighting;
mod theme;
pub use config::{Config, Taxonomy}; pub use config::{Config, Taxonomy};
use std::path::Path; use std::path::Path;

View file

@ -7,7 +7,6 @@ use toml::Value as Toml;
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
/// Holds the data from a `theme.toml` file. /// Holds the data from a `theme.toml` file.
/// There are other fields than `extra` in it but Zola /// There are other fields than `extra` in it but Zola
/// itself doesn't care about them. /// itself doesn't care about them.
@ -36,7 +35,6 @@ impl Theme {
bail!("Expected the `theme.toml` to be a TOML table") bail!("Expected the `theme.toml` to be a TOML table")
} }
Ok(Theme { extra }) Ok(Theme { extra })
} }
@ -44,11 +42,11 @@ impl Theme {
pub fn from_file(path: &PathBuf) -> Result<Theme> { pub fn from_file(path: &PathBuf) -> Result<Theme> {
let mut content = String::new(); let mut content = String::new();
File::open(path) File::open(path)
.chain_err(|| .chain_err(|| {
"No `theme.toml` file found. \ "No `theme.toml` file found. \
Is the `theme` defined in your `config.toml present in the `themes` directory \ Is the `theme` defined in your `config.toml present in the `themes` directory \
and does it have a `theme.toml` inside?" and does it have a `theme.toml` inside?"
)? })?
.read_to_string(&mut content)?; .read_to_string(&mut content)?;
Theme::parse(&content) Theme::parse(&content)

View file

@ -2,10 +2,10 @@
#[macro_use] #[macro_use]
extern crate error_chain; extern crate error_chain;
extern crate tera;
extern crate toml;
extern crate image; extern crate image;
extern crate syntect; extern crate syntect;
extern crate tera;
extern crate toml;
error_chain! { error_chain! {
errors {} errors {}

View file

@ -2,18 +2,18 @@
extern crate lazy_static; extern crate lazy_static;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
extern crate serde;
extern crate toml;
extern crate regex;
extern crate tera;
extern crate chrono; extern crate chrono;
extern crate regex;
extern crate serde;
extern crate tera;
extern crate toml;
#[macro_use] #[macro_use]
extern crate errors; extern crate errors;
use std::path::Path;
use regex::Regex;
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use regex::Regex;
use std::path::Path;
mod page; mod page;
mod section; mod section;
@ -22,7 +22,8 @@ pub use page::PageFrontMatter;
pub use section::SectionFrontMatter; pub use section::SectionFrontMatter;
lazy_static! { lazy_static! {
static ref PAGE_RE: Regex = Regex::new(r"^[[:space:]]*\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap(); static ref PAGE_RE: Regex =
Regex::new(r"^[[:space:]]*\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap();
} }
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
@ -44,12 +45,14 @@ pub enum InsertAnchor {
None, None,
} }
/// Split a file between the front matter and its content /// Split a file between the front matter and its content
/// Will return an error if the front matter wasn't found /// 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(file_path: &Path, content: &str) -> Result<(String, String)> {
if !PAGE_RE.is_match(content) { if !PAGE_RE.is_match(content) {
bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy()); bail!(
"Couldn't find front matter in `{}`. Did you forget to add `+++`?",
file_path.to_string_lossy()
);
} }
// 2. extract the front matter and the content // 2. extract the front matter and the content
@ -62,10 +65,14 @@ fn split_content(file_path: &Path, content: &str) -> Result<(String, String)> {
/// Split a file between the front matter and its content. /// Split a file between the front matter and its content.
/// Returns a parsed `SectionFrontMatter` and the rest of the content /// Returns a parsed `SectionFrontMatter` and the rest of the content
pub fn split_section_content(file_path: &Path, content: &str) -> Result<(SectionFrontMatter, String)> { pub fn split_section_content(
file_path: &Path,
content: &str,
) -> Result<(SectionFrontMatter, String)> {
let (front_matter, content) = split_content(file_path, content)?; let (front_matter, content) = split_content(file_path, content)?;
let meta = SectionFrontMatter::parse(&front_matter) let meta = SectionFrontMatter::parse(&front_matter).chain_err(|| {
.chain_err(|| format!("Error when parsing front matter of section `{}`", file_path.to_string_lossy()))?; format!("Error when parsing front matter of section `{}`", file_path.to_string_lossy())
})?;
Ok((meta, content)) Ok((meta, content))
} }
@ -73,8 +80,9 @@ pub fn split_section_content(file_path: &Path, content: &str) -> Result<(Section
/// Returns a parsed `PageFrontMatter` and the rest of the 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(file_path: &Path, content: &str) -> Result<(PageFrontMatter, String)> {
let (front_matter, content) = split_content(file_path, content)?; let (front_matter, content) = split_content(file_path, content)?;
let meta = PageFrontMatter::parse(&front_matter) let meta = PageFrontMatter::parse(&front_matter).chain_err(|| {
.chain_err(|| format!("Error when parsing front matter of page `{}`", file_path.to_string_lossy()))?; format!("Error when parsing front matter of page `{}`", file_path.to_string_lossy())
})?;
Ok((meta, content)) Ok((meta, content))
} }
@ -82,7 +90,7 @@ pub fn split_page_content(file_path: &Path, content: &str) -> Result<(PageFrontM
mod tests { mod tests {
use std::path::Path; use std::path::Path;
use super::{split_section_content, split_page_content}; use super::{split_page_content, split_section_content};
#[test] #[test]
fn can_split_page_content_valid() { fn can_split_page_content_valid() {

View file

@ -2,19 +2,17 @@ use std::collections::HashMap;
use std::result::Result as StdResult; use std::result::Result as StdResult;
use chrono::prelude::*; use chrono::prelude::*;
use tera::{Map, Value};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use tera::{Map, Value};
use toml; use toml;
use errors::Result; use errors::Result;
fn from_toml_datetime<'de, D>(deserializer: D) -> StdResult<Option<String>, D::Error> fn from_toml_datetime<'de, D>(deserializer: D) -> StdResult<Option<String>, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
toml::value::Datetime::deserialize(deserializer) toml::value::Datetime::deserialize(deserializer).map(|s| Some(s.to_string()))
.map(|s| Some(s.to_string()))
} }
/// Returns key/value for a converted date from TOML. /// Returns key/value for a converted date from TOML.
@ -36,7 +34,9 @@ fn convert_toml_date(table: Map<String, Value>) -> Value {
} }
new.insert(k, convert_toml_date(o)); new.insert(k, convert_toml_date(o));
} }
_ => { new.insert(k, v); } _ => {
new.insert(k, v);
}
} }
} }
@ -53,14 +53,15 @@ fn fix_toml_dates(table: Map<String, Value>) -> Value {
Value::Object(mut o) => { Value::Object(mut o) => {
new.insert(key, convert_toml_date(o)); new.insert(key, convert_toml_date(o));
} }
_ => { new.insert(key, value); } _ => {
new.insert(key, value);
}
} }
} }
Value::Object(new) Value::Object(new)
} }
/// The front matter of every page /// The front matter of every page
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(default)] #[serde(default)]
@ -143,7 +144,9 @@ impl PageFrontMatter {
if d.contains('T') { if d.contains('T') {
DateTime::parse_from_rfc3339(&d).ok().and_then(|s| Some(s.naive_local())) DateTime::parse_from_rfc3339(&d).ok().and_then(|s| Some(s.naive_local()))
} else { } else {
NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0, 0, 0))) NaiveDate::parse_from_str(&d, "%Y-%m-%d")
.ok()
.and_then(|s| Some(s.and_hms(0, 0, 0)))
} }
} else { } else {
None None
@ -187,11 +190,10 @@ impl Default for PageFrontMatter {
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use tera::to_value;
use super::PageFrontMatter; use super::PageFrontMatter;
use tera::to_value;
#[test] #[test]
fn can_have_empty_front_matter() { fn can_have_empty_front_matter() {
@ -213,7 +215,6 @@ mod tests {
assert_eq!(res.description.unwrap(), "hey there".to_string()) assert_eq!(res.description.unwrap(), "hey there".to_string())
} }
#[test] #[test]
fn errors_with_invalid_front_matter() { fn errors_with_invalid_front_matter() {
let content = r#"title = 1\n"#; let content = r#"title = 1\n"#;

View file

@ -5,11 +5,10 @@ use toml;
use errors::Result; use errors::Result;
use super::{SortBy, InsertAnchor}; use super::{InsertAnchor, SortBy};
static DEFAULT_PAGINATE_PATH: &'static str = "page"; static DEFAULT_PAGINATE_PATH: &'static str = "page";
/// The front matter of every section /// The front matter of every section
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
@ -69,7 +68,7 @@ impl SectionFrontMatter {
pub fn is_paginated(&self) -> bool { pub fn is_paginated(&self) -> bool {
match self.paginate_by { match self.paginate_by {
Some(v) => v > 0, Some(v) => v > 0,
None => false None => false,
} }
} }
} }

View file

@ -1,32 +1,32 @@
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
extern crate regex;
extern crate image; extern crate image;
extern crate rayon; extern crate rayon;
extern crate regex;
extern crate utils;
extern crate errors; extern crate errors;
extern crate utils;
use std::path::{Path, PathBuf};
use std::hash::{Hash, Hasher};
use std::collections::HashMap;
use std::collections::hash_map::Entry as HEntry;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::collections::hash_map::Entry as HEntry;
use std::collections::HashMap;
use std::fs::{self, File}; use std::fs::{self, File};
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use regex::Regex;
use image::{FilterType, GenericImageView};
use image::jpeg::JPEGEncoder; use image::jpeg::JPEGEncoder;
use image::{FilterType, GenericImageView};
use rayon::prelude::*; use rayon::prelude::*;
use regex::Regex;
use utils::fs as ufs;
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use utils::fs as ufs;
static RESIZED_SUBDIR: &'static str = "processed_images"; static RESIZED_SUBDIR: &'static str = "processed_images";
lazy_static! { lazy_static! {
pub static ref RESIZED_FILENAME: Regex = Regex::new(r#"([0-9a-f]{16})([0-9a-f]{2})[.]jpg"#).unwrap(); pub static ref RESIZED_FILENAME: Regex =
Regex::new(r#"([0-9a-f]{16})([0-9a-f]{2})[.]jpg"#).unwrap();
} }
/// Describes the precise kind of a resize operation /// Describes the precise kind of a resize operation
@ -57,16 +57,22 @@ impl ResizeOp {
// Validate args: // Validate args:
match op { match op {
"fit_width" => if width.is_none() { "fit_width" => {
return Err("op=\"fit_width\" requires a `width` argument".to_string().into()); if width.is_none() {
}, return Err("op=\"fit_width\" requires a `width` argument".to_string().into());
"fit_height" => if height.is_none() { }
return Err("op=\"fit_height\" requires a `height` argument".to_string().into()); }
}, "fit_height" => {
"scale" | "fit" | "fill" => if width.is_none() || height.is_none() { if height.is_none() {
return Err(format!("op={} requires a `width` and `height` argument", op).into()); return Err("op=\"fit_height\" requires a `height` argument".to_string().into());
}, }
_ => return Err(format!("Invalid image resize operation: {}", op).into()) }
"scale" | "fit" | "fill" => {
if width.is_none() || height.is_none() {
return Err(format!("op={} requires a `width` and `height` argument", op).into());
}
}
_ => return Err(format!("Invalid image resize operation: {}", op).into()),
}; };
Ok(match op { Ok(match op {
@ -121,8 +127,12 @@ impl From<ResizeOp> for u8 {
impl Hash for ResizeOp { impl Hash for ResizeOp {
fn hash<H: Hasher>(&self, hasher: &mut H) { fn hash<H: Hasher>(&self, hasher: &mut H) {
hasher.write_u8(u8::from(*self)); hasher.write_u8(u8::from(*self));
if let Some(w) = self.width() { hasher.write_u32(w); } if let Some(w) = self.width() {
if let Some(h) = self.height() { hasher.write_u32(h); } hasher.write_u32(w);
}
if let Some(h) = self.height() {
hasher.write_u32(h);
}
} }
} }
@ -207,8 +217,7 @@ impl ImageOp {
((img_w - crop_w) / 2, 0) ((img_w - crop_w) / 2, 0)
}; };
img.crop(offset_w, offset_h, crop_w, crop_h) img.crop(offset_w, offset_h, crop_w, crop_h).resize_exact(w, h, RESIZE_FILTER)
.resize_exact(w, h, RESIZE_FILTER)
} }
} }
}; };
@ -221,7 +230,6 @@ impl ImageOp {
} }
} }
/// A strcture into which image operations can be enqueued and then performed. /// A strcture into which image operations can be enqueued and then performed.
/// All output is written in a subdirectory in `static_path`, /// All output is written in a subdirectory in `static_path`,
/// taking care of file stale status based on timestamps and possible hash collisions. /// taking care of file stale status based on timestamps and possible hash collisions.
@ -271,7 +279,11 @@ impl Processor {
fn insert_with_collisions(&mut self, mut img_op: ImageOp) -> u32 { fn insert_with_collisions(&mut self, mut img_op: ImageOp) -> u32 {
match self.img_ops.entry(img_op.hash) { match self.img_ops.entry(img_op.hash) {
HEntry::Occupied(entry) => if *entry.get() == img_op { return 0; }, HEntry::Occupied(entry) => {
if *entry.get() == img_op {
return 0;
}
}
HEntry::Vacant(entry) => { HEntry::Vacant(entry) => {
entry.insert(img_op); entry.insert(img_op);
return 0; return 0;
@ -341,9 +353,8 @@ impl Processor {
let filename = entry_path.file_name().unwrap().to_string_lossy(); let filename = entry_path.file_name().unwrap().to_string_lossy();
if let Some(capts) = RESIZED_FILENAME.captures(filename.as_ref()) { if let Some(capts) = RESIZED_FILENAME.captures(filename.as_ref()) {
let hash = u64::from_str_radix(capts.get(1).unwrap().as_str(), 16).unwrap(); let hash = u64::from_str_radix(capts.get(1).unwrap().as_str(), 16).unwrap();
let collision_id = u32::from_str_radix( let collision_id =
capts.get(2).unwrap().as_str(), 16, u32::from_str_radix(capts.get(2).unwrap().as_str(), 16).unwrap();
).unwrap();
if collision_id > 0 || !self.img_ops.contains_key(&hash) { if collision_id > 0 || !self.img_ops.contains_key(&hash) {
fs::remove_file(&entry_path)?; fs::remove_file(&entry_path)?;
@ -359,24 +370,28 @@ impl Processor {
ufs::ensure_directory_exists(&self.resized_path)?; ufs::ensure_directory_exists(&self.resized_path)?;
} }
self.img_ops.par_iter().map(|(hash, op)| { self.img_ops
let target = self.resized_path.join(Self::op_filename(*hash, op.collision_id)); .par_iter()
op.perform(&self.content_path, &target) .map(|(hash, op)| {
.chain_err(|| format!("Failed to process image: {}", op.source)) let target = self.resized_path.join(Self::op_filename(*hash, op.collision_id));
}).collect::<Result<()>>() op.perform(&self.content_path, &target)
.chain_err(|| format!("Failed to process image: {}", op.source))
})
.collect::<Result<()>>()
} }
} }
/// Looks at file's extension and returns whether it's a supported image format /// Looks at file's extension and returns whether it's a supported image format
pub fn file_is_img<P: AsRef<Path>>(p: P) -> bool { pub fn file_is_img<P: AsRef<Path>>(p: P) -> bool {
p.as_ref().extension().and_then(|s| s.to_str()).map(|ext| { p.as_ref()
match ext.to_lowercase().as_str() { .extension()
.and_then(|s| s.to_str())
.map(|ext| match ext.to_lowercase().as_str() {
"jpg" | "jpeg" => true, "jpg" | "jpeg" => true,
"png" => true, "png" => true,
"gif" => true, "gif" => true,
"bmp" => true, "bmp" => true,
_ => false, _ => false,
} })
}).unwrap_or(false) .unwrap_or(false)
} }

View file

@ -114,7 +114,8 @@ mod tests {
#[test] #[test]
fn can_find_content_components() { fn can_find_content_components() {
let res = find_content_components("/home/vincent/code/site/content/posts/tutorials/python.md"); let res =
find_content_components("/home/vincent/code/site/content/posts/tutorials/python.md");
assert_eq!(res, ["posts".to_string(), "tutorials".to_string()]); assert_eq!(res, ["posts".to_string(), "tutorials".to_string()]);
} }
} }

View file

@ -2,19 +2,19 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tera::{Tera, Context as TeraContext};
use slug::slugify;
use slotmap::{Key};
use regex::Regex; use regex::Regex;
use slotmap::Key;
use slug::slugify;
use tera::{Context as TeraContext, Tera};
use errors::{Result, ResultExt};
use config::Config; use config::Config;
use utils::fs::{read_file, find_related_assets}; use errors::{Result, ResultExt};
use front_matter::{split_page_content, InsertAnchor, PageFrontMatter};
use library::Library;
use rendering::{render_content, Header, RenderContext};
use utils::fs::{find_related_assets, read_file};
use utils::site::get_reading_analytics; use utils::site::get_reading_analytics;
use utils::templates::render_template; use utils::templates::render_template;
use front_matter::{PageFrontMatter, InsertAnchor, split_page_content};
use rendering::{RenderContext, Header, render_content};
use library::Library;
use content::file_info::FileInfo; use content::file_info::FileInfo;
use content::ser::SerializingPage; use content::ser::SerializingPage;
@ -24,7 +24,6 @@ lazy_static! {
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(); 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();
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Page { pub struct Page {
/// All info about the actual file /// All info about the actual file
@ -71,7 +70,6 @@ pub struct Page {
pub reading_time: Option<usize>, pub reading_time: Option<usize>,
} }
impl Page { impl Page {
pub fn new<P: AsRef<Path>>(file_path: P, meta: PageFrontMatter) -> Page { pub fn new<P: AsRef<Path>>(file_path: P, meta: PageFrontMatter) -> Page {
let file_path = file_path.as_ref(); let file_path = file_path.as_ref();
@ -155,7 +153,9 @@ impl Page {
page.path = format!("{}/", page.path); page.path = format!("{}/", page.path);
} }
page.components = page.path.split('/') page.components = page
.path
.split('/')
.map(|p| p.to_string()) .map(|p| p.to_string())
.filter(|p| !p.is_empty()) .filter(|p| !p.is_empty())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -182,13 +182,13 @@ impl Page {
// against the remaining path. Note that the current behaviour effectively means that // against the remaining path. Note that the current behaviour effectively means that
// the `ignored_content` setting in the config file is limited to single-file glob // the `ignored_content` setting in the config file is limited to single-file glob
// patterns (no "**" patterns). // patterns (no "**" patterns).
page.assets = assets.into_iter() page.assets = assets
.filter(|path| .into_iter()
match path.file_name() { .filter(|path| match path.file_name() {
None => true, None => true,
Some(file) => !globset.is_match(file) Some(file) => !globset.is_match(file),
} })
).collect(); .collect();
} else { } else {
page.assets = assets; page.assets = assets;
} }
@ -210,13 +210,8 @@ impl Page {
config: &Config, config: &Config,
anchor_insert: InsertAnchor, anchor_insert: InsertAnchor,
) -> Result<()> { ) -> Result<()> {
let mut context = RenderContext::new( let mut context =
tera, RenderContext::new(tera, config, &self.permalink, permalinks, anchor_insert);
config,
&self.permalink,
permalinks,
anchor_insert,
);
context.tera_context.insert("page", &SerializingPage::from_page_basic(self, None)); context.tera_context.insert("page", &SerializingPage::from_page_basic(self, None));
@ -234,7 +229,7 @@ impl Page {
pub fn render_html(&self, tera: &Tera, config: &Config, library: &Library) -> Result<String> { pub fn render_html(&self, tera: &Tera, config: &Config, library: &Library) -> Result<String> {
let tpl_name = match self.meta.template { let tpl_name = match self.meta.template {
Some(ref l) => l.to_string(), Some(ref l) => l.to_string(),
None => "page.html".to_string() None => "page.html".to_string(),
}; };
let mut context = TeraContext::new(); let mut context = TeraContext::new();
@ -249,7 +244,8 @@ impl Page {
/// Creates a vectors of asset URLs. /// Creates a vectors of asset URLs.
fn serialize_assets(&self) -> Vec<String> { fn serialize_assets(&self) -> Vec<String> {
self.assets.iter() self.assets
.iter()
.filter_map(|asset| asset.file_name()) .filter_map(|asset| asset.file_name())
.filter_map(|filename| filename.to_str()) .filter_map(|filename| filename.to_str())
.map(|filename| self.path.clone() + filename) .map(|filename| self.path.clone() + filename)
@ -294,19 +290,18 @@ impl Default for Page {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::{create_dir, File};
use std::io::Write; use std::io::Write;
use std::fs::{File, create_dir};
use std::path::Path; use std::path::Path;
use tera::Tera;
use tempfile::tempdir;
use globset::{Glob, GlobSetBuilder}; use globset::{Glob, GlobSetBuilder};
use tempfile::tempdir;
use tera::Tera;
use config::Config;
use super::Page; use super::Page;
use config::Config;
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
#[test] #[test]
fn test_can_parse_a_valid_page() { fn test_can_parse_a_valid_page() {
let content = r#" let content = r#"
@ -324,7 +319,8 @@ Hello world"#;
&Tera::default(), &Tera::default(),
&Config::default(), &Config::default(),
InsertAnchor::None, InsertAnchor::None,
).unwrap(); )
.unwrap();
assert_eq!(page.meta.title.unwrap(), "Hello".to_string()); assert_eq!(page.meta.title.unwrap(), "Hello".to_string());
assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string()); assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string());
@ -426,16 +422,13 @@ Hello world"#;
+++ +++
+++ +++
Hello world Hello world
<!-- more -->"#.to_string(); <!-- more -->"#
.to_string();
let res = Page::parse(Path::new("hello.md"), &content, &config); let res = Page::parse(Path::new("hello.md"), &content, &config);
assert!(res.is_ok()); assert!(res.is_ok());
let mut page = res.unwrap(); let mut page = res.unwrap();
page.render_markdown( page.render_markdown(&HashMap::default(), &Tera::default(), &config, InsertAnchor::None)
&HashMap::default(), .unwrap();
&Tera::default(),
&config,
InsertAnchor::None,
).unwrap();
assert_eq!(page.summary, Some("<p>Hello world</p>\n".to_string())); assert_eq!(page.summary, Some("<p>Hello world</p>\n".to_string()));
} }
@ -453,10 +446,7 @@ Hello world
File::create(nested_path.join("graph.jpg")).unwrap(); File::create(nested_path.join("graph.jpg")).unwrap();
File::create(nested_path.join("fail.png")).unwrap(); File::create(nested_path.join("fail.png")).unwrap();
let res = Page::from_file( let res = Page::from_file(nested_path.join("index.md").as_path(), &Config::default());
nested_path.join("index.md").as_path(),
&Config::default(),
);
assert!(res.is_ok()); assert!(res.is_ok());
let page = res.unwrap(); let page = res.unwrap();
assert_eq!(page.file.parent, path.join("content").join("posts")); assert_eq!(page.file.parent, path.join("content").join("posts"));
@ -479,10 +469,7 @@ Hello world
File::create(nested_path.join("graph.jpg")).unwrap(); File::create(nested_path.join("graph.jpg")).unwrap();
File::create(nested_path.join("fail.png")).unwrap(); File::create(nested_path.join("fail.png")).unwrap();
let res = Page::from_file( let res = Page::from_file(nested_path.join("index.md").as_path(), &Config::default());
nested_path.join("index.md").as_path(),
&Config::default(),
);
assert!(res.is_ok()); assert!(res.is_ok());
let page = res.unwrap(); let page = res.unwrap();
assert_eq!(page.file.parent, path.join("content").join("posts")); assert_eq!(page.file.parent, path.join("content").join("posts"));
@ -510,10 +497,7 @@ Hello world
let mut config = Config::default(); let mut config = Config::default();
config.ignored_content_globset = Some(gsb.build().unwrap()); config.ignored_content_globset = Some(gsb.build().unwrap());
let res = Page::from_file( let res = Page::from_file(nested_path.join("index.md").as_path(), &config);
nested_path.join("index.md").as_path(),
&config,
);
assert!(res.is_ok()); assert!(res.is_ok());
let page = res.unwrap(); let page = res.unwrap();
@ -528,7 +512,8 @@ Hello world
+++ +++
+++ +++
Hello world Hello world
<!-- more -->"#.to_string(); <!-- more -->"#
.to_string();
let res = Page::parse(Path::new("2018-10-08_hello.md"), &content, &config); let res = Page::parse(Path::new("2018-10-08_hello.md"), &content, &config);
assert!(res.is_ok()); assert!(res.is_ok());
let page = res.unwrap(); let page = res.unwrap();
@ -539,14 +524,14 @@ Hello world
#[test] #[test]
fn frontmatter_date_override_filename_date() { fn frontmatter_date_override_filename_date() {
let config = Config::default(); let config = Config::default();
let content = r#" let content = r#"
+++ +++
date = 2018-09-09 date = 2018-09-09
+++ +++
Hello world Hello world
<!-- more -->"#.to_string(); <!-- more -->"#
.to_string();
let res = Page::parse(Path::new("2018-10-08_hello.md"), &content, &config); let res = Page::parse(Path::new("2018-10-08_hello.md"), &content, &config);
assert!(res.is_ok()); assert!(res.is_ok());
let page = res.unwrap(); let page = res.unwrap();

View file

@ -1,22 +1,21 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tera::{Tera, Context as TeraContext};
use slotmap::Key; use slotmap::Key;
use tera::{Context as TeraContext, Tera};
use config::Config; use config::Config;
use front_matter::{SectionFrontMatter, split_section_content};
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use utils::fs::{read_file, find_related_assets}; use front_matter::{split_section_content, SectionFrontMatter};
use utils::templates::render_template; use rendering::{render_content, Header, RenderContext};
use utils::fs::{find_related_assets, read_file};
use utils::site::get_reading_analytics; use utils::site::get_reading_analytics;
use rendering::{RenderContext, Header, render_content}; use utils::templates::render_template;
use content::file_info::FileInfo; use content::file_info::FileInfo;
use content::ser::SerializingSection; use content::ser::SerializingSection;
use library::Library; use library::Library;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Section { pub struct Section {
/// All info about the actual file /// All info about the actual file
@ -86,7 +85,9 @@ impl Section {
section.word_count = Some(word_count); section.word_count = Some(word_count);
section.reading_time = Some(reading_time); section.reading_time = Some(reading_time);
section.path = format!("{}/", section.file.components.join("/")); section.path = format!("{}/", section.file.components.join("/"));
section.components = section.path.split('/') section.components = section
.path
.split('/')
.map(|p| p.to_string()) .map(|p| p.to_string())
.filter(|p| !p.is_empty()) .filter(|p| !p.is_empty())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -111,13 +112,13 @@ impl Section {
// against the remaining path. Note that the current behaviour effectively means that // against the remaining path. Note that the current behaviour effectively means that
// the `ignored_content` setting in the config file is limited to single-file glob // the `ignored_content` setting in the config file is limited to single-file glob
// patterns (no "**" patterns). // patterns (no "**" patterns).
section.assets = assets.into_iter() section.assets = assets
.filter(|path| .into_iter()
match path.file_name() { .filter(|path| match path.file_name() {
None => true, None => true,
Some(file) => !globset.is_match(file) Some(file) => !globset.is_match(file),
} })
).collect(); .collect();
} else { } else {
section.assets = assets; section.assets = assets;
} }
@ -185,7 +186,8 @@ impl Section {
/// Creates a vectors of asset URLs. /// Creates a vectors of asset URLs.
fn serialize_assets(&self) -> Vec<String> { fn serialize_assets(&self) -> Vec<String> {
self.assets.iter() self.assets
.iter()
.filter_map(|asset| asset.file_name()) .filter_map(|asset| asset.file_name())
.filter_map(|filename| filename.to_str()) .filter_map(|filename| filename.to_str())
.map(|filename| self.path.clone() + filename) .map(|filename| self.path.clone() + filename)
@ -227,14 +229,14 @@ impl Default for Section {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fs::{create_dir, File};
use std::io::Write; use std::io::Write;
use std::fs::{File, create_dir};
use tempfile::tempdir;
use globset::{Glob, GlobSetBuilder}; use globset::{Glob, GlobSetBuilder};
use tempfile::tempdir;
use config::Config;
use super::Section; use super::Section;
use config::Config;
#[test] #[test]
fn section_with_assets_gets_right_info() { fn section_with_assets_gets_right_info() {
@ -250,10 +252,7 @@ mod tests {
File::create(nested_path.join("graph.jpg")).unwrap(); File::create(nested_path.join("graph.jpg")).unwrap();
File::create(nested_path.join("fail.png")).unwrap(); File::create(nested_path.join("fail.png")).unwrap();
let res = Section::from_file( let res = Section::from_file(nested_path.join("_index.md").as_path(), &Config::default());
nested_path.join("_index.md").as_path(),
&Config::default(),
);
assert!(res.is_ok()); assert!(res.is_ok());
let section = res.unwrap(); let section = res.unwrap();
assert_eq!(section.assets.len(), 3); assert_eq!(section.assets.len(), 3);
@ -279,10 +278,7 @@ mod tests {
let mut config = Config::default(); let mut config = Config::default();
config.ignored_content_globset = Some(gsb.build().unwrap()); config.ignored_content_globset = Some(gsb.build().unwrap());
let res = Section::from_file( let res = Section::from_file(nested_path.join("_index.md").as_path(), &config);
nested_path.join("_index.md").as_path(),
&config,
);
assert!(res.is_ok()); assert!(res.is_ok());
let page = res.unwrap(); let page = res.unwrap();

View file

@ -1,13 +1,12 @@
//! What we are sending to the templates when rendering them //! What we are sending to the templates when rendering them
use std::collections::HashMap; use std::collections::HashMap;
use tera::{Value, Map}; use tera::{Map, Value};
use library::Library;
use content::{Page, Section}; use content::{Page, Section};
use library::Library;
use rendering::Header; use rendering::Header;
#[derive(Clone, Debug, PartialEq, Serialize)] #[derive(Clone, Debug, PartialEq, Serialize)]
pub struct SerializingPage<'a> { pub struct SerializingPage<'a> {
relative_path: &'a str, relative_path: &'a str,
@ -49,11 +48,23 @@ impl<'a> SerializingPage<'a> {
day = Some(d.2); day = Some(d.2);
} }
let pages = library.pages(); let pages = library.pages();
let lighter = page.lighter.map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library)))); let lighter = page
let heavier = page.heavier.map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library)))); .lighter
let earlier = page.earlier.map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library)))); .map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library))));
let later = page.later.map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library)))); let heavier = page
let ancestors = page.ancestors.iter().map(|k| library.get_section_by_key(*k).file.relative.clone()).collect(); .heavier
.map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library))));
let earlier = page
.earlier
.map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library))));
let later = page
.later
.map(|k| Box::new(Self::from_page_basic(pages.get(k).unwrap(), Some(library))));
let ancestors = page
.ancestors
.iter()
.map(|k| library.get_section_by_key(*k).file.relative.clone())
.collect();
SerializingPage { SerializingPage {
relative_path: &page.file.relative, relative_path: &page.file.relative,
@ -95,7 +106,10 @@ impl<'a> SerializingPage<'a> {
day = Some(d.2); day = Some(d.2);
} }
let ancestors = if let Some(ref lib) = library { let ancestors = if let Some(ref lib) = library {
page.ancestors.iter().map(|k| lib.get_section_by_key(*k).file.relative.clone()).collect() page.ancestors
.iter()
.map(|k| lib.get_section_by_key(*k).file.relative.clone())
.collect()
} else { } else {
vec![] vec![]
}; };
@ -130,7 +144,6 @@ impl<'a> SerializingPage<'a> {
} }
} }
#[derive(Clone, Debug, PartialEq, Serialize)] #[derive(Clone, Debug, PartialEq, Serialize)]
pub struct SerializingSection<'a> { pub struct SerializingSection<'a> {
relative_path: &'a str, relative_path: &'a str,
@ -145,7 +158,7 @@ pub struct SerializingSection<'a> {
word_count: Option<usize>, word_count: Option<usize>,
reading_time: Option<usize>, reading_time: Option<usize>,
toc: &'a [Header], toc: &'a [Header],
assets: &'a [String], assets: &'a [String],
pages: Vec<SerializingPage<'a>>, pages: Vec<SerializingPage<'a>>,
subsections: Vec<&'a str>, subsections: Vec<&'a str>,
} }
@ -163,7 +176,11 @@ impl<'a> SerializingSection<'a> {
subsections.push(library.get_section_path_by_key(*k)); subsections.push(library.get_section_path_by_key(*k));
} }
let ancestors = section.ancestors.iter().map(|k| library.get_section_by_key(*k).file.relative.clone()).collect(); let ancestors = section
.ancestors
.iter()
.map(|k| library.get_section_by_key(*k).file.relative.clone())
.collect();
SerializingSection { SerializingSection {
relative_path: &section.file.relative, relative_path: &section.file.relative,
@ -187,7 +204,11 @@ impl<'a> SerializingSection<'a> {
/// Same as from_section but doesn't fetch pages and sections /// Same as from_section but doesn't fetch pages and sections
pub fn from_section_basic(section: &'a Section, library: Option<&'a Library>) -> Self { pub fn from_section_basic(section: &'a Section, library: Option<&'a Library>) -> Self {
let ancestors = if let Some(ref lib) = library { let ancestors = if let Some(ref lib) = library {
section.ancestors.iter().map(|k| lib.get_section_by_key(*k).file.relative.clone()).collect() section
.ancestors
.iter()
.map(|k| lib.get_section_by_key(*k).file.relative.clone())
.collect()
} else { } else {
vec![] vec![]
}; };

View file

@ -1,39 +1,39 @@
extern crate tera;
extern crate slug;
extern crate serde; extern crate serde;
extern crate slug;
extern crate tera;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
extern crate chrono; extern crate chrono;
extern crate slotmap;
extern crate rayon; extern crate rayon;
extern crate slotmap;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
extern crate regex; extern crate regex;
#[cfg(test)]
extern crate globset;
#[cfg(test)] #[cfg(test)]
extern crate tempfile; extern crate tempfile;
#[cfg(test)] #[cfg(test)]
extern crate toml; extern crate toml;
#[cfg(test)]
extern crate globset;
extern crate front_matter;
extern crate config; extern crate config;
extern crate utils; extern crate front_matter;
extern crate rendering; extern crate rendering;
extern crate utils;
#[macro_use] #[macro_use]
extern crate errors; extern crate errors;
mod content; mod content;
mod taxonomies; mod library;
mod pagination; mod pagination;
mod sorting; mod sorting;
mod library; mod taxonomies;
pub use slotmap::{Key, DenseSlotMap}; pub use slotmap::{DenseSlotMap, Key};
pub use sorting::sort_actual_pages_by_date; pub use content::{Page, Section, SerializingPage, SerializingSection};
pub use content::{Page, SerializingPage, Section, SerializingSection};
pub use library::Library; pub use library::Library;
pub use taxonomies::{Taxonomy, TaxonomyItem, find_taxonomies};
pub use pagination::Paginator; pub use pagination::Paginator;
pub use sorting::sort_actual_pages_by_date;
pub use taxonomies::{find_taxonomies, Taxonomy, TaxonomyItem};

View file

@ -5,9 +5,8 @@ use slotmap::{DenseSlotMap, Key};
use front_matter::SortBy; use front_matter::SortBy;
use sorting::{find_siblings, sort_pages_by_weight, sort_pages_by_date};
use content::{Page, Section}; use content::{Page, Section};
use sorting::{find_siblings, sort_pages_by_date, sort_pages_by_weight};
/// Houses everything about pages and sections /// Houses everything about pages and sections
/// Think of it as a database where each page and section has an id (Key here) /// Think of it as a database where each page and section has an id (Key here)
@ -81,12 +80,13 @@ impl Library {
/// Find out the direct subsections of each subsection if there are some /// Find out the direct subsections of each subsection if there are some
/// as well as the pages for each section /// as well as the pages for each section
pub fn populate_sections(&mut self) { pub fn populate_sections(&mut self) {
let (root_path, index_path) = self.sections let (root_path, index_path) = self
.sections
.values() .values()
.find(|s| s.is_index()) .find(|s| s.is_index())
.map(|s| (s.file.parent.clone(), s.file.path.clone())) .map(|s| (s.file.parent.clone(), s.file.path.clone()))
.unwrap(); .unwrap();
let root_key = self.paths_to_sections[&index_path]; let root_key = self.paths_to_sections[&index_path];
// We are going to get both the ancestors and grandparents for each section in one go // We are going to get both the ancestors and grandparents for each section in one go
let mut ancestors: HashMap<PathBuf, Vec<_>> = HashMap::new(); let mut ancestors: HashMap<PathBuf, Vec<_>> = HashMap::new();
@ -130,7 +130,8 @@ impl Library {
let parent_section_path = page.file.parent.join("_index.md"); let parent_section_path = page.file.parent.join("_index.md");
if let Some(section_key) = self.paths_to_sections.get(&parent_section_path) { if let Some(section_key) = self.paths_to_sections.get(&parent_section_path) {
self.sections.get_mut(*section_key).unwrap().pages.push(key); self.sections.get_mut(*section_key).unwrap().pages.push(key);
page.ancestors = ancestors.get(&parent_section_path).cloned().unwrap_or_else(|| vec![]); page.ancestors =
ancestors.get(&parent_section_path).cloned().unwrap_or_else(|| vec![]);
// Don't forget to push the actual parent // Don't forget to push the actual parent
page.ancestors.push(*section_key); page.ancestors.push(*section_key);
} }
@ -150,7 +151,8 @@ impl Library {
children.sort_by(|a, b| sections_weight[a].cmp(&sections_weight[b])); children.sort_by(|a, b| sections_weight[a].cmp(&sections_weight[b]));
section.subsections = children; section.subsections = children;
} }
section.ancestors = ancestors.get(&section.file.path).cloned().unwrap_or_else(|| vec![]); section.ancestors =
ancestors.get(&section.file.path).cloned().unwrap_or_else(|| vec![]);
} }
} }
@ -161,7 +163,8 @@ impl Library {
let (sorted_pages, cannot_be_sorted_pages) = match section.meta.sort_by { let (sorted_pages, cannot_be_sorted_pages) = match section.meta.sort_by {
SortBy::None => continue, SortBy::None => continue,
SortBy::Date => { SortBy::Date => {
let data = section.pages let data = section
.pages
.iter() .iter()
.map(|k| { .map(|k| {
if let Some(page) = self.pages.get(*k) { if let Some(page) = self.pages.get(*k) {
@ -173,9 +176,10 @@ impl Library {
.collect(); .collect();
sort_pages_by_date(data) sort_pages_by_date(data)
}, }
SortBy::Weight => { SortBy::Weight => {
let data = section.pages let data = section
.pages
.iter() .iter()
.map(|k| { .map(|k| {
if let Some(page) = self.pages.get(*k) { if let Some(page) = self.pages.get(*k) {
@ -194,13 +198,18 @@ impl Library {
for (key, (sorted, cannot_be_sorted, sort_by)) in updates { for (key, (sorted, cannot_be_sorted, sort_by)) in updates {
// Find sibling between sorted pages first // Find sibling between sorted pages first
let with_siblings = find_siblings(sorted.iter().map(|k| { let with_siblings = find_siblings(
if let Some(page) = self.pages.get(*k) { sorted
(k, page.is_draft()) .iter()
} else { .map(|k| {
unreachable!("Sorting got an unknown page") if let Some(page) = self.pages.get(*k) {
} (k, page.is_draft())
}).collect()); } else {
unreachable!("Sorting got an unknown page")
}
})
.collect(),
);
for (k2, val1, val2) in with_siblings { for (k2, val1, val2) in with_siblings {
if let Some(page) = self.pages.get_mut(k2) { if let Some(page) = self.pages.get_mut(k2) {
@ -208,12 +217,12 @@ impl Library {
SortBy::Date => { SortBy::Date => {
page.earlier = val2; page.earlier = val2;
page.later = val1; page.later = val1;
}, }
SortBy::Weight => { SortBy::Weight => {
page.lighter = val1; page.lighter = val1;
page.heavier = val2; page.heavier = val2;
}, }
SortBy::None => unreachable!("Impossible to find siblings in SortBy::None") SortBy::None => unreachable!("Impossible to find siblings in SortBy::None"),
} }
} else { } else {
unreachable!("Sorting got an unknown page") unreachable!("Sorting got an unknown page")
@ -229,10 +238,8 @@ impl Library {
/// Find all the orphan pages: pages that are in a folder without an `_index.md` /// Find all the orphan pages: pages that are in a folder without an `_index.md`
pub fn get_all_orphan_pages(&self) -> Vec<&Page> { pub fn get_all_orphan_pages(&self) -> Vec<&Page> {
let pages_in_sections = self.sections let pages_in_sections =
.values() self.sections.values().flat_map(|s| &s.pages).collect::<HashSet<_>>();
.flat_map(|s| &s.pages)
.collect::<HashSet<_>>();
self.pages self.pages
.iter() .iter()
@ -245,7 +252,7 @@ impl Library {
let page_key = self.paths_to_pages[path]; let page_key = self.paths_to_pages[path];
for s in self.sections.values() { for s in self.sections.values() {
if s.pages.contains(&page_key) { if s.pages.contains(&page_key) {
return Some(s) return Some(s);
} }
} }

View file

@ -1,16 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use tera::{Tera, Context, to_value, Value}; use slotmap::Key;
use slotmap::{Key}; use tera::{to_value, Context, Tera, Value};
use errors::{Result, ResultExt};
use config::Config; use config::Config;
use errors::{Result, ResultExt};
use utils::templates::render_template; use utils::templates::render_template;
use content::{Section, SerializingSection, SerializingPage}; use content::{Section, SerializingPage, SerializingSection};
use taxonomies::{TaxonomyItem, Taxonomy};
use library::Library; use library::Library;
use taxonomies::{Taxonomy, TaxonomyItem};
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
enum PaginationRoot<'a> { enum PaginationRoot<'a> {
@ -18,7 +17,6 @@ enum PaginationRoot<'a> {
Taxonomy(&'a Taxonomy), Taxonomy(&'a Taxonomy),
} }
/// 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
#[derive(Clone, Debug, PartialEq, Serialize)] #[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Pager<'a> { pub struct Pager<'a> {
@ -33,13 +31,13 @@ pub struct Pager<'a> {
} }
impl<'a> Pager<'a> { impl<'a> Pager<'a> {
fn new(index: usize, pages: Vec<SerializingPage<'a>>, permalink: String, path: String) -> Pager<'a> { fn new(
Pager { index: usize,
index, pages: Vec<SerializingPage<'a>>,
permalink, permalink: String,
path, path: String,
pages, ) -> Pager<'a> {
} Pager { index, permalink, path, pages }
} }
} }
@ -83,7 +81,11 @@ impl<'a> Paginator<'a> {
/// Create a new paginator from a taxonomy /// Create a new paginator from a taxonomy
/// It will always at least create one pager (the first) even if there are not enough pages to paginate /// It will always at least create one pager (the first) even if there are not enough pages to paginate
pub fn from_taxonomy(taxonomy: &'a Taxonomy, item: &'a TaxonomyItem, library: &'a Library) -> Paginator<'a> { pub fn from_taxonomy(
taxonomy: &'a Taxonomy,
item: &'a TaxonomyItem,
library: &'a Library,
) -> Paginator<'a> {
let paginate_by = taxonomy.kind.paginate_by.unwrap(); let paginate_by = taxonomy.kind.paginate_by.unwrap();
let mut paginator = Paginator { let mut paginator = Paginator {
all_pages: &item.pages, all_pages: &item.pages,
@ -92,7 +94,11 @@ impl<'a> Paginator<'a> {
root: PaginationRoot::Taxonomy(taxonomy), root: PaginationRoot::Taxonomy(taxonomy),
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.kind.paginate_path.clone().unwrap_or_else(|| "pages".to_string()), paginate_path: taxonomy
.kind
.paginate_path
.clone()
.unwrap_or_else(|| "pages".to_string()),
is_index: false, is_index: false,
}; };
@ -142,12 +148,7 @@ impl<'a> Paginator<'a> {
format!("{}/{}", self.path, page_path) format!("{}/{}", self.path, page_path)
}; };
pagers.push(Pager::new( pagers.push(Pager::new(index + 1, page, permalink, pager_path));
index + 1,
page,
permalink,
pager_path,
));
} }
// We always have the index one at least // We always have the index one at least
@ -184,19 +185,29 @@ impl<'a> Paginator<'a> {
paginator.insert("next", Value::Null); paginator.insert("next", Value::Null);
} }
paginator.insert("number_pagers", to_value(&self.pagers.len()).unwrap()); paginator.insert("number_pagers", to_value(&self.pagers.len()).unwrap());
paginator.insert("base_url", to_value(&format!("{}{}/", self.permalink, self.paginate_path)).unwrap()); paginator.insert(
"base_url",
to_value(&format!("{}{}/", self.permalink, self.paginate_path)).unwrap(),
);
paginator.insert("pages", to_value(&current_pager.pages).unwrap()); paginator.insert("pages", to_value(&current_pager.pages).unwrap());
paginator.insert("current_index", to_value(current_pager.index).unwrap()); paginator.insert("current_index", to_value(current_pager.index).unwrap());
paginator paginator
} }
pub fn render_pager(&self, pager: &Pager, config: &Config, tera: &Tera, library: &Library) -> Result<String> { pub fn render_pager(
&self,
pager: &Pager,
config: &Config,
tera: &Tera,
library: &Library,
) -> Result<String> {
let mut context = Context::new(); let mut context = Context::new();
context.insert("config", &config); context.insert("config", &config);
let template_name = match self.root { let template_name = match self.root {
PaginationRoot::Section(s) => { PaginationRoot::Section(s) => {
context.insert("section", &SerializingSection::from_section_basic(s, Some(library))); context
.insert("section", &SerializingSection::from_section_basic(s, Some(library)));
s.get_template_name() s.get_template_name()
} }
PaginationRoot::Taxonomy(t) => { PaginationRoot::Taxonomy(t) => {
@ -217,11 +228,11 @@ impl<'a> Paginator<'a> {
mod tests { mod tests {
use tera::to_value; use tera::to_value;
use front_matter::SectionFrontMatter;
use content::{Page, Section};
use config::Taxonomy as TaxonomyConfig; use config::Taxonomy as TaxonomyConfig;
use taxonomies::{Taxonomy, TaxonomyItem}; use content::{Page, Section};
use front_matter::SectionFrontMatter;
use library::Library; use library::Library;
use taxonomies::{Taxonomy, TaxonomyItem};
use super::Paginator; use super::Paginator;

View file

@ -1,8 +1,8 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use chrono::NaiveDateTime;
use rayon::prelude::*; use rayon::prelude::*;
use slotmap::Key; use slotmap::Key;
use chrono::NaiveDateTime;
use content::Page; use content::Page;
@ -21,19 +21,17 @@ pub fn sort_actual_pages_by_date(a: &&Page, b: &&Page) -> Ordering {
/// Pages without date will be put in the unsortable bucket /// Pages without date will be put in the unsortable bucket
/// The permalink is used to break ties /// The permalink is used to break ties
pub fn sort_pages_by_date(pages: Vec<(&Key, Option<NaiveDateTime>, &str)>) -> (Vec<Key>, Vec<Key>) { pub fn sort_pages_by_date(pages: Vec<(&Key, Option<NaiveDateTime>, &str)>) -> (Vec<Key>, Vec<Key>) {
let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) = pages let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) =
.into_par_iter() pages.into_par_iter().partition(|page| page.1.is_some());
.partition(|page| page.1.is_some());
can_be_sorted can_be_sorted.par_sort_unstable_by(|a, b| {
.par_sort_unstable_by(|a, b| { let ord = b.1.unwrap().cmp(&a.1.unwrap());
let ord = b.1.unwrap().cmp(&a.1.unwrap()); if ord == Ordering::Equal {
if ord == Ordering::Equal { a.2.cmp(&b.2)
a.2.cmp(&b.2) } else {
} else { ord
ord }
} });
});
(can_be_sorted.iter().map(|p| *p.0).collect(), cannot_be_sorted.iter().map(|p| *p.0).collect()) (can_be_sorted.iter().map(|p| *p.0).collect(), cannot_be_sorted.iter().map(|p| *p.0).collect())
} }
@ -42,19 +40,17 @@ pub fn sort_pages_by_date(pages: Vec<(&Key, Option<NaiveDateTime>, &str)>) -> (V
/// Pages without weight will be put in the unsortable bucket /// Pages without weight will be put in the unsortable bucket
/// The permalink is used to break ties /// The permalink is used to break ties
pub fn sort_pages_by_weight(pages: Vec<(&Key, Option<usize>, &str)>) -> (Vec<Key>, Vec<Key>) { pub fn sort_pages_by_weight(pages: Vec<(&Key, Option<usize>, &str)>) -> (Vec<Key>, Vec<Key>) {
let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) = pages let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) =
.into_par_iter() pages.into_par_iter().partition(|page| page.1.is_some());
.partition(|page| page.1.is_some());
can_be_sorted can_be_sorted.par_sort_unstable_by(|a, b| {
.par_sort_unstable_by(|a, b| { let ord = a.1.unwrap().cmp(&b.1.unwrap());
let ord = a.1.unwrap().cmp(&b.1.unwrap()); if ord == Ordering::Equal {
if ord == Ordering::Equal { a.2.cmp(&b.2)
a.2.cmp(&b.2) } else {
} else { ord
ord }
} });
});
(can_be_sorted.iter().map(|p| *p.0).collect(), cannot_be_sorted.iter().map(|p| *p.0).collect()) (can_be_sorted.iter().map(|p| *p.0).collect(), cannot_be_sorted.iter().map(|p| *p.0).collect())
} }
@ -118,9 +114,9 @@ pub fn find_siblings(sorted: Vec<(&Key, bool)>) -> Vec<(Key, Option<Key>, Option
mod tests { mod tests {
use slotmap::DenseSlotMap; use slotmap::DenseSlotMap;
use front_matter::{PageFrontMatter}; use super::{find_siblings, sort_pages_by_date, sort_pages_by_weight};
use content::Page; use content::Page;
use super::{sort_pages_by_date, sort_pages_by_weight, find_siblings}; use front_matter::PageFrontMatter;
fn create_page_with_date(date: &str) -> Page { fn create_page_with_date(date: &str) -> Page {
let mut front_matter = PageFrontMatter::default(); let mut front_matter = PageFrontMatter::default();
@ -179,7 +175,6 @@ mod tests {
assert_eq!(pages[2], key2); assert_eq!(pages[2], key2);
} }
#[test] #[test]
fn ignore_page_with_missing_field() { fn ignore_page_with_missing_field() {
let mut dense = DenseSlotMap::new(); let mut dense = DenseSlotMap::new();
@ -196,7 +191,7 @@ mod tests {
(&key3, page3.meta.weight, page3.permalink.as_ref()), (&key3, page3.meta.weight, page3.permalink.as_ref()),
]; ];
let (pages,unsorted) = sort_pages_by_weight(input); let (pages, unsorted) = sort_pages_by_weight(input);
assert_eq!(pages.len(), 2); assert_eq!(pages.len(), 2);
assert_eq!(unsorted.len(), 1); assert_eq!(unsorted.len(), 1);
} }
@ -211,11 +206,8 @@ mod tests {
let page3 = create_page_with_weight(3); let page3 = create_page_with_weight(3);
let key3 = dense.insert(page3.clone()); let key3 = dense.insert(page3.clone());
let input = vec![ let input =
(&key1, page1.is_draft()), vec![(&key1, page1.is_draft()), (&key2, page2.is_draft()), (&key3, page3.is_draft())];
(&key2, page2.is_draft()),
(&key3, page3.is_draft()),
];
let pages = find_siblings(input); let pages = find_siblings(input);

View file

@ -1,16 +1,16 @@
use std::collections::HashMap; use std::collections::HashMap;
use slotmap::Key;
use slug::slugify; use slug::slugify;
use tera::{Context, Tera}; use tera::{Context, Tera};
use slotmap::{Key};
use config::{Config, Taxonomy as TaxonomyConfig}; use config::{Config, Taxonomy as TaxonomyConfig};
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use utils::templates::render_template; use utils::templates::render_template;
use content::SerializingPage; use content::SerializingPage;
use sorting::sort_pages_by_date;
use library::Library; use library::Library;
use sorting::sort_pages_by_date;
#[derive(Debug, Clone, PartialEq, Serialize)] #[derive(Debug, Clone, PartialEq, Serialize)]
struct SerializedTaxonomyItem<'a> { struct SerializedTaxonomyItem<'a> {
@ -34,7 +34,6 @@ impl<'a> SerializedTaxonomyItem<'a> {
slug: &item.slug, slug: &item.slug,
permalink: &item.permalink, permalink: &item.permalink,
pages, pages,
} }
} }
} }
@ -70,12 +69,7 @@ impl TaxonomyItem {
// We still append pages without dates at the end // We still append pages without dates at the end
pages.extend(ignored_pages); pages.extend(ignored_pages);
TaxonomyItem { TaxonomyItem { name: name.to_string(), permalink, slug, pages }
name: name.to_string(),
permalink,
slug,
pages,
}
} }
} }
@ -87,11 +81,9 @@ pub struct SerializedTaxonomy<'a> {
impl<'a> SerializedTaxonomy<'a> { impl<'a> SerializedTaxonomy<'a> {
pub fn from_taxonomy(taxonomy: &'a Taxonomy, library: &'a Library) -> Self { pub fn from_taxonomy(taxonomy: &'a Taxonomy, library: &'a Library) -> Self {
let items: Vec<SerializedTaxonomyItem> = taxonomy.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect(); let items: Vec<SerializedTaxonomyItem> =
SerializedTaxonomy { taxonomy.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect();
kind: &taxonomy.kind, SerializedTaxonomy { kind: &taxonomy.kind, items }
items,
}
} }
} }
@ -104,19 +96,19 @@ pub struct Taxonomy {
} }
impl Taxonomy { impl Taxonomy {
fn new(kind: TaxonomyConfig, config: &Config, items: HashMap<String, Vec<Key>>, library: &Library) -> Taxonomy { fn new(
kind: TaxonomyConfig,
config: &Config,
items: HashMap<String, Vec<Key>>,
library: &Library,
) -> Taxonomy {
let mut sorted_items = vec![]; let mut sorted_items = vec![];
for (name, pages) in items { for (name, pages) in items {
sorted_items.push( sorted_items.push(TaxonomyItem::new(&name, &kind.name, config, pages, library));
TaxonomyItem::new(&name, &kind.name, 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));
Taxonomy { Taxonomy { kind, items: sorted_items }
kind,
items: sorted_items,
}
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
@ -127,22 +119,37 @@ impl Taxonomy {
self.len() == 0 self.len() == 0
} }
pub fn render_term(&self, item: &TaxonomyItem, tera: &Tera, config: &Config, library: &Library) -> Result<String> { pub fn render_term(
&self,
item: &TaxonomyItem,
tera: &Tera,
config: &Config,
library: &Library,
) -> Result<String> {
let mut context = Context::new(); let mut context = Context::new();
context.insert("config", config); context.insert("config", config);
context.insert("term", &SerializedTaxonomyItem::from_item(item, library)); context.insert("term", &SerializedTaxonomyItem::from_item(item, library));
context.insert("taxonomy", &self.kind); context.insert("taxonomy", &self.kind);
context.insert("current_url", &config.make_permalink(&format!("{}/{}", self.kind.name, item.slug))); context.insert(
"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) render_template(&format!("{}/single.html", self.kind.name), tera, &context, &config.theme)
.chain_err(|| format!("Failed to render single term {} page.", self.kind.name)) .chain_err(|| format!("Failed to render single term {} page.", self.kind.name))
} }
pub fn render_all_terms(&self, tera: &Tera, config: &Config, library: &Library) -> Result<String> { pub fn render_all_terms(
&self,
tera: &Tera,
config: &Config,
library: &Library,
) -> Result<String> {
let mut context = Context::new(); let mut context = Context::new();
context.insert("config", config); context.insert("config", config);
let terms: Vec<SerializedTaxonomyItem> = self.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect(); let terms: Vec<SerializedTaxonomyItem> =
self.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect();
context.insert("terms", &terms); context.insert("terms", &terms);
context.insert("taxonomy", &self.kind); context.insert("taxonomy", &self.kind);
context.insert("current_url", &config.make_permalink(&self.kind.name)); context.insert("current_url", &config.make_permalink(&self.kind.name));
@ -175,19 +182,22 @@ pub fn find_taxonomies(config: &Config, library: &Library) -> Result<Vec<Taxonom
for (name, val) in &page.meta.taxonomies { for (name, val) in &page.meta.taxonomies {
if taxonomies_def.contains_key(name) { if taxonomies_def.contains_key(name) {
all_taxonomies all_taxonomies.entry(name).or_insert_with(HashMap::new);
.entry(name)
.or_insert_with(HashMap::new);
for v in val { for v in val {
all_taxonomies.get_mut(name) all_taxonomies
.get_mut(name)
.unwrap() .unwrap()
.entry(v.to_string()) .entry(v.to_string())
.or_insert_with(|| vec![]) .or_insert_with(|| vec![])
.push(key); .push(key);
} }
} else { } else {
bail!("Page `{}` has taxonomy `{}` which is not defined in config.toml", page.file.path.display(), name); bail!(
"Page `{}` has taxonomy `{}` which is not defined in config.toml",
page.file.path.display(),
name
);
} }
} }
} }
@ -201,7 +211,6 @@ pub fn find_taxonomies(config: &Config, library: &Library) -> Result<Vec<Taxonom
Ok(taxonomies) Ok(taxonomies)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -284,7 +293,10 @@ mod tests {
assert_eq!(categories.items[1].name, "Programming tutorials"); assert_eq!(categories.items[1].name, "Programming tutorials");
assert_eq!(categories.items[1].slug, "programming-tutorials"); assert_eq!(categories.items[1].slug, "programming-tutorials");
assert_eq!(categories.items[1].permalink, "http://a-website.com/categories/programming-tutorials/"); assert_eq!(
categories.items[1].permalink,
"http://a-website.com/categories/programming-tutorials/"
);
assert_eq!(categories.items[1].pages.len(), 1); assert_eq!(categories.items[1].pages.len(), 1);
} }
@ -293,9 +305,8 @@ mod tests {
let mut config = Config::default(); let mut config = Config::default();
let mut library = Library::new(2, 0); let mut library = Library::new(2, 0);
config.taxonomies = vec![ config.taxonomies =
TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() }, vec![TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() }];
];
let mut page1 = Page::default(); let mut page1 = Page::default();
let mut taxo_page1 = HashMap::new(); let mut taxo_page1 = HashMap::new();
taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]); taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
@ -306,6 +317,9 @@ mod tests {
assert!(taxonomies.is_err()); assert!(taxonomies.is_err());
let err = taxonomies.unwrap_err(); let err = taxonomies.unwrap_err();
// no path as this is created by Default // no path as this is created by Default
assert_eq!(err.description(), "Page `` has taxonomy `tags` which is not defined in config.toml"); assert_eq!(
err.description(),
"Page `` has taxonomy `tags` which is not defined in config.toml"
);
} }
} }

View file

@ -3,7 +3,7 @@ extern crate reqwest;
extern crate lazy_static; extern crate lazy_static;
use reqwest::header::{HeaderMap, ACCEPT}; use reqwest::header::{HeaderMap, ACCEPT};
use reqwest::{StatusCode}; use reqwest::StatusCode;
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
@ -62,14 +62,8 @@ pub fn check_url(url: &str) -> LinkResult {
// Need to actually do the link checking // Need to actually do the link checking
let res = match client.get(url).headers(headers).send() { let res = match client.get(url).headers(headers).send() {
Ok(response) => LinkResult { Ok(response) => LinkResult { code: Some(response.status()), error: None },
code: Some(response.status()), Err(e) => LinkResult { code: None, error: Some(e.description().to_string()) },
error: None,
},
Err(e) => LinkResult {
code: None,
error: Some(e.description().to_string()),
},
}; };
LINKS.write().unwrap().insert(url.to_string(), res.clone()); LINKS.write().unwrap().insert(url.to_string(), res.clone());

View file

@ -1,16 +1,15 @@
extern crate site; extern crate site;
#[macro_use] #[macro_use]
extern crate errors; extern crate errors;
extern crate library;
extern crate front_matter; extern crate front_matter;
extern crate library;
use std::path::{Path, Component}; use std::path::{Component, Path};
use errors::Result; use errors::Result;
use site::Site;
use library::{Page, Section};
use front_matter::{PageFrontMatter, SectionFrontMatter}; use front_matter::{PageFrontMatter, SectionFrontMatter};
use library::{Page, Section};
use site::Site;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum PageChangesNeeded { pub enum PageChangesNeeded {
@ -37,7 +36,10 @@ pub enum SectionChangesNeeded {
/// Evaluates all the params in the front matter that changed so we can do the smallest /// Evaluates all the params in the front matter that changed so we can do the smallest
/// delta in the serve command /// delta in the serve command
/// Order matters as the actions will be done in insertion order /// Order matters as the actions will be done in insertion order
fn find_section_front_matter_changes(current: &SectionFrontMatter, new: &SectionFrontMatter) -> Vec<SectionChangesNeeded> { fn find_section_front_matter_changes(
current: &SectionFrontMatter,
new: &SectionFrontMatter,
) -> Vec<SectionChangesNeeded> {
let mut changes_needed = vec![]; let mut changes_needed = vec![];
if current.sort_by != new.sort_by { if current.sort_by != new.sort_by {
@ -54,7 +56,8 @@ fn find_section_front_matter_changes(current: &SectionFrontMatter, new: &Section
if current.paginate_by != new.paginate_by if current.paginate_by != new.paginate_by
|| current.paginate_path != new.paginate_path || current.paginate_path != new.paginate_path
|| current.insert_anchor_links != new.insert_anchor_links { || current.insert_anchor_links != new.insert_anchor_links
{
changes_needed.push(SectionChangesNeeded::RenderWithPages); changes_needed.push(SectionChangesNeeded::RenderWithPages);
// Nothing else we can do // Nothing else we can do
return changes_needed; return changes_needed;
@ -68,14 +71,18 @@ fn find_section_front_matter_changes(current: &SectionFrontMatter, new: &Section
/// Evaluates all the params in the front matter that changed so we can do the smallest /// Evaluates all the params in the front matter that changed so we can do the smallest
/// delta in the serve command /// delta in the serve command
/// Order matters as the actions will be done in insertion order /// Order matters as the actions will be done in insertion order
fn find_page_front_matter_changes(current: &PageFrontMatter, other: &PageFrontMatter) -> Vec<PageChangesNeeded> { fn find_page_front_matter_changes(
current: &PageFrontMatter,
other: &PageFrontMatter,
) -> Vec<PageChangesNeeded> {
let mut changes_needed = vec![]; let mut changes_needed = vec![];
if current.taxonomies != other.taxonomies { if current.taxonomies != other.taxonomies {
changes_needed.push(PageChangesNeeded::Taxonomies); changes_needed.push(PageChangesNeeded::Taxonomies);
} }
if current.date != other.date || current.order != other.order || current.weight != other.weight { if current.date != other.date || current.order != other.order || current.weight != other.weight
{
changes_needed.push(PageChangesNeeded::Sort); changes_needed.push(PageChangesNeeded::Sort);
} }
@ -86,7 +93,9 @@ fn find_page_front_matter_changes(current: &PageFrontMatter, other: &PageFrontMa
/// Handles a path deletion: could be a page, a section, a folder /// Handles a path deletion: could be a page, a section, a folder
fn delete_element(site: &mut Site, path: &Path, is_section: bool) -> Result<()> { fn delete_element(site: &mut Site, path: &Path, is_section: bool) -> Result<()> {
// Ignore the event if this path was not known // Ignore the event if this path was not known
if !site.library.contains_section(&path.to_path_buf()) && !site.library.contains_page(&path.to_path_buf()) { if !site.library.contains_section(&path.to_path_buf())
&& !site.library.contains_page(&path.to_path_buf())
{
return Ok(()); return Ok(());
} }
@ -127,14 +136,21 @@ fn handle_section_editing(site: &mut Site, path: &Path) -> Result<()> {
} }
// Front matter changed // Front matter changed
for changes in find_section_front_matter_changes(&site.library.get_section(&pathbuf).unwrap().meta, &prev.meta) { for changes in find_section_front_matter_changes(
&site.library.get_section(&pathbuf).unwrap().meta,
&prev.meta,
) {
// Sort always comes first if present so the rendering will be fine // Sort always comes first if present so the rendering will be fine
match changes { match changes {
SectionChangesNeeded::Sort => { SectionChangesNeeded::Sort => {
site.register_tera_global_fns(); site.register_tera_global_fns();
} }
SectionChangesNeeded::Render => site.render_section(&site.library.get_section(&pathbuf).unwrap(), false)?, SectionChangesNeeded::Render => {
SectionChangesNeeded::RenderWithPages => site.render_section(&site.library.get_section(&pathbuf).unwrap(), true)?, site.render_section(&site.library.get_section(&pathbuf).unwrap(), false)?
}
SectionChangesNeeded::RenderWithPages => {
site.render_section(&site.library.get_section(&pathbuf).unwrap(), true)?
}
// not a common enough operation to make it worth optimizing // not a common enough operation to make it worth optimizing
SectionChangesNeeded::Delete => { SectionChangesNeeded::Delete => {
site.build()?; site.build()?;
@ -157,7 +173,7 @@ macro_rules! render_parent_section {
if let Some(s) = $site.library.find_parent_section($path) { if let Some(s) = $site.library.find_parent_section($path) {
$site.render_section(s, false)?; $site.render_section(s, false)?;
}; };
} };
} }
/// Handles a page being edited in some ways /// Handles a page being edited in some ways
@ -181,7 +197,10 @@ fn handle_page_editing(site: &mut Site, path: &Path) -> Result<()> {
} }
// Front matter changed // Front matter changed
for changes in find_page_front_matter_changes(&site.library.get_page(&pathbuf).unwrap().meta, &prev.meta) { for changes in find_page_front_matter_changes(
&site.library.get_page(&pathbuf).unwrap().meta,
&prev.meta,
) {
site.register_tera_global_fns(); site.register_tera_global_fns();
// Sort always comes first if present so the rendering will be fine // Sort always comes first if present so the rendering will be fine
@ -213,7 +232,6 @@ fn handle_page_editing(site: &mut Site, path: &Path) -> Result<()> {
} }
} }
/// What happens when a section or a page is changed /// What happens when a section or a page is changed
pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> { pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
let is_section = path.file_name().unwrap() == "_index.md"; let is_section = path.file_name().unwrap() == "_index.md";
@ -294,16 +312,15 @@ pub fn after_template_change(site: &mut Site, path: &Path) -> Result<()> {
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use front_matter::{PageFrontMatter, SectionFrontMatter, SortBy};
use super::{ use super::{
find_page_front_matter_changes, find_section_front_matter_changes, find_page_front_matter_changes, find_section_front_matter_changes, PageChangesNeeded,
PageChangesNeeded, SectionChangesNeeded, SectionChangesNeeded,
}; };
use front_matter::{PageFrontMatter, SectionFrontMatter, SortBy};
#[test] #[test]
fn can_find_taxonomy_changes_in_page_frontmatter() { fn can_find_taxonomy_changes_in_page_frontmatter() {
@ -320,7 +337,10 @@ mod tests {
taxonomies.insert("categories".to_string(), vec!["a category".to_string()]); taxonomies.insert("categories".to_string(), vec!["a category".to_string()]);
let current = PageFrontMatter { taxonomies, order: Some(1), ..PageFrontMatter::default() }; let current = PageFrontMatter { taxonomies, order: Some(1), ..PageFrontMatter::default() };
let changes = find_page_front_matter_changes(&current, &PageFrontMatter::default()); let changes = find_page_front_matter_changes(&current, &PageFrontMatter::default());
assert_eq!(changes, vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Sort, PageChangesNeeded::Render]); assert_eq!(
changes,
vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Sort, PageChangesNeeded::Render]
);
} }
#[test] #[test]

View file

@ -1,89 +1,88 @@
extern crate fs_extra;
extern crate rebuild; extern crate rebuild;
extern crate site; extern crate site;
extern crate tempfile; extern crate tempfile;
extern crate fs_extra;
use std::env; use std::env;
use std::fs::{remove_dir_all, File}; use std::fs::{remove_dir_all, File};
use std::io::prelude::*; use std::io::prelude::*;
use fs_extra::dir; use fs_extra::dir;
use tempfile::tempdir;
use site::Site; use site::Site;
use tempfile::tempdir;
use rebuild::after_content_change; use rebuild::after_content_change;
// Loads the test_site in a tempdir and build it there // Loads the test_site in a tempdir and build it there
// Returns (site_path_in_tempdir, site) // Returns (site_path_in_tempdir, site)
macro_rules! load_and_build_site { macro_rules! load_and_build_site {
($tmp_dir: expr) => { ($tmp_dir: expr) => {{
{ let mut path =
let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site"); path.push("test_site");
let mut options = dir::CopyOptions::new(); let mut options = dir::CopyOptions::new();
options.copy_inside = true; options.copy_inside = true;
dir::copy(&path, &$tmp_dir, &options).unwrap(); dir::copy(&path, &$tmp_dir, &options).unwrap();
let site_path = $tmp_dir.path().join("test_site"); let site_path = $tmp_dir.path().join("test_site");
// delete useless sections for those tests // delete useless sections for those tests
remove_dir_all(site_path.join("content").join("paginated")).unwrap(); remove_dir_all(site_path.join("content").join("paginated")).unwrap();
remove_dir_all(site_path.join("content").join("posts")).unwrap(); remove_dir_all(site_path.join("content").join("posts")).unwrap();
let mut site = Site::new(&site_path, "config.toml").unwrap(); let mut site = Site::new(&site_path, "config.toml").unwrap();
site.load().unwrap(); site.load().unwrap();
let public = &site_path.join("public"); let public = &site_path.join("public");
site.set_output_path(&public); site.set_output_path(&public);
site.build().unwrap(); site.build().unwrap();
(site_path, site) (site_path, site)
} }};
}
} }
/// Replace the file at the path (starting from root) by the given content /// Replace the file at the path (starting from root) by the given content
/// and return the file path that was modified /// and return the file path that was modified
macro_rules! edit_file { macro_rules! edit_file {
($site_path: expr, $path: expr, $content: expr) => { ($site_path: expr, $path: expr, $content: expr) => {{
{ let mut t = $site_path.clone();
let mut t = $site_path.clone(); for c in $path.split('/') {
for c in $path.split('/') { t.push(c);
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
} }
} 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 { macro_rules! file_contains {
($site_path: expr, $path: expr, $text: expr) => { ($site_path: expr, $path: expr, $text: expr) => {{
{ let mut path = $site_path.clone();
let mut path = $site_path.clone(); for component in $path.split("/") {
for component in $path.split("/") { path.push(component);
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)
} }
} 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)
}};
} }
#[test] #[test]
fn can_rebuild_after_simple_change_to_page_content() { fn can_rebuild_after_simple_change_to_page_content() {
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 file_path = edit_file!(site_path, "content/rebuild/first.md", br#" let file_path = edit_file!(
site_path,
"content/rebuild/first.md",
br#"
+++ +++
title = "first" title = "first"
weight = 1 weight = 1
date = 2017-01-01 date = 2017-01-01
+++ +++
Some content"#); Some content"#
);
let res = after_content_change(&mut site, &file_path); let res = after_content_change(&mut site, &file_path);
assert!(res.is_ok()); assert!(res.is_ok());
@ -94,14 +93,18 @@ Some content"#);
fn can_rebuild_after_title_change_page_global_func_usage() { fn can_rebuild_after_title_change_page_global_func_usage() {
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 file_path = edit_file!(site_path, "content/rebuild/first.md", br#" let file_path = edit_file!(
site_path,
"content/rebuild/first.md",
br#"
+++ +++
title = "Premier" title = "Premier"
weight = 10 weight = 10
date = 2017-01-01 date = 2017-01-01
+++ +++
# A title"#); # A title"#
);
let res = after_content_change(&mut site, &file_path); let res = after_content_change(&mut site, &file_path);
assert!(res.is_ok()); assert!(res.is_ok());
@ -112,15 +115,23 @@ date = 2017-01-01
fn can_rebuild_after_sort_change_in_section() { fn can_rebuild_after_sort_change_in_section() {
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 file_path = edit_file!(site_path, "content/rebuild/_index.md", br#" let file_path = edit_file!(
site_path,
"content/rebuild/_index.md",
br#"
+++ +++
paginate_by = 1 paginate_by = 1
sort_by = "weight" sort_by = "weight"
template = "rebuild.html" template = "rebuild.html"
+++ +++
"#); "#
);
let res = after_content_change(&mut site, &file_path); let res = after_content_change(&mut site, &file_path);
assert!(res.is_ok()); assert!(res.is_ok());
assert!(file_contains!(site_path, "public/rebuild/index.html", "<h1>first</h1><h1>second</h1>")); assert!(file_contains!(
site_path,
"public/rebuild/index.html",
"<h1>first</h1><h1>second</h1>"
));
} }

View file

@ -1,18 +1,18 @@
#![feature(test)] #![feature(test)]
extern crate test;
extern crate tera; extern crate tera;
extern crate test;
extern crate rendering;
extern crate config; extern crate config;
extern crate front_matter; extern crate front_matter;
extern crate rendering;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use tera::Tera;
use rendering::{RenderContext, render_content, render_shortcodes};
use front_matter::InsertAnchor;
use config::Config; use config::Config;
use front_matter::InsertAnchor;
use rendering::{render_content, render_shortcodes, RenderContext};
use tera::Tera;
static CONTENT: &'static str = r#" static CONTENT: &'static str = r#"
# Modus cognitius profanam ne duae virtutis mundi # Modus cognitius profanam ne duae virtutis mundi
@ -92,7 +92,8 @@ fn bench_render_content_with_highlighting(b: &mut test::Bencher) {
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap(); tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None); let context =
RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None);
b.iter(|| render_content(CONTENT, &context).unwrap()); b.iter(|| render_content(CONTENT, &context).unwrap());
} }
@ -103,7 +104,8 @@ fn bench_render_content_without_highlighting(b: &mut test::Bencher) {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut config = Config::default(); let mut config = Config::default();
config.highlight_code = false; config.highlight_code = false;
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None); let context =
RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None);
b.iter(|| render_content(CONTENT, &context).unwrap()); b.iter(|| render_content(CONTENT, &context).unwrap());
} }
@ -114,7 +116,8 @@ fn bench_render_content_no_shortcode(b: &mut test::Bencher) {
let mut config = Config::default(); let mut config = Config::default();
config.highlight_code = false; config.highlight_code = false;
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None); let context =
RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None);
b.iter(|| render_content(&content2, &context).unwrap()); b.iter(|| render_content(&content2, &context).unwrap());
} }
@ -125,8 +128,8 @@ fn bench_render_shortcodes_one_present(b: &mut test::Bencher) {
tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap(); tera.add_raw_template("shortcodes/youtube.html", "{{id}}").unwrap();
let config = Config::default(); let config = Config::default();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None); let context =
RenderContext::new(&tera, &config, "", &permalinks_ctx, Path::new(""), InsertAnchor::None);
b.iter(|| render_shortcodes(CONTENT, &context)); b.iter(|| render_shortcodes(CONTENT, &context));
} }

View file

@ -1,9 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use tera::{Tera, Context};
use front_matter::InsertAnchor;
use config::Config; use config::Config;
use front_matter::InsertAnchor;
use tera::{Context, Tera};
/// All the information from the zola site that is needed to render HTML from markdown /// All the information from the zola site that is needed to render HTML from markdown
#[derive(Debug)] #[derive(Debug)]

View file

@ -1,35 +1,35 @@
extern crate tera;
extern crate syntect;
extern crate pulldown_cmark; extern crate pulldown_cmark;
extern crate slug; extern crate slug;
extern crate syntect;
extern crate tera;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
extern crate serde;
extern crate pest; extern crate pest;
extern crate serde;
#[macro_use] #[macro_use]
extern crate pest_derive; extern crate pest_derive;
#[macro_use] #[macro_use]
extern crate errors; extern crate errors;
extern crate front_matter;
extern crate utils;
extern crate config; extern crate config;
extern crate front_matter;
extern crate link_checker; extern crate link_checker;
extern crate utils;
#[cfg(test)] #[cfg(test)]
extern crate templates; extern crate templates;
mod context; mod context;
mod markdown; mod markdown;
mod table_of_contents;
mod shortcode; mod shortcode;
mod table_of_contents;
use errors::Result; use errors::Result;
use markdown::markdown_to_html;
pub use table_of_contents::Header;
pub use shortcode::render_shortcodes;
pub use context::RenderContext; pub use context::RenderContext;
use markdown::markdown_to_html;
pub use shortcode::render_shortcodes;
pub use table_of_contents::Header;
pub fn render_content(content: &str, context: &RenderContext) -> Result<markdown::Rendered> { pub fn render_content(content: &str, context: &RenderContext) -> Result<markdown::Rendered> {
// Don't do anything if there is nothing like a shortcode in the content // Don't do anything if there is nothing like a shortcode in the content

View file

@ -1,18 +1,20 @@
use std::borrow::Cow::{Owned, Borrowed}; use std::borrow::Cow::{Borrowed, Owned};
use self::cmark::{Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES, OPTION_ENABLE_TABLES};
use pulldown_cmark as cmark; use pulldown_cmark as cmark;
use self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
use slug::slugify; use slug::slugify;
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::html::{start_highlighted_html_snippet, styled_line_to_highlighted_html, IncludeBackground}; use syntect::html::{
start_highlighted_html_snippet, styled_line_to_highlighted_html, IncludeBackground,
};
use config::highlighting::{get_highlighter, SYNTAX_SET, THEME_SET};
use errors::Result; use errors::Result;
use utils::site::resolve_internal_link;
use config::highlighting::{get_highlighter, THEME_SET, SYNTAX_SET};
use link_checker::check_url; use link_checker::check_url;
use utils::site::resolve_internal_link;
use table_of_contents::{TempHeader, Header, make_table_of_contents};
use context::RenderContext; use context::RenderContext;
use table_of_contents::{make_table_of_contents, Header, TempHeader};
const CONTINUE_READING: &str = "<p><a name=\"continue-reading\"></a></p>\n"; const CONTINUE_READING: &str = "<p><a name=\"continue-reading\"></a></p>\n";
@ -113,7 +115,8 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
let theme = &THEME_SET.themes[&context.config.highlight_theme]; let theme = &THEME_SET.themes[&context.config.highlight_theme];
highlighter = Some(get_highlighter(info, &context.config)); highlighter = Some(get_highlighter(info, &context.config));
// This selects the background color the same way that start_coloured_html_snippet does // This selects the background color the same way that start_coloured_html_snippet does
let color = theme.settings.background.unwrap_or(::syntect::highlighting::Color::WHITE); let color =
theme.settings.background.unwrap_or(::syntect::highlighting::Color::WHITE);
background = IncludeBackground::IfDifferent(color); background = IncludeBackground::IfDifferent(color);
let snippet = start_highlighted_html_snippet(theme); let snippet = start_highlighted_html_snippet(theme);
Event::Html(Owned(snippet.0)) Event::Html(Owned(snippet.0))
@ -128,12 +131,10 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
} }
Event::Start(Tag::Image(src, title)) => { Event::Start(Tag::Image(src, title)) => {
if is_colocated_asset_link(&src) { if is_colocated_asset_link(&src) {
return Event::Start( return Event::Start(Tag::Image(
Tag::Image( Owned(format!("{}{}", context.current_page_permalink, src)),
Owned(format!("{}{}", context.current_page_permalink, src)), title,
title, ));
)
);
} }
Event::Start(Tag::Image(src, title)) Event::Start(Tag::Image(src, title))
@ -157,13 +158,14 @@ pub fn markdown_to_html(content: &str, context: &RenderContext) -> Result<Render
format!("{}{}", context.current_page_permalink, link) format!("{}{}", context.current_page_permalink, link)
} else if context.config.check_external_links } else if context.config.check_external_links
&& !link.starts_with('#') && !link.starts_with('#')
&& !link.starts_with("mailto:") { && !link.starts_with("mailto:")
{
let res = check_url(&link); let res = check_url(&link);
if res.is_valid() { if res.is_valid() {
link.to_string() link.to_string()
} else { } else {
error = Some( error = Some(
format!("Link {} is not valid: {}", link, res.message()).into() format!("Link {} is not valid: {}", link, res.message()).into(),
); );
String::new() String::new()
} }

View file

@ -1,9 +1,9 @@
use pest::Parser;
use pest::iterators::Pair; use pest::iterators::Pair;
use tera::{Map, Context, Value, to_value}; use pest::Parser;
use tera::{to_value, Context, Map, Value};
use context::RenderContext;
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use ::context::RenderContext;
// This include forces recompiling this source file if the grammar file changes. // This include forces recompiling this source file if the grammar file changes.
// Uncomment it when doing changes to the .pest file // Uncomment it when doing changes to the .pest file
@ -13,7 +13,6 @@ const _GRAMMAR: &str = include_str!("content.pest");
#[grammar = "content.pest"] #[grammar = "content.pest"]
pub struct ContentParser; pub struct ContentParser;
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(),
@ -39,7 +38,7 @@ fn parse_literal(pair: Pair<Rule>) -> Value {
Rule::int => { Rule::int => {
val = Some(to_value(p.as_str().parse::<i64>().unwrap()).unwrap()); val = Some(to_value(p.as_str().parse::<i64>().unwrap()).unwrap());
} }
_ => unreachable!("Unknown literal: {:?}", p) _ => unreachable!("Unknown literal: {:?}", p),
}; };
} }
@ -53,20 +52,29 @@ fn parse_shortcode_call(pair: Pair<Rule>) -> (String, Map<String, Value>) {
for p in pair.into_inner() { for p in pair.into_inner() {
match p.as_rule() { match p.as_rule() {
Rule::ident => { name = Some(p.into_span().as_str().to_string()); } Rule::ident => {
name = Some(p.into_span().as_str().to_string());
}
Rule::kwarg => { Rule::kwarg => {
let mut arg_name = None; let mut arg_name = None;
let mut arg_val = None; let mut arg_val = None;
for p2 in p.into_inner() { for p2 in p.into_inner() {
match p2.as_rule() { match p2.as_rule() {
Rule::ident => { arg_name = Some(p2.into_span().as_str().to_string()); } Rule::ident => {
Rule::literal => { arg_val = Some(parse_literal(p2)); } arg_name = Some(p2.into_span().as_str().to_string());
}
Rule::literal => {
arg_val = Some(parse_literal(p2));
}
Rule::array => { Rule::array => {
let mut vals = vec![]; let mut vals = vec![];
for p3 in p2.into_inner() { for p3 in p2.into_inner() {
match p3.as_rule() { match p3.as_rule() {
Rule::literal => vals.push(parse_literal(p3)), Rule::literal => vals.push(parse_literal(p3)),
_ => unreachable!("Got something other than literal in an array: {:?}", p3), _ => unreachable!(
"Got something other than literal in an array: {:?}",
p3
),
} }
} }
arg_val = Some(Value::Array(vals)); arg_val = Some(Value::Array(vals));
@ -77,14 +85,18 @@ fn parse_shortcode_call(pair: Pair<Rule>) -> (String, Map<String, Value>) {
args.insert(arg_name.unwrap(), arg_val.unwrap()); args.insert(arg_name.unwrap(), arg_val.unwrap());
} }
_ => unreachable!("Got something unexpected in a shortcode: {:?}", p) _ => unreachable!("Got something unexpected in a shortcode: {:?}", p),
} }
} }
(name.unwrap(), args) (name.unwrap(), args)
} }
fn render_shortcode(
fn render_shortcode(name: &str, args: &Map<String, Value>, context: &RenderContext, body: Option<&str>) -> Result<String> { name: &str,
args: &Map<String, Value>,
context: &RenderContext,
body: Option<&str>,
) -> Result<String> {
let mut tera_context = Context::new(); let mut tera_context = Context::new();
for (key, value) in args.iter() { for (key, value) in args.iter() {
tera_context.insert(key, value); tera_context.insert(key, value);
@ -96,7 +108,8 @@ fn render_shortcode(name: &str, args: &Map<String, Value>, context: &RenderConte
tera_context.extend(context.tera_context.clone()); tera_context.extend(context.tera_context.clone());
let tpl_name = format!("shortcodes/{}.html", name); let tpl_name = format!("shortcodes/{}.html", name);
let res = context.tera let res = context
.tera
.render(&tpl_name, &tera_context) .render(&tpl_name, &tera_context)
.chain_err(|| format!("Failed to render {} shortcode", name))?; .chain_err(|| format!("Failed to render {} shortcode", name))?;
@ -109,38 +122,36 @@ pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<Strin
let mut pairs = match ContentParser::parse(Rule::page, content) { let mut pairs = match ContentParser::parse(Rule::page, content) {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
let fancy_e = e.renamed_rules(|rule| { let fancy_e = e.renamed_rules(|rule| match *rule {
match *rule { Rule::int => "an integer".to_string(),
Rule::int => "an integer".to_string(), Rule::float => "a float".to_string(),
Rule::float => "a float".to_string(), Rule::string => "a string".to_string(),
Rule::string => "a string".to_string(), Rule::literal => "a literal (int, float, string, bool)".to_string(),
Rule::literal => "a literal (int, float, string, bool)".to_string(), Rule::array => "an array".to_string(),
Rule::array => "an array".to_string(), Rule::kwarg => "a keyword argument".to_string(),
Rule::kwarg => "a keyword argument".to_string(), Rule::ident => "an identifier".to_string(),
Rule::ident => "an identifier".to_string(), Rule::inline_shortcode => "an inline shortcode".to_string(),
Rule::inline_shortcode => "an inline shortcode".to_string(), Rule::ignored_inline_shortcode => "an ignored inline shortcode".to_string(),
Rule::ignored_inline_shortcode => "an ignored inline shortcode".to_string(), Rule::sc_body_start => "the start of a shortcode".to_string(),
Rule::sc_body_start => "the start of a shortcode".to_string(), Rule::ignored_sc_body_start => "the start of an ignored shortcode".to_string(),
Rule::ignored_sc_body_start => "the start of an ignored shortcode".to_string(), Rule::text => "some text".to_string(),
Rule::text => "some text".to_string(), Rule::EOI => "end of input".to_string(),
Rule::EOI => "end of input".to_string(), Rule::double_quoted_string => "double quoted string".to_string(),
Rule::double_quoted_string => "double quoted string".to_string(), Rule::single_quoted_string => "single quoted string".to_string(),
Rule::single_quoted_string => "single quoted string".to_string(), Rule::backquoted_quoted_string => "backquoted quoted string".to_string(),
Rule::backquoted_quoted_string => "backquoted quoted string".to_string(), Rule::boolean => "a boolean (true, false)".to_string(),
Rule::boolean => "a boolean (true, false)".to_string(), Rule::all_chars => "a alphanumerical character".to_string(),
Rule::all_chars => "a alphanumerical character".to_string(), Rule::kwargs => "a list of keyword arguments".to_string(),
Rule::kwargs => "a list of keyword arguments".to_string(), Rule::sc_def => "a shortcode definition".to_string(),
Rule::sc_def => "a shortcode definition".to_string(), Rule::shortcode_with_body => "a shortcode with body".to_string(),
Rule::shortcode_with_body => "a shortcode with body".to_string(), Rule::ignored_shortcode_with_body => "an ignored shortcode with body".to_string(),
Rule::ignored_shortcode_with_body => "an ignored shortcode with body".to_string(), Rule::sc_body_end => "{% end %}".to_string(),
Rule::sc_body_end => "{% end %}".to_string(), Rule::ignored_sc_body_end => "{%/* end */%}".to_string(),
Rule::ignored_sc_body_end => "{%/* end */%}".to_string(), Rule::text_in_body_sc => "text in a shortcode body".to_string(),
Rule::text_in_body_sc => "text in a shortcode body".to_string(), Rule::text_in_ignored_body_sc => "text in an ignored shortcode body".to_string(),
Rule::text_in_ignored_body_sc => "text in an ignored shortcode body".to_string(), Rule::content => "some content".to_string(),
Rule::content => "some content".to_string(), Rule::page => "a page".to_string(),
Rule::page => "a page".to_string(), Rule::WHITESPACE => "whitespace".to_string(),
Rule::WHITESPACE => "whitespace".to_string(),
}
}); });
bail!("{}", fancy_e); bail!("{}", fancy_e);
} }
@ -164,9 +175,7 @@ pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<Strin
} }
Rule::ignored_inline_shortcode => { Rule::ignored_inline_shortcode => {
res.push_str( res.push_str(
&p.into_span().as_str() &p.into_span().as_str().replacen("{{/*", "{{", 1).replacen("*/}}", "}}", 1),
.replacen("{{/*", "{{", 1)
.replacen("*/}}", "}}", 1)
); );
} }
Rule::ignored_shortcode_with_body => { Rule::ignored_shortcode_with_body => {
@ -174,16 +183,17 @@ pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<Strin
match p2.as_rule() { match p2.as_rule() {
Rule::ignored_sc_body_start | Rule::ignored_sc_body_end => { Rule::ignored_sc_body_start | Rule::ignored_sc_body_end => {
res.push_str( res.push_str(
&p2.into_span().as_str() &p2.into_span()
.as_str()
.replacen("{%/*", "{%", 1) .replacen("{%/*", "{%", 1)
.replacen("*/%}", "%}", 1) .replacen("*/%}", "%}", 1),
); );
} }
Rule::text_in_ignored_body_sc => res.push_str(p2.into_span().as_str()), Rule::text_in_ignored_body_sc => res.push_str(p2.into_span().as_str()),
_ => unreachable!("Got something weird in an ignored shortcode: {:?}", p2), _ => unreachable!("Got something weird in an ignored shortcode: {:?}", p2),
} }
} }
}, }
Rule::EOI => (), Rule::EOI => (),
_ => unreachable!("unexpected page rule: {:?}", p.as_rule()), _ => unreachable!("unexpected page rule: {:?}", p.as_rule()),
} }
@ -196,10 +206,10 @@ pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<Strin
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use tera::Tera; use super::*;
use config::Config; use config::Config;
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
use super::*; use tera::Tera;
macro_rules! assert_lex_rule { macro_rules! assert_lex_rule {
($rule: expr, $input: expr) => { ($rule: expr, $input: expr) => {
@ -297,7 +307,7 @@ mod tests {
{% hello() %} {% hello() %}
Body {{ var }} Body {{ var }}
{% end %} {% end %}
"# "#,
]; ];
for i in inputs { for i in inputs {
assert_lex_rule!(Rule::page, i); assert_lex_rule!(Rule::page, i);
@ -318,19 +328,25 @@ mod tests {
#[test] #[test]
fn can_unignore_shortcode_with_body() { fn can_unignore_shortcode_with_body() {
let res = render_shortcodes(r#" let res = render_shortcodes(
r#"
Hello World Hello World
{%/* youtube() */%}Some body {{ hello() }}{%/* end */%}"#, &Tera::default()); {%/* youtube() */%}Some body {{ hello() }}{%/* end */%}"#,
&Tera::default(),
);
assert_eq!(res, "\nHello World\n{% youtube() %}Some body {{ hello() }}{% end %}"); assert_eq!(res, "\nHello World\n{% youtube() %}Some body {{ hello() }}{% end %}");
} }
// https://github.com/Keats/gutenberg/issues/383 // https://github.com/Keats/gutenberg/issues/383
#[test] #[test]
fn unignore_shortcode_with_body_does_not_swallow_initial_whitespace() { fn unignore_shortcode_with_body_does_not_swallow_initial_whitespace() {
let res = render_shortcodes(r#" let res = render_shortcodes(
r#"
Hello World Hello World
{%/* youtube() */%} {%/* youtube() */%}
Some body {{ hello() }}{%/* end */%}"#, &Tera::default()); Some body {{ hello() }}{%/* end */%}"#,
&Tera::default(),
);
assert_eq!(res, "\nHello World\n{% youtube() %}\nSome body {{ hello() }}{% end %}"); assert_eq!(res, "\nHello World\n{% youtube() %}\nSome body {{ hello() }}{% end %}");
} }
@ -338,28 +354,20 @@ Some body {{ hello() }}{%/* end */%}"#, &Tera::default());
fn can_parse_shortcode_arguments() { fn can_parse_shortcode_arguments() {
let inputs = vec![ let inputs = vec![
("{{ youtube() }}", "youtube", Map::new()), ("{{ youtube() }}", "youtube", Map::new()),
( ("{{ youtube(id=1, autoplay=true, hello='salut', float=1.2) }}", "youtube", {
"{{ youtube(id=1, autoplay=true, hello='salut', float=1.2) }}", let mut m = Map::new();
"youtube", m.insert("id".to_string(), to_value(1).unwrap());
{ m.insert("autoplay".to_string(), to_value(true).unwrap());
let mut m = Map::new(); m.insert("hello".to_string(), to_value("salut").unwrap());
m.insert("id".to_string(), to_value(1).unwrap()); m.insert("float".to_string(), to_value(1.2).unwrap());
m.insert("autoplay".to_string(), to_value(true).unwrap()); m
m.insert("hello".to_string(), to_value("salut").unwrap()); }),
m.insert("float".to_string(), to_value(1.2).unwrap()); ("{{ gallery(photos=['something', 'else'], fullscreen=true) }}", "gallery", {
m let mut m = Map::new();
} m.insert("photos".to_string(), to_value(["something", "else"]).unwrap());
), m.insert("fullscreen".to_string(), to_value(true).unwrap());
( m
"{{ gallery(photos=['something', 'else'], fullscreen=true) }}", }),
"gallery",
{
let mut m = Map::new();
m.insert("photos".to_string(), to_value(["something", "else"]).unwrap());
m.insert("fullscreen".to_string(), to_value(true).unwrap());
m
}
),
]; ];
for (i, n, a) in inputs { for (i, n, a) in inputs {

View file

@ -1,6 +1,5 @@
use tera::{Tera, Context as TeraContext};
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
use tera::{Context as TeraContext, Tera};
#[derive(Debug, PartialEq, Clone, Serialize)] #[derive(Debug, PartialEq, Clone, Serialize)]
pub struct Header { pub struct Header {
@ -65,9 +64,26 @@ impl TempHeader {
}; };
match insert_anchor { match insert_anchor {
InsertAnchor::None => format!("<h{lvl} id=\"{id}\">{t}</h{lvl}>\n", lvl = self.level, t = self.html, id = self.id), InsertAnchor::None => format!(
InsertAnchor::Left => format!("<h{lvl} id=\"{id}\">{a}{t}</h{lvl}>\n", lvl = self.level, a = anchor_link, t = self.html, id = self.id), "<h{lvl} id=\"{id}\">{t}</h{lvl}>\n",
InsertAnchor::Right => format!("<h{lvl} id=\"{id}\">{t}{a}</h{lvl}>\n", lvl = self.level, a = anchor_link, t = self.html, id = self.id), lvl = self.level,
t = self.html,
id = self.id
),
InsertAnchor::Left => format!(
"<h{lvl} id=\"{id}\">{a}{t}</h{lvl}>\n",
lvl = self.level,
a = anchor_link,
t = self.html,
id = self.id
),
InsertAnchor::Right => format!(
"<h{lvl} id=\"{id}\">{t}{a}</h{lvl}>\n",
lvl = self.level,
a = anchor_link,
t = self.html,
id = self.id
),
} }
} }
} }
@ -78,9 +94,12 @@ impl Default for TempHeader {
} }
} }
/// Recursively finds children of a header /// Recursively finds children of a header
fn find_children(parent_level: i32, start_at: usize, temp_headers: &[TempHeader]) -> (usize, Vec<Header>) { fn find_children(
parent_level: i32,
start_at: usize,
temp_headers: &[TempHeader],
) -> (usize, Vec<Header>) {
let mut headers = vec![]; let mut headers = vec![];
let mut start_at = start_at; let mut start_at = start_at;
@ -124,7 +143,6 @@ fn find_children(parent_level: i32, start_at: usize, temp_headers: &[TempHeader]
(start_at, headers) (start_at, headers)
} }
/// Converts the flat temp headers into a nested set of headers /// Converts the flat temp headers into a nested set of headers
/// representing the hierarchy /// representing the hierarchy
pub fn make_table_of_contents(temp_headers: &[TempHeader]) -> Vec<Header> { pub fn make_table_of_contents(temp_headers: &[TempHeader]) -> Vec<Header> {
@ -148,11 +166,7 @@ mod tests {
#[test] #[test]
fn can_make_basic_toc() { fn can_make_basic_toc() {
let input = vec![ let input = vec![TempHeader::new(1), TempHeader::new(1), TempHeader::new(1)];
TempHeader::new(1),
TempHeader::new(1),
TempHeader::new(1),
];
let toc = make_table_of_contents(&input); let toc = make_table_of_contents(&input);
assert_eq!(toc.len(), 3); assert_eq!(toc.len(), 3);
} }

View file

@ -1,8 +1,8 @@
extern crate tera;
extern crate front_matter;
extern crate templates;
extern crate rendering;
extern crate config; extern crate config;
extern crate front_matter;
extern crate rendering;
extern crate templates;
extern crate tera;
use std::collections::HashMap; use std::collections::HashMap;
@ -10,9 +10,8 @@ use tera::Tera;
use config::Config; use config::Config;
use front_matter::InsertAnchor; use front_matter::InsertAnchor;
use rendering::{render_content, RenderContext};
use templates::ZOLA_TERA; use templates::ZOLA_TERA;
use rendering::{RenderContext, render_content};
#[test] #[test]
fn can_do_render_content_simple() { fn can_do_render_content_simple() {
@ -32,10 +31,7 @@ fn doesnt_highlight_code_block_with_highlighting_off() {
config.highlight_code = false; config.highlight_code = false;
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("```\n$ gutenberg server\n```", &context).unwrap(); let res = render_content("```\n$ gutenberg server\n```", &context).unwrap();
assert_eq!( assert_eq!(res.body, "<pre><code>$ gutenberg server\n</code></pre>\n");
res.body,
"<pre><code>$ gutenberg server\n</code></pre>\n"
);
} }
#[test] #[test]
@ -86,11 +82,15 @@ fn can_render_shortcode() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(r#" let res = render_content(
r#"
Hello Hello
{{ youtube(id="ub36ffWAqgQ") }} {{ youtube(id="ub36ffWAqgQ") }}
"#, &context).unwrap(); "#,
&context,
)
.unwrap();
assert!(res.body.contains("<p>Hello</p>\n<div >")); assert!(res.body.contains("<p>Hello</p>\n<div >"));
assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#)); assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
} }
@ -100,14 +100,10 @@ fn can_render_shortcode_with_markdown_char_in_args_name() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let input = vec![ let input = vec!["name", "na_me", "n_a_me", "n1"];
"name",
"na_me",
"n_a_me",
"n1",
];
for i in input { for i in input {
let res = render_content(&format!("{{{{ youtube(id=\"hey\", {}=1) }}}}", i), &context).unwrap(); let res =
render_content(&format!("{{{{ youtube(id=\"hey\", {}=1) }}}}", i), &context).unwrap();
assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/hey""#)); assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/hey""#));
} }
} }
@ -126,7 +122,9 @@ fn can_render_shortcode_with_markdown_char_in_args_value() {
]; ];
for i in input { for i in input {
let res = render_content(&format!("{{{{ youtube(id=\"{}\") }}}}", i), &context).unwrap(); let res = render_content(&format!("{{{{ youtube(id=\"{}\") }}}}", i), &context).unwrap();
assert!(res.body.contains(&format!(r#"<iframe src="https://www.youtube.com/embed/{}""#, i))); assert!(
res.body.contains(&format!(r#"<iframe src="https://www.youtube.com/embed/{}""#, i))
);
} }
} }
@ -135,17 +133,20 @@ fn can_render_body_shortcode_with_markdown_char_in_name() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut tera = Tera::default(); let mut tera = Tera::default();
tera.extend(&ZOLA_TERA).unwrap(); tera.extend(&ZOLA_TERA).unwrap();
let input = vec![ let input = vec!["quo_te", "qu_o_te"];
"quo_te",
"qu_o_te",
];
let config = Config::default(); let config = Config::default();
for i in input { for i in input {
tera.add_raw_template(&format!("shortcodes/{}.html", i), "<blockquote>{{ body }} - {{ author}}</blockquote>").unwrap(); tera.add_raw_template(
&format!("shortcodes/{}.html", i),
"<blockquote>{{ body }} - {{ author}}</blockquote>",
)
.unwrap();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(&format!("{{% {}(author=\"Bob\") %}}\nhey\n{{% end %}}", i), &context).unwrap(); let res =
render_content(&format!("{{% {}(author=\"Bob\") %}}\nhey\n{{% end %}}", i), &context)
.unwrap();
println!("{:?}", res); println!("{:?}", res);
assert!(res.body.contains("<blockquote>hey - Bob</blockquote>")); assert!(res.body.contains("<blockquote>hey - Bob</blockquote>"));
} }
@ -217,7 +218,8 @@ fn can_render_several_shortcode_in_row() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(r#" let res = render_content(
r#"
Hello Hello
{{ youtube(id="ub36ffWAqgQ") }} {{ youtube(id="ub36ffWAqgQ") }}
@ -230,10 +232,15 @@ Hello
{{ gist(url="https://gist.github.com/Keats/32d26f699dcc13ebd41b") }} {{ gist(url="https://gist.github.com/Keats/32d26f699dcc13ebd41b") }}
"#, &context).unwrap(); "#,
&context,
)
.unwrap();
assert!(res.body.contains("<p>Hello</p>\n<div >")); assert!(res.body.contains("<p>Hello</p>\n<div >"));
assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#)); assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ""#));
assert!(res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#)); assert!(
res.body.contains(r#"<iframe src="https://www.youtube.com/embed/ub36ffWAqgQ?autoplay=1""#)
);
assert!(res.body.contains(r#"<iframe src="https://www.streamable.com/e/c0ic""#)); assert!(res.body.contains(r#"<iframe src="https://www.streamable.com/e/c0ic""#));
assert!(res.body.contains(r#"//player.vimeo.com/video/210073083""#)); assert!(res.body.contains(r#"//player.vimeo.com/video/210073083""#));
} }
@ -252,17 +259,25 @@ fn doesnt_render_ignored_shortcodes() {
fn can_render_shortcode_with_body() { fn can_render_shortcode_with_body() {
let mut tera = Tera::default(); let mut tera = Tera::default();
tera.extend(&ZOLA_TERA).unwrap(); tera.extend(&ZOLA_TERA).unwrap();
tera.add_raw_template("shortcodes/quote.html", "<blockquote>{{ body }} - {{ author }}</blockquote>").unwrap(); tera.add_raw_template(
"shortcodes/quote.html",
"<blockquote>{{ body }} - {{ author }}</blockquote>",
)
.unwrap();
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(r#" let res = render_content(
r#"
Hello Hello
{% quote(author="Keats") %} {% quote(author="Keats") %}
A quote A quote
{% end %} {% end %}
"#, &context).unwrap(); "#,
&context,
)
.unwrap();
assert_eq!(res.body, "<p>Hello</p>\n<blockquote>A quote - Keats</blockquote>\n"); assert_eq!(res.body, "<p>Hello</p>\n<blockquote>A quote - Keats</blockquote>\n");
} }
@ -286,7 +301,8 @@ fn can_make_valid_relative_link() {
let res = render_content( let res = render_content(
r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#, r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#,
&context, &context,
).unwrap(); )
.unwrap();
assert!( assert!(
res.body.contains(r#"<p><a href="https://vincent.is/about">rel link</a>, <a href="https://vincent.is/about">abs link</a></p>"#) res.body.contains(r#"<p><a href="https://vincent.is/about">rel link</a>, <a href="https://vincent.is/about">abs link</a></p>"#)
@ -302,9 +318,7 @@ fn can_make_relative_links_with_anchors() {
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
let res = render_content(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap(); let res = render_content(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap();
assert!( assert!(res.body.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#));
res.body.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#)
);
} }
#[test] #[test]
@ -411,7 +425,8 @@ fn can_make_toc() {
InsertAnchor::Left, InsertAnchor::Left,
); );
let res = render_content(r#" let res = render_content(
r#"
# Header 1 # Header 1
## Header 2 ## Header 2
@ -419,7 +434,10 @@ fn can_make_toc() {
## Another Header 2 ## Another Header 2
### Last one ### Last one
"#, &context).unwrap(); "#,
&context,
)
.unwrap();
let toc = res.toc; let toc = res.toc;
assert_eq!(toc.len(), 1); assert_eq!(toc.len(), 1);
@ -439,13 +457,17 @@ fn can_ignore_tags_in_toc() {
InsertAnchor::Left, InsertAnchor::Left,
); );
let res = render_content(r#" let res = render_content(
r#"
## header with `code` ## header with `code`
## [anchor](https://duckduckgo.com/) in header ## [anchor](https://duckduckgo.com/) in header
## **bold** and *italics* ## **bold** and *italics*
"#, &context).unwrap(); "#,
&context,
)
.unwrap();
let toc = res.toc; let toc = res.toc;
@ -465,10 +487,7 @@ fn can_understand_backtick_in_titles() {
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# `Hello`", &context).unwrap(); let res = render_content("# `Hello`", &context).unwrap();
assert_eq!( assert_eq!(res.body, "<h1 id=\"hello\"><code>Hello</code></h1>\n");
res.body,
"<h1 id=\"hello\"><code>Hello</code></h1>\n"
);
} }
#[test] #[test]
@ -477,10 +496,7 @@ fn can_understand_backtick_in_paragraphs() {
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("Hello `world`", &context).unwrap(); let res = render_content("Hello `world`", &context).unwrap();
assert_eq!( assert_eq!(res.body, "<p>Hello <code>world</code></p>\n");
res.body,
"<p>Hello <code>world</code></p>\n"
);
} }
// https://github.com/Keats/gutenberg/issues/297 // https://github.com/Keats/gutenberg/issues/297
@ -490,10 +506,7 @@ fn can_understand_links_in_header() {
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# [Rust](https://rust-lang.org)", &context).unwrap(); let res = render_content("# [Rust](https://rust-lang.org)", &context).unwrap();
assert_eq!( assert_eq!(res.body, "<h1 id=\"rust\"><a href=\"https://rust-lang.org\">Rust</a></h1>\n");
res.body,
"<h1 id=\"rust\"><a href=\"https://rust-lang.org\">Rust</a></h1>\n"
);
} }
#[test] #[test]
@ -501,7 +514,8 @@ fn can_understand_link_with_title_in_header() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# [Rust](https://rust-lang.org \"Rust homepage\")", &context).unwrap(); let res =
render_content("# [Rust](https://rust-lang.org \"Rust homepage\")", &context).unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
"<h1 id=\"rust\"><a href=\"https://rust-lang.org\" title=\"Rust homepage\">Rust</a></h1>\n" "<h1 id=\"rust\"><a href=\"https://rust-lang.org\" title=\"Rust homepage\">Rust</a></h1>\n"
@ -515,10 +529,7 @@ fn can_make_valid_relative_link_in_header() {
let tera_ctx = Tera::default(); let tera_ctx = Tera::default();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
let res = render_content( let res = render_content(r#" # [rel link](./pages/about.md)"#, &context).unwrap();
r#" # [rel link](./pages/about.md)"#,
&context,
).unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
@ -530,19 +541,28 @@ fn can_make_valid_relative_link_in_header() {
fn can_make_permalinks_with_colocated_assets_for_link() { fn can_make_permalinks_with_colocated_assets_for_link() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(
let res = render_content("[an image](image.jpg)", &context).unwrap(); &ZOLA_TERA,
assert_eq!( &config,
res.body, "https://vincent.is/about/",
"<p><a href=\"https://vincent.is/about/image.jpg\">an image</a></p>\n" &permalinks_ctx,
InsertAnchor::None,
); );
let res = render_content("[an image](image.jpg)", &context).unwrap();
assert_eq!(res.body, "<p><a href=\"https://vincent.is/about/image.jpg\">an image</a></p>\n");
} }
#[test] #[test]
fn can_make_permalinks_with_colocated_assets_for_image() { fn can_make_permalinks_with_colocated_assets_for_image() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(
&ZOLA_TERA,
&config,
"https://vincent.is/about/",
&permalinks_ctx,
InsertAnchor::None,
);
let res = render_content("![alt text](image.jpg)", &context).unwrap(); let res = render_content("![alt text](image.jpg)", &context).unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
@ -554,8 +574,15 @@ fn can_make_permalinks_with_colocated_assets_for_image() {
fn markdown_doesnt_wrap_html_in_paragraph() { fn markdown_doesnt_wrap_html_in_paragraph() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(
let res = render_content(r#" &ZOLA_TERA,
&config,
"https://vincent.is/about/",
&permalinks_ctx,
InsertAnchor::None,
);
let res = render_content(
r#"
Some text Some text
<h1>Helo</h1> <h1>Helo</h1>
@ -565,7 +592,10 @@ Some text
<img src="mobx-flow.png" alt="MobX flow"> <img src="mobx-flow.png" alt="MobX flow">
</a> </a>
</div> </div>
"#, &context).unwrap(); "#,
&context,
)
.unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
"<p>Some text</p>\n<h1>Helo</h1>\n<div>\n<a href=\"mobx-flow.png\">\n <img src=\"mobx-flow.png\" alt=\"MobX flow\">\n </a>\n</div>\n" "<p>Some text</p>\n<h1>Helo</h1>\n<div>\n<a href=\"mobx-flow.png\">\n <img src=\"mobx-flow.png\" alt=\"MobX flow\">\n </a>\n</div>\n"
@ -577,12 +607,15 @@ fn can_validate_valid_external_links() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut config = Config::default(); let mut config = Config::default();
config.check_external_links = true; config.check_external_links = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(
let res = render_content("[a link](http://google.com)", &context).unwrap(); &ZOLA_TERA,
assert_eq!( &config,
res.body, "https://vincent.is/about/",
"<p><a href=\"http://google.com\">a link</a></p>\n" &permalinks_ctx,
InsertAnchor::None,
); );
let res = render_content("[a link](http://google.com)", &context).unwrap();
assert_eq!(res.body, "<p><a href=\"http://google.com\">a link</a></p>\n");
} }
#[test] #[test]
@ -590,7 +623,13 @@ fn can_show_error_message_for_invalid_external_links() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut config = Config::default(); let mut config = Config::default();
config.check_external_links = true; config.check_external_links = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(
&ZOLA_TERA,
&config,
"https://vincent.is/about/",
&permalinks_ctx,
InsertAnchor::None,
);
let res = render_content("[a link](http://google.comy)", &context); let res = render_content("[a link](http://google.comy)", &context);
assert!(res.is_err()); assert!(res.is_err());
let err = res.unwrap_err(); let err = res.unwrap_err();
@ -602,12 +641,15 @@ fn doesnt_try_to_validate_email_links_mailto() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut config = Config::default(); let mut config = Config::default();
config.check_external_links = true; config.check_external_links = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(
let res = render_content("Email: [foo@bar.baz](mailto:foo@bar.baz)", &context).unwrap(); &ZOLA_TERA,
assert_eq!( &config,
res.body, "https://vincent.is/about/",
"<p>Email: <a href=\"mailto:foo@bar.baz\">foo@bar.baz</a></p>\n" &permalinks_ctx,
InsertAnchor::None,
); );
let res = render_content("Email: [foo@bar.baz](mailto:foo@bar.baz)", &context).unwrap();
assert_eq!(res.body, "<p>Email: <a href=\"mailto:foo@bar.baz\">foo@bar.baz</a></p>\n");
} }
#[test] #[test]
@ -615,12 +657,15 @@ fn doesnt_try_to_validate_email_links_angled_brackets() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let mut config = Config::default(); let mut config = Config::default();
config.check_external_links = true; config.check_external_links = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(
let res = render_content("Email: <foo@bar.baz>", &context).unwrap(); &ZOLA_TERA,
assert_eq!( &config,
res.body, "https://vincent.is/about/",
"<p>Email: <a href=\"mailto:foo@bar.baz\">foo@bar.baz</a></p>\n" &permalinks_ctx,
InsertAnchor::None,
); );
let res = render_content("Email: <foo@bar.baz>", &context).unwrap();
assert_eq!(res.body, "<p>Email: <a href=\"mailto:foo@bar.baz\">foo@bar.baz</a></p>\n");
} }
#[test] #[test]
@ -629,7 +674,11 @@ fn can_handle_summaries() {
let permalinks_ctx = HashMap::new(); let permalinks_ctx = HashMap::new();
let config = Config::default(); let config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None); let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("Hello [world]\n\n<!-- more -->\n\nBla bla\n\n[world]: https://vincent.is/about/", &context).unwrap(); let res = render_content(
"Hello [world]\n\n<!-- more -->\n\nBla bla\n\n[world]: https://vincent.is/about/",
&context,
)
.unwrap();
assert_eq!( assert_eq!(
res.body, res.body,
"<p>Hello <a href=\"https://vincent.is/about/\">world</a></p>\n<p><a name=\"continue-reading\"></a></p>\n<p>Bla bla</p>\n" "<p>Hello <a href=\"https://vincent.is/about/\">world</a></p>\n<p><a name=\"continue-reading\"></a></p>\n<p>Bla bla</p>\n"

View file

@ -11,9 +11,8 @@ use std::collections::{HashMap, HashSet};
use elasticlunr::{Index, Language}; use elasticlunr::{Index, Language};
use library::{Library, Section};
use errors::Result; use errors::Result;
use library::{Library, Section};
pub const ELASTICLUNR_JS: &str = include_str!("elasticlunr.min.js"); pub const ELASTICLUNR_JS: &str = include_str!("elasticlunr.min.js");
@ -34,7 +33,6 @@ lazy_static! {
}; };
} }
/// Returns the generated JSON index with all the documents of the site added using /// Returns the generated JSON index with all the documents of the site added using
/// the language given /// the language given
/// Errors if the language given is not available in Elasticlunr /// Errors if the language given is not available in Elasticlunr
@ -42,7 +40,9 @@ lazy_static! {
pub fn build_index(lang: &str, library: &Library) -> Result<String> { pub fn build_index(lang: &str, library: &Library) -> Result<String> {
let language = match Language::from_code(lang) { let language = match Language::from_code(lang) {
Some(l) => l, Some(l) => l,
None => { bail!("Tried to build search index for language {} which is not supported", lang); } None => {
bail!("Tried to build search index for language {} which is not supported", lang);
}
}; };
let mut index = Index::with_language(language, &["title", "body"]); let mut index = Index::with_language(language, &["title", "body"]);
@ -63,7 +63,10 @@ fn add_section_to_index(index: &mut Index, section: &Section, library: &Library)
if section.meta.redirect_to.is_none() { if section.meta.redirect_to.is_none() {
index.add_doc( index.add_doc(
&section.permalink, &section.permalink,
&[&section.meta.title.clone().unwrap_or_default(), &AMMONIA.clean(&section.content).to_string()], &[
&section.meta.title.clone().unwrap_or_default(),
&AMMONIA.clean(&section.content).to_string(),
],
); );
} }
@ -75,7 +78,10 @@ fn add_section_to_index(index: &mut Index, section: &Section, library: &Library)
index.add_doc( index.add_doc(
&page.permalink, &page.permalink,
&[&page.meta.title.clone().unwrap_or_default(), &AMMONIA.clean(&page.content).to_string()], &[
&page.meta.title.clone().unwrap_or_default(),
&AMMONIA.clean(&page.content).to_string(),
],
); );
} }
} }

View file

@ -1,14 +1,13 @@
//! Benchmarking loading/markdown rendering of generated sites of various sizes //! Benchmarking loading/markdown rendering of generated sites of various sizes
#![feature(test)] #![feature(test)]
extern crate test;
extern crate site; extern crate site;
extern crate test;
use std::env; use std::env;
use site::Site; use site::Site;
#[bench] #[bench]
fn bench_loading_small_blog(b: &mut test::Bencher) { fn bench_loading_small_blog(b: &mut test::Bencher) {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().to_path_buf();

View file

@ -1,15 +1,14 @@
#![feature(test)] #![feature(test)]
extern crate test;
extern crate site;
extern crate library; extern crate library;
extern crate site;
extern crate tempfile; extern crate tempfile;
extern crate test;
use std::env; use std::env;
use tempfile::tempdir;
use site::Site;
use library::Paginator; use library::Paginator;
use site::Site;
use tempfile::tempdir;
fn setup_site(name: &str) -> Site { fn setup_site(name: &str) -> Site {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().to_path_buf();

View file

@ -1,7 +1,7 @@
extern crate tera;
extern crate rayon;
extern crate glob; extern crate glob;
extern crate rayon;
extern crate serde; extern crate serde;
extern crate tera;
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
extern crate sass_rs; extern crate sass_rs;
@ -9,34 +9,36 @@ extern crate sass_rs;
#[macro_use] #[macro_use]
extern crate errors; extern crate errors;
extern crate config; extern crate config;
extern crate utils;
extern crate front_matter; extern crate front_matter;
extern crate templates;
extern crate search;
extern crate imageproc; extern crate imageproc;
extern crate library; extern crate library;
extern crate search;
extern crate templates;
extern crate utils;
#[cfg(test)] #[cfg(test)]
extern crate tempfile; extern crate tempfile;
use std::collections::{HashMap}; use std::collections::HashMap;
use std::fs::{create_dir_all, remove_dir_all, copy}; use std::fs::{copy, create_dir_all, remove_dir_all};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use glob::glob; use glob::glob;
use tera::{Tera, Context};
use sass_rs::{Options as SassOptions, OutputStyle, compile_file};
use rayon::prelude::*; use rayon::prelude::*;
use sass_rs::{compile_file, Options as SassOptions, OutputStyle};
use tera::{Context, Tera};
use config::{get_config, Config};
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use config::{Config, get_config}; use front_matter::InsertAnchor;
use utils::fs::{create_file, copy_directory, create_directory, ensure_directory_exists}; use library::{
use utils::templates::{render_template, rewrite_theme_paths}; find_taxonomies, sort_actual_pages_by_date, Library, Page, Paginator, Section, Taxonomy,
};
use templates::{global_fns, render_redirect_template, ZOLA_TERA};
use utils::fs::{copy_directory, create_directory, create_file, ensure_directory_exists};
use utils::net::get_available_port; use utils::net::get_available_port;
use templates::{ZOLA_TERA, global_fns, render_redirect_template}; use utils::templates::{render_template, rewrite_theme_paths};
use front_matter::{InsertAnchor};
use library::{Page, Section, sort_actual_pages_by_date, Library, Taxonomy, find_taxonomies, Paginator};
/// The sitemap only needs links and potentially date so we trim down /// The sitemap only needs links and potentially date so we trim down
/// all pages to only that /// all pages to only that
@ -81,7 +83,8 @@ impl Site {
let mut config = get_config(path, config_file); let mut config = get_config(path, config_file);
config.load_extra_syntaxes(path)?; config.load_extra_syntaxes(path)?;
let tpl_glob = format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*.*ml"); let tpl_glob =
format!("{}/{}", path.to_string_lossy().replace("\\", "/"), "templates/**/*.*ml");
// Only parsing as we might be extending templates from themes and that would error // Only parsing as we might be extending templates from themes and that would error
// as we haven't loaded them yet // as we haven't loaded them yet
let mut tera = Tera::parse(&tpl_glob).chain_err(|| "Error parsing templates")?; let mut tera = Tera::parse(&tpl_glob).chain_err(|| "Error parsing templates")?;
@ -100,11 +103,13 @@ impl Site {
path.to_string_lossy().replace("\\", "/"), path.to_string_lossy().replace("\\", "/"),
format!("themes/{}/templates/**/*.*ml", theme) format!("themes/{}/templates/**/*.*ml", theme)
); );
let mut tera_theme = Tera::parse(&theme_tpl_glob).chain_err(|| "Error parsing templates from themes")?; let mut tera_theme =
Tera::parse(&theme_tpl_glob).chain_err(|| "Error parsing templates from themes")?;
rewrite_theme_paths(&mut tera_theme, &theme); rewrite_theme_paths(&mut tera_theme, &theme);
// TODO: same as below // TODO: same as below
if theme_path.join("templates").join("robots.txt").exists() { if theme_path.join("templates").join("robots.txt").exists() {
tera_theme.add_template_file(theme_path.join("templates").join("robots.txt"), None)?; tera_theme
.add_template_file(theme_path.join("templates").join("robots.txt"), None)?;
} }
tera_theme.build_inheritance_chains()?; tera_theme.build_inheritance_chains()?;
tera.extend(&tera_theme)?; tera.extend(&tera_theme)?;
@ -121,7 +126,8 @@ impl Site {
let content_path = path.join("content"); let content_path = path.join("content");
let static_path = path.join("static"); let static_path = path.join("static");
let imageproc = imageproc::Processor::new(content_path.clone(), &static_path, &config.base_url); let imageproc =
imageproc::Processor::new(content_path.clone(), &static_path, &config.base_url);
let site = Site { let site = Site {
base_path: path.to_path_buf(), base_path: path.to_path_buf(),
@ -238,7 +244,10 @@ impl Site {
let mut pages_insert_anchors = HashMap::new(); let mut pages_insert_anchors = HashMap::new();
for page in pages { for page in pages {
let p = page?; let p = page?;
pages_insert_anchors.insert(p.file.path.clone(), self.find_parent_section_insert_anchor(&p.file.parent.clone())); pages_insert_anchors.insert(
p.file.path.clone(),
self.find_parent_section_insert_anchor(&p.file.parent.clone()),
);
self.add_page(p, false)?; self.add_page(p, false)?;
} }
@ -263,7 +272,10 @@ impl Site {
// This is needed in the first place because of silly borrow checker // This is needed in the first place because of silly borrow checker
let mut pages_insert_anchors = HashMap::new(); let mut pages_insert_anchors = HashMap::new();
for (_, p) in self.library.pages() { for (_, p) in self.library.pages() {
pages_insert_anchors.insert(p.file.path.clone(), self.find_parent_section_insert_anchor(&p.file.parent.clone())); pages_insert_anchors.insert(
p.file.path.clone(),
self.find_parent_section_insert_anchor(&p.file.parent.clone()),
);
} }
self.library self.library
@ -291,10 +303,12 @@ impl Site {
/// Adds global fns that are to be available to shortcodes while rendering markdown /// Adds global fns that are to be available to shortcodes while rendering markdown
pub fn register_early_global_fns(&mut self) { pub fn register_early_global_fns(&mut self) {
self.tera.register_function( self.tera.register_function(
"get_url", global_fns::make_get_url(self.permalinks.clone(), self.config.clone()), "get_url",
global_fns::make_get_url(self.permalinks.clone(), self.config.clone()),
); );
self.tera.register_function( self.tera.register_function(
"resize_image", global_fns::make_resize_image(self.imageproc.clone()), "resize_image",
global_fns::make_resize_image(self.imageproc.clone()),
); );
} }
@ -310,7 +324,10 @@ impl Site {
"get_taxonomy_url", "get_taxonomy_url",
global_fns::make_get_taxonomy_url(&self.taxonomies), global_fns::make_get_taxonomy_url(&self.taxonomies),
); );
self.tera.register_function("load_data", global_fns::make_load_data(self.content_path.clone(), self.base_path.clone())); self.tera.register_function(
"load_data",
global_fns::make_load_data(self.content_path.clone(), self.base_path.clone()),
);
} }
/// Add a page to the site /// Add a page to the site
@ -349,7 +366,7 @@ impl Site {
pub fn find_parent_section_insert_anchor(&self, parent_path: &PathBuf) -> InsertAnchor { pub fn find_parent_section_insert_anchor(&self, parent_path: &PathBuf) -> InsertAnchor {
match self.library.get_section(&parent_path.join("_index.md")) { match self.library.get_section(&parent_path.join("_index.md")) {
Some(s) => s.meta.insert_anchor_links, Some(s) => s.meta.insert_anchor_links,
None => InsertAnchor::None None => InsertAnchor::None,
} }
} }
@ -375,7 +392,10 @@ impl Site {
if let Some(port) = self.live_reload { if let Some(port) = self.live_reload {
return html.replace( return html.replace(
"</body>", "</body>",
&format!(r#"<script src="/livereload.js?port={}&mindelay=10"></script></body>"#, port), &format!(
r#"<script src="/livereload.js?port={}&mindelay=10"></script></body>"#,
port
),
); );
} }
@ -498,10 +518,7 @@ impl Site {
)?; )?;
// then elasticlunr.min.js // then elasticlunr.min.js
create_file( create_file(&self.output_path.join("elasticlunr.min.js"), search::ELASTICLUNR_JS)?;
&self.output_path.join("elasticlunr.min.js"),
search::ELASTICLUNR_JS,
)?;
Ok(()) Ok(())
} }
@ -537,12 +554,19 @@ impl Site {
Ok(()) Ok(())
} }
fn compile_sass_glob(&self, sass_path: &Path, extension: &str, options: &SassOptions) -> Result<Vec<(PathBuf, PathBuf)>> { fn compile_sass_glob(
&self,
sass_path: &Path,
extension: &str,
options: &SassOptions,
) -> Result<Vec<(PathBuf, PathBuf)>> {
let glob_string = format!("{}/**/*.{}", sass_path.display(), extension); let glob_string = format!("{}/**/*.{}", sass_path.display(), extension);
let files = glob(&glob_string) let files = glob(&glob_string)
.unwrap() .unwrap()
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.filter(|entry| !entry.as_path().file_name().unwrap().to_string_lossy().starts_with('_')) .filter(|entry| {
!entry.as_path().file_name().unwrap().to_string_lossy().starts_with('_')
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut compiled_paths = Vec::new(); let mut compiled_paths = Vec::new();
@ -579,7 +603,7 @@ impl Site {
split.push(part); split.push(part);
"index.html" "index.html"
} }
None => "index.html" None => "index.html",
}; };
for component in split { for component in split {
@ -589,7 +613,10 @@ impl Site {
create_directory(&output_path)?; create_directory(&output_path)?;
} }
} }
create_file(&output_path.join(page_name), &render_redirect_template(&page.permalink, &self.tera)?)?; create_file(
&output_path.join(page_name),
&render_redirect_template(&page.permalink, &self.tera)?,
)?;
} }
} }
Ok(()) Ok(())
@ -650,15 +677,16 @@ impl Site {
} }
if taxonomy.kind.is_paginated() { if taxonomy.kind.is_paginated() {
self.render_paginated(&output_path, &Paginator::from_taxonomy(&taxonomy, item, &self.library)) self.render_paginated(
&output_path,
&Paginator::from_taxonomy(&taxonomy, item, &self.library),
)
} else { } else {
let single_output = taxonomy.render_term(item, &self.tera, &self.config, &self.library)?; let single_output =
taxonomy.render_term(item, &self.tera, &self.config, &self.library)?;
let path = output_path.join(&item.slug); let path = output_path.join(&item.slug);
create_directory(&path)?; create_directory(&path)?;
create_file( create_file(&path.join("index.html"), &self.inject_livereload(single_output))
&path.join("index.html"),
&self.inject_livereload(single_output),
)
} }
}) })
.collect::<Result<()>>() .collect::<Result<()>>()
@ -670,7 +698,8 @@ impl Site {
let mut context = Context::new(); let mut context = Context::new();
let mut pages = self.library let mut pages = self
.library
.pages_values() .pages_values()
.iter() .iter()
.filter(|p| !p.is_draft()) .filter(|p| !p.is_draft())
@ -685,7 +714,8 @@ impl Site {
pages.sort_by(|a, b| a.permalink.cmp(&b.permalink)); pages.sort_by(|a, b| a.permalink.cmp(&b.permalink));
context.insert("pages", &pages); context.insert("pages", &pages);
let mut sections = self.library let mut sections = self
.library
.sections_values() .sections_values()
.iter() .iter()
.map(|s| SitemapEntry::new(s.permalink.clone(), None)) .map(|s| SitemapEntry::new(s.permalink.clone(), None))
@ -699,7 +729,10 @@ impl Site {
let mut terms = vec![]; let mut terms = vec![];
terms.push(SitemapEntry::new(self.config.make_permalink(name), None)); terms.push(SitemapEntry::new(self.config.make_permalink(name), None));
for item in &taxonomy.items { for item in &taxonomy.items {
terms.push(SitemapEntry::new(self.config.make_permalink(&format!("{}/{}", &name, item.slug)), None)); terms.push(SitemapEntry::new(
self.config.make_permalink(&format!("{}/{}", &name, item.slug)),
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);
@ -718,7 +751,11 @@ impl Site {
/// Renders a RSS feed for the given path and at the given path /// Renders a RSS feed for the given path and at the given path
/// If both arguments are `None`, it will render only the RSS feed for the whole /// If both arguments are `None`, it will render only the RSS feed for the whole
/// site at the root folder. /// site at the root folder.
pub fn render_rss_feed(&self, all_pages: Vec<&Page>, base_path: Option<&PathBuf>) -> Result<()> { pub fn render_rss_feed(
&self,
all_pages: Vec<&Page>,
base_path: Option<&PathBuf>,
) -> Result<()> {
ensure_directory_exists(&self.output_path)?; ensure_directory_exists(&self.output_path)?;
let mut context = Context::new(); let mut context = Context::new();
@ -806,7 +843,10 @@ impl Site {
if let Some(ref redirect_to) = section.meta.redirect_to { if let Some(ref redirect_to) = section.meta.redirect_to {
let permalink = self.config.make_permalink(redirect_to); let permalink = self.config.make_permalink(redirect_to);
create_file(&output_path.join("index.html"), &render_redirect_template(&permalink, &self.tera)?)?; create_file(
&output_path.join("index.html"),
&render_redirect_template(&permalink, &self.tera)?,
)?;
return Ok(()); return Ok(());
} }
@ -861,12 +901,16 @@ impl Site {
.map(|pager| { .map(|pager| {
let page_path = folder_path.join(&format!("{}", pager.index)); let page_path = folder_path.join(&format!("{}", pager.index));
create_directory(&page_path)?; create_directory(&page_path)?;
let output = paginator.render_pager(pager, &self.config, &self.tera, &self.library)?; let output =
paginator.render_pager(pager, &self.config, &self.tera, &self.library)?;
if pager.index > 1 { if pager.index > 1 {
create_file(&page_path.join("index.html"), &self.inject_livereload(output))?; create_file(&page_path.join("index.html"), &self.inject_livereload(output))?;
} else { } else {
create_file(&output_path.join("index.html"), &self.inject_livereload(output))?; create_file(&output_path.join("index.html"), &self.inject_livereload(output))?;
create_file(&page_path.join("index.html"), &render_redirect_template(&paginator.permalink, &self.tera)?)?; create_file(
&page_path.join("index.html"),
&render_redirect_template(&paginator.permalink, &self.tera)?,
)?;
} }
Ok(()) Ok(())
}) })

View file

@ -3,13 +3,12 @@ extern crate tempfile;
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
use std::path::Path;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
use std::path::Path;
use tempfile::tempdir;
use site::Site; use site::Site;
use tempfile::tempdir;
#[test] #[test]
fn can_parse_site() { fn can_parse_site() {
@ -27,7 +26,8 @@ fn can_parse_site() {
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 // Make sure the article in a folder with only asset doesn't get counted as a section
let asset_folder_post = site.library.get_page(&posts_path.join("with-assets").join("index.md")).unwrap(); let asset_folder_post =
site.library.get_page(&posts_path.join("with-assets").join("index.md")).unwrap();
assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]); assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]);
// That we have the right number of sections // That we have the right number of sections
@ -42,7 +42,10 @@ fn can_parse_site() {
let posts_section = site.library.get_section(&posts_path.join("_index.md")).unwrap(); let posts_section = site.library.get_section(&posts_path.join("_index.md")).unwrap();
assert_eq!(posts_section.subsections.len(), 1); assert_eq!(posts_section.subsections.len(), 1);
assert_eq!(posts_section.pages.len(), 8); assert_eq!(posts_section.pages.len(), 8);
assert_eq!(posts_section.ancestors, vec![*site.library.get_section_key(&index_section.file.path).unwrap()]); assert_eq!(
posts_section.ancestors,
vec![*site.library.get_section_key(&index_section.file.path).unwrap()]
);
// Make sure we remove all the pwd + content from the sections // Make sure we remove all the pwd + content from the sections
let basic = site.library.get_page(&posts_path.join("simple.md")).unwrap(); let basic = site.library.get_page(&posts_path.join("simple.md")).unwrap();
@ -55,7 +58,8 @@ fn can_parse_site() {
] ]
); );
let tutorials_section = site.library.get_section(&posts_path.join("tutorials").join("_index.md")).unwrap(); let tutorials_section =
site.library.get_section(&posts_path.join("tutorials").join("_index.md")).unwrap();
assert_eq!(tutorials_section.subsections.len(), 2); assert_eq!(tutorials_section.subsections.len(), 2);
let sub1 = site.library.get_section_by_key(tutorials_section.subsections[0]); let sub1 = site.library.get_section_by_key(tutorials_section.subsections[0]);
let sub2 = site.library.get_section_by_key(tutorials_section.subsections[1]); let sub2 = site.library.get_section_by_key(tutorials_section.subsections[1]);
@ -63,7 +67,10 @@ fn can_parse_site() {
assert_eq!(sub2.clone().meta.title.unwrap(), "DevOps"); assert_eq!(sub2.clone().meta.title.unwrap(), "DevOps");
assert_eq!(tutorials_section.pages.len(), 0); assert_eq!(tutorials_section.pages.len(), 0);
let devops_section = site.library.get_section(&posts_path.join("tutorials").join("devops").join("_index.md")).unwrap(); let devops_section = site
.library
.get_section(&posts_path.join("tutorials").join("devops").join("_index.md"))
.unwrap();
assert_eq!(devops_section.subsections.len(), 0); assert_eq!(devops_section.subsections.len(), 0);
assert_eq!(devops_section.pages.len(), 2); assert_eq!(devops_section.pages.len(), 2);
assert_eq!( assert_eq!(
@ -75,38 +82,37 @@ fn can_parse_site() {
] ]
); );
let prog_section = site.library.get_section(&posts_path.join("tutorials").join("programming").join("_index.md")).unwrap(); let prog_section = site
.library
.get_section(&posts_path.join("tutorials").join("programming").join("_index.md"))
.unwrap();
assert_eq!(prog_section.subsections.len(), 0); assert_eq!(prog_section.subsections.len(), 0);
assert_eq!(prog_section.pages.len(), 2); assert_eq!(prog_section.pages.len(), 2);
} }
// 2 helper macros to make all the build testing more bearable // 2 helper macros to make all the build testing more bearable
macro_rules! file_exists { macro_rules! file_exists {
($root: expr, $path: expr) => { ($root: expr, $path: expr) => {{
{ let mut path = $root.clone();
let mut path = $root.clone(); for component in $path.split("/") {
for component in $path.split("/") { path = path.join(component);
path = path.join(component);
}
Path::new(&path).exists()
} }
} Path::new(&path).exists()
}};
} }
macro_rules! file_contains { macro_rules! file_contains {
($root: expr, $path: expr, $text: expr) => { ($root: expr, $path: expr, $text: expr) => {{
{ let mut path = $root.clone();
let mut path = $root.clone(); for component in $path.split("/") {
for component in $path.split("/") { path = path.join(component);
path = path.join(component);
}
let mut file = File::open(&path).unwrap();
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
println!("{}", s);
s.contains($text)
} }
} let mut file = File::open(&path).unwrap();
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
println!("{}", s);
s.contains($text)
}};
} }
#[test] #[test]
@ -145,7 +151,11 @@ fn can_build_site_without_live_reload() {
// Pages and section get their relative path // Pages and section get their relative path
assert!(file_contains!(public, "posts/tutorials/index.html", "posts/tutorials/_index.md")); assert!(file_contains!(public, "posts/tutorials/index.html", "posts/tutorials/_index.md"));
assert!(file_contains!(public, "posts/tutorials/devops/nix/index.html", "posts/tutorials/devops/nix.md")); assert!(file_contains!(
public,
"posts/tutorials/devops/nix/index.html",
"posts/tutorials/devops/nix.md"
));
// aliases work // aliases work
assert!(file_exists!(public, "an-old-url/old-page/index.html")); assert!(file_exists!(public, "an-old-url/old-page/index.html"));
@ -183,14 +193,26 @@ fn can_build_site_without_live_reload() {
assert_eq!(file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10"), false); assert_eq!(file_contains!(public, "index.html", "/livereload.js?port=1112&mindelay=10"), false);
// Both pages and sections are in the sitemap // Both pages and sections are in the sitemap
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/posts/simple/</loc>")); assert!(file_contains!(
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/posts/</loc>")); public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/posts/simple/</loc>"
));
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/posts/</loc>"
));
// Drafts are not in the sitemap // Drafts are not in the sitemap
assert!(!file_contains!(public, "sitemap.xml", "draft")); assert!(!file_contains!(public, "sitemap.xml", "draft"));
// robots.txt has been rendered from the template // robots.txt has been rendered from the template
assert!(file_contains!(public, "robots.txt", "User-agent: zola")); assert!(file_contains!(public, "robots.txt", "User-agent: zola"));
assert!(file_contains!(public, "robots.txt", "Sitemap: https://replace-this-with-your-url.com/sitemap.xml")); assert!(file_contains!(
public,
"robots.txt",
"Sitemap: https://replace-this-with-your-url.com/sitemap.xml"
));
} }
#[test] #[test]
@ -231,7 +253,11 @@ fn can_build_site_with_live_reload() {
assert!(file_contains!(public, "index.html", "/livereload.js")); assert!(file_contains!(public, "index.html", "/livereload.js"));
// the summary anchor link has been created // the summary anchor link has been created
assert!(file_contains!(public, "posts/python/index.html", r#"<a name="continue-reading"></a>"#)); assert!(file_contains!(
public,
"posts/python/index.html",
r#"<a name="continue-reading"></a>"#
));
assert!(file_contains!(public, "posts/draft/index.html", r#"THEME_SHORTCODE"#)); assert!(file_contains!(public, "posts/draft/index.html", r#"THEME_SHORTCODE"#));
} }
@ -245,7 +271,10 @@ fn can_build_site_with_taxonomies() {
for (i, (_, page)) in site.library.pages_mut().iter_mut().enumerate() { for (i, (_, page)) in site.library.pages_mut().iter_mut().enumerate() {
page.meta.taxonomies = { page.meta.taxonomies = {
let mut taxonomies = HashMap::new(); let mut taxonomies = HashMap::new();
taxonomies.insert("categories".to_string(), vec![if i % 2 == 0 { "A" } else { "B" }.to_string()]); taxonomies.insert(
"categories".to_string(),
vec![if i % 2 == 0 { "A" } else { "B" }.to_string()],
);
taxonomies taxonomies
}; };
} }
@ -278,15 +307,27 @@ fn can_build_site_with_taxonomies() {
assert!(file_exists!(public, "categories/a/index.html")); assert!(file_exists!(public, "categories/a/index.html"));
assert!(file_exists!(public, "categories/b/index.html")); assert!(file_exists!(public, "categories/b/index.html"));
assert!(file_exists!(public, "categories/a/rss.xml")); assert!(file_exists!(public, "categories/a/rss.xml"));
assert!(file_contains!(public, "categories/a/rss.xml", "https://replace-this-with-your-url.com/categories/a/rss.xml")); assert!(file_contains!(
public,
"categories/a/rss.xml",
"https://replace-this-with-your-url.com/categories/a/rss.xml"
));
// Extending from a theme works // Extending from a theme works
assert!(file_contains!(public, "categories/a/index.html", "EXTENDED")); assert!(file_contains!(public, "categories/a/index.html", "EXTENDED"));
// Tags aren't // Tags aren't
assert_eq!(file_exists!(public, "tags/index.html"), false); assert_eq!(file_exists!(public, "tags/index.html"), false);
// Categories are in the sitemap // Categories are in the sitemap
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/categories/</loc>")); assert!(file_contains!(
assert!(file_contains!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/categories/a/</loc>")); public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/categories/</loc>"
));
assert!(file_contains!(
public,
"sitemap.xml",
"<loc>https://replace-this-with-your-url.com/categories/a/</loc>"
));
} }
#[test] #[test]
@ -303,7 +344,11 @@ fn can_build_site_and_insert_anchor_links() {
assert!(Path::new(&public).exists()); assert!(Path::new(&public).exists());
// anchor link inserted // anchor link inserted
assert!(file_contains!(public, "posts/something-else/index.html", "<h1 id=\"title\"><a class=\"zola-anchor\" href=\"#title\"")); assert!(file_contains!(
public,
"posts/something-else/index.html",
"<h1 id=\"title\"><a class=\"zola-anchor\" href=\"#title\""
));
} }
#[test] #[test]
@ -352,8 +397,16 @@ fn can_build_site_with_pagination_for_section() {
assert!(file_contains!(public, "posts/index.html", "Current index: 1")); assert!(file_contains!(public, "posts/index.html", "Current index: 1"));
assert!(!file_contains!(public, "posts/index.html", "has_prev")); assert!(!file_contains!(public, "posts/index.html", "has_prev"));
assert!(file_contains!(public, "posts/index.html", "has_next")); assert!(file_contains!(public, "posts/index.html", "has_next"));
assert!(file_contains!(public, "posts/index.html", "First: https://replace-this-with-your-url.com/posts/")); assert!(file_contains!(
assert!(file_contains!(public, "posts/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/")); public,
"posts/index.html",
"First: https://replace-this-with-your-url.com/posts/"
));
assert!(file_contains!(
public,
"posts/index.html",
"Last: https://replace-this-with-your-url.com/posts/page/4/"
));
assert_eq!(file_contains!(public, "posts/index.html", "has_prev"), false); assert_eq!(file_contains!(public, "posts/index.html", "has_prev"), false);
assert!(file_exists!(public, "posts/page/2/index.html")); assert!(file_exists!(public, "posts/page/2/index.html"));
@ -362,8 +415,16 @@ fn can_build_site_with_pagination_for_section() {
assert!(file_contains!(public, "posts/page/2/index.html", "Current index: 2")); assert!(file_contains!(public, "posts/page/2/index.html", "Current index: 2"));
assert!(file_contains!(public, "posts/page/2/index.html", "has_prev")); assert!(file_contains!(public, "posts/page/2/index.html", "has_prev"));
assert!(file_contains!(public, "posts/page/2/index.html", "has_next")); assert!(file_contains!(public, "posts/page/2/index.html", "has_next"));
assert!(file_contains!(public, "posts/page/2/index.html", "First: https://replace-this-with-your-url.com/posts/")); assert!(file_contains!(
assert!(file_contains!(public, "posts/page/2/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/")); public,
"posts/page/2/index.html",
"First: https://replace-this-with-your-url.com/posts/"
));
assert!(file_contains!(
public,
"posts/page/2/index.html",
"Last: https://replace-this-with-your-url.com/posts/page/4/"
));
assert!(file_exists!(public, "posts/page/3/index.html")); assert!(file_exists!(public, "posts/page/3/index.html"));
assert!(file_contains!(public, "posts/page/3/index.html", "Num pagers: 4")); assert!(file_contains!(public, "posts/page/3/index.html", "Num pagers: 4"));
@ -371,8 +432,16 @@ fn can_build_site_with_pagination_for_section() {
assert!(file_contains!(public, "posts/page/3/index.html", "Current index: 3")); assert!(file_contains!(public, "posts/page/3/index.html", "Current index: 3"));
assert!(file_contains!(public, "posts/page/3/index.html", "has_prev")); assert!(file_contains!(public, "posts/page/3/index.html", "has_prev"));
assert!(file_contains!(public, "posts/page/3/index.html", "has_next")); assert!(file_contains!(public, "posts/page/3/index.html", "has_next"));
assert!(file_contains!(public, "posts/page/3/index.html", "First: https://replace-this-with-your-url.com/posts/")); assert!(file_contains!(
assert!(file_contains!(public, "posts/page/3/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/")); public,
"posts/page/3/index.html",
"First: https://replace-this-with-your-url.com/posts/"
));
assert!(file_contains!(
public,
"posts/page/3/index.html",
"Last: https://replace-this-with-your-url.com/posts/page/4/"
));
assert!(file_exists!(public, "posts/page/4/index.html")); assert!(file_exists!(public, "posts/page/4/index.html"));
assert!(file_contains!(public, "posts/page/4/index.html", "Num pagers: 4")); assert!(file_contains!(public, "posts/page/4/index.html", "Num pagers: 4"));
@ -380,8 +449,16 @@ fn can_build_site_with_pagination_for_section() {
assert!(file_contains!(public, "posts/page/4/index.html", "Current index: 4")); assert!(file_contains!(public, "posts/page/4/index.html", "Current index: 4"));
assert!(file_contains!(public, "posts/page/4/index.html", "has_prev")); assert!(file_contains!(public, "posts/page/4/index.html", "has_prev"));
assert!(!file_contains!(public, "posts/page/4/index.html", "has_next")); assert!(!file_contains!(public, "posts/page/4/index.html", "has_next"));
assert!(file_contains!(public, "posts/page/4/index.html", "First: https://replace-this-with-your-url.com/posts/")); assert!(file_contains!(
assert!(file_contains!(public, "posts/page/4/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/")); public,
"posts/page/4/index.html",
"First: https://replace-this-with-your-url.com/posts/"
));
assert!(file_contains!(
public,
"posts/page/4/index.html",
"Last: https://replace-this-with-your-url.com/posts/page/4/"
));
} }
#[test] #[test]
@ -448,7 +525,6 @@ fn can_build_rss_feed() {
assert!(file_contains!(public, "rss.xml", "Simple article with shortcodes")); assert!(file_contains!(public, "rss.xml", "Simple article with shortcodes"));
} }
#[test] #[test]
fn can_build_search_index() { fn can_build_search_index() {
let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
@ -479,6 +555,9 @@ fn can_build_with_extra_syntaxes() {
assert!(&public.exists()); assert!(&public.exists());
assert!(file_exists!(public, "posts/extra-syntax/index.html")); assert!(file_exists!(public, "posts/extra-syntax/index.html"));
assert!(file_contains!(public, "posts/extra-syntax/index.html", assert!(file_contains!(
r#"<span style="color:#d08770;">test</span>"#)); public,
"posts/extra-syntax/index.html",
r#"<span style="color:#d08770;">test</span>"#
));
} }

View file

@ -1,9 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use base64::{encode, decode}; use base64::{decode, encode};
use pulldown_cmark as cmark; use pulldown_cmark as cmark;
use tera::{Value, to_value, Result as TeraResult}; use tera::{to_value, Result as TeraResult, Value};
pub fn markdown(value: Value, args: HashMap<String, Value>) -> TeraResult<Value> { pub fn markdown(value: Value, args: HashMap<String, Value>) -> TeraResult<Value> {
let s = try_get_value!("markdown", "value", String, value); let s = try_get_value!("markdown", "value", String, value);
@ -31,33 +30,23 @@ pub fn markdown(value: Value, args: HashMap<String, Value>) -> TeraResult<Value>
Ok(to_value(&html).unwrap()) Ok(to_value(&html).unwrap())
} }
pub fn base64_encode(value: Value, _: HashMap<String, Value>) -> TeraResult<Value> { pub fn base64_encode(value: Value, _: HashMap<String, Value>) -> TeraResult<Value> {
let s = try_get_value!("base64_encode", "value", String, value); let s = try_get_value!("base64_encode", "value", String, value);
Ok( Ok(to_value(&encode(s.as_bytes())).unwrap())
to_value(&encode(s.as_bytes())).unwrap()
)
} }
pub fn base64_decode(value: Value, _: HashMap<String, Value>) -> TeraResult<Value> { pub fn base64_decode(value: Value, _: HashMap<String, Value>) -> TeraResult<Value> {
let s = try_get_value!("base64_decode", "value", String, value); let s = try_get_value!("base64_decode", "value", String, value);
Ok( Ok(to_value(&String::from_utf8(decode(s.as_bytes()).unwrap()).unwrap()).unwrap())
to_value(
&String::from_utf8(
decode(s.as_bytes()).unwrap()
).unwrap()
).unwrap()
)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use tera::to_value; use tera::to_value;
use super::{markdown, base64_decode, base64_encode}; use super::{base64_decode, base64_encode, markdown};
#[test] #[test]
fn markdown_filter() { fn markdown_filter() {
@ -70,7 +59,10 @@ mod tests {
fn markdown_filter_inline() { fn markdown_filter_inline() {
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("inline".to_string(), to_value(true).unwrap()); args.insert("inline".to_string(), to_value(true).unwrap());
let result = markdown(to_value(&"Using `map`, `filter`, and `fold` instead of `for`").unwrap(), args); let result = markdown(
to_value(&"Using `map`, `filter`, and `fold` instead of `for`").unwrap(),
args,
);
assert!(result.is_ok()); assert!(result.is_ok());
assert_eq!(result.unwrap(), to_value(&"Using <code>map</code>, <code>filter</code>, and <code>fold</code> instead of <code>for</code>").unwrap()); assert_eq!(result.unwrap(), to_value(&"Using <code>map</code>, <code>filter</code>, and <code>fold</code> instead of <code>for</code>").unwrap());
} }
@ -80,12 +72,18 @@ mod tests {
fn markdown_filter_inline_tables() { fn markdown_filter_inline_tables() {
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("inline".to_string(), to_value(true).unwrap()); args.insert("inline".to_string(), to_value(true).unwrap());
let result = markdown(to_value(&r#" let result = markdown(
to_value(
&r#"
|id|author_id| timestamp_created|title |content | |id|author_id| timestamp_created|title |content |
|-:|--------:|-----------------------:|:---------------------|:-----------------| |-:|--------:|-----------------------:|:---------------------|:-----------------|
| 1| 1|2018-09-05 08:03:43.141Z|How to train your ORM |Badly written blog| | 1| 1|2018-09-05 08:03:43.141Z|How to train your ORM |Badly written blog|
| 2| 1|2018-08-22 13:11:50.050Z|How to bake a nice pie|Badly written blog| | 2| 1|2018-08-22 13:11:50.050Z|How to bake a nice pie|Badly written blog|
"#).unwrap(), args); "#,
)
.unwrap(),
args,
);
assert!(result.is_ok()); assert!(result.is_ok());
assert!(result.unwrap().as_str().unwrap().contains("<table>")); assert!(result.unwrap().as_str().unwrap().contains("<table>"));
} }
@ -100,7 +98,7 @@ mod tests {
("foo", "Zm9v"), ("foo", "Zm9v"),
("foob", "Zm9vYg=="), ("foob", "Zm9vYg=="),
("fooba", "Zm9vYmE="), ("fooba", "Zm9vYmE="),
("foobar", "Zm9vYmFy") ("foobar", "Zm9vYmFy"),
]; ];
for (input, expected) in tests { for (input, expected) in tests {
let args = HashMap::new(); let args = HashMap::new();
@ -110,7 +108,6 @@ mod tests {
} }
} }
#[test] #[test]
fn base64_decode_filter() { fn base64_decode_filter() {
let tests = vec![ let tests = vec![
@ -120,7 +117,7 @@ mod tests {
("Zm9v", "foo"), ("Zm9v", "foo"),
("Zm9vYg==", "foob"), ("Zm9vYg==", "foob"),
("Zm9vYmE=", "fooba"), ("Zm9vYmE=", "fooba"),
("Zm9vYmFy", "foobar") ("Zm9vYmFy", "foobar"),
]; ];
for (input, expected) in tests { for (input, expected) in tests {
let args = HashMap::new(); let args = HashMap::new();

View file

@ -1,28 +1,28 @@
extern crate toml;
extern crate serde_json; extern crate serde_json;
extern crate toml;
use utils::fs::{read_file, is_path_in_directory, get_file_time}; use utils::fs::{get_file_time, is_path_in_directory, read_file};
use std::hash::{Hasher, Hash}; use reqwest::{header, Client};
use std::str::FromStr;
use std::fmt;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use reqwest::{Client, header}; use std::fmt;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
use url::Url; use url::Url;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use csv::Reader; use csv::Reader;
use std::collections::HashMap; use std::collections::HashMap;
use tera::{GlobalFn, Value, from_value, to_value, Result, Map, Error}; use tera::{from_value, to_value, Error, GlobalFn, Map, Result, Value};
static GET_DATA_ARGUMENT_ERROR_MESSAGE: &str = "`load_data`: requires EITHER a `path` or `url` argument"; static GET_DATA_ARGUMENT_ERROR_MESSAGE: &str =
"`load_data`: requires EITHER a `path` or `url` argument";
enum DataSource { enum DataSource {
Url(Url), Url(Url),
Path(PathBuf) Path(PathBuf),
} }
#[derive(Debug)] #[derive(Debug)]
@ -30,7 +30,7 @@ enum OutputFormat {
Toml, Toml,
Json, Json,
Csv, Csv,
Plain Plain,
} }
impl fmt::Display for OutputFormat { impl fmt::Display for OutputFormat {
@ -54,7 +54,7 @@ impl FromStr for OutputFormat {
"csv" => Ok(OutputFormat::Csv), "csv" => Ok(OutputFormat::Csv),
"json" => Ok(OutputFormat::Json), "json" => Ok(OutputFormat::Json),
"plain" => Ok(OutputFormat::Plain), "plain" => Ok(OutputFormat::Plain),
format => Err(format!("Unknown output format {}", format).into()) format => Err(format!("Unknown output format {}", format).into()),
}; };
} }
} }
@ -71,7 +71,11 @@ impl OutputFormat {
} }
impl DataSource { impl DataSource {
fn from_args(path_arg: Option<String>, url_arg: Option<String>, content_path: &PathBuf) -> Result<Self> { fn from_args(
path_arg: Option<String>,
url_arg: Option<String>,
content_path: &PathBuf,
) -> Result<Self> {
if path_arg.is_some() && url_arg.is_some() { if path_arg.is_some() && url_arg.is_some() {
return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into()); return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into());
} }
@ -85,7 +89,9 @@ impl DataSource {
} }
if let Some(url) = url_arg { if let Some(url) = url_arg {
return Url::parse(&url).map(|parsed_url| DataSource::Url(parsed_url)).map_err(|e| format!("Failed to parse {} as url: {}", url, e).into()); return Url::parse(&url)
.map(|parsed_url| DataSource::Url(parsed_url))
.map_err(|e| format!("Failed to parse {} as url: {}", url, e).into());
} }
return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into()); return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into());
@ -111,32 +117,37 @@ impl Hash for DataSource {
} }
} }
fn get_data_source_from_args(
content_path: &PathBuf,
args: &HashMap<String, Value>,
) -> Result<DataSource> {
let path_arg = optional_arg!(String, args.get("path"), GET_DATA_ARGUMENT_ERROR_MESSAGE);
fn get_data_source_from_args(content_path: &PathBuf, args: &HashMap<String, Value>) -> Result<DataSource> { let url_arg = optional_arg!(String, args.get("url"), GET_DATA_ARGUMENT_ERROR_MESSAGE);
let path_arg = optional_arg!(
String,
args.get("path"),
GET_DATA_ARGUMENT_ERROR_MESSAGE
);
let url_arg = optional_arg!(
String,
args.get("url"),
GET_DATA_ARGUMENT_ERROR_MESSAGE
);
return DataSource::from_args(path_arg, url_arg, content_path); return DataSource::from_args(path_arg, url_arg, content_path);
} }
fn read_data_file(base_path: &PathBuf, full_path: PathBuf) -> Result<String> { fn read_data_file(base_path: &PathBuf, full_path: PathBuf) -> Result<String> {
if !is_path_in_directory(&base_path, &full_path).map_err(|e| format!("Failed to read data file {}: {}", full_path.display(), e))? { if !is_path_in_directory(&base_path, &full_path)
return Err(format!("{} is not inside the base site directory {}", full_path.display(), base_path.display()).into()); .map_err(|e| format!("Failed to read data file {}: {}", full_path.display(), e))?
{
return Err(format!(
"{} is not inside the base site directory {}",
full_path.display(),
base_path.display()
)
.into());
} }
return read_file(&full_path) return read_file(&full_path).map_err(|e| {
.map_err(|e| format!("`load_data`: error {} loading file {}", full_path.to_str().unwrap(), e).into()); format!("`load_data`: error {} loading file {}", full_path.to_str().unwrap(), e).into()
});
} }
fn get_output_format_from_args(args: &HashMap<String, Value>, data_source: &DataSource) -> Result<OutputFormat> { fn get_output_format_from_args(
args: &HashMap<String, Value>,
data_source: &DataSource,
) -> Result<OutputFormat> {
let format_arg = optional_arg!( let format_arg = optional_arg!(
String, String,
args.get("format"), args.get("format"),
@ -148,7 +159,10 @@ fn get_output_format_from_args(args: &HashMap<String, Value>, data_source: &Data
} }
let from_extension = if let DataSource::Path(path) = data_source { let from_extension = if let DataSource::Path(path) = data_source {
let extension_result: Result<&str> = path.extension().map(|extension| extension.to_str().unwrap()).ok_or(format!("Could not determine format for {} from extension", path.display()).into()); let extension_result: Result<&str> =
path.extension().map(|extension| extension.to_str().unwrap()).ok_or(
format!("Could not determine format for {} from extension", path.display()).into(),
);
extension_result? extension_result?
} else { } else {
"plain" "plain"
@ -156,7 +170,6 @@ fn get_output_format_from_args(args: &HashMap<String, Value>, data_source: &Data
return OutputFormat::from_str(from_extension); return OutputFormat::from_str(from_extension);
} }
/// A global function to load data from a file or from a URL /// A global function to load data from a file or from a URL
/// Currently the supported formats are json, toml, csv and plain text /// Currently the supported formats are json, toml, csv and plain text
pub fn make_load_data(content_path: PathBuf, base_path: PathBuf) -> GlobalFn { pub fn make_load_data(content_path: PathBuf, base_path: PathBuf) -> GlobalFn {
@ -180,9 +193,22 @@ pub fn make_load_data(content_path: PathBuf, base_path: PathBuf) -> GlobalFn {
let data = match data_source { let data = match data_source {
DataSource::Path(path) => read_data_file(&base_path, path), DataSource::Path(path) => read_data_file(&base_path, path),
DataSource::Url(url) => { DataSource::Url(url) => {
let mut response = response_client.get(url.as_str()).header(header::ACCEPT, file_format.as_accept_header()).send().and_then(|res| res.error_for_status()).map_err(|e| format!("Failed to request {}: {}", url, e.status().expect("response status")))?; let mut response = response_client
response.text().map_err(|e| format!("Failed to parse response from {}: {:?}", url, e).into()) .get(url.as_str())
}, .header(header::ACCEPT, file_format.as_accept_header())
.send()
.and_then(|res| res.error_for_status())
.map_err(|e| {
format!(
"Failed to request {}: {}",
url,
e.status().expect("response status")
)
})?;
response
.text()
.map_err(|e| format!("Failed to parse response from {}: {:?}", url, e).into())
}
}?; }?;
let result_value: Result<Value> = match file_format { let result_value: Result<Value> = match file_format {
@ -202,7 +228,8 @@ pub fn make_load_data(content_path: PathBuf, base_path: PathBuf) -> GlobalFn {
/// Parse a JSON string and convert it to a Tera Value /// Parse a JSON string and convert it to a Tera Value
fn load_json(json_data: String) -> Result<Value> { fn load_json(json_data: String) -> Result<Value> {
let json_content: Value = serde_json::from_str(json_data.as_str()).map_err(|e| format!("{:?}", e))?; let json_content: Value =
serde_json::from_str(json_data.as_str()).map_err(|e| format!("{:?}", e))?;
return Ok(json_content); return Ok(json_content);
} }
@ -235,12 +262,11 @@ fn load_csv(csv_data: String) -> Result<Value> {
let mut csv_map = Map::new(); let mut csv_map = Map::new();
{ {
let hdrs = reader.headers() let hdrs = reader.headers().map_err(|e| {
.map_err(|e| format!("'load_data': {} - unable to read CSV header line (line 1) for CSV file", e))?; format!("'load_data': {} - unable to read CSV header line (line 1) for CSV file", e)
})?;
let headers_array = hdrs.iter() let headers_array = hdrs.iter().map(|v| Value::String(v.to_string())).collect();
.map(|v| Value::String(v.to_string()))
.collect();
csv_map.insert(String::from("headers"), Value::Array(headers_array)); csv_map.insert(String::from("headers"), Value::Array(headers_array));
} }
@ -268,7 +294,6 @@ fn load_csv(csv_data: String) -> Result<Value> {
to_value(csv_value).map_err(|err| err.into()) to_value(csv_value).map_err(|err| err.into())
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{make_load_data, DataSource, OutputFormat}; use super::{make_load_data, DataSource, OutputFormat};
@ -285,7 +310,8 @@ mod tests {
#[test] #[test]
fn fails_when_missing_file() { fn fails_when_missing_file() {
let static_fn = make_load_data(PathBuf::from("../utils/test-files"), PathBuf::from("../utils")); let static_fn =
make_load_data(PathBuf::from("../utils/test-files"), PathBuf::from("../utils"));
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("../../../READMEE.md").unwrap()); args.insert("path".to_string(), to_value("../../../READMEE.md").unwrap());
let result = static_fn(args); let result = static_fn(args);
@ -295,40 +321,54 @@ mod tests {
#[test] #[test]
fn cant_load_outside_content_dir() { fn cant_load_outside_content_dir() {
let static_fn = make_load_data(PathBuf::from("../utils/test-files"), PathBuf::from("../utils")); let static_fn =
make_load_data(PathBuf::from("../utils/test-files"), PathBuf::from("../utils"));
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("../../../README.md").unwrap()); args.insert("path".to_string(), to_value("../../../README.md").unwrap());
args.insert("format".to_string(), to_value("plain").unwrap()); args.insert("format".to_string(), to_value("plain").unwrap());
let result = static_fn(args); let result = static_fn(args);
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().description().contains("README.md is not inside the base site directory")); assert!(
result
.unwrap_err()
.description()
.contains("README.md is not inside the base site directory")
);
} }
#[test] #[test]
fn calculates_cache_key_for_path() { fn calculates_cache_key_for_path() {
// We can't test against a fixed value, due to the fact the cache key is built from the absolute path // We can't test against a fixed value, due to the fact the cache key is built from the absolute path
let cache_key = DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml); let cache_key =
let cache_key_2 = DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml); DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml);
let cache_key_2 =
DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml);
assert_eq!(cache_key, cache_key_2); assert_eq!(cache_key, cache_key_2);
} }
#[test] #[test]
fn calculates_cache_key_for_url() { fn calculates_cache_key_for_url() {
let cache_key = DataSource::Url("https://api.github.com/repos/getzola/zola".parse().unwrap()).get_cache_key(&OutputFormat::Plain); let cache_key =
DataSource::Url("https://api.github.com/repos/getzola/zola".parse().unwrap())
.get_cache_key(&OutputFormat::Plain);
assert_eq!(cache_key, 8916756616423791754); assert_eq!(cache_key, 8916756616423791754);
} }
#[test] #[test]
fn different_cache_key_per_filename() { fn different_cache_key_per_filename() {
let toml_cache_key = DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml); let toml_cache_key =
let json_cache_key = DataSource::Path(get_test_file("test.json")).get_cache_key(&OutputFormat::Toml); DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml);
let json_cache_key =
DataSource::Path(get_test_file("test.json")).get_cache_key(&OutputFormat::Toml);
assert_ne!(toml_cache_key, json_cache_key); assert_ne!(toml_cache_key, json_cache_key);
} }
#[test] #[test]
fn different_cache_key_per_format() { fn different_cache_key_per_format() {
let toml_cache_key = DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml); let toml_cache_key =
let json_cache_key = DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Json); DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Toml);
let json_cache_key =
DataSource::Path(get_test_file("test.toml")).get_cache_key(&OutputFormat::Json);
assert_ne!(toml_cache_key, json_cache_key); assert_ne!(toml_cache_key, json_cache_key);
} }
@ -339,7 +379,10 @@ mod tests {
args.insert("url".to_string(), to_value("https://httpbin.org/json").unwrap()); args.insert("url".to_string(), to_value("https://httpbin.org/json").unwrap());
args.insert("format".to_string(), to_value("json").unwrap()); args.insert("format".to_string(), to_value("json").unwrap());
let result = static_fn(args).unwrap(); let result = static_fn(args).unwrap();
assert_eq!(result.get("slideshow").unwrap().get("title").unwrap(), &to_value("Sample Slide Show").unwrap()); assert_eq!(
result.get("slideshow").unwrap().get("title").unwrap(),
&to_value("Sample Slide Show").unwrap()
);
} }
#[test] #[test]
@ -350,60 +393,78 @@ mod tests {
args.insert("format".to_string(), to_value("json").unwrap()); args.insert("format".to_string(), to_value("json").unwrap());
let result = static_fn(args); let result = static_fn(args);
assert!(result.is_err()); assert!(result.is_err());
assert_eq!(result.unwrap_err().description(), "Failed to request https://httpbin.org/status/404/: 404 Not Found"); assert_eq!(
result.unwrap_err().description(),
"Failed to request https://httpbin.org/status/404/: 404 Not Found"
);
} }
#[test] #[test]
fn can_load_toml() fn can_load_toml() {
{ let static_fn = make_load_data(
let static_fn = make_load_data(PathBuf::from("../utils/test-files"), PathBuf::from("../utils/test-files")); PathBuf::from("../utils/test-files"),
PathBuf::from("../utils/test-files"),
);
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.toml").unwrap()); args.insert("path".to_string(), to_value("test.toml").unwrap());
let result = static_fn(args.clone()).unwrap(); let result = static_fn(args.clone()).unwrap();
//TOML does not load in order, and also dates are not returned as strings, but //TOML does not load in order, and also dates are not returned as strings, but
//rather as another object with a key and value //rather as another object with a key and value
assert_eq!(result, json!({ assert_eq!(
result,
json!({
"category": { "category": {
"date": { "date": {
"$__toml_private_datetime": "1979-05-27T07:32:00Z" "$__toml_private_datetime": "1979-05-27T07:32:00Z"
}, },
"key": "value" "key": "value"
}, },
})); })
);
} }
#[test] #[test]
fn can_load_csv() fn can_load_csv() {
{ let static_fn = make_load_data(
let static_fn = make_load_data(PathBuf::from("../utils/test-files"), PathBuf::from("../utils/test-files")); PathBuf::from("../utils/test-files"),
PathBuf::from("../utils/test-files"),
);
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.csv").unwrap()); args.insert("path".to_string(), to_value("test.csv").unwrap());
let result = static_fn(args.clone()).unwrap(); let result = static_fn(args.clone()).unwrap();
assert_eq!(result, json!({ assert_eq!(
result,
json!({
"headers": ["Number", "Title"], "headers": ["Number", "Title"],
"records": [ "records": [
["1", "Gutenberg"], ["1", "Gutenberg"],
["2", "Printing"] ["2", "Printing"]
], ],
})) })
)
} }
#[test] #[test]
fn can_load_json() fn can_load_json() {
{ let static_fn = make_load_data(
let static_fn = make_load_data(PathBuf::from("../utils/test-files"), PathBuf::from("../utils/test-files")); PathBuf::from("../utils/test-files"),
PathBuf::from("../utils/test-files"),
);
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.json").unwrap()); args.insert("path".to_string(), to_value("test.json").unwrap());
let result = static_fn(args.clone()).unwrap(); let result = static_fn(args.clone()).unwrap();
assert_eq!(result, json!({ assert_eq!(
result,
json!({
"key": "value", "key": "value",
"array": [1, 2, 3], "array": [1, 2, 3],
"subpackage": { "subpackage": {
"subkey": 5 "subkey": 5
} }
})) })
)
} }
} }

View file

@ -4,9 +4,9 @@ macro_rules! required_arg {
match $e { match $e {
Some(v) => match from_value::<$ty>(v.clone()) { Some(v) => match from_value::<$ty>(v.clone()) {
Ok(u) => u, Ok(u) => u,
Err(_) => return Err($err.into()) Err(_) => return Err($err.into()),
}, },
None => return Err($err.into()) None => return Err($err.into()),
} }
}; };
} }
@ -17,9 +17,9 @@ macro_rules! optional_arg {
match $e { match $e {
Some(v) => match from_value::<$ty>(v.clone()) { Some(v) => match from_value::<$ty>(v.clone()) {
Ok(u) => Some(u), Ok(u) => Some(u),
Err(_) => return Err($err.into()) Err(_) => return Err($err.into()),
}, },
None => None None => None,
} }
}; };
} }

View file

@ -3,10 +3,10 @@ extern crate error_chain;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tera::{GlobalFn, Value, from_value, to_value, Result}; use tera::{from_value, to_value, GlobalFn, Result, Value};
use library::{Taxonomy, Library};
use config::Config; use config::Config;
use library::{Library, Taxonomy};
use utils::site::resolve_internal_link; use utils::site::resolve_internal_link;
use imageproc; use imageproc;
@ -18,24 +18,19 @@ mod load_data;
pub use self::load_data::make_load_data; pub use self::load_data::make_load_data;
pub fn make_trans(config: Config) -> GlobalFn { pub fn make_trans(config: Config) -> GlobalFn {
let translations_config = config.translations; let translations_config = config.translations;
let default_lang = config.default_language.clone(); let default_lang = config.default_language.clone();
Box::new(move |args| -> Result<Value> { Box::new(move |args| -> Result<Value> {
let key = required_arg!(String, args.get("key"), "`trans` requires a `key` argument."); let key = required_arg!(String, args.get("key"), "`trans` requires a `key` argument.");
let lang = optional_arg!( let lang = optional_arg!(String, args.get("lang"), "`trans`: `lang` must be a string.")
String, .unwrap_or_else(|| default_lang.clone());
args.get("lang"),
"`trans`: `lang` must be a string."
).unwrap_or_else(|| default_lang.clone());
let translations = &translations_config[lang.as_str()]; let translations = &translations_config[lang.as_str()];
Ok(to_value(&translations[key.as_str()]).unwrap()) Ok(to_value(&translations[key.as_str()]).unwrap())
}) })
} }
pub fn make_get_page(library: &Library) -> GlobalFn { pub fn make_get_page(library: &Library) -> GlobalFn {
let mut pages = HashMap::new(); let mut pages = HashMap::new();
for page in library.pages_values() { for page in library.pages_values() {
@ -53,7 +48,7 @@ pub fn make_get_page(library: &Library) -> GlobalFn {
); );
match pages.get(&path) { match pages.get(&path) {
Some(p) => Ok(p.clone()), Some(p) => Ok(p.clone()),
None => Err(format!("Page `{}` not found.", path).into()) None => Err(format!("Page `{}` not found.", path).into()),
} }
}) })
} }
@ -64,12 +59,14 @@ pub fn make_get_section(library: &Library) -> GlobalFn {
for section in library.sections_values() { for section in library.sections_values() {
sections.insert( sections.insert(
section.file.relative.clone(), section.file.relative.clone(),
to_value(library.get_section(&section.file.path).unwrap().to_serialized(library)).unwrap(), to_value(library.get_section(&section.file.path).unwrap().to_serialized(library))
.unwrap(),
); );
sections_basic.insert( sections_basic.insert(
section.file.relative.clone(), section.file.relative.clone(),
to_value(library.get_section(&section.file.path).unwrap().to_serialized_basic(library)).unwrap(), to_value(library.get_section(&section.file.path).unwrap().to_serialized_basic(library))
.unwrap(),
); );
} }
@ -82,36 +79,25 @@ pub fn make_get_section(library: &Library) -> GlobalFn {
let metadata_only = args let metadata_only = args
.get("metadata_only") .get("metadata_only")
.map_or(false, |c| { .map_or(false, |c| from_value::<bool>(c.clone()).unwrap_or(false));
from_value::<bool>(c.clone()).unwrap_or(false)
});
let container = if metadata_only { let container = if metadata_only { &sections_basic } else { &sections };
&sections_basic
} else {
&sections
};
match container.get(&path) { match container.get(&path) {
Some(p) => Ok(p.clone()), Some(p) => Ok(p.clone()),
None => Err(format!("Section `{}` not found.", path).into()) None => Err(format!("Section `{}` not found.", path).into()),
} }
}) })
} }
pub fn make_get_url(permalinks: HashMap<String, String>, config: Config) -> GlobalFn { pub fn make_get_url(permalinks: HashMap<String, String>, config: Config) -> GlobalFn {
Box::new(move |args| -> Result<Value> { Box::new(move |args| -> Result<Value> {
let cachebust = args let cachebust =
.get("cachebust") args.get("cachebust").map_or(false, |c| from_value::<bool>(c.clone()).unwrap_or(false));
.map_or(false, |c| {
from_value::<bool>(c.clone()).unwrap_or(false)
});
let trailing_slash = args let trailing_slash = args
.get("trailing_slash") .get("trailing_slash")
.map_or(false, |c| { .map_or(false, |c| from_value::<bool>(c.clone()).unwrap_or(false));
from_value::<bool>(c.clone()).unwrap_or(false)
});
let path = required_arg!( let path = required_arg!(
String, String,
@ -121,7 +107,9 @@ pub fn make_get_url(permalinks: HashMap<String, String>, config: Config) -> Glob
if path.starts_with("./") { if path.starts_with("./") {
match resolve_internal_link(&path, &permalinks) { match resolve_internal_link(&path, &permalinks) {
Ok(url) => Ok(to_value(url).unwrap()), Ok(url) => Ok(to_value(url).unwrap()),
Err(_) => Err(format!("Could not resolve URL for link `{}` not found.", path).into()) Err(_) => {
Err(format!("Could not resolve URL for link `{}` not found.", path).into())
}
} }
} else { } else {
// anything else // anything else
@ -141,10 +129,8 @@ pub fn make_get_url(permalinks: HashMap<String, String>, config: Config) -> Glob
pub fn make_get_taxonomy(all_taxonomies: &[Taxonomy], library: &Library) -> GlobalFn { pub fn make_get_taxonomy(all_taxonomies: &[Taxonomy], library: &Library) -> GlobalFn {
let mut taxonomies = HashMap::new(); let mut taxonomies = HashMap::new();
for taxonomy in all_taxonomies { for taxonomy in all_taxonomies {
taxonomies.insert( taxonomies
taxonomy.kind.name.clone(), .insert(taxonomy.kind.name.clone(), to_value(taxonomy.to_serialized(library)).unwrap());
to_value(taxonomy.to_serialized(library)).unwrap()
);
} }
Box::new(move |args| -> Result<Value> { Box::new(move |args| -> Result<Value> {
@ -155,9 +141,11 @@ pub fn make_get_taxonomy(all_taxonomies: &[Taxonomy], library: &Library) -> Glob
); );
let container = match taxonomies.get(&kind) { let container = match taxonomies.get(&kind) {
Some(c) => c, Some(c) => c,
None => return Err( None => {
format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into() return Err(
), format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into()
)
}
}; };
Ok(to_value(container).unwrap()) Ok(to_value(container).unwrap())
@ -187,18 +175,20 @@ pub fn make_get_taxonomy_url(all_taxonomies: &[Taxonomy]) -> GlobalFn {
); );
let container = match taxonomies.get(&kind) { let container = match taxonomies.get(&kind) {
Some(c) => c, Some(c) => c,
None => return Err( None => {
format!("`get_taxonomy_url` received an unknown taxonomy as kind: {}", kind).into() return Err(format!(
) "`get_taxonomy_url` received an unknown taxonomy as kind: {}",
kind
)
.into())
}
}; };
if let Some(ref permalink) = container.get(&name) { if let Some(ref permalink) = container.get(&name) {
return Ok(to_value(permalink.clone()).unwrap()); return Ok(to_value(permalink.clone()).unwrap());
} }
Err( Err(format!("`get_taxonomy_url`: couldn't find `{}` in `{}` taxonomy", name, kind).into())
format!("`get_taxonomy_url`: couldn't find `{}` in `{}` taxonomy", name, kind).into()
)
}) })
} }
@ -222,16 +212,11 @@ pub fn make_resize_image(imageproc: Arc<Mutex<imageproc::Processor>>) -> GlobalF
args.get("height"), args.get("height"),
"`resize_image`: `height` must be a non-negative integer" "`resize_image`: `height` must be a non-negative integer"
); );
let op = optional_arg!( let op = optional_arg!(String, args.get("op"), "`resize_image`: `op` must be a string")
String, .unwrap_or_else(|| DEFAULT_OP.to_string());
args.get("op"), let quality =
"`resize_image`: `op` must be a string" optional_arg!(u8, args.get("quality"), "`resize_image`: `quality` must be a number")
).unwrap_or_else(|| DEFAULT_OP.to_string()); .unwrap_or(DEFAULT_Q);
let quality = optional_arg!(
u8,
args.get("quality"),
"`resize_image`: `quality` must be a number"
).unwrap_or(DEFAULT_Q);
if quality == 0 || quality > 100 { if quality == 0 || quality > 100 {
return Err("`resize_image`: `quality` must be in range 1-100".to_string().into()); return Err("`resize_image`: `quality` must be in range 1-100".to_string().into());
} }
@ -249,19 +234,16 @@ pub fn make_resize_image(imageproc: Arc<Mutex<imageproc::Processor>>) -> GlobalF
}) })
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{make_get_url, make_get_taxonomy, make_get_taxonomy_url, make_trans}; use super::{make_get_taxonomy, make_get_taxonomy_url, make_get_url, make_trans};
use std::collections::HashMap; use std::collections::HashMap;
use tera::{to_value, Value}; use tera::{to_value, Value};
use config::{Config, Taxonomy as TaxonomyConfig}; use config::{Config, Taxonomy as TaxonomyConfig};
use library::{Taxonomy, TaxonomyItem, Library}; use library::{Library, Taxonomy, TaxonomyItem};
#[test] #[test]
fn can_add_cachebust_to_url() { fn can_add_cachebust_to_url() {
@ -307,17 +289,8 @@ mod tests {
fn can_get_taxonomy() { fn can_get_taxonomy() {
let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() };
let library = Library::new(0, 0); let library = Library::new(0, 0);
let tag = TaxonomyItem::new( let tag = TaxonomyItem::new("Programming", "tags", &Config::default(), vec![], &library);
"Programming", let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
"tags",
&Config::default(),
vec![],
&library
);
let tags = Taxonomy {
kind: taxo_config,
items: vec![tag],
};
let taxonomies = vec![tags.clone()]; let taxonomies = vec![tags.clone()];
let static_fn = make_get_taxonomy(&taxonomies, &library); let static_fn = make_get_taxonomy(&taxonomies, &library);
@ -337,7 +310,8 @@ mod tests {
Value::String("programming".to_string()) Value::String("programming".to_string())
); );
assert_eq!( assert_eq!(
res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()["permalink"], res_obj["items"].clone().as_array().unwrap()[0].clone().as_object().unwrap()
["permalink"],
Value::String("http://a-website.com/tags/programming/".to_string()) Value::String("http://a-website.com/tags/programming/".to_string())
); );
assert_eq!( assert_eq!(
@ -354,17 +328,8 @@ mod tests {
fn can_get_taxonomy_url() { fn can_get_taxonomy_url() {
let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() };
let library = Library::new(0, 0); let library = Library::new(0, 0);
let tag = TaxonomyItem::new( let tag = TaxonomyItem::new("Programming", "tags", &Config::default(), vec![], &library);
"Programming", let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
"tags",
&Config::default(),
vec![],
&library
);
let tags = Taxonomy {
kind: taxo_config,
items: vec![tag],
};
let taxonomies = vec![tags.clone()]; let taxonomies = vec![tags.clone()];
let static_fn = make_get_taxonomy_url(&taxonomies); let static_fn = make_get_taxonomy_url(&taxonomies);
@ -372,7 +337,10 @@ mod tests {
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap()); args.insert("kind".to_string(), to_value("tags").unwrap());
args.insert("name".to_string(), to_value("Programming").unwrap()); args.insert("name".to_string(), to_value("Programming").unwrap());
assert_eq!(static_fn(args).unwrap(), to_value("http://a-website.com/tags/programming/").unwrap()); assert_eq!(
static_fn(args).unwrap(),
to_value("http://a-website.com/tags/programming/").unwrap()
);
// and errors if it can't find it // and errors if it can't find it
let mut args = HashMap::new(); let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap()); args.insert("kind".to_string(), to_value("tags").unwrap());

View file

@ -3,28 +3,27 @@ extern crate lazy_static;
#[macro_use] #[macro_use]
extern crate tera; extern crate tera;
extern crate base64; extern crate base64;
extern crate pulldown_cmark;
extern crate csv; extern crate csv;
extern crate pulldown_cmark;
extern crate reqwest; extern crate reqwest;
extern crate url; extern crate url;
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
#[cfg(not(test))] #[cfg(not(test))]
extern crate serde_json; extern crate serde_json;
extern crate errors;
extern crate utils;
extern crate library;
extern crate config; extern crate config;
extern crate errors;
extern crate imageproc; extern crate imageproc;
extern crate library;
extern crate utils;
pub mod filters; pub mod filters;
pub mod global_fns; pub mod global_fns;
use tera::{Tera, Context}; use tera::{Context, Tera};
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
@ -37,14 +36,13 @@ lazy_static! {
("sitemap.xml", include_str!("builtins/sitemap.xml")), ("sitemap.xml", include_str!("builtins/sitemap.xml")),
("robots.txt", include_str!("builtins/robots.txt")), ("robots.txt", include_str!("builtins/robots.txt")),
("anchor-link.html", include_str!("builtins/anchor-link.html")), ("anchor-link.html", include_str!("builtins/anchor-link.html")),
("shortcodes/youtube.html", include_str!("builtins/shortcodes/youtube.html")), ("shortcodes/youtube.html", include_str!("builtins/shortcodes/youtube.html")),
("shortcodes/vimeo.html", include_str!("builtins/shortcodes/vimeo.html")), ("shortcodes/vimeo.html", include_str!("builtins/shortcodes/vimeo.html")),
("shortcodes/gist.html", include_str!("builtins/shortcodes/gist.html")), ("shortcodes/gist.html", include_str!("builtins/shortcodes/gist.html")),
("shortcodes/streamable.html", include_str!("builtins/shortcodes/streamable.html")), ("shortcodes/streamable.html", include_str!("builtins/shortcodes/streamable.html")),
("internal/alias.html", include_str!("builtins/internal/alias.html")), ("internal/alias.html", include_str!("builtins/internal/alias.html")),
]).unwrap(); ])
.unwrap();
tera.register_filter("markdown", filters::markdown); tera.register_filter("markdown", filters::markdown);
tera.register_filter("base64_encode", filters::base64_encode); tera.register_filter("base64_encode", filters::base64_encode);
tera.register_filter("base64_decode", filters::base64_decode); tera.register_filter("base64_decode", filters::base64_decode);
@ -52,7 +50,6 @@ lazy_static! {
}; };
} }
/// Renders the `internal/alias.html` template that will redirect /// Renders the `internal/alias.html` template that will redirect
/// via refresh to the url given /// via refresh to the url given
pub fn render_redirect_template(url: &str, tera: &Tera) -> Result<String> { pub fn render_redirect_template(url: &str, tera: &Tera) -> Result<String> {

View file

@ -1,20 +1,22 @@
use std::fs::{copy, create_dir_all, read_dir, File};
use std::io::prelude::*; use std::io::prelude::*;
use std::fs::{File, create_dir_all, read_dir, copy};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::SystemTime; use std::time::SystemTime;
use walkdir::WalkDir; use walkdir::WalkDir;
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
pub fn is_path_in_directory(parent: &Path, path: &Path) -> Result<bool> { pub fn is_path_in_directory(parent: &Path, path: &Path) -> Result<bool> {
let canonical_path = path.canonicalize().map_err(|e| format!("Failed to canonicalize {}: {}", path.display(), e))?; let canonical_path = path
let canonical_parent = parent.canonicalize().map_err(|e| format!("Failed to canonicalize {}: {}", parent.display(), e))?; .canonicalize()
.map_err(|e| format!("Failed to canonicalize {}: {}", path.display(), e))?;
let canonical_parent = parent
.canonicalize()
.map_err(|e| format!("Failed to canonicalize {}: {}", parent.display(), e))?;
Ok(canonical_path.starts_with(canonical_parent)) Ok(canonical_path.starts_with(canonical_parent))
} }
/// Create a file with the content given /// Create a file with the content given
pub fn create_file(path: &Path, content: &str) -> Result<()> { pub fn create_file(path: &Path, content: &str) -> Result<()> {
let mut file = File::create(&path)?; let mut file = File::create(&path)?;
@ -119,7 +121,11 @@ pub fn get_file_time(path: &Path) -> Option<SystemTime> {
/// Compares source and target files' timestamps and returns true if the source file /// Compares source and target files' timestamps and returns true if the source file
/// has been created _or_ updated after the target file has /// has been created _or_ updated after the target file has
pub fn file_stale<PS, PT>(p_source: PS, p_target: PT) -> bool where PS: AsRef<Path>, PT: AsRef<Path> { pub fn file_stale<PS, PT>(p_source: PS, p_target: PT) -> bool
where
PS: AsRef<Path>,
PT: AsRef<Path>,
{
let p_source = p_source.as_ref(); let p_source = p_source.as_ref();
let p_target = p_target.as_ref(); let p_target = p_target.as_ref();
@ -133,7 +139,6 @@ pub fn file_stale<PS, PT>(p_source: PS, p_target: PT) -> bool where PS: AsRef<Pa
time_source.and_then(|ts| time_target.map(|tt| ts > tt)).unwrap_or(true) time_source.and_then(|ts| time_target.map(|tt| ts > tt)).unwrap_or(true)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fs::File; use std::fs::File;

View file

@ -4,10 +4,10 @@ extern crate errors;
#[cfg(test)] #[cfg(test)]
extern crate tempfile; extern crate tempfile;
extern crate tera; extern crate tera;
extern crate walkdir;
extern crate unicode_segmentation; extern crate unicode_segmentation;
extern crate walkdir;
pub mod fs; pub mod fs;
pub mod net;
pub mod site; pub mod site;
pub mod templates; pub mod templates;
pub mod net;

View file

@ -1,9 +1,7 @@
use std::net::TcpListener; use std::net::TcpListener;
pub fn get_available_port(avoid: u16) -> Option<u16> { pub fn get_available_port(avoid: u16) -> Option<u16> {
(1000..9000) (1000..9000).find(|port| *port != avoid && port_is_available(*port))
.find(|port| *port != avoid && port_is_available(*port))
} }
pub fn port_is_available(port: u16) -> bool { pub fn port_is_available(port: u16) -> bool {

View file

@ -31,12 +31,11 @@ pub fn resolve_internal_link(link: &str, permalinks: &HashMap<String, String>) -
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use super::{resolve_internal_link, get_reading_analytics}; use super::{get_reading_analytics, resolve_internal_link};
#[test] #[test]
fn can_resolve_valid_internal_link() { fn can_resolve_valid_internal_link() {

View file

@ -1,56 +1,55 @@
use std::collections::HashMap; use std::collections::HashMap;
use tera::{Tera, Context}; use tera::{Context, Tera};
use errors::Result; use errors::Result;
static DEFAULT_TPL: &str = include_str!("default_tpl.html"); static DEFAULT_TPL: &str = include_str!("default_tpl.html");
macro_rules! render_default_tpl { macro_rules! render_default_tpl {
($filename: expr, $url: expr) => { ($filename: expr, $url: expr) => {{
{ let mut context = Context::new();
let mut context = Context::new(); context.insert("filename", $filename);
context.insert("filename", $filename); context.insert("url", $url);
context.insert("url", $url); Tera::one_off(DEFAULT_TPL, &context, true).map_err(|e| e.into())
Tera::one_off(DEFAULT_TPL, &context, true).map_err(|e| e.into()) }};
}
};
} }
/// Renders the given template with the given context, but also ensures that, if the default file /// Renders the given template with the given context, but also ensures that, if the default file
/// is not found, it will look up for the equivalent template for the current theme if there is one. /// is not found, it will look up for the equivalent template for the current theme if there is one.
/// Lastly, if it's a default template (index, section or page), it will just return an empty string /// Lastly, if it's a default template (index, section or page), it will just return an empty string
/// to avoid an error if there isn't a template with that name /// to avoid an error if there isn't a template with that name
pub fn render_template(name: &str, tera: &Tera, context: &Context, theme: &Option<String>) -> Result<String> { pub fn render_template(
name: &str,
tera: &Tera,
context: &Context,
theme: &Option<String>,
) -> Result<String> {
if tera.templates.contains_key(name) { if tera.templates.contains_key(name) {
return tera return tera.render(name, context).map_err(|e| e.into());
.render(name, context)
.map_err(|e| e.into());
} }
if let Some(ref t) = *theme { if let Some(ref t) = *theme {
return tera return tera.render(&format!("{}/templates/{}", t, name), context).map_err(|e| e.into());
.render(&format!("{}/templates/{}", t, name), context)
.map_err(|e| e.into());
} }
// maybe it's a default one? // maybe it's a default one?
match name { match name {
"index.html" | "section.html" => { "index.html" | "section.html" => render_default_tpl!(
render_default_tpl!(name, "https://www.getzola.org/documentation/templates/pages-sections/#section-variables") name,
} "https://www.getzola.org/documentation/templates/pages-sections/#section-variables"
"page.html" => { ),
render_default_tpl!(name, "https://www.getzola.org/documentation/templates/pages-sections/#page-variables") "page.html" => render_default_tpl!(
} name,
"https://www.getzola.org/documentation/templates/pages-sections/#page-variables"
),
"single.html" | "list.html" => { "single.html" | "list.html" => {
render_default_tpl!(name, "https://www.getzola.org/documentation/templates/taxonomies/") render_default_tpl!(name, "https://www.getzola.org/documentation/templates/taxonomies/")
} }
_ => bail!("Tried to render `{}` but the template wasn't found", name) _ => bail!("Tried to render `{}` but the template wasn't found", name),
} }
} }
/// Rewrites the path from extend/macros of the theme used to ensure /// Rewrites the path from extend/macros of the theme used to ensure
/// that they will point to the right place (theme/templates/...) /// 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 /// Include is NOT supported as it would be a pain to add and using blocks
@ -63,7 +62,7 @@ pub fn rewrite_theme_paths(tera: &mut Tera, theme: &str) {
let old_templates = ::std::mem::replace(&mut tera.templates, HashMap::new()); let old_templates = ::std::mem::replace(&mut tera.templates, HashMap::new());
// We want to match the paths in the templates to the new names // We want to match the paths in the templates to the new names
for (key, mut tpl) in old_templates{ for (key, mut tpl) in old_templates {
tpl.name = format!("{}/templates/{}", theme, tpl.name); tpl.name = format!("{}/templates/{}", theme, tpl.name);
// First the parent if there is none // First the parent if there is none
if let Some(ref p) = tpl.parent.clone() { if let Some(ref p) = tpl.parent.clone() {
@ -97,8 +96,8 @@ pub fn rewrite_theme_paths(tera: &mut Tera, theme: &str) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use tera::Tera;
use super::rewrite_theme_paths; use super::rewrite_theme_paths;
use tera::Tera;
#[test] #[test]
fn can_rewrite_all_paths_of_theme() { fn can_rewrite_all_paths_of_theme() {

1
rustfmt.toml Normal file
View file

@ -0,0 +1 @@
use_small_heuristics = "max"

View file

@ -1,12 +1,11 @@
use std::fs::{create_dir, canonicalize}; use std::fs::{canonicalize, create_dir};
use std::path::Path; use std::path::Path;
use errors::Result; use errors::Result;
use utils::fs::create_file; use utils::fs::create_file;
use prompt::{ask_bool, ask_url};
use console; use console;
use prompt::{ask_bool, ask_url};
const CONFIG: &str = r#" const CONFIG: &str = r#"
# The URL the site will be built for # The URL the site will be built for
@ -26,7 +25,6 @@ build_search_index = %SEARCH%
# Put all your custom variables here # Put all your custom variables here
"#; "#;
pub fn create_new_project(name: &str) -> Result<()> { pub fn create_new_project(name: &str) -> Result<()> {
let path = Path::new(name); let path = Path::new(name);
// Better error message than the rust default // Better error message than the rust default
@ -62,7 +60,9 @@ pub fn create_new_project(name: &str) -> Result<()> {
println!(); println!();
console::success(&format!("Done! Your site was created in {:?}", canonicalize(path).unwrap())); console::success(&format!("Done! Your site was created in {:?}", canonicalize(path).unwrap()));
println!(); println!();
console::info("Get started by moving into the directory and using the built-in server: `zola serve`"); console::info(
"Get started by moving into the directory and using the built-in server: `zola serve`",
);
println!("Visit https://www.getzola.org for the full documentation."); println!("Visit https://www.getzola.org for the full documentation.");
Ok(()) Ok(())
} }

View file

@ -1,7 +1,7 @@
mod init;
mod build; mod build;
mod init;
mod serve; mod serve;
pub use self::init::create_new_project;
pub use self::build::build; pub use self::build::build;
pub use self::init::create_new_project;
pub use self::serve::serve; pub use self::serve::serve;

View file

@ -26,18 +26,18 @@ use std::fs::{remove_dir_all, File};
use std::io::{self, Read}; use std::io::{self, Read};
use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use std::path::{Path, PathBuf, MAIN_SEPARATOR};
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use std::time::{Instant, Duration};
use std::thread; use std::thread;
use std::time::{Duration, Instant};
use chrono::prelude::*; use actix_web::middleware::{Middleware, Response, Started};
use actix_web::{self, fs, http, server, App, HttpRequest, HttpResponse, Responder}; use actix_web::{self, fs, http, server, App, HttpRequest, HttpResponse, Responder};
use actix_web::middleware::{Middleware, Started, Response}; use chrono::prelude::*;
use notify::{Watcher, RecursiveMode, watcher};
use ws::{WebSocket, Sender, Message};
use ctrlc; use ctrlc;
use notify::{watcher, RecursiveMode, Watcher};
use ws::{Message, Sender, WebSocket};
use site::Site;
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use site::Site;
use utils::fs::copy_file; use utils::fs::copy_file;
use console; use console;
@ -93,7 +93,9 @@ fn livereload_handler(_: &HttpRequest) -> &'static str {
fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &str) { fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &str) {
match res { match res {
Ok(_) => { Ok(_) => {
broadcaster.send(format!(r#" broadcaster
.send(format!(
r#"
{{ {{
"command": "reload", "command": "reload",
"path": "{}", "path": "{}",
@ -101,14 +103,22 @@ fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &st
"liveCSS": true, "liveCSS": true,
"liveImg": true, "liveImg": true,
"protocol": ["http://livereload.com/protocols/official-7"] "protocol": ["http://livereload.com/protocols/official-7"]
}}"#, reload_path) }}"#,
).unwrap(); reload_path
}, ))
Err(e) => console::unravel_errors("Failed to build the site", &e) .unwrap();
}
Err(e) => console::unravel_errors("Failed to build the site", &e),
} }
} }
fn create_new_site(interface: &str, port: u16, output_dir: &str, base_url: &str, config_file: &str) -> Result<(Site, String)> { fn create_new_site(
interface: &str,
port: u16,
output_dir: &str,
base_url: &str,
config_file: &str,
) -> Result<(Site, String)> {
let mut site = Site::new(env::current_dir().unwrap(), config_file)?; let mut site = Site::new(env::current_dir().unwrap(), config_file)?;
let base_address = format!("{}:{}", base_url, port); let base_address = format!("{}:{}", base_url, port);
@ -140,14 +150,23 @@ fn create_new_site(interface: &str, port: u16, output_dir: &str, base_url: &str,
/// Rather than deal with all of that, we can hijack a hook for presenting a /// Rather than deal with all of that, we can hijack a hook for presenting a
/// custom directory listing response and serve it up using their /// custom directory listing response and serve it up using their
/// `NamedFile` responder. /// `NamedFile` responder.
fn handle_directory<'a, 'b>(dir: &'a fs::Directory, req: &'b HttpRequest) -> io::Result<HttpResponse> { fn handle_directory<'a, 'b>(
dir: &'a fs::Directory,
req: &'b HttpRequest,
) -> io::Result<HttpResponse> {
let mut path = PathBuf::from(&dir.base); let mut path = PathBuf::from(&dir.base);
path.push(&dir.path); path.push(&dir.path);
path.push("index.html"); path.push("index.html");
fs::NamedFile::open(path)?.respond_to(req) fs::NamedFile::open(path)?.respond_to(req)
} }
pub fn serve(interface: &str, port: u16, output_dir: &str, base_url: &str, config_file: &str) -> Result<()> { pub fn serve(
interface: &str,
port: u16,
output_dir: &str,
base_url: &str,
config_file: &str,
) -> Result<()> {
let start = Instant::now(); let start = Instant::now();
let (mut site, address) = create_new_site(interface, port, output_dir, base_url, config_file)?; let (mut site, address) = create_new_site(interface, port, output_dir, base_url, config_file)?;
console::report_elapsed_time(start); console::report_elapsed_time(start);
@ -157,20 +176,24 @@ pub fn serve(interface: &str, port: u16, output_dir: &str, base_url: &str, confi
let mut watching_templates = false; let mut watching_templates = false;
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap(); let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap();
watcher.watch("content/", RecursiveMode::Recursive) watcher
.watch("content/", RecursiveMode::Recursive)
.chain_err(|| "Can't watch the `content` folder. Does it exist?")?; .chain_err(|| "Can't watch the `content` folder. Does it exist?")?;
watcher.watch(config_file, RecursiveMode::Recursive) watcher
.watch(config_file, RecursiveMode::Recursive)
.chain_err(|| "Can't watch the `config` file. Does it exist?")?; .chain_err(|| "Can't watch the `config` file. Does it exist?")?;
if Path::new("static").exists() { if Path::new("static").exists() {
watching_static = true; watching_static = true;
watcher.watch("static/", RecursiveMode::Recursive) watcher
.watch("static/", RecursiveMode::Recursive)
.chain_err(|| "Can't watch the `static` folder.")?; .chain_err(|| "Can't watch the `static` folder.")?;
} }
if Path::new("templates").exists() { if Path::new("templates").exists() {
watching_templates = true; watching_templates = true;
watcher.watch("templates/", RecursiveMode::Recursive) watcher
.watch("templates/", RecursiveMode::Recursive)
.chain_err(|| "Can't watch the `templates` folder.")?; .chain_err(|| "Can't watch the `templates` folder.")?;
} }
@ -186,16 +209,16 @@ pub fn serve(interface: &str, port: u16, output_dir: &str, base_url: &str, confi
thread::spawn(move || { thread::spawn(move || {
let s = server::new(move || { let s = server::new(move || {
App::new() App::new()
.middleware(NotFoundHandler { rendered_template: static_root.join("404.html") }) .middleware(NotFoundHandler { rendered_template: static_root.join("404.html") })
.resource(r"/livereload.js", |r| r.f(livereload_handler)) .resource(r"/livereload.js", |r| r.f(livereload_handler))
// Start a webserver that serves the `output_dir` directory // Start a webserver that serves the `output_dir` directory
.handler( .handler(
r"/", r"/",
fs::StaticFiles::new(&static_root) fs::StaticFiles::new(&static_root)
.unwrap() .unwrap()
.show_files_listing() .show_files_listing()
.files_listing_renderer(handle_directory) .files_listing_renderer(handle_directory),
) )
}) })
.bind(&address) .bind(&address)
.expect("Can't start the webserver") .expect("Can't start the webserver")
@ -208,17 +231,20 @@ pub fn serve(interface: &str, port: u16, output_dir: &str, base_url: &str, confi
let ws_server = WebSocket::new(|output: Sender| { let ws_server = WebSocket::new(|output: Sender| {
move |msg: Message| { move |msg: Message| {
if msg.into_text().unwrap().contains("\"hello\"") { if msg.into_text().unwrap().contains("\"hello\"") {
return output.send(Message::text(r#" return output.send(Message::text(
r#"
{ {
"command": "hello", "command": "hello",
"protocols": [ "http://livereload.com/protocols/official-7" ], "protocols": [ "http://livereload.com/protocols/official-7" ],
"serverName": "Zola" "serverName": "Zola"
} }
"#)); "#,
));
} }
Ok(()) Ok(())
} }
}).unwrap(); })
.unwrap();
let broadcaster = ws_server.broadcaster(); let broadcaster = ws_server.broadcaster();
thread::spawn(move || { thread::spawn(move || {
ws_server.listen(&*ws_address).unwrap(); ws_server.listen(&*ws_address).unwrap();
@ -237,14 +263,20 @@ pub fn serve(interface: &str, port: u16, output_dir: &str, base_url: &str, confi
watchers.push("sass"); watchers.push("sass");
} }
println!("Listening for changes in {}{}{{{}}}", pwd.display(), MAIN_SEPARATOR, watchers.join(", ")); println!(
"Listening for changes in {}{}{{{}}}",
pwd.display(),
MAIN_SEPARATOR,
watchers.join(", ")
);
println!("Press Ctrl+C to stop\n"); println!("Press Ctrl+C to stop\n");
// Delete the output folder on ctrl+C // Delete the output folder on ctrl+C
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
remove_dir_all(&output_path).expect("Failed to delete output directory"); remove_dir_all(&output_path).expect("Failed to delete output directory");
::std::process::exit(0); ::std::process::exit(0);
}).expect("Error setting Ctrl-C handler"); })
.expect("Error setting Ctrl-C handler");
use notify::DebouncedEvent::*; use notify::DebouncedEvent::*;
@ -252,47 +284,74 @@ pub fn serve(interface: &str, port: u16, output_dir: &str, base_url: &str, confi
match rx.recv() { match rx.recv() {
Ok(event) => { Ok(event) => {
match event { match event {
Create(path) | Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
Write(path) |
Remove(path) |
Rename(_, path) => {
if is_temp_file(&path) || path.is_dir() { if is_temp_file(&path) || path.is_dir() {
continue; continue;
} }
println!("Change detected @ {}", Local::now().format("%Y-%m-%d %H:%M:%S").to_string()); println!(
"Change detected @ {}",
Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
);
let start = Instant::now(); let start = Instant::now();
match detect_change_kind(&pwd, &path) { match detect_change_kind(&pwd, &path) {
(ChangeKind::Content, _) => { (ChangeKind::Content, _) => {
console::info(&format!("-> Content changed {}", path.display())); console::info(&format!("-> Content changed {}", path.display()));
// Force refresh // Force refresh
rebuild_done_handling(&broadcaster, rebuild::after_content_change(&mut site, &path), "/x.js"); rebuild_done_handling(
}, &broadcaster,
rebuild::after_content_change(&mut site, &path),
"/x.js",
);
}
(ChangeKind::Templates, _) => { (ChangeKind::Templates, _) => {
console::info(&format!("-> Template changed {}", path.display())); console::info(&format!("-> Template changed {}", path.display()));
// Force refresh // Force refresh
rebuild_done_handling(&broadcaster, rebuild::after_template_change(&mut site, &path), "/x.js"); rebuild_done_handling(
}, &broadcaster,
rebuild::after_template_change(&mut site, &path),
"/x.js",
);
}
(ChangeKind::StaticFiles, p) => { (ChangeKind::StaticFiles, p) => {
if path.is_file() { if path.is_file() {
console::info(&format!("-> Static file changes detected {}", path.display())); console::info(&format!(
rebuild_done_handling(&broadcaster, copy_file(&path, &site.output_path, &site.static_path), &p.to_string_lossy()); "-> Static file changes detected {}",
path.display()
));
rebuild_done_handling(
&broadcaster,
copy_file(&path, &site.output_path, &site.static_path),
&p.to_string_lossy(),
);
} }
}, }
(ChangeKind::Sass, p) => { (ChangeKind::Sass, p) => {
console::info(&format!("-> Sass file changed {}", path.display())); console::info(&format!("-> Sass file changed {}", path.display()));
rebuild_done_handling(&broadcaster, site.compile_sass(&site.base_path), &p.to_string_lossy()); rebuild_done_handling(
}, &broadcaster,
site.compile_sass(&site.base_path),
&p.to_string_lossy(),
);
}
(ChangeKind::Config, _) => { (ChangeKind::Config, _) => {
console::info("-> Config changed. The whole site will be reloaded. The browser needs to be refreshed to make the changes visible."); console::info("-> Config changed. The whole site will be reloaded. The browser needs to be refreshed to make the changes visible.");
site = create_new_site(interface, port, output_dir, base_url, config_file).unwrap().0; site = create_new_site(
interface,
port,
output_dir,
base_url,
config_file,
)
.unwrap()
.0;
} }
}; };
console::report_elapsed_time(start); console::report_elapsed_time(start);
} }
_ => {} _ => {}
} }
}, }
Err(e) => console::error(&format!("Watch error: {:?}", e)), Err(e) => console::error(&format!("Watch error: {:?}", e)),
}; };
} }
@ -321,9 +380,7 @@ fn is_temp_file(path: &Path) -> bool {
} }
} }
}, },
None => { None => true,
true
},
} }
} }
@ -354,7 +411,7 @@ fn detect_change_kind(pwd: &Path, path: &Path) -> (ChangeKind, PathBuf) {
mod tests { mod tests {
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::{is_temp_file, detect_change_kind, ChangeKind}; use super::{detect_change_kind, is_temp_file, ChangeKind};
#[test] #[test]
fn can_recognize_temp_files() { fn can_recognize_temp_files() {
@ -380,23 +437,28 @@ mod tests {
let test_cases = vec![ let test_cases = vec![
( (
(ChangeKind::Templates, PathBuf::from("/templates/hello.html")), (ChangeKind::Templates, PathBuf::from("/templates/hello.html")),
Path::new("/home/vincent/site"), Path::new("/home/vincent/site/templates/hello.html") Path::new("/home/vincent/site"),
Path::new("/home/vincent/site/templates/hello.html"),
), ),
( (
(ChangeKind::StaticFiles, PathBuf::from("/static/site.css")), (ChangeKind::StaticFiles, PathBuf::from("/static/site.css")),
Path::new("/home/vincent/site"), Path::new("/home/vincent/site/static/site.css") Path::new("/home/vincent/site"),
Path::new("/home/vincent/site/static/site.css"),
), ),
( (
(ChangeKind::Content, PathBuf::from("/content/posts/hello.md")), (ChangeKind::Content, PathBuf::from("/content/posts/hello.md")),
Path::new("/home/vincent/site"), Path::new("/home/vincent/site/content/posts/hello.md") Path::new("/home/vincent/site"),
Path::new("/home/vincent/site/content/posts/hello.md"),
), ),
( (
(ChangeKind::Sass, PathBuf::from("/sass/print.scss")), (ChangeKind::Sass, PathBuf::from("/sass/print.scss")),
Path::new("/home/vincent/site"), Path::new("/home/vincent/site/sass/print.scss") Path::new("/home/vincent/site"),
Path::new("/home/vincent/site/sass/print.scss"),
), ),
( (
(ChangeKind::Config, PathBuf::from("/config.toml")), (ChangeKind::Config, PathBuf::from("/config.toml")),
Path::new("/home/vincent/site"), Path::new("/home/vincent/site/config.toml") Path::new("/home/vincent/site"),
Path::new("/home/vincent/site/config.toml"),
), ),
]; ];

View file

@ -21,7 +21,6 @@ lazy_static! {
}; };
} }
pub fn info(message: &str) { pub fn info(message: &str) {
colorize(message, ColorSpec::new().set_bold(true)); colorize(message, ColorSpec::new().set_bold(true));
} }
@ -58,13 +57,12 @@ pub fn notify_site_size(site: &Site) {
/// Display a warning in the console if there are ignored pages in the site /// Display a warning in the console if there are ignored pages in the site
pub fn warn_about_ignored_pages(site: &Site) { pub fn warn_about_ignored_pages(site: &Site) {
let ignored_pages: Vec<_> = site.library let ignored_pages: Vec<_> = site
.library
.sections_values() .sections_values()
.iter() .iter()
.flat_map(|s| { .flat_map(|s| {
s.ignored_pages s.ignored_pages.iter().map(|k| site.library.get_page_by_key(*k).file.path.clone())
.iter()
.map(|k| site.library.get_page_by_key(*k).file.path.clone())
}) })
.collect(); .collect();
@ -104,8 +102,9 @@ pub fn unravel_errors(message: &str, error: &Error) {
/// Check whether to output colors /// Check whether to output colors
fn has_color() -> bool { fn has_color() -> bool {
let use_colors = env::var("CLICOLOR").unwrap_or_else(|_| "1".to_string()) != "0" && env::var("NO_COLOR").is_err(); let use_colors = env::var("CLICOLOR").unwrap_or_else(|_| "1".to_string()) != "0"
let force_colors = env::var("CLICOLOR_FORCE").unwrap_or_else(|_|"0".to_string()) != "0"; && env::var("NO_COLOR").is_err();
let force_colors = env::var("CLICOLOR_FORCE").unwrap_or_else(|_| "0".to_string()) != "0";
force_colors || use_colors && atty::is(atty::Stream::Stdout) force_colors || use_colors && atty::is(atty::Stream::Stdout)
} }

View file

@ -1,33 +1,32 @@
extern crate atty;
extern crate actix_web; extern crate actix_web;
extern crate atty;
#[macro_use] #[macro_use]
extern crate clap; extern crate clap;
extern crate chrono; extern crate chrono;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
extern crate ctrlc;
extern crate notify; extern crate notify;
extern crate termcolor; extern crate termcolor;
extern crate url; extern crate url;
extern crate ws; extern crate ws;
extern crate ctrlc;
extern crate site; extern crate site;
#[macro_use] #[macro_use]
extern crate errors; extern crate errors;
extern crate front_matter; extern crate front_matter;
extern crate utils;
extern crate rebuild; extern crate rebuild;
extern crate utils;
use std::time::Instant; use std::time::Instant;
use utils::net::{get_available_port, port_is_available}; use utils::net::{get_available_port, port_is_available};
mod cli;
mod cmd; mod cmd;
mod console; mod console;
mod cli;
mod prompt; mod prompt;
fn main() { fn main() {
let matches = cli::build_cli().get_matches(); let matches = cli::build_cli().get_matches();
@ -40,9 +39,9 @@ fn main() {
Err(e) => { Err(e) => {
console::unravel_errors("Failed to create the project", &e); console::unravel_errors("Failed to create the project", &e);
::std::process::exit(1); ::std::process::exit(1);
}, }
}; };
}, }
("build", Some(matches)) => { ("build", Some(matches)) => {
console::info("Building site..."); console::info("Building site...");
let start = Instant::now(); let start = Instant::now();
@ -52,9 +51,9 @@ fn main() {
Err(e) => { Err(e) => {
console::unravel_errors("Failed to build the site", &e); console::unravel_errors("Failed to build the site", &e);
::std::process::exit(1); ::std::process::exit(1);
}, }
}; };
}, }
("serve", Some(matches)) => { ("serve", Some(matches)) => {
let interface = matches.value_of("interface").unwrap_or("127.0.0.1"); let interface = matches.value_of("interface").unwrap_or("127.0.0.1");
let mut port: u16 = match matches.value_of("port").unwrap().parse() { let mut port: u16 = match matches.value_of("port").unwrap().parse() {
@ -87,9 +86,9 @@ fn main() {
Err(e) => { Err(e) => {
console::unravel_errors("", &e); console::unravel_errors("", &e);
::std::process::exit(1); ::std::process::exit(1);
}, }
}; };
}, }
_ => unreachable!(), _ => unreachable!(),
} }
} }

View file

@ -1,4 +1,4 @@
use std::io::{self, Write, BufRead}; use std::io::{self, BufRead, Write};
use url::Url; use url::Url;
@ -28,7 +28,7 @@ pub fn ask_bool(question: &str, default: bool) -> Result<bool> {
_ => { _ => {
println!("Invalid choice: '{}'", input); println!("Invalid choice: '{}'", input);
ask_bool(question, default) ask_bool(question, default)
}, }
} }
} }
@ -40,13 +40,11 @@ pub fn ask_url(question: &str, default: &str) -> Result<String> {
match &*input { match &*input {
"" => Ok(default.to_string()), "" => Ok(default.to_string()),
_ => { _ => match Url::parse(&input) {
match Url::parse(&input) { Ok(_) => Ok(input),
Ok(_) => Ok(input), Err(_) => {
Err(_) => { println!("Invalid URL: '{}'", input);
println!("Invalid URL: '{}'", input); ask_url(question, default)
ask_url(question, default)
}
} }
}, },
} }