From 1c600bda309e2987c3afe3429c7b1a7fb45a5523 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 18 May 2021 11:38:14 -0700 Subject: [PATCH] Discover manually registered templates. This includes one breaking change: the default Content-Type of templates without an identifying extension is now 'Text'. This is to prevent Tera templates from rendering as HTML without being escaped. Resolves #1637. --- contrib/lib/src/templates/context.rs | 134 +++++++++++++++++- contrib/lib/src/templates/engine.rs | 42 ++++-- contrib/lib/src/templates/fairing.rs | 132 +---------------- .../lib/src/templates/handlebars_templates.rs | 19 +-- contrib/lib/src/templates/metadata.rs | 2 +- contrib/lib/src/templates/mod.rs | 36 +++-- contrib/lib/src/templates/tera_templates.rs | 16 +-- examples/templating/src/hbs.rs | 20 ++- examples/templating/src/main.rs | 2 +- examples/templating/src/tera.rs | 23 ++- examples/templating/src/tests.rs | 1 + examples/templating/templates/hbs/about.hbs | 13 -- .../hbs/error/{404.hbs => 404.html.hbs} | 0 .../hbs/{footer.hbs => footer.html.hbs} | 0 .../hbs/{index.hbs => index.html.hbs} | 0 .../hbs/{layout.hbs => layout.html.hbs} | 0 .../templates/hbs/{nav.hbs => nav.html.hbs} | 0 .../templating/templates/tera/base.html.tera | 9 +- 18 files changed, 251 insertions(+), 198 deletions(-) delete mode 100644 examples/templating/templates/hbs/about.hbs rename examples/templating/templates/hbs/error/{404.hbs => 404.html.hbs} (100%) rename examples/templating/templates/hbs/{footer.hbs => footer.html.hbs} (100%) rename examples/templating/templates/hbs/{index.hbs => index.html.hbs} (100%) rename examples/templating/templates/hbs/{layout.hbs => layout.html.hbs} (100%) rename examples/templating/templates/hbs/{nav.hbs => nav.html.hbs} (100%) diff --git a/contrib/lib/src/templates/context.rs b/contrib/lib/src/templates/context.rs index 97a8fce4..6b9b9fdc 100644 --- a/contrib/lib/src/templates/context.rs +++ b/contrib/lib/src/templates/context.rs @@ -1,11 +1,15 @@ use std::path::{Path, PathBuf}; use std::collections::HashMap; +use std::error::Error; use crate::templates::{Engines, TemplateInfo}; use rocket::http::ContentType; use normpath::PathExt; +pub(crate) type Callback = + Box Result<(), Box> + Send + Sync + 'static>; + pub(crate) struct Context { /// The root of the template directory. pub root: PathBuf, @@ -15,11 +19,13 @@ pub(crate) struct Context { pub engines: Engines, } +pub(crate) use self::manager::ContextManager; + impl Context { /// 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. - pub fn initialize(root: &Path) -> Option { + pub fn initialize(root: &Path, callback: &Callback) -> Option { let root = match root.normalize() { Ok(root) => root.into_path_buf(), Err(e) => { @@ -46,18 +52,134 @@ impl Context { let data_type = data_type_str.as_ref() .and_then(|ext| ContentType::from_extension(ext)) - .unwrap_or(ContentType::HTML); + .unwrap_or(ContentType::Text); templates.insert(name, TemplateInfo { - path: path.to_path_buf(), - extension: ext.to_string(), + path: Some(path.clone()), + engine_ext: ext, data_type, }); } } - Engines::init(&templates) - .map(|engines| Context { root: root.into(), templates, engines } ) + 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; + use crate::templates::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(Context); + + impl ContextManager { + pub fn new(ctxt: Context) -> ContextManager { + ContextManager(ctxt) + } + + pub fn context<'a>(&'a self) -> impl Deref + '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, + /// A filesystem watcher and the receive queue for its events. + watcher: Option<(RecommendedWatcher, Mutex>)>, + } + + 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 + '_ { + self.context.read().unwrap() + } + + pub fn is_reloading(&self) -> bool { + self.watcher.is_some() + } + + fn context_mut(&self) -> impl DerefMut + '_ { + 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."); + }; + } + } } } diff --git a/contrib/lib/src/templates/engine.rs b/contrib/lib/src/templates/engine.rs index cafb39d6..bb999679 100644 --- a/contrib/lib/src/templates/engine.rs +++ b/contrib/lib/src/templates/engine.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::collections::HashMap; use serde::Serialize; @@ -7,10 +8,10 @@ use crate::templates::TemplateInfo; #[cfg(feature = "tera_templates")] use crate::templates::tera::Tera; #[cfg(feature = "handlebars_templates")] use crate::templates::handlebars::Handlebars; -pub(crate) trait Engine: Send + Sync + 'static { +pub(crate) trait Engine: Send + Sync + Sized + 'static { const EXT: &'static str; - fn init(templates: &[(&str, &TemplateInfo)]) -> Option where Self: Sized; + fn init<'a>(templates: impl Iterator) -> Option; fn render(&self, name: &str, context: C) -> Option; } @@ -74,11 +75,11 @@ impl Engines { pub(crate) fn init(templates: &HashMap) -> Option { fn inner(templates: &HashMap) -> Option { let named_templates = templates.iter() - .filter(|&(_, i)| i.extension == E::EXT) - .map(|(k, i)| (k.as_str(), i)) - .collect::>(); + .filter(|&(_, i)| i.engine_ext == E::EXT) + .filter_map(|(k, i)| Some((k.as_str(), i.path.as_ref()?))) + .map(|(k, p)| (k, p.as_path())); - E::init(&*named_templates) + E::init(named_templates) } Some(Engines { @@ -101,20 +102,37 @@ impl Engines { info: &TemplateInfo, context: C ) -> Option { - #[cfg(feature = "tera_templates")] - { - if info.extension == Tera::EXT { + #[cfg(feature = "tera_templates")] { + if info.engine_ext == Tera::EXT { return Engine::render(&self.tera, name, context); } } - #[cfg(feature = "handlebars_templates")] - { - if info.extension == Handlebars::EXT { + #[cfg(feature = "handlebars_templates")] { + if info.engine_ext == Handlebars::EXT { return Engine::render(&self.handlebars, name, context); } } None } + + pub(crate) fn templates(&self) -> impl Iterator { + #[cfg(all(feature = "tera_templates", feature = "handlebars_templates"))] { + self.tera.templates.keys() + .map(|name| (name.as_str(), Tera::EXT)) + .chain(self.handlebars.get_templates().keys() + .map(|name| (name.as_str(), Handlebars::EXT))) + } + + #[cfg(all(feature = "tera_templates", not(feature = "handlebars_templates")))] { + self.tera.templates.keys() + .map(|name| (name.as_str(), Tera::EXT)) + } + + #[cfg(all(feature = "handlebars_templates", not(feature = "tera_templates")))] { + self.handlebars.get_templates().keys() + .map(|name| (name.as_str(), Handlebars::EXT)) + } + } } diff --git a/contrib/lib/src/templates/fairing.rs b/contrib/lib/src/templates/fairing.rs index 3c5e1528..d34bb054 100644 --- a/contrib/lib/src/templates/fairing.rs +++ b/contrib/lib/src/templates/fairing.rs @@ -1,119 +1,9 @@ -use std::error::Error; - use crate::templates::{DEFAULT_TEMPLATE_DIR, Context, Engines}; +use crate::templates::context::{Callback, ContextManager}; use rocket::{Rocket, Build, Orbit}; use rocket::fairing::{self, Fairing, Info, Kind}; -pub(crate) use self::context::ContextManager; - -type Callback = Box Result<(), Box>+ Send + Sync + 'static>; - -#[cfg(not(debug_assertions))] -mod context { - use std::ops::Deref; - use crate::templates::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(Context); - - impl ContextManager { - pub fn new(ctxt: Context) -> ContextManager { - ContextManager(ctxt) - } - - pub fn context<'a>(&'a self) -> impl Deref + 'a { - &self.0 - } - - pub fn is_reloading(&self) -> bool { - false - } - } -} - -#[cfg(debug_assertions)] -mod context { - 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, - /// A filesystem watcher and the receive queue for its events. - watcher: Option<(RecommendedWatcher, Mutex>)>, - } - - 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 + '_ { - self.context.read().unwrap() - } - - pub fn is_reloading(&self) -> bool { - self.watcher.is_some() - } - - fn context_mut(&self) -> impl DerefMut + '_ { - 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(mut new_ctxt) = Context::initialize(&root) { - match callback(&mut new_ctxt.engines) { - Ok(()) => *self.context_mut() = new_ctxt, - Err(e) => { - warn_!("The template customization callback returned an error:"); - warn_!("{}", e); - warn_!("The existing templates will remain active."); - } - } - } else { - warn_!("An error occurred while reloading templates."); - warn_!("The existing templates will remain active."); - }; - } - } - } -} - /// The TemplateFairing initializes the template system on attach, running /// custom_callback after templates have been loaded. In debug mode, the fairing /// checks for modifications to templates before every request and reloads them @@ -155,21 +45,11 @@ impl Fairing for TemplateFairing { } }; - match Context::initialize(&path) { - Some(mut ctxt) => { - match (self.callback)(&mut ctxt.engines) { - Ok(()) => Ok(rocket.manage(ContextManager::new(ctxt))), - Err(e) => { - error_!("The template customization callback returned an error:"); - error_!("{}", e); - Err(rocket) - } - } - } - None => { - error_!("Launch will be aborted due to failed template initialization."); - Err(rocket) - } + if let Some(ctxt) = Context::initialize(&path, &self.callback) { + Ok(rocket.manage(ContextManager::new(ctxt))) + } else { + error_!("Template initialization failed. Aborting launch."); + Err(rocket) } } diff --git a/contrib/lib/src/templates/handlebars_templates.rs b/contrib/lib/src/templates/handlebars_templates.rs index 9d667c76..45edea61 100644 --- a/contrib/lib/src/templates/handlebars_templates.rs +++ b/contrib/lib/src/templates/handlebars_templates.rs @@ -1,25 +1,26 @@ -use serde::Serialize; +use std::path::Path; -use crate::templates::{Engine, TemplateInfo}; +use serde::Serialize; +use crate::templates::Engine; pub use crate::templates::handlebars::Handlebars; impl Engine for Handlebars<'static> { const EXT: &'static str = "hbs"; - fn init(templates: &[(&str, &TemplateInfo)]) -> Option> { + fn init<'a>(templates: impl Iterator) -> Option { let mut hb = Handlebars::new(); - for &(name, info) in templates { - let path = &info.path; + let mut ok = true; + for (name, path) in templates { if let Err(e) = hb.register_template_file(name, path) { - error!("Error in Handlebars template '{}'.", name); - info_!("{}", e); + error!("Handlebars template '{}' failed to register.", name); + error_!("{}", e); info_!("Template path: '{}'.", path.to_string_lossy()); - return None; + ok = false; } } - Some(hb) + ok.then(|| hb) } fn render(&self, name: &str, context: C) -> Option { diff --git a/contrib/lib/src/templates/metadata.rs b/contrib/lib/src/templates/metadata.rs index 3cb81696..aa44194b 100644 --- a/contrib/lib/src/templates/metadata.rs +++ b/contrib/lib/src/templates/metadata.rs @@ -2,7 +2,7 @@ use rocket::{Request, Rocket, Ignite, Sentinel}; use rocket::http::Status; use rocket::request::{self, FromRequest}; -use crate::templates::ContextManager; +use crate::templates::context::ContextManager; /// Request guard for dynamically querying template metadata. /// diff --git a/contrib/lib/src/templates/mod.rs b/contrib/lib/src/templates/mod.rs index ac06ba0f..1341f4a9 100644 --- a/contrib/lib/src/templates/mod.rs +++ b/contrib/lib/src/templates/mod.rs @@ -14,7 +14,7 @@ //! features = ["handlebars_templates", "tera_templates"] //! ``` //! -//! 1. Write your template files in Handlebars (extension: `.hbs`) or tera +//! 1. Write your template files in Handlebars (extension: `.hbs`) and/or tera //! (extensions: `.tera`) in the templates directory (default: //! `{rocket_root}/templates`). //! @@ -34,7 +34,7 @@ //! ``` //! //! 3. Return a [`Template`] using [`Template::render()`], supplying the name -//! of the template file minus the last two extensions, from a handler. +//! of the template file **minus the last two extensions**, from a handler. //! //! ```rust //! # #[macro_use] extern crate rocket; @@ -49,6 +49,25 @@ //! } //! ``` //! +//! ## Naming +//! +//! Templates discovered by Rocket are _renamed_ from their file name to their +//! file name **without the last two extensions**. As such, refer to a template +//! with file name `foo.html.hbs` or `foo.html.tera` as `foo`. See +//! [Discovery](#discovery) for more. +//! +//! Templates that are _not_ discovered by Rocket, such as those registered +//! directly via [`Template::custom()`], are _not_ renamed. +//! +//! ## Content Type +//! +//! The `Content-Type` of the response is automatically determined by the +//! non-engine extension of the template name or `text/plain` if there is no +//! extension or the extension is unknown. For example, for a discovered +//! template with file name `foo.html.hbs` or a manually registered template +//! with name ending in `foo.html`, the `Content-Type` is automatically set to +//! [`ContentType::HTML`]. +//! //! ## Discovery //! //! Template names passed in to [`Template::render()`] must correspond to a @@ -66,10 +85,10 @@ //! | Engine | Version | Extension | //! |--------------|---------|-----------| //! | [Tera] | 1 | `.tera` | -//! | [Handlebars] | 2 | `.hbs` | +//! | [Handlebars] | 3 | `.hbs` | //! //! [Tera]: https://docs.rs/crate/tera/1 -//! [Handlebars]: https://docs.rs/crate/handlebars/2 +//! [Handlebars]: https://docs.rs/crate/handlebars/3 //! //! Any file that ends with one of these extension will be discovered and //! rendered with the corresponding templating engine. The _name_ of the @@ -124,11 +143,10 @@ mod metadata; pub use self::engine::Engines; pub use self::metadata::Metadata; -pub(crate) use self::context::Context; -pub(crate) use self::fairing::ContextManager; use self::engine::Engine; use self::fairing::TemplateFairing; +use self::context::{Context, ContextManager}; use serde::Serialize; use serde_json::{Value, to_value}; @@ -205,10 +223,10 @@ pub struct Template { #[derive(Debug)] pub(crate) struct TemplateInfo { - /// The complete path, including `template_dir`, to this template. - path: PathBuf, + /// The complete path, including `template_dir`, to this template, if any. + path: Option, /// The extension for the engine of this template. - extension: String, + engine_ext: &'static str, /// The extension before the engine extension in the template, if any. data_type: ContentType } diff --git a/contrib/lib/src/templates/tera_templates.rs b/contrib/lib/src/templates/tera_templates.rs index d841b414..7772651e 100644 --- a/contrib/lib/src/templates/tera_templates.rs +++ b/contrib/lib/src/templates/tera_templates.rs @@ -1,26 +1,26 @@ -use serde::Serialize; +use std::path::Path; use std::error::Error; -use crate::templates::{Engine, TemplateInfo}; +use serde::Serialize; +use crate::templates::Engine; pub use crate::templates::tera::{Context, Tera}; impl Engine for Tera { const EXT: &'static str = "tera"; - fn init(templates: &[(&str, &TemplateInfo)]) -> Option { + fn init<'a>(templates: impl Iterator) -> Option { // Create the Tera instance. let mut tera = Tera::default(); let ext = [".html.tera", ".htm.tera", ".xml.tera", ".html", ".htm", ".xml"]; tera.autoescape_on(ext.to_vec()); - // Collect into a tuple of (name, path) for Tera. - let tera_templates = templates.iter() - .map(|&(name, info)| (&info.path, Some(name))) - .collect::>(); + // Collect into a tuple of (name, path) for Tera. If we register one at + // a time, it will complain about unregistered base templates. + let files = templates.map(|(name, path)| (path, Some(name))); // Finally try to tell Tera about all of the templates. - if let Err(e) = tera.add_template_files(tera_templates) { + if let Err(e) = tera.add_template_files(files) { error!("Failed to initialize Tera templating."); let mut error = Some(&e as &dyn Error); diff --git a/examples/templating/src/hbs.rs b/examples/templating/src/hbs.rs index ba94f0ea..2b92541a 100644 --- a/examples/templating/src/hbs.rs +++ b/examples/templating/src/hbs.rs @@ -29,12 +29,10 @@ pub fn hello(name: &str) -> Template { #[get("/about")] pub fn about() -> Template { - Template::render("hbs/about", &TemplateContext { - title: "About", - name: None, - items: vec!["Some", "Important", "Info"], - parent: "hbs/layout", - }) + let mut map = std::collections::HashMap::new(); + map.insert("title", "About"); + map.insert("parent", "hbs/layout"); + Template::render("hbs/about.html", &map) } #[catch(404)] @@ -62,4 +60,14 @@ fn wow_helper( pub fn customize(hbs: &mut Handlebars) { hbs.register_helper("wow", Box::new(wow_helper)); + hbs.register_template_string("hbs/about.html", r#" + {{#*inline "page"}} + +
+

About - Here's another page!

+
+ + {{/inline}} + {{~> (parent)~}} + "#).expect("valid HBS template"); } diff --git a/examples/templating/src/main.rs b/examples/templating/src/main.rs index 92d51287..5154632d 100644 --- a/examples/templating/src/main.rs +++ b/examples/templating/src/main.rs @@ -17,7 +17,7 @@ fn index() -> Html<&'static str> { fn rocket() -> _ { rocket::build() .mount("/", routes![index]) - .mount("/tera", routes![tera::index, tera::hello]) + .mount("/tera", routes![tera::index, tera::hello, tera::about]) .mount("/hbs", routes![hbs::index, hbs::hello, hbs::about]) .register("/hbs", catchers![hbs::not_found]) .register("/tera", catchers![tera::not_found]) diff --git a/examples/templating/src/tera.rs b/examples/templating/src/tera.rs index a4271157..54409f73 100644 --- a/examples/templating/src/tera.rs +++ b/examples/templating/src/tera.rs @@ -7,7 +7,7 @@ use rocket_contrib::templates::{Template, tera::Tera}; #[derive(serde::Serialize)] struct TemplateContext<'r> { title: &'r str, - name: &'r str, + name: Option<&'r str>, items: Vec<&'r str> } @@ -19,12 +19,19 @@ pub fn index() -> Redirect { #[get("/hello/")] pub fn hello(name: &str) -> Template { Template::render("tera/index", &TemplateContext { - name, title: "Hello", + name: Some(name), items: vec!["One", "Two", "Three"], }) } +#[get("/about")] +pub fn about() -> Template { + let mut map = HashMap::new(); + map.insert("title", "About"); + Template::render("tera/about.html", &map) +} + #[catch(404)] pub fn not_found(req: &Request<'_>) -> Template { let mut map = HashMap::new(); @@ -32,6 +39,14 @@ pub fn not_found(req: &Request<'_>) -> Template { Template::render("tera/error/404", &map) } -pub fn customize(_tera: &mut Tera) { - /* register helpers, and so on */ +pub fn customize(tera: &mut Tera) { + tera.add_raw_template("tera/about.html", r#" + {% extends "tera/base" %} + + {% block content %} +
+

About - Here's another page!

+
+ {% endblock content %} + "#).expect("valid Tera template"); } diff --git a/examples/templating/src/tests.rs b/examples/templating/src/tests.rs index 50b44be9..076bcb8a 100644 --- a/examples/templating/src/tests.rs +++ b/examples/templating/src/tests.rs @@ -82,4 +82,5 @@ fn tera() { test_root("tera"); test_name("tera"); test_404("tera"); + test_about("tera"); } diff --git a/examples/templating/templates/hbs/about.hbs b/examples/templating/templates/hbs/about.hbs deleted file mode 100644 index 45c042c2..00000000 --- a/examples/templating/templates/hbs/about.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{#*inline "page"}} - -
-

About - Here's another page!

-
    - {{#each items}} -
  • {{ this }}
  • - {{/each}} -
-
- -{{/inline}} -{{~> (parent)~}} diff --git a/examples/templating/templates/hbs/error/404.hbs b/examples/templating/templates/hbs/error/404.html.hbs similarity index 100% rename from examples/templating/templates/hbs/error/404.hbs rename to examples/templating/templates/hbs/error/404.html.hbs diff --git a/examples/templating/templates/hbs/footer.hbs b/examples/templating/templates/hbs/footer.html.hbs similarity index 100% rename from examples/templating/templates/hbs/footer.hbs rename to examples/templating/templates/hbs/footer.html.hbs diff --git a/examples/templating/templates/hbs/index.hbs b/examples/templating/templates/hbs/index.html.hbs similarity index 100% rename from examples/templating/templates/hbs/index.hbs rename to examples/templating/templates/hbs/index.html.hbs diff --git a/examples/templating/templates/hbs/layout.hbs b/examples/templating/templates/hbs/layout.html.hbs similarity index 100% rename from examples/templating/templates/hbs/layout.hbs rename to examples/templating/templates/hbs/layout.html.hbs diff --git a/examples/templating/templates/hbs/nav.hbs b/examples/templating/templates/hbs/nav.html.hbs similarity index 100% rename from examples/templating/templates/hbs/nav.hbs rename to examples/templating/templates/hbs/nav.html.hbs diff --git a/examples/templating/templates/tera/base.html.tera b/examples/templating/templates/tera/base.html.tera index 8f3db199..df807cf0 100644 --- a/examples/templating/templates/tera/base.html.tera +++ b/examples/templating/templates/tera/base.html.tera @@ -5,9 +5,12 @@ Tera Demo - {{ title }} + Hello | About + {% block content %}{% endblock content %} + + -