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.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +56 -1
- data/Gemfile +10 -6
- data/LICENSE +21 -0
- data/NOTICE.adoc +3 -3
- data/README.adoc +62 -151
- data/asciidoctor-epub3.gemspec +9 -6
- data/data/fonts/awesome/LICENSE.txt +34 -0
- data/data/fonts/awesome/fa-solid-900.ttf +0 -0
- data/data/fonts/awesome/icons.yml +20935 -0
- data/data/fonts/awesome/shims.yml +298 -0
- data/data/styles/epub3-css3-only.css +3 -8
- data/data/styles/epub3-fonts.css +1 -1
- data/data/styles/epub3.css +22 -15
- data/lib/asciidoctor-epub3.rb +0 -1
- data/lib/asciidoctor-epub3/converter.rb +1062 -236
- data/lib/asciidoctor-epub3/font_icon_map.rb +22 -371
- data/lib/asciidoctor-epub3/version.rb +1 -1
- metadata +80 -38
- data/.yardopts +0 -12
- data/LICENSE.adoc +0 -25
- data/data/fonts/fontawesome-icons.ttf +0 -0
- data/lib/asciidoctor-epub3/packager.rb +0 -722
- data/lib/asciidoctor-epub3/spine_item_processor.rb +0 -92
data/lib/asciidoctor-epub3.rb
CHANGED
@@ -5,7 +5,6 @@ require 'asciidoctor/extensions'
|
|
5
5
|
require 'gepub'
|
6
6
|
require_relative 'asciidoctor-epub3/ext'
|
7
7
|
require_relative 'asciidoctor-epub3/converter'
|
8
|
-
require_relative 'asciidoctor-epub3/packager'
|
9
8
|
|
10
9
|
# We need to be able to write files with unicode names. See https://github.com/asciidoctor/asciidoctor-epub3/issues/217
|
11
10
|
::Zip.unicode_names = true
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require 'mime/types'
|
4
|
+
require 'open3'
|
4
5
|
require_relative 'font_icon_map'
|
5
6
|
|
6
7
|
module Asciidoctor
|
@@ -14,56 +15,45 @@ module Asciidoctor
|
|
14
15
|
|
15
16
|
register_for 'epub3'
|
16
17
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
logger.error %(#{::File.basename node.document.attr('docfile')}: failed to find spine items, produced file will be invalid)
|
38
|
-
spine_items = []
|
39
|
-
end
|
40
|
-
Packager.new node, spine_items, node.attributes['ebook-format'].to_sym
|
41
|
-
# converting an element from the spine document, such as an inline node in the doctitle
|
42
|
-
elsif name.start_with? 'inline_'
|
43
|
-
(@content_converter ||= ::Asciidoctor::Converter::Factory.default.create 'epub3-xhtml5').convert node, name
|
44
|
-
else
|
45
|
-
raise ::ArgumentError, %(Encountered unexpected node in epub3 package converter: #{name})
|
18
|
+
def write output, target
|
19
|
+
epub_file = @format == :kf8 ? %(#{::Asciidoctor::Helpers.rootname target}-kf8.epub) : target
|
20
|
+
output.generate_epub epub_file
|
21
|
+
logger.debug %(Wrote #{@format.upcase} to #{epub_file})
|
22
|
+
if @extract
|
23
|
+
extract_dir = epub_file.sub EpubExtensionRx, ''
|
24
|
+
::FileUtils.remove_dir extract_dir if ::File.directory? extract_dir
|
25
|
+
::Dir.mkdir extract_dir
|
26
|
+
::Dir.chdir extract_dir do
|
27
|
+
::Zip::File.open epub_file do |entries|
|
28
|
+
entries.each do |entry|
|
29
|
+
next unless entry.file?
|
30
|
+
unless (entry_dir = ::File.dirname entry.name) == '.' || (::File.directory? entry_dir)
|
31
|
+
::FileUtils.mkdir_p entry_dir
|
32
|
+
end
|
33
|
+
entry.extract entry.name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
logger.debug %(Extracted #{@format.upcase} to #{extract_dir})
|
46
38
|
end
|
47
|
-
end
|
48
39
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
40
|
+
if @format == :kf8
|
41
|
+
# QUESTION shouldn't we validate this epub file too?
|
42
|
+
distill_epub_to_mobi epub_file, target, @compress, @kindlegen_path
|
43
|
+
elsif @validate
|
44
|
+
validate_epub epub_file, @epubcheck_path
|
45
|
+
end
|
53
46
|
end
|
54
|
-
end
|
55
47
|
|
56
|
-
|
57
|
-
# content documents in an EPUB3 publication.
|
58
|
-
class ContentConverter
|
59
|
-
include ::Asciidoctor::Converter
|
60
|
-
include ::Asciidoctor::Logging
|
48
|
+
CsvDelimiterRx = /\s*,\s*/
|
61
49
|
|
62
|
-
|
50
|
+
DATA_DIR = ::File.expand_path ::File.join(__dir__, '..', '..', 'data')
|
51
|
+
ImageMacroRx = /^image::?(.*?)\[(.*?)\]$/
|
52
|
+
ImgSrcScanRx = /<img src="(.+?)"/
|
53
|
+
SvgImgSniffRx = /<img src=".+?\.svg"/
|
63
54
|
|
64
55
|
LF = ?\n
|
65
56
|
NoBreakSpace = ' '
|
66
|
-
ThinNoBreakSpace = ' '
|
67
57
|
RightAngleQuote = '›'
|
68
58
|
CalloutStartNum = %(\u2460)
|
69
59
|
|
@@ -87,13 +77,27 @@ module Asciidoctor
|
|
87
77
|
|
88
78
|
ToHtmlSpecialCharsRx = /[#{ToHtmlSpecialCharsMap.keys.join}]/
|
89
79
|
|
90
|
-
|
80
|
+
EpubExtensionRx = /\.epub$/i
|
81
|
+
KindlegenCompression = ::Hash['0', '-c0', '1', '-c1', '2', '-c2', 'none', '-c0', 'standard', '-c1', 'huffdic', '-c2']
|
82
|
+
|
83
|
+
(QUOTE_TAGS = {
|
84
|
+
monospaced: ['<code>', '</code>', true],
|
85
|
+
emphasis: ['<em>', '</em>', true],
|
86
|
+
strong: ['<strong>', '</strong>', true],
|
87
|
+
double: ['“', '”'],
|
88
|
+
single: ['‘', '’'],
|
89
|
+
mark: ['<mark>', '</mark>', true],
|
90
|
+
superscript: ['<sup>', '</sup>', true],
|
91
|
+
subscript: ['<sub>', '</sub>', true],
|
92
|
+
asciimath: ['<code>', '</code>', true],
|
93
|
+
latexmath: ['<code>', '</code>', true],
|
94
|
+
}).default = ['', '']
|
95
|
+
|
96
|
+
def initialize backend, opts = {}
|
91
97
|
super
|
92
98
|
basebackend 'html'
|
93
|
-
outfilesuffix '.
|
99
|
+
outfilesuffix '.epub' # dummy outfilesuffix since it may be .mobi
|
94
100
|
htmlsyntax 'xml'
|
95
|
-
@xrefs_seen = ::Set.new
|
96
|
-
@icon_names = []
|
97
101
|
end
|
98
102
|
|
99
103
|
def convert node, name = nil, _opts = {}
|
@@ -101,37 +105,234 @@ module Asciidoctor
|
|
101
105
|
if respond_to? method_name
|
102
106
|
send method_name, node
|
103
107
|
else
|
104
|
-
logger.warn %(conversion missing in backend #{@backend} for #{name})
|
108
|
+
logger.warn %(#{::File.basename node.attr('docfile')}: conversion missing in backend #{@backend} for #{name})
|
109
|
+
nil
|
105
110
|
end
|
106
111
|
end
|
107
112
|
|
113
|
+
# See https://asciidoctor.org/docs/user-manual/#book-parts-and-chapters
|
114
|
+
def get_chapter_name node
|
115
|
+
if node.document.doctype != 'book'
|
116
|
+
return Asciidoctor::Document === node ? node.attr('docname') || node.id : nil
|
117
|
+
end
|
118
|
+
return (node.id || 'preamble') if node.context == :preamble && node.level == 0
|
119
|
+
chapter_level = [node.document.attr('epub-chapter-level', 1).to_i, 1].max
|
120
|
+
Asciidoctor::Section === node && node.level <= chapter_level ? node.id : nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_numbered_title node
|
124
|
+
doc_attrs = node.document.attributes
|
125
|
+
level = node.level
|
126
|
+
if node.caption
|
127
|
+
title = node.captioned_title
|
128
|
+
elsif node.respond_to?(:numbered) && node.numbered && level <= (doc_attrs['sectnumlevels'] || 3).to_i
|
129
|
+
if level < 2 && node.document.doctype == 'book'
|
130
|
+
if node.sectname == 'chapter'
|
131
|
+
title = %(#{(signifier = doc_attrs['chapter-signifier']) ? "#{signifier} " : ''}#{node.sectnum} #{node.title})
|
132
|
+
elsif node.sectname == 'part'
|
133
|
+
title = %(#{(signifier = doc_attrs['part-signifier']) ? "#{signifier} " : ''}#{node.sectnum nil, ':'} #{node.title})
|
134
|
+
else
|
135
|
+
title = %(#{node.sectnum} #{node.title})
|
136
|
+
end
|
137
|
+
else
|
138
|
+
title = %(#{node.sectnum} #{node.title})
|
139
|
+
end
|
140
|
+
else
|
141
|
+
title = node.title
|
142
|
+
end
|
143
|
+
title
|
144
|
+
end
|
145
|
+
|
108
146
|
def convert_document node
|
109
|
-
|
110
|
-
|
147
|
+
@format = node.attr('ebook-format').to_sym
|
148
|
+
|
149
|
+
@validate = node.attr? 'ebook-validate'
|
150
|
+
@extract = node.attr? 'ebook-extract'
|
151
|
+
@compress = node.attr 'ebook-compress'
|
152
|
+
@kindlegen_path = node.attr 'ebook-kindlegen-path'
|
153
|
+
@epubcheck_path = node.attr 'ebook-epubcheck-path'
|
154
|
+
@xrefs_seen = ::Set.new
|
155
|
+
@icon_names = []
|
156
|
+
@media_files = []
|
157
|
+
@footnotes = []
|
158
|
+
|
159
|
+
@book = GEPUB::Book.new 'EPUB/package.opf'
|
160
|
+
@book.epub_backward_compat = @format != :kf8
|
161
|
+
@book.language node.attr('lang', 'en'), id: 'pub-language'
|
162
|
+
|
163
|
+
if node.attr? 'uuid'
|
164
|
+
@book.primary_identifier node.attr('uuid'), 'pub-identifier', 'uuid'
|
165
|
+
else
|
166
|
+
@book.primary_identifier node.id, 'pub-identifier', 'uuid'
|
167
|
+
end
|
168
|
+
# replace with next line once the attributes argument is supported
|
169
|
+
#unique_identifier doc.id, 'pub-id', 'uuid', 'scheme' => 'xsd:string'
|
170
|
+
|
171
|
+
# NOTE we must use :plain_text here since gepub reencodes
|
172
|
+
@book.add_title sanitize_doctitle_xml(node, :plain_text), id: 'pub-title'
|
173
|
+
|
174
|
+
# see https://www.w3.org/publishing/epub3/epub-packages.html#sec-opf-dccreator
|
175
|
+
(1..(node.attr 'authorcount', 1).to_i).map do |idx|
|
176
|
+
author = node.attr(idx == 1 ? 'author' : %(author_#{idx}))
|
177
|
+
@book.add_creator author, role: 'aut' unless author.nil_or_empty?
|
178
|
+
end
|
179
|
+
|
180
|
+
publisher = node.attr 'publisher'
|
181
|
+
# NOTE Use producer as both publisher and producer if publisher isn't specified
|
182
|
+
publisher = node.attr 'producer' if publisher.nil_or_empty?
|
183
|
+
@book.publisher = publisher unless publisher.nil_or_empty?
|
184
|
+
|
185
|
+
if node.attr? 'reproducible'
|
186
|
+
# We need to set lastmodified to some fixed value. Otherwise, gepub will set it to current date.
|
187
|
+
@book.lastmodified = (::Time.at 0).utc
|
188
|
+
# Is it correct that we do not populate dc:date when 'reproducible' is set?
|
189
|
+
else
|
190
|
+
if node.attr? 'revdate'
|
191
|
+
begin
|
192
|
+
@book.date = node.attr 'revdate'
|
193
|
+
rescue ArgumentError => e
|
194
|
+
logger.error %(#{::File.basename node.attr('docfile')}: failed to parse revdate: #{e})
|
195
|
+
@book.date = node.attr 'docdatetime'
|
196
|
+
end
|
197
|
+
else
|
198
|
+
@book.date = node.attr 'docdatetime'
|
199
|
+
end
|
200
|
+
@book.lastmodified = node.attr 'localdatetime'
|
201
|
+
end
|
111
202
|
|
112
|
-
|
203
|
+
@book.description = node.attr 'description' if node.attr? 'description'
|
204
|
+
@book.source = node.attr 'source' if node.attr? 'source'
|
205
|
+
@book.rights = node.attr 'copyright' if node.attr? 'copyright'
|
206
|
+
|
207
|
+
(node.attr 'keywords', '').split(CsvDelimiterRx).each do |s|
|
208
|
+
@book.metadata.add_metadata 'subject', s
|
209
|
+
end
|
210
|
+
|
211
|
+
if node.attr? 'series-name'
|
212
|
+
series_name = node.attr 'series-name'
|
213
|
+
series_volume = node.attr 'series-volume', 1
|
214
|
+
series_id = node.attr 'series-id'
|
215
|
+
|
216
|
+
series_meta = @book.metadata.add_metadata 'meta', series_name, id: 'pub-collection', group_position: series_volume
|
217
|
+
series_meta['property'] = 'belongs-to-collection'
|
218
|
+
series_meta.refine 'dcterms:identifier', series_id unless series_id.nil?
|
219
|
+
# Calibre only understands 'series'
|
220
|
+
series_meta.refine 'collection-type', 'series'
|
221
|
+
end
|
222
|
+
|
223
|
+
add_cover_image node
|
224
|
+
add_front_matter_page node
|
225
|
+
|
226
|
+
if node.doctype == 'book'
|
227
|
+
toc_items = node.sections
|
228
|
+
node.content
|
229
|
+
else
|
230
|
+
toc_items = [node]
|
231
|
+
add_chapter node
|
232
|
+
end
|
233
|
+
|
234
|
+
nav_xhtml = @book.add_item 'nav.xhtml', content: postprocess_xhtml(nav_doc(node, toc_items)), id: 'nav'
|
235
|
+
nav_xhtml.nav
|
236
|
+
|
237
|
+
# NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
|
238
|
+
toc_ncx = ncx_doc node, toc_items
|
239
|
+
@book.add_item 'toc.ncx', content: toc_ncx.to_ios, id: 'ncx'
|
240
|
+
|
241
|
+
docimagesdir = (node.attr 'imagesdir', '.').chomp '/'
|
242
|
+
docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/))
|
243
|
+
|
244
|
+
@media_files.each do |file|
|
245
|
+
if file[:name].start_with? %(#{docimagesdir}jacket/cover.)
|
246
|
+
logger.warn %(path is reserved for cover artwork: #{file[:name]}; skipping file found in content)
|
247
|
+
elsif file[:path].nil? || File.readable?(file[:path])
|
248
|
+
mime_types = MIME::Types.type_for file[:name]
|
249
|
+
mime_types.delete_if {|x| x.media_type != file[:media_type] }
|
250
|
+
preferred_mime_type = mime_types.empty? ? nil : mime_types[0].content_type
|
251
|
+
@book.add_item file[:name], content: file[:path], media_type: preferred_mime_type
|
252
|
+
else
|
253
|
+
logger.error %(#{File.basename node.attr('docfile')}: media file not found or not readable: #{file[:path]})
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
#add_metadata 'ibooks:specified-fonts', true
|
258
|
+
|
259
|
+
add_theme_assets node
|
260
|
+
if node.doctype != 'book'
|
261
|
+
usernames = [node].map {|item| item.attr 'username' }.compact.uniq
|
262
|
+
add_profile_images node, usernames
|
263
|
+
end
|
264
|
+
|
265
|
+
@book
|
266
|
+
end
|
267
|
+
|
268
|
+
# FIXME: move to Asciidoctor::Helpers
|
269
|
+
def sanitize_doctitle_xml doc, content_spec
|
270
|
+
doctitle = doc.doctitle use_fallback: true
|
271
|
+
sanitize_xml doctitle, content_spec
|
272
|
+
end
|
273
|
+
|
274
|
+
# FIXME: move to Asciidoctor::Helpers
|
275
|
+
def sanitize_xml content, content_spec
|
276
|
+
if content_spec != :pcdata && (content.include? '<')
|
277
|
+
if (content = (content.gsub XmlElementRx, '').strip).include? ' '
|
278
|
+
content = content.tr_s ' ', ' '
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
case content_spec
|
283
|
+
when :attribute_cdata
|
284
|
+
content = content.gsub '"', '"' if content.include? '"'
|
285
|
+
when :cdata, :pcdata
|
286
|
+
# noop
|
287
|
+
when :plain_text
|
288
|
+
if content.include? ';'
|
289
|
+
content = content.gsub(CharEntityRx) { [$1.to_i].pack 'U*' } if content.include? '&#'
|
290
|
+
content = content.gsub FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap
|
291
|
+
end
|
292
|
+
else
|
293
|
+
raise ::ArgumentError, %(Unknown content spec: #{content_spec})
|
294
|
+
end
|
295
|
+
content
|
296
|
+
end
|
297
|
+
|
298
|
+
def add_chapter node
|
299
|
+
docid = get_chapter_name node
|
300
|
+
return nil if docid.nil?
|
301
|
+
|
302
|
+
chapter_item = @book.add_ordered_item %(#{docid}.xhtml)
|
303
|
+
|
304
|
+
if node.context == :document && (doctitle = node.doctitle partition: true, use_fallback: true).subtitle?
|
113
305
|
title = %(#{doctitle.main} )
|
114
306
|
subtitle = doctitle.subtitle
|
115
|
-
|
307
|
+
elsif node.title
|
116
308
|
# HACK: until we get proper handling of title-only in CSS
|
117
309
|
title = ''
|
118
|
-
subtitle =
|
310
|
+
subtitle = get_numbered_title node
|
311
|
+
else
|
312
|
+
title = nil
|
313
|
+
subtitle = nil
|
119
314
|
end
|
120
315
|
|
121
|
-
doctitle_sanitized = (node.doctitle sanitize: true, use_fallback: true).to_s
|
122
|
-
|
316
|
+
doctitle_sanitized = (node.document.doctitle sanitize: true, use_fallback: true).to_s
|
317
|
+
|
318
|
+
# By default, Kindle does not allow the line height to be adjusted.
|
319
|
+
# But if you float the elements, then the line height disappears and can be restored manually using margins.
|
320
|
+
# See https://github.com/asciidoctor/asciidoctor-epub3/issues/123
|
321
|
+
subtitle_formatted = subtitle ? subtitle.split.map {|w| %(<b>#{w}</b>) } * ' ' : nil
|
123
322
|
|
124
|
-
if
|
323
|
+
if node.document.doctype == 'book'
|
125
324
|
byline = ''
|
126
325
|
else
|
127
326
|
author = node.attr 'author'
|
128
327
|
username = node.attr 'username', 'default'
|
129
|
-
imagesdir = (node.
|
328
|
+
imagesdir = (node.document.attr 'imagesdir', '.').chomp '/'
|
130
329
|
imagesdir = imagesdir == '.' ? '' : %(#{imagesdir}/)
|
131
330
|
byline = %(<p class="byline"><img src="#{imagesdir}avatars/#{username}.jpg"/> <b class="author">#{author}</b></p>#{LF})
|
132
331
|
end
|
133
332
|
|
134
|
-
mark_last_paragraph node unless
|
333
|
+
mark_last_paragraph node unless node.document.doctype == 'book'
|
334
|
+
|
335
|
+
@xrefs_seen.clear
|
135
336
|
content = node.content
|
136
337
|
|
137
338
|
# NOTE must run after content is resolved
|
@@ -140,7 +341,7 @@ module Asciidoctor
|
|
140
341
|
icon_css_head = ''
|
141
342
|
else
|
142
343
|
icon_defs = @icon_names.map {|name|
|
143
|
-
%(.i-#{name}::before { content: "#{FontIconMap
|
344
|
+
%(.i-#{name}::before { content: "#{FontIconMap.unicode name}"; })
|
144
345
|
} * LF
|
145
346
|
icon_css_head = %(<style>
|
146
347
|
#{icon_defs}
|
@@ -148,9 +349,18 @@ module Asciidoctor
|
|
148
349
|
)
|
149
350
|
end
|
150
351
|
|
352
|
+
header = (title || subtitle) ? %(<header>
|
353
|
+
<div class="chapter-header">
|
354
|
+
#{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle_formatted}</small>) : ''}</h1>
|
355
|
+
</div>
|
356
|
+
</header>) : ''
|
357
|
+
|
358
|
+
# TODO : support writing code highlighter CSS to a separate file
|
359
|
+
linkcss = false
|
360
|
+
|
151
361
|
# NOTE kindlegen seems to mangle the <header> element, so we wrap its content in a div
|
152
362
|
lines = [%(<!DOCTYPE html>
|
153
|
-
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = node.attr 'lang', 'en'}" lang="#{lang}">
|
363
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = node.document.attr 'lang', 'en'}" lang="#{lang}">
|
154
364
|
<head>
|
155
365
|
<meta charset="UTF-8"/>
|
156
366
|
<title>#{doctitle_sanitized}</title>
|
@@ -164,23 +374,26 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
164
374
|
}
|
165
375
|
document.body.setAttribute('class', reader.name.toLowerCase().replace(/ /g, '-'));
|
166
376
|
});
|
167
|
-
]]></script>
|
168
|
-
|
377
|
+
]]></script>)]
|
378
|
+
|
379
|
+
if self.class.supports_highlighter_docinfo? && (syntax_hl = node.document.syntax_highlighter) && (syntax_hl.docinfo? :head)
|
380
|
+
lines << (syntax_hl.docinfo :head, node, linkcss: linkcss, self_closing_tag_slash: '/')
|
381
|
+
end
|
382
|
+
|
383
|
+
lines << %(</head>
|
169
384
|
<body>
|
170
385
|
<section class="chapter" title="#{doctitle_sanitized.gsub '"', '"'}" epub:type="chapter" id="#{docid}">
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
#{content})]
|
386
|
+
#{header}
|
387
|
+
#{content})
|
388
|
+
|
389
|
+
unless (fns = node.document.footnotes - @footnotes).empty?
|
390
|
+
@footnotes += fns
|
177
391
|
|
178
|
-
if node.footnotes?
|
179
392
|
# NOTE kindlegen seems to mangle the <footer> element, so we wrap its content in a div
|
180
393
|
lines << '<footer>
|
181
394
|
<div class="chapter-footer">
|
182
395
|
<div class="footnotes">'
|
183
|
-
|
396
|
+
fns.each do |footnote|
|
184
397
|
lines << %(<aside id="note-#{footnote.index}" epub:type="footnote">
|
185
398
|
<p><sup class="noteref"><a href="#noteref-#{footnote.index}">#{footnote.index}</a></sup> #{footnote.text}</p>
|
186
399
|
</aside>)
|
@@ -190,44 +403,53 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
190
403
|
</footer>'
|
191
404
|
end
|
192
405
|
|
193
|
-
lines << '</section>
|
194
|
-
|
406
|
+
lines << '</section>'
|
407
|
+
|
408
|
+
lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl && (syntax_hl.docinfo? :footer)
|
409
|
+
|
410
|
+
lines << '</body>
|
195
411
|
</html>'
|
196
412
|
|
197
|
-
lines * LF
|
198
|
-
|
413
|
+
chapter_item.add_content postprocess_xhtml lines * LF
|
414
|
+
epub_properties = node.attr 'epub-properties'
|
415
|
+
chapter_item.add_property 'svg' if epub_properties&.include? 'svg'
|
199
416
|
|
200
|
-
|
201
|
-
|
202
|
-
|
417
|
+
# # QUESTION reenable?
|
418
|
+
# #linear 'yes' if i == 0
|
419
|
+
|
420
|
+
chapter_item
|
203
421
|
end
|
204
422
|
|
205
423
|
def convert_section node
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
424
|
+
if add_chapter(node).nil?
|
425
|
+
hlevel = node.level
|
426
|
+
epub_type_attr = node.special ? %( epub:type="#{node.sectname}") : ''
|
427
|
+
div_classes = [%(sect#{node.level}), node.role].compact
|
428
|
+
title = get_numbered_title node
|
429
|
+
title_sanitized = xml_sanitize title
|
212
430
|
%(<section class="#{div_classes * ' '}" title="#{title_sanitized}"#{epub_type_attr}>
|
213
431
|
<h#{hlevel} id="#{node.id}">#{title}</h#{hlevel}>#{(content = node.content).empty? ? '' : %(
|
214
432
|
#{content})}
|
215
433
|
</section>)
|
216
|
-
else
|
217
|
-
# document has no level-0 heading and this heading serves as the document title
|
218
|
-
node.content
|
219
434
|
end
|
220
435
|
end
|
221
436
|
|
437
|
+
# NOTE embedded is used for AsciiDoc table cell content
|
438
|
+
def convert_embedded node
|
439
|
+
node.content
|
440
|
+
end
|
441
|
+
|
222
442
|
# TODO: support use of quote block as abstract
|
223
443
|
def convert_preamble node
|
224
|
-
if (
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
444
|
+
if add_chapter(node).nil?
|
445
|
+
if (first_block = node.blocks[0]) && first_block.style == 'abstract'
|
446
|
+
convert_abstract first_block
|
447
|
+
# REVIEW: should we treat the preamble as an abstract in general?
|
448
|
+
elsif first_block && node.blocks.size == 1
|
449
|
+
convert_abstract first_block
|
450
|
+
else
|
451
|
+
node.content
|
452
|
+
end
|
231
453
|
end
|
232
454
|
end
|
233
455
|
|
@@ -250,15 +472,16 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
250
472
|
end
|
251
473
|
|
252
474
|
def convert_paragraph node
|
475
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
253
476
|
role = node.role
|
254
477
|
# stack-head is the alternative to the default, inline-head (where inline means "run-in")
|
255
478
|
head_stop = node.attr 'head-stop', (role && (node.has_role? 'stack-head') ? nil : '.')
|
256
479
|
head = node.title? ? %(<strong class="head">#{title = node.title}#{head_stop && title !~ TrailingPunctRx ? head_stop : ''}</strong> ) : ''
|
257
480
|
if role
|
258
481
|
node.set_option 'hardbreaks' if node.has_role? 'signature'
|
259
|
-
%(<p class="#{role}">#{head}#{node.content}</p>)
|
482
|
+
%(<p#{id_attr} class="#{role}">#{head}#{node.content}</p>)
|
260
483
|
else
|
261
|
-
%(<p>#{head}#{node.content}</p>)
|
484
|
+
%(<p#{id_attr}>#{head}#{node.content}</p>)
|
262
485
|
end
|
263
486
|
end
|
264
487
|
|
@@ -292,6 +515,9 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
292
515
|
'note'
|
293
516
|
when 'important', 'warning', 'caution'
|
294
517
|
'warning'
|
518
|
+
else
|
519
|
+
logger.warn %(unknown admonition type: #{type})
|
520
|
+
'note'
|
295
521
|
end
|
296
522
|
%(<aside#{id_attr} class="admonition #{type}"#{title_attr} epub:type="#{epub_type}">
|
297
523
|
#{title_el}<div class="content">
|
@@ -318,18 +544,35 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
318
544
|
end
|
319
545
|
|
320
546
|
def convert_listing node
|
547
|
+
nowrap = (node.option? 'nowrap') || !(node.document.attr? 'prewrap')
|
548
|
+
if node.style == 'source'
|
549
|
+
lang = node.attr 'language'
|
550
|
+
if self.class.supports_highlighter_docinfo? && (syntax_hl = node.document.syntax_highlighter)
|
551
|
+
opts = syntax_hl.highlight? ? {
|
552
|
+
css_mode: ((doc_attrs = node.document.attributes)[%(#{syntax_hl.name}-css)] || :class).to_sym,
|
553
|
+
style: doc_attrs[%(#{syntax_hl.name}-style)],
|
554
|
+
} : {}
|
555
|
+
opts[:nowrap] = nowrap
|
556
|
+
else
|
557
|
+
pre_open = %(<pre class="highlight#{nowrap ? ' nowrap' : ''}"><code#{lang ? %( class="language-#{lang}" data-lang="#{lang}") : ''}>)
|
558
|
+
pre_close = '</code></pre>'
|
559
|
+
end
|
560
|
+
else
|
561
|
+
pre_open = %(<pre#{nowrap ? ' class="nowrap"' : ''}>)
|
562
|
+
pre_close = '</pre>'
|
563
|
+
syntax_hl = nil
|
564
|
+
end
|
321
565
|
figure_classes = ['listing']
|
322
566
|
figure_classes << 'coalesce' if node.option? 'unbreakable'
|
323
|
-
|
324
|
-
|
325
|
-
) : ''
|
326
|
-
# patches conums to fix extra or missing leading space
|
327
|
-
# TODO remove patch once upgrading to Asciidoctor 1.5.6
|
328
|
-
%(<figure class="#{figure_classes * ' '}">
|
329
|
-
#{title_div}<pre class="#{pre_classes * ' '}"><code>#{(node.content || '').gsub(/(?<! )<i class="conum"| +<i class="conum"/, ' <i class="conum"')}</code></pre>
|
567
|
+
title_div = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
|
568
|
+
%(<figure class="#{figure_classes * ' '}">#{title_div}
|
569
|
+
#{syntax_hl ? (syntax_hl.format node, lang, opts) : pre_open + (node.content || '') + pre_close}
|
330
570
|
</figure>)
|
331
571
|
end
|
332
572
|
|
573
|
+
# TODO: implement proper stem support. See https://github.com/asciidoctor/asciidoctor-epub3/issues/10
|
574
|
+
alias convert_stem convert_listing
|
575
|
+
|
333
576
|
# QUESTION should we wrap the <pre> in either <div> or <figure>?
|
334
577
|
def convert_literal node
|
335
578
|
%(<pre class="screen">#{node.content}</pre>)
|
@@ -344,7 +587,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
344
587
|
end
|
345
588
|
|
346
589
|
def convert_quote node
|
347
|
-
id_attr = %( id="#{node.id}")
|
590
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
348
591
|
class_attr = (role = node.role) ? %( class="blockquote #{role}") : ' class="blockquote"'
|
349
592
|
|
350
593
|
footer_content = []
|
@@ -370,7 +613,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
370
613
|
end
|
371
614
|
|
372
615
|
def convert_verse node
|
373
|
-
id_attr = %( id="#{node.id}")
|
616
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
374
617
|
class_attr = (role = node.role) ? %( class="verse #{role}") : ' class="verse"'
|
375
618
|
|
376
619
|
footer_content = []
|
@@ -450,7 +693,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
450
693
|
# end
|
451
694
|
#end
|
452
695
|
lines << '</colgroup>'
|
453
|
-
[:head, :
|
696
|
+
[:head, :body, :foot].reject {|tsec| node.rows[tsec].empty? }.each do |tsec|
|
454
697
|
lines << %(<t#{tsec}>)
|
455
698
|
node.rows[tsec].each do |row|
|
456
699
|
lines << '<tr>'
|
@@ -513,16 +756,30 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
513
756
|
# TODO: add complex class if list has nested blocks
|
514
757
|
def convert_dlist node
|
515
758
|
lines = []
|
759
|
+
id_attribute = node.id ? %( id="#{node.id}") : ''
|
760
|
+
|
761
|
+
classes = case node.style
|
762
|
+
when 'horizontal'
|
763
|
+
['hdlist', node.role]
|
764
|
+
when 'itemized', 'ordered'
|
765
|
+
# QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
|
766
|
+
['dlist', %(#{node.style}-list), node.role]
|
767
|
+
else
|
768
|
+
['description-list']
|
769
|
+
end.compact
|
770
|
+
|
771
|
+
class_attribute = %( class="#{classes.join ' '}")
|
772
|
+
|
773
|
+
lines << %(<div#{id_attribute}#{class_attribute}>)
|
774
|
+
lines << %(<div class="title">#{node.title}</div>) if node.title?
|
775
|
+
|
516
776
|
case (style = node.style)
|
517
777
|
when 'itemized', 'ordered'
|
518
778
|
list_tag_name = style == 'itemized' ? 'ul' : 'ol'
|
519
779
|
role = node.role
|
520
780
|
subject_stop = node.attr 'subject-stop', (role && (node.has_role? 'stack') ? nil : ':')
|
521
|
-
# QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
|
522
|
-
div_classes = [%(#{style}-list), role].compact
|
523
781
|
list_class_attr = (node.option? 'brief') ? ' class="brief"' : ''
|
524
|
-
lines << %(
|
525
|
-
<#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
|
782
|
+
lines << %(<#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
|
526
783
|
node.items.each do |subjects, dd|
|
527
784
|
# consists of one term (a subject) and supporting content
|
528
785
|
subject = [*subjects].first.text
|
@@ -538,11 +795,40 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
538
795
|
end
|
539
796
|
lines << '</li>'
|
540
797
|
end
|
541
|
-
lines << %(</#{list_tag_name}>
|
542
|
-
|
798
|
+
lines << %(</#{list_tag_name}>)
|
799
|
+
when 'horizontal'
|
800
|
+
lines << '<table>'
|
801
|
+
if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
|
802
|
+
lines << '<colgroup>'
|
803
|
+
col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : ''
|
804
|
+
lines << %(<col#{col_style_attribute} />)
|
805
|
+
col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : ''
|
806
|
+
lines << %(<col#{col_style_attribute} />)
|
807
|
+
lines << '</colgroup>'
|
808
|
+
end
|
809
|
+
node.items.each do |terms, dd|
|
810
|
+
lines << '<tr>'
|
811
|
+
lines << %(<td class="hdlist1#{(node.option? 'strong') ? ' strong' : ''}">)
|
812
|
+
first_term = true
|
813
|
+
terms.each do |dt|
|
814
|
+
lines << %(<br />) unless first_term
|
815
|
+
lines << '<p>'
|
816
|
+
lines << dt.text
|
817
|
+
lines << '</p>'
|
818
|
+
first_term = nil
|
819
|
+
end
|
820
|
+
lines << '</td>'
|
821
|
+
lines << '<td class="hdlist2">'
|
822
|
+
if dd
|
823
|
+
lines << %(<p>#{dd.text}</p>) if dd.text?
|
824
|
+
lines << dd.content if dd.blocks?
|
825
|
+
end
|
826
|
+
lines << '</td>'
|
827
|
+
lines << '</tr>'
|
828
|
+
end
|
829
|
+
lines << '</table>'
|
543
830
|
else
|
544
|
-
lines << '<
|
545
|
-
<dl>'
|
831
|
+
lines << '<dl>'
|
546
832
|
node.items.each do |terms, dd|
|
547
833
|
[*terms].each do |dt|
|
548
834
|
lines << %(<dt>
|
@@ -559,9 +845,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
559
845
|
end
|
560
846
|
lines << '</dd>'
|
561
847
|
end
|
562
|
-
lines << '</dl>
|
563
|
-
</div>'
|
848
|
+
lines << '</dl>'
|
564
849
|
end
|
850
|
+
|
851
|
+
lines << '</div>'
|
565
852
|
lines * LF
|
566
853
|
end
|
567
854
|
|
@@ -620,110 +907,200 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
620
907
|
lines * LF
|
621
908
|
end
|
622
909
|
|
623
|
-
def
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
910
|
+
def doc_option document, key
|
911
|
+
loop do
|
912
|
+
value = document.options[key]
|
913
|
+
return value unless value.nil?
|
914
|
+
document = document.parent_document
|
915
|
+
break if document.nil?
|
916
|
+
end
|
917
|
+
nil
|
918
|
+
end
|
919
|
+
|
920
|
+
def root_document document
|
921
|
+
document = document.parent_document until document.parent_document.nil?
|
922
|
+
document
|
923
|
+
end
|
924
|
+
|
925
|
+
def register_media_file node, target, media_type
|
926
|
+
if target.end_with?('.svg') || target.start_with?('data:image/svg+xml')
|
927
|
+
chapter = get_enclosing_chapter node
|
928
|
+
chapter.set_attr 'epub-properties', [] unless chapter.attr? 'epub-properties'
|
929
|
+
epub_properties = chapter.attr 'epub-properties'
|
633
930
|
epub_properties << 'svg' unless epub_properties.include? 'svg'
|
931
|
+
end
|
932
|
+
|
933
|
+
if Asciidoctor::Helpers.uriish? target
|
934
|
+
# We need to add both local and remote media files to manifect
|
935
|
+
fs_path = nil
|
634
936
|
else
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
# img_attrs << %(width="#{node.attr 'scaledheight'}" height="#{node.attr 'scaledheight'}")
|
645
|
-
# ePub3
|
646
|
-
elsif node.attr? 'scaledheight'
|
647
|
-
img_attrs << %(height="#{node.attr 'scaledheight'}" style="max-height: #{node.attr 'scaledheight'} !important")
|
648
|
-
else
|
649
|
-
# Aldiko doesn't not scale width to 100% by default
|
650
|
-
img_attrs << %(width="100%")
|
937
|
+
out_dir = node.attr('outdir', nil, true) || doc_option(node.document, :to_dir)
|
938
|
+
fs_path = (::File.join out_dir, target)
|
939
|
+
unless ::File.exist? fs_path
|
940
|
+
base_dir = root_document(node.document).base_dir
|
941
|
+
fs_path = ::File.join base_dir, target
|
942
|
+
end
|
943
|
+
end
|
944
|
+
# We need *both* virtual and physical image paths. Unfortunately, references[:images] only has one of them.
|
945
|
+
@media_files << { name: target, path: fs_path, media_type: media_type }
|
651
946
|
end
|
652
|
-
|
653
|
-
|
947
|
+
|
948
|
+
def resolve_image_attrs node
|
949
|
+
img_attrs = []
|
950
|
+
img_attrs << %(alt="#{node.attr 'alt'}") if node.attr? 'alt'
|
951
|
+
|
952
|
+
width = node.attr 'scaledwidth'
|
953
|
+
width = node.attr 'width' if width.nil?
|
954
|
+
|
955
|
+
# Unlike browsers, Calibre/Kindle *do* scale image if only height is specified
|
956
|
+
# So, in order to match browser behavior, we just always omit height
|
957
|
+
img_attrs << %(width="#{width}") unless width.nil?
|
958
|
+
|
959
|
+
img_attrs
|
960
|
+
end
|
961
|
+
|
962
|
+
def convert_audio node
|
963
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
964
|
+
target = node.media_uri node.attr 'target'
|
965
|
+
register_media_file node, target, 'audio'
|
966
|
+
title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
|
967
|
+
|
968
|
+
autoplay_attr = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
|
969
|
+
controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
|
970
|
+
loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
|
971
|
+
|
972
|
+
start_t = node.attr 'start'
|
973
|
+
end_t = node.attr 'end'
|
974
|
+
if start_t || end_t
|
975
|
+
time_anchor = %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
|
976
|
+
else
|
977
|
+
time_anchor = ''
|
978
|
+
end
|
979
|
+
|
980
|
+
%(<figure#{id_attr} class="audioblock#{prepend_space node.role}">#{title_element}
|
981
|
+
<div class="content">
|
982
|
+
<audio src="#{target}#{time_anchor}"#{autoplay_attr}#{controls_attr}#{loop_attr}>
|
983
|
+
<div>Your Reading System does not support (this) audio.</div>
|
984
|
+
</audio>
|
985
|
+
</div>
|
986
|
+
</figure>)
|
987
|
+
end
|
988
|
+
|
989
|
+
# TODO: Support multiple video files in different formats for a single video
|
990
|
+
def convert_video node
|
991
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
992
|
+
target = node.media_uri node.attr 'target'
|
993
|
+
register_media_file node, target, 'video'
|
994
|
+
title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
|
995
|
+
|
996
|
+
width_attr = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : ''
|
997
|
+
height_attr = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : ''
|
998
|
+
autoplay_attr = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
|
999
|
+
controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
|
1000
|
+
loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
|
1001
|
+
|
1002
|
+
start_t = node.attr 'start'
|
1003
|
+
end_t = node.attr 'end'
|
1004
|
+
if start_t || end_t
|
1005
|
+
time_anchor = %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
|
1006
|
+
else
|
1007
|
+
time_anchor = ''
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
if (poster = node.attr 'poster').nil_or_empty?
|
1011
|
+
poster_attr = ''
|
1012
|
+
else
|
1013
|
+
poster = node.media_uri poster
|
1014
|
+
register_media_file node, poster, 'image'
|
1015
|
+
poster_attr = %( poster="#{poster}")
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
%(<figure#{id_attr} class="video#{prepend_space node.role}">#{title_element}
|
1019
|
+
<div class="content">
|
1020
|
+
<video src="#{target}#{time_anchor}"#{width_attr}#{height_attr}#{autoplay_attr}#{poster_attr}#{controls_attr}#{loop_attr}>
|
1021
|
+
<div>Your Reading System does not support (this) video.</div>
|
1022
|
+
</video>
|
1023
|
+
</div>
|
1024
|
+
</figure>)
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
def convert_image node
|
1028
|
+
target = node.image_uri node.attr 'target'
|
1029
|
+
register_media_file node, target, 'image'
|
1030
|
+
id_attr = node.id ? %( id="#{node.id}") : ''
|
1031
|
+
title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
|
1032
|
+
img_attrs = resolve_image_attrs node
|
654
1033
|
%(<figure#{id_attr} class="image#{prepend_space node.role}">
|
655
1034
|
<div class="content">
|
656
|
-
<img src="#{
|
657
|
-
</div>#{
|
658
|
-
<figcaption>#{node.captioned_title}</figcaption>) : ''}
|
1035
|
+
<img src="#{target}"#{prepend_space img_attrs * ' '} />
|
1036
|
+
</div>#{title_element}
|
659
1037
|
</figure>)
|
660
1038
|
end
|
661
1039
|
|
1040
|
+
def get_enclosing_chapter node
|
1041
|
+
loop do
|
1042
|
+
return nil if node.nil?
|
1043
|
+
return node unless get_chapter_name(node).nil?
|
1044
|
+
node = node.parent
|
1045
|
+
end
|
1046
|
+
end
|
1047
|
+
|
662
1048
|
def convert_inline_anchor node
|
663
|
-
target = node.target
|
664
1049
|
case node.type
|
665
|
-
when :xref
|
666
|
-
doc, refid,
|
667
|
-
|
668
|
-
|
669
|
-
if path
|
670
|
-
#
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
if refdoc
|
689
|
-
if (refs = refdoc.references[:refs]) && ::Asciidoctor::AbstractNode === (ref = refs[refdoc_refid])
|
690
|
-
text ||= ::Asciidoctor::Document === ref ? ((ref.attr 'docreftext') || ref.doctitle) : ref.xreftext((@xrefstyle ||= (doc.attr 'xrefstyle')))
|
691
|
-
elsif (xreftext = refdoc.references[:ids][refdoc_refid])
|
692
|
-
text ||= xreftext
|
1050
|
+
when :xref
|
1051
|
+
doc, refid, target, text = node.document, node.attr('refid'), node.target, node.text
|
1052
|
+
id_attr = ''
|
1053
|
+
|
1054
|
+
if (path = node.attributes['path'])
|
1055
|
+
# NOTE non-nil path indicates this is an inter-document xref that's not included in current document
|
1056
|
+
text = node.text || path
|
1057
|
+
elsif refid == '#'
|
1058
|
+
logger.warn %(#{::File.basename doc.attr('docfile')}: <<chapter#>> xref syntax isn't supported anymore. Use either <<chapter>> or <<chapter#anchor>>)
|
1059
|
+
elsif refid
|
1060
|
+
ref = doc.references[:refs][refid]
|
1061
|
+
our_chapter = get_enclosing_chapter node
|
1062
|
+
ref_chapter = get_enclosing_chapter ref
|
1063
|
+
if ref_chapter
|
1064
|
+
ref_docname = get_chapter_name ref_chapter
|
1065
|
+
if ref_chapter == our_chapter
|
1066
|
+
# ref within same chapter file
|
1067
|
+
id_attr = %( id="xref-#{refid}")
|
1068
|
+
target = %(##{refid})
|
1069
|
+
elsif refid == ref_docname
|
1070
|
+
# ref to top section of other chapter file
|
1071
|
+
id_attr = %( id="xref--#{refid}")
|
1072
|
+
target = %(#{refid}.xhtml)
|
693
1073
|
else
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
logger.warn %(#{::File.basename doc.attr('docfile')}: invalid reference to anchor in unknown chapter: #{refdoc_id})
|
698
|
-
end
|
699
|
-
else
|
700
|
-
id_attr = (@xrefs_seen.add? refid) ? %( id="xref-#{refid}") : ''
|
701
|
-
if (refs = doc.references[:refs])
|
702
|
-
if ::Asciidoctor::AbstractNode === (ref = refs[refid])
|
703
|
-
xreftext = text || ref.xreftext((@xrefstyle ||= (doc.attr 'xrefstyle')))
|
1074
|
+
# ref to section within other chapter file
|
1075
|
+
id_attr = %( id="xref--#{ref_docname}--#{refid}")
|
1076
|
+
target = %(#{ref_docname}.xhtml##{refid})
|
704
1077
|
end
|
705
|
-
else
|
706
|
-
xreftext = doc.references[:ids][refid]
|
707
|
-
end
|
708
1078
|
|
709
|
-
|
710
|
-
text ||= xreftext
|
1079
|
+
id_attr = '' unless @xrefs_seen.add? refid
|
1080
|
+
text ||= (ref.xreftext node.attr('xrefstyle', nil, true))
|
711
1081
|
else
|
712
|
-
#
|
713
|
-
logger.warn %(#{::File.basename doc.attr('docfile')}: invalid reference to unknown local anchor (or valid bibref): #{refid})
|
1082
|
+
logger.warn %(#{::File.basename doc.attr('docfile')}: invalid reference to unknown anchor: #{refid})
|
714
1083
|
end
|
715
1084
|
end
|
1085
|
+
|
716
1086
|
%(<a#{id_attr} href="#{target}" class="xref">#{text || "[#{refid}]"}</a>)
|
717
1087
|
when :ref
|
718
|
-
|
1088
|
+
# NOTE id is used instead of target starting in Asciidoctor 2.0.0
|
1089
|
+
%(<a id="#{node.target || node.id}"></a>)
|
719
1090
|
when :link
|
720
|
-
%(<a href="#{target}" class="link">#{node.text}</a>)
|
1091
|
+
%(<a href="#{node.target}" class="link">#{node.text}</a>)
|
721
1092
|
when :bibref
|
722
|
-
|
723
|
-
|
1093
|
+
# NOTE reftext is no longer enclosed in [] starting in Asciidoctor 2.0.0
|
1094
|
+
# NOTE id is used instead of target starting in Asciidoctor 2.0.0
|
1095
|
+
if (reftext = node.reftext)
|
1096
|
+
reftext = %([#{reftext}]) unless reftext.start_with? '['
|
724
1097
|
else
|
725
|
-
%(
|
1098
|
+
reftext = %([#{node.target || node.id}])
|
726
1099
|
end
|
1100
|
+
%(<a id="#{node.target || node.id}"></a>#{reftext})
|
1101
|
+
else
|
1102
|
+
logger.warn %(unknown anchor type: #{node.type.inspect})
|
1103
|
+
nil
|
727
1104
|
end
|
728
1105
|
end
|
729
1106
|
|
@@ -761,16 +1138,11 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
761
1138
|
%(<i class="#{i_classes * ' '}"></i>)
|
762
1139
|
else
|
763
1140
|
target = node.image_uri node.target
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
epub_properties << 'svg' unless epub_properties.include? 'svg'
|
770
|
-
elsif node.attr? 'scaledwidth'
|
771
|
-
img_attrs << %(style="width: #{node.attr 'scaledwidth'}")
|
772
|
-
end
|
773
|
-
%(<img src="#{target}" #{img_attrs * ' '}/>)
|
1141
|
+
register_media_file node, target, 'image'
|
1142
|
+
|
1143
|
+
img_attrs = resolve_image_attrs node
|
1144
|
+
img_attrs << %(class="inline#{prepend_space node.role}")
|
1145
|
+
%(<img src="#{target}"#{prepend_space img_attrs * ' '}/>)
|
774
1146
|
end
|
775
1147
|
end
|
776
1148
|
|
@@ -802,25 +1174,27 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
802
1174
|
end
|
803
1175
|
|
804
1176
|
def convert_inline_quoted node
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
1177
|
+
open, close, tag = QUOTE_TAGS[node.type]
|
1178
|
+
|
1179
|
+
# TODO: implement proper stem support. See https://github.com/asciidoctor/asciidoctor-epub3/issues/10
|
1180
|
+
node.add_role 'literal' if [:monospaced, :asciimath, :latexmath].include? node.type
|
1181
|
+
|
1182
|
+
if node.id
|
1183
|
+
class_attr = class_string node
|
1184
|
+
if tag
|
1185
|
+
%(#{open.chop} id="#{node.id}"#{class_attr}>#{node.text}#{close})
|
1186
|
+
else
|
1187
|
+
%(<span id="#{node.id}"#{class_attr}>#{open}#{node.text}#{close}</span>)
|
1188
|
+
end
|
1189
|
+
elsif role_valid_class? node.role
|
1190
|
+
class_attr = class_string node
|
1191
|
+
if tag
|
1192
|
+
%(#{open.chop}#{class_attr}>#{node.text}#{close})
|
1193
|
+
else
|
1194
|
+
%(<span#{class_attr}>#{open}#{node.text}#{close}</span>)
|
1195
|
+
end
|
822
1196
|
else
|
823
|
-
node.text
|
1197
|
+
%(#{open}#{node.text}#{close})
|
824
1198
|
end
|
825
1199
|
end
|
826
1200
|
|
@@ -854,6 +1228,460 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
854
1228
|
def prepend_space value
|
855
1229
|
value ? %( #{value}) : ''
|
856
1230
|
end
|
1231
|
+
|
1232
|
+
def add_theme_assets doc
|
1233
|
+
format = @format
|
1234
|
+
workdir = if doc.attr? 'epub3-stylesdir'
|
1235
|
+
stylesdir = doc.attr 'epub3-stylesdir'
|
1236
|
+
# FIXME: make this work for Windows paths!!
|
1237
|
+
if stylesdir.start_with? '/'
|
1238
|
+
stylesdir
|
1239
|
+
else
|
1240
|
+
docdir = doc.attr 'docdir', '.'
|
1241
|
+
docdir = '.' if docdir.empty?
|
1242
|
+
::File.join docdir, stylesdir
|
1243
|
+
end
|
1244
|
+
else
|
1245
|
+
::File.join DATA_DIR, 'styles'
|
1246
|
+
end
|
1247
|
+
|
1248
|
+
# TODO: improve design/UX of custom theme functionality, including custom fonts
|
1249
|
+
|
1250
|
+
if format == :kf8
|
1251
|
+
# NOTE add layer of indirection so Kindle Direct Publishing (KDP) doesn't strip font-related CSS rules
|
1252
|
+
@book.add_item 'styles/epub3.css', content: '@import url("epub3-proxied.css");'.to_ios
|
1253
|
+
@book.add_item 'styles/epub3-css3-only.css', content: '@import url("epub3-css3-only-proxied.css");'.to_ios
|
1254
|
+
@book.add_item 'styles/epub3-proxied.css', content: (postprocess_css_file ::File.join(workdir, 'epub3.css'), format)
|
1255
|
+
@book.add_item 'styles/epub3-css3-only-proxied.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
|
1256
|
+
else
|
1257
|
+
@book.add_item 'styles/epub3.css', content: (postprocess_css_file ::File.join(workdir, 'epub3.css'), format)
|
1258
|
+
@book.add_item 'styles/epub3-css3-only.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
|
1259
|
+
end
|
1260
|
+
|
1261
|
+
font_files, font_css = select_fonts ::File.join(DATA_DIR, 'styles/epub3-fonts.css'), (doc.attr 'scripts', 'latin')
|
1262
|
+
@book.add_item 'styles/epub3-fonts.css', content: font_css
|
1263
|
+
unless font_files.empty?
|
1264
|
+
# NOTE metadata property in oepbs package manifest doesn't work; must use proprietary iBooks file instead
|
1265
|
+
#(@book.metadata.add_metadata 'meta', 'true')['property'] = 'ibooks:specified-fonts' unless format == :kf8
|
1266
|
+
@book.add_optional_file 'META-INF/com.apple.ibooks.display-options.xml', '<?xml version="1.0" encoding="UTF-8"?>
|
1267
|
+
<display_options>
|
1268
|
+
<platform name="*">
|
1269
|
+
<option name="specified-fonts">true</option>
|
1270
|
+
</platform>
|
1271
|
+
</display_options>'.to_ios unless format == :kf8
|
1272
|
+
|
1273
|
+
font_files.each do |font_file|
|
1274
|
+
@book.add_item font_file, content: File.join(DATA_DIR, font_file)
|
1275
|
+
end
|
1276
|
+
end
|
1277
|
+
nil
|
1278
|
+
end
|
1279
|
+
|
1280
|
+
def add_cover_image doc
|
1281
|
+
return if (image_path = doc.attr 'front-cover-image').nil?
|
1282
|
+
|
1283
|
+
imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
|
1284
|
+
imagesdir = (imagesdir == '.' ? '' : %(#{imagesdir}/))
|
1285
|
+
|
1286
|
+
image_attrs = {}
|
1287
|
+
if (image_path.include? ':') && image_path =~ ImageMacroRx
|
1288
|
+
logger.warn %(deprecated block macro syntax detected in front-cover-image attribute) if image_path.start_with? 'image::'
|
1289
|
+
image_path = %(#{imagesdir}#{$1})
|
1290
|
+
(::Asciidoctor::AttributeList.new $2).parse_into image_attrs, %w(alt width height) unless $2.empty?
|
1291
|
+
end
|
1292
|
+
|
1293
|
+
image_href = %(#{imagesdir}jacket/cover#{::File.extname image_path})
|
1294
|
+
|
1295
|
+
workdir = doc.attr 'docdir'
|
1296
|
+
workdir = '.' if workdir.nil_or_empty?
|
1297
|
+
|
1298
|
+
unless ::File.readable? ::File.join(workdir, image_path)
|
1299
|
+
logger.error %(#{::File.basename doc.attr('docfile')}: front cover image not found or readable: #{::File.expand_path image_path, workdir})
|
1300
|
+
return
|
1301
|
+
end
|
1302
|
+
|
1303
|
+
unless !image_attrs.empty? && (width = image_attrs['width']) && (height = image_attrs['height'])
|
1304
|
+
width, height = 1050, 1600
|
1305
|
+
end
|
1306
|
+
|
1307
|
+
@book.add_item(image_href, content: File.join(workdir, image_path)).cover_image
|
1308
|
+
|
1309
|
+
unless @format == :kf8
|
1310
|
+
# NOTE SVG wrapper maintains aspect ratio and confines image to view box
|
1311
|
+
content = %(<!DOCTYPE html>
|
1312
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
|
1313
|
+
<head>
|
1314
|
+
<meta charset="UTF-8"/>
|
1315
|
+
<title>#{sanitize_doctitle_xml doc, :cdata}</title>
|
1316
|
+
<style type="text/css">
|
1317
|
+
@page {
|
1318
|
+
margin: 0;
|
1319
|
+
}
|
1320
|
+
html {
|
1321
|
+
margin: 0 !important;
|
1322
|
+
padding: 0 !important;
|
1323
|
+
}
|
1324
|
+
body {
|
1325
|
+
margin: 0;
|
1326
|
+
padding: 0 !important;
|
1327
|
+
text-align: center;
|
1328
|
+
}
|
1329
|
+
body > svg {
|
1330
|
+
/* prevent bleed onto second page (removes descender space) */
|
1331
|
+
display: block;
|
1332
|
+
}
|
1333
|
+
</style>
|
1334
|
+
</head>
|
1335
|
+
<body epub:type="cover"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
1336
|
+
width="100%" height="100%" viewBox="0 0 #{width} #{height}" preserveAspectRatio="xMidYMid meet">
|
1337
|
+
<image width="#{width}" height="#{height}" xlink:href="#{image_href}"/>
|
1338
|
+
</svg></body>
|
1339
|
+
</html>).to_ios
|
1340
|
+
|
1341
|
+
# Gitden expects a cover.xhtml, so add it to the spine
|
1342
|
+
@book.add_ordered_item 'cover.xhtml', content: content, id: 'cover'
|
1343
|
+
end
|
1344
|
+
nil
|
1345
|
+
end
|
1346
|
+
|
1347
|
+
def get_frontmatter_files doc, workdir
|
1348
|
+
if doc.attr? 'epub3-frontmatterdir'
|
1349
|
+
fmdir = doc.attr 'epub3-frontmatterdir'
|
1350
|
+
fmglob = 'front-matter.*\.html'
|
1351
|
+
fm_path = File.join workdir, fmdir
|
1352
|
+
unless Dir.exist? fm_path
|
1353
|
+
logger.warn %(#{File.basename doc.attr('docfile')}: directory specified by 'epub3-frontmattderdir' doesn't exist! Ignoring ...)
|
1354
|
+
return []
|
1355
|
+
end
|
1356
|
+
fms = Dir.entries(fm_path).delete_if {|x| !x.match fmglob }.sort.map {|y| File.join fm_path, y }
|
1357
|
+
if fms && !fms.empty?
|
1358
|
+
fms
|
1359
|
+
else
|
1360
|
+
logger.warn %(#{File.basename doc.attr('docfile')}: directory specified by 'epub3-frontmattderdir' contains no suitable files! Ignoring ...)
|
1361
|
+
[]
|
1362
|
+
end
|
1363
|
+
elsif File.exist? File.join workdir, 'front-matter.html'
|
1364
|
+
[File.join(workdir, 'front-matter.html')]
|
1365
|
+
else
|
1366
|
+
[]
|
1367
|
+
end
|
1368
|
+
end
|
1369
|
+
|
1370
|
+
def add_front_matter_page doc
|
1371
|
+
workdir = doc.attr 'docdir'
|
1372
|
+
workdir = '.' if workdir.nil_or_empty?
|
1373
|
+
|
1374
|
+
get_frontmatter_files(doc, workdir).each do |front_matter|
|
1375
|
+
front_matter_content = ::File.read front_matter
|
1376
|
+
|
1377
|
+
front_matter_file = File.basename front_matter, '.html'
|
1378
|
+
item = @book.add_ordered_item "#{front_matter_file}.xhtml", content: (postprocess_xhtml front_matter_content)
|
1379
|
+
item.add_property 'svg' if SvgImgSniffRx =~ front_matter_content
|
1380
|
+
|
1381
|
+
front_matter_content.scan ImgSrcScanRx do
|
1382
|
+
@book.add_item $1, content: File.join(File.dirname(front_matter), $1)
|
1383
|
+
end
|
1384
|
+
end
|
1385
|
+
|
1386
|
+
nil
|
1387
|
+
end
|
1388
|
+
|
1389
|
+
def add_profile_images doc, usernames
|
1390
|
+
imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
|
1391
|
+
imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
|
1392
|
+
|
1393
|
+
@book.add_item %(#{imagesdir}avatars/default.jpg), content: ::File.join(DATA_DIR, 'images/default-avatar.jpg')
|
1394
|
+
@book.add_item %(#{imagesdir}headshots/default.jpg), content: ::File.join(DATA_DIR, 'images/default-headshot.jpg')
|
1395
|
+
|
1396
|
+
workdir = (workdir = doc.attr 'docdir').nil_or_empty? ? '.' : workdir
|
1397
|
+
|
1398
|
+
usernames.each do |username|
|
1399
|
+
avatar = %(#{imagesdir}avatars/#{username}.jpg)
|
1400
|
+
if ::File.readable? (resolved_avatar = (::File.join workdir, avatar))
|
1401
|
+
@book.add_item avatar, content: resolved_avatar
|
1402
|
+
else
|
1403
|
+
logger.error %(avatar for #{username} not found or readable: #{avatar}; falling back to default avatar)
|
1404
|
+
@book.add_item avatar, content: ::File.join(DATA_DIR, 'images/default-avatar.jpg')
|
1405
|
+
end
|
1406
|
+
|
1407
|
+
headshot = %(#{imagesdir}headshots/#{username}.jpg)
|
1408
|
+
if ::File.readable? (resolved_headshot = (::File.join workdir, headshot))
|
1409
|
+
@book.add_item headshot, content: resolved_headshot
|
1410
|
+
elsif doc.attr? 'builder', 'editions'
|
1411
|
+
logger.error %(headshot for #{username} not found or readable: #{headshot}; falling back to default headshot)
|
1412
|
+
@book.add_item headshot, content: ::File.join(DATA_DIR, 'images/default-headshot.jpg')
|
1413
|
+
end
|
1414
|
+
end
|
1415
|
+
nil
|
1416
|
+
end
|
1417
|
+
|
1418
|
+
# TODO: aggregate authors of chapters into authors attribute(s) on main document
|
1419
|
+
def nav_doc doc, items
|
1420
|
+
lines = [%(<!DOCTYPE html>
|
1421
|
+
<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}">
|
1422
|
+
<head>
|
1423
|
+
<meta charset="UTF-8"/>
|
1424
|
+
<title>#{sanitize_doctitle_xml doc, :cdata}</title>
|
1425
|
+
<link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
|
1426
|
+
<link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
|
1427
|
+
</head>
|
1428
|
+
<body>
|
1429
|
+
<h1>#{sanitize_doctitle_xml doc, :pcdata}</h1>
|
1430
|
+
<nav epub:type="toc" id="toc">
|
1431
|
+
<h2>#{doc.attr 'toc-title'}</h2>)]
|
1432
|
+
lines << (nav_level items, [(doc.attr 'toclevels', 1).to_i, 0].max)
|
1433
|
+
lines << %(</nav>
|
1434
|
+
</body>
|
1435
|
+
</html>)
|
1436
|
+
lines * LF
|
1437
|
+
end
|
1438
|
+
|
1439
|
+
def nav_level items, depth, state = {}
|
1440
|
+
lines = []
|
1441
|
+
lines << '<ol>'
|
1442
|
+
items.each do |item|
|
1443
|
+
#index = (state[:index] = (state.fetch :index, 0) + 1)
|
1444
|
+
if (chapter_name = get_chapter_name item).nil?
|
1445
|
+
item_label = sanitize_xml get_numbered_title(item), :pcdata
|
1446
|
+
item_href = %(#{state[:content_doc_href]}##{item.id})
|
1447
|
+
else
|
1448
|
+
# NOTE we sanitize the chapter titles because we use formatting to control layout
|
1449
|
+
if item.context == :document
|
1450
|
+
item_label = sanitize_doctitle_xml item, :cdata
|
1451
|
+
else
|
1452
|
+
item_label = sanitize_xml get_numbered_title(item), :cdata
|
1453
|
+
end
|
1454
|
+
item_href = (state[:content_doc_href] = %(#{chapter_name}.xhtml))
|
1455
|
+
end
|
1456
|
+
lines << %(<li><a href="#{item_href}">#{item_label}</a>)
|
1457
|
+
if depth == 0 || (child_sections = item.sections).empty?
|
1458
|
+
lines[-1] = %(#{lines[-1]}</li>)
|
1459
|
+
else
|
1460
|
+
lines << (nav_level child_sections, depth - 1, state)
|
1461
|
+
lines << '</li>'
|
1462
|
+
end
|
1463
|
+
state.delete :content_doc_href unless chapter_name.nil?
|
1464
|
+
end
|
1465
|
+
lines << '</ol>'
|
1466
|
+
lines * LF
|
1467
|
+
end
|
1468
|
+
|
1469
|
+
def ncx_doc doc, items
|
1470
|
+
# TODO: populate docAuthor element based on unique authors in work
|
1471
|
+
lines = [%(<?xml version="1.0" encoding="utf-8"?>
|
1472
|
+
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="#{doc.attr 'lang', 'en'}">
|
1473
|
+
<head>
|
1474
|
+
<meta name="dtb:uid" content="#{@book.identifier}"/>
|
1475
|
+
%{depth}
|
1476
|
+
<meta name="dtb:totalPageCount" content="0"/>
|
1477
|
+
<meta name="dtb:maxPageNumber" content="0"/>
|
1478
|
+
</head>
|
1479
|
+
<docTitle><text>#{sanitize_doctitle_xml doc, :cdata}</text></docTitle>
|
1480
|
+
<navMap>)]
|
1481
|
+
lines << (ncx_level items, [(doc.attr 'toclevels', 1).to_i, 0].max, state = {})
|
1482
|
+
lines[0] = lines[0].sub '%{depth}', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>)
|
1483
|
+
lines << %(</navMap>
|
1484
|
+
</ncx>)
|
1485
|
+
lines * LF
|
1486
|
+
end
|
1487
|
+
|
1488
|
+
def ncx_level items, depth, state = {}
|
1489
|
+
lines = []
|
1490
|
+
state[:max_depth] = (state.fetch :max_depth, 0) + 1
|
1491
|
+
items.each do |item|
|
1492
|
+
index = (state[:index] = (state.fetch :index, 0) + 1)
|
1493
|
+
item_id = %(nav_#{index})
|
1494
|
+
if (chapter_name = get_chapter_name item).nil?
|
1495
|
+
item_label = sanitize_xml get_numbered_title(item), :cdata
|
1496
|
+
item_href = %(#{state[:content_doc_href]}##{item.id})
|
1497
|
+
else
|
1498
|
+
if item.context == :document
|
1499
|
+
item_label = sanitize_doctitle_xml item, :cdata
|
1500
|
+
else
|
1501
|
+
item_label = sanitize_xml get_numbered_title(item), :cdata
|
1502
|
+
end
|
1503
|
+
item_href = (state[:content_doc_href] = %(#{chapter_name}.xhtml))
|
1504
|
+
end
|
1505
|
+
lines << %(<navPoint id="#{item_id}" playOrder="#{index}">)
|
1506
|
+
lines << %(<navLabel><text>#{item_label}</text></navLabel>)
|
1507
|
+
lines << %(<content src="#{item_href}"/>)
|
1508
|
+
unless depth == 0 || (child_sections = item.sections).empty?
|
1509
|
+
lines << (ncx_level child_sections, depth - 1, state)
|
1510
|
+
end
|
1511
|
+
lines << %(</navPoint>)
|
1512
|
+
state.delete :content_doc_href unless chapter_name.nil?
|
1513
|
+
end
|
1514
|
+
lines * LF
|
1515
|
+
end
|
1516
|
+
|
1517
|
+
# Swap fonts in CSS based on the value of the document attribute 'scripts',
|
1518
|
+
# then return the list of fonts as well as the font CSS.
|
1519
|
+
def select_fonts filename, scripts = 'latin'
|
1520
|
+
font_css = ::File.read filename
|
1521
|
+
font_css = font_css.gsub(/(?<=-)latin(?=\.ttf\))/, scripts) unless scripts == 'latin'
|
1522
|
+
|
1523
|
+
# match CSS font urls in the forms of:
|
1524
|
+
# src: url(../fonts/notoserif-regular-latin.ttf);
|
1525
|
+
# src: url(../fonts/notoserif-regular-latin.ttf) format("truetype");
|
1526
|
+
font_list = font_css.scan(/url\(\.\.\/([^)]+\.ttf)\)/).flatten
|
1527
|
+
|
1528
|
+
[font_list, font_css.to_ios]
|
1529
|
+
end
|
1530
|
+
|
1531
|
+
def postprocess_css_file filename, format
|
1532
|
+
return filename unless format == :kf8
|
1533
|
+
postprocess_css ::File.read(filename), format
|
1534
|
+
end
|
1535
|
+
|
1536
|
+
def postprocess_css content, format
|
1537
|
+
return content.to_ios unless format == :kf8
|
1538
|
+
# TODO: convert regular expressions to constants
|
1539
|
+
content
|
1540
|
+
.gsub(/^ -webkit-column-break-.*\n/, '')
|
1541
|
+
.gsub(/^ max-width: .*\n/, '')
|
1542
|
+
.to_ios
|
1543
|
+
end
|
1544
|
+
|
1545
|
+
# NOTE Kindle requires that
|
1546
|
+
# <meta charset="utf-8"/>
|
1547
|
+
# be converted to
|
1548
|
+
# <meta http-equiv="Content-Type" content="application/xml+xhtml; charset=UTF-8"/>
|
1549
|
+
def postprocess_xhtml content
|
1550
|
+
return content.to_ios unless @format == :kf8
|
1551
|
+
# TODO: convert regular expressions to constants
|
1552
|
+
content
|
1553
|
+
.gsub(/<meta charset="(.+?)"\/>/, '<meta http-equiv="Content-Type" content="application/xml+xhtml; charset=\1"/>')
|
1554
|
+
.gsub(/<img([^>]+) style="width: (\d\d)%;"/, '<img\1 style="width: \2%; height: \2%;"')
|
1555
|
+
.gsub(/<script type="text\/javascript">.*?<\/script>\n?/m, '')
|
1556
|
+
.to_ios
|
1557
|
+
end
|
1558
|
+
|
1559
|
+
def get_kindlegen_command kindlegen_path
|
1560
|
+
unless kindlegen_path.nil?
|
1561
|
+
logger.debug %(Using ebook-kindlegen-path attribute: #{kindlegen_path})
|
1562
|
+
return [kindlegen_path]
|
1563
|
+
end
|
1564
|
+
|
1565
|
+
unless (result = ENV['KINDLEGEN']).nil?
|
1566
|
+
logger.debug %(Using KINDLEGEN env variable: #{result})
|
1567
|
+
return [result]
|
1568
|
+
end
|
1569
|
+
|
1570
|
+
begin
|
1571
|
+
require 'kindlegen' unless defined? ::Kindlegen
|
1572
|
+
result = ::Kindlegen.command.to_s
|
1573
|
+
logger.debug %(Using KindleGen from gem: #{result})
|
1574
|
+
[result]
|
1575
|
+
rescue LoadError => e
|
1576
|
+
logger.debug %(#{e}; Using KindleGen from PATH)
|
1577
|
+
[%(kindlegen#{::Gem.win_platform? ? '.exe' : ''})]
|
1578
|
+
end
|
1579
|
+
end
|
1580
|
+
|
1581
|
+
def distill_epub_to_mobi epub_file, target, compress, kindlegen_path
|
1582
|
+
mobi_file = ::File.basename target.sub(EpubExtensionRx, '.mobi')
|
1583
|
+
compress_flag = KindlegenCompression[compress ? (compress.empty? ? '1' : compress.to_s) : '0']
|
1584
|
+
|
1585
|
+
argv = get_kindlegen_command(kindlegen_path) + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
|
1586
|
+
begin
|
1587
|
+
# This duplicates Kindlegen.run, but we want to override executable
|
1588
|
+
out, err, res = Open3.capture3(*argv) do |r|
|
1589
|
+
r.force_encoding 'UTF-8' if ::Gem.win_platform? && r.respond_to?(:force_encoding)
|
1590
|
+
end
|
1591
|
+
rescue Errno::ENOENT => e
|
1592
|
+
raise 'Unable to run KindleGen. Either install the kindlegen gem or place `kindlegen` executable on PATH or set KINDLEGEN environment variable with path to it', cause: e
|
1593
|
+
end
|
1594
|
+
|
1595
|
+
out.each_line do |line|
|
1596
|
+
log_line line
|
1597
|
+
end
|
1598
|
+
err.each_line do |line|
|
1599
|
+
log_line line
|
1600
|
+
end
|
1601
|
+
|
1602
|
+
output_file = ::File.join ::File.dirname(epub_file), mobi_file
|
1603
|
+
if res.success?
|
1604
|
+
logger.debug %(Wrote MOBI to #{output_file})
|
1605
|
+
else
|
1606
|
+
logger.error %(KindleGen failed to write MOBI to #{output_file})
|
1607
|
+
end
|
1608
|
+
end
|
1609
|
+
|
1610
|
+
def get_epubcheck_command epubcheck_path
|
1611
|
+
unless epubcheck_path.nil?
|
1612
|
+
logger.debug %(Using ebook-epubcheck-path attribute: #{epubcheck_path})
|
1613
|
+
return [epubcheck_path]
|
1614
|
+
end
|
1615
|
+
|
1616
|
+
unless (result = ENV['EPUBCHECK']).nil?
|
1617
|
+
logger.debug %(Using EPUBCHECK env variable: #{result})
|
1618
|
+
return [result]
|
1619
|
+
end
|
1620
|
+
|
1621
|
+
begin
|
1622
|
+
result = ::Gem.bin_path 'epubcheck-ruby', 'epubcheck'
|
1623
|
+
logger.debug %(Using EPUBCheck from gem: #{result})
|
1624
|
+
[::Gem.ruby, result]
|
1625
|
+
rescue ::Gem::Exception => e
|
1626
|
+
logger.debug %(#{e}; Using EPUBCheck from PATH)
|
1627
|
+
['epubcheck']
|
1628
|
+
end
|
1629
|
+
end
|
1630
|
+
|
1631
|
+
def validate_epub epub_file, epubcheck_path
|
1632
|
+
argv = get_epubcheck_command(epubcheck_path) + ['-w', epub_file]
|
1633
|
+
begin
|
1634
|
+
out, err, res = Open3.capture3(*argv)
|
1635
|
+
rescue Errno::ENOENT => e
|
1636
|
+
raise 'Unable to run EPUBCheck. Either install epubcheck-ruby gem or place `epubcheck` executable on PATH or set EPUBCHECK environment variable with path to it', cause: e
|
1637
|
+
end
|
1638
|
+
|
1639
|
+
out.each_line do |line|
|
1640
|
+
logger.info line
|
1641
|
+
end
|
1642
|
+
err.each_line do |line|
|
1643
|
+
log_line line
|
1644
|
+
end
|
1645
|
+
|
1646
|
+
logger.error %(EPUB validation failed: #{epub_file}) unless res.success?
|
1647
|
+
end
|
1648
|
+
|
1649
|
+
def log_line line
|
1650
|
+
line = line.strip
|
1651
|
+
|
1652
|
+
if line =~ /^fatal/i
|
1653
|
+
logger.fatal line
|
1654
|
+
elsif line =~ /^error/i
|
1655
|
+
logger.error line
|
1656
|
+
elsif line =~ /^warning/i
|
1657
|
+
logger.warn line
|
1658
|
+
else
|
1659
|
+
logger.info line
|
1660
|
+
end
|
1661
|
+
end
|
1662
|
+
|
1663
|
+
private
|
1664
|
+
|
1665
|
+
def class_string node
|
1666
|
+
role = node.role
|
1667
|
+
|
1668
|
+
return '' unless role_valid_class? role
|
1669
|
+
|
1670
|
+
%( class="#{role}")
|
1671
|
+
end
|
1672
|
+
|
1673
|
+
# Handles asciidoctor 1.5.6 quirk when role can be parent
|
1674
|
+
def role_valid_class? role
|
1675
|
+
role.is_a? String
|
1676
|
+
end
|
1677
|
+
|
1678
|
+
class << self
|
1679
|
+
def supports_highlighter_docinfo?
|
1680
|
+
# Asciidoctor only got pluggable syntax highlighters since 2.0:
|
1681
|
+
# https://github.com/asciidoctor/asciidoctor/commit/23ddbaed6818025cbe74365fec7e8101f34eadca
|
1682
|
+
Asciidoctor::Document.method_defined? :syntax_highlighter
|
1683
|
+
end
|
1684
|
+
end
|
857
1685
|
end
|
858
1686
|
|
859
1687
|
class DocumentIdGenerator
|
@@ -914,19 +1742,19 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
914
1742
|
end
|
915
1743
|
end
|
916
1744
|
|
917
|
-
require_relative 'packager'
|
918
|
-
|
919
1745
|
Extensions.register do
|
920
1746
|
if (document = @document).backend == 'epub3'
|
921
|
-
document.attributes['spine'] = ''
|
922
1747
|
document.set_attribute 'listing-caption', 'Listing'
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
1748
|
+
|
1749
|
+
# TODO: bw theme for CodeRay
|
1750
|
+
document.set_attribute 'pygments-style', 'bw' unless document.attr? 'pygments-style'
|
1751
|
+
document.set_attribute 'rouge-style', 'bw' unless document.attr? 'rouge-style'
|
1752
|
+
unless Converter.supports_highlighter_docinfo?
|
1753
|
+
document.set_attribute 'coderay-css', 'style'
|
1754
|
+
document.set_attribute 'pygments-css', 'style'
|
1755
|
+
document.set_attribute 'rouge-css', 'style'
|
929
1756
|
end
|
1757
|
+
|
930
1758
|
case (ebook_format = document.attributes['ebook-format'])
|
931
1759
|
when 'epub3', 'kf8'
|
932
1760
|
# all good
|
@@ -937,8 +1765,6 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
937
1765
|
ebook_format = document.attributes['ebook-format'] = 'epub3'
|
938
1766
|
end
|
939
1767
|
document.attributes[%(ebook-format-#{ebook_format})] = ''
|
940
|
-
# Only fire SpineItemProcessor for top-level include directives
|
941
|
-
include_processor SpineItemProcessor.new(document)
|
942
1768
|
treeprocessor do
|
943
1769
|
process do |doc|
|
944
1770
|
doc.id = DocumentIdGenerator.generate_id doc, (doc.attr 'idprefix'), (doc.attr 'idseparator')
|