Browse Source

Add support for custom tags having access to extra data

The following arguments are conditionally exposed to custom tags:
* RenderVariables
* Locale
* TranslationFun
Serge Aleynikov 12 years ago
parent
commit
6e7bb36678

+ 13 - 6
README.markdown

@@ -53,12 +53,19 @@ will evaluate to `<b>100</b>`. Get it?
 
 
 * `custom_tags_modules` - A list of modules to be used for handling custom
 * `custom_tags_modules` - A list of modules to be used for handling custom
 tags. The modules will be searched in order and take precedence over
 tags. The modules will be searched in order and take precedence over
-`custom_tags_dir`. Each custom tag should correspond to an exported function,
-e.g.: 
-
-    some_tag(Variables, Context) -> iolist()
-
-The `Context` is specified at render-time with the `custom_tags_context` option.
+`custom_tags_dir`. Each custom tag should correspond to an exported function
+with one of the following signatures: 
+
+    some_tag(TagVars)          -> iolist()
+    some_tag(TagVars, Options) -> iolist()
+
+The `TagVars` are variables provided to a custom tag in the template's body
+(e.g. `{% foo bar=100 %}` results in `TagVars = [{"bar", 100}]`).
+The `Options` are options passed as the second argument to the `render/2` call
+at render-time.  For backward compatibility, if render `Options` include
+a `custom_tags_context` option, its value will be passed as `Options` to the
+custom tag handling function. Note that this backward-compatibility functionality
+will be deprecated in one of the next releases.
 
 
 * `custom_filters_modules` - A list of modules to be used for handling custom
 * `custom_filters_modules` - A list of modules to be used for handling custom
 filters. The modules will be searched in order and take precedence over the
 filters. The modules will be searched in order and take precedence over the

+ 64 - 58
src/erlydtl_compiler.erl

@@ -58,6 +58,7 @@
     binary_strings = true,
     binary_strings = true,
     force_recompile = false,
     force_recompile = false,
     locale = none,
     locale = none,
+    verbose = false,
     is_compiling_dir = false}).
     is_compiling_dir = false}).
 
 
 -record(ast_info, {
 -record(ast_info, {
@@ -160,19 +161,17 @@ write_binary(Module1, Bin, Options, Warnings) ->
     Verbose = proplists:get_value(verbose, Options, false),
     Verbose = proplists:get_value(verbose, Options, false),
     case proplists:get_value(out_dir, Options) of
     case proplists:get_value(out_dir, Options) of
         undefined ->
         undefined ->
-            Verbose =:= true andalso
-                io:format("Template module: ~w not saved (no out_dir option)\n", [Module1]),
+            print(Verbose, "Template module: ~w not saved (no out_dir option)\n", [Module1]),
             ok;
             ok;
         OutDir ->
         OutDir ->
             BeamFile = filename:join([OutDir, atom_to_list(Module1) ++ ".beam"]),
             BeamFile = filename:join([OutDir, atom_to_list(Module1) ++ ".beam"]),
 
 
-            Verbose =:= true andalso
-                io:format("Template module: ~w -> ~s~s\n",
-                    [Module1, BeamFile,
-                        case Warnings of
+            print(Verbose, "Template module: ~w -> ~s~s\n",
+                [Module1, BeamFile,
+                    case Warnings of
                         [] -> "";
                         [] -> "";
                         _  -> io_lib:format("\n  Warnings: ~p", [Warnings])
                         _  -> io_lib:format("\n  Warnings: ~p", [Warnings])
-                        end]),
+                    end]),
 
 
             case file:write_file(BeamFile, Bin) of
             case file:write_file(BeamFile, Bin) of
                 ok ->
                 ok ->
@@ -235,14 +234,12 @@ load_code(Module, Bin, Warnings) ->
         _ -> {error, lists:concat(["code reload failed: ", Module])}
         _ -> {error, lists:concat(["code reload failed: ", Module])}
     end.
     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) ->
+init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) ->
     Ctx = #dtl_context{},
     Ctx = #dtl_context{},
     #dtl_context{
     #dtl_context{
-        parse_trail = [File], 
+        parse_trail = ParseTrail,
         module = Module,
         module = Module,
-        doc_root = proplists:get_value(doc_root, Options, filename:dirname(File)),
+        doc_root = proplists:get_value(doc_root, Options, DefDir),
         filter_modules = proplists:get_value(custom_filters_modules, Options, Ctx#dtl_context.filter_modules) ++ [erlydtl_filters],
         filter_modules = proplists:get_value(custom_filters_modules, Options, Ctx#dtl_context.filter_modules) ++ [erlydtl_filters],
         custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
         custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
         custom_tags_modules = proplists:get_value(custom_tags_modules, Options, Ctx#dtl_context.custom_tags_modules),
         custom_tags_modules = proplists:get_value(custom_tags_modules, Options, Ctx#dtl_context.custom_tags_modules),
@@ -254,29 +251,18 @@ init_dtl_context(File, Module, Options) ->
         binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
         binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
         force_recompile = proplists:get_value(force_recompile, Options, Ctx#dtl_context.force_recompile),
         force_recompile = proplists:get_value(force_recompile, Options, Ctx#dtl_context.force_recompile),
         locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale),
         locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale),
-        is_compiling_dir = false}.
+        verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose),
+        is_compiling_dir = IsCompilingDir}.
+
+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) ->
+    init_context(false, [File], filename:dirname(File), Module, Options).
 
 
 init_dtl_context_dir(Dir, Module, Options) when is_list(Module) ->
 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, list_to_atom(Module), Options);
 init_dtl_context_dir(Dir, 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),
-        filter_modules = proplists:get_value(custom_filters_modules, Options, Ctx#dtl_context.filter_modules) ++ [erlydtl_filters],
-        custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
-        custom_tags_modules = proplists:get_value(custom_tags_modules, Options, Ctx#dtl_context.custom_tags_modules),
-        blocktrans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.blocktrans_fun),
-        blocktrans_locales = proplists:get_value(blocktrans_locales, Options, Ctx#dtl_context.blocktrans_locales),
-        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),
-        binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
-        force_recompile = proplists:get_value(force_recompile, Options, Ctx#dtl_context.force_recompile),
-        locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale),
-        is_compiling_dir = true}.
-
+    init_context(true, [], Dir, Module, Options).
 
 
 is_up_to_date(_, #dtl_context{force_recompile = true}) ->
 is_up_to_date(_, #dtl_context{force_recompile = true}) ->
     false;
     false;
@@ -451,10 +437,7 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
                 erl_syntax:atom(proplists),
                 erl_syntax:atom(proplists),
                 erl_syntax:atom(get_value),
                 erl_syntax:atom(get_value),
                 [erl_syntax:atom(locale), erl_syntax:variable("Options"), erl_syntax:atom(none)]),
                 [erl_syntax:atom(locale), erl_syntax:variable("Options"), erl_syntax:atom(none)]),
-            erl_syntax:application(
-                erl_syntax:atom(proplists),
-                erl_syntax:atom(get_value),
-                [erl_syntax:atom(custom_tags_context), erl_syntax:variable("Options"), erl_syntax:atom(none)])
+            erl_syntax:variable("Options")
         ]),
         ]),
     ClauseOk = erl_syntax:clause([erl_syntax:variable("Val")], none,
     ClauseOk = erl_syntax:clause([erl_syntax:variable("Val")], none,
         [erl_syntax:tuple([erl_syntax:atom(ok), erl_syntax:variable("Val")])]),     
         [erl_syntax:tuple([erl_syntax:atom(ok), erl_syntax:variable("Val")])]),     
@@ -479,16 +462,30 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
 
 
     VariablesAst = variables_function(MergedInfo#ast_info.var_names),
     VariablesAst = variables_function(MergedInfo#ast_info.var_names),
 
 
-    BodyAstTmp = erl_syntax:application(
-                    erl_syntax:atom(erlydtl_runtime),
-                    erl_syntax:atom(stringify_final),
-                    [BodyAst, erl_syntax:atom(BinaryStrings)]),
+    BodyAstTmp = [
+        erl_syntax:match_expr(
+            erl_syntax:variable("_CustomTagOptions"),
+            erl_syntax:application(
+                erl_syntax:atom(proplists),
+                erl_syntax:atom(get_value),
+                [erl_syntax:atom(custom_tags_context),
+                 erl_syntax:variable("RenderOptions"),
+                 erl_syntax:variable("RenderOptions")])),
+        erl_syntax:application(
+            erl_syntax:atom(erlydtl_runtime),
+            erl_syntax:atom(stringify_final),
+            [BodyAst, erl_syntax:atom(BinaryStrings)])
+    ],
 
 
     RenderInternalFunctionAst = erl_syntax:function(
     RenderInternalFunctionAst = erl_syntax:function(
         erl_syntax:atom(render_internal), 
         erl_syntax:atom(render_internal), 
-        [erl_syntax:clause([erl_syntax:variable("_Variables"), erl_syntax:variable("_TranslationFun"), 
-                    erl_syntax:variable("_CurrentLocale"), erl_syntax:variable("_CustomTagsContext")], none, 
-                [BodyAstTmp])]),   
+        [erl_syntax:clause([
+            erl_syntax:variable("_Variables"),
+            erl_syntax:variable("_TranslationFun"), 
+            erl_syntax:variable("_CurrentLocale"),
+            erl_syntax:variable("RenderOptions")],
+            none, BodyAstTmp)]
+    ),   
     
     
     ModuleAst  = erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(Module)]),
     ModuleAst  = erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(Module)]),
     
     
@@ -825,10 +822,12 @@ widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalker) ->
 binary_string(String) ->
 binary_string(String) ->
     erl_syntax:binary([erl_syntax:binary_field(erl_syntax:integer(X)) || X <- String]).
     erl_syntax:binary([erl_syntax:binary_field(erl_syntax:integer(X)) || X <- String]).
 
 
-string_ast(String, #dtl_context{ binary_strings = true }, TreeWalker) ->
+string_ast(String, #dtl_context{ binary_strings = true }, TreeWalker) when is_list(String) ->
     {{binary_string(String), #ast_info{}}, TreeWalker};
     {{binary_string(String), #ast_info{}}, TreeWalker};
-string_ast(String, #dtl_context{ binary_strings = false }, TreeWalker) ->
-    {{erl_syntax:string(String), #ast_info{}}, TreeWalker}. %% less verbose AST, better for development and debugging
+string_ast(String, #dtl_context{ binary_strings = false }, TreeWalker) when is_list(String) ->
+    {{erl_syntax:string(String), #ast_info{}}, TreeWalker}; %% less verbose AST, better for development and debugging
+string_ast(S, Context, TreeWalker) when is_atom(S) ->
+    string_ast(atom_to_list(S), Context, TreeWalker).
 
 
 
 
 include_ast(File, ArgList, Scopes, Context, TreeWalker) ->
 include_ast(File, ArgList, Scopes, Context, TreeWalker) ->
@@ -1260,26 +1259,33 @@ tag_ast(Name, Args, Context, TreeWalker) ->
 
 
 custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = false }) ->
 custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = false }) ->
     {erl_syntax:application(none, erl_syntax:atom(render_tag),
     {erl_syntax:application(none, erl_syntax:atom(render_tag),
-            [erl_syntax:string(Name), erl_syntax:list(InterpretedArgs), erl_syntax:variable("_CustomTagsContext")]),
+            [key_to_string(Name), erl_syntax:list(InterpretedArgs),
+             erl_syntax:variable("_CustomTagOptions")]),
         #ast_info{custom_tags = [Name]}};
         #ast_info{custom_tags = [Name]}};
 custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = true, module = Module }) ->
 custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = true, module = Module }) ->
     {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
     {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
-            [erl_syntax:list(InterpretedArgs), erl_syntax:variable("_CustomTagsContext")]), #ast_info{ custom_tags = [Name] }};
+            [erl_syntax:list(InterpretedArgs), erl_syntax:variable("_CustomTagOptions")]),
+             #ast_info{ custom_tags = [Name] }};
 custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [Module|Rest] } = Context) ->
 custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [Module|Rest] } = Context) ->
-    case lists:member({Name, 2}, Module:module_info(exports)) of
-        true ->
+    try lists:max([I || {N,I} <- Module:module_info(exports), N =:= Name]) of
+        2 ->
             {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
             {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
-                    [erl_syntax:list(InterpretedArgs), erl_syntax:variable("_CustomTagsContext")]), #ast_info{}};
-        false ->
-            case lists:member({Name, 1}, Module:module_info(exports)) of
-                true ->
-                    {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
-                            [erl_syntax:list(InterpretedArgs)]), #ast_info{}};
-                false ->
-                    custom_tags_modules_ast(Name, InterpretedArgs, Context#dtl_context{ custom_tags_modules = Rest })
-            end
+                [erl_syntax:list(InterpretedArgs),
+                 erl_syntax:variable("_CustomTagOptions")]), #ast_info{}};
+        1 ->
+            {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
+                [erl_syntax:list(InterpretedArgs)]), #ast_info{}};
+        I ->
+            throw({unsupported_custom_tag_fun, {Module, Name, I}})
+    catch _:function_clause ->
+        custom_tags_modules_ast(Name, InterpretedArgs, Context#dtl_context{ custom_tags_modules = Rest })
     end.
     end.
 
 
+print(true, Fmt, Args) ->
+    io:format(Fmt, Args);
+print(_, _Fmt, _Args) ->
+    ok.
+
 options_ast() ->
 options_ast() ->
     erl_syntax:list([
     erl_syntax:list([
             erl_syntax:tuple([erl_syntax:atom(translation_fun), erl_syntax:variable("_TranslationFun")]),
             erl_syntax:tuple([erl_syntax:atom(translation_fun), erl_syntax:variable("_TranslationFun")]),

+ 1 - 1
src/erlydtl_runtime.erl

@@ -192,7 +192,7 @@ stringify_final([], Out, _) ->
 stringify_final([El | Rest], Out, false = BinaryStrings) when is_atom(El) ->
 stringify_final([El | Rest], Out, false = BinaryStrings) when is_atom(El) ->
     stringify_final(Rest, [atom_to_list(El) | Out], BinaryStrings);
     stringify_final(Rest, [atom_to_list(El) | Out], BinaryStrings);
 stringify_final([El | Rest], Out, true = BinaryStrings) when is_atom(El) ->
 stringify_final([El | Rest], Out, true = BinaryStrings) when is_atom(El) ->
-    stringify_final(Rest, [list_to_binary(atom_to_list(El)) | Out], BinaryStrings);
+    stringify_final(Rest, [atom_to_binary(El, latin1) | Out], BinaryStrings);
 stringify_final([El | Rest], Out, BinaryStrings) when is_list(El) ->
 stringify_final([El | Rest], Out, BinaryStrings) when is_list(El) ->
     stringify_final(Rest, [stringify_final(El, BinaryStrings) | Out], BinaryStrings);
     stringify_final(Rest, [stringify_final(El, BinaryStrings) | Out], BinaryStrings);
 stringify_final([El | Rest], Out, false = BinaryStrings) when is_tuple(El) ->
 stringify_final([El | Rest], Out, false = BinaryStrings) when is_tuple(El) ->

+ 1 - 0
tests/input/custom_tag1

@@ -0,0 +1 @@
+{% custom1 %}

+ 1 - 0
tests/input/custom_tag2

@@ -0,0 +1 @@
+{% custom2 %}

+ 1 - 0
tests/input/custom_tag3

@@ -0,0 +1 @@
+{% custom3 %}

+ 13 - 0
tests/src/erlydtl_custom_tags.erl

@@ -0,0 +1,13 @@
+-module(erlydtl_custom_tags).
+
+-export([custom1/1, custom2/2, custom3/2]).
+
+custom1(_TagVars = []) ->
+    <<"b1">>.
+
+custom2([], _CustomTagsContext = ctx) ->
+    <<"b2">>.
+
+custom3([], _RenderOptions = [{locale, ru}]) ->
+    <<"b3">>.
+

+ 39 - 14
tests/src/erlydtl_functional_tests.erl

@@ -44,7 +44,9 @@ test_list() ->
         "for_tuple", "for_list_preset", "for_preset", "for_records",
         "for_tuple", "for_list_preset", "for_preset", "for_records",
         "for_records_preset", "include", "if", "if_preset", "ifequal",
         "for_records_preset", "include", "if", "if_preset", "ifequal",
         "ifequal_preset", "ifnotequal", "ifnotequal_preset", "now",
         "ifequal_preset", "ifnotequal", "ifnotequal_preset", "now",
-        "var", "var_preset", "cycle", "custom_tag", "custom_call", 
+        "var", "var_preset", "cycle",
+        "custom_tag", "custom_tag1", "custom_tag2", "custom_tag3",
+        "custom_call", 
         "include_template", "include_path", "ssi",
         "include_template", "include_path", "ssi",
         "extends_path", "extends_path2", "trans" ].
         "extends_path", "extends_path2", "trans" ].
 
 
@@ -154,6 +156,14 @@ setup("extends_path2") ->
 setup("trans") ->
 setup("trans") ->
     RenderVars = [{locale, "reverse"}],
     RenderVars = [{locale, "reverse"}],
     {ok, RenderVars};
     {ok, RenderVars};
+setup("locale") ->
+    {ok, _RenderVars = [{locale, "ru"}]};
+setup("custom_tag1") ->
+    {ok, [{a, <<"a1">>}], [{locale, ru}, {custom_tags_context, ctx}], [<<"b1">>, <<"\n">>]};
+setup("custom_tag2") ->
+    {ok, [{a, <<"a1">>}], [{locale, ru}, {custom_tags_context, ctx}], [<<"b2">>, <<"\n">>]};
+setup("custom_tag3") ->
+    {ok, [{a, <<"a1">>}], [{locale, ru}], [<<"b3">>, <<"\n">>]};
 setup("ssi") ->
 setup("ssi") ->
     RenderVars = [{path, filename:absname(filename:join(["tests", "input", "ssi_include.html"]))}],
     RenderVars = [{path, filename:absname(filename:join(["tests", "input", "ssi_include.html"]))}],
     {ok, RenderVars};
     {ok, RenderVars};
@@ -215,7 +225,8 @@ test_compile_render(Name) ->
         {CompileStatus, CompileVars} ->
         {CompileStatus, CompileVars} ->
             Options = [
             Options = [
                 {vars, CompileVars}, 
                 {vars, CompileVars}, 
-                {force_recompile, true}],
+                {force_recompile, true},
+                {custom_tags_modules, [erlydtl_custom_tags]}],
             io:format(" Template: ~p, ... compiling ... ", [Name]),
             io:format(" Template: ~p, ... compiling ... ", [Name]),
             case erlydtl:compile(File, Module, Options) of
             case erlydtl:compile(File, Module, Options) of
                 ok ->
                 ok ->
@@ -242,22 +253,36 @@ test_compile_render(Name) ->
 
 
 test_render(Name, Module) ->
 test_render(Name, Module) ->
     File = filename:join([templates_docroot(), Name]),
     File = filename:join([templates_docroot(), Name]),
-    {RenderStatus, Vars} = setup(Name),
-    case catch Module:render(Vars) of
+    {RenderStatus, Vars, Opts, RenderResult} =
+        case setup(Name) of
+            {RS, V}       -> {RS, V, [], undefined};
+            {RS, V, O}    -> {RS, V, O, undefined};
+            {RS, V, O, R} -> {RS, V, O, R}
+        end,
+    case catch Module:render(Vars, Opts) of
         {ok, Data} ->
         {ok, Data} ->
             io:format("rendering~n"), 
             io:format("rendering~n"), 
             case RenderStatus of
             case RenderStatus of
                 ok ->
                 ok ->
-                    {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
-                    end;
+                    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
+                            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}
                     {error, "rendering should have failed :" ++ File}
             end;
             end;