File Coverage

blib/lib/EPublisher/Target/Plugin/EPub.pm
Criterion Covered Total %
statement 33 206 16.0
branch 0 40 0.0
condition 0 25 0.0
subroutine 11 27 40.7
pod 8 12 66.6
total 52 310 16.7


line stmt bran cond sub pod time code
1             package EPublisher::Target::Plugin::EPub;
2              
3             # ABSTRACT: Use EPub as a target for EPublisher
4              
5 1     1   562 use strict;
  1         1  
  1         33  
6 1     1   4 use warnings;
  1         1  
  1         26  
7 1     1   428 use Data::UUID;
  1         534  
  1         54  
8 1     1   460 use EBook::EPUB;
  1         863163  
  1         35  
9 1     1   9 use File::Basename;
  1         1  
  1         73  
10 1     1   4 use File::Temp qw(tempfile);
  1         2  
  1         38  
11 1     1   4 use File::Path qw(remove_tree);
  1         2  
  1         41  
12 1     1   608 use Pod::Simple::XHTML;
  1         30625  
  1         33  
13              
14 1     1   468 use EPublisher;
  1         7452  
  1         27  
15 1     1   410 use EPublisher::Target::Base;
  1         200  
  1         1260  
16             our @ISA = qw(EPublisher::Target::Base);
17              
18             our $VERSION = 0.6;
19              
20             sub deploy {
21 0     0 1   my ($self) = @_;
22            
23 0   0       my $pods = $self->_config->{source} || [];
24            
25 0   0       my $author = $self->_config->{author} || 'Perl Author';
26 0   0       my $title = $self->_config->{title} || 'Pod Document';
27 0   0       my $language = $self->_config->{lang} || 'en';
28 0   0       my $out_filename = $self->_config->{output} || '';
29 0   0       my $css_filename = $self->_config->{css} || '';
30 0   0       my $cover_filename = $self->_config->{cover} || '';
31 0   0       my $encoding = $self->_config->{encoding} || ':encoding(UTF-8)';
32 0           my $version = 0;
33            
34             # Create EPUB object
35 0           my $epub = EBook::EPUB->new();
36              
37             # Set the ePub metadata.
38 0           $epub->add_title( $title );
39 0           $epub->add_author( $author );
40 0           $epub->add_language( $language );
41              
42             # Add user defined cover image if it supplied.
43 0 0         $self->add_cover( $epub, $cover_filename ) if $cover_filename;
44              
45             # Add the Dublin Core UUID.
46 0           my $du = Data::UUID->new();
47 0           my $uuid = $du->create_str;
48              
49             {
50              
51             # Ignore overridden UUID warning form EBook::EPUB.
52 0     0     local $SIG{__WARN__} = sub { };
  0            
  0            
53 0           $epub->add_identifier( "urn:uuid:$uuid" );
54             }
55              
56             # Add some other metadata to the OPF file.
57 0           $epub->add_meta_item( 'EPublisher version', $EPublisher::VERSION );
58 0           $epub->add_meta_item( 'EBook::EPUB version', $EBook::EPUB::VERSION );
59              
60              
61             # Get the user supplied or default css file name.
62 0           $css_filename = $self->get_css_file( $css_filename );
63              
64              
65             # Add package content: stylesheet, font, xhtml
66 0           $epub->copy_stylesheet( $css_filename, 'styles/style.css' );
67            
68 0           my $counter = 1;
69 0           my $image_counter = 1;
70            
71 0           for my $pod ( @{$pods} ) {
  0            
72 0           my $parser = Pod::Simple::XHTML->new;
73 0           $parser->index(0);
74            
75 0           $parser->accept_directive_as_processed( 'image' );
76              
77             # we have to decrease all headings to the layer below
78 0           $pod->{pod} =~ s/=[hH][eE][aA][dD]1[ ]/=head2 /g;
79 0           $pod->{pod} =~ s/=[hH][eE][aA][dD]2[ ]/=head3 /g;
80 0           $pod->{pod} =~ s/=[hH][eE][aA][dD]3[ ]/=head4 /g;
81             #TODO: need a fix for head4
82            
83 0           my ($in_fh_temp,$in_file_temp) = tempfile();
84 0           binmode $in_fh_temp, $encoding;
85             # adding a title, given from the meta-data
86 0   0       print $in_fh_temp "=head1 $pod->{title}\n\n" || '';
87             # adding the content
88 0   0       print $in_fh_temp $pod->{pod} || '';
89 0           close $in_fh_temp;
90            
91 0           my $in_fh;
92 0           open $in_fh, "<$encoding", $in_file_temp;
93            
94 0           my ($xhtml_fh, $xhtml_filename) = tempfile();
95            
96 0           $parser->output_fh( $xhtml_fh );
97 0           $parser->parse_file( $in_fh );
98              
99 0           close $xhtml_fh;
100 0           close $in_fh;
101            
102 0           $epub->copy_xhtml( $xhtml_filename, "text/$counter.xhtml", linear => 'no' );
103            
104             # cleaning up...
105 0           unlink $xhtml_filename;
106 0           unlink $in_file_temp;
107            
108 0           $self->add_to_table_of_contents( $counter, $parser->{to_index} );
109              
110             # add images
111 0           my @images = $parser->images_to_import();
112 0           for my $image ( @images ) {
113 0           my $path = $image->{path};
114 0           my $name = $image->{name};
115 0           my $image_id = $epub->copy_image( $path, "images/$name" );
116 0           $epub->add_meta_item( "image$image_counter", $image_id );
117             }
118            
119 0           $counter++;
120             }
121              
122             # Add Pod headings to table of contents.
123 0           $self->set_table_of_contents( $epub, $self->table_of_contents );
124              
125             # clean up...
126 0 0         unlink $css_filename if !$self->user_css;
127              
128             # Generate the ePub eBook.
129 0           my $success = $epub->pack_zip( $out_filename );
130 0 0         if ( !$success ) {
131 0           $self->publisher->debug( "402: can't create epub" );
132 0           return '';
133             }
134              
135             # delete tmp dir created by EBook::EPUB
136 0           my $epub_tmp = $epub->tmpdir;
137 0 0 0       remove_tree $epub_tmp if $epub_tmp and -d $epub_tmp;
138            
139 0           return $out_filename;
140             }
141              
142             sub add_to_table_of_contents {
143 0     0 1   my ($self,$page, $arrayref) = @_;
144            
145 0           push @{ $self->{__toc} }, +{ page => $page, headings => $arrayref };
  0            
146 0           return 1;
147             }
148              
149             sub table_of_contents {
150 0     0 1   my ($self) = @_;
151            
152 0           return $self->{__toc};
153             }
154              
155             sub _html_header {
156             return
157 0     0     qq{<?xml version="1.0" encoding="UTF-8"?>\n}
158             . qq{<!DOCTYPE html\n}
159             . qq{ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n}
160             . qq{ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n}
161             . qq{\n}
162             . qq{<html xmlns="http://www.w3.org/1999/xhtml">\n}
163             . qq{<head>\n}
164             . qq{<title></title>\n}
165             . qq{<meta http-equiv="Content-Type" }
166             . qq{content="text/html; charset=iso-8859-1"/>\n}
167             . qq{<link rel="stylesheet" href="../styles/style.css" }
168             . qq{type="text/css"/>\n}
169             . qq{</head>\n}
170             . qq{\n}
171             . qq{<body>\n};
172             }
173              
174             sub set_table_of_contents {
175 0     0 1   my ($self,$epub,$pod_headings) = @_;
176              
177 0           my $play_order = 1;
178 0   0       my $max_heading_level = $self->_config->{max_heading_level} || 2;
179 0           my @navpoints = ($epub) x ($max_heading_level + 1);
180            
181 0           for my $content_part ( @{$pod_headings} ) {
  0            
182            
183 0           my $headings = $content_part->{headings};
184 0           my $page = $content_part->{page};
185              
186 0           for my $heading ( @{$headings} ) {
  0            
187              
188 0           my $heading_level = $heading->[0];
189 0           my $section = $heading->[1];
190 0           my $label = $heading->[2];
191 0           my $content = "text/$page.xhtml";
192              
193             # Only deal with head1 and head2 headings.
194 0 0         next if $heading_level > $max_heading_level;
195              
196             # Add the pod section to the NCX data, Except for the root heading.
197 0           $content .= '#' . $section;# if $play_order > 1;
198              
199 0           my %options = (
200             content => $content,
201             id => 'navPoint-' . $play_order,
202             play_order => $play_order,
203             label => $label,
204             );
205              
206 0           $play_order++;
207              
208             # Add the navpoints at the correct nested level.
209 0           my $navpoint_obj = $navpoints[ $heading_level - 1 ];
210 0           $navpoint_obj = $navpoint_obj->add_navpoint( %options );
211            
212 0           $navpoints[ $heading_level ] = $navpoint_obj;
213             }
214             }
215             }
216              
217             sub get_css_file {
218 0     0 1   my ($self,$css_filename) = @_;
219            
220 0           my $css_fh;
221              
222             # If the user supplied the css filename check if it exists.
223 0 0         if ( $css_filename ) {
224 0 0         if ( -e $css_filename ) {
225 0           $self->user_css(1);
226 0           return $css_filename;
227             }
228             else {
229 0           warn "CSS file $css_filename not found.\n";
230             }
231             }
232              
233             # If the css file doesn't exist or wasted supplied create a default.
234 0           ( $css_fh, $css_filename ) = tempfile();
235              
236 0           print $css_fh "h1 { font-size: 110%; }\n";
237 0           print $css_fh "h2, h3, h4 { font-size: 100%; }\n";
238 0           print $css_fh ".code { font-family: Courier; }\n";
239              
240 0           close $css_fh;
241              
242 0           return $css_filename;
243             }
244              
245             sub user_css {
246 0     0 1   my ($self,$value) = @_;
247            
248 0 0         return $self->{__user_css} if @_ != 2;
249 0           $self->{__user_css} = $value;
250             }
251              
252             sub add_cover {
253 0     0 1   my ($self,$epub,$cover_filename) = @_;
254              
255             # Check if the cover image exists.
256 0 0         if ( !-e $cover_filename ) {
257 0           warn "Cover image $cover_filename not found.\n";
258 0           return undef;
259             }
260            
261 0           my $cover_basename = basename $cover_filename;
262              
263             # Add cover metadata for iBooks.
264 0           my $cover_id = $epub->copy_image( $cover_filename, "images/$cover_basename" );
265 0           $epub->add_meta_item( 'cover', $cover_id );
266              
267             # Add an additional cover page for other eBook readers.
268 0           my $cover_xhtml =
269             qq[<?xml version="1.0" encoding="UTF-8"?>\n]
270             . qq[<!DOCTYPE html\n]
271             . qq[ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n]
272             . qq[ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n\n]
273             . qq[<html xmlns="http://www.w3.org/1999/xhtml">\n]
274             . qq[<head>\n]
275             . qq[<title></title>\n]
276             . qq[<meta http-equiv="Content-Type" ]
277             . qq[content="text/html; charset=iso-8859-1"/>\n]
278             . qq[<style type="text/css"> img { max-width: 100%; }</style>\n]
279             . qq[</head>\n]
280             . qq[<body>\n]
281             . qq[ <p><img alt="" src="../images/$cover_basename" /></p>\n]
282             . qq[</body>\n]
283             . qq[</html>\n\n];
284              
285             # Crete a temp file for the cover xhtml.
286 0           my ( $tmp_fh, $tmp_filename ) = tempfile();
287              
288 0           print $tmp_fh $cover_xhtml;
289 0           close $tmp_fh;
290              
291             # Add the cover page to the ePub doc.
292 0           $epub->copy_xhtml( $tmp_filename, 'text/cover.xhtml', linear => 'no' );
293              
294             # Add the cover to the OPF guide.
295 0           my $guide_options = {
296             type => 'cover',
297             href => 'text/cover.xhtml',
298             title => 'Cover',
299             };
300              
301 0           $epub->guide->add_reference( $guide_options );
302              
303             # Cleanup the temp file.
304 0           unlink $cover_xhtml;
305              
306 0           return $cover_id;
307             }
308              
309             ## -------------------------------------------------------------------------- ##
310             ## Change behavour of Pod::Simple::XHTML
311             ## -------------------------------------------------------------------------- ##
312              
313             {
314 1     1   8 no warnings 'redefine';
  1         1  
  1         756  
315            
316             sub Pod::Simple::XHTML::idify {
317 0     0 1   my ($self, $t, $not_unique) = @_;
318 0           for ($t) {
319 0           s/<[^>]+>//g; # Strip HTML.
320 0           s/&[^;]+;//g; # Strip entities.
321 0           s/^([^a-zA-Z]+)$/pod$1/; # Prepend "pod" if no valid chars.
322 0           s/^[^a-zA-Z]+//; # First char must be a letter.
323 0           s/[^-a-zA-Z0-9_]+/-/g; # All other chars must be valid.
324             }
325 0 0         return $t if $not_unique;
326 0           my $i = '';
327 0           $i++ while $self->{ids}{"$t$i"}++;
328 0           return "$t$i";
329             }
330            
331 0     0 0   sub Pod::Simple::XHTML::start_Verbatim {}
332            
333             sub Pod::Simple::XHTML::end_Verbatim {
334 0     0 0   my ($self) = @_;
335            
336 0           $self->{scratch} =~ s{ }{ &nbsp;}g;
337 0           $self->{scratch} =~ s{\n}{<br />}g;
338             #$self->{scratch} = '<div class="code">' . $self->{scratch} . '</div>';
339 0           $self->{scratch} = '<p><code class="code">' . $self->{scratch} . '</code></p>';
340            
341 0           $self->emit;
342             }
343            
344             sub Pod::Simple::XHTML::images_to_import {
345 0     0 0   my ($self) = @_;
346            
347 0 0         return @{ $self->{images_to_import} || [] };
  0            
348             };
349            
350             sub Pod::Simple::XHTML::end_image {
351 0     0 0   my ($self) = @_;
352            
353 0           my %regexe = (
354             path_quoted => qr/"([^"]+)"(?:\s+(.*))?/s, # =image "C:\path with\whitespace.png" alt text
355             path_plain => qr/([^\s]+)(?:\s+(.*))?/s, # =image C:\path\img.png alt text
356             );
357            
358 0           my $text = $self->{scratch};
359 0 0         my $regex = $text =~ /^\s*"/ ? $regexe{path_quoted} : $regexe{path_plain};
360            
361 0           my ($path,$alt) = $text =~ $regex;
362 0 0         $alt = '' if !defined $alt;
363            
364 0 0         return if !$path;
365            
366 0 0         if ( !-e $path ) {
367 0           warn "Image $path does not exist!";
368 0           return;
369             }
370            
371 0           my $filename = basename $path;
372            
373             # save complete path in $self->{images_to_import}
374 0           push @{$self->{images_to_import}}, { path => $path, name => $filename };
  0            
375            
376 0           $self->{scratch} = qq~<p><img src="../images/$filename" alt="$alt" /></p>~;
377            
378 0           $self->emit;
379             };
380              
381             *Pod::Simple::XHTML::start_L = sub {
382              
383             # The main code is taken from Pod::Simple::XHTML.
384 0     0     my ( $self, $flags ) = @_;
385 0           my ( $type, $to, $section ) = @{$flags}{ 'type', 'to', 'section' };
  0            
386 0 0         my $url =
    0          
    0          
387             $type eq 'url' ? $to
388             : $type eq 'pod' ? $self->resolve_pod_page_link( $to, $section )
389             : $type eq 'man' ? $self->resolve_man_page_link( $to, $section )
390             : undef;
391              
392             # This is the new/overridden section.
393 0 0         if ( defined $url ) {
394 0           $url = $self->encode_entities( $url );
395             }
396              
397             # If it's an unknown type, use an attribute-less <a> like HTML.pm.
398 0 0         $self->{'scratch'} .= '<a' . ( $url ? ' href="' . $url . '">' : '>' );
399             };
400            
401             *Pod::Simple::XHTML::start_Document = sub {
402 0     0     my ($self) = @_;
403              
404 0           my $xhtml_headers =
405             qq{<?xml version="1.0" encoding="UTF-8"?>\n}
406             . qq{<!DOCTYPE html\n}
407             . qq{ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n}
408             . qq{ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n} . qq{\n}
409             . qq{<html xmlns="http://www.w3.org/1999/xhtml">\n}
410             . qq{<head>\n}
411             . qq{<title></title>\n}
412             . qq{<meta http-equiv="Content-Type" }
413             . qq{content="text/html; charset=utf-8"/>\n}
414             . qq{<link rel="stylesheet" href="../styles/style.css" }
415             . qq{type="text/css"/>\n}
416             . qq{</head>\n} . qq{\n}
417             . qq{<body>\n};
418              
419              
420 0           $self->{'scratch'} .= $xhtml_headers;
421 0           $self->emit('nowrap');
422             }
423             }
424              
425             1;
426              
427             __END__
428              
429             =pod
430              
431             =encoding UTF-8
432              
433             =head1 NAME
434              
435             EPublisher::Target::Plugin::EPub - Use EPub as a target for EPublisher
436              
437             =head1 VERSION
438              
439             version 0.6
440              
441             =head1 SYNOPSIS
442              
443             use EPublisher::Target;
444             my $EPub = EPublisher::Target->new( { type => 'EPub' } );
445             $EPub->deploy;
446              
447             =head1 METHODS
448              
449             =head2 deploy
450              
451             creates the output.
452              
453             $EPub->deploy;
454              
455             =head1 YAML SPEC
456              
457             EPubTest:
458             source:
459             #...
460             target:
461             type: EPub
462             author: reneeb
463             output: /path/to/test.epub
464             title: The Books Title
465             cover: /path/to/an/image/for/the/cover.jpg
466             encoding: utf-8
467              
468             =head1 TODO
469              
470             =head2 document methods
471              
472             =over
473              
474             =item add_to_table_of_contents
475              
476             =item table_of_contents
477              
478             =item _html_header
479              
480             =item set_table_of_contents
481              
482             =item get_css_file
483              
484             =item user_css
485              
486             =item add_cover
487              
488             =back
489              
490             =head2 write more tests
491              
492             Untile now the test just cover the basics. Tests of output should be added.
493              
494             =head1 AUTHOR
495              
496             Renee Bäcker <reneeb@cpan.org>, Boris Däppen <boris_daeppen@bluewin.ch>
497              
498             =head1 COPYRIGHT AND LICENSE
499              
500             This software is Copyright (c) 2015 by Renee Bäcker, Boris Däppen.
501              
502             This is free software, licensed under:
503              
504             The Artistic License 2.0 (GPL Compatible)
505              
506             =cut