From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

use strict;
=head1 NAME
Decl::Template - implements a template in the Decl system.
=head1 VERSION
Version 0.01
=cut
our $VERSION = '0.01';
=head1 SYNOPSIS
A I<template> is a textual representation of a class of data structures. A template has named slots and other value specifications that
permit an arbitary set of data to be extracted from the environment of its invocation and formatted into a text block that can be used
to do something else. In Decl, that "something else" is often the creation of a new set of nodes, but the typical use of templates
is to express data in display text.
Decl templates are based pretty closely on JSON::Templates (a system used in Python and Javascript that I rather like).
This class, instead of implementing a template itself, actually implements a template environment. A template, after all, is just a
piece of text - and its output is also a piece of text. All the interesting stuff falls into the environment - it is in the environment
that we determine the values of all our slots, after all. And in the Decl context, that is no trivial task.
=head2 new()
The C<new> function, of course, doesn't do much except set up the engine according to the parameters.
=cut
sub new {
my ($class, %values) = @_;
my $self = bless \%values, $class;
#TODO: 'brackets' split
$self->{left} = '\[\[' unless $self->{left};
$self->{right} = '\]\]' unless $self->{right};
$self->{leftp} = $self->{left};
$self->{leftp} =~ s/\\//g;
$self->{rightp} = $self->{right};
$self->{rightp} =~ s/\\//g;
$self->{valuator} = \&default_valuator unless $self->{valuator};
$self->{leave_misses} = 1 unless defined $self->{leave_misses};
$self->{spanners} = {} unless $self->{spanners};
$self->{spanners}->{with} = \&do_with unless defined $self->{spanners}->{with};
$self->{spanners}->{if} = \&do_if unless defined $self->{spanners}->{if};
$self->{spanners}->{repeat} = \&do_repeat unless defined $self->{spanners}->{repeat};
$self;
}
=head2 default_valuator
Given a value environment (by default, a hashref) and the name of a value, a valuator finds the value.
If nothing else is specified, the hashref valuator is used. In the Decl context, a node will generally
be used and the node's own valuation function is used as the valuator.
=cut
sub default_valuator {
my ($name, $env) = @_;
$$env{$name};
}
=head2 prepare_varname
Given a variable specification, prepare it for use as a lookup key.
=cut
sub prepare_varname {
my ($name) = @_;
$name =~ s/\n */ /sg;
$name =~ s/^ *//g;
$name =~ s/ *$//g;
$name;
}
=head2 parse_spanning_command
Given a spanning command string, parse out the initial word (the command) and leave the arguments (the rest). Drop the . or +.
=cut
sub parse_spanning_command {
my $piece = shift;
$piece =~ s/^[+\.] *//;
split m[ +], $piece, 2;
}
=head2 handle_spanning_command
Given a parsed spanning command and the value object and valuator function to be used, express the command. This is really just
a dispatcher for a command table.
=cut
sub handle_spanning_command {
my ($self, $command, $values, $valuator) = @_;
my $c = $self->{spanners}->{$$command[0]};
return $c->($self, $command, $values, $valuator) if $c;
return ''; # TODO: consider better error handling. As always.
}
=head2 register_spanning_command ($name, $closure)
Here's how you register a spanning command. Just name it and provide a closure for it, and you're good to go.
=cut
sub register_spanning_command {
my ($self, $name, $sub) = @_;
$self->{spanners}->{$name} = $sub;
}
=head2 do_with, do_if, do_repeat, express_repeat
These are our three default spanning commands. More can be added to the table.
The C<express_repeat> does the heavy lifting in expressing the list template and can be recycled in other forms of list template.
=cut
sub do_with {
my ($self, $command, $values, $valuator) = @_;
my $with = do_lookup($$command[1], $values, $valuator);
$self->express_parsed ($$command[2], $with, \&default_valuator);
}
sub do_if {
my ($self, $command, $values, $valuator) = @_;
my $test = do_lookup ($$command[1], $values, $valuator);
if ($test) {
return $self->express_parsed ($$command[2], $values, $valuator);
}
my @alternatives = @{$$command[3]};
foreach my $check (@alternatives) {
if ($$check[0] eq 'elif') {
$test = do_lookup ($$check[1], $values, $valuator);
if ($test) {
return $self->express_parsed ($$check[2], $values, $valuator);
}
}
if ($$check[0] eq 'else') {
return $self->express_parsed ($$check[2], $values, $valuator);
}
}
return '';
}
sub do_repeat {
my ($self, $command, $values, $valuator) = @_;
my $loop = do_lookup ($$command[1], $values, $valuator);
my @list;
if (ref $loop eq 'ARRAY') {
@list = @$loop;
} elsif (not defined $loop) {
@list = ();
} else {
@list = ($loop);
}
$self->express_repeat ($command, $values, $valuator, @list);
}
sub express_repeat {
my $self = shift;
my $command = shift;
my $values = shift;
my $valuator = shift;
if (not @_) {
foreach my $else (@{$$command[3]}) {
if ($$else[0] eq 'else') {
return $self->express_parsed ($$else[2], $values, $valuator);
}
}
return '';
}
my $body = $$command[2];
my $before = '';
my $alternate = '';
my $after = '';
foreach (@{$$command[3]}) {
if ($$_[0] eq 'before') {
$before = $self->express_parsed ($$_[2], $values, $valuator);
next;
}
if ($$_[0] eq 'alt') {
$alternate = $self->express_parsed ($$_[2], $values, $valuator);
next;
}
if ($$_[0] eq 'after') {
$after = $self->express_parsed ($$_[2], $values, $valuator);
next;
}
if ($$_[0] eq 'body') {
$body = $$_[2];
next;
}
}
my $return = $before;
while (@_) {
my $this = shift;
$return .= $self->express_parsed ($body, $this, \&default_valuator);
$return .= $alternate if (@_);
}
$return .= $after;
$return;
}
=head2 do_lookup ($name, $values, $valuator)
Takes a value specification, cleans up the name, applies filters, and returns the final value. TODO: filters.
=cut
sub do_lookup {
my ($name, $values, $valuator) = @_;
$valuator->(prepare_varname($name), $values);
}
=head2 parse_template ($template)
This parses our template language into a kind of interlanguage consisting of interleaved plain text and commands to be carried out to generate
text. Then we can either express the command structure, or alternatively translate it into some other template language.
So. Our template language is text interspersed with fields delimited by default delimiters of [[ and ]]. (These can be overridden.)
Its output is a list (or arrayref) of commands. Plain text between fields is output exactly as-is, and so the output "command" is simply a
string containing that text.
Most fields are variable lookups, and these turn into ['lookup', '<variable>'] - where "variable" can be an extended command if our valuation
function knows how to handle them. The basic template engine, however, simply looks up names in a hashref.
Any field of the form [[.<command> <args>]] is a "dotted command" or "spanning command".
The default ones defined are .repeat, .with, and .if; you can write
your own, though. (TODO: provide a way to hook them in.) A dotted command extends until it hits [[.end]], and they can of course be
nested.
The Decl node framework will also provide a .select command, which will do exactly what you think it will. I think, based on this
alone, you could probably build a believable report generator.
Within a spanning command, you can define subranges with [[+<range> <args]]. This is used for C<elif> and C<else> in the C<.if>
command, and for C<alt> and C<else> in the C<.repeat> command. What you get back is then a hashref with any subranges stored
by name for your viewing pleasure.
The parser outputs a spanning command like this:
['<command with string arguments>',
[<list of contained template items>],
[ [<subcommand>, <subcommand arguments>, <list of contained items>],
... (this part is optional and repeated for as many subcommands as appear)
]
]
The hashref of named subspans likewise has arrayrefs for values. Each of the arrayrefs in this structure has already been parsed by the
time all is said and done.
=cut
sub parse_template {
my ($self, $template) = @_;
my @pieces;
my $main_body = [];
my @arglist = ();
my $curspan = '';
my $curspan_args = '';
if (ref($template) eq 'ARRAY') {
# An arrayref means we've already split the template and we
# just need to express it.
@pieces = @$template;
} else {
@pieces = split /$self->{left}(.*?)$self->{right}/s, $template;
}
# First step: scan the template pieces and take care of any spanning
# commands (.repeat, .with, or .if)
my @current_span = ();
my $on = 1;
my $trailing_indent;
while (@pieces) {
$on = not $on;
if (not $on) {
my $literal = shift @pieces;
push @current_span, $literal;
if ($literal =~ /\n([^\n]*?)\z/s) {
$trailing_indent = length($1);
if ($1 =~ /^\s*$/) {
$trailing_indent = 0;
}
} else {
$trailing_indent = length($literal);
}
next;
}
my $piece = shift @pieces;
if ($piece !~ /^[+\.]/) {
push @current_span, ['lookup', $piece];
next;
}
$pieces[0] =~ s/^\s*\n//s;
my ($command, $args) = parse_spanning_command ($piece);
if ($piece =~ /^\+/) {
if ($curspan eq '') {
$main_body = [@current_span];
} else {
push @arglist, [$curspan, $curspan_args, [@current_span]];
}
@current_span = ();
$curspan = $command;
$curspan_args = $args;
next;
}
if ($command eq 'end') {
if ($curspan eq '') {
$main_body = [@current_span];
} else {
push @arglist, [$curspan, $curspan_args, [@current_span]];
}
# If we've encountered an .end command, return what we've got.
return ($main_body, \@arglist, \@pieces);
}
my ($body, $arglist, $rest) = $self->parse_template (\@pieces);
push @current_span, [$command, $args, $body, $arglist];
@pieces = @$rest;
}
if ($curspan eq '') {
$main_body = \@current_span;
} else {
push @arglist, [$curspan, $curspan_args, \@current_span];
}
return ($main_body, \@arglist, \@pieces) if wantarray;
$main_body;
}
=head2 express_parsed ($template, $values, $valuator)
Given a parse tree returned from the above function, plus a value structure and a way to retrieve first-level values from it (second-level
values are presumed to be hashrefs or arrayrefs returned from the first level, and perhaps there might someday be motivation to extend
that notion to some kind of lazy evaluation, but I<today is not that day>), (breathe) returns the expressed template. Which is just a string.
Non-default spanning commands must be defined in the engine before use.
=cut
sub express_parsed {
my ($self, $template, $values, $valuator) = @_;
$values = $self->{values} || {} unless defined $values;
$valuator = $self->{valuator} unless defined $valuator;
my $return = '';
my $indent = 0;
my $literal;
my $value = undef;
foreach my $piece (@$template) {
next unless defined $piece; # Just in case.
if (not ref $piece) { # Strings just pass through.
$value = $piece;
$literal = 1;
} else {
next unless ref $piece eq 'ARRAY'; # Anything but an arrayref or string will be roundly ignored.
$literal = 0;
if ($$piece[0] eq 'lookup') {
$value = do_lookup($$piece[1], $values, $valuator);
if (not defined $value) { # If the value is undefined, then either leave the field in place, or don't.
if ($self->{leave_misses}) {
$value = $self->{leftp} . $$piece[1] . $self->{rightp}
} else {
$value = '';
}
}
} else { # We have a spanning command.
$value = $self->handle_spanning_command($piece, $values, $valuator);
}
}
# If the value is an arrayref, use its length. This gives us a cheap way to say "search returned [[x]] rows"; just reuse the result variable.
if (ref $value eq 'ARRAY') {
$value = scalar @$value;
}
# If the value is a hashref, run it through our JSONifier for output as a debugging value.
# If it's an object, do .... hell, I dunno. If it can "describe" (i.e. it's a node) then it should do that.
# TODO: both of the above cases. I just don't need these right now.
# Now we've got a value, so we insert it into the expression,
# taking care to keep track of indentation so we can do literate
# programming of Python. (No, seriously, that was my major
# motivation here; sort of a left-over from a decade ago.)
my $indent_incr;
if ($value =~ /\n([^\n]*?)\z/s) { # 2011-08-17 - learned about \z today!
if ($literal) {
$indent = length($1);
} else {
$indent_incr += length($1);
my $spaces = ' ' x $indent;
$value =~ s/\n/\n$spaces/g;
$indent += $indent_incr;
}
} else {
$indent += length($value);
}
$return .= $value;
}
$return;
}
=head2 express($template, $values, $valuator)
If C<$template> is omitted, the default template for the engine is used. If C<$values> is omitted, same goes for any previously
defined values. And the default C<$valuator> is defined above.
=cut
sub express {
my ($self, $template, $values, $valuator) = @_;
$template = $self->{template} || '' unless defined $template;
my $pieces = $self->parse_template($template); # Note that anything after a superfluous [[+command]] or [[.end]] will be ignored.
$self->express_parsed ($pieces, $values, $valuator);
}
=head1 AUTHOR
Michael Roberts, C<< <michael at vivtek.com> >>
=head1 BUGS
Please report any bugs or feature requests to C<bug-decl at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Decl>. I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.
=head1 LICENSE AND COPYRIGHT
Copyright 2010 Michael Roberts.
This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.
See http://dev.perl.org/licenses/ for more information.
=cut
1; # End of Decl::Template