package App::Followme::Template;
use 5.008005;
use strict;
use warnings;
use integer;
use lib '../..';
use Carp;
use App::Followme::FIO;
use App::Followme::Web;
use base qw(App::Followme::ConfiguredObject);
our $VERSION = "1.90";
use constant COMMAND_START => '<!-- ';
use constant COMMAND_END => '-->';
#----------------------------------------------------------------------
# Compile a template into a subroutine which when called fills itself
sub compile {
my ($pkg, $template) = @_;
my $self = ref $pkg ? $pkg : $pkg->new();
my @lines = split(/\n/, $template);
my $start = <<'EOQ';
sub {
my ($meta, $item, $loop) = @_;
my @text;
my @loop;
@loop = @$loop if defined $loop;
EOQ
my @mid = $self->parse_code(\@lines);
my $end .= <<'EOQ';
return join('', @text);
}
EOQ
my $code = join("\n", $start, @mid, $end);
my $sub = eval ($code);
croak $@ unless $sub;
return $sub;
}
#----------------------------------------------------------------------
# Replace variable references with hashlist fetches
sub encode_expression {
my ($self, $value) = @_;
if (defined $value) {
my $pre = '{$meta->build(\'';
my $post = '\', $item, \@loop)}';
$value =~ s/(?<!\\)([\$\@])(\w+)/$1$pre$1$2$post/g;
} else {
$value = '';
}
return $value;
}
#----------------------------------------------------------------------
# Get the translation of a template command
sub get_command {
my ($self, $cmd) = @_;
my $commands = {
do => '%%;',
for => 'if (%%) { foreach my $item (my @loop = (%%)) {',
endfor => '}}',
if => 'if (%%) { do {',
elsif => '}} elsif (%%) { do {',
else => '}} else { do {',
endif => '}}',
};
return $commands->{$cmd};
}
#----------------------------------------------------------------------
# Parse the templace source
sub parse_code {
my ($self, $lines, $command) = @_;
my @code;
my @stash;
while (defined (my $line = shift @$lines)) {
my ($cmd, $cmdline) = $self->parse_command($line);
if (defined $cmd) {
if (@stash) {
push(@code, 'push @text, <<"EOQ";', @stash, 'EOQ');
@stash = ();
}
push(@code, $cmdline);
if (substr($cmd, 0, 3) eq 'end') {
my $startcmd = substr($cmd, 3);
die "Mismatched block end ($command/$cmd)"
if defined $startcmd && $startcmd ne $command;
return @code;
} elsif ($self->get_command("end$cmd")) {
push(@code, $self->parse_code($lines, $cmd));
}
} else {
push(@stash, $self->encode_expression($line));
}
}
die "Missing end (end$command)" if $command;
push(@code, 'push @text, <<"EOQ";', @stash, 'EOQ') if @stash;
return @code;
}
#----------------------------------------------------------------------
# Parse a command and its argument
sub parse_command {
my ($self, $line) = @_;
my $command_start_pattern = COMMAND_START;
return unless $line =~ s/$command_start_pattern//;
my $command_end_pattern = COMMAND_END;
$line =~ s/$command_end_pattern//;
my ($cmd, $arg) = split(' ', $line, 2);
$arg = '' unless defined $arg;
my $cmdline = $self->get_command($cmd);
return unless $cmdline;
$arg = $self->encode_expression($arg);
$cmdline =~ s/%%/$arg/g;
return ($cmd, $cmdline);
}
#----------------------------------------------------------------------
# Set the regular expression patterns used to match a command
sub setup {
my ($self, %configuration) = @_;
$self->{command_start_pattern} = '^\s*' . quotemeta(COMMAND_START);
$self->{command_end_pattern} = '\s*' . quotemeta(COMMAND_END) . '\s*$';
return;
}
1;
=pod
=encoding utf-8
=head1 NAME
App::Followme::Template - Handle templates and prototype files
=head1 SYNOPSIS
use App::Followme::Template;
my $template = App::Followme::Template->new;
my $render = $template->compile($template_file);
my $output = $render->($hash);
=head1 DESCRIPTION
This module contains the methods that perform template handling. A Template is a
file containing commands and variables for making a web page. First, the
template is compiled into a subroutine and then the subroutine is called with a
hash as an argument to fill in the variables and produce a web
page.
=head1 METHODS
This module has one public method:
=over 4
=item $sub = $self->compile($template_file);
Compile a template and return the compiled subroutine. A template if a file
containing commands and variables that describe how data is to be represented.
The method returns a subroutine reference, which when called with a metadata
object, returns a web page containing the fields from the metadata substituted
into variables in the template. Variables in the template are preceded by Perl
sigils, so that a link would look like:
<li><a href="$url">$title</a></li>
=back
=head1 TEMPLATE SYNTAX
Templates support the basic control structures in Perl: "for" loops and
"if-else" blocks. Creating output is a two step process. First you generate a
subroutine from one or more templates, then you call the subroutine with your
data to generate the output.
The template format is line oriented. Commands are enclosed in html comments
(<!-- -->). A command may be preceded by white space. If a command is a block
command, it is terminated by the word "end" followed by the command name. For
example, the "for" command is terminated by an "endfor" command and the "if"
command by an "endif" command.
All lines may contain variables. As in Perl, variables are a sigil character
('$' or '@') followed by one or more word characters. For example, C<$name> or
C<@names>. To indicate a literal character instead of a variable, precede the
sigil with a backslash. When you run the subroutine that this module generates,
you pass it a metadata object. The subroutine replaces variables in the template
with the value in the field built by the metadata object.
If the first non-white characters on a line are the command start string, the
line is interpreted as a command. The command name continues up to the first
white space character. The text following the initial span of white space is the
command argument. The argument continues up to the command end string.
Variables in the template have the same format as ordinary Perl variables,
a string of word characters starting with a sigil character. for example,
$body @files
are examples of variables. The following commands are supported in templates:
=over 4
=item do
The remainder of the line is interpreted as Perl code.
=item for
Expand the text between the "for" and "endfor" commands several times. The
argument to the "for" command should be an expression evaluating to a list. The
code will expand the text in the for block once for each element in the list.
<ul>
<!-- for @files -->
<li><a href="$url">$title</a></li>
<!-- endfor -->
</ul>
=item if
The text until the matching C<endif> is included only if the expression in the
"if" command is true. If false, the text is skipped.
<div class="column">
<!-- for @files -->
<!-- if $count % 20 == 0 -->
</div>
<div class="column">
<!-- endif -->
$title<br />
<!-- endfor -->
</div>
=item else
The "if" and "for" commands can contain an C<else>. The text before the "else"
is included if the expression in the enclosing command is true and the
text after the "else" is included if the "if" command is false or the "for"
command does not execute. You can also place an "elsif" command inside a block,
which includes the following text if its expression is true.
=back
=head1 ERRORS
What to check when this module throws an error
=over 4
=item Couldn't read template
The template is in a file and the file could not be opened. Check the filename
and permissions on the file. Relative filenames can cause problems and the web
server is probably running another account than yours.
=item Unknown command
Either a command was spelled incorrectly or a line that is not a command
begins with the command start string.
=item Missing end
The template contains a command for the start of a block, but
not the command for the end of the block. For example an "if" command
is missing an "endif" command.
=item Mismatched block end
The parser found a different end command than the begin command for the block
it was parsing. Either an end command is missing, or block commands are nested
incorrectly.
=item Syntax error
The expression used in a command is not valid Perl.
=back
=head1 LICENSE
Copyright (C) Bernie Simon.
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=head1 AUTHOR
Bernie Simon E<lt>bernie.simon@gmail.comE<gt>
=cut