File Coverage

blib/lib/Pod/Manual.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Pod::Manual;
2              
3 3     3   94386 use Object::InsideOut;
  3         225522  
  3         21  
4              
5 3     3   322 use warnings;
  3         5  
  3         108  
6 3     3   14 no warnings qw/ uninitialized /;
  3         10  
  3         85  
7 3     3   13 use strict;
  3         5  
  3         94  
8 3     3   13 use Carp;
  3         5  
  3         249  
9              
10 3     3   14 use Cwd;
  3         6  
  3         167  
11 3     3   1831 use XML::LibXML;
  0            
  0            
12             use Pod::XML;
13             use Pod::2::DocBook;
14             use Pod::Find qw/ pod_where /;
15             use File::Temp qw/ tempfile tempdir /;
16             use File::Copy;
17             use List::MoreUtils qw/ any /;
18             use Params::Validate;
19              
20             our $VERSION = '0.08_04';
21              
22             my @parser_of :Field;
23             my @dom_of :Field;
24             my @appendix_of :Field;
25             my @root_of :Field;
26             my @ignore_sections :Field
27             :Set(set_ignore)
28             :Arg(Name => 'ignore_sections', Type => 'array')
29             ;
30             my @appendix_sections :Field
31             :Args(Name => 'appendix_sections', Type => 'array')
32             :Set(set_appendix_sections)
33             ;
34             my @title :Field
35             :Arg(title)
36             :Get(get_title);
37             my @unique_id :Field;
38             my @pdf_generator :Field
39             :Arg(Name => 'pdf_generator', Default => 'latex', Pre => sub { lc $_[4] if $_[4]} )
40             :Std(Name => 'pdf_generator', Pre => sub { lc $_[4] } )
41             :Type(sub { grep { $_[0] eq $_ } qw/ prince latex / } )
42             ;
43             my @prince_css :Field
44             :Arg('prince_css')
45             :Set(set_prince_css)
46             ;
47              
48              
49             ### Special accessors ##########################################
50              
51             sub get_appendix_sections {
52             my $self = shift;
53             return $appendix_sections[ $$self ] ? @{ $appendix_sections[ $$self ] }
54             : ()
55             ;
56             }
57              
58             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
59              
60             sub get_ignore_sections {
61             my $self = shift;
62              
63             return unless $ignore_sections[ $$self ];
64              
65             return @{ $ignore_sections[ $$self ] };
66             }
67              
68             sub get_prince_css {
69             my $self = shift;
70              
71             unless ( $prince_css[$$self] ) {
72              
73             # try to find the stylesheet
74             #<<< perltidy off
75             my ($css) = grep { -f $_ }
76             map { $_ . '/lib/prince/style/docbook.css' }
77             qw# /usr /usr/local #;
78             #>>>
79              
80             $self->set_prince_css($css) if $css;
81             }
82              
83             return $prince_css[$$self];
84             }
85              
86             sub unique_id {
87             return ++$unique_id[ ${$_[0]} ];
88             }
89              
90             sub _init :Init {
91             my $self = shift;
92             my $args_ref = shift;
93              
94             my $parser = $parser_of[ $$self ] = XML::LibXML->new;
95              
96             $dom_of[ $$self ] = $parser->parse_string(
97             '' </td> </tr> <tr> <td class="h" > <a name="98">98</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> . $self->get_title </td> </tr> <tr> <td class="h" > <a name="99">99</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> . ''
100             );
101              
102             $dom_of[ $$self ]->setEncoding( 'iso-8859-1' );
103              
104             $root_of[ $$self ] = $dom_of[ $$self ]->documentElement;
105              
106             $appendix_of[ $$self ] = undef;
107              
108             }
109              
110             sub set_title {
111             my( $self, $title ) = @_;
112              
113             $title[ $$self ] = $title;
114              
115             return unless $dom_of[ $$self ];
116              
117             my $title_node = $dom_of[ $$self ]->findnodes( '/book/bookinfo/title')
118             ->[0];
119             # remove any possible title already there
120             $title_node->removeChild( $_ ) for $title_node->childNodes;
121              
122             $title_node->appendText( $title );
123              
124             return;
125             }
126              
127             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
128              
129             sub _find_module_pod {
130             my $self = shift;
131             my $module = shift;
132              
133             my $file_location = pod_where( { -inc => 1 }, $module )
134             or die "couldn't find pod for module $module\n";
135              
136             local $/ = undef;
137             open my $pod_fh, '<', $file_location
138             or die "can't open pod file $file_location: $!";
139              
140             return <$pod_fh>;
141             }
142              
143             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
144              
145             sub _convert_pod_to_xml {
146             my $self = shift;
147             my $pod = shift;
148              
149             my $parser = Pod::2::DocBook->new ( doctype => 'chapter',);
150              
151             my $podxml;
152             local *STDOUT;
153             open STDOUT, '>', \$podxml;
154             open my $input_fh, '<', \$pod;
155             $parser->parse_from_filehandle( $input_fh );
156              
157             my $dom = eval {
158             $parser_of[ $$self ]->parse_string( $podxml )
159             } or die "error while converting raw pod to xml for '$pod': $@";
160              
161             return $dom;
162             }
163              
164             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
165              
166             sub _get_podxml {
167             my $self = shift;
168             my $pod = shift;
169              
170             my $pod_location = pod_where( { -inc => 1 }, $pod );
171              
172             my $parser = Pod::XML->new;
173              
174             my $podxml;
175             local *STDOUT;
176             open STDOUT, '>', \$podxml;
177             $parser->parse_from_file( $pod_location );
178             close STDOUT;
179              
180             $podxml =~ s/xmlns=".*?"//;
181             $podxml =~ s#]]>\n
182              
183             my $dom = eval {
184             $parser_of[ $$self ]->parse_string( $podxml )
185             } or die "error while converting raw pod to xml for '$pod': $@";
186              
187             return $dom;
188             }
189              
190             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
191              
192             sub add_chapters {
193             my $self = shift;
194             my $options = 'HASH' eq ref $_[-1] ? pop @_ : { };
195              
196             $self->add_chapter( $_ => $options ) for @_;
197             }
198              
199             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
200              
201              
202             sub add_chapter {
203             my $self = shift;
204             my $chapter = shift;
205              
206             my %option = validate( @_, {
207             ignore_sections => { default => [ $self->get_ignore_sections ] },
208             appendix_sections => { default => [ $self->get_appendix_sections ] },
209             set_title => 0,
210             } );
211              
212             # simplify things later
213             for my $i ( qw/ ignore_sections appendix_sections / ) {
214             unless ( ref $option{ $i } ) {
215             $option{$i} = [ $option{$i} ];
216             }
217             }
218              
219             my $podxml;
220            
221             # the chapter can be passed as various things
222             if ( $chapter =~ /\n/ ) { # it's pure pod
223             $podxml = $self->_convert_pod_to_xml( $chapter );
224             }
225             elsif ( -f $chapter ) { # it's a file
226             local $/ = undef;
227             open my $pod_fh, '<', $chapter
228             or die "can't open pod file $chapter: $!";
229             $podxml = $self->_convert_pod_to_xml( <$pod_fh> );
230             }
231             else { # it's a module name
232             $podxml = $self->_convert_pod_to_xml(
233             $self->_find_module_pod( $chapter )
234             );
235             }
236              
237             my $dom = $dom_of[ $$self ];
238              
239             my $subdoc = $podxml->documentElement;
240             #my $docbook = XML::XPathScript->new->transform( $podxml,
241             # $Pod::Manual::PodXML2Docbook::stylesheet );
242              
243             #my $subdoc = eval {
244             # XML::LibXML->new->parse_string( $docbook )->documentElement;
245             #};
246              
247             if ( $@ ) {
248             croak "chapter couldn't be converted to docbook: $@";
249             }
250              
251             # give the chapter an id if there isn't
252             unless ( $subdoc->getAttribute( 'id' ) ) {
253             $subdoc->setAttribute( 'id' => 'chapter-'.$self->unique_id );
254             }
255              
256             # fudge the id of the sections as well
257             for my $s ( $subdoc->findnodes( '//section' ) ) {
258             $s->setAttribute( id => 'section-'.$self->unique_id );
259             }
260              
261             # fix the title
262             if ( my ( $node ) = $subdoc->findnodes( 'section[title/text()="NAME"]' ) ) {
263             my $title = $node->findvalue( 'para/text()' );
264             my ( $title_node ) = $subdoc->findnodes( 'title' );
265             $title_node->appendText( $title );
266             if ( $title =~ /-/ ) {
267             my ( $short ) = split /\s*-\s*/, $title;
268              
269             my $abbrev = $title_node->ownerDocument->createElement( 'titleabbrev' );
270             $abbrev->appendText( $short );
271              
272             $title_node->appendChild( $abbrev );
273             }
274             $node->unbindNode;
275             }
276              
277             # trash sections we don't want to see
278             for my $section ( $subdoc->findnodes( 'section' ) ) {
279             my $title = $section->findvalue( 'title/text()' );
280             if ( any { $_ eq $title } @{ $option{ignore_sections} } ) {
281             $section->unbindNode;
282             }
283             }
284              
285             # give abbreviations to all section titles
286             for my $title ( $subdoc->findnodes( 'section/title' ) ) {
287             next if $title->findnodes( 'titleabbrev' ); #already there
288             my $abbrev = $title->ownerDocument->createElement( 'titleabbrev' );
289             my $text = $title->toString;
290             $title->appendChild( $abbrev );
291             # FIXME
292             $text =~ s/^<.*?>//;
293             $text =~ s#$##;
294             if ( length( $text ) > 20 ) {
295             # heuristics... *cross fingers*
296             $text =~ s/\s+-.*$//; # something - like this
297             $text =~ s/\(.*?\)/( ... )/;
298             }
299             $abbrev->appendText( $text );
300             }
301            
302              
303             # use the title of that section if the 'doc_title' option is
304             # used, or if there are no title given yet
305             if ( $option{set_title} or not defined $self->get_title ) {
306             my $title = $subdoc->findvalue( '/chapter/title/text()' );
307             $title =~ s/\s*-.*//; # remove desc after the '-'
308             $self->set_title( $title ) if $title;
309             }
310              
311              
312             $dom->adoptNode( $subdoc );
313              
314             # if there is no appendix, it adds the chapter
315             # at the end of the document
316             $root_of[ $$self ]->insertBefore( $subdoc, $appendix_of[ $$self ] );
317              
318             for my $section_title ( @{ $option{appendix_sections} } ) {
319             $self->_add_to_appendix(
320             grep { $_->findvalue( 'title/text()' ) eq $section_title }
321             $subdoc->findnodes( 'section' )
322             );
323             }
324              
325             return $self;
326             }
327              
328             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
329              
330             sub as_dom {
331             my $self = shift;
332             return $dom_of[ $$self ];
333             }
334              
335             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
336              
337             sub as_docbook {
338             my $self = shift;
339             my %option = ref $_[0] eq 'HASH' ? %{ $_[0] } : () ;
340              
341             # generate the toc
342             $self->generate_toc;
343              
344             my $dom = $dom_of[ $$self ];
345              
346             if ( my $css = $option{ css } ) {
347             # make a copy of the dom so that we're not stuck with the PI
348             $dom = $parser_of[ $$self ]->parse_string( $dom->toString );
349              
350             my $pi = $dom->createPI( 'xml-stylesheet'
351             => qq{href="$css" type="text/css"} );
352             $dom->insertBefore( $pi, $dom->firstChild );
353             }
354              
355              
356             return $dom->toString;
357             }
358              
359             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
360              
361             sub generate_toc {
362             my $self = shift;
363             my $dom = $dom_of[ $$self ];
364              
365             # if there's already a toc, nuke it
366             for ( $dom->findnodes( 'toc' ) ) {
367             $_->unbindNode;
368             }
369              
370             my $toc = $dom->createElement( 'toc' );
371             my ( $bookinfo ) = $dom->findnodes( '/book/bookinfo' );
372             $bookinfo->parentNode->insertAfter( $toc, $bookinfo );
373              
374             for my $chapter ( $dom->findnodes( '/book/chapter' ),
375             $dom->findnodes( '/book/appendix' ) ) {
376             $self->add_entry_to_toc( 0, $toc, $chapter );
377             }
378             }
379              
380             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
381              
382             sub tag_content {
383             my $node;
384             my $text;
385             $text .= $_->toString for $node->childNodes;
386             return $text;
387             }
388              
389             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
390              
391             sub add_entry_to_toc {
392             my ( $self, $level, $toc, $chapter ) = @_;
393              
394             my $tocchap = $chapter->ownerDocument->createElement(
395             $level == 0 ? 'tocchap' : 'toclevel'.$level
396             );
397             $toc->addChild( $tocchap );
398              
399             my $title = $chapter->findvalue( 'title/titleabbrev/text()' )
400             || $chapter->findvalue( 'title/text()' );
401              
402             my $tocentry = $chapter->ownerDocument->createElement( 'tocentry' );
403             $tocchap->addChild( $tocentry );
404             $tocentry->setAttribute( href => '#'.$chapter->getAttribute( 'id' ) );
405             $tocentry->appendText( $title );
406              
407             for my $child ( $chapter->findnodes( 'section' ) ) {
408             $self->add_entry_to_toc( $level + 1, $tocchap, $child );
409             }
410             }
411              
412             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
413              
414             sub as_latex {
415             my $self = shift;
416              
417             eval {
418             require XML::XPathScript;
419             require Pod::Manual::Docbook2LaTeX;
420             };
421              
422             croak 'as_latex() requires module XML::XPathScript to be installed'
423             if $@;
424              
425             my $xps = XML::XPathScript->new;
426              
427             my $docbook = eval { $xps->transform(
428             $self->as_docbook => $Pod::Manual::Docbook2LaTeX::stylesheet
429             ) } ;
430              
431             croak "couldn't convert to docbook: $@" if $@;
432              
433             return $docbook;
434             }
435              
436             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
437              
438             sub generate_pdf_using_prince {
439             my ( $self, $filename ) = @_;
440              
441             # if no css, we must create our own
442              
443             my $docbook = $self->as_docbook({ css =>
444             $self->local_prince_css( '.' ) });
445              
446             open my $db_fh, '>', 'manual.docbook'
447             or croak "can't open file 'manual.docbook' for writing: $!";
448              
449             print $db_fh $docbook;
450             close $db_fh;
451              
452             system 'prince', 'manual.docbook', '-o', 'manual.pdf';
453             }
454              
455             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
456              
457             sub generate_pdf_using_latex {
458             my ( $self ) = @_;
459              
460             my $latex = $self->as_latex;
461              
462             open my $latex_fh, '>', 'manual.tex'
463             or croak "can't write to 'manual.tex': $!";
464             print {$latex_fh} $latex;
465             close $latex_fh;
466              
467             for ( 1..2 ) { # two times to populate the toc
468             system "pdflatex -interaction=batchmode manual.tex > /dev/null";
469             # and croak "problem running pdflatex: $!";
470             }
471              
472             }
473              
474             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
475              
476             sub save_as_pdf {
477             # TODO: add -force argument
478             my $self = shift;
479              
480             my %option;
481              
482             if ( ref $_[-1] eq 'HASH' ) {
483             my @a = %{$_[-1]};
484             %option = validate( @a, { force => 0 } );
485             }
486              
487             my $filename = shift
488             or croak 'save_as_pdf: requires a filename as an argument';
489              
490             $filename =~ s/\.pdf$//
491             or croak "save_as_pdf: filename '$filename'"
492             ."must have suffix '.pdf'";
493              
494             if ( -f $filename.'.pdf' ) {
495             if ( $option{force} ) {
496             unlink $filename.'.pdf'
497             or die "can't remove file $filename.pdf: $!\n";
498             }
499             else {
500             croak "file $filename.pdf already exist";
501             }
502             }
503              
504             my $original_dir = cwd(); # let's remember where we are
505              
506             my $tmpdir = tempdir( 'podmanualXXXX', CLEANUP => 1 );
507              
508             chdir $tmpdir or die "can't switch to dir $tmpdir: $!\n";
509              
510             #if ( $filename =~ s#^(.*)/## ) {
511             # chdir $1 or croak "can't chdir to $1: $!";
512             #}
513              
514             if ( $self->get_pdf_generator eq 'latex' ) {
515             $self->generate_pdf_using_latex( $filename, $original_dir );
516             }
517             elsif( $self->get_pdf_generator eq 'prince' ) {
518             $self->generate_pdf_using_prince( $filename, $original_dir );
519             }
520              
521              
522             chdir $original_dir;
523              
524             copy( $tmpdir.'/manual.pdf' => $filename.'.pdf' ) or die $!;
525              
526             return 1;
527             }
528              
529             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
530              
531             sub create_appendix {
532             my $self = shift;
533              
534             return $appendix_of[ $$self ] if $appendix_of[ $$self ];
535              
536             my $appendix = $root_of[ $$self ]->new( 'appendix' );
537             $appendix->setAttribute( id => 'appendix-'.$self->unique_id );
538            
539             $root_of[ $$self ]->appendChild( $appendix );
540              
541             my $label = $appendix->new( 'title' );
542             $label->appendText( 'Appendix' );
543             $appendix->appendChild( $label );
544             }
545              
546             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
547              
548             sub get_appendix {
549             my ( $self, $create_if_missing ) = @_;
550              
551             return $create_if_missing ? $self->create_appendix : $appendix_of[ $$self ];
552             }
553              
554             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
555              
556             sub _add_to_appendix {
557             my ( $self, @nodes ) = @_;
558              
559             my $appendix = $self->get_appendix(1);
560              
561             $appendix->appendChild( $_ ) for @nodes;
562              
563             return $self;
564             }
565              
566             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
567              
568             sub local_prince_css {
569             my $self = shift;
570             my $tempdir = shift;
571              
572             my $css;
573              
574             if ( my $prince = $self->get_prince_css ) {
575             $css = qq{\@import '$prince';\n};
576             }
577              
578             $css .= <<'END_CSS';
579             bookinfo title {
580             font-size: 24pt;
581             text-align: center;
582             }
583              
584             tocentry { display: block; }
585             tocentry::after { content: leader(".") target-counter(attr(href), page); }
586              
587             toclevel1, toclevel2, toclevel3,
588             toclevel4, toclevel5, toclevel6 {
589             display: block;
590             position: inherit;
591             padding-left: 20px;
592             }
593              
594             bookinfo > title {
595             string-set: doctitle content();
596             }
597              
598              
599             title {
600             string-set: currentSection content();
601             }
602              
603             section > title {
604             margin-top: 5px;
605             font: 14pt;
606             }
607              
608             chapter > section > title > titleabbrev,
609             appendix > section > title > titleabbrev
610             {
611             display: block;
612             font: 10pt;
613             flow: static(currentSection);
614             }
615              
616             chapter > title > titleabbrev,
617             appendix > title > titleabbrev {
618             display: block;
619             font: 10pt;
620             text-align: left;
621             flow: static(currentChapter);
622             }
623              
624             chapter > title > titleabbrev,
625             appendix > title > titleabbrev {
626             string-set: currentChapter content();
627             }
628              
629             @page:first {
630             @top-left { content: normal }
631             @top-right { content: normal }
632             @bottom-left { content: normal }
633             @bottom-right { content: normal }
634             }
635              
636             chapter > title::before {
637             display: none;
638             }
639              
640             chapter > title,
641             appendix > title {
642             text-align: left;
643             }
644              
645              
646              
647             emphasis[role="italic"] {
648             font-style: italic;
649             }
650              
651              
652             @page {
653             @bottom-left {
654             content: string(doctitle)
655             }
656             @bottom-right {
657             content: counter(page);
658             font-style: italic
659             }
660             @top-right {
661             content: flow(currentSection);
662             }
663             @top-left {
664             content: flow(currentChapter);
665             }
666             }
667              
668             appendix {
669             page-break-before: always;
670             display: block;
671             }
672              
673             appendix > title {
674             font-size: 24pt;
675             font-weight: bold;
676             }
677             END_CSS
678              
679             open my $css_fh, '>', 'docbook.css'
680             or croak "can't open file docbook.css for writing: $!";
681             print $css_fh $css;
682              
683             return 'docbook.css';
684             }
685              
686             'end of Pod::Manual'; # Magic true value required at end of module
687              
688             __END__