Skip to content

Commit

Permalink
Allow subscripts on literal arrays/map (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
Keats authored Aug 26, 2024
1 parent 6b9cf0f commit 8d91a3a
Show file tree
Hide file tree
Showing 14 changed files with 128 additions and 94 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ TODO:
- [x] Uncomment all tests using filters/functions/tests
- [x] Parsing errors should report with the source context like Rust errors with the right spans
- [ ] Add more helpful errors when loading templates (eg a forloop with for key, key in value/missing includes etc)
- [ ] Some constant folding: maths, subscript on literals
- [x] Allow escape chars (eg \n) in strings concat, there was an issue about that in Zola
- [ ] Feature to load templates from a glob with optional dep
- [x] MAYBE: add a way to add global context to the Tera struct that are passed on every render automatically
Expand Down
4 changes: 3 additions & 1 deletion tera/bytes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import dis
import ast

text = """
"majeur" if age >= 18 else "mineur"
[1,2,3][0]
"""

print(ast.dump(ast.parse(text)))
print(dis.dis(text))

"""
Expand Down
22 changes: 12 additions & 10 deletions tera/src/functions.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::sync::Arc;
use crate::args::Kwargs;
use crate::errors::{Error, TeraResult};
use crate::Value;
use crate::value::FunctionResult;
use crate::vm::state::State;
use crate::Value;
use std::sync::Arc;

/// The function function type definition
pub trait Function<Res>: Sync + Send + 'static {
Expand All @@ -18,25 +18,25 @@ pub trait Function<Res>: Sync + Send + 'static {
}

impl<Func, Res> Function<Res> for Func
where
Func: Fn(Kwargs, &State) -> Res + Sync + Send + 'static,
Res: FunctionResult,
where
Func: Fn(Kwargs, &State) -> Res + Sync + Send + 'static,
Res: FunctionResult,
{
fn call(&self, kwargs: Kwargs, state: &State) -> Res {
(self)(kwargs, state)
}
}

type FunctionFunc = dyn Fn( Kwargs, &State) -> TeraResult<Value> + Sync + Send + 'static;
type FunctionFunc = dyn Fn(Kwargs, &State) -> TeraResult<Value> + Sync + Send + 'static;

#[derive(Clone)]
pub(crate) struct StoredFunction(Arc<FunctionFunc>);

impl StoredFunction {
pub fn new<Func, Res>(f: Func) -> Self
where
Func: Function<Res>,
Res: FunctionResult,
where
Func: Function<Res>,
Res: FunctionResult,
{
let closure = move |kwargs, state: &State| -> TeraResult<Value> {
f.call(kwargs, state).into_result()
Expand All @@ -55,7 +55,9 @@ pub(crate) fn range(kwargs: Kwargs, _: &State) -> TeraResult<Vec<isize>> {
let end = kwargs.must_get::<isize>("end")?;
let step_by = kwargs.get::<usize>("step_by")?.unwrap_or(1);
if start > end {
return Err(Error::message("Function `range` was called with a `start` argument greater than the `end` one"));
return Err(Error::message(
"Function `range` was called with a `start` argument greater than the `end` one",
));
}

Ok((start..end).step_by(step_by).collect())
Expand Down
2 changes: 1 addition & 1 deletion tera/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ mod args;
mod context;
mod errors;
mod filters;
mod functions;
mod parsing;
mod reporting;
mod template;
mod tera;
mod tests;
mod functions;
mod utils;
pub mod value;
pub(crate) mod vm;
Expand Down
30 changes: 25 additions & 5 deletions tera/src/parsing/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::fmt;
use std::sync::Arc;

use crate::utils::{Span, Spanned};
use crate::value::{Key, Value};
use crate::value::{format_map, Key, Value};
use crate::HashMap;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -90,12 +90,11 @@ impl fmt::Display for BinaryOperator {
pub enum Expression {
/// A constant: string, number, boolean, array or null
Const(Spanned<Value>),
/// An array that contains things not
/// An array that contains things that we need to look up in the context
Array(Spanned<Array>),
/// A hashmap defined in the template
/// A hashmap defined in the template where we need to look up in the context
Map(Spanned<Map>),
/// A variable to look up in the context.
/// Note that
Var(Spanned<Var>),
/// The `.` getter, as in item.field
GetAttr(Spanned<GetAttr>),
Expand All @@ -118,6 +117,14 @@ impl Expression {
matches!(self, Expression::Const(..))
}

pub fn is_array_or_map_literal(&self) -> bool {
match self {
Expression::Const(c) => c.as_map().is_some() || c.as_vec().is_some(),
Expression::Map(_) | Expression::Array(_) => true,
_ => false,
}
}

pub(crate) fn as_value(&self) -> Option<Value> {
match self {
Expression::Const(c) => Some(c.node().clone()),
Expand Down Expand Up @@ -202,6 +209,9 @@ impl fmt::Display for Expression {
Value::String(s, _) => write!(f, "'{}'", *s),
Value::I64(s) => write!(f, "{}", *s),
Value::F64(s) => write!(f, "{}", *s),
Value::U64(s) => write!(f, "{}", *s),
Value::U128(s) => write!(f, "{}", *s),
Value::I128(s) => write!(f, "{}", *s),
Value::Bool(s) => write!(f, "{}", *s),
Value::Array(s) => {
write!(f, "[")?;
Expand All @@ -217,7 +227,17 @@ impl fmt::Display for Expression {
write!(f, "]")
}
Value::Null => write!(f, "null"),
_ => unreachable!(),
Value::Undefined => write!(f, "undefined"),
Value::Bytes(_) => write!(f, "<bytes>"),
Value::Map(s) => {
let mut buf: Vec<u8> = Vec::new();
format_map(s, &mut buf).expect("failed to write map to vec");
write!(
f,
"{}",
std::str::from_utf8(&buf).expect("valid utf-8 in display")
)
}
},
Map(i) => write!(f, "{}", **i),
Array(i) => write!(f, "{}", **i),
Expand Down
54 changes: 35 additions & 19 deletions tera/src/parsing/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,29 @@ impl<'a> Parser<'a> {
.any(|b| *b == BodyContext::ForLoop)
}

// Parse something in brackets [..] after an ident or a literal array/map
fn parse_subscript(&mut self, expr: Expression) -> TeraResult<Expression> {
expect_token!(self, Token::LeftBracket, "[")?;
self.num_left_brackets += 1;
if self.num_left_brackets > MAX_NUM_LEFT_BRACKETS {
return Err(Error::syntax_error(
format!("Identifiers can only have up to {MAX_NUM_LEFT_BRACKETS} nested brackets."),
&self.current_span,
));
}

let sub_expr = self.parse_expression(0)?;
let mut span = expr.span().clone();
span.expand(&self.current_span);

let expr = Expression::GetItem(Spanned::new(GetItem { expr, sub_expr }, span));

expect_token!(self, Token::RightBracket, "]")?;
self.num_left_brackets -= 1;

Ok(expr)
}

/// Can be just an ident or a macro call/fn
fn parse_ident(&mut self, ident: &str) -> TeraResult<Expression> {
let mut start_span = self.current_span.clone();
Expand Down Expand Up @@ -226,26 +249,9 @@ impl<'a> Parser<'a> {
));
}
}
// Subscript
Some(Ok((Token::LeftBracket, _))) => {
expect_token!(self, Token::LeftBracket, "[")?;
self.num_left_brackets += 1;
if self.num_left_brackets > MAX_NUM_LEFT_BRACKETS {
return Err(Error::syntax_error(
format!("Identifiers can only have up to {MAX_NUM_LEFT_BRACKETS} nested brackets."),
&self.current_span,
));
}

let sub_expr = self.parse_expression(0)?;
start_span.expand(&self.current_span);

expr = Expression::GetItem(Spanned::new(
GetItem { expr, sub_expr },
start_span.clone(),
));

expect_token!(self, Token::RightBracket, "]")?;
self.num_left_brackets -= 1;
expr = self.parse_subscript(expr)?;
}
// Function
Some(Ok((Token::LeftParen, _))) => {
Expand Down Expand Up @@ -542,6 +548,16 @@ impl<'a> Parser<'a> {
Token::Ident("and") => BinaryOperator::And,
Token::Ident("or") => BinaryOperator::Or,
Token::Ident("is") => BinaryOperator::Is,
// A subscript. Should only be here after a literal array or map
Token::LeftBracket => {
if !lhs.is_array_or_map_literal() {
return Err(Error::syntax_error(
format!("Subscript is only allowed after a map or array literal"),
&self.current_span,
));
}
return self.parse_subscript(lhs);
}
// A ternary
Token::Ident("if") => {
self.next_or_error()?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
{{ (person in groupA) and person in groupB }}
{{ (query.tags ~ ' ' ~ tags) | trim }}
{{ a and a is containing(pat='a') or b and b is containing(pat='b') }}
{{ ['up', 'down', 'left', 'right'][1] }}
{{ {"hello": "world"}["hello"] }}

// Macro calls
{{ macros::input(label='Name', type='text') }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ Hello
{{ 2 / 0.5 }}
{{ 2.1 / 0.5 }}
{{ true and 10 }}
{{ true and not 10 }}
{{ true and not 10 }}
{{ ['up', 'down', 'left', 'right'][1] }}
{{ {"hello": "world"}["hello"] }}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ three' indent{})
(and (in person groupA) (in person groupB))
(| (~ (~ query.tags ' ') tags) trim{})
(and (or (and a (is a containing{pat='a'})) b) (is b containing{pat='b'}))
["up", "down", "left", "right"][1]
{hello: 'world'}['hello']
macros::input{label='Name', type='text'}
(| macros::input{} safe{})
macros::input{n=(- a 1)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ true
4.2
10
false
down
world
8 changes: 4 additions & 4 deletions tera/src/tera.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::args::ArgFromValue;
use crate::errors::{Error, TeraResult};
use crate::filters::{Filter, StoredFilter};
use crate::functions::{Function, StoredFunction};
use crate::template::{find_parents, Template};
use crate::tests::{StoredTest, Test, TestResult};
use crate::value::FunctionResult;
use crate::vm::interpreter::VirtualMachine;
use crate::{escape_html, Context, HashMap};
use crate::functions::{Function, StoredFunction};

/// Default template name used for `Tera::render_str` and `Tera::one_off`.
const ONE_OFF_TEMPLATE_NAME: &str = "__tera_one_off";
Expand Down Expand Up @@ -137,9 +137,9 @@ impl Tera {
///
/// If a function with that name already exists, it will be overwritten
pub fn register_function<Func, Res>(&mut self, name: &'static str, func: Func)
where
Func: Function<Res>,
Res: FunctionResult,
where
Func: Function<Res>,
Res: FunctionResult,
{
self.functions.insert(name, StoredFunction::new(func));
}
Expand Down
1 change: 0 additions & 1 deletion tera/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use crate::value::{Key, Map};
use crate::vm::state::State;
use crate::{HashMap, Value};


pub trait TestResult {
fn into_result(self) -> TeraResult<bool>;
}
Expand Down
Loading

0 comments on commit 8d91a3a

Please sign in to comment.