Просмотр исходного кода

Preliminary blocktrans support

Evan Miller 14 лет назад
Родитель
Сommit
694fc92e46
6 измененных файлов с 116 добавлено и 45 удалено
  1. 19 5
      README.markdown
  2. 51 13
      src/erlydtl_compiler.erl
  3. 4 4
      src/erlydtl_filters.erl
  4. 17 9
      src/erlydtl_parser.yrl
  5. 4 1
      src/erlydtl_scanner.erl
  6. 21 13
      tests/src/erlydtl_unittests.erl

+ 19 - 5
README.markdown

@@ -3,7 +3,7 @@ ErlyDTL
 
 ErlyDTL compiles Django Template Language to Erlang bytecode.
 
-*Supported tags*: autoescape, block, comment, cycle, extends, filter, firstof, for, if, ifequal, ifnotequal, include, now, spaceless, ssi, templatetag, trans, widthratio, with
+*Supported tags*: autoescape, block, blocktrans, comment, cycle, extends, filter, firstof, for, if, ifequal, ifnotequal, include, now, spaceless, ssi, templatetag, trans, widthratio, with
 
 _Unsupported tags_: csrf_token, ifchanged, regroup, url
 
@@ -74,6 +74,15 @@ For example, adding {locale, "en_US"} will call {key2str, Key, "en_US"}
 for all string marked as trans (`{% trans "StringValue" %}` on templates).
 See README_I18N.
 
+* `blocktrans_fun` - A two-argument fun to use for translating `blocktrans`
+blocks. This will be called once for each pair of `blocktrans` block and locale
+specified in `blocktrans_locales`. The fun should take the form:
+
+   Fun(BlockName, Locale) -> <<"ErlyDTL code">> | default
+
+* `blocktrans_locales` - A list of locales to be passed to `blocktrans_fun`.
+Defaults to [].
+
 
 Helper compilation
 ------------------
@@ -105,12 +114,17 @@ values can be atoms, strings, binaries, or (nested) variables.
 
 IOList is the rendered template.
 
-    my_compiled_template:render(Variables, TranslationFun) -> 
+    my_compiled_template:render(Variables, Options) -> 
             {ok, IOList} | {error, Err}
 
-Same as `render/1`, but TranslationFun is a fun/1 that will be used to 
-translate strings appearing inside `{% trans %}` tags. The simplest
-TranslationFun would be `fun(Val) -> Val end`
+Same as `render/1`, but with the following options:
+
+* `translation_fun` - A fun/1 that will be used to translate strings appearing
+inside `{% trans %}` tags. The simplest TranslationFun would be `fun(Val) ->
+Val end`
+
+* `locale` - A string specifying the current locale, for use with the
+`blocktrans_fun` compile-time option.
 
     my_compiled_template:translatable_strings() -> [String]
 

+ 51 - 13
src/erlydtl_compiler.erl

@@ -43,6 +43,8 @@
 -record(dtl_context, {
     local_scopes = [], 
     block_dict = dict:new(), 
+    blocktrans_fun = none,
+    blocktrans_locales = [],
     auto_escape = off, 
     doc_root = "", 
     parse_trail = [],
@@ -111,14 +113,11 @@ compile_dir(Dir, Module) ->
 
 compile_dir(Dir, Module, Options) ->
     Context = init_dtl_context_dir(Dir, Module, Options),
-    Files = case file:list_dir(Dir) of
-        {ok, FileList} -> FileList;
-        _ -> []
-    end,
+    Files = filelib:fold_files(Dir, ".*", true, fun(F1,Acc1) -> [F1 | Acc1] end, []),
     {ParserResults, ParserErrors} = lists:foldl(fun
             ("."++_, Acc) -> Acc;
-            (File, {ResultAcc, ErrorAcc}) ->
-                FilePath = filename:join([Dir, File]),
+            (FilePath, {ResultAcc, ErrorAcc}) ->
+                File = filename:basename(FilePath),
                 case parse(FilePath, Context) of
                     ok -> {ResultAcc, ErrorAcc};
                     {ok, DjangoParseTree, CheckSum} -> {[{File, DjangoParseTree, CheckSum}|ResultAcc], ErrorAcc};
@@ -210,6 +209,8 @@ init_dtl_context(File, Module, Options) ->
         doc_root = proplists:get_value(doc_root, Options, filename:dirname(File)),
         custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
         custom_tags_module = proplists:get_value(custom_tags_module, Options, Ctx#dtl_context.custom_tags_module),
+        blocktrans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.blocktrans_fun),
+        blocktrans_locales = proplists:get_value(blocktrans_locales, Options, Ctx#dtl_context.blocktrans_locales),
         vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars), 
         reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
         compiler_options = proplists:get_value(compiler_options, Options, Ctx#dtl_context.compiler_options),
@@ -226,6 +227,8 @@ init_dtl_context_dir(Dir, Module, Options) ->
         doc_root = proplists:get_value(doc_root, Options, Dir),
         custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
         custom_tags_module = proplists:get_value(custom_tags_module, Options, Module),
+        blocktrans_fun = proplists:get_value(blocktrans_fun, Options, Ctx#dtl_context.blocktrans_fun),
+        blocktrans_locales = proplists:get_value(blocktrans_locales, Options, Ctx#dtl_context.blocktrans_locales),
         vars = proplists:get_value(vars, Options, Ctx#dtl_context.vars), 
         reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
         compiler_options = proplists:get_value(compiler_options, Options, Ctx#dtl_context.compiler_options),
@@ -299,7 +302,7 @@ parse(CheckSum, Data, Context) ->
     end.
 
 parse(Data) ->
-    case erlydtl_scanner:scan(binary_to_list(Data)) of
+    case erlydtl_scanner:scan(unicode:characters_to_list(Data)) of
         {ok, Tokens} ->
             erlydtl_parser:parse(Tokens);
         Err ->
@@ -369,7 +372,7 @@ custom_forms(Dir, Module, Functions, AstInfo) ->
     FunctionAsts = lists:foldl(fun({_, Function1, Function2}, Acc) -> [Function1, Function2 | Acc] end, [], Functions),
 
     [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsFunctionAst
-            | FunctionAsts]].
+            | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts].
 
 forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum) ->
     MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
@@ -380,16 +383,25 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
         [erl_syntax:clause([erl_syntax:variable("Variables")], none, 
                 [erl_syntax:application(none,
                         erl_syntax:atom(render),
-                        [erl_syntax:variable("Variables"), erl_syntax:atom(none)])])]),
+                        [erl_syntax:variable("Variables"), erl_syntax:list([])])])]),
     Function2 = erl_syntax:application(none, erl_syntax:atom(render_internal), 
-        [erl_syntax:variable("Variables"), erl_syntax:variable("TranslationFun")]),
+        [erl_syntax:variable("Variables"), 
+            erl_syntax:application(
+                erl_syntax:atom(proplists),
+                erl_syntax:atom(get_value),
+                [erl_syntax:atom(translation_fun), erl_syntax:variable("Options"), erl_syntax:atom(none)]),
+            erl_syntax:application(
+                erl_syntax:atom(proplists),
+                erl_syntax:atom(get_value),
+                [erl_syntax:atom(locale), erl_syntax:variable("Options"), erl_syntax:atom(none)])
+        ]),
     ClauseOk = erl_syntax:clause([erl_syntax:variable("Val")], none,
         [erl_syntax:tuple([erl_syntax:atom(ok), erl_syntax:variable("Val")])]),     
     ClauseCatch = erl_syntax:clause([erl_syntax:variable("Err")], none,
         [erl_syntax:tuple([erl_syntax:atom(error), erl_syntax:variable("Err")])]),            
     Render2FunctionAst = erl_syntax:function(erl_syntax:atom(render),
         [erl_syntax:clause([erl_syntax:variable("Variables"), 
-                    erl_syntax:variable("TranslationFun")], none, 
+                    erl_syntax:variable("Options")], none, 
             [erl_syntax:try_expr([Function2], [ClauseOk], [ClauseCatch])])]),  
      
     SourceFunctionTuple = erl_syntax:tuple(
@@ -409,7 +421,7 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}
 
     RenderInternalFunctionAst = erl_syntax:function(
         erl_syntax:atom(render_internal), 
-        [erl_syntax:clause([erl_syntax:variable("Variables"), erl_syntax:variable("TranslationFun")], none, 
+        [erl_syntax:clause([erl_syntax:variable("Variables"), erl_syntax:variable("TranslationFun"), erl_syntax:variable("CurrentLocale")], none, 
                 [BodyAstTmp])]),   
     
     ModuleAst  = erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(Module)]),
@@ -465,6 +477,8 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
                     _ -> Contents
                 end,
                 body_ast(Block, Context, TreeWalkerAcc);
+            ({'blocktrans', {identifier, _, Name}, Contents}, TreeWalkerAcc) ->
+                blocktrans_ast(Name, Contents, Context, TreeWalkerAcc);
             ({'call', {'identifier', _, Name}}, TreeWalkerAcc) ->
             	call_ast(Name, TreeWalkerAcc);
             ({'call', {'identifier', _, Name}, With}, TreeWalkerAcc) ->
@@ -634,6 +648,27 @@ with_dependency(FilePath, {{Ast, Info}, TreeWalker}) ->
 empty_ast(TreeWalker) ->
     {{erl_syntax:list([]), #ast_info{}}, TreeWalker}.
 
+blocktrans_ast(Name, Contents, Context, TreeWalker) ->
+    case Context#dtl_context.blocktrans_fun of
+        none ->
+            body_ast(Contents, Context, TreeWalker);
+        BlockTransFun when is_function(BlockTransFun) ->
+            {{DefaultAst, AstInfo}, TreeWalker1} = body_ast(Contents, Context, TreeWalker),
+            {FinalAstInfo, FinalTreeWalker, Clauses} = lists:foldr(fun(Locale, {AstInfoAcc, ThisTreeWalker, ClauseAcc}) ->
+                        case BlockTransFun(Name, Locale) of
+                            default ->
+                                {AstInfoAcc, ThisTreeWalker, ClauseAcc};
+                            Body ->
+                                {ok, DjangoParseTree} = parse(Body),
+                                {{ThisAst, ThisAstInfo}, TreeWalker2} = body_ast(DjangoParseTree, Context, ThisTreeWalker),
+                                {merge_info(ThisAstInfo, AstInfoAcc), TreeWalker2, 
+                                    [erl_syntax:clause([erl_syntax:string(Locale)], none, [ThisAst])|ClauseAcc]}
+                        end
+                end, {AstInfo, TreeWalker1, []}, Context#dtl_context.blocktrans_locales),
+            Ast = erl_syntax:case_expr(erl_syntax:variable("CurrentLocale"),
+                Clauses ++ [erl_syntax:clause([erl_syntax:underscore()], none, [DefaultAst])]),
+            {{Ast, FinalAstInfo}, FinalTreeWalker}
+    end.
 
 translated_ast({string_literal, _, String}, Context, TreeWalker) ->
     NewStr = unescape_string_literal(String),
@@ -1060,7 +1095,10 @@ call_ast(Module, Variable, AstInfo, TreeWalker) ->
      AppAst = erl_syntax:application(
 		erl_syntax:atom(Module),
 		erl_syntax:atom(render),
-                [Variable, erl_syntax:variable("TranslationFun")]),
+                [Variable, erl_syntax:list([
+                            erl_syntax:tuple([erl_syntax:atom(translation_fun), erl_syntax:variable("TranslationFun")]),
+                            erl_syntax:tuple([erl_syntax:atom(locale), erl_syntax:variable("CurrentLocale")])
+                        ])]),
     RenderedAst = erl_syntax:variable("Rendered"),
     OkAst = erl_syntax:clause(
 	      [erl_syntax:tuple([erl_syntax:atom(ok), RenderedAst])], 

+ 4 - 4
src/erlydtl_filters.erl

@@ -269,7 +269,7 @@ fix_ampersands(Input) when is_list(Input) ->
     fix_ampersands(Input, []).
 
 %% @doc When used without an argument, rounds a floating-point number to one decimal place
-%% @doc -- but only if there's a decimal part to be displayed
+%% -- but only if there's a decimal part to be displayed
 floatformat(Number, Place) when is_binary(Number) ->
     floatformat(binary_to_list(Number), Place);
 floatformat(Number, Place) ->
@@ -420,7 +420,7 @@ lower(Input) ->
     string:to_lower(Input).
 
 %% @doc Returns the value turned into a list. For an integer, it's a list of digits. 
-%% @doc For a string, it's a list of characters.
+%% For a string, it's a list of characters.
 %% Added this for DTL compatibility, but since strings are lists in Erlang, no need for this.
 make_list(Input) when is_binary(Input) ->
     make_list(binary_to_list(Input));
@@ -750,7 +750,7 @@ truncatewords_html(Input, Max) when is_binary(Input) ->
 truncatewords_html(Input, Max) ->
     truncatewords_html(Input, Max, [], [], text).
 
-%% @doc Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing <ul> tags. 
+%% @doc Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing `<ul>' tags. 
 unordered_list(List) ->
     String = lists:flatten(unordered_list(List, [])),
     string:substr(String, 5, erlang:length(String) - 9).
@@ -784,7 +784,7 @@ wordcount(Input) when is_binary(Input) ->
 wordcount(Input) when is_list(Input) ->
     wordcount(Input, 0).
 
-%% @doc Wraps words at specified line length, uses <BR/> html tag to delimit lines
+%% @doc Wraps words at specified line length, uses `<BR/>' html tag to delimit lines
 wordwrap(Input, Number) when is_binary(Input) ->
     wordwrap(binary_to_list(Input), Number);
 wordwrap(Input, Number) when is_list(Input) ->

+ 17 - 9
src/erlydtl_parser.yrl

@@ -100,6 +100,7 @@ Nonterminals
 
     SSITag
 
+    BlockTransBlock
     TransTag    
 
     TemplatetagTag
@@ -120,6 +121,7 @@ Terminals
     and_keyword
     autoescape_keyword
     block_keyword
+    blocktrans_keyword
     call_keyword
     close_tag
     close_var
@@ -129,6 +131,7 @@ Terminals
     empty_keyword
     endautoescape_keyword
     endblock_keyword
+    endblocktrans_keyword
     endcomment_keyword
     endfilter_keyword
     endfor_keyword
@@ -161,14 +164,14 @@ Terminals
     string_literal
     string
     templatetag_keyword
-        openblock_keyword
-        closeblock_keyword
-        openvariable_keyword
-        closevariable_keyword
-        openbrace_keyword
-        closebrace_keyword
-        opencomment_keyword
-        closecomment_keyword
+    openblock_keyword
+    closeblock_keyword
+    openvariable_keyword
+    closevariable_keyword
+    openbrace_keyword
+    closebrace_keyword
+    opencomment_keyword
+    closecomment_keyword
     trans_keyword
     widthratio_keyword
     with_keyword
@@ -176,7 +179,8 @@ Terminals
     '==' '!='
     '>=' '<='
     '>' '<'
-    '(' ')'.
+    '(' ')'
+    '_'.
 
 Rootsymbol
     Elements.
@@ -191,6 +195,7 @@ Elements -> '$empty' : [].
 Elements -> Elements string : '$1' ++ ['$2'].
 Elements -> Elements AutoEscapeBlock : '$1' ++ ['$2'].
 Elements -> Elements BlockBlock : '$1' ++ ['$2'].
+Elements -> Elements BlockTransBlock : '$1' ++ ['$2'].
 Elements -> Elements CallTag : '$1' ++ ['$2'].
 Elements -> Elements CallWithTag : '$1' ++ ['$2'].
 Elements -> Elements CommentBlock : '$1' ++ ['$2'].
@@ -313,6 +318,8 @@ SpacelessBlock -> open_tag spaceless_keyword close_tag Elements open_tag endspac
 SSITag -> open_tag ssi_keyword Value close_tag : {ssi, '$3'}.
 SSITag -> open_tag ssi_keyword string_literal parsed_keyword close_tag : {ssi_parsed, '$3'}.
 
+BlockTransBlock -> open_tag blocktrans_keyword identifier close_tag Elements open_tag endblocktrans_keyword close_tag : {blocktrans, '$3', '$5'}.
+
 TemplatetagTag -> open_tag templatetag_keyword Templatetag close_tag : {templatetag, '$3'}.
 
 Templatetag -> openblock_keyword : '$1'.
@@ -339,6 +346,7 @@ Filter -> identifier : ['$1'].
 Filter -> identifier ':' Literal : ['$1', '$3'].
 Filter -> identifier ':' Variable : ['$1', '$3'].
 
+Literal -> '_' '(' string_literal ')' : {trans, '$3'}.
 Literal -> string_literal : '$1'.
 Literal -> number_literal : '$1'.
 

+ 4 - 1
src/erlydtl_scanner.erl

@@ -102,7 +102,7 @@ scan([], Scanned, _, in_text) ->
 
                             "call", "with", "endwith",
                             
-                            "trans", "noop"
+                            "trans", "blocktrans", "endblocktrans", "noop"
                         ], 
                         Type = case lists:member(RevString, Keywords) of
                             true ->
@@ -252,6 +252,9 @@ scan(":" ++ T, Scanned, {Row, Column}, {_, Closer}) ->
 scan("." ++ T, Scanned, {Row, Column}, {_, Closer}) ->
     scan(T, [{'.', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer});
 
+scan("_(" ++ T, Scanned, {Row, Column}, {in_code, Closer}) ->
+    scan(T, lists:reverse([{'_', {Row, Column}}, {'(', {Row, Column + 1}}], Scanned), {Row, Column + 2}, {in_code, Closer});
+
 scan(" " ++ T, Scanned, {Row, Column}, {_, Closer}) ->
     scan(T, Scanned, {Row, Column + 1}, {in_code, Closer});
 

+ 21 - 13
tests/src/erlydtl_unittests.erl

@@ -922,21 +922,29 @@ tests() ->
                 <<"Hello {% trans \"Hi\" %}">>, [], <<"Hello Hi">>
             },
             {"trans functional reverse locale",
-                <<"Hello {% trans \"Hi\" %}">>, [], none, [{locale, "reverse"}], <<"Hello iH">>
+                <<"Hello {% trans \"Hi\" %}">>, [], [], [{locale, "reverse"}], <<"Hello iH">>
             },
             {"trans literal at run-time",
-                <<"Hello {% trans \"Hi\" %}">>, [], fun("Hi") -> "Konichiwa" end, [],
+                <<"Hello {% trans \"Hi\" %}">>, [], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [],
                 <<"Hello Konichiwa">>},
             {"trans variable at run-time",
-                <<"Hello {% trans var1 %}">>, [{var1, "Hi"}], fun("Hi") -> "Konichiwa" end, [],
+                <<"Hello {% trans var1 %}">>, [{var1, "Hi"}], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [],
                 <<"Hello Konichiwa">>},
             {"trans literal at run-time: No-op",
-                <<"Hello {% trans \"Hi\" noop %}">>, [], fun("Hi") -> "Konichiwa" end, [],
+                <<"Hello {% trans \"Hi\" noop %}">>, [], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [],
                 <<"Hello Hi">>},
             {"trans variable at run-time: No-op",
-                <<"Hello {% trans var1 noop %}">>, [{var1, "Hi"}], fun("Hi") -> "Konichiwa" end, [],
+                <<"Hello {% trans var1 noop %}">>, [{var1, "Hi"}], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [],
                 <<"Hello Hi">>}
         ]},
+    {"blocktrans",
+        [
+            {"blocktrans default locale",
+                <<"{% blocktrans foo %}Hello{% endblocktrans %}">>, [], <<"Hello">>},
+            {"blocktrans choose locale",
+                <<"{% blocktrans hello %}Hello, {{ name }}{% endblocktrans %}">>, [{name, "Mr. President"}], [{locale, "de"}],
+                [{blocktrans_locales, ["de"]}, {blocktrans_fun, fun(hello, "de") -> <<"Guten tag, {{ name }}">> end}], <<"Guten tag, Mr. President">>}
+        ]},
     {"widthratio", [
             {"Literals", <<"{% widthratio 5 10 100 %}">>, [], <<"50">>},
             {"Rounds up", <<"{% widthratio a b 100 %}">>, [{a, 175}, {b, 200}], <<"88">>}
@@ -959,23 +967,23 @@ run_tests() ->
                 lists:foldl(fun
                         ({Name, DTL, Vars, Output}, Acc) ->
                             process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, []),
-                                Vars, none, Output, Acc, Group, Name);
-                        ({Name, DTL, Vars, Dictionary, Output}, Acc) ->
+                                Vars, [], Output, Acc, Group, Name);
+                        ({Name, DTL, Vars, RenderOpts, Output}, Acc) ->
                             process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, []),
-                                Vars, Dictionary, Output, Acc, Group, Name);
-                        ({Name, DTL, Vars, Dictionary, CompilerOpts, Output}, Acc) ->
+                                Vars, RenderOpts, Output, Acc, Group, Name);
+                        ({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}, Acc) ->
                             process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts),
-                                Vars, Dictionary, Output, Acc, Group, Name)
+                                Vars, RenderOpts, Output, Acc, Group, Name)
                             end, GroupAcc, Assertions)
         end, [], tests()),
  
     io:format("Unit test failures: ~p~n", [lists:reverse(Failures)]).
  
-process_unit_test(CompiledTemplate, Vars, Dictionary, Output,Acc, Group, Name) ->
+process_unit_test(CompiledTemplate, Vars, RenderOpts, Output,Acc, Group, Name) ->
         case CompiledTemplate of
              {ok, _} ->
-                   {ok, IOList} = erlydtl_running_test:render(Vars, Dictionary),
-                   {ok, IOListBin} = erlydtl_running_test:render(vars_to_binary(Vars), Dictionary),
+                   {ok, IOList} = erlydtl_running_test:render(Vars, RenderOpts),
+                   {ok, IOListBin} = erlydtl_running_test:render(vars_to_binary(Vars), RenderOpts),
                    case {iolist_to_binary(IOList), iolist_to_binary(IOListBin)} of
                         {Output, Output} ->
                                   Acc;