File Coverage

blib/lib/Filter/Simple.pm
Criterion Covered Total %
statement 117 125 93.6
branch 34 50 68.0
condition 4 9 44.4
subroutine 17 18 94.4
pod 0 6 0.0
total 172 208 82.6


line stmt bran cond sub pod time code
1             package Filter::Simple;
2              
3 9     9   35035 use Text::Balanced ':ALL';
  9         176900  
  9         2252  
4              
5 9     9   100 use vars qw{ $VERSION @EXPORT };
  9         23  
  9         601  
6              
7             $VERSION = '0.93_01';
8              
9 9     9   5327 use Filter::Util::Call;
  9         8474  
  9         617  
10 9     9   71 use Carp;
  9         25  
  9         15875  
11              
12             @EXPORT = qw( FILTER FILTER_ONLY );
13              
14              
15             sub import {
16 7 50   7   108 if (@_>1) { shift; goto &FILTER }
  0         0  
  0         0  
17 7         39 else { *{caller()."::$_"} = \&$_ foreach @EXPORT }
  14         815  
18             }
19              
20             sub fail {
21 0     0 0 0 croak "FILTER_ONLY: ", @_;
22             }
23              
24             my $exql = sub {
25             my @bits = extract_quotelike $_[0], qr//;
26             return unless $bits[0];
27             return \@bits;
28             };
29              
30             my $ncws = qr/\s+/;
31             my $nl = qr/(?:\r\n?|\n)/;
32             my $comment = qr/(?
33             my $ws = qr/(?:$ncws|$comment)+/;
34             my $id = qr/\b(?!([ysm]|q[rqxw]?|tr)\b)\w+/;
35             my $EOP = qr/\n\n|\Z/;
36             my $CUT = qr/\n=cut.*$EOP/;
37             my $pod_or_DATA = qr/
38             ^=(?:head[1-4]|item) .*? $CUT
39             | ^=pod .*? $CUT
40             | ^=for .*? $CUT
41             | ^=begin .*? $CUT
42             | ^__(DATA|END)__\r?\n.*
43             /smx;
44             my $variable = qr{
45             [\$*\@%]\s*
46             \{\s*(?!::)(?:\d+|[][&`'#+*./|,";%=~:?!\@<>()-]|\^[A-Z]?)\}
47             | (?:\$#?|[*\@\%]|\\&)\$*\s*
48             (?: \{\s*(?:\^(?=[A-Z_]))?(?:\w|::|'\w)*\s*\}
49             | (?:\^(?=[A-Z_]))?(?:\w|::|'\w)*
50             | (?=\{) # ${ block }
51             )
52             )
53             | \$\s*(?!::)(?:\d+|[][&`'#+*./|,";%=~:?!\@<>()-]|\^[A-Z]?)
54             }x;
55              
56             my %extractor_for = (
57             quotelike => [ $ws, $variable, $id, { MATCH => \&extract_quotelike } ], # here we need to extract here documents better!
58             regex => [ $ws, $pod_or_DATA, $id, $exql ],
59             string => [ $ws, $pod_or_DATA, $id, $exql ],
60             code => [ $ws, { DONT_MATCH => $pod_or_DATA }, $variable,
61             $id, { DONT_MATCH => \&extract_quotelike } ], # here we need to extract here documents better!
62             code_no_comments
63             => [ { DONT_MATCH => $comment },
64             $ncws, { DONT_MATCH => $pod_or_DATA }, $variable,
65             $id, { DONT_MATCH => \&extract_quotelike } ], # here we need to extract here documents better!
66             executable => [ $ws, { DONT_MATCH => $pod_or_DATA } ],
67             executable_no_comments
68             => [ { DONT_MATCH => $comment },
69             $ncws, { DONT_MATCH => $pod_or_DATA } ],
70             all => [ { MATCH => qr/(?s:.*)/ } ],
71             );
72              
73             my %selector_for = (
74             all => sub { my ($t)=@_; sub{ $_=$$_; $t->(@_); $_} },
75             executable=> sub { my ($t)=@_; sub{ref() ? $_=$$_ : $t->(@_); $_} },
76             quotelike => sub { my ($t)=@_; sub{ref() && do{$_=$$_; $t->(@_)};
77             # Here, transform here documents back
78             # depending on whether they had content
79             # coming after them or not
80             # This might break other stuff
81             # that relies on that transform
82             warn "Returning [[$_]]";
83             $_} },
84             regex => sub { my ($t)=@_;
85             sub{ref() or return $_;
86             my ($ql,$rest_of_line,$pre,$op,$ld,$pat) = @$_;
87             return $_->[0] unless $op =~ /^(qr|m|s)/
88             || !$op && ($ld eq '/' || $ld eq '?');
89             $_ = $pat;
90             $t->(@_);
91             $ql =~ s/^(\s*\Q$op\E\s*\Q$ld\E)\Q$pat\E/$1$_/;
92             return "$pre$ql";
93             };
94             },
95             string => sub { my ($t)=@_;
96             sub{ref() or return $_;
97             local *args = \@_;
98             my ($pre,$op,$ld1,$str1,$rd1,$ld2,$str2,$rd2,$flg) = @{$_}[2..10];
99             return $_->[0] if $op =~ /^(qr|m)/
100             || !$op && ($ld1 eq '/' || $ld1 eq '?');
101             if (!$op || $op eq 'tr' || $op eq 'y') {
102             local *_ = \$str1;
103             $t->(@args);
104             }
105             if ($op =~ /^(tr|y|s)/) {
106             local *_ = \$str2;
107             $t->(@args);
108             }
109             my $result = "$pre$op$ld1$str1$rd1";
110             $result .= $ld2 if $ld1 =~ m/[[({<]/; #])}>
111             $result .= "$str2$rd2$flg";
112             return $result;
113             };
114             },
115             );
116              
117             sub gen_std_filter_for {
118 8     8 0 38 my ($type, $transform) = @_;
119             return sub {
120 25     25   11788 my $instr;
121 25         58 local @components; # See documentation, this is a global variable
122 25         108 for (extract_multiple($_,$extractor_for{$type})) {
123 715 100       132715 if (ref()) { push @components, $_; $instr=0 }
  79 100       145  
  79         141  
124 534         852 elsif ($instr) { $components[-1] .= $_; }
125 102         281 else { push @components, $_; $instr=1 }
  102         180  
126             }
127              
128 25 100       146 if ($type =~ /^code/) {
129 23         63 my $count = 0;
130 23         186 local $placeholder = qr/\Q$;\E(.{4})\Q$;\E/s; # see documentation
131 23         58 my $extractor = $placeholder;
132 23         132 my $extractor_nc = qr/\Q$;\E(?:.{4})\Q$;\E/s;
133             $_ = join "",
134 23 100       69 map { ref $_ ? $;.pack('N',$count++).$; : $_ }
  149         478  
135             @components;
136 23         62 @components = grep { ref $_ } @components;
  149         355  
137 23         97 $transform->(@_);
138              
139 23         101 my @res;
140              
141             # The approach is as follows:
142             # Since we have eliminated all (multiline) strings and comments
143             # and converted them to strings that match /$;....$;/,
144             # we can do our replacement line-by-line, appending here documents
145             # below the line by pushing their output onto @res as well
146              
147             # We split up each line into non-string and string parts and then
148             # rebuild the target string anew, replacing all placeholders
149             # within it. If we encounter a here document, finish the current
150             # line and append the here document immediately as the next line.
151             # If we encounter any other quotelike, replace it inline and then
152             # stash away anything after the newline for later reassembly.
153             # This handles these cases:
154             # foo(<
155             # EOM
156             # single-quoted]);
157             # and
158             # foo(q[
159             # single-quoted],<
160             # EOM
161             my @following_lines;
162 23         320 my $whole_line = qr/((?:[^$;\r\n]+|$;....$;)*(?:$nl|\z))/s;
163             @res = map {
164 23 50       356 if( ! defined ) {
  174 100       706  
165 0         0 ''
166             } elsif( /$placeholder/ ) {
167 44         207 s{$placeholder}{
168 64         157 my $str = ${$components[unpack('N',$1)]};
  64         252  
169 64 100 66     500 if( $str =~ s/^(<<[^\r\n]+)$nl// ) {
    100          
170             # Split the here document into the header
171             # and the body
172 13         30 push @following_lines, $str;
173 13         27 $str = $1;
174              
175             } elsif( @following_lines and $str =~ s/^([^\n\r]*$nl)// ) {
176             # Here we have a multiline string being
177             # replaced for the placeholder, so we need to
178             # rebuild it respecting potential preceding
179             # here documents
180 2         7 push @following_lines, $str;
181 2         5 $str = $1;
182             # Just purge onto the end:
183 2         9 $str = $1 . join "", (splice( @following_lines, 0 ));
184             }
185             $str
186 64         211 }ge;
187             # must have been a newline, so purge the
188             # accumulated output:
189 44         115 ($_,splice(@following_lines));
190             } else {
191             # Plain string replacement
192 130         333 $_
193             };
194             } /$whole_line/g;
195              
196 23         263 $_ = join "", @res;
197             }
198             else {
199 2         10 my $selector = $selector_for{$type}->($transform);
200 2         18 $_ = join "", map $selector->(@_), @components;
201             }
202             }
203 8         68 };
204              
205             sub FILTER (&;$) {
206 3     3 0 777 my $caller = caller;
207 3         9 my ($filter, $terminator) = @_;
208 9     9   87 no warnings 'redefine';
  9         24  
  9         2240  
209 3         12 *{"${caller}::import"} = gen_filter_import($caller,$filter,$terminator);
  3         17  
210 3         19 *{"${caller}::unimport"} = gen_filter_unimport($caller);
  3         24  
211             }
212              
213             sub FILTER_ONLY {
214 4     4 0 52 my $caller = caller;
215 4         24 while (@_ > 1) {
216 6         31 my ($what, $how) = splice(@_, 0, 2);
217             fail "Unknown selector: $what"
218 6 50       30 unless exists $extractor_for{$what};
219 6 50       29 fail "Filter for $what is not a subroutine reference"
220             unless ref $how eq 'CODE';
221 6         23 push @transforms, gen_std_filter_for($what,$how);
222             }
223 4         13 my $terminator = shift;
224              
225             my $multitransform = sub {
226 4     4   13 foreach my $transform ( @transforms ) {
227 6         26 $transform->(@_);
228             }
229 4         17 };
230 9     9   73 no warnings 'redefine';
  9         23  
  9         5316  
231 4         23 *{"${caller}::import"} =
  4         21  
232             gen_filter_import($caller,$multitransform,$terminator);
233 4         18 *{"${caller}::unimport"} = gen_filter_unimport($caller);
  4         59  
234             }
235              
236             my $ows = qr/(?:[ \t]+|#[^\n]*)*/;
237              
238             sub gen_filter_import {
239 7     7 0 24 my ($class, $filter, $terminator) = @_;
240 7         18 my %terminator;
241 7         17 my $prev_import = *{$class."::import"}{CODE};
  7         52  
242             return sub {
243 7     7   140 my ($imported_class, @args) = @_;
244 7         352 my $def_terminator =
245             qr/^(?:\s*no\s+$imported_class\s*;$ows|__(?:END|DATA)__)\r?$/;
246 7 50 0     46 if (!defined $terminator) {
    0          
    0          
    0          
247 7         26 $terminator{terminator} = $def_terminator;
248             }
249             elsif (!ref $terminator || ref $terminator eq 'Regexp') {
250 0         0 $terminator{terminator} = $terminator;
251             }
252             elsif (ref $terminator ne 'HASH') {
253 0         0 croak "Terminator must be specified as scalar or hash ref"
254             }
255             elsif (!exists $terminator->{terminator}) {
256 0         0 $terminator{terminator} = $def_terminator;
257             }
258             filter_add(
259             sub {
260 11     11   180 my ($status, $lastline);
261 11         28 my $count = 0;
262 11         29 my $data = "";
263 11         88 while ($status = filter_read()) {
264 103 50       276 return $status if $status < 0;
265 103 100 66     781 if ($terminator{terminator} &&
266             m/$terminator{terminator}/) {
267 3         9 $lastline = $_;
268 3         8 $count++;
269 3         8 last;
270             }
271 100         241 $data .= $_;
272 100         168 $count++;
273 100         404 $_ = "";
274             }
275 11 100       5413 return $count if not $count;
276 7         21 $_ = $data;
277 7 50       60 $filter->($imported_class, @args) unless $status < 0;
278 7 100       77 if (defined $lastline) {
279 3 50       30 if (defined $terminator{becomes}) {
    50          
280 0         0 $_ .= $terminator{becomes};
281             }
282             elsif ($lastline =~ $def_terminator) {
283 3         11 $_ .= $lastline;
284             }
285             }
286 7         1853 return $count;
287             }
288 7         65 );
289 7 100       267 if ($prev_import) {
    100          
290 1         6 goto &$prev_import;
291             }
292             elsif ($class->isa('Exporter')) {
293 1         118 $class->export_to_level(1,@_);
294             }
295             }
296 7         47 }
297              
298             sub gen_filter_unimport {
299 7     7 0 23 my ($class) = @_;
300             return sub {
301 2     2   24 filter_del();
302 2 50       2199 goto &$prev_unimport if $prev_unimport;
303             }
304 7         36 }
305              
306             1;
307              
308             __END__