Permalink
Browse files

* 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.
  • Loading branch information...
1 parent 8406c54 commit b2e3534da9426709654fe0bf462f6c4c89b96f0b @evanmiller evanmiller committed Feb 12, 2008
Showing with 583 additions and 142 deletions.
  1. +1 −1 Emakefile
  2. +5 −3 Makefile
  3. +98 −95 src/erlydtl/erlydtl_compiler.erl
  4. +175 −42 src/erlydtl/erlydtl_filters.erl
  5. +54 −0 src/erlydtl/erlydtl_runtime.erl
  6. +249 −0 src/tests/erlydtl_unittests.erl
  7. +1 −1 tests/out/test_filters
View
@@ -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"}]}.
View
@@ -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
@@ -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}).
Oops, something went wrong.

0 comments on commit b2e3534

Please sign in to comment.