diff --git a/CHANGELOG.md b/CHANGELOG.md index 65743edb..1c035759 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Breaking - Newlines are now required after the closing `+++` of front-matter +- `resize_image` now returns a map: `{url, static_path}` instead of just the URL so you can follow up with other functions - i18n rework: languages now have their sections in `config.toml` to set up all their options 1. taxonomies don't have a `lang` anymore in the config, you need to declare them in their respective language section 2. the `config` variable in templates has been changed and is now a stripped down language aware version of the previous `config` diff --git a/Cargo.lock b/Cargo.lock index d7b834f5..6e9b889f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1022,6 +1022,7 @@ dependencies = [ name = "imageproc" version = "0.1.0" dependencies = [ + "config", "errors", "image", "lazy_static", @@ -2592,6 +2593,8 @@ dependencies = [ "nom-bibtex", "rendering", "reqwest", + "serde", + "serde_derive", "serde_json", "sha2", "svg_metadata", diff --git a/components/imageproc/Cargo.toml b/components/imageproc/Cargo.toml index a24e8a34..70a9af1a 100644 --- a/components/imageproc/Cargo.toml +++ b/components/imageproc/Cargo.toml @@ -10,7 +10,8 @@ regex = "1.0" tera = "1" image = "0.23" rayon = "1" -webp="0.1.1" +webp = "0.1.1" errors = { path = "../errors" } utils = { path = "../utils" } +config = { path = "../config" } diff --git a/components/imageproc/src/lib.rs b/components/imageproc/src/lib.rs index 2ce000fe..4e04f033 100644 --- a/components/imageproc/src/lib.rs +++ b/components/imageproc/src/lib.rs @@ -11,10 +11,12 @@ use lazy_static::lazy_static; use rayon::prelude::*; use regex::Regex; +use config::Config; use errors::{Error, Result}; use utils::fs as ufs; static RESIZED_SUBDIR: &str = "processed_images"; +const DEFAULT_Q_JPG: u8 = 75; lazy_static! { pub static ref RESIZED_FILENAME: Regex = @@ -51,14 +53,12 @@ impl ResizeOp { match op { "fit_width" => { if width.is_none() { - return Err("op=\"fit_width\" requires a `width` argument".to_string().into()); + return Err("op=\"fit_width\" requires a `width` argument".into()); } } "fit_height" => { if height.is_none() { - return Err("op=\"fit_height\" requires a `height` argument" - .to_string() - .into()); + return Err("op=\"fit_height\" requires a `height` argument".into()); } } "scale" | "fit" | "fill" => { @@ -132,8 +132,6 @@ impl Hash for ResizeOp { } } } -const DEFAULT_Q_JPG: u8 = 75; - /// Thumbnail image format #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Format { @@ -215,6 +213,7 @@ impl Hash for Format { #[derive(Debug, PartialEq, Eq)] pub struct ImageOp { source: String, + input_path: PathBuf, op: ResizeOp, format: Format, /// Hash of the above parameters @@ -227,18 +226,9 @@ pub struct ImageOp { } impl ImageOp { - pub fn new(source: String, op: ResizeOp, format: Format) -> ImageOp { - let mut hasher = DefaultHasher::new(); - hasher.write(source.as_ref()); - op.hash(&mut hasher); - format.hash(&mut hasher); - let hash = hasher.finish(); - - ImageOp { source, op, format, hash, collision_id: 0 } - } - pub fn from_args( source: String, + input_path: PathBuf, op: &str, width: Option, height: Option, @@ -247,18 +237,24 @@ impl ImageOp { ) -> Result { let op = ResizeOp::from_args(op, width, height)?; let format = Format::from_args(&source, format, quality)?; - Ok(Self::new(source, op, format)) + + let mut hasher = DefaultHasher::new(); + hasher.write(source.as_ref()); + op.hash(&mut hasher); + format.hash(&mut hasher); + let hash = hasher.finish(); + + Ok(ImageOp { source, input_path, op, format, hash, collision_id: 0 }) } - fn perform(&self, content_path: &Path, target_path: &Path) -> Result<()> { + fn perform(&self, target_path: &Path) -> Result<()> { use ResizeOp::*; - let src_path = content_path.join(&self.source); - if !ufs::file_stale(&src_path, target_path) { + if !ufs::file_stale(&self.input_path, target_path) { return Ok(()); } - let mut img = image::open(&src_path)?; + let mut img = image::open(&self.input_path)?; let (img_w, img_h) = img.dimensions(); const RESIZE_FILTER: FilterType = FilterType::Lanczos3; @@ -266,8 +262,8 @@ impl ImageOp { let img = match self.op { Scale(w, h) => img.resize_exact(w, h, RESIZE_FILTER), - FitWidth(w) => img.resize(w, u32::max_value(), RESIZE_FILTER), - FitHeight(h) => img.resize(u32::max_value(), h, RESIZE_FILTER), + FitWidth(w) => img.resize(w, u32::MAX, RESIZE_FILTER), + FitHeight(h) => img.resize(u32::MAX, h, RESIZE_FILTER), Fit(w, h) => { if img_w > w || img_h > h { img.resize(w, h, RESIZE_FILTER) @@ -328,14 +324,15 @@ impl ImageOp { } } -/// A strcture into which image operations can be enqueued and then performed. +/// A struct 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. #[derive(Debug)] pub struct Processor { - content_path: PathBuf, - resized_path: PathBuf, - resized_url: String, + /// The base path of the Zola site + base_path: PathBuf, + base_url: String, + output_dir: PathBuf, /// A map of a ImageOps by their stored hash. /// Note that this cannot be a HashSet, because hashset handles collisions and we don't want that, /// we need to be aware of and handle collisions ourselves. @@ -345,30 +342,18 @@ pub struct Processor { } impl Processor { - pub fn new(content_path: PathBuf, static_path: &Path, base_url: &str) -> Processor { + pub fn new(base_path: PathBuf, config: &Config) -> Processor { Processor { - content_path, - resized_path: static_path.join(RESIZED_SUBDIR), - resized_url: Self::resized_url(base_url), + output_dir: base_path.join("static").join(RESIZED_SUBDIR), + base_url: config.make_permalink(RESIZED_SUBDIR), + base_path, img_ops: HashMap::new(), img_ops_collisions: Vec::new(), } } - fn resized_url(base_url: &str) -> String { - if base_url.ends_with('/') { - format!("{}{}", base_url, RESIZED_SUBDIR) - } else { - format!("{}/{}", base_url, RESIZED_SUBDIR) - } - } - - pub fn set_base_url(&mut self, base_url: &str) { - self.resized_url = Self::resized_url(base_url); - } - - pub fn source_exists(&self, source: &str) -> bool { - self.content_path.join(source).exists() + pub fn set_base_url(&mut self, config: &Config) { + self.base_url = config.make_permalink(RESIZED_SUBDIR); } pub fn num_img_ops(&self) -> usize { @@ -427,25 +412,25 @@ impl Processor { format!("{:016x}{:02x}.{}", hash, collision_id, format.extension()) } - fn op_url(&self, hash: u64, collision_id: u32, format: Format) -> String { - format!("{}/{}", &self.resized_url, Self::op_filename(hash, collision_id, format)) - } - - pub fn insert(&mut self, img_op: ImageOp) -> String { + /// Adds the given operation to the queue but do not process it immediately. + /// Returns (path in static folder, final URL). + pub fn insert(&mut self, img_op: ImageOp) -> (PathBuf, String) { let hash = img_op.hash; let format = img_op.format; let collision_id = self.insert_with_collisions(img_op); - self.op_url(hash, collision_id, format) + let filename = Self::op_filename(hash, collision_id, format); + let url = format!("{}{}", self.base_url, filename); + (Path::new("static").join(RESIZED_SUBDIR).join(filename), url) } pub fn prune(&self) -> Result<()> { // Do not create folders if they don't exist - if !self.resized_path.exists() { + if !self.output_dir.exists() { return Ok(()); } - ufs::ensure_directory_exists(&self.resized_path)?; - let entries = fs::read_dir(&self.resized_path)?; + ufs::ensure_directory_exists(&self.output_dir)?; + let entries = fs::read_dir(&self.output_dir)?; for entry in entries { let entry_path = entry?.path(); if entry_path.is_file() { @@ -466,15 +451,15 @@ impl Processor { pub fn do_process(&mut self) -> Result<()> { if !self.img_ops.is_empty() { - ufs::ensure_directory_exists(&self.resized_path)?; + ufs::ensure_directory_exists(&self.output_dir)?; } self.img_ops .par_iter() .map(|(hash, op)| { let target = - self.resized_path.join(Self::op_filename(*hash, op.collision_id, op.format)); - op.perform(&self.content_path, &target) + self.output_dir.join(Self::op_filename(*hash, op.collision_id, op.format)); + op.perform(&target) .map_err(|e| Error::chain(format!("Failed to process image: {}", op.source), e)) }) .collect::>() diff --git a/components/library/src/content/mod.rs b/components/library/src/content/mod.rs index dc00271b..161a1ab9 100644 --- a/components/library/src/content/mod.rs +++ b/components/library/src/content/mod.rs @@ -3,11 +3,15 @@ mod page; mod section; mod ser; +use std::fs::read_dir; +use std::path::{Path, PathBuf}; + pub use self::file_info::FileInfo; pub use self::page::Page; pub use self::section::Section; pub use self::ser::{SerializingPage, SerializingSection}; +use config::Config; use rendering::Heading; pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool { @@ -23,9 +27,67 @@ pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool { false } +/// Looks into the current folder for the path and see if there's anything that is not a .md +/// file. Those will be copied next to the rendered .html file +pub fn find_related_assets(path: &Path, config: &Config) -> Vec { + let mut assets = vec![]; + + for entry in read_dir(path).unwrap().filter_map(std::result::Result::ok) { + let entry_path = entry.path(); + if entry_path.is_file() { + match entry_path.extension() { + Some(e) => match e.to_str() { + Some("md") => continue, + _ => assets.push(entry_path.to_path_buf()), + }, + None => continue, + } + } + } + + if let Some(ref globset) = config.ignored_content_globset { + // `find_related_assets` only scans the immediate directory (it is not recursive) so our + // filtering only needs to work against the file_name component, not the full suffix. If + // `find_related_assets` was changed to also return files in subdirectories, we could + // use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter + // 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). + assets = assets + .into_iter() + .filter(|path| match path.file_name() { + None => false, + Some(file) => !globset.is_match(file), + }) + .collect(); + } + + assets +} + #[cfg(test)] mod tests { use super::*; + use std::fs::File; + + use config::Config; + use tempfile::tempdir; + + #[test] + fn can_find_related_assets() { + let tmp_dir = tempdir().expect("create temp dir"); + File::create(tmp_dir.path().join("index.md")).unwrap(); + File::create(tmp_dir.path().join("example.js")).unwrap(); + File::create(tmp_dir.path().join("graph.jpg")).unwrap(); + File::create(tmp_dir.path().join("fail.png")).unwrap(); + + let assets = find_related_assets(tmp_dir.path(), &Config::default()); + assert_eq!(assets.len(), 3); + assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3); + assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").count(), 1); + assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1); + assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1); + } #[test] fn can_find_anchor_at_root() { diff --git a/components/library/src/content/page.rs b/components/library/src/content/page.rs index d0937187..9fa4c2a1 100644 --- a/components/library/src/content/page.rs +++ b/components/library/src/content/page.rs @@ -12,14 +12,14 @@ use config::Config; use errors::{Error, Result}; use front_matter::{split_page_content, InsertAnchor, PageFrontMatter}; use rendering::{render_content, Heading, RenderContext}; -use utils::fs::{find_related_assets, read_file}; use utils::site::get_reading_analytics; +use utils::slugs::slugify_paths; use utils::templates::render_template; use crate::content::file_info::FileInfo; -use crate::content::has_anchor; use crate::content::ser::SerializingPage; -use utils::slugs::slugify_paths; +use crate::content::{find_related_assets, has_anchor}; +use utils::fs::read_file; lazy_static! { // Based on https://regex101.com/r/H2n38Z/1/tests @@ -43,7 +43,7 @@ pub struct Page { pub raw_content: String, /// All the non-md files we found next to the .md file pub assets: Vec, - /// All the non-md files we found next to the .md file as string for use in templates + /// All the non-md files we found next to the .md file pub serialized_assets: Vec, /// The HTML rendered of the page pub content: String, @@ -216,27 +216,7 @@ impl Page { if page.file.name == "index" { let parent_dir = path.parent().unwrap(); - let assets = find_related_assets(parent_dir); - - if let Some(ref globset) = config.ignored_content_globset { - // `find_related_assets` only scans the immediate directory (it is not recursive) so our - // filtering only needs to work against the file_name component, not the full suffix. If - // `find_related_assets` was changed to also return files in subdirectories, we could - // use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter - // against the remaining path. Note that the current behaviour effectively means that - // the `ignored_content` setting in the config file is limited to single-file glob - // patterns (no "**" patterns). - page.assets = assets - .into_iter() - .filter(|path| match path.file_name() { - None => false, - Some(file) => !globset.is_match(file), - }) - .collect(); - } else { - page.assets = assets; - } - + page.assets = find_related_assets(parent_dir, config); page.serialized_assets = page.serialize_assets(&base_path); } else { page.assets = vec![]; diff --git a/components/library/src/content/section.rs b/components/library/src/content/section.rs index 3d4fb6fd..a2d693ad 100644 --- a/components/library/src/content/section.rs +++ b/components/library/src/content/section.rs @@ -8,13 +8,13 @@ use config::Config; use errors::{Error, Result}; use front_matter::{split_section_content, SectionFrontMatter}; use rendering::{render_content, Heading, RenderContext}; -use utils::fs::{find_related_assets, read_file}; +use utils::fs::read_file; use utils::site::get_reading_analytics; use utils::templates::render_template; use crate::content::file_info::FileInfo; -use crate::content::has_anchor; use crate::content::ser::SerializingSection; +use crate::content::{find_related_assets, has_anchor}; use crate::library::Library; // Default is used to create a default index section if there is no _index.md in the root content directory @@ -36,7 +36,7 @@ pub struct Section { pub content: String, /// All the non-md files we found next to the .md file pub assets: Vec, - /// All the non-md files we found next to the .md file as string for use in templates + /// All the non-md files we found next to the .md file as string pub serialized_assets: Vec, /// All direct pages of that section pub pages: Vec, @@ -122,27 +122,7 @@ impl Section { let mut section = Section::parse(path, &content, config, base_path)?; let parent_dir = path.parent().unwrap(); - let assets = find_related_assets(parent_dir); - - if let Some(ref globset) = config.ignored_content_globset { - // `find_related_assets` only scans the immediate directory (it is not recursive) so our - // filtering only needs to work against the file_name component, not the full suffix. If - // `find_related_assets` was changed to also return files in subdirectories, we could - // use `PathBuf.strip_prefix` to remove the parent directory and then glob-filter - // against the remaining path. Note that the current behaviour effectively means that - // the `ignored_content` setting in the config file is limited to single-file glob - // patterns (no "**" patterns). - section.assets = assets - .into_iter() - .filter(|path| match path.file_name() { - None => false, - Some(file) => !globset.is_match(file), - }) - .collect(); - } else { - section.assets = assets; - } - + section.assets = find_related_assets(parent_dir, config); section.serialized_assets = section.serialize_assets(); Ok(section) diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index a69bf39b..37d8c32c 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -85,8 +85,7 @@ 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(path.to_path_buf(), &config); let output_path = path.join(config.output_dir.clone()); let site = Site { @@ -152,9 +151,9 @@ impl Site { } pub fn set_base_url(&mut self, base_url: String) { - let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)"); - imageproc.set_base_url(&base_url); self.config.base_url = base_url; + let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)"); + imageproc.set_base_url(&self.config); } pub fn set_output_path>(&mut self, path: P) { diff --git a/components/site/src/tpls.rs b/components/site/src/tpls.rs index 19c6f7ff..0c3ec9d8 100644 --- a/components/site/src/tpls.rs +++ b/components/site/src/tpls.rs @@ -21,11 +21,13 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> { vec![site.static_path.clone(), site.output_path.clone(), site.content_path.clone()], ), ); - site.tera - .register_function("resize_image", global_fns::ResizeImage::new(site.imageproc.clone())); + site.tera.register_function( + "resize_image", + global_fns::ResizeImage::new(site.base_path.clone(), site.imageproc.clone()), + ); site.tera.register_function( "get_image_metadata", - global_fns::GetImageMeta::new(site.content_path.clone()), + global_fns::GetImageMetadata::new(site.base_path.clone()), ); site.tera.register_function("load_data", global_fns::LoadData::new(site.base_path.clone())); site.tera.register_function("trans", global_fns::Trans::new(site.config.clone())); diff --git a/components/templates/Cargo.toml b/components/templates/Cargo.toml index 8baca786..385c1e97 100644 --- a/components/templates/Cargo.toml +++ b/components/templates/Cargo.toml @@ -11,7 +11,9 @@ lazy_static = "1" toml = "0.5" csv = "1" image = "0.23" -serde_json = "1.0" +serde = "1" +serde_json = "1" +serde_derive = "1" sha2 = "0.9" url = "2" nom-bibtex = "0.3" diff --git a/components/templates/gutenberg.jpg b/components/templates/gutenberg.jpg new file mode 100644 index 00000000..0b031e9c Binary files /dev/null and b/components/templates/gutenberg.jpg differ diff --git a/components/templates/src/global_fns/images.rs b/components/templates/src/global_fns/images.rs index d225ecdb..ca85d839 100644 --- a/components/templates/src/global_fns/images.rs +++ b/components/templates/src/global_fns/images.rs @@ -4,16 +4,31 @@ use std::path::PathBuf; use std::sync::{Arc, Mutex}; use image::GenericImageView; +use serde_derive::{Deserialize, Serialize}; use svg_metadata as svg; use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value}; +#[derive(Debug, Serialize, Deserialize)] +struct ResizeImageResponse { + /// The final URL for that asset + url: String, + /// The path to the static asset generated + static_path: String, +} + #[derive(Debug)] pub struct ResizeImage { + /// The base path of the Zola site + base_path: PathBuf, + search_paths: [PathBuf; 2], imageproc: Arc>, } + impl ResizeImage { - pub fn new(imageproc: Arc>) -> Self { - Self { imageproc } + pub fn new(base_path: PathBuf, imageproc: Arc>) -> Self { + let search_paths = + [base_path.join("static").to_path_buf(), base_path.join("content").to_path_buf()]; + Self { base_path, imageproc, search_paths } } } @@ -22,7 +37,7 @@ static DEFAULT_FMT: &str = "auto"; impl TeraFn for ResizeImage { fn call(&self, args: &HashMap) -> Result { - let path = required_arg!( + let mut path = required_arg!( String, args.get("path"), "`resize_image` requires a `path` argument with a string value" @@ -53,45 +68,38 @@ impl TeraFn for ResizeImage { } let mut imageproc = self.imageproc.lock().unwrap(); - if !imageproc.source_exists(&path) { + if path.starts_with("@/") { + path = path.replace("@/", "content/"); + } + + let mut file_path = self.base_path.join(&path); + let mut file_exists = file_path.exists(); + if !file_exists { + // we need to search in both search folders now + for dir in &self.search_paths { + let p = dir.join(&path); + if p.exists() { + file_path = p; + file_exists = true; + break; + } + } + } + + if !file_exists { return Err(format!("`resize_image`: Cannot find path: {}", path).into()); } - let imageop = imageproc::ImageOp::from_args(path, &op, width, height, &format, quality) - .map_err(|e| format!("`resize_image`: {}", e))?; - let url = imageproc.insert(imageop); + let imageop = + imageproc::ImageOp::from_args(path, file_path, &op, width, height, &format, quality) + .map_err(|e| format!("`resize_image`: {}", e))?; + let (static_path, url) = imageproc.insert(imageop); - to_value(url).map_err(|err| err.into()) - } -} - -#[derive(Debug)] -pub struct GetImageMeta { - content_path: PathBuf, -} - -impl GetImageMeta { - pub fn new(content_path: PathBuf) -> Self { - Self { content_path } - } -} - -impl TeraFn for GetImageMeta { - fn call(&self, args: &HashMap) -> Result { - let path = required_arg!( - String, - args.get("path"), - "`get_image_metadata` requires a `path` argument with a string value" - ); - let src_path = self.content_path.join(&path); - if !src_path.exists() { - return Err(format!("`get_image_metadata`: Cannot find path: {}", path).into()); - } - let (height, width) = image_dimensions(&src_path)?; - let mut map = tera::Map::new(); - map.insert(String::from("height"), Value::Number(tera::Number::from(height))); - map.insert(String::from("width"), Value::Number(tera::Number::from(width))); - Ok(Value::Object(map)) + to_value(ResizeImageResponse { + static_path: static_path.to_string_lossy().into_owned(), + url, + }) + .map_err(|err| err.into()) } } @@ -112,9 +120,163 @@ fn image_dimensions(path: &PathBuf) -> Result<(u32, u32)> { } } +#[derive(Debug)] +pub struct GetImageMetadata { + /// The base path of the Zola site + base_path: PathBuf, +} + +impl GetImageMetadata { + pub fn new(base_path: PathBuf) -> Self { + Self { base_path } + } +} + +impl TeraFn for GetImageMetadata { + fn call(&self, args: &HashMap) -> Result { + let mut path = required_arg!( + String, + args.get("path"), + "`get_image_metadata` requires a `path` argument with a string value" + ); + if path.starts_with("@/") { + path = path.replace("@/", "content/"); + } + let src_path = self.base_path.join(&path); + if !src_path.exists() { + return Err(format!("`get_image_metadata`: Cannot find path: {}", path).into()); + } + let (height, width) = image_dimensions(&src_path)?; + let mut map = tera::Map::new(); + map.insert(String::from("height"), Value::Number(tera::Number::from(height))); + map.insert(String::from("width"), Value::Number(tera::Number::from(width))); + Ok(Value::Object(map)) + } +} + #[cfg(test)] mod tests { - use super::*; + use super::{GetImageMetadata, ResizeImage}; - // TODO + use std::collections::HashMap; + use std::fs::{copy, create_dir_all}; + + use config::Config; + use std::sync::{Arc, Mutex}; + use tempfile::{tempdir, TempDir}; + use tera::{to_value, Function}; + + fn create_dir_with_image() -> TempDir { + let dir = tempdir().unwrap(); + create_dir_all(dir.path().join("content").join("gallery")).unwrap(); + create_dir_all(dir.path().join("static")).unwrap(); + copy("gutenberg.jpg", dir.path().join("content").join("gutenberg.jpg")).unwrap(); + copy("gutenberg.jpg", dir.path().join("content").join("gallery").join("asset.jpg")) + .unwrap(); + copy("gutenberg.jpg", dir.path().join("static").join("gutenberg.jpg")).unwrap(); + dir + } + + // https://github.com/getzola/zola/issues/788 + // https://github.com/getzola/zola/issues/1035 + #[test] + fn can_resize_image() { + let dir = create_dir_with_image(); + let imageproc = imageproc::Processor::new(dir.path().to_path_buf(), &Config::default()); + + let static_fn = ResizeImage::new(dir.path().to_path_buf(), Arc::new(Mutex::new(imageproc))); + let mut args = HashMap::new(); + args.insert("height".to_string(), to_value(40).unwrap()); + args.insert("width".to_string(), to_value(40).unwrap()); + + // hashing is stable based on filename and params so we can compare with hashes + + // 1. resizing an image in static + args.insert("path".to_string(), to_value("static/gutenberg.jpg").unwrap()); + let data = static_fn.call(&args).unwrap().as_object().unwrap().clone(); + assert_eq!( + data["static_path"], + to_value("static/processed_images/e49f5bd23ec5007c00.jpg").unwrap() + ); + assert_eq!( + data["url"], + to_value("http://a-website.com/processed_images/e49f5bd23ec5007c00.jpg").unwrap() + ); + + // 2. resizing an image in content with a relative path + args.insert("path".to_string(), to_value("content/gutenberg.jpg").unwrap()); + let data = static_fn.call(&args).unwrap().as_object().unwrap().clone(); + assert_eq!( + data["static_path"], + to_value("static/processed_images/32454a1e0243976c00.jpg").unwrap() + ); + assert_eq!( + data["url"], + to_value("http://a-website.com/processed_images/32454a1e0243976c00.jpg").unwrap() + ); + + // 3. resizing an image in content starting with `@/` + args.insert("path".to_string(), to_value("@/gutenberg.jpg").unwrap()); + let data = static_fn.call(&args).unwrap().as_object().unwrap().clone(); + assert_eq!( + data["static_path"], + to_value("static/processed_images/32454a1e0243976c00.jpg").unwrap() + ); + assert_eq!( + data["url"], + to_value("http://a-website.com/processed_images/32454a1e0243976c00.jpg").unwrap() + ); + + // 4. resizing an image with a relative path not starting with static or content + args.insert("path".to_string(), to_value("gallery/asset.jpg").unwrap()); + let data = static_fn.call(&args).unwrap().as_object().unwrap().clone(); + assert_eq!( + data["static_path"], + to_value("static/processed_images/c8aaba7b0593a60b00.jpg").unwrap() + ); + assert_eq!( + data["url"], + to_value("http://a-website.com/processed_images/c8aaba7b0593a60b00.jpg").unwrap() + ); + + // 5. resizing with an absolute path + args.insert("path".to_string(), to_value("/content/gutenberg.jpg").unwrap()); + assert!(static_fn.call(&args).is_err()); + } + + // TODO: consider https://github.com/getzola/zola/issues/1161 + #[test] + fn can_get_image_metadata() { + let dir = create_dir_with_image(); + + let static_fn = GetImageMetadata::new(dir.path().to_path_buf()); + + // Let's test a few scenarii + let mut args = HashMap::new(); + + // 1. a call to something in `static` with a relative path + args.insert("path".to_string(), to_value("static/gutenberg.jpg").unwrap()); + let data = static_fn.call(&args).unwrap().as_object().unwrap().clone(); + assert_eq!(data["height"], to_value(380).unwrap()); + assert_eq!(data["width"], to_value(300).unwrap()); + + // 2. a call to something in `static` with an absolute path is not handled currently + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("/static/gutenberg.jpg").unwrap()); + assert!(static_fn.call(&args).is_err()); + + // 3. a call to something in `content` with a relative path + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("content/gutenberg.jpg").unwrap()); + let data = static_fn.call(&args).unwrap().as_object().unwrap().clone(); + assert_eq!(data["height"], to_value(380).unwrap()); + assert_eq!(data["width"], to_value(300).unwrap()); + + // 4. a call to something in `content` with a @/ path corresponds to + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("@/gutenberg.jpg").unwrap()); + let data = static_fn.call(&args).unwrap().as_object().unwrap().clone(); + assert_eq!(data["height"], to_value(380).unwrap()); + assert_eq!(data["width"], to_value(300).unwrap()); + } } diff --git a/components/templates/src/global_fns/mod.rs b/components/templates/src/global_fns/mod.rs index 12638923..9ee87d11 100644 --- a/components/templates/src/global_fns/mod.rs +++ b/components/templates/src/global_fns/mod.rs @@ -19,7 +19,7 @@ mod load_data; pub use self::content::{GetPage, GetSection, GetTaxonomy, GetTaxonomyUrl}; pub use self::i18n::Trans; -pub use self::images::{GetImageMeta, ResizeImage}; +pub use self::images::{GetImageMetadata, ResizeImage}; pub use self::load_data::LoadData; #[derive(Debug)] diff --git a/components/utils/src/fs.rs b/components/utils/src/fs.rs index afb7a04e..81d07605 100644 --- a/components/utils/src/fs.rs +++ b/components/utils/src/fs.rs @@ -1,5 +1,5 @@ use filetime::{set_file_mtime, FileTime}; -use std::fs::{copy, create_dir_all, metadata, read_dir, File}; +use std::fs::{copy, create_dir_all, metadata, File}; use std::io::prelude::*; use std::path::{Path, PathBuf}; use std::time::SystemTime; @@ -60,27 +60,6 @@ pub fn read_file(path: &Path) -> Result { Ok(content) } -/// Looks into the current folder for the path and see if there's anything that is not a .md -/// file. Those will be copied next to the rendered .html file -pub fn find_related_assets(path: &Path) -> Vec { - let mut assets = vec![]; - - for entry in read_dir(path).unwrap().filter_map(std::result::Result::ok) { - let entry_path = entry.path(); - if entry_path.is_file() { - match entry_path.extension() { - Some(e) => match e.to_str() { - Some("md") => continue, - _ => assets.push(entry_path.to_path_buf()), - }, - None => continue, - } - } - } - - assets -} - /// Copy a file but takes into account where to start the copy as /// there might be folders we need to create on the way. pub fn copy_file(src: &Path, dest: &PathBuf, base_path: &PathBuf, hard_link: bool) -> Result<()> { @@ -204,25 +183,9 @@ mod tests { use std::path::PathBuf; use std::str::FromStr; - use tempfile::{tempdir, tempdir_in}; + use tempfile::tempdir_in; - use super::{copy_file, find_related_assets}; - - #[test] - fn can_find_related_assets() { - let tmp_dir = tempdir().expect("create temp dir"); - File::create(tmp_dir.path().join("index.md")).unwrap(); - File::create(tmp_dir.path().join("example.js")).unwrap(); - File::create(tmp_dir.path().join("graph.jpg")).unwrap(); - File::create(tmp_dir.path().join("fail.png")).unwrap(); - - let assets = find_related_assets(tmp_dir.path()); - assert_eq!(assets.len(), 3); - assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3); - assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").count(), 1); - assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1); - assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1); - } + use super::copy_file; #[test] fn test_copy_file_timestamp_preserved() {