| blib/lib/Aozora2Epub.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 113 | 169 | 66.8 |
| branch | 29 | 56 | 51.7 |
| condition | 5 | 17 | 29.4 |
| subroutine | 22 | 29 | 75.8 |
| pod | 4 | 7 | 57.1 |
| total | 173 | 278 | 62.2 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package Aozora2Epub; | ||||||
| 2 | 4 | 4 | 815549 | use utf8; | |||
| 4 | 375 | ||||||
| 4 | 30 | ||||||
| 3 | 4 | 4 | 162 | use strict; | |||
| 4 | 8 | ||||||
| 4 | 89 | ||||||
| 4 | 4 | 4 | 19 | use warnings; | |||
| 4 | 8 | ||||||
| 4 | 187 | ||||||
| 5 | 4 | 4 | 1839 | use Aozora2Epub::Gensym; | |||
| 4 | 11 | ||||||
| 4 | 334 | ||||||
| 6 | 4 | 4 | 1791 | use Aozora2Epub::CachedGet qw/http_get/; | |||
| 4 | 19 | ||||||
| 4 | 363 | ||||||
| 7 | 4 | 4 | 2022 | use Aozora2Epub::Epub; | |||
| 4 | 22 | ||||||
| 4 | 29 | ||||||
| 8 | 4 | 4 | 2543 | use Aozora2Epub::XHTML; | |||
| 4 | 35 | ||||||
| 4 | 33 | ||||||
| 9 | 4 | 4 | 209 | use Path::Tiny; | |||
| 4 | 8 | ||||||
| 4 | 294 | ||||||
| 10 | 4 | 4 | 2781 | use URI; | |||
| 4 | 24557 | ||||||
| 4 | 192 | ||||||
| 11 | 4 | 4 | 1745 | use HTML::Escape qw/escape_html/; | |||
| 4 | 3302 | ||||||
| 4 | 293 | ||||||
| 12 | |||||||
| 13 | 4 | 4 | 29 | use base qw(Class::Accessor); | |||
| 4 | 7 | ||||||
| 4 | 1708 | ||||||
| 14 | __PACKAGE__->mk_accessors(qw/files title author epub bib_info notation_notes/); | ||||||
| 15 | |||||||
| 16 | our $VERSION = '0.05'; | ||||||
| 17 | |||||||
| 18 | our $AOZORA_GAIJI_URL = 'https://www.aozora.gr.jp/gaiji/'; | ||||||
| 19 | our $AOZORA_CARDS_URL = 'https://www.aozora.gr.jp/cards'; | ||||||
| 20 | |||||||
| 21 | sub _base_url { | ||||||
| 22 | 0 | 0 | 0 | my $base = shift; | |||
| 23 | 0 | 0 | $base =~ s{[^/]+\.html$}{}s; | ||||
| 24 | 0 | 0 | return $base; | ||||
| 25 | } | ||||||
| 26 | |||||||
| 27 | sub _get_file { | ||||||
| 28 | 0 | 0 | 0 | my $url_or_path = "" . shift; # force to string. | |||
| 29 | |||||||
| 30 | 0 | 0 | 0 | if ($url_or_path =~ m{^https?://}) { | |||
| 31 | 0 | 0 | return http_get($url_or_path); | ||||
| 32 | } | ||||||
| 33 | 0 | 0 | 0 | if ($url_or_path =~ m{\.html$}) { | |||
| 34 | 0 | 0 | return path($url_or_path)->slurp_utf8; | ||||
| 35 | } | ||||||
| 36 | 0 | 0 | return path($url_or_path)->slurp_raw; | ||||
| 37 | } | ||||||
| 38 | |||||||
| 39 | sub _get_content { | ||||||
| 40 | 37 | 37 | 81 | my $xhtml = shift; | |||
| 41 | 37 | 50 | 165 | if ($xhtml =~ m{/card\d+\.html$}) { # 図書カード | |||
| 42 | 0 | 0 | 0 | unless ($xhtml =~ m{^https?://}) { # $xhtml shuld be \d+/card\d+.html | |||
| 43 | 0 | 0 | $xhtml = "$AOZORA_CARDS_URL/$xhtml"; | ||||
| 44 | } | ||||||
| 45 | 0 | 0 | my $text = _get_file($xhtml); | ||||
| 46 | 0 | 0 | my $tree = Aozora2Epub::XHTML::Tree->new($text); | ||||
| 47 | 0 | 0 | my $xhtml_url; | ||||
| 48 | $tree->process('//a[text()="いますぐXHTML版で読む"]' => sub { | ||||||
| 49 | 0 | 0 | 0 | $xhtml_url = shift->attr('href'); | |||
| 50 | 0 | 0 | }); | ||||
| 51 | 0 | 0 | my $xhtml_uri = URI->new($xhtml_url)->abs(URI->new($xhtml)); | ||||
| 52 | 0 | 0 | return _get_content($xhtml_uri->as_string); | ||||
| 53 | } | ||||||
| 54 | 37 | 50 | 226 | if ($xhtml =~ m{/files/\d+_\d+\.html$}) { # XHTML | |||
| 55 | 0 | 0 | 0 | unless ($xhtml =~ m{^https?://}) { # $xhtml shuld be \d+/files/xxx_xxx.html | |||
| 56 | 0 | 0 | $xhtml = "$AOZORA_CARDS_URL/$xhtml"; | ||||
| 57 | } | ||||||
| 58 | 0 | 0 | my $text = _get_file($xhtml); | ||||
| 59 | 0 | 0 | return ($text, _base_url($xhtml)); | ||||
| 60 | } | ||||||
| 61 | # XHTML string | ||||||
| 62 | 37 | 183 | return (qq{ $xhtml }, undef); |
||||
| 63 | } | ||||||
| 64 | |||||||
| 65 | sub new { | ||||||
| 66 | 37 | 37 | 1 | 874 | my ($class, $content, %options) = @_; | ||
| 67 | 37 | 371 | my $self = bless { | ||||
| 68 | files => [], | ||||||
| 69 | epub => Aozora2Epub::Epub->new, | ||||||
| 70 | title => undef, | ||||||
| 71 | author => undef, | ||||||
| 72 | bib_info => '', | ||||||
| 73 | notation_notes => '', | ||||||
| 74 | }, $class; | ||||||
| 75 | 37 | 50 | 286 | $self->append($content, %options, title=>'') if $content; | |||
| 76 | 37 | 1197 | return $self; | ||||
| 77 | } | ||||||
| 78 | |||||||
| 79 | sub _cat_url { | ||||||
| 80 | 0 | 0 | 0 | my ($base, $path) = @_; | |||
| 81 | 0 | 0 | 0 | unless ($base =~ m{^https?://}) { | |||
| 82 | 0 | 0 | return path($base, $path); | ||||
| 83 | } | ||||||
| 84 | 0 | 0 | return URI->new($path)->abs(URI->new($base)); | ||||
| 85 | } | ||||||
| 86 | |||||||
| 87 | sub _build_elemlist_from_xhtml { | ||||||
| 88 | 0 | 0 | 0 | my $xhtml = shift; | |||
| 89 | 0 | 0 | my $tr = Aozora2Epub::XHTML->new_from_string(qq{ $xhtml });; |
||||
| 90 | 0 | 0 | return @{$tr->contents}; | ||||
| 0 | 0 | ||||||
| 91 | } | ||||||
| 92 | |||||||
| 93 | sub append { | ||||||
| 94 | 37 | 37 | 1 | 249 | my ($self, $xhtml_like, %options) = @_; | ||
| 95 | |||||||
| 96 | 37 | 142 | my ($xhtml, $base_url) = _get_content($xhtml_like); | ||||
| 97 | 37 | 390 | my $doc = Aozora2Epub::XHTML->new_from_string($xhtml); | ||||
| 98 | |||||||
| 99 | 37 | 100 | 163 | unless ($options{no_fetch_assets}) { | |||
| 100 | 4 | 5 | for my $path (@{$doc->gaiji}) { | ||||
| 4 | 8 | ||||||
| 101 | 0 | 0 | my $png = _get_file(_cat_url($AOZORA_GAIJI_URL, $path)); | ||||
| 102 | 0 | 0 | $self->epub->add_gaiji($png, $path); | ||||
| 103 | } | ||||||
| 104 | 4 | 67 | for my $path (@{$doc->fig}) { | ||||
| 4 | 7 | ||||||
| 105 | 0 | 0 | my $png = _get_file(_cat_url($base_url, $path)); | ||||
| 106 | 0 | 0 | $self->epub->add_image($png, $path); | ||||
| 107 | } | ||||||
| 108 | } | ||||||
| 109 | 37 | 250 | my @files = $doc->split; | ||||
| 110 | 37 | 89 | my $part_title; | ||||
| 111 | 37 | 50 | 167 | if (defined $options{title_html}) { | |||
| 112 | 0 | 0 | $files[0]->insert_content(_build_elemlist_from_xhtml($options{title_html})); | ||||
| 113 | } else { | ||||||
| 114 | 37 | 50 | 210 | unless (defined $options{title}) { | |||
| 50 | |||||||
| 115 | 0 | 0 | 0 | if ($options{use_subtitle}) { | |||
| 116 | 0 | 0 | $part_title = $doc->subtitle; | ||||
| 117 | } | ||||||
| 118 | 0 | 0 | 0 | $part_title ||= $doc->title; | |||
| 119 | 0 | 0 | } elsif ($options{title} eq '') { | ||||
| 120 | 37 | 75 | $part_title = undef; | ||||
| 121 | } else { | ||||||
| 122 | 0 | 0 | $part_title = $options{title}; | ||||
| 123 | } | ||||||
| 124 | 37 | 50 | 33 | 197 | if ($files[0] && $part_title) { | ||
| 125 | 0 | 0 | 0 | my $title_level = $options{title_level} || 2; | |||
| 126 | 0 | 0 | my $tag = "h$title_level"; | ||||
| 127 | 0 | 0 | my $header_elem = HTML::Element->new_from_lol([ $tag, { id => gensym }, | ||||
| 128 | $part_title ]); | ||||||
| 129 | 0 | 0 | $files[0]->insert_content($header_elem); | ||||
| 130 | } | ||||||
| 131 | } | ||||||
| 132 | 37 | 68 | push @{$self->files}, @files; | ||||
| 37 | 169 | ||||||
| 133 | 37 | 50 | 651 | $self->title or $self->title($doc->title); | |||
| 134 | 37 | 50 | 1146 | $self->author or $self->author($doc->author); | |||
| 135 | 37 | 1092 | $self->add_bib_info($part_title, $doc->bib_info); | ||||
| 136 | 37 | 910 | $self->add_notation_notes($part_title, $doc->notation_notes); | ||||
| 137 | } | ||||||
| 138 | |||||||
| 139 | sub add_bib_info { | ||||||
| 140 | 37 | 37 | 0 | 510 | my ($self, $part_title, $bib_info) = @_; | ||
| 141 | |||||||
| 142 | 37 | 50 | 168 | $self->bib_info(join('', | |||
| 143 | $self->bib_info, | ||||||
| 144 | " ", |
||||||
| 145 | ($part_title | ||||||
| 146 | ? (q{}, escape_html($part_title), "") |
||||||
| 147 | : ()), | ||||||
| 148 | $bib_info)); | ||||||
| 149 | } | ||||||
| 150 | |||||||
| 151 | sub add_notation_notes { | ||||||
| 152 | 37 | 37 | 0 | 457 | my ($self, $part_title, $notes) = @_; | ||
| 153 | |||||||
| 154 | 37 | 50 | 124 | $self->notation_notes(join('', | |||
| 155 | $self->notation_notes, | ||||||
| 156 | " ", |
||||||
| 157 | ($part_title | ||||||
| 158 | ? (q{}, escape_html($part_title), "") |
||||||
| 159 | : ()), | ||||||
| 160 | $notes)); | ||||||
| 161 | } | ||||||
| 162 | |||||||
| 163 | sub _make_content_iterator { | ||||||
| 164 | 4 | 4 | 6 | my $files = shift; | |||
| 165 | |||||||
| 166 | 4 | 9 | my @files = @$files; | ||||
| 167 | 4 | 5 | my $file = shift @files; | ||||
| 168 | 4 | 6 | my @content = @{$file->content}; | ||||
| 4 | 18 | ||||||
| 169 | 4 | 37 | my $last; | ||||
| 170 | |||||||
| 171 | return ( | ||||||
| 172 | sub { # get next element | ||||||
| 173 | 34 | 100 | 34 | 433 | if ($last) { | ||
| 174 | 8 | 9 | my $x = $last; | ||||
| 175 | 8 | 11 | undef $last; | ||||
| 176 | 8 | 19 | return $x; | ||||
| 177 | } | ||||||
| 178 | 26 | 30 | my $elem = shift @content; | ||||
| 179 | 26 | 100 | 36 | unless ($elem) { | |||
| 180 | 11 | 16 | $file = shift @files; | ||||
| 181 | 11 | 100 | 27 | return unless $file; | |||
| 182 | 3 | 5 | @content = @{$file->content}; | ||||
| 3 | 8 | ||||||
| 183 | 3 | 28 | $elem = shift @content; | ||||
| 184 | } | ||||||
| 185 | 18 | 34 | return { elem=>$elem, file=>$file->name }; | ||||
| 186 | }, | ||||||
| 187 | 8 | 8 | 10 | sub { $last = shift; } # putback | |||
| 188 | 4 | 29 | ); | ||||
| 189 | } | ||||||
| 190 | |||||||
| 191 | sub _toc { | ||||||
| 192 | 10 | 10 | 19 | my ($level, $next, $putback) = @_; | |||
| 193 | |||||||
| 194 | 10 | 11 | my @cur; | ||||
| 195 | 10 | 14 | while (my $c = $next->()) { | ||||
| 196 | 26 | 166 | my $e = $c->{elem}; | ||||
| 197 | 26 | 100 | 80 | next unless $e->isa('HTML::Element'); | |||
| 198 | 19 | 34 | my $tag = $e->tag; | ||||
| 199 | 19 | 138 | my ($lev) = ($tag =~ m{h(\d)}); | ||||
| 200 | 19 | 50 | 34 | next unless $lev; | |||
| 201 | 19 | 100 | 33 | if ($lev > $level) { | |||
| 202 | 6 | 13 | $putback->($c); | ||||
| 203 | 6 | 13 | my $children = _toc($lev, $next, $putback); | ||||
| 204 | 6 | 100 | 100 | 22 | if ($cur[-1] && $cur[-1]->{level} < $lev) { | ||
| 205 | 1 | 2 | $cur[-1]->{children} = $children; | ||||
| 206 | } else { | ||||||
| 207 | 5 | 6 | push @cur, @{$children}; | ||||
| 5 | 10 | ||||||
| 208 | } | ||||||
| 209 | 6 | 19 | next; | ||||
| 210 | } | ||||||
| 211 | 13 | 100 | 24 | if ($lev < $level) { | |||
| 212 | 2 | 7 | $putback->($c); | ||||
| 213 | 2 | 7 | return \@cur; | ||||
| 214 | } | ||||||
| 215 | push @cur, { | ||||||
| 216 | name => gensym, | ||||||
| 217 | level => $lev, | ||||||
| 218 | id => $e->attr('id'), | ||||||
| 219 | title => $e->as_text, | ||||||
| 220 | file => $c->{file}, | ||||||
| 221 | 11 | 24 | }; | ||||
| 222 | } | ||||||
| 223 | 8 | 38 | return \@cur; | ||||
| 224 | } | ||||||
| 225 | |||||||
| 226 | sub _make_toc { | ||||||
| 227 | 4 | 4 | 7 | my $self = shift; | |||
| 228 | 4 | 10 | my ($next, $putback) = _make_content_iterator($self->{files}); | ||||
| 229 | 4 | 11 | return _toc(1, $next, $putback); | ||||
| 230 | } | ||||||
| 231 | |||||||
| 232 | sub toc { | ||||||
| 233 | 4 | 4 | 0 | 21 | my ($self, $toc) = @_; | ||
| 234 | 4 | 50 | 9 | unless ($toc) { | |||
| 235 | 4 | 33 | 19 | $self->{toc} ||= $self->_make_toc; | |||
| 236 | 4 | 12 | return $self->{toc}; | ||||
| 237 | } | ||||||
| 238 | 0 | $self->{toc} = $toc; | |||||
| 239 | } | ||||||
| 240 | |||||||
| 241 | sub to_epub { | ||||||
| 242 | 0 | 0 | 1 | my ($self, %options) = @_; | |||
| 243 | |||||||
| 244 | 0 | my $epub_filename = $options{output}; | |||||
| 245 | 0 | 0 | $epub_filename ||= $self->title . ".epub"; | ||||
| 246 | |||||||
| 247 | 0 | 0 | if ($options{cover}) { | ||||
| 248 | 0 | $self->epub->set_cover($options{cover}); | |||||
| 249 | } | ||||||
| 250 | 0 | $self->epub->build_from_doc($self); | |||||
| 251 | |||||||
| 252 | 0 | $self->epub->save($epub_filename); | |||||
| 253 | } | ||||||
| 254 | |||||||
| 255 | sub as_html { | ||||||
| 256 | 0 | 0 | 1 | my $self = shift; | |||
| 257 | 0 | return join('', map { $_->as_html } @{$self->files}); | |||||
| 0 | |||||||
| 0 | |||||||
| 258 | } | ||||||
| 259 | 1; | ||||||
| 260 | __END__ |