File Coverage

blib/lib/Pod/Site.pm
Criterion Covered Total %
statement 273 295 92.5
branch 96 132 72.7
condition 39 62 62.9
subroutine 44 45 97.7
pod 19 19 100.0
total 471 553 85.1


line stmt bran cond sub pod time code
1             package Pod::Site;
2              
3 3     3   134220 use strict;
  3         7  
  3         146  
4 3     3   18 use warnings;
  3         6  
  3         149  
5 3     3   21 use File::Spec;
  3         11  
  3         99  
6 3     3   18 use Carp;
  3         6  
  3         435  
7 3     3   3359 use Pod::Simple '3.12';
  3         120059  
  3         118  
8 3     3   2305 use HTML::Entities;
  3         22715  
  3         344  
9 3     3   28 use File::Path;
  3         7  
  3         226  
10 3         24 use Object::Tiny qw(
11             module_roots
12             doc_root
13             base_uri
14             index_file
15             css_path
16             favicon_uri
17             js_path
18             versioned_title
19             replace_css
20             replace_js
21             label
22             verbose
23             mod_files
24             bin_files
25 3     3   2164 );
  3         1131  
26              
27             our $VERSION = '0.56';
28              
29             sub go {
30 0     0 1 0 my $class = shift;
31 0         0 $class->new( $class->_config )->build;
32             }
33              
34             sub new {
35 5     5 1 8652 my ( $class, $params ) = @_;
36             my $self = bless {
37             index_file => 'index.html',
38             verbose => 0,
39             js_path => '',
40             css_path => '',
41 5 100       15 %{ $params || {} }
  5         65  
42             } => $class;
43              
44 5 100       17 if (my @req = grep { !$self->{$_} } qw(doc_root base_uri module_roots)) {
  15         61  
45 1 50       5 my $pl = @req > 1 ? 's' : '';
46 1         2 my $last = pop @req;
47 1 50       11 my $disp = @req ? join(', ', @req) . (@req > 1 ? ',' : '')
    50          
48             . " and $last" : $last;
49 1         256 croak "Missing required parameters $disp";
50             }
51              
52             my $roots = ref $self->{module_roots} eq 'ARRAY'
53             ? $self->{module_roots}
54 4 100       25 : ( $self->{module_roots} = [$self->{module_roots}] );
55 4         9 for my $path (@{ $roots }) {
  4         15  
56 4 100       372 croak "The module root $path does not exist\n" unless -e $path;
57             }
58              
59 3 100       16 $self->{base_uri} = [$self->{base_uri}] unless ref $self->{base_uri};
60 3         20 return $self;
61             }
62              
63             sub build {
64 2     2 1 5 my $self = shift;
65 2         475 File::Path::mkpath($self->{doc_root}, 0, 0755);
66              
67 2         9 $self->batch_html;
68              
69             # The index file is the home page.
70 2         115 my $idx_file = File::Spec->catfile( $self->doc_root, $self->index_file );
71 2 50       267 open my $idx_fh, '>', $idx_file or die qq{Cannot open "$idx_file": $!\n};
72              
73             # The TOC file has the table of contents for all modules and programs in
74             # the distribution.
75 2         34 my $toc_file = File::Spec->catfile( $self->{doc_root}, 'toc.html' );
76 2 50       143 open my $toc_fh, '>', $toc_file or die qq{Cannot open "$toc_file": $!\n};
77              
78             # Set things up.
79 2         15 $self->{toc_fh} = $toc_fh;
80 2         8 $self->{seen} = {};
81 2         8 $self->{indent} = 1;
82 2         7 $self->{base_space} = ' ';
83 2         5 $self->{spacer} = ' ';
84 2         8 $self->{uri} = '';
85              
86             # Make it so!
87 2         11 $self->sort_files;
88 2         9 $self->start_nav($idx_fh);
89 2         12 $self->start_toc($toc_fh);
90 2         57 $self->output($idx_fh, $self->mod_files);
91 2         10 $self->output_bin($idx_fh);
92 2         7 $self->finish_nav($idx_fh);
93 2         8 $self->finish_toc($toc_fh);
94 2         9 $self->copy_etc;
95              
96             # Close up shop.
97 2 50       1000 close $idx_fh or die qq{Could not close "$idx_file": $!\n};
98 2 50       95 close $toc_fh or die qq{Could not close "$toc_file": $!\n};
99             }
100              
101             sub sort_files {
102 2     2 1 5 my $self = shift;
103              
104             # Let's see what the search has found.
105 2         18 my $stuff = Pod::Site::Search->instance->name2path;
106              
107             # Sort the modules from the scripts.
108 2         23 my (%mods, %bins);
109 2         4 while (my ($name, $path) = each %{ $stuff }) {
  16         61  
110 14 50       53 if ($name =~ /[.]p(?:m|od)$/) {
    50          
111             # Likely a module.
112 0         0 _set_mod(\%mods, $name, $stuff->{$name});
113             } elsif ($name =~ /[.](?:plx?|bat)$/) {
114             # Likely a script.
115 0         0 (my $script = $name) =~ s{::}{/}g;
116 0         0 $bins{$script} = $stuff->{$name};
117             } else {
118             # Look for a shebang line.
119 14 50       628 if (open my $fh, '<', $path) {
120 14         121 my $shebang = <$fh>;
121 14         97 close $fh;
122 14 50 33     124 if ($shebang && $shebang =~ /^#!.*\bperl/) {
123             # Likely a script.
124 0         0 (my $script = $name) =~ s{::}{/}g;
125 0         0 $bins{$script} = $stuff->{$name};
126             } else {
127             # Likely a module.
128 14         56 _set_mod(\%mods, $name, $stuff->{$name});
129             }
130             } else {
131             # Who knows? Default to module.
132 0         0 _set_mod(\%mods, $name, $stuff->{$name});
133             }
134             }
135             }
136              
137             # Save our findings.
138 2         16 $self->{mod_files} = \%mods;
139 2         9 $self->{bin_files} = \%bins;
140             }
141              
142             sub start_nav {
143 2     2 1 5 my ($self, $fh) = @_;
144 2         5 my $class = ref $self;
145 2         27 my $version = __PACKAGE__->VERSION;
146 2         16 my $title = encode_entities $self->title;
147 2         270 my $head = encode_entities $self->nav_header;
148              
149 2 50       86 print STDERR "Starting site navigation file\n" if $self->verbose > 1;
150             my $base = join "\n ", map {
151 2         11 qq{}
152 2         15 } @{ $self->{base_uri} };
  2         8  
153              
154 2         16 my $favicon = '';
155 2 50       10 if (my $uri = $self->{favicon_uri}) {
156 0         0 my $type = $uri;
157 0         0 $type =~ s/.*\.([^.]+)/$1/;
158 0         0 $favicon = qq();
159             }
160 2         28 print $fh _udent( <<" EOF" );
161            
162             "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
163            
164            
165            
166             $title
167            
168             $base
169             $favicon
170            
171            
172            
173            
174            
175            

$head

176            
177            
  • TOC
  • 178             EOF
    179             }
    180              
    181             sub start_toc {
    182 2     2 1 3 my ($self, $fh) = @_;
    183              
    184 2         9 my $sample = encode_entities $self->sample_module;
    185 2         54 my $version = Pod::Site->VERSION;
    186 2         10 my $title = encode_entities $self->title;
    187              
    188 2 50       78 print STDERR "Starting browser TOC file\n" if $self->verbose > 1;
    189 2         34 print $fh _udent( <<" EOF");
    190            
    191             "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
    192            
    193            
    194            
    195             $title
    196            
    197            
    198              
    199            
    200            

    $title

    201            

    Instructions

    202              
    203            

    Select class names from the navigation tree to the left. The tree

    204             shows a hierarchical list of modules and programs. In addition to
    205             this URL, you can link directly to the page for a particular module
    206             or program. For example, if you wanted to access
    207             $sample, any of these links will work:

    208              
    209            
    210            
  • /?$sample
  • 211            
  • /$sample
  • 212            
    213              
    214            

    Happy Hacking!

    215              
    216            

    Classes & Modules

    217            
    218             EOF
    219             }
    220              
    221             sub output {
    222 10     10 1 32 my ($self, $fh, $tree) = @_;
    223 10         26 for my $key (sort keys %{ $tree }) {
      10         61  
    224 22         41 my $data = $tree->{$key};
    225 22         81 (my $fn = $key) =~ s/\.[^.]+$//;
    226 22         76 my $class = join ('::', split('/', $self->{uri}), $fn);
    227 22 50       613 print STDERR "Reading $class\n" if $self->verbose > 1;
    228 22 100       127 if (ref $data) {
    229             # It's a directory tree. Output a class for it, first, if there
    230             # is one.
    231 8         12 my $item = $key;
    232 8 100       28 if ($tree->{"$key.pm"}) {
    233 6         14 my $path = $tree->{"$key.pm"};
    234 6 50       22 if (my $desc = $self->get_desc($class, $path)) {
    235 6         26 $item = qq{$key};
    236 6         18 $self->_output_navlink($fh, $fn, $path, $class, 1, $desc);
    237             }
    238 6         18 $self->{seen}{$class} = 1;
    239             }
    240              
    241             # Now recursively descend the tree.
    242 8 50       263 print STDERR "Outputting nav link\n" if $self->verbose > 2;
    243             print $fh $self->{base_space}, $self->{spacer} x $self->{indent},
    244             qq{
  • $item\n}, $self->{base_space},
  • 245 8         86 $self->{spacer} x ++$self->{indent}, "
      \n";
    246 8         11 ++$self->{indent};
    247 8         16 $self->{uri} .= "$key/";
    248 8         36 $self->output($fh, $data);
    249             print $fh $self->{base_space}, $self->{spacer} x --$self->{indent},
    250             "\n", $self->{base_space},
    251 8         46 $self->{spacer} x --$self->{indent}, "\n";
    252 8         161 $self->{uri} =~ s|$key/$||;
    253             } else {
    254             # It's a class. Create a link to it.
    255             $self->_output_navlink($fh, $fn, $data, $class)
    256 14 100       52 unless $self->{seen}{$class};
    257             }
    258             }
    259             }
    260              
    261             sub output_bin {
    262 2     2 1 5 my ($self, $fh) = @_;
    263 2         52 my $files = $self->bin_files;
    264 2 50       9 return unless %{ $files };
      2         10  
    265              
    266             # Start the list in the tree browser.
    267             print $fh $self->{base_space}, $self->{spacer} x $self->{indent},
    268 0         0 qq{
  • bin\n}, $self->{base_space}, $self->{spacer} x ++$self->{indent}, "
      \n";
  • 269 0         0 ++$self->{indent};
    270              
    271 0         0 for my $pl (sort { lc $a cmp lc $b } keys %{ $files }) {
      0         0  
      0         0  
    272 0         0 my $file = $files->{$pl};
    273 0         0 $self->_output_navlink($fh, $pl, $file, $pl);
    274             }
    275              
    276             print $fh $self->{base_space}, $self->{spacer} x --$self->{indent}, "\n",
    277 0         0 $self->{base_space}, $self->{spacer} x --$self->{indent}, "\n";
    278             }
    279              
    280             sub finish_nav {
    281 2     2 1 4 my ($self, $fh) = @_;
    282 2 50       52 print STDERR "Finishing browser navigation file\n" if $self->verbose > 1;
    283 2         16 print $fh _udent( <<" EOF" );
    284            
    285            
    286            
    287            
    288            
    289             EOF
    290             }
    291              
    292             sub finish_toc {
    293 2     2 1 3 my ($self, $fh) = @_;
    294 2 50       53 print STDERR "finishing browser TOC file\n" if $self->verbose > 1;
    295 2         17 print $fh _udent( <<" EOF" );
    296            
    297            
    298            
    299             EOF
    300             }
    301              
    302             sub batch_html {
    303 2     2 1 4 my $self = shift;
    304 2         1058 require Pod::Simple::HTMLBatch;
    305 2 50       31702 print STDERR "Creating HTML with Pod::Simple::XHTML\n" if $self->verbose > 1;
    306 2         34 my $batchconv = Pod::Simple::HTMLBatch->new;
    307 2         519 $batchconv->index(1);
    308 2         71 $batchconv->verbose($self->verbose);
    309 2         24 $batchconv->contents_file( undef );
    310 2         35 $batchconv->css_flurry(0);
    311 2         14 $batchconv->javascript_flurry(0);
    312 2         13 $batchconv->html_render_class('Pod::Site::XHTML');
    313 2         15 $batchconv->search_class('Pod::Site::Search');
    314 2         10 our $BASE_URI;
    315 2         60 local $BASE_URI = $self->base_uri->[0];
    316 2         58 $batchconv->batch_convert( $self->module_roots, $self->{doc_root} );
    317 2         685 return 1;
    318             }
    319              
    320             sub copy_etc {
    321 2     2 1 3 my $self = shift;
    322 2         869 require File::Copy;
    323 2         3249 (my $from = __FILE__) =~ s/[.]pm$//;
    324 2         6 for my $ext (qw(css js)) {
    325 4         633 my $dest = File::Spec->catfile($self->{doc_root}, "podsite.$ext");
    326             File::Copy::copy(
    327             File::Spec->catfile( $from, "podsite.$ext" ),
    328             $self->{doc_root}
    329 4 100 66     166 ) unless -e $dest && !$self->{"replace_$ext"};
    330             }
    331             }
    332              
    333             sub get_desc {
    334 16     16 1 918 my ($self, $what, $file) = @_;
    335              
    336 16 50   1   746 open my $fh, '<', $file or die "Cannot open $file: $!\n";
      1         13  
      1         35  
      1         11  
    337 16         1051 my ($desc, $encoding);
    338 16         25 local $_;
    339             # Cribbed from Module::Build::PodParser.
    340 16   100     239 while (not ($desc and $encoding) and $_ = <$fh>) {
          66        
    341 540 100       1307 next unless /^=(?!cut)/ .. /^=cut/; # in POD
    342 504 100       852 ($desc) = /^ (?: [a-z0-9:]+ \s+ - \s+ ) (.*\S) /ix unless $desc;
    343 504 100       2855 ($encoding) = /^=encoding\s+(.*\S)/ unless $encoding;
    344             }
    345 16 100 66     75 Encode::from_to($desc, $encoding, 'UTF-8') if $desc && $encoding;
    346              
    347 16 50       285 close $fh or die "Cannot close $file: $!\n";
    348             print "$what has no POD or no description in a =head1 NAME section\n"
    349 16 50 33     56 if $self->{verbose} && !$desc;
    350 16   50     113 return $desc || '';
    351             }
    352              
    353             sub sample_module {
    354 3     3 1 5 my $self = shift;
    355 3   66     24 $self->{sample_module} ||= $self->main_module;
    356             }
    357              
    358             sub main_module {
    359 16     16 1 22 my $self = shift;
    360 16   66     230 $self->{main_module} ||= $self->_find_module;
    361             }
    362              
    363             sub name {
    364 12     12 1 4027 my $self = shift;
    365 12 50       57 $self->{name} || $self->main_module;
    366             }
    367              
    368             sub title {
    369 6     6 1 16 my $self = shift;
    370 6 100 66     52 return $self->{title} ||= join ' ',
        100          
    371             $self->name,
    372             ( $self->versioned_title ? $self->version : () ),
    373             ( $self->label ? $self->label : () );
    374             }
    375              
    376             sub nav_header {
    377 4     4 1 588 my $self = shift;
    378 4 100       11 $self->name . ($self->versioned_title ? ' ' . $self->version : '');
    379             }
    380              
    381             sub version {
    382 5     5 1 33 my $self = shift;
    383 5 100       115 return $self->{version} if $self->{version};
    384 1         1012 require Module::Metadata;
    385 1         10885 my $mod = $self->main_module;
    386 1 50       14 my $file = Pod::Site::Search->instance->name2path->{$mod}
    387             or die "Could not find $mod\n";
    388 1 50       24 my $info = Module::Metadata->new_from_file( $file )
    389             or die "Could not find $file\n";
    390 1   33     1572 return $self->{version} ||= $info->version;
    391             }
    392              
    393             sub _pod2usage {
    394 1     1   62670 shift;
    395 1         17 require Pod::Usage;
    396 1         6 Pod::Usage::pod2usage(
    397             '-verbose' => 99,
    398             '-sections' => '(?i:(Usage|Options))',
    399             '-exitval' => 1,
    400             '-input' => __FILE__,
    401             @_
    402             );
    403             }
    404              
    405             sub _config {
    406 13     13   6923 my $self = shift;
    407 13         1239 require Getopt::Long;
    408 13         13091 Getopt::Long::Configure( qw(bundling) );
    409              
    410 13         447 my %opts = (
    411             verbose => 0,
    412             css_path => '',
    413             js_path => '',
    414             index_file => 'index.html',
    415             base_uri => undef,
    416             );
    417              
    418             Getopt::Long::GetOptions(
    419             'name|n=s' => \$opts{name},
    420             'doc-root|d=s' => \$opts{doc_root},
    421             'base-uri|u=s@' => \$opts{base_uri},
    422             'favicon-uri=s' => \$opts{favicon_uri},
    423             'sample-module|s=s' => \$opts{sample_module},
    424             'main-module|m=s' => \$opts{main_module},
    425             'versioned-title|t!' => \$opts{versioned_title},
    426             'label|l=s' => \$opts{label},
    427             'index-file|i=s' => \$opts{index_file},
    428             'css-path|c=s' => \$opts{css_path},
    429             'js-path|j=s' => \$opts{js_path},
    430             'replace-css' => \$opts{replace_css},
    431             'replace-js' => \$opts{replace_js},
    432             'verbose|V+' => \$opts{verbose},
    433             'help|h' => \$opts{help},
    434             'man|M' => \$opts{man},
    435             'version|v' => \$opts{version},
    436 13 50       156 ) or $self->_pod2usage;
    437              
    438             # Handle documentation requests.
    439             $self->_pod2usage(
    440             ( $opts{man} ? ( '-sections' => '.+' ) : ()),
    441             '-exitval' => 0,
    442 13 100 66     17994 ) if $opts{help} or $opts{man};
        100          
    443              
    444             # Handle version request.
    445 13 50       44 if ($opts{version}) {
    446 0         0 require File::Basename;
    447 0         0 print File::Basename::basename($0), ' (', __PACKAGE__, ') ',
    448             __PACKAGE__->VERSION, $/;
    449 0         0 exit;
    450             }
    451              
    452             # Check required options.
    453 13 100       26 if (my @missing = map {
    454 4         14 ( my $opt = $_ ) =~ s/_/-/g;
    455 4         17 "--$opt";
    456 26         74 } grep { !$opts{$_} } qw(doc_root base_uri)) {
    457 3 100       10 my $pl = @missing > 1 ? 's' : '';
    458 3         5 my $last = pop @missing;
    459 3 50       13 my $disp = @missing ? join(', ', @missing) . (@missing > 1 ? ',' : '')
        100          
    460             . " and $last" : $last;
    461 3         19 $self->_pod2usage( '-message' => "Missing required $disp option$pl" );
    462             }
    463              
    464             # Check for one or more module roots.
    465 13 100       42 $self->_pod2usage( '-message' => "Missing path to module root" )
    466             unless @ARGV;
    467              
    468 13         31 $opts{module_roots} = \@ARGV;
    469              
    470             # Modify options and set defaults as appropriate.
    471 13 100       14 for (@{ $opts{base_uri} }) { $_ .= '/' unless m{/$}; }
      13         33  
      12         58  
    472              
    473 13         121 return \%opts;
    474             }
    475              
    476             sub _set_mod {
    477 14     14   23 my ($mods, $mod, $file) = @_;
    478 14 100       43 if ($mod =~ /::/) {
    479 10         37 my @names = split /::/ => $mod;
    480 10   100     50 my $data = $mods->{shift @names} ||= {};
    481 10         20 my $lln = pop @names;
    482 10   50     23 for (@names) { $data = $data->{$_} ||= {} }
      4         34  
    483 10         67 $data->{"$lln.pm"} = $file;
    484             } else {
    485 4         27 $mods->{"$mod.pm"} = $file;
    486             }
    487             }
    488              
    489             sub _udent {
    490 8     8   11 my $string = shift;
    491 8         132 $string =~ s/^[ ]{4}//gm;
    492 8         58 return $string;
    493             }
    494              
    495             sub _output_navlink {
    496 14     14   30 my ($self, $fh, $key, $fn, $class, $no_link, $desc) = @_;
    497              
    498 14   66     48 $desc ||= $self->get_desc($class, $fn);
    499 14 50       49 $desc = "—$desc" if $desc;
    500              
    501             # Output the Tree Browser Link.
    502 14 50       37 print "Outputting $class nav link\n" if $self->{verbose} > 2;
    503             print $fh $self->{base_space}, $self->{spacer} x $self->{indent},
    504 14 100       61 qq{
  • $key
  • \n}
    505             unless $no_link;
    506              
    507             # Output the TOC link.
    508 14 50       35 print "Outputting $class TOC link\n" if $self->{verbose} > 2;
    509 14         63 print {$self->{toc_fh}} $self->{base_space}, $self->{spacer},
    510 14         15 qq{
  • $class$desc
  • \n};
    511 14         68 return 1;
    512             }
    513              
    514             sub _find_module {
    515 2     2   5 my $self = shift;
    516 2 50       11 my $search = Pod::Site::Search->instance or return;
    517 2   50     67 my $bins = $self->bin_files || {};
    518 2         13 for my $mod (sort {
    519 24         66 lc $a cmp lc $b
    520 2         7 } keys %{ $search->instance->name2path }) {
    521 2 50       77 return $mod unless $bins->{$mod};
    522             }
    523             }
    524              
    525             ##############################################################################
    526             package Pod::Site::Search;
    527              
    528 3     3   14651 use base 'Pod::Simple::Search';
      3         6  
      3         2779  
    529 3     3   17764 use strict;
      3         6  
      3         77  
    530 3     3   13 use warnings;
      3         4  
      3         472  
    531             our $VERSION = '0.56';
    532              
    533             my $instance;
    534 15     15   98 sub instance { $instance }
    535              
    536             sub new {
    537 2     2   276 my $self = shift->SUPER::new(@_);
    538 2         99 $self->laborious(1);
    539 2         16 $self->inc(0);
    540 2         15 $instance = $self;
    541 2         35 return $self;
    542             }
    543              
    544             ##############################################################################
    545             package Pod::Site::XHTML;
    546              
    547 3     3   18 use strict;
      3         4  
      3         79  
    548 3     3   13 use base 'Pod::Simple::XHTML';
      3         4  
      3         2552  
    549             our $VERSION = '0.56';
    550              
    551             sub new {
    552 14     14   21067 my $self = shift->SUPER::new;
    553 14         1854 $self->index(1);
    554              
    555             # Strip leading spaces from verbatim blocks equivalent to the indent of
    556             # the first line.
    557             $self->strip_verbatim_indent(sub {
    558 14     14   14287 my $lines = shift;
    559 14         86 (my $indent = $lines->[0]) =~ s/\S.*//;
    560 14         51 return $indent;
    561 14         231 });
    562              
    563 14         114 return $self;
    564             }
    565              
    566             sub start_L {
    567 8     8   11600 my ($self, $flags) = @_;
    568 8 50       41 my $search = Pod::Site::Search->instance
    569             or return $self->SUPER::start_L($self);
    570 8   100     42 my $to = $flags->{to} || '';
    571 8 100 66     149 my $url = $to && $search->name2path->{$to} ? $Pod::Site::BASE_URI . join('/', split /::/ => $to) . '.html' : '';
    572 8         313 my $id = $flags->{section};
    573 8 100 66     76 return $self->SUPER::start_L($flags) unless $url || ($id && !$to);
          66        
    574 6 100       74 my $rel = $id ? 'subsection' : 'section';
    575 6 100       91 $url .= '#' . $self->idify($id, 1) if $id;
    576 6   50     523 $to ||= $self->title || $self->default_title || '';
          66        
    577 6         178 $self->{scratch} .= qq{};
    578             }
    579              
    580             sub html_header {
    581 70     70   67683 my $self = shift;
    582 70   50     224 my $title = $self->force_title || $self->title || $self->default_title || '';
    583 70         2130 my $version = Pod::Site->VERSION;
    584 70         384 return qq{
    585             "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
    586            
    587            
    588            
    589            
    590             $title
    591            
    592             };
    593             }
    594              
    595             1;
    596             __END__