Repository: Keats/tera
Pull request #44 (2016-08-22 15:34:49)

Filters


Patch:
diff --git a/src/errors.rs b/src/errors.rs
index 08b4285..322220b 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -1,3 +1,5 @@
+use serde_json::value::Value;
+
 /// Library generic result type.
 pub type TeraResult<T> = Result<T, TeraError>;
 
@@ -21,6 +23,11 @@ quick_error! {
             display("Template `{}` wasn't found", name)
             description("template not found")
         }
+        FilterNotFound(name: String) {
+            display("Filter `{}` was not found in the context.", name)
+            description("filter not found")
+        }
+
         // Runtime errors
         NotANumber(name: String) {
             display("Field `{}` was used in a math operation but is not a number", name)
@@ -34,5 +41,9 @@ quick_error! {
             display("Field `{}` was not found in the context.", name)
             description("field not found")
         }
+        FilterIncorrectArgType(filter_name: String, arg_name: String, arg_value: Value, expected_type: String) {
+            display("Filter `{}` received an incorrect type for arg `{}`: got {:?} but expected a {}", filter_name, arg_name, arg_value, expected_type)
+            description("incorrect filter arg type")
+        }
     }
 }
diff --git a/src/filters/array.rs b/src/filters/array.rs
new file mode 100644
index 0000000..e69de29
diff --git a/src/filters/mod.rs b/src/filters/mod.rs
new file mode 100644
index 0000000..c970886
--- /dev/null
+++ b/src/filters/mod.rs
@@ -0,0 +1,11 @@
+use std::collections::HashMap;
+
+use serde_json::value::Value;
+use errors::TeraResult;
+
+
+pub mod string;
+pub mod number;
+pub mod array;
+
+pub type FilterFn = fn(Value, HashMap<String, Value>) -> TeraResult<Value>;
diff --git a/src/filters/number.rs b/src/filters/number.rs
new file mode 100644
index 0000000..e69de29
diff --git a/src/filters/string.rs b/src/filters/string.rs
new file mode 100644
index 0000000..ebc1a96
--- /dev/null
+++ b/src/filters/string.rs
@@ -0,0 +1,94 @@
+/// Filters operating on string
+use std::collections::HashMap;
+
+use serde_json::value::{Value, to_value};
+
+use errors::TeraResult;
+
+
+/// Convert a value to uppercase.
+pub fn upper(value: Value, _: HashMap<String, Value>) -> TeraResult<Value> {
+    let s = try_get_value!("upper", "value", String, value);
+
+    Ok(to_value(&s.to_uppercase()))
+}
+
+
+/// Strip leading and trailing whitespace.
+pub fn trim(value: Value, _: HashMap<String, Value>) -> TeraResult<Value> {
+    let s = try_get_value!("trim", "value", String, value);
+
+    Ok(to_value(&s.trim()))
+}
+
+/// Truncates a string to the indicated length
+pub fn truncate(value: Value, args: HashMap<String, Value>) -> TeraResult<Value> {
+    let s = try_get_value!("truncate", "value", String, value);
+    let length = match args.get("length") {
+        Some(l) => try_get_value!("truncate", "length", usize, l.clone()),
+        None => 255
+    };
+
+    // Nothing to truncate?
+    if length > s.len() {
+        return Ok(to_value(&s));
+    }
+
+
+    let result = s[.. s.char_indices().nth(length).unwrap().0].to_string() + "…";
+    Ok(to_value(&result))
+}
+
+
+#[cfg(test)]
+mod tests {
+    use std::collections::HashMap;
+
+    use serde_json::value::{to_value};
+
+    use errors::TeraError::*;
+
+    use super::*;
+
+    #[test]
+    fn test_upper() {
+        let result = upper(to_value("hello"), HashMap::new());
+        assert!(result.is_ok());
+        assert_eq!(result.unwrap(), to_value("HELLO"));
+    }
+
+    #[test]
+    fn test_upper_error() {
+        let result = upper(to_value(&50), HashMap::new());
+        assert!(result.is_err());
+        assert_eq!(
+            result.err().unwrap(),
+            FilterIncorrectArgType("upper".to_string(), "value".to_string(), to_value(&50), "String".to_string())
+        );
+    }
+
+    #[test]
+    fn test_trim() {
+        let result = trim(to_value("  hello  "), HashMap::new());
+        assert!(result.is_ok());
+        assert_eq!(result.unwrap(), to_value("hello"));
+    }
+
+    #[test]
+    fn test_truncate_smaller_than_length() {
+        let mut args = HashMap::new();
+        args.insert("length".to_string(), to_value(&255));
+        let result = truncate(to_value("hello"), args);
+        assert!(result.is_ok());
+        assert_eq!(result.unwrap(), to_value("hello"));
+    }
+
+    #[test]
+    fn test_truncate_when_required() {
+        let mut args = HashMap::new();
+        args.insert("length".to_string(), to_value(&2));
+        let result = truncate(to_value("日本語"), args);
+        assert!(result.is_ok());
+        assert_eq!(result.unwrap(), to_value("日本…"));
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 83f61e6..5a4aa7b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -18,11 +18,14 @@ extern crate quick_error;
 
 
 mod errors;
+#[macro_use]
+mod macros;
 mod parser;
 mod context;
 mod render;
 mod template;
 mod tera;
+mod filters;
 
 
 // Library exports
diff --git a/src/macros.rs b/src/macros.rs
new file mode 100644
index 0000000..0e68eb6
--- /dev/null
+++ b/src/macros.rs
@@ -0,0 +1,19 @@
+/// Helper macro to get real values out of Value while retaining
+/// proper errors
+#[macro_export]
+macro_rules! try_get_value {
+    ($filter_name:expr, $var_name:expr, $ty:ty, $val:expr) => {{
+        let v: $ty = match ::serde_json::value::from_value($val.clone()) {
+            Ok(s) => s,
+            Err(_) => {
+                return Err(::errors::TeraError::FilterIncorrectArgType(
+                    $filter_name.to_string(),
+                    $var_name.to_string(),
+                    $val,
+                    stringify!($ty).to_string())
+                );
+            }
+        };
+        v
+    }};
+}
diff --git a/src/parser.rs b/src/parser.rs
index 8a22496..dd080d6 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -1,7 +1,7 @@
-use std::collections::LinkedList;
+use std::collections::{HashMap, LinkedList};
 
 use pest::prelude::*;
-
+use serde_json::value::{Value, to_value};
 use errors::{TeraResult, TeraError};
 
 
@@ -24,8 +24,12 @@ pub enum Node {
     For {variable: String, array: String, body: Box<Node>},
     Block {name: String, body: Box<Node>},
 
+    // For now all params are strings. We still need to differentiate between
+    // user input (ie "dd/mm/YY" arg in a date filter)
+    Filter {name: String, args: HashMap<String, Value>, args_ident: HashMap<String, String>},
+    Identifier {name: String, filters: Option<LinkedList<Node>>},
+
     Raw(String),
-    Identifier(String),
     Extends(String),
     VariableBlock(Box<Node>),
 }
@@ -62,6 +66,7 @@ impl_rdp! {
         op_true      = { ["true"] }
         op_false     = { ["false"] }
         boolean      = _{ op_true | op_false }
+        op_filter    = _{ ["|"] }
 
         int   = @{ ["-"]? ~ (["0"] | ['1'..'9'] ~ ['0'..'9']*) }
         float = @{
@@ -69,18 +74,35 @@ impl_rdp! {
                 ["0"] ~ ["."] ~ ['0'..'9']+ |
                 ['1'..'9'] ~ ['0'..'9']* ~ ["."] ~ ['0'..'9']+
         }
+        // matches anything between 2 double quotes
+        string  = @{ ["\""] ~ (!(["\""]) ~ any )* ~ ["\""]}
+
+        // FUNCTIONS
+        // include macros later on
+        // Almost same as identifier minus no . allowed
+        simple_ident = @{
+            (['a'..'z'] | ['A'..'Z'] | ["_"]) ~
+            (['a'..'z'] | ['A'..'Z'] | ["_"] | ['0'..'9'])*
+        }
+        // named args
+        fn_arg_value = @{ boolean | int | float | string | identifier }
+        fn_arg       = @{ simple_ident ~ ["="] ~ fn_arg_value }
+        fn_args      = !@{ fn_arg ~ ([","] ~ fn_arg )* }
+        fn_call      = { simple_ident ~ ["("] ~ fn_args ~ [")"] | simple_ident }
+
+        filters = { (op_filter ~ fn_call)+ }
 
         identifier = @{
             (['a'..'z'] | ['A'..'Z'] | ["_"]) ~
             (['a'..'z'] | ['A'..'Z'] | ["_"] | ["."] | ['0'..'9'])*
         }
-        // matches anything between 2 double quotes
-        string = @{ ["\""] ~ (!(["\""]) ~ any )* ~ ["\""]}
+        identifier_with_filter = { identifier ~ filters }
+        idents = _{ identifier_with_filter | identifier }
 
         // Precedence climbing
         expression = _{
             // boolean first so they are not caught as identifiers
-            { boolean | identifier | float | int }
+            { boolean | idents | float | int }
             or          = { op_or | op_wrong_or }
             and         = { op_and | op_wrong_and }
             comparison  = { op_gt | op_lt | op_eq | op_ineq | op_lte | op_gte }
@@ -107,7 +129,7 @@ impl_rdp! {
         if_tag          = !@{ tag_start ~ ["if"] ~ expression ~ tag_end }
         elif_tag        = !@{ tag_start ~ ["elif"] ~ expression ~ tag_end }
         else_tag        = !@{ tag_start ~ ["else"] ~ tag_end }
-        for_tag         = !@{ tag_start ~ ["for"] ~ identifier ~ ["in"] ~ identifier ~ tag_end }
+        for_tag         = !@{ tag_start ~ ["for"] ~ identifier ~ ["in"] ~ idents ~ tag_end }
         raw_tag         = !@{ tag_start ~ ["raw"] ~ tag_end }
         endraw_tag      = !@{ tag_start ~ ["endraw"] ~ tag_end }
         endblock_tag    = !@{ tag_start ~ ["endblock"] ~ identifier ~ tag_end }
@@ -285,6 +307,73 @@ impl_rdp! {
             },
         }
 
+        _fn_arg_value(&self) -> Value {
+            (&number: int) => {
+                to_value(&number.parse::<i32>().unwrap())
+            },
+            (&number: float) => {
+                to_value(&number.parse::<f32>().unwrap())
+            },
+            (_: op_true) => {
+                to_value(&true)
+            },
+            (_: op_false) => {
+                to_value(&false)
+            },
+            (&text: text) => {
+                to_value(text)
+            }
+        }
+
+        _fn_args(&self) -> (HashMap<String, Value>, HashMap<String, String>) {
+            // first arg of the fn
+            (_: fn_args, _: fn_arg, &name: simple_ident, _: fn_arg_value, &ident: identifier, mut tail: _fn_args()) => {
+                tail.1.insert(name.to_string(), ident.to_string());
+                tail
+            },
+            (_: fn_args, _: fn_arg, &name: simple_ident, _: fn_arg_value, arg: _fn_arg_value(), mut tail: _fn_args()) => {
+                tail.0.insert(name.to_string(), arg);
+                tail
+            },
+            // arguments after the first
+            (_: fn_arg, &name: simple_ident, _: fn_arg_value, &ident: identifier, mut tail: _fn_args()) => {
+                tail.1.insert(name.to_string(), ident.to_string());
+                tail
+            },
+            (_: fn_arg, &name: simple_ident, _: fn_arg_value, arg: _fn_arg_value(), mut tail: _fn_args()) => {
+                tail.0.insert(name.to_string(), arg);
+                tail
+            },
+            () => (HashMap::new(), HashMap::new())
+        }
+
+        // TODO: reuse that for macros
+        _fn(&self) -> TeraResult<Node> {
+            (_: fn_call, &name: simple_ident, args: _fn_args()) => {
+                Ok(Node::Filter{name: name.to_string(), args: args.0, args_ident: args.1})
+            },
+            // The filters parser will need to consume the `fn_call` token
+            // It might not be needed in next version of pest
+            // https://github.com/dragostis/pest/issues/74
+            (&name: simple_ident, args: _fn_args()) => {
+                Ok(Node::Filter{name: name.to_string(), args: args.0, args_ident: args.1})
+            },
+        }
+
+        _filters(&self) -> TeraResult<LinkedList<Node>> {
+            (_: filters, filter: _fn(), tail: _filters()) => {
+                let mut tail2 = try!(tail);
+                tail2.push_front(try!(filter));
+                Ok(tail2)
+            },
+            (_: fn_call, filter: _fn(), tail: _filters()) => {
+                let mut tail2 = try!(tail);
+                tail2.push_front(try!(filter));
+                Ok(tail2)
+            },
+            () => Ok(LinkedList::new())
+        }
+
         _expression(&self) -> TeraResult<Node> {
             (_: add_sub, left: _expression(), sign, right: _expression()) => {
                 Ok(Node::Math {
@@ -337,8 +426,14 @@ impl_rdp! {
                     operator: "or".to_string()
                 })
             },
+            (_: identifier_with_filter, &ident: identifier, tail: _filters()) => {
+                Ok(Node::Identifier {
+                    name: ident.to_string(),
+                    filters: Some(try!(tail)),
+                })
+            },
             (&ident: identifier) => {
-                Ok(Node::Identifier(ident.to_string()))
+                Ok(Node::Identifier {name: ident.to_string(), filters: None })
             },
             (&number: int) => {
                 Ok(Node::Int(number.parse::<i32>().unwrap()))
@@ -370,6 +465,7 @@ pub fn parse(input: &str) -> TeraResult<Node> {
         let (line_no, col_no) = parser.input().line_col(pos);
         return Err(TeraError::InvalidSyntax(line_no, col_no));
     }
+    // println!("{:#?}", parser.queue_with_captures());
 
     // We need to check for deprecated syntaxes
     for token in parser.queue() {
@@ -399,9 +495,11 @@ pub fn parse(input: &str) -> TeraResult<Node> {
 
 #[cfg(test)]
 mod tests {
-    use std::collections::LinkedList;
+    use std::collections::{LinkedList, HashMap};
 
     use pest::prelude::*;
+    use serde_json::value::{to_value};
+
     use super::{Rdp, Node, parse};
     use errors::TeraError;
 
@@ -426,6 +524,15 @@ mod tests {
         assert!(parser.end());
     }
 
+    #[test]
+    fn test_identifier_with_filter() {
+        let mut parser = Rdp::new(
+            StringInput::new("phone_number | phone(format=user.country) | truncate(limit=50)")
+        );
+        assert!(parser.identifier_with_filter());
+        assert!(parser.end());
+    }
+
     #[test]
     fn test_text() {
         let mut parser = Rdp::new(StringInput::new("Hello\n 世界"));
@@ -491,6 +598,13 @@ mod tests {
         assert!(parser.end());
     }
 
+    #[test]
+    fn test_for_tag_with_filter() {
+        let mut parser = Rdp::new(StringInput::new("{% for client in clients | slice(start=1, end=9) %}"));
+        assert!(parser.for_tag());
+        assert!(parser.end());
+    }
+
     #[test]
     fn test_endfor_tag() {
         let mut parser = Rdp::new(StringInput::new("{% endfor %}"));
@@ -526,6 +640,13 @@ mod tests {
         assert!(parser.end());
     }
 
+    #[test]
+    fn test_if_tag_with_filter() {
+        let mut parser = Rdp::new(StringInput::new("{% if 1 + something | test %}"));
+        assert!(parser.if_tag());
+        assert!(parser.end());
+    }
+
     #[test]
     fn test_variable_tag() {
         let mut parser = Rdp::new(StringInput::new("{{loop.index + 1}}"));
@@ -533,6 +654,13 @@ mod tests {
         assert!(parser.end());
     }
 
+    #[test]
+    fn test_variable_tag_with_filter() {
+        let mut parser = Rdp::new(StringInput::new("{{ greeting | i18n(lang=user.lang) | truncate(limit=50) }}"));
+        assert!(parser.variable_tag());
+        assert!(parser.end());
+    }
+
     #[test]
     fn test_content() {
         let mut parser = Rdp::new(StringInput::new("{% if i18n %}世界{% else %}world{% endif %}"));
@@ -591,7 +719,7 @@ mod tests {
         ast.push_front(Node::Text(" ".to_string()));
         ast.push_front(Node::VariableBlock(
             Box::new(Node::Math {
-                lhs: Box::new(Node::Identifier("count".to_string())),
+                lhs: Box::new(Node::Identifier{name: "count".to_string(), filters: None}),
                 rhs: Box::new(Node::Math {
                     lhs: Box::new(Node::Int(1)),
                     rhs: Box::new(Node::Float(2.5)),
@@ -626,7 +754,7 @@ mod tests {
         let mut ast = LinkedList::new();
         let mut inner_content = LinkedList::new();
         inner_content.push_front(Node::VariableBlock(
-            Box::new(Node::Identifier("user.email".to_string()))
+            Box::new(Node::Identifier {name: "user.email".to_string(), filters: None})
         ));
         ast.push_front(Node::For {
             variable: "user".to_string(),
@@ -655,7 +783,7 @@ mod tests {
 
         let mut condition_nodes = LinkedList::new();
         condition_nodes.push_front(Node::Conditional {
-            condition: Box::new(Node::Identifier("superadmin".to_string())),
+            condition: Box::new(Node::Identifier {name: "superadmin".to_string(), filters: None}),
             body: Box::new(Node::List(body.clone()))
         });
 
@@ -676,7 +804,7 @@ mod tests {
 
         let mut condition_nodes = LinkedList::new();
         condition_nodes.push_front(Node::Conditional {
-            condition: Box::new(Node::Identifier("superadmin".to_string())),
+            condition: Box::new(Node::Identifier {name: "superadmin".to_string(), filters: None}),
             body: Box::new(Node::List(body.clone()))
         });
 
@@ -697,11 +825,11 @@ mod tests {
 
         let mut condition_nodes = LinkedList::new();
         condition_nodes.push_front(Node::Conditional {
-            condition: Box::new(Node::Identifier("superadmin".to_string())),
+            condition: Box::new(Node::Identifier {name: "superadmin".to_string(), filters: None}),
             body: Box::new(Node::List(body.clone()))
         });
         condition_nodes.push_back(Node::Conditional {
-            condition: Box::new(Node::Identifier("admin".to_string())),
+            condition: Box::new(Node::Identifier {name: "admin".to_string(), filters: None}),
             body: Box::new(Node::List(body.clone()))
         });
 
@@ -722,11 +850,11 @@ mod tests {
 
         let mut condition_nodes = LinkedList::new();
         condition_nodes.push_back(Node::Conditional {
-            condition: Box::new(Node::Identifier("admin".to_string())),
+            condition: Box::new(Node::Identifier {name: "admin".to_string(), filters: None}),
             body: Box::new(Node::List(body.clone()))
         });
         condition_nodes.push_front(Node::Conditional {
-            condition: Box::new(Node::Identifier("superadmin".to_string())),
+            condition: Box::new(Node::Identifier {name: "superadmin".to_string(), filters: None}),
             body: Box::new(Node::List(body.clone()))
         });
 
@@ -750,6 +878,40 @@ mod tests {
         assert_eq!(parsed_ast.unwrap(), root);
     }
 
+    #[test]
+    fn test_ast_filter() {
+        let parsed_ast = parse("{{ greeting | i18n(lang=user.lang, units=user.units) | truncate(limit=50, cut_word=true) }}");
+        let mut filters = LinkedList::new();
+
+        let mut args_truncate = HashMap::new();
+        args_truncate.insert("limit".to_string(), to_value(&50));
+        args_truncate.insert("cut_word".to_string(), to_value(&true));
+        let mut args_i18n = HashMap::new();
+        args_i18n.insert("lang".to_string(), "user.lang".to_string());
+        args_i18n.insert("units".to_string(), "user.units".to_string());
+
+        filters.push_front(Node::Filter {
+            name: "truncate".to_string(),
+            args: args_truncate,
+            args_ident: HashMap::new(),
+        });
+        filters.push_front(Node::Filter {
+            name: "i18n".to_string(),
+            args: HashMap::new(),
+            args_ident: args_i18n,
+        });
+
+        let mut ast = LinkedList::new();
+        ast.push_front(Node::VariableBlock(
+            Box::new(Node::Identifier {
+                name: "greeting".to_string(),
+                filters: Some(filters),
+            })
+        ));
+        let root = Node::List(ast);
+        assert_eq!(parsed_ast.unwrap(), root);
+    }
+
     #[test]
     fn test_ast_error_old_and() {
         let parsed_ast = parse("{{ true && 1 }}");
diff --git a/src/render.rs b/src/render.rs
index b23ce88..d47e915 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -106,13 +106,42 @@ impl<'a> Renderer<'a> {
         self.context.lookup(key).cloned().ok_or_else(|| FieldNotFound(key.to_string()))
     }
 
+    // Gets an identifier and return its json value
+    // If there is no filter, it's itself, otherwise call the filters in order
+    // an return their result
+    fn eval_ident(&self, node: &Node) -> TeraResult<Json> {
+        match *node {
+            Identifier { ref name, ref filters } => {
+                let mut value = try!(self.lookup_variable(name));
+                if let Some(ref _filters) = *filters {
+                    for filter in _filters {
+                        match *filter {
+                            Filter { ref name, ref args, ref args_ident } => {
+                                let filter_fn = try!(self.tera.get_filter(name));
+                                let mut all_args = args.clone();
+                                for (arg_name, ident_name) in args_ident {
+                                    all_args.insert(arg_name.to_string(), try!(self.lookup_variable(ident_name)));
+                                }
+                                value = try!(filter_fn(value, all_args));
+                            },
+                            _ => unreachable!(),
+                        };
+                    }
+                    return Ok(value);
+                }
+                Ok(value)
+            },
+            _ => unreachable!()
+        }
+    }
+
     fn eval_math(&self, node: &Node) -> TeraResult<f32> {
         match *node {
-            Identifier(ref s) => {
-                let value = try!(self.lookup_variable(s));
+            Identifier { ref name, .. } => {
+                let value = try!(self.eval_ident(node));
                 match value.to_number() {
                     Ok(v) =>  Ok(v),
-                    Err(_) => Err(NotANumber(s.to_string()))
+                    Err(_) => Err(NotANumber(name.to_string()))
                 }
             },
             Int(s) => Ok(s as f32),
@@ -142,8 +171,8 @@ impl<'a> Renderer<'a> {
     fn eval_condition(&self, node: Node) -> TeraResult<bool> {
         match node {
             // Simple truthiness check
-            Identifier(ref n) => {
-                let value = try!(self.lookup_variable(n));
+            Identifier { .. } => {
+                let value = try!(self.eval_ident(&node));
                 Ok(value.is_truthy())
             },
             Logic { lhs, rhs, operator } => {
@@ -177,13 +206,13 @@ impl<'a> Renderer<'a> {
                                 // TODO: rhs MUST be bool like
                                 panic!("Unimplemented");
                             },
-                            Identifier(ref n) => {
-                                let l = try!(self.lookup_variable(n));
+                            Identifier { .. } => {
+                                let l = try!(self.eval_ident(&lhs));
                                 // who knows what rhs is
                                 // Here goes a whole new level of ugliness
                                 match *rhs {
-                                    Identifier(ref i) => {
-                                        let r = try!(self.lookup_variable(i));
+                                    Identifier { .. } => {
+                                        let r = try!(self.eval_ident(&rhs));
                                         let result = match operator.as_str() {
                                             "==" => l == r,
                                             "!=" => l != r,
@@ -194,7 +223,7 @@ impl<'a> Renderer<'a> {
                                     Int(r) => {
                                         let l2: i32 = match from_value(l.clone()) {
                                             Ok(k) => k,
-                                            Err(_) => { return Err(NotANumber(n.to_string())); }
+                                            Err(_) => { return Err(NotANumber(l.to_string())); }
                                         };
                                         let result = match operator.as_str() {
                                             "==" => l2 == r,
@@ -206,7 +235,7 @@ impl<'a> Renderer<'a> {
                                     Float(r) => {
                                         let l2: f32 = match from_value(l.clone()) {
                                             Ok(k) => k,
-                                            Err(_) => { return Err(NotANumber(n.to_string())); }
+                                            Err(_) => { return Err(NotANumber(l.to_string())); }
                                         };
                                         let result = match operator.as_str() {
                                             "==" => (l2 - r).abs() < EPSILON,
@@ -261,11 +290,11 @@ impl<'a> Renderer<'a> {
         }
     }
 
-    // eval all the values in a  {{ }} block
+    // eval all the values in a {{ }} block
     fn render_variable_block(&mut self, node: Node) -> TeraResult<String>  {
         match node {
-            Identifier(ref s) => {
-                let value = try!(self.lookup_variable(s));
+            Identifier { .. } => {
+                let value = try!(self.eval_ident(&node));
                 Ok(value.render())
             },
             Math { .. } => {
@@ -503,4 +532,16 @@ mod tests {
 
         assert_eq!(result.unwrap(), "10truefalse21falsefalse32falsetrue".to_owned());
     }
+
+    #[test]
+    fn test_render_filter() {
+        let mut context = Context::new();
+        context.add("greeting", &"hello");
+        let result = render_template(
+            "{{ greeting | upper }}",
+            context
+        );
+
+        assert_eq!(result.unwrap(), "HELLO".to_owned());
+    }
 }
diff --git a/src/tera.rs b/src/tera.rs
index 2ab0805..324061f 100644
--- a/src/tera.rs
+++ b/src/tera.rs
@@ -5,6 +5,7 @@ use std::fs::File;
 use glob::glob;
 
 use template::Template;
+use filters::{FilterFn, string};
 use context::Context;
 use errors::{TeraResult, TeraError};
 use render::Renderer;
@@ -13,6 +14,7 @@ use render::Renderer;
 #[derive(Debug)]
 pub struct Tera {
     pub templates: HashMap<String, Template>,
+    pub filters: HashMap<String, FilterFn>,
 }
 
 impl Tera {
@@ -43,9 +45,12 @@ impl Tera {
             }
         }
 
-        Tera {
-            templates: templates
-        }
+        let mut tera = Tera {
+            templates: templates,
+            filters: HashMap::new(),
+        };
+        tera.register_tera_filters();
+        tera
     }
 
     pub fn render(&self, template_name: &str, data: Context) -> TeraResult<String> {
@@ -63,16 +68,37 @@ impl Tera {
     }
 
     // Can panic!
-    // Only for internal tests, do not use publicly for now
+    // Only for internal tests, do not use publicly
     pub fn add_template(&mut self, name: &str, content: &str) {
         self.templates.insert(name.to_string(), Template::new(name, content));
     }
+
+    pub fn get_filter(&self, filter_name: &str) -> TeraResult<&FilterFn> {
+        match self.filters.get(filter_name) {
+            Some(fil) => Ok(fil),
+            None => Err(TeraError::FilterNotFound(filter_name.to_string()))
+        }
+    }
+
+    pub fn register_filter(&mut self, name: &str, filter: FilterFn) {
+        self.filters.insert(name.to_string(), filter);
+    }
+
+    fn register_tera_filters(&mut self) {
+        self.register_filter("upper", string::upper);
+        self.register_filter("trim", string::trim);
+        self.register_filter("truncate", string::truncate);
+    }
 }
 
 impl Default for Tera {
     fn default() -> Tera {
-        Tera {
-            templates: HashMap::new()
-        }
+        let mut tera = Tera {
+            templates: HashMap::new(),
+            filters: HashMap::new(),
+        };
+
+        tera.register_tera_filters();
+        tera
     }
 }


Test patch:
diff --git a/tests/expected/filters.html b/tests/expected/filters.html
new file mode 100644
index 0000000..19c7e75
--- /dev/null
+++ b/tests/expected/filters.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>MOTO G</title>
+  </head>
+  <body>
+    <h1>Moto G - Moto…</h1>
+  </body>
+</html>
diff --git a/tests/render_ok.rs b/tests/render_ok.rs
index 7876c62..118f0c7 100644
--- a/tests/render_ok.rs
+++ b/tests/render_ok.rs
@@ -92,3 +92,8 @@ fn test_ok_basic_inheritance_template() {
 fn test_ok_raw_template() {
     assert_template_ok("tests/templates/raw.html", None);
 }
+
+#[test]
+fn test_ok_filters_template() {
+    assert_template_ok("tests/templates/filters.html", None);
+}
diff --git a/tests/templates/filters.html b/tests/templates/filters.html
new file mode 100644
index 0000000..75e7b4d
--- /dev/null
+++ b/tests/templates/filters.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>{{ product.name | upper | trim }}</title>
+  </head>
+  <body>
+    <h1>{{ product.name | trim }} - {{ product.manufacturer | trim | truncate(length=4) }}</h1>
+  </body>
+</html>