From bb6c24e94da80e18c7641e9352419c9c4c86ed9e Mon Sep 17 00:00:00 2001 From: Guido Witmond Date: Wed, 6 Jun 2012 00:29:13 +0200 Subject: [PATCH 001/361] Added clauses to truncate null-values and integers correctly. You get these with the postgres-adapter from boss_db. Error messages are: {function_clause, [{erlydtl_filters,truncatewords, [1800,8,[]], [{file,"src/erlydtl_filters.erl"},{line,994}]}, ... and function_clause, [{erlydtl_filters,truncatewords, [null,8,[]], [{file,"src/erlydtl_filters.erl"},{line,994}]}, ... --- src/erlydtl_filters.erl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index 90d6168..96f3a5f 100755 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -752,6 +752,10 @@ title(Input) when is_list(Input) -> %% @doc Truncates a string after a certain number of characters. truncatechars(_Input, Max) when Max =< 0 -> ""; +truncatechars(null, Max) -> + truncatechars("null", Max); +truncatechars(Input, Max) when is_integer(Input) -> + truncatechars(integer_to_list(Input), Max); truncatechars(Input, Max) when is_binary(Input) -> list_to_binary(truncatechars(binary_to_list(Input), Max)); truncatechars(Input, Max) -> @@ -760,6 +764,10 @@ truncatechars(Input, Max) -> %% @doc Truncates a string after a certain number of words. truncatewords(_Input, Max) when Max =< 0 -> ""; +truncatewords(null, Max) -> + truncatewords("null", Max); +truncatewords(Input, Max) when is_integer(Input) -> + truncatewords(integer_to_list(Input), Max); truncatewords(Input, Max) when is_binary(Input) -> list_to_binary(truncatewords(binary_to_list(Input), Max)); truncatewords(Input, Max) -> From 313edd5edd376ca12b773285a261086138120f20 Mon Sep 17 00:00:00 2001 From: Guido Witmond Date: Thu, 19 Jul 2012 15:24:59 +0200 Subject: [PATCH 002/361] Added filter date_unix_epoch that displays unix time in seconds since 1-1-1970. Added clauses to display 'undefined' for date and floatformat when input is undefined. This prevents an rendering error. --- src/erlydtl_filters.erl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index 96f3a5f..90c0bc2 100755 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -53,6 +53,7 @@ cut/2, date/1, date/2, + date_unix_epoch/1, default/2, default_if_none/2, dictsort/2, @@ -195,8 +196,12 @@ date(Input) -> date(Input, "F j, Y"). %% @doc Formats a date according to the given format. +date(undefined, _) -> "undefined"; date(Input, FormatStr) when is_binary(Input) -> list_to_binary(date(binary_to_list(Input), FormatStr)); +%% @doc format calendar:gregorian_seconds to string (year starts at year 0, not 1970. Use date_unix_epoch for latter). +date(Input, FormatStr) when is_integer(Input) -> + date(calendar:gregorian_seconds_to_datetime(Input), FormatStr); date({{_,_,_} = Date,{_,_,_} = Time}, FormatStr) -> erlydtl_dateformat:format({Date, Time}, FormatStr); date({_,_,_} = Date, FormatStr) -> @@ -204,6 +209,12 @@ date({_,_,_} = Date, FormatStr) -> date(Input, _FormatStr) when is_list(Input) -> io:format("Unexpected date parameter : ~p~n", [Input]), "". +%% @doc format a date given in unix-epoch seconds. +%% Input is seconds since 1-1-1970. +%%% offset is calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}), the difference between +%%% gregorian calendar and unix epoch. +date_unix_epoch(Input) -> + date(Input + 62167219200). %% @doc If value evaluates to `false', use given default. Otherwise, use the value. default(Input, Default) -> @@ -275,8 +286,12 @@ fix_ampersands(Input) when is_list(Input) -> %% @doc When used without an argument, rounds a floating-point number to one decimal place %% -- but only if there's a decimal part to be displayed +floatformat(undefined, _) -> + "undefined"; floatformat(Number, Place) when is_binary(Number) -> floatformat(binary_to_list(Number), Place); +floatformat(Number, Place) when is_list(Number) -> + floatformat(list_to_float(Number), Place); floatformat(Number, Place) -> floatformat_io(Number, cast_to_integer(Place)). @@ -445,6 +460,7 @@ phone2numeric(Input) when is_list(Input) -> %% @doc Returns a plural suffix if the value is not 1. By default, this suffix is 's'. pluralize(Number, Suffix) when is_binary(Suffix) -> pluralize_io(Number, binary_to_list(Suffix) ); + pluralize(Number, Suffix) when is_list(Suffix) -> pluralize_io(Number, Suffix). From 4ebc0697f6893afb597587e48ea8040699ec6a57 Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Tue, 19 Mar 2013 08:47:10 -0500 Subject: [PATCH 003/361] Support negative number literals --- src/erlydtl_scanner.erl | 4 ++++ tests/src/erlydtl_unittests.erl | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/erlydtl_scanner.erl b/src/erlydtl_scanner.erl index e89e654..64d6c76 100644 --- a/src/erlydtl_scanner.erl +++ b/src/erlydtl_scanner.erl @@ -251,6 +251,8 @@ scan([H | T], Scanned, {Row, Column}, {in_code, Closer}) -> case char_type(H) of letter_underscore -> scan(T, [{identifier, {Row, Column}, [H]} | Scanned], {Row, Column + 1}, {in_identifier, Closer}); + hyphen_minus -> + scan(T, [{number_literal, {Row, Column}, [H]} | Scanned], {Row, Column + 1}, {in_number, Closer}); digit -> scan(T, [{number_literal, {Row, Column}, [H]} | Scanned], {Row, Column + 1}, {in_number, Closer}); _ -> @@ -291,6 +293,8 @@ char_type(C) when ((C >= $a) andalso (C =< $z)) orelse ((C >= $A) andalso (C =< letter_underscore; char_type(C) when ((C >= $0) andalso (C =< $9)) -> digit; +char_type($-) -> + hyphen_minus; char_type(_C) -> undefined. diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 021b537..7ba1a5f 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -177,6 +177,8 @@ tests() -> {"if size comparison", [ {"If int greater than number literal", <<"{% if var1 > 2 %}yay{% endif %}">>, [{var1, 3}], <<"yay">>}, + {"If int greater than negative number literal", + <<"{% if var1 > -2 %}yay{% endif %}">>, [{var1, -1}], <<"yay">>}, {"If int greater than number literal (false)", <<"{% if var1 > 2 %}yay{% endif %}">>, [{var1, 2}], <<"">>}, From 3937d46bda4bad75b4ea9e8f92beff28d392ecf8 Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Tue, 26 Mar 2013 19:48:12 -0500 Subject: [PATCH 004/361] Be more Django-y about undefined values Previously evaluating undefined variables threw an error. Django doesn't do this, so we won't either. Instead evaluate to the empty list, which conveniently enough works both as an emptry string and as an empty array --- src/erlydtl_runtime.erl | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/erlydtl_runtime.erl b/src/erlydtl_runtime.erl index 63ae025..2e30651 100644 --- a/src/erlydtl_runtime.erl +++ b/src/erlydtl_runtime.erl @@ -59,15 +59,10 @@ find_deep_value([Key|Rest],Item) -> end; find_deep_value([],Item) -> Item. -fetch_value(Key, Data, FileName, Pos) -> +fetch_value(Key, Data, _FileName, _Pos) -> case find_value(Key, Data) of - undefined -> - throw({undefined_variable, - [{name, Key}, - {file, FileName}, - {line, Pos}]}); - Val -> - Val + undefined -> []; + Val -> Val end. regroup(List, Attribute) -> From 8d9f830354d3f433679d37df96350f61f7c47f30 Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Tue, 26 Mar 2013 20:05:53 -0500 Subject: [PATCH 005/361] Rename variables for clarity --- src/erlydtl_compiler.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index ac129d5..c6ba653 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -677,17 +677,17 @@ body_ast(DjangoParseTree, Context, TreeWalker) -> {{erl_syntax:list(AstList), Info}, TreeWalker3}. -value_ast(ValueToken, AsString, ThrowIfUndefined, Context, TreeWalker) -> +value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) -> case ValueToken of {'expr', Operator, Value} -> - {{ValueAst,InfoValue}, TreeWalker1} = value_ast(Value, false, ThrowIfUndefined, Context, TreeWalker), + {{ValueAst,InfoValue}, TreeWalker1} = value_ast(Value, false, EmptyIfUndefined, Context, TreeWalker), Ast = erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(Operator), [ValueAst]), {{Ast, InfoValue}, TreeWalker1}; {'expr', Operator, Value1, Value2} -> - {{Value1Ast,InfoValue1}, TreeWalker1} = value_ast(Value1, false, ThrowIfUndefined, Context, TreeWalker), - {{Value2Ast,InfoValue2}, TreeWalker2} = value_ast(Value2, false, ThrowIfUndefined, Context, TreeWalker1), + {{Value1Ast,InfoValue1}, TreeWalker1} = value_ast(Value1, false, EmptyIfUndefined, Context, TreeWalker), + {{Value2Ast,InfoValue2}, TreeWalker2} = value_ast(Value2, false, EmptyIfUndefined, Context, TreeWalker1), Ast = erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(Operator), [Value1Ast, Value2Ast]), @@ -702,10 +702,10 @@ value_ast(ValueToken, AsString, ThrowIfUndefined, Context, TreeWalker) -> {'apply_filter', Variable, Filter} -> filter_ast(Variable, Filter, Context, TreeWalker); {'attribute', _} = Variable -> - {Ast, VarName} = resolve_variable_ast(Variable, Context, ThrowIfUndefined), + {Ast, VarName} = resolve_variable_ast(Variable, Context, EmptyIfUndefined), {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker}; {'variable', _} = Variable -> - {Ast, VarName} = resolve_variable_ast(Variable, Context, ThrowIfUndefined), + {Ast, VarName} = resolve_variable_ast(Variable, Context, EmptyIfUndefined), {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker} end. From 604cddada92808bf5aa292854cdf29a3436a7d7a Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Tue, 2 Apr 2013 11:42:22 -0500 Subject: [PATCH 006/361] Support number literals in custom tag calls --- src/erlydtl_compiler.erl | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index c6ba653..8006f64 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -1264,20 +1264,17 @@ key_to_string(Key) when is_list(Key) -> erl_syntax:string(Key). tag_ast(Name, Args, Context, TreeWalker) -> - {InterpretedArgs, AstInfo} = lists:mapfoldl(fun - ({{identifier, _, Key}, {string_literal, _, Value}}, AstInfoAcc) -> - {{StringAst, StringAstInfo}, _} = string_ast(unescape_string_literal(Value), Context, TreeWalker), - {erl_syntax:tuple([erl_syntax:atom(Key), StringAst]), merge_info(StringAstInfo, AstInfoAcc)}; - ({{identifier, _, Key}, {trans, StringLiteral}}, AstInfoAcc) -> - {{TransAst, TransAstInfo}, _} = translated_ast(StringLiteral, Context, TreeWalker), - {erl_syntax:tuple([erl_syntax:atom(Key), TransAst]), merge_info(TransAstInfo, AstInfoAcc)}; - ({{identifier, _, Key}, Value}, AstInfoAcc) -> - {AST, VarName} = resolve_variable_ast(Value, Context, false), - {erl_syntax:tuple([erl_syntax:atom(Key), format(AST,Context, TreeWalker)]), merge_info(#ast_info{var_names=[VarName]}, AstInfoAcc)} - end, #ast_info{}, Args), + {{InterpretedArgs, AstInfo1}, TreeWalker1} = lists:foldr(fun + ({{identifier, _, Key}, {trans, StringLiteral}}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> + {{TransAst, TransAstInfo}, TreeWalker0} = translated_ast(StringLiteral, Context, TreeWalkerAcc), + {{[erl_syntax:tuple([erl_syntax:atom(Key), TransAst])|ArgsAcc], merge_info(TransAstInfo, AstInfoAcc)}, TreeWalker0}; + ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> + {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc), + {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0} + end, {{[], #ast_info{}}, TreeWalker}, Args), {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, InterpretedArgs, Context), - {{RenderAst, merge_info(AstInfo, RenderInfo)}, TreeWalker}. + {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker1}. custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = false }) -> {erl_syntax:application(none, erl_syntax:atom(render_tag), From 898fbae78cd199795653b0d741073c7aa8d976d9 Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Thu, 4 Apr 2013 07:24:46 -0500 Subject: [PATCH 007/361] Support {% for .. in .. reversed %} syntax --- src/erlydtl_compiler.erl | 21 +++++++++++++-------- src/erlydtl_parser.yrl | 4 +++- src/erlydtl_scanner.erl | 2 ++ tests/src/erlydtl_unittests.erl | 3 +++ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 8006f64..bef0c40 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -570,12 +570,12 @@ body_ast(DjangoParseTree, Context, TreeWalker) -> filter_tag_ast(FilterList, Contents, Context, TreeWalkerAcc); ({'firstof', Vars}, TreeWalkerAcc) -> firstof_ast(Vars, Context, TreeWalkerAcc); - ({'for', {'in', IteratorList, Variable}, Contents}, TreeWalkerAcc) -> + ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TreeWalkerAcc) -> {EmptyAstInfo, TreeWalker1} = empty_ast(TreeWalkerAcc), - for_loop_ast(IteratorList, Variable, Contents, EmptyAstInfo, Context, TreeWalker1); - ({'for', {'in', IteratorList, Variable}, Contents, EmptyPartContents}, TreeWalkerAcc) -> + for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, Context, TreeWalker1); + ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TreeWalkerAcc) -> {EmptyAstInfo, TreeWalker1} = body_ast(EmptyPartContents, Context, TreeWalkerAcc), - for_loop_ast(IteratorList, Variable, Contents, EmptyAstInfo, Context, TreeWalker1); + for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, Context, TreeWalker1); ({'if', Expression, Contents, Elif}, TreeWalkerAcc) -> {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), {ElifAstInfo, TreeWalker2} = body_ast(Elif, Context, TreeWalker1), @@ -1114,7 +1114,7 @@ regroup_filter({variable,{identifier,_,Var}},Acc) -> erl_syntax:list([erl_syntax:atom(Var)|Acc]). -for_loop_ast(IteratorList, LoopValue, Contents, {EmptyContentsAst, EmptyContentsInfo}, Context, TreeWalker) -> +for_loop_ast(IteratorList, LoopValue, IsReversed, Contents, {EmptyContentsAst, EmptyContentsInfo}, Context, TreeWalker) -> Vars = lists:map(fun({identifier, _, Iterator}) -> erl_syntax:variable(lists:concat(["Var_", Iterator])) end, IteratorList), @@ -1129,11 +1129,16 @@ for_loop_ast(IteratorList, LoopValue, Contents, {EmptyContentsAst, EmptyContents {{LoopValueAst, LoopValueInfo}, TreeWalker2} = value_ast(LoopValue, false, true, Context, TreeWalker1), + LoopValueAst0 = case IsReversed of + true -> erl_syntax:application(erl_syntax:atom(lists), erl_syntax:atom(reverse), [LoopValueAst]); + false -> LoopValueAst + end, + CounterVars0 = case resolve_scoped_variable_ast('forloop', Context) of undefined -> - erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst]); + erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst0]); Value -> - erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst, Value]) + erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst0, Value]) end, {{erl_syntax:case_expr( erl_syntax:application( @@ -1145,7 +1150,7 @@ for_loop_ast(IteratorList, LoopValue, Contents, {EmptyContentsAst, EmptyContents _ -> [erl_syntax:list(Vars), erl_syntax:variable("Counters")] end, none, [erl_syntax:tuple([InnerAst, CounterAst])]) ]), - CounterVars0, LoopValueAst]), + CounterVars0, LoopValueAst0]), [erl_syntax:clause( [erl_syntax:tuple([erl_syntax:underscore(), erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(counter), erl_syntax:integer(1)])], diff --git a/src/erlydtl_parser.yrl b/src/erlydtl_parser.yrl index bdce088..bd146e0 100644 --- a/src/erlydtl_parser.yrl +++ b/src/erlydtl_parser.yrl @@ -176,6 +176,7 @@ Terminals open_var parsed_keyword regroup_keyword + reversed_keyword spaceless_keyword ssi_keyword string_literal @@ -295,7 +296,8 @@ ForBlock -> ForBraced Elements EmptyBraced Elements EndForBraced : {for, '$1', ' EmptyBraced -> open_tag empty_keyword close_tag. ForBraced -> open_tag for_keyword ForExpression close_tag : '$3'. EndForBraced -> open_tag endfor_keyword close_tag. -ForExpression -> ForGroup in_keyword Variable : {'in', '$1', '$3'}. +ForExpression -> ForGroup in_keyword Variable : {'in', '$1', '$3', false}. +ForExpression -> ForGroup in_keyword Variable reversed_keyword : {'in', '$1', '$3', true}. ForGroup -> identifier : ['$1']. ForGroup -> ForGroup ',' identifier : '$1' ++ ['$3']. diff --git a/src/erlydtl_scanner.erl b/src/erlydtl_scanner.erl index 64d6c76..7751a6a 100644 --- a/src/erlydtl_scanner.erl +++ b/src/erlydtl_scanner.erl @@ -335,6 +335,8 @@ mark_keywords([{identifier, Pos, "parsed" = String}, {close_tag, _, _} = CloseTa mark_keywords(T, lists:reverse([{parsed_keyword, Pos, String}, CloseTag], Acc)); mark_keywords([{identifier, Pos, "noop" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> mark_keywords(T, lists:reverse([{noop_keyword, Pos, String}, CloseTag], Acc)); +mark_keywords([{identifier, Pos, "reversed" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> + mark_keywords(T, lists:reverse([{reversed_keyword, Pos, String}, CloseTag], Acc)); mark_keywords([{identifier, Pos, "openblock" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> mark_keywords(T, lists:reverse([{openblock_keyword, Pos, String}, CloseTag], Acc)); mark_keywords([{identifier, Pos, "closeblock" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 7ba1a5f..8b36727 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -213,6 +213,9 @@ tests() -> {"Simple loop", <<"{% for x in list %}{{ x }}{% endfor %}">>, [{'list', ["1", "2", "3"]}], <<"123">>}, + {"Reversed loop", + <<"{% for x in list reversed %}{{ x }}{% endfor %}">>, [{'list', ["1", "2", "3"]}], + <<"321">>}, {"Expand list", <<"{% for x, y in list %}{{ x }},{{ y }}\n{% endfor %}">>, [{'list', [["X", "1"], ["X", "2"]]}], <<"X,1\nX,2\n">>}, From dfaf33aadad84c73a71fe44a4e5569e90f884c3d Mon Sep 17 00:00:00 2001 From: Michael Truog Date: Thu, 18 Apr 2013 13:51:24 -0700 Subject: [PATCH 008/361] Avoid problems with rebar eunit using the TEST macro. #69 --- src/erlydtl_filters.erl | 3 +++ src/filter_lib/erlydtl_slice.erl | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index faa8f27..72a55ce 100755 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -36,6 +36,9 @@ -author('emmiller@gmail.com'). -author('drew dot gulino at google dot com'). +-ifdef(TEST). +-undef(TEST). +-endif. -define(TEST,""). %-define(NOTEST,1). -define(NODEBUG,1). diff --git a/src/filter_lib/erlydtl_slice.erl b/src/filter_lib/erlydtl_slice.erl index ffedc87..bc40835 100644 --- a/src/filter_lib/erlydtl_slice.erl +++ b/src/filter_lib/erlydtl_slice.erl @@ -2,6 +2,9 @@ -export([slice/2,slice_input_cases/7]). +-ifdef(TEST). +-undef(TEST). +-endif. -define(TEST,""). -define(NOTEST,1). % remark out NODEBUG when running tests; unremark when debugging indivdual use cases From d9af9e1a23aa3868bbda984202f9e2616565664a Mon Sep 17 00:00:00 2001 From: "David N. Welton" Date: Fri, 17 May 2013 16:34:17 +0200 Subject: [PATCH 009/361] Added in a newer rebar, and the pmod_pt parse transform. --- Emakefile | 2 +- Makefile | 4 ++-- rebar | Bin 85084 -> 137242 bytes rebar.config | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Emakefile b/Emakefile index cceeb9b..dd12c76 100755 --- a/Emakefile +++ b/Emakefile @@ -1 +1 @@ -{"tests/src/*", [debug_info, {outdir, "ebintest"}]}. +{"tests/src/*", [debug_info, {outdir, "ebintest"}, {parse_transform, pmod_pt}]}. diff --git a/Makefile b/Makefile index 3d69a08..9f1c008 100755 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ compile: compile_test: -mkdir -p ebintest - $(ERL) -make + $(ERL) -pa deps/pmod_transform/ebin/ -make test: compile compile_test $(ERL) -noshell -pa ebin -pa ebintest \ @@ -18,7 +18,7 @@ test: compile compile_test -s erlydtl_unittests run_tests \ -s sources_parser_unittests run_tests \ -s init stop - + clean: @$(REBAR) clean rm -fv ebintest/* diff --git a/rebar b/rebar index dc66e87a2b822f0e23c2eb8ed558542d3dbbda1f..3e61ebf5ece2462acbad9fb012ad26d6b0897bb1 100755 GIT binary patch literal 137242 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

eI1&CkuQ@EOR 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 U*?$HI3V|>IYZ~dmLm(jkA1x@Hy#N3J literal 85084 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%0Dq#nWYIe%TTAU|OYD`tS`B!_18K)OT*>uYi?D zBL+X}w|iS=I1o3hXVTql63X9qziyQ68SiW47jPBtG^L|g-qG~^aS|SLRgYiFU;DVj z`?$r|R?*%OMlM!O;j8bafwanNZ z``kC)oCf8-hnA*9%xl|5btwP^yANYuzH{}j>eYA2psIw_)t)ii1-^p2^a(zmha=6g z4s$W|dmo9WZ@CWdXPmev25%S3J+iUKZJj5Ac-Z+zdwr(`LDp;hhI1SE7C(MB8_J0- z`a733e%7Er8`PpFs4pBnlPW*VR^3<5rB=!>>>v2Ouri2hb?^e;2kkdC0k0yCugP-N z>$iZ;+0(-Zojt+spY$h10{mv5L7TP%u&2+Befn^@x{f=?8!p~}i{5^hk>~l)`cLa2 zViv%K!D)1!D`Ebu-=T=blK;gXeBtZ(ig^9km1V$r`fJ;q0ixW-m&V6_%kDsIqC?=y zwh@-Kfg6*L`(P?>*H~Pat>*gA80r*H8@}SdB((FEqe*GxZb9>#)abkG|e>9c45<I=@0jrb_s*lyN5=mA&YQ>m z2Va;3$AH(}L&Nxu$Lo>w+{L$XR>Yq3z>61u#-}~^Cu{>QdpmvPe<_uW%a~!>TEz&Q z5#Aop8;@)4TrYn>6|HApPmlHZMgRBtAHvx+1evk6R_mu=74vg|tHKds=#eiY;Rb*C z<1e=7#*cAxxNnQ@k7M6d$AWH)8%%?>NX&ry%pafjzD^du_HWeFsOdO+fa=RCKNaDt z*Y$n4#nB>U;m@nwVCU?kBBSNaIm$~x-{Wj{ZHV2*&G)#hS z7(BIZ#W0}%duGqSKCsQ^pf=J{0LS!0!2h|(=wl<3F`z83vtu>TUh+utd+%oBTcthV zC>nIH;Ny5}0(Y)3bN1(`C;_s7ht;dLsWF7xsm|mrHt}ihLbq2_X-tQld13SY%ym4c zXVstA(D$TUSyf|}hxe!Z5*@LLu)o(-a|)F9B>iUFr^?b`e2)O@PF_6trZO|&S!;`N@7Zr_)xsIVEnnw_f35|0HZ0-`zvpL}dG=f2n^P>k z*1H2&AK-1foZ!d7fDFvnE({3 zxE54i{umiWHzt0%UqnBVYwrFt6twTYN4SnhIfg}Y()6hVI`RdJhKLFLIAce0d zSG^B>{`OZ}%0*O{pYJEOij1}hd>(#>7nZvLzfTh^8DIB&hZfupQxnI$hx{dTCE_}o zuDlx`VnwG4b%pc0=KbDQ|7;p6bbMtS+;nacaNQn6)qC*qIk}uClumJevSwc5=P9s@ z^)QA0P2#BcuxUPnO8*peA3+4RTf6-N+G@e}z#YJd@540;l^EaXlpg+I%UfOLwDLj} zVodLWBxBPnpLrsHN3^x|!T0~yHl%PI ztyl_XBMcYBtU@#{;Q5H%Q~WrY-RI~12{_UXW<3n_a9duE+%l%u1*{SG3%sS8;6Y;x z#Ha?MxR0Xh>tAajvY3gX#4uuAl(;ofVoAW@UzR)$HgKlhP6(X8Cw2pa*a{GiYP2tN zE_dZzV6M5VbS}{Y&IL8SOIv%k1x7~bs;ymCIA;w(=z#nGR@=R(XZTuVWNQAwuhQR4 zEXh~vs&$jF248f0ZMGsB2F>jC+@||P-4?&*TlwQfhWs#@ujZ{Si^q}i7Sh|x-`C_d zt&1V=B_iyY{eF=y)A8-=Rfz}duI7E{b*=h;%~gzIl`ge^-4wKcOV0lSBK)TzVEP~2 zp1tOO0}&hSzn9fq$7klbv* zEaE)y8iQ(u^0b1_TjJm}YDCM+Xl8}*g^a2|c$P5&8^`)}YX4;ryJh*UQ@w9?Z$5T! zybEu<_Z%JDY-eU3piF!eB!Bse{GMJY(6k{Z*avEwO+t}Rw35no71MMp-k zYiFuL;A^6|h(w!F`%slx&3bVaO{Eo;n%lT)$(rkKq1p(7xK@ZxyTkdU)`+X=&o-h~ zRCX-hoM*->1&YLnp=vi}K$XAa2mG+nlGsy0?-U`LNuoA^pQWh!=%mi{G?i86EMd(r z&$!9#;4Q+(4=wBEvX3T-uFoVO{8=bUnh=#-Fr$MFNmCOHVO-W&i`dlJ!i&j4*K9Rg zH+lpP3O3>r;j#c>nz&8E3yEhd!`{I&y|tssORE%CmtHuCN`Q2O(HL$t^AL%TKn1OG zFr&7CVsoKD+xfR;BC*4AiD4C1OBxj5vxNs@N=Y)^@Y)b_8eIt!7xElCmg?HWPN>Kk z6kib&NdSpJUW^1(D1;(#^{WypP)TU0C`g#m&rO@GQ_G=Yrpcs!EJZhP>>S?ik+W77 zK#VpmRc7I7s<=#BCt$O(z_pGNE~VJeeZxpe~SW0_xnSAjXZGxh{AI}?xjlM zbrlD0pbDn9<_^|TBq{<$R)aiD3gbibREL4-aYI-L*%yoo2|+{io&Xh1#sz?EfQSTN zj;=;xMi{J*w)4v>?7Im=j}L-*i~XL*qTJW#*UQ|8hwu$42wEHL=5HYy{GDp*9h|2Z z@uxX~FEf&~{@5umjEZ_a(yFc;AiF%-C2zy0^zg3Dc|)@ghRn3zG5m=oDOCsZ&@-q! z;^Ys;4KDnbN*+#jFu!M16h>h^PEG)fKJHx`0e=*P8IR0z`xk5~B$^CY%7H5|!VOOd z*)AlmwL>{t-mx&2PFB_lUx*Zu*XknZSZb75z^bVAFP6WG&UQs;JwviWcMuM7<8vp) zgd_4_dmVmyDv6+6On4??%_I|OK)i$vv@Pf?m^%aeb`q8A*c}?H#GSPCcF2D<(<%*- z1D8_}Z)S(*lk6cJFc4a(hIG(Gf6D}$$+$!oVfvDz6-QG?od^dh=ok_ClEM|u=R+Y{ zCpe2z4yt`WQP-fcQR^b;PiA1s;OOEQPf3-bg}^j8bbM==XNh#P*k1AR@}vh31IC1I62mg^&N z<0C8yNg7gV$K%WfI*<8vU4w_PTv`H!7D%8NE7$i&!=#g@Q4by2gJVMN5j6z@F%%+* zAwobK=C2!~>io2iGVft;e9hbxG!})>!N)+_ZA$gRB^ivYF3b~pk+~Dm5Wcy1y7t-_ zj9XSB^w>DGmO+m##ia?1@s?drRJ5kap=;2qACW=2sRf(t^fVA{)45gxfDuDmP#}zu zK>kuMzLp}p(}Gr&N)f2UX2RnW>z*RWE?kVgbAC}+V!w8`2&hHOOdO8Pgx!YJ4u4`< zCo;#JunOT5CblqY@C10BB&)uCphpnQcF0H`tA==pBSkJ0{G@phryug)(F1S|{0ZhU zOh-2Pnu(G}wEJjIvTfsAmZ``@ja~;U#P%1wGD|3=91prYV9T z#pfJroOLUT+6FX+i9&2pDwwT3H<&T9O*<6*SlA^GLt6|preoMTc|$p9A2~H*?MaqO ziuE|FQxgInR4NGQiwm`!he*LJU2v_)t*KjV(bFMe`SmfAUn;bdWr(to0uu@y?lgGB zuM zOLVrd+@EW_s~$MnWL~dtGyOE;++b$M=4v%^_ka^H?e#bC%Ye{@joV8L?i@%M4`{wJ z&C7tp$FTU@@CdPoydJ{N`EsO(|LhZK|Ek#3D0d-K?qlEUq*u*v3 z@IRlw{f;E?KI~pRHOyvh_*-bZ4f&(~bb0>bS>ON0JpQLWQL~UHUU2*N9T0M7`lUGg z8R@zfK2~`1<*?dnFS%IXduwd&2<;EwtK4#b9*gL}IEU||=i{<+L-81q+0Q@ybr+kE z)g$TH_f%(gBkJj=;o%b4wZ$sW?KVzYV0hE_T64b^kJxZu*6+V{Yjt(q^xHp_@X6o# zSGn;@3@G}Md%feE*~Lh7ljdC9j>mZ2oTNuye|6RQCnP%A4=|6@=keHNxnu59>)qTj zhQ)d7zWvKh|HrOo!uE^t{?SwBCg-(}WP$s|>c)97^*=g*VAt1XdWyzDP0zK7Y8 z>$|w7EjX48)Jinrm<^Ig7mtG{3SiRCu>!t)(|UCS2=Qz5>gRbpdeA!$Kecb4g1qTR<< zPv%ubk@4l#YqU55mkHQrN%^#J3~7PSZ7w|WNv>%p;*J07+qtTIXsB$rsFO)dd>ub&5w zNGo;}N^`vHIz7K=_gX!e*uq*uT+P(f-hgHl8^h-q6wa3_i z4I`R!ZTvHog4wHhK6nG^z^*3Wa4YPTN>q9hR2#YD7<0Ie36)|vp<&6avf7%S1`*la z{laZs3sbet0Y7@>OsBZ!ZR>^$NN4>XTh+QdjpK94Yab^798k55XMkiJk(o|39gaAQnT-=O zY9}_*>t>`SAM;7)L5|If1~Vvu*j+H-ZYs4Mzk+o^A|U-5;3y$3fe&xuFWFF)vECT5 z<~$WL-uQ@@XawQ*?GjuSSaUt?zWMlPy(?;5yu-VrvHNbHIT)qJd^7P)U)_==m5%Zlw}M7&Yxp7BDSLxqlHq8Wc(+h6_LD$3X=D{bo#_ zbxk|XBzfcyMbyuUBu^7QF(@UXT8K%Qmk~v&+7%5V9uHyS59NkdW@=o@iK0667K0;2 z6QSfs^$*fDrqqvuQ$sQIRvEzFmkMrig-*U$X>CkiRf2YvuW0ZxUFg2U)v6(eH&fRv z%v40&Yeb5HrIHowMW&IX5C%$3K8z6)i5^IjbS`;2S+d+RlvFw-#8pENIiSp5hY_-L zAa^~0)xlaaqO!!IEmcmkP!f!Va0Pb?8RE>IP8d(7Djiw{M{A?PZ1G8X2yrOQhC&x* zMZl7*<(M~BL7Wy>=jZ;X{yC#+GQmwu{d2-g2c9~Bd=)bKNQfy(>J@Z2Sc(lPjs{|0 zLQQ_oikMA4TG^89Ae9n&PblfYj80KKDJKEj zX8!N_*}?0q78?Q71=}*FbkVUBU41_y_)7)2Fgp_LQ?c5wxr&h#YxT~80FyQ%l$X%V zh+Qk9bQKebU&(+TGr>SnyX;>vsYS$rR5-z4wtYIa-gjg$@ZrqTRf6a*<%`i3DrM!6 zdR@VRU2Uq94CHAF$)9q#F_BA2+9_veW~nZ_ry6@yy0+B`5p-Lnn_V`QK0d2@WEm>& zXzm*B3fQZwr%o6`yiXDyGZnKKwFZSLyf zGi=6UrBs+{%;NHyzh1a$9#|oqIEA=p0O@KlElPAwEG$#rJ$i|7AmpS59HbHhr7lUC z4`N=-+3+aiP8~xB67ddXP9UdZXh&js8#6$Wk%s`$KSaR6F1ZsRb$}XI)61|gHWf9M7_7wrURv-ucB~tMPijY1ZAT5$y z`{RNHY72tS3Z6qxeMqzbCwh?0(sVXqo@8fQcX2FCspzI6W%0_K5MsZctUiWnZlS~h zo0h(L2UoxaxQlV&zl*`Dm#j(pW#s|!CrAvNX)bszJ4vXif2=bb-a?4RvK>1K=fKJX zqakzH)dRF3@)u1GG9!-@_a4-~`#qY=ecsFjECVIuTTKTZHn=w1lz>57bQA0Z$*@$% zLhz{&+CwXq7j3~DF0Gj&Vxx%(+)z|-cOZCyY_n@n7=@Jw!?LCT8q^5eT!Lj$-01`H zlU?Vf^ngMfs5H7EZFXUrWYYIXo`rqh`;dxvQ z>DFMkJlHVWjp<~m2l5`OQ&^|zw7=XHoD62;v65TD|1wX}QxWFfFJ6+&2T@4j185HN z>3@Q0iJQ}Vlt!R^@|hv7P0BdT3-;f=j989< zH}hK0R~AH@Wu;ryMEU8!!dmm=p{Sq-7yF*xt1Bkfmh+v|yU$t5O)*XaV2z)ef6PRF}4rPw=|HFsVCVO&w8cbZ9?LX*m9KeOci^@nmk#V8Ib{Lye} z`A$%WeVZeWlWtAeG5-fCMe8C_MKznT243HPCTuHF(rS60E z{=}?B%}XV@;#LZAn4-Y$*JlSELv- zV{SFuTn9~PH>b4I^%(LXG2vo0Ey|GbA+ja-C2BFq$@{u0t&_-}x(m8fUBBjxJ8_j$ z8KDC!?JlCSh3`f0cA{et(uUT2WH@9y3Lr7iVASX+T2 z;?hr>J3ACXd4A9MXuzzR2<45dzVpesqDpBw+Omik5lNqIIaF{HP;G2(avh5MLq0T| zk-|0$F^SiNk29y|l2dfK5|%GXGoCV@OE(~38k3J4oCwa-viSZZnQG3-c?Lk))ewHf z;wR9o9`TK1NSq-R*=Sq3v=>jw>^KSm`6%P%$M&R?@|137VI8NA^>99P`3%t(@A_|{?^FuJ5*Uy-x;BtBvVxx!1h>PC=3)2rP zU2>cG&;d)+fbJ;@?jcIgmO{@?!zeOx76kIVbTMwfMyC#ll+UD zBim>BU z$=LWb_880Z51#ve?f&)ZHg|72C&yqOL@YCx4j7aTZMNgF)dqw|myvOISbOq8lRWG! z;^32z6EGBmRC7Rsk^+Mie-IQHG?0pL5)n~47&LOEu(H+=@oy@y7@Ykq?%vdRV@!%i-GRzi})4vCj|bTF)VD0wcP}WLtNI;)7gw zZt;R*T_DFb7B7jkfjx_}w_GC!hDL3nFdQRCwLlX{J2*K>ugseD`cabLhQ^V&R9QA+ zx9*Uj1%N|zjn+Ka_Q_~6z+9N!_S4p(_QSXELbmYm(5YQ^Lf)P3-sOqGl`_`QX0tZ3lV&Hc{voxCwddv^{^f8|`kf zW$k=UO@!-YzHB*STj-QsgYNPOoxQrX>zbT(LJkvR`nQCjrxXu{Fp0i^)-W@%`JCHzldFeO+S>{IwtV*iA2cGqXqW5Lq z@t!CI=VT7S>ph*8FZ7g$PK|W?&6++oX?R$P*EC=%_u2+$-vu} zYK1ptBi5I;DJ?+P4o|)>-4FSf1PFqNkVtVH9ti3e(m=)0($m~#OG4)Hekr$?c0$1h zj{u@kUxFq`6!&Kao?nqe>}!kGkDlT!jlKtTkosvoM8$&heP9p|KIQurPaoWV2Hxi? zUXSoNK2Utujyh1&zY9w>MR#iz>V8WS4V~2`$9qQgYd(m`|4B8{I9dg8!uWqM}L9OMa$m6EG{}fy?H;A7nbK8 z+v3^XN!CQLxd0u%n-$pNc*MT$6A}9Z!FUG-EsxHxbk)^sL5b6Xx8wAo!?SY3soaN= zu1;CZgx*%s9u>}yX+$2yud{VcTMS4;R&5Wd2pxx<`NkW|YkfyU+ceF%LVxRL{wv2C zx;?FzqeMjUIe~eCHok9K(*w_d=fTrwm$hSb-MvCVYV23+Qvh%n-xi^IY9K6+PP@D1uy`Lyi$<=FeMQ8p=Y zAqYuPkSW)GS|!Xc8(cKIqMg7{V~Jnp9#PewB_-6v_D$lB&cPO z4s*mc)O;Ar9LK#V>_SU@>DA+%Xg#cpVwcshxaT--X$=$hj%;TnNlpEkr8xWq20&e(fGzGvK*xr}ZrS9w&sCH1*w$+v#Rr8&hlRH*Z3)Uo7ObMm-+8mdjq_hoL{p)3zN+>ab5ajmaH6ZBU z>_HmkCLn^^EnEaZ4qUzazztL%m7I3D`E~atJO=33*Qb7aocsKl)~Y;Wz%PH=FkLdo zb;VzCxQEedWCXG);UP@nt&SL< z6OZ#sLOb$UFo$PqT)z=i;!xs7{=nd4m%t@!QRN`e8rz&-qy~VsIAyW$bZ>6ze&*X> zY= zOF|c$^3OH2saBT%T4b^hOVMb9oL5a1u`W&dA+>>ok=zuhu3Yhyca zpa1S6>T3kb;jZBao_elZjw2unQVL0(fujzI44g=!fROSVje#PI zsW>@MsA#j!VihN(a%WFu+v_Vu#i1Z0)<#oFhxxN$cTmY`IEA^YqUTL_ifH6fg0qnv z9qUd=f%saHV3&75xFo?VlNI?OlcSsqMw38J9ht9#SNCmHq zXlKQRR#qW`!I)x?0pP$o=#eg>!aU3?jbKxmxDd5SCkT}1F>7N=Y7v&m_aHA?{HY3tQn62JP_sxPSp*O9yH`VpZY2_~0u?5OI`0e$5=wy` zunR`VhOsfiANf6mB3QidKt3;qZUxoSr29CEsjUmTMI8KKUsTIAUqv<-dM_j1u9 zex6#7oK5zL&%ijrX%t)Qa7qvqf0!gwj53CLOe*Y?gxo;aj5RQUBG(4I0dA^_giM%h zqs26BV;F3dx=#Quj0lxO8HI*Xa>cX*yOgo3L`yKj*2UQp3L%Ymu9UJ?fr9RTAOA#l zK&PuEhMcMB8i_J$un$(*O4)+13WqpNJT5Rz@N1zH1lJLSDu_NB2pGv|4t-$w4e8Vt z$k@RVV)_2bX8@zl@@Jf|qJi+3<6&pazc|R^dJANIkp0qocY@tMNFUvUcg+pRWZS~e zGQ6yrcKsG>v9RIqWVv!6Yq#5CMkL9=-I&o0YQ28xX{pN z;x~jS=4tM06yL`zKc55FKCO_z(9XEw`5!H&d3$_ebd+b* znx4nfw7!78V9JnW?(#hm>9c*)_zZLDW5-Y z8|`jK+9Py!FFW23Hj4WVd`L|Qdpgcm--;tWAOfGC*K+rI*ggR{Yr|i|q=AnMyf=pS zZ2|00p;N%To4#+mPt%V5i)?&cZ%5FIJFtO2mw@~m{D7Co;_9ino$Vg-uCJn(h+Xcl z!*a#=H^=_%S48`t#R*K8F~AZp0ng_1V0ie=r}=64bpHDJ{!4*`)%Ik=ZX#&UW#54B z)_tdI#bxjGTxD=!{Xk$n{B|9_8y@c1YWaQKgb!O-BBs2j2@0BOVZV<5&1zwx&_5iG-q*pm0siGV{l{q9? zRA6}eST?7w=%+We8Ecgd9y}r;E+LTG-Rn_^B(kdeF#i_wMt;y!v&T}N3?E-04JRLq zJ4=*|m-OZ$|N45`>g;Tnq1Jv?{<6aXleihL>iledsWI32x9Rkmwj&PlUKNfKxw;a+ zO?s~R0?h_Cf27BF0@@&R8Fu+UnwN<rbo>q#lt)(fgp=f^q-T z56J)S%88@Rl5zb*ocjF(q5c1^oVl5!>3;x|YVvldl4u1x+Lm9sioIx)_TpS>VI#KV z4yk#o(xMG$0cawLHI|-ZcVibjE8k)R&}s;INGiTH%TUq0F(b&(#Kg`*P{K)8h=8Dx z*3&9HuVw?W6@CALf8^6%Z^y9!0axSd@T8QQx|i9f6r&KNcg$OR?n91vx>*oN&~w$y zh@y0gI?T16pr&d3E`rQZXKAWhBDuqJwR5oV`JF*tK4q~A5S=-9t3!x@!8@)eX4hgB zEO4Q5X+4mlR>|Hp&9XP`?ECpAZ4D9%Rh{)Et#Ic=%I(8tY;_7?S>pZoVWS91f+x>& zli{EFO+Ss+je?iFRLwXG+ubb2m>oZ!1M%Fk?BG?9v4hVaAgCOHc3?wb7?fmON;m0x zl%0{7O5^lj00x6I6H|0jd-T+e&bl`9S%#c8i^=A)kQs4t_NdNaHdNRj4&kbzIrlpa zQ0o+D4B4`6uKChfbuqDsT4?UoX?PY}fx#-{EZnZL^D+?vj0tfv_+treQ57qWTW^*m z2dGRbPjMI~AF3-**EA`_jNSbz1s^P&QAstIqh-||V>5iD_1M2=`hy+9Y-h|#;P*|kj?Bqcy(K`>M&c~R-E-5Go74Hs|tlR@>s@q8~(`?)_G zBCc|u*I@kt?A2Qn7uKC^X0yzwY%XMGM#R!bZVZ=l_c7sFj7+OmRt(MkXp(dviyOP`K6`_K)Q_AF~ZFM{qk@ zEae~@eoJ8(v3MRF^Ybc@^t@-cu z>Yrt#YiuqN^e1cgL;}Z(((YiBft{QsENN>6P2UX*n&6lBuAI%EU9b0oLYwLmrS7*y zSK_zJQwAFGavjaG4X?iypLvzZq4Tz-c7c6Qm~D>Lj?c&)was!#FS%`=onG-rgKy;z z_3XU#^#}H+v$@?WA&%Gfrb)qH6V|Agao3&AaV6xg;ra5Nn%S3|nb%IbwQ4ox6K%E3 zag|<*EjZhHW!Sa13frnrqK<~BHP~*bEYvbXA91f4q!4jExqMt+ZC@>oI{$P|&}?)z zZSI#P+4XABb;{4Wt!6{1mFeJMAyO%#O}#+-vLz;HIh+O3$M;Tvn>!XUbhlyRJAy zMe6jR+}zpa)nwjM!~hwtOKo1iRp=nkD%J2ZubOl{?P#Q1XuP*z)z~$2(_G&COpjQk zY#Z&;&%L*J!3KrG??yQ$lWEhlHjNrDHSDV?7LZh72J19NVc=|=@XHTdnEWvI+iYfq z3Ha7%XIqRkDKY>y?B!er^&2xw>3+Rt?I%e;cD0A*^&8+#dPtRbf22lR)0^y1cSfmb zO7TFT#ty|6W??(;?K-q1vZgv!RxvM6bj)D_V%f_bIOUIJ>UJ*ITf0=ps&$-gX3~A& zzgrcLW28j5eR)pPirTz43kW0!J5|NOc=ptC-Dt{nv!>}_Bz}vFrmh}mlvis=f~Hra z6NgSVJ;*LoX;?>;c8s>V0Gh6p>PJ%S=ji^WL`$_L$bAQGkY+YBR<2RSp#dO^CcN6q z;ZheOJoJTX*nZw0!wi z=Z2Mv_!ra{woZXiU&5hSyRMs~bUE52d%0VWBih{h#8I8>$Y7*f>{_*oh|8|n4d_nu zHrmE)8}~$FjV(cZi# zS6I1}nsi1{a3zLT`9|9_`w-z&i)=LkPHUO19`Q_gr z>@65=TjZtGFGlj{l>v1JLhRh2U+(navWIT0G#}STJN64%_E}#Vfr3@zGe(>(c!VwG z&g(!*3`)GmKCDF0h`S;Pq8*(x!RFV_4UkB0~yv{KQT=B1-V8(AJ~Y$@vgba4Ae3T0=^y(4uo> za7*VZJf~LZ47p|`Z)@=1t-9KyvQwT^v^Yv=A)pVUI6VfPQ3eb3+vZmz zi>MfsS(7rvXwS{{+eO>pNM5u71_nCfY>ee5aUi!G>5D=~g*<1JZ2ohqPL3KLVgO+t+Bkj}Es7~9$ zQQ3>+@O%o!vsxY42b=}Df(+OHxSZun#zcFeD zxgh>XeobPS*-xpgiqXV_ThXUuAX}wYFtO3QTg5vR(SsD=7Pv!@iKbQlEo;)oLQ0#V z>fnzdMgpJz9*R4jf$KXHKK2`44MeZW+sP~jt+xTw~drnvzpGoj8CP{qW&F|nn0C)f-NPlXk$1kd$&K=C+4NTGGs@-}xA|BB1 zFogNAXK!eB6y1&HZZShxkX=sI+Vl* z_(LTsrHqI#fSveaUYu}|s?g3fC6M}%1D)gN4{?fy-3iYxgbg+$f74H+Gc5d_O0-S! z#s(oEWAt8&ViO5%CNvp0`V%YwpsJH1U9HDs_Q0M@P9IT2Kmi~z;*Xe(scA5$yUPJc zrMb6aY`H`k=}f|<<2W89RJkRVT&HE{s1_fP7~`vovkWD4W=n^uMN8IbO?-i(y`x?g zBR_GNKJ?4NrBNatikW-GirS?F-1!RTNTSOk<$45&Aq9PKqnFRPde+ z%e7SK$wDc%mVzqwii1kaN-->EQWQqfnsQnnwYyRBy4MABZkW^HbF5=cb&|2pB{ z#XJtF)T)Q;`K={&)RK}qHOa*Y7znr0GGN;kV#7d+&;1oRe58{^>$STLgkW>}@A=3h zc1%;+hBFH|*p@b1bYQ!wMbugzN7_dn;Pt|MPeaARposvtmqy#gP|f}w&IukH%@Tb` zd%s8q5R)a<8y!On9s%hk7cW{ckGcm_=M$%`20LS)S6E6Ex^Z=MmrZrMA+g8)tEyyh zrdkC8S2DRlDSPyh6j1XTu6ZPr4I>CWwq)e?j`k9Zg)dl)tEr$ehJ$4}kKv9Yi$W@q zBEhPBeNw6#G&eNAOfn$_?G#YvlJyd-cXrW#LN?l zrJjy$=8=(UvCPeYJ}6m+wW=Z|LQ}_ny}KbUh9$)8@QV>QiWQAmA}nnnDHcz0poW#{ z!mjW>ZQh0<-o-CyKjN=S0h-z3D_CDe^&=#3|C}NRlhmWO+m)&_02(K_ocJC(%;6dq z<`D1J@=jo{e3~%=V{iM>)tkO=r<7sDn^t3)@Zuq$_rV!}xAF)_mLGZoJ5#neDr-1) z$CqF@$UG~H>6|xyr;X?9RlM-W)Qa9p0LAwPsRl%9gL?*~kyjpR;nSfrks=%oseDY~W}r3_klN zfG})PHM^CEQvlc*`&RuwuFkPLlz>~(Nlt9r=80|Fwr$(CZQHiZ6Wg|p$;_R(v);Q_ zf9XH4dw12dt6C$+i*Ct(^UgZGKXgQ7tv6<#IlZ3cGJHTwTVP(jc81N!zrJ|1vCv_- zU*0~Y-}d94hh6^dctiK1hZ;i57;_Cg$Lf2Jk9uyJ&bG~_e&T!xr`N#gnBJ1r=NPo- z-IjVYIXdNL%owQ5${$OeRikRm2Pb00}-ZB)?IG<>Wg)&F2qEbns0>g@V*K1`3|dA#%mVAM!&Z|C4oywW}Se%>sJw#E2&y7O~z z^LKs6;|mTB7l9YGLlgGvvL4Ud$$3T7cErVz_hX^`%r)wUo9*YIYw>Apm-lOlmbbmm z^h@!_!D`2)+w*n`Z@Bk?^?e}NH`n_tG_|$r?RF)$1Meu8Ds6VV)At+=u+@5>v-YL? zdFsV9hx2V`XPOI(=cVU%KN9Ub&*$r6R=jt<605>{g`>t9m*c&b_h;m;_Uk(M{qeRw zCYF%*&24Y#VeJRq^=Iho>|y8Q@X9)PjS5Z2$Ky@Sd-tu^%l+`&vbN{tc=nHPmoxF> z`{G9O&xG%Lv&D)i*-oKyCb}2_EpE%8S zAx9^dIL*7WKjtDgdf5ga6TdHXZhJ8rwkCeoy3ucq1NZ{h@U+46iy>%vQW% z&O!g?)*RB-#7!5;6Y)+kxy4_cVz~RBovyD;9-B^F9a@jA-~tykwkHl3*c;NXG{G#H z&kv5>F`a!F;!c7TZPqxGlx^0Y_H5Wc>EHfk4j=q{e!c8)*b}mAlI7* zTHh$}Ip!#K^=cb!y+1n5q!$BU+5#(PnZ=?_A`hUIw&8zZiRWml(6ah)=j0;$O`fo` z+Z(wV**@RielH@gY+F9N{WNlab#r=npoJF|j!U_`_RaNE$YfKWZg?7+$ac9J5bkVj zI1eyJeTU$rNZU)_S#Pv%iRv3vvsufs7&?LaW+dL}PF7Dhd#H3IcNnfIZq8g2{YCG5 z7-lpzdDyBkbXNUMFfCZR`cfYM<28e~(z53S6prm`{$=+A^`DA^WlNP}>~Dnl+;4>V zujuf9&8}a?!QIHo>OVz?|D7Vfv5Z3MOc5VnoQ8pe&~v<*pkRLeeq6!B%`Qm zzd4GU2T7v9Vi>+CMoC*$9m$bieb%`fzCN4!_6$T@@yo_pj2?DLLxaDOiKw=`2spEzIgPIA z4|7C?Nppe9JannZQbZ)rMVfj;#?Qz=((JsNBo$h67phzys-Uqmr)0~C#6(jHjZzdq z6!x@+LLDE!zySY5Oz|Pk$TFPecLd-$EowCuNMp*WF0)SZ=W)$e_qYL%y=(<%vJ^?W zcZtpC4myNEQPFIZd`}T5wxb5)<@@2vFH0tdRB%cYe@M7|pf5P59AO|c;C@sd+{<2P z!%*U`>l7^(DaptsG6z{AL<8~amfxkZu z`Zu@<@Bp4*7BHZ(E))9R8Af>kOQFP<*X-bd>~RjB7YTYdAKqX?7_bNKB%)XL0K=h7 z?yv)B&x|3SmrlS^fRHYfIAS>odSDa=^^h8_Yp^?VRp{q2G32Bt0+mUfmI!ulY5_a$ zDDc#fJrKQuTQD-J-b0;SY+&@Bn4K}3gkwAwf)7QPR6#pAEK7bF@myBjpMP4=E;=B$ z4aFF%9_XXm;%J9BV7zNtY&YQ+2+|3sOh9{sPYl&&FH^7R~<{49v_Yhqc%OWE4#&qz32&B0t-q zjBmF-5Dg8~j*4tP1f6lXRRc*Edl1eZZ#ra$Bp=A1<%Zj$1yN-woMG8vkFIu(2oc5~ zvEmjlJL=U96>1oBVoz%>{Q%UDEn81GI_L{LuL&Z=9jD_6-g!2>`G}YoU;2Z8`g=O> z!V`0(*saDxRfo{hNwj?*IQf3MhQ$U}i z$?s?%;HLzU0Od!xBIZZ~U=w<~{B4H?8L5WNca0%C>V4M)Pu5xeSY#@e$;YJtgpkBf zJ|U=J?Y)QEMx(_k2Lqri>f44?W>Qfmr@<4u%T(Z|(Q}xhEP8M)nmfsBmzSfmWD6MZT&vuGwiy%3a^g|dn@v}0$bV9{SZ5xIq_w}LiK5PEPWjLPQ_XXPSwqP zd$YZ%{0V&+MtK-$g2;kw8e-%$ z!4nHp&B^>RpltNtaBObuzS*0{3z+43eB_or%N^2)T^04={kX$wwfg$FlT~~VUi`OQ z7)znryS2&KVR~E}i``r6GWztP>yc0TJr8!`)jqq;zIeZwlejnY;`7xVJGp?Dwf*A; z@N>>8F`BB$)5X?)ei|bB!0Tmmyav_)X94!$x?}^rmC>wu7&(FB;wgDCHljw~Tb)lL z0}>Ou{m?o+cDa=6^E!by+ueS};mzTCKgv1HbvD`MYFhE3uoVe7OLpSfd6MY3b1=;6 zX%B1*Wcgs{P%CtPoUJLm)fEZk{17A3h3?>+>cItAhY4p2iaknra@9}BU*bg>{Jasu zN#}OH#VoDMqNDN>i3EItf8Adu=fUdMc$?i7hU-sZy_`i+$ z1tsg)qyXP?UZ`Adg&hNU*OR&H%hT1ds|93UW(u2g+Q*W6i<-HeLYFH?q>Wm279$qv zw~u9OwbmTHsKV5yaz5Ca*AHF7y9O@QfV|9n9^V{+rs- zv_e)!=V>%;;neOpx|}h9qT!%Jp|tE&?9hWkl+{daR>;dw0xq;Nbv4;AH+5xk1Y9?> z66otecZ&lfA`S+{jUvv2m-p%u(V0WXcZ0wr@e2<(=n5*k8~(8>-D}9SeV_gD{dvl2 zEX8WIu@n@u>s-*z?9{3ZgMVQWAg`ZWcC5%=%I>mG(%ypoNTAREAZ^x~@u<<~07{gw zkdJ-Os*<0`oVs3|l7CPs*MnAtyRcbqy01_#-uN{_V2Kgk2FO*9thJq={FqS}tv4Df z9S>@9obi!Vq!Ve;OK`UhvGNQJp0vakFh~6II1MyCfA4xMG21HwjM0=?%`jswf*?!I zJ3J`#*x7U!38~qsm`W>!LsuL{!e=!zaDjTOOje+ACBO%VU=k!XgOSb8eB&`$-+7 zwTucv<^#s3%E(PEKqH!m=;^$N-y0;s(X-BpF-()Q6_ID->YV78V-eM3A*tUDJr$^m znyzSJ9rlseJ{Xm2id=0moQb<#VFWw;j#b_(glc?Lh!9g}oc>{akuUIG>c#xj0AHB` z+!T@+K?Psa_k3Uo*)EZ?gp@yQIHPhPC}+&%MqwuJ`Kb(|NVqv$d5&Lj^&zrD2D$38VaH zK||V7IG_xu?No8HDQ%g|LK&{8feJ?Sdp=;2i9--Y1_|knT&cUHvV`;nanT8BdyLdF zQDIr|Afd@YNCwPdL1Q=B2ne-*Hgk=IkWC3dEmh(3RS95mr^Ap?|DkUS0N;8?neW=- zNPxhOX*SFbs-ZVmrOSn7dm)Fz~?Ccjc1BIk0-8>4yN~|I@{P-rO}Ee4(8Z4Ls>k&M z@3qx;qy5@l%W39KP47$XS<*k-8~gJ;P1lQ5Gf|FF-^*C7uP5{!j<*#*_PXc$ch5Ur zmmXAHZ0^P%cBbc*wOCit0NI^=*=_gZ-O7)fbG$4MCEFe~ubU0o zv3u8<>}>m~gIOPSUnQQmm&~83>-Y2MPTTDCZu`z>yV2gCYPX+48pe}M+MVi+N6j5+ zvx491a-H|jX6aW(GvKQG595ov`CRd-+>6gBV_lPrExKN`4y_fF#tl=3+IYW^TboiP zcM622v(uRT_gc%~?5%04Q#ddS5m8SMNszi>O0S$dLDbD0m2v$DN*G~ESMBAy2;n7M z(;&)8$30R^!Q_o_b2dZh;8o*u^Nx^;?t~JwsDzqmy*hJDNLRS$XYSms?M`;?5Y!r# z_1#J*qrG+w@#(}M9PU4B+ca3MF$4?@)Xpx*>l^nPHFNbEl2Iy<8`T@IN-QA(<;%_0 zmT57r(FHsT$$^RXf-sc<6Xk%+YUT6wT%5JB`OV|}VAzQn^z`&KBsiS8)Sm1MV`NmS z=E8Slya?5HH?f3^$&Okrfd8`~09u|GsQwr11SbFh`1RWV-!HGTjro5CqC}}fI3XM7 z)Jiy)M#}J%6=mSN@`C}f=LxV90|E)J&jks_*^RB8IjJU)W?z?dA`MW4YB_`v9{AIR zad6s>I%BA2V~iqfy3dpybnuv9@3irR^gHL)aE;nKtr%{1^!hHL&6B@}y!2fAe%*Xj z!aAbSR-*>d3kV)$2|G57|K%U^6@U*a1Tk-9&Vo(f3Rhb;_l7-{5t?pM8;FwFQ>vIN z5lskhQS0p7nhIGz7+<#I;!TtuMZ9`jGb&9VgKlW=!#kfD*~duF6Kb1u8Zk__BJG{L zv|wJCX|dxn^V}KIcX>44Ab~Vo0TQ=gLGHtUqCAJAcnsHJk-(u;Tz8fVBAUxipsv0a;3K92wO_7Kf+dLOr$Bw3T_yeLwCj$!?gYyS7JGc zF&5x9>0nN$-}+LcA|h5cJGitMNB$EZ`BA6B)`Cm*ldo1lL-C7|He+=f#?Od_Vw^z- zdk)mM0T9FT9wT|ORjv;qdysFC?Y7v{OGJZkEd2IS#3jlwX3LaB)+l0}ONcz!cN~;hAq-rV zL@s1yau~$N2((!Y<8By;JZKL7v+L+?BbtgQkZ>uh=9Jf?s&9yo?PxaTEP5Y*vf00+ z83z~pu zmK?ZK^CV0XGocwaL0-KUQmNWUu#qTa+?8FtbS73b889aREgCqZ4iG1u^FH;<-_e6L zo(dDTl^iL&`&3Kd`X#vs*Q(NQ50ATO^$pl%Ig^{>IASrgUD?O6G@Xf%5-ThBf}}bjCLb`f9}UV@Lgy+;QC=Sb*Oh_`ko_=0*PcQQf)TLu z5;&&aZWMw0Whu#WA{YzK->WPHD}PxuYVHu-09zvH*wi{<+|+LlfV6h#M|1MN74iE` zz*!u(ww<`HqH$UbomfFboyiAO0i1>)fOrnql9AtDi&y2@IhvSABtt5f=N#rC(lCC&aquMa!oPkNf`%s*p%T8UeNs= zsP&Mb=A`3>oUdFj%wv<8eq?i_7hyZ06_I{hoV%0PU; ze&A*>fcfnNc|w${Qo0n?K_;OH`O2)57Ldszl}ZA-)O-x4@P91I#)>7NNcb%t#S{$7 zg~o5o#vFai0!6k;y+d>KD;jb)jez?@h@8@NL$nLh{x8>d6$Ae=!0Y=9f}o3F8j zRgWz!=DMlFOj3=6althHApae8&6DF3P${FBI6AhL5yqtz2a#!)LbQHh4YWt8*ib7o zXKJrF>Y6!@zjGJV*sNUBdcj0>sx{`9&#_!Wz(zV(Lz-V-%w2|DF6toDw_j>>FTe&a zB%ci&6TSgXgjn8MkU1GeOgh-nW2 z;zRX$)i5oWpA>fq1zZkeXw!`P=nHkM&A>&qLeJZ+4;=m3l&YFb-o zTkaBlz4r~TLKu|H3JKWLdND+$k+t!UTCpf!*W0*yJCAEbC-qk$Plh*B)*weOrzejc z2InN+zu)gk8pSWB8c*KXl>n-#8i*s1!$X%)_G#CLVBH4^I?vZHtpT-!to~(itCNMi z$_U#=<%O3~A=h0>Nk2%Gl`WXMSSU5rD3Jy%(i^s&QXq^WaY2P9o5;D%j0MKl6iCH3 z@EMg0s(E25Mh=pXv7xI22{iUEq76K#DiDs~^>>Y-mH@@1Yfz5p;@~0Oes}ZIodHCdG z-qq6rnDlGGl3F-S3Is}u3^NPGmcODF@78DxuRe8bZaVWg@!KI1>N%DH$6>`wJ6Oap zwXWs-<(fg2!I{b=9$ZW#FX7xsE;`prDLp2&rX`W0`U5LqE(1~9NLQN!xuXt9*->xRjlc9Xpd zZzJQ*}nX_{T`RZV*NSi>p$0eHOe{|;=vlqr`}JQi!aC+X-bIr+BZETgX9rr_ci zErc|F96XTW<>aG;&dHH*Qsh!rdzEW?eG`=ut{^nW>Wv4aK9g{!zu2He*O5cnV(%RH=jfkB->9E6| zMRk$b9Aqg@u;8=E`OR~jmC`rseCO*TRqKpBG3g&n>K zJ7R2<(Ls06gCsjQ@EPe(TM}Tn>3+C_Te~a-EwVF=@jZFALiVUmMf5>ygr8Mxb=nhx z>|$)$h66`8Qae1-{Upb4akfNuIPME}9wkSvqoGk_h@1&;d^Zo7OGd~2jOiyiT<93G z9CqxR!%xInQ=*fzhv?mQHvSo2JX63I|6R>?>NSmwLSK2RwY;q-L(X6) z1a7%=J7lhqU)*+SuUMYtC&CrXPy3KEwtIDbj*0snE4##Ovy@qEq2aB88~K<*Y(EBB zbTNS|Rw$#2(W#DW05XiB=GQiRb~rLfC#5-^=@+#F=DQqZ4r#Z>Y>aKQOmGWrxEaM1 z*d^>$ho8dOq+fw{lx@@p9l$!R_5wefCr{b!TM+9Q^4FuNF|v;A|DF!W%6VBFy&^Wy zv=VWNvNPFuMtVV3c%!Iq7}3k7y%Ud1|QNwIA9_bIQ-SUn3J`yUeZL%x*3vdEeZhPQO?` z60B2oJZ;L>Z@cE}JmTJsCgVFFC9A5O-*;p2KHYja?e_n@KE99gcw5ZwRhY5Zj(v#A z9CA~LlrDegW^gdS3z*%!x!G17k?oC>mE61!*5qnayehurw(xph=HCm0#Cm_XRxnfj zRjG82e5uMo%~ikukj&J5pL{cX`IKGCw9z1Tk$!}9jnW8`^qU=;_WNau4IVvwsKP?)kMM-_QGJFFqglvOk+|pSe3< zwNw`-wV!o*dKZ?iKF<7)h6$*oM8CHv5YOA}6CZEXGYGJ`BkV(aiELo(UcGuhy#P;7 z-E1H1-!@s@*d^Lcp@YP&nS(+*!}{;r<4Iu1Zqw97!?(+QSpy96)Hy>oY;mJh*Mrv));&yO$N!1_0huKh9`L3EB=&Ti8 zv{rZLW@>q5_mEV5kgrRQ>tL<2?7d>C4|HEh*JIX)kZ1M4eWpS!BLPDl~_`q7Kxh zgOfE6S_%HtYi)W`a(J+wQrM~EGnFE#5Qzy~e)))p)z^mmCZ45Qa0K4~|4Foz2}}dw z{vN4y{klOY|1IyCm|Gd^Iy;$L{a5Ck_V2mcrVu)g=cg(eDUv6v_@AN@DqwP^wIT{) zd}7lVF7376#>3!r6JFBe*g3;nfn+g@pt3 z{Rec=0)QWY&j*U5Cf#|E5Wv_TDY{1o8t7UV=AbT_RsB!dd9@LSjTfGO>*xWd1RHdK zV`^~%QXzyu>(AAPP9f*yaT_w~0rJZJAP^v&DWc`oLn-IcuSLeut3CZSP|2l|Pesfn zmlRqvSi*BQ=|npV8Nq<+AU#(%%HtX}xu+|P&c%Nw7^k^j7gMVtny%-twam z3Z;#mq9XyA2$j%8w5q)F@s+QgoLI{1Vb-$Q!{X5S+E7 zo+{02DNM`|QI!VCM)vLi#(;>QP~fecFdq^-Z%PR1=Ew++BO(=A%#c^O z7D|^l=1U9}L7^eaB^7ZiD&rtBA2Q?lO-QCkRpC@VA|N+ujSfaknzQZ`qey^s%{?h9 zP4cl2HY@@>H;PL`C+Hv$45l*6jh9gjXu~A6EXeYK1Comxbnb|coJRHb6`Ygg69EwI zL%anB0u+2n=aVp~P(+m0bs#0LMJVbpAPENp2TgQiV`puep#rWY<%*u|f^U4gz_C?L zcqh_!j3Pd<3&YR8rRH^l76Mmn-mna$357Sgo{Kc+$hFlX=qn#q@bkg;(8Rx&u%y3I;Yj=t#wC+da%v z_oFf^8f;*3mg|%ckHSM)^t1YH@RQoTX*#ugzJf-SPd+H(ZEbBS9g+*Jn+_<;qT37F z=OCvp`^}jaeGu2R-E}6P%DfDEYjNnx;|1*V0r%-?mnP5C>v%hOP`&l>^uAee6GvZN zcv7YVtx=Elia-L4MV<|+Cvs)70Lfa;2fj{kP6pGr*Kna2Vvoh=^cAtvQV%23?2W}) z6MKb8Y}f*GxH+wFPnJ|GThbkkI>LC>iV5#DLs>#u( zz#ujIOx&|+o8AQ_6v2KO;B#@6(-4h(bl3)IC5T~dua|l)fqKM%rsseGa$O&UwXgF4 z3qc&abqkA?SDyntec(yvqwL?ABtsWvaepv;q@rb0g)LKLXdvoFl%% zlc#mDx%RSlcP{t=LJ#*Mu=3Z zFta~y=r8E>YGmB;>K*mDlo4II_5i@XYcac+QBkA<0`TF#{cuW?I1d-VBqgp^Q-G9n zT}zaZDnZ$z`={m?VyaAOZlN*W#yBU1{M*~EvZgfu%KE2R(;~RLuYAk$%v?M%-;k|R zOg*-u9D*%?f!rcQiOE!BAyRw+;z>YZnr*781}60v1UF}MHq+>cz$KApHQ$6SKcb4r zu*!#w$XKSd=Z&rI=I`_&$ClExkOK#ri;4KdCwJPdUgv?suNJ$DuQuNrO!f-HMU?@?b)PjfR#Z%qCAam-j@b?4GxjQXoaJeqQq_*t#ivWw=rWMQ<>ykHfx+}2;&iO^ z9QKZGOYz&THO9L${TqX$_F{#fw>})xPg(Lumatya2e@2auBY3*(fIetNu8h8E2RBL zFOa*5am7R^B;DwE)8xDMOzD^6K;8z%#KCJ}^xAaZoDajwpAp4c*DAkV?x!grf}Gc@ z(%}v5o9FB5r5rBzyMf|0uyAqXjk(#{zq2{))>67*Q}e0;j~|jN7CF5w2Zib4*Vj*+ zwnFcfBPunyI9*JwFE{x&Az;1Z+#Ghk6}8t+hkX*0nzEMw=TKm+`Dn40^BJOtzFZC) zjvY%(&X*s1KrRfD6MmQsi}wP?{mP_W&W|tYuV#}h20|9kKYp%5O1&Oh{*lrIh-5yYr!RKh4hL77m8 zyc%cFTYu*`pe6O-bQO8XW1wCv)HIXGqo-!73E%Ww_e9L@KkpE^!jpUssBtyO7GKeQ`+;??@C%km8uo#>^;|q=U;4^ah25 z9-sK7oetyjCG!)KDajSeyfTu*fAFH5Ppu{hz%&#?!h3st|Hrxluo|Vb`g>Qe_B&l* z|NnHc{jaWaX{Bk~10m$xyTd_e#>M)DlHA44D?p0MWg6|%>k6~^c^K{HEsIv|ev0}7 z@$->-q78~IZj@&{J~8aLxL1YRFsuNmNszYNd)Ex z8f9UfyhB;Fayf;Z;6t@@Y}9my7`Yy)dnuA760nRg`Bz&ENY>#xauOfc!4+yX^J~NA zv*CqeKsWpsp4^m&LWKOGJeyG2unD|CZ>kEu*Fm*F$JCOWCc&}TgVaFItm2JelsY6R zDI3K#wggv=^mp_Vo=YNJ9mI@nFQTUEV#St~B(t(C#73J{!LP;E4TEBXQH}Uk>1w(K zYjWWYlkvTi%NUE1zLKWl?RTr^A>@_d(K6v|5I)O`DBO{FC(=oY8l`9-g;%8{PUVud zz_^qPD8;7F^HLbfOmY&j4!g#XugHXdV1C9-#C=$b=jm1E9)hd%8Er1 zd_z%AP%_b+jmnf>?nxY66$<#$q zToPB(>6fY*i>{jT8zAbtXP6slZuK`1fX{USn+Gsj0B=PPYTMUiUB%SZ0yX#{_i zC9rI%ne_m-IX>!it5iZX_1Z3`#GpCY9(o<@P0&K^eA`t<_453Xy?5TfMoPW!Tw3aS z^Sq2dhx%?S&aK@zL1jRmcZ%Z)coLQ~gQ82bRb!75te)U{5O&bAhhdBO*dA1x(b9JR zq~Bdf;A%Fh3!= zG{XrrR^lohE5^u9gB-R5jy|*jfe$esyZzp#JdhU;*)=dkRz0k<9z#EY4zMN*{A)1= z_V)i1UfZ%#Il}z~*NFcE!nZNDb+WbA|IaPMS;f){TNNc|jdL5L)-|z{(~{FZ%Oh)x z32yZH6(x(5U?uixM1G$ulSioo_hxGSU@KdFnuu0u+&XDOKspScfG}E;Jer(FOdhJp zf)AKv@99r)UI`yT6J_y!F=!Hg9CM=)wl+4J4XzZ7 z71r$~;V8(A9X9LyE;>?1Vm&k#A5CI&BuS$l8=)id){F`E#RYaA?fXqT5^_sTc1J{MOdWW+g%%6R7;Vb5nv-Rq zw6#(cFTus=tYfq;juDZk@F=OV^$z()--#)iV44jek~qr%*ag^QtI{%e=ItcI8#Jy_ zRtpD>er6$7+Z2fq-1#eaZv|K$>S{AFC1&RVCBczKM$9S1sw9k{3dtU_xm~~PShbM+ z&Ie>dVW?xFqC9>=;xzV!Im?M+{ArH1IeLc>Lp625*`z3ORXZ5;x~MPbj2hFZ0C^_r zS{YWzhaT|61*^~ap_(|$51E$DM?b@`76E`2HX)Sp5gOZLhh9|RvN?N; z$Jm`}$BA0L1CC8qWNpU;g(Izb7tU5V(p09jMc%a@J*8iTKpdXyP-};U+S%Gy+E)Wj z)*3&oBEjg*+qq0R&Glq2|NUXuq{3awHL{v zW3boVwM`^?Na-6Df`*@7`Iw{&>6W3BAUmQZTXyVbbid#&puo1Gv5O`S} zeLn>r{I#fJ$xFl|I=J@8Q-s6sF_D*I%xpUMnwn%6yQN+%Me0XbtJ{tzm zLZUje%7AlaVA`myoW{@|K9NY0*JG&N?LVl%Nc}*1h-x$+HGiN2|1E+%dJlSGP{WP{ zGW#(%XiYSR?MY(1aBUCVqOUT9(1LZQ?osurFl6;?tgm*)yjxb>K2!Dv9u3^EufUMf z<=}=axPPRXMATMKe4$6?`}m^t)Ij#uO8kUS&cHyIGW%rh0bBXK1?mYZw^N*#!p`NS zS{8XMH?E={J4Vsc*%I{#A9$`kfKO@;8?b*rCfoL{&|&HJzrOZWzGSakcsO~p_RAgk zGdgmz+VoZ#{6to27)aW0Vd-K(=Wc$k&}f?CdXak^zSKu`eSD1icv+2>GyD2}#LvYK zFTZ7?(e`|N*U>&P0D5oTglu&akpsJ~CRq1m(0hz8uT^{C+Tfzu$7bou?CPyKb+r zD)HVG4qZlbkvKC0al~IrxbNRLhv9Ob(YPMew(6$^TSxKIv)?t!kLn9Ub-RvCSG-%9fuD3IJ!&tCJ$&;9igBDiH!-S2+MY`Cgz zrnlGT_W@m&)81>%zq4N#uBst;z2DtfXalO4Jo26gtg_yfC9kchO}kPypbLf z^{~=+AeYDYGu32%2RQNIMSn)R?5uFoo73N^<;0vpu_YZydM)%@uF&h!!AIl=MZj#q z53R~iJ^gDqJgyGU$4o}Ff!yzfxhbTzv$L~&c`7i&$jLjPXf&!7nN%SD>`3$&Lr#np zT2$r}e3-mcp0=}N_Wm?FnuuGI93*Kf&0A-|6AN#sn$i;EQ$_F*r)(&gZQc%NwnkGY zP0CrCOO?)D-K*I?)7EuPJmM>y^=u3rthJnz+r^@W8s3$KOFR;{y*O1H-1o`Ox%4)HKM;3BH(`t7oqD}cKBcb08_u!Z;1c>YhmkTr>k#g z_kU8mf`7LOWlp=5Mju}E)nT~Hrl|Y8OOzJ!gc&08#QPNl7K9lpWjA8wgx`Jvav;^h!a{^36#_|* zn``vy0Tmw3Ub9&b*|syT*H7=)o2-mcaX$k_4hFM9hNuYlSHj@Y5GgtG)lFwf3l{BD z#?uCCTk0wq$y zW2zy%bcfdneHztp`=}~TbDm|Qc3oj&)zP4?xZtk5FmJq+j&wbi&(2f0ZX@4qgR(ep z<=3-%1vAoBL)oA=V^GIoI>Qhf&v|62Kt&=wi$S7&ASK!89GBHNWsre*=R$IzX0g2I z0J3ENsK7UiUfev<5mkd~y&OW%sw8M$n8e6B7rw;RDry;N;LsEo%|$pGQs~g+&oF00 zu!i$xVOrk?g#&tO_vl+vDFuGzM?f&-GSE&~_TC800=(P@uX|t;9uL%jY=nX>FI2S) zFB<0sYCCGGk(6$oPE15aaOW==RS;xHH-tz~rWe1~8$$Au-T$IkRWylmJVq?CPfWV^ zTbBqVe`vHCnn*xRNfG2Oo{)ZzsfQ>@1fT)vKoXUV;N}~a9E5uGQjh>ag@7Gp*U+^r zcFf5*OHYQPqD7c+k8n&3k|C$2MFw$&*jQDyE23^Bb2Bofu3OkEZ?Fvu@O#dtz>fE4 za5|;p`zKlf>{tGiE@6ITdIOzwOsrjd?axAm{BQH!aI3zGX5C2XGIbNyMW8YUQ#d3 za0#7VfKEXXF}zm;r5XbNoDVv%GARulI6)VXeoWT!n0ibX3=hgcsEdCBDP<|fVIS!O z4WM`c#zKD4p~YJ#cKbSQlK}joq}-AOauT#ZB5G)I65(HQG4;MIFG=yMQLa4*gIAW_ z3$UJNz9BZo35hW1gUj?AvIFd?a3cCweb3 z?0WvOaH0GmrwTru8iG(_D_JxgAZ+p9zUyUI!yEM5Exv!gaKqyE;ytP}B5_E8%8>X) zMJW{Q+=tw8HUYP!Vx#(7RZRQcLAEBZbkThuL`vYmBt5}DmAoc#A80R=2dMT92ItGR|Yd!QR5JRrqhfilcA3D4=fR^pa zg`3now9?svP&j{PcONLV%Fzwj<-I+Ml7)xPz7B4#CsvOh;FfYy+~-?_IvSlWu&NbL z-sl(iJHWbrOy2XQ7Pp;X%Z|ego>1=G`zWvB7x!1|MOhw^lMuw58aEi0Ywr^qP|L@2 zpzZVZroFlJLFjZ@`x_VCIKlDZrG;c)-96MC@olypyR@I}dEcPi&j*7q&v>zo z!r9+EYb-1+qP|+72CFLqhhCG{;_j&@BYqRXYIH7Jik81=)Ls@e~zu)PuV?y zx}Lu7)>F;uwZ6|?q`ts%XuE`TjqX9JzPLS3nHJ#}Kwo??9r3CA@mayo(b?rz|KuC* zW9G1m(8up^bp4o6_r=rmxNKu+1M?(ze0OcNd0Wfp!g^Fc&nwCEbtx1P`44-OODUq~ ziQRP8ukE4Vb2rasy{$J)_A5#0_nzi9&NF%(-Y2+bTCANnnbYlb&`K1K^yZPmZ)g{QXC$f7B5gh_IfwTSh17iSy z4e5I?4)m;|hv)eSIWsF&{E+m{fh2dz67bUBy}9;#yRo2&w^9Z!7QwcNX@x23O~)6a z)KltlNJ52YnZ8YCvsy!wFCL{qgdJ^^^>8@yPz#RUxxtAX^tma+Ti*%PT~{`I&yGzX zu>qg^;eT6TRubNz!y^L$z5L`@Vg3K_se`?f%YV=4wp_3-xPMbw{xq;9eP$<0G#vU+ zrX_LyHi;!A2TztzE@~0DDlp}Y?Q&Z7;3HePE#V< zCRiJ~KBIS#=GsR!*cUt_V26bqSNa0qxb9(5Xc+&MO}Xv9`}w`o)L3AjA8Tmlzl?ge zvts*?5-ZF<+Yn2H+Nua(RGq+p(;(_sm@r`_#sh98SDnj%j`~N>8kyp*LxqB_n2j`T z<}{2>;F__bd8=5j2E7TOvvGV6$kd^}spo8bWreJQnQ--{)?Yq|Y^W4fi#`#JCISCh zXptuWwNBDfV`;`pazF(L=eQLk&5=*BuO%{PVWT6MH%^FlXVB)INFZ84i=;=D=u|5% zmwz@i@Mfq_lDDW(l#!cfgbg~%Dxk4|2ZK^(WAmvkFP7MJkK>Lcsij7lyUwIds0SCI zIu?yK&NIssMwc99+ECZxl2|eOQlZPTC7}yNHJ~l0T?x8_HS=NS&Est#i%gwm`p8oq z-$N8uvUo-um-D5@;yRcySS4%yUX_$zO7h~OBZKuaMtwD*NL@&}#3^(AGxKlM)_PUA zIZlPVWkG49)viWy)?_DgIs7hqY=w3P(eKd=ym+p3}xctcm#&>DU7~ zC?6f3FKrs_Rg}LX^XqUn?pj#T>Y=E!KyJ9+tFmAb#j%LHt_Vz9uP)35_#f74oR%uc zNK#aKquJFuONVG{$}&_6QGM_ZT82S*{+|tmV-~JiTD*vWT&~iFDKjEfoJ*FwQhPJ0 zqS#?-_ThLlr=Z;UGjO_s`_Y9Uit}(5h+&ijhwN#y2~i)N=v2!~AOn7L4OLVIwCYJ@ zq`_buNquM-5d7>w7@8;euf`0)`8+v~@KnJpeL&Zz8W@N)c%w9f2@8Z!*8)gL)C#1$aZ&+N>7dA$zqC^u)!~ z&t}&2JT=l1tNZ&-v(px{G?xU#$hIXil?!pEWEuUL!k3(RCh}7q#sI@XMIJa290R|| zn8#fyjWI({jRmz`+9oI6zG`V=wn>j=^rXqh4RrAqscs(`3-Yo!r^io}s`Bh~^KlGR zg_WvoZ93`7nW85rQ>)=S!6vCAveXm#s7hT7y0sBf3lwmV#A;ES2xaozbcV`qi5Bp` zNRqHscC$yv?@}TXAr~kpdOzqGP!TUik>Fa2QX^Is@=?WyFVj`oG zyacew%CM0v($W9Mo-dJHf~_^2i^pBH{q?79B5jGfJW!qR9kIPZZ(Bf2#A}SU(rU#M zR2~^q#5r;7P-O^@s>o}!NPiE+*rQIAB+;(_%R`=-W7QX3m3m}{U_=gZa}+%)NiOst zr&7J6Bk`6f2OH#yU=j=>p}CYOjPYQU zQY(TnPrs=TA|E$5RYUS68sma(s*|kV+n8<;=F3>5Z46R@Wd9T9F^@#(s4R_4wQa`z z0%e~(X=JNHTR#hiyyK^4VL|EKu~FfNdE+D!kYu%11tC&drk)z6GFu;wXdD9v1z-H} zi>*?c)h0}p%_LCxxFGMof6lXbfh&F5C{)~KqM!RTUB@on@Gl67r@hA2W4VZ;n-JTC zs!*g{!ys)m0UUnA$JjaQM4?;4_KXw(VJ0P4cm2JKcaY&4IKJUpXxC6Mv_K}u8e7C0 zzTx7(H4(EWVdaQv8H09DsI~zq1EJ<0q2N!L@LxTL{Yx$8aCEcaVo{fK>Or1fv2hq@ z*{%VdDLaR~sBLEv6AE>jrz#w3lW(45K-~jv(W$4gO zxdQ%!eT}M4B6RbTWac97O-6LZWQ;Be6`?loPI|&LDrhRld%Ida2xE(dwAN(P$me-FLS?5pv&D<3cZekWxbNDC$`L$4qYSs@> zLbM4;8f(BWspq~*EwXA&gVe(TGgZFDx<(DLMM2S<^suqj^M0m2PCLz&UP7?bAckpN zAt4lmXM5ep|4!Rmqma$IbXD%HLeaT}uDM^XE78$aVG30h#|iki?RUVFfSmdh*aTY% z##c}@RaFoX79xhGAW7psg_hYM>jKLa?bYfb?Xet1V>siS4T)ff#=h!9t|h?dfk7qF zkAqk)1g#K#>g*|dSdlMyt$9s!iz<;+DGb#V%al>h0pog#)ezfzE4-ni4yU3H?MZf6 zynNO3bWbd}CJL-*kK(+%7NQ)hRn_xFYS)AFwM49N!jOr)c=$0MWDj^jJ-O&!A}08y z#$7MSG@^{exc!TIVd#afB*x|;uWJUgRBme^E-Aq)kuY}hQMV|R5kYXon^3e_fu_oI z!L+ai9EX)t-3+#jkD+Fqn{cd~``$A6I_|Ulr(J#-yg_4~dqqgKnT!Eob)4Ybg~Fg# zmjBu#Fj;}g26|vA#x=@k2WYnD=Op=HE&oif8^uAMBv{_Gs20|rLLlsqXb{KLi=rK8 zIa5{}9ZecOgs&!KIk7qOk=G_YTA~ zFXSfZe!cfkORO0FymgeCpKLhX*91wFHpqfLXdRv@*m^~Uj&`&v2Emd(3h%fY_a`;r z9jSZ`k^QP#744D%(SEzdyM_`*r_KAe)0H4WmUoi9p$jRR0}IUam(E~D9Jv|EbnJQ$ z-9wzFj9b$neN_1K^aC`_CC2+ys~B09=@P-cGlK>@$nP%kns8?Hw1z?cVL{F(V~-0* z))$R^AR-B1t}S^Z2boBRzC&lL`~z}uA@J>^E(#11NC&uBAY%}$!z+{KX{b`V5e&UB z{64&HfO=GqrMoqNr{q(a6NHLq^z(3rWK(@B6eE@GsJPYN&?0)P?AdY#Ss{6B0NwI zgw|w0BkpdeHDI)w)rVJiums+urcFC`BYPnxwlEB!m2Gc^wIQXKFk=(#)LaIu;@-$V<(39GipJ|+!HY5K2#a@?gNxLD%ck@VRceIl(HaR z9e)KGO@*llEl9H^oCbDjg8DwpoJ2(OLbC}+2Ry-IRtGMM@c%(!`Gdun*Gn~EtG$gg z*)6B|+K`ShP3Ql{8x)9QmqXjzbc5(ut@qGStq%x7HsCT?H~8CMS>gdw6?Xuzr@^F6 zJGl6@18+?+QZ&7uf(dY@^5d@tPtgl&Nf?YapetpCVkme?gYjB!X%@a=hAH|uLKA=g zOd;481g}#PY6?0~U&Kn;ce7&ISkWBV6az>$l?u;lNf!;Cm$I5>lsOHi@Pk-`es-aG za;UvDMhhQhocg+`-48DGNebpw%hw6s5#BeD5MA(o6Up4k*_r?=(Q$VEVN5xcDPZ?&5&RguE(APjyTZJzNtIfC9ny>fn_V;n#C(rAl{l*vL zcTR2eOeIe62$+ zvF7h{=&Fl?UH0QFY!@P5?&pYe`gg&vyV>{4+S2FO#IVF!W!St=g1-2L(m#X^{-hrV z3LL&;8cl8K@7uz}pLhRu!~8oOCa&sSKBnTI1prI2PZyc{D*qH`+#$ZaUq9mWHURxC z)^P}1hj`o^fV{}}gQ`C7y*{hP)xP(7fV=xenSdPq^;ISR{>`TAxzg=juOZXRN{6a8 zzyFjz&Xy~E@A;?y9j}YQqr166_tUFjQLb}ImVoZt$jtZKQgj}o=Z%k&r?vKK(VXqe zf>^%xqwu4#!p_s|9HHj@3E*q%Lq@-c<3!~J@wGpl5~NMkJh#t(J6P_?n!Y((p#I>| ze}|@LZPnFqb)S5zb_f=tRV{A~wR-cu`f6=NKyQhIi_zzhfraw2WWqbX+2S7dIs3(7 zmU7karBaF0@nmk-|BUUVOSw$zBd|f8zbWU`f@kUDW9F{0LmOiATjiyG|4#i~uiaB- zEOW{@9{$RjTfliJHhDY#>-LC2N5AiWgJsH^#VP;gqO9brtMmH%Z@cwq4d(8~$D76` z*GUb>$Jimm=a=_rofQLOpJr#1#K+@X_*bR>-;ODLz30D=c^_^sfR5Fiw||g?TdsF6 z+xFd*muK3ZQ>&1@efughCz)Z!@nB|*b&LWpSI&Vasd>IfpLwgpkKN}^mtP#|o)Jz5B6h8{8-YOcyN2z>nD+wwuv%pFQ zG+Cwh4Y?r|F=sn{(c)Q57)G84HbelNENx3GW9x01gPg|RzhZoMMQk+`Fee8*d*cQ< z?|G^l;<{q}J$s%xB*yfXc*_4@Q{=cvf_h>UsviW#8 zY_DSBZ8tEm&CRM6H}?EX_#pCVQWP^K*U$-KY&Uuf?6Z-_7M&!#sn^mZ<4QNw$$TN@ zhED6=4`o%$T~#lnhXLYd!njpo%bWV~Jcq*Bv6}~~rKu}(H;1z324x5_fhNam4Mh_H zda__JB?~qb$c+BF9LWCrgmqdiMi|s}-YR2WreB2ry^_$l)tjIK0Rm$DVLV~{Ul5A3 zi~j#u_56Qp2~(vRyCp%S+!jD0Sbj8Ir794+JufpP&IPOQx?DfTZ- z>z>+;C{Ps0T1m-ls>| zJ{SsT)$F8$(3v-3II!nak`xvhR%hJ=OxY7rthvm=xAU>)a^(62V{oYJZ3&EquXr-7 zWRv5|T8OkJ>%pyt$jeJw-WZKnOIzNxExfQjRsp8>;i&{Rrrd1;7jzay!z&xAQ(zC` z)}LSxJ9cMdrJ?*%bE5jTJvC5OWqHtkwJ`F9qU-pPNusKS6a_NN(R!?r-sgVg^OJg; zeVxfaJo1YXf~Y3x5$rbhG2;8V=o>XI@c~>?;`Zh&dlB?ZYpjPU*4do3koZmi;pcRP z*4&EWUV zBP95IVPk=5#(~=#^Ep@SA*_`rdWvv`ocoCdKcE?*I)>ey`NWI-HfNg3UaUN>9 zqrNEp_1=k-!}O&b9INeFPJLM>dXk@Z{0>}z-97hz!*t?$m1eMC|0|(Jp22mG_@}mL ze`@PL#D51T)Bn4)bd_Z6|4(UY8F8VYaNKhesZK@<4$VkOmPp!0ZKRNnKG`M35?a?> zQC~q15bh1)JML}e^~dr^ys%ty*Fvqmxw-Yenz?o79yZ<62fUfEV&@_#V}SJ62#Wkj zv)b{UjVKAq9JK%%SW5v|q1(`@Jpr7Fxph3vLhQp+i@K(C(Ttk&5qqDuzVTwPoz-+w zR*=)qAfd01wq!Dv2~-o61kBlyhZfBSIQxO9bht{rBJtQ1g(oV4;a4oEYnW7|x=aL_ zsBO4bFH==(z4q?5ydHC;z^yy~OgGZWb z!?#EB!x2|KjnzXa3t0#!KUbnxoF`J1aFSAwK~(&>yygE2#T}&F)1#(2#=kf$_sWVI z;VxxmS#Qa=ug0-(Sewr9WipK2TJtuVTva%tZ}{`Y-Fht(+F@skDk@{4%wRmxh4!Lb@(!7^oJN<6 zu2wP3aklTt3(EU!7f;>Hi*dnHF$}y~h=FXCXmq)OY($#Hr!gEI4h~TWQPmS-s8A<# z0!1K*OC_sZ|Gggl2=?HD^Ye?R-`u@7|4#JH+H!?qh}P<;voYl;Z&w(SJ1lYW6Q}rZ zXBgM9r%R(E9XD9=E;Wv-FjBB6#W0z_v{5J1m3oVKd?s`Exl4?LrZwj{hC_}vWv+a3 ze{)BO=)#o$R-L#giM{QJ(ySDv+oZ-iky~Y{ zSzjqE_zP2wk*G^sV-_Jc^O&kgO|F(*c@vwKN3|7x=iF*3*5A0FN;@=v<1D2*^9aMH z%`sJ4;EX5=>OC+ootj|TmSS60>RDqa)%0NbG~?CNuwbLNX2|$s-}$;1{agiX?gu1q)`1XygSm7BO9c zcF*gj%69wP#$~~1mTTtsJKF|JbAw)gMkn^Yn?$NRYcY+-0^wA z8eORNhrL@`lB8vnspRs&eN?l+`y>3IOo9^eR;uD9->_~xiRd>llhrxh7Df?59C4<0 zsn~ZfN>s~4h;}Rqk&3BM0jq-PZ?4F8HV0Q^d9s<_hCpXcA{r3Q8EGEIBjS97 zR<^*15MockJ63;FmTMvLg{~dUS#rmq<2umUIZyw0kU$}uBF$-esH5R4&5Gz6ESznC z<#eR)&#pOV{!Y(A6FU`!voe7xrhp+Mdd*Q|BQ#!8syfD)EdIy#RZ8l0Fn>AELI%0= z7)5Y~<=j-QP~km)c`mXGC|tqONFbKQ3cOO%q$CH}HL2kmVns+}6)1LJA_D7hm~R@y z_0qHoxQGPAjSwrG$mhE}CFlUHk25o83k%!`OAT*H^n-->5(qW)A3J;Ir1ZN%UyK7% zk_nNsmkQ3{??G-8?1SeyAaA5U#^H^NM6lC>`7nX!<*YG9)MtCE;mQ9d^48X+jUvcs z;Z=yo)|({F#+g)SDerywbCZelU_8T`lpvQ@jE(`Li|E_=1OX7hA-xA?T(nx9> zL_B)&xZaEukhThxBgSo22Cu8Ls`so=tSuN9B+h+jp5Ukrai|v$Y7zV!77mw6{?XuOMrhXtg zx1nIdW6xn+u_Ysn*=&g*%hXEv?fKuwD7@D!zPn%rNw3*1lAqm7R{t>Egbr-UC1ojm@{$>U;i zO4J0%)nuWj6AKC1)XJi;%Enk6^h>SXc90(BEzDe^2QzcIuPDQKh`C5%eiU!8N@M== zPP2dMW3UssePZKKbKzs(`u<;%1bRf_Exn&cO7N$V`j3~Ulc~-BEud7Evqlxd@}q2; zD3y~5d)RYD7Hycfv8Mf{*IHv4`5>~Pa@)LqIZcs#DUq>VV~dRTus0wwA$Lo3Fp_mT zp-<#>Tl+2efcQW%Ytu%QbCl`1d-Azw_>>d7-6ICHrDI)VS4Z(12uOj3h7&4;k(Q9|v6p5)-q@?D8^GAkiP(&$z zljo@zIfW19rMhbTw}nPLRWeSl3=*e44+;wk4oT>Ss$iIlO8|)_Ff+MGf+{(L<-S}$ zH!;SFi&4g}-ryygm4AD5BF~Y~xDY$pHdbuR;hk2vyF;gz1y9KH6{K(Pg^#x+$`I?xnJTa82qPs?RxY(+}v87?jA2Q`raOqo)Y+h*P^y& zLs$NiA>XwFrq#D0!C_t)5m+e69DI8Lls@6TpI;BJ_O}Nz=A1hA;khw?8-3)CE*3cV ze`C=UiW;UtBSZ7Z2lpe7Q5T?DeDTy0W!~eSX7(WG?!YX0x zahu=iw07UvtU*>upB8(~l+(VJF*_uw_%QI@wY#yoj0=E!8B?~^xSVfXbyq2Q9PgMyIab1n?!Jek!MoImZ)%8hf2GEY@n@H_H%NQy=ZW_ zIBY&~Zp)qV5ZCd7f_1V8zoYkdEWCdBUqfS2lSzcgk2a?GXJY&hkL{-(UF_{`{)fk= z0q=sknwqMLlr*KpSWwB&i<5eKs>`WTa|8v+ z0V#Mhw_=(g>=H0m%&82&7m>+-(@*@;OeJ+<<=4c>OF2|d1bvO+%1t$qp~jAulq%`= zX!M}MnWCl^X*Mae%#0Mf$5GCm6fcqF&5|OcoHqFj;?tX;GoIfq8CREm`DO$P!Xb+5>@uh1izs$N!6gGF(KnhoE}eVvFIw#AfH05u%Z3aC~E9! zgehcGy$(A3dNv+dp41jNS`7A^)G$WWxucs4F+y{XL%9DS-|t zraAlfRI0EUKcdV81im&u% zsY#A4v;`(h$S9D}aoQqCeY(uapE_RDjaX0=_&G_?%GB0-GANql^yyNeF7izpf2M;f zHKgRF%$CVTEk$Rn5YZ?12Po zVcEa_2*DyjJ4BL=!JxvmC(8(_$Q(w&VNx+la&W2snYMlb(%`PtLaCQZWhwn2Q=!X_ zm?{|w$wcd@1ln-s8V{otYvfYeM5Mu!%UUq4aml8~)%~-Tcr}rw^ zPA_XxGW3fTopIQm|Hw45u(g*T$$8(Zv7Na-214T!(|i`X0jL0W@f6|8gO26UX~eX2 zg2ZS!neP5n*c($ea?O0aV+}BvAByou$c_&x^JBYCNIsBC-;(a^CxsfWMdPQM;EIFP zikc-_UozE==>hZLJJ?q>bJ^-OWjQZg&zR{WxyIt4#lSGFmWvD!!!Ys2TVCRb4WVn~ zhewIya}L9DhSAzN8qU37h@QxzGekYaq=L*tB?Ns^!`_S%BR?5L@f%edct-IXmQZaW zjECz8h!S(KLvw~8lg24OV8iBV`(lK;h}1Lc>39$z7Qsm8&jm(V<@$}muqUn$&b`zY z)LKJauJ(IER_=kn!Nj*ffzKcXDaM`dTS9sB^fjD|3nyQKww>p!@0o+<1?r-NL)6(XCaiT!qA)1|5ya!2io_K()6X*rZ5U z#_qAD_UbYk(9Y6CMZtRvC>n?;5*k_n;>3yhV)t01=oh-1Tppqe9AL+|QMc%WAMS;E zQjca`Iz1FEfR%iJO&MaiCBUTQUelt9ho%+WccdL2gvL_CW~yFf-y>>Z zgbm;q#Gy9Y&wF;FL9UPmF*p*Q$(^^J8Hm2l50;!gMWLjkoPQ*(f;y(?@5W%Pb91&% zT;V}jY(|LK;38~;=&$(|WIR5 zp88p+52RfN7-BbHC~)v()al?BZp;&k)FRD;%t^WIaI-D1d;eT*+aVpEnPxzS!F*}J zjE(cbd@2dX{9+tF$O5d9QLaT9HljFz5|JKNf_w;{o^9sx#M_*gjB$JB-?>yps9F{p_% z>9YLE$Fy~0AMO@jqBW4@&G1o7A zLIx!v7VJ~m5}+y$I9Dt~-F(#6B&}`9KiyeC8(;zKp!ozn6VVVHnDBNhaB28130NyW zZxs?8(V13^JvvkD%3_PUhX?ft#`iuV21do056^+mU-EWXkA;G3V^>fvknQ+?Sx7J) zoIf)*`G?^bW%~*9559o%+?{z2a1ZN&n}mJ@^d~{AOJnhK4_%{yfGvIBxxslvCG z6yxyw*r$dHgv}71?O`;*uSiX_NDp9=LK_Xr0oOa4aIQ5FgrMqex0lA11KU|Gf{Ee3 zQeYgF6l}kFDf|Vqb^F<7?T&;m+3kRCw`Ey~t*@a{RLL>k9CDkve}tjD2=RNG(89{` z?=Enu2G~|MV2gZ4vd>VmAX4)L_Z$TCfa@3P8Bmw#F?R5DxHp}Q?;<`2{ zxAt5WQ+P8rfo$`}7|k3I^G({y-5M-xgV~&{gs{x;{ZWt`X~RYlW)RhfdlVSm!%~PK z_6FH|4urY*3yj_Z(Vn99v9v@zX9^@OsKtk@&dFge$QpEL_a{JInvm>-pX-yMU|UGC zbX^-&%dt#h9zg#fIIy-O+8I-HLEBPw$83u-YqXlquIH!OJ#K(ysWc*q)UfjZBAKljj6k+DzDWuFoTMInfY&4pdFsXguy=HCABct@01GFiUu zQ3$XHo~1q1K4q!DBzqFa!!zCz1rzY0J?bmPU^V{%8L2%!aEwzOf*+p1HMw(-l zg`ihdL|3HVaKN%mluur^2&IUZj}iQQB)v5*;lRhUG-%2CKd_#hdj18%*<{32A~P$9 z8|GdHL0-z!AR?+W*NGYxFMtZrfW!MnIMPOBizR9rh>k)V+hO7n1|N1sj}Z%2`D|gm zaGE_h#&o;Gyw#&-QY4%X*~S>GflAv226keq=U?%HBA#hSfGzJz3quMgYKpMD3$`QL zh;esNHFl2Zft`Vj-%cVbxM6bC_8cerWutoDAqa;hVWWqicsQ1T4d33zdv*9q3ZkK_ zr6S*rjRs=vY1TL&71GJ`G%!YO9XHVGcRaYHtXrla~*%x;eFQgJf?se)t%R0gD- z-5m+CaqB~>)LrO6|6{y#$9bw=cx3?Uc;ExmZkkQLuYrKG`@Eqbv0th)=^NWiR~Og> zZXO6&FUuq6>~iITIRASnJo^UU3kH`V(&+;WT{VmPDhI(|d_!s8hWmtwV*`KY9dW}0 zw-)C5N~NeV)`V-3E_6Zw+S(JQv2Asmb6dJ6l;lo_AUa3)_Ey8KaCTAyfnVym@keF9 z+7c%2>vpd}*kjDV6t~ab!aJdf5Ca$0NPDvjdpG;@(%;xx`MHls`(Fv^cSi5Wc09cf z=lmbb*O@tij;jutGmgaPnPt#t{ja=Xf$txU#oR)m_X_;iK`^0Y_%nan7tKvUjdmP| zUY|(EJHXhww$JJ40g~-`#%JMcCf|AWJW3(2_u^ptrjP$GUTgNZU)PxHyR*Qn_T2hn z=R4?hROf&@12urMR;sVhXX;v}jz2@-byz2s)WhYipz_T2ar3ixHTy~5|5Ybr-Q{h% z0+f)W2fcl;`!-oY!3*LB;B#B>xjQe#{_y1d@mLm`91J*5_xFAI@b|lY)6dR&g}nHi zaG1%r`bNOp?ELHTvF;P9M}O_faNXDc+v@LG;7pyjKqkTI`n+AuTGtVeu617LMNZr` zp$}k@w2siuIum*y-t=-IT7)kLQ2>y0*ynM2)q;5Qwe3Bl@TqC>T@~N!^?6y5C*XZI zR}y8vT>E9UpM8Hg_1ZL}9^dogp6dO|wlTnSjl96 z*nd`|s%_ZhF4pfk`W$yWaw~y@<$qfBzS^(K zY@IDNm+F(1D9sx^KxfBt%4h$r9| z@R9I-)tI}RbXRaKS`Ht4J*_}Bw*RU-+qT9;sndLu>3fx4D!7d|Z!Fr5_pM0d`kbO< z-rdAnJM1Iy?G|}afB$-`d+619oA><&NR4%VdwKnI7fzQ_cvS-5Q+#8TdOo(-@3C_` zOl|L5Q;%~#7p=RyuUT@K4EQ{LA3bH_gnR4py}lECzUS)pM=%U{bf&enZ-Al9rXzbl zov$T-g>FIHnNg`8etC5BIsP6DQbA*G`^(TAL0>Q1WWVj|Rd>XB@thkltA!KLOd9LQ zrYctwA5uls7S3o<5r)l*kxb>9d$7$9cidKWRrL1SOu59rQputPKU0QV@_xloTKd<| zJ=wr=luC|?&{FgA_VpPUC=0UUVcVB^*=FMW&s!xIN}4=sskc`AF4Tp)QZ^aBWIc$Q zJoVQk?N|~vNkOu53VCK^8B7xG?`VBGO5|!Ol#w?H$krz@afnv%vhtFvYxb9$>zID~ zu1%i4$aQQe35eEbMETzz|3XeLWIZ+xlxP>$={ugR`yb|_lvN_zN{USR5;OmrXywwz z{!Td(PKjmy&21HxW4IJszANb?+$M@wwdrBu$M_0?AeUh8A-T&+1PHY@wP*)-HPEkOkaH}!eHAgP|ZH+k5u050? zB>{lsHN?Z6;MOiRYn6+E5SLO!nqSD%T-m<^pIN3Yxp2i3F}R@c@iA@$9g4FXCW)`% z;L;m+(TOG!#0~a0eunHg^!#)s7ZAP_S!OGw>WneFS_|n_Y+grXhLUD3hRgYQ~p%UI&>JGU0Z^59-S%0WHKB;ka#a!?zY7{g$ z_XesH5-x1hmuW?X(SxRM=t%|WYeGMGf!HA%A*VwQy<5_=^R-#^S_Jpj+o%3AMjfr0Xxs*G5kxVYSi_8(}GOD-lEk%MaZLF`e!}gk^ zy%`R*#t5q*lZ<`CtgzOw<~R)@u8F;3{`UgUUcEC!=LgGF`8mx0!H%3=jZ7?^{>KVV zRe9TPi4n{7;K5vXtlA7yM?40WY?Rdu-R27sia}4^VRW)Ecgm0Fu@HP z%FYuaH!vIvg#pwpEi&9u#_?9ktgWZpu084$e^ zlDnk!&c&Mr#LaKF2JfqJ|BsD^F55C)L=90?hSJfXCa?fq`SNK%I2cIVYKy4I;C#Z4 zmx-ksDluFv4iJ{zJ}T-()>?qaR5=$E?lBuk*dsER#T@hV?v04eAO<6wguO2?&M2Kn z2<=r#Ht`jnh%$0o5#8g8f{ZuE3q)2LTSykJ6ZQ`4Uw({H#cP8D_z4>w(&9mFiy!M0 z;nJsyJQ37fGo!*J{4trwQ51CO@;myu->K!P+1zuLA#V27GSw9~MWaP+V%d}>{v{7; zQ%qEc=kA<)U&Ym#cWg4f{L{<@2R;!g%&X7=n3hViC}0@L1D+=kNdkTSt8=MMStCa7 z*g{>%@tI!9IP+1*ttHgklO{Fh zMu@JiE(#BNZ_jiY^8Hoe0PH~@VKJ$;7gKg=CiR>Cf7jv@z6@dA501I~^P>C*S9dYB zb+9q~KN(dy8ovK8l9{)FD`mVs7sJHLQQVg#?47l__c< z*G|h`h=`wW2IKfu!0IY8P7g0d#A@5LZJSiu#noJa+!KS$Tv4u;5m6R9 z+JSEwUP?JQ;ixSVOqc5vQ}R$zZzvC0%tD(|fK1Ns;41yH$R1>uT+pm-O76-7FSBbb}zS*oBlKImXHuP>j(MABt)j zyNSD!T+1}0XiRK1Wmzn|+=xG4{>R#fAMQ7N6%$=?i7FqzZOG_Bz<{Oi(EXwc$)A3X zv;4D|L;*}kDgrw2lLb$f`wGE5Dc1Btz{5ZI4!Tsg{-m_6A!X-@+munc;sc<}^FIVz z6j{th8?mU;nA2S`qmSU0bOjY0-GD@g4c63j$&bR##I7Q zOXAY2cPvZ4t^THDSUL|zma#w=F`_7hJ$@3?1oCFvQq_bd|NQ0(@}(`bOK~l$nCQ}^ zKgz3576x4LR+;>?3W_?)2)e-{i?-aVp#7Dj3Z{z?Gkw-|1dulr8gGda64?M33~J7a zQ?u@%Rt062Fm^&lkOJLUxst+HD>!O-Sl}uyA?c>Q`KJXfCqb{V*bHYlH^ISsA@bw^ zQkl3CjCt9g>=Jfca!wi{$o9;k8buWL)w@JQhf!y~XMzB%USK$1V*u`Ez5h-ucMs>s!-6Uc(L zME^-9AWOkINyF`n9*Dc0@kgrKj37snb4d?=t8Q4)tdP4kD;jVflg160f;rw&5lfQc zQ6FR{0ihV7N+|}_pgALY9DmRZqXH2qBI(oCmRpNNgxGWgNi;{VJgR&+9wfl%7Mh`I&vIHLVN^-vxeS)Owk?22SA5Dr!0okD63Ed@lgly z-BROGfo_8jrfuFDOGtvR=#bMVh(%r~3n6+SF)Sh!u`ViN(E>8H3LPa3>YfU2THn1K zoh}rX|MF@YMB*YtqT4|BxVHdFU~fMHUoM!)jZ|5%9)gkR;oiz(iLUg{d>c-_PvPGd z+y}u>+cr%H>1-{y+>TQxr~>UZABG2`rr|dt-HnIN2=cGc#BTyY#$i&)bx70*N=QeP z8jy+Jp8fs)Ux&&nX9d?5550txfXg(?5mRF@#HussHoFwf9Z1``HlcA~q`F^7WKDja zSk*$k z(~?Iu!_+>Wakjljs_`!H$Ur7U(AYSI-jH6#4W{P7w=7KQGmt4%EDtfZ<+Ndjr+tKo z#XQ^+LW(9qB3Z0dJmb3m`~{F249!MiLR$M1%q;5j9mF;S;ujPVEyLHYC- zha0-Y*jZu`;33hrh(ez6yPj`EZjI|Ay#nkwN0WZJ54Pb$^7812U^0Aib2v$Hauu{5 zR-J+?jtt*)&lz`Y^nM){q_RCX!W|zPgWF$SZ4-24|!@cTf{Z8h@^ ziI5*tOE(y;NEVgLG5kW<8Uj2Mn7&(rQM2aWKASUSs^HvB@dQx8@X#<8#6AMj&U%zP zF&qX@!nmovE^m+FE!%n|TyRKd2|ZVeFX+|Xfg$%F_u$B!pP>^yTrm&`gVRIo0NK#` zDRmD%NXS}YJlcJUuc+P}>nTLr5p6>?#a9D;GholD$|VX;2ed|AfMMbxr{J~)o-;_N zt5F@Oo-nsbKgU==m<8IJ(M*LZ>C%<-e57c(0z%$>2n(Ag0V>B#QIz1BVzhAx{I{SB zsWV)B@G(?>bjtxWtRCLp2ym|OzGDu_7X0Z5kR5%aCliwRnVcL#yXid%bRA2-p6PZD zuoB$#NK=9lt-$`sp`<7ri@|B|L((n_8@$33NxiEeUIP9zZQW;ZO=?g6HTKWPAZ zDVAjO3rfDyg?;$u=`MU4e<}ln5uIP;h_ZfCiO8er6vCr+R)%1GT z;6X+A$ic7H)HFGU`O2TX5w46)C!d-YmyN)cqv18niBCaYj9qxN=7TjLwE9aMunm(l z#opC}Utt6?;dk23jP^|~RAIo;Wqy#(|2752mU`U(Wy6j*Q;y)RNz5%jTma!TK3z3? zyxBt{%|>oo0uZ8Y)O1?8&S%LgW?-gP?5usK8^<{5{oXz7w}9}q;nLzRc|3jU8OYgT z?fIR)X}Nsc-%`+e><e|@c6$LDJ3b{qv#_R&E2eVxQ>QTkAh#$9^|M?E zf(56KntcCuc?5qFJ68Bs<#<0wMDQD})jmAsTVCzCiEHScUG=*5JZfngtJ3qmewchQ zmiRtW$WswT*xv1{IJ5Rn>w8;k|DGPAtmRd|0qB3<7J!cJd~ZLl|AQp`cK-L6tpEMb zzW43XeaFjl>|XbVc1pXW=2|`6%Wv_0#M$4Y(f9wU?5)GH+M362x)G2DrI8M4kdiLx z?(RP{?rVTN zyrylpmKR9oh~ZDu4VZXtd#tF77zE#s3WtzZD|ntPRwr^Q^6(|-il^f)>RqoG#+Ifo zD@I=!Nf+%nCXSIqt6;l`=>Y7zCr z#?U^;=qRUFQQGZg&zC+t*XyObmP&8Qr;77)_c8O2MCU_-IaXZO>so#87Vtz}LsxEd zhRw7eJZO%6Y>tBB@NqOR?R1`4Sv`+CqQth19`?ALrRW|Vri7iQJ@@x^`&ykg>)b59 z8I9R<$$MDc42QF@t~ytrjYPZf6`kITr4-(sQI+0b<>FJYb8y-0$k|-45)~Y!1#LfU zB3=wS+(=*1;a@&&_XZMM-3L4h^l|lmSrs`fyH!m;Aiwug>?bZp%^m*8UHzFmWzu`VZiJ2>k3 zabxQKb;*R1s~ly?fK&Vr1e^*}#>Es+}ao9m4@x=Qe_pN z2hoG`Tz3&pRXFUqOKLxIv~gn4fikOc9;Ce~GT3n|uDfPP&%ux~l1=HE!j?8Tf#*Qr z57@3ufVjY`62z`1OJQmwfqdh6-D2~^c(l^CwSyXSA+?bk2LeKW%+*h8!kiPQjrEIb zNEhgJz_Qw|Kr2Oi4>c-Gd&DR=Law7R>;$wq9yor$o$0Wc!!Le_-jx?psu49*n3Bd} zVG-PLT5aK3h@P3w-IX9gQA!$4bNtY;?UAgRu^R~Mzxz2GG4bmYQ-Y$KMpsiif@hh zXk=i*8D{V$L235uImsE)I<)s241>>{Vt>p%7v$2YuGuXfRk16;9_b%7jy-U=w7!qU zZ8HotISNV8@M+J8Ho%;#j-9r{E?FB%Y|k~4ka3<4j9+=dfVUT!|XEjHO5 zs;H}{&gP@-+RSyZ0Of}_go;XL{vigl)IY-D`hmDl(kxc5z;rhoifhZS@C&hd2a^T0 zwD*Pb^erEXY)0NAv2I9L+nNvxf-kAQ&D4> z2v#N9A;Qy-(;2dqQnQ&uTeGv|-)xe3v)(4BQj`B}kUHssWT6=Z9pu=2Y{ zZ`1F*af~~*tTK!Xf-;xTrmH|NEO})SWONZ=ZH;}gUxWxO^JAdwU7jsL;+1ZUtujLNT1n@5^aTSqS(s-E%D+cs4(BQh!Uzj zSfJhSB|<+bsmujbq*{WloU?h%dv5b8)p{R(CkYmkZxm!Lp#!{pl9Z=jXreAkZ`4px zRL&vYN-THjiI4YPKl1Et^;9OGX70p5D2VolF~f)o7TY5}K)ca}-`&ysT zMzLMF#FBt%O68JkB9*FW7a>I%2%};2fRKu;ZQgWHh(+>53;WwmT|f<>OjYN1lpq2i z0;F$bBjd@t^)i`2(-G2(VKDP_8Dhc)e9*t-WCdm+H~2{tAu@rYK?iBag?kSvDSzWH z2doug7GPDc*8n!KH4Mx1;}PxJ2F`?$2#Jp|EkQVTI58HEnrdtMjBK<@GRO%bQ)@BS zf(rP$kcuGiB?Oa@CRz$PPE~$X0n~z|E6qOT!Sky)w$3;?-{ac->b|61iEBeskgxv z?c?DpvSzWed;8v=1I!tp&ji`=x3>ilZn#Vk0%asO!PJ@k!BQa+6oea0Oa0+>^LxK^ zNYf8OHmH`q;IARoaF8htE814n&R@~fF&2zg>tMDH#WJ;4U&F~#C$5V!>l_yg_s_^B zpY0Y^XY|jGlDCUC`>x7{%}>OAiBJ>!wJgZCsD6402cd+Pz2ePa$+w02 z8Z6T@Ue~K&@4Y6hN6Gy3!IGW_l@C=;mWJ8~T}6q=tB4!ev|bP~KQPdAcwc_eq=2oe z{B#i=4KpdP+X;zzK%~yZ^7$Q@A)39i)#sz&Dbfs5F89q4hCKvM3T&sfM^z8-5zE4^ zGN<=#-D}t1DLGTV9kv_mnl(A2H!6H;KamjCM8Wq(q16j^Fg@kpv8}}`U4yIlp=`yJ zkp|R%q5ZxDUkjQa`J+$A4AooJsB8;`L@`cGNoN$9TT?z2gyLL0DAN$Wp!*0-7!E_UGbwuV141p~*PX zJ|5S?jD)RGwY|1wg)3OexSeqH_R|g1g{b84KL8=G7-4$%f}r`P3f21Ba*fO@4dMnP zaT#_~^?iPo&)0LjiW=mly2Rnhxhs0ER0yz~`@gb{;gv1{W5 z4QhO{Ym9UOY;Nh;FC>vFopIOiJN5AW1KqNR@!cGYhXdXc`CeJK>~s^AVqS1Z;2THx zhoRs-9e^w-e&0~+?1O0#Nfy89bv__sQH+@updBiL_$qYH`jVa6cYi_bqxum}W}ooK zkd0Q@h$@9o$7ykt)A4V{a$4;W(d^iIjq+uY4&S)C_HzrSeRO_>*97B%Kk;5c^WppI zTE&;*BT!v>EtOgSGe*&44ROb1c-L|yRERW^TVvKn5BSs#h%|z?6Q#$DdR_jZNAR1$ zk2mWD0~M?0ZN)H0JloF4$MA8SeKYR+er8!b*+q3v35~IwQ;jEPMMLrS5o~ zMjsuRZQMyV;yLf13;^=uI_>y2LMQIg@i0Bg9?NfBb=8oiVb|XVD+M^+Q`i>FB(LI4 zxAh&F-5p$a?*`~5ai^K?!XO-QHCf+xmy|||Gq2X*xsN(kZW)!eOhufy-9P%xht0aC ziCugcX?2I6$G<#xR}Xrz8>&%$`|X;@48YNR0iapPX)uy>nP zYc%bC#}EJDZlB6xOpWJ;%=D?{hJ>j(q1VCP>uYOs=~fMOnU?d&;J&o8s=ip*pB!zKjOY5Pr370Zj0-N zZv*z6+V_4&0<5^nAEy(Tn{# zRf5+(QqDmQAuqJBFNOh0WF-7gd*46@)MX#yy%HUk8)}M{yjE(IJ6^~s)_SSy)JrDo zy9n(#EZ4bI{B?>8byvbD*Ql|HA)_Ts@9x3}GIc}_eSeX))~X%L*RwFbDA`~1vGn`2 za_Ee@s^5meuIm-&^z>Vkj*hSpo1O6vi2wx=r_JCg72@S@_h1_!20S=5U2%^s^k{iB zxT_7G;Qu@mP%c#QRs#~yUIP;6A^rUY;pbN!}|$EF$Qd7qYqGwHAv4uFcG1g7ewRQH%4@NR!%_ z#;^ce>6IlBa}(2Iz7o5nh|R0GNOLNyg@*lFg_k>7T_*94{Eo#!rLv=oxh*M+Ot@yP zw@|r@4Q9C-RI`dHVNohJ#g1?m;1hSM)+_O}v6YSc1m(%{S# zb-E%;_LCf%zHORb2$k+C_k@Yfo3K3=DvYSrj+ZCRXjeg94lx(SQ!$(Q2{mXAp%07o zpu9G0YV(V^W0P+$PKY_cogs9(o6_dkWmjc`yIw5+k?cIOG#aC1=74)|!T}Og_}wsF z{w*$I(f37b4%T9eK8Ax%)xKC26V>IO$|@Dm9Mb`Nx2?uD`MM%Qep~3li@1Fz=9CaQ zhNjn5`dh_83N@yPZ3ari3#MK)eqXcTsUDkEm=2~#YZv(qS4`#%LHWdATjFPeC9NNt zu0HTdwwPe@Vw{e{jxKJwRq4Rm&x6Mc?qPq~&0sIuo~6m9{7zFPrg4cmlURcYy$Zdg zmo#f@F{+qdq)Nv^rC6#?HLWI+dJ!22&BkpYArF_)cV5u{iTf?*2`sGFYf7DVV+niZ zT8%n<5>~{G^X?9*NRUNsVbg%lUE3>Fr294!xaH`d zB~>k*Q42k^Gd1kw8ifgY8Ts{IV7@$@zwY*>y5e?vl+u|+W{M>F*lx34rbB6WXao(Y zhekkmcPk{Xxtuiw;nxoX*Pq<3qfBm}G5Bqfoc|QAg83yMs0JxH)|&lI7-yd8E`FYj z*cl;*JJ)Lo6OmoS+OII8cC_ub_biDGp@XJ*RxMR*gtmm2}Z1pH=EEtt>0jAJ??!rD@niAnUW~- z!HznwN2Itv;63*gO9lromYL2A8Gk8c8I1tkgAV-Y6nj;n}xq0_IML` z`wK~P$Tt(5>C%ZHcFolZ`{_d{u_Jq-g3b(h-9sTpc{%E{r?+B895srMbH2cVGY%_k z#`+PfkZGK)lrh&x-glzO=%H1buydVGjw*l{kjKIr#X1k={`j4>*!-PQ$yK!ew1cN|ueHhmL(Utv~K9F=FZiws`fEvqh z_ej?jqmHCEFra0^`h?}_eMI9cGPdC?90s8wLlX%jB0~*IMBI?!=fLR6(l%vUl$<6H z9qs}VD_XNg2!H$OEiaZF41uv12svf?ju%6&Xn6b7j-Rk>kheYtNtG$l(1b|1Of#_; zCNyXNHx#z-DA`W?P2Ml~+95Itc3y$1AoQNFiG&b8XcF|sAd71G@lPlpE~87gfMKuk zrS~39YaxV_8GV@tGxy@zpz%x=P;7-<53{EX>Vv0e`8#UV=d4*P6mEZ!YH$JQGhUQIV;bIkF*)R@h2JHNE(Qm z5g`bf-Zc|dzMQml9@^_c3{x@drX^9jQHr%2@4n!?tZN8CPH=JxK&g}zQKNKHNY)0$ zqO`0EnA%(>VxOkT{P0Q6%vn$@ZSB(KZB+}YrR~c>2(MAFYjzl6BLA^U|L9LqVy32b zFAxLa`4s#eiOy8z0EY3J87V2~8*!$Gcv-JkY zC=7xh+byE1$2iI%>&_%areY^D@5HM~wLtXZ+Gm5`x&@M&U`JIK@2s?fN`S}v2W=%O zp+YGE>?hzTk_H*uq!~&aypRibEGqCquaB1@>xV4)=uX#j$Xj_w4BK^~94p_tC8(%_ z22;UxC*ufeGl{fSF}a|JyL35xru8XLH{s&NwB|ymjD6_-T3p{-)%=k?=-WGx4x~8q zF3k)ceT>2+`qMM|*I=ih6Bel>kSsyGDdyp$q?hkLAL_8kE_C(cjalVrrgO6>dHLWa zv<%M>jB_JBdG!VGn`bi+<2jFXAvs?oG4h0iyf$FiNWDReIILjqvX=r$IqygAT}?SU z{et%hwxI=~ntT7%0Y}O*j|PO?7WyKfsjt>QiOp0P@DJ&YN(o`;{z?d%x6OMU1#I{M`Cxf@^S zp=R6TVsv9>3TbsED2nW{@pcKCPTlzq9(pgl?Q|1ftAtkK;UtjG(~^+B{3)eh3eSPh zGnJ03;o~07M#<_3RNM97EWbv0&puB}6WjH}@Q0qqLC>Z~hNq(>eD14>HnFFZRb{Q) zdEJ)sb-a?8GlX|W7&|YMrm!@%Z8B`Q#;)ynx3PFmDzH)v&b|QZzP^G^G&o6v3Svyi z0(1frxL|@r{})trW1oTXM);~5(AcSE7hD57)uAql-0_gKthAMIUvt`#&O5Z8 zMum)IX01qmHOZ2i{DCF!EGwbwF}FwlgMuVh49~YUt*xo*`7zo^b(@fs`1w|oO6UO$ z_ku6#R?ciCt5lf}EGXB?SaFZVsykp6Th+5(wKjJ^4%YDHs8k0nwOnm7L%8-3k&)8U zQKycSP*dly)Tf$ofqA-6LywtoHBymiiW=I%V}7T#v_4%Z#0ifH5y^&XrNb`h(m z7JJ}jgsU!We|o+7b`TY3tFY$#n4S6>Qm%f`8>`#lu-Lf6p>rtAgvK9U>+%(D8@-M; zgB9>{Nh5AK>hY!zY1uvXS5nEMAD^`HVEr)-aCU%;H*vgyihu${7={DVgkS?+aII}k z9O-@qb=lGB>e!j+S(+Hq>Xpb(SSHbyc(MpJIf0q^cM5ICeg(?5)J=^(RzhkT zaob_|<~xq=Qv5vE3v^2Nv+LzGWsZ2(HN*tbkcJ*Y5JV-m@V+%{6|OFtnr(scv7#A7 zRMm;FobsD^9UwKC%%+d$2&{8&*Q%T%o5ZyRC2tEv-g)Z#7~l^u4Gb1o4_(FPZG7Ad zh7O=xBEVqj$^MGMT(TdG)24D=HYOnZ9dv6QkO);*z%8)%>zS`%q!XdVtoWejO?gc1 zyrYSne*GpjkD5_EO~{_k0@0WIsf<&$!D2}yy;2W{xNHMof|#?~jHiUrnOT$`R0hNV zmfm+X>)gJjb~qU?3;TsQ_aHS}bn0+ZH^<16w%X-kZ3>hri6kUpKe)wjz0KyF&h38p z9`_#A^i|7?J@q$zt*+uElU8|(qEwX3!FdU;5!e-3Z>vHOn2TqnGhTApa5jE*)S1Lq z5-!+#R~+NSC+Z~mm2OO#0lI9T31(MgZPlOqemC5&PU}7Z--907g_#qps5k6|Yl}tz z*JA%kDT(zwV1c}=pFdyyd<_(ydZ)-7zyzq$JO>y+xPKTt2DauUQ6Hp!R&hRhKv&EW zP52sa^BRZDzuN$sMG{8AWW!4(R>A~cLgDG1=peV$m-f6lmxnhV>-KsAMG+3m)_|;e z7=jcYR-qPi!M$Z}Q3B=RBa*d^ru{qj-i_TWQ-{! zMvxeW-o8Ow4yXQnlR^b%CI*Q{o0G1d$WdnvdKn5Ov2|n3xe82Wo)si9QuzM3svatN z13A;Gc3a(C-z2p%6;rQyytp!&bM=^f=2yEGhtrnFczqq@ zp5pdzN(D&E3MlUpg|Sjp$kq%GULW$Bq?^ppwxy%6IL&1O1@)snP-VyhlpFxK2>zh# zUz4|pR-AfJLSj&qW=7_7>F9_Q#fa#%jPzj1C;AvuJ+q7YLUCC;c-xoaybPj&JqZKO z2_`NDg6v|v9yT(S9?bH37BtFeabfxqx0@fCUJnpl0OeW0Jl{-l1hix2-`itn0tgH> z_*tsu=VfQBXA#x`_lX`s;OHS}jh*_(RexnYi^!ZM&4-hh_fBSN(&~1wjC8{S0m!ey z`Pb)(^9=_aH70niK)yzWMDpio)ZL{A|5%@w&kiJ&AqgIDD|CWGrxu;Pkv-o$_7|xP z<9SLKeEPnW)t7hFEN;`Y8J8*1>KTvt&*S_(NJoM?puMAjix6-Dt#;3C{&yuxQj_;f zO#IX#L}m0pbSUfH*^|I)NGUCb^eH$vDu&lbaNu?tIi;q?8djZS<2{^gur0lPC#m%w zHEmA@>KmCNXdrdAm$U~a1Da$8m`?hc~;UhVMBeLJA9gi1`Ev+24|pQM_euiWg_C$s}mY7a(q|C#lF6 zSQQb@s3&RTU8Tn;9E!E|TC}B17=v_d5^|&e3|BxcGA-j({yKOvm>J#vpdQT&1Vyzc z2U1a+=7c@Lc^-!#&4&|_C7lGll#oX)93JS&0>Y)LumDdr3|J-p_+)kt*0evD3RxNP zE_!5-v9kKO_izRKcy1{2-LO;=XkP&b(b!yC>JMUxFLE~9TpgD8x>kO?ua9V=Te@-= zULV9LiLKuMs8K}pso7+ICn(}0mD7E?`9688h?8`vgS!rkZ@_GH_i0h>=+uXehO-7m zLmoikf^P>QARRR;wiQ-FpBM(mF!NSozRn?_V~+9#e&&N4NnuDMa1_Io#3?5Y;g6_~ z$YF@$E}guH%0*adGsAwnGr56HrdvJ$1}>nuB(j06P9|y#p-nT=N#A|&q=yVOI0I8w zY?6B$7vg3Naq=28L?$eWXG>D;Yf{I<+0d!p1KMbv=4X=v&FYVoy$}A<$=BVrRmKJZ z1O2a*Ncut)8I(!B&FaGR9{}a{;-ySM2>@ud2(WR#`rUXjw6plB(Hc1ovrfR?eWkQ% zfSfVyeq9}a*a6qc62PD(z&E4G9KRxhD@gHpMXJi&Xo;kzW^8iE#m;{9U7bg9LJVxA z(gY{ixUPuR^pR}%Mm1l7&&X?-#GKxzfH%EMn;s4d&)MU^iCYuwOTqFI^3!U#fl1qy zh?8Fi2ApU)AkkJHX~nGD+}VUpp>t*eTD|d#q*zQEYtijX2)O0uv(FTQ;QV;}XMhww zYmw}IB4EDE>5enI7+sum9DoemF+Ij2y59ahw0!8G@QAulBZ>{7jrH4M7cu2+Im~-* zJ=K!;c{uEvBUxVtvf&z-gt*yUoGPn@dr|Kc_e0?ZYu%!2zNjQBr4;xHU*=g4j9%6U z9pK!BZHW$o?Y{^|u-Fm#R6YS+y|Q1g$2F^lE3J z9xkNsoKG$-^UJ~5suZ-e0Zoe>fe>3EK*z}eF2X-`;BVFUX5<82!K-BH%OfaD+Fu@@#?I7eWwY2rHeE&w8O2EcVSMr4j6EhttBVs;Qp#s0$ z``Iw@_y4wFJhCiaDgqi51-O2znV*gNcg+B{x`F?{n(59VZY8Oy&&J9{>V9u55Cv`u zygpNDY>U1FG)Mq&5dp4$-8uk`(y}+Ov$y*0ODlbYzqVRED@#KYBlobD z7e4f8eplQ*u%dN^@t+&_LQshb5p@Ykv_A|HM71`>;rOvx*PUH$$1?xZeXmO&5C`G_lp$+(W@y|~S7?ZUB$-8PH>uR=04_JHB)?@1} z5}6&ZK*0dXbKpjNNpKP10?kv5~nbK*T|H~DDoLBGsMEHo$oo{Y9p5Iq(KlZorRZ2S_}Cp)kH=L-eU2D$xjLE|hevte7YJaLCfqpg^>+}Fkr8*Om)7C=4WdHCi; zTbg4bq@{M!H_y9jZBN;ZS!s7U3cKR9MlY}t=vV>{$f7?u0`xq%tHQ%2bcS0y(8l#j~KZwQ+mkK_$Sc?r3b3jgs2ol`m$ z((Z;`ZVwtx$&8ab?QKpSrmu#i(E6vyuh_2v=}g&l?XOqL-RO;Bt2XeLc9fe2j4hbKB@`7ZqdfagabL1lU)@ zXu~;lTw6#9T2J2;S050#?;b;A25S$nm@ctLy7BD@r3y{z}>Q zpA9xf*22n{0QvE-jKFOHCv*NNp+u$uM-~psn6dBrsHtkAa-JDGZS&B%!Il1Y0RB?5 zd@)efAyhuh^p0ev5}XDrd_oykEVujQvUm1T!>a<7&3DJO13IwKIpe~m#X`f)Si{%_ zv4k+mvw<`Epcrw)&=-N8!&M#!!q*O3)@Rt|cw!%l-xL`|`-ML~he4rj+=t7xWAOU5 zAq~Mb%mP&|cb_}BDVW2U?X#K<^6%2p?Z zrVYs{cXN}4a`3IoA`r_U@Z0NYUI7?)O=lrM2x!POUGY|;EmY4F&+Z07LxkWZKkE=2^EKF6$SN!;K1CmqSQ--nXZ(t9=4a&=71bi)= zNwEiCKHlLC%&N!kTH>h^zfKu)YCRL}6JFsGZ*e7>jb(1a3xAkjZsx(2#B}ogR=nSg zL~N}&y6S9HW^D^LFL$0%d$*Ky=QlmHBI=%Ad4~DWX}+B-te`xv{-O0%ZQ#vd;)^^% z0x8mjIwRSe`qaCsQiIoBpE;!?8q(8eBzzDchtSsI`lq6Es0dyWbHU6I(w)Et;^`d{e~q1n-zi{PGFX`s;F4{a0E!_+w$uN#qUTS4*!J4A&T}M>}rx zY33a>ACACN(zD^vSe<1-=g{F$P-87SC8rnrP)IFA_sMXjZHjbbUmv1zCVl0^&+*DCYjMcq7D3PAaQuEw3TDc-slRHB>N=$-51 z-Oi45?oENxkC;k@%#@o*MiS};N9vs;ZaIS4GQU2Wrs3clk*;0$K zVGg+Wv>%ue@k6#rQDs3L-%6EZmJ7^O$WulJ1f9sMWT#B2Zw!GXonWl&kWemof6JOVr{zS1(|I5{6M|*@$~HLLn)!Gm5<{!S2%fP2zlv-p!^$cC@Eq!YA~S< znLTcBow_C6482Lv9~a_XYut;zC*DeRr3+W{d|%$%y(#kVO=dn zxS7Jxequpt7Z1$m(Dn|?uqP4p+agS=bb8ILG374zo_qj0V>Pk-e~ zpnlPEV(L5AWmBPrh1ZBrDd%awJe`qPko2L|IOYfn3e(NN;+Pq|qoCX5?xgFic?xRt zQKU!}j>lb^DKKwNelKC7O$+$00Lt zb{U1C&D=D1W34y=N%t4*TtZf|hS{0< za?Sd41amIhGFib26iJla#!uiWjB)?i{75qpq#I>`t0lpQOH-aCE6?KRW`1sLDOP9| zq_es=Jfp3{NhxZqV)(Fh3uHKw-Q!yb25fYSfKBhu@3Fs**AC%Ba9sq*f>!{`Eyo-Z zrN*58OQ>GxEJU=E)%}QCe`!{RWY=)(GefKqUxKhMns+?lqZ{cz#3dCD1+{F!aQjK} zId!?-GmP*KxK4g?f-f!fCnMVPv7hu3?F)QMB0~~BJIG<-yg4@W5cveglLBseaX&9F zCQMKG>dlWy?~5xdMO;n2s@8N$tADAO-do z`KK2`uiki=2#ZIzS;Sg~m>l%JKGM<&5ZIX#FMx?3qeaYDxxN0w6Uds+?4Dw zieXp3%e7RK-1EW#mX2Q=n)~I~QPzE<>@ODQ%y?s-llG0TLNIm_Zn<(EfvgdcS@hXI z$Brjp7yqrv$j;XBrwOD+Rt8{rMD{o;>kmdI)Tq0u{mby!oiU6V88Kf4rMDg>|0?In zJRhC`mtwk+vhNUBZNdRfo5z z;&{<)Ro})^>lgf~K6t$k2C~Q#CMrkT^6hAmR&I&u?8|E9+o{HT8ib=8Ypw0?oGt~m z5I6O2ma4O7ln$qSf%^gzGI^B7eB7TRNPe!9xb1G=IUAGY)) zC1Am~O28=A_{jHpp)s>PIbKqLh4ob8Tj5mJG<9<8cT0((NP7#hH>wJ!s6dmzSNlxt z=X?fSznug>`{UPi{-npKh`p8Ng7IV8rj0olIe{?TG0`jeo<;s$U!tCu!I6$ zhrfJ71m2nY52l}c7gWm$q<>n-K*7-dN(KT0a%LvRB`Q37&Im{m;)MwU0yrfDet>cE z_s)j1B%(RDxnb5&>W? zN&-Y_0?J(ck7yjA(6Oz7u8!?bvl#Fl2j4eJr~t%X0bct7lK&$%2pH7=5z{r$u>h8{ zrVnDI1C0D(=>KFvd2XpV{(t3udT9P9*Eletre!!t4`AC;2N>>u)?t1o6a&m%f8AM2 z$IkNYKZ|n!Q}mA!5Mp2e*;oKt`zJ+^;ui(ryf^qWMG;&`3J0KtbHMd8ztD4AkQsha z^Z;Hwz>E05DjzAbDDobl4JLr*0THtQBd^NzxBOrB$bS`wE3sp00_2k61*q<4bnWx) z{WzY*ZU4RFF|uSE;rc&~PBQ&l*hI(N+2uD%6GR&&VFL_1H^3nH=iMu8egO>h zt@Qp?W%(8rkeq;~#{>NB|Lk`Rhrfkw&Gr7=?`E~da^Zl{GY%L%KOOhaivR5QKNM$u zd-Feo0wig>ZU6(o4FLO@6zCZw9ricqKQ`LmXahlp2do01M;O4`1$eIiA-G2WO>nR@ z`S+g8=NkdT1L(0C(1xE?lb-Ldn*U4M(8S!}5A!i|@ryVG03i=R0M@PlK@GZ1Tj$yyo!YW7&@ z{69KlA=#P$ur3V)X2m~evwq7jkd?jlUmoOtTibvu#6K&uy8D;>|E(7fJU@Voq&-uv zrk^SQESUz(04|30%#fS=oAJ-mSilJ2+DFfbyoG1PzgI#6W&xL!cxEMk`^EZafeBy& za1nrK0`=zq5dJO+04xuj(eYV6?etmxpZOhu8NkWpo*BlMzZn0US`JtqI04wR{Q31S z`CsoC|Gr%Se_{Phn1%-Z?^JvK-WvG+z~5p#OYfrolK%V43}9j4IJalvK|D}kXWnzv z8!!eq;^`R!P565Z;7u491RTxt46-NsJ?NKufoZ_8IL|Z);{QW?j&k{LWDYP5I7;T3 zR{tm3?-~aj+478&Bl&&fe#N-}Hx4+OG~YMBq5U_e0+}NfTiA{+%29@6!S>4%iR=j63@S?%692 z%ma3AKl3apf73i5S2r*b*n#{^RHynA@!6dWOaylQJrmDq{zUxc2n6N<`y8HmLvR1Y z`= Date: Fri, 17 May 2013 16:41:15 +0200 Subject: [PATCH 010/361] Added a few tests of with with variables/attributes. --- tests/src/erlydtl_unittests.erl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 8b36727..c18a44e 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -1085,6 +1085,10 @@ tests() -> <<"{% with a=1 %}{{ a }}{% endwith %}">>, [], <<"1">>}, {"Cache variable", <<"{% with a=b %}{{ a }}{% endwith %}">>, [{b, "foo"}], <<"foo">>}, + {"Cache variable with attribute", + <<"I enjoy {% with a = var1 %}{{ a.game }}{% endwith %}">>, [{var1, [{game, "Othello"}]}], <<"I enjoy Othello">>}, + {"Cache variable attribute", + <<"I enjoy {% with a = var1.game %}{{ a }}{% endwith %}">>, [{var1, [{game, "Othello"}]}], <<"I enjoy Othello">>}, {"Cache multiple", <<"{% with alpha=1 beta=b %}{{ alpha }}/{{ beta }}{% endwith %}">>, [{b, 2}], <<"1/2">>} ]}, From 4d0dc8fb75494d6b3793a9fb1b37f99a48e19729 Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Tue, 28 May 2013 09:19:07 -0500 Subject: [PATCH 011/361] Remove pmod_pt dependency --- Emakefile | 2 +- Makefile | 2 +- rebar.config | 4 ---- tests/src/erlydtl_example_variable_storage.erl | 9 +++++++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Emakefile b/Emakefile index dd12c76..cceeb9b 100755 --- a/Emakefile +++ b/Emakefile @@ -1 +1 @@ -{"tests/src/*", [debug_info, {outdir, "ebintest"}, {parse_transform, pmod_pt}]}. +{"tests/src/*", [debug_info, {outdir, "ebintest"}]}. diff --git a/Makefile b/Makefile index 9f1c008..657228a 100755 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ compile: compile_test: -mkdir -p ebintest - $(ERL) -pa deps/pmod_transform/ebin/ -make + $(ERL) -make test: compile compile_test $(ERL) -noshell -pa ebin -pa ebintest \ diff --git a/rebar.config b/rebar.config index d23d747..0f5d40e 100644 --- a/rebar.config +++ b/rebar.config @@ -1,5 +1 @@ {erl_opts, [debug_info]}. - -{deps,[ - {pmod_transform, ".*", {git, "git://github.com/erlang/pmod_transform.git", "HEAD"}} - ]}. diff --git a/tests/src/erlydtl_example_variable_storage.erl b/tests/src/erlydtl_example_variable_storage.erl index c9ab246..44ee65f 100644 --- a/tests/src/erlydtl_example_variable_storage.erl +++ b/tests/src/erlydtl_example_variable_storage.erl @@ -1,5 +1,10 @@ --module(erlydtl_example_variable_storage, [SomeVar]). +-module(erlydtl_example_variable_storage). -compile(export_all). -some_var() -> +% fake pmod + +new(SomeVar) -> + {?MODULE, SomeVar}. + +some_var({?MODULE, SomeVar}) -> SomeVar. From 5d19807675196a5a6d078cb6ca489b114522b906 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 13 Jun 2013 23:07:10 +0200 Subject: [PATCH 012/361] Let emacs have its way with the indentation... blargh. --- src/erlydtl_compiler.erl | 1184 +++++++++++++++++++------------------- 1 file changed, 592 insertions(+), 592 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index bef0c40..50d59f0 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -41,38 +41,38 @@ -export([compile/2, compile/3, compile_dir/2, compile_dir/3, parse/1]). -record(dtl_context, { - local_scopes = [], - block_dict = dict:new(), - blocktrans_fun = none, - blocktrans_locales = [], - auto_escape = off, - doc_root = "", - parse_trail = [], - vars = [], - filter_modules = [], - custom_tags_dir = [], - custom_tags_modules = [], - reader = {file, read_file}, - module = [], - compiler_options = [verbose, report_errors], - binary_strings = true, - force_recompile = false, - locale = none, - verbose = false, - is_compiling_dir = false}). + local_scopes = [], + block_dict = dict:new(), + blocktrans_fun = none, + blocktrans_locales = [], + auto_escape = off, + doc_root = "", + parse_trail = [], + vars = [], + filter_modules = [], + custom_tags_dir = [], + custom_tags_modules = [], + reader = {file, read_file}, + module = [], + compiler_options = [verbose, report_errors], + binary_strings = true, + force_recompile = false, + locale = none, + verbose = false, + is_compiling_dir = false}). -record(ast_info, { - dependencies = [], - translatable_strings = [], - translated_blocks= [], - custom_tags = [], - var_names = [], - pre_render_asts = []}). - + dependencies = [], + translatable_strings = [], + translated_blocks= [], + custom_tags = [], + var_names = [], + pre_render_asts = []}). + -record(treewalker, { - counter = 0, - safe = false -}). + counter = 0, + safe = false + }). compile(Binary, Module) when is_binary(Binary) -> compile(Binary, Module, []); @@ -86,7 +86,7 @@ compile(Binary, Module, Options) when is_binary(Binary) -> case parse(Binary) of {ok, DjangoParseTree} -> case compile_to_binary(File, DjangoParseTree, - init_dtl_context(File, Module, Options), CheckSum) of + init_dtl_context(File, Module, Options), CheckSum) of {ok, Module1, _, _} -> {ok, Module1}; Err -> @@ -95,7 +95,7 @@ compile(Binary, Module, Options) when is_binary(Binary) -> Err -> Err end; - + compile(File, Module, Options) -> Context = init_dtl_context(File, Module, Options), case parse(File, Context) of @@ -111,7 +111,7 @@ compile(File, Module, Options) -> Err -> Err end. - + compile_dir(Dir, Module) -> compile_dir(Dir, Module, []). @@ -122,25 +122,25 @@ compile_dir(Dir, Module, Options) -> %% files ending in "~"). Files = filelib:fold_files(Dir, ".+[^~]$", true, fun(F1,Acc1) -> [F1 | Acc1] end, []), {ParserResults, ParserErrors} = lists:foldl(fun - (File, {ResultAcc, ErrorAcc}) -> - case filename:basename(File) of - "."++_ -> - {ResultAcc, ErrorAcc}; - _ -> - FilePath = filename:absname(File), - case filelib:is_dir(FilePath) of - true -> - {ResultAcc, ErrorAcc}; - false -> - case parse(FilePath, Context) of - ok -> {ResultAcc, ErrorAcc}; - {ok, DjangoParseTree, CheckSum} -> - {[{File, DjangoParseTree, CheckSum}|ResultAcc], ErrorAcc}; - Err -> {ResultAcc, [Err|ErrorAcc]} - end - end - end - end, {[], []}, Files), + (File, {ResultAcc, ErrorAcc}) -> + case filename:basename(File) of + "."++_ -> + {ResultAcc, ErrorAcc}; + _ -> + FilePath = filename:absname(File), + case filelib:is_dir(FilePath) of + true -> + {ResultAcc, ErrorAcc}; + false -> + case parse(FilePath, Context) of + ok -> {ResultAcc, ErrorAcc}; + {ok, DjangoParseTree, CheckSum} -> + {[{File, DjangoParseTree, CheckSum}|ResultAcc], ErrorAcc}; + Err -> {ResultAcc, [Err|ErrorAcc]} + end + end + end + end, {[], []}, Files), case ParserErrors of [] -> case compile_multiple_to_binary(Dir, ParserResults, Context) of @@ -167,37 +167,37 @@ write_binary(Module1, Bin, Options, Warnings) -> BeamFile = filename:join([OutDir, atom_to_list(Module1) ++ ".beam"]), print(Verbose, "Template module: ~w -> ~s~s\n", - [Module1, BeamFile, - case Warnings of - [] -> ""; - _ -> io_lib:format("\n Warnings: ~p", [Warnings]) - end]), + [Module1, BeamFile, + case Warnings of + [] -> ""; + _ -> io_lib:format("\n Warnings: ~p", [Warnings]) + end]), case file:write_file(BeamFile, Bin) of ok -> ok; {error, Reason} -> {error, lists:flatten( - io_lib:format("Beam generation of '~s' failed: ~p", - [BeamFile, file:format_error(Reason)]))} + io_lib:format("Beam generation of '~s' failed: ~p", + [BeamFile, file:format_error(Reason)]))} end end. compile_multiple_to_binary(Dir, ParserResults, Context) -> MatchAst = options_match_ast(), {Functions, {AstInfo, _}} = lists:mapfoldl(fun({File, DjangoParseTree, CheckSum}, {AstInfo, TreeWalker}) -> - FilePath = full_path(File, Context#dtl_context.doc_root), - {{BodyAst, BodyInfo}, TreeWalker1} = with_dependency({FilePath, CheckSum}, body_ast(DjangoParseTree, Context, TreeWalker)), - FunctionName = filename:rootname(filename:basename(File)), - Function1 = erl_syntax:function(erl_syntax:atom(FunctionName), - [erl_syntax:clause([erl_syntax:variable("_Variables")], none, - [erl_syntax:application(none, erl_syntax:atom(FunctionName), - [erl_syntax:variable("_Variables"), erl_syntax:list([])])])]), - Function2 = erl_syntax:function(erl_syntax:atom(FunctionName), - [erl_syntax:clause([erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], none, - MatchAst ++ [BodyAst])]), - {{FunctionName, Function1, Function2}, {merge_info(AstInfo, BodyInfo), TreeWalker1}} - end, {#ast_info{}, #treewalker{}}, ParserResults), + FilePath = full_path(File, Context#dtl_context.doc_root), + {{BodyAst, BodyInfo}, TreeWalker1} = with_dependency({FilePath, CheckSum}, body_ast(DjangoParseTree, Context, TreeWalker)), + FunctionName = filename:rootname(filename:basename(File)), + Function1 = erl_syntax:function(erl_syntax:atom(FunctionName), + [erl_syntax:clause([erl_syntax:variable("_Variables")], none, + [erl_syntax:application(none, erl_syntax:atom(FunctionName), + [erl_syntax:variable("_Variables"), erl_syntax:list([])])])]), + Function2 = erl_syntax:function(erl_syntax:atom(FunctionName), + [erl_syntax:clause([erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], none, + MatchAst ++ [BodyAst])]), + {{FunctionName, Function1, Function2}, {merge_info(AstInfo, BodyInfo), TreeWalker1}} + end, {#ast_info{}, #treewalker{}}, ParserResults), Forms = custom_forms(Dir, Context#dtl_context.module, Functions, AstInfo), compile_forms_and_reload(Dir, Forms, Context#dtl_context.compiler_options). @@ -207,7 +207,7 @@ compile_to_binary(File, DjangoParseTree, Context, CheckSum) -> try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of {{CustomTagsAst, CustomTagsInfo}, _} -> Forms = forms(File, Context#dtl_context.module, {BodyAst, BodyInfo}, - {CustomTagsAst, CustomTagsInfo}, Context#dtl_context.binary_strings, CheckSum), + {CustomTagsAst, CustomTagsInfo}, Context#dtl_context.binary_strings, CheckSum), compile_forms_and_reload(File, Forms, Context#dtl_context.compiler_options) catch throw:Error -> Error @@ -227,7 +227,7 @@ compile_forms_and_reload(File, Forms, CompilerOptions) -> OtherError -> OtherError end. - + load_code(Module, Bin, Warnings) -> code:purge(Module), case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Bin) of @@ -238,22 +238,22 @@ load_code(Module, Bin, Warnings) -> init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) -> Ctx = #dtl_context{}, #dtl_context{ - parse_trail = ParseTrail, - module = Module, - doc_root = proplists:get_value(doc_root, Options, DefDir), - filter_modules = proplists:get_value(custom_filters_modules, Options, Ctx#dtl_context.filter_modules) ++ [erlydtl_filters], - custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])), - custom_tags_modules = proplists:get_value(custom_tags_modules, Options, Ctx#dtl_context.custom_tags_modules), - 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), - binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings), - force_recompile = proplists:get_value(force_recompile, Options, Ctx#dtl_context.force_recompile), - locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale), - verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose), - is_compiling_dir = IsCompilingDir}. + parse_trail = ParseTrail, + module = Module, + doc_root = proplists:get_value(doc_root, Options, DefDir), + filter_modules = proplists:get_value(custom_filters_modules, Options, Ctx#dtl_context.filter_modules) ++ [erlydtl_filters], + custom_tags_dir = proplists:get_value(custom_tags_dir, Options, filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])), + custom_tags_modules = proplists:get_value(custom_tags_modules, Options, Ctx#dtl_context.custom_tags_modules), + 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), + binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings), + force_recompile = proplists:get_value(force_recompile, Options, Ctx#dtl_context.force_recompile), + locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale), + verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose), + is_compiling_dir = IsCompilingDir}. init_dtl_context(File, Module, Options) when is_list(Module) -> init_dtl_context(File, list_to_atom(Module), Options); @@ -275,19 +275,19 @@ is_up_to_date(CheckSum, Context) -> case catch Module:dependencies() of L when is_list(L) -> RecompileList = lists:foldl(fun - ({XFile, XCheckSum}, Acc) -> - case catch M:F(XFile) of - {ok, Data} -> - case binary_to_list(erlang:md5(Data)) of - XCheckSum -> - Acc; - _ -> - [recompile | Acc] - end; - _ -> - [recompile | Acc] - end - end, [], L), + ({XFile, XCheckSum}, Acc) -> + case catch M:F(XFile) of + {ok, Data} -> + case binary_to_list(erlang:md5(Data)) of + XCheckSum -> + Acc; + _ -> + [recompile | Acc] + end; + _ -> + [recompile | Acc] + end + end, [], L), case RecompileList of [] -> true; _ -> false @@ -298,8 +298,8 @@ is_up_to_date(CheckSum, Context) -> _ -> false end. - - + + parse(File, Context) -> {M, F} = Context#dtl_context.reader, case catch M:F(File) of @@ -316,7 +316,7 @@ parse(File, Context) -> _ -> {error, {File, [{0, Context#dtl_context.module, "Failed to read file"}]}} end. - + parse(CheckSum, Data, Context) -> case is_up_to_date(CheckSum, Context) of true -> @@ -347,8 +347,8 @@ custom_tags_clauses_ast(CustomTags, Context, TreeWalker) -> custom_tags_clauses_ast1([], _ExcludeTags, ClauseAcc, InfoAcc, _Context, TreeWalker) -> {{lists:reverse([erl_syntax:clause([erl_syntax:variable("TagName"), erl_syntax:underscore(), erl_syntax:underscore()], none, - [erl_syntax:list([])])|ClauseAcc - ]), InfoAcc}, TreeWalker}; + [erl_syntax:list([])])|ClauseAcc + ]), InfoAcc}, TreeWalker}; custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker) -> case lists:member(Tag, ExcludeTags) of true -> @@ -360,101 +360,101 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Cont case parse(CustomTagFile, Context) of {ok, DjangoParseTree, CheckSum} -> {{BodyAst, BodyAstInfo}, TreeWalker1} = with_dependency( - {CustomTagFile, CheckSum}, body_ast(DjangoParseTree, Context, TreeWalker)), + {CustomTagFile, CheckSum}, body_ast(DjangoParseTree, Context, TreeWalker)), MatchAst = options_match_ast(), Clause = erl_syntax:clause( - [key_to_string(Tag), erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], - none, MatchAst ++ [BodyAst]), + [key_to_string(Tag), erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], + none, MatchAst ++ [BodyAst]), custom_tags_clauses_ast1(CustomTags, [Tag|ExcludeTags], - [Clause|ClauseAcc], merge_info(BodyAstInfo, InfoAcc), - Context, TreeWalker1); + [Clause|ClauseAcc], merge_info(BodyAstInfo, InfoAcc), + Context, TreeWalker1); Error -> throw(Error) end; false -> custom_tags_clauses_ast1(CustomTags, [Tag | ExcludeTags], - ClauseAcc, InfoAcc, Context, TreeWalker) + ClauseAcc, InfoAcc, Context, TreeWalker) end end. dependencies_function(Dependencies) -> erl_syntax:function( - erl_syntax:atom(dependencies), [erl_syntax:clause([], none, - [erl_syntax:list(lists:map(fun - ({XFile, XCheckSum}) -> - erl_syntax:tuple([erl_syntax:string(XFile), erl_syntax:string(XCheckSum)]) - end, Dependencies))])]). + erl_syntax:atom(dependencies), [erl_syntax:clause([], none, + [erl_syntax:list(lists:map(fun + ({XFile, XCheckSum}) -> + erl_syntax:tuple([erl_syntax:string(XFile), erl_syntax:string(XCheckSum)]) + end, Dependencies))])]). translatable_strings_function(TranslatableStrings) -> - erl_syntax:function( - erl_syntax:atom(translatable_strings), [erl_syntax:clause([], none, - [erl_syntax:list(lists:map(fun(String) -> - erl_syntax:string(String) - end, - TranslatableStrings))])]). + erl_syntax:function( + erl_syntax:atom(translatable_strings), [erl_syntax:clause([], none, + [erl_syntax:list(lists:map(fun(String) -> + erl_syntax:string(String) + end, + TranslatableStrings))])]). translated_blocks_function(TranslatedBlocks) -> - erl_syntax:function( - erl_syntax:atom(translated_blocks), [erl_syntax:clause([], none, - [erl_syntax:list(lists:map(fun(String) -> - erl_syntax:string(String) - end, - TranslatedBlocks))])]). + erl_syntax:function( + erl_syntax:atom(translated_blocks), [erl_syntax:clause([], none, + [erl_syntax:list(lists:map(fun(String) -> + erl_syntax:string(String) + end, + TranslatedBlocks))])]). variables_function(Variables) -> - erl_syntax:function( - erl_syntax:atom(variables), [erl_syntax:clause([], none, - [erl_syntax:list([erl_syntax:atom(S) || S <- lists:usort(Variables)])])]). + erl_syntax:function( + erl_syntax:atom(variables), [erl_syntax:clause([], none, + [erl_syntax:list([erl_syntax:atom(S) || S <- lists:usort(Variables)])])]). custom_forms(Dir, Module, Functions, AstInfo) -> 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(source_dir), erl_syntax:integer(0)), - erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)), - erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0)) - | - lists:foldl(fun({FunctionName, _, _}, Acc) -> - [erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(1)), - erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(2))|Acc] - end, [], Functions)] - )]), + [erl_syntax:list([ + erl_syntax:arity_qualifier(erl_syntax:atom(source_dir), erl_syntax:integer(0)), + erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)), + erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0)) + | + lists:foldl(fun({FunctionName, _, _}, Acc) -> + [erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(1)), + erl_syntax:arity_qualifier(erl_syntax:atom(FunctionName), erl_syntax:integer(2))|Acc] + end, [], Functions)] + )]), SourceFunctionAst = erl_syntax:function( - erl_syntax:atom(source_dir), [erl_syntax:clause([], none, [erl_syntax:string(Dir)])]), + erl_syntax:atom(source_dir), [erl_syntax:clause([], none, [erl_syntax:string(Dir)])]), DependenciesFunctionAst = dependencies_function(AstInfo#ast_info.dependencies), TranslatableStringsFunctionAst = translatable_strings_function(AstInfo#ast_info.translatable_strings), FunctionAsts = lists:foldl(fun({_, Function1, Function2}, Acc) -> [Function1, Function2 | Acc] end, [], Functions), [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsFunctionAst - | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts]. + | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts]. forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, BinaryStrings, CheckSum) -> MergedInfo = merge_info(BodyInfo, CustomTagsInfo), Render0FunctionAst = erl_syntax:function(erl_syntax:atom(render), - [erl_syntax:clause([], none, [erl_syntax:application(none, - erl_syntax:atom(render), [erl_syntax:list([])])])]), + [erl_syntax:clause([], none, [erl_syntax:application(none, + erl_syntax:atom(render), [erl_syntax:list([])])])]), Render1FunctionAst = erl_syntax:function(erl_syntax:atom(render), - [erl_syntax:clause([erl_syntax:variable("_Variables")], none, - [erl_syntax:application(none, - erl_syntax:atom(render), - [erl_syntax:variable("_Variables"), erl_syntax:list([])])])]), + [erl_syntax:clause([erl_syntax:variable("_Variables")], none, + [erl_syntax:application(none, + erl_syntax:atom(render), + [erl_syntax:variable("_Variables"), erl_syntax:list([])])])]), Function2 = erl_syntax:application(none, erl_syntax:atom(render_internal), - [erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")]), + [erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")]), ClauseOk = erl_syntax:clause([erl_syntax:variable("Val")], none, - [erl_syntax:tuple([erl_syntax:atom(ok), erl_syntax:variable("Val")])]), + [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")])]), + [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("RenderOptions")], none, - [erl_syntax:try_expr([Function2], [ClauseOk], [ClauseCatch])])]), - + [erl_syntax:clause([erl_syntax:variable("_Variables"), + erl_syntax:variable("RenderOptions")], none, + [erl_syntax:try_expr([Function2], [ClauseOk], [ClauseCatch])])]), + SourceFunctionTuple = erl_syntax:tuple( - [erl_syntax:string(File), erl_syntax:string(CheckSum)]), + [erl_syntax:string(File), erl_syntax:string(CheckSum)]), SourceFunctionAst = erl_syntax:function( - erl_syntax:atom(source), - [erl_syntax:clause([], none, [SourceFunctionTuple])]), - + erl_syntax:atom(source), + [erl_syntax:clause([], none, [SourceFunctionTuple])]), + DependenciesFunctionAst = dependencies_function(MergedInfo#ast_info.dependencies), TranslatableStringsAst = translatable_strings_function(MergedInfo#ast_info.translatable_strings), @@ -464,57 +464,57 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo} VariablesAst = variables_function(MergedInfo#ast_info.var_names), MatchAst = options_match_ast(), - + BodyAstTmp = MatchAst ++ [ - erl_syntax:application( - erl_syntax:atom(erlydtl_runtime), - erl_syntax:atom(stringify_final), - [BodyAst, erl_syntax:atom(BinaryStrings)]) - ], + erl_syntax:application( + erl_syntax:atom(erlydtl_runtime), + erl_syntax:atom(stringify_final), + [BodyAst, erl_syntax:atom(BinaryStrings)]) + ], RenderInternalFunctionAst = erl_syntax:function( - erl_syntax:atom(render_internal), - [erl_syntax:clause([ - erl_syntax:variable("_Variables"), - erl_syntax:variable("RenderOptions")], - none, BodyAstTmp)] - ), - + erl_syntax:atom(render_internal), + [erl_syntax:clause([ + erl_syntax:variable("_Variables"), + erl_syntax:variable("RenderOptions")], + none, BodyAstTmp)] + ), + 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(render), erl_syntax:integer(0)), - erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(1)), - erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(2)), - erl_syntax:arity_qualifier(erl_syntax:atom(source), erl_syntax:integer(0)), - erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)), - erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0)), - erl_syntax:arity_qualifier(erl_syntax:atom(translated_blocks), erl_syntax:integer(0)), - erl_syntax:arity_qualifier(erl_syntax:atom(variables), erl_syntax:integer(0)) - ])]), - + [erl_syntax:list([erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(0)), + erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(1)), + erl_syntax:arity_qualifier(erl_syntax:atom(render), erl_syntax:integer(2)), + erl_syntax:arity_qualifier(erl_syntax:atom(source), erl_syntax:integer(0)), + erl_syntax:arity_qualifier(erl_syntax:atom(dependencies), erl_syntax:integer(0)), + erl_syntax:arity_qualifier(erl_syntax:atom(translatable_strings), erl_syntax:integer(0)), + erl_syntax:arity_qualifier(erl_syntax:atom(translated_blocks), erl_syntax:integer(0)), + erl_syntax:arity_qualifier(erl_syntax:atom(variables), erl_syntax:integer(0)) + ])]), + [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, Render0FunctionAst, Render1FunctionAst, Render2FunctionAst, - SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst, - TranslatedBlocksAst, VariablesAst, RenderInternalFunctionAst, - CustomTagsFunctionAst | BodyInfo#ast_info.pre_render_asts]]. + SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst, + TranslatedBlocksAst, VariablesAst, RenderInternalFunctionAst, + CustomTagsFunctionAst | BodyInfo#ast_info.pre_render_asts]]. options_match_ast() -> [ - erl_syntax:match_expr( - erl_syntax:variable("_TranslationFun"), - erl_syntax:application( - erl_syntax:atom(proplists), - erl_syntax:atom(get_value), - [erl_syntax:atom(translation_fun), erl_syntax:variable("RenderOptions"), erl_syntax:atom(none)])), - erl_syntax:match_expr( - erl_syntax:variable("_CurrentLocale"), - erl_syntax:application( - erl_syntax:atom(proplists), - erl_syntax:atom(get_value), - [erl_syntax:atom(locale), erl_syntax:variable("RenderOptions"), erl_syntax:atom(none)])) + erl_syntax:match_expr( + erl_syntax:variable("_TranslationFun"), + erl_syntax:application( + erl_syntax:atom(proplists), + erl_syntax:atom(get_value), + [erl_syntax:atom(translation_fun), erl_syntax:variable("RenderOptions"), erl_syntax:atom(none)])), + erl_syntax:match_expr( + erl_syntax:variable("_CurrentLocale"), + erl_syntax:application( + erl_syntax:atom(proplists), + erl_syntax:atom(get_value), + [erl_syntax:atom(locale), erl_syntax:variable("RenderOptions"), erl_syntax:atom(none)])) ]. - -% child templates should only consist of blocks at the top level + + % child templates should only consist of blocks at the top level body_ast([{'extends', {string_literal, _Pos, String}} | ThisParseTree], Context, TreeWalker) -> File = full_path(unescape_string_literal(String), Context#dtl_context.doc_root), case lists:member(File, Context#dtl_context.parse_trail) of @@ -524,156 +524,156 @@ body_ast([{'extends', {string_literal, _Pos, String}} | ThisParseTree], Context, case parse(File, Context) of {ok, ParentParseTree, CheckSum} -> BlockDict = lists:foldl( - fun - ({block, {identifier, _, Name}, Contents}, Dict) -> - dict:store(Name, Contents, Dict); - (_, Dict) -> - Dict - end, dict:new(), ThisParseTree), + fun + ({block, {identifier, _, Name}, Contents}, Dict) -> + dict:store(Name, Contents, Dict); + (_, Dict) -> + Dict + end, dict:new(), ThisParseTree), with_dependency({File, CheckSum}, body_ast(ParentParseTree, Context#dtl_context{ - block_dict = dict:merge(fun(_Key, _ParentVal, ChildVal) -> ChildVal end, - BlockDict, Context#dtl_context.block_dict), - parse_trail = [File | Context#dtl_context.parse_trail]}, TreeWalker)); + block_dict = dict:merge(fun(_Key, _ParentVal, ChildVal) -> ChildVal end, + BlockDict, Context#dtl_context.block_dict), + parse_trail = [File | Context#dtl_context.parse_trail]}, TreeWalker)); Err -> throw(Err) end end; - - + + 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 - end, - body_ast(Block, Context, TreeWalkerAcc); - ({'blocktrans', Args, Contents}, TreeWalkerAcc) -> - blocktrans_ast(Args, Contents, 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); - ({'filter', FilterList, Contents}, TreeWalkerAcc) -> - filter_tag_ast(FilterList, Contents, Context, TreeWalkerAcc); - ({'firstof', Vars}, TreeWalkerAcc) -> - firstof_ast(Vars, Context, TreeWalkerAcc); - ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TreeWalkerAcc) -> - {EmptyAstInfo, TreeWalker1} = empty_ast(TreeWalkerAcc), - for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, Context, TreeWalker1); - ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TreeWalkerAcc) -> - {EmptyAstInfo, TreeWalker1} = body_ast(EmptyPartContents, Context, TreeWalkerAcc), - for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, Context, TreeWalker1); - ({'if', Expression, Contents, Elif}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), - {ElifAstInfo, TreeWalker2} = body_ast(Elif, Context, TreeWalker1), - ifelse_ast(Expression, IfAstInfo, ElifAstInfo, Context, TreeWalker2); - ({'if', Expression, Contents}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), - {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1), - ifelse_ast(Expression, IfAstInfo, ElseAstInfo, Context, TreeWalker2); - ({'ifchanged', '$undefined', Contents}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), - {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1), - ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, Context, TreeWalker2); - ({'ifchanged', Values, Contents}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), - {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1), - ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, Context, TreeWalker2); - ({'ifchangedelse', '$undefined', IfContents, ElseContents}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), - {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1), - ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, Context, TreeWalker2); - ({'ifchangedelse', Values, IfContents, ElseContents}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), - {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1), - ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, Context, TreeWalker2); - ({'ifelse', Expression, IfContents, ElseContents}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), - {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1), - ifelse_ast(Expression, IfAstInfo, ElseAstInfo, Context, TreeWalker2); - ({'ifequal', [Arg1, Arg2], Contents}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), - {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1), - ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2); - ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), - {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context,TreeWalker1), - ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2); - ({'ifnotequal', [Arg1, Arg2], Contents}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), - {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1), - ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2); - ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TreeWalkerAcc) -> - {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), - {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1), - ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2); - ({'include', {string_literal, _, File}, Args}, TreeWalkerAcc) -> - include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, Context, TreeWalkerAcc); - ({'include_only', {string_literal, _, File}, Args}, TreeWalkerAcc) -> - include_ast(unescape_string_literal(File), Args, [], Context, TreeWalkerAcc); - ({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}, Contents}, TreeWalkerAcc) -> - regroup_ast(ListVariable, Grouper, NewVariable, Contents, Context, TreeWalkerAcc); - ({'spaceless', Contents}, TreeWalkerAcc) -> - spaceless_ast(Contents, Context, TreeWalkerAcc); - ({'ssi', Arg}, TreeWalkerAcc) -> - ssi_ast(Arg, Context, TreeWalkerAcc); - ({'ssi_parsed', {string_literal, _, FileName}}, TreeWalkerAcc) -> - include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, Context, TreeWalkerAcc); - ({'string', _Pos, String}, TreeWalkerAcc) -> - string_ast(String, Context, TreeWalkerAcc); - ({'tag', {identifier, _, Name}, Args}, TreeWalkerAcc) -> - tag_ast(Name, Args, 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, true, Context, TreeWalkerAcc), - {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker} - end, TreeWalker, DjangoParseTree), + 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 + end, + body_ast(Block, Context, TreeWalkerAcc); + ({'blocktrans', Args, Contents}, TreeWalkerAcc) -> + blocktrans_ast(Args, Contents, 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); + ({'filter', FilterList, Contents}, TreeWalkerAcc) -> + filter_tag_ast(FilterList, Contents, Context, TreeWalkerAcc); + ({'firstof', Vars}, TreeWalkerAcc) -> + firstof_ast(Vars, Context, TreeWalkerAcc); + ({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TreeWalkerAcc) -> + {EmptyAstInfo, TreeWalker1} = empty_ast(TreeWalkerAcc), + for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, Context, TreeWalker1); + ({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TreeWalkerAcc) -> + {EmptyAstInfo, TreeWalker1} = body_ast(EmptyPartContents, Context, TreeWalkerAcc), + for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, Context, TreeWalker1); + ({'if', Expression, Contents, Elif}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), + {ElifAstInfo, TreeWalker2} = body_ast(Elif, Context, TreeWalker1), + ifelse_ast(Expression, IfAstInfo, ElifAstInfo, Context, TreeWalker2); + ({'if', Expression, Contents}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), + {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1), + ifelse_ast(Expression, IfAstInfo, ElseAstInfo, Context, TreeWalker2); + ({'ifchanged', '$undefined', Contents}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), + {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1), + ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, Context, TreeWalker2); + ({'ifchanged', Values, Contents}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), + {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1), + ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, Context, TreeWalker2); + ({'ifchangedelse', '$undefined', IfContents, ElseContents}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), + {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1), + ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, Context, TreeWalker2); + ({'ifchangedelse', Values, IfContents, ElseContents}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), + {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1), + ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, Context, TreeWalker2); + ({'ifelse', Expression, IfContents, ElseContents}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), + {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1), + ifelse_ast(Expression, IfAstInfo, ElseAstInfo, Context, TreeWalker2); + ({'ifequal', [Arg1, Arg2], Contents}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), + {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1), + ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2); + ({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), + {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context,TreeWalker1), + ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2); + ({'ifnotequal', [Arg1, Arg2], Contents}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(Contents, Context, TreeWalkerAcc), + {ElseAstInfo, TreeWalker2} = empty_ast(TreeWalker1), + ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2); + ({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TreeWalkerAcc) -> + {IfAstInfo, TreeWalker1} = body_ast(IfContents, Context, TreeWalkerAcc), + {ElseAstInfo, TreeWalker2} = body_ast(ElseContents, Context, TreeWalker1), + ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, Context, TreeWalker2); + ({'include', {string_literal, _, File}, Args}, TreeWalkerAcc) -> + include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, Context, TreeWalkerAcc); + ({'include_only', {string_literal, _, File}, Args}, TreeWalkerAcc) -> + include_ast(unescape_string_literal(File), Args, [], Context, TreeWalkerAcc); + ({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}, Contents}, TreeWalkerAcc) -> + regroup_ast(ListVariable, Grouper, NewVariable, Contents, Context, TreeWalkerAcc); + ({'spaceless', Contents}, TreeWalkerAcc) -> + spaceless_ast(Contents, Context, TreeWalkerAcc); + ({'ssi', Arg}, TreeWalkerAcc) -> + ssi_ast(Arg, Context, TreeWalkerAcc); + ({'ssi_parsed', {string_literal, _, FileName}}, TreeWalkerAcc) -> + include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, Context, TreeWalkerAcc); + ({'string', _Pos, String}, TreeWalkerAcc) -> + string_ast(String, Context, TreeWalkerAcc); + ({'tag', {identifier, _, Name}, Args}, TreeWalkerAcc) -> + tag_ast(Name, Args, 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, true, Context, TreeWalkerAcc), + {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker} + end, TreeWalker, DjangoParseTree), {AstList, {Info, TreeWalker3}} = lists:mapfoldl( - fun({Ast, Info}, {InfoAcc, TreeWalkerAcc}) -> - PresetVars = lists:foldl(fun - (X, Acc) -> - case proplists:lookup(X, Context#dtl_context.vars) of - none -> - Acc; - Val -> - [erl_syntax:abstract(Val) | Acc] - end - end, [], Info#ast_info.var_names), - case PresetVars of - [] -> - {Ast, {merge_info(Info, InfoAcc), TreeWalkerAcc}}; - _ -> - Counter = TreeWalkerAcc#treewalker.counter, - Name = lists:concat([pre_render, Counter]), - Ast1 = erl_syntax:application(none, erl_syntax:atom(Name), - [erl_syntax:list(PresetVars)]), - PreRenderAst = erl_syntax:function(erl_syntax:atom(Name), - [erl_syntax:clause([erl_syntax:variable("_Variables")], none, [Ast])]), - PreRenderAsts = Info#ast_info.pre_render_asts, - Info1 = Info#ast_info{pre_render_asts = [PreRenderAst | PreRenderAsts]}, - {Ast1, {merge_info(Info1, InfoAcc), TreeWalkerAcc#treewalker{counter = Counter + 1}}} - end - end, {#ast_info{}, TreeWalker2}, AstInfoList), + fun({Ast, Info}, {InfoAcc, TreeWalkerAcc}) -> + PresetVars = lists:foldl(fun + (X, Acc) -> + case proplists:lookup(X, Context#dtl_context.vars) of + none -> + Acc; + Val -> + [erl_syntax:abstract(Val) | Acc] + end + end, [], Info#ast_info.var_names), + case PresetVars of + [] -> + {Ast, {merge_info(Info, InfoAcc), TreeWalkerAcc}}; + _ -> + Counter = TreeWalkerAcc#treewalker.counter, + Name = lists:concat([pre_render, Counter]), + Ast1 = erl_syntax:application(none, erl_syntax:atom(Name), + [erl_syntax:list(PresetVars)]), + PreRenderAst = erl_syntax:function(erl_syntax:atom(Name), + [erl_syntax:clause([erl_syntax:variable("_Variables")], none, [Ast])]), + PreRenderAsts = Info#ast_info.pre_render_asts, + Info1 = Info#ast_info{pre_render_asts = [PreRenderAst | PreRenderAsts]}, + {Ast1, {merge_info(Info1, InfoAcc), TreeWalkerAcc#treewalker{counter = Counter + 1}}} + end + end, {#ast_info{}, TreeWalker2}, AstInfoList), {{erl_syntax:list(AstList), Info}, TreeWalker3}. @@ -711,36 +711,36 @@ value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) -> merge_info(Info1, Info2) -> #ast_info{ - dependencies = - lists:merge( - lists:sort(Info1#ast_info.dependencies), - lists:sort(Info2#ast_info.dependencies)), - var_names = - lists:merge( - lists:sort(Info1#ast_info.var_names), - lists:sort(Info2#ast_info.var_names)), - translatable_strings = - lists:merge( - lists:sort(Info1#ast_info.translatable_strings), - lists:sort(Info2#ast_info.translatable_strings)), - translated_blocks = - lists:merge( - lists:sort(Info1#ast_info.translated_blocks), - lists:sort(Info2#ast_info.translated_blocks)), - custom_tags = - lists:merge( - lists:sort(Info1#ast_info.custom_tags), - lists:sort(Info2#ast_info.custom_tags)), - pre_render_asts = - lists:merge( - Info1#ast_info.pre_render_asts, - Info2#ast_info.pre_render_asts)}. + dependencies = + lists:merge( + lists:sort(Info1#ast_info.dependencies), + lists:sort(Info2#ast_info.dependencies)), + var_names = + lists:merge( + lists:sort(Info1#ast_info.var_names), + lists:sort(Info2#ast_info.var_names)), + translatable_strings = + lists:merge( + lists:sort(Info1#ast_info.translatable_strings), + lists:sort(Info2#ast_info.translatable_strings)), + translated_blocks = + lists:merge( + lists:sort(Info1#ast_info.translated_blocks), + lists:sort(Info2#ast_info.translated_blocks)), + custom_tags = + lists:merge( + lists:sort(Info1#ast_info.custom_tags), + lists:sort(Info2#ast_info.custom_tags)), + pre_render_asts = + lists:merge( + Info1#ast_info.pre_render_asts, + Info2#ast_info.pre_render_asts)}. with_dependencies([], Args) -> Args; with_dependencies([Dependency | Rest], Args) -> - with_dependencies(Rest, with_dependency(Dependency, Args)). + with_dependencies(Rest, with_dependency(Dependency, Args)). with_dependency(FilePath, {{Ast, Info}, TreeWalker}) -> {{Ast, Info#ast_info{dependencies = [FilePath | Info#ast_info.dependencies]}}, TreeWalker}. @@ -751,10 +751,10 @@ empty_ast(TreeWalker) -> blocktrans_ast(ArgList, Contents, Context, TreeWalker) -> {NewScope, {ArgInfo, TreeWalker1}} = lists:mapfoldl(fun - ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) -> - {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1), - {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}} - end, {#ast_info{}, TreeWalker}, ArgList), + ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) -> + {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1), + {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}} + end, {#ast_info{}, TreeWalker}, ArgList), NewContext = Context#dtl_context{ local_scopes = [NewScope|Context#dtl_context.local_scopes] }, SourceText = lists:flatten(erlydtl_unparser:unparse(Contents)), {{DefaultAst, AstInfo}, TreeWalker2} = body_ast(Contents, NewContext, TreeWalker1), @@ -764,41 +764,41 @@ blocktrans_ast(ArgList, Contents, Context, TreeWalker) -> {{DefaultAst, MergedInfo}, TreeWalker2}; BlockTransFun when is_function(BlockTransFun) -> {FinalAstInfo, FinalTreeWalker, Clauses} = lists:foldr(fun(Locale, {AstInfoAcc, ThisTreeWalker, ClauseAcc}) -> - case BlockTransFun(SourceText, Locale) of - default -> - {AstInfoAcc, ThisTreeWalker, ClauseAcc}; - Body -> - {ok, DjangoParseTree} = parse(Body), - {{ThisAst, ThisAstInfo}, TreeWalker3} = body_ast(DjangoParseTree, NewContext, ThisTreeWalker), - {merge_info(ThisAstInfo, AstInfoAcc), TreeWalker3, - [erl_syntax:clause([erl_syntax:string(Locale)], none, [ThisAst])|ClauseAcc]} - end - end, {MergedInfo, TreeWalker2, []}, Context#dtl_context.blocktrans_locales), + case BlockTransFun(SourceText, Locale) of + default -> + {AstInfoAcc, ThisTreeWalker, ClauseAcc}; + Body -> + {ok, DjangoParseTree} = parse(Body), + {{ThisAst, ThisAstInfo}, TreeWalker3} = body_ast(DjangoParseTree, NewContext, ThisTreeWalker), + {merge_info(ThisAstInfo, AstInfoAcc), TreeWalker3, + [erl_syntax:clause([erl_syntax:string(Locale)], none, [ThisAst])|ClauseAcc]} + end + end, {MergedInfo, TreeWalker2, []}, Context#dtl_context.blocktrans_locales), Ast = erl_syntax:case_expr(erl_syntax:variable("_CurrentLocale"), - Clauses ++ [erl_syntax:clause([erl_syntax:underscore()], none, [DefaultAst])]), + Clauses ++ [erl_syntax:clause([erl_syntax:underscore()], none, [DefaultAst])]), {{Ast, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }}, FinalTreeWalker} end. translated_ast({string_literal, _, String}, Context, TreeWalker) -> NewStr = unescape_string_literal(String), DefaultString = case Context#dtl_context.locale of - none -> NewStr; - Locale -> erlydtl_i18n:translate(NewStr,Locale) - end, + none -> NewStr; + Locale -> erlydtl_i18n:translate(NewStr,Locale) + end, translated_ast2(erl_syntax:string(NewStr), erl_syntax:string(DefaultString), - #ast_info{translatable_strings = [NewStr]}, TreeWalker); + #ast_info{translatable_strings = [NewStr]}, TreeWalker); translated_ast(ValueToken, Context, TreeWalker) -> {{Ast, Info}, TreeWalker1} = value_ast(ValueToken, true, false, Context, TreeWalker), translated_ast2(Ast, Ast, Info, TreeWalker1). translated_ast2(NewStrAst, DefaultStringAst, AstInfo, TreeWalker) -> StringLookupAst = erl_syntax:application( - erl_syntax:atom(erlydtl_runtime), - erl_syntax:atom(translate), - [NewStrAst, erl_syntax:variable("_TranslationFun"), DefaultStringAst]), + erl_syntax:atom(erlydtl_runtime), + erl_syntax:atom(translate), + [NewStrAst, erl_syntax:variable("_TranslationFun"), DefaultStringAst]), {{StringLookupAst, AstInfo}, TreeWalker}. -% Completely unnecessary in ErlyDTL (use {{ "{%" }} etc), but implemented for compatibility. + % Completely unnecessary in ErlyDTL (use {{ "{%" }} etc), but implemented for compatibility. templatetag_ast("openblock", Context, TreeWalker) -> string_ast("{%", Context, TreeWalker); templatetag_ast("closeblock", Context, TreeWalker) -> @@ -822,10 +822,10 @@ widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalker) -> {{DenAst, DenInfo}, TreeWalker2} = value_ast(Denominator, false, true, Context, TreeWalker1), {{ScaleAst, ScaleInfo}, TreeWalker3} = value_ast(Scale, false, true, 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}. + erl_syntax:atom(erlydtl_runtime), + erl_syntax:atom(widthratio), + [NumAst, DenAst, ScaleAst])), merge_info(ScaleInfo, merge_info(NumInfo, DenInfo))}, + TreeWalker3}. binary_string(String) -> erl_syntax:binary([erl_syntax:binary_field(erl_syntax:integer(X)) || X <- String]). @@ -843,57 +843,57 @@ include_ast(File, ArgList, Scopes, Context, TreeWalker) -> case parse(FilePath, Context) of {ok, InclusionParseTree, CheckSum} -> {NewScope, {ArgInfo, TreeWalker1}} = lists:mapfoldl(fun - ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) -> - {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1), - {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}} - end, {#ast_info{}, TreeWalker}, ArgList), + ({{identifier, _, LocalVarName}, Value}, {AstInfo1, TreeWalker1}) -> + {{Ast, Info}, TreeWalker2} = value_ast(Value, false, false, Context, TreeWalker1), + {{LocalVarName, Ast}, {merge_info(AstInfo1, Info), TreeWalker2}} + end, {#ast_info{}, TreeWalker}, ArgList), {{BodyAst, BodyInfo}, TreeWalker2} = with_dependency({FilePath, CheckSum}, - body_ast(InclusionParseTree, Context#dtl_context{ - parse_trail = [FilePath | Context#dtl_context.parse_trail], - local_scopes = [NewScope|Scopes] - }, TreeWalker1)), + body_ast(InclusionParseTree, Context#dtl_context{ + parse_trail = [FilePath | Context#dtl_context.parse_trail], + local_scopes = [NewScope|Scopes] + }, TreeWalker1)), {{BodyAst, merge_info(BodyInfo, ArgInfo)}, TreeWalker2}; Err -> throw(Err) end. - -% include at run-time + + % include at run-time ssi_ast(FileName, Context, TreeWalker) -> {{Ast, Info}, TreeWalker1} = value_ast(FileName, true, 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), erl_syntax:string(Context#dtl_context.doc_root), Ast]), Info}, TreeWalker1}. + erl_syntax:atom(erlydtl_runtime), + erl_syntax:atom(read_file), + [erl_syntax:atom(Mod), erl_syntax:atom(Fun), erl_syntax:string(Context#dtl_context.doc_root), Ast]), Info}, TreeWalker1}. 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(erlang), - erl_syntax:atom(iolist_to_binary), - [InnerAst]), Info}, TreeWalker1}, FilterList), + ([{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(erlang), + erl_syntax:atom(iolist_to_binary), + [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, + 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}) -> @@ -917,21 +917,21 @@ 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 + % the escape filter is special; it is always applied last, so we have to go digging for it - % AutoEscape = 'did' means we (will have) decided whether to escape the current variable, - % so don't do any more escaping + % AutoEscape = 'did' means we (will have) decided whether to escape the current variable, + % so don't do any more escaping {{UnescapedAst, Info}, TreeWalker2} = filter_ast_noescape(Variable, Filter, - Context#dtl_context{auto_escape = did}, TreeWalker), + Context#dtl_context{auto_escape = did}, TreeWalker), 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]); - _ -> - UnescapedAst - end, + on -> + 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) -> @@ -961,7 +961,7 @@ filter_ast2(Name, VariableAst, [], VarNames, #dtl_context{ filter_modules = [Mod case lists:member({Name, 1}, Module:module_info(exports)) of true -> {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name), - [VariableAst]), #ast_info{var_names = VarNames}}; + [VariableAst]), #ast_info{var_names = VarNames}}; false -> filter_ast2(Name, VariableAst, [], VarNames, Context#dtl_context{ filter_modules = Rest }) end; @@ -969,11 +969,11 @@ filter_ast2(Name, VariableAst, [Arg], VarNames, #dtl_context{ filter_modules = [ case lists:member({Name, 2}, Module:module_info(exports)) of true -> {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name), - [VariableAst, Arg]), #ast_info{var_names = VarNames}}; + [VariableAst, Arg]), #ast_info{var_names = VarNames}}; false -> filter_ast2(Name, VariableAst, [Arg], VarNames, Context#dtl_context{ filter_modules = Rest }) end. - + search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) -> search_for_safe_filter(Variable, Filter); search_for_escape_filter(_, _, #dtl_context{auto_escape = did}) -> @@ -998,51 +998,51 @@ resolve_variable_ast(VarTuple, Context, true) -> resolve_variable_ast1(VarTuple, Context, 'fetch_value'); resolve_variable_ast(VarTuple, Context, false) -> resolve_variable_ast1(VarTuple, Context, 'find_value'). - + resolve_variable_ast1({attribute, {{identifier, {Row, Col}, AttrName}, Variable}}, Context, FinderFunction) -> {VarAst, VarName} = resolve_variable_ast1(Variable, Context, FinderFunction), FileNameAst = case Context#dtl_context.parse_trail of - [] -> erl_syntax:atom(undefined); - [H|_] -> erl_syntax:string(H) - end, + [] -> erl_syntax:atom(undefined); + [H|_] -> erl_syntax:string(H) + end, {erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction), - [erl_syntax:atom(AttrName), VarAst, FileNameAst, - erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)]) - ]), VarName}; + [erl_syntax:atom(AttrName), VarAst, FileNameAst, + erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)]) + ]), VarName}; resolve_variable_ast1({variable, {identifier, {Row, Col}, VarName}}, Context, FinderFunction) -> VarValue = case resolve_scoped_variable_ast(VarName, Context) of - undefined -> - FileNameAst = case Context#dtl_context.parse_trail of - [] -> erl_syntax:atom(undefined); - [H|_] -> erl_syntax:string(H) - end, - erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction), - [erl_syntax:atom(VarName), erl_syntax:variable("_Variables"), FileNameAst, - erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)]) - ]); - Val -> - Val - end, + undefined -> + FileNameAst = case Context#dtl_context.parse_trail of + [] -> erl_syntax:atom(undefined); + [H|_] -> erl_syntax:string(H) + end, + erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction), + [erl_syntax:atom(VarName), erl_syntax:variable("_Variables"), FileNameAst, + erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)]) + ]); + Val -> + Val + end, {VarValue, VarName}; resolve_variable_ast1(What, _Context, _FinderFunction) -> - error_logger:error_msg("~p:resolve_variable_ast unhandled: ~p~n", [?MODULE, What]). + error_logger:error_msg("~p:resolve_variable_ast unhandled: ~p~n", [?MODULE, What]). resolve_scoped_variable_ast(VarName, Context) -> lists:foldl(fun(Scope, Value) -> - case Value of - undefined -> proplists:get_value(VarName, Scope); - _ -> Value - end - end, undefined, Context#dtl_context.local_scopes). + case Value of + undefined -> proplists:get_value(VarName, Scope); + _ -> Value + end + end, undefined, Context#dtl_context.local_scopes). format(Ast, Context, TreeWalker) -> auto_escape(format_number_ast(Ast), Context, TreeWalker). format_number_ast(Ast) -> erl_syntax:application(erl_syntax:atom(erlydtl_filters), erl_syntax:atom(format_number), - [Ast]). + [Ast]). auto_escape(Value, _, #treewalker{safe = true}) -> @@ -1053,59 +1053,59 @@ auto_escape(Value, _, _) -> Value. firstof_ast(Vars, Context, TreeWalker) -> - body_ast([lists:foldr(fun - ({L, _, _}=Var, []) when L=:=string_literal;L=:=number_literal -> - Var; - ({L, _, _}, _) when L=:=string_literal;L=:=number_literal -> - erlang:error(errbadliteral); - (Var, []) -> - {'ifelse', Var, [Var], []}; - (Var, Acc) -> - {'ifelse', Var, [Var], [Acc]} end, - [], Vars)], Context, TreeWalker). + body_ast([lists:foldr(fun + ({L, _, _}=Var, []) when L=:=string_literal;L=:=number_literal -> + Var; + ({L, _, _}, _) when L=:=string_literal;L=:=number_literal -> + erlang:error(errbadliteral); + (Var, []) -> + {'ifelse', Var, [Var], []}; + (Var, Acc) -> + {'ifelse', Var, [Var], [Acc]} end, + [], Vars)], Context, TreeWalker). ifelse_ast(Expression, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) -> Info = merge_info(IfContentsInfo, ElseContentsInfo), {{Ast, ExpressionInfo}, TreeWalker1} = value_ast(Expression, false, false, Context, TreeWalker), {{erl_syntax:case_expr(erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(is_true), [Ast]), - [erl_syntax:clause([erl_syntax:atom(true)], none, - [IfContentsAst]), - erl_syntax:clause([erl_syntax:underscore()], none, - [ElseContentsAst]) - ]), merge_info(ExpressionInfo, Info)}, TreeWalker1}. + [erl_syntax:clause([erl_syntax:atom(true)], none, + [IfContentsAst]), + erl_syntax:clause([erl_syntax:underscore()], none, + [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, false, Context, TreeWalker1), - {Ast, {merge_info(AstInfo1, Info), TreeWalker2}} - end, {#ast_info{}, TreeWalker}, ArgList), + ({{identifier, _, _LocalVarName}, Value}, {AstInfo1, TreeWalker1}) -> + {{Ast, Info}, TreeWalker2} = value_ast(Value, false, 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), + {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), + 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}. + erl_syntax:fun_expr([ + erl_syntax:clause(lists:map(fun({_, Var}) -> Var end, NewScope), none, + [InnerAst])]), ArgAstList), merge_info(ArgInfo, InnerInfo)}, TreeWalker2}. regroup_ast(ListVariable, GrouperVariable, LocalVarName, Contents, Context, TreeWalker) -> {{ListAst, ListInfo}, TreeWalker1} = value_ast(ListVariable, false, true, Context, TreeWalker), NewScope = [{LocalVarName, erl_syntax:variable(lists:concat(["Var_", LocalVarName]))}], {{InnerAst, InnerInfo}, TreeWalker2} = body_ast(Contents, - Context#dtl_context{ local_scopes = [NewScope|Context#dtl_context.local_scopes] }, TreeWalker1), + Context#dtl_context{ local_scopes = [NewScope|Context#dtl_context.local_scopes] }, TreeWalker1), Ast = {erl_syntax:application( - erl_syntax:fun_expr([ - erl_syntax:clause([erl_syntax:variable(lists:concat(["Var_", LocalVarName]))], none, - [InnerAst])]), - [erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(regroup), - [ListAst, regroup_filter(GrouperVariable,[])])]), merge_info(ListInfo, InnerInfo)}, + erl_syntax:fun_expr([ + erl_syntax:clause([erl_syntax:variable(lists:concat(["Var_", LocalVarName]))], none, + [InnerAst])]), + [erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(regroup), + [ListAst, regroup_filter(GrouperVariable,[])])]), merge_info(ListInfo, InnerInfo)}, {Ast,TreeWalker2}. regroup_filter({attribute,{{identifier,_,Ident},Next}},Acc) -> @@ -1116,52 +1116,52 @@ regroup_filter({variable,{identifier,_,Var}},Acc) -> for_loop_ast(IteratorList, LoopValue, IsReversed, Contents, {EmptyContentsAst, EmptyContentsInfo}, Context, TreeWalker) -> Vars = lists:map(fun({identifier, _, Iterator}) -> - erl_syntax:variable(lists:concat(["Var_", Iterator])) - end, IteratorList), + erl_syntax:variable(lists:concat(["Var_", Iterator])) + end, IteratorList), {{InnerAst, Info}, TreeWalker1} = body_ast(Contents, - Context#dtl_context{local_scopes = [ - [{'forloop', erl_syntax:variable("Counters")} | lists:map( - fun({identifier, _, Iterator}) -> - {Iterator, erl_syntax:variable(lists:concat(["Var_", Iterator]))} - end, IteratorList)] | Context#dtl_context.local_scopes]}, TreeWalker), + Context#dtl_context{local_scopes = [ + [{'forloop', erl_syntax:variable("Counters")} | lists:map( + fun({identifier, _, Iterator}) -> + {Iterator, erl_syntax:variable(lists:concat(["Var_", Iterator]))} + end, IteratorList)] | Context#dtl_context.local_scopes]}, TreeWalker), CounterAst = erl_syntax:application(erl_syntax:atom(erlydtl_runtime), - erl_syntax:atom(increment_counter_stats), [erl_syntax:variable("Counters")]), + erl_syntax:atom(increment_counter_stats), [erl_syntax:variable("Counters")]), {{LoopValueAst, LoopValueInfo}, TreeWalker2} = value_ast(LoopValue, false, true, Context, TreeWalker1), LoopValueAst0 = case IsReversed of - true -> erl_syntax:application(erl_syntax:atom(lists), erl_syntax:atom(reverse), [LoopValueAst]); - false -> LoopValueAst - end, + true -> erl_syntax:application(erl_syntax:atom(lists), erl_syntax:atom(reverse), [LoopValueAst]); + false -> LoopValueAst + end, CounterVars0 = case resolve_scoped_variable_ast('forloop', Context) of - undefined -> - erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst0]); - Value -> - erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst0, Value]) - end, + undefined -> + erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst0]); + Value -> + erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(init_counter_stats), [LoopValueAst0, Value]) + end, {{erl_syntax:case_expr( - erl_syntax:application( - erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('forloop'), - [erl_syntax:fun_expr([ + erl_syntax:application( + erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('forloop'), + [erl_syntax:fun_expr([ erl_syntax:clause([erl_syntax:tuple(Vars), erl_syntax:variable("Counters")], none, - [erl_syntax:tuple([InnerAst, CounterAst])]), + [erl_syntax:tuple([InnerAst, CounterAst])]), erl_syntax:clause(case Vars of [H] -> [H, erl_syntax:variable("Counters")]; - _ -> [erl_syntax:list(Vars), erl_syntax:variable("Counters")] end, none, - [erl_syntax:tuple([InnerAst, CounterAst])]) - ]), - CounterVars0, LoopValueAst0]), - [erl_syntax:clause( - [erl_syntax:tuple([erl_syntax:underscore(), - erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(counter), erl_syntax:integer(1)])], - erl_syntax:underscore())])], - none, [EmptyContentsAst]), - erl_syntax:clause( - [erl_syntax:tuple([erl_syntax:variable("L"), erl_syntax:underscore()])], - none, [erl_syntax:variable("L")])] - ), - merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo) - }, TreeWalker2}. + _ -> [erl_syntax:list(Vars), erl_syntax:variable("Counters")] end, none, + [erl_syntax:tuple([InnerAst, CounterAst])]) + ]), + CounterVars0, LoopValueAst0]), + [erl_syntax:clause( + [erl_syntax:tuple([erl_syntax:underscore(), + erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(counter), erl_syntax:integer(1)])], + erl_syntax:underscore())])], + none, [EmptyContentsAst]), + erl_syntax:clause( + [erl_syntax:tuple([erl_syntax:variable("L"), erl_syntax:underscore()])], + none, [erl_syntax:variable("L")])] + ), + merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo) + }, TreeWalker2}. ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, Context, TreeWalker) -> Info = merge_info(IfContentsInfo, ElseContentsInfo), @@ -1170,36 +1170,36 @@ ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, {ETw, merge_info(LInfo, EInfo), [erl_syntax:tuple([erl_syntax:integer(erlang:phash2(Expr)), EAst])|Acc]} end, {TreeWalker1, MergedInfo, Changed} = lists:foldl(ValueAstFun, {TreeWalker, Info, []}, Values), {{erl_syntax:case_expr(erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(ifchanged), [erl_syntax:list(Changed)]), - [erl_syntax:clause([erl_syntax:atom(true)], none, - [IfContentsAst]), - erl_syntax:clause([erl_syntax:underscore()], none, - [ElseContentsAst]) - ]), MergedInfo}, TreeWalker1}. + [erl_syntax:clause([erl_syntax:atom(true)], none, + [IfContentsAst]), + erl_syntax:clause([erl_syntax:underscore()], none, + [ElseContentsAst]) + ]), MergedInfo}, TreeWalker1}. ifchanged_contents_ast(Contents, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, _Context, TreeWalker) -> Info = merge_info(IfContentsInfo, ElseContentsInfo), Key = erl_syntax:integer(erlang:phash2(Contents)), {{erl_syntax:case_expr(erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(ifchanged), [erl_syntax:list([erl_syntax:tuple([Key, IfContentsAst])])]), - [erl_syntax:clause([erl_syntax:atom(true)], none, - [IfContentsAst]), - erl_syntax:clause([erl_syntax:underscore()], none, - [ElseContentsAst]) - ]), Info}, TreeWalker}. + [erl_syntax:clause([erl_syntax:atom(true)], none, + [IfContentsAst]), + erl_syntax:clause([erl_syntax:underscore()], none, + [ElseContentsAst]) + ]), Info}, TreeWalker}. cycle_ast(Names, Context, TreeWalker) -> {NamesTuple, VarNames} = lists:mapfoldl(fun - ({string_literal, _, Str}, VarNamesAcc) -> - {{S, _}, _} = string_ast(unescape_string_literal(Str), Context, TreeWalker), - {S, VarNamesAcc}; - ({variable, _}=Var, VarNamesAcc) -> - {V, VarName} = resolve_variable_ast(Var, Context, true), - {V, [VarName|VarNamesAcc]}; - ({number_literal, _, Num}, VarNamesAcc) -> - {format(erl_syntax:integer(Num), Context, TreeWalker), VarNamesAcc}; - (_, VarNamesAcc) -> - {[], VarNamesAcc} - end, [], Names), + ({string_literal, _, Str}, VarNamesAcc) -> + {{S, _}, _} = string_ast(unescape_string_literal(Str), Context, TreeWalker), + {S, VarNamesAcc}; + ({variable, _}=Var, VarNamesAcc) -> + {V, VarName} = resolve_variable_ast(Var, Context, true), + {V, [VarName|VarNamesAcc]}; + ({number_literal, _, Num}, VarNamesAcc) -> + {format(erl_syntax:integer(Num), Context, TreeWalker), VarNamesAcc}; + (_, VarNamesAcc) -> + {[], VarNamesAcc} + end, [], Names), {{erl_syntax:application( erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('cycle'), [erl_syntax:tuple(NamesTuple), erl_syntax:variable("Counters")]), #ast_info{ var_names = VarNames }}, TreeWalker}. @@ -1207,19 +1207,19 @@ cycle_ast(Names, Context, TreeWalker) -> %% Older Django templates treat cycle with comma-delimited elements as strings cycle_compat_ast(Names, Context, TreeWalker) -> NamesTuple = lists:map(fun - ({identifier, _, X}) -> - {{S, _}, _} = string_ast(X, Context, TreeWalker), - S - end, Names), + ({identifier, _, X}) -> + {{S, _}, _} = string_ast(X, Context, TreeWalker), + S + end, Names), {{erl_syntax:application( erl_syntax:atom('erlydtl_runtime'), erl_syntax:atom('cycle'), [erl_syntax:tuple(NamesTuple), erl_syntax:variable("Counters")]), #ast_info{}}, TreeWalker}. now_ast(FormatString, Context, TreeWalker) -> - % Note: we can't use unescape_string_literal here - % because we want to allow escaping in the format string. - % We only want to remove the surrounding escapes, - % i.e. \"foo\" becomes "foo" + % Note: we can't use unescape_string_literal here + % because we want to allow escaping in the format string. + % We only want to remove the surrounding escapes, + % i.e. \"foo\" becomes "foo" UnescapeOuter = string:strip(FormatString, both, 34), {{StringAst, Info}, TreeWalker1} = string_ast(UnescapeOuter, Context, TreeWalker), {{erl_syntax:application( @@ -1230,9 +1230,9 @@ now_ast(FormatString, Context, TreeWalker) -> spaceless_ast(Contents, Context, TreeWalker) -> {{Ast, Info}, TreeWalker1} = body_ast(Contents, Context, TreeWalker), {{erl_syntax:application( - erl_syntax:atom(erlydtl_runtime), - erl_syntax:atom(spaceless), - [Ast]), Info}, TreeWalker1}. + erl_syntax:atom(erlydtl_runtime), + erl_syntax:atom(spaceless), + [Ast]), Info}, TreeWalker1}. unescape_string_literal(String) -> unescape_string_literal(string:strip(String, both, 34), [], noslash). @@ -1258,7 +1258,7 @@ full_path(File, DocRoot) -> File -> File; _ -> filename:join([DocRoot, File]) end. - + %%------------------------------------------------------------------- %% Custom tags %%------------------------------------------------------------------- @@ -1270,40 +1270,40 @@ key_to_string(Key) when is_list(Key) -> tag_ast(Name, Args, Context, TreeWalker) -> {{InterpretedArgs, AstInfo1}, TreeWalker1} = lists:foldr(fun - ({{identifier, _, Key}, {trans, StringLiteral}}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> - {{TransAst, TransAstInfo}, TreeWalker0} = translated_ast(StringLiteral, Context, TreeWalkerAcc), - {{[erl_syntax:tuple([erl_syntax:atom(Key), TransAst])|ArgsAcc], merge_info(TransAstInfo, AstInfoAcc)}, TreeWalker0}; - ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> - {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc), - {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0} - end, {{[], #ast_info{}}, TreeWalker}, Args), + ({{identifier, _, Key}, {trans, StringLiteral}}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> + {{TransAst, TransAstInfo}, TreeWalker0} = translated_ast(StringLiteral, Context, TreeWalkerAcc), + {{[erl_syntax:tuple([erl_syntax:atom(Key), TransAst])|ArgsAcc], merge_info(TransAstInfo, AstInfoAcc)}, TreeWalker0}; + ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> + {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc), + {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0} + end, {{[], #ast_info{}}, TreeWalker}, Args), {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, InterpretedArgs, Context), {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker1}. custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = false }) -> {erl_syntax:application(none, erl_syntax:atom(render_tag), - [key_to_string(Name), erl_syntax:list(InterpretedArgs), - erl_syntax:variable("RenderOptions")]), - #ast_info{custom_tags = [Name]}}; + [key_to_string(Name), erl_syntax:list(InterpretedArgs), + erl_syntax:variable("RenderOptions")]), + #ast_info{custom_tags = [Name]}}; custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = true, module = Module }) -> {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name), - [erl_syntax:list(InterpretedArgs), erl_syntax:variable("RenderOptions")]), - #ast_info{ custom_tags = [Name] }}; + [erl_syntax:list(InterpretedArgs), erl_syntax:variable("RenderOptions")]), + #ast_info{ custom_tags = [Name] }}; custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [Module|Rest] } = Context) -> try lists:max([I || {N,I} <- Module:module_info(exports), N =:= Name]) of 2 -> {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name), - [erl_syntax:list(InterpretedArgs), - erl_syntax:variable("RenderOptions")]), #ast_info{}}; + [erl_syntax:list(InterpretedArgs), + erl_syntax:variable("RenderOptions")]), #ast_info{}}; 1 -> {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name), - [erl_syntax:list(InterpretedArgs)]), #ast_info{}}; + [erl_syntax:list(InterpretedArgs)]), #ast_info{}}; I -> throw({unsupported_custom_tag_fun, {Module, Name, I}}) catch _:function_clause -> - custom_tags_modules_ast(Name, InterpretedArgs, - Context#dtl_context{ custom_tags_modules = Rest }) + custom_tags_modules_ast(Name, InterpretedArgs, + Context#dtl_context{ custom_tags_modules = Rest }) end. print(true, Fmt, Args) -> @@ -1317,12 +1317,12 @@ call_ast(Module, TreeWalkerAcc) -> call_with_ast(Module, Variable, Context, TreeWalker) -> {VarAst, VarName} = resolve_variable_ast(Variable, Context, false), call_ast(Module, VarAst, #ast_info{var_names=[VarName]}, TreeWalker). - + call_ast(Module, Variable, AstInfo, TreeWalker) -> - AppAst = erl_syntax:application( - erl_syntax:atom(Module), - erl_syntax:atom(render), - [Variable, erl_syntax:variable("RenderOptions")]), + AppAst = erl_syntax:application( + erl_syntax:atom(Module), + erl_syntax:atom(render), + [Variable, erl_syntax:variable("RenderOptions")]), RenderedAst = erl_syntax:variable("Rendered"), OkAst = erl_syntax:clause( [erl_syntax:tuple([erl_syntax:atom(ok), RenderedAst])], From c019a574ea33766072c29dfb077f49035a9f4a70 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 13 Jun 2013 23:23:07 +0200 Subject: [PATCH 013/361] add extension_module option, and pass the dtl context to the parse function. --- src/erlydtl_compiler.erl | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 50d59f0..9b4c525 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -59,7 +59,9 @@ force_recompile = false, locale = none, verbose = false, - is_compiling_dir = false}). + is_compiling_dir = false, + extension_module = undefined + }). -record(ast_info, { dependencies = [], @@ -83,10 +85,10 @@ compile(File, Module) -> compile(Binary, Module, Options) when is_binary(Binary) -> File = "", CheckSum = "", - case parse(Binary) of + Context = init_dtl_context(File, Module, Options), + case parse(Binary, Context) of {ok, DjangoParseTree} -> - case compile_to_binary(File, DjangoParseTree, - init_dtl_context(File, Module, Options), CheckSum) of + case compile_to_binary(File, DjangoParseTree, Context, CheckSum) of {ok, Module1, _, _} -> {ok, Module1}; Err -> @@ -253,7 +255,9 @@ init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) -> force_recompile = proplists:get_value(force_recompile, Options, Ctx#dtl_context.force_recompile), locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale), verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose), - is_compiling_dir = IsCompilingDir}. + is_compiling_dir = IsCompilingDir, + extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module) + }. init_dtl_context(File, Module, Options) when is_list(Module) -> init_dtl_context(File, list_to_atom(Module), Options); @@ -299,7 +303,16 @@ is_up_to_date(CheckSum, Context) -> false end. +parse(Data) -> + parse(Data, #dtl_context{}). +parse(Data, _Context) when is_binary(Data) -> + case erlydtl_scanner:scan(binary_to_list(Data)) of + {ok, Tokens} -> + erlydtl_parser:parse(Tokens); + Err -> + Err + end; parse(File, Context) -> {M, F} = Context#dtl_context.reader, case catch M:F(File) of @@ -322,7 +335,7 @@ parse(CheckSum, Data, Context) -> true -> ok; _ -> - case parse(Data) of + case parse(Data, Context) of {ok, Val} -> {ok, Val, CheckSum}; Err -> @@ -330,14 +343,6 @@ parse(CheckSum, Data, Context) -> end end. -parse(Data) -> - case erlydtl_scanner:scan(binary_to_list(Data)) of - {ok, Tokens} -> - erlydtl_parser:parse(Tokens); - Err -> - Err - end. - custom_tags_ast(CustomTags, Context, TreeWalker) -> {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} = custom_tags_clauses_ast(CustomTags, Context, TreeWalker), {{erl_syntax:function(erl_syntax:atom(render_tag), CustomTagsClauses), CustomTagsInfo}, TreeWalker1}. @@ -768,7 +773,7 @@ blocktrans_ast(ArgList, Contents, Context, TreeWalker) -> default -> {AstInfoAcc, ThisTreeWalker, ClauseAcc}; Body -> - {ok, DjangoParseTree} = parse(Body), + {ok, DjangoParseTree} = parse(Body, Context), {{ThisAst, ThisAstInfo}, TreeWalker3} = body_ast(DjangoParseTree, NewContext, ThisTreeWalker), {merge_info(ThisAstInfo, AstInfoAcc), TreeWalker3, [erl_syntax:clause([erl_syntax:string(Locale)], none, [ThisAst])|ClauseAcc]} From 4309edeb28827da6510020f96ded1b2eb7b64b95 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 13 Jun 2013 23:53:41 +0200 Subject: [PATCH 014/361] indentational.. --- tests/src/erlydtl_unittests.erl | 2252 +++++++++++++++---------------- 1 file changed, 1126 insertions(+), 1126 deletions(-) diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index c18a44e..350b898 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -1,1180 +1,1180 @@ -module(erlydtl_unittests). - + -export([run_tests/0]). - + tests() -> [ - {"vars", [ - {"string", - <<"String value is: {{ var1 }}">>, - [{var1, "foo"}], <<"String value is: foo">>}, - {"int", - <<"The magic number is: {{ var1 }}">>, - [{var1, 42}], <<"The magic number is: 42">>}, - {"float", - <<"The price of milk is: {{ var1 }}">>, - [{var1, 0.42}], <<"The price of milk is: 0.42">>}, - {"No spaces", - <<"{{var1}}">>, - [{var1, "foo"}], <<"foo">>}, - {"Variable name is a tag name", - <<"{{ comment }}">>, - [{comment, "Nice work!"}], <<"Nice work!">>} - ]}, - {"comment", [ - {"comment block is excised", - <<"bob {% comment %}(moron){% endcomment %} loblaw">>, - [], <<"bob loblaw">>}, - {"inline comment is excised", - <<"you're {# not #} a very nice person">>, - [], <<"you're a very nice person">>} - ]}, - {"autoescape", [ - {"Autoescape works", - <<"{% autoescape on %}{{ var1 }}{% endautoescape %}">>, - [{var1, "bold"}], <<"<b>bold</b>">>}, - {"Nested autoescape", - <<"{% autoescape on %}{{ var1 }}{% autoescape off %}{{ var1 }}{% endautoescape %}{% endautoescape %}">>, - [{var1, ""}], <<"<b>">>} - ]}, - {"string literal", [ - {"Render literal", - <<"{{ \"foo\" }} is my name">>, [], <<"foo is my name">>}, - {"Newlines are escaped", - <<"{{ \"foo\\n\" }}">>, [], <<"foo\n">>} - ]}, - {"cycle", [ + {"vars", [ + {"string", + <<"String value is: {{ var1 }}">>, + [{var1, "foo"}], <<"String value is: foo">>}, + {"int", + <<"The magic number is: {{ var1 }}">>, + [{var1, 42}], <<"The magic number is: 42">>}, + {"float", + <<"The price of milk is: {{ var1 }}">>, + [{var1, 0.42}], <<"The price of milk is: 0.42">>}, + {"No spaces", + <<"{{var1}}">>, + [{var1, "foo"}], <<"foo">>}, + {"Variable name is a tag name", + <<"{{ comment }}">>, + [{comment, "Nice work!"}], <<"Nice work!">>} + ]}, + {"comment", [ + {"comment block is excised", + <<"bob {% comment %}(moron){% endcomment %} loblaw">>, + [], <<"bob loblaw">>}, + {"inline comment is excised", + <<"you're {# not #} a very nice person">>, + [], <<"you're a very nice person">>} + ]}, + {"autoescape", [ + {"Autoescape works", + <<"{% autoescape on %}{{ var1 }}{% endautoescape %}">>, + [{var1, "bold"}], <<"<b>bold</b>">>}, + {"Nested autoescape", + <<"{% autoescape on %}{{ var1 }}{% autoescape off %}{{ var1 }}{% endautoescape %}{% endautoescape %}">>, + [{var1, ""}], <<"<b>">>} + ]}, + {"string literal", [ + {"Render literal", + <<"{{ \"foo\" }} is my name">>, [], <<"foo is my name">>}, + {"Newlines are escaped", + <<"{{ \"foo\\n\" }}">>, [], <<"foo\n">>} + ]}, + {"cycle", [ {"Cycling through quoted strings", - <<"{% for i in test %}{% cycle 'a' 'b' %}{{ i }},{% endfor %}">>, - [{test, ["0", "1", "2", "3", "4"]}], <<"a0,b1,a2,b3,a4,">>}, + <<"{% for i in test %}{% cycle 'a' 'b' %}{{ i }},{% endfor %}">>, + [{test, ["0", "1", "2", "3", "4"]}], <<"a0,b1,a2,b3,a4,">>}, {"Cycling through normal variables", - <<"{% for i in test %}{% cycle aye bee %}{{ i }},{% endfor %}">>, - [{test, ["0", "1", "2", "3", "4"]}, {aye, "a"}, {bee, "b"}], - <<"a0,b1,a2,b3,a4,">>} - ]}, - {"number literal", [ - {"Render integer", - <<"{{ 5 }}">>, [], <<"5">>} - ]}, - {"variable", [ - {"Render variable", + <<"{% for i in test %}{% cycle aye bee %}{{ i }},{% endfor %}">>, + [{test, ["0", "1", "2", "3", "4"]}, {aye, "a"}, {bee, "b"}], + <<"a0,b1,a2,b3,a4,">>} + ]}, + {"number literal", [ + {"Render integer", + <<"{{ 5 }}">>, [], <<"5">>} + ]}, + {"variable", [ + {"Render variable", <<"{{ var1 }} is my game">>, [{var1, "bar"}], <<"bar is my game">>}, - {"Render variable with attribute", + {"Render variable with attribute", <<"I enjoy {{ var1.game }}">>, [{var1, [{game, "Othello"}]}], <<"I enjoy Othello">>}, - {"Render variable with string-key attribute", + {"Render variable with string-key attribute", <<"I also enjoy {{ var1.game }}">>, [{var1, [{"game", "Parcheesi"}]}], <<"I also enjoy Parcheesi">>}, - {"Render variable with binary-key attribute", + {"Render variable with binary-key attribute", <<"I also enjoy {{ var1.game }}">>, [{var1, [{<<"game">>, "Parcheesi"}]}], <<"I also enjoy Parcheesi">>}, - {"Render variable in dict", + {"Render variable in dict", <<"{{ var1 }}">>, dict:store(var1, "bar", dict:new()), <<"bar">>}, - {"Render variable in gb_tree", + {"Render variable in gb_tree", <<"{{ var1 }}">>, gb_trees:insert(var1, "bar", gb_trees:empty()), <<"bar">>}, - {"Render variable in arity-1 func", + {"Render variable in arity-1 func", <<"I enjoy {{ var1 }}">>, fun (var1) -> "Othello" end, <<"I enjoy Othello">>}, - {"Render variable with attribute in dict", + {"Render variable with attribute in dict", <<"{{ var1.attr }}">>, [{var1, dict:store(attr, "Othello", dict:new())}], <<"Othello">>}, - {"Render variable with attribute in gb_tree", + {"Render variable with attribute in gb_tree", <<"{{ var1.attr }}">>, [{var1, gb_trees:insert(attr, "Othello", gb_trees:empty())}], <<"Othello">>}, - {"Render variable with attribute in arity-1 func", + {"Render variable with attribute in arity-1 func", <<"I enjoy {{ var1.game }}">>, [{var1, fun (game) -> "Othello" end}], <<"I enjoy Othello">>}, - {"Render variable in parameterized module", + {"Render variable in parameterized module", <<"{{ var1.some_var }}">>, [{var1, erlydtl_example_variable_storage:new("foo")}], <<"foo">>}, - {"Nested attributes", + {"Nested attributes", <<"{{ person.city.state.country }}">>, [{person, [{city, [{state, [{country, "Italy"}]}]}]}], <<"Italy">>} + ]}, + {"now", [ + {"now functional", + <<"It is the {% now \"jS o\\f F Y\" %}.">>, [{var1, ""}], generate_test_date()} + ]}, + {"if", [ + {"If/else", + <<"{% if var1 %}boo{% else %}yay{% endif %}">>, [{var1, ""}], <<"yay">>}, + {"If elif", + <<"{% if var1 %}boo{% elif var2 %}yay{% endif %}">>, [{var1, ""}, {var2, "happy"}], <<"yay">>}, + {"If elif/else", + <<"{% if var1 %}boo{% elif var2 %}sad{% else %}yay{% endif %}">>, [{var1, ""}, {var2, ""}], <<"yay">>}, + {"If elif/elif/else", + <<"{% if var1 %}boo{% elif var2 %}yay{% elif var3 %}sad{% else %}noo{% endif %}">>, [{var1, ""}, + {var2, "happy"}, {var3, "not_taken"}], <<"yay">>}, + {"If", + <<"{% if var1 %}boo{% endif %}">>, [{var1, ""}], <<>>}, + {"If not", + <<"{% if not var1 %}yay{% endif %}">>, [{var1, ""}], <<"yay">>}, + {"If \"0\"", + <<"{% if var1 %}boo{% endif %}">>, [{var1, "0"}], <<>>}, + {"If false", + <<"{% if var1 %}boo{% endif %}">>, [{var1, false}], <<>>}, + {"If false string", + <<"{% if var1 %}boo{% endif %}">>, [{var1, "false"}], <<"boo">>}, + {"If undefined", + <<"{% if var1 %}boo{% endif %}">>, [{var1, undefined}], <<>>}, + {"If other atom", + <<"{% if var1 %}yay{% endif %}">>, [{var1, foobar}], <<"yay">>}, + {"If non-empty string", + <<"{% if var1 %}yay{% endif %}">>, [{var1, "hello"}], <<"yay">>}, + {"If proplist", + <<"{% if var1 %}yay{% endif %}">>, [{var1, [{foo, "bar"}]}], <<"yay">>}, + {"If complex", + <<"{% if foo.bar.baz %}omgwtfbbq{% endif %}">>, [], <<"">>} ]}, - {"now", [ - {"now functional", - <<"It is the {% now \"jS o\\f F Y\" %}.">>, [{var1, ""}], generate_test_date()} - ]}, - {"if", [ - {"If/else", - <<"{% if var1 %}boo{% else %}yay{% endif %}">>, [{var1, ""}], <<"yay">>}, - {"If elif", - <<"{% if var1 %}boo{% elif var2 %}yay{% endif %}">>, [{var1, ""}, {var2, "happy"}], <<"yay">>}, - {"If elif/else", - <<"{% if var1 %}boo{% elif var2 %}sad{% else %}yay{% endif %}">>, [{var1, ""}, {var2, ""}], <<"yay">>}, - {"If elif/elif/else", - <<"{% if var1 %}boo{% elif var2 %}yay{% elif var3 %}sad{% else %}noo{% endif %}">>, [{var1, ""}, - {var2, "happy"}, {var3, "not_taken"}], <<"yay">>}, - {"If", - <<"{% if var1 %}boo{% endif %}">>, [{var1, ""}], <<>>}, - {"If not", - <<"{% if not var1 %}yay{% endif %}">>, [{var1, ""}], <<"yay">>}, - {"If \"0\"", - <<"{% if var1 %}boo{% endif %}">>, [{var1, "0"}], <<>>}, - {"If false", - <<"{% if var1 %}boo{% endif %}">>, [{var1, false}], <<>>}, - {"If false string", - <<"{% if var1 %}boo{% endif %}">>, [{var1, "false"}], <<"boo">>}, - {"If undefined", - <<"{% if var1 %}boo{% endif %}">>, [{var1, undefined}], <<>>}, - {"If other atom", - <<"{% if var1 %}yay{% endif %}">>, [{var1, foobar}], <<"yay">>}, - {"If non-empty string", - <<"{% if var1 %}yay{% endif %}">>, [{var1, "hello"}], <<"yay">>}, - {"If proplist", - <<"{% if var1 %}yay{% endif %}">>, [{var1, [{foo, "bar"}]}], <<"yay">>}, - {"If complex", - <<"{% if foo.bar.baz %}omgwtfbbq{% endif %}">>, [], <<"">>} - ]}, - {"if .. in ..", [ - {"If substring in string", - <<"{% if var1 in var2 %}yay{% endif %}">>, [{var1, "rook"}, {var2, "Crooks"}], <<"yay">>}, - {"If substring in string (false)", - <<"{% if var1 in var2 %}boo{% endif %}">>, [{var1, "Cook"}, {var2, "Crooks"}], <<>>}, - {"If substring not in string", - <<"{% if var1 not in var2 %}yay{% endif %}">>, [{var1, "Cook"}, {var2, "Crooks"}], <<"yay">>}, - {"If substring not in string (false)", - <<"{% if var1 not in var2 %}boo{% endif %}">>, [{var1, "rook"}, {var2, "Crooks"}], <<>>}, - {"If literal substring in string", - <<"{% if \"man\" in \"Ottoman\" %}yay{% endif %}">>, [], <<"yay">>}, - {"If literal substring in string (false)", - <<"{% if \"woman\" in \"Ottoman\" %}boo{% endif %}">>, [], <<>>}, - {"If element in list", - <<"{% if var1 in var2 %}yay{% endif %}">>, [{var1, "foo"}, {var2, ["bar", "foo", "baz"]}], <<"yay">>}, - {"If element in list (false)", - <<"{% if var1 in var2 %}boo{% endif %}">>, [{var1, "FOO"}, {var2, ["bar", "foo", "baz"]}], <<>>} - ]}, - {"if .. and ..", [ - {"If true and true", - <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, true}, {var2, true}], <<"yay">>}, - {"If true and false", - <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, true}, {var2, false}], <<"">>}, - {"If false and true", - <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, false}, {var2, true}], <<"">>}, - {"If false and false ", - <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, false}, {var2, false}], <<"">>} - ]}, - {"if .. or ..", [ - {"If true or true", - <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, true}, {var2, true}], <<"yay">>}, - {"If true or false", - <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, true}, {var2, false}], <<"yay">>}, - {"If false or true", - <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, false}, {var2, true}], <<"yay">>}, - {"If false or false ", - <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, false}, {var2, false}], <<"">>} - ]}, - {"if equality", [ - {"If int equals number literal", - <<"{% if var1 == 2 %}yay{% endif %}">>, [{var1, 2}], <<"yay">>}, - {"If int equals number literal (false)", - <<"{% if var1 == 2 %}yay{% endif %}">>, [{var1, 3}], <<"">>}, - {"If string equals string literal", - <<"{% if var1 == \"2\" %}yay{% endif %}">>, [{var1, "2"}], <<"yay">>}, - {"If string equals string literal (false)", - <<"{% if var1 == \"2\" %}yay{% endif %}">>, [{var1, "3"}], <<"">>}, - {"If int not equals number literal", - <<"{% if var1 != 2 %}yay{% endif %}">>, [{var1, 3}], <<"yay">>}, - {"If string not equals string literal", - <<"{% if var1 != \"2\" %}yay{% endif %}">>, [{var1, "3"}], <<"yay">>}, - {"If filter result equals number literal", - <<"{% if var1|length == 2 %}yay{% endif %}">>, [{var1, ["fo", "bo"]}], <<"yay">>}, - {"If filter result equals string literal", - <<"{% if var1|capfirst == \"Foo\" %}yay{% endif %}">>, [{var1, "foo"}], <<"yay">>} - ]}, - {"if size comparison", [ - {"If int greater than number literal", - <<"{% if var1 > 2 %}yay{% endif %}">>, [{var1, 3}], <<"yay">>}, - {"If int greater than negative number literal", - <<"{% if var1 > -2 %}yay{% endif %}">>, [{var1, -1}], <<"yay">>}, - {"If int greater than number literal (false)", - <<"{% if var1 > 2 %}yay{% endif %}">>, [{var1, 2}], <<"">>}, - - {"If int greater than or equal to number literal", - <<"{% if var1 >= 2 %}yay{% endif %}">>, [{var1, 3}], <<"yay">>}, - {"If int greater than or equal to number literal (2)", - <<"{% if var1 >= 2 %}yay{% endif %}">>, [{var1, 2}], <<"yay">>}, - {"If int greater than or equal to number literal (false)", - <<"{% if var1 >= 2 %}yay{% endif %}">>, [{var1, 1}], <<"">>}, - - {"If int less than number literal", - <<"{% if var1 < 2 %}yay{% endif %}">>, [{var1, 1}], <<"yay">>}, - {"If int less than number literal (false)", - <<"{% if var1 < 2 %}yay{% endif %}">>, [{var1, 2}], <<"">>}, - - {"If int less than or equal to number literal", - <<"{% if var1 <= 2 %}yay{% endif %}">>, [{var1, 1}], <<"yay">>}, - {"If int less than or equal to number literal", - <<"{% if var1 <= 2 %}yay{% endif %}">>, [{var1, 2}], <<"yay">>}, - {"If int less than or equal to number literal (false)", - <<"{% if var1 <= 2 %}yay{% endif %}">>, [{var1, 3}], <<"">>} - ]}, - {"if complex bool", [ - {"If (true or false) and true", - <<"{% if (var1 or var2) and var3 %}yay{% endif %}">>, - [{var1, true}, {var2, false}, {var3, true}], <<"yay">>}, - {"If true or (false and true)", - <<"{% if var1 or (var2 and var3) %}yay{% endif %}">>, - [{var1, true}, {var2, false}, {var3, true}], <<"yay">>} - ]}, - {"for", [ - {"Simple loop", - <<"{% for x in list %}{{ x }}{% endfor %}">>, [{'list', ["1", "2", "3"]}], - <<"123">>}, - {"Reversed loop", - <<"{% for x in list reversed %}{{ x }}{% endfor %}">>, [{'list', ["1", "2", "3"]}], - <<"321">>}, - {"Expand list", - <<"{% for x, y in list %}{{ x }},{{ y }}\n{% endfor %}">>, [{'list', [["X", "1"], ["X", "2"]]}], - <<"X,1\nX,2\n">>}, - {"Expand tuple", - <<"{% for x, y in list %}{{ x }},{{ y }}\n{% endfor %}">>, [{'list', [{"X", "1"}, {"X", "2"}]}], - <<"X,1\nX,2\n">>}, - {"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">>}, - {"Counter0", - <<"{% for number in numbers %}{{ forloop.counter0 }}. {{ number }}\n{% endfor %}">>, - [{numbers, ["Zero", "One", "Two"]}], <<"0. Zero\n1. One\n2. Two\n">>}, - {"Counter", - <<"{% for number in numbers %}{{ forloop.counter }}. {{ number }}\n{% endfor %}">>, - [{numbers, ["One", "Two", "Three"]}], <<"1. One\n2. Two\n3. Three\n">>}, - {"Reverse Counter0", - <<"{% for number in numbers %}{{ forloop.revcounter0 }}. {{ number }}\n{% endfor %}">>, - [{numbers, ["Two", "One", "Zero"]}], <<"2. Two\n1. One\n0. Zero\n">>}, - {"Reverse Counter", - <<"{% for number in numbers %}{{ forloop.revcounter }}. {{ number }}\n{% endfor %}">>, - [{numbers, ["Three", "Two", "One"]}], <<"3. Three\n2. Two\n1. One\n">>}, - {"Counter \"first\"", - <<"{% for number in numbers %}{% if forloop.first %}{{ number }}{% endif %}{% endfor %}">>, - [{numbers, ["One", "Two", "Three"]}], <<"One">>}, - {"Counter \"last\"", - <<"{% for number in numbers %}{% if forloop.last %}{{ number }}{% endif %}{% endfor %}">>, - [{numbers, ["One", "Two", "Three"]}], <<"Three">>}, - {"Nested for loop", - <<"{% for outer in list %}{% for inner in outer %}{{ inner }}\n{% endfor %}{% endfor %}">>, - [{'list', [["Al", "Albert"], ["Jo", "Joseph"]]}], - <<"Al\nAlbert\nJo\nJoseph\n">>}, - {"Access parent loop counters", - <<"{% for outer in list %}{% for inner in outer %}({{ forloop.parentloop.counter0 }}, {{ forloop.counter0 }})\n{% endfor %}{% endfor %}">>, - [{'list', [["One", "two"], ["One", "two"]]}], <<"(0, 0)\n(0, 1)\n(1, 0)\n(1, 1)\n">>}, - {"If changed", - <<"{% for x in list %}{% ifchanged %}{{ x }}\n{% endifchanged %}{% endfor %}">>, - [{'list', ["one", "two", "two", "three", "three", "three"]}], <<"one\ntwo\nthree\n">>}, - {"If changed/2", - <<"{% for x, y in list %}{% ifchanged %}{{ x|upper }}{% endifchanged %}{% ifchanged %}{{ y|lower }}{% endifchanged %}\n{% endfor %}">>, - [{'list', [["one", "a"], ["two", "A"], ["two", "B"], ["three", "b"], ["three", "c"], ["Three", "b"]]}], <<"ONEa\nTWO\nb\nTHREE\nc\nb\n">>}, - {"If changed/else", - <<"{% for x in list %}{% ifchanged %}{{ x }}\n{% else %}foo\n{% endifchanged %}{% endfor %}">>, - [{'list', ["one", "two", "two", "three", "three", "three"]}], <<"one\ntwo\nfoo\nthree\nfoo\nfoo\n">>}, - {"If changed/param", - <<"{% for date in list %}{% ifchanged date.month %} {{ date.month }}:{{ date.day }}{% else %},{{ date.day }}{% endifchanged %}{% endfor %}\n">>, - [{'list', [[{month,"Jan"},{day,1}],[{month,"Jan"},{day,2}],[{month,"Apr"},{day,10}], - [{month,"Apr"},{day,11}],[{month,"May"},{day,4}]]}], - <<" Jan:1,2 Apr:10,11 May:4\n">>}, - {"If changed/param2", - <<"{% for x, y in list %}{% ifchanged y|upper %}{{ x|upper }}{% endifchanged %}\n{% endfor %}">>, - [{'list', [["one", "a"], ["two", "A"], ["two", "B"], ["three", "b"], ["three", "c"], ["Three", "b"]]}], <<"ONE\n\nTWO\n\nTHREE\nTHREE\n">>}, - {"If changed/param2 combined", - <<"{% for x, y in list %}{% ifchanged x y|upper %}{{ x }}{% endifchanged %}\n{% endfor %}">>, - [{'list', [["one", "a"], ["two", "A"], ["two", "B"], ["three", "b"], ["three", "B"], ["three", "c"]]}], <<"one\ntwo\ntwo\nthree\n\nthree\n">>}, - {"If changed/resolve", - <<"{% for x in list %}{% ifchanged x.name|first %}{{ x.value }}{% endifchanged %}\n{% endfor %}">>, - [{'list', [[{"name", ["nA","nB"]},{"value","1"}],[{"name", ["nA","nC"]},{"value","2"}], - [{"name", ["nB","nC"]},{"value","3"}],[{"name", ["nB","nA"]},{"value","4"}]]}], - <<"1\n\n3\n\n">>} - ]}, - {"for/empty", [ - {"Simple loop", - <<"{% for x in list %}{{ x }}{% empty %}shucks{% endfor %}">>, [{'list', ["1", "2", "3"]}], - <<"123">>}, - {"Simple loop (empty)", - <<"{% for x in list %}{{ x }}{% empty %}shucks{% endfor %}">>, [{'list', []}], - <<"shucks">>} - ]}, - {"ifequal", [ - {"Compare variable to literal", - <<"{% ifequal var1 \"foo\" %}yay{% endifequal %}">>, - [{var1, "foo"}], <<"yay">>}, - {"Compare variable to unequal literal", - <<"{% ifequal var1 \"foo\" %}boo{% endifequal %}">>, - [{var1, "bar"}], <<>>}, - {"Compare literal to variable", - <<"{% ifequal \"foo\" var1 %}yay{% endifequal %}">>, - [{var1, "foo"}], <<"yay">>}, - {"Compare literal to unequal variable", - <<"{% ifequal \"foo\" var1 %}boo{% endifequal %}">>, - [{var1, "bar"}], <<>>}, - {"Compare variable to literal (int string)", - <<"{% ifequal var1 \"2\" %}yay{% else %}boo{% endifequal %}">>, - [{var1, "2"}], <<"yay">>}, - {"Compare variable to literal (int)", - <<"{% ifequal var1 2 %}yay{% else %}boo{% endifequal %}">>, - [{var1, 2}], <<"yay">>}, - {"Compare variable to unequal literal (int)", - <<"{% ifequal var1 2 %}boo{% else %}yay{% endifequal %}">>, - [{var1, 3}], <<"yay">>}, - {"Compare variable to equal literal (atom)", - <<"{% ifequal var1 \"foo\"%}yay{% endifequal %}">>, - [{var1, foo}], <<"yay">>}, - {"Compare variable to unequal literal (atom)", - <<"{% ifequal var1 \"foo\"%}yay{% else %}boo{% endifequal %}">>, - [{var1, bar}], <<"boo">>} - ]}, - {"ifequal/else", [ - {"Compare variable to literal", - <<"{% ifequal var1 \"foo\" %}yay{% else %}boo{% endifequal %}">>, - [{var1, "foo"}], <<"yay">>}, - {"Compare variable to unequal literal", - <<"{% ifequal var1 \"foo\" %}boo{% else %}yay{% endifequal %}">>, - [{var1, "bar"}], <<"yay">>}, - {"Compare literal to variable", - <<"{% ifequal \"foo\" var1 %}yay{% else %}boo{% endifequal %}">>, - [{var1, "foo"}], <<"yay">>}, - {"Compare literal to unequal variable", - <<"{% ifequal \"foo\" var1 %}boo{% else %}yay{% endifequal %}">>, - [{var1, "bar"}], <<"yay">>} - ]}, - {"ifnotequal", [ - {"Compare variable to literal", - <<"{% ifnotequal var1 \"foo\" %}boo{% endifnotequal %}">>, - [{var1, "foo"}], <<>>}, - {"Compare variable to unequal literal", - <<"{% ifnotequal var1 \"foo\" %}yay{% endifnotequal %}">>, - [{var1, "bar"}], <<"yay">>}, - {"Compare literal to variable", - <<"{% ifnotequal \"foo\" var1 %}boo{% endifnotequal %}">>, - [{var1, "foo"}], <<>>}, - {"Compare literal to unequal variable", - <<"{% ifnotequal \"foo\" var1 %}yay{% endifnotequal %}">>, - [{var1, "bar"}], <<"yay">>} - ]}, - {"ifnotequal/else", [ - {"Compare variable to literal", - <<"{% ifnotequal var1 \"foo\" %}boo{% else %}yay{% endifnotequal %}">>, - [{var1, "foo"}], <<"yay">>}, - {"Compare variable to unequal literal", - <<"{% ifnotequal var1 \"foo\" %}yay{% else %}boo{% endifnotequal %}">>, - [{var1, "bar"}], <<"yay">>}, - {"Compare literal to variable", - <<"{% ifnotequal \"foo\" var1 %}boo{% else %}yay{% endifnotequal %}">>, - [{var1, "foo"}], <<"yay">>}, - {"Compare literal to unequal variable", - <<"{% ifnotequal \"foo\" var1 %}yay{% else %}boo{% endifnotequal %}">>, - [{var1, "bar"}], <<"yay">>} - ]}, - {"filter tag", [ - {"Apply a filter", - <<"{% filter escape %}&{% endfilter %}">>, [], <<"&">>}, - {"Chained filters", - <<"{% filter linebreaksbr|escape %}\n{% endfilter %}">>, [], <<"<br />">>} - ]}, - {"filters", [ - {"Filter a literal", - <<"{{ \"pop\"|capfirst }}">>, [], - <<"Pop">>}, - {"Filters applied in order", - <<"{{ var1|force_escape|length }}">>, [{var1, <<"&">>}], - <<"5">>}, - {"Escape is applied last", - <<"{{ var1|escape|linebreaksbr }}">>, [{var1, <<"\n">>}], - <<"<br />">>}, - {"add; lhs number, rhs number", - <<"{{ one|add:4}}">>, [{one, 1}], - <<"5">>}, - {"add; lhs numeric string, rhs number", - <<"{{ one|add:4}}">>, [{one, "1"}], - <<"5">>}, - {"add; lhs number, rhs numeric string", - <<"{{ one|add:'4'}}">>, [{one, 1}], - <<"5">>}, - {"add; lhs non-numeric string, rhs number", - <<"{{ one|add:4}}">>, [{one, "foo"}], - <<"foo4">>}, - {"add; lhs number, rhs non-numeric string", - <<"{{ one|add:'foo'}}">>, [{one, 1}], - <<"1foo">>}, - {"add; lhs non-numeric string, rhs non-numeric string", - <<"{{ one|add:'bar'}}">>, [{one, "foo"}], - <<"foobar">>}, - {"add; lhs numeric string, rhs numeric string", - <<"{{ one|add:'4'}}">>, [{one, "1"}], - <<"5">>}, - {"|addslashes", - <<"{{ var1|addslashes }}">>, [{var1, "Jimmy's \"great\" meats'n'things"}], - <<"Jimmy\\'s \\\"great\\\" meats\\'n\\'things">>}, - {"|capfirst", - <<"{{ var1|capfirst }}">>, [{var1, "dana boyd"}], - <<"Dana boyd">>}, - {"|center:10", - <<"{{ var1|center:10 }}">>, [{var1, "MB"}], - <<" MB ">>}, - {"|center:1", - <<"{{ var1|center:1 }}">>, [{var1, "KBR"}], - <<"B">>}, - {"|cut:\" \"", - <<"{{ var1|cut:\" \" }}">>, [{var1, "String with spaces"}], - <<"Stringwithspaces">>}, - {"|date 1", + {"if .. in ..", [ + {"If substring in string", + <<"{% if var1 in var2 %}yay{% endif %}">>, [{var1, "rook"}, {var2, "Crooks"}], <<"yay">>}, + {"If substring in string (false)", + <<"{% if var1 in var2 %}boo{% endif %}">>, [{var1, "Cook"}, {var2, "Crooks"}], <<>>}, + {"If substring not in string", + <<"{% if var1 not in var2 %}yay{% endif %}">>, [{var1, "Cook"}, {var2, "Crooks"}], <<"yay">>}, + {"If substring not in string (false)", + <<"{% if var1 not in var2 %}boo{% endif %}">>, [{var1, "rook"}, {var2, "Crooks"}], <<>>}, + {"If literal substring in string", + <<"{% if \"man\" in \"Ottoman\" %}yay{% endif %}">>, [], <<"yay">>}, + {"If literal substring in string (false)", + <<"{% if \"woman\" in \"Ottoman\" %}boo{% endif %}">>, [], <<>>}, + {"If element in list", + <<"{% if var1 in var2 %}yay{% endif %}">>, [{var1, "foo"}, {var2, ["bar", "foo", "baz"]}], <<"yay">>}, + {"If element in list (false)", + <<"{% if var1 in var2 %}boo{% endif %}">>, [{var1, "FOO"}, {var2, ["bar", "foo", "baz"]}], <<>>} + ]}, + {"if .. and ..", [ + {"If true and true", + <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, true}, {var2, true}], <<"yay">>}, + {"If true and false", + <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, true}, {var2, false}], <<"">>}, + {"If false and true", + <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, false}, {var2, true}], <<"">>}, + {"If false and false ", + <<"{% if var1 and var2 %}yay{% endif %}">>, [{var1, false}, {var2, false}], <<"">>} + ]}, + {"if .. or ..", [ + {"If true or true", + <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, true}, {var2, true}], <<"yay">>}, + {"If true or false", + <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, true}, {var2, false}], <<"yay">>}, + {"If false or true", + <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, false}, {var2, true}], <<"yay">>}, + {"If false or false ", + <<"{% if var1 or var2 %}yay{% endif %}">>, [{var1, false}, {var2, false}], <<"">>} + ]}, + {"if equality", [ + {"If int equals number literal", + <<"{% if var1 == 2 %}yay{% endif %}">>, [{var1, 2}], <<"yay">>}, + {"If int equals number literal (false)", + <<"{% if var1 == 2 %}yay{% endif %}">>, [{var1, 3}], <<"">>}, + {"If string equals string literal", + <<"{% if var1 == \"2\" %}yay{% endif %}">>, [{var1, "2"}], <<"yay">>}, + {"If string equals string literal (false)", + <<"{% if var1 == \"2\" %}yay{% endif %}">>, [{var1, "3"}], <<"">>}, + {"If int not equals number literal", + <<"{% if var1 != 2 %}yay{% endif %}">>, [{var1, 3}], <<"yay">>}, + {"If string not equals string literal", + <<"{% if var1 != \"2\" %}yay{% endif %}">>, [{var1, "3"}], <<"yay">>}, + {"If filter result equals number literal", + <<"{% if var1|length == 2 %}yay{% endif %}">>, [{var1, ["fo", "bo"]}], <<"yay">>}, + {"If filter result equals string literal", + <<"{% if var1|capfirst == \"Foo\" %}yay{% endif %}">>, [{var1, "foo"}], <<"yay">>} + ]}, + {"if size comparison", [ + {"If int greater than number literal", + <<"{% if var1 > 2 %}yay{% endif %}">>, [{var1, 3}], <<"yay">>}, + {"If int greater than negative number literal", + <<"{% if var1 > -2 %}yay{% endif %}">>, [{var1, -1}], <<"yay">>}, + {"If int greater than number literal (false)", + <<"{% if var1 > 2 %}yay{% endif %}">>, [{var1, 2}], <<"">>}, + + {"If int greater than or equal to number literal", + <<"{% if var1 >= 2 %}yay{% endif %}">>, [{var1, 3}], <<"yay">>}, + {"If int greater than or equal to number literal (2)", + <<"{% if var1 >= 2 %}yay{% endif %}">>, [{var1, 2}], <<"yay">>}, + {"If int greater than or equal to number literal (false)", + <<"{% if var1 >= 2 %}yay{% endif %}">>, [{var1, 1}], <<"">>}, + + {"If int less than number literal", + <<"{% if var1 < 2 %}yay{% endif %}">>, [{var1, 1}], <<"yay">>}, + {"If int less than number literal (false)", + <<"{% if var1 < 2 %}yay{% endif %}">>, [{var1, 2}], <<"">>}, + + {"If int less than or equal to number literal", + <<"{% if var1 <= 2 %}yay{% endif %}">>, [{var1, 1}], <<"yay">>}, + {"If int less than or equal to number literal", + <<"{% if var1 <= 2 %}yay{% endif %}">>, [{var1, 2}], <<"yay">>}, + {"If int less than or equal to number literal (false)", + <<"{% if var1 <= 2 %}yay{% endif %}">>, [{var1, 3}], <<"">>} + ]}, + {"if complex bool", [ + {"If (true or false) and true", + <<"{% if (var1 or var2) and var3 %}yay{% endif %}">>, + [{var1, true}, {var2, false}, {var3, true}], <<"yay">>}, + {"If true or (false and true)", + <<"{% if var1 or (var2 and var3) %}yay{% endif %}">>, + [{var1, true}, {var2, false}, {var3, true}], <<"yay">>} + ]}, + {"for", [ + {"Simple loop", + <<"{% for x in list %}{{ x }}{% endfor %}">>, [{'list', ["1", "2", "3"]}], + <<"123">>}, + {"Reversed loop", + <<"{% for x in list reversed %}{{ x }}{% endfor %}">>, [{'list', ["1", "2", "3"]}], + <<"321">>}, + {"Expand list", + <<"{% for x, y in list %}{{ x }},{{ y }}\n{% endfor %}">>, [{'list', [["X", "1"], ["X", "2"]]}], + <<"X,1\nX,2\n">>}, + {"Expand tuple", + <<"{% for x, y in list %}{{ x }},{{ y }}\n{% endfor %}">>, [{'list', [{"X", "1"}, {"X", "2"}]}], + <<"X,1\nX,2\n">>}, + {"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">>}, + {"Counter0", + <<"{% for number in numbers %}{{ forloop.counter0 }}. {{ number }}\n{% endfor %}">>, + [{numbers, ["Zero", "One", "Two"]}], <<"0. Zero\n1. One\n2. Two\n">>}, + {"Counter", + <<"{% for number in numbers %}{{ forloop.counter }}. {{ number }}\n{% endfor %}">>, + [{numbers, ["One", "Two", "Three"]}], <<"1. One\n2. Two\n3. Three\n">>}, + {"Reverse Counter0", + <<"{% for number in numbers %}{{ forloop.revcounter0 }}. {{ number }}\n{% endfor %}">>, + [{numbers, ["Two", "One", "Zero"]}], <<"2. Two\n1. One\n0. Zero\n">>}, + {"Reverse Counter", + <<"{% for number in numbers %}{{ forloop.revcounter }}. {{ number }}\n{% endfor %}">>, + [{numbers, ["Three", "Two", "One"]}], <<"3. Three\n2. Two\n1. One\n">>}, + {"Counter \"first\"", + <<"{% for number in numbers %}{% if forloop.first %}{{ number }}{% endif %}{% endfor %}">>, + [{numbers, ["One", "Two", "Three"]}], <<"One">>}, + {"Counter \"last\"", + <<"{% for number in numbers %}{% if forloop.last %}{{ number }}{% endif %}{% endfor %}">>, + [{numbers, ["One", "Two", "Three"]}], <<"Three">>}, + {"Nested for loop", + <<"{% for outer in list %}{% for inner in outer %}{{ inner }}\n{% endfor %}{% endfor %}">>, + [{'list', [["Al", "Albert"], ["Jo", "Joseph"]]}], + <<"Al\nAlbert\nJo\nJoseph\n">>}, + {"Access parent loop counters", + <<"{% for outer in list %}{% for inner in outer %}({{ forloop.parentloop.counter0 }}, {{ forloop.counter0 }})\n{% endfor %}{% endfor %}">>, + [{'list', [["One", "two"], ["One", "two"]]}], <<"(0, 0)\n(0, 1)\n(1, 0)\n(1, 1)\n">>}, + {"If changed", + <<"{% for x in list %}{% ifchanged %}{{ x }}\n{% endifchanged %}{% endfor %}">>, + [{'list', ["one", "two", "two", "three", "three", "three"]}], <<"one\ntwo\nthree\n">>}, + {"If changed/2", + <<"{% for x, y in list %}{% ifchanged %}{{ x|upper }}{% endifchanged %}{% ifchanged %}{{ y|lower }}{% endifchanged %}\n{% endfor %}">>, + [{'list', [["one", "a"], ["two", "A"], ["two", "B"], ["three", "b"], ["three", "c"], ["Three", "b"]]}], <<"ONEa\nTWO\nb\nTHREE\nc\nb\n">>}, + {"If changed/else", + <<"{% for x in list %}{% ifchanged %}{{ x }}\n{% else %}foo\n{% endifchanged %}{% endfor %}">>, + [{'list', ["one", "two", "two", "three", "three", "three"]}], <<"one\ntwo\nfoo\nthree\nfoo\nfoo\n">>}, + {"If changed/param", + <<"{% for date in list %}{% ifchanged date.month %} {{ date.month }}:{{ date.day }}{% else %},{{ date.day }}{% endifchanged %}{% endfor %}\n">>, + [{'list', [[{month,"Jan"},{day,1}],[{month,"Jan"},{day,2}],[{month,"Apr"},{day,10}], + [{month,"Apr"},{day,11}],[{month,"May"},{day,4}]]}], + <<" Jan:1,2 Apr:10,11 May:4\n">>}, + {"If changed/param2", + <<"{% for x, y in list %}{% ifchanged y|upper %}{{ x|upper }}{% endifchanged %}\n{% endfor %}">>, + [{'list', [["one", "a"], ["two", "A"], ["two", "B"], ["three", "b"], ["three", "c"], ["Three", "b"]]}], <<"ONE\n\nTWO\n\nTHREE\nTHREE\n">>}, + {"If changed/param2 combined", + <<"{% for x, y in list %}{% ifchanged x y|upper %}{{ x }}{% endifchanged %}\n{% endfor %}">>, + [{'list', [["one", "a"], ["two", "A"], ["two", "B"], ["three", "b"], ["three", "B"], ["three", "c"]]}], <<"one\ntwo\ntwo\nthree\n\nthree\n">>}, + {"If changed/resolve", + <<"{% for x in list %}{% ifchanged x.name|first %}{{ x.value }}{% endifchanged %}\n{% endfor %}">>, + [{'list', [[{"name", ["nA","nB"]},{"value","1"}],[{"name", ["nA","nC"]},{"value","2"}], + [{"name", ["nB","nC"]},{"value","3"}],[{"name", ["nB","nA"]},{"value","4"}]]}], + <<"1\n\n3\n\n">>} + ]}, + {"for/empty", [ + {"Simple loop", + <<"{% for x in list %}{{ x }}{% empty %}shucks{% endfor %}">>, [{'list', ["1", "2", "3"]}], + <<"123">>}, + {"Simple loop (empty)", + <<"{% for x in list %}{{ x }}{% empty %}shucks{% endfor %}">>, [{'list', []}], + <<"shucks">>} + ]}, + {"ifequal", [ + {"Compare variable to literal", + <<"{% ifequal var1 \"foo\" %}yay{% endifequal %}">>, + [{var1, "foo"}], <<"yay">>}, + {"Compare variable to unequal literal", + <<"{% ifequal var1 \"foo\" %}boo{% endifequal %}">>, + [{var1, "bar"}], <<>>}, + {"Compare literal to variable", + <<"{% ifequal \"foo\" var1 %}yay{% endifequal %}">>, + [{var1, "foo"}], <<"yay">>}, + {"Compare literal to unequal variable", + <<"{% ifequal \"foo\" var1 %}boo{% endifequal %}">>, + [{var1, "bar"}], <<>>}, + {"Compare variable to literal (int string)", + <<"{% ifequal var1 \"2\" %}yay{% else %}boo{% endifequal %}">>, + [{var1, "2"}], <<"yay">>}, + {"Compare variable to literal (int)", + <<"{% ifequal var1 2 %}yay{% else %}boo{% endifequal %}">>, + [{var1, 2}], <<"yay">>}, + {"Compare variable to unequal literal (int)", + <<"{% ifequal var1 2 %}boo{% else %}yay{% endifequal %}">>, + [{var1, 3}], <<"yay">>}, + {"Compare variable to equal literal (atom)", + <<"{% ifequal var1 \"foo\"%}yay{% endifequal %}">>, + [{var1, foo}], <<"yay">>}, + {"Compare variable to unequal literal (atom)", + <<"{% ifequal var1 \"foo\"%}yay{% else %}boo{% endifequal %}">>, + [{var1, bar}], <<"boo">>} + ]}, + {"ifequal/else", [ + {"Compare variable to literal", + <<"{% ifequal var1 \"foo\" %}yay{% else %}boo{% endifequal %}">>, + [{var1, "foo"}], <<"yay">>}, + {"Compare variable to unequal literal", + <<"{% ifequal var1 \"foo\" %}boo{% else %}yay{% endifequal %}">>, + [{var1, "bar"}], <<"yay">>}, + {"Compare literal to variable", + <<"{% ifequal \"foo\" var1 %}yay{% else %}boo{% endifequal %}">>, + [{var1, "foo"}], <<"yay">>}, + {"Compare literal to unequal variable", + <<"{% ifequal \"foo\" var1 %}boo{% else %}yay{% endifequal %}">>, + [{var1, "bar"}], <<"yay">>} + ]}, + {"ifnotequal", [ + {"Compare variable to literal", + <<"{% ifnotequal var1 \"foo\" %}boo{% endifnotequal %}">>, + [{var1, "foo"}], <<>>}, + {"Compare variable to unequal literal", + <<"{% ifnotequal var1 \"foo\" %}yay{% endifnotequal %}">>, + [{var1, "bar"}], <<"yay">>}, + {"Compare literal to variable", + <<"{% ifnotequal \"foo\" var1 %}boo{% endifnotequal %}">>, + [{var1, "foo"}], <<>>}, + {"Compare literal to unequal variable", + <<"{% ifnotequal \"foo\" var1 %}yay{% endifnotequal %}">>, + [{var1, "bar"}], <<"yay">>} + ]}, + {"ifnotequal/else", [ + {"Compare variable to literal", + <<"{% ifnotequal var1 \"foo\" %}boo{% else %}yay{% endifnotequal %}">>, + [{var1, "foo"}], <<"yay">>}, + {"Compare variable to unequal literal", + <<"{% ifnotequal var1 \"foo\" %}yay{% else %}boo{% endifnotequal %}">>, + [{var1, "bar"}], <<"yay">>}, + {"Compare literal to variable", + <<"{% ifnotequal \"foo\" var1 %}boo{% else %}yay{% endifnotequal %}">>, + [{var1, "foo"}], <<"yay">>}, + {"Compare literal to unequal variable", + <<"{% ifnotequal \"foo\" var1 %}yay{% else %}boo{% endifnotequal %}">>, + [{var1, "bar"}], <<"yay">>} + ]}, + {"filter tag", [ + {"Apply a filter", + <<"{% filter escape %}&{% endfilter %}">>, [], <<"&">>}, + {"Chained filters", + <<"{% filter linebreaksbr|escape %}\n{% endfilter %}">>, [], <<"<br />">>} + ]}, + {"filters", [ + {"Filter a literal", + <<"{{ \"pop\"|capfirst }}">>, [], + <<"Pop">>}, + {"Filters applied in order", + <<"{{ var1|force_escape|length }}">>, [{var1, <<"&">>}], + <<"5">>}, + {"Escape is applied last", + <<"{{ var1|escape|linebreaksbr }}">>, [{var1, <<"\n">>}], + <<"<br />">>}, + {"add; lhs number, rhs number", + <<"{{ one|add:4}}">>, [{one, 1}], + <<"5">>}, + {"add; lhs numeric string, rhs number", + <<"{{ one|add:4}}">>, [{one, "1"}], + <<"5">>}, + {"add; lhs number, rhs numeric string", + <<"{{ one|add:'4'}}">>, [{one, 1}], + <<"5">>}, + {"add; lhs non-numeric string, rhs number", + <<"{{ one|add:4}}">>, [{one, "foo"}], + <<"foo4">>}, + {"add; lhs number, rhs non-numeric string", + <<"{{ one|add:'foo'}}">>, [{one, 1}], + <<"1foo">>}, + {"add; lhs non-numeric string, rhs non-numeric string", + <<"{{ one|add:'bar'}}">>, [{one, "foo"}], + <<"foobar">>}, + {"add; lhs numeric string, rhs numeric string", + <<"{{ one|add:'4'}}">>, [{one, "1"}], + <<"5">>}, + {"|addslashes", + <<"{{ var1|addslashes }}">>, [{var1, "Jimmy's \"great\" meats'n'things"}], + <<"Jimmy\\'s \\\"great\\\" meats\\'n\\'things">>}, + {"|capfirst", + <<"{{ var1|capfirst }}">>, [{var1, "dana boyd"}], + <<"Dana boyd">>}, + {"|center:10", + <<"{{ var1|center:10 }}">>, [{var1, "MB"}], + <<" MB ">>}, + {"|center:1", + <<"{{ var1|center:1 }}">>, [{var1, "KBR"}], + <<"B">>}, + {"|cut:\" \"", + <<"{{ var1|cut:\" \" }}">>, [{var1, "String with spaces"}], + <<"Stringwithspaces">>}, + {"|date 1", <<"{{ var1|date:\"jS F Y H:i\" }}">>, [{var1, {1975,7,24}}], <<"24th July 1975 00:00">>}, - {"|date 2", + {"|date 2", <<"{{ var1|date:\"jS F Y H:i\" }}">>, [{var1, {{1975,7,24}, {7,13,1}}}], <<"24th July 1975 07:13">>}, - {"|date 3", + {"|date 3", <<"{{ var1|date }}">>, [{var1, {{1975,7,24}, {7,13,1}}}], <<"July 24, 1975">>}, - {"|default:\"foo\" 1", + {"|default:\"foo\" 1", <<"{{ var1|default:\"foo\" }}">>, [], <<"foo">>}, - {"|default:\"foo\" 2", - <<"{{ var1|default:\"foo\" }}">>, [{var1, "bar"}], <<"bar">>}, - {"|default:\"foo\" 3", - <<"{{ var1|default:\"foo\" }}">>, [{var1, "0"}], <<"foo">>}, - {"|default_if_none:\"foo\"", + {"|default:\"foo\" 2", + <<"{{ var1|default:\"foo\" }}">>, [{var1, "bar"}], <<"bar">>}, + {"|default:\"foo\" 3", + <<"{{ var1|default:\"foo\" }}">>, [{var1, "0"}], <<"foo">>}, + {"|default_if_none:\"foo\"", <<"{{ var1|default_if_none:\"foo\" }}">>, [], <<"foo">>}, - {"|default_if_none:\"foo\" 2", - <<"{{ var1|default_if_none:\"foo\" }}">>, [{var1, "bar"}], <<"bar">>}, - {"|dictsort 1", - <<"{{ var1|dictsort:\"foo\" }}">>, - [{var1,[[{foo,2}],[{foo,1}]]}], <<"{foo,1}{foo,2}">>}, - {"|dictsort 2", - <<"{{ var1|dictsort:\"foo.bar\" }}">>, - [{var1,[[{foo,[{bar,2}]}],[{foo,[{bar,1}]}]]}], - <<"{foo,[{bar,1}]}{foo,[{bar,2}]}">>}, - {"|divisibleby:\"3\"", - <<"{% if var1|divisibleby:\"3\" %}yay{% endif %}">>, [{var1, 21}], <<"yay">>}, - {"|divisibleby:\"3\"", - <<"{% if var1|divisibleby:\"3\" %}yay{% endif %}">>, [{var1, 22}], <<"">>}, - {"|escape", - <<"{% autoescape on %}{{ var1|escape|escape|escape }}{% endautoescape %}">>, [{var1, ">&1"}], <<">&1">>}, - {"|escapejs", - <<"{{ var1|escapejs }}">>, [{var1, "testing\r\njavascript 'string\" escaping"}], - <<"testing\\u000D\\u000Ajavascript \\u0027string\\u0022 \\u003Cb\\u003Eescaping\\u003C/b\\u003E">>}, - {"|filesizeformat (bytes)", - <<"{{ var1|filesizeformat }}">>, [{var1, 1023}], <<"1023 bytes">>}, - {"|filesizeformat (KB)", - <<"{{ var1|filesizeformat }}">>, [{var1, 3487}], <<"3.4 KB">>}, - {"|filesizeformat (MB)", - <<"{{ var1|filesizeformat }}">>, [{var1, 6277098}], <<"6.0 MB">>}, - {"|filesizeformat (GB)", - <<"{{ var1|filesizeformat }}">>, [{var1, 1024 * 1024 * 1024}], <<"1.0 GB">>}, - {"|first", - <<"{{ var1|first }}">>, [{var1, "James"}], - <<"J">>}, - {"|fix_ampersands", - <<"{{ var1|fix_ampersands }}">>, [{var1, "Ben & Jerry's"}], - <<"Ben & Jerry's">>}, - - {"|floatformat:\"-1\"", - <<"{{ var1|floatformat:\"-1\" }}">>, [{var1, 34.23234}], - <<"34.2">>}, -%% ?assertEqual( "", erlydtl_filters:floatformat(,)), -%% ?assertEqual( "34", erlydtl_filters:floatformat(34.00000,-1)), -%% ?assertEqual( "34.3", erlydtl_filters:floatformat(34.26000,-1)), -%% ?assertEqual( "34.232", erlydtl_filters:floatformat(34.23234,3)), -%% ?assertEqual( "34.000", erlydtl_filters:floatformat(34.00000,3)), -%% ?assertEqual( "34.260", erlydtl_filters:floatformat(34.26000,3)), -%% ?assertEqual( "34.232", erlydtl_filters:floatformat(34.23234,-3)), -%% ?assertEqual( "34", erlydtl_filters:floatformat(34.00000,-3)), -%% ?assertEqual( "34.260", erlydtl_filters:floatformat(34.26000,-3)). - {"|force_escape", - <<"{{ var1|force_escape }}">>, [{var1, "Ben & Jerry's <=> \"The World's Best Ice Cream\""}], - <<"Ben & Jerry's <=> "The World's Best Ice Cream"">>}, - {"|format_integer", - <<"{{ var1|format_integer }}">>, [{var1, 28}], <<"28">>}, - {"|format_number 1", - <<"{{ var1|format_number }}">>, [{var1, 28}], <<"28">>}, - {"|format_number 2", - <<"{{ var1|format_number }}">>, [{var1, 23.77}], <<"23.77">>}, - {"|format_number 3", - <<"{{ var1|format_number }}">>, [{var1, "28.77"}], <<"28.77">>}, - {"|format_number 4", - <<"{{ var1|format_number }}">>, [{var1, "23.77"}], <<"23.77">>}, - {"|format_number 5", - <<"{{ var1|format_number }}">>, [{var1, fun() -> 29 end}], <<"29">>}, - {"|format_number 6", - <<"{{ var1|format_number }}">>, [{var1, fun() -> fun() -> 31 end end}], <<"31">>}, - {"|get_digit:\"2\"", - <<"{{ var1|get_digit:\"2\" }}">>, [{var1, 42}], <<"4">>}, - {"|iriencode", - <<"{{ url|iriencode }}">>, [{url, "You #$*@!!"}], <<"You+#$*@!!">>}, - {"|join:\", \" (list)", - <<"{{ var1|join:\", \" }}">>, [{var1, ["Liberte", "Egalite", "Fraternite"]}], - <<"Liberte, Egalite, Fraternite">>}, - {"|join:\", \" (binary)", - <<"{{ var1|join:\", \" }}">>, [{var1, [<<"Liberte">>, "Egalite", <<"Fraternite">>]}], - <<"Liberte, Egalite, Fraternite">>}, - {"|last", - <<"{{ var1|last }}">>, [{var1, "XYZ"}], - <<"Z">>}, - {"|length", - <<"{{ var1|length }}">>, [{var1, "antidisestablishmentarianism"}], - <<"28">>}, - {"|linebreaks", - <<"{{ var1|linebreaks }}">>, [{var1, "Joel\nis a slug"}], - <<"

Joel
is a slug

">>}, - {"|linebreaks", - <<"{{ var1|linebreaks }}">>, [{var1, "Joel\n\n\n\nis a slug"}], - <<"

Joel

is a slug

">>}, - {"|linebreaks", - <<"{{ var1|linebreaks }}">>, [{var1, "Joel\n\nis a \nslug"}], - <<"

Joel

is a
slug

">>}, - {"|linebreaksbr", - <<"{{ var1|linebreaksbr }}">>, [{var1, "One\nTwo\n\nThree\n\n\n"}], - <<"One
Two

Three


">>}, - {"|linebreaksbr", - <<"{{ \"One\\nTwo\\n\\nThree\\n\\n\\n\"|linebreaksbr }}">>, [], - <<"One
Two

Three


">>}, - {"|linenumbers", - <<"{{ var1|linenumbers }}">>, [{var1, "a\nb\nc"}], - <<"1. a\n2. b\n3. c">>}, - {"|linenumbers", - <<"{{ var1|linenumbers }}">>, [{var1, "a"}], - <<"1. a">>}, - {"|linenumbers", - <<"{{ var1|linenumbers }}">>, [{var1, "a\n"}], - <<"1. a\n2. ">>}, - {"|ljust:10", - <<"{{ var1|ljust:10 }}">>, [{var1, "Gore"}], - <<"Gore ">>}, - {"|lower", - <<"{{ var1|lower }}">>, [{var1, "E. E. Cummings"}], - <<"e. e. cummings">>}, - {"|makelist", - <<"{{ list|make_list }}">>, [{list, "Joel"}], - <<"J","o","e","l">>}, - {"|pluralize", - <<"{{ num|pluralize }}">>, [{num, 1}], - <<"">>}, - {"|pluralize", - <<"{{ num|pluralize }}">>, [{num, 2}], - <<"s">>}, - {"|pluralize:\"s\"", - <<"{{ num|pluralize }}">>, [{num, 1}], - <<"">>}, - {"|pluralize:\"s\"", - <<"{{ num|pluralize }}">>, [{num, 2}], - <<"s">>}, - {"|pluralize:\"y,es\" (list)", - <<"{{ num|pluralize:\"y,es\" }}">>, [{num, 1}], - <<"y">>}, - {"|pluralize:\"y,es\" (list)", - <<"{{ num|pluralize:\"y,es\" }}">>, [{num, 2}], - <<"es">>}, - {"|random", - <<"{{ var1|random }}">>, [{var1, ["foo", "foo", "foo"]}], - <<"foo">>}, - {"|removetags:\"b span\"", - <<"{{ var1|removetags:\"b span\" }}">>, [{var1, "Joel a slug"}], - <<"Joel a slug">>}, - {"|rjust:10", - <<"{{ var1|rjust:10 }}">>, [{var1, "Bush"}], - <<" Bush">>}, - {"|safe", - <<"{% autoescape on %}{{ var1|safe|escape }}{% endautoescape %}">>, [{var1, "&"}], - <<"&">>}, - %%python/django slice is zero based, erlang lists are 1 based - %%first number included, second number not - %%negative numbers are allowed - %%regex to convert from erlydtl_filters_tests: - % for slice: \?assert.*\( \[(.*)\], erlydtl_filters:(.*)\((.*),"(.*)"\)\), - % {"|slice:\"$4\"", <<"{{ var|$2:\"$4\" }}">>, [{var, $3}],<<$1>>}, - % \t\t{"|slice:\"$4\"",\n\t\t\t\t\t <<"{{ var|$2:\"$4\" }}">>, [{var, $3}],\n\t\t\t\t\t<<$1>>}, - % - % for stringformat: - % \?assert.*\( (.*), erlydtl_filters:(.*)\((.*), "(.*)"\) \) - % \t\t{"|stringformat:\"$4\"",\n\t\t\t\t\t <<"{{ var|$2:\"$4\" }}">>, [{var, $3}],\n\t\t\t\t\t<<$1>>} - - {"|slice:\":\"", - <<"{{ var|slice:\":\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<1,2,3,4,5,6,7,8,9>>}, - {"|slice:\"1\"", - <<"{{ var|slice:\"1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<"2">>}, - {"|slice:\"100\"", - <<"{{ var|slice:\"100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<"indexError">>}, - {"|slice:\"-1\"", - <<"{{ var|slice:\"-1\" }}">>, [{var, ["a","b","c","d","e","f","g","h","i"]}], - <<"i">>}, - {"|slice:\"-1\"", - <<"{{ var|slice:\"-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<"9">>}, - {"|slice:\"-100\"", - <<"{{ var|slice:\"-100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<"indexError">>}, - {"|slice:\"1:\"", - <<"{{ var|slice:\"1:\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<2,3,4,5,6,7,8,9>>}, - {"|slice:\"100:\"", - <<"{{ var|slice:\"100:\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"-1:\"", - <<"{{ var|slice:\"-1:\" }}">>, [{var, ["a","b","c","d","e","f","h","i","j"]}], - <<"j">>}, - {"|slice:\"-1:\"", - <<"{{ var|slice:\"-1:\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<9>>}, - {"|slice:\"-100:\"", - <<"{{ var|slice:\"-100:\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<1,2,3,4,5,6,7,8,9>>}, - - {"|slice:\":1\"", - <<"{{ var|slice:\":1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<1>>}, - {"|slice:\":100\"", - <<"{{ var|slice:\":100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<1,2,3,4,5,6,7,8,9>>}, - {"|slice:\":-1\"", - <<"{{ var|slice:\":-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<1,2,3,4,5,6,7,8>>}, - {"|slice:\":-100\"", - <<"{{ var|slice:\":-100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - - {"|slice:\"-1:-1\"", - <<"{{ var|slice:\"-1:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"1:1\"", - <<"{{ var|slice:\"1:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"1:-1\"", - <<"{{ var|slice:\"1:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<2,3,4,5,6,7,8>>}, - {"|slice:\"-1:1\"", - <<"{{ var|slice:\"-1:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - - {"|slice:\"-100:-100\"", - <<"{{ var|slice:\"-100:-100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"100:100\"", - <<"{{ var|slice:\"100:100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"100:-100\"", - <<"{{ var|slice:\"100:-100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"-100:100\"", - <<"{{ var|slice:\"-100:100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<1,2,3,4,5,6,7,8,9>>}, - - - {"|slice:\"1:3\"", - <<"{{ var|slice:\"1:3\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<2,3>>}, - - {"|slice:\"::\"", - <<"{{ var|slice:\"::\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<1,2,3,4,5,6,7,8,9>>}, - {"|slice:\"1:9:1\"", - <<"{{ var|slice:\"1:9:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<2,3,4,5,6,7,8,9>>}, - {"|slice:\"10:1:-1\"", - <<"{{ var|slice:\"10:1:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<9,8,7,6,5,4,3>>}, - {"|slice:\"-111:-1:1\"", - <<"{{ var|slice:\"-111:-1:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<1,2,3,4,5,6,7,8>>}, - - {"|slice:\"-111:-111:1\"", - <<"{{ var|slice:\"-111:-111:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"111:111:1\"", - <<"{{ var|slice:\"111:111:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"-111:111:1\"", - <<"{{ var|slice:\"-111:111:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<1,2,3,4,5,6,7,8,9>>}, - {"|slice:\"111:-111:1\"", - <<"{{ var|slice:\"111:-111:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - - {"|slice:\"-111:-111:-1\"", - <<"{{ var|slice:\"-111:-111:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"111:111:-1\"", - <<"{{ var|slice:\"111:111:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"-111:111:-1\"", - <<"{{ var|slice:\"-111:111:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<>>}, - {"|slice:\"111:-111:-1\"", - <<"{{ var|slice:\"111:-111:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], - <<9,8,7,6,5,4,3,2,1>>}, {"|phone2numeric", - <<"{{ var1|phone2numeric }}">>, [{var1, "1-800-COLLECT"}], - <<"1-800-2655328">>}, - {"|slugify", - <<"{{ var1|slugify }}">>, [{var1, "What The $#_! Was He Thinking?"}], - <<"what-the-_-was-he-thinking">>}, - {"|slice:\"s\"", - <<"{{ var|stringformat:\"s\" }}">>, [{var, "test"}], - <<"test">>}, - {"|stringformat:\"s\"", - <<"{{ var|stringformat:\"s\" }}">>, [{var, "test"}], - <<"test">>}, - {"|stringformat:\"s\"", - <<"{{ var|stringformat:\"s\" }}">>, [{var, "1"}], - <<"1">>}, - {"|stringformat:\"s\"", - <<"{{ var|stringformat:\"s\" }}">>, [{var, "test"}], - <<"test">>}, - {"|stringformat:\"10s\"", - <<"{{ var|stringformat:\"10s\" }}">>, [{var, "test"}], - <<" test">>}, - {"|stringformat:\"-10s\"", - <<"{{ var|stringformat:\"-10s\" }}">>, [{var, "test"}], - <<"test ">>}, - - {"|stringformat:\"d\"", - <<"{{ var|stringformat:\"d\" }}">>, [{var, "90"}], - <<"90">>}, - {"|stringformat:\"10d\"", - <<"{{ var|stringformat:\"10d\" }}">>, [{var, "90"}], - <<" 90">>}, - {"|stringformat:\"-10d\"", - <<"{{ var|stringformat:\"-10d\" }}">>, [{var, "90"}], - <<"90 ">>}, - {"|stringformat:\"i\"", - <<"{{ var|stringformat:\"i\" }}">>, [{var, "90"}], - <<"90">>}, - {"|stringformat:\"10i\"", - <<"{{ var|stringformat:\"10i\" }}">>, [{var, "90"}], - <<" 90">>}, - {"|stringformat:\"-10i\"", - <<"{{ var|stringformat:\"-10i\" }}">>, [{var, "90"}], - <<"90 ">>}, - {"|stringformat:\"0.2d\"", - <<"{{ var|stringformat:\"0.2d\" }}">>, [{var, "9"}], - <<"09">>}, - {"|stringformat:\"10.4d\"", - <<"{{ var|stringformat:\"10.4d\" }}">>, [{var, "9"}], - <<" 0009">>}, - {"|stringformat:\"-10.4d\"", - <<"{{ var|stringformat:\"-10.4d\" }}">>, [{var, "9"}], - <<"0009 ">>}, + {"|default_if_none:\"foo\" 2", + <<"{{ var1|default_if_none:\"foo\" }}">>, [{var1, "bar"}], <<"bar">>}, + {"|dictsort 1", + <<"{{ var1|dictsort:\"foo\" }}">>, + [{var1,[[{foo,2}],[{foo,1}]]}], <<"{foo,1}{foo,2}">>}, + {"|dictsort 2", + <<"{{ var1|dictsort:\"foo.bar\" }}">>, + [{var1,[[{foo,[{bar,2}]}],[{foo,[{bar,1}]}]]}], + <<"{foo,[{bar,1}]}{foo,[{bar,2}]}">>}, + {"|divisibleby:\"3\"", + <<"{% if var1|divisibleby:\"3\" %}yay{% endif %}">>, [{var1, 21}], <<"yay">>}, + {"|divisibleby:\"3\"", + <<"{% if var1|divisibleby:\"3\" %}yay{% endif %}">>, [{var1, 22}], <<"">>}, + {"|escape", + <<"{% autoescape on %}{{ var1|escape|escape|escape }}{% endautoescape %}">>, [{var1, ">&1"}], <<">&1">>}, + {"|escapejs", + <<"{{ var1|escapejs }}">>, [{var1, "testing\r\njavascript 'string\" escaping"}], + <<"testing\\u000D\\u000Ajavascript \\u0027string\\u0022 \\u003Cb\\u003Eescaping\\u003C/b\\u003E">>}, + {"|filesizeformat (bytes)", + <<"{{ var1|filesizeformat }}">>, [{var1, 1023}], <<"1023 bytes">>}, + {"|filesizeformat (KB)", + <<"{{ var1|filesizeformat }}">>, [{var1, 3487}], <<"3.4 KB">>}, + {"|filesizeformat (MB)", + <<"{{ var1|filesizeformat }}">>, [{var1, 6277098}], <<"6.0 MB">>}, + {"|filesizeformat (GB)", + <<"{{ var1|filesizeformat }}">>, [{var1, 1024 * 1024 * 1024}], <<"1.0 GB">>}, + {"|first", + <<"{{ var1|first }}">>, [{var1, "James"}], + <<"J">>}, + {"|fix_ampersands", + <<"{{ var1|fix_ampersands }}">>, [{var1, "Ben & Jerry's"}], + <<"Ben & Jerry's">>}, + + {"|floatformat:\"-1\"", + <<"{{ var1|floatformat:\"-1\" }}">>, [{var1, 34.23234}], + <<"34.2">>}, + %% ?assertEqual( "", erlydtl_filters:floatformat(,)), + %% ?assertEqual( "34", erlydtl_filters:floatformat(34.00000,-1)), + %% ?assertEqual( "34.3", erlydtl_filters:floatformat(34.26000,-1)), + %% ?assertEqual( "34.232", erlydtl_filters:floatformat(34.23234,3)), + %% ?assertEqual( "34.000", erlydtl_filters:floatformat(34.00000,3)), + %% ?assertEqual( "34.260", erlydtl_filters:floatformat(34.26000,3)), + %% ?assertEqual( "34.232", erlydtl_filters:floatformat(34.23234,-3)), + %% ?assertEqual( "34", erlydtl_filters:floatformat(34.00000,-3)), + %% ?assertEqual( "34.260", erlydtl_filters:floatformat(34.26000,-3)). + {"|force_escape", + <<"{{ var1|force_escape }}">>, [{var1, "Ben & Jerry's <=> \"The World's Best Ice Cream\""}], + <<"Ben & Jerry's <=> "The World's Best Ice Cream"">>}, + {"|format_integer", + <<"{{ var1|format_integer }}">>, [{var1, 28}], <<"28">>}, + {"|format_number 1", + <<"{{ var1|format_number }}">>, [{var1, 28}], <<"28">>}, + {"|format_number 2", + <<"{{ var1|format_number }}">>, [{var1, 23.77}], <<"23.77">>}, + {"|format_number 3", + <<"{{ var1|format_number }}">>, [{var1, "28.77"}], <<"28.77">>}, + {"|format_number 4", + <<"{{ var1|format_number }}">>, [{var1, "23.77"}], <<"23.77">>}, + {"|format_number 5", + <<"{{ var1|format_number }}">>, [{var1, fun() -> 29 end}], <<"29">>}, + {"|format_number 6", + <<"{{ var1|format_number }}">>, [{var1, fun() -> fun() -> 31 end end}], <<"31">>}, + {"|get_digit:\"2\"", + <<"{{ var1|get_digit:\"2\" }}">>, [{var1, 42}], <<"4">>}, + {"|iriencode", + <<"{{ url|iriencode }}">>, [{url, "You #$*@!!"}], <<"You+#$*@!!">>}, + {"|join:\", \" (list)", + <<"{{ var1|join:\", \" }}">>, [{var1, ["Liberte", "Egalite", "Fraternite"]}], + <<"Liberte, Egalite, Fraternite">>}, + {"|join:\", \" (binary)", + <<"{{ var1|join:\", \" }}">>, [{var1, [<<"Liberte">>, "Egalite", <<"Fraternite">>]}], + <<"Liberte, Egalite, Fraternite">>}, + {"|last", + <<"{{ var1|last }}">>, [{var1, "XYZ"}], + <<"Z">>}, + {"|length", + <<"{{ var1|length }}">>, [{var1, "antidisestablishmentarianism"}], + <<"28">>}, + {"|linebreaks", + <<"{{ var1|linebreaks }}">>, [{var1, "Joel\nis a slug"}], + <<"

Joel
is a slug

">>}, + {"|linebreaks", + <<"{{ var1|linebreaks }}">>, [{var1, "Joel\n\n\n\nis a slug"}], + <<"

Joel

is a slug

">>}, + {"|linebreaks", + <<"{{ var1|linebreaks }}">>, [{var1, "Joel\n\nis a \nslug"}], + <<"

Joel

is a
slug

">>}, + {"|linebreaksbr", + <<"{{ var1|linebreaksbr }}">>, [{var1, "One\nTwo\n\nThree\n\n\n"}], + <<"One
Two

Three


">>}, + {"|linebreaksbr", + <<"{{ \"One\\nTwo\\n\\nThree\\n\\n\\n\"|linebreaksbr }}">>, [], + <<"One
Two

Three


">>}, + {"|linenumbers", + <<"{{ var1|linenumbers }}">>, [{var1, "a\nb\nc"}], + <<"1. a\n2. b\n3. c">>}, + {"|linenumbers", + <<"{{ var1|linenumbers }}">>, [{var1, "a"}], + <<"1. a">>}, + {"|linenumbers", + <<"{{ var1|linenumbers }}">>, [{var1, "a\n"}], + <<"1. a\n2. ">>}, + {"|ljust:10", + <<"{{ var1|ljust:10 }}">>, [{var1, "Gore"}], + <<"Gore ">>}, + {"|lower", + <<"{{ var1|lower }}">>, [{var1, "E. E. Cummings"}], + <<"e. e. cummings">>}, + {"|makelist", + <<"{{ list|make_list }}">>, [{list, "Joel"}], + <<"J","o","e","l">>}, + {"|pluralize", + <<"{{ num|pluralize }}">>, [{num, 1}], + <<"">>}, + {"|pluralize", + <<"{{ num|pluralize }}">>, [{num, 2}], + <<"s">>}, + {"|pluralize:\"s\"", + <<"{{ num|pluralize }}">>, [{num, 1}], + <<"">>}, + {"|pluralize:\"s\"", + <<"{{ num|pluralize }}">>, [{num, 2}], + <<"s">>}, + {"|pluralize:\"y,es\" (list)", + <<"{{ num|pluralize:\"y,es\" }}">>, [{num, 1}], + <<"y">>}, + {"|pluralize:\"y,es\" (list)", + <<"{{ num|pluralize:\"y,es\" }}">>, [{num, 2}], + <<"es">>}, + {"|random", + <<"{{ var1|random }}">>, [{var1, ["foo", "foo", "foo"]}], + <<"foo">>}, + {"|removetags:\"b span\"", + <<"{{ var1|removetags:\"b span\" }}">>, [{var1, "Joel a slug"}], + <<"Joel a slug">>}, + {"|rjust:10", + <<"{{ var1|rjust:10 }}">>, [{var1, "Bush"}], + <<" Bush">>}, + {"|safe", + <<"{% autoescape on %}{{ var1|safe|escape }}{% endautoescape %}">>, [{var1, "&"}], + <<"&">>}, + %%python/django slice is zero based, erlang lists are 1 based + %%first number included, second number not + %%negative numbers are allowed + %%regex to convert from erlydtl_filters_tests: + % for slice: \?assert.*\( \[(.*)\], erlydtl_filters:(.*)\((.*),"(.*)"\)\), + % {"|slice:\"$4\"", <<"{{ var|$2:\"$4\" }}">>, [{var, $3}],<<$1>>}, + % \t\t{"|slice:\"$4\"",\n\t\t\t\t\t <<"{{ var|$2:\"$4\" }}">>, [{var, $3}],\n\t\t\t\t\t<<$1>>}, + % + % for stringformat: + % \?assert.*\( (.*), erlydtl_filters:(.*)\((.*), "(.*)"\) \) + % \t\t{"|stringformat:\"$4\"",\n\t\t\t\t\t <<"{{ var|$2:\"$4\" }}">>, [{var, $3}],\n\t\t\t\t\t<<$1>>} + + {"|slice:\":\"", + <<"{{ var|slice:\":\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<1,2,3,4,5,6,7,8,9>>}, + {"|slice:\"1\"", + <<"{{ var|slice:\"1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<"2">>}, + {"|slice:\"100\"", + <<"{{ var|slice:\"100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<"indexError">>}, + {"|slice:\"-1\"", + <<"{{ var|slice:\"-1\" }}">>, [{var, ["a","b","c","d","e","f","g","h","i"]}], + <<"i">>}, + {"|slice:\"-1\"", + <<"{{ var|slice:\"-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<"9">>}, + {"|slice:\"-100\"", + <<"{{ var|slice:\"-100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<"indexError">>}, + {"|slice:\"1:\"", + <<"{{ var|slice:\"1:\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<2,3,4,5,6,7,8,9>>}, + {"|slice:\"100:\"", + <<"{{ var|slice:\"100:\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"-1:\"", + <<"{{ var|slice:\"-1:\" }}">>, [{var, ["a","b","c","d","e","f","h","i","j"]}], + <<"j">>}, + {"|slice:\"-1:\"", + <<"{{ var|slice:\"-1:\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<9>>}, + {"|slice:\"-100:\"", + <<"{{ var|slice:\"-100:\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<1,2,3,4,5,6,7,8,9>>}, + + {"|slice:\":1\"", + <<"{{ var|slice:\":1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<1>>}, + {"|slice:\":100\"", + <<"{{ var|slice:\":100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<1,2,3,4,5,6,7,8,9>>}, + {"|slice:\":-1\"", + <<"{{ var|slice:\":-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<1,2,3,4,5,6,7,8>>}, + {"|slice:\":-100\"", + <<"{{ var|slice:\":-100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + + {"|slice:\"-1:-1\"", + <<"{{ var|slice:\"-1:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"1:1\"", + <<"{{ var|slice:\"1:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"1:-1\"", + <<"{{ var|slice:\"1:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<2,3,4,5,6,7,8>>}, + {"|slice:\"-1:1\"", + <<"{{ var|slice:\"-1:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + + {"|slice:\"-100:-100\"", + <<"{{ var|slice:\"-100:-100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"100:100\"", + <<"{{ var|slice:\"100:100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"100:-100\"", + <<"{{ var|slice:\"100:-100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"-100:100\"", + <<"{{ var|slice:\"-100:100\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<1,2,3,4,5,6,7,8,9>>}, + + + {"|slice:\"1:3\"", + <<"{{ var|slice:\"1:3\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<2,3>>}, + + {"|slice:\"::\"", + <<"{{ var|slice:\"::\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<1,2,3,4,5,6,7,8,9>>}, + {"|slice:\"1:9:1\"", + <<"{{ var|slice:\"1:9:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<2,3,4,5,6,7,8,9>>}, + {"|slice:\"10:1:-1\"", + <<"{{ var|slice:\"10:1:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<9,8,7,6,5,4,3>>}, + {"|slice:\"-111:-1:1\"", + <<"{{ var|slice:\"-111:-1:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<1,2,3,4,5,6,7,8>>}, + + {"|slice:\"-111:-111:1\"", + <<"{{ var|slice:\"-111:-111:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"111:111:1\"", + <<"{{ var|slice:\"111:111:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"-111:111:1\"", + <<"{{ var|slice:\"-111:111:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<1,2,3,4,5,6,7,8,9>>}, + {"|slice:\"111:-111:1\"", + <<"{{ var|slice:\"111:-111:1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, - {"|stringformat:\"f\"", - <<"{{ var|stringformat:\"f\" }}">>, [{var, "1"}], - <<"1.000000">>}, - {"|stringformat:\".2f\"", - <<"{{ var|stringformat:\".2f\" }}">>, [{var, "1"}], - <<"1.00">>}, - {"|stringformat:\"0.2f\"", - <<"{{ var|stringformat:\"0.2f\" }}">>, [{var, "1"}], - <<"1.00">>}, - {"|stringformat:\"-0.2f\"", - <<"{{ var|stringformat:\"-0.2f\" }}">>, [{var, "1"}], - <<"1.00">>}, - {"|stringformat:\"10.2f\"", - <<"{{ var|stringformat:\"10.2f\" }}">>, [{var, "1"}], - <<" 1.00">>}, - {"|stringformat:\"-10.2f\"", - <<"{{ var|stringformat:\"-10.2f\" }}">>, [{var, "1"}], - <<"1.00 ">>}, - {"|stringformat:\".2f\"", - <<"{{ var|stringformat:\".2f\" }}">>, [{var, "1"}], - <<"1.00">>}, - {"|stringformat:\"x\"", - <<"{{ var|stringformat:\"x\" }}">>, [{var, "90"}], - <<"5a">>}, - {"|stringformat:\"X\"", - <<"{{ var|stringformat:\"X\" }}">>, [{var, "90"}], - <<"5A">>}, - - {"|stringformat:\"o\"", - <<"{{ var|stringformat:\"o\" }}">>, [{var, "90"}], - <<"132">>}, - - {"|stringformat:\"e\"", - <<"{{ var|stringformat:\"e\" }}">>, [{var, "90"}], - <<"9.000000e+01">>}, - {"|stringformat:\"e\"", - <<"{{ var|stringformat:\"e\" }}">>, [{var, "90000000000"}], - <<"9.000000e+10">>}, - {"|stringformat:\"E\"", - <<"{{ var|stringformat:\"E\" }}">>, [{var, "90"}], - <<"9.000000E+01">>}, - {"|striptags", - <<"{{ var|striptags }}">>, [{var, "Joel a slug"}], - <<"Joel is a slug">>}, - {"|striptags", - <<"{{ var|striptags }}">>, [{var, "Joel a slug"}], - <<"Joel is a slug">>}, - {"|striptags", - <<"{{ var|striptags }}">>, [{var, "Check out
http://www.djangoproject.com"}], - <<"Check out http://www.djangoproject.com">>}, - {"|time:\"H:i\"", - <<"{{ var|time:\"H:i\" }}">>, [{var, {{2010,12,1}, {10,11,12}} }], - <<"10:11">>}, - {"|time", - <<"{{ var|time }}">>, [{var, {{2010,12,1}, {10,11,12}} }], - <<"10:11 a.m.">>}, - {"|timesince:from_date", - <<"{{ from_date|timesince:conference_date }}">>, [{conference_date, {{2006,6,1},{8,0,0}} }, {from_date, {{2006,6,1},{0,0,0}} }], + {"|slice:\"-111:-111:-1\"", + <<"{{ var|slice:\"-111:-111:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"111:111:-1\"", + <<"{{ var|slice:\"111:111:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"-111:111:-1\"", + <<"{{ var|slice:\"-111:111:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<>>}, + {"|slice:\"111:-111:-1\"", + <<"{{ var|slice:\"111:-111:-1\" }}">>, [{var, [1,2,3,4,5,6,7,8,9]}], + <<9,8,7,6,5,4,3,2,1>>}, {"|phone2numeric", + <<"{{ var1|phone2numeric }}">>, [{var1, "1-800-COLLECT"}], + <<"1-800-2655328">>}, + {"|slugify", + <<"{{ var1|slugify }}">>, [{var1, "What The $#_! Was He Thinking?"}], + <<"what-the-_-was-he-thinking">>}, + {"|slice:\"s\"", + <<"{{ var|stringformat:\"s\" }}">>, [{var, "test"}], + <<"test">>}, + {"|stringformat:\"s\"", + <<"{{ var|stringformat:\"s\" }}">>, [{var, "test"}], + <<"test">>}, + {"|stringformat:\"s\"", + <<"{{ var|stringformat:\"s\" }}">>, [{var, "1"}], + <<"1">>}, + {"|stringformat:\"s\"", + <<"{{ var|stringformat:\"s\" }}">>, [{var, "test"}], + <<"test">>}, + {"|stringformat:\"10s\"", + <<"{{ var|stringformat:\"10s\" }}">>, [{var, "test"}], + <<" test">>}, + {"|stringformat:\"-10s\"", + <<"{{ var|stringformat:\"-10s\" }}">>, [{var, "test"}], + <<"test ">>}, + + {"|stringformat:\"d\"", + <<"{{ var|stringformat:\"d\" }}">>, [{var, "90"}], + <<"90">>}, + {"|stringformat:\"10d\"", + <<"{{ var|stringformat:\"10d\" }}">>, [{var, "90"}], + <<" 90">>}, + {"|stringformat:\"-10d\"", + <<"{{ var|stringformat:\"-10d\" }}">>, [{var, "90"}], + <<"90 ">>}, + {"|stringformat:\"i\"", + <<"{{ var|stringformat:\"i\" }}">>, [{var, "90"}], + <<"90">>}, + {"|stringformat:\"10i\"", + <<"{{ var|stringformat:\"10i\" }}">>, [{var, "90"}], + <<" 90">>}, + {"|stringformat:\"-10i\"", + <<"{{ var|stringformat:\"-10i\" }}">>, [{var, "90"}], + <<"90 ">>}, + {"|stringformat:\"0.2d\"", + <<"{{ var|stringformat:\"0.2d\" }}">>, [{var, "9"}], + <<"09">>}, + {"|stringformat:\"10.4d\"", + <<"{{ var|stringformat:\"10.4d\" }}">>, [{var, "9"}], + <<" 0009">>}, + {"|stringformat:\"-10.4d\"", + <<"{{ var|stringformat:\"-10.4d\" }}">>, [{var, "9"}], + <<"0009 ">>}, + + {"|stringformat:\"f\"", + <<"{{ var|stringformat:\"f\" }}">>, [{var, "1"}], + <<"1.000000">>}, + {"|stringformat:\".2f\"", + <<"{{ var|stringformat:\".2f\" }}">>, [{var, "1"}], + <<"1.00">>}, + {"|stringformat:\"0.2f\"", + <<"{{ var|stringformat:\"0.2f\" }}">>, [{var, "1"}], + <<"1.00">>}, + {"|stringformat:\"-0.2f\"", + <<"{{ var|stringformat:\"-0.2f\" }}">>, [{var, "1"}], + <<"1.00">>}, + {"|stringformat:\"10.2f\"", + <<"{{ var|stringformat:\"10.2f\" }}">>, [{var, "1"}], + <<" 1.00">>}, + {"|stringformat:\"-10.2f\"", + <<"{{ var|stringformat:\"-10.2f\" }}">>, [{var, "1"}], + <<"1.00 ">>}, + {"|stringformat:\".2f\"", + <<"{{ var|stringformat:\".2f\" }}">>, [{var, "1"}], + <<"1.00">>}, + {"|stringformat:\"x\"", + <<"{{ var|stringformat:\"x\" }}">>, [{var, "90"}], + <<"5a">>}, + {"|stringformat:\"X\"", + <<"{{ var|stringformat:\"X\" }}">>, [{var, "90"}], + <<"5A">>}, + + {"|stringformat:\"o\"", + <<"{{ var|stringformat:\"o\" }}">>, [{var, "90"}], + <<"132">>}, + + {"|stringformat:\"e\"", + <<"{{ var|stringformat:\"e\" }}">>, [{var, "90"}], + <<"9.000000e+01">>}, + {"|stringformat:\"e\"", + <<"{{ var|stringformat:\"e\" }}">>, [{var, "90000000000"}], + <<"9.000000e+10">>}, + {"|stringformat:\"E\"", + <<"{{ var|stringformat:\"E\" }}">>, [{var, "90"}], + <<"9.000000E+01">>}, + {"|striptags", + <<"{{ var|striptags }}">>, [{var, "Joel a slug"}], + <<"Joel is a slug">>}, + {"|striptags", + <<"{{ var|striptags }}">>, [{var, "Joel a slug"}], + <<"Joel is a slug">>}, + {"|striptags", + <<"{{ var|striptags }}">>, [{var, "Check out http://www.djangoproject.com"}], + <<"Check out http://www.djangoproject.com">>}, + {"|time:\"H:i\"", + <<"{{ var|time:\"H:i\" }}">>, [{var, {{2010,12,1}, {10,11,12}} }], + <<"10:11">>}, + {"|time", + <<"{{ var|time }}">>, [{var, {{2010,12,1}, {10,11,12}} }], + <<"10:11 a.m.">>}, + {"|timesince:from_date", + <<"{{ from_date|timesince:conference_date }}">>, [{conference_date, {{2006,6,1},{8,0,0}} }, {from_date, {{2006,6,1},{0,0,0}} }], <<"8 hours">>}, - {"|timesince:from_date", - <<"{{ from_date|timesince:conference_date }}">>, [{conference_date, {{2010,6,1},{8,0,0}} },{from_date, {{2006,6,1},{0,0,0}} }], - <<"4 years, 1 day">>}, % leap year - {"|timesince:from_date", - <<"{{ from_date|timesince:conference_date }}">>, [{conference_date, {{2006,7,15},{8,0,0}} },{from_date, {{2006,6,1},{0,0,0}} }], - <<"1 month, 2 weeks">>}, - {"|timeuntil:from_date", - <<"{{ conference_date|timeuntil:from_date }}">>, [{conference_date, {{2006,6,1},{8,0,0}} }, {from_date, {{2006,6,1},{0,0,0}} }], - <<"8 hours">>}, - {"|timeuntil:from_date", - <<"{{ conference_date|timeuntil:from_date }}">>, [{conference_date, {{2010,6,1},{8,0,0}} },{from_date, {{2006,6,1},{0,0,0}} }], - <<"4 years, 1 day">>}, - {"|timeuntil:from_date", - <<"{{ conference_date|timeuntil:from_date }}">>, [{conference_date, {{2006,7,15},{8,0,0}} },{from_date, {{2006,6,1},{0,0,0}} }], - <<"1 month, 2 weeks">>}, - {"|title", - <<"{{ \"my title case\"|title }}">>, [], - <<"My Title Case">>}, - {"|title (pre-formatted)", - <<"{{ \"My Title Case\"|title }}">>, [], - <<"My Title Case">>}, - {"|title (wacky separators)", - <<"{{ \"my-title!case\"|title }}">>, [], - <<"My-Title!Case">>}, - {"|title (numbers)", - <<"{{ \"my-title123CaSe\"|title }}">>, [], - <<"My-Title123case">>}, - {"|title (Irish names)", - <<"{{ \"who's o'malley?\"|title }}">>, [], - <<"Who's O'Malley?">>}, - {"|truncatechars:0", - <<"{{ var1|truncatechars:0 }}">>, [{var1, "Empty Me"}], - <<"">>}, - {"|truncatechars:11", - <<"{{ var1|truncatechars:11 }}">>, [{var1, "Truncate Me Please"}], - <<"Truncate Me...">>}, - {"|truncatechars:17", - <<"{{ var1|truncatechars:17 }}">>, [{var1, "Don't Truncate Me"}], - <<"Don't Truncate Me">>}, - {"|truncatechars:1 (UTF-8)", - <<"{{ var1|truncatechars:1 }}">>, [{var1, "\x{E2}\x{82}\x{AC}1.99"}], - <<"\x{E2}\x{82}\x{AC}...">>}, - {"|truncatechars:2 (UTF-8)", - <<"{{ var1|truncatechars:2 }}">>, [{var1, "\x{E2}\x{82}\x{AC}1.99"}], - <<"\x{E2}\x{82}\x{AC}1...">>}, - {"|truncatewords:0", - <<"{{ var1|truncatewords:0 }}">>, [{var1, "Empty Me"}], - <<"">>}, - {"|truncatewords:2", - <<"{{ var1|truncatewords:2 }}">>, [{var1, "Truncate Me Please"}], - <<"Truncate Me...">>}, - {"|truncatewords:3", - <<"{{ var1|truncatewords:3 }}">>, [{var1, "Don't Truncate Me"}], - <<"Don't Truncate Me">>}, - {"|truncatewords_html:4", - <<"{{ var1|truncatewords_html:4 }}">>, [{var1, "

The Long and Winding Road is too long

"}], - <<"

The Long and Winding...

">>}, - {"|unordered_list", - <<"{{ var1|unordered_list }}">>, [{var1, ["States", ["Kansas", ["Lawrence", "Topeka"], "Illinois"]]}], - <<"
  • States
    • Kansas
      • Lawrence
      • Topeka
    • Illinois
  • ">>}, - {"|upper", - <<"{{ message|upper }}">>, [{message, "That man has a gun."}], - <<"THAT MAN HAS A GUN.">>}, - {"|urlencode", - <<"{{ url|urlencode }}">>, [{url, "You #$*@!!"}], - <<"You+%23%24%2A%40%21%21">>}, - {"|urlize", - <<"{{ var|urlize }}">>, [{var, "Check out www.djangoproject.com"}], - <<"Check out www.djangoproject.com">>}, - {"|urlize", - <<"{{ var|urlize }}">>, [{var, "Check out http://www.djangoproject.com"}], - <<"Check out http://www.djangoproject.com">>}, - {"|urlize", - <<"{{ var|urlize }}">>, [{var, "Check out \"http://www.djangoproject.com\""}], - <<"Check out \"http://www.djangoproject.com\"">>}, - {"|urlizetrunc:15", - <<"{{ var|urlizetrunc:15 }}">>, [{var, "Check out www.djangoproject.com"}], - <<"Check out www.djangopr...">>}, - {"|wordcount", - <<"{{ words|wordcount }}">>, [{words, "Why Hello There!"}], - <<"3">>}, - {"|wordwrap:2", - <<"{{ words|wordwrap:2 }}">>, [{words, "this is"}], - <<"this \nis">>}, - {"|wordwrap:100", - <<"{{ words|wordwrap:100 }}">>, [{words, "testing testing"}], - <<"testing testing">>}, - {"|wordwrap:10", - <<"{{ words|wordwrap:10 }}">>, [{words, ""}], - <<"">>}, - {"|wordwrap:1", - <<"{{ words|wordwrap:1 }}">>, [{words, "two"}], - <<"two">>}, - % yesno match: \?assert.*\( (.*), erlydtl_filters:(.*)\((.*), "(.*)"\)\) - % yesno replace: \t\t{"|$2:\"$4\"",\n\t\t\t\t\t <<"{{ var|$2:\"$4\" }}">>, [{var, $3}],\n\t\t\t\t\t<<$1>>} - {"|yesno:\"yeah,no,maybe\"", - <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, true}], - <<"yeah">>}, - {"|yesno:\"yeah,no,maybe\"", - <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, false}], - <<"no">>}, - {"|yesno:\"yeah,no\"", - <<"{{ var|yesno:\"yeah,no\" }}">>, [{var, undefined}], - <<"no">>}, - {"|yesno:\"yeah,no,maybe\"", - <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, undefined}], - <<"maybe">>} - ]}, - {"filters_if", [ - {"Filter if 1.1", - <<"{% if var1|length_is:0 %}Y{% else %}N{% endif %}">>, - [{var1, []}], - <<"Y">>}, - {"Filter if 1.2", - <<"{% if var1|length_is:1 %}Y{% else %}N{% endif %}">>, - [{var1, []}], - <<"N">>}, - {"Filter if 1.3", - <<"{% if var1|length_is:7 %}Y{% else %}N{% endif %}">>, - [{var1, []}], - <<"N">>}, - {"Filter if 2.1", - <<"{% if var1|length_is:0 %}Y{% else %}N{% endif %}">>, - [{var1, ["foo"]}], - <<"N">>}, - {"Filter if 2.2", - <<"{% if var1|length_is:1 %}Y{% else %}N{% endif %}">>, - [{var1, ["foo"]}], - <<"Y">>}, - {"Filter if 2.3", - <<"{% if var1|length_is:7 %}Y{% else %}N{% endif %}">>, - [{var1, ["foo"]}], - <<"N">>}, - {"Filter if 3.1", - <<"{% ifequal var1|length 0 %}Y{% else %}N{% endifequal %}">>, - [{var1, []}], - <<"Y">>}, - {"Filter if 3.2", - <<"{% ifequal var1|length 1 %}Y{% else %}N{% endifequal %}">>, - [{var1, []}], - <<"N">>}, - {"Filter if 4.1", - <<"{% ifequal var1|length 3 %}Y{% else %}N{% endifequal %}">>, - [{var1, ["foo", "bar", "baz"]}], - <<"Y">>}, - {"Filter if 4.2", - <<"{% ifequal var1|length 0 %}Y{% else %}N{% endifequal %}">>, - [{var1, ["foo", "bar", "baz"]}], - <<"N">>}, - {"Filter if 4.3", - <<"{% ifequal var1|length 1 %}Y{% else %}N{% endifequal %}">>, - [{var1, ["foo", "bar", "baz"]}], - <<"N">>} - ]}, - {"firstof", [ - {"Firstof first", - <<"{% firstof foo bar baz %}">>, - [{foo, "1"},{bar, "2"}], - <<"1">>}, - {"Firstof second", - <<"{% firstof foo bar baz %}">>, - [{bar, "2"}], - <<"2">>}, - {"Firstof none", - <<"{% firstof foo bar baz %}">>, - [], - <<"">>}, - {"Firstof complex", - <<"{% firstof foo.bar.baz bar %}">>, - [{foo, [{bar, [{baz, "quux"}]}]}], - <<"quux">>}, - {"Firstof undefined complex", - <<"{% firstof foo.bar.baz bar %}">>, - [{bar, "bar"}], - <<"bar">>}, - {"Firstof literal", - <<"{% firstof foo bar \"baz\" %}">>, - [], - <<"baz">>} - ]}, - {"regroup", [ - {"Ordered", <<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}{% endregroup %}">>, - [{people, [[{first_name, "George"}, {gender, "Male"}], [{first_name, "Bill"}, {gender, "Male"}], - [{first_name, "Margaret"}, {gender, "Female"}], [{first_name, "Condi"}, {gender, "Female"}]]}], - <<"Male\nGeorge\nBill\nFemale\nMargaret\nCondi\n">>}, - {"Unordered", <<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}{% endregroup %}">>, - [{people, [[{first_name, "George"}, {gender, "Male"}], - [{first_name, "Margaret"}, {gender, "Female"}], - [{first_name, "Condi"}, {gender, "Female"}], - [{first_name, "Bill"}, {gender, "Male"}] - ]}], - <<"Male\nGeorge\nFemale\nMargaret\nCondi\nMale\nBill\n">>}, - {"NestedOrdered", <<"{% regroup people by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}{% endregroup %}">>, - [{people, [[{name, [{first,"George"},{last,"Costanza"}]}], - [{name, [{first,"Margaret"},{last,"Costanza"}]}], - [{name, [{first,"Bill"},{last,"Buffalo"}]}], - [{name, [{first,"Condi"},{last,"Buffalo"}]}]]}], - <<"Costanza\nGeorge\nMargaret\nBuffalo\nBill\nCondi\n">>}, - {"NestedUnordered", <<"{% regroup people by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}{% endregroup %}">>, - [{people, [[{name, [{first,"George"},{last,"Costanza"}]}], - [{name, [{first,"Bill"},{last,"Buffalo"}]}], - [{name, [{first,"Margaret"},{last,"Costanza"}]}], - [{name, [{first,"Condi"},{last,"Buffalo"}]}]]}], - <<"Costanza\nGeorge\nBuffalo\nBill\nCostanza\nMargaret\nBuffalo\nCondi\n">>}, - {"Filter", <<"{% regroup people|dictsort:\"name.last\" by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}{% endregroup %}">>, - [{people, [[{name, [{first,"George"},{last,"Costanza"}]}], - [{name, [{first,"Bill"},{last,"Buffalo"}]}], - [{name, [{first,"Margaret"},{last,"Costanza"}]}], - [{name, [{first,"Condi"},{last,"Buffalo"}]}]]}], - <<"Buffalo\nBill\nCondi\nCostanza\nGeorge\nMargaret\n">>} - ]}, - {"spaceless", [ - {"Beginning", <<"{% spaceless %} foo{% endspaceless %}">>, [], <<"foo">>}, - {"Middle", <<"{% spaceless %}foo bar{% endspaceless %}">>, [], <<"foobar">>}, - {"End", <<"{% spaceless %}foo {% endspaceless %}">>, [], <<"foo">>}, - {"NewLine", <<"{% spaceless %}\n
    \n foo \n
    \n {% endspaceless %}">>, [], <<"
    foo
    ">>} - ]}, - {"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\" %}">>, [], [], [{locale, "reverse"}], <<"Hello iH">> - }, - {"trans literal at run-time", - <<"Hello {% trans \"Hi\" %}">>, [], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [], - <<"Hello Konichiwa">>}, - {"trans variable at run-time", - <<"Hello {% trans var1 %}">>, [{var1, <<"Hi">>}], [{translation_fun, fun(<<"Hi">>) -> <<"Konichiwa">> end}], [], - <<"Hello Konichiwa">>}, - {"trans literal at run-time: No-op", - <<"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">>}], [{translation_fun, fun(<<"Hi">>) -> <<"Konichiwa">> end}], [], - <<"Hello Hi">>} - ]}, - {"blocktrans", - [ - {"blocktrans default locale", - <<"{% blocktrans %}Hello{% endblocktrans %}">>, [], <<"Hello">>}, - {"blocktrans choose locale", - <<"{% blocktrans %}Hello, {{ name }}{% endblocktrans %}">>, [{name, "Mr. President"}], [{locale, "de"}], - [{blocktrans_locales, ["de"]}, {blocktrans_fun, fun("Hello, {{ name }}", "de") -> <<"Guten tag, {{ name }}">> end}], <<"Guten tag, Mr. President">>}, - {"blocktrans with args", - <<"{% blocktrans with var1=foo %}{{ var1 }}{% endblocktrans %}">>, [{foo, "Hello"}], <<"Hello">>} - ]}, - {"verbatim", [ - {"Plain verbatim", - <<"{% verbatim %}{{ oh no{% foobar %}{% endverbatim %}">>, [], - <<"{{ oh no{% foobar %}">>}, - {"Named verbatim", - <<"{% verbatim foobar %}{% verbatim %}{% endverbatim foobar2 %}{% endverbatim foobar %}">>, [], - <<"{% verbatim %}{% endverbatim foobar2 %}">>} - ]}, - {"widthratio", [ - {"Literals", <<"{% widthratio 5 10 100 %}">>, [], <<"50">>}, - {"Rounds up", <<"{% widthratio a b 100 %}">>, [{a, 175}, {b, 200}], <<"88">>} - ]}, - {"with", [ - {"Cache literal", + {"|timesince:from_date", + <<"{{ from_date|timesince:conference_date }}">>, [{conference_date, {{2010,6,1},{8,0,0}} },{from_date, {{2006,6,1},{0,0,0}} }], + <<"4 years, 1 day">>}, % leap year + {"|timesince:from_date", + <<"{{ from_date|timesince:conference_date }}">>, [{conference_date, {{2006,7,15},{8,0,0}} },{from_date, {{2006,6,1},{0,0,0}} }], + <<"1 month, 2 weeks">>}, + {"|timeuntil:from_date", + <<"{{ conference_date|timeuntil:from_date }}">>, [{conference_date, {{2006,6,1},{8,0,0}} }, {from_date, {{2006,6,1},{0,0,0}} }], + <<"8 hours">>}, + {"|timeuntil:from_date", + <<"{{ conference_date|timeuntil:from_date }}">>, [{conference_date, {{2010,6,1},{8,0,0}} },{from_date, {{2006,6,1},{0,0,0}} }], + <<"4 years, 1 day">>}, + {"|timeuntil:from_date", + <<"{{ conference_date|timeuntil:from_date }}">>, [{conference_date, {{2006,7,15},{8,0,0}} },{from_date, {{2006,6,1},{0,0,0}} }], + <<"1 month, 2 weeks">>}, + {"|title", + <<"{{ \"my title case\"|title }}">>, [], + <<"My Title Case">>}, + {"|title (pre-formatted)", + <<"{{ \"My Title Case\"|title }}">>, [], + <<"My Title Case">>}, + {"|title (wacky separators)", + <<"{{ \"my-title!case\"|title }}">>, [], + <<"My-Title!Case">>}, + {"|title (numbers)", + <<"{{ \"my-title123CaSe\"|title }}">>, [], + <<"My-Title123case">>}, + {"|title (Irish names)", + <<"{{ \"who's o'malley?\"|title }}">>, [], + <<"Who's O'Malley?">>}, + {"|truncatechars:0", + <<"{{ var1|truncatechars:0 }}">>, [{var1, "Empty Me"}], + <<"">>}, + {"|truncatechars:11", + <<"{{ var1|truncatechars:11 }}">>, [{var1, "Truncate Me Please"}], + <<"Truncate Me...">>}, + {"|truncatechars:17", + <<"{{ var1|truncatechars:17 }}">>, [{var1, "Don't Truncate Me"}], + <<"Don't Truncate Me">>}, + {"|truncatechars:1 (UTF-8)", + <<"{{ var1|truncatechars:1 }}">>, [{var1, "\x{E2}\x{82}\x{AC}1.99"}], + <<"\x{E2}\x{82}\x{AC}...">>}, + {"|truncatechars:2 (UTF-8)", + <<"{{ var1|truncatechars:2 }}">>, [{var1, "\x{E2}\x{82}\x{AC}1.99"}], + <<"\x{E2}\x{82}\x{AC}1...">>}, + {"|truncatewords:0", + <<"{{ var1|truncatewords:0 }}">>, [{var1, "Empty Me"}], + <<"">>}, + {"|truncatewords:2", + <<"{{ var1|truncatewords:2 }}">>, [{var1, "Truncate Me Please"}], + <<"Truncate Me...">>}, + {"|truncatewords:3", + <<"{{ var1|truncatewords:3 }}">>, [{var1, "Don't Truncate Me"}], + <<"Don't Truncate Me">>}, + {"|truncatewords_html:4", + <<"{{ var1|truncatewords_html:4 }}">>, [{var1, "

    The Long and Winding Road is too long

    "}], + <<"

    The Long and Winding...

    ">>}, + {"|unordered_list", + <<"{{ var1|unordered_list }}">>, [{var1, ["States", ["Kansas", ["Lawrence", "Topeka"], "Illinois"]]}], + <<"
  • States
    • Kansas
      • Lawrence
      • Topeka
    • Illinois
  • ">>}, + {"|upper", + <<"{{ message|upper }}">>, [{message, "That man has a gun."}], + <<"THAT MAN HAS A GUN.">>}, + {"|urlencode", + <<"{{ url|urlencode }}">>, [{url, "You #$*@!!"}], + <<"You+%23%24%2A%40%21%21">>}, + {"|urlize", + <<"{{ var|urlize }}">>, [{var, "Check out www.djangoproject.com"}], + <<"Check out www.djangoproject.com">>}, + {"|urlize", + <<"{{ var|urlize }}">>, [{var, "Check out http://www.djangoproject.com"}], + <<"Check out http://www.djangoproject.com">>}, + {"|urlize", + <<"{{ var|urlize }}">>, [{var, "Check out \"http://www.djangoproject.com\""}], + <<"Check out \"http://www.djangoproject.com\"">>}, + {"|urlizetrunc:15", + <<"{{ var|urlizetrunc:15 }}">>, [{var, "Check out www.djangoproject.com"}], + <<"Check out www.djangopr...">>}, + {"|wordcount", + <<"{{ words|wordcount }}">>, [{words, "Why Hello There!"}], + <<"3">>}, + {"|wordwrap:2", + <<"{{ words|wordwrap:2 }}">>, [{words, "this is"}], + <<"this \nis">>}, + {"|wordwrap:100", + <<"{{ words|wordwrap:100 }}">>, [{words, "testing testing"}], + <<"testing testing">>}, + {"|wordwrap:10", + <<"{{ words|wordwrap:10 }}">>, [{words, ""}], + <<"">>}, + {"|wordwrap:1", + <<"{{ words|wordwrap:1 }}">>, [{words, "two"}], + <<"two">>}, + % yesno match: \?assert.*\( (.*), erlydtl_filters:(.*)\((.*), "(.*)"\)\) + % yesno replace: \t\t{"|$2:\"$4\"",\n\t\t\t\t\t <<"{{ var|$2:\"$4\" }}">>, [{var, $3}],\n\t\t\t\t\t<<$1>>} + {"|yesno:\"yeah,no,maybe\"", + <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, true}], + <<"yeah">>}, + {"|yesno:\"yeah,no,maybe\"", + <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, false}], + <<"no">>}, + {"|yesno:\"yeah,no\"", + <<"{{ var|yesno:\"yeah,no\" }}">>, [{var, undefined}], + <<"no">>}, + {"|yesno:\"yeah,no,maybe\"", + <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, undefined}], + <<"maybe">>} + ]}, + {"filters_if", [ + {"Filter if 1.1", + <<"{% if var1|length_is:0 %}Y{% else %}N{% endif %}">>, + [{var1, []}], + <<"Y">>}, + {"Filter if 1.2", + <<"{% if var1|length_is:1 %}Y{% else %}N{% endif %}">>, + [{var1, []}], + <<"N">>}, + {"Filter if 1.3", + <<"{% if var1|length_is:7 %}Y{% else %}N{% endif %}">>, + [{var1, []}], + <<"N">>}, + {"Filter if 2.1", + <<"{% if var1|length_is:0 %}Y{% else %}N{% endif %}">>, + [{var1, ["foo"]}], + <<"N">>}, + {"Filter if 2.2", + <<"{% if var1|length_is:1 %}Y{% else %}N{% endif %}">>, + [{var1, ["foo"]}], + <<"Y">>}, + {"Filter if 2.3", + <<"{% if var1|length_is:7 %}Y{% else %}N{% endif %}">>, + [{var1, ["foo"]}], + <<"N">>}, + {"Filter if 3.1", + <<"{% ifequal var1|length 0 %}Y{% else %}N{% endifequal %}">>, + [{var1, []}], + <<"Y">>}, + {"Filter if 3.2", + <<"{% ifequal var1|length 1 %}Y{% else %}N{% endifequal %}">>, + [{var1, []}], + <<"N">>}, + {"Filter if 4.1", + <<"{% ifequal var1|length 3 %}Y{% else %}N{% endifequal %}">>, + [{var1, ["foo", "bar", "baz"]}], + <<"Y">>}, + {"Filter if 4.2", + <<"{% ifequal var1|length 0 %}Y{% else %}N{% endifequal %}">>, + [{var1, ["foo", "bar", "baz"]}], + <<"N">>}, + {"Filter if 4.3", + <<"{% ifequal var1|length 1 %}Y{% else %}N{% endifequal %}">>, + [{var1, ["foo", "bar", "baz"]}], + <<"N">>} + ]}, + {"firstof", [ + {"Firstof first", + <<"{% firstof foo bar baz %}">>, + [{foo, "1"},{bar, "2"}], + <<"1">>}, + {"Firstof second", + <<"{% firstof foo bar baz %}">>, + [{bar, "2"}], + <<"2">>}, + {"Firstof none", + <<"{% firstof foo bar baz %}">>, + [], + <<"">>}, + {"Firstof complex", + <<"{% firstof foo.bar.baz bar %}">>, + [{foo, [{bar, [{baz, "quux"}]}]}], + <<"quux">>}, + {"Firstof undefined complex", + <<"{% firstof foo.bar.baz bar %}">>, + [{bar, "bar"}], + <<"bar">>}, + {"Firstof literal", + <<"{% firstof foo bar \"baz\" %}">>, + [], + <<"baz">>} + ]}, + {"regroup", [ + {"Ordered", <<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}{% endregroup %}">>, + [{people, [[{first_name, "George"}, {gender, "Male"}], [{first_name, "Bill"}, {gender, "Male"}], + [{first_name, "Margaret"}, {gender, "Female"}], [{first_name, "Condi"}, {gender, "Female"}]]}], + <<"Male\nGeorge\nBill\nFemale\nMargaret\nCondi\n">>}, + {"Unordered", <<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}{% endregroup %}">>, + [{people, [[{first_name, "George"}, {gender, "Male"}], + [{first_name, "Margaret"}, {gender, "Female"}], + [{first_name, "Condi"}, {gender, "Female"}], + [{first_name, "Bill"}, {gender, "Male"}] + ]}], + <<"Male\nGeorge\nFemale\nMargaret\nCondi\nMale\nBill\n">>}, + {"NestedOrdered", <<"{% regroup people by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}{% endregroup %}">>, + [{people, [[{name, [{first,"George"},{last,"Costanza"}]}], + [{name, [{first,"Margaret"},{last,"Costanza"}]}], + [{name, [{first,"Bill"},{last,"Buffalo"}]}], + [{name, [{first,"Condi"},{last,"Buffalo"}]}]]}], + <<"Costanza\nGeorge\nMargaret\nBuffalo\nBill\nCondi\n">>}, + {"NestedUnordered", <<"{% regroup people by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}{% endregroup %}">>, + [{people, [[{name, [{first,"George"},{last,"Costanza"}]}], + [{name, [{first,"Bill"},{last,"Buffalo"}]}], + [{name, [{first,"Margaret"},{last,"Costanza"}]}], + [{name, [{first,"Condi"},{last,"Buffalo"}]}]]}], + <<"Costanza\nGeorge\nBuffalo\nBill\nCostanza\nMargaret\nBuffalo\nCondi\n">>}, + {"Filter", <<"{% regroup people|dictsort:\"name.last\" by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}{% endregroup %}">>, + [{people, [[{name, [{first,"George"},{last,"Costanza"}]}], + [{name, [{first,"Bill"},{last,"Buffalo"}]}], + [{name, [{first,"Margaret"},{last,"Costanza"}]}], + [{name, [{first,"Condi"},{last,"Buffalo"}]}]]}], + <<"Buffalo\nBill\nCondi\nCostanza\nGeorge\nMargaret\n">>} + ]}, + {"spaceless", [ + {"Beginning", <<"{% spaceless %} foo{% endspaceless %}">>, [], <<"foo">>}, + {"Middle", <<"{% spaceless %}foo bar{% endspaceless %}">>, [], <<"foobar">>}, + {"End", <<"{% spaceless %}foo {% endspaceless %}">>, [], <<"foo">>}, + {"NewLine", <<"{% spaceless %}\n
    \n foo \n
    \n {% endspaceless %}">>, [], <<"
    foo
    ">>} + ]}, + {"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\" %}">>, [], [], [{locale, "reverse"}], <<"Hello iH">> + }, + {"trans literal at run-time", + <<"Hello {% trans \"Hi\" %}">>, [], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [], + <<"Hello Konichiwa">>}, + {"trans variable at run-time", + <<"Hello {% trans var1 %}">>, [{var1, <<"Hi">>}], [{translation_fun, fun(<<"Hi">>) -> <<"Konichiwa">> end}], [], + <<"Hello Konichiwa">>}, + {"trans literal at run-time: No-op", + <<"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">>}], [{translation_fun, fun(<<"Hi">>) -> <<"Konichiwa">> end}], [], + <<"Hello Hi">>} + ]}, + {"blocktrans", + [ + {"blocktrans default locale", + <<"{% blocktrans %}Hello{% endblocktrans %}">>, [], <<"Hello">>}, + {"blocktrans choose locale", + <<"{% blocktrans %}Hello, {{ name }}{% endblocktrans %}">>, [{name, "Mr. President"}], [{locale, "de"}], + [{blocktrans_locales, ["de"]}, {blocktrans_fun, fun("Hello, {{ name }}", "de") -> <<"Guten tag, {{ name }}">> end}], <<"Guten tag, Mr. President">>}, + {"blocktrans with args", + <<"{% blocktrans with var1=foo %}{{ var1 }}{% endblocktrans %}">>, [{foo, "Hello"}], <<"Hello">>} + ]}, + {"verbatim", [ + {"Plain verbatim", + <<"{% verbatim %}{{ oh no{% foobar %}{% endverbatim %}">>, [], + <<"{{ oh no{% foobar %}">>}, + {"Named verbatim", + <<"{% verbatim foobar %}{% verbatim %}{% endverbatim foobar2 %}{% endverbatim foobar %}">>, [], + <<"{% verbatim %}{% endverbatim foobar2 %}">>} + ]}, + {"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", + {"Cache variable", <<"{% with a=b %}{{ a }}{% endwith %}">>, [{b, "foo"}], <<"foo">>}, - {"Cache variable with attribute", - <<"I enjoy {% with a = var1 %}{{ a.game }}{% endwith %}">>, [{var1, [{game, "Othello"}]}], <<"I enjoy Othello">>}, - {"Cache variable attribute", - <<"I enjoy {% with a = var1.game %}{{ a }}{% endwith %}">>, [{var1, [{game, "Othello"}]}], <<"I enjoy Othello">>}, - {"Cache multiple", + {"Cache variable with attribute", + <<"I enjoy {% with a = var1 %}{{ a.game }}{% endwith %}">>, [{var1, [{game, "Othello"}]}], <<"I enjoy Othello">>}, + {"Cache variable attribute", + <<"I enjoy {% with a = var1.game %}{{ a }}{% endwith %}">>, [{var1, [{game, "Othello"}]}], <<"I enjoy Othello">>}, + {"Cache multiple", <<"{% with alpha=1 beta=b %}{{ alpha }}/{{ beta }}{% endwith %}">>, [{b, 2}], <<"1/2">>} - ]}, + ]}, {"unicode", [ - {"(tm) somewhere", - <<"โ„ข">>, [], <<"โ„ข">>} - ]}, + {"(tm) somewhere", + <<"โ„ข">>, [], <<"โ„ข">>} + ]}, {"contrib_humanize", [ - {"intcomma", - <<"{{ a|intcomma }} {{ b|intcomma }} {{ c|intcomma }} {{ d|intcomma }}">>, - [{a, 999}, {b, 123456789}, {c, 12345}, {d, 1234567890}], - <<"999 123,456,789 12,345 1,234,567,890">>} - ]} + {"intcomma", + <<"{{ a|intcomma }} {{ b|intcomma }} {{ c|intcomma }} {{ d|intcomma }}">>, + [{a, 999}, {b, 123456789}, {c, 12345}, {d, 1234567890}], + <<"999 123,456,789 12,345 1,234,567,890">>} + ]} ]. - + run_tests() -> io:format("Running unit tests...~n"), DefaultOptions = [{custom_filters_modules, [erlydtl_contrib_humanize]}], Failures = lists:foldl( - fun({Group, Assertions}, GroupAcc) -> - io:format(" Test group ~p...~n", [Group]), - lists:foldl(fun - ({Name, DTL, Vars, Output}, Acc) -> - process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), - Vars, [], Output, Acc, Group, Name); - ({Name, DTL, Vars, RenderOpts, Output}, Acc) -> - process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), - Vars, RenderOpts, Output, Acc, Group, Name); - ({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}, Acc) -> - process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts ++ DefaultOptions), - Vars, RenderOpts, Output, Acc, Group, Name) - end, GroupAcc, Assertions) - end, [], tests()), - + fun({Group, Assertions}, GroupAcc) -> + io:format(" Test group ~p...~n", [Group]), + lists:foldl(fun + ({Name, DTL, Vars, Output}, Acc) -> + process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), + Vars, [], Output, Acc, Group, Name); + ({Name, DTL, Vars, RenderOpts, Output}, Acc) -> + process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), + Vars, RenderOpts, Output, Acc, Group, Name); + ({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}, Acc) -> + process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts ++ DefaultOptions), + 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, RenderOpts, Output,Acc, Group, Name) -> - case CompiledTemplate of - {ok, _} -> - {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; - {Output, Unexpected} -> - [{Group, Name, 'binary', Unexpected, Output} | Acc]; - {Unexpected, Output} -> - [{Group, Name, 'list', Unexpected, Output} | Acc]; - {Unexpected1, Unexpected2} -> - [{Group, Name, 'list', Unexpected1, Output}, - {Group, Name, 'binary', Unexpected2, Output} | Acc] - end; - Err -> - [{Group, Name, Err} | Acc] - end. - - + case CompiledTemplate of + {ok, _} -> + {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; + {Output, Unexpected} -> + [{Group, Name, 'binary', Unexpected, Output} | Acc]; + {Unexpected, Output} -> + [{Group, Name, 'list', Unexpected, Output} | Acc]; + {Unexpected1, Unexpected2} -> + [{Group, Name, 'list', Unexpected1, Output}, + {Group, Name, 'binary', Unexpected2, Output} | Acc] + end; + Err -> + [{Group, Name, Err} | Acc] + end. + + vars_to_binary(Vars) when is_list(Vars) -> lists:map(fun - ({Key, [H|_] = Value}) when is_tuple(H) -> - {Key, vars_to_binary(Value)}; - ({Key, [H|_] = Value}) when is_integer(H) -> - {Key, list_to_binary(Value)}; - ({Key, Value}) -> - {Key, Value} - end, Vars); + ({Key, [H|_] = Value}) when is_tuple(H) -> + {Key, vars_to_binary(Value)}; + ({Key, [H|_] = Value}) when is_integer(H) -> + {Key, list_to_binary(Value)}; + ({Key, Value}) -> + {Key, Value} + end, Vars); vars_to_binary(Vars) -> Vars. - + generate_test_date() -> {{Y,M,D}, _} = erlang:localtime(), MonthName = [ - "January", "February", "March", "April", - "May", "June", "July", "August", "September", - "October", "November", "December" - ], + "January", "February", "March", "April", + "May", "June", "July", "August", "September", + "October", "November", "December" + ], OrdinalSuffix = [ - "st","nd","rd","th","th","th","th","th","th","th", % 1-10 - "th","th","th","th","th","th","th","th","th","th", % 10-20 - "st","nd","rd","th","th","th","th","th","th","th", % 20-30 - "st" - ], + "st","nd","rd","th","th","th","th","th","th","th", % 1-10 + "th","th","th","th","th","th","th","th","th","th", % 10-20 + "st","nd","rd","th","th","th","th","th","th","th", % 20-30 + "st" + ], list_to_binary([ - "It is the ", - integer_to_list(D), - lists:nth(D, OrdinalSuffix), - " of ", lists:nth(M, MonthName), - " ", integer_to_list(Y), "." - ]). + "It is the ", + integer_to_list(D), + lists:nth(D, OrdinalSuffix), + " of ", lists:nth(M, MonthName), + " ", integer_to_list(Y), "." + ]). From 682bc7a9ba2b15ad95934c07a85cc26edef78dd1 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 14 Jun 2013 00:06:16 +0200 Subject: [PATCH 015/361] indentation... --- src/erlydtl_scanner.erl | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/erlydtl_scanner.erl b/src/erlydtl_scanner.erl index 7751a6a..4b51e9e 100644 --- a/src/erlydtl_scanner.erl +++ b/src/erlydtl_scanner.erl @@ -85,11 +85,11 @@ scan("#}" ++ T, Scanned, {Row, Column}, {in_comment, "#}"}) -> scan(""}); + {Row, Column + length(""}); scan("{%" ++ T, Scanned, {Row, Column}, in_text) -> scan(T, [{open_tag, {Row, Column}, '{%'} | Scanned], - {Row, Column + length("{%")}, {in_code, "%}"}); + {Row, Column + length("{%")}, {in_code, "%}"}); scan([_ | T], Scanned, {Row, Column}, {in_comment, Closer}) -> scan(T, Scanned, {Row, Column + 1}, {in_comment, Closer}); @@ -124,11 +124,11 @@ scan([$\\ | T], Scanned, {Row, Column}, {in_single_quote, Closer}) -> scan([H | T], Scanned, {Row, Column}, {in_single_quote_slash, Closer}) -> scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_single_quote, Closer}); -% end quote + % end quote scan("\"" ++ T, Scanned, {Row, Column}, {in_double_quote, Closer}) -> scan(T, append_char(Scanned, 34), {Row, Column + 1}, {in_code, Closer}); -% treat single quotes the same as double quotes + % treat single quotes the same as double quotes scan("\'" ++ T, Scanned, {Row, Column}, {in_single_quote, Closer}) -> scan(T, append_char(Scanned, 34), {Row, Column + 1}, {in_code, Closer}); @@ -141,25 +141,25 @@ scan([H | T], Scanned, {Row, Column}, {in_single_quote, Closer}) -> scan("}}-->" ++ T, Scanned, {Row, Column}, {_, "}}-->"}) -> scan(T, [{close_var, {Row, Column}, '}}-->'} | Scanned], - {Row, Column + length("}}-->")}, in_text); + {Row, Column + length("}}-->")}, in_text); scan("}}" ++ T, Scanned, {Row, Column}, {_, "}}"}) -> scan(T, [{close_var, {Row, Column}, '}}'} | Scanned], {Row, Column + 2}, in_text); scan("%}-->" ++ T, Scanned, {Row, Column}, {_, "%}-->"}) -> scan(T, [{close_tag, {Row, Column}, '%}-->'} | Scanned], - {Row, Column + length("%}-->")}, in_text); + {Row, Column + length("%}-->")}, in_text); scan("%}" ++ T, [{identifier, _, "mitabrev"}, {open_tag, _, '{%'}|Scanned], {Row, Column}, {_, "%}"}) -> scan(T, [{string, {Row, Column + 2}, ""}|Scanned], {Row, Column + 2}, {in_verbatim, undefined}); scan("%}" ++ T, [{identifier, _, ReversedTag}, {identifier, _, "mitabrev"}, {open_tag, _, '{%'}|Scanned], - {Row, Column}, {_, "%}"}) -> + {Row, Column}, {_, "%}"}) -> scan(T, [{string, {Row, Column + 2}, ""}|Scanned], {Row, Column + 2}, {in_verbatim, ReversedTag}); scan("%}" ++ T, Scanned, {Row, Column}, {_, "%}"}) -> scan(T, [{close_tag, {Row, Column}, '%}'} | Scanned], - {Row, Column + 2}, in_text); + {Row, Column + 2}, in_text); scan("{%" ++ T, Scanned, {Row, Column}, {in_verbatim, Tag}) -> scan(T, Scanned, {Row, Column + 2}, {in_verbatim_code, lists:reverse("{%"), Tag}); @@ -172,7 +172,7 @@ scan("endverbatim%}" ++ T, Scanned, {Row, Column}, {in_verbatim_code, _BackTrack scan("endverbatim " ++ T, Scanned, {Row, Column}, {in_verbatim_code, BackTrack, Tag}) -> scan(T, Scanned, {Row, Column + length("endverbatim ")}, - {in_endverbatim_code, "", lists:reverse("endverbatim ", BackTrack), Tag}); + {in_endverbatim_code, "", lists:reverse("endverbatim ", BackTrack), Tag}); scan(" " ++ T, Scanned, {Row, Column}, {in_endverbatim_code, "", BackTrack, Tag}) -> scan(T, Scanned, {Row, Column + 1}, {in_endverbatim_code, "", [$\ |BackTrack], Tag}); @@ -277,7 +277,7 @@ scan([H | T], Scanned, {Row, Column}, {in_identifier, Closer}) -> {error, {Row, ?MODULE, lists:concat(["Illegal character in column ", Column])}} end. -% internal functions + % internal functions append_char([{Type, Pos, Chars}|Scanned], Char) -> [{Type, Pos, [Char | Chars]} | Scanned]. @@ -328,7 +328,7 @@ mark_keywords([{identifier, Pos, "by" = String}|T], Acc) -> mark_keywords(T, [{by_keyword, Pos, String}|Acc]); mark_keywords([{identifier, Pos, "with" = String}|T], Acc) -> mark_keywords(T, [{with_keyword, Pos, String}|Acc]); -% These must be succeeded by a close_tag + % These must be succeeded by a close_tag mark_keywords([{identifier, Pos, "only" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> mark_keywords(T, lists:reverse([{only_keyword, Pos, String}, CloseTag], Acc)); mark_keywords([{identifier, Pos, "parsed" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> @@ -353,8 +353,8 @@ mark_keywords([{identifier, Pos, "opencomment" = String}, {close_tag, _, _} = Cl mark_keywords(T, lists:reverse([{opencomment_keyword, Pos, String}, CloseTag], Acc)); mark_keywords([{identifier, Pos, "closecomment" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> mark_keywords(T, lists:reverse([{closecomment_keyword, Pos, String}, CloseTag], Acc)); -% The rest must be preceded by an open_tag. -% This allows variables to have the same names as tags. + % The rest must be preceded by an open_tag. + % This allows variables to have the same names as tags. mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "autoescape" = String}|T], Acc) -> mark_keywords(T, lists:reverse([OpenToken, {autoescape_keyword, Pos, String}], Acc)); mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endautoescape" = String}|T], Acc) -> From 6fe1ea5a76f77615cccc09593492c076c8be6bdf Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 14 Jun 2013 07:57:39 +0200 Subject: [PATCH 016/361] support recovering from a scanner error using the extension module. --- .gitignore | 1 + Emakefile | 2 +- include/erlydtl_ext.hrl | 8 ++++++ src/erlydtl_compiler.erl | 29 ++++++++++++++++----- src/erlydtl_scanner.erl | 39 ++++++++++++++++++++++++---- tests/src/erlydtl_extension_test.erl | 19 ++++++++++++++ tests/src/erlydtl_unittests.erl | 8 ++++++ 7 files changed, 93 insertions(+), 13 deletions(-) create mode 100755 include/erlydtl_ext.hrl create mode 100755 tests/src/erlydtl_extension_test.erl diff --git a/.gitignore b/.gitignore index aea5fdf..b43c124 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ ebin erl_crash.dump examples/rendered_output src/erlydtl_parser.erl +*~ diff --git a/Emakefile b/Emakefile index cceeb9b..2b33bbf 100755 --- a/Emakefile +++ b/Emakefile @@ -1 +1 @@ -{"tests/src/*", [debug_info, {outdir, "ebintest"}]}. +{"tests/src/*", [debug_info, {outdir, "ebintest"}, {i, "include"}]}. diff --git a/include/erlydtl_ext.hrl b/include/erlydtl_ext.hrl new file mode 100755 index 0000000..2dad8fd --- /dev/null +++ b/include/erlydtl_ext.hrl @@ -0,0 +1,8 @@ + +-record(scanner_state, + { + template=[], + scanned=[], + pos={1,1}, + state=in_text + }). diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 9b4c525..aef7e3a 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -306,13 +306,10 @@ is_up_to_date(CheckSum, Context) -> parse(Data) -> parse(Data, #dtl_context{}). -parse(Data, _Context) when is_binary(Data) -> - case erlydtl_scanner:scan(binary_to_list(Data)) of - {ok, Tokens} -> - erlydtl_parser:parse(Tokens); - Err -> - Err - end; +parse(Data, Context) when is_binary(Data) -> + check_scan(erlydtl_scanner:scan(binary_to_list(Data)), Context); +parse(State, Context) when is_tuple(State) -> + check_scan(erlydtl_scanner:scan(State), Context); parse(File, Context) -> {M, F} = Context#dtl_context.reader, case catch M:F(File) of @@ -343,6 +340,24 @@ parse(CheckSum, Data, Context) -> end end. +check_scan({ok, Tokens}, _Context) -> + erlydtl_parser:parse(Tokens); +check_scan({error, Err, State}, Context) -> + case Context#dtl_context.extension_module of + undefined -> + {error, Err}; + M -> + case erlydtl_scanner:recover(M, State) of + {ok, NewState} -> + %% io:format("recover from:~p~nto: ~p~n", [State, NewState]), + parse(NewState, Context); + undefined -> + {error, Err}; + ExtErr -> + ExtErr + end + end. + custom_tags_ast(CustomTags, Context, TreeWalker) -> {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} = custom_tags_clauses_ast(CustomTags, Context, TreeWalker), {{erl_syntax:function(erl_syntax:atom(render_tag), CustomTagsClauses), CustomTagsInfo}, TreeWalker1}. diff --git a/src/erlydtl_scanner.erl b/src/erlydtl_scanner.erl index 4b51e9e..cf4e499 100644 --- a/src/erlydtl_scanner.erl +++ b/src/erlydtl_scanner.erl @@ -35,7 +35,8 @@ -author('rsaccon@gmail.com'). -author('emmiller@gmail.com'). --export([scan/1]). +-export([scan/1, recover/2]). +-include("erlydtl_ext.hrl"). %%==================================================================== @@ -49,8 +50,33 @@ %% an error. %% @end %%-------------------------------------------------------------------- +scan(#scanner_state{ template=Template, scanned=Scanned, + pos=Pos, state=State}) -> + scan(Template, Scanned, Pos, State); scan(Template) -> - scan(Template, [], {1, 1}, in_text). + scan(Template, [], {1, 1}, in_text). + +recover(Mod, State) -> + M = case code:is_loaded(Mod) of + false -> + case code:load_file(Mod) of + {module, Mod} -> + Mod; + _ -> + undefined + end; + _ -> Mod + end, + if M /= undefined -> + case erlang:function_exported(M, scan, 1) of + true -> + M:scan(State); + false -> + undefined + end; + true -> + undefined + end. scan([], Scanned, _, in_text) -> Tokens = lists:reverse(Scanned), @@ -256,7 +282,8 @@ scan([H | T], Scanned, {Row, Column}, {in_code, Closer}) -> digit -> scan(T, [{number_literal, {Row, Column}, [H]} | Scanned], {Row, Column + 1}, {in_number, Closer}); _ -> - {error, {Row, ?MODULE, lists:concat(["Illegal character in column ", Column])}} + {error, {Row, ?MODULE, lists:concat(["Illegal character in column ", Column])}, + #scanner_state{ template=[H|T], scanned=Scanned, pos={Row, Column}, state={in_code, Closer}}} end; scan([H | T], Scanned, {Row, Column}, {in_number, Closer}) -> @@ -264,7 +291,8 @@ scan([H | T], Scanned, {Row, Column}, {in_number, Closer}) -> digit -> scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_number, Closer}); _ -> - {error, {Row, ?MODULE, lists:concat(["Illegal character in column ", Column])}} + {error, {Row, ?MODULE, lists:concat(["Illegal character in column ", Column])}, + #scanner_state{ template=[H|T], scanned=Scanned, pos={Row, Column}, state={in_code, Closer}}} end; scan([H | T], Scanned, {Row, Column}, {in_identifier, Closer}) -> @@ -274,7 +302,8 @@ scan([H | T], Scanned, {Row, Column}, {in_identifier, Closer}) -> digit -> scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_identifier, Closer}); _ -> - {error, {Row, ?MODULE, lists:concat(["Illegal character in column ", Column])}} + {error, {Row, ?MODULE, lists:concat(["Illegal character in column ", Column])}, + #scanner_state{ template=[H|T], scanned=Scanned, pos={Row, Column}, state={in_code, Closer}}} end. % internal functions diff --git a/tests/src/erlydtl_extension_test.erl b/tests/src/erlydtl_extension_test.erl new file mode 100755 index 0000000..efef5ba --- /dev/null +++ b/tests/src/erlydtl_extension_test.erl @@ -0,0 +1,19 @@ +-module(erlydtl_extension_test). + +-export([scan/1]). +-include("erlydtl_ext.hrl"). + +%% look for a foo identifer followed by a # +scan(#scanner_state{ template="#" ++ T, + scanned=[{identifier, Loc, "oof"}|Scanned], + pos={L,C} }=S) -> + %% return new state with the hash dropped, and the foo identifer replaced with bar + {ok, S#scanner_state{ template=T, + scanned=[{identifier, Loc, "rab"}|Scanned], + pos={L, C+1} }}; +scan(#scanner_state{ template="#" ++ T, pos={L, C} }) -> + %% give error when # not follows foo + {error, {L,?MODULE,lists:concat(["Unexpected '#' in code at column ", C])}}; +scan(_) -> + %% for anything else, fallback to the error message from erlydtl_scanner.. + undefined. diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 350b898..ce77bed 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -1101,6 +1101,13 @@ tests() -> <<"{{ a|intcomma }} {{ b|intcomma }} {{ c|intcomma }} {{ d|intcomma }}">>, [{a, 999}, {b, 123456789}, {c, 12345}, {d, 1234567890}], <<"999 123,456,789 12,345 1,234,567,890">>} + ]}, + %% custom syntax stuff + {"extension_module", [ + %% the erlydtl_extension_test module replaces a foo identifier with bar when hitting a #. + {"replace parsed token", <<"{{ foo # }}">>, [{bar, "ok"}], [], [{extension_module, erlydtl_extension_test}], <<"ok">>}, + {"proper error message", <<"{{ bar # }}">>, [{bar, "ok"}], [], [{extension_module, erlydtl_extension_test}], + {error, {1,erlydtl_extension_test,"Unexpected '#' in code at column 8"}}} ]} ]. @@ -1141,6 +1148,7 @@ process_unit_test(CompiledTemplate, Vars, RenderOpts, Output,Acc, Group, Name) - [{Group, Name, 'list', Unexpected1, Output}, {Group, Name, 'binary', Unexpected2, Output} | Acc] end; + Output -> Acc; Err -> [{Group, Name, Err} | Acc] end. From 9f124f446ff4150e460cd7cc24bdba86b47691c9 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 14 Jun 2013 16:15:27 +0200 Subject: [PATCH 017/361] add debug_compiler to the list of valid compiler options. This will dump the compiled template module source to stdout. --- src/erlydtl_compiler.erl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index aef7e3a..922c1b4 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -219,6 +219,12 @@ compile_to_binary(File, DjangoParseTree, Context, CheckSum) -> end. compile_forms_and_reload(File, Forms, CompilerOptions) -> + case proplists:get_value(debug_compiler, CompilerOptions) of + true -> + io:format("Template ~p compiled with options: ~p~n", [File, CompilerOptions]), + [io:format("~s~n", [erl_pp:form(Form)]) || Form <- Forms]; + _ -> nop + end, case compile:forms(Forms, CompilerOptions) of {ok, Module1, Bin} -> load_code(Module1, Bin, []); From 9213d0fa7540404bc9acc089507b84dfaafed6dd Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Sat, 15 Jun 2013 13:09:17 +0200 Subject: [PATCH 018/361] add customized yeccpre.hrl file. --- include/erlydtl_preparser.hrl | 216 ++++++++++++++++++++++++++++++++++ src/erlydtl_parser.yrl | 2 +- 2 files changed, 217 insertions(+), 1 deletion(-) create mode 100755 include/erlydtl_preparser.hrl diff --git a/include/erlydtl_preparser.hrl b/include/erlydtl_preparser.hrl new file mode 100755 index 0000000..780bd69 --- /dev/null +++ b/include/erlydtl_preparser.hrl @@ -0,0 +1,216 @@ +%% -*- mode: 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). +%% +%% The applied modifiactions are to enable the caller to recover +%% after a parse error, and then resume normal parsing. + +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2010. 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/. +%% +%% 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. +%% +%% %CopyrightEnd% +%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% The parser generator will insert appropriate declarations before this line.% + +-export([parse/1, parse_and_scan/1, format_error/1, recover/1]). + +-type yecc_ret() :: {'error', _} | {'ok', _}. + +-spec parse(Tokens :: list()) -> yecc_ret(). +parse(Tokens) -> + yeccpars0(Tokens, {no_func, no_line}, 0, [], []). + +-spec parse_and_scan({function() | {atom(), atom()}, [_]} + | {atom(), atom(), [_]}) -> yecc_ret(). +parse_and_scan({F, A}) -> % Fun or {M, F} + yeccpars0([], {{F, A}, no_line}, 0, [], []); +parse_and_scan({M, F, A}) -> + yeccpars0([], {{{M, F}, A}, no_line}, 0, [], []). + +recover([Tokens, Tzr, State, States, Vstack]) -> + yeccpars0(Tokens, Tzr, State, States, Vstack). + +-spec format_error(any()) -> [char() | list()]. +format_error(Message) -> + case io_lib:deep_char_list(Message) of + true -> + Message; + _ -> + io_lib:write(Message) + end. + +%% To be used in grammar files to throw an error message to the parser +%% toplevel. Doesn't have to be exported! +-compile({nowarn_unused_function, return_error/2}). +-spec return_error(integer(), any()) -> no_return(). +return_error(Line, Message) -> + throw({error, {Line, ?MODULE, Message}}). + +-compile({nowarn_unused_function, return_state/0}). +return_state() -> + throw(return_state). + +-define(CODE_VERSION, "1.4"). + +yeccpars0(Tokens, Tzr, State, States, Vstack) -> + try yeccpars1(Tokens, Tzr, State, States, Vstack) + catch + 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; + %% Probably thrown from return_error/2: + throw: {error, {_Line, ?MODULE, _M}} = Error -> + Error + end. + +yecc_error_type(function_clause, [{?MODULE,F,ArityOrArgs} | _]) -> + case atom_to_list(F) of + "yeccgoto_" ++ SymbolL -> + {ok,[{atom,_,Symbol}],_} = erl_scan:string(SymbolL), + State = case ArityOrArgs of + [S,_,_,_,_,_,_] -> S; + _ -> state_is_unknown + end, + {Symbol, State, missing_in_goto_table} + end. + +-define(checkparse(CALL, STATE), + try case CALL of + {error, Error} -> + {error, Error, STATE}; + Else -> + Else + end + catch + throw: return_state -> + {ok, STATE} + end). + +yeccpars1([Token | Tokens], Tzr, State, States, Vstack) -> + ?checkparse( + yeccpars2(State, element(1, Token), States, Vstack, Token, Tokens, Tzr), + [[Token|Tokens], Tzr, State, States, Vstack] + ); +yeccpars1([], {{F, A},_Line}, State, States, Vstack) -> + case apply(F, A) of + {ok, Tokens, Endline} -> + yeccpars1(Tokens, {{F, A}, Endline}, State, States, Vstack); + {eof, Endline} -> + yeccpars1([], {no_func, Endline}, State, States, Vstack); + {error, Descriptor, _Endline} -> + {error, Descriptor} + end; +yeccpars1([], {no_func, no_line}, State, States, Vstack) -> + Line = 999999, + yeccpars2(State, '$end', States, Vstack, yecc_end(Line), [], + {no_func, Line}); +yeccpars1([], {no_func, Endline}, State, States, Vstack) -> + yeccpars2(State, '$end', States, Vstack, yecc_end(Endline), [], + {no_func, Endline}). + +%% yeccpars1/7 is called from generated code. +%% +%% When using the {includefile, Includefile} option, make sure that +%% yeccpars1/7 can be found by parsing the file without following +%% include directives. yecc will otherwise assume that an old +%% yeccpre.hrl is included (one which defines yeccpars1/5). +yeccpars1(State1, State, States, Vstack, Token0, [Token | Tokens], Tzr) -> + ?checkparse( + yeccpars2(State, element(1, Token), [State1 | States], + [Token0 | Vstack], Token, Tokens, Tzr), + [[Token0, Token|Tokens], Tzr, [State, State1|States], Vstack] + ); +yeccpars1(State1, State, States, Vstack, Token0, [], {{_F,_A}, _Line}=Tzr) -> + yeccpars1([], Tzr, State, [State1 | States], [Token0 | Vstack]); +yeccpars1(State1, State, States, Vstack, Token0, [], {no_func, no_line}) -> + Line = yecctoken_end_location(Token0), + yeccpars2(State, '$end', [State1 | States], [Token0 | Vstack], + yecc_end(Line), [], {no_func, Line}); +yeccpars1(State1, State, States, Vstack, Token0, [], {no_func, Line}) -> + yeccpars2(State, '$end', [State1 | States], [Token0 | Vstack], + yecc_end(Line), [], {no_func, Line}). + +%% For internal use only. +yecc_end({Line,_Column}) -> + {'$end', Line}; +yecc_end(Line) -> + {'$end', Line}. + +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) + end. + +-compile({nowarn_unused_function, yeccerror/1}). +yeccerror(Token) -> + Text = yecctoken_to_string(Token), + Location = yecctoken_location(Token), + {error, {Location, ?MODULE, ["syntax error before: ", Text]}}. + +-compile({nowarn_unused_function, yecctoken_to_string/1}). +yecctoken_to_string(Token) -> + case catch erl_scan:token_info(Token, text) of + {text, Txt} -> Txt; + _ -> yecctoken2string(Token) + end. + +yecctoken_location(Token) -> + case catch erl_scan:token_info(Token, location) of + {location, Loc} -> Loc; + _ -> element(2, Token) + end. + +-compile({nowarn_unused_function, yecctoken2string/1}). +yecctoken2string({atom, _, A}) -> io_lib:write(A); +yecctoken2string({integer,_,N}) -> io_lib:write(N); +yecctoken2string({float,_,F}) -> io_lib:write(F); +yecctoken2string({char,_,C}) -> io_lib:write_char(C); +yecctoken2string({var,_,V}) -> io_lib:format("~s", [V]); +yecctoken2string({string,_,S}) -> io_lib:write_unicode_string(S); +yecctoken2string({reserved_symbol, _, A}) -> io_lib:write(A); +yecctoken2string({_Cat, _, Val}) -> io_lib:format("~p",[Val]); +yecctoken2string({dot, _}) -> "'.'"; +yecctoken2string({'$end', _}) -> + []; +yecctoken2string({Other, _}) when is_atom(Other) -> + io_lib:write(Other); +yecctoken2string(Other) -> + io_lib:write(Other). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + diff --git a/src/erlydtl_parser.yrl b/src/erlydtl_parser.yrl index bd146e0..7573e3d 100644 --- a/src/erlydtl_parser.yrl +++ b/src/erlydtl_parser.yrl @@ -1,4 +1,4 @@ -%%%------------------------------------------------------------------- +%%% -*- mode: erlang -*- ------------------------------------------------------------------ %%% File: erlydtl_parser.erl %%% @author Roberto Saccon [http://rsaccon.com] %%% @author Evan Miller From fcc1f31cb897ebd3286fe9929da5c9ffa92d9659 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 20 Jun 2013 14:22:52 +0200 Subject: [PATCH 019/361] Add support for the extension module in the parser. The parser can be resumed after resolving parse errors (it is up to the extension module to do that). The extension module test still fails since the compiler part has not yet been implemented, and the tests run at a functional level instead of unit level. --- Makefile | 5 +- include/erlydtl_preparser.hrl | 6 +-- rebar.config | 1 + src/erlydtl_compiler.erl | 77 +++++++++++++++++++++++++--- src/erlydtl_scanner.erl | 31 +++-------- tests/src/erlydtl_extension_test.erl | 11 +++- tests/src/erlydtl_unittests.erl | 6 ++- 7 files changed, 95 insertions(+), 42 deletions(-) diff --git a/Makefile b/Makefile index 657228a..9c84b75 100755 --- a/Makefile +++ b/Makefile @@ -1,14 +1,15 @@ ERL=erl +ERLC=erlc REBAR=./rebar - all: compile -compile: +compile: @$(REBAR) compile compile_test: -mkdir -p ebintest + $(ERLC) -o tests/src -I include/erlydtl_preparser.hrl tests/src/erlydtl_extension_testparser.yrl $(ERL) -make test: compile compile_test diff --git a/include/erlydtl_preparser.hrl b/include/erlydtl_preparser.hrl index 780bd69..398682e 100755 --- a/include/erlydtl_preparser.hrl +++ b/include/erlydtl_preparser.hrl @@ -29,7 +29,7 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % The parser generator will insert appropriate declarations before this line.% --export([parse/1, parse_and_scan/1, format_error/1, recover/1]). +-export([parse/1, parse_and_scan/1, format_error/1, resume/1]). -type yecc_ret() :: {'error', _} | {'ok', _}. @@ -44,7 +44,7 @@ parse_and_scan({F, A}) -> % Fun or {M, F} parse_and_scan({M, F, A}) -> yeccpars0([], {{{M, F}, A}, no_line}, 0, [], []). -recover([Tokens, Tzr, State, States, Vstack]) -> +resume([Tokens, Tzr, State, States, Vstack]) -> yeccpars0(Tokens, Tzr, State, States, Vstack). -spec format_error(any()) -> [char() | list()]. @@ -140,7 +140,7 @@ yeccpars1(State1, State, States, Vstack, Token0, [Token | Tokens], Tzr) -> ?checkparse( yeccpars2(State, element(1, Token), [State1 | States], [Token0 | Vstack], Token, Tokens, Tzr), - [[Token0, Token|Tokens], Tzr, [State, State1|States], Vstack] + [[Token0, Token | Tokens], Tzr, State1, States, Vstack] ); yeccpars1(State1, State, States, Vstack, Token0, [], {{_F,_A}, _Line}=Tzr) -> yeccpars1([], Tzr, State, [State1 | States], [Token0 | Vstack]); diff --git a/rebar.config b/rebar.config index 0f5d40e..9b940e4 100644 --- a/rebar.config +++ b/rebar.config @@ -1 +1,2 @@ {erl_opts, [debug_info]}. +{yrl_opts, [{includefile, "include/erlydtl_preparser.hrl"}]}. diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 922c1b4..d0963ca 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -2,6 +2,7 @@ %%% File: erlydtl_compiler.erl %%% @author Roberto Saccon [http://rsaccon.com] %%% @author Evan Miller +%%% @author Andreas Stenius %%% @copyright 2008 Roberto Saccon, Evan Miller %%% @doc %%% ErlyDTL template compiler @@ -34,6 +35,7 @@ -module(erlydtl_compiler). -author('rsaccon@gmail.com'). -author('emmiller@gmail.com'). +-author('Andreas Stenius '). %% -------------------------------------------------------------------- %% Definitions @@ -314,8 +316,6 @@ parse(Data) -> parse(Data, Context) when is_binary(Data) -> check_scan(erlydtl_scanner:scan(binary_to_list(Data)), Context); -parse(State, Context) when is_tuple(State) -> - check_scan(erlydtl_scanner:scan(State), Context); parse(File, Context) -> {M, F} = Context#dtl_context.reader, case catch M:F(File) of @@ -346,24 +346,85 @@ parse(CheckSum, Data, Context) -> end end. -check_scan({ok, Tokens}, _Context) -> - erlydtl_parser:parse(Tokens); +recover(Mod, Fun, Args) + when is_atom(Mod), is_atom(Fun), is_list(Args) -> + M = case code:is_loaded(Mod) of + false -> + case code:load_file(Mod) of + {module, Mod} -> + Mod; + _ -> + undefined + end; + _ -> Mod + end, + if M /= undefined -> + case erlang:function_exported(M, Fun, length(Args)) of + true -> + apply(M, Fun, Args); + false -> + undefined + end; + true -> + undefined + end. + +check_scan({ok, Tokens}, Context) -> + check_parse(erlydtl_parser:parse(Tokens), [], Context); check_scan({error, Err, State}, Context) -> case Context#dtl_context.extension_module of undefined -> {error, Err}; M -> - case erlydtl_scanner:recover(M, State) of + case recover(M, scan, [State]) of {ok, NewState} -> %% io:format("recover from:~p~nto: ~p~n", [State, NewState]), - parse(NewState, Context); + check_scan(erlydtl_scanner:resume(NewState), Context); undefined -> {error, Err}; - ExtErr -> - ExtErr + ExtRes -> + ExtRes end end. +check_parse({ok, _}=Ok, [], _Context) -> Ok; +check_parse({ok, _, _}=Ok, [], _Context) -> Ok; +check_parse({ok, Parsed}, Acc, _Context) -> {ok, Acc ++ Parsed}; +check_parse({ok, Parsed, C}, Acc, _Context) -> {ok, Acc ++ Parsed, C}; +check_parse({error, _}=Err, _, _Context) -> Err; +check_parse({error, Err, State}, Acc, Context) -> + io:format("parse error: ~p~nstate: ~p~n",[Err, State]), + case Context#dtl_context.extension_module of + undefined -> + {error, Err}; + M -> + {State1, Parsed} = reset_parse_state(State), + case recover(M, parse, [State1]) of + {ok, ExtParsed} -> + io:format("parse recovered: ~p~n", [ExtParsed]), + {ok, Acc ++ Parsed ++ ExtParsed}; + {error, ExtErr, ExtState} -> + case reset_parse_state(ExtState) of + {_, []} -> + %% todo: see if this is indeed a sensible ext error, + %% or if we should rather present the original Err message + {error, ExtErr}; + {State2, ExtParsed} -> + check_parse(erlydtl_parser:resume(State2), Acc ++ Parsed ++ ExtParsed, Context) + end; + undefined -> + {error, Err}; + ExtRes -> + ExtRes + end + end. + +%% backtrack up to the Rootsymbol, and keep the current top-level value stack +reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]]) -> + {[Ts, Tzr, 0, [], []], Parsed}; +reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]]) -> + reset_parse_state([[T | Ts], Tzr, S, Ss, Stack]). + custom_tags_ast(CustomTags, Context, TreeWalker) -> {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} = custom_tags_clauses_ast(CustomTags, Context, TreeWalker), {{erl_syntax:function(erl_syntax:atom(render_tag), CustomTagsClauses), CustomTagsInfo}, TreeWalker1}. diff --git a/src/erlydtl_scanner.erl b/src/erlydtl_scanner.erl index cf4e499..6f4bef5 100644 --- a/src/erlydtl_scanner.erl +++ b/src/erlydtl_scanner.erl @@ -2,6 +2,7 @@ %%% File: erlydtl_scanner.erl %%% @author Roberto Saccon [http://rsaccon.com] %%% @author Evan Miller +%%% @author Andreas Stenius %%% @copyright 2008 Roberto Saccon, Evan Miller %%% @doc %%% Template language scanner @@ -34,8 +35,9 @@ -module(erlydtl_scanner). -author('rsaccon@gmail.com'). -author('emmiller@gmail.com'). +-author('Andreas Stenius '). --export([scan/1, recover/2]). +-export([scan/1, resume/1]). -include("erlydtl_ext.hrl"). @@ -50,33 +52,12 @@ %% an error. %% @end %%-------------------------------------------------------------------- -scan(#scanner_state{ template=Template, scanned=Scanned, - pos=Pos, state=State}) -> - scan(Template, Scanned, Pos, State); scan(Template) -> scan(Template, [], {1, 1}, in_text). -recover(Mod, State) -> - M = case code:is_loaded(Mod) of - false -> - case code:load_file(Mod) of - {module, Mod} -> - Mod; - _ -> - undefined - end; - _ -> Mod - end, - if M /= undefined -> - case erlang:function_exported(M, scan, 1) of - true -> - M:scan(State); - false -> - undefined - end; - true -> - undefined - end. +resume(#scanner_state{ template=Template, scanned=Scanned, + pos=Pos, state=State}) -> + scan(Template, Scanned, Pos, State). scan([], Scanned, _, in_text) -> Tokens = lists:reverse(Scanned), diff --git a/tests/src/erlydtl_extension_test.erl b/tests/src/erlydtl_extension_test.erl index efef5ba..d00f1d7 100755 --- a/tests/src/erlydtl_extension_test.erl +++ b/tests/src/erlydtl_extension_test.erl @@ -1,6 +1,6 @@ -module(erlydtl_extension_test). --export([scan/1]). +-export([scan/1, parse/1]). -include("erlydtl_ext.hrl"). %% look for a foo identifer followed by a # @@ -11,9 +11,16 @@ scan(#scanner_state{ template="#" ++ T, {ok, S#scanner_state{ template=T, scanned=[{identifier, Loc, "rab"}|Scanned], pos={L, C+1} }}; -scan(#scanner_state{ template="#" ++ T, pos={L, C} }) -> +scan(#scanner_state{ template="#" ++ _T, pos={L, C} }) -> %% give error when # not follows foo {error, {L,?MODULE,lists:concat(["Unexpected '#' in code at column ", C])}}; scan(_) -> %% for anything else, fallback to the error message from erlydtl_scanner.. undefined. + +parse(State) -> + io:format("extension:parse got: ~p~n", [State]), + %code:load_file(erlydtl_extension_testparser), + Res=erlydtl_extension_testparser:resume(State), + io:format("extension result: ~p~n", [Res]), + Res. diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index ce77bed..db03076 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -1104,10 +1104,12 @@ tests() -> ]}, %% custom syntax stuff {"extension_module", [ - %% the erlydtl_extension_test module replaces a foo identifier with bar when hitting a #. + %% the erlydtl_extension_test module replaces a foo identifier with bar when hitting a # following foo. {"replace parsed token", <<"{{ foo # }}">>, [{bar, "ok"}], [], [{extension_module, erlydtl_extension_test}], <<"ok">>}, {"proper error message", <<"{{ bar # }}">>, [{bar, "ok"}], [], [{extension_module, erlydtl_extension_test}], - {error, {1,erlydtl_extension_test,"Unexpected '#' in code at column 8"}}} + {error, {1,erlydtl_extension_test,"Unexpected '#' in code at column 8"}}}, + %% accept identifiers as expressions (this is a dummy functionality to test the parser extensibility) + {"identifiers as expressions", <<"{{ test }} data {{ foo.bar or baz }}">>, [{baz, "ok"}], [], [{extension_module, erlydtl_extension_test}], <<"ok">>} ]} ]. From 973d527c03aff4bf11a0761380ffa97be6e93ea2 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 20 Jun 2013 15:56:23 +0200 Subject: [PATCH 020/361] Add support for 'extension' ast tags. When the parser tags a ast token with a {extension, Tag} tuple, the Tag is passed to the extension module for compilation. The test case for the extension module adds the functionality to write {{ varA or varB }} Which is equivalent to {% if varA %}{{ varA }}{% else %}{{ varB }}{% endif %} With a few (hrmm... ok, maybe not so few) lines of code ;) --- src/erlydtl_compiler.erl | 73 +++++++++++++++------------- tests/src/erlydtl_extension_test.erl | 17 ++++--- tests/src/erlydtl_unittests.erl | 2 +- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index d0963ca..b20bdda 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -42,6 +42,9 @@ %% -------------------------------------------------------------------- -export([compile/2, compile/3, compile_dir/2, compile_dir/3, parse/1]). +%% exported for use by extension modules +-export([merge_info/2, value_ast/5]). + -record(dtl_context, { local_scopes = [], block_dict = dict:new(), @@ -346,6 +349,7 @@ parse(CheckSum, Data, Context) -> end end. +recover(undefined, _Fun, _Args) -> undefined; recover(Mod, Fun, Args) when is_atom(Mod), is_atom(Fun), is_list(Args) -> M = case code:is_loaded(Mod) of @@ -372,19 +376,14 @@ recover(Mod, Fun, Args) check_scan({ok, Tokens}, Context) -> check_parse(erlydtl_parser:parse(Tokens), [], Context); check_scan({error, Err, State}, Context) -> - case Context#dtl_context.extension_module of - undefined -> - {error, Err}; - M -> - case recover(M, scan, [State]) of - {ok, NewState} -> - %% io:format("recover from:~p~nto: ~p~n", [State, NewState]), - check_scan(erlydtl_scanner:resume(NewState), Context); - undefined -> - {error, Err}; - ExtRes -> - ExtRes - end + case recover(Context#dtl_context.extension_module, scan, [State]) of + undefined -> + {error, Err}; + {ok, NewState} -> + %% io:format("recover from:~p~nto: ~p~n", [State, NewState]), + check_scan(erlydtl_scanner:resume(NewState), Context); + ExtRes -> + ExtRes end. check_parse({ok, _}=Ok, [], _Context) -> Ok; @@ -393,30 +392,24 @@ check_parse({ok, Parsed}, Acc, _Context) -> {ok, Acc ++ Parsed}; check_parse({ok, Parsed, C}, Acc, _Context) -> {ok, Acc ++ Parsed, C}; check_parse({error, _}=Err, _, _Context) -> Err; check_parse({error, Err, State}, Acc, Context) -> - io:format("parse error: ~p~nstate: ~p~n",[Err, State]), - case Context#dtl_context.extension_module of + %% io:format("parse error: ~p~nstate: ~p~n",[Err, State]), + {State1, Parsed} = reset_parse_state(State), + case recover(Context#dtl_context.extension_module, parse, [State1]) of undefined -> {error, Err}; - M -> - {State1, Parsed} = reset_parse_state(State), - case recover(M, parse, [State1]) of - {ok, ExtParsed} -> - io:format("parse recovered: ~p~n", [ExtParsed]), - {ok, Acc ++ Parsed ++ ExtParsed}; - {error, ExtErr, ExtState} -> - case reset_parse_state(ExtState) of - {_, []} -> - %% todo: see if this is indeed a sensible ext error, - %% or if we should rather present the original Err message - {error, ExtErr}; - {State2, ExtParsed} -> - check_parse(erlydtl_parser:resume(State2), Acc ++ Parsed ++ ExtParsed, Context) - end; - undefined -> - {error, Err}; - ExtRes -> - ExtRes - end + {ok, ExtParsed} -> + {ok, Acc ++ Parsed ++ ExtParsed}; + {error, ExtErr, ExtState} -> + case reset_parse_state(ExtState) of + {_, []} -> + %% todo: see if this is indeed a sensible ext error, + %% or if we should rather present the original Err message + {error, ExtErr}; + {State2, ExtParsed} -> + check_parse(erlydtl_parser:resume(State2), Acc ++ Parsed ++ ExtParsed, Context) + end; + ExtRes -> + ExtRes end. %% backtrack up to the Rootsymbol, and keep the current top-level value stack @@ -731,6 +724,8 @@ body_ast(DjangoParseTree, Context, TreeWalker) -> widthratio_ast(Numerator, Denominator, Scale, Context, TreeWalkerAcc); ({'with', Args, Contents}, TreeWalkerAcc) -> with_ast(Args, Contents, Context, TreeWalkerAcc); + ({'extension', Tag}, TreeWalkerAcc) -> + extension_ast(Tag, Context, TreeWalkerAcc); (ValueToken, TreeWalkerAcc) -> {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, true, Context, TreeWalkerAcc), {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker} @@ -796,6 +791,14 @@ value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) -> {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker} end. +extension_ast(Tag, Context, TreeWalker) -> + case recover(Context#dtl_context.extension_module, compile_ast, [Tag, Context, TreeWalker]) of + undefined -> + throw({error, {unknown_extension, Tag}}); + Result -> + Result + end. + merge_info(Info1, Info2) -> #ast_info{ dependencies = diff --git a/tests/src/erlydtl_extension_test.erl b/tests/src/erlydtl_extension_test.erl index d00f1d7..be481ad 100755 --- a/tests/src/erlydtl_extension_test.erl +++ b/tests/src/erlydtl_extension_test.erl @@ -1,6 +1,6 @@ -module(erlydtl_extension_test). --export([scan/1, parse/1]). +-export([scan/1, parse/1, compile_ast/3]). -include("erlydtl_ext.hrl"). %% look for a foo identifer followed by a # @@ -19,8 +19,13 @@ scan(_) -> undefined. parse(State) -> - io:format("extension:parse got: ~p~n", [State]), - %code:load_file(erlydtl_extension_testparser), - Res=erlydtl_extension_testparser:resume(State), - io:format("extension result: ~p~n", [Res]), - Res. + erlydtl_extension_testparser:resume(State). + +%% {{ varA or varB }} is equivalent to {% if varA %}{{ varA }}{% else %}{{ varB }}{% endif %} +compile_ast({value_or, {Value1, Value2}}, Context, TreeWalker) -> + {{V1_Ast, V1_Info}, TW1} = erlydtl_compiler:value_ast(Value1, false, false, Context, TreeWalker), + {{V2_Ast, V2_Info}, TW2} = erlydtl_compiler:value_ast(Value2, false, false, Context, TW1), + {{erl_syntax:case_expr(V1_Ast, + [erl_syntax:clause([erl_syntax:atom(undefined)], none, [V2_Ast]), + erl_syntax:clause([erl_syntax:underscore()], none, [V1_Ast]) + ]), erlydtl_compiler:merge_info(V1_Info, V2_Info)}, TW2}. diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index db03076..d92f96c 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -1109,7 +1109,7 @@ tests() -> {"proper error message", <<"{{ bar # }}">>, [{bar, "ok"}], [], [{extension_module, erlydtl_extension_test}], {error, {1,erlydtl_extension_test,"Unexpected '#' in code at column 8"}}}, %% accept identifiers as expressions (this is a dummy functionality to test the parser extensibility) - {"identifiers as expressions", <<"{{ test }} data {{ foo.bar or baz }}">>, [{baz, "ok"}], [], [{extension_module, erlydtl_extension_test}], <<"ok">>} + {"identifiers as expressions", <<"{{ foo.bar or baz }}">>, [{baz, "ok"}], [], [{extension_module, erlydtl_extension_test}], <<"ok">>} ]} ]. From c9fb6dfb0eeb9640e3812ff55003c1c9eae7ff6c Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 20 Jun 2013 16:01:13 +0200 Subject: [PATCH 021/361] Add missing test parser. Oops. This was hiding in the long list of files that isn't ignored by git (yet)... --- tests/src/erlydtl_extension_testparser.yrl | 153 +++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 tests/src/erlydtl_extension_testparser.yrl diff --git a/tests/src/erlydtl_extension_testparser.yrl b/tests/src/erlydtl_extension_testparser.yrl new file mode 100644 index 0000000..0c5fe7e --- /dev/null +++ b/tests/src/erlydtl_extension_testparser.yrl @@ -0,0 +1,153 @@ +%%% -*- mode: erlang -*- ------------------------------------------------------------------ +%%% File: erlydtl_parser.erl +%%% @author Andreas Stenius +%%% @copyright 2013 Andreas Stenius +%%% @doc Sample extension grammar +%%% @reference See http://erlydtl.googlecode.com for more information +%%% @end +%%% +%%% The MIT License +%%% +%%% Copyright (c) 2013 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. +%%% +%%% @since 2013-06-20 by Andreas Stenius +%%%------------------------------------------------------------------- + +Nonterminals + Extensions + Literal + + ValueExpressionBraced + + ValueExpression + Value + Variable +. + +Terminals + %% "new" terminals that are partially parsed tokens from the erlydtl parser: + variable + + %% standard scanner tokens: + + %% and_keyword + %% as_keyword + %% autoescape_keyword + %% block_keyword + %% blocktrans_keyword + %% by_keyword + %% call_keyword + %% close_tag + close_var + %% comment_keyword + %% cycle_keyword + %% elif_keyword + %% else_keyword + %% empty_keyword + %% endautoescape_keyword + %% endblock_keyword + %% endblocktrans_keyword + %% endcomment_keyword + %% endfilter_keyword + %% endfor_keyword + %% endif_keyword + %% endifchanged_keyword + %% endifequal_keyword + %% endifnotequal_keyword + %% endregroup_keyword + %% endspaceless_keyword + %% endwith_keyword + %% extends_keyword + %% filter_keyword + %% firstof_keyword + %% for_keyword + identifier + %% if_keyword + %% ifchanged_keyword + %% ifequal_keyword + %% ifnotequal_keyword + %% in_keyword + %% include_keyword + %% noop_keyword + %% not_keyword + %% now_keyword + number_literal + %% only_keyword + or_keyword + %% open_tag + open_var + %% parsed_keyword + %% regroup_keyword + %% reversed_keyword + %% spaceless_keyword + %% 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 + %% ',' '|' '=' ':' + '.' + %% '==' '!=' + %% '>=' '<=' + %% '>' '<' + %% '(' ')' + %% '_' +. + +Rootsymbol + Extensions. + +%% Operator precedences for the E non terminal +Left 100 or_keyword. +%Left 110 and_keyword. +%Nonassoc 300 '==' '!=' '>=' '<=' '>' '<'. +%Unary 600 Unot. + +Extensions -> ValueExpressionBraced : ['$1']. + +ValueExpressionBraced -> open_var ValueExpression close_var : '$2'. + +ValueExpression -> Value or_keyword Value : {extension, {value_or, {'$1', '$3'}}}. + +%Value -> Value '|' Filter : {apply_filter, '$1', '$3'}. +%Value -> '_' '(' Value ')' : {trans, '$3'}. +Value -> Variable : '$1'. +Value -> Literal : '$1'. + +Variable -> identifier : {variable, '$1'}. +Variable -> variable : '$1'. +Variable -> Variable '.' identifier : {attribute, {'$3', '$1'}}. + +Literal -> string_literal : '$1'. +Literal -> number_literal : '$1'. + + +%% vim: syntax=erlang From 7d1750ef7860e9db83f646d4eda52c8a32b33ac8 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Tue, 25 Jun 2013 13:59:13 +0100 Subject: [PATCH 022/361] move compiler records to header file. --- include/erlydtl_ext.hrl | 36 ++++++++++++++++++++++++++++++++++++ src/erlydtl_compiler.erl | 36 +----------------------------------- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/include/erlydtl_ext.hrl b/include/erlydtl_ext.hrl index 2dad8fd..f1c450b 100755 --- a/include/erlydtl_ext.hrl +++ b/include/erlydtl_ext.hrl @@ -1,4 +1,40 @@ +-record(dtl_context, { + local_scopes = [], + block_dict = dict:new(), + blocktrans_fun = none, + blocktrans_locales = [], + auto_escape = off, + doc_root = "", + parse_trail = [], + vars = [], + filter_modules = [], + custom_tags_dir = [], + custom_tags_modules = [], + reader = {file, read_file}, + module = [], + compiler_options = [verbose, report_errors], + binary_strings = true, + force_recompile = false, + locale = none, + verbose = false, + is_compiling_dir = false, + extension_module = undefined + }). + +-record(ast_info, { + dependencies = [], + translatable_strings = [], + translated_blocks= [], + custom_tags = [], + var_names = [], + pre_render_asts = []}). + +-record(treewalker, { + counter = 0, + safe = false + }). + -record(scanner_state, { template=[], diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index b20bdda..72e227f 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -45,41 +45,7 @@ %% exported for use by extension modules -export([merge_info/2, value_ast/5]). --record(dtl_context, { - local_scopes = [], - block_dict = dict:new(), - blocktrans_fun = none, - blocktrans_locales = [], - auto_escape = off, - doc_root = "", - parse_trail = [], - vars = [], - filter_modules = [], - custom_tags_dir = [], - custom_tags_modules = [], - reader = {file, read_file}, - module = [], - compiler_options = [verbose, report_errors], - binary_strings = true, - force_recompile = false, - locale = none, - verbose = false, - is_compiling_dir = false, - extension_module = undefined - }). - --record(ast_info, { - dependencies = [], - translatable_strings = [], - translated_blocks= [], - custom_tags = [], - var_names = [], - pre_render_asts = []}). - --record(treewalker, { - counter = 0, - safe = false - }). +-include("erlydtl_ext.hrl"). compile(Binary, Module) when is_binary(Binary) -> compile(Binary, Module, []); From d02e849e63a2374e483b0520d1cf743704c5c52d Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Tue, 25 Jun 2013 15:32:31 +0100 Subject: [PATCH 023/361] renamed recover function to be more generic. It is now called call_extension, and is used to extend the compiler functionlity at interesting points. In this commit, the options_match_ast function got such an extension point. --- src/erlydtl_compiler.erl | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 72e227f..9c08f8c 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -157,7 +157,7 @@ write_binary(Module1, Bin, Options, Warnings) -> end. compile_multiple_to_binary(Dir, ParserResults, Context) -> - MatchAst = options_match_ast(), + MatchAst = options_match_ast(Context), {Functions, {AstInfo, _}} = lists:mapfoldl(fun({File, DjangoParseTree, CheckSum}, {AstInfo, TreeWalker}) -> FilePath = full_path(File, Context#dtl_context.doc_root), {{BodyAst, BodyInfo}, TreeWalker1} = with_dependency({FilePath, CheckSum}, body_ast(DjangoParseTree, Context, TreeWalker)), @@ -180,7 +180,7 @@ compile_to_binary(File, DjangoParseTree, Context, CheckSum) -> try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of {{CustomTagsAst, CustomTagsInfo}, _} -> Forms = forms(File, Context#dtl_context.module, {BodyAst, BodyInfo}, - {CustomTagsAst, CustomTagsInfo}, Context#dtl_context.binary_strings, CheckSum), + {CustomTagsAst, CustomTagsInfo}, CheckSum, Context), compile_forms_and_reload(File, Forms, Context#dtl_context.compiler_options) catch throw:Error -> Error @@ -315,8 +315,9 @@ parse(CheckSum, Data, Context) -> end end. -recover(undefined, _Fun, _Args) -> undefined; -recover(Mod, Fun, Args) +call_extension(#dtl_context{ extension_module=undefined }, _Fun, _Args) -> + undefined; +call_extension(#dtl_context{ extension_module=Mod }, Fun, Args) when is_atom(Mod), is_atom(Fun), is_list(Args) -> M = case code:is_loaded(Mod) of false -> @@ -342,11 +343,11 @@ recover(Mod, Fun, Args) check_scan({ok, Tokens}, Context) -> check_parse(erlydtl_parser:parse(Tokens), [], Context); check_scan({error, Err, State}, Context) -> - case recover(Context#dtl_context.extension_module, scan, [State]) of + case call_extension(Context, scan, [State]) of undefined -> {error, Err}; {ok, NewState} -> - %% io:format("recover from:~p~nto: ~p~n", [State, NewState]), + %% io:format("call_extension from:~p~nto: ~p~n", [State, NewState]), check_scan(erlydtl_scanner:resume(NewState), Context); ExtRes -> ExtRes @@ -360,7 +361,7 @@ check_parse({error, _}=Err, _, _Context) -> Err; check_parse({error, Err, State}, Acc, Context) -> %% io:format("parse error: ~p~nstate: ~p~n",[Err, State]), {State1, Parsed} = reset_parse_state(State), - case recover(Context#dtl_context.extension_module, parse, [State1]) of + case call_extension(Context, parse, [State1]) of undefined -> {error, Err}; {ok, ExtParsed} -> @@ -407,7 +408,7 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Cont {ok, DjangoParseTree, CheckSum} -> {{BodyAst, BodyAstInfo}, TreeWalker1} = with_dependency( {CustomTagFile, CheckSum}, body_ast(DjangoParseTree, Context, TreeWalker)), - MatchAst = options_match_ast(), + MatchAst = options_match_ast(Context), Clause = erl_syntax:clause( [key_to_string(Tag), erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], none, MatchAst ++ [BodyAst]), @@ -474,7 +475,7 @@ custom_forms(Dir, Module, Functions, AstInfo) -> [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsFunctionAst | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts]. -forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, BinaryStrings, CheckSum) -> +forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum, Context) -> MergedInfo = merge_info(BodyInfo, CustomTagsInfo), Render0FunctionAst = erl_syntax:function(erl_syntax:atom(render), [erl_syntax:clause([], none, [erl_syntax:application(none, @@ -509,13 +510,13 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo} VariablesAst = variables_function(MergedInfo#ast_info.var_names), - MatchAst = options_match_ast(), + MatchAst = options_match_ast(Context), BodyAstTmp = MatchAst ++ [ erl_syntax:application( erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(stringify_final), - [BodyAst, erl_syntax:atom(BinaryStrings)]) + [BodyAst, erl_syntax:atom(Context#dtl_context.binary_strings)]) ], RenderInternalFunctionAst = erl_syntax:function( @@ -544,7 +545,7 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo} TranslatedBlocksAst, VariablesAst, RenderInternalFunctionAst, CustomTagsFunctionAst | BodyInfo#ast_info.pre_render_asts]]. -options_match_ast() -> +options_match_ast(Context) -> [ erl_syntax:match_expr( erl_syntax:variable("_TranslationFun"), @@ -558,6 +559,10 @@ options_match_ast() -> erl_syntax:atom(proplists), erl_syntax:atom(get_value), [erl_syntax:atom(locale), erl_syntax:variable("RenderOptions"), erl_syntax:atom(none)])) + | case call_extension(Context, setup_render_ast, []) of + undefined -> []; + Ast when is_list(Ast) -> Ast + end ]. % child templates should only consist of blocks at the top level @@ -758,7 +763,7 @@ value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) -> end. extension_ast(Tag, Context, TreeWalker) -> - case recover(Context#dtl_context.extension_module, compile_ast, [Tag, Context, TreeWalker]) of + case call_extension(Context, compile_ast, [Tag, Context, TreeWalker]) of undefined -> throw({error, {unknown_extension, Tag}}); Result -> From 650828cff172342554f8524665941db286f5cdba Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Wed, 26 Jun 2013 15:40:54 +0100 Subject: [PATCH 024/361] add more extendability.. minor api tweaks. --- include/erlydtl_ext.hrl | 74 +++++++++++++------------- src/erlydtl_compiler.erl | 111 +++++++++++++++++++++++++++++---------- 2 files changed, 119 insertions(+), 66 deletions(-) diff --git a/include/erlydtl_ext.hrl b/include/erlydtl_ext.hrl index f1c450b..ba92f93 100755 --- a/include/erlydtl_ext.hrl +++ b/include/erlydtl_ext.hrl @@ -1,44 +1,44 @@ -record(dtl_context, { - local_scopes = [], - block_dict = dict:new(), - blocktrans_fun = none, - blocktrans_locales = [], - auto_escape = off, - doc_root = "", - parse_trail = [], - vars = [], - filter_modules = [], - custom_tags_dir = [], - custom_tags_modules = [], - reader = {file, read_file}, - module = [], - compiler_options = [verbose, report_errors], - binary_strings = true, - force_recompile = false, - locale = none, - verbose = false, - is_compiling_dir = false, - extension_module = undefined - }). + local_scopes = [], + block_dict = dict:new(), + blocktrans_fun = none, + blocktrans_locales = [], + auto_escape = off, + doc_root = "", + parse_trail = [], + vars = [], + filter_modules = [], + custom_tags_dir = [], + custom_tags_modules = [], + reader = {file, read_file}, + module = [], + compiler_options = [verbose, report_errors], + binary_strings = true, + force_recompile = false, + locale = none, + verbose = false, + is_compiling_dir = false, + extension_module = undefined + }). -record(ast_info, { - dependencies = [], - translatable_strings = [], - translated_blocks= [], - custom_tags = [], - var_names = [], - pre_render_asts = []}). + dependencies = [], + translatable_strings = [], + translated_blocks= [], + custom_tags = [], + var_names = [], + pre_render_asts = []}). -record(treewalker, { - counter = 0, - safe = false - }). + counter = 0, + safe = false, + extension = undefined + }). --record(scanner_state, - { - template=[], - scanned=[], - pos={1,1}, - state=in_text - }). +-record(scanner_state, { + template=[], + scanned=[], + pos={1,1}, + state=in_text + }). diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 9c08f8c..dba1a10 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -43,7 +43,12 @@ -export([compile/2, compile/3, compile_dir/2, compile_dir/3, parse/1]). %% exported for use by extension modules --export([merge_info/2, value_ast/5]). +-export([ + merge_info/2, + format/3, + value_ast/5, + resolve_scoped_variable_ast/2 + ]). -include("erlydtl_ext.hrl"). @@ -170,18 +175,18 @@ compile_multiple_to_binary(Dir, ParserResults, Context) -> [erl_syntax:clause([erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], none, MatchAst ++ [BodyAst])]), {{FunctionName, Function1, Function2}, {merge_info(AstInfo, BodyInfo), TreeWalker1}} - end, {#ast_info{}, #treewalker{}}, ParserResults), + end, {#ast_info{}, init_treewalker(Context)}, ParserResults), Forms = custom_forms(Dir, Context#dtl_context.module, Functions, AstInfo), - compile_forms_and_reload(Dir, Forms, Context#dtl_context.compiler_options). + compile_forms_and_reload(Dir, Forms, Context). compile_to_binary(File, DjangoParseTree, Context, CheckSum) -> - try body_ast(DjangoParseTree, Context, #treewalker{}) of + try body_ast(DjangoParseTree, Context, init_treewalker(Context)) of {{BodyAst, BodyInfo}, BodyTreeWalker} -> try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of {{CustomTagsAst, CustomTagsInfo}, _} -> Forms = forms(File, Context#dtl_context.module, {BodyAst, BodyInfo}, - {CustomTagsAst, CustomTagsInfo}, CheckSum, Context), - compile_forms_and_reload(File, Forms, Context#dtl_context.compiler_options) + {CustomTagsAst, CustomTagsInfo}, CheckSum, Context, BodyTreeWalker), + compile_forms_and_reload(File, Forms, Context) catch throw:Error -> Error end @@ -189,14 +194,21 @@ compile_to_binary(File, DjangoParseTree, Context, CheckSum) -> throw:Error -> Error end. -compile_forms_and_reload(File, Forms, CompilerOptions) -> - case proplists:get_value(debug_compiler, CompilerOptions) of +compile_forms_and_reload(File, Forms, Context) -> + case proplists:get_value(debug_compiler, Context#dtl_context.compiler_options) of true -> - io:format("Template ~p compiled with options: ~p~n", [File, CompilerOptions]), - [io:format("~s~n", [erl_pp:form(Form)]) || Form <- Forms]; + io:format("template source:~n~s~n", + [try + erl_prettypr:format(erl_syntax:form_list(Forms)) + catch + error:Err -> + io_lib:format("Pretty printing failed: ~p~nContext: ~n~p~nForms: ~n~p~n", + [Err, Context, Forms]) + end + ]); _ -> nop end, - case compile:forms(Forms, CompilerOptions) of + case compile:forms(Forms, Context#dtl_context.compiler_options) of {ok, Module1, Bin} -> load_code(Module1, Bin, []); {ok, Module1, Bin, Warnings} -> @@ -216,7 +228,7 @@ load_code(Module, Bin, Warnings) -> init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) -> Ctx = #dtl_context{}, - #dtl_context{ + Context = #dtl_context{ parse_trail = ParseTrail, module = Module, doc_root = proplists:get_value(doc_root, Options, DefDir), @@ -234,7 +246,11 @@ init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) -> verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose), is_compiling_dir = IsCompilingDir, extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module) - }. + }, + case call_extension(Context, init_context, [Context]) of + {ok, C} when is_record(C, dtl_context) -> C; + undefined -> Context + end. init_dtl_context(File, Module, Options) when is_list(Module) -> init_dtl_context(File, list_to_atom(Module), Options); @@ -246,6 +262,13 @@ init_dtl_context_dir(Dir, Module, Options) when is_list(Module) -> init_dtl_context_dir(Dir, Module, Options) -> init_context(true, [], Dir, Module, Options). +init_treewalker(Context) -> + TreeWalker = #treewalker{}, + case call_extension(Context, init_treewalker, [TreeWalker]) of + {ok, TW} when is_record(TW, treewalker) -> TW; + undefined -> TreeWalker + end. + is_up_to_date(_, #dtl_context{force_recompile = true}) -> false; is_up_to_date(CheckSum, Context) -> @@ -341,7 +364,11 @@ call_extension(#dtl_context{ extension_module=Mod }, Fun, Args) end. check_scan({ok, Tokens}, Context) -> - check_parse(erlydtl_parser:parse(Tokens), [], Context); + Tokens1 = case call_extension(Context, post_scan, [Tokens]) of + undefined -> Tokens; + {ok, T} -> T + end, + check_parse(erlydtl_parser:parse(Tokens1), [], Context); check_scan({error, Err, State}, Context) -> case call_extension(Context, scan, [State]) of undefined -> @@ -383,19 +410,39 @@ check_parse({error, Err, State}, Acc, Context) -> reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]]) -> {[Ts, Tzr, 0, [], []], Parsed}; reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]]) -> - reset_parse_state([[T | Ts], Tzr, S, Ss, Stack]). + reset_parse_state([[T | Ts], Tzr, S, Ss, Stack]); +reset_parse_state([_, _, 0, [], []]=State) -> + {State, []}. custom_tags_ast(CustomTags, Context, TreeWalker) -> {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} = custom_tags_clauses_ast(CustomTags, Context, TreeWalker), - {{erl_syntax:function(erl_syntax:atom(render_tag), CustomTagsClauses), CustomTagsInfo}, TreeWalker1}. + %% This doesn't work since erl_syntax:revert/1 chokes on the airity_qualifier in the -compile attribute.. + %% bug report sent to the erlang-bugs mailing list. + %% {{erl_syntax:form_list( + %% [erl_syntax:attribute( + %% erl_syntax:atom(compile), + %% [erl_syntax:tuple( + %% [erl_syntax:atom(nowarn_unused_function), + %% erl_syntax:arity_qualifier( + %% erl_syntax:atom(render_tag), + %% erl_syntax:integer(3))])]), + %% erl_syntax:function(erl_syntax:atom(render_tag), CustomTagsClauses)]), + {{erl_syntax:function(erl_syntax:atom(render_tag), CustomTagsClauses), + CustomTagsInfo}, + TreeWalker1}. custom_tags_clauses_ast(CustomTags, Context, TreeWalker) -> custom_tags_clauses_ast1(CustomTags, [], [], #ast_info{}, Context, TreeWalker). custom_tags_clauses_ast1([], _ExcludeTags, ClauseAcc, InfoAcc, _Context, TreeWalker) -> - {{lists:reverse([erl_syntax:clause([erl_syntax:variable("TagName"), erl_syntax:underscore(), erl_syntax:underscore()], none, - [erl_syntax:list([])])|ClauseAcc - ]), InfoAcc}, TreeWalker}; + {{lists:reverse( + [erl_syntax:clause( + [erl_syntax:variable("_TagName"), erl_syntax:underscore(), erl_syntax:underscore()], + none, + [erl_syntax:list([])]) + |ClauseAcc]), + InfoAcc}, + TreeWalker}; custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker) -> case lists:member(Tag, ExcludeTags) of true -> @@ -408,7 +455,7 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Cont {ok, DjangoParseTree, CheckSum} -> {{BodyAst, BodyAstInfo}, TreeWalker1} = with_dependency( {CustomTagFile, CheckSum}, body_ast(DjangoParseTree, Context, TreeWalker)), - MatchAst = options_match_ast(Context), + MatchAst = options_match_ast(Context, TreeWalker), Clause = erl_syntax:clause( [key_to_string(Tag), erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], none, MatchAst ++ [BodyAst]), @@ -475,7 +522,7 @@ custom_forms(Dir, Module, Functions, AstInfo) -> [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsFunctionAst | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts]. -forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum, Context) -> +forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum, Context, TreeWalker) -> MergedInfo = merge_info(BodyInfo, CustomTagsInfo), Render0FunctionAst = erl_syntax:function(erl_syntax:atom(render), [erl_syntax:clause([], none, [erl_syntax:application(none, @@ -510,7 +557,7 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo} VariablesAst = variables_function(MergedInfo#ast_info.var_names), - MatchAst = options_match_ast(Context), + MatchAst = options_match_ast(Context, TreeWalker), BodyAstTmp = MatchAst ++ [ erl_syntax:application( @@ -540,12 +587,16 @@ forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo} erl_syntax:arity_qualifier(erl_syntax:atom(variables), erl_syntax:integer(0)) ])]), - [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, Render0FunctionAst, Render1FunctionAst, Render2FunctionAst, - SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst, - TranslatedBlocksAst, VariablesAst, RenderInternalFunctionAst, - CustomTagsFunctionAst | BodyInfo#ast_info.pre_render_asts]]. + erl_syntax:revert_forms( + erl_syntax:form_list( + [ModuleAst, ExportAst, Render0FunctionAst, Render1FunctionAst, Render2FunctionAst, + SourceFunctionAst, DependenciesFunctionAst, TranslatableStringsAst, + TranslatedBlocksAst, VariablesAst, RenderInternalFunctionAst, + CustomTagsFunctionAst + |BodyInfo#ast_info.pre_render_asts])). -options_match_ast(Context) -> +options_match_ast(Context) -> options_match_ast(Context, undefined). +options_match_ast(Context, TreeWalker) -> [ erl_syntax:match_expr( erl_syntax:variable("_TranslationFun"), @@ -559,7 +610,7 @@ options_match_ast(Context) -> erl_syntax:atom(proplists), erl_syntax:atom(get_value), [erl_syntax:atom(locale), erl_syntax:variable("RenderOptions"), erl_syntax:atom(none)])) - | case call_extension(Context, setup_render_ast, []) of + | case call_extension(Context, setup_render_ast, [Context, TreeWalker]) of undefined -> []; Ast when is_list(Ast) -> Ast end @@ -759,7 +810,9 @@ value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) -> {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker}; {'variable', _} = Variable -> {Ast, VarName} = resolve_variable_ast(Variable, Context, EmptyIfUndefined), - {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker} + {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker}; + {extension, Tag} -> + extension_ast(Tag, Context, TreeWalker) end. extension_ast(Tag, Context, TreeWalker) -> From 68922594d54bef0d1c197442ccf71069a0bfa4c5 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 20 Jun 2013 20:33:36 +0100 Subject: [PATCH 025/361] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b43c124..f8c556f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ erl_crash.dump examples/rendered_output src/erlydtl_parser.erl *~ +ebintest +tests/src/erlydtl_extension_testparser.erl +tests/output From 4d5c8c5b8855ba0e915406ae495c02bb5eadb0ab Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Wed, 26 Jun 2013 15:37:09 -0500 Subject: [PATCH 026/361] Don't use a plus-sign to quote spaces --- src/erlydtl_filters.erl | 6 ------ tests/src/erlydtl_unittests.erl | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index 72a55ce..d807d64 100755 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -1129,14 +1129,10 @@ wordwrap([C | Rest], Acc, WordAcc, LineLength, WrapAt) when erlang:length(WordAc wordwrap([C | Rest], Acc, WordAcc, LineLength, WrapAt) -> wordwrap(Rest, Acc, [C | WordAcc], LineLength, WrapAt). -% Taken from quote_plus of mochiweb_util - urlencode(Input, Index) when is_binary(Input) -> case Input of <<_:Index/binary, Byte, _/binary>> when ?NO_ENCODE(Byte) -> urlencode(Input, Index + 1); - <> -> - process_binary_match(Pre, <<"+">>, size(Post), urlencode(Post, 0)); <> -> HiDigit = hexdigit(Hi), LoDigit = hexdigit(Lo), @@ -1149,8 +1145,6 @@ urlencode([], Acc) -> lists:reverse(Acc); urlencode([C | Rest], Acc) when ?NO_ENCODE(C) -> urlencode(Rest, [C | Acc]); -urlencode([$\s | Rest], Acc) -> - urlencode(Rest, [$+ | Acc]); urlencode([C | Rest], Acc) -> <> = <>, urlencode(Rest, [hexdigit(Lo), hexdigit(Hi), $\% | Acc]). diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index d92f96c..7776572 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -875,7 +875,7 @@ tests() -> <<"THAT MAN HAS A GUN.">>}, {"|urlencode", <<"{{ url|urlencode }}">>, [{url, "You #$*@!!"}], - <<"You+%23%24%2A%40%21%21">>}, + <<"You%20%23%24%2A%40%21%21">>}, {"|urlize", <<"{{ var|urlize }}">>, [{var, "Check out www.djangoproject.com"}], <<"Check out www.djangoproject.com">>}, From d0d824853bc87bf2e4cc52b1f7e7e0ffcaedd1d4 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 27 Jun 2013 11:43:21 +0200 Subject: [PATCH 027/361] Allow extension module to hook into the custom tags. Also changed the tagname to be passed as atom rather than list. --- src/erlydtl_compiler.erl | 55 +++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index dba1a10..7f0f991 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -434,15 +434,23 @@ custom_tags_ast(CustomTags, Context, TreeWalker) -> custom_tags_clauses_ast(CustomTags, Context, TreeWalker) -> custom_tags_clauses_ast1(CustomTags, [], [], #ast_info{}, Context, TreeWalker). -custom_tags_clauses_ast1([], _ExcludeTags, ClauseAcc, InfoAcc, _Context, TreeWalker) -> - {{lists:reverse( - [erl_syntax:clause( - [erl_syntax:variable("_TagName"), erl_syntax:underscore(), erl_syntax:underscore()], - none, - [erl_syntax:list([])]) - |ClauseAcc]), - InfoAcc}, - TreeWalker}; +custom_tags_clauses_ast1([], _ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker) -> + {{DefaultAst, DefaultInfo}, TreeWalker1} = + case call_extension(Context, custom_tag_ast, [Context, TreeWalker]) of + undefined -> + {{erl_syntax:clause( + [erl_syntax:variable("_TagName"), erl_syntax:underscore(), erl_syntax:underscore()], + none, + [erl_syntax:list([])]), + InfoAcc}, + TreeWalker}; + {{ExtAst, ExtInfo}, ExtTreeWalker} -> + Clause = erl_syntax:clause( + [erl_syntax:variable("TagName"), erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], + none, options_match_ast(Context, ExtTreeWalker) ++ [ExtAst]), + {{Clause, merge_info(ExtInfo, InfoAcc)}, ExtTreeWalker} + end, + {{lists:reverse([DefaultAst|ClauseAcc]), DefaultInfo}, TreeWalker1}; custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Context, TreeWalker) -> case lists:member(Tag, ExcludeTags) of true -> @@ -457,7 +465,7 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Cont {CustomTagFile, CheckSum}, body_ast(DjangoParseTree, Context, TreeWalker)), MatchAst = options_match_ast(Context, TreeWalker), Clause = erl_syntax:clause( - [key_to_string(Tag), erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], + [erl_syntax:atom(Tag), erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], none, MatchAst ++ [BodyAst]), custom_tags_clauses_ast1(CustomTags, [Tag|ExcludeTags], [Clause|ClauseAcc], merge_info(BodyAstInfo, InfoAcc), @@ -466,8 +474,20 @@ custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, Cont throw(Error) end; false -> - custom_tags_clauses_ast1(CustomTags, [Tag | ExcludeTags], - ClauseAcc, InfoAcc, Context, TreeWalker) + case call_extension(Context, custom_tag_ast, [Tag, Context, TreeWalker]) of + undefined -> + custom_tags_clauses_ast1( + CustomTags, [Tag | ExcludeTags], + ClauseAcc, InfoAcc, Context, TreeWalker); + {{Ast, Info}, TW} -> + Clause = erl_syntax:clause( + [erl_syntax:atom(Tag), erl_syntax:variable("_Variables"), erl_syntax:variable("RenderOptions")], + none, options_match_ast(Context, TW) ++ [Ast]), + custom_tags_clauses_ast1( + CustomTags, [Tag | ExcludeTags], + [Clause|ClauseAcc], merge_info(Info, InfoAcc), + Context, TW) + end end end. @@ -1377,11 +1397,6 @@ full_path(File, DocRoot) -> %% Custom tags %%------------------------------------------------------------------- -key_to_string(Key) when is_atom(Key) -> - erl_syntax:string(atom_to_list(Key)); -key_to_string(Key) when is_list(Key) -> - erl_syntax:string(Key). - tag_ast(Name, Args, Context, TreeWalker) -> {{InterpretedArgs, AstInfo1}, TreeWalker1} = lists:foldr(fun ({{identifier, _, Key}, {trans, StringLiteral}}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> @@ -1391,13 +1406,13 @@ tag_ast(Name, Args, Context, TreeWalker) -> {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc), {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0} end, {{[], #ast_info{}}, TreeWalker}, Args), - - {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, InterpretedArgs, Context), + TagArgs = [erl_syntax:tuple([erl_syntax:atom('__render_variables'), erl_syntax:variable("_Variables")])|InterpretedArgs], + {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, TagArgs, Context), {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker1}. custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = false }) -> {erl_syntax:application(none, erl_syntax:atom(render_tag), - [key_to_string(Name), erl_syntax:list(InterpretedArgs), + [erl_syntax:atom(Name), erl_syntax:list(InterpretedArgs), erl_syntax:variable("RenderOptions")]), #ast_info{custom_tags = [Name]}}; custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = true, module = Module }) -> From 908d2e818fbfdaca24e16583935d9633f4385f80 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 27 Jun 2013 20:16:00 +0100 Subject: [PATCH 028/361] Keep original scanned tokens. In case of parse failures, the consumed tokens may have been transformed when put onto the value stack, so we can't simply move them back. So we look up the correct point in the original list of tokens and replace our remaining tokens with those from the original list. --- include/erlydtl_ext.hrl | 3 ++- src/erlydtl_compiler.erl | 25 +++++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/include/erlydtl_ext.hrl b/include/erlydtl_ext.hrl index ba92f93..5d8a962 100755 --- a/include/erlydtl_ext.hrl +++ b/include/erlydtl_ext.hrl @@ -19,7 +19,8 @@ locale = none, verbose = false, is_compiling_dir = false, - extension_module = undefined + extension_module = undefined, + scanned_tokens = [] }). -record(ast_info, { diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 7f0f991..531bf30 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -368,13 +368,12 @@ check_scan({ok, Tokens}, Context) -> undefined -> Tokens; {ok, T} -> T end, - check_parse(erlydtl_parser:parse(Tokens1), [], Context); + check_parse(erlydtl_parser:parse(Tokens1), [], Context#dtl_context{ scanned_tokens=Tokens1 }); check_scan({error, Err, State}, Context) -> case call_extension(Context, scan, [State]) of undefined -> {error, Err}; {ok, NewState} -> - %% io:format("call_extension from:~p~nto: ~p~n", [State, NewState]), check_scan(erlydtl_scanner:resume(NewState), Context); ExtRes -> ExtRes @@ -386,15 +385,14 @@ check_parse({ok, Parsed}, Acc, _Context) -> {ok, Acc ++ Parsed}; check_parse({ok, Parsed, C}, Acc, _Context) -> {ok, Acc ++ Parsed, C}; check_parse({error, _}=Err, _, _Context) -> Err; check_parse({error, Err, State}, Acc, Context) -> - %% io:format("parse error: ~p~nstate: ~p~n",[Err, State]), - {State1, Parsed} = reset_parse_state(State), + {State1, Parsed} = reset_parse_state(State, Context), case call_extension(Context, parse, [State1]) of undefined -> {error, Err}; {ok, ExtParsed} -> {ok, Acc ++ Parsed ++ ExtParsed}; {error, ExtErr, ExtState} -> - case reset_parse_state(ExtState) of + case reset_parse_state(ExtState, Context) of {_, []} -> %% todo: see if this is indeed a sensible ext error, %% or if we should rather present the original Err message @@ -407,13 +405,20 @@ check_parse({error, Err, State}, Acc, Context) -> end. %% backtrack up to the Rootsymbol, and keep the current top-level value stack -reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]]) -> - {[Ts, Tzr, 0, [], []], Parsed}; -reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]]) -> - reset_parse_state([[T | Ts], Tzr, S, Ss, Stack]); -reset_parse_state([_, _, 0, [], []]=State) -> +reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]], Context) -> + {[reset_token_stream(Ts, Context#dtl_context.scanned_tokens), + Tzr, 0, [], []], Parsed}; +reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]], Context) -> + reset_parse_state([[T|Ts], Tzr, S, Ss, Stack], Context); +reset_parse_state([_, _, 0, [], []]=State, _Context) -> {State, []}. +reset_token_stream([T|_], [T|Ts]) -> [T|Ts]; +reset_token_stream(Ts, [_|S]) -> + reset_token_stream(Ts, S). +%% we should find the next token in the list of scanned tokens, or something is real fishy + + custom_tags_ast(CustomTags, Context, TreeWalker) -> {{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} = custom_tags_clauses_ast(CustomTags, Context, TreeWalker), %% This doesn't work since erl_syntax:revert/1 chokes on the airity_qualifier in the -compile attribute.. From 9b0c4177326356e244753dea3e79cb1648c58f9f Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 28 Jun 2013 13:02:31 +0100 Subject: [PATCH 029/361] Don't backtrack further up the stack than really needed. --- src/erlydtl_compiler.erl | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 531bf30..7b444f7 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -404,14 +404,23 @@ check_parse({error, Err, State}, Acc, Context) -> ExtRes end. -%% backtrack up to the Rootsymbol, and keep the current top-level value stack -reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]], Context) -> - {[reset_token_stream(Ts, Context#dtl_context.scanned_tokens), +%% backtrack up to the nearest opening tag, and keep the value stack parsed ok so far +reset_parse_state([[{Tag, _, _}|_]=Ts, Tzr, _, _, Stack], Context) + when Tag==open_tag; Tag==open_var -> + %% reached opening tag, so the stack should be sensible here + {[reset_token_stream(Ts, Context#dtl_context.scanned_tokens), + Tzr, 0, [], []], lists:flatten(Stack)}; +reset_parse_state([_, _, 0, [], []]=State, _Context) -> + %% top of (empty) stack + {State, []}; +reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]], Context) + when is_list(Parsed) -> + %% top of good stack + {[reset_token_stream(Ts, Context#dtl_context.scanned_tokens), Tzr, 0, [], []], Parsed}; -reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]], Context) -> - reset_parse_state([[T|Ts], Tzr, S, Ss, Stack], Context); -reset_parse_state([_, _, 0, [], []]=State, _Context) -> - {State, []}. +reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]], Context) -> + %% backtrack... + reset_parse_state([[T|Ts], Tzr, S, Ss, Stack], Context). reset_token_stream([T|_], [T|Ts]) -> [T|Ts]; reset_token_stream(Ts, [_|S]) -> From 7fce7b8c89ea7c3ec6ddb00164ebdf4f1a8a6f84 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 28 Jun 2013 16:56:16 +0100 Subject: [PATCH 030/361] Allow extension in custom tag args. --- src/erlydtl_compiler.erl | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 7b444f7..87c5450 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -1412,14 +1412,21 @@ full_path(File, DocRoot) -> %%------------------------------------------------------------------- tag_ast(Name, Args, Context, TreeWalker) -> - {{InterpretedArgs, AstInfo1}, TreeWalker1} = lists:foldr(fun - ({{identifier, _, Key}, {trans, StringLiteral}}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> - {{TransAst, TransAstInfo}, TreeWalker0} = translated_ast(StringLiteral, Context, TreeWalkerAcc), - {{[erl_syntax:tuple([erl_syntax:atom(Key), TransAst])|ArgsAcc], merge_info(TransAstInfo, AstInfoAcc)}, TreeWalker0}; - ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> - {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc), - {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0} - end, {{[], #ast_info{}}, TreeWalker}, Args), + {{InterpretedArgs, AstInfo1}, TreeWalker1} = + lists:foldr( + fun ({{identifier, _, Key}, {trans, StringLiteral}}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> + {{TransAst, TransAstInfo}, TreeWalker0} = translated_ast(StringLiteral, Context, TreeWalkerAcc), + {{[erl_syntax:tuple([erl_syntax:atom(Key), TransAst])|ArgsAcc], merge_info(TransAstInfo, AstInfoAcc)}, TreeWalker0}; + ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> + {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc), + {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0}; + ({extension, Tag}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}=Acc) -> + case call_extension(Context, compile_ast, [Tag, Context, TreeWalkerAcc]) of + undefined -> Acc; + {{ExtAst, ExtInfo}, ExtTreeWalker} -> + {{[ExtAst|ArgsAcc], merge_info(ExtInfo, AstInfoAcc)}, ExtTreeWalker} + end + end, {{[], #ast_info{}}, TreeWalker}, Args), TagArgs = [erl_syntax:tuple([erl_syntax:atom('__render_variables'), erl_syntax:variable("_Variables")])|InterpretedArgs], {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, TagArgs, Context), {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker1}. From 13875e1965e759be5adf9795120b0ba047c9458e Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 28 Jun 2013 20:41:43 +0100 Subject: [PATCH 031/361] Don't add __render_variables to custom tag vars. It makes ChicagoBoss choke. Thanks @entronic for reporting. The same effect can be had by passing a copy of the Variables with the RenderOptions. Thanks @evanmiller for tip. See pull request #83 for discussion. The same effect can be achieved --- src/erlydtl_compiler.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 87c5450..c2045bd 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -1427,8 +1427,7 @@ tag_ast(Name, Args, Context, TreeWalker) -> {{[ExtAst|ArgsAcc], merge_info(ExtInfo, AstInfoAcc)}, ExtTreeWalker} end end, {{[], #ast_info{}}, TreeWalker}, Args), - TagArgs = [erl_syntax:tuple([erl_syntax:atom('__render_variables'), erl_syntax:variable("_Variables")])|InterpretedArgs], - {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, TagArgs, Context), + {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, InterpretedArgs, Context), {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker1}. custom_tags_modules_ast(Name, InterpretedArgs, #dtl_context{ custom_tags_modules = [], is_compiling_dir = false }) -> From aa679b200f71f17ce0fff89ce4e15ba7bc1a80a0 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Tue, 3 Sep 2013 15:08:27 +0200 Subject: [PATCH 032/361] Add extension point to resolve_variable_ast. For this, we need to pass the TreeWalker to resolve_variable_ast, which affects the filter and cycle functions as well.. Allow the extension module to provide it's own find/fetch value runtime function. --- src/erlydtl_compiler.erl | 142 ++++++++++++++++++++++----------------- 1 file changed, 82 insertions(+), 60 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index c2045bd..111ef7e 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -840,11 +840,9 @@ value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) -> {'apply_filter', Variable, Filter} -> filter_ast(Variable, Filter, Context, TreeWalker); {'attribute', _} = Variable -> - {Ast, VarName} = resolve_variable_ast(Variable, Context, EmptyIfUndefined), - {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker}; + resolve_variable_ast(Variable, Context, TreeWalker, EmptyIfUndefined); {'variable', _} = Variable -> - {Ast, VarName} = resolve_variable_ast(Variable, Context, EmptyIfUndefined), - {{Ast, #ast_info{var_names = [VarName]}}, TreeWalker}; + resolve_variable_ast(Variable, Context, TreeWalker, EmptyIfUndefined); {extension, Tag} -> extension_ast(Tag, Context, TreeWalker) end. @@ -1026,8 +1024,8 @@ filter_tag_ast(FilterList, Contents, Context, TreeWalker) -> ([{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} + {{Ast, AstInfo}, TW} = filter_ast1(Filter, AstAcc, Context, TreeWalkerAcc), + {{Ast, merge_info(InfoAcc, AstInfo)}, TW} end, {{erl_syntax:application( erl_syntax:atom(erlang), erl_syntax:atom(iolist_to_binary), @@ -1090,36 +1088,44 @@ filter_ast_noescape(Variable, [{identifier, _, 'safeseq'}], Context, TreeWalker) value_ast(Variable, true, false, Context, TreeWalker#treewalker{safe = true}); filter_ast_noescape(Variable, Filter, Context, TreeWalker) -> {{VariableAst, Info1}, TreeWalker2} = value_ast(Variable, true, false, Context, TreeWalker), - {VarValue, Info2} = filter_ast1(Filter, VariableAst, Context), - {{VarValue, merge_info(Info1, Info2)}, TreeWalker2}. - -filter_ast1([{identifier, _, Name}, {string_literal, _, ArgName}], VariableAst, #dtl_context{ binary_strings = true } = Context) -> - filter_ast2(Name, VariableAst, [binary_string(unescape_string_literal(ArgName))], [], Context); -filter_ast1([{identifier, _, Name}, {string_literal, _, ArgName}], VariableAst, #dtl_context{ binary_strings = false } = Context) -> - filter_ast2(Name, VariableAst, [erl_syntax:string(unescape_string_literal(ArgName))], [], Context); -filter_ast1([{identifier, _, Name}, {number_literal, _, ArgName}], VariableAst, Context) -> - filter_ast2(Name, VariableAst, [erl_syntax:integer(list_to_integer(ArgName))], [], Context); -filter_ast1([{identifier, _, Name}, ArgVariable], VariableAst, Context) -> - {ArgAst, ArgVarName} = resolve_variable_ast(ArgVariable, Context, false), - filter_ast2(Name, VariableAst, [ArgAst], [ArgVarName], Context); -filter_ast1([{identifier, _, Name}], VariableAst, Context) -> - filter_ast2(Name, VariableAst, [], [], Context). - -filter_ast2(Name, VariableAst, [], VarNames, #dtl_context{ filter_modules = [Module|Rest] } = Context) -> + {{VarValue, Info2}, TreeWalker3} = filter_ast1(Filter, VariableAst, Context, TreeWalker2), + {{VarValue, merge_info(Info1, Info2)}, TreeWalker3}. + +filter_ast1([{identifier, _, Name}, {string_literal, _, ArgName}], VariableAst, + #dtl_context{ binary_strings = true } = Context, TreeWalker) -> + filter_ast2(Name, VariableAst, [binary_string(unescape_string_literal(ArgName))], #ast_info{}, Context, TreeWalker); +filter_ast1([{identifier, _, Name}, {string_literal, _, ArgName}], VariableAst, + #dtl_context{ binary_strings = false } = Context, TreeWalker) -> + filter_ast2(Name, VariableAst, [erl_syntax:string(unescape_string_literal(ArgName))], #ast_info{}, Context, TreeWalker); +filter_ast1([{identifier, _, Name}, {number_literal, _, ArgName}], VariableAst, Context, TreeWalker) -> + filter_ast2(Name, VariableAst, [erl_syntax:integer(list_to_integer(ArgName))], #ast_info{}, Context, TreeWalker); +filter_ast1([{identifier, _, Name}, ArgVariable], VariableAst, Context, TreeWalker) -> + {{ArgAst, ArgInfo}, TreeWalker2} = resolve_variable_ast(ArgVariable, Context, TreeWalker, false), + filter_ast2(Name, VariableAst, [ArgAst], ArgInfo, Context, TreeWalker2); +filter_ast1([{identifier, _, Name}], VariableAst, Context, TreeWalker) -> + filter_ast2(Name, VariableAst, [], #ast_info{}, Context, TreeWalker). + +filter_ast2(Name, VariableAst, [], VarInfo, #dtl_context{ filter_modules = [Module|Rest] } = Context, TreeWalker) -> case lists:member({Name, 1}, Module:module_info(exports)) of true -> - {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name), - [VariableAst]), #ast_info{var_names = VarNames}}; + {{erl_syntax:application( + erl_syntax:atom(Module), erl_syntax:atom(Name), + [VariableAst]), + VarInfo}, + TreeWalker}; false -> - filter_ast2(Name, VariableAst, [], VarNames, Context#dtl_context{ filter_modules = Rest }) + filter_ast2(Name, VariableAst, [], VarInfo, Context#dtl_context{ filter_modules = Rest }, TreeWalker) end; -filter_ast2(Name, VariableAst, [Arg], VarNames, #dtl_context{ filter_modules = [Module|Rest] } = Context) -> +filter_ast2(Name, VariableAst, [Arg], VarInfo, #dtl_context{ filter_modules = [Module|Rest] } = Context, TreeWalker) -> case lists:member({Name, 2}, Module:module_info(exports)) of true -> - {erl_syntax:application(erl_syntax:atom(Module), erl_syntax:atom(Name), - [VariableAst, Arg]), #ast_info{var_names = VarNames}}; + {{erl_syntax:application( + erl_syntax:atom(Module), erl_syntax:atom(Name), + [VariableAst, Arg]), + VarInfo}, + TreeWalker}; false -> - filter_ast2(Name, VariableAst, [Arg], VarNames, Context#dtl_context{ filter_modules = Rest }) + filter_ast2(Name, VariableAst, [Arg], VarInfo, Context#dtl_context{ filter_modules = Rest }, TreeWalker) end. search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) -> @@ -1142,40 +1148,56 @@ search_for_safe_filter({apply_filter, Variable, Filter}, _) -> search_for_safe_filter(_Variable, _Filter) -> on. -resolve_variable_ast(VarTuple, Context, true) -> - resolve_variable_ast1(VarTuple, Context, 'fetch_value'); -resolve_variable_ast(VarTuple, Context, false) -> - resolve_variable_ast1(VarTuple, Context, 'find_value'). +finder_function(true) -> {erlydtl_runtime, fetch_value}; +finder_function(false) -> {erlydtl_runtime, find_value}. + +finder_function(EmptyIfUndefined, Context) -> + case call_extension(Context, finder_function, [EmptyIfUndefined]) of + undefined -> finder_function(EmptyIfUndefined); + Result -> Result + end. + +resolve_variable_ast({extension, Tag}, Context, TreeWalker, _) -> + extension_ast(Tag, Context, TreeWalker); +resolve_variable_ast(VarTuple, Context, TreeWalker, EmptyIfUndefined) + when is_boolean(EmptyIfUndefined) -> + resolve_variable_ast(VarTuple, Context, TreeWalker, finder_function(EmptyIfUndefined, Context)); +resolve_variable_ast(VarTuple, Context, TreeWalker, FinderFunction) -> + resolve_variable_ast1(VarTuple, Context, TreeWalker, FinderFunction). -resolve_variable_ast1({attribute, {{identifier, {Row, Col}, AttrName}, Variable}}, Context, FinderFunction) -> - {VarAst, VarName} = resolve_variable_ast1(Variable, Context, FinderFunction), +resolve_variable_ast1({attribute, {{identifier, {Row, Col}, AttrName}, Variable}}, Context, TreeWalker, FinderFunction) -> + {{VarAst, VarInfo}, TreeWalker1} = resolve_variable_ast(Variable, Context, TreeWalker, FinderFunction), FileNameAst = case Context#dtl_context.parse_trail of [] -> erl_syntax:atom(undefined); [H|_] -> erl_syntax:string(H) end, - {erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction), - [erl_syntax:atom(AttrName), VarAst, FileNameAst, - erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)]) - ]), VarName}; - -resolve_variable_ast1({variable, {identifier, {Row, Col}, VarName}}, Context, FinderFunction) -> + {Runtime, Finder} = FinderFunction, + {{erl_syntax:application( + erl_syntax:atom(Runtime), + erl_syntax:atom(Finder), + [erl_syntax:atom(AttrName), VarAst, FileNameAst, + erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)]) + ]), + VarInfo}, + TreeWalker1}; + +resolve_variable_ast1({variable, {identifier, {Row, Col}, VarName}}, Context, TreeWalker, FinderFunction) -> VarValue = case resolve_scoped_variable_ast(VarName, Context) of - undefined -> - FileNameAst = case Context#dtl_context.parse_trail of - [] -> erl_syntax:atom(undefined); - [H|_] -> erl_syntax:string(H) - end, - erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(FinderFunction), - [erl_syntax:atom(VarName), erl_syntax:variable("_Variables"), FileNameAst, - erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)]) - ]); - Val -> - Val - end, - {VarValue, VarName}; - -resolve_variable_ast1(What, _Context, _FinderFunction) -> - error_logger:error_msg("~p:resolve_variable_ast unhandled: ~p~n", [?MODULE, What]). + undefined -> + FileNameAst = case Context#dtl_context.parse_trail of + [] -> erl_syntax:atom(undefined); + [H|_] -> erl_syntax:string(H) + end, + {Runtime, Finder} = FinderFunction, + erl_syntax:application( + erl_syntax:atom(Runtime), erl_syntax:atom(Finder), + [erl_syntax:atom(VarName), erl_syntax:variable("_Variables"), FileNameAst, + erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)]) + ]); + Val -> + Val + end, + {{VarValue, #ast_info{ var_names=[VarName] }}, TreeWalker}. resolve_scoped_variable_ast(VarName, Context) -> lists:foldl(fun(Scope, Value) -> @@ -1341,7 +1363,7 @@ cycle_ast(Names, Context, TreeWalker) -> {{S, _}, _} = string_ast(unescape_string_literal(Str), Context, TreeWalker), {S, VarNamesAcc}; ({variable, _}=Var, VarNamesAcc) -> - {V, VarName} = resolve_variable_ast(Var, Context, true), + {{V, #ast_info{ var_names=[VarName] }}, _} = resolve_variable_ast(Var, Context, TreeWalker, true), {V, [VarName|VarNamesAcc]}; ({number_literal, _, Num}, VarNamesAcc) -> {format(erl_syntax:integer(Num), Context, TreeWalker), VarNamesAcc}; @@ -1464,8 +1486,8 @@ call_ast(Module, TreeWalkerAcc) -> call_ast(Module, erl_syntax:variable("_Variables"), #ast_info{}, TreeWalkerAcc). call_with_ast(Module, Variable, Context, TreeWalker) -> - {VarAst, VarName} = resolve_variable_ast(Variable, Context, false), - call_ast(Module, VarAst, #ast_info{var_names=[VarName]}, TreeWalker). + {{VarAst, VarInfo}, TreeWalker2} = resolve_variable_ast(Variable, Context, TreeWalker, false), + call_ast(Module, VarAst, VarInfo, TreeWalker2). call_ast(Module, Variable, AstInfo, TreeWalker) -> AppAst = erl_syntax:application( From a65d1803277d51122108474b46725767224403ab Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Wed, 11 Sep 2013 14:17:28 +0200 Subject: [PATCH 033/361] Bail on unknown extension to custom tag. --- src/erlydtl_compiler.erl | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 111ef7e..1769cc4 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -1442,12 +1442,9 @@ tag_ast(Name, Args, Context, TreeWalker) -> ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc), {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0}; - ({extension, Tag}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}=Acc) -> - case call_extension(Context, compile_ast, [Tag, Context, TreeWalkerAcc]) of - undefined -> Acc; - {{ExtAst, ExtInfo}, ExtTreeWalker} -> - {{[ExtAst|ArgsAcc], merge_info(ExtInfo, AstInfoAcc)}, ExtTreeWalker} - end + ({extension, Tag}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> + {{ExtAst, ExtInfo}, ExtTreeWalker} = extension_ast(Tag, Context, TreeWalkerAcc), + {{[ExtAst|ArgsAcc], merge_info(ExtInfo, AstInfoAcc)}, ExtTreeWalker} end, {{[], #ast_info{}}, TreeWalker}, Args), {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, InterpretedArgs, Context), {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker1}. From d43b9546826e2d206803903c021ff3e32d75b5d1 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Wed, 11 Sep 2013 16:43:28 +0200 Subject: [PATCH 034/361] Expose new interpret_args/3 to extensions. This is factored out of the tag_ast/4 function for parsing arguments. --- src/erlydtl_compiler.erl | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 1769cc4..3db0237 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -47,7 +47,8 @@ merge_info/2, format/3, value_ast/5, - resolve_scoped_variable_ast/2 + resolve_scoped_variable_ast/2, + interpret_args/3 ]). -include("erlydtl_ext.hrl"). @@ -1433,19 +1434,21 @@ full_path(File, DocRoot) -> %% Custom tags %%------------------------------------------------------------------- +interpret_args(Args, Context, TreeWalker) -> + lists:foldr( + fun ({{identifier, _, Key}, {trans, StringLiteral}}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> + {{TransAst, TransAstInfo}, TreeWalker0} = translated_ast(StringLiteral, Context, TreeWalkerAcc), + {{[erl_syntax:tuple([erl_syntax:atom(Key), TransAst])|ArgsAcc], merge_info(TransAstInfo, AstInfoAcc)}, TreeWalker0}; + ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> + {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc), + {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0}; + ({extension, Tag}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> + {{ExtAst, ExtInfo}, ExtTreeWalker} = extension_ast(Tag, Context, TreeWalkerAcc), + {{[ExtAst|ArgsAcc], merge_info(ExtInfo, AstInfoAcc)}, ExtTreeWalker} + end, {{[], #ast_info{}}, TreeWalker}, Args). + tag_ast(Name, Args, Context, TreeWalker) -> - {{InterpretedArgs, AstInfo1}, TreeWalker1} = - lists:foldr( - fun ({{identifier, _, Key}, {trans, StringLiteral}}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> - {{TransAst, TransAstInfo}, TreeWalker0} = translated_ast(StringLiteral, Context, TreeWalkerAcc), - {{[erl_syntax:tuple([erl_syntax:atom(Key), TransAst])|ArgsAcc], merge_info(TransAstInfo, AstInfoAcc)}, TreeWalker0}; - ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> - {{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, Context, TreeWalkerAcc), - {{[erl_syntax:tuple([erl_syntax:atom(Key), Ast0])|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0}; - ({extension, Tag}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) -> - {{ExtAst, ExtInfo}, ExtTreeWalker} = extension_ast(Tag, Context, TreeWalkerAcc), - {{[ExtAst|ArgsAcc], merge_info(ExtInfo, AstInfoAcc)}, ExtTreeWalker} - end, {{[], #ast_info{}}, TreeWalker}, Args), + {{InterpretedArgs, AstInfo1}, TreeWalker1} = interpret_args(Args, Context, TreeWalker), {RenderAst, RenderInfo} = custom_tags_modules_ast(Name, InterpretedArgs, Context), {{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker1}. From 69c224c3a8b9df04bc2531f96cc4c8c2a30dc8f4 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 13 Sep 2013 10:53:25 +0200 Subject: [PATCH 035/361] Check for regressions of the functional tests. --- tests/expect/autoescape | 8 ++ tests/expect/comment | 15 +++ tests/expect/custom_call | 23 ++++ tests/expect/custom_tag | 33 ++++++ tests/expect/cycle | 47 ++++++++ tests/expect/extends | 11 ++ tests/expect/extends_path | 14 +++ tests/expect/extends_path2 | 10 ++ tests/expect/filters | 45 ++++++++ tests/expect/for | 13 +++ tests/expect/for_list | 7 ++ tests/expect/for_list_preset | 7 ++ tests/expect/for_preset | 13 +++ tests/expect/for_records | 13 +++ tests/expect/for_records_preset | 23 ++++ tests/expect/for_tuple | 7 ++ tests/expect/if | 4 + tests/expect/if_preset | 4 + tests/expect/ifequal | 28 +++++ tests/expect/ifequal_preset | 28 +++++ tests/expect/ifnotequal | 28 +++++ tests/expect/ifnotequal_preset | 28 +++++ tests/expect/include | 2 + tests/expect/include_path | 12 ++ tests/expect/include_template | 14 +++ tests/expect/now | 11 ++ tests/expect/ssi | 2 + tests/expect/trans | 1 + tests/expect/var | 5 + tests/expect/var_preset | 9 ++ tests/src/erlydtl_functional_tests.erl | 152 +++++++++++++------------ 31 files changed, 547 insertions(+), 70 deletions(-) create mode 100644 tests/expect/autoescape create mode 100644 tests/expect/comment create mode 100644 tests/expect/custom_call create mode 100644 tests/expect/custom_tag create mode 100644 tests/expect/cycle create mode 100644 tests/expect/extends create mode 100644 tests/expect/extends_path create mode 100644 tests/expect/extends_path2 create mode 100644 tests/expect/filters create mode 100644 tests/expect/for create mode 100644 tests/expect/for_list create mode 100644 tests/expect/for_list_preset create mode 100644 tests/expect/for_preset create mode 100644 tests/expect/for_records create mode 100644 tests/expect/for_records_preset create mode 100644 tests/expect/for_tuple create mode 100644 tests/expect/if create mode 100644 tests/expect/if_preset create mode 100644 tests/expect/ifequal create mode 100644 tests/expect/ifequal_preset create mode 100644 tests/expect/ifnotequal create mode 100644 tests/expect/ifnotequal_preset create mode 100644 tests/expect/include create mode 100644 tests/expect/include_path create mode 100644 tests/expect/include_template create mode 100644 tests/expect/now create mode 100644 tests/expect/ssi create mode 100644 tests/expect/trans create mode 100644 tests/expect/var create mode 100644 tests/expect/var_preset diff --git a/tests/expect/autoescape b/tests/expect/autoescape new file mode 100644 index 0000000..4adaa07 --- /dev/null +++ b/tests/expect/autoescape @@ -0,0 +1,8 @@ + + This is escaped: <b>bold</b> + + This is not escaped: bold + + This is escaped: <b>bold</b> + + diff --git a/tests/expect/comment b/tests/expect/comment new file mode 100644 index 0000000..3114d07 --- /dev/null +++ b/tests/expect/comment @@ -0,0 +1,15 @@ + + + + + Test Comment + + + + bla + + blue + + black + + diff --git a/tests/expect/custom_call b/tests/expect/custom_call new file mode 100644 index 0000000..6aa07fc --- /dev/null +++ b/tests/expect/custom_call @@ -0,0 +1,23 @@ +>>>> before custom call tag 'comment' +before + +
      + +
    • preset-apple
    • + +
    • preset-banana
    • + +
    • preset-coconut
    • + +
    + +after +>>>> after custom call tag 'comment' + +>>>> before custom call tag 'if' +One but not two: two +Two but not one: one +One: +None: + +>>>> after custom call tag 'if' diff --git a/tests/expect/custom_tag b/tests/expect/custom_tag new file mode 100644 index 0000000..0dedba0 --- /dev/null +++ b/tests/expect/custom_tag @@ -0,0 +1,33 @@ + + + + + Test variable + + + before + + + + + + + + + + + +

    To view the Video:

    +

    + + Get Adobe Flash player + +

    + +
    + +
    + + after + + diff --git a/tests/expect/cycle b/tests/expect/cycle new file mode 100644 index 0000000..230038f --- /dev/null +++ b/tests/expect/cycle @@ -0,0 +1,47 @@ +before + +
      + +
    • 1. 1 - Apple
    • + +
    • 2. 2 - Banana
    • + +
    • 3. 3 - Cherry
    • + +
    • 4. 4 - Apple
    • + +
    • 5. 5 - Banana
    • + +
    • 6. 6 - Cherry
    • + +
    • 7. 7 - Apple
    • + +
    • 8. 8 - Banana
    • + +
    • 9. 9 - Cherry
    • + +
    • 10. 10 - Apple
    • + +
    • 11. 11 - Banana
    • + +
    • 12. 12 - Cherry
    • + +
    • 13. 13 - Apple
    • + +
    • 14. 14 - Banana
    • + +
    • 15. 15 - Cherry
    • + +
    • 16. 16 - Apple
    • + +
    • 17. 17 - Banana
    • + +
    • 18. 18 - Cherry
    • + +
    • 19. 19 - Apple
    • + +
    • 20. 20 - Banana
    • + +
    + +after diff --git a/tests/expect/extends b/tests/expect/extends new file mode 100644 index 0000000..72252dd --- /dev/null +++ b/tests/expect/extends @@ -0,0 +1,11 @@ +base-barstring + +base template + +replacing the base title + +more of base template + +replacing the base content - variable: test-barstring after variable + +end of base template diff --git a/tests/expect/extends_path b/tests/expect/extends_path new file mode 100644 index 0000000..f723b0b --- /dev/null +++ b/tests/expect/extends_path @@ -0,0 +1,14 @@ +base-barstring + +base2 template + +replacing the base title +block title 2 from base1 + +more of base2 template + +replacing the base content - variable: test-barstring after variable + +block content2 in base 2, should pass through + +end of base2 template diff --git a/tests/expect/extends_path2 b/tests/expect/extends_path2 new file mode 100644 index 0000000..ee0a4f7 --- /dev/null +++ b/tests/expect/extends_path2 @@ -0,0 +1,10 @@ +pre content + + +start_content +This is include1 + +end_content + + +post diff --git a/tests/expect/filters b/tests/expect/filters new file mode 100644 index 0000000..54ce998 --- /dev/null +++ b/tests/expect/filters @@ -0,0 +1,45 @@ +Add: 2 + 2 = 4 + +Capfirst: Capitalized + +Centered: +
    +       center       
    +
    + +Date format: Thu, 24 Jul 1975 00:00:00 +0100 +DateTime format: Thu, 24 Jul 1975 07:13:01 +0100 + +Escape JS: \u0022 \u0027 + +First letter: f + +Fix ampersands: & + +Force_escape: <b></b> + +Joined: eins, zwei, drei + +Last: t + +Length: 3 + +Length is 2?: false + +Left adjust: +
    +left                
    +
    + +Line breaks: Line 1
    Line 2
    Line 3 + +Lowercase: lowercase + +Right adjust: +
    +               right
    +
    + +Uppercase: UPPERCASE + +URL Encode: Let%27s%20go%21 diff --git a/tests/expect/for b/tests/expect/for new file mode 100644 index 0000000..a388d66 --- /dev/null +++ b/tests/expect/for @@ -0,0 +1,13 @@ +before + +
      + +
    • 1. apple
    • + +
    • 2. banana
    • + +
    • 3. coconut
    • + +
    + +after diff --git a/tests/expect/for_list b/tests/expect/for_list new file mode 100644 index 0000000..8eaf1fa --- /dev/null +++ b/tests/expect/for_list @@ -0,0 +1,7 @@ + +More than one apple is called "apples". Only $1 each! + +More than one banana is called "bananas". Only $2 each! + +More than one coconut is called "coconuts". Only $500 each! + diff --git a/tests/expect/for_list_preset b/tests/expect/for_list_preset new file mode 100644 index 0000000..60f88f1 --- /dev/null +++ b/tests/expect/for_list_preset @@ -0,0 +1,7 @@ + +More than one apple is called "apples". + +More than one banana is called "bananas". + +More than one coconut is called "coconuts". + diff --git a/tests/expect/for_preset b/tests/expect/for_preset new file mode 100644 index 0000000..b66639e --- /dev/null +++ b/tests/expect/for_preset @@ -0,0 +1,13 @@ +before + +
      + +
    • preset-apple
    • + +
    • preset-banana
    • + +
    • preset-coconut
    • + +
    + +after \ No newline at end of file diff --git a/tests/expect/for_records b/tests/expect/for_records new file mode 100644 index 0000000..a850352 --- /dev/null +++ b/tests/expect/for_records @@ -0,0 +1,13 @@ +before + + + +after \ No newline at end of file diff --git a/tests/expect/for_records_preset b/tests/expect/for_records_preset new file mode 100644 index 0000000..6a3cf54 --- /dev/null +++ b/tests/expect/for_records_preset @@ -0,0 +1,23 @@ +before + + + + + +after \ No newline at end of file diff --git a/tests/expect/for_tuple b/tests/expect/for_tuple new file mode 100644 index 0000000..bd43e44 --- /dev/null +++ b/tests/expect/for_tuple @@ -0,0 +1,7 @@ + +One apple, two apples! + +One banana, two bananas! + +One coconut, two coconuts! + diff --git a/tests/expect/if b/tests/expect/if new file mode 100644 index 0000000..818022d --- /dev/null +++ b/tests/expect/if @@ -0,0 +1,4 @@ +One but not two: one +Two but not one: two +One: one +None: diff --git a/tests/expect/if_preset b/tests/expect/if_preset new file mode 100644 index 0000000..818022d --- /dev/null +++ b/tests/expect/if_preset @@ -0,0 +1,4 @@ +One but not two: one +Two but not one: two +One: one +None: diff --git a/tests/expect/ifequal b/tests/expect/ifequal new file mode 100644 index 0000000..75191c8 --- /dev/null +++ b/tests/expect/ifequal @@ -0,0 +1,28 @@ + +if: var1="foo" and var2="foo" are equal + + + +if: var1="foo" and var2="foo" are equal + + + + + +else: var1="foo" and var3="bar" are not equal + + + +if: "foo" and "foo" are equal + + + +else: "foo" and "bar" are not equal + + + +if: 99 and 99 are equal + + + +else: 77 and 99 are not equal diff --git a/tests/expect/ifequal_preset b/tests/expect/ifequal_preset new file mode 100644 index 0000000..75191c8 --- /dev/null +++ b/tests/expect/ifequal_preset @@ -0,0 +1,28 @@ + +if: var1="foo" and var2="foo" are equal + + + +if: var1="foo" and var2="foo" are equal + + + + + +else: var1="foo" and var3="bar" are not equal + + + +if: "foo" and "foo" are equal + + + +else: "foo" and "bar" are not equal + + + +if: 99 and 99 are equal + + + +else: 77 and 99 are not equal diff --git a/tests/expect/ifnotequal b/tests/expect/ifnotequal new file mode 100644 index 0000000..4ab3c15 --- /dev/null +++ b/tests/expect/ifnotequal @@ -0,0 +1,28 @@ + + + +else: var1="foo" and var2="foo" are not equal + + + +if: var1="foo" and var3="bar" are equal + + + +if: var1="foo" and var3="bar" are equal + + + +else: "foo" and "foo" are not equal + + + +if: "foo" and "bar" are equal + + + +else: 99 and 99 are not equal + + + +if: 77 and 99 are equal diff --git a/tests/expect/ifnotequal_preset b/tests/expect/ifnotequal_preset new file mode 100644 index 0000000..4ab3c15 --- /dev/null +++ b/tests/expect/ifnotequal_preset @@ -0,0 +1,28 @@ + + + +else: var1="foo" and var2="foo" are not equal + + + +if: var1="foo" and var3="bar" are equal + + + +if: var1="foo" and var3="bar" are equal + + + +else: "foo" and "foo" are not equal + + + +if: "foo" and "bar" are equal + + + +else: 99 and 99 are not equal + + + +if: 77 and 99 are equal diff --git a/tests/expect/include b/tests/expect/include new file mode 100644 index 0000000..7dcc35f --- /dev/null +++ b/tests/expect/include @@ -0,0 +1,2 @@ +Including another file: This is included! foostring1 + diff --git a/tests/expect/include_path b/tests/expect/include_path new file mode 100644 index 0000000..925c564 --- /dev/null +++ b/tests/expect/include_path @@ -0,0 +1,12 @@ +main file + +This is template 1. + +test-barstring + + +This is template 2 + + + +base-barstring diff --git a/tests/expect/include_template b/tests/expect/include_template new file mode 100644 index 0000000..270b971 --- /dev/null +++ b/tests/expect/include_template @@ -0,0 +1,14 @@ +Including another template: base-barstring + +base template + +base title + +more of base template + +base content + +end of base template + + +test variable: test-barstring diff --git a/tests/expect/now b/tests/expect/now new file mode 100644 index 0000000..b893048 --- /dev/null +++ b/tests/expect/now @@ -0,0 +1,11 @@ +Expected format : Thu, 21 Dec 2000 16:01:07 +0200 +Got : Fri, 13 Sep 2013 10:19:05 +0200 + +Expected format : 27th February 2008 01:24 +Got : 13th September 2013 10:19 + +Expected format : It is the 4th of September 2007 +Got : It is the 13th of September 2013 + +Expected format : '' +Got : '' diff --git a/tests/expect/ssi b/tests/expect/ssi new file mode 100644 index 0000000..4d8acbb --- /dev/null +++ b/tests/expect/ssi @@ -0,0 +1,2 @@ +{{ "Don't evaluate me!" }} + diff --git a/tests/expect/trans b/tests/expect/trans new file mode 100644 index 0000000..5c36a07 --- /dev/null +++ b/tests/expect/trans @@ -0,0 +1 @@ +Example String diff --git a/tests/expect/var b/tests/expect/var new file mode 100644 index 0000000..b694a79 --- /dev/null +++ b/tests/expect/var @@ -0,0 +1,5 @@ +before varriable1 +foostring1 +after variable1 +foostring2 +after variable2 (HTML-comment-wrapped) \ No newline at end of file diff --git a/tests/expect/var_preset b/tests/expect/var_preset new file mode 100644 index 0000000..7d6e9e8 --- /dev/null +++ b/tests/expect/var_preset @@ -0,0 +1,9 @@ +one +foostring1 +two +preset-var1 +three +foostring2 +four +preset-var2 +five diff --git a/tests/src/erlydtl_functional_tests.erl b/tests/src/erlydtl_functional_tests.erl index 11f24b6..384086c 100644 --- a/tests/src/erlydtl_functional_tests.erl +++ b/tests/src/erlydtl_functional_tests.erl @@ -159,11 +159,11 @@ setup("trans") -> setup("locale") -> {ok, _RenderVars = [{locale, "ru"}]}; setup("custom_tag1") -> - {ok, [{a, <<"a1">>}], [{locale, ru}], [<<"b1">>, <<"\n">>]}; + {ok, [{a, <<"a1">>}], [{locale, ru}], <<"b1\n">>}; setup("custom_tag2") -> - {ok, [{a, <<"a1">>}], [{locale, ru}, {foo, bar}], [<<"b2">>, <<"\n">>]}; + {ok, [{a, <<"a1">>}], [{locale, ru}, {foo, bar}], <<"b2\n">>}; setup("custom_tag3") -> - {ok, [{a, <<"a1">>}], [{locale, ru}], [<<"b3">>, <<"\n">>]}; + {ok, [{a, <<"a1">>}], [{locale, ru}], <<"b3\n">>}; setup("ssi") -> RenderVars = [{path, filename:absname(filename:join(["tests", "input", "ssi_include.html"]))}], {ok, RenderVars}; @@ -182,19 +182,26 @@ setup(_) -> run_tests() -> io:format("Running functional tests...~n"), - case filelib:ensure_dir(filename:join([templates_outdir(), "foo"])) of - ok -> + case [filelib:ensure_dir( + filename:join([templates_dir(Dir), "foo"])) + || Dir <- ["output", "expect"]] -- [ok,ok] + of + [] -> case fold_tests() of {N, []}-> Msg = lists:concat(["All ", N, " functional tests passed~n~n"]), io:format(Msg), {ok, Msg}; - {_, Errs} -> - io:format("Errors: ~p~n~n",[Errs]), + {N, Errs} -> + io:format( + "~b / ~b functional tests failed.~nErrors: ~n", + [length(Errs), N]), + [io:format(" ~s [~s] ~s~n", [Name, Error, Reason]) + || {Name, Error, Reason} <- Errs], failed end; - {error, Reason} -> - io:format("Error: ~p~n~n", [Reason]), + Err -> + [io:format("Ensure dir failed: ~p~n~n", [Reason]) || {error, Reason} <- Err], failed end. @@ -209,97 +216,102 @@ run_test(Name) -> fold_tests() -> lists:foldl(fun(Name, {AccCount, AccErrs}) -> - case test_compile_render(Name) of - ok -> - {AccCount + 1, AccErrs}; - {error, Reason} -> - {AccCount + 1, [{Name, Reason} | AccErrs]} - end - end, {0, []}, test_list() - ). + Res = case catch test_compile_render(Name) of + ok -> {AccCount + 1, AccErrs}; + {'EXIT', Reason} -> + {AccCount + 1, [{Name, crash, + io_lib:format("~p", [Reason])} + | AccErrs]}; + {Error, Reason} -> + {AccCount + 1, [{Name, Error, Reason} + | AccErrs]} + end, + io:format("~n"), Res + end, {0, []}, test_list()). test_compile_render(Name) -> File = filename:join([templates_docroot(), Name]), Module = "example_" ++ Name, + io:format(" Template: ~p, ... ", [Name]), case setup_compile(Name) of {CompileStatus, CompileVars} -> Options = [ {vars, CompileVars}, {force_recompile, true}, {custom_tags_modules, [erlydtl_custom_tags]}], - io:format(" Template: ~p, ... compiling ... ", [Name]), + io:format("compiling ... "), case erlydtl:compile(File, Module, Options) of ok -> - case CompileStatus of - ok -> test_render(Name, list_to_atom(Module)); - _ -> {error, "compiling should have failed :" ++ File} + if CompileStatus =:= ok -> test_render(Name, list_to_atom(Module)); + true -> + io:format("missing error"), + {error, "compiling should have failed :" ++ File} end; {error, Err} -> - case CompileStatus of - error -> - io:format("~n"), - ok; - _ -> - io:format("~nCompile errror: ~p~n",[Err]), - Err + if CompileStatus =:= error -> io:format("ok"); + true -> + io:format("failed"), + {compile_error, io_lib:format("~p", [Err])} end end; - skip -> - ok; - _ -> - {error, "no 'setup' clause defined for this test"} + skip -> io:format("skipped") end. - test_render(Name, Module) -> File = filename:join([templates_docroot(), Name]), {RenderStatus, Vars, Opts, RenderResult} = case setup(Name) of - {RS, V} -> {RS, V, [], undefined}; - {RS, V, O} -> {RS, V, O, undefined}; + {RS, V} -> {RS, V, [], get_expected_result(Name)}; + {RS, V, O} -> {RS, V, O, get_expected_result(Name)}; {RS, V, O, R} -> {RS, V, O, R} end, + io:format("rendering ... "), case catch Module:render(Vars, Opts) of - {ok, Data} -> - io:format("rendering~n"), - case RenderStatus of - ok -> - case RenderResult of - undefined -> - {File, _} = Module:source(), - OutFile = filename:join([templates_outdir(), filename:basename(File)]), - case file:open(OutFile, [write]) of - {ok, IoDev} -> - file:write(IoDev, Data), - file:close(IoDev), - ok; - Err -> - Err + {ok, Output} -> + Data = iolist_to_binary(Output), + if RenderStatus =:= ok -> + if RenderResult =:= undefined -> + Devs = [begin + FileName = filename:join([templates_dir(Dir), Name]), + {ok, IoDev} = file:open(FileName, [write]), + IoDev + end || Dir <- ["output", "expect"]], + try + [file:write(IoDev, Data) || IoDev <- Devs], + io:format("~n #### NOTE: created new expected output file: \"tests/expect/~s\"." + "~n Please verify contents.", [Name]) + after + [file:close(IoDev) || IoDev <- Devs] end; - _ when Data =:= RenderResult -> - ok; - _ -> - {error, lists:flatten(io_lib:format("Test ~s failed\n" - "Expected: ~p\n" - "Value: ~p\n", [Name, RenderResult, Data]))} - end; - _ -> - {error, "rendering should have failed :" ++ File} + RenderResult =:= Data -> + io:format("ok"); + true -> + io:format("failed"), + {error, io_lib:format( + "Expected output does not match rendered output~n" + "==Expected==~n~s~n--Actual--~n~s~n==End==~n", + [RenderResult, Data])} + end; + true -> + io:format("missing error"), + {missing_error, "rendering should have failed :" ++ File} end; {'EXIT', Reason} -> - io:format("~n"), - {error, lists:flatten(io_lib:format("failed invoking render method of ~p ~p", [Module, Reason]))}; + io:format("failed"), + {render_error, io_lib:format("failed invoking render method of ~p ~p", [Module, Reason])}; Err -> - io:format("~n"), - case RenderStatus of - error -> ok; - _ -> Err + if RenderStatus =:= error -> io:format("ok"); + true -> io:format("failed"), + {render_error, io_lib:format("~p", [Err])} end end. +get_expected_result(Name) -> + FileName = filename:join([templates_dir("expect"), Name]), + case filelib:is_regular(FileName) of + true -> {ok, Data} = file:read_file(FileName), Data; + false -> undefined + end. -templates_docroot() -> - filename:join([erlydtl_deps:get_base_dir(), "tests", "input"]). - -templates_outdir() -> - filename:join([erlydtl_deps:get_base_dir(), "tests", "output"]). +templates_docroot() -> templates_dir("input"). +templates_dir(Name) -> filename:join([erlydtl_deps:get_base_dir(), "tests", Name]). From 00df393913164ea896bb7f548475b97e86b7b6cb Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 13 Sep 2013 17:06:16 +0200 Subject: [PATCH 036/361] Skip regression test on the functional test for "now". --- tests/expect/now | 11 ----------- tests/src/erlydtl_functional_tests.erl | 4 ++++ 2 files changed, 4 insertions(+), 11 deletions(-) delete mode 100644 tests/expect/now diff --git a/tests/expect/now b/tests/expect/now deleted file mode 100644 index b893048..0000000 --- a/tests/expect/now +++ /dev/null @@ -1,11 +0,0 @@ -Expected format : Thu, 21 Dec 2000 16:01:07 +0200 -Got : Fri, 13 Sep 2013 10:19:05 +0200 - -Expected format : 27th February 2008 01:24 -Got : 13th September 2013 10:19 - -Expected format : It is the 4th of September 2007 -Got : It is the 13th of September 2013 - -Expected format : '' -Got : '' diff --git a/tests/src/erlydtl_functional_tests.erl b/tests/src/erlydtl_functional_tests.erl index 384086c..fcd7a48 100644 --- a/tests/src/erlydtl_functional_tests.erl +++ b/tests/src/erlydtl_functional_tests.erl @@ -131,6 +131,8 @@ setup("ifequal_preset") -> setup("ifnotequal") -> RenderVars = [{var1, "foo"}, {var2, "foo"}, {var3, "bar"}], {ok, RenderVars}; +setup("now") -> + {ok, [], [], skip_check}; setup("var") -> RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}, {var_not_used, "foostring3"}], {ok, RenderVars}; @@ -285,6 +287,8 @@ test_render(Name, Module) -> end; RenderResult =:= Data -> io:format("ok"); + RenderResult =:= skip_check -> + io:format("ok (not checked for regression)"); true -> io:format("failed"), {error, io_lib:format( From d4e6d65581af59cebd3a9f8756dafb906681e34c Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Mon, 16 Sep 2013 14:42:03 +0200 Subject: [PATCH 037/361] Do not crash unit tests on compile error. --- tests/src/erlydtl_unittests.erl | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 7776572..638d46b 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -1121,14 +1121,23 @@ run_tests() -> io:format(" Test group ~p...~n", [Group]), lists:foldl(fun ({Name, DTL, Vars, Output}, Acc) -> - process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), - Vars, [], Output, Acc, Group, Name); + try + process_unit_test( + erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), + Vars, [], Output, Acc, Group, Name) + catch _:Error -> [Error|Acc] end; ({Name, DTL, Vars, RenderOpts, Output}, Acc) -> - process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), - Vars, RenderOpts, Output, Acc, Group, Name); + try + process_unit_test( + erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), + Vars, RenderOpts, Output, Acc, Group, Name) + catch _:Error -> [Error|Acc] end; ({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}, Acc) -> - process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts ++ DefaultOptions), - Vars, RenderOpts, Output, Acc, Group, Name) + try + process_unit_test( + erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts ++ DefaultOptions), + Vars, RenderOpts, Output, Acc, Group, Name) + catch _:Error -> [Error|Acc] end end, GroupAcc, Assertions) end, [], tests()), From e45c96dac4ab0f23e6f0106227c3c8963125fe93 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Mon, 16 Sep 2013 15:55:51 +0200 Subject: [PATCH 038/361] Improve error reporting for unit tests. --- tests/src/erlydtl_unittests.erl | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 638d46b..ab4d8f3 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -1125,24 +1125,28 @@ run_tests() -> process_unit_test( erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), Vars, [], Output, Acc, Group, Name) - catch _:Error -> [Error|Acc] end; + catch Class:Error -> format_error(Group, Name, Class, Error, Acc) end; ({Name, DTL, Vars, RenderOpts, Output}, Acc) -> try process_unit_test( erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), Vars, RenderOpts, Output, Acc, Group, Name) - catch _:Error -> [Error|Acc] end; + catch Class:Error -> format_error(Group, Name, Class, Error, Acc) end; ({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}, Acc) -> try process_unit_test( erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts ++ DefaultOptions), Vars, RenderOpts, Output, Acc, Group, Name) - catch _:Error -> [Error|Acc] end + catch Class:Error -> format_error(Group, Name, Class, Error, Acc) end end, GroupAcc, Assertions) end, [], tests()), - io:format("Unit test failures: ~p~n", [lists:reverse(Failures)]). + io:format("Unit test failures: ~b~n", [length(Failures)]), + [io:format(" ~s:~s ~s~n", [Group, Name, Error]) || {Group, Name, Error} <- lists:reverse(Failures)]. +format_error(Group, Name, Class, Error, Acc) -> + [{Group, Name, io_lib:format("~n ~s:~s~n ~p", [Class, Error, erlang:get_stacktrace()])}|Acc]. + process_unit_test(CompiledTemplate, Vars, RenderOpts, Output,Acc, Group, Name) -> case CompiledTemplate of {ok, _} -> @@ -1152,16 +1156,18 @@ process_unit_test(CompiledTemplate, Vars, RenderOpts, Output,Acc, Group, Name) - {Output, Output} -> Acc; {Output, Unexpected} -> - [{Group, Name, 'binary', Unexpected, Output} | Acc]; + [{Group, Name, io_lib:format("Unexpected result with binary variables: ~nExpected: ~p~nActual: ~p", + [Output, Unexpected])} | Acc]; {Unexpected, Output} -> - [{Group, Name, 'list', Unexpected, Output} | Acc]; + [{Group, Name, io_lib:format("Unexpected result with list variables: ~nExpected: ~p~nActual: ~p", + [Output, Unexpected])} | Acc]; {Unexpected1, Unexpected2} -> - [{Group, Name, 'list', Unexpected1, Output}, - {Group, Name, 'binary', Unexpected2, Output} | Acc] + [{Group, Name, io_lib:format("Unexpected result: ~nExpected: ~p~nActual (list): ~p~nActual (binary): ~p", + [Output, Unexpected1, Unexpected2])} | Acc] end; Output -> Acc; Err -> - [{Group, Name, Err} | Acc] + [{Group, Name, io_lib:format("Render error: ~p~n", [Err])} | Acc] end. From 0a755de86e5bba6d4b43f889bc038f9d491e38d5 Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Sun, 6 Oct 2013 09:35:48 -0500 Subject: [PATCH 039/361] Custom tag vars are atoms, not lists --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 8794de4..9275f6f 100644 --- a/README.markdown +++ b/README.markdown @@ -60,7 +60,7 @@ with one of the following signatures: some_tag(TagVars, Options) -> iolist() The `TagVars` are variables provided to a custom tag in the template's body -(e.g. `{% foo bar=100 %}` results in `TagVars = [{"bar", 100}]`). +(e.g. `{% foo bar=100 %}` results in `TagVars = [{bar, 100}]`). The `Options` are options passed as the second argument to the `render/2` call at render-time. (These may include any options, not just `locale` and `translation_fun`.) From 7148ba16142e702a659dd159afeb69e8b305f45b Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Sun, 6 Oct 2013 14:17:01 -0500 Subject: [PATCH 040/361] Support iterating over Ecto has_many associations --- src/erlydtl_compiler.erl | 5 +---- src/erlydtl_runtime.erl | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 3db0237..4d32616 100755 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -1300,10 +1300,7 @@ for_loop_ast(IteratorList, LoopValue, IsReversed, Contents, {EmptyContentsAst, E {{LoopValueAst, LoopValueInfo}, TreeWalker2} = value_ast(LoopValue, false, true, Context, TreeWalker1), - LoopValueAst0 = case IsReversed of - true -> erl_syntax:application(erl_syntax:atom(lists), erl_syntax:atom(reverse), [LoopValueAst]); - false -> LoopValueAst - end, + LoopValueAst0 = erl_syntax:application(erl_syntax:atom(erlydtl_runtime), erl_syntax:atom(to_list), [LoopValueAst, erl_syntax:atom(IsReversed)]), CounterVars0 = case resolve_scoped_variable_ast('forloop', Context) of undefined -> diff --git a/src/erlydtl_runtime.erl b/src/erlydtl_runtime.erl index 2e30651..b347e21 100644 --- a/src/erlydtl_runtime.erl +++ b/src/erlydtl_runtime.erl @@ -34,8 +34,7 @@ find_value(Key, {GBSize, GBData}) when is_integer(GBSize) -> undefined end; find_value(Key, Tuple) when is_tuple(Tuple) -> - Module = element(1, Tuple), - case Module of + case element(1, Tuple) of dict -> case dict:find(Key, Tuple) of {ok, Val} -> @@ -203,6 +202,18 @@ stringify_final([El | Rest], Out, true = BinaryStrings) when is_tuple(El) -> stringify_final([El | Rest], Out, BinaryStrings) -> stringify_final(Rest, [El | Out], BinaryStrings). +to_list(Value, true) -> + lists:reverse(to_list(Value, false)); +to_list(Value, false) when is_list(Value) -> + Value; +to_list(Value, false) when is_tuple(Value) -> + case element(1, Value) of + 'Elixir.Ecto.Associations.HasMany' -> + Value:to_list(); + _ -> + tuple_to_list(Value) + end. + init_counter_stats(List) -> init_counter_stats(List, undefined). From 8054566c189bd957c0586ada3b9cb9c1ab7b567b Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Sun, 6 Oct 2013 15:41:53 -0500 Subject: [PATCH 041/361] Make tests work across time zones --- tests/expect/filters | 4 ++-- tests/input/filters | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/expect/filters b/tests/expect/filters index 54ce998..3367ec1 100644 --- a/tests/expect/filters +++ b/tests/expect/filters @@ -7,8 +7,8 @@ Centered: center -Date format: Thu, 24 Jul 1975 00:00:00 +0100 -DateTime format: Thu, 24 Jul 1975 07:13:01 +0100 +Date format: Thu, 24 Jul 1975 +DateTime format: Thu, 24 Jul 1975 07:13:01 Escape JS: \u0022 \u0027 diff --git a/tests/input/filters b/tests/input/filters index 5947d18..bc28c4a 100644 --- a/tests/input/filters +++ b/tests/input/filters @@ -7,8 +7,8 @@ Centered: {{ "center"|center:20 }} -Date format: {{ date_var1|date:"r" }} -DateTime format: {{ datetime_var1|date:"r" }} +Date format: {{ date_var1|date:"D, d M Y" }} +DateTime format: {{ datetime_var1|date:"D, d M Y H:i:s" }} Escape JS: {{ "\" '"|escapejs }} From c28fb798c7fce78c66364739f0c02a6721e0a57b Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Sun, 6 Oct 2013 15:51:36 -0500 Subject: [PATCH 042/361] Support Ecto belongs_to and has_one associations --- src/erlydtl_runtime.erl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/erlydtl_runtime.erl b/src/erlydtl_runtime.erl index b347e21..35b25e3 100644 --- a/src/erlydtl_runtime.erl +++ b/src/erlydtl_runtime.erl @@ -45,7 +45,15 @@ find_value(Key, Tuple) when is_tuple(Tuple) -> Module -> case lists:member({Key, 1}, Module:module_info(exports)) of true -> - Tuple:Key(); + case Tuple:Key() of + Val when is_tuple(Val) -> + case element(1, Val) of + 'Elixir.Ecto.Associations.BelongsTo' -> Val:get(); + 'Elixir.Ecto.Associations.HasOne' -> Val:get(); + _ -> Val + end; + Val -> Val + end; _ -> undefined end From 350733733038574d353fecefd8a2b28683c4fe87 Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Fri, 18 Oct 2013 11:48:26 -0500 Subject: [PATCH 043/361] Fix "now" tag in sources_parser --- src/i18n/sources_parser.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/sources_parser.erl b/src/i18n/sources_parser.erl index 1323b87..b9f0178 100644 --- a/src/i18n/sources_parser.erl +++ b/src/i18n/sources_parser.erl @@ -57,6 +57,7 @@ process_ast(Fname,[Head|Tail], Acc) -> process_token(Fname, {block,{identifier,{_Line,_Col},_Identifier},Children}, Acc ) -> process_ast(Fname, Children, Acc); process_token(Fname, {trans,{string_literal,{Line,Col},String}}, Acc ) -> [{unescape(String), {Fname, Line, Col}} | Acc]; process_token(_Fname, {apply_filter, _Value, _Filter}, Acc) -> Acc; +process_token(_Fname, {date, now, _Filter}, Acc) -> Acc; process_token(Fname, {_Instr, _Cond, Children}, Acc) -> process_ast(Fname, Children, Acc); process_token(Fname, {_Instr, _Cond, Children, Children2}, Acc) -> AccModified = process_ast(Fname, Children, Acc), From a62101b0cc377fac27a6d849e30c829c66dc75e6 Mon Sep 17 00:00:00 2001 From: Drew Gulino Date: Thu, 24 Oct 2013 16:31:54 -0400 Subject: [PATCH 044/361] =?UTF-8?q?Added=20new=20time=20format=20string:?= =?UTF-8?q?=20o=20o=20=E2=80=93=20the=20ISO=208601=20year=20number=20https?= =?UTF-8?q?://docs.djangoproject.com/en/dev/releases/1.4/#two-new-date-for?= =?UTF-8?q?mat-strings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/filter_lib/.erlydtl_dateformat.erl.swo | Bin 0 -> 24576 bytes src/filter_lib/erlydtl_dateformat.erl | 70 +++++++++++++++++++-- tests/src/erlydtl_dateformat_tests.erl | 42 +++++++++++-- tests/src/erlydtl_unittests.erl | 2 +- 4 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 src/filter_lib/.erlydtl_dateformat.erl.swo diff --git a/src/filter_lib/.erlydtl_dateformat.erl.swo b/src/filter_lib/.erlydtl_dateformat.erl.swo new file mode 100644 index 0000000000000000000000000000000000000000..d41e9d2ca3baa4e766f7ff972f6b1a2f9d0f3c17 GIT binary patch literal 24576 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 Ss3i+uX}dLRxq2Czs{TJ@M7sI_ literal 0 HcmV?d00001 diff --git a/src/filter_lib/erlydtl_dateformat.erl b/src/filter_lib/erlydtl_dateformat.erl index 2482af8..9b3efba 100644 --- a/src/filter_lib/erlydtl_dateformat.erl +++ b/src/filter_lib/erlydtl_dateformat.erl @@ -1,5 +1,5 @@ -module(erlydtl_dateformat). --export([format/1, format/2]). +-export([format/1, format/2, weeknum_year/3, year_weeknum/3]). -define(TAG_SUPPORTED(C), C =:= $a orelse @@ -37,7 +37,9 @@ C =:= $y orelse C =:= $Y orelse C =:= $z orelse - C =:= $Z + C =:= $Z orelse + C =:= $o orelse + C =:= $e ). % @@ -288,6 +290,14 @@ tag_to_value($z, {Y,M,D}, _) -> tag_to_value($Z, _, _) -> "TODO"; +%% e โ€“ the name of the timezone of the given datetime object +tag_to_value($e, _, _) -> + "TODO"; + +%% o โ€“ the ISO 8601 year number +tag_to_value($o, {Y,M,D}, _) -> + integer_to_list(weeknum_year(Y,M,D)); + tag_to_value(C, Date, Time) -> io:format("Unimplemented tag : ~p [Date : ~p] [Time : ~p]", [C, Date, Time]), @@ -295,7 +305,7 @@ tag_to_value(C, Date, Time) -> % Date helper functions day_of_year(Y,M,D) -> - day_of_year(Y,M,D,0). + day_of_year(Y,M,D,0). day_of_year(_Y,M,D,Count) when M =< 1 -> D + Count; day_of_year(Y,M,D,Count) when M =< 12 -> @@ -320,7 +330,59 @@ year_weeknum(Y,M,D) -> _ -> Wk end end. - + +weeknum_year(Y,M,D) -> + WeekNum = year_weeknum(Y,M,D), + case M of + 1 -> + case WeekNum of + 53 -> Y - 1; + 52 -> Y - 1; + _ -> Y + end; + 12 -> + case WeekNum of + 2 -> Y + 1; + 1 -> Y + 1; + _ -> Y + end; + _ -> Y + end. + + +%%weeknum_year(Year,Month,Date) -> +%% FirstMondayy = first_monday(Year), +%% LastyeSunday = last_sunday(Year), +%% % if Today < FirstMonday but still in Year, then WeekNumYear = Year - 1; +%% % if Today > LastSunday but still in Year, then WeekNumYear = Year + 1; +%% % else WeekNumYear = Year; +%% % WeekNumYear. +%% +%% end. +%% +%%first_monday(Year) -> +%% First = (calendar:day_of_the_week(Y, 1, 1), +%% case First >= 1 of +%% true -> +%% case First == 1 of +%% true -> 1; +%% false -> +%% false -> +%% case First > 1 +%% + + + +%Dates in January Effect +%M T W T F S S Week number Week assigned to +%1 2 3 4 5 6 7 1 New year +% 1 2 3 4 5 6 1 New year +% 1 2 3 4 5 1 New year +% 1 2 3 4 1 New year +% 1 2 3 53 Previous year +% 1 2 53 or 52 Previous year +% 1 52 Previous year + weeks_in_year(Y) -> D1 = calendar:day_of_the_week(Y, 1, 1), D2 = calendar:day_of_the_week(Y, 12, 31), diff --git a/tests/src/erlydtl_dateformat_tests.erl b/tests/src/erlydtl_dateformat_tests.erl index a86104f..08b7f7b 100644 --- a/tests/src/erlydtl_dateformat_tests.erl +++ b/tests/src/erlydtl_dateformat_tests.erl @@ -18,7 +18,7 @@ run_tests() -> {"s", "00"}, {"S", "th"}, {"t", "31"}, {"w", "0"}, {"W", "27"}, {"y", "79"}, {"Y", "1979"}, {"z", "189"}, {"jS F Y H:i", "8th July 1979 00:00"}, - {"jS o\\f F", "8th of July"}, + {"jS \\o\\f F", "8th of July"}, % We expect these to come back verbatim {"x", "x"}, {"C", "C"}, {";", ";"}, {"%", "%"} @@ -43,7 +43,7 @@ run_tests() -> {"s", "12"}, {"S", "th"}, {"t", "31"}, {"w", "0"}, {"W", "27"}, {"y", "79"}, {"Y", "1979"}, {"z", "189"}, {"jS F Y H:i", "8th July 1979 22:07"}, - {"jS o\\f F", "8th of July"}, + {"jS \\o\\f F", "8th of July"}, % We expect these to come back verbatim {"x", "x"}, {"C", "C"}, {";", ";"}, {"%", "%"} % TODO : timzeone related tests. @@ -67,7 +67,7 @@ run_tests() -> {"s", "09"}, {"S", "th"}, {"t", "31"}, {"w", "4"}, {"W", "52"}, {"y", "08"}, {"Y", "2008"}, {"z", "360"}, {"jS F Y H:i", "25th December 2008 07:00"}, - {"jS o\\f F", "25th of December"}, + {"jS \\o\\f F", "25th of December"}, % We expect these to come back verbatim {"x", "x"}, {"C", "C"}, {";", ";"}, {"%", "%"} % TODO : timzeone related tests. @@ -91,7 +91,7 @@ run_tests() -> {"s", "59"}, {"S", "th"}, {"t", "29"}, {"w", "0"}, {"W", "9"}, {"y", "04"}, {"Y", "2004"}, {"z", "58"}, {"jS F Y H:i", "29th February 2004 12:00"}, - {"jS o\\f F", "29th of February"}, + {"jS \\o\\f F", "29th of February"}, % We expect these to come back verbatim {"x", "x"}, {"C", "C"}, {";", ";"}, {"%", "%"} % TODO : timzeone related tests. @@ -115,7 +115,7 @@ run_tests() -> {"s", "09"}, {"S", "th"}, {"t", "29"}, {"w", "0"}, {"W", "9"}, {"y", "04"}, {"Y", "2004"}, {"z", "58"}, {"jS F Y H:i", "29th February 2004 12:00"}, - {"jS o\\f F", "29th of February"}, + {"jS \\o\\f F", "29th of February"}, % We expect these to come back verbatim {"x", "x"}, {"C", "C"}, {";", ";"}, {"%", "%"} % TODO : timzeone related tests. @@ -158,6 +158,38 @@ run_tests() -> { "weeknum 4.1", {2008, 2, 28}, [{"W", "9"}] }, { "weeknum 4.2", {1975, 7, 24}, [{"W","30"}] }, + % Yearweek tests. Largely based on examples from : + % http://en.wikipedia.org/wiki/ISO_week_date + { "weeknum_year 1.1", {2005, 1, 1}, [{"o", "2004"}] }, + { "weeknum_year 1.2", {2005, 1, 2}, [{"o", "2004"}] }, + { "weeknum_year 1.3", {2005, 12, 31}, [{"o", "2005"}] }, + { "weeknum_year 1.4", {2007, 1, 1}, [{"o", "2007"}] }, + { "weeknum_year 1.5", {2007, 12, 30}, [{"o", "2007"}] }, + { "weeknum_year 1.6", {2007, 12, 31}, [{"o", "2008"}] }, + { "weeknum_year 1.6", {2008, 1, 1}, [{"o", "2008"}] }, + { "weeknum_year 1.7", {2008, 12, 29}, [{"o", "2009"}] }, + { "weeknum_year 1.8", {2008, 12, 31}, [{"o", "2009"}] }, + { "weeknum_year 1.9", {2009, 1, 1}, [{"o", "2009"}] }, + { "weeknum_year 1.10", {2009, 12, 31}, [{"o", "2009"}] }, + { "weeknum_year 1.11", {2010, 1, 3}, [{"o", "2009"}] }, + % Examples where the ISO year is three days into + % the next Gregorian year + { "weeknum_year 2.1", {2009, 12, 31}, [{"o", "2009"}] }, + { "weeknum_year 2.2", {2010, 1, 1}, [{"o", "2009"}] }, + { "weeknum_year 2.3", {2010, 1, 2}, [{"o", "2009"}] }, + { "weeknum_year 2.4", {2010, 1, 3}, [{"o", "2009"}] }, + { "weeknum_year 2.5", {2010, 1, 5}, [{"o", "2010"}] }, + % Example where the ISO year is three days into + % the previous Gregorian year + { "weeknum_year 3.1", {2008, 12, 28}, [{"o", "2008"}] }, + { "weeknum_year 3.2", {2008, 12, 29}, [{"o", "2009"}] }, + { "weeknum_year 3.3", {2008, 12, 30}, [{"o", "2009"}] }, + { "weeknum_year 3.4", {2008, 12, 31}, [{"o", "2009"}] }, + { "weeknum_year 3.5", {2009, 1, 1}, [{"o", "2009"}] }, + % freeform tests + { "weeknum_year 4.1", {2008, 2, 28}, [{"o", "2008"}] }, + { "weeknum_year 4.2", {1975, 7, 24}, [{"o", "1975"}] }, + % Ordinal suffix tests. { "Ordinal suffix 1", {1984,1,1}, [{"S", "st"}] }, { "Ordinal suffix 2", {1984,2,2}, [{"S", "nd"}] }, diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index ab4d8f3..117449a 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -85,7 +85,7 @@ tests() -> ]}, {"now", [ {"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()} ]}, {"if", [ {"If/else", From d2284732940a136548d4889bf1fed671de2a19a4 Mon Sep 17 00:00:00 2001 From: Drew Gulino Date: Thu, 24 Oct 2013 16:39:17 -0400 Subject: [PATCH 045/361] removed remarked out junk --- src/filter_lib/erlydtl_dateformat.erl | 34 --------------------------- 1 file changed, 34 deletions(-) diff --git a/src/filter_lib/erlydtl_dateformat.erl b/src/filter_lib/erlydtl_dateformat.erl index 9b3efba..1f24511 100644 --- a/src/filter_lib/erlydtl_dateformat.erl +++ b/src/filter_lib/erlydtl_dateformat.erl @@ -349,40 +349,6 @@ weeknum_year(Y,M,D) -> _ -> Y end. - -%%weeknum_year(Year,Month,Date) -> -%% FirstMondayy = first_monday(Year), -%% LastyeSunday = last_sunday(Year), -%% % if Today < FirstMonday but still in Year, then WeekNumYear = Year - 1; -%% % if Today > LastSunday but still in Year, then WeekNumYear = Year + 1; -%% % else WeekNumYear = Year; -%% % WeekNumYear. -%% -%% end. -%% -%%first_monday(Year) -> -%% First = (calendar:day_of_the_week(Y, 1, 1), -%% case First >= 1 of -%% true -> -%% case First == 1 of -%% true -> 1; -%% false -> -%% false -> -%% case First > 1 -%% - - - -%Dates in January Effect -%M T W T F S S Week number Week assigned to -%1 2 3 4 5 6 7 1 New year -% 1 2 3 4 5 6 1 New year -% 1 2 3 4 5 1 New year -% 1 2 3 4 1 New year -% 1 2 3 53 Previous year -% 1 2 53 or 52 Previous year -% 1 52 Previous year - weeks_in_year(Y) -> D1 = calendar:day_of_the_week(Y, 1, 1), D2 = calendar:day_of_the_week(Y, 12, 31), From 99ea1ac3c28514e6715d8bc07999e1f1dd18a5e8 Mon Sep 17 00:00:00 2001 From: Drew Gulino Date: Thu, 24 Oct 2013 16:40:39 -0400 Subject: [PATCH 046/361] removed swap file --- src/filter_lib/.erlydtl_dateformat.erl.swo | Bin 24576 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/filter_lib/.erlydtl_dateformat.erl.swo diff --git a/src/filter_lib/.erlydtl_dateformat.erl.swo b/src/filter_lib/.erlydtl_dateformat.erl.swo deleted file mode 100644 index d41e9d2ca3baa4e766f7ff972f6b1a2f9d0f3c17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 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 Ss3i+uX}dLRxq2Czs{TJ@M7sI_ From 83eb0a8962cde6969d912c1d046ba3509ac9bde5 Mon Sep 17 00:00:00 2001 From: Drew Gulino Date: Thu, 24 Oct 2013 16:42:19 -0400 Subject: [PATCH 047/361] fixed indentation --- src/filter_lib/erlydtl_dateformat.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filter_lib/erlydtl_dateformat.erl b/src/filter_lib/erlydtl_dateformat.erl index 1f24511..69586e4 100644 --- a/src/filter_lib/erlydtl_dateformat.erl +++ b/src/filter_lib/erlydtl_dateformat.erl @@ -305,7 +305,7 @@ tag_to_value(C, Date, Time) -> % Date helper functions day_of_year(Y,M,D) -> - day_of_year(Y,M,D,0). + day_of_year(Y,M,D,0). day_of_year(_Y,M,D,Count) when M =< 1 -> D + Count; day_of_year(Y,M,D,Count) when M =< 12 -> From 5926f5ea727ac1f65cdbb9919f7ce85ccce8517b Mon Sep 17 00:00:00 2001 From: Drew Gulino Date: Fri, 25 Oct 2013 09:10:10 -0400 Subject: [PATCH 048/361] removed exports used for manual testing removed unimplemented '$e' tag --- src/filter_lib/erlydtl_dateformat.erl | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/filter_lib/erlydtl_dateformat.erl b/src/filter_lib/erlydtl_dateformat.erl index 69586e4..48140cf 100644 --- a/src/filter_lib/erlydtl_dateformat.erl +++ b/src/filter_lib/erlydtl_dateformat.erl @@ -1,5 +1,5 @@ -module(erlydtl_dateformat). --export([format/1, format/2, weeknum_year/3, year_weeknum/3]). +-export([format/1, format/2]). -define(TAG_SUPPORTED(C), C =:= $a orelse @@ -38,8 +38,7 @@ C =:= $Y orelse C =:= $z orelse C =:= $Z orelse - C =:= $o orelse - C =:= $e + C =:= $o ). % @@ -290,10 +289,6 @@ tag_to_value($z, {Y,M,D}, _) -> tag_to_value($Z, _, _) -> "TODO"; -%% e โ€“ the name of the timezone of the given datetime object -tag_to_value($e, _, _) -> - "TODO"; - %% o โ€“ the ISO 8601 year number tag_to_value($o, {Y,M,D}, _) -> integer_to_list(weeknum_year(Y,M,D)); From b9d5ee7422cd7481f36ad2e522712b4bef4cb57f Mon Sep 17 00:00:00 2001 From: Evan Miller Date: Sat, 26 Oct 2013 10:53:45 -0500 Subject: [PATCH 049/361] Clean up weeknum_year function --- src/filter_lib/erlydtl_dateformat.erl | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/filter_lib/erlydtl_dateformat.erl b/src/filter_lib/erlydtl_dateformat.erl index 48140cf..cdc9bfd 100644 --- a/src/filter_lib/erlydtl_dateformat.erl +++ b/src/filter_lib/erlydtl_dateformat.erl @@ -328,19 +328,11 @@ year_weeknum(Y,M,D) -> weeknum_year(Y,M,D) -> WeekNum = year_weeknum(Y,M,D), - case M of - 1 -> - case WeekNum of - 53 -> Y - 1; - 52 -> Y - 1; - _ -> Y - end; - 12 -> - case WeekNum of - 2 -> Y + 1; - 1 -> Y + 1; - _ -> Y - end; + case {M, WeekNum} of + {1, 53} -> Y - 1; + {1, 52} -> Y - 1; + {12, 1} -> Y + 1; + {12, 2} -> Y + 1; _ -> Y end. From 0c35774b869532c0a70d80e2969d3d531832fc36 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Mon, 25 Nov 2013 14:47:10 +0100 Subject: [PATCH 050/361] remove exec flag on source files. --- src/erlydtl.erl | 0 src/erlydtl_compiler.erl | 0 src/erlydtl_filters.erl | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 src/erlydtl.erl mode change 100755 => 100644 src/erlydtl_compiler.erl mode change 100755 => 100644 src/erlydtl_filters.erl diff --git a/src/erlydtl.erl b/src/erlydtl.erl old mode 100755 new mode 100644 diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl old mode 100755 new mode 100644 diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl old mode 100755 new mode 100644 From cd1d4795f4b8825b55ef43aa9c473e3506c768e3 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Wed, 6 Nov 2013 06:54:22 +0100 Subject: [PATCH 051/361] Add `scanner_module` as compiler option. --- include/erlydtl_ext.hrl | 1 + src/erlydtl_compiler.erl | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/include/erlydtl_ext.hrl b/include/erlydtl_ext.hrl index 5d8a962..181d0a6 100755 --- a/include/erlydtl_ext.hrl +++ b/include/erlydtl_ext.hrl @@ -20,6 +20,7 @@ verbose = false, is_compiling_dir = false, extension_module = undefined, + scanner_module = erlydtl_scanner, scanned_tokens = [] }). diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 4d32616..a6f5729 100644 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -246,7 +246,8 @@ init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) -> locale = proplists:get_value(locale, Options, Ctx#dtl_context.locale), verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose), is_compiling_dir = IsCompilingDir, - extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module) + extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module), + scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module) }, case call_extension(Context, init_context, [Context]) of {ok, C} when is_record(C, dtl_context) -> C; @@ -307,8 +308,9 @@ is_up_to_date(CheckSum, Context) -> parse(Data) -> parse(Data, #dtl_context{}). -parse(Data, Context) when is_binary(Data) -> - check_scan(erlydtl_scanner:scan(binary_to_list(Data)), Context); +parse(Data, #dtl_context{ scanner_module=Scanner }=Context) + when is_binary(Data) -> + check_scan(apply(Scanner, scan, [binary_to_list(Data)]), Context); parse(File, Context) -> {M, F} = Context#dtl_context.reader, case catch M:F(File) of @@ -375,7 +377,7 @@ check_scan({error, Err, State}, Context) -> undefined -> {error, Err}; {ok, NewState} -> - check_scan(erlydtl_scanner:resume(NewState), Context); + check_scan(apply(Context#dtl_context.scanner_module, resume, [NewState]), Context); ExtRes -> ExtRes end. From 73a0835b10188ee79eb48dc35effb7d69bd8adac Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Wed, 6 Nov 2013 12:02:43 +0100 Subject: [PATCH 052/361] Check OS environment for additional compiler options. Any options found in the "ERLYDTL_COMPILER_OPTIONS" env variable will be appended to the options passed to erlydtl. For instance, to use this to run the test suite with a non-default scanner:: $ ERLYDTL_COMPILER_OPTIONS={scanner_module,erlydtl_new_scanner} make test --- README.markdown | 5 +++++ src/erlydtl_compiler.erl | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index 9275f6f..3950e3d 100644 --- a/README.markdown +++ b/README.markdown @@ -109,6 +109,11 @@ lists). Defaults to `true`. * `verbose` - Enable verbose printing of compilation results. + +Additional compiler options can be provided with the `ERLYDTL_COMPILER_OPTIONS` +OS environment variable. + + Helper compilation ------------------ diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index a6f5729..2a7075c 100644 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -59,9 +59,10 @@ compile(Binary, Module) when is_binary(Binary) -> compile(File, Module) -> compile(File, Module, []). -compile(Binary, Module, Options) when is_binary(Binary) -> +compile(Binary, Module, Options0) when is_binary(Binary) -> File = "", CheckSum = "", + Options = maybe_add_env_default_opts(Options0), Context = init_dtl_context(File, Module, Options), case parse(Binary, Context) of {ok, DjangoParseTree} -> @@ -75,7 +76,8 @@ compile(Binary, Module, Options) when is_binary(Binary) -> Err end; -compile(File, Module, Options) -> +compile(File, Module, Options0) -> + Options = maybe_add_env_default_opts(Options0), Context = init_dtl_context(File, Module, Options), case parse(File, Context) of ok -> @@ -95,7 +97,8 @@ compile(File, Module, Options) -> compile_dir(Dir, Module) -> compile_dir(Dir, Module, []). -compile_dir(Dir, Module, Options) -> +compile_dir(Dir, Module, Options0) -> + Options = maybe_add_env_default_opts(Options0), Context = init_dtl_context_dir(Dir, Module, Options), %% Find all files in Dir (recursively), matching the regex (no %% files ending in "~"). @@ -136,6 +139,35 @@ compile_dir(Dir, Module, Options) -> %% Internal functions %%==================================================================== +%% shamelessly borrowed from: +%% https://github.com/erlang/otp/blob/21095e6830f37676dd29c33a590851ba2c76499b/\ +%% lib/compiler/src/compile.erl#L128 +env_default_opts() -> + Key = "ERLYDTL_COMPILER_OPTIONS", + case os:getenv(Key) of + false -> []; + Str when is_list(Str) -> + case erl_scan:string(Str) of + {ok,Tokens,_} -> + case erl_parse:parse_term(Tokens ++ [{dot, 1}]) of + {ok,List} when is_list(List) -> List; + {ok,Term} -> [Term]; + {error,_Reason} -> + io:format("Ignoring bad term in ~s\n", [Key]), + [] + end; + {error, {_,_,_Reason}, _} -> + io:format("Ignoring bad term in ~s\n", [Key]), + [] + end + end. + +maybe_add_env_default_opts(Options) -> + case proplists:get_value(no_env, Options) of + true -> Options; + _ -> Options ++ env_default_opts() + end. + write_binary(Module1, Bin, Options, Warnings) -> Verbose = proplists:get_value(verbose, Options, false), case proplists:get_value(out_dir, Options) of From 9af4b7d837429cbdeb40da91fa61c5f841c46365 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Wed, 27 Nov 2013 11:45:17 +0100 Subject: [PATCH 053/361] Switch to new slex based scanner. --- .gitignore | 2 + Makefile | 20 +- rebar-slex.config | 8 + src/erlydtl_scanner.erl | 920 +++++++++++++++++++++------------------ src/erlydtl_scanner.slex | 363 +++++++++++++++ 5 files changed, 893 insertions(+), 420 deletions(-) create mode 100644 rebar-slex.config create mode 100644 src/erlydtl_scanner.slex diff --git a/.gitignore b/.gitignore index f8c556f..d337722 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ src/erlydtl_parser.erl ebintest tests/src/erlydtl_extension_testparser.erl tests/output +deps +.emacs* diff --git a/Makefile b/Makefile index 9c84b75..d0faee5 100755 --- a/Makefile +++ b/Makefile @@ -1,12 +1,16 @@ ERL=erl ERLC=erlc -REBAR=./rebar +REBAR=./rebar $(REBAR_ARGS) all: compile -compile: +compile: check-slex @$(REBAR) compile +check-slex: src/erlydtl_scanner.erl +src/erlydtl_scanner.erl: src/erlydtl_scanner.slex + @echo Notice: $@ is outdated by $<, consider running "'make slex'". + compile_test: -mkdir -p ebintest $(ERLC) -o tests/src -I include/erlydtl_preparser.hrl tests/src/erlydtl_extension_testparser.yrl @@ -25,3 +29,15 @@ clean: rm -fv ebintest/* rm -fv erl_crash.dump rm -fv tests/output/* + +# 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 +# been modified locally. +slex: REBAR_DEPS ?= get-deps update-deps +slex: slex-compile + +slex-skip-deps: REBAR_DEPS:= +slex-skip-deps: slex-compile + +slex-compile: + @$(REBAR) -C rebar-slex.config $(REBAR_DEPS) compile diff --git a/rebar-slex.config b/rebar-slex.config new file mode 100644 index 0000000..f832cbb --- /dev/null +++ b/rebar-slex.config @@ -0,0 +1,8 @@ +%% -*- mode: erlang -*- + +{deps, + [{slex, ".*", {git, "git://github.com/erlydtl/slex.git", {branch, "master"}}} + ] +}. + +{plugins, [rebar_slex]}. diff --git a/src/erlydtl_scanner.erl b/src/erlydtl_scanner.erl index 6f4bef5..231ff4a 100644 --- a/src/erlydtl_scanner.erl +++ b/src/erlydtl_scanner.erl @@ -1,16 +1,14 @@ %%%------------------------------------------------------------------- -%%% File: erlydtl_scanner.erl -%%% @author Roberto Saccon [http://rsaccon.com] -%%% @author Evan Miller +%%% File: erlydtl_scanner.slex %%% @author Andreas Stenius -%%% @copyright 2008 Roberto Saccon, Evan Miller -%%% @doc -%%% Template language scanner -%%% @end +%%% @copyright 2013 Andreas Stenius +%%% @doc +%%% erlydtl scanner +%%% @end %%% %%% The MIT License %%% -%%% Copyright (c) 2007 Roberto Saccon, Evan Miller +%%% Copyright (c) 2013 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 @@ -30,426 +28,512 @@ %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN %%% THE SOFTWARE. %%% -%%% @since 2007-11-11 by Roberto Saccon, Evan Miller +%%% @since 2013-11-05 by Andreas Stenius +%%% +%%% Rules based on the original erlydtl_scanner by Robert Saccon and Evan Miller. %%%------------------------------------------------------------------- -module(erlydtl_scanner). --author('rsaccon@gmail.com'). --author('emmiller@gmail.com'). --author('Andreas Stenius '). - --export([scan/1, resume/1]). --include("erlydtl_ext.hrl"). - - -%%==================================================================== -%% API -%%==================================================================== -%%-------------------------------------------------------------------- -%% @spec scan(T::template()) -> {ok, S::tokens()} | {error, Reason} -%% @type template() = string() | binary(). Template to parse -%% @type tokens() = [tuple()]. -%% @doc Scan the template string T and return the a token list or -%% an error. -%% @end -%%-------------------------------------------------------------------- -scan(Template) -> - scan(Template, [], {1, 1}, in_text). - -resume(#scanner_state{ template=Template, scanned=Scanned, - pos=Pos, state=State}) -> - scan(Template, Scanned, Pos, State). - -scan([], Scanned, _, in_text) -> - Tokens = lists:reverse(Scanned), - FixedTokens = reverse_strings(Tokens), - MarkedTokens = mark_keywords(FixedTokens), - AtomizedTokens = atomize_identifiers(MarkedTokens), - {ok, AtomizedTokens}; - -scan([], _Scanned, _, {in_comment, _}) -> - {error, "Reached end of file inside a comment."}; - -scan([], _Scanned, _, _) -> - {error, "Reached end of file inside a code block."}; - -scan(""}); - -scan("{{" ++ T, Scanned, {Row, Column}, in_text) -> - scan(T, [{open_var, {Row, Column}, '{{'} | Scanned], {Row, Column + length("{{")}, {in_code, "}}"}); - -scan(""}); - -scan("{#" ++ T, Scanned, {Row, Column}, in_text) -> - scan(T, Scanned, {Row, Column + length("{#")}, {in_comment, "#}"}); - -scan("#}-->" ++ T, Scanned, {Row, Column}, {in_comment, "#}-->"}) -> - scan(T, Scanned, {Row, Column + length("#}-->")}, in_text); - -scan("#}" ++ T, Scanned, {Row, Column}, {in_comment, "#}"}) -> - scan(T, Scanned, {Row, Column + length("#}")}, in_text); - -scan(""}); - -scan("{%" ++ T, Scanned, {Row, Column}, in_text) -> - scan(T, [{open_tag, {Row, Column}, '{%'} | Scanned], - {Row, Column + length("{%")}, {in_code, "%}"}); - -scan([_ | T], Scanned, {Row, Column}, {in_comment, Closer}) -> - scan(T, Scanned, {Row, Column + 1}, {in_comment, Closer}); - -scan("\n" ++ T, Scanned, {Row, Column}, in_text) -> - scan(T, append_text_char(Scanned, {Row, Column}, $\n), {Row + 1, 1}, in_text); - -scan([H | T], Scanned, {Row, Column}, in_text) -> - scan(T, append_text_char(Scanned, {Row, Column}, H), {Row, Column + 1}, in_text); - -scan("\"" ++ T, Scanned, {Row, Column}, {in_code, Closer}) -> - scan(T, [{string_literal, {Row, Column}, "\""} | Scanned], {Row, Column + 1}, {in_double_quote, Closer}); - -scan("\"" ++ T, Scanned, {Row, Column}, {in_identifier, Closer}) -> - scan(T, [{string_literal, {Row, Column}, "\""} | Scanned], {Row, Column + 1}, {in_double_quote, Closer}); - -scan("\'" ++ T, Scanned, {Row, Column}, {in_code, Closer}) -> - scan(T, [{string_literal, {Row, Column}, "\""} | Scanned], {Row, Column + 1}, {in_single_quote, Closer}); - -scan("\'" ++ T, Scanned, {Row, Column}, {in_identifier, Closer}) -> - scan(T, [{string_literal, {Row, Column}, "\""} | Scanned], {Row, Column + 1}, {in_single_quote, Closer}); - -scan([$\\ | T], Scanned, {Row, Column}, {in_double_quote, Closer}) -> - scan(T, append_char(Scanned, $\\), {Row, Column + 1}, {in_double_quote_slash, Closer}); - -scan([H | T], Scanned, {Row, Column}, {in_double_quote_slash, Closer}) -> - scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_double_quote, Closer}); - -scan([$\\ | T], Scanned, {Row, Column}, {in_single_quote, Closer}) -> - scan(T, append_char(Scanned, $\\), {Row, Column + 1}, {in_single_quote_slash, Closer}); - -scan([H | T], Scanned, {Row, Column}, {in_single_quote_slash, Closer}) -> - scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_single_quote, Closer}); - - % end quote -scan("\"" ++ T, Scanned, {Row, Column}, {in_double_quote, Closer}) -> - scan(T, append_char(Scanned, 34), {Row, Column + 1}, {in_code, Closer}); - - % treat single quotes the same as double quotes -scan("\'" ++ T, Scanned, {Row, Column}, {in_single_quote, Closer}) -> - scan(T, append_char(Scanned, 34), {Row, Column + 1}, {in_code, Closer}); - -scan([H | T], Scanned, {Row, Column}, {in_double_quote, Closer}) -> - scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_double_quote, Closer}); - -scan([H | T], Scanned, {Row, Column}, {in_single_quote, Closer}) -> - scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_single_quote, Closer}); - - -scan("}}-->" ++ T, Scanned, {Row, Column}, {_, "}}-->"}) -> - scan(T, [{close_var, {Row, Column}, '}}-->'} | Scanned], - {Row, Column + length("}}-->")}, in_text); - -scan("}}" ++ T, Scanned, {Row, Column}, {_, "}}"}) -> - scan(T, [{close_var, {Row, Column}, '}}'} | Scanned], {Row, Column + 2}, in_text); - -scan("%}-->" ++ T, Scanned, {Row, Column}, {_, "%}-->"}) -> - scan(T, [{close_tag, {Row, Column}, '%}-->'} | Scanned], - {Row, Column + length("%}-->")}, in_text); -scan("%}" ++ T, [{identifier, _, "mitabrev"}, {open_tag, _, '{%'}|Scanned], {Row, Column}, {_, "%}"}) -> - scan(T, [{string, {Row, Column + 2}, ""}|Scanned], {Row, Column + 2}, {in_verbatim, undefined}); +-export([scan/1, scan/4]). -scan("%}" ++ T, [{identifier, _, ReversedTag}, {identifier, _, "mitabrev"}, {open_tag, _, '{%'}|Scanned], - {Row, Column}, {_, "%}"}) -> - scan(T, [{string, {Row, Column + 2}, ""}|Scanned], {Row, Column + 2}, {in_verbatim, ReversedTag}); +-compile(nowarn_unused_vars). -scan("%}" ++ T, Scanned, {Row, Column}, {_, "%}"}) -> - scan(T, [{close_tag, {Row, Column}, '%}'} | Scanned], - {Row, Column + 2}, in_text); +-export([resume/1]). -scan("{%" ++ T, Scanned, {Row, Column}, {in_verbatim, Tag}) -> - scan(T, Scanned, {Row, Column + 2}, {in_verbatim_code, lists:reverse("{%"), Tag}); +-record(scanner_state, + {template = [], scanned = [], pos = {1, 1}, + state = in_text}). -scan(" " ++ T, Scanned, {Row, Column}, {in_verbatim_code, BackTrack, Tag}) -> - scan(T, Scanned, {Row, Column + 1}, {in_verbatim_code, [$\ |BackTrack], Tag}); - -scan("endverbatim%}" ++ T, Scanned, {Row, Column}, {in_verbatim_code, _BackTrack, undefined}) -> - scan(T, Scanned, {Row, Column + length("endverbatim%}")}, in_text); - -scan("endverbatim " ++ T, Scanned, {Row, Column}, {in_verbatim_code, BackTrack, Tag}) -> - scan(T, Scanned, {Row, Column + length("endverbatim ")}, - {in_endverbatim_code, "", lists:reverse("endverbatim ", BackTrack), Tag}); - -scan(" " ++ T, Scanned, {Row, Column}, {in_endverbatim_code, "", BackTrack, Tag}) -> - scan(T, Scanned, {Row, Column + 1}, {in_endverbatim_code, "", [$\ |BackTrack], Tag}); - -scan([H|T], Scanned, {Row, Column}, {in_endverbatim_code, EndTag, BackTrack, Tag}) when H >= $a, H =< $z; H >= $0, H =< $9; H =:= $_ -> - scan(T, Scanned, {Row, Column + 1}, {in_endverbatim_code, [H|EndTag], [H|BackTrack], Tag}); - -scan(" " ++ T, Scanned, {Row, Column}, {in_endverbatim_code, Tag, BackTrack, Tag}) -> - scan(T, Scanned, {Row, Column + 1}, {in_endverbatim_code, Tag, [$\ |BackTrack], Tag}); - -scan("%}" ++ T, Scanned, {Row, Column}, {in_endverbatim_code, Tag, _BackTrack, Tag}) -> - scan(T, Scanned, {Row, Column + 2}, in_text); - -scan("%}" ++ T, Scanned, {Row, Column}, {in_endverbatim_code, "", _BackTrack, undefined}) -> - scan(T, Scanned, {Row, Column + 2}, in_text); - -scan([H|T], [{string, Pos, Data}|Scanned], {Row, Column}, {in_endverbatim_code, _, BackTrack, Tag}) -> - NewPos = case H of $\n -> {Row + 1, 1}; _ -> {Row, Column + 1} end, - scan(T, [{string, Pos, [H|BackTrack] ++ Data}|Scanned], NewPos, {in_verbatim, Tag}); - -scan([H|T], [{string, Pos, Data}|Scanned], {Row, Column}, {in_verbatim_code, BackTrack, Tag}) -> - NewPos = case H of $\n -> {Row + 1, 1}; _ -> {Row, Column + 1} end, - scan(T, [{string, Pos, [H|BackTrack] ++ Data}|Scanned], NewPos, {in_verbatim, Tag}); - -scan([H|T], [{string, Pos, Data}|Scanned], {Row, Column}, {in_verbatim, Tag}) -> - NewPos = case H of $\n -> {Row + 1, 1}; _ -> {Row, Column + 1} end, - scan(T, [{string, Pos, [H|Data]}|Scanned], NewPos, {in_verbatim, Tag}); - -scan("==" ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{'==', {Row, Column}} | Scanned], {Row, Column + 2}, {in_code, Closer}); - -scan("!=" ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{'!=', {Row, Column}} | Scanned], {Row, Column + 2}, {in_code, Closer}); - -scan(">=" ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{'>=', {Row, Column}} | Scanned], {Row, Column + 2}, {in_code, Closer}); - -scan("<=" ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{'<=', {Row, Column}} | Scanned], {Row, Column + 2}, {in_code, Closer}); - -scan("<" ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{'<', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer}); - -scan(">" ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{'>', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer}); - -scan("("++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{'(', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer}); - -scan(")" ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{')', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer}); - -scan("," ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{',', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer}); - -scan("|" ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{'|', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer}); - -scan("=" ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{'=', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer}); - -scan(":" ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{':', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer}); - -scan("." ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, [{'.', {Row, Column}} | Scanned], {Row, Column + 1}, {in_code, Closer}); +resume(#scanner_state{template = Template, + scanned = Scanned, pos = Pos, state = State}) -> + scan(Template, Scanned, Pos, State). -scan("_(" ++ T, Scanned, {Row, Column}, {in_code, Closer}) -> - scan(T, lists:reverse([{'_', {Row, Column}}, {'(', {Row, Column + 1}}], Scanned), {Row, Column + 2}, {in_code, Closer}); +to_atom(L) when is_list(L) -> list_to_atom(L). -scan(" " ++ T, Scanned, {Row, Column}, {_, Closer}) -> - scan(T, Scanned, {Row, Column + 1}, {in_code, Closer}); +to_keyword(L, P) -> {to_atom(L ++ "_keyword"), P, L}. +atomize(L, T) -> setelement(3, T, to_atom(L)). -scan([H | T], Scanned, {Row, Column}, {in_code, Closer}) -> - case char_type(H) of - letter_underscore -> - scan(T, [{identifier, {Row, Column}, [H]} | Scanned], {Row, Column + 1}, {in_identifier, Closer}); - hyphen_minus -> - scan(T, [{number_literal, {Row, Column}, [H]} | Scanned], {Row, Column + 1}, {in_number, Closer}); - digit -> - scan(T, [{number_literal, {Row, Column}, [H]} | Scanned], {Row, Column + 1}, {in_number, Closer}); - _ -> - {error, {Row, ?MODULE, lists:concat(["Illegal character in column ", Column])}, - #scanner_state{ template=[H|T], scanned=Scanned, pos={Row, Column}, state={in_code, Closer}}} +is_keyword(Class, {_, _, L} = T) -> + L1 = lists:reverse(L), + case is_keyword(Class, L1) of + true -> to_keyword(L1, element(2, T)); + false -> atomize(L1, T) end; - -scan([H | T], Scanned, {Row, Column}, {in_number, Closer}) -> - case char_type(H) of - digit -> - scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_number, Closer}); - _ -> - {error, {Row, ?MODULE, lists:concat(["Illegal character in column ", Column])}, - #scanner_state{ template=[H|T], scanned=Scanned, pos={Row, Column}, state={in_code, Closer}}} +is_keyword([C | Cs], L) -> + is_keyword(C, L) orelse is_keyword(Cs, L); +is_keyword(all, L) -> is_keyword([any, open, close], L); +is_keyword(open_tag, L) -> is_keyword([any, open], L); +is_keyword(close_tag, L) -> is_keyword([any, close], L); +is_keyword(any, "in") -> true; +is_keyword(any, "not") -> true; +is_keyword(any, "or") -> true; +is_keyword(any, "and") -> true; +is_keyword(any, "as") -> true; +is_keyword(any, "by") -> true; +is_keyword(any, "with") -> true; +is_keyword(close, "only") -> true; +is_keyword(close, "parsed") -> true; +is_keyword(close, "noop") -> true; +is_keyword(close, "reversed") -> true; +is_keyword(close, "openblock") -> true; +is_keyword(close, "closeblock") -> true; +is_keyword(close, "openvariable") -> true; +is_keyword(close, "closevariable") -> true; +is_keyword(close, "openbrace") -> true; +is_keyword(close, "closebrace") -> true; +is_keyword(close, "opencomment") -> true; +is_keyword(close, "closecomment") -> true; +is_keyword(open, "autoescape") -> true; +is_keyword(open, "endautoescape") -> true; +is_keyword(open, "block") -> true; +is_keyword(open, "endblock") -> true; +is_keyword(open, "comment") -> true; +is_keyword(open, "endcomment") -> true; +is_keyword(open, "cycle") -> true; +is_keyword(open, "extends") -> true; +is_keyword(open, "filter") -> true; +is_keyword(open, "endfilter") -> true; +is_keyword(open, "firstof") -> true; +is_keyword(open, "for") -> true; +is_keyword(open, "empty") -> true; +is_keyword(open, "endfor") -> true; +is_keyword(open, "if") -> true; +is_keyword(open, "elif") -> true; +is_keyword(open, "else") -> true; +is_keyword(open, "endif") -> true; +is_keyword(open, "ifchanged") -> true; +is_keyword(open, "endifchanged") -> true; +is_keyword(open, "ifequal") -> true; +is_keyword(open, "endifequal") -> true; +is_keyword(open, "ifnotequal") -> true; +is_keyword(open, "endifnotequal") -> true; +is_keyword(open, "include") -> true; +is_keyword(open, "now") -> true; +is_keyword(open, "regroup") -> true; +is_keyword(open, "endregroup") -> true; +is_keyword(open, "spaceless") -> true; +is_keyword(open, "endspaceless") -> true; +is_keyword(open, "ssi") -> true; +is_keyword(open, "templatetag") -> true; +is_keyword(open, "widthratio") -> true; +is_keyword(open, "call") -> true; +is_keyword(open, "endwith") -> true; +is_keyword(open, "trans") -> true; +is_keyword(open, "blocktrans") -> true; +is_keyword(open, "endblocktrans") -> true; +is_keyword(_, _) -> false. + +scan(Template) when is_list(Template) -> + scan(Template, [], {1, 1}, in_text). + +scan("{{" ++ T, S, {R, C} = P, in_text) -> + scan(T, + [{open_var, P, "{{"} | post_process(S, open_var)], + {R, C + 2}, {in_code, "}}"}); +scan("{%" ++ T, S, {R, C} = P, in_text) -> + scan(T, + [{open_tag, P, "{%"} | post_process(S, open_tag)], + {R, C + 2}, {in_code, "%}"}); +scan(""}); +scan(""}); +scan("{#" ++ T, S, {R, C}, in_text) -> + scan(T, S, {R, C + 2}, {in_comment, "#}"}); +scan(""}); +scan("#}-->" ++ T, S, {R, C}, {_, "#}-->"}) -> + scan(T, S, {R, C + 5}, in_text); +scan("#}" ++ T, S, {R, C}, {_, "#}"}) -> + scan(T, S, {R, C + 2}, in_text); +scan([H | T], S, {R, C}, {in_comment, E} = St) -> + scan(T, S, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + St); +scan([H | T], S, {R, C} = P, in_text = St) -> + scan(T, + case S of + [{string, _, L} = M | Ss] -> + [setelement(3, M, [H | L]) | Ss]; + _ -> [{string, P, [H]} | post_process(S, string)] + end, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + St); +scan("\"" ++ T, S, {R, C} = P, {in_code, E}) -> + scan(T, + [{string_literal, P, "\""} | post_process(S, + string_literal)], + {R, C + 1}, {in_double_quote, E}); +scan("'" ++ T, S, {R, C} = P, {in_code, E}) -> + scan(T, + [{string_literal, P, "\""} | post_process(S, + string_literal)], + {R, C + 1}, {in_single_quote, E}); +scan("\"" ++ T, S, {R, C} = P, {in_double_quote, E}) -> + scan(T, + case S of + [{string_literal, _, L} = M | Ss] -> + [setelement(3, M, "\"" ++ L) | Ss]; + _ -> + [{string_literal, P, "\""} | post_process(S, + string_literal)] + end, + {R, C + 1}, {in_code, E}); +scan("'" ++ T, S, {R, C} = P, {in_single_quote, E}) -> + scan(T, + case S of + [{string_literal, _, L} = M | Ss] -> + [setelement(3, M, "\"" ++ L) | Ss]; + _ -> + [{string_literal, P, "\""} | post_process(S, + string_literal)] + end, + {R, C + 1}, {in_code, E}); +scan("\\" ++ T, S, {R, C} = P, {in_double_quote, E}) -> + scan(T, + case S of + [{string_literal, _, L} = M | Ss] -> + [setelement(3, M, "\\" ++ L) | Ss]; + _ -> + [{string_literal, P, "\\"} | post_process(S, + string_literal)] + end, + {R, C + 1}, {in_double_quote_escape, E}); +scan("\\" ++ T, S, {R, C} = P, {in_single_quote, E}) -> + scan(T, + case S of + [{string_literal, _, L} = M | Ss] -> + [setelement(3, M, "\\" ++ L) | Ss]; + _ -> + [{string_literal, P, "\\"} | post_process(S, + string_literal)] + end, + {R, C + 1}, {in_single_quote_escape, E}); +scan([H | T], S, {R, C} = P, + {in_double_quote, E} = St) -> + scan(T, + case S of + [{string_literal, _, L} = M | Ss] -> + [setelement(3, M, [H | L]) | Ss]; + _ -> + [{string_literal, P, [H]} | post_process(S, + string_literal)] + end, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + St); +scan([H | T], S, {R, C} = P, + {in_single_quote, E} = St) -> + scan(T, + case S of + [{string_literal, _, L} = M | Ss] -> + [setelement(3, M, [H | L]) | Ss]; + _ -> + [{string_literal, P, [H]} | post_process(S, + string_literal)] + end, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + St); +scan([H | T], S, {R, C} = P, + {in_double_quote_escape, E}) -> + scan(T, + case S of + [{string_literal, _, L} = M | Ss] -> + [setelement(3, M, [H | L]) | Ss]; + _ -> + [{string_literal, P, [H]} | post_process(S, + string_literal)] + end, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + {in_double_quote, E}); +scan([H | T], S, {R, C} = P, + {in_single_quote_escape, E}) -> + scan(T, + case S of + [{string_literal, _, L} = M | Ss] -> + [setelement(3, M, [H | L]) | Ss]; + _ -> + [{string_literal, P, [H]} | post_process(S, + string_literal)] + end, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + {in_single_quote, E}); +scan("}}-->" ++ T, S, {R, C} = P, {_, "}}-->"}) -> + scan(T, + [{close_var, P, "}}-->"} | post_process(S, close_var)], + {R, C + 5}, in_text); +scan("%}-->" ++ T, S, {R, C} = P, {_, "%}-->"}) -> + scan(T, + [{close_tag, P, "%}-->"} | post_process(S, close_tag)], + {R, C + 5}, in_text); +scan("}}" ++ T, S, {R, C} = P, {_, "}}"}) -> + scan(T, + [{close_var, P, "}}"} | post_process(S, close_var)], + {R, C + 2}, in_text); +scan("%}" ++ T, S, {R, C} = P, {_, "%}"} = St) -> + case S of + [{identifier, _, "mitabrev"}, {open_tag, _, '{%'} + | Ss] -> + scan(T, [{string, {R, C + 2}, ""} | Ss], {R, C + 2}, + {in_verbatim, undefined}); + [{identifier, _, Tag}, {identifier, _, verbatim}, + {open_tag, _, '{%'} + | Ss] -> + scan(T, [{string, {R, C + 2}, ""} | Ss], {R, C + 2}, + {in_verbatim, Tag}); + _ -> + scan(T, + [{close_tag, P, "%}"} | post_process(S, close_tag)], + {R, C + 2}, in_text) end; - -scan([H | T], Scanned, {Row, Column}, {in_identifier, Closer}) -> - case char_type(H) of - letter_underscore -> - scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_identifier, Closer}); - digit -> - scan(T, append_char(Scanned, H), {Row, Column + 1}, {in_identifier, Closer}); - _ -> - {error, {Row, ?MODULE, lists:concat(["Illegal character in column ", Column])}, - #scanner_state{ template=[H|T], scanned=Scanned, pos={Row, Column}, state={in_code, Closer}}} - end. - - % internal functions - -append_char([{Type, Pos, Chars}|Scanned], Char) -> - [{Type, Pos, [Char | Chars]} | Scanned]. - -append_text_char([], {Row, Column}, Char) -> - [{string, {Row, Column}, [Char]}]; -append_text_char([{string, StrPos, Chars} |Scanned1], _, Char) -> - [{string, StrPos, [Char | Chars]} | Scanned1]; -append_text_char(Scanned, {Row, Column}, Char) -> - [{string, {Row, Column}, [Char]} | Scanned]. - -char_type(C) when ((C >= $a) andalso (C =< $z)) orelse ((C >= $A) andalso (C =< $Z)) orelse (C == $_) -> - letter_underscore; -char_type(C) when ((C >= $0) andalso (C =< $9)) -> - digit; -char_type($-) -> - hyphen_minus; -char_type(_C) -> - undefined. - -reverse_strings(Tokens) -> - reverse_strings(Tokens, []). - -reverse_strings([], Acc) -> - lists:reverse(Acc); -reverse_strings([{Category, Pos, String}|T], Acc) when Category =:= string; Category =:= identifier; - Category =:= string_literal; Category =:= number_literal -> - reverse_strings(T, [{Category, Pos, lists:reverse(String)}|Acc]); -reverse_strings([Other|T], Acc) -> - reverse_strings(T, [Other|Acc]). - -mark_keywords(Tokens) -> - mark_keywords(Tokens, []). - -mark_keywords([], Acc) -> - lists:reverse(Acc); -mark_keywords([{identifier, Pos, "in" = String}|T], Acc) -> - mark_keywords(T, [{in_keyword, Pos, String}|Acc]); -mark_keywords([{identifier, Pos, "not" = String}|T], Acc) -> - mark_keywords(T, [{not_keyword, Pos, String}|Acc]); -mark_keywords([{identifier, Pos, "or" = String}|T], Acc) -> - mark_keywords(T, [{or_keyword, Pos, String}|Acc]); -mark_keywords([{identifier, Pos, "and" = String}|T], Acc) -> - mark_keywords(T, [{and_keyword, Pos, String}|Acc]); -mark_keywords([{identifier, Pos, "as" = String}|T], Acc) -> - mark_keywords(T, [{as_keyword, Pos, String}|Acc]); -mark_keywords([{identifier, Pos, "by" = String}|T], Acc) -> - mark_keywords(T, [{by_keyword, Pos, String}|Acc]); -mark_keywords([{identifier, Pos, "with" = String}|T], Acc) -> - mark_keywords(T, [{with_keyword, Pos, String}|Acc]); - % These must be succeeded by a close_tag -mark_keywords([{identifier, Pos, "only" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{only_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "parsed" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{parsed_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "noop" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{noop_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "reversed" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{reversed_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "openblock" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{openblock_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "closeblock" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{closeblock_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "openvariable" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{openvariable_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "closevariable" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{closevariable_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "openbrace" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{openbrace_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "closebrace" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{closebrace_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "opencomment" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{opencomment_keyword, Pos, String}, CloseTag], Acc)); -mark_keywords([{identifier, Pos, "closecomment" = String}, {close_tag, _, _} = CloseTag|T], Acc) -> - mark_keywords(T, lists:reverse([{closecomment_keyword, Pos, String}, CloseTag], Acc)); - % The rest must be preceded by an open_tag. - % This allows variables to have the same names as tags. -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "autoescape" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {autoescape_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endautoescape" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endautoescape_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "block" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {block_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endblock" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endblock_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "comment" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {comment_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endcomment" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endcomment_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "cycle" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {cycle_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "extends" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {extends_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "filter" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {filter_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endfilter" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endfilter_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "firstof" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {firstof_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "for" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {for_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "empty" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {empty_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endfor" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endfor_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "if" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {if_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "elif" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {elif_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "else" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {else_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endif" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endif_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "ifchanged" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {ifchanged_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endifchanged" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endifchanged_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "ifequal" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {ifequal_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endifequal" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endifequal_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "ifnotequal" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {ifnotequal_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endifnotequal" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endifnotequal_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "include" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {include_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "now" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {now_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "regroup" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {regroup_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endregroup" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endregroup_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "spaceless" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {spaceless_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endspaceless" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endspaceless_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "ssi" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {ssi_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "templatetag" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {templatetag_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "widthratio" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {widthratio_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "call" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {call_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endwith" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endwith_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "trans" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {trans_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "blocktrans" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {blocktrans_keyword, Pos, String}], Acc)); -mark_keywords([{open_tag, _, _} = OpenToken, {identifier, Pos, "endblocktrans" = String}|T], Acc) -> - mark_keywords(T, lists:reverse([OpenToken, {endblocktrans_keyword, Pos, String}], Acc)); -mark_keywords([H|T], Acc) -> - mark_keywords(T, [H|Acc]). - -atomize_identifiers(Tokens) -> - atomize_identifiers(Tokens, []). - -atomize_identifiers([], Acc) -> - lists:reverse(Acc); -atomize_identifiers([{identifier, Pos, String}|T], Acc) -> - atomize_identifiers(T, [{identifier, Pos, list_to_atom(String)}|Acc]); -atomize_identifiers([H|T], Acc) -> - atomize_identifiers(T, [H|Acc]). +scan("{%" ++ T, S, {R, C} = P, {in_verbatim, E} = St) -> + scan(T, S, {R, C + 2}, {in_verbatim_code, {E, "%{"}}); +scan(" " ++ T, S, {R, C} = P, + {in_verbatim_code, E} = St) -> + {Tag, Backtrack} = E, + scan(T, S, {R, C + 1}, + {in_verbatim_code, {Tag, [$ | Backtrack]}}); +scan("endverbatim%}" ++ T, S, {R, C} = P, + {in_verbatim_code, E} = St) + when element(1, E) =:= undefined -> + scan(T, S, {R, C + 13}, in_text); +scan("endverbatim " ++ T, S, {R, C} = P, + {in_verbatim_code, E} = St) -> + {Tag, Backtrack} = E, + scan(T, S, {R, C + 12}, + {in_endverbatim_code, + {Tag, lists:reverse("endverbatim ", Backtrack), ""}}); +scan(" " ++ T, S, {R, C} = P, + {in_endverbatim_code, E} = St) + when element(3, E) =:= "" -> + {Tag, Backtrack, EndTag} = E, + scan(T, S, {R, C + 1}, + {in_endverbatim_code, {Tag, [$ | Backtrack], EndTag}}); +scan([H | T], S, {R, C} = P, + {in_endverbatim_code, E} = St) + when H >= $a andalso H =< $z orelse + H >= $0 andalso H =< $9 orelse H =:= $_ -> + {Tag, Backtrack, EndTag} = E, + scan(T, S, {R, C + 1}, + {in_endverbatim_code, + {Tag, [H | Backtrack], [H | EndTag]}}); +scan(" " ++ T, S, {R, C} = P, + {in_endverbatim_code, E} = St) + when element(1, E) =:= element(3, E) -> + {Tag, Backtrack, Tag} = E, + scan(T, S, {R, C + 1}, + {in_endverbatim_code, {Tag, [$ | Backtrack], Tag}}); +scan("%}" ++ T, S, {R, C} = P, + {in_endverbatim_code, E} = St) + when element(1, E) =:= element(3, E) -> + scan(T, S, {R, C + 2}, in_text); +scan("%}" ++ T, S, {R, C} = P, + {in_endverbatim_code, E} = St) + when element(1, E) =:= undefined andalso + element(3, E) =:= "" -> + scan(T, S, {R, C + 2}, in_text); +scan([H | T], S, {R, C} = P, + {in_endverbatim_code, E} = St) -> + {Tag, Backtrack, _} = E, + scan(T, + case S of + [{string, _, L} = M | Ss] -> + [setelement(3, M, [H | Backtrack] ++ L) | Ss]; + _ -> [{string, P, [H | Backtrack]} | S] + end, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + {in_verbatim, Tag}); +scan([H | T], S, {R, C} = P, + {in_verbatim_code, E} = St) -> + {Tag, Backtrack} = E, + scan(T, + case S of + [{string, _, L} = M | Ss] -> + [setelement(3, M, [H | Backtrack] ++ L) | Ss]; + _ -> [{string, P, [H | Backtrack]} | S] + end, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + {in_verbatim, Tag}); +scan([H | T], S, {R, C} = P, {in_verbatim, E} = St) -> + scan(T, + case S of + [{string, _, L} = M | Ss] -> + [setelement(3, M, [H | L]) | Ss]; + _ -> [{string, P, [H]} | S] + end, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + {in_verbatim, E}); +scan("==" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'==', P} | post_process(S, '==')], {R, C + 2}, + {in_code, E}); +scan("!=" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'!=', P} | post_process(S, '!=')], {R, C + 2}, + {in_code, E}); +scan(">=" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'>=', P} | post_process(S, '>=')], {R, C + 2}, + {in_code, E}); +scan("<=" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'<=', P} | post_process(S, '<=')], {R, C + 2}, + {in_code, E}); +scan(">" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'>', P} | post_process(S, '>')], {R, C + 1}, + {in_code, E}); +scan("<" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'<', P} | post_process(S, '<')], {R, C + 1}, + {in_code, E}); +scan("(" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'(', P} | post_process(S, '(')], {R, C + 1}, + {in_code, E}); +scan(")" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{')', P} | post_process(S, ')')], {R, C + 1}, + {in_code, E}); +scan("," ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{',', P} | post_process(S, ',')], {R, C + 1}, + {in_code, E}); +scan("|" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'|', P} | post_process(S, '|')], {R, C + 1}, + {in_code, E}); +scan("=" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'=', P} | post_process(S, '=')], {R, C + 1}, + {in_code, E}); +scan(":" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{':', P} | post_process(S, ':')], {R, C + 1}, + {in_code, E}); +scan("." ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'.', P} | post_process(S, '.')], {R, C + 1}, + {in_code, E}); +scan("_(" ++ T, S, {R, C} = P, {_, E}) -> + scan(T, [{'(', P}, {'_', P} | post_process(S, '_')], + {R, C + 2}, {in_code, E}); +scan(" " ++ T, S, {R, C}, {_, E}) -> + scan(T, S, {R, C + 1}, {in_code, E}); +scan([H | T], S, {R, C} = P, {in_code, E}) + when H >= $a andalso H =< $z orelse + H >= $A andalso H =< $Z orelse H == $_ -> + scan(T, + [{identifier, P, [H]} | post_process(S, identifier)], + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + {in_identifier, E}); +scan([H | T], S, {R, C} = P, {in_code, E}) + when H >= $0 andalso H =< $9 orelse H == $- -> + scan(T, + [{number_literal, P, [H]} | post_process(S, + number_literal)], + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + {in_number, E}); +scan([H | T], S, {R, C} = P, {in_code, E} = St) -> + {error, + {R, erlydtl_scanner, + lists:concat(["Illegal character in column ", C])}, + #scanner_state{template = [H | T], scanned = S, pos = P, + state = St}}; +scan([H | T], S, {R, C} = P, {in_number, E} = St) + when H >= $0 andalso H =< $9 -> + scan(T, + case S of + [{number_literal, _, L} = M | Ss] -> + [setelement(3, M, [H | L]) | Ss]; + _ -> + [{number_literal, P, [H]} | post_process(S, + number_literal)] + end, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + St); +scan([H | T], S, {R, C} = P, {in_number, E} = St) -> + {error, + {R, erlydtl_scanner, + lists:concat(["Illegal character in column ", C])}, + #scanner_state{template = [H | T], scanned = S, pos = P, + state = St}}; +scan([H | T], S, {R, C} = P, {in_identifier, E}) + when H >= $a andalso H =< $z orelse + H >= $A andalso H =< $Z orelse + H >= $0 andalso H =< $9 orelse H == $_ -> + scan(T, + case S of + [{identifier, _, L} = M | Ss] -> + [setelement(3, M, [H | L]) | Ss]; + _ -> + [{identifier, P, [H]} | post_process(S, identifier)] + end, + case H of + $\n -> {R + 1, 1}; + _ -> {R, C + 1} + end, + {in_identifier, E}); +scan([], S, {R, C} = P, in_text = St) -> + {ok, lists:reverse(post_process(S, eof))}; +scan([], S, {R, C} = P, {in_comment, E} = St) -> + {error, "Reached end of file inside a comment."}; +scan([], S, {R, C} = P, {_, E} = St) -> + {error, "Reached end of file inside a code block."}. + +post_process(_, {string, _, L} = T, _) -> + setelement(3, T, begin L1 = lists:reverse(L), L1 end); +post_process(_, {string_literal, _, L} = T, _) -> + setelement(3, T, begin L1 = lists:reverse(L), L1 end); +post_process(_, {number_literal, _, L} = T, _) -> + setelement(3, T, begin L1 = lists:reverse(L), L1 end); +post_process(_, {open_var, _, L} = T, _) -> + setelement(3, T, begin L1 = to_atom(L), L1 end); +post_process(_, {close_var, _, L} = T, _) -> + setelement(3, T, begin L1 = to_atom(L), L1 end); +post_process(_, {open_tag, _, L} = T, _) -> + setelement(3, T, begin L1 = to_atom(L), L1 end); +post_process(_, {close_tag, _, L} = T, _) -> + setelement(3, T, begin L1 = to_atom(L), L1 end); +post_process([{open_tag, _, _} | _], + {identifier, _, L} = T, close_tag) -> + is_keyword(all, T); +post_process([{open_tag, _, _} | _], + {identifier, _, L} = T, _) -> + is_keyword(open_tag, T); +post_process(_, {identifier, _, L} = T, close_tag) -> + is_keyword(close_tag, T); +post_process(_, {identifier, _, L} = T, _) -> + is_keyword(any, T); +post_process(_, T, _) -> T. + +post_process([S | Ss], N) -> + [post_process(Ss, S, N) | Ss]; +post_process(T, N) -> post_process(undefined, T, N). diff --git a/src/erlydtl_scanner.slex b/src/erlydtl_scanner.slex new file mode 100644 index 0000000..5043470 --- /dev/null +++ b/src/erlydtl_scanner.slex @@ -0,0 +1,363 @@ +%%%------------------------------------------------------------------- +%%% File: erlydtl_scanner.slex +%%% @author Andreas Stenius +%%% @copyright 2013 Andreas Stenius +%%% @doc +%%% erlydtl scanner +%%% @end +%%% +%%% The MIT License +%%% +%%% Copyright (c) 2013 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. +%%% +%%% @since 2013-11-05 by Andreas Stenius +%%% +%%% Rules based on the original erlydtl_scanner by Robert Saccon and Evan Miller. +%%%------------------------------------------------------------------- + +-module erlydtl_scanner. +-function scan. +-init_state in_text. +form -compile(nowarn_unused_vars) end. +form -export([resume/1]) end. +form \ +-record(scanner_state, { \ + template=[], \ + scanned=[], \ + pos={1,1}, \ + state=in_text \ + }) \ +end. + +form \ +resume(#scanner_state{ template=Template, scanned=Scanned, \ + pos=Pos, state=State }) -> \ + scan(Template, Scanned, Pos, State) \ +end. + +%% Rule syntax: Prio Prefix|any|- InState[-]|any[+|-] [, Guard] : {: Body}|{[Action...,] NewState [until Closer]}. +%% `state-' means a state without a closer state. +%% Where Guard and Body are one erlang expression (see it as a begin ... end block): expr end + +%% Open tags +10 {{ in_text-: open_var, in_code until }}. +10 {% in_text-: open_tag, in_code until %}. +10 . +10 . + +%% Comments +20 {# in_text-: in_comment until #}. +20 . + +%% `any+' will match the closer with the prefix.. +30 #}--> any+: skip, in_text-. +30 #} any+: skip, in_text-. + +%% must come before the `space any' rule +40 any in_comment: skip. +%% end comment rules + +%% The rest is "just" text.. +50 any in_text-: +string. + +%% Quoted strings +60 \" in_code: string_literal, in_double_quote. +62 \" in_double_quote: +string_literal, in_code. +64 \\ in_double_quote: +string_literal, in_double_quote_escape. +66 any in_double_quote: +string_literal. +68 any in_double_quote_escape: +string_literal, in_double_quote. + +60 \' in_code: string_literal-\", in_single_quote. +62 \' in_single_quote: +string_literal-\", in_code. +64 \\ in_single_quote: +string_literal, in_single_quote_escape. +66 any in_single_quote: +string_literal. +68 any in_single_quote_escape: +string_literal, in_single_quote. + +%% Close tags +70 }}--> any+: close_var, in_text-. +70 %}--> any+: close_tag, in_text-. +72 }} any+: close_var, in_text-. +72 %} any+:: + expr \ + case S of \ + [{identifier,_,"mitabrev"}, {open_tag,_,'{%'}|Ss] -> \ + scan(T, [{string, {R, C + 2}, ""} | Ss], \ + {R, C + 2}, {in_verbatim, undefined}); \ + [{identifier,_,Tag}, {identifier,_,verbatim}, {open_tag,_,'{%'}|Ss] -> \ + scan(T, [{string, {R, C + 2}, ""} | Ss], \ + {R, C + 2}, {in_verbatim, Tag}); \ + _ -> scan(T, [{close_tag, P, "%}"} | post_process(S, close_tag)], \ + {R, C + 2}, in_text) \ + end \ + end. + +%% verbatim stuff +80 {% in_verbatim:: + expr scan(T, S, {R, C + 2}, {in_verbatim_code, {E, "%{"}}) end. +82 \s in_verbatim_code:: + expr \ + {Tag, Backtrack} = E, \ + scan(T, S, {R, C + 1}, {in_verbatim_code, {Tag, [$\ |Backtrack]}}) \ + end. +84 'endverbatim%}' in_verbatim_code, + expr element(1, E) =:= undefined end:: + expr scan(T, S, {R, C + 13}, in_text) end. +86 'endverbatim ' in_verbatim_code:: + expr \ + {Tag, Backtrack} = E, \ + scan(T, S, {R, C + 12}, \ + {in_endverbatim_code, \ + {Tag, lists:reverse("endverbatim ", Backtrack), ""}}) \ + end. +88 \s in_endverbatim_code, + expr element(3, E) =:= "" end:: + expr \ + {Tag, Backtrack, EndTag} = E, \ + scan(T, S, {R, C + 1}, \ + {in_endverbatim_code, \ + {Tag, [$\ |Backtrack], EndTag}}) \ + end. +90 any in_endverbatim_code, + expr \ + H >= $a andalso H =< $z orelse \ + H >= $0 andalso H =< $9 orelse H =:= $_ end:: + expr \ + {Tag, Backtrack, EndTag} = E, \ + scan(T, S, {R, C + 1}, \ + {in_endverbatim_code, \ + {Tag, [H|Backtrack], [H|EndTag]}}) \ + end. +92 \s in_endverbatim_code, + expr element(1, E) =:= element(3, E) end:: + expr \ + {Tag, Backtrack, Tag} = E, \ + scan(T, S, {R, C + 1}, \ + {in_endverbatim_code, \ + {Tag, [$\ |Backtrack], Tag}}) \ + end. +94 %} in_endverbatim_code, + expr element(1, E) =:= element(3, E) end:: + expr scan(T, S, {R, C + 2}, (in_text)) end. +96 %} in_endverbatim_code, + expr element(1, E) =:= undefined andalso \ + element(3, E) =:= "" end:: + expr scan(T, S, {R, C + 2}, in_text) end. +98 any in_endverbatim_code:: + expr \ + {Tag, Backtrack, _} = E, \ + scan(T, \ + case S of \ + [{string,_, L}=M|Ss] -> \ + [setelement(3, M, [H|Backtrack] ++ L)|Ss]; \ + _ -> [{string, P, [H|Backtrack]}|S] \ + end, \ + case H of $\n -> {R + 1, 1}; _ -> {R, C + 1} end, \ + {in_verbatim, Tag}) \ + end. +100 any in_verbatim_code:: + expr \ + {Tag, Backtrack} = E, \ + scan(T, \ + case S of \ + [{string,_, L}=M|Ss] -> \ + [setelement(3, M, [H|Backtrack] ++ L)|Ss]; \ + _ -> [{string, P, [H|Backtrack]}|S] \ + end, \ + case H of $\n -> {R + 1, 1}; _ -> {R, C + 1} end, \ + {in_verbatim, Tag}) \ + end. +102 any in_verbatim:: + expr \ + scan(T, \ + case S of \ + [{string,_, L}=M|Ss] -> \ + [setelement(3, M, [H|L])|Ss]; \ + _ -> [{string, P, [H]}|S] \ + end, \ + case H of $\n -> {R + 1, 1}; _ -> {R, C + 1} end, \ + {in_verbatim, E}) \ + end. + + +%% Get back to `in_code' on these tokens: +110 == any: ==, in_code. +110 != any: !=, in_code. +110 >= any: >=, in_code. +110 <= any: <=, in_code. +110 > any: >, in_code. +110 < any: <, in_code. +110 ( any: (, in_code. +110 ) any: ), in_code. +110 \, any: \,, in_code. +110 | any: |, in_code. +110 = any: =, in_code. +110 \: any: \:, in_code. +110 \. any: \., in_code. +110 \_( any: \_ \(, in_code. + +%% Eat space (and get back to `in_code') +%% note that `any' here will match states *with* a closer, i.e. not `in_text'. +%% (`any-' would match any stateless state.) +110 \s any: skip, in_code. + +120 any in_code, + expr \ + (H >= $a andalso H =< $z) orelse \ + (H >= $A andalso H =< $Z) orelse \ + H == $_ \ + end: identifier, in_identifier. + +122 any in_code, + expr \ + (H >= $0 andalso H =< $9) orelse H == $- \ + end: number_literal, in_number. + +124 any in_code:: + expr \ + {error, {R, erlydtl_scanner, \ + lists:concat(["Illegal character in column ", C])}, \ + #scanner_state{ template=[H|T], scanned=S, pos=P, state=St } \ + } \ + end. + +130 any in_number, expr H >= $0 andalso H =< $9 end: +number_literal. +132 any in_number:: + expr \ + {error, {R, erlydtl_scanner, \ + lists:concat(["Illegal character in column ", C])}, \ + #scanner_state{ template=[H|T], scanned=S, pos=P, state=St } \ + } \ + end. + +140 any in_identifier, + expr \ + (H >= $a andalso H =< $z) orelse \ + (H >= $A andalso H =< $Z) orelse \ + (H >= $0 andalso H =< $9) orelse \ + H == $_ \ + end: +identifier, in_identifier. + +200 : in_text- :: + expr \ + {ok, lists:reverse(post_process(S,eof))} \ + end. + +202 : in_comment :: expr {error, "Reached end of file inside a comment."} end. +204 : any :: expr {error, "Reached end of file inside a code block."} end. + + +%% Process tokens as we parse them + +string: lists reverse. +string_literal: lists reverse. +number_literal: lists reverse. +open_var: to_atom. +close_var: to_atom. +open_tag: to_atom. +close_tag: to_atom. + +open_tag identifier, close_tag:: expr is_keyword(all, T) end. +open_tag identifier:: expr is_keyword(open_tag, T) end. +identifier, close_tag:: expr is_keyword(close_tag, T) end. +identifier:: expr is_keyword(any, T) end. + + +%% Utility functions + +form to_atom(L) when is_list(L) -> list_to_atom(L) end. +form to_keyword(L, P) -> {to_atom(L ++ "_keyword"), P, L} end. +form atomize(L, T) -> setelement(3, T, to_atom(L)) end. + +form \ + is_keyword(Class, {_, _, L} = T) -> \ + L1 = lists:reverse(L), \ + case is_keyword(Class, L1) of \ + true -> to_keyword(L1, element(2, T)); \ + false -> atomize(L1, T) \ + end; \ + is_keyword([C|Cs], L) -> \ + is_keyword(C, L) orelse \ + is_keyword(Cs, L); \ + is_keyword(all, L) -> is_keyword([any, open, close], L); \ + is_keyword(open_tag, L) -> is_keyword([any, open], L); \ + is_keyword(close_tag, L) -> is_keyword([any, close], L); \ + \ + is_keyword(any, "in") -> true; \ + is_keyword(any, "not") -> true; \ + is_keyword(any, "or") -> true; \ + is_keyword(any, "and") -> true; \ + is_keyword(any, "as") -> true; \ + is_keyword(any, "by") -> true; \ + is_keyword(any, "with") -> true; \ + \ + is_keyword(close, "only") -> true; \ + is_keyword(close, "parsed") -> true; \ + is_keyword(close, "noop") -> true; \ + is_keyword(close, "reversed") -> true; \ + is_keyword(close, "openblock") -> true; \ + is_keyword(close, "closeblock") -> true; \ + is_keyword(close, "openvariable") -> true; \ + is_keyword(close, "closevariable") -> true; \ + is_keyword(close, "openbrace") -> true; \ + is_keyword(close, "closebrace") -> true; \ + is_keyword(close, "opencomment") -> true; \ + is_keyword(close, "closecomment") -> true; \ + \ + is_keyword(open, "autoescape") -> true; \ + is_keyword(open, "endautoescape") -> true; \ + is_keyword(open, "block") -> true; \ + is_keyword(open, "endblock") -> true; \ + is_keyword(open, "comment") -> true; \ + is_keyword(open, "endcomment") -> true; \ + is_keyword(open, "cycle") -> true; \ + is_keyword(open, "extends") -> true; \ + is_keyword(open, "filter") -> true; \ + is_keyword(open, "endfilter") -> true; \ + is_keyword(open, "firstof") -> true; \ + is_keyword(open, "for") -> true; \ + is_keyword(open, "empty") -> true; \ + is_keyword(open, "endfor") -> true; \ + is_keyword(open, "if") -> true; \ + is_keyword(open, "elif") -> true; \ + is_keyword(open, "else") -> true; \ + is_keyword(open, "endif") -> true; \ + is_keyword(open, "ifchanged") -> true; \ + is_keyword(open, "endifchanged") -> true; \ + is_keyword(open, "ifequal") -> true; \ + is_keyword(open, "endifequal") -> true; \ + is_keyword(open, "ifnotequal") -> true; \ + is_keyword(open, "endifnotequal") -> true; \ + is_keyword(open, "include") -> true; \ + is_keyword(open, "now") -> true; \ + is_keyword(open, "regroup") -> true; \ + is_keyword(open, "endregroup") -> true; \ + is_keyword(open, "spaceless") -> true; \ + is_keyword(open, "endspaceless") -> true; \ + is_keyword(open, "ssi") -> true; \ + is_keyword(open, "templatetag") -> true; \ + is_keyword(open, "widthratio") -> true; \ + is_keyword(open, "call") -> true; \ + is_keyword(open, "endwith") -> true; \ + is_keyword(open, "trans") -> true; \ + is_keyword(open, "blocktrans") -> true; \ + is_keyword(open, "endblocktrans") -> true; \ + is_keyword(_, _) -> false \ +end. From 131d45b69ea272fb0281731248cb88c14ecf7ce9 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Wed, 27 Nov 2013 16:14:15 +0100 Subject: [PATCH 054/361] Fix erlydtl_scanner.slex due to syntax updates. --- src/erlydtl_scanner.slex | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/erlydtl_scanner.slex b/src/erlydtl_scanner.slex index 5043470..3f8fa22 100644 --- a/src/erlydtl_scanner.slex +++ b/src/erlydtl_scanner.slex @@ -95,7 +95,7 @@ end. 70 }}--> any+: close_var, in_text-. 70 %}--> any+: close_tag, in_text-. 72 }} any+: close_var, in_text-. -72 %} any+:: +72 %} any+: expr \ case S of \ [{identifier,_,"mitabrev"}, {open_tag,_,'{%'}|Ss] -> \ @@ -110,17 +110,17 @@ end. end. %% verbatim stuff -80 {% in_verbatim:: +80 {% in_verbatim: expr scan(T, S, {R, C + 2}, {in_verbatim_code, {E, "%{"}}) end. -82 \s in_verbatim_code:: +82 \s in_verbatim_code: expr \ {Tag, Backtrack} = E, \ scan(T, S, {R, C + 1}, {in_verbatim_code, {Tag, [$\ |Backtrack]}}) \ end. 84 'endverbatim%}' in_verbatim_code, - expr element(1, E) =:= undefined end:: + expr element(1, E) =:= undefined end: expr scan(T, S, {R, C + 13}, in_text) end. -86 'endverbatim ' in_verbatim_code:: +86 'endverbatim ' in_verbatim_code: expr \ {Tag, Backtrack} = E, \ scan(T, S, {R, C + 12}, \ @@ -128,7 +128,7 @@ end. {Tag, lists:reverse("endverbatim ", Backtrack), ""}}) \ end. 88 \s in_endverbatim_code, - expr element(3, E) =:= "" end:: + expr element(3, E) =:= "" end: expr \ {Tag, Backtrack, EndTag} = E, \ scan(T, S, {R, C + 1}, \ @@ -138,7 +138,7 @@ end. 90 any in_endverbatim_code, expr \ H >= $a andalso H =< $z orelse \ - H >= $0 andalso H =< $9 orelse H =:= $_ end:: + H >= $0 andalso H =< $9 orelse H =:= $_ end: expr \ {Tag, Backtrack, EndTag} = E, \ scan(T, S, {R, C + 1}, \ @@ -146,7 +146,7 @@ end. {Tag, [H|Backtrack], [H|EndTag]}}) \ end. 92 \s in_endverbatim_code, - expr element(1, E) =:= element(3, E) end:: + expr element(1, E) =:= element(3, E) end: expr \ {Tag, Backtrack, Tag} = E, \ scan(T, S, {R, C + 1}, \ @@ -154,13 +154,13 @@ end. {Tag, [$\ |Backtrack], Tag}}) \ end. 94 %} in_endverbatim_code, - expr element(1, E) =:= element(3, E) end:: + expr element(1, E) =:= element(3, E) end: expr scan(T, S, {R, C + 2}, (in_text)) end. 96 %} in_endverbatim_code, expr element(1, E) =:= undefined andalso \ - element(3, E) =:= "" end:: + element(3, E) =:= "" end: expr scan(T, S, {R, C + 2}, in_text) end. -98 any in_endverbatim_code:: +98 any in_endverbatim_code: expr \ {Tag, Backtrack, _} = E, \ scan(T, \ @@ -172,7 +172,7 @@ end. case H of $\n -> {R + 1, 1}; _ -> {R, C + 1} end, \ {in_verbatim, Tag}) \ end. -100 any in_verbatim_code:: +100 any in_verbatim_code: expr \ {Tag, Backtrack} = E, \ scan(T, \ @@ -184,7 +184,7 @@ end. case H of $\n -> {R + 1, 1}; _ -> {R, C + 1} end, \ {in_verbatim, Tag}) \ end. -102 any in_verbatim:: +102 any in_verbatim: expr \ scan(T, \ case S of \ @@ -230,7 +230,7 @@ end. (H >= $0 andalso H =< $9) orelse H == $- \ end: number_literal, in_number. -124 any in_code:: +124 any in_code: expr \ {error, {R, erlydtl_scanner, \ lists:concat(["Illegal character in column ", C])}, \ @@ -239,7 +239,7 @@ end. end. 130 any in_number, expr H >= $0 andalso H =< $9 end: +number_literal. -132 any in_number:: +132 any in_number: expr \ {error, {R, erlydtl_scanner, \ lists:concat(["Illegal character in column ", C])}, \ @@ -255,13 +255,13 @@ end. H == $_ \ end: +identifier, in_identifier. -200 : in_text- :: +200 : in_text- : expr \ {ok, lists:reverse(post_process(S,eof))} \ end. -202 : in_comment :: expr {error, "Reached end of file inside a comment."} end. -204 : any :: expr {error, "Reached end of file inside a code block."} end. +202 : in_comment : expr {error, "Reached end of file inside a comment."} end. +204 : any : expr {error, "Reached end of file inside a code block."} end. %% Process tokens as we parse them @@ -274,10 +274,10 @@ close_var: to_atom. open_tag: to_atom. close_tag: to_atom. -open_tag identifier, close_tag:: expr is_keyword(all, T) end. -open_tag identifier:: expr is_keyword(open_tag, T) end. -identifier, close_tag:: expr is_keyword(close_tag, T) end. -identifier:: expr is_keyword(any, T) end. +open_tag identifier, close_tag: expr is_keyword(all, T) end. +open_tag identifier: expr is_keyword(open_tag, T) end. +identifier, close_tag: expr is_keyword(close_tag, T) end. +identifier: expr is_keyword(any, T) end. %% Utility functions From 62e4b98a38c07dc46c73ef596680e5291f359058 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 28 Nov 2013 13:36:09 +0100 Subject: [PATCH 055/361] Regenerate erlydtl_scanner.erl to get the new generated comments. --- src/erlydtl_scanner.erl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/erlydtl_scanner.erl b/src/erlydtl_scanner.erl index 231ff4a..cbc0c51 100644 --- a/src/erlydtl_scanner.erl +++ b/src/erlydtl_scanner.erl @@ -1,3 +1,5 @@ +%%%%% THIS IS A SLEX GENERATED FILE %%%%% + %%%------------------------------------------------------------------- %%% File: erlydtl_scanner.slex %%% @author Andreas Stenius @@ -34,6 +36,10 @@ %%%------------------------------------------------------------------- -module(erlydtl_scanner). +%% This file was generated 2013-11-28 12:34:50 UTC by slex 0.1.0-1-gfa2a50d. +%% http://github.com/erlydtl/slex +-slex_source("src/erlydtl_scanner.slex"). + -export([scan/1, scan/4]). -compile(nowarn_unused_vars). From 98f89cb0cd3fd02a660caad317338721cb1ae114 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 28 Nov 2013 15:45:44 +0100 Subject: [PATCH 056/361] Add failing test for issue #31. --- tests/input/extends2 | 4 ++++ tests/src/erlydtl_functional_tests.erl | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/input/extends2 diff --git a/tests/input/extends2 b/tests/input/extends2 new file mode 100644 index 0000000..43af542 --- /dev/null +++ b/tests/input/extends2 @@ -0,0 +1,4 @@ + +{% extends "base" %} +{% block title %}replacing the base title{% endblock %} +{% block content %}replacing the base content - variable: {{ test_var }} after variable {% endblock %} diff --git a/tests/src/erlydtl_functional_tests.erl b/tests/src/erlydtl_functional_tests.erl index fcd7a48..6eb86fd 100644 --- a/tests/src/erlydtl_functional_tests.erl +++ b/tests/src/erlydtl_functional_tests.erl @@ -48,7 +48,7 @@ test_list() -> "custom_tag", "custom_tag1", "custom_tag2", "custom_tag3", "custom_call", "include_template", "include_path", "ssi", - "extends_path", "extends_path2", "trans" ]. + "extends_path", "extends_path2", "trans", "extends2" ]. setup_compile("for_list_preset") -> CompileVars = [{fruit_list, [["apple", "apples"], ["banana", "bananas"], ["coconut", "coconuts"]]}], @@ -88,6 +88,9 @@ setup("autoescape") -> setup("extends") -> RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}], {ok, RenderVars}; +setup("extends2") -> + RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}], + {ok, RenderVars}; setup("filters") -> RenderVars = [ {date_var1, {1975,7,24}}, From e976f07dcbedeb7ce947a5b397ff17f7b913e922 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Thu, 28 Nov 2013 22:15:21 +0100 Subject: [PATCH 057/361] Give proper error message on badly placed extends tag. Fixes #31. Related to #85, #60. The extends tag really should be the very first thing in the template. --- src/erlydtl_compiler.erl | 2 ++ tests/src/erlydtl_functional_tests.erl | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 2a7075c..5f38295 100644 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -817,6 +817,8 @@ body_ast(DjangoParseTree, Context, TreeWalker) -> with_ast(Args, Contents, Context, TreeWalkerAcc); ({'extension', Tag}, TreeWalkerAcc) -> extension_ast(Tag, Context, TreeWalkerAcc); + ({'extends', _}, TreeWalkerAcc) -> + throw({error, "The extends tag must be at the very top of the template"}); (ValueToken, TreeWalkerAcc) -> {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, true, Context, TreeWalkerAcc), {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker} diff --git a/tests/src/erlydtl_functional_tests.erl b/tests/src/erlydtl_functional_tests.erl index 6eb86fd..f65b710 100644 --- a/tests/src/erlydtl_functional_tests.erl +++ b/tests/src/erlydtl_functional_tests.erl @@ -74,6 +74,8 @@ setup_compile("ifnotequal_preset") -> setup_compile("var_preset") -> CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}], {ok, CompileVars}; +setup_compile("extends2") -> + {error, []}; setup_compile(_) -> {ok, []}. @@ -88,9 +90,6 @@ setup("autoescape") -> setup("extends") -> RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}], {ok, RenderVars}; -setup("extends2") -> - RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}], - {ok, RenderVars}; setup("filters") -> RenderVars = [ {date_var1, {1975,7,24}}, From 2dd42a28310909abb612ef6c83f66602fbbc7b76 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 29 Nov 2013 06:35:06 +0100 Subject: [PATCH 058/361] Add unit test for missing dict lookup. (#4) --- tests/src/erlydtl_unittests.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 117449a..4eaa1c6 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -67,6 +67,8 @@ tests() -> <<"I also enjoy {{ var1.game }}">>, [{var1, [{<<"game">>, "Parcheesi"}]}], <<"I also enjoy Parcheesi">>}, {"Render variable in dict", <<"{{ var1 }}">>, dict:store(var1, "bar", dict:new()), <<"bar">>}, + {"Render variable with missing attribute in dict", + <<"{{ var1.foo }}">>, [{var1, dict:store(bar, "Othello", dict:new())}], <<"">>}, {"Render variable in gb_tree", <<"{{ var1 }}">>, gb_trees:insert(var1, "bar", gb_trees:empty()), <<"bar">>}, {"Render variable in arity-1 func", From 3ebe859180893511fd6a51a147b62bee4aae13b7 Mon Sep 17 00:00:00 2001 From: Garrett Smith Date: Mon, 29 Apr 2013 10:38:08 -0500 Subject: [PATCH 059/361] Support for empty values in yesno filter The previous implementation was over specified and didn't support empty strings as yesno options. This implementation uses binary:split/3, which is as efficient as string:tokens/2 but returns empty tokens. See https://github.com/gar1t/erlang-bench/blob/master/comma-parse.escript for details on parsing implementations. This commit removes the odd conversion of Bool to a list, which was never used in yesno_io. This change returns a *binary* value, which may be contrary to the spirit of the template implementation. If this is the case, the the result can be easily converted to a list (a seeming waste of cycles, but perhaps there's a good reason for it). --- src/erlydtl_filters.erl | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index d807d64..3e5f71f 100644 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -867,12 +867,10 @@ wordwrap(Input, Number) when is_list(Input) -> wordwrap(Input, [], [], 0, Number). %% @doc Given a string mapping values for true, false and (optionally) undefined, returns one of those strings according to the value. -yesno(Bool, Choices) when is_binary(Bool) -> - yesno_io(binary_to_list(Bool), Choices); yesno(Bool, Choices) when is_binary(Choices) -> - yesno_io(Bool, binary_to_list(Choices)); + yesno_io(Bool, Choices); yesno(Bool, Choices) when is_list(Choices) -> - yesno_io(Bool, Choices). + yesno_io(Bool, list_to_binary(Choices)). % internal @@ -1206,19 +1204,12 @@ process_binary_match(Pre, Insertion, SizePost, Post) -> end. yesno_io(Bool, Choices) -> - %% io:format("Bool, Choices: ~p, ~p ~n",[Bool, Choices]), - Terms = string:tokens(Choices, ","), - case Bool of - true -> - lists:nth(1, Terms); - false -> - lists:nth(2, Terms); - undefined when erlang:length(Terms) == 2 -> % (converts undefined to false if no mapping for undefined is given) - lists:nth(2, Terms); - undefined when erlang:length(Terms) == 3 -> - lists:nth(3, Terms); - _ -> - error + case {Bool, binary:split(Choices, <<",">>, [global])} of + {true, [T|_]} -> T; + {false, [_,F|_]} -> F; + {undefined, [_,_,U|_]} -> U; + {undefined, [_,F|_]} -> F; + _ -> error end. %% unjoin == split in other languages; inverse of join From a46a2d2ed5371e412f9c6c25556e42886de910de Mon Sep 17 00:00:00 2001 From: Garrett Smith Date: Mon, 29 Apr 2013 16:30:11 -0500 Subject: [PATCH 060/361] Use yesno filter with lists and binaries This comes by default with Python as empty lists evaluate to false. It's explicitly implemented as a term_to_bool function in filters. --- src/erlydtl_filters.erl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index 3e5f71f..7a7afc9 100644 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -1203,8 +1203,8 @@ process_binary_match(Pre, Insertion, SizePost, Post) -> _ -> [Pre, Insertion, Post] end. -yesno_io(Bool, Choices) -> - case {Bool, binary:split(Choices, <<",">>, [global])} of +yesno_io(Val, Choices) -> + case {term_to_bool(Val), binary:split(Choices, <<",">>, [global])} of {true, [T|_]} -> T; {false, [_,F|_]} -> F; {undefined, [_,_,U|_]} -> U; @@ -1212,6 +1212,13 @@ yesno_io(Bool, Choices) -> _ -> error end. +term_to_bool(true) -> true; +term_to_bool(false) -> false; +term_to_bool(undefined) -> undefined; +term_to_bool(Str) when is_list(Str); is_binary(Str) -> + iolist_size(Str) > 0; +term_to_bool(_) -> true. + %% unjoin == split in other languages; inverse of join %%FROM: http://www.erlang.org/pipermail/erlang-questions/2008-October/038896.html unjoin(String, []) -> From fa670cad59d67fe2ad693ff867a99f91151cb81a Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 29 Nov 2013 08:58:41 +0100 Subject: [PATCH 061/361] Add more unit tests for filter yesno. Also refactored yesno_io/2 a bit. Fixes #72. --- src/erlydtl_filters.erl | 27 ++++++++++++++------------- tests/src/erlydtl_unittests.erl | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index 7a7afc9..68511f6 100644 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -1204,21 +1204,22 @@ process_binary_match(Pre, Insertion, SizePost, Post) -> end. yesno_io(Val, Choices) -> - case {term_to_bool(Val), binary:split(Choices, <<",">>, [global])} of - {true, [T|_]} -> T; - {false, [_,F|_]} -> F; - {undefined, [_,_,U|_]} -> U; - {undefined, [_,F|_]} -> F; - _ -> error + {True, False, Undefined} = + case binary:split(Choices, <<",">>, [global]) of + [T, F, U] -> {T, F, U}; + [T, F] -> {T, F, F}; + _ -> throw({error, "invalid choices to yesno filter"}) + end, + if Val =:= false -> False; + Val =:= undefined -> Undefined; + is_list(Val); is_binary(Val) -> + case iolist_size(Val) of + 0 -> False; + _ -> True + end; + true -> True end. -term_to_bool(true) -> true; -term_to_bool(false) -> false; -term_to_bool(undefined) -> undefined; -term_to_bool(Str) when is_list(Str); is_binary(Str) -> - iolist_size(Str) > 0; -term_to_bool(_) -> true. - %% unjoin == split in other languages; inverse of join %%FROM: http://www.erlang.org/pipermail/erlang-questions/2008-October/038896.html unjoin(String, []) -> diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 4eaa1c6..4b3ba6d 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -918,7 +918,23 @@ tests() -> <<"no">>}, {"|yesno:\"yeah,no,maybe\"", <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, undefined}], - <<"maybe">>} + <<"maybe">>}, + + {"string |yesno:\"yeah,no,maybe\"", + <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, "non-empty string"}], + <<"yeah">>}, + {"binary |yesno:\"yeah,no,maybe\"", + <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, <<"non-empty binary">>}], + <<"yeah">>}, + {"empty string |yesno:\"yeah,no,maybe\"", + <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, ""}], + <<"no">>}, + {"empty binary |yesno:\"yeah,no\"", + <<"{{ var|yesno:\"yeah,no\" }}">>, [{var, <<"">>}], + <<"no">>}, + {"term |yesno:\"yeah,no,maybe\"", + <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, {my, [term, "test"]}}], + <<"yeah">>} ]}, {"filters_if", [ {"Filter if 1.1", From 97724ffa4650a3d16098dbfeeb6750ae90afe73a Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 29 Nov 2013 10:16:34 +0100 Subject: [PATCH 062/361] Add support for expected failures in unittests. Test failure for yesno filter on bad arguments. --- src/erlydtl_filters.erl | 2 +- tests/src/erlydtl_unittests.erl | 151 +++++++++++++++++++++----------- 2 files changed, 99 insertions(+), 54 deletions(-) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index 68511f6..9adade9 100644 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -1208,7 +1208,7 @@ yesno_io(Val, Choices) -> case binary:split(Choices, <<",">>, [global]) of [T, F, U] -> {T, F, U}; [T, F] -> {T, F, F}; - _ -> throw({error, "invalid choices to yesno filter"}) + _ -> throw({yesno, choices}) end, if Val =:= false -> False; Val =:= undefined -> Undefined; diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 4b3ba6d..3a10b45 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -930,11 +930,20 @@ tests() -> <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, ""}], <<"no">>}, {"empty binary |yesno:\"yeah,no\"", - <<"{{ var|yesno:\"yeah,no\" }}">>, [{var, <<"">>}], + <<"{{ var|yesno:\",no\" }}">>, [{var, <<"">>}], <<"no">>}, - {"term |yesno:\"yeah,no,maybe\"", + {"term |yesno:\"yeah,,maybe\"", <<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, {my, [term, "test"]}}], - <<"yeah">>} + <<"yeah">>}, + {"|yesno:\"yeah,\"", + <<"{{ var|yesno:\"yeah,\" }}">>, [{var, false}], + <<"">>}, + {"|yesno:\"yeah,,maybe\"", + <<"{{ var|yesno:\"yeah,,maybe\" }}">>, [{var, false}], + <<"">>}, + {"|yesno:\"missing_false_choice\"", + <<"{{ var|yesno:\"missing_false_choice\" }}">>, [{var, true}], + {error, {yesno, choices}}} ]}, {"filters_if", [ {"Filter if 1.1", @@ -1132,60 +1141,96 @@ tests() -> ]. run_tests() -> - io:format("Running unit tests...~n"), - DefaultOptions = [{custom_filters_modules, [erlydtl_contrib_humanize]}], + io:format("Running unit tests..."), Failures = lists:foldl( - fun({Group, Assertions}, GroupAcc) -> - io:format(" Test group ~p...~n", [Group]), - lists:foldl(fun - ({Name, DTL, Vars, Output}, Acc) -> - try - process_unit_test( - erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), - Vars, [], Output, Acc, Group, Name) - catch Class:Error -> format_error(Group, Name, Class, Error, Acc) end; - ({Name, DTL, Vars, RenderOpts, Output}, Acc) -> - try - process_unit_test( - erlydtl:compile(DTL, erlydtl_running_test, DefaultOptions), - Vars, RenderOpts, Output, Acc, Group, Name) - catch Class:Error -> format_error(Group, Name, Class, Error, Acc) end; - ({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}, Acc) -> - try - process_unit_test( - erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts ++ DefaultOptions), - Vars, RenderOpts, Output, Acc, Group, Name) - catch Class:Error -> format_error(Group, Name, Class, Error, Acc) end - end, GroupAcc, Assertions) - end, [], tests()), + fun({Group, Assertions}, GroupAcc) -> + io:format("~n Test group ~p ", [Group]), + Failed = + lists:foldl( + fun(Setup, Acc) -> + try process_unit_test(Setup, Acc) of + Acc -> + io:format("."), + Acc; + AccOut -> + io:format("!"), + AccOut + catch + Class:Error -> + format_error(element(1, Setup), + Class, Error, Acc) + end + end, [], Assertions), + if length(Failed) =:= 0 -> GroupAcc; + true -> [{Group, Failed}|GroupAcc] + end + end, [], tests()), + + case length(Failures) of + 0 -> io:format("~nAll unit tests PASS~n"); + Length -> + io:format("~n### FAILED groups: ~b ####~n", [Length]), + [begin + io:format(" Group: ~s (~b failures)~n", [Group, length(Failed)]), + [io:format(" Test: ~s~n~s~n", [Name, Error]) + || {Name, Error} <- lists:reverse(Failed)] + end || {Group, Failed} <- lists:reverse(Failures)] + end. - io:format("Unit test failures: ~b~n", [length(Failures)]), - [io:format(" ~s:~s ~s~n", [Group, Name, Error]) || {Group, Name, Error} <- lists:reverse(Failures)]. +format_error(Name, Class, Error, Acc) -> + io:format("!"), + [{Name, io_lib:format("~s:~p~n ~p", [Class, Error, erlang:get_stacktrace()])}|Acc]. -format_error(Group, Name, Class, Error, Acc) -> - [{Group, Name, io_lib:format("~n ~s:~s~n ~p", [Class, Error, erlang:get_stacktrace()])}|Acc]. +compile_test(DTL, Opts) -> + Options = [{custom_filters_modules, [erlydtl_contrib_humanize]}|Opts], + erlydtl:compile(DTL, erlydtl_running_test, Options). -process_unit_test(CompiledTemplate, Vars, RenderOpts, Output,Acc, Group, Name) -> - case CompiledTemplate of - {ok, _} -> - {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; - {Output, Unexpected} -> - [{Group, Name, io_lib:format("Unexpected result with binary variables: ~nExpected: ~p~nActual: ~p", - [Output, Unexpected])} | Acc]; - {Unexpected, Output} -> - [{Group, Name, io_lib:format("Unexpected result with list variables: ~nExpected: ~p~nActual: ~p", - [Output, Unexpected])} | Acc]; - {Unexpected1, Unexpected2} -> - [{Group, Name, io_lib:format("Unexpected result: ~nExpected: ~p~nActual (list): ~p~nActual (binary): ~p", - [Output, Unexpected1, Unexpected2])} | Acc] - end; - Output -> Acc; - Err -> - [{Group, Name, io_lib:format("Render error: ~p~n", [Err])} | Acc] +process_unit_test({Name, DTL, Vars, Output}, Acc) -> + process_unit_test({Name, DTL, Vars, [], [], Output}, Acc); +process_unit_test({Name, DTL, Vars, RenderOpts, Output}, Acc) -> + process_unit_test({Name, DTL, Vars, RenderOpts, [], Output}, Acc); +process_unit_test({Name, DTL, Vars, RenderOpts, CompilerOpts, Output}, Acc) -> + case compile_test(DTL, CompilerOpts) of + {ok, _} -> + case erlydtl_running_test:render(Vars, RenderOpts) of + {ok, IOList} -> + case erlydtl_running_test:render(vars_to_binary(Vars), RenderOpts) of + {ok, IOListBin} -> + case {iolist_to_binary(IOList), iolist_to_binary(IOListBin)} of + {Output, Output} -> + Acc; + {Output, Unexpected} -> + [{Name, io_lib:format( + "Unexpected result with binary variables: ~n" + "Expected: ~p~n" + "Actual: ~p", + [Output, Unexpected])} | Acc]; + {Unexpected, Output} -> + [{Name, io_lib:format( + "Unexpected result with list variables: ~n" + "Expected: ~p~n" + "Actual: ~p", + [Output, Unexpected])} | Acc]; + {Unexpected1, Unexpected2} -> + [{Name, io_lib:format( + "Unexpected result: ~n" + "Expected: ~p~n" + "Actual (list): ~p~n" + "Actual (binary): ~p", + [Output, Unexpected1, Unexpected2])} | Acc] + end; + Output -> Acc; + Err -> + [{Name, io_lib:format("Render error (with binary variables): ~p", + [Err])} | Acc] + end; + Output -> Acc; + Err -> + [{Name, io_lib:format("Render error (with list variables): ~p", [Err])} | Acc] + end; + Output -> Acc; + Err -> + [{Name, io_lib:format("Compile error: ~p", [Err])} | Acc] end. From ce362c2750534654e69616042fd0f6f0afd24db0 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 29 Nov 2013 10:35:40 +0100 Subject: [PATCH 063/361] force_escape should work in iolists. Fixes #70. --- src/erlydtl_filters.erl | 2 ++ tests/src/erlydtl_unittests.erl | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index 9adade9..579c4e5 100644 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -917,6 +917,8 @@ escape("\"" ++ Rest, Acc) -> escape(Rest, lists:reverse(""", Acc)); escape("'" ++ Rest, Acc) -> escape(Rest, lists:reverse("'", Acc)); +escape([S | Rest], Acc) when is_list(S); is_binary(S)-> + escape(Rest, [force_escape(S) | Acc]); escape([C | Rest], Acc) -> escape(Rest, [C | Acc]). diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 3a10b45..5150c31 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -480,6 +480,12 @@ tests() -> {"|force_escape", <<"{{ var1|force_escape }}">>, [{var1, "Ben & Jerry's <=> \"The World's Best Ice Cream\""}], <<"Ben & Jerry's <=> "The World's Best Ice Cream"">>}, + {"iolist |force_escape", + <<"{{ var1|force_escape }}">>, [{var1, ["'a'"]}], + <<"'a'">>}, + {"nested iolist |force_escape", + <<"{{ var1|force_escape }}">>, [{var1, ["a'", <<"b">>, [<<">, "d", ["e>"]]]}], + <<"a'b<cde>">>}, {"|format_integer", <<"{{ var1|format_integer }}">>, [{var1, 28}], <<"28">>}, {"|format_number 1", From 8ee31b8cdb0ce96d54d2f4f176b55c36678db4e9 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 29 Nov 2013 11:19:11 +0100 Subject: [PATCH 064/361] Give proper error message on unknown filters. Related to #41 and #85. --- src/erlydtl_compiler.erl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/erlydtl_compiler.erl b/src/erlydtl_compiler.erl index 5f38295..eb6a452 100644 --- a/src/erlydtl_compiler.erl +++ b/src/erlydtl_compiler.erl @@ -817,7 +817,7 @@ body_ast(DjangoParseTree, Context, TreeWalker) -> with_ast(Args, Contents, Context, TreeWalkerAcc); ({'extension', Tag}, TreeWalkerAcc) -> extension_ast(Tag, Context, TreeWalkerAcc); - ({'extends', _}, TreeWalkerAcc) -> + ({'extends', _}, _TreeWalkerAcc) -> throw({error, "The extends tag must be at the very top of the template"}); (ValueToken, TreeWalkerAcc) -> {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, true, Context, TreeWalkerAcc), @@ -1163,7 +1163,9 @@ filter_ast2(Name, VariableAst, [Arg], VarInfo, #dtl_context{ filter_modules = [M TreeWalker}; false -> filter_ast2(Name, VariableAst, [Arg], VarInfo, Context#dtl_context{ filter_modules = Rest }, TreeWalker) - end. + end; +filter_ast2(Name, _, Arg, _, _, _) -> + throw({error, {unknown_filter, Name, length(Arg)}}). search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) -> search_for_safe_filter(Variable, Filter); From b57284af4f10516e183a76552fb8e2bcc7157565 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 29 Nov 2013 11:48:42 +0100 Subject: [PATCH 065/361] Improve floatformat. Fixes #41, #53. --- src/erlydtl_filters.erl | 38 ++++++++++++---------- tests/src/erlydtl_unittests.erl | 57 +++++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index 579c4e5..f75a622 100644 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -66,6 +66,7 @@ filesizeformat/1, first/1, fix_ampersands/1, + floatformat/1, floatformat/2, force_escape/1, format_integer/1, @@ -328,33 +329,30 @@ fix_ampersands(Input) when is_list(Input) -> %% @doc When used without an argument, rounds a floating-point number to one decimal place %% -- 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) -> + floatformat(Number, []). + floatformat(Number, Place) -> - floatformat_io(Number, cast_to_integer(Place)). + floatformat_io(cast_to_float(Number), cast_to_integer(Place)). floatformat_io(Number, []) -> floatformat_io(Number, -1); +floatformat_io(Number, 0) -> + hd(io_lib:format("~B", [erlang:round(Number)])); floatformat_io(Number, Precision) when Precision > 0 -> - Format = lists:flatten(io_lib:format("~~.~Bf",[Precision])), - [Result] = io_lib:format(Format,[Number]), - Result; + hd(io_lib:format("~.*f",[Precision, Number])); floatformat_io(Number, Precision) when Precision < 0 -> Round = erlang:round(Number), RoundPrecision = round(Number, -Precision), - case RoundPrecision == Round of - true -> - %Format = lists:flatten(io_lib:format("~~~BB",[-Precision])), - [Result] = io_lib:format("~B",[Round]); - false -> - Format = lists:flatten(io_lib:format("~~.~Bf",[-Precision])), - [Result] = io_lib:format(Format,[RoundPrecision]) - end, - Result. + if RoundPrecision == Round -> + floatformat_io(Round, 0); + true -> + floatformat_io(RoundPrecision, -Precision) + end. round(Number, Precision) -> P = math:pow(10, Precision), - round(Number * P) / P. + erlang:round(Number * P) / P. %% @doc Applies HTML escaping to a string. force_escape(Input) when is_list(Input) -> @@ -720,7 +718,13 @@ cast_to_float(Input) when is_float(Input) -> Input; cast_to_float(Input) when is_integer(Input) -> Input + 0.0; -cast_to_float(Input) -> +cast_to_float(Input) when is_binary(Input) -> + try binary_to_float(Input) + catch + error:_Reason -> + binary_to_integer(Input) + 0.0 + end; +cast_to_float(Input) when is_list(Input) -> try list_to_float(Input) catch error:_Reason -> diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 5150c31..775dd37 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -468,15 +468,54 @@ tests() -> {"|floatformat:\"-1\"", <<"{{ var1|floatformat:\"-1\" }}">>, [{var1, 34.23234}], <<"34.2">>}, - %% ?assertEqual( "", erlydtl_filters:floatformat(,)), - %% ?assertEqual( "34", erlydtl_filters:floatformat(34.00000,-1)), - %% ?assertEqual( "34.3", erlydtl_filters:floatformat(34.26000,-1)), - %% ?assertEqual( "34.232", erlydtl_filters:floatformat(34.23234,3)), - %% ?assertEqual( "34.000", erlydtl_filters:floatformat(34.00000,3)), - %% ?assertEqual( "34.260", erlydtl_filters:floatformat(34.26000,3)), - %% ?assertEqual( "34.232", erlydtl_filters:floatformat(34.23234,-3)), - %% ?assertEqual( "34", erlydtl_filters:floatformat(34.00000,-3)), - %% ?assertEqual( "34.260", erlydtl_filters:floatformat(34.26000,-3)). + {"int |floatformat", + <<"{{ var1|floatformat:\"-1\" }}">>, [{var1, 123}], + <<"123">>}, + {"string |floatformat", + <<"{{ var1|floatformat:\"-1\" }}">>, [{var1, "123.321"}], + <<"123.3">>}, + {"binary |floatformat", + <<"{{ var1|floatformat:\"-1\" }}">>, [{var1, <<"123.321">>}], + <<"123.3">>}, + + %% from: https://docs.djangoproject.com/en/1.6/ref/templates/builtins/#floatformat + {"1.a) |floatformat", + <<"{{ var1|floatformat }}">>, [{var1, 34.23234}], + <<"34.2">>}, + {"1.b) |floatformat", + <<"{{ var1|floatformat }}">>, [{var1, 34.00000}], + <<"34">>}, + {"1.c) |floatformat", + <<"{{ var1|floatformat }}">>, [{var1, 34.26000}], + <<"34.3">>}, + {"2.a) |floatformat:\"3\"", + <<"{{ var1|floatformat:\"3\" }}">>, [{var1, 34.23234}], + <<"34.232">>}, + {"2.b) |floatformat:\"3\"", + <<"{{ var1|floatformat:\"3\" }}">>, [{var1, 34.00000}], + <<"34.000">>}, + {"2.c) |floatformat:\"3\"", + <<"{{ var1|floatformat:\"3\" }}">>, [{var1, 34.26000}], + <<"34.260">>}, + {"3.a) |floatformat:\"0\"", + <<"{{ var1|floatformat:\"0\" }}">>, [{var1, 34.23234}], + <<"34">>}, + {"3.b) |floatformat:\"0\"", + <<"{{ var1|floatformat:\"0\" }}">>, [{var1, 34.00000}], + <<"34">>}, + {"3.c) |floatformat:\"0\"", + <<"{{ var1|floatformat:\"0\" }}">>, [{var1, 39.56000}], + <<"40">>}, + {"4.a) |floatformat:\"-3\"", + <<"{{ var1|floatformat:\"-3\" }}">>, [{var1, 34.23234}], + <<"34.232">>}, + {"4.b) |floatformat:\"-3\"", + <<"{{ var1|floatformat:\"-3\" }}">>, [{var1, 34.00000}], + <<"34">>}, + {"4.c) |floatformat:\"-3\"", + <<"{{ var1|floatformat:\"-3\" }}">>, [{var1, 34.26000}], + <<"34.260">>}, + {"|force_escape", <<"{{ var1|force_escape }}">>, [{var1, "Ben & Jerry's <=> \"The World's Best Ice Cream\""}], <<"Ben & Jerry's <=> "The World's Best Ice Cream"">>}, From d719e3f27d0c319cf5a5727ca2c538ef8f3da1d1 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 29 Nov 2013 12:23:13 +0100 Subject: [PATCH 066/361] Fix urlencode filter, with support for safe characters in the argument. Fixes #82. --- src/erlydtl_filters.erl | 46 +++++++++++++++++++++------------ tests/src/erlydtl_unittests.erl | 9 +++++++ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index f75a622..b3c2bc9 100644 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -111,6 +111,7 @@ unordered_list/1, upper/1, urlencode/1, + urlencode/2, urlize/1, urlize/2, urlizetrunc/2, @@ -853,10 +854,13 @@ upper(Input) -> string:to_upper(Input). %% @doc Escapes a value for use in a URL. -urlencode(Input) when is_binary(Input) -> - urlencode(Input, 0); -urlencode(Input) when is_list(Input) -> - urlencode(Input, []). +urlencode(Input) -> + urlencode(Input, <<"/">>). + +urlencode(Input, Safe) when is_binary(Input) -> + urlencode_io(Input, Safe, 0); +urlencode(Input, Safe) when is_list(Input) -> + urlencode_io(Input, Safe, []). %% @doc Returns the number of words. wordcount(Input) when is_binary(Input) -> @@ -1133,25 +1137,33 @@ wordwrap([C | Rest], Acc, WordAcc, LineLength, WrapAt) when erlang:length(WordAc wordwrap([C | Rest], Acc, WordAcc, LineLength, WrapAt) -> wordwrap(Rest, Acc, [C | WordAcc], LineLength, WrapAt). -urlencode(Input, Index) when is_binary(Input) -> +urlencode_io(Input, Safe, Index) when is_binary(Input) -> case Input of <<_:Index/binary, Byte, _/binary>> when ?NO_ENCODE(Byte) -> - urlencode(Input, Index + 1); - <> -> - HiDigit = hexdigit(Hi), - LoDigit = hexdigit(Lo), - Code = <<$\%, HiDigit, LoDigit>>, - process_binary_match(Pre, Code, size(Post), urlencode(Post, 0)); + urlencode_io(Input, Safe, Index + 1); + <> -> + process_binary_match( + Pre, maybe_urlencode_char(C, Safe), + size(Post), urlencode_io(Post, Safe, 0)); Input -> Input end; -urlencode([], Acc) -> +urlencode_io([], _Safe, Acc) -> lists:reverse(Acc); -urlencode([C | Rest], Acc) when ?NO_ENCODE(C) -> - urlencode(Rest, [C | Acc]); -urlencode([C | Rest], Acc) -> - <> = <>, - urlencode(Rest, [hexdigit(Lo), hexdigit(Hi), $\% | Acc]). +urlencode_io([C | Rest], Safe, Acc) when ?NO_ENCODE(C) -> + urlencode_io(Rest, Safe, [C | Acc]); +urlencode_io([C | Rest], Safe, Acc) -> + urlencode_io(Rest, Safe, [maybe_urlencode_char(<>, Safe) | Acc]). + +maybe_urlencode_char(C, Safe) -> + case binary:match(Safe, C) of + nomatch -> + <> = C, + HiDigit = hexdigit(Hi), + LoDigit = hexdigit(Lo), + <<$%, HiDigit, LoDigit>>; + _ -> C + end. %% @doc Converts URLs in text into clickable links. %%TODO: Autoescape not yet implemented diff --git a/tests/src/erlydtl_unittests.erl b/tests/src/erlydtl_unittests.erl index 775dd37..b53d28d 100644 --- a/tests/src/erlydtl_unittests.erl +++ b/tests/src/erlydtl_unittests.erl @@ -923,6 +923,15 @@ tests() -> {"|urlencode", <<"{{ url|urlencode }}">>, [{url, "You #$*@!!"}], <<"You%20%23%24%2A%40%21%21">>}, + {"|urlencode", + <<"{{ url|urlencode }}">>, [{url, "http://www.example.org/foo?a=b&c=d"}], + <<"http%3A//www.example.org/foo%3Fa%3Db%26c%3Dd">>}, + {"|urlencode", + <<"{{ url|urlencode:\"\" }}">>, [{url, "http://www.example.org/foo?a=b&c=d"}], + <<"http%3A%2F%2Fwww.example.org%2Ffoo%3Fa%3Db%26c%3Dd">>}, + {"|urlencode", + <<"{{ url|urlencode:\":/?=&\" }}">>, [{url, "http://www.example.org/foo?a=b&c=d"}], + <<"http://www.example.org/foo?a=b&c=d">>}, {"|urlize", <<"{{ var|urlize }}">>, [{var, "Check out www.djangoproject.com"}], <<"Check out www.djangoproject.com">>}, From 3a8c835fbfdc4c01b545215cba66f6c3e144f87c Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 29 Nov 2013 13:29:13 +0100 Subject: [PATCH 067/361] Test error case for missing template from extends tag. Fixes #60. --- tests/input/custom_call | 4 ++-- tests/input/extends3 | 3 +++ tests/src/erlydtl_functional_tests.erl | 14 +++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 tests/input/extends3 diff --git a/tests/input/custom_call b/tests/input/custom_call index 37f7e9e..4bea4ad 100755 --- a/tests/input/custom_call +++ b/tests/input/custom_call @@ -1,7 +1,7 @@ >>>> before custom call tag 'comment' -{% call example_for_preset %} +{% call functional_test_for_preset %} >>>> after custom call tag 'comment' >>>> before custom call tag 'if' -{% call example_if with var1 %} +{% call functional_test_if with var1 %} >>>> after custom call tag 'if' diff --git a/tests/input/extends3 b/tests/input/extends3 new file mode 100644 index 0000000..582aa49 --- /dev/null +++ b/tests/input/extends3 @@ -0,0 +1,3 @@ +{% extends "imaginary" %} +{% block title %}replacing the base title{% endblock %} +{% block content %}replacing the base content - variable: {{ test_var }} after variable {% endblock %} diff --git a/tests/src/erlydtl_functional_tests.erl b/tests/src/erlydtl_functional_tests.erl index f65b710..07e4ba1 100644 --- a/tests/src/erlydtl_functional_tests.erl +++ b/tests/src/erlydtl_functional_tests.erl @@ -48,7 +48,7 @@ test_list() -> "custom_tag", "custom_tag1", "custom_tag2", "custom_tag3", "custom_call", "include_template", "include_path", "ssi", - "extends_path", "extends_path2", "trans", "extends2" ]. + "extends_path", "extends_path2", "trans", "extends2", "extends3" ]. setup_compile("for_list_preset") -> CompileVars = [{fruit_list, [["apple", "apples"], ["banana", "bananas"], ["coconut", "coconuts"]]}], @@ -75,7 +75,11 @@ setup_compile("var_preset") -> CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}], {ok, CompileVars}; setup_compile("extends2") -> - {error, []}; + {{error, "The extends tag must be at the very top of the template"}, []}; +setup_compile("extends3") -> + File = templates_dir("input/imaginary"), + Error = {0, functional_test_extends3, "Failed to read file"}, + {{error, {File, [Error]}}, []}; %% Huh?! what kind of error message is that!? setup_compile(_) -> {ok, []}. @@ -235,7 +239,7 @@ fold_tests() -> test_compile_render(Name) -> File = filename:join([templates_docroot(), Name]), - Module = "example_" ++ Name, + Module = "functional_test_" ++ Name, io:format(" Template: ~p, ... ", [Name]), case setup_compile(Name) of {CompileStatus, CompileVars} -> @@ -251,8 +255,8 @@ test_compile_render(Name) -> io:format("missing error"), {error, "compiling should have failed :" ++ File} end; - {error, Err} -> - if CompileStatus =:= error -> io:format("ok"); + {error, _}=Err -> + if CompileStatus =:= Err -> io:format("ok"); true -> io:format("failed"), {compile_error, io_lib:format("~p", [Err])} From 32455f923ad6b16fb621709499c31b76365a0e8a Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 29 Nov 2013 16:49:06 +0100 Subject: [PATCH 068/361] Revert date filter changes from 313edd5edd376ca12b773285a261086138120f20. I don't mind the unix epoch features as such. But they belong in another filter module, as it's not a feature of Django. --- src/erlydtl_filters.erl | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index 606abeb..ce2e445 100644 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -56,7 +56,6 @@ cut/2, date/1, date/2, - date_unix_epoch/1, default/2, default_if_none/2, dictsort/2, @@ -240,25 +239,13 @@ date(Input) -> date(Input, "F j, Y"). %% @doc Formats a date according to the given format. -date(undefined, _) -> "undefined"; -date(Input, FormatStr) when is_binary(Input) -> - list_to_binary(date(binary_to_list(Input), FormatStr)); -%% @doc format calendar:gregorian_seconds to string (year starts at year 0, not 1970. Use date_unix_epoch for latter). -date(Input, FormatStr) when is_integer(Input) -> - date(calendar:gregorian_seconds_to_datetime(Input), FormatStr); -date({{_,_,_} = Date,{_,_,_} = Time}, FormatStr) -> - erlydtl_dateformat:format({Date, Time}, FormatStr); -date({_,_,_} = Date, FormatStr) -> - erlydtl_dateformat:format(Date, FormatStr); -date(Input, _FormatStr) when is_list(Input) -> - io:format("Unexpected date parameter : ~p~n", [Input]), +date(Input, FormatStr) + when is_tuple(Input) + andalso (size(Input) == 2 orelse size(Input) == 3) -> + erlydtl_dateformat:format(Input, FormatStr); +date(Input, _FormatStr) -> + io:format("Unexpected date parameter: ~p~n", [Input]), "". -%% @doc format a date given in unix-epoch seconds. -%% Input is seconds since 1-1-1970. -%%% offset is calendar:datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}}), the difference between -%%% gregorian calendar and unix epoch. -date_unix_epoch(Input) -> - date(Input + 62167219200). %% @doc If value evaluates to `false', use given default. Otherwise, use the value. default(Input, Default) -> @@ -510,7 +497,6 @@ phone2numeric(Input) when is_list(Input) -> %% @doc Returns a plural suffix if the value is not 1. By default, this suffix is 's'. pluralize(Number, Suffix) when is_binary(Suffix) -> pluralize_io(Number, binary_to_list(Suffix) ); - pluralize(Number, Suffix) when is_list(Suffix) -> pluralize_io(Number, Suffix). From 50d631b991c58d879a4b79ca5a282a6721518ea9 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 29 Nov 2013 17:20:55 +0100 Subject: [PATCH 069/361] Fix truncatechars and truncatewords. They weren't behaving quite Django enough. --- src/erlydtl_filters.erl | 149 ++++++++++++++++---------------- tests/src/erlydtl_unittests.erl | 20 ++--- 2 files changed, 84 insertions(+), 85 deletions(-) diff --git a/src/erlydtl_filters.erl b/src/erlydtl_filters.erl index ce2e445..6a5aeaa 100644 --- a/src/erlydtl_filters.erl +++ b/src/erlydtl_filters.erl @@ -748,6 +748,11 @@ cast_to_integer(Input) when is_list(Input)-> erlang:list_to_integer(Input) end. +cast_to_list(Input) when is_list(Input) -> Input; +cast_to_list(Input) when is_binary(Input) -> binary_to_list(Input); +cast_to_list(Input) when is_atom(Input) -> atom_to_list(Input); +cast_to_list(Input) -> hd(io_lib:format("~p", [Input])). + %% @doc Converts to lowercase, removes non-word characters (alphanumerics and underscores) and converts spaces to hyphens. slugify(Input) when is_binary(Input) -> slugify(binary_to_list(Input)); @@ -808,36 +813,16 @@ title(Input) when is_list(Input) -> title(lower(Input), []). %% @doc Truncates a string after a certain number of characters. -truncatechars(_Input, Max) when Max =< 0 -> - ""; -truncatechars(null, Max) -> - truncatechars("null", Max); -truncatechars(Input, Max) when is_integer(Input) -> - truncatechars(integer_to_list(Input), Max); -truncatechars(Input, Max) when is_binary(Input) -> - list_to_binary(truncatechars(binary_to_list(Input), Max)); truncatechars(Input, Max) -> - truncatechars(Input, Max, []). + truncatechars_io(cast_to_list(Input), Max, []). %% @doc Truncates a string after a certain number of words. -truncatewords(_Input, Max) when Max =< 0 -> - ""; -truncatewords(null, Max) -> - truncatewords("null", Max); -truncatewords(Input, Max) when is_integer(Input) -> - truncatewords(integer_to_list(Input), Max); -truncatewords(Input, Max) when is_binary(Input) -> - list_to_binary(truncatewords(binary_to_list(Input), Max)); truncatewords(Input, Max) -> - truncatewords(Input, Max, []). + truncatewords_io(cast_to_list(Input), Max, []). %% @doc Similar to truncatewords, except that it is aware of HTML tags. -truncatewords_html(_Input, Max) when Max =< 0 -> - ""; -truncatewords_html(Input, Max) when is_binary(Input) -> - truncatewords_html(binary_to_list(Input), Max); truncatewords_html(Input, Max) -> - truncatewords_html(Input, Max, [], [], text). + truncatewords_html_io(cast_to_list(Input), Max, [], [], text). %% @doc Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing `