1
2require 5;
3package Pod::Simple::HTMLBatch;
4use strict;
5use vars qw( $VERSION $HTML_RENDER_CLASS $HTML_EXTENSION
6 $CSS $JAVASCRIPT $SLEEPY $SEARCH_CLASS @ISA
7);
8$VERSION = '3.14';
9@ISA = ();  # Yup, we're NOT a subclass of Pod::Simple::HTML!
10
11# TODO: nocontents stylesheets. Strike some of the color variations?
12
13use Pod::Simple::HTML ();
14BEGIN {*esc = \&Pod::Simple::HTML::esc }
15use File::Spec ();
16
17use Pod::Simple::Search;
18$SEARCH_CLASS ||= 'Pod::Simple::Search';
19
20BEGIN {
21  if(defined &DEBUG) { } # no-op
22  elsif( defined &Pod::Simple::DEBUG ) { *DEBUG = \&Pod::Simple::DEBUG }
23  else { *DEBUG = sub () {0}; }
24}
25
26$SLEEPY = 1 if !defined $SLEEPY and $^O =~ /mswin|mac/i;
27# flag to occasionally sleep for $SLEEPY - 1 seconds.
28
29$HTML_RENDER_CLASS ||= "Pod::Simple::HTML";
30
31#
32# Methods beginning with "_" are particularly internal and possibly ugly.
33#
34
35Pod::Simple::_accessorize( __PACKAGE__,
36 'verbose', # how verbose to be during batch conversion
37 'html_render_class', # what class to use to render
38 'search_class', # what to use to search for POD documents
39 'contents_file', # If set, should be the name of a file (in current directory)
40                  # to write the list of all modules to
41 'index', # will set $htmlpage->index(...) to this (true or false)
42 'progress', # progress object
43 'contents_page_start',  'contents_page_end',
44
45 'css_flurry', '_css_wad', 'javascript_flurry', '_javascript_wad',
46 'no_contents_links', # set to true to suppress automatic adding of << links.
47 '_contents',
48);
49
50# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
51# Just so we can run from the command line more easily
52sub go {
53  @ARGV == 2 or die sprintf(
54    "Usage: perl -M%s -e %s:go indirs outdir\n  (or use \"\@INC\" for indirs)\n",
55    __PACKAGE__, __PACKAGE__,
56  );
57
58  if(defined($ARGV[1]) and length($ARGV[1])) {
59    my $d = $ARGV[1];
60    -e $d or die "I see no output directory named \"$d\"\nAborting";
61    -d $d or die "But \"$d\" isn't a directory!\nAborting";
62    -w $d or die "Directory \"$d\" isn't writeable!\nAborting";
63  }
64
65  __PACKAGE__->batch_convert(@ARGV);
66}
67# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
68
69
70sub new {
71  my $new = bless {}, ref($_[0]) || $_[0];
72  $new->html_render_class($HTML_RENDER_CLASS);
73  $new->search_class($SEARCH_CLASS);
74  $new->verbose(1 + DEBUG);
75  $new->_contents([]);
76
77  $new->index(1);
78
79  $new->       _css_wad([]);         $new->css_flurry(1);
80  $new->_javascript_wad([]);  $new->javascript_flurry(1);
81
82  $new->contents_file(
83    'index' . ($HTML_EXTENSION || $Pod::Simple::HTML::HTML_EXTENSION)
84  );
85
86  $new->contents_page_start( join "\n", grep $_,
87    $Pod::Simple::HTML::Doctype_decl,
88    "<html><head>",
89    "<title>Perl Documentation</title>",
90    $Pod::Simple::HTML::Content_decl,
91    "</head>",
92    "\n<body class='contentspage'>\n<h1>Perl Documentation</h1>\n"
93  ); # override if you need a different title
94
95
96  $new->contents_page_end( sprintf(
97    "\n\n<p class='contentsfooty'>Generated by %s v%s under Perl v%s\n<br >At %s GMT, which is %s local time.</p>\n\n</body></html>\n",
98    esc(
99      ref($new),
100      eval {$new->VERSION} || $VERSION,
101      $], scalar(gmtime), scalar(localtime),
102  )));
103
104  return $new;
105}
106
107# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
108
109sub muse {
110  my $self = shift;
111  if($self->verbose) {
112    print 'T+', int(time() - $self->{'_batch_start_time'}), "s: ", @_, "\n";
113  }
114  return 1;
115}
116
117# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
118
119sub batch_convert {
120  my($self, $dirs, $outdir) = @_;
121  $self ||= __PACKAGE__; # tolerate being called as an optionless function
122  $self = $self->new unless ref $self; # tolerate being used as a class method
123
124  if(!defined($dirs)  or  $dirs eq ''  or  $dirs eq '@INC' ) {
125    $dirs = '';
126  } elsif(ref $dirs) {
127    # OK, it's an explicit set of dirs to scan, specified as an arrayref.
128  } else {
129    # OK, it's an explicit set of dirs to scan, specified as a
130    #  string like "/thing:/also:/whatever/perl" (":"-delim, as usual)
131    #  or, under MSWin, like "c:/thing;d:/also;c:/whatever/perl" (";"-delim!)
132    require Config;
133    my $ps = quotemeta( $Config::Config{'path_sep'} || ":" );
134    $dirs = [ grep length($_), split qr/$ps/, $dirs ];
135  }
136
137  $outdir = $self->filespecsys->curdir
138   unless defined $outdir and length $outdir;
139
140  $self->_batch_convert_main($dirs, $outdir);
141}
142
143# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
144
145sub _batch_convert_main {
146  my($self, $dirs, $outdir) = @_;
147  # $dirs is either false, or an arrayref.
148  # $outdir is a pathspec.
149
150  $self->{'_batch_start_time'} ||= time();
151
152  $self->muse( "= ", scalar(localtime) );
153  $self->muse( "Starting batch conversion to \"$outdir\"" );
154
155  my $progress = $self->progress;
156  if(!$progress and $self->verbose > 0 and $self->verbose() <= 5) {
157    require Pod::Simple::Progress;
158    $progress = Pod::Simple::Progress->new(
159        ($self->verbose  < 2) ? () # Default omission-delay
160      : ($self->verbose == 2) ? 1  # Reduce the omission-delay
161                              : 0  # Eliminate the omission-delay
162    );
163    $self->progress($progress);
164  }
165
166  if($dirs) {
167    $self->muse(scalar(@$dirs), " dirs to scan: @$dirs");
168  } else {
169    $self->muse("Scanning \@INC.  This could take a minute or two.");
170  }
171  my $mod2path = $self->find_all_pods($dirs ? $dirs : ());
172  $self->muse("Done scanning.");
173
174  my $total = keys %$mod2path;
175  unless($total) {
176    $self->muse("No pod found.  Aborting batch conversion.\n");
177    return $self;
178  }
179
180  $progress and $progress->goal($total);
181  $self->muse("Now converting pod files to HTML.",
182    ($total > 25) ? "  This will take a while more." : ()
183  );
184
185  $self->_spray_css(        $outdir );
186  $self->_spray_javascript( $outdir );
187
188  $self->_do_all_batch_conversions($mod2path, $outdir);
189
190  $progress and $progress->done(sprintf (
191    "Done converting %d files.",  $self->{"__batch_conv_page_count"}
192  ));
193  return $self->_batch_convert_finish($outdir);
194  return $self;
195}
196
197
198sub _do_all_batch_conversions {
199  my($self, $mod2path, $outdir) = @_;
200  $self->{"__batch_conv_page_count"} = 0;
201
202  foreach my $module (sort {lc($a) cmp lc($b)} keys %$mod2path) {
203    $self->_do_one_batch_conversion($module, $mod2path, $outdir);
204    sleep($SLEEPY - 1) if $SLEEPY;
205  }
206
207  return;
208}
209
210sub _batch_convert_finish {
211  my($self, $outdir) = @_;
212  $self->write_contents_file($outdir);
213  $self->muse("Done with batch conversion.  $$self{'__batch_conv_page_count'} files done.");
214  $self->muse( "= ", scalar(localtime) );
215  $self->progress and $self->progress->done("All done!");
216  return;
217}
218
219# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
220
221sub _do_one_batch_conversion {
222  my($self, $module, $mod2path, $outdir, $outfile) = @_;
223
224  my $retval;
225  my $total    = scalar keys %$mod2path;
226  my $infile   = $mod2path->{$module};
227  my @namelets = grep m/\S/, split "::", $module;
228        # this can stick around in the contents LoL
229  my $depth    = scalar @namelets;
230  die "Contentless thingie?! $module $infile" unless @namelets; #sanity
231
232  $outfile  ||= do {
233    my @n = @namelets;
234    $n[-1] .= $HTML_EXTENSION || $Pod::Simple::HTML::HTML_EXTENSION;
235    $self->filespecsys->catfile( $outdir, @n );
236  };
237
238  my $progress = $self->progress;
239
240  my $page = $self->html_render_class->new;
241  if(DEBUG > 5) {
242    $self->muse($self->{"__batch_conv_page_count"} + 1, "/$total: ",
243      ref($page), " render ($depth) $module => $outfile");
244  } elsif(DEBUG > 2) {
245    $self->muse($self->{"__batch_conv_page_count"} + 1, "/$total: $module => $outfile")
246  }
247
248  # Give each class a chance to init the converter:
249  $page->batch_mode_page_object_init($self, $module, $infile, $outfile, $depth)
250   if $page->can('batch_mode_page_object_init');
251  # Init for the index (TOC), too.
252  $self->batch_mode_page_object_init($page, $module, $infile, $outfile, $depth)
253   if $self->can('batch_mode_page_object_init');
254
255  # Now get busy...
256  $self->makepath($outdir => \@namelets);
257
258  $progress and $progress->reach($self->{"__batch_conv_page_count"}, "Rendering $module");
259
260  if( $retval = $page->parse_from_file($infile, $outfile) ) {
261    ++ $self->{"__batch_conv_page_count"} ;
262    $self->note_for_contents_file( \@namelets, $infile, $outfile );
263  } else {
264    $self->muse("Odd, parse_from_file(\"$infile\", \"$outfile\") returned false.");
265  }
266
267  $page->batch_mode_page_object_kill($self, $module, $infile, $outfile, $depth)
268   if $page->can('batch_mode_page_object_kill');
269  # The following isn't a typo.  Note that it switches $self and $page.
270  $self->batch_mode_page_object_kill($page, $module, $infile, $outfile, $depth)
271   if $self->can('batch_mode_page_object_kill');
272
273  DEBUG > 4 and printf "%s %sb < $infile %s %sb\n",
274     $outfile, -s $outfile, $infile, -s $infile
275  ;
276
277  undef($page);
278  return $retval;
279}
280
281# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
282sub filespecsys { $_[0]{'_filespecsys'} || 'File::Spec' }
283
284# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
285
286sub note_for_contents_file {
287  my($self, $namelets, $infile, $outfile) = @_;
288
289  # I think the infile and outfile parts are never used. -- SMB
290  # But it's handy to have them around for debugging.
291
292  if( $self->contents_file ) {
293    my $c = $self->_contents();
294    push @$c,
295     [ join("::", @$namelets), $infile, $outfile, $namelets ]
296     #            0               1         2         3
297    ;
298    DEBUG > 3 and print "Noting @$c[-1]\n";
299  }
300  return;
301}
302
303#_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
304
305sub write_contents_file {
306  my($self, $outdir) = @_;
307  my $outfile  = $self->_contents_filespec($outdir) || return;
308
309  $self->muse("Preparing list of modules for ToC");
310
311  my($toplevel,           # maps  toplevelbit => [all submodules]
312     $toplevel_form_freq, # ends up being  'foo' => 'Foo'
313    ) = $self->_prep_contents_breakdown;
314
315  my $Contents = eval { $self->_wopen($outfile) };
316  if( $Contents ) {
317    $self->muse( "Writing contents file $outfile" );
318  } else {
319    warn "Couldn't write-open contents file $outfile: $!\nAbort writing to $outfile at all";
320    return;
321  }
322
323  $self->_write_contents_start(  $Contents, $outfile, );
324  $self->_write_contents_middle( $Contents, $outfile, $toplevel, $toplevel_form_freq );
325  $self->_write_contents_end(    $Contents, $outfile, );
326  return $outfile;
327}
328
329# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
330
331sub _write_contents_start {
332  my($self, $Contents, $outfile) = @_;
333  my $starter = $self->contents_page_start || '';
334
335  {
336    my $css_wad = $self->_css_wad_to_markup(1);
337    if( $css_wad ) {
338      $starter =~ s{(</head>)}{\n$css_wad\n$1}i;  # otherwise nevermind
339    }
340
341    my $javascript_wad = $self->_javascript_wad_to_markup(1);
342    if( $javascript_wad ) {
343      $starter =~ s{(</head>)}{\n$javascript_wad\n$1}i;   # otherwise nevermind
344    }
345  }
346
347  unless(print $Contents $starter, "<dl class='superindex'>\n" ) {
348    warn "Couldn't print to $outfile: $!\nAbort writing to $outfile at all";
349    close($Contents);
350    return 0;
351  }
352  return 1;
353}
354
355# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
356
357sub _write_contents_middle {
358  my($self, $Contents, $outfile, $toplevel2submodules, $toplevel_form_freq) = @_;
359
360  foreach my $t (sort keys %$toplevel2submodules) {
361    my @downlines = sort {$a->[-1] cmp $b->[-1]}
362                          @{ $toplevel2submodules->{$t} };
363
364    printf $Contents qq[<dt><a name="%s">%s</a></dt>\n<dd>\n],
365      esc( $t, $toplevel_form_freq->{$t} )
366    ;
367
368    my($path, $name);
369    foreach my $e (@downlines) {
370      $name = $e->[0];
371      $path = join( "/", '.', esc( @{$e->[3]} ) )
372        . ($HTML_EXTENSION || $Pod::Simple::HTML::HTML_EXTENSION);
373      print $Contents qq{  <a href="$path">}, esc($name), "</a>&nbsp;&nbsp;\n";
374    }
375    print $Contents "</dd>\n\n";
376  }
377  return 1;
378}
379
380# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
381
382sub _write_contents_end {
383  my($self, $Contents, $outfile) = @_;
384  unless(
385    print $Contents "</dl>\n",
386      $self->contents_page_end || '',
387  ) {
388    warn "Couldn't write to $outfile: $!";
389  }
390  close($Contents) or warn "Couldn't close $outfile: $!";
391  return 1;
392}
393
394# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
395
396sub _prep_contents_breakdown {
397  my($self) = @_;
398  my $contents = $self->_contents;
399  my %toplevel; # maps  lctoplevelbit => [all submodules]
400  my %toplevel_form_freq; # ends up being  'foo' => 'Foo'
401                               # (mapping anycase forms to most freq form)
402
403  foreach my $entry (@$contents) {
404    my $toplevel =
405      $entry->[0] =~ m/^perl\w*$/ ? 'perl_core_docs'
406          # group all the perlwhatever docs together
407      : $entry->[3][0] # normal case
408    ;
409    ++$toplevel_form_freq{ lc $toplevel }{ $toplevel };
410    push @{ $toplevel{ lc $toplevel } }, $entry;
411    push @$entry, lc($entry->[0]); # add a sort-order key to the end
412  }
413
414  foreach my $toplevel (sort keys %toplevel) {
415    my $fgroup = $toplevel_form_freq{$toplevel};
416    $toplevel_form_freq{$toplevel} =
417    (
418      sort { $fgroup->{$b} <=> $fgroup->{$a}  or  $a cmp $b }
419        keys %$fgroup
420      # This hash is extremely unlikely to have more than 4 members, so this
421      # sort isn't so very wasteful
422    )[0];
423  }
424
425  return(\%toplevel, \%toplevel_form_freq) if wantarray;
426  return \%toplevel;
427}
428
429# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
430
431sub _contents_filespec {
432  my($self, $outdir) = @_;
433  my $outfile = $self->contents_file;
434  return unless $outfile;
435  return $self->filespecsys->catfile( $outdir, $outfile );
436}
437
438#_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
439
440sub makepath {
441  my($self, $outdir, $namelets) = @_;
442  return unless @$namelets > 1;
443  for my $i (0 .. ($#$namelets - 1)) {
444    my $dir = $self->filespecsys->catdir( $outdir, @$namelets[0 .. $i] );
445    if(-e $dir) {
446      die "$dir exists but not as a directory!?" unless -d $dir;
447      next;
448    }
449    DEBUG > 3 and print "  Making $dir\n";
450    mkdir $dir, 0777
451     or die "Can't mkdir $dir: $!\nAborting"
452    ;
453  }
454  return;
455}
456
457#_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
458
459sub batch_mode_page_object_init {
460  my $self = shift;
461  my($page, $module, $infile, $outfile, $depth) = @_;
462
463  # TODO: any further options to percolate onto this new object here?
464
465  $page->default_title($module);
466  $page->index( $self->index );
467
468  $page->html_css(        $self->       _css_wad_to_markup($depth) );
469  $page->html_javascript( $self->_javascript_wad_to_markup($depth) );
470
471  $self->add_header_backlink($page, $module, $infile, $outfile, $depth);
472  $self->add_footer_backlink($page, $module, $infile, $outfile, $depth);
473
474
475  return $self;
476}
477
478# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
479
480sub add_header_backlink {
481  my $self = shift;
482  return if $self->no_contents_links;
483  my($page, $module, $infile, $outfile, $depth) = @_;
484  $page->html_header_after_title( join '',
485    $page->html_header_after_title || '',
486
487    qq[<p class="backlinktop"><b><a name="___top" href="],
488    $self->url_up_to_contents($depth),
489    qq[" accesskey="1" title="All Documents">&lt;&lt;</a></b></p>\n],
490  )
491   if $self->contents_file
492  ;
493  return;
494}
495
496sub add_footer_backlink {
497  my $self = shift;
498  return if $self->no_contents_links;
499  my($page, $module, $infile, $outfile, $depth) = @_;
500  $page->html_footer( join '',
501    qq[<p class="backlinkbottom"><b><a name="___bottom" href="],
502    $self->url_up_to_contents($depth),
503    qq[" title="All Documents">&lt;&lt;</a></b></p>\n],
504
505    $page->html_footer || '',
506  )
507   if $self->contents_file
508  ;
509  return;
510}
511
512sub url_up_to_contents {
513  my($self, $depth) = @_;
514  --$depth;
515  return join '/', ('..') x $depth, esc($self->contents_file);
516}
517
518#_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
519
520sub find_all_pods {
521  my($self, $dirs) = @_;
522  # You can override find_all_pods in a subclass if you want to
523  #  do extra filtering or whatnot.  But for the moment, we just
524  #  pass to modnames2paths:
525  return $self->modnames2paths($dirs);
526}
527
528#_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-
529
530sub modnames2paths { # return a hashref mapping modulenames => paths
531  my($self, $dirs) = @_;
532
533  my $m2p;
534  {
535    my $search = $self->search_class->new;
536    DEBUG and print "Searching via $search\n";
537    $search->verbose(1) if DEBUG > 10;
538    $search->progress( $self->progress->copy->goal(0) ) if $self->progress;
539    $search->shadows(0);  # don't bother noting shadowed files
540    $search->inc(     $dirs ? 0      :  1 );
541    $search->survey(  $dirs ? @$dirs : () );
542    $m2p = $search->name2path;
543    die "What, no name2path?!" unless $m2p;
544  }
545
546  $self->muse("That's odd... no modules found!") unless keys %$m2p;
547  if( DEBUG > 4 ) {
548    print "Modules found (name => path):\n";
549    foreach my $m (sort {lc($a) cmp lc($b)} keys %$m2p) {
550      print "  $m  $$m2p{$m}\n";
551    }
552    print "(total ",     scalar(keys %$m2p), ")\n\n";
553  } elsif( DEBUG ) {
554    print      "Found ", scalar(keys %$m2p), " modules.\n";
555  }
556  $self->muse( "Found ", scalar(keys %$m2p), " modules." );
557
558  # return the Foo::Bar => /whatever/Foo/Bar.pod|pm hashref
559  return $m2p;
560}
561
562#===========================================================================
563
564sub _wopen {
565  # this is abstracted out so that the daemon class can override it
566  my($self, $outpath) = @_;
567  require Symbol;
568  my $out_fh = Symbol::gensym();
569  DEBUG > 5 and print "Write-opening to $outpath\n";
570  return $out_fh if open($out_fh, "> $outpath");
571  require Carp;
572  Carp::croak("Can't write-open $outpath: $!");
573}
574
575#==========================================================================
576
577sub add_css {
578  my($self, $url, $is_default, $name, $content_type, $media, $_code) = @_;
579  return unless $url;
580  unless($name) {
581    # cook up a reasonable name based on the URL
582    $name = $url;
583    if( $name !~ m/\?/ and $name =~ m{([^/]+)$}s ) {
584      $name = $1;
585      $name =~ s/\.css//i;
586    }
587  }
588  $media        ||= 'all';
589  $content_type ||= 'text/css';
590
591  my $bunch = [$url, $name, $content_type, $media, $_code];
592  if($is_default) { unshift @{ $self->_css_wad }, $bunch }
593  else            { push    @{ $self->_css_wad }, $bunch }
594  return;
595}
596
597# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
598
599sub _spray_css {
600  my($self, $outdir) = @_;
601
602  return unless $self->css_flurry();
603  $self->_gen_css_wad();
604
605  my $lol = $self->_css_wad;
606  foreach my $chunk (@$lol) {
607    my $url = $chunk->[0];
608    my $outfile;
609    if( ref($chunk->[-1]) and $url =~ m{^(_[-a-z0-9_]+\.css$)} ) {
610      $outfile = $self->filespecsys->catfile( $outdir, "$1" );
611      DEBUG > 5 and print "Noting $$chunk[0] as a file I'll create.\n";
612    } else {
613      DEBUG > 5 and print "OK, noting $$chunk[0] as an external CSS.\n";
614      # Requires no further attention.
615      next;
616    }
617
618    #$self->muse( "Writing autogenerated CSS file $outfile" );
619    my $Cssout = $self->_wopen($outfile);
620    print $Cssout ${$chunk->[-1]}
621     or warn "Couldn't print to $outfile: $!\nAbort writing to $outfile at all";
622    close($Cssout);
623    DEBUG > 5 and print "Wrote $outfile\n";
624  }
625
626  return;
627}
628
629# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
630
631sub _css_wad_to_markup {
632  my($self, $depth) = @_;
633
634  my @css  = @{ $self->_css_wad || return '' };
635  return '' unless @css;
636
637  my $rel = 'stylesheet';
638  my $out = '';
639
640  --$depth;
641  my $uplink = $depth ? ('../' x $depth) : '';
642
643  foreach my $chunk (@css) {
644    next unless $chunk and @$chunk;
645
646    my( $url1, $url2, $title, $type, $media) = (
647      $self->_maybe_uplink( $chunk->[0], $uplink ),
648      esc(grep !ref($_), @$chunk)
649    );
650
651    $out .= qq{<link rel="$rel" title="$title" type="$type" href="$url1$url2" media="$media" >\n};
652
653    $rel = 'alternate stylesheet'; # alternates = all non-first iterations
654  }
655  return $out;
656}
657
658# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
659sub _maybe_uplink {
660  # if the given URL looks relative, return the given uplink string --
661  # otherwise return emptystring
662  my($self, $url, $uplink) = @_;
663  ($url =~ m{^\./} or $url !~ m{[/\:]} )
664    ? $uplink
665    : ''
666    # qualify it, if/as needed
667}
668
669# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
670sub _gen_css_wad {
671  my $self = $_[0];
672  my $css_template = $self->_css_template;
673  foreach my $variation (
674
675   # Commented out for sake of concision:
676   #
677   #  011n=black_with_red_on_white
678   #  001n=black_with_yellow_on_white
679   #  101n=black_with_green_on_white
680   #  110=white_with_yellow_on_black
681   #  010=white_with_green_on_black
682   #  011=white_with_blue_on_black
683   #  100=white_with_red_on_black
684    '110n=blkbluw',  # black_with_blue_on_white
685    '010n=blkmagw',  # black_with_magenta_on_white
686    '100n=blkcynw',  # black_with_cyan_on_white
687    '101=whtprpk',   # white_with_purple_on_black
688    '001=whtnavk',   # white_with_navy_blue_on_black
689    '010a=grygrnk',  # grey_with_green_on_black
690    '010b=whtgrng',  # white_with_green_on_grey
691    '101an=blkgrng', # black_with_green_on_grey
692    '101bn=grygrnw', # grey_with_green_on_white
693  ) {
694
695    my $outname = $variation;
696    my($flipmode, @swap) = ( ($4 || ''), $1,$2,$3)
697      if $outname =~ s/^([012])([012])([[012])([a-z]*)=?//s;
698    @swap = () if '010' eq join '', @swap; # 010 is a swop-no-op!
699
700    my $this_css =
701      "/* This file is autogenerated.  Do not edit.  $variation */\n\n"
702      . $css_template;
703
704    # Only look at three-digitty colors, for now at least.
705    if( $flipmode =~ m/n/ ) {
706      $this_css =~ s/(#[0-9a-fA-F]{3})\b/_color_negate($1)/eg;
707      $this_css =~ s/\bthin\b/medium/g;
708    }
709    $this_css =~ s<#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])\b>
710                  < join '', '#', ($1,$2,$3)[@swap] >eg   if @swap;
711
712    if(   $flipmode =~ m/a/)
713       { $this_css =~ s/#fff\b/#999/gi } # black -> dark grey
714    elsif($flipmode =~ m/b/)
715       { $this_css =~ s/#000\b/#666/gi } # white -> light grey
716
717    my $name = $outname;
718    $name =~ tr/-_/  /;
719    $self->add_css( "_$outname.css", 0, $name, 0, 0, \$this_css);
720  }
721
722  # Now a few indexless variations:
723  foreach my $variation (
724      'blkbluw', # black_with_blue_on_white
725      'whtpurk', # white_with_purple_on_black
726      'whtgrng', # white_with_green_on_grey
727      'grygrnw', # grey_with_green_on_white
728  ) {
729    my $outname = "$variation\_";
730    my $this_css = join "\n",
731      "/* This file is autogenerated.  Do not edit.  $outname */\n",
732      "\@import url(\"./_$variation.css\");",
733      ".indexgroup { display: none; }",
734      "\n",
735    ;
736    my $name = $outname;
737    $name =~ tr/-_/  /;
738    $self->add_css( "$outname.css", 0, $name, 0, 0, \$this_css);
739  }
740
741  return;
742}
743
744sub _color_negate {
745  my $x = lc $_[0];
746  $x =~ tr[0123456789abcdef]
747          [fedcba9876543210];
748  return $x;
749}
750
751#===========================================================================
752
753sub add_javascript {
754  my($self, $url, $content_type, $_code) = @_;
755  return unless $url;
756  push  @{ $self->_javascript_wad }, [
757    $url, $content_type || 'text/javascript', $_code
758  ];
759  return;
760}
761
762sub _spray_javascript {
763  my($self, $outdir) = @_;
764  return unless $self->javascript_flurry();
765  $self->_gen_javascript_wad();
766
767  my $lol = $self->_javascript_wad;
768  foreach my $script (@$lol) {
769    my $url = $script->[0];
770    my $outfile;
771
772    if( ref($script->[-1]) and $url =~ m{^(_[-a-z0-9_]+\.js$)} ) {
773      $outfile = $self->filespecsys->catfile( $outdir, "$1" );
774      DEBUG > 5 and print "Noting $$script[0] as a file I'll create.\n";
775    } else {
776      DEBUG > 5 and print "OK, noting $$script[0] as an external JavaScript.\n";
777      next;
778    }
779
780    #$self->muse( "Writing JavaScript file $outfile" );
781    my $Jsout = $self->_wopen($outfile);
782
783    print $Jsout ${$script->[-1]}
784     or warn "Couldn't print to $outfile: $!\nAbort writing to $outfile at all";
785    close($Jsout);
786    DEBUG > 5 and print "Wrote $outfile\n";
787  }
788
789  return;
790}
791
792sub _gen_javascript_wad {
793  my $self = $_[0];
794  my $js_code = $self->_javascript || return;
795  $self->add_javascript( "_podly.js", 0, \$js_code);
796  return;
797}
798
799sub _javascript_wad_to_markup {
800  my($self, $depth) = @_;
801
802  my @scripts  = @{ $self->_javascript_wad || return '' };
803  return '' unless @scripts;
804
805  my $out = '';
806
807  --$depth;
808  my $uplink = $depth ? ('../' x $depth) : '';
809
810  foreach my $s (@scripts) {
811    next unless $s and @$s;
812
813    my( $url1, $url2, $type, $media) = (
814      $self->_maybe_uplink( $s->[0], $uplink ),
815      esc(grep !ref($_), @$s)
816    );
817
818    $out .= qq{<script type="$type" src="$url1$url2"></script>\n};
819  }
820  return $out;
821}
822
823#===========================================================================
824
825sub _css_template { return $CSS }
826sub _javascript   { return $JAVASCRIPT }
827
828$CSS = <<'EOCSS';
829/* For accessibility reasons, never specify text sizes in px/pt/pc/in/cm/mm */
830
831@media all { .hide { display: none; } }
832
833@media print {
834  .noprint, div.indexgroup, .backlinktop, .backlinkbottom { display: none }
835
836  * {
837    border-color: black !important;
838    color: black !important;
839    background-color: transparent !important;
840    background-image: none !important;
841  }
842
843  dl.superindex > dd  {
844    word-spacing: .6em;
845  }
846}
847
848@media aural, braille, embossed {
849  div.indexgroup  { display: none; }  /* Too noisy, don't you think? */
850  dl.superindex > dt:before { content: "Group ";  }
851  dl.superindex > dt:after  { content: " contains:"; }
852  .backlinktop    a:before  { content: "Back to contents"; }
853  .backlinkbottom a:before  { content: "Back to contents"; }
854}
855
856@media aural {
857  dl.superindex > dt  { pause-before: 600ms; }
858}
859
860@media screen, tty, tv, projection {
861  .noscreen { display: none; }
862
863  a:link    { color: #7070ff; text-decoration: underline; }
864  a:visited { color: #e030ff; text-decoration: underline; }
865  a:active  { color: #800000; text-decoration: underline; }
866  body.contentspage a            { text-decoration: none; }
867  a.u { color: #fff !important; text-decoration: none; }
868
869  body.pod {
870    margin: 0 5px;
871    color:            #fff;
872    background-color: #000;
873  }
874
875  body.pod h1, body.pod h2, body.pod h3, body.pod h4  {
876    font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif;
877    font-weight: normal;
878    margin-top: 1.2em;
879    margin-bottom: .1em;
880    border-top: thin solid transparent;
881    /* margin-left: -5px;  border-left: 2px #7070ff solid;  padding-left: 3px; */
882  }
883
884  body.pod h1  { border-top-color: #0a0; }
885  body.pod h2  { border-top-color: #080; }
886  body.pod h3  { border-top-color: #040; }
887  body.pod h4  { border-top-color: #010; }
888
889  p.backlinktop + h1 { border-top: none; margin-top: 0em;  }
890  p.backlinktop + h2 { border-top: none; margin-top: 0em;  }
891  p.backlinktop + h3 { border-top: none; margin-top: 0em;  }
892  p.backlinktop + h4 { border-top: none; margin-top: 0em;  }
893
894  body.pod dt {
895    font-size: 105%; /* just a wee bit more than normal */
896  }
897
898  .indexgroup { font-size: 80%; }
899
900  .backlinktop,   .backlinkbottom    {
901    margin-left:  -5px;
902    margin-right: -5px;
903    background-color:         #040;
904    border-top:    thin solid #050;
905    border-bottom: thin solid #050;
906  }
907
908  .backlinktop a, .backlinkbottom a  {
909    text-decoration: none;
910    color: #080;
911    background-color:  #000;
912    border: thin solid #0d0;
913  }
914  .backlinkbottom { margin-bottom: 0; padding-bottom: 0; }
915  .backlinktop    { margin-top:    0; padding-top:    0; }
916
917  body.contentspage {
918    color:            #fff;
919    background-color: #000;
920  }
921
922  body.contentspage h1  {
923    color:            #0d0;
924    margin-left: 1em;
925    margin-right: 1em;
926    text-indent: -.9em;
927    font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif;
928    font-weight: normal;
929    border-top:    thin solid #fff;
930    border-bottom: thin solid #fff;
931    text-align: center;
932  }
933
934  dl.superindex > dt  {
935    font-family: Tahoma, Verdana, Helvetica, Arial, sans-serif;
936    font-weight: normal;
937    font-size: 90%;
938    margin-top: .45em;
939    /* margin-bottom: -.15em; */
940  }
941  dl.superindex > dd  {
942    word-spacing: .6em;    /* most important rule here! */
943  }
944  dl.superindex > a:link  {
945    text-decoration: none;
946    color: #fff;
947  }
948
949  .contentsfooty {
950    border-top: thin solid #999;
951    font-size: 90%;
952  }
953
954}
955
956/* The End */
957
958EOCSS
959
960#==========================================================================
961
962$JAVASCRIPT = <<'EOJAVASCRIPT';
963
964// From http://www.alistapart.com/articles/alternate/
965
966function setActiveStyleSheet(title) {
967  var i, a, main;
968  for(i=0  ;  (a = document.getElementsByTagName("link")[i])  ;  i++) {
969    if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) {
970      a.disabled = true;
971      if(a.getAttribute("title") == title) a.disabled = false;
972    }
973  }
974}
975
976function getActiveStyleSheet() {
977  var i, a;
978  for(i=0  ;  (a = document.getElementsByTagName("link")[i])  ;  i++) {
979    if(   a.getAttribute("rel").indexOf("style") != -1
980       && a.getAttribute("title")
981       && !a.disabled
982       ) return a.getAttribute("title");
983  }
984  return null;
985}
986
987function getPreferredStyleSheet() {
988  var i, a;
989  for(i=0  ;  (a = document.getElementsByTagName("link")[i])  ;  i++) {
990    if(   a.getAttribute("rel").indexOf("style") != -1
991       && a.getAttribute("rel").indexOf("alt") == -1
992       && a.getAttribute("title")
993       ) return a.getAttribute("title");
994  }
995  return null;
996}
997
998function createCookie(name,value,days) {
999  if (days) {
1000    var date = new Date();
1001    date.setTime(date.getTime()+(days*24*60*60*1000));
1002    var expires = "; expires="+date.toGMTString();
1003  }
1004  else expires = "";
1005  document.cookie = name+"="+value+expires+"; path=/";
1006}
1007
1008function readCookie(name) {
1009  var nameEQ = name + "=";
1010  var ca = document.cookie.split(';');
1011  for(var i=0  ;  i < ca.length  ;  i++) {
1012    var c = ca[i];
1013    while (c.charAt(0)==' ') c = c.substring(1,c.length);
1014    if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
1015  }
1016  return null;
1017}
1018
1019window.onload = function(e) {
1020  var cookie = readCookie("style");
1021  var title = cookie ? cookie : getPreferredStyleSheet();
1022  setActiveStyleSheet(title);
1023}
1024
1025window.onunload = function(e) {
1026  var title = getActiveStyleSheet();
1027  createCookie("style", title, 365);
1028}
1029
1030var cookie = readCookie("style");
1031var title = cookie ? cookie : getPreferredStyleSheet();
1032setActiveStyleSheet(title);
1033
1034// The End
1035
1036EOJAVASCRIPT
1037
1038# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
10391;
1040__END__
1041
1042
1043=head1 NAME
1044
1045Pod::Simple::HTMLBatch - convert several Pod files to several HTML files
1046
1047=head1 SYNOPSIS
1048
1049  perl -MPod::Simple::HTMLBatch -e 'Pod::Simple::HTMLBatch::go' in out
1050
1051
1052=head1 DESCRIPTION
1053
1054This module is used for running batch-conversions of a lot of HTML
1055documents
1056
1057This class is NOT a subclass of Pod::Simple::HTML
1058(nor of bad old Pod::Html) -- although it uses
1059Pod::Simple::HTML for doing the conversion of each document.
1060
1061The normal use of this class is like so:
1062
1063  use Pod::Simple::HTMLBatch;
1064  my $batchconv = Pod::Simple::HTMLBatch->new;
1065  $batchconv->some_option( some_value );
1066  $batchconv->some_other_option( some_other_value );
1067  $batchconv->batch_convert( \@search_dirs, $output_dir );
1068
1069=head2 FROM THE COMMAND LINE
1070
1071Note that this class also provides
1072(but does not export) the function Pod::Simple::HTMLBatch::go.
1073This is basically just a shortcut for C<<
1074Pod::Simple::HTMLBatch->batch_convert(@ARGV) >>.
1075It's meant to be handy for calling from the command line.
1076
1077However, the shortcut requires that you specify exactly two command-line
1078arguments, C<indirs> and C<outdir>.
1079
1080Example:
1081
1082  % mkdir out_html
1083  % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go @INC out_html
1084      (to convert the pod from Perl's @INC
1085       files under the directory ../htmlversion)
1086
1087(Note that the command line there contains a literal atsign-I-N-C.  This
1088is handled as a special case by batch_convert, in order to save you having
1089to enter the odd-looking "" as the first command-line parameter when you
1090mean "just use whatever's in @INC".)
1091
1092Example:
1093
1094  % mkdir ../seekrut
1095  % chmod og-rx ../seekrut
1096  % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go . ../htmlversion
1097      (to convert the pod under the current dir into HTML
1098       files under the directory ../htmlversion)
1099
1100Example:
1101
1102  % perl -MPod::Simple::HTMLBatch -e Pod::Simple::HTMLBatch::go happydocs .
1103      (to convert all pod from happydocs into the current directory)
1104
1105
1106
1107=head1 MAIN METHODS
1108
1109=over
1110
1111=item $batchconv = Pod::Simple::HTMLBatch->new;
1112
1113This TODO
1114
1115
1116=item $batchconv->batch_convert( I<indirs>, I<outdir> );
1117
1118this TODO
1119
1120=item $batchconv->batch_convert( undef    , ...);
1121
1122=item $batchconv->batch_convert( q{@INC}, ...);
1123
1124These two values for I<indirs> specify that the normal Perl @INC
1125
1126=item $batchconv->batch_convert( \@dirs , ...);
1127
1128This specifies that the input directories are the items in
1129the arrayref C<\@dirs>.
1130
1131=item $batchconv->batch_convert( "somedir" , ...);
1132
1133This specifies that the director "somedir" is the input.
1134(This can be an absolute or relative path, it doesn't matter.)
1135
1136A common value you might want would be just "." for the current
1137directory:
1138
1139     $batchconv->batch_convert( "." , ...);
1140
1141
1142=item $batchconv->batch_convert( 'somedir:someother:also' , ...);
1143
1144This specifies that you want the dirs "somedir", "somother", and "also"
1145scanned, just as if you'd passed the arrayref
1146C<[qw( somedir someother also)]>.  Note that a ":"-separator is normal
1147under Unix, but Under MSWin, you'll need C<'somedir;someother;also'>
1148instead, since the pathsep on MSWin is ";" instead of ":".  (And
1149I<that> is because ":" often comes up in paths, like
1150C<"c:/perl/lib">.)
1151
1152(Exactly what separator character should be used, is gotten from
1153C<$Config::Config{'path_sep'}>, via the L<Config> module.)
1154
1155=item $batchconv->batch_convert( ... , undef );
1156
1157This specifies that you want the HTML output to go into the current
1158directory.
1159
1160(Note that a missing or undefined value means a different thing in
1161the first slot than in the second.  That's so that C<batch_convert()>
1162with no arguments (or undef arguments) means "go from @INC, into
1163the current directory.)
1164
1165=item $batchconv->batch_convert( ... , 'somedir' );
1166
1167This specifies that you want the HTML output to go into the
1168directory 'somedir'.
1169(This can be an absolute or relative path, it doesn't matter.)
1170
1171=back
1172
1173
1174Note that you can also call C<batch_convert> as a class method,
1175like so:
1176
1177  Pod::Simple::HTMLBatch->batch_convert( ... );
1178
1179That is just short for this:
1180
1181  Pod::Simple::HTMLBatch-> new-> batch_convert(...);
1182
1183That is, it runs a conversion with default options, for
1184whatever inputdirs and output dir you specify.
1185
1186
1187=head2 ACCESSOR METHODS
1188
1189The following are all accessor methods -- that is, they don't do anything
1190on their own, but just alter the contents of the conversion object,
1191which comprises the options for this particular batch conversion.
1192
1193We show the "put" form of the accessors below (i.e., the syntax you use
1194for setting the accessor to a specific value).  But you can also
1195call each method with no parameters to get its current value.  For
1196example, C<< $self->contents_file() >> returns the current value of
1197the contents_file attribute.
1198
1199=over
1200
1201
1202=item $batchconv->verbose( I<nonnegative_integer> );
1203
1204This controls how verbose to be during batch conversion, as far as
1205notes to STDOUT (or whatever is C<select>'d) about how the conversion
1206is going.  If 0, no progress information is printed.
1207If 1 (the default value), some progress information is printed.
1208Higher values print more information.
1209
1210
1211=item $batchconv->index( I<true-or-false> );
1212
1213This controls whether or not each HTML page is liable to have a little
1214table of contents at the top (which we call an "index" for historical
1215reasons).  This is true by default.
1216
1217
1218=item $batchconv->contents_file( I<filename> );
1219
1220If set, should be the name of a file (in the output directory)
1221to write the HTML index to.  The default value is "index.html".
1222If you set this to a false value, no contents file will be written.
1223
1224=item $batchconv->contents_page_start( I<HTML_string> );
1225
1226This specifies what string should be put at the beginning of
1227the contents page.
1228The default is a string more or less like this:
1229
1230  <html>
1231  <head><title>Perl Documentation</title></head>
1232  <body class='contentspage'>
1233  <h1>Perl Documentation</h1>
1234
1235=item $batchconv->contents_page_end( I<HTML_string> );
1236
1237This specifies what string should be put at the end of the contents page.
1238The default is a string more or less like this:
1239
1240  <p class='contentsfooty'>Generated by
1241  Pod::Simple::HTMLBatch v3.01 under Perl v5.008
1242  <br >At Fri May 14 22:26:42 2004 GMT,
1243  which is Fri May 14 14:26:42 2004 local time.</p>
1244
1245
1246
1247=item $batchconv->add_css( $url );
1248
1249TODO
1250
1251=item $batchconv->add_javascript( $url );
1252
1253TODO
1254
1255=item $batchconv->css_flurry( I<true-or-false> );
1256
1257If true (the default value), we autogenerate some CSS files in the
1258output directory, and set our HTML files to use those.
1259TODO: continue
1260
1261=item $batchconv->javascript_flurry( I<true-or-false> );
1262
1263If true (the default value), we autogenerate a JavaScript in the
1264output directory, and set our HTML files to use it.  Currently,
1265the JavaScript is used only to get the browser to remember what
1266stylesheet it prefers.
1267TODO: continue
1268
1269=item $batchconv->no_contents_links( I<true-or-false> );
1270
1271TODO
1272
1273=item $batchconv->html_render_class( I<classname> );
1274
1275This sets what class is used for rendering the files.
1276The default is "Pod::Simple::HTML".  If you set it to something else,
1277it should probably be a subclass of Pod::Simple::HTML, and you should
1278C<require> or C<use> that class so that's it's loaded before
1279Pod::Simple::HTMLBatch tries loading it.
1280
1281=item $batchconv->search_class( I<classname> );
1282
1283This sets what class is used for searching for the files.
1284The default is "Pod::Simple::Search".  If you set it to something else,
1285it should probably be a subclass of Pod::Simple::Search, and you should
1286C<require> or C<use> that class so that's it's loaded before
1287Pod::Simple::HTMLBatch tries loading it.
1288
1289=back
1290
1291
1292
1293
1294=head1 NOTES ON CUSTOMIZATION
1295
1296TODO
1297
1298  call add_css($someurl) to add stylesheet as alternate
1299  call add_css($someurl,1) to add as primary stylesheet
1300
1301  call add_javascript
1302
1303  subclass Pod::Simple::HTML and set $batchconv->html_render_class to
1304    that classname
1305  and maybe override
1306    $page->batch_mode_page_object_init($self, $module, $infile, $outfile, $depth)
1307  or maybe override
1308    $batchconv->batch_mode_page_object_init($page, $module, $infile, $outfile, $depth)
1309  subclass Pod::Simple::Search and set $batchconv->search_class to
1310    that classname
1311
1312
1313
1314=head1 ASK ME!
1315
1316If you want to do some kind of big pod-to-HTML version with some
1317particular kind of option that you don't see how to achieve using this
1318module, email me (C<sburke@cpan.org>) and I'll probably have a good idea
1319how to do it. For reasons of concision and energetic laziness, some
1320methods and options in this module (and the dozen modules it depends on)
1321are undocumented; but one of those undocumented bits might be just what
1322you're looking for.
1323
1324
1325=head1 SEE ALSO
1326
1327L<Pod::Simple>, L<Pod::Simple::HTMLBatch>, L<perlpod>, L<perlpodspec>
1328
1329=head1 SUPPORT
1330
1331Questions or discussion about POD and Pod::Simple should be sent to the
1332pod-people@perl.org mail list. Send an empty email to
1333pod-people-subscribe@perl.org to subscribe.
1334
1335This module is managed in an open GitHub repository,
1336L<http://github.com/theory/pod-simple/>. Feel free to fork and contribute, or
1337to clone L<git://github.com/theory/pod-simple.git> and send patches!
1338
1339Patches against Pod::Simple are welcome. Please send bug reports to
1340<bug-pod-simple@rt.cpan.org>.
1341
1342=head1 COPYRIGHT AND DISCLAIMERS
1343
1344Copyright (c) 2002 Sean M. Burke.
1345
1346This library is free software; you can redistribute it and/or modify it
1347under the same terms as Perl itself.
1348
1349This program is distributed in the hope that it will be useful, but
1350without any warranty; without even the implied warranty of
1351merchantability or fitness for a particular purpose.
1352
1353=head1 AUTHOR
1354
1355Pod::Simple was created by Sean M. Burke <sburke@cpan.org>.
1356But don't bother him, he's retired.
1357
1358Pod::Simple is maintained by:
1359
1360=over
1361
1362=item * Allison Randal C<allison@perl.org>
1363
1364=item * Hans Dieter Pearcey C<hdp@cpan.org>
1365
1366=item * David E. Wheeler C<dwheeler@cpan.org>
1367
1368=back
1369
1370=cut
1371