Browse Source

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.
Andreas Stenius 11 years ago
parent
commit
7bfc99ed89

+ 22 - 12
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()
 

+ 3 - 2
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 = [],

+ 35 - 35
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) ->

+ 55 - 34
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) ->

+ 28 - 0
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}.

+ 45 - 0
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().