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