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

Allow subscripts on literal arrays/map #42

Merged
merged 1 commit into from
Aug 26, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 2 additions & 0 deletions .../snapshot_tests/snapshots/[email protected]
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