package Template::Liquid::Filters;
our $VERSION = '1.0.11';

sub import {
    Template::Liquid::register_filter(
        qw[ date capitalize upcase downcase first last join split sort size
            strip_html strip_newlines newline_to_br replace replace_first remove
            remove_first truncate truncatewords prepend append minus plus times
            divided_by modulo round ceil floor abs money stock_price default]
    );
}

sub round {
    return if $_[0] !~ m[^(\d*\.)?\d+?$]o;
    return sprintf '%.' . int($_[1] || 0) . 'f', $_[0];
}

sub date {
    $_[0] = time() if lc $_[0] eq 'now' || lc $_[0] eq 'today';
    $_[1] = defined $_[1] ? $_[1] : '%c';
    return $_[0]->strftime($_[1]) if ref $_[0] && $_[0]->can('strftime');
    return                        if $_[0] !~ m[^(\d*\.)?\d+?$]o;
    require POSIX;
    return POSIX::strftime($_[1], gmtime($_[0]));
}
sub capitalize { return ucfirst lc $_[0]; }
sub upcase     { return uc $_[0] }
sub downcase   { return lc $_[0] }
sub first      { return @{$_[0]}[0] if ref $_[0] eq 'ARRAY'; }

sub last {
    my $ref = ref $_[0];
    return substr $_[0], -1 if !$ref;
    return @{$_[0]}[-1] if $ref eq 'ARRAY';
}

sub join {
    $_[1] = defined $_[1] ? $_[1] : ' ';
    return CORE::join($_[1], @{$_[0]})      if ref $_[0] eq 'ARRAY';
    return CORE::join($_[1], keys %{$_[0]}) if ref $_[0] eq 'HASH';
    return $_[0];
}
sub split { [split $_[1], $_[0]] }

sub sort {
    return [sort { $a <=> $b } @{$_[0]}] if ref $_[0] eq 'ARRAY';
    return sort keys %{$_[0]}            if ref $_[0] eq 'HASH';
    return $_[0];
}

sub size {
    return 0                    if !defined $_[0];
    return scalar @{$_[0]}      if ref $_[0] eq 'ARRAY';
    return scalar keys %{$_[0]} if ref $_[0] eq 'HASH';
    return length $_[0];
}

sub strip_html {
    $_[0] =~ s[<.*?>][]go;
    $_[0] =~ s[<!--.*?-->][]go;
    $_[0] =~ s[<script.*?<\/script>][]go;
    return $_[0];
}
sub strip_newlines { $_[0] =~ s[\n][]go;         return $_[0]; }
sub newline_to_br  { $_[0] =~ s[\n][<br />\n]go; return $_[0]; }

sub replace {
    $_[2] = defined $_[2] ? $_[2] : '';
    $_[0] =~ s{$_[1]}{$_[2]}g if $_[1];
    return $_[0];
}

sub replace_first {
    $_[2] = defined $_[2] ? $_[2] : '';
    $_[0] =~ s{$_[1]}{$_[2]};
    return $_[0];
}
sub remove       { $_[0] =~ s{$_[1]}{}g; return $_[0] }
sub remove_first { $_[0] =~ s{$_[1]}{};  return $_[0] }

sub truncate {
    my ($data, $length, $truncate_string) = @_;
    $length          = defined $length          ? $length          : 50;
    $truncate_string = defined $truncate_string ? $truncate_string : '...';
    return if !$data;
    my $l = $length - length($truncate_string);
    $l = 0 if $l < 0;
    return
        length($data) > $length
        ? substr($data, 0, $l) . $truncate_string
        : $data;
}

sub truncatewords {
    my ($data, $words, $truncate_string) = @_;
    $words           = defined $words           ? $words           : 15;
    $truncate_string = defined $truncate_string ? $truncate_string : '...';
    return if !$data;
    my @wordlist = split ' ', $data;
    my $l        = $words - 1;
    $l = 0 if $l < 0;
    return $#wordlist > $l
        ? CORE::join(' ', @wordlist[0 .. $l]) . $truncate_string
        : $data;
}
sub prepend { return (defined $_[1]         ? $_[1] : '') . $_[0]; }
sub append  { return $_[0] . (defined $_[1] ? $_[1] : ''); }

sub minus {
    return $_[0] =~ m[^[\+-]?(\d*\.)?\d+?$]o &&
        $_[1] =~ m[^[\+-]?(\d*\.)?\d+?$]o ? $_[0] - $_[1] : ();
}

sub plus {
    return $_[0] =~ m[^[\+-]?(\d*\.)?\d+?$]o &&
        $_[1] =~ m[^[\+-]?(\d*\.)?\d+?$]o ? $_[0] + $_[1] : $_[0] . $_[1];
}

sub times {
    return $_[0]         if $_[1] !~ m[^[\+-]?(\d*\.)?\d+?$]o;
    return $_[0] x $_[1] if $_[0] !~ m[^[\+-]?(\d*\.)?\d+?$]o;
    return $_[0] * $_[1];
}
sub divided_by { return $_[0] / $_[1]; }

sub modulo {
    return (  (!defined $_[0] && $_[0] =~ m[[\+-]?[^\d\.]]o) ? ''
            : (!defined $_[1] && $_[1] =~ m[[\+-]?[^\d\.]]o) ? $_[0]
            :                                                  $_[0] % $_[1]);
}
sub floor { int $_[0] }
sub ceil  { int($_[0] + 1) }
sub abs   { CORE::abs($_[0]) }

sub money {
    return if $_[0] !~ m[^[\+-]?(\d*\.)?\d+?$]o;
    return (($_[0] < 0 ? '-' : '') . (defined $_[1] ? $_[1] : '$') .
                sprintf '%.2f',
            CORE::abs($_[0])
    );
}

sub stock_price {
    return if $_[0] !~ m[^[\+-]?(\d*\.)?\d+?$]o;
    return (($_[0] < 0 ? '-' : '') . (defined $_[1] ? $_[1] : '$') .
                sprintf '%.' . (int(CORE::abs($_[0])) > 0 ? 2 : 4) . 'f',
            CORE::abs($_[0])
    );
}

sub default {
    return length $_[0]  ? $_[0] : $_[1] if !ref $_[0];
    return defined $_[0] ? $_[0] : $_[1];
}
#
# TODO
#sub escape {...}    # Escape's HTML
#sub escape_once {
#    ...;
#} # returns an escaped version of html without affecting existing escaped entities
#sub map {...}    # map/collect on a given property
#url_encode
#url_escape
#url_param_escape
#camelcase
#handle/handleize
#md5
#sha1
#sha256
#hmac_sha1
#hmac_sha256
#newline_to_br
#pluralize
#   See https://help.shopify.com/themes/liquid/filters/string-filters
1;

=pod

=encoding UTF-8

=head1 NAME

Template::Liquid::Filters - Default Filters Based on Liquid's Standard Set

=head1 Synopsis

Filters are simple methods that modify the output of numbers, strings,
variables and objects. They are placed within an output tag C<{{ }}> and are
denoted by a pipe character C<|>.

    # product.title = "Awesome Shoes"
    {{ product.title | upcase }}
    # Output: AWESOME SHOES

In the example above, C<product> is the object, C<title> is its attribute, and
C<upcase> is the filter being applied.

Some filters require a parameter to be passed.

    {{ product.title | remove: "Awesome" }}
    # Output: Shoes

Multiple filters can be used on one output. They are applied from left to
right.

    {{ product.title | upcase | remove: "AWESOME"  }}
    # SHOES

=head1 Array Filters

Array filters change the output of arrays.

=head2 C<join>

Joins elements of the array with a certain character between them.

    # Where array is [1..6]
    {{ array | join }}      => 1 2 3 4 5 6
    {{ array | join:', ' }} => 1, 2, 3, 4, 5, 6

=head2 C<first>

Get the first element of the passed in array

    # Where array is [1..6]
    {{ array | first }} => 1

=head2 C<last>

Get the last element of the passed in array.

    # Where array is [1..6]
    {{ array | last }} => 6

You can use last with dot notation when you need to use the filter inside a
tag.

    {% if product.tags.last == "sale"%}
        This product is on sale!
    {% endif %}

Using last on a string returns the last character in the string.

    {{ product.title | last }}

=head1 Standard Filters

These are the current default filters. They have been written to behave exactly
like their Ruby Liquid counterparts accept where Perl makes improvment
irresistable.

=head2 C<date>

Reformat a date...

=over 4

=item * ...by calling the object's C<strftime> method. This is tried first so
dates may be defined as a L<DateTime|DateTime/"strftime Patterns"> or
L<DateTimeX::Lite|DateTimeX::Lite> object...

    # On my dev machine where obj is an object built with DateTime->now()
    {{ obj  | date:'%c' }} => Dec 14, 2009 2:05:31 AM

=item * ...with the L<POSIX> module's L<strftime|POSIX/"strftime"> function.
This is the last resort and flags may differ by system so... Buyer beware.

    # On my dev machine where date contains the current epoch as an integer
    {{ date | date:'%c' }} => 12/14/2009 2:05:31 AM

=back

The C<date> filter also supports simple replacements of C<'now'> and C<'today'>
with the current time. For example:

    {{ 'now' | date :'%Y' }}

...would print the current year.

=head2 C<capitalize>

Capitalize words in the input sentence. This filter first applies Perl's C<lc>
function and then the C<ucfirst> function.

    {{ 'this is ONLY a test.' | capitalize }} => This is only a test.

=head2 C<downcase>

Convert an input string to lowercase using Perl's C<lc> function.

    {{ 'This is HUGE!' | downcase }} => this is huge!

=head2 C<upcase>

Convert a input string to uppercase using Perl's C<uc> function.


=head2 C<split>

Split input string into an array of substrings separated by given pattern.

    # Where values is 'foo,bar,baz'
    {{ values | split: ',' | last }} => baz

=head2 C<sort>

Sort elements of the array.

    # Where array is defined as [3, 5, 7, 2, 8]
    {{ array | sort }} => 2, 3, 5, 7, 8

=head2 C<size>

Return the size of an array, the length of a string, or the number of keys in a
hash. Undefined values return C<0>.

    # Where array is [1..6] and hash is { child => 'blarg'}
    {{ array     | size }} => 6
    {{ 'Testing' | size }} => 7
    {{ hash      | size }} => 1
    {{ undefined | size }} => 0

=head2 C<strip_html>

Strip html from string. Note that this filter uses C<s[<.*?>][]g> in emmulation
of the Ruby Liquid library's strip_html function. ...so don't email me if you
(correcly) think this is a braindead way of stripping html.

    {{ '<div>Hello, <em id="whom">world!</em></div>' | strip_html }}  => Hello, world!
    '{{ '<IMG SRC = "foo.gif" ALT = "A > B">'        | strip_html }}' => ' B">'
    '{{ '<!-- <A comment> -->'                       | strip_html }}' => ' -->'

=head2 C<strip_newlines>

Strips all newlines (C<\n>) from string using the regular expression
C<s[\n][]g>.

=head2 C<newline_to_br>

Replaces each newline (C<\n>) with html break (C<< <br />\n >>).

=head2 C<replace>

Replace all occurrences of a string with another string. The replacement value
is optional and defaults to an empty string (C<''>).

    {{ 'foofoo'                 | replace:'foo','bar' }} => barbar
    {% assign this = 'that' %}
    {{ 'Replace that with this' | replace:this,'this' }} => Replace this with this
    {{ 'I have a listhp.'       | replace:'th' }}        => I have a lisp.

=head2 C<replace_first>

Replaces the first occurrence of a string with another string. The replacement
value is optional and defaults to an empty string (C<''>).

    {{ 'barbar' | replace_first:'bar','foo' }} => 'foobar'

=head2 C<remove>

Remove each occurrence of a string.

    {{ 'foobarfoobar' | remove:'foo' }} => 'barbar'

=head2 C<remove_first>

Remove the first occurrence of a string.

    {{ 'barbar' | remove_first:'bar' }} => 'bar'

=head2 C<truncate>

Truncate a string down to C<x> characters.

    {{ 'Running the halls!!!' | truncate:19 }}         => Running the hall..
    {% assign blarg = 'STOP!' %}
    {{ 'Any Colour You Like' | truncate:10,blarg }}    => Any CSTOP!
    {{ 'Why are you running away?' | truncate:4,'?' }} => Why?
    {{ 'Ha' | truncate:4 }}                            => Ha
    {{ 'Ha' | truncate:1,'Laugh' }}                    => Laugh
    {{ 'Ha' | truncate:1,'...' }}                      => ...

...and...

    {{ 'This is a long line of text to test the default values for truncate' | truncate }}

...becomes...

    This is a long line of text to test the default...

=head2 C<truncatewords>

Truncate a string down to C<x> words.

    {{ 'This is a very quick test of truncating a number of words' | truncatewords:5,'...' }}
    {{ 'This is a very quick test of truncating a number of words where the limit is fifteen' | truncatewords: }}

...becomes...

    This is a very quick...
    This is a very quick test of truncating a number of words where the limit...

=head2 C<prepend>

Prepend a string.

    {{ 'bar' | prepend:'foo' }} => 'foobar'

=head2 C<append>

Append a string.

    {{ 'foo' | append:'bar' }} => 'foobar'

=head2 C<minus>

Simple subtraction.

    {{ 4       | minus:2 }}  => 2
    '{{ 'Test' | minus:2 }}' => ''

=head2 C<plus>

Simple addition or string contatenation.

    {{ 154    | plus:1183 }}  => 1337
    {{ 'What' | plus:'Uhu' }} => WhatUhu

=head3 MATHFAIL!

Please note that integer behavior differs with Perl vs. Ruby so...

    {{ '1' | plus:'1' }}

...becomes C<11> in Ruby but C<2> in Perl.

=head2 C<times>

Simple multiplication or string repetion.

    {{ 'foo' | times:4 }} => foofoofoofoo
    {{ 5     | times:4 }} => 20

=head2 C<divided_by>

Simple division.

    {{ 10 | divided_by:2 }} => 5

=head2 C<modulo>

Simple modulo operation.

    {{ 10 | modulo:3 }} => 1

=head2 C<round>

Rounds the output to the nearest integer or specified number of decimals.

    {{ 4.6 | round }}       => 5
    {{ 4.3 | round }}       => 4
    {{ 4.5612 | round: 2 }} => 4.56

=head2 C<money>

Formats floats and integers as if they were money.

    {{  4.6    | money }} => $4.60
    {{ -4.3    | money }} => -$4.30
    {{  4.5612 | money }} => $4.56

You may pass a currency symbol to override the default dollar sign (C<$>).

    {{  4.6    | money:'€' }} => €4.60

=head2 C<stock_price>

Formats floats and integers as if they were stock prices.

    {{ 4.6    | stock_price }} => $4.60
    {{ 0.30   | stock_price }} => $0.3000
    {{ 4.5612 | stock_price }} => $4.56

You may pass a currency symbol to override the default dollar sign (C<$>).

    {{  4.6    | stock_price:'€' }} => €4.60

=head2 C<abs>

Returns the absolute value of a number.

    {{  4 | abs }} => 4
    {{ -4 | abs }} => 4

=head2 C<ceil>

Rounds an integer up to the nearest integer.

    {{ 4.6 | ceil }} => 5
    {{ 4.3 | ceil }} => 5

=head2 C<floor>

Rounds an integer down to the nearest integer.

    {{ 4.6 | floor }} => 4
    {{ 4.3 | floor }} => 4

=head2 C<default>

Sets a default avlue for any variable with no assigned value. Can be used with
strings, arrays, and hashes.

The default value is returne if the cariable resolves to C<undef> or an empty
string (C<''>). A string containing whitespace characters will not resolve to
the default value.

    {{ customer.name | default: "customer" }} => "customer"

=head1 Author

Sanko Robinson <sanko@cpan.org> - http://sankorobinson.com/

CPAN ID: SANKO

=head1 License and Legal

Copyright (C) 2009-2016 by Sanko Robinson E<lt>sanko@cpan.orgE<gt>

This program is free software; you can redistribute it and/or modify it under
the terms of L<The Artistic License
2.0|http://www.perlfoundation.org/artistic_license_2_0>. See the F<LICENSE>
file included with this distribution or L<notes on the Artistic License
2.0|http://www.perlfoundation.org/artistic_2_0_notes> for clarification.

When separated from the distribution, all original POD documentation is covered
by the L<Creative Commons Attribution-Share Alike 3.0
License|http://creativecommons.org/licenses/by-sa/3.0/us/legalcode>. See the
L<clarification of the
CCA-SA3.0|http://creativecommons.org/licenses/by-sa/3.0/us/>.

=cut
