Re: extending <% | %>

Hans Dieter Pearcey <[email protected]> Wed, 16 Jan 2008 11:16:02 -0500
Newsgroups gmane.comp.web.mason.devel
Message-ID <[email protected]>
On Wed, Jan 16, 2008 at 11:12:04AM -0500, Hans Dieter Pearcey wrote:
> Attached is a patch that makes | in substitutions more powerful.

And, of course, it isn't.  Oops.

hdp.

-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/

_______________________________________________
Mason-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/mason-devel
filter.patch (text/plain, 19.4 KB)
diff --git a/lib/HTML/Mason/Compiler.pm b/lib/HTML/Mason/Compiler.pm
index f2c598e..38e6839 100644
--- a/lib/HTML/Mason/Compiler.pm
+++ b/lib/HTML/Mason/Compiler.pm
@@ -1,3 +1,6 @@
+# -*- cperl-indent-level: 4; cperl-continued-brace-offset: -4; cperl-continued-statement-offset: 4 -*-
+# vim: tabstop=4 shiftwidth=4 softtabstop=4:
+
 # Copyright (c) 1998-2005 by Jonathan Swartz. All rights reserved.
 # This program is free software; you can redistribute it and/or modify it
 # under the same terms as Perl itself.
@@ -7,6 +10,7 @@ package HTML::Mason::Compiler;
 use strict;
 use warnings;
 use Data::Dumper;
+use Scalar::Util ();
 use HTML::Mason::Component::FileBased;
 use HTML::Mason::Component::Subcomponent;
 use HTML::Mason::Exceptions( abbr => [qw(param_error compiler_error syntax_error)] );
@@ -439,6 +443,7 @@ sub end_named_block
     $self->{current_compile}{in_main}++;
 }
 
+my $code_num = 0;
 sub substitution
 {
     my $self = shift;
@@ -460,39 +465,90 @@ sub substitution
         return;
     }
 
-    if ( ( exists $p{escape} && defined $p{escape} ) ||
-         @{ $self->{default_escape_flags} }
-       )
+    if ( ( exists $p{filters} && defined $p{filters} ) ||
+        @{ $self->{default_escape_flags} } ||
+        @{ $self->{default_filters} }
+        )
     {
+        my @filters = (
+            defined $p{filters} ? @{ $p{filters} } : (),
+        );
+
         my @flags;
-        if ( defined $p{escape} )
+        # the lexer creates a one-element array for escapes; do we want to
+        # depend on that knowledge?  I'm iffy.
+        if ( @filters and @{ $filters[-1] } == 1 )
         {
-            $p{escape} =~ s/\s+$//;
-
-            if ( $p{escape} =~ /$old_escape_re/ )
+            my $flags = (pop @filters)->[0];
+            if ( $flags =~ /$old_escape_re/ )
             {
-                @flags = split //, $p{escape};
+                @flags = split //, $flags;
             }
             else
             {
-                @flags = split /\s*,\s*/, $p{escape};
+                @flags = split /\s*,\s*/, $flags;
             }
         }
 
         # is there any way to check the flags for validity and still
         # allow them to be dynamically set from components?
+        if ( ! grep { $_ eq 'n' } @flags )
+        {
+            push @filters,
+                @{ $self->{default_filters}      || [] };
 
-        unshift @flags, @{ $self->default_escape_flags }
-            unless grep { $_ eq 'n' } @flags;
+            push @flags,
+                @{ $self->{default_escape_flags} || [] };
+        }
+        else
+        {
+            @flags = grep { $_ ne 'n' } @flags;
+        }
+            
+        {
+            my %seen;
+            @flags = grep { ! $seen{$_}++ } @flags;
+        }
+        
+        # @flags had gotten popped off earlier -- it's easier to have one kind
+        # of data structure turning into code, rather than two
+        #push @filters, map { [ $_ ] } @flags;
+
+        #$text = "(join '', grep { defined } ($text))";
+        #$text = "(join '', ($text))";
+        if ( @filters and $filters[0][0] eq 'perl')
+        {
+            # special case -- not a real filter
+            $text = "do { my \@_args = \@_; HTML::Mason::Filters::Result->new(sub { sub { ($text) }->(\@_args) }) }";
+            #$text = "HTML::Mason::Filters::Result->new(sub { ($text) } )";
+            shift @filters;
+        }
+        else
+        {
+            # XXX untested, if anything breaks, assume this is wrong
+            $text = "HTML::Mason::Filters::Result->new(sub { \"\Q$text\E\" })";
+        }
 
-        my %seen;
-        my $flags =
-            ( join ', ',
-              map { $seen{$_}++ ? () : "'$_'" }
-              grep { $_ ne 'n' } @flags
-            );
+        if ( @filters )
+        {
+            $text = sprintf 
+                "\$m->interp->apply_filters(%s, %s, %s)",
+                '{ request => $m }', # context
+                $text,               # input
+                join(', ', map {
+                    sprintf "[ '%s', [ %s ] ]", $_->[0], $_->[1] || '()'
+                } reverse @filters)
+            ;
+        }
 
-        $text = "\$m->interp->apply_escapes( (join '', ($text)), $flags )" if $flags;
+        if ( @flags )
+        {
+            $text = sprintf
+                "\$m->interp->apply_escapes(%s, %s)",
+                $text,
+                join ',', map { "'$_'" } reverse @flags
+            ;
+        }
     }
 
     my $code;
@@ -506,12 +562,25 @@ sub substitution
         # output defined bits, which is what $m->print does internally
         # as well. use 'if defined' for maximum efficiency; grep
         # creates a list.
-        $code = "for ( $text ) { \$\$_outbuf .= \$_ if defined }\n";
+        $code = sprintf "for my \$_text ( %s ) { %s }\n",
+            $text,
+            join('; ',
+                '$_text = $_text->as_string if Scalar::Util::blessed($_text)'
+                    . ' && $_text->isa("HTML::Mason::Filters::Result")',
+                '$$_outbuf .= $_text if defined($_text)',
+            )
+        ;
     }
 
     eval { $self->postprocess_perl->(\$code) } if $self->postprocess_perl;
     compiler_error $@ if $@;
 
+#    open my $fh, ">out." . sprintf("%03d", ++$code_num);
+#    use Data::Dumper;
+#    print $fh Dumper(\%p);
+#    print $fh $code;
+#    close $fh;
+
     $self->_add_body_code($code);
 
     $self->{current_compile}{last_body_code_type} = 'substitution';
diff --git a/lib/HTML/Mason/Filters.pm b/lib/HTML/Mason/Filters.pm
new file mode 100644
index 0000000..26c27ca
--- /dev/null
+++ b/lib/HTML/Mason/Filters.pm
@@ -0,0 +1,90 @@
+# -*- cperl-indent-level: 4; cperl-continued-brace-offset: -4; cperl-continued-statement-offset: 4 -*-
+# vim: tabstop=4 shiftwidth=4 softtabstop=4:
+
+use strict;
+use warnings;
+
+package HTML::Mason::Filters;
+
+# input = code
+sub cache
+{
+    my ( $m, $input, $args ) = @_;
+
+    my ($get, $set) = $m->_cache_access(@$args);
+
+    my $cached = $get->();
+
+    if ( $cached )
+    {
+        return HTML::Mason::Filters::Result->new(sub { $cached });
+    }
+    else
+    {
+        return HTML::Mason::Filters::Result->new(sub {
+            my $output = $input->();
+            $set->($output);
+            return $output;
+        });
+    }
+}
+
+# this would be input = code
+#sub format
+#{
+#    my ( $m, $input, $args ) = @_;
+#
+#    my ( $format ) = @$args;
+#
+#    return HTML::Mason::Filters::Result->new(sub {
+#        sprintf $format, $input->()
+#    });
+#}
+
+# input = text
+sub format
+{
+    my ( $input, $args ) = @_;
+    return sprintf $args->[0], $$input;
+}
+
+package HTML::Mason::Filters::Result;
+
+use Scalar::Util ();
+
+sub new {
+    my $class = shift;
+    my $arg   = shift;
+    my $code  = (
+        ref $arg eq 'CODE' or (
+            Scalar::Util::blessed($arg) &&
+            $arg->isa($class)
+        )
+    )
+    ? $arg
+    : sub { $arg };
+    return bless $code => $class;
+}
+
+sub as_string {
+    my ($self) = @_;
+    my @rv = $self->();
+    $_[0] = HTML::Mason::Filters::Result::Static->new(sub { @rv });
+    return join '', grep { defined } @rv;
+}
+
+use overload (
+    fallback => 1,
+    q{""} => 'as_string',
+);
+
+package HTML::Mason::Filters::Result::Static;
+
+our @ISA = 'HTML::Mason::Filters::Result';
+
+sub as_string {
+    my ($self) = @_;
+    return join '', grep { defined } $self->();
+}
+
+1;
diff --git a/lib/HTML/Mason/Interp.pm b/lib/HTML/Mason/Interp.pm
index 82525b3..01de0d1 100644
--- a/lib/HTML/Mason/Interp.pm
+++ b/lib/HTML/Mason/Interp.pm
@@ -1,4 +1,5 @@
 # -*- cperl-indent-level: 4; cperl-continued-brace-offset: -4; cperl-continued-statement-offset: 4 -*-
+# vim: tabstop=4 shiftwidth=4 softtabstop=4:
 
 # Copyright (c) 1998-2005 by Jonathan Swartz. All rights reserved.
 # This program is free software; you can redistribute it and/or modify it
@@ -15,6 +16,7 @@ use File::Spec;
 use File::Temp;
 use HTML::Mason;
 use HTML::Mason::Escapes;
+use HTML::Mason::Filters;
 use HTML::Mason::Request;
 use HTML::Mason::Resolver::File;
 use HTML::Mason::Tools qw(read_file taint_is_on load_pkg);
@@ -168,6 +170,7 @@ sub _initialize
     $self->_check_data_dir();
     $self->_create_data_subdirs();
     $self->_initialize_escapes();
+    $self->_initialize_filters();
 
     #
     # Create preallocated buffer for requests.
@@ -249,6 +252,28 @@ sub _initialize_escapes
     }
 }
 
+sub _initialize_filters
+{
+    my $self = shift;
+
+    # Add filters (including defaults)
+    $self->set_filter(
+        cache => {
+            code => \&HTML::Mason::Filters::cache,
+            input => 'code',
+        },
+        format => {
+            code => \&HTML::Mason::Filters::format,
+            input => 'text',
+        },
+    );
+
+    if ( my $f = delete $self->{filter_flags} )
+    {
+        $self->set_filter(%$f);
+    }
+}
+
 sub _set_code_cache_attributes
 {
     my $self = shift;
@@ -987,7 +1012,12 @@ sub set_escape
             }
         }
 
-        $self->{escapes}{$name} = $coderef;
+        $self->set_filter(
+            $name => {
+                code  => $coderef,
+                input => 'text',
+            },
+        );
     }
 }
 
@@ -995,23 +1025,99 @@ sub remove_escape
 {
     my $self = shift;
 
-    delete $self->{escapes}{ shift() };
+    delete $self->{filters}{ shift() };
 }
 
-sub apply_escapes
+sub apply_escapes 
 {
     my $self = shift;
     my $text = shift;
+    # keep the original error message in case anyone is depending on it
+    return $self->apply_filters( {}, $text, map {
+        exists $self->{filters}{$_} &&
+        $self->{filters}{$_}{input} eq 'text' # reasonable guess
+        ? [ $_ ]
+        : param_error "Invalid escape flag: $_"
+    } @_ );
+}
+
+sub set_filter
+{
+    my $self = shift;
+    my %p = @_;
 
-    foreach my $flag (@_)
+    while ( my ($name, $arg) = each %p )
     {
-        param_error "Invalid escape flag: $flag"
-            unless exists $self->{escapes}{$flag};
+        if ( ref $arg eq 'CODE' )
+        {
+            $arg = { code => $arg };
+        }
 
-        $self->{escapes}{$flag}->(\$text);
+        unless ( $arg->{input} and
+            ( $arg->{input} eq 'code' or $arg->{input} eq 'text' ) )
+        {
+            param_error "Invalid filter input style: $arg->{input}";
+        }
+
+        if ( $arg->{input} eq 'text' ) 
+        {
+            my $code = $arg->{code};
+            $arg->{code} = sub {
+                my ( $m, $input, $args ) = @_;
+                return HTML::Mason::Filters::Result->new(sub {
+                    # force passing in a string, just in case some user-defined
+                    # escape becomes confused otherwise, though I think this is
+                    # unlikely
+                    $input = join('', $input);
+                    $code->(\$input, $args);
+                    return $input;
+                });
+            };
+        }
+
+        $self->{filters}{$name} = $arg;
+    }
+}
+
+sub remove_filter
+{
+    my $self = shift;
+    delete $self->{filters}{ shift() };
+}
+
+sub apply_filters
+{
+    my $self = shift;
+
+    my $context = shift;
+    my $m = delete $context->{request};
+
+    #my $result = HTML::Mason::Filters::Result->new(shift);
+    my $result = shift;
+
+    my @filter_args = @_;
+
+    if ( my ($not_found) =
+        grep { ! exists $self->{filters}{$_->[0]} } @filter_args)
+    {
+        param_error "Invalid filter: $not_found";
+    }
+
+    for my $filter_arg (@filter_args)
+    {
+        my ( $name, $args ) = @$filter_arg;
+
+        my $input = $result;
+        $result = HTML::Mason::Filters::Result->new(sub {
+            $self->{filters}{$name}{code}->(
+                $m,
+                $input,
+                $args,
+            )
+        });
     }
 
-    return $text;
+    return $result;
 }
 
 1;
diff --git a/lib/HTML/Mason/Lexer.pm b/lib/HTML/Mason/Lexer.pm
index 0d88b71..eed10b0 100644
--- a/lib/HTML/Mason/Lexer.pm
+++ b/lib/HTML/Mason/Lexer.pm
@@ -1,3 +1,6 @@
+# -*- cperl-indent-level: 4; cperl-continued-brace-offset: -4; cperl-continued-statement-offset: 4 -*-
+# vim: tabstop=4 shiftwidth=4 softtabstop=4:
+
 # Copyright (c) 1998-2005 by Jonathan Swartz. All rights reserved.
 # This program is free software; you can redistribute it and/or modify it
 # under the same terms as Perl itself.
@@ -364,6 +367,18 @@ sub match_named_block
 my $flag = qr/[[:alpha:]_]\w*/;
 sub escape_flag_regex { $flag }
 
+my $pipe_re = qr/
+    \s*
+    (?<!\|)
+    \|
+    \s*
+/x;
+
+my $old_flag_re = qr/
+   $flag
+   (?:\s*,\s*$flag)*
+/x;
+
 sub match_substitute
 {
     # This routine relies on there *not* to be an opening <%foo> tag
@@ -374,27 +389,45 @@ sub match_substitute
     return 0 unless $self->{current}{comp_source} =~ /\G<%/gcs;
 
     if ( $self->{current}{comp_source} =~
-         m{
-           \G
-           (.+?)                # Substitution body ($1)
-           (
-            \s*
-            (?<!\|)             # Not preceded by a '|'
-            \|                  # A '|'
-            \s*
-            (                   # (Start $3)
-             $flag              # A flag
-             (?:\s*,\s*$flag)*  # More flags, with comma separators
-            )
-            \s*
-           )?
-           %>                   # Closing tag
-          }xcigs )
+        m{ \G \s* (.+?) \s* %> }xcigs )
     {
-        $self->{current}{lines} += tr/\n// foreach grep defined, ($1, $2);
+        my $body = $1;
+        $self->{current}{lines} += $body =~ tr/\n//;
+
+        my @filters;
+           
+        if ( $body =~
+            s{
+                $pipe_re
+                ( $old_flag_re )
+                \s* $
+            }{}xis )
+        {
+            push @filters, [ $1 ];
+        }
+                
+        while ( $body =~
+            s{
+                $pipe_re
+                ( $flag )       # filter name
+                (               # (optional) filter args
+                    \( .* \)
+                )?
+                \s* $
+            }{}xis )
+        {
+            unshift @filters, [ $1 => $2 ];
+        }
+
+        # special case -- inline substiutions are always interpreted as Perl
+        # code; later, substitution-with-content may be either Perl or text
+        unshift @filters, [ 'perl', '' ];
+
+        $self->{current}{compiler}->substitution(
+            substitution => $body,
+            filters      => \@filters,
+        );
 
-        $self->{current}{compiler}->substitution( substitution => $1,
-                                                  escape => $3 );
         return 1;
     }
     else
diff --git a/lib/HTML/Mason/Request.pm b/lib/HTML/Mason/Request.pm
index 1af1e7e..ad6754b 100644
--- a/lib/HTML/Mason/Request.pm
+++ b/lib/HTML/Mason/Request.pm
@@ -1,4 +1,5 @@
 # -*- cperl-indent-level: 4; cperl-continued-brace-offset: -4; cperl-continued-statement-offset: 4 -*-
+# vim: tabstop=4 shiftwidth=4 softtabstop=4:
 
 # Copyright (c) 1998-2005 by Jonathan Swartz. All rights reserved.
 # This program is free software; you can redistribute it and/or modify it
@@ -771,13 +772,15 @@ sub _cache_1_x
     }
 }
 
-sub cache_self {
+# abstract out the details of data_cache_api so that the client code doesn't
+# need to care; return a coderef for getting from the cache with the given
+# params and a coderef for setting.
+sub _cache_access {
     my ($self, %options) = @_;
-
-    return if $self->{top_stack}->[STACK_IN_CALL_SELF]->{'CACHE_SELF'};
-
+  
     my (%store_options, %retrieve_options);
     my ($expires_in, $key, $cache);
+
     if ($self->data_cache_api eq '1.0') {
         foreach (qw(key expire_if busy_lock)) {
             $retrieve_options{$_} = $options{$_} if (exists($options{$_}));
@@ -795,17 +798,46 @@ sub cache_self {
             $retrieve_options{$_} = delete($options{$_}) if (exists($options{$_}));
         }
         $expires_in = delete $options{expires_in} || delete $options{expire_in} || 'never';
-        $key = delete $options{key} || '__mason_cache_self__';
+        $key = delete $options{key} || delete $options{default_key}
+            || '__mason_cache_default__';
         $cache = $self->cache(%options);
     }
 
+    return ( $self->data_cache_api eq '1.0'
+        ? (
+            # 1.0 getter
+            sub {
+                $self->cache(%retrieve_options)
+            },
+            # 1.0 setter
+            sub {
+                $self->cache(
+                  action => 'store',
+                  key    => $key,
+                  value  => shift,
+                  %store_options
+                );
+            },
+        )
+        : (
+            # modern getter
+            sub { $cache->get($key, %retrieve_options) },
+            # modern setter
+            sub { $cache->set($key, shift, $expires_in) },
+        )
+    );
+}
+
+sub cache_self {
+    my ($self, %options) = @_;
+
+    return if $self->{top_stack}->[STACK_IN_CALL_SELF]->{'CACHE_SELF'};
+
+    my ( $get, $set ) = $self->_cache_access(%options);
+
     my ($output, @retval, $error);
 
-    my $cached =
-        ( $self->data_cache_api eq '1.0' ?
-          $self->cache(%retrieve_options) :
-          $cache->get($key, %retrieve_options)
-        );
+    my $cached = $get->();
 
     if ($cached) {
         ($output, my $retval) = @$cached;
@@ -820,11 +852,7 @@ sub cache_self {
             unless ($self->_aborted_or_declined($error));
 
         my $value = [$output, \@retval];
-        if ($self->data_cache_api eq '1.0') {
-            $self->cache(action=>'store', key=>$key, value=>$value, %store_options);
-        } else {
-            $cache->set($key, $value, $expires_in);
-        }
+        $set->($value);
     }
 
     #
@@ -1171,8 +1199,10 @@ sub print
         );
 
     # use 'if defined' for maximum efficiency; grep creates a list.
-    for ( @_ ) {
-        $$bufref .= $_ if defined;
+    for my $_text ( @_ ) {
+        $_text = $_text->as_string if Scalar::Util::blessed($_text)
+            && $_text->isa("HTML::Mason::Filters::Result");
+        $$bufref .= $_text if defined($_text);
     }
 
     $self->flush_buffer if $self->{autoflush};
diff --git a/lib/HTML/Mason/Tests.pm b/lib/HTML/Mason/Tests.pm
index e69ab8d..ab68da1 100644
--- a/lib/HTML/Mason/Tests.pm
+++ b/lib/HTML/Mason/Tests.pm
@@ -503,10 +503,13 @@ sub _run_test
 
     $self->{buffer} = '';
     my $interp = $self->_make_main_interp;
-    $interp->out_method( sub { for (@_) { $self->{buffer} .= $_ if defined $_ } } );
+    $interp->out_method( sub {
+      for my $text (@_) { $self->{buffer} .= $text if defined $text } 
+    } );
 
     my $warnings = '';
-    local $SIG{__WARN__} = sub { $warnings .= $_ for @_ };
+    local $SIG{__WARN__} = sub { for my $text (@_) { $warnings .= $text } };
+
     eval {
         # Run pre_code if test has it - pass in interp
         if ($test->{pre_code}) {
diff --git a/t/10-cache.t b/t/10-cache.t
index c8b9bd5..420809d 100644
--- a/t/10-cache.t
+++ b/t/10-cache.t
@@ -581,6 +581,35 @@ EOF
 
 #------------------------------------------------------------
 
+    $group->add_support(
+        path => 'support/cache_substitution',
+        component => <<'EOF',
+<% ($m->print("hello\n"), "world") | cache(expires_in => "5s") %>
+EOF
+    );
+
+    $group->add_test(
+        name => 'cache_substition',
+        description => 'test | cache in substitution',
+        component => <<'EOF',
+<& support/cache_substitution &>
+<& support/cache_substitution &>
+% sleep 5;
+<& support/cache_substitution &>
+EOF
+        expect => <<'EOF',
+hello
+world
+
+world
+
+hello
+world
+EOF
+    );
+
+#------------------------------------------------------------
+
     return $group;
 }