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

Align extends semantics with Jinja2 #140

Merged
merged 3 commits into from Nov 8, 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
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