The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package HTML::Template::Parser;

use 5.008_001;
use strict;
use warnings;

our $VERSION = '0.1010';

use base qw(Class::Accessor::Fast);
__PACKAGE__->mk_accessors(qw());

use Parse::RecDescent;
use English;

use HTML::Template::Parser::NodeBuilder;

use vars '$errortext';
use vars '$errorprefix';

sub parse {
    my($self, $template_string) = @_;

    $self->_list_to_tree($self->_template_string_to_list($template_string));
}

sub _template_string_to_list {
    my($self, $template_string) = @_;

    my @list;
    my($line, $column) = (1, 1); # 1 orign
    while($template_string =~ m!</?TMPL_!i){
        my $pre = $PREMATCH;
        my $tag = $MATCH . $POSTMATCH;
        my $tmp;

        # keep as plain text
        push(@list, ['string', [$line, $column], $pre]) if(length($pre));

        # calc line & column
        $line += (($tmp = $pre) =~ s/\n//g);
        $column = 1 if($pre =~ /\n/);
        ($tmp = $pre) =~ s/.*\n//s;
        $column += length($tmp);
        my $xxx = ( split(/\n/, $tag) )[0];

        # parse TMPL_* tag
        my $tag_temp = $tag;
        my $parsed_tag;
        # capture error message.
        my $error_string = '';
        eval {
            local (*STDERR, *Parse::RecDescent::ERROR);
            if(Parse::RecDescent->can('_write_ERROR')){ # @@@ @@@
                open(STDERR, '>:scalar', \$error_string);
            }else{
                open(Parse::RecDescent::ERROR, '>', \my $error_string) or die "open:[$!]\n";
            }
            $parsed_tag = $self->_get_parser_instance->tag(\$tag_temp);
        };
        if($@){
            die "line $line. column $column. something wrong. $@\n";
        }
        if($tag eq $tag_temp){
            my $first_line_of_tag = ( split(/\n/, $tag) )[0];
            my $first_line_of_error_string = ( split(/\n/, $error_string) )[0];
            die "line $line. column $column. something wrong. Couldn't parse tag well\n[$first_line_of_tag][$first_line_of_error_string]\n";
        }
        splice(@$parsed_tag, 1, 0, [$line, $column]);
        push(@list, $parsed_tag);

        # calc line & column
        my $num_parsed = length($tag)-length($tag_temp);
        my $parsed_string = substr($tag, 0, $num_parsed);
        $line += (($tmp = $parsed_string) =~ s/\n//g);
        $column = 1 if($parsed_string =~ /\n/);
        ($tmp = $parsed_string) =~ s/.*\n//s;
        $column += length($tmp);

        $template_string = $tag_temp;
    }
    push(@list, ['string', [$line, $column], $template_string]) if(length($template_string));
    \@list;
}

sub _list_to_tree {
    my($self, $raw_list) = @_;

    # insert Node::Group before Node::(If|Loop|Unless) and insert Node::GrooupEnd after Node::(IfEnd|LoopEnd|UnlessEnd) to make easier to convert.
    my @node_list;
    foreach my $raw_item (@$raw_list){
        my $node = HTML::Template::Parser::NodeBuilder::createNode($raw_item);

        if($node->type =~ /\A(if|loop|unless)\Z/){
            push(@node_list, HTML::Template::Parser::Node::Group->new({sub_type => $1, line => $node->line, column => $node->column}));
        }
        push(@node_list, $node);
        if($node->type =~ /\A(if|loop|unless)_end\Z/){
            push(@node_list, HTML::Template::Parser::Node::GroupEnd->new({sub_type => $1, line => $node->line, column => $node->column}));
        }
    }

    my $root = HTML::Template::Parser::Node::Root->new();
    $root->add_chidren(\@node_list);
    $root;
}

my $_instance;

sub _get_parser_instance {
    return $_instance if $_instance;
    $::RD_ERRORS=1;
    $::RD_WARN=1;
    $::RD_HINT=1;
#    $::RD_TRACE=1; # @@@
    return $_instance = Parse::RecDescent->new(<<'END;');
{
  use strict;
  use warnings;

  use HTML::Template::Parser::ExprParser;

  sub _parse_name_or_expr {
      my $name_or_expr = shift;

      if($name_or_expr->[0] eq 'name'){
          my $name = $name_or_expr->[1];
          if($name =~ /^\$/){
            die "Can't use \${name} at NAME. [$name]\n";
          }
          $name =~ s/\$?{([^}]+)}/$1/;
          return [ 'name', [ 'variable', $name ] ];
      }

      my $expr = $name_or_expr->[1];
      my $expr_temp = $expr;
      my $parsed_expr = HTML::Template::Parser::ExprParser->new->parse(\$expr_temp);
      if($expr_temp !~ /\A\s*\Z/){
          die "something wrong. Couldn't parse expr well\n[$expr]=>[$expr_temp]\n";
      }
      [ 'expr', $parsed_expr ];
  }

  sub __dump_item__ {
    require Data::Dumper;
    my($thisrule, $a_item, $h_item) = @_;
    print STDERR "Rule: $thisrule\n";
    print STDERR Data::Dumper->Dump([{
       '@item' => $a_item,
       '%item' => $h_item,
    }]);
  }
}

tag:  htp_tag | <error: error near "$text">

htp_tag: htp_var
htp_tag: htp_include
htp_tag: htp_if
htp_tag: htp_else
htp_tag: htp_elsif
htp_tag: htp_unless
htp_tag: htp_loop

htp_var: '<' /TMPL_VAR/i escape_1(?) name_or_expr escape_2(?) default(?) m!/?>! {
  my $name_or_expr = _parse_name_or_expr($item{name_or_expr});
  my $escape = $item{'escape_1(?)'}->[0] || $item{'escape_2(?)'}->[0];
  my $default = $item{'default(?)'}->[0];

  [ 'var', $name_or_expr, $escape, $default ];
}

htp_include: '<' /TMPL_INCLUDE/i name_or_expr m!/?>! {
  my $name_or_expr = _parse_name_or_expr($item{name_or_expr});
  [ 'include',  $name_or_expr, ];
}
htp_include: '<' /TMPL_INCLUDE/i name_or_expr_bare m!/?>! {
  my $name = [ 'name', $item{name_or_expr_bare} ];
  [ 'include',  ['name', $name, ]];
}

htp_if: '<' /TMPL_IF/i name_or_expr '>' {
  my $name_or_expr = _parse_name_or_expr($item{name_or_expr});
 
  [ 'if',  $name_or_expr, ];
}
htp_if: '<' /TMPL_IF/i name_or_expr_bare '>' {
  my $name = $item{name_or_expr_bare};

  [ 'if',  [ 'name', [ 'variable', $name, ] ] ];
}
htp_if: '</' /TMPL_IF/i '>' {
  [ 'if_end' ];
}

htp_else: '<' /TMPL_ELSE/i '>' {
  [ 'else' ];
}

htp_elsif: '<' /TMPL_ELSIF/i name_or_expr '>' {
  my $name_or_expr = _parse_name_or_expr($item{name_or_expr});
  [ 'elsif',  $name_or_expr, ];
}
htp_elsif: '<' /TMPL_ELSIF/i name_or_expr_bare '>' {
  my $name = $item{name_or_expr_bare};
  [ 'elsif',  $name, ];
}

htp_unless: '<' /TMPL_UNLESS/i name_or_expr '>' {
  my $name_or_expr = _parse_name_or_expr($item{name_or_expr});
  [ 'unless',  $name_or_expr, ];
}
htp_unless: '<' /TMPL_UNLESS/i name_or_expr_bare '>' {
  my $name = $item{name_or_expr_bare};
  [ 'unless',  $name, ];
}
htp_unless: '</' /TMPL_UNLESS/i '>' {
  [ 'unless_end' ];
}

htp_loop: '<' /TMPL_LOOP/i name_or_expr default(?) '>' {
#  __dump_item__($thisrule, \@item, \%item);
  my $name_or_expr = _parse_name_or_expr($item{name_or_expr});
  my $default = $item{'default(?)'}->[0];
  [ 'loop',  $name_or_expr, $default ];
}
htp_loop: '</' /TMPL_LOOP/i '>' {
  [ 'loop_end' ];
}

name_or_expr: /NAME|EXPR/i '=' name_or_expr_bare {
  my $type = lc($item[1]);

  [ $type, $item{name_or_expr_bare} ];
}

name_or_expr_bare: /'([^']*)'/ 		{  $1; }
name_or_expr_bare: /"([^"]*)"/		{  $1; }

name_or_expr_bare: /[^>\s]+/ <reject: $item[1] =~ /^(NAME|EXPR)=/i or $item[1] =~ /^['"]|['"]$/> { $item[1]; }


escape_1: escape
escape_2: escape

escape: /ESCAPE/i '=' /['"]?(0|1|URL|NONE|HTML|JS)['"]?/i {
  lc($1);
}

default: /DEFAULT/i '=' name_or_expr_bare { [ 'default', $item[3] ]; }

END;
}

1;

1;
__END__

=head1 NAME

HTML::Template::Parser - Parser for HTML::Template syntax template file & writer.

=head1 VERSION

This document describes HTML::Template::Parser version 0.1.

=head1 SYNOPSIS

    use strict;
    use warnings FATAL => qw(recursion);

    use HTML::Template::Parser;
    use HTML::Template::Parser::TreeWriter::TextXslate::Metakolon;

    my $parser = HTML::Template::Parser->new;
    my $tree = $parser->parse("<TMPL_VAR EXPR=html(name)>");

    my $writer = HTML::Template::Parser::TreeWriter::TextXslate::Metakolon->new;
    print $writer->write($tree);

=head1 DESCRIPTION

HTML::Template::Parser is parser module for tempalte file that is written in HTML::Template.
It parse template file to tree object.
It can write tree as TextXslate::Metakolon format.

=head1 INTERFACE

=head2 B<< HTML::Template::Parser->new() >>

Creates a new tempalte parser.

=head2 B<< $parser->parse($string) >>

Parse $string to tree.

=head1 ACKNOWLEDGEMENT

Thanks to __gfx__ for the bug reports and patches.

=head1 AUTHOR

Shigeki Morimoto E<lt>Shigeki(at)Morimo.toE<gt>

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2011, Shigeki, Morimoto. All rights reserved.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut