]> Dogcows Code - chaz/p5-CGI-Ex/blobdiff - lib/CGI/Ex/Var.pm
CGI::Ex 2.00
[chaz/p5-CGI-Ex] / lib / CGI / Ex / Var.pm
diff --git a/lib/CGI/Ex/Var.pm b/lib/CGI/Ex/Var.pm
new file mode 100644 (file)
index 0000000..10bf7e0
--- /dev/null
@@ -0,0 +1,1197 @@
+package CGI::Ex::Var;
+
+=head1 NAME
+
+CGI::Ex::Var - Variable and expression parsing and execution for CGI::Ex::Template (and other takers)
+
+=head1 DESCRIPTION
+
+Experimental - The storage structure will change to match CGI::Ex::Template by the next release.
+
+=cut
+
+###----------------------------------------------------------------###
+#  Copyright 2006 - Paul Seamons                                     #
+#  Distributed under the Perl Artistic License without warranty      #
+###----------------------------------------------------------------###
+
+use strict;
+
+use vars qw(
+            $SCALAR_OPS
+            $FILTER_OPS
+            $LIST_OPS
+            $HASH_OPS
+            $FILTERS
+
+            $OPERATORS
+            $OP_UNARY
+            $OP_BINARY
+            $OP_TRINARY
+
+            $QR_OP
+            $QR_OP_UNARY
+            $QR_OP_PARENED
+            $QR_COMMENTS
+            $QR_AQ_NOTDOT
+            $QR_PRIVATE
+
+            $RT_NAMESPACE
+            $RT_FILTERS
+            $RT_CONTEXT_SUB
+            $RT_DEBUG_UNDEF
+            $RT_UNDEFINED_SUB
+            $RT_OPERATOR_PRECEDENCE
+            $RT_DURING_COMPILE
+
+            $TT_FILTERS
+            );
+use constant trace => 0;
+
+BEGIN {
+    $SCALAR_OPS = {
+        chunk    => \&vmethod_chunk,
+        collapse => sub { local $_ = $_[0]; s/^\s+//; s/\s+$//; s/\s+/ /g; $_ },
+        defined  => sub { 1 },
+        indent   => \&vmethod_indent,
+        'format' => \&vmethod_format,
+        hash     => sub { {value => $_[0]} },
+        html     => sub { local $_ = $_[0]; s/&/&amp;/g; s/</&lt;/g; s/>/&gt;/g; s/\"/&quot;/g; $_ },
+        lcfirst  => sub { lcfirst $_[0] },
+        length   => sub { defined($_[0]) ? length($_[0]) : 0 },
+        lower    => sub { lc $_[0] },
+        match    => \&vmethod_match,
+        null     => sub { '' },
+        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 $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) },
+        trim     => sub { local $_ = $_[0]; s/^\s+//; s/\s+$//; $_ },
+        ucfirst  => sub { ucfirst $_[0] },
+        upper    => sub { uc $_[0] },
+        uri      => sub { local $_ = $_[0]; s/([^;\/?:@&=+\$,A-Za-z0-9\-_.!~*\'()])/sprintf('%%%02X', ord($1))/eg; $_ },
+    };
+
+    $FILTER_OPS = { # generally - non-dynamic filters belong in scalar ops
+        eval     => [\&filter_eval, 1],
+        evaltt   => [\&filter_eval, 1],
+        file     => [\&filter_redirect, 1],
+        redirect => [\&filter_redirect, 1],
+    };
+
+    $LIST_OPS = {
+        first   => sub { my ($ref, $i) = @_; return $ref->[0] if ! $i; return [@{$ref}[0 .. $i - 1]]},
+        grep    => sub { my ($ref, $pat) = @_; [grep {/$pat/} @$ref] },
+        hash    => sub { my ($list, $i) = @_; defined($i) ? {map {$i++ => $_} @$list} : {@$list} },
+        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] } },
+        merge   => sub { my $ref = shift; return [ @$ref, grep {defined} map {ref eq 'ARRAY' ? @$_ : undef} @_ ] },
+        nsort   => \&vmethod_nsort,
+        pop     => sub { pop @{ $_[0] } },
+        push    => sub { my $ref = shift; push @$ref, @_; return '' },
+        reverse => sub { [ reverse @{ $_[0] } ] },
+        shift   => sub { shift  @{ $_[0] } },
+        size    => sub { 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] } ] },
+        unshift => sub { my $ref = shift; unshift @$ref, @_; return '' },
+    };
+
+    $HASH_OPS = {
+        defined => sub { return '' if ! defined $_[1]; defined $_[0]->{ $_[1] } },
+        delete  => sub { return '' if ! defined $_[1]; delete  $_[0]->{ $_[1] } },
+        each    => sub { [%{ $_[0] }] },
+        exists  => sub { return '' if ! defined $_[1]; exists $_[0]->{ $_[1] } },
+        hash    => sub { $_[0] },
+        import  => sub { my ($a, $b) = @_; return '' if ref($b) ne 'HASH'; @{$a}{keys %$b} = values %$b; '' },
+        keys    => sub { [keys %{ $_[0] }] },
+        list    => sub { [$_[0]] },
+        pairs   => sub { [map { {key => $_, value => $_[0]->{$_}} } keys %{ $_[0] } ] },
+        nsort   => sub { my $ref = shift; [sort {$ref->{$a}    <=> $ref->{$b}   } keys %$ref] },
+        size    => sub { scalar keys %{ $_[0] } },
+        sort    => sub { my $ref = shift; [sort {lc $ref->{$a} cmp lc $ref->{$b}} keys %$ref] },
+        values  => sub { [values %{ $_[0] }] },
+    };
+
+    ### Runtime set variables that control lookups of various pieces of info
+    $RT_NAMESPACE   = {};
+    $RT_FILTERS     = {};
+    $RT_CONTEXT_SUB = sub { {} };
+    $RT_DEBUG_UNDEF = 0;
+    $RT_OPERATOR_PRECEDENCE = 0;
+
+    ### setup the operator parsing
+    $OPERATORS ||= [
+        # name => # order, precedence, symbols, only_in_parens, sub to create
+        [2, 96, ['**', '^', 'pow'],  0, sub {bless(shift(), 'CGI::Ex::_pow')}    ],
+        [1, 93, ['!'],               0, sub {bless(shift(), 'CGI::Ex::_not')}    ],
+        [1, 93, ['-'],               0, sub {bless(shift(), 'CGI::Ex::_negate')} ],
+        [2, 90, ['*'],               0, sub {bless(shift(), 'CGI::Ex::_mult')}   ],
+        [2, 90, ['/'],               0, sub {bless(shift(), 'CGI::Ex::_div')}    ],
+        [2, 90, ['div', 'DIV'],      0, sub {bless(shift(), 'CGI::Ex::_intdiv')} ],
+        [2, 90, ['%', 'mod', 'MOD'], 0, sub {bless(shift(), 'CGI::Ex::_mod')}    ],
+        [2, 85, ['+'],               0, sub {bless(shift(), 'CGI::Ex::_plus')}   ],
+        [2, 85, ['-'],               0, sub {bless(shift(), 'CGI::Ex::_subtr')}  ],
+        [2, 85, ['_', '~'],          0, \&_concat                                ],
+        [2, 80, ['<'],               0, sub {bless(shift(), 'CGI::Ex::_num_lt')} ],
+        [2, 80, ['>'],               0, sub {bless(shift(), 'CGI::Ex::_num_gt')} ],
+        [2, 80, ['<='],              0, sub {bless(shift(), 'CGI::Ex::_num_le')} ],
+        [2, 80, ['>='],              0, sub {bless(shift(), 'CGI::Ex::_num_ge')} ],
+        [2, 80, ['lt'],              0, sub {bless(shift(), 'CGI::Ex::_str_lt')} ],
+        [2, 80, ['gt'],              0, sub {bless(shift(), 'CGI::Ex::_str_gt')} ],
+        [2, 80, ['le'],              0, sub {bless(shift(), 'CGI::Ex::_str_le')} ],
+        [2, 80, ['ge'],              0, sub {bless(shift(), 'CGI::Ex::_str_ge')} ],
+        [2, 75, ['==', 'eq'],        0, sub {bless(shift(), 'CGI::Ex::_eq')}     ],
+        [2, 75, ['!=', 'ne'],        0, sub {bless(shift(), 'CGI::Ex::_ne')}     ],
+        [2, 70, ['&&'],              0, sub {bless(shift(), 'CGI::Ex::_and')}    ],
+        [2, 65, ['||'],              0, sub {bless(shift(), 'CGI::Ex::_or')}     ],
+        [2, 60, ['..'],              0, sub {bless(shift(), 'CGI::Ex::_range')}  ],
+        [3, 55, ['?', ':'],          0, sub {bless(shift(), 'CGI::Ex::_ifelse')} ],
+        [2, 52, ['='],               1, sub {bless(shift(), 'CGI::Ex::_set')}    ],
+        [1, 50, ['not', 'NOT'],      0, sub {bless(shift(), 'CGI::Ex::_not')}    ],
+        [2, 45, ['and', 'AND'],      0, sub {bless(shift(), 'CGI::Ex::_and')}    ],
+        [2, 40, ['or', 'OR'],        0, sub {bless(shift(), 'CGI::Ex::_or')}     ],
+    ];
+
+    $OP_UNARY   ||= {map {my $ref = $_; map {$_ => $ref} @{$ref->[2]}} grep {$_->[0] == 1} @$OPERATORS};
+    $OP_BINARY  ||= {map {my $ref = $_; map {$_ => $ref} @{$ref->[2]}} grep {$_->[0] == 2} @$OPERATORS};
+    $OP_TRINARY ||= {map {my $ref = $_; map {$_ => $ref} @{$ref->[2]}} grep {$_->[0] == 3} @$OPERATORS};
+    sub _op_qr { # no mixed \w\W operators
+        my %used;
+        my $chrs = join '|', map {quotemeta $_} grep {++$used{$_} < 2} grep {/^\W{2,}$/} @_;
+        my $chr  = join '',  map {quotemeta $_} grep {++$used{$_} < 2} grep {/^\W$/}     @_;
+        my $word = join '|',                    grep {++$used{$_} < 2} grep {/^\w+$/}    @_;
+        $chr = "[$chr]" if $chr;
+        $word = "\\b(?:$word)\\b" if $word;
+        return join('|', grep {length} $chrs, $chr, $word) || die "Missing operator regex";
+    }
+    sub _build_op_qr       { _op_qr(sort map {@{ $_->[2] }} grep {$_->[0] > 1 && ! $_->[3]} @$OPERATORS) } # all binary, trinary, non-parened ops
+    sub _build_op_qr_unary { _op_qr(sort map {@{ $_->[2] }} grep {$_->[0] == 1            } @$OPERATORS) } # unary operators
+    sub _build_op_qr_paren { _op_qr(sort map {@{ $_->[2] }} grep {                 $_->[3]} @$OPERATORS) } # paren
+    $QR_OP         ||= _build_op_qr();
+    $QR_OP_UNARY   ||= _build_op_qr_unary();
+    $QR_OP_PARENED ||= _build_op_qr_paren();
+
+    $QR_COMMENTS   = '(?-s: \# .* \s*)*';
+    $QR_AQ_NOTDOT  = "(?! \\s* $QR_COMMENTS \\.)";
+    $QR_PRIVATE    = qr/^_/;
+};
+
+###----------------------------------------------------------------###
+
+sub _var     { return bless(shift(), __PACKAGE__        ) }
+sub _literal { return bless(shift(), 'CGI::Ex::_literal') }
+sub _hash    { return bless(shift(), 'CGI::Ex::_hash'   ) }
+sub _array   { return bless(shift(), 'CGI::Ex::_array'  ) }
+sub _concat  { return bless(shift(), 'CGI::Ex::_concat' ) }
+sub _autobox { return bless(shift(), 'CGI::Ex::_autobox') }
+sub _not     { return bless(shift(), 'CGI::Ex::_not'    ) }
+
+sub throw {
+    require CGI::Ex::Template;
+    CGI::Ex::Template->throw(@_);
+}
+
+###----------------------------------------------------------------###
+
+sub parse_exp {
+    my $str_ref = shift;
+    my $ARGS    = shift || {};
+
+    ### allow for custom auto_quoting (such as hash constructors)
+    if ($ARGS->{'auto_quote'}) {
+        if ($$str_ref =~ $ARGS->{'auto_quote'}) {
+            my $str = $1;
+            substr($$str_ref, 0, length($str), '');
+            $$str_ref =~ s{ ^ \s* $QR_COMMENTS }{}ox;
+            return $str;
+        ### allow for auto-quoted $foo or ${foo.bar} type constructs
+        } elsif ($$str_ref =~ s{ ^ \$ (\w+ (?:\.\w+)*) \b \s* $QR_COMMENTS }{}ox
+                 || $$str_ref =~ s{ ^ \$\{ \s* ([^\}]+) \} \s* $QR_COMMENTS }{}ox) {
+            my $name = $1;
+            return parse_exp(\$name);
+        }
+    }
+
+    my $copy = $$str_ref; # copy while parsing to allow for errors
+
+    ### test for leading unary operators
+    my $has_unary;
+    if ($copy =~ s{ ^ ($QR_OP_UNARY) \s* $QR_COMMENTS }{}ox) {
+        return if $ARGS->{'auto_quote'}; # auto_quoted thing was too complicated
+        $has_unary = $1;
+    }
+
+    my @var;
+    my $is_literal;
+    my $is_construct;
+    my $is_namespace;
+
+    ### allow for numbers
+    if ($copy =~ s{ ^ ( (?:\d*\.\d+ | \d+) ) \s* $QR_COMMENTS }{}ox) {
+        my $number = $1;
+        push @var, _literal(\ $number);
+        $is_literal = 1;
+
+    ### looks like a normal variable start
+    } elsif ($copy =~ s{ ^ (\w+) \s* $QR_COMMENTS }{}ox) {
+        push @var, $1;
+        $is_namespace = 1 if $RT_NAMESPACE->{$1};
+
+    ### allow for literal strings
+    } elsif ($copy =~ s{ ^ ([\"\']) (|.*?[^\\]) \1 \s* $QR_COMMENTS }{}sox) {
+        if ($1 eq "'") { # no interpolation on single quoted strings
+            my $str = $2;
+            $str =~ s{ \\\' }{\'}xg;
+            push @var, _literal(\ $str);
+            $is_literal = 1;
+        } else {
+            my $str = $2;
+            $str =~ s/\\n/\n/g;
+            $str =~ s/\\t/\t/g;
+            $str =~ s/\\r/\r/g;
+            $str =~ s/\\([\"\$])/$1/g;
+            my @pieces = $ARGS->{'auto_quote'}
+                ? split(m{ (\$\w+            | \$\{ [^\}]+ \}) }x, $str)  # autoquoted items get a single $\w+ - no nesting
+                : split(m{ (\$\w+ (?:\.\w+)* | \$\{ [^\}]+ \}) }x, $str);
+            my $n = 0;
+            foreach my $piece (@pieces) {
+                next if ! ($n++ % 2);
+                next if $piece !~ m{ ^ \$ (\w+ (?:\.\w+)*) $ }x
+                    && $piece !~ m{ ^ \$\{ \s* ([^\}]+) \} $ }x;
+                my $name = $1;
+                $piece = parse_exp(\$name);
+            }
+            @pieces = grep {defined && length} @pieces;
+            if (@pieces == 1 && ! ref $pieces[0]) {
+                push @var, _literal(\ $pieces[0]);
+                $is_literal = 1;
+            } elsif (! @pieces) {
+                my $str = '';
+                push @var, _literal(\ $str);
+                $is_literal = 1;
+            } else {
+                push @var, _concat(\@pieces);
+                $is_construct = 1;
+            }
+        }
+        if ($ARGS->{'auto_quote'}){
+            $$str_ref = $copy;
+            return ${ $var[0] } if $is_literal;
+            return _var([@var, 0]);
+        }
+
+    ### allow for leading $foo or ${foo.bar} type constructs
+    } elsif ($copy =~ s{ ^ \$ (\w+) \b \s* $QR_COMMENTS }{}ox
+        || $copy =~ s{ ^ \$\{ \s* ([^\}]+) \} \s* $QR_COMMENTS }{}ox) {
+        my $name = $1;
+        push @var, parse_exp(\$name);
+
+    ### looks like an array constructor
+    } elsif ($copy =~ s{ ^ \[ \s* $QR_COMMENTS }{}ox) {
+        local $RT_OPERATOR_PRECEDENCE = 0; # reset presedence
+        my $arrayref = [];
+        while (defined(my $var = parse_exp(\$copy))) {
+            push @$arrayref, $var;
+            $copy =~ s{ ^ , \s* $QR_COMMENTS }{}ox;
+        }
+        $copy =~ s{ ^ \] \s* $QR_COMMENTS }{}ox
+            || throw('parse.missing.square', "Missing close \]", undef, length($$str_ref) - length($copy));
+        push @var, _array($arrayref);
+        $is_construct = 1;
+
+    ### looks like a hash constructor
+    } elsif ($copy =~ s{ ^ \{ \s* $QR_COMMENTS }{}ox) {
+        local $RT_OPERATOR_PRECEDENCE = 0; # reset precedence
+        my $hashref = [];
+        while (defined(my $key = parse_exp(\$copy, {auto_quote => qr{ ^ (\w+) $QR_AQ_NOTDOT }xo}))) {
+            $copy =~ s{ ^ = >? \s* $QR_COMMENTS }{}ox;
+            my $val = parse_exp(\$copy);
+            push @$hashref, $key, $val;
+            $copy =~ s{ ^ , \s* $QR_COMMENTS }{}ox;
+        }
+        $copy =~ s{ ^ \} \s* $QR_COMMENTS }{}ox
+            || throw('parse.missing.curly', "Missing close \} ($copy)", undef, length($$str_ref) - length($copy));
+        push @var, _hash($hashref);
+        $is_construct = 1;
+
+    ### looks like a paren grouper
+    } elsif ($copy =~ s{ ^ \( \s* $QR_COMMENTS }{}ox) {
+        local $RT_OPERATOR_PRECEDENCE = 0; # reset precedence
+        my $var = parse_exp(\$copy, {allow_parened_ops => 1});
+        $copy =~ s{ ^ \) \s* $QR_COMMENTS }{}ox
+            || throw('parse.missing.paren', "Missing close \)", undef, length($$str_ref) - length($copy));
+        push @var, $var;
+        $is_construct = 1;
+
+    ### nothing to find - return failure
+    } else {
+        return;
+    }
+
+    return if $ARGS->{'auto_quote'}; # auto_quoted thing was too complicated
+
+    ### looks for args for the initial
+    if ($copy =~ s{ ^ \( \s* $QR_COMMENTS }{}ox) {
+        local $RT_OPERATOR_PRECEDENCE = 0; # reset precedence
+        my $args = parse_args(\$copy);
+        $copy =~ s{ ^ \) \s* $QR_COMMENTS }{}ox
+            || throw('parse.missing.paren', "Missing close \)", undef, length($$str_ref) - length($copy));
+        push @var, $args;
+    } else {
+        push @var, 0;
+    }
+
+    ### allow for nested items
+    while ($copy =~ s{ ^ ( \.(?!\.) | \|(?!\|) ) \s* $QR_COMMENTS }{}ox) {
+        push(@var, $1) if ! $ARGS->{'no_dots'};
+
+        ### allow for interpolated variables in the middle - one.$foo.two or one.${foo.bar}.two
+        if ($copy =~ s{ ^ \$(\w+) \s* $QR_COMMENTS }{}ox
+            || $copy =~ s{ ^ \$\{ \s* ([^\}]+)\} \s* $QR_COMMENTS }{}ox) {
+            my $name = $1;
+            my $var = parse_exp(\$name);
+            push @var, $var;
+        } elsif ($copy =~ s{ ^ (\w+) \s* $QR_COMMENTS }{}ox) {
+            push @var, $1;
+        } else {
+            throw('parse', "Not sure how to continue parsing on \"$copy\" ($$str_ref)");
+        }
+
+        ### looks for args for the nested item
+        if ($copy =~ s{ ^ \( \s* $QR_COMMENTS }{}ox) {
+            local $RT_OPERATOR_PRECEDENCE = 0; # reset precedence
+            my $args = parse_args(\$copy);
+            $copy =~ s{ ^ \) \s* $QR_COMMENTS }{}ox
+                || throw('parse.missing.paren', "Missing close \)", undef, length($$str_ref) - length($copy));
+            push @var, $args;
+        } else {
+            push @var, 0;
+        }
+
+    }
+
+    ### flatten literals and constants as much as possible
+    my $var;
+    if (@var == 2) {
+        if ($is_literal) {
+            $var = ${ $var[0] };
+        } elsif ($is_construct) {
+            $var = $var[0];
+        } else {
+            $var = _var(\@var);
+        }
+    } else {
+        if ($is_construct && ! $var[0]->does_autobox) {
+            $var[0] = _autobox([$var[0]]);
+        }
+
+        if ($is_namespace) { # attempt to "fold" constant variables into the parse tree
+            local $RT_DURING_COMPILE = 1;
+            $var = _var(\@var)->call({});
+        } else {
+            $var = _var(\@var);
+        }
+    }
+
+    ### allow for all "operators"
+    if (! $RT_OPERATOR_PRECEDENCE) {
+        my $tree;
+        my $found;
+        while ($copy =~ s{ ^ ($QR_OP) \s* $QR_COMMENTS }{}ox ## look for operators - then move along
+               || ($ARGS->{'allow_parened_ops'}
+                   && $copy =~ s{ ^ ($QR_OP_PARENED) \s* $QR_COMMENTS }{}ox) ) {
+            local $RT_OPERATOR_PRECEDENCE = 1;
+            my $op   = $1;
+            my $var2 = parse_exp(\$copy);
+
+            ### allow for unary operator precedence
+            if ($has_unary && (($OP_BINARY->{$op} || $OP_TRINARY->{$op})->[1] < $OP_UNARY->{$has_unary}->[1])) {
+                if ($tree) {
+                    if (@$tree == 2) { # only one operator - keep simple things fast
+                        $var = $OP_BINARY->{$tree->[0]}->[4]->([$var, $tree->[1]]);
+                    } else {
+                        unshift @$tree, $var;
+                        $var = apply_precedence($tree, $found);
+                    }
+                    undef $tree;
+                    undef $found;
+                }
+                $var = $OP_UNARY->{$has_unary}->[4]->([$var]);
+                undef $has_unary;
+            }
+
+            ### add the operator to the tree
+            push (@{ $tree ||= [] }, $op, $var2);
+            my $ref = $OP_BINARY->{$op} || $OP_TRINARY->{$op};
+            $found->{$op} = $ref->[1];
+        }
+
+        ### if we found operators - tree the nodes by operator precedence
+        if ($tree) {
+            if (@$tree == 2 && $OP_BINARY->{$tree->[0]}) { # only one operator - keep simple things fast
+                $var = $OP_BINARY->{$tree->[0]}->[4]->([$var, $tree->[1]]);
+            } else {
+                unshift @$tree, $var;
+                $var = apply_precedence($tree, $found);
+            }
+        }
+    }
+
+    ### allow for unary on non-chained variables
+    if ($has_unary) {
+        $var = $OP_UNARY->{$has_unary}->[4]->([$var]);
+    }
+
+    $$str_ref = $copy; # commit the changes
+    return $var;
+}
+
+### this is used to put the parsed variables into the correct operations tree
+sub apply_precedence {
+    my ($tree, $found) = @_;
+
+    my @var;
+    my $trees;
+    ### look at the operators we found in the order we found them
+    for my $op (sort {$found->{$a} <=> $found->{$b}} keys %$found) {
+        local $found->{$op};
+        delete $found->{$op};
+        my @trees;
+        my @trinary;
+
+        ### split the array on the current operator
+        for (my $i = 0; $i <= $#$tree; $i ++) {
+            my $is_trinary = $OP_TRINARY->{$op} && grep {$_ eq $tree->[$i]} @{ $OP_TRINARY->{$op}->[2] };
+            next if $tree->[$i] ne $op && ! $is_trinary;
+            push @trees, [splice @$tree, 0, $i, ()]; # everything up to the operator
+            push @trinary, $tree->[0] if $is_trinary;
+            shift @$tree; # pull off the operator
+            $i = -1;
+        }
+        next if ! @trees; # this iteration didn't have the current operator
+        push @trees, $tree if scalar @$tree; # elements after last operator
+
+        ### now - for this level split on remaining operators, or add the variable to the tree
+        for my $node (@trees) {
+            if (@$node == 1) {
+                $node = $node->[0]; # single item - its not a tree
+            } elsif (@$node == 3) {
+                my $ref = $OP_BINARY->{$node->[1]} || $OP_TRINARY->{$node->[1]};
+                $node = $ref->[4]->([$node->[0], $node->[2]]); # single operator - put it straight on
+            } else {
+                $node = apply_precedence($node, $found); # more complicated - recurse
+            }
+        }
+
+        ### return binary
+        if ($OP_BINARY->{$op}) {
+            my $val = $trees[0];
+            $val = $OP_BINARY->{$op}->[4]->([$val, $trees[$_]]) for 1 .. $#trees;
+            return $val;
+        }
+
+        ### return simple trinary
+        if (@trinary == 2) {
+            return $OP_TRINARY->{$op}->[4]->(\@trees);
+        }
+
+        ### reorder complex trinary - rare case
+        while ($#trinary >= 1) {
+            ### if we look starting from the back - the first lead trinary op will always be next to its matching op
+            for (my $i = $#trinary; $i >= 0; $i --) {
+                next if $OP_TRINARY->{$trinary[$i]}->[2]->[1] eq $trinary[$i];
+                my ($op, $op2) = splice @trinary, $i, 2, (); # remove the found pair of operators
+                my $node = $OP_TRINARY->{$op}->[4]->([@trees[$i .. $i + 2]]);
+                splice @trees, $i, 3, $node; # replace the previous 3 pieces with the one new node
+            }
+        }
+        return $trees[0]; # at this point the trinary has been reduced to a single operator
+
+    }
+
+    throw('parse', "Couldn't apply precedence");
+}
+
+### look for arguments - both positional and named
+sub parse_args {
+    my $str_ref = shift;
+    my $ARGS    = shift || {};
+    my $copy    = $$str_ref;
+
+    my @args;
+    my @named;
+    while (length $$str_ref) {
+        my $copy = $$str_ref;
+        if (defined(my $name = parse_exp(\$copy, {auto_quote => qr{ ^ (\w+) $QR_AQ_NOTDOT }xo}))
+            && $copy =~ s{ ^ = >? \s* $QR_COMMENTS }{}ox) {
+            throw('parse', 'Named arguments not allowed') if $ARGS->{'positional_only'};
+            my $val = parse_exp(\$copy);
+            $copy =~ s{ ^ , \s* $QR_COMMENTS }{}ox;
+            push @named, $name, $val;
+            $$str_ref = $copy;
+        } elsif (defined(my $arg = parse_exp($str_ref))) {
+            push @args, $arg;
+            $$str_ref =~ s{ ^ , \s* $QR_COMMENTS }{}ox;
+        } else {
+            last;
+        }
+    }
+
+    ### allow for named arguments to be added also
+    push @args, _hash(\@named) if scalar @named;
+
+    return \@args;
+}
+
+sub get_exp { ref($_[0]) ? $_[0]->call($_[1]) : $_[0] }
+
+sub set_exp {
+    my $var = shift;
+    $var = _var([$var, 0]) if ! ref $var; # allow for the parse tree to store literals - the literal is used as a name (like [% 'a' = 'A' %])
+    return $var->set($_[0], $_[1]);
+}
+
+
+sub dump_parse {
+    my $str = shift;
+    require Data::Dumper;
+    return Data::Dumper::Dumper(parse_exp(\$str));
+}
+
+sub dump_get {
+    my ($str, $hash) = @_;
+    require Data::Dumper;
+    return Data::Dumper::Dumper(get_exp(parse_exp(\$str), $hash));
+}
+
+sub dump_set {
+    my ($str, $val, $hash) = @_;
+    $hash ||= {};
+    require Data::Dumper;
+    set_exp(parse_exp(\$str), $val, $hash);
+    return Data::Dumper::Dumper($hash);
+}
+
+sub vivify_args {
+    my $vars = shift;
+    my $hash = shift;
+    return [map {get_exp($_, $hash)} @$vars];
+}
+
+###----------------------------------------------------------------###
+
+sub new {
+    my $class = shift;
+    return bless $_[0], $class;
+}
+
+sub does_autobox { 0 }
+
+sub call {
+    my $self = shift;
+    my $hash = shift || {};
+    my $i    = 0;
+
+    ### determine the top level of this particular variable access
+    my $ref  = $self->[$i++];
+    my $args = $self->[$i++];
+    warn "CGI::Ex::Var::call: begin \"$ref\"\n" if trace;
+
+    if (ref $ref) {
+        if ($ref->does_autobox) {
+            $ref = $ref->call($hash);
+        } else {
+            $ref = $ref->call($hash);
+            return if $ref =~ $QR_PRIVATE; # don't allow vars that begin with _
+            $ref = $hash->{$ref};
+        }
+    } else {
+        if ($RT_DURING_COMPILE) {
+            $ref = $RT_NAMESPACE->{$ref};
+        } else {
+            return if $ref =~ $QR_PRIVATE; # don't allow vars that begin with _
+            $ref = $hash->{$ref};
+        }
+    }
+
+    my %seen_filters;
+    while (defined $ref) {
+
+        ### check at each point if the returned thing was a code
+        if (UNIVERSAL::isa($ref, 'CODE')) {
+            my @results = $ref->($args ? (map {get_exp($_, $hash)} @$args) : ());
+            if (defined $results[0]) {
+                $ref = ($#results > 0) ? \@results : $results[0];
+            } elsif (defined $results[1]) {
+                die $results[1]; # TT behavior - why not just throw ?
+            } else {
+                $ref = undef;
+                last;
+            }
+        }
+
+        ### descend one chained level
+        last if $i >= $#$self;
+        my $was_dot_call = $self->[$i++] eq '.';
+        my $name         = $self->[$i++];
+        my $args         = $self->[$i++];
+        warn "CGI::Ex::Var::get_exp: nested \"$name\"\n" if trace;
+
+        ### allow for named portions of a variable name (foo.$name.bar)
+        if (ref $name) {
+            $name = $name->call($hash);
+            if (! defined $name) {
+                $ref = undef;
+                last;
+            }
+        }
+
+        if ($name =~ $QR_PRIVATE) { # don't allow vars that begin with _
+            $ref = undef;
+            last;
+        }
+
+        ### allow for scalar and filter access (this happens for every non virtual method call)
+        if (! ref $ref) {
+            if ($SCALAR_OPS->{$name}) {                        # normal scalar op
+                $ref = $SCALAR_OPS->{$name}->($ref, $args ? (map {get_exp($_, $hash)} @$args) : ());
+
+            } elsif ($LIST_OPS->{$name}) {                     # auto-promote to list and use list op
+                $ref = $LIST_OPS->{$name}->([$ref], $args ? (map {get_exp($_, $hash)} @$args) : ());
+
+            } elsif (my $filter = $RT_FILTERS->{$name}           # filter configured in Template args
+                     || $FILTER_OPS->{$name}                     # predefined filters in CET
+                     || (UNIVERSAL::isa($name, 'CODE') && $name) # looks like a filter sub passed in the stash
+                     || list_filters()->{$name}) {               # filter defined in Template::Filters
+
+                if (UNIVERSAL::isa($filter, 'CODE')) {
+                    $ref = eval { $filter->($ref) }; # non-dynamic filter - no args
+                    if (my $err = $@) {
+                        throw('filter', $err) if ref($err) !~ /Template::Exception$/;
+                        die $err;
+                    }
+                } elsif (! UNIVERSAL::isa($filter, 'ARRAY')) {
+                    throw('filter', "invalid FILTER entry for '$name' (not a CODE ref)");
+
+                } elsif (@$filter == 2 && UNIVERSAL::isa($filter->[0], 'CODE')) { # these are the TT style filters
+                    eval {
+                        my $sub = $filter->[0];
+                        if ($filter->[1]) { # it is a "dynamic filter" that will return a sub
+                            ($sub, my $err) = $sub->($RT_CONTEXT_SUB->(), $args ? (map {get_exp($_, $hash)} @$args) : ());
+                            if (! $sub && $err) {
+                                throw('filter', $err) if ref($err) !~ /Template::Exception$/;
+                                die $err;
+                            } elsif (! UNIVERSAL::isa($sub, 'CODE')) {
+                                throw('filter', "invalid FILTER for '$name' (not a CODE ref)")
+                                    if ref($sub) !~ /Template::Exception$/;
+                                die $sub;
+                            }
+                        }
+                        $ref = $sub->($ref);
+                    };
+                    if (my $err = $@) {
+                        throw('filter', $err) if ref($err) !~ /Template::Exception$/;
+                        die $err;
+                    }
+                } else { # this looks like our vmethods turned into "filters" (a filter stored under a name)
+                    throw('filter', 'Recursive filter alias \"$name\"') if $seen_filters{$name} ++;
+                    $self = [$name, 0, '|', @$filter, @{$self}[$i..$#$self]]; # splice the filter into our current tree
+                    $i = 2;
+                }
+                if (scalar keys %seen_filters
+                    && $seen_filters{$self->[$i - 5] || ''}) {
+                    throw('filter', "invalid FILTER entry for '".$self->[$i - 5]."' (not a CODE ref)");
+                }
+            } else {
+                $ref = undef;
+            }
+
+        } else {
+
+            ### method calls on objects
+            if (UNIVERSAL::can($ref, 'can')) {
+                my @args = $args ? (map {get_exp($_, $hash)} @$args) : ();
+                my @results = eval { $ref->$name(@args) };
+                if ($@) {
+                    die $@ if ref $@ || $@ !~ /Can\'t locate object method/;
+                } elsif (defined $results[0]) {
+                    $ref = ($#results > 0) ? \@results : $results[0];
+                    next;
+                } elsif (defined $results[1]) {
+                    die $results[1]; # TT behavior - why not just throw ?
+                } else {
+                    $ref = undef;
+                    last;
+                }
+                # didn't find a method by that name - so fail down to hash and array access
+            }
+
+            ### hash member access
+            if (UNIVERSAL::isa($ref, 'HASH')) {
+                if ($was_dot_call && exists($ref->{$name}) ) {
+                    $ref = $ref->{$name};
+                } elsif ($HASH_OPS->{$name}) {
+                    $ref = $HASH_OPS->{$name}->($ref, $args ? (map {get_exp($_, $hash)} @$args) : ());
+                } elsif ($RT_DURING_COMPILE) {
+                    return $self; # abort - can't fold namespace variable
+                } else {
+                    $ref = undef;
+                }
+
+            ### array access
+            } elsif (UNIVERSAL::isa($ref, 'ARRAY')) {
+                if ($name =~ /^\d+$/) {
+                    $ref = ($name > $#$ref) ? undef : $ref->[$name];
+                } else {
+                    $ref = (! $LIST_OPS->{$name}) ? undef : $LIST_OPS->{$name}->($ref, $args ? (map {get_exp($_, $hash)} @$args) : ());
+                }
+            }
+        }
+
+    } # end of while
+
+    ### allow for undefinedness
+    if (! defined $ref) {
+        if ($RT_DEBUG_UNDEF) {
+            my $chunk = $self->[$i - 2];
+            $chunk = $chunk->call($hash) if ref $chunk;
+            die "$chunk is undefined\n";
+        } else {
+            $ref = $self->undefined_any($self);
+        }
+    }
+
+    return $ref;
+}
+
+sub undefined_any { $RT_UNDEFINED_SUB ? $RT_UNDEFINED_SUB->(@_) : undef }
+
+sub set {
+    my ($self, $val, $hash) = @_;
+    my $i = 0;
+
+    ### determine the top level of this particular variable access
+    my $ref  = $self->[$i++];
+    my $args = $self->[$i++];
+
+    if (ref $ref) {
+        $ref = $ref->call($hash);
+        return if ! defined $ref;
+    }
+
+    return if $ref =~ $QR_PRIVATE; # don't allow vars that begin with _
+
+    if ($#$self <= $i) {
+        $hash->{$ref} = $val;
+        return;
+    } else {
+        $ref = $hash->{$ref} ||= {};
+    }
+
+    ### let the top level thing be a code block
+    return if UNIVERSAL::isa($ref, 'CODE');
+
+    ### vivify the chained levels
+    while (defined $ref && $#$self > $i) {
+        my $was_dot_call = $self->[$i++] eq '.';
+        my $name         = $self->[$i++];
+        my $args         = $self->[$i++];
+
+        ### allow for named portions of a variable name (foo.$name.bar)
+        if (ref $name) {
+            $name = $name->call($hash);
+            if (! defined $name) {
+                $ref = undef;
+                last;
+            }
+        }
+
+        if ($name =~ $QR_PRIVATE) { # don't allow vars that begin with _
+            return;
+        }
+
+        ### method calls on objects
+        if (UNIVERSAL::can($ref, 'can')) {
+            my $lvalueish;
+            my @args = $args ? (map {get_exp($_, $hash)} @$args) : ();
+            if ($i >= $#$self) {
+                $lvalueish = 1;
+                push @args, $val;
+            }
+            my @results = eval { $ref->$name(@args) };
+            if ($@) {
+                die $@ if ref $@ || $@ !~ /Can\'t locate object method/;
+            } elsif (defined $results[0]) {
+                $ref = ($#results > 0) ? \@results : $results[0];
+            } elsif (defined $results[1]) {
+                die $results[1]; # TT behavior - why not just throw ?
+            } else {
+                $ref = undef;
+            }
+            return if $lvalueish;
+            next;
+        }
+
+        ### hash member access
+        if (UNIVERSAL::isa($ref, 'HASH')) {
+            if ($#$self <= $i) {
+                $ref->{$name} = $val;
+                return;
+            } else {
+                $ref = $ref->{$name} ||= {};
+                next;
+            }
+
+        ### array access
+        } elsif (UNIVERSAL::isa($ref, 'ARRAY')) {
+            if ($name =~ /^\d+$/) {
+                if ($#$self <= $i) {
+                    $ref->[$name] = $val;
+                    return;
+                } else {
+                    $ref = $ref->[$name] ||= {};
+                    next;
+                }
+            } else {
+                return;
+            }
+
+        ### scalar access
+        } elsif (! ref($ref) && defined($ref)) {
+            return;
+        }
+
+        ### check at each point if the returned thing was a code
+        if (defined($ref) && UNIVERSAL::isa($ref, 'CODE')) {
+            my @results = $ref->($args ? (map {get_exp($_, $hash)} @$args) : ());
+            if (defined $results[0]) {
+                $ref = ($#results > 0) ? \@results : $results[0];
+            } elsif (defined $results[1]) {
+                die $results[1]; # TT behavior - why not just throw ?
+            } else {
+                return;
+            }
+        }
+
+    }
+
+    return $ref;
+}
+
+###----------------------------------------------------------------###
+### filters and vmethod definition
+
+sub list_filters {
+    return $TT_FILTERS ||= eval { require Template::Filters; $Template::Filters::FILTERS } || {};
+}
+
+sub vmethod_chunk {
+    my $str  = shift;
+    my $size = shift || 1;
+    my @list;
+    if ($size < 0) { # chunk from the opposite end
+        $str = reverse $str;
+        $size = -$size;
+        unshift(@list, scalar reverse $1) while $str =~ /( .{$size} | .+ )/xg;
+    } else {
+        push(@list, $1)                   while $str =~ /( .{$size} | .+ )/xg;
+    }
+    return \@list;
+}
+
+sub vmethod_indent {
+    my $str = shift; $str = '' if ! defined $str;
+    my $pre = shift; $pre = 4  if ! defined $pre;
+    $pre = ' ' x $pre if $pre =~ /^\d+$/;
+    $str =~ s/^/$pre/mg;
+    return $str;
+}
+
+sub vmethod_format {
+    my $str = shift; $str = ''   if ! defined $str;
+    my $pat = shift; $pat = '%s' if ! defined $pat;
+    return join "\n", map{ sprintf $pat, $_ } split(/\n/, $str);
+}
+
+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] : '';
+}
+
+sub vmethod_nsort {
+    my ($list, $field) = @_;
+    return defined($field)
+        ? [map {$_->[0]} sort {$a->[1] <=> $b->[1]} map {[$_, (ref $_ eq 'HASH' ? $_->{$field}
+                                                               : UNIVERSAL::can($_, $field) ? $_->$field()
+                                                               : $_)]} @$list ]
+        : [sort {$a <=> $b} @$list];
+}
+
+sub vmethod_repeat {
+    my ($str, $n, $join) = @_;
+    return if ! length $str;
+    $n = 1 if ! defined($n) || ! length $n;
+    $join = '' if ! defined $join;
+    return join $join, ($str) x $n;
+}
+
+### This method is a combination of my submissions along
+### with work from Andy Wardley, Sergey Martynoff, Nik Clayton, and Josh Rosenbaum
+sub vmethod_replace {
+    my ($text, $pattern, $replace, $global) = @_;
+    $text      = '' unless defined $text;
+    $pattern   = '' unless defined $pattern;
+    $replace   = '' unless defined $replace;
+    $global    = 1  unless defined $global;
+    my $expand = sub {
+        my ($chunk, $start, $end) = @_;
+        $chunk =~ s{ \\(\\|\$) | \$ (\d+) }{
+            $1 ? $1
+                : ($2 > $#$start || $2 == 0) ? ''
+                : substr($text, $start->[$2], $end->[$2] - $start->[$2]);
+        }exg;
+        $chunk;
+    };
+    if ($global) {
+        $text =~ s{$pattern}{ $expand->($replace, [@-], [@+]) }eg;
+    } else {
+        $text =~ s{$pattern}{ $expand->($replace, [@-], [@+]) }e;
+    }
+    return $text;
+}
+
+sub vmethod_sort {
+    my ($list, $field) = @_;
+    return defined($field)
+        ? [map {$_->[0]} sort {$a->[1] cmp $b->[1]} map {[$_, lc(ref $_ eq 'HASH' ? $_->{$field}
+                                                                 : UNIVERSAL::can($_, $field) ? $_->$field()
+                                                                 : $_)]} @$list ]
+        : [map {$_->[0]} sort {$a->[1] cmp $b->[1]} map {[$_, lc $_]} @$list ]; # case insensitive
+}
+
+sub vmethod_splice {
+    my ($ref, $i, $len, @replace) = @_;
+    @replace = @{ $replace[0] } if @replace == 1 && ref $replace[0] eq 'ARRAY';
+    if (defined $len) {
+        return [splice @$ref, $i || 0, $len, @replace];
+    } else {
+        return [splice @$ref, $i || 0];
+    }
+}
+
+sub vmethod_split {
+    my ($str, $pat, @args) = @_;
+    $str = '' if ! defined $str;
+    return defined $pat ? [split $pat, $str, @args] : [split ' ', $str, @args];
+}
+
+sub filter_eval {
+    my $context = shift;
+    return sub {
+        my $text = shift;
+        return $context->process(\$text);
+    };
+}
+
+sub filter_redirect {
+    my ($context, $file, $options) = @_;
+    my $path = $context->config->{'OUTPUT_PATH'} || $context->throw('redirect', 'OUTPUT_PATH is not set');
+
+    return sub {
+        my $text = shift;
+        if (! -d $path) {
+            require File::Path;
+            File::Path::mkpath($path) || $context->throw('redirect', "Couldn't mkpath \"$path\": $!");
+        }
+        local *FH;
+        open (FH, ">$path/$file") || $context->throw('redirect', "Couldn't open \"$file\": $!");
+        if (my $bm = (! $options) ? 0 : ref($options) ? $options->{'binmode'} : $options) {
+            if (+$bm == 1) { binmode FH }
+            else { binmode FH, $bm}
+        }
+        print FH $text;
+        close FH;
+        return '';
+    };
+}
+
+###----------------------------------------------------------------###
+### "here be dragons"
+
+package CGI::Ex::_literal;
+sub call { ${ $_[0] } }
+sub set {}
+sub does_autobox { 1 }
+
+package CGI::Ex::_autobox;
+sub call { $_[0]->[0]->call($_[1]) }
+sub set {}
+sub does_autobox { 1 }
+
+package CGI::Ex::_concat;
+sub call { join "", grep {defined} map {ref($_) ? $_->call($_[1]) : $_} @{ $_[0] } }
+sub set {}
+sub does_autobox { 1 }
+
+package CGI::Ex::_hash;
+sub call { return {map {ref($_) ? $_->call($_[1]) : $_} @{ $_[0] }} }
+sub set {}
+sub does_autobox { 1 }
+
+package CGI::Ex::_array;
+sub call { return [map {ref($_) ? $_->call($_[1]) : $_} @{ $_[0] }] }
+sub set {}
+sub does_autobox { 1 }
+
+package CGI::Ex::_set;
+sub call {
+    my ($var, $val) = @{ $_[0] };
+    $val = CGI::Ex::Var::get_exp($val, $_[1]);
+    CGI::Ex::Var::set_exp($var, $val, $_[1]);
+    return $val;
+}
+sub set {}
+sub does_autobox { 1 }
+
+
+package CGI::Ex::_not;
+sub call { ! (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0]) || '' }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_and;
+sub call { (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  &&  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_or;
+sub call { ((ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  ||  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1])) || '' }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_ifelse;
+sub call {
+    (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])
+        ? (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1])
+        : (ref($_[0]->[2]) ? $_[0]->[2]->call($_[1]) : $_[0]->[2]);
+}
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_str_lt;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  lt  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_str_gt;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  gt  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_str_le;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  le  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_str_ge;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  ge  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_eq;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  eq  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_ne;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  ne  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_negate;
+sub call { local $^W; 0 - (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_pow;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  **  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_mult;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  *   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_div;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  /   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_intdiv;
+sub call { local $^W; int( (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  /   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) ) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_mod;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  %   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_plus;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  +   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_subtr;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  -   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_num_lt;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  <   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_num_gt;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  >   (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_num_le;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  <=  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_num_ge;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0])  >=  (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) }
+sub set {}
+sub does_autobox { 0 }
+
+package CGI::Ex::_range;
+sub call { local $^W; (ref($_[0]->[0]) ? $_[0]->[0]->call($_[1]) : $_[0]->[0]) || 0 .. (ref($_[0]->[1]) ? $_[0]->[1]->call($_[1]) : $_[0]->[1]) || 0 }
+sub set {}
+sub does_autobox { 0 }
+
+###----------------------------------------------------------------###
+
+=head1 DESCRIPTION
+
+Experimental.  An attempt for abstracting out a fast parser and hash
+from CGI::Ex::Template.  It is functional - but currently too
+cumbersome for use in CET.
+
+=cut
+
+1;
This page took 0.049032 seconds and 4 git commands to generate.