File Coverage

blib/lib/MojoX/Plugin/PODRenderer.pm
Criterion Covered Total %
statement 30 253 11.8
branch 0 88 0.0
condition 0 27 0.0
subroutine 10 25 40.0
pod 1 1 100.0
total 41 394 10.4


line stmt bran cond sub pod time code
1             package MojoX::Plugin::PODRenderer;
2 1     1   1569 use Mojo::Base 'Mojolicious::Plugin';
  1         11132  
  1         8  
3              
4 1     1   2075 use Mojo::Asset::File;
  1         70850  
  1         16  
5 1     1   911 use Mojo::ByteStream 'b';
  1         5000  
  1         58  
6 1     1   957 use Mojo::DOM;
  1         15834  
  1         43  
7 1     1   11 use Mojo::Util qw(slurp url_escape class_to_path xml_escape);
  1         2  
  1         79  
8 1     1   1130 use Pod::Simple::HTML;
  1         52616  
  1         48  
9 1     1   4147 use Pod::Simple::Search;
  1         12370  
  1         40  
10 1     1   1049 use boolean;
  1         1549  
  1         6  
11 1     1   1914 use Class::MOP;
  1         230724  
  1         75  
12 1     1   14 use File::Find;
  1         1  
  1         4785  
13              
14             our $VERSION = '0.01';
15              
16             # Paths to search
17             my @PATHS = map { $_, "$_/pods" } @INC;
18              
19             sub register {
20 0     0 1   my ($self, $app, $conf) = @_;
21              
22 0   0       my $preprocess = $conf->{preprocess} || 'ep';
23             $app->renderer->add_handler(
24             $conf->{name} || 'pod' => sub {
25 0     0     my ($renderer, $c, $output, $options) = @_;
26              
27             # Preprocess and render
28 0           my $handler = $renderer->handlers->{$preprocess};
29 0 0         return undef unless $handler->($renderer, $c, $output, $options);
30 0           $$output = _pod_to_html($$output);
31 0           return 1;
32             }
33 0   0       );
34              
35             # Perldoc browser
36 0           return $app->routes->any(
37             '/perldoc/*module' => {module => 'DocIndex'} => \&_perldoc
38             );
39             }
40              
41             # ------------------------------------------------------------------------------
42              
43             sub _process_found_file {
44 0     0     my ($name2path, $path2name) = @_;
45              
46 0           warn "2path %s - 2name %s \n", $name2path, $path2name;
47             }
48              
49             # ------------------------------------------------------------------------------
50              
51             sub _generateIndex {
52 0     0     my $self = shift;
53              
54 0           my ($lib) = grep "script\/\.\.\/lib", @INC;
55              
56 0           my ($name2path, $path2name) = ({},{}); # It's an owl!
57              
58             find(
59             {
60             wanted => sub {
61 0 0   0     return unless $_ =~ /\.(pm|pl|pod)$/;
62 0           my $path = $File::Find::name;
63 0           my $name = $path;
64 0           $name =~ s/^$lib\/?//;
65 0           $name =~ s/\.(pm|pl|pod)$//g;
66 0           $name =~ s!/!::!g;
67              
68 0           $path2name->{$path} = $name;
69 0           $name2path->{$name} = $path;
70             },
71             },
72 0           $lib
73             );
74              
75 0           my $guides = [];
76 0           my $modules = {};
77              
78 0           foreach my $path (sort keys %$path2name) {
79 0           my $name = $path2name->{$path};
80 0 0         if ($path =~ /\.pod$/) { # guide
81 0           (my $url = '/perldoc/'.class_to_path($name)) =~ s/\.pm$/\.pod/;
82              
83 0           push @{$guides}, { name => $name, has_doc => true, path => $url };
  0            
84             }
85             else { # module
86 0           (my $url = '/perldoc/'.class_to_path($name)) =~ s/\.pm//;
87              
88             # Check whether it actually has pod
89 0           my $search = Pod::Simple::Search->new();
90 0           my $has_pod = $search->contains_pod($path);
91              
92 0           my $section = 'other';
93              
94 0 0         if ( $name =~ /::Role::/) { $section = 'roles' }
  0 0          
    0          
    0          
    0          
95 0           elsif ($name =~ /::Models::/) { $section = 'models' }
96 0           elsif ($name =~ /::Controllers::/) { $section = 'controllers' }
97 0           elsif ($name =~ /::Adapter::/) { $section = 'adapters' }
98 0           elsif ($name =~ /::Plugins?::/) { $section = 'plugins' }
99            
100 0 0         push @{$modules->{$section}}, { name => $name, has_doc => $has_pod?true:false, path => $url };
  0            
101             }
102             }
103              
104              
105 0           my ($template, undef) = $self->app->renderer->render(
106             $self,
107             {
108             template => 'perldoc/perldocindex',
109             partial => 1,
110             handler => 'ep',
111             title => "Index",
112             guides => $guides,
113             modules => $modules,
114             }
115             );
116 0           $self->render(inline => $template);
117 0           $self->res->headers->content_type('text/html;charset="UTF-8"');
118              
119 0           return;
120             }
121              
122             # ------------------------------------------------------------------------------
123              
124             sub _perldoc {
125 0     0     my $self = shift;
126              
127 0           my $module = $self->param('module');
128 0           $module =~ s/\.pod$//;
129              
130 0 0         if ($module eq 'DocIndex') {
131 0           return _generateIndex($self);
132             }
133              
134 0   0       my $path = Pod::Simple::Search->new->find($module, @PATHS) || '';
135              
136             # Check whether the file we're dealing with is a perl module with embedded
137             # pod or whether it's a pure pod doc.
138             # If the extension is "pod" then it's a standalone. If it's "pm" then there
139             # will be source code.
140 0           my $extension = ($path =~ /\.(pm|pod)$/)[0];
141              
142             # Convert the full module name to a perl package
143 0           my $package = $module;
144 0           $package =~ s!/!::!g;
145              
146              
147              
148 0           my $file_name = ($module =~ /(\w+)(\.(pm|pod))?$/)[0];
149              
150             # If we're looking at perl source then we want to know if we're expecting the
151             # doc view or the source view.
152 0           my $is_perl_source = false;
153 0           my $linked_file_name = '';
154 0 0 0       if ($extension && $extension eq 'pm') {
155             # We know if we're viewing the source as the extension of the module name
156             # passed in will have the pm extension.
157 0 0         $is_perl_source = true if $module =~ /\.pm$/;
158              
159 0 0         if ($is_perl_source) {
160 0           $linked_file_name = $file_name;
161             }
162             else {
163 0           $linked_file_name = $file_name . '.pm'; # Link is source
164             }
165             }
166            
167 0           my $html = undef;
168              
169 0 0         if (!-e $path) {
170             # Redirect to the index page
171 0           return _generateIndex($self);
172             }
173             else {
174 0           my $slurped = slurp $path;
175 0 0         $html = $is_perl_source ? "<pre>".xml_escape($slurped)."</pre>" : _pod_to_html($slurped);
176              
177             # Ensure % gets escaped before going into the template
178             # for perl source files.
179 0           $html =~ s/^( *)\%/$1<%='%'%>/gm;
180             }
181              
182              
183             # TODO ATTRIBUTES ==== TODO Autoinsert
184             # Introspect the class to find the attributes
185 0 0 0       _parse_attributes(\$html, $package, $module) if !$is_perl_source && ($html =~ /\[\[ATTRIBUTES\]\]/);
186            
187              
188             # Rewrite links
189 0           my $dom = Mojo::DOM->new("$html");
190 0           my $perldoc = $self->url_for('/perldoc/');
191             $dom->find('a[href]')->each(
192             sub {
193 0     0     my $attrs = shift->attrs;
194 0 0         $attrs->{href} =~ s!%3A%3A!/!gi
195             if $attrs->{href} =~ s!^http://search\.cpan\.org/perldoc\?!$perldoc!;
196             }
197 0           );
198              
199            
200             # Rewrite code blocks for syntax highlighting
201             $dom->find('pre')->each(
202             sub {
203 0     0     my $e = shift;
204 0 0         return if $e->all_text =~ /^\s*\$\s+/m;
205              
206 0           my $attrs = $e->attrs;
207 0           my $class = $attrs->{class};
208 0 0         $attrs->{class} = defined $class ? "$class prettyprint" : 'prettyprint';
209             }
210 0           );
211              
212             # Rewrite headers
213 0           my $url = $self->req->url->clone;
214 0           my (%anchors, @parts);
215             $dom->find('h1, h2, h3')->each(
216             sub {
217 0     0     my $e = shift;
218              
219             # Anchor and text
220 0           my $name = my $text = $e->all_text;
221 0           $name =~ s/\s+/_/g;
222 0           $name =~ s/[^\w\-]//g;
223 0           my $anchor = $name;
224 0           my $i = 1;
225 0           $anchor = $name . $i++ while $anchors{$anchor}++;
226              
227             # Rewrite
228 0 0 0       push @parts, [] if $e->type eq 'h1' || !@parts;
229              
230 0           my $link_text = $text;
231 0           $link_text =~ s/\[.*\]//;
232 0           $link_text =~ s/\(.*\)//;
233              
234 0           push @{$parts[-1]}, $text, $url->fragment($anchor)->to_abs;
  0            
235              
236 0           $e->replace_content(
237             $self->link_to(
238             $text => $url->fragment('toc')->to_abs,
239             class => 'mojoscroll',
240             id => $anchor
241             )
242             );
243             }
244 0           );
245              
246             # Format h2's if they're method names
247             $dom->find('h2')->each(
248             sub {
249 0     0     my $e = shift;
250 0           my $text = $e->all_text;
251              
252 0 0         if ($text !~ /\[(.+)\] *(\w+) *\((.*)\)/) {
253 0           return;
254             }
255              
256 0           my ($type, $name, $args) = ($text =~ /\[(.+)\] *(\w+) *\((.*)\)/);
257 0           $e->replace_content(
258             '<span class="code">'
259             .'<span class="return-type">['.$type.']</span> '
260             ."$name "
261             .'<span class="arg-list">('.$args.')</span>'
262             .'</span>'
263             );
264             }
265 0           );
266              
267             # Reformat PRE blocks (again - need to combine this possibly with the mojo written one above)
268 0 0         if (!$is_perl_source) {
269             $dom->find('pre')->each(
270             sub {
271 0     0     my $e = shift;
272            
273 0           my $re = qr/\@(param|returns|named|throws) (.+)/;
274 0           my $context = 'before';
275 0           my $has_seen_tags = false;
276            
277 0           my %parts = (
278             before => [[]], after => [[]],
279             param => [], returns => [],
280             named => [], throws => [],
281             );
282            
283 0 0         if ($e->all_text =~ $re) {
284 0           foreach my $line (split "\n", $e->all_text) {
285            
286 0 0         if ($line =~ /^ *$/) { # Blank lines switch
287 0 0         $context = $has_seen_tags ? 'after' : 'before';
288             }
289            
290 0 0         if ($line =~ $re) {
291 0           $context = $1; # One of the tag contexts
292 0           $line = $2;
293 0           $has_seen_tags = true;
294 0           push @{$parts{$context}},[]; # Create a new array for the new context
  0            
295             }
296            
297 0 0         if (defined $context) {
298             # Get the last item of this type, and add to it.
299 0 0         $line =~ s/^ *// if ($context !~ /before|after/);
300 0           push @{$parts{ $context }->[-1]}, $line;
  0            
301 0           next;
302             }
303            
304             }
305            
306             # Output the parts - we do this by appending to the original element
307             # in reverse order and then removing the original.
308              
309             # Output AFTER
310 0 0         if (scalar @{$parts{after}->[0]}) {
  0            
311 0           $e->append('<pre>' . join(" ",@{$parts{after}->[0]}) . '</pre>');
  0            
312             }
313            
314 0 0 0       if (@{$parts{returns}} || @{$parts{param}} || @{$parts{named}}) {
  0   0        
  0            
  0            
315 0           my $block = '<div class="tag-table-block">';
316            
317             # Output Parameters
318 0 0         if (scalar @{$parts{param}}) {
  0            
319 0           $block .= __start_table( 'parameters', '3' );
320 0           foreach my $param (@{$parts{param}}) {
  0            
321 0           (my $whole_line = join ' ',@$param ) =~ /(\S+) +\[([^\]]+)\] +(.+)/;
322 0           $block .= qq|<tr><td class="code">$1</td><td class="italic">$2</td><td>$3</td></tr>|;
323             }
324 0           $block .= '</table>';
325             }
326            
327             # Output Named Parameters
328 0 0         if (scalar @{$parts{named}}) {
  0            
329 0           $block .= __start_table( 'named parameters', '3' );
330 0           foreach my $param (@{$parts{named}}) {
  0            
331 0           (my $whole_line = join ' ',@$param ) =~ /(\S+) +\[([^\]]+)\] +(.+)/;
332 0           $block .= qq|<tr><td class="code">$1</td><td class="italic">$2</td><td>$3</td></tr>|;
333             }
334 0           $block .= '</table>';
335             }
336            
337             # Output Return
338 0 0         if (scalar @{$parts{returns}}) {
  0            
339 0           $block .= __start_table( 'returns', '1' );
340 0           my $whole_line = join ' ', @{$parts{returns}->[0]};
  0            
341 0           $block .= qq|<tr><td>$whole_line</td></tr>|;
342 0           $block .= '</table>';
343             }
344            
345             # Output Throws
346 0 0         if (scalar @{$parts{throws}}) {
  0            
347 0           $block .= __start_table( 'throws', '1' );
348 0           foreach my $param (@{$parts{throws}}) {
  0            
349 0           my $whole_line = join ' ', @{$parts{throws}->[0]};
  0            
350 0           $block .= qq|<tr><td>$whole_line</td></tr>|;
351             }
352 0           $block .= '</table>';
353             }
354 0           $block .= '</div>';
355 0           $e->append( $block );
356             }
357            
358             # Output BEFORE
359 0 0         if (scalar @{$parts{before}->[0]}) {
  0            
360 0           $e->append( '<pre class="prettyprint">' . join(" ",@{$parts{before}->[0]}) . '</pre>');
  0            
361             }
362            
363             # Remove the original element
364 0           $e->remove;
365             }
366             }
367 0           );
368             }
369              
370             # Try to find a title
371 0           my $title = 'Perldoc';
372 0     0     $dom->find('h1 + p')->first(sub { $title = shift->text });
  0            
373              
374             # Combine everything to a proper response
375 0           $self->content_for(perldoc => "$dom");
376              
377 0 0         my $template_name = $is_perl_source ? 'perlsource' : 'perldoc';
378              
379 0           my ($template, undef) = $self->app->renderer->render(
380             $self,
381             {
382             template => 'perldoc/'.$template_name,
383             partial => 1,
384             handler => 'ep',
385             title => $title,
386             linked_file => $linked_file_name,
387             parts => \@parts,
388             }
389             );
390 0           $self->render(inline => $template);
391 0           $self->res->headers->content_type('text/html;charset="UTF-8"');
392 0           return;
393             }
394              
395             # ------------------------------------------------------------------------------
396              
397             sub __start_table {
398 0     0     my ($name, $span) = @_;
399 0           return qq|<table class="tag-table"><tr><th colspan="$span">$name</th></tr>|;
400             }
401              
402             # ------------------------------------------------------------------------------
403              
404             sub _pod_to_html {
405 0 0   0     return undef unless defined(my $pod = shift);
406              
407             # Block
408 0 0         $pod = $pod->() if ref $pod eq 'CODE';
409              
410 0           my $parser = Pod::Simple::HTML->new;
411 0           $parser->force_title('');
412 0           $parser->html_header_before_title('');
413 0           $parser->html_header_after_title('');
414 0           $parser->html_footer('');
415 0           $parser->output_string(\(my $output));
416 0 0         return $@ unless eval { $parser->parse_string_document("$pod"); 1 };
  0            
  0            
417              
418             # Filter
419 0           $output =~ s!<a name='___top' class='dummyTopAnchor'\s*?></a>\n!!g;
420 0           $output =~ s!<a class='u'.*?name=".*?"\s*>(.*?)</a>!$1!sg;
421              
422 0           return $output;
423             }
424              
425             # ------------------------------------------------------------------------------
426              
427             sub _parse_attributes {
428 0     0     my ($html_r, $package, $module) = @_;
429            
430 0           $module =~ s/\.pm$//;
431              
432 0           require "$module.pm";
433              
434 0           my $meta = Class::MOP::Class->initialize($package);
435              
436 0           my %local_attributes = ();
437 0           my %inherited_attributes = ();
438              
439 0 0         if ($meta->can("get_attribute_list")) {
440 0           foreach my $attr ($meta->get_attribute_list) {
441 0           $local_attributes{$attr} = 1;
442             }
443             }
444            
445 0 0         if ($meta->can("get_all_attributes")) {
446 0           foreach my $attr ($meta->get_all_attributes) {
447 0 0         if (!exists $local_attributes{$attr->name}) {
448 0           $inherited_attributes{$attr->name} = 1;
449             }
450             }
451             }
452              
453 0           my $replace = '';
454              
455 0           my $local = join(", ", sort keys %local_attributes);
456 0           my $inherited = join(", ", sort keys %inherited_attributes);
457              
458 0 0 0       if ($local and $inherited) { $local .= ', ' };
  0            
459              
460 0 0 0       if ($local or $inherited) {
461 0           $replace = qq|<div class="code">$local<em>$inherited</em></div><br>|;
462             }
463 0           $$html_r =~ s/\[\[ATTRIBUTES\]\]/$replace/;
464 0           return;
465             }
466              
467             # ==============================================================================
468              
469             1;
470              
471             =head1 NAME
472              
473             MojoX::Plugin::PODRenderer
474              
475             =head1 SYNOPSIS
476              
477             use MojoX::Plugin::PODRenderer;
478              
479             $self->plugin( 'MojoX::Plugin::PODRenderer' );
480              
481             =head1 DESCRIPTION
482              
483             Perl pod rendering plugin. Based on the original Mojo::PODRenderer.
484              
485             =head1 METHODS
486              
487             =head2 [void] register( $app, $conf )
488              
489             Called by Mojo app to register the plugin
490              
491             @param app [mojo application] ref to the mojo application
492             @param conf [hash] configuration hash
493              
494             =cut