Revamp the images template functions
This commit is contained in:
parent
b0937fa5b7
commit
7fb99eaa44
|
@ -5,6 +5,7 @@
|
||||||
### Breaking
|
### Breaking
|
||||||
|
|
||||||
- Newlines are now required after the closing `+++` of front-matter
|
- 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
|
- 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
|
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`
|
2. the `config` variable in templates has been changed and is now a stripped down language aware version of the previous `config`
|
||||||
|
|
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -1022,6 +1022,7 @@ dependencies = [
|
||||||
name = "imageproc"
|
name = "imageproc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"config",
|
||||||
"errors",
|
"errors",
|
||||||
"image",
|
"image",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
@ -2592,6 +2593,8 @@ dependencies = [
|
||||||
"nom-bibtex",
|
"nom-bibtex",
|
||||||
"rendering",
|
"rendering",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"svg_metadata",
|
"svg_metadata",
|
||||||
|
|
|
@ -14,3 +14,4 @@ webp="0.1.1"
|
||||||
|
|
||||||
errors = { path = "../errors" }
|
errors = { path = "../errors" }
|
||||||
utils = { path = "../utils" }
|
utils = { path = "../utils" }
|
||||||
|
config = { path = "../config" }
|
||||||
|
|
|
@ -11,10 +11,12 @@ use lazy_static::lazy_static;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
|
use config::Config;
|
||||||
use errors::{Error, Result};
|
use errors::{Error, Result};
|
||||||
use utils::fs as ufs;
|
use utils::fs as ufs;
|
||||||
|
|
||||||
static RESIZED_SUBDIR: &str = "processed_images";
|
static RESIZED_SUBDIR: &str = "processed_images";
|
||||||
|
const DEFAULT_Q_JPG: u8 = 75;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref RESIZED_FILENAME: Regex =
|
pub static ref RESIZED_FILENAME: Regex =
|
||||||
|
@ -51,14 +53,12 @@ impl ResizeOp {
|
||||||
match op {
|
match op {
|
||||||
"fit_width" => {
|
"fit_width" => {
|
||||||
if width.is_none() {
|
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" => {
|
"fit_height" => {
|
||||||
if height.is_none() {
|
if height.is_none() {
|
||||||
return Err("op=\"fit_height\" requires a `height` argument"
|
return Err("op=\"fit_height\" requires a `height` argument".into());
|
||||||
.to_string()
|
|
||||||
.into());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"scale" | "fit" | "fill" => {
|
"scale" | "fit" | "fill" => {
|
||||||
|
@ -132,8 +132,6 @@ impl Hash for ResizeOp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const DEFAULT_Q_JPG: u8 = 75;
|
|
||||||
|
|
||||||
/// Thumbnail image format
|
/// Thumbnail image format
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Format {
|
pub enum Format {
|
||||||
|
@ -215,6 +213,7 @@ impl Hash for Format {
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct ImageOp {
|
pub struct ImageOp {
|
||||||
source: String,
|
source: String,
|
||||||
|
input_path: PathBuf,
|
||||||
op: ResizeOp,
|
op: ResizeOp,
|
||||||
format: Format,
|
format: Format,
|
||||||
/// Hash of the above parameters
|
/// Hash of the above parameters
|
||||||
|
@ -227,18 +226,9 @@ pub struct ImageOp {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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(
|
pub fn from_args(
|
||||||
source: String,
|
source: String,
|
||||||
|
input_path: PathBuf,
|
||||||
op: &str,
|
op: &str,
|
||||||
width: Option<u32>,
|
width: Option<u32>,
|
||||||
height: Option<u32>,
|
height: Option<u32>,
|
||||||
|
@ -247,18 +237,24 @@ impl ImageOp {
|
||||||
) -> Result<ImageOp> {
|
) -> Result<ImageOp> {
|
||||||
let op = ResizeOp::from_args(op, width, height)?;
|
let op = ResizeOp::from_args(op, width, height)?;
|
||||||
let format = Format::from_args(&source, format, quality)?;
|
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::*;
|
use ResizeOp::*;
|
||||||
|
|
||||||
let src_path = content_path.join(&self.source);
|
if !ufs::file_stale(&self.input_path, target_path) {
|
||||||
if !ufs::file_stale(&src_path, target_path) {
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut img = image::open(&src_path)?;
|
let mut img = image::open(&self.input_path)?;
|
||||||
let (img_w, img_h) = img.dimensions();
|
let (img_w, img_h) = img.dimensions();
|
||||||
|
|
||||||
const RESIZE_FILTER: FilterType = FilterType::Lanczos3;
|
const RESIZE_FILTER: FilterType = FilterType::Lanczos3;
|
||||||
|
@ -266,8 +262,8 @@ impl ImageOp {
|
||||||
|
|
||||||
let img = match self.op {
|
let img = match self.op {
|
||||||
Scale(w, h) => img.resize_exact(w, h, RESIZE_FILTER),
|
Scale(w, h) => img.resize_exact(w, h, RESIZE_FILTER),
|
||||||
FitWidth(w) => img.resize(w, u32::max_value(), RESIZE_FILTER),
|
FitWidth(w) => img.resize(w, u32::MAX, RESIZE_FILTER),
|
||||||
FitHeight(h) => img.resize(u32::max_value(), h, RESIZE_FILTER),
|
FitHeight(h) => img.resize(u32::MAX, h, RESIZE_FILTER),
|
||||||
Fit(w, h) => {
|
Fit(w, h) => {
|
||||||
if img_w > w || img_h > h {
|
if img_w > w || img_h > h {
|
||||||
img.resize(w, h, RESIZE_FILTER)
|
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`,
|
/// All output is written in a subdirectory in `static_path`,
|
||||||
/// taking care of file stale status based on timestamps and possible hash collisions.
|
/// taking care of file stale status based on timestamps and possible hash collisions.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Processor {
|
pub struct Processor {
|
||||||
content_path: PathBuf,
|
/// The base path of the Zola site
|
||||||
resized_path: PathBuf,
|
base_path: PathBuf,
|
||||||
resized_url: String,
|
base_url: String,
|
||||||
|
output_dir: PathBuf,
|
||||||
/// A map of a ImageOps by their stored hash.
|
/// 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,
|
/// 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.
|
/// we need to be aware of and handle collisions ourselves.
|
||||||
|
@ -345,30 +342,18 @@ pub struct Processor {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
Processor {
|
||||||
content_path,
|
output_dir: base_path.join("static").join(RESIZED_SUBDIR),
|
||||||
resized_path: static_path.join(RESIZED_SUBDIR),
|
base_url: config.make_permalink(RESIZED_SUBDIR),
|
||||||
resized_url: Self::resized_url(base_url),
|
base_path,
|
||||||
img_ops: HashMap::new(),
|
img_ops: HashMap::new(),
|
||||||
img_ops_collisions: Vec::new(),
|
img_ops_collisions: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resized_url(base_url: &str) -> String {
|
pub fn set_base_url(&mut self, config: &Config) {
|
||||||
if base_url.ends_with('/') {
|
self.base_url = config.make_permalink(RESIZED_SUBDIR);
|
||||||
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 num_img_ops(&self) -> usize {
|
pub fn num_img_ops(&self) -> usize {
|
||||||
|
@ -427,25 +412,25 @@ impl Processor {
|
||||||
format!("{:016x}{:02x}.{}", hash, collision_id, format.extension())
|
format!("{:016x}{:02x}.{}", hash, collision_id, format.extension())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn op_url(&self, hash: u64, collision_id: u32, format: Format) -> String {
|
/// Adds the given operation to the queue but do not process it immediately.
|
||||||
format!("{}/{}", &self.resized_url, Self::op_filename(hash, collision_id, format))
|
/// Returns (path in static folder, final URL).
|
||||||
}
|
pub fn insert(&mut self, img_op: ImageOp) -> (PathBuf, String) {
|
||||||
|
|
||||||
pub fn insert(&mut self, img_op: ImageOp) -> String {
|
|
||||||
let hash = img_op.hash;
|
let hash = img_op.hash;
|
||||||
let format = img_op.format;
|
let format = img_op.format;
|
||||||
let collision_id = self.insert_with_collisions(img_op);
|
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<()> {
|
pub fn prune(&self) -> Result<()> {
|
||||||
// Do not create folders if they don't exist
|
// Do not create folders if they don't exist
|
||||||
if !self.resized_path.exists() {
|
if !self.output_dir.exists() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
ufs::ensure_directory_exists(&self.resized_path)?;
|
ufs::ensure_directory_exists(&self.output_dir)?;
|
||||||
let entries = fs::read_dir(&self.resized_path)?;
|
let entries = fs::read_dir(&self.output_dir)?;
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let entry_path = entry?.path();
|
let entry_path = entry?.path();
|
||||||
if entry_path.is_file() {
|
if entry_path.is_file() {
|
||||||
|
@ -466,15 +451,15 @@ impl Processor {
|
||||||
|
|
||||||
pub fn do_process(&mut self) -> Result<()> {
|
pub fn do_process(&mut self) -> Result<()> {
|
||||||
if !self.img_ops.is_empty() {
|
if !self.img_ops.is_empty() {
|
||||||
ufs::ensure_directory_exists(&self.resized_path)?;
|
ufs::ensure_directory_exists(&self.output_dir)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.img_ops
|
self.img_ops
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.map(|(hash, op)| {
|
.map(|(hash, op)| {
|
||||||
let target =
|
let target =
|
||||||
self.resized_path.join(Self::op_filename(*hash, op.collision_id, op.format));
|
self.output_dir.join(Self::op_filename(*hash, op.collision_id, op.format));
|
||||||
op.perform(&self.content_path, &target)
|
op.perform(&target)
|
||||||
.map_err(|e| Error::chain(format!("Failed to process image: {}", op.source), e))
|
.map_err(|e| Error::chain(format!("Failed to process image: {}", op.source), e))
|
||||||
})
|
})
|
||||||
.collect::<Result<()>>()
|
.collect::<Result<()>>()
|
||||||
|
|
|
@ -3,11 +3,15 @@ mod page;
|
||||||
mod section;
|
mod section;
|
||||||
mod ser;
|
mod ser;
|
||||||
|
|
||||||
|
use std::fs::read_dir;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub use self::file_info::FileInfo;
|
pub use self::file_info::FileInfo;
|
||||||
pub use self::page::Page;
|
pub use self::page::Page;
|
||||||
pub use self::section::Section;
|
pub use self::section::Section;
|
||||||
pub use self::ser::{SerializingPage, SerializingSection};
|
pub use self::ser::{SerializingPage, SerializingSection};
|
||||||
|
|
||||||
|
use config::Config;
|
||||||
use rendering::Heading;
|
use rendering::Heading;
|
||||||
|
|
||||||
pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool {
|
pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool {
|
||||||
|
@ -23,9 +27,67 @@ pub fn has_anchor(headings: &[Heading], anchor: &str) -> bool {
|
||||||
false
|
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<PathBuf> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn can_find_anchor_at_root() {
|
fn can_find_anchor_at_root() {
|
||||||
|
|
|
@ -12,14 +12,14 @@ use config::Config;
|
||||||
use errors::{Error, Result};
|
use errors::{Error, Result};
|
||||||
use front_matter::{split_page_content, InsertAnchor, PageFrontMatter};
|
use front_matter::{split_page_content, InsertAnchor, PageFrontMatter};
|
||||||
use rendering::{render_content, Heading, RenderContext};
|
use rendering::{render_content, Heading, RenderContext};
|
||||||
use utils::fs::{find_related_assets, read_file};
|
|
||||||
use utils::site::get_reading_analytics;
|
use utils::site::get_reading_analytics;
|
||||||
|
use utils::slugs::slugify_paths;
|
||||||
use utils::templates::render_template;
|
use utils::templates::render_template;
|
||||||
|
|
||||||
use crate::content::file_info::FileInfo;
|
use crate::content::file_info::FileInfo;
|
||||||
use crate::content::has_anchor;
|
|
||||||
use crate::content::ser::SerializingPage;
|
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! {
|
lazy_static! {
|
||||||
// Based on https://regex101.com/r/H2n38Z/1/tests
|
// Based on https://regex101.com/r/H2n38Z/1/tests
|
||||||
|
@ -43,7 +43,7 @@ pub struct Page {
|
||||||
pub raw_content: String,
|
pub raw_content: String,
|
||||||
/// All the non-md files we found next to the .md file
|
/// All the non-md files we found next to the .md file
|
||||||
pub assets: Vec<PathBuf>,
|
pub assets: Vec<PathBuf>,
|
||||||
/// 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<String>,
|
pub serialized_assets: Vec<String>,
|
||||||
/// The HTML rendered of the page
|
/// The HTML rendered of the page
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
@ -216,27 +216,7 @@ impl Page {
|
||||||
|
|
||||||
if page.file.name == "index" {
|
if page.file.name == "index" {
|
||||||
let parent_dir = path.parent().unwrap();
|
let parent_dir = path.parent().unwrap();
|
||||||
let assets = find_related_assets(parent_dir);
|
page.assets = find_related_assets(parent_dir, config);
|
||||||
|
|
||||||
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.serialized_assets = page.serialize_assets(&base_path);
|
page.serialized_assets = page.serialize_assets(&base_path);
|
||||||
} else {
|
} else {
|
||||||
page.assets = vec![];
|
page.assets = vec![];
|
||||||
|
|
|
@ -8,13 +8,13 @@ use config::Config;
|
||||||
use errors::{Error, Result};
|
use errors::{Error, Result};
|
||||||
use front_matter::{split_section_content, SectionFrontMatter};
|
use front_matter::{split_section_content, SectionFrontMatter};
|
||||||
use rendering::{render_content, Heading, RenderContext};
|
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::site::get_reading_analytics;
|
||||||
use utils::templates::render_template;
|
use utils::templates::render_template;
|
||||||
|
|
||||||
use crate::content::file_info::FileInfo;
|
use crate::content::file_info::FileInfo;
|
||||||
use crate::content::has_anchor;
|
|
||||||
use crate::content::ser::SerializingSection;
|
use crate::content::ser::SerializingSection;
|
||||||
|
use crate::content::{find_related_assets, has_anchor};
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
|
|
||||||
// Default is used to create a default index section if there is no _index.md in the root content directory
|
// 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,
|
pub content: String,
|
||||||
/// All the non-md files we found next to the .md file
|
/// All the non-md files we found next to the .md file
|
||||||
pub assets: Vec<PathBuf>,
|
pub assets: Vec<PathBuf>,
|
||||||
/// 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<String>,
|
pub serialized_assets: Vec<String>,
|
||||||
/// All direct pages of that section
|
/// All direct pages of that section
|
||||||
pub pages: Vec<DefaultKey>,
|
pub pages: Vec<DefaultKey>,
|
||||||
|
@ -122,27 +122,7 @@ impl Section {
|
||||||
let mut section = Section::parse(path, &content, config, base_path)?;
|
let mut section = Section::parse(path, &content, config, base_path)?;
|
||||||
|
|
||||||
let parent_dir = path.parent().unwrap();
|
let parent_dir = path.parent().unwrap();
|
||||||
let assets = find_related_assets(parent_dir);
|
section.assets = find_related_assets(parent_dir, config);
|
||||||
|
|
||||||
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.serialized_assets = section.serialize_assets();
|
section.serialized_assets = section.serialize_assets();
|
||||||
|
|
||||||
Ok(section)
|
Ok(section)
|
||||||
|
|
|
@ -85,8 +85,7 @@ impl Site {
|
||||||
|
|
||||||
let content_path = path.join("content");
|
let content_path = path.join("content");
|
||||||
let static_path = path.join("static");
|
let static_path = path.join("static");
|
||||||
let imageproc =
|
let imageproc = imageproc::Processor::new(path.to_path_buf(), &config);
|
||||||
imageproc::Processor::new(content_path.clone(), &static_path, &config.base_url);
|
|
||||||
let output_path = path.join(config.output_dir.clone());
|
let output_path = path.join(config.output_dir.clone());
|
||||||
|
|
||||||
let site = Site {
|
let site = Site {
|
||||||
|
@ -152,9 +151,9 @@ impl Site {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_base_url(&mut self, base_url: String) {
|
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;
|
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<P: AsRef<Path>>(&mut self, path: P) {
|
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) {
|
||||||
|
|
|
@ -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()],
|
vec![site.static_path.clone(), site.output_path.clone(), site.content_path.clone()],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
site.tera
|
site.tera.register_function(
|
||||||
.register_function("resize_image", global_fns::ResizeImage::new(site.imageproc.clone()));
|
"resize_image",
|
||||||
|
global_fns::ResizeImage::new(site.base_path.clone(), site.imageproc.clone()),
|
||||||
|
);
|
||||||
site.tera.register_function(
|
site.tera.register_function(
|
||||||
"get_image_metadata",
|
"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("load_data", global_fns::LoadData::new(site.base_path.clone()));
|
||||||
site.tera.register_function("trans", global_fns::Trans::new(site.config.clone()));
|
site.tera.register_function("trans", global_fns::Trans::new(site.config.clone()));
|
||||||
|
|
|
@ -11,7 +11,9 @@ lazy_static = "1"
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
csv = "1"
|
csv = "1"
|
||||||
image = "0.23"
|
image = "0.23"
|
||||||
serde_json = "1.0"
|
serde = "1"
|
||||||
|
serde_json = "1"
|
||||||
|
serde_derive = "1"
|
||||||
sha2 = "0.9"
|
sha2 = "0.9"
|
||||||
url = "2"
|
url = "2"
|
||||||
nom-bibtex = "0.3"
|
nom-bibtex = "0.3"
|
||||||
|
|
BIN
components/templates/gutenberg.jpg
Normal file
BIN
components/templates/gutenberg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
|
@ -4,16 +4,31 @@ use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use svg_metadata as svg;
|
use svg_metadata as svg;
|
||||||
use tera::{from_value, to_value, Error, Function as TeraFn, Result, Value};
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct ResizeImage {
|
pub struct ResizeImage {
|
||||||
|
/// The base path of the Zola site
|
||||||
|
base_path: PathBuf,
|
||||||
|
search_paths: [PathBuf; 2],
|
||||||
imageproc: Arc<Mutex<imageproc::Processor>>,
|
imageproc: Arc<Mutex<imageproc::Processor>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResizeImage {
|
impl ResizeImage {
|
||||||
pub fn new(imageproc: Arc<Mutex<imageproc::Processor>>) -> Self {
|
pub fn new(base_path: PathBuf, imageproc: Arc<Mutex<imageproc::Processor>>) -> Self {
|
||||||
Self { imageproc }
|
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 {
|
impl TeraFn for ResizeImage {
|
||||||
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
|
fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
|
||||||
let path = required_arg!(
|
let mut path = required_arg!(
|
||||||
String,
|
String,
|
||||||
args.get("path"),
|
args.get("path"),
|
||||||
"`resize_image` requires a `path` argument with a string value"
|
"`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();
|
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());
|
return Err(format!("`resize_image`: Cannot find path: {}", path).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageop = imageproc::ImageOp::from_args(path, &op, width, height, &format, quality)
|
let imageop =
|
||||||
|
imageproc::ImageOp::from_args(path, file_path, &op, width, height, &format, quality)
|
||||||
.map_err(|e| format!("`resize_image`: {}", e))?;
|
.map_err(|e| format!("`resize_image`: {}", e))?;
|
||||||
let url = imageproc.insert(imageop);
|
let (static_path, url) = imageproc.insert(imageop);
|
||||||
|
|
||||||
to_value(url).map_err(|err| err.into())
|
to_value(ResizeImageResponse {
|
||||||
}
|
static_path: static_path.to_string_lossy().into_owned(),
|
||||||
}
|
url,
|
||||||
|
})
|
||||||
#[derive(Debug)]
|
.map_err(|err| err.into())
|
||||||
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<String, Value>) -> Result<Value> {
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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<String, Value>) -> Result<Value> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ mod load_data;
|
||||||
|
|
||||||
pub use self::content::{GetPage, GetSection, GetTaxonomy, GetTaxonomyUrl};
|
pub use self::content::{GetPage, GetSection, GetTaxonomy, GetTaxonomyUrl};
|
||||||
pub use self::i18n::Trans;
|
pub use self::i18n::Trans;
|
||||||
pub use self::images::{GetImageMeta, ResizeImage};
|
pub use self::images::{GetImageMetadata, ResizeImage};
|
||||||
pub use self::load_data::LoadData;
|
pub use self::load_data::LoadData;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use filetime::{set_file_mtime, FileTime};
|
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::io::prelude::*;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
@ -60,27 +60,6 @@ pub fn read_file(path: &Path) -> Result<String> {
|
||||||
Ok(content)
|
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<PathBuf> {
|
|
||||||
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
|
/// Copy a file but takes into account where to start the copy as
|
||||||
/// there might be folders we need to create on the way.
|
/// 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<()> {
|
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::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use tempfile::{tempdir, tempdir_in};
|
use tempfile::tempdir_in;
|
||||||
|
|
||||||
use super::{copy_file, find_related_assets};
|
use super::copy_file;
|
||||||
|
|
||||||
#[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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_copy_file_timestamp_preserved() {
|
fn test_copy_file_timestamp_preserved() {
|
||||||
|
|
Loading…
Reference in a new issue