Browse Source

Support for custom tags modules

Also, provide a way to compile a whole directory into a custom tag module
Evan Miller 14 years ago
parent
commit
d4aa8a1b89
2 changed files with 180 additions and 49 deletions
  1. 24 0
      README.markdown
  2. 156 49
      src/erlydtl/erlydtl_compiler.erl

+ 24 - 0
README.markdown

@@ -41,6 +41,11 @@ defaults to the compiled template's directory.
 E.g. if $custom_tags_dir/foo contains `<b>{{ bar }}</b>`, then `{% foo bar=100 %}` 
 will evaluate to `<b>100</b>`. Get it?
 
+* `custom_tags_module` - A module to be used for handling custom tags. Each custom
+tag should correspond to an exported function, e.g.: 
+
+    some_tag(Variables, TranslationFun) -> iolist()
+
 * `vars` - Variables (and their values) to evaluate at compile-time rather than
 render-time. 
 
@@ -60,6 +65,25 @@ for all string marked as trans (`{% trans "StringValue" %}` on templates).
 See README_I18N.
 
 
+Helper compilation
+------------------
+
+Helpers provide additional templating functionality and can be used in
+conjunction with the `custom_tags_module` option above. They can be created
+from a directory of templates thusly:
+
+    erlydtl:compile_dir("/path/to/dir", my_helper_module_name)
+    
+    erlydtl:compile_dir("/path/to/dir", my_helper_module_name, Options)
+
+The resulting module will export a function for each template appearing
+in the specified directory. Options is the same as for compile/3.
+
+Compiling a helper module can be more efficient than using `custom_tags_dir`
+because the helper functions will be compiled only once (rather than once
+per template).
+
+
 Usage (of a compiled template)
 ------------------------------ 
 

+ 156 - 49
src/erlydtl/erlydtl_compiler.erl

@@ -38,7 +38,7 @@
 %% --------------------------------------------------------------------
 %% Definitions
 %% --------------------------------------------------------------------
--export([compile/2, compile/3, parse/1]).
+-export([compile/2, compile/3, compile_dir/2, compile_dir/3, parse/1]).
 
 -record(dtl_context, {
     local_scopes = [], 
@@ -48,6 +48,7 @@
     parse_trail = [],
     vars = [],
     custom_tags_dir = [],
+    custom_tags_module = none,
     reader = {file, read_file},
     module = [],
     compiler_options = [verbose, report_errors],
@@ -62,7 +63,6 @@
     pre_render_asts = []}).
     
 -record(treewalker, {
-%    custom_tags = [],
     counter = 0
 }).    
 
@@ -96,18 +96,7 @@ compile(File, Module, Options) ->
         {ok, DjangoParseTree, CheckSum} ->
             case compile_to_binary(File, DjangoParseTree, Context, CheckSum) of
                 {ok, Module1, Bin} ->
-                    case proplists:get_value(out_dir, Options) of
-                        undefined ->
-                            ok;
-                        OutDir ->
-                            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
-                    end;
+                    write_binary(Module1, Bin, Options);
                 Err ->
                     Err
             end;
@@ -116,34 +105,99 @@ compile(File, Module, Options) ->
     end.
     
 
+compile_dir(Dir, Module) ->
+    compile_dir(Dir, Module, []).
+
+compile_dir(Dir, Module, Options) ->
+    Context = init_dtl_context_dir(Dir, Module, Options),
+    Files = case file:list_dir(Dir) of
+        {ok, FileList} -> FileList;
+        _ -> []
+    end,
+    {ParserResults, ParserErrors} = lists:foldl(fun
+            ("."++_, Acc) -> Acc;
+            (File, {ResultAcc, ErrorAcc}) ->
+                FilePath = filename:join([Dir, File]),
+                case parse(FilePath, Context) of
+                    ok -> {ResultAcc, ErrorAcc};
+                    {ok, DjangoParseTree, CheckSum} -> {[{File, DjangoParseTree, CheckSum}|ResultAcc], ErrorAcc};
+                    Err -> {ResultAcc, [Err|ErrorAcc]}
+                end
+        end, {[], []}, Files),
+    case ParserErrors of
+        [] ->
+            case compile_multiple_to_binary(Dir, ParserResults, Context) of
+                {ok, Module1, Bin} ->
+                    write_binary(Module1, Bin, Options);
+                Err ->
+                    Err
+            end;
+        [Error|_] ->
+            Error
+    end.
+
 %%====================================================================
 %% Internal functions
 %%====================================================================
 
+write_binary(Module1, Bin, Options) ->
+    case proplists:get_value(out_dir, Options) of
+        undefined ->
+            ok;
+        OutDir ->
+            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
+    end.
+
+compile_multiple_to_binary(Dir, ParserResults, Context) ->
+    {Functions, {AstInfo, _}} = lists:mapfoldl(fun({File, DjangoParseTree, CheckSum}, {AstInfo, TreeWalker}) ->
+                FilePath = full_path(File, Context#dtl_context.doc_root),
+                {{BodyAst, BodyInfo}, TreeWalker1} = with_dependency({FilePath, CheckSum}, body_ast(DjangoParseTree, Context, TreeWalker)),
+                FunctionName = filename:rootname(filename:basename(File)),
+                Function1 = erl_syntax:function(erl_syntax:atom(FunctionName),
+                    [erl_syntax:clause([erl_syntax:variable("Variables")], none,
+                            [erl_syntax:application(none, erl_syntax:atom(FunctionName), 
+                                    [erl_syntax:variable("Variables"), erl_syntax:atom(none)])])]),
+                Function2 = erl_syntax:function(erl_syntax:atom(FunctionName), 
+                    [erl_syntax:clause([erl_syntax:variable("Variables"), erl_syntax:variable("TranslationFun")], none,
+                            [BodyAst])]),
+                {{FunctionName, Function1, Function2}, {merge_info(AstInfo, BodyInfo), TreeWalker1}}
+        end, {#ast_info{}, #treewalker{}}, ParserResults),
+    Forms = custom_forms(Dir, Context#dtl_context.module, Functions, AstInfo),
+    compile_forms_and_reload(Dir, Forms, Context#dtl_context.compiler_options).
+
 compile_to_binary(File, DjangoParseTree, Context, CheckSum) ->
     try body_ast(DjangoParseTree, Context, #treewalker{}) of
         {{BodyAst, BodyInfo}, BodyTreeWalker} ->
             try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of
                 {{CustomTagsAst, CustomTagsInfo}, _} ->
-                    case compile:forms(forms(File, Context#dtl_context.module, {BodyAst, BodyInfo}, {CustomTagsAst, CustomTagsInfo}, 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
+                    Forms = forms(File, Context#dtl_context.module, {BodyAst, BodyInfo}, {CustomTagsAst, CustomTagsInfo}, CheckSum), 
+                    compile_forms_and_reload(File, Forms, Context#dtl_context.compiler_options)
             catch 
                 throw:Error -> Error
             end
     catch 
         throw:Error -> Error
     end.
+
+compile_forms_and_reload(File, Forms, CompilerOptions) ->
+    case compile:forms(Forms, CompilerOptions) 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.
                 
 init_dtl_context(File, Module, Options) when is_list(Module) ->
     init_dtl_context(File, list_to_atom(Module), Options);
@@ -154,6 +208,23 @@ init_dtl_context(File, Module, Options) ->
         module = Module,
         doc_root = proplists:get_value(doc_root, Options, filename:dirname(File)),
         custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
+        custom_tags_module = proplists:get_value(custom_tags_module, Options, Ctx#dtl_context.custom_tags_module),
+        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),
+        locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale)}.
+
+init_dtl_context_dir(Dir, Module, Options) when is_list(Module) ->
+    init_dtl_context_dir(Dir, list_to_atom(Module), Options);
+init_dtl_context_dir(Dir, Module, Options) ->
+    Ctx = #dtl_context{},
+    #dtl_context{
+        parse_trail = [], 
+        module = Module,
+        doc_root = proplists:get_value(doc_root, Options, Dir),
+        custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
+        custom_tags_module = proplists:get_value(custom_tags_module, Options, Module),
         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),
@@ -249,7 +320,7 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Cont
         true ->
             custom_tags_clauses_ast1(CustomTags, ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker);
         false ->
-            CustomTagFile = filename:join([Context#dtl_context.custom_tags_dir, Tag]),
+            CustomTagFile = full_path(Tag, Context#dtl_context.custom_tags_dir),
             case parse(CustomTagFile, Context) of
                 {ok, DjangoParseTree, CheckSum} ->
                     {{BodyAst, BodyAstInfo}, TreeWalker1} = with_dependency({CustomTagFile, CheckSum}, 
@@ -262,7 +333,43 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Cont
                     throw(Error)
             end
     end.
-  
+
+dependencies_function(Dependencies) ->
+    erl_syntax:function(
+        erl_syntax:atom(dependencies), [erl_syntax:clause([], none, 
+            [erl_syntax:list(lists:map(fun 
+                    ({XFile, XCheckSum}) -> 
+                        erl_syntax:tuple([erl_syntax:string(XFile), erl_syntax:string(XCheckSum)])
+                end, Dependencies))])]).
+
+translatable_strings_function(TranslatableStrings) ->
+        erl_syntax:function(
+        erl_syntax:atom(translatable_strings), [erl_syntax:clause([], none,
+                [erl_syntax:list(lists:map(fun(String) -> erl_syntax:string(String) end,
+                            TranslatableStrings))])]).
+
+custom_forms(Dir, Module, Functions, AstInfo) ->
+    ModuleAst = erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(Module)]),
+    ExportAst = erl_syntax:attribute(erl_syntax:atom(export),
+        [erl_syntax:list([
+                    erl_syntax:arity_qualifier(erl_syntax:atom(source_dir), erl_syntax:integer(0)),
+                    erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)),
+                    erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0))
+                    | 
+                        lists:foldl(fun({FunctionName, _, _}, Acc) ->
+                            [erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(1)),
+                                erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(2))|Acc]
+                    end, [], Functions)]
+            )]),
+    SourceFunctionAst = erl_syntax:function(
+        erl_syntax:atom(source_dir), [erl_syntax:clause([], none, [erl_syntax:string(Dir)])]),
+    DependenciesFunctionAst = dependencies_function(AstInfo#ast_info.dependencies), 
+    TranslatableStringsFunctionAst = translatable_strings_function(AstInfo#ast_info.translatable_strings),
+    FunctionAsts = lists:foldl(fun({_, Function1, Function2}, Acc) -> [Function1, Function2 | Acc] end, [], Functions),
+
+    [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsFunctionAst
+            | FunctionAsts]].
+
 forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum) ->
     MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
     Render0FunctionAst = erl_syntax:function(erl_syntax:atom(render),
@@ -290,17 +397,9 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
         erl_syntax:atom(source),
             [erl_syntax:clause([], none, [SourceFunctionTuple])]),
     
-    DependenciesFunctionAst = erl_syntax:function(
-        erl_syntax:atom(dependencies), [erl_syntax:clause([], none, 
-            [erl_syntax:list(lists:map(fun 
-                    ({XFile, XCheckSum}) -> 
-                        erl_syntax:tuple([erl_syntax:string(XFile), erl_syntax:string(XCheckSum)])
-                end, MergedInfo#ast_info.dependencies))])]),     
+    DependenciesFunctionAst = dependencies_function(MergedInfo#ast_info.dependencies),
 
-    TranslatableStringsAst = erl_syntax:function(
-        erl_syntax:atom(translatable_strings), [erl_syntax:clause([], none,
-                [erl_syntax:list(lists:map(fun(String) -> erl_syntax:string(String) end,
-                            MergedInfo#ast_info.translatable_strings))])]),
+    TranslatableStringsAst = translatable_strings_function(MergedInfo#ast_info.translatable_strings),
 
     BodyAstTmp = erl_syntax:application(
                     erl_syntax:atom(erlydtl_runtime),
@@ -790,15 +889,23 @@ full_path(File, DocRoot) ->
 
 tag_ast(Name, Args, Context, TreeWalker) ->
     {InterpretedArgs, AstInfo} = lists:mapfoldl(fun
-                ({{identifier, _, Key}, {string_literal, _, Value}}, AstInfoAcc) ->
-                    {erl_syntax:tuple([erl_syntax:string(Key), erl_syntax:string(unescape_string_literal(Value))]), AstInfoAcc};
-                ({{identifier, _, Key}, Value}, AstInfoAcc) ->
-                    {AST, AstInfo1} = resolve_variable_ast(Value, Context),
-                    {erl_syntax:tuple([erl_syntax:string(Key), format(AST,Context)]), merge_info(AstInfo1, AstInfoAcc)}
-            end, #ast_info{}, Args),
-    RenderAst = erl_syntax:application(none, erl_syntax:atom(render_tag),
-        [erl_syntax:string(Name), erl_syntax:list(InterpretedArgs), erl_syntax:variable("TranslationFun")]),
-    {{RenderAst, AstInfo#ast_info{custom_tags = [Name]}}, TreeWalker}.
+            ({{identifier, _, Key}, {string_literal, _, Value}}, AstInfoAcc) ->
+                {erl_syntax:tuple([erl_syntax:string(Key), erl_syntax:string(unescape_string_literal(Value))]), AstInfoAcc};
+            ({{identifier, _, Key}, Value}, AstInfoAcc) ->
+                {AST, AstInfo1} = resolve_variable_ast(Value, Context),
+                {erl_syntax:tuple([erl_syntax:string(Key), format(AST,Context)]), merge_info(AstInfo1, AstInfoAcc)}
+        end, #ast_info{}, Args),
+
+    {RenderAst, RenderInfo} = case Context#dtl_context.custom_tags_module of
+        none ->
+            {erl_syntax:application(none, erl_syntax:atom(render_tag),
+                [erl_syntax:string(Name), erl_syntax:list(InterpretedArgs), erl_syntax:variable("TranslationFun")]),
+            AstInfo#ast_info{custom_tags = [Name]}};
+        Module ->
+            {erl_syntax:application(Module, erl_syntax:atom(Name),
+                [erl_syntax:list(InterpretedArgs), erl_syntax:variable("TranslationFun")]), AstInfo}
+    end,
+    {{RenderAst, RenderInfo}, TreeWalker}.
 
 call_ast(Module, TreeWalkerAcc) ->
     call_ast(Module, erl_syntax:variable("Variables"), #ast_info{}, TreeWalkerAcc).
@@ -811,7 +918,7 @@ call_ast(Module, Variable, AstInfo, TreeWalker) ->
      AppAst = erl_syntax:application(
 		erl_syntax:atom(Module),
 		erl_syntax:atom(render),
-		[Variable]),
+                [Variable, erl_syntax:variable("TranslationFun")]),
     RenderedAst = erl_syntax:variable("Rendered"),
     OkAst = erl_syntax:clause(
 	      [erl_syntax:tuple([erl_syntax:atom(ok), RenderedAst])],