package Mojo::Template; use Mojo::Base -base; use Carp 'croak'; use Mojo::ByteStream; use Mojo::Exception; use Mojo::Util qw(decode encode monkey_patch slurp); use constant DEBUG => $ENV{MOJO_TEMPLATE_DEBUG} || 0; has [qw(auto_escape compiled)]; has [qw(append code prepend template)] => ''; has capture_end => 'end'; has capture_start => 'begin'; has comment_mark => '#'; has encoding => 'UTF-8'; has escape => sub { \&Mojo::Util::xml_escape }; has [qw(escape_mark expression_mark trim_mark)] => '='; has [qw(line_start replace_mark)] => '%'; has name => 'template'; has namespace => 'Mojo::Template::SandBox'; has tag_start => '<%'; has tag_end => '%>'; has tree => sub { [] }; sub build { my $self = shift; my (@lines, $cpst, $multi); my $escape = $self->auto_escape; for my $line (@{$self->tree}) { push @lines, ''; for (my $j = 0; $j < @{$line}; $j += 2) { my $type = $line->[$j]; my $value = $line->[$j + 1] || ''; my $newline = chomp $value; # Capture end if ($type eq 'cpen') { # End block $lines[-1] .= 'return Mojo::ByteStream->new($_M) }'; # No following code my $next = $line->[$j + 3]; $lines[-1] .= ';' if !defined $next || $next =~ /^\s*$/; } # Text if ($type eq 'text') { # Quote and fix line ending $value = quotemeta $value; $value .= '\n' if $newline; $lines[-1] .= "\$_M .= \"" . $value . "\";" if length $value; } # Code or multiline expression if ($type eq 'code' || $multi) { $lines[-1] .= "$value" } # Expression if ($type eq 'expr' || $type eq 'escp') { # Start unless ($multi) { # Escaped if (($type eq 'escp' && !$escape) || ($type eq 'expr' && $escape)) { $lines[-1] .= "\$_M .= _escape"; $lines[-1] .= " scalar $value" if length $value; } # Raw else { $lines[-1] .= "\$_M .= scalar $value" } } # Multiline $multi = !(($line->[$j + 2] // '') eq 'text' && ($line->[$j + 3] // '') eq ''); # Append semicolon $lines[-1] .= ';' if !$multi && !$cpst; } # Capture start if ($cpst) { $lines[-1] .= $cpst; $cpst = undef; } $cpst = " sub { my \$_M = ''; " if $type eq 'cpst'; } } return $self->code($self->_wrap(\@lines))->tree([]); } sub compile { my $self = shift; # Compile with line directive return undef unless my $code = $self->code; my $name = $self->name; $name =~ s/"//g; my $compiled = eval qq{#line 1 "$name"\n$code}; $self->compiled($compiled) and return undef unless $@; # Use local stacktrace for compile exceptions return Mojo::Exception->new($@, [$self->template, $code])->trace->verbose(1); } sub interpret { my $self = shift; # Stacktrace local $SIG{__DIE__} = sub { CORE::die($_[0]) if ref $_[0]; Mojo::Exception->throw(shift, [$self->template, $self->code]); }; return undef unless my $compiled = $self->compiled; my $output = eval { $compiled->(@_) }; return $output unless $@; # Exception with template context return Mojo::Exception->new($@, [$self->template])->verbose(1); } sub parse { my ($self, $template) = @_; # Clean start my $tree = $self->template($template)->tree([])->tree; my $tag = $self->tag_start; my $replace = $self->replace_mark; my $expr = $self->expression_mark; my $escp = $self->escape_mark; my $cpen = $self->capture_end; my $cmnt = $self->comment_mark; my $cpst = $self->capture_start; my $trim = $self->trim_mark; my $end = $self->tag_end; my $start = $self->line_start; my $token_re = qr/ ( \Q$tag$replace\E # Replace | \Q$tag$expr$escp\E\s*\Q$cpen\E(?!\w) # Escaped expression (end) | \Q$tag$expr$escp\E # Escaped expression | \Q$tag$expr\E\s*\Q$cpen\E(?!\w) # Expression (end) | \Q$tag$expr\E # Expression | \Q$tag$cmnt\E # Comment | \Q$tag\E\s*\Q$cpen\E(?!\w) # Code (end) | \Q$tag\E # Code | (?_trim(\@token); } # Hint at end push @token, 'text', ''; } # Code elsif ($token =~ /^\Q$tag\E$/) { $state = 'code' } # Expression elsif ($token =~ /^\Q$tag$expr\E$/) { $state = 'expr' } # Expression that needs to be escaped elsif ($token =~ /^\Q$tag$expr$escp\E$/) { $state = 'escp' } # Comment elsif ($token =~ /^\Q$tag$cmnt\E$/) { $state = 'cmnt' } # Text else { # Replace $token = $tag if $token eq "$tag$replace"; # Convert whitespace text to line noise if ($trimming && $token =~ s/^(\s+)//) { push @token, 'code', $1; $trimming = 0; } # Comments are ignored next if $state eq 'cmnt'; push @token, @capture_token, $state, $token; @capture_token = (); } } push @$tree, \@token; } return $self; } sub render { my $self = shift; return $self->parse(shift)->build->compile || $self->interpret(@_); } sub render_file { my ($self, $path) = (shift, shift); $self->name($path) unless defined $self->{name}; my $template = slurp $path; my $encoding = $self->encoding; croak qq{Template "$path" has invalid encoding.} if $encoding && !defined($template = decode $encoding, $template); return $self->render($template, @_); } sub _trim { my ($self, $line) = @_; # Walk line backwards for (my $j = @$line - 4; $j >= 0; $j -= 2) { # Skip captures next if $line->[$j] eq 'cpst' || $line->[$j] eq 'cpen'; # Only trim text return unless $line->[$j] eq 'text'; # Convert whitespace text to line noise my $value = $line->[$j + 1]; if ($line->[$j + 1] =~ s/(\s+)$//) { $value = $line->[$j + 1]; splice @$line, $j, 0, 'code', $1; } # Text left return if length $value; } } sub _wrap { my ($self, $lines) = @_; # Escape function my $escape = $self->escape; monkey_patch $self->namespace, _escape => sub { no warnings 'uninitialized'; ref $_[0] eq 'Mojo::ByteStream' ? $_[0] : $escape->("$_[0]"); }; # Wrap lines my $first = $lines->[0] ||= ''; $lines->[0] = "package @{[$self->namespace]}; use Mojo::Base -strict;"; $lines->[0] .= "sub { my \$_M = ''; @{[$self->prepend]}; do { $first"; $lines->[-1] .= "@{[$self->append]}; \$_M } };"; my $code = join "\n", @$lines; warn "-- Code for @{[$self->name]}\n@{[encode 'UTF-8', $code]}\n\n" if DEBUG; return $code; } 1; =encoding utf8 =head1 NAME Mojo::Template - Perl-ish templates! =head1 SYNOPSIS use Mojo::Template; my $mt = Mojo::Template->new; # Simple my $output = $mt->render(<<'EOF'); % use Time::Piece; Simple % my $now = localtime; Time: <%= $now->hms %> EOF say $output; # More advanced my $output = $mt->render(<<'EOF', 23, 'foo bar'); % my ($num, $text) = @_; %= 5 * 5 More advanced test 123 foo <% my $i = $num + 2; %> % for (1 .. 23) { * some text <%= $i++ %> % } EOF say $output; =head1 DESCRIPTION L is a minimalistic and very Perl-ish template engine, designed specifically for all those small tasks that come up during big projects. Like preprocessing a configuration file, generating text from heredocs and stuff like that. See L for information on how to generate content with the L renderer. =head1 SYNTAX For all templates L, L, L and Perl 5.10 features are automatically enabled. <% Perl code %> <%= Perl expression, replaced with result %> <%== Perl expression, replaced with XML escaped result %> <%# Comment, useful for debugging %> <%% Replaced with "<%", useful for generating templates %> % Perl code line, treated as "<% line =%>" %= Perl expression line, treated as "<%= line %>" %== Perl expression line, treated as "<%== line %>" %# Comment line, useful for debugging %% Replaced with "%", useful for generating templates Escaping behavior can be reversed with the L attribute, this is the default in L C<.ep> templates for example. <%= Perl expression, replaced with XML escaped result %> <%== Perl expression, replaced with result %> L objects are always excluded from automatic escaping. % use Mojo::ByteStream 'b'; <%= b('
excluded!
') %> Newline characters can be escaped with a backslash. This is <%= 1 + 1 %> a\ single line And a backslash in front of a newline character can be escaped with another backslash. This will <%= 1 + 1 %> result\\ in multiple\\ lines Whitespace characters around tags can be trimmed with a special tag ending. <%= All whitespace characters around this expression will be trimmed =%> You can capture whole template blocks for reuse later with the C and C keywords. <% my $block = begin %> <% my $name = shift; =%> Hello <%= $name %>. <% end %> <%= $block->('Baerbel') %> <%= $block->('Wolfgang') %> Perl lines can also be indented freely. % my $block = begin % my $name = shift; Hello <%= $name %>. % end %= $block->('Baerbel') %= $block->('Wolfgang') L templates get compiled to a Perl subroutine, that means you can access arguments simply via C<@_>. % my ($foo, $bar) = @_; % my $x = shift; test 123 <%= $foo %> The compilation of templates to Perl code can make debugging a bit tricky, but L will return L objects that stringify to error messages with context. Bareword "xx" not allowed while "strict subs" in use at template line 4. 2: 3: 4: % my $i = 2; xx 5: %= $i * 2 6: =head1 ATTRIBUTES L implements the following attributes. =head2 auto_escape my $bool = $mt->auto_escape; $mt = $mt->auto_escape($bool); Activate automatic escaping. =head2 append my $code = $mt->append; $mt = $mt->append('warn "Processed template"'); Append Perl code to compiled template. Note that this code should not contain newline characters, or line numbers in error messages might end up being wrong. =head2 capture_end my $end = $mt->capture_end; $mt = $mt->capture_end('end'); Keyword indicating the end of a capture block, defaults to C. <% my $block = begin %> Some data! <% end %> =head2 capture_start my $start = $mt->capture_start; $mt = $mt->capture_start('begin'); Keyword indicating the start of a capture block, defaults to C. <% my $block = begin %> Some data! <% end %> =head2 code my $code = $mt->code; $mt = $mt->code($code); Perl code for template. =head2 comment_mark my $mark = $mt->comment_mark; $mt = $mt->comment_mark('#'); Character indicating the start of a comment, defaults to C<#>. <%# This is a comment %> =head2 compiled my $compiled = $mt->compiled; $mt = $mt->compiled($compiled); Compiled template code. =head2 encoding my $encoding = $mt->encoding; $mt = $mt->encoding('UTF-8'); Encoding used for template files. =head2 escape my $cb = $mt->escape; $mt = $mt->escape(sub { reverse $_[0] }); A callback used to escape the results of escaped expressions, defaults to L. =head2 escape_mark my $mark = $mt->escape_mark; $mt = $mt->escape_mark('='); Character indicating the start of an escaped expression, defaults to C<=>. <%== $foo %> =head2 expression_mark my $mark = $mt->expression_mark; $mt = $mt->expression_mark('='); Character indicating the start of an expression, defaults to C<=>. <%= $foo %> =head2 line_start my $start = $mt->line_start; $mt = $mt->line_start('%'); Character indicating the start of a code line, defaults to C<%>. % $foo = 23; =head2 name my $name = $mt->name; $mt = $mt->name('foo.mt'); Name of template currently being processed, defaults to C