File Coverage

blib/lib/Stenciller.pm
Criterion Covered Total %
statement 106 108 98.1
branch 40 48 83.3
condition 16 28 57.1
subroutine 21 21 100.0
pod 1 5 20.0
total 184 210 87.6


line stmt bran cond sub pod time code
1 7     7   519088 use 5.10.1;
  7         80  
2 7     7   34 use strict;
  7         11  
  7         182  
3 7     7   30 use warnings;
  7         10  
  7         376  
4              
5             package Stenciller;
6              
7             our $VERSION = '0.1400'; # VERSION:
8             # ABSTRACT: Transforms a flat file format to different output
9              
10 7     7   3314 use Moose;
  7         2751104  
  7         45  
11             with 'Stenciller::Utils';
12 7     7   47350 use MooseX::AttributeDocumented;
  7         42831  
  7         24  
13 7     7   771836 use namespace::autoclean;
  7         15  
  7         43  
14              
15 7     7   4746 use Module::Pluggable search_path => ['Stenciller::Plugin'];
  7         50016  
  7         52  
16 7     7   3501 use Module::Load;
  7         6663  
  7         40  
17 7     7   400 use Carp 'croak';
  7         15  
  7         291  
18 7     7   37 use List::Util qw/any/;
  7         12  
  7         334  
19 7     7   2723 use PerlX::Maybe qw/maybe provided/;
  7         14513  
  7         26  
20 7     7   3908 use Types::Standard qw/Maybe Bool ArrayRef Str/;
  7         451816  
  7         72  
21 7     7   10353 use Types::Path::Tiny qw/File/;
  7         211183  
  7         56  
22 7     7   5375 use Types::Stenciller qw/Stencil/;
  7         96  
  7         56  
23 7     7   4930 use Stenciller::Stencil;
  7         31  
  7         9344  
24              
25             has filepath => (
26             is => 'ro',
27             isa => File,
28             required => 1,
29             coerce => 1,
30             documentation => 'The textfile to parse.',
31             );
32             has is_utf8 => (
33             is => 'ro',
34             isa => Bool,
35             default => 1,
36             documentation => 'Determines how the stencil file is read.'
37             );
38             has stencils => (
39             is => 'ro',
40             isa => ArrayRef[Stencil],
41             traits => ['Array'],
42             default => sub { [ ] },
43             documentation => 'After parsing, this contains all parsed stencils.',
44             init_arg => undef,
45             handles => {
46             add_stencil => 'push',
47             all_stencils => 'elements',
48             get_stencil => 'get',
49             count_stencils => 'count',
50             has_stencils => 'count',
51             },
52             );
53             has header_lines => (
54             is => 'ro',
55             isa => ArrayRef[Str],
56             traits => ['Array'],
57             default => sub { [] },
58             init_arg => undef,
59             documentation => 'After parsing, this contains all lines in the header.',
60             handles => {
61             add_header_line => 'push',
62             all_header_lines => 'elements',
63             },
64             );
65             has skip_if_input_empty => (
66             is => 'ro',
67             isa => Bool,
68             default => 1,
69             documentation => 'If a stencil has no input content, skip entire stencil.',
70             );
71             has skip_if_output_empty => (
72             is => 'ro',
73             isa => Bool,
74             default => 1,
75             documentation => 'If a stencil has no output content, skip entire stencil.',
76             );
77              
78             around BUILDARGS => sub {
79             my $next = shift;
80             my $class = shift;
81             my @args = @_;
82              
83             if(scalar @args == 1 && ref $args[0] eq 'HASH') {
84             $class->$next(%{ $args[0] });
85             }
86             else {
87             $class->$next(@args);
88             }
89             };
90              
91             sub BUILD {
92 7     7 0 28 shift->parse;
93             }
94             around has_stencils => sub {
95             my $next = shift;
96             my $self = shift;
97              
98             my $count = $self->$next;
99             return !!$count || 0;
100             };
101              
102             # Str :$plugin_name! does doc('Plugin that will generate output.'),
103             # HashRef :$constructor_args? does doc('Constructor arguments for the plugin.') = {},
104             # HashRef :$transform_args? does doc('Settings for the specific transformation.') = {}, ...
105             # --> Str but assumed does doc('The transformed content.')
106             sub transform {
107 7     7 1 16 my $self = shift;
108 7         29 my %args = @_;
109              
110 7   33     28 my $plugin_name = $args{'plugin_name'} || carp('plugin_name is a mandatory key in the call to transform()');
111 7   100     31 my $constructor_args = $args{'constructor_args'} || {};
112 7   100     27 my $transform_args = $args{'transform_args'} || {};
113              
114 7         24 my $plugin_class = "Stenciller::Plugin::$plugin_name";
115 7         39 Module::Load::load($plugin_class);
116 7 50       178 die sprintf "Can't load %s: %s", $plugin_class, $@ if $@;
117              
118 7 50       56 if(!$plugin_class->does('Stenciller::Transformer')) {
119 0         0 croak("[$plugin_name] doesn't do the Stenciller::Transformer role. Quitting.");
120             }
121 7         1788 return $plugin_class->new(stenciller => $self, %{ $constructor_args })->transform($transform_args);
  7         203  
122             }
123              
124             sub parse {
125 7     7 0 17 my $self = shift;
126 7 50       166 my @contents = split /\v/ => $self->is_utf8 ? $self->filepath->slurp_utf8 : $self->filepath->slurp;
127              
128 7         7366 my $stencil_start = qr/^== +stencil +(\{.*\} +)?==$/;
129 7         31 my $input_start = qr/^--+input--+$/;
130 7         21 my $input_end = qr/^--+end input--+$/;
131 7         20 my $output_start = qr/^--+output--+$/;
132 7         22 my $output_end = qr/^--+end output--+$/;
133              
134 7         14 my $environment = 'header';
135 7         14 my $line_count = 0;
136              
137 7         14 my $stencil = undef;
138              
139             LINE:
140 7         20 foreach my $line (@contents) {
141 276 100       524 ++$line_count if $environment ne 'next_stencil'; # because then we are redo-ing the line
142              
143 276 100   524   851 if(any { $environment eq $_ } (qw/header next_stencil/)) {
  524 100       1011  
    100          
    100          
    100          
    50          
144 36 100 50     792 $self->add_header_line($line) and next LINE if $line !~ $stencil_start;
145              
146 15         39 my $possible_hash = $1;
147 15 100 66     329 my $settings = defined $possible_hash && $possible_hash =~ m{\{.*\}}
148             ? $self->eval_to_hashref($possible_hash, $self->filepath)
149             : {}
150             ;
151              
152 15 100       288 my $stencil_name = exists $settings->{'name'} ? delete $settings->{'name'} : $self->filepath->basename(qr/\..*/) . "-$line_count";
153 15         523 $stencil_name =~ s{[. -]}{_}g;
154 15         38 $stencil_name =~ s{[^a-zA-Z0-9_]}{}g;
155              
156             $stencil = Stenciller::Stencil->new(
157             stencil_name => $stencil_name,
158             loop_values => delete $settings->{'loop'},
159             line_number => $line_count,
160             maybe skip => delete $settings->{'skip'},
161 15         60 provided scalar keys %{ $settings }, extra_settings => $settings,
  15         178  
162             );
163 15         72 $environment = 'before_input';
164             }
165             elsif($environment eq 'before_input') {
166 56 100 50     351 $stencil->add_before_input($line) and next LINE if $line !~ $input_start;
167 15         41 $environment = 'input';
168             }
169             elsif($environment eq 'input') {
170 56 100 50     324 $stencil->add_input($line) and next LINE if $line !~ $input_end;
171 15         47 $environment = 'between';
172             }
173             elsif($environment eq 'between') {
174 53 100 50     309 $stencil->add_between($line) and next LINE if $line !~ $output_start;
175 15         39 $environment = 'output';
176             }
177             elsif($environment eq 'output') {
178 56 100 50     327 $stencil->add_output($line) and next LINE if $line !~ $output_end;
179 15         40 $environment = 'after_output';
180             }
181             elsif($environment eq 'after_output') {
182 19 100 50     126 $stencil->add_after_output($line) and next LINE if $line !~ $stencil_start;
183 8         28 $self->handle_completed_stencil($stencil);
184 8         10 $environment = 'next_stencil';
185 8         24 redo LINE;
186             }
187             }
188 7 50       28 if($environment ne 'after_output') {
189 0         0 croak (sprintf 'File <%s> appears malformed. Ended on <%s>', $self->filepath, $environment);
190             }
191 7         31 $self->handle_completed_stencil($stencil);
192             }
193              
194             sub handle_completed_stencil {
195 15     15 0 41 my $self = shift;
196 15         20 my $stencil = shift;
197              
198 15 50       52 return if !Stencil->check($stencil);
199 15 50       734 return if $stencil->skip;
200 15 100 66     429 return if !$stencil->has_input && $self->skip_if_input_empty;
201 13 50 33     375 return if !$stencil->has_output && $self->skip_if_output_empty;
202              
203 13 100       374 if(!$stencil->has_loop_values) {
204 12         370 $self->add_stencil($stencil);
205 12         154 return;
206             }
207              
208 1         31 foreach my $loop_value ($stencil->all_loop_values) {
209 2         7 my $clone = $stencil->clone_with_loop_value($loop_value);
210 2         62 $self->add_stencil($clone);
211             }
212             }
213              
214             sub max_stencil_index {
215 9     9 0 253 return shift->count_stencils - 1;
216             }
217              
218             __PACKAGE__->meta->make_immutable;
219              
220             1;
221              
222             __END__
223              
224             =pod
225              
226             =encoding UTF-8
227              
228             =head1 NAME
229              
230             Stenciller - Transforms a flat file format to different output
231              
232              
233              
234             =begin HTML
235              
236             <p><img src="https://img.shields.io/badge/perl-5.10.1+-brightgreen.svg" alt="Requires Perl 5.10.1+" /> <a href="https://travis-ci.org/Csson/p5-Stenciller"><img src="https://api.travis-ci.org/Csson/p5-Stenciller.svg?branch=master" alt="Travis status" /></a> <img src="https://img.shields.io/badge/coverage-90.0%-yellow.svg" alt="coverage 90.0%" /></p>
237              
238             =end HTML
239              
240              
241             =begin markdown
242              
243             ![Requires Perl 5.10.1+](https://img.shields.io/badge/perl-5.10.1+-brightgreen.svg) [![Travis status](https://api.travis-ci.org/Csson/p5-Stenciller.svg?branch=master)](https://travis-ci.org/Csson/p5-Stenciller) ![coverage 90.0%](https://img.shields.io/badge/coverage-90.0%-yellow.svg)
244              
245             =end markdown
246              
247             =head1 VERSION
248              
249             Version 0.1400, released 2016-02-03.
250              
251              
252              
253             =head1 SYNOPSIS
254              
255             use Stenciller;
256             my $stenciller = Stenciller->new(filepath => 't/corpus/test-1.stencil');
257             my $content = $stenciller->transform(plugin_name => 'ToUnparsedText');
258              
259             =head1 DESCRIPTION
260              
261             Stenciller reads a special fileformat and provides a way to convert the content into different types of output. For example, it can be used to create documentation and tests from the same source file.
262              
263             =head2 File format
264              
265             == stencil {} ==
266              
267             --input--
268              
269             --end input--
270              
271             --output--
272              
273             --end output--
274              
275             This is the basic layout. A stencil ends when a new stencil block is discovered (there is no fixed limit to the number of stencils in a file). The (optional) hash is for settings. Each stencil has five parts: C<before_input>, C<input>, C<between>, C<output> and C<after_output>. In addition to this
276             there is a header before the first stencil.
277              
278             =head1 ATTRIBUTES
279              
280              
281             =head2 filepath
282              
283             =begin HTML
284              
285             <table cellpadding="0" cellspacing="0">
286             <tr>
287             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Path::Tiny#File">File</a></td>
288             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">required</td>
289             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
290             </tr>
291             </table>
292              
293             <p>The textfile to parse.</p>
294              
295             =end HTML
296              
297             =begin markdown
298              
299             <table cellpadding="0" cellspacing="0">
300             <tr>
301             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Path::Tiny#File">File</a></td>
302             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">required</td>
303             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
304             </tr>
305             </table>
306              
307             <p>The textfile to parse.</p>
308              
309             =end markdown
310              
311             =head2 is_utf8
312              
313             =begin HTML
314              
315             <table cellpadding="0" cellspacing="0">
316             <tr>
317             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#Bool">Bool</a></td>
318             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">optional, default: <code>1</code></td>
319             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
320             </tr>
321             </table>
322              
323             <p>Determines how the stencil file is read.</p>
324              
325             =end HTML
326              
327             =begin markdown
328              
329             <table cellpadding="0" cellspacing="0">
330             <tr>
331             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#Bool">Bool</a></td>
332             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">optional, default: <code>1</code></td>
333             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
334             </tr>
335             </table>
336              
337             <p>Determines how the stencil file is read.</p>
338              
339             =end markdown
340              
341             =head2 skip_if_input_empty
342              
343             =begin HTML
344              
345             <table cellpadding="0" cellspacing="0">
346             <tr>
347             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#Bool">Bool</a></td>
348             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">optional, default: <code>1</code></td>
349             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
350             </tr>
351             </table>
352              
353             <p>If a stencil has no input content, skip entire stencil.</p>
354              
355             =end HTML
356              
357             =begin markdown
358              
359             <table cellpadding="0" cellspacing="0">
360             <tr>
361             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#Bool">Bool</a></td>
362             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">optional, default: <code>1</code></td>
363             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
364             </tr>
365             </table>
366              
367             <p>If a stencil has no input content, skip entire stencil.</p>
368              
369             =end markdown
370              
371             =head2 skip_if_output_empty
372              
373             =begin HTML
374              
375             <table cellpadding="0" cellspacing="0">
376             <tr>
377             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#Bool">Bool</a></td>
378             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">optional, default: <code>1</code></td>
379             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
380             </tr>
381             </table>
382              
383             <p>If a stencil has no output content, skip entire stencil.</p>
384              
385             =end HTML
386              
387             =begin markdown
388              
389             <table cellpadding="0" cellspacing="0">
390             <tr>
391             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#Bool">Bool</a></td>
392             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">optional, default: <code>1</code></td>
393             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
394             </tr>
395             </table>
396              
397             <p>If a stencil has no output content, skip entire stencil.</p>
398              
399             =end markdown
400              
401             =head2 header_lines
402              
403             =begin HTML
404              
405             <table cellpadding="0" cellspacing="0">
406             <tr>
407             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#Str">Str</a> ]</td>
408             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
409             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
410             </tr>
411             </table>
412              
413             <p>After parsing, this contains all lines in the header.</p>
414              
415             =end HTML
416              
417             =begin markdown
418              
419             <table cellpadding="0" cellspacing="0">
420             <tr>
421             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#Str">Str</a> ]</td>
422             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
423             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
424             </tr>
425             </table>
426              
427             <p>After parsing, this contains all lines in the header.</p>
428              
429             =end markdown
430              
431             =head2 stencils
432              
433             =begin HTML
434              
435             <table cellpadding="0" cellspacing="0">
436             <tr>
437             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Stenciller#Stencil">Stencil</a> ]</td>
438             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
439             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
440             </tr>
441             </table>
442              
443             <p>After parsing, this contains all parsed stencils.</p>
444              
445             =end HTML
446              
447             =begin markdown
448              
449             <table cellpadding="0" cellspacing="0">
450             <tr>
451             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Stenciller#Stencil">Stencil</a> ]</td>
452             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
453             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td>
454             </tr>
455             </table>
456              
457             <p>After parsing, this contains all parsed stencils.</p>
458              
459             =end markdown
460              
461             =head1 METHODS
462              
463             =head2 transform
464              
465             $stenciller->transform(
466             plugin_name => 'ToUnparsedText',
467             constructor_args => {
468             plugin_specific_args => ...,
469             },
470             tranform_args => {
471             transformation_specific_args => ...,
472             },
473             );
474              
475             C<plugin_name> is mandatory and should be a class under the C<Stenciller::Plugin> namespace.
476              
477             C<constructor_args> is optional. This hash reference will be passed on to the plugin constructor. Valid keys depends on the plugin.
478              
479             C<transform_args> is optional. This hash reference will be passed on to the C<transform> method in the plugin. Valid keys depends on the plugin.
480              
481             =head1 PLUGINS
482              
483             The actual transforming is done by plugins. There are two plugins bundled in this distribution:
484              
485             =over 4
486              
487             =item *
488              
489             L<Stenciller::Plugin::ToUnparsedText>
490              
491             =item *
492              
493             L<Stenciller::Plugin::ToHtmlPreBlock>
494              
495             =item *
496              
497             L<Pod::Elemental::Transformer::Stenciller>
498              
499             =back
500              
501             Custom plugins should be in the L<Stenciller::Plugin> namespace and consume the L<Stenciller::Transformer> role.
502              
503             =head1 SOURCE
504              
505             L<https://github.com/Csson/p5-Stenciller>
506              
507             =head1 HOMEPAGE
508              
509             L<https://metacpan.org/release/Stenciller>
510              
511             =head1 AUTHOR
512              
513             Erik Carlsson <info@code301.com>
514              
515             =head1 COPYRIGHT AND LICENSE
516              
517             This software is copyright (c) 2016 by Erik Carlsson.
518              
519             This is free software; you can redistribute it and/or modify it under
520             the same terms as the Perl 5 programming language system itself.
521              
522             =cut