File Coverage

blib/lib/Pod/POM/Web.pm
Criterion Covered Total %
statement 447 613 72.9
branch 121 220 55.0
condition 37 87 42.5
subroutine 67 85 78.8
pod 2 35 5.7
total 674 1040 64.8


line stmt bran cond sub pod time code
1             #======================================================================
2             package Pod::POM::Web; # see doc at end of file
3             #======================================================================
4 1     1   139047 use strict;
  1         4  
  1         41  
5 1     1   9 use warnings;
  1         3  
  1         93  
6 1     1   9 no warnings 'uninitialized';
  1         2  
  1         55  
7              
8 1     1   540 use Pod::POM 0.25; # parsing Pod
  1         28635  
  1         109  
9 1     1   16 use List::Util qw/max/; # maximum
  1         2  
  1         128  
10 1     1   602 use List::MoreUtils qw/uniq firstval any/;
  1         7269  
  1         11  
11 1     1   3384 use Module::CoreList; # asking if a module belongs to Perl core
  1         79905  
  1         25  
12 1     1   1589 use HTTP::Daemon; # for the builtin HTTP server
  1         65035  
  1         19  
13 1     1   831 use URI; # parsing incoming requests
  1         4  
  1         58  
14 1     1   506 use URI::QueryParam; # implements URI->query_form_hash
  1         1082  
  1         47  
15 1     1   423 use MIME::Types; # translate file extension into MIME type
  1         4984  
  1         73  
16 1     1   484 use Alien::GvaScript 1.021000; # javascript files
  1         4151  
  1         47  
17 1     1   470 use Encode::Guess; # guessing if pod source is utf8 or latin1
  1         17408  
  1         7  
18 1     1   97 use Config; # where are the script directories
  1         3  
  1         60  
19 1     1   625 use Getopt::Long qw/GetOptions/; # parsing options from command-line
  1         13720  
  1         8  
20 1     1   265 use Module::Metadata; # get version number from module
  1         4  
  1         466  
21              
22             #----------------------------------------------------------------------
23             # globals
24             #---------------------------------------------------------------------
25              
26             our $VERSION = '1.22';
27              
28             # some subdirs never contain Pod documentation
29             my @ignore_toc_dirs = qw/auto unicore/;
30              
31             # filter @INC (don't want '.', nor server_root added by mod_perl)
32             my $server_root = eval {Apache2::ServerUtil::server_root()} || "";
33             our                # because accessed from Pod::POM::Web::Indexer
34                @search_dirs = grep {!/^\./ && $_ ne $server_root} @INC;
35              
36             # directories for executable perl scripts
37             my @config_script_dirs = qw/sitescriptexp vendorscriptexp scriptdirexp/;
38             my @script_dirs = grep {$_} @Config{@config_script_dirs};
39              
40             # syntax coloring (optional)
41             my $coloring_package
42               = eval {require PPI::HTML} ? "PPI"
43               : eval {require ActiveState::Scineplex} ? "SCINEPLEX"
44               : "";
45              
46             # fulltext indexing (optional)
47             my $no_indexer = eval {require Pod::POM::Web::Indexer} ? 0 : $@;
48              
49             # CPAN latest version info (tentative, but disabled because CPAN is too slow)
50             my $has_cpan = 0; # eval {require CPAN};
51              
52             # A sequence of optional filters to apply to the source code before
53             # running it through Pod::POM. Source code is passed in $_[0] and
54             # should be modified in place.
55             my @podfilters = (
56              
57             # AnnoCPAN must be first in the filter list because
58             # it uses the MD5 of the original source
59               eval {require AnnoCPAN::Perldoc::Filter}
60                 ? sub {$_[0] = AnnoCPAN::Perldoc::Filter->new->filter($_[0])}
61                 : (),
62              
63             # Pod::POM fails to parse correctly when there is an initial blank line
64               sub { $_[0] =~ s/\A\s*// },
65              
66             );
67              
68              
69             our # because used by Pod::POM::View::HTML::_PerlDoc
70               %escape_entity = ('&' => '&',
71                                 '<' => '&lt;',
72                                 '>' => '&gt;',
73                                 '"' => '&quot;');
74              
75              
76             #----------------------------------------------------------------------
77             # import : just export the "server" function if called from command-line
78             #----------------------------------------------------------------------
79              
80             sub import {
81 1     1   14   my $class = shift;
82 1         5   my ($package, $filename) = caller;
83              
84 1     1   11   no strict 'refs';
  1         3  
  1         9470  
85 0     0   0   *{'main::server'} = sub {$class->server(@_)}
  0         0  
86 1 50 33     20     if $package eq 'main' and $filename eq '-e';
87             }
88              
89             #----------------------------------------------------------------------
90             # main entry point
91             #----------------------------------------------------------------------
92              
93             sub server { # builtin HTTP server; unused if running under Apache
94 0     0 1 0   my ($class, $port, $options) = @_;
95              
96 0   0     0   $options ||= $class->_options_from_cmd_line;
97 0   0     0   $port ||= $options->{port} || 8080;
      0        
98              
99 0 0       0   my $daemon = HTTP::Daemon->new(LocalPort => $port,
100                                              ReuseAddr => 1) # patch by CDOLAN
101                 or die "could not start daemon on port $port";
102 0         0   print STDERR "Please contact me at: <URL:", $daemon->url, ">\n";
103              
104             # main server loop
105 0         0   while (my $client_connection = $daemon->accept) {
106 0         0     while (my $req = $client_connection->get_request) {
107 0         0       print STDERR "URL : " , $req->url, "\n";
108 0         0       $client_connection->force_last_request; # patch by CDOLAN
109 0         0       my $response = HTTP::Response->new;
110 0         0       $class->handler($req, $response, $options);
111 0         0       $client_connection->send_response($response);
112                 }
113 0         0     $client_connection->close;
114 0         0     undef($client_connection);
115               }
116             }
117              
118              
119             sub _options_from_cmd_line {
120 0     0   0   GetOptions(\my %options, qw/port=i page_title|title=s/);
121 0 0 0     0   $options{port} ||= $ARGV[0] if @ARGV; # backward support for old API
122 0         0   return \%options;
123             }
124              
125              
126             sub handler : method {
127 10     10 1 27999   my ($class, $request, $response, $options) = @_;
128 10         43   my $self = $class->new($request, $response, $options);
129 10 50       26   eval { $self->dispatch_request(); 1}
  10         37  
  10         3125  
130                 or $self->send_content({content => $@, code => 500});
131 10         83   return 0; # Apache2::Const::OK;
132             }
133              
134              
135             sub new {
136 10     10 0 32   my ($class, $request, $response, $options) = @_;
137 10   50     77   $options ||= {};
138 10         38   my $self = {%$options};
139              
140             # cheat: will create an instance of the Indexer subclass if possible
141 10 50 33     46   if (!$no_indexer && $class eq __PACKAGE__) {
142 0         0     $class = "Pod::POM::Web::Indexer";
143               }
144 10         45   for (ref $request) {
145              
146 10 50       47     /^Apache/ and do { # coming from mod_perl
147 0         0       my $path = $request->path_info;
148 0         0       my $q = URI->new;
149 0         0       $q->query($request->args);
150 0         0       my $params = $q->query_form_hash;
151 0         0       (my $uri = $request->uri) =~ s/$path$//;
152 0         0       $self->{response} = $request; # Apache API: same object for both
153 0         0       $self->{root_url} = $uri;
154 0         0       $self->{path} = $path;
155 0         0       $self->{params} = $params;
156 0         0       last;
157                 };
158              
159 10 50       68     /^HTTP/ and do { # coming from HTTP::Daemon // server() method above
160 10         34       $self->{response} = $response;
161 10         27       $self->{root_url} = "";
162 10         37       $self->{path} = $request->url->path;
163 10         285       $self->{params} = $request->url->query_form_hash;
164 10         591       last;
165                 };
166              
167             #otherwise (coming from cgi-bin or mod_perl Registry)
168 0         0     my $q = URI->new;
169 0         0     $q->query($ENV{QUERY_STRING});
170 0         0     my $params = $q->query_form_hash;
171 0         0     $self->{response} = undef;
172 0         0     $self->{root_url} = $ENV{SCRIPT_NAME};
173 0         0     $self->{path} = $ENV{PATH_INFO};
174 0         0     $self->{params} = $params;
175               }
176              
177 10         38   bless $self, $class;
178             }
179              
180              
181              
182              
183             sub dispatch_request {
184 10     10 0 28   my ($self) = @_;
185 10         29   my $path_info = $self->{path};
186              
187             # security check : no outside directories
188 10 50       63   $path_info =~ m[(\.\.|//|\\|:)] and die "illegal path: $path_info";
189              
190 10 100       68   $path_info =~ s[^/][] or return $self->index_frameset;
191 9         29   for ($path_info) {
192 9 100       36     /^$/ and return $self->index_frameset;
193 8 100       32     /^index$/ and return $self->index_frameset;
194 7 100       30     /^toc$/ and return $self->main_toc;
195 6 100       27     /^toc\/(.*)$/ and return $self->toc_for($1); # Ajax calls
196 5 50       15     /^script\/(.*)$/ and return $self->serve_script($1);
197 5 100       23     /^search$/ and return $self->dispatch_search;
198 3 100       17     /^source\/(.*)$/ and return $self->serve_source($1);
199              
200             # for debugging
201 2 50       6     /^_dirs$/ and return $self->send_html(join "<br>", @search_dirs);
202              
203             # file extension : passthrough
204 2 100       11     /\.(\w+)$/ and return $self->serve_file($path_info, $1);
205              
206             #otherwise
207 1         6     return $self->serve_pod($path_info);
208               }
209             }
210              
211              
212             sub index_frameset{
213 3     3 0 8   my ($self) = @_;
214              
215             # initial page to open
216 3         7   my $ini = $self->{params}{open};
217 3   50     14   my $ini_content = $ini || "perl";
218 3 50       9   my $ini_toc = $ini ? "toc?open=$ini" : "toc";
219              
220             # HTML title
221 3   50     10   my $title = $self->{page_title} || 'Perl documentation';
222 3         8   $title =~ s/([&<>"])/$escape_entity{$1}/g;
223              
224 3         20   return $self->send_html(<<__EOHTML__);
225             <html>
226             <head><title>$title</title></head>
227             <frameset cols="25%, 75%">
228             <frame name="tocFrame" src="$self->{root_url}/$ini_toc">
229             <frame name="contentFrame" src="$self->{root_url}/$ini_content">
230             </frameset>
231             </html>
232             __EOHTML__
233             }
234              
235              
236              
237              
238             #----------------------------------------------------------------------
239             # serving a single file
240             #----------------------------------------------------------------------
241              
242             sub serve_source {
243 1     1 0 4   my ($self, $path) = @_;
244              
245 1         3   my $params = $self->{params};
246              
247             # default (if not printing): line numbers and syntax coloring are on
248 1 50       9   $params->{print} or $params->{lines} = $params->{coloring} = 1;
249              
250 1 50       4   my @files = $self->find_source($path) or die "No file for '$path'";
251 1         3   my $mtime = max map {(stat $_)[9]} @files;
  1         13  
252              
253 1         3   my $display_text;
254              
255 1         3   foreach my $file (@files) {
256 1         5     my $text = $self->slurp_file($file, ":crlf");
257                 my $view = $self->mk_view(
258                   line_numbering => $params->{lines},
259 1 50       14       syntax_coloring => ($params->{coloring} ? $coloring_package : "")
260                  );
261 1         5     $text = $view->view_verbatim($text);
262 1         53     $display_text .= "<p/><h2>$file</h2><p/><pre>$text</pre>";
263               }
264              
265              
266 1 50       7   my $offer_print = $params->{print} ? "" : <<__EOHTML__;
267             <form method="get" target="_blank">
268             <input type="submit" name="print" value="Print"> with<br>
269             <input type="checkbox" name="lines" checked>line numbers<br>
270             <input type="checkbox" name="coloring" checked>syntax coloring
271             </form>
272             __EOHTML__
273              
274 1 50       4   my $script = $params->{print} ? <<__EOHTML__ : "";
275             <script>
276             window.onload = function () {window.print()};
277             </script>
278             __EOHTML__
279              
280 1 50       6   my $doc_link = $params->{print} ? "" : <<__EOHTML__;
281             <a href="$self->{root_url}/$path" style="float:right">Doc</a>
282             __EOHTML__
283              
284 1         42   return $self->send_html(<<__EOHTML__, $mtime);
285             <html>
286             <head>
287             <title>Source of $path</title>
288             <link href="$self->{root_url}/Alien/GvaScript/lib/GvaScript.css" rel="stylesheet" type="text/css">
289             <link href="$self->{root_url}/Pod/POM/Web/lib/PodPomWeb.css" rel="stylesheet" type="text/css">
290             <style>
291             PRE {border: none; background: none}
292             FORM {float: right; font-size: 70%; border: 1px solid}
293             </style>
294             </head>
295             <body>
296             $doc_link
297             <h1>Source of $path</h1>
298            
299             $offer_print
300            
301             $display_text
302             </body>
303             </html>
304             __EOHTML__
305             }
306              
307              
308             sub serve_file {
309 1     1 0 4   my ($self, $path, $extension) = @_;
310              
311 1 50   8   7   my $fullpath = firstval {-f $_} map {"$_/$path"} @search_dirs
  8         117  
  10         31  
312                 or die "could not find $path";
313              
314 1         30   my $mime_type = MIME::Types->new->mimeTypeOf($extension);
315 1         54214   my $content = $self->slurp_file($fullpath, ":raw");
316 1         20   my $mtime = (stat $fullpath)[9];
317 1         12   $self->send_content({
318                 content => $content,
319                 mtime => $mtime,
320                 mime_type => $mime_type,
321               });
322             }
323              
324              
325              
326             sub serve_pod {
327 1     1 0 3   my ($self, $path) = @_;
328 1         2   $path =~ s[::][/]g; # just in case, if called as /perldoc/Foo::Bar
329              
330             # if several sources, will be first *.pod, then *.pm
331 1 50       7   my @sources = $self->find_source($path) or die "No file for '$path'";
332 1         7   my $mtime = max map {(stat $_)[9]} @sources;
  1         26  
333 1 50       11   my $content = $path =~ /\bperltoc\b/
334                                ? $self->fake_perltoc
335                                : $self->slurp_file($sources[0], ":crlf");
336              
337 1         10   (my $mod_name = $path) =~ s[/][::]g;
338 1 50       57   my $version = @sources > 1
339                 ? $self->parse_version($self->slurp_file($sources[-1], ":crlf"), $mod_name)
340                 : $self->parse_version($content, $mod_name);
341              
342 1         31   for my $filter (@podfilters) {
343 2         147     $filter->($content);
344               }
345              
346             # special handling for perlfunc: change initial C<..> to hyperlinks
347 1 50       9   if ($path =~ /\bperlfunc$/) {
348 0     0   0     my $sub = sub {my $txt = shift; $txt =~ s[C<(.*?)>][C<L</$1>>]g; $txt};
  0         0  
  0         0  
  0         0  
349 0         0     $content =~ s[(Perl Functions by Category)(.*?)(Alphabetical Listing)]
  0         0  
350             [$1 . $sub->($2) . $3]es;
351               }
352 1         13  
353 1 50       25   my $parser = Pod::POM->new;
354 1         14116   my $pom = $parser->parse_text($content) or die $parser->error;
355               my $view = $self->mk_view(version => $version,
356                                         mtime => $mtime,
357                                         path => $path,
358                                         mod_name => $mod_name,
359                                         syntax_coloring => $coloring_package);
360 1         8  
361               my $html = $view->print($pom);
362              
363 1 50       546 # again special handling for perlfunc : ids should be just function names
364 0         0   if ($path =~ /\bperlfunc$/) {
365                 $html =~ s/li id="(.*?)_.*?"/li id="$1"/g;
366               }
367              
368 1 50       6 # special handling for 'perl' : hyperlinks to man pages
369 0     0   0   if ($path =~ /\bperl$/) {
370 0         0     my $sub = sub {my $txt = shift;
371 0         0                    $txt =~ s[(perl\w+)]
  0         0  
372 0         0 [<a href="$self->{root_url}/$1">$1</a>]g;
  0         0  
373                                return $txt};
374                 $html =~ s[(<pre.*?</pre>)][$sub->($1)]egs;
375 1         8   }
376              
377               return $self->send_html($html, $mtime);
378             }
379 0     0 0 0  
380             sub fake_perltoc {
381 0         0   my ($self) = @_;
382              
383               return "=head1 NAME\n\nperltoc\n\n=head1 DESCRIPTION\n\n"
384                    . "I<Sorry, this page cannot be displayed in HTML by Pod:POM::Web "
385                    . "(too CPU-intensive). "
386                    . "If you really need it, please consult the source, using the link "
387                    . "in the top-right corner.>";
388             }
389              
390 0     0 0 0  
391             sub serve_script {
392 0         0   my ($self, $path) = @_;
393              
394               my $fullpath;
395 0         0  
396 0         0  DIR:
397 0         0   foreach my $dir (@script_dirs) {
398 0 0       0     foreach my $ext ("", ".pl", ".bat") {
399                   $fullpath = "$dir/$path$ext";
400                   last DIR if -f $fullpath;
401                 }
402 0 0       0   }
403              
404 0         0   $fullpath or die "no such script : $path";
405 0         0  
406               my $content = $self->slurp_file($fullpath, ":crlf");
407 0         0   my $mtime = (stat $fullpath)[9];
408 0         0  
409               for my $filter (@podfilters) {
410                 $filter->($content);
411 0         0   }
412 0 0       0  
413 0         0   my $parser = Pod::POM->new;
414               my $pom = $parser->parse_text($content) or die $parser->error;
415               my $view = $self->mk_view(path => "scripts/$path",
416 0         0                               mtime => $mtime,
417                                           syntax_coloring => $coloring_package);
418 0         0   my $html = $view->print($pom);
419              
420               return $self->send_html($html, $mtime);
421             }
422              
423 16     16 0 3265  
424             sub find_source {
425               my ($self, $path) = @_;
426 16 50       89  
427             # serving a script ? # TODO : factorize common code with serve_script
428 0         0   if ($path =~ s[^scripts/][]) {
429 0         0   DIR:
430 0 0       0     foreach my $dir (@script_dirs) {
431 0         0       foreach my $ext ("", ".pl", ".bat") {
432                     -f "$dir/$path$ext" or next;
433                     return ("$dir/$path$ext");
434 0         0       }
435                 }
436                 return;
437               }
438 16         71  
439 156         578 # otherwise, serving a module
  624         4658  
440               foreach my $prefix (@search_dirs) {
441                 my @found = grep {-f} ("$prefix/$path.pod",
442                                         "$prefix/$path.pm",
443 156 100       502                             "$prefix/pod/$path.pod",
444                                         "$prefix/pods/$path.pod");
445 0         0     return @found if @found;
446               }
447               return;
448             }
449              
450 11     11 0 45  
451 11         52 sub pod2pom {
452               my ($self, $sourcefile) = @_;
453 11         58   my $content = $self->slurp_file($sourcefile, ":crlf");
454 22         22345  
455               for my $filter (@podfilters) {
456                 $filter->($content);
457 11         138   }
458 11 50       477  
459 11         2047947   my $parser = Pod::POM->new;
460               my $pom = $parser->parse_text($content) or die $parser->error;
461               return $pom;
462             }
463              
464             #----------------------------------------------------------------------
465             # tables of contents
466             #----------------------------------------------------------------------
467              
468 1     1 0 8  
469             sub toc_for { # partial toc (called through Ajax)
470               my ($self, $prefix) = @_;
471 1         6  
472 1 50       6 # special handling for builtin paths
473 1 50       5   for ($prefix) {
474 1 50       8     /^perldocs$/ and return $self->toc_perldocs;
475                 /^pragmas$/ and return $self->toc_pragmas;
476                 /^scripts$/ and return $self->toc_scripts;
477               }
478 1         7  
479 1 50       7 # otherwise, find and htmlize entries under a given prefix
480 0         0   my $entries = $self->find_entries_for($prefix);
481               if ($prefix eq 'Pod') { # Pod/perl* should not appear under Pod
482 1         9     delete $entries->{$_} for grep /^perl/, keys %$entries;
483               }
484               return $self->send_html($self->htmlize_entries($entries));
485             }
486              
487 0     0 0 0  
488             sub toc_perldocs {
489 0         0   my ($self) = @_;
490              
491               my %perldocs;
492 0         0  
493 0         0 # perl basic docs may be found under "pod", "pods", or the root dir
494               for my $subdir (qw/pod pods/, "") {
495                 my $entries = $self->find_entries_for($subdir);
496 0         0  
497 0         0 # just keep the perl* entries, without subdir prefix
498 0         0     foreach my $key (grep /^perl/, keys %$entries) {
499                   $perldocs{$key} = $entries->{$key};
500                   $perldocs{$key}{node} =~ s[^subdir/][]i;
501                 }
502 0         0   }
503              
504               return $self->send_html($self->htmlize_perldocs(\%perldocs));
505             }
506              
507              
508 0     0 0 0  
509             sub toc_pragmas {
510 0         0   my ($self) = @_;
511 0         0  
512 0 0       0   my $entries = $self->find_entries_for(""); # files found at root level
  0         0  
513               delete $entries->{$_} for @ignore_toc_dirs, qw/pod pods inc/;
514 0         0   delete $entries->{$_} for grep {/^perl/ or !/^[[:lower:]]/} keys %$entries;
515              
516               return $self->send_html($self->htmlize_entries($entries));
517             }
518              
519 0     0 0 0  
520             sub toc_scripts {
521 0         0   my ($self) = @_;
522              
523               my %scripts;
524 0         0  
525 0 0       0 # gather all scripts and group them by initial letter
526               foreach my $dir (@script_dirs) {
527 0         0     opendir my $dh, $dir or next;
528 0         0   NAME:
529 0 0 0     0     foreach my $name (readdir $dh) {
      0        
530                   for ("$dir/$name") {
531 0         0         -x && !-d && -T or next NAME ; # try to just keep Perl executables
532 0         0       }
533 0         0       $name =~ s/\.(pl|bat)$//i;
534                   my $letter = uc substr $name, 0, 1;
535                   $scripts{$letter}{$name} = {node => "script/$name", pod => 1};
536                 }
537               }
538 0         0  
539 0         0 # htmlize the structure
540 0         0   my $html = "";
541 0         0   foreach my $letter (sort keys %scripts) {
542                 my $content = $self->htmlize_entries($scripts{$letter});
543                 $html .= closed_node(label => $letter,
544                                      content => $content);
545 0         0   }
546              
547               return $self->send_html($html);
548             }
549              
550 1     1 0 5  
551             sub find_entries_for {
552               my ($self, $prefix) = @_;
553              
554 1         3 # if $prefix is of shape A*, we want top-level modules starting
555 1 50       6 # with that letter
556 0         0   my $filter;
557 0         0   if ($prefix =~ /^([A-Z])\*/) {
558                 $filter = qr/^$1/;
559                 $prefix = "";
560 1         5   }
561              
562 1         5   my %entries;
563 10 50       48  
564 10 100       275   foreach my $root_dir (@search_dirs) {
565 2         65     my $dirname = $prefix ? "$root_dir/$prefix" : $root_dir;
566 15 100       90     opendir my $dh, $dirname or next;
567 11 50 33     40     foreach my $name (readdir $dh) {
568 11         143       next if $name =~ /^\./;
569 11         76       next if $filter and $name !~ $filter;
570                   my $is_dir = -d "$dirname/$name";
571                   my $has_pod = $name =~ s/\.(pm|pod)$//;
572 11 50 66     50  
  20         151  
573             # skip if this subdir is a member of @INC (not a real module namespace)
574 11 50 66     55       next if $is_dir and grep {m[^\Q$dirname/$name\E]} @search_dirs;
575 11 50       72  
576 11 100       32       if ($is_dir || $has_pod) { # found a TOC entry
577 11 100       48         $entries{$name}{node} = $prefix ? "$prefix/$name" : $name;
578                     $entries{$name}{dir} = 1 if $is_dir;
579                     $entries{$name}{pod} = 1 if $has_pod;
580                   }
581 1         6     }
582               }
583               return \%entries;
584             }
585              
586 0     0 0 0  
587 0         0 sub htmlize_perldocs {
588               my ($self, $perldocs) = @_;
589               my $parser = Pod::POM->new;
590 0 0       0  
591             # Pod/perl.pom Synopsis contains a classification of perl*.pod documents
592 0         0   my ($perlpod) = $self->find_source("perl", ":crlf")
593 0 0       0       or die "'perl.pod' does not seem to be installed on this system";
594               my $source = $self->slurp_file($perlpod);
595 0     0   0   my $perlpom = $parser->parse_text($source) or die $parser->error;
596 0   0 0   0  
  0         0  
597 0         0   my $h1 = (firstval {$_->title eq 'GETTING HELP'} $perlpom->head1)
598                      || (firstval {$_->title eq 'SYNOPSIS'} $perlpom->head1);
599               my $html = "";
600 0         0  
601 0         0 # classified pages mentioned in the synopsis
602 0         0   foreach my $h2 ($h1->head2) {
603                 my $title = $h2->title;
604                 my $content = $h2->verbatim;
605 0         0  
606             # "Internals and C-Language Interface" is too long
607                 $title =~ s/^Internals.*/Internals/;
608 0         0  
609 0         0 # gather leaf entries
610 0         0     my @leaves;
611 0 0       0     while ($content =~ /^\s*(perl\S*?)\s*\t(.+)/gm) {
612                   my ($ref, $descr) = ($1, $2);
613                   my $entry = delete $perldocs->{$ref} or next;
614 0         0       push @leaves, {label => $ref,
615                                  href => $entry->{node},
616                                  attrs => qq{id='$ref' title='$descr'}};
617 0         0     }
618 0         0 # sort and transform into HTML
  0         0  
619 0         0     @leaves = map {leaf(%$_)}
620                           sort {$a->{label} cmp $b->{label}} @leaves;
621                 $html .= closed_node(label => $title,
622                                      content => join("\n", @leaves));
623               }
624 0 0       0  
625 0         0 # maybe some remaining pages
626               if (keys %$perldocs) {
627                 $html .= closed_node(label => 'Unclassified',
628                                      content => $self->htmlize_entries($perldocs));
629 0         0   }
630              
631               return $html;
632             }
633              
634              
635              
636 1     1 0 6  
637 1         4 sub htmlize_entries {
638 1         16   my ($self, $entries) = @_;
  17         42  
639 9         28   my $html = "";
640 9         49   foreach my $name (sort {uc($a) cmp uc($b)} keys %$entries) {
641 9         48     my $entry = $entries->{$name};
642                 (my $id = $entry->{node}) =~ s[/][::]g;
643                 my %args = (class => 'TN_leaf',
644 9 100       32                 label => $name,
645 2         7                 attrs => qq{id='$id'});
646 2         8     if ($entry->{dir}) {
647                   $args{class} = 'TN_node TN_closed';
648 9 50       32       $args{attrs} .= qq{ TN:contentURL='toc/$entry->{node}'};
649 9         27     }
650 9         33     if ($entry->{pod}) {
651                   $args{href} = $entry->{node};
652 9         40       $args{abstract} = $self->get_abstract($entry->{node});
653                 }
654 1         9     $html .= generic_node(%args);
655               }
656               return $html;
657       9 0   }
658              
659             sub get_abstract {
660             # override in indexer
661             }
662              
663              
664              
665 1     1 0 5  
666             sub main_toc {
667               my ($self) = @_;
668 1         4  
669 1 50       6 # initial page to open
670               my $ini = $self->{params}{open};
671               my $select_ini = $ini ? "selectToc('$ini');" : "";
672 1         7  
  226         7146  
  378         673831  
673 1         112 # perlfunc entries in JSON format for the DHTML autocompleter
674 1         68109   my @funcs = map {$_->title} grep {$_->content =~ /\S/} $self->perlfunc_items;
  224         12533  
675               s|[/\s(].*||s foreach @funcs;
676               my $json_funcs = "[" . join(",", map {qq{"$_"}} uniq @funcs) . "]";
677 1         70  
  140         5725  
  161         168117  
  85         225  
678 1         66 # perlVAR entries in JSON format for the DHTML autocompleter
679 1         46222   my @vars = map {$_->title} grep {!/->/} map {@$_} $self->perlvar_items;
680 1         3254   s|\s*X<.*||s foreach @vars;
681 1         3213   s|\\|\\\\|g foreach @vars;
  140         5088  
682               s|"|\\"|g foreach @vars;
683 1 50       17   my $json_vars = "[" . join(",", map {qq{"$_"}} uniq @vars) . "]";
684              
685 1         4   my $js_no_indexer = $no_indexer ? 'true' : 'false';
  3         12  
686              
687               my @perl_sections = map {closed_node(
688                   label => ucfirst($_),
689                   label_class => "TN_label small_title",
690                   attrs => qq{TN:contentURL='toc/$_' id='$_'},
691 1         3      )} qw/perldocs pragmas scripts/;
692 1         6  
693 26         55   my $alpha_list = "";
694               for my $letter ('A' .. 'Z') {
695                 $alpha_list .= closed_node (
696                   label => $letter,
697                   label_class => "TN_label",
698                   attrs => qq{TN:contentURL='toc/$letter*' id='${letter}:'},
699 1         3      );
700               }
701               my $modules = generic_node (label => "Modules",
702                                           label_class => "TN_label small_title",
703                                           content => $alpha_list);
704 1         50  
705              
706               return $self->send_html(<<__EOHTML__);
707             <html>
708             <head>
709             <base target="contentFrame">
710             <link href="$self->{root_url}/Alien/GvaScript/lib/GvaScript.css"
711             rel="stylesheet" type="text/css">
712             <link href="$self->{root_url}/Pod/POM/Web/lib/PodPomWeb.css"
713             rel="stylesheet" type="text/css">
714             <script src="$self->{root_url}/Alien/GvaScript/lib/prototype.js"></script>
715             <script src="$self->{root_url}/Alien/GvaScript/lib/GvaScript.js"></script>
716             <script>
717             var treeNavigator;
718             var perlfuncs = $json_funcs;
719             var perlvars = $json_vars;
720             var completers = {};
721             var no_indexer = $js_no_indexer;
722            
723             function submit_on_event(event) {
724             \$('search_form').submit();
725             }
726            
727             function resize_tree_navigator() {
728             // compute available height -- comes either from body or documentElement,
729             // depending on browser and on compatibility mode !!
730             var doc_el_height = document.documentElement.clientHeight;
731             var avail_height
732             = (Prototype.Browser.IE && doc_el_height) ? doc_el_height
733             : document.body.clientHeight;
734            
735             var tree_height = avail_height - \$('toc_frame_top').scrollHeight - 5;
736             if (tree_height > 100)
737             \$('TN_tree').style.height = tree_height + "px";
738             }
739            
740             function open_nodes(first_node, rest) {
741            
742             var node = \$(first_node);
743             if (!node || !treeNavigator) return;
744            
745             // shift to next node in sequence
746             first_node = rest.shift();
747            
748             // build a handler for "onAfterLoadContent" (closure on first_node/rest)
749             var open_or_select_next = function() {
750            
751             // delete handler that might have been placed by previous call
752             delete treeNavigator.onAfterLoadContent;
753            
754             //
755             if (rest.length > 0) {
756             open_nodes(first_node, rest)
757             }
758             else {
759             treeNavigator.openEnclosingNodes(\$(first_node));
760             treeNavigator.select(\$(first_node));
761             }
762             };
763            
764            
765             // if node is closed and currently has no content, we need to register
766             // a handler, open the node so that it gets its content by Ajax,
767             // and then execute the handler to open the rest after Ajax returns
768             if (treeNavigator.isClosed(node)
769             && !treeNavigator.content(node)) {
770             treeNavigator.onAfterLoadContent = open_or_select_next;
771             treeNavigator.open(node);
772             }
773             // otherwise just a direct call
774             else {
775             open_or_select_next();
776             }
777            
778             }
779            
780            
781             function selectToc(entry) {
782            
783             // build array of intermediate nodes (i.e "Foo", "Foo::Bar", etc.)
784             var parts = entry.split(new RegExp("/|::"));
785             var accu = '';
786             var sequence = parts.map(function(e) {
787             accu = accu ? (accu + "::" + e) : e;
788             return accu;
789             });
790            
791             // choose id of first_node by analysis of entry
792             var initial = entry.substr(0, 1);
793             var first_node
794            
795             // CASE module (starting with uppercase)
796             = (initial <= 'Z') ? (initial + ":")
797            
798             // CASE perl* documentation page
799             : entry.search(/^perl/) > -1 ? "perldocs"
800            
801             // CASE other lowercase entries
802             : "pragmas"
803             ;
804            
805             // open each node in sequence
806             open_nodes(first_node, sequence);
807             }
808            
809             function setup() {
810            
811             treeNavigator
812             = new GvaScript.TreeNavigator('TN_tree', {tabIndex:-1});
813            
814             completers.perlfunc = new GvaScript.AutoCompleter(
815             perlfuncs,
816             {minimumChars: 1,
817             minWidth: 100,
818             offsetX: -20,
819             autoSuggestDelay: 400});
820             completers.perlfunc.onComplete = submit_on_event;
821            
822             completers.perlvar = new GvaScript.AutoCompleter(
823             perlvars,
824             {minimumChars: 1,
825             minWidth: 100,
826             offsetX: -20,
827             autoSuggestDelay: 400});
828             completers.perlvar.onComplete = submit_on_event;
829            
830             if (!no_indexer) {
831             completers.modlist = new GvaScript.AutoCompleter(
832             "search?source=modlist&search=",
833             {minimumChars: 2, minWidth: 100, offsetX: -20, typeAhead: false});
834             completers.modlist.onComplete = submit_on_event;
835             }
836            
837             resize_tree_navigator();
838             $select_ini
839             }
840            
841             document.observe('dom:loaded', setup);
842             window.onresize = resize_tree_navigator;
843             // Note: observe('resize') doesn't work. Why ?
844            
845             function displayContent(event) {
846             var label = event.controller.label(event.target);
847             if (label && label.tagName == "A") {
848             label.focus();
849             return Event. stopNone;
850             }
851             }
852            
853             function maybe_complete(input) {
854             if (input._autocompleter)
855             input._autocompleter.detach(input);
856            
857             switch (input.form.source.selectedIndex) {
858             case 0: completers.perlfunc.autocomplete(input); break;
859             case 1: completers.perlvar.autocomplete(input); break;
860             case 3: if (!no_indexer)
861             completers.modlist.autocomplete(input);
862             break;
863             }
864             }
865            
866            
867             </script>
868             <style>
869             .small_title {color: midnightblue; font-weight: bold; padding: 0 3 0 3}
870             FORM {margin:0px}
871             BODY {margin:0px; font-size: 70%; overflow-x: hidden}
872             DIV {margin:0px; width: 100%}
873             #TN_tree {overflow-y:scroll; overflow-x: hidden}
874             </style>
875             </head>
876             <body>
877            
878             <div id='toc_frame_top'>
879             <div class="small_title"
880             style="text-align:center;border-bottom: 1px solid">
881             Perl Documentation
882             </div>
883             <div style="text-align:right">
884             <a href="Pod/POM/Web/Help" class="small_title">Help</a>
885             </div>
886            
887             <form action="search" id="search_form" method="get">
888             <span class="small_title">Search in</span>
889             <select name="source">
890             <option>perlfunc</option>
891             <option>perlvar</option>
892             <option>perlfaq</option>
893             <option>modules</option>
894             <option>fulltext</option>
895             </select><br>
896             <span class="small_title">&nbsp;for&nbsp;</span><input
897             name="search" size="15"
898             autocomplete="off"
899             onfocus="maybe_complete(this)">
900             </form>
901             <br>
902             <div class="small_title"
903             style="border-bottom: 1px solid">Browse</div>
904             </div>
905            
906             <!-- In principle the tree navigator below would best belong in a
907             different frame, but instead it's in a div because the autocompleter
908             from the form above sometimes needs to overlap the tree nav. -->
909             <div id='TN_tree' onPing='displayContent'>
910             @perl_sections
911             $modules
912             </div>
913            
914             </body>
915             </html>
916             __EOHTML__
917             }
918              
919             #----------------------------------------------------------------------
920             # searching
921             #----------------------------------------------------------------------
922 2     2 0 8  
923             sub dispatch_search {
924 2         6   my ($self) = @_;
925 2         5  
926               my $params = $self->{params};
927               my $source = $params->{source};
928               my $method = {perlfunc => 'perlfunc',
929                             perlvar => 'perlvar',
930                             perlfaq => 'perlfaq',
931                             modules => 'serve_pod',
932 2 50       21                 fulltext => 'fulltext',
933                             modlist => 'modlist',
934 2 50 33     15                 }->{$source} or die "cannot search in '$source'";
935 0         0  
936               if ($method =~ /fulltext|modlist/ and $no_indexer) {
937                 die "<p>this method requires <b>Search::Indexer</b></p>"
938                   . "<p>please ask your system administrator to install it</p>"
939                   . "(<small>error message : $no_indexer</small>)";
940 2         14   }
941              
942               return $self->$method($params->{search});
943             }
944              
945              
946              
947             my @_perlfunc_items; # simple-minded cache
948 2     2 0 6  
949             sub perlfunc_items {
950 2 100       10   my ($self) = @_;
951 1 50       4  
952               unless (@_perlfunc_items) {
953 1         6     my ($funcpod) = $self->find_source("perlfunc")
954 1         19       or die "'perlfunc.pod' does not seem to be installed on this system";
  2         304  
955                 my $funcpom = $self->pod2pom($funcpod);
956 1         101     my ($description) = grep {$_->title eq 'DESCRIPTION'} $funcpom->head1;
  4         821  
957 1         101     my ($alphalist)
958                   = grep {$_->title =~ /^Alphabetical Listing/i} $description->head2;
959 2         25354     @_perlfunc_items = $alphalist->over->[0]->item;
960               };
961               return @_perlfunc_items;
962             }
963              
964 1     1 0 4  
965 1 50       5 sub perlfunc {
  378         90877  
966               my ($self, $func) = @_;
967               my @items = grep {$_->title =~ /^$func\b/} $self->perlfunc_items
968                  or return $self->send_html("No documentation found for perl "
969 1         174                                ."function '<tt>$func</tt>'");
970              
971 1         5   my $view = $self->mk_view(path => "perlfunc/$func");
  2         123  
972 1         150  
973               my @li_items = map {$_->present($view)} @items;
974               return $self->send_html(<<__EOHTML__);
975             <html>
976             <head>
977             <link href="$self->{root_url}/Pod/POM/Web/lib/PodPomWeb.css" rel="stylesheet" type="text/css">
978             </head>
979             <body>
980             <h2>Extract from <a href="$self->{root_url}/perlfunc">perlfunc</a></h2>
981            
982             <ul>@li_items</ul>
983             </body>
984             __EOHTML__
985             }
986              
987              
988              
989              
990              
991             my @_perlvar_items; # simple-minded cache
992 1     1 0 7  
993             sub perlvar_items {
994 1 50       10   my ($self) = @_;
995              
996               unless (@_perlvar_items) {
997 1 50       8  
998             # get items defining variables
999 1         8     my ($varpod) = $self->find_source("perlvar")
1000 1         10       or die "'perlvar.pod' does not seem to be installed on this system";
1001                 my $varpom = $self->pod2pom($varpod);
1002                 my @items = _extract_items($varpom);
1003 1         8  
1004 1         5 # group items having common content
1005 161         3181     my $tmp = [];
1006 161 100       1287     foreach my $item (@items) {
1007 85         128132       push @$tmp, $item;
1008 85         438       if ($item->content . "") { # force stringification
1009                     push @_perlvar_items, $tmp;
1010                     $tmp = [];
1011                   }
1012 1         7991     }
1013               };
1014               return @_perlvar_items;
1015             }
1016              
1017 0     0 0 0  
1018             sub perlvar {
1019 0 0   0   0   my ($self, $var) = @_;
  0         0  
  0         0  
1020              
1021               my @items = grep {any {$_->title =~ /^\Q$var\E(\s|$)/} @$_}
1022                                $self->perlvar_items
1023 0         0      or return $self->send_html("No documentation found for perl "
1024                                            ."variable '<tt>$var</tt>'");
1025 0         0   my $view = $self->mk_view(path => "perlvar/$var");
  0         0  
  0         0  
1026 0         0  
1027               my @li_items = map {$_->present($view)} map {@$_} @items;
1028               return $self->send_html(<<__EOHTML__);
1029             <html>
1030             <head>
1031             <link href="$self->{root_url}/Pod/POM/Web/lib/PodPomWeb.css" rel="stylesheet" type="text/css">
1032             </head>
1033             <body>
1034             <h2>Extract from <a href="$self->{root_url}/perlvar">perlvar</a></h2>
1035            
1036             <ul>@li_items</ul>
1037             </body>
1038             __EOHTML__
1039             }
1040              
1041              
1042 1     1 0 5  
1043 1         19 sub perlfaq {
1044 1         2   my ($self, $faq_entry) = @_;
1045 1         2   my $regex = qr/\b\Q$faq_entry\E\b/i;
1046               my $answers = "";
1047 1         4   my $n_answers = 0;
1048              
1049               my $view = $self->mk_view(path => "perlfaq/$faq_entry");
1050 1         5  
1051 9         134612  FAQ:
1052 9 50       60   for my $num (1..9) {
1053                 my $faq = "perlfaq$num";
1054 9         38     my ($faqpod) = $self->find_source($faq)
1055 9 100       98       or die "'$faq.pod' does not seem to be installed on this system";
  42         3138  
  305         47002  
1056                 my $faqpom = $self->pod2pom($faqpod);
1057 4         95     my @questions = map {grep {$_->title =~ $regex} $_->head2} $faqpom->head1
  16         1076  
1058 4         308       or next FAQ;
1059                 my @nodes = map {$view->print($_)} @questions;
1060                 $answers .= generic_node(label => "Found in perlfaq$num",
1061 4         56                              label_tag => "h2",
1062                                          content => join("", @nodes));
1063                 $n_answers += @nodes;
1064 1         8774   }
1065              
1066               return $self->send_html(<<__EOHTML__);
1067             <html>
1068             <head>
1069             <link href="$self->{root_url}/Alien/GvaScript/lib/GvaScript.css" rel="stylesheet" type="text/css">
1070             <link href="$self->{root_url}/Pod/POM/Web/lib/PodPomWeb.css" rel="stylesheet" type="text/css">
1071             <script src="$self->{root_url}/Alien/GvaScript/lib/prototype.js"></script>
1072             <script src="$self->{root_url}/Alien/GvaScript/lib/GvaScript.js"></script>
1073             <script>
1074             var treeNavigator;
1075             function setup() {
1076             treeNavigator = new GvaScript.TreeNavigator('TN_tree');
1077             }
1078             window.onload = setup;
1079             </script>
1080             </head>
1081             <body>
1082             <h1>Extracts from <a href="$self->{root_url}/perlfaq">perlfaq</a></h1><br>
1083             <em>searching for '$faq_entry' : $n_answers answers</em><br><br>
1084             <div id='TN_tree'>
1085             $answers
1086             </div>
1087             </body>
1088             __EOHTML__
1089              
1090             }
1091              
1092              
1093             #----------------------------------------------------------------------
1094             # miscellaneous
1095             #----------------------------------------------------------------------
1096              
1097 4     4 0 30  
1098             sub mk_view {
1099               my ($self, %args) = @_;
1100              
1101 4         91   my $view = Pod::POM::View::HTML::_PerlDoc->new(
1102                 root_url => $self->{root_url},
1103                 %args
1104 4         148    );
1105              
1106               return $view;
1107             }
1108              
1109              
1110 9     9 0 38  
1111             sub send_html {
1112               my ($self, $html, $mtime) = @_;
1113 9         93  
1114             # dirty hack for MSIE8 (TODO: send proper HTTP header instead)
1115 9         73   $html =~ s[<head>]
1116             [<head>\n<meta http-equiv="X-UA-Compatible" content="IE=edge">];
1117              
1118               $self->send_content({content => $html, code => 200, mtime => $mtime});
1119             }
1120              
1121 10     10 0 30  
1122 10         67  
1123 10 50       63311 sub send_content {
1124 10         95   my ($self, $args) = @_;
1125 10         42   my $encoding = guess_encoding($args->{content}, qw/ascii utf8 latin1/);
1126 10   100     75   my $charset = ref $encoding ? $encoding->name : "";
1127 10 100 66     96      $charset =~ s/^ascii/US-ascii/; # Firefox insists on that imperialist name
1128 10         156   my $length = length $args->{content};
1129 10   100     46   my $mime_type = $args->{mime_type} || "text/html";
1130                  $mime_type .= "; charset=$charset" if $charset and $mime_type =~ /html/;
1131 10         31   my $modified = gmtime $args->{mtime};
1132 10         47   my $code = $args->{code} || 200;
1133              
1134 10 50       42   my $r = $self->{response};
1135 0         0   for (ref $r) {
1136 0         0  
1137 0         0     /^Apache/ and do {
1138 0 0       0       require Apache2::Response;
1139 0         0       $r->content_type($mime_type);
1140 0         0       $r->set_content_length($length);
1141                   $r->set_last_modified($args->{mtime}) if $args->{mtime};
1142                   $r->print($args->{content});
1143 10 50       52       return;
1144 10         67     };
1145 10         234  
1146                 /^HTTP::Response/ and do {
1147 10 100       1341       $r->code($code);
1148 10         211       $r->header(Content_type => $mime_type,
1149 10         612                  Content_length => $length);
1150                   $r->header(Last_modified => $modified) if $args->{mtime};
1151                   $r->add_content($args->{content});
1152                   return;
1153 0         0     };
1154 0 0       0  
1155 0         0 # otherwise (cgi-bin)
1156 0         0     my $headers = "Content-type: $mime_type\nContent-length: $length\n";
1157 0         0     $headers .= "Last-modified: $modified\n" if $args->{mtime};
1158                 binmode(STDOUT);
1159                 print "$headers\n$args->{content}";
1160                 return;
1161               }
1162             }
1163              
1164              
1165              
1166              
1167             #----------------------------------------------------------------------
1168             # generating GvaScript treeNavigator structure
1169 43     43 0 163 #----------------------------------------------------------------------
1170 43   100     110  
1171 43   66     164 sub generic_node {
1172 43   100     148   my %args = @_;
1173 43   66     148   $args{class} ||= "TN_node";
1174               $args{attrs} &&= " $args{attrs}";
1175 43 100       108   $args{content} ||= "";
1176               $args{content} &&= qq{<div class="TN_content">$args{content}</div>};
1177 43   66     156   my ($default_label_tag, $label_attrs)
1178 43   100     149     = $args{href} ? ("a", qq{ href='$args{href}'})
1179 43 50       80                   : ("span", "" );
1180 0         0   $args{label_tag} ||= $default_label_tag;
1181 0         0   $args{label_class} ||= "TN_label";
1182               if ($args{abstract}) {
1183                 $args{abstract} =~ s/([&<>"])/$escape_entity{$1}/g;
1184                 $label_attrs .= qq{ title="$args{abstract}"};
1185               }
1186               return qq{<div class="$args{class}"$args{attrs}>}
1187                    . qq{<$args{label_tag} class="$args{label_class}"$label_attrs>}
1188 43         338        . $args{label}
1189                    . qq{</$args{label_tag}>}
1190                    . $args{content}
1191                    . qq{</div>};
1192             }
1193 29     29 0 48  
1194              
1195             sub closed_node {
1196               return generic_node(@_, class => "TN_node TN_closed");
1197 0     0 0 0 }
1198              
1199             sub leaf {
1200               return generic_node(@_, class => "TN_leaf");
1201             }
1202              
1203              
1204             #----------------------------------------------------------------------
1205             # utilities
1206             #----------------------------------------------------------------------
1207 14     14 0 109  
1208 14 50       893  
1209 14 50       222 sub slurp_file {
1210 14         145   my ($self, $file, $io_layer) = @_;
1211 14         3846   open my $fh, $file or die "open $file: $!";
1212               binmode($fh, $io_layer) if $io_layer;
1213               local $/ = undef;
1214               return <$fh>;
1215             }
1216 1     1 0 3  
1217              
1218             sub parse_version {
1219 1     1   34   my ($self, $content, $mod_name) = @_;
  1         2  
  1         7  
  1         47  
1220 1 50       655  
1221             # filehandle on string content
1222               open my $fh, "<", \$content;
1223 1         3321   my $mm = Module::Metadata->new_from_handle($fh, $mod_name)
1224                 or die "couldn't create Module::Metadata";
1225              
1226               return $mm->version;
1227             }
1228              
1229 247     247   506  
1230              
1231 247         7672 sub _extract_items { # recursively grab all nodes of type 'item'
1232 247 100       6429   my $node = shift;
1233 86 100       445  
  246         1116  
1234               for ($node->type) {
1235 66         197     /^item/ and return ($node);
1236                 /^(pod|head|over)/ and return map {_extract_items($_)} $node->content;
1237               }
1238               return ();
1239             }
1240              
1241              
1242             1;
1243             #======================================================================
1244             # END OF package Pod::POM::Web
1245             #======================================================================
1246              
1247              
1248 1     1   22 #======================================================================
  1         4  
  1         47  
1249 1     1   9 package Pod::POM::View::HTML::_PerlDoc; # View package
  1         4  
  1         50  
1250 1     1   8 #======================================================================
  1         3  
  1         65  
1251 1     1   10 use strict;
  1         3  
  1         631  
1252 1     1   7427 use warnings;
  1         8377  
  1         19  
1253 1     1   2314 no warnings qw/uninitialized/;
  1         4  
  1         17  
1254             use base qw/Pod::POM::View::HTML/;
1255             use POSIX qw/strftime/; # date formatting
1256             use List::MoreUtils qw/firstval/;
1257              
1258             # SUPER::view_seq_text tries to find links automatically ... but is buggy
1259 434     434   10784 # for URLs that contain '$' or ' '. So we disable it, and only consider
1260             # links explicitly marked with L<..>, handled in view_seq_link() below.
1261 434         829 sub view_seq_text {
1262 434         915   my ($self, $text) = @_;
1263 434         698  
1264 434         954   for ($text) {
1265                 s/&/&amp;/g;
1266                 s/</&lt;/g;
1267 434         1865     s/>/&gt;/g;
1268               }
1269              
1270               return $text;
1271             }
1272              
1273              
1274 35     35   449  
1275             # SUPER::view_seq_link needs some adaptations
1276             sub view_seq_link {
1277                 my ($self, $link) = @_;
1278 35         63  
1279 35 100       176 # we handle the L<link_text|...> syntax here, because we also want
1280             # link_text for http URLS (not supported by SUPER::view_seq_link)
1281                 my $link_text;
1282 35         96     $link =~ s/^([^|]+)\|// and $link_text = $1;
1283              
1284             # links to external resources will open in a blank page
1285 35         137     my $is_external_resource = ($link =~ m[^\w+://]);
1286 35         920  
1287             # call parent and reparse the result
1288                 my $linked = $self->SUPER::view_seq_link($link);
1289 35 100       170     my ($url, $label) = ($linked =~ m[^<a href="(.*?)">(.*)</a>]);
  15         46  
1290              
1291             # fix link for 'hash' part of the url
1292 35 100       130     $url =~ s[#(.*)]['#' . _title_to_id($1)]e unless $is_external_resource;
    100          
1293 10         23  
1294             # if explicit link_text given by client, take that as label, unchanged
1295                 if ($link_text) {
1296                   $label = $link_text;
1297 24 50       56     }
  5         27  
1298             # if "$page/$section", replace by "$section in $page"
1299                 elsif ($label !~ m{^\w+://}s) { # but only if not a full-blown URL
1300                   $label =~ s[^(.*?)/(.*)$][$1 ? "$2 in $1" : $2]e ;
1301 35 100       96     }
1302 35         211  
1303             # return link (if external resource, opens in a new browser window)
1304                 my $target = $is_external_resource ? " target='_blank'" : "";
1305                 return qq{<a href="$url"$target>$label</a>};
1306             }
1307              
1308 37     37   780  
1309 37         117  
1310 37         130 sub view_seq_link_transform_path {
1311                 my($self, $page) = @_;
1312                 $page =~ s[::][/]g;
1313                 return "$self->{root_url}/$page";
1314             }
1315 21     21   2361  
1316              
1317 21   50     51 sub view_item {
1318 21 50       281   my ($self, $item) = @_;
1319              
1320 21         49   my $title = eval {$item->title->present($self)} || "";
1321 21         39      $title = "" if $title =~ /^\s*\*\s*$/;
1322              
1323 21 50       69   my $class = "";
1324 0         0   my $id = "";
1325              
1326               if ($title =~ /^AnnoCPAN/) {
1327 21         99     $class = " class='AnnoCPAN'";
1328 21   33     139   }
1329               else {
1330                 $id = _title_to_id($title);
1331 21         168     $id &&= qq{ id="$id"};
1332 21 50       853   }
1333 21         154  
1334               my $content = $item->content->present($self);
1335               $title = qq{<b>$title</b>} if $title;
1336               return qq{<li$id$class>$title\n$content</li>\n};
1337             }
1338              
1339 66     66   174  
1340 66         155  
1341 66         275 sub _title_to_id {
1342 66         507   my $title = shift;
1343 66         428   $title =~ s/<.*?>//g; # no tags
1344 66         229   $title =~ s/[,(].*//; # drop argument lists or text lists
1345               $title =~ s/\s*$//; # drop final spaces
1346               $title =~ s/[^A-Za-z0-9_]/_/g; # replace chars unsuitable for an id
1347               return $title;
1348             }
1349 1     1   41  
1350              
1351             sub view_pod {
1352 1 50       11   my ($self, $pom) = @_;
1353              
1354             # compute view
1355               my $content = $pom->content->present($self)
1356                 or return "no documentation found in <tt>$self->{path}</tt><br>\n"
1357 1     1   102             . "<a href='$self->{root_url}/source/$self->{path}'>Source</a>";
  1         46  
1358 1 50       134  
1359             # parse name and description
1360               my $name_h1 = firstval {$_->title =~ /^(NAME|TITLE)\b/} $pom->head1();
1361 1         180   my $doc_title = $name_h1 ? $name_h1->content->present('Pod::POM::View')
1362 1   33     7 # retrieve content as plain text
1363 1         6                            : 'Untitled';
1364               my ($name, $description) = ($doc_title =~ /^\s*(.*?)\s+-+\s+(.*)/);
1365               $name ||= $doc_title;
1366 1         101   $name =~ s/\n.*//s;
1367              
1368             # installation date
1369 1         9   my $installed = strftime("%x", localtime($self->{mtime}));
1370              
1371 1 50       7 # if this is a module (and not a script), get additional info
1372               my ($version, $core_release, $orig_version, $cpan_info, $module_refs)
1373                 = ("") x 6;
1374 1 50       32   if (my $mod_name = $self->{mod_name}) {
1375              
1376             # version
1377 1   50     18     $version = $self->{version} ? "v. $self->{version}, " : "";
1378              
1379 1   50     627 # is this module in Perl core ?
1380 1   33     6     $core_release = Module::CoreList->first_release($mod_name) || "";
1381 1   33     4     $orig_version
1382                   = $Module::CoreList::version{$core_release}{$mod_name} || "";
1383                 $orig_version &&= "v. $orig_version ";
1384 1         8     $core_release &&= "; ${orig_version}entered Perl core in $core_release";
1385              
1386             # hyperlinks to various internet resources
1387                 $module_refs = qq{<br>
1388             <a href="https://metacpan.org/pod/$mod_name"
1389             target="_blank">meta::cpan</a> |
1390             <a href="http://www.annocpan.org/?mode=search&field=Module&name=$mod_name"
1391 1 50       5 target="_blank">Anno</a>
1392 0         0 };
1393 0 0       0  
1394 0         0     if ($has_cpan) {
1395                   my $mod = CPAN::Shell->expand("Module", $mod_name);
1396 0 0       0       if ($mod) {
1397                     my $cpan_version = $mod->cpan_version;
1398                     $cpan_info = "; CPAN has v. $cpan_version"
1399                       if $cpan_version ne $self->{version};
1400                   }
1401 1         8     }
1402               }
1403              
1404               my $toc = $self->make_toc($pom, 0);
1405              
1406               return <<__EOHTML__
1407             <html>
1408             <head>
1409             <title>$name</title>
1410             <link href="$self->{root_url}/Alien/GvaScript/lib/GvaScript.css" rel="stylesheet" type="text/css">
1411             <link href="$self->{root_url}/Pod/POM/Web/lib/PodPomWeb.css" rel="stylesheet" type="text/css">
1412             <script src="$self->{root_url}/Alien/GvaScript/lib/prototype.js"></script>
1413             <script src="$self->{root_url}/Alien/GvaScript/lib/GvaScript.js"></script>
1414             <script>
1415             var treeNavigator;
1416             function setup() {
1417             new GvaScript.TreeNavigator(
1418             'TN_tree',
1419             {selectFirstNode: (location.hash ? false : true),
1420             tabIndex: 0}
1421             );
1422            
1423             var tocFrame = window.parent.frames.tocFrame;
1424             if (tocFrame) {
1425             try {tocFrame.eval("selectToc('$name')")}
1426             catch(e) {};
1427             }
1428             }
1429             window.onload = setup;
1430             function jumpto_href(event) {
1431             var label = event.controller.label(event.target);
1432             if (label && label.tagName == "A") {
1433             /* label.focus(); */
1434             return Event.stopNone;
1435             }
1436             }
1437             </script>
1438             <style>
1439             #TOC .TN_content .TN_label {font-size: 80%; font-weight: bold}
1440             #TOC .TN_leaf .TN_label {font-weight: normal}
1441            
1442             #ref_box {
1443             clear: right;
1444             float: right;
1445             text-align: right;
1446             font-size: 80%;
1447             }
1448             #title_descr {
1449             clear: right;
1450             float: right;
1451             font-style: italic;
1452             margin-top: 8px;
1453             margin-bottom: 8px;
1454             padding: 5px;
1455             text-align: center;
1456             border: 3px double #888;
1457             }
1458             </style>
1459             </head>
1460             <body>
1461             <div id='TN_tree'>
1462             <div class="TN_node">
1463             <h1 class="TN_label">$name</h1>
1464             <small>(${version}installed $installed$core_release$cpan_info)</small>
1465            
1466            
1467             <span id="title_descr">$description</span>
1468            
1469             <span id="ref_box">
1470             <a href="$self->{root_url}/source/$self->{path}">Source</a>
1471             $module_refs
1472             </span>
1473            
1474             <div class="TN_content">
1475             <div class="TN_node" onPing="jumpto_href" id="TOC">
1476             <h3 class="TN_label">Table of contents</h3>
1477             <div class="TN_content">
1478             $toc
1479             </div>
1480             </div>
1481             <hr/>
1482             </div>
1483             </div>
1484             $content
1485             </div>
1486             </body>
1487 1         63 </html>
1488             __EOHTML__
1489              
1490             }
1491 1     1   10  
1492 1     1   2801 # generating family of methods for view_head1, view_head2, etc.
  1         4  
  1         222  
1493 6         1099 BEGIN {
1494 23     23   1142   for my $num (1..6) {
1495 23         144     no strict 'refs';
1496 23         211     *{"view_head$num"} = sub {
1497 23         161       my ($self, $item) = @_;
1498 23         614       my $title = $item->title->present($self);
1499                   my $id = _title_to_id($title);
1500                   my $content = $item->content->present($self);
1501                   my $h_num = $num + 1;
1502                   return <<EOHTML
1503             <div class="TN_node" id="$id">
1504             <h$h_num class="TN_label">$title</h$h_num>
1505             <div class="TN_content">
1506             $content
1507 23         219 </div>
1508 6         41 </div>
1509             EOHTML
1510                 }
1511               }
1512             }
1513 10     10   94  
1514 10         34  
1515             sub view_seq_index {
1516               my ($self, $item) = @_;
1517               return ""; # Pod index tags have no interest for HTML
1518             }
1519 57     57   1316  
1520              
1521 57         106 sub view_verbatim {
1522 57 100       116   my ($self, $text) = @_;
1523 3         12  
1524 3         19   my $coloring = $self->{syntax_coloring};
1525               if ($coloring) {
1526                 my $method = "${coloring}_coloring";
1527 54         328     $text = $self->$method($text);
1528               }
1529               else {
1530                 $text =~ s/([&<>"])/$Pod::POM::Web::escape_entity{$1}/g;
1531 57         203   }
  11         34  
1532 11         113  
1533             # hyperlinks to other modules
1534 57 100       154   $text =~ s{(\buse\b(?:</span>)?\ +(?:<span.*?>)?)([\w:]+)}
1535 1         4 {my $url = $self->view_seq_link_transform_path($2);
1536 1         7 qq{$1<a href="$url">$2</a>} }eg;
  257         588  
1537              
1538 57         423   if ($self->{line_numbering}) {
1539                 my $line = 1;
1540                 $text =~ s/^/sprintf "%6d\t", $line++/egm;
1541               }
1542               return qq{<pre class="$coloring">$text</pre>};
1543             }
1544 3     3   13  
1545 3         30  
1546 3         111  
1547             sub PPI_coloring {
1548 3 50       368160   my ($self, $text) = @_;
1549 3         475   my $ppi = PPI::HTML->new();
1550 3         56   my $html = $ppi->html(\$text);
1551              
1552               if ($html) {
1553 0         0     $html =~ s/<br>//g;
1554 0         0     return $html;
1555               }
1556               else { # PPI failed to parse that text
1557                 $text =~ s/([&<>"])/$Pod::POM::Web::escape_entity{$1}/g;
1558                 return $text;
1559               }
1560 0     0   0 }
1561 0         0  
1562 0         0  
1563             sub SCINEPLEX_coloring {
1564               my ($self, $text) = @_;
1565               eval {
1566 0         0     $text = ActiveState::Scineplex::Annotate($text,
1567                                                          'perl',
1568                                                          outputFormat => 'html');
1569               };
1570               return $text;
1571             }
1572              
1573              
1574 8     8   25  
1575              
1576 8         18  
1577 8         23 sub make_toc {
1578 8         57   my ($self, $item, $level) = @_;
1579              
1580 8         189   my $html = "";
1581 7         45   my $method = "head" . ($level + 1);
1582 7         73   my $sub_items = $item->$method;
1583              
1584 7         28   foreach my $sub_item (@$sub_items) {
1585 7 50       24     my $title = $sub_item->title->present($self);
1586 7   33     20     my $id = _title_to_id($title);
1587              
1588 7         53     my $node_content = $self->make_toc($sub_item, $level + 1);
1589                 my $class = $node_content ? "TN_node" : "TN_leaf";
1590                 $node_content &&= qq{<div class="TN_content">$node_content</div>};
1591              
1592                 $html .= qq{<div class="$class">}
1593                        . qq{<a class="TN_label" href="#$id">$title</a>}
1594 8         25            . $node_content
1595                        . qq{</div>};
1596               }
1597              
1598       0       return $html;
1599             }
1600              
1601              
1602             sub DESTROY {} # avoid AUTOLOAD
1603              
1604              
1605             1;
1606              
1607              
1608              
1609             __END__
1610            
1611             =encoding ISO8859-1
1612            
1613             =head1 NAME
1614            
1615             Pod::POM::Web - HTML Perldoc server
1616            
1617             =head1 DESCRIPTION
1618            
1619             L<Pod::POM::Web> is a Web application for browsing
1620             the documentation of Perl components installed
1621             on your local machine. Since pages are dynamically
1622             generated, they are always in sync with code actually
1623             installed.
1624            
1625             The application offers
1626            
1627             =over
1628            
1629             =item *
1630            
1631             a tree view for browsing through installed modules
1632             (with dynamic expansion of branches as they are visited)
1633            
1634             =item *
1635            
1636             a tree view for navigating and opening / closing sections while
1637             visiting a documentation page
1638            
1639             =item *
1640            
1641             a source code view with hyperlinks between used modules
1642             and optionally with syntax coloring
1643             (see section L</"Optional features">)
1644            
1645            
1646             =item *
1647            
1648             direct access to L<perlfunc> entries (builtin Perl functions)
1649            
1650             =item *
1651            
1652             search through L<perlfaq> headers
1653            
1654             =item *
1655            
1656             fulltext search, including names of Perl variables
1657             (this is an optional feature -- see section L</"Optional features">).
1658            
1659             =item *
1660            
1661             parsing and display of version number
1662            
1663             =item *
1664            
1665             display if and when the displayed module entered Perl core.
1666            
1667             =item *
1668            
1669             parsing pod links and translating them into hypertext links
1670            
1671             =item *
1672            
1673             links to CPAN sites
1674            
1675             =back
1676            
1677             The application may be hosted by an existing Web server, or otherwise
1678             may run its own builtin Web server.
1679            
1680             The DHTML code for navigating through documentation trees requires a
1681             modern browser. So far it has been tested on Microsoft Internet
1682             Explorer 8.0, Firefox 3.5, Google Chrome 3.0 and Safari 4.0.4.
1683            
1684             =head1 USAGE
1685            
1686             Usage is described in a separate document
1687             L<Pod::POM::Web::Help>.
1688            
1689             =head1 INSTALLATION
1690            
1691             =head2 Starting the Web application
1692            
1693             Once the code is installed (most probably through
1694             L<CPAN> or L<CPANPLUS>), you have to configure
1695             the web server :
1696            
1697             =head3 As a mod_perl service
1698            
1699             If you have Apache2 with mod_perl 2.0, then edit your
1700             F<perl.conf> as follows :
1701            
1702             PerlModule Apache2::RequestRec
1703             PerlModule Apache2::RequestIO
1704             <Location /perldoc>
1705             SetHandler modperl
1706             PerlResponseHandler Pod::POM::Web->handler
1707             </Location>
1708            
1709             Then navigate to URL L<http://localhost/perldoc>.
1710            
1711             =head3 As a cgi-bin script
1712            
1713             Alternatively, you can run this application as a cgi-script
1714             by writing a simple file F<perldoc> in your C<cgi-bin> directory,
1715             containing :
1716            
1717             #!/path/to/perl
1718             use Pod::POM::Web;
1719             Pod::POM::Web->handler;
1720            
1721             Make this script executable,
1722             then navigate to URL L<http://localhost/cgi-bin/perldoc>.
1723            
1724             The same can be done for running under mod_perl Registry
1725             (write the same script as above and put it in your
1726             Apache/perl directory). However, this does not make much sense,
1727             because if you have mod_perl Registry then you could as well
1728             run it as a basic mod_perl handler.
1729            
1730             =head3 As a standalone server
1731            
1732             A third way to use this application is to start a process invoking
1733             the builtin HTTP server :
1734            
1735             perl -MPod::POM::Web -e server
1736            
1737             This is useful if you have no other HTTP server, or if
1738             you want to run this module under the perl debugger.
1739             The server will listen at L<http://localhost:8080>.
1740             A different port may be specified, in several ways :
1741            
1742             perl -MPod::POM::Web -e server 8888
1743             perl -MPod::POM::Web -e server(8888)
1744             perl -MPod::POM::Web -e server -- --port 8888
1745            
1746             =head2 Opening a specific initial page
1747            
1748             By default, the initial page displayed by the application
1749             is F<perl>. This can be changed by supplying an C<open> argument
1750             with the name of any documentation page: for example
1751            
1752             http://localhost:8080?open=Pod/POM/Web
1753             http://localhost:8080?open=perlfaq
1754            
1755             =head2 Setting a specific title
1756            
1757             If you run several instances of C<Pod::POM::Web> simultaneously, you may
1758             want them to have distinct titles. This can be done like this:
1759            
1760             perl -MPod::POM::Web -e server -- --title "My Own Perl Doc"
1761            
1762            
1763             =head1 MISCELLANEOUS
1764            
1765             =head2 Note about security
1766            
1767             This application is intended as a power tool for Perl developers,
1768             not as an Internet application. It will give access to any file
1769             installed under your C<@INC> path or Apache C<lib/perl> directory
1770             (but not outside of those directories);
1771             so it is probably a B<bad idea>
1772             to put it on a public Internet server.
1773            
1774            
1775             =head2 Optional features
1776            
1777             =head3 Syntax coloring
1778            
1779             Syntax coloring improves readability of code excerpts.
1780             If your Perl distribution is from ActiveState, then
1781             C<Pod::POM::Web> will take advantage
1782             of the L<ActiveState::Scineplex> module
1783             which is already installed on your system. Otherwise,
1784             you need to install L<PPI::HTML>, available from CPAN.
1785            
1786             =head3 Fulltext indexing
1787            
1788             C<Pod::POM::Web> can index the documentation and source code
1789             of all your installed modules, including Perl variable names,
1790             C<Names:::Of::Modules>, etc. To use this feature you need to
1791            
1792             =over
1793            
1794             =item *
1795            
1796             install L<Search::Indexer> from CPAN
1797            
1798             =item *
1799            
1800             build the index as described in L<Pod::POM::Web::Indexer> documentation.
1801            
1802             =back
1803            
1804            
1805            
1806             =head3 AnnoCPAN comments
1807            
1808             The website L<http://annocpan.org/> lets people add comments to the
1809             documentation of CPAN modules. The AnnoCPAN database is freely
1810             downloadable and can be easily integrated with locally installed
1811             modules via runtime filtering.
1812            
1813             If you want AnnoCPAN comments to show up in Pod::POM::Web, do the following:
1814            
1815             =over
1816            
1817             =item *
1818            
1819             install L<AnnoCPAN::Perldoc> from CPAN;
1820            
1821             =item *
1822            
1823             download the database from L<http://annocpan.org/annopod.db> and save
1824             it as F<$HOME/.annopod.db> (see the documentation in the above module
1825             for more details). You may also like to try
1826             L<AnnoCPAN::Perldoc::SyncDB> which is a crontab-friendly tool for
1827             periodically downloading the AnnoCPAN database.
1828            
1829             =back
1830            
1831            
1832             =head1 HINTS TO POD AUTHORING
1833            
1834             =head2 Images
1835            
1836             The Pod::Pom::Web server also serves non-pod files within the C<@INC>
1837             hierarchy. This is useful for example to include images in your
1838             documentation, by inserting chunks of HTML as follows :
1839            
1840             =for html
1841             <img src="pretty_diagram.jpg">
1842            
1843             or
1844            
1845             =for html
1846             <object type="image/svg+xml" data="try.svg" width="640" height="480">
1847             </object>
1848            
1849             Here it is assumed that auxiliary files C<pretty_diagram.jpg> or
1850             C<try.svg> are in the same directory than the POD source; but
1851             of course relative or absolute links can be used.
1852            
1853            
1854            
1855             =head1 METHODS
1856            
1857             =head2 handler
1858            
1859             Pod::POM::Web->handler($request, $response, $options);
1860            
1861             Public entry point for serving a request. Objects C<$request> and
1862             C<$response> are specific to the hosting HTTP server (modperl, HTTP::Daemon
1863             or cgi-bin); C<$options> is a hashref that currently contains
1864             only one possible entry : C<page_title>, for specifying the HTML title
1865             of the application (useful if you run several concurrent instances
1866             of Pod::POM::Web).
1867            
1868             =head2 server
1869            
1870             Pod::POM::Web->server($port, $options);
1871            
1872             Starts the event loop for the builtin HTTP server.
1873             The C<$port> number can be given as optional first argument
1874             (default is 8080). The second argument C<$options> may be
1875             used to specify a page title (see L</"handler"> method above).
1876            
1877             This function is exported into the C<main::> namespace if perl
1878             is called with the C<-e> flag, so that you can write
1879            
1880             perl -MPod::POM::Web -e server
1881            
1882             Options and port may be specified on the command line :
1883            
1884             perl -MPod::POM::Web -e server -- --port 8888 --title FooBar
1885            
1886             =head1 ACKNOWLEDGEMENTS
1887            
1888             This web application was deeply inspired by :
1889            
1890             =over
1891            
1892             =item *
1893            
1894             the structure of HTML Perl documentation released with
1895             ActivePerl (L<http://www.activeperl.com/ASPN/Perl>).
1896            
1897            
1898             =item *
1899            
1900             the excellent tree navigation in Microsoft's former MSDN Library Web site
1901             -- since they rebuilt the site, keyboard navigation has gone !
1902            
1903             =item *
1904            
1905             the standalone HTTP server implemented in L<Pod::WebServer>.
1906            
1907             =item *
1908            
1909             the wide possibilities of Andy Wardley's L<Pod::POM> parser.
1910            
1911             =back
1912            
1913             Thanks
1914             to Philippe Bruhat who mentioned a weakness in the API,
1915             to Chris Dolan who supplied many useful suggestions and patches
1916             (esp. integration with AnnoCPAN),
1917             to Rémi Pauchet who pointed out a regression bug with Firefox CSS,
1918             to Alexandre Jousset who fixed a bug in the TOC display,
1919             to Cédric Bouvier who pointed out a IO bug in serving binary files,
1920             to Elliot Shank who contributed the "page_title" option,
1921             and to Olivier 'dolmen' Mengué who suggested to export "server" into C<main::>.
1922            
1923            
1924             =head1 RELEASE NOTES
1925            
1926             Indexed information since version 1.04 is not compatible
1927             with previous versions.
1928            
1929             So if you upgraded from a previous version and want to use
1930             the index, you need to rebuild it entirely, by running the
1931             command :
1932            
1933             perl -MPod::POM::Web::Indexer -e "index(-from_scratch => 1)"
1934            
1935            
1936             =head1 BUGS
1937            
1938             Please report any bugs or feature requests to
1939             C<bug-pod-pom-web at rt.cpan.org>, or through the web interface at
1940             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Pod-POM-Web>.
1941             I will be notified, and then you'll automatically be notified of progress on
1942             your bug as I make changes.
1943            
1944            
1945             =head1 AUTHOR
1946            
1947             Laurent Dami, C<< <laurent.d...@justice.ge.ch> >>
1948            
1949            
1950             =head1 COPYRIGHT & LICENSE
1951            
1952             Copyright 2007-2017 Laurent Dami, all rights reserved.
1953            
1954             This program is free software; you can redistribute it and/or modify it
1955             under the same terms as Perl itself.
1956            
1957             =head1 TODO
1958            
1959             - real tests !
1960             - factorization (esp. initial <head> in html pages)
1961             - use Getopts to choose colouring package, toggle CPAN, etc.
1962             - declare Pod::POM bugs
1963             - perlre : line 1693 improper parsing of L<C<< (?>pattern) >>>
1964             - bug: doc files taken as pragmas (lwptut, lwpcook, pip, pler)
1965             - exploit doc index X<...>
1966             - do something with perllocal (installation history)
1967             - restrict to given set of paths/ modules
1968             - ned to change toc (no perlfunc, no scripts/pragmas, etc)
1969             - treenav with letter entries or not ?
1970             - port to Plack
1971