Skip to content

Commit

Permalink
Align extends semantics with Jinja2 (#140)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Nov 8, 2022
1 parent 41ef953 commit 07eb55a
Show file tree
Hide file tree
Showing 21 changed files with 263 additions and 63 deletions.
17 changes: 12 additions & 5 deletions minijinja/src/compiler/ast.rs
Expand Up @@ -59,10 +59,11 @@ pub enum Stmt<'a> {
WithBlock(Spanned<WithBlock<'a>>),
Set(Spanned<Set<'a>>),
SetBlock(Spanned<SetBlock<'a>>),
Block(Spanned<Block<'a>>),
AutoEscape(Spanned<AutoEscape<'a>>),
FilterBlock(Spanned<FilterBlock<'a>>),
#[cfg(feature = "multi-template")]
Block(Spanned<Block<'a>>),
#[cfg(feature = "multi-template")]
Import(Spanned<Import<'a>>),
#[cfg(feature = "multi-template")]
FromImport(Spanned<FromImport<'a>>),
Expand All @@ -86,10 +87,11 @@ impl<'a> fmt::Debug for Stmt<'a> {
Stmt::WithBlock(s) => fmt::Debug::fmt(s, f),
Stmt::Set(s) => fmt::Debug::fmt(s, f),
Stmt::SetBlock(s) => fmt::Debug::fmt(s, f),
Stmt::Block(s) => fmt::Debug::fmt(s, f),
Stmt::AutoEscape(s) => fmt::Debug::fmt(s, f),
Stmt::FilterBlock(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi-template")]
Stmt::Block(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi-template")]
Stmt::Extends(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "multi-template")]
Stmt::Include(s) => fmt::Debug::fmt(s, f),
Expand Down Expand Up @@ -193,6 +195,7 @@ pub struct SetBlock<'a> {

/// A block for inheritance elements.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "multi-template")]
pub struct Block<'a> {
pub name: &'a str,
pub body: Vec<Stmt<'a>>,
Expand Down Expand Up @@ -458,6 +461,7 @@ impl<'a> Map<'a> {
pub enum CallType<'ast, 'source> {
Function(&'source str),
Method(&'ast Expr<'source>, &'source str),
#[cfg(feature = "multi-template")]
Block(&'source str),
Object(&'ast Expr<'source>),
}
Expand All @@ -472,9 +476,12 @@ impl<'a> Call<'a> {
match self.expr {
Expr::Var(ref var) => CallType::Function(var.id),
Expr::GetAttr(ref attr) => {
if let Expr::Var(ref var) = attr.expr {
if var.id == "self" {
return CallType::Block(attr.name);
#[cfg(feature = "multi-template")]
{
if let Expr::Var(ref var) = attr.expr {
if var.id == "self" {
return CallType::Block(attr.name);
}
}
}
CallType::Method(&attr.expr, attr.name)
Expand Down
36 changes: 27 additions & 9 deletions minijinja/src/compiler/codegen.rs
Expand Up @@ -6,6 +6,7 @@ use crate::compiler::instructions::{
};
use crate::compiler::tokens::Span;
use crate::error::Error;
use crate::output::CaptureMode;
use crate::value::Value;

#[cfg(test)]
Expand Down Expand Up @@ -41,6 +42,8 @@ pub struct CodeGenerator<'source> {
span_stack: Vec<Span>,
filter_local_ids: BTreeMap<&'source str, LocalId>,
test_local_ids: BTreeMap<&'source str, LocalId>,
#[cfg(feature = "multi-template")]
has_extends: bool,
}

impl<'source> CodeGenerator<'source> {
Expand All @@ -54,6 +57,8 @@ impl<'source> CodeGenerator<'source> {
span_stack: Vec::new(),
filter_local_ids: BTreeMap::new(),
test_local_ids: BTreeMap::new(),
#[cfg(feature = "multi-template")]
has_extends: false,
}
}

Expand Down Expand Up @@ -99,6 +104,7 @@ impl<'source> CodeGenerator<'source> {
}

/// Creats a sub generator.
#[cfg(feature = "multi-template")]
fn new_subgenerator(&self) -> CodeGenerator<'source> {
let mut sub = CodeGenerator::new(self.instructions.name(), self.instructions.source());
sub.current_line = self.current_line;
Expand All @@ -107,6 +113,7 @@ impl<'source> CodeGenerator<'source> {
}

/// Finishes a sub generator and syncs it back.
#[cfg(feature = "multi-template")]
fn finish_subgenerator(&mut self, sub: CodeGenerator<'source>) -> Instructions<'source> {
self.current_line = sub.current_line;
let (instructions, blocks) = sub.finish();
Expand Down Expand Up @@ -226,6 +233,12 @@ impl<'source> CodeGenerator<'source> {
for node in &t.children {
ok!(self.compile_stmt(node));
}
#[cfg(feature = "multi-template")]
{
if self.has_extends {
self.add(Instruction::RenderParent);
}
}
}
ast::Stmt::EmitExpr(expr) => {
ok!(self.compile_emit_expr(expr));
Expand Down Expand Up @@ -259,7 +272,7 @@ impl<'source> CodeGenerator<'source> {
}
ast::Stmt::SetBlock(set_block) => {
self.set_line_from_span(set_block.span());
self.add(Instruction::BeginCapture);
self.add(Instruction::BeginCapture(CaptureMode::Capture));
for node in &set_block.body {
ok!(self.compile_stmt(node));
}
Expand All @@ -269,9 +282,6 @@ impl<'source> CodeGenerator<'source> {
}
ok!(self.compile_assignment(&set_block.target));
}
ast::Stmt::Block(block) => {
ok!(self.compile_block(block));
}
ast::Stmt::AutoEscape(auto_escape) => {
self.set_line_from_span(auto_escape.span());
ok!(self.compile_expr(&auto_escape.enabled));
Expand All @@ -283,7 +293,7 @@ impl<'source> CodeGenerator<'source> {
}
ast::Stmt::FilterBlock(filter_block) => {
self.set_line_from_span(filter_block.span());
self.add(Instruction::BeginCapture);
self.add(Instruction::BeginCapture(CaptureMode::Capture));
for node in &filter_block.body {
ok!(self.compile_stmt(node));
}
Expand All @@ -292,8 +302,12 @@ impl<'source> CodeGenerator<'source> {
self.add(Instruction::Emit);
}
#[cfg(feature = "multi-template")]
ast::Stmt::Block(block) => {
ok!(self.compile_block(block));
}
#[cfg(feature = "multi-template")]
ast::Stmt::Import(import) => {
self.add(Instruction::BeginCapture);
self.add(Instruction::BeginCapture(CaptureMode::Discard));
self.add(Instruction::PushWith);
ok!(self.compile_expr(&import.expr));
self.add_with_span(Instruction::Include(false), import.span());
Expand All @@ -304,7 +318,7 @@ impl<'source> CodeGenerator<'source> {
}
#[cfg(feature = "multi-template")]
ast::Stmt::FromImport(from_import) => {
self.add(Instruction::BeginCapture);
self.add(Instruction::BeginCapture(CaptureMode::Discard));
self.add(Instruction::PushWith);
ok!(self.compile_expr(&from_import.expr));
self.add_with_span(Instruction::Include(false), from_import.span());
Expand All @@ -321,7 +335,8 @@ impl<'source> CodeGenerator<'source> {
ast::Stmt::Extends(extends) => {
self.set_line_from_span(extends.span());
ok!(self.compile_expr(&extends.name));
self.add(Instruction::LoadBlocks);
self.add_with_span(Instruction::LoadBlocks, extends.span());
self.has_extends = true;
}
#[cfg(feature = "multi-template")]
ast::Stmt::Include(include) => {
Expand All @@ -337,6 +352,7 @@ impl<'source> CodeGenerator<'source> {
Ok(())
}

#[cfg(feature = "multi-template")]
fn compile_block(&mut self, block: &ast::Spanned<ast::Block<'source>>) -> Result<(), Error> {
self.set_line_from_span(block.span());
let mut sub = self.new_subgenerator();
Expand Down Expand Up @@ -450,6 +466,7 @@ impl<'source> CodeGenerator<'source> {
return Ok(());
}
}
#[cfg(feature = "multi-template")]
ast::CallType::Block(name) => {
self.add(Instruction::CallBlock(name));
return Ok(());
Expand Down Expand Up @@ -664,8 +681,9 @@ impl<'source> CodeGenerator<'source> {
}
self.add(Instruction::CallFunction(name, c.args.len()));
}
#[cfg(feature = "multi-template")]
ast::CallType::Block(name) => {
self.add(Instruction::BeginCapture);
self.add(Instruction::BeginCapture(CaptureMode::Capture));
self.add(Instruction::CallBlock(name));
self.add(Instruction::EndCapture);
}
Expand Down
16 changes: 11 additions & 5 deletions minijinja/src/compiler/instructions.rs
Expand Up @@ -5,6 +5,7 @@ use std::fmt;
use similar_asserts::assert_eq;

use crate::compiler::tokens::Span;
use crate::output::CaptureMode;
use crate::value::Value;

/// This loop has the loop var.
Expand Down Expand Up @@ -152,17 +153,14 @@ pub enum Instruction<'source> {
/// Jump if the stack top evaluates to true or pops the value
JumpIfTrueOrPop(usize),

/// Call into a block.
CallBlock(&'source str),

/// Sets the auto escape flag to the current value.
PushAutoEscape,

/// Resets the auto escape flag to the previous value.
PopAutoEscape,

/// Begins capturing of output.
BeginCapture,
/// Begins capturing of output (false) or discard (true).
BeginCapture(CaptureMode),

/// Ends capturing of output.
EndCapture,
Expand All @@ -188,10 +186,18 @@ pub enum Instruction<'source> {
/// A fast loop recurse instruction without intermediate capturing.
FastRecurse,

/// Call into a block.
#[cfg(feature = "multi-template")]
CallBlock(&'source str),

/// Loads block from a template with name on stack ("extends")
#[cfg(feature = "multi-template")]
LoadBlocks,

/// Renders the parent template
#[cfg(feature = "multi-template")]
RenderParent,

/// Includes another template.
#[cfg(feature = "multi-template")]
Include(bool),
Expand Down
13 changes: 7 additions & 6 deletions minijinja/src/compiler/meta.rs
Expand Up @@ -133,12 +133,6 @@ pub fn find_macro_closure<'a>(m: &ast::Macro<'a>) -> HashSet<&'a str> {
assign_nested(&stmt.target, state);
visit_expr(&stmt.expr, state);
}
ast::Stmt::Block(stmt) => {
state.push();
state.assign("super");
stmt.body.iter().for_each(|x| walk(x, state));
state.pop();
}
ast::Stmt::AutoEscape(stmt) => {
state.push();
stmt.body.iter().for_each(|x| walk(x, state));
Expand All @@ -156,6 +150,13 @@ pub fn find_macro_closure<'a>(m: &ast::Macro<'a>) -> HashSet<&'a str> {
state.pop();
}
#[cfg(feature = "multi-template")]
ast::Stmt::Block(stmt) => {
state.push();
state.assign("super");
stmt.body.iter().for_each(|x| walk(x, state));
state.pop();
}
#[cfg(feature = "multi-template")]
ast::Stmt::Extends(_) | ast::Stmt::Include(_) => {}
#[cfg(feature = "multi-template")]
ast::Stmt::Import(stmt) => {
Expand Down
5 changes: 4 additions & 1 deletion minijinja/src/compiler/parser.rs
Expand Up @@ -150,6 +150,7 @@ impl<'a> TokenStream<'a> {

struct Parser<'a> {
stream: TokenStream<'a>,
#[allow(unused)]
in_macro: bool,
depth: usize,
}
Expand Down Expand Up @@ -638,14 +639,15 @@ impl<'a> Parser<'a> {
SetParseResult::Set(rv) => ast::Stmt::Set(respan!(rv)),
SetParseResult::SetBlock(rv) => ast::Stmt::SetBlock(respan!(rv)),
},
Token::Ident("block") => ast::Stmt::Block(respan!(ok!(self.parse_block()))),
Token::Ident("autoescape") => {
ast::Stmt::AutoEscape(respan!(ok!(self.parse_auto_escape())))
}
Token::Ident("filter") => {
ast::Stmt::FilterBlock(respan!(ok!(self.parse_filter_block())))
}
#[cfg(feature = "multi-template")]
Token::Ident("block") => ast::Stmt::Block(respan!(ok!(self.parse_block()))),
#[cfg(feature = "multi-template")]
Token::Ident("extends") => ast::Stmt::Extends(respan!(ok!(self.parse_extends()))),
#[cfg(feature = "multi-template")]
Token::Ident("include") => ast::Stmt::Include(respan!(ok!(self.parse_include()))),
Expand Down Expand Up @@ -820,6 +822,7 @@ impl<'a> Parser<'a> {
}
}

#[cfg(feature = "multi-template")]
fn parse_block(&mut self) -> Result<ast::Block<'a>, Error> {
if self.in_macro {
syntax_error!("block tags in macros are not allowed");
Expand Down
46 changes: 34 additions & 12 deletions minijinja/src/output.rs
Expand Up @@ -3,14 +3,22 @@ use std::{fmt, io};
use crate::utils::AutoEscape;
use crate::value::Value;

/// How should output be captured?
#[derive(Debug, Clone, Copy)]
pub enum CaptureMode {
Capture,
#[allow(unused)]
Discard,
}

/// An abstraction over [`Write`](std::fmt::Write) for the rendering.
///
/// This is a utility type used in the engine which can be written into like one
/// can write into an [`std::fmt::Write`] value. It's primarily used internally
/// in the engine but it's also passed to the custom formatter function.
pub struct Output<'a> {
w: &'a mut (dyn fmt::Write + 'a),
capture_stack: Vec<String>,
capture_stack: Vec<Option<String>>,
}

impl<'a> Output<'a> {
Expand All @@ -31,33 +39,38 @@ impl<'a> Output<'a> {

/// Creates a null output that writes nowhere.
pub(crate) fn null() -> Self {
static mut NULL_WRITER: NullWriter = NullWriter;
Self {
// SAFETY: this is safe as the null writer is a ZST
w: unsafe { &mut NULL_WRITER },
w: NullWriter::get_mut(),
capture_stack: Vec::new(),
}
}

/// Begins capturing into a string.
pub(crate) fn begin_capture(&mut self) {
self.capture_stack.push(String::new());
/// Begins capturing into a string or discard.
pub(crate) fn begin_capture(&mut self, mode: CaptureMode) {
self.capture_stack.push(match mode {
CaptureMode::Capture => Some(String::new()),
CaptureMode::Discard => None,
});
}

/// Ends capturing and returns the captured string as value.
pub(crate) fn end_capture(&mut self, auto_escape: AutoEscape) -> Value {
let captured = self.capture_stack.pop().unwrap();
if !matches!(auto_escape, AutoEscape::None) {
Value::from_safe_string(captured)
if let Some(captured) = self.capture_stack.pop().unwrap() {
if !matches!(auto_escape, AutoEscape::None) {
Value::from_safe_string(captured)
} else {
Value::from(captured)
}
} else {
Value::from(captured)
Value::UNDEFINED
}
}

#[inline(always)]
fn target(&mut self) -> &mut dyn fmt::Write {
match self.capture_stack.last_mut() {
Some(stream) => stream as _,
Some(Some(stream)) => stream as _,
Some(None) => NullWriter::get_mut(),
None => self.w,
}
}
Expand Down Expand Up @@ -94,6 +107,15 @@ impl fmt::Write for Output<'_> {

pub struct NullWriter;

impl NullWriter {
/// Returns a reference to the null writer.
pub fn get_mut() -> &'static mut NullWriter {
static mut NULL_WRITER: NullWriter = NullWriter;
// SAFETY: this is safe as the null writer is a ZST
unsafe { &mut NULL_WRITER }
}
}

impl fmt::Write for NullWriter {
#[inline]
fn write_str(&mut self, _s: &str) -> fmt::Result {
Expand Down

0 comments on commit 07eb55a

Please sign in to comment.