Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

* Support for unpacking tuples in a for loop.

* "plus" filter for increasing a number. This isn't in Django, but I needed it...
* Call integer_to_list before rendering a single integer
* Set forloop.counter and forloop.counter0 inside for loops
* Minor adjustment for coding style in erlydtl_scanner.erl
  • Loading branch information...
commit 677116dc35b98da4c054a330340758e0ceea19cb 1 parent 7ebb7dd
@evanmiller evanmiller authored
View
4 demo/out/test_filters.html
@@ -24,8 +24,12 @@
left
</pre>
+Line breaks: Line 1<br />Line 2<br />Line 3
+
Lowercase: lowercase
+Plus: 2 + 2 = 4
+
Right adjust:
<pre>
right
View
8 demo/out/test_for.html
@@ -2,12 +2,12 @@
<ul>
- <li>apple</li>
+<li>1. apple</li>
- <li>banana</li>
+<li>2. banana</li>
- <li>coconut</li>
+<li>3. coconut</li>
</ul>
-after
+after
View
6 demo/out/test_for_list.html
@@ -1,7 +1,7 @@
-More than one apple is called "apples".
+More than one apple is called "apples". Only $1 each!
-More than one banana is called "bananas".
+More than one banana is called "bananas". Only $2 each!
-More than one coconut is called "coconuts".
+More than one coconut is called "coconuts". Only $500 each!
View
7 demo/out/test_for_tuple.html
@@ -0,0 +1,7 @@
+
+One apple, two apples!
+
+One banana, two bananas!
+
+One coconut, two coconuts!
+
View
4 demo/templates/test_filters.html
@@ -24,8 +24,12 @@
{{ "left"|ljust:20 }}
</pre>
+Line breaks: {{ "Line 1\nLine 2\nLine 3"|linebreaksbr }}
+
Lowercase: {{ "LOWERCASE"|lower }}
+Plus: 2 + 2 = {{ 2|plus:2 }}
+
Right adjust:
<pre>
{{ "right"|rjust:20 }}
View
4 demo/templates/test_for.html
@@ -2,8 +2,8 @@
<ul>
{% for iterator in fruit_list %}
- <li>{{ iterator }}</li>
+<li>{{ forloop.counter }}. {{ iterator }}</li>
{% endfor %}
</ul>
-after
+after
View
4 demo/templates/test_for_list.html
@@ -1,3 +1,3 @@
-{% for singular, plural in fruit_list %}
-More than one {{ singular }} is called "{{ plural }}".
+{% for singular, plural, price in fruit_list %}
+More than one {{ singular }} is called "{{ plural }}". Only {{ price }} each!
{% endfor %}
View
3  demo/templates/test_for_tuple.html
@@ -0,0 +1,3 @@
+{% for singular, plural in fruit_list %}
+One {{ singular }}, two {{ plural }}!
+{% endfor %}
View
12 src/demo/erlydtl_demo.erl
@@ -77,6 +77,7 @@ compile_all() ->
compile("for_list_preset"),
compile("for_records"),
compile("for_records_preset"),
+ compile("for_tuple"),
compile("htmltags"),
compile("if"),
compile("if_preset"),
@@ -146,6 +147,9 @@ compile("for_records" = Name) ->
compile("for_list" = Name) ->
compile(Name, ".html", []);
+
+compile("for_tuple" = Name) ->
+ compile(Name, ".html", []);
compile("for_list_preset" = Name) ->
Vars = [{fruit_list, [["apple", "apples"], ["banana", "bananas"], ["coconut", "coconuts"]]}],
@@ -200,6 +204,7 @@ render_all() ->
render("for"),
render("for_preset"),
render("for_list"),
+ render("for_tuple"),
render("for_list_preset"),
render("for_records"),
render("for_records_preset"),
@@ -260,7 +265,12 @@ render("for_preset" = Name) ->
render(Name, []);
render("for_list" = Name) ->
- render(Name, [{fruit_list, [["apple", "apples"], ["banana", "bananas"], ["coconut", "coconuts"]]}]);
+ %render(Name, [{fruit_list, [["apple", "apples"], ["banana", "bananas"], ["coconut", "coconuts"]]}]);
+ render(Name, [{fruit_list, [["apple", "apples", "$1"], ["banana", "bananas", "$2"], ["coconut", "coconuts", "$500"]]}]);
+
+render("for_tuple" = Name) ->
+ %render(Name, [{fruit_list, [{"apple", "apples", "$1"}, {"banana", "bananas", "$2"}, {"coconut", "coconuts", "$500"}]}]);
+ render(Name, [{fruit_list, [{"apple", "apples"}, {"banana", "bananas"}, {"coconut", "coconuts"}]}]);
render("for_list_preset" = Name) ->
render(Name, []);
View
143 src/erlydtl/erlydtl_compiler.erl
@@ -35,7 +35,7 @@
-author('rsaccon@gmail.com').
-author('emmiller@gmail.com').
--export([compile/2, compile/3, compile/4, compile/5, compile/6, parse/1, scan/1]).
+-export([compile/2, compile/3, compile/4, compile/5, compile/6, parse/1, scan/1, body_ast/2]).
-record(dtl_context, {
local_scopes = [],
@@ -68,12 +68,12 @@ compile(File, Module, DocRoot, Vars, Function, OutDir) ->
{ok, DjangoParseTree} ->
OldProcessDictVal = put(erlydtl_counter, 0),
- {BodyAst, BodyInfo} = body_ast(DjangoParseTree,
- #dtl_context{doc_root = DocRoot, parse_trail = [File], preset_vars = Vars}),
+ {BodyAst, BodyInfo} = body_ast(DjangoParseTree, #dtl_context{
+ doc_root = DocRoot, parse_trail = [File], preset_vars = Vars}),
Render0FunctionAst = erl_syntax:function(erl_syntax:atom(Function),
[erl_syntax:clause([], none, [erl_syntax:application(none,
- erl_syntax:atom(Function), [erl_syntax:list([])])])]),
+ erl_syntax:atom(Function), [erl_syntax:list([])])])]),
Function2 = erl_syntax:application(none, erl_syntax:atom(Function ++ "2"),
[erl_syntax:variable("Variables")]),
@@ -83,44 +83,45 @@ compile(File, Module, DocRoot, Vars, Function, OutDir) ->
[erl_syntax:tuple([erl_syntax:atom(error), erl_syntax:variable("Err")])]),
Render1FunctionAst = erl_syntax:function(erl_syntax:atom(Function),
[erl_syntax:clause([erl_syntax:variable("Variables")], none,
- [erl_syntax:try_expr([Function2], [ClauseOk], [ClauseCatch])])]),
-
+ [erl_syntax:try_expr([Function2], [ClauseOk], [ClauseCatch])])]),
+
SourceFunctionAst = erl_syntax:function(
erl_syntax:atom(source),
- [erl_syntax:clause([], none, [erl_syntax:string(File)])]),
-
+ [erl_syntax:clause([], none, [erl_syntax:string(File)])]),
+
DependenciesFunctionAst = erl_syntax:function(
erl_syntax:atom(dependencies), [erl_syntax:clause([], none,
- [erl_syntax:list(lists:map(fun(Dep) -> erl_syntax:string(Dep) end,
- BodyInfo#ast_info.dependencies))])]),
-
+ [erl_syntax:list(lists:map(fun(Dep) -> erl_syntax:string(Dep) end,
+ BodyInfo#ast_info.dependencies))])]),
+
RenderInternalFunctionAst = erl_syntax:function(
erl_syntax:atom(Function ++ "2"),
- [erl_syntax:clause([erl_syntax:variable("Variables")], none,
+ [erl_syntax:clause([erl_syntax:variable("Variables")], none,
[BodyAst])]),
-
+
ProplistsClauseErr = erl_syntax:clause([erl_syntax:atom(undefined)], none,
- [erl_syntax:application(none, erl_syntax:atom(throw),
- [erl_syntax:tuple([erl_syntax:atom(undefined_variable), erl_syntax:variable("Key")])])]),
+ [erl_syntax:string("")]),
+ %[erl_syntax:application(none, erl_syntax:atom(throw),
+ % [erl_syntax:tuple([erl_syntax:atom(undefined_variable), erl_syntax:variable("Key")])])]),
ProplistsClauseOk = erl_syntax:clause([erl_syntax:variable("Val")], none,
[erl_syntax:variable("Val")]),
ProplistsFunctionAst = erl_syntax:function(erl_syntax:atom(get_value),
[erl_syntax:clause([erl_syntax:variable("Key"), erl_syntax:variable("L")], none,
- [erl_syntax:case_expr(erl_syntax:application(erl_syntax:atom(proplists),
- erl_syntax:atom(get_value), [erl_syntax:variable("Key"), erl_syntax:variable("L")]),
- [ProplistsClauseErr, ProplistsClauseOk])])]),
+ [erl_syntax:case_expr(erl_syntax:application(erl_syntax:atom(proplists),
+ erl_syntax:atom(get_value), [erl_syntax:variable("Key"), erl_syntax:variable("L")]),
+ [ProplistsClauseErr, ProplistsClauseOk])])]),
ModuleAst = erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(Module)]),
ExportAst = erl_syntax:attribute(erl_syntax:atom(export),
[erl_syntax:list([erl_syntax:arity_qualifier(erl_syntax:atom(Function), erl_syntax:integer(0)),
- erl_syntax:arity_qualifier(erl_syntax:atom(Function), erl_syntax:integer(1)),
- erl_syntax:arity_qualifier(erl_syntax:atom(source), erl_syntax:integer(0)),
+ erl_syntax:arity_qualifier(erl_syntax:atom(Function), erl_syntax:integer(1)),
+ erl_syntax:arity_qualifier(erl_syntax:atom(source), erl_syntax:integer(0)),
erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0))])]),
-
+
Forms = [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, Render0FunctionAst,
- Render1FunctionAst, SourceFunctionAst, DependenciesFunctionAst, RenderInternalFunctionAst,
+ Render1FunctionAst, SourceFunctionAst, DependenciesFunctionAst, RenderInternalFunctionAst,
ProplistsFunctionAst | BodyInfo#ast_info.pre_render_asts]],
-
+
case OldProcessDictVal of
undefined -> erase(erlydtl_counter);
_ -> put(erlydtl_counter, OldProcessDictVal)
@@ -138,14 +139,15 @@ compile(File, Module, DocRoot, Vars, Function, OutDir) ->
{error, Reason} ->
{error, lists:concat(["beam generation failed (", Reason, "): ", Path])}
end;
- _ ->
- {error, "compilation failed"}
+ error ->
+ {error, "compilation failed"};
+ Other ->
+ Other
end;
Error ->
Error
end.
-
scan(File) ->
case file:read_file(File) of
{ok, B} ->
@@ -237,10 +239,8 @@ body_ast(DjangoParseTree, Context) ->
body_ast(IfContents, Context), Context);
({'apply_filter', Variable, Filter}) ->
filter_ast(Variable, Filter, Context);
- ({'for', {'in', {identifier, _, Iterator}, {identifier, _, List}}, Contents}) ->
- for_loop_ast(Iterator, List, Contents, Context);
- ({'for', {'in', IteratorList, {identifier, _, List}}, Contents}) when is_list(IteratorList) ->
- for_list_loop_ast(IteratorList, List, Contents, Context)
+ ({'for', {'in', IteratorList, {identifier, _, List}}, Contents}) ->
+ for_loop_ast(IteratorList, List, Contents, Context)
end, DjangoParseTree),
{AstList, Info} = lists:mapfoldl(
fun({Ast, Info}, InfoAcc) ->
@@ -354,13 +354,13 @@ resolve_ifvariable_ast(VarTuple, Context) ->
resolve_variable_ast(VarTuple, Context, erl_syntax:atom(proplists)).
resolve_variable_ast({{identifier, _, VarName}}, Context, ModuleAst) ->
- {auto_escape(resolve_variable_name_ast(VarName, Context, ModuleAst), Context), VarName};
+ {auto_escape(format_integer_ast(resolve_variable_name_ast(VarName, Context, ModuleAst)), Context), VarName};
resolve_variable_ast({{identifier, _, VarName}, {identifier, _, AttrName}}, Context, ModuleAst) ->
- {auto_escape(erl_syntax:application(
- ModuleAst, erl_syntax:atom(get_value),
- [erl_syntax:atom(AttrName), resolve_variable_name_ast(VarName, Context)]),
- Context), []}.
+ {auto_escape(format_integer_ast(erl_syntax:application(
+ ModuleAst, erl_syntax:atom(get_value),
+ [erl_syntax:atom(AttrName), resolve_variable_name_ast(VarName, Context)])),
+ Context), []}.
resolve_variable_name_ast(VarName, Context) ->
resolve_variable_name_ast(VarName, Context, none).
@@ -376,11 +376,15 @@ resolve_variable_name_ast(VarName, Context, ModuleAst) ->
end, undefined, Context#dtl_context.local_scopes),
case VarValue of
undefined ->
- erl_syntax:application(ModuleAst, erl_syntax:atom(get_value),
+ erl_syntax:application(ModuleAst, erl_syntax:atom('get_value'),
[erl_syntax:atom(VarName), erl_syntax:variable("Variables")]);
_ ->
VarValue
- end.
+ end.
+
+format_integer_ast(Ast) ->
+ erl_syntax:application(erl_syntax:atom(erlydtl_filters), erl_syntax:atom(format_integer),
+ [Ast]).
auto_escape(Value, Context) ->
case Context#dtl_context.auto_escape of
@@ -400,7 +404,9 @@ ifelse_ast(Variable, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseCont
[ElseContentsAst]),
erl_syntax:clause([erl_syntax:atom(undefined)], none,
[ElseContentsAst]),
- erl_syntax:clause([erl_syntax:integer(0)], none,
+ erl_syntax:clause([erl_syntax:atom(false)], none,
+ [ElseContentsAst]),
+ erl_syntax:clause([erl_syntax:string("0")], none,
[ElseContentsAst]),
erl_syntax:clause([erl_syntax:underscore()], none,
[IfContentsAst])
@@ -428,33 +434,46 @@ ifequalelse_ast(Args, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseCon
erl_syntax:list([Arg1Ast, Arg2Ast])]),
{Ast, Info#ast_info{var_names = VarNames}}.
-for_loop_ast(Iterator, List, Contents, Context) ->
- {InnerAst, Info} = body_ast(Contents,
- Context#dtl_context{local_scopes =
- [[{list_to_atom(Iterator), erl_syntax:variable("Var_" ++ Iterator)}]
- | Context#dtl_context.local_scopes]}),
- {erl_syntax:application(erl_syntax:atom(lists), erl_syntax:atom(map),
- [erl_syntax:fun_expr([
- erl_syntax:clause([erl_syntax:variable("Var_" ++ Iterator)],
- none, [InnerAst])]),
- resolve_variable_name_ast(list_to_atom(List), Context)]), Info#ast_info{var_names = [List]}}.
-
-for_list_loop_ast(IteratorList, List, Contents, Context) ->
- Vars = erl_syntax:list(lists:map(
- fun({identifier, _, Iterator}) ->
+for_loop_ast(IteratorList, List, Contents, Context) ->
+ Vars = lists:map(fun({identifier, _, Iterator}) ->
erl_syntax:variable("Var_" ++ Iterator)
- end, IteratorList)),
+ end, IteratorList),
+ CounterVars = erl_syntax:list([
+ erl_syntax:tuple([erl_syntax:atom('counter'), erl_syntax:variable("Counter")]),
+ erl_syntax:tuple([erl_syntax:atom('counter0'), erl_syntax:variable("Counter0")])
+ ]),
{InnerAst, Info} = body_ast(Contents,
- Context#dtl_context{local_scopes = [lists:map(
+ Context#dtl_context{local_scopes = [
+ [{'forloop', CounterVars} | lists:map(
fun({identifier, _, Iterator}) ->
{list_to_atom(Iterator), erl_syntax:variable("Var_" ++ Iterator)}
- end, IteratorList) | Context#dtl_context.local_scopes]}),
- {erl_syntax:application(erl_syntax:atom(lists), erl_syntax:atom(map),
- [erl_syntax:fun_expr([erl_syntax:clause(
- [Vars], none, [InnerAst])]),
- resolve_variable_name_ast(list_to_atom(List), Context)]), Info#ast_info{var_names = [List]}}.
-
-%% TODO: implement "laod" tag to make custom tags work like in original django
+ end, IteratorList)] | Context#dtl_context.local_scopes]}),
+ CounterAst = erl_syntax:list([
+ erl_syntax:tuple([erl_syntax:atom('counter'),
+ erl_syntax:infix_expr(erl_syntax:variable("Counter"), erl_syntax:operator("+"), erl_syntax:integer(1))]),
+ erl_syntax:tuple([erl_syntax:atom('counter0'),
+ erl_syntax:infix_expr(erl_syntax:variable("Counter0"), erl_syntax:operator("+"), erl_syntax:integer(1))])
+ ]),
+ ListAst = resolve_variable_name_ast(List, Context),
+ CounterVars0 = erl_syntax:list([
+ erl_syntax:tuple([erl_syntax:atom('counter'), erl_syntax:integer(1)]),
+ erl_syntax:tuple([erl_syntax:atom('counter0'), erl_syntax:integer(0)])
+ ]),
+ {erl_syntax:application(
+ erl_syntax:atom('erlang'), erl_syntax:atom('element'),
+ [erl_syntax:integer(1), erl_syntax:application(
+ erl_syntax:atom('lists'), erl_syntax:atom('mapfoldl'),
+ [erl_syntax:fun_expr([
+ erl_syntax:clause([erl_syntax:tuple(Vars), CounterVars], none,
+ [erl_syntax:tuple([InnerAst, CounterAst])]),
+ erl_syntax:clause(case Vars of [H] -> [H, CounterVars];
+ _ -> [erl_syntax:list(Vars), CounterVars] end, none,
+ [erl_syntax:tuple([InnerAst, CounterAst])])
+ ]),
+ CounterVars0, ListAst])]),
+ Info#ast_info{var_names = [List]}}.
+
+%% TODO: implement "load" tag to make custom tags work like in original django
tag_ast(Name, Args, Context) ->
InterpretedArgs = lists:map(fun
({{identifier, _, Key}, {string_literal, _, Value}}) ->
@@ -489,4 +508,4 @@ unescape_string_literal("t" ++ Rest, Acc, slash) ->
unescape_string_literal(Rest, ["\t" | Acc], noslash);
unescape_string_literal([C | Rest], Acc, slash) ->
unescape_string_literal(Rest, [C | Acc], noslash).
-
+
View
43 src/erlydtl/erlydtl_filters.erl
@@ -50,21 +50,38 @@ first([[First|_Rest]]) ->
fix_ampersands(Input) ->
fix_ampersands(lists:flatten(Input), []).
+force_escape(Input) when is_list(Input) ->
+ escape(lists:flatten(Input), []);
+force_escape(Input) when is_binary(Input) ->
+ escape(binary_to_list(Input), []);
force_escape(Input) ->
- escape(lists:flatten(Input), []).
+ Input.
-join([Input], Separator) ->
+format_integer(Input) ->
+ case Input of
+ N when is_integer(N) ->
+ integer_to_list(N);
+ Other ->
+ Other
+ end.
+
+join([Input], Separator) when is_list(Input) ->
string:join(Input, Separator).
-last([Input]) ->
+last([Input]) when is_list(Input) ->
[lists:last(Input)].
-length([Input]) ->
+length([Input]) when is_list(Input) ->
+ integer_to_list(erlang:length(Input));
+length(Input) when is_list(Input) ->
integer_to_list(erlang:length(Input)).
-length_is([Input], Number) ->
+length_is(Input, Number) when is_list(Input) ->
lists:concat([erlang:length(Input) =:= Number]).
+linebreaksbr(Input) ->
+ linebreaksbr(lists:flatten(Input), []).
+
ljust(Input, Number) ->
string:left(lists:flatten(Input), Number).
@@ -74,10 +91,18 @@ lower(Input) ->
rjust(Input, Number) ->
string:right(lists:flatten(Input), Number).
+plus([Input], Number) when is_list(Input) ->
+ integer_to_list(list_to_integer(Input) + Number);
+plus(Input, Number) when is_list(Input) ->
+ integer_to_list(list_to_integer(Input) + Number);
+plus(Input, Number) when is_integer(Input) ->
+ Input + Number.
+
upper(Input) ->
string:to_upper(lists:flatten(Input)).
% internal
+
escape([], Acc) ->
lists:reverse(Acc);
escape("<" ++ Rest, Acc) ->
@@ -100,3 +125,11 @@ fix_ampersands("&" ++ Rest, Acc) ->
fix_ampersands([C | Rest], Acc) ->
fix_ampersands(Rest, [C | Acc]).
+linebreaksbr([], Acc) ->
+ lists:reverse(Acc);
+linebreaksbr("\r\n" ++ Rest, Acc) ->
+ linebreaksbr(Rest, lists:reverse("<br />", Acc));
+linebreaksbr("\n" ++ Rest, Acc) ->
+ linebreaksbr(Rest, lists:reverse("<br />", Acc));
+linebreaksbr([C | Rest], Acc) ->
+ linebreaksbr(Rest, [C | Acc]).
View
3,098 src/erlydtl/erlydtl_parser.erl
1,595 additions, 1,503 deletions not shown
View
6 src/erlydtl/erlydtl_parser.yrl
@@ -161,8 +161,8 @@ ForBraced -> open_tag for_keyword ForExpression close_tag : '$3'.
EndForBraced -> open_tag endfor_keyword close_tag.
ForExpression -> identifier : '$1'.
ForExpression -> ForGroup in_keyword identifier : {'in', '$1', '$3'}.
-ForGroup -> identifier : '$1'.
-ForGroup -> ForGroup comma identifier : ['$1', '$3'].
+ForGroup -> identifier : ['$1'].
+ForGroup -> ForGroup comma identifier : '$1' ++ ['$3'].
IfBlock -> IfBraced Elements ElseBraced Elements EndIfBraced : {ifelse, '$1', '$2', '$4'}.
IfBlock -> IfBraced Elements EndIfBraced : {'if', '$1', '$2'}.
@@ -193,4 +193,4 @@ Filter -> identifier : ['$1'].
Filter -> identifier colon Literal : ['$1', '$3'].
Literal -> string_literal : '$1'.
-Literal -> number_literal : '$1'.
+Literal -> number_literal : '$1'.
View
4 src/erlydtl/erlydtl_scanner.erl
@@ -199,7 +199,7 @@ scan([H | T], Scanned, {Row, Column}, {in_identifier, Closer}) ->
append_char(Scanned, Char) ->
[String | Scanned1] = Scanned,
- [{element(1, String), element(2, String), [Char | element(3, String)]} | Scanned1].
+ [setelement(3, String, [Char | element(3, String)]) | Scanned1].
append_text_char(Scanned, {Row, Column}, Char) ->
case length(Scanned) of
@@ -223,4 +223,4 @@ char_type(Char) ->
digit;
_ ->
undefined
- end.
+ end.
Please sign in to comment.
Something went wrong with that request. Please try again.