mirror of https://github.com/rwf2/Rocket.git
Support minijinja in 'dyn_templates'.
This commit is contained in:
parent
8c273edb44
commit
57b6b8007c
|
@ -13,28 +13,26 @@ edition = "2021"
|
|||
rust-version = "1.64"
|
||||
|
||||
[features]
|
||||
tera = ["tera_"]
|
||||
handlebars = ["handlebars_"]
|
||||
tera = ["dep:tera"]
|
||||
handlebars = ["dep:handlebars"]
|
||||
minijinja = ["dep:minijinja"]
|
||||
|
||||
[dependencies]
|
||||
walkdir = "2.4"
|
||||
notify = "6"
|
||||
normpath = "1"
|
||||
|
||||
tera = { version = "1.19.0", optional = true }
|
||||
handlebars = { version = "5.1", optional = true }
|
||||
minijinja = { version = "1.0.16", optional = true, features = ["loader"] }
|
||||
|
||||
[dependencies.rocket]
|
||||
version = "0.5.0"
|
||||
path = "../../core/lib"
|
||||
default-features = false
|
||||
|
||||
[dependencies.tera_]
|
||||
package = "tera"
|
||||
version = "1.10.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.handlebars_]
|
||||
package = "handlebars"
|
||||
version = "5.1"
|
||||
optional = true
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
|
|
@ -2,7 +2,8 @@ use std::path::{Path, PathBuf};
|
|||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
|
||||
use crate::{Engines, TemplateInfo};
|
||||
use crate::engine::Engines;
|
||||
use crate::template::TemplateInfo;
|
||||
|
||||
use rocket::http::ContentType;
|
||||
use normpath::PathExt;
|
||||
|
@ -99,7 +100,7 @@ impl Context {
|
|||
#[cfg(not(debug_assertions))]
|
||||
mod manager {
|
||||
use std::ops::Deref;
|
||||
use crate::Context;
|
||||
use super::Context;
|
||||
|
||||
/// Wraps a Context. With `cfg(debug_assertions)` active, this structure
|
||||
/// additionally provides a method to reload the context at runtime.
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use std::path::Path;
|
||||
|
||||
use handlebars::Handlebars;
|
||||
use rocket::serde::Serialize;
|
||||
|
||||
use crate::engine::Engine;
|
||||
pub use crate::handlebars::Handlebars;
|
||||
|
||||
impl Engine for Handlebars<'static> {
|
||||
const EXT: &'static str = "hbs";
|
|
@ -0,0 +1,55 @@
|
|||
use std::sync::Arc;
|
||||
use std::path::Path;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rocket::serde::Serialize;
|
||||
use minijinja::{Environment, Error, ErrorKind, AutoEscape};
|
||||
|
||||
use crate::engine::Engine;
|
||||
|
||||
impl Engine for Environment<'static> {
|
||||
const EXT: &'static str = "j2";
|
||||
|
||||
fn init<'a>(templates: impl Iterator<Item = (&'a str, &'a Path)>) -> Option<Self> {
|
||||
let _templates = Arc::new(templates
|
||||
.map(|(k, p)| (k.to_owned(), p.to_owned()))
|
||||
.collect::<HashMap<_, _>>());
|
||||
|
||||
let templates = _templates.clone();
|
||||
let mut env = Environment::new();
|
||||
env.set_loader(move |name| {
|
||||
let Some(path) = templates.get(name) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(result) => Ok(Some(result)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(e) => Err(
|
||||
Error::new(ErrorKind::InvalidOperation, "template read failed").with_source(e)
|
||||
),
|
||||
}
|
||||
});
|
||||
|
||||
let templates = _templates.clone();
|
||||
env.set_auto_escape_callback(move |name| {
|
||||
templates.get(name)
|
||||
.and_then(|path| path.to_str())
|
||||
.map(minijinja::default_auto_escape_callback)
|
||||
.unwrap_or(AutoEscape::None)
|
||||
});
|
||||
|
||||
Some(env)
|
||||
}
|
||||
|
||||
fn render<C: Serialize>(&self, name: &str, context: C) -> Option<String> {
|
||||
let Ok(template) = self.get_template(name) else {
|
||||
error_!("Minijinja template '{name}' was not found.");
|
||||
return None;
|
||||
};
|
||||
|
||||
template.render(context)
|
||||
.map_err(|e| error_!("Minijinja: {}", e))
|
||||
.ok()
|
||||
}
|
||||
}
|
|
@ -3,10 +3,22 @@ use std::collections::HashMap;
|
|||
|
||||
use rocket::serde::Serialize;
|
||||
|
||||
use crate::TemplateInfo;
|
||||
use crate::template::TemplateInfo;
|
||||
|
||||
#[cfg(feature = "tera")] use crate::tera::Tera;
|
||||
#[cfg(feature = "handlebars")] use crate::handlebars::Handlebars;
|
||||
#[cfg(feature = "tera")]
|
||||
mod tera;
|
||||
#[cfg(feature = "tera")]
|
||||
use ::tera::Tera;
|
||||
|
||||
#[cfg(feature = "handlebars")]
|
||||
mod handlebars;
|
||||
#[cfg(feature = "handlebars")]
|
||||
use ::handlebars::Handlebars;
|
||||
|
||||
#[cfg(feature = "minijinja")]
|
||||
mod minijinja;
|
||||
#[cfg(feature = "minijinja")]
|
||||
use ::minijinja::Environment;
|
||||
|
||||
pub(crate) trait Engine: Send + Sync + Sized + 'static {
|
||||
const EXT: &'static str;
|
||||
|
@ -52,24 +64,38 @@ pub(crate) trait Engine: Send + Sync + Sized + 'static {
|
|||
/// [`tera::Value`]: crate::tera::Value
|
||||
/// [`tera::Result`]: crate::tera::Result
|
||||
pub struct Engines {
|
||||
/// A `Tera` templating engine. This field is only available when the
|
||||
/// `tera_templates` feature is enabled. When calling methods on the `Tera`
|
||||
/// instance, ensure you use types imported from
|
||||
/// `rocket_dyn_templates::tera` to avoid version mismatches.
|
||||
/// A `Tera` templating engine.
|
||||
///
|
||||
/// This field is only available when the `tera` feature is enabled. When
|
||||
/// calling methods on the `Tera` instance, ensure you use types imported
|
||||
/// from `rocket_dyn_templates::tera` to avoid version mismatches.
|
||||
#[cfg(feature = "tera")]
|
||||
pub tera: Tera,
|
||||
/// The Handlebars templating engine. This field is only available when the
|
||||
/// `handlebars_templates` feature is enabled. When calling methods on the
|
||||
/// `Handlebars` instance, ensure you use types imported from
|
||||
/// `rocket_dyn_templates::handlebars` to avoid version mismatches.
|
||||
|
||||
/// The Handlebars templating engine.
|
||||
///
|
||||
/// This field is only available when the `handlebars` feature is enabled.
|
||||
/// When calling methods on the `Handlebars` instance, ensure you use types
|
||||
/// imported from `rocket_dyn_templates::handlebars` to avoid version
|
||||
/// mismatches.
|
||||
#[cfg(feature = "handlebars")]
|
||||
pub handlebars: Handlebars<'static>,
|
||||
|
||||
/// The minijinja templating engine.
|
||||
///
|
||||
/// This field is only available when the `minijinja` feature is enabled.
|
||||
/// When calling methods on the [`Environment`] instance, ensure you use
|
||||
/// types imported from `rocket_dyn_templates::minijinja` to avoid version
|
||||
/// mismatches.
|
||||
#[cfg(feature = "minijinja")]
|
||||
pub minijinja: Environment<'static>,
|
||||
}
|
||||
|
||||
impl Engines {
|
||||
pub(crate) const ENABLED_EXTENSIONS: &'static [&'static str] = &[
|
||||
#[cfg(feature = "tera")] Tera::EXT,
|
||||
#[cfg(feature = "handlebars")] Handlebars::EXT,
|
||||
#[cfg(feature = "minijinja")] Environment::EXT,
|
||||
];
|
||||
|
||||
pub(crate) fn init(templates: &HashMap<String, TemplateInfo>) -> Option<Engines> {
|
||||
|
@ -93,6 +119,11 @@ impl Engines {
|
|||
Some(hb) => hb,
|
||||
None => return None
|
||||
},
|
||||
#[cfg(feature = "minijinja")]
|
||||
minijinja: match inner::<Environment<'static>>(templates) {
|
||||
Some(hb) => hb,
|
||||
None => return None
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -100,7 +131,7 @@ impl Engines {
|
|||
&self,
|
||||
name: &str,
|
||||
info: &TemplateInfo,
|
||||
context: C
|
||||
context: C,
|
||||
) -> Option<String> {
|
||||
#[cfg(feature = "tera")] {
|
||||
if info.engine_ext == Tera::EXT {
|
||||
|
@ -114,6 +145,12 @@ impl Engines {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "minijinja")] {
|
||||
if info.engine_ext == Environment::EXT {
|
||||
return Engine::render(&self.minijinja, name, context);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
use std::path::Path;
|
||||
use std::error::Error;
|
||||
|
||||
use tera::{Context, Tera};
|
||||
use rocket::serde::Serialize;
|
||||
|
||||
use crate::engine::Engine;
|
||||
|
||||
pub use crate::tera::{Context, Tera};
|
||||
|
||||
impl Engine for Tera {
|
||||
const EXT: &'static str = "tera";
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::{DEFAULT_TEMPLATE_DIR, Context, Engines};
|
||||
use crate::context::{Callback, ContextManager};
|
||||
|
||||
use rocket::{Rocket, Build, Orbit};
|
||||
use rocket::fairing::{self, Fairing, Info, Kind};
|
||||
|
||||
use crate::context::{Callback, Context, ContextManager};
|
||||
use crate::template::DEFAULT_TEMPLATE_DIR;
|
||||
use crate::engine::Engines;
|
||||
|
||||
/// 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
|
||||
|
|
|
@ -7,22 +7,34 @@
|
|||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! 1. Enable the `rocket_dyn_templates` feature corresponding to your
|
||||
//! templating engine(s) of choice:
|
||||
//! 1. Depend on `rocket_dyn_templates`. Enable the feature(s) corresponding
|
||||
//! to your templating engine(s) of choice:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies.rocket_dyn_templates]
|
||||
//! version = "0.1.0"
|
||||
//! features = ["handlebars", "tera"]
|
||||
//! features = ["handlebars", "tera", "minijinja"]
|
||||
//! ```
|
||||
//!
|
||||
//! 1. Write your template files in Handlebars (`.hbs`) and/or Tera (`.tera`)
|
||||
//! in the configurable `template_dir` directory (default:
|
||||
//! `{rocket_root}/templates`).
|
||||
//! 2. Write your templates inside of the [configurable]
|
||||
//! `${ROCKET_ROOT}/templates`. The filename _must_ end with an extension
|
||||
//! corresponding to an enabled engine. The second-to-last extension should
|
||||
//! correspond to the file's type:
|
||||
//!
|
||||
//! 2. Attach `Template::fairing()` return a `Template` using
|
||||
//! `Template::render()`, supplying the name of the template file **minus
|
||||
//! the last two extensions**:
|
||||
//! | Engine | Extension | Example |
|
||||
//! |--------------|-----------|--------------------------------------------|
|
||||
//! | [Tera] | `.tera` | `${ROCKET_ROOT}/templates/index.html.tera` |
|
||||
//! | [Handlebars] | `.hbs` | `${ROCKET_ROOT}/templates/index.html.hbs` |
|
||||
//! | [MiniJinja] | `.j2` | `${ROCKET_ROOT}/templates/index.html.j2` |
|
||||
//!
|
||||
//! [configurable]: #configuration
|
||||
//! [Tera]: https://docs.rs/crate/tera/1
|
||||
//! [Handlebars]: https://docs.rs/crate/handlebars/5
|
||||
//! [MiniJinja]: https://docs.rs/minijinja/1
|
||||
//!
|
||||
//! 3. Attach `Template::fairing()` and return a [`Template`] from your routes
|
||||
//! via [`Template::render()`], supplying the name of the template file
|
||||
//! **minus the last two extensions**:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[macro_use] extern crate rocket;
|
||||
|
@ -30,7 +42,7 @@
|
|||
//!
|
||||
//! #[get("/")]
|
||||
//! fn index() -> Template {
|
||||
//! Template::render("template-name", context! { field: "value" })
|
||||
//! Template::render("index", context! { field: "value" })
|
||||
//! }
|
||||
//!
|
||||
//! #[launch]
|
||||
|
@ -39,98 +51,128 @@
|
|||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Naming
|
||||
//! ## Configuration
|
||||
//!
|
||||
//! 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.
|
||||
//! This crate reads one configuration parameter from the configured figment:
|
||||
//!
|
||||
//! Templates that are _not_ discovered by Rocket, such as those registered
|
||||
//! directly via [`Template::custom()`], are _not_ renamed. Use the name with
|
||||
//! which the template was originally registered.
|
||||
//! * `template_dir` (**default: `templates/`**)
|
||||
//!
|
||||
//! ## Content Type
|
||||
//! A path to a directory to search for template files in. Relative paths
|
||||
//! are considered relative to the configuration file, or there is no file,
|
||||
//! the current working directory.
|
||||
//!
|
||||
//! For example, to change the default and set `template_dir` to different
|
||||
//! values based on whether the application was compiled for debug or release
|
||||
//! from a `Rocket.toml` file (read by the default figment), you might write:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [debug]
|
||||
//! template_dir = "static/templates"
|
||||
//!
|
||||
//! [release]
|
||||
//! template_dir = "/var/opt/www/templates"
|
||||
//! ```
|
||||
//!
|
||||
//! **Note:** `template_dir` defaults to `templates/`. It _does not_ need to be
|
||||
//! specified if the default suffices.
|
||||
//!
|
||||
//! See the [configuration chapter] of the guide for more information on
|
||||
//! configuration.
|
||||
//!
|
||||
//! [configuration chapter]: https://rocket.rs/master/guide/configuration
|
||||
//!
|
||||
//! ## Template Naming and Content-Types
|
||||
//!
|
||||
//! Templates are rendered by _name_ via [`Template::render()`], which returns a
|
||||
//! [`Template`] responder. The _name_ of the template is the path to the
|
||||
//! template file, relative to `template_dir`, minus at most two extensions.
|
||||
//!
|
||||
//! 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`.
|
||||
//! non-engine extension using [`ContentType::from_extension()`]. If there is no
|
||||
//! such extension or it is unknown, `text/plain` is used.
|
||||
//!
|
||||
//! ## Discovery
|
||||
//! The following table contains examples:
|
||||
//!
|
||||
//! Template names passed in to [`Template::render()`] must correspond to a
|
||||
//! previously discovered template in the configured template directory. The
|
||||
//! template directory is configured via the `template_dir` configuration
|
||||
//! parameter and defaults to `templates/`. The path set in `template_dir` is
|
||||
//! relative to the Rocket configuration file. See the [configuration
|
||||
//! chapter](https://rocket.rs/v0.5/guide/configuration) of the guide for more
|
||||
//! information on configuration.
|
||||
//!
|
||||
//! The corresponding templating engine used for a given template is based on a
|
||||
//! template's extension. At present, this library supports the following
|
||||
//! engines and extensions:
|
||||
//!
|
||||
//! | Engine | Version | Extension |
|
||||
//! |--------------|---------|-----------|
|
||||
//! | [Tera] | 1 | `.tera` |
|
||||
//! | [Handlebars] | 5 | `.hbs` |
|
||||
//!
|
||||
//! [Tera]: https://docs.rs/crate/tera/1
|
||||
//! [Handlebars]: https://docs.rs/crate/handlebars/5
|
||||
//!
|
||||
//! Any file that ends with one of these extension will be discovered and
|
||||
//! rendered with the corresponding templating engine. The _name_ of the
|
||||
//! template will be the path to the template file relative to `template_dir`
|
||||
//! minus at most two extensions. The following table contains examples of this
|
||||
//! mapping:
|
||||
//!
|
||||
//! | example template path | template name |
|
||||
//! |-----------------------------------------------|-----------------------|
|
||||
//! | {template_dir}/index.html.hbs | index |
|
||||
//! | {template_dir}/index.tera | index |
|
||||
//! | {template_dir}/index.hbs | index |
|
||||
//! | {template_dir}/dir/index.hbs | dir/index |
|
||||
//! | {template_dir}/dir/index.html.tera | dir/index |
|
||||
//! | {template_dir}/index.template.html.hbs | index.template |
|
||||
//! | {template_dir}/subdir/index.template.html.hbs | subdir/index.template |
|
||||
//! | template path | [`Template::render()`] call | content-type |
|
||||
//! |-----------------------------------------------|-----------------------------------|--------------|
|
||||
//! | {template_dir}/index.html.hbs | `render("index")` | HTML |
|
||||
//! | {template_dir}/index.tera | `render("index")` | `text/plain` |
|
||||
//! | {template_dir}/index.hbs | `render("index")` | `text/plain` |
|
||||
//! | {template_dir}/dir/index.hbs | `render("dir/index")` | `text/plain` |
|
||||
//! | {template_dir}/dir/data.json.tera | `render("dir/data")` | JSON |
|
||||
//! | {template_dir}/data.template.xml.hbs | `render("data.template")` | XML |
|
||||
//! | {template_dir}/subdir/index.template.html.hbs | `render("subdir/index.template")` | HTML |
|
||||
//!
|
||||
//! The recommended naming scheme is to use two extensions: one for the file
|
||||
//! type, and one for the template extension. This means that template
|
||||
//! extensions should look like: `.html.hbs`, `.html.tera`, `.xml.hbs`, etc.
|
||||
//! extensions should look like: `.html.hbs`, `.html.tera`, `.xml.hbs`, and so
|
||||
//! on.
|
||||
//!
|
||||
//! ## Template Fairing and Customization
|
||||
//! [`ContentType::from_extension()`]: ../rocket/http/struct.ContentType.html#method.from_extension
|
||||
//!
|
||||
//! Template discovery is actualized by the template fairing, which itself is
|
||||
//! created via [`Template::fairing()`], [`Template::custom()`], or
|
||||
//! [`Template::try_custom()`], the latter two allowing customizations to
|
||||
//! templating engines such as registering template helpers and register
|
||||
//! templates from strings.
|
||||
//! ### Rendering Context
|
||||
//!
|
||||
//! In order for _any_ templates to be rendered, the template fairing _must_ be
|
||||
//! [attached](rocket::Rocket::attach()) to the running Rocket instance. Failure
|
||||
//! to do so will result in an ignite-time error.
|
||||
//! In addition to a name, [`Template::render()`] requires a context to use
|
||||
//! during rendering. The context can be any [`Serialize`] type that serializes
|
||||
//! to an `Object` (a dictionary) value. The [`context!`] macro can be used to
|
||||
//! create inline `Serialize`-able context objects.
|
||||
//!
|
||||
//! ## Rendering
|
||||
//! ```rust
|
||||
//! # #[macro_use] extern crate rocket;
|
||||
//! use rocket::serde::Serialize;
|
||||
//! use rocket_dyn_templates::{Template, context};
|
||||
//!
|
||||
//! Templates are typically rendered indirectly via [`Template::render()`] which
|
||||
//! returns a `Template` responder which renders the template at response time.
|
||||
//! To render a template directly into a `String`, use [`Metadata::render()`]
|
||||
//! instead.
|
||||
//! #[get("/")]
|
||||
//! fn index() -> Template {
|
||||
//! // Using the `context! { }` macro.
|
||||
//! Template::render("index", context! {
|
||||
//! site_name: "Rocket - Home Page",
|
||||
//! version: 127,
|
||||
//! })
|
||||
//! }
|
||||
//!
|
||||
//! Both methods take in a template name and context to use while rendering. The
|
||||
//! context can be any [`Serialize`] type that serializes to an `Object` (a
|
||||
//! dictionary) value. The [`context!`] macro may be used to create inline
|
||||
//! `Serialize`-able context objects.
|
||||
//! #[get("/")]
|
||||
//! fn index2() -> Template {
|
||||
//! #[derive(Serialize)]
|
||||
//! #[serde(crate = "rocket::serde")]
|
||||
//! struct IndexContext {
|
||||
//! site_name: &'static str,
|
||||
//! version: u8
|
||||
//! }
|
||||
//!
|
||||
//! ## Automatic Reloading
|
||||
//! // Using an existing `IndexContext`, which implements `Serialize`.
|
||||
//! Template::render("index", IndexContext {
|
||||
//! site_name: "Rocket - Home Page",
|
||||
//! version: 127,
|
||||
//! })
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Discovery, Automatic Reloads, and Engine Customization
|
||||
//!
|
||||
//! As long as one of [`Template::fairing()`], [`Template::custom()`], or
|
||||
//! [`Template::try_custom()`] is [attached], any file in the configured
|
||||
//! `template_dir` ending with a known engine extension (as described in the
|
||||
//! [usage section](#usage)) can be rendered. The latter two fairings allow
|
||||
//! customizations such as registering helpers and templates from strings.
|
||||
//!
|
||||
//! _**Note:** Templates that are registered directly via [`Template::custom()`],
|
||||
//! use whatever name provided during that registration; no extensions are
|
||||
//! automatically removed._
|
||||
//!
|
||||
//! In debug mode (without the `--release` flag passed to `cargo`), templates
|
||||
//! will be automatically reloaded from disk if any changes have been made to
|
||||
//! the templates directory since the previous request. In release builds,
|
||||
//! template reloading is disabled to improve performance and cannot be enabled.
|
||||
//! are **automatically reloaded** from disk when changes are made. In release
|
||||
//! builds, template reloading is disabled to improve performance and cannot be
|
||||
//! enabled.
|
||||
//!
|
||||
//! [attached]: Rocket::attach()
|
||||
//!
|
||||
//! ### Metadata and Rendering to `String`
|
||||
//!
|
||||
//! The [`Metadata`] request guard allows dynamically querying templating
|
||||
//! metadata, such as whether a template is known to exist
|
||||
//! ([`Metadata::contains_template()`]), and to render templates to `String`
|
||||
//! ([`Metadata::render()`]).
|
||||
|
||||
#![doc(html_root_url = "https://api.rocket.rs/v0.5/rocket_dyn_templates")]
|
||||
#![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")]
|
||||
|
@ -138,421 +180,30 @@
|
|||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
#[cfg(not(any(feature = "tera", feature = "handlebars")))]
|
||||
compile_error!("at least one of \"tera\" or \"handlebars\" features must be enabled");
|
||||
|
||||
#[doc(inline)]
|
||||
#[cfg(feature = "tera")]
|
||||
/// The tera templating engine library, reexported.
|
||||
pub use tera;
|
||||
|
||||
#[doc(inline)]
|
||||
#[cfg(feature = "tera")]
|
||||
pub use tera_ as tera;
|
||||
|
||||
#[cfg(feature = "tera")]
|
||||
mod tera_templates;
|
||||
|
||||
#[cfg(feature = "handlebars")]
|
||||
/// The handlebars templating engine library, reexported.
|
||||
#[doc(inline)]
|
||||
#[cfg(feature = "handlebars")]
|
||||
pub use handlebars_ as handlebars;
|
||||
pub use handlebars;
|
||||
|
||||
#[cfg(feature = "handlebars")]
|
||||
mod handlebars_templates;
|
||||
#[doc(inline)]
|
||||
#[cfg(feature = "minijinja")]
|
||||
/// The minijinja templating engine library, reexported.
|
||||
pub use minijinja;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use rocket::serde;
|
||||
|
||||
mod engine;
|
||||
mod fairing;
|
||||
mod context;
|
||||
mod metadata;
|
||||
mod template;
|
||||
|
||||
pub use self::engine::Engines;
|
||||
pub use self::metadata::Metadata;
|
||||
|
||||
use self::fairing::TemplateFairing;
|
||||
use self::context::{Context, ContextManager};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use rocket::serde;
|
||||
|
||||
use rocket::{Rocket, Orbit, Ignite, Sentinel};
|
||||
use rocket::request::Request;
|
||||
use rocket::fairing::Fairing;
|
||||
use rocket::response::{self, Responder};
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::figment::{value::Value, error::Error};
|
||||
use rocket::serde::Serialize;
|
||||
use rocket::yansi::Paint;
|
||||
|
||||
const DEFAULT_TEMPLATE_DIR: &str = "templates";
|
||||
|
||||
/// Responder that renders a dynamic template.
|
||||
///
|
||||
/// `Template` serves as a _proxy_ type for rendering a template and _does not_
|
||||
/// contain the rendered template itself. The template is lazily rendered, at
|
||||
/// response time. To render a template greedily, use [`Template::show()`].
|
||||
///
|
||||
/// See the [crate root](crate) for usage details.
|
||||
#[derive(Debug)]
|
||||
pub struct Template {
|
||||
name: Cow<'static, str>,
|
||||
value: Result<Value, Error>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TemplateInfo {
|
||||
/// The complete path, including `template_dir`, to this template, if any.
|
||||
path: Option<PathBuf>,
|
||||
/// The extension for the engine of this template.
|
||||
engine_ext: &'static str,
|
||||
/// The extension before the engine extension in the template, if any.
|
||||
data_type: ContentType
|
||||
}
|
||||
|
||||
impl Template {
|
||||
/// Returns a fairing that initializes and maintains templating state.
|
||||
///
|
||||
/// This fairing, or the one returned by [`Template::custom()`], _must_ be
|
||||
/// attached to any `Rocket` instance that wishes to render templates.
|
||||
/// Failure to attach this fairing will result in a "Uninitialized template
|
||||
/// context: missing fairing." error message when a template is attempted to
|
||||
/// be rendered.
|
||||
///
|
||||
/// If you wish to customize the internal templating engines, use
|
||||
/// [`Template::custom()`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// To attach this fairing, simple call `attach` on the application's
|
||||
/// `Rocket` instance with `Template::fairing()`:
|
||||
///
|
||||
/// ```rust
|
||||
/// extern crate rocket;
|
||||
/// extern crate rocket_dyn_templates;
|
||||
///
|
||||
/// use rocket_dyn_templates::Template;
|
||||
///
|
||||
/// fn main() {
|
||||
/// rocket::build()
|
||||
/// // ...
|
||||
/// .attach(Template::fairing())
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn fairing() -> impl Fairing {
|
||||
Template::custom(|_| {})
|
||||
}
|
||||
|
||||
/// Returns a fairing that initializes and maintains templating state.
|
||||
///
|
||||
/// Unlike [`Template::fairing()`], this method allows you to configure
|
||||
/// templating engines via the function `f`. Note that only the enabled
|
||||
/// templating engines will be accessible from the `Engines` type.
|
||||
///
|
||||
/// This method does not allow the function `f` to fail. If `f` is fallible,
|
||||
/// use [`Template::try_custom()`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// extern crate rocket;
|
||||
/// extern crate rocket_dyn_templates;
|
||||
///
|
||||
/// use rocket_dyn_templates::Template;
|
||||
///
|
||||
/// fn main() {
|
||||
/// rocket::build()
|
||||
/// // ...
|
||||
/// .attach(Template::custom(|engines| {
|
||||
/// // engines.handlebars.register_helper ...
|
||||
/// }))
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn custom<F: Send + Sync + 'static>(f: F) -> impl Fairing
|
||||
where F: Fn(&mut Engines)
|
||||
{
|
||||
Self::try_custom(move |engines| { f(engines); Ok(()) })
|
||||
}
|
||||
|
||||
/// Returns a fairing that initializes and maintains templating state.
|
||||
///
|
||||
/// This variant of [`Template::custom()`] allows a fallible `f`. If `f`
|
||||
/// returns an error during initialization, it will cancel the launch. If
|
||||
/// `f` returns an error during template reloading (in debug mode), then the
|
||||
/// newly-reloaded templates are discarded.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// extern crate rocket;
|
||||
/// extern crate rocket_dyn_templates;
|
||||
///
|
||||
/// use rocket_dyn_templates::Template;
|
||||
///
|
||||
/// fn main() {
|
||||
/// rocket::build()
|
||||
/// // ...
|
||||
/// .attach(Template::try_custom(|engines| {
|
||||
/// // engines.handlebars.register_helper ...
|
||||
/// Ok(())
|
||||
/// }))
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn try_custom<F: Send + Sync + 'static>(f: F) -> impl Fairing
|
||||
where F: Fn(&mut Engines) -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
TemplateFairing { callback: Box::new(f) }
|
||||
}
|
||||
|
||||
/// Render the template named `name` with the context `context`. The
|
||||
/// `context` is typically created using the [`context!`] macro, but it can
|
||||
/// be of any type that implements `Serialize`, such as `HashMap` or a
|
||||
/// custom `struct`.
|
||||
///
|
||||
/// To render a template directly into a string, use [`Metadata::render()`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Using the `context` macro:
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket_dyn_templates::{Template, context};
|
||||
///
|
||||
/// let template = Template::render("index", context! {
|
||||
/// foo: "Hello, world!",
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// Using a `HashMap` as the context:
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::collections::HashMap;
|
||||
/// use rocket_dyn_templates::Template;
|
||||
///
|
||||
/// // Create a `context` from a `HashMap`.
|
||||
/// let mut context = HashMap::new();
|
||||
/// context.insert("foo", "Hello, world!");
|
||||
///
|
||||
/// let template = Template::render("index", context);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn render<S, C>(name: S, context: C) -> Template
|
||||
where S: Into<Cow<'static, str>>, C: Serialize
|
||||
{
|
||||
Template { name: name.into(), value: Value::serialize(context) }
|
||||
}
|
||||
|
||||
/// Render the template named `name` with the context `context` into a
|
||||
/// `String`. This method should **not** be used in any running Rocket
|
||||
/// application. This method should only be used during testing to validate
|
||||
/// `Template` responses. For other uses, use [`render()`](#method.render)
|
||||
/// instead.
|
||||
///
|
||||
/// The `context` can be of any type that implements `Serialize`. This is
|
||||
/// typically a `HashMap` or a custom `struct`.
|
||||
///
|
||||
/// Returns `Some` if the template could be rendered. Otherwise, returns
|
||||
/// `None`. If rendering fails, error output is printed to the console.
|
||||
/// `None` is also returned if a `Template` fairing has not been attached.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # extern crate rocket;
|
||||
/// # extern crate rocket_dyn_templates;
|
||||
/// use std::collections::HashMap;
|
||||
///
|
||||
/// use rocket_dyn_templates::Template;
|
||||
/// use rocket::local::blocking::Client;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let rocket = rocket::build().attach(Template::fairing());
|
||||
/// let client = Client::untracked(rocket).expect("valid rocket");
|
||||
///
|
||||
/// // Create a `context`. Here, just an empty `HashMap`.
|
||||
/// let mut context = HashMap::new();
|
||||
/// # context.insert("test", "test");
|
||||
/// let template = Template::show(client.rocket(), "index", context);
|
||||
/// }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn show<S, C>(rocket: &Rocket<Orbit>, name: S, context: C) -> Option<String>
|
||||
where S: Into<Cow<'static, str>>, C: Serialize
|
||||
{
|
||||
let ctxt = rocket.state::<ContextManager>().map(ContextManager::context).or_else(|| {
|
||||
warn!("Uninitialized template context: missing fairing.");
|
||||
info!("To use templates, you must attach `Template::fairing()`.");
|
||||
info!("See the `Template` documentation for more information.");
|
||||
None
|
||||
})?;
|
||||
|
||||
Template::render(name, context).finalize(&ctxt).ok().map(|v| v.1)
|
||||
}
|
||||
|
||||
/// Actually render this template given a template context. This method is
|
||||
/// called by the `Template` `Responder` implementation as well as
|
||||
/// `Template::show()`.
|
||||
#[inline(always)]
|
||||
fn finalize(self, ctxt: &Context) -> Result<(ContentType, String), Status> {
|
||||
let name = &*self.name;
|
||||
let info = ctxt.templates.get(name).ok_or_else(|| {
|
||||
let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect();
|
||||
error_!("Template '{}' does not exist.", name);
|
||||
info_!("Known templates: {}.", ts.join(", "));
|
||||
info_!("Searched in {:?}.", ctxt.root);
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
|
||||
let value = self.value.map_err(|e| {
|
||||
error_!("Template context failed to serialize: {}.", e);
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
|
||||
let string = ctxt.engines.render(name, &info, value).ok_or_else(|| {
|
||||
error_!("Template '{}' failed to render.", name);
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
|
||||
Ok((info.data_type.clone(), string))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a response with the Content-Type derived from the template's
|
||||
/// extension and a fixed-size body containing the rendered template. If
|
||||
/// rendering fails, an `Err` of `Status::InternalServerError` is returned.
|
||||
impl<'r> Responder<'r, 'static> for Template {
|
||||
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
|
||||
let ctxt = req.rocket()
|
||||
.state::<ContextManager>()
|
||||
.ok_or_else(|| {
|
||||
error_!("Uninitialized template context: missing fairing.");
|
||||
info_!("To use templates, you must attach `Template::fairing()`.");
|
||||
info_!("See the `Template` documentation for more information.");
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
|
||||
self.finalize(&ctxt.context())?.respond_to(req)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sentinel for Template {
|
||||
fn abort(rocket: &Rocket<Ignite>) -> bool {
|
||||
if rocket.state::<ContextManager>().is_none() {
|
||||
let template = "Template".primary().bold();
|
||||
let fairing = "Template::fairing()".primary().bold();
|
||||
error!("returning `{}` responder without attaching `{}`.", template, fairing);
|
||||
info_!("To use or query templates, you must attach `{}`.", fairing);
|
||||
info_!("See the `Template` documentation for more information.");
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A macro to easily create a template rendering context.
|
||||
///
|
||||
/// Invocations of this macro expand to a value of an anonymous type which
|
||||
/// implements [`serde::Serialize`]. Fields can be literal expressions or
|
||||
/// variables captured from a surrounding scope, as long as all fields implement
|
||||
/// `Serialize`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// The following code:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// # use rocket_dyn_templates::{Template, context};
|
||||
/// #[get("/<foo>")]
|
||||
/// fn render_index(foo: u64) -> Template {
|
||||
/// Template::render("index", context! {
|
||||
/// // Note that shorthand field syntax is supported.
|
||||
/// // This is equivalent to `foo: foo,`
|
||||
/// foo,
|
||||
/// bar: "Hello world",
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// is equivalent to the following, but without the need to manually define an
|
||||
/// `IndexContext` struct:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rocket_dyn_templates::Template;
|
||||
/// # use rocket::serde::Serialize;
|
||||
/// # use rocket::get;
|
||||
/// #[derive(Serialize)]
|
||||
/// # #[serde(crate = "rocket::serde")]
|
||||
/// struct IndexContext<'a> {
|
||||
/// foo: u64,
|
||||
/// bar: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// #[get("/<foo>")]
|
||||
/// fn render_index(foo: u64) -> Template {
|
||||
/// Template::render("index", IndexContext {
|
||||
/// foo,
|
||||
/// bar: "Hello world",
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Nesting
|
||||
///
|
||||
/// Nested objects can be created by nesting calls to `context!`:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rocket_dyn_templates::context;
|
||||
/// # fn main() {
|
||||
/// let ctx = context! {
|
||||
/// planet: "Earth",
|
||||
/// info: context! {
|
||||
/// mass: 5.97e24,
|
||||
/// radius: "6371 km",
|
||||
/// moons: 1,
|
||||
/// },
|
||||
/// };
|
||||
/// # }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! context {
|
||||
($($key:ident $(: $value:expr)?),*$(,)?) => {{
|
||||
use $crate::serde::ser::{Serialize, Serializer, SerializeMap};
|
||||
use ::std::fmt::{Debug, Formatter};
|
||||
use ::std::result::Result;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
struct ContextMacroCtxObject<$($key: Serialize),*> {
|
||||
$($key: $key),*
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
impl<$($key: Serialize),*> Serialize for ContextMacroCtxObject<$($key),*> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(None)?;
|
||||
$(map.serialize_entry(stringify!($key), &self.$key)?;)*
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
impl<$($key: Debug + Serialize),*> Debug for ContextMacroCtxObject<$($key),*> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result {
|
||||
f.debug_struct("context!")
|
||||
$(.field(stringify!($key), &self.$key))*
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
ContextMacroCtxObject {
|
||||
$($key $(: $value)?),*
|
||||
}
|
||||
}};
|
||||
}
|
||||
pub use engine::Engines;
|
||||
pub use metadata::Metadata;
|
||||
pub use template::Template;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::fmt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rocket::{Request, Rocket, Ignite, Sentinel};
|
||||
|
@ -124,6 +125,14 @@ impl Metadata<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Metadata<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_map()
|
||||
.entries(&self.0.context().templates)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Sentinel for Metadata<'_> {
|
||||
fn abort(rocket: &Rocket<Ignite>) -> bool {
|
||||
if rocket.state::<ContextManager>().is_none() {
|
||||
|
|
|
@ -0,0 +1,392 @@
|
|||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rocket::{Rocket, Orbit, Ignite, Sentinel};
|
||||
use rocket::request::Request;
|
||||
use rocket::fairing::Fairing;
|
||||
use rocket::response::{self, Responder};
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::figment::{value::Value, error::Error};
|
||||
use rocket::serde::Serialize;
|
||||
use rocket::yansi::Paint;
|
||||
|
||||
use crate::Engines;
|
||||
use crate::fairing::TemplateFairing;
|
||||
use crate::context::{Context, ContextManager};
|
||||
|
||||
pub(crate) const DEFAULT_TEMPLATE_DIR: &str = "templates";
|
||||
|
||||
/// Responder that renders a dynamic template.
|
||||
///
|
||||
/// `Template` serves as a _proxy_ type for rendering a template and _does not_
|
||||
/// contain the rendered template itself. The template is lazily rendered, at
|
||||
/// response time. To render a template greedily, use [`Template::show()`].
|
||||
///
|
||||
/// See the [crate root](crate) for usage details.
|
||||
#[derive(Debug)]
|
||||
pub struct Template {
|
||||
name: Cow<'static, str>,
|
||||
value: Result<Value, Error>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TemplateInfo {
|
||||
/// The complete path, including `template_dir`, to this template, if any.
|
||||
pub(crate) path: Option<PathBuf>,
|
||||
/// The extension for the engine of this template.
|
||||
pub(crate) engine_ext: &'static str,
|
||||
/// The extension before the engine extension in the template, if any.
|
||||
pub(crate) data_type: ContentType
|
||||
}
|
||||
|
||||
impl Template {
|
||||
/// Returns a fairing that initializes and maintains templating state.
|
||||
///
|
||||
/// This fairing, or the one returned by [`Template::custom()`], _must_ be
|
||||
/// attached to any `Rocket` instance that wishes to render templates.
|
||||
/// Failure to attach this fairing will result in a "Uninitialized template
|
||||
/// context: missing fairing." error message when a template is attempted to
|
||||
/// be rendered.
|
||||
///
|
||||
/// If you wish to customize the internal templating engines, use
|
||||
/// [`Template::custom()`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// To attach this fairing, simple call `attach` on the application's
|
||||
/// `Rocket` instance with `Template::fairing()`:
|
||||
///
|
||||
/// ```rust
|
||||
/// extern crate rocket;
|
||||
/// extern crate rocket_dyn_templates;
|
||||
///
|
||||
/// use rocket_dyn_templates::Template;
|
||||
///
|
||||
/// fn main() {
|
||||
/// rocket::build()
|
||||
/// // ...
|
||||
/// .attach(Template::fairing())
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn fairing() -> impl Fairing {
|
||||
Template::custom(|_| {})
|
||||
}
|
||||
|
||||
/// Returns a fairing that initializes and maintains templating state.
|
||||
///
|
||||
/// Unlike [`Template::fairing()`], this method allows you to configure
|
||||
/// templating engines via the function `f`. Note that only the enabled
|
||||
/// templating engines will be accessible from the `Engines` type.
|
||||
///
|
||||
/// This method does not allow the function `f` to fail. If `f` is fallible,
|
||||
/// use [`Template::try_custom()`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// extern crate rocket;
|
||||
/// extern crate rocket_dyn_templates;
|
||||
///
|
||||
/// use rocket_dyn_templates::Template;
|
||||
///
|
||||
/// fn main() {
|
||||
/// rocket::build()
|
||||
/// // ...
|
||||
/// .attach(Template::custom(|engines| {
|
||||
/// // engines.handlebars.register_helper ...
|
||||
/// }))
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn custom<F: Send + Sync + 'static>(f: F) -> impl Fairing
|
||||
where F: Fn(&mut Engines)
|
||||
{
|
||||
Self::try_custom(move |engines| { f(engines); Ok(()) })
|
||||
}
|
||||
|
||||
/// Returns a fairing that initializes and maintains templating state.
|
||||
///
|
||||
/// This variant of [`Template::custom()`] allows a fallible `f`. If `f`
|
||||
/// returns an error during initialization, it will cancel the launch. If
|
||||
/// `f` returns an error during template reloading (in debug mode), then the
|
||||
/// newly-reloaded templates are discarded.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// extern crate rocket;
|
||||
/// extern crate rocket_dyn_templates;
|
||||
///
|
||||
/// use rocket_dyn_templates::Template;
|
||||
///
|
||||
/// fn main() {
|
||||
/// rocket::build()
|
||||
/// // ...
|
||||
/// .attach(Template::try_custom(|engines| {
|
||||
/// // engines.handlebars.register_helper ...
|
||||
/// Ok(())
|
||||
/// }))
|
||||
/// // ...
|
||||
/// # ;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn try_custom<F: Send + Sync + 'static>(f: F) -> impl Fairing
|
||||
where F: Fn(&mut Engines) -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
TemplateFairing { callback: Box::new(f) }
|
||||
}
|
||||
|
||||
/// Render the template named `name` with the context `context`. The
|
||||
/// `context` is typically created using the [`context!`] macro, but it can
|
||||
/// be of any type that implements `Serialize`, such as `HashMap` or a
|
||||
/// custom `struct`.
|
||||
///
|
||||
/// To render a template directly into a string, use [`Metadata::render()`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Using the `context` macro:
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket_dyn_templates::{Template, context};
|
||||
///
|
||||
/// let template = Template::render("index", context! {
|
||||
/// foo: "Hello, world!",
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// Using a `HashMap` as the context:
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::collections::HashMap;
|
||||
/// use rocket_dyn_templates::Template;
|
||||
///
|
||||
/// // Create a `context` from a `HashMap`.
|
||||
/// let mut context = HashMap::new();
|
||||
/// context.insert("foo", "Hello, world!");
|
||||
///
|
||||
/// let template = Template::render("index", context);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn render<S, C>(name: S, context: C) -> Template
|
||||
where S: Into<Cow<'static, str>>, C: Serialize
|
||||
{
|
||||
Template {
|
||||
name: name.into(),
|
||||
value: Value::serialize(context),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the template named `name` with the context `context` into a
|
||||
/// `String`. This method should **not** be used in any running Rocket
|
||||
/// application. This method should only be used during testing to validate
|
||||
/// `Template` responses. For other uses, use [`render()`](#method.render)
|
||||
/// instead.
|
||||
///
|
||||
/// The `context` can be of any type that implements `Serialize`. This is
|
||||
/// typically a `HashMap` or a custom `struct`.
|
||||
///
|
||||
/// Returns `Some` if the template could be rendered. Otherwise, returns
|
||||
/// `None`. If rendering fails, error output is printed to the console.
|
||||
/// `None` is also returned if a `Template` fairing has not been attached.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # extern crate rocket;
|
||||
/// # extern crate rocket_dyn_templates;
|
||||
/// use std::collections::HashMap;
|
||||
///
|
||||
/// use rocket_dyn_templates::Template;
|
||||
/// use rocket::local::blocking::Client;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let rocket = rocket::build().attach(Template::fairing());
|
||||
/// let client = Client::untracked(rocket).expect("valid rocket");
|
||||
///
|
||||
/// // Create a `context`. Here, just an empty `HashMap`.
|
||||
/// let mut context = HashMap::new();
|
||||
/// # context.insert("test", "test");
|
||||
/// let template = Template::show(client.rocket(), "index", context);
|
||||
/// }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn show<S, C>(rocket: &Rocket<Orbit>, name: S, context: C) -> Option<String>
|
||||
where S: Into<Cow<'static, str>>, C: Serialize
|
||||
{
|
||||
let ctxt = rocket.state::<ContextManager>().map(ContextManager::context).or_else(|| {
|
||||
warn!("Uninitialized template context: missing fairing.");
|
||||
info!("To use templates, you must attach `Template::fairing()`.");
|
||||
info!("See the `Template` documentation for more information.");
|
||||
None
|
||||
})?;
|
||||
|
||||
Template::render(name, context).finalize(&ctxt).ok().map(|v| v.1)
|
||||
}
|
||||
|
||||
/// Actually render this template given a template context. This method is
|
||||
/// called by the `Template` `Responder` implementation as well as
|
||||
/// `Template::show()`.
|
||||
#[inline(always)]
|
||||
pub(crate) fn finalize(self, ctxt: &Context) -> Result<(ContentType, String), Status> {
|
||||
let name = &*self.name;
|
||||
let info = ctxt.templates.get(name).ok_or_else(|| {
|
||||
let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect();
|
||||
error_!("Template '{}' does not exist.", name);
|
||||
info_!("Known templates: {}.", ts.join(", "));
|
||||
info_!("Searched in {:?}.", ctxt.root);
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
|
||||
let value = self.value.map_err(|e| {
|
||||
error_!("Template context failed to serialize: {}.", e);
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
|
||||
let string = ctxt.engines.render(name, info, value).ok_or_else(|| {
|
||||
error_!("Template '{}' failed to render.", name);
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
|
||||
Ok((info.data_type.clone(), string))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a response with the Content-Type derived from the template's
|
||||
/// extension and a fixed-size body containing the rendered template. If
|
||||
/// rendering fails, an `Err` of `Status::InternalServerError` is returned.
|
||||
impl<'r> Responder<'r, 'static> for Template {
|
||||
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
|
||||
let ctxt = req.rocket()
|
||||
.state::<ContextManager>()
|
||||
.ok_or_else(|| {
|
||||
error_!("Uninitialized template context: missing fairing.");
|
||||
info_!("To use templates, you must attach `Template::fairing()`.");
|
||||
info_!("See the `Template` documentation for more information.");
|
||||
Status::InternalServerError
|
||||
})?;
|
||||
|
||||
self.finalize(&ctxt.context())?.respond_to(req)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sentinel for Template {
|
||||
fn abort(rocket: &Rocket<Ignite>) -> bool {
|
||||
if rocket.state::<ContextManager>().is_none() {
|
||||
let template = "Template".primary().bold();
|
||||
let fairing = "Template::fairing()".primary().bold();
|
||||
error!("returning `{}` responder without attaching `{}`.", template, fairing);
|
||||
info_!("To use or query templates, you must attach `{}`.", fairing);
|
||||
info_!("See the `Template` documentation for more information.");
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A macro to easily create a template rendering context.
|
||||
///
|
||||
/// Invocations of this macro expand to a value of an anonymous type which
|
||||
/// implements [`serde::Serialize`]. Fields can be literal expressions or
|
||||
/// variables captured from a surrounding scope, as long as all fields implement
|
||||
/// `Serialize`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// The following code:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// # use rocket_dyn_templates::{Template, context};
|
||||
/// #[get("/<foo>")]
|
||||
/// fn render_index(foo: u64) -> Template {
|
||||
/// Template::render("index", context! {
|
||||
/// // Note that shorthand field syntax is supported.
|
||||
/// // This is equivalent to `foo: foo,`
|
||||
/// foo,
|
||||
/// bar: "Hello world",
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// is equivalent to the following, but without the need to manually define an
|
||||
/// `IndexContext` struct:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rocket_dyn_templates::Template;
|
||||
/// # use rocket::serde::Serialize;
|
||||
/// # use rocket::get;
|
||||
/// #[derive(Serialize)]
|
||||
/// # #[serde(crate = "rocket::serde")]
|
||||
/// struct IndexContext<'a> {
|
||||
/// foo: u64,
|
||||
/// bar: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// #[get("/<foo>")]
|
||||
/// fn render_index(foo: u64) -> Template {
|
||||
/// Template::render("index", IndexContext {
|
||||
/// foo,
|
||||
/// bar: "Hello world",
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Nesting
|
||||
///
|
||||
/// Nested objects can be created by nesting calls to `context!`:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rocket_dyn_templates::context;
|
||||
/// # fn main() {
|
||||
/// let ctx = context! {
|
||||
/// planet: "Earth",
|
||||
/// info: context! {
|
||||
/// mass: 5.97e24,
|
||||
/// radius: "6371 km",
|
||||
/// moons: 1,
|
||||
/// },
|
||||
/// };
|
||||
/// # }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! context {
|
||||
($($key:ident $(: $value:expr)?),*$(,)?) => {{
|
||||
use $crate::serde::ser::{Serialize, Serializer, SerializeMap};
|
||||
use ::std::fmt::{Debug, Formatter};
|
||||
use ::std::result::Result;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
struct ContextMacroCtxObject<$($key: Serialize),*> {
|
||||
$($key: $key),*
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
impl<$($key: Serialize),*> Serialize for ContextMacroCtxObject<$($key),*> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(None)?;
|
||||
$(map.serialize_entry(stringify!($key), &self.$key)?;)*
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
impl<$($key: Debug + Serialize),*> Debug for ContextMacroCtxObject<$($key),*> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result {
|
||||
f.debug_struct("context!")
|
||||
$(.field(stringify!($key), &self.$key))*
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
ContextMacroCtxObject {
|
||||
$($key $(: $value)?),*
|
||||
}
|
||||
}};
|
||||
}
|
|
@ -206,11 +206,12 @@ mod tera_tests {
|
|||
use std::collections::HashMap;
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::request::FromRequest;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const UNESCAPED_EXPECTED: &'static str
|
||||
= "\nh_start\ntitle: _test_\nh_end\n\n\n<script />\n\nfoot\n";
|
||||
= "\nh_start\ntitle: _test_\nh_end\n\n\n<script />\n\nfoot";
|
||||
const ESCAPED_EXPECTED: &'static str
|
||||
= "\nh_start\ntitle: _test_\nh_end\n\n\n<script />\n\nfoot\n";
|
||||
= "\nh_start\ntitle: _test_\nh_end\n\n\n<script />\n\nfoot";
|
||||
|
||||
#[async_test]
|
||||
async fn test_tera_templates() {
|
||||
|
@ -230,7 +231,7 @@ mod tera_tests {
|
|||
assert_eq!(template, Some(UNESCAPED_EXPECTED.into()));
|
||||
assert_eq!(md_rendered, Some((ContentType::Text, UNESCAPED_EXPECTED.into())));
|
||||
|
||||
// Now with an HTML file, which should.
|
||||
// Now with an HTML file, which should escape.
|
||||
let template = Template::show(client.rocket(), "tera/html_test", &map);
|
||||
let md_rendered = metadata.render("tera/html_test", &map);
|
||||
assert_eq!(template, Some(ESCAPED_EXPECTED.into()));
|
||||
|
@ -288,6 +289,7 @@ mod handlebars_tests {
|
|||
use std::collections::HashMap;
|
||||
use rocket::request::FromRequest;
|
||||
use rocket::http::{ContentType, Status};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[async_test]
|
||||
async fn test_handlebars_templates() {
|
||||
|
@ -398,3 +400,87 @@ mod handlebars_tests {
|
|||
panic!("failed to reload modified template in 1.5s");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "minijinja")]
|
||||
mod j2_tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::request::FromRequest;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const UNESCAPED_EXPECTED: &'static str
|
||||
= "\nh_start\ntitle: _test_\nh_end\n\n\n<script />\n\nfoot";
|
||||
const ESCAPED_EXPECTED: &'static str
|
||||
= "\nh_start\ntitle: _test_\nh_end\n\n\n<script />\n\nfoot";
|
||||
|
||||
#[async_test]
|
||||
async fn test_j2_templates() {
|
||||
use rocket::local::asynchronous::Client;
|
||||
|
||||
let client = Client::debug(rocket()).await.unwrap();
|
||||
let req = client.get("/");
|
||||
let metadata = Metadata::from_request(&req).await.unwrap();
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("title", "_test_");
|
||||
map.insert("content", "<script />");
|
||||
|
||||
// Test with a txt file, which shouldn't escape.
|
||||
let template = Template::show(client.rocket(), "j2/txt_test", &map);
|
||||
let md_rendered = (&metadata).render("j2/txt_test", &map);
|
||||
assert_eq!(template, Some(UNESCAPED_EXPECTED.into()));
|
||||
assert_eq!(md_rendered, Some((ContentType::Text, UNESCAPED_EXPECTED.into())));
|
||||
|
||||
// Now with an HTML file, which should escaped.
|
||||
let template = Template::show(client.rocket(), "j2/html_test", &map);
|
||||
let md_rendered = metadata.render("j2/html_test", &map);
|
||||
assert_eq!(template, Some(ESCAPED_EXPECTED.into()));
|
||||
assert_eq!(md_rendered, Some((ContentType::HTML, ESCAPED_EXPECTED.into())));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_globby_paths() {
|
||||
use rocket::local::asynchronous::Client;
|
||||
|
||||
let client = Client::debug(rocket()).await.unwrap();
|
||||
let req = client.get("/");
|
||||
let metadata = Metadata::from_request(&req).await.unwrap();
|
||||
assert!(metadata.contains_template("j2/[test]/html_test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_j2_u128() {
|
||||
const EXPECTED: &'static str
|
||||
= "\nh_start\ntitle: 123\nh_end\n\n\n1208925819614629174706176\n\nfoot";
|
||||
|
||||
use rocket::local::blocking::Client;
|
||||
|
||||
let client = Client::debug(rocket()).unwrap();
|
||||
let mut map = HashMap::new();
|
||||
map.insert("title", 123);
|
||||
map.insert("content", 1u128 << 80);
|
||||
|
||||
let template = Template::show(client.rocket(), "j2/txt_test", &map);
|
||||
assert_eq!(template, Some(EXPECTED.into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_metadata_with_j2() {
|
||||
use rocket::local::blocking::Client;
|
||||
|
||||
let client = Client::debug(rocket()).unwrap();
|
||||
|
||||
let response = client.get("/j2/txt_test").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
let response = client.get("/j2/html_test").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
let response = client.get("/j2/not_existing").dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
|
||||
let response = client.get("/hbs/txt_test").dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{% extends "j2/base" %}
|
||||
{% block title %}{{ title }}{% endblock title %}
|
||||
{% block content %}
|
||||
{{ content }}
|
||||
{% endblock content %}
|
|
@ -0,0 +1,7 @@
|
|||
{% block head %}
|
||||
h_start
|
||||
title: {% block title %}{% endblock title %}
|
||||
h_end
|
||||
{% endblock head %}
|
||||
{% block content %}{% endblock content %}
|
||||
{% block footer %}foot{% endblock footer %}
|
|
@ -0,0 +1,5 @@
|
|||
{% extends "j2/base" %}
|
||||
{% block title %}{{ title }}{% endblock title %}
|
||||
{% block content %}
|
||||
{{ content }}
|
||||
{% endblock content %}
|
|
@ -0,0 +1,5 @@
|
|||
{% extends "j2/base" %}
|
||||
{% block title %}{{ title }}{% endblock title %}
|
||||
{% block content %}
|
||||
{{ content }}
|
||||
{% endblock content %}
|
|
@ -4,4 +4,4 @@ title: {% block title %}{% endblock title %}
|
|||
h_end
|
||||
{% endblock head %}
|
||||
{% block content %}{% endblock content %}
|
||||
{% block footer %}foot{% endblock footer %}
|
||||
{% block footer %}foot{% endblock footer -%}
|
||||
|
|
|
@ -97,6 +97,7 @@ function test_contrib() {
|
|||
DYN_TEMPLATES_FEATURES=(
|
||||
tera
|
||||
handlebars
|
||||
minijinja
|
||||
)
|
||||
|
||||
WS_FEATURES=(
|
||||
|
|
Loading…
Reference in New Issue