Commits

Anonymous committed fa760e8

1. Can resolve variables from function calls to parameterized modules, e.g.

-module(foo, [Var]).
get_var() -> Var.
...
render([{var1, foo:new("bar")}]).
...
{{ var1.get_var }} => <<"bar">>

2. Support for recursive variable attributes, e.g. {{ var.attr.attr.attr }}

Tests added for both.

Comments (0)

Files changed (5)

src/erlydtl/erlydtl_compiler.erl

             ({'comment', _Contents}, TreeWalkerAcc) ->
                 empty_ast(TreeWalkerAcc);
             ({'autoescape', {identifier, _, OnOrOff}, Contents}, TreeWalkerAcc) ->
-                body_ast(Contents, Context#dtl_context{auto_escape = list_to_atom(OnOrOff)}, TreeWalkerAcc);
+                body_ast(Contents, Context#dtl_context{auto_escape = list_to_atom(OnOrOff)}, 
+                    TreeWalkerAcc);
             ({'text', _Pos, String}, TreeWalkerAcc) -> 
                 string_ast(String, TreeWalkerAcc);
             ({'string_literal', _Pos, String}, TreeWalkerAcc) ->
-                {{auto_escape(erl_syntax:string(unescape_string_literal(String)), Context), #ast_info{}}, TreeWalkerAcc};
+                {{auto_escape(erl_syntax:string(unescape_string_literal(String)), Context), 
+                        #ast_info{}}, TreeWalkerAcc};
             ({'number_literal', _Pos, Number}, TreeWalkerAcc) ->
                 string_ast(Number, TreeWalkerAcc);
-            ({'variable', Variable}, TreeWalkerAcc) ->
+            ({'attribute', _} = Variable, TreeWalkerAcc) ->
+                {Ast, VarName} = resolve_variable_ast(Variable, Context),
+                {{format(Ast, Context), #ast_info{var_names = [VarName]}}, TreeWalkerAcc};
+            ({'variable', _} = Variable, TreeWalkerAcc) ->
                 {Ast, VarName} = resolve_variable_ast(Variable, Context),
                 {{format(Ast, Context), #ast_info{var_names = [VarName]}}, TreeWalkerAcc};              
             ({'include', {string_literal, _, File}}, TreeWalkerAcc) ->
                 include_ast(unescape_string_literal(File), Context, TreeWalkerAcc);
-            ({'if', {variable, Variable}, Contents}, TreeWalkerAcc) ->
+            ({'if', {'not', Variable}, Contents}, TreeWalkerAcc) ->
+                {IfAstInfo, TreeWalker1} = empty_ast(TreeWalkerAcc),
+                {ElseAstInfo, TreeWalker2} = body_ast(Contents, Context, TreeWalker1),
+                ifelse_ast(Variable, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
+            ({'if', Variable, Contents}, TreeWalkerAcc) ->
                 {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
                 {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
                 ifelse_ast(Variable, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-            ({'if', {'not', {variable, Variable}}, Contents}, TreeWalkerAcc) ->
-                {IfAstInfo, TreeWalker1} = empty_ast(TreeWalkerAcc),
-                {ElseAstInfo, TreeWalker2} = body_ast(Contents, Context, TreeWalker1),
-                ifelse_ast(Variable, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-            ({'ifelse', {variable, Variable}, IfContents, ElseContents}, TreeWalkerAcc) ->
+            ({'ifelse', {'not', Variable}, IfContents, ElseContents}, TreeWalkerAcc) ->
+                {IfAstInfo, TreeWalker1} = body_ast(ElseContents, Context, TreeWalkerAcc),
+                {ElseAstInfo, TreeWalker2} = body_ast(IfContents, Context, TreeWalker1),
+                ifelse_ast(Variable, IfAstInfo, ElseAstInfo, Context, TreeWalker2);                  
+            ({'ifelse', Variable, IfContents, ElseContents}, TreeWalkerAcc) ->
                 {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
                 {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
                 ifelse_ast(Variable, IfAstInfo, ElseAstInfo, Context, TreeWalker2);
-            ({'ifelse', {'not', {variable, Variable}}, IfContents, ElseContents}, TreeWalkerAcc) ->
-                {IfAstInfo, TreeWalker1} = body_ast(ElseContents, Context, TreeWalkerAcc),
-                {ElseAstInfo, TreeWalker2} = body_ast(IfContents, Context, TreeWalker1),
-                ifelse_ast(Variable, IfAstInfo, ElseAstInfo, Context, TreeWalker2);                  
             ({'ifequal', Args, Contents}, TreeWalkerAcc) ->
                 {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
                 {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
                 tag_ast(Name, Args, Context, TreeWalkerAcc);            
             ({'call', {'identifier', _, Name}}, TreeWalkerAcc) ->
             	call_ast(Name, TreeWalkerAcc);
-            ({'call', {'identifier', _, Name}, {variable, With}}, TreeWalkerAcc) ->
-            	call_with_ast(Name, With, Context, TreeWalkerAcc)                
+            ({'call', {'identifier', _, Name}, With}, TreeWalkerAcc) ->
+            	call_with_ast(Name, With, Context, TreeWalkerAcc)
         end, TreeWalker, DjangoParseTree),   
     {AstList, {Info, TreeWalker3}} = lists:mapfoldl(
         fun({Ast, Info}, {InfoAcc, TreeWalkerAcc}) -> 
 resolve_ifvariable_ast(VarTuple, Context) ->
     resolve_variable_ast(VarTuple, Context, 'find_value').
            
-resolve_variable_ast({{identifier, _, VarName}}, Context, FinderFunction) ->
-    {resolve_variable_name_ast(VarName, Context, FinderFunction), VarName};
+resolve_variable_ast({attribute, {{identifier, _, AttrName}, Variable}}, Context, FinderFunction) ->
+    {VarAst, VarName} = resolve_variable_ast(Variable, Context, FinderFunction),
+    {erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction),
+                    [erl_syntax:atom(AttrName), VarAst]), VarName};
 
-resolve_variable_ast({{identifier, _, VarName}, {identifier, _, AttrName}}, Context, FinderFunction) ->
-    {erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction),
-                    [erl_syntax:atom(AttrName), resolve_variable_name_ast(VarName, Context)]), VarName}.
-
-resolve_variable_name_ast(VarName, Context) ->
-    resolve_variable_name_ast(VarName, Context, 'fetch_value').
-    
-resolve_variable_name_ast(VarName, Context, FinderFunction) ->
+resolve_variable_ast({variable, {identifier, _, VarName}}, Context, FinderFunction) ->
     VarValue = lists:foldl(fun(Scope, Value) ->
                 case Value of
-                    undefined ->
-                        proplists:get_value(list_to_atom(VarName), Scope);
-                    _ ->
-                        Value
+                    undefined -> proplists:get_value(list_to_atom(VarName), Scope);
+                    _ -> Value
                 end
         end, undefined, Context#dtl_context.local_scopes),
-    case VarValue of
+    VarValue1 = case VarValue of
         undefined ->
             erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction),
                 [erl_syntax:atom(VarName), erl_syntax:variable("Variables")]);
         _ ->
             VarValue
-    end.
-
+    end,
+    {VarValue1, VarName}.
 
 format(Ast, Context) ->
     auto_escape(format_integer_ast(Ast), Context).
     {[Arg1Ast, Arg2Ast], VarNames} = lists:foldl(fun
             (X, {Asts, AccVarNames}) ->
                 case X of
-                    {variable, Var} ->
-                        {Ast, VarName} = resolve_ifvariable_ast(Var, Context),
-                        {[Ast | Asts], [VarName | AccVarNames]};
                     {string_literal, _, Literal} ->
                         {[erl_syntax:string(unescape_string_literal(Literal)) | Asts], AccVarNames};
                     {number_literal, _, Literal} ->
-                        {[erl_syntax:integer(list_to_integer(Literal)) | Asts], AccVarNames}
+                        {[erl_syntax:integer(list_to_integer(Literal)) | Asts], AccVarNames};
+                    Variable ->
+                        {Ast, VarName} = resolve_ifvariable_ast(Variable, Context),
+                        {[Ast | Asts], [VarName | AccVarNames]}
                 end                
         end,
         {[], Info#ast_info.var_names},
     {{Ast, Info#ast_info{var_names = VarNames}}, TreeWalker}.         
 
 
-for_loop_ast(IteratorList, {variable, Variable}, Contents, Context, TreeWalker) ->
+for_loop_ast(IteratorList, Variable, Contents, Context, TreeWalker) ->
     Vars = lists:map(fun({identifier, _, Iterator}) -> 
                     erl_syntax:variable("Var_" ++ Iterator) 
             end, IteratorList),
             InterpretedArgs = lists:map(fun
                     ({{identifier, _, Key}, {string_literal, _, Value}}) ->
                         {list_to_atom(Key), erl_syntax:string(unescape_string_literal(Value))};
-                    ({{identifier, _, Key}, {variable, Value}}) ->
+                    ({{identifier, _, Key}, Value}) ->
                         {list_to_atom(Key), format(resolve_variable_ast(Value, Context), Context)}
                 end, Args),
             DefaultFilePath = filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags", Name]),

src/erlydtl/erlydtl_parser.yrl

     Elements
     Literal
 
-    VariableBraced
+    ValueBraced
 
     ExtendsTag
     IncludeTag
     AutoEscapeBraced
     EndAutoEscapeBraced
 
+    Value
     Variable
     Filter
     
 
 Elements -> '$empty' : [].
 Elements -> Elements text : '$1' ++ ['$2'].
-Elements -> Elements VariableBraced : '$1' ++ ['$2'].
+Elements -> Elements ValueBraced : '$1' ++ ['$2'].
 Elements -> Elements ExtendsTag : '$1' ++ ['$2'].
 Elements -> Elements IncludeTag : '$1' ++ ['$2'].
 Elements -> Elements LoadTag : '$1' ++ ['$2'].
 Elements -> Elements CallTag : '$1' ++ ['$2'].
 Elements -> Elements CallWithTag : '$1' ++ ['$2'].
 
-VariableBraced -> open_var Variable close_var : '$2'.
+ValueBraced -> open_var Value close_var : '$2'.
 
-Variable -> Variable pipe Filter : {apply_filter, '$1', '$3'}.
-Variable -> identifier : {variable, {'$1'}}.
-Variable -> identifier dot identifier : {variable, {'$1', '$3'}}.
-Variable -> string_literal : '$1'.
-Variable -> number_literal : '$1'.
+Value -> Value pipe Filter : {apply_filter, '$1', '$3'}.
+Value -> Variable : '$1'.
+Value -> Literal : '$1'.
+
+Variable -> identifier : {variable, '$1'}.
+Variable -> Value dot identifier : {attribute, {'$3', '$1'}}.
 
 ExtendsTag -> open_tag extends_keyword string_literal close_tag : {extends, '$3'}.
 IncludeTag -> open_tag include_keyword string_literal close_tag : {include, '$3'}.
 ForBlock -> ForBraced Elements EndForBraced : {for, '$1', '$2'}.
 ForBraced -> open_tag for_keyword ForExpression close_tag : '$3'.
 EndForBraced -> open_tag endfor_keyword close_tag.
-ForExpression -> Variable : '$1'.
 ForExpression -> ForGroup in_keyword Variable : {'in', '$1', '$3'}.
 ForGroup -> identifier : ['$1'].
 ForGroup -> ForGroup comma identifier : '$1' ++ ['$3'].
 IfBlock -> IfBraced Elements EndIfBraced : {'if', '$1', '$2'}.
 IfBraced -> open_tag if_keyword IfExpression close_tag : '$3'.
 IfExpression -> not_keyword IfExpression : {'not', '$2'}.
-IfExpression -> Variable : '$1'.
+IfExpression -> Value : '$1'.
 
 ElseBraced -> open_tag else_keyword close_tag.
 EndIfBraced -> open_tag endif_keyword close_tag.
 
 IfEqualBlock -> IfEqualBraced Elements ElseBraced Elements EndIfEqualBraced : {ifequalelse, '$1', '$2', '$4'}.
 IfEqualBlock -> IfEqualBraced Elements EndIfEqualBraced : {ifequal, '$1', '$2'}.
-IfEqualBraced -> open_tag ifequal_keyword IfEqualExpression Variable close_tag : ['$3', '$4'].
-IfEqualExpression -> Variable : '$1'.
+IfEqualBraced -> open_tag ifequal_keyword IfEqualExpression Value close_tag : ['$3', '$4'].
+IfEqualExpression -> Value : '$1'.
 EndIfEqualBraced -> open_tag endifequal_keyword close_tag.
 
 IfNotEqualBlock -> IfNotEqualBraced Elements ElseBraced Elements EndIfNotEqualBraced : {ifnotequalelse, '$1', '$2', '$4'}.
 IfNotEqualBlock -> IfNotEqualBraced Elements EndIfNotEqualBraced : {ifnotequal, '$1', '$2'}.
-IfNotEqualBraced -> open_tag ifnotequal_keyword IfNotEqualExpression Variable close_tag : ['$3', '$4'].
-IfNotEqualExpression -> Variable : '$1'.
+IfNotEqualBraced -> open_tag ifnotequal_keyword IfNotEqualExpression Value close_tag : ['$3', '$4'].
+IfNotEqualExpression -> Value : '$1'.
 EndIfNotEqualBraced -> open_tag endifnotequal_keyword close_tag.
 
 AutoEscapeBlock -> AutoEscapeBraced Elements EndAutoEscapeBraced : {autoescape, '$1', '$2'}.
 CustomTag -> open_tag identifier Args close_tag : {tag, '$2', '$3'}.
 
 Args -> '$empty' : [].
-Args -> Args identifier equal Variable : '$1' ++ [{'$2', '$4'}].
+Args -> Args identifier equal Value : '$1' ++ [{'$2', '$4'}].
 
 CallTag -> open_tag call_keyword identifier close_tag : {call, '$3'}.
-CallWithTag -> open_tag call_keyword identifier with_keyword Variable close_tag: {call, '$3', '$5'}.
+CallWithTag -> open_tag call_keyword identifier with_keyword Value close_tag : {call, '$3', '$5'}.

src/erlydtl/erlydtl_runtime.erl

 
 find_value(Key, L) when is_list(L) ->
     proplists:get_value(Key, L);
-find_value(Key, {GBSize, GBData}) ->
+find_value(Key, {GBSize, GBData}) when is_integer(GBSize) ->
     case gb_trees:lookup(Key, {GBSize, GBData}) of
         {value, Val} ->
             Val;
         _ ->
             undefined
     end;
-find_value(Key, Dict) ->
-    case dict:find(Key, Dict) of
-        {ok, Val} ->
-            Val;
-        _ ->
-            undefined
+find_value(Key, Tuple) when is_tuple(Tuple) ->
+    Module = element(1, Tuple),
+    case Module of
+        dict -> 
+            case dict:find(Key, Tuple) of
+                {ok, Val} ->
+                    Val;
+                _ ->
+                    undefined
+            end;
+        Module ->
+            case proplists:get_value(Key, Module:module_info(exports)) of
+                1 ->
+                    Tuple:Key();
+                _ ->
+                    undefined
+            end
     end.
 
 fetch_value(Key, Data) ->

src/tests/erlydtl_example_variable_storage.erl

+-module(erlydtl_example_variable_storage, [SomeVar]).
+-compile(export_all).
+
+some_var() ->
+    SomeVar.

src/tests/erlydtl_unittests.erl

                 {"Render variable with attribute in dict",
                     <<"{{ var1.attr }}">>, [{var1, dict:store(attr, "Othello", dict:new())}], <<"Othello">>},
                 {"Render variable with attribute in gb_tree",
-                    <<"{{ var1.attr }}">>, [{var1, gb_trees:insert(attr, "Othello", gb_trees:empty())}], <<"Othello">>}
+                    <<"{{ var1.attr }}">>, [{var1, gb_trees:insert(attr, "Othello", gb_trees:empty())}], <<"Othello">>},
+                {"Render variable in parameterized module",
+                    <<"{{ var1.some_var }}">>, [{var1, erlydtl_example_variable_storage:new("foo")}], <<"foo">>},
+                {"Nested attributes",
+                    <<"{{ person.city.state.country }}">>, [{person, [{city, [{state, [{country, "Italy"}]}]}]}],
+                    <<"Italy">>}
             ]},
         {"if", [
                 {"If/else",
                 {"Resolve variable attribute",
                     <<"{% for number in person.numbers %}{{ number }}\n{% endfor %}">>, [{person, [{numbers, ["411", "911"]}]}],
                     <<"411\n911\n">>},
+                {"Resolve nested variable attribute",
+                    <<"{% for number in person.home.numbers %}{{ number }}\n{% endfor %}">>, [{person, [{home, [{numbers, ["411", "911"]}]}]}],
+                    <<"411\n911\n">>},
                 {"Nested for loop",
                     <<"{% for outer in list %}{% for inner in outer %}{{ inner }}\n{% endfor %}{% endfor %}">>,
                     [{'list', [["Al", "Albert"], ["Jo", "Joseph"]]}],