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() {
// disabled below as it fails in CI
// let mut app = build_cli();
// app.gen_completions("zola", Shell::Bash, "completions/");
// app.gen_completions("zola", Shell::Fish, "completions/");
// app.gen_completions("zola", Shell::Zsh, "completions/");
// app.gen_completions("zola", Shell::PowerShell, "completions/");
// let mut app = build_cli();
// app.gen_completions("zola", Shell::Bash, "completions/");
// app.gen_completions("zola", Shell::Fish, "completions/");
// app.gen_completions("zola", Shell::Zsh, "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
//! to do this yourself unless you want to cache your own compiled grammars.
extern crate syntect;
use syntect::parsing::SyntaxSetBuilder;
use syntect::highlighting::ThemeSet;
use syntect::dumps::*;
use std::env;
use syntect::dumps::*;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSetBuilder;
fn usage_and_exit() -> ! {
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);
}
}
},
}
(Some(ref cmd), Some(ref theme_dir), Some(ref packpath)) if cmd == "themepack" => {
let ts = ThemeSet::load_from_folder(theme_dir).unwrap();
for path in ts.themes.keys() {

View file

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

View file

@ -1,18 +1,18 @@
use syntect::dumps::from_binary;
use syntect::parsing::SyntaxSet;
use syntect::highlighting::ThemeSet;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet;
use Config;
lazy_static! {
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
};
pub static ref THEME_SET: ThemeSet = from_binary(include_bytes!("../../../sublime_themes/all.themedump"));
pub static ref THEME_SET: ThemeSet =
from_binary(include_bytes!("../../../sublime_themes/all.themedump"));
}
/// 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;
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(|| {
if let Some(ref extra) = config.extra_syntax_set {
let s = extra.find_syntax_by_token(lang);

View file

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

View file

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

View file

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

View file

@ -2,18 +2,18 @@
extern crate lazy_static;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate toml;
extern crate regex;
extern crate tera;
extern crate chrono;
extern crate regex;
extern crate serde;
extern crate tera;
extern crate toml;
#[macro_use]
extern crate errors;
use std::path::Path;
use regex::Regex;
use errors::{Result, ResultExt};
use regex::Regex;
use std::path::Path;
mod page;
mod section;
@ -22,7 +22,8 @@ pub use page::PageFrontMatter;
pub use section::SectionFrontMatter;
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)]
@ -44,12 +45,14 @@ pub enum InsertAnchor {
None,
}
/// Split a file between the front matter and its content
/// Will return an error if the front matter wasn't found
fn split_content(file_path: &Path, content: &str) -> Result<(String, String)> {
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
@ -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.
/// 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 meta = SectionFrontMatter::parse(&front_matter)
.chain_err(|| format!("Error when parsing front matter of section `{}`", file_path.to_string_lossy()))?;
let meta = SectionFrontMatter::parse(&front_matter).chain_err(|| {
format!("Error when parsing front matter of section `{}`", file_path.to_string_lossy())
})?;
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
pub fn split_page_content(file_path: &Path, content: &str) -> Result<(PageFrontMatter, String)> {
let (front_matter, content) = split_content(file_path, content)?;
let meta = PageFrontMatter::parse(&front_matter)
.chain_err(|| format!("Error when parsing front matter of page `{}`", file_path.to_string_lossy()))?;
let meta = PageFrontMatter::parse(&front_matter).chain_err(|| {
format!("Error when parsing front matter of page `{}`", file_path.to_string_lossy())
})?;
Ok((meta, content))
}
@ -82,7 +90,7 @@ pub fn split_page_content(file_path: &Path, content: &str) -> Result<(PageFrontM
mod tests {
use std::path::Path;
use super::{split_section_content, split_page_content};
use super::{split_page_content, split_section_content};
#[test]
fn can_split_page_content_valid() {

View file

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

View file

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

View file

@ -1,32 +1,32 @@
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate image;
extern crate rayon;
extern crate regex;
extern crate utils;
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::Entry as HEntry;
use std::collections::HashMap;
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::{FilterType, GenericImageView};
use rayon::prelude::*;
use regex::Regex;
use utils::fs as ufs;
use errors::{Result, ResultExt};
use utils::fs as ufs;
static RESIZED_SUBDIR: &'static str = "processed_images";
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
@ -57,16 +57,22 @@ impl ResizeOp {
// Validate args:
match op {
"fit_width" => 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());
},
"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())
"fit_width" => {
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());
}
}
"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 {
@ -121,8 +127,12 @@ impl From<ResizeOp> for u8 {
impl Hash for ResizeOp {
fn hash<H: Hasher>(&self, hasher: &mut H) {
hasher.write_u8(u8::from(*self));
if let Some(w) = self.width() { hasher.write_u32(w); }
if let Some(h) = self.height() { hasher.write_u32(h); }
if let Some(w) = self.width() {
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.crop(offset_w, offset_h, crop_w, crop_h)
.resize_exact(w, h, RESIZE_FILTER)
img.crop(offset_w, offset_h, crop_w, crop_h).resize_exact(w, h, RESIZE_FILTER)
}
}
};
@ -221,7 +230,6 @@ impl ImageOp {
}
}
/// A strcture into which image operations can be enqueued and then performed.
/// All output is written in a subdirectory in `static_path`,
/// 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 {
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) => {
entry.insert(img_op);
return 0;
@ -341,9 +353,8 @@ impl Processor {
let filename = entry_path.file_name().unwrap().to_string_lossy();
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 collision_id = u32::from_str_radix(
capts.get(2).unwrap().as_str(), 16,
).unwrap();
let collision_id =
u32::from_str_radix(capts.get(2).unwrap().as_str(), 16).unwrap();
if collision_id > 0 || !self.img_ops.contains_key(&hash) {
fs::remove_file(&entry_path)?;
@ -359,24 +370,28 @@ impl Processor {
ufs::ensure_directory_exists(&self.resized_path)?;
}
self.img_ops.par_iter().map(|(hash, op)| {
let target = self.resized_path.join(Self::op_filename(*hash, op.collision_id));
op.perform(&self.content_path, &target)
.chain_err(|| format!("Failed to process image: {}", op.source))
}).collect::<Result<()>>()
self.img_ops
.par_iter()
.map(|(hash, op)| {
let target = self.resized_path.join(Self::op_filename(*hash, op.collision_id));
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
pub fn file_is_img<P: AsRef<Path>>(p: P) -> bool {
p.as_ref().extension().and_then(|s| s.to_str()).map(|ext| {
match ext.to_lowercase().as_str() {
p.as_ref()
.extension()
.and_then(|s| s.to_str())
.map(|ext| match ext.to_lowercase().as_str() {
"jpg" | "jpeg" => true,
"png" => true,
"gif" => true,
"bmp" => true,
_ => false,
}
}).unwrap_or(false)
})
.unwrap_or(false)
}

View file

@ -114,7 +114,8 @@ mod tests {
#[test]
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()]);
}
}

View file

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

View file

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

View file

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

View file

@ -1,39 +1,39 @@
extern crate tera;
extern crate slug;
extern crate serde;
extern crate slug;
extern crate tera;
#[macro_use]
extern crate serde_derive;
extern crate chrono;
extern crate slotmap;
extern crate rayon;
extern crate slotmap;
#[macro_use]
extern crate lazy_static;
extern crate regex;
#[cfg(test)]
extern crate globset;
#[cfg(test)]
extern crate tempfile;
#[cfg(test)]
extern crate toml;
#[cfg(test)]
extern crate globset;
extern crate front_matter;
extern crate config;
extern crate utils;
extern crate front_matter;
extern crate rendering;
extern crate utils;
#[macro_use]
extern crate errors;
mod content;
mod taxonomies;
mod library;
mod pagination;
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, SerializingPage, Section, SerializingSection};
pub use content::{Page, Section, SerializingPage, SerializingSection};
pub use library::Library;
pub use taxonomies::{Taxonomy, TaxonomyItem, find_taxonomies};
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 sorting::{find_siblings, sort_pages_by_weight, sort_pages_by_date};
use content::{Page, Section};
use sorting::{find_siblings, sort_pages_by_date, sort_pages_by_weight};
/// Houses everything about pages and sections
/// 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
/// as well as the pages for each section
pub fn populate_sections(&mut self) {
let (root_path, index_path) = self.sections
let (root_path, index_path) = self
.sections
.values()
.find(|s| s.is_index())
.map(|s| (s.file.parent.clone(), s.file.path.clone()))
.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
let mut ancestors: HashMap<PathBuf, Vec<_>> = HashMap::new();
@ -130,7 +130,8 @@ impl Library {
let parent_section_path = page.file.parent.join("_index.md");
if let Some(section_key) = self.paths_to_sections.get(&parent_section_path) {
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
page.ancestors.push(*section_key);
}
@ -150,7 +151,8 @@ impl Library {
children.sort_by(|a, b| sections_weight[a].cmp(&sections_weight[b]));
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 {
SortBy::None => continue,
SortBy::Date => {
let data = section.pages
let data = section
.pages
.iter()
.map(|k| {
if let Some(page) = self.pages.get(*k) {
@ -173,9 +176,10 @@ impl Library {
.collect();
sort_pages_by_date(data)
},
}
SortBy::Weight => {
let data = section.pages
let data = section
.pages
.iter()
.map(|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 {
// Find sibling between sorted pages first
let with_siblings = find_siblings(sorted.iter().map(|k| {
if let Some(page) = self.pages.get(*k) {
(k, page.is_draft())
} else {
unreachable!("Sorting got an unknown page")
}
}).collect());
let with_siblings = find_siblings(
sorted
.iter()
.map(|k| {
if let Some(page) = self.pages.get(*k) {
(k, page.is_draft())
} else {
unreachable!("Sorting got an unknown page")
}
})
.collect(),
);
for (k2, val1, val2) in with_siblings {
if let Some(page) = self.pages.get_mut(k2) {
@ -208,12 +217,12 @@ impl Library {
SortBy::Date => {
page.earlier = val2;
page.later = val1;
},
}
SortBy::Weight => {
page.lighter = val1;
page.heavier = val2;
},
SortBy::None => unreachable!("Impossible to find siblings in SortBy::None")
}
SortBy::None => unreachable!("Impossible to find siblings in SortBy::None"),
}
} else {
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`
pub fn get_all_orphan_pages(&self) -> Vec<&Page> {
let pages_in_sections = self.sections
.values()
.flat_map(|s| &s.pages)
.collect::<HashSet<_>>();
let pages_in_sections =
self.sections.values().flat_map(|s| &s.pages).collect::<HashSet<_>>();
self.pages
.iter()
@ -245,7 +252,7 @@ impl Library {
let page_key = self.paths_to_pages[path];
for s in self.sections.values() {
if s.pages.contains(&page_key) {
return Some(s)
return Some(s);
}
}

View file

@ -1,16 +1,15 @@
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 errors::{Result, ResultExt};
use utils::templates::render_template;
use content::{Section, SerializingSection, SerializingPage};
use taxonomies::{TaxonomyItem, Taxonomy};
use content::{Section, SerializingPage, SerializingSection};
use library::Library;
use taxonomies::{Taxonomy, TaxonomyItem};
#[derive(Clone, Debug, PartialEq)]
enum PaginationRoot<'a> {
@ -18,7 +17,6 @@ enum PaginationRoot<'a> {
Taxonomy(&'a Taxonomy),
}
/// A list of all the pages in the paginator with their index and links
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Pager<'a> {
@ -33,13 +31,13 @@ pub struct Pager<'a> {
}
impl<'a> Pager<'a> {
fn new(index: usize, pages: Vec<SerializingPage<'a>>, permalink: String, path: String) -> Pager<'a> {
Pager {
index,
permalink,
path,
pages,
}
fn new(
index: usize,
pages: Vec<SerializingPage<'a>>,
permalink: String,
path: String,
) -> Pager<'a> {
Pager { index, permalink, path, pages }
}
}
@ -83,7 +81,11 @@ impl<'a> Paginator<'a> {
/// 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
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 mut paginator = Paginator {
all_pages: &item.pages,
@ -92,7 +94,11 @@ impl<'a> Paginator<'a> {
root: PaginationRoot::Taxonomy(taxonomy),
permalink: item.permalink.clone(),
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,
};
@ -142,12 +148,7 @@ impl<'a> Paginator<'a> {
format!("{}/{}", self.path, page_path)
};
pagers.push(Pager::new(
index + 1,
page,
permalink,
pager_path,
));
pagers.push(Pager::new(index + 1, page, permalink, pager_path));
}
// We always have the index one at least
@ -184,19 +185,29 @@ impl<'a> Paginator<'a> {
paginator.insert("next", Value::Null);
}
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("current_index", to_value(current_pager.index).unwrap());
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();
context.insert("config", &config);
let template_name = match self.root {
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()
}
PaginationRoot::Taxonomy(t) => {
@ -217,11 +228,11 @@ impl<'a> Paginator<'a> {
mod tests {
use tera::to_value;
use front_matter::SectionFrontMatter;
use content::{Page, Section};
use config::Taxonomy as TaxonomyConfig;
use taxonomies::{Taxonomy, TaxonomyItem};
use content::{Page, Section};
use front_matter::SectionFrontMatter;
use library::Library;
use taxonomies::{Taxonomy, TaxonomyItem};
use super::Paginator;

View file

@ -1,8 +1,8 @@
use std::cmp::Ordering;
use chrono::NaiveDateTime;
use rayon::prelude::*;
use slotmap::Key;
use chrono::NaiveDateTime;
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
/// The permalink is used to break ties
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
.into_par_iter()
.partition(|page| page.1.is_some());
let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) =
pages.into_par_iter().partition(|page| page.1.is_some());
can_be_sorted
.par_sort_unstable_by(|a, b| {
let ord = b.1.unwrap().cmp(&a.1.unwrap());
if ord == Ordering::Equal {
a.2.cmp(&b.2)
} else {
ord
}
});
can_be_sorted.par_sort_unstable_by(|a, b| {
let ord = b.1.unwrap().cmp(&a.1.unwrap());
if ord == Ordering::Equal {
a.2.cmp(&b.2)
} else {
ord
}
});
(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
/// The permalink is used to break ties
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
.into_par_iter()
.partition(|page| page.1.is_some());
let (mut can_be_sorted, cannot_be_sorted): (Vec<_>, Vec<_>) =
pages.into_par_iter().partition(|page| page.1.is_some());
can_be_sorted
.par_sort_unstable_by(|a, b| {
let ord = a.1.unwrap().cmp(&b.1.unwrap());
if ord == Ordering::Equal {
a.2.cmp(&b.2)
} else {
ord
}
});
can_be_sorted.par_sort_unstable_by(|a, b| {
let ord = a.1.unwrap().cmp(&b.1.unwrap());
if ord == Ordering::Equal {
a.2.cmp(&b.2)
} else {
ord
}
});
(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 {
use slotmap::DenseSlotMap;
use front_matter::{PageFrontMatter};
use super::{find_siblings, sort_pages_by_date, sort_pages_by_weight};
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 {
let mut front_matter = PageFrontMatter::default();
@ -179,7 +175,6 @@ mod tests {
assert_eq!(pages[2], key2);
}
#[test]
fn ignore_page_with_missing_field() {
let mut dense = DenseSlotMap::new();
@ -196,7 +191,7 @@ mod tests {
(&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!(unsorted.len(), 1);
}
@ -211,11 +206,8 @@ mod tests {
let page3 = create_page_with_weight(3);
let key3 = dense.insert(page3.clone());
let input = vec![
(&key1, page1.is_draft()),
(&key2, page2.is_draft()),
(&key3, page3.is_draft()),
];
let input =
vec![(&key1, page1.is_draft()), (&key2, page2.is_draft()), (&key3, page3.is_draft())];
let pages = find_siblings(input);

View file

@ -1,16 +1,16 @@
use std::collections::HashMap;
use slotmap::Key;
use slug::slugify;
use tera::{Context, Tera};
use slotmap::{Key};
use config::{Config, Taxonomy as TaxonomyConfig};
use errors::{Result, ResultExt};
use utils::templates::render_template;
use content::SerializingPage;
use sorting::sort_pages_by_date;
use library::Library;
use sorting::sort_pages_by_date;
#[derive(Debug, Clone, PartialEq, Serialize)]
struct SerializedTaxonomyItem<'a> {
@ -34,7 +34,6 @@ impl<'a> SerializedTaxonomyItem<'a> {
slug: &item.slug,
permalink: &item.permalink,
pages,
}
}
}
@ -70,12 +69,7 @@ impl TaxonomyItem {
// We still append pages without dates at the end
pages.extend(ignored_pages);
TaxonomyItem {
name: name.to_string(),
permalink,
slug,
pages,
}
TaxonomyItem { name: name.to_string(), permalink, slug, pages }
}
}
@ -87,11 +81,9 @@ pub struct SerializedTaxonomy<'a> {
impl<'a> SerializedTaxonomy<'a> {
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();
SerializedTaxonomy {
kind: &taxonomy.kind,
items,
}
let items: Vec<SerializedTaxonomyItem> =
taxonomy.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect();
SerializedTaxonomy { kind: &taxonomy.kind, items }
}
}
@ -104,19 +96,19 @@ pub struct 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![];
for (name, pages) in items {
sorted_items.push(
TaxonomyItem::new(&name, &kind.name, config, pages, library)
);
sorted_items.push(TaxonomyItem::new(&name, &kind.name, config, pages, library));
}
sorted_items.sort_by(|a, b| a.name.cmp(&b.name));
Taxonomy {
kind,
items: sorted_items,
}
Taxonomy { kind, items: sorted_items }
}
pub fn len(&self) -> usize {
@ -127,22 +119,37 @@ impl Taxonomy {
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();
context.insert("config", config);
context.insert("term", &SerializedTaxonomyItem::from_item(item, library));
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));
render_template(&format!("{}/single.html", self.kind.name), tera, &context, &config.theme)
.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();
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("taxonomy", &self.kind);
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 {
if taxonomies_def.contains_key(name) {
all_taxonomies
.entry(name)
.or_insert_with(HashMap::new);
all_taxonomies.entry(name).or_insert_with(HashMap::new);
for v in val {
all_taxonomies.get_mut(name)
all_taxonomies
.get_mut(name)
.unwrap()
.entry(v.to_string())
.or_insert_with(|| vec![])
.push(key);
}
} 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)
}
#[cfg(test)]
mod tests {
use super::*;
@ -284,7 +293,10 @@ mod tests {
assert_eq!(categories.items[1].name, "Programming tutorials");
assert_eq!(categories.items[1].slug, "programming-tutorials");
assert_eq!(categories.items[1].permalink, "http://a-website.com/categories/programming-tutorials/");
assert_eq!(
categories.items[1].permalink,
"http://a-website.com/categories/programming-tutorials/"
);
assert_eq!(categories.items[1].pages.len(), 1);
}
@ -293,9 +305,8 @@ mod tests {
let mut config = Config::default();
let mut library = Library::new(2, 0);
config.taxonomies = vec![
TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() },
];
config.taxonomies =
vec![TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() }];
let mut page1 = Page::default();
let mut taxo_page1 = HashMap::new();
taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
@ -306,6 +317,9 @@ mod tests {
assert!(taxonomies.is_err());
let err = taxonomies.unwrap_err();
// 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;
use reqwest::header::{HeaderMap, ACCEPT};
use reqwest::{StatusCode};
use reqwest::StatusCode;
use std::collections::HashMap;
use std::error::Error;
use std::sync::{Arc, RwLock};
@ -62,14 +62,8 @@ pub fn check_url(url: &str) -> LinkResult {
// Need to actually do the link checking
let res = match client.get(url).headers(headers).send() {
Ok(response) => LinkResult {
code: Some(response.status()),
error: None,
},
Err(e) => LinkResult {
code: None,
error: Some(e.description().to_string()),
},
Ok(response) => LinkResult { code: Some(response.status()), error: None },
Err(e) => LinkResult { code: None, error: Some(e.description().to_string()) },
};
LINKS.write().unwrap().insert(url.to_string(), res.clone());

View file

@ -1,16 +1,15 @@
extern crate site;
#[macro_use]
extern crate errors;
extern crate library;
extern crate front_matter;
extern crate library;
use std::path::{Path, Component};
use std::path::{Component, Path};
use errors::Result;
use site::Site;
use library::{Page, Section};
use front_matter::{PageFrontMatter, SectionFrontMatter};
use library::{Page, Section};
use site::Site;
#[derive(Debug, Clone, Copy, PartialEq)]
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
/// delta in the serve command
/// Order matters as the actions will be done in insertion order
fn find_section_front_matter_changes(current: &SectionFrontMatter, new: &SectionFrontMatter) -> Vec<SectionChangesNeeded> {
fn find_section_front_matter_changes(
current: &SectionFrontMatter,
new: &SectionFrontMatter,
) -> Vec<SectionChangesNeeded> {
let mut changes_needed = vec![];
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
|| 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);
// Nothing else we can do
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
/// delta in the serve command
/// Order matters as the actions will be done in insertion order
fn find_page_front_matter_changes(current: &PageFrontMatter, other: &PageFrontMatter) -> Vec<PageChangesNeeded> {
fn find_page_front_matter_changes(
current: &PageFrontMatter,
other: &PageFrontMatter,
) -> Vec<PageChangesNeeded> {
let mut changes_needed = vec![];
if current.taxonomies != other.taxonomies {
changes_needed.push(PageChangesNeeded::Taxonomies);
}
if current.date != other.date || current.order != other.order || current.weight != other.weight {
if current.date != other.date || current.order != other.order || current.weight != other.weight
{
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
fn delete_element(site: &mut Site, path: &Path, is_section: bool) -> Result<()> {
// 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(());
}
@ -127,14 +136,21 @@ fn handle_section_editing(site: &mut Site, path: &Path) -> Result<()> {
}
// 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
match changes {
SectionChangesNeeded::Sort => {
site.register_tera_global_fns();
}
SectionChangesNeeded::Render => site.render_section(&site.library.get_section(&pathbuf).unwrap(), false)?,
SectionChangesNeeded::RenderWithPages => site.render_section(&site.library.get_section(&pathbuf).unwrap(), true)?,
SectionChangesNeeded::Render => {
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
SectionChangesNeeded::Delete => {
site.build()?;
@ -157,7 +173,7 @@ macro_rules! render_parent_section {
if let Some(s) = $site.library.find_parent_section($path) {
$site.render_section(s, false)?;
};
}
};
}
/// 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
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();
// 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
pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
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)]
mod tests {
use std::collections::HashMap;
use front_matter::{PageFrontMatter, SectionFrontMatter, SortBy};
use super::{
find_page_front_matter_changes, find_section_front_matter_changes,
PageChangesNeeded, SectionChangesNeeded,
find_page_front_matter_changes, find_section_front_matter_changes, PageChangesNeeded,
SectionChangesNeeded,
};
use front_matter::{PageFrontMatter, SectionFrontMatter, SortBy};
#[test]
fn can_find_taxonomy_changes_in_page_frontmatter() {
@ -320,7 +337,10 @@ mod tests {
taxonomies.insert("categories".to_string(), vec!["a category".to_string()]);
let current = PageFrontMatter { taxonomies, order: Some(1), ..PageFrontMatter::default() };
let changes = find_page_front_matter_changes(&current, &PageFrontMatter::default());
assert_eq!(changes, vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Sort, PageChangesNeeded::Render]);
assert_eq!(
changes,
vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Sort, PageChangesNeeded::Render]
);
}
#[test]

View file

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

View file

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

View file

@ -1,9 +1,8 @@
use std::collections::HashMap;
use tera::{Tera, Context};
use front_matter::InsertAnchor;
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
#[derive(Debug)]

View file

@ -1,35 +1,35 @@
extern crate tera;
extern crate syntect;
extern crate pulldown_cmark;
extern crate slug;
extern crate syntect;
extern crate tera;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate pest;
extern crate serde;
#[macro_use]
extern crate pest_derive;
#[macro_use]
extern crate errors;
extern crate front_matter;
extern crate utils;
extern crate config;
extern crate front_matter;
extern crate link_checker;
extern crate utils;
#[cfg(test)]
extern crate templates;
mod context;
mod markdown;
mod table_of_contents;
mod shortcode;
mod table_of_contents;
use errors::Result;
use markdown::markdown_to_html;
pub use table_of_contents::Header;
pub use shortcode::render_shortcodes;
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> {
// 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 self::cmark::{Parser, Event, Tag, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
use slug::slugify;
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 utils::site::resolve_internal_link;
use config::highlighting::{get_highlighter, THEME_SET, SYNTAX_SET};
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 table_of_contents::{make_table_of_contents, Header, TempHeader};
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];
highlighter = Some(get_highlighter(info, &context.config));
// 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);
let snippet = start_highlighted_html_snippet(theme);
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)) => {
if is_colocated_asset_link(&src) {
return Event::Start(
Tag::Image(
Owned(format!("{}{}", context.current_page_permalink, src)),
title,
)
);
return Event::Start(Tag::Image(
Owned(format!("{}{}", context.current_page_permalink, 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)
} else if context.config.check_external_links
&& !link.starts_with('#')
&& !link.starts_with("mailto:") {
&& !link.starts_with("mailto:")
{
let res = check_url(&link);
if res.is_valid() {
link.to_string()
} else {
error = Some(
format!("Link {} is not valid: {}", link, res.message()).into()
format!("Link {} is not valid: {}", link, res.message()).into(),
);
String::new()
}

View file

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

View file

@ -1,6 +1,5 @@
use tera::{Tera, Context as TeraContext};
use front_matter::InsertAnchor;
use tera::{Context as TeraContext, Tera};
#[derive(Debug, PartialEq, Clone, Serialize)]
pub struct Header {
@ -65,9 +64,26 @@ impl TempHeader {
};
match insert_anchor {
InsertAnchor::None => format!("<h{lvl} id=\"{id}\">{t}</h{lvl}>\n", 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),
InsertAnchor::None => format!(
"<h{lvl} id=\"{id}\">{t}</h{lvl}>\n",
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
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 start_at = start_at;
@ -124,7 +143,6 @@ fn find_children(parent_level: i32, start_at: usize, temp_headers: &[TempHeader]
(start_at, headers)
}
/// Converts the flat temp headers into a nested set of headers
/// representing the hierarchy
pub fn make_table_of_contents(temp_headers: &[TempHeader]) -> Vec<Header> {
@ -148,11 +166,7 @@ mod tests {
#[test]
fn can_make_basic_toc() {
let input = vec![
TempHeader::new(1),
TempHeader::new(1),
TempHeader::new(1),
];
let input = vec![TempHeader::new(1), TempHeader::new(1), TempHeader::new(1)];
let toc = make_table_of_contents(&input);
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 front_matter;
extern crate rendering;
extern crate templates;
extern crate tera;
use std::collections::HashMap;
@ -10,9 +10,8 @@ use tera::Tera;
use config::Config;
use front_matter::InsertAnchor;
use rendering::{render_content, RenderContext};
use templates::ZOLA_TERA;
use rendering::{RenderContext, render_content};
#[test]
fn can_do_render_content_simple() {
@ -32,10 +31,7 @@ fn doesnt_highlight_code_block_with_highlighting_off() {
config.highlight_code = false;
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("```\n$ gutenberg server\n```", &context).unwrap();
assert_eq!(
res.body,
"<pre><code>$ gutenberg server\n</code></pre>\n"
);
assert_eq!(res.body, "<pre><code>$ gutenberg server\n</code></pre>\n");
}
#[test]
@ -86,11 +82,15 @@ fn can_render_shortcode() {
let permalinks_ctx = HashMap::new();
let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(r#"
let res = render_content(
r#"
Hello
{{ youtube(id="ub36ffWAqgQ") }}
"#, &context).unwrap();
"#,
&context,
)
.unwrap();
assert!(res.body.contains("<p>Hello</p>\n<div >"));
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 config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let input = vec![
"name",
"na_me",
"n_a_me",
"n1",
];
let input = vec!["name", "na_me", "n_a_me", "n1"];
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""#));
}
}
@ -126,7 +122,9 @@ fn can_render_shortcode_with_markdown_char_in_args_value() {
];
for i in input {
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 mut tera = Tera::default();
tera.extend(&ZOLA_TERA).unwrap();
let input = vec![
"quo_te",
"qu_o_te",
];
let input = vec!["quo_te", "qu_o_te"];
let config = Config::default();
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 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);
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 config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(r#"
let res = render_content(
r#"
Hello
{{ youtube(id="ub36ffWAqgQ") }}
@ -230,10 +232,15 @@ Hello
{{ 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(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#"//player.vimeo.com/video/210073083""#));
}
@ -252,17 +259,25 @@ fn doesnt_render_ignored_shortcodes() {
fn can_render_shortcode_with_body() {
let mut tera = Tera::default();
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 config = Config::default();
let context = RenderContext::new(&tera, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content(r#"
let res = render_content(
r#"
Hello
{% quote(author="Keats") %}
A quote
{% end %}
"#, &context).unwrap();
"#,
&context,
)
.unwrap();
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(
r#"[rel link](./pages/about.md), [abs link](https://vincent.is/about)"#,
&context,
).unwrap();
)
.unwrap();
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>"#)
@ -302,9 +318,7 @@ fn can_make_relative_links_with_anchors() {
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
let res = render_content(r#"[rel link](./pages/about.md#cv)"#, &context).unwrap();
assert!(
res.body.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#)
);
assert!(res.body.contains(r#"<p><a href="https://vincent.is/about#cv">rel link</a></p>"#));
}
#[test]
@ -411,7 +425,8 @@ fn can_make_toc() {
InsertAnchor::Left,
);
let res = render_content(r#"
let res = render_content(
r#"
# Header 1
## Header 2
@ -419,7 +434,10 @@ fn can_make_toc() {
## Another Header 2
### Last one
"#, &context).unwrap();
"#,
&context,
)
.unwrap();
let toc = res.toc;
assert_eq!(toc.len(), 1);
@ -439,13 +457,17 @@ fn can_ignore_tags_in_toc() {
InsertAnchor::Left,
);
let res = render_content(r#"
let res = render_content(
r#"
## header with `code`
## [anchor](https://duckduckgo.com/) in header
## **bold** and *italics*
"#, &context).unwrap();
"#,
&context,
)
.unwrap();
let toc = res.toc;
@ -465,10 +487,7 @@ fn can_understand_backtick_in_titles() {
let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# `Hello`", &context).unwrap();
assert_eq!(
res.body,
"<h1 id=\"hello\"><code>Hello</code></h1>\n"
);
assert_eq!(res.body, "<h1 id=\"hello\"><code>Hello</code></h1>\n");
}
#[test]
@ -477,10 +496,7 @@ fn can_understand_backtick_in_paragraphs() {
let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("Hello `world`", &context).unwrap();
assert_eq!(
res.body,
"<p>Hello <code>world</code></p>\n"
);
assert_eq!(res.body, "<p>Hello <code>world</code></p>\n");
}
// https://github.com/Keats/gutenberg/issues/297
@ -490,10 +506,7 @@ fn can_understand_links_in_header() {
let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "", &permalinks_ctx, InsertAnchor::None);
let res = render_content("# [Rust](https://rust-lang.org)", &context).unwrap();
assert_eq!(
res.body,
"<h1 id=\"rust\"><a href=\"https://rust-lang.org\">Rust</a></h1>\n"
);
assert_eq!(res.body, "<h1 id=\"rust\"><a href=\"https://rust-lang.org\">Rust</a></h1>\n");
}
#[test]
@ -501,7 +514,8 @@ fn can_understand_link_with_title_in_header() {
let permalinks_ctx = HashMap::new();
let config = Config::default();
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!(
res.body,
"<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 config = Config::default();
let context = RenderContext::new(&tera_ctx, &config, "", &permalinks, InsertAnchor::None);
let res = render_content(
r#" # [rel link](./pages/about.md)"#,
&context,
).unwrap();
let res = render_content(r#" # [rel link](./pages/about.md)"#, &context).unwrap();
assert_eq!(
res.body,
@ -530,19 +541,28 @@ fn can_make_valid_relative_link_in_header() {
fn can_make_permalinks_with_colocated_assets_for_link() {
let permalinks_ctx = HashMap::new();
let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &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"
let context = RenderContext::new(
&ZOLA_TERA,
&config,
"https://vincent.is/about/",
&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]
fn can_make_permalinks_with_colocated_assets_for_image() {
let permalinks_ctx = HashMap::new();
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();
assert_eq!(
res.body,
@ -554,8 +574,15 @@ fn can_make_permalinks_with_colocated_assets_for_image() {
fn markdown_doesnt_wrap_html_in_paragraph() {
let permalinks_ctx = HashMap::new();
let config = Config::default();
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &permalinks_ctx, InsertAnchor::None);
let res = render_content(r#"
let context = RenderContext::new(
&ZOLA_TERA,
&config,
"https://vincent.is/about/",
&permalinks_ctx,
InsertAnchor::None,
);
let res = render_content(
r#"
Some text
<h1>Helo</h1>
@ -565,7 +592,10 @@ Some text
<img src="mobx-flow.png" alt="MobX flow">
</a>
</div>
"#, &context).unwrap();
"#,
&context,
)
.unwrap();
assert_eq!(
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"
@ -577,12 +607,15 @@ fn can_validate_valid_external_links() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.check_external_links = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &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"
let context = RenderContext::new(
&ZOLA_TERA,
&config,
"https://vincent.is/about/",
&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]
@ -590,7 +623,13 @@ fn can_show_error_message_for_invalid_external_links() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
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);
assert!(res.is_err());
let err = res.unwrap_err();
@ -602,12 +641,15 @@ fn doesnt_try_to_validate_email_links_mailto() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.check_external_links = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &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"
let context = RenderContext::new(
&ZOLA_TERA,
&config,
"https://vincent.is/about/",
&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]
@ -615,12 +657,15 @@ fn doesnt_try_to_validate_email_links_angled_brackets() {
let permalinks_ctx = HashMap::new();
let mut config = Config::default();
config.check_external_links = true;
let context = RenderContext::new(&ZOLA_TERA, &config, "https://vincent.is/about/", &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"
let context = RenderContext::new(
&ZOLA_TERA,
&config,
"https://vincent.is/about/",
&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]
@ -629,7 +674,11 @@ fn can_handle_summaries() {
let permalinks_ctx = HashMap::new();
let config = Config::default();
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!(
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"

View file

@ -11,9 +11,8 @@ use std::collections::{HashMap, HashSet};
use elasticlunr::{Index, Language};
use library::{Library, Section};
use errors::Result;
use library::{Library, Section};
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
/// the language given
/// 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> {
let language = match Language::from_code(lang) {
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"]);
@ -63,7 +63,10 @@ fn add_section_to_index(index: &mut Index, section: &Section, library: &Library)
if section.meta.redirect_to.is_none() {
index.add_doc(
&section.permalink,
&[&section.meta.title.clone().unwrap_or_default(), &AMMONIA.clean(&section.content).to_string()],
&[
&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(
&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
#![feature(test)]
extern crate test;
extern crate site;
extern crate test;
use std::env;
use site::Site;
#[bench]
fn bench_loading_small_blog(b: &mut test::Bencher) {
let mut path = env::current_dir().unwrap().to_path_buf();

View file

@ -1,15 +1,14 @@
#![feature(test)]
extern crate test;
extern crate site;
extern crate library;
extern crate site;
extern crate tempfile;
extern crate test;
use std::env;
use tempfile::tempdir;
use site::Site;
use library::Paginator;
use site::Site;
use tempfile::tempdir;
fn setup_site(name: &str) -> Site {
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 rayon;
extern crate serde;
extern crate tera;
#[macro_use]
extern crate serde_derive;
extern crate sass_rs;
@ -9,34 +9,36 @@ extern crate sass_rs;
#[macro_use]
extern crate errors;
extern crate config;
extern crate utils;
extern crate front_matter;
extern crate templates;
extern crate search;
extern crate imageproc;
extern crate library;
extern crate search;
extern crate templates;
extern crate utils;
#[cfg(test)]
extern crate tempfile;
use std::collections::{HashMap};
use std::fs::{create_dir_all, remove_dir_all, copy};
use std::collections::HashMap;
use std::fs::{copy, create_dir_all, remove_dir_all};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use glob::glob;
use tera::{Tera, Context};
use sass_rs::{Options as SassOptions, OutputStyle, compile_file};
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 config::{Config, get_config};
use utils::fs::{create_file, copy_directory, create_directory, ensure_directory_exists};
use utils::templates::{render_template, rewrite_theme_paths};
use front_matter::InsertAnchor;
use library::{
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 templates::{ZOLA_TERA, global_fns, render_redirect_template};
use front_matter::{InsertAnchor};
use library::{Page, Section, sort_actual_pages_by_date, Library, Taxonomy, find_taxonomies, Paginator};
use utils::templates::{render_template, rewrite_theme_paths};
/// The sitemap only needs links and potentially date so we trim down
/// all pages to only that
@ -81,7 +83,8 @@ impl Site {
let mut config = get_config(path, config_file);
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
// as we haven't loaded them yet
let mut tera = Tera::parse(&tpl_glob).chain_err(|| "Error parsing templates")?;
@ -100,11 +103,13 @@ impl Site {
path.to_string_lossy().replace("\\", "/"),
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);
// TODO: same as below
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.extend(&tera_theme)?;
@ -121,7 +126,8 @@ impl Site {
let content_path = path.join("content");
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 {
base_path: path.to_path_buf(),
@ -238,7 +244,10 @@ impl Site {
let mut pages_insert_anchors = HashMap::new();
for page in pages {
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)?;
}
@ -263,7 +272,10 @@ impl Site {
// This is needed in the first place because of silly borrow checker
let mut pages_insert_anchors = HashMap::new();
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
@ -291,10 +303,12 @@ impl Site {
/// Adds global fns that are to be available to shortcodes while rendering markdown
pub fn register_early_global_fns(&mut self) {
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(
"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",
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
@ -349,7 +366,7 @@ impl Site {
pub fn find_parent_section_insert_anchor(&self, parent_path: &PathBuf) -> InsertAnchor {
match self.library.get_section(&parent_path.join("_index.md")) {
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 {
return html.replace(
"</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
create_file(
&self.output_path.join("elasticlunr.min.js"),
search::ELASTICLUNR_JS,
)?;
create_file(&self.output_path.join("elasticlunr.min.js"), search::ELASTICLUNR_JS)?;
Ok(())
}
@ -537,12 +554,19 @@ impl Site {
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 files = glob(&glob_string)
.unwrap()
.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<_>>();
let mut compiled_paths = Vec::new();
@ -579,7 +603,7 @@ impl Site {
split.push(part);
"index.html"
}
None => "index.html"
None => "index.html",
};
for component in split {
@ -589,7 +613,10 @@ impl Site {
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(())
@ -650,15 +677,16 @@ impl Site {
}
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 {
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);
create_directory(&path)?;
create_file(
&path.join("index.html"),
&self.inject_livereload(single_output),
)
create_file(&path.join("index.html"), &self.inject_livereload(single_output))
}
})
.collect::<Result<()>>()
@ -670,7 +698,8 @@ impl Site {
let mut context = Context::new();
let mut pages = self.library
let mut pages = self
.library
.pages_values()
.iter()
.filter(|p| !p.is_draft())
@ -685,7 +714,8 @@ impl Site {
pages.sort_by(|a, b| a.permalink.cmp(&b.permalink));
context.insert("pages", &pages);
let mut sections = self.library
let mut sections = self
.library
.sections_values()
.iter()
.map(|s| SitemapEntry::new(s.permalink.clone(), None))
@ -699,7 +729,10 @@ impl Site {
let mut terms = vec![];
terms.push(SitemapEntry::new(self.config.make_permalink(name), None));
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));
taxonomies.push(terms);
@ -718,7 +751,11 @@ impl Site {
/// 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
/// 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)?;
let mut context = Context::new();
@ -806,7 +843,10 @@ impl Site {
if let Some(ref redirect_to) = section.meta.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(());
}
@ -861,12 +901,16 @@ impl Site {
.map(|pager| {
let page_path = folder_path.join(&format!("{}", pager.index));
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 {
create_file(&page_path.join("index.html"), &self.inject_livereload(output))?;
} else {
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(())
})

View file

@ -3,13 +3,12 @@ extern crate tempfile;
use std::collections::HashMap;
use std::env;
use std::path::Path;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use tempfile::tempdir;
use site::Site;
use tempfile::tempdir;
#[test]
fn can_parse_site() {
@ -27,7 +26,8 @@ fn can_parse_site() {
assert_eq!(url_post.path, "a-fixed-url/");
// Make sure the article in a folder with only asset doesn't get counted as a section
let asset_folder_post = 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()]);
// 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();
assert_eq!(posts_section.subsections.len(), 1);
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
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);
let sub1 = site.library.get_section_by_key(tutorials_section.subsections[0]);
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!(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.pages.len(), 2);
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.pages.len(), 2);
}
// 2 helper macros to make all the build testing more bearable
macro_rules! file_exists {
($root: expr, $path: expr) => {
{
let mut path = $root.clone();
for component in $path.split("/") {
path = path.join(component);
}
Path::new(&path).exists()
($root: expr, $path: expr) => {{
let mut path = $root.clone();
for component in $path.split("/") {
path = path.join(component);
}
}
Path::new(&path).exists()
}};
}
macro_rules! file_contains {
($root: expr, $path: expr, $text: expr) => {
{
let mut path = $root.clone();
for component in $path.split("/") {
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)
($root: expr, $path: expr, $text: expr) => {{
let mut path = $root.clone();
for component in $path.split("/") {
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)
}};
}
#[test]
@ -145,7 +151,11 @@ fn can_build_site_without_live_reload() {
// 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/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
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);
// 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!(public, "sitemap.xml", "<loc>https://replace-this-with-your-url.com/posts/</loc>"));
assert!(file_contains!(
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
assert!(!file_contains!(public, "sitemap.xml", "draft"));
// robots.txt has been rendered from the template
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]
@ -231,7 +253,11 @@ fn can_build_site_with_live_reload() {
assert!(file_contains!(public, "index.html", "/livereload.js"));
// 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"#));
}
@ -245,7 +271,10 @@ fn can_build_site_with_taxonomies() {
for (i, (_, page)) in site.library.pages_mut().iter_mut().enumerate() {
page.meta.taxonomies = {
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
};
}
@ -278,15 +307,27 @@ fn can_build_site_with_taxonomies() {
assert!(file_exists!(public, "categories/a/index.html"));
assert!(file_exists!(public, "categories/b/index.html"));
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
assert!(file_contains!(public, "categories/a/index.html", "EXTENDED"));
// Tags aren't
assert_eq!(file_exists!(public, "tags/index.html"), false);
// Categories are in the sitemap
assert!(file_contains!(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>"));
assert!(file_contains!(
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]
@ -303,7 +344,11 @@ fn can_build_site_and_insert_anchor_links() {
assert!(Path::new(&public).exists());
// 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]
@ -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", "has_prev"));
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!(public, "posts/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/"));
assert!(file_contains!(
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!(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", "has_prev"));
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!(public, "posts/page/2/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/"));
assert!(file_contains!(
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_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", "has_prev"));
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!(public, "posts/page/3/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/"));
assert!(file_contains!(
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_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", "has_prev"));
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!(public, "posts/page/4/index.html", "Last: https://replace-this-with-your-url.com/posts/page/4/"));
assert!(file_contains!(
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]
@ -448,7 +525,6 @@ fn can_build_rss_feed() {
assert!(file_contains!(public, "rss.xml", "Simple article with shortcodes"));
}
#[test]
fn can_build_search_index() {
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!(file_exists!(public, "posts/extra-syntax/index.html"));
assert!(file_contains!(public, "posts/extra-syntax/index.html",
r#"<span style="color:#d08770;">test</span>"#));
assert!(file_contains!(
public,
"posts/extra-syntax/index.html",
r#"<span style="color:#d08770;">test</span>"#
));
}

View file

@ -1,9 +1,8 @@
use std::collections::HashMap;
use base64::{encode, decode};
use base64::{decode, encode};
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> {
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())
}
pub fn base64_encode(value: Value, _: HashMap<String, Value>) -> TeraResult<Value> {
let s = try_get_value!("base64_encode", "value", String, value);
Ok(
to_value(&encode(s.as_bytes())).unwrap()
)
Ok(to_value(&encode(s.as_bytes())).unwrap())
}
pub fn base64_decode(value: Value, _: HashMap<String, Value>) -> TeraResult<Value> {
let s = try_get_value!("base64_decode", "value", String, value);
Ok(
to_value(
&String::from_utf8(
decode(s.as_bytes()).unwrap()
).unwrap()
).unwrap()
)
Ok(to_value(&String::from_utf8(decode(s.as_bytes()).unwrap()).unwrap()).unwrap())
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use tera::to_value;
use super::{markdown, base64_decode, base64_encode};
use super::{base64_decode, base64_encode, markdown};
#[test]
fn markdown_filter() {
@ -70,7 +59,10 @@ mod tests {
fn markdown_filter_inline() {
let mut args = HashMap::new();
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_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() {
let mut args = HashMap::new();
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 |
|-:|--------:|-----------------------:|:---------------------|:-----------------|
| 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|
"#).unwrap(), args);
"#,
)
.unwrap(),
args,
);
assert!(result.is_ok());
assert!(result.unwrap().as_str().unwrap().contains("<table>"));
}
@ -100,7 +98,7 @@ mod tests {
("foo", "Zm9v"),
("foob", "Zm9vYg=="),
("fooba", "Zm9vYmE="),
("foobar", "Zm9vYmFy")
("foobar", "Zm9vYmFy"),
];
for (input, expected) in tests {
let args = HashMap::new();
@ -110,7 +108,6 @@ mod tests {
}
}
#[test]
fn base64_decode_filter() {
let tests = vec![
@ -120,7 +117,7 @@ mod tests {
("Zm9v", "foo"),
("Zm9vYg==", "foob"),
("Zm9vYmE=", "fooba"),
("Zm9vYmFy", "foobar")
("Zm9vYmFy", "foobar"),
];
for (input, expected) in tests {
let args = HashMap::new();

View file

@ -1,28 +1,28 @@
extern crate toml;
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 std::str::FromStr;
use std::fmt;
use reqwest::{header, Client};
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 std::path::PathBuf;
use std::sync::{Arc, Mutex};
use csv::Reader;
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 {
Url(Url),
Path(PathBuf)
Path(PathBuf),
}
#[derive(Debug)]
@ -30,7 +30,7 @@ enum OutputFormat {
Toml,
Json,
Csv,
Plain
Plain,
}
impl fmt::Display for OutputFormat {
@ -54,7 +54,7 @@ impl FromStr for OutputFormat {
"csv" => Ok(OutputFormat::Csv),
"json" => Ok(OutputFormat::Json),
"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 {
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() {
return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into());
}
@ -85,7 +89,9 @@ impl DataSource {
}
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());
@ -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 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
);
let url_arg = optional_arg!(String, args.get("url"), GET_DATA_ARGUMENT_ERROR_MESSAGE);
return DataSource::from_args(path_arg, url_arg, content_path);
}
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))? {
return Err(format!("{} is not inside the base site directory {}", full_path.display(), base_path.display()).into());
if !is_path_in_directory(&base_path, &full_path)
.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)
.map_err(|e| format!("`load_data`: error {} loading file {}", full_path.to_str().unwrap(), e).into());
return read_file(&full_path).map_err(|e| {
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!(
String,
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 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?
} else {
"plain"
@ -156,7 +170,6 @@ fn get_output_format_from_args(args: &HashMap<String, Value>, data_source: &Data
return OutputFormat::from_str(from_extension);
}
/// A global function to load data from a file or from a URL
/// Currently the supported formats are json, toml, csv and plain text
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 {
DataSource::Path(path) => read_data_file(&base_path, path),
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")))?;
response.text().map_err(|e| format!("Failed to parse response from {}: {:?}", url, e).into())
},
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")
)
})?;
response
.text()
.map_err(|e| format!("Failed to parse response from {}: {:?}", url, e).into())
}
}?;
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
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);
}
@ -235,12 +262,11 @@ fn load_csv(csv_data: String) -> Result<Value> {
let mut csv_map = Map::new();
{
let hdrs = reader.headers()
.map_err(|e| format!("'load_data': {} - unable to read CSV header line (line 1) for CSV file", e))?;
let hdrs = reader.headers().map_err(|e| {
format!("'load_data': {} - unable to read CSV header line (line 1) for CSV file", e)
})?;
let headers_array = hdrs.iter()
.map(|v| Value::String(v.to_string()))
.collect();
let headers_array = hdrs.iter().map(|v| Value::String(v.to_string())).collect();
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())
}
#[cfg(test)]
mod tests {
use super::{make_load_data, DataSource, OutputFormat};
@ -285,7 +310,8 @@ mod tests {
#[test]
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();
args.insert("path".to_string(), to_value("../../../READMEE.md").unwrap());
let result = static_fn(args);
@ -295,40 +321,54 @@ mod tests {
#[test]
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();
args.insert("path".to_string(), to_value("../../../README.md").unwrap());
args.insert("format".to_string(), to_value("plain").unwrap());
let result = static_fn(args);
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]
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
let cache_key = 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);
let cache_key =
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);
}
#[test]
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);
}
#[test]
fn different_cache_key_per_filename() {
let toml_cache_key = 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);
let toml_cache_key =
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);
}
#[test]
fn different_cache_key_per_format() {
let toml_cache_key = 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);
let toml_cache_key =
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);
}
@ -339,7 +379,10 @@ mod tests {
args.insert("url".to_string(), to_value("https://httpbin.org/json").unwrap());
args.insert("format".to_string(), to_value("json").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]
@ -350,60 +393,78 @@ mod tests {
args.insert("format".to_string(), to_value("json").unwrap());
let result = static_fn(args);
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]
fn can_load_toml()
{
let static_fn = make_load_data(PathBuf::from("../utils/test-files"), PathBuf::from("../utils/test-files"));
fn can_load_toml() {
let static_fn = make_load_data(
PathBuf::from("../utils/test-files"),
PathBuf::from("../utils/test-files"),
);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.toml").unwrap());
let result = static_fn(args.clone()).unwrap();
//TOML does not load in order, and also dates are not returned as strings, but
//rather as another object with a key and value
assert_eq!(result, json!({
assert_eq!(
result,
json!({
"category": {
"date": {
"$__toml_private_datetime": "1979-05-27T07:32:00Z"
},
"key": "value"
},
}));
})
);
}
#[test]
fn can_load_csv()
{
let static_fn = make_load_data(PathBuf::from("../utils/test-files"), PathBuf::from("../utils/test-files"));
fn can_load_csv() {
let static_fn = make_load_data(
PathBuf::from("../utils/test-files"),
PathBuf::from("../utils/test-files"),
);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.csv").unwrap());
let result = static_fn(args.clone()).unwrap();
assert_eq!(result, json!({
assert_eq!(
result,
json!({
"headers": ["Number", "Title"],
"records": [
["1", "Gutenberg"],
["2", "Printing"]
],
}))
})
)
}
#[test]
fn can_load_json()
{
let static_fn = make_load_data(PathBuf::from("../utils/test-files"), PathBuf::from("../utils/test-files"));
fn can_load_json() {
let static_fn = make_load_data(
PathBuf::from("../utils/test-files"),
PathBuf::from("../utils/test-files"),
);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.json").unwrap());
let result = static_fn(args.clone()).unwrap();
assert_eq!(result, json!({
assert_eq!(
result,
json!({
"key": "value",
"array": [1, 2, 3],
"subpackage": {
"subkey": 5
}
}))
})
)
}
}

View file

@ -4,9 +4,9 @@ macro_rules! required_arg {
match $e {
Some(v) => match from_value::<$ty>(v.clone()) {
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 {
Some(v) => match from_value::<$ty>(v.clone()) {
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::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 library::{Library, Taxonomy};
use utils::site::resolve_internal_link;
use imageproc;
@ -18,24 +18,19 @@ mod load_data;
pub use self::load_data::make_load_data;
pub fn make_trans(config: Config) -> GlobalFn {
let translations_config = config.translations;
let default_lang = config.default_language.clone();
Box::new(move |args| -> Result<Value> {
let key = required_arg!(String, args.get("key"), "`trans` requires a `key` argument.");
let lang = optional_arg!(
String,
args.get("lang"),
"`trans`: `lang` must be a string."
).unwrap_or_else(|| default_lang.clone());
let lang = optional_arg!(String, args.get("lang"), "`trans`: `lang` must be a string.")
.unwrap_or_else(|| default_lang.clone());
let translations = &translations_config[lang.as_str()];
Ok(to_value(&translations[key.as_str()]).unwrap())
})
}
pub fn make_get_page(library: &Library) -> GlobalFn {
let mut pages = HashMap::new();
for page in library.pages_values() {
@ -53,7 +48,7 @@ pub fn make_get_page(library: &Library) -> GlobalFn {
);
match pages.get(&path) {
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() {
sections.insert(
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(
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
.get("metadata_only")
.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 container = if metadata_only {
&sections_basic
} else {
&sections
};
let container = if metadata_only { &sections_basic } else { &sections };
match container.get(&path) {
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 {
Box::new(move |args| -> Result<Value> {
let cachebust = args
.get("cachebust")
.map_or(false, |c| {
from_value::<bool>(c.clone()).unwrap_or(false)
});
let cachebust =
args.get("cachebust").map_or(false, |c| from_value::<bool>(c.clone()).unwrap_or(false));
let trailing_slash = args
.get("trailing_slash")
.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 path = required_arg!(
String,
@ -121,7 +107,9 @@ pub fn make_get_url(permalinks: HashMap<String, String>, config: Config) -> Glob
if path.starts_with("./") {
match resolve_internal_link(&path, &permalinks) {
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 {
// 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 {
let mut taxonomies = HashMap::new();
for taxonomy in all_taxonomies {
taxonomies.insert(
taxonomy.kind.name.clone(),
to_value(taxonomy.to_serialized(library)).unwrap()
);
taxonomies
.insert(taxonomy.kind.name.clone(), to_value(taxonomy.to_serialized(library)).unwrap());
}
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) {
Some(c) => c,
None => return Err(
format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into()
),
None => {
return Err(
format!("`get_taxonomy` received an unknown taxonomy as kind: {}", kind).into()
)
}
};
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) {
Some(c) => c,
None => return Err(
format!("`get_taxonomy_url` received an unknown taxonomy as kind: {}", kind).into()
)
None => {
return Err(format!(
"`get_taxonomy_url` received an unknown taxonomy as kind: {}",
kind
)
.into())
}
};
if let Some(ref permalink) = container.get(&name) {
return Ok(to_value(permalink.clone()).unwrap());
}
Err(
format!("`get_taxonomy_url`: couldn't find `{}` in `{}` taxonomy", name, kind).into()
)
Err(format!("`get_taxonomy_url`: couldn't find `{}` in `{}` taxonomy", name, kind).into())
})
}
@ -222,16 +212,11 @@ pub fn make_resize_image(imageproc: Arc<Mutex<imageproc::Processor>>) -> GlobalF
args.get("height"),
"`resize_image`: `height` must be a non-negative integer"
);
let op = optional_arg!(
String,
args.get("op"),
"`resize_image`: `op` must be a string"
).unwrap_or_else(|| DEFAULT_OP.to_string());
let quality = optional_arg!(
u8,
args.get("quality"),
"`resize_image`: `quality` must be a number"
).unwrap_or(DEFAULT_Q);
let op = optional_arg!(String, args.get("op"), "`resize_image`: `op` must be a string")
.unwrap_or_else(|| DEFAULT_OP.to_string());
let quality =
optional_arg!(u8, args.get("quality"), "`resize_image`: `quality` must be a number")
.unwrap_or(DEFAULT_Q);
if quality == 0 || quality > 100 {
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)]
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 tera::{to_value, Value};
use config::{Config, Taxonomy as TaxonomyConfig};
use library::{Taxonomy, TaxonomyItem, Library};
use library::{Library, Taxonomy, TaxonomyItem};
#[test]
fn can_add_cachebust_to_url() {
@ -307,17 +289,8 @@ mod tests {
fn can_get_taxonomy() {
let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() };
let library = Library::new(0, 0);
let tag = TaxonomyItem::new(
"Programming",
"tags",
&Config::default(),
vec![],
&library
);
let tags = Taxonomy {
kind: taxo_config,
items: vec![tag],
};
let tag = TaxonomyItem::new("Programming", "tags", &Config::default(), vec![], &library);
let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
let taxonomies = vec![tags.clone()];
let static_fn = make_get_taxonomy(&taxonomies, &library);
@ -337,7 +310,8 @@ mod tests {
Value::String("programming".to_string())
);
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())
);
assert_eq!(
@ -354,17 +328,8 @@ mod tests {
fn can_get_taxonomy_url() {
let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() };
let library = Library::new(0, 0);
let tag = TaxonomyItem::new(
"Programming",
"tags",
&Config::default(),
vec![],
&library
);
let tags = Taxonomy {
kind: taxo_config,
items: vec![tag],
};
let tag = TaxonomyItem::new("Programming", "tags", &Config::default(), vec![], &library);
let tags = Taxonomy { kind: taxo_config, items: vec![tag] };
let taxonomies = vec![tags.clone()];
let static_fn = make_get_taxonomy_url(&taxonomies);
@ -372,7 +337,10 @@ mod tests {
let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap());
args.insert("name".to_string(), to_value("Programming").unwrap());
assert_eq!(static_fn(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
let mut args = HashMap::new();
args.insert("kind".to_string(), to_value("tags").unwrap());

View file

@ -3,28 +3,27 @@ extern crate lazy_static;
#[macro_use]
extern crate tera;
extern crate base64;
extern crate pulldown_cmark;
extern crate csv;
extern crate pulldown_cmark;
extern crate reqwest;
extern crate url;
#[cfg(test)]
#[macro_use]
extern crate serde_json;
#[cfg(not(test))]
extern crate serde_json;
extern crate errors;
extern crate utils;
extern crate library;
extern crate config;
extern crate errors;
extern crate imageproc;
extern crate library;
extern crate utils;
pub mod filters;
pub mod global_fns;
use tera::{Tera, Context};
use tera::{Context, Tera};
use errors::{Result, ResultExt};
@ -37,14 +36,13 @@ lazy_static! {
("sitemap.xml", include_str!("builtins/sitemap.xml")),
("robots.txt", include_str!("builtins/robots.txt")),
("anchor-link.html", include_str!("builtins/anchor-link.html")),
("shortcodes/youtube.html", include_str!("builtins/shortcodes/youtube.html")),
("shortcodes/vimeo.html", include_str!("builtins/shortcodes/vimeo.html")),
("shortcodes/gist.html", include_str!("builtins/shortcodes/gist.html")),
("shortcodes/streamable.html", include_str!("builtins/shortcodes/streamable.html")),
("internal/alias.html", include_str!("builtins/internal/alias.html")),
]).unwrap();
])
.unwrap();
tera.register_filter("markdown", filters::markdown);
tera.register_filter("base64_encode", filters::base64_encode);
tera.register_filter("base64_decode", filters::base64_decode);
@ -52,7 +50,6 @@ lazy_static! {
};
}
/// Renders the `internal/alias.html` template that will redirect
/// via refresh to the url given
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::fs::{File, create_dir_all, read_dir, copy};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use walkdir::WalkDir;
use errors::{Result, ResultExt};
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_parent = parent.canonicalize().map_err(|e| format!("Failed to canonicalize {}: {}", parent.display(), e))?;
let canonical_path = path
.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))
}
/// Create a file with the content given
pub fn create_file(path: &Path, content: &str) -> Result<()> {
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
/// 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_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)
}
#[cfg(test)]
mod tests {
use std::fs::File;

View file

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

View file

@ -1,9 +1,7 @@
use std::net::TcpListener;
pub fn get_available_port(avoid: u16) -> Option<u16> {
(1000..9000)
.find(|port| *port != avoid && port_is_available(*port))
(1000..9000).find(|port| *port != avoid && port_is_available(*port))
}
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)]
mod tests {
use std::collections::HashMap;
use super::{resolve_internal_link, get_reading_analytics};
use super::{get_reading_analytics, resolve_internal_link};
#[test]
fn can_resolve_valid_internal_link() {

View file

@ -1,56 +1,55 @@
use std::collections::HashMap;
use tera::{Tera, Context};
use tera::{Context, Tera};
use errors::Result;
static DEFAULT_TPL: &str = include_str!("default_tpl.html");
macro_rules! render_default_tpl {
($filename: expr, $url: expr) => {
{
let mut context = Context::new();
context.insert("filename", $filename);
context.insert("url", $url);
Tera::one_off(DEFAULT_TPL, &context, true).map_err(|e| e.into())
}
};
($filename: expr, $url: expr) => {{
let mut context = Context::new();
context.insert("filename", $filename);
context.insert("url", $url);
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
/// 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
/// 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) {
return tera
.render(name, context)
.map_err(|e| e.into());
return tera.render(name, context).map_err(|e| e.into());
}
if let Some(ref t) = *theme {
return tera
.render(&format!("{}/templates/{}", t, name), context)
.map_err(|e| e.into());
return tera.render(&format!("{}/templates/{}", t, name), context).map_err(|e| e.into());
}
// maybe it's a default one?
match name {
"index.html" | "section.html" => {
render_default_tpl!(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")
}
"index.html" | "section.html" => render_default_tpl!(
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"
),
"single.html" | "list.html" => {
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
/// 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
@ -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());
// 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);
// First the parent if there is none
if let Some(ref p) = tpl.parent.clone() {
@ -97,8 +96,8 @@ pub fn rewrite_theme_paths(tera: &mut Tera, theme: &str) {
#[cfg(test)]
mod tests {
use tera::Tera;
use super::rewrite_theme_paths;
use tera::Tera;
#[test]
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 errors::Result;
use utils::fs::create_file;
use prompt::{ask_bool, ask_url};
use console;
use prompt::{ask_bool, ask_url};
const CONFIG: &str = r#"
# The URL the site will be built for
@ -26,7 +25,6 @@ build_search_index = %SEARCH%
# Put all your custom variables here
"#;
pub fn create_new_project(name: &str) -> Result<()> {
let path = Path::new(name);
// Better error message than the rust default
@ -62,7 +60,9 @@ pub fn create_new_project(name: &str) -> Result<()> {
println!();
console::success(&format!("Done! Your site was created in {:?}", canonicalize(path).unwrap()));
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.");
Ok(())
}

View file

@ -1,7 +1,7 @@
mod init;
mod build;
mod init;
mod serve;
pub use self::init::create_new_project;
pub use self::build::build;
pub use self::init::create_new_project;
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::path::{Path, PathBuf, MAIN_SEPARATOR};
use std::sync::mpsc::channel;
use std::time::{Instant, Duration};
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::middleware::{Middleware, Started, Response};
use notify::{Watcher, RecursiveMode, watcher};
use ws::{WebSocket, Sender, Message};
use chrono::prelude::*;
use ctrlc;
use notify::{watcher, RecursiveMode, Watcher};
use ws::{Message, Sender, WebSocket};
use site::Site;
use errors::{Result, ResultExt};
use site::Site;
use utils::fs::copy_file;
use console;
@ -93,7 +93,9 @@ fn livereload_handler(_: &HttpRequest) -> &'static str {
fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &str) {
match res {
Ok(_) => {
broadcaster.send(format!(r#"
broadcaster
.send(format!(
r#"
{{
"command": "reload",
"path": "{}",
@ -101,14 +103,22 @@ fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &st
"liveCSS": true,
"liveImg": true,
"protocol": ["http://livereload.com/protocols/official-7"]
}}"#, reload_path)
).unwrap();
},
Err(e) => console::unravel_errors("Failed to build the site", &e)
}}"#,
reload_path
))
.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 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
/// custom directory listing response and serve it up using their
/// `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);
path.push(&dir.path);
path.push("index.html");
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 (mut site, address) = create_new_site(interface, port, output_dir, base_url, config_file)?;
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 (tx, rx) = channel();
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?")?;
watcher.watch(config_file, RecursiveMode::Recursive)
watcher
.watch(config_file, RecursiveMode::Recursive)
.chain_err(|| "Can't watch the `config` file. Does it exist?")?;
if Path::new("static").exists() {
watching_static = true;
watcher.watch("static/", RecursiveMode::Recursive)
watcher
.watch("static/", RecursiveMode::Recursive)
.chain_err(|| "Can't watch the `static` folder.")?;
}
if Path::new("templates").exists() {
watching_templates = true;
watcher.watch("templates/", RecursiveMode::Recursive)
watcher
.watch("templates/", RecursiveMode::Recursive)
.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 || {
let s = server::new(move || {
App::new()
.middleware(NotFoundHandler { rendered_template: static_root.join("404.html") })
.resource(r"/livereload.js", |r| r.f(livereload_handler))
// Start a webserver that serves the `output_dir` directory
.handler(
r"/",
fs::StaticFiles::new(&static_root)
.unwrap()
.show_files_listing()
.files_listing_renderer(handle_directory)
)
.middleware(NotFoundHandler { rendered_template: static_root.join("404.html") })
.resource(r"/livereload.js", |r| r.f(livereload_handler))
// Start a webserver that serves the `output_dir` directory
.handler(
r"/",
fs::StaticFiles::new(&static_root)
.unwrap()
.show_files_listing()
.files_listing_renderer(handle_directory),
)
})
.bind(&address)
.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| {
move |msg: Message| {
if msg.into_text().unwrap().contains("\"hello\"") {
return output.send(Message::text(r#"
return output.send(Message::text(
r#"
{
"command": "hello",
"protocols": [ "http://livereload.com/protocols/official-7" ],
"serverName": "Zola"
}
"#));
"#,
));
}
Ok(())
}
}).unwrap();
})
.unwrap();
let broadcaster = ws_server.broadcaster();
thread::spawn(move || {
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");
}
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");
// Delete the output folder on ctrl+C
ctrlc::set_handler(move || {
remove_dir_all(&output_path).expect("Failed to delete output directory");
::std::process::exit(0);
}).expect("Error setting Ctrl-C handler");
})
.expect("Error setting Ctrl-C handler");
use notify::DebouncedEvent::*;
@ -252,47 +284,74 @@ pub fn serve(interface: &str, port: u16, output_dir: &str, base_url: &str, confi
match rx.recv() {
Ok(event) => {
match event {
Create(path) |
Write(path) |
Remove(path) |
Rename(_, path) => {
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
if is_temp_file(&path) || path.is_dir() {
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();
match detect_change_kind(&pwd, &path) {
(ChangeKind::Content, _) => {
console::info(&format!("-> Content changed {}", path.display()));
// Force refresh
rebuild_done_handling(&broadcaster, rebuild::after_content_change(&mut site, &path), "/x.js");
},
rebuild_done_handling(
&broadcaster,
rebuild::after_content_change(&mut site, &path),
"/x.js",
);
}
(ChangeKind::Templates, _) => {
console::info(&format!("-> Template changed {}", path.display()));
// 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) => {
if path.is_file() {
console::info(&format!("-> Static file changes detected {}", path.display()));
rebuild_done_handling(&broadcaster, copy_file(&path, &site.output_path, &site.static_path), &p.to_string_lossy());
console::info(&format!(
"-> 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) => {
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, _) => {
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);
}
_ => {}
}
},
}
Err(e) => console::error(&format!("Watch error: {:?}", e)),
};
}
@ -321,9 +380,7 @@ fn is_temp_file(path: &Path) -> bool {
}
}
},
None => {
true
},
None => true,
}
}
@ -354,7 +411,7 @@ fn detect_change_kind(pwd: &Path, path: &Path) -> (ChangeKind, PathBuf) {
mod tests {
use std::path::{Path, PathBuf};
use super::{is_temp_file, detect_change_kind, ChangeKind};
use super::{detect_change_kind, is_temp_file, ChangeKind};
#[test]
fn can_recognize_temp_files() {
@ -380,23 +437,28 @@ mod tests {
let test_cases = vec![
(
(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")),
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")),
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")),
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")),
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) {
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
pub fn warn_about_ignored_pages(site: &Site) {
let ignored_pages: Vec<_> = site.library
let ignored_pages: Vec<_> = site
.library
.sections_values()
.iter()
.flat_map(|s| {
s.ignored_pages
.iter()
.map(|k| site.library.get_page_by_key(*k).file.path.clone())
s.ignored_pages.iter().map(|k| site.library.get_page_by_key(*k).file.path.clone())
})
.collect();
@ -104,8 +102,9 @@ pub fn unravel_errors(message: &str, error: &Error) {
/// Check whether to output colors
fn has_color() -> bool {
let use_colors = env::var("CLICOLOR").unwrap_or_else(|_| "1".to_string()) != "0" && env::var("NO_COLOR").is_err();
let force_colors = env::var("CLICOLOR_FORCE").unwrap_or_else(|_|"0".to_string()) != "0";
let use_colors = env::var("CLICOLOR").unwrap_or_else(|_| "1".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)
}

View file

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

View file

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