diff options
Diffstat (limited to 'lib/Template/View.pm')
-rw-r--r-- | lib/Template/View.pm | 754 |
1 files changed, 754 insertions, 0 deletions
diff --git a/lib/Template/View.pm b/lib/Template/View.pm new file mode 100644 index 0000000..312ff45 --- /dev/null +++ b/lib/Template/View.pm @@ -0,0 +1,754 @@ +#============================================================= -*-Perl-*- +# +# Template::View +# +# DESCRIPTION +# A custom view of a template processing context. Can be used to +# implement custom "skins". +# +# AUTHOR +# Andy Wardley <abw@kfs.org> +# +# COPYRIGHT +# Copyright (C) 2000 Andy Wardley. All Rights Reserved. +# +# This module is free software; you can redistribute it and/or +# modify it under the same terms as Perl itself. +# +# TODO +# * allowing print to have a hash ref as final args will cause problems +# if you do this: [% view.print(hash1, hash2, hash3) %]. Current +# work-around is to do [% view.print(hash1); view.print(hash2); +# view.print(hash3) %] or [% view.print(hash1, hash2, hash3, { }) %] +# +# REVISION +# $Id: View.pm,v 2.8 2002/04/15 15:53:37 abw Exp $ +# +#============================================================================ + +package Template::View; + +require 5.004; + +use strict; +use vars qw( $VERSION $DEBUG $AUTOLOAD @BASEARGS $MAP ); +use base qw( Template::Base ); + +$VERSION = sprintf("%d.%02d", q$Revision: 2.8 $ =~ /(\d+)\.(\d+)/); +$DEBUG = 0 unless defined $DEBUG; +@BASEARGS = qw( context ); +$MAP = { + HASH => 'hash', + ARRAY => 'list', + TEXT => 'text', + default => '', +}; + +#$DEBUG = 1; + +#------------------------------------------------------------------------ +# _init(\%config) +# +# Initialisation method called by the Template::Base class new() +# constructor. $self->{ context } has already been set, by virtue of +# being named in @BASEARGS. Remaining config arguments are presented +# as a hash reference. +#------------------------------------------------------------------------ + +sub _init { + my ($self, $config) = @_; + + # move 'context' somewhere more private + $self->{ _CONTEXT } = $self->{ context }; + delete $self->{ context }; + + # generate table mapping object types to templates + my $map = $config->{ map } || { }; + $map->{ default } = $config->{ default } unless defined $map->{ default }; + $self->{ map } = { + %$MAP, + %$map, + }; + + # local BLOCKs definition table + $self->{ _BLOCKS } = $config->{ blocks } || { }; + + # name of presentation method which printed objects might provide + $self->{ method } = defined $config->{ method } + ? $config->{ method } : 'present'; + + # view is sealed by default preventing variable update after + # definition, however we don't actually seal a view until the + # END of the view definition + my $sealed = $config->{ sealed }; + $sealed = 1 unless defined $sealed; + $self->{ sealed } = $sealed ? 1 : 0; + + # copy remaining config items from $config or set defaults + foreach my $arg (qw( base prefix suffix notfound silent )) { + $self->{ $arg } = $config->{ $arg } || ''; + } + + # name of data item used by view() + $self->{ item } = $config->{ item } || 'item'; + + # map methods of form ${include_prefix}_foobar() to include('foobar')? + $self->{ include_prefix } = $config->{ include_prefix } || 'include_'; + # what about mapping foobar() to include('foobar')? + $self->{ include_naked } = defined $config->{ include_naked } + ? $config->{ include_naked } : 1; + + # map methods of form ${view_prefix}_foobar() to include('foobar')? + $self->{ view_prefix } = $config->{ view_prefix } || 'view_'; + # what about mapping foobar() to view('foobar')? + $self->{ view_naked } = $config->{ view_naked } || 0; + + # the view is initially unsealed, allowing directives in the initial + # view template to create data items via the AUTOLOAD; once sealed via + # call to seal(), the AUTOLOAD will not update any internal items. + delete @$config{ qw( base method map default prefix suffix notfound item + include_prefix include_naked silent sealed + view_prefix view_naked blocks ) }; + $config = { %{ $self->{ base }->{ data } }, %$config } + if $self->{ base }; + $self->{ data } = $config; + $self->{ SEALED } = 0; + + return $self; +} + + +#------------------------------------------------------------------------ +# seal() +# unseal() +# +# Seal or unseal the view to allow/prevent new datat items from being +# automatically created by the AUTOLOAD method. +#------------------------------------------------------------------------ + +sub seal { + my $self = shift; + $self->{ SEALED } = $self->{ sealed }; +} + +sub unseal { + my $self = shift; + $self->{ SEALED } = 0; +} + + +#------------------------------------------------------------------------ +# clone(\%config) +# +# Cloning method which takes a copy of $self and then applies to it any +# modifications specified in the $config hash passed as an argument. +# Configuration items may also be specified as a list of "name => $value" +# arguments. Returns a reference to the cloned Template::View object. +# +# NOTE: may need to copy BLOCKS??? +#------------------------------------------------------------------------ + +sub clone { + my $self = shift; + my $clone = bless { %$self }, ref $self; + my $config = ref $_[0] eq 'HASH' ? shift : { @_ }; + + # merge maps + $clone->{ map } = { + %{ $self->{ map } }, + %{ $config->{ map } || { } }, + }; + + # "map => { default=>'xxx' }" can be specified as "default => 'xxx'" + $clone->{ map }->{ default } = $config->{ default } + if defined $config->{ default }; + + # update any remaining config items + my @args = qw( base prefix suffix notfound item method include_prefix + include_naked view_prefix view_naked ); + foreach my $arg (@args) { + $clone->{ $arg } = $config->{ $arg } if defined $config->{ $arg }; + } + push(@args, qw( default map )); + delete @$config{ @args }; + + # anything left is data + my $data = $clone->{ data } = { %{ $self->{ data } } }; + @$data{ keys %$config } = values %$config; + + return $clone; +} + + +#------------------------------------------------------------------------ +# print(@items, ..., \%config) +# +# Prints @items in turn by mapping each to an approriate template using +# the internal 'map' hash. If an entry isn't found and the item is an +# object that implements the method named in the internal 'method' item, +# (default: 'present'), then the method will be called passing a reference +# to $self, against which the presenter method may make callbacks (e.g. +# to view_item()). If the presenter method isn't implemented, then the +# 'default' map entry is consulted and used if defined. The final argument +# may be a reference to a hash array providing local overrides to the internal +# defaults for various items (prefix, suffix, etc). In the presence +# of this parameter, a clone of the current object is first made, applying +# any configuration updates, and control is then delegated to it. +#------------------------------------------------------------------------ + +sub print { + my $self = shift; + + # if final config hash is specified then create a clone and delegate to it + # NOTE: potential problem when called print(\%data_hash1, \%data_hash2); + if ((scalar @_ > 1) && (ref $_[-1] eq 'HASH')) { + my $cfg = pop @_; + my $clone = $self->clone($cfg) + || return; + return $clone->print(@_) + || $self->error($clone->error()); + } + my ($item, $type, $template, $present); + my $method = $self->{ method }; + my $map = $self->{ map }; + my $output = ''; + + # print each argument + foreach $item (@_) { + my $newtype; + + if (! ($type = ref $item)) { + # non-references are TEXT + $type = 'TEXT'; + $template = $map->{ $type }; + } + elsif (! defined ($template = $map->{ $type })) { + # no specific map entry for object, maybe it implements a + # 'present' (or other) method? +# $self->DEBUG("determining if $item can $method\n") if $DEBUG; + if ( $method && UNIVERSAL::can($item, $method) ) { + $self->DEBUG("Calling \$item->$method\n") if $DEBUG; + $present = $item->$method($self); ## call item method + # undef returned indicates error, note that we expect + # $item to have called error() on the view + return unless defined $present; + $output .= $present; + next; ## NEXT + } + elsif ( UNIVERSAL::isa($item, 'HASH' ) + && defined($newtype = $item->{$method}) + && defined($template = $map->{"$method=>$newtype"})) { + } + elsif ( defined($newtype) + && defined($template = $map->{"$method=>*"}) ) { + $template =~ s/\*/$newtype/; + } + elsif (! ($template = $map->{ default }) ) { + # default not defined, so construct template name from type + ($template = $type) =~ s/\W+/_/g; + } + } +# else { +# $self->DEBUG("defined map type for $type: $template\n"); +# } + $self->DEBUG("printing view '", $template || '', "', $item\n") if $DEBUG; + $output .= $self->view($template, $item) + if $template; + } + return $output; +} + + +#------------------------------------------------------------------------ +# view($template, $item, \%vars) +# +# Wrapper around include() which expects a template name, $template, +# followed by a data item, $item, and optionally, a further hash array +# of template variables. The $item is added as an entry to the $vars +# hash (which is created empty if not passed as an argument) under the +# name specified by the internal 'item' member, which is appropriately +# 'item' by default. Thus an external object present() method can +# callback against this object method, simply passing a data item to +# be displayed. The external object doesn't have to know what the +# view expects the item to be called in the $vars hash. +#------------------------------------------------------------------------ + +sub view { + my ($self, $template, $item) = splice(@_, 0, 3); + my $vars = ref $_[0] eq 'HASH' ? shift : { @_ }; + $vars->{ $self->{ item } } = $item if defined $item; + $self->include($template, $vars); +} + + +#------------------------------------------------------------------------ +# include($template, \%vars) +# +# INCLUDE a template, $template, mapped according to the current prefix, +# suffix, default, etc., where $vars is an optional hash reference +# containing template variable definitions. If the template isn't found +# then the method will default to any 'notfound' template, if defined +# as an internal item. +#------------------------------------------------------------------------ + +sub include { + my ($self, $template, $vars) = @_; + my $context = $self->{ _CONTEXT }; + + $template = $self->template($template); + + $vars = { } unless ref $vars eq 'HASH'; + $vars->{ view } ||= $self; + + $context->include( $template, $vars ); + +# DEBUGGING +# my $out = $context->include( $template, $vars ); +# print STDERR "VIEW return [$out]\n"; +# return $out; +} + + +#------------------------------------------------------------------------ +# template($template) +# +# Returns a compiled template for the specified template name, according +# to the current configuration parameters. +#------------------------------------------------------------------------ + +sub template { + my ($self, $name) = @_; + my $context = $self->{ _CONTEXT }; + return $context->throw(Template::Constants::ERROR_VIEW, + "no view template specified") + unless $name; + + my $notfound = $self->{ notfound }; + my $base = $self->{ base }; + my ($template, $block, $error); + + return $block + if ($block = $self->{ _BLOCKS }->{ $name }); + + # try the named template + $template = $self->template_name($name); + $self->DEBUG("looking for $template\n") if $DEBUG; + eval { $template = $context->template($template) }; + + # try asking the base view if not found + if (($error = $@) && $base) { + $self->DEBUG("asking base for $name\n") if $DEBUG; + eval { $template = $base->template($name) }; + } + + # try the 'notfound' template (if defined) if that failed + if (($error = $@) && $notfound) { + unless ($template = $self->{ _BLOCKS }->{ $notfound }) { + $notfound = $self->template_name($notfound); + $self->DEBUG("not found, looking for $notfound\n") if $DEBUG; + eval { $template = $context->template($notfound) }; + + return $context->throw(Template::Constants::ERROR_VIEW, $error) + if $@; # return first error + } + } + elsif ($error) { + $self->DEBUG("no 'notfound'\n") + if $DEBUG; + return $context->throw(Template::Constants::ERROR_VIEW, $error); + } + return $template; +} + + +#------------------------------------------------------------------------ +# template_name($template) +# +# Returns the name of the specified template with any appropriate prefix +# and/or suffix added. +#------------------------------------------------------------------------ + +sub template_name { + my ($self, $template) = @_; + $template = $self->{ prefix } . $template . $self->{ suffix } + if $template; + + $self->DEBUG("template name: $template\n") if $DEBUG; + return $template; +} + + +#------------------------------------------------------------------------ +# default($val) +# +# Special case accessor to retrieve/update 'default' as an alias for +# '$map->{ default }'. +#------------------------------------------------------------------------ + +sub default { + my $self = shift; + return @_ ? ($self->{ map }->{ default } = shift) + : $self->{ map }->{ default }; +} + + +#------------------------------------------------------------------------ +# AUTOLOAD +# + +# Returns/updates public internal data items (i.e. not prefixed '_' or +# '.') or presents a view if the method matches the view_prefix item, +# e.g. view_foo(...) => view('foo', ...). Similarly, the +# include_prefix is used, if defined, to map include_foo(...) to +# include('foo', ...). If that fails then the entire method name will +# be used as the name of a template to include iff the include_named +# parameter is set (default: 1). Last attempt is to match the entire +# method name to a view() call, iff view_naked is set. Otherwise, a +# 'view' exception is raised reporting the error "no such view member: +# $method". +#------------------------------------------------------------------------ + +sub AUTOLOAD { + my $self = shift; + my $item = $AUTOLOAD; + $item =~ s/.*:://; + return if $item eq 'DESTROY'; + + if ($item =~ /^[\._]/) { + return $self->{ _CONTEXT }->throw(Template::Constants::ERROR_VIEW, + "attempt to view private member: $item"); + } + elsif (exists $self->{ $item }) { + # update existing config item (e.g. 'prefix') if unsealed + return $self->{ _CONTEXT }->throw(Template::Constants::ERROR_VIEW, + "cannot update config item in sealed view: $item") + if @_ && $self->{ SEALED }; + $self->DEBUG("accessing item: $item\n") if $DEBUG; + return @_ ? ($self->{ $item } = shift) : $self->{ $item }; + } + elsif (exists $self->{ data }->{ $item }) { + # get/update existing data item (must be unsealed to update) + if (@_ && $self->{ SEALED }) { + return $self->{ _CONTEXT }->throw(Template::Constants::ERROR_VIEW, + "cannot update item in sealed view: $item") + unless $self->{ silent }; + # ignore args if silent + @_ = (); + } + $self->DEBUG(@_ ? "updating data item: $item <= $_[0]\n" + : "returning data item: $item\n") if $DEBUG; + return @_ ? ($self->{ data }->{ $item } = shift) + : $self->{ data }->{ $item }; + } + elsif (@_ && ! $self->{ SEALED }) { + # set data item if unsealed + $self->DEBUG("setting unsealed data: $item => @_\n") if $DEBUG; + $self->{ data }->{ $item } = shift; + } + elsif ($item =~ s/^$self->{ view_prefix }//) { + $self->DEBUG("returning view($item)\n") if $DEBUG; + return $self->view($item, @_); + } + elsif ($item =~ s/^$self->{ include_prefix }//) { + $self->DEBUG("returning include($item)\n") if $DEBUG; + return $self->include($item, @_); + } + elsif ($self->{ include_naked }) { + $self->DEBUG("returning naked include($item)\n") if $DEBUG; + return $self->include($item, @_); + } + elsif ($self->{ view_naked }) { + $self->DEBUG("returning naked view($item)\n") if $DEBUG; + return $self->view($item, @_); + } + else { + return $self->{ _CONTEXT }->throw(Template::Constants::ERROR_VIEW, + "no such view member: $item"); + } +} + + +1; + + +__END__ + +=head1 NAME + +Template::View - customised view of a template processing context + +=head1 SYNOPSIS + + # define a view + [% VIEW view + # some standard args + prefix => 'my_', + suffix => '.tt2', + notfound => 'no_such_file' + ... + + # any other data + title => 'My View title' + other_item => 'Joe Random Data' + ... + %] + # add new data definitions, via 'my' self reference + [% my.author = "$abw.name <$abw.email>" %] + [% my.copy = "© Copyright 2000 $my.author" %] + + # define a local block + [% BLOCK header %] + This is the header block, title: [% title or my.title %] + [% END %] + + [% END %] + + # access data items for view + [% view.title %] + [% view.other_item %] + + # access blocks directly ('include_naked' option, set by default) + [% view.header %] + [% view.header(title => 'New Title') %] + + # non-local templates have prefix/suffix attached + [% view.footer %] # => [% INCLUDE my_footer.tt2 %] + + # more verbose form of block access + [% view.include( 'header', title => 'The Header Title' ) %] + [% view.include_header( title => 'The Header Title' ) %] + + # very short form of above ('include_naked' option, set by default) + [% view.header( title => 'The Header Title' ) %] + + # non-local templates have prefix/suffix attached + [% view.footer %] # => [% INCLUDE my_footer.tt2 %] + + # fallback on the 'notfound' template ('my_no_such_file.tt2') + # if template not found + [% view.include('missing') %] + [% view.include_missing %] + [% view.missing %] + + # print() includes a template relevant to argument type + [% view.print("some text") %] # type=TEXT, template='text' + + [% BLOCK my_text.tt2 %] # 'text' with prefix/suffix + Text: [% item %] + [% END %] + + # now print() a hash ref, mapped to 'hash' template + [% view.print(some_hash_ref) %] # type=HASH, template='hash' + + [% BLOCK my_hash.tt2 %] # 'hash' with prefix/suffix + hash keys: [% item.keys.sort.join(', ') + [% END %] + + # now print() a list ref, mapped to 'list' template + [% view.print(my_list_ref) %] # type=ARRAY, template='list' + + [% BLOCK my_list.tt2 %] # 'list' with prefix/suffix + list: [% item.join(', ') %] + [% END %] + + # print() maps 'My::Object' to 'My_Object' + [% view.print(myobj) %] + + [% BLOCK my_My_Object.tt2 %] + [% item.this %], [% item.that %] + [% END %] + + # update mapping table + [% view.map.ARRAY = 'my_list_template' %] + [% view.map.TEXT = 'my_text_block' %] + + + # change prefix, suffix, item name, etc. + [% view.prefix = 'your_' %] + [% view.default = 'anyobj' %] + ... + +=head1 DESCRIPTION + +TODO + +=head1 METHODS + +=head2 new($context, \%config) + +Creates a new Template::View presenting a custom view of the specified +$context object. + +A reference to a hash array of configuration options may be passed as the +second argument. + +=over 4 + +=item prefix + +Prefix added to all template names. + + [% USE view(prefix => 'my_') %] + [% view.view('foo', a => 20) %] # => my_foo + +=item suffix + +Suffix added to all template names. + + [% USE view(suffix => '.tt2') %] + [% view.view('foo', a => 20) %] # => foo.tt2 + +=item map + +Hash array mapping reference types to template names. The print() +method uses this to determine which template to use to present any +particular item. The TEXT, HASH and ARRAY items default to 'test', +'hash' and 'list' appropriately. + + [% USE view(map => { ARRAY => 'my_list', + HASH => 'your_hash', + My::Foo => 'my_foo', } ) %] + + [% view.print(some_text) %] # => text + [% view.print(a_list) %] # => my_list + [% view.print(a_hash) %] # => your_hash + [% view.print(a_foo) %] # => my_foo + + [% BLOCK text %] + Text: [% item %] + [% END %] + + [% BLOCK my_list %] + list: [% item.join(', ') %] + [% END %] + + [% BLOCK your_hash %] + hash keys: [% item.keys.sort.join(', ') + [% END %] + + [% BLOCK my_foo %] + Foo: [% item.this %], [% item.that %] + [% END %] + +=item method + +Name of a method which objects passed to print() may provide for presenting +themselves to the view. If a specific map entry can't be found for an +object reference and it supports the method (default: 'present') then +the method will be called, passing the view as an argument. The object +can then make callbacks against the view to present itself. + + package Foo; + + sub present { + my ($self, $view) = @_; + return "a regular view of a Foo\n"; + } + + sub debug { + my ($self, $view) = @_; + return "a debug view of a Foo\n"; + } + +In a template: + + [% USE view %] + [% view.print(my_foo_object) %] # a regular view of a Foo + + [% USE view(method => 'debug') %] + [% view.print(my_foo_object) %] # a debug view of a Foo + +=item default + +Default template to use if no specific map entry is found for an item. + + [% USE view(default => 'my_object') %] + + [% view.print(objref) %] # => my_object + +If no map entry or default is provided then the view will attempt to +construct a template name from the object class, substituting any +sequence of non-word characters to single underscores, e.g. + + # 'fubar' is an object of class Foo::Bar + [% view.print(fubar) %] # => Foo_Bar + +Any current prefix and suffix will be added to both the default template +name and any name constructed from the object class. + +=item notfound + +Fallback template to use if any other isn't found. + +=item item + +Name of the template variable to which the print() method assigns the current +item. Defaults to 'item'. + + [% USE view %] + [% BLOCK list %] + [% item.join(', ') %] + [% END %] + [% view.print(a_list) %] + + [% USE view(item => 'thing') %] + [% BLOCK list %] + [% thing.join(', ') %] + [% END %] + [% view.print(a_list) %] + +=item view_prefix + +Prefix of methods which should be mapped to view() by AUTOLOAD. Defaults +to 'view_'. + + [% USE view %] + [% view.view_header() %] # => view('header') + + [% USE view(view_prefix => 'show_me_the_' %] + [% view.show_me_the_header() %] # => view('header') + +=item view_naked + +Flag to indcate if any attempt should be made to map method names to +template names where they don't match the view_prefix. Defaults to 0. + + [% USE view(view_naked => 1) %] + + [% view.header() %] # => view('header') + +=back + +=head2 print( $obj1, $obj2, ... \%config) + +TODO + +=head2 view( $template, \%vars, \%config ); + +TODO + +=head1 AUTHOR + +Andy Wardley E<lt>abw@kfs.orgE<gt> + +=head1 REVISION + +$Revision: 2.8 $ + +=head1 COPYRIGHT + +Copyright (C) 2000 Andy Wardley. All Rights Reserved. + +This module is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +=head1 SEE ALSO + +L<Template::Plugin|Template::Plugin>, + +=cut + + + + + |