Browse Source

* Added unit test suite covering most tags that don't require file IO. Please add new tests when implementing new language features, and always run "make test" before committing!
* Support for looking up variables and attributes in dicts and gb_trees (not just proplists).
* Small refactor of erlydtl_compiler to support in-memory compilation and reload (just pass in a binary as the first argument to compile/2 or compile/3)
* Move some template evaluation logic into erlydtl_runtime, a new dependency for compiled templates.
* All filters will now process binaries, not just lists. Some filters operate on the binary directly, others convert to a list and back.
* Removed the "temporary" line in Makefile... sorry Roberto, time to set up your Bash aliases properly :-)
* New option: "compiler_options" is a list that will be passed directly to the Erlang compiler.



git-svn-id: http://erlydtl.googlecode.com/svn/trunk@121 a5195066-8e3e-0410-a82a-05b01b1b9875

emmiller 17 years ago
parent
commit
b80fc1007b

+ 1 - 1
Emakefile

@@ -1,3 +1,3 @@
 {"src/erlydtl/*", [debug_info, {outdir, "ebin"}]}.
 {"src/tests/*", [debug_info, {outdir, "ebin"}]}.
-{"src/demo/*", [debug_info, {outdir, "ebin"}]}.
+{"src/demo/*", [debug_info, {outdir, "ebin"}]}.

+ 5 - 3
Makefile

@@ -1,12 +1,14 @@
-ERL=/Users/rsaccon/R11B/start.sh  # temporary
-# ERL=erl
+ERL=erl
 
 all:
 	$(ERL) -make 
 
 run:
 	$(ERL) -pa `pwd`/ebin
+
+test:
+	$(ERL) -noshell -s erlydtl_unittests run_tests
 	
 clean:
 	rm -fv ebin/*.beam
-	rm -fv erl_crash.dump
+	rm -fv erl_crash.dump

+ 98 - 95
src/erlydtl/erlydtl_compiler.erl

@@ -38,8 +38,6 @@
 %% --------------------------------------------------------------------
 %% Definitions
 %% --------------------------------------------------------------------
--define(COMPILE_OPTIONS, [verbose, report_errors, report_warnings]).
-
 -export([compile/2, compile/3]).
 
 -record(dtl_context, {
@@ -52,6 +50,7 @@
     custom_tags_dir = [],
     reader = {file, read_file},
     module = [],
+    compiler_options = [verbose, report_errors],
     force_recompile = false}).
 
 -record(ast_info, {
@@ -63,9 +62,27 @@
     counter = 0,
     custom_tags = []}).    
 
+compile(Binary, Module) when is_binary(Binary) ->
+    compile(Binary, Module, []);
 
-compile(File, Module) -> 
+compile(File, Module) ->
     compile(File, Module, []).
+
+compile(Binary, Module, Options) when is_binary(Binary) ->
+    File = "",
+    CheckSum = "",
+    case parse(Binary) of
+        {ok, DjangoParseTree} ->
+            case compile_to_binary(File, DjangoParseTree, 
+                    init_dtl_context(File, Module, Options), CheckSum) of
+                {ok, Module1, _} ->
+                    {ok, Module1};
+                Err ->
+                    Err
+            end;
+        Err ->
+            Err
+    end;
     
 compile(File, Module, Options) ->  
     crypto:start(),
@@ -74,33 +91,18 @@ compile(File, Module, Options) ->
         ok ->
             ok;
         {ok, DjangoParseTree, CheckSum} ->
-            try body_ast(DjangoParseTree, Context, #treewalker{}) of
-                {{Ast, Info}, _} ->                  
-                    CompileOptions = case  proplists:get_value(verbose, Options, false) of
-                        true -> ?COMPILE_OPTIONS;
-                        _ -> []
-                    end,
-                    case compile:forms(forms(File, Module, Ast, Info, CheckSum), CompileOptions) of
-                        {ok, Module1, Bin} -> 
-                            OutDir = proplists:get_value(out_dir, Options, "ebin"),       
-                            BeamFile = filename:join([OutDir, atom_to_list(Module1) ++ ".beam"]),
-                            case file:write_file(BeamFile, Bin) of
-                                ok ->
-                                    code:purge(Module1),
-                                    case code:load_binary(Module1, atom_to_list(Module1) ++ ".erl", Bin) of
-                                        {module, _} -> ok;
-                                        _ -> {error, lists:concat(["code reload failed: ", BeamFile])}
-                                    end;
-                                {error, Reason} ->
-                                    {error, lists:concat(["beam generation failed (", Reason, "): ", BeamFile])}
-                            end;
-                        error ->
-                            {error, lists:concat(["compilation failed: ", File])};
-                        OtherError ->
-                            OtherError
-                    end
-            catch 
-                throw:Error -> Error
+            case compile_to_binary(File, DjangoParseTree, Context, CheckSum) of
+                {ok, Module1, Bin} ->
+                    OutDir = proplists:get_value(out_dir, Options, "ebin"),       
+                    BeamFile = filename:join([OutDir, atom_to_list(Module1) ++ ".beam"]),
+                    case file:write_file(BeamFile, Bin) of
+                        ok ->
+                            ok;
+                        {error, Reason} ->
+                            {error, lists:concat(["beam generation failed (", Reason, "): ", BeamFile])}
+                    end;
+                Err ->
+                    Err
             end;
         Err ->
             Err
@@ -111,17 +113,40 @@ compile(File, Module, Options) ->
 %% Internal functions
 %%====================================================================
 
+compile_to_binary(File, DjangoParseTree, Context, CheckSum) ->
+    try body_ast(DjangoParseTree, Context, #treewalker{}) of
+        {{Ast, Info}, _} ->
+            case compile:forms(forms(File, Context#dtl_context.module, Ast, Info, CheckSum), 
+                    Context#dtl_context.compiler_options) of
+                {ok, Module1, Bin} -> 
+                    code:purge(Module1),
+                    case code:load_binary(Module1, atom_to_list(Module1) ++ ".erl", Bin) of
+                        {module, _} -> {ok, Module1, Bin};
+                        _ -> {error, lists:concat(["code reload failed: ", Module1])}
+                    end;
+                error ->
+                    {error, lists:concat(["compilation failed: ", File])};
+                OtherError ->
+                    OtherError
+            end
+    catch 
+        throw:Error -> Error
+    end.
+                
+init_dtl_context(File, Module, Options) when is_list(Module) ->
+    init_dtl_context(File, list_to_atom(Module), Options);
 init_dtl_context(File, Module, Options) ->
     Ctx = #dtl_context{},
     #dtl_context{
         parse_trail = [File], 
-        module = list_to_atom(Module),        
+        module = Module,
         doc_root = proplists:get_value(doc_root, Options, filename:dirname(File)),
         custom_tags_dir = proplists:get_value(custom_tags_dir, Options, Ctx#dtl_context.custom_tags_dir),
         vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars), 
         reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
+        compiler_options = proplists:get_value(compiler_options, Options, Ctx#dtl_context.compiler_options),
         force_recompile = proplists:get_value(force_recompile, Options, Ctx#dtl_context.force_recompile)}.
-    
+
 
 is_up_to_date(_, #dtl_context{force_recompile = true}) ->
     false;
@@ -158,14 +183,15 @@ is_up_to_date(CheckSum, Context) ->
     end.
     
     
-parse(Data) ->
-    case erlydtl_scanner:scan(binary_to_list(Data)) of
-        {ok, Tokens} ->
-            erlydtl_parser:parse(Tokens);
-        Err ->
-            Err
-    end.        
-  
+parse(File, Context) ->  
+    {M, F} = Context#dtl_context.reader,
+    case catch M:F(File) of
+        {ok, Data} ->
+            CheckSum = binary_to_list(crypto:sha(Data)),
+            parse(CheckSum, Data, Context);
+        _ ->
+            {error, "reading " ++ File ++ " failed "}
+    end.
         
 parse(CheckSum, Data, Context) ->
     case is_up_to_date(CheckSum, Context) of
@@ -180,23 +206,19 @@ parse(CheckSum, Data, Context) ->
             end
     end.
 
-
-parse(File, Context) ->  
-    {M, F} = Context#dtl_context.reader,
-    case catch M:F(File) of
-        {ok, Data} ->
-            CheckSum = binary_to_list(crypto:sha(Data)),
-            parse(CheckSum, Data, Context);
-        _ ->
-            {error, "reading " ++ File ++ " failed "}
-    end.
-
+parse(Data) ->
+    case erlydtl_scanner:scan(binary_to_list(Data)) of
+        {ok, Tokens} ->
+            erlydtl_parser:parse(Tokens);
+        Err ->
+            Err
+    end.        
   
-forms(File, Module, BodyAst, BodyInfo, CheckSum) ->    
+forms(File, Module, BodyAst, BodyInfo, CheckSum) ->
     Render0FunctionAst = erl_syntax:function(erl_syntax:atom(render),
         [erl_syntax:clause([], none, [erl_syntax:application(none, 
                         erl_syntax:atom(render), [erl_syntax:list([])])])]),
-    Function2 = erl_syntax:application(none, erl_syntax:atom(render2),
+    Function2 = erl_syntax:application(none, erl_syntax:atom(render2), 
         [erl_syntax:variable("Variables")]),
     ClauseOk = erl_syntax:clause([erl_syntax:variable("Val")], none,
         [erl_syntax:tuple([erl_syntax:atom(ok), erl_syntax:variable("Val")])]),     
@@ -224,17 +246,6 @@ forms(File, Module, BodyAst, BodyInfo, CheckSum) ->
             [erl_syntax:clause([erl_syntax:variable("Variables")], none, 
                 [BodyAst])]),   
     
-    ProplistsClauseErr = erl_syntax:clause([erl_syntax:atom(undefined)], none, 
-        [erl_syntax:application(none, erl_syntax:atom(throw),
-            [erl_syntax:tuple([erl_syntax:atom(undefined_variable), erl_syntax:variable("Key")])])]),  
-    ProplistsClauseOk = erl_syntax:clause([erl_syntax:variable("Val")], none, 
-        [erl_syntax:variable("Val")]),       
-    ProplistsFunctionAst = erl_syntax:function(erl_syntax:atom(get_value), 
-        [erl_syntax:clause([erl_syntax:variable("Key"), erl_syntax:variable("L")], none, 
-                [erl_syntax:case_expr(erl_syntax:application(erl_syntax:atom(proplists), 
-                            erl_syntax:atom(get_value), [erl_syntax:variable("Key"), erl_syntax:variable("L")]), 
-                        [ProplistsClauseErr, ProplistsClauseOk])])]),
-    
     ModuleAst  = erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(Module)]),
     
     ExportAst = erl_syntax:attribute(erl_syntax:atom(export),
@@ -244,8 +255,8 @@ forms(File, Module, BodyAst, BodyInfo, CheckSum) ->
                     erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0))])]),
     
     [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, Render0FunctionAst,
-            Render1FunctionAst, SourceFunctionAst, DependenciesFunctionAst, RenderInternalFunctionAst, 
-            ProplistsFunctionAst | BodyInfo#ast_info.pre_render_asts]].    
+            Render1FunctionAst, SourceFunctionAst, DependenciesFunctionAst, RenderInternalFunctionAst
+            | BodyInfo#ast_info.pre_render_asts]].    
 
         
 % child templates should only consist of blocks at the top level
@@ -465,24 +476,22 @@ search_for_escape_filter(_Variable, _Filter) ->
     off.
 
 resolve_variable_ast(VarTuple, Context) ->
-    resolve_variable_ast(VarTuple, Context, none).
+    resolve_variable_ast(VarTuple, Context, 'fetch_value').
  
 resolve_ifvariable_ast(VarTuple, Context) ->
-    resolve_variable_ast(VarTuple, Context, erl_syntax:atom(proplists)).
+    resolve_variable_ast(VarTuple, Context, 'find_value').
            
-resolve_variable_ast({{identifier, _, VarName}}, Context, ModuleAst) ->
-    {resolve_variable_name_ast(VarName, Context, ModuleAst), VarName};
+resolve_variable_ast({{identifier, _, VarName}}, Context, FinderFunction) ->
+    {resolve_variable_name_ast(VarName, Context, FinderFunction), VarName};
 
-resolve_variable_ast({{identifier, _, VarName}, {identifier, _, AttrName}}, Context, ModuleAst) ->
-    {erl_syntax:application(ModuleAst, erl_syntax:atom(get_value),
+resolve_variable_ast({{identifier, _, VarName}, {identifier, _, AttrName}}, Context, FinderFunction) ->
+    {erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction),
                     [erl_syntax:atom(AttrName), resolve_variable_name_ast(VarName, Context)]), VarName}.
 
-
 resolve_variable_name_ast(VarName, Context) ->
-    resolve_variable_name_ast(VarName, Context, none).
-  
+    resolve_variable_name_ast(VarName, Context, 'fetch_value').
     
-resolve_variable_name_ast(VarName, Context, ModuleAst) ->
+resolve_variable_name_ast(VarName, Context, FinderFunction) ->
     VarValue = lists:foldl(fun(Scope, Value) ->
                 case Value of
                     undefined ->
@@ -493,7 +502,7 @@ resolve_variable_name_ast(VarName, Context, ModuleAst) ->
         end, undefined, Context#dtl_context.local_scopes),
     case VarValue of
         undefined ->
-            erl_syntax:application(ModuleAst, erl_syntax:atom('get_value'),
+            erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction),
                 [erl_syntax:atom(VarName), erl_syntax:variable("Variables")]);
         _ ->
             VarValue
@@ -523,14 +532,8 @@ ifelse_ast(Variable, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseCont
     Info = merge_info(IfContentsInfo, ElseContentsInfo),
     VarNames = Info#ast_info.var_names,
     {Ast, VarName} = resolve_ifvariable_ast(Variable, Context),
-    {{erl_syntax:case_expr(Ast,
-        [erl_syntax:clause([erl_syntax:string("")], none, 
-                [ElseContentsAst]),
-            erl_syntax:clause([erl_syntax:atom(undefined)], none,
-                [ElseContentsAst]),
-            erl_syntax:clause([erl_syntax:atom(false)], none,
-                [ElseContentsAst]),
-            erl_syntax:clause([erl_syntax:string("0")], none,
+    {{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])
@@ -553,12 +556,12 @@ ifequalelse_ast(Args, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseCon
         end,
         {[], Info#ast_info.var_names},
         Args),
-    Ast = erl_syntax:application(none, erl_syntax:atom(apply), [erl_syntax:fun_expr(
-        [erl_syntax:clause([erl_syntax:variable("Arg1"), erl_syntax:variable("Arg2")], none, 
-            [erl_syntax:case_expr(erl_syntax:variable("Arg1"),
-                [erl_syntax:clause([erl_syntax:variable("Arg2")], none, [IfContentsAst]),
-                    erl_syntax:clause([erl_syntax:underscore()], none, [ElseContentsAst])])])]), 
-                        erl_syntax:list([Arg1Ast, Arg2Ast])]),    
+    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}.         
 
 
@@ -616,11 +619,11 @@ unescape_string_literal([$\\ | Rest], Acc, noslash) ->
 unescape_string_literal([C | Rest], Acc, noslash) ->
     unescape_string_literal(Rest, [C | Acc], noslash);
 unescape_string_literal("n" ++ Rest, Acc, slash) ->
-    unescape_string_literal(Rest, ["\n" | Acc], noslash);
+    unescape_string_literal(Rest, [$\n | Acc], noslash);
 unescape_string_literal("r" ++ Rest, Acc, slash) ->
-    unescape_string_literal(Rest, ["\r" | Acc], noslash);
+    unescape_string_literal(Rest, [$\r | Acc], noslash);
 unescape_string_literal("t" ++ Rest, Acc, slash) ->
-    unescape_string_literal(Rest, ["\t" | Acc], noslash);
+    unescape_string_literal(Rest, [$\t | Acc], noslash);
 unescape_string_literal([C | Rest], Acc, slash) ->
     unescape_string_literal(Rest, [C | Acc], noslash).
 
@@ -706,4 +709,4 @@ call_ast(Module, Variable, AstInfo, TreeWalker) ->
 		 [ErrStrAst]),
     CallAst = erl_syntax:case_expr(AppAst, [OkAst, ErrorAst]),   
     Module2 = list_to_atom(Module),
-    with_dependencies(Module2:dependencies(), {{CallAst, AstInfo}, TreeWalker}).
+    with_dependencies(Module2:dependencies(), {{CallAst, AstInfo}, TreeWalker}).

+ 175 - 42
src/erlydtl/erlydtl_filters.erl

@@ -37,82 +37,153 @@
 
 -compile(export_all).
 
-capfirst(Input) ->
-    [H|T] = lists:flatten(Input),
-    [string:to_upper(H)] ++ T.
+-define(NO_ENCODE(C), ((C >= $a andalso C =< $z) orelse
+                                  (C >= $A andalso C =< $Z) orelse
+                                  (C >= $0 andalso C =< $9) orelse
+                                  (C =:= $\. orelse C =:= $- 
+                                  orelse C =:= $~ orelse C =:= $_))).
 
-center(Input, Number) ->
-    string:centre(lists:flatten(Input), Number).
+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].
 
-escapejs([Input]) when is_list(Input) ->
+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).
+
+escapejs([Input]) when is_list(Input) or is_binary(Input) ->
     escapejs(Input);
-escapejs(Input) ->
+escapejs(Input) when is_binary(Input) ->
+    escapejs(Input, 0);
+escapejs(Input) when is_list(Input) ->
     escapejs(Input, []).
 
-first([[First|_Rest]]) ->
-    [First].
+first([Input]) when is_list(Input) or is_binary(Input) ->
+    first(Input);
+first([First|_Rest]) ->
+    [First];
+first(<<First, _/binary>>) ->
+    <<First>>.
 
-fix_ampersands(Input) ->
-    fix_ampersands(lists:flatten(Input), []).
+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, []).
 
-force_escape([Input]) when is_list(Input) ->
+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) ->
-    escape(binary_to_list(Input), []);
-force_escape(Input) ->
-    Input.
+    escape(Input, 0).
 
+format_integer(Input) when is_integer(Input) ->
+    integer_to_list(Input);
 format_integer(Input) ->
-    case Input of
-        N when is_integer(N) ->
-            integer_to_list(N);
-        Other ->
-            Other
-    end.
+    Input.
 
 join([Input], Separator) when is_list(Input) ->
     string:join(Input, Separator).
 
-last([Input]) when is_list(Input) ->
+last([Input]) when is_list(Input) or is_binary(Input) ->
+    last(Input);
+last(Input) when is_binary(Input) ->
+    case size(Input) of
+        0 -> Input;
+        N ->
+            Offset = N - 1,
+            <<_:Offset/binary, Byte/binary>> = Input,
+            Byte
+    end;
+last(Input) when is_list(Input) ->
     [lists:last(Input)].
 
 length([Input]) when is_list(Input) ->
     integer_to_list(erlang:length(Input));
-length(Input) when is_list(Input) ->
-    integer_to_list(erlang:length(Input)).
+length([Input]) when is_binary(Input) ->
+    integer_to_list(size(Input)).
 
 length_is(Input, Number) when is_list(Input) ->
-    lists:concat([erlang:length(Input) =:= Number]).
+    lists:concat([?MODULE:length(Input) =:= integer_to_list(Number)]).
 
+linebreaksbr([Input]) when is_list(Input) or is_binary(Input) ->
+    linebreaksbr(Input);
+linebreaksbr(Input) when is_binary(Input) ->
+    linebreaksbr(Input, 0);
 linebreaksbr(Input) ->
-    linebreaksbr(lists:flatten(Input), []).
+    linebreaksbr(Input, []).
 
-ljust(Input, Number) ->
-    string:left(lists:flatten(Input), Number).
+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).
 
+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(lists:flatten(Input)).
+    string:to_lower(Input).
 
+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(lists:flatten(Input), Number).
+    string:right(Input, Number).
 
-plus([Input], Number) when is_list(Input) ->
-    integer_to_list(list_to_integer(Input) + Number);
+plus([Input], Number) when is_list(Input) or is_binary(Input) ->
+    plus(Input, Number);
+plus(Input, Number) when is_binary(Input) ->
+    list_to_binary(plus(binary_to_list(Input), Number));
 plus(Input, Number) when is_list(Input) ->
-    integer_to_list(list_to_integer(Input) + Number);
+    integer_to_list(plus(list_to_integer(Input), Number));
 plus(Input, Number) when is_integer(Input) ->
     Input + Number.
 
+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(lists:flatten(Input)).
+    string:to_upper(Input).
 
-urlencode(Input) ->
-    urlencode(lists:flatten(Input), []).
+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) ->
+    urlencode(Input, []).
 
 % internal
 
+escape(Binary, Index) when is_binary(Binary) ->
+    case Binary of
+        <<Pre:Index/binary, $<, Post/binary>> ->
+            process_binary_match(Pre, <<"&lt;">>, size(Post), escape(Post, 0));
+        <<Pre:Index/binary, $>, Post/binary>> ->
+            process_binary_match(Pre, <<"&gt;">>, size(Post), escape(Post, 0));
+        <<Pre:Index/binary, $&, Post/binary>> ->
+            process_binary_match(Pre, <<"&amp;">>, size(Post), escape(Post, 0));
+        <<Pre:Index/binary, 34, Post/binary>> ->
+            process_binary_match(Pre, <<"&quot;">>, size(Post), escape(Post, 0));
+        <<Pre:Index/binary, 39, Post/binary>> ->
+            process_binary_match(Pre, <<"&#039;">>, size(Post), escape(Post, 0));
+        <<_:Index/binary, _, _/binary>> ->
+            escape(Binary, Index + 1);
+        Binary ->
+            Binary
+    end;
 escape([], Acc) ->
     lists:reverse(Acc);
 escape("<" ++ Rest, Acc) ->
@@ -128,6 +199,7 @@ escape("'" ++ Rest, Acc) ->
 escape([C | Rest], Acc) ->
     escape(Rest, [C | Acc]).
 
+
 escapejs([], Acc) ->
     lists:reverse(Acc);
 escapejs("'" ++ Rest, Acc) ->
@@ -135,8 +207,28 @@ escapejs("'" ++ Rest, Acc) ->
 escapejs("\"" ++ Rest, Acc) ->
     escapejs(Rest, lists:reverse("\\\"", Acc));
 escapejs([C | Rest], Acc) ->
-    escapejs(Rest, [C | Acc]).
+    escapejs(Rest, [C | Acc]);
+escapejs(Binary, Index) when is_binary(Binary) ->
+    case Binary of
+        <<Pre:Index/binary, 39, Post/binary>> ->
+            process_binary_match(Pre, <<"\\'">>, size(Post), escapejs(Post, 0));
+        <<Pre:Index/binary, 34, Post/binary>> ->
+            process_binary_match(Pre, <<"\\\"">>, size(Post), escapejs(Post, 0));
+        <<_:Index/binary, _/binary>> ->
+            escapejs(Binary, Index + 1);
+        _ ->
+            Binary
+    end.
 
+fix_ampersands(Input, Index) when is_binary(Input) ->
+    case Input of
+        <<Pre:Index/binary, $&, Post/binary>> ->
+            process_binary_match(Pre, <<"&amp;">>, size(Post), Post);
+        <<_:Index/binary, _/binary>> ->
+            fix_ampersands(Input, Index + 1);
+        _ ->
+            Input
+    end;
 fix_ampersands([], Acc) ->
     lists:reverse(Acc);
 fix_ampersands("&" ++ Rest, Acc) ->
@@ -144,6 +236,18 @@ fix_ampersands("&" ++ Rest, Acc) ->
 fix_ampersands([C | Rest], Acc) ->
     fix_ampersands(Rest, [C | Acc]).
 
+linebreaksbr(Input, Index) when is_binary(Input) ->
+    Break = <<"<br />">>,
+    case Input of
+        <<Pre:Index/binary, $\r, $\n, Post/binary>> ->
+            process_binary_match(Pre, Break, size(Post), linebreaksbr(Post, 0));
+        <<Pre:Index/binary, $\n, Post/binary>> ->
+            process_binary_match(Pre, Break, size(Post), linebreaksbr(Post, 0));
+        <<_:Index/binary, _/binary>> ->
+            linebreaksbr(Input, Index + 1);
+        _ ->
+            Input
+    end;
 linebreaksbr([], Acc) ->
     lists:reverse(Acc);
 linebreaksbr("\r\n" ++ Rest, Acc) ->
@@ -153,14 +257,35 @@ linebreaksbr("\n" ++ Rest, Acc) ->
 linebreaksbr([C | Rest], Acc) ->
     linebreaksbr(Rest, [C | Acc]).
 
+lower(Input, Index) ->
+    case Input of
+        <<Pre:Index/binary, Byte, Post/binary>> when Byte >= $A andalso Byte =< $Z ->
+            process_binary_match(Pre, <<(Byte - $A + $a)>>, size(Post), lower(Post, 0));
+        <<_:Index/binary, _/binary>> ->
+            lower(Input, Index + 1);
+        _ ->
+            Input
+    end.
+
 % Taken from quote_plus of mochiweb_util
+
+urlencode(Input, Index) when is_binary(Input) ->
+    case Input of
+        <<_:Index/binary, Byte, _/binary>> when ?NO_ENCODE(Byte) ->
+            urlencode(Input, Index + 1);
+        <<Pre:Index/binary, $\s, Post/binary>> ->
+            process_binary_match(Pre, <<"+">>, size(Post), urlencode(Post, 0));
+        <<Pre:Index/binary, Hi:4, Lo:4, Post/binary>> ->
+            HiDigit = hexdigit(Hi),
+            LoDigit = hexdigit(Lo),
+            Code = <<$\%, HiDigit, LoDigit>>,
+            process_binary_match(Pre, Code, size(Post), urlencode(Post, 0));
+        Input ->
+            Input
+    end;
 urlencode([], Acc) ->
     lists:reverse(Acc);
-urlencode([C | Rest], Acc) when ((C >= $a andalso C =< $z) orelse
-                                  (C >= $A andalso C =< $Z) orelse
-                                  (C >= $0 andalso C =< $9) orelse
-                                  (C =:= $\. orelse C =:= $- 
-                                      orelse C =:= $~ orelse C =:= $_)) ->
+urlencode([C | Rest], Acc) when ?NO_ENCODE(C) ->
     urlencode(Rest, [C | Acc]);
 urlencode([$\s | Rest], Acc) ->
     urlencode(Rest, [$+ | Acc]);
@@ -170,3 +295,11 @@ urlencode([C | Rest], Acc) ->
 
 hexdigit(C) when C < 10 -> $0 + C;
 hexdigit(C) when C < 16 -> $A + (C - 10).
+
+process_binary_match(Pre, Insertion, SizePost, Post) ->
+    case {size(Pre), SizePost} of
+        {0, 0} -> Insertion;
+        {0, _} -> [Insertion, Post];
+        {_, 0} -> [Pre, Insertion];
+        _ -> [Pre, Insertion, Post]
+    end.

+ 54 - 0
src/erlydtl/erlydtl_runtime.erl

@@ -0,0 +1,54 @@
+-module(erlydtl_runtime).
+
+-compile(export_all).
+
+find_value(Key, L) when is_list(L) ->
+    proplists:get_value(Key, L);
+find_value(Key, {GBSize, GBData}) ->
+    case gb_trees:lookup(Key, {GBSize, GBData}) of
+        {value, Val} ->
+            Val;
+        _ ->
+            undefined
+    end;
+find_value(Key, Dict) ->
+    case dict:find(Key, Dict) of
+        {ok, Val} ->
+            Val;
+        _ ->
+            undefined
+    end.
+
+fetch_value(Key, Data) ->
+    case find_value(Key, Data) of
+        undefined ->
+            throw({undefined_variable, Key});
+        Val ->
+            Val
+    end.
+
+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_binary(Arg1) ->
+    are_equal(binary_to_list(Arg1), Arg2);
+are_equal(Arg1, Arg2) when is_binary(Arg2) ->
+    are_equal(Arg1, binary_to_list(Arg2));
+are_equal(Arg1, Arg2) ->
+    Arg1 =:= Arg2.
+
+is_false("") ->
+    true;
+is_false(false) ->
+    true;
+is_false(undefined) ->
+    true;
+is_false("0") ->
+    true;
+is_false(<<"0">>) ->
+    true;
+is_false(<<>>) ->
+    true;
+is_false(_) ->
+    false.

+ 249 - 0
src/tests/erlydtl_unittests.erl

@@ -0,0 +1,249 @@
+-module(erlydtl_unittests).
+
+-export([run_tests/0]).
+
+tests() ->
+    [
+        {"comment", [
+                {"Comment block is excised",
+                    <<"Bob {% comment %}(moron){% endcomment %} Loblaw">>,
+                    [], <<"Bob  Loblaw">>},
+                {"Inline comment is excised",
+                    <<"You're {# not #} a very nice person">>,
+                    [], <<"You're  a very nice person">>}
+            ]},
+        {"autoescape", [
+                {"Autoescape works",
+                    <<"{% autoescape on %}{{ var1 }}{% endautoescape %}">>,
+                    [{var1, "<b>bold</b>"}], <<"&lt;b&gt;bold&lt;/b&gt;">>},
+                {"Nested autoescape",
+                    <<"{% autoescape on %}{{ var1 }}{% autoescape off %}{{ var1 }}{% endautoescape %}{% endautoescape %}">>,
+                    [{var1, "<b>"}], <<"&lt;b&gt;<b>">>}
+            ]},
+        {"string literal", [
+                {"Render literal",
+                    <<"{{ \"foo\" }} is my name">>, [], <<"foo is my name">>},
+                {"Newlines are escaped",
+                    <<"{{ \"foo\\n\" }}">>, [], <<"foo\n">>}
+            ]},
+        {"number literal", [
+                {"Render integer",
+                    <<"{{ 5 }}">>, [], <<"5">>}
+            ]},
+        {"variable", [
+                {"Render variable",
+                    <<"{{ var1 }} is my game">>, [{var1, "bar"}], <<"bar is my game">>},
+                {"Render variable with attribute",
+                    <<"I enjoy {{ var1.game }}">>, [{var1, [{game, "Othello"}]}], <<"I enjoy Othello">>},
+                {"Render variable in dict",
+                    <<"{{ var1 }}">>, dict:store(var1, "bar", dict:new()), <<"bar">>},
+                {"Render variable in gb_tree",
+                    <<"{{ var1 }}">>, gb_trees:insert(var1, "bar", gb_trees:empty()), <<"bar">>},
+                {"Render variable with attribute in dict",
+                    <<"{{ var1.attr }}">>, [{var1, dict:store(attr, "Othello", dict:new())}], <<"Othello">>},
+                {"Render variable with attribute in gb_tree",
+                    <<"{{ var1.attr }}">>, [{var1, gb_trees:insert(attr, "Othello", gb_trees:empty())}], <<"Othello">>}
+            ]},
+        {"if", [
+                {"If/else",
+                    <<"{% if var1 %}boo{% else %}yay{% endif %}">>, [{var1, ""}], <<"yay">>},
+                {"If",
+                    <<"{% if var1 %}boo{% endif %}">>, [{var1, ""}], <<>>},
+                {"If not",
+                    <<"{% if not var1 %}yay{% endif %}">>, [{var1, ""}], <<"yay">>},
+                {"If \"0\"",
+                    <<"{% if var1 %}boo{% endif %}">>, [{var1, "0"}], <<>>},
+                {"If false",
+                    <<"{% if var1 %}boo{% endif %}">>, [{var1, false}], <<>>},
+                {"If undefined",
+                    <<"{% if var1 %}boo{% endif %}">>, [{var1, undefined}], <<>>},
+                {"If other atom",
+                    <<"{% if var1 %}yay{% endif %}">>, [{var1, foobar}], <<"yay">>},
+                {"If non-empty string",
+                    <<"{% if var1 %}yay{% endif %}">>, [{var1, "hello"}], <<"yay">>},
+                {"If proplist",
+                    <<"{% if var1 %}yay{% endif %}">>, [{var1, [{foo, "bar"}]}], <<"yay">>}
+            ]},
+        {"for", [
+                {"Simple loop",
+                    <<"{% for x in list %}{{ x }}{% endfor %}">>, [{'list', ["1", "2", "3"]}],
+                    <<"123">>},
+                {"Expand list",
+                    <<"{% for x, y in list %}{{ x }},{{ y }}\n{% endfor %}">>, [{'list', [["X", "1"], ["X", "2"]]}],
+                    <<"X,1\nX,2\n">>},
+                {"Expand tuple",
+                    <<"{% for x, y in list %}{{ x }},{{ y }}\n{% endfor %}">>, [{'list', [{"X", "1"}, {"X", "2"}]}],
+                    <<"X,1\nX,2\n">>},
+                {"Resolve variable attribute",
+                    <<"{% for number in person.numbers %}{{ number }}\n{% endfor %}">>, [{person, [{numbers, ["411", "911"]}]}],
+                    <<"411\n911\n">>},
+                {"Nested for loop",
+                    <<"{% for outer in list %}{% for inner in outer %}{{ inner }}\n{% endfor %}{% endfor %}">>,
+                    [{'list', [["Al", "Albert"], ["Jo", "Joseph"]]}],
+                    <<"Al\nAlbert\nJo\nJoseph\n">>}
+            ]},
+        {"ifequal", [
+                {"Compare variable to literal",
+                    <<"{% ifequal var1 \"foo\" %}yay{% endifequal %}">>,
+                    [{var1, "foo"}], <<"yay">>},
+                {"Compare variable to unequal literal",
+                    <<"{% ifequal var1 \"foo\" %}boo{% endifequal %}">>,
+                    [{var1, "bar"}], <<>>},
+                {"Compare literal to variable",
+                    <<"{% ifequal \"foo\" var1 %}yay{% endifequal %}">>,
+                    [{var1, "foo"}], <<"yay">>},
+                {"Compare literal to unequal variable",
+                    <<"{% ifequal \"foo\" var1 %}boo{% endifequal %}">>,
+                    [{var1, "bar"}], <<>>}
+            ]},
+        {"ifequal/else", [
+                {"Compare variable to literal",
+                    <<"{% ifequal var1 \"foo\" %}yay{% else %}boo{% endifequal %}">>,
+                    [{var1, "foo"}], <<"yay">>},
+                {"Compare variable to unequal literal",
+                    <<"{% ifequal var1 \"foo\" %}boo{% else %}yay{% endifequal %}">>,
+                    [{var1, "bar"}], <<"yay">>},
+                {"Compare literal to variable",
+                    <<"{% ifequal \"foo\" var1 %}yay{% else %}boo{% endifequal %}">>,
+                    [{var1, "foo"}], <<"yay">>},
+                {"Compare literal to unequal variable",
+                    <<"{% ifequal \"foo\" var1 %}boo{% else %}yay{% endifequal %}">>,
+                    [{var1, "bar"}], <<"yay">>}
+            ]},
+        {"ifnotequal", [
+                {"Compare variable to literal",
+                    <<"{% ifnotequal var1 \"foo\" %}boo{% endifnotequal %}">>,
+                    [{var1, "foo"}], <<>>},
+                {"Compare variable to unequal literal",
+                    <<"{% ifnotequal var1 \"foo\" %}yay{% endifnotequal %}">>,
+                    [{var1, "bar"}], <<"yay">>},
+                {"Compare literal to variable",
+                    <<"{% ifnotequal \"foo\" var1 %}boo{% endifnotequal %}">>,
+                    [{var1, "foo"}], <<>>},
+                {"Compare literal to unequal variable",
+                    <<"{% ifnotequal \"foo\" var1 %}yay{% endifnotequal %}">>,
+                    [{var1, "bar"}], <<"yay">>}
+            ]},
+        {"ifnotequal/else", [
+                {"Compare variable to literal",
+                    <<"{% ifnotequal var1 \"foo\" %}boo{% else %}yay{% endifnotequal %}">>,
+                    [{var1, "foo"}], <<"yay">>},
+                {"Compare variable to unequal literal",
+                    <<"{% ifnotequal var1 \"foo\" %}yay{% else %}boo{% endifnotequal %}">>,
+                    [{var1, "bar"}], <<"yay">>},
+                {"Compare literal to variable",
+                    <<"{% ifnotequal \"foo\" var1 %}boo{% else %}yay{% endifnotequal %}">>,
+                    [{var1, "foo"}], <<"yay">>},
+                {"Compare literal to unequal variable",
+                    <<"{% ifnotequal \"foo\" var1 %}yay{% else %}boo{% endifnotequal %}">>,
+                    [{var1, "bar"}], <<"yay">>}
+            ]},
+        {"filters", [
+                {"Filter a literal",
+                    <<"{{ \"pop\"|capfirst }}">>, [],
+                    <<"Pop">>},
+                {"Filters applied in order",
+                    <<"{{ var1|force_escape|length }}">>, [{var1, <<"&">>}],
+                    <<"5">>},
+                {"Escape is applied last",
+                    <<"{{ var1|escape|linebreaksbr }}">>, [{var1, <<"\n">>}],
+                    <<"&lt;br /&gt;">>},
+                {"|capfirst",
+                    <<"{{ var1|capfirst }}">>, [{var1, "dana boyd"}], 
+                    <<"Dana boyd">>},
+                {"|center:10",
+                    <<"{{ var1|center:10 }}">>, [{var1, "MB"}], 
+                    <<"    MB    ">>},
+                {"|center:1",
+                    <<"{{ var1|center:1 }}">>, [{var1, "KBR"}], 
+                    <<"B">>},
+                {"|escapejs",
+                    <<"{{ var1|escapejs }}">>, [{var1, "Skip's \"Old-Timey\" Diner"}], 
+                    <<"Skip\\'s \\\"Old-Timey\\\" Diner">>},
+                {"|first",
+                    <<"{{ var1|first }}">>, [{var1, "James"}], 
+                    <<"J">>},
+                {"|fix_ampersands",
+                    <<"{{ var1|fix_ampersands }}">>, [{var1, "Ben & Jerry's"}], 
+                    <<"Ben &amp; Jerry's">>},
+                {"|force_escape",
+                    <<"{{ var1|force_escape }}">>, [{var1, "Ben & Jerry's <=> \"The World's Best Ice Cream\""}],
+                    <<"Ben &amp; Jerry&#039;s &lt;=&gt; &quot;The World&#039;s Best Ice Cream&quot;">>},
+                {"|format_integer",
+                    <<"{{ var1|format_integer }}">>, [{var1, 28}], <<"28">>},
+                {"|join:\", \"",
+                    <<"{{ var1|join:\", \" }}">>, [{var1, ["Liberte", "Egalite", "Fraternite"]}],
+                    <<"Liberte, Egalite, Fraternite">>},
+                {"|last",
+                    <<"{{ var1|last }}">>, [{var1, "XYZ"}],
+                    <<"Z">>},
+                {"|length",
+                    <<"{{ var1|length }}">>, [{var1, "antidisestablishmentarianism"}],
+                    <<"28">>},
+                {"|linebreaksbr",
+                    <<"{{ var1|linebreaksbr }}">>, [{var1, "One\nTwo\n\nThree\n\n\n"}],
+                    <<"One<br />Two<br /><br />Three<br /><br /><br />">>},
+                {"|linebreaksbr",
+                    <<"{{ \"One\\nTwo\\n\\nThree\\n\\n\\n\"|linebreaksbr }}">>, [],
+                    <<"One<br />Two<br /><br />Three<br /><br /><br />">>},
+                {"|ljust:10",
+                    <<"{{ var1|ljust:10 }}">>, [{var1, "Gore"}],
+                    <<"Gore      ">>},
+                {"|lower",
+                    <<"{{ var1|lower }}">>, [{var1, "E. E. Cummings"}],
+                    <<"e. e. cummings">>},
+                {"|rjust:10",
+                    <<"{{ var1|rjust:10 }}">>, [{var1, "Bush"}],
+                    <<"      Bush">>},
+                {"|plus:4",
+                    <<"{{ one|plus:4 }}">>, [{one, "1"}],
+                    <<"5">>},
+                {"|upper",
+                    <<"{{ message|upper }}">>, [{message, "That man has a gun."}],
+                    <<"THAT MAN HAS A GUN.">>},
+                {"|urlencode",
+                    <<"{{ url|urlencode }}">>, [{url, "You #$*@!!"}],
+                    <<"You+%23%24%2A%40%21%21">>}
+            ]}
+    ].
+
+run_tests() ->
+    Failures = lists:foldl(
+        fun({Group, Assertions}, GroupAcc) ->
+                io:format("Running test group ~p...~n", [Group]),
+                lists:foldl(fun({Name, DTL, Vars, Output}, Acc) ->
+                            case erlydtl_compiler:compile(DTL, erlydtl_running_test, []) of
+                                {ok, _} ->
+                                    {ok, IOList} = erlydtl_running_test:render(Vars),
+                                    {ok, IOListBin} = erlydtl_running_test:render(vars_to_binary(Vars)),
+                                    case {iolist_to_binary(IOList), iolist_to_binary(IOListBin)} of
+                                        {Output, Output} ->
+                                            Acc;
+                                        {Output, Unexpected} ->
+                                            [{Group, Name, 'binary', Unexpected, Output} | Acc];
+                                        {Unexpected, Output} ->
+                                            [{Group, Name, 'list', Unexpected, Output} | Acc];
+                                        {Unexpected1, Unexpected2} ->
+                                            [{Group, Name, 'list', Unexpected1, Output}, 
+                                                {Group, Name, 'binary', Unexpected2, Output} | Acc]
+                                    end;
+                                Err ->
+                                    [{Group, Name, Err} | Acc]
+                            end
+                    end, GroupAcc, Assertions)
+        end, [], tests()),
+    
+    io:format("Failures: ~p~n", [Failures]),
+    erlang:halt().
+
+vars_to_binary(Vars) when is_list(Vars) ->
+    lists:map(fun
+            ({Key, [H|_] = Value}) when is_tuple(H) ->
+                {Key, vars_to_binary(Value)};
+            ({Key, [H|_] = Value}) when is_integer(H) ->
+                {Key, list_to_binary(Value)};
+            ({Key, Value}) ->
+                {Key, Value}
+        end, Vars);
+vars_to_binary(Vars) ->
+    Vars.

+ 1 - 1
tests/out/test_filters

@@ -13,7 +13,7 @@ Fix ampersands: &amp;
 
 Force_escape: &lt;b&gt;&lt;/b&gt;
 
-Joined: 
+Joined: eins, zwei, drei
 
 Last: t