# Copyright (C) 2008-2009, Sebastian Riedel. package Mojo::Template; use strict; use warnings; use base 'Mojo::Base'; use constant DEBUG => $ENV{MOJO_TEMPLATE_DEBUG} || 0; use Carp 'croak'; use IO::File; __PACKAGE__->attr(code => (chained => 1, default => '')); __PACKAGE__->attr(comment_mark => (chained => 1, default => '#')); __PACKAGE__->attr(compiled => (chained => 1)); __PACKAGE__->attr(expression_mark => (chained => 1, default => '=')); __PACKAGE__->attr(line_start => (chained => 1, default => '%')); __PACKAGE__->attr(template => (chained => 1, default => '')); __PACKAGE__->attr(tree => (chained => 1, default => sub { [] })); __PACKAGE__->attr(tag_start => (chained => 1, default => '<%')); __PACKAGE__->attr(tag_end => (chained => 1, default => '%>')); sub build { my $self = shift; # Compile my @lines; for my $line (@{$self->tree}) { # New line push @lines, ''; for (my $j = 0; $j < @{$line}; $j += 2) { my $type = $line->[$j]; my $value = $line->[$j + 1]; # Need to fix line ending? my $newline = chomp $value; # Text if ($type eq 'text') { # Quote and fix line ending $value = quotemeta($value); $value .= '\n' if $newline; $lines[-1] .= "\$_MOJO .= \"" . $value . "\";"; } # Code if ($type eq 'code') { $lines[-1] .= "$value;"; } # Expression if ($type eq 'expr') { $lines[-1] .= "\$_MOJO .= $value;"; } } } # Wrap $lines[0] ||= ''; $lines[0] = q/sub { my $_MOJO = '';/ . $lines[0]; $lines[-1] .= q/return $_MOJO; };/; $self->code(join "\n", @lines); return $self; } sub compile { my $self = shift; # Shortcut my $code = $self->code; return undef unless $code; # Catch compilation warnings local $SIG{__WARN__} = sub { my $error = shift; warn $self->_error($error); }; # Compile my $compiled = eval $code; die $self->_error($@) if $@; $self->compiled($compiled); return $self; } sub interpret { my $self = shift; my $output = shift; # Shortcut my $compiled = $self->compiled; return undef unless $compiled; # Catch interpreter warnings local $SIG{__WARN__} = sub { my $error = shift; warn $self->_error($error); }; # Interpret $$output = eval { $compiled->(@_) }; if ($@) { $$output = $self->_error($@); return 0; } return 1; } # I am so smart! I am so smart! S-M-R-T! I mean S-M-A-R-T... sub parse { my ($self, $tmpl) = @_; $self->template($tmpl); # Clean start delete $self->{tree}; # Tags my $line_start = quotemeta $self->line_start; my $tag_start = quotemeta $self->tag_start; my $tag_end = quotemeta $self->tag_end; my $cmnt_mark = quotemeta $self->comment_mark; my $expr_mark = quotemeta $self->expression_mark; # Tokenize my $state = 'text'; my $multiline_expression = 0; for my $line (split /\n/, $tmpl) { # Perl line without return value if ($line =~ /^$line_start\s+(.+)$/) { push @{$self->tree}, ['code', $1]; $multiline_expression = 0; next; } # Perl line with return value if ($line =~ /^$line_start$expr_mark\s+(.+)$/) { push @{$self->tree}, ['expr', $1]; $multiline_expression = 0; next; } # Comment line, dummy token needed for line count if ($line =~ /^$line_start$cmnt_mark\s+(.+)$/) { push @{$self->tree}, []; $multiline_expression = 0; next; } # Escaped line ending? if ($line =~ /(\\+)$/) { my $length = length $1; # Newline escaped if ($length == 1) { $line =~ s/\\$//; } # Backslash escaped if ($length >= 2) { $line =~ s/\\\\$/\\/; $line .= "\n"; } } # Normal line ending else { $line .= "\n" } # Mixed line my @token; for my $token ( split / ( $tag_start$expr_mark # Expression | $tag_start$cmnt_mark # Comment | $tag_start # Code | $tag_end # End ) /x, $line ) { # Garbage next unless $token; # End if ($token =~ /^$tag_end$/) { $state = 'text'; $multiline_expression = 0; } # Code elsif ($token =~ /^$tag_start$/) { $state = 'code' } # Comment elsif ($token =~ /^$tag_start$cmnt_mark$/) { $state = 'cmnt' } # Expression elsif ($token =~ /^$tag_start$expr_mark$/) { $state = 'expr'; } # Value else { # Comments are ignored next if $state eq 'cmnt'; # Multiline expressions are a bit complicated, # only the first line can be compiled as 'expr' $state = 'code' if $multiline_expression; $multiline_expression = 1 if $state eq 'expr'; # Store value push @token, $state, $token; } } push @{$self->tree}, \@token; } return $self; } sub render { my $self = shift; my $tmpl = shift; # Parse $self->parse($tmpl); # Build $self->build; # Compile $self->compile; # Interpret return $self->interpret(@_); } sub render_file { my $self = shift; my $path = shift; # Open file my $file = IO::File->new; $file->open("< $path") || croak "Can't open template '$path': $!"; # Slurp file my $tmpl = ''; while ($file->sysread(my $buffer, 4096, 0)) { $tmpl .= $buffer; } # Render return $self->render($tmpl, @_); } sub render_file_to_file { my $self = shift; my $spath = shift; my $tpath = shift; # Render my $output; return 0 unless $self->render_file($spath, \$output, @_); # Write to file return $self->_write_file($tpath, $output); } sub render_to_file { my $self = shift; my $tmpl = shift; my $path = shift; # Render my $output; return 0 unless $self->render($tmpl, \$output, @_); # Write to file return $self->_write_file($path, $output); } sub _context { my ($self, $text, $line) = @_; $line -= 1; my $nline = $line + 1; my $pline = $line - 1; my $nnline = $line + 2; my $ppline = $line - 2; my @lines = split /\n/, $text; # Context my $context = (($line + 1) . ': ' . $lines[$line] . "\n"); # -1 $context = (($pline + 1) . ': ' . $lines[$pline] . "\n" . $context) if $lines[$pline]; # -2 $context = (($ppline + 1) . ': ' . $lines[$ppline] . "\n" . $context) if $lines[$ppline]; # +1 $context = ($context . ($nline + 1) . ': ' . $lines[$nline] . "\n") if $lines[$nline]; # +2 $context = ($context . ($nnline + 1) . ': ' . $lines[$nnline] . "\n") if $lines[$nnline]; return $context; } # Debug goodness sub _error { my ($self, $error) = @_; # No trace in production mode return undef unless DEBUG; # Line if ($error =~ /at\s+\(eval\s+\d+\)\s+line\s+(\d+)/) { my $line = $1; my $delim = '-' x 76; my $report = "\nTemplate error around line $line.\n"; my $template = $self->_context($self->template, $line); $report .= "$delim\n$template$delim\n"; # Advanced debugging if (DEBUG >= 2) { my $code = $self->_context($self->code, $line); $report .= "$code$delim\n"; } $report .= "$error\n"; return $report; } # No line found return "Template error: $error"; } sub _write_file { my ($self, $path, $output) = @_; # Write to file my $file = IO::File->new; $file->open("> $path") or croak "Can't open file '$path': $!"; $file->syswrite($output) or croak "Can't write to file '$path': $!"; return 1; } 1; __END__ =head1 NAME Mojo::Template - Perlish Templates! =head1 SYNOPSIS use Mojo::Template; my $mt = Mojo::Template->new; # Simple my $output; $mt->render(<<'EOF', \$output);
Time: <%= localtime(time) %> EOF print $output; # More complicated my $output; $mt->render(<<'EOF', \$output, 23, 'foo bar'); %= 5 * 5 % my ($number, $text) = @_; test 123 foo <% my $i = $number + 2 %> % for (1 .. 23) { * some text <%= $i++ %> % } EOF print $output; =head1 DESCRIPTION L
my $code = $mt->code;
$mt = $mt->code($code);
=head2 C
my $comment_mark = $mt->comment_mark;
$mt = $mt->comment_mark('#');
=head2 C
my $expression_mark = $mt->expression_mark;
$mt = $mt->expression_mark('=');
=head2 C
my $line_start = $mt->line_start;
$mt = $mt->line_start('%');
=head2 C
my $template = $mt->template;
$mt = $mt->template($template);
=head2 C
my $tree = $mt->tree;
$mt = $mt->tree($tree);
=head2 C
my $tag_start = $mt->tag_start;
$mt = $mt->tag_start('<%');
=head2 C
my $tag_end = $mt->tag_end;
$mt = $mt->tag_end('%>');
=head1 METHODS
L inherits all methods from L and implements the
following new ones.
=head2 C
my $mt = Mojo::Template->new;
=head2 C
$mt = $mt->build;
=head2 C
$mt = $mt->compile;
=head2 C
my $success = $mt->interpret;
my $success = $mt->interpret(\$output, @arguments);
=head2 C
$mt = $mt->parse($template);
=head2 C
my $success = $mt->render($template);
my $success = $mt->render($template, \$output, @arguments);
=head2 C
my $success = $mt->render_file($template_file);
my $success = $mt->render_file($template_file, \$result, @arguments);
=head2 C
my $success = $mt->render_file_to_file($template_file, $output_file);
my $success = $mt->render_file_to_file(
$template_file, $output_file, @arguments
);
=head2 C
my $success = $mt->render_to_file($template, $output_file);
my $success = $mt->render_to_file(
$template, $output_file, @arguments
);
=cut