asciidoctor-epub3 1.5.0.alpha.12 → 1.5.0.alpha.17

Sign up to get free protection for your applications and to get access to all the features.
data/.yardopts DELETED
@@ -1,12 +0,0 @@
1
- --charset UTF-8
2
- --readme README.adoc
3
- --no-private
4
- --hide-api private
5
- --title "Asciidoctor EPUB3 API Docs"
6
- --output-dir apidoc
7
- --exclude /ext(?:\.rb$|/)
8
- lib/**/*.rb
9
- -
10
- CHANGELOG.adoc
11
- LICENSE.adoc
12
- NOTICE.adoc
@@ -1,25 +0,0 @@
1
- [[LICENSE]]
2
- = LICENSE
3
-
4
- .The MIT License
5
- ....
6
- Copyright (C) 2014-2019 OpenDevise Inc. and the Asciidoctor Project
7
-
8
- Permission is hereby granted, free of charge, to any person obtaining a copy
9
- of this software and associated documentation files (the "Software"), to deal
10
- in the Software without restriction, including without limitation the rights
11
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
- copies of the Software, and to permit persons to whom the Software is
13
- furnished to do so, subject to the following conditions:
14
-
15
- The above copyright notice and this permission notice shall be included in
16
- all copies or substantial portions of the Software.
17
-
18
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
- THE SOFTWARE.
25
- ....
@@ -1,722 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- autoload :FileUtils, 'fileutils'
4
- autoload :Open3, 'open3'
5
-
6
- module Asciidoctor
7
- module Epub3
8
- module GepubBuilderMixin
9
- include ::Asciidoctor::Logging
10
- DATA_DIR = ::File.expand_path ::File.join(__dir__, '..', '..', 'data')
11
- SAMPLES_DIR = ::File.join DATA_DIR, 'samples'
12
- LF = ?\n
13
- CharEntityRx = ContentConverter::CharEntityRx
14
- XmlElementRx = ContentConverter::XmlElementRx
15
- FromHtmlSpecialCharsMap = ContentConverter::FromHtmlSpecialCharsMap
16
- FromHtmlSpecialCharsRx = ContentConverter::FromHtmlSpecialCharsRx
17
- CsvDelimiterRx = /\s*,\s*/
18
- ImageMacroRx = /^image::?(.*?)\[(.*?)\]$/
19
- ImgSrcScanRx = /<img src="(.+?)"/
20
- SvgImgSniffRx = /<img src=".+?\.svg"/
21
-
22
- attr_reader :book, :format, :spine
23
-
24
- # FIXME: move to Asciidoctor::Helpers
25
- def sanitize_doctitle_xml doc, content_spec
26
- doctitle = doc.header? ? doc.doctitle : (doc.attr 'untitled-label')
27
- sanitize_xml doctitle, content_spec
28
- end
29
-
30
- # FIXME: move to Asciidoctor::Helpers
31
- def sanitize_xml content, content_spec
32
- if content_spec != :pcdata && (content.include? '<')
33
- if (content = (content.gsub XmlElementRx, '').strip).include? ' '
34
- content = content.tr_s ' ', ' '
35
- end
36
- end
37
-
38
- case content_spec
39
- when :attribute_cdata
40
- content = content.gsub '"', '&quot;' if content.include? '"'
41
- when :cdata, :pcdata
42
- # noop
43
- when :plain_text
44
- if content.include? ';'
45
- content = content.gsub(CharEntityRx) { [$1.to_i].pack 'U*' } if content.include? '&#'
46
- content = content.gsub FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap
47
- end
48
- else
49
- raise ::ArgumentError, %(Unknown content spec: #{content_spec})
50
- end
51
- content
52
- end
53
-
54
- def add_theme_assets doc
55
- builder = self
56
- format = @format
57
- workdir = if doc.attr? 'epub3-stylesdir'
58
- stylesdir = doc.attr 'epub3-stylesdir'
59
- # FIXME: make this work for Windows paths!!
60
- if stylesdir.start_with? '/'
61
- stylesdir
62
- else
63
- docdir = doc.attr 'docdir', '.'
64
- docdir = '.' if docdir.empty?
65
- ::File.join docdir, stylesdir
66
- end
67
- else
68
- ::File.join DATA_DIR, 'styles'
69
- end
70
-
71
- # TODO: improve design/UX of custom theme functionality, including custom fonts
72
- resources do
73
- if format == :kf8
74
- # NOTE add layer of indirection so Kindle Direct Publishing (KDP) doesn't strip font-related CSS rules
75
- file 'styles/epub3.css' => '@import url("epub3-proxied.css");'.to_ios
76
- file 'styles/epub3-css3-only.css' => '@import url("epub3-css3-only-proxied.css");'.to_ios
77
- file 'styles/epub3-proxied.css' => (builder.postprocess_css_file ::File.join(workdir, 'epub3.css'), format)
78
- file 'styles/epub3-css3-only-proxied.css' => (builder.postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
79
- else
80
- file 'styles/epub3.css' => (builder.postprocess_css_file ::File.join(workdir, 'epub3.css'), format)
81
- file 'styles/epub3-css3-only.css' => (builder.postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
82
- end
83
- end
84
-
85
- resources do
86
- font_files, font_css = builder.select_fonts ::File.join(DATA_DIR, 'styles/epub3-fonts.css'), (doc.attr 'scripts', 'latin')
87
- file 'styles/epub3-fonts.css' => font_css
88
- unless font_files.empty?
89
- # NOTE metadata property in oepbs package manifest doesn't work; must use proprietary iBooks file instead
90
- #(@book.metadata.add_metadata 'meta', 'true')['property'] = 'ibooks:specified-fonts' unless format == :kf8
91
- builder.optional_file 'META-INF/com.apple.ibooks.display-options.xml' => '<?xml version="1.0" encoding="UTF-8"?>
92
- <display_options>
93
- <platform name="*">
94
- <option name="specified-fonts">true</option>
95
- </platform>
96
- </display_options>'.to_ios unless format == :kf8
97
-
98
- # https://github.com/asciidoctor/asciidoctor-epub3/issues/120
99
- #
100
- # 'application/x-font-ttf' causes warnings in epubcheck 4.0.2,
101
- # "non-standard font type". Discussion:
102
- # https://www.mobileread.com/forums/showthread.php?t=231272
103
- #
104
- # 3.1 spec recommends 'application/font-sfnt', but epubcheck doesn't
105
- # implement that yet (warnings). https://idpf.github.io/epub-cmt/v3/
106
- #
107
- # 3.0 spec recommends 'application/vnd.ms-opentype', this works without
108
- # warnings.
109
- # http://www.idpf.org/epub/30/spec/epub30-publications.html#sec-core-media-types
110
- with_media_type 'application/vnd.ms-opentype' do
111
- font_files.each do |font_file|
112
- file font_file => ::File.join(DATA_DIR, font_file)
113
- end
114
- end
115
- end
116
- end
117
- nil
118
- end
119
-
120
- def add_cover_image doc
121
- return if (image_path = doc.attr 'front-cover-image').nil?
122
-
123
- imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
124
- imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
125
-
126
- image_attrs = {}
127
- if (image_path.include? ':') && image_path =~ ImageMacroRx
128
- logger.warn %(deprecated block macro syntax detected in front-cover-image attribute) if image_path.start_with? 'image::'
129
- image_path = %(#{imagesdir}#{$1})
130
- (::Asciidoctor::AttributeList.new $2).parse_into image_attrs, %w(alt width height) unless $2.empty?
131
- end
132
-
133
- workdir = (workdir = doc.attr 'docdir').nil_or_empty? ? '.' : workdir
134
- unless ::File.readable? ::File.join(workdir, image_path)
135
- logger.error %(#{::File.basename doc.attr('docfile')}: front cover image not found or readable: #{::File.expand_path image_path, workdir})
136
- return
137
- end
138
-
139
- unless !image_attrs.empty? && (width = image_attrs['width']) && (height = image_attrs['height'])
140
- width, height = 1050, 1600
141
- end
142
-
143
- resources do
144
- cover_image %(#{imagesdir}jacket/cover#{::File.extname image_path}) => (::File.join workdir, image_path)
145
- @last_defined_item.tap do |last_item|
146
- last_item['width'] = width
147
- last_item['height'] = height
148
- end
149
- end
150
- nil
151
- end
152
-
153
- # NOTE must be called within the ordered block
154
- def add_cover_page doc, spine_builder, manifest
155
- return if (cover_item_attrs = manifest.items['item_cover'].instance_variable_get :@attributes).nil?
156
-
157
- href = cover_item_attrs['href']
158
- # NOTE we only store width and height temporarily to pass through the values
159
- width = cover_item_attrs.delete 'width'
160
- height = cover_item_attrs.delete 'height'
161
-
162
- # NOTE SVG wrapper maintains aspect ratio and confines image to view box
163
- content = %(<!DOCTYPE html>
164
- <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
165
- <head>
166
- <meta charset="UTF-8"/>
167
- <title>#{sanitize_doctitle_xml doc, :cdata}</title>
168
- <style type="text/css">
169
- @page {
170
- margin: 0;
171
- }
172
- html {
173
- margin: 0 !important;
174
- padding: 0 !important;
175
- }
176
- body {
177
- margin: 0;
178
- padding: 0 !important;
179
- text-align: center;
180
- }
181
- body > svg {
182
- /* prevent bleed onto second page (removes descender space) */
183
- display: block;
184
- }
185
- </style>
186
- </head>
187
- <body epub:type="cover"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
188
- width="100%" height="100%" viewBox="0 0 #{width} #{height}" preserveAspectRatio="xMidYMid meet">
189
- <image width="#{width}" height="#{height}" xlink:href="#{href}"/>
190
- </svg></body>
191
- </html>).to_ios
192
- # Gitden expects a cover.xhtml, so add it to the spine
193
- spine_builder.file 'cover.xhtml' => content
194
- assigned_id = (spine_builder.instance_variable_get :@last_defined_item).item.id
195
- spine_builder.id 'cover'
196
- # clearly a deficiency of gepub that it does not match the id correctly
197
- # FIXME can we move this hack elsewhere?
198
- @book.spine.itemref_by_id[assigned_id].idref = 'cover'
199
- nil
200
- end
201
-
202
- def add_images_from_front_matter
203
- (::File.read 'front-matter.html').scan ImgSrcScanRx do
204
- resources do
205
- file $1
206
- end
207
- end if ::File.file? 'front-matter.html'
208
- nil
209
- end
210
-
211
- def add_front_matter_page _doc, spine_builder
212
- if ::File.file? 'front-matter.html'
213
- front_matter_content = ::File.read 'front-matter.html'
214
- spine_builder.file 'front-matter.xhtml' => (postprocess_xhtml front_matter_content, @format)
215
- spine_builder.add_property 'svg' unless (spine_builder.property? 'svg') || SvgImgSniffRx !~ front_matter_content
216
- end
217
- nil
218
- end
219
-
220
- def add_content_images doc, images
221
- docimagesdir = (doc.attr 'imagesdir', '.').chomp '/'
222
- docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/))
223
-
224
- self_logger = logger
225
- workdir = (workdir = doc.attr 'docdir').nil_or_empty? ? '.' : workdir
226
- resources workdir: workdir do
227
- images.each do |image|
228
- if (image_path = image[:path]).start_with? %(#{docimagesdir}jacket/cover.)
229
- self_logger.warn %(image path is reserved for cover artwork: #{image_path}; skipping image found in content)
230
- elsif ::File.readable? image_path
231
- file image_path
232
- else
233
- self_logger.error %(#{::File.basename image[:docfile]}: image not found or not readable: #{::File.expand_path image_path, workdir})
234
- end
235
- end
236
- end
237
- nil
238
- end
239
-
240
- def add_profile_images doc, usernames
241
- imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
242
- imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
243
-
244
- resources do
245
- file %(#{imagesdir}avatars/default.jpg) => ::File.join(DATA_DIR, 'images/default-avatar.jpg')
246
- file %(#{imagesdir}headshots/default.jpg) => ::File.join(DATA_DIR, 'images/default-headshot.jpg')
247
- end
248
-
249
- self_logger = logger
250
- workdir = (workdir = doc.attr 'docdir').nil_or_empty? ? '.' : workdir
251
- resources do
252
- usernames.each do |username|
253
- avatar = %(#{imagesdir}avatars/#{username}.jpg)
254
- if ::File.readable? (resolved_avatar = (::File.join workdir, avatar))
255
- file avatar => resolved_avatar
256
- else
257
- self_logger.error %(avatar for #{username} not found or readable: #{avatar}; falling back to default avatar)
258
- file avatar => ::File.join(DATA_DIR, 'images/default-avatar.jpg')
259
- end
260
-
261
- headshot = %(#{imagesdir}headshots/#{username}.jpg)
262
- if ::File.readable? (resolved_headshot = (::File.join workdir, headshot))
263
- file headshot => resolved_headshot
264
- elsif doc.attr? 'builder', 'editions'
265
- self_logger.error %(headshot for #{username} not found or readable: #{headshot}; falling back to default headshot)
266
- file headshot => ::File.join(DATA_DIR, 'images/default-headshot.jpg')
267
- end
268
- end
269
- end
270
- nil
271
- end
272
-
273
- def add_content doc
274
- builder, spine, format, images = self, @spine, @format, {}
275
- workdir = (doc.attr 'docdir').nil_or_empty? ? '.' : workdir
276
- resources workdir: workdir do
277
- extend GepubResourceBuilderMixin
278
- builder.add_images_from_front_matter
279
- builder.add_nav_doc doc, self, spine, format
280
- builder.add_ncx_doc doc, self, spine
281
- ordered do
282
- builder.add_cover_page doc, self, @book.manifest unless format == :kf8
283
- builder.add_front_matter_page doc, self
284
- spine.each_with_index do |item, _i|
285
- docfile = item.attr 'docfile'
286
- imagesdir = (item.attr 'imagesdir', '.').chomp '/'
287
- imagesdir = (imagesdir == '.' ? '' : %(#{imagesdir}/))
288
- file %(#{item.id || (item.attr 'docname')}.xhtml) => (builder.postprocess_xhtml item.convert, format)
289
- add_property 'svg' if ((item.attr 'epub-properties') || []).include? 'svg'
290
- # QUESTION should we pass the document itself?
291
- item.references[:images].each do |target|
292
- images[image_path = %(#{imagesdir}#{target})] ||= { docfile: docfile, path: image_path }
293
- end
294
- # QUESTION reenable?
295
- #linear 'yes' if i == 0
296
- end
297
- end
298
- end
299
- add_content_images doc, images.values
300
- nil
301
- end
302
-
303
- def add_nav_doc doc, spine_builder, spine, format
304
- spine_builder.nav 'nav.xhtml' => (postprocess_xhtml nav_doc(doc, spine), format)
305
- spine_builder.id 'nav'
306
- nil
307
- end
308
-
309
- # TODO: aggregate authors of spine document into authors attribute(s) on main document
310
- def nav_doc doc, spine
311
- lines = [%(<!DOCTYPE html>
312
- <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = doc.attr 'lang', 'en'}" lang="#{lang}">
313
- <head>
314
- <meta charset="UTF-8"/>
315
- <title>#{sanitize_doctitle_xml doc, :cdata}</title>
316
- <link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
317
- <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
318
- </head>
319
- <body>
320
- <h1>#{sanitize_doctitle_xml doc, :pcdata}</h1>
321
- <nav epub:type="toc" id="toc">
322
- <h2>#{doc.attr 'toc-title'}</h2>)]
323
- lines << (nav_level spine, [(doc.attr 'toclevels', 1).to_i, 0].max)
324
- lines << %(</nav>
325
- </body>
326
- </html>)
327
- lines * LF
328
- end
329
-
330
- def nav_level items, depth, state = {}
331
- lines = []
332
- lines << '<ol>'
333
- items.each do |item|
334
- #index = (state[:index] = (state.fetch :index, 0) + 1)
335
- if item.context == :document
336
- # NOTE we sanitize the chapter titles because we use formatting to control layout
337
- item_label = sanitize_doctitle_xml item, :cdata
338
- item_href = (state[:content_doc_href] = %(#{item.id || (item.attr 'docname')}.xhtml))
339
- else
340
- item_label = sanitize_xml item.title, :pcdata
341
- item_href = %(#{state[:content_doc_href]}##{item.id})
342
- end
343
- lines << %(<li><a href="#{item_href}">#{item_label}</a>)
344
- if depth == 0 || (child_sections = item.sections).empty?
345
- lines[-1] = %(#{lines[-1]}</li>)
346
- else
347
- lines << (nav_level child_sections, depth - 1, state)
348
- lines << '</li>'
349
- end
350
- state.delete :content_doc_href if item.context == :document
351
- end
352
- lines << '</ol>'
353
- lines * LF
354
- end
355
-
356
- # NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
357
- def add_ncx_doc doc, spine_builder, spine
358
- spine_builder.file 'toc.ncx' => (ncx_doc doc, spine).to_ios
359
- spine_builder.id 'ncx'
360
- nil
361
- end
362
-
363
- def ncx_doc doc, spine
364
- # TODO: populate docAuthor element based on unique authors in work
365
- lines = [%(<?xml version="1.0" encoding="utf-8"?>
366
- <ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="#{doc.attr 'lang', 'en'}">
367
- <head>
368
- <meta name="dtb:uid" content="#{@book.identifier}"/>
369
- %{depth}
370
- <meta name="dtb:totalPageCount" content="0"/>
371
- <meta name="dtb:maxPageNumber" content="0"/>
372
- </head>
373
- <docTitle><text>#{sanitize_doctitle_xml doc, :cdata}</text></docTitle>
374
- <navMap>)]
375
- lines << (ncx_level spine, [(doc.attr 'toclevels', 1).to_i, 0].max, state = {})
376
- lines[0] = lines[0].sub '%{depth}', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>)
377
- lines << %(</navMap>
378
- </ncx>)
379
- lines * LF
380
- end
381
-
382
- def ncx_level items, depth, state = {}
383
- lines = []
384
- state[:max_depth] = (state.fetch :max_depth, 0) + 1
385
- items.each do |item|
386
- index = (state[:index] = (state.fetch :index, 0) + 1)
387
- item_id = %(nav_#{index})
388
- if item.context == :document
389
- item_label = sanitize_doctitle_xml item, :cdata
390
- item_href = (state[:content_doc_href] = %(#{item.id || (item.attr 'docname')}.xhtml))
391
- else
392
- item_label = sanitize_xml item.title, :cdata
393
- item_href = %(#{state[:content_doc_href]}##{item.id})
394
- end
395
- lines << %(<navPoint id="#{item_id}" playOrder="#{index}">)
396
- lines << %(<navLabel><text>#{item_label}</text></navLabel>)
397
- lines << %(<content src="#{item_href}"/>)
398
- unless depth == 0 || (child_sections = item.sections).empty?
399
- lines << (ncx_level child_sections, depth - 1, state)
400
- end
401
- lines << %(</navPoint>)
402
- state.delete :content_doc_href if item.context == :document
403
- end
404
- lines * LF
405
- end
406
-
407
- def collect_keywords doc, spine
408
- ([doc] + spine).map {|item|
409
- if item.attr? 'keywords'
410
- (item.attr 'keywords').split CsvDelimiterRx
411
- else
412
- []
413
- end
414
- }.flatten.uniq
415
- end
416
-
417
- # Swap fonts in CSS based on the value of the document attribute 'scripts',
418
- # then return the list of fonts as well as the font CSS.
419
- def select_fonts filename, scripts = 'latin'
420
- font_css = ::File.read filename
421
- font_css = font_css.gsub(/(?<=-)latin(?=\.ttf\))/, scripts) unless scripts == 'latin'
422
-
423
- # match CSS font urls in the forms of:
424
- # src: url(../fonts/notoserif-regular-latin.ttf);
425
- # src: url(../fonts/notoserif-regular-latin.ttf) format("truetype");
426
- font_list = font_css.scan(/url\(\.\.\/([^)]+\.ttf)\)/).flatten
427
-
428
- [font_list, font_css.to_ios]
429
- end
430
-
431
- def postprocess_css_file filename, format
432
- return filename unless format == :kf8
433
- postprocess_css ::File.read(filename), format
434
- end
435
-
436
- def postprocess_css content, format
437
- return content.to_ios unless format == :kf8
438
- # TODO: convert regular expressions to constants
439
- content
440
- .gsub(/^ -webkit-column-break-.*\n/, '')
441
- .gsub(/^ max-width: .*\n/, '')
442
- .to_ios
443
- end
444
-
445
- def postprocess_xhtml_file filename, format
446
- return filename unless format == :kf8
447
- postprocess_xhtml ::File.read(filename), format
448
- end
449
-
450
- # NOTE Kindle requires that
451
- # <meta charset="utf-8"/>
452
- # be converted to
453
- # <meta http-equiv="Content-Type" content="application/xml+xhtml; charset=UTF-8"/>
454
- def postprocess_xhtml content, format
455
- return content.to_ios unless format == :kf8
456
- # TODO: convert regular expressions to constants
457
- content
458
- .gsub(/<meta charset="(.+?)"\/>/, '<meta http-equiv="Content-Type" content="application/xml+xhtml; charset=\1"/>')
459
- .gsub(/<img([^>]+) style="width: (\d\d)%;"/, '<img\1 style="width: \2%; height: \2%;"')
460
- .gsub(/<script type="text\/javascript">.*?<\/script>\n?/m, '')
461
- .to_ios
462
- end
463
- end
464
-
465
- module GepubResourceBuilderMixin
466
- # Add missing method to builder to add a property to last defined item
467
- def add_property property
468
- @last_defined_item.add_property property
469
- end
470
-
471
- # Add helper method to builder to check if property is set on last defined item
472
- def property? property
473
- (@last_defined_item['properties'] || []).include? property
474
- end
475
- end
476
-
477
- class Packager
478
- include ::Asciidoctor::Logging
479
-
480
- EpubExtensionRx = /\.epub$/i
481
- KindlegenCompression = ::Hash['0', '-c0', '1', '-c1', '2', '-c2', 'none', '-c0', 'standard', '-c1', 'huffdic', '-c2']
482
-
483
- def initialize spine_doc, spine, format = :epub3, _options = {}
484
- @document = spine_doc
485
- @spine = spine || []
486
- @format = format
487
- end
488
-
489
- def package options = {}
490
- doc = @document
491
- spine = @spine
492
- fmt = @format
493
- target = options[:target]
494
- dest = File.dirname target
495
-
496
- # FIXME: authors should be aggregated already on parent document
497
- if doc.attr? 'authors'
498
- authors = (doc.attr 'authors').split(GepubBuilderMixin::CsvDelimiterRx).concat(spine.map {|item| item.attr 'author' }.compact).uniq
499
- else
500
- authors = []
501
- end
502
-
503
- builder = ::GEPUB::Builder.new do
504
- extend GepubBuilderMixin
505
- @document = doc
506
- @spine = spine
507
- @format = fmt
508
- @book.epub_backward_compat = fmt != :kf8
509
-
510
- language doc.attr('lang', 'en')
511
- id 'pub-language'
512
-
513
- if doc.attr? 'uuid'
514
- unique_identifier doc.attr('uuid'), 'pub-identifier', 'uuid'
515
- else
516
- unique_identifier doc.id, 'pub-identifier', 'uuid'
517
- end
518
- # replace with next line once the attributes argument is supported
519
- #unique_identifier doc.id, 'pub-id', 'uuid', 'scheme' => 'xsd:string'
520
-
521
- # NOTE we must use :plain_text here since gepub reencodes
522
- title sanitize_doctitle_xml(doc, :plain_text)
523
- id 'pub-title'
524
-
525
- # FIXME: this logic needs some work
526
- if doc.attr? 'publisher'
527
- publisher (publisher_name = (doc.attr 'publisher'))
528
- # marc role: Book producer (see http://www.loc.gov/marc/relators/relaterm.html)
529
- creator (doc.attr 'producer', publisher_name), 'bkp'
530
- elsif doc.attr? 'producer'
531
- # NOTE Use producer as both publisher and producer if publisher isn't specified
532
- producer_name = doc.attr 'producer'
533
- publisher producer_name
534
- # marc role: Book producer (see http://www.loc.gov/marc/relators/relaterm.html)
535
- creator producer_name, 'bkp'
536
- elsif doc.attr? 'author'
537
- # NOTE Use author as creator if both publisher or producer are absent
538
- # marc role: Author (see http://www.loc.gov/marc/relators/relaterm.html)
539
- creator doc.attr('author'), 'aut'
540
- end
541
-
542
- if doc.attr? 'creator'
543
- # marc role: Creator (see http://www.loc.gov/marc/relators/relaterm.html)
544
- creator doc.attr('creator'), 'cre'
545
- else
546
- # marc role: Manufacturer (see http://www.loc.gov/marc/relators/relaterm.html)
547
- # QUESTION should this be bkp?
548
- creator 'Asciidoctor', 'mfr'
549
- end
550
-
551
- # TODO: getting author list should be a method on Asciidoctor API
552
- contributors(*authors)
553
-
554
- if doc.attr? 'revdate'
555
- begin
556
- date doc.attr('revdate')
557
- rescue ArgumentError => e
558
- logger.error %(#{::File.basename doc.attr('docfile')}: failed to parse revdate: #{e}, using current time as a fallback)
559
- date ::Time.now
560
- end
561
- else
562
- date ::Time.now
563
- end
564
-
565
- description doc.attr('description') if doc.attr? 'description'
566
-
567
- (collect_keywords doc, spine).each do |s|
568
- subject s
569
- end
570
-
571
- source doc.attr('source') if doc.attr? 'source'
572
-
573
- rights doc.attr('copyright') if doc.attr? 'copyright'
574
-
575
- #add_metadata 'ibooks:specified-fonts', true
576
-
577
- add_theme_assets doc
578
- add_cover_image doc
579
- if (doc.attr 'publication-type', 'book') != 'book'
580
- usernames = spine.map {|item| item.attr 'username' }.compact.uniq
581
- add_profile_images doc, usernames
582
- end
583
- add_content doc
584
- end
585
-
586
- ::FileUtils.mkdir_p dest unless ::File.directory? dest
587
-
588
- epub_file = fmt == :kf8 ? %(#{::Asciidoctor::Helpers.rootname target}-kf8.epub) : target
589
- builder.generate_epub epub_file
590
- logger.debug %(Wrote #{fmt.upcase} to #{epub_file})
591
- if options[:extract]
592
- extract_dir = epub_file.sub EpubExtensionRx, ''
593
- ::FileUtils.remove_dir extract_dir if ::File.directory? extract_dir
594
- ::Dir.mkdir extract_dir
595
- ::Dir.chdir extract_dir do
596
- ::Zip::File.open epub_file do |entries|
597
- entries.each do |entry|
598
- next unless entry.file?
599
- unless (entry_dir = ::File.dirname entry.name) == '.' || (::File.directory? entry_dir)
600
- ::FileUtils.mkdir_p entry_dir
601
- end
602
- entry.extract
603
- end
604
- end
605
- end
606
- logger.debug %(Extracted #{fmt.upcase} to #{extract_dir})
607
- end
608
-
609
- if fmt == :kf8
610
- # QUESTION shouldn't we validate this epub file too?
611
- distill_epub_to_mobi epub_file, target, options[:compress], options[:kindlegen_path]
612
- elsif options[:validate]
613
- validate_epub epub_file, options[:epubcheck_path]
614
- end
615
- end
616
-
617
- def get_kindlegen_command kindlegen_path
618
- unless kindlegen_path.nil?
619
- logger.debug %(Using ebook-kindlegen-path attribute: #{kindlegen_path})
620
- return [kindlegen_path]
621
- end
622
-
623
- unless (result = ENV['KINDLEGEN']).nil?
624
- logger.debug %(Using KINDLEGEN env variable: #{result})
625
- return [result]
626
- end
627
-
628
- begin
629
- require 'kindlegen' unless defined? ::Kindlegen
630
- result = ::Kindlegen.command.to_s
631
- logger.debug %(Using KindleGen from gem: #{result})
632
- [result]
633
- rescue LoadError => e
634
- logger.debug %(#{e}; Using KindleGen from PATH)
635
- [%(kindlegen#{::Gem.win_platform? ? '.exe' : ''})]
636
- end
637
- end
638
-
639
- def distill_epub_to_mobi epub_file, target, compress, kindlegen_path
640
- mobi_file = ::File.basename target.sub(EpubExtensionRx, '.mobi')
641
- compress_flag = KindlegenCompression[compress ? (compress.empty? ? '1' : compress.to_s) : '0']
642
-
643
- argv = get_kindlegen_command(kindlegen_path) + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
644
- begin
645
- # This duplicates Kindlegen.run, but we want to override executable
646
- out, err, res = Open3.capture3(*argv) do |r|
647
- r.force_encoding 'UTF-8' if ::Gem.win_platform? && r.respond_to?(:force_encoding)
648
- end
649
- rescue Errno::ENOENT => e
650
- raise 'Unable to run KindleGen. Either install the kindlegen gem or set KINDLEGEN environment variable with path to KindleGen executable', cause: e
651
- end
652
-
653
- out.each_line do |line|
654
- logger.info line
655
- end
656
- err.each_line do |line|
657
- log_line line
658
- end
659
-
660
- output_file = ::File.join ::File.dirname(epub_file), mobi_file
661
- if res.success?
662
- logger.debug %(Wrote MOBI to #{output_file})
663
- else
664
- logger.error %(kindlegen failed to write MOBI to #{output_file})
665
- end
666
- end
667
-
668
- def get_epubcheck_command epubcheck_path
669
- unless epubcheck_path.nil?
670
- logger.debug %(Using ebook-epubcheck-path attribute: #{epubcheck_path})
671
- return [epubcheck_path]
672
- end
673
-
674
- unless (result = ENV['EPUBCHECK']).nil?
675
- logger.debug %(Using EPUBCHECK env variable: #{result})
676
- return [result]
677
- end
678
-
679
- begin
680
- result = ::Gem.bin_path 'epubcheck-ruby', 'epubcheck'
681
- logger.debug %(Using EPUBCheck from gem: #{result})
682
- [::Gem.ruby, result]
683
- rescue ::Gem::Exception => e
684
- logger.debug %(#{e}; Using EPUBCheck from PATH)
685
- ['epubcheck']
686
- end
687
- end
688
-
689
- def validate_epub epub_file, epubcheck_path
690
- argv = get_epubcheck_command(epubcheck_path) + ['-w', epub_file]
691
- begin
692
- out, err, res = Open3.capture3(*argv)
693
- rescue Errno::ENOENT => e
694
- raise 'Unable to run EPUBCheck. Either install epubcheck-ruby gem or set EPUBCHECK environment variable with path to EPUBCheck executable', cause: e
695
- end
696
-
697
- out.each_line do |line|
698
- logger.info line
699
- end
700
- err.each_line do |line|
701
- log_line line
702
- end
703
-
704
- logger.error %(EPUB validation failed: #{epub_file}) unless res.success?
705
- end
706
-
707
- def log_line line
708
- line = line.strip
709
-
710
- if line =~ /^fatal/i
711
- logger.fatal line
712
- elsif line =~ /^error/i
713
- logger.error line
714
- elsif line =~ /^warning/i
715
- logger.warn line
716
- else
717
- logger.info line
718
- end
719
- end
720
- end
721
- end
722
- end