Browse Source

Eleven new filters

* New filters from standard Django:
    + addslashes
    + cut
    + default
    + divisibleby
    + filesizeformat
    + get_digit
    + phone2numeric
    + random
    + slugify
    + title
    + wordcount
Evan Miller 15 years ago
parent
commit
d957c3bc0d
2 changed files with 219 additions and 4 deletions
  1. 177 3
      src/erlydtl/erlydtl_filters.erl
  2. 42 1
      src/tests/erlydtl_unittests.erl

+ 177 - 3
src/erlydtl/erlydtl_filters.erl

@@ -36,16 +36,22 @@
 -author('emmiller@gmail.com').
 
 -export([add/2, 
+        addslashes/1,
         capfirst/1, 
         center/2, 
+        cut/2,
         date/2, 
+        default/2,
         default_if_none/2, 
+        divisibleby/2,
         escapejs/1,
+        filesizeformat/1,
         first/1, 
         fix_ampersands/1, 
         force_escape/1, 
         format_integer/1, 
         format_number/1,
+        get_digit/2,
         join/2, 
         last/1, 
         length/1, 
@@ -53,10 +59,15 @@
         linebreaksbr/1, 
         ljust/2,
         lower/1, 
+        phone2numeric/1,
+        random/1,
         rjust/2, 
+        slugify/1,
+        title/1,
         truncatewords/2, 
         upper/1, 
-        urlencode/1]).
+        urlencode/1,
+        wordcount/1]).
 
 -define(NO_ENCODE(C), ((C >= $a andalso C =< $z) orelse
         (C >= $A andalso C =< $Z) orelse
@@ -64,6 +75,10 @@
         (C =:= $\. orelse C =:= $- 
         orelse C =:= $~ orelse C =:= $_))).
 
+-define(KILOBYTE, 1024).
+-define(MEGABYTE, (1024 * ?KILOBYTE)).
+-define(GIGABYTE, (1024 * ?MEGABYTE)).
+
 %% @doc Adds a number to the value.
 add(Input, Number) when is_binary(Input) ->
     list_to_binary(add(binary_to_list(Input), Number));
@@ -72,6 +87,12 @@ add(Input, Number) when is_list(Input) ->
 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];
@@ -84,6 +105,12 @@ center(Input, Number) when is_binary(Input) ->
 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));
@@ -95,17 +122,49 @@ date(Input, _FormatStr) when is_list(Input) ->
     io:format("Unexpected date parameter : ~p~n", [Input]),
     "".
 
+%% @doc If value evaluates to `false', use given default. Otherwise, use the value.
+default(Input, Default) ->
+    case erlydtl_runtime:is_false(Input) of
+        true -> Default;
+        false -> Input
+    end.
+
+%% @doc If (and only if) value is `undefined', use given default. Otherwise, use the value.
 default_if_none(undefined, Default) ->
     Default;
 default_if_none(Input, _) ->
     Input.
 
+%% @doc Returns `true' if the value is divisible by the argument.
+divisibleby(Input, Divisor) when is_binary(Input) ->
+    divisibleby(binary_to_list(Input), Divisor);
+divisibleby(Input, Divisor) when is_list(Input) ->
+    divisibleby(list_to_integer(Input), Divisor);
+divisibleby(Input, Divisor) when is_list(Divisor) ->
+    divisibleby(Input, list_to_integer(Divisor));
+divisibleby(Input, Divisor) when is_integer(Input), is_integer(Divisor) ->
+    Input rem Divisor =:= 0.
+
 %% @doc Escapes characters for use in JavaScript strings.
 escapejs(Input) when is_binary(Input) ->
     escapejs(Input, 0);
 escapejs(Input) when is_list(Input) ->
     escapejs(Input, []).
 
+%% @doc Format the value like a human-readable file size.
+filesizeformat(Input) when is_binary(Input) ->
+    filesizeformat(binary_to_list(Input));
+filesizeformat(Input) when is_list(Input) ->
+    filesizeformat(list_to_integer(Input));
+filesizeformat(Bytes) when is_integer(Bytes), Bytes >= ?GIGABYTE->
+    filesizeformat(Bytes / ?GIGABYTE, "GB");
+filesizeformat(Bytes) when is_integer(Bytes), Bytes >= ?MEGABYTE ->
+    filesizeformat(Bytes / ?MEGABYTE, "MB");
+filesizeformat(Bytes) when is_integer(Bytes), Bytes >= ?KILOBYTE ->
+    filesizeformat(Bytes / ?KILOBYTE, "KB");
+filesizeformat(Bytes) when is_integer(Bytes) ->
+    integer_to_list(Bytes) ++ " bytes".
+
 %% @doc Returns the first item in a list.
 first([First|_Rest]) ->
     [First];
@@ -138,6 +197,20 @@ format_number(Input) when is_function(Input, 0) ->
 format_number(Input) ->
     Input.
 
+%% @doc Given a whole number, returns the requested digit, where 1 is the right-most digit.
+get_digit(Input, Digit) when is_binary(Input) ->
+    get_digit(binary_to_list(Input), Digit);
+get_digit(Input, Digit) when is_integer(Input) ->
+    get_digit(integer_to_list(Input), Digit);
+get_digit(Input, Digit) when is_list(Digit) ->
+    get_digit(Input, list_to_integer(Digit));
+get_digit(Input, Digit) when Digit > length(Input) ->
+    0;
+get_digit(Input, Digit) when Digit > 0 ->
+    lists:nth(Digit, lists:reverse(Input)) - $0;
+get_digit(Input, _) ->
+    Input.
+
 %% @doc Joins a list with a given separator.
 join(Input, Separator) when is_list(Input) ->
     join_io(Input, Separator).
@@ -184,12 +257,36 @@ lower(Input) when is_binary(Input) ->
 lower(Input) ->
     string:to_lower(Input).
 
+%% @doc Converts a phone number (possibly containing letters) to its numerical equivalent.
+phone2numeric(Input) when is_binary(Input) ->
+    phone2numeric(binary_to_list(Input));
+phone2numeric(Input) when is_list(Input) ->
+    phone2numeric(Input, []).
+
+%% @doc Returns a random item from the given list.
+random(Input) when is_list(Input) ->
+    lists:nth(random:uniform(erlang:length(Input)), Input);
+random(_) ->
+    "".
+
 %% @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));
 rjust(Input, Number) ->
     string:right(Input, Number).
 
+%% @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));
+slugify(Input) when is_list(Input) ->
+    slugify(Input, []).
+
+%% @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).
+
 %% @doc Truncates a string after a certain number of words.
 truncatewords(Input, Max) when is_binary(Input) ->
     list_to_binary(truncatewords(binary_to_list(Input), Max));
@@ -210,9 +307,29 @@ urlencode(Input) when is_binary(Input) ->
 urlencode(Input) when is_list(Input) ->
     urlencode(Input, []).
 
+%% @doc Returns the number of words.
+wordcount(Input) when is_binary(Input) ->
+    wordcount(binary_to_list(Input));
+wordcount(Input) when is_list(Input) ->
+    wordcount(Input, 0).
+
 
 % internal
 
+addslashes([], Acc) ->
+    lists:reverse(Acc);
+addslashes([H|T], Acc) when H =:= $"; H =:= $' -> 
+    addslashes(T, [H, $\\|Acc]);
+addslashes([H|T], Acc) ->
+    addslashes(T, [H|Acc]).
+
+cut([], _, Acc) ->
+    lists:reverse(Acc);
+cut([H|T], H, Acc) ->
+    cut(T, H, Acc);
+cut([H|T], Char, Acc) ->
+    cut(T, Char, [H|Acc]).
+
 escape(Binary, Index) when is_binary(Binary) ->
     case Binary of
         <<Pre:Index/binary, $<, Post/binary>> ->
@@ -266,6 +383,9 @@ escapejs(Binary, Index) when is_binary(Binary) ->
             Binary
     end.
 
+filesizeformat(Bytes, UnitStr) ->
+    lists:flatten(io_lib:format("~.1f ~s", [Bytes, UnitStr])).
+
 fix_ampersands(Input, Index) when is_binary(Input) ->
     case Input of
         <<Pre:Index/binary, $&, Post/binary>> ->
@@ -317,15 +437,69 @@ lower(Input, Index) ->
             Input
     end.
 
-truncatewords(Input, _WordsLeft, Acc) when length(Input) =:= 0 ->
+phone2numeric([], Acc) ->
+    lists:reverse(Acc);
+phone2numeric([H|T], Acc) when H >= $a, H =< $c; H >= $A, H =< $C ->
+    phone2numeric(T, [$2|Acc]);
+phone2numeric([H|T], Acc) when H >= $d, H =< $f; H >= $D, H =< $F ->
+    phone2numeric(T, [$3|Acc]);
+phone2numeric([H|T], Acc) when H >= $g, H =< $i; H >= $G, H =< $I ->
+    phone2numeric(T, [$4|Acc]);
+phone2numeric([H|T], Acc) when H >= $j, H =< $l; H >= $J, H =< $L ->
+    phone2numeric(T, [$5|Acc]);
+phone2numeric([H|T], Acc) when H >= $m, H =< $o; H >= $M, H =< $O ->
+    phone2numeric(T, [$6|Acc]);
+phone2numeric([H|T], Acc) when H >= $p, H =< $s; H >= $P, H =< $S ->
+    phone2numeric(T, [$7|Acc]);
+phone2numeric([H|T], Acc) when H >= $t, H =< $v; H >= $T, H =< $V ->
+    phone2numeric(T, [$8|Acc]);
+phone2numeric([H|T], Acc) when H >= $w, H =< $z; H >= $W, H =< $Z ->
+    phone2numeric(T, [$9|Acc]);
+phone2numeric([H|T], Acc) ->
+    phone2numeric(T, [H|Acc]).
+
+slugify([], Acc) ->
     lists:reverse(Acc);
-truncatewords(_Input, WordsLeft, Acc) when WordsLeft =:= 0 ->
+slugify([H|T], Acc) when H >= $A, H =< $Z ->
+    slugify(T, [H-$A+$a|Acc]);
+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], Acc) ->
+    slugify(T, 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).
+
+truncatewords([], _WordsLeft, Acc) ->
+    lists:reverse(Acc);
+truncatewords(_Input, 0, Acc) ->
     lists:reverse("..." ++ Acc);
 truncatewords([C1, C2|Rest], WordsLeft, Acc) when C1 =/= $\  andalso C2 =:= $\  ->
     truncatewords([C2|Rest], WordsLeft - 1, [C1|Acc]);
 truncatewords([C1|Rest], WordsLeft, Acc) ->
     truncatewords(Rest, WordsLeft, [C1|Acc]).
 
+wordcount([], Count) ->
+    Count;
+wordcount([C1], Count) when C1 =/= $\  ->
+    Count+1;
+wordcount([C1, C2|Rest], Count) when C1 =/= $\  andalso C2 =:= $\  ->
+    wordcount([C2|Rest], Count + 1);
+wordcount([_|Rest], Count) ->
+    wordcount(Rest, Count).
+
 % Taken from quote_plus of mochiweb_util
 
 urlencode(Input, Index) when is_binary(Input) ->

+ 42 - 1
src/tests/erlydtl_unittests.erl

@@ -340,6 +340,9 @@ tests() ->
                 {"|add:4",
                     <<"{{ one|add:4 }}">>, [{one, "1"}],
                     <<"5">>},
+                {"|addslashes",
+                    <<"{{ var1|addslashes }}">>, [{var1, "Jimmy's \"great\" meats'n'things"}],
+                    <<"Jimmy\\'s \\\"great\\\" meats\\'n\\'things">>},
                 {"|capfirst",
                     <<"{{ var1|capfirst }}">>, [{var1, "dana boyd"}], 
                     <<"Dana boyd">>},
@@ -349,6 +352,9 @@ tests() ->
                 {"|center:1",
                     <<"{{ var1|center:1 }}">>, [{var1, "KBR"}], 
                     <<"B">>},
+                {"|cut:\" \"",
+                    <<"{{ var1|cut:\" \" }}">>, [{var1, "String with spaces"}],
+                    <<"Stringwithspaces">>},
                 {"|date 1",
                    <<"{{ var1|date:\"jS F Y H:i\" }}">>,
                    [{var1, {1975,7,24}}],
@@ -357,13 +363,31 @@ tests() ->
                    <<"{{ var1|date:\"jS F Y H:i\" }}">>,
                    [{var1, {{1975,7,24}, {7,13,1}}}],
                    <<"24th July 1975 07:13">>},
+                {"|default:\"foo\" 1",
+                   <<"{{ var1|default:\"foo\" }}">>, [], <<"foo">>},
+                {"|default:\"foo\" 2",
+                    <<"{{ var1|default:\"foo\" }}">>, [{var1, "bar"}], <<"bar">>},
+                {"|default:\"foo\" 3",
+                    <<"{{ var1|default:\"foo\" }}">>, [{var1, "0"}], <<"foo">>},
                 {"|default_if_none:\"foo\"",
                    <<"{{ var1|default_if_none:\"foo\" }}">>, [], <<"foo">>},
                 {"|default_if_none:\"foo\" 2",
                     <<"{{ var1|default_if_none:\"foo\" }}">>, [{var1, "bar"}], <<"bar">>},
+                {"|divisibleby:\"3\"",
+                    <<"{% if var1|divisibleby:\"3\" %}yay{% endif %}">>, [{var1, 21}], <<"yay">>},
+                {"|divisibleby:\"3\"",
+                    <<"{% if var1|divisibleby:\"3\" %}yay{% endif %}">>, [{var1, 22}], <<"">>},
                 {"|escapejs",
                     <<"{{ var1|escapejs }}">>, [{var1, "Skip's \"Old-Timey\" Diner"}], 
                     <<"Skip\\'s \\\"Old-Timey\\\" Diner">>},
+                {"|filesizeformat (bytes)",
+                    <<"{{ var1|filesizeformat }}">>, [{var1, 1023}], <<"1023 bytes">>},
+                {"|filesizeformat (KB)",
+                    <<"{{ var1|filesizeformat }}">>, [{var1, 3487}], <<"3.4 KB">>},
+                {"|filesizeformat (MB)",
+                    <<"{{ var1|filesizeformat }}">>, [{var1, 6277098}], <<"6.0 MB">>},
+                {"|filesizeformat (GB)",
+                    <<"{{ var1|filesizeformat }}">>, [{var1, 1024 * 1024 * 1024}], <<"1.0 GB">>},
                 {"|first",
                     <<"{{ var1|first }}">>, [{var1, "James"}], 
                     <<"J">>},
@@ -387,6 +411,8 @@ tests() ->
                     <<"{{ var1|format_number }}">>, [{var1, fun() -> 29 end}], <<"29">>},
                 {"|format_number 6",
                     <<"{{ var1|format_number }}">>, [{var1, fun() -> fun() -> 31 end end}], <<"31">>},
+                {"|get_digit:\"2\"",
+                    <<"{{ var1|get_digit:\"2\" }}">>, [{var1, 42}], <<"4">>},
                 {"|join:\", \" (list)",
                     <<"{{ var1|join:\", \" }}">>, [{var1, ["Liberte", "Egalite", "Fraternite"]}],
                     <<"Liberte, Egalite, Fraternite">>},
@@ -411,9 +437,21 @@ tests() ->
                 {"|lower",
                     <<"{{ var1|lower }}">>, [{var1, "E. E. Cummings"}],
                     <<"e. e. cummings">>},
+                {"|random",
+                    <<"{{ var1|random }}">>, [{var1, ["foo", "foo", "foo"]}],
+                    <<"foo">>},
                 {"|rjust:10",
                     <<"{{ var1|rjust:10 }}">>, [{var1, "Bush"}],
                     <<"      Bush">>},
+                {"|phone2numeric",
+                    <<"{{ var1|phone2numeric }}">>, [{var1, "1-800-COLLECT"}],
+                    <<"1-800-2655328">>},
+                {"|slugify",
+                    <<"{{ var1|slugify }}">>, [{var1, "What The $#_! Was He Thinking?"}],
+                    <<"what-the-_-was-he-thinking">>},
+                {"|title",
+                    <<"{{ \"my title case\"|title }}">>, [],
+                    <<"My Title Case">>},
                 {"|truncatewords:0",
                     <<"{{ var1|truncatewords:0 }}">>, [{var1, "Empty Me"}],
                     <<"">>},
@@ -428,7 +466,10 @@ tests() ->
                     <<"THAT MAN HAS A GUN.">>},
                 {"|urlencode",
                     <<"{{ url|urlencode }}">>, [{url, "You #$*@!!"}],
-                    <<"You+%23%24%2A%40%21%21">>}
+                    <<"You+%23%24%2A%40%21%21">>},
+                {"|wordcount",
+                    <<"{{ words|wordcount }}">>, [{words, "Why Hello There!"}],
+                    <<"3">>}
             ]},
         {"filters_if", [
                 {"Filter if 1.1",