asciidoctor-epub3 1.5.0.alpha.6 → 1.5.0.alpha.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +134 -0
- data/Gemfile +10 -0
- data/README.adoc +248 -172
- data/asciidoctor-epub3.gemspec +42 -0
- data/bin/adb-push-ebook +19 -10
- data/data/styles/epub3-css3-only.css +28 -11
- data/data/styles/epub3.css +40 -39
- data/lib/asciidoctor-epub3/converter.rb +188 -121
- data/lib/asciidoctor-epub3/core_ext/string.rb +1 -1
- data/lib/asciidoctor-epub3/font_icon_map.rb +1 -1
- data/lib/asciidoctor-epub3/packager.rb +240 -104
- data/lib/asciidoctor-epub3/spine_item_processor.rb +22 -11
- data/lib/asciidoctor-epub3/version.rb +1 -1
- metadata +24 -35
- data/data/samples/asciidoctor-epub3-readme.adoc +0 -849
- data/data/samples/asciidoctor-js-browser-extension.adoc +0 -46
- data/data/samples/asciidoctor-js-introduction.adoc +0 -91
- data/data/samples/i18n.adoc +0 -161
- data/data/samples/images/asciidoctor-js-chrome-extension.png +0 -0
- data/data/samples/images/avatars/graphitefriction.jpg +0 -0
- data/data/samples/images/avatars/mogztter.jpg +0 -0
- data/data/samples/images/avatars/mojavelinux.jpg +0 -0
- data/data/samples/images/correct-text-justification.png +0 -0
- data/data/samples/images/incorrect-text-justification.png +0 -0
- data/data/samples/images/screenshots/chapter-title-day.png +0 -0
- data/data/samples/images/screenshots/chapter-title.png +0 -0
- data/data/samples/images/screenshots/figure-admonition.png +0 -0
- data/data/samples/images/screenshots/section-title-paragraph.png +0 -0
- data/data/samples/images/screenshots/sidebar.png +0 -0
- data/data/samples/images/screenshots/table.png +0 -0
- data/data/samples/images/screenshots/text.png +0 -0
- data/data/samples/sample-book.adoc +0 -21
- data/data/samples/sample-content.adoc +0 -168
- data/scripts/generate-font-subsets.pe +0 -235
@@ -8,26 +8,46 @@ module Epub3
|
|
8
8
|
module GepubBuilderMixin
|
9
9
|
DATA_DIR = ::File.expand_path(::File.join ::File.dirname(__FILE__), '..', '..', 'data')
|
10
10
|
SAMPLES_DIR = ::File.join DATA_DIR, 'samples'
|
11
|
-
|
11
|
+
CharEntityRx = ContentConverter::CharEntityRx
|
12
|
+
XmlElementRx = ContentConverter::XmlElementRx
|
12
13
|
FromHtmlSpecialCharsMap = ContentConverter::FromHtmlSpecialCharsMap
|
13
14
|
FromHtmlSpecialCharsRx = ContentConverter::FromHtmlSpecialCharsRx
|
14
15
|
CsvDelimiterRx = /\s*,\s*/
|
15
16
|
DefaultCoverImage = 'images/default-cover.png'
|
16
|
-
|
17
|
+
ImageMacroRx = /^image::?(.*?)\[(.*?)\]$/
|
18
|
+
ImgSrcScanRx = /<img src="(.+?)"/
|
19
|
+
SvgImgSniffRx = /<img src=".+?\.svg"/
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
+
attr_reader :book, :format, :spine
|
22
|
+
|
23
|
+
# FIXME move to Asciidoctor::Helpers
|
24
|
+
def sanitize_doctitle_xml doc, content_spec
|
25
|
+
doctitle = doc.header? ? doc.doctitle : (doc.attr 'untitled-label')
|
26
|
+
sanitize_xml doctitle, content_spec
|
27
|
+
end
|
28
|
+
|
29
|
+
# FIXME move to Asciidoctor::Helpers
|
30
|
+
def sanitize_xml content, content_spec
|
31
|
+
if content_spec != :pcdata && (content.include? '<')
|
32
|
+
if (content = (content.gsub XmlElementRx, '').strip).include? ' '
|
33
|
+
content = content.tr_s ' ', ' '
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
case content_spec
|
21
38
|
when :attribute_cdata
|
22
|
-
|
23
|
-
when :
|
24
|
-
|
25
|
-
when :
|
26
|
-
|
27
|
-
|
28
|
-
|
39
|
+
content = content.gsub '"', '"' if content.include? '"'
|
40
|
+
when :cdata, :pcdata
|
41
|
+
# noop
|
42
|
+
when :plain_text
|
43
|
+
if content.include? ';'
|
44
|
+
content = content.gsub(CharEntityRx) { [$1.to_i].pack 'U*' } if content.include? '&#'
|
45
|
+
content = content.gsub FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap
|
46
|
+
end
|
47
|
+
else
|
48
|
+
raise ::ArgumentError, %(Unknown content spec: #{content_spec})
|
29
49
|
end
|
30
|
-
|
50
|
+
content
|
31
51
|
end
|
32
52
|
|
33
53
|
def add_theme_assets doc
|
@@ -81,50 +101,61 @@ module GepubBuilderMixin
|
|
81
101
|
end
|
82
102
|
end
|
83
103
|
end
|
104
|
+
nil
|
84
105
|
end
|
85
106
|
|
86
107
|
def add_cover_image doc
|
87
108
|
imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
|
88
109
|
imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
|
89
110
|
|
90
|
-
if (
|
91
|
-
|
92
|
-
|
111
|
+
if (image_path = doc.attr 'front-cover-image')
|
112
|
+
image_attrs = {}
|
113
|
+
if (image_path.include? ':') && image_path =~ ImageMacroRx
|
114
|
+
if image_path.start_with? 'image::'
|
115
|
+
warn %(asciidoctor: WARNING: deprecated block macro syntax detected in front-cover-image attribute)
|
116
|
+
end
|
117
|
+
image_path = %(#{imagesdir}#{$1})
|
118
|
+
(::Asciidoctor::AttributeList.new $2).parse_into image_attrs, %w(alt width height) unless $2.empty?
|
93
119
|
end
|
94
|
-
workdir = doc.attr 'docdir'
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
120
|
+
workdir = (workdir = doc.attr 'docdir').nil_or_empty? ? '.' : workdir
|
121
|
+
if ::File.readable?(::File.join workdir, image_path)
|
122
|
+
unless !image_attrs.empty? && (width = image_attrs['width']) && (height = image_attrs['height'])
|
123
|
+
width, height = 1050, 1600
|
124
|
+
end
|
125
|
+
else
|
126
|
+
warn %(asciidoctor: ERROR: front cover image not found or readable: #{image_path})
|
127
|
+
image_path = nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
unless image_path
|
132
|
+
image_path, workdir, width, height = DefaultCoverImage, DATA_DIR, 1050, 1600
|
99
133
|
end
|
100
134
|
|
101
135
|
resources do
|
102
|
-
cover_image %(#{imagesdir}jacket/cover#{::File.extname
|
136
|
+
cover_image %(#{imagesdir}jacket/cover#{::File.extname image_path}) => (::File.join workdir, image_path)
|
137
|
+
@last_defined_item.tap do |last_item|
|
138
|
+
last_item['width'] = width
|
139
|
+
last_item['height'] = height
|
140
|
+
end
|
103
141
|
end
|
142
|
+
nil
|
104
143
|
end
|
105
144
|
|
106
145
|
# NOTE must be called within the ordered block
|
107
|
-
def add_cover_page doc, spine_builder,
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
if img =~ InlineImageMacroRx
|
114
|
-
img = %(#{imagesdir}#{$1})
|
115
|
-
# TODO use proper attribute parser
|
116
|
-
_, w, h = $2.split ',', 3
|
117
|
-
end
|
146
|
+
def add_cover_page doc, spine_builder, manifest
|
147
|
+
cover_item_attrs = manifest.items['item_cover'].instance_variable_get :@attributes
|
148
|
+
href = cover_item_attrs['href']
|
149
|
+
# NOTE we only store width and height temporarily to pass through the values
|
150
|
+
width = cover_item_attrs.delete 'width'
|
151
|
+
height = cover_item_attrs.delete 'height'
|
118
152
|
|
119
|
-
w ||= 1050
|
120
|
-
h ||= 1600
|
121
|
-
img_path = %(#{imagesdir}jacket/cover#{::File.extname img})
|
122
153
|
# NOTE SVG wrapper maintains aspect ratio and confines image to view box
|
123
154
|
content = %(<!DOCTYPE html>
|
124
155
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
|
125
156
|
<head>
|
126
157
|
<meta charset="UTF-8"/>
|
127
|
-
<title>#{
|
158
|
+
<title>#{sanitize_doctitle_xml doc, :cdata}</title>
|
128
159
|
<style type="text/css">
|
129
160
|
@page {
|
130
161
|
margin: 0;
|
@@ -145,60 +176,64 @@ body > svg {
|
|
145
176
|
</style>
|
146
177
|
</head>
|
147
178
|
<body epub:type="cover"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
148
|
-
width="100%" height="100%" viewBox="0 0 #{
|
149
|
-
<image width="#{
|
179
|
+
width="100%" height="100%" viewBox="0 0 #{width} #{height}" preserveAspectRatio="xMidYMid meet">
|
180
|
+
<image width="#{width}" height="#{height}" xlink:href="#{href}"/>
|
150
181
|
</svg></body>
|
151
182
|
</html>).to_ios
|
152
|
-
#
|
183
|
+
# Gitden expects a cover.xhtml, so add it to the spine
|
153
184
|
spine_builder.file 'cover.xhtml' => content
|
185
|
+
assigned_id = (spine_builder.instance_variable_get :@last_defined_item).item.id
|
154
186
|
spine_builder.id 'cover'
|
155
187
|
# clearly a deficiency of gepub that it does not match the id correctly
|
156
|
-
|
188
|
+
# FIXME can we move this hack elsewhere?
|
189
|
+
@book.spine.itemref_by_id[assigned_id].idref = 'cover'
|
190
|
+
nil
|
157
191
|
end
|
158
192
|
|
159
193
|
def add_images_from_front_matter
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
file $1
|
164
|
-
end
|
194
|
+
(::File.read 'front-matter.html').scan ImgSrcScanRx do
|
195
|
+
resources do
|
196
|
+
file $1
|
165
197
|
end
|
166
|
-
end
|
198
|
+
end if ::File.file? 'front-matter.html'
|
199
|
+
nil
|
167
200
|
end
|
168
201
|
|
169
|
-
def add_front_matter_page doc, spine_builder
|
170
|
-
if ::File.
|
171
|
-
|
172
|
-
|
202
|
+
def add_front_matter_page doc, spine_builder
|
203
|
+
if ::File.file? 'front-matter.html'
|
204
|
+
front_matter_content = ::File.read 'front-matter.html'
|
205
|
+
spine_builder.file 'front-matter.xhtml' => (postprocess_xhtml front_matter_content, @format)
|
206
|
+
unless (spine_builder.property? 'svg') || SvgImgSniffRx !~ front_matter_content
|
207
|
+
spine_builder.add_property 'svg'
|
208
|
+
end
|
173
209
|
end
|
210
|
+
nil
|
174
211
|
end
|
175
212
|
|
176
|
-
# FIXME don't add same image more than once
|
177
213
|
# FIXME add inline images
|
178
214
|
def add_content_images doc, images
|
179
215
|
docimagesdir = (doc.attr 'imagesdir', '.').chomp '/'
|
180
216
|
docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/))
|
181
217
|
|
182
|
-
workdir = doc.attr 'docdir'
|
183
|
-
workdir = '.' if workdir.empty?
|
218
|
+
workdir = (workdir = doc.attr 'docdir').nil_or_empty? ? '.' : workdir
|
184
219
|
resources workdir: workdir do
|
185
220
|
images.each do |image|
|
186
221
|
imagesdir = (image.document.attr 'imagesdir', '.').chomp '/'
|
187
222
|
imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
|
188
223
|
image_path = %(#{imagesdir}#{image.attr 'target'})
|
189
224
|
if image_path.start_with? %(#{docimagesdir}jacket/cover.)
|
190
|
-
warn %(
|
225
|
+
warn %(asciidoctor: WARNING: image path is reserved for cover artwork: #{image_path}; skipping image found in content)
|
191
226
|
elsif ::File.readable? image_path
|
192
227
|
file image_path
|
193
228
|
else
|
194
|
-
warn %(
|
229
|
+
warn %(asciidoctor: ERROR: image not found or not readable: #{image_path})
|
195
230
|
end
|
196
231
|
end
|
197
232
|
end
|
233
|
+
nil
|
198
234
|
end
|
199
235
|
|
200
236
|
def add_profile_images doc, usernames
|
201
|
-
spine = @spine
|
202
237
|
imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
|
203
238
|
imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
|
204
239
|
|
@@ -207,15 +242,14 @@ body > svg {
|
|
207
242
|
file %(#{imagesdir}headshots/default.jpg) => ::File.join(DATA_DIR, 'images/default-headshot.jpg')
|
208
243
|
end
|
209
244
|
|
210
|
-
workdir = doc.attr 'docdir'
|
211
|
-
workdir = '.' if workdir.empty?
|
245
|
+
workdir = (workdir = doc.attr 'docdir').nil_or_empty? ? '.' : workdir
|
212
246
|
resources do
|
213
247
|
usernames.each do |username|
|
214
248
|
avatar = %(#{imagesdir}avatars/#{username}.jpg)
|
215
249
|
if ::File.readable?(resolved_avatar = ::File.join(workdir, avatar))
|
216
250
|
file avatar => resolved_avatar
|
217
251
|
else
|
218
|
-
warn %(
|
252
|
+
warn %(asciidoctor: ERROR: avatar for #{username} not found or readable: #{avatar}; falling back to default avatar)
|
219
253
|
file avatar => ::File.join(DATA_DIR, 'images/default-avatar.jpg')
|
220
254
|
end
|
221
255
|
|
@@ -223,55 +257,139 @@ body > svg {
|
|
223
257
|
if ::File.readable?(resolved_headshot = ::File.join(workdir, headshot))
|
224
258
|
file headshot => resolved_headshot
|
225
259
|
elsif doc.attr? 'builder', 'editions'
|
226
|
-
warn %(
|
260
|
+
warn %(asciidoctor: ERROR: headshot for #{username} not found or readable: #{headshot}; falling back to default headshot)
|
227
261
|
file headshot => ::File.join(DATA_DIR, 'images/default-headshot.jpg')
|
228
262
|
end
|
229
263
|
end
|
230
|
-
=begin
|
231
|
-
spine.each do |item|
|
232
|
-
username = (item.attr 'username') || 'default'
|
233
|
-
avatar_target = %(#{imagesdir}avatars/#{username}.jpg)
|
234
|
-
if ::File.readable?(avatar = %(#{item.attr 'docname'}/avatar.jpg))
|
235
|
-
file avatar_target => avatar
|
236
|
-
else
|
237
|
-
warn %(Avatar #{avatar} not found or not readable. Falling back to default avatar for #{username}.)
|
238
|
-
::Dir.chdir DATA_DIR do
|
239
|
-
file avatar_target => %(images/default-avatar.jpg)
|
240
|
-
end
|
241
|
-
end
|
242
|
-
if ::File.readable? (headshot = %(#{item.attr 'docname'}/headshot.jpg))
|
243
|
-
file headshot
|
244
|
-
# TODO default headshot?
|
245
|
-
end
|
246
|
-
end
|
247
|
-
=end
|
248
264
|
end
|
265
|
+
nil
|
249
266
|
end
|
250
267
|
|
251
268
|
def add_content doc
|
252
|
-
builder = self
|
253
|
-
|
254
|
-
format = @format
|
255
|
-
workdir = doc.attr 'docdir', '.'
|
256
|
-
workdir = '.' if workdir.empty?
|
269
|
+
builder, spine, format = self, @spine, @format
|
270
|
+
workdir = (doc.attr 'docdir').nil_or_empty? ? '.' : workdir
|
257
271
|
resources workdir: workdir do
|
272
|
+
extend GepubResourceBuilderMixin
|
258
273
|
builder.add_images_from_front_matter
|
259
|
-
|
260
|
-
|
261
|
-
nav 'nav.xhtml' => (builder.postprocess_xhtml ::Asciidoctor::Converter::Factory.default.create('epub3-xhtml5').navigation_document(doc, spine), format)
|
274
|
+
builder.add_nav_doc doc, self, spine, format
|
275
|
+
builder.add_ncx_doc doc, self, spine
|
262
276
|
ordered do
|
263
|
-
builder.add_cover_page doc, self, @book unless format == :kf8
|
264
|
-
builder.add_front_matter_page doc, self
|
277
|
+
builder.add_cover_page doc, self, @book.manifest unless format == :kf8
|
278
|
+
builder.add_front_matter_page doc, self
|
265
279
|
spine.each_with_index do |item, i|
|
266
|
-
|
267
|
-
|
268
|
-
#
|
269
|
-
heading builder.sanitized_doctitle(item)
|
270
|
-
@last_defined_item.properties << 'svg' if ((item.attr 'epub-properties') || []).include? 'svg'
|
280
|
+
file %(#{item.id || (item.attr 'docname')}.xhtml) => (builder.postprocess_xhtml item.convert, format)
|
281
|
+
add_property 'svg' if ((item.attr 'epub-properties') || []).include? 'svg'
|
282
|
+
# QUESTION reenable?
|
271
283
|
#linear 'yes' if i == 0
|
272
284
|
end
|
273
285
|
end
|
274
286
|
end
|
287
|
+
nil
|
288
|
+
end
|
289
|
+
|
290
|
+
def add_nav_doc doc, spine_builder, spine, format
|
291
|
+
spine_builder.nav 'nav.xhtml' => (postprocess_xhtml nav_doc(doc, spine), format)
|
292
|
+
spine_builder.id 'nav'
|
293
|
+
nil
|
294
|
+
end
|
295
|
+
|
296
|
+
# TODO aggregate authors of spine document into authors attribute(s) on main document
|
297
|
+
def nav_doc doc, spine
|
298
|
+
lines = [%(<!DOCTYPE html>
|
299
|
+
<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}">
|
300
|
+
<head>
|
301
|
+
<meta charset="UTF-8"/>
|
302
|
+
<title>#{sanitize_doctitle_xml doc, :cdata}</title>
|
303
|
+
<link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
|
304
|
+
<link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
|
305
|
+
</head>
|
306
|
+
<body>
|
307
|
+
<h1>#{sanitize_doctitle_xml doc, :pcdata}</h1>
|
308
|
+
<nav epub:type="toc" id="toc">
|
309
|
+
<h2>#{doc.attr 'toc-title'}</h2>)]
|
310
|
+
lines << (nav_level spine, [(doc.attr 'toclevels', 1).to_i, 0].max)
|
311
|
+
lines << %(</nav>
|
312
|
+
</body>
|
313
|
+
</html>)
|
314
|
+
lines * EOL
|
315
|
+
end
|
316
|
+
|
317
|
+
def nav_level items, depth, state = {}
|
318
|
+
lines = []
|
319
|
+
lines << '<ol>'
|
320
|
+
items.each do |item|
|
321
|
+
#index = (state[:index] = (state.fetch :index, 0) + 1)
|
322
|
+
if item.context == :document
|
323
|
+
# NOTE we sanitize the chapter titles because we use formatting to control layout
|
324
|
+
item_label = sanitize_doctitle_xml item, :cdata
|
325
|
+
item_href = (state[:content_doc_href] = %(#{item.id || (item.attr 'docname')}.xhtml))
|
326
|
+
else
|
327
|
+
item_label = sanitize_xml item.title, :pcdata
|
328
|
+
item_href = %(#{state[:content_doc_href]}##{item.id})
|
329
|
+
end
|
330
|
+
lines << %(<li><a href="#{item_href}">#{item_label}</a>)
|
331
|
+
unless depth == 0 || (child_sections = item.sections).empty?
|
332
|
+
lines << (nav_level child_sections, depth - 1, state)
|
333
|
+
lines << '</li>'
|
334
|
+
else
|
335
|
+
lines[-1] = %(#{lines[-1]}</li>)
|
336
|
+
end
|
337
|
+
state.delete :content_doc_href if item.context == :document
|
338
|
+
end
|
339
|
+
lines << '</ol>'
|
340
|
+
lines * EOL
|
341
|
+
end
|
342
|
+
|
343
|
+
# NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
|
344
|
+
def add_ncx_doc doc, spine_builder, spine
|
345
|
+
spine_builder.file 'toc.ncx' => (ncx_doc doc, spine).to_ios
|
346
|
+
spine_builder.id 'ncx'
|
347
|
+
nil
|
348
|
+
end
|
349
|
+
|
350
|
+
def ncx_doc doc, spine
|
351
|
+
# TODO populate docAuthor element based on unique authors in work
|
352
|
+
lines = [%(<?xml version="1.0" encoding="utf-8"?>
|
353
|
+
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="#{doc.attr 'lang', 'en'}">
|
354
|
+
<head>
|
355
|
+
<meta name="dtb:uid" content="#{@book.identifier}"/>
|
356
|
+
%{depth}
|
357
|
+
<meta name="dtb:totalPageCount" content="0"/>
|
358
|
+
<meta name="dtb:maxPageNumber" content="0"/>
|
359
|
+
</head>
|
360
|
+
<docTitle><text>#{sanitize_doctitle_xml doc, :cdata}</text></docTitle>
|
361
|
+
<navMap>)]
|
362
|
+
lines << (ncx_level spine, [(doc.attr 'toclevels', 1).to_i, 0].max, state = {})
|
363
|
+
lines[0] = lines[0].sub '%{depth}', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>)
|
364
|
+
lines << %(</navMap>
|
365
|
+
</ncx>)
|
366
|
+
lines * EOL
|
367
|
+
end
|
368
|
+
|
369
|
+
def ncx_level items, depth, state = {}
|
370
|
+
lines = []
|
371
|
+
state[:max_depth] = (state.fetch :max_depth, 0) + 1
|
372
|
+
items.each do |item|
|
373
|
+
index = (state[:index] = (state.fetch :index, 0) + 1)
|
374
|
+
if item.context == :document
|
375
|
+
item_id = %(nav_#{index})
|
376
|
+
item_label = sanitize_doctitle_xml item, :cdata
|
377
|
+
item_href = (state[:content_doc_href] = %(#{item.id || (item.attr 'docname')}.xhtml))
|
378
|
+
else
|
379
|
+
item_id = %(nav_#{index})
|
380
|
+
item_label = sanitize_xml item.title, :cdata
|
381
|
+
item_href = %(#{state[:content_doc_href]}##{item.id})
|
382
|
+
end
|
383
|
+
lines << %(<navPoint id="#{item_id}" playOrder="#{index}">)
|
384
|
+
lines << %(<navLabel><text>#{item_label}</text></navLabel>)
|
385
|
+
lines << %(<content src="#{item_href}"/>)
|
386
|
+
unless depth == 0 || (child_sections = item.sections).empty?
|
387
|
+
lines << (ncx_level child_sections, depth - 1, state)
|
388
|
+
end
|
389
|
+
lines << %(</navPoint>)
|
390
|
+
state.delete :content_doc_href if item.context == :document
|
391
|
+
end
|
392
|
+
lines * EOL
|
275
393
|
end
|
276
394
|
|
277
395
|
def collect_keywords doc, spine
|
@@ -327,10 +445,23 @@ body > svg {
|
|
327
445
|
end
|
328
446
|
end
|
329
447
|
|
448
|
+
module GepubResourceBuilderMixin
|
449
|
+
# Add missing method to builder to add a property to last defined item
|
450
|
+
def add_property property
|
451
|
+
@last_defined_item.add_property property
|
452
|
+
end
|
453
|
+
|
454
|
+
# Add helper method to builder to check if property is set on last defined item
|
455
|
+
def property? property
|
456
|
+
(@last_defined_item['properties'] || []).include? property
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
330
460
|
class Packager
|
331
461
|
KINDLEGEN = ENV['KINDLEGEN'] || 'kindlegen'
|
332
462
|
EPUBCHECK = ENV['EPUBCHECK'] || %(epubcheck#{::Gem.win_platform? ? '.bat' : '.sh'})
|
333
463
|
EpubExtensionRx = /\.epub$/i
|
464
|
+
KindlegenCompression = ::Hash['0', '-c0', '1', '-c1', '2', '-c2', 'none', '-c0', 'standard', '-c1', 'huffdic', '-c2']
|
334
465
|
|
335
466
|
def initialize spine_doc, spine, format = :epub3, options = {}
|
336
467
|
@document = spine_doc
|
@@ -345,8 +476,8 @@ class Packager
|
|
345
476
|
target = options[:target]
|
346
477
|
dest = File.dirname target
|
347
478
|
|
348
|
-
images = spine.map {|item|
|
349
|
-
|
479
|
+
images = spine.map {|item| item.find_by context: :image }.compact.flatten
|
480
|
+
.uniq {|img| %(#{(img.document.attr 'imagesdir', '.').chomp '/'}/#{img.attr 'target'}) }
|
350
481
|
# FIXME authors should be aggregated already on parent document
|
351
482
|
authors = if doc.attr? 'authors'
|
352
483
|
(doc.attr 'authors').split(GepubBuilderMixin::CsvDelimiterRx).concat(spine.map {|item| item.attr 'author' }).uniq
|
@@ -372,7 +503,8 @@ class Packager
|
|
372
503
|
# replace with next line once the attributes argument is supported
|
373
504
|
#unique_identifier doc.id, 'pub-id', 'uuid', 'scheme' => 'xsd:string'
|
374
505
|
|
375
|
-
|
506
|
+
# NOTE we must use :plain_text here since gepub reencodes
|
507
|
+
title(sanitize_doctitle_xml doc, :plain_text)
|
376
508
|
id 'pub-title'
|
377
509
|
|
378
510
|
# FIXME this logic needs some work
|
@@ -429,11 +561,14 @@ class Packager
|
|
429
561
|
rights(doc.attr 'copyright')
|
430
562
|
end
|
431
563
|
|
432
|
-
#add_metadata 'ibooks:specified-fonts', true
|
564
|
+
#add_metadata 'ibooks:specified-fonts', true
|
433
565
|
|
434
566
|
add_theme_assets doc
|
435
567
|
add_cover_image doc
|
436
|
-
|
568
|
+
if (doc.attr 'publication-type', 'book') != 'book'
|
569
|
+
usernames = spine.map {|item| item.attr 'username' }.compact.uniq
|
570
|
+
add_profile_images doc, usernames
|
571
|
+
end
|
437
572
|
# QUESTION move add_content_images to add_content method?
|
438
573
|
add_content_images doc, images
|
439
574
|
add_content doc
|
@@ -464,21 +599,22 @@ class Packager
|
|
464
599
|
|
465
600
|
if fmt == :kf8
|
466
601
|
# QUESTION shouldn't we validate this epub file too?
|
467
|
-
distill_epub_to_mobi epub_file, target
|
602
|
+
distill_epub_to_mobi epub_file, target, options[:compress]
|
468
603
|
elsif options[:validate]
|
469
604
|
validate_epub epub_file
|
470
605
|
end
|
471
606
|
end
|
472
607
|
|
473
|
-
|
474
|
-
def distill_epub_to_mobi epub_file, target
|
608
|
+
def distill_epub_to_mobi epub_file, target, compress
|
475
609
|
kindlegen_cmd = KINDLEGEN
|
476
610
|
unless ::File.executable? kindlegen_cmd
|
477
611
|
require 'kindlegen' unless defined? ::Kindlegen
|
478
612
|
kindlegen_cmd = ::Kindlegen.command
|
479
613
|
end
|
480
614
|
mobi_file = ::File.basename(target.sub EpubExtensionRx, '.mobi')
|
481
|
-
|
615
|
+
compress_flag = KindlegenCompression[compress ? (compress.empty? ? '1' : compress.to_s) : '0']
|
616
|
+
cmd = [kindlegen_cmd, '-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
|
617
|
+
::Open3.popen2e(::Shellwords.join cmd) {|input, output, wait_thr|
|
482
618
|
output.each {|line| puts line } unless $VERBOSE.nil?
|
483
619
|
}
|
484
620
|
puts %(Wrote MOBI to #{::File.join ::File.dirname(epub_file), mobi_file}) if $VERBOSE
|
@@ -487,7 +623,7 @@ class Packager
|
|
487
623
|
def validate_epub epub_file
|
488
624
|
epubcheck_cmd = EPUBCHECK
|
489
625
|
unless ::File.executable? epubcheck_cmd
|
490
|
-
epubcheck_cmd = ::Gem.bin_path 'epubcheck', 'epubcheck'
|
626
|
+
epubcheck_cmd = ::Gem.bin_path 'epubcheck', 'epubcheck'
|
491
627
|
end
|
492
628
|
# NOTE epubcheck gem doesn't support epubcheck command options; enable -quiet once supported
|
493
629
|
::Open3.popen2e(::Shellwords.join [epubcheck_cmd, epub_file]) {|input, output, wait_thr|
|