Browse Source

Integrate new filters and tests from dgulino

Restructured the file layout somewhat.
Evan Miller 14 years ago
parent
commit
ae6bf4b036
66 changed files with 921 additions and 396 deletions
  1. 4 6
      Emakefile
  2. 3 3
      Makefile
  3. 1 1
      README.markdown
  4. 1 5
      ebin/erlydtl.app
  5. 0 0
      src/erlydtl.erl
  6. 17 20
      src/erlydtl_compiler.erl
  7. 0 0
      src/erlydtl_deps.erl
  8. 409 303
      src/erlydtl_filters.erl
  9. 0 0
      src/erlydtl_i18n.erl
  10. 1 0
      src/erlydtl_parser.yrl
  11. 0 0
      src/erlydtl_runtime.erl
  12. 0 0
      src/erlydtl_scanner.erl
  13. 0 0
      src/filter_lib/erlydtl_dateformat.erl
  14. 13 17
      src/filter_lib/erlydtl_slice.erl
  15. 0 0
      src/i18n/Makefile
  16. 0 0
      src/i18n/i18n_manager.erl
  17. 0 0
      src/i18n/po_generator.erl
  18. 0 0
      src/i18n/po_scanner.erl
  19. 0 0
      src/i18n/sources_parser.erl
  20. 0 0
      tests/input/autoescape
  21. 0 0
      tests/input/base
  22. 0 0
      tests/input/comment
  23. 0 0
      tests/input/custom_call
  24. 0 0
      tests/input/custom_tag
  25. 0 0
      tests/input/cycle
  26. 0 0
      tests/input/extends
  27. 0 0
      tests/input/extends_path
  28. 0 0
      tests/input/extends_path2
  29. 0 0
      tests/input/filters
  30. 0 0
      tests/input/for
  31. 0 0
      tests/input/for_list
  32. 0 0
      tests/input/for_list_preset
  33. 0 0
      tests/input/for_preset
  34. 0 0
      tests/input/for_records
  35. 0 0
      tests/input/for_records_preset
  36. 0 0
      tests/input/for_tuple
  37. 0 0
      tests/input/if
  38. 0 0
      tests/input/if_preset
  39. 0 0
      tests/input/ifequal
  40. 0 0
      tests/input/ifequal_preset
  41. 0 0
      tests/input/ifnotequal
  42. 0 0
      tests/input/ifnotequal_preset
  43. 0 0
      tests/input/include
  44. 0 0
      tests/input/include.html
  45. 0 0
      tests/input/include_path
  46. 0 0
      tests/input/include_template
  47. 0 0
      tests/input/now
  48. 0 0
      tests/input/path1/base1
  49. 0 0
      tests/input/path1/base2
  50. 0 0
      tests/input/path1/include1
  51. 0 0
      tests/input/path1/template1
  52. 0 0
      tests/input/path2/base2
  53. 0 0
      tests/input/path2/template2
  54. 0 0
      tests/input/trans
  55. 0 0
      tests/input/var
  56. 0 0
      tests/input/var_preset
  57. 49 0
      tests/py/erlydtl_python_test.py
  58. BIN
      tests/py/erlydtl_python_test.pyc
  59. 29 0
      tests/py/python_dtl_setup.txt
  60. 15 0
      tests/py/run_erl_node.sh
  61. 0 0
      tests/src/erlydtl_dateformat_tests.erl
  62. 0 0
      tests/src/erlydtl_example_variable_storage.erl
  63. 2 2
      tests/src/erlydtl_functional_tests.erl
  64. 377 39
      tests/src/erlydtl_unittests.erl
  65. 0 0
      tests/src/gettext.erl
  66. 0 0
      tests/src/sources_parser_unittests.erl

+ 4 - 6
Emakefile

@@ -1,6 +1,4 @@
-{"src/erlydtl/*", [debug_info, {outdir, "ebin"}]}.
-{"src/erlydtl/i18n/*", [debug_info, {outdir, "ebin"}]}.
-{"src/erlydtl/filter_lib/*", [debug_info, {outdir, "ebin"}]}.
-{"src/tests/*", [debug_info, {outdir, "ebintest"}]}.
-{"src/tests/i18n/*", [debug_info, {outdir, "ebintest"}]}.
-{"src/demo/*", [debug_info, {outdir, "ebintest"}]}.
+{"src/*", [debug_info, {outdir, "ebin"}]}.
+{"src/i18n/*", [debug_info, {outdir, "ebin"}]}.
+{"src/filter_lib/*", [debug_info, {outdir, "ebin"}]}.
+{"tests/src/*", [debug_info, {outdir, "ebintest"}]}.

+ 3 - 3
Makefile

@@ -1,7 +1,7 @@
 ERL=erl
 ERLC=erlc
 
-PARSER=src/erlydtl/erlydtl_parser
+PARSER=src/erlydtl_parser
 
 
 all: compile
@@ -11,7 +11,7 @@ compile: $(PARSER).erl
 	$(ERL) -make 
 
 $(PARSER).erl: $(PARSER).yrl
-	$(ERLC) -o src/erlydtl src/erlydtl/erlydtl_parser.yrl
+	$(ERLC) -o src src/erlydtl_parser.yrl
  
 run: compile
 	$(ERL) -pa ebin
@@ -29,4 +29,4 @@ clean:
 	rm -fv ebin/*.beam
 	rm -fv ebintest/*
 	rm -fv erl_crash.dump $(PARSER).erl
-	rm -fv examples/rendered_output/*
+	rm -fv tests/output/*

+ 1 - 1
README.markdown

@@ -125,4 +125,4 @@ From a Unix shell, run:
 
     make test
 
-Note that the tests will create some output in examples/rendered_output.
+Note that the tests will create some output in tests/output.

+ 1 - 5
ebin/erlydtl.app

@@ -1,21 +1,17 @@
 %% -*- mode: erlang -*-
 {application, erlydtl,
  [{description, "ErlyDTL implements most but not all of the Django Template Language"},
-  {vsn, "0.6.1"},
+  {vsn, "0.7.0"},
   {modules, [
              erlydtl,
              erlydtl_compiler,
              erlydtl_dateformat,
-             erlydtl_dateformat_tests,
              erlydtl_deps,
-             erlydtl_example_variable_storage,
              erlydtl_filters,
-             erlydtl_functional_tests,
              erlydtl_parser,
              erlydtl_runtime,
              erlydtl_scanner,
              erlydtl_slice,
-             erlydtl_unittests,
              erlydtl_i18n,
              i18n_manager,
              po_generator,

+ 0 - 0
src/erlydtl/erlydtl.erl → src/erlydtl.erl


+ 17 - 20
src/erlydtl/erlydtl_compiler.erl → src/erlydtl_compiler.erl

@@ -677,20 +677,23 @@ filter_ast(Variable, Filter, Context, TreeWalker) ->
 filter_ast_noescape(Variable, [{identifier, _, 'escape'}], Context, TreeWalker) ->
     value_ast(Variable, true, Context, TreeWalker);
 filter_ast_noescape(Variable, Filter, Context, TreeWalker) ->
-    {{VariableAst, Info}, TreeWalker2} = value_ast(Variable, true, Context, TreeWalker),
-    VarValue = filter_ast1(Filter, VariableAst),
-    {{VarValue, Info}, TreeWalker2}.
-
-filter_ast1([{identifier, _, Name}, {string_literal, _, ArgName}], VariableAst) ->
-    filter_ast2(Name, VariableAst, [erl_syntax:string(unescape_string_literal(ArgName))]);
-filter_ast1([{identifier, _, Name}, {number_literal, _, ArgName}], VariableAst) ->
-    filter_ast2(Name, VariableAst, [erl_syntax:integer(list_to_integer(ArgName))]);
-filter_ast1([{identifier, _, Name}|_], VariableAst) ->
-    filter_ast2(Name, VariableAst, []).
-
-filter_ast2(Name, VariableAst, AdditionalArgs) ->
-    erl_syntax:application(erl_syntax:atom(erlydtl_filters), erl_syntax:atom(Name), 
-        [VariableAst | AdditionalArgs]).
+    {{VariableAst, Info1}, TreeWalker2} = value_ast(Variable, true, Context, TreeWalker),
+    {VarValue, Info2} = filter_ast1(Filter, VariableAst, Context),
+    {{VarValue, merge_info(Info1, Info2)}, TreeWalker2}.
+
+filter_ast1([{identifier, _, Name}, {string_literal, _, ArgName}], VariableAst, _Context) ->
+    filter_ast2(Name, VariableAst, [erl_syntax:string(unescape_string_literal(ArgName))], []);
+filter_ast1([{identifier, _, Name}, {number_literal, _, ArgName}], VariableAst, _Context) ->
+    filter_ast2(Name, VariableAst, [erl_syntax:integer(list_to_integer(ArgName))], []);
+filter_ast1([{identifier, _, Name}, ArgVariable], VariableAst, Context) ->
+    {ArgAst, ArgVarName} = resolve_variable_ast(ArgVariable, Context),
+    filter_ast2(Name, VariableAst, [ArgAst], [ArgVarName]);
+filter_ast1([{identifier, _, Name}], VariableAst, _Context) ->
+    filter_ast2(Name, VariableAst, [], []).
+
+filter_ast2(Name, VariableAst, AdditionalArgs, VarNames) ->
+    {erl_syntax:application(erl_syntax:atom(erlydtl_filters), erl_syntax:atom(Name), 
+            [VariableAst | AdditionalArgs]), #ast_info{var_names = VarNames}}.
  
 search_for_escape_filter(_, _, #dtl_context{auto_escape = on}) ->
     on;
@@ -707,7 +710,6 @@ search_for_escape_filter(_Variable, _Filter) ->
     off.
 
 
-
 resolve_variable_ast(VarTuple, Context) ->
     resolve_variable_ast(VarTuple, Context, 'find_value').
  
@@ -726,11 +728,6 @@ resolve_variable_ast({variable, {identifier, _, VarName}}, Context, FinderFuncti
     end,
     {VarValue, VarName};
 
-resolve_variable_ast({apply_filter, Variable, Filter}, Context, FinderFunction) ->
-    {VarAst, VarName} = resolve_variable_ast(Variable, Context, FinderFunction),
-    VarValue = filter_ast1(Filter, erl_syntax:list([VarAst])),
-    {VarValue, VarName};
-
 resolve_variable_ast(What, _Context, _FinderFunction) ->
    error_logger:error_msg("~p:resolve_variable_ast unhandled: ~p~n", [?MODULE, What]).
 

+ 0 - 0
src/erlydtl/erlydtl_deps.erl → src/erlydtl_deps.erl


+ 409 - 303
src/erlydtl/erlydtl_filters.erl → src/erlydtl_filters.erl

@@ -3,9 +3,9 @@
 %%% @author    Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
 %%% @author    Evan Miller <emmiller@gmail.com>
 %%% @copyright 2008 Roberto Saccon, Evan Miller
-%%% @doc 
+%%% @doc
 %%% Template filters
-%%% @end  
+%%% @end 
 %%%
 %%% The MIT License
 %%%
@@ -42,82 +42,93 @@
 -include_lib("eunit/include/eunit.hrl").
  
 -ifdef(TEST).
-        -export([cast_to_float/1,cast_to_integer/1,stringformat_io/7,round/2,unjoin/2]).
+        -export([cast_to_float/1,cast_to_integer/1,stringformat_io/7,round/2,unjoin/2,addDefaultURI/1]).
 -endif.
  
-
--export([add/2, 
+ 
+-export([add/2,
         addslashes/1,
-        capfirst/1, 
-        center/2, 
+        capfirst/1,
+        center/2,
         cut/2,
-        date/2, 
+        date/2,
         default/2,
-        default_if_none/2, 
-        %dictsort/,
-        %dictsortreversed/,
+        default_if_none/2,
+        dictsort/2,
+        dictsortreversed/2,
         divisibleby/2,
         %escape/,
         escapejs/1,
         filesizeformat/1,
-        first/1, 
-        fix_ampersands/1, 
+        first/1,
+        fix_ampersands/1,
         floatformat/2,
-        force_escape/1, 
-        format_integer/1, 
+        force_escape/1,
+        format_integer/1,
         format_number/1,
         get_digit/2,
         %iriencode/1,
-        join/2, 
-        last/1, 
-        length/1, 
-        length_is/2, 
+        join/2,
+        last/1,
+        length/1,
+        length_is/2,
         linebreaks/1,
-        linebreaksbr/1, 
+        linebreaksbr/1,
         linenumbers/1,
         ljust/2,
-        lower/1, 
+        lower/1,
         make_list/1, 
         phone2numeric/1,
         pluralize/1,
         pluralize/2,
-        %pprint/,
+        pprint/1,
         random/1,
         random_num/1,
         random_range/1,
         removetags/2,
-        rjust/2, 
+        rjust/2,
         %safe/,
         %safeseq/,
         slice/2,
         slugify/1,
         stringformat/2,
-        %striptags/,
+        striptags/1,
+        time/1,
         time/2,
-        %timesince/,
-        %timeuntil/,
+        timesince/1,
+        timesince/2,
+        timeuntil/1,
+        timeuntil/2,
         title/1,
-        truncatewords/2, 
+        truncatewords/2,
         %truncatewords_html/,
-        %unordered_list/,
-        upper/1, 
+        %unordered_list/1,
+        upper/1,
         urlencode/1,
-        %urlize/,
-        %urlizetrunc/,
+        urlize/1,
+        urlize/2,
+        urlizetrunc/2,
         wordcount/1,
         wordwrap/2,
         yesno/2]).
-
+ 
 -define(NO_ENCODE(C), ((C >= $a andalso C =< $z) orelse
         (C >= $A andalso C =< $Z) orelse
         (C >= $0 andalso C =< $9) orelse
-        (C =:= $\. orelse C =:= $- 
+        (C =:= $\. orelse C =:= $-
         orelse C =:= $~ orelse C =:= $_))).
-
+ 
 -define(KILOBYTE, 1024).
 -define(MEGABYTE, (1024 * ?KILOBYTE)).
 -define(GIGABYTE, (1024 * ?MEGABYTE)).
 
+-define(SECONDS_PER_MINUTE, 60).
+-define(SECONDS_PER_HOUR,  (60 * ?SECONDS_PER_MINUTE)).
+-define(SECONDS_PER_DAY,   (24 * ?SECONDS_PER_HOUR)).
+-define(SECONDS_PER_WEEK,   (7 * ?SECONDS_PER_DAY)).
+-define(SECONDS_PER_MONTH, (30 * ?SECONDS_PER_DAY)).
+-define(SECONDS_PER_YEAR, (365 * ?SECONDS_PER_DAY)).
+ 
 %% @doc Adds a number to the value.
 add(Input, Number) when is_binary(Input) ->
     list_to_binary(add(binary_to_list(Input), Number));
@@ -125,31 +136,35 @@ add(Input, Number) when is_list(Input) ->
     integer_to_list(add(list_to_integer(Input), Number));
 add(Input, Number) when is_integer(Input) ->
     Input + Number.
-
+ 
 %% @doc Adds slashes before quotes.
 addslashes(Input) when is_binary(Input) ->
     addslashes(binary_to_list(Input));
 addslashes(Input) when is_list(Input) ->
     addslashes(Input, []).
-
+ 
 %% @doc Capitalizes the first character of the value.
 capfirst([H|T]) when H >= $a andalso H =< $z ->
     [H + $A - $a | T];
+capfirst(Other) when is_list(Other) ->
+    Other;
 capfirst(<<Byte:8/integer, Binary/binary>>) when Byte >= $a andalso Byte =< $z ->
-    [(Byte + $A - $a)|binary_to_list(Binary)].
-
+    [(Byte + $A - $a)|binary_to_list(Binary)];
+capfirst(Other) when is_binary(Other) ->
+    Other.
+ 
 %% @doc Centers the value in a field of a given width.
 center(Input, Number) when is_binary(Input) ->
     list_to_binary(center(binary_to_list(Input), Number));
 center(Input, Number) when is_list(Input) ->
     string:centre(Input, Number).
-
+ 
 %% @doc Removes all values of arg from the given string.
 cut(Input, Arg) when is_binary(Input) ->
     cut(binary_to_list(Input), Arg);
 cut(Input, [Char]) when is_list(Input) ->
     cut(Input, Char, []).
-
+ 
 %% @doc Formats a date according to the given format.
 date(Input, FormatStr) when is_binary(Input) ->
     list_to_binary(date(binary_to_list(Input), FormatStr));
@@ -176,6 +191,17 @@ default_if_none(undefined, Default) ->
 default_if_none(Input, _) ->
     Input.
 
+%% @doc Takes a list of dictionaries and returns that list sorted by the key given in the argument.
+dictsort(DictList, Key) ->
+    case lists:all(fun(Dict) -> dict:is_key(Key, Dict) end, DictList) of
+        true -> lists:sort(fun(K1,K2) -> dict:find(Key,K1) =< dict:find(Key,K2) end, DictList);
+        false -> error
+    end.
+
+%% @doc Same as dictsort, but the list is reversed.
+dictsortreversed(DictList, Key) ->
+    lists:reverse(dictsort(DictList, Key)).
+
 %% @doc Returns `true' if the value is divisible by the argument.
 divisibleby(Input, Divisor) when is_binary(Input) ->
     divisibleby(binary_to_list(Input), Divisor);
@@ -186,8 +212,6 @@ divisibleby(Input, Divisor) when is_list(Divisor) ->
 divisibleby(Input, Divisor) when is_integer(Input), is_integer(Divisor) ->
     Input rem Divisor =:= 0.
 
-%% @doc Escapes 
- 
 %% @doc Escapes characters for use in JavaScript strings.
 escapejs(Input) when is_binary(Input) ->
     escapejs(binary_to_list(Input));
@@ -225,7 +249,7 @@ fix_ampersands(Input) when is_list(Input) ->
 floatformat(Number, Place) when is_binary(Number) ->
     floatformat(binary_to_list(Number), Place);
 floatformat(Number, Place) ->
-    floatformat_io(Number, Place).
+    floatformat_io(Number, cast_to_integer(Place)).
 
 floatformat_io(Number, []) ->
     floatformat_io(Number, -1);
@@ -249,7 +273,7 @@ floatformat_io(Number, Precision) when Precision < 0 ->
 round(Number, Precision) ->
     P = math:pow(10, Precision),
     round(Number * P) / P.
- 
+
 %% @doc Applies HTML escaping to a string.
 force_escape(Input) when is_list(Input) ->
     escape(Input, []);
@@ -332,44 +356,20 @@ linebreaksbr(Input) ->
     linebreaksbr(Input, []).
 
 %% @doc Displays text with line numbers.
-%% probably slow implementation
 linenumbers(Input) when is_binary(Input) ->
-         linenumbers(binary_to_list(Input));
+    linenumbers(binary_to_list(Input));
 linenumbers(Input) when is_list(Input) ->
-         linenumbers_io(Input, [], 1).
- 
-%linenumbers_io(Input) ->
-%         Lines = string:tokens(Input, "\n"),
-%         Numbers = lists:seq(1, erlang:length(Lines)),
-%         lists:concat(lists:zipwith(fun(Number, Line) -> erlang:integer_to_list(Number) ++ ". " ++ Line ++ "\n" end, Numbers, Lines)).
-
-%linenumbers(Input) ->
-%   linenumbers_io(Input, [], 1).
+    linenumbers_io(Input, [], 1).
 
 linenumbers_io([], Acc, _) ->
     lists:reverse(Acc);
 linenumbers_io(Input, [], LineNumber) ->
-    linenumbers_io(Input,
-    lists:reverse(integer_to_list(LineNumber)++". "), LineNumber + 1);
+    linenumbers_io(Input, lists:reverse(integer_to_list(LineNumber)++". "), LineNumber + 1);
 linenumbers_io("\n"++Rest, Acc, LineNumber) ->
-    linenumbers_io(Rest, lists:reverse("\n" ++ integer_to_list(LineNumber) ++ ". ") ++ Acc, LineNumber + 1);
+    linenumbers_io(Rest, lists:reverse("\n" ++ integer_to_list(LineNumber) ++ ". ", Acc), LineNumber + 1);
 linenumbers_io([H|T], Acc, LineNumber) ->
-   linenumbers_io(T, [H|Acc], LineNumber).
- 
-%% @doc Displays text with line numbers.
-%% tail recursive implementation
-%% linenumbers(Input) when is_binary(Input) ->
-%%       linenumbers_io(binary_to_list(Input), [], 0);
-%% linenumbers(Input) when is_list(Input) ->
-%%       linenumbers_io(Input, [], 0).
-%%
-%% linenumbers([], Acc, ) ->
-%%     lists:reverse(Acc);
-%% linenumbers([Head|Rest], Acc, CountAcc) ->
-%%       Count = CountAcc + 1,
-%%       LineNumber = integer_to_list(Count) ++ ". ",
-%%     linenumbers(Rest, lists:append(LineNumber, Acc));
- 
+    linenumbers_io(T, [H|Acc], LineNumber).
+
 %% @doc Left-aligns the value in a field of a given width.
 ljust(Input, Number) when is_binary(Input) ->
     list_to_binary(ljust(binary_to_list(Input), Number));
@@ -389,7 +389,7 @@ make_list(Input) when is_binary(Input) ->
     make_list(binary_to_list(Input));
 make_list(Input) ->
     unjoin(Input,"").
- 
+
 %% @doc Converts a phone number (possibly containing letters) to its numerical equivalent.
 phone2numeric(Input) when is_binary(Input) ->
     phone2numeric(binary_to_list(Input));
@@ -398,33 +398,34 @@ 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_io(Number, binary_to_list(Suffix) );
 pluralize(Number, Suffix) when is_list(Suffix) ->
-        pluralize_io(Number, Suffix).
- 
+    pluralize_io(Number, Suffix).
+
 pluralize(Number) ->
-        pluralize(Number, "s").
-       
+    pluralize(Number, "s").
+
 pluralize_io(Number, Suffix) ->
-%%       io:format("Number, Suffix: ~p, ~p ~n", [Number, Suffix]),
-        case lists:member($, , Suffix) of
-                true ->
-                        [Singular, Plural] = string:tokens(Suffix,","),
-                        case Number > 1 of
-                                false ->
-                                        Singular;
-                                true  ->
-                                        Plural
-                        end;
-                false ->
-                        case Number > 1 of
-                                false ->
-                                        [];
-                                true  ->
-                                        Suffix
-                        end
-        end.
- 
+    case lists:member($, , Suffix) of
+        true ->
+            [Singular, Plural] = string:tokens(Suffix,","),
+            case Number of
+                0 -> Plural;
+                1 -> Singular;
+                _ -> Plural
+            end;
+        false ->
+            case Number of
+                0 -> Suffix;
+                1 -> [];
+                _ -> Suffix
+            end
+    end.
+
+%% @doc "pretty print" arbitrary data structures.  Used for debugging.
+pprint(Input) ->
+    io_lib:format("~p",[Input]).
+
 %% @doc Returns a random item from the given list.
 random(Input) when is_list(Input) ->
     lists:nth(random:uniform(erlang:length(Input)), Input);
@@ -458,7 +459,7 @@ removetags(Input, Tags) ->
     Regex = lists:flatten(io_lib:format("</?(~s)( |\n)?>",[TagListString])),
     Result = re:replace(Input,Regex,"", [global,{return,list}]),
     Result.
- 
+
 %% @doc Right-aligns the value in a field of a given width.
 rjust(Input, Number) when is_binary(Input) ->
     list_to_binary(rjust(binary_to_list(Input), Number));
@@ -467,23 +468,23 @@ rjust(Input, Number) ->
 
 %% @doc Returns a slice of the list.
 slice(Input, Index) when is_binary(Input) ->
-        erlydtl_slice:slice(binary_to_list(Input), Index);
+    erlydtl_slice:slice(binary_to_list(Input), Index);
 slice(Input, Index) when is_list(Input) ->
-        erlydtl_slice:slice(Input, Index).
+    erlydtl_slice:slice(Input, Index).
 
 %% regex  " ^([#0-\s+].)([0-9\*]+)(\.[0-9]+)([diouxXeEfFgGcrs]) " matches ALL of "-10.0f"
 %% ([#0-\s+]?)([0-9\*]+)?(\.?)([0-9]?)([diouxXeEfFgGcrs])
 %% @doc Returns a formatted string
 stringformat(Input, Conversion) when is_binary(Input) ->
-        stringformat(binary_to_list(Input), Conversion);
+    stringformat(binary_to_list(Input), Conversion);
 stringformat(Input, Conversion) ->
-        ParsedConversion = re:replace(Conversion, "([\-#\+ ]?)([0-9\*]+)?(\.?)([0-9]?)([diouxXeEfFgGcrs])", "\\1 ,\\2 ,\\3 ,\\4 ,\\5 ", [{return,list}]),
-        ?debugFmt("ParsedConversion: ~p~n", [ParsedConversion]),
-        ParsedConversion1 = lists:map(fun(X) -> string:strip(X) end, string:tokens(ParsedConversion, ",")),
-         [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType] = ParsedConversion1,
-        ?debugFmt("ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType: ~p, ~p, ~p, ~p, ~p ~n", [ConversionFlag, cast_to_integer(MinFieldWidth), Precision, cast_to_integer(PrecisionLength), ConversionType]),
-        [String] = stringformat_io(Input, Conversion, ConversionFlag, cast_to_integer(MinFieldWidth), Precision, cast_to_integer(PrecisionLength), ConversionType),
-        lists:flatten(String).
+    ParsedConversion = re:replace(Conversion, "([\-#\+ ]?)([0-9\*]+)?(\.?)([0-9]?)([diouxXeEfFgGcrs])", "\\1 ,\\2 ,\\3 ,\\4 ,\\5 ", [{return,list}]),
+    ?debugFmt("ParsedConversion: ~p~n", [ParsedConversion]),
+    ParsedConversion1 = lists:map(fun(X) -> string:strip(X) end, string:tokens(ParsedConversion, ",")),
+    [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType] = ParsedConversion1,
+    ?debugFmt("ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType: ~p, ~p, ~p, ~p, ~p ~n", [ConversionFlag, cast_to_integer(MinFieldWidth), Precision, cast_to_integer(PrecisionLength), ConversionType]),
+    [String] = stringformat_io(Input, Conversion, ConversionFlag, cast_to_integer(MinFieldWidth), Precision, cast_to_integer(PrecisionLength), ConversionType),
+    lists:flatten(String).
 
 %% @doc
 %% A conversion specifier contains two or more characters and has the following components, which must occur in this order:
@@ -495,140 +496,148 @@ stringformat(Input, Conversion) ->
 %%    5. Precision (optional), given as a "." (dot) followed by the precision. If specified as "*" (an asterisk), the actual width is read from the next element of the tuple in values, and the value to convert comes after the precision.
 %%    6. Length modifier (optional).
 %%    7. Conversion type.
- 
+
 stringformat_io(Input, _Conversion, _ConversionFlag, [],
-                                _Precision, _PrecisionLength, "s") when is_list(Input) ->
-        Format = lists:flatten(io_lib:format("~~s", [])),
-       io_lib:format(Format, [Input]);
+    _Precision, _PrecisionLength, "s") when is_list(Input) ->
+    Format = lists:flatten(io_lib:format("~~s", [])),
+    io_lib:format(Format, [Input]);
 stringformat_io(Input, _Conversion, ConversionFlag, MinFieldWidth,
-                                _Precision, _PrecisionLength, "s") when is_list(Input) ->
-        %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
-        InputLength = erlang:length(Input),
-        case erlang:abs(MinFieldWidth) < InputLength of
-            true ->
-                MFW = InputLength;
-            false ->
-                MFW = MinFieldWidth
-        end,
-        Format = lists:flatten(io_lib:format("~~~s~ps", [ConversionFlag,MFW])),
-        io_lib:format(Format, [Input]);
+    _Precision, _PrecisionLength, "s") when is_list(Input) ->
+    %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
+    InputLength = erlang:length(Input),
+    case erlang:abs(MinFieldWidth) < InputLength of
+        true ->
+            MFW = InputLength;
+        false ->
+            MFW = MinFieldWidth
+    end,
+    Format = lists:flatten(io_lib:format("~~~s~ps", [ConversionFlag,MFW])),
+    io_lib:format(Format, [Input]);
 stringformat_io(Input, _Conversion, _ConversionFlag, MinFieldWidth,
-                                Precision, PrecisionLength, "f") when Precision == ".", MinFieldWidth == 0 ->
+    Precision, PrecisionLength, "f") when Precision == ".", MinFieldWidth == 0 ->
     Conversion1 = lists:concat(["","",Precision,PrecisionLength,"f"]),
     stringformat_io(Input, Conversion1, [], [], Precision, PrecisionLength, "f");
 stringformat_io(Input, Conversion, ConversionFlag, MinFieldWidth,
-                                Precision, "", "f") when Precision == "." ->
+    Precision, "", "f") when Precision == "." ->
     Format = re:replace(Conversion, "f", "d", [{return, list}] ),
     stringformat_io(Input, Format, ConversionFlag, MinFieldWidth,
-                                    Precision, "", "d");
+        Precision, "", "d");
 stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
-                                _Precision, _PrecisionLength, "f")->
-        %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
-        Format = "~" ++ Conversion,
-        io_lib:format(Format, [cast_to_float(Input)]);
+    _Precision, _PrecisionLength, "f")->
+    %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
+    Format = "~" ++ Conversion,
+    io_lib:format(Format, [cast_to_float(Input)]);
 stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
-                                [], [], "d")->
-        %?debugMsg("plain d"),
-        %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
-        Format = "~" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
-        io_lib:format(Format, [cast_to_integer(Input)]);
+    [], [], "d")->
+    %?debugMsg("plain d"),
+    %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
+    Format = "~" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
+    io_lib:format(Format, [cast_to_integer(Input)]);
 stringformat_io(Input, _Conversion, "-", MinFieldWidth,
-                                _Precision, PrecisionLength, "d") when MinFieldWidth > 0 ->
-        %Format = "~" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
-        DecimalFormat = "~" ++ integer_to_list(PrecisionLength) ++ "..0B", 
-        Decimal = lists:flatten( io_lib:format(DecimalFormat, [cast_to_integer(Input)]) ),
-        SpaceFormat = "~" ++ integer_to_list(MinFieldWidth - erlang:length(Decimal)) ++ "s",
-        Spaces = io_lib:format(SpaceFormat,[""]),
-        ?debugFmt("Spaces: |~s|", [Spaces]),
-        ?debugFmt("Decimal: ~s", [Decimal]),
-        [lists:flatten(Decimal  ++ Spaces)];
+    _Precision, PrecisionLength, "d") when MinFieldWidth > 0 ->
+    %Format = "~" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
+    DecimalFormat = "~" ++ integer_to_list(PrecisionLength) ++ "..0B", 
+    Decimal = lists:flatten( io_lib:format(DecimalFormat, [cast_to_integer(Input)]) ),
+    SpaceFormat = "~" ++ integer_to_list(MinFieldWidth - erlang:length(Decimal)) ++ "s",
+    Spaces = io_lib:format(SpaceFormat,[""]),
+    ?debugFmt("Spaces: |~s|", [Spaces]),
+    ?debugFmt("Decimal: ~s", [Decimal]),
+    [lists:flatten(Decimal  ++ Spaces)];
 stringformat_io(Input, _Conversion, _ConversionFlag, MinFieldWidth,
-                                _Precision, PrecisionLength, "d") when MinFieldWidth > 0 ->
-        %Format = "~" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
-        DecimalFormat = "~" ++ integer_to_list(PrecisionLength) ++ "..0B", 
-        Decimal = lists:flatten( io_lib:format(DecimalFormat, [cast_to_integer(Input)]) ),
-        SpaceFormat = "~" ++ integer_to_list(MinFieldWidth - erlang:length(Decimal)) ++ "s",
-        Spaces = io_lib:format(SpaceFormat,[""]),
-        ?debugFmt("Spaces: |~s|", [Spaces]),
-        ?debugFmt("Decimal: ~s", [Decimal]),
-        [lists:flatten(Spaces ++ Decimal)];
+    _Precision, PrecisionLength, "d") when MinFieldWidth > 0 ->
+    %Format = "~" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
+    DecimalFormat = "~" ++ integer_to_list(PrecisionLength) ++ "..0B", 
+    Decimal = lists:flatten( io_lib:format(DecimalFormat, [cast_to_integer(Input)]) ),
+    SpaceFormat = "~" ++ integer_to_list(MinFieldWidth - erlang:length(Decimal)) ++ "s",
+    Spaces = io_lib:format(SpaceFormat,[""]),
+    ?debugFmt("Spaces: |~s|", [Spaces]),
+    ?debugFmt("Decimal: ~s", [Decimal]),
+    [lists:flatten(Spaces ++ Decimal)];
 stringformat_io(Input, _Conversion, _ConversionFlag, _MinFieldWidth,
-                                _Precision, PrecisionLength, "d") ->
-        %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
-        %Format = "~" ++ PrecisionLength ++ "..0" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
-        ?debugFmt("precision d, Conversion: ~p~n", [Conversion]),
-        Format = lists:flatten("~" ++ io_lib:format("~B..0B",[PrecisionLength])), 
-        ?debugFmt("Format: ~p~n",[Format]),
-        io_lib:format(Format, [cast_to_integer(Input)]);
+    _Precision, PrecisionLength, "d") ->
+    %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
+    %Format = "~" ++ PrecisionLength ++ "..0" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
+    %?debugFmt("precision d, Conversion: ~p~n", [Conversion]),
+    Format = lists:flatten("~" ++ io_lib:format("~B..0B",[PrecisionLength])), 
+    ?debugFmt("Format: ~p~n",[Format]),
+    io_lib:format(Format, [cast_to_integer(Input)]);
 stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
-                                _Precision, _PrecisionLength, "i")->
-        Format = "~" ++ re:replace(Conversion, "i", "B", [{return, list}] ),
-        io_lib:format(Format, [cast_to_integer(Input)]);
+    _Precision, _PrecisionLength, "i")->
+    Format = "~" ++ re:replace(Conversion, "i", "B", [{return, list}] ),
+    io_lib:format(Format, [cast_to_integer(Input)]);
 stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
-                                _Precision, _PrecisionLength, "X")->
-        Format = "~" ++ re:replace(Conversion, "X", ".16B", [{return, list}] ),
-        io_lib:format(Format, [cast_to_integer(Input)]);
+    _Precision, _PrecisionLength, "X")->
+    Format = "~" ++ re:replace(Conversion, "X", ".16B", [{return, list}] ),
+    io_lib:format(Format, [cast_to_integer(Input)]);
 stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
-                                _Precision, _PrecisionLength, "x")->
-        Format = "~" ++ re:replace(Conversion, "x", ".16b", [{return, list}] ),
-        io_lib:format(Format, [cast_to_integer(Input)]);
+    _Precision, _PrecisionLength, "x")->
+    Format = "~" ++ re:replace(Conversion, "x", ".16b", [{return, list}] ),
+    io_lib:format(Format, [cast_to_integer(Input)]);
 stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
-                                _Precision, _PrecisionLength, "o")->
-        Format = "~" ++ re:replace(Conversion, "o", ".8b", [{return, list}] ),
-        io_lib:format(Format, [cast_to_integer(Input)]);
+    _Precision, _PrecisionLength, "o")->
+    Format = "~" ++ re:replace(Conversion, "o", ".8b", [{return, list}] ),
+    io_lib:format(Format, [cast_to_integer(Input)]);
 stringformat_io(Input, _Conversion, _ConversionFlag, _MinFieldWidth,
-                                Precision, PrecisionLength, "e") when is_integer(PrecisionLength), PrecisionLength >= 2->
-        ?debugFmt("PrecisionLength ~p~n", [PrecisionLength]),
-        Conversion1 = lists:concat(["","",Precision,PrecisionLength + 1,"e"]),
-        Format = "~" ++ Conversion1,
-        io_lib:format(Format, [cast_to_float(Input)]);
+    Precision, PrecisionLength, "e") when is_integer(PrecisionLength), PrecisionLength >= 2->
+    ?debugFmt("PrecisionLength ~p~n", [PrecisionLength]),
+    Conversion1 = lists:concat(["","",Precision,PrecisionLength + 1,"e"]),
+    Format = "~" ++ Conversion1,
+    io_lib:format(Format, [cast_to_float(Input)]);
 stringformat_io(Input, Conversion, ConversionFlag, MinFieldWidth,
-                                "", [], "e")->
-        Format = "~" ++ re:replace(Conversion, "e", ".6e", [{return, list}] ),
-        Raw = lists:flatten(stringformat_io(Input, Format, ConversionFlag, MinFieldWidth,
-                                ".", 6, "e")
-                           ),
-        %io:format("Raw: ~p~n", [Raw]),
-        Elocate = string:rstr(Raw,"e+"),
-        %io:format("Elocate: ~p~n", [Elocate]),
-        String = [string:substr(Raw,1,Elocate-1) ++ "e+" 
-                 ++ io_lib:format("~2..0B",[list_to_integer(string:substr(Raw,Elocate+2))])], %works till +99, then outputs "**"
-        %io:format("String: ~p~n", [String]),
-        String;
+    "", [], "e")->
+    Format = "~" ++ re:replace(Conversion, "e", ".6e", [{return, list}] ),
+    Raw = lists:flatten(stringformat_io(Input, Format, ConversionFlag, MinFieldWidth,
+            ".", 6, "e")
+    ),
+    %io:format("Raw: ~p~n", [Raw]),
+    Elocate = string:rstr(Raw,"e+"),
+    %io:format("Elocate: ~p~n", [Elocate]),
+    String = [string:substr(Raw,1,Elocate-1) ++ "e+" 
+        ++ io_lib:format("~2..0B",[list_to_integer(string:substr(Raw,Elocate+2))])], %works till +99, then outputs "**"
+    %io:format("String: ~p~n", [String]),
+    String;
 stringformat_io(Input, Conversion, ConversionFlag, MinFieldWidth,
-                                Precision, PrecisionLength, "E")->
-        Format = re:replace(Conversion, "E", "e", [{return, list}] ),
-        [Str] = stringformat_io(Input, Format, ConversionFlag, MinFieldWidth,
-                                Precision, PrecisionLength, "e"),
-        [string:to_upper(Str)].
- 
+    Precision, PrecisionLength, "E")->
+    Format = re:replace(Conversion, "E", "e", [{return, list}] ),
+    [Str] = stringformat_io(Input, Format, ConversionFlag, MinFieldWidth,
+        Precision, PrecisionLength, "e"),
+    [string:to_upper(Str)].
+
+%% @doc Strips all [X]HTML tags.
+striptags(Input) when is_binary(Input) ->
+    striptags(binary_to_list(Input));
+striptags(Input) ->
+    Regex = "(<[^>]+>)",
+    Result = re:replace(Input,Regex,"", [global,{return,list}]),
+    Result.
+
 cast_to_float([]) ->
     [];
 cast_to_float(Input) when is_float(Input) ->
-        Input;
+    Input;
 cast_to_float(Input) when is_integer(Input) ->
-        Input + 0.0;
+    Input + 0.0;
 cast_to_float(Input) ->
-        try list_to_float(Input)
-        catch
-                error:_Reason ->
-                        list_to_integer(Input) + 0.0
-        end.
+    try list_to_float(Input)
+    catch
+        error:_Reason ->
+            list_to_integer(Input) + 0.0
+    end.
 
 cast_to_integer([]) ->
     [];
 cast_to_integer(Input) when is_integer(Input) ->
-        Input;
+    Input;
 cast_to_integer(Input) when is_float(Input) ->
-        erlang:round(Input);
+    erlang:round(Input);
 cast_to_integer(Input) when is_list(Input)->
-        case lists:member($., Input) of
-                true ->
-                        erlang:round(erlang:list_to_float(Input));
-                false ->       
-                        erlang:list_to_integer(Input)
-        end.
-       
+    case lists:member($., Input) of
+        true ->
+            erlang:round(erlang:list_to_float(Input));
+        false ->       
+            erlang:list_to_integer(Input)
+    end.
+
 %% @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));
@@ -636,16 +645,57 @@ slugify(Input) when is_list(Input) ->
     slugify(Input, []).
 
 %% @doc Formats a time according to the given format.
-time(Input, "") ->
-    date(Input, "f a");
+time(Input) ->
+    date(Input, "f a").
+
 time(Input, FormatStr) ->
     date(Input, FormatStr).
- 
+
+timesince(Date) ->
+    timesince(Date, calendar:local_time()).
+%%algorithm taken from django code
+timesince(Date,Comparison) ->
+    Since = calendar:datetime_to_gregorian_seconds(Comparison) - calendar:datetime_to_gregorian_seconds(Date),
+    timesince0(Since, [], 0).
+
+timesince0(_, Acc, 2) ->
+    string:join(lists:reverse(Acc), ", ");
+timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_YEAR ->
+    Years = Seconds div ?SECONDS_PER_YEAR,
+    timesince0(Seconds rem ?SECONDS_PER_YEAR,  [io_lib:format("~B ~s~s", [Years, "year", pluralize(Years)])|Acc], Terms+1);
+timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_MONTH ->
+    Months = Seconds div ?SECONDS_PER_MONTH,
+    timesince0(Seconds rem ?SECONDS_PER_MONTH, [io_lib:format("~B ~s~s", [Months, "month", pluralize(Months)])|Acc], Terms+1);
+timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_WEEK ->
+    Weeks = Seconds div ?SECONDS_PER_WEEK,
+    timesince0(Seconds rem ?SECONDS_PER_WEEK,  [io_lib:format("~B ~s~s", [Weeks, "week", pluralize(Weeks)])|Acc], Terms+1);
+timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_DAY ->
+    Days = Seconds div ?SECONDS_PER_DAY,
+    timesince0(Seconds rem ?SECONDS_PER_DAY,   [io_lib:format("~B ~s~s", [Days, "day", pluralize(Days)])|Acc], Terms+1);
+timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_HOUR ->
+    Hours = Seconds div ?SECONDS_PER_HOUR,
+    timesince0(Seconds rem ?SECONDS_PER_HOUR,  [io_lib:format("~B ~s~s", [Hours, "hour", pluralize(Hours)])|Acc], Terms+1);
+timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_MINUTE ->
+    Minutes = Seconds div ?SECONDS_PER_MINUTE,
+    timesince0(Seconds rem ?SECONDS_PER_MINUTE,[io_lib:format("~B ~s~s", [Minutes, "minute", pluralize(Minutes)])|Acc], Terms+1);
+timesince0(Seconds, Acc, Terms) when Seconds >= 1 ->
+    timesince0(0, [io_lib:format("~B ~s~s", [Seconds, "second", pluralize(Seconds)])|Acc], Terms+1);
+timesince0(Seconds, [], 0) when Seconds =< 0 -> 
+    timesince0(0, ["0 minutes"], 1);
+timesince0(0, Acc, Terms) ->
+    timesince0(0, Acc, Terms+1).
+
+timeuntil(Date) ->
+    timesince(calendar:local_time(),Date).
+
+timeuntil(Date,Comparison) ->
+    timesince(Comparison,Date).
+
 %% @doc Converts a string into titlecase.
 title(Input) when is_binary(Input) ->
     title(binary_to_list(Input));
 title(Input) when is_list(Input) ->
-    title(Input, [], capnext).
+    title(Input, []).
 
 %% @doc Truncates a string after a certain number of words.
 truncatewords(Input, Max) when is_binary(Input) ->
@@ -655,6 +705,28 @@ truncatewords(_Input, Max) when Max =< 0 ->
 truncatewords(Input, Max) ->
     truncatewords(Input, Max, []).
 
+%% @doc Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing <ul> tags. 
+%%TODO: finish unordered_list
+%% unordered_list(List) ->
+%%         lists:reverse(unordered_list(List, [])).
+%%  
+%% unordered_list([], Acc) ->
+%%         Acc;
+%% unordered_list(List, Acc) ->   
+%%         [First|Rest] = List,
+%%         io:format("First is_list: ~p, ~p~n", [First, is_list(First)]),
+%%         case is_list(First) of
+%%             true ->
+%%                 [First|Rest] = First,
+%%                 Return = [First | Acc],
+%%                 "<ul>" ++ unordered_list(Rest,Return) + "</ul>";
+%%             false ->
+%%                 Return = [First | Acc],
+%%                 "<li>" ++ unordered_list(Rest,Return) + "</li>"
+%%         end.
+
+
+
 %% @doc Converts a string into all uppercase.
 upper(Input) when is_binary(Input) ->
     list_to_binary(upper(binary_to_list(Input)));
@@ -675,22 +747,22 @@ wordcount(Input) when is_list(Input) ->
 
 %% @doc Wraps words at specified line length, uses <BR/> html tag to delimit lines
 wordwrap(Input, Number) when is_binary(Input) ->
-    wordwrap_io(binary_to_list(Input), Number);
+    wordwrap(binary_to_list(Input), Number);
 wordwrap(Input, Number) when is_list(Input) ->
-    wordwrap_io(Input, Number).
- 
+    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(Choices) ->
-        yesno_io(binary_to_list(Bool), Choices);
+    yesno_io(binary_to_list(Bool), Choices);
 yesno(Bool, Choices) when is_list(Choices) ->
-        yesno_io(Bool, Choices).
+    yesno_io(Bool, Choices).
 
 % internal
 
 addslashes([], Acc) ->
     lists:reverse(Acc);
-addslashes([H|T], Acc) when H =:= $"; H =:= $' -> 
-    addslashes(T, [H, $\\|Acc]);
+addslashes([H|T], Acc) when H =:= $"; H =:= $' ->
+addslashes(T, [H, $\\|Acc]);
 addslashes([H|T], Acc) ->
     addslashes(T, [H|Acc]).
 
@@ -737,9 +809,9 @@ escape([C | Rest], Acc) ->
 escapejs([], Acc) ->
     lists:reverse(Acc);
 escapejs([C | Rest], Acc) when C < 32; C =:= $"; C =:= $'; C =:= $\\; C =:= $<;
-                               C =:= $>; C =:= $&; C =:= $=; C =:= $-; C =:= $;; 
-                               C =:= 8232; C =:= 8233 -> % just following django here...
-    escapejs(Rest, lists:reverse(lists:flatten(io_lib:format("\\u~4.16.0B", [C])), Acc));
+C =:= $>; C =:= $&; C =:= $=; C =:= $-; C =:= $;;
+C =:= 8232; C =:= 8233 -> % just following django here...
+escapejs(Rest, lists:reverse(lists:flatten(io_lib:format("\\u~4.16.0B", [C])), Acc));
 escapejs([C | Rest], Acc) ->
     escapejs(Rest, [C | Acc]).
 
@@ -800,26 +872,26 @@ lower(Input, Index) ->
 phone2numeric([], Acc) ->
     lists:reverse(Acc);
 phone2numeric([H|T], Acc) when H >= $a, H =< $c; H >= $A, H =< $C ->
-    phone2numeric(T, [$2|Acc]);
+phone2numeric(T, [$2|Acc]);
 phone2numeric([H|T], Acc) when H >= $d, H =< $f; H >= $D, H =< $F ->
-    phone2numeric(T, [$3|Acc]);
+phone2numeric(T, [$3|Acc]);
 phone2numeric([H|T], Acc) when H >= $g, H =< $i; H >= $G, H =< $I ->
-    phone2numeric(T, [$4|Acc]);
+phone2numeric(T, [$4|Acc]);
 phone2numeric([H|T], Acc) when H >= $j, H =< $l; H >= $J, H =< $L ->
-    phone2numeric(T, [$5|Acc]);
+phone2numeric(T, [$5|Acc]);
 phone2numeric([H|T], Acc) when H >= $m, H =< $o; H >= $M, H =< $O ->
-    phone2numeric(T, [$6|Acc]);
+phone2numeric(T, [$6|Acc]);
 phone2numeric([H|T], Acc) when H >= $p, H =< $s; H >= $P, H =< $S ->
-    phone2numeric(T, [$7|Acc]);
+phone2numeric(T, [$7|Acc]);
 phone2numeric([H|T], Acc) when H >= $t, H =< $v; H >= $T, H =< $V ->
-    phone2numeric(T, [$8|Acc]);
+phone2numeric(T, [$8|Acc]);
 phone2numeric([H|T], Acc) when H >= $w, H =< $z; H >= $W, H =< $Z ->
-    phone2numeric(T, [$9|Acc]);
+phone2numeric(T, [$9|Acc]);
 phone2numeric([H|T], Acc) ->
     phone2numeric(T, [H|Acc]).
 
- 
- 
+
+
 slugify([], Acc) ->
     lists:reverse(Acc);
 slugify([H|T], Acc) when H >= $A, H =< $Z ->
@@ -827,22 +899,18 @@ slugify([H|T], Acc) when H >= $A, H =< $Z ->
 slugify([$\ |T], Acc) ->
     slugify(T, [$-|Acc]);
 slugify([H|T], Acc) when H >= $a, H =< $z; H >= $0, H =< $9; H =:= $_ ->
-    slugify(T, [H|Acc]);
+slugify(T, [H|Acc]);
 slugify([_|T], Acc) ->
     slugify(T, Acc).
 
-title([], Acc, _) ->
+title([], Acc) ->
     lists:reverse(Acc);
-title([$\  = H|T], Acc, capnext) ->
-    title(T, [H|Acc], capnext);
-title([H|T], Acc, capnext) when H >= $a, H =< $z ->
-    title(T, [H - $a + $A|Acc], dont);
-title([H|T], Acc, capnext) ->
-    title(T, [H|Acc], dont);
-title([$\  = H|T], Acc, dont) ->
-    title(T, [H|Acc], capnext);
-title([H|T], Acc, dont) ->
-    title(T, [H|Acc], dont).
+title([Char | Rest], [] = Acc) when Char >= $a, Char =< $z ->
+    title(Rest, [Char + ($A - $a) | Acc]);
+title([Char | Rest], [$\  |_] = Acc) when Char >= $a, Char =< $z ->
+    title(Rest, [Char + ($A - $a) | Acc]);
+title([Char | Rest], Acc) ->
+    title(Rest, [Char | Acc]).
 
 truncatewords([], _WordsLeft, Acc) ->
     lists:reverse(Acc);
@@ -862,28 +930,26 @@ wordcount([C1, C2|Rest], Count) when C1 =/= $\  andalso C2 =:= $\  ->
 wordcount([_|Rest], Count) ->
     wordcount(Rest, Count).
 
-% based on: http://pragdave.pragprog.com/pragdave/2007/04/testfirst_word_.html 
-% This is the exported function: it passes the initial
-% result set to the internal versions
-wordwrap_io(Input, Number) ->
-        Words = string:tokens(Input, " "),
-    string:join(lists:reverse(wordwrap_io(Words, [""], Number)),"").
-% When we run out of words, we're done 
-wordwrap_io([], Result, _Number) ->
-    Result;
-% Adding a word to an empty line
-wordwrap_io([ NextWord | Rest ], [ "" | PreviousLines ], Number) ->
-    wordwrap_io(Rest, [ NextWord | PreviousLines ], Number);
-% Or to a line that's already partially full. There are two cases:
-% 1. The word fits
-wordwrap_io([ NextWord | Rest ], [ CurrentLine | PreviousLines ], Number)
-  when erlang:length(NextWord) + erlang:length(CurrentLine) < Number ->
-    %wordwrap_io(Rest, [ NextWord, " ", CurrentLine | PreviousLines ], Number);
-    wordwrap_io(Rest, [ lists:flatten([CurrentLine," " ,NextWord]) | PreviousLines ], Number);    
-% 2. The word doesn't fit, so we create a new line 
-wordwrap_io([ NextWord | Rest], [ CurrentLine | PreviousLines ], Number) ->
-        wordwrap_io(Rest, [ NextWord, "\n", CurrentLine | PreviousLines ], Number).
-
+% No more input, we're done
+wordwrap([], Acc, WordAcc, _LineLength, _WrapAt) ->
+    lists:reverse(WordAcc ++ Acc);
+% Premature newline
+wordwrap([$\n | Rest], Acc, WordAcc, _LineLength, WrapAt) ->
+    wordwrap(Rest, [$\n | WordAcc ++ Acc], [], 0, WrapAt);
+% Hit the wrap length at a space character. Add a newline
+wordwrap([$\  | Rest], Acc, WordAcc, WrapAt, WrapAt) ->
+    wordwrap(Rest, [$\n | WordAcc ++ Acc], [], 0, WrapAt);
+% Hit a space character before the wrap length. Keep going
+wordwrap([$\  | Rest], Acc, WordAcc, LineLength, WrapAt) ->
+    wordwrap(Rest, [$\  | WordAcc ++ Acc], [], LineLength + 1 + erlang:length(WordAcc), WrapAt);
+% Overflowed the current line while building a word
+wordwrap([C | Rest], Acc, WordAcc, 0, WrapAt) when erlang:length(WordAcc) > WrapAt ->
+    wordwrap(Rest, Acc, [C | WordAcc], 0, WrapAt);
+wordwrap([C | Rest], Acc, WordAcc, LineLength, WrapAt) when erlang:length(WordAcc) + LineLength > WrapAt ->
+    wordwrap(Rest, [$\n | Acc], [C | WordAcc], 0, WrapAt);
+% Just building a word...
+wordwrap([C | Rest], Acc, WordAcc, LineLength, WrapAt) ->
+    wordwrap(Rest, Acc, [C | WordAcc], LineLength, WrapAt).
 
 % Taken from quote_plus of mochiweb_util
 
@@ -911,6 +977,46 @@ urlencode([C | Rest], Acc) ->
     <<Hi:4, Lo:4>> = <<C>>,
     urlencode(Rest, [hexdigit(Lo), hexdigit(Hi), $\% | Acc]).
 
+%% @doc Converts URLs in text into clickable links.
+%%TODO: Autoescape not yet implemented
+urlize(Input) when is_binary(Input) ->
+    urlize(binary_to_list(Input),0);
+urlize(Input) ->
+    urlize(Input,0).
+
+urlize(Input, Trunc) when is_binary(Input) ->
+    urlize(binary_to_list(Input),Trunc);
+urlize(Input, Trunc) ->
+    {ok,RE} = re:compile("(([[:alpha:]]+://|www\.)[^<>[:space:]]+[[:alnum:]/])"),
+    {match,Matches} = re:run(Input,RE,[global]),
+    Indexes = lists:map(fun(Match) -> lists:nth(2,Match) end, Matches),
+    Domains = lists:map(fun({Start, Length}) -> lists:sublist(Input, Start+1, Length) end, Indexes),
+    URIDomains = lists:map(fun(Domain) -> addDefaultURI(Domain) end, Domains),
+    case Trunc == 0 of
+        true ->
+            DomainsTrunc = Domains;
+        false ->
+            DomainsTrunc = lists:map(fun(Domain) -> string:concat( string:substr(Domain,1,Trunc-3), "...") end, Domains)
+    end,
+    ReplaceList = lists:zip(URIDomains,DomainsTrunc),
+    ReplaceStrings = lists:map(fun({URIDomain,Domain}) -> lists:flatten(io_lib:format("<a href=\"~s\" rel=\"nofollow\">~s</a>",[URIDomain,Domain])) end, ReplaceList),
+    Template = re:replace(Input,"(([[:alpha:]]+://|www\.)[^<>[:space:]]+[[:alnum:]/])", "~s", [global,{return,list}]),
+    Result = lists:flatten(io_lib:format(Template,ReplaceStrings)),
+    Result.
+
+%% @doc Converts URLs into clickable links just like urlize, but truncates URLs longer than the given character limit.
+urlizetrunc(Input, Trunc) ->
+    urlize(Input, Trunc).
+
+addDefaultURI(Domain) ->
+    case string:str(Domain,"://") of
+        0 ->
+            Domain1 = string:concat("http://",Domain);
+        _ ->
+            Domain1 = Domain
+    end,
+    Domain1.
+
 hexdigit(C) when C < 10 -> $0 + C;
 hexdigit(C) when C < 16 -> $A + (C - 10).
 
@@ -921,69 +1027,69 @@ process_binary_match(Pre, Insertion, SizePost, Post) ->
         {_, 0} -> [Pre, Insertion];
         _ -> [Pre, Insertion, 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
-        end.
+    %%       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
+    end.
 
 %% unjoin == split in other languages; inverse of join
-%% from http://www.erlang.org/pipermail/erlang-questions/2008-October/038896.html
+%%FROM: http://www.erlang.org/pipermail/erlang-questions/2008-October/038896.html
 unjoin(String, []) ->
-     unjoin0(String);
+    unjoin0(String);
 unjoin(String, [Sep]) when is_integer(Sep) ->
-     unjoin1(String, Sep);
+    unjoin1(String, Sep);
 unjoin(String, [C1,C2|L]) when is_integer(C1), is_integer(C2) ->
-     unjoin2(String, C1, C2, L).
+    unjoin2(String, C1, C2, L).
 
 %% Split a string at "", which is deemed to occur _between_
 %% adjacent characters, but queerly, not at the beginning
 %% or the end.
 
 unjoin0([C|Cs]) ->
-     [[C] | unjoin0(Cs)];
+    [[C] | unjoin0(Cs)];
 unjoin0([]) ->
-     [].
+    [].
 
 %% Split a string at a single character separator.
 
 unjoin1(String, Sep) ->
-     unjoin1_loop(String, Sep, "").
+    unjoin1_loop(String, Sep, "").
 
 unjoin1_loop([Sep|String], Sep, Rev) ->
-     [lists:reverse(Rev) | unjoin1(String, Sep)];
+    [lists:reverse(Rev) | unjoin1(String, Sep)];
 unjoin1_loop([Chr|String], Sep, Rev) ->
-     unjoin1_loop(String, Sep, [Chr|Rev]);
+    unjoin1_loop(String, Sep, [Chr|Rev]);
 unjoin1_loop([], _, Rev) ->
-     [lists:reverse(Rev)].
+    [lists:reverse(Rev)].
 
 %% Split a string at a multi-character separator
 %% [C1,C2|L].  These components are split out for
 %% a fast match.
 
 unjoin2(String, C1, C2, L) ->
-     unjoin2_loop(String, C1, C2, L, "").
+    unjoin2_loop(String, C1, C2, L, "").
 
 unjoin2_loop([C1|S = [C2|String]], C1, C2, L, Rev) ->
-     case unjoin_prefix(L, String)
-       of no   -> unjoin2_loop(S, C1, C2, L, [C1|Rev])
-        ; Rest -> [lists:reverse(Rev) | unjoin2(Rest, C1, C2, L)]
-     end;
+    case unjoin_prefix(L, String)
+        of no   -> unjoin2_loop(S, C1, C2, L, [C1|Rev])
+            ; Rest -> [lists:reverse(Rev) | unjoin2(Rest, C1, C2, L)]
+    end;
 unjoin2_loop([Chr|String], C1, C2, L, Rev) ->
-     unjoin2_loop(String, C1, C2, L, [Chr|Rev]);
+    unjoin2_loop(String, C1, C2, L, [Chr|Rev]);
 unjoin2_loop([], _, _, _, Rev) ->
-     [lists:reverse(Rev)].
+    [lists:reverse(Rev)].
 
 unjoin_prefix([C|L], [C|S]) -> unjoin_prefix(L, S);
 unjoin_prefix([],    S)     -> S;

+ 0 - 0
src/erlydtl/erlydtl_i18n.erl → src/erlydtl_i18n.erl


+ 1 - 0
src/erlydtl/erlydtl_parser.yrl → src/erlydtl_parser.yrl

@@ -262,6 +262,7 @@ EndAutoEscapeBraced -> open_tag endautoescape_keyword close_tag.
 
 Filter -> identifier : ['$1'].
 Filter -> identifier ':' Literal : ['$1', '$3'].
+Filter -> identifier ':' Variable : ['$1', '$3'].
 
 Literal -> string_literal : '$1'.
 Literal -> number_literal : '$1'.

+ 0 - 0
src/erlydtl/erlydtl_runtime.erl → src/erlydtl_runtime.erl


+ 0 - 0
src/erlydtl/erlydtl_scanner.erl → src/erlydtl_scanner.erl


+ 0 - 0
src/erlydtl/filter_lib/erlydtl_dateformat.erl → src/filter_lib/erlydtl_dateformat.erl


+ 13 - 17
src/erlydtl/filter_lib/erlydtl_slice.erl → src/filter_lib/erlydtl_slice.erl

@@ -1,6 +1,6 @@
 -module(erlydtl_slice).
 
--export([slice/2]).
+-export([slice/2,slice_input_cases/7]).
 
 -define(TEST,"").
 -define(NOTEST,1).
@@ -28,21 +28,22 @@ slice_input_cases(_List,ListLength,Start,false,[],false,[]) when Start < 0, Star
     throw(indexError);
 %[-1]
 slice_input_cases(List,ListLength,Start,false,[],false,[]) when Start<0 ->
-    S = start_transform(ListLength,Start+ListLength+1), 
-    %E = end_transform(ListLength,Start+ListLength+2), 
-    Step = 1, 
+    S = start_transform(ListLength,Start+ListLength+1),   
     LowerBound = single_index_bounds(S), 
-    ?debugFmt("slice_transform exit: ~p, ~p, ~p, ~p~n",[List,S,E,Step]), 
-    [Result] = lists:sublist(List,LowerBound,Step), 
-    Result;
+    ?debugFmt("slice_transform exit: ~p, ~p, ~p~n",[List,S,LowerBound]), 
+    %[Result] = lists:sublist(List,LowerBound,Step),
+    lists:nth(LowerBound,List);
 %[1]
 slice_input_cases(List,ListLength,Start,false,[],false,[]) ->
-    S = start_transform(ListLength,Start+1), 
+    %S = start_transform(ListLength,Start+1), 
     %E = end_transform(ListLength,Start+2), 
-    Step = 1, 
-    LowerBound = single_index_bounds(S), 
-    ?debugFmt("slice_transform exit: ~p, ~p, ~p, ~p~n",[List,S,E,Step]), 
-    [Result] = lists:sublist(List,LowerBound,Step), 
+    Step = 1,
+    End = Start + 1,
+    {Start1,End1,Step1} = index_defaults(ListLength,Start,End,Step),
+    S = start_transform(ListLength,Start1), 
+    E = end_transform(ListLength,End1), 
+    ?debugFmt("slice_transform: S,E,Step1: ~p,~p,~p~n",[S,E,Step1]),
+    [Result] = slice_list(List,ListLength,S,false,E,false,Step1),
     Result;
 %slice_transform(List, ListLength, Start, C1, End, C2, Step) when End < 0, Step > 0 ->
 %    [];
@@ -79,7 +80,6 @@ slice_list(List,ListLength,Start,_C1,End,_C2,Step) when Step > 0 ->
     {LowerBound,UpperBound} = index_bounds(Step,ListLength,Start,End), 
     ?debugFmt("LowerBound+1, UpperBound+1, UpperBound - LowerBound + 1: ~p, ~p, ~p~n",[LowerBound+1,UpperBound,UpperBound-LowerBound]), 
     BoundList = lists:sublist(List,LowerBound+1,UpperBound-LowerBound), 
-    %{ok, lists:map(fun(N) -> lists:nth(N, List) end, lists:sort(BoundList)) };
     SequenceList = lists:seq(1,erlang:length(BoundList),Step), 
     lists:map(fun (N) -> lists:nth(N,BoundList) end,SequenceList);
 slice_list(List,ListLength,Start,_C1,End,_C2,Step) when Step < 0 ->    
@@ -192,10 +192,6 @@ end_transform(ListLength, End) ->
 
 cast_to_integer([]) ->
     [];
-%% cast_to_integer(Input) when is_integer(Input) ->
-%%         Input;
-%% cast_to_integer(Input) when is_float(Input) ->
-%%         erlang:round(Input);
 cast_to_integer(Input) when is_list(Input)->
         case lists:member($., Input) of
                 true ->

+ 0 - 0
src/erlydtl/i18n/Makefile → src/i18n/Makefile


+ 0 - 0
src/erlydtl/i18n/i18n_manager.erl → src/i18n/i18n_manager.erl


+ 0 - 0
src/erlydtl/i18n/po_generator.erl → src/i18n/po_generator.erl


+ 0 - 0
src/erlydtl/i18n/po_scanner.erl → src/i18n/po_scanner.erl


+ 0 - 0
src/erlydtl/i18n/sources_parser.erl → src/i18n/sources_parser.erl


+ 0 - 0
examples/docroot/autoescape → tests/input/autoescape


+ 0 - 0
examples/docroot/base → tests/input/base


+ 0 - 0
examples/docroot/comment → tests/input/comment


+ 0 - 0
examples/docroot/custom_call → tests/input/custom_call


+ 0 - 0
examples/docroot/custom_tag → tests/input/custom_tag


+ 0 - 0
examples/docroot/cycle → tests/input/cycle


+ 0 - 0
examples/docroot/extends → tests/input/extends


+ 0 - 0
examples/docroot/extends_path → tests/input/extends_path


+ 0 - 0
examples/docroot/extends_path2 → tests/input/extends_path2


+ 0 - 0
examples/docroot/filters → tests/input/filters


+ 0 - 0
examples/docroot/for → tests/input/for


+ 0 - 0
examples/docroot/for_list → tests/input/for_list


+ 0 - 0
examples/docroot/for_list_preset → tests/input/for_list_preset


+ 0 - 0
examples/docroot/for_preset → tests/input/for_preset


+ 0 - 0
examples/docroot/for_records → tests/input/for_records


+ 0 - 0
examples/docroot/for_records_preset → tests/input/for_records_preset


+ 0 - 0
examples/docroot/for_tuple → tests/input/for_tuple


+ 0 - 0
examples/docroot/if → tests/input/if


+ 0 - 0
examples/docroot/if_preset → tests/input/if_preset


+ 0 - 0
examples/docroot/ifequal → tests/input/ifequal


+ 0 - 0
examples/docroot/ifequal_preset → tests/input/ifequal_preset


+ 0 - 0
examples/docroot/ifnotequal → tests/input/ifnotequal


+ 0 - 0
examples/docroot/ifnotequal_preset → tests/input/ifnotequal_preset


+ 0 - 0
examples/docroot/include → tests/input/include


+ 0 - 0
examples/docroot/include.html → tests/input/include.html


+ 0 - 0
examples/docroot/include_path → tests/input/include_path


+ 0 - 0
examples/docroot/include_template → tests/input/include_template


+ 0 - 0
examples/docroot/now → tests/input/now


+ 0 - 0
examples/docroot/path1/base1 → tests/input/path1/base1


+ 0 - 0
examples/docroot/path1/base2 → tests/input/path1/base2


+ 0 - 0
examples/docroot/path1/include1 → tests/input/path1/include1


+ 0 - 0
examples/docroot/path1/template1 → tests/input/path1/template1


+ 0 - 0
examples/docroot/path2/base2 → tests/input/path2/base2


+ 0 - 0
examples/docroot/path2/template2 → tests/input/path2/template2


+ 0 - 0
examples/docroot/trans → tests/input/trans


+ 0 - 0
examples/docroot/var → tests/input/var


+ 0 - 0
examples/docroot/var_preset → tests/input/var_preset


+ 49 - 0
tests/py/erlydtl_python_test.py

@@ -0,0 +1,49 @@
+from erlport import Port, Protocol, String, Atom
+from erlport.erlterms import decode
+from django.template import Context, Template
+from django.conf import settings
+import types
+
+# Inherit custom protocol from erlport.Protocol
+class ErlydtlProtocol(Protocol):
+    
+    def handle_slice(self, command):
+        list = command[0]
+        slice = "%s" % String(command[1])
+        slice_test1_string = "%s[%s]" % (list,slice)
+        try:
+            result_list = eval(slice_test1_string)
+        except(IndexError):
+            result_list = Atom("indexError")
+        except:
+            result_list = Atom("error")
+        #print "result_list: %s" % (result_list)
+        return result_list
+    
+    #@doc Start list with 'object' to pass in a python term along with import statement in the form:
+    #@doc object|module to import|term (three strings delimited by "|"
+    def handle_template(self, command):
+        file = open("/tmp/debug.txt",'a')
+        template_text = "%s" % String(command[0])
+        #value = "%s" % String(command[1])
+        value = "%s" % String(command[1])
+        value_split = value.split("|")
+        
+        if value_split[0] == u"object":
+            module = value_split[1]
+            exec "import %s" % module
+            value = eval(value_split[2])
+            #term(((term.year, term.month, term.day), (term.hour, term.minute, term.second)))
+        c = Context({"value":value})
+        t = Template(template_text)
+        result = String(t.render(c))
+        file.close()
+        return result
+
+
+if __name__ == "__main__":
+    settings.configure(DEBUG=True, TEMPLATE_DEBUG=True)
+    proto = ErlydtlProtocol()
+    # Run protocol with port open on STDIO
+    proto.run(Port(use_stdio=True))
+    

BIN
tests/py/erlydtl_python_test.pyc


+ 29 - 0
tests/py/python_dtl_setup.txt

@@ -0,0 +1,29 @@
+from erlport import Port, Protocol, String, Atom
+from erlport.erlterms import decode
+from django.template import Context, Template
+from django.conf import settings
+import types
+from erlydtl_python_test import ErlydtlProtocol as proto
+settings.configure(DEBUG=True, TEMPLATE_DEBUG=True)
+ep = proto()
+
+ep.handle_template([u"{{ value|date }}",u"object,datetime,datetime.date.today()"])
+ep.handle_template([u"{{ value|time:\"H:i\" }}",u"object,datetime,datetime.datetime.now()"])
+
+
+DateFormat = erlydtl_filters_tests:erlydtl_render("{{ value|random }}", [ {value, ["b","c","d","D","F","j","l","L","m","M","n","N","t","w","W","y","Y","z"]} ] ).
+Template = "{{ value|date:\"" ++ DateFormat ++ "\" }}".
+Value =  { {2010,12,1}, {10,11,12} }.
+Port = erlydtl_python_test:start().
+PyDate = lists:flatten(io_lib:format("object|datetime|~s", [erlydtl_filters_tests:python_datetime_encode(Value)])).
+erlydtl_filters_tests:py_template(Port, Template, PyDate).
+
+
+from django.template import Context, Template
+from django.conf import settings
+settings.configure(DEBUG=True, TEMPLATE_DEBUG=True)
+from erlydtl_python_test import ErlydtlProtocol as proto
+ep = proto()
+c = Context({"value": "Check out www.yahoo.com"})
+t = Template("{{ value|urlize }}")
+t.render(c)

+ 15 - 0
tests/py/run_erl_node.sh

@@ -0,0 +1,15 @@
+#! /bin/sh
+
+# A test case for testing packing/unpacking of erlang-terms:
+#
+# A message is sent from an erlang node to a python node.
+# That message is echoed back to the erlang node, which checks
+# if the received message matches the original message.
+#
+
+# First make sure epmd is up and running. (needed by the python-node)
+erl -noinput -detach -sname ensure_epmd_started@localhost -s erlang halt
+
+
+erl -sname enode1@localhost \
+    -setcookie cookie -pa ../../../../ebin/

+ 0 - 0
src/tests/erlydtl_dateformat_tests.erl → tests/src/erlydtl_dateformat_tests.erl


+ 0 - 0
src/tests/erlydtl_example_variable_storage.erl → tests/src/erlydtl_example_variable_storage.erl


+ 2 - 2
src/tests/erlydtl_functional_tests.erl → tests/src/erlydtl_functional_tests.erl

@@ -271,7 +271,7 @@ test_render(Name, Module) ->
 
 
 templates_docroot() ->
-    filename:join([erlydtl_deps:get_base_dir(), "examples", "docroot"]).
+    filename:join([erlydtl_deps:get_base_dir(), "tests", "input"]).
 
 templates_outdir() ->   
-    filename:join([erlydtl_deps:get_base_dir(), "examples", "rendered_output"]).
+    filename:join([erlydtl_deps:get_base_dir(), "tests", "output"]).

+ 377 - 39
src/tests/erlydtl_unittests.erl → tests/src/erlydtl_unittests.erl

@@ -1,7 +1,7 @@
 -module(erlydtl_unittests).
-
+ 
 -export([run_tests/0]).
-
+ 
 tests() ->
     [
         {"vars", [
@@ -84,12 +84,12 @@ tests() ->
                {"now functional",
                   <<"It is the {% now \"jS o\\f F Y\" %}.">>, [{var1, ""}], generate_test_date()}
             ]},
-	{"trans", 
-		[
-		{"trans functional default locale",
-		  <<"Hello {% trans \"Hi\" %}">>, [], <<"Hello Hi">>
-		},
-		{"trans functional reverse locale",
+        {"trans",
+                [
+                {"trans functional default locale",
+                  <<"Hello {% trans \"Hi\" %}">>, [], <<"Hello Hi">>
+                },
+                {"trans functional reverse locale",
                     <<"Hello {% trans \"Hi\" %}">>, [], none, [{locale, "reverse"}], <<"Hello iH">>
                 },
                 {"trans literal at run-time",
@@ -104,7 +104,7 @@ tests() ->
                 {"trans variable at run-time: No-op",
                     <<"Hello {% trans var1 noop %}">>, [{var1, "Hi"}], fun("Hi") -> "Konichiwa" end, [],
                     <<"Hello Hi">>}
-	]},
+        ]},
         {"if", [
                 {"If/else",
                     <<"{% if var1 %}boo{% else %}yay{% endif %}">>, [{var1, ""}], <<"yay">>},
@@ -190,19 +190,19 @@ tests() ->
                     <<"{% if var1 > 2 %}yay{% endif %}">>, [{var1, 3}], <<"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",
@@ -212,10 +212,10 @@ tests() ->
             ]},
         {"if complex bool", [
                 {"If (true or false) and true",
-                    <<"{% if (var1 or var2) and var3 %}yay{% endif %}">>, 
+                    <<"{% 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 %}">>, 
+                    <<"{% if var1 or (var2 and var3) %}yay{% endif %}">>,
                     [{var1, true}, {var2, false}, {var3, true}], <<"yay">>}
             ]},
         {"for", [
@@ -235,16 +235,16 @@ tests() ->
                     <<"{% 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 %}">>, 
+                    <<"{% 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 %}">>, 
+                    <<"{% 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 %}">>, 
+                    <<"{% 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 %}">>, 
+                    <<"{% 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 %}">>,
@@ -356,13 +356,13 @@ tests() ->
                     <<"{{ var1|addslashes }}">>, [{var1, "Jimmy's \"great\" meats'n'things"}],
                     <<"Jimmy\\'s \\\"great\\\" meats\\'n\\'things">>},
                 {"|capfirst",
-                    <<"{{ var1|capfirst }}">>, [{var1, "dana boyd"}], 
+                    <<"{{ var1|capfirst }}">>, [{var1, "dana boyd"}],
                     <<"Dana boyd">>},
                 {"|center:10",
-                    <<"{{ var1|center:10 }}">>, [{var1, "MB"}], 
+                    <<"{{ var1|center:10 }}">>, [{var1, "MB"}],
                     <<"    MB    ">>},
                 {"|center:1",
-                    <<"{{ var1|center:1 }}">>, [{var1, "KBR"}], 
+                    <<"{{ var1|center:1 }}">>, [{var1, "KBR"}],
                     <<"B">>},
                 {"|cut:\" \"",
                     <<"{{ var1|cut:\" \" }}">>, [{var1, "String with spaces"}],
@@ -401,11 +401,24 @@ tests() ->
                 {"|filesizeformat (GB)",
                     <<"{{ var1|filesizeformat }}">>, [{var1, 1024 * 1024 * 1024}], <<"1.0 GB">>},
                 {"|first",
-                    <<"{{ var1|first }}">>, [{var1, "James"}], 
+                    <<"{{ var1|first }}">>, [{var1, "James"}],
                     <<"J">>},
                 {"|fix_ampersands",
-                    <<"{{ var1|fix_ampersands }}">>, [{var1, "Ben & Jerry's"}], 
+                    <<"{{ var1|fix_ampersands }}">>, [{var1, "Ben & Jerry's"}],
                     <<"Ben &amp; 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 &amp; Jerry&#039;s &lt;=&gt; &quot;The World&#039;s Best Ice Cream&quot;">>},
@@ -437,33 +450,320 @@ tests() ->
                 {"|length",
                     <<"{{ var1|length }}">>, [{var1, "antidisestablishmentarianism"}],
                     <<"28">>},
+                {"|linebreaks",
+                    <<"{{ var1|linebreaks }}">>, [{var1, "Joel\nis a slug"}],
+                    <<"<p>Joel<br />is a slug</p>">>},               
                 {"|linebreaksbr",
                     <<"{{ var1|linebreaksbr }}">>, [{var1, "One\nTwo\n\nThree\n\n\n"}],
                     <<"One<br />Two<br /><br />Three<br /><br /><br />">>},
                 {"|linebreaksbr",
                     <<"{{ \"One\\nTwo\\n\\nThree\\n\\n\\n\"|linebreaksbr }}">>, [],
-                    <<"One<br />Two<br /><br />Three<br /><br /><br />">>},
+                    <<"One<br />Two<br /><br />Three<br /><br /><br />">>},             
+                {"|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, "<B>Joel</B> <button>is</button> a <span>slug</span>"}],
+                    <<"<B>Joel</B> <button>is</button> a slug">>},
                 {"|rjust:10",
                     <<"{{ var1|rjust:10 }}">>, [{var1, "Bush"}],
                     <<"      Bush">>},
-                {"|phone2numeric",
+                %%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      ">>},
+
+                {"|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, "<b>Joel</b> <button>is</button> a <span>slug</span>"}],
+                    <<"Joel is a slug">>},
+                {"|striptags",
+                     <<"{{ var|striptags }}">>, [{var, "<B>Joel</B> <button>is</button> a <span>slug</Span>"}],
+                    <<"Joel is a slug">>},
+                {"|striptags",
+                     <<"{{ var|striptags }}">>, [{var, "Check out <a href=\"http://www.djangoproject.com\" rel=\"nofollow\">http://www.djangoproject.com</a>"}],
+                    <<"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">>},
                 {"|truncatewords:0",
                     <<"{{ var1|truncatewords:0 }}">>, [{var1, "Empty Me"}],
                     <<"">>},
@@ -479,9 +779,47 @@ tests() ->
                 {"|urlencode",
                     <<"{{ url|urlencode }}">>, [{url, "You #$*@!!"}],
                     <<"You+%23%24%2A%40%21%21">>},
+                {"|urlize",    
+                    <<"{{ var|urlize }}">>, [{var, "Check out www.djangoproject.com"}],
+                    <<"Check out <a href=\"http://www.djangoproject.com\" rel=\"nofollow\">www.djangoproject.com</a>">>},
+                {"|urlize",    
+                    <<"{{ var|urlize }}">>, [{var, "Check out http://www.djangoproject.com"}],
+                    <<"Check out <a href=\"http://www.djangoproject.com\" rel=\"nofollow\">http://www.djangoproject.com</a>">>},
+                {"|urlize",    
+                    <<"{{ var|urlize }}">>, [{var, "Check out \"http://www.djangoproject.com\""}],
+                    <<"Check out \"<a href=\"http://www.djangoproject.com\" rel=\"nofollow\">http://www.djangoproject.com</a>\"">>},
+                {"|urlizetrunc:15",    
+                    <<"{{ var|urlizetrunc:15 }}">>, [{var, "Check out www.djangoproject.com"}],
+                    <<"Check out <a href=\"http://www.djangoproject.com\" rel=\"nofollow\">www.djangopr...</a>">>},    
                 {"|wordcount",
                     <<"{{ words|wordcount }}">>, [{words, "Why Hello There!"}],
-                    <<"3">>}
+                    <<"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",
@@ -556,35 +894,35 @@ tests() ->
                 <<"baz">>}
         ]}
     ].
-
+ 
 run_tests() ->
     io:format("Running unit tests...~n"),
     Failures = lists:foldl(
         fun({Group, Assertions}, GroupAcc) ->
                 io:format(" Test group ~p...~n", [Group]),
                 lists:foldl(fun
-                        ({Name, DTL, Vars, Output}, Acc) -> 
+                        ({Name, DTL, Vars, Output}, Acc) ->
                             process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, []),
                                 Vars, none, Output, Acc, Group, Name);
-                        ({Name, DTL, Vars, Dictionary, Output}, Acc) -> 
-                            process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, []), 
+                        ({Name, DTL, Vars, Dictionary, Output}, Acc) ->
+                            process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, []),
                                 Vars, Dictionary, Output, Acc, Group, Name);
-                        ({Name, DTL, Vars, Dictionary, CompilerOpts, Output}, Acc) -> 
-                            process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts), 
+                        ({Name, DTL, Vars, Dictionary, CompilerOpts, Output}, Acc) ->
+                            process_unit_test(erlydtl:compile(DTL, erlydtl_running_test, CompilerOpts),
                                 Vars, Dictionary, Output, Acc, Group, Name)
                             end, GroupAcc, Assertions)
         end, [], tests()),
-    
+ 
     io:format("Unit test failures: ~p~n", [lists:reverse(Failures)]).
-
+ 
 process_unit_test(CompiledTemplate, Vars, Dictionary, Output,Acc, Group, Name) ->
-	case CompiledTemplate of
+        case CompiledTemplate of
              {ok, _} ->
                    {ok, IOList} = erlydtl_running_test:render(Vars, Dictionary),
                    {ok, IOListBin} = erlydtl_running_test:render(vars_to_binary(Vars), Dictionary),
                    case {iolist_to_binary(IOList), iolist_to_binary(IOListBin)} of
                         {Output, Output} ->
-	                          Acc; 
+                                  Acc;
                         {Output, Unexpected} ->
                                   [{Group, Name, 'binary', Unexpected, Output} | Acc];
                         {Unexpected, Output} ->
@@ -596,8 +934,8 @@ process_unit_test(CompiledTemplate, Vars, Dictionary, Output,Acc, Group, Name) -
              Err ->
                    [{Group, Name, Err} | Acc]
         end.
-
-
+ 
+ 
 vars_to_binary(Vars) when is_list(Vars) ->
     lists:map(fun
             ({Key, [H|_] = Value}) when is_tuple(H) ->
@@ -609,7 +947,7 @@ vars_to_binary(Vars) when is_list(Vars) ->
         end, Vars);
 vars_to_binary(Vars) ->
     Vars.
-
+ 
 generate_test_date() ->
     {{Y,M,D}, _} = erlang:localtime(),
     MonthName = [

+ 0 - 0
src/tests/gettext.erl → tests/src/gettext.erl


+ 0 - 0
src/tests/i18n/sources_parser_unittests.erl → tests/src/sources_parser_unittests.erl