2017-05-19 10:29:08 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use std::collections::HashMap;
|
2021-05-18 18:38:14 +00:00
|
|
|
use std::error::Error;
|
2017-05-19 10:29:08 +00:00
|
|
|
|
2021-05-25 01:58:05 +00:00
|
|
|
use crate::{Engines, TemplateInfo};
|
2017-05-19 10:29:08 +00:00
|
|
|
|
|
|
|
use rocket::http::ContentType;
|
2021-02-23 02:12:30 +00:00
|
|
|
use normpath::PathExt;
|
2017-05-19 10:29:08 +00:00
|
|
|
|
2021-05-18 18:38:14 +00:00
|
|
|
pub(crate) type Callback =
|
|
|
|
Box<dyn Fn(&mut Engines) -> Result<(), Box<dyn Error>> + Send + Sync + 'static>;
|
|
|
|
|
2019-09-20 20:43:05 +00:00
|
|
|
pub(crate) struct Context {
|
2017-05-19 10:29:08 +00:00
|
|
|
/// The root of the template directory.
|
2019-09-20 20:43:05 +00:00
|
|
|
pub root: PathBuf,
|
2017-05-19 10:29:08 +00:00
|
|
|
/// Mapping from template name to its information.
|
2019-09-20 20:43:05 +00:00
|
|
|
pub templates: HashMap<String, TemplateInfo>,
|
2018-01-10 19:27:51 +00:00
|
|
|
/// Loaded template engines
|
2019-09-20 20:43:05 +00:00
|
|
|
pub engines: Engines,
|
2017-05-19 10:29:08 +00:00
|
|
|
}
|
|
|
|
|
2021-05-18 18:38:14 +00:00
|
|
|
pub(crate) use self::manager::ContextManager;
|
|
|
|
|
2017-05-19 10:29:08 +00:00
|
|
|
impl Context {
|
2018-01-10 19:27:51 +00:00
|
|
|
/// Load all of the templates at `root`, initialize them using the relevant
|
|
|
|
/// template engine, and store all of the initialized state in a `Context`
|
|
|
|
/// structure, which is returned if all goes well.
|
2021-05-18 18:38:14 +00:00
|
|
|
pub fn initialize(root: &Path, callback: &Callback) -> Option<Context> {
|
2021-02-23 02:12:30 +00:00
|
|
|
let root = match root.normalize() {
|
|
|
|
Ok(root) => root.into_path_buf(),
|
2021-02-19 20:50:03 +00:00
|
|
|
Err(e) => {
|
|
|
|
error!("Invalid template directory '{}': {}.", root.display(), e);
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-05-19 10:29:08 +00:00
|
|
|
let mut templates: HashMap<String, TemplateInfo> = HashMap::new();
|
|
|
|
for ext in Engines::ENABLED_EXTENSIONS {
|
|
|
|
let mut glob_path = root.join("**").join("*");
|
|
|
|
glob_path.set_extension(ext);
|
|
|
|
let glob_path = glob_path.to_str().expect("valid glob path string");
|
|
|
|
|
2019-06-13 02:17:59 +00:00
|
|
|
for path in glob::glob(glob_path).unwrap().filter_map(Result::ok) {
|
2017-05-19 10:29:08 +00:00
|
|
|
let (name, data_type_str) = split_path(&root, &path);
|
|
|
|
if let Some(info) = templates.get(&*name) {
|
|
|
|
warn_!("Template name '{}' does not have a unique path.", name);
|
|
|
|
info_!("Existing path: {:?}", info.path);
|
|
|
|
info_!("Additional path: {:?}", path);
|
|
|
|
warn_!("Using existing path for template '{}'.", name);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let data_type = data_type_str.as_ref()
|
2017-06-20 00:20:10 +00:00
|
|
|
.and_then(|ext| ContentType::from_extension(ext))
|
2021-05-18 18:38:14 +00:00
|
|
|
.unwrap_or(ContentType::Text);
|
2017-05-19 10:29:08 +00:00
|
|
|
|
|
|
|
templates.insert(name, TemplateInfo {
|
2021-05-18 18:38:14 +00:00
|
|
|
path: Some(path.clone()),
|
|
|
|
engine_ext: ext,
|
2018-07-28 16:58:10 +00:00
|
|
|
data_type,
|
2017-05-19 10:29:08 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-18 18:38:14 +00:00
|
|
|
let mut engines = Engines::init(&templates)?;
|
|
|
|
if let Err(e) = callback(&mut engines) {
|
|
|
|
error_!("Template customization callback failed.");
|
|
|
|
error_!("{}", e);
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (name, engine_ext) in engines.templates() {
|
|
|
|
if !templates.contains_key(name) {
|
|
|
|
let data_type = Path::new(name).extension()
|
|
|
|
.and_then(|osstr| osstr.to_str())
|
|
|
|
.and_then(|ext| ContentType::from_extension(ext))
|
|
|
|
.unwrap_or(ContentType::Text);
|
|
|
|
|
|
|
|
let info = TemplateInfo { path: None, engine_ext, data_type };
|
|
|
|
templates.insert(name.to_string(), info);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(Context { root, templates, engines })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(not(debug_assertions))]
|
|
|
|
mod manager {
|
|
|
|
use std::ops::Deref;
|
2021-05-25 01:58:05 +00:00
|
|
|
use crate::Context;
|
2021-05-18 18:38:14 +00:00
|
|
|
|
|
|
|
/// Wraps a Context. With `cfg(debug_assertions)` active, this structure
|
|
|
|
/// additionally provides a method to reload the context at runtime.
|
|
|
|
pub(crate) struct ContextManager(Context);
|
|
|
|
|
|
|
|
impl ContextManager {
|
|
|
|
pub fn new(ctxt: Context) -> ContextManager {
|
|
|
|
ContextManager(ctxt)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn context<'a>(&'a self) -> impl Deref<Target=Context> + 'a {
|
|
|
|
&self.0
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_reloading(&self) -> bool {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
mod manager {
|
|
|
|
use std::ops::{Deref, DerefMut};
|
|
|
|
use std::sync::{RwLock, Mutex};
|
|
|
|
use std::sync::mpsc::{channel, Receiver};
|
|
|
|
|
|
|
|
use notify::{raw_watcher, RawEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
|
|
|
|
|
|
|
use super::{Callback, Context};
|
|
|
|
|
|
|
|
/// Wraps a Context. With `cfg(debug_assertions)` active, this structure
|
|
|
|
/// additionally provides a method to reload the context at runtime.
|
|
|
|
pub(crate) struct ContextManager {
|
|
|
|
/// The current template context, inside an RwLock so it can be updated.
|
|
|
|
context: RwLock<Context>,
|
|
|
|
/// A filesystem watcher and the receive queue for its events.
|
|
|
|
watcher: Option<(RecommendedWatcher, Mutex<Receiver<RawEvent>>)>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ContextManager {
|
|
|
|
pub fn new(ctxt: Context) -> ContextManager {
|
|
|
|
let (tx, rx) = channel();
|
|
|
|
let watcher = raw_watcher(tx).and_then(|mut watcher| {
|
|
|
|
watcher.watch(ctxt.root.canonicalize()?, RecursiveMode::Recursive)?;
|
|
|
|
Ok(watcher)
|
|
|
|
});
|
|
|
|
|
|
|
|
let watcher = match watcher {
|
|
|
|
Ok(watcher) => Some((watcher, Mutex::new(rx))),
|
|
|
|
Err(e) => {
|
|
|
|
warn!("Failed to enable live template reloading: {}", e);
|
|
|
|
debug_!("Reload error: {:?}", e);
|
|
|
|
warn_!("Live template reloading is unavailable.");
|
|
|
|
None
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
ContextManager { watcher, context: RwLock::new(ctxt), }
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn context(&self) -> impl Deref<Target=Context> + '_ {
|
|
|
|
self.context.read().unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_reloading(&self) -> bool {
|
|
|
|
self.watcher.is_some()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn context_mut(&self) -> impl DerefMut<Target=Context> + '_ {
|
|
|
|
self.context.write().unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Checks whether any template files have changed on disk. If there
|
|
|
|
/// have been changes since the last reload, all templates are
|
|
|
|
/// reinitialized from disk and the user's customization callback is run
|
|
|
|
/// again.
|
|
|
|
pub fn reload_if_needed(&self, callback: &Callback) {
|
|
|
|
let templates_changes = self.watcher.as_ref()
|
|
|
|
.map(|(_, rx)| rx.lock().expect("fsevents lock").try_iter().count() > 0);
|
|
|
|
|
|
|
|
if let Some(true) = templates_changes {
|
|
|
|
info_!("Change detected: reloading templates.");
|
|
|
|
let root = self.context().root.clone();
|
|
|
|
if let Some(new_ctxt) = Context::initialize(&root, &callback) {
|
|
|
|
*self.context_mut() = new_ctxt;
|
|
|
|
} else {
|
|
|
|
warn_!("An error occurred while reloading templates.");
|
|
|
|
warn_!("Existing templates will remain active.");
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2017-05-19 10:29:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Removes the file path's extension or does nothing if there is none.
|
2021-02-19 20:50:03 +00:00
|
|
|
fn remove_extension(path: &Path) -> PathBuf {
|
2017-05-19 10:29:08 +00:00
|
|
|
let stem = match path.file_stem() {
|
|
|
|
Some(stem) => stem,
|
|
|
|
None => return path.to_path_buf()
|
|
|
|
};
|
|
|
|
|
|
|
|
match path.parent() {
|
|
|
|
Some(parent) => parent.join(stem),
|
|
|
|
None => PathBuf::from(stem)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Splits a path into a name that may be used to identify the template, and the
|
|
|
|
/// template's data type, if any.
|
|
|
|
fn split_path(root: &Path, path: &Path) -> (String, Option<String>) {
|
|
|
|
let rel_path = path.strip_prefix(root).unwrap().to_path_buf();
|
|
|
|
let path_no_ext = remove_extension(&rel_path);
|
|
|
|
let data_type = path_no_ext.extension();
|
|
|
|
let mut name = remove_extension(&path_no_ext).to_string_lossy().into_owned();
|
|
|
|
|
|
|
|
// Ensure template name consistency on Windows systems
|
|
|
|
if cfg!(windows) {
|
|
|
|
name = name.replace("\\", "/");
|
|
|
|
}
|
|
|
|
|
|
|
|
(name, data_type.map(|d| d.to_string_lossy().into_owned()))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn template_path_index_html() {
|
|
|
|
for root in &["/", "/a/b/c/", "/a/b/c/d/", "/a/"] {
|
|
|
|
for filename in &["index.html.hbs", "index.html.tera"] {
|
|
|
|
let path = Path::new(root).join(filename);
|
|
|
|
let (name, data_type) = split_path(Path::new(root), &path);
|
|
|
|
|
|
|
|
assert_eq!(name, "index");
|
|
|
|
assert_eq!(data_type, Some("html".into()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn template_path_subdir_index_html() {
|
|
|
|
for root in &["/", "/a/b/c/", "/a/b/c/d/", "/a/"] {
|
|
|
|
for sub in &["a/", "a/b/", "a/b/c/", "a/b/c/d/"] {
|
|
|
|
for filename in &["index.html.hbs", "index.html.tera"] {
|
|
|
|
let path = Path::new(root).join(sub).join(filename);
|
|
|
|
let (name, data_type) = split_path(Path::new(root), &path);
|
|
|
|
|
|
|
|
let expected_name = format!("{}index", sub);
|
|
|
|
assert_eq!(name, expected_name.as_str());
|
|
|
|
assert_eq!(data_type, Some("html".into()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn template_path_doc_examples() {
|
|
|
|
fn name_for(path: &str) -> String {
|
|
|
|
split_path(Path::new("templates/"), &Path::new("templates/").join(path)).0
|
|
|
|
}
|
|
|
|
|
|
|
|
assert_eq!(name_for("index.html.hbs"), "index");
|
|
|
|
assert_eq!(name_for("index.tera"), "index");
|
|
|
|
assert_eq!(name_for("index.hbs"), "index");
|
|
|
|
assert_eq!(name_for("dir/index.hbs"), "dir/index");
|
|
|
|
assert_eq!(name_for("dir/index.html.tera"), "dir/index");
|
|
|
|
assert_eq!(name_for("index.template.html.hbs"), "index.template");
|
|
|
|
assert_eq!(name_for("subdir/index.template.html.hbs"), "subdir/index.template");
|
|
|
|
}
|
|
|
|
}
|