File Coverage

blib/lib/Filter/Simple.pm
Criterion Covered Total %
statement 116 124 93.5
branch 34 50 68.0
condition 4 9 44.4
subroutine 17 18 94.4
pod 0 6 0.0
total 171 207 82.6


line stmt bran cond sub pod time code
1             package Filter::Simple;
2              
3 8     8   22812 use Text::Balanced ':ALL';
  8         113123  
  8         1453  
4              
5 8     8   60 use vars qw{ $VERSION @EXPORT };
  8         15  
  8         363  
6              
7             $VERSION = '0.92_01';
8              
9 8     8   3465 use Filter::Util::Call;
  8         5598  
  8         428  
10 8     8   58 use Carp;
  8         16  
  8         10370  
11              
12             @EXPORT = qw( FILTER FILTER_ONLY );
13              
14              
15             sub import {
16 7 50   7   78 if (@_>1) { shift; goto &FILTER }
  0         0  
  0         0  
17 7         23 else { *{caller()."::$_"} = \&$_ foreach @EXPORT }
  14         556  
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 7     7 0 24 my ($type, $transform) = @_;
119             return sub {
120 24     24   10928 my $instr;
121 24         58 local @components; # See documentation, this is a global variable
122 24         92 for (extract_multiple($_,$extractor_for{$type})) {
123 710 100       107271 if (ref()) { push @components, $_; $instr=0 }
  79 100       119  
  79         131  
124 530         796 elsif ($instr) { $components[-1] .= $_; }
125 101         176 else { push @components, $_; $instr=1 }
  101         181  
126             }
127              
128 24 100       112 if ($type =~ /^code/) {
129 22         49 my $count = 0;
130 22         142 local $placeholder = qr/\Q$;\E(.{4})\Q$;\E/s; # see documentation
131 22         46 my $extractor = $placeholder;
132 22         100 my $extractor_nc = qr/\Q$;\E(?:.{4})\Q$;\E/s;
133             $_ = join "",
134 22 100       59 map { ref $_ ? $;.pack('N',$count++).$; : $_ }
  148         470  
135             @components;
136 22         58 @components = grep { ref $_ } @components;
  148         270  
137 22         92 $transform->(@_);
138              
139 22         90 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 22         221 my $whole_line = qr/((?:[^$;\r\n]+|$;....$;)*$nl)/s;
163             @res = map {
164 22 50       296 if( ! defined ) {
  147 100       553  
165 0         0 ''
166             } elsif( /$placeholder/ ) {
167 43         179 s{$placeholder}{
168 62         113 my $str = ${$components[unpack('N',$1)]};
  62         210  
169 62 100 66     472 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         25 $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 62         189 }ge;
187             # must have been a newline, so purge the
188             # accumulated output:
189 43         125 ($_,splice(@following_lines));
190             } else {
191             # Plain string replacement
192 104         225 $_
193             };
194             } /$whole_line/g;
195              
196              
197 22         188 $_ = join "", @res;
198             }
199             else {
200 2         6 my $selector = $selector_for{$type}->($transform);
201 2         7 $_ = join "", map $selector->(@_), @components;
202             }
203             }
204 7         46 };
205              
206             sub FILTER (&;$) {
207 3     3 0 764 my $caller = caller;
208 3         7 my ($filter, $terminator) = @_;
209 8     8   63 no warnings 'redefine';
  8         21  
  8         1568  
210 3         9 *{"${caller}::import"} = gen_filter_import($caller,$filter,$terminator);
  3         11  
211 3         13 *{"${caller}::unimport"} = gen_filter_unimport($caller);
  3         14  
212             }
213              
214             sub FILTER_ONLY {
215 4     4 0 35 my $caller = caller;
216 4         20 while (@_ > 1) {
217 6         25 my ($what, $how) = splice(@_, 0, 2);
218             fail "Unknown selector: $what"
219 6 50       21 unless exists $extractor_for{$what};
220 6 50       24 fail "Filter for $what is not a subroutine reference"
221             unless ref $how eq 'CODE';
222 6         15 push @transforms, gen_std_filter_for($what,$how);
223             }
224 4         10 my $terminator = shift;
225              
226             my $multitransform = sub {
227 4     4   11 foreach my $transform ( @transforms ) {
228 6         16 $transform->(@_);
229             }
230 4         15 };
231 8     8   55 no warnings 'redefine';
  8         31  
  8         3633  
232 4         17 *{"${caller}::import"} =
  4         15  
233             gen_filter_import($caller,$multitransform,$terminator);
234 4         10 *{"${caller}::unimport"} = gen_filter_unimport($caller);
  4         35  
235             }
236              
237             my $ows = qr/(?:[ \t]+|#[^\n]*)*/;
238              
239             sub gen_filter_import {
240 7     7 0 21 my ($class, $filter, $terminator) = @_;
241 7         14 my %terminator;
242 7         14 my $prev_import = *{$class."::import"}{CODE};
  7         42  
243             return sub {
244 7     7   93 my ($imported_class, @args) = @_;
245 7         265 my $def_terminator =
246             qr/^(?:\s*no\s+$imported_class\s*;$ows|__(?:END|DATA)__)\r?$/;
247 7 50 0     34 if (!defined $terminator) {
    0          
    0          
    0          
248 7         19 $terminator{terminator} = $def_terminator;
249             }
250             elsif (!ref $terminator || ref $terminator eq 'Regexp') {
251 0         0 $terminator{terminator} = $terminator;
252             }
253             elsif (ref $terminator ne 'HASH') {
254 0         0 croak "Terminator must be specified as scalar or hash ref"
255             }
256             elsif (!exists $terminator->{terminator}) {
257 0         0 $terminator{terminator} = $def_terminator;
258             }
259             filter_add(
260             sub {
261 11     11   100 my ($status, $lastline);
262 11         20 my $count = 0;
263 11         20 my $data = "";
264 11         61 while ($status = filter_read()) {
265 103 50       208 return $status if $status < 0;
266 103 100 66     540 if ($terminator{terminator} &&
267             m/$terminator{terminator}/) {
268 3         9 $lastline = $_;
269 3         7 last;
270             }
271 100         192 $data .= $_;
272 100         131 $count++;
273 100         294 $_ = "";
274             }
275 11 100       4026 return $count if not $count;
276 7         16 $_ = $data;
277 7 50       40 $filter->($imported_class, @args) unless $status < 0;
278 7 100       64 if (defined $lastline) {
279 3 50       27 if (defined $terminator{becomes}) {
    50          
280 0         0 $_ .= $terminator{becomes};
281             }
282             elsif ($lastline =~ $def_terminator) {
283 3         10 $_ .= $lastline;
284             }
285             }
286 7         1536 return $count;
287             }
288 7         45 );
289 7 100       192 if ($prev_import) {
    100          
290 1         4 goto &$prev_import;
291             }
292             elsif ($class->isa('Exporter')) {
293 1         82 $class->export_to_level(1,@_);
294             }
295             }
296 7         34 }
297              
298             sub gen_filter_unimport {
299 7     7 0 15 my ($class) = @_;
300             return sub {
301 2     2   22 filter_del();
302 2 50       2212 goto &$prev_unimport if $prev_unimport;
303             }
304 7         24 }
305              
306             1;
307              
308             __END__