The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use warnings;

=head1 NAME

EJS::Template::Parser::Context - Implementation of EJS::Template::Parser

=cut

package EJS::Template::Parser::Context;
use base 'EJS::Template::Base';

use EJS::Template::Runtime;

my $states = {
    'INIT' => {
        key => 'INIT', method => '_in_init',
        js_open => '', js_close => '',
    },
    'TEXT' => {
        key => 'TEXT', method => '_in_text',
        js_open => 'print("', js_close => '");',
    },
    '<%' => {
        key => '<%'  , method => '_in_script',
        js_open => '', js_close => '',
    },
    '<%=' => {
        key => '<%=' , method => '_in_script',
        js_open => 'print(' , js_close => ');',
    },
};

=head1 Methods

=head2 new

Creates a new parser context.

=cut

sub new {
    my ($class, $config) = @_;
    my $self = $class->SUPER::new($config);
    
    $self->{stack} = [];
    $self->{result} = [];
    
    $self->{default_escape} = do {
        my $name = $self->config('escape') || '';
        
        if ($name eq '' || $name eq 'raw') {
            '';
        } else {
            $EJS::Template::Runtime::ESCAPES{$name} || '';
        }
    };
    
    $self->_push_state('INIT');
    return $self;
}

=head2 read_line

Parses a line.

=cut

sub read_line {
    my ($self, $line) = @_;
    
    while ($line =~ m{(.*?)((^\s*)?<%(?:(?:\s*:\s*\w+\s*)?=)?|%>(\s*?(?:\n|$))?|\\?["']|/\*|\*/|//|\n|$)}g) {
        my ($text, $mark, $left, $right) = ($1, $2, $3, $4);
        my $escape;
        
        if ($mark =~ s/<%\s*:\s*(\w+)\s*=/<%=/) {
            my $name = $1;
            $escape = $EJS::Template::Runtime::ESCAPES{$name} || '';
        } elsif ($mark eq '<%=') {
            $escape = $self->{default_escape};
        }
        
        $mark =~ s/\s+(<%=?)/$1/;
        $mark =~ s/(%>)\s+/$1/;
        
        my $opt = {
            left   => $left  ,
            right  => $right ,
            escape => $escape,
        };
        
        $self->_handle_token($text) if $text ne '';
        $self->_handle_token($mark, $opt) if $mark ne '';
    }
}

=head2 result

Retrieves the result (an array ref of the generated JavaScript texts).

=cut

sub result {
    my ($self) = @_;
    $self->_pop_state() until @{$self->{stack}} <= 1;
    return $self->{result};
}

sub _handle_token {
    my ($self, $token, $opt) = @_;
    my $method = $self->{stack}[-1]{state}{method};
    return $self->$method($token, $opt);
}

sub _top_key {
    my ($self) = @_;
    return $self->{stack}[-1]{state}{key};
}

sub _top_opt {
    my ($self) = @_;
    return $self->{stack}[-1]{opt} || {};
}

sub _append_result {
    my ($self, $text) = @_;
    push @{$self->{result}}, $text;
}

sub _push_state {
    my ($self, $state_key, $opt) = @_;
    my $state = $states->{$state_key};
    $self->_append_result($state->{js_open}) if $state->{js_open} ne '';
    my $entry = {state => $state, opt => $opt};
    push @{$self->{stack}}, $entry;
}

sub _pop_state {
    my ($self) = @_;
    my $entry = pop @{$self->{stack}};
    my $state = $entry->{state};
    $self->_append_result($state->{js_close}) if $state->{js_close} ne '';
}

sub _in_init {
    my ($self, $token, $opt) = @_;
    
    if ($token eq '<%') {
        if (defined $opt->{left}) {
            $opt->{ltrim} = {};
            
            if ($opt->{left} ne '') {
                $self->_append_result($opt->{left});
                $opt->{ltrim}{index} = $#{$self->{result}};
            }
        }
        
        $self->_push_state($token, $opt);
    } elsif ($token eq "<%=") {
        if (defined $opt->{left} && $opt->{left} ne '') {
            $self->_push_state('TEXT');
            $self->_append_result($opt->{left});
            $self->_pop_state();
        }
        
        $self->_push_state($token, $opt);
        
        if (my $escape = $opt->{escape}) {
            $self->_append_result(qq{EJS.$escape(});
        }
    } elsif ($token eq "\n") {
        $self->_push_state('TEXT');
        $self->_append_result("\\n");
        $self->_pop_state();
        $self->_append_result("\n");
    } else {
        $token =~ s/([\\"])/\\$1/g;
        $self->_push_state('TEXT');
        $self->_append_result($token);
    }
}

sub _in_text {
    my ($self, $token, $opt) = @_;
    
    if ($token eq '<%') {
        $self->_pop_state();
        $self->_push_state($token, $opt);
    } elsif ($token eq '<%=') {
        $self->_pop_state();
        $self->_push_state($token, $opt);
        
        if (my $escape = $opt->{escape}) {
            $self->_append_result(qq{EJS.$escape(});
        }
    } elsif ($token eq "\n") {
        $self->_append_result("\\n");
        $self->_pop_state();
        $self->_append_result("\n");
    } else {
        $token =~ s/([\\"])/\\$1/g;
        $self->_append_result($token);
    }
}

sub _in_script {
    my ($self, $token, $opt) = @_;
    
    if ($token eq '%>') {
        if (my $ltrim = $self->_top_opt->{ltrim}) {
            if (defined $opt->{right}) {
                if ($opt->{right} ne '') {
                    $self->_append_result($opt->{right});
                }
            } else {
                if (defined(my $idx = $ltrim->{index})) {
                    my $left = $self->{result}[$idx];
                    $self->{result}[$idx] = qq{print("$left");};
                }
            }
            
            $self->_pop_state();
        } else {
            if ($self->_top_opt->{escape}) {
                $self->_append_result(qq{)});
            }
            
            $self->_pop_state();
            
            my $right = $opt->{right};
            
            if (defined $right && $right ne '') {
                if ($right =~ s/\n$//) {
                    $self->_append_result(qq{print("$right\\n");\n});
                } else {
                    $self->_append_result(qq{print("$right");});
                }
            }
        }
    } else {
        $self->_append_result($token);
    }
}

=head1 SEE ALSO

=over 4

=item * L<EJS::Template>

=item * L<EJS::Template::Parser>

=back

=cut

1;