Skip to content

Commit

Permalink
Adds a render! macro and a render_str method to env (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Sep 2, 2022
1 parent 4760ec3 commit 90cbcd1
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 101 deletions.
3 changes: 2 additions & 1 deletion examples/README.md
Expand Up @@ -16,5 +16,6 @@ the `cargo run` command. Alternatively you can do `cargo run -p example-name`.
* [loader](loader): shows how to load templates dynamically at runtime.
* [minimal](minimal): a Hello World example without default features.
* [recursive-for](recursive-for): demonstrates the recursive for loop.
* [render-macro](render-macro): minimal Hello World example using the `render!` macro.
* [render-template](render-template): CLI app that renders templates from string.
* [source](source): Demonstrates how to load templates from disk with the `source` feature.
* [source](source): Demonstrates how to load templates from disk with the `source` feature.
9 changes: 9 additions & 0 deletions examples/render-macro/Cargo.toml
@@ -0,0 +1,9 @@
[package]
name = "render-macro"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
minijinja = { path = "../../minijinja" }
9 changes: 9 additions & 0 deletions examples/render-macro/README.md
@@ -0,0 +1,9 @@
# render-macro

A simple example of how to render a single template from a string and render it
with the `render!` macro.

```console
$ cargo run
Hello John!
```
5 changes: 5 additions & 0 deletions examples/render-macro/src/main.rs
@@ -0,0 +1,5 @@
use minijinja::render;

fn main() {
println!("{}", render!("Hello {{ name }}!", name => "John"));
}
98 changes: 0 additions & 98 deletions minijinja/src/context.rs

This file was deleted.

42 changes: 42 additions & 0 deletions minijinja/src/environment.rs
Expand Up @@ -445,6 +445,14 @@ impl<'source> Environment<'source> {
/// This requires that the template has been loaded with
/// [`add_template`](Environment::add_template) beforehand. If the template was
/// not loaded an error of kind `TemplateNotFound` is returned.
///
/// ```
/// # use minijinja::{Environment, context};
/// let mut env = Environment::new();
/// env.add_template("hello.txt", "Hello {{ name }}!").unwrap();
/// let tmpl = env.get_template("hello.txt").unwrap();
/// println!("{}", tmpl.render(context!{ name => "World" }).unwrap());
/// ```
pub fn get_template(&self, name: &str) -> Result<Template<'_>, Error> {
let compiled = match &self.templates {
Source::Borrowed(ref map) => map
Expand All @@ -461,6 +469,40 @@ impl<'source> Environment<'source> {
})
}

/// Parses and renders a template from a string in one go.
///
/// In some cases you really only need a template to be rendered once from
/// a string and returned. The internal name of the template is `<string>`.
///
/// ```
/// # use minijinja::{Environment, context};
/// let env = Environment::new();
/// let rv = env.render_str("Hello {{ name }}", context! { name => "World" });
/// println!("{}", rv.unwrap());
/// ```
pub fn render_str<S: Serialize>(&self, source: &str, ctx: S) -> Result<String, Error> {
// reduce total amount of code faling under mono morphization into
// this function, and share the rest in _eval.
self._render_str(source, Value::from_serializable(&ctx))
}

fn _render_str(&self, source: &str, root: Value) -> Result<String, Error> {
let name = "<string>";
let compiled = CompiledTemplate::from_name_and_source(name, source)?;
let mut output = String::new();
let vm = Vm::new(self);
let blocks = &compiled.blocks;
let initial_auto_escape = self.get_initial_auto_escape(name);
vm.eval(
&compiled.instructions,
root,
blocks,
initial_auto_escape,
&mut output,
)?;
Ok(output)
}

/// Compiles an expression.
///
/// This lets one compile an expression in the template language and
Expand Down
4 changes: 2 additions & 2 deletions minijinja/src/lib.rs
Expand Up @@ -126,11 +126,11 @@ mod key;

mod ast;
mod compiler;
mod context;
mod environment;
mod error;
mod instructions;
mod lexer;
mod macros;
mod parser;
mod tokens;
mod utils;
Expand Down Expand Up @@ -159,7 +159,7 @@ pub use self::error::DebugInfo;
#[cfg(feature = "source")]
pub use self::source::Source;

pub use self::context::*;
pub use self::macros::__context;
pub use self::vm::State;

/// This module gives access to the low level machinery.
Expand Down
180 changes: 180 additions & 0 deletions minijinja/src/macros.rs
@@ -0,0 +1,180 @@
#[cfg(test)]
use similar_asserts::assert_eq;

/// Hidden utility module for the [`context!`](crate::context!) macro.
#[doc(hidden)]
pub mod __context {
use crate::key::Key;
use crate::value::{RcType, Value, ValueMap, ValueRepr};
use crate::Environment;

#[inline(always)]
pub fn make() -> ValueMap {
ValueMap::default()
}

#[inline(always)]
pub fn add(ctx: &mut ValueMap, key: &'static str, value: Value) {
ctx.insert(Key::Str(key), value);
}

#[inline(always)]
pub fn build(ctx: ValueMap) -> Value {
ValueRepr::Map(RcType::new(ctx)).into()
}

pub fn thread_local_env() -> Environment<'static> {
thread_local! {
static ENV: Environment<'static> = Environment::new()
}
ENV.with(|x| x.clone())
}
}

/// Creates a template context with keys and values.
///
/// ```rust
/// # use minijinja::context;
/// let ctx = context! {
/// name => "Peter",
/// location => "World",
/// };
/// ```
///
/// Alternatively if the variable name matches the key name it can
/// be omitted:
///
/// ```rust
/// # use minijinja::context;
/// let name = "Peter";
/// let ctx = context! { name };
/// ```
///
/// The return value is a [`Value`](crate::value::Value).
///
/// Note that [`context!`](crate::context!) can also be used recursively if you need to
/// create nested objects:
///
/// ```rust
/// # use minijinja::context;
/// let ctx = context! {
/// nav => vec![
/// context!(path => "/", title => "Index"),
/// context!(path => "/downloads", title => "Downloads"),
/// context!(path => "/faq", title => "FAQ"),
/// ]
/// };
/// ```
#[macro_export]
macro_rules! context {
() => {
$crate::__context::build($crate::__context::make())
};
(
$($key:ident $(=> $value:expr)?),* $(,)?
) => {{
let mut ctx = $crate::__context::make();
$(
$crate::__context_pair!(ctx, $key $(, $value)?);
)*
$crate::__context::build(ctx)
}}
}

#[macro_export]
#[doc(hidden)]
macro_rules! __context_pair {
($ctx:ident, $key:ident) => {{
$crate::__context_pair!($ctx, $key, $key);
}};
($ctx:ident, $key:ident, $value:expr) => {
$crate::__context::add(
&mut $ctx,
stringify!($key),
$crate::value::Value::from_serializable(&$value),
);
};
}

/// A macro similar to [`format!`] but that uses MiniJinja for rendering.
///
/// This can be used to quickly render a MiniJinja template into a string
/// without having to create an environment first which can be useful in
/// some situations. Note however that the template is re-parsed every
/// time the [`render!`](crate::render) macro is called which is potentially
/// slow.
///
/// There are two forms for this macro. The default form takes template
/// source and context variables, the extended form also lets you provide
/// a custom environment that should be used rather than a default one.
/// The context variables are passed the same way as with the
/// [`context!](crate::context) macro.
///
/// # Example
///
/// Passing context explicitly:
///
/// ```
/// # use minijinja::render;
/// println!("{}", render!("Hello {{ name }}!", name => "World"));
/// ```
///
/// Passing variables with the default name:
///
/// ```
/// # use minijinja::render;
/// let name = "World";
/// println!("{}", render!("Hello {{ name }}!", name));
/// ```
///
/// Passing an explicit environment:
///
/// ```
/// # use minijinja::{Environment, render};
/// let env = Environment::new();
/// println!("{}", render!(in env, "Hello {{ name }}!", name => "World"));
/// ```
///
/// # Panics
///
/// This macro panics if the format string is an invalid template or the
/// template evaluation failed.
#[macro_export]
macro_rules! render {
(
in $env:expr,
$tmpl:expr
$(, $key:ident $(=> $value:expr)?)* $(,)?
) => {
($env).render_str($tmpl, $crate::context! { $($key $(=> $value)? ,)* })
.expect("failed to render expression")
};
(
$tmpl:expr
$(, $key:ident $(=> $value:expr)?)* $(,)?
) => {
$crate::render!(in $crate::__context::thread_local_env(), $tmpl, $($key $(=> $value)? ,)*)
}
}

#[test]
fn test_context() {
use crate::value::Value;
let var1 = 23;
let ctx = context!(var1, var2 => 42);
assert_eq!(ctx.get_attr("var1").unwrap(), Value::from(23));
assert_eq!(ctx.get_attr("var2").unwrap(), Value::from(42));
}

#[test]
fn test_render() {
let env = crate::Environment::new();
let rv = render!(in env, "Hello {{ name }}!", name => "World");
assert_eq!(rv, "Hello World!");

let rv = render!("Hello {{ name }}!", name => "World");
assert_eq!(rv, "Hello World!");

let rv = render!("Hello World!");
assert_eq!(rv, "Hello World!");
}

0 comments on commit 90cbcd1

Please sign in to comment.