The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Text::Xslate::Syntax::TTerse;
use Mouse;
use Text::Xslate::Util qw(p any_in);
use Scalar::Util ();

extends qw(Text::Xslate::Parser);

sub _build_identity_pattern {
    return qr/(?: [A-Za-z_] [A-Za-z0-9_]* )/xms;
}

# [% ... %] and %% ...
sub _build_line_start { '%%' }
sub _build_tag_start  { '[%' }
sub _build_tag_end    { '%]' }

around trim_code => sub {
    my($super, $self, $code) = @_;

    if($code =~ /^\#/) { # multiline comments
        return '';
    }

    return $super->($self, $code);
};

sub init_symbols {
    my($parser) = @_;
    my $s;

    $parser->init_basic_operators();

    $parser->symbol('$')->set_nud(\&nud_dollar);
    $parser->make_alias('~' => '_');
    $parser->make_alias('|' => 'FILTER');
    $parser->symbol('.')->set_led(\&led_dot); # redefine

    $parser->symbol('END')  ->is_block_end(1);
    $parser->symbol('ELSE') ->is_block_end(1);
    $parser->symbol('ELSIF')->is_block_end(1);
    $parser->symbol('CASE') ->is_block_end(1);

    $parser->symbol('IN');

    $s = $parser->symbol('IF');
    $s->set_std(\&std_if);
    $s->can_be_modifier(1);
    $s = $parser->symbol('UNLESS');
    $s->set_std(\&std_if);
    $s->can_be_modifier(1);

    $parser->symbol('FOREACH') ->set_std(\&std_for);
    $parser->symbol('FOR')     ->set_std(\&std_for);
    $parser->symbol('WHILE')   ->set_std(\&std_while);
    $parser->symbol('SWITCH')  ->set_std(\&std_switch);
    $parser->symbol('CASE')    ->set_std(\&std_case);

    $parser->symbol('INCLUDE') ->set_std(\&std_include);
    $parser->symbol('WITH');

    $parser->symbol('GET')     ->set_std(\&std_get);
    $parser->symbol('SET')     ->set_std(\&std_set);
    $parser->symbol('DEFAULT') ->set_std(\&std_set);
    $parser->symbol('CALL')    ->set_std(\&std_call);

    $parser->symbol('NEXT')    ->set_std( $parser->can('std_next') );
    $parser->symbol('LAST')    ->set_std( $parser->can('std_last') );

    $parser->symbol('MACRO') ->set_std(\&std_macro);
    $parser->symbol('BLOCK');
    $parser->symbol('WRAPPER')->set_std(\&std_wrapper);
    $parser->symbol('INTO');

    $parser->symbol('FILTER')->set_std(\&std_filter);

    # unsupported directives
    my $nos = $parser->can('not_supported');
    foreach my $keyword (qw(
            INSERT PROCESS PERL RAWPERL TRY THROW RETURN
            STOP CLEAR META TAGS DEBUG VIEW)) {
        $parser->symbol($keyword)->set_std($nos);
    }

    # not supported, but ignored (synonym to CALL)
    $parser->symbol('USE')->set_std(\&std_call);

    foreach my $id(keys %{$parser->symbol_table}) {
        if($id =~ /\A [A-Z]+ \z/xms) { # upper-cased keywords
            $parser->make_alias($id => lc $id)->set_nud(\&aliased_nud);
        }
    }

    $parser->make_alias('not' => 'NOT');
    $parser->make_alias('and' => 'AND');
    $parser->make_alias('or'  => 'OR');

    return;
}

around _build_iterator_element => sub {
    my($super, $parser) = @_;

    my $table = $super->($parser);

    # make aliases
    $table->{first} = $table->{is_first};
    $table->{last}  = $table->{is_last};
    $table->{next}  = $table->{peek_next};
    $table->{prev}  = $table->{peek_prev};
    $table->{max}   = $table->{max_index};

    return $table;
};

sub default_nud {
    my($parser, $symbol) = @_;
    return $symbol->clone(
        arity => 'variable',
    );
}

# same as default_nud, except for aliased symbols
sub aliased_nud {
    my($parser, $symbol) = @_;
    return $symbol->clone(
        arity => 'name',
        id    => lc( $symbol->id ),
        value => $symbol->id,
    );
}

sub nud_dollar {
    my($parser, $symbol) = @_;
    my $expr;
    my $t = $parser->token;
    if($t->id eq "{") {
        $parser->advance("{");
        $expr = $parser->expression(0);
        $parser->advance("}");
    }
    else {
        if(!any_in($t->arity, qw(name variable))) {
            $parser->_unexpected("a name", $t);
        }
        $parser->advance();
        $expr = $t->clone( arity => 'variable' );
    }
    return $expr;
}

sub undefined_name {
    my($parser, $name) = @_;
    # undefined names are always variables
    return $parser->symbol_table->{'(variable)'}->clone(
        id => $name,
    );
}

sub is_valid_field {
    my($parser, $token) = @_;
    return $parser->SUPER::is_valid_field($token)
        || $token->arity eq "variable";
}

sub led_dot {
    my($parser, $symbol, $left) = @_;

    # special case: foo.$field, foo.${expr}
    if($parser->token->id eq '$') {
        return $symbol->clone(
            arity  => "field",
            first  => $left,
            second => $parser->expression( $symbol->lbp ),
        );
    }

    return $parser->SUPER::led_dot($symbol, $left);
}

sub led_assignment {
    my($parser, $symbol, $left) = @_;

    my $assign = $parser->led_infixr($symbol, $left);
    $assign->arity('assign');
    $assign->is_statement(1);

    my $name = $assign->first;
    if(not $parser->find_or_create($name->id)->is_defined) {
        $parser->define($name);
        $assign->third('declare');
    }

    return $assign;
}

sub assignment {
    my($parser, $id, $bp) = @_;

    $parser->symbol($id, $bp)->set_led(\&led_assignment);
    return;
}

sub std_if {
    my($parser, $symbol, $expr) = @_;
    my $if = $symbol->clone(arity => "if");

    my $is_modifier = defined $expr;

    $parser->new_scope() unless $is_modifier; # whole if block

    my $cond = $parser->expression(0);

    if($symbol->id eq 'UNLESS') {
        $cond = $parser->symbol('!')->clone(
            arity  => 'unary',
            first  => $cond,
        );
    }
    $if->first($cond);

    if($is_modifier) {
        $if->second([ $expr ]);
        return $if;
    }

    # then block
    {
        $parser->new_scope();
        $if->second( $parser->statements() );
        $parser->pop_scope();
    }

    my $t = $parser->token;

    my $top_if = $if;

    while($t->id eq "ELSIF") {
        $parser->reserve($t);
        $parser->advance(); # "ELSIF"

        my $elsif = $t->clone(arity => "if");
        $elsif->first(  $parser->expression(0) );

        {
            $parser->new_scope();
            $elsif->second( $parser->statements() );
            $parser->pop_scope();
        }

        $if->third([$elsif]);
        $if = $elsif;
        $t  = $parser->token;
    }

    if($t->id eq "ELSE") {
        my $else_line = $t->line;
        $parser->reserve($t);
        $t = $parser->advance(); # "ELSE"

        if($t->id eq "IF" and $t->line != $else_line) {
            Carp::carp(sprintf "%s: Parsing ELSE-IF sequense as ELSIF, but it is likely to be a misuse of ELSE-IF. Please insert semicolon as ELSE; IF, or write it in the same line (around input line %d)", ref $parser, $t->line);
        }

        {
            $parser->new_scope();
            $if->third( $t->id eq "IF"
                ? [$parser->statement()]
                :  $parser->statements());
            $parser->pop_scope();
        }
    }

    $parser->advance("END");
    $parser->pop_scope();
    return $top_if;
}

sub std_switch {
    my($parser, $symbol) = @_;

    $parser->new_scope();

    my $topic  = $parser->symbol('$_')->clone(arity => 'variable' );
    my $switch = $symbol->clone(
        arity  => 'given',
        first  => $parser->expression(0),
        second => [ $topic ],
    );

    local $parser->{in_given} = 1;

    my @cases;
    while(!($parser->token->id eq "END" or $parser->token->id eq '(end)')) {
        push @cases, $parser->statement();
    }
    $switch->third( \@cases );

    $parser->build_given_body($switch, "case");

    $parser->advance("END");
    $parser->pop_scope();
    return $switch;
}

sub std_case {
    my($parser, $symbol) = @_;
    if(!$parser->in_given) {
        $parser->_error("You cannot use $symbol statements outside switch statements");
    }
    my $case = $symbol->clone(arity => "case");

    if($parser->token->id ne "DEFAULT") {
        $case->first( $parser->expression(0) );
    }
    else {
        $parser->advance();
    }
    $case->second( $parser->statements() );
    return $case;
}

sub iterator_name {
    return 'loop'; # always 'loop'
}

# FOR ... IN ...; ...; END
sub std_for {
    my($parser, $symbol) = @_;

    my $proc = $symbol->clone(arity => "for");

    my $var = $parser->token;
    if(!any_in($var->arity, qw(variable name))) {
        $parser->_unexpected("a variable name", $var);
    }
    $parser->advance();
    $parser->advance("IN");
    $proc->first( $parser->expression(0) );
    $proc->second([$var]);

    $parser->new_scope();
    $parser->define_iterator($var);

    $proc->third( $parser->statements() );

    # for-else
    if($parser->token->id eq 'ELSE') {
        $parser->reserve($parser->token);
        $parser->advance();
        my $else = $parser->statements();
        $proc = $symbol->clone( arity => 'for_else',
            first  => $proc,
            second => $else,
        );
    }
    $parser->advance("END");
    $parser->pop_scope();
    return $proc;
}

sub std_while {
    my($parser, $symbol) = @_;

    my $while = $symbol->clone(arity => "while");

    $while->first( $parser->expression(0) );
    $while->second([]); # no vars
    $parser->new_scope();
    $while->third( $parser->statements() );
    $parser->advance("END");
    $parser->pop_scope();
    return $while;
}

around std_include => sub {
    my($super, $self, $symbol) = @_;
    $symbol->id( lc $symbol->id );
    return $self->$super( $symbol );
};

sub localize_vars {
    my($parser, $symbol) = @_;

# should make 'WITH' optional?
#    my $t = $parser->token;
#    if($t->id eq "WITH" or $t->arity eq "variable") {
#        $parser->advance() if $t->id eq "WITH";
    if($parser->token->id eq "WITH") {
        $parser->advance();
        $parser->new_scope();
        my $vars = $parser->set_list();
        $parser->pop_scope();
        return $vars;
    }
    return undef;
}

sub set_list {
    my($parser) = @_;
    my @args;
    while(1) {
        my $key = $parser->token;

        if(!(any_in($key->arity, qw(variable name))
                && $parser->next_token_is("="))) {
            last;
        }
        $parser->advance();
        $parser->advance("=");

        my $value = $parser->expression(0);

        push @args, $key => $value;

        if($parser->token->id eq ",") { # , is optional
            $parser->advance();
        }
    }

    return \@args;
}

sub std_get {
    my($parser, $symbol) = @_;

    my $stmt = $parser->print( $parser->expression(0) );
    return $parser->finish_statement($stmt);
}

sub std_set {
    my($parser, $symbol) = @_;

    my $is_default = ($symbol->id eq 'DEFAULT');

    my $set_list = $parser->set_list();
    my @assigns;
    for(my $i = 0; $i < @{$set_list}; $i += 2) {
        my($name, $value) = @{$set_list}[$i, $i+1];

        if($is_default) { # DEFAULT a = b -> a = a || b
            my $var = $parser->symbol('(variable)')->clone(
                id => $name->id,
            );

            $value = $parser->binary('||', $var, $value);
        }
        my $assign = $symbol->clone(
            id     => '=',
            arity  => 'assign',
            first  => $name,
            second => $value,
        );

        if(not $parser->find_or_create($name->id)->is_defined) {
            $parser->define($name);
            $assign->third('declare');
        }
        push @assigns, $assign;
    }
    return @assigns;
}

sub std_call {
    my($parser, $symbol) = @_;
    my $stmt = $parser->expression(0);
    return $parser->finish_statement($stmt);
}

sub std_macro {
    my($parser, $symbol) = @_;
    my $proc = $symbol->clone(
        arity => 'proc',
        id    => 'macro',
    );

    my $name = $parser->token;
    if($name->arity ne "variable") {
        $parser->_error("a name", $name);
    }

    $parser->define_function($name->id);

    $proc->first($name);
    $parser->advance();

    $parser->new_scope();

    my $paren = ($parser->token->id eq "(");

    $parser->advance("(") if $paren;

    my $t = $parser->token;
    my @vars;
    while($t->arity eq "variable") {
        push @vars, $t;
        $parser->define($t);

        $t = $parser->advance();

        if($t->id eq ",") {
            $t = $parser->advance(); # ","
        }
        else {
            last;
        }
    }
    $parser->advance(")") if $paren;

    $proc->second(\@vars);

    $parser->advance("BLOCK");
    $proc->third( $parser->statements() );
    $parser->advance("END");
    $parser->pop_scope();
    return $proc;
}


# WRAPPER "foo.tt" ...  END
# is
# cascade "foo.tt" { content => lambda@xxx() }
# macro content@xxx -> { ... }
sub std_wrapper {
    my($parser, $symbol) = @_;

    my $base  = $parser->barename();
    my $into;
    if($parser->token->id eq "INTO") {
        my $t = $parser->advance();
        if(!any_in($t->arity, qw(name variable))) {
            $parser->_unexpected("a variable name", $t);
        }
        $parser->advance();
        $into = $t->id;
    }
    else {
        $into = 'content';
    }
    my $vars  = $parser->localize_vars() || [];
    my $body  = $parser->statements();
    $parser->advance("END");

    return $parser->wrap(
        $symbol,
        $base,
        $into,
        $vars,
        $body,
    );
}

sub wrap {
    my($parser, $proto, $base, $into, $vars, $body) = @_;
    my $cascade = $proto->clone(
        arity => 'cascade',
        first => $base,
    );

    my $content = $parser->lambda($proto);
    $content->second([]); # args
    $content->third($body);

    my $call_content = $parser->call($content->first);

    my $into_name = $proto->clone(
        arity => 'literal',
        id    => $into,
    );

    push @{$vars}, $into_name => $call_content;
    $cascade->third($vars);
    return( $cascade, $content );
}

# [% FILTER html %]
# ...
# [% END %]
# is
# : block filter_xxx | html -> {
#   ...
# : }
# in Kolon

sub std_filter {
    my($parser, $symbol) = @_;

    my $filter = $parser->expression(0);

    my $proc = $parser->lambda($symbol);

    $proc->second([]);
    $proc->third( $parser->statements() );
    $parser->advance("END");

    my $callmacro  = $parser->call($proc->first);

    if($filter->id eq 'html') {
        # for compatibility with TT2
        $filter = 'unmark_raw';
    }
    my $callfilter = $parser->call($filter, $callmacro);
    return( $proc, $parser->print($callfilter) );
}

no Mouse;
__PACKAGE__->meta->make_immutable();
__END__

=head1 NAME

Text::Xslate::Syntax::TTerse - An alternative syntax compatible with Template Toolkit 2

=head1 SYNOPSIS

    use Text::Xslate;
    my $tx = Text::Xslate->new(
        syntax => 'TTerse',
    );

    print $tx->render_string(
        'Hello, [% dialect %] world!',
        { dialect => 'TTerse' }
    );

    # PRE_PROCESS/POST_PROCESS
    $tx = Text::Xslate->new(
        syntax => 'TTerse',
        header => ['header.tt'],
        footer => ['footer.tt'],
    );

=head1 DESCRIPTION

TTerse is a subset of the Template-Toolkit 2 (and partially  3) syntax,
using C<< [% ... %] >> tags and C<< %% ... >> line code.

Note that TTerse itself has few methods and filters while Template-Toolkit 2
has a lot. See C<Text::Xslate::Bridge::*> modules on CPAN which provide extra
methods and filters if you want to use those features.

(TODO: I should concentrate on the difference between Template-Toolkit 2 and
TTerse)

=head1 SYNTAX

This supports a Template-Toolkit compatible syntax, although the details might be different.

Note that lower-cased keywords, which are inspired in Template-Toolkit 3,
are also allowed.

=head2 Variable access

Scalar access:

    [%  var %]
    [% $var %]
    [% GET var # 'GET' is optional %]

Field access:

    [% var.0 %]
    [% var.field %]
    [% var.accessor %]
    [% var.$field ]%
    [% var[$field] # TTerse specific %]

Variables may be HASH references, ARRAY references, or objects.

If I<$var> is an object instance, you can call its methods.

    [% $var.method() %]
    [% $var.method(1, 2, 3) %]
    [% $var.method(foo => [1, 2, 3]) %]
    [% $var.method({ foo => 'bar' }) %]

=head2 Expressions

Almost the same as L<Text::Xslate::Syntax::Kolon>, but C<< infix:<_> >> for
concatenation is supported for compatibility.

=head2 Loops

    [% FOREACH item IN arrayref %]
        * [% item %]
    [% END %]

Loop iterators are partially supported.

    [% FOREACH item IN arrayref %]
        [%- IF loop.is_first -%]
        <first>
        [%- END -%]
        * [% loop.index %]  # 0 origin
        * [% loop.count     # loop.index + 1 %]
        * [% loop.body      # alias to arrayref %]
        * [% loop.size      # loop.body.size %]
        * [% loop.max_index # loop.size - 1 %]
        * [% loop.peek_next # loop.body[ loop.index + 1 ]
        * [% loop.peek_prev # loop.body[ loop.index - 1 ]
        [%- IF loop.is_last -%]
        <last>
        [%- END -%]
    [% END %]

Unlike Template-Toolkit, C<FOREACH> doesn't accept a HASH reference, so
you must convert HASH references to ARRAY references by C<keys()>, C<values()>, or C<kv()> methods.

Template-Toolkit compatible names are also supported, but the use of them is
discouraged because they are not easy to understand:

    loop.max   # for loop.max_index
    loop.next  # for loop.peek_next
    loop.prev  # for loop.peek_prev
    loop.first # for loop.is_first
    loop.last  # for loop.is_last

Loop control statements, namely C<NEXT> and C<LAST>, are also supported
in both C<FOR> and C<WHILE> loops.

    [% FOR item IN data -%]
        [% LAST IF item == 42 -%]
        ...
    [% END -%]

=head2 Conditional statements

    [% IF logical_expression %]
        Case 1
    [% ELSIF logical_expression %]
        Case 2
    [% ELSE %]
        Case 3
    [% END %]

    [% UNLESS logical_expression %]
        Case 1
    [% ELSE %]
        Case 2
    [% END %]

    [% SWITCH expression %]
    [% CASE case1 %]
        Case 1
    [% CASE case2 %]
        Case 2
    [% CASE DEFAULT %]
        Case 3
    [% END %]

=head2 Functions and filters

    [% var | f %]
    [% f(var)  %]

=head2 Template inclusion

The C<INCLUDE> statement is supported.

    [% INCLUDE "file.tt" %]
    [% INCLUDE $var %]

C<< WITH variables >> syntax is also supported, although
the C<WITH> keyword is optional in Template-Toolkit:

    [% INCLUDE "file.tt" WITH foo = 42, bar = 3.14 %]
    [% INCLUDE "file.tt" WITH
        foo = 42
        bar = 3.14
    %]

The C<WRAPPER> statement is also supported.
The argument of C<WRAPPER>, however, must be string literals, because
templates will be statically linked while compiling.

    [% WRAPPER "file.tt" %]
    Hello, world!
    [% END %]

    %%# with variable
    [% WRAPPER "file.tt" WITH title = "Foo!" %]
    Hello, world!
    [% END %]

The content will be set into C<content>, but you can specify its name with
the C<INTO> keyword.

    [% WRAPPER "foo.tt" INTO wrapped_content WITH title = "Foo!" %]
    ...
    [% END %]

This is a syntactic sugar to template cascading. Here is a counterpart of
the example in Kolon.

    : macro my_content -> {
        Hello, world!
    : }
    : cascade "file.tx" { content => my_content() }

Note that the WRAPPER option
(L<http://template-toolkit.org/docs/manual/Config.html#section_WRAPPER>)
in Template-Toolkit is not supported directly. Instead, you can emulate
it with C<header> and C<footer> options as follows:

    my %vpath = (
        wrap_begin => '[% WRAPPER "base" %]',
        wrap_end => '[% END %]',

        base => 'Hello, [% content %] world!' . "\n",
        content => 'Xslate',
    );

    my $tx = Text::Xslate->new(
        syntax => 'TTerse',
        path => \%vpath,

        header => ['wrap_begin'],
        footer => ['wrap_end'],
    );

    print $tx->render('content'); # => Hello, Xslate world!;

=head2 Macro blocks

Definition:

    [% MACRO foo BLOCK -%]
        This is a macro.
    [% END -%]

    [% MACRO add(a, b) BLOCK -%]
    [%  a + b -%]
    [% END -%]

Call:

    [% foo()     %]
    [% add(1, 2) %]

Unlike Template-Toolkit, calling macros requires parens (C<()>).

=head2 Virtual methods

A few methods are supported in the Xslate core.

    %% a.size();
    %% a.join(", ");
    %% a.reverse();

    %% h.size();
    %% h.keys();
    %% h.values();
    %% h.kv();

However, there is a bridge mechanism that allows you to use external methods.
For example, Text::Xslate::Bridge::TT2 provides the TT2 virtual methods for
Xslate, which bridges Template::VMethods implementation.

    use Text::Xslate::Bridge::TT2;

    my $tx = Text::Xslate->new(
        syntax => 'TTerse',
        module => [qw(Text::Xslate::Bridge::TT2)],
    );

   print $tx->render_string('[% "foo".length() %]'); # => 3

See L<Text::Xslate::Bridge>, or search for C<Text::Xslate::Bridge::*> on CPAN.

=head2 Misc.

CALL evaluates expressions, but does not print it.

    [% CALL expr %]

SET and assignments, although the use of them are strongly discouraged.

    [% SET var1 = expr1, var2 = expr2 %]
    [% var = expr %]

DEFAULT statements as a syntactic sugar to C<< SET var = var // expr >>:

    [% DEFAULT lang = "TTerse" %]

FILTER blocks to apply filters to text sections:

    [% FILTER html -%]
    Hello, <Xslate> world!
    [% END -%]

=head1 COMPATIBILITY

There are some differences between TTerse and Template-Toolkit.

=over

=item *

C<INCLUDE> of TTerse requires an expression for the file name, while
that of Template-Toolkit allows a bare token:

    [% INCLUDE  foo.tt  # doesn't work! %]
    [% INCLUDE "foo.tt" # OK %]

=item *

C<FOREACH item = list> is forbidden in TTerse. It must be C<FOREACH item IN list>.

=item *

TTerse does not support plugins (i.e. C<USE> directive), but understands
the C<USE> keyword as an alias to C<CALL>, so you could use some simple
plugins which do not depend on the context object of Template-Toolkit.

    use Template::Plugin::Math;
    use Template::Plugin::String;

    my $tt = Text::Xslate->new(...);

    mt %vars = (
        Math   => Template::Plugin::Math->new(),   # as a namespace
        String => Template::Plugin::String->new(), # as a prototype
    );
    print $tt->render_string(<<'T', \%vars);
    [% USE Math # does nothing actually, only for compatibility %]
    [% USE String %]
    [% Math.abs(-100)          # => 100 %]
    [% String.new("foo").upper # => FOO %]

=item *

The following directives are not supported:
C<INSERT>, C<PROCESS>, C<BLOCK> as a named blocks, C<USE> (but see above),
C<PERL>, C<RAWPERL>, C<TRY>, C<THROW>,
C<RETURN>, C<STOP>, C<CLEAR>, C<META>, C<TAGS>, C<DEBUG>, and C<VIEW>.

Some might be supported in a future.

=back

=head1 SEE ALSO

L<Text::Xslate>

L<Template> (Template::Toolkit)

L<Template::Tiny>

L<Text::Xslate::Bridge::TT2>

L<Text::Xslate::Bridge::TT2Like>

L<Text::Xslate::Bridge::Alloy>

=cut