Skip to content
This repository
Browse code

Add support for custom tags having access to extra data

The following arguments are conditionally exposed to custom tags:
* RenderVariables
* Locale
* TranslationFun
  • Loading branch information...
commit 6e7bb36678986fbb0efd8c5fe1f31de9ab01fb45 1 parent 2ef2f7d
Serge Aleynikov authored October 24, 2012
19  README.markdown
Source Rendered
@@ -53,12 +53,19 @@ will evaluate to `<b>100</b>`. Get it?
53 53
 
54 54
 * `custom_tags_modules` - A list of modules to be used for handling custom
55 55
 tags. The modules will be searched in order and take precedence over
56  
-`custom_tags_dir`. Each custom tag should correspond to an exported function,
57  
-e.g.: 
58  
-
59  
-    some_tag(Variables, Context) -> iolist()
60  
-
61  
-The `Context` is specified at render-time with the `custom_tags_context` option.
  56
+`custom_tags_dir`. Each custom tag should correspond to an exported function
  57
+with one of the following signatures: 
  58
+
  59
+    some_tag(TagVars)          -> iolist()
  60
+    some_tag(TagVars, Options) -> iolist()
  61
+
  62
+The `TagVars` are variables provided to a custom tag in the template's body
  63
+(e.g. `{% foo bar=100 %}` results in `TagVars = [{"bar", 100}]`).
  64
+The `Options` are options passed as the second argument to the `render/2` call
  65
+at render-time.  For backward compatibility, if render `Options` include
  66
+a `custom_tags_context` option, its value will be passed as `Options` to the
  67
+custom tag handling function. Note that this backward-compatibility functionality
  68
+will be deprecated in one of the next releases.
62 69
 
63 70
 * `custom_filters_modules` - A list of modules to be used for handling custom
64 71
 filters. The modules will be searched in order and take precedence over the
122  src/erlydtl_compiler.erl
@@ -58,6 +58,7 @@
58 58
     binary_strings = true,
59 59
     force_recompile = false,
60 60
     locale = none,
  61
+    verbose = false,
61 62
     is_compiling_dir = false}).
62 63
 
63 64
 -record(ast_info, {
@@ -160,19 +161,17 @@ write_binary(Module1, Bin, Options, Warnings) ->
160 161
     Verbose = proplists:get_value(verbose, Options, false),
161 162
     case proplists:get_value(out_dir, Options) of
162 163
         undefined ->
163  
-            Verbose =:= true andalso
164  
-                io:format("Template module: ~w not saved (no out_dir option)\n", [Module1]),
  164
+            print(Verbose, "Template module: ~w not saved (no out_dir option)\n", [Module1]),
165 165
             ok;
166 166
         OutDir ->
167 167
             BeamFile = filename:join([OutDir, atom_to_list(Module1) ++ ".beam"]),
168 168
 
169  
-            Verbose =:= true andalso
170  
-                io:format("Template module: ~w -> ~s~s\n",
171  
-                    [Module1, BeamFile,
172  
-                        case Warnings of
  169
+            print(Verbose, "Template module: ~w -> ~s~s\n",
  170
+                [Module1, BeamFile,
  171
+                    case Warnings of
173 172
                         [] -> "";
174 173
                         _  -> io_lib:format("\n  Warnings: ~p", [Warnings])
175  
-                        end]),
  174
+                    end]),
176 175
 
177 176
             case file:write_file(BeamFile, Bin) of
178 177
                 ok ->
@@ -235,14 +234,12 @@ load_code(Module, Bin, Warnings) ->
235 234
         _ -> {error, lists:concat(["code reload failed: ", Module])}
236 235
     end.
237 236
 
238  
-init_dtl_context(File, Module, Options) when is_list(Module) ->
239  
-    init_dtl_context(File, list_to_atom(Module), Options);
240  
-init_dtl_context(File, Module, Options) ->
  237
+init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) ->
241 238
     Ctx = #dtl_context{},
242 239
     #dtl_context{
243  
-        parse_trail = [File], 
  240
+        parse_trail = ParseTrail,
244 241
         module = Module,
245  
-        doc_root = proplists:get_value(doc_root, Options, filename:dirname(File)),
  242
+        doc_root = proplists:get_value(doc_root, Options, DefDir),
246 243
         filter_modules = proplists:get_value(custom_filters_modules, Options, Ctx#dtl_context.filter_modules) ++ [erlydtl_filters],
247 244
         custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
248 245
         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) ->
254 251
         binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
255 252
         force_recompile = proplists:get_value(force_recompile, Options, Ctx#dtl_context.force_recompile),
256 253
         locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale),
257  
-        is_compiling_dir = false}.
  254
+        verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose),
  255
+        is_compiling_dir = IsCompilingDir}.
  256
+
  257
+init_dtl_context(File, Module, Options) when is_list(Module) ->
  258
+    init_dtl_context(File, list_to_atom(Module), Options);
  259
+init_dtl_context(File, Module, Options) ->
  260
+    init_context(false, [File], filename:dirname(File), Module, Options).
258 261
 
259 262
 init_dtl_context_dir(Dir, Module, Options) when is_list(Module) ->
260 263
     init_dtl_context_dir(Dir, list_to_atom(Module), Options);
261 264
 init_dtl_context_dir(Dir, Module, Options) ->
262  
-    Ctx = #dtl_context{},
263  
-    #dtl_context{
264  
-        parse_trail = [], 
265  
-        module = Module,
266  
-        doc_root = proplists:get_value(doc_root, Options, Dir),
267  
-        filter_modules = proplists:get_value(custom_filters_modules, Options, Ctx#dtl_context.filter_modules) ++ [erlydtl_filters],
268  
-        custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
269  
-        custom_tags_modules = proplists:get_value(custom_tags_modules, Options, Ctx#dtl_context.custom_tags_modules),
270  
-        blocktrans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.blocktrans_fun),
271  
-        blocktrans_locales = proplists:get_value(blocktrans_locales, Options, Ctx#dtl_context.blocktrans_locales),
272  
-        vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars), 
273  
-        reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
274  
-        compiler_options = proplists:get_value(compiler_options, Options, Ctx#dtl_context.compiler_options),
275  
-        binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
276  
-        force_recompile = proplists:get_value(force_recompile, Options, Ctx#dtl_context.force_recompile),
277  
-        locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale),
278  
-        is_compiling_dir = true}.
279  
-
  265
+    init_context(true, [], Dir, Module, Options).
280 266
 
281 267
 is_up_to_date(_, #dtl_context{force_recompile = true}) ->
282 268
     false;
@@ -451,10 +437,7 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
451 437
                 erl_syntax:atom(proplists),
452 438
                 erl_syntax:atom(get_value),
453 439
                 [erl_syntax:atom(locale), erl_syntax:variable("Options"), erl_syntax:atom(none)]),
454  
-            erl_syntax:application(
455  
-                erl_syntax:atom(proplists),
456  
-                erl_syntax:atom(get_value),
457  
-                [erl_syntax:atom(custom_tags_context), erl_syntax:variable("Options"), erl_syntax:atom(none)])
  440
+            erl_syntax:variable("Options")
458 441
         ]),
459 442
     ClauseOk = erl_syntax:clause([erl_syntax:variable("Val")], none,
460 443
         [erl_syntax:tuple([erl_syntax:atom(ok), erl_syntax:variable("Val")])]),     
@@ -479,16 +462,30 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
479 462
 
480 463
     VariablesAst = variables_function(MergedInfo#ast_info.var_names),
481 464
 
482  
-    BodyAstTmp = erl_syntax:application(
483  
-                    erl_syntax:atom(erlydtl_runtime),
484  
-                    erl_syntax:atom(stringify_final),
485  
-                    [BodyAst, erl_syntax:atom(BinaryStrings)]),
  465
+    BodyAstTmp = [
  466
+        erl_syntax:match_expr(
  467
+            erl_syntax:variable("_CustomTagOptions"),
  468
+            erl_syntax:application(
  469
+                erl_syntax:atom(proplists),
  470
+                erl_syntax:atom(get_value),
  471
+                [erl_syntax:atom(custom_tags_context),
  472
+                 erl_syntax:variable("RenderOptions"),
  473
+                 erl_syntax:variable("RenderOptions")])),
  474
+        erl_syntax:application(
  475
+            erl_syntax:atom(erlydtl_runtime),
  476
+            erl_syntax:atom(stringify_final),
  477
+            [BodyAst, erl_syntax:atom(BinaryStrings)])
  478
+    ],
486 479
 
487 480
     RenderInternalFunctionAst = erl_syntax:function(
488 481
         erl_syntax:atom(render_internal), 
489  
-        [erl_syntax:clause([erl_syntax:variable("_Variables"), erl_syntax:variable("_TranslationFun"), 
490  
-                    erl_syntax:variable("_CurrentLocale"), erl_syntax:variable("_CustomTagsContext")], none, 
491  
-                [BodyAstTmp])]),   
  482
+        [erl_syntax:clause([
  483
+            erl_syntax:variable("_Variables"),
  484
+            erl_syntax:variable("_TranslationFun"), 
  485
+            erl_syntax:variable("_CurrentLocale"),
  486
+            erl_syntax:variable("RenderOptions")],
  487
+            none, BodyAstTmp)]
  488
+    ),   
492 489
     
493 490
     ModuleAst  = erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(Module)]),
494 491
     
@@ -825,10 +822,12 @@ widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalker) ->
825 822
 binary_string(String) ->
826 823
     erl_syntax:binary([erl_syntax:binary_field(erl_syntax:integer(X)) || X <- String]).
827 824
 
828  
-string_ast(String, #dtl_context{ binary_strings = true }, TreeWalker) ->
  825
+string_ast(String, #dtl_context{ binary_strings = true }, TreeWalker) when is_list(String) ->
829 826
     {{binary_string(String), #ast_info{}}, TreeWalker};
830  
-string_ast(String, #dtl_context{ binary_strings = false }, TreeWalker) ->
831  
-    {{erl_syntax:string(String), #ast_info{}}, TreeWalker}. %% less verbose AST, better for development and debugging
  827
+string_ast(String, #dtl_context{ binary_strings = false }, TreeWalker) when is_list(String) ->
  828
+    {{erl_syntax:string(String), #ast_info{}}, TreeWalker}; %% less verbose AST, better for development and debugging
  829
+string_ast(S, Context, TreeWalker) when is_atom(S) ->
  830
+    string_ast(atom_to_list(S), Context, TreeWalker).
832 831
 
833 832
 
834 833
 include_ast(File, ArgList, Scopes, Context, TreeWalker) ->
@@ -1260,26 +1259,33 @@ tag_ast(Name, Args, Context, TreeWalker) ->
1260 1259
 
1261 1260
 custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = false }) ->
1262 1261
     {erl_syntax:application(none, erl_syntax:atom(render_tag),
1263  
-            [erl_syntax:string(Name), erl_syntax:list(InterpretedArgs), erl_syntax:variable("_CustomTagsContext")]),
  1262
+            [key_to_string(Name), erl_syntax:list(InterpretedArgs),
  1263
+             erl_syntax:variable("_CustomTagOptions")]),
1264 1264
         #ast_info{custom_tags = [Name]}};
1265 1265
 custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = true, module = Module }) ->
1266 1266
     {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
1267  
-            [erl_syntax:list(InterpretedArgs), erl_syntax:variable("_CustomTagsContext")]), #ast_info{ custom_tags = [Name] }};
  1267
+            [erl_syntax:list(InterpretedArgs), erl_syntax:variable("_CustomTagOptions")]),
  1268
+             #ast_info{ custom_tags = [Name] }};
1268 1269
 custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [Module|Rest] } = Context) ->
1269  
-    case lists:member({Name, 2}, Module:module_info(exports)) of
1270  
-        true ->
  1270
+    try lists:max([I || {N,I} <- Module:module_info(exports), N =:= Name]) of
  1271
+        2 ->
1271 1272
             {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
1272  
-                    [erl_syntax:list(InterpretedArgs), erl_syntax:variable("_CustomTagsContext")]), #ast_info{}};
1273  
-        false ->
1274  
-            case lists:member({Name, 1}, Module:module_info(exports)) of
1275  
-                true ->
1276  
-                    {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
1277  
-                            [erl_syntax:list(InterpretedArgs)]), #ast_info{}};
1278  
-                false ->
1279  
-                    custom_tags_modules_ast(Name, InterpretedArgs, Context#dtl_context{ custom_tags_modules = Rest })
1280  
-            end
  1273
+                [erl_syntax:list(InterpretedArgs),
  1274
+                 erl_syntax:variable("_CustomTagOptions")]), #ast_info{}};
  1275
+        1 ->
  1276
+            {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name),
  1277
+                [erl_syntax:list(InterpretedArgs)]), #ast_info{}};
  1278
+        I ->
  1279
+            throw({unsupported_custom_tag_fun, {Module, Name, I}})
  1280
+    catch _:function_clause ->
  1281
+        custom_tags_modules_ast(Name, InterpretedArgs, Context#dtl_context{ custom_tags_modules = Rest })
1281 1282
     end.
1282 1283
 
  1284
+print(true, Fmt, Args) ->
  1285
+    io:format(Fmt, Args);
  1286
+print(_, _Fmt, _Args) ->
  1287
+    ok.
  1288
+
1283 1289
 options_ast() ->
1284 1290
     erl_syntax:list([
1285 1291
             erl_syntax:tuple([erl_syntax:atom(translation_fun), erl_syntax:variable("_TranslationFun")]),
2  src/erlydtl_runtime.erl
@@ -192,7 +192,7 @@ stringify_final([], Out, _) ->
192 192
 stringify_final([El | Rest], Out, false = BinaryStrings) when is_atom(El) ->
193 193
     stringify_final(Rest, [atom_to_list(El) | Out], BinaryStrings);
194 194
 stringify_final([El | Rest], Out, true = BinaryStrings) when is_atom(El) ->
195  
-    stringify_final(Rest, [list_to_binary(atom_to_list(El)) | Out], BinaryStrings);
  195
+    stringify_final(Rest, [atom_to_binary(El, latin1) | Out], BinaryStrings);
196 196
 stringify_final([El | Rest], Out, BinaryStrings) when is_list(El) ->
197 197
     stringify_final(Rest, [stringify_final(El, BinaryStrings) | Out], BinaryStrings);
198 198
 stringify_final([El | Rest], Out, false = BinaryStrings) when is_tuple(El) ->
1  tests/input/custom_tag1
... ...
@@ -0,0 +1 @@
  1
+{% custom1 %}
1  tests/input/custom_tag2
... ...
@@ -0,0 +1 @@
  1
+{% custom2 %}
1  tests/input/custom_tag3
... ...
@@ -0,0 +1 @@
  1
+{% custom3 %}
13  tests/src/erlydtl_custom_tags.erl
... ...
@@ -0,0 +1,13 @@
  1
+-module(erlydtl_custom_tags).
  2
+
  3
+-export([custom1/1, custom2/2, custom3/2]).
  4
+
  5
+custom1(_TagVars = []) ->
  6
+    <<"b1">>.
  7
+
  8
+custom2([], _CustomTagsContext = ctx) ->
  9
+    <<"b2">>.
  10
+
  11
+custom3([], _RenderOptions = [{locale, ru}]) ->
  12
+    <<"b3">>.
  13
+
53  tests/src/erlydtl_functional_tests.erl
@@ -44,7 +44,9 @@ test_list() ->
44 44
         "for_tuple", "for_list_preset", "for_preset", "for_records",
45 45
         "for_records_preset", "include", "if", "if_preset", "ifequal",
46 46
         "ifequal_preset", "ifnotequal", "ifnotequal_preset", "now",
47  
-        "var", "var_preset", "cycle", "custom_tag", "custom_call", 
  47
+        "var", "var_preset", "cycle",
  48
+        "custom_tag", "custom_tag1", "custom_tag2", "custom_tag3",
  49
+        "custom_call", 
48 50
         "include_template", "include_path", "ssi",
49 51
         "extends_path", "extends_path2", "trans" ].
50 52
 
@@ -154,6 +156,14 @@ setup("extends_path2") ->
154 156
 setup("trans") ->
155 157
     RenderVars = [{locale, "reverse"}],
156 158
     {ok, RenderVars};
  159
+setup("locale") ->
  160
+    {ok, _RenderVars = [{locale, "ru"}]};
  161
+setup("custom_tag1") ->
  162
+    {ok, [{a, <<"a1">>}], [{locale, ru}, {custom_tags_context, ctx}], [<<"b1">>, <<"\n">>]};
  163
+setup("custom_tag2") ->
  164
+    {ok, [{a, <<"a1">>}], [{locale, ru}, {custom_tags_context, ctx}], [<<"b2">>, <<"\n">>]};
  165
+setup("custom_tag3") ->
  166
+    {ok, [{a, <<"a1">>}], [{locale, ru}], [<<"b3">>, <<"\n">>]};
157 167
 setup("ssi") ->
158 168
     RenderVars = [{path, filename:absname(filename:join(["tests", "input", "ssi_include.html"]))}],
159 169
     {ok, RenderVars};
@@ -215,7 +225,8 @@ test_compile_render(Name) ->
215 225
         {CompileStatus, CompileVars} ->
216 226
             Options = [
217 227
                 {vars, CompileVars}, 
218  
-                {force_recompile, true}],
  228
+                {force_recompile, true},
  229
+                {custom_tags_modules, [erlydtl_custom_tags]}],
219 230
             io:format(" Template: ~p, ... compiling ... ", [Name]),
220 231
             case erlydtl:compile(File, Module, Options) of
221 232
                 ok ->
@@ -242,22 +253,36 @@ test_compile_render(Name) ->
242 253
 
243 254
 test_render(Name, Module) ->
244 255
     File = filename:join([templates_docroot(), Name]),
245  
-    {RenderStatus, Vars} = setup(Name),
246  
-    case catch Module:render(Vars) of
  256
+    {RenderStatus, Vars, Opts, RenderResult} =
  257
+        case setup(Name) of
  258
+            {RS, V}       -> {RS, V, [], undefined};
  259
+            {RS, V, O}    -> {RS, V, O, undefined};
  260
+            {RS, V, O, R} -> {RS, V, O, R}
  261
+        end,
  262
+    case catch Module:render(Vars, Opts) of
247 263
         {ok, Data} ->
248 264
             io:format("rendering~n"), 
249 265
             case RenderStatus of
250 266
                 ok ->
251  
-                    {File, _} = Module:source(),
252  
-                    OutFile = filename:join([templates_outdir(), filename:basename(File)]),
253  
-                    case file:open(OutFile, [write]) of
254  
-                        {ok, IoDev} ->
255  
-                            file:write(IoDev, Data),
256  
-                            file:close(IoDev),
257  
-                            ok;    
258  
-                        Err ->
259  
-                            Err
260  
-                    end;
  267
+                    case RenderResult of
  268
+                        undefined ->
  269
+                            {File, _} = Module:source(),
  270
+                            OutFile = filename:join([templates_outdir(), filename:basename(File)]),
  271
+                            case file:open(OutFile, [write]) of
  272
+                                {ok, IoDev} ->
  273
+                                    file:write(IoDev, Data),
  274
+                                    file:close(IoDev),
  275
+                                    ok;
  276
+                                Err ->
  277
+                                    Err
  278
+                            end;
  279
+                        _ when Data =:= RenderResult ->
  280
+                            ok;
  281
+                        _ ->
  282
+                            {error, lists:flatten(io_lib:format("Test ~s failed\n"
  283
+                                "Expected: ~p\n"
  284
+                                "Value:    ~p\n", [Name, RenderResult, Data]))}
  285
+                        end;
261 286
                 _ ->
262 287
                     {error, "rendering should have failed :" ++ File}
263 288
             end;

0 notes on commit 6e7bb36

Please sign in to comment.
Something went wrong with that request. Please try again.