Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
@saleyn saleyn authored
View
19 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
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
filters. The modules will be searched in order and take precedence over the
View
122 src/erlydtl_compiler.erl
@@ -58,6 +58,7 @@
binary_strings = true,
force_recompile = false,
locale = none,
+ verbose = false,
is_compiling_dir = false}).
-record(ast_info, {
@@ -160,19 +161,17 @@ write_binary(Module1, Bin, Options, Warnings) ->
Verbose = proplists:get_value(verbose, Options, false),
case proplists:get_value(out_dir, Options) of
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;
OutDir ->
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])
- end]),
+ end]),
case file:write_file(BeamFile, Bin) of
ok ->
@@ -235,14 +234,12 @@ load_code(Module, Bin, Warnings) ->
_ -> {error, lists:concat(["code reload failed: ", Module])}
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{},
#dtl_context{
- parse_trail = [File],
+ parse_trail = ParseTrail,
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],
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),
@@ -254,29 +251,18 @@ init_dtl_context(File, Module, 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 = 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, 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),
- 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}) ->
false;
@@ -451,10 +437,7 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
erl_syntax:atom(proplists),
erl_syntax:atom(get_value),
[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,
[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),
- 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(
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)]),
@@ -825,10 +822,12 @@ widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalker) ->
binary_string(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};
-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) ->
@@ -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 }) ->
{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]}};
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: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) ->
- 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: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.
+print(true, Fmt, Args) ->
+ io:format(Fmt, Args);
+print(_, _Fmt, _Args) ->
+ ok.
+
options_ast() ->
erl_syntax:list([
erl_syntax:tuple([erl_syntax:atom(translation_fun), erl_syntax:variable("_TranslationFun")]),
View
2  src/erlydtl_runtime.erl
@@ -192,7 +192,7 @@ stringify_final([], Out, _) ->
stringify_final([El | Rest], Out, false = BinaryStrings) when is_atom(El) ->
stringify_final(Rest, [atom_to_list(El) | Out], BinaryStrings);
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(Rest, [stringify_final(El, BinaryStrings) | Out], BinaryStrings);
stringify_final([El | Rest], Out, false = BinaryStrings) when is_tuple(El) ->
View
1  tests/input/custom_tag1
@@ -0,0 +1 @@
+{% custom1 %}
View
1  tests/input/custom_tag2
@@ -0,0 +1 @@
+{% custom2 %}
View
1  tests/input/custom_tag3
@@ -0,0 +1 @@
+{% custom3 %}
View
13 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">>.
+
View
53 tests/src/erlydtl_functional_tests.erl
@@ -44,7 +44,9 @@ test_list() ->
"for_tuple", "for_list_preset", "for_preset", "for_records",
"for_records_preset", "include", "if", "if_preset", "ifequal",
"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",
"extends_path", "extends_path2", "trans" ].
@@ -154,6 +156,14 @@ setup("extends_path2") ->
setup("trans") ->
RenderVars = [{locale, "reverse"}],
{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") ->
RenderVars = [{path, filename:absname(filename:join(["tests", "input", "ssi_include.html"]))}],
{ok, RenderVars};
@@ -215,7 +225,8 @@ test_compile_render(Name) ->
{CompileStatus, CompileVars} ->
Options = [
{vars, CompileVars},
- {force_recompile, true}],
+ {force_recompile, true},
+ {custom_tags_modules, [erlydtl_custom_tags]}],
io:format(" Template: ~p, ... compiling ... ", [Name]),
case erlydtl:compile(File, Module, Options) of
ok ->
@@ -242,22 +253,36 @@ test_compile_render(Name) ->
test_render(Name, Module) ->
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} ->
io:format("rendering~n"),
case RenderStatus of
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}
end;
Please sign in to comment.
Something went wrong with that request. Please try again.