mirror of
https://github.com/getzola/zola
synced 2024-11-10 06:14:19 +00:00
rustfmt
This commit is contained in:
parent
8586bc1838
commit
b7ce4e59fb
52 changed files with 1418 additions and 1091 deletions
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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?"
|
||||
)?
|
||||
})?
|
||||
.read_to_string(&mut content)?;
|
||||
|
||||
Theme::parse(&content)
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>,
|
||||
{
|
||||
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"#;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
"fit_width" => {
|
||||
if width.is_none() {
|
||||
return Err("op=\"fit_width\" requires a `width` argument".to_string().into());
|
||||
},
|
||||
"fit_height" => if height.is_none() {
|
||||
}
|
||||
}
|
||||
"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() {
|
||||
}
|
||||
}
|
||||
"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())
|
||||
}
|
||||
}
|
||||
_ => 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)| {
|
||||
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<()>>()
|
||||
})
|
||||
.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)
|
||||
}
|
||||
|
|
|
@ -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()]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
page.assets = assets
|
||||
.into_iter()
|
||||
.filter(|path| match path.file_name() {
|
||||
None => true,
|
||||
Some(file) => !globset.is_match(file)
|
||||
}
|
||||
).collect();
|
||||
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();
|
||||
|
|
|
@ -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() {
|
||||
section.assets = assets
|
||||
.into_iter()
|
||||
.filter(|path| match path.file_name() {
|
||||
None => true,
|
||||
Some(file) => !globset.is_match(file)
|
||||
}
|
||||
).collect();
|
||||
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();
|
||||
|
|
|
@ -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,
|
||||
|
@ -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: §ion.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![]
|
||||
};
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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,7 +80,8 @@ 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()))
|
||||
|
@ -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(§ions_weight[b]));
|
||||
section.subsections = children;
|
||||
}
|
||||
section.ancestors = ancestors.get(§ion.file.path).cloned().unwrap_or_else(|| vec![]);
|
||||
section.ancestors =
|
||||
ancestors.get(§ion.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| {
|
||||
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());
|
||||
})
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(¤t_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;
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use rayon::prelude::*;
|
||||
use slotmap::Key;
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
use content::Page;
|
||||
|
||||
|
@ -21,12 +21,10 @@ 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| {
|
||||
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)
|
||||
|
@ -42,12 +40,10 @@ 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| {
|
||||
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)
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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(¤t, &PageFrontMatter::default());
|
||||
assert_eq!(changes, vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Sort, PageChangesNeeded::Render]);
|
||||
assert_eq!(
|
||||
changes,
|
||||
vec![PageChangesNeeded::Taxonomies, PageChangesNeeded::Sort, PageChangesNeeded::Render]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
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();
|
||||
($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;
|
||||
|
@ -36,15 +36,13 @@ macro_rules! load_and_build_site {
|
|||
site.build().unwrap();
|
||||
|
||||
(site_path, site)
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
/// Replace the file at the path (starting from root) by the given content
|
||||
/// and return the file path that was modified
|
||||
macro_rules! edit_file {
|
||||
($site_path: expr, $path: expr, $content: expr) => {
|
||||
{
|
||||
($site_path: expr, $path: expr, $content: expr) => {{
|
||||
let mut t = $site_path.clone();
|
||||
for c in $path.split('/') {
|
||||
t.push(c);
|
||||
|
@ -52,13 +50,11 @@ macro_rules! edit_file {
|
|||
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) => {
|
||||
{
|
||||
($site_path: expr, $path: expr, $text: expr) => {{
|
||||
let mut path = $site_path.clone();
|
||||
for component in $path.split("/") {
|
||||
path.push(component);
|
||||
|
@ -68,22 +64,25 @@ macro_rules! file_contains {
|
|||
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>"
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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,8 +122,7 @@ 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 {
|
||||
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(),
|
||||
|
@ -140,7 +152,6 @@ pub fn render_shortcodes(content: &str, context: &RenderContext) -> Result<Strin
|
|||
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",
|
||||
{
|
||||
("{{ 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",
|
||||
{
|
||||
}),
|
||||
("{{ 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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
§ion.permalink,
|
||||
&[§ion.meta.title.clone().unwrap_or_default(), &AMMONIA.clean(§ion.content).to_string()],
|
||||
&[
|
||||
§ion.meta.title.clone().unwrap_or_default(),
|
||||
&AMMONIA.clean(§ion.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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(())
|
||||
})
|
||||
|
|
|
@ -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,27 +82,27 @@ 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) => {
|
||||
{
|
||||
($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) => {
|
||||
{
|
||||
($root: expr, $path: expr, $text: expr) => {{
|
||||
let mut path = $root.clone();
|
||||
for component in $path.split("/") {
|
||||
path = path.join(component);
|
||||
|
@ -105,8 +112,7 @@ macro_rules! file_contains {
|
|||
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>"#
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}))
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(§ion.file.path).unwrap().to_serialized(library)).unwrap(),
|
||||
to_value(library.get_section(§ion.file.path).unwrap().to_serialized(library))
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
sections_basic.insert(
|
||||
section.file.relative.clone(),
|
||||
to_value(library.get_section(§ion.file.path).unwrap().to_serialized_basic(library)).unwrap(),
|
||||
to_value(library.get_section(§ion.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 {
|
||||
§ions_basic
|
||||
} else {
|
||||
§ions
|
||||
};
|
||||
let container = if metadata_only { §ions_basic } else { §ions };
|
||||
|
||||
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(
|
||||
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());
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) => {
|
||||
{
|
||||
($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
|
||||
|
@ -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
1
rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
use_small_heuristics = "max"
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
160
src/cmd/serve.rs
160
src/cmd/serve.rs
|
@ -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.")?;
|
||||
}
|
||||
|
||||
|
@ -194,7 +217,7 @@ pub fn serve(interface: &str, port: u16, output_dir: &str, base_url: &str, confi
|
|||
fs::StaticFiles::new(&static_root)
|
||||
.unwrap()
|
||||
.show_files_listing()
|
||||
.files_listing_renderer(handle_directory)
|
||||
.files_listing_renderer(handle_directory),
|
||||
)
|
||||
})
|
||||
.bind(&address)
|
||||
|
@ -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"),
|
||||
),
|
||||
];
|
||||
|
||||
|
|
|
@ -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,7 +102,8 @@ 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 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)
|
||||
|
|
21
src/main.rs
21
src/main.rs
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,14 +40,12 @@ pub fn ask_url(question: &str, default: &str) -> Result<String> {
|
|||
|
||||
match &*input {
|
||||
"" => Ok(default.to_string()),
|
||||
_ => {
|
||||
match Url::parse(&input) {
|
||||
_ => match Url::parse(&input) {
|
||||
Ok(_) => Ok(input),
|
||||
Err(_) => {
|
||||
println!("Invalid URL: '{}'", input);
|
||||
ask_url(question, default)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue