File Coverage

lib/Mojolicious/Plugin/DOCRenderer.pm
Criterion Covered Total %
statement 91 96 94.7
branch 11 18 61.1
condition 9 18 50.0
subroutine 16 17 94.1
pod 1 1 100.0
total 128 150 85.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::DOCRenderer;
2 1     1   934 use Mojo::Base 'Mojolicious::Plugin';
  1         2  
  1         8  
3              
4 1     1   220 use File::Basename 'dirname';
  1         2  
  1         71  
5 1     1   17 use File::Spec::Functions 'catdir';
  1         2  
  1         50  
6 1     1   5 use Mojo::Asset::File;
  1         2  
  1         14  
7 1     1   23 use Mojo::ByteStream 'b';
  1         2  
  1         66  
8 1     1   10 use Mojo::DOM;
  1         2  
  1         22  
9 1     1   5 use Mojo::URL;
  1         2  
  1         9  
10 1     1   34 use Mojo::Util qw(slurp unindent url_escape);
  1         2  
  1         74  
11 1     1   1172 use Pod::Simple::HTML;
  1         16898  
  1         50  
12 1     1   11904 use Pod::Simple::Search;
  1         14925  
  1         2038  
13              
14             our $VERSION = '4.00';
15              
16             # "Futurama - The One Bright Spot in Your Life!"
17             sub register {
18 1     1 1 72 my ($self, $app, $conf) = @_;
19              
20             # Add "doc" handler
21 1   50     12 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              
26             # Preprocess and render
27 0         0 my $handler = $renderer->handlers->{$preprocess};
28 0 0       0 return undef unless $handler->($renderer, $c, $output, $options);
29 0         0 $$output = _pod_to_html($$output);
30 0         0 return 1;
31             }
32 1   50     37 );
33              
34             # Append "templates" and "public" directories
35 1         148 push @{$app->renderer->paths}, catdir(dirname(__FILE__), 'DOCRenderer', 'templates');
  1         25  
36              
37             # Doc
38 1   50     98 my $url = $conf->{url} || '/doc';
39 1   33     7 my $module = $conf->{module} || $ENV{MOJO_APP};
40 1         5 my $defaults = {url => $url, module => $module, format => 'html'};
41 1         28 return $app->routes->any(
42             "$url/:module" => $defaults => [module => qr/[^.]+/] => \&_doc);
43             }
44              
45             sub _html {
46 3     3   8 my ($self, $src) = @_;
47              
48             # Rewrite links
49 3         18 my $dom = Mojo::DOM->new(_pod_to_html($src));
50 3         8718 my $doc = $self->url_for( $self->param('url') . '/' );
51 3         2192 for my $e ($dom->find('a[href]')->each) {
52 9         4998 my $attrs = $e->attr;
53 9 100       376 $attrs->{href} =~ s!%3A%3A!/!gi
54             if $attrs->{href} =~ s!^http://search\.cpan\.org/perldoc\?!$doc!;
55             }
56              
57             # Rewrite code blocks for syntax highlighting and correct indentation
58 3         1069 for my $e ($dom->find('pre')->each) {
59 7         2380 $e->content(my $str = unindent $e->content);
60 7 100 66     4763 next if $str =~ /^\s*(?:\$|Usage:)\s+/m || $str !~ /[\$\@\%]\w|->\w/m;
61 3         11 my $attrs = $e->attr;
62 3         118 my $class = $attrs->{class};
63 3 50       15 $attrs->{class} = defined $class ? "$class prettyprint" : 'prettyprint';
64             }
65              
66             # Rewrite headers
67 3         774 my $toc = Mojo::URL->new->fragment('toc');
68 3         117 my (%anchors, @parts);
69 3         14 for my $e ($dom->find('h1, h2, h3')->each) {
70              
71             # Anchor and text
72 16         24383 my $name = my $text = $e->all_text;
73 16         3310 $name =~ s/\s+/_/g;
74 16         35 $name =~ s/[^\w\-]//g;
75 16         24 my $anchor = $name;
76 16         26 my $i = 1;
77 16         83 $anchor = $name . $i++ while $anchors{$anchor}++;
78              
79             # Rewrite
80 16 100 66     52 push @parts, [] if $e->type eq 'h1' || !@parts;
81 16         804 my $link = Mojo::URL->new->fragment($anchor);
82 16         601 push @{$parts[-1]}, $text, $link;
  16         48  
83 16         139 my $permalink = $self->link_to('#' => $link, class => 'permalink');
84 16         11928 $e->content($permalink . $self->link_to($text => $toc, id => $anchor));
85             }
86              
87             # Try to find a title
88 3         3550 my $title = 'Doc';
89 3     3   16 $dom->find('h1 + p')->first(sub { $title = shift->text });
  3         10087  
90              
91             # Combine everything to a proper response
92 3         628 $self->content_for(doc => "$dom");
93 3         5554 $self->render(title => $title, parts => \@parts);
94             }
95              
96             sub _doc {
97 3     3   194492 my $self = shift;
98              
99             # Find module or redirect to CPAN
100 3         19 my $module = $self->param('module');
101 3         391 $module =~ s!/!::!g;
102 39         236 my $path
103 3         40 = Pod::Simple::Search->new->find($module, map { $_, "$_/pods" } @INC);
104 3 50 33     1982 return $self->redirect_to("http://metacpan.org/module/$module")
105             unless $path && -r $path;
106              
107 3         17 my $src = slurp $path;
108 3     3   323 $self->respond_to(txt => {data => $src}, any => sub { _html($self, $src) });
  3         2517  
109             }
110              
111             sub _pod_to_html {
112 3 50   3   19 return '' unless defined(my $pod = ref $_[0] eq 'CODE' ? shift->() : shift);
    50          
113              
114 3         36 my $parser = Pod::Simple::HTML->new;
115 3         1987 $parser->$_('') for qw(force_title html_header_before_title);
116 3         64 $parser->$_('') for qw(html_header_after_title html_footer);
117 3         72 $parser->output_string(\(my $output));
118 3 50       2823 return $@ unless eval { $parser->parse_string_document("$pod"); 1 };
  3         40  
  3         48719  
119              
120             # Filter
121 3         90 $output =~ s!\n!!g;
122 3         99 $output =~ s!(.*?)!$1!sg;
123              
124 3         300 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             # use this script base name as a default module to show for "/doc"
159             module => fileparse( __FILE__, qr/\.[^.]*/ );
160             };
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 development_mode {
183             # Enable browsing of "/doc" only in development mode
184             shift->plugin( 'DOCRenderer' );
185             }
186              
187             sub startup {
188             my $self = shift;
189             # some code
190             }
191              
192             __END__
193              
194             =head1 NAME
195              
196             MyApp - My Mojolicious Application
197              
198             =head1 DESCRIPTION
199              
200             This documentation will be available online, for example from L.
201              
202             =cut
203              
204             =head1 DESCRIPTION
205              
206             L generates on-the-fly and browses online
207             POD documentation directly from your Mojolicious application source codes
208             and makes it available under I (customizable).
209              
210             The plugin expects that you use POD to document your codes of course.
211              
212             The plugin is simple modification of L.
213              
214             =head1 OPTIONS
215              
216             =head2 C
217              
218             # Mojolicious::Lite
219             plugin DOCRenderer => {module => 'MyApp'};
220              
221             Name of the module to initially display. Default is C<$ENV{MOJO_APP}>.
222             Mojolicious::Lite application may have undefined C<$ENV{MOJO_APP}>; in such
223             case you should set C, see Mojolicious::Lite example.
224              
225             =head2 C
226              
227             # Mojolicious::Lite
228             plugin DOCRenderer => {name => 'foo'};
229              
230             Handler name.
231              
232             =head2 C
233              
234             # Mojolicious::Lite
235             plugin DOCRenderer => {preprocess => 'epl'};
236              
237             Handler name of preprocessor.
238              
239             =head2 C
240              
241             # Mojolicious::Lite
242             plugin DOCRenderer => {url => '/mydoc'};
243              
244             URL from which the documentation of your project is available. Default is I.
245              
246             =head1 METHODS
247              
248             L inherits all methods from
249             L and implements the following new ones.
250              
251             =head2 C
252              
253             my $route = $plugin->register(Mojolicious->new);
254             my $route = $plugin->register(Mojolicious->new, {name => 'foo'});
255              
256             Register renderer in L application.
257              
258             =head1 SEE ALSO
259              
260             L, L, L, L.
261              
262             =cut