Allow functions to look into current theme static folder

Closes #1416
This commit is contained in:
Vincent Prouillet 2021-06-24 23:12:04 +02:00
parent 0cd1ea9537
commit 545e766818
7 changed files with 123 additions and 73 deletions

View file

@ -23,13 +23,20 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> {
);
site.tera.register_function(
"resize_image",
global_fns::ResizeImage::new(site.base_path.clone(), site.imageproc.clone()),
global_fns::ResizeImage::new(
site.base_path.clone(),
site.imageproc.clone(),
site.config.theme.clone(),
),
);
site.tera.register_function(
"get_image_metadata",
global_fns::GetImageMetadata::new(site.base_path.clone()),
global_fns::GetImageMetadata::new(site.base_path.clone(), site.config.theme.clone()),
);
site.tera.register_function(
"load_data",
global_fns::LoadData::new(site.base_path.clone(), site.config.theme.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(
"get_taxonomy_url",
@ -39,8 +46,10 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> {
site.config.slugify.taxonomies,
),
);
site.tera
.register_function("get_file_hash", global_fns::GetFileHash::new(site.base_path.clone()));
site.tera.register_function(
"get_file_hash",
global_fns::GetFileHash::new(site.base_path.clone(), site.config.theme.clone()),
);
Ok(())
}

View file

@ -111,7 +111,7 @@ impl TeraFn for GetUrl {
}
if cachebust {
match search_for_file(&self.base_path, &path_with_lang)
match search_for_file(&self.base_path, &path_with_lang, &self.config.theme)
.map_err(|e| format!("`get_url`: {}", e))?
.and_then(|(p, _)| fs::File::open(&p).ok())
.and_then(|f| compute_file_hash::<Sha256>(f, false).ok())
@ -141,10 +141,11 @@ impl TeraFn for GetUrl {
#[derive(Debug)]
pub struct GetFileHash {
base_path: PathBuf,
theme: Option<String>,
}
impl GetFileHash {
pub fn new(base_path: PathBuf) -> Self {
Self { base_path }
pub fn new(base_path: PathBuf, theme: Option<String>) -> Self {
Self { base_path, theme }
}
}
@ -168,7 +169,7 @@ impl TeraFn for GetFileHash {
)
.unwrap_or(true);
let file_path = match search_for_file(&self.base_path, &path)
let file_path = match search_for_file(&self.base_path, &path, &self.theme)
.map_err(|e| format!("`get_file_hash`: {}", e))?
{
Some((f, _)) => f,
@ -358,7 +359,7 @@ title = "A title"
#[test]
fn can_get_file_hash_sha256_no_base64() {
let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path());
let static_fn = GetFileHash::new(dir.into_path(), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("sha_type".to_string(), to_value(256).unwrap());
@ -372,7 +373,7 @@ title = "A title"
#[test]
fn can_get_file_hash_sha256_base64() {
let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path());
let static_fn = GetFileHash::new(dir.into_path(), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("sha_type".to_string(), to_value(256).unwrap());
@ -383,7 +384,7 @@ title = "A title"
#[test]
fn can_get_file_hash_sha384_no_base64() {
let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path());
let static_fn = GetFileHash::new(dir.into_path(), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("base64".to_string(), to_value(false).unwrap());
@ -396,7 +397,7 @@ title = "A title"
#[test]
fn can_get_file_hash_sha384() {
let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path());
let static_fn = GetFileHash::new(dir.into_path(), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
assert_eq!(
@ -408,7 +409,7 @@ title = "A title"
#[test]
fn can_get_file_hash_sha512_no_base64() {
let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path());
let static_fn = GetFileHash::new(dir.into_path(), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("sha_type".to_string(), to_value(512).unwrap());
@ -422,7 +423,7 @@ title = "A title"
#[test]
fn can_get_file_hash_sha512() {
let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path());
let static_fn = GetFileHash::new(dir.into_path(), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("app.css").unwrap());
args.insert("sha_type".to_string(), to_value(512).unwrap());
@ -435,7 +436,7 @@ title = "A title"
#[test]
fn error_when_file_not_found_for_hash() {
let dir = create_temp_dir();
let static_fn = GetFileHash::new(dir.into_path());
let static_fn = GetFileHash::new(dir.into_path(), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("doesnt-exist").unwrap());
let err = format!("{}", static_fn.call(&args).unwrap_err());

View file

@ -9,12 +9,20 @@ use utils::fs::is_path_in_directory;
/// 1. base_path + path
/// 2. base_path + static + path
/// 3. base_path + content + path
/// 4. base_path + themes + {current_theme} + static + path
/// A path starting with @/ will replace it with `content/` and a path starting with `/` will have
/// it removed.
/// It also returns the unified path so it can be used as unique hash for a given file.
/// It will error if the file is not contained in the Zola directory.
pub fn search_for_file(base_path: &Path, path: &str) -> Result<Option<(PathBuf, String)>> {
let search_paths = [base_path.join("static"), base_path.join("content")];
pub fn search_for_file(
base_path: &Path,
path: &str,
theme: &Option<String>,
) -> Result<Option<(PathBuf, String)>> {
let mut search_paths = vec![base_path.join("static"), base_path.join("content")];
if let Some(t) = theme {
search_paths.push(base_path.join("themes").join(t).join("static"));
}
let actual_path = if path.starts_with("@/") {
Cow::Owned(path.replace("@/", "content/"))
} else {

View file

@ -10,12 +10,17 @@ use crate::global_fns::helpers::search_for_file;
pub struct ResizeImage {
/// The base path of the Zola site
base_path: PathBuf,
theme: Option<String>,
imageproc: Arc<Mutex<imageproc::Processor>>,
}
impl ResizeImage {
pub fn new(base_path: PathBuf, imageproc: Arc<Mutex<imageproc::Processor>>) -> Self {
Self { base_path, imageproc }
pub fn new(
base_path: PathBuf,
imageproc: Arc<Mutex<imageproc::Processor>>,
theme: Option<String>,
) -> Self {
Self { base_path, imageproc, theme }
}
}
@ -55,7 +60,7 @@ impl TeraFn for ResizeImage {
}
let mut imageproc = self.imageproc.lock().unwrap();
let (file_path, unified_path) = match search_for_file(&self.base_path, &path)
let (file_path, unified_path) = match search_for_file(&self.base_path, &path, &self.theme)
.map_err(|e| format!("`resize_image`: {}", e))?
{
Some(f) => f,
@ -76,12 +81,13 @@ impl TeraFn for ResizeImage {
pub struct GetImageMetadata {
/// The base path of the Zola site
base_path: PathBuf,
theme: Option<String>,
result_cache: Arc<Mutex<HashMap<String, Value>>>,
}
impl GetImageMetadata {
pub fn new(base_path: PathBuf) -> Self {
Self { base_path, result_cache: Arc::new(Mutex::new(HashMap::new())) }
pub fn new(base_path: PathBuf, theme: Option<String>) -> Self {
Self { base_path, result_cache: Arc::new(Mutex::new(HashMap::new())), theme }
}
}
@ -99,7 +105,7 @@ impl TeraFn for GetImageMetadata {
)
.unwrap_or(false);
let (src_path, unified_path) = match search_for_file(&self.base_path, &path)
let (src_path, unified_path) = match search_for_file(&self.base_path, &path, &self.theme)
.map_err(|e| format!("`get_image_metadata`: {}", e))?
{
Some((f, p)) => (f, p),
@ -142,10 +148,16 @@ mod tests {
let dir = tempdir().unwrap();
create_dir_all(dir.path().join("content").join("gallery")).unwrap();
create_dir_all(dir.path().join("static")).unwrap();
create_dir_all(dir.path().join("themes").join("name").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();
copy(
"gutenberg.jpg",
dir.path().join("themes").join("name").join("static").join("in-theme.jpg"),
)
.unwrap();
dir
}
@ -156,7 +168,11 @@ mod tests {
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 static_fn = ResizeImage::new(
dir.path().to_path_buf(),
Arc::new(Mutex::new(imageproc)),
Some("name".to_owned()),
);
let mut args = HashMap::new();
args.insert("height".to_string(), to_value(40).unwrap());
args.insert("width".to_string(), to_value(40).unwrap());
@ -212,6 +228,17 @@ mod tests {
data["url"],
to_value("http://a-website.com/processed_images/6296a3c153f701be00.jpg").unwrap()
);
// 6. Looking up a file in the theme
args.insert("path".to_string(), to_value("in-theme.jpg").unwrap());
assert_eq!(
data["static_path"],
to_value(&format!("{}", static_path.join("6296a3c153f701be00.jpg").display())).unwrap()
);
assert_eq!(
data["url"],
to_value("http://a-website.com/processed_images/6296a3c153f701be00.jpg").unwrap()
);
}
// TODO: consider https://github.com/getzola/zola/issues/1161
@ -219,7 +246,7 @@ mod tests {
fn can_get_image_metadata() {
let dir = create_dir_with_image();
let static_fn = GetImageMetadata::new(dir.path().to_path_buf());
let static_fn = GetImageMetadata::new(dir.path().to_path_buf(), None);
// Let's test a few scenarii
let mut args = HashMap::new();

View file

@ -88,13 +88,14 @@ impl DataSource {
path_arg: Option<String>,
url_arg: Option<String>,
base_path: &Path,
theme: &Option<String>,
) -> Result<Option<Self>> {
if path_arg.is_some() && url_arg.is_some() {
return Err(GET_DATA_ARGUMENT_ERROR_MESSAGE.into());
}
if let Some(path) = path_arg {
return match search_for_file(&base_path, &path)
return match search_for_file(&base_path, &path, &theme)
.map_err(|e| format!("`load_data`: {}", e))?
{
Some((f, _)) => Ok(Some(DataSource::Path(f))),
@ -165,11 +166,12 @@ fn get_output_format_from_args(
#[derive(Debug)]
pub struct LoadData {
base_path: PathBuf,
theme: Option<String>,
client: Arc<Mutex<Client>>,
result_cache: Arc<Mutex<HashMap<u64, Value>>>,
}
impl LoadData {
pub fn new(base_path: PathBuf) -> Self {
pub fn new(base_path: PathBuf, theme: Option<String>) -> Self {
let client = Arc::new(Mutex::new(
Client::builder()
.user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))
@ -177,7 +179,7 @@ impl LoadData {
.expect("reqwest client build"),
));
let result_cache = Arc::new(Mutex::new(HashMap::new()));
Self { base_path, client, result_cache }
Self { base_path, client, result_cache, theme }
}
}
@ -221,8 +223,10 @@ impl TeraFn for LoadData {
};
// If the file doesn't exist, source is None
let data_source =
match (DataSource::from_args(path_arg.clone(), url_arg, &self.base_path), required) {
let data_source = match (
DataSource::from_args(path_arg.clone(), url_arg, &self.base_path, &self.theme),
required,
) {
// If the file was not required, return a Null value to the template
(Ok(None), false) | (Err(_), false) => {
return Ok(Value::Null);
@ -474,7 +478,7 @@ mod tests {
#[test]
fn fails_illegal_method_parameter() {
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")));
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")), None);
let mut args = HashMap::new();
args.insert("url".to_string(), to_value("https://example.com").unwrap());
args.insert("format".to_string(), to_value("plain").unwrap());
@ -501,7 +505,7 @@ mod tests {
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y");
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")));
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")), None);
let mut args = HashMap::new();
args.insert("url".to_string(), to_value(url).unwrap());
args.insert("format".to_string(), to_value("plain").unwrap());
@ -529,7 +533,7 @@ mod tests {
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4yw");
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")));
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")), None);
let mut args = HashMap::new();
args.insert("url".to_string(), to_value(url).unwrap());
args.insert("format".to_string(), to_value("plain").unwrap());
@ -558,7 +562,7 @@ mod tests {
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y");
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")));
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")), None);
let mut args = HashMap::new();
args.insert("url".to_string(), to_value(url).unwrap());
args.insert("format".to_string(), to_value("plain").unwrap());
@ -574,7 +578,7 @@ mod tests {
#[test]
fn fails_when_missing_file() {
let static_fn = LoadData::new(PathBuf::from("../utils"));
let static_fn = LoadData::new(PathBuf::from("../utils"), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("../../../READMEE.md").unwrap());
let result = static_fn.call(&args);
@ -584,7 +588,7 @@ mod tests {
#[test]
fn doesnt_fail_when_missing_file_is_not_required() {
let static_fn = LoadData::new(PathBuf::from("../utils"));
let static_fn = LoadData::new(PathBuf::from("../utils"), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("../../../READMEE.md").unwrap());
args.insert("required".to_string(), to_value(false).unwrap());
@ -603,7 +607,7 @@ mod tests {
.unwrap();
copy(get_test_file("test.css"), dir.path().join("static").join("test.css")).unwrap();
let static_fn = LoadData::new(dir.path().to_path_buf());
let static_fn = LoadData::new(dir.path().to_path_buf(), None);
let mut args = HashMap::new();
let val = if cfg!(windows) { ".hello {}\r\n" } else { ".hello {}\n" };
@ -630,7 +634,7 @@ mod tests {
#[test]
fn cannot_load_outside_base_dir() {
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")));
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("../../README.md").unwrap());
args.insert("format".to_string(), to_value("plain").unwrap());
@ -707,7 +711,7 @@ mod tests {
.create();
let url = format!("{}{}", mockito::server_url(), "/zpydpkjj67");
let static_fn = LoadData::new(PathBuf::new());
let static_fn = LoadData::new(PathBuf::new(), None);
let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("json").unwrap());
@ -724,7 +728,7 @@ mod tests {
.create();
let url = format!("{}{}", mockito::server_url(), "/aazeow0kog");
let static_fn = LoadData::new(PathBuf::new());
let static_fn = LoadData::new(PathBuf::new(), None);
let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("json").unwrap());
@ -745,7 +749,7 @@ mod tests {
.create();
let url = format!("{}{}", mockito::server_url(), "/aazeow0kog");
let static_fn = LoadData::new(PathBuf::new());
let static_fn = LoadData::new(PathBuf::new(), None);
let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("json").unwrap());
@ -772,7 +776,7 @@ mod tests {
.create();
let url = format!("{}{}", mockito::server_url(), "/chu8aizahBiy");
let static_fn = LoadData::new(PathBuf::new());
let static_fn = LoadData::new(PathBuf::new(), None);
let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("json").unwrap());
@ -782,7 +786,7 @@ mod tests {
#[test]
fn can_load_toml() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"));
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.toml").unwrap());
let result = static_fn.call(&args.clone()).unwrap();
@ -802,7 +806,7 @@ mod tests {
#[test]
fn unknown_extension_defaults_to_plain() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"));
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.css").unwrap());
let result = static_fn.call(&args.clone()).unwrap();
@ -817,7 +821,7 @@ mod tests {
#[test]
fn can_override_known_extension_with_format() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"));
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.csv").unwrap());
args.insert("format".to_string(), to_value("plain").unwrap());
@ -835,7 +839,7 @@ mod tests {
#[test]
fn will_use_format_on_unknown_extension() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"));
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.css").unwrap());
args.insert("format".to_string(), to_value("plain").unwrap());
@ -850,7 +854,7 @@ mod tests {
#[test]
fn can_load_csv() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"));
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.csv").unwrap());
let result = static_fn.call(&args.clone()).unwrap();
@ -870,7 +874,7 @@ mod tests {
// Test points to bad csv file with uneven row lengths
#[test]
fn bad_csv_should_result_in_error() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"));
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("uneven_rows.csv").unwrap());
let result = static_fn.call(&args.clone());
@ -890,7 +894,7 @@ mod tests {
#[test]
fn bad_csv_should_result_in_error_even_when_not_required() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"));
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("uneven_rows.csv").unwrap());
args.insert("required".to_string(), to_value(false).unwrap());
@ -911,7 +915,7 @@ mod tests {
#[test]
fn can_load_json() {
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"));
let static_fn = LoadData::new(PathBuf::from("../utils/test-files"), None);
let mut args = HashMap::new();
args.insert("path".to_string(), to_value("test.json").unwrap());
let result = static_fn.call(&args.clone()).unwrap();
@ -937,7 +941,7 @@ mod tests {
.create();
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y3");
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")));
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")), None);
let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("plain").unwrap());
@ -969,7 +973,7 @@ mod tests {
.create();
let url = format!("{}{}", mockito::server_url(), "/kr1zdgbm4y2");
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")));
let static_fn = LoadData::new(PathBuf::from(PathBuf::from("../utils")), None);
let mut args = HashMap::new();
args.insert("url".to_string(), to_value(&url).unwrap());
args.insert("format".to_string(), to_value("plain").unwrap());

View file

@ -93,10 +93,11 @@ logic applies.
1. The base directory is the Zola root directory, where the `config.toml` is
2. For the given path: if it starts with `@/`, replace that with `content/` instead and trim any leading `/`
3. Search in the following 3 locations in this order, returning the first where the file exists:
3. Search in the following 3 or 4 locations in this order, returning the first where the file exists:
a. $base_directory + $path
b. $base_directory + "static/" + $path
c. $base_directory + "content/" + $path
d. $base_directory + "themes" + $theme + "static/" + $path only if using a theme
In practice this means that `@/some/image.jpg`, `/content/some/image.jpg` and `content/some/image.jpg` will point to the
same thing.

View file

@ -288,7 +288,7 @@ pub fn serve(
// Stop right there if we can't bind to the address
let bind_address: SocketAddrV4 = match address.parse() {
Ok(a) => a,
Err(_) => return Err(format!("Invalid address: {}.", address).into())
Err(_) => return Err(format!("Invalid address: {}.", address).into()),
};
if (TcpListener::bind(&bind_address)).is_err() {
return Err(format!("Cannot start server on address {}.", address).into());