File Coverage

blib/lib/EPublisher/Target/Plugin/EPub.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


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   1006 use strict;
  1         3  
  1         54  
6 1     1   10 use warnings;
  1         2  
  1         44  
7 1     1   1164 use Data::UUID;
  1         18301  
  1         200  
8 1     1   781 use EBook::EPUB;
  0            
  0            
9             use File::Basename;
10             use File::Temp qw(tempfile);
11             use File::Path qw(remove_tree);
12             use Pod::Simple::XHTML;
13              
14             use EPublisher;
15             use EPublisher::Target::Base;
16             our @ISA = qw(EPublisher::Target::Base);
17              
18             our $VERSION = 0.5;
19              
20             sub deploy {
21             my ($self) = @_;
22            
23             my $pods = $self->_config->{source} || [];
24            
25             my $author = $self->_config->{author} || 'Perl Author';
26             my $title = $self->_config->{title} || 'Pod Document';
27             my $language = $self->_config->{lang} || 'en';
28             my $out_filename = $self->_config->{output} || '';
29             my $css_filename = $self->_config->{css} || '';
30             my $cover_filename = $self->_config->{cover} || '';
31             my $encoding = $self->_config->{encoding} || ':encoding(UTF-8)';
32             my $version = 0;
33            
34             # Create EPUB object
35             my $epub = EBook::EPUB->new();
36              
37             # Set the ePub metadata.
38             $epub->add_title( $title );
39             $epub->add_author( $author );
40             $epub->add_language( $language );
41              
42             # Add user defined cover image if it supplied.
43             $self->add_cover( $epub, $cover_filename ) if $cover_filename;
44              
45             # Add the Dublin Core UUID.
46             my $du = Data::UUID->new();
47             my $uuid = $du->create_str;
48              
49             {
50              
51             # Ignore overridden UUID warning form EBook::EPUB.
52             local $SIG{__WARN__} = sub { };
53             $epub->add_identifier( "urn:uuid:$uuid" );
54             }
55              
56             # Add some other metadata to the OPF file.
57             $epub->add_meta_item( 'EPublisher version', $EPublisher::VERSION );
58             $epub->add_meta_item( 'EBook::EPUB version', $EBook::EPUB::VERSION );
59              
60              
61             # Get the user supplied or default css file name.
62             $css_filename = $self->get_css_file( $css_filename );
63              
64              
65             # Add package content: stylesheet, font, xhtml
66             $epub->copy_stylesheet( $css_filename, 'styles/style.css' );
67            
68             my $counter = 1;
69             my $image_counter = 1;
70            
71             for my $pod ( @{$pods} ) {
72             my $parser = Pod::Simple::XHTML->new;
73             $parser->index(0);
74            
75             $parser->accept_directive_as_processed( 'image' );
76              
77             # we have to decrease all headings to the layer below
78             $pod->{pod} =~ s/=[hH][eE][aA][dD]1[ ]/=head2 /g;
79             $pod->{pod} =~ s/=[hH][eE][aA][dD]2[ ]/=head3 /g;
80             $pod->{pod} =~ s/=[hH][eE][aA][dD]3[ ]/=head4 /g;
81             #TODO: need a fix for head4
82            
83             my ($in_fh_temp,$in_file_temp) = tempfile();
84             binmode $in_fh_temp, $encoding;
85             # adding a title, given from the meta-data
86             print $in_fh_temp "=head1 $pod->{title}\n\n" || '';
87             # adding the content
88             print $in_fh_temp $pod->{pod} || '';
89             close $in_fh_temp;
90            
91             my $in_fh;
92             open $in_fh, "<$encoding", $in_file_temp;
93            
94             my ($xhtml_fh, $xhtml_filename) = tempfile();
95            
96             $parser->output_fh( $xhtml_fh );
97             $parser->parse_file( $in_fh );
98              
99             close $xhtml_fh;
100             close $in_fh;
101            
102             $epub->copy_xhtml( $xhtml_filename, "text/$counter.xhtml", linear => 'no' );
103            
104             # cleaning up...
105             unlink $xhtml_filename;
106             unlink $in_file_temp;
107            
108             $self->add_to_table_of_contents( $counter, $parser->{to_index} );
109              
110             # add images
111             my @images = $parser->images_to_import();
112             for my $image ( @images ) {
113             my $path = $image->{path};
114             my $name = $image->{name};
115             my $image_id = $epub->copy_image( $path, "images/$name" );
116             $epub->add_meta_item( "image$image_counter", $image_id );
117             }
118            
119             $counter++;
120             }
121              
122             # Add Pod headings to table of contents.
123             $self->set_table_of_contents( $epub, $self->table_of_contents );
124              
125             # clean up...
126             unlink $css_filename if !$self->user_css;
127              
128             # Generate the ePub eBook.
129             my $success = $epub->pack_zip( $out_filename );
130             if ( !$success ) {
131             $self->publisher->debug( "402: can't create epub" );
132             return '';
133             }
134              
135             # delete tmp dir created by EBook::EPUB
136             my $epub_tmp = $epub->tmpdir;
137             remove_tree $epub_tmp if $epub_tmp and -d $epub_tmp;
138            
139             return $out_filename;
140             }
141              
142             sub add_to_table_of_contents {
143             my ($self,$page, $arrayref) = @_;
144            
145             push @{ $self->{__toc} }, +{ page => $page, headings => $arrayref };
146             return 1;
147             }
148              
149             sub table_of_contents {
150             my ($self) = @_;
151            
152             return $self->{__toc};
153             }
154              
155             sub _html_header {
156             return
157             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             my ($self,$epub,$pod_headings) = @_;
176              
177             my $play_order = 1;
178             my $max_heading_level = $self->_config->{max_heading_level} || 2;
179             my @navpoints = ($epub) x ($max_heading_level + 1);
180            
181             for my $content_part ( @{$pod_headings} ) {
182            
183             my $headings = $content_part->{headings};
184             my $page = $content_part->{page};
185              
186             for my $heading ( @{$headings} ) {
187              
188             my $heading_level = $heading->[0];
189             my $section = $heading->[1];
190             my $label = $heading->[2];
191             my $content = "text/$page.xhtml";
192              
193             # Only deal with head1 and head2 headings.
194             next if $heading_level > $max_heading_level;
195              
196             # Add the pod section to the NCX data, Except for the root heading.
197             $content .= '#' . $section;# if $play_order > 1;
198              
199             my %options = (
200             content => $content,
201             id => 'navPoint-' . $play_order,
202             play_order => $play_order,
203             label => $label,
204             );
205              
206             $play_order++;
207              
208             # Add the navpoints at the correct nested level.
209             my $navpoint_obj = $navpoints[ $heading_level - 1 ];
210             $navpoint_obj = $navpoint_obj->add_navpoint( %options );
211            
212             $navpoints[ $heading_level ] = $navpoint_obj;
213             }
214             }
215             }
216              
217             sub get_css_file {
218             my ($self,$css_filename) = @_;
219            
220             my $css_fh;
221              
222             # If the user supplied the css filename check if it exists.
223             if ( $css_filename ) {
224             if ( -e $css_filename ) {
225             $self->user_css(1);
226             return $css_filename;
227             }
228             else {
229             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             ( $css_fh, $css_filename ) = tempfile();
235              
236             print $css_fh "h1 { font-size: 110%; }\n";
237             print $css_fh "h2, h3, h4 { font-size: 100%; }\n";
238             print $css_fh ".code { font-family: Courier; }\n";
239              
240             close $css_fh;
241              
242             return $css_filename;
243             }
244              
245             sub user_css {
246             my ($self,$value) = @_;
247            
248             return $self->{__user_css} if @_ != 2;
249             $self->{__user_css} = $value;
250             }
251              
252             sub add_cover {
253             my ($self,$epub,$cover_filename) = @_;
254              
255             # Check if the cover image exists.
256             if ( !-e $cover_filename ) {
257             warn "Cover image $cover_filename not found.\n";
258             return undef;
259             }
260            
261             my $cover_basename = basename $cover_filename;
262              
263             # Add cover metadata for iBooks.
264             my $cover_id = $epub->copy_image( $cover_filename, "images/$cover_basename" );
265             $epub->add_meta_item( 'cover', $cover_id );
266              
267             # Add an additional cover page for other eBook readers.
268             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             my ( $tmp_fh, $tmp_filename ) = tempfile();
287              
288             print $tmp_fh $cover_xhtml;
289             close $tmp_fh;
290              
291             # Add the cover page to the ePub doc.
292             $epub->copy_xhtml( $tmp_filename, 'text/cover.xhtml', linear => 'no' );
293              
294             # Add the cover to the OPF guide.
295             my $guide_options = {
296             type => 'cover',
297             href => 'text/cover.xhtml',
298             title => 'Cover',
299             };
300              
301             $epub->guide->add_reference( $guide_options );
302              
303             # Cleanup the temp file.
304             unlink $cover_xhtml;
305              
306             return $cover_id;
307             }
308              
309             ## -------------------------------------------------------------------------- ##
310             ## Change behavour of Pod::Simple::XHTML
311             ## -------------------------------------------------------------------------- ##
312              
313             {
314             no warnings 'redefine';
315            
316             sub Pod::Simple::XHTML::idify {
317             my ($self, $t, $not_unique) = @_;
318             for ($t) {
319             s/<[^>]+>//g; # Strip HTML.
320             s/&[^;]+;//g; # Strip entities.
321             s/^([^a-zA-Z]+)$/pod$1/; # Prepend "pod" if no valid chars.
322             s/^[^a-zA-Z]+//; # First char must be a letter.
323             s/[^-a-zA-Z0-9_]+/-/g; # All other chars must be valid.
324             }
325             return $t if $not_unique;
326             my $i = '';
327             $i++ while $self->{ids}{"$t$i"}++;
328             return "$t$i";
329             }
330            
331             sub Pod::Simple::XHTML::start_Verbatim {}
332            
333             sub Pod::Simple::XHTML::end_Verbatim {
334             my ($self) = @_;
335            
336             $self->{scratch} =~ s{ }{ &nbsp;}g;
337             $self->{scratch} =~ s{\n}{<br />}g;
338             #$self->{scratch} = '<div class="code">' . $self->{scratch} . '</div>';
339             $self->{scratch} = '<p><code class="code">' . $self->{scratch} . '</code></p>';
340            
341             $self->emit;
342             }
343            
344             sub Pod::Simple::XHTML::images_to_import {
345             my ($self) = @_;
346            
347             return @{ $self->{images_to_import} || [] };
348             };
349            
350             sub Pod::Simple::XHTML::end_image {
351             my ($self) = @_;
352            
353             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             my $text = $self->{scratch};
359             my $regex = $text =~ /^\s*"/ ? $regexe{path_quoted} : $regexe{path_plain};
360            
361             my ($path,$alt) = $text =~ $regex;
362             $alt = '' if !defined $alt;
363            
364             return if !$path;
365            
366             if ( !-e $path ) {
367             warn "Image $path does not exist!";
368             return;
369             }
370            
371             my $filename = basename $path;
372            
373             # save complete path in $self->{images_to_import}
374             push @{$self->{images_to_import}}, { path => $path, name => $filename };
375            
376             $self->{scratch} = qq~<p><img src="../images/$filename" alt="$alt" /></p>~;
377            
378             $self->emit;
379             };
380              
381             *Pod::Simple::XHTML::start_L = sub {
382              
383             # The main code is taken from Pod::Simple::XHTML.
384             my ( $self, $flags ) = @_;
385             my ( $type, $to, $section ) = @{$flags}{ 'type', 'to', 'section' };
386             my $url =
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             if ( defined $url ) {
394             $url = $self->encode_entities( $url );
395             }
396              
397             # If it's an unknown type, use an attribute-less <a> like HTML.pm.
398             $self->{'scratch'} .= '<a' . ( $url ? ' href="' . $url . '">' : '>' );
399             };
400            
401             *Pod::Simple::XHTML::start_Document = sub {
402             my ($self) = @_;
403              
404             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             $self->{'scratch'} .= $xhtml_headers;
421             $self->emit('nowrap');
422             }
423             }
424              
425             1;
426              
427              
428              
429             =pod
430              
431             =head1 NAME
432              
433             EPublisher::Target::Plugin::EPub - Use EPub as a target for EPublisher
434              
435             =head1 VERSION
436              
437             version 0.5
438              
439             =head1 SYNOPSIS
440              
441             use EPublisher::Target;
442             my $EPub = EPublisher::Target->new( { type => 'EPub' } );
443             $EPub->deploy;
444              
445             =encoding utf8
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 <module@renee-baecker.de>, Boris Däppen <boris_daeppen@bluewin.ch>
497              
498             =head1 COPYRIGHT AND LICENSE
499              
500             This software is Copyright (c) 2012 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
507              
508              
509             __END__
510