Browse Source

Zotonic: Basic boolean operators in "if" clause

Support for "and", "or", "==", and "/=" in if statements, as well as
arbitrary nesting of expressions. Adapted from Zotonic ErlyDTL.

Includes tests.
Evan Miller 15 years ago
parent
commit
b96dd0bff4

+ 55 - 75
src/erlydtl/erlydtl_compiler.erl

@@ -325,17 +325,6 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
                 string_ast(String, TreeWalkerAcc);
 	    ({'trans', {string_literal, _Pos, FormatString}}, TreeWalkerAcc) ->
                 translated_ast(FormatString, Context, TreeWalkerAcc);
-            ({'string_literal', _Pos, String}, TreeWalkerAcc) ->
-                {{auto_escape(erl_syntax:string(unescape_string_literal(String)), Context), 
-                        #ast_info{}}, TreeWalkerAcc};
-            ({'number_literal', _Pos, Number}, TreeWalkerAcc) ->
-                string_ast(Number, TreeWalkerAcc);
-            ({'attribute', _} = Variable, TreeWalkerAcc) ->
-                {Ast, VarName} = resolve_variable_ast(Variable, Context),
-                {{format(Ast, Context), #ast_info{var_names = [VarName]}}, TreeWalkerAcc};
-            ({'variable', _} = Variable, TreeWalkerAcc) ->
-                {Ast, VarName} = resolve_variable_ast(Variable, Context),
-                {{format(Ast, Context), #ast_info{var_names = [VarName]}}, TreeWalkerAcc};              
             ({'include', {string_literal, _, File}}, TreeWalkerAcc) ->
                 include_ast(unescape_string_literal(File), Context, TreeWalkerAcc);
             ({'if', Expression, Contents}, TreeWalkerAcc) ->
@@ -346,24 +335,22 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
                 {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
                 {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
                 ifelse_ast(Expression, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-            ({'ifequal', Args, Contents}, TreeWalkerAcc) ->
+            ({'ifequal', [Arg1, Arg2], Contents}, TreeWalkerAcc) ->
                 {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
                 {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
-                ifequalelse_ast(Args, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-            ({'ifequalelse', Args, IfContents, ElseContents}, TreeWalkerAcc) ->
+                ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+            ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TreeWalkerAcc) ->
                 {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), 
                 {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context,TreeWalker1),
-                ifequalelse_ast(Args, IfAstInfo, ElseAstInfo, Context, TreeWalker2);                
-            ({'ifnotequal', Args, Contents}, TreeWalkerAcc) ->
-                {IfAstInfo, TreeWalker1} = empty_ast(TreeWalkerAcc),
-                {ElseAstInfo, TreeWalker2} = body_ast(Contents, Context, TreeWalker1),
-                ifequalelse_ast(Args, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-            ({'ifnotequalelse', Args, IfContents, ElseContents}, TreeWalkerAcc) ->
-                {IfAstInfo, TreeWalker1} = body_ast(ElseContents, Context, TreeWalkerAcc),
-                {ElseAstInfo, TreeWalker2} = body_ast(IfContents, Context, TreeWalker1),
-                ifequalelse_ast(Args, IfAstInfo, ElseAstInfo, Context, TreeWalker2);                    
-            ({'apply_filter', Variable, Filter}, TreeWalkerAcc) ->
-                filter_ast(Variable, Filter, Context, TreeWalkerAcc);
+                ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);                
+            ({'ifnotequal', [Arg1, Arg2], Contents}, TreeWalkerAcc) ->
+                {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
+                {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
+                ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+            ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TreeWalkerAcc) ->
+                {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
+                {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
+                ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);                    
             ({'for', {'in', IteratorList, Variable}, Contents}, TreeWalkerAcc) ->
                 for_loop_ast(IteratorList, Variable, Contents, Context, TreeWalkerAcc);
             ({'load', Names}, TreeWalkerAcc) ->
@@ -379,7 +366,10 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
             ({'cycle', Names}, TreeWalkerAcc) ->
                 cycle_ast(Names, Context, TreeWalkerAcc);
             ({'cycle_compat', Names}, TreeWalkerAcc) ->
-                cycle_compat_ast(Names, Context, TreeWalkerAcc)
+                cycle_compat_ast(Names, Context, TreeWalkerAcc);
+            (ValueToken, TreeWalkerAcc) -> 
+                {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, Context, TreeWalkerAcc),
+                {{format(ValueAst, Context),ValueInfo},ValueTreeWalker}
         end, TreeWalker, DjangoParseTree),   
     {AstList, {Info, TreeWalker3}} = lists:mapfoldl(
         fun({Ast, Info}, {InfoAcc, TreeWalkerAcc}) -> 
@@ -410,6 +400,39 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
     {{erl_syntax:list(AstList), Info}, TreeWalker3}.
 
 
+value_ast(ValueToken, AsString, Context, TreeWalker) ->
+    case ValueToken of
+        {'expr', Operator, Value} ->
+            {{ValueAst,InfoValue}, TreeWalker1} = value_ast(Value, false, Context, TreeWalker),
+            Ast = erl_syntax:application(erl_syntax:atom(erlydtl_runtime), 
+                                         erl_syntax:atom(Operator), 
+                                         [ValueAst]),
+            {{Ast, InfoValue}, TreeWalker1};
+        {'expr', Operator, Value1, Value2} ->
+            {{Value1Ast,InfoValue1}, TreeWalker1} = value_ast(Value1, false, Context, TreeWalker),
+            {{Value2Ast,InfoValue2}, TreeWalker2} = value_ast(Value2, false, Context, TreeWalker1),
+            Ast = erl_syntax:application(erl_syntax:atom(erlydtl_runtime), 
+                                         erl_syntax:atom(Operator), 
+                                         [Value1Ast, Value2Ast]),
+            {{Ast, merge_info(InfoValue1,InfoValue2)}, TreeWalker2};
+        {'string_literal', _Pos, String} ->
+            {{auto_escape(erl_syntax:string(unescape_string_literal(String)), Context), 
+                    #ast_info{}}, TreeWalker};
+        {'number_literal', _Pos, Number} ->
+            case AsString of
+                true  -> string_ast(Number, TreeWalker);
+                false -> {{erl_syntax:integer(list_to_integer(Number)), #ast_info{}}, TreeWalker}
+            end;
+        {'apply_filter', Variable, Filter} ->
+            filter_ast(Variable, Filter, Context, TreeWalker);
+        {'attribute', _} = Variable ->
+            {Ast, VarName} = resolve_variable_ast(Variable, Context),
+            {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker};
+        {'variable', _} = Variable ->
+            {Ast, VarName} = resolve_variable_ast(Variable, Context),
+            {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker}
+    end.
+
 merge_info(Info1, Info2) ->
     #ast_info{dependencies = 
         lists:merge(
@@ -479,9 +502,9 @@ filter_ast(Variable, Filter, Context, TreeWalker) ->
     end.
 
 filter_ast_noescape(Variable, [{identifier, _, "escape"}], Context, TreeWalker) ->
-    body_ast([Variable], Context, TreeWalker);
+    value_ast(Variable, true, Context, TreeWalker);
 filter_ast_noescape(Variable, Filter, Context, TreeWalker) ->
-    {{VariableAst, Info}, TreeWalker2} = body_ast([Variable], Context, TreeWalker),
+    {{VariableAst, Info}, TreeWalker2} = value_ast(Variable, true, Context, TreeWalker),
     VarValue = filter_ast1(Filter, VariableAst),
     {{VarValue, Info}, TreeWalker2}.
 
@@ -515,9 +538,6 @@ search_for_escape_filter(_Variable, _Filter) ->
 resolve_variable_ast(VarTuple, Context) ->
     resolve_variable_ast(VarTuple, Context, 'find_value').
  
-resolve_ifvariable_ast(VarTuple, Context) ->
-    resolve_variable_ast(VarTuple, Context, 'find_value').
-           
 resolve_variable_ast({attribute, {{identifier, _, AttrName}, Variable}}, Context, FinderFunction) ->
     {VarAst, VarName} = resolve_variable_ast(Variable, Context, FinderFunction),
     {erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction),
@@ -541,22 +561,6 @@ resolve_variable_ast({apply_filter, Variable, Filter}, Context, FinderFunction)
 resolve_variable_ast(What, _Context, _FinderFunction) ->
    error_logger:error_msg("~p:resolve_variable_ast unhandled: ~p~n", [?MODULE, What]).
 
-resolve_multiple_ifvariable_ast(Args, Info, Context) ->
-    lists:foldr(fun
-            (X, {Asts, AccVarNames}) ->
-                case X of
-                    {string_literal, _, Literal} ->
-                        {[erl_syntax:string(unescape_string_literal(Literal)) | Asts], AccVarNames};
-                    {number_literal, _, Literal} ->
-                        {[erl_syntax:integer(list_to_integer(Literal)) | Asts], AccVarNames};
-                    Variable ->
-                        {Ast, VarName} = resolve_ifvariable_ast(Variable, Context),
-                        {[Ast | Asts], [VarName | AccVarNames]}
-                end                
-        end,
-        {[], Info#ast_info.var_names},
-        Args).
-
 resolve_scoped_variable_ast(VarName, Context) ->
     lists:foldl(fun(Scope, Value) ->
                 case Value of
@@ -595,39 +599,15 @@ firstof_ast(Vars, Context, TreeWalker) ->
             {'ifelse', Var, [Var], [Acc]} end,
     	[], Vars)], Context, TreeWalker).
 
-ifelse_ast({'not', Expression}, IfAstInfo, ElseAstInfo, Context, TreeWalker) ->
-    ifelse_ast(Expression, ElseAstInfo, IfAstInfo, Context, TreeWalker);
-ifelse_ast({'in', Variable1, Variable2}, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) ->
+ifelse_ast(Expression, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) ->
     Info = merge_info(IfContentsInfo, ElseContentsInfo),
-    {[Ast1, Ast2], VarNames} = resolve_multiple_ifvariable_ast([Variable1, Variable2], Info, Context),
-    {{erl_syntax:case_expr(erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(is_in), [Ast1, Ast2]),
+    {{Ast, ExpressionInfo}, TreeWalker1} = value_ast(Expression, false, Context, TreeWalker), 
+    {{erl_syntax:case_expr(erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(is_true), [Ast]),
         [erl_syntax:clause([erl_syntax:atom(true)], none, 
                 [IfContentsAst]),
             erl_syntax:clause([erl_syntax:underscore()], none,
                 [ElseContentsAst])
-        ]), Info#ast_info{var_names = VarNames}}, TreeWalker};
-ifelse_ast(Variable, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) ->
-    Info = merge_info(IfContentsInfo, ElseContentsInfo),
-    VarNames = Info#ast_info.var_names,
-    {Ast, VarName} = resolve_ifvariable_ast(Variable, Context),
-    {{erl_syntax:case_expr(erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(is_false), [Ast]),
-        [erl_syntax:clause([erl_syntax:atom(true)], none, 
-                [ElseContentsAst]),
-            erl_syntax:clause([erl_syntax:underscore()], none,
-                [IfContentsAst])
-        ]), Info#ast_info{var_names = [VarName | VarNames]}}, TreeWalker}.
-
-        
-ifequalelse_ast(Args, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) ->
-    Info = merge_info(IfContentsInfo, ElseContentsInfo),
-    {[Arg1Ast, Arg2Ast], VarNames} = resolve_multiple_ifvariable_ast(Args, Info, Context),
-    Ast = erl_syntax:case_expr(erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(are_equal),
-            [Arg1Ast, Arg2Ast]),
-        [
-            erl_syntax:clause([erl_syntax:atom(true)], none, [IfContentsAst]),
-            erl_syntax:clause([erl_syntax:underscore()], none, [ElseContentsAst])
-        ]),
-    {{Ast, Info#ast_info{var_names = VarNames}}, TreeWalker}.         
+        ]), merge_info(ExpressionInfo, Info)}, TreeWalker1}.
 
 
 for_loop_ast(IteratorList, Variable, Contents, Context, TreeWalker) ->

+ 2 - 0
src/erlydtl/erlydtl_dateformat.erl

@@ -312,6 +312,8 @@ weeks_in_year(Y) ->
     D2 = calendar:day_of_the_week(Y, 12, 31),
     if (D1 =:= 4 orelse D2 =:= 4) -> 53; true -> 52 end.
 
+utc_diff({Y, M, D}, Time) when Y < 1970->
+    utc_diff({1970, M, D}, Time);
 utc_diff(Date, Time) ->
    LTime = {Date, Time},
    UTime = erlang:localtime_to_universaltime(LTime),

+ 6 - 41
src/erlydtl/erlydtl_filters.erl

@@ -65,8 +65,6 @@
         orelse C =:= $~ orelse C =:= $_))).
 
 %% @doc Adds a number to the value.
-add([Input], Number) when is_list(Input) or is_binary(Input) ->
-    add(Input, Number);
 add(Input, Number) when is_binary(Input) ->
     list_to_binary(add(binary_to_list(Input), Number));
 add(Input, Number) when is_list(Input) ->
@@ -75,68 +73,52 @@ add(Input, Number) when is_integer(Input) ->
     Input + Number.
 
 %% @doc Capitalizes the first character of the value.
-capfirst([Input]) when is_list(Input) or is_binary (Input) ->
-    capfirst(Input);
 capfirst([H|T]) when H >= $a andalso H =< $z ->
     [H + $A - $a | T];
 capfirst(<<Byte:8/integer, Binary/binary>>) when Byte >= $a andalso Byte =< $z ->
-    [<<(Byte + $A - $a)>>, Binary].
+    [(Byte + $A - $a)|binary_to_list(Binary)].
 
 %% @doc Centers the value in a field of a given width.
-center([Input], Number) when is_list(Input) or is_binary(Input) ->
-    center(Input, Number);
 center(Input, Number) when is_binary(Input) ->
     list_to_binary(center(binary_to_list(Input), Number));
 center(Input, Number) when is_list(Input) ->
     string:centre(Input, Number).
 
 %% @doc Formats a date according to the given format.
-date([Input], FormatStr) when is_list(Input) or is_binary(Input) ->
-    date(Input, FormatStr);
 date(Input, FormatStr) when is_binary(Input) ->
     list_to_binary(date(binary_to_list(Input), FormatStr));
-date([{{_,_,_} = Date,{_,_,_} = Time}], FormatStr) ->
+date({{_,_,_} = Date,{_,_,_} = Time}, FormatStr) ->
     erlydtl_dateformat:format({Date, Time}, FormatStr);
-date([{_,_,_} = Date], FormatStr) ->
+date({_,_,_} = Date, FormatStr) ->
     erlydtl_dateformat:format(Date, FormatStr);
 date(Input, _FormatStr) when is_list(Input) ->
     io:format("Unexpected date parameter : ~p~n", [Input]),
     "".
 
-default_if_none([undefined], Default) ->
-    Default;
 default_if_none(undefined, Default) ->
     Default;
 default_if_none(Input, _) ->
     Input.
 
 %% @doc Escapes characters for use in JavaScript strings.
-escapejs([Input]) when is_list(Input) or is_binary(Input) ->
-    escapejs(Input);
 escapejs(Input) when is_binary(Input) ->
     escapejs(Input, 0);
 escapejs(Input) when is_list(Input) ->
     escapejs(Input, []).
 
 %% @doc Returns the first item in a list.
-first([Input]) when is_list(Input) or is_binary(Input) ->
-    first(Input);
 first([First|_Rest]) ->
     [First];
 first(<<First, _/binary>>) ->
     <<First>>.
 
 %% @doc Replaces ampersands with &amp; entities.
-fix_ampersands([Input]) when is_list(Input) or is_binary(Input) ->
-    fix_ampersands(Input);
 fix_ampersands(Input) when is_binary(Input) ->
     fix_ampersands(Input, 0);
 fix_ampersands(Input) when is_list(Input) ->
     fix_ampersands(Input, []).
 
 %% @doc Applies HTML escaping to a string.
-force_escape([Input]) when is_list(Input) or is_binary(Input) ->
-    force_escape(Input);
 force_escape(Input) when is_list(Input) ->
     escape(Input, []);
 force_escape(Input) when is_binary(Input) ->
@@ -157,12 +139,10 @@ format_number(Input) ->
     Input.
 
 %% @doc Joins a list with a given separator.
-join([Input], Separator) when is_list(Input) ->
+join(Input, Separator) when is_list(Input) ->
     join_io(Input, Separator).
 
 %% @doc Returns the last item in a list.
-last([Input]) when is_list(Input) or is_binary(Input) ->
-    last(Input);
 last(Input) when is_binary(Input) ->
     case size(Input) of
         0 -> Input;
@@ -175,10 +155,9 @@ last(Input) when is_list(Input) ->
     [lists:last(Input)].
 
 %% @doc Returns the length of the value.
-length([]) -> "0";
-length([Input]) when is_list(Input) ->
+length(Input) when is_list(Input) ->
     integer_to_list(erlang:length(Input));
-length([Input]) when is_binary(Input) ->
+length(Input) when is_binary(Input) ->
     integer_to_list(size(Input)).
 
 %% @doc Returns True iff the value's length is the argument.
@@ -188,40 +167,30 @@ length_is(Input, Number) when is_list(Input), is_list(Number) ->
     ?MODULE:length(Input) =:= Number.
 
 %% @doc Converts all newlines to HTML line breaks.
-linebreaksbr([Input]) when is_list(Input) or is_binary(Input) ->
-    linebreaksbr(Input);
 linebreaksbr(Input) when is_binary(Input) ->
     linebreaksbr(Input, 0);
 linebreaksbr(Input) ->
     linebreaksbr(Input, []).
 
 %% @doc Left-aligns the value in a field of a given width.
-ljust([Input], Number) when is_list(Input) or is_binary(Input) ->
-    ljust(Input, Number);
 ljust(Input, Number) when is_binary(Input) ->
     list_to_binary(ljust(binary_to_list(Input), Number));
 ljust(Input, Number) when is_list(Input) ->
     string:left(Input, Number).
 
 %% @doc Converts a string into all lowercase.
-lower([Input]) when is_list(Input) or is_binary(Input) ->
-    lower(Input);
 lower(Input) when is_binary(Input) ->
     lower(Input, 0);
 lower(Input) ->
     string:to_lower(Input).
 
 %% @doc Right-aligns the value in a field of a given width.
-rjust([Input], Number) when is_list(Input) or is_binary(Input) ->
-    rjust(Input, Number);
 rjust(Input, Number) when is_binary(Input) ->
     list_to_binary(rjust(binary_to_list(Input), Number));
 rjust(Input, Number) ->
     string:right(Input, Number).
 
 %% @doc Truncates a string after a certain number of words.
-truncatewords([Input], Max) when is_list(Input) or is_binary(Input) ->
-    truncatewords(Input, Max);
 truncatewords(Input, Max) when is_binary(Input) ->
     list_to_binary(truncatewords(binary_to_list(Input), Max));
 truncatewords(_Input, Max) when Max =< 0 ->
@@ -230,16 +199,12 @@ truncatewords(Input, Max) ->
     truncatewords(Input, Max, []).
 
 %% @doc Converts a string into all uppercase.
-upper([Input]) when is_list(Input) or is_binary(Input) ->
-    upper(Input);
 upper(Input) when is_binary(Input) ->
     list_to_binary(upper(binary_to_list(Input)));
 upper(Input) ->
     string:to_upper(Input).
 
 %% @doc Escapes a value for use in a URL.
-urlencode([Input]) when is_list(Input) or is_binary(Input) ->
-    urlencode(Input);
 urlencode(Input) when is_binary(Input) ->
     urlencode(Input, 0);
 urlencode(Input) when is_list(Input) ->

+ 24 - 5
src/erlydtl/erlydtl_parser.yrl

@@ -97,9 +97,12 @@ Nonterminals
     TransTag    
 
     CallTag
-    CallWithTag.
+    CallWithTag
+    
+    Unot.
 
 Terminals
+    and_keyword
     autoescape_keyword
     block_keyword
     call_keyword
@@ -132,17 +135,26 @@ Terminals
     not_keyword
     now_keyword
     number_literal
+    or_keyword
     open_tag
     open_var
     pipe
     string_literal
     text
     trans_keyword
-    with_keyword.
+    with_keyword
+    '==' '/='
+    '(' ')'.
 
 Rootsymbol
     Elements.
 
+%% Operator precedences for the E non terminal
+Left 100 or_keyword.
+Left 110 and_keyword.
+Nonassoc 300 '==' '/='.
+Unary 600 Unot.
+
 Elements -> '$empty' : [].
 Elements -> Elements text : '$1' ++ ['$2'].
 Elements -> Elements TransTag : '$1' ++ ['$2'].
@@ -216,11 +228,18 @@ ForGroup -> ForGroup comma identifier : '$1' ++ ['$3'].
 IfBlock -> IfBraced Elements ElseBraced Elements EndIfBraced : {ifelse, '$1', '$2', '$4'}.
 IfBlock -> IfBraced Elements EndIfBraced : {'if', '$1', '$2'}.
 IfBraced -> open_tag if_keyword IfExpression close_tag : '$3'.
-IfExpression -> not_keyword Value : {'not', '$2'}.
-IfExpression -> Value in_keyword Value : {'in', '$1', '$3'}.
-IfExpression -> Value not_keyword in_keyword Value : {'not', {'in', '$1', '$4'}}.
+IfExpression -> Value in_keyword Value : {'expr', "in", '$1', '$3'}.
+IfExpression -> Value not_keyword in_keyword Value : {'expr', "not", {'expr', "in", '$1', '$4'}}.
+IfExpression -> Value '==' Value : {'expr', "eq", '$1', '$3'}.
+IfExpression -> Value '/=' Value : {'expr', "ne", '$1', '$3'}.
+IfExpression -> '(' IfExpression ')' : '$2'.
+IfExpression -> Unot : '$1'.
+IfExpression -> IfExpression or_keyword IfExpression : {'expr', "or", '$1', '$3'}.
+IfExpression -> IfExpression and_keyword IfExpression : {'expr', "and", '$1', '$3'}.
 IfExpression -> Value : '$1'.
 
+Unot -> not_keyword IfExpression : {expr, "not", '$2'}.
+
 ElseBraced -> open_tag else_keyword close_tag.
 EndIfBraced -> open_tag endif_keyword close_tag.
 

+ 34 - 20
src/erlydtl/erlydtl_runtime.erl

@@ -60,14 +60,10 @@ are_equal(Arg1, Arg2) when is_integer(Arg1) ->
     are_equal(integer_to_list(Arg1), Arg2);
 are_equal(Arg1, Arg2) when is_integer(Arg2) ->
     are_equal(Arg1, integer_to_list(Arg2));
-are_equal([Arg1], Arg2) when is_list(Arg1) ->
-    are_equal(Arg1, Arg2);
-are_equal(Arg1, [Arg2]) when is_list(Arg1) ->
-    are_equal(Arg1, Arg2);
 are_equal(Arg1, Arg2) when is_atom(Arg1), is_list(Arg2) ->
-	 are_equal(atom_to_list(Arg1), Arg2);
+    are_equal(atom_to_list(Arg1), Arg2);
 are_equal(Arg1, Arg2) when is_list(Arg1), is_atom(Arg2) ->
-	 are_equal(Arg1, atom_to_list(Arg2));
+    are_equal(Arg1, atom_to_list(Arg2));
 are_equal(_, _) ->
     false.
 
@@ -86,25 +82,43 @@ is_false(<<>>) ->
 is_false(_) ->
     false.
 
-is_in(Sublist, [Sublist|_]) ->
+is_true(V) ->
+    not is_false(V).
+
+'in'(Sublist, [Sublist|_]) ->
     true;
-is_in(Sublist, List) when is_atom(List) ->
-    is_in(Sublist, atom_to_list(List));
-is_in(Sublist, List) when is_binary(Sublist) ->
-    is_in(binary_to_list(Sublist), List);
-is_in(Sublist, List) when is_binary(List) ->
-    is_in(Sublist, binary_to_list(List));
-is_in(Sublist, [C|Rest]) when is_list(Sublist) andalso is_binary(C) ->
-    is_in(Sublist, [binary_to_list(C)|Rest]);
-is_in(Sublist, [C|Rest]) when is_list(Sublist) andalso is_list(C) ->
-    is_in(Sublist, Rest);
-is_in(Sublist, List) when is_list(Sublist) andalso is_list(List) ->
+'in'(Sublist, List) when is_atom(List) ->
+    'in'(Sublist, atom_to_list(List));
+'in'(Sublist, List) when is_binary(Sublist) ->
+    'in'(binary_to_list(Sublist), List);
+'in'(Sublist, List) when is_binary(List) ->
+    'in'(Sublist, binary_to_list(List));
+'in'(Sublist, [C|Rest]) when is_list(Sublist) andalso is_binary(C) ->
+    'in'(Sublist, [binary_to_list(C)|Rest]);
+'in'(Sublist, [C|Rest]) when is_list(Sublist) andalso is_list(C) ->
+    'in'(Sublist, Rest);
+'in'(Sublist, List) when is_list(Sublist) andalso is_list(List) ->
     string:str(List, Sublist) > 0;
-is_in(Element, List) when is_list(List) ->
+'in'(Element, List) when is_list(List) ->
     lists:member(Element, List);
-is_in(_, _) ->
+'in'(_, _) ->
     false.
 
+'not'(Value) ->
+    not is_true(Value).
+
+'or'(Value1, Value2) ->
+    is_true(Value1) or is_true(Value2).
+
+'and'(Value1, Value2) ->
+    is_true(Value1) and is_true(Value2).
+
+'eq'(Value1, Value2) ->
+    are_equal(Value1, Value2).
+
+'ne'(Value1, Value2) ->
+    not are_equal(Value1, Value2).
+
 stringify_final(In) ->
    stringify_final(In, []).
 stringify_final([], Out) ->

+ 45 - 14
src/erlydtl/erlydtl_scanner.erl

@@ -154,6 +154,51 @@ scan([H | T], Scanned, {Row, Column}, {in_single_quote, Closer}) ->
     scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_single_quote, Closer});
 
 
+scan("}}-->" ++ T, Scanned, {Row, Column}, {_, "}}-->"}) ->
+    scan(T, [{close_var, {Row, Column}, lists:reverse("}}-->")} | Scanned], 
+        {Row, Column + 2}, in_text);
+
+scan("}}" ++ T, Scanned, {Row, Column}, {_, "}}"}) ->
+    scan(T, [{close_var, {Row, Column}, "}}"} | Scanned], {Row, Column + 2}, in_text);
+
+scan("%}-->" ++ T, Scanned, {Row, Column}, {_, "%}-->"}) ->
+    scan(T, [{close_tag, {Row, Column}, lists:reverse("%}-->")} | Scanned], 
+        {Row, Column + 2}, in_text);
+
+scan("%}" ++ T, Scanned, {Row, Column}, {_, "%}"}) ->
+    scan(T, [{close_tag, {Row, Column}, lists:reverse("%}")} | Scanned], 
+        {Row, Column + 2}, in_text);
+
+scan("==" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
+    scan(T, [{'==', {Row, Column}, "=="} | Scanned], {Row, Column + 2}, {in_code, Closer});
+
+scan("/=" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
+    scan(T, [{'/=', {Row, Column}, "/="} | Scanned], {Row, Column + 2}, {in_code, Closer});
+
+scan("!=" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
+    scan(T, [{'/=', {Row, Column}, "!="} | Scanned], {Row, Column + 2}, {in_code, Closer});
+
+scan(">=" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
+    scan(T, [{'>=', {Row, Column}, ">="} | Scanned], {Row, Column + 2}, {in_code, Closer});
+
+scan("=<" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
+    scan(T, [{'=<', {Row, Column}, "=<"} | Scanned], {Row, Column + 2}, {in_code, Closer});
+
+scan("<=" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
+    scan(T, [{'=<', {Row, Column}, "<="} | Scanned], {Row, Column + 2}, {in_code, Closer});
+
+scan("<" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
+    scan(T, [{'<', {Row, Column}, "<"} | Scanned], {Row, Column + 1}, {in_code, Closer});
+
+scan(">" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
+    scan(T, [{'>', {Row, Column}, ">"} | Scanned], {Row, Column + 1}, {in_code, Closer});
+
+scan("("++ T, Scanned, {Row, Column}, {_, Closer}) ->
+    scan(T, [{'(', {Row, Column}, "("} | Scanned], {Row, Column + 1}, {in_code, Closer});
+
+scan(")" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
+    scan(T, [{')', {Row, Column}, ")"} | Scanned], {Row, Column + 1}, {in_code, Closer});
+
 scan("," ++ T, Scanned, {Row, Column}, {_, Closer}) ->
     scan(T, [{comma, {Row, Column}, ","} | Scanned], {Row, Column + 1}, {in_code, Closer});
 
@@ -172,20 +217,6 @@ scan("." ++ T, Scanned, {Row, Column}, {_, Closer}) ->
 scan(" " ++ T, Scanned, {Row, Column}, {_, Closer}) ->
     scan(T, Scanned, {Row, Column + 1}, {in_code, Closer});
 
-scan("}}-->" ++ T, Scanned, {Row, Column}, {_, "}}-->"}) ->
-    scan(T, [{close_var, {Row, Column}, lists:reverse("}}-->")} | Scanned], 
-        {Row, Column + 2}, in_text);
-
-scan("}}" ++ T, Scanned, {Row, Column}, {_, "}}"}) ->
-    scan(T, [{close_var, {Row, Column}, "}}"} | Scanned], {Row, Column + 2}, in_text);
-
-scan("%}-->" ++ T, Scanned, {Row, Column}, {_, "%}-->"}) ->
-    scan(T, [{close_tag, {Row, Column}, lists:reverse("%}-->")} | Scanned], 
-        {Row, Column + 2}, in_text);
-
-scan("%}" ++ T, Scanned, {Row, Column}, {_, "%}"}) ->
-    scan(T, [{close_tag, {Row, Column}, lists:reverse("%}")} | Scanned], 
-        {Row, Column + 2}, in_text);
 
 scan([H | T], Scanned, {Row, Column}, {in_code, Closer}) ->
     case char_type(H) of

+ 53 - 5
src/tests/erlydtl_unittests.erl

@@ -114,6 +114,10 @@ tests() ->
                     <<"{% if var1 %}yay{% endif %}">>, [{var1, "hello"}], <<"yay">>},
                 {"If proplist",
                     <<"{% if var1 %}yay{% endif %}">>, [{var1, [{foo, "bar"}]}], <<"yay">>},
+                {"If complex",
+                    <<"{% if foo.bar.baz %}omgwtfbbq{% endif %}">>, [], <<"">>}
+            ]},
+        {"if .. in ..", [
                 {"If substring in string",
                     <<"{% if var1 in var2 %}yay{% endif %}">>, [{var1, "rook"}, {var2, "Crooks"}], <<"yay">>},
                 {"If substring in string (false)",
@@ -129,9 +133,53 @@ tests() ->
                 {"If element in list",
                     <<"{% if var1 in var2 %}yay{% endif %}">>, [{var1, "foo"}, {var2, ["bar", "foo", "baz"]}], <<"yay">>},
                 {"If element in list (false)",
-                    <<"{% if var1 in var2 %}boo{% endif %}">>, [{var1, "FOO"}, {var2, ["bar", "foo", "baz"]}], <<>>},
-                {"If complex",
-                    <<"{% if foo.bar.baz %}omgwtfbbq{% endif %}">>, [], <<"">>}
+                    <<"{% if var1 in var2 %}boo{% endif %}">>, [{var1, "FOO"}, {var2, ["bar", "foo", "baz"]}], <<>>}
+            ]},
+        {"if .. and ..", [
+                {"If true and true",
+                    <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, true}, {var2, true}], <<"yay">>},
+                {"If true and false",
+                    <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, true}, {var2, false}], <<"">>},
+                {"If false and true",
+                    <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, false}, {var2, true}], <<"">>},
+                {"If false and false ",
+                    <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, false}, {var2, false}], <<"">>}
+            ]},
+        {"if .. or ..", [
+                {"If true or true",
+                    <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, true}, {var2, true}], <<"yay">>},
+                {"If true or false",
+                    <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, true}, {var2, false}], <<"yay">>},
+                {"If false or true",
+                    <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, false}, {var2, true}], <<"yay">>},
+                {"If false or false ",
+                    <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, false}, {var2, false}], <<"">>}
+            ]},
+        {"if comparisons", [
+                {"If int equals number literal",
+                    <<"{% if var1 == 2 %}yay{% endif %}">>, [{var1, 2}], <<"yay">>},
+                {"If int equals number literal (false)",
+                    <<"{% if var1 == 2 %}yay{% endif %}">>, [{var1, 3}], <<"">>},
+                {"If string equals string literal",
+                    <<"{% if var1 == \"2\" %}yay{% endif %}">>, [{var1, "2"}], <<"yay">>},
+                {"If string equals string literal (false)",
+                    <<"{% if var1 == \"2\" %}yay{% endif %}">>, [{var1, "3"}], <<"">>},
+                {"If int not equals number literal",
+                    <<"{% if var1 /= 2 %}yay{% endif %}">>, [{var1, 3}], <<"yay">>},
+                {"If string not equals string literal",
+                    <<"{% if var1 /= \"2\" %}yay{% endif %}">>, [{var1, "3"}], <<"yay">>},
+                {"If filter result equals number literal",
+                    <<"{% if var1|length == 2 %}yay{% endif %}">>, [{var1, ["fo", "bo"]}], <<"yay">>},
+                {"If filter result equals string literal",
+                    <<"{% if var1|capfirst == \"Foo\" %}yay{% endif %}">>, [{var1, "foo"}], <<"yay">>}
+            ]},
+        {"if complex bool", [
+                {"If (true or false) and true",
+                    <<"{% if (var1 or var2) and var3 %}yay{% endif %}">>, 
+                    [{var1, true}, {var2, false}, {var3, true}], <<"yay">>},
+                {"If true or (false and true)",
+                    <<"{% if var1 or (var2 and var3) %}yay{% endif %}">>, 
+                    [{var1, true}, {var2, false}, {var3, true}], <<"yay">>}
             ]},
         {"for", [
                 {"Simple loop",
@@ -378,7 +426,7 @@ tests() ->
                     <<"{% ifequal var1|length 0 %}Y{% else %}N{% endifequal %}">>,
                      [{var1, []}],
                      <<"Y">>},
-                {"Filter if 3.1",
+                {"Filter if 3.2",
                     <<"{% ifequal var1|length 1 %}Y{% else %}N{% endifequal %}">>,
                      [{var1, []}],
                      <<"N">>},
@@ -433,7 +481,7 @@ run_tests() ->
                             end, GroupAcc, Assertions)
         end, [], tests()),
     
-    io:format("Unit test failures: ~p~n", [Failures]).
+    io:format("Unit test failures: ~p~n", [lists:reverse(Failures)]).
 
 process_unit_test(CompiledTemplate, Vars, Output,Acc, Group, Name) ->
 	case CompiledTemplate of