The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#
# (c) Jan Gehring <jan.gehring@gmail.com>
#
# vim: set ts=2 sw=2 tw=0:
# vim: set expandtab:

package Rex::Template::NG;

use strict;
use warnings;

our $VERSION = '1.2.1'; # VERSION

sub new {
  my $that  = shift;
  my $proto = ref($that) || $that;
  my $self  = {@_};

  bless( $self, $proto );

  $self->{__output__}   = "";
  $self->{__code__}     = "";
  $self->{__raw_data__} = "";

  return $self;
}

sub parse {
  my ( $self, $c, %vars ) = @_;

  # some backward compat. to old template module.
  $c =~ s/\$::([a-zA-Z0-9_]+)/_replace_var($1, \%vars)/egms;

  my $code = "";

  my $var_data = '
  
  return sub {
    my (
      $self,
  ';

  my @code_values;
  for my $var ( keys %vars ) {
    $var_data .= '$' . $var . ", \n";
    push( @code_values, $vars{$var} );
  }

  $var_data .= '$this_is_really_nothing) = @_;';
  $var_data .= "\n";

  $code = $var_data;

  $code .= $self->_parse($c);

  $code .= "\n}";

  my $idx_c = 1;
  for my $l ( split( /\n/, $code ) ) {
    $idx_c++;
    $l ||= "";
    Rex::Logger::debug("$idx_c. $l");
  }

  $self->{__code__}     = $code;
  $self->{__raw_data__} = $c;

  no warnings;
  my $tpl_code = eval($code);
  use warnings;

  if ($@) {

    my $error = $@;

    my ($error_line) = ( $error =~ m/line (\d+)[\.,]/ );
    my @code_lines = split( /\n/, $code );
    my @raw_lines  = split( /\n/, $c );

    my $idx = $error_line - 5;
    for my $l ( @code_lines[ $error_line - 5 .. $error_line + 5 ] ) {
      $idx++;
      $l ||= "";
      Rex::Logger::debug("$idx. $l");
    }

    my $template_line     = 0;
    my $add_to_error_line = -1;

    # search the error line
    Rex::Logger::debug("Template-Error-Line: $error_line");
    for ( my $bi = $error_line - 1 ; $bi >= 0 ; $bi-- ) {
      if ( $code_lines[$bi] =~ m/^# LINE: (\d+)$/ ) {
        $template_line = $1 + $add_to_error_line;
        last;
      }
      $add_to_error_line++;
    }

    if ( !$template_line ) {
      die "Uncatchable error in template: $error ($error_line)";
    }

    my $start_part = $template_line - 5;
    $start_part = 0 if $start_part <= 0;
    my $end_part = $template_line + 5;
    $end_part = scalar @raw_lines if $end_part > scalar @raw_lines;

    my $idx_t = $start_part;

    for my $l ( @raw_lines[ $start_part .. $end_part ] ) {
      $idx_t++;
      $l ||= "";
      Rex::Logger::info("$idx_t. $l");
    }

    my $tpl_error = $error;
    $tpl_error =~ s/at \(eval \d+\) line \d+/at template line $template_line/;

    if ( $error =~ m/Global symbol "([^"]+)" requires explicit package name/ ) {
      $tpl_error =
        "Unknown variable name $1 in code line: ,,$raw_lines[$template_line-1]'' line: $template_line.\nOriginal Error:\n$error\n";
    }

    # internal parsing error, maybe runaway line without ";"
    elsif ( $raw_lines[ $template_line - 2 ] =~ m/^%/
      && $raw_lines[ $template_line - 2 ] !~ m/[;{("']/ )
    {
      Rex::Logger::debug(
        "Template Error in compiled line: $code_lines[$error_line-1]");
      Rex::Logger::info(
        "Template Error somewhere around: $raw_lines[$template_line-2]",
        "error" );

      my $template_line_ = $template_line - 1;
      $tpl_error =
        "Maybe missing <<;, {, (, \" or '>> in code line: ,,$raw_lines[$template_line-2]'' line $template_line_.\nOriginal Error:\n$error\n";
    }
    else {
      $tpl_error =
        "Failed parsing template. Unkown error near $template_line.\nOriginal Error:\n$error\n";
    }

    die $tpl_error;
  }

  $tpl_code->( $self, @code_values );

  return $self->{__output__};
}

sub __out {
  my ( $self, $str ) = @_;

  $self->{__output__} .= $str;
}

sub _parse {
  my ( $self, $c ) = @_;

  my $parsed = "";

  my @chars = split( //, $c );

  my $begin_line        = 0;
  my $code_line         = 0;
  my $code_block        = 0;
  my $code_block_output = 0;
  my $current_char_idx  = -1;
  my $line_count        = 1;
  my $string_open       = 0;
  my $skip_next         = 0;
  my $skip_next_newline = 0;

  for my $curr_char (@chars) {
    $current_char_idx++;

    if ($skip_next) {
      $skip_next = 0;
      next;
    }

    my $prev_char = $chars[ $current_char_idx - 1 ] || "";
    my $next_char = $chars[ $current_char_idx + 1 ] || "";

    if ( $skip_next_newline && $curr_char eq "\n" ) {
      $skip_next_newline = 0;
      $curr_char         = "";
    }

    if ( $curr_char eq "\n" && $prev_char ne "\n" ) { # count lines, for error messages
      $line_count++;
      $parsed .= $curr_char;

      if ($string_open) {
        $parsed .= "});\n";
      }

      # reset vars
      $code_line   = 0;
      $string_open = 0;

      next;
    }

    if ( $curr_char eq "\n" && $prev_char eq "\n" ) {
      $parsed .= "\$self->__out(q{\n});\n";
      $line_count++;
      next;
    }

    if ( $curr_char eq "-"
      && $next_char eq "%"
      && ( $prev_char eq " " || $prev_char eq "\n" )
      && $chars[ $current_char_idx + 2 ] eq ">" )
    {
      # skip "-" of -%> sequence
      $skip_next_newline = 1;
      next;
    }

    # catch code line
    # % some code
    if (
      !$code_block
      && ( $prev_char eq "\n"
        || $current_char_idx == 0 ) # first line or new line
      && $curr_char eq "%"
      && $next_char eq " "          # code block, and no % char escape sequence
      )
    {
      $code_line = 1;
      $parsed .= "\n# LINE: $line_count\n";
      next;
    }

    # catch '<% ' ...
    if ( $prev_char eq "<"
      && $curr_char eq "%"
      && ( $next_char eq " " || $next_char eq "\n" ) )
    {
      $code_block = 1;
      if ($string_open) {
        $parsed .= "});\n";
        $string_open = 0;
      }

      $parsed .= "\n# LINE: $line_count\n";

      next;
    }

    # catch ' %>'
    if ( $code_block
      && ( $prev_char eq " " || $prev_char eq "\n" || $prev_char eq "-" )
      && $curr_char eq "%"
      && $next_char eq ">" )
    {
      $code_block = 0;

      if ($code_block_output) {
        $parsed .= ");\n";
        $code_block_output = 0;
      }

      $string_open = 1;
      $parsed .= "\n\$self->__out(q{";

      next;
    }

    # catch '<%='
    if ( $prev_char eq "<" && $curr_char eq "%" && $next_char eq "=" ) {
      $code_block        = 1;
      $code_block_output = 1;

      if ($string_open) {
        $parsed .= "});\n";
      }

      $parsed .= "\n# LINE: $line_count\n";
      $parsed .= "\$self->__out(";
      $skip_next = 1;

      next;
    }

    if ( $code_line || $code_block ) {
      $parsed .= $curr_char;
      next;
    }

    if ( !$string_open ) {
      $string_open = 1;
      $parsed .= '$self->__out(q{';
    }

    # don't catch opening <
    if ( $curr_char eq "<" && $next_char eq "%" ) {
      next;
    }

    # don't catch closing >
    if ( $curr_char eq ">" && $prev_char eq "%" ) {
      next;
    }

    # escaping of % sign
    if ( $curr_char eq "%" && $prev_char eq "%" ) {
      next;
    }

    $parsed .= $curr_char;

  }

  if ($string_open) {
    $parsed .= "});\n";
  }

  return $parsed;

}

sub _replace_var {
  my ( $var, $t_vars ) = @_;

  if ( exists $t_vars->{$var} ) {
    return '$' . $var;
  }
  else {
    return '$::' . $var;
  }
}

1;