Browse Source

New tags: ssi, filter, templatetag, and widthratio

Evan Miller 14 years ago
parent
commit
7bd6d356f4

+ 2 - 2
README.markdown

@@ -3,9 +3,9 @@ ErlyDTL
 
 ErlyDTL implements most but not all of the Django Template Language.
 
-*Supported tags*: autoescape, block, comment, cycle, extends, firstof, for, if, ifchanged, ifequal, ifnotequal, include, now, trans
+*Supported tags*: autoescape, block, comment, cycle, extends, filter, firstof, for, if, ifequal, ifnotequal, include, now, ssi, templatetag, trans, widthratio, with
 
-_Unsupported tags_: csrf_token, filter, regroup, spaceless, ssi, templatetag, url, widthratio
+_Unsupported tags_: csrf_token, ifchanged, regroup, spaceless, url
 
 *Supported filters*: add, addslashes, capfirst, center, cut, date, default, default_if_none, dictsort, dictsortreversed, divisibleby, escape, escapejs, filesizeformat, first, fix_ampersands, floatformat, force_escape, format_integer, format_number, get_digit, join, last, length, length_is, linebreaks, linebreaksbr, linenumbers, ljust, lower, make_list, phonenumeric, pluralize, pprint, random, random_num, random_range, removetags, rjust, safe, safeseq, slice, slugify, stringformat, striptags, time, timesince, timeuntil, title, truncatewords, truncatewords_html, unordered_list, upper, urlencode, urlize, urlizetrunc, wordcount, wordwrap, yesno
 

+ 150 - 38
src/erlydtl_compiler.erl

@@ -456,27 +456,37 @@ body_ast([{extends, {string_literal, _Pos, String}} | ThisParseTree], Context, T
 body_ast(DjangoParseTree, Context, TreeWalker) ->
     {AstInfoList, TreeWalker2} = lists:mapfoldl(
         fun
+            ({'autoescape', {identifier, _, OnOrOff}, Contents}, TreeWalkerAcc) ->
+                body_ast(Contents, Context#dtl_context{auto_escape = OnOrOff}, 
+                    TreeWalkerAcc);
             ({'block', {identifier, _, Name}, Contents}, TreeWalkerAcc) ->
                 Block = case dict:find(Name, Context#dtl_context.block_dict) of
-                    {ok, ChildBlock} ->
-                        ChildBlock;
-                    _ ->
-                        Contents
+                    {ok, ChildBlock} -> ChildBlock;
+                    _ -> Contents
                 end,
                 body_ast(Block, Context, TreeWalkerAcc);
+            ({'call', {'identifier', _, Name}}, TreeWalkerAcc) ->
+            	call_ast(Name, TreeWalkerAcc);
+            ({'call', {'identifier', _, Name}, With}, TreeWalkerAcc) ->
+            	call_with_ast(Name, With, Context, TreeWalkerAcc);
             ({'comment', _Contents}, TreeWalkerAcc) ->
                 empty_ast(TreeWalkerAcc);
+            ({'cycle', Names}, TreeWalkerAcc) ->
+                cycle_ast(Names, Context, TreeWalkerAcc);
+            ({'cycle_compat', Names}, TreeWalkerAcc) ->
+                cycle_compat_ast(Names, Context, TreeWalkerAcc);
             ({'date', 'now', {string_literal, _Pos, FormatString}}, TreeWalkerAcc) ->
                 now_ast(FormatString, Context, TreeWalkerAcc);
-            ({'autoescape', {identifier, _, OnOrOff}, Contents}, TreeWalkerAcc) ->
-                body_ast(Contents, Context#dtl_context{auto_escape = OnOrOff}, 
-                    TreeWalkerAcc);
-            ({'string', _Pos, String}, TreeWalkerAcc) -> 
-                string_ast(String, TreeWalkerAcc);
-	    ({'trans', Value}, TreeWalkerAcc) ->
-                translated_ast(Value, Context, TreeWalkerAcc);
-            ({'include', {string_literal, _, File}}, TreeWalkerAcc) ->
-                include_ast(unescape_string_literal(File), Context, TreeWalkerAcc);
+            ({'filter', FilterList, Contents}, TreeWalkerAcc) ->
+                filter_tag_ast(FilterList, Contents, Context, TreeWalkerAcc);
+            ({'firstof', Vars}, TreeWalkerAcc) ->
+                firstof_ast(Vars, Context, TreeWalkerAcc);
+            ({'for', {'in', IteratorList, Variable}, Contents}, TreeWalkerAcc) ->
+                {EmptyAstInfo, TreeWalker1} = empty_ast(TreeWalkerAcc),
+                for_loop_ast(IteratorList, Variable, Contents, EmptyAstInfo, Context, TreeWalker1);
+            ({'for', {'in', IteratorList, Variable}, Contents, EmptyPartContents}, TreeWalkerAcc) ->
+                {EmptyAstInfo, TreeWalker1} = body_ast(EmptyPartContents, Context, TreeWalkerAcc),
+                for_loop_ast(IteratorList, Variable, Contents, EmptyAstInfo, Context, TreeWalker1);
             ({'if', Expression, Contents}, TreeWalkerAcc) ->
                 {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc),
                 {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1),
@@ -501,24 +511,22 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
                 {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc),
                 {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1),
                 ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2);                    
-            ({'for', {'in', IteratorList, Variable}, Contents}, TreeWalkerAcc) ->
-                {EmptyAstInfo, TreeWalker1} = empty_ast(TreeWalkerAcc),
-                for_loop_ast(IteratorList, Variable, Contents, EmptyAstInfo, Context, TreeWalker1);
-            ({'for', {'in', IteratorList, Variable}, Contents, EmptyPartContents}, TreeWalkerAcc) ->
-                {EmptyAstInfo, TreeWalker1} = body_ast(EmptyPartContents, Context, TreeWalkerAcc),
-                for_loop_ast(IteratorList, Variable, Contents, EmptyAstInfo, Context, TreeWalker1);
+            ({'include', {string_literal, _, File}}, TreeWalkerAcc) ->
+                include_ast(unescape_string_literal(File), Context, TreeWalkerAcc);
+            ({'ssi', Arg}, TreeWalkerAcc) ->
+                ssi_ast(Arg, Context, TreeWalkerAcc);
+            ({'string', _Pos, String}, TreeWalkerAcc) -> 
+                string_ast(String, TreeWalkerAcc);
             ({'tag', {'identifier', _, Name}, Args}, TreeWalkerAcc) ->
                 tag_ast(Name, Args, Context, TreeWalkerAcc);            
-            ({'call', {'identifier', _, Name}}, TreeWalkerAcc) ->
-            	call_ast(Name, TreeWalkerAcc);
-            ({'call', {'identifier', _, Name}, With}, TreeWalkerAcc) ->
-            	call_with_ast(Name, With, Context, TreeWalkerAcc);
-            ({'firstof', Vars}, TreeWalkerAcc) ->
-                firstof_ast(Vars, Context, TreeWalkerAcc);
-            ({'cycle', Names}, TreeWalkerAcc) ->
-                cycle_ast(Names, Context, TreeWalkerAcc);
-            ({'cycle_compat', Names}, TreeWalkerAcc) ->
-                cycle_compat_ast(Names, Context, TreeWalkerAcc);
+            ({'templatetag', {_, _, TagName}}, TreeWalkerAcc) ->
+                templatetag_ast(TagName, Context, TreeWalkerAcc);
+	    ({'trans', Value}, TreeWalkerAcc) ->
+                translated_ast(Value, Context, TreeWalkerAcc);
+            ({'widthratio', Numerator, Denominator, Scale}, TreeWalkerAcc) ->
+                widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalkerAcc);
+            ({'with', Args, Contents}, TreeWalkerAcc) ->
+                with_ast(Args, Contents, Context, TreeWalkerAcc);
             (ValueToken, TreeWalkerAcc) -> 
                 {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, Context, TreeWalkerAcc),
                 {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker}
@@ -640,6 +648,36 @@ translated_ast2(NewStrAst, DefaultStringAst, AstInfo, TreeWalker) ->
         [NewStrAst, erl_syntax:variable("TranslationFun"), DefaultStringAst]),
     {{StringLookupAst, AstInfo}, TreeWalker}.
 
+% Completely unnecessary in ErlyDTL (use {{ "{%" }} etc), but implemented for compatibility.
+templatetag_ast('openblock', _Context, TreeWalker) ->
+    {{erl_syntax:string("{%"), #ast_info{}}, TreeWalker};
+templatetag_ast('closeblock', _Context, TreeWalker) ->
+    {{erl_syntax:string("%}"), #ast_info{}}, TreeWalker};
+templatetag_ast('openvariable', _Context, TreeWalker) ->
+    {{erl_syntax:string("{{"), #ast_info{}}, TreeWalker};
+templatetag_ast('closevariable', _Context, TreeWalker) ->
+    {{erl_syntax:string("}}"), #ast_info{}}, TreeWalker};
+templatetag_ast('openbrace', _Context, TreeWalker) ->
+    {{erl_syntax:string("{"), #ast_info{}}, TreeWalker};
+templatetag_ast('closebrace', _Context, TreeWalker) ->
+    {{erl_syntax:string("}"), #ast_info{}}, TreeWalker};
+templatetag_ast('opencomment', _Context, TreeWalker) ->
+    {{erl_syntax:string("{#"), #ast_info{}}, TreeWalker};
+templatetag_ast('closecomment', _Context, TreeWalker) ->
+    {{erl_syntax:string("#}"), #ast_info{}}, TreeWalker}.
+
+
+widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalker) ->
+    {{NumAst, NumInfo}, TreeWalker1} = value_ast(Numerator, false, Context, TreeWalker),
+    {{DenAst, DenInfo}, TreeWalker2} = value_ast(Denominator, false, Context, TreeWalker1),
+    {{ScaleAst, ScaleInfo}, TreeWalker3} = value_ast(Scale, false, Context, TreeWalker2),
+    {{format_number_ast(erl_syntax:application(
+                erl_syntax:atom(erlydtl_runtime),
+                erl_syntax:atom(widthratio),
+                [NumAst, DenAst, ScaleAst])), merge_info(ScaleInfo, merge_info(NumInfo, DenInfo))},
+        TreeWalker3}.
+
+
 string_ast(String, TreeWalker) ->
     {{erl_syntax:string(String), #ast_info{}}, TreeWalker}. %% less verbose AST, better for development and debugging
     % {{erl_syntax:binary([erl_syntax:binary_field(erl_syntax:integer(X)) || X <- String]), #ast_info{}}, TreeWalker}.       
@@ -656,6 +694,54 @@ include_ast(File, Context, TreeWalker) ->
     end.
     
 
+filter_tag_ast(FilterList, Contents, Context, TreeWalker) ->
+    {{InnerAst, Info}, TreeWalker1} = body_ast(Contents, Context#dtl_context{auto_escape = did}, TreeWalker),
+    {{FilteredAst, FilteredInfo}, TreeWalker2} = lists:foldl(fun
+            ([{identifier, _, 'escape'}], {{AstAcc, InfoAcc}, TreeWalkerAcc}) ->
+                {{AstAcc, InfoAcc}, TreeWalkerAcc#treewalker{safe = true}};
+            ([{identifier, _, 'safe'}], {{AstAcc, InfoAcc}, TreeWalkerAcc}) ->
+                {{AstAcc, InfoAcc}, TreeWalkerAcc#treewalker{safe = true}};
+            ([{identifier, _, 'safeseq'}], {{AstAcc, InfoAcc}, TreeWalkerAcc}) ->
+                {{AstAcc, InfoAcc}, TreeWalkerAcc#treewalker{safe = true}};
+            (Filter, {{AstAcc, InfoAcc}, TreeWalkerAcc}) ->
+                {Ast, AstInfo} = filter_ast1(Filter, AstAcc, Context),
+                {{Ast, merge_info(InfoAcc, AstInfo)}, TreeWalkerAcc}
+        end, {{erl_syntax:application(
+                    erl_syntax:atom(lists),
+                    erl_syntax:atom(flatten),
+                    [InnerAst]), Info}, TreeWalker1}, FilterList),
+
+    EscapedAst = case search_for_escape_filter(lists:reverse(FilterList), Context) of
+        on ->
+            erl_syntax:application(
+                erl_syntax:atom(erlydtl_filters), 
+                erl_syntax:atom(force_escape), 
+                [FilteredAst]);
+        _ ->
+            FilteredAst
+    end,
+    {{EscapedAst, FilteredInfo}, TreeWalker2}.
+
+search_for_escape_filter(FilterList, #dtl_context{auto_escape = on}) ->
+    search_for_safe_filter(FilterList);
+search_for_escape_filter(_, #dtl_context{auto_escape = did}) ->
+    off;
+search_for_escape_filter([[{identifier, _, 'escape'}]|Rest], _Context) ->
+    search_for_safe_filter(Rest);
+search_for_escape_filter([_|Rest], Context) ->
+    search_for_escape_filter(Rest, Context);
+search_for_escape_filter([], _Context) ->
+    off.
+
+search_for_safe_filter([[{identifier, _, 'safe'}]|_]) ->
+    off;
+search_for_safe_filter([[{identifier, _, 'safeseq'}]|_]) ->
+    off;
+search_for_safe_filter([_|Rest]) ->
+    search_for_safe_filter(Rest);
+search_for_safe_filter([]) ->
+    on.
+
 filter_ast(Variable, Filter, Context, TreeWalker) ->
     % the escape filter is special; it is always applied last, so we have to go digging for it
 
@@ -663,16 +749,16 @@ filter_ast(Variable, Filter, Context, TreeWalker) ->
     % so don't do any more escaping
     {{UnescapedAst, Info}, TreeWalker2} = filter_ast_noescape(Variable, Filter, 
         Context#dtl_context{auto_escape = did}, TreeWalker),
-    case search_for_escape_filter(Variable, Filter, Context) of
+    EscapedAst = case search_for_escape_filter(Variable, Filter, Context) of
         on ->
-            {{erl_syntax:application(
-                    erl_syntax:atom(erlydtl_filters), 
-                    erl_syntax:atom(force_escape), 
-                    [UnescapedAst]), 
-                Info}, TreeWalker2};
-        _ ->
-            {{UnescapedAst, Info}, TreeWalker2}
-    end.
+            erl_syntax:application(
+                erl_syntax:atom(erlydtl_filters), 
+                erl_syntax:atom(force_escape), 
+                [UnescapedAst]);
+        _ -> 
+            UnescapedAst
+    end,
+    {{EscapedAst, Info}, TreeWalker2}.
 
 filter_ast_noescape(Variable, [{identifier, _, 'escape'}], Context, TreeWalker) ->
     value_ast(Variable, true, Context, TreeWalker#treewalker{safe = true});
@@ -785,6 +871,24 @@ ifelse_ast(Expression, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseCo
                 [ElseContentsAst])
         ]), merge_info(ExpressionInfo, Info)}, TreeWalker1}.
 
+with_ast(ArgList, Contents, Context, TreeWalker) ->
+    {ArgAstList, {ArgInfo, TreeWalker1}} = lists:mapfoldl(fun
+            ({{identifier, _, _LocalVarName}, Value}, {AstInfo1, TreeWalker1}) ->
+                {{Ast, Info}, TreeWalker2} = value_ast(Value, false, Context, TreeWalker1),
+                {Ast, {merge_info(AstInfo1, Info), TreeWalker2}}
+        end, {#ast_info{}, TreeWalker}, ArgList),
+
+    NewScope = lists:map(fun({{identifier, _, LocalVarName}, _Value}) ->
+                    {LocalVarName, erl_syntax:variable(lists:concat(["Var_", LocalVarName]))}
+            end, ArgList),
+
+    {{InnerAst, InnerInfo}, TreeWalker2} = body_ast(Contents,
+        Context#dtl_context{local_scopes = [NewScope|Context#dtl_context.local_scopes]}, TreeWalker1),
+
+    {{erl_syntax:application(
+                erl_syntax:fun_expr([
+            erl_syntax:clause(lists:map(fun({_, Var}) -> Var end, NewScope), none,
+                [InnerAst])]), ArgAstList), merge_info(ArgInfo, InnerInfo)}, TreeWalker2}.
 
 for_loop_ast(IteratorList, LoopValue, Contents, {EmptyContentsAst, EmptyContentsInfo}, Context, TreeWalker) ->
     Vars = lists:map(fun({identifier, _, Iterator}) -> 
@@ -864,6 +968,14 @@ now_ast(FormatString, _Context, TreeWalker) ->
         [erl_syntax:string(UnescapeOuter)]),
         #ast_info{}}, TreeWalker}.
 
+ssi_ast(FileName, Context, TreeWalker) ->
+    {{Ast, Info}, TreeWalker1} = value_ast(FileName, true, Context, TreeWalker),
+    {Mod, Fun} = Context#dtl_context.reader,
+    {{erl_syntax:application(
+                erl_syntax:atom(erlydtl_runtime),
+                erl_syntax:atom(read_file),
+                [erl_syntax:atom(Mod), erl_syntax:atom(Fun), Ast]), Info}, TreeWalker1}.
+
 unescape_string_literal(String) ->
     unescape_string_literal(string:strip(String, both, 34), [], noslash).
 

+ 94 - 33
src/erlydtl_parser.yrl

@@ -38,9 +38,13 @@ Nonterminals
 
     ValueBraced
 
-    ExtendsTag
-    IncludeTag
-    NowTag
+    Value
+    Variable
+    Filter
+    
+    AutoEscapeBlock
+    AutoEscapeBraced
+    EndAutoEscapeBraced
 
     BlockBlock
     BlockBraced
@@ -54,10 +58,19 @@ Nonterminals
     CycleNames
     CycleNamesCompat
 
+    ExtendsTag
+    IncludeTag
+    NowTag
+
     FirstofTag
     FirstofList
     FirstofValues
 
+    FilterBlock
+    FilterBraced
+    EndFilterBraced
+    Filters
+
     ForBlock
     ForBraced
     EmptyBraced
@@ -81,19 +94,22 @@ Nonterminals
     IfNotEqualExpression
     EndIfNotEqualBraced      
 
-    AutoEscapeBlock
-    AutoEscapeBraced
-    EndAutoEscapeBraced
-
-    Value
-    Variable
-    Filter
-    
     CustomTag
     Args
 
+    SSITag
+
     TransTag    
 
+    TemplatetagTag
+    Templatetag
+
+    WidthRatioTag
+
+    WithBlock
+    WithBraced
+    EndWithBraced
+
     CallTag
     CallWithTag
     
@@ -113,11 +129,14 @@ Terminals
     endautoescape_keyword
     endblock_keyword
     endcomment_keyword
+    endfilter_keyword
     endfor_keyword
     endif_keyword
     endifequal_keyword
     endifnotequal_keyword
+    endwith_keyword
     extends_keyword
+    filter_keyword
     firstof_keyword
     for_keyword
     identifier
@@ -133,9 +152,20 @@ Terminals
     or_keyword
     open_tag
     open_var
+    ssi_keyword
     string_literal
     string
+    templatetag_keyword
+        openblock_keyword
+        closeblock_keyword
+        openvariable_keyword
+        closevariable_keyword
+        openbrace_keyword
+        closebrace_keyword
+        opencomment_keyword
+        closecomment_keyword
     trans_keyword
+    widthratio_keyword
     with_keyword
     ',' '|' '=' ':' '.'
     '==' '!='
@@ -154,23 +184,28 @@ Unary 600 Unot.
 
 Elements -> '$empty' : [].
 Elements -> Elements string : '$1' ++ ['$2'].
-Elements -> Elements TransTag : '$1' ++ ['$2'].
-Elements -> Elements ValueBraced : '$1' ++ ['$2'].
-Elements -> Elements ExtendsTag : '$1' ++ ['$2'].
-Elements -> Elements IncludeTag : '$1' ++ ['$2'].
-Elements -> Elements NowTag : '$1' ++ ['$2'].
-Elements -> Elements CycleTag : '$1' ++ ['$2'].
+Elements -> Elements AutoEscapeBlock : '$1' ++ ['$2'].
 Elements -> Elements BlockBlock : '$1' ++ ['$2'].
+Elements -> Elements CallTag : '$1' ++ ['$2'].
+Elements -> Elements CallWithTag : '$1' ++ ['$2'].
+Elements -> Elements CommentBlock : '$1' ++ ['$2'].
+Elements -> Elements CustomTag : '$1' ++ ['$2'].
+Elements -> Elements CycleTag : '$1' ++ ['$2'].
+Elements -> Elements ExtendsTag : '$1' ++ ['$2'].
+Elements -> Elements FilterBlock : '$1' ++ ['$2'].
 Elements -> Elements FirstofTag : '$1' ++ ['$2'].
 Elements -> Elements ForBlock : '$1' ++ ['$2'].
 Elements -> Elements IfBlock : '$1' ++ ['$2'].
 Elements -> Elements IfEqualBlock : '$1' ++ ['$2'].
 Elements -> Elements IfNotEqualBlock : '$1' ++ ['$2'].
-Elements -> Elements AutoEscapeBlock : '$1' ++ ['$2'].
-Elements -> Elements CommentBlock : '$1' ++ ['$2'].
-Elements -> Elements CustomTag : '$1' ++ ['$2'].
-Elements -> Elements CallTag : '$1' ++ ['$2'].
-Elements -> Elements CallWithTag : '$1' ++ ['$2'].
+Elements -> Elements IncludeTag : '$1' ++ ['$2'].
+Elements -> Elements NowTag : '$1' ++ ['$2'].
+Elements -> Elements SSITag : '$1' ++ ['$2'].
+Elements -> Elements TemplatetagTag : '$1' ++ ['$2'].
+Elements -> Elements TransTag : '$1' ++ ['$2'].
+Elements -> Elements ValueBraced : '$1' ++ ['$2'].
+Elements -> Elements WidthRatioTag : '$1' ++ ['$2'].
+Elements -> Elements WithBlock : '$1' ++ ['$2'].
 
 ValueBraced -> open_var Value close_var : '$2'.
 
@@ -181,19 +216,18 @@ Value -> Literal : '$1'.
 Variable -> identifier : {variable, '$1'}.
 Variable -> Variable '.' identifier : {attribute, {'$3', '$1'}}.
 
-TransTag -> open_tag trans_keyword string_literal close_tag : {trans, '$3'}.
-TransTag -> open_tag trans_keyword Variable close_tag : {trans, '$3'}.
-TransTag -> open_tag trans_keyword string_literal noop_keyword close_tag : '$3'.
-TransTag -> open_tag trans_keyword Variable noop_keyword close_tag : '$3'.
-
-ExtendsTag -> open_tag extends_keyword string_literal close_tag : {extends, '$3'}.
-IncludeTag -> open_tag include_keyword string_literal close_tag : {include, '$3'}.
-NowTag -> open_tag now_keyword string_literal close_tag : {date, now, '$3'}.
+AutoEscapeBlock -> AutoEscapeBraced Elements EndAutoEscapeBraced : {autoescape, '$1', '$2'}.
+AutoEscapeBraced -> open_tag autoescape_keyword identifier close_tag : '$3'.
+EndAutoEscapeBraced -> open_tag endautoescape_keyword close_tag.
 
 BlockBlock -> BlockBraced Elements EndBlockBraced : {block, '$1', '$2'}.
 BlockBraced -> open_tag block_keyword identifier close_tag : '$3'.
 EndBlockBraced -> open_tag endblock_keyword close_tag.
 
+ExtendsTag -> open_tag extends_keyword string_literal close_tag : {extends, '$3'}.
+IncludeTag -> open_tag include_keyword string_literal close_tag : {include, '$3'}.
+NowTag -> open_tag now_keyword string_literal close_tag : {date, now, '$3'}.
+
 CommentBlock -> CommentBraced Elements EndCommentBraced : {comment, '$2'}.
 CommentBraced -> open_tag comment_keyword close_tag.
 EndCommentBraced -> open_tag endcomment_keyword close_tag.
@@ -208,6 +242,13 @@ CycleNamesCompat -> identifier ',' : ['$1'].
 CycleNamesCompat -> CycleNamesCompat identifier ',' : '$1' ++ ['$2'].
 CycleNamesCompat -> CycleNamesCompat identifier : '$1' ++ ['$2'].
 
+FilterBlock -> FilterBraced Elements EndFilterBraced : {filter, '$1', '$2'}.
+FilterBraced -> open_tag filter_keyword Filters close_tag : '$3'.
+EndFilterBraced -> open_tag endfilter_keyword close_tag.
+
+Filters -> Filter : ['$1'].
+Filters -> Filters '|' Filter : '$1' ++ ['$3'].
+
 FirstofTag -> open_tag firstof_keyword FirstofList close_tag : '$3'.
 FirstofList -> FirstofValues : {firstof, '$1'}.
 FirstofValues -> FirstofValues Value : ['$2'|'$1'].
@@ -256,9 +297,29 @@ IfNotEqualBraced -> open_tag ifnotequal_keyword IfNotEqualExpression Value close
 IfNotEqualExpression -> Value : '$1'.
 EndIfNotEqualBraced -> open_tag endifnotequal_keyword close_tag.
 
-AutoEscapeBlock -> AutoEscapeBraced Elements EndAutoEscapeBraced : {autoescape, '$1', '$2'}.
-AutoEscapeBraced -> open_tag autoescape_keyword identifier close_tag : '$3'.
-EndAutoEscapeBraced -> open_tag endautoescape_keyword close_tag.
+SSITag -> open_tag ssi_keyword Value close_tag : {ssi, '$3'}.
+
+TemplatetagTag -> open_tag templatetag_keyword Templatetag close_tag : {templatetag, '$3'}.
+
+Templatetag -> openblock_keyword : '$1'.
+Templatetag -> closeblock_keyword : '$1'.
+Templatetag -> openvariable_keyword : '$1'.
+Templatetag -> closevariable_keyword : '$1'.
+Templatetag -> openbrace_keyword : '$1'.
+Templatetag -> closebrace_keyword : '$1'.
+Templatetag -> opencomment_keyword : '$1'.
+Templatetag -> closecomment_keyword : '$1'.
+
+TransTag -> open_tag trans_keyword string_literal close_tag : {trans, '$3'}.
+TransTag -> open_tag trans_keyword Variable close_tag : {trans, '$3'}.
+TransTag -> open_tag trans_keyword string_literal noop_keyword close_tag : '$3'.
+TransTag -> open_tag trans_keyword Variable noop_keyword close_tag : '$3'.
+
+WidthRatioTag -> open_tag widthratio_keyword Value Value number_literal close_tag : {widthratio, '$3', '$4', '$5'}.
+
+WithBlock -> WithBraced Elements EndWithBraced : {with, '$1', '$2'}.
+WithBraced -> open_tag with_keyword Args close_tag : '$3'.
+EndWithBraced -> open_tag endwith_keyword close_tag.
 
 Filter -> identifier : ['$1'].
 Filter -> identifier ':' Literal : ['$1', '$3'].

+ 8 - 0
src/erlydtl_runtime.erl

@@ -189,3 +189,11 @@ increment_counter_stats([{counter, Counter}, {counter0, Counter0}, {revcounter,
 
 cycle(NamesTuple, Counters) when is_tuple(NamesTuple) ->
     element(fetch_value(counter0, Counters) rem size(NamesTuple) + 1, NamesTuple).
+
+widthratio(Numerator, Denominator, Scale) ->
+    round(Numerator / Denominator * Scale).
+
+read_file(Module, Function, FileName) ->
+    FileName = filename:absname(FileName),
+    {ok, Binary} = Module:Function(FileName),
+    binary_to_list(Binary).

+ 6 - 6
src/erlydtl_scanner.erl

@@ -70,7 +70,7 @@ scan([], Scanned, _, in_text) ->
                             
                             "extends", 
 
-                            %TODO "filter", "endfilter",
+                            "filter", "endfilter",
 
                             "firstof",
 
@@ -78,7 +78,7 @@ scan([], Scanned, _, in_text) ->
 
                             "if", "else", "endif", "not", "or", "and", 
 
-                            "ifchanged", 
+                            %TODO "ifchanged", 
                             
                             "ifequal", "endifequal", 
 
@@ -92,15 +92,15 @@ scan([], Scanned, _, in_text) ->
                             
                             %TODO "spaceless", "endspaceless", 
                             
-                            %TODO "ssi", 
+                            "ssi", 
                             
-                            %TODO "templatetag",
+                            "templatetag", "openblock", "closeblock", "openvariable", "closevariable", "openbrace", "closebrace", "opencomment", "closecomment",
 
                             %TODO "url",
 
-                            %TODO "widthratio",
+                            "widthratio",
 
-                            "call", "with", %TODO "endwith",
+                            "call", "with", "endwith",
                             
                             "trans", "noop"
                         ], 

+ 4 - 1
tests/src/erlydtl_functional_tests.erl

@@ -45,7 +45,7 @@ test_list() ->
         "for_records_preset", "include", "if", "if_preset", "ifequal",
         "ifequal_preset", "ifnotequal", "ifnotequal_preset", "now",
         "var", "var_preset", "cycle", "custom_tag", "custom_call", 
-        "include_template", "include_path",
+        "include_template", "include_path", "ssi",
         "extends_path", "extends_path2", "trans" ].
 
 setup_compile("for_list_preset") ->
@@ -154,6 +154,9 @@ setup("extends_path2") ->
 setup("trans") ->
     RenderVars = [{locale, "reverse"}],
     {ok, RenderVars};
+setup("ssi") ->
+    RenderVars = [{path, filename:absname(filename:join(["tests", "input", "ssi_include.html"]))}],
+    {ok, RenderVars};
 
 
 %%--------------------------------------------------------------------       

+ 50 - 22
tests/src/erlydtl_unittests.erl

@@ -84,27 +84,6 @@ tests() ->
                {"now functional",
                   <<"It is the {% now \"jS o\\f F Y\" %}.">>, [{var1, ""}], generate_test_date()}
             ]},
-        {"trans",
-                [
-                {"trans functional default locale",
-                  <<"Hello {% trans \"Hi\" %}">>, [], <<"Hello Hi">>
-                },
-                {"trans functional reverse locale",
-                    <<"Hello {% trans \"Hi\" %}">>, [], none, [{locale, "reverse"}], <<"Hello iH">>
-                },
-                {"trans literal at run-time",
-                    <<"Hello {% trans \"Hi\" %}">>, [], fun("Hi") -> "Konichiwa" end, [],
-                    <<"Hello Konichiwa">>},
-                {"trans variable at run-time",
-                    <<"Hello {% trans var1 %}">>, [{var1, "Hi"}], fun("Hi") -> "Konichiwa" end, [],
-                    <<"Hello Konichiwa">>},
-                {"trans literal at run-time: No-op",
-                    <<"Hello {% trans \"Hi\" noop %}">>, [], fun("Hi") -> "Konichiwa" end, [],
-                    <<"Hello Hi">>},
-                {"trans variable at run-time: No-op",
-                    <<"Hello {% trans var1 noop %}">>, [{var1, "Hi"}], fun("Hi") -> "Konichiwa" end, [],
-                    <<"Hello Hi">>}
-        ]},
         {"if", [
                 {"If/else",
                     <<"{% if var1 %}boo{% else %}yay{% endif %}">>, [{var1, ""}], <<"yay">>},
@@ -339,7 +318,13 @@ tests() ->
                     <<"{% ifnotequal \"foo\" var1 %}yay{% else %}boo{% endifnotequal %}">>,
                     [{var1, "bar"}], <<"yay">>}
             ]},
-       {"filters", [
+        {"filter tag", [
+                {"Apply a filter",
+                    <<"{% filter escape %}&{% endfilter %}">>, [], <<"&amp;">>},
+                {"Chained filters",
+                    <<"{% filter linebreaksbr|escape %}\n{% endfilter %}">>, [], <<"&lt;br /&gt;">>}
+            ]},
+        {"filters", [
                {"Filter a literal",
                     <<"{{ \"pop\"|capfirst }}">>, [],
                     <<"Pop">>},
@@ -903,6 +888,49 @@ tests() ->
                 <<"{% firstof foo bar \"baz\" %}">>,
                 [],
                 <<"baz">>}
+        ]},
+    {"templatetag", [
+            {"openblock", <<"{% templatetag openblock %}">>, [], <<"{%">>},
+            {"closeblock", <<"{% templatetag closeblock %}">>, [], <<"%}">>},
+            {"openvariable", <<"{% templatetag openvariable %}">>, [], <<"{{">>},
+            {"closevariable", <<"{% templatetag closevariable %}">>, [], <<"}}">>},
+            {"openbrace", <<"{% templatetag openbrace %}">>, [], <<"{">>},
+            {"closebrace", <<"{% templatetag closebrace %}">>, [], <<"}">>},
+            {"opencomment", <<"{% templatetag opencomment %}">>, [], <<"{#">>},
+            {"closecomment", <<"{% templatetag closecomment %}">>, [], <<"#}">>}
+        ]},
+    {"trans",
+        [
+            {"trans functional default locale",
+                <<"Hello {% trans \"Hi\" %}">>, [], <<"Hello Hi">>
+            },
+            {"trans functional reverse locale",
+                <<"Hello {% trans \"Hi\" %}">>, [], none, [{locale, "reverse"}], <<"Hello iH">>
+            },
+            {"trans literal at run-time",
+                <<"Hello {% trans \"Hi\" %}">>, [], fun("Hi") -> "Konichiwa" end, [],
+                <<"Hello Konichiwa">>},
+            {"trans variable at run-time",
+                <<"Hello {% trans var1 %}">>, [{var1, "Hi"}], fun("Hi") -> "Konichiwa" end, [],
+                <<"Hello Konichiwa">>},
+            {"trans literal at run-time: No-op",
+                <<"Hello {% trans \"Hi\" noop %}">>, [], fun("Hi") -> "Konichiwa" end, [],
+                <<"Hello Hi">>},
+            {"trans variable at run-time: No-op",
+                <<"Hello {% trans var1 noop %}">>, [{var1, "Hi"}], fun("Hi") -> "Konichiwa" end, [],
+                <<"Hello Hi">>}
+        ]},
+    {"widthratio", [
+            {"Literals", <<"{% widthratio 5 10 100 %}">>, [], <<"50">>},
+            {"Rounds up", <<"{% widthratio a b 100 %}">>, [{a, 175}, {b, 200}], <<"88">>}
+        ]},
+    {"with", [
+            {"Cache literal",
+                <<"{% with a=1 %}{{ a }}{% endwith %}">>, [], <<"1">>},
+            {"Cache variable",
+                <<"{% with a=b %}{{ a }}{% endwith %}">>, [{b, "foo"}], <<"foo">>},
+            {"Cache multiple",
+                <<"{% with alpha=1 beta=b %}{{ alpha }}/{{ beta }}{% endwith %}">>, [{b, 2}], <<"1/2">>}
         ]}
     ].