Skip to content
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 MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ A lot of things have changed for the better.
* `{{ hey }}` should error if hey is undefined
* `{{ existing.hey }}` should error if hey is undefined but existing is
* `{{ hey or 1 }}` should print 1
* `{{ false and user.name }}` will not evaluate `user.name` and print `false`
* `{% if hey or true %}` should be truthy
* `{% if hey.other or true %}` should error if `hey` is not defined (currently truthy)
* `{{ hey.other or 1 }}` should error if `hey` is not defined (currently prints "true")
Expand Down
4 changes: 3 additions & 1 deletion tera-contrib/src/dates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ fn parse_to_zoned(val: &Value, tz: Option<TimeZone>) -> TeraResult<Zoned> {
))
})
} else if let Some(Number::Integer(ts)) = val.as_number() {
Timestamp::new(ts as i64, 0)
let ts = i64::try_from(ts)
.map_err(|_| tera::Error::message(format!("Invalid timestamp: {ts}")))?;
Timestamp::new(ts, 0)
.map(|t| t.to_zoned(default_tz))
.map_err(|e| tera::Error::message(format!("Invalid timestamp: {e}")))
} else {
Expand Down
7 changes: 6 additions & 1 deletion tera-contrib/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ pub fn format(val: Value, kwargs: Kwargs, _: &State) -> TeraResult<String> {
formatx::formatx!(&fmt_str, s)
.map_err(|e| Error::message(format!("format error: {}", e)))
}
ValueKind::I64 | ValueKind::I128 | ValueKind::U64 | ValueKind::U128 => {
ValueKind::I64 | ValueKind::I128 | ValueKind::U64 => {
let n = val.as_i128().unwrap();
formatx::formatx!(&fmt_str, n)
.map_err(|e| Error::message(format!("format error: {}", e)))
}
ValueKind::U128 => {
let n = val.as_u128().unwrap();
formatx::formatx!(&fmt_str, n)
.map_err(|e| Error::message(format!("format error: {}", e)))
}
ValueKind::F64 => {
let n = val.as_number().unwrap();
let f = n.as_float();
Expand Down
46 changes: 42 additions & 4 deletions tera/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,39 @@ use crate::errors::{Error, TeraResult};
use crate::value::number::Number;
use crate::value::{Key, Map, ValueInner};

pub trait ArgFromValue<'k> {
mod private {
use super::{Map, Number, Value};
use std::borrow::Cow;

pub trait Sealed {}

impl Sealed for bool {}
impl Sealed for f32 {}
impl Sealed for f64 {}
impl Sealed for u8 {}
impl Sealed for u16 {}
impl Sealed for u32 {}
impl Sealed for u64 {}
impl Sealed for u128 {}
impl Sealed for usize {}
impl Sealed for i8 {}
impl Sealed for i16 {}
impl Sealed for i32 {}
impl Sealed for i64 {}
impl Sealed for i128 {}
impl Sealed for isize {}
impl Sealed for String {}
impl Sealed for &str {}
impl<'a> Sealed for Cow<'a, str> {}
impl Sealed for Value {}
impl Sealed for &Value {}
impl Sealed for Number {}
impl Sealed for Map {}
impl<T: Sealed> Sealed for Vec<T> {}
}

#[doc(hidden)]
pub trait ArgFromValue<'k>: private::Sealed {
type Output;

fn from_value(value: &'k Value) -> TeraResult<Self::Output>;
Expand Down Expand Up @@ -135,9 +167,15 @@ impl<'k> ArgFromValue<'k> for Number {
type Output = Number;

fn from_value(value: &'k Value) -> TeraResult<Self::Output> {
value
.as_number()
.ok_or_else(|| Error::invalid_arg_type("Number", value.name()))
if let Some(n) = value.as_number() {
Ok(n)
} else if value.is_number() {
Err(Error::message(format!(
"Number `{value}` is out of range for i128"
)))
} else {
Err(Error::invalid_arg_type("Number", value.name()))
}
}
}

Expand Down
36 changes: 19 additions & 17 deletions tera/src/delimiters.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
use std::borrow::Cow;

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.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Delimiters {
/// Start delimiter for blocks, default: `{%`
pub block_start: &'static str,
pub block_start: Cow<'static, str>,
/// End delimiter for blocks, default: `%}`
pub block_end: &'static str,
pub block_end: Cow<'static, str>,
/// Start delimiter for variables, default: `{{`
pub variable_start: &'static str,
pub variable_start: Cow<'static, str>,
/// End delimiter for variables, default: `}}`
pub variable_end: &'static str,
pub variable_end: Cow<'static, str>,
/// Start delimiter for comments, default: `{#`
pub comment_start: &'static str,
pub comment_start: Cow<'static, str>,
/// End delimiter for comments, default: `#}`
pub comment_end: &'static str,
pub comment_end: Cow<'static, str>,
}

impl Default for Delimiters {
fn default() -> Self {
Self {
block_start: "{%",
block_end: "%}",
variable_start: "{{",
variable_end: "}}",
comment_start: "{#",
comment_end: "#}",
block_start: "{%".into(),
block_end: "%}".into(),
variable_start: "{{".into(),
variable_end: "}}".into(),
comment_start: "{#".into(),
comment_end: "#}".into(),
}
}
}
Expand Down Expand Up @@ -93,16 +95,16 @@ mod tests {
fn errors_on_invalid_delimiters() {
let inputs = vec![
Delimiters {
block_start: "",
block_start: "".into(),
..Delimiters::default()
},
Delimiters {
block_start: "[[[",
block_start: "[[[".into(),
..Delimiters::default()
},
Delimiters {
block_start: "[[",
comment_start: "[[",
block_start: "[[".into(),
comment_start: "[[".into(),
..Delimiters::default()
},
];
Expand Down
35 changes: 28 additions & 7 deletions tera/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::error::Error as StdError;
use crate::utils::Span;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Note {
pub(crate) struct Note {
pub(crate) filename: String,
pub(crate) source: String,
pub(crate) span: Span,
Expand All @@ -23,7 +23,7 @@ pub struct ReportError {
}

impl ReportError {
pub fn new(message: String, filename: &str, source: &str, span: &Span) -> Self {
pub(crate) fn new(message: String, filename: &str, source: &str, span: &Span) -> Self {
Self {
message,
filename: filename.to_string(),
Expand All @@ -34,7 +34,7 @@ impl ReportError {
}

/// Create a ReportError without filename/source - must call set_source before generating report
pub fn new_without_source(message: String, span: &Span) -> Self {
pub(crate) fn new_without_source(message: String, span: &Span) -> Self {
Self {
message,
filename: String::new(),
Expand All @@ -44,12 +44,12 @@ impl ReportError {
}
}

pub fn set_source(&mut self, filename: &str, source: &str) {
pub(crate) fn set_source(&mut self, filename: &str, source: &str) {
self.filename = filename.to_string();
self.source = source.to_string();
}

pub fn add_note(&mut self, filename: &str, source: &str, span: &Span) {
pub(crate) fn add_note(&mut self, filename: &str, source: &str, span: &Span) {
self.notes.push(Note {
filename: filename.to_string(),
source: source.to_string(),
Expand All @@ -61,12 +61,29 @@ impl ReportError {
generate_report(self)
}

pub fn unexpected_end_of_input(span: &Span) -> Self {
pub fn message(&self) -> &str {
&self.message
}

pub fn span(&self) -> &Span {
&self.span
}

pub fn filename(&self) -> &str {
&self.filename
}

pub fn source(&self) -> &str {
&self.source
}

pub(crate) fn unexpected_end_of_input(span: &Span) -> Self {
Self::new_without_source("Unexpected end of input".to_string(), span)
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
/// Generic error
Msg(String),
Expand Down Expand Up @@ -193,7 +210,7 @@ impl fmt::Display for ErrorKind {

#[derive(Debug)]
pub struct Error {
pub kind: ErrorKind,
pub(crate) kind: ErrorKind,
// If the error comes from some third party libs, TODO we need that?
pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
Expand All @@ -209,6 +226,10 @@ impl Error {
Self { kind, source: None }
}

pub fn kind(&self) -> &ErrorKind {
&self.kind
}

/// Creates generic error with a source
pub fn chain(value: impl ToString, source: impl Into<Box<dyn StdError + Send + Sync>>) -> Self {
Self {
Expand Down
22 changes: 9 additions & 13 deletions tera/src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,15 +282,14 @@ pub(crate) fn as_str(val: Value, _: Kwargs, _: &State) -> String {
pub(crate) fn int(val: Value, kwargs: Kwargs, _: &State) -> TeraResult<Value> {
let base = kwargs.get::<u32>("base")?.unwrap_or(10);

let handle_f64 = |v: f64| {
if let Some(i) = Number::Float(v).as_integer() {
Ok(i.into())
} else {
Err(Error::message(format!(
"The float {v} would have to be truncated to convert to an int"
)))
}
};
let handle_f64 =
|v: f64| {
Number::Float(v).as_integer().map(Into::into).ok_or_else(|| {
Error::message(format!(
"The float {v} cannot be converted to an int (non-integer or out of i128 range)"
))
})
};

match val.kind() {
ValueKind::String => {
Expand Down Expand Up @@ -331,10 +330,7 @@ pub(crate) fn int(val: Value, kwargs: Kwargs, _: &State) -> TeraResult<Value> {
let v = val.as_i128().unwrap();
Ok(v.into())
}
ValueKind::U128 => {
let v = val.as_i128().unwrap() as u128;
Ok(v.into())
}
ValueKind::U128 => Ok(val),
ValueKind::F64 => {
let v = val.as_f64().unwrap();
handle_f64(v)
Expand Down
60 changes: 59 additions & 1 deletion tera/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,62 @@
//! # Tera
//!
//! A powerful, fast and easy-to-use template engine for Rust
//!
//! This crate provides an implementation of the Tera template engine, which is designed for use in
//! Rust applications. Inspired by [Jinja2] and [Django] templates, Tera provides a familiar and
//! expressive syntax for creating dynamic HTML, XML, and other text-based documents. It supports
//! template inheritance, variable interpolation, conditionals, loops, filters, and custom
//! functions, enabling developers to build complex applications with ease.
//!
//! See the [site](http://keats.github.io/tera/) for more information and to get started.
//!
//! ## Features
//!
//! - High-performance template rendering
//! - Safe and sandboxed execution environment
//! - Template inheritance and includes
//! - Expressive and familiar syntax
//! - Extensible with custom filters and functions
//! - Automatic escaping of HTML/XML by default
//! - Template caching and auto-reloading for efficient development
//! - Built-in support for JSON and other data formats
//! - Comprehensive error messages and debugging information
//!
//! ## Example
//!
//! ```rust
//! use tera::Tera;
//!
//! // Create a new Tera instance and add a template from a string
//! let mut tera = Tera::new();
//! tera.register_filter("do_nothing", do_nothing_filter);
//! tera.load_from_glob("examples/basic/templates/**/*")?;
//! // Prepare the context with some data
//! let mut context = tera::Context::new();
//! context.insert("name", "World");
//!
//! // Render the template with the given context
//! let rendered = tera.render("hello", &context)?;
//! assert_eq!(rendered, "Hello, World!");
//! ```
//!
//! ## Getting Started
//!
//! Add the following to your Cargo.toml file:
//!
//! ```toml
//! [dependencies]
//! tera = "2"
//! ```
//!
//! Then, consult the official documentation and examples to learn more about using Tera in your
//! Rust projects.
//!
//! [Jinja2]: http://jinja.pocoo.org/
//! [Django]: https://docs.djangoproject.com/en/3.1/topics/templates/

//#![deny(missing_docs)]

mod args;
mod components;
mod context;
Expand All @@ -24,7 +83,6 @@ pub use delimiters::Delimiters;
pub use errors::{Error, ErrorKind, TeraResult};
pub use filters::Filter;
pub use functions::Function;
pub use parsing::parser::Parser;
pub use tests::Test;
pub use utils::escape_html;
pub use value::number::Number;
Expand Down
Loading