Browse Source

Filters galore! Thanks to Drew Gulino

Evan Miller 14 years ago
parent
commit
69c06cc297
3 changed files with 678 additions and 1 deletions
  1. 464 1
      src/erlydtl/erlydtl_filters.erl
  2. 213 0
      src/erlydtl/erlydtl_slice.erl
  3. 1 0
      src/erlydtl/i18n/sources_parser.erl

+ 464 - 1
src/erlydtl/erlydtl_filters.erl

@@ -34,6 +34,17 @@
 -module(erlydtl_filters).
 -author('rsaccon@gmail.com').
 -author('emmiller@gmail.com').
+-author('drew dot gulino at google dot com').
+ 
+-define(TEST,"").
+%-define(NOTEST,1).
+-define(NODEBUG,1).
+-include_lib("eunit/include/eunit.hrl").
+ 
+-ifdef(TEST).
+        -export([cast_to_float/1,cast_to_integer/1,stringformat_io/7,round/2,unjoin/2]).
+-endif.
+ 
 
 -export([add/2, 
         addslashes/1,
@@ -43,31 +54,59 @@
         date/2, 
         default/2,
         default_if_none/2, 
+        %dictsort/,
+        %dictsortreversed/,
         divisibleby/2,
+        %escape/,
         escapejs/1,
         filesizeformat/1,
         first/1, 
         fix_ampersands/1, 
+        floatformat/2,
         force_escape/1, 
         format_integer/1, 
         format_number/1,
         get_digit/2,
+        %iriencode/1,
         join/2, 
         last/1, 
         length/1, 
         length_is/2, 
+        linebreaks/1,
         linebreaksbr/1, 
+        linenumbers/1,
         ljust/2,
         lower/1, 
+        make_list/1, 
         phone2numeric/1,
+        pluralize/1,
+        pluralize/2,
+        %pprint/,
         random/1,
+        random_num/1,
+        random_range/1,
+        removetags/2,
         rjust/2, 
+        %safe/,
+        %safeseq/,
+        slice/2,
         slugify/1,
+        stringformat/2,
+        %striptags/,
+        time/2,
+        %timesince/,
+        %timeuntil/,
         title/1,
         truncatewords/2, 
+        %truncatewords_html/,
+        %unordered_list/,
         upper/1, 
         urlencode/1,
-        wordcount/1]).
+        %urlize/,
+        %urlizetrunc/,
+        wordcount/1,
+        wordwrap/2,
+        yesno/2]).
 
 -define(NO_ENCODE(C), ((C >= $a andalso C =< $z) orelse
         (C >= $A andalso C =< $Z) orelse
@@ -114,6 +153,8 @@ cut(Input, [Char]) when is_list(Input) ->
 %% @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));
+date(Input, "") ->
+    date(Input, "F j, Y");
 date({{_,_,_} = Date,{_,_,_} = Time}, FormatStr) ->
     erlydtl_dateformat:format({Date, Time}, FormatStr);
 date({_,_,_} = Date, FormatStr) ->
@@ -145,6 +186,8 @@ 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));
@@ -177,6 +220,36 @@ fix_ampersands(Input) when is_binary(Input) ->
 fix_ampersands(Input) when is_list(Input) ->
     fix_ampersands(Input, []).
 
+%% @doc When used without an argument, rounds a floating-point number to one decimal place
+%% @doc -- but only if there's a decimal part to be displayed
+floatformat(Number, Place) when is_binary(Number) ->
+    floatformat(binary_to_list(Number), Place);
+floatformat(Number, Place) ->
+    floatformat_io(Number, Place).
+
+floatformat_io(Number, []) ->
+    floatformat_io(Number, -1);
+floatformat_io(Number, Precision) when Precision > 0 ->
+    Format = lists:flatten(io_lib:format("~~.~Bf",[Precision])),
+    [Result] = io_lib:format(Format,[Number]),
+    Result;
+floatformat_io(Number, Precision) when Precision < 0 ->   
+    Round = erlang:round(Number),
+    RoundPrecision = round(Number, -Precision),
+    case RoundPrecision == Round of
+        true ->
+            %Format = lists:flatten(io_lib:format("~~~BB",[-Precision])),
+            [Result] = io_lib:format("~B",[Round]);
+        false ->
+            Format = lists:flatten(io_lib:format("~~.~Bf",[-Precision])),
+            [Result] = io_lib:format(Format,[RoundPrecision])
+    end,
+    Result.
+
+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, []);
@@ -239,12 +312,64 @@ length_is(Input, Number) when is_list(Input), is_integer(Number) ->
 length_is(Input, Number) when is_list(Input), is_list(Number) ->
     ?MODULE:length(Input) =:= Number.
 
+%% @doc Replaces line breaks in plain text with appropriate HTML
+linebreaks(Input) when is_binary(Input) ->
+    linebreaks(binary_to_list(Input));
+% a bit of a hack; unlikely, but could replace "</p>\z\z<p>" literal by mistake 
+linebreaks(Input) ->
+    Input1 = re:replace(Input,"\r\n"       ,"<br />", [global,{return,list}]),
+    Input2 = re:replace(Input1, "\n[\n\r]+"  ,"</p>\z\z<p>", [global,{return,list}]),
+    Input3 = re:replace(Input2,"\r[\n\r]+"  ,"</p>\z\z<p>", [global,{return,list}]),
+    Input4 = re:replace(Input3,"\n"         ,"<br />", [global,{return,list}]),
+    Input5 = re:replace(Input4,"\r"         ,"<br />", [global,{return,list}]),
+    Input6 = re:replace(Input5,"</p>\z\z<p>","</p>\n\n<p>", [global,{return,list}]),
+    lists:flatten(["<p>", Input6,"</p>"]).
+
 %% @doc Converts all newlines to HTML line breaks.
 linebreaksbr(Input) when is_binary(Input) ->
     linebreaksbr(Input, 0);
 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(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([], Acc, _) ->
+    lists:reverse(Acc);
+linenumbers_io(Input, [], LineNumber) ->
+    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([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));
+ 
 %% @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));
@@ -257,30 +382,265 @@ lower(Input) when is_binary(Input) ->
 lower(Input) ->
     string:to_lower(Input).
 
+%% @doc Returns the value turned into a list. For an integer, it's a list of digits. 
+%% @doc For a string, it's a list of characters.
+%% Added this for DTL compatibility, but since strings are lists in Erlang, no need for this.
+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));
 phone2numeric(Input) when is_list(Input) ->
     phone2numeric(Input, []).
 
+%% @doc Returns a plural suffix if the value is not 1. By default, this suffix is 's'.
+pluralize(Number, Suffix) when is_binary(Suffix) ->
+        pluralize_io(Number, binary_to_list(Suffix) );
+pluralize(Number, Suffix) when is_list(Suffix) ->
+        pluralize_io(Number, Suffix).
+ 
+pluralize(Number) ->
+        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.
+ 
 %% @doc Returns a random item from the given list.
 random(Input) when is_list(Input) ->
     lists:nth(random:uniform(erlang:length(Input)), Input);
 random(_) ->
     "".
 
+random_num(Value) ->
+    {A1,A2,A3} = now(),
+    random:seed(A1, A2, A3),
+    Rand = random:uniform(Value),
+    Rand.
+
+%% random tags to be used when using erlydtl in testing
+random_range(Range) ->
+    [Start, End] = string:tokens(Range,","),
+    %?debugFmt("Start, End: ~p,~p~n",[Start,End]),
+    random_range(cast_to_integer(Start),cast_to_integer(End)).
+
+random_range(Start, End) when End >= Start ->
+    %?debugFmt("Input, Start, End: ~p,~p,~p~n",[Input,Start,End]),
+    Range = End - Start,
+    Rand = random:uniform(Range),
+    Num = Rand + Start,
+    lists:flatten(io_lib:format("~B",[Num])).
+
+removetags(Input, Tags) when is_binary(Input), is_binary(Tags) ->
+    removetags(binary_to_list(Input), binary_to_list(Tags));
+removetags(Input, Tags) ->
+    TagList = string:tokens(Tags," "),
+    TagListString = string:join(TagList,"|"),
+    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));
 rjust(Input, Number) ->
     string:right(Input, Number).
 
+%% @doc Returns a slice of the list.
+slice(Input, Index) when is_binary(Input) ->
+        erlydtl_slice:slice(binary_to_list(Input), Index);
+slice(Input, Index) when is_list(Input) ->
+        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(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).
+
+%% @doc
+%% A conversion specifier contains two or more characters and has the following components, which must occur in this order:
+%% 
+%%    1. The "%" character, which marks the start of the specifier.
+%%    2. Mapping key (optional), consisting of a parenthesised sequence of characters (for example, (somename)).
+%%    3. Conversion flags (optional), which affect the result of some conversion types.
+%%    4. Minimum field width (optional). If specified as an "*" (asterisk), the actual width is read from the next element of the tuple in values, and the object to convert comes after the minimum field width and optional precision.
+%%    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]);
+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]);
+stringformat_io(Input, _Conversion, _ConversionFlag, MinFieldWidth,
+                                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 == "." ->
+    Format = re:replace(Conversion, "f", "d", [{return, list}] ),
+    stringformat_io(Input, Format, ConversionFlag, MinFieldWidth,
+                                    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)]);
+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)]);
+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)];
+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)];
+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)]);
+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)]);
+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)]);
+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)]);
+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)]);
+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)]);
+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;
+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)].
+ 
+cast_to_float([]) ->
+    [];
+cast_to_float(Input) when is_float(Input) ->
+        Input;
+cast_to_float(Input) when is_integer(Input) ->
+        Input + 0.0;
+cast_to_float(Input) ->
+        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;
+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 ->
+                        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));
 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, FormatStr) ->
+    date(Input, FormatStr).
+ 
 %% @doc Converts a string into titlecase.
 title(Input) when is_binary(Input) ->
     title(binary_to_list(Input));
@@ -313,6 +673,17 @@ wordcount(Input) when is_binary(Input) ->
 wordcount(Input) when is_list(Input) ->
     wordcount(Input, 0).
 
+%% @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(Input, Number) when is_list(Input) ->
+    wordwrap_io(Input, 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(Bool, Choices) when is_list(Choices) ->
+        yesno_io(Bool, Choices).
 
 % internal
 
@@ -447,6 +818,8 @@ phone2numeric([H|T], Acc) when H >= $w, H =< $z; H >= $W, H =< $Z ->
 phone2numeric([H|T], Acc) ->
     phone2numeric(T, [H|Acc]).
 
+ 
+ 
 slugify([], Acc) ->
     lists:reverse(Acc);
 slugify([H|T], Acc) when H >= $A, H =< $Z ->
@@ -489,6 +862,29 @@ 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).
+
+
 % Taken from quote_plus of mochiweb_util
 
 urlencode(Input, Index) when is_binary(Input) ->
@@ -525,3 +921,70 @@ 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.
+
+%% unjoin == split in other languages; inverse of join
+%% from http://www.erlang.org/pipermail/erlang-questions/2008-October/038896.html
+unjoin(String, []) ->
+     unjoin0(String);
+unjoin(String, [Sep]) when is_integer(Sep) ->
+     unjoin1(String, Sep);
+unjoin(String, [C1,C2|L]) when is_integer(C1), is_integer(C2) ->
+     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)];
+unjoin0([]) ->
+     [].
+
+%% Split a string at a single character separator.
+
+unjoin1(String, Sep) ->
+     unjoin1_loop(String, Sep, "").
+
+unjoin1_loop([Sep|String], Sep, Rev) ->
+     [lists:reverse(Rev) | unjoin1(String, Sep)];
+unjoin1_loop([Chr|String], Sep, Rev) ->
+     unjoin1_loop(String, Sep, [Chr|Rev]);
+unjoin1_loop([], _, 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([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;
+unjoin2_loop([Chr|String], C1, C2, L, Rev) ->
+     unjoin2_loop(String, C1, C2, L, [Chr|Rev]);
+unjoin2_loop([], _, _, _, Rev) ->
+     [lists:reverse(Rev)].
+
+unjoin_prefix([C|L], [C|S]) -> unjoin_prefix(L, S);
+unjoin_prefix([],    S)     -> S;
+unjoin_prefix(_,     _)     -> no.

+ 213 - 0
src/erlydtl/erlydtl_slice.erl

@@ -0,0 +1,213 @@
+-module(erlydtl_slice).
+
+-export([slice/2]).
+
+-define(TEST,"").
+-define(NOTEST,1).
+% remark out NODEBUG when running tests; unremark when debugging indivdual use cases
+-define(NODEBUG,1).
+-include_lib("eunit/include/eunit.hrl").
+
+slice(List,":") ->
+    List;
+slice(List,Index) ->
+    ListLength = erlang:length(List), 
+    {Start,End,C1,C2,Step} = parse_index(Index), 
+    try
+      slice_input_cases(List,ListLength,Start,C1,End,C2,Step)
+    catch
+      throw:outOfBounds ->
+	  [];
+      throw:indexError ->
+	  indexError
+    end.
+
+slice_input_cases(_List,ListLength,Start,false,[],false,[]) when Start > 0, Start >= ListLength ->
+    throw(indexError);
+slice_input_cases(_List,ListLength,Start,false,[],false,[]) when Start < 0, Start < -ListLength ->
+    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, 
+    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;
+%[1]
+slice_input_cases(List,ListLength,Start,false,[],false,[]) ->
+    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), 
+    Result;
+%slice_transform(List, ListLength, Start, C1, End, C2, Step) when End < 0, Step > 0 ->
+%    [];
+%slice_transform(List, ListLength, Start, C1, End, C2, Step) when End > 0, Step < 0 ->
+%    [];
+%[::-1]
+slice_input_cases(List,ListLength,[],true,[],true,Step) when Step < 0 ->
+    ?debugMsg("here"), 
+    slice_transform(List,ListLength,ListLength,true,-2*(ListLength+1),true,Step);
+%[::1]
+slice_input_cases(List,ListLength,[],true,[],true,Step) when Step > 0 ->
+    slice_transform(List,ListLength,0,true,ListLength,true,Step);
+slice_input_cases(List,ListLength,Start,C1,End,C2,Step) ->
+    slice_transform(List,ListLength,Start,C1,End,C2,Step).
+
+%[N:N:N]
+slice_transform(List,ListLength,Start,C1,End,C2,Step) ->
+    {Start1,End1,Step1} = index_defaults(ListLength,Start,End,Step),
+    S = start_transform(ListLength,Start1), 
+    E = end_transform(ListLength,End1), 
+    ?debugFmt("slice_transform: S,C1,E,C2,Step1: ~p,~p,~p,~p,~p~n",[S,C1,E,C2,Step1]),
+    slice_list(List,ListLength,S,C1,E,C2,Step1).
+
+%[N:N:N]
+slice_list(_List,_ListLength,Start,_C1,End,_C2,Step) when Start > End, Step > 0 ->
+    throw(outOfBounds);
+slice_list(_List,_ListLength,Start,_C1,End,_C2,Step) when Start < End andalso Step < 0 ->
+	  throw(outOfBounds); 
+slice_list(_List,_ListLength,Start,_C1,End,_C2,_Step) when Start < 0 andalso End < 0 ->
+	  throw(outOfBounds);
+slice_list(_List,ListLength,Start,_C1,End,_C2,_Step) when Start > ListLength andalso End > ListLength-1 ->
+	  throw(outOfBounds);
+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 ->    
+    {LowerBound,UpperBound} = index_bounds(Step,ListLength,Start,End),
+    %S1 = S0 - 1,
+    ?debugFmt("Start,End: ~p, ~p~n",[Start,End]),
+    case erlang:abs(End) > ListLength of
+        true ->
+            ?debugFmt("LowerBound, UpperBound, UpperBound - LowerBound + 1: ~p, ~p, ~p~n",[LowerBound+1,UpperBound,UpperBound-LowerBound+1]), 
+            BoundList = lists:sublist(List, LowerBound+1, UpperBound - LowerBound + 1);
+        false ->
+            ?debugFmt("LowerBound+2, UpperBound, UpperBound - LowerBound: ~p, ~p, ~p~n",[LowerBound+2,UpperBound,UpperBound-LowerBound]), 
+            BoundList = lists:sublist(List, LowerBound+2, UpperBound - LowerBound)
+    end,
+    ?debugFmt("BoundList: ~p~n",[BoundList]),
+    SequenceList = lists:seq(erlang:length(BoundList),1,Step),
+    ?debugFmt("SequenceList: ~p~n",[SequenceList]),
+    lists:map(fun (N) -> lists:nth(N,BoundList) end,SequenceList).
+
+index_defaults(ListLength, Start, End, Step) ->
+    case Start==[] of
+      true -> Start1 = 0;
+      false -> Start1 = Start
+    end, 
+    case End==[] of
+      true -> End1 = ListLength;
+      false -> End1 = End
+    end, 
+    case Step==[] of
+      true -> Step1 = 1;
+      false -> Step1 = Step
+    end, 
+    {Start1, End1, Step1}.
+
+single_index_bounds(S) ->
+    if 
+       S >= 0 -> LowerBound = S;
+       S < 0 -> LowerBound = 0
+    end,    
+    LowerBound.
+
+index_bounds(Step1, ListLength, S, E) ->
+    AbsListLength = erlang:abs(ListLength),
+    case Step1 < 0 of 
+        true ->
+            ?debugMsg("true"),       
+            if 
+                S > AbsListLength -> UpperBound = ListLength;
+                S =< AbsListLength -> UpperBound = S
+            end,
+            if 
+                E >= 0 -> 
+                    LowerBound = E;
+                    %List1 = tl(List);
+                E < 0 -> 
+                    LowerBound = 0
+                    %List1 = List
+            end;
+        false ->
+            ?debugMsg("false"),
+            if 
+                S >= 0 -> LowerBound = S;
+                S < 0 -> LowerBound = 0
+            end,
+            if 
+                E > AbsListLength -> UpperBound = ListLength;
+                E =< AbsListLength -> UpperBound = E
+            end            
+    end,
+    ?debugFmt("index_bounds: LowerBound,UpperBound: ~p,~p~n",[LowerBound,UpperBound]),
+    {LowerBound, UpperBound}.
+
+parse_index(Index) ->
+    ParsedIndex = re:replace(Index, "([0-9\-]+)?(:)?([0-9\-]+)?(:)?([0-9\-]+)?", "\\1 ,\\2 ,\\3 ,\\4 ,\\5 ", [{return,list}]),
+    ParsedIndex1 = lists:map(fun(X) -> string:strip(X) end, string:tokens(ParsedIndex, ",")),
+    [Start, D1, End, D2, Step] = ParsedIndex1,
+    Start1 = cast_to_integer(Start),
+    End1 = cast_to_integer(End),
+    C1 = parse_colon(D1),    
+    C2 = parse_colon(D2),
+    Step1 = cast_to_integer(Step),
+    ?debugFmt("Parsed: Start1, End1, C1, C2, Step1: ~p, ~p, ~p, ~p, ~p~n",[Start1, End1, C1, C2, Step1]),
+    {Start1, End1, C1, C2, Step1}.
+
+start_transform(_ListLength, []) ->
+    ?debugFmt("start_transform: ~p~n", [0]),
+    0;
+start_transform(ListLength, Start) ->
+    case Start >= 0 orelse erlang:abs(Start) > ListLength of
+        true ->
+            ?debugFmt("start_transform: ~p~n", [Start]),
+            Start;
+        false ->
+            ?debugFmt("start_transform: ~p~n", [ListLength + Start]),
+            ListLength + Start
+    end.
+
+end_transform(ListLength, []) ->
+    ?debugFmt("end_transform: ~p~n", [ListLength]),
+    ListLength;
+end_transform(ListLength, End) ->
+    case End >= 0 orelse erlang:abs(End) > ListLength of
+        true ->
+            ?debugFmt("end_transform: ~p~n", [End]),
+            End;
+        false ->
+            ?debugFmt("end_transform: ~p~n", [ListLength + End]),
+            ListLength + End
+    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 ->
+                        erlang:round(erlang:list_to_float(Input));
+                false ->       
+                        erlang:list_to_integer(Input)
+        end.
+
+parse_colon([]) ->
+    false;
+parse_colon(Colon) ->
+    case Colon of
+        ":" -> true;
+        _ -> false
+    end.

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

@@ -56,6 +56,7 @@ process_ast(Fname,[Head|Tail], Acc) ->
 %%Block are recursivelly processed, trans are accumulated and other tags are ignored
 process_token(Fname, {block,{identifier,{_Line,_Col},_Identifier},Children}, Acc ) -> process_ast(Fname, Children, Acc);
 process_token(Fname, {trans,{string_literal,{Line,Col},String}}, Acc ) -> [{unescape(String), {Fname, Line, Col}} | Acc];
+process_token(_Fname, {apply_filter, _Value, _Filter}, Acc) -> Acc;
 process_token(Fname, {_Instr, _Cond, Children}, Acc) -> process_ast(Fname, Children, Acc);
 process_token(Fname, {_Instr, _Cond, Children, Children2}, Acc) -> 
 	AccModified = process_ast(Fname, Children, Acc),