Skip to content
Merged

More #113

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
cd0124a
Some more non-exhaustive
Keats May 6, 2026
ac17eca
Add success snapshot tests for include
Keats May 6, 2026
f4e18e9
Add missing bytes component arg type
Keats May 6, 2026
1b0b3da
Check component calls inside components
Keats May 6, 2026
d701072
Fix 1 item for loop iteration with else
Keats May 6, 2026
a286586
Non destructive load_from_glob
Keats May 6, 2026
6aa9e0c
Fix display of bytes
Keats May 6, 2026
9c50136
Error on duplicate component/component args
Keats May 6, 2026
d31c622
Mark component body as safe
Keats May 6, 2026
ab30296
Fix plural filter for -1
Keats May 6, 2026
0fdee1e
Validate base for int filter
Keats May 6, 2026
170f59c
Fix multiple super() call in blocks
Keats May 6, 2026
4024183
Fix int/float comparison
Keats May 6, 2026
63974d5
Strings are iterable
Keats May 6, 2026
79b0d66
no super() outside of blocks
Keats May 6, 2026
2b9aac7
Use try_from_serializable for Context::from_serialize
Keats May 6, 2026
9156b28
Fix optional slicing
Keats May 6, 2026
865ad9a
Fix filesize_format binary arg
Keats May 6, 2026
6679c1a
Fix power binding power
Keats May 6, 2026
739f789
Fix indent filter last line missing \n
Keats May 6, 2026
0a1a56f
Ensure includes are captured
Keats May 6, 2026
331fd7a
Fix super() in top level block
Keats May 6, 2026
cedfe13
Make context! macro hygienic
Keats May 6, 2026
c242793
Fix foo.call() not being rejected by parser
Keats May 6, 2026
623b786
Fix delimiters validation using chars instead of bytes
Keats May 6, 2026
6e9ceda
Fix endraw ws check
Keats May 7, 2026
750538e
Treat super() as an expr
Keats May 7, 2026
aeda8b7
Require comma in between kwargs
Keats May 7, 2026
5fd5a7b
We can iterate on bytes
Keats May 7, 2026
a12fe51
Docs are written
Keats May 7, 2026
4722974
Also require comma between component args
Keats May 7, 2026
0939b4a
Match rem and div/floor_div for errors
Keats May 7, 2026
fc82f74
Fix order in get_value for include_parent
Keats May 7, 2026
465c6a7
Reserve body name for components
Keats May 7, 2026
ab8dc76
Use key for containing test and maps
Keats May 7, 2026
ef03ebb
Fix not precedence
Keats May 7, 2026
78df4c1
Fix inline component check
Keats May 7, 2026
803abd7
Fix not being allowed where it shouldnt
Keats May 7, 2026
388d8dc
Handle non-integer slicing args
Keats May 7, 2026
416a773
Make range function work in reverse
Keats May 7, 2026
bd4bdbf
Fix or/and powers
Keats May 7, 2026
6e40c0d
Another super() fix
Keats May 7, 2026
5e91bfa
Python encodes %
Keats May 7, 2026
20f097b
Allow Token::String, not just Token::Str in multiple places
Keats May 7, 2026
625d19f
Check RESERVED_NAMES in for loops
Keats May 7, 2026
081f5f9
Fix Tera::render_str not using the components from the given template
Keats May 7, 2026
ac50c18
Fix one undefined issue with paths
Keats May 7, 2026
bfe2ed5
Small stuff
Keats May 7, 2026
9a70cbb
Reserve none/None
Keats May 7, 2026
e29f1d9
Match | and ** precedence to jinja2/v1
Keats May 11, 2026
497fc47
Add table of precedence to docs
Keats May 11, 2026
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
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# Tera

See [migration guide](./MIGRATION.md).

Alpha, docs not written yet.
26 changes: 20 additions & 6 deletions docs/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,6 @@ You can use the following operators:
- `*`: performs a multiplication, `{{ 5 * 2 }}` will print `10`
- `%`: performs a modulo, `{{ 2 % 2 }}` will print `0`

The priority of operations is the following, from lowest to highest:

- `+` and `-`
- `*` and `/` and `%`

#### Comparisons

- `==`: checks whether the values are equal
Expand Down Expand Up @@ -317,6 +312,25 @@ You can use slicing on your arrays, similar to Python slicing:

You can do `{{ "majeur" if age >= 18 else "mineur" }}`. Both `if` and `else` are required.

#### Operator precedence

From lowest to highest binding power. Operators on the same row have the same precedence.

| Operators |
|---|
| `or` |
| `and` |
| `not` |
| `in`, `not in`, `is`, `is not` |
| `==`, `!=`, `<`, `<=`, `>`, `>=` |
| `+`, `-` |
| `*`, `/`, `//`, `%`, `~` |
| `**` |
| `\|` |
| `-` (unary) |
| `.`, `[]`, `()` |


### Filters

You can modify variables using **filters**.
Expand Down Expand Up @@ -1257,7 +1271,7 @@ There are 3 arguments, all integers:

- `end`: stop before `end`, mandatory
- `start`: where to start from, defaults to `0`
- `step_by`: with what number do we increment, defaults to `1`
- `step_by`: the step between values, defaults to `1`, use a negative value to count down

##### throw
The template rendering will error with the given message when encountered.
Expand Down
22 changes: 14 additions & 8 deletions tera-contrib/src/filesize_format.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use tera::{Kwargs, State};
use tera::{Kwargs, State, TeraResult};

/// Formats a number of bytes into a human-readable file size string.
/// Uses binary units (KiB, MiB, GiB, etc.) by default but can use decimal.
Expand All @@ -7,13 +7,13 @@ use tera::{Kwargs, State};
/// {{ num_bytes | filesize_format }}
/// {{ num_bytes | filesize_format(binary=false) }}
/// ```
pub fn filesize_format(val: u64, kwargs: Kwargs, _: &State) -> String {
let binary = kwargs.get::<bool>("binary").ok().flatten().unwrap_or(true);
pub fn filesize_format(val: u64, kwargs: Kwargs, _: &State) -> TeraResult<String> {
let binary = kwargs.get::<bool>("binary")?.unwrap_or(true);

if binary {
humansize::format_size(val, humansize::BINARY)
Ok(humansize::format_size(val, humansize::BINARY))
} else {
humansize::format_size(val, humansize::DECIMAL)
Ok(humansize::format_size(val, humansize::DECIMAL))
}
}

Expand All @@ -28,8 +28,14 @@ mod tests {
fn test_filesizeformat_binary() {
let ctx = Context::new();
let state = State::new(&ctx);
assert_eq!(filesize_format(1024, Kwargs::default(), &state), "1 KiB");
assert_eq!(filesize_format(1048576, Kwargs::default(), &state), "1 MiB");
assert_eq!(
filesize_format(1024, Kwargs::default(), &state).unwrap(),
"1 KiB"
);
assert_eq!(
filesize_format(1048576, Kwargs::default(), &state).unwrap(),
"1 MiB"
);
}

#[test]
Expand All @@ -39,7 +45,7 @@ mod tests {
let mut map = Map::new();
map.insert("binary".into(), false.into());
let kwargs = Kwargs::new(Arc::new(map));
assert_eq!(filesize_format(1000, kwargs, &state), "1 kB");
assert_eq!(filesize_format(1000, kwargs, &state).unwrap(), "1 kB");
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions tera-contrib/src/urlencode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const USERINFO_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET
/// with `/` not escaped
const PYTHON_ENCODE_SET: &AsciiSet = &USERINFO_ENCODE_SET
.remove(b'/')
.add(b'%')
.add(b':')
.add(b'?')
.add(b'#')
Expand Down
5 changes: 5 additions & 0 deletions tera/src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::value::Value;

/// The type of component arguments.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ComponentArgType {
#[allow(missing_docs)]
String,
Expand All @@ -21,6 +22,8 @@ pub enum ComponentArgType {
Array,
#[allow(missing_docs)]
Map,
#[allow(missing_docs)]
Bytes,
}

impl ComponentArgType {
Expand All @@ -34,6 +37,7 @@ impl ComponentArgType {
ComponentArgType::Number => "number",
ComponentArgType::Array => "array",
ComponentArgType::Map => "map",
ComponentArgType::Bytes => "bytes",
}
}
}
Expand All @@ -54,6 +58,7 @@ impl From<Type> for ComponentArgType {
Type::Number => ComponentArgType::Number,
Type::Array => ComponentArgType::Array,
Type::Map => ComponentArgType::Map,
Type::Bytes => ComponentArgType::Bytes,
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions tera/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl Context {
/// Meant to be used if you have a hashmap or a struct and don't want to insert values
/// one by one in the context.
pub fn from_serialize<T: Serialize + ?Sized>(value: &T) -> crate::TeraResult<Self> {
let val = Value::from_serializable(value);
let val = Value::try_from_serializable(value)?;
let type_name = val.name();

match val.into_map() {
Expand Down Expand Up @@ -125,7 +125,7 @@ macro_rules! context {
)*
) => {
{
let mut context = Context::new();
let mut context = $crate::Context::new();
$(
context.insert(stringify!($key), $($value)?);
)*
Expand Down
31 changes: 18 additions & 13 deletions tera/src/delimiters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::errors::{Error, TeraResult};

/// This allows customizing the delimiters used for blocks, variables, and comments in case
/// you want to template files that contains text like `{{`, like LaTeX.
/// Delimiters need to be 2 ASCII characters.
/// Delimiters need to be exactly 2 bytes long (e.g. `{{`, `«`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Delimiters {
/// Start delimiter for blocks, default: `{%`
Expand Down Expand Up @@ -37,32 +37,32 @@ impl Default for Delimiters {
impl Delimiters {
/// Returns an error if any delimiter is empty or if there are conflicts
pub(crate) fn validate(&self) -> TeraResult<()> {
if self.block_start.chars().count() != 2 {
if self.block_start.len() != 2 {
return Err(Error::message(
"`block_start` delimiter must be 2 characters",
"`block_start` delimiter must be 2 bytes long",
));
}
if self.block_end.chars().count() != 2 {
return Err(Error::message("`block_end` delimiter must be 2 characters"));
if self.block_end.len() != 2 {
return Err(Error::message("`block_end` delimiter must be 2 bytes long"));
}
if self.variable_start.chars().count() != 2 {
if self.variable_start.len() != 2 {
return Err(Error::message(
"`variable_start` delimiter must be 2 characters",
"`variable_start` delimiter must be 2 bytes long",
));
}
if self.variable_end.chars().count() != 2 {
if self.variable_end.len() != 2 {
return Err(Error::message(
"`variable_end` delimiter must be 2 characters",
"`variable_end` delimiter must be 2 bytes long",
));
}
if self.comment_start.chars().count() != 2 {
if self.comment_start.len() != 2 {
return Err(Error::message(
"`comment_start` delimiter must be 2 characters",
"`comment_start` delimiter must be 2 bytes long",
));
}
if self.comment_end.chars().count() != 2 {
if self.comment_end.len() != 2 {
return Err(Error::message(
"`comment_end` delimiter must be 2 characters",
"`comment_end` delimiter must be 2 bytes long",
));
}

Expand Down Expand Up @@ -107,6 +107,11 @@ mod tests {
comment_start: "[[".into(),
..Delimiters::default()
},
Delimiters {
// 3 bytes
block_start: "日".into(),
..Delimiters::default()
},
];

for i in inputs {
Expand Down
4 changes: 3 additions & 1 deletion tera/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::utils::Span;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Note {
pub(crate) label: String,
pub(crate) filename: String,
pub(crate) source: String,
pub(crate) span: Span,
Expand Down Expand Up @@ -50,8 +51,9 @@ impl ReportError {
self.source = source.to_string();
}

pub(crate) fn add_note(&mut self, filename: &str, source: &str, span: &Span) {
pub(crate) fn add_note(&mut self, label: &str, filename: &str, source: &str, span: &Span) {
self.notes.push(Note {
label: label.to_string(),
filename: filename.to_string(),
source: source.to_string(),
span: span.clone(),
Expand Down
13 changes: 11 additions & 2 deletions tera/src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ pub(crate) fn pluralize(val: Value, kwargs: Kwargs, _: &State) -> TeraResult<Str
let plural = kwargs.get::<&str>("plural")?.unwrap_or("s");

let is_singular = match val.as_i128() {
Some(n) => n == 1,
Some(n) => n == 1 || n == -1,
None => {
return Err(Error::message(format!(
"pluralize filter requires an integer, got `{}`",
Expand Down Expand Up @@ -271,6 +271,10 @@ pub(crate) fn indent(val: &str, kwargs: Kwargs, _: &State) -> TeraResult<String>
res.push_str(line);
}

if val.ends_with('\n') {
res.push('\n');
}

Ok(res)
}

Expand All @@ -281,6 +285,11 @@ pub(crate) fn as_str(val: Value, _: Kwargs, _: &State) -> String {
/// Converts a Value into an int. It defaults to a base of `10` but can be changed.
pub(crate) fn int(val: Value, kwargs: Kwargs, _: &State) -> TeraResult<Value> {
let base = kwargs.get::<u32>("base")?.unwrap_or(10);
if !(2..=36).contains(&base) {
return Err(Error::message(format!(
"int filter `base` must be between 2 and 36, got {base}"
)));
}

let handle_f64 =
|v: f64| {
Expand Down Expand Up @@ -635,7 +644,7 @@ pub(crate) fn group_by(val: Vec<Value>, kwargs: Kwargs, _: &State) -> TeraResult
match v.get_from_path(attribute) {
x if x.is_undefined() => {
return Err(Error::message(format!(
"Value {v} does not have an attribute after following path; {attribute}"
"Value {v} does not have an attribute after following path: {attribute}"
)));
}
x if x.is_none() => (),
Expand Down
27 changes: 19 additions & 8 deletions tera/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ impl StoredFunction {
const MAX_RANGE_LEN: usize = 100_000;

pub(crate) fn range(kwargs: Kwargs, _: &State) -> TeraResult<Vec<isize>> {
let start = kwargs.get::<isize>("start")?.unwrap_or_default();
let end = kwargs.must_get::<isize>("end")?;
let step_by = kwargs.get::<usize>("step_by")?.unwrap_or(1);
if start > end {
let start = kwargs.get::<i128>("start")?.unwrap_or_default();
let end = kwargs.must_get::<i128>("end")?;
let step_by = kwargs.get::<i128>("step_by")?.unwrap_or(1);
if start > end && step_by > 0 {
return Err(Error::message(
"Function `range` was called with a `start` argument greater than the `end` one",
));
Expand All @@ -79,16 +79,27 @@ pub(crate) fn range(kwargs: Kwargs, _: &State) -> TeraResult<Vec<isize>> {
));
}

let span = (end as i128) - (start as i128);
let step = step_by as i128;
let len = (span + step - 1) / step;
let len = if step_by > 0 {
let span = end - start;
(span + step_by - 1) / step_by
} else if start <= end {
0
} else {
let span = start - end;
let step = -step_by;
(span + step - 1) / step
};
if len > MAX_RANGE_LEN as i128 {
return Err(Error::message(format!(
"Function `range` would produce {len} elements, which exceeds the limit of {MAX_RANGE_LEN}"
)));
}

Ok((start..end).step_by(step_by).collect())
let mut values = Vec::with_capacity(len as usize);
for i in 0..len {
values.push((start + i * step_by) as isize);
}
Ok(values)
}

pub(crate) fn throw(kwargs: Kwargs, _: &State) -> TeraResult<bool> {
Expand Down
9 changes: 7 additions & 2 deletions tera/src/parsing/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ pub enum Type {
Number,
Array,
Map,
Bytes,
}

impl FromStr for Type {
Expand All @@ -660,8 +661,9 @@ impl FromStr for Type {
"number" => Ok(Type::Number),
"array" => Ok(Type::Array),
"map" => Ok(Type::Map),
"bytes" => Ok(Type::Bytes),
_ => Err(Error::message(format!(
"Found {s} but the only types allowed are: string, bool, integer, float, number, array and map"
"Found {s} but the only types allowed are: string, bool, integer, float, number, array, map and bytes"
))),
}
}
Expand All @@ -677,6 +679,7 @@ impl Type {
Type::Number => "number",
Type::Array => "array",
Type::Map => "map",
Type::Bytes => "bytes",
}
}

Expand All @@ -694,6 +697,7 @@ impl Type {
Type::Number => value.is_number(),
Type::Map => value.is_map(),
Type::Array => value.is_array(),
Type::Bytes => value.is_bytes(),
}
}

Expand All @@ -709,7 +713,8 @@ impl Type {
ValueKind::F64 => Some(Type::Float),
ValueKind::Array => Some(Type::Array),
ValueKind::Map => Some(Type::Map),
ValueKind::Undefined | ValueKind::None | ValueKind::Bytes => None,
ValueKind::Bytes => Some(Type::Bytes),
ValueKind::Undefined | ValueKind::None => None,
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions tera/src/parsing/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,7 @@ impl Compiler {
.or_default()
.push(span.clone());

let is_inline = component_call.body.is_empty();
if !is_inline {
if !component_call.self_closing {
self.chunk.add(Instruction::Capture, None);
for node in component_call.body {
self.compile_node(node);
Expand All @@ -274,7 +273,7 @@ impl Compiler {

self.compile_map_entries(component_call.kwargs, None);

if is_inline {
if component_call.self_closing {
self.chunk.add(
Instruction::RenderInlineComponent(component_call.name),
Some(span),
Expand Down
Loading
Loading