+2.09 2007-04-05
+ * Add more documentation about file paths
+ * Allow for base_dir_abs to return a single value, or an arrayref of values, or
+ a coderef that returns a single value or arrayref.
+ * Allow CGI::Ex send_status to work on mod_perl2.
+ * Add tests to cover all virtual methods in CGI::Ex::Template.
+ * Bring all virtual methods into line with TT2.18
+
2.08 2007-03-02
* Allow for digits passed to %*s patterns in format, and fmt or CGI::Ex::Template
* Fix bug in validate.js to allow for non-existant fields (with replace)
# http://module-build.sourceforge.net/META-spec.html
#XXXXXXX This is a prototype!!! It will change in the future!!! XXXXX#
name: CGI-Ex
-version: 2.08
+version: 2.09
version_from: lib/CGI/Ex.pm
installdirs: site
requires:
use base qw(Exporter);
BEGIN {
- $VERSION = '2.08';
+ $VERSION = '2.09';
$PREFERRED_CGI_MODULE ||= 'CGI';
@EXPORT = ();
@EXPORT_OK = qw(get_form
$r->send_http_header;
$r->print($mesg);
} else {
- # not sure of best way to send the message in MP2
+ $r->content_type('text/html');
+ $r->print($mesg);
+ $r->rflush;
}
} else {
print "Status: $code\r\n";
use vars qw($VERSION);
BEGIN {
- $VERSION = '2.08';
+ $VERSION = '2.09';
Time::HiRes->import('time') if eval {require Time::HiRes};
eval {require Scalar::Util};
my $args = $self->run_hook('template_args', $step);
my $copy = $self;
eval {require Scalar::Util; Scalar::Util::weaken($copy)};
- $args->{'INCLUDE_PATH'} ||= sub { $copy->base_dir_abs || die "Could not find base_dir_abs while looking for template INCLUDE_PATH on step \"$step\"" };
-
- require CGI::Ex::Template;
- my $t = CGI::Ex::Template->new($args);
+ $args->{'INCLUDE_PATH'} ||= sub {
+ my $dir = $copy->base_dir_abs || die "Could not find base_dir_abs while looking for template INCLUDE_PATH on step \"$step\"";
+ $dir = $dir->() if UNIVERSAL::isa($dir, 'CODE');
+ return $dir;
+ };
+ my $t = $self->template_obj($args);
my $out = '';
+
$t->process($file, $swap, \$out) || die $t->error;
return $out;
sub template_args { {} }
+sub template_obj {
+ my ($self, $args) = @_;
+
+ require CGI::Ex::Template;
+ my $t = CGI::Ex::Template->new($args);
+}
+
sub fill_template {
my ($self, $step, $outref, $fill) = @_;
sub post_print { 0 }
sub post_step { 0 } # success indicates we handled step (don't continue step or loop)
-sub name_step {
- my ($self, $step) = @_;
- return $step;
-}
-
sub morph_package {
my $self = shift;
my $step = shift || '';
};
}
+sub name_step {
+ my ($self, $step) = @_;
+ return $step;
+}
+
sub file_print {
my $self = shift;
my $step = shift;
my $self = shift;
my $step = shift;
- my $abs = $self->base_dir_abs || return {};
+ ### determine the path to begin looking for files - allow for an arrayref
+ my $abs = $self->base_dir_abs || [];
+ $abs = $abs->() if UNIVERSAL::isa($abs, 'CODE');
+ $abs = [$abs] if ! UNIVERSAL::isa($abs, 'ARRAY');
+ return {} if @$abs == 0;
+
my $base_dir = $self->base_dir_rel;
my $module = $self->run_hook('name_module', $step);
- my $_step = $self->run_hook('name_step', $step);
+ my $_step = $self->run_hook('name_step', $step) || die "Missing name_step";
$_step .= '.'. $self->ext_val if $_step !~ /\.\w+$/;
- foreach ($abs, $base_dir, $module) { $_ .= '/' if length($_) && ! m|/$| }
+ foreach (@$abs, $base_dir, $module) { $_ .= '/' if length($_) && ! m|/$| }
+
+ if (@$abs > 1) {
+ foreach my $_abs (@$abs) {
+ my $path = $_abs . $base_dir . $module . $_step;
+ return $path if -e $path;
+ }
+ }
- return $abs . $base_dir . $module . $_step;
+ return $abs->[0] . $base_dir . $module . $_step;
}
sub info_complete {
return $self->{'base_dir_abs'} || '';
}
-sub ext_val {
- my $self = shift;
- $self->{'ext_val'} = shift if $#_ != -1;
- return $self->{'ext_val'} || 'val';
-}
-
sub ext_print {
my $self = shift;
$self->{'ext_print'} = shift if $#_ != -1;
return $self->{'ext_print'} || 'html';
}
+sub ext_val {
+ my $self = shift;
+ $self->{'ext_val'} = shift if $#_ != -1;
+ return $self->{'ext_val'} || 'val';
+}
+
### where to find the javascript files
### default to using this script as a handler
sub js_uri_path {
=head1 DESCRIPTION
-Fill in the blanks and get a ready made web application. This module
-is somewhat similar in spirit to CGI::Application, CGI::Path, and
-CGI::Builder and any other "CGI framework." As with the others,
-CGI::Ex::App tries to do as much of the mundane things, in a simple
-manner, without getting in the developer's way. However, there are
-various design patterns for CGI applications that CGI::Ex::App handles
-for you that the other frameworks require you to bring in extra support.
-The entire CGI::Ex suite has been taylored to work seamlessly together.
-Your mileage in building applications may vary.
+Fill in the blanks and get a ready made web application.
+
+This module is somewhat similar in spirit to CGI::Application,
+CGI::Path, and CGI::Builder and any other "CGI framework." As with
+the others, CGI::Ex::App tries to do as much of the mundane things, in
+a simple manner, without getting in the developer's way. However,
+there are various design patterns for CGI applications that
+CGI::Ex::App handles for you that the other frameworks require you to
+bring in extra support. The entire CGI::Ex suite has been taylored to
+work seamlessly together. Your mileage in building applications may
+vary.
If you build applications that submit user information, validate it,
re-display it, fill in forms, or separate logic into separate modules,
provide your key actions and html.
One of the great benefits of CGI::Ex::App vs. Catalyst or Rails style
-frameworks is that the model of CGI::Ex::App can be much more abstract
-as models often are.
+frameworks is that the model of CGI::Ex::App can be much more abstract.
+And models often are abstract.
=head1 DEFAULT PROCESS FLOW
sensible defaults, it is very easy to override a little or a lot which
ends up giving the developer a lot of flexibility.
-Consequently, it should be possible to use CGI::Ex::App with the other
+Additionally, it should be possible to use CGI::Ex::App with the other
frameworks such as CGI::Application or CGI::Prototype. For these you
could simple let each "runmode" call the run_step hook of CGI::Ex::App
and you will instantly get all of the common process flow for free.
The default out of the box configuration will map URIs to steps as follows:
- # Assuming /cgi-bin/my_cgi is the program being run
+ # Assuming /cgi-bin/my_app is the program being run
- URI: /cgi-bin/my_cgi
+ URI: /cgi-bin/my_app
STEP: main
FORM: {}
WHY: No other information is passed. The path method is
called which eventually calls ->default_step which
defaults to "main"
- URI: /cgi-bin/my_cgi?foo=bar
+ URI: /cgi-bin/my_app?foo=bar
STEP: main
FORM: {foo => "bar"}
WHY: Same as previous example except that QUERY_STRING
information was passed and placed in form.
- URI: /cgi-bin/my_cgi?step=my_step
+ URI: /cgi-bin/my_app?step=my_step
STEP: my_step
FORM: {step => "my_step"}
WHY: The path method is called which looks in $self->form
for the key ->step_key (which defaults to "step").
- URI: /cgi-bin/my_cgi?step=my_step&foo=bar
+ URI: /cgi-bin/my_app?step=my_step&foo=bar
STEP: my_step
FORM: {foo => "bar", step => "my_step"}
WHY: Same as before but has other parameters were passed.
- URI: /cgi-bin/my_cgi/my_step
+ URI: /cgi-bin/my_app/my_step
STEP: my_step
FORM: {step => "my_step"}
WHY: The path method is called which called path_info_map_base
$self->form->{$self->step_key} for the initial step. See
the path_info_map_base method for more information.
- URI: /cgi-bin/my_cgi/my_step?foo=bar
+ URI: /cgi-bin/my_app/my_step?foo=bar
STEP: my_step
FORM: {foo => "bar", step => "my_step"}
WHY: Same as before but other parameters were passed.
- URI: /cgi-bin/my_cgi/my_step?step=other_step
+ URI: /cgi-bin/my_app/my_step?step=other_step
STEP: other_step
FORM: {step => "other_step"}
WHY: The same procedure took place, but when the PATH_INFO
];
}
- URI: /cgi-bin/my_cgi/my_step/bar
+ URI: /cgi-bin/my_app/my_step/bar
STEP: my_step
FORM: {foo => "bar"}
WHY: The step was matched as in previous examples using
and the corresponding matched value was placed into
the form using the keys specified following the regex.
- URI: /cgi-bin/my_cgi/my_step/bar/1234
+ URI: /cgi-bin/my_app/my_step/bar/1234
STEP: my_step
FORM: {foo => "bar", id => "1234"}
WHY: Same as the previous example, except that the first
order that will match the most data. The third regex
would also match this PATH_INFO.
- URI: /cgi-bin/my_cgi/my_step/some/other/type/of/data
+ URI: /cgi-bin/my_app/my_step/some/other/type/of/data
STEP: my_step
FORM: {anything_else => 'some/other/type/of/data'}
WHY: Same as the previous example, except that the third
regex matched.
- URI: /cgi-bin/my_cgi/my_step/bar?bling=blang
+ URI: /cgi-bin/my_app/my_step/bar?bling=blang
STEP: my_step
FORM: {foo => "bar", bling => "blang"}
WHY: Same as the first step, but additional QUERY_STRING
information was passed.
- URI: /cgi-bin/my_cgi/my_step/one%20two?bar=three%20four
+ URI: /cgi-bin/my_app/my_step/one%20two?bar=three%20four
STEP: my_step
FORM: {anything_else => "one two", bar => "three four"}
WHY: The third path_info_map regex matched. Note that the
See the path_info_map_base method, and path_info_map hook for more information
on how the path_info maps function.
-A Dumper($self->dump_history) is very useful for determing what hooks have
-taken place.
+Using the following code is very useful for determing what hooks have
+taken place:
+
+ use CGI::Ex::Dump qw(debug);
+
+ sub post_navigate {
+ my $self = shift;
+ debug $self->dump_history, $self->form;
+ }
=head1 ADDING DATA VALIDATION TO A STEP
The default form filler is CGI::Ex::Fill which is similar to HTML::FillInForm but
has several benefits. See the CGI::Ex::Fill module for the available options.
+=head1 FINDING TEMPLATES AND VALIDATION FILES
+
+CGI::Ex::App tries to help your applications use a good template directory layout, but allows
+for you to override everything.
+
+External template files are used for storing your html templates and
+for storing your validation files (if you use externally stored
+validation files).
+
+The default file_print hook will look for content on your file system,
+but it can also be completely overridden to return a reference to a
+scalar containing the contents of your file. Actually it can return
+anything that CGI::Ex::Template (Template::Toolkit compatible) will
+treat as input. This templated html is displayed to the user during
+any step that enters the "print" phase.
+
+Similarly the default file_val hook will look for a validation file on
+the file system, but it too can return a reference to a scalar
+containing the contents of a validation file. It may actually return
+anything that the CGI::Ex::Validate get_validation method is able to
+understand. This validation is used by the default "info_complete"
+method for verifying if the submitted information passes its specific
+checks. A more common way of inlining validation is to return a
+validation hash from a hash_validation hook override.
+
+If the default file_print and file_val hooks are used, the following methods
+are employed for finding templates and validation files on your filesystem (they
+are also documented more in the HOOKS AND METHODS section.
+
+=over 4
+
+=item base_dir_abs
+
+Absolute path or arrayref of paths to the base templates directory. Default "".
+
+=item base_dir_rel
+
+Relative path inside of the base_dir_abs directory where content can be found. Default "".
+
+=item name_module
+
+Directory inside of base_dir_rel where files for the current CGI (module) will be
+stored. Default value is $ENV{SCRIPT_NAME} with path and extension removed.
+
+=item name_step
+
+Used with ext_print and ext_val for creating the filename that will be looked for
+inside of the name_module directory. Default value is the current step.
+
+=item ext_print and ext_val
+
+Filename extensions added to name_step to create the filename looked for
+inside of the name_module directory. Default is "html" for ext_print and "val"
+for ext_val.
+
+=back
+
+It may be easier to understand the usage of each of these methods by showing
+a contrived example. The following is a hypothetical layout for your templates:
+
+ /home/user/templates/
+ /home/user/templates/chunks/
+ /home/user/templates/wrappers/
+ /home/user/templates/content/
+ /home/user/templates/content/my_app/
+ /home/user/templates/content/my_app/main.html
+ /home/user/templates/content/my_app/step1.html
+ /home/user/templates/content/my_app/step1.val
+ /home/user/templates/content/another_cgi/main.html
+
+In this example we would most likely set values as follows:
+
+ base_dir_abs /home/user/templates
+ base_dir_rel content
+ name_module my_app
+
+The name_module method defaults to the name of the running program, but
+with the path and extension removed. So if we were running
+/cgi-bin/my_app.pl, /cgi-bin/my_app, or /anypath/my_app, then
+name_module would default to "my_app" and we wouldn't have to
+hard code the value. Often it is wise to set the value anyway so
+that we can change the name of the cgi script without effecting
+where template content should be stored.
+
+Continuing with the example and assuming that name of the step that
+the user has requested is "step1" then the following values would be
+returned:
+
+ base_dir_abs /home/user/templates
+ base_dir_rel content
+ name_module my_app
+ name_step step1
+ ext_print html
+ ext_val val
+
+ file_print content/my_app/step1.html
+ file_val /home/user/templates/content/my_app/step1.val
+
+The call to the template engine would look something like
+the following:
+
+ my $t = $self->template_obj({
+ INCLUDE_PATH => $self->base_dir_abs,
+ });
+
+ $t->process($self->file_print($step), \%vars);
+
+The template engine would then look for the relative file
+inside of the absolute paths (from base_dir_abs).
+
+The call to the validation engine would pass the absolute
+filename that is returned by file_val.
+
+The name_module and name_step methods can return filenames with
+additional directories included. The previous example could
+also have been setup using the following values:
+
+ base_dir_abs /home/user/templates
+ base_dir_rel
+ name_module content/my_app
+
+In this case the same values would be returned for the file_print and file_val hooks
+as were returned in the previous setup.
+
=head1 SYNOPSIS (A LONG "SYNOPSIS")
This example script would most likely be in the form of a cgi, accessible via
See the get_valid_auth method.
+=item base_dir_abs (method)
+
+Used as the absolute base directory to find template files and validation files.
+It may return a single value or an arrayref of values, or a coderef that
+returns an arrayref or coderef of values. You may pass base_dir_abs
+as a parameter in the arguments passed to the "new" method.
+
+Default value is "".
+
+For example, to pass multiple paths, you would use something
+similar to the following:
+
+ sub base_dir_abs {
+ return ['/my/path/one', '/some/other/path'];
+ }
+
+The base_dir_abs value is used along with the base_dir_rel, name_module,
+name_step, ext_print and ext_values for determining the values
+returned by the default file_print and file_val hooks. See those methods
+for further discussion.
+
+See the section on FINDING TEMPLATES for further discussion.
+
+=item base_dir_rel (method)
+
+Added as a relative base directory to content under the base_dir_abs directory.
+
+Default value is "".
+
+The base_dir_abs method is used as top level where template includes may
+pull from, while the base_dir_rel is directory relative to the base_dir_abs
+where the content files will be stored.
+
+A value for base_dir_rel may passed as a parameter in the arguments passed
+to the new method.
+
+See the base_dir_abs method for more discussion.
+
+See the section on FINDING TEMPLATES for further discussion.
+
=item cleanup_user (method)
-Installed as a hook during get_valid_auth. Allows for cleaning
+Used as a hook during get_valid_auth. Allows for cleaning
up the username. See the get_valid_auth method.
- sub cleanup_user {
- my ($self, $user) = @_;
- return lc $user;
- }
+ sub cleanup_user {
+ my ($self, $user) = @_;
+ return lc $user;
+ }
=item clear_app (method)
unmorphed before returning. Also - the post_navigate method will
still be called.
+=item ext_print (method)
+
+Added as suffix to "name_step" during the default file_print hook.
+
+Default value is 'html'.
+
+For example, if name_step returns "foo" and ext_print returns "html"
+then the file "foo.html" will be searched for.
+
+See the section on FINDING TEMPLATES for further discussion.
+
+=item ext_val (method)
+
+Added as suffix to "name_step" during the default file_val hook.
+
+Default value is 'val'.
+
+For example, if name_step returns "foo" and ext_val returns "val"
+then the file "foo.val" will be searched for.
+
+See the section on FINDING TEMPLATES for further discussion.
+
=item first_step (method)
Returns the first step of the path. Note that first_step may not be the same
Returns a filename containing the validation. Performs the same
as file_print, but uses ext_val to get the extension, and it adds
base_dir_abs onto the returned value (file_print is relative to
-base_dir_abs, while file_val is fully qualified with base_dir_abs)
+base_dir_abs, while file_val is fully qualified with base_dir_abs).
+If base_dir_abs returns an arrayref of paths, then each path is
+checked for the existence of the file.
The file should be readable by CGI::Ex::Validate::get_validation.
See the file_print method for more information.
+See the section on FINDING TEMPLATES for further discussion.
+
=item name_step (hook)
Return the step (appended to name_module) that should used when
looking up the file in file_print and file_val lookups. Defaults to
the current step.
+See the section on FINDING TEMPLATES for further discussion.
+
=item nav_loop (method)
This is the main loop runner. It figures out the current path
Arguments are the template and the swap hashref. The template can be either a
scalar reference to the actual content, or the filename of the content. If the
-filename is specified - it should be relative to base_dir_abs.
+filename is specified - it should be relative to base_dir_abs (which will be
+used to initialize INCLUDE_PATH by default).
+
+The default method will create a template object by calling the template_args hook
+and passing the returned hashref to the template_obj method. The default template_obj method
+returns a CGI::Ex::Template object, but could easily be swapped to use a Template::Toolkit
+based object. If a non-Template::Toolkit compatible object is to be used, then
+the swap_template hook can be overridden to use another templating engine.
+
+For example to use the HTML::Template engine you could override the swap_template
+method as follows:
+
+ use HTML::Template;
+
+ sub swap_template {
+ my ($self, $step, $file, $swap) = @_;
+
+ my $type = UNIVERSAL::isa($file, 'SCALAR') ? 'scalarref'
+ : UNIVERSAL::isa($file, 'ARRAY') ? 'arrayref'
+ : ref($file) ? 'filehandle'
+ : 'filename';
+
+ my $t = HTML::Template->new(source => $file,
+ type => $type,
+ path => $self->base_dir_abs,
+ die_on_bad_params => 0,
+ );
+
+ $t->param($swap);
+
+ return $t->output;
+ }
=item template_args (hook)
Returns a hashref of args that will be passed to the "new" method of CGI::Ex::Template.
-By default this hashref contains INCLUDE_PATH which is set equal to base_dir_abs.
-It can be augmented with any arguments that CGI::Ex::Template would understand.
-
- sub template_args {
- return {
- INCLUDE_PATH => '/my/own/include/path',
- WRAPPER => 'wrappers/main_wrapper.html',
- };
- }
+The method is normally called from the swap_template hook. The swap_template hook
+will add a value for INCLUDE_PATH which is set equal to base_dir_abs, if the INCLUDE_PATH
+value is not already set.
+
+The returned hashref can contain any arguments that CGI::Ex::Template would understand.
+
+ sub template_args {
+ return {
+ PRE_CHOMP => 1,
+ WRAPPER => 'wrappers/main_wrapper.html',
+ };
+ }
+
+=item template_obj (method)
+
+Called from swap_template. It is passed the result of template_args that have
+had a default INCLUDE_PATH added. The default implementation uses CGI::Ex::Template
+but can easily be changed to use Template::Toolkit by using code similar to the following:
+
+ use Template;
+
+ sub template_obj {
+ my ($self, $args) = @_;
+ return Template->new($args);
+ }
=item unmorph (method)
Paul Seamons <paul at seamons dot com>
+=head1 LICENSE
+
+This module may be distributed under the same terms as Perl itself.
+
=cut
use Digest::MD5 qw(md5_hex);
use CGI::Ex;
-$VERSION = '2.08';
+$VERSION = '2.09';
###----------------------------------------------------------------###
);
@EXPORT_OK = qw(conf_read conf_write in_cache);
-$VERSION = '2.08';
+$VERSION = '2.09';
$DEFAULT_EXT = 'conf';
use CGI::Ex::Dump qw(debug ctrace dex_html);
BEGIN {
- $VERSION = '2.08';
+ $VERSION = '2.09';
$SHOW_TRACE = 0 if ! defined $SHOW_TRACE;
$IGNORE_EVAL = 0 if ! defined $IGNORE_EVAL;
$EXTENDED_ERRORS = 1 if ! defined $EXTENDED_ERRORS;
use strict;
use Exporter;
-$VERSION = '2.08';
+$VERSION = '2.09';
@ISA = qw(Exporter);
@EXPORT = qw(dex dex_warn dex_text dex_html ctrace dex_trace);
@EXPORT_OK = qw(dex dex_warn dex_text dex_html ctrace dex_trace debug);
use base qw(Exporter);
BEGIN {
- $VERSION = '2.08';
+ $VERSION = '2.09';
@EXPORT = qw(form_fill);
@EXPORT_OK = qw(fill form_fill html_escape get_tagval_by_key swap_tagval_by_key);
};
use base qw(Exporter);
BEGIN {
- $VERSION = '2.08';
+ $VERSION = '2.09';
@EXPORT = qw(JSONDump);
@EXPORT_OK = @EXPORT;
);
BEGIN {
- $VERSION = '2.08';
+ $VERSION = '2.09';
$PACKAGE_EXCEPTION = 'CGI::Ex::Template::Exception';
$PACKAGE_ITERATOR = 'CGI::Ex::Template::Iterator';
};
$SCALAR_OPS = {
- '0' => sub { shift },
+ '0' => sub { $_[0] },
as => \&vmethod_as_scalar,
chunk => \&vmethod_chunk,
collapse => sub { local $_ = $_[0]; s/^\s+//; s/\s+$//; s/\s+/ /g; $_ },
- defined => sub { 1 },
+ defined => sub { defined $_[0] ? 1 : '' },
indent => \&vmethod_indent,
int => sub { local $^W; int $_[0] },
fmt => \&vmethod_as_scalar,
'format' => \&vmethod_format,
hash => sub { {value => $_[0]} },
html => sub { local $_ = $_[0]; s/&/&/g; s/</</g; s/>/>/g; s/\"/"/g; $_ },
+ item => sub { $_[0] },
lcfirst => sub { lcfirst $_[0] },
length => sub { defined($_[0]) ? length($_[0]) : 0 },
list => sub { [$_[0]] },
remove => sub { vmethod_replace(shift, shift, '', 1) },
repeat => \&vmethod_repeat,
replace => \&vmethod_replace,
- search => sub { my ($str, $pat) = @_; return $str if ! defined $str || ! defined $pat; return scalar $str =~ /$pat/ },
+ search => sub { my ($str, $pat) = @_; return $str if ! defined $str || ! defined $pat; return $str =~ /$pat/ },
size => sub { 1 },
split => \&vmethod_split,
stderr => sub { print STDERR $_[0]; '' },
- substr => sub { my ($str, $i, $len) = @_; defined($len) ? substr($str, $i, $len) : substr($str, $i) },
+ substr => \&vmethod_substr,
trim => sub { local $_ = $_[0]; s/^\s+//; s/\s+$//; $_ },
ucfirst => sub { ucfirst $_[0] },
upper => sub { uc $_[0] },
$LIST_OPS = {
as => \&vmethod_as_list,
+ defined => sub { return 1 if @_ == 1; defined $_[0]->[ defined($_[1]) ? $_[1] : 0 ] },
first => sub { my ($ref, $i) = @_; return $ref->[0] if ! $i; return [@{$ref}[0 .. $i - 1]]},
fmt => \&vmethod_as_list,
- grep => sub { my ($ref, $pat) = @_; [grep {/$pat/} @$ref] },
- hash => sub { local $^W; my ($list, $i) = @_; defined($i) ? {map {$i++ => $_} @$list} : {@$list} },
+ grep => sub { local $^W; my ($ref, $pat) = @_; [grep {/$pat/} @$ref] },
+ hash => sub { local $^W; my $list = shift; return {@$list} if ! @_; my $i = shift || 0; return {map {$i++ => $_} @$list} },
+ import => sub { my $ref = shift; push @$ref, grep {defined} map {ref eq 'ARRAY' ? @$_ : undef} @_; '' },
+ item => sub { $_[0]->[ $_[1] || 0 ] },
join => sub { my ($ref, $join) = @_; $join = ' ' if ! defined $join; local $^W; return join $join, @$ref },
last => sub { my ($ref, $i) = @_; return $ref->[-1] if ! $i; return [@{$ref}[-$i .. -1]]},
list => sub { $_[0] },
- max => sub { $#{ $_[0] } },
+ max => sub { local $^W; $#{ $_[0] } },
merge => sub { my $ref = shift; return [ @$ref, grep {defined} map {ref eq 'ARRAY' ? @$_ : undef} @_ ] },
new => sub { local $^W; return [@_] },
+ null => sub { '' },
nsort => \&vmethod_nsort,
pop => sub { pop @{ $_[0] } },
push => sub { my $ref = shift; push @$ref, @_; return '' },
random => sub { my $ref = shift; $ref->[ rand @$ref ] },
reverse => sub { [ reverse @{ $_[0] } ] },
shift => sub { shift @{ $_[0] } },
- size => sub { scalar @{ $_[0] } },
+ size => sub { local $^W; scalar @{ $_[0] } },
slice => sub { my ($ref, $a, $b) = @_; $a ||= 0; $b = $#$ref if ! defined $b; return [@{$ref}[$a .. $b]] },
sort => \&vmethod_sort,
splice => \&vmethod_splice,
- unique => sub { my %u; return [ grep { ! $u{$_} ++ } @{ $_[0] } ] },
+ unique => sub { my %u; return [ grep { ! $u{$_}++ } @{ $_[0] } ] },
unshift => sub { my $ref = shift; unshift @$ref, @_; return '' },
};
$HASH_OPS = {
as => \&vmethod_as_hash,
- defined => sub { return '' if ! defined $_[1]; defined $_[0]->{ $_[1] } },
- delete => sub { return '' if ! defined $_[1]; delete $_[0]->{ $_[1] } },
+ defined => sub { return 1 if @_ == 1; defined $_[0]->{ defined($_[1]) ? $_[1] : '' } },
+ delete => sub { my $h = shift; my @v = delete @{ $h }{map {defined($_) ? $_ : ''} @_}; @_ == 1 ? $v[0] : \@v },
each => sub { [%{ $_[0] }] },
- exists => sub { return '' if ! defined $_[1]; exists $_[0]->{ $_[1] } },
+ exists => sub { exists $_[0]->{ defined($_[1]) ? $_[1] : '' } },
fmt => \&vmethod_as_hash,
hash => sub { $_[0] },
- import => sub { my ($a, $b) = @_; return '' if ref($b) ne 'HASH'; @{$a}{keys %$b} = values %$b; '' },
- item => sub { my ($h, $k) = @_; return '' if ! defined $k || $k =~ $QR_PRIVATE; $h->{$k} },
+ import => sub { my ($a, $b) = @_; @{$a}{keys %$b} = values %$b if ref($b) eq 'HASH'; '' },
+ item => sub { my ($h, $k) = @_; $k = '' if ! defined $k; $k =~ $QR_PRIVATE ? undef : $h->{$k} },
items => sub { [ %{ $_[0] } ] },
keys => sub { [keys %{ $_[0] }] },
- list => sub { [$_[0]] },
+ list => \&vmethod_list_hash,
new => sub { local $^W; return (@_ == 1 && ref $_[-1] eq 'HASH') ? $_[-1] : {@_} },
- nsort => sub { my $ref = shift; [sort {$ref->{$a} <=> $ref->{$b} } keys %$ref] },
- pairs => sub { [map { {key => $_, value => $_[0]->{$_}} } keys %{ $_[0] } ] },
+ null => sub { '' },
+ nsort => sub { my $ref = shift; [sort { $ref->{$a} <=> $ref->{$b}} keys %$ref] },
+ pairs => sub { [map { {key => $_, value => $_[0]->{$_}} } sort keys %{ $_[0] } ] },
size => sub { scalar keys %{ $_[0] } },
sort => sub { my $ref = shift; [sort {lc $ref->{$a} cmp lc $ref->{$b}} keys %$ref] },
values => sub { [values %{ $_[0] }] },
}
}
+sub vmethod_list_hash {
+ my ($hash, $what) = @_;
+ $what = 'pairs' if ! $what || $what !~ /^(keys|values|each|pairs)$/;
+ return $HASH_OPS->{$what}->($hash);
+}
+
+
sub vmethod_match {
my ($str, $pat, $global) = @_;
return [] if ! defined $str || ! defined $pat;
my @res = $global ? ($str =~ /$pat/g) : ($str =~ /$pat/);
- return (@res >= 2) ? \@res : (@res == 1) ? $res[0] : '';
+ return @res ? \@res : '';
}
sub vmethod_nsort {
sub vmethod_repeat {
my ($str, $n, $join) = @_;
- return if ! length $str;
+ return '' if ! defined $str || ! length $str;
$n = 1 if ! defined($n) || ! length $n;
$join = '' if ! defined $join;
return join $join, ($str) x $n;
@replace = @{ $replace[0] } if @replace == 1 && ref $replace[0] eq 'ARRAY';
if (defined $len) {
return [splice @$ref, $i || 0, $len, @replace];
+ } elsif (defined $i) {
+ return [splice @$ref, $i];
} else {
- return [splice @$ref, $i || 0];
+ return [splice @$ref];
}
}
sub vmethod_split {
- my ($str, $pat, @args) = @_;
+ my ($str, $pat, $lim) = @_;
$str = '' if ! defined $str;
- return defined $pat ? [split $pat, $str, @args] : [split ' ', $str, @args];
+ if (defined $lim) { return defined $pat ? [split $pat, $str, $lim] : [split ' ', $str, $lim] }
+ else { return defined $pat ? [split $pat, $str ] : [split ' ', $str ] }
+}
+
+sub vmethod_substr {
+ my ($str, $i, $len, $replace) = @_;
+ $i ||= 0;
+ return substr($str, $i) if ! defined $len;
+ return substr($str, $i, $len) if ! defined $replace;
+ substr($str, $i, $len, $replace);
+ return $str;
}
sub vmethod_uri {
@UNSUPPORTED_BROWSERS
);
-$VERSION = '2.08';
+$VERSION = '2.09';
$DEFAULT_EXT = 'val';
$QR_EXTRA = qr/^(\w+_error|as_(array|string|hash)_\w+|no_\w+)/;
use vars qw($module $is_tt);
BEGIN {
- $module = 'CGI::Ex::Template'; #real 0m1.243s #user 0m0.695s #sys 0m0.018s
- #$module = 'Template'; #real 0m2.329s #user 0m1.466s #sys 0m0.021s
+ $module = 'CGI::Ex::Template'; #real 0m1.113s #user 0m0.416s #sys 0m0.016s
+# $module = 'Template'; #real 0m3.022s #user 0m1.168s #sys 0m0.024s
$is_tt = $module eq 'Template';
};
use strict;
-use Test::More tests => 521 - ($is_tt ? 109 : 0);
+use Test::More tests => ! $is_tt ? 662 : 519;
use Data::Dumper qw(Dumper);
use constant test_taint => 0 && eval { require Taint::Runtime };
ok($t == 3, "CALL method actually called var");
###----------------------------------------------------------------###
-### virtual methods / filters
+### virtual method tests
+
+# scalar vmethods
+process_ok("[% n.0 %]" => '7', {n => 7}) if ! $is_tt;
+process_ok("[% n.as %]" => '7', {n => 7}) if ! $is_tt;
+process_ok("[% n.as('%02d') %]" => '07', {n => 7}) if ! $is_tt;
+process_ok("[% n.as('%0*d', 3) %]" => '007', {n => 7}) if ! $is_tt;
+process_ok("[% n.as('(%s)') %]" => "(a\nb)", {n => "a\nb"}) if ! $is_tt;
+process_ok("[% n.chunk(3).join %]" => 'abc def g', {n => 'abcdefg'});
+process_ok("[% n.chunk(-3).join %]" => 'a bcd efg', {n => 'abcdefg'});
+process_ok("[% n|collapse %]" => "a b", {n => ' a b '}); # TT2 filter
+process_ok("[% n.defined %]" => "1", {n => ''});
+process_ok("[% n.defined %]" => "", {n => undef});
+process_ok("[% n.defined %]" => "1", {n => '1'});
+process_ok("[% n|indent %]" => " a\n b", {n => "a\nb"}); # TT2 filter
+process_ok("[% n|indent(2) %]" => " a\n b", {n => "a\nb"}); # TT2 filter
+process_ok("[% n|indent('wow ') %]" => "wow a\nwow b", {n => "a\nb"}); # TT2 filter
+process_ok("[% n.int %]" => "123", {n => "123.234"}) if ! $is_tt;
+process_ok("[% n.int %]" => "123", {n => "123gggg"}) if ! $is_tt;
+process_ok("[% n.int %]" => "0", {n => "ff123.234"}) if ! $is_tt;
+process_ok("[% n.fmt('%02d') %]" => '07', {n => 7}) if ! $is_tt;
+process_ok("[% n.fmt('%0*d', 3) %]" => '007', {n => 7}) if ! $is_tt;
+process_ok("[% n.fmt('(%s)') %]" => "(a\nb)", {n => "a\nb"}) if ! $is_tt;
+process_ok("[% n|format('%02d') %]" => '07', {n => 7}); # TT2 filter
+process_ok("[% n.format('%0*d', 3) %]" => '007', {n => 7}) if ! $is_tt;
+process_ok("[% n|format('(%s)') %]" => "(a)\n(b)", {n => "a\nb"}); # TT2 filter
+process_ok("[% n.hash.items.1 %]" => "b", {n => {a => "b"}});
+process_ok("[% n|html %]" => "&", {n => '&'}); # TT2 filter
+process_ok("[% n.item %]" => '7', {n => 7});
+process_ok("[% n|lcfirst %]" => 'fOO', {n => "FOO"}); # TT2 filter
+process_ok("[% n.length %]" => 3, {n => "abc"});
+process_ok("[% n.list.0 %]" => 'abc', {n => "abc"});
+process_ok("[% n|lower %]" => 'abc', {n => "ABC"}); # TT2 filter
+process_ok("[% n.match('foo').join %]" => '', {n => "bar"});
+process_ok("[% n.match('foo').join %]" => '1', {n => "foo"});
+process_ok("[% n.match('foo',1).join %]" => 'foo', {n => "foo"});
+process_ok("[% n.match('(foo)').join %]" => 'foo', {n => "foo"});
+process_ok("[% n.match('(foo)').join %]" => 'foo', {n => "foofoo"});
+process_ok("[% n.match('(foo)',1).join %]" => 'foo foo', {n => "foofoo"});
+process_ok("[% n.null %]" => '', {n => "abc"});
+process_ok("[% n.rand %]" => qr{^\d+\.\d+}, {n => "2"}) if ! $is_tt;
+process_ok("[% n.rand %]" => qr{^\d+\.\d+}, {n => "ab"}) if ! $is_tt;
+process_ok("[% n.remove('bc') %]" => "a", {n => "abc"});
+process_ok("[% n.remove('bc') %]" => "aa", {n => "abcabc"});
+process_ok("[% n.repeat %]" => '1', {n => 1}) if ! $is_tt; # tt2 virtual method defaults to 0
+process_ok("[% n.repeat(0) %]" => '', {n => 1});
+process_ok("[% n.repeat(1) %]" => '1', {n => 1});
+process_ok("[% n.repeat(2) %]" => '11', {n => 1});
+process_ok("[% n.repeat(2,'|') %]" => '1|1', {n => 1}) if ! $is_tt;
+process_ok("[% n.replace('foo', 'bar') %]" => 'barbar', {n => 'foofoo'});
+process_ok("[% n.replace('(foo)', 'bar\$1') %]" => 'barfoobarfoo', {n => 'foofoo'}) if ! $is_tt;
+process_ok("[% n.replace('foo', 'bar', 0) %]" => 'barfoo', {n => 'foofoo'}) if ! $is_tt;
+process_ok("[% n.search('foo') %]" => '', {n => "bar"});
+process_ok("[% n.search('foo') %]" => '1', {n => "foo"});
+process_ok("[% n.size %]" => '1', {n => "foo"});
+process_ok("[% n.split.join('|') %]" => "abc", {n => "abc"});
+process_ok("[% n.split.join('|') %]" => "a|b|c", {n => "a b c"});
+process_ok("[% n.split.join('|') %]" => "a|b|c", {n => "a b c"});
+process_ok("[% n.split(u,2).join('|') %]" => "a|b c", {n => "a b c", u => undef}) if ! $is_tt;
+process_ok("[% n.split(u,2).join('|') %]" => "a| b c", {n => "a b c", u => undef}) if $is_tt;
+process_ok("[% n.split('/').join('|') %]" => "a|b|c", {n => "a/b/c"});
+process_ok("[% n.split('/', 2).join('|') %]" => "a|b/c", {n => "a/b/c"});
+process_ok("[% n.stderr %]" => "", {n => "# testing stderr ... ok\n"});
+process_ok("[% n|trim %]" => "a b", {n => ' a b '}); # TT2 filter
+process_ok("[% n|ucfirst %]" => 'Foo', {n => "foo"}); # TT2 filter
+process_ok("[% n|upper %]" => 'FOO', {n => "foo"}); # TT2 filter
+process_ok("[% n|uri %]" => 'a%20b', {n => "a b"}); # TT2 filter
+
+# list vmethods
+process_ok("[% a.as %]" => '2 3', {a => [2,3]}) if ! $is_tt;
+process_ok("[% a.as('%02d') %]" => '02 03', {a => [2,3]}) if ! $is_tt;
+process_ok("[% a.as('%02d',' ') %]" => '02 03', {a => [2,3]}) if ! $is_tt;
+process_ok("[% a.as('%02d','|') %]" => '02|03', {a => [2,3]}) if ! $is_tt;
+process_ok("[% a.as('%0*d','|', 3) %]" => '002|003', {a => [2,3]}) if ! $is_tt;
+process_ok("[% a.defined %]" => '1', {a => [2,3]});
+process_ok("[% a.defined(1) %]" => '1', {a => [2,3]});
+process_ok("[% a.defined(3) %]" => '', {a => [2,3]});
+process_ok("[% a.first %]" => '2', {a => [2..10]});
+process_ok("[% a.first(3).join %]" => '2 3 4', {a => [2..10]});
+process_ok("[% a.fmt('%02d',' ') %]" => '02 03', {a => [2,3]}) if ! $is_tt;
+process_ok("[% a.fmt('%02d','|') %]" => '02|03', {a => [2,3]}) if ! $is_tt;
+process_ok("[% a.fmt('%0*d','|', 3) %]" => '002|003', {a => [2,3]}) if ! $is_tt;
+process_ok("[% a.grep.join %]" => '2 3', {a => [2,3]});
+process_ok("[% a.grep(2).join %]" => '2', {a => [2,3]});
+process_ok("[% a.hash.items.join %]" => '2 3', {a => [2,3]});
+process_ok("[% a.hash(5).items.sort.join %]" => '2 3 5 6', {a => [2,3]});
+process_ok("[% a.import(5) %]|[% a.join %]" => '|2 3', {a => [2,3]}) if ! $is_tt;
+process_ok("[% a.import(5) %]|[% a.join %]" => qr{^ARRAY.+|2 3$ }x, {a => [2,3]}) if $is_tt;
+process_ok("[% a.import([5]) %]|[% a.join %]" => '|2 3 5', {a => [2,3]}) if ! $is_tt;
+process_ok("[% a.import([5]) %]|[% a.join %]" => qr{ARRAY.+|2 3 5$ }x, {a => [2,3]}) if $is_tt;
+process_ok("[% a.item %]" => '2', {a => [2,3]});
+process_ok("[% a.item(1) %]" => '3', {a => [2,3]});
+process_ok("[% a.join %]" => '2 3', {a => [2,3]});
+process_ok("[% a.join('|') %]" => '2|3', {a => [2,3]});
+process_ok("[% a.last %]" => '10', {a => [2..10]});
+process_ok("[% a.last(3).join %]" => '8 9 10', {a => [2..10]});
+process_ok("[% a.list.join %]" => '2 3', {a => [2, 3]});
+process_ok("[% a.max %]" => '1', {a => [2, 3]});
+process_ok("[% a.merge(5).join %]" => '2 3', {a => [2,3]});
+process_ok("[% a.merge([5]).join %]" => '2 3 5', {a => [2,3]});
+process_ok("[% a.merge([5]).null %][% a.join %]" => '2 3', {a => [2,3]});
+process_ok("[% a.nsort.join %]" => '1 2 3', {a => [2, 3, 1]});
+process_ok("[% a.nsort('b').0.b %]" => '7', {a => [{b => 23}, {b => 7}]});
+process_ok("[% a.pop %][% a.join %]" => '32', {a => [2, 3]});
+process_ok("[% a.push(3) %][% a.join %]" => '2 3 3', {a => [2, 3]});
+process_ok("[% a.random %]" => qr{ ^\d$ }x, {a => [2, 3]}) if ! $is_tt;
+process_ok("[% a.reverse.join %]" => '3 2', {a => [2, 3]});
+process_ok("[% a.shift %][% a.join %]" => '23', {a => [2, 3]});
+process_ok("[% a.size %]" => '2', {a => [2, 3]});
+process_ok("[% a.slice.join %]" => '2 3 4 5', {a => [2..5]});
+process_ok("[% a.slice(2).join %]" => '4 5', {a => [2..5]});
+process_ok("[% a.slice(0,2).join %]" => '2 3 4', {a => [2..5]});
+process_ok("[% a.sort.join %]" => '1 2 3', {a => [2, 3, 1]});
+process_ok("[% a.sort('b').0.b %]" => 'wee', {a => [{b => "wow"}, {b => "wee"}]});
+process_ok("[% a.splice.join %]|[% a.join %]" => '2 3 4 5|', {a => [2..5]});
+process_ok("[% a.splice(2).join %]|[% a.join %]" => '4 5|2 3', {a => [2..5]});
+process_ok("[% a.splice(0,2).join %]|[% a.join %]" => '2 3|4 5', {a => [2..5]});
+process_ok("[% a.splice(0,2,'hrm').join %]|[% a.join %]" => '2 3|hrm 4 5', {a => [2..5]});
+process_ok("[% a.unique.join %]" => '2 3', {a => [2,3,3,3,2]});
+process_ok("[% a.unshift(3) %][% a.join %]" => '3 2 3', {a => [2, 3]});
+
+# hash vmethods
+process_ok("[% h.as %]" => "b\tB\nc\tC", {h => {b => "B", c => "C"}}) if ! $is_tt;
+process_ok("[% h.as('%s => %s') %]" => "b => B\nc => C", {h => {b => "B", c => "C"}}) if ! $is_tt;
+process_ok("[% h.as('%s => %s', '|') %]" => "b => B|c => C", {h => {b => "B", c => "C"}}) if ! $is_tt;
+process_ok("[% h.as('%*s=>%s', '|', 3) %]" => " b=>B| c=>C", {h => {b => "B", c => "C"}}) if ! $is_tt;
+process_ok("[% h.as('%*s=>%*s', '|', 3, 4) %]" => " b=> B| c=> C", {h => {b => "B", c => "C"}}) if ! $is_tt;
+process_ok("[% h.defined %]" => "1", {h => {}});
+process_ok("[% h.defined('a') %]" => "1", {h => {a => 1}});
+process_ok("[% h.defined('b') %]" => "", {h => {a => 1}});
+process_ok("[% h.defined('a') %]" => "", {h => {a => undef}});
+process_ok("[% h.delete('a') %]|[% h.keys.0 %]" => "1|b", {h => {a => 1, b=> 2}}) if ! $is_tt;
+process_ok("[% h.delete('a') %]|[% h.keys.0 %]" => "|b", {h => {a => 1, b=> 2}}) if $is_tt;
+process_ok("[% h.delete('a', 'b').join %]|[% h.keys.0 %]" => "1 2|", {h => {a => 1, b=> 2}}) if ! $is_tt;
+process_ok("[% h.delete('a', 'b').join %]|[% h.keys.0 %]" => "|", {h => {a => 1, b=> 2}}) if $is_tt;
+process_ok("[% h.delete('a', 'c').join %]|[% h.keys.0 %]" => "1 |b", {h => {a => 1, b=> 2}}) if ! $is_tt;
+process_ok("[% h.delete('a', 'c').join %]|[% h.keys.0 %]" => "|b", {h => {a => 1, b=> 2}}) if $is_tt;
+process_ok("[% h.each.sort.join %]" => "1 2 a b", {h => {a => 1, b=> 2}});
+process_ok("[% h.exists('a') %]" => "1", {h => {a => 1}});
+process_ok("[% h.exists('b') %]" => "", {h => {a => 1}});
+process_ok("[% h.exists('a') %]" => "1", {h => {a => undef}});
+process_ok("[% h.fmt('%s => %s') %]" => "b => B\nc => C", {h => {b => "B", c => "C"}}) if ! $is_tt;
+process_ok("[% h.fmt('%s => %s', '|') %]" => "b => B|c => C", {h => {b => "B", c => "C"}}) if ! $is_tt;
+process_ok("[% h.fmt('%*s=>%s', '|', 3) %]" => " b=>B| c=>C", {h => {b => "B", c => "C"}}) if ! $is_tt;
+process_ok("[% h.fmt('%*s=>%*s', '|', 3, 4) %]" => " b=> B| c=> C", {h => {b => "B", c => "C"}}) if ! $is_tt;
+process_ok("[% h.hash.fmt %]" => "b\tB\nc\tC", {h => {b => "B", c => "C"}}) if ! $is_tt;
+process_ok("[% h.import('a') %]|[% h.items.sort.join %]" => "|b B c C", {h => {b => "B", c => "C"}});
+process_ok("[% h.import({'b' => 'boo'}) %]|[% h.items.sort.join %]" => "|b boo c C", {h => {b => "B", c => "C"}});
+process_ok("[% h.item('a') %]" => 'A', {h => {a => 'A'}});
+process_ok("[% h.item('_a') %]" => '', {h => {_a => 'A'}}) if ! $is_tt;
+process_ok("[% h.items.sort.join %]" => "1 2 a b", {h => {a => 1, b=> 2}});
+process_ok("[% h.keys.sort.join %]" => "a b", {h => {a => 1, b=> 2}});
+process_ok("[% h.list('each').sort.join %]" => "1 2 a b", {h => {a => 1, b=> 2}});
+process_ok("[% h.list('keys').sort.join %]" => "a b", {h => {a => 1, b=> 2}});
+process_ok("[% h.list('pairs').0.items.sort.join %]" => "1 a key value", {h => {a => 1, b=> 2}});
+process_ok("[% h.list('values').sort.join %]" => "1 2", {h => {a => 1, b=> 2}});
+process_ok("[% h.null %]" => "", {h => {}});
+process_ok("[% h.nsort.join %]" => "b a", {h => {a => 7, b => 2}});
+process_ok("[% h.pairs.0.items.sort.join %]" => "1 a key value", {h => {a => 1, b=> 2}});
+process_ok("[% h.size %]" => "2", {h => {a => 1, b=> 2}});
+process_ok("[% h.sort.join %]" => "b a", {h => {a => "BBB", b => "A"}});
+process_ok("[% h.values.sort.join %]" => "1 2", {h => {a => 1, b=> 2}});
+
+###----------------------------------------------------------------###
+### more virtual methods / filters
process_ok("[% [0 .. 10].reverse.1 %]" => 9) if ! $is_tt;
process_ok("[% {a => 'A'}.a %]" => 'A') if ! $is_tt;
process_ok("[% a = 23; a.0 %]" => 23) if ! $is_tt; # '0' is a scalar_op
process_ok('[% 1.rand %]' => qr/^0\.\d+(?:e-?\d+)?$/) if ! $is_tt;
-process_ok("[% n.repeat %]" => '1', {n => 1}) if ! $is_tt; # tt2 virtual method defaults to 0
-process_ok("[% n.repeat(0) %]" => '', {n => 1});
-process_ok("[% n.repeat(1) %]" => '1', {n => 1});
-process_ok("[% n.repeat(2) %]" => '11', {n => 1});
-process_ok("[% n.repeat(2,'|') %]" => '1|1', {n => 1}) if ! $is_tt;
-
process_ok("[% n.size %]", => 'SIZE', {n => {size => 'SIZE', a => 'A'}});
process_ok("[% n|size %]", => '2', {n => {size => 'SIZE', a => 'A'}}) if ! $is_tt; # tt2 | is alias for FILTER
process_ok('[% foo | eval %]' => 'baz', {foo => '[% bar %]', bar => 'baz'});
process_ok('[% "1" | indent(2) %]' => ' 1');
-process_ok("[% n.replace('foo', 'bar') %]" => 'barbar', {n => 'foofoo'});
-process_ok("[% n.replace('(foo)', 'bar\$1') %]" => 'barfoobarfoo', {n => 'foofoo'}) if ! $is_tt;
-process_ok("[% n.replace('foo', 'bar', 0) %]" => 'barfoo', {n => 'foofoo'}) if ! $is_tt;
process_ok("[% n FILTER size %]", => '1', {n => {size => 'SIZE', a => 'A'}}) if ! $is_tt; # tt2 doesn't have size