File Coverage

blib/lib/Pod/PluginCatalog.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             #---------------------------------------------------------------------
2             package Pod::PluginCatalog;
3             #
4             # Copyright 2012 Christopher J. Madsen
5             #
6             # Author: Christopher J. Madsen <perl@cjmweb.net>
7             # Created: 18 Jul 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: Format a catalog of plugin modules
18             #---------------------------------------------------------------------
19              
20 1     1   19123 use 5.010;
  1         2  
  1         32  
21 1     1   253 use Moose;
  0            
  0            
22             use namespace::autoclean;
23              
24             our $VERSION = '0.02';
25             # This file is part of Pod-PluginCatalog 0.02 (January 3, 2015)
26              
27             use autodie ':io';
28             use Encode ();
29             use Pod::PluginCatalog::Entry ();
30             use Pod::Elemental ();
31             use Pod::Elemental::Selectors qw(s_command s_flat);
32             use Pod::Elemental::Transformer::Nester ();
33             use Text::Template ();
34              
35             #=====================================================================
36              
37              
38             has namespace_rewriter => (
39             is => 'ro',
40             isa => 'CodeRef',
41             required => 1,
42             );
43              
44             has pod_formatter => (
45             is => 'ro',
46             isa => 'CodeRef',
47             required => 1,
48             );
49              
50             has _plugins => (
51             is => 'ro',
52             isa => 'HashRef[Pod::PluginCatalog::Entry]',
53             default => sub { {} },
54             );
55              
56             has _tags => (
57             is => 'ro',
58             isa => 'HashRef[Str]',
59             default => sub { {} },
60             traits => ['Hash'],
61             handles => {
62             tags => 'keys',
63             },
64             );
65              
66             has _author_selector => (
67             is => 'ro',
68             lazy => 1,
69             builder => '_build_author_selector',
70             );
71              
72             sub _build_author_selector { s_command('author') }
73              
74             has _plugin_selector => (
75             is => 'ro',
76             lazy => 1,
77             builder => '_build_plugin_selector',
78             );
79              
80             sub _build_plugin_selector { s_command('plugin') }
81              
82             has _tag_selector => (
83             is => 'ro',
84             lazy => 1,
85             builder => '_build_tag_selector',
86             );
87              
88             sub _build_tag_selector { s_command('tag') }
89              
90             has _nester => (
91             is => 'ro',
92             lazy => 1,
93             builder => '_build_nester',
94             );
95              
96             sub _build_nester
97             {
98             Pod::Elemental::Transformer::Nester->new({
99             top_selector => s_command(['plugin', 'tag']),
100             content_selectors => [
101             s_command([ qw(head3 head4 over item back) ]),
102             s_flat,
103             ],
104             });
105             }
106              
107              
108             has delimiters => (
109             is => 'ro',
110             isa => 'ArrayRef',
111             lazy => 1,
112             default => sub { [ qw( {{ }} ) ] },
113             );
114              
115             has file_extension => (
116             is => 'ro',
117             isa => 'Str',
118             default => '.html',
119             );
120              
121             has perlio_layers => (
122             is => 'ro',
123             isa => 'Str',
124             default => ':utf8',
125             );
126              
127             #=====================================================================
128             sub _err
129             {
130             my ($source, $node, $err) = @_;
131              
132             my $line = $node->start_line;
133             $line = ($line ? "$line:" : '');
134             confess "$source:$line $err";
135             } # end _err
136             #---------------------------------------------------------------------
137              
138              
139             sub add_file
140             {
141             my ($self, $filename) = @_;
142              
143             $self->add_document($filename => Pod::Elemental->read_file($filename));
144             } # end add_file
145             #---------------------------------------------------------------------
146              
147              
148             sub add_document
149             {
150             my ($self, $source, $doc) = @_;
151              
152             my $plugins = $self->_plugins;
153             my $tags = $self->_tags;
154             my $rewriter = $self->namespace_rewriter;
155             my $author_selector = $self->_author_selector;
156             my $plugin_selector = $self->_plugin_selector;
157             my $tag_selector = $self->_tag_selector;
158              
159             $self->_nester->transform_node($doc);
160              
161             my @author;
162              
163             foreach my $node (@{ $doc->children }) {
164             if ($author_selector->($node)) {
165             my $author = $node->content;
166             chomp $author;
167             if (length $author) {
168             @author = (author => $author);
169             } else {
170             @author = ();
171             }
172             } elsif ($tag_selector->($node)) {
173             my $tag = $node->content;
174             _err($source, $node, "=tag without tag name") unless length $tag;
175             _err($source, $node, "Duplicate description for tag $tag")
176             if defined $tags->{$tag};
177             $tags->{$tag} = $self->format_description($node);
178             } elsif ($plugin_selector->($node)) {
179             my ($name, @tags) = split(' ', $node->content);
180              
181             _err($source, $node, "Plugin $name has no tags") unless @tags;
182              
183             _err($source, $node, "Plugin $name already seen in " .
184             ($plugins->{$name}->source_file // 'unknown file'))
185             if $plugins->{$name};
186              
187             $tags->{$_} //= undef for @tags;
188              
189             my $module = $rewriter->($name);
190              
191             $plugins->{$name} = Pod::PluginCatalog::Entry->new(
192             name => $name, module => $module,
193             description => $self->format_description($node),
194             source_file => $source, tags => \@tags,
195             @author,
196             );
197             }
198             } # end foreach $node
199              
200             } # end add_document
201             #---------------------------------------------------------------------
202              
203             sub format_description
204             {
205             my ($self, $node) = @_;
206              
207             my $pod = join('', map { $_->as_pod_string } @{ $node->children });
208              
209             $self->pod_formatter->("=pod\n\n$pod");
210             } # end format_description
211             #---------------------------------------------------------------------
212              
213              
214             sub generate_tag_pages
215             {
216             my ($self, $header, $template, $footer) = @_;
217              
218             $self->compile_templates($header, $template, $footer);
219              
220             $self->generate_tag_page($_, $header, $template, $footer)
221             for sort $self->tags;
222             } # end generate_tag_pages
223             #---------------------------------------------------------------------
224              
225             sub generate_tag_page
226             {
227             my ($self, $tag, $header, $template, $footer) = @_;
228              
229             confess "index is a reserved name" if $tag eq 'index';
230              
231             my %data = (tag => $tag, tag_description => $self->_tags->{$tag});
232              
233             warn "No description for tag $tag\n" unless $data{tag_description};
234              
235             my @plugins = sort { $a->name cmp $b->name }
236             grep { $_->has_tag($tag) }
237             values %{ $self->_plugins };
238              
239             unless (@plugins) {
240             warn "No plugins for tag $tag\n";
241             return;
242             }
243              
244             open(my $out, '>' . $self->perlio_layers, $tag . $self->file_extension);
245              
246             $header->fill_in(HASH => \%data, OUTPUT => $out)
247             or confess("Filling in the header template failed for $tag");
248              
249             for my $plugin (@plugins) {
250             my %data = (
251             %data,
252             other_tags => [ $plugin->other_tags($tag) ],
253             map { $_ => $plugin->$_() } qw(name module description author)
254             );
255              
256             $template->fill_in(HASH => \%data, OUTPUT => $out)
257             or confess("Filling in the entry template failed for $data{name}");
258             }
259              
260             $footer->fill_in(HASH => \%data, OUTPUT => $out)
261             or confess("Filling in the footer template failed for $tag");
262              
263             close $out;
264             } # end generate_tag_page
265             #---------------------------------------------------------------------
266              
267              
268             sub generate_index_page
269             {
270             my ($self, $header, $template, $footer) = @_;
271              
272             $self->compile_templates($header, $template, $footer);
273              
274             open(my $out, '>' . $self->perlio_layers, 'index' . $self->file_extension);
275              
276             my %data = (tag => undef, tag_description => undef);
277              
278             $header->fill_in(HASH => \%data, OUTPUT => $out)
279             or confess("Filling in the index header template failed");
280              
281             my $tags = $self->_tags;
282              
283             for my $tag (sort keys %$tags) {
284             my %data = (tag => $tag, description => $tags->{$tag});
285              
286             $template->fill_in(HASH => \%data, OUTPUT => $out)
287             or confess("Filling in the entry template failed for $tag");
288             }
289              
290             $footer->fill_in(HASH => \%data, OUTPUT => $out)
291             or confess("Filling in the index footer template failed");
292              
293             close $out;
294             } # end generate_index_page
295             #---------------------------------------------------------------------
296              
297             sub compile_templates {
298             my $self = shift;
299              
300             foreach my $string (@_) {
301             confess("Cannot use undef as a template string") unless defined $string;
302              
303             my $tmpl = Text::Template->new(
304             TYPE => 'STRING',
305             SOURCE => $string,
306             DELIMITERS => $self->delimiters,
307             BROKEN => sub { my %hash = @_; die $hash{error}; },
308             STRICT => 1,
309             );
310              
311             confess("Could not create a Text::Template object from:\n$string")
312             unless $tmpl;
313              
314             $string = $tmpl; # Modify arguments in-place
315             } # end for each $string in @_
316             } # end compile_templates
317              
318             #=====================================================================
319             # Package Return Value:
320              
321             __PACKAGE__->meta->make_immutable;
322             1;
323              
324             __END__
325              
326             =head1 NAME
327              
328             Pod::PluginCatalog - Format a catalog of plugin modules
329              
330             =head1 VERSION
331              
332             This document describes version 0.02 of
333             Pod::PluginCatalog, released January 3, 2015
334             as part of Pod-PluginCatalog version 0.02.
335              
336             =head1 SYNOPSIS
337              
338             use Pod::PluginCatalog;
339              
340             my $catalog = Pod::PluginCatalog->new(
341             namespace_rewriter => sub { 'My::Plugin::Namespace::' . shift },
342             pod_formatter => sub {
343             my $parser = Pod::Simple::XHTML->new;
344             $parser->output_string(\my $html);
345             $parser->html_header('');
346             $parser->html_footer('');
347             $parser->perldoc_url_prefix("https://metacpan.org/module/");
348             $parser->parse_string_document( shift );
349             $html;
350             },
351             );
352              
353             $catalog->add_file('catalog.pod');
354              
355             $catalog->generate_tag_pages($header, $entry, $footer);
356             $catalog->generate_index_page($header, $entry, $footer);
357              
358             =head1 DESCRIPTION
359              
360             B<Warning:> This is still early code, not yet in production.
361             The API might change.
362              
363             This module aids in formatting a tag-based catalog of plugins. It
364             was written to create the catalog at L<http://dzil.org/plugins/> but
365             should also be useful for similar catalogs.
366             (That catalog is not yet live as of this writing; a preview is at
367             L<http://dzil.cjmweb.net/plugins/> and the code that generates it at
368             L<https://github.com/madsen/dzil.org/tree/plugin-catalog>.)
369              
370             The catalog begins with one or more POD files defining the available
371             plugins and the tags used to categorize them. You load each file into
372             the catalog with the C<add_file> method, and then call the
373             C<generate_tag_pages> and C<generate_index_page> methods to produce a
374             formatted page for each tag and an index page listing all the tags.
375              
376             =head1 EXTENDED POD SYNTAX
377              
378             This module defines three non-standard POD command paragraphs used to
379             create the catalog:
380              
381             =head2 C<=author>
382              
383             =author CPANID
384              
385             This sets the author for all following plugins (until the next
386             C<=author>). If CPANID is omitted, it resets the author to the
387             default (which is no listed author, represented by C<undef>).
388              
389             =head2 C<=plugin>
390              
391             =plugin PluginName tagname tagname...
392              
393             This paragraph defines a plugin and associates it with the specifed
394             tags. Neither PluginName nor the tag names may contain whitespace,
395             because the paragraph content is simply C<split(' ', ...)>. The first
396             element is the name, and the rest are the tags. (This means that a
397             single newline is equivalent to a space.)
398              
399             The following paragraphs (if any) form the description of the
400             plugin. The description may include ordinary paragraphs, verbatim
401             (indented) paragraphs, and the commands C<=head3>, C<=head4>,
402             C<=over>, C<=item>, and C<=back>.
403              
404             =head2 C<=tag>
405              
406             =tag tagname
407              
408             This defines a tag. The following paragraphs (if any) form the
409             description of the tag (using the same rules as a plugin's
410             description).
411              
412             You'll get a warning if any plugin uses a tag that was not defined by
413             a C<=tag> command, or if any tag is defined but never used by any
414             plugin. (The warnings are generated only when you output the results;
415             the order C<=tag> and C<=plugin> occur doesn't matter.)
416              
417             =for Pod::Coverage
418             compile_templates
419             format_description
420             generate_tag_page
421              
422             =head1 ATTRIBUTES
423              
424             =head2 delimiters
425              
426             This is an arrayref of two strings: the opening and closing delimiters
427             for L<Text::Template>. (default C<< {{ }} >>)
428              
429              
430             =head2 file_extension
431              
432             This suffix is appended to the tag name to form the filename for each
433             generated page. (default C<.html>)
434              
435              
436             =head2 namespace_rewriter
437              
438             This is a coderef to a function that takes one argument (a plugin
439             name) and returns the corresponding module name. (required)
440              
441              
442             =head2 perlio_layers
443              
444             This string contains the PerlIO layers to be used when opening files
445             for output. (default C<:utf8>)
446              
447              
448             =head2 pod_formatter
449              
450             This is a coderef to a function that takes one argument (a string
451             containing POD) and returns the string formatted as it should appear
452             in the output. That can be HTML or any other format. The string is
453             guaranteed to start with a POD command paragraph.
454             (required)
455              
456             =head1 METHODS
457              
458             =head2 add_document
459              
460             $catalog->add_document($name => $doc);
461              
462             This adds a L<Pod::Elemental::Document> to the catalog. The C<$name>
463             is used for error messages. May be called multiple times.
464              
465              
466             =head2 add_file
467              
468             $catalog->add_file($filename);
469              
470             This is just a wrapper around C<add_document> to read a file on disk.
471              
472              
473             =head2 generate_index_page
474              
475             $catalog->generate_index_page($header, $entry, $footer);
476              
477             This generates an index file listing each tag in the catalog.
478             The filename will be C<index> with the L</file_extension> appended.
479              
480             C<$header>, C<$entry>, and C<$footer> are strings to be passed to
481             L<Text::Template> using the L</delimiters>.
482              
483             The C<$header> and C<$footer> templates can refer to the following
484             variables (so that you can use the same header & footer for
485             C<generate_tag_pages> if you want to):
486              
487             $tag Will be undef
488             $tag_description Will be undef
489              
490             The C<$entry> template is printed once for each tag. In addition
491             to the previous variables, it may also use these:
492              
493             $tag The tag being listed (no longer undef)
494             $description The description of that tag
495              
496              
497             =head2 generate_tag_pages
498              
499             $catalog->generate_tag_pages($header, $entry, $footer);
500              
501             This generates a file for each tag in the catalog. It generates the
502             filenames by appending the L</file_extension> to each tag name.
503              
504             C<$header>, C<$entry>, and C<$footer> are strings to be passed to
505             L<Text::Template> using the L</delimiters>.
506              
507             The C<$header> and C<$footer> templates can refer to the following
508             variables:
509              
510             $tag The name of the tag being processed
511             $tag_description The description of that tag (may be undef)
512              
513             The C<$entry> template is printed once for each plugin. In addition
514             to the previous variables, it may also use these:
515              
516             $author The author of this plugin (may be undef)
517             $name The name of this plugin
518             $module The module name of the plugin
519             $description The description of the plugin
520             @other_tags The tags for this plugin (not including $tag)
521              
522             =head1 CONFIGURATION AND ENVIRONMENT
523              
524             Pod::PluginCatalog requires no configuration files or environment variables.
525              
526             =head1 INCOMPATIBILITIES
527              
528             None reported.
529              
530             =head1 BUGS AND LIMITATIONS
531              
532             No bugs have been reported.
533              
534             =head1 AUTHOR
535              
536             Christopher J. Madsen S<C<< <perl AT cjmweb.net> >>>
537              
538             Please report any bugs or feature requests
539             to S<C<< <bug-Pod-PluginCatalog AT rt.cpan.org> >>>
540             or through the web interface at
541             L<< http://rt.cpan.org/Public/Bug/Report.html?Queue=Pod-PluginCatalog >>.
542              
543             You can follow or contribute to Pod-PluginCatalog's development at
544             L<< https://github.com/madsen/pod-plugincatalog >>.
545              
546             =head1 COPYRIGHT AND LICENSE
547              
548             This software is copyright (c) 2015 by Christopher J. Madsen.
549              
550             This is free software; you can redistribute it and/or modify it under
551             the same terms as the Perl 5 programming language system itself.
552              
553             =head1 DISCLAIMER OF WARRANTY
554              
555             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
556             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
557             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
558             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
559             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
560             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
561             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
562             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
563             NECESSARY SERVICING, REPAIR, OR CORRECTION.
564              
565             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
566             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
567             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENSE, BE
568             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
569             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
570             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
571             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
572             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
573             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
574             SUCH DAMAGES.
575              
576             =cut