File Coverage

blib/lib/Pod/Elemental/Transformer/Pod5.pm
Criterion Covered Total %
statement 131 131 100.0
branch 40 42 95.2
condition 18 19 94.7
subroutine 21 21 100.0
pod 0 1 0.0
total 210 214 98.1


line stmt bran cond sub pod time code
1             package Pod::Elemental::Transformer::Pod5 0.103006;
2             # ABSTRACT: the default, minimal semantics of Perl5's pod element hierarchy
3              
4 11     11   1067 use Moose;
  11         23  
  11         78  
5             with 'Pod::Elemental::Transformer';
6              
7             #pod =head1 SYNOPSIS
8             #pod
9             #pod Pod::Elemental::Transformer::Pod5->new->transform_node($pod_elem_document);
10             #pod
11             #pod ...and that's it.
12             #pod
13             #pod =head1 OVERVIEW
14             #pod
15             #pod The Pod5 transformer is meant to be used to convert the result of a "stock"
16             #pod Pod::Elemental::Document into something simpler to work with. It assumes that
17             #pod the document conforms more or less to the convetions laid out in L<perlpod> and
18             #pod L<perlpodspec>. It is not very strict, and makes very few assumptions,
19             #pod described here:
20             #pod
21             #pod =over 4
22             #pod
23             #pod =item * =begin/=end and =for enclose or produce regions within the document
24             #pod
25             #pod =item * regions are associated with format names; format names that begin with a colon enclose more pod-like content
26             #pod
27             #pod =item * regions nest strictly; all inner regions must end before outer regions
28             #pod
29             #pod =item * paragraphs in non-pod regions are "data" paragraphs
30             #pod
31             #pod =item * non-data paragraphs that start with spaces are "verbatim" paragraphs
32             #pod
33             #pod =item * groups of data or verbatim paragraphs can be consolidated
34             #pod
35             #pod =back
36             #pod
37             #pod Further, all elements are replaced with equivalent elements that perform the
38             #pod L<Pod::Elemental::Autoblank> role, so all "blank" events can be removed form
39             #pod the tree and ignored.
40             #pod
41             #pod =head1 CONFIGURATION
42             #pod
43             #pod None. For now, it just does the same thing every time with no configuration or
44             #pod options.
45             #pod
46             #pod =cut
47              
48 11     11   69613 use namespace::autoclean;
  11         24  
  11         97  
49              
50 11     11   1488 use Pod::Elemental::Document;
  11         21  
  11         258  
51 11     11   4652 use Pod::Elemental::Element::Pod5::Command;
  11         37  
  11         389  
52 11     11   5310 use Pod::Elemental::Element::Pod5::Data;
  11         40  
  11         449  
53 11     11   5112 use Pod::Elemental::Element::Pod5::Nonpod;
  11         70  
  11         373  
54 11     11   5149 use Pod::Elemental::Element::Pod5::Ordinary;
  11         37  
  11         392  
55 11     11   5325 use Pod::Elemental::Element::Pod5::Verbatim;
  11         37  
  11         398  
56 11     11   5435 use Pod::Elemental::Element::Pod5::Region;
  11         40  
  11         458  
57              
58 11     11   5397 use Pod::Elemental::Selectors -all;
  11         31  
  11         58  
59              
60 629     629   2026 sub _gen_class { "Pod::Elemental::Element::Generic::$_[1]" }
61 881     881   3840 sub _class { "Pod::Elemental::Element::Pod5::$_[1]" }
62              
63             sub _region_para_parts {
64 40     40   98 my ($self, $para) = @_;
65              
66 40         1125 my ($colon, $target, $content, $nl) = $para->content =~ m/
67             \A
68             (:)?
69             (\S+)
70             (?:\s+(.+))?
71             (\s+)\z
72             /xsm;
73              
74 40 50       102 confess("=begin cannot be parsed") unless defined $target;
75              
76 40   100     158 $colon ||= '';
77 40   100     140 $content ||= '';
78              
79 40         142 return ($colon, $target, "$content$nl");
80             }
81              
82             sub __extract_region {
83 13     13   35 my ($self, $name, $in_paras) = @_;
84              
85 13         47 my %nest = ($name => 1);
86 13         21 my @region_paras;
87              
88 13         51 REGION_PARA: while (my $region_para = shift @$in_paras) {
89 157 100       363 if (s_command([ qw(begin end) ], $region_para)) {
90 25         75 my ($r_colon, $r_target) = $self->_region_para_parts($region_para);
91              
92 25         80 for ($nest{ "$r_colon$r_target" }) {
93 25 100       702 $_ += $region_para->command eq 'begin' ? 1 : -1;
94              
95 25 50       70 confess("=end $r_colon$r_target without matching begin") if $_ < 0;
96              
97 25 100 100     141 last REGION_PARA if !$_ and "$r_colon$r_target" eq $name;
98             }
99             }
100              
101 144         5043 push @region_paras, $region_para;
102             };
103              
104 13         387 return \@region_paras;
105             }
106              
107             sub _upgrade_nonpod {
108 12     12   35 my ($self, $in_paras) = @_;
109              
110             @$in_paras = map {
111 12 100       35 $_->isa( $self->_gen_class('Nonpod') )
  233         359  
112             ? $self->_class('Nonpod')->new({
113             content => $_->content,
114             })
115             : $_
116             } @$in_paras;
117             }
118              
119             sub _collect_regions {
120 25     25   66 my ($self, $in_paras) = @_;
121              
122 25         37 my @out_paras;
123              
124 25         77 my $s_region = s_command([ qw(begin for) ]);
125 25         73 my $region_class = $self->_class('Region');
126              
127 25         43 PARA: while (my $para = shift @{ $in_paras }) {
  220         6425  
128 195 100       352 push(@out_paras, $para), next PARA unless $s_region->($para);
129              
130 15 100       438 if ($para->command eq 'for') {
131             # factor out (for vertical space if nothing else) -- rjbs, 2009-10-20
132 2         8 my ($colon, $target, $content) = $self->_region_para_parts($para);
133              
134 2 100       6 my $region = $region_class->new({
135             children => [
136             $self->_gen_class('Text')->new({ content => $content }),
137             ],
138             format_name => $target,
139             is_pod => $colon ? 1 : 0,
140             content => "\n",
141             });
142              
143 2         7 push @out_paras, $region;
144 2         59 next PARA;
145             }
146              
147 13         50 my ($colon, $target, $content) = $self->_region_para_parts($para);
148              
149 13         79 my $region_paras = $self->__extract_region("$colon$target", $in_paras);
150              
151 13         48 shift @$region_paras while s_blank($region_paras->[0]);
152 13   100     68 pop @$region_paras while @$region_paras && s_blank($region_paras->[-1]);
153              
154 13 100       97 my $region = $region_class->new({
155             children => $self->_collect_regions($region_paras),
156             format_name => $target,
157             is_pod => $colon ? 1 : 0,
158             content => $content,
159             });
160              
161 13         449 push @out_paras, $region;
162             }
163              
164 25         73 @$in_paras = @out_paras;
165              
166 25         593 return $in_paras;
167             }
168              
169             sub _strip_markers {
170 12     12   34 my ($self, $in_paras) = @_;
171              
172 12         30 @$in_paras = grep { ! s_command([ qw(cut pod) ], $_) } @$in_paras;
  262         7067  
173 12   66     167 shift @$in_paras while @$in_paras and s_blank($in_paras->[0]);
174             }
175              
176             sub _autotype_paras {
177 27     27   75 my ($self, $paras, $is_pod) = @_;
178              
179             @$paras = map {
180 27         62 my $elem = $_;
  197         247  
181 197 100       360 if ($elem->isa( $self->_gen_class('Text') )) {
182 48 100       1007 my $class = $is_pod
    100          
183             ? $elem->content =~ /\A\s/
184             ? $self->_class('Verbatim')
185             : $self->_class('Ordinary')
186             : $self->_class('Data');
187              
188 48         1359 $elem = $class->new({ content => $elem->content });
189             }
190              
191 197 100       413 if ($elem->isa( $self->_class('Region') )) {
192 15         403 $self->_autotype_paras( $elem->children, $elem->is_pod );
193             }
194              
195 197 100       368 if ($elem->isa( $self->_gen_class('Command') )) {
196 40         93 $elem = $self->_class('Command')->new({
197             command => $elem->command,
198             content => $elem->content,
199             });
200             }
201              
202 197         981 $elem;
203              
204             } @$paras;
205             }
206              
207             sub __text_class {
208 145     145   210 my ($self, $para) = @_;
209              
210 145         196 for my $type (qw(Verbatim Data)) {
211 286         406 my $class = $self->_class($type);
212 286 100       758 return $class if $para->isa($class);
213             }
214              
215 135         198 return;
216             }
217              
218             sub _collect_runs {
219 27     27   60 my ($self, $paras) = @_;
220              
221             $self->_collect_runs($_->children)
222 27         59 foreach grep { $_->isa( $self->_class('Region') ) } @$paras;
  197         294  
223              
224 27         85 PASS: for my $start (0 .. $#$paras) {
225 171 100       323 last PASS if $#$paras - $start < 2; # we need X..Blank..X at minimum
226              
227 145         248 my $class = $self->__text_class( $paras->[ $start ] );
228 145 100       285 next PASS unless $class;
229              
230 10         25 my @to_collect = ($start);
231 10         30 NEXT: for my $next ($start+1 .. $#$paras) {
232 26 100 100     117 if ($paras->[ $next ]->isa($class) or s_blank($paras->[ $next ])) {
233 19         43 push @to_collect, $next;
234 19         39 next NEXT;
235             }
236              
237 7         17 last NEXT;
238             }
239              
240 10         32 pop @to_collect while s_blank($paras->[ $to_collect[ -1 ] ]);
241              
242 10 100       43 next PASS unless @to_collect >= 3;
243              
244             my $new_content = join(qq{\n},
245 4         13 map { $_ = $_->content; chomp; $_ } @$paras[@to_collect]
  16         450  
  16         427  
  16         45  
246             );
247              
248 4         132 splice @$paras, $start, scalar(@to_collect), $class->new({
249             content => $new_content,
250             });
251              
252 4         12 redo PASS;
253             }
254              
255 27         47 my @out;
256 27         74 PASS: for (my $i = 0; $i < @$paras; $i++) {
257 101         161 my $this = $paras->[$i];
258 101         137 push @out, $this;
259              
260 101   100     336 while ($paras->[$i+1] and s_blank($paras->[$i+1])) {
261 84         126 $i++;
262 84 100       142 next unless $this->isa( $self->_class('Data') );
263 3         97 $this->content( $this->content . $paras->[$i]->content );
264             }
265             }
266              
267             # @out = grep { not s_blank($_) } @$paras;
268              
269             # I really don't feel bad about rewriting in place by the time we get here.
270             # These are private methods, and I know the consequence of calling them.
271             # Nobody else should be. So there. -- rjbs, 2009-10-17
272 27         621 @$paras = @out;
273 27         92 return \@out;
274             }
275              
276             sub transform_node {
277 12     12 0 105 my ($self, $node) = @_;
278              
279 12         353 $self->_strip_markers($node->children);
280 12         343 $self->_upgrade_nonpod($node->children);
281 12         335 $self->_collect_regions($node->children);
282 12         342 $self->_autotype_paras($node->children, 1);
283 12         310 $self->_collect_runs($node->children);
284              
285 12         37 return $node;
286             }
287              
288             __PACKAGE__->meta->make_immutable;
289              
290             1;
291              
292             __END__
293              
294             =pod
295              
296             =encoding UTF-8
297              
298             =head1 NAME
299              
300             Pod::Elemental::Transformer::Pod5 - the default, minimal semantics of Perl5's pod element hierarchy
301              
302             =head1 VERSION
303              
304             version 0.103006
305              
306             =head1 SYNOPSIS
307              
308             Pod::Elemental::Transformer::Pod5->new->transform_node($pod_elem_document);
309              
310             ...and that's it.
311              
312             =head1 OVERVIEW
313              
314             The Pod5 transformer is meant to be used to convert the result of a "stock"
315             Pod::Elemental::Document into something simpler to work with. It assumes that
316             the document conforms more or less to the convetions laid out in L<perlpod> and
317             L<perlpodspec>. It is not very strict, and makes very few assumptions,
318             described here:
319              
320             =over 4
321              
322             =item * =begin/=end and =for enclose or produce regions within the document
323              
324             =item * regions are associated with format names; format names that begin with a colon enclose more pod-like content
325              
326             =item * regions nest strictly; all inner regions must end before outer regions
327              
328             =item * paragraphs in non-pod regions are "data" paragraphs
329              
330             =item * non-data paragraphs that start with spaces are "verbatim" paragraphs
331              
332             =item * groups of data or verbatim paragraphs can be consolidated
333              
334             =back
335              
336             Further, all elements are replaced with equivalent elements that perform the
337             L<Pod::Elemental::Autoblank> role, so all "blank" events can be removed form
338             the tree and ignored.
339              
340             =head1 PERL VERSION
341              
342             This library should run on perls released even a long time ago. It should work
343             on any version of perl released in the last five years.
344              
345             Although it may work on older versions of perl, no guarantee is made that the
346             minimum required version will not be increased. The version may be increased
347             for any reason, and there is no promise that patches will be accepted to lower
348             the minimum required perl.
349              
350             =head1 CONFIGURATION
351              
352             None. For now, it just does the same thing every time with no configuration or
353             options.
354              
355             =head1 AUTHOR
356              
357             Ricardo SIGNES <cpan@semiotic.systems>
358              
359             =head1 COPYRIGHT AND LICENSE
360              
361             This software is copyright (c) 2022 by Ricardo SIGNES.
362              
363             This is free software; you can redistribute it and/or modify it under
364             the same terms as the Perl 5 programming language system itself.
365              
366             =cut