File Coverage

lib/Mojolicious/Plugin/DOCRenderer.pm
Criterion Covered Total %
statement 90 94 95.7
branch 12 20 60.0
condition 10 20 50.0
subroutine 17 18 94.4
pod 1 1 100.0
total 130 153 84.9


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::DOCRenderer;
2 1     1   752 use Mojo::Base 'Mojolicious::Plugin';
  1         2  
  1         8  
3              
4 1     1   221 use File::Basename 'dirname';
  1         11  
  1         55  
5 1     1   6 use File::Spec::Functions 'catdir';
  1         2  
  1         46  
6 1     1   6 use Mojo::Asset::File;
  1         2  
  1         13  
7 1     1   26 use Mojo::ByteStream;
  1         2  
  1         57  
8 1     1   7 use Mojo::DOM;
  1         2  
  1         26  
9 1     1   4 use Mojo::File 'path';
  1         2  
  1         54  
10 1     1   6 use Mojo::URL;
  1         2  
  1         13  
11 1     1   764 use Pod::Simple::XHTML;
  1         12463  
  1         37  
12 1     1   724 use Pod::Simple::Search;
  1         6359  
  1         1375  
13              
14             our $VERSION = '5.02';
15              
16             # "Futurama - The One Bright Spot in Your Life!"
17             sub register {
18 1     1 1 51 my ($self, $app, $conf) = @_;
19              
20             # Add "doc" handler
21 1   50     11 my $preprocess = $conf->{preprocess} || 'ep';
22             $app->renderer->add_handler(
23             $conf->{name} || 'doc' => sub {
24 0     0   0 my ($renderer, $c, $output, $options) = @_;
25 0         0 $renderer->handlers->{$preprocess}($renderer, $c, $output, $options);
26 0 0       0 $$output = _pod_to_html($$output) if defined $$output;
27             }
28 1   50     8 );
29              
30             # Append "templates" and "public" directories
31 1         38 push @{$app->renderer->paths}, catdir(dirname(__FILE__), 'DOCRenderer', 'templates');
  1         2  
32              
33             # Doc
34 1   50     75 my $url = $conf->{url} || '/doc';
35 1         4 my $module = $conf->{module};
36              
37             # Detect script location based on 2nd or 3rd parent (in case of Mojolicious::Lite app)
38 1         14 my ( $package, $script ) = (caller(2))[0, 1];
39 1 50       5 if ( $package eq 'Mojolicious::Lite' ) {
40 1         14 $script = (caller(3))[1];
41             }
42             else {
43 0   0     0 $module ||= $package;
44             }
45              
46 1         6 my $defaults = {url => $url, module => $module, script => $script, format => 'html'};
47 1         6 return $app->routes->any("$url/:module"
48             => $defaults
49             => [format => [qw(html txt)], module => qr/[^.]+/]
50             => \&_doc
51             );
52             }
53              
54             sub _indentation {
55 6     6   24234 (sort map {/^(\s+)/} @{shift()})[0];
  78         150  
  6         13  
56             }
57              
58             sub _html {
59 2     2   4 my ($c, $src) = @_;
60              
61             # Rewrite links
62 2         6 my $dom = Mojo::DOM->new(_pod_to_html($src));
63 2         7952 my $doc = $c->url_for( $c->param('url') . '/' );
64             $_->{href} =~ s!^https://metacpan\.org/pod/!$doc!
65             and $_->{href} =~ s!::!/!gi
66 2   100     646 for $dom->find('a[href]')->map('attr')->each;
67              
68             # Rewrite code blocks for syntax highlighting and correct indentation
69 2         4236 for my $e ($dom->find('pre > code')->each) {
70 6         2328 my $str = $e->content;
71 6 100 66     746 next if $str =~ /^\s*(?:\$|Usage:)\s+/m || $str !~ /[\$\@\%]\w|->\w/m;
72 2         11 my $attrs = $e->attr;
73 2         30 my $class = $attrs->{class};
74 2 50       9 $attrs->{class} = defined $class ? "$class prettyprint" : 'prettyprint';
75             }
76              
77             # Rewrite headers
78 2         321 my $toc = Mojo::URL->new->fragment('toc');
79 2         29 my @parts;
80 2         8 for my $e ($dom->find('h1, h2, h3, h4')->each) {
81              
82 13 100 66     13379 push @parts, [] if $e->tag eq 'h1' || !@parts;
83 13         238 my $link = Mojo::URL->new->fragment($e->{id});
84 13         356 push @{$parts[-1]}, my $text = $e->all_text, $link;
  13         37  
85 13         559 my $permalink = $c->link_to('#' => $link, class => 'permalink');
86 13         6247 $e->content($permalink . $c->link_to($text => $toc));
87             }
88              
89             # Try to find a title
90 2         1554 my $title = 'Doc';
91 2     2   8 $dom->find('h1 + p')->first(sub { $title = shift->text });
  2         7151  
92              
93             # Combine everything to a proper response
94 2         79 $c->content_for(doc => "$dom");
95 2         4183 $c->render(title => $title, parts => \@parts);
96             }
97              
98             sub _doc {
99 3     3   59242 my $c = shift;
100              
101 3         10 my $path = $c->stash('script');
102 3         61 my $module = $c->param('module');
103              
104 3 100       113 if (defined $module) {
105             # Find module or redirect to CPAN
106 1         8 my $module = join '::', split('/', $module);
107             $path
108 1         12 = Pod::Simple::Search->new->find($module, map { $_, "$_/pods" } @INC);
  14         74  
109 1 50 33     1309 return $c->redirect_to("https://metacpan.org/pod/$module")
110             unless $path && -r $path;
111             }
112              
113 3         12 my $src = path($path)->slurp;
114 3     2   430 $c->respond_to(txt => {data => $src}, html => sub { _html($c, $src) });
  2         1386  
115             }
116              
117             sub _pod_to_html {
118 2 50   2   10 return '' unless defined(my $pod = ref $_[0] eq 'CODE' ? shift->() : shift);
    50          
119              
120 2         15 my $parser = Pod::Simple::XHTML->new;
121 2         251 $parser->perldoc_url_prefix('https://metacpan.org/pod/');
122 2         16 $parser->$_('') for qw(html_header html_footer);
123 2         27 $parser->strip_verbatim_indent(\&_indentation);
124 2         20 $parser->output_string(\(my $output));
125 2 50       1270 return $@ unless eval { $parser->parse_string_document("$pod"); 1 };
  2         39  
  2         6190  
126              
127 2         72 return $output;
128             }
129              
130             1;
131              
132             =encoding utf8
133              
134             =head1 NAME
135              
136             Mojolicious::Plugin::DOCRenderer - Doc Renderer Plugin
137              
138             =head1 SYNOPSIS
139              
140             # Mojolicious::Lite
141             plugin 'DOCRenderer';
142             plugin DOCRenderer => {module => 'MyApp'};
143             plugin DOCRenderer => {name => 'foo'};
144             plugin DOCRenderer => {url => '/mydoc'};
145             plugin DOCRenderer => {preprocess => 'epl'};
146              
147             # Mojolicious
148             $self->plugin('DOCRenderer');
149             $self->plugin(DOCRenderer => {module => 'MyApp'});
150             $self->plugin(DOCRenderer => {name => 'foo'});
151             $self->plugin(DOCRenderer => {url => '/mydoc'});
152             $self->plugin(DOCRenderer => {preprocess => 'epl'});
153              
154             #############################
155             # Mojolicious::Lite example #
156             #############################
157             use Mojolicious::Lite;
158             use File::Basename;
159              
160             plugin 'DOCRenderer';
161              
162             app->start;
163              
164             __END__
165              
166             =head1 NAME
167              
168             MyApp - My Mojolicious::Lite Application
169              
170             =head1 DESCRIPTION
171              
172             This documentation will be available online, for example from L.
173              
174             =cut
175              
176             #######################
177             # Mojolicious example #
178             #######################
179             package MyApp;
180             use Mojo::Base 'Mojolicious';
181              
182             sub startup {
183             my $self = shift;
184              
185             # Enable browsing of "/doc" only in development mode
186             $self->plugin( 'DOCRenderer' ) if $self->mode eq 'development';
187              
188             # some code
189             }
190              
191             __END__
192              
193             =head1 NAME
194              
195             MyApp - My Mojolicious Application
196              
197             =head1 DESCRIPTION
198              
199             This documentation will be available online, for example from L.
200              
201             =cut
202              
203             =head1 DESCRIPTION
204              
205             L generates on-the-fly and browses online
206             POD documentation directly from your Mojolicious application source codes
207             and makes it available under I (customizable).
208              
209             The plugin expects that you use POD to document your codes of course.
210              
211             The plugin is simple modification of L.
212              
213             =head1 OPTIONS
214              
215             =head2 C
216              
217             # Mojolicious::Lite
218             plugin DOCRenderer => {module => 'MyApp'};
219              
220             Name of the module to initially display. Default is C<$ENV{MOJO_APP}>.
221             Mojolicious::Lite application may have undefined C<$ENV{MOJO_APP}>; in such
222             case you should set C, see Mojolicious::Lite example.
223              
224             =head2 C
225              
226             # Mojolicious::Lite
227             plugin DOCRenderer => {name => 'foo'};
228              
229             Handler name.
230              
231             =head2 C
232              
233             # Mojolicious::Lite
234             plugin DOCRenderer => {preprocess => 'epl'};
235              
236             Handler name of preprocessor.
237              
238             =head2 C
239              
240             # Mojolicious::Lite
241             plugin DOCRenderer => {url => '/mydoc'};
242              
243             URL from which the documentation of your project is available. Default is I.
244              
245             =head1 METHODS
246              
247             L inherits all methods from
248             L and implements the following new ones.
249              
250             =head2 C
251              
252             my $route = $plugin->register(Mojolicious->new);
253             my $route = $plugin->register(Mojolicious->new, {name => 'foo'});
254              
255             Register renderer in L application.
256              
257             =head1 SEE ALSO
258              
259             L, L, L, L.
260              
261             =cut