Skip to content
Browse files

Add erlydtl_library behaviour with accompanying options (fixes #137)

New options are libraries and default_libraries.
Deprecated but still supported options are custom_filters_modules
and custom_tags_modules.

Note, that the built in filters are still in the old format,
not yet migrated to the new library behaviour.
  • Loading branch information...
1 parent 274bd52 commit 7bfc99ed8906bcff6a72cf5f3552faf3ce0deffa @kaos kaos committed Feb 27, 2014
Showing with 188 additions and 83 deletions.
  1. +22 −12 README.markdown
  2. +3 −2 include/erlydtl_ext.hrl
  3. +35 −35 src/erlydtl_beam_compiler.erl
  4. +55 −34 src/erlydtl_compiler.erl
  5. +28 −0 src/erlydtl_compiler_utils.erl
  6. +45 −0 src/erlydtl_library.erl
View
34 README.markdown
@@ -58,14 +58,24 @@ Options is a proplist possibly containing:
* `doc_root` - Included template paths will be relative to this
directory; defaults to the compiled template's directory.
+* `libraries` - A list of `{Name, Module}` libraries implementing
+ custom tags and filters. `Module` should implement the
+ `erlydtl_library` behaviour.
+
+* `default_libraries` - A list of libraries that should be loaded by
+ default when compiling a template. Libraries can be specified either
+ by name (when there is a name to module mapping also provided in the
+ `libraries` option) or by module.
+
* `custom_tags_dir` - Directory of DTL files (no extension) includable
- as tags. E.g. if $custom_tags_dir/foo contains `<b>{{ bar }}</b>`,
- then `{% foo bar=100 %}` will evaluate to `<b>100</b>`. Get it?
+ as tags. E.g. if `$custom_tags_dir/foo` contains `<b>{{ bar }}</b>`,
+ then `{% foo bar=100 %}` will evaluate to `<b>100</b>`.
-* `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 with one of the following signatures:
+* `custom_tags_modules` **deprecated** - 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 with one of the following
+ signatures:
some_tag(TagVars) -> iolist()
some_tag(TagVars, Options) -> iolist()
@@ -76,15 +86,15 @@ Options is a proplist possibly containing:
argument to the `render/2` call at render-time. (These may include
any options, not just `locale` and `translation_fun`.)
-* `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 built-in filters. Each custom filter should
- correspond to an exported filter, e.g.
+* `custom_filters_modules` **deprecated** - A list of modules to be
+ used for handling custom filters. The modules will be searched in
+ order and take precedence over the built-in filters. Each custom
+ filter should correspond to an exported filter, e.g.
some_filter(Value) -> iolist()
- If the filter takes an argument (e.g. "foo:2"), the argument will be
- also be passed in:
+ If the filter takes any arguments (e.g. "foo:2"), those will be
+ added to the call:
some_filter(Value, Arg) -> iolist()
View
5 include/erlydtl_ext.hrl
@@ -15,9 +15,10 @@
parse_trail = [],
vars = [],
record_info = [],
- filter_modules = [],
+ filters = [],
+ tags = [],
+ libraries = [],
custom_tags_dir = [],
- custom_tags_modules = [],
reader = {file, read_file},
module = undefined,
compiler_options = [],
View
70 src/erlydtl_beam_compiler.erl
@@ -1012,15 +1012,18 @@ filter_ast1({{identifier, _, Name}, Args}, ValueAst, TreeWalker) ->
FilterAst = filter_ast2(Name, [ValueAst|ArgsAst], TreeWalker2#treewalker.context),
{{FilterAst, ArgsInfo}, TreeWalker2}.
-filter_ast2(Name, Args, #dtl_context{ filter_modules = [Module|Rest] } = Context) ->
- case lists:member({Name, length(Args)}, Module:module_info(exports)) of
- true -> ?Q("'@Module@':'@Name@'(_@Args)");
- false ->
- filter_ast2(Name, Args, Context#dtl_context{ filter_modules = Rest })
- end;
-filter_ast2(Name, Args, _) ->
- %% TODO: when we don't throw errors, this could be a warning..
- throw({unknown_filter, Name, length(Args)}).
+filter_ast2(Name, Args, #dtl_context{ filters = Filters }) ->
+ case proplists:get_value(Name, Filters) of
+ {Mod, Fun}=Filter ->
+ case erlang:function_exported(Mod, Fun, length(Args)) of
+ true -> ?Q("'@Mod@':'@Fun@'(_@Args)");
+ false ->
+ throw({filter_args, Name, Filter, Args})
+ end;
+ undefined ->
+ %% TODO: when we don't throw errors, this could be a warning..
+ throw({unknown_filter, Name, length(Args)})
+ end.
search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) ->
search_for_safe_filter(Variable, Filter);
@@ -1343,32 +1346,29 @@ tag_ast(Name, Args, TreeWalker) ->
custom_tags_modules_ast(Name, InterpretedArgs,
#dtl_context{
- custom_tags_modules = [],
- is_compiling_dir = false }) ->
- {?Q("render_tag(_@Name@, [_@InterpretedArgs], RenderOptions)"),
- #ast_info{custom_tags = [Name]}};
-custom_tags_modules_ast(Name, InterpretedArgs,
- #dtl_context{
- custom_tags_modules = [],
- is_compiling_dir = true,
- module = Module }) ->
- {?Q("'@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)"),
- #ast_info{ custom_tags = [Name] }};
-custom_tags_modules_ast(Name, InterpretedArgs,
- #dtl_context{
- custom_tags_modules = [Module|Rest]
- } = Context) ->
- try lists:max([I || {N,I} <- Module:module_info(exports), N =:= Name]) of
- 2 ->
- {?Q("'@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)"), #ast_info{}};
- 1 ->
- {?Q("'@Module@':'@Name@'([_@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 })
+ tags = Tags,
+ module = Module,
+ is_compiling_dir=IsCompilingDir }) ->
+ case proplists:get_value(Name, Tags) of
+ {Mod, Fun}=Tag ->
+ case lists:max([0] ++ [I || {N,I} <- Mod:module_info(exports), N =:= Fun]) of
+ 2 ->
+ {?Q("'@Mod@':'@Fun@'([_@InterpretedArgs], RenderOptions)"), #ast_info{}};
+ 1 ->
+ {?Q("'@Mod@':'@Fun@'([_@InterpretedArgs])"), #ast_info{}};
+ 0 ->
+ throw({custom_tag_not_exported, Name, Tag});
+ I ->
+ throw({unsupported_custom_tag_fun, {Module, Name, I}})
+ end;
+ undefined ->
+ if IsCompilingDir ->
+ {?Q("'@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)"),
+ #ast_info{ custom_tags = [Name] }};
+ true ->
+ {?Q("render_tag(_@Name@, [_@InterpretedArgs], RenderOptions)"),
+ #ast_info{ custom_tags = [Name] }}
+ end
end.
call_ast(Module, TreeWalker) ->
View
89 src/erlydtl_compiler.erl
@@ -51,8 +51,8 @@
-export([parse_file/2, parse_template/2, do_parse_template/2]).
-import(erlydtl_compiler_utils,
- [print/3, call_extension/3
- ]).
+ [add_filters/2, add_tags/2, print/3, call_extension/3,
+ load_library/2]).
-include("erlydtl_ext.hrl").
@@ -231,38 +231,37 @@ init_context(ParseTrail, DefDir, Module, Options) ->
{Val, undefined} -> [Val];
_ -> lists:usort([Locale | BlocktransLocales])
end,
- Context = #dtl_context{
- all_options = Options,
- auto_escape = case proplists:get_value(auto_escape, Options, true) of
- true -> on;
- _ -> off
- end,
- parse_trail = ParseTrail,
- module = Module,
- 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),
- trans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.trans_fun),
- trans_locales = TransLocales,
- vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars),
- reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
- compiler_options = proplists:append_values(compiler_options, Options),
- binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
- force_recompile = proplists:get_bool(force_recompile, Options),
- verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose),
- is_compiling_dir = ParseTrail == [],
- extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module),
- scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module),
- record_info = [{R, lists:zip(I, lists:seq(2, length(I) + 1))}
- || {R, I} <- proplists:get_value(record_info, Options, Ctx#dtl_context.record_info)],
- errors = init_error_info(errors, Ctx#dtl_context.errors, Options),
- warnings = init_error_info(warnings, Ctx#dtl_context.warnings, Options)
- },
+ Context0 =
+ #dtl_context{
+ all_options = Options,
+ auto_escape = case proplists:get_value(auto_escape, Options, true) of
+ true -> on;
+ _ -> off
+ end,
+ parse_trail = ParseTrail,
+ module = Module,
+ doc_root = proplists:get_value(doc_root, Options, DefDir),
+ libraries = proplists:get_value(libraries, Options, Ctx#dtl_context.libraries),
+ custom_tags_dir = proplists:get_value(
+ custom_tags_dir, Options,
+ filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
+ trans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.trans_fun),
+ trans_locales = TransLocales,
+ vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars),
+ reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
+ compiler_options = proplists:append_values(compiler_options, Options),
+ binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
+ force_recompile = proplists:get_bool(force_recompile, Options),
+ verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose),
+ is_compiling_dir = ParseTrail == [],
+ extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module),
+ scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module),
+ record_info = [{R, lists:zip(I, lists:seq(2, length(I) + 1))}
+ || {R, I} <- proplists:get_value(record_info, Options, Ctx#dtl_context.record_info)],
+ errors = init_error_info(errors, Ctx#dtl_context.errors, Options),
+ warnings = init_error_info(warnings, Ctx#dtl_context.warnings, Options)
+ },
+ Context = load_libraries(proplists:get_value(default_libraries, Options, []), Context0),
case call_extension(Context, init_context, [Context]) of
{ok, C} when is_record(C, dtl_context) -> C;
undefined -> Context
@@ -299,6 +298,28 @@ get_error_info_opts(Class, Options) ->
{Value, proplists:get_bool(Key, Options)}
end || Flag <- Flags].
+load_libraries([], #dtl_context{ all_options=Options }=Context) ->
+ %% Load filters and tags passed using the old options
+ Filters = proplists:get_value(custom_filters_modules, Options, []) ++ [erlydtl_filters],
+ Tags = proplists:get_value(custom_tags_modules, Options, []),
+ load_legacy_filters(Filters, load_legacy_tags(Tags, Context));
+load_libraries([Lib|Libs], Context) ->
+ load_libraries(Libs, load_library(Lib, Context)).
+
+load_legacy_filters([], Context) -> Context;
+load_legacy_filters([Mod|Filters], Context) ->
+ load_legacy_filters(Filters, add_filters(read_legacy_library(Mod), Context)).
+
+load_legacy_tags([], Context) -> Context;
+load_legacy_tags([Mod|Tags], Context) ->
+ load_legacy_tags(Tags, add_tags(read_legacy_library(Mod), Context)).
+
+read_legacy_library(Mod) ->
+ [{Name, {Mod, Name}}
+ || {Name, _} <- lists:ukeysort(1, Mod:module_info(exports)),
+ Name =/= module_info
+ ].
+
is_up_to_date(_, #dtl_context{force_recompile = true}) ->
false;
is_up_to_date(CheckSum, Context) ->
View
28 src/erlydtl_compiler_utils.erl
@@ -46,12 +46,14 @@
-export([
add_error/3, add_errors/2,
+ add_filters/2, add_tags/2,
add_warning/3, add_warnings/2,
call_extension/3,
format_error/1,
full_path/2,
get_current_file/1,
init_treewalker/1,
+ load_library/2,
merge_info/2,
print/3,
to_string/2,
@@ -225,6 +227,23 @@ reset_parse_trail(ParseTrail, #treewalker{ context=Context }=TreeWalker) ->
reset_parse_trail(ParseTrail, Context) ->
Context#dtl_context{ parse_trail=ParseTrail }.
+load_library(Lib, #treewalker{ context=Context }=TreeWalker) ->
+ TreeWalker#treewalker{ context=load_library(Lib, Context) };
+load_library(Lib, Context) ->
+ Mod = lib_module(Lib, Context),
+ add_filters(
+ [{Name, lib_function(Mod, Filter)}
+ || {Name, Filter} <- Mod:inventory(filters)],
+ add_tags(
+ [{Name, lib_function(Mod, Tag)}
+ || {Name, Tag} <- Mod:inventory(tags)],
+ Context)).
+
+add_filters(Load, #dtl_context{ filters=Filters }=Context) ->
+ Context#dtl_context{ filters=Load ++ Filters }.
+
+add_tags(Load, #dtl_context{ tags=Tags }=Context) ->
+ Context#dtl_context{ tags=Load ++ Tags }.
format_error(Other) ->
io_lib:format("## Error description for ~p not implemented.", [Other]).
@@ -357,3 +376,12 @@ split_ast(Split, [Ast|Rest], {Pre, Acc}) ->
split_ast(Split, Rest, {Pre, [Ast|Acc]});
split_ast(Split, [Ast|Rest], Acc) ->
split_ast(Split, Rest, [Ast|Acc]).
+
+lib_module(Name, #dtl_context{ libraries=Libs }) ->
+ proplists:get_value(Name, Libs, Name).
+
+lib_function(_, {Mod, Fun}) ->
+ lib_function(Mod, Fun);
+lib_function(Mod, Fun) ->
+ %% TODO: we can check for lib function availability here.. (sanity check)
+ {Mod, Fun}.
View
45 src/erlydtl_library.erl
@@ -0,0 +1,45 @@
+%%%-------------------------------------------------------------------
+%%% File: erlydtl_library.erl
+%%% @author Andreas Stenius <kaos@astekk.se>
+%%% @copyright 2014 Andreas Stenius
+%%% @doc
+%%% ErlyDTL library behaviour.
+%%% @end
+%%%
+%%% The MIT License
+%%%
+%%% Copyright (c) 2014 Andreas Stenius
+%%%
+%%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%%% of this software and associated documentation files (the "Software"), to deal
+%%% in the Software without restriction, including without limitation the rights
+%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%%% copies of the Software, and to permit persons to whom the Software is
+%%% furnished to do so, subject to the following conditions:
+%%%
+%%% The above copyright notice and this permission notice shall be included in
+%%% all copies or substantial portions of the Software.
+%%%
+%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%%% THE SOFTWARE.
+%%%
+%%% @since 2014 by Andreas Stenius
+%%%-------------------------------------------------------------------
+-module(erlydtl_library).
+-author('Andreas Stenius <kaos@astekk.se>').
+
+%% --------------------------------------------------------------------
+%% Definitions
+%% --------------------------------------------------------------------
+
+-type exported_fun() :: Name::atom().
+-type external_fun() :: {Module::atom(), exported_fun()}.
+-type library_function() :: exported_fun() | external_fun().
+-type inventory() :: [{Name::atom(), library_function()}].
+
+-callback inventory(filters|tags) -> inventory().

0 comments on commit 7bfc99e

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