Browse Source

Merge branch 'master' of github.com:evanmiller/erlydtl

Evan Miller 11 years ago
parent
commit
7a11883ea4

+ 2 - 1
include/erlydtl_ext.hrl

@@ -19,7 +19,8 @@
           locale = none,
           verbose = false,
           is_compiling_dir = false,
-          extension_module = undefined
+          extension_module = undefined,
+          scanned_tokens = []
          }).
 
 -record(ast_info, {

+ 125 - 83
src/erlydtl_compiler.erl

@@ -47,7 +47,8 @@
          merge_info/2, 
          format/3, 
          value_ast/5,
-         resolve_scoped_variable_ast/2
+         resolve_scoped_variable_ast/2,
+         interpret_args/3
         ]).
 
 -include("erlydtl_ext.hrl").
@@ -368,13 +369,12 @@ check_scan({ok, Tokens}, Context) ->
                   undefined -> Tokens;
                   {ok, T} -> T
               end,
-    check_parse(erlydtl_parser:parse(Tokens1), [], Context);
+    check_parse(erlydtl_parser:parse(Tokens1), [], Context#dtl_context{ scanned_tokens=Tokens1 });
 check_scan({error, Err, State}, Context) ->
     case call_extension(Context, scan, [State]) of
         undefined ->
             {error, Err};
         {ok, NewState} ->
-            %% io:format("call_extension from:~p~nto: ~p~n", [State, NewState]),
             check_scan(erlydtl_scanner:resume(NewState), Context);
         ExtRes ->
             ExtRes
@@ -386,15 +386,14 @@ check_parse({ok, Parsed}, Acc, _Context) -> {ok, Acc ++ Parsed};
 check_parse({ok, Parsed, C}, Acc, _Context) -> {ok, Acc ++ Parsed, C};
 check_parse({error, _}=Err, _, _Context) -> Err;
 check_parse({error, Err, State}, Acc, Context) ->
-    %% io:format("parse error: ~p~nstate: ~p~n",[Err, State]),
-    {State1, Parsed} = reset_parse_state(State),
+    {State1, Parsed} = reset_parse_state(State, Context),
     case call_extension(Context, parse, [State1]) of
         undefined ->
             {error, Err};
         {ok, ExtParsed} ->
             {ok, Acc ++ Parsed ++ ExtParsed};
         {error, ExtErr, ExtState} ->
-            case reset_parse_state(ExtState) of
+            case reset_parse_state(ExtState, Context) of
                 {_, []} ->
                     %% todo: see if this is indeed a sensible ext error,
                     %% or if we should rather present the original Err message
@@ -406,13 +405,29 @@ check_parse({error, Err, State}, Acc, Context) ->
             ExtRes
     end.
 
-%% backtrack up to the Rootsymbol, and keep the current top-level value stack
-reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]]) ->
-    {[Ts, Tzr, 0, [], []], Parsed};
-reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]]) -> 
-    reset_parse_state([[T | Ts], Tzr, S, Ss, Stack]);
-reset_parse_state([_, _, 0, [], []]=State) -> 
-    {State, []}.
+%% backtrack up to the nearest opening tag, and keep the value stack parsed ok so far
+reset_parse_state([[{Tag, _, _}|_]=Ts, Tzr, _, _, Stack], Context)
+  when Tag==open_tag; Tag==open_var ->
+    %% reached opening tag, so the stack should be sensible here
+    {[reset_token_stream(Ts, Context#dtl_context.scanned_tokens),
+      Tzr, 0, [], []], lists:flatten(Stack)};
+reset_parse_state([_, _, 0, [], []]=State, _Context) ->
+    %% top of (empty) stack
+    {State, []};
+reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]], Context)
+  when is_list(Parsed) ->
+    %% top of good stack
+    {[reset_token_stream(Ts, Context#dtl_context.scanned_tokens),
+      Tzr, 0, [], []], Parsed};
+reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]], Context) ->
+    %% backtrack...
+    reset_parse_state([[T|Ts], Tzr, S, Ss, Stack], Context).
+
+reset_token_stream([T|_], [T|Ts]) -> [T|Ts];
+reset_token_stream(Ts, [_|S]) ->
+    reset_token_stream(Ts, S).
+%% we should find the next token in the list of scanned tokens, or something is real fishy
+
 
 custom_tags_ast(CustomTags, Context, TreeWalker) ->
     {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} = custom_tags_clauses_ast(CustomTags, Context, TreeWalker),
@@ -826,11 +841,9 @@ value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) ->
         {'apply_filter', Variable, Filter} ->
             filter_ast(Variable, Filter, Context, TreeWalker);
         {'attribute', _} = Variable ->
-            {Ast, VarName} = resolve_variable_ast(Variable, Context, EmptyIfUndefined),
-            {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker};
+            resolve_variable_ast(Variable, Context, TreeWalker, EmptyIfUndefined);
         {'variable', _} = Variable ->
-            {Ast, VarName} = resolve_variable_ast(Variable, Context, EmptyIfUndefined),
-            {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker};
+            resolve_variable_ast(Variable, Context, TreeWalker, EmptyIfUndefined);
         {extension, Tag} ->
             extension_ast(Tag, Context, TreeWalker)
     end.
@@ -1012,8 +1025,8 @@ filter_tag_ast(FilterList, Contents, Context, TreeWalker) ->
 								 ([{identifier, _, 'safeseq'}], {{AstAcc, InfoAcc}, TreeWalkerAcc}) ->
 								    {{AstAcc, InfoAcc}, TreeWalkerAcc#treewalker{safe = true}};
 								 (Filter, {{AstAcc, InfoAcc}, TreeWalkerAcc}) ->
-								    {Ast, AstInfo} = filter_ast1(Filter, AstAcc, Context),
-								    {{Ast, merge_info(InfoAcc, AstInfo)}, TreeWalkerAcc}
+								    {{Ast, AstInfo}, TW} = filter_ast1(Filter, AstAcc, Context, TreeWalkerAcc),
+								    {{Ast, merge_info(InfoAcc, AstInfo)}, TW}
 							    end, {{erl_syntax:application(
 								     erl_syntax:atom(erlang),
 								     erl_syntax:atom(iolist_to_binary),
@@ -1076,36 +1089,44 @@ filter_ast_noescape(Variable, [{identifier, _, 'safeseq'}], Context, TreeWalker)
     value_ast(Variable, true, false, Context, TreeWalker#treewalker{safe = true});
 filter_ast_noescape(Variable, Filter, Context, TreeWalker) ->
     {{VariableAst, Info1}, TreeWalker2} = value_ast(Variable, true, false, Context, TreeWalker),
-    {VarValue, Info2} = filter_ast1(Filter, VariableAst, Context),
-    {{VarValue, merge_info(Info1, Info2)}, TreeWalker2}.
-
-filter_ast1([{identifier, _, Name}, {string_literal, _, ArgName}], VariableAst, #dtl_context{ binary_strings = true } = Context) ->
-    filter_ast2(Name, VariableAst, [binary_string(unescape_string_literal(ArgName))], [], Context);
-filter_ast1([{identifier, _, Name}, {string_literal, _, ArgName}], VariableAst, #dtl_context{ binary_strings = false } = Context) ->
-    filter_ast2(Name, VariableAst, [erl_syntax:string(unescape_string_literal(ArgName))], [], Context);
-filter_ast1([{identifier, _, Name}, {number_literal, _, ArgName}], VariableAst, Context) ->
-    filter_ast2(Name, VariableAst, [erl_syntax:integer(list_to_integer(ArgName))], [], Context);
-filter_ast1([{identifier, _, Name}, ArgVariable], VariableAst, Context) ->
-    {ArgAst, ArgVarName} = resolve_variable_ast(ArgVariable, Context, false),
-    filter_ast2(Name, VariableAst, [ArgAst], [ArgVarName], Context);
-filter_ast1([{identifier, _, Name}], VariableAst, Context) ->
-    filter_ast2(Name, VariableAst, [], [], Context).
-
-filter_ast2(Name, VariableAst, [], VarNames, #dtl_context{ filter_modules = [Module|Rest] } = Context) ->
+    {{VarValue, Info2}, TreeWalker3} = filter_ast1(Filter, VariableAst, Context, TreeWalker2),
+    {{VarValue, merge_info(Info1, Info2)}, TreeWalker3}.
+
+filter_ast1([{identifier, _, Name}, {string_literal, _, ArgName}], VariableAst,
+            #dtl_context{ binary_strings = true } = Context, TreeWalker) ->
+    filter_ast2(Name, VariableAst, [binary_string(unescape_string_literal(ArgName))], #ast_info{}, Context, TreeWalker);
+filter_ast1([{identifier, _, Name}, {string_literal, _, ArgName}], VariableAst,
+            #dtl_context{ binary_strings = false } = Context, TreeWalker) ->
+    filter_ast2(Name, VariableAst, [erl_syntax:string(unescape_string_literal(ArgName))], #ast_info{}, Context, TreeWalker);
+filter_ast1([{identifier, _, Name}, {number_literal, _, ArgName}], VariableAst, Context, TreeWalker) ->
+    filter_ast2(Name, VariableAst, [erl_syntax:integer(list_to_integer(ArgName))], #ast_info{}, Context, TreeWalker);
+filter_ast1([{identifier, _, Name}, ArgVariable], VariableAst, Context, TreeWalker) ->
+    {{ArgAst, ArgInfo}, TreeWalker2} = resolve_variable_ast(ArgVariable, Context, TreeWalker, false),
+    filter_ast2(Name, VariableAst, [ArgAst], ArgInfo, Context, TreeWalker2);
+filter_ast1([{identifier, _, Name}], VariableAst, Context, TreeWalker) ->
+    filter_ast2(Name, VariableAst, [], #ast_info{}, Context, TreeWalker).
+
+filter_ast2(Name, VariableAst, [], VarInfo, #dtl_context{ filter_modules = [Module|Rest] } = Context, TreeWalker) ->
     case lists:member({Name, 1}, Module:module_info(exports)) of
         true ->
-            {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name), 
-				    [VariableAst]), #ast_info{var_names = VarNames}};
+            {{erl_syntax:application(
+                erl_syntax:atom(Module), erl_syntax:atom(Name),
+                [VariableAst]),
+              VarInfo},
+             TreeWalker};
         false ->
-            filter_ast2(Name, VariableAst, [], VarNames, Context#dtl_context{ filter_modules = Rest })
+            filter_ast2(Name, VariableAst, [], VarInfo, Context#dtl_context{ filter_modules = Rest }, TreeWalker)
     end;
-filter_ast2(Name, VariableAst, [Arg], VarNames, #dtl_context{ filter_modules = [Module|Rest] } = Context) ->
+filter_ast2(Name, VariableAst, [Arg], VarInfo, #dtl_context{ filter_modules = [Module|Rest] } = Context, TreeWalker) ->
     case lists:member({Name, 2}, Module:module_info(exports)) of
         true ->
-            {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
-				    [VariableAst, Arg]), #ast_info{var_names = VarNames}};
+            {{erl_syntax:application(
+                erl_syntax:atom(Module), erl_syntax:atom(Name),
+                [VariableAst, Arg]),
+              VarInfo},
+             TreeWalker};
         false ->
-            filter_ast2(Name, VariableAst, [Arg], VarNames, Context#dtl_context{ filter_modules = Rest })
+            filter_ast2(Name, VariableAst, [Arg], VarInfo, Context#dtl_context{ filter_modules = Rest }, TreeWalker)
     end.
 
 search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) ->
@@ -1128,40 +1149,56 @@ search_for_safe_filter({apply_filter, Variable, Filter}, _) ->
 search_for_safe_filter(_Variable, _Filter) ->
     on.
 
-resolve_variable_ast(VarTuple, Context, true) ->
-    resolve_variable_ast1(VarTuple, Context, 'fetch_value');
-resolve_variable_ast(VarTuple, Context, false) ->
-    resolve_variable_ast1(VarTuple, Context, 'find_value').
+finder_function(true) -> {erlydtl_runtime, fetch_value};
+finder_function(false) -> {erlydtl_runtime, find_value}.
+
+finder_function(EmptyIfUndefined, Context) ->
+    case call_extension(Context, finder_function, [EmptyIfUndefined]) of
+        undefined -> finder_function(EmptyIfUndefined);
+        Result -> Result
+    end.
+
+resolve_variable_ast({extension, Tag}, Context, TreeWalker, _) ->
+    extension_ast(Tag, Context, TreeWalker);
+resolve_variable_ast(VarTuple, Context, TreeWalker, EmptyIfUndefined)
+  when is_boolean(EmptyIfUndefined) ->
+    resolve_variable_ast(VarTuple, Context, TreeWalker, finder_function(EmptyIfUndefined, Context));
+resolve_variable_ast(VarTuple, Context, TreeWalker, FinderFunction) ->
+    resolve_variable_ast1(VarTuple, Context, TreeWalker, FinderFunction).
 
-resolve_variable_ast1({attribute, {{identifier, {Row, Col}, AttrName}, Variable}}, Context, FinderFunction) ->
-    {VarAst, VarName} = resolve_variable_ast1(Variable, Context, FinderFunction),
+resolve_variable_ast1({attribute, {{identifier, {Row, Col}, AttrName}, Variable}}, Context, TreeWalker, FinderFunction) ->
+    {{VarAst, VarInfo}, TreeWalker1} = resolve_variable_ast(Variable, Context, TreeWalker, FinderFunction),
     FileNameAst = case Context#dtl_context.parse_trail of 
 		      [] -> erl_syntax:atom(undefined); 
 		      [H|_] -> erl_syntax:string(H)
 		  end,
-    {erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction),
-			    [erl_syntax:atom(AttrName), VarAst, FileNameAst,
-			     erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)])
-			    ]), VarName};
-
-resolve_variable_ast1({variable, {identifier, {Row, Col}, VarName}}, Context, FinderFunction) ->
+    {Runtime, Finder} = FinderFunction,
+    {{erl_syntax:application(
+        erl_syntax:atom(Runtime),
+        erl_syntax:atom(Finder),
+        [erl_syntax:atom(AttrName), VarAst, FileNameAst,
+         erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)])
+        ]),
+      VarInfo},
+     TreeWalker1};
+
+resolve_variable_ast1({variable, {identifier, {Row, Col}, VarName}}, Context, TreeWalker, FinderFunction) ->
     VarValue = case resolve_scoped_variable_ast(VarName, Context) of
-		   undefined ->
-		       FileNameAst = case Context#dtl_context.parse_trail of 
-					 [] -> erl_syntax:atom(undefined); 
-					 [H|_] -> erl_syntax:string(H)
-				     end,
-		       erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction),
-					      [erl_syntax:atom(VarName), erl_syntax:variable("_Variables"), FileNameAst,
-					       erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)])
-					      ]);
-		   Val ->
-		       Val
-	       end,
-    {VarValue, VarName};
-
-resolve_variable_ast1(What, _Context, _FinderFunction) ->
-    error_logger:error_msg("~p:resolve_variable_ast unhandled: ~p~n", [?MODULE, What]).
+                   undefined ->
+                       FileNameAst = case Context#dtl_context.parse_trail of
+                                         [] -> erl_syntax:atom(undefined);
+                                         [H|_] -> erl_syntax:string(H)
+                                     end,
+                       {Runtime, Finder} = FinderFunction,
+                       erl_syntax:application(
+                         erl_syntax:atom(Runtime), erl_syntax:atom(Finder),
+                         [erl_syntax:atom(VarName), erl_syntax:variable("_Variables"), FileNameAst,
+                          erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)])
+                         ]);
+                   Val ->
+                       Val
+               end,
+    {{VarValue, #ast_info{ var_names=[VarName] }}, TreeWalker}.
 
 resolve_scoped_variable_ast(VarName, Context) ->
     lists:foldl(fun(Scope, Value) ->
@@ -1327,7 +1364,7 @@ cycle_ast(Names, Context, TreeWalker) ->
 						   {{S, _}, _} = string_ast(unescape_string_literal(Str), Context, TreeWalker),
 						   {S, VarNamesAcc};
 						({variable, _}=Var, VarNamesAcc) ->
-						   {V, VarName} = resolve_variable_ast(Var, Context, true),
+						   {{V, #ast_info{ var_names=[VarName] }}, _} = resolve_variable_ast(Var, Context, TreeWalker, true),
 						   {V, [VarName|VarNamesAcc]};
 						({number_literal, _, Num}, VarNamesAcc) ->
 						   {format(erl_syntax:integer(Num), Context, TreeWalker), VarNamesAcc};
@@ -1397,17 +1434,22 @@ full_path(File, DocRoot) ->
 %% Custom tags
 %%-------------------------------------------------------------------
 
+interpret_args(Args, Context, TreeWalker) ->
+    lists:foldr(
+      fun ({{identifier, _, Key}, {trans, StringLiteral}}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
+              {{TransAst, TransAstInfo}, TreeWalker0} = translated_ast(StringLiteral, Context, TreeWalkerAcc),
+              {{[erl_syntax:tuple([erl_syntax:atom(Key), TransAst])|ArgsAcc], merge_info(TransAstInfo, AstInfoAcc)}, TreeWalker0};
+          ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
+              {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc),
+              {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0};
+          ({extension, Tag}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
+              {{ExtAst, ExtInfo}, ExtTreeWalker} = extension_ast(Tag, Context, TreeWalkerAcc),
+              {{[ExtAst|ArgsAcc], merge_info(ExtInfo, AstInfoAcc)}, ExtTreeWalker}
+      end, {{[], #ast_info{}}, TreeWalker}, Args).
+
 tag_ast(Name, Args, Context, TreeWalker) ->
-    {{InterpretedArgs, AstInfo1}, TreeWalker1} = lists:foldr(fun
-								 ({{identifier, _, Key}, {trans, StringLiteral}}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
-								    {{TransAst, TransAstInfo}, TreeWalker0} = translated_ast(StringLiteral, Context, TreeWalkerAcc),
-								    {{[erl_syntax:tuple([erl_syntax:atom(Key), TransAst])|ArgsAcc], merge_info(TransAstInfo, AstInfoAcc)}, TreeWalker0};
-								 ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
-								    {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc),
-								    {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0}
-							    end, {{[], #ast_info{}}, TreeWalker}, Args),
-    TagArgs = [erl_syntax:tuple([erl_syntax:atom('__render_variables'), erl_syntax:variable("_Variables")])|InterpretedArgs],
-    {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, TagArgs, Context),
+    {{InterpretedArgs, AstInfo1}, TreeWalker1} = interpret_args(Args, Context, TreeWalker),
+    {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, InterpretedArgs, Context),
     {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker1}.
 
 custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = false }) ->
@@ -1444,8 +1486,8 @@ call_ast(Module, TreeWalkerAcc) ->
     call_ast(Module, erl_syntax:variable("_Variables"), #ast_info{}, TreeWalkerAcc).
 
 call_with_ast(Module, Variable, Context, TreeWalker) ->
-    {VarAst, VarName} = resolve_variable_ast(Variable, Context, false),
-    call_ast(Module, VarAst, #ast_info{var_names=[VarName]}, TreeWalker).
+    {{VarAst, VarInfo}, TreeWalker2} = resolve_variable_ast(Variable, Context, TreeWalker, false),
+    call_ast(Module, VarAst, VarInfo, TreeWalker2).
 
 call_ast(Module, Variable, AstInfo, TreeWalker) ->
     AppAst = erl_syntax:application(

+ 8 - 0
tests/expect/autoescape

@@ -0,0 +1,8 @@
+
+    This is escaped: <b>bold</b>
+    
+    This is not escaped: <b>bold</b>
+
+    This is escaped: &lt;b&gt;bold&lt;/b&gt;
+    
+

+ 15 - 0
tests/expect/comment

@@ -0,0 +1,15 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <title>Test Comment</title>								 
+  </head>
+  <body>
+	
+	bla
+	
+        blue
+        
+        black
+  </body>
+</html>

+ 23 - 0
tests/expect/custom_call

@@ -0,0 +1,23 @@
+>>>> before custom call tag 'comment'
+before
+
+<ul>
+
+	<li>preset-apple</li>
+
+	<li>preset-banana</li>
+
+	<li>preset-coconut</li>
+
+</ul>
+
+after
+>>>> after custom call tag 'comment'
+
+>>>> before custom call tag 'if'
+One but not two:  two 
+Two but not one:  one 
+One: 
+None: 
+
+>>>> after custom call tag 'if'

+ 33 - 0
tests/expect/custom_tag

@@ -0,0 +1,33 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <title>Test variable</title>								 
+  </head>
+  <body>
+	before
+	<object id="myvideo" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="800" height="600">
+	<param name="movie" value="/static/mediaplayer.swf">
+    <param name="allowfullscreen" value="true">
+    <param name="menu" value="false">
+    <param name="flashvars" value="file=/myvid.mp4&image=/mypic.jpg">
+    <!--[if !IE]>-->
+    <object type="application/x-shockwave-flash" data="/static/mediaplayer.swf" width="800" height="620">
+    <param name="allowfullscreen" value="true">
+    <param name="menu" value="false">
+    <param name="flashvars" value="file=/myvid.mp4&image=/mypic.jpg">
+    <!--<![endif]-->
+    <h2>To view the Video:</h2>
+    <p>
+    	<a href="http://www.adobe.com/go/getflashplayer">
+        	<img src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player">
+    	</a>
+    </p>
+    <!--[if !IE]>-->
+    </object>
+    <!--<![endif]-->
+</object>
+
+	after
+  </body>
+</html>

+ 47 - 0
tests/expect/cycle

@@ -0,0 +1,47 @@
+before
+
+<ul>
+
+<li>1. 1 - Apple</li>
+
+<li>2. 2 - Banana</li>
+
+<li>3. 3 - Cherry</li>
+
+<li>4. 4 - Apple</li>
+
+<li>5. 5 - Banana</li>
+
+<li>6. 6 - Cherry</li>
+
+<li>7. 7 - Apple</li>
+
+<li>8. 8 - Banana</li>
+
+<li>9. 9 - Cherry</li>
+
+<li>10. 10 - Apple</li>
+
+<li>11. 11 - Banana</li>
+
+<li>12. 12 - Cherry</li>
+
+<li>13. 13 - Apple</li>
+
+<li>14. 14 - Banana</li>
+
+<li>15. 15 - Cherry</li>
+
+<li>16. 16 - Apple</li>
+
+<li>17. 17 - Banana</li>
+
+<li>18. 18 - Cherry</li>
+
+<li>19. 19 - Apple</li>
+
+<li>20. 20 - Banana</li>
+
+</ul>
+
+after

+ 11 - 0
tests/expect/extends

@@ -0,0 +1,11 @@
+base-barstring
+
+base template
+
+replacing the base title
+
+more of base template
+
+replacing the base content - variable: test-barstring after variable 
+
+end of base template

+ 14 - 0
tests/expect/extends_path

@@ -0,0 +1,14 @@
+base-barstring
+
+base2 template
+
+replacing the base title
+block title 2 from  base1 
+
+more of base2 template
+
+replacing the base content - variable: test-barstring after variable 
+
+block content2 in base 2, should pass through
+
+end of base2 template

+ 10 - 0
tests/expect/extends_path2

@@ -0,0 +1,10 @@
+pre content
+
+
+start_content
+This is include1
+
+end_content
+
+
+post

+ 45 - 0
tests/expect/filters

@@ -0,0 +1,45 @@
+Add: 2 + 2 = 4
+
+Capfirst: Capitalized
+
+Centered:
+<pre>
+       center       
+</pre>
+
+Date format:  Thu, 24 Jul 1975 00:00:00 +0100
+DateTime format: Thu, 24 Jul 1975 07:13:01 +0100
+
+Escape JS: \u0022 \u0027
+
+First letter: f
+
+Fix ampersands: &amp;
+
+Force_escape: &lt;b&gt;&lt;/b&gt;
+
+Joined: eins, zwei, drei
+
+Last: t
+
+Length: 3
+
+Length is 2?: false
+
+Left adjust: 
+<pre>
+left                
+</pre>
+
+Line breaks: Line 1<br />Line 2<br />Line 3
+
+Lowercase: lowercase
+
+Right adjust:
+<pre>
+               right
+</pre>
+
+Uppercase: UPPERCASE
+
+URL Encode: Let%27s%20go%21

+ 13 - 0
tests/expect/for

@@ -0,0 +1,13 @@
+before
+
+<ul>
+
+<li>1. apple</li>
+
+<li>2. banana</li>
+
+<li>3. coconut</li>
+
+</ul>
+
+after

+ 7 - 0
tests/expect/for_list

@@ -0,0 +1,7 @@
+
+More than one apple is called "apples". Only $1 each!
+
+More than one banana is called "bananas". Only $2 each!
+
+More than one coconut is called "coconuts". Only $500 each!
+

+ 7 - 0
tests/expect/for_list_preset

@@ -0,0 +1,7 @@
+
+More than one apple is called "apples".
+
+More than one banana is called "bananas".
+
+More than one coconut is called "coconuts".
+

+ 13 - 0
tests/expect/for_preset

@@ -0,0 +1,13 @@
+before
+
+<ul>
+
+	<li>preset-apple</li>
+
+	<li>preset-banana</li>
+
+	<li>preset-coconut</li>
+
+</ul>
+
+after

+ 13 - 0
tests/expect/for_records

@@ -0,0 +1,13 @@
+before
+
+<ul>
+
+	<li><a href="http://amazon.com">Amazon</a></li>
+
+	<li><a href="http://google.com">Google</a></li>
+
+	<li><a href="http://microsoft.com">Microsoft</a></li>
+
+</ul>
+
+after

+ 23 - 0
tests/expect/for_records_preset

@@ -0,0 +1,23 @@
+before
+
+<ul>
+
+	<li><a href="http://canon.com">Canon</a></li>
+
+	<li><a href="http://leica.com">Leica</a></li>
+
+	<li><a href="http://nikon.com">Nikon</a></li>
+
+</ul>
+
+<ul>
+
+	<li><a href="http://amazon.com">Amazon (preset)</a></li>
+
+	<li><a href="http://google.com">Google (preset)</a></li>
+
+	<li><a href="http://microsoft.com">Microsoft (preset)</a></li>
+
+</ul>
+
+after

+ 7 - 0
tests/expect/for_tuple

@@ -0,0 +1,7 @@
+
+One apple, two apples!
+
+One banana, two bananas!
+
+One coconut, two coconuts!
+

+ 4 - 0
tests/expect/if

@@ -0,0 +1,4 @@
+One but not two:  one 
+Two but not one:  two 
+One:  one 
+None: 

+ 4 - 0
tests/expect/if_preset

@@ -0,0 +1,4 @@
+One but not two:  one 
+Two but not one:  two 
+One:  one 
+None: 

+ 28 - 0
tests/expect/ifequal

@@ -0,0 +1,28 @@
+
+if: var1="foo" and var2="foo" are equal
+
+
+
+if: var1="foo" and var2="foo" are equal
+
+
+
+
+
+else: var1="foo" and var3="bar" are not equal
+
+
+
+if: "foo" and "foo" are equal
+
+
+
+else: "foo" and "bar" are not equal
+
+
+
+if: 99 and 99 are equal
+
+
+
+else: 77 and 99 are not equal

+ 28 - 0
tests/expect/ifequal_preset

@@ -0,0 +1,28 @@
+
+if: var1="foo" and var2="foo" are equal
+
+
+
+if: var1="foo" and var2="foo" are equal
+
+
+
+
+
+else: var1="foo" and var3="bar" are not equal
+
+
+
+if: "foo" and "foo" are equal
+
+
+
+else: "foo" and "bar" are not equal
+
+
+
+if: 99 and 99 are equal
+
+
+
+else: 77 and 99 are not equal

+ 28 - 0
tests/expect/ifnotequal

@@ -0,0 +1,28 @@
+
+
+
+else: var1="foo" and var2="foo" are not equal
+
+
+
+if: var1="foo" and var3="bar" are equal
+
+
+
+if: var1="foo" and var3="bar" are equal
+
+
+
+else: "foo" and "foo" are not equal
+
+
+
+if: "foo" and "bar" are equal
+
+
+
+else: 99 and 99 are not equal
+
+
+
+if: 77 and 99 are equal

+ 28 - 0
tests/expect/ifnotequal_preset

@@ -0,0 +1,28 @@
+
+
+
+else: var1="foo" and var2="foo" are not equal
+
+
+
+if: var1="foo" and var3="bar" are equal
+
+
+
+if: var1="foo" and var3="bar" are equal
+
+
+
+else: "foo" and "foo" are not equal
+
+
+
+if: "foo" and "bar" are equal
+
+
+
+else: 99 and 99 are not equal
+
+
+
+if: 77 and 99 are equal

+ 2 - 0
tests/expect/include

@@ -0,0 +1,2 @@
+Including another file: This is included! foostring1
+

+ 12 - 0
tests/expect/include_path

@@ -0,0 +1,12 @@
+main file
+
+This is template 1.
+
+test-barstring
+
+
+This is template 2
+
+
+
+base-barstring

+ 14 - 0
tests/expect/include_template

@@ -0,0 +1,14 @@
+Including another template: base-barstring
+
+base template
+
+base title
+
+more of base template
+
+base content
+
+end of base template
+
+
+test variable: test-barstring

+ 2 - 0
tests/expect/ssi

@@ -0,0 +1,2 @@
+{{ "Don't evaluate me!" }}
+

+ 1 - 0
tests/expect/trans

@@ -0,0 +1 @@
+Example String

+ 5 - 0
tests/expect/var

@@ -0,0 +1,5 @@
+before varriable1
+foostring1
+after variable1
+foostring2
+after variable2 (HTML-comment-wrapped)

+ 9 - 0
tests/expect/var_preset

@@ -0,0 +1,9 @@
+one
+foostring1
+two
+preset-var1
+three
+foostring2
+four
+preset-var2
+five

+ 86 - 70
tests/src/erlydtl_functional_tests.erl

@@ -131,6 +131,8 @@ setup("ifequal_preset") ->
 setup("ifnotequal") ->
     RenderVars = [{var1, "foo"}, {var2, "foo"}, {var3, "bar"}],
     {ok, RenderVars};        
+setup("now") ->
+    {ok, [], [], skip_check};
 setup("var") ->
     RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}, {var_not_used, "foostring3"}],
     {ok, RenderVars};
@@ -159,11 +161,11 @@ setup("trans") ->
 setup("locale") ->
     {ok, _RenderVars = [{locale, "ru"}]};
 setup("custom_tag1") ->
-    {ok, [{a, <<"a1">>}], [{locale, ru}], [<<"b1">>, <<"\n">>]};
+    {ok, [{a, <<"a1">>}], [{locale, ru}], <<"b1\n">>};
 setup("custom_tag2") ->
-    {ok, [{a, <<"a1">>}], [{locale, ru}, {foo, bar}], [<<"b2">>, <<"\n">>]};
+    {ok, [{a, <<"a1">>}], [{locale, ru}, {foo, bar}], <<"b2\n">>};
 setup("custom_tag3") ->
-    {ok, [{a, <<"a1">>}], [{locale, ru}], [<<"b3">>, <<"\n">>]};
+    {ok, [{a, <<"a1">>}], [{locale, ru}], <<"b3\n">>};
 setup("ssi") ->
     RenderVars = [{path, filename:absname(filename:join(["tests", "input", "ssi_include.html"]))}],
     {ok, RenderVars};
@@ -182,19 +184,26 @@ setup(_) ->
 
 run_tests() ->    
     io:format("Running functional tests...~n"),
-    case filelib:ensure_dir(filename:join([templates_outdir(), "foo"])) of
-        ok ->
+    case [filelib:ensure_dir(
+            filename:join([templates_dir(Dir), "foo"]))
+          || Dir <- ["output", "expect"]] -- [ok,ok]
+    of
+        [] ->
             case fold_tests() of
                 {N, []}->
                     Msg = lists:concat(["All ", N, " functional tests passed~n~n"]),
                     io:format(Msg),
                     {ok, Msg};
-                {_, Errs} ->
-                    io:format("Errors: ~p~n~n",[Errs]),
+                {N, Errs} ->
+                    io:format(
+                      "~b / ~b functional tests failed.~nErrors: ~n",
+                      [length(Errs), N]),
+                    [io:format("  ~s [~s] ~s~n", [Name, Error, Reason])
+                     || {Name, Error, Reason} <- Errs],
                     failed
             end;
-        {error, Reason} ->
-            io:format("Error: ~p~n~n", [Reason]),
+        Err ->
+            [io:format("Ensure dir failed: ~p~n~n", [Reason]) || {error, Reason} <- Err],
             failed
     end.
 
@@ -209,97 +218,104 @@ run_test(Name) ->
 
 fold_tests() ->
     lists:foldl(fun(Name, {AccCount, AccErrs}) ->
-                case test_compile_render(Name) of
-                    ok -> 
-                        {AccCount + 1, AccErrs};
-                    {error, Reason} -> 
-                        {AccCount + 1, [{Name, Reason} | AccErrs]}
-                end
-        end, {0, []}, test_list()
-    ).
+                        Res = case catch test_compile_render(Name) of
+                                  ok -> {AccCount + 1, AccErrs};
+                                  {'EXIT', Reason} ->
+                                      {AccCount + 1, [{Name, crash,
+                                                       io_lib:format("~p", [Reason])}
+                                                      | AccErrs]};
+                                  {Error, Reason} ->
+                                      {AccCount + 1, [{Name, Error, Reason}
+                                                      | AccErrs]}
+                              end,
+                        io:format("~n"), Res
+                end, {0, []}, test_list()).
 
 test_compile_render(Name) ->  
     File = filename:join([templates_docroot(), Name]),
     Module = "example_" ++ Name,
+    io:format(" Template: ~p, ... ", [Name]),
     case setup_compile(Name) of
         {CompileStatus, CompileVars} ->
             Options = [
                 {vars, CompileVars}, 
                 {force_recompile, true},
                 {custom_tags_modules, [erlydtl_custom_tags]}],
-            io:format(" Template: ~p, ... compiling ... ", [Name]),
+            io:format("compiling ... "),
             case erlydtl:compile(File, Module, Options) of
                 ok ->
-                    case CompileStatus of
-                        ok -> test_render(Name, list_to_atom(Module));
-                        _ -> {error, "compiling should have failed :" ++ File}
+                    if CompileStatus =:= ok -> test_render(Name, list_to_atom(Module));
+                       true ->
+                            io:format("missing error"),
+                            {error, "compiling should have failed :" ++ File}
                     end;
                 {error, Err} ->
-                    case CompileStatus of
-                        error ->
-                            io:format("~n"),  
-                            ok;
-                        _ ->
-                            io:format("~nCompile errror: ~p~n",[Err]), 
-                            Err
+                    if CompileStatus =:= error -> io:format("ok");
+                       true -> 
+                            io:format("failed"),
+                            {compile_error, io_lib:format("~p", [Err])}
                     end
             end;
-        skip ->
-            ok;
-        _ ->
-            {error, "no 'setup' clause defined for this test"}
+        skip -> io:format("skipped")
     end.
 
-
 test_render(Name, Module) ->
     File = filename:join([templates_docroot(), Name]),
     {RenderStatus, Vars, Opts, RenderResult} =
         case setup(Name) of
-            {RS, V}       -> {RS, V, [], undefined};
-            {RS, V, O}    -> {RS, V, O, undefined};
+            {RS, V}       -> {RS, V, [], get_expected_result(Name)};
+            {RS, V, O}    -> {RS, V, O, get_expected_result(Name)};
             {RS, V, O, R} -> {RS, V, O, R}
         end,
+    io:format("rendering ... "), 
     case catch Module:render(Vars, Opts) of
-        {ok, Data} ->
-            io:format("rendering~n"), 
-            case RenderStatus of
-                ok ->
-                    case RenderResult of
-                        undefined ->
-                            {File, _} = Module:source(),
-                            OutFile = filename:join([templates_outdir(), filename:basename(File)]),
-                            case file:open(OutFile, [write]) of
-                                {ok, IoDev} ->
-                                    file:write(IoDev, Data),
-                                    file:close(IoDev),
-                                    ok;
-                                Err ->
-                                    Err
+        {ok, Output} ->
+            Data = iolist_to_binary(Output),
+            if RenderStatus =:= ok ->
+                    if RenderResult =:= undefined ->
+                            Devs = [begin 
+                                        FileName = filename:join([templates_dir(Dir), Name]),
+                                        {ok, IoDev} = file:open(FileName, [write]),
+                                        IoDev
+                                    end || Dir <- ["output", "expect"]],
+                            try
+                                [file:write(IoDev, Data) || IoDev <- Devs],
+                                io:format("~n    #### NOTE: created new expected output file: \"tests/expect/~s\"."
+                                          "~n    Please verify contents.", [Name])
+                            after
+                                [file:close(IoDev) || IoDev <- Devs]
                             end;
-                        _ when Data =:= RenderResult ->
-                            ok;
-                        _ ->
-                            {error, lists:flatten(io_lib:format("Test ~s failed\n"
-                                "Expected: ~p\n"
-                                "Value:    ~p\n", [Name, RenderResult, Data]))}
-                        end;
-                _ ->
-                    {error, "rendering should have failed :" ++ File}
+                       RenderResult =:= Data ->
+                            io:format("ok");
+                       RenderResult =:= skip_check ->
+                            io:format("ok (not checked for regression)");
+                       true ->
+                            io:format("failed"),
+                            {error, io_lib:format(
+                                      "Expected output does not match rendered output~n"
+                                      "==Expected==~n~s~n--Actual--~n~s~n==End==~n",
+                                      [RenderResult, Data])}
+                    end;
+               true ->
+                    io:format("missing error"),
+                    {missing_error, "rendering should have failed :" ++ File}
             end;
         {'EXIT', Reason} ->
-            io:format("~n"),
-            {error, lists:flatten(io_lib:format("failed invoking render method of ~p ~p", [Module, Reason]))};
+            io:format("failed"),
+            {render_error, io_lib:format("failed invoking render method of ~p ~p", [Module, Reason])};
         Err ->
-            io:format("~n"),
-            case RenderStatus of
-                error ->  ok;
-                _ -> Err
+            if RenderStatus =:= error -> io:format("ok");
+               true -> io:format("failed"),
+                       {render_error, io_lib:format("~p", [Err])}
             end
     end.   
 
+get_expected_result(Name) ->
+    FileName = filename:join([templates_dir("expect"), Name]),
+    case filelib:is_regular(FileName) of
+        true -> {ok, Data} = file:read_file(FileName), Data;
+        false -> undefined
+    end.
 
-templates_docroot() ->
-    filename:join([erlydtl_deps:get_base_dir(), "tests", "input"]).
-
-templates_outdir() ->   
-    filename:join([erlydtl_deps:get_base_dir(), "tests", "output"]).
+templates_docroot() -> templates_dir("input").
+templates_dir(Name) -> filename:join([erlydtl_deps:get_base_dir(), "tests", Name]).

+ 27 - 12
tests/src/erlydtl_unittests.erl

@@ -1121,19 +1121,32 @@ run_tests() ->
 			 io:format(" Test group ~p...~n", [Group]),
 			 lists:foldl(fun
 					 ({Name, DTL, Vars, Output}, Acc) ->
-					    process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions),
-							      Vars, [], Output, Acc, Group, Name);
+					    try
+                            process_unit_test(
+                              erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions),
+                              Vars, [], Output, Acc, Group, Name)
+                        catch Class:Error -> format_error(Group, Name, Class, Error, Acc) end;
 					 ({Name, DTL, Vars, RenderOpts, Output}, Acc) ->
-					    process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions),
-							      Vars, RenderOpts, Output, Acc, Group, Name);
+					    try
+                            process_unit_test(
+                              erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions),
+                              Vars, RenderOpts, Output, Acc, Group, Name)
+                        catch Class:Error -> format_error(Group, Name, Class, Error, Acc) end;
 					 ({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}, Acc) ->
-					    process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts ++ DefaultOptions),
-							      Vars, RenderOpts, Output, Acc, Group, Name)
+					    try
+                            process_unit_test(
+                              erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts ++ DefaultOptions),
+                              Vars, RenderOpts, Output, Acc, Group, Name)
+                        catch Class:Error -> format_error(Group, Name, Class, Error, Acc) end
 				    end, GroupAcc, Assertions)
 		 end, [], tests()),
 
-    io:format("Unit test failures: ~p~n", [lists:reverse(Failures)]).
+    io:format("Unit test failures: ~b~n", [length(Failures)]),
+    [io:format("  ~s:~s ~s~n", [Group, Name, Error]) || {Group, Name, Error} <- lists:reverse(Failures)].
 
+format_error(Group, Name, Class, Error, Acc) ->
+    [{Group, Name, io_lib:format("~n    ~s:~s~n    ~p", [Class, Error, erlang:get_stacktrace()])}|Acc].
+    
 process_unit_test(CompiledTemplate, Vars, RenderOpts, Output,Acc, Group, Name) ->
     case CompiledTemplate of
 	{ok, _} ->
@@ -1143,16 +1156,18 @@ process_unit_test(CompiledTemplate, Vars, RenderOpts, Output,Acc, Group, Name) -
 		{Output, Output} ->
 		    Acc;
 		{Output, Unexpected} ->
-		    [{Group, Name, 'binary', Unexpected, Output} | Acc];
+		    [{Group, Name, io_lib:format("Unexpected result with binary variables: ~nExpected: ~p~nActual: ~p",
+                                         [Output, Unexpected])} | Acc];
 		{Unexpected, Output} ->
-		    [{Group, Name, 'list', Unexpected, Output} | Acc];
+		    [{Group, Name, io_lib:format("Unexpected result with list variables: ~nExpected: ~p~nActual: ~p",
+                                         [Output, Unexpected])} | Acc];
 		{Unexpected1, Unexpected2} ->
-		    [{Group, Name, 'list', Unexpected1, Output},
-		     {Group, Name, 'binary', Unexpected2, Output} | Acc]
+		    [{Group, Name, io_lib:format("Unexpected result: ~nExpected: ~p~nActual (list): ~p~nActual (binary): ~p",
+                                         [Output, Unexpected1, Unexpected2])} | Acc]
 	    end;
 	Output -> Acc;
 	Err ->
-	    [{Group, Name, Err} | Acc]
+	    [{Group, Name, io_lib:format("Render error: ~p~n", [Err])} | Acc]
     end.