File Coverage

blib/lib/Pod/Elemental/MakeSelector.pm
Criterion Covered Total %
statement 69 79 87.3
branch 24 30 80.0
condition 2 3 66.6
subroutine 15 15 100.0
pod 1 9 11.1
total 111 136 81.6


line stmt bran cond sub pod time code
1             #---------------------------------------------------------------------
2             package Pod::Elemental::MakeSelector;
3             #
4             # Copyright 2012 Christopher J. Madsen
5             #
6             # Author: Christopher J. Madsen <perl@cjmweb.net>
7             # Created: 5 Jun 2012
8             #
9             # This program is free software; you can redistribute it and/or modify
10             # it under the same terms as Perl itself.
11             #
12             # This program is distributed in the hope that it will be useful,
13             # but WITHOUT ANY WARRANTY; without even the implied warranty of
14             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the
15             # GNU General Public License or the Artistic License for more details.
16             #
17             # ABSTRACT: Build complex selectors as a single sub
18             #---------------------------------------------------------------------
19              
20 3     3   2138340 use 5.008;
  3         12  
21 3     3   17 use strict;
  3         7  
  3         77  
22 3     3   15 use warnings;
  3         5  
  3         150  
23              
24             our $VERSION = '0.12';
25             # This file is part of Pod-Elemental-MakeSelector 0.12 (October 17, 2015)
26              
27 3     3   16 use Carp qw(croak);
  3         6  
  3         240  
28              
29 3         37 use Sub::Exporter -setup => {
30             exports => [ qw(make_selector) ],
31             groups => { default => [ qw(make_selector) ]},
32 3     3   950 };
  3         17227  
33              
34             #=====================================================================
35             # Recturn true if the first element of the arrayref is not a string
36             # starting with -
37              
38             sub _has_optional_parameter
39             {
40 34     34   48 my ($inputR) = @_;
41              
42 34 100 66     242 @$inputR and (ref $inputR->[0] or not $inputR->[0] =~ /^-/);
43             } # end _has_optional_parameter
44              
45             #---------------------------------------------------------------------
46             sub required_parameter
47             {
48 25     25 0 37 my ($inputR, $error_message) = @_;
49              
50 25 50       63 croak($error_message) unless defined(my $val = shift @$inputR);
51              
52 25         59 $val;
53             } # end required_parameter
54              
55             #---------------------------------------------------------------------
56             sub add_value
57             {
58 57     57 0 91 my ($valuesR, $value) = @_;
59              
60 57         86 push @$valuesR, $value;
61              
62 57         320 '$val' . $#$valuesR;
63             } # end add_value
64              
65             #---------------------------------------------------------------------
66             sub join_expressions
67             {
68 51     51 0 82 my ($op, $expressionsR) = @_;
69              
70 51 100       133 return @$expressionsR unless @$expressionsR > 1;
71              
72 47         801 '(' . join("\n $op ", @$expressionsR) . "\n )";
73             } # end join_expressions
74              
75             #---------------------------------------------------------------------
76             # Supports only string, Regexp, or arrayref of either.
77             # Nested arrayrefs should work, but are not documented.
78              
79             sub smart_match
80             {
81 67     67 0 105 my ($valuesR, $value, $match) = @_;
82              
83             TEST_REF: {
84 67         82 my $ref = ref $match;
  67         102  
85              
86 67 100       157 if ($ref eq 'ARRAY') {
    100          
87 10         17 my $count = @$match;
88 10 100       32 if ($count == 0) {
    50          
89 2         9 return '0'; # Empty array never matches
90             } elsif ($count == 1) {
91 0         0 $match = $match->[0];
92 0         0 redo TEST_REF;
93             } else {
94 8         26 my $exp = join_expressions('or',
95             [ map smart_match($valuesR, '$v', $_), @$match ]
96             );
97 8 50       26 if ($value eq '$v') {
98 0         0 return $exp;
99             } else {
100 8         46 return sprintf 'do { my $v = %s; %s }', $value, $exp;
101             }
102             }
103             } elsif ($ref) {
104 17         41 return "$value =~ " . add_value($valuesR, $match);
105             } else {
106 40         93 return "$value eq " . add_value($valuesR, $match);
107             }
108             }
109              
110 0         0 die "Can't reach";
111             } # end smart_match
112              
113             #---------------------------------------------------------------------
114             sub conjunction_action
115             {
116 9     9 0 14 my ($op, $valuesR, $inputR) = @_;
117              
118 9         16 my $arrayR = shift @$inputR;
119 9 50       27 croak "Expected arrayref for -$op, got $arrayR"
120             unless ref($arrayR) eq 'ARRAY';
121              
122 9         12 my @expressions;
123 9         32 build_selector($valuesR, \@expressions, @$arrayR);
124              
125 9         18 join_expressions($op, \@expressions);
126             } # end conjunction_action
127              
128             #---------------------------------------------------------------------
129             sub region_action
130             {
131 10     10 0 21 my ($valuesR, $inputR, $pod) = @_;
132              
133 10         18 my @expressions = type_action(qw(isa Element::Pod5::Region));
134              
135 10 100       40 push @expressions, ($pod ? '' : 'not ') . '$para->is_pod'
    100          
136             if defined $pod;
137              
138 10 100       21 if (_has_optional_parameter($inputR)) {
139 4         11 push @expressions, smart_match($valuesR, '$para->format_name',
140             shift @$inputR);
141             } # end if specific format(s) listed
142              
143 10         28 join_expressions(and => \@expressions);
144             } # end region_action
145              
146             #---------------------------------------------------------------------
147             sub type_action
148             {
149 44     44 0 72 my ($check, $class) = @_;
150              
151 44         164 "\$para->$check('Pod::Elemental::$class')";
152             } # end type_action
153              
154             #---------------------------------------------------------------------
155             our %action = (
156             -and => sub { conjunction_action(and => @_) },
157             -or => sub { conjunction_action(or => @_) },
158             -blank => sub { type_action(qw(isa Element::Generic::Blank)) },
159             -flat => sub { type_action(qw(does Flat)) },
160             -node => sub { type_action(qw(does Node)) },
161              
162             -code => sub {
163             my ($valuesR, $inputR) = @_;
164              
165             my $name = add_value($valuesR,
166             required_parameter($inputR, "-code requires a value"));
167             "$name->(\$para)";
168             }, #end -code
169              
170             -command => sub {
171             my ($valuesR, $inputR) = @_;
172              
173             my @expressions = type_action(qw(does Command));
174              
175             if (_has_optional_parameter($inputR)) {
176             push @expressions, smart_match($valuesR, '$para->command', shift @$inputR);
177             } # end if specific command(s) listed
178              
179             join_expressions(and => \@expressions);
180             }, #end -command
181              
182             -content => sub {
183             my ($valuesR, $inputR) = @_;
184              
185             smart_match($valuesR, '$para->content',
186             required_parameter($inputR, "-content requires a value"));
187             }, #end -content
188              
189             -region => \&region_action,
190             -podregion => sub { region_action(@_, 1) },
191             -nonpodregion => sub { region_action(@_, 0) },
192             ); # end %action
193              
194              
195             #---------------------------------------------------------------------
196             sub build_selector
197             {
198 51     51 0 68 my $valuesR = shift;
199 51         98 my $expR = shift;
200              
201 51         149 while (@_) {
202 78         106 my $type = shift;
203              
204 78 50       205 my $action = $action{$type}
205             or croak "Expected selector type, got $type";
206              
207 78         196 push @$expR, $action->($valuesR, \@_);
208             } # end while more selectors
209             } # end build_selector
210             #---------------------------------------------------------------------
211              
212             # FIXME: These subs will be documented when I figure out how
213             # make_selector should be extended.
214              
215              
216             sub make_selector
217             {
218 42     42 1 60402 my @values;
219             my @expressions;
220              
221 42         121 build_selector(\@values, \@expressions, @_);
222              
223 42         117 my $code = ("sub { my \$para = shift; return (\n " .
224             join("\n and ", @expressions) .
225             "\n)}\n");
226              
227             $code = sprintf("my (%s) = \@values;\n\n%s",
228 42 100       136 join(', ', map { '$val' . $_ } 0 .. $#values),
  57         208  
229             $code)
230             if @values;
231              
232             #print STDERR $code;
233 42         68 my ($sub, $err);
234             {
235 42         56 local $@;
  42         72  
236 42         4484 $sub = eval $code;
237 42         98 $err = $@;
238             }
239              
240 42 50       114 unless (ref $sub) {
241 0         0 my $lineNum = ($code =~ tr/\n//);
242 0         0 my $fmt = '%' . length($lineNum) . 'd: ';
243 0         0 $lineNum = 0;
244 0         0 $code =~ s/^/sprintf $fmt, ++$lineNum/gem;
  0         0  
245              
246 0         0 die "Building selector failed:\n$code$err";
247             }
248              
249 42         135 $sub;
250             } # end make_selector
251              
252             #=====================================================================
253             # Package Return Value:
254              
255             1;
256              
257             __END__
258              
259             =pod
260              
261             =head1 NAME
262              
263             Pod::Elemental::MakeSelector - Build complex selectors as a single sub
264              
265             =head1 VERSION
266              
267             This document describes version 0.12 of
268             Pod::Elemental::MakeSelector, released October 17, 2015.
269              
270             =head1 SYNOPSIS
271              
272             use Pod::Elemental::MakeSelector;
273              
274             my $author_selector = make_selector(
275             -command => 'head1',
276             -content => qr/^AUTHORS?$/,
277             );
278              
279             =head1 DESCRIPTION
280              
281             The selectors provided by L<Pod::Elemental::Selectors> are fairly
282             limited, and there's no built-in way to combine them. For example,
283             there's no simple way to generate a selector that matches a section
284             with a specific name (a fairly common requirement).
285              
286             This module exports a single subroutine: C<make_selector>. It can
287             handle everything that Pod::Elemental::Selectors can do, plus many
288             things it can't. It also makes it easy to combine criteria. It
289             compiles all the criteria you supply into a single coderef.
290              
291             A selector is just a coderef that expects a single parameter: an
292             object that does Pod::Elemental::Paragraph. It returns a true value
293             if the paragraph meets the selector's criteria.
294              
295             =head1 CRITERIA
296              
297             Most criteria that accept a parameter accept a string, a regex, or an
298             arrayref of strings and/or regexes. However,
299             Pod::Elemental::MakeSelector I<does not> use Perl's C<~~> smartmatch
300             operator, because it is considered experimental. Instead, a limited
301             form of smartmatching is performed by the code generator. This means
302             arrayrefs are iterated when the selector is compiled. Modifying the
303             arrayref later will not affect the selector.
304              
305             Optional parameters must not begin with C<->, or they will be treated
306             as criteria instead. If you need an optional parameter that begins
307             with C<->, put it inside an arrayref.
308              
309             =head2 Simple Criteria
310              
311             -blank, # isa Pod::Elemental::Element::Generic::Blank
312             -flat, # does Pod::Elemental::Flat
313             -node, # does Pod::Elemental::Node
314              
315             =head2 Command Paragraphs
316              
317             -command, # does Pod::Elemental::Command
318             -command => 'head1', # and is =head1
319             -command => qr/^head[23]/, # and matches regex
320             -command => [qw(head1 head2)], # 1 element must match
321              
322             =head2 Content
323              
324             -content => 'AUTHOR', # matches =head1 AUTHOR
325             -content => qr/^AUTHORS?$/, # or =head2 AUTHORS
326             -content => [qw(AUTHOR BUGS)], # 1 element must match
327              
328             This criterion is normally used in conjunction with C<-command> to
329             select a section with a specific title.
330              
331             =head2 Regions
332              
333             -region, # isa Pod::Elemental::Element::Pod5::Region
334             -region => 'list', # and format_name eq 'list'
335             -region => qr/^list$/i, # and format_name matches regex
336             -region => [qw(list group)], # 1 element must match
337             -podregion => 'list', # =for :list
338             -nonpodregion => 'Pod::Coverage', # =for Pod::Coverage
339              
340             Regions are created with the C<=begin> or C<=for> commands. The
341             C<-podregion> and C<-nonpodregion> criteria work exactly like
342             C<-region>, but they ensure that C<is_pod> is either true or false,
343             respectively.
344              
345             =head2 Conjunctions
346              
347             -and => [ ... ], # all criteria must be true
348             -or => [ ... ], # at least one must be true
349              
350             These take an arrayref of criteria, and combine them using the
351             specified operator. Note that C<make_selector> does C<-and> by default;
352             S<C<make_selector @criteria>> is equivalent to
353             S<C<< make_selector -and => \@criteria >>>.
354              
355             =head2 Custom Criteria
356              
357             -code => sub { ... }, # test $_[0] any way you want
358             -code => $selector, # also accepts another selector
359              
360             =head1 SUBROUTINES
361              
362             =head2 make_selector
363              
364             $selector = make_selector( ... );
365              
366             C<make_selector> takes a list of criteria and returns a selector that
367             tests whether a supplied paragraph matches all the criteria. It does
368             not allow you to pass a paragraph to be checked immediately; if you
369             want to do that, then call the selector yourself. i.e., these two
370             lines are equivalent:
371              
372             s_command(head1 => $para); # From Pod::Elemental::Selectors
373             make_selector(qw(-command head1))->($para);
374              
375             =for Pod::Coverage add_value
376             build_selector
377             conjunction_action
378             join_expressions
379             region_action
380             required_parameter
381             smart_match
382             type_action
383              
384             =head1 SEE ALSO
385              
386             L<Pod::Elemental::Selectors> comes with L<Pod::Elemental>, but is much
387             more limited than this module.
388              
389             =head1 DEPENDENCIES
390              
391             Pod::Elemental::MakeSelector requires L<Pod::Elemental> and Perl 5.8.0
392             or later.
393              
394             =head1 BUGS
395              
396             Please report any bugs or feature requests to bug-pod-elemental-makeselector@rt.cpan.org or through the web interface at:
397             http://rt.cpan.org/Public/Dist/Display.html?Name=Pod-Elemental-MakeSelector
398              
399             =head1 AUTHOR
400              
401             Christopher J. Madsen <perl@cjmweb.net>
402              
403             =head1 SOURCE
404              
405             The development version is on github at L<http://github.com/madsen/pod-elemental-makeselector>
406             and may be cloned from L<git://github.com/madsen/pod-elemental-makeselector.git>
407              
408             =head1 COPYRIGHT AND LICENSE
409              
410             This software is copyright (c) 2015 by Christopher J. Madsen.
411              
412             This is free software; you can redistribute it and/or modify it under
413             the same terms as the Perl 5 programming language system itself.
414              
415             =cut