Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added preliminary macro support #123

Merged
merged 8 commits into from Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -10,6 +10,12 @@ All notable changes to MiniJinja are documented here.
- Improve error reporting for failures in blocks and trying to
`super()` when there is no parent block.
- Performance improvements.
- Added support for `{% import %}` / `{% from .. import .. %}`
and `{% macro %}`. (#123)
- Added `Value::is_kwargs` which disambiugates if an object passed
to a function or filter is a normal object or if it represents
keyword arguments.
- Added the ability to call functions stored on objects.

# 0.22.1

Expand Down
17 changes: 13 additions & 4 deletions COMPATIBILITY.md
Expand Up @@ -38,6 +38,10 @@ mapping them to filter functions is tricky. This also means that some filters i
MiniJinja do not accept the parameters with keyword arguments whereas in Jinja2
they do.

### Variadic Calls

MiniJinja does not support the `*args` and `**kwargs` syntax for calls.

### Undefined

The Jinja2 undefined type tracks the origin of creation, in MiniJinja the undefined
Expand Down Expand Up @@ -87,14 +91,19 @@ MiniJinja is different as mentioned above.

### `{% import %}`

This tag is currently not supported. If support for macros is added
this would make sense to add.
This tag is supported but the returned item is a map of the exported local
variables. This means that the rendered content of the template is lost.

### `{% macro %}`

This tag is currently not supported.
The macro tag works very similar to Jinja2 but with some differences. Most
importantly the special `caller`, `varargs` and `kwargs` arguments are not
supported. The external introspectable attributes `catch_kwargs`, `catch_varargs`
and `caller` are not supported.

### `{% call %}`

**Tracking issue:** [#44](https://github.com/mitsuhiko/minijinja/issues/44)
This tag is not supported.

### `{% with %}`

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -7,7 +7,7 @@ build:
@cargo build --all

doc:
@cd minijinja; RUSTC_BOOTSTRAP=1 RUSTDOCFLAGS="--cfg=docsrs --html-in-header doc-header.html" cargo doc --all --no-deps --features=$(DOC_FEATURES)
@cd minijinja; RUSTC_BOOTSTRAP=1 RUSTDOCFLAGS="--cfg=docsrs --html-in-header doc-header.html" cargo doc -p minijinja -p minijinja-autoreload --no-deps --features=$(DOC_FEATURES)

test:
@$(MAKE) run-tests FEATURES=$(TEST_FEATURES)
Expand Down
2 changes: 1 addition & 1 deletion doc-header.html
Expand Up @@ -4,7 +4,7 @@
<script>
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll("pre code").forEach(function(node) {
if (node.parentNode.className.match(/jinja/)) {
if (node.parentNode.className.match(/jinja|json/)) {
hljs.highlightElement(node.parentNode);
}
});
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Expand Up @@ -16,6 +16,7 @@ the `cargo run` command. Alternatively you can do `cargo run -p example-name`.
* [hello](hello): minimal Hello World example.
* [inheritance](inheritance): demonstrates how to use template inheritance.
* [loader](loader): shows how to load templates dynamically at runtime.
* [macros](macros): Demonstrates how to use macros and imports.
* [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.
Expand Down
9 changes: 9 additions & 0 deletions examples/macros/Cargo.toml
@@ -0,0 +1,9 @@
[package]
name = "macros"
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" }
7 changes: 7 additions & 0 deletions examples/macros/README.md
@@ -0,0 +1,7 @@
# macros

This example demonstrates how to use macros and imports.

```console
$ cargo run
```
4 changes: 4 additions & 0 deletions examples/macros/src/macros.html
@@ -0,0 +1,4 @@
{% macro render_input(name, value, type="text") -%}
<input name="{{ name }}" value="{{ value }}" type="{{ type }}">
{%- endmacro %}
{% set alias = render_input %}
16 changes: 16 additions & 0 deletions examples/macros/src/main.rs
@@ -0,0 +1,16 @@
use minijinja::{context, Environment};

fn main() {
let mut env = Environment::new();
env.add_template("macros.html", include_str!("macros.html"))
.unwrap();
env.add_template("template.html", include_str!("template.html"))
.unwrap();

let template = env.get_template("template.html").unwrap();
let context = context! {
username => "John Doe"
};

println!("{}", template.render(&context).unwrap());
}
15 changes: 15 additions & 0 deletions examples/macros/src/template.html
@@ -0,0 +1,15 @@
<!doctype html>
<title>Demo</title>
{%- import "macros.html" as all_macros %}
{%- from "macros.html" import render_input %}
<form action="" method="post">
<dl>
<dt>Username</dt>
<dd>{{ render_input("username", username) }}</dd>
<dt>Password</dt>
<dd>{{ render_input("password", type="password") }}</dd>
</dl>
</form>
<!--
Declared macros: {{ all_macros|safe }}
-->
59 changes: 57 additions & 2 deletions minijinja/src/compiler/ast.rs
Expand Up @@ -4,7 +4,8 @@ use std::ops::Deref;
use std::fmt;

use crate::compiler::tokens::Span;
use crate::value::{Value, ValueMap, ValueRepr};
use crate::key::Key;
use crate::value::{MapType, Value, ValueMap, ValueRepr};

/// Container for nodes with location info.
///
Expand Down Expand Up @@ -63,6 +64,9 @@ pub enum Stmt<'a> {
Include(Spanned<Include<'a>>),
AutoEscape(Spanned<AutoEscape<'a>>),
FilterBlock(Spanned<FilterBlock<'a>>),
Macro(Spanned<Macro<'a>>),
Import(Spanned<Import<'a>>),
FromImport(Spanned<FromImport<'a>>),
}

#[cfg(feature = "internal_debug")]
Expand All @@ -82,6 +86,9 @@ impl<'a> fmt::Debug for Stmt<'a> {
Stmt::Include(s) => fmt::Debug::fmt(s, f),
Stmt::AutoEscape(s) => fmt::Debug::fmt(s, f),
Stmt::FilterBlock(s) => fmt::Debug::fmt(s, f),
Stmt::Macro(s) => fmt::Debug::fmt(s, f),
Stmt::Import(s) => fmt::Debug::fmt(s, f),
Stmt::FromImport(s) => fmt::Debug::fmt(s, f),
}
}
}
Expand All @@ -102,6 +109,7 @@ pub enum Expr<'a> {
Call(Spanned<Call<'a>>),
List(Spanned<List<'a>>),
Map(Spanned<Map<'a>>),
Kwargs(Spanned<Kwargs<'a>>),
}

#[cfg(feature = "internal_debug")]
Expand All @@ -121,6 +129,7 @@ impl<'a> fmt::Debug for Expr<'a> {
Expr::Call(s) => fmt::Debug::fmt(s, f),
Expr::List(s) => fmt::Debug::fmt(s, f),
Expr::Map(s) => fmt::Debug::fmt(s, f),
Expr::Kwargs(s) => fmt::Debug::fmt(s, f),
}
}
}
Expand Down Expand Up @@ -206,6 +215,29 @@ pub struct FilterBlock<'a> {
pub body: Vec<Stmt<'a>>,
}

/// Declares a macro.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub struct Macro<'a> {
pub name: &'a str,
pub args: Vec<Expr<'a>>,
pub defaults: Vec<Expr<'a>>,
pub body: Vec<Stmt<'a>>,
}

/// A "from" import
#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub struct FromImport<'a> {
pub expr: Expr<'a>,
pub names: Vec<(Expr<'a>, Option<Expr<'a>>)>,
}

/// A full module import
#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub struct Import<'a> {
pub expr: Expr<'a>,
pub name: Expr<'a>,
}

/// Outputs the expression.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub struct EmitExpr<'a> {
Expand Down Expand Up @@ -351,6 +383,29 @@ impl<'a> List<'a> {
}
}

/// Creates a map of kwargs
#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub struct Kwargs<'a> {
pub pairs: Vec<(&'a str, Expr<'a>)>,
}

impl<'a> Kwargs<'a> {
pub fn as_const(&self) -> Option<Value> {
if !self.pairs.iter().all(|x| matches!(x.1, Expr::Const(_))) {
return None;
}

let mut rv = ValueMap::new();
for (key, value) in &self.pairs {
if let Expr::Const(value) = value {
rv.insert(Key::make_string_key(key), value.value.clone());
}
}

Some(Value(ValueRepr::Map(rv.into(), MapType::Kwargs)))
}
}

/// Creates a map of values.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub struct Map<'a> {
Expand All @@ -376,7 +431,7 @@ impl<'a> Map<'a> {
}
}

Some(Value(ValueRepr::Map(rv.into())))
Some(Value(ValueRepr::Map(rv.into(), MapType::Normal)))
}
}

Expand Down