Browse Source

Release 0.11.0

Andreas Stenius 9 years ago
parent
commit
8093fc3c45
46 changed files with 1489 additions and 405 deletions
  1. 5 0
      .gitignore
  2. 19 10
      .travis.yml
  3. 22 0
      LICENSE
  4. 21 11
      Makefile
  5. 4 0
      NEWS.md
  6. 74 7
      README.markdown
  7. 1 0
      include/erlydtl_ext.hrl
  8. 35 51
      include/erlydtl_preparser.hrl
  9. BIN
      rebar
  10. 2 2
      rebar-slex.config
  11. 22 0
      rebar-tests.config
  12. 1 31
      rebar.config
  13. 31 0
      rebar.config.script
  14. 14 0
      src/erlydtl.app.src.script
  15. 1 0
      src/erlydtl.erl
  16. 138 32
      src/erlydtl_beam_compiler.erl
  17. 3 1
      src/erlydtl_compiler.erl
  18. 10 8
      src/erlydtl_compiler_utils.erl
  19. 24 4
      src/erlydtl_filters.erl
  20. 14 0
      src/erlydtl_parser.yrl
  21. 33 12
      src/erlydtl_runtime.erl
  22. 4 1
      src/erlydtl_scanner.erl
  23. 3 0
      src/erlydtl_scanner.slex
  24. 306 0
      src/erlydtl_time_compat.erl
  25. 137 109
      src/erlydtl_unparser.erl
  26. 139 88
      src/filter_lib/erlydtl_dateformat.erl
  27. 9 5
      src/i18n/sources_parser.erl
  28. 4 1
      test/erlydtl_custom_tags.erl
  29. 11 0
      test/erlydtl_custom_tags_lib.erl
  30. 7 1
      test/erlydtl_eunit_testrunner.erl
  31. 6 0
      test/erlydtl_lib_test1.erl
  32. 16 0
      test/erlydtl_lib_test2.erl
  33. 16 0
      test/erlydtl_lib_test2a.erl
  34. 182 17
      test/erlydtl_test_defs.erl
  35. 81 0
      test/erlydtl_translation_tests.erl
  36. 6 0
      test/files/expect/extend_doubleblock
  37. 11 0
      test/files/expect/reader_options
  38. 2 0
      test/files/expect/ssi_reader_options
  39. 1 1
      test/files/expect/trans
  40. 6 0
      test/files/input/base_doubleblock
  41. 3 0
      test/files/input/custom_tag_lib_var
  42. 3 0
      test/files/input/custom_tag_var
  43. 2 0
      test/files/input/extend_doubleblock
  44. 1 0
      test/files/input/reader_options
  45. 1 0
      test/files/input/ssi_reader_options
  46. 58 13
      test/sources_parser_tests.erl

+ 5 - 0
.gitignore

@@ -10,3 +10,8 @@ tests/output
 deps
 deps
 .emacs*
 .emacs*
 .eunit
 .eunit
+/.edts
+/.rebar/
+/*.beam
+/.settings/
+/.project

+ 19 - 10
.travis.yml

@@ -1,19 +1,28 @@
 language: erlang
 language: erlang
+sudo: false
 otp_release:
 otp_release:
 # Test on all supported releases accepted by the `require_otp_vsn` in rebar.config
 # Test on all supported releases accepted by the `require_otp_vsn` in rebar.config
-  - 17.3
-  - R16B03-1
-#  - R16B03 this version is broken!
-  - R16B02
-  - R16B01
-  - R16B
+   - 18.0
+   - 17.4
+   - 17.3
+   - 17.1
+   - 17.0
+   - R16B03-1
+#   - R16B03 this version is broken!
+   - R16B02
+   - R16B01
+   - R16B
 #  - R15B03-1 not available on travis
 #  - R15B03-1 not available on travis
-  - R15B03
-  - R15B02
+   - R15B03
+   - R15B02
+#   - R15B01 failing with timeout
+#   - R15B failing with timeout
 
 
 # since Travis is naughty and calls rebar get-deps behind our backs,
 # since Travis is naughty and calls rebar get-deps behind our backs,
-# we'll have to clean it up and build merl our selves..
-script: "make -C deps/merl && make tests"
+# we'll have to clean it up and build merl our selves.. (pre OTP 18.0)
+script: 
+  - "[ ! -d deps/merl ] || make -C deps/merl && make check"
 
 
 notifications:
 notifications:
   irc: "chat.freenode.net#erlydtl"
   irc: "chat.freenode.net#erlydtl"
+  email: false

+ 22 - 0
LICENSE

@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2008 Roberto Saccon, Evan Miller
+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.

+ 21 - 11
Makefile

@@ -19,28 +19,38 @@ update-deps:
 
 
 .PHONY: tests
 .PHONY: tests
 tests: src/erlydtl_parser.erl
 tests: src/erlydtl_parser.erl
-	@$(REBAR) eunit
+	@$(REBAR) -C rebar-tests.config eunit
 
 
 check: tests dialyze
 check: tests dialyze
 
 
-DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions -Wunmatched_returns
-dialyze:
-	@dialyzer -nn $(DIALYZER_OPTS) ebin || [ $$? -eq 2 ];
+## dialyzer
+PLT_FILE = ~/erlydtl.plt
+PLT_APPS ?= kernel stdlib compiler erts eunit syntax_tools
+DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions -Wunmatched_returns \
+		-Wunderspecs --verbose --fullpath
+.PHONY: dialyze
+dialyze: compile
+	@[ -f $(PLT_FILE) ] || $(MAKE) plt
+	@dialyzer --plt $(PLT_FILE) $(DIALYZER_OPTS) ebin || [ $$? -eq 2 ];
 
 
 ## In case you are missing a plt file for dialyzer,
 ## In case you are missing a plt file for dialyzer,
 ## you can run/adapt this command
 ## you can run/adapt this command
-PLT_APPS ?=
-plt:
-	@dialyzer -n -nn --build_plt --apps \
-		erts kernel stdlib sasl compiler \
-		crypto syntax_tools runtime_tools \
-		tools webtool hipe inets eunit
+.PHONY: plt
+plt: compile
+# we need to remove second copy of file
+	rm -f deps/merl/priv/merl_transform.beam
+	@echo "Building PLT, may take a few minutes"
+	@dialyzer --build_plt --output_plt $(PLT_FILE) --apps \
+		$(PLT_APPS) deps/* || [ $$? -eq 2 ];
 
 
 clean:
 clean:
-	@echo "Clean merl..." ; $(MAKE) -C deps/merl clean
+	@[ ! -d deps/merl ] || { echo "Clean merl..." ; $(MAKE) -C deps/merl clean ;}
 	@$(REBAR) -C rebar-slex.config clean
 	@$(REBAR) -C rebar-slex.config clean
 	rm -fv erl_crash.dump
 	rm -fv erl_crash.dump
 
 
+really-clean: clean
+	rm -f $(PLT_FILE)
+
 # rebuild any .slex files as well..  not included by default to avoid
 # rebuild any .slex files as well..  not included by default to avoid
 # the slex dependency, which is only needed in case the .slex file has
 # the slex dependency, which is only needed in case the .slex file has
 # been modified locally.
 # been modified locally.

+ 4 - 0
NEWS.md

@@ -4,6 +4,10 @@ This file records noteworthy changes and additions to erlydtl as
 suggested by the [GNU Coding
 suggested by the [GNU Coding
 Standards](http://www.gnu.org/prep/standards/html_node/NEWS-File.html#NEWS-File).
 Standards](http://www.gnu.org/prep/standards/html_node/NEWS-File.html#NEWS-File).
 
 
+## 0.11.0 (2015-10-25)
+
+I've failed to keep track. Please check resolved issues on [github](https://github.com/erlydtl/erlydtl).
+
 
 
 ## 0.10.0 (2014-12-20)
 ## 0.10.0 (2014-12-20)
 
 

+ 74 - 7
README.markdown

@@ -218,10 +218,35 @@ Options is a proplist possibly containing:
   specified, no .beam files will be created and a warning is
   specified, no .beam files will be created and a warning is
   emitted. To silence the warning, use `{out_dir, false}`.
   emitted. To silence the warning, use `{out_dir, false}`.
 
 
-* `reader` - {module, function} tuple that takes a path to a template
-  and returns a binary with the file contents. Defaults to `{file,
-  read_file}`. Useful for reading templates from a network resource.
+* `reader` - {module, function} tuple that takes a path to a template,
+  may be ReaderOptions  and returns a binary with the file contents.
+  Defaults to `{file,  read_file}`. Useful for reading templates from
+  a network resource.
 
 
+* `reader_options` - list of {option_name, Option} that passed as the
+  second parameter to `reader`.
+
+  ```erlang
+    extra_reader(FileName, ReaderOptions) ->
+     UserID = proplists:get_value(user_id, ReaderOptions, <<"IDUnknown">>),
+     UserName = proplists:get_value(user_name, ReaderOptions, <<"NameUnknown">>),
+     case file:read_file(FileName) of
+      {ok, Data} when UserID == <<"007">>, UserName == <<"Agent">> ->
+       {ok, Data};
+      {ok, _Data} ->
+       {error, "Not Found"};
+      Err ->
+       Err
+     end.
+  ```
+  ```
+    CompileResult = erlydtl:compile(Body, ModName,
+    										[return,
+    										  {reader, {?MODULE, extra_reader}},
+    										  {reader_options, [{user_id, <<"007">>},
+    											    {user_name, <<"Agent">>}]}
+    									    ]),
+  ```
 * `record_info` - List of records to look for when rendering the
 * `record_info` - List of records to look for when rendering the
   template. Each record info is a tuple with the fields of the record:
   template. Each record info is a tuple with the fields of the record:
 
 
@@ -256,8 +281,10 @@ Options is a proplist possibly containing:
     when Locale::string(), Context::string().
     when Locale::string(), Context::string().
   ```
   ```
 
 
+  Please, keep in mind, that if your templates where not specially 
+  designed, you probably still need render time translations.
   See description of the `translation_fun` render option for more
   See description of the `translation_fun` render option for more
-  details on the translation `context`.
+  details on the translation `context`. 
 
 
   Notice, you may instead pass a `fun/0`, `{Module, Function}` or
   Notice, you may instead pass a `fun/0`, `{Module, Function}` or
   `{Module, Function, Args}` which will be called recursively until it
   `{Module, Function, Args}` which will be called recursively until it
@@ -387,9 +414,43 @@ Same as `render/1`, but with the following options:
   {% endblocktrans %}
   {% endblocktrans %}
   ```
   ```
 
 
-  Notice, the translation fun can also be a `fun/0` or a MFA-tuple to
-  setup the translation prior to rendering. See the `translation_fun`
-  compile option for more details.
+  Render time translation function is also used to translate dates.
+  Date tokens mimics those used in django, so you may reuse django translations.
+  Tokens may appear in a date are:
+  1. Full months names, capitalized ("January" .. "December");
+  2. 3 letters months names ("jan" .. "dec");
+  3. Associated Press style months 
+     ("Jan." .. "Dec." with "abbrev.month" context);
+  4. Alternative month name, for "E" option
+     ("January" .. "December" with "alt. month" context);
+  5. Full week day names, capitalized ("Monday" .. "Sunday");
+  6. Abbreviated week day names, capitalized ("Mon" .. "Sun");
+  7. day time tokens ("AM", "PM", "a.m.", "p.m.", "noon", "midnight");
+
+  While date token values may be passed as lists, consider using binaries 
+  as a default string format.
+  Here is a simple but robust translation function stub:
+  ```erlang
+     translation_placeholder({Val,{Plural, Count}}, {L, C}) when is_list(Val) ->
+       translation_placeholder({list_to_binary(Val),{Plural, Count}}, LC);
+     translation_placeholder(Val, {L, C}) when is_list(Val) ->
+       translation_placeholder(list_to_binary(Val), LC);
+     translation_placeholder(Val, {L, C}) when is_list(C) ->
+       translation_placeholder(Val, list_to_binary(C));
+     translation_placeholder(Val, {L, C}) when is_list(C) ->
+       io:format("Translating ~p into ~p with context ~p~n", [Val, L, C]),
+       %% do nothing and return original value
+       Val.
+  ```
+
+  The translation fun can also be a `fun/0`, `{Module, Function}` or
+  `{Module, Function, Args}` which will be called recursively until it
+  yields a valid translation function, at which time any needed
+  translation setup actions can be carried out prior to returning the
+  next step (either another setup function/tuple, or the translation
+  function).
+
+  
 
 
 * `lists_0_based` - If the compile option `lists_0_based` was set to
 * `lists_0_based` - If the compile option `lists_0_based` was set to
   `defer`, pass this option (or set it to true, `{lists_0_based,
   `defer`, pass this option (or set it to true, `{lists_0_based,
@@ -540,3 +601,9 @@ From a Unix shell, run:
     make tests
     make tests
 
 
 Note that the tests will create some output in tests/output in case of regressions.
 Note that the tests will create some output in tests/output in case of regressions.
+
+
+License
+-------
+
+ErlyDTL is released under the MIT license.

+ 1 - 0
include/erlydtl_ext.hrl

@@ -21,6 +21,7 @@
           libraries = [],
           libraries = [],
           custom_tags_dir = [],
           custom_tags_dir = [],
           reader = {file, read_file},
           reader = {file, read_file},
+          reader_options = [],
           module = undefined,
           module = undefined,
           compiler_options = [],
           compiler_options = [],
           binary_strings = true,
           binary_strings = true,

+ 35 - 51
include/erlydtl_preparser.hrl

@@ -1,8 +1,7 @@
 %% -*- mode: erlang -*-
 %% -*- mode: erlang -*-
 %% vim: syntax=erlang
 %% vim: syntax=erlang
 
 
-%% This file is based on the yeccpre.hrl file found here:
-%% -file("/usr/lib/erlang/lib/parsetools-2.0.6/include/yeccpre.hrl", 0).
+%% This file is based on parsetools/include/yeccpre.hrl
 %%
 %%
 %% The applied modifiactions are to enable the caller to recover
 %% The applied modifiactions are to enable the caller to recover
 %% after a parse error, and then resume normal parsing.
 %% after a parse error, and then resume normal parsing.
@@ -10,18 +9,19 @@
 %%
 %%
 %% %CopyrightBegin%
 %% %CopyrightBegin%
 %%
 %%
-%% Copyright Ericsson AB 1996-2010. All Rights Reserved.
+%% Copyright Ericsson AB 1996-2015. All Rights Reserved.
 %%
 %%
-%% The contents of this file are subject to the Erlang Public License,
-%% Version 1.1, (the "License"); you may not use this file except in
-%% compliance with the License. You should have received a copy of the
-%% Erlang Public License along with this software. If not, it can be
-%% retrieved online at http://www.erlang.org/.
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
 %%
 %%
-%% Software distributed under the License is distributed on an "AS IS"
-%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
-%% the License for the specific language governing rights and limitations
-%% under the License.
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
 %%
 %%
 %% %CopyrightEnd%
 %% %CopyrightEnd%
 %%
 %%
@@ -39,10 +39,11 @@ parse(Tokens) ->
 
 
 -spec parse_and_scan({function() | {atom(), atom()}, [_]}
 -spec parse_and_scan({function() | {atom(), atom()}, [_]}
                      | {atom(), atom(), [_]}) -> yecc_ret().
                      | {atom(), atom(), [_]}) -> yecc_ret().
-parse_and_scan({F, A}) -> % Fun or {M, F}
+parse_and_scan({F, A}) ->
     yeccpars0([], {{F, A}, no_line}, 0, [], []);
     yeccpars0([], {{F, A}, no_line}, 0, [], []);
 parse_and_scan({M, F, A}) ->
 parse_and_scan({M, F, A}) ->
-    yeccpars0([], {{{M, F}, A}, no_line}, 0, [], []).
+    Arity = length(A),
+    yeccpars0([], {{fun M:F/Arity, A}, no_line}, 0, [], []).
 
 
 resume([Tokens, Tzr, State, States, Vstack]) ->
 resume([Tokens, Tzr, State, States, Vstack]) ->
     yeccpars0(Tokens, Tzr, State, States, Vstack).
     yeccpars0(Tokens, Tzr, State, States, Vstack).
@@ -72,23 +73,20 @@ return_state() ->
 yeccpars0(Tokens, Tzr, State, States, Vstack) ->
 yeccpars0(Tokens, Tzr, State, States, Vstack) ->
     try yeccpars1(Tokens, Tzr, State, States, Vstack)
     try yeccpars1(Tokens, Tzr, State, States, Vstack)
     catch 
     catch 
-        error:function_clause = Error ->
-            case erlang:get_stacktrace() of
-                %% [{atom() | tuple(),atom(),[any()] | byte(),[{'file',string()} | {'line',pos_integer()}]}]
-                [{?MODULE, F, ArityOrArgs, _Source}|_]=Stacktrace ->
-                    erlang:raise(
-                      error,
-                      yecc_error_type(Error, F, ArityOrArgs),
-                      Stacktrace);
-                Stacktrace ->
-                    erlang:raise(error, Error, Stacktrace)
+        error: Error ->
+            Stacktrace = erlang:get_stacktrace(),
+            try yecc_error_type(Error, Stacktrace) of
+                Desc ->
+                    erlang:raise(error, {yecc_bug, ?CODE_VERSION, Desc},
+                                 Stacktrace)
+            catch _:_ -> erlang:raise(error, Error, Stacktrace)
             end;
             end;
         %% Probably thrown from return_error/2:
         %% Probably thrown from return_error/2:
         throw: {error, {_Line, ?MODULE, _M}} = Error ->
         throw: {error, {_Line, ?MODULE, _M}} = Error ->
             Error
             Error
     end.
     end.
 
 
-yecc_error_type(function_clause=Error, F, ArityOrArgs) ->
+yecc_error_type(function_clause, [{?MODULE,F,ArityOrArgs,_} | _]) ->
     case atom_to_list(F) of
     case atom_to_list(F) of
         "yeccgoto_" ++ SymbolL ->
         "yeccgoto_" ++ SymbolL ->
             {ok,[{atom,_,Symbol}],_} = erl_scan:string(SymbolL),
             {ok,[{atom,_,Symbol}],_} = erl_scan:string(SymbolL),
@@ -96,9 +94,7 @@ yecc_error_type(function_clause=Error, F, ArityOrArgs) ->
                         [S,_,_,_,_,_,_] -> S;
                         [S,_,_,_,_,_,_] -> S;
                         _ -> state_is_unknown
                         _ -> state_is_unknown
                     end,
                     end,
-            Desc = {Symbol, State, missing_in_goto_table},
-            {yecc_bug, ?CODE_VERSION, Desc};
-        _ -> Error
+            {Symbol, State, missing_in_goto_table}
     end.
     end.
 
 
 -define(checkparse(CALL, STATE),
 -define(checkparse(CALL, STATE),
@@ -164,21 +160,10 @@ yecc_end(Line) ->
     {'$end', Line}.
     {'$end', Line}.
 
 
 yecctoken_end_location(Token) ->
 yecctoken_end_location(Token) ->
-    try
-        {text, Str} = erl_scan:token_info(Token, text),
-        {line, Line} = erl_scan:token_info(Token, line),
-        Parts = re:split(Str, "\n"),
-        Dline = length(Parts) - 1,
-        Yline = Line + Dline,
-        case erl_scan:token_info(Token, column) of
-            {column, Column} ->
-                Col = byte_size(lists:last(Parts)),
-                {Yline, Col + if Dline =:= 0 -> Column; true -> 1 end};
-            undefined ->
-                Yline
-        end
-    catch _:_ ->
-        yecctoken_location(Token)
+    try erl_anno:end_location(element(2, Token)) of
+        undefined -> yecctoken_location(Token);
+        Loc -> Loc
+    catch _:_ -> yecctoken_location(Token)
     end.
     end.
 
 
 -compile({nowarn_unused_function, yeccerror/1}).
 -compile({nowarn_unused_function, yeccerror/1}).
@@ -189,15 +174,15 @@ yeccerror(Token) ->
 
 
 -compile({nowarn_unused_function, yecctoken_to_string/1}).
 -compile({nowarn_unused_function, yecctoken_to_string/1}).
 yecctoken_to_string(Token) ->
 yecctoken_to_string(Token) ->
-    case catch erl_scan:token_info(Token, text) of
-        {text, Txt} -> Txt;
-        _ -> yecctoken2string(Token)
+    try erl_scan:text(Token) of
+        undefined -> yecctoken2string(Token);
+        Txt -> Txt
+    catch _:_ -> yecctoken2string(Token)
     end.
     end.
 
 
 yecctoken_location(Token) ->
 yecctoken_location(Token) ->
-    case catch erl_scan:token_info(Token, location) of
-        {location, Loc} -> Loc;
-        _ -> element(2, Token)
+    try erl_scan:location(Token)
+    catch _:_ -> element(2, Token)
     end.
     end.
 
 
 -compile({nowarn_unused_function, yecctoken2string/1}).
 -compile({nowarn_unused_function, yecctoken2string/1}).
@@ -206,7 +191,7 @@ yecctoken2string({integer,_,N}) -> io_lib:write(N);
 yecctoken2string({float,_,F}) -> io_lib:write(F);
 yecctoken2string({float,_,F}) -> io_lib:write(F);
 yecctoken2string({char,_,C}) -> io_lib:write_char(C);
 yecctoken2string({char,_,C}) -> io_lib:write_char(C);
 yecctoken2string({var,_,V}) -> io_lib:format("~s", [V]);
 yecctoken2string({var,_,V}) -> io_lib:format("~s", [V]);
-yecctoken2string({string,_,S}) -> io_lib:write_unicode_string(S);
+yecctoken2string({string,_,S}) -> io_lib:write_string(S);
 yecctoken2string({reserved_symbol, _, A}) -> io_lib:write(A);
 yecctoken2string({reserved_symbol, _, A}) -> io_lib:write(A);
 yecctoken2string({_Cat, _, Val}) -> io_lib:format("~p",[Val]);
 yecctoken2string({_Cat, _, Val}) -> io_lib:format("~p",[Val]);
 yecctoken2string({dot, _}) -> "'.'";
 yecctoken2string({dot, _}) -> "'.'";
@@ -218,4 +203,3 @@ yecctoken2string(Other) ->
     io_lib:write(Other).
     io_lib:write(Other).
 
 
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-

BIN
rebar


+ 2 - 2
rebar-slex.config

@@ -1,8 +1,8 @@
 %% -*- mode: erlang -*-
 %% -*- mode: erlang -*-
 
 
 {deps,
 {deps,
- [{slex, ".*", {git, "git://github.com/erlydtl/slex.git", {tag, "0.2.1"}}},
-  {merl, ".*", {git, "git://github.com/erlydtl/merl.git", "28e5b3829168199e8475fa91b997e0c03b90d280"}, [raw]}
+ [{slex, ".*", {git, "git://github.com/erlydtl/slex.git", {tag, "0.2.1"}}}
+  %%,{merl, ".*", {git, "git://github.com/erlydtl/merl.git", "28e5b3829168199e8475fa91b997e0c03b90d280"}, [raw]}
  ]
  ]
 }.
 }.
 
 

+ 22 - 0
rebar-tests.config

@@ -0,0 +1,22 @@
+%% -*- mode: erlang -*-
+
+{eunit_opts,
+ [%% This turns off the default output, MUST HAVE
+  no_tty,
+  %% Uses the progress formatter with ANSI-colored output
+  {report, {eunit_progress, [colored
+                             %% uncomment to get a list of slowest running tests
+                             %%, profile
+                            ]}}
+ ]}.
+
+{deps,
+ [{eunit_formatters, ".*",
+   {git, "git://github.com/seancribbs/eunit_formatters", "35e3e6ab2db48af776a1a21bba6f1461c97caacb"}}
+ ]}.
+
+{pre_hooks,
+ [{eunit,
+   "erlc -I include/erlydtl_preparser.hrl -o test"
+   " test/erlydtl_extension_testparser.yrl"}
+ ]}.

+ 1 - 31
rebar.config

@@ -2,37 +2,7 @@
 
 
 %% accept R15B02.., any R16B except R16B03
 %% accept R15B02.., any R16B except R16B03
 %% also accept OTP v17, altough it may not work properly on that release yet..
 %% also accept OTP v17, altough it may not work properly on that release yet..
-{require_otp_vsn, "R15B0[^1]|R16B$|R16B[^0]|R16B0[^3]|R16B03-1|17"}.
+{require_otp_vsn, "R15B0[^1]|R16B$|R16B[^0]|R16B0[^3]|R16B03-1|17|18"}.
 
 
 {erl_opts, [debug_info]}.
 {erl_opts, [debug_info]}.
 {yrl_opts, [{includefile, "include/erlydtl_preparser.hrl"}]}.
 {yrl_opts, [{includefile, "include/erlydtl_preparser.hrl"}]}.
-
-{eunit_opts,
- [%% This turns off the default output, MUST HAVE
-  no_tty,
-  %% Uses the progress formatter with ANSI-colored output
-  {report, {eunit_progress, [colored
-                             %% uncomment to get a list of slowest running tests
-                             %%, profile
-                            ]}}
- ]}.
-
-{deps,
- [{merl, ".*",
-   {git, "git://github.com/erlydtl/merl.git", "28e5b3829168199e8475fa91b997e0c03b90d280"},
-   [raw]},
-  {eunit_formatters, ".*",
-   {git, "git://github.com/seancribbs/eunit_formatters", "7f79fa3fb953b94990bd9b41e92cef7cfecf91ef"}}
- ]}.
-
-{pre_hooks,
- [{"(linux|darwin|solaris)", compile, "make -C \"$REBAR_DEPS_DIR/merl\" all -W test"},
-  {"(freebsd|netbsd|openbsd)", compile, "gmake -C \"$REBAR_DEPS_DIR/merl\" all"},
-  {"win32", compile, "make -C \"%REBAR_DEPS_DIR%/merl\" all -W test"},
-  {eunit,
-   "erlc -I include/erlydtl_preparser.hrl -o test"
-   " test/erlydtl_extension_testparser.yrl"},
-  {"(linux|darwin|solaris)", eunit, "make -C \"$REBAR_DEPS_DIR/merl\" test"},
-  {"(freebsd|netbsd|openbsd)", eunit, "gmake -C \"$REBAR_DEPS_DIR/merl\" test"},
-  {"win32", eunit, "make -C \"%REBAR_DEPS_DIR%/merl\" test"}
- ]}.

+ 31 - 0
rebar.config.script

@@ -0,0 +1,31 @@
+%% -*- mode: erlang -*-
+
+Patch = fun (Key, Value, Config) ->
+                case lists:keysearch(Key, 1, Config) of
+                    {value, {_, Org}} ->
+                        lists:keyreplace(Key, 1, Config, {Key, Org ++ Value});
+                    false ->
+                        [{Key, Value}|Config]
+                end
+        end,
+
+application:load(syntax_tools),
+case application:get_key(syntax_tools, vsn) of
+    {ok, Vsn} when "1.7" > Vsn ->
+        Deps = [{merl, ".*",
+                 {git, "git://github.com/erlydtl/merl.git", {branch, "erlydtl"}},
+                 [raw]}],
+        PreHooks =
+            [{"(linux|darwin|solaris)", compile, "make -C \"$REBAR_DEPS_DIR/merl\" all -W test"},
+             {"(freebsd|netbsd|openbsd)", compile, "gmake -C \"$REBAR_DEPS_DIR/merl\" all"},
+             {"win32", compile, "make -C \"%REBAR_DEPS_DIR%/merl\" all -W test"},
+             {"(linux|darwin|solaris)", eunit, "make -C \"$REBAR_DEPS_DIR/merl\" test"},
+             {"(freebsd|netbsd|openbsd)", eunit, "gmake -C \"$REBAR_DEPS_DIR/merl\" test"},
+             {"win32", eunit, "make -C \"%REBAR_DEPS_DIR%/merl\" test"}],
+        lists:foldl(fun ({K, V}, C) -> Patch(K, V, C) end, CONFIG,
+                    [{deps, Deps},
+                     {pre_hooks, PreHooks},
+                     {erl_opts, [{d, 'MERL_DEP'}]}]);
+    _ ->
+        CONFIG
+end.

+ 14 - 0
src/erlydtl.app.src.script

@@ -0,0 +1,14 @@
+%% -*- mode: erlang -*-
+application:load(syntax_tools),
+ExtraApps = case application:get_key(syntax_tools, vsn) of
+                {ok, Vsn} when "1.7" > Vsn -> [merl];
+                _ -> []
+            end,
+
+{application, erlydtl,
+ [{description, "Django Template Language for Erlang"},
+  {vsn, git},
+  {modules, []},
+  {applications, [kernel, stdlib, compiler, syntax_tools|ExtraApps]},
+  {registered, []}
+ ]}.

+ 1 - 0
src/erlydtl.erl

@@ -86,6 +86,7 @@
                         | {locale, string()}
                         | {locale, string()}
                         | {out_dir, false | filename()}
                         | {out_dir, false | filename()}
                         | {reader, {Module::atom(), Function::atom}}
                         | {reader, {Module::atom(), Function::atom}}
+                        | {reader_options, [{Name::atom(), iodata()}]}
                         | {record_info, [{Name::atom(), [Field::atom()]}]}
                         | {record_info, [{Name::atom(), [Field::atom()]}]}
                         | {scanner_module, Module::atom()}
                         | {scanner_module, Module::atom()}
                         | {vars, [{atom(), iodata()}]}.
                         | {vars, [{atom(), iodata()}]}.

+ 138 - 32
src/erlydtl_beam_compiler.erl

@@ -67,7 +67,11 @@
          load_library/4, shorten_filename/2, push_auto_escape/2,
          load_library/4, shorten_filename/2, push_auto_escape/2,
          pop_auto_escape/1, token_pos/1, is_stripped_token_empty/1]).
          pop_auto_escape/1, token_pos/1, is_stripped_token_empty/1]).
 
 
+-ifdef(MERL_DEP).
 -include_lib("merl/include/merl.hrl").
 -include_lib("merl/include/merl.hrl").
+-else.
+-include_lib("syntax_tools/include/merl.hrl").
+-endif.
 -include("erlydtl_ext.hrl").
 -include("erlydtl_ext.hrl").
 
 
 
 
@@ -94,13 +98,15 @@ format_error({write_file, Error}) ->
 format_error(compile_beam) ->
 format_error(compile_beam) ->
     "Failed to compile template to BEAM code";
     "Failed to compile template to BEAM code";
 format_error({unknown_filter, Name, Arity}) ->
 format_error({unknown_filter, Name, Arity}) ->
-    io_lib:format("Unknown filter '~p' (arity ~p)", [Name, Arity]);
+    io_lib:format("Unknown filter '~s' (arity ~b)", [Name, Arity]);
 format_error({filter_args, Name, {Mod, Fun}, Arity}) ->
 format_error({filter_args, Name, {Mod, Fun}, Arity}) ->
-    io_lib:format("Wrong number of arguments to filter '~p' (~p:~p): ~p", [Name, Mod, Fun, Arity]);
+    io_lib:format("Wrong number of arguments to filter '~s' (~s:~s): ~b", [Name, Mod, Fun, Arity]);
+format_error({unknown_tag, Name}) ->
+    io_lib:format("Unknown tag '~s'", [Name]);
 format_error({missing_tag, Name, {Mod, Fun}}) ->
 format_error({missing_tag, Name, {Mod, Fun}}) ->
-    io_lib:format("Custom tag '~p' not exported (~p:~p)", [Name, Mod, Fun]);
+    io_lib:format("Custom tag '~s' not exported (~s:~s)", [Name, Mod, Fun]);
 format_error({bad_tag, Name, {Mod, Fun}, Arity}) ->
 format_error({bad_tag, Name, {Mod, Fun}, Arity}) ->
-    io_lib:format("Invalid tag '~p' (~p:~p/~p)", [Name, Mod, Fun, Arity]);
+    io_lib:format("Invalid tag '~s' (~s:~s/~b)", [Name, Mod, Fun, Arity]);
 format_error({load_code, Error}) ->
 format_error({load_code, Error}) ->
     io_lib:format("Failed to load BEAM code: ~p", [Error]);
     io_lib:format("Failed to load BEAM code: ~p", [Error]);
 format_error({reserved_variable, ReservedName}) ->
 format_error({reserved_variable, ReservedName}) ->
@@ -109,7 +115,7 @@ format_error({translation_fun, Fun}) ->
     io_lib:format("Invalid translation function: ~s~n",
     io_lib:format("Invalid translation function: ~s~n",
                   [if is_function(Fun) ->
                   [if is_function(Fun) ->
                            Info = erlang:fun_info(Fun),
                            Info = erlang:fun_info(Fun),
-                           io_lib:format("~s:~s/~p", [proplists:get_value(K, Info) || K <- [module, name, arity]]);
+                           io_lib:format("~s:~s/~b", [proplists:get_value(K, Info) || K <- [module, name, arity]]);
                       true -> io_lib:format("~p", [Fun])
                       true -> io_lib:format("~p", [Fun])
                    end]);
                    end]);
 format_error(non_block_tag) ->
 format_error(non_block_tag) ->
@@ -221,7 +227,7 @@ compile_to_binary(DjangoParseTree, CheckSum, Context) ->
 compile_forms(Forms, Context) ->
 compile_forms(Forms, Context) ->
     maybe_debug_template(Forms, Context),
     maybe_debug_template(Forms, Context),
     Options = Context#dtl_context.compiler_options,
     Options = Context#dtl_context.compiler_options,
-    case compile:forms(Forms, Options) of
+    case compile:forms(Forms, [nowarn_shadow_vars|Options]) of
         Compiled when element(1, Compiled) =:= ok ->
         Compiled when element(1, Compiled) =:= ok ->
             [ok, Module, Bin|Info] = tuple_to_list(Compiled),
             [ok, Module, Bin|Info] = tuple_to_list(Compiled),
             lists:foldl(
             lists:foldl(
@@ -320,13 +326,14 @@ maybe_debug_template(Forms, Context) ->
 is_up_to_date(CheckSum, Context) ->
 is_up_to_date(CheckSum, Context) ->
     Module = Context#dtl_context.module,
     Module = Context#dtl_context.module,
     {M, F} = Context#dtl_context.reader,
     {M, F} = Context#dtl_context.reader,
+    ReaderOptions = Context#dtl_context.reader_options,
     case catch Module:source() of
     case catch Module:source() of
         {_, CheckSum} ->
         {_, CheckSum} ->
             case catch Module:dependencies() of
             case catch Module:dependencies() of
                 L when is_list(L) ->
                 L when is_list(L) ->
                     RecompileList = lists:foldl(
                     RecompileList = lists:foldl(
                                       fun ({XFile, XCheckSum}, Acc) ->
                                       fun ({XFile, XCheckSum}, Acc) ->
-                                              case catch M:F(XFile) of
+                                              case catch erlydtl_runtime:read_file_internal(M, F, XFile, ReaderOptions) of
                                                   {ok, Data} ->
                                                   {ok, Data} ->
                                                       case binary_to_list(erlang:md5(Data)) of
                                                       case binary_to_list(erlang:md5(Data)) of
                                                           XCheckSum ->
                                                           XCheckSum ->
@@ -361,7 +368,7 @@ custom_tags_ast(CustomTags, TreeWalker) ->
     case custom_tags_clauses_ast(CustomTags, TreeWalker) of
     case custom_tags_clauses_ast(CustomTags, TreeWalker) of
         skip ->
         skip ->
             {{erl_syntax:comment(
             {{erl_syntax:comment(
-                ["% render_tag/3 is not used in this template."]),
+                ["%% render_tag/3 is not used in this template."]),
               #ast_info{}},
               #ast_info{}},
              TreeWalker};
              TreeWalker};
         {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} ->
         {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} ->
@@ -414,7 +421,9 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Tree
                         undefined ->
                         undefined ->
                             custom_tags_clauses_ast1(
                             custom_tags_clauses_ast1(
                               CustomTags, [Tag | ExcludeTags],
                               CustomTags, [Tag | ExcludeTags],
-                              ClauseAcc, InfoAcc, TreeWalker);
+                              ClauseAcc, InfoAcc,
+                              ?WARN({unknown_tag, Tag}, TreeWalker)
+                             );
                         {{Ast, Info}, TW} ->
                         {{Ast, Info}, TW} ->
                             Clause = ?Q("(_@Tag@, _Variables, RenderOptions) -> _@match, _@Ast",
                             Clause = ?Q("(_@Tag@, _Variables, RenderOptions) -> _@match, _@Ast",
                                         [{match, options_match_ast(TW)}]),
                                         [{match, options_match_ast(TW)}]),
@@ -428,16 +437,27 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Tree
 custom_forms(Dir, Module, Functions, AstInfo) ->
 custom_forms(Dir, Module, Functions, AstInfo) ->
     Dependencies = AstInfo#ast_info.dependencies,
     Dependencies = AstInfo#ast_info.dependencies,
     TranslatableStrings = AstInfo#ast_info.translatable_strings,
     TranslatableStrings = AstInfo#ast_info.translatable_strings,
+    TranslatedBlocks = AstInfo#ast_info.translated_blocks,
+    Variables = lists:usort(AstInfo#ast_info.var_names),
+    DefaultVariables = lists:usort(AstInfo#ast_info.def_names),
+    Constants = lists:usort(AstInfo#ast_info.const_names),
 
 
     erl_syntax:revert_forms(
     erl_syntax:revert_forms(
       lists:flatten(
       lists:flatten(
         ?Q(["-module('@Module@').",
         ?Q(["-module('@Module@').",
             "-export([source_dir/0, dependencies/0, translatable_strings/0,",
             "-export([source_dir/0, dependencies/0, translatable_strings/0,",
-            "         render/1, render/2, render/3]).",
+            "         translated_blocks/0, variables/0, default_variables/0,",
+            "         constants/0, render/1, render/2, render/3]).",
             "-export(['@__export_functions'/0]).",
             "-export(['@__export_functions'/0]).",
+
             "source_dir() -> _@Dir@.",
             "source_dir() -> _@Dir@.",
             "dependencies() -> _@Dependencies@.",
             "dependencies() -> _@Dependencies@.",
+            "variables() -> _@Variables@.",
+            "default_variables() -> _@DefaultVariables@.",
+            "constants() -> _@Constants@.",
             "translatable_strings() -> _@TranslatableStrings@.",
             "translatable_strings() -> _@TranslatableStrings@.",
+            "translated_blocks() -> _@TranslatedBlocks@.",
+
             "render(Tag) -> render(Tag, [], []).",
             "render(Tag) -> render(Tag, [], []).",
             "render(Tag, Vars) -> render(Tag, Vars, []).",
             "render(Tag, Vars) -> render(Tag, Vars, []).",
             "render(Tag, Vars, Opts) ->",
             "render(Tag, Vars, Opts) ->",
@@ -481,6 +501,7 @@ forms({BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum,
           "-export([render/0, render/1, render/2, source/0, dependencies/0,",
           "-export([render/0, render/1, render/2, source/0, dependencies/0,",
           "         translatable_strings/0, translated_blocks/0, variables/0,",
           "         translatable_strings/0, translated_blocks/0, variables/0,",
           "         default_variables/0, constants/0]).",
           "         default_variables/0, constants/0]).",
+
           "source() -> {_@File@, _@CheckSum@}.",
           "source() -> {_@File@, _@CheckSum@}.",
           "dependencies() -> _@Dependencies@.",
           "dependencies() -> _@Dependencies@.",
           "variables() -> _@Variables@.",
           "variables() -> _@Variables@.",
@@ -488,7 +509,9 @@ forms({BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum,
           "constants() -> _@Constants@.",
           "constants() -> _@Constants@.",
           "translatable_strings() -> _@TranslatableStrings@.",
           "translatable_strings() -> _@TranslatableStrings@.",
           "translated_blocks() -> _@TranslatedBlocks@.",
           "translated_blocks() -> _@TranslatedBlocks@.",
+
           "'@_CustomTagsFunctionAst'() -> _.",
           "'@_CustomTagsFunctionAst'() -> _.",
+
           "render() -> render([], []).",
           "render() -> render([], []).",
           "render(Variables) -> render(Variables, []).",
           "render(Variables) -> render(Variables, []).",
           "render(Variables, RenderOptions) ->",
           "render(Variables, RenderOptions) ->",
@@ -497,6 +520,7 @@ forms({BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum,
           "  catch",
           "  catch",
           "    Err -> {error, Err}",
           "    Err -> {error, Err}",
           "  end.",
           "  end.",
+
           "render_internal(_Variables, RenderOptions) -> _@FinalBodyAst."
           "render_internal(_Variables, RenderOptions) -> _@FinalBodyAst."
          ])).
          ])).
 
 
@@ -569,9 +593,10 @@ body_ast([{'extends', {string_literal, _Pos, String}} | ThisParseTree], #treewal
     end;
     end;
 
 
 body_ast(DjangoParseTree, TreeWalker) ->
 body_ast(DjangoParseTree, TreeWalker) ->
-    body_ast(DjangoParseTree, empty_scope(), TreeWalker).
+    ScopeFun = fun ([ScopeVars|ScopeBody]) -> [?Q("(fun() -> _@ScopeVars, [_@ScopeBody] end)()")] end,
+    body_ast(DjangoParseTree, empty_scope(), ScopeFun, TreeWalker).
 
 
-body_ast(DjangoParseTree, BodyScope, TreeWalker) ->
+body_ast(DjangoParseTree, BodyScope, ScopeFun, TreeWalker) ->
     {ScopeId, TreeWalkerScope} = begin_scope(BodyScope, TreeWalker),
     {ScopeId, TreeWalkerScope} = begin_scope(BodyScope, TreeWalker),
     BodyFun =
     BodyFun =
         fun ({'autoescape', {identifier, _, OnOrOff}, Contents}, TW) ->
         fun ({'autoescape', {identifier, _, OnOrOff}, Contents}, TW) ->
@@ -584,9 +609,9 @@ body_ast(DjangoParseTree, BodyScope, TreeWalker) ->
                         lists:foldr(
                         lists:foldr(
                           fun ({ChildFile, ChildPos, ChildBlock}, {{SuperAst, SuperInfo}, AccTW}) ->
                           fun ({ChildFile, ChildPos, ChildBlock}, {{SuperAst, SuperInfo}, AccTW}) ->
                                   BlockScope = create_scope(
                                   BlockScope = create_scope(
-                                                 [{block, ?Q("[{super, _@SuperAst}]"), safe}],
+                                                 [{block, ?Q("fun (super) -> _@SuperAst; (_) -> [] end"), safe}],
                                                  ChildPos, ChildFile, AccTW),
                                                  ChildPos, ChildFile, AccTW),
-                                  {{BlockAst, BlockInfo}, BlockTW} = body_ast(ChildBlock, BlockScope, AccTW),
+                                  {{BlockAst, BlockInfo}, BlockTW} = body_ast(ChildBlock, BlockScope, ScopeFun, AccTW),
                                   {{BlockAst, merge_info(SuperInfo, BlockInfo)}, BlockTW}
                                   {{BlockAst, merge_info(SuperInfo, BlockInfo)}, BlockTW}
                           end,
                           end,
                           ContentsAst, ChildBlocks);
                           ContentsAst, ChildBlocks);
@@ -686,6 +711,8 @@ body_ast(DjangoParseTree, BodyScope, TreeWalker) ->
                 string_ast(String, TW);
                 string_ast(String, TW);
             ({'tag', Name, Args}, TW) ->
             ({'tag', Name, Args}, TW) ->
                 tag_ast(Name, Args, TW);
                 tag_ast(Name, Args, TW);
+            ({'tag', Name, Args, {identifier, _, NewTagVar}}, TW) ->
+                tag_ast(Name, Args, NewTagVar, TW);
             ({'templatetag', {_, _, TagName}}, TW) ->
             ({'templatetag', {_, _, TagName}}, TW) ->
                 templatetag_ast(TagName, TW);
                 templatetag_ast(TagName, TW);
             ({'trans', Value}, TW) ->
             ({'trans', Value}, TW) ->
@@ -702,6 +729,11 @@ body_ast(DjangoParseTree, BodyScope, TreeWalker) ->
                 extension_ast(Tag, TW);
                 extension_ast(Tag, TW);
             ({'extends', _}, TW) ->
             ({'extends', _}, TW) ->
                 empty_ast(?ERR(unexpected_extends_tag, TW));
                 empty_ast(?ERR(unexpected_extends_tag, TW));
+            ({'language', Locale, Contents}, TW) ->
+                {{LocaleAst, LocaleInfo}, LocaleTW} = value_ast(Locale, true, false, TW),
+                LanguageScopeFun = fun ([ScopeVars|ScopeBody]) -> [?Q("(fun(_CurrentLocale) -> _@ScopeVars, [_@ScopeBody] end)(_@LocaleAst)")] end,
+                {{BodyAst, BodyInfo}, BodyTW} = body_ast(Contents, {[], [?Q("")]}, LanguageScopeFun, LocaleTW),
+                {{BodyAst, merge_info(BodyInfo, LocaleInfo)}, BodyTW};
             (ValueToken, TW) ->
             (ValueToken, TW) ->
                 format(value_ast(ValueToken, true, true, TW))
                 format(value_ast(ValueToken, true, true, TW))
         end,
         end,
@@ -714,9 +746,7 @@ body_ast(DjangoParseTree, BodyScope, TreeWalker) ->
                   {Ast, merge_info(Info, InfoAcc)}
                   {Ast, merge_info(Info, InfoAcc)}
           end, #ast_info{}, AstInfoList),
           end, #ast_info{}, AstInfoList),
 
 
-    {Ast, TreeWalker2} = end_scope(
-                           fun ([ScopeVars|ScopeBody]) -> [?Q("begin _@ScopeVars, [_@ScopeBody] end")] end,
-                           ScopeId, AstList, TreeWalker1),
+    {Ast, TreeWalker2} = end_scope(ScopeFun, ScopeId, AstList, TreeWalker1),
     {{erl_syntax:list(Ast), Info}, TreeWalker2}.
     {{erl_syntax:list(Ast), Info}, TreeWalker2}.
 
 
 
 
@@ -785,6 +815,7 @@ blocktrans_ast(Args, Contents, PluralContents, TreeWalker) ->
                   {string_literal, _, S} ->
                   {string_literal, _, S} ->
                       unescape_string_literal(S)
                       unescape_string_literal(S)
               end,
               end,
+    Trimmed = proplists:get_value(trimmed, Args),
 
 
     %% add new scope using 'with' values
     %% add new scope using 'with' values
     {NewScope, {ArgInfo, TreeWalker1}} =
     {NewScope, {ArgInfo, TreeWalker1}} =
@@ -804,18 +835,18 @@ blocktrans_ast(Args, Contents, PluralContents, TreeWalker) ->
     TreeWalker2 = push_scope(NewScope, TreeWalker1),
     TreeWalker2 = push_scope(NewScope, TreeWalker1),
 
 
     %% key for translation lookup
     %% key for translation lookup
-    SourceText = erlydtl_unparser:unparse(Contents),
+    SourceText = erlydtl_unparser:unparse(Contents, Trimmed),
     {{DefaultAst, AstInfo}, TreeWalker3} = body_ast(Contents, TreeWalker2),
     {{DefaultAst, AstInfo}, TreeWalker3} = body_ast(Contents, TreeWalker2),
     MergedInfo = merge_info(AstInfo, ArgInfo),
     MergedInfo = merge_info(AstInfo, ArgInfo),
 
 
     #dtl_context{
     #dtl_context{
       trans_fun = TFun,
       trans_fun = TFun,
-      trans_locales = TLocales } = TreeWalker3#treewalker.context,
+      trans_locales = TLocales, auto_escape = AutoEscape } = TreeWalker3#treewalker.context,
     if TFun =:= none; PluralContents =/= undefined ->
     if TFun =:= none; PluralContents =/= undefined ->
             %% translate in runtime
             %% translate in runtime
             {FinalAst, FinalTW} = blocktrans_runtime_ast(
             {FinalAst, FinalTW} = blocktrans_runtime_ast(
-                                    {DefaultAst, MergedInfo}, SourceText, Contents, Context,
-                                    plural_contents(PluralContents, Count, TreeWalker3)),
+                                    {DefaultAst, MergedInfo}, SourceText, Contents, Context, AutoEscape,
+                                    plural_contents(PluralContents, Count, TreeWalker3), Trimmed),
             {FinalAst, restore_scope(TreeWalker1, FinalTW)};
             {FinalAst, restore_scope(TreeWalker1, FinalTW)};
        is_function(TFun, 2) ->
        is_function(TFun, 2) ->
             %% translate in compile-time
             %% translate in compile-time
@@ -840,7 +871,7 @@ blocktrans_ast(Args, Contents, PluralContents, TreeWalker) ->
             empty_ast(?ERR({translation_fun, TFun}, TreeWalker3))
             empty_ast(?ERR({translation_fun, TFun}, TreeWalker3))
     end.
     end.
 
 
-blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, {Plural, TreeWalker}) ->
+blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, AutoEscape, {Plural, TreeWalker}, Trimmed) ->
     %% Contents is flat - only strings and '{{var}}' allowed.
     %% Contents is flat - only strings and '{{var}}' allowed.
     %% build sorted list (orddict) of pre-resolved variables to pass to runtime translation function
     %% build sorted list (orddict) of pre-resolved variables to pass to runtime translation function
     USortedVariables = lists:usort(fun({variable, {identifier, _, A}},
     USortedVariables = lists:usort(fun({variable, {identifier, _, A}},
@@ -856,13 +887,14 @@ blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, {Plura
     VarListAst = erl_syntax:list(VarAsts),
     VarListAst = erl_syntax:list(VarAsts),
     BlockTransAst = ?Q(["begin",
     BlockTransAst = ?Q(["begin",
                         "  case erlydtl_runtime:translate_block(",
                         "  case erlydtl_runtime:translate_block(",
-                        "         _@phrase, _@locale,",
+                        "         _@phrase, _@locale, _@auto_escape, ",
                         "         _@VarListAst, _TranslationFun) of",
                         "         _@VarListAst, _TranslationFun) of",
                         "    default -> _@DefaultAst;",
                         "    default -> _@DefaultAst;",
                         "    Text -> Text",
                         "    Text -> Text",
                         "  end",
                         "  end",
                         "end"],
                         "end"],
-                       [{phrase, phrase_ast(SourceText, Plural)},
+                       [{phrase, phrase_ast(SourceText, Plural, Trimmed)},
+                        {auto_escape, autoescape_ast(AutoEscape)},
                         {locale, phrase_locale_ast(Context)}]),
                         {locale, phrase_locale_ast(Context)}]),
     {{BlockTransAst, merge_count_info(Info, Plural)}, TreeWalker1}.
     {{BlockTransAst, merge_count_info(Info, Plural)}, TreeWalker1}.
 
 
@@ -878,14 +910,16 @@ plural_contents(Contents, {_CountVarName, Value}, TreeWalker) ->
     {CountAst, TW} = value_ast(Value, false, false, TreeWalker),
     {CountAst, TW} = value_ast(Value, false, false, TreeWalker),
     {{Contents, CountAst}, TW}.
     {{Contents, CountAst}, TW}.
 
 
-phrase_ast(Text, undefined) -> merl:term(Text);
-phrase_ast(Text, {Contents, {CountAst, _CountInfo}}) ->
+phrase_ast(Text, undefined, _) -> merl:term(Text);
+phrase_ast(Text, {Contents, {CountAst, _CountInfo}}, Trimmed) ->
     erl_syntax:tuple(
     erl_syntax:tuple(
       [merl:term(Text),
       [merl:term(Text),
        erl_syntax:tuple(
        erl_syntax:tuple(
-         [merl:term(erlydtl_unparser:unparse(Contents)),
+         [merl:term(erlydtl_unparser:unparse(Contents, Trimmed)),
           CountAst])
           CountAst])
       ]).
       ]).
+autoescape_ast([]) -> autoescape_ast([on]);
+autoescape_ast([V | _]) -> erl_syntax:atom(V == on).
 
 
 phrase_locale_ast(undefined) -> merl:var('_CurrentLocale');
 phrase_locale_ast(undefined) -> merl:var('_CurrentLocale');
 phrase_locale_ast(Context) -> erl_syntax:tuple([merl:var('_CurrentLocale'), merl:term(Context)]).
 phrase_locale_ast(Context) -> erl_syntax:tuple([merl:var('_CurrentLocale'), merl:term(Context)]).
@@ -1026,11 +1060,12 @@ include_ast(File, ArgList, Scopes, #treewalker{ context=Context }=TreeWalker) ->
 ssi_ast(FileName, #treewalker{
 ssi_ast(FileName, #treewalker{
                      context=#dtl_context{
                      context=#dtl_context{
                                 reader = {Mod, Fun},
                                 reader = {Mod, Fun},
+                                reader_options = ReaderOptions,
                                 doc_root = Dir
                                 doc_root = Dir
                                }
                                }
                     }=TreeWalker) ->
                     }=TreeWalker) ->
     {{FileAst, Info}, TreeWalker1} = value_ast(FileName, true, true, TreeWalker),
     {{FileAst, Info}, TreeWalker1} = value_ast(FileName, true, true, TreeWalker),
-    {{?Q("erlydtl_runtime:read_file(_@Mod@, _@Fun@, _@Dir@, _@FileAst)"), Info}, TreeWalker1}.
+    {{?Q("erlydtl_runtime:read_file(_@Mod@, _@Fun@, _@Dir@, _@FileAst, _@ReaderOptions@)"), Info}, TreeWalker1}.
 
 
 filter_tag_ast(FilterList, Contents, TreeWalker) ->
 filter_tag_ast(FilterList, Contents, TreeWalker) ->
     {{InnerAst, Info}, TreeWalker1} = body_ast(Contents, push_auto_escape(did, TreeWalker)),
     {{InnerAst, Info}, TreeWalker1} = body_ast(Contents, push_auto_escape(did, TreeWalker)),
@@ -1112,13 +1147,28 @@ filter_ast1({{identifier, Pos, Name}, Args}, ValueAst, TreeWalker) ->
             empty_ast(?WARN({Pos, Error}, TreeWalker1))
             empty_ast(?WARN({Pos, Error}, TreeWalker1))
     end.
     end.
 
 
-filter_ast2(Name, Args, #dtl_context{ filters = Filters }) ->
+% special case for date, which reqires localisation
+% may be replaced later with a query to a list 
+% of functions which require translation
+filter_ast2('date' = Name, Args, #dtl_context{ filters = Filters } = Ctx) ->
+    case proplists:get_value(Name, Filters) of
+        {Mod, Fun} -> 
+            case erlang:function_exported(Mod, Fun, length(Args) + 2) of
+                true -> {ok, ?Q("'@Mod@':'@Fun@'(_@Args, _TranslationFun, _CurrentLocale )")};
+                false -> filter_ast3(Name, Args, Ctx) % redefined 'date'?
+            end;
+        % should never happen
+        undefined -> {unknown_filter, Name, length(Args)}
+    end;
+filter_ast2(Name, Args, Ctx) ->
+    filter_ast3(Name, Args, Ctx).
+
+filter_ast3(Name, Args, #dtl_context{ filters = Filters }) ->
     case proplists:get_value(Name, Filters) of
     case proplists:get_value(Name, Filters) of
         {Mod, Fun}=Filter ->
         {Mod, Fun}=Filter ->
             case erlang:function_exported(Mod, Fun, length(Args)) of
             case erlang:function_exported(Mod, Fun, length(Args)) of
                 true -> {ok, ?Q("'@Mod@':'@Fun@'(_@Args)")};
                 true -> {ok, ?Q("'@Mod@':'@Fun@'(_@Args)")};
-                false ->
-                    {filter_args, Name, Filter, length(Args)}
+                false -> {filter_args, Name, Filter, length(Args)}
             end;
             end;
         undefined ->
         undefined ->
             {unknown_filter, Name, length(Args)}
             {unknown_filter, Name, length(Args)}
@@ -1475,7 +1525,7 @@ now_ast(FormatString, TreeWalker) ->
     %% i.e. \"foo\" becomes "foo"
     %% i.e. \"foo\" becomes "foo"
     UnescapeOuter = string:strip(FormatString, both, 34),
     UnescapeOuter = string:strip(FormatString, both, 34),
     {{StringAst, Info}, TreeWalker1} = string_ast(UnescapeOuter, TreeWalker),
     {{StringAst, Info}, TreeWalker1} = string_ast(UnescapeOuter, TreeWalker),
-    {{?Q("erlydtl_dateformat:format(_@StringAst)"), Info}, TreeWalker1}.
+    {{?Q("erlydtl_dateformat:format(_@StringAst, _TranslationFun, _CurrentLocale)"), Info}, TreeWalker1}.
 
 
 spaceless_ast(Contents, TreeWalker) ->
 spaceless_ast(Contents, TreeWalker) ->
     {{Ast, Info}, TreeWalker1} = body_ast(Contents, TreeWalker),
     {{Ast, Info}, TreeWalker1} = body_ast(Contents, TreeWalker),
@@ -1552,6 +1602,62 @@ custom_tags_modules_ast({identifier, Pos, Name}, InterpretedArgs,
             end
             end
     end.
     end.
 
 
+tag_ast(Name, Args, NewTagVar, TreeWalker) ->
+    {{InterpretedArgs, AstInfo1}, TreeWalker1} = interpret_args(Args, TreeWalker),
+    {{RenderAst, RenderInfo}, TreeWalker2} = custom_tags_modules_ast(Name, InterpretedArgs, NewTagVar, TreeWalker1),
+    {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker2}.
+
+custom_tags_modules_ast({identifier, Pos, Name}, InterpretedArgs, NewTagVar,
+                        #treewalker{
+                           context=#dtl_context{
+                                      tags = Tags,
+                                      module = Module,
+                                      is_compiling_dir=IsCompilingDir
+                                     }
+                          }=TreeWalker) ->
+  LocalVarAst = varname_ast(NewTagVar),
+    case proplists:get_value(Name, Tags) of
+        {Mod, Fun}=Tag ->
+            case lists:max([-1] ++ [I || {N,I} <- Mod:module_info(exports), N =:= Fun]) of
+                2 ->
+                      {Id, TreeWalker1} = begin_scope(
+                        {[{NewTagVar, LocalVarAst}],
+                          [?Q("_@LocalVarAst = '@Mod@':'@Fun@'([_@InterpretedArgs], RenderOptions)")]},
+                        TreeWalker
+                      ),
+                  {{Id, #ast_info{}}, TreeWalker1};
+                1 ->
+                      {Id, TreeWalker1} = begin_scope(
+                        {[{NewTagVar, LocalVarAst}],
+                          [?Q("_@LocalVarAst = '@Mod@':'@Fun@'([_@InterpretedArgs])")]},
+                        TreeWalker
+                  ),
+                  {{Id, #ast_info{}}, TreeWalker1};
+
+                -1 ->
+                    empty_ast(?WARN({Pos, {missing_tag, Name, Tag}}, TreeWalker));
+                I ->
+                    empty_ast(?WARN({Pos, {bad_tag, Name, Tag, I}}, TreeWalker))
+            end;
+        undefined ->
+            if IsCompilingDir =/= false ->
+                      {Id, TreeWalker1} = begin_scope(
+                        {[{NewTagVar, LocalVarAst}],
+                          [?Q("_@LocalVarAst = '@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)")]},
+                        TreeWalker
+                      ),
+                      {{Id, #ast_info{ custom_tags = [Name]}}, TreeWalker1};
+               true ->
+                     {Id, TreeWalker1} = begin_scope(
+                       {[{NewTagVar, LocalVarAst}],
+                         [?Q("_@LocalVarAst = '@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)")]},
+                       TreeWalker
+                     ),
+                     {{Id, #ast_info{ custom_tags = [Name]}}, TreeWalker1}
+
+            end
+    end.
+
 call_ast(Module, TreeWalker) ->
 call_ast(Module, TreeWalker) ->
     call_ast(Module, merl:var("_Variables"), #ast_info{}, TreeWalker).
     call_ast(Module, merl:var("_Variables"), #ast_info{}, TreeWalker).
 
 

+ 3 - 1
src/erlydtl_compiler.erl

@@ -266,6 +266,7 @@ init_context(ParseTrail, DefDir, Module, Options) ->
            vars = proplists:get_value(default_vars, Options, Ctx#dtl_context.vars),
            vars = proplists:get_value(default_vars, Options, Ctx#dtl_context.vars),
            const = proplists:get_value(constants, Options, Ctx#dtl_context.const),
            const = proplists:get_value(constants, Options, Ctx#dtl_context.const),
            reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
            reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
+           reader_options = proplists:get_value(reader_options, Options, Ctx#dtl_context.reader_options),
            compiler_options = proplists:append_values(compiler_options, Options),
            compiler_options = proplists:append_values(compiler_options, 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_bool(force_recompile, Options),
            force_recompile = proplists:get_bool(force_recompile, Options),
@@ -366,7 +367,8 @@ is_up_to_date(CheckSum, Context) ->
 
 
 parse_file(File, Context) ->
 parse_file(File, Context) ->
     {M, F} = Context#dtl_context.reader,
     {M, F} = Context#dtl_context.reader,
-    case catch M:F(File) of
+    ReaderOptions = Context#dtl_context.reader_options,
+    case catch erlydtl_runtime:read_file_internal(M, F, File, ReaderOptions) of
         {ok, Data} ->
         {ok, Data} ->
             parse_template(Data, Context);
             parse_template(Data, Context);
         {error, Reason} ->
         {error, Reason} ->

+ 10 - 8
src/erlydtl_compiler_utils.erl

@@ -483,22 +483,25 @@ lib_module(Name, #dtl_context{ libraries=Libs }) ->
     Mod = proplists:get_value(Name, Libs, Name),
     Mod = proplists:get_value(Name, Libs, Name),
     case code:ensure_loaded(Mod) of
     case code:ensure_loaded(Mod) of
         {module, Mod} ->
         {module, Mod} ->
-            IsLib = case proplists:get_value(behaviour, Mod:module_info(attributes)) of
-                        Behaviours when is_list(Behaviours) ->
-                            lists:member(erlydtl_library, Behaviours);
-                        _ -> false
-                    end,
-            if IsLib ->
+            case implements_behaviour(erlydtl_library, Mod) of
+                true ->
                     case Mod:version() of
                     case Mod:version() of
                         ?LIB_VERSION -> {ok, Mod};
                         ?LIB_VERSION -> {ok, Mod};
                         V -> {load_library, Name, Mod, {version, V}}
                         V -> {load_library, Name, Mod, {version, V}}
                     end;
                     end;
-               true -> {load_library, Name, Mod, behaviour}
+               false -> {load_library, Name, Mod, behaviour}
             end;
             end;
         {error, Reason} ->
         {error, Reason} ->
             {load_library, Name, Mod, Reason}
             {load_library, Name, Mod, Reason}
     end.
     end.
 
 
+implements_behaviour(Behaviour, Mod) ->
+    Attrs = Mod:module_info(attributes),
+    Found =
+        [B || [B] <- proplists:get_all_values(behaviour, Attrs)] ++
+        [B || [B] <- proplists:get_all_values(behavior, Attrs)],
+    [] =:= [Behaviour] -- Found.
+
 read_library(Mod, Section, Which) ->
 read_library(Mod, Section, Which) ->
     [{Name, lib_function(Mod, Fun)}
     [{Name, lib_function(Mod, Fun)}
      || {Name, Fun} <- read_inventory(Mod, Section),
      || {Name, Fun} <- read_inventory(Mod, Section),
@@ -530,4 +533,3 @@ remove_first_quote(String) ->
 
 
 remove_last_quote(String) ->
 remove_last_quote(String) ->
     lists:reverse(remove_first_quote(lists:reverse(String))).
     lists:reverse(remove_first_quote(lists:reverse(String))).
-  

+ 24 - 4
src/erlydtl_filters.erl

@@ -48,7 +48,8 @@
         -export([cast_to_float/1,cast_to_integer/1,stringformat_io/7,round/2,unjoin/2,addDefaultURI/1]).
         -export([cast_to_float/1,cast_to_integer/1,stringformat_io/7,round/2,unjoin/2,addDefaultURI/1]).
 -endif.
 -endif.
  
  
- 
+-import(erlydtl_time_compat, [phash2/1, monotonic_time/0, unique_integer/0]).
+
 -export([add/2,
 -export([add/2,
         addslashes/1,
         addslashes/1,
         capfirst/1,
         capfirst/1,
@@ -56,6 +57,8 @@
         cut/2,
         cut/2,
         date/1,
         date/1,
         date/2,
         date/2,
+        date/3,
+        date/4,
         default/2,
         default/2,
         default_if_none/2,
         default_if_none/2,
         dictsort/2,
         dictsort/2,
@@ -234,7 +237,7 @@ cut(Input, Arg) when is_binary(Input) ->
 cut(Input, [Char]) when is_list(Input) ->
 cut(Input, [Char]) when is_list(Input) ->
     cut(Input, Char, []).
     cut(Input, Char, []).
  
  
-%% @doc Formats a date according to the default format.
+%% @doc Formats a date according to the default format. 
 date(Input) ->
 date(Input) ->
     date(Input, "F j, Y").
     date(Input, "F j, Y").
 
 
@@ -247,6 +250,19 @@ date(Input, _FormatStr) ->
     io:format("Unexpected date parameter: ~p~n", [Input]),
     io:format("Unexpected date parameter: ~p~n", [Input]),
     "".
     "".
 
 
+%% @doc Formats a date according to the default format 
+%%      localizing it with provided translation function.
+date(Input, TransFun, Locale) ->
+    date(Input, "F j, Y", TransFun, Locale).
+date(Input, FormatStr, TransFun, Locale)
+  when is_tuple(Input)
+       andalso (size(Input) == 2 orelse size(Input) == 3) ->
+    erlydtl_dateformat:format(Input, FormatStr, TransFun, Locale);
+date(Input, _FormatStr, _TransFun, _Locale) ->
+    io:format("Unexpected date parameter: ~p~n", [Input]),
+    "".
+
+
 %% @doc If value evaluates to `false', use given default. Otherwise, use the value.
 %% @doc If value evaluates to `false', use given default. Otherwise, use the value.
 default(Input, Default) ->
 default(Input, Default) ->
     case erlydtl_runtime:is_false(Input) of
     case erlydtl_runtime:is_false(Input) of
@@ -524,7 +540,9 @@ random(_) ->
     "".
     "".
 
 
 random_num(Value) ->
 random_num(Value) ->
-    _ = random:seed(now()),
+    random:seed(phash2([node()]),
+                monotonic_time(),
+                unique_integer()),
     random:uniform(Value).
     random:uniform(Value).
 
 
 %% random tags to be used when using erlydtl in testing
 %% random tags to be used when using erlydtl in testing
@@ -1117,7 +1135,9 @@ truncatewords_html_io([C|Rest], WordsLeft, Acc, Tags, tag) ->
 truncatewords_html_io([C|Rest], WordsLeft, Acc, Tags, attrs) when C =:= $> ->
 truncatewords_html_io([C|Rest], WordsLeft, Acc, Tags, attrs) when C =:= $> ->
     truncatewords_html_io(Rest, WordsLeft, [C|Acc], Tags, text);
     truncatewords_html_io(Rest, WordsLeft, [C|Acc], Tags, text);
 truncatewords_html_io([C|Rest], WordsLeft, Acc, [_Tag|RestOfTags], close_tag) when C =:= $> ->
 truncatewords_html_io([C|Rest], WordsLeft, Acc, [_Tag|RestOfTags], close_tag) when C =:= $> ->
-    truncatewords_html_io(Rest, WordsLeft, [C|Acc], RestOfTags, text).
+    truncatewords_html_io(Rest, WordsLeft, [C|Acc], RestOfTags, text);
+truncatewords_html_io([C|Rest], WordsLeft, Acc, Tags, close_tag) when C =/= $> ->
+    truncatewords_html_io(Rest, WordsLeft, [C|Acc], Tags, close_tag).
 
 
 wordcount([], Count) ->
 wordcount([], Count) ->
     Count;
     Count;

+ 14 - 0
src/erlydtl_parser.yrl

@@ -66,6 +66,10 @@ Nonterminals
     IncludeTag
     IncludeTag
     NowTag
     NowTag
 
 
+    LanguageBlock
+    LanguageBraced
+    EndLanguageBraced
+
     FirstofTag
     FirstofTag
 
 
     FilterBlock
     FilterBlock
@@ -171,6 +175,7 @@ Terminals
     endifchanged_keyword
     endifchanged_keyword
     endifequal_keyword
     endifequal_keyword
     endifnotequal_keyword
     endifnotequal_keyword
+    endlanguage_keyword
     endregroup_keyword
     endregroup_keyword
     endspaceless_keyword
     endspaceless_keyword
     endwith_keyword
     endwith_keyword
@@ -186,6 +191,7 @@ Terminals
     ifnotequal_keyword
     ifnotequal_keyword
     in_keyword
     in_keyword
     include_keyword
     include_keyword
+    language_keyword
     load_keyword
     load_keyword
     noop_keyword
     noop_keyword
     not_keyword
     not_keyword
@@ -205,6 +211,7 @@ Terminals
     string_literal
     string_literal
     string
     string
     templatetag_keyword
     templatetag_keyword
+    trimmed_keyword
     openblock_keyword
     openblock_keyword
     closeblock_keyword
     closeblock_keyword
     openvariable_keyword
     openvariable_keyword
@@ -252,6 +259,7 @@ Elements -> Elements IfEqualBlock : '$1' ++ ['$2'].
 Elements -> Elements IfNotEqualBlock : '$1' ++ ['$2'].
 Elements -> Elements IfNotEqualBlock : '$1' ++ ['$2'].
 Elements -> Elements IfChangedBlock : '$1' ++ ['$2'].
 Elements -> Elements IfChangedBlock : '$1' ++ ['$2'].
 Elements -> Elements IncludeTag : '$1' ++ ['$2'].
 Elements -> Elements IncludeTag : '$1' ++ ['$2'].
+Elements -> Elements LanguageBlock : '$1' ++ ['$2'].
 Elements -> Elements LoadTag : '$1' ++ ['$2'].
 Elements -> Elements LoadTag : '$1' ++ ['$2'].
 Elements -> Elements NowTag : '$1' ++ ['$2'].
 Elements -> Elements NowTag : '$1' ++ ['$2'].
 Elements -> Elements RegroupTag : '$1' ++ ['$2'].
 Elements -> Elements RegroupTag : '$1' ++ ['$2'].
@@ -294,6 +302,10 @@ BlockBlock -> BlockBraced Elements EndBlockBraced : {block, '$1', '$2'}.
 BlockBraced -> open_tag block_keyword identifier close_tag : '$3'.
 BlockBraced -> open_tag block_keyword identifier close_tag : '$3'.
 EndBlockBraced -> open_tag endblock_keyword close_tag.
 EndBlockBraced -> open_tag endblock_keyword close_tag.
 
 
+LanguageBlock -> LanguageBraced Elements EndLanguageBraced : {language, '$1', '$2'}.
+LanguageBraced -> open_tag language_keyword Value close_tag : '$3'.
+EndLanguageBraced -> open_tag endlanguage_keyword close_tag.
+
 ExtendsTag -> open_tag extends_keyword string_literal close_tag : {extends, '$3'}.
 ExtendsTag -> open_tag extends_keyword string_literal close_tag : {extends, '$3'}.
 
 
 IncludeTag -> open_tag include_keyword string_literal close_tag : {include, '$3', []}.
 IncludeTag -> open_tag include_keyword string_literal close_tag : {include, '$3', []}.
@@ -410,6 +422,7 @@ BlockTransArgs -> '$empty' : [].
 BlockTransArgs -> count_keyword Arg BlockTransArgs : [{count, '$2'}|'$3'].
 BlockTransArgs -> count_keyword Arg BlockTransArgs : [{count, '$2'}|'$3'].
 BlockTransArgs -> with_keyword Args BlockTransArgs : [{args, '$2'}|'$3'].
 BlockTransArgs -> with_keyword Args BlockTransArgs : [{args, '$2'}|'$3'].
 BlockTransArgs -> context_keyword string_literal BlockTransArgs : [{context, '$2'}|'$3'].
 BlockTransArgs -> context_keyword string_literal BlockTransArgs : [{context, '$2'}|'$3'].
+BlockTransArgs -> trimmed_keyword BlockTransArgs : [trimmed|'$2'].
 
 
 BlockTransContents -> '$empty' : [].
 BlockTransContents -> '$empty' : [].
 BlockTransContents -> open_var identifier close_var BlockTransContents : [{variable, '$2'}|'$4'].
 BlockTransContents -> open_var identifier close_var BlockTransContents : [{variable, '$2'}|'$4'].
@@ -447,6 +460,7 @@ WithBraced -> open_tag with_keyword Args close_tag : '$3'.
 EndWithBraced -> open_tag endwith_keyword close_tag.
 EndWithBraced -> open_tag endwith_keyword close_tag.
 
 
 CustomTag -> open_tag identifier CustomArgs close_tag : {tag, '$2', '$3'}.
 CustomTag -> open_tag identifier CustomArgs close_tag : {tag, '$2', '$3'}.
+CustomTag -> open_tag identifier CustomArgs as_keyword identifier close_tag : {tag, '$2', '$3', '$5'}.
 
 
 CustomArgs -> '$empty' : [].
 CustomArgs -> '$empty' : [].
 CustomArgs -> identifier '=' Value CustomArgs : [{'$1', '$3'}|'$4'].
 CustomArgs -> identifier '=' Value CustomArgs : [{'$1', '$3'}|'$4'].

+ 33 - 12
src/erlydtl_runtime.erl

@@ -72,7 +72,8 @@ find_value(Key, L) when is_integer(Key), is_list(L) ->
     if Key =< length(L) -> lists:nth(Key, L);
     if Key =< length(L) -> lists:nth(Key, L);
        true -> undefined
        true -> undefined
     end;
     end;
-find_value(Key, {GBSize, GBData}) when is_integer(GBSize) ->
+find_value(_, {0, nil}) -> undefined;
+find_value(Key, {GBSize, {_, _, _, _}=GBData}) when is_integer(GBSize) ->
     case gb_trees:lookup(Key, {GBSize, GBData}) of
     case gb_trees:lookup(Key, {GBSize, GBData}) of
         {value, Val} ->
         {value, Val} ->
             Val;
             Val;
@@ -219,12 +220,12 @@ do_translate(Phrase, Locale, TranslationFun)
 %%  * Each interpolation variable should exist
 %%  * Each interpolation variable should exist
 %%    (String="{{a}}", Variables=[{"b", "b-val"}] will fall)
 %%    (String="{{a}}", Variables=[{"b", "b-val"}] will fall)
 %%  * Orddict keys should be string(), not binary()
 %%  * Orddict keys should be string(), not binary()
--spec translate_block(phrase(), locale(), orddict:orddict(), none | translate_fun()) -> iodata().
-translate_block(Phrase, Locale, Variables, TranslationFun) ->
+-spec translate_block(phrase(), locale(), atom(), orddict:orddict(), none | translate_fun()) -> iodata().
+translate_block(Phrase, Locale, AutoEscape, Variables, TranslationFun) ->
     case translate(Phrase, Locale, TranslationFun, default) of
     case translate(Phrase, Locale, TranslationFun, default) of
         default -> default;
         default -> default;
         Translated ->
         Translated ->
-            try interpolate_variables(Translated, Variables)
+            try interpolate_variables(Translated, Variables, AutoEscape)
             catch
             catch
                 {no_close_var, T} ->
                 {no_close_var, T} ->
                     io:format(standard_error, "Warning: template translation: variable not closed: \"~s\"~n", [T]),
                     io:format(standard_error, "Warning: template translation: variable not closed: \"~s\"~n", [T]),
@@ -233,13 +234,13 @@ translate_block(Phrase, Locale, Variables, TranslationFun) ->
             end
             end
     end.
     end.
 
 
-interpolate_variables(Tpl, []) ->
+interpolate_variables(Tpl, [], _) ->
     Tpl;
     Tpl;
-interpolate_variables(Tpl, Variables) ->
+interpolate_variables(Tpl, Variables, AutoEscape) ->
     BTpl = iolist_to_binary(Tpl),
     BTpl = iolist_to_binary(Tpl),
-    interpolate_variables1(BTpl, Variables).
+    interpolate_variables1(BTpl, Variables, AutoEscape).
 
 
-interpolate_variables1(Tpl, Vars) ->
+interpolate_variables1(Tpl, Vars, AutoEscape) ->
     %% pre-compile binary patterns?
     %% pre-compile binary patterns?
     case binary:split(Tpl, <<"{{">>) of
     case binary:split(Tpl, <<"{{">>) of
         [Tpl]=NoVars -> NoVars; %% need to enclose in list due to list tail call below..
         [Tpl]=NoVars -> NoVars; %% need to enclose in list due to list tail call below..
@@ -248,11 +249,19 @@ interpolate_variables1(Tpl, Vars) ->
                 [_] -> throw({no_close_var, Tpl});
                 [_] -> throw({no_close_var, Tpl});
                 [Var, Post1] ->
                 [Var, Post1] ->
                     Var1 = string:strip(binary_to_list(Var)),
                     Var1 = string:strip(binary_to_list(Var)),
-                    Value = orddict:fetch(Var1, Vars),
-                    [Pre, Value | interpolate_variables1(Post1, Vars)]
+                    Value = cast(orddict:fetch(Var1, Vars), AutoEscape),
+                    [Pre, Value | interpolate_variables1(Post1, Vars, AutoEscape)]
             end
             end
     end.
     end.
 
 
+cast(V, _) when is_integer(V); is_float(V) ->
+    erlydtl_filters:format_number(V);
+cast(V, true) when is_binary(V); is_list(V) ->
+    erlydtl_filters:force_escape(V);
+cast(V, false) when is_binary(V); is_list(V) ->
+    V;
+cast(V, AutoEscape) ->
+    cast(io_lib:format("~p", [V]), AutoEscape).
 
 
 are_equal(Arg1, Arg2) when Arg1 =:= Arg2 ->
 are_equal(Arg1, Arg2) when Arg1 =:= Arg2 ->
     true;
     true;
@@ -450,13 +459,25 @@ spaceless(Contents) ->
     Contents4 = re:replace(Contents3, ">\\s+<", "><", [global, {return,list}]),
     Contents4 = re:replace(Contents3, ">\\s+<", "><", [global, {return,list}]),
     Contents4.
     Contents4.
 
 
-read_file(Module, Function, DocRoot, FileName) ->
+read_file(Module, Function, DocRoot, FileName, ReaderOptions) ->
     AbsName = case filename:absname(FileName) of
     AbsName = case filename:absname(FileName) of
                   FileName -> FileName;
                   FileName -> FileName;
                   _ -> filename:join([DocRoot, FileName])
                   _ -> filename:join([DocRoot, FileName])
               end,
               end,
-    case Module:Function(AbsName) of
+    case read_file_internal(Module, Function, AbsName, ReaderOptions) of
         {ok, Data} -> Data;
         {ok, Data} -> Data;
         {error, Reason} ->
         {error, Reason} ->
             throw({read_file, AbsName, Reason})
             throw({read_file, AbsName, Reason})
     end.
     end.
+read_file_internal(Module, Function, FileName, ReaderOptions) ->
+    case erlang:function_exported(Module, Function,1) of
+        true ->
+            Module:Function(FileName);
+        false ->
+            case erlang:function_exported(Module, Function,2) of
+                true ->
+                    Module:Function(FileName, ReaderOptions);
+                false ->
+                    {error, "Empty reader"}
+            end
+    end.

+ 4 - 1
src/erlydtl_scanner.erl

@@ -36,7 +36,7 @@
 %%%-------------------------------------------------------------------
 %%%-------------------------------------------------------------------
 -module(erlydtl_scanner).
 -module(erlydtl_scanner).
 
 
-%% This file was generated 2014-12-16 18:46:16 UTC by slex 0.2.1-2-g7814678.
+%% This file was generated 2015-10-17 21:30:29 UTC by slex 0.2.1-2-g7814678.
 %% http://github.com/erlydtl/slex
 %% http://github.com/erlydtl/slex
 -slex_source(["src/erlydtl_scanner.slex"]).
 -slex_source(["src/erlydtl_scanner.slex"]).
 
 
@@ -90,6 +90,7 @@ is_keyword(any, "from") -> true;
 is_keyword(any, "count") -> true;
 is_keyword(any, "count") -> true;
 is_keyword(any, "context") -> true;
 is_keyword(any, "context") -> true;
 is_keyword(any, "noop") -> true;
 is_keyword(any, "noop") -> true;
+is_keyword(any, "trimmed") -> true;
 is_keyword(close, "only") -> true;
 is_keyword(close, "only") -> true;
 is_keyword(close, "parsed") -> true;
 is_keyword(close, "parsed") -> true;
 is_keyword(close, "silent") -> true;
 is_keyword(close, "silent") -> true;
@@ -106,6 +107,8 @@ is_keyword(open, "autoescape") -> true;
 is_keyword(open, "endautoescape") -> true;
 is_keyword(open, "endautoescape") -> true;
 is_keyword(open, "block") -> true;
 is_keyword(open, "block") -> true;
 is_keyword(open, "endblock") -> true;
 is_keyword(open, "endblock") -> true;
+is_keyword(open, "language") -> true;
+is_keyword(open, "endlanguage") -> true;
 is_keyword(open, "comment") -> true;
 is_keyword(open, "comment") -> true;
 is_keyword(open, "endcomment") -> true;
 is_keyword(open, "endcomment") -> true;
 is_keyword(open, "cycle") -> true;
 is_keyword(open, "cycle") -> true;

+ 3 - 0
src/erlydtl_scanner.slex

@@ -314,6 +314,7 @@ form \
   is_keyword(any, "count") -> true; \
   is_keyword(any, "count") -> true; \
   is_keyword(any, "context") -> true; \
   is_keyword(any, "context") -> true; \
   is_keyword(any, "noop") -> true; \
   is_keyword(any, "noop") -> true; \
+  is_keyword(any, "trimmed") -> true; \
   \
   \
   is_keyword(close, "only") -> true; \
   is_keyword(close, "only") -> true; \
   is_keyword(close, "parsed") -> true; \
   is_keyword(close, "parsed") -> true; \
@@ -332,6 +333,8 @@ form \
   is_keyword(open, "endautoescape") -> true; \
   is_keyword(open, "endautoescape") -> true; \
   is_keyword(open, "block") -> true; \
   is_keyword(open, "block") -> true; \
   is_keyword(open, "endblock") -> true; \
   is_keyword(open, "endblock") -> true; \
+  is_keyword(open, "language") -> true; \
+  is_keyword(open, "endlanguage") -> true; \
   is_keyword(open, "comment") -> true; \
   is_keyword(open, "comment") -> true; \
   is_keyword(open, "endcomment") -> true; \
   is_keyword(open, "endcomment") -> true; \
   is_keyword(open, "cycle") -> true; \
   is_keyword(open, "cycle") -> true; \

+ 306 - 0
src/erlydtl_time_compat.erl

@@ -0,0 +1,306 @@
+%%
+%% %CopyrightBegin%
+%% 
+%% Copyright Ericsson AB 2014-2015. All Rights Reserved.
+%% 
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%%     http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%% 
+%% %CopyrightEnd%
+%%
+
+%%
+%% If your code need to be able to execute on ERTS versions both
+%% earlier and later than 7.0, the best approach is to use the new
+%% time API introduced in ERTS 7.0 and implement a fallback
+%% solution using the old primitives to be used on old ERTS
+%% versions. This way your code can automatically take advantage
+%% of the improvements in the API when available. This is an
+%% example of how to implement such an API, but it can be used
+%% as is if you want to. Just add (a preferrably renamed version of)
+%% this module to your project, and call the API via this module
+%% instead of calling the BIFs directly.
+%%
+
+%% use own name to avoid conflicts..
+-module(erlydtl_time_compat).
+
+%% We don't want warnings about the use of erlang:now/0 in
+%% this module.
+-compile(nowarn_deprecated_function).
+%%
+%% We don't use
+%%   -compile({nowarn_deprecated_function, [{erlang, now, 0}]}).
+%% since this will produce warnings when compiled on systems
+%% where it has not yet been deprecated.
+%%
+
+-export([monotonic_time/0,
+	 monotonic_time/1,
+	 erlang_system_time/0,
+	 erlang_system_time/1,
+	 os_system_time/0,
+	 os_system_time/1,
+	 time_offset/0,
+	 time_offset/1,
+	 convert_time_unit/3,
+	 timestamp/0,
+	 unique_integer/0,
+	 unique_integer/1,
+	 monitor/2,
+	 system_info/1,
+	 system_flag/2]).
+
+monotonic_time() ->
+    try
+	erlang:monotonic_time()
+    catch
+	error:undef ->
+	    %% Use Erlang system time as monotonic time
+	    erlang_system_time_fallback()
+    end.
+
+monotonic_time(Unit) ->
+    try
+	erlang:monotonic_time(Unit)
+    catch
+	error:badarg ->
+	    erlang:error(badarg, [Unit]);
+	error:undef ->
+	    %% Use Erlang system time as monotonic time
+	    STime = erlang_system_time_fallback(),
+	    try
+		convert_time_unit_fallback(STime, native, Unit)
+	    catch
+		error:bad_time_unit -> erlang:error(badarg, [Unit])
+	    end
+    end.
+
+erlang_system_time() ->
+    try
+	erlang:system_time()
+    catch
+	error:undef ->
+	    erlang_system_time_fallback()
+    end.
+
+erlang_system_time(Unit) ->
+    try
+	erlang:system_time(Unit)
+    catch
+	error:badarg ->
+	    erlang:error(badarg, [Unit]);
+	error:undef ->
+	    STime = erlang_system_time_fallback(),
+	    try
+		convert_time_unit_fallback(STime, native, Unit)
+	    catch
+		error:bad_time_unit -> erlang:error(badarg, [Unit])
+	    end
+    end.
+
+os_system_time() ->
+    try
+	os:system_time()
+    catch
+	error:undef ->
+	    os_system_time_fallback()
+    end.
+
+os_system_time(Unit) ->
+    try
+	os:system_time(Unit)
+    catch
+	error:badarg ->
+	    erlang:error(badarg, [Unit]);
+	error:undef ->
+	    STime = os_system_time_fallback(),
+	    try
+		convert_time_unit_fallback(STime, native, Unit)
+	    catch
+		error:bad_time_unit -> erlang:error(badarg, [Unit])
+	    end
+    end.
+
+time_offset() ->
+    try
+	erlang:time_offset()
+    catch
+	error:undef ->
+	    %% Erlang system time and Erlang monotonic
+	    %% time are always aligned
+	    0
+    end.
+
+time_offset(Unit) ->
+    try
+	erlang:time_offset(Unit)
+    catch
+	error:badarg ->
+	    erlang:error(badarg, [Unit]);
+	error:undef ->
+	    try
+		_ = integer_time_unit(Unit)
+	    catch
+		error:bad_time_unit -> erlang:error(badarg, [Unit])
+	    end,
+	    %% Erlang system time and Erlang monotonic
+	    %% time are always aligned
+	    0
+    end.
+
+convert_time_unit(Time, FromUnit, ToUnit) ->
+    try
+	erlang:convert_time_unit(Time, FromUnit, ToUnit)
+    catch
+	error:undef ->
+	    try
+		convert_time_unit_fallback(Time, FromUnit, ToUnit)
+	    catch
+		_:_ ->
+		    erlang:error(badarg, [Time, FromUnit, ToUnit])
+	    end;
+	error:Error ->
+	    erlang:error(Error, [Time, FromUnit, ToUnit])
+    end.
+
+timestamp() ->
+    try
+	erlang:timestamp()
+    catch
+	error:undef ->
+	    erlang:now()
+    end.
+
+unique_integer() ->
+    try
+	erlang:unique_integer()
+    catch
+	error:undef ->
+	    {MS, S, US} = erlang:now(),
+	    (MS*1000000+S)*1000000+US
+    end.
+
+unique_integer(Modifiers) ->
+    try
+	erlang:unique_integer(Modifiers)
+    catch
+	error:badarg ->
+	    erlang:error(badarg, [Modifiers]);
+	error:undef ->
+	    case is_valid_modifier_list(Modifiers) of
+		true ->
+		    %% now() converted to an integer
+		    %% fullfill the requirements of
+		    %% all modifiers: unique, positive,
+		    %% and monotonic...
+		    {MS, S, US} = erlang:now(),
+		    (MS*1000000+S)*1000000+US;
+		false ->
+		    erlang:error(badarg, [Modifiers])
+	    end
+    end.
+
+monitor(Type, Item) ->
+    try
+	erlang:monitor(Type, Item)
+    catch
+	error:Error ->
+	    case {Error, Type, Item} of
+		{badarg, time_offset, clock_service} ->
+		    %% Time offset is final and will never change.
+		    %% Return a dummy reference, there will never
+		    %% be any need for 'CHANGE' messages...
+		    make_ref();
+		_ ->
+		    erlang:error(Error, [Type, Item])
+	    end
+    end.
+
+system_info(Item) ->
+    try
+	erlang:system_info(Item)
+    catch
+	error:badarg ->
+	    case Item of
+		time_correction ->
+		    case erlang:system_info(tolerant_timeofday) of
+			enabled -> true;
+			disabled -> false
+		    end;
+		time_warp_mode ->
+		    no_time_warp;
+		time_offset ->
+		    final;
+		NotSupArg when NotSupArg == os_monotonic_time_source;
+			       NotSupArg == os_system_time_source;
+			       NotSupArg == start_time;
+			       NotSupArg == end_time ->
+		    %% Cannot emulate this...
+		    erlang:error(notsup, [NotSupArg]);
+		_ ->
+		    erlang:error(badarg, [Item])
+	    end;
+	error:Error ->
+	    erlang:error(Error, [Item])
+    end.
+
+system_flag(Flag, Value) ->
+    try
+	erlang:system_flag(Flag, Value)
+    catch
+	error:Error ->
+	    case {Error, Flag, Value} of
+		{badarg, time_offset, finalize} ->
+		    %% Time offset is final
+		    final;
+		_ ->
+		    erlang:error(Error, [Flag, Value])
+	    end
+    end.
+
+%%
+%% Internal functions
+%%
+
+integer_time_unit(native) -> 1000*1000;
+integer_time_unit(nano_seconds) -> 1000*1000*1000;
+integer_time_unit(micro_seconds) -> 1000*1000;
+integer_time_unit(milli_seconds) -> 1000;
+integer_time_unit(seconds) -> 1;
+integer_time_unit(I) when is_integer(I), I > 0 -> I;
+integer_time_unit(BadRes) -> erlang:error(bad_time_unit, [BadRes]).
+
+erlang_system_time_fallback() ->
+    {MS, S, US} = erlang:now(),
+    (MS*1000000+S)*1000000+US.
+
+os_system_time_fallback() ->
+    {MS, S, US} = os:timestamp(),
+    (MS*1000000+S)*1000000+US.
+
+convert_time_unit_fallback(Time, FromUnit, ToUnit) ->
+    FU = integer_time_unit(FromUnit),
+    TU = integer_time_unit(ToUnit),
+    case Time < 0 of
+	true -> TU*Time - (FU - 1);
+	false -> TU*Time
+    end div FU.
+
+is_valid_modifier_list([positive|Ms]) ->
+    is_valid_modifier_list(Ms);
+is_valid_modifier_list([monotonic|Ms]) ->
+    is_valid_modifier_list(Ms);
+is_valid_modifier_list([]) ->
+    true;
+is_valid_modifier_list(_) ->
+    false.

+ 137 - 109
src/erlydtl_unparser.erl

@@ -1,129 +1,153 @@
 -module(erlydtl_unparser).
 -module(erlydtl_unparser).
--export([unparse/1]).
+-export([unparse/1, unparse/2]).
+
+unparse(DjangoParseTree, undefined) ->
+    unparse(DjangoParseTree);
+unparse(DjangoParseTree, true) ->
+    Text = unparse(DjangoParseTree),
+    Trimmed = re:replace(Text, <<"(^\\s+)|(\\s+$)|\n">>, <<"">>, [global, multiline]),
+    Joined = join_iolist(Trimmed, " "),
+    binary_to_list(iolist_to_binary(Joined)).
+
+join_iolist(IOList, Sep) ->
+    join_iolist(IOList, Sep, []).
+
+join_iolist([[]|IOList], Sep, Acc) ->
+    join_iolist(IOList, Sep, Acc);
+join_iolist([Data|IOList], Sep, Acc) ->
+    join_iolist(IOList, Sep, [Sep, Data|Acc]);
+join_iolist([], _, [_|Acc]) ->
+    lists:reverse(Acc);
+join_iolist(IOList, _, Acc) ->
+    lists:reverse([IOList|Acc]).
+
 
 
 unparse(DjangoParseTree) ->
 unparse(DjangoParseTree) ->
-    unparse(DjangoParseTree, []).
+    do_unparse(DjangoParseTree).
+
+do_unparse(DjangoParseTree) ->
+    do_unparse(DjangoParseTree, []).
 
 
-unparse([], Acc) ->
+do_unparse([], Acc) ->
     lists:flatten(lists:reverse(Acc));
     lists:flatten(lists:reverse(Acc));
-unparse([{'extends', Value}|Rest], Acc) ->
-    unparse(Rest, [["{% extends ", unparse_value(Value), " %}"]|Acc]);
-unparse([{'autoescape', OnOrOff, Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% autoescape ", unparse_identifier(OnOrOff), " %}", unparse(Contents), "{% endautoescape %}"]|Acc]);
-unparse([{'block', Identifier, Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% block ", unparse_identifier(Identifier), " %}", unparse(Contents), "{% endblock %}"]|Acc]);
-unparse([{'blocktrans', Args, Contents, undefined}|Rest], Acc) ->
-    unparse(Rest, [["{% blocktrans ", unparse_blocktrans_args(Args), "%}", unparse(Contents), "{% endblocktrans %}"]|Acc]);
-unparse([{'blocktrans', Args, Contents, PluralContents}|Rest], Acc) ->
-    unparse(Rest, [["{% blocktrans ", unparse_args(Args), " %}",
-                    unparse(Contents),
+do_unparse([{'extends', Value}|Rest], Acc) ->
+    do_unparse(Rest, [["{% extends ", unparse_value(Value), " %}"]|Acc]);
+do_unparse([{'autoescape', OnOrOff, Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% autoescape ", unparse_identifier(OnOrOff), " %}", do_unparse(Contents), "{% endautoescape %}"]|Acc]);
+do_unparse([{'block', Identifier, Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% block ", unparse_identifier(Identifier), " %}", do_unparse(Contents), "{% endblock %}"]|Acc]);
+do_unparse([{'blocktrans', Args, Contents, undefined}|Rest], Acc) ->
+    do_unparse(Rest, [["{% blocktrans ", unparse_blocktrans_args(Args), " %}", do_unparse(Contents), "{% endblocktrans %}"]|Acc]);
+do_unparse([{'blocktrans', Args, Contents, PluralContents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% blocktrans ", unparse_blocktrans_args(Args), " %}",
+                    do_unparse(Contents),
                     "{% plural %}",
                     "{% plural %}",
-                    unparse(PluralContents),
+                    do_unparse(PluralContents),
                     "{% endblocktrans %}"]|Acc]);
                     "{% endblocktrans %}"]|Acc]);
-unparse([{'call', Identifier}|Rest], Acc) ->
-    unparse(Rest, [["{% call ", unparse_identifier(Identifier), " %}"]|Acc]);
-unparse([{'call', Identifier, With}|Rest], Acc) ->
-    unparse(Rest, [["{% call ", unparse_identifier(Identifier), " with ", unparse_args(With), " %}"]|Acc]);
-unparse([{'comment', Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% comment %}", unparse(Contents), "{% endcomment %}"]|Acc]);
-unparse([{'comment_tag', _Pos, Text}|Rest], Acc) ->
-    unparse(Rest, [["{#", Text, "#}"]|Acc]);
-unparse([{'cycle', Names}|Rest], Acc) ->
-    unparse(Rest, [["{% cycle ", unparse(Names), " %}"]|Acc]);
-unparse([{'cycle_compat', Names}|Rest], Acc) ->
-    unparse(Rest, [["{% cycle ", unparse_cycle_compat_names(Names), " %}"]|Acc]);
-unparse([{'date', 'now', Value}|Rest], Acc) ->
-    unparse(Rest, [["{% now ", unparse_value(Value), " %}"]|Acc]);
-unparse([{'filter', FilterList, Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% filter ", unparse_filters(FilterList), " %}", unparse(Contents), "{% endfilter %}"]|Acc]);
-unparse([{'firstof', Vars}|Rest], Acc) ->
-    unparse(Rest, [["{% firstof ", unparse(Vars), " %}"]|Acc]);
-unparse([{'for', {'in', IteratorList, Identifier}, Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% for ", unparse_identifier(Identifier), " in ", unparse(IteratorList), " %}",
-                    unparse(Contents),
+do_unparse([{'call', Identifier}|Rest], Acc) ->
+    do_unparse(Rest, [["{% call ", unparse_identifier(Identifier), " %}"]|Acc]);
+do_unparse([{'call', Identifier, With}|Rest], Acc) ->
+    do_unparse(Rest, [["{% call ", unparse_identifier(Identifier), " with ", unparse_args(With), " %}"]|Acc]);
+do_unparse([{'comment', Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% comment %}", do_unparse(Contents), "{% endcomment %}"]|Acc]);
+do_unparse([{'comment_tag', _Pos, Text}|Rest], Acc) ->
+    do_unparse(Rest, [["{#", Text, "#}"]|Acc]);
+do_unparse([{'cycle', Names}|Rest], Acc) ->
+    do_unparse(Rest, [["{% cycle ", do_unparse(Names), " %}"]|Acc]);
+do_unparse([{'cycle_compat', Names}|Rest], Acc) ->
+    do_unparse(Rest, [["{% cycle ", unparse_cycle_compat_names(Names), " %}"]|Acc]);
+do_unparse([{'date', 'now', Value}|Rest], Acc) ->
+    do_unparse(Rest, [["{% now ", unparse_value(Value), " %}"]|Acc]);
+do_unparse([{'filter', FilterList, Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% filter ", unparse_filters(FilterList), " %}", do_unparse(Contents), "{% endfilter %}"]|Acc]);
+do_unparse([{'firstof', Vars}|Rest], Acc) ->
+    do_unparse(Rest, [["{% firstof ", do_unparse(Vars), " %}"]|Acc]);
+do_unparse([{'for', {'in', IteratorList, Identifier}, Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% for ", unparse_identifier(Identifier), " in ", do_unparse(IteratorList), " %}",
+                    do_unparse(Contents),
                     "{% endfor %}"]|Acc]);
                     "{% endfor %}"]|Acc]);
-unparse([{'for', {'in', IteratorList, Identifier}, Contents, EmptyPartsContents}|Rest], Acc) ->
-    unparse(Rest, [["{% for ", unparse_identifier(Identifier), " in ", unparse(IteratorList), " %}",
-                    unparse(Contents),
+do_unparse([{'for', {'in', IteratorList, Identifier}, Contents, EmptyPartsContents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% for ", unparse_identifier(Identifier), " in ", do_unparse(IteratorList), " %}",
+                    do_unparse(Contents),
                     "{% empty %}",
                     "{% empty %}",
-                    unparse(EmptyPartsContents),
+                    do_unparse(EmptyPartsContents),
                     "{% endfor %}"]|Acc]);
                     "{% endfor %}"]|Acc]);
-unparse([{'if', Expression, Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% if ", unparse_expression(Expression), " %}",
-                    unparse(Contents),
+do_unparse([{'if', Expression, Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% if ", unparse_expression(Expression), " %}",
+                    do_unparse(Contents),
                     "{% endif %}"]|Acc]);
                     "{% endif %}"]|Acc]);
-unparse([{'ifchanged', Expression, IfContents}|Rest], Acc) ->
-    unparse(Rest, [["{% ifchanged ", unparse_expression(Expression), " %}",
-                    unparse(IfContents),
+do_unparse([{'ifchanged', Expression, IfContents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% ifchanged ", unparse_expression(Expression), " %}",
+                    do_unparse(IfContents),
                     "{% endifchanged %}"]|Acc]);
                     "{% endifchanged %}"]|Acc]);
-unparse([{'ifchangedelse', Expression, IfContents, ElseContents}|Rest], Acc) ->
-    unparse(Rest, [["{% ifchanged ", unparse_expression(Expression), " %}",
-                    unparse(IfContents),
+do_unparse([{'ifchangedelse', Expression, IfContents, ElseContents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% ifchanged ", unparse_expression(Expression), " %}",
+                    do_unparse(IfContents),
                     "{% else %}",
                     "{% else %}",
-                    unparse(ElseContents),
+                    do_unparse(ElseContents),
                     "{% endifchanged %}"]|Acc]);
                     "{% endifchanged %}"]|Acc]);
-unparse([{'ifelse', Expression, IfContents, ElseContents}|Rest], Acc) ->
-    unparse(Rest, [["{% if ", unparse_expression(Expression), " %}",
-                    unparse(IfContents),
+do_unparse([{'ifelse', Expression, IfContents, ElseContents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% if ", unparse_expression(Expression), " %}",
+                    do_unparse(IfContents),
                     "{% else %}",
                     "{% else %}",
-                    unparse(ElseContents),
+                    do_unparse(ElseContents),
                     "{% endif %}"]|Acc]);
                     "{% endif %}"]|Acc]);
-unparse([{'ifequal', [Arg1, Arg2], Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% ifequal ", unparse_value(Arg1), " ", unparse_value(Arg2), " %}",
-                    unparse(Contents),
+do_unparse([{'ifequal', [Arg1, Arg2], Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% ifequal ", unparse_value(Arg1), " ", unparse_value(Arg2), " %}",
+                    do_unparse(Contents),
                     "{% endifequal %}"]|Acc]);
                     "{% endifequal %}"]|Acc]);
-unparse([{'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}|Rest], Acc) ->
-    unparse(Rest, [["{% ifequal ", unparse_value(Arg1), " ", unparse_value(Arg2), " %}",
-                    unparse(IfContents),
+do_unparse([{'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% ifequal ", unparse_value(Arg1), " ", unparse_value(Arg2), " %}",
+                    do_unparse(IfContents),
                     "{% else %}",
                     "{% else %}",
-                    unparse(ElseContents),
+                    do_unparse(ElseContents),
                     "{% endifequal %}"]|Acc]);
                     "{% endifequal %}"]|Acc]);
-unparse([{'ifnotequal', [Arg1, Arg2], Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% ifnotequal ", unparse_value(Arg1), " ", unparse_value(Arg2), " %}",
-                    unparse(Contents),
+do_unparse([{'ifnotequal', [Arg1, Arg2], Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% ifnotequal ", unparse_value(Arg1), " ", unparse_value(Arg2), " %}",
+                    do_unparse(Contents),
                     "{% endifnotequal %}"]|Acc]);
                     "{% endifnotequal %}"]|Acc]);
-unparse([{'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}|Rest], Acc) ->
-    unparse(Rest, [["{% ifnotequal ", unparse_value(Arg1), " ", unparse_value(Arg2), " %}",
-                    unparse(IfContents),
+do_unparse([{'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% ifnotequal ", unparse_value(Arg1), " ", unparse_value(Arg2), " %}",
+                    do_unparse(IfContents),
                     "{% else %}",
                     "{% else %}",
-                    unparse(ElseContents),
+                    do_unparse(ElseContents),
                     "{% endifnotequal %}"]|Acc]);
                     "{% endifnotequal %}"]|Acc]);
-unparse([{'include', Value, []}|Rest], Acc) ->
-    unparse(Rest, [["{% include ", unparse_value(Value), " %}"]|Acc]);
-unparse([{'include', Value, Args}|Rest], Acc) ->
-    unparse(Rest, [["{% include ", unparse_value(Value), " with ", unparse_args(Args)]|Acc]);
-unparse([{'include_only', Value, []}|Rest], Acc) ->
-    unparse(Rest, [["{% include ", unparse_value(Value), " only %}"]|Acc]);
-unparse([{'include_only', Value, Args}|Rest], Acc) ->
-    unparse(Rest, [["{% include ", unparse_value(Value), " with ", unparse_args(Args), " only %}"]|Acc]);
-unparse([{'regroup', {Variable, Identifier1, Identifier2}, Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% regroup ", unparse_value(Variable), " by ", unparse_identifier(Identifier1), " as ", unparse_identifier(Identifier2), " %}",
-                    unparse(Contents),
+do_unparse([{'include', Value, []}|Rest], Acc) ->
+    do_unparse(Rest, [["{% include ", unparse_value(Value), " %}"]|Acc]);
+do_unparse([{'include', Value, Args}|Rest], Acc) ->
+    do_unparse(Rest, [["{% include ", unparse_value(Value), " with ", unparse_args(Args)]|Acc]);
+do_unparse([{'include_only', Value, []}|Rest], Acc) ->
+    do_unparse(Rest, [["{% include ", unparse_value(Value), " only %}"]|Acc]);
+do_unparse([{'include_only', Value, Args}|Rest], Acc) ->
+    do_unparse(Rest, [["{% include ", unparse_value(Value), " with ", unparse_args(Args), " only %}"]|Acc]);
+do_unparse([{'regroup', {Variable, Identifier1, Identifier2}, Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% regroup ", unparse_value(Variable), " by ", unparse_identifier(Identifier1), " as ", unparse_identifier(Identifier2), " %}",
+                    do_unparse(Contents),
                     "{% endregroup %}"]|Acc]);
                     "{% endregroup %}"]|Acc]);
-unparse([{'spaceless', Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% spaceless %}", unparse(Contents), "{% endspaceless %}"]|Acc]);
-unparse([{'ssi', Arg}|Rest], Acc) ->
-    unparse(Rest, [["{% ssi ", unparse_value(Arg), " %}"]|Acc]);
-unparse([{'ssi_parsed', Arg}|Rest], Acc) ->
-    unparse(Rest, [["{% ssi ", unparse_value(Arg), " parsed %}"]|Acc]);
-unparse([{'string', _, String}|Rest], Acc) ->
-    unparse(Rest, [[String]|Acc]);
-unparse([{'tag', Identifier, []}|Rest], Acc) ->
-    unparse(Rest, [["{% ", unparse_identifier(Identifier), " %}"]|Acc]);
-unparse([{'tag', Identifier, Args}|Rest], Acc) ->
-    unparse(Rest, [["{% ", unparse_identifier(Identifier), " ", unparse_args(Args), " %}"]|Acc]);
-unparse([{'templatetag', Identifier}|Rest], Acc) ->
-    unparse(Rest, [["{% templatetag ", unparse_identifier(Identifier), " %}"]|Acc]);
-unparse([{'trans', Value}|Rest], Acc) ->
-    unparse(Rest, [["{% trans ", unparse_value(Value), " %}"]|Acc]);
-unparse([{'widthratio', Numerator, Denominator, Scale}|Rest], Acc) ->
-    unparse(Rest, [["{% widthratio ", unparse_value(Numerator), " ", unparse_value(Denominator), " ", unparse_value(Scale), " %}"]|Acc]);
-unparse([{'with', Args, Contents}|Rest], Acc) ->
-    unparse(Rest, [["{% with ", unparse_args(Args), " %}",
-                    unparse(Contents),
+do_unparse([{'spaceless', Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% spaceless %}", do_unparse(Contents), "{% endspaceless %}"]|Acc]);
+do_unparse([{'ssi', Arg}|Rest], Acc) ->
+    do_unparse(Rest, [["{% ssi ", unparse_value(Arg), " %}"]|Acc]);
+do_unparse([{'ssi_parsed', Arg}|Rest], Acc) ->
+    do_unparse(Rest, [["{% ssi ", unparse_value(Arg), " parsed %}"]|Acc]);
+do_unparse([{'string', _, String}|Rest], Acc) ->
+    do_unparse(Rest, [[String]|Acc]);
+do_unparse([{'tag', Identifier, []}|Rest], Acc) ->
+    do_unparse(Rest, [["{% ", unparse_identifier(Identifier), " %}"]|Acc]);
+do_unparse([{'tag', Identifier, Args}|Rest], Acc) ->
+    do_unparse(Rest, [["{% ", unparse_identifier(Identifier), " ", unparse_args(Args), " %}"]|Acc]);
+do_unparse([{'templatetag', Identifier}|Rest], Acc) ->
+    do_unparse(Rest, [["{% templatetag ", unparse_identifier(Identifier), " %}"]|Acc]);
+do_unparse([{'trans', Value}|Rest], Acc) ->
+    do_unparse(Rest, [["{% trans ", unparse_value(Value), " %}"]|Acc]);
+do_unparse([{'widthratio', Numerator, Denominator, Scale}|Rest], Acc) ->
+    do_unparse(Rest, [["{% widthratio ", unparse_value(Numerator), " ", unparse_value(Denominator), " ", unparse_value(Scale), " %}"]|Acc]);
+do_unparse([{'with', Args, Contents}|Rest], Acc) ->
+    do_unparse(Rest, [["{% with ", unparse_args(Args), " %}",
+                    do_unparse(Contents),
                     "{% endwidth %}"]|Acc]);
                     "{% endwidth %}"]|Acc]);
-unparse([ValueToken|Rest], Acc) ->
-    unparse(Rest, [["{{ ", unparse_value(ValueToken), " }}"]|Acc]).
+do_unparse([ValueToken|Rest], Acc) ->
+    do_unparse(Rest, [["{{ ", unparse_value(ValueToken), " }}"]|Acc]).
 
 
 
 
 unparse_identifier({identifier, _, Name}) ->
 unparse_identifier({identifier, _, Name}) ->
@@ -184,11 +208,9 @@ unparse_args(Args) ->
     unparse_args(Args, []).
     unparse_args(Args, []).
 
 
 unparse_args([], Acc) ->
 unparse_args([], Acc) ->
-    lists:reverse(Acc);
-unparse_args([{{identifier, _, Name}, Value}], Acc) ->
-    unparse_args([], [[atom_to_list(Name), "=", unparse_value(Value)]|Acc]);
-unparse_args([{{identifier, _, Name}, Value}|Rest], Acc) ->
-    unparse_args(Rest, lists:reverse([[atom_to_list(Name), "=", unparse_value(Value)], " "], Acc)).
+    collect_args_acc(Acc);
+unparse_args([{{identifier, _, Name}, Value}|Args], Acc) ->
+    unparse_args(Args, [[atom_to_list(Name), "=", unparse_value(Value)]|Acc]).
 
 
 unparse_cycle_compat_names(Names) ->
 unparse_cycle_compat_names(Names) ->
     unparse_cycle_compat_names(Names, []).
     unparse_cycle_compat_names(Names, []).
@@ -203,6 +225,8 @@ unparse_cycle_compat_names([{identifier, _, Name}|Rest], Acc) ->
 unparse_blocktrans_args(Args) ->
 unparse_blocktrans_args(Args) ->
     unparse_blocktrans_args(Args, []).
     unparse_blocktrans_args(Args, []).
 
 
+unparse_blocktrans_args([], Acc) ->
+    collect_args_acc(Acc);
 unparse_blocktrans_args([{args, WithArgs}|Args], Acc) ->
 unparse_blocktrans_args([{args, WithArgs}|Args], Acc) ->
     unparse_blocktrans_args(
     unparse_blocktrans_args(
       Args, [["with ", unparse_args(WithArgs)]|Acc]);
       Args, [["with ", unparse_args(WithArgs)]|Acc]);
@@ -212,5 +236,9 @@ unparse_blocktrans_args([{count, Count}|Args], Acc) ->
 unparse_blocktrans_args([{context, Context}|Args], Acc) ->
 unparse_blocktrans_args([{context, Context}|Args], Acc) ->
     unparse_blocktrans_args(
     unparse_blocktrans_args(
       Args, [["context ", unparse_value(Context)]|Acc]);
       Args, [["context ", unparse_value(Context)]|Acc]);
-unparse_blocktrans_args([], Acc) ->
-    lists:reverse(Acc).
+unparse_blocktrans_args([trimmed|Args], Acc) ->
+    unparse_blocktrans_args(
+      Args, ["trimmed"|Acc]).
+
+collect_args_acc(Acc) ->
+    lists:flatten(string:join(lists:reverse(Acc), " ")).

+ 139 - 88
src/filter_lib/erlydtl_dateformat.erl

@@ -1,5 +1,5 @@
 -module(erlydtl_dateformat).
 -module(erlydtl_dateformat).
--export([format/1, format/2]).
+-export([format/1, format/2, format/3, format/4]).
 
 
 -define(TAG_SUPPORTED(C),
 -define(TAG_SUPPORTED(C),
         C =:= $a orelse
         C =:= $a orelse
@@ -9,6 +9,7 @@
         C =:= $c orelse
         C =:= $c orelse
         C =:= $d orelse
         C =:= $d orelse
         C =:= $D orelse
         C =:= $D orelse
+        C =:= $E orelse
         C =:= $f orelse
         C =:= $f orelse
         C =:= $F orelse
         C =:= $F orelse
         C =:= $g orelse
         C =:= $g orelse
@@ -45,41 +46,74 @@
 %% Format the current date/time
 %% Format the current date/time
 %%
 %%
 format(FormatString) when is_binary(FormatString) ->
 format(FormatString) when is_binary(FormatString) ->
-    format(binary_to_list(FormatString));
+    format(binary_to_list(FormatString), fun stub_tran/2, <<>>);
 format(FormatString) ->
 format(FormatString) ->
     {Date, Time} = erlang:localtime(),
     {Date, Time} = erlang:localtime(),
-    replace_tags(Date, Time, FormatString).
+    replace_tags(Date, Time, FormatString, fun stub_tran/2, <<>>).
+
 %%
 %%
 %% Format a tuple of the form {{Y,M,D},{H,M,S}}
 %% Format a tuple of the form {{Y,M,D},{H,M,S}}
 %% This is the format returned by erlang:localtime()
 %% This is the format returned by erlang:localtime()
 %% and other standard date/time BIFs
 %% and other standard date/time BIFs
 %%
 %%
 format(DateTime, FormatString) when is_binary(FormatString) ->
 format(DateTime, FormatString) when is_binary(FormatString) ->
-    format(DateTime, binary_to_list(FormatString));
+    format(DateTime, binary_to_list(FormatString),fun stub_tran/2, <<>>);
 format({{_,_,_} = Date,{_,_,_} = Time}, FormatString) ->
 format({{_,_,_} = Date,{_,_,_} = Time}, FormatString) ->
-    replace_tags(Date, Time, FormatString);
+    replace_tags(Date, Time, FormatString, fun stub_tran/2, <<>> );
 %%
 %%
 %% Format a tuple of the form {Y,M,D}
 %% Format a tuple of the form {Y,M,D}
 %%
 %%
 format({_,_,_} = Date, FormatString) ->
 format({_,_,_} = Date, FormatString) ->
-    replace_tags(Date, {0,0,0}, FormatString);
+    replace_tags(Date, {0,0,0}, FormatString, fun stub_tran/2, <<>> );
 format(DateTime, FormatString) ->
 format(DateTime, FormatString) ->
     io:format("Unrecognised date paramater : ~p~n", [DateTime]),
     io:format("Unrecognised date paramater : ~p~n", [DateTime]),
     FormatString.
     FormatString.
 
 
-replace_tags(Date, Time, Input) ->
-    replace_tags(Date, Time, Input, [], noslash).
-replace_tags(_Date, _Time, [], Out, _State) ->
+%% The same set of functions with TranslationFunction and Locale args
+%% Translation function may be 'none' atom - handle this separately
+%% replacing atom with a stub function (it's easier to do it this way)
+format(FormatString, TransFun, Locale) when is_binary(FormatString) ->
+    format(binary_to_list(FormatString), TransFun, Locale);
+format(FormatString, none, _Locale) -> 
+    format(FormatString, fun stub_tran/2, <<>>);
+format(FormatString, TransFun, Locale) ->
+    {Date, Time} = erlang:localtime(),
+    replace_tags(Date, Time, FormatString, TransFun, Locale).
+
+format(DateTime, FormatString, TransFun, Locale) when is_binary(FormatString) ->
+    format(DateTime, binary_to_list(FormatString), TransFun, Locale);
+format(DateTime, FormatString, none, _Locale) ->
+    format(DateTime, FormatString, fun stub_tran/2, <<>>);
+format({{_,_,_} = Date,{_,_,_} = Time}, FormatString, TransFun, Locale) ->
+    replace_tags(Date, Time, FormatString, TransFun, Locale );
+
+format({_,_,_} = Date, FormatString, none, _Locale) ->
+    replace_tags(Date, {0,0,0}, FormatString, fun stub_tran/2, <<>>);
+format({_,_,_} = Date, FormatString, TransFun, Locale) ->
+    replace_tags(Date, {0,0,0}, FormatString, TransFun, Locale);
+format(DateTime, FormatString, _TransFun, _Locale) ->
+    io:format("Unrecognised date paramater : ~p~n", [DateTime]),
+    FormatString.
+
+replace_tags(Date, Time, Input, TransFun, Locale) ->
+    replace_tags(Date, Time, Input, [], noslash, TransFun, Locale).
+replace_tags(_Date, _Time, [], Out, _State, _TransFun, _Locale) ->
     lists:reverse(Out);
     lists:reverse(Out);
-replace_tags(Date, Time, [C|Rest], Out, noslash) when ?TAG_SUPPORTED(C) ->
-    replace_tags(Date, Time, Rest,
-                 lists:reverse(tag_to_value(C, Date, Time)) ++ Out, noslash);
-replace_tags(Date, Time, [$\\|Rest], Out, noslash) ->
-    replace_tags(Date, Time, Rest, Out, slash);
-replace_tags(Date, Time, [C|Rest], Out, slash) ->
-    replace_tags(Date, Time, Rest, [C|Out], noslash);
-replace_tags(Date, Time, [C|Rest], Out, _State) ->
-    replace_tags(Date, Time, Rest, [C|Out], noslash).
+replace_tags(Date, Time, [C|Rest], Out, noslash, TransFun, Locale) when ?TAG_SUPPORTED(C) ->
+    case tag_to_value(C, Date, Time, TransFun, Locale) of
+        V when is_binary(V) -> replace_tags(Date, Time, Rest, 
+                                            [V] ++ Out, noslash, 
+                                            TransFun, Locale);
+        V when is_list(V) ->   replace_tags(Date, Time, Rest, 
+                                            lists:reverse(V) ++ Out, 
+                                            noslash, TransFun, Locale)
+    end;
+replace_tags(Date, Time, [$\\|Rest], Out, noslash, TransFun, Locale) ->
+    replace_tags(Date, Time, Rest, Out, slash, TransFun, Locale);
+replace_tags(Date, Time, [C|Rest], Out, slash, TransFun, Locale) ->
+    replace_tags(Date, Time, Rest, [C|Out], noslash, TransFun, Locale);
+replace_tags(Date, Time, [C|Rest], Out, _State, TransFun, Locale) ->
+    replace_tags(Date, Time, Rest, [C|Out], noslash, TransFun, Locale).
 
 
 
 
 %%-----------------------------------------------------------
 %%-----------------------------------------------------------
@@ -87,25 +121,29 @@ replace_tags(Date, Time, [C|Rest], Out, _State) ->
 %%-----------------------------------------------------------
 %%-----------------------------------------------------------
 
 
 %% 'a.m.' or 'p.m.'
 %% 'a.m.' or 'p.m.'
-tag_to_value($a, _, {H, _, _}) when H > 11 -> "p.m.";
-tag_to_value($a, _, _) -> "a.m.";
+tag_to_value($a, _, {H, _, _}, TransFun, Locale) when H > 11 -> 
+    TransFun("p.m.", Locale);
+tag_to_value($a, _, _, TransFun, Locale) -> 
+    TransFun("a.m.", Locale);
 
 
 %% 'AM' or 'PM'
 %% 'AM' or 'PM'
-tag_to_value($A, _, {H, _, _}) when H > 11 -> "PM";
-tag_to_value($A, _, _) -> "AM";
+tag_to_value($A, _, {H, _, _}, TransFun, Locale) when H > 11 -> 
+    TransFun("PM", Locale);
+tag_to_value($A, _, _, TransFun, Locale) -> 
+    TransFun("AM", Locale);
 
 
 %% Swatch Internet time
 %% Swatch Internet time
-tag_to_value($B, _, _) ->
+tag_to_value($B, _, _, _TransFun, _Locale) ->
     ""; %% NotImplementedError
     ""; %% NotImplementedError
 
 
 %% ISO 8601 Format.
 %% ISO 8601 Format.
-tag_to_value($c, Date, Time) ->
-    tag_to_value($Y, Date, Time) ++
-        "-" ++ tag_to_value($m, Date, Time) ++
-        "-" ++ tag_to_value($d, Date, Time) ++
-        "T" ++ tag_to_value($H, Date, Time) ++
-        ":" ++ tag_to_value($i, Date, Time) ++
-        ":" ++ tag_to_value($s, Date, Time);
+tag_to_value($c, Date, Time, TransFun, Locale) ->
+    tag_to_value($Y, Date, Time, TransFun, Locale) ++
+        "-" ++ tag_to_value($m, Date, Time, TransFun, Locale) ++
+        "-" ++ tag_to_value($d, Date, Time, TransFun, Locale) ++
+        "T" ++ tag_to_value($H, Date, Time, TransFun, Locale) ++
+        ":" ++ tag_to_value($i, Date, Time, TransFun, Locale) ++
+        ":" ++ tag_to_value($s, Date, Time, TransFun, Locale);
 
 
 %%
 %%
 %% Time, in 12-hour hours and minutes, with minutes
 %% Time, in 12-hour hours and minutes, with minutes
@@ -115,46 +153,48 @@ tag_to_value($c, Date, Time) ->
 %%
 %%
 %% Proprietary extension.
 %% Proprietary extension.
 %%
 %%
-tag_to_value($f, Date, {H, 0, S}) ->
+tag_to_value($f, Date, {H, 0, S}, TransFun, Locale) ->
     %% If min is zero then return the hour only
     %% If min is zero then return the hour only
-    tag_to_value($g, Date, {H, 0, S});
-tag_to_value($f, Date, Time) ->
+    tag_to_value($g, Date, {H, 0, S}, TransFun, Locale);
+tag_to_value($f, Date, Time, TransFun, Locale) ->
     %% Otherwise return hours and mins
     %% Otherwise return hours and mins
-    tag_to_value($g, Date, Time)
-        ++ ":" ++ tag_to_value($i, Date, Time);
+    tag_to_value($g, Date, Time, TransFun, Locale)
+        ++ ":" ++ tag_to_value($i, Date, Time, TransFun, Locale);
 
 
 %% Hour, 12-hour format without leading zeros; i.e. '1' to '12'
 %% Hour, 12-hour format without leading zeros; i.e. '1' to '12'
-tag_to_value($g, _, {H,_,_}) ->
+tag_to_value($g, _, {H,_,_}, _TransFun, _Locale) ->
     integer_to_list(hour_24to12(H));
     integer_to_list(hour_24to12(H));
 
 
 %% Hour, 24-hour format without leading zeros; i.e. '0' to '23'
 %% Hour, 24-hour format without leading zeros; i.e. '0' to '23'
-tag_to_value($G, _, {H,_,_}) ->
+tag_to_value($G, _, {H,_,_}, _TransFun, _Locale) ->
     integer_to_list(H);
     integer_to_list(H);
 
 
 %% Hour, 12-hour format; i.e. '01' to '12'
 %% Hour, 12-hour format; i.e. '01' to '12'
-tag_to_value($h, _, {H,_,_}) ->
+tag_to_value($h, _, {H,_,_}, _TransFun, _Locale) ->
     integer_to_list_zerofill(hour_24to12(H));
     integer_to_list_zerofill(hour_24to12(H));
 
 
 %% Hour, 24-hour format; i.e. '00' to '23'
 %% Hour, 24-hour format; i.e. '00' to '23'
-tag_to_value($H, _, {H,_,_}) ->
+tag_to_value($H, _, {H,_,_}, _TransFun, _Locale) ->
     integer_to_list_zerofill(H);
     integer_to_list_zerofill(H);
 
 
 %% Minutes; i.e. '00' to '59'
 %% Minutes; i.e. '00' to '59'
-tag_to_value($i, _, {_,M,_}) ->
+tag_to_value($i, _, {_,M,_}, _TransFun, _Locale) ->
     integer_to_list_zerofill(M);
     integer_to_list_zerofill(M);
 
 
 %% Time, in 12-hour hours, minutes and 'a.m.'/'p.m.', with minutes left off
 %% Time, in 12-hour hours, minutes and 'a.m.'/'p.m.', with minutes left off
 %% if they're zero and the strings 'midnight' and 'noon' if appropriate.
 %% if they're zero and the strings 'midnight' and 'noon' if appropriate.
 %% Examples: '1 a.m.', '1:30 p.m.', 'midnight', 'noon', '12:30 p.m.'
 %% Examples: '1 a.m.', '1:30 p.m.', 'midnight', 'noon', '12:30 p.m.'
 %% Proprietary extension.
 %% Proprietary extension.
-tag_to_value($P, _, {0,  0, _}) -> "midnight";
-tag_to_value($P, _, {12, 0, _}) -> "noon";
-tag_to_value($P, Date, Time) ->
-    tag_to_value($f, Date, Time)
-        ++ " " ++ tag_to_value($a, Date, Time);
+tag_to_value($P, _, {0,  0, _}, TransFun, Locale) -> 
+    TransFun("midnight", Locale);
+tag_to_value($P, _, {12, 0, _}, TransFun, Locale) -> 
+    TransFun("noon", Locale);
+tag_to_value($P, Date, Time, TransFun, Locale) ->
+    tag_to_value($f, Date, Time, TransFun, Locale)
+        ++ " " ++ tag_to_value($a, Date, Time, TransFun, Locale);
 
 
 %% Seconds; i.e. '00' to '59'
 %% Seconds; i.e. '00' to '59'
-tag_to_value($s, _, {_,_,S}) ->
+tag_to_value($s, _, {_,_,S}, _TransFun, _Locale) ->
     integer_to_list_zerofill(S);
     integer_to_list_zerofill(S);
 
 
 %%-----------------------------------------------------------
 %%-----------------------------------------------------------
@@ -162,67 +202,74 @@ tag_to_value($s, _, {_,_,S}) ->
 %%-----------------------------------------------------------
 %%-----------------------------------------------------------
 
 
 %% Month, textual, 3 letters, lowercase; e.g. 'jan'
 %% Month, textual, 3 letters, lowercase; e.g. 'jan'
-tag_to_value($b, {_,M,_}, _) ->
-    string:sub_string(monthname(M), 1, 3);
+tag_to_value($b, {_,M,_}, _, TransFun, Locale) ->
+    TransFun(string:sub_string(monthname(M), 1, 3), Locale);
 
 
 %% Day of the month, 2 digits with leading zeros; i.e. '01' to '31'
 %% Day of the month, 2 digits with leading zeros; i.e. '01' to '31'
-tag_to_value($d, {_, _, D}, _) ->
+tag_to_value($d, {_, _, D}, _, _TransFun, _Locale) ->
     integer_to_list_zerofill(D);
     integer_to_list_zerofill(D);
 
 
 %% Day of the week, textual, 3 letters; e.g. 'Fri'
 %% Day of the week, textual, 3 letters; e.g. 'Fri'
-tag_to_value($D, Date, _) ->
+tag_to_value($D, Date, _, TransFun, Locale) ->
     Dow = calendar:day_of_the_week(Date),
     Dow = calendar:day_of_the_week(Date),
-    ucfirst(string:sub_string(dayname(Dow), 1, 3));
+    TransFun(ucfirst(string:sub_string(dayname(Dow), 1, 3)), Locale);
+
+%% Month, textual, long, alternative; e.g. 'Listopada'
+tag_to_value($E, {_,M,_}, _, TransFun, Locale) ->
+    TransFun(ucfirst(monthname(M)), {Locale, <<"alt. month">>});
+
 
 
 %% Month, textual, long; e.g. 'January'
 %% Month, textual, long; e.g. 'January'
-tag_to_value($F, {_,M,_}, _) ->
-    ucfirst(monthname(M));
+tag_to_value($F, {_,M,_}, _, TransFun, Locale) ->
+    TransFun(ucfirst(monthname(M)), Locale);
 
 
 %% '1' if Daylight Savings Time, '0' otherwise.
 %% '1' if Daylight Savings Time, '0' otherwise.
-tag_to_value($I, _, _) ->
+tag_to_value($I, _, _, _TransFun, _Locale) ->
     "TODO";
     "TODO";
 
 
 %% Day of the month without leading zeros; i.e. '1' to '31'
 %% Day of the month without leading zeros; i.e. '1' to '31'
-tag_to_value($j, {_, _, D}, _) ->
+tag_to_value($j, {_, _, D}, _, _TransFun, _Locale) ->
     integer_to_list(D);
     integer_to_list(D);
 
 
 %% Day of the week, textual, long; e.g. 'Friday'
 %% Day of the week, textual, long; e.g. 'Friday'
-tag_to_value($l, Date, _) ->
-    ucfirst(dayname(calendar:day_of_the_week(Date)));
+tag_to_value($l, Date, _, TransFun, Locale) ->
+    TransFun(ucfirst(dayname(calendar:day_of_the_week(Date))), Locale);
 
 
 %% Boolean for whether it is a leap year; i.e. True or False
 %% Boolean for whether it is a leap year; i.e. True or False
-tag_to_value($L, {Y,_,_}, _) ->
+tag_to_value($L, {Y,_,_}, _, _TransFun, _Locale) ->
     case calendar:is_leap_year(Y) of
     case calendar:is_leap_year(Y) of
         true -> "True";
         true -> "True";
         _ -> "False"
         _ -> "False"
     end;
     end;
 
 
 %% Month; i.e. '01' to '12'
 %% Month; i.e. '01' to '12'
-tag_to_value($m, {_, M, _}, _) ->
+tag_to_value($m, {_, M, _}, _, _TransFun, _Locale) ->
     integer_to_list_zerofill(M);
     integer_to_list_zerofill(M);
 
 
 %% Month, textual, 3 letters; e.g. 'Jan'
 %% Month, textual, 3 letters; e.g. 'Jan'
-tag_to_value($M, {_,M,_}, _) ->
-    ucfirst(string:sub_string(monthname(M), 1, 3));
+tag_to_value($M, {_,M,_}, _, TransFun, Locale) ->
+    TransFun(ucfirst(string:sub_string(monthname(M), 1, 3)), Locale);
 
 
 %% Month without leading zeros; i.e. '1' to '12'
 %% Month without leading zeros; i.e. '1' to '12'
-tag_to_value($n, {_, M, _}, _) ->
+tag_to_value($n, {_, M, _}, _, _TransFun, _Locale) ->
     integer_to_list(M);
     integer_to_list(M);
 
 
 %% Month abbreviation in Associated Press style. Proprietary extension.
 %% Month abbreviation in Associated Press style. Proprietary extension.
-tag_to_value($N, {_,M,_}, _) when M =:= 9 ->
+tag_to_value($N, {_,M,_}, _, TransFun, Locale) when M =:= 9 ->
     %% Special case - "Sept."
     %% Special case - "Sept."
-    ucfirst(string:sub_string(monthname(M), 1, 4)) ++ ".";
-tag_to_value($N, {_,M,_}, _) when M < 3 orelse M > 7 ->
+    TransFun(ucfirst(string:sub_string(monthname(M), 1, 4)) ++ ".",
+             {Locale, <<"abbrev. month">>});
+tag_to_value($N, {_,M,_}, _, TransFun, Locale) when M < 3 orelse M > 7 ->
     %% Jan, Feb, Aug, Oct, Nov, Dec are all
     %% Jan, Feb, Aug, Oct, Nov, Dec are all
     %% abbreviated with a full-stop appended.
     %% abbreviated with a full-stop appended.
-    ucfirst(string:sub_string(monthname(M), 1, 3)) ++ ".";
-tag_to_value($N, {_,M,_}, _) ->
+    TransFun(ucfirst(string:sub_string(monthname(M), 1, 3)) ++ ".",
+                 {Locale, <<"abbrev. month">>});
+tag_to_value($N, {_,M,_}, _, TransFun, Locale) ->
     %% The rest are the fullname.
     %% The rest are the fullname.
-    ucfirst(monthname(M));
+    TransFun(ucfirst(monthname(M)), {Locale, <<"abbrev. month">>});
 
 
 %% Difference to Greenwich time in hours; e.g. '+0200'
 %% Difference to Greenwich time in hours; e.g. '+0200'
-tag_to_value($O, Date, Time) ->
+tag_to_value($O, Date, Time, _TransFun, _Locale) ->
     Diff = utc_diff(Date, Time),
     Diff = utc_diff(Date, Time),
     Offset = if
     Offset = if
                  Diff < 0 ->
                  Diff < 0 ->
@@ -233,67 +280,68 @@ tag_to_value($O, Date, Time) ->
     lists:flatten(Offset);
     lists:flatten(Offset);
 
 
 %% RFC 2822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'
 %% RFC 2822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'
-tag_to_value($r, Date, Time) ->
-    replace_tags(Date, Time, "D, j M Y H:i:s O");
+tag_to_value($r, Date, Time, _TransFun, _Locale) ->
+    % afaik, date should not be translated in case RFC format is specified.
+    replace_tags(Date, Time, "D, j M Y H:i:s O", fun stub_tran/2, <<>> );
 
 
 %% English ordinal suffix for the day of the month, 2 characters;
 %% English ordinal suffix for the day of the month, 2 characters;
 %% i.e. 'st', 'nd', 'rd' or 'th'
 %% i.e. 'st', 'nd', 'rd' or 'th'
-tag_to_value($S, {_, _, D}, _) when
+tag_to_value($S, {_, _, D}, _, _TransFun, _Locale) when
       D rem 100 =:= 11 orelse
       D rem 100 =:= 11 orelse
       D rem 100 =:= 12 orelse
       D rem 100 =:= 12 orelse
       D rem 100 =:= 13 -> "th";
       D rem 100 =:= 13 -> "th";
-tag_to_value($S, {_, _, D}, _) when D rem 10 =:= 1 -> "st";
-tag_to_value($S, {_, _, D}, _) when D rem 10 =:= 2 -> "nd";
-tag_to_value($S, {_, _, D}, _) when D rem 10 =:= 3 -> "rd";
-tag_to_value($S, _, _) -> "th";
+tag_to_value($S, {_, _, D}, _, _TransFun, _Locale) when D rem 10 =:= 1 -> "st";
+tag_to_value($S, {_, _, D}, _, _TransFun, _Locale) when D rem 10 =:= 2 -> "nd";
+tag_to_value($S, {_, _, D}, _, _TransFun, _Locale) when D rem 10 =:= 3 -> "rd";
+tag_to_value($S, _, _, _TransFun, _Locale) -> "th";
 
 
 %% Number of days in the given month; i.e. '28' to '31'
 %% Number of days in the given month; i.e. '28' to '31'
-tag_to_value($t, {Y,M,_}, _) ->
+tag_to_value($t, {Y,M,_}, _, _TransFun, _Locale) ->
     integer_to_list(calendar:last_day_of_the_month(Y,M));
     integer_to_list(calendar:last_day_of_the_month(Y,M));
 
 
 %% Time zone of this machine; e.g. 'EST' or 'MDT'
 %% Time zone of this machine; e.g. 'EST' or 'MDT'
-tag_to_value($T, _, _) ->
+tag_to_value($T, _, _, _TransFun, _Locale) ->
     "TODO";
     "TODO";
 
 
 %% Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)
 %% Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)
-tag_to_value($U, Date, Time) ->
+tag_to_value($U, Date, Time, _TransFun, _Locale) ->
     EpochSecs = calendar:datetime_to_gregorian_seconds({Date, Time})
     EpochSecs = calendar:datetime_to_gregorian_seconds({Date, Time})
         - calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}),
         - calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}),
     integer_to_list(EpochSecs);
     integer_to_list(EpochSecs);
 
 
 %% Day of the week, numeric, i.e. '0' (Sunday) to '6' (Saturday)
 %% Day of the week, numeric, i.e. '0' (Sunday) to '6' (Saturday)
-tag_to_value($w, Date, _) ->
+tag_to_value($w, Date, _, _TransFun, _Locale) ->
     %% Note: calendar:day_of_the_week returns
     %% Note: calendar:day_of_the_week returns
     %%   1 | .. | 7. Monday = 1, Tuesday = 2, ..., Sunday = 7
     %%   1 | .. | 7. Monday = 1, Tuesday = 2, ..., Sunday = 7
     integer_to_list(calendar:day_of_the_week(Date) rem 7);
     integer_to_list(calendar:day_of_the_week(Date) rem 7);
 
 
 %% ISO-8601 week number of year, weeks starting on Monday
 %% ISO-8601 week number of year, weeks starting on Monday
-tag_to_value($W, {Y,M,D}, _) ->
+tag_to_value($W, {Y,M,D}, _, _TransFun, _Locale) ->
     integer_to_list(year_weeknum(Y,M,D));
     integer_to_list(year_weeknum(Y,M,D));
 
 
 %% Year, 2 digits; e.g. '99'
 %% Year, 2 digits; e.g. '99'
-tag_to_value($y, {Y, _, _}, _) ->
+tag_to_value($y, {Y, _, _}, _, _TransFun, _Locale) ->
     string:sub_string(integer_to_list(Y), 3);
     string:sub_string(integer_to_list(Y), 3);
 
 
 %% Year, 4 digits; e.g. '1999'
 %% Year, 4 digits; e.g. '1999'
-tag_to_value($Y, {Y, _, _}, _) ->
+tag_to_value($Y, {Y, _, _}, _, _TransFun, _Locale) ->
     integer_to_list(Y);
     integer_to_list(Y);
 
 
 %% Day of the year; i.e. '0' to '365'
 %% Day of the year; i.e. '0' to '365'
-tag_to_value($z, {Y,M,D}, _) ->
+tag_to_value($z, {Y,M,D}, _, _TransFun, _Locale) ->
     integer_to_list(day_of_year(Y,M,D));
     integer_to_list(day_of_year(Y,M,D));
 
 
 %% Time zone offset in seconds (i.e. '-43200' to '43200'). The offset for
 %% Time zone offset in seconds (i.e. '-43200' to '43200'). The offset for
 %% timezones west of UTC is always negative, and for those east of UTC is
 %% timezones west of UTC is always negative, and for those east of UTC is
 %% always positive.
 %% always positive.
-tag_to_value($Z, _, _) ->
+tag_to_value($Z, _, _, _TransFun, _Locale) ->
     "TODO";
     "TODO";
 
 
 %% o – the ISO 8601 year number
 %% o – the ISO 8601 year number
-tag_to_value($o, {Y,M,D}, _) ->
+tag_to_value($o, {Y,M,D}, _, _TransFun, _Locale) ->
     integer_to_list(weeknum_year(Y,M,D));
     integer_to_list(weeknum_year(Y,M,D));
 
 
-tag_to_value(C, Date, Time) ->
+tag_to_value(C, Date, Time, _TransFun, _Locale) ->
     io:format("Unimplemented tag : ~p [Date : ~p] [Time : ~p]",
     io:format("Unimplemented tag : ~p [Date : ~p] [Time : ~p]",
               [C, Date, Time]),
               [C, Date, Time]),
     "".
     "".
@@ -384,4 +432,7 @@ ucfirst([First | Rest]) when First >= $a, First =< $z ->
 ucfirst(Other) ->
 ucfirst(Other) ->
     Other.
     Other.
 
 
-
+stub_tran(A,_) -> 
+    % userful for test debuggging
+    % io:format("calling stub translation!!!",[]),
+    A.

+ 9 - 5
src/i18n/sources_parser.erl

@@ -26,16 +26,19 @@
 
 
 %% New API
 %% New API
 -export([parse_pattern/1, parse_file/1, parse_content/2, phrase_info/2]).
 -export([parse_pattern/1, parse_file/1, parse_content/2, phrase_info/2]).
+
 %% Deprecated API
 %% Deprecated API
 -export([parse/0, parse/1, process_content/2]).
 -export([parse/0, parse/1, process_content/2]).
+-deprecated([{parse, '_'}, {process_content, 2}]).
 
 
+%% Type exports
 -export_type([phrase/0, compat_phrase/0, field/0]).
 -export_type([phrase/0, compat_phrase/0, field/0]).
 
 
 %%
 %%
 %% Include files
 %% Include files
 %%
 %%
 
 
--include("include/erlydtl_ext.hrl").
+-include("erlydtl_ext.hrl").
 
 
 -record(phrase, {msgid :: string(),
 -record(phrase, {msgid :: string(),
                  msgid_plural :: string() | undefined,
                  msgid_plural :: string() | undefined,
@@ -170,8 +173,9 @@ process_token(Fname,
     St#state{acc=[Phrase | Acc], translators_comment=undefined};
     St#state{acc=[Phrase | Acc], translators_comment=undefined};
 process_token(Fname, {blocktrans, Args, Contents, PluralContents}, #state{acc=Acc, translators_comment=Comment}=St) ->
 process_token(Fname, {blocktrans, Args, Contents, PluralContents}, #state{acc=Acc, translators_comment=Comment}=St) ->
     {Fname, Line, Col} = guess_blocktrans_lc(Fname, Args, Contents),
     {Fname, Line, Col} = guess_blocktrans_lc(Fname, Args, Contents),
-    Phrase = #phrase{msgid=unparse(Contents),
-                     msgid_plural=unparse(PluralContents),
+    Trim = proplists:get_value(trimmed, Args),
+    Phrase = #phrase{msgid=unparse(Contents, Trim),
+                     msgid_plural=unparse(PluralContents, Trim),
                      context=case proplists:get_value(context, Args) of
                      context=case proplists:get_value(context, Args) of
                                  {string_literal, _, String} ->
                                  {string_literal, _, String} ->
                                      erlydtl_compiler_utils:unescape_string_literal(String);
                                      erlydtl_compiler_utils:unescape_string_literal(String);
@@ -198,8 +202,8 @@ trans({string_literal,Pos,String}) -> {Pos, String}.
 
 
 unescape(String) -> string:sub_string(String, 2, string:len(String) -1).
 unescape(String) -> string:sub_string(String, 2, string:len(String) -1).
 
 
-unparse(undefined) -> undefined;
-unparse(Contents) -> erlydtl_unparser:unparse(Contents).
+unparse(undefined, _) -> undefined;
+unparse(Contents, Trim) -> erlydtl_unparser:unparse(Contents, Trim).
 
 
 %% hack to guess ~position of blocktrans
 %% hack to guess ~position of blocktrans
 guess_blocktrans_lc(Fname, [{{identifier, {L, C}, _}, _} | _], _) ->
 guess_blocktrans_lc(Fname, [{{identifier, {L, C}, _}, _} | _], _) ->

+ 4 - 1
test/erlydtl_custom_tags.erl

@@ -1,6 +1,6 @@
 -module(erlydtl_custom_tags).
 -module(erlydtl_custom_tags).
 
 
--export([custom1/1, custom2/2, custom3/2, custom4/1]).
+-export([custom1/1, custom2/2, custom3/2, custom4/1, custom1_var/2]).
 
 
 custom1(_TagVars = []) ->
 custom1(_TagVars = []) ->
     <<"b1">>.
     <<"b1">>.
@@ -13,3 +13,6 @@ custom3([], _RenderOptions = [{locale, ru}]) ->
 
 
 custom4(_TagVars = [<<"a">>]) ->
 custom4(_TagVars = [<<"a">>]) ->
     <<"a">>.
     <<"a">>.
+
+custom1_var(_TagVars = [], _O) ->
+[{name, <<"b1">>}, {count, 11}].

+ 11 - 0
test/erlydtl_custom_tags_lib.erl

@@ -0,0 +1,11 @@
+-module(erlydtl_custom_tags_lib).
+-behaviour(erlydtl_library).
+
+-export([version/0, inventory/1, customtag2_var/2]).
+
+version() -> 1.
+
+inventory(filters) -> [];
+inventory(tags) -> [customtag2_var].
+
+customtag2_var(_V,_R) -> [{name, <<"b1">>}, {count, 11}].

+ 7 - 1
test/erlydtl_eunit_testrunner.erl

@@ -58,8 +58,14 @@ run_compile(T) ->
            T#test.module,
            T#test.module,
            compile_opts(T))
            compile_opts(T))
     of
     of
-        {ok, M, W} ->
+        {ok, M, W0} ->
             ?assertEqual(T#test.module, M),
             ?assertEqual(T#test.module, M),
+            %% ignore useless_building warnings
+            W = lists:flatten(
+                  [case W1 of
+                       {_, [{_, sys_core_fold, useless_building}]} -> [];
+                       _ -> W1
+                   end || W1 <- W0]),
             ?assertEqual(T#test.warnings, W);
             ?assertEqual(T#test.warnings, W);
         {error, E, W} ->
         {error, E, W} ->
             ?assertEqual(T#test.errors, E),
             ?assertEqual(T#test.errors, E),

+ 6 - 0
test/erlydtl_lib_test1.erl

@@ -3,6 +3,12 @@
 
 
 -export([version/0, inventory/1, reverse/1]).
 -export([version/0, inventory/1, reverse/1]).
 
 
+%% dummy behaviour for lib_test2
+-export([behaviour_info/1]).
+behaviour_info(callbacks) -> [].
+%% end behaviour
+
+
 version() -> 1.
 version() -> 1.
 
 
 inventory(filters) -> [reverse];
 inventory(filters) -> [reverse];

+ 16 - 0
test/erlydtl_lib_test2.erl

@@ -0,0 +1,16 @@
+-module(erlydtl_lib_test2).
+%% test multiple behaviours
+-behaviour(erlydtl_lib_test1).
+-behaviour(erlydtl_library).
+
+-export([version/0, inventory/1, reverse/1]).
+
+version() -> 1.
+
+inventory(filters) -> [reverse];
+inventory(tags) -> [].
+
+reverse(String) when is_list(String) ->
+    lists:reverse(String);
+reverse(String) when is_binary(String) ->
+    reverse(binary_to_list(String)).

+ 16 - 0
test/erlydtl_lib_test2a.erl

@@ -0,0 +1,16 @@
+-module(erlydtl_lib_test2a).
+%% test multiple behaviors (alternative spelling)
+-behavior(erlydtl_lib_test1).
+-behavior(erlydtl_library).
+
+-export([version/0, inventory/1, reverse/1]).
+
+version() -> 1.
+
+inventory(filters) -> [reverse];
+inventory(tags) -> [].
+
+reverse(String) when is_list(String) ->
+    lists:reverse(String);
+reverse(String) when is_binary(String) ->
+    reverse(binary_to_list(String)).

+ 182 - 17
test/erlydtl_test_defs.erl

@@ -1,6 +1,7 @@
+%% -*- coding: utf-8 -*-
 -module(erlydtl_test_defs).
 -module(erlydtl_test_defs).
 
 
--export([tests/0]).
+-export([tests/0, extra_reader/2]).
 -include("testrunner.hrl").
 -include("testrunner.hrl").
 -record(testrec, {foo, bar, baz}).
 -record(testrec, {foo, bar, baz}).
 -record(person, {first_name, gender}).
 -record(person, {first_name, gender}).
@@ -136,6 +137,8 @@ all_test_defs() ->
         <<"{{ var1 }}">>, dict:store(var1, "bar", dict:new()), <<"bar">>},
         <<"{{ var1 }}">>, dict:store(var1, "bar", dict:new()), <<"bar">>},
        {"Render variable with missing attribute in dict",
        {"Render variable with missing attribute in dict",
         <<"{{ var1.foo }}">>, [{var1, dict:store(bar, "Othello", dict:new())}], <<"">>},
         <<"{{ var1.foo }}">>, [{var1, dict:store(bar, "Othello", dict:new())}], <<"">>},
+       {"Render variable in a two elements tuple",
+        <<"{{ var1.2 }}">>, [{var1,{12,[bar]}}], <<"bar">>},
        {"Render variable in gb_tree",
        {"Render variable in gb_tree",
         <<"{{ var1 }}">>, gb_trees:insert(var1, "bar", gb_trees:empty()), <<"bar">>},
         <<"{{ var1 }}">>, gb_trees:insert(var1, "bar", gb_trees:empty()), <<"bar">>},
        {"Render variable in arity-1 func",
        {"Render variable in arity-1 func",
@@ -203,7 +206,11 @@ all_test_defs() ->
      {"now",
      {"now",
       [{"now functional",
       [{"now functional",
         <<"It is the {% now \"jS \\o\\f F Y\" %}.">>, [{var1, ""}], generate_test_date()}
         <<"It is the {% now \"jS \\o\\f F Y\" %}.">>, [{var1, ""}], generate_test_date()}
-      ]},
+     ]},
+      {"now",
+      [{"now function with translation", % notice, that only date output is traslated. While you might want to transle the whole format string ('F'->'E')
+        <<"It is the {% now \"jS \\o\\f F Y\" %}.">>, [{var1, ""}], [{locale, <<"ru">>}, {translation_fun, fun date_translation/2}], generate_test_date(russian)}
+     ]},
      {"if",
      {"if",
       [{"If/else",
       [{"If/else",
         <<"{% if var1 %}boo{% else %}yay{% endif %}">>, [{var1, ""}], <<"yay">>},
         <<"{% if var1 %}boo{% else %}yay{% endif %}">>, [{var1, ""}], <<"yay">>},
@@ -560,6 +567,48 @@ all_test_defs() ->
         <<"{{ var1|date }}">>,
         <<"{{ var1|date }}">>,
         [{var1, {{1975,7,24}, {7,13,1}}}],
         [{var1, {{1975,7,24}, {7,13,1}}}],
         <<"July 24, 1975">>},
         <<"July 24, 1975">>},
+        % I doubt someone need first two, but test we support it
+        {"|date a translation",
+        <<"{{ var1|date:\"a\" }}">>,
+        [{var1, {{1975,7,24},{12,00,00}}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"п.п."/utf8>>},
+        {"|date A translation",
+        <<"{{ var1|date:\"A\" }}">>,
+        [{var1, {{1975,7,24},{12,00,00}}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"ПП"/utf8>>},
+        {"|date b translation", 
+        <<"{{ var1|date:\"b\" }}">>,
+        [{var1, {1975,7,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"июл"/utf8>>},
+        {"|date D translation",
+        <<"{{ var1|date:\"D\" }}">>,
+        [{var1, {1975,7,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Чтв"/utf8>>},
+        {"|date E translation",
+        <<"{{ var1|date:\"E\" }}">>,
+        [{var1, {1975,7,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Июля"/utf8>>},
+        {"|date F translation",
+        <<"{{ var1|date:\"F\" }}">>,
+        [{var1, {1975,7,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Июль"/utf8>>},
+        {"|date l translation",
+        <<"{{ var1|date:\"l\" }}">>,
+        [{var1, {1975,7,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Четверг"/utf8>>},
+        {"|date M translation",
+        <<"{{ var1|date:\"M\" }}">>,
+        [{var1, {1986,9,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Сен"/utf8>>},
+        {"|date N translation",
+        <<"{{ var1|date:\"N\" }}">>,
+        [{var1, {1986,9,24}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"Сен."/utf8>>},
+        {"|date P translation",
+        <<"{{ var1|date:\"P\" }}">>,
+        [{var1, {{1986,9,24},{12,0,0}}}],[{translation_fun, fun date_translation/2},{locale, <<"ru">>}],
+        <<"полдень"/utf8>>},
+
        {"|default:\"foo\" 1",
        {"|default:\"foo\" 1",
         <<"{{ var1|default:\"foo\" }}">>, [], <<"foo">>},
         <<"{{ var1|default:\"foo\" }}">>, [], <<"foo">>},
        {"|default:\"foo\" 2",
        {"|default:\"foo\" 2",
@@ -1059,6 +1108,9 @@ all_test_defs() ->
        {"|truncatewords_html:4",
        {"|truncatewords_html:4",
         <<"{{ var1|truncatewords_html:4 }}">>, [{var1, "<p>The <strong>Long and <em>Winding</em> Road</strong> is too long</p>"}],
         <<"{{ var1|truncatewords_html:4 }}">>, [{var1, "<p>The <strong>Long and <em>Winding</em> Road</strong> is too long</p>"}],
         <<"<p>The <strong>Long and <em>Winding</em>...</strong></p>">>},
         <<"<p>The <strong>Long and <em>Winding</em>...</strong></p>">>},
+       {"|truncatewords_html:50",
+        <<"{{ var1|truncatewords_html:50 }}">>, [{var1, "<p>The <strong>Long and <em>Winding</em> Road</strong> is too long</p>"}],
+        <<"<p>The <strong>Long and <em>Winding</em> Road</strong> is too long</p>">>},
        {"|unordered_list",
        {"|unordered_list",
         <<"{{ var1|unordered_list }}">>, [{var1, ["States", ["Kansas", ["Lawrence", "Topeka"], "Illinois"]]}],
         <<"{{ var1|unordered_list }}">>, [{var1, ["States", ["Kansas", ["Lawrence", "Topeka"], "Illinois"]]}],
         <<"<li>States<ul><li>Kansas<ul><li>Lawrence</li><li>Topeka</li></ul></li><li>Illinois</li></ul></li>">>},
         <<"<li>States<ul><li>Kansas<ul><li>Lawrence</li><li>Topeka</li></ul></li><li>Illinois</li></ul></li>">>},
@@ -1398,10 +1450,14 @@ all_test_defs() ->
           errors = [error_info([{{1,31}, erlydtl_parser, ["syntax error before: ","'.'"]}])]
           errors = [error_info([{{1,31}, erlydtl_parser, ["syntax error before: ","'.'"]}])]
          },
          },
        {"blocktrans runtime",
        {"blocktrans runtime",
-        <<"{% blocktrans with v1=foo%}Hello, {{ name }}! See {{v1}}.{%endblocktrans%}">>,
+        <<"{%blocktrans with v1=foo%}Hello, {{ name }}! See {{v1}}.{%endblocktrans%}">>,
         [{name, "Mr. President"}, {foo, <<"rubber-duck">>}],
         [{name, "Mr. President"}, {foo, <<"rubber-duck">>}],
         [{translation_fun, fun("Hello, {{ name }}! See {{ v1 }}.") -> <<"Guten tag, {{name}}! Sehen {{    v1   }}.">> end}],
         [{translation_fun, fun("Hello, {{ name }}! See {{ v1 }}.") -> <<"Guten tag, {{name}}! Sehen {{    v1   }}.">> end}],
-        [], <<"Guten tag, Mr. President! Sehen rubber-duck.">>}
+        [], <<"Guten tag, Mr. President! Sehen rubber-duck.">>},
+       {"trimmed",
+        <<"{% blocktrans trimmed %}\n  foo  \n   bar   here .\n \n   \n baz{% endblocktrans %}">>,
+        [], [{translation_fun, fun ("foo bar   here . baz") -> "ok" end}],
+        <<"ok">>}
       ]},
       ]},
      {"extended translation features (#131)",
      {"extended translation features (#131)",
       [{"trans default locale",
       [{"trans default locale",
@@ -1495,6 +1551,12 @@ all_test_defs() ->
         [{locale, default}, {translation_fun, fun () -> fun lists:reverse/1 end}],
         [{locale, default}, {translation_fun, fun () -> fun lists:reverse/1 end}],
         <<"oof">>}
         <<"oof">>}
       ]},
       ]},
+     {"language",
+      [{"override locale",
+        <<"{% trans 'foo' %}{% language 'other' %}{% trans 'foo' %}{% endlanguage %}">>,
+        [], [{locale, <<"default">>}, {translation_fun, fun ("foo", <<"default">>) -> "1"; ("foo", <<"other">>) -> "2"; (A, B) -> [A, B] end}],
+        <<"12">>}
+      ]},
      {"verbatim",
      {"verbatim",
       [{"Plain verbatim",
       [{"Plain verbatim",
         <<"{% verbatim %}{{ oh no{% foobar %}{% endverbatim %}">>, [],
         <<"{% verbatim %}{{ oh no{% foobar %}{% endverbatim %}">>, [],
@@ -1628,13 +1690,21 @@ all_test_defs() ->
         <<"{% foo %}">>, [], [],
         <<"{% foo %}">>, [], [],
         [{custom_tags_modules, [foo]}],
         [{custom_tags_modules, [foo]}],
         <<"">>,
         <<"">>,
-        [error_info([{none,erlydtl_compiler,{load_library,'(custom-legacy)',foo,nofile}}])]
+        [error_info(
+           [{none,erlydtl_beam_compiler,{unknown_tag, foo}},
+            {none,erlydtl_compiler,{load_library,'(custom-legacy)',foo,nofile}}
+           ])]
        },
        },
        {"unknown filter",
        {"unknown filter",
         <<"{{ '123'|foo }}">>, [], [], [],
         <<"{{ '123'|foo }}">>, [], [], [],
         <<"">>,
         <<"">>,
         [error_info([{{1,10},erlydtl_beam_compiler,{unknown_filter,foo,1}}])]
         [error_info([{{1,10},erlydtl_beam_compiler,{unknown_filter,foo,1}}])]
        },
        },
+       {"unknown tag",
+        <<"a{% b %}c">>, [], [], [],
+        <<"ac">>,
+        [error_info([{none,erlydtl_beam_compiler,{unknown_tag, b}}])]
+       },
        {"ssi file not found",
        {"ssi file not found",
         <<"{% ssi 'foo' %}">>, [],
         <<"{% ssi 'foo' %}">>, [],
         {error, {read_file, <<"./foo">>, enoent}}
         {error, {read_file, <<"./foo">>, enoent}}
@@ -1665,6 +1735,18 @@ all_test_defs() ->
         [{default_libraries, [test1]},
         [{default_libraries, [test1]},
          {libraries, [{test1, erlydtl_lib_test1}]}],
          {libraries, [{test1, erlydtl_lib_test1}]}],
         <<"ytrewQ">>
         <<"ytrewQ">>
+       },
+       {"lib with multiple behaviours",
+        <<"{{ QWER|reverse }}">>, [{'QWER', "Qwerty"}], [],
+        [{default_libraries, [test2]},
+         {libraries, [{test2, erlydtl_lib_test2}]}],
+        <<"ytrewQ">>
+       },
+       {"lib with multiple behaviors (alternative spelling)",
+        <<"{{ QWER|reverse }}">>, [{'QWER', "Qwerty"}], [],
+        [{default_libraries, [test2]},
+         {libraries, [{test2, erlydtl_lib_test2a}]}],
+        <<"ytrewQ">>
        }
        }
       ]},
       ]},
      {"compile time default vars/constants",
      {"compile time default vars/constants",
@@ -1731,11 +1813,11 @@ all_test_defs() ->
                 "for_list_preset", "for_preset", "for_records", "for_records_preset", "include",
                 "for_list_preset", "for_preset", "for_records", "for_records_preset", "include",
                 "if", "if_preset", "ifequal", "ifequal_preset", "ifnotequal", "ifnotequal_preset",
                 "if", "if_preset", "ifequal", "ifequal_preset", "ifnotequal", "ifnotequal_preset",
                 "now", "var", "var_preset", "cycle", "custom_tag", "custom_tag1", "custom_tag2",
                 "now", "var", "var_preset", "cycle", "custom_tag", "custom_tag1", "custom_tag2",
-                "custom_tag3", "custom_tag4", "custom_call", "include_template", "include_path",
+                "custom_tag3", "custom_tag4", "custom_tag_var", "custom_tag_lib_var", "custom_call", "include_template", "include_path",
                 "ssi", "extends_path", "extends_path2", "trans", "extends_for", "extends2",
                 "ssi", "extends_path", "extends_path2", "trans", "extends_for", "extends2",
                 "extends3", "recursive_block", "extend_recursive_block", "missing", "block_super",
                 "extends3", "recursive_block", "extend_recursive_block", "missing", "block_super",
-                "wrapper", "extends4", "super_escaped", "extends_chain"]
-
+                "wrapper", "extends4", "super_escaped", "extends_chain", "reader_options", "ssi_reader_options",
+                "extend_doubleblock"]
       ]},
       ]},
      {"compile_dir",
      {"compile_dir",
       [setup_compile(T)
       [setup_compile(T)
@@ -1782,13 +1864,65 @@ def_to_test(Group, {Name, DTL, Vars, RenderOpts, CompilerOpts, Output, Warnings}
       }.
       }.
 
 
 
 
+date_translation(Val, LC) when is_list(Val) ->
+    io:format("Translating ~p~n", [Val]),
+    date_translation(list_to_binary(Val),LC);
+% date a
+date_translation(<<"p.m.">>, <<"ru">>) ->
+    <<"п.п."/utf8>>;
+% date A
+date_translation(<<"PM">>, <<"ru">>) ->
+    <<"ПП"/utf8>>;
+% date b
+date_translation(<<"jul">>, <<"ru">>) ->
+    <<"июл"/utf8>>;
+% date D
+date_translation(<<"Thu">>, <<"ru">>) ->
+    <<"Чтв"/utf8>>;
+% date E
+date_translation(<<"July">>, {<<"ru">>, <<"alt. month">>}) ->
+    <<"Июля"/utf8>>;
+% date F
+date_translation(<<"July">>, <<"ru">>) ->
+    <<"Июль"/utf8>>;
+% date l
+date_translation(<<"Thursday">>, <<"ru">>) ->
+    <<"Четверг"/utf8>>;
+% date M
+date_translation(<<"Sep">>, <<"ru">>) ->
+    <<"Сен"/utf8>>;
+% date N
+date_translation(<<"Sept.">>, {<<"ru">>, <<"abbrev. month">>}) ->
+    <<"Сен."/utf8>>;
+% date P
+date_translation(<<"noon">>, <<"ru">>) ->
+    <<"полдень"/utf8>>;
+date_translation(Text, <<"ru">>) ->
+    proplists:get_value(Text,
+                        lists:zip(
+                              lists:map(fun list_to_binary/1, en_months()),
+                              ru_months()),
+                        Text);
+date_translation(Text, _) ->
+    Text.
+
+ru_months() -> [ <<"Январь"/utf8>>, <<"Февраль"/utf8>>, <<"Март"/utf8>>, <<"Апрель"/utf8>>,
+             <<"Май"/utf8>>, <<"Июнь"/utf8>>, <<"Июль"/utf8>>, <<"Август"/utf8>>, <<"Сентябрь"/utf8>>,
+             <<"Октябрь"/utf8>>, <<"Ноябрь"/utf8>>, <<"Декабрь"/utf8>>].
+en_months() -> ["January", "February", "March", "April",
+             "May", "June", "July", "August", "September",
+             "October", "November", "December"].
+
+
+
 generate_test_date() ->
 generate_test_date() ->
+    generate_test_date(false).
+generate_test_date(Translation) ->
     {{Y,M,D}, _} = erlang:localtime(),
     {{Y,M,D}, _} = erlang:localtime(),
-    MonthName = [
-                 "January", "February", "March", "April",
-                 "May", "June", "July", "August", "September",
-                 "October", "November", "December"
-                ],
+    MonthName = case Translation of
+                    russian -> ru_months();
+                    _ -> en_months()
+                end,
     OrdinalSuffix = [
     OrdinalSuffix = [
                      "st","nd","rd","th","th","th","th","th","th","th", % 1-10
                      "st","nd","rd","th","th","th","th","th","th","th", % 1-10
                      "th","th","th","th","th","th","th","th","th","th", % 10-20
                      "th","th","th","th","th","th","th","th","th","th", % 10-20
@@ -1882,8 +2016,8 @@ setup_compile("var_preset") ->
     CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}],
     CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}],
     {ok, [CompileVars]};
     {ok, [CompileVars]};
 setup_compile("extends_for") ->
 setup_compile("extends_for") ->
-	CompileVars = [{veggie_list, ["broccoli", "beans", "peas", "carrots"]}],
-	{ok, [CompileVars]};
+    CompileVars = [{veggie_list, ["broccoli", "beans", "peas", "carrots"]}],
+    {ok, [CompileVars]};
 setup_compile("extends2") ->
 setup_compile("extends2") ->
     File = template_file(input, "extends2"),
     File = template_file(input, "extends2"),
     Error = {none, erlydtl_beam_compiler, unexpected_extends_tag},
     Error = {none, erlydtl_beam_compiler, unexpected_extends_tag},
@@ -1907,11 +2041,31 @@ setup_compile("custom_tag1") -> setup_compile("custom_tag");
 setup_compile("custom_tag2") -> setup_compile("custom_tag");
 setup_compile("custom_tag2") -> setup_compile("custom_tag");
 setup_compile("custom_tag3") -> setup_compile("custom_tag");
 setup_compile("custom_tag3") -> setup_compile("custom_tag");
 setup_compile("custom_tag4") -> setup_compile("custom_tag");
 setup_compile("custom_tag4") -> setup_compile("custom_tag");
+setup_compile("custom_tag_var") -> setup_compile("custom_tag");
+setup_compile("custom_tag_lib_var") ->
+ {ok, [[]|[{compile_opts, [{libraries, [{custom_tag_lib,erlydtl_custom_tags_lib}]}, {default_libraries, [custom_tag_lib]}]}]]};
 setup_compile("super_escaped") ->
 setup_compile("super_escaped") ->
     {ok, [[]|[{compile_opts, [auto_escape]}]]};
     {ok, [[]|[{compile_opts, [auto_escape]}]]};
+setup_compile("reader_options") ->
+ {ok, [[]|[{compile_opts, [{reader, {?MODULE, extra_reader}}, {reader_options, [{user_id, <<"007">>}, {user_name, <<"Agent">>}]}]}]]};
+setup_compile("ssi_reader_options") ->
+ {ok, [[]|[{compile_opts, [{reader, {?MODULE, extra_reader}}, {reader_options, [{user_id, <<"007">>}, {user_name, <<"Agent">>}]}]}]]};
+%%setup_compile("path1") ->
+%%    {ok, [[]|[{compile_opts, [debug_compiler]}]]};
 setup_compile(_) ->
 setup_compile(_) ->
     {ok, [[]]}.
     {ok, [[]]}.
 
 
+extra_reader(FileName, ReaderOptions) ->
+ UserID = proplists:get_value(user_id, ReaderOptions, <<"IDUnknown">>),
+ UserName = proplists:get_value(user_name, ReaderOptions, <<"NameUnknown">>),
+ case file:read_file(FileName) of
+  {ok, Data} when UserID == <<"007">>, UserName == <<"Agent">> ->
+   {ok, Data};
+  {ok, _Data} ->
+   {error, "Not Found"};
+  Err ->
+   Err
+ end.
 
 
 expected(File) ->
 expected(File) ->
     Filename = template_file(expect, File),
     Filename = template_file(expect, File),
@@ -2010,8 +2164,8 @@ setup("cycle") ->
                   {a, "Apple"}, {b, "Banana"}, {c, "Cherry"}],
                   {a, "Apple"}, {b, "Banana"}, {c, "Cherry"}],
     {ok, RenderVars};
     {ok, RenderVars};
 setup("trans") ->
 setup("trans") ->
-    RenderVars = [{locale, "reverse"}],
-    {ok, RenderVars};
+    RenderOpts = [{translation_fun, fun lists:reverse/1}],
+    {ok, [], RenderOpts};
 setup("locale") ->
 setup("locale") ->
     {ok, _RenderVars = [{locale, "ru"}]};
     {ok, _RenderVars = [{locale, "ru"}]};
 setup("custom_tag1") ->
 setup("custom_tag1") ->
@@ -2022,12 +2176,23 @@ setup("custom_tag3") ->
     {ok, [{a, <<"a1">>}], [{locale, ru}], <<"b3\n">>};
     {ok, [{a, <<"a1">>}], [{locale, ru}], <<"b3\n">>};
 setup("custom_tag4") ->
 setup("custom_tag4") ->
     {ok, [], [], <<"a\n">>};
     {ok, [], [], <<"a\n">>};
+setup("custom_tag_var") ->
+ {ok, [{a, <<"a1">>}], [{locale, ru}], <<"\nb1\n11\n">>};
+setup("custom_tag_lib_var") ->
+ {ok, [{a, <<"a1">>}], [{locale, ru}], <<"\nb1\n11\n">>};
 setup("ssi") ->
 setup("ssi") ->
     RenderVars = [{path, "ssi_include.html"}],
     RenderVars = [{path, "ssi_include.html"}],
     {ok, RenderVars};
     {ok, RenderVars};
 setup("wrapper") ->
 setup("wrapper") ->
     RenderVars = [{types, ["b", "a", "c"]}],
     RenderVars = [{types, ["b", "a", "c"]}],
     {ok, RenderVars};
     {ok, RenderVars};
+setup("reader_options") ->
+ RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}],
+% Options = [],%[{compile_opts, [{reader, {?MODULE, extra_reader}}, {reader_options, [{user_id, <<"007">>}, {user_name, <<"Agent">>}]}]}],
+  {ok, RenderVars};
+setup("ssi_reader_options") ->
+ RenderVars = [{path, "ssi_include.html"}],
+ {ok, RenderVars};
 
 
 %%--------------------------------------------------------------------
 %%--------------------------------------------------------------------
 %% Custom tags
 %% Custom tags

+ 81 - 0
test/erlydtl_translation_tests.erl

@@ -0,0 +1,81 @@
+-module(erlydtl_translation_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+all_sources_parser_test_() ->
+    [{Title, [test_fun(Test) || Test <- Tests]}
+     || {Title, Tests} <- test_defs()].
+
+test_fun({Name, Template, Variables, Options, Output}) ->
+    {Name, fun () ->
+                   Tokens = (catch compile_and_render(Template, Variables, Options)),
+                   ?assertMatch(Output, Tokens)
+           end}.
+
+
+
+compile_and_render(Template, Variables, Options) ->
+    {ok, test} = erlydtl:compile_template(Template, test),
+    {ok, R}  = test:render(Variables, Options),
+    iolist_to_binary(R).
+
+
+test_defs() ->
+    [
+        {"trans", [
+            {"simple", "{% trans \"hello\" %}", [], [], <<"hello">>},
+            {"with_fun", "{% trans \"text\" %}", [], [{translation_fun, fun(_ID, _L) -> "hola" end}], <<"hola">>},
+            {"with_fun_utf8", "{% trans \"text\" %}", [],
+                [{translation_fun, fun(_ID, _L) -> <<"привет"/utf8>> end}], <<"привет"/utf8>>}
+        ]},
+        {"blocktrans", [
+            {"simple", "{% blocktrans %} hello {% endblocktrans %}", [], [], <<" hello ">>},
+            {"with_fun", "{% blocktrans %} hello {% endblocktrans %}", [],
+                [{translation_fun, fun(_ID, _L) -> "hola" end}], <<"hola">>},
+    
+            {"s_param_no_fun", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, "mundo"}],
+                [], <<" hello mundo ">>},
+    
+            {"s_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, "mundo"}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola mundo">>},
+            {"b_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, <<"mundo">>}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola mundo">>},
+            {"i_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, 1}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola 1">>},
+            {"f_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, 3.1415}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola 3.1415">>},
+    
+            {"b_xss", "{% blocktrans %} hello {{ p }} {% endblocktrans %}",
+                [{p, <<"<script>alert('pwnd');</script>">>}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
+                <<"hola &lt;script&gt;alert(&#039;pwnd&#039;);&lt;/script&gt;">>},
+            {"s_xss", "{% blocktrans %} hello {{ p }} {% endblocktrans %}",
+                [{p, "<script>alert('pwnd');</script>"}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
+                <<"hola &lt;script&gt;alert(&#039;pwnd&#039;);&lt;/script&gt;">>},
+    
+            {"b_autoecape_off",
+                "{% autoescape off %}{% blocktrans %} hello {{ p }} {% endblocktrans %}{% endautoescape %}",
+                [{p, <<"<script>alert('pwnd');</script>">>}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
+                <<"hola <script>alert('pwnd');</script>">>},
+            {"b_autoecape_nested",
+                "{% autoescape off %}{% autoescape on %}{% blocktrans %} hello {{ p }} {% endblocktrans %}{% endautoescape %}{% endautoescape %}",
+                [{p, <<"<script>alert('pwnd');</script>">>}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
+                <<"hola &lt;script&gt;alert(&#039;pwnd&#039;);&lt;/script&gt;">>},
+             {"term_hack_", "{% blocktrans %} hello {{ p }} {% endblocktrans %}",
+                [{p, {"<script>alert('pwnd');</script>"}}],
+                [{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
+                <<"hola {&quot;&lt;script&gt;alert(&#039;pwnd&#039;);&lt;/script&gt;&quot;}">>},
+            {"plural_2",
+                "{% blocktrans count counter=p %} hello world {% plural %} hello {{ p }} worlds {% endblocktrans %}",
+                [{p, 2}],
+                [{translation_fun, fun({" hello world ", {" hello {{ p }} worlds ", 2}}, _L)  ->
+                                        "hola {{ p }} mundos"
+                                   end}],
+                <<"hola 2 mundos">>}
+        ]}
+    ].
+
+

+ 6 - 0
test/files/expect/extend_doubleblock

@@ -0,0 +1,6 @@
+<head>
+  <title>Chicago Boss Admin - General Info</title>
+</head>
+<body>
+  <h1>Chicago Boss Admin - General Info</h1>
+</body>

+ 11 - 0
test/files/expect/reader_options

@@ -0,0 +1,11 @@
+base-barstring
+
+base template
+
+base title
+
+more of base template
+
+base content
+
+end of base template

+ 2 - 0
test/files/expect/ssi_reader_options

@@ -0,0 +1,2 @@
+{{ "Don't evaluate me!" }}
+

+ 1 - 1
test/files/expect/trans

@@ -1 +1 @@
-Example String
+gnirtS elpmaxE

+ 6 - 0
test/files/input/base_doubleblock

@@ -0,0 +1,6 @@
+<head>
+  <title>Chicago Boss Admin - {% block title %}{% endblock %}</title>
+</head>
+<body>
+  <h1>Chicago Boss Admin - {% block title %}{% endblock %}</h1>
+</body>

+ 3 - 0
test/files/input/custom_tag_lib_var

@@ -0,0 +1,3 @@
+{% customtag2_var as tagvar %}
+{{ tagvar.name }}
+{{ tagvar.count }}

+ 3 - 0
test/files/input/custom_tag_var

@@ -0,0 +1,3 @@
+{% custom1_var as tagvar %}
+{{ tagvar.name }}
+{{ tagvar.count }}

+ 2 - 0
test/files/input/extend_doubleblock

@@ -0,0 +1,2 @@
+{% extends "base_doubleblock" %}
+{% block title %}General Info{% endblock %}

+ 1 - 0
test/files/input/reader_options

@@ -0,0 +1 @@
+{% extends "base" %}

+ 1 - 0
test/files/input/ssi_reader_options

@@ -0,0 +1 @@
+{% ssi path %}

+ 58 - 13
test/sources_parser_tests.erl

@@ -32,7 +32,13 @@ test_defs() ->
         [{"Hello inside an if inside a for",{"dummy_path",1,73}}]},
         [{"Hello inside an if inside a for",{"dummy_path",1,73}}]},
        {"if and else both with trans",
        {"if and else both with trans",
         <<"<html>{% block content %}{% if thing %} {% trans \"Hello inside an if\" %} {% else %} {% trans \"Hello inside an else\" %} {% endif %} {% endblock %}</html>">>,
         <<"<html>{% block content %}{% if thing %} {% trans \"Hello inside an if\" %} {% else %} {% trans \"Hello inside an else\" %} {% endif %} {% endblock %}</html>">>,
-        [{"Hello inside an else",{"dummy_path",1,94}}, {"Hello inside an if",{"dummy_path",1,50}}]}
+        [{"Hello inside an else",{"dummy_path",1,94}}, {"Hello inside an if",{"dummy_path",1,50}}]},
+       {"blocktrans with pretty format",
+        <<"<html>{% blocktrans %}\n  This is a multiline\n  message... \n{% endblocktrans %}">>,
+        [{"\n  This is a multiline\n  message... \n", {"dummy_path",1,10}}]},
+       {"blocktrans with pretty format, trimmed",
+        <<"<html>{% blocktrans trimmed %}\n  This is a multiline\n  message... \n{% endblocktrans %}">>,
+        [{"This is a multiline message...", {"dummy_path",1,18}}]}
       ]}
       ]}
     ].
     ].
 
 
@@ -80,18 +86,33 @@ unparser_test_() ->
 test_unparser_fun({Name, Tpl}) ->
 test_unparser_fun({Name, Tpl}) ->
     {Name, fun() ->
     {Name, fun() ->
                    %% take input Tpl value, parse it, "unparse" it, then parse it again.
                    %% take input Tpl value, parse it, "unparse" it, then parse it again.
-                   %% the both parsed values should be equvialent, even if the source versions
+                   %% both parsed values should be equvialent, even if the source versions
                    %% are not an exact match (there can be whitespace differences)
                    %% are not an exact match (there can be whitespace differences)
-                   {ok, Dpt} = erlydtl_compiler:do_parse_template(
-                                 Tpl, #dtl_context{}),
-                   Unparsed = erlydtl_unparser:unparse(Dpt),
-                   {ok, DptU} = erlydtl_compiler:do_parse_template(
-                                 Unparsed, #dtl_context{}),
-                   compare_tree(Dpt, DptU)
+                   case erlydtl_compiler:do_parse_template(
+                          Tpl, #dtl_context{}) of
+                       {ok, Dpt} ->
+                           Unparsed = erlydtl_unparser:unparse(Dpt),
+                           case erlydtl_compiler:do_parse_template(
+                                  Unparsed, #dtl_context{}) of
+                               {ok, DptU} ->
+                                   case catch compare_tree(Dpt, DptU) of
+                                       ok -> ok;
+                                       Err -> throw({compare_failed, Err, {test_ast, Dpt}, {unparsed, {source, Unparsed}, {ast, DptU}}})
+                                   end;
+                               Err ->
+                                   throw({unparsed_source, Err})
+                           end;
+                       Err ->
+                           throw({test_source, Err})
+                   end
            end}.
            end}.
 
 
 unparser_test_defs() ->
 unparser_test_defs() ->
-    [{"comment tag", <<"here it is: {# this is my comment #} <-- it was right there.">>}
+    [{"comment tag", <<"here it is: {# this is my comment #} <-- it was right there.">>},
+     {"blocktrans plain", <<"{% blocktrans %}foo bar{% endblocktrans %}">>},
+     {"blocktrans trimmed", <<"{% blocktrans trimmed %}\n foo \n   bar \n\n{% endblocktrans %}">>},
+     {"blocktrans with args", <<"{% blocktrans with var1=foo var2=bar count c=d %}blarg{% endblocktrans %}">>},
+     {"blocktrans with all", <<"{% blocktrans with var1=foo var2=bar trimmed context 'baz' count c=d %}blarg{% endblocktrans %}">>}
     ].
     ].
 
 
 
 
@@ -108,9 +129,13 @@ compare_token({'autoescape', OnOrOff1, Contents1}, {'autoescape', OnOrOff2, Cont
 compare_token({'block', Identifier1, Contents1}, {'block', Identifier2, Contents2}) ->
 compare_token({'block', Identifier1, Contents1}, {'block', Identifier2, Contents2}) ->
     compare_identifier(Identifier1, Identifier2),
     compare_identifier(Identifier1, Identifier2),
     compare_tree(Contents1, Contents2);
     compare_tree(Contents1, Contents2);
-compare_token({'blocktrans', Args1, Contents1}, {'blocktrans', Args2, Contents2}) ->
-    compare_args(Args1, Args2),
-    compare_tree(Contents1, Contents2);
+compare_token({'blocktrans', Args1, Contents1, Plural1}, {'blocktrans', Args2, Contents2, Plural2}) ->
+    compare_blocktrans_args(Args1, Args2),
+    compare_tree(Contents1, Contents2),
+    case {Plural1, Plural2} of
+        {undefined, undefined} -> ok;
+        _ -> compare_tree(Plural1, Plural2)
+    end;
 compare_token({'call', Identifier1}, {'call', Identifier2}) ->
 compare_token({'call', Identifier1}, {'call', Identifier2}) ->
     compare_identifier(Identifier1, Identifier2);
     compare_identifier(Identifier1, Identifier2);
 compare_token({'call', Identifier1, With1}, {'call', Identifier2, With2}) ->
 compare_token({'call', Identifier1, With1}, {'call', Identifier2, With2}) ->
@@ -197,14 +222,34 @@ compare_value({'attribute', {Variable1, Identifier1}}, {'attribute', {Variable2,
 compare_value({'variable', Identifier1}, {'variable', Identifier2}) ->
 compare_value({'variable', Identifier1}, {'variable', Identifier2}) ->
     compare_identifier(Identifier1, Identifier2).
     compare_identifier(Identifier1, Identifier2).
 
 
-compare_args(Args1, Args2) ->
+compare_args(Args1, Args2) when length(Args1) =:= length(Args2) ->
     [compare_arg(A1, A2)
     [compare_arg(A1, A2)
      || {A1, A2} <- lists:zip(Args1, Args2)].
      || {A1, A2} <- lists:zip(Args1, Args2)].
 
 
+compare_arg(Arg, Arg) when is_atom(Arg) -> ok;
 compare_arg({{identifier, _, Name1}, Value1}, {{identifier, _, Name2}, Value2}) ->
 compare_arg({{identifier, _, Name1}, Value1}, {{identifier, _, Name2}, Value2}) ->
     ?assertEqual(Name1, Name2),
     ?assertEqual(Name1, Name2),
     compare_value(Value1, Value2).
     compare_value(Value1, Value2).
 
 
+compare_blocktrans_args([], []) -> ok;
+compare_blocktrans_args([{args, WithArgs1}|Args1], Args2) ->
+    {value, {args, WithArgs2}, Args3} = lists:keytake(args, 1, Args2),
+    compare_args(WithArgs1, WithArgs2),
+    compare_blocktrans_args(Args1, Args3);
+compare_blocktrans_args([{count, Count1}|Args1], Args2) ->
+    {value, {count, Count2}, Args3} = lists:keytake(count, 1, Args2),
+    compare_arg(Count1, Count2),
+    compare_blocktrans_args(Args1, Args3);
+compare_blocktrans_args([{context, Context1}|Args1], Args2) ->
+    {value, {context, Context2}, Args3} = lists:keytake(context, 1, Args2),
+    compare_value(Context1, Context2),
+    compare_blocktrans_args(Args1, Args3);
+compare_blocktrans_args([trimmed|Args1], Args2) ->
+    Args3 = Args2 -- [trimmed],
+    if Args2 =/= Args3 ->
+            compare_blocktrans_args(Args1, Args3)
+    end.
+
 compare_cycle_compat_names(Names1, Names2) ->
 compare_cycle_compat_names(Names1, Names2) ->
     [compare_identifier(N1, N2)
     [compare_identifier(N1, N2)
      || {N1, N2} <- lists:zip(Names1, Names2)].
      || {N1, N2} <- lists:zip(Names1, Names2)].