[Templates-cvs] cvs commit: TT3/lib/Template Templates.pm
cvs@template-toolkit.org
cvs@template-toolkit.org
Thu, 25 Mar 2004 14:30:41 +0000
cvs 04/03/25 14:30:41
Added: lib/Template Templates.pm
Log:
* added the Template::Templates module which implements the resource
manager for locating, loading, compiling, caching, storing and playing
table tennis with templates. Well, all apart from the ping pong.
I just added that to bring small amusement to an otherwise serious day.
Revision Changes Path
1.1 TT3/lib/Template/Templates.pm
Index: Templates.pm
===================================================================
#========================================================================
#
# Template::Templates
#
# DESCRIPTION
# Templates manager.
#
# AUTHOR
# Andy Wardley <abw@wardley.org>
#
# COPYRIGHT
# Copyright (C) 1996-2004 Andy Wardley. All Rights Reserved.
# Copyright (C) 1998-2002 Canon Research Centre Europe Ltd.
# Copyright (C) 2004 Fotango Ltd.
#
# This module is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
# REVISION
# $Id: Templates.pm,v 1.1 2004/03/25 14:30:41 abw Exp $
#
#========================================================================
package Template::Templates;
use strict;
use warnings;
use Template::Document;
use Template::Base;
use base qw( Template::Base );
use vars qw( $VERSION $DEBUG $ERROR $THROW
$ABSOLUTE $RELATIVE $DEFINITIVE $WALK
$SOURCE @CONFIG );
$VERSION = sprintf("%d.%02d", q$Revision: 1.1 $ =~ /(\d+)\.(\d+)/);
$DEBUG = 0 unless defined $DEBUG;
$ERROR = '';
$THROW = 'template';
# default options
$ABSOLUTE = 1 unless defined $ABSOLUTE;
$RELATIVE = 1 unless defined $RELATIVE;
$DEFINITIVE = 1 unless defined $DEFINITIVE;
$WALK = 0 unless defined $WALK;
$SOURCE = 'Template::Source';
@CONFIG = qw( absolute relative definitive walk source );
sub init {
my ($self, $config) = @_;
$self->SUPER::init($config) || return;
$self->pkgconf($config, @CONFIG) || return;
$self->{ load_source } = $config->{ load_source }
if exists $self->{ load_source };
$self->{ idmap } = $config->{ static } ? { } : '';
$self->{ cache } = $config->{ cache };
$self->{ store } = $config->{ store };
return $self;
}
sub fetch {
my ($self, $context, $name) = @_;
my ($path, $source, $id, $object);
my $debug = $self->{ DEBUG };
$self->debug("fetch template: $name\n") if $debug;
# Three levels of caching are implemented:
# idmap: map path to source id for template
# cache: map source id to template component in memory
# store: map source id to template component on disk
my $idmap = $self->{ idmap };
my $cache = $self->{ cache };
my $store = $self->{ store };
my ($mapsrc);
# Fetch a list of one or more paths where the template may be located.
# $path can already be a reference to a template document or something
# else, but that's OK because the paths() method will spit it back and
# the source() method will do the right thing to handle it
my $paths = $self->paths($context, $name) || return;
eval {
PATH: foreach $path (@$paths) {
$self->debug("looking for path: $path\n") if $debug;
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Source:
# If source paths are static and the requested path is an
# absolute one then we can cache the source id returned by a
# provider for the requested path. Then we can subsequently
# use the id to locate the component in the cache or store
# instead of fetching a full source object from the provider
# each time we use it
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
$mapsrc = $idmap && $path =~ m[^(\w+:)?/];
$source = $id = undef;
if (ref $path) {
$self->debug("casting reference to a source object\n") if $debug;
$source = $self->source($context, $path) || return;
$id = $source->id();
$self->debug("new source: $id\n") if $debug
}
elsif ($mapsrc && ($id = $idmap->{ $path })) {
$self->debug("mapped path $path to component id $id\n") if $debug;
}
elsif ($source = $self->source($context, $path)) {
$id = $source->id();
$self->debug("sourced document: $id\n") if $debug;
# save path/id mapping for next time if static paths
$idmap->{ $path } = $id if $mapsrc;
}
else {
# nothing showed up for this path so try the next on
next PATH;
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Cache
# look in the memory cache for a compiled template component,
# and check that it is fresh with respect to its source code
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if ($cache && ($object = $cache->get($id))) {
if ($object->fresh()) {
$self->debug("found cached template\n") if $debug;
last PATH; ## OK ##
}
else {
$object = undef;
$self->debug('stale cached component: ', $object->error())
if $debug;
}
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Store
# If we didn't find a template component in the cache then we
# look for one in the store. If we find one then we restore
# its source link to a valid source object so that it can
# subsequently check its freshness. If we haven't got a
# source object (e.g. if we used a component id stored in the
# idmap) then we need to go and fetch a complete source object
# first. We then save the restored template object in the
# cache, if we have one and the rights options are set.
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if ($store && ($object = $store->get($id))) {
# fetch source object if we need to
$source ||= $self->source($context, $path) || return;
# notify component of its source object
$object->source($source);
if ($object->fresh()) {
$self->debug("found stored template\n") if $debug;
$object->cache($cache) if $cache;
last PATH;
}
else {
$object = undef;
$self->debug('stale stored component: ', $object->error())
if $debug;
}
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Compile
# If we haven't got a cached/stored object by now then we're
# going to have to compile it ourselves. We first need to
# fetch a complete source object if we haven't got one (e.g.
# if we used an id stored in the idmap)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
$source ||= $self->source($context, $path);
return unless defined $source;
if ($source) {
# save the source info for the path we located
$idmap->{ $path } = $source->id() if $mapsrc;
}
else {
# it's unusual but not impossible for us to get here -
# a provider could advertise a template one minute and
# then remove it some time later, invalidating any
# cached source id we previously had in the idmap.
delete $idmap->{ $path } if $mapsrc;
next PATH; ## NEXT PATH ##
}
# OK, now we should have a valid source ready to be compiled
$object = $source->component(context => $context)
|| return $self->error($source->error());
$object->cache($cache) if $cache;
$object->store($store) if $store;
# all done - we've got a compiled template component
last PATH;
}
};
return $self->error($@) if $@;
return $object
|| $self->decline("$name not found");
}
#------------------------------------------------------------------------
# source($context, $path)
#
# Locates the source document for a template identified by $path.
#------------------------------------------------------------------------
sub source {
my ($self, $context, $path) = @_;
my $srcmod = $self->{ source };
my $source;
$self->debug("source($path)\n") if $self->{ DEBUG };
# $name may already be a template source object
return $path if UNIVERSAL::isa($path, $srcmod);
if (ref $path) {
# it may still be something else ref-like, such as
# a code reference or a scalar text reference
$source = $path;
}
else {
# otherwise it's the name of a template
$source = $context->search( templates => $path ) || return;
}
# $source should be a Template::Source object (or whatever
# the current value of $srcmod is), or a code reference, or
# a text string or reference to one. Either way we need to
# make sure we pass back a Template::Source object.
if (UNIVERSAL::isa($source, $srcmod)) {
# $source is already a source object, yay!
return $source;
}
elsif (UNIVERSAL::isa($source, 'HASH')) {
# $source is a hash reference
$self->debug("constructing source object from hash reference\n")
if $self->{ DEBUG };
$source->{ path } = $path
unless defined $source->{ path };
$source->{ id } = "hash:$path"
unless defined $source->{ id };
return $self->object( source => $source );
}
elsif (UNIVERSAL::isa($source, 'CODE')) {
# $source is an anonymous subroutine reference
$self->debug("constructing source object from code reference\n")
if $self->{ DEBUG };
return $self->object( source => {
id => "code:$path",
path => $path,
time => time(),
code => $source,
text => '',
fresh => 1,
cache => 0,
store => 0,
});
}
elsif (UNIVERSAL::isa($source, 'SCALAR') || ! ref $source) {
# $source is plain text or a reference to a scalar
$self->debug("constructing source object from template text\n")
if $self->{ DEBUG };
return $self->object( source => {
id => "text:$path",
path => $path,
time => time(),
text => $source,
fresh => 1,
cache => 0,
store => 0,
});
}
else {
return $self->error("invalid template source: $source");
}
}
#------------------------------------------------------------------------
# paths($context, $name)
#
# Returns a list (list context) or reference to a list (scalar context)
# of candidate paths based on current context path, and a file name of
# $name. If the $name is relative (e.g. ./foo ../foo ../../foo) then it
# is computed relative to the current path. If the $name is absolute
# (e.g. /foo) then it is left alone. Otherwise, the method returns a
# list of paths, starting with $name by itself, and then appearing in
# each directory up to the root. Examples:
# /foo/bar + /baz => [ /baz ]
# /foo/bar + ./baz => [ /foo/bar/baz ]
# /foo/bar + ../baz => [ /foo/baz ]
# /foo/bar + baz => [ baz, /foo/bar/baz, /foo/baz, /baz ]
#------------------------------------------------------------------------
sub paths {
my ($self, $context, $name) = @_;
my ($prefix, $root, @paths);
# TODO: we must make sure all components have a path, or
# figure out what to do if they don't...
my $base = $context->{ path } || '';
# $name could be a reference to something template-like, in
# which case we leave the caller to sort it out.
return wantarray ? ($name) : [$name]
if ref $name;
# copy original name
my $path = $name;
# look for a word: prefix and remove it
$prefix = $1
if $path =~ s/^(\w+)://;
if ($path =~ m{^//}) {
return $self->decline("definitive paths not allowed: $name")
unless $self->{ definitive };
$self->debug("- using definitive path: $name\n") if $DEBUG;
push(@paths, $name);
}
elsif ($path =~ m{^/}) {
return $self->decline("absolute paths not allowed: $name")
unless $self->{ absolute };
$self->debug("- using absolute path: $name\n") if $DEBUG;
push(@paths, $name);
}
elsif ($path =~ /^\./) {
return $self->decline("relative paths not allowed: $name")
unless $self->{ relative };
# any name starting in '.' is relative to current $base path so
# keep stripping dots and slashes, modifying it accordingly
while ($path =~ s[^(\.+)/][]) {
my $dots = $1;
if ($dots eq '.') {
# do nothing - keep path as it is
}
elsif ($dots eq '..') {
# remove last part of current path to move up a directory
$base =~ s{ / [^/]+ $ }{}x;
}
else {
return $self->decline("invalid prefix on template name: $dots/");
}
}
$path = "$base/$path";
$path = "$prefix:$path" if $prefix;
$self->debug("- using relative path: $path\n") if $DEBUG;
push(@paths, $path);
}
else {
# first try the name by itself
push(@paths, $name);
$self->debug("- using own path: $name\n") if $DEBUG;
# TODO: allow user to set what the default behaviour is,
# instead of just having a walk flag
# then try walking up or down the path
if ($self->{ walk }) {
my @walk;
for (;;) {
push(@walk, "$base/$path");
last unless $base;
$base =~ s{ / [^/]+ $ }{}x;
}
if ($self->{ walk } eq 'down') {
@walk = reverse @walk;
}
elsif ($self->{ walk } ne 'up') {
return $self->error( "invalid path walking direction: ",
$self->{ walk } );
}
push(@paths, map {
$self->debug("- walking path $self->{ walk }: $_\n") if $DEBUG;
$prefix ? "$prefix:$_" : $_;
} @walk);
}
}
return wantarray ? @paths : \@paths;
}
1;
__END__
=head1 NAME
Template::Templates - templates resource manger
=head1 SYNOPSIS
# we have lots of friends who join us for the ride
use Template::Cache;
use Template::Store;
use Template::Provider;
use Template::Component;
use Template::Compiler; # not working yet
use Template::TT2::Compiler; # this is!
use Template::Templates;
# memory cache and persistant store for compiled templates
my $cache = Template::Cache( size => 128 );
my $store = Template::Store( directory => '/tmp/tt3/store' );
# templates resource manager
my $templates = Template::Templates->new({
cache => $cache, # in-memory cache
store => $store, # persistant store
static => 1, # no dynamic provider paths
});
# templates provider fetches template source
my $provider = Template::Provider({
# where to look for templates
path => [
'/home/dent/web/tt3', # default options
'/home/dent/web/tt2' => { # custom options
compiler => 'tt2', # TT2 templates
cache => 1, # do cache
store => 0, # don't store
},
],
# default options for templates
options => {
compiler => 'tt3', # default compiler
cache => 1, # cache template
store => 1, # store template
},
});
# TODO: this should use Template::Compilers
# compilers required to compile templates
my $compilers = {
tt3 => Template::Compiler->new(), # not yet working :-(
tt2 => Template::TT2::Compiler->new(), # is working, kinda
};
# now we need a parent component to hang it all together
my $component = Template::Component->new({
templates => $provider, # the provider, not us
compilers => $compilers, # compilers we need
resources => {
templates => $templates, # here we are
},
});
# Phew! Finally we can ask the component to process a template
# and it will delegate to the Template::Templates object
$component->process('/product/header');
# or we can go direct if we really want to
$templates->fetch($component, '/product/header');
=head1 DESCRIPTION
This module implements a resource manager for finding, fetching, compiling,
caching and storing templates.
It's really not half as complicated as the above synopsis might suggest.
The module is itself reasonably straightforward, but it sits right in the
middle of a collaboration between a number of different objects. That
makes it hard to show it being used in isolation without bringing a whole
bunch of other players onto the field.
But then again, you wouldn't normally be using a Template::Templates
object directly because that's all hidden away behind the nice front
end that we'll be providing for you.
=head1 METHODS
=head2 new()
Constructor method which creates a new Template::Templates object.
my $templates = Template::Templates->new();
Named parameters can be passed as a list of options, or as a reference
to a hash array:
# list of options
$templates = Template::Templates->new( walk => 'up' );
# reference to hash array of options
$templates = Template::Templates->new({ walk => 'up' });
The following named parameters can be defined.
=head3 absolute
This flag is enabled by default and allows template names to be
specified with absolute paths starting withing a C</> character.
[% PROCESS /product/header %]
Note however that this represents an absolute I<template> path, not an
absolute I<file> path. The name specified will still be resolved relative
to whatever search path(s) the template provider(s) define.
For example, a provider may be defined as follows:
my $provider = Template::Provider->new(
path => [ '/home/dent/web/tt3', '/home/dent/web/tt2' ]
);
When we request a template using an absolute template path, as shown
in the left column below, it will be resolved to one of two possible
files with absolute file paths, shown in the column on the right.
/header => /home/dent/web/tt3/header
=> /home/dent/web/tt2/header
/product/header => /home/dent/web/tt3/product/header
=> /home/dent/web/tt2/product/header
From the perspective of a template author, the virtual filestem
represented by the template paths on the left, is the only one that
exists. How that maps onto a real file system or other storage
mechanism is entirely hidden from view, being of concern only to the
providers responsible for implementing the abstraction.
This is a change in behaviour from previous versions of the Template
Toolkit (pre-v3) in which the C<ABSOLUTE> flag allowed you to access
templates using absolute file systems paths, and was disabled by default.
If you want to emulate the behaviour of the TT2 C<ABSOLUTE> flag then
you should define the root filesystem directory (e.g. C</>) in the
search path of one or more template providers.
my $provider = Template::Provider->new(
path => [ '/home/dent/web/tt3', '/' ]
);
Now template paths will be mapped to the F</home/dent/web/tt3> directory,
or to C</>.
/header => /home/dent/web/tt3/header
=> /header
/etc/passwd => /home/dent/web/tt3/etc/passwd
=> /etc/passwd
If it's not immediately obvious that this is an inherently dangerous
practice, then consider this a formal warning to that effect. Doing
so allows template authors to access any files on the file system that
are accessible by the current user (who may not be the template author).
That's one reason why we decided to change the meaning of the
C<ABSOLUTE> option for TT3. It is very unusual for someone to want to
enable access to an entire file system, making this option all but
redundant. On the other hand, it is a more common requirement (and
oft-cited limitation of previous versions of the Template Toolkit) to
request a template based on the illusion of the virtual template file
system that the providers collectively implement.
=head3 relative
This flag is enabled by default and allows template names to be
specified with relative paths starting withing C<./> or C<../>
character sequences.
[% PROCESS ./header %]
[% PROCESS ../header %]
[% PROCESS ../../header %]
The full path of the template is resolved relative to the path of the
current template being processed. For example, if you are currently
processing the F</product/widget/info> template then the following
paths will be resolved as shown below:
./header => /product/widget/header
../header => /product/header
../../header => /header
../../../header => /header # can't go any higher
Note that excessive C<../> elements in the path are ignored, as shown
in the last example. There is no directory above C</>, so the extra
C<../> has no effect.
As with the C<ABSOLUTE> option, this marks a change in behaviour from
the corresponding C<RELATIVE> option in TT2 which located templates
relative to the current working directory. Like C<ABSOLUTE>, this
turned out to be an ill-conceived idea and has been replaced by something
far more useful and powerful in TT3.
To emulate the behaviour of the TT2 C<RELATIVE> option, you can
explicitly add the current working directory to the search path of one
or more template providers. This is shown in the following example:
use Cwd;
my $provider = Template::Provider->new(
path => [ '/home/dent/web/tt3', getcwd() ]
);
Template names can also be specified with a additional prefix. This
is used to bind it to a particular provider, or to indicate to a
provider what kind of template is requested, how it should be fetched,
where it should be fetched from, or to denote some other custom
behaviour.
[% PROCESS src:header %]
[% PROCESS user:header %]
The prefix is effectively ignored when relative paths are expanded (or
more accurately, it is removed, the path is expanded, and then the
prefix is replaced). This is shown in the following examples where
the template names in the left column are mapped to the template paths
on the right.
src:./header => src:/product/widget/header
src:../header => src:/product/header
src:../../header => src:/header
src:../../../header => src:/header
=head3 definitive
This flag is also enabled by default and allows template to be
specified using definitive paths that start with the C<//> character
sequence. This is most often used in conjunction with a provider
prefix.
[% PROCESS http://template-toolkit.org/templates/news.tt3 %]
[% INSERT file://tmp/report.log %]
An absolute template name starting with a single C</> character is
always assumed to be relative to one of the root paths in the virtual
file system that the providers collectively define. So it's not
really absolute, but it just looks that way from the perspective
of our virtual template file system.
On the other hand, a definitive path starting with a double C<//>
character sequence is taken to be really, truly and utterly absolute
to the whole information space of the visible universe (and that's a
pretty big place). Well OK, maybe we're exaggerating slightly, but
that's the basic idea. Unlike an absolute path which is only really
absolute within our virtual template namespace (but does eventually
get mapped by a provider onto a definitive path, e.g. a file in a real
filesytem), a definitive path is assumed to be part of someone else's
address space, and we don't go messing with it. Instead we just hand
it over to each provider in turn, passing the full path exactly as
written, and leave it up to one of them to figure out what to do with
it.
Providers that haven't been configured to accept a particular prefix
will decline the request outright. In practice, that means it is
perfectly safe and legal to write the following in a template:
[% INSERT file://etc/passwd %]
Unless you explicitly define a provider that responds to the C<file:>
prefix, the directive will fail, reporting that the template cannot be
found. Furthermore, your C<file:> provider doesn't have to map its
files directly onto the filesystem root. You can set its path to
any directory you like so that the requested file is fetched from
somewhere else.
# TODO: implement prefix management in providers (not yet working)
my $provider = Template::Provider->new(
prefix => 'file',
path => '/home/dent/web/files',
);
You can even map the C<file:> prefix to a different provider,
say one that fetches templates from a database. From the
perspective of the template, it looks like a file, but behind the
scenes it may be implemented as something else.
=head3 walk
The C<walk> option determines the behaviour used to locate a template
that isn't specified using an absolute, relative, or definitive path.
[% INCLUDE header %]
If C<walk> is set to C<up>, then the C<fetch()> method will first
look for C<header> defined as a block or local template, and then
go on to look for C<header> in the current directory (based on the
current template location, as per the C<relative> option), and then
in the parent directory, and so on.
So if we are current processing the F</product/widget/info> and
request F<header>, the following paths will be tried in order until
the first that matches a valid template.
header
/product/widget/header
/product/header
/header
If C<walk> is set to C<down> then the order of the paths searched
is reversed. However, the original name requested, C<header>, is
always tried first.
header
/header
/product/header
/product/widget/header
TODO: I'm planning to add an explicit path syntax for these as well,
I was thinking C<.../header> for walk up and C</.../header> to walk
down.
=head3 static
This flag can be set to indicate that a particular template path
(e.g. F</product/header>) will always resolve to the same template
component.
The Template::Templates module does not load any templates itself, but
instead delegates to one or more Template::Providers objects. A
provider may use a dynamic search path that changes the list of
directories in which it looks for templates over time. So if we ask
it for F</product/header> now, we might get back a template from the
F</web/templates/product/header> file. But when we ask it again some
time later, its search path may have changed and we might instead get
a template from the F</web/templates/custom/orange/product/header>
file.
To account for this, the C<fetch()> method asks the provider(s) to
locate (but not load) the template each time it is used. The provider
returns a unique identifier for each template (usually generated from
the full file path) which allows us to disambiguate one
C</product/header> template from another. Once we know which specific
template component we're looking for, we can check the cache and/or
store for a previously compiled component, or we can load and compile
the template ourselves and cache/store it for later.
If the C<static> flag is set then the module assumes that the provider
paths are not going to change. This saves us from having to ask the
provider to map the template name to a component id each time we use
it, making it run a little faster.
=head3 cache
A reference to a Template::Cache object, or a subclass, or an object
with a compatible get()/set() interface (e.g. Cache::Cache objects).
The cache is responsible for caching compiled templates in memory.
my $cache = Template::Cache->new( size => 128 );
my $templates = Template::Templates->new(
cache => $cache,
);
=head3 store
A reference to a Template::Store object, or a subclass, or an object
with a compatible get()/set() interface. The store is responsible for
storing compiled templates persistantly on disk or other storage
medium.
=head3 source
This option can be used to provide the name of an object class to be
used instead of Template::Source. This is used to upgrade templates
that are specified as subroutine references, scalar references or
other "inline" declarations that are not sourced from a provider.
my $templates = Template::Templates->new(
source => 'My::Template::Source',
);
The module specified in this option will be loaded automatically.
=head3 load_source
This flag can be explciticly set to 0 to prevent the module defined in
the C<source> option from being loaded automatically.
package My::Template::Source;
...
package main;
my $template = Template::Template->new({
source => 'My::Template::Source',
load_source => 0,
});
=head2 fetch($context, $name)
This method locates a template in the context passed as the first
argument with the name passed as the second.
my $header = $templates->fetch($context, 'header');
The first argument, C<$context>, should be a reference to the current
component from where the search to locate the template should begin.
Given that the C<fetch()> method is usually called from the
C<template()> method of Template::Component (TODO: perhaps soon to be
the Template::Template subclass of it?), the C<$context> argument
passed is the C<$self> reference of the component.
package Template::Component;
sub template {
my ($self, $name) = @_;
...
my $component = $templates->fetch($self, $name);
...
}
The C<fetch()> method first calls the C<paths()> method to expand the
template name into one or more candidate paths. It then calls the
C<source()> method for each (first looking in a local cache if the
C<static> flag is set), to see if the template can be located. If it
is, then a unique identifier for the template is returned (wrapped up
as part of a Template::Source object). This identifier can then be
used to load a previously compiled template from the in-memory cache,
or from the persistant store. Failing that, the template source is
loaded and compiled and the resulting component is added to the cache
and store as appropriate.
If the C<static> flag is set then the C<fetch()> method only locates
the source for a given path once. The unique identifier returned for
a path is stored locally, allowing the template to be subsequently
fetched from the cache or store without have to query the provider
each time.
=head2 source($context, $path)
This method locates the source of a template and returns a
Template::Source object.
my $source = $templates->source($context, '/product/header');
The first argument is a reference to a component object that defines
the current operating context. The second argument provides the full
template path (<i>not</i> the full file system path) of the template
in question.
The method returns a Template::Source object representing the located
template, or undef of the template cannot be located.
The C<$path> argument can also contain a reference to a subroutine,
template text, or hash array, all of which are upgraded to
Template::Source objects and returned.
=head2 paths($context, $name)
This method expands the template name passed as a second argument and
returns a list of one or more absolute templates paths (absolute in
the sense of a template name like F</product/header>, not absolute
file names) computed relative to the current context path.
For example, if the template defining the current context is
F</product/info> then a request for the F<./header> template will
resolve to F</product/header>, while F<../header> will resolve to
F</header>.
=head1 AUTHOR
Andy Wardley E<lt>abw@wardley.orgE<gt>
=head1 VERSION
$Revision: 1.1 $
=head1 COPYRIGHT
Copyright (C) 1996-2004 Andy Wardley. All Rights Reserved.
Copyright (C) 1998-2002 Canon Research Centre Europe Ltd.
Copyright (C) 2004 Fotango Ltd.
This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
=cut
# Local Variables:
# mode: perl
# perl-indent-level: 4
# indent-tabs-mode: nil
# End:
#
# vim: expandtab shiftwidth=4: