asciidoctor-epub3 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +20 -2
- data/Gemfile +7 -10
- data/README.adoc +5 -6
- data/Rakefile +1 -1
- data/asciidoctor-epub3.gemspec +18 -15
- data/bin/adb-push-ebook +17 -15
- data/data/styles/_colors.scss +90 -0
- data/data/styles/_reset.scss +54 -0
- data/data/styles/{epub3-css3-only.css → epub3-css3-only.scss} +1 -31
- data/data/styles/{epub3.css → epub3.scss} +140 -236
- data/lib/asciidoctor-epub3/converter.rb +496 -420
- data/lib/asciidoctor-epub3/ext/asciidoctor/abstract_node.rb +16 -0
- data/lib/asciidoctor-epub3/ext/asciidoctor.rb +1 -2
- data/lib/asciidoctor-epub3/ext/core/file.rb +10 -0
- data/lib/asciidoctor-epub3/ext/core/string.rb +5 -3
- data/lib/asciidoctor-epub3/ext/core.rb +1 -0
- data/lib/asciidoctor-epub3/font_icon_map.rb +1 -1
- data/lib/asciidoctor-epub3/version.rb +1 -1
- data/lib/asciidoctor-epub3.rb +1 -1
- metadata +57 -40
- data/data/styles/color-palette.css +0 -25
- data/lib/asciidoctor-epub3/ext/asciidoctor/document.rb +0 -19
- data/lib/asciidoctor-epub3/ext/asciidoctor/logging_shim.rb +0 -33
- /data/data/styles/{epub3-fonts.css → epub3-fonts.scss} +0 -0
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'mime/types'
|
4
4
|
require 'open3'
|
5
|
+
require 'sass'
|
5
6
|
require_relative 'font_icon_map'
|
6
7
|
|
7
8
|
module Asciidoctor
|
@@ -15,18 +16,19 @@ module Asciidoctor
|
|
15
16
|
|
16
17
|
register_for 'epub3'
|
17
18
|
|
18
|
-
def write
|
19
|
+
def write(output, target)
|
19
20
|
epub_file = @format == :kf8 ? %(#{::Asciidoctor::Helpers.rootname target}-kf8.epub) : target
|
20
21
|
output.generate_epub epub_file
|
21
22
|
logger.debug %(Wrote #{@format.upcase} to #{epub_file})
|
22
23
|
if @extract
|
23
|
-
extract_dir = epub_file.sub
|
24
|
+
extract_dir = epub_file.sub EPUB_EXTENSION_RX, ''
|
24
25
|
::FileUtils.remove_dir extract_dir if ::File.directory? extract_dir
|
25
26
|
::Dir.mkdir extract_dir
|
26
27
|
::Dir.chdir extract_dir do
|
27
28
|
::Zip::File.open epub_file do |entries|
|
28
29
|
entries.each do |entry|
|
29
30
|
next unless entry.file?
|
31
|
+
|
30
32
|
unless (entry_dir = ::File.dirname entry.name) == '.' || (::File.directory? entry_dir)
|
31
33
|
::FileUtils.mkdir_p entry_dir
|
32
34
|
end
|
@@ -38,69 +40,80 @@ module Asciidoctor
|
|
38
40
|
end
|
39
41
|
|
40
42
|
if @format == :kf8
|
41
|
-
# QUESTION shouldn't we validate this epub file too?
|
43
|
+
# QUESTION: shouldn't we validate this epub file too?
|
42
44
|
distill_epub_to_mobi epub_file, target, @compress
|
43
45
|
elsif @validate
|
44
46
|
validate_epub epub_file
|
45
47
|
end
|
46
48
|
end
|
47
49
|
|
48
|
-
|
50
|
+
CSV_DELIMITED_RX = /\s*,\s*/.freeze
|
49
51
|
|
50
52
|
DATA_DIR = ::File.expand_path ::File.join(__dir__, '..', '..', 'data')
|
51
|
-
|
52
|
-
|
53
|
-
|
53
|
+
IMAGE_MACRO_RX = /^image::?(.*?)\[(.*?)\]$/.freeze
|
54
|
+
IMAGE_SRC_SCAN_RX = /<img src="(.+?)"/.freeze
|
55
|
+
SVG_IMG_SNIFF_RX = /<img src=".+?\.svg"/.freeze
|
54
56
|
|
55
|
-
LF =
|
56
|
-
|
57
|
-
|
58
|
-
|
57
|
+
LF = "\n"
|
58
|
+
NO_BREAK_SPACE = ' '
|
59
|
+
RIGHT_ANGLE_QUOTE = '›'
|
60
|
+
CALLOUT_START_NUM = %(\u2460)
|
59
61
|
|
60
|
-
|
61
|
-
|
62
|
-
|
62
|
+
CHAR_ENTITY_RX = /&#(\d{2,6});/.freeze
|
63
|
+
XML_ELEMENT_RX = %r{</?.+?>}.freeze
|
64
|
+
TRAILING_PUNCT_RX = /[[:punct:]]$/.freeze
|
63
65
|
|
64
|
-
|
66
|
+
FROM_HTML_SPECIAL_CHARS_MAP = {
|
65
67
|
'<' => '<',
|
66
68
|
'>' => '>',
|
67
|
-
'&' => '&'
|
68
|
-
}
|
69
|
+
'&' => '&'
|
70
|
+
}.freeze
|
69
71
|
|
70
|
-
|
72
|
+
FROM_HTML_SPECIAL_CHARS_RX = /(?:#{FROM_HTML_SPECIAL_CHARS_MAP.keys * '|'})/.freeze
|
71
73
|
|
72
|
-
|
74
|
+
TO_HTML_SPECIAL_CHARS_MAP = {
|
73
75
|
'&' => '&',
|
74
76
|
'<' => '<',
|
75
|
-
'>' => '>'
|
76
|
-
}
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
77
|
+
'>' => '>'
|
78
|
+
}.freeze
|
79
|
+
|
80
|
+
TO_HTML_SPECIAL_CHARS_RX = /[#{TO_HTML_SPECIAL_CHARS_MAP.keys.join}]/.freeze
|
81
|
+
|
82
|
+
EPUB_EXTENSION_RX = /\.epub$/i.freeze
|
83
|
+
KINDLEGEN_COMPRESSION = {
|
84
|
+
'0' => '-c0',
|
85
|
+
'1' => '-c1',
|
86
|
+
'2' => '-c2',
|
87
|
+
'none' => '-c0',
|
88
|
+
'standard' => '-c1',
|
89
|
+
'huffdic' => '-c2'
|
90
|
+
}.freeze
|
91
|
+
|
92
|
+
QUOTE_TAGS = begin
|
93
|
+
tags = {
|
94
|
+
monospaced: ['<code>', '</code>', true],
|
95
|
+
emphasis: ['<em>', '</em>', true],
|
96
|
+
strong: ['<strong>', '</strong>', true],
|
97
|
+
double: ['“', '”'],
|
98
|
+
single: ['‘', '’'],
|
99
|
+
mark: ['<mark>', '</mark>', true],
|
100
|
+
superscript: ['<sup>', '</sup>', true],
|
101
|
+
subscript: ['<sub>', '</sub>', true],
|
102
|
+
asciimath: ['<code>', '</code>', true],
|
103
|
+
latexmath: ['<code>', '</code>', true]
|
104
|
+
}
|
105
|
+
tags.default = ['', '']
|
106
|
+
tags.freeze
|
107
|
+
end
|
108
|
+
|
109
|
+
def initialize(backend, opts = {})
|
97
110
|
super
|
98
111
|
basebackend 'html'
|
99
112
|
outfilesuffix '.epub' # dummy outfilesuffix since it may be .mobi
|
100
113
|
htmlsyntax 'xml'
|
101
114
|
end
|
102
115
|
|
103
|
-
def convert
|
116
|
+
def convert(node, name = nil, _opts = {})
|
104
117
|
method_name = %(convert_#{name ||= node.node_name})
|
105
118
|
if respond_to? method_name
|
106
119
|
send method_name, node
|
@@ -110,44 +123,41 @@ module Asciidoctor
|
|
110
123
|
end
|
111
124
|
end
|
112
125
|
|
113
|
-
#
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
126
|
+
# @param node [Asciidoctor::AbstractNode]
|
127
|
+
# @return [String, nil]
|
128
|
+
def get_chapter_filename(node)
|
129
|
+
node.id if node.chapter?
|
121
130
|
end
|
122
131
|
|
123
|
-
def get_numbered_title
|
132
|
+
def get_numbered_title(node)
|
124
133
|
doc_attrs = node.document.attributes
|
125
134
|
level = node.level
|
126
135
|
if node.caption
|
127
|
-
|
136
|
+
node.captioned_title
|
128
137
|
elsif node.respond_to?(:numbered) && node.numbered && level <= (doc_attrs['sectnumlevels'] || 3).to_i
|
129
138
|
if level < 2 && node.document.doctype == 'book'
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
139
|
+
case node.sectname
|
140
|
+
when 'chapter'
|
141
|
+
%(#{(signifier = doc_attrs['chapter-signifier']) ? "#{signifier} " : ''}#{node.sectnum} #{node.title})
|
142
|
+
when 'part'
|
143
|
+
%(#{(signifier = doc_attrs['part-signifier']) ? "#{signifier} " : ''}#{node.sectnum nil,
|
144
|
+
':'} #{node.title})
|
134
145
|
else
|
135
|
-
|
146
|
+
%(#{node.sectnum} #{node.title})
|
136
147
|
end
|
137
148
|
else
|
138
|
-
|
149
|
+
%(#{node.sectnum} #{node.title})
|
139
150
|
end
|
140
151
|
else
|
141
|
-
|
152
|
+
node.title
|
142
153
|
end
|
143
|
-
title
|
144
154
|
end
|
145
155
|
|
146
156
|
def icon_names
|
147
157
|
@icon_names ||= []
|
148
158
|
end
|
149
159
|
|
150
|
-
def convert_document
|
160
|
+
def convert_document(node)
|
151
161
|
@format = node.attr('ebook-format').to_sym
|
152
162
|
|
153
163
|
@validate = node.attr? 'ebook-validate'
|
@@ -169,9 +179,9 @@ module Asciidoctor
|
|
169
179
|
@book.primary_identifier node.id, 'pub-identifier', 'uuid'
|
170
180
|
end
|
171
181
|
# replace with next line once the attributes argument is supported
|
172
|
-
#unique_identifier doc.id, 'pub-id', 'uuid', 'scheme' => 'xsd:string'
|
182
|
+
# unique_identifier doc.id, 'pub-id', 'uuid', 'scheme' => 'xsd:string'
|
173
183
|
|
174
|
-
# NOTE we must use :plain_text here since gepub reencodes
|
184
|
+
# NOTE: we must use :plain_text here since gepub reencodes
|
175
185
|
@book.add_title sanitize_doctitle_xml(node, :plain_text), id: 'pub-title'
|
176
186
|
|
177
187
|
# see https://www.w3.org/publishing/epub3/epub-packages.html#sec-opf-dccreator
|
@@ -181,7 +191,7 @@ module Asciidoctor
|
|
181
191
|
end
|
182
192
|
|
183
193
|
publisher = node.attr 'publisher'
|
184
|
-
# NOTE Use producer as both publisher and producer if publisher isn't specified
|
194
|
+
# NOTE: Use producer as both publisher and producer if publisher isn't specified
|
185
195
|
publisher = node.attr 'producer' if publisher.nil_or_empty?
|
186
196
|
@book.publisher = publisher unless publisher.nil_or_empty?
|
187
197
|
|
@@ -207,7 +217,7 @@ module Asciidoctor
|
|
207
217
|
@book.source = node.attr 'source' if node.attr? 'source'
|
208
218
|
@book.rights = node.attr 'copyright' if node.attr? 'copyright'
|
209
219
|
|
210
|
-
(node.attr 'keywords', '').split(
|
220
|
+
(node.attr 'keywords', '').split(CSV_DELIMITED_RX).each do |s|
|
211
221
|
@book.metadata.add_metadata 'subject', s
|
212
222
|
end
|
213
223
|
|
@@ -216,7 +226,8 @@ module Asciidoctor
|
|
216
226
|
series_volume = node.attr 'series-volume', 1
|
217
227
|
series_id = node.attr 'series-id'
|
218
228
|
|
219
|
-
series_meta = @book.metadata.add_metadata 'meta', series_name, id: 'pub-collection',
|
229
|
+
series_meta = @book.metadata.add_metadata 'meta', series_name, id: 'pub-collection',
|
230
|
+
group_position: series_volume
|
220
231
|
series_meta['property'] = 'belongs-to-collection'
|
221
232
|
series_meta.refine 'dcterms:identifier', series_id unless series_id.nil?
|
222
233
|
# Calibre only understands 'series'
|
@@ -228,10 +239,17 @@ module Asciidoctor
|
|
228
239
|
landmarks = []
|
229
240
|
|
230
241
|
front_cover = add_cover_page node, 'front-cover'
|
242
|
+
if front_cover.nil? && @format != :kf8 && node.doctype == 'book'
|
243
|
+
# TODO(#352): add textual front cover similar to PDF
|
244
|
+
end
|
245
|
+
|
231
246
|
landmarks << { type: 'cover', href: front_cover.href, title: 'Front Cover' } unless front_cover.nil?
|
232
247
|
|
233
248
|
front_matter_page = add_front_matter_page node
|
234
|
-
|
249
|
+
unless front_matter_page.nil?
|
250
|
+
landmarks << { type: 'frontmatter', href: front_matter_page.href,
|
251
|
+
title: 'Front Matter' }
|
252
|
+
end
|
235
253
|
|
236
254
|
nav_item = @book.add_item('nav.xhtml', id: 'nav').nav
|
237
255
|
|
@@ -256,17 +274,26 @@ module Asciidoctor
|
|
256
274
|
_back_cover = add_cover_page node, 'back-cover'
|
257
275
|
# TODO: add landmark for back cover? But what epub:type?
|
258
276
|
|
259
|
-
|
277
|
+
unless toc_items.empty?
|
278
|
+
landmarks << { type: 'bodymatter', href: %(#{get_chapter_filename toc_items[0]}.xhtml),
|
279
|
+
title: 'Start of Content' }
|
280
|
+
end
|
260
281
|
|
261
282
|
toc_items.each do |item|
|
262
|
-
|
283
|
+
next unless %w[appendix bibliography glossary index preface].include? item.style
|
284
|
+
|
285
|
+
landmarks << {
|
286
|
+
type: item.style,
|
287
|
+
href: %(#{get_chapter_filename item}.xhtml),
|
288
|
+
title: item.title
|
289
|
+
}
|
263
290
|
end
|
264
291
|
|
265
292
|
nav_item.add_content postprocess_xhtml(nav_doc(node, toc_items, landmarks, outlinelevels))
|
266
293
|
# User is not supposed to see landmarks, so pass empty array here
|
267
294
|
toc_item&.add_content postprocess_xhtml(nav_doc(node, toc_items, [], toclevels))
|
268
295
|
|
269
|
-
# NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
|
296
|
+
# NOTE: gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
|
270
297
|
toc_ncx = ncx_doc node, toc_items, outlinelevels
|
271
298
|
@book.add_item 'toc.ncx', content: toc_ncx.to_ios, id: 'ncx'
|
272
299
|
|
@@ -278,7 +305,7 @@ module Asciidoctor
|
|
278
305
|
logger.warn %(path is reserved for cover artwork: #{name}; skipping file found in content)
|
279
306
|
elsif file[:path].nil? || File.readable?(file[:path])
|
280
307
|
mime_types = MIME::Types.type_for name
|
281
|
-
mime_types.delete_if {|x| x.media_type != file[:media_type] }
|
308
|
+
mime_types.delete_if { |x| x.media_type != file[:media_type] }
|
282
309
|
preferred_mime_type = mime_types.empty? ? nil : mime_types[0].content_type
|
283
310
|
@book.add_item name, content: file[:path], media_type: preferred_mime_type
|
284
311
|
else
|
@@ -286,11 +313,11 @@ module Asciidoctor
|
|
286
313
|
end
|
287
314
|
end
|
288
315
|
|
289
|
-
#add_metadata 'ibooks:specified-fonts', true
|
316
|
+
# add_metadata 'ibooks:specified-fonts', true
|
290
317
|
|
291
318
|
add_theme_assets node
|
292
319
|
if node.doctype != 'book'
|
293
|
-
usernames = [node].map {|item| item.attr 'username' }.compact.uniq
|
320
|
+
usernames = [node].map { |item| item.attr 'username' }.compact.uniq
|
294
321
|
add_profile_images node, usernames
|
295
322
|
end
|
296
323
|
|
@@ -298,17 +325,16 @@ module Asciidoctor
|
|
298
325
|
end
|
299
326
|
|
300
327
|
# FIXME: move to Asciidoctor::Helpers
|
301
|
-
def sanitize_doctitle_xml
|
328
|
+
def sanitize_doctitle_xml(doc, content_spec)
|
302
329
|
doctitle = doc.doctitle use_fallback: true
|
303
330
|
sanitize_xml doctitle, content_spec
|
304
331
|
end
|
305
332
|
|
306
333
|
# FIXME: move to Asciidoctor::Helpers
|
307
|
-
def sanitize_xml
|
308
|
-
if content_spec != :pcdata && (content.include? '<')
|
309
|
-
|
310
|
-
|
311
|
-
end
|
334
|
+
def sanitize_xml(content, content_spec)
|
335
|
+
if content_spec != :pcdata && (content.include? '<') && ((content = (content.gsub XML_ELEMENT_RX,
|
336
|
+
'').strip).include? ' ')
|
337
|
+
content = content.tr_s ' ', ' '
|
312
338
|
end
|
313
339
|
|
314
340
|
case content_spec
|
@@ -318,8 +344,8 @@ module Asciidoctor
|
|
318
344
|
# noop
|
319
345
|
when :plain_text
|
320
346
|
if content.include? ';'
|
321
|
-
content = content.gsub(
|
322
|
-
content = content.gsub
|
347
|
+
content = content.gsub(CHAR_ENTITY_RX) { [::Regexp.last_match(1).to_i].pack 'U*' } if content.include? '&#'
|
348
|
+
content = content.gsub FROM_HTML_SPECIAL_CHARS_RX, FROM_HTML_SPECIAL_CHARS_MAP
|
323
349
|
end
|
324
350
|
else
|
325
351
|
raise ::ArgumentError, %(Unknown content spec: #{content_spec})
|
@@ -327,11 +353,12 @@ module Asciidoctor
|
|
327
353
|
content
|
328
354
|
end
|
329
355
|
|
330
|
-
|
331
|
-
|
332
|
-
|
356
|
+
# @param node [Asciidoctor::AbstractBlock]
|
357
|
+
def add_chapter(node)
|
358
|
+
filename = get_chapter_filename node
|
359
|
+
return nil if filename.nil?
|
333
360
|
|
334
|
-
chapter_item = @book.add_ordered_item %(#{
|
361
|
+
chapter_item = @book.add_ordered_item %(#{filename}.xhtml)
|
335
362
|
|
336
363
|
doctitle = node.document.doctitle partition: true, use_fallback: true
|
337
364
|
chapter_title = doctitle.combined
|
@@ -364,12 +391,12 @@ module Asciidoctor
|
|
364
391
|
@xrefs_seen.clear
|
365
392
|
content = node.content
|
366
393
|
|
367
|
-
# NOTE must run after content is resolved
|
394
|
+
# NOTE: must run after content is resolved
|
368
395
|
# TODO perhaps create dynamic CSS file?
|
369
396
|
if icon_names.empty?
|
370
397
|
icon_css_head = ''
|
371
398
|
else
|
372
|
-
icon_defs = icon_names.map {|name|
|
399
|
+
icon_defs = icon_names.map { |name|
|
373
400
|
%(.i-#{name}::before { content: "#{FontIconMap.unicode name}"; })
|
374
401
|
} * LF
|
375
402
|
icon_css_head = %(<style>
|
@@ -378,21 +405,26 @@ module Asciidoctor
|
|
378
405
|
)
|
379
406
|
end
|
380
407
|
|
381
|
-
header =
|
408
|
+
header = if title || subtitle
|
409
|
+
%(<header>
|
382
410
|
<div class="chapter-header">
|
383
411
|
#{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle}</small>) : ''}</h1>
|
384
412
|
</div>
|
385
|
-
</header>)
|
413
|
+
</header>)
|
414
|
+
else
|
415
|
+
''
|
416
|
+
end
|
386
417
|
|
387
418
|
# We want highlighter CSS to be stored in a separate file
|
388
419
|
# in order to avoid style duplication across chapter files
|
389
420
|
linkcss = true
|
390
421
|
|
391
|
-
# NOTE kindlegen seems to mangle the <header> element, so we wrap its content in a div
|
392
|
-
lines = [%(
|
393
|
-
|
422
|
+
# NOTE: kindlegen seems to mangle the <header> element, so we wrap its content in a div
|
423
|
+
lines = [%(<?xml version='1.0' encoding='utf-8'?>
|
424
|
+
<!DOCTYPE html>
|
425
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xmlns:mml="http://www.w3.org/1998/Math/MathML" xml:lang="#{lang = node.document.attr 'lang',
|
426
|
+
'en'}" lang="#{lang}">
|
394
427
|
<head>
|
395
|
-
<meta charset="UTF-8"/>
|
396
428
|
<title>#{chapter_title}</title>
|
397
429
|
<link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
|
398
430
|
<link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
|
@@ -409,24 +441,27 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
409
441
|
syntax_hl = node.document.syntax_highlighter
|
410
442
|
epub_type_attr = node.respond_to?(:section) && node.sectname != 'section' ? %( epub:type="#{node.sectname}") : ''
|
411
443
|
|
412
|
-
|
444
|
+
if syntax_hl&.docinfo? :head
|
445
|
+
lines << (syntax_hl.docinfo :head, node, linkcss: linkcss,
|
446
|
+
self_closing_tag_slash: '/')
|
447
|
+
end
|
413
448
|
|
414
449
|
lines << %(</head>
|
415
450
|
<body>
|
416
|
-
<section class="chapter" title=#{chapter_title.encode xml: :attr}#{epub_type_attr} id="#{
|
451
|
+
<section class="chapter" title=#{chapter_title.encode xml: :attr}#{epub_type_attr} id="#{filename}">
|
417
452
|
#{header}
|
418
453
|
#{content})
|
419
454
|
|
420
455
|
unless (fns = node.document.footnotes - @footnotes).empty?
|
421
456
|
@footnotes += fns
|
422
457
|
|
423
|
-
# NOTE kindlegen seems to mangle the <footer> element, so we wrap its content in a div
|
458
|
+
# NOTE: kindlegen seems to mangle the <footer> element, so we wrap its content in a div
|
424
459
|
lines << '<footer>
|
425
460
|
<div class="chapter-footer">
|
426
461
|
<div class="footnotes">'
|
427
462
|
fns.each do |footnote|
|
428
463
|
lines << %(<aside id="note-#{footnote.index}" epub:type="footnote">
|
429
|
-
<p
|
464
|
+
<p>#{footnote.text}</p>
|
430
465
|
</aside>)
|
431
466
|
end
|
432
467
|
lines << '</div>
|
@@ -436,7 +471,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
436
471
|
|
437
472
|
lines << '</section>'
|
438
473
|
|
439
|
-
|
474
|
+
if syntax_hl&.docinfo? :footer
|
475
|
+
lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss,
|
476
|
+
self_closing_tag_slash: '/')
|
477
|
+
end
|
440
478
|
|
441
479
|
lines << '</body>
|
442
480
|
</html>'
|
@@ -451,39 +489,43 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
451
489
|
chapter_item
|
452
490
|
end
|
453
491
|
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
492
|
+
# @param node [Asciidoctor::Section]
|
493
|
+
def convert_section(node)
|
494
|
+
return unless add_chapter(node).nil?
|
495
|
+
|
496
|
+
hlevel = node.level.clamp 1, 6
|
497
|
+
epub_type_attr = node.sectname == 'section' ? '' : %( epub:type="#{node.sectname}")
|
498
|
+
div_classes = [%(sect#{node.level}), node.role].compact
|
499
|
+
title = get_numbered_title node
|
500
|
+
%(<section class="#{div_classes * ' '}" title=#{title.encode xml: :attr}#{epub_type_attr}>
|
501
|
+
<h#{hlevel} id="#{node.id}">#{title}</h#{hlevel}>#{if (content = node.content).empty?
|
502
|
+
''
|
503
|
+
else
|
504
|
+
%(
|
505
|
+
#{content})
|
506
|
+
end}
|
463
507
|
</section>)
|
464
|
-
end
|
465
508
|
end
|
466
509
|
|
467
|
-
# NOTE embedded is used for AsciiDoc table cell content
|
468
|
-
def convert_embedded
|
510
|
+
# NOTE: embedded is used for AsciiDoc table cell content
|
511
|
+
def convert_embedded(node)
|
469
512
|
node.content
|
470
513
|
end
|
471
514
|
|
472
515
|
# TODO: support use of quote block as abstract
|
473
|
-
def convert_preamble
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
end
|
516
|
+
def convert_preamble(node)
|
517
|
+
return unless add_chapter(node).nil?
|
518
|
+
|
519
|
+
if ((first_block = node.blocks[0]) && first_block.style == 'abstract') ||
|
520
|
+
# REVIEW: should we treat the preamble as an abstract in general?
|
521
|
+
(first_block && node.blocks.size == 1)
|
522
|
+
convert_abstract first_block
|
523
|
+
else
|
524
|
+
node.content
|
483
525
|
end
|
484
526
|
end
|
485
527
|
|
486
|
-
def convert_open
|
528
|
+
def convert_open(node)
|
487
529
|
id_attr = node.id ? %( id="#{node.id}") : nil
|
488
530
|
class_attr = node.role ? %( class="#{node.role}") : nil
|
489
531
|
if id_attr || class_attr
|
@@ -495,18 +537,22 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
495
537
|
end
|
496
538
|
end
|
497
539
|
|
498
|
-
def convert_abstract
|
540
|
+
def convert_abstract(node)
|
499
541
|
%(<div class="abstract" epub:type="preamble">
|
500
542
|
#{output_content node}
|
501
543
|
</div>)
|
502
544
|
end
|
503
545
|
|
504
|
-
def convert_paragraph
|
546
|
+
def convert_paragraph(node)
|
505
547
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
506
548
|
role = node.role
|
507
549
|
# stack-head is the alternative to the default, inline-head (where inline means "run-in")
|
508
550
|
head_stop = node.attr 'head-stop', (role && (node.has_role? 'stack-head') ? nil : '.')
|
509
|
-
head =
|
551
|
+
head = if node.title?
|
552
|
+
%(<strong class="head">#{title = node.title}#{head_stop && title !~ TRAILING_PUNCT_RX ? head_stop : ''}</strong> )
|
553
|
+
else
|
554
|
+
''
|
555
|
+
end
|
510
556
|
if role
|
511
557
|
node.set_option 'hardbreaks' if node.has_role? 'signature'
|
512
558
|
%(<p#{id_attr} class="#{role}">#{head}#{node.content}</p>)
|
@@ -515,7 +561,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
515
561
|
end
|
516
562
|
end
|
517
563
|
|
518
|
-
def convert_pass
|
564
|
+
def convert_pass(node)
|
519
565
|
content = node.content
|
520
566
|
if content == '<?hard-pagebreak?>'
|
521
567
|
'<hr epub:type="pagebreak" class="pagebreak"/>'
|
@@ -524,7 +570,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
524
570
|
end
|
525
571
|
end
|
526
572
|
|
527
|
-
def convert_admonition
|
573
|
+
def convert_admonition(node)
|
528
574
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
529
575
|
if node.title?
|
530
576
|
title = node.title
|
@@ -547,17 +593,21 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
547
593
|
logger.warn %(unknown admonition type: #{type})
|
548
594
|
'notice'
|
549
595
|
end
|
550
|
-
%(<aside#{id_attr} class="admonition #{type}"#{title_attr} epub:type="#{epub_type}">
|
596
|
+
%(<aside#{id_attr} class="admonition #{type}#{(role = node.role) ? " #{role}" : ''}"#{title_attr} epub:type="#{epub_type}">
|
551
597
|
#{title_el}<div class="content">
|
552
598
|
#{output_content node}
|
553
599
|
</div>
|
554
600
|
</aside>)
|
555
601
|
end
|
556
602
|
|
557
|
-
def convert_example
|
603
|
+
def convert_example(node)
|
558
604
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
559
|
-
title_div = node.title?
|
560
|
-
|
605
|
+
title_div = if node.title?
|
606
|
+
%(<div class="example-title">#{node.title}</div>
|
607
|
+
)
|
608
|
+
else
|
609
|
+
''
|
610
|
+
end
|
561
611
|
%(<div#{id_attr} class="example">
|
562
612
|
#{title_div}<div class="example-content">
|
563
613
|
#{output_content node}
|
@@ -565,23 +615,28 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
565
615
|
</div>)
|
566
616
|
end
|
567
617
|
|
568
|
-
def convert_floating_title
|
618
|
+
def convert_floating_title(node)
|
569
619
|
tag_name = %(h#{node.level + 1})
|
570
620
|
id_attribute = node.id ? %( id="#{node.id}") : ''
|
571
621
|
%(<#{tag_name}#{id_attribute} class="#{['discrete', node.role].compact * ' '}">#{node.title}</#{tag_name}>)
|
572
622
|
end
|
573
623
|
|
574
|
-
|
624
|
+
# @param node [Asciidoctor::Block]
|
625
|
+
def convert_listing(node)
|
575
626
|
id_attribute = node.id ? %( id="#{node.id}") : ''
|
576
627
|
nowrap = (node.option? 'nowrap') || !(node.document.attr? 'prewrap')
|
577
628
|
if node.style == 'source'
|
578
629
|
lang = node.attr 'language'
|
579
630
|
syntax_hl = node.document.syntax_highlighter
|
580
631
|
if syntax_hl
|
581
|
-
opts = syntax_hl.highlight?
|
582
|
-
|
583
|
-
|
584
|
-
|
632
|
+
opts = if syntax_hl.highlight?
|
633
|
+
{
|
634
|
+
css_mode: ((doc_attrs = node.document.attributes)[%(#{syntax_hl.name}-css)] || :class).to_sym,
|
635
|
+
style: doc_attrs[%(#{syntax_hl.name}-style)]
|
636
|
+
}
|
637
|
+
else
|
638
|
+
{}
|
639
|
+
end
|
585
640
|
opts[:nowrap] = nowrap
|
586
641
|
else
|
587
642
|
pre_open = %(<pre class="highlight#{nowrap ? ' nowrap' : ''}"><code#{lang ? %( class="language-#{lang}" data-lang="#{lang}") : ''}>)
|
@@ -600,7 +655,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
600
655
|
</figure>)
|
601
656
|
end
|
602
657
|
|
603
|
-
def convert_stem
|
658
|
+
def convert_stem(node)
|
604
659
|
return convert_listing node if node.style != 'asciimath' || !asciimath_available?
|
605
660
|
|
606
661
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
@@ -623,7 +678,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
623
678
|
Helpers.require_library('asciimath', true, :warn).nil? ? :unavailable : :loaded
|
624
679
|
end
|
625
680
|
|
626
|
-
def convert_literal
|
681
|
+
def convert_literal(node)
|
627
682
|
id_attribute = node.id ? %( id="#{node.id}") : ''
|
628
683
|
title_element = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
|
629
684
|
%(<figure#{id_attribute} class="literalblock#{prepend_space node.role}">
|
@@ -632,15 +687,15 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
632
687
|
</figure>)
|
633
688
|
end
|
634
689
|
|
635
|
-
def convert_page_break
|
690
|
+
def convert_page_break(_node)
|
636
691
|
'<hr epub:type="pagebreak" class="pagebreak"/>'
|
637
692
|
end
|
638
693
|
|
639
|
-
def convert_thematic_break
|
694
|
+
def convert_thematic_break(_node)
|
640
695
|
'<hr class="thematicbreak"/>'
|
641
696
|
end
|
642
697
|
|
643
|
-
def convert_quote
|
698
|
+
def convert_quote(node)
|
644
699
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
645
700
|
class_attr = (role = node.role) ? %( class="blockquote #{role}") : ' class="blockquote"'
|
646
701
|
|
@@ -656,8 +711,12 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
656
711
|
|
657
712
|
footer_content << %(<span class="context">#{node.title}</span>) if node.title?
|
658
713
|
|
659
|
-
footer_tag = footer_content.empty?
|
714
|
+
footer_tag = if footer_content.empty?
|
715
|
+
''
|
716
|
+
else
|
717
|
+
%(
|
660
718
|
<footer>~ #{footer_content * ' '}</footer>)
|
719
|
+
end
|
661
720
|
content = (output_content node).strip
|
662
721
|
%(<div#{id_attr}#{class_attr}>
|
663
722
|
<blockquote>
|
@@ -666,7 +725,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
666
725
|
</div>)
|
667
726
|
end
|
668
727
|
|
669
|
-
def convert_verse
|
728
|
+
def convert_verse(node)
|
670
729
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
671
730
|
class_attr = (role = node.role) ? %( class="verse #{role}") : ' class="verse"'
|
672
731
|
|
@@ -680,14 +739,18 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
680
739
|
footer_content << %(<cite title="#{citetitle_sanitized}">#{citetitle}</cite>)
|
681
740
|
end
|
682
741
|
|
683
|
-
footer_tag =
|
684
|
-
|
742
|
+
footer_tag = if footer_content.empty?
|
743
|
+
''
|
744
|
+
else
|
745
|
+
%(
|
746
|
+
<span class="attribution">~ #{footer_content * ', '}</span>)
|
747
|
+
end
|
685
748
|
%(<div#{id_attr}#{class_attr}>
|
686
749
|
<pre>#{node.content}#{footer_tag}</pre>
|
687
750
|
</div>)
|
688
751
|
end
|
689
752
|
|
690
|
-
def convert_sidebar
|
753
|
+
def convert_sidebar(node)
|
691
754
|
classes = ['sidebar']
|
692
755
|
if node.title?
|
693
756
|
classes << 'titled'
|
@@ -707,18 +770,21 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
707
770
|
</aside>)
|
708
771
|
end
|
709
772
|
|
710
|
-
def convert_table
|
773
|
+
def convert_table(node)
|
711
774
|
lines = [%(<div class="table">)]
|
712
775
|
lines << %(<div class="content">)
|
713
776
|
table_id_attr = node.id ? %( id="#{node.id}") : ''
|
714
777
|
table_classes = [
|
715
778
|
'table',
|
716
779
|
%(table-framed-#{node.attr 'frame', 'rows', 'table-frame'}),
|
717
|
-
%(table-grid-#{node.attr 'grid', 'rows', 'table-grid'})
|
780
|
+
%(table-grid-#{node.attr 'grid', 'rows', 'table-grid'})
|
718
781
|
]
|
719
782
|
if (role = node.role)
|
720
783
|
table_classes << role
|
721
784
|
end
|
785
|
+
if (float = node.attr 'float')
|
786
|
+
table_classes << float
|
787
|
+
end
|
722
788
|
table_styles = []
|
723
789
|
if (autowidth = node.option? 'autowidth') && !(node.attr? 'width')
|
724
790
|
table_classes << 'fit-content'
|
@@ -726,21 +792,21 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
726
792
|
table_styles << %(width: #{node.attr 'tablepcwidth'}%;)
|
727
793
|
end
|
728
794
|
table_class_attr = %( class="#{table_classes * ' '}")
|
729
|
-
table_style_attr =
|
795
|
+
table_style_attr = table_styles.empty? ? '' : %( style="#{table_styles * '; '}")
|
730
796
|
|
731
797
|
lines << %(<table#{table_id_attr}#{table_class_attr}#{table_style_attr}>)
|
732
798
|
lines << %(<caption>#{node.captioned_title}</caption>) if node.title?
|
733
|
-
if (node.attr 'rowcount')
|
799
|
+
if (node.attr 'rowcount').positive?
|
734
800
|
lines << '<colgroup>'
|
735
801
|
if autowidth
|
736
802
|
lines += (Array.new node.columns.size, %(<col/>))
|
737
803
|
else
|
738
804
|
node.columns.each do |col|
|
739
|
-
lines << (
|
805
|
+
lines << (col.option?('autowidth') ? %(<col/>) : %(<col style="width: #{col.attr 'colpcwidth'}%;" />))
|
740
806
|
end
|
741
807
|
end
|
742
808
|
lines << '</colgroup>'
|
743
|
-
[
|
809
|
+
%i[head body foot].reject { |tsec| node.rows[tsec].empty? }.each do |tsec|
|
744
810
|
lines << %(<t#{tsec}>)
|
745
811
|
node.rows[tsec].each do |row|
|
746
812
|
lines << '<tr>'
|
@@ -766,12 +832,12 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
766
832
|
cell_tag_name = tsec == :head || cell.style == :header ? 'th' : 'td'
|
767
833
|
cell_classes = [
|
768
834
|
"halign-#{cell.attr 'halign'}",
|
769
|
-
"valign-#{cell.attr 'valign'}"
|
835
|
+
"valign-#{cell.attr 'valign'}"
|
770
836
|
]
|
771
|
-
cell_class_attr =
|
837
|
+
cell_class_attr = cell_classes.empty? ? '' : %( class="#{cell_classes * ' '}")
|
772
838
|
cell_colspan_attr = cell.colspan ? %( colspan="#{cell.colspan}") : ''
|
773
839
|
cell_rowspan_attr = cell.rowspan ? %( rowspan="#{cell.rowspan}") : ''
|
774
|
-
cell_style_attr =
|
840
|
+
cell_style_attr = node.document.attr?('cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'}") : ''
|
775
841
|
lines << %(<#{cell_tag_name}#{cell_class_attr}#{cell_colspan_attr}#{cell_rowspan_attr}#{cell_style_attr}>#{cell_content}</#{cell_tag_name}>)
|
776
842
|
end
|
777
843
|
lines << '</tr>'
|
@@ -785,10 +851,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
785
851
|
lines * LF
|
786
852
|
end
|
787
853
|
|
788
|
-
def convert_colist
|
854
|
+
def convert_colist(node)
|
789
855
|
lines = ['<div class="callout-list">
|
790
856
|
<ol>']
|
791
|
-
num =
|
857
|
+
num = CALLOUT_START_NUM
|
792
858
|
node.items.each_with_index do |item, i|
|
793
859
|
lines << %(<li><i class="conum" data-value="#{i + 1}">#{num}</i> #{item.text}</li>)
|
794
860
|
num = num.next
|
@@ -798,7 +864,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
798
864
|
end
|
799
865
|
|
800
866
|
# TODO: add complex class if list has nested blocks
|
801
|
-
def convert_dlist
|
867
|
+
def convert_dlist(node)
|
802
868
|
lines = []
|
803
869
|
id_attribute = node.id ? %( id="#{node.id}") : ''
|
804
870
|
|
@@ -806,7 +872,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
806
872
|
when 'horizontal'
|
807
873
|
['hdlist', node.role]
|
808
874
|
when 'itemized', 'ordered'
|
809
|
-
# QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
|
875
|
+
# QUESTION: should we just use itemized-list and ordered-list as the class here? or just list?
|
810
876
|
['dlist', %(#{node.style}-list), node.role]
|
811
877
|
else
|
812
878
|
['description-list']
|
@@ -822,13 +888,13 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
822
888
|
list_tag_name = style == 'itemized' ? 'ul' : 'ol'
|
823
889
|
role = node.role
|
824
890
|
subject_stop = node.attr 'subject-stop', (role && (node.has_role? 'stack') ? nil : ':')
|
825
|
-
list_class_attr =
|
891
|
+
list_class_attr = node.option?('brief') ? ' class="brief"' : ''
|
826
892
|
lines << %(<#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
|
827
893
|
node.items.each do |subjects, dd|
|
828
894
|
# consists of one term (a subject) and supporting content
|
829
|
-
subject =
|
895
|
+
subject = Array(subjects).first.text
|
830
896
|
subject_plain = xml_sanitize subject, :plain
|
831
|
-
subject_element = %(<strong class="subject">#{subject}#{subject_stop && subject_plain !~
|
897
|
+
subject_element = %(<strong class="subject">#{subject}#{subject_stop && subject_plain !~ TRAILING_PUNCT_RX ? subject_stop : ''}</strong>)
|
832
898
|
lines << '<li>'
|
833
899
|
if dd
|
834
900
|
# NOTE: must wrap remaining text in a span to help webkit justify the text properly
|
@@ -844,15 +910,15 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
844
910
|
lines << '<table>'
|
845
911
|
if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
|
846
912
|
lines << '<colgroup>'
|
847
|
-
col_style_attribute =
|
913
|
+
col_style_attribute = node.attr?('labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : ''
|
848
914
|
lines << %(<col#{col_style_attribute} />)
|
849
|
-
col_style_attribute =
|
915
|
+
col_style_attribute = node.attr?('itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : ''
|
850
916
|
lines << %(<col#{col_style_attribute} />)
|
851
917
|
lines << '</colgroup>'
|
852
918
|
end
|
853
919
|
node.items.each do |terms, dd|
|
854
920
|
lines << '<tr>'
|
855
|
-
lines << %(<td class="hdlist1#{
|
921
|
+
lines << %(<td class="hdlist1#{node.option?('strong') ? ' strong' : ''}">)
|
856
922
|
first_term = true
|
857
923
|
terms.each do |dt|
|
858
924
|
lines << %(<br />) unless first_term
|
@@ -874,12 +940,13 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
874
940
|
else
|
875
941
|
lines << '<dl>'
|
876
942
|
node.items.each do |terms, dd|
|
877
|
-
|
943
|
+
Array(terms).each do |dt|
|
878
944
|
lines << %(<dt>
|
879
945
|
<span class="term">#{dt.text}</span>
|
880
946
|
</dt>)
|
881
947
|
end
|
882
948
|
next unless dd
|
949
|
+
|
883
950
|
lines << '<dd>'
|
884
951
|
if dd.blocks?
|
885
952
|
lines << %(<span class="principal">#{dd.text}</span>) if dd.text?
|
@@ -896,22 +963,22 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
896
963
|
lines * LF
|
897
964
|
end
|
898
965
|
|
899
|
-
def convert_olist
|
966
|
+
def convert_olist(node)
|
900
967
|
complex = false
|
901
968
|
div_classes = ['ordered-list', node.style, node.role].compact
|
902
|
-
ol_classes = [node.style, (
|
969
|
+
ol_classes = [node.style, (node.option?('brief') ? 'brief' : nil)].compact
|
903
970
|
ol_class_attr = ol_classes.empty? ? '' : %( class="#{ol_classes * ' '}")
|
904
|
-
ol_start_attr =
|
971
|
+
ol_start_attr = node.attr?('start') ? %( start="#{node.attr 'start'}") : ''
|
905
972
|
id_attribute = node.id ? %( id="#{node.id}") : ''
|
906
973
|
lines = [%(<div#{id_attribute} class="#{div_classes * ' '}">)]
|
907
974
|
lines << %(<h3 class="list-heading">#{node.title}</h3>) if node.title?
|
908
|
-
lines << %(<ol#{ol_class_attr}#{ol_start_attr}#{
|
975
|
+
lines << %(<ol#{ol_class_attr}#{ol_start_attr}#{node.option?('reversed') ? ' reversed="reversed"' : ''}>)
|
909
976
|
node.items.each do |item|
|
910
977
|
lines << %(<li>
|
911
978
|
<span class="principal">#{item.text}</span>)
|
912
979
|
if item.blocks?
|
913
980
|
lines << item.content
|
914
|
-
complex = true unless item.blocks.size == 1 &&
|
981
|
+
complex = true unless item.blocks.size == 1 && item.blocks[0].is_a?(::Asciidoctor::List)
|
915
982
|
end
|
916
983
|
lines << '</li>'
|
917
984
|
end
|
@@ -924,10 +991,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
924
991
|
lines * LF
|
925
992
|
end
|
926
993
|
|
927
|
-
def convert_ulist
|
994
|
+
def convert_ulist(node)
|
928
995
|
complex = false
|
929
996
|
div_classes = ['itemized-list', node.style, node.role].compact
|
930
|
-
ul_classes = [node.style, (
|
997
|
+
ul_classes = [node.style, (node.option?('brief') ? 'brief' : nil)].compact
|
931
998
|
ul_class_attr = ul_classes.empty? ? '' : %( class="#{ul_classes * ' '}")
|
932
999
|
id_attribute = node.id ? %( id="#{node.id}") : ''
|
933
1000
|
lines = [%(<div#{id_attribute} class="#{div_classes * ' '}">)]
|
@@ -938,7 +1005,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
938
1005
|
<span class="principal">#{item.text}</span>)
|
939
1006
|
if item.blocks?
|
940
1007
|
lines << item.content
|
941
|
-
complex = true unless item.blocks.size == 1 &&
|
1008
|
+
complex = true unless item.blocks.size == 1 && item.blocks[0].is_a?(::Asciidoctor::List)
|
942
1009
|
end
|
943
1010
|
lines << '</li>'
|
944
1011
|
end
|
@@ -951,22 +1018,23 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
951
1018
|
lines * LF
|
952
1019
|
end
|
953
1020
|
|
954
|
-
def doc_option
|
1021
|
+
def doc_option(document, key)
|
955
1022
|
loop do
|
956
1023
|
value = document.options[key]
|
957
1024
|
return value unless value.nil?
|
1025
|
+
|
958
1026
|
document = document.parent_document
|
959
1027
|
break if document.nil?
|
960
1028
|
end
|
961
1029
|
nil
|
962
1030
|
end
|
963
1031
|
|
964
|
-
def root_document
|
1032
|
+
def root_document(document)
|
965
1033
|
document = document.parent_document until document.parent_document.nil?
|
966
1034
|
document
|
967
1035
|
end
|
968
1036
|
|
969
|
-
def register_media_file
|
1037
|
+
def register_media_file(node, target, media_type)
|
970
1038
|
if target.end_with?('.svg') || target.start_with?('data:image/svg+xml')
|
971
1039
|
chapter = get_enclosing_chapter node
|
972
1040
|
chapter.set_attr 'epub-properties', [] unless chapter.attr? 'epub-properties'
|
@@ -991,9 +1059,14 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
991
1059
|
@media_files[target] ||= { path: fs_path, media_type: media_type }
|
992
1060
|
end
|
993
1061
|
|
994
|
-
|
1062
|
+
# @param node [Asciidoctor::Block]
|
1063
|
+
# @return [Array<String>]
|
1064
|
+
def resolve_image_attrs(node)
|
995
1065
|
img_attrs = []
|
996
|
-
|
1066
|
+
|
1067
|
+
unless (alt = encode_attribute_value(node.alt)).empty?
|
1068
|
+
img_attrs << %(alt="#{alt}")
|
1069
|
+
end
|
997
1070
|
|
998
1071
|
# Unlike browsers, Calibre/Kindle *do* scale image if only height is specified
|
999
1072
|
# So, in order to match browser behavior, we just always omit height
|
@@ -1004,33 +1077,33 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1004
1077
|
# HTML5 spec (and EPUBCheck) only allows pixels in width, but browsers also accept percents
|
1005
1078
|
# and there are multiple AsciiDoc files in the wild that have width=percents%
|
1006
1079
|
# So, for compatibility reasons, output percentage width as a CSS style
|
1007
|
-
if width[/^\d+%$/]
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1080
|
+
img_attrs << if width[/^\d+%$/]
|
1081
|
+
%(style="width: #{width}")
|
1082
|
+
else
|
1083
|
+
%(width="#{width}")
|
1084
|
+
end
|
1012
1085
|
end
|
1013
1086
|
|
1014
1087
|
img_attrs
|
1015
1088
|
end
|
1016
1089
|
|
1017
|
-
def convert_audio
|
1090
|
+
def convert_audio(node)
|
1018
1091
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
1019
1092
|
target = node.media_uri node.attr 'target'
|
1020
1093
|
register_media_file node, target, 'audio'
|
1021
1094
|
title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
|
1022
1095
|
|
1023
|
-
autoplay_attr =
|
1024
|
-
controls_attr =
|
1025
|
-
loop_attr =
|
1096
|
+
autoplay_attr = node.option?('autoplay') ? ' autoplay="autoplay"' : ''
|
1097
|
+
controls_attr = node.option?('nocontrols') ? '' : ' controls="controls"'
|
1098
|
+
loop_attr = node.option?('loop') ? ' loop="loop"' : ''
|
1026
1099
|
|
1027
1100
|
start_t = node.attr 'start'
|
1028
1101
|
end_t = node.attr 'end'
|
1029
|
-
if start_t || end_t
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1102
|
+
time_anchor = if start_t || end_t
|
1103
|
+
%(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
|
1104
|
+
else
|
1105
|
+
''
|
1106
|
+
end
|
1034
1107
|
|
1035
1108
|
%(<figure#{id_attr} class="audioblock#{prepend_space node.role}">#{title_element}
|
1036
1109
|
<div class="content">
|
@@ -1042,25 +1115,25 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1042
1115
|
end
|
1043
1116
|
|
1044
1117
|
# TODO: Support multiple video files in different formats for a single video
|
1045
|
-
def convert_video
|
1118
|
+
def convert_video(node)
|
1046
1119
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
1047
1120
|
target = node.media_uri node.attr 'target'
|
1048
1121
|
register_media_file node, target, 'video'
|
1049
1122
|
title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
|
1050
1123
|
|
1051
|
-
width_attr =
|
1052
|
-
height_attr =
|
1053
|
-
autoplay_attr =
|
1054
|
-
controls_attr =
|
1055
|
-
loop_attr =
|
1124
|
+
width_attr = node.attr?('width') ? %( width="#{node.attr 'width'}") : ''
|
1125
|
+
height_attr = node.attr?('height') ? %( height="#{node.attr 'height'}") : ''
|
1126
|
+
autoplay_attr = node.option?('autoplay') ? ' autoplay="autoplay"' : ''
|
1127
|
+
controls_attr = node.option?('nocontrols') ? '' : ' controls="controls"'
|
1128
|
+
loop_attr = node.option?('loop') ? ' loop="loop"' : ''
|
1056
1129
|
|
1057
1130
|
start_t = node.attr 'start'
|
1058
1131
|
end_t = node.attr 'end'
|
1059
|
-
if start_t || end_t
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1132
|
+
time_anchor = if start_t || end_t
|
1133
|
+
%(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
|
1134
|
+
else
|
1135
|
+
''
|
1136
|
+
end
|
1064
1137
|
|
1065
1138
|
if (poster = node.attr 'poster').nil_or_empty?
|
1066
1139
|
poster_attr = ''
|
@@ -1070,7 +1143,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1070
1143
|
poster_attr = %( poster="#{poster}")
|
1071
1144
|
end
|
1072
1145
|
|
1073
|
-
%(<figure#{id_attr} class="video#{prepend_space node.role}">#{title_element}
|
1146
|
+
%(<figure#{id_attr} class="video#{prepend_space node.role}#{prepend_space node.attr('float')}">#{title_element}
|
1074
1147
|
<div class="content">
|
1075
1148
|
<video src="#{target}#{time_anchor}"#{width_attr}#{height_attr}#{autoplay_attr}#{poster_attr}#{controls_attr}#{loop_attr}>
|
1076
1149
|
<div>Your Reading System does not support (this) video.</div>
|
@@ -1079,35 +1152,41 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1079
1152
|
</figure>)
|
1080
1153
|
end
|
1081
1154
|
|
1082
|
-
|
1155
|
+
# @param node [Asciidoctor::Block]
|
1156
|
+
# @return [String]
|
1157
|
+
def convert_image(node)
|
1083
1158
|
target = node.image_uri node.attr 'target'
|
1084
1159
|
register_media_file node, target, 'image'
|
1085
1160
|
id_attr = node.id ? %( id="#{node.id}") : ''
|
1086
1161
|
title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
|
1087
1162
|
img_attrs = resolve_image_attrs node
|
1088
|
-
%(<figure#{id_attr} class="image#{prepend_space node.role}">
|
1163
|
+
%(<figure#{id_attr} class="image#{prepend_space node.role}#{prepend_space node.attr('float')}">
|
1089
1164
|
<div class="content">
|
1090
1165
|
<img src="#{target}"#{prepend_space img_attrs * ' '} />
|
1091
1166
|
</div>#{title_element}
|
1092
1167
|
</figure>)
|
1093
1168
|
end
|
1094
1169
|
|
1095
|
-
def get_enclosing_chapter
|
1170
|
+
def get_enclosing_chapter(node)
|
1096
1171
|
loop do
|
1097
1172
|
return nil if node.nil?
|
1098
|
-
return node unless
|
1173
|
+
return node unless get_chapter_filename(node).nil?
|
1174
|
+
|
1099
1175
|
node = node.parent
|
1100
1176
|
end
|
1101
1177
|
end
|
1102
1178
|
|
1103
|
-
def convert_inline_anchor
|
1179
|
+
def convert_inline_anchor(node)
|
1104
1180
|
case node.type
|
1105
1181
|
when :xref
|
1106
|
-
doc
|
1182
|
+
doc = node.document
|
1183
|
+
refid = node.attr('refid')
|
1184
|
+
target = node.target
|
1185
|
+
text = node.text
|
1107
1186
|
id_attr = ''
|
1108
1187
|
|
1109
1188
|
if (path = node.attributes['path'])
|
1110
|
-
# NOTE non-nil path indicates this is an inter-document xref that's not included in current document
|
1189
|
+
# NOTE: non-nil path indicates this is an inter-document xref that's not included in current document
|
1111
1190
|
text = node.text || path
|
1112
1191
|
elsif refid == '#'
|
1113
1192
|
logger.warn %(#{::File.basename doc.attr('docfile')}: <<chapter#>> xref syntax isn't supported anymore. Use either <<chapter>> or <<chapter#anchor>>)
|
@@ -1116,7 +1195,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1116
1195
|
our_chapter = get_enclosing_chapter node
|
1117
1196
|
ref_chapter = get_enclosing_chapter ref
|
1118
1197
|
if ref_chapter
|
1119
|
-
ref_docname =
|
1198
|
+
ref_docname = get_chapter_filename ref_chapter
|
1120
1199
|
if ref_chapter == our_chapter
|
1121
1200
|
# ref within same chapter file
|
1122
1201
|
id_attr = %( id="xref-#{refid}")
|
@@ -1140,12 +1219,12 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1140
1219
|
|
1141
1220
|
%(<a#{id_attr} href="#{target}" class="xref">#{text || "[#{refid}]"}</a>)
|
1142
1221
|
when :ref
|
1143
|
-
# NOTE id is used instead of target starting in Asciidoctor 2.0.0
|
1222
|
+
# NOTE: id is used instead of target starting in Asciidoctor 2.0.0
|
1144
1223
|
%(<a id="#{node.target || node.id}"></a>)
|
1145
1224
|
when :link
|
1146
1225
|
%(<a href="#{node.target}" class="link">#{node.text}</a>)
|
1147
1226
|
when :bibref
|
1148
|
-
# NOTE reftext is no longer enclosed in [] starting in Asciidoctor 2.0.0
|
1227
|
+
# NOTE: reftext is no longer enclosed in [] starting in Asciidoctor 2.0.0
|
1149
1228
|
# NOTE id is used instead of target starting in Asciidoctor 2.0.0
|
1150
1229
|
if (reftext = node.reftext)
|
1151
1230
|
reftext = %([#{reftext}]) unless reftext.start_with? '['
|
@@ -1159,30 +1238,37 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1159
1238
|
end
|
1160
1239
|
end
|
1161
1240
|
|
1162
|
-
def convert_inline_break
|
1241
|
+
def convert_inline_break(node)
|
1163
1242
|
%(#{node.text}<br/>)
|
1164
1243
|
end
|
1165
1244
|
|
1166
|
-
|
1167
|
-
|
1245
|
+
# @param node [Asciidoctor::Inline]
|
1246
|
+
# @return [String]
|
1247
|
+
def convert_inline_button(node)
|
1248
|
+
%(<b class="button">#{node.text}</b>)
|
1168
1249
|
end
|
1169
1250
|
|
1170
|
-
def convert_inline_callout
|
1171
|
-
num =
|
1251
|
+
def convert_inline_callout(node)
|
1252
|
+
num = CALLOUT_START_NUM
|
1172
1253
|
int_num = node.text.to_i
|
1173
1254
|
(int_num - 1).times { num = num.next }
|
1174
1255
|
%(<i class="conum" data-value="#{int_num}">#{num}</i>)
|
1175
1256
|
end
|
1176
1257
|
|
1177
|
-
|
1258
|
+
# @param node [Asciidoctor::Inline]
|
1259
|
+
# @return [String]
|
1260
|
+
def convert_inline_footnote(node)
|
1178
1261
|
if (index = node.attr 'index')
|
1179
|
-
|
1262
|
+
attrs = []
|
1263
|
+
attrs << %(id="#{node.id}") if node.id
|
1264
|
+
|
1265
|
+
%(<sup class="noteref">[<a#{prepend_space attrs * ' '}href="#note-#{index}" epub:type="noteref">#{index}</a>]</sup>)
|
1180
1266
|
elsif node.type == :xref
|
1181
1267
|
%(<mark class="noteref" title="Unresolved note reference">#{node.text}</mark>)
|
1182
1268
|
end
|
1183
1269
|
end
|
1184
1270
|
|
1185
|
-
def convert_inline_image
|
1271
|
+
def convert_inline_image(node)
|
1186
1272
|
if node.type == 'icon'
|
1187
1273
|
icon_names << (icon_name = node.target)
|
1188
1274
|
i_classes = ['icon', %(i-#{icon_name})]
|
@@ -1190,36 +1276,37 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1190
1276
|
i_classes << %(icon-flip-#{(node.attr 'flip')[0]}) if node.attr? 'flip'
|
1191
1277
|
i_classes << %(icon-rotate-#{node.attr 'rotate'}) if node.attr? 'rotate'
|
1192
1278
|
i_classes << node.role if node.role?
|
1279
|
+
i_classes << node.attr('float') if node.attr 'float'
|
1193
1280
|
%(<i class="#{i_classes * ' '}"></i>)
|
1194
1281
|
else
|
1195
1282
|
target = node.image_uri node.target
|
1196
1283
|
register_media_file node, target, 'image'
|
1197
1284
|
|
1198
1285
|
img_attrs = resolve_image_attrs node
|
1199
|
-
img_attrs << %(class="inline#{prepend_space node.role}")
|
1286
|
+
img_attrs << %(class="inline#{prepend_space node.role}#{prepend_space node.attr('float')}")
|
1200
1287
|
%(<img src="#{target}"#{prepend_space img_attrs * ' '}/>)
|
1201
1288
|
end
|
1202
1289
|
end
|
1203
1290
|
|
1204
|
-
def convert_inline_indexterm
|
1291
|
+
def convert_inline_indexterm(node)
|
1205
1292
|
node.type == :visible ? node.text : ''
|
1206
1293
|
end
|
1207
1294
|
|
1208
|
-
def convert_inline_kbd
|
1295
|
+
def convert_inline_kbd(node)
|
1209
1296
|
if (keys = node.attr 'keys').size == 1
|
1210
1297
|
%(<kbd>#{keys[0]}</kbd>)
|
1211
1298
|
else
|
1212
|
-
key_combo = keys.map {|key| %(<kbd>#{key}</kbd>) }.join '+'
|
1299
|
+
key_combo = keys.map { |key| %(<kbd>#{key}</kbd>) }.join '+'
|
1213
1300
|
%(<span class="keyseq">#{key_combo}</span>)
|
1214
1301
|
end
|
1215
1302
|
end
|
1216
1303
|
|
1217
|
-
def convert_inline_menu
|
1304
|
+
def convert_inline_menu(node)
|
1218
1305
|
menu = node.attr 'menu'
|
1219
|
-
# NOTE we swap right angle quote with chevron right from FontAwesome using CSS
|
1220
|
-
caret = %(#{
|
1306
|
+
# NOTE: we swap right angle quote with chevron right from FontAwesome using CSS
|
1307
|
+
caret = %(#{NO_BREAK_SPACE}<span class="caret">#{RIGHT_ANGLE_QUOTE}</span> )
|
1221
1308
|
if !(submenus = node.attr 'submenus').empty?
|
1222
|
-
submenu_path = submenus.map {|submenu| %(<span class="submenu">#{submenu}</span>#{caret}) }.join.chop
|
1309
|
+
submenu_path = submenus.map { |submenu| %(<span class="submenu">#{submenu}</span>#{caret}) }.join.chop
|
1223
1310
|
%(<span class="menuseq"><span class="menu">#{menu}</span>#{caret}#{submenu_path} <span class="menuitem">#{node.attr 'menuitem'}</span></span>)
|
1224
1311
|
elsif (menuitem = node.attr 'menuitem')
|
1225
1312
|
%(<span class="menuseq"><span class="menu">#{menu}</span>#{caret}<span class="menuitem">#{menuitem}</span></span>)
|
@@ -1228,16 +1315,16 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1228
1315
|
end
|
1229
1316
|
end
|
1230
1317
|
|
1231
|
-
def convert_inline_quoted
|
1318
|
+
def convert_inline_quoted(node)
|
1232
1319
|
open, close, tag = QUOTE_TAGS[node.type]
|
1233
1320
|
|
1234
|
-
if node.type == :asciimath && asciimath_available?
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1321
|
+
content = if node.type == :asciimath && asciimath_available?
|
1322
|
+
AsciiMath.parse(node.text).to_mathml 'mml:'
|
1323
|
+
else
|
1324
|
+
node.text
|
1325
|
+
end
|
1239
1326
|
|
1240
|
-
node.add_role 'literal' if [
|
1327
|
+
node.add_role 'literal' if %i[monospaced asciimath latexmath].include? node.type
|
1241
1328
|
|
1242
1329
|
if node.id
|
1243
1330
|
class_attr = class_string node
|
@@ -1258,16 +1345,24 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1258
1345
|
end
|
1259
1346
|
end
|
1260
1347
|
|
1261
|
-
def output_content
|
1348
|
+
def output_content(node)
|
1262
1349
|
node.content_model == :simple ? %(<p>#{node.content}</p>) : node.content
|
1263
1350
|
end
|
1264
1351
|
|
1352
|
+
def encode_attribute_value(val)
|
1353
|
+
val.gsub '"', '"'
|
1354
|
+
end
|
1355
|
+
|
1265
1356
|
# FIXME: merge into with xml_sanitize helper
|
1266
|
-
def xml_sanitize
|
1267
|
-
sanitized =
|
1357
|
+
def xml_sanitize(value, target = :attribute)
|
1358
|
+
sanitized = value.include?('<') ? value.gsub(XML_ELEMENT_RX, '').strip.tr_s(' ', ' ') : value
|
1268
1359
|
if target == :plain && (sanitized.include? ';')
|
1269
|
-
|
1270
|
-
|
1360
|
+
if sanitized.include? '&#'
|
1361
|
+
sanitized = sanitized.gsub(CHAR_ENTITY_RX) do
|
1362
|
+
[::Regexp.last_match(1).to_i].pack 'U*'
|
1363
|
+
end
|
1364
|
+
end
|
1365
|
+
sanitized = sanitized.gsub FROM_HTML_SPECIAL_CHARS_RX, FROM_HTML_SPECIAL_CHARS_MAP
|
1271
1366
|
elsif target == :attribute
|
1272
1367
|
sanitized = sanitized.gsub '"', '"' if sanitized.include? '"'
|
1273
1368
|
end
|
@@ -1275,8 +1370,9 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1275
1370
|
end
|
1276
1371
|
|
1277
1372
|
# TODO: make check for last content paragraph a feature of Asciidoctor
|
1278
|
-
def mark_last_paragraph
|
1373
|
+
def mark_last_paragraph(root)
|
1279
1374
|
return unless (last_block = root.blocks[-1])
|
1375
|
+
|
1280
1376
|
last_block = last_block.blocks[-1] while last_block.context == :section && last_block.blocks?
|
1281
1377
|
if last_block.context == :paragraph
|
1282
1378
|
last_block.attributes['role'] = last_block.role? ? %(#{last_block.role} last) : 'last'
|
@@ -1285,11 +1381,11 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1285
1381
|
end
|
1286
1382
|
|
1287
1383
|
# Prepend a space to the value if it's non-nil, otherwise return empty string.
|
1288
|
-
def prepend_space
|
1384
|
+
def prepend_space(value)
|
1289
1385
|
value ? %( #{value}) : ''
|
1290
1386
|
end
|
1291
1387
|
|
1292
|
-
def add_theme_assets
|
1388
|
+
def add_theme_assets(doc)
|
1293
1389
|
format = @format
|
1294
1390
|
workdir = if doc.attr? 'epub3-stylesdir'
|
1295
1391
|
stylesdir = doc.attr 'epub3-stylesdir'
|
@@ -1306,42 +1402,46 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1306
1402
|
end
|
1307
1403
|
|
1308
1404
|
# TODO: improve design/UX of custom theme functionality, including custom fonts
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
|
1318
|
-
@book.add_item 'styles/epub3-css3-only.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
|
1405
|
+
%w[epub3 epub3-css3-only].each do |f|
|
1406
|
+
css = load_css_file File.join(workdir, %(#{f}.scss))
|
1407
|
+
if format == :kf8
|
1408
|
+
# NOTE: add layer of indirection so Kindle Direct Publishing (KDP) doesn't strip font-related CSS rules
|
1409
|
+
@book.add_item %(styles/#{f}.css), content: %(@import url("#{f}-proxied.css");).to_ios
|
1410
|
+
@book.add_item %(styles/#{f}-proxied.css), content: css.to_ios
|
1411
|
+
else
|
1412
|
+
@book.add_item %(styles/#{f}.css), content: css.to_ios
|
1413
|
+
end
|
1319
1414
|
end
|
1320
1415
|
|
1321
1416
|
syntax_hl = doc.syntax_highlighter
|
1322
1417
|
if syntax_hl&.write_stylesheet? doc
|
1323
1418
|
Dir.mktmpdir do |dir|
|
1324
1419
|
syntax_hl.write_stylesheet doc, dir
|
1325
|
-
Pathname.glob(dir
|
1420
|
+
Pathname.glob("#{dir}/**/*").map do |filename|
|
1326
1421
|
# Workaround for https://github.com/skoji/gepub/pull/117
|
1422
|
+
next unless filename.file?
|
1423
|
+
|
1327
1424
|
filename.open do |f|
|
1328
1425
|
@book.add_item filename.basename.to_s, content: f
|
1329
|
-
end
|
1426
|
+
end
|
1330
1427
|
end
|
1331
1428
|
end
|
1332
1429
|
end
|
1333
1430
|
|
1334
|
-
font_files, font_css = select_fonts
|
1335
|
-
|
1431
|
+
font_files, font_css = select_fonts load_css_file(File.join(DATA_DIR, 'styles/epub3-fonts.scss')),
|
1432
|
+
(doc.attr 'scripts', 'latin')
|
1433
|
+
@book.add_item 'styles/epub3-fonts.css', content: font_css.to_ios
|
1336
1434
|
unless font_files.empty?
|
1337
|
-
# NOTE metadata property in oepbs package manifest doesn't work; must use proprietary iBooks file instead
|
1338
|
-
#(@book.metadata.add_metadata 'meta', 'true')['property'] = 'ibooks:specified-fonts' unless format == :kf8
|
1339
|
-
|
1435
|
+
# NOTE: metadata property in oepbs package manifest doesn't work; must use proprietary iBooks file instead
|
1436
|
+
# (@book.metadata.add_metadata 'meta', 'true')['property'] = 'ibooks:specified-fonts' unless format == :kf8
|
1437
|
+
unless format == :kf8
|
1438
|
+
@book.add_optional_file 'META-INF/com.apple.ibooks.display-options.xml', '<?xml version="1.0" encoding="UTF-8"?>
|
1340
1439
|
<display_options>
|
1341
1440
|
<platform name="*">
|
1342
1441
|
<option name="specified-fonts">true</option>
|
1343
1442
|
</platform>
|
1344
|
-
</display_options>'.to_ios
|
1443
|
+
</display_options>'.to_ios
|
1444
|
+
end
|
1345
1445
|
|
1346
1446
|
font_files.each do |font_file|
|
1347
1447
|
@book.add_item font_file, content: File.join(DATA_DIR, font_file)
|
@@ -1350,7 +1450,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1350
1450
|
nil
|
1351
1451
|
end
|
1352
1452
|
|
1353
|
-
|
1453
|
+
# @param doc [Asciidoctor::Document]
|
1454
|
+
# @param name [String]
|
1455
|
+
# @return [GEPUB::Item, nil]
|
1456
|
+
def add_cover_page(doc, name)
|
1354
1457
|
image_attr_name = %(#{name}-image)
|
1355
1458
|
|
1356
1459
|
return nil if (image_path = doc.attr image_attr_name).nil?
|
@@ -1359,10 +1462,14 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1359
1462
|
imagesdir = (imagesdir == '.' ? '' : %(#{imagesdir}/))
|
1360
1463
|
|
1361
1464
|
image_attrs = {}
|
1362
|
-
if (image_path.include? ':') && image_path =~
|
1465
|
+
if (image_path.include? ':') && image_path =~ IMAGE_MACRO_RX
|
1363
1466
|
logger.warn %(deprecated block macro syntax detected in :#{image_attr_name}: attribute) if image_path.start_with? 'image::'
|
1364
|
-
image_path = %(#{imagesdir}#{
|
1365
|
-
|
1467
|
+
image_path = %(#{imagesdir}#{::Regexp.last_match(1)})
|
1468
|
+
unless ::Regexp.last_match(2).empty?
|
1469
|
+
(::Asciidoctor::AttributeList.new ::Regexp.last_match(2)).parse_into image_attrs,
|
1470
|
+
%w[alt width
|
1471
|
+
height]
|
1472
|
+
end
|
1366
1473
|
end
|
1367
1474
|
|
1368
1475
|
image_href = %(#{imagesdir}jacket/#{name}#{::File.extname image_path})
|
@@ -1370,9 +1477,11 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1370
1477
|
workdir = doc.attr 'docdir'
|
1371
1478
|
workdir = '.' if workdir.nil_or_empty?
|
1372
1479
|
|
1480
|
+
image_path = File.join workdir, image_path unless File.absolute_path? image_path
|
1481
|
+
|
1373
1482
|
begin
|
1374
|
-
@book.add_item(image_href, content:
|
1375
|
-
rescue => e
|
1483
|
+
@book.add_item(image_href, content: image_path).cover_image
|
1484
|
+
rescue StandardError => e
|
1376
1485
|
logger.error %(#{::File.basename doc.attr('docfile')}: error adding cover image. Make sure that :#{image_attr_name}: attribute points to a valid image file. #{e})
|
1377
1486
|
return nil
|
1378
1487
|
end
|
@@ -1380,14 +1489,15 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
|
|
1380
1489
|
return nil if @format == :kf8
|
1381
1490
|
|
1382
1491
|
unless !image_attrs.empty? && (width = image_attrs['width']) && (height = image_attrs['height'])
|
1383
|
-
width
|
1492
|
+
width = 1050
|
1493
|
+
height = 1600
|
1384
1494
|
end
|
1385
1495
|
|
1386
|
-
# NOTE SVG wrapper maintains aspect ratio and confines image to view box
|
1387
|
-
content = %(
|
1496
|
+
# NOTE: SVG wrapper maintains aspect ratio and confines image to view box
|
1497
|
+
content = %(<?xml version='1.0' encoding='utf-8'?>
|
1498
|
+
<!DOCTYPE html>
|
1388
1499
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
|
1389
1500
|
<head>
|
1390
|
-
<meta charset="UTF-8"/>
|
1391
1501
|
<title>#{sanitize_doctitle_xml doc, :cdata}</title>
|
1392
1502
|
<style type="text/css">
|
1393
1503
|
@page {
|
@@ -1417,7 +1527,7 @@ body > svg {
|
|
1417
1527
|
@book.add_ordered_item %(#{name}.xhtml), content: content, id: name
|
1418
1528
|
end
|
1419
1529
|
|
1420
|
-
def get_frontmatter_files
|
1530
|
+
def get_frontmatter_files(doc, workdir)
|
1421
1531
|
if doc.attr? 'epub3-frontmatterdir'
|
1422
1532
|
fmdir = doc.attr 'epub3-frontmatterdir'
|
1423
1533
|
fmglob = 'front-matter.*\.html'
|
@@ -1426,7 +1536,7 @@ body > svg {
|
|
1426
1536
|
logger.warn %(#{File.basename doc.attr('docfile')}: directory specified by 'epub3-frontmattderdir' doesn't exist! Ignoring ...)
|
1427
1537
|
return []
|
1428
1538
|
end
|
1429
|
-
fms = Dir.entries(fm_path).delete_if {|x| !x.match fmglob }.sort.map {|y| File.join fm_path, y }
|
1539
|
+
fms = Dir.entries(fm_path).delete_if { |x| !x.match fmglob }.sort.map { |y| File.join fm_path, y }
|
1430
1540
|
if fms && !fms.empty?
|
1431
1541
|
fms
|
1432
1542
|
else
|
@@ -1440,7 +1550,7 @@ body > svg {
|
|
1440
1550
|
end
|
1441
1551
|
end
|
1442
1552
|
|
1443
|
-
def add_front_matter_page
|
1553
|
+
def add_front_matter_page(doc)
|
1444
1554
|
workdir = doc.attr 'docdir'
|
1445
1555
|
workdir = '.' if workdir.nil_or_empty?
|
1446
1556
|
|
@@ -1450,30 +1560,32 @@ body > svg {
|
|
1450
1560
|
|
1451
1561
|
front_matter_file = File.basename front_matter, '.html'
|
1452
1562
|
item = @book.add_ordered_item "#{front_matter_file}.xhtml", content: (postprocess_xhtml front_matter_content)
|
1453
|
-
item.add_property 'svg' if
|
1563
|
+
item.add_property 'svg' if SVG_IMG_SNIFF_RX =~ front_matter_content
|
1454
1564
|
# Store link to first frontmatter page
|
1455
1565
|
result = item if result.nil?
|
1456
1566
|
|
1457
|
-
front_matter_content.scan
|
1458
|
-
@book.add_item
|
1567
|
+
front_matter_content.scan IMAGE_SRC_SCAN_RX do
|
1568
|
+
@book.add_item ::Regexp.last_match(1),
|
1569
|
+
content: File.join(File.dirname(front_matter), ::Regexp.last_match(1))
|
1459
1570
|
end
|
1460
1571
|
end
|
1461
1572
|
|
1462
1573
|
result
|
1463
1574
|
end
|
1464
1575
|
|
1465
|
-
def add_profile_images
|
1576
|
+
def add_profile_images(doc, usernames)
|
1466
1577
|
imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
|
1467
1578
|
imagesdir = (imagesdir == '.' ? nil : %(#{imagesdir}/))
|
1468
1579
|
|
1469
1580
|
@book.add_item %(#{imagesdir}avatars/default.jpg), content: ::File.join(DATA_DIR, 'images/default-avatar.jpg')
|
1470
|
-
@book.add_item %(#{imagesdir}headshots/default.jpg),
|
1581
|
+
@book.add_item %(#{imagesdir}headshots/default.jpg),
|
1582
|
+
content: ::File.join(DATA_DIR, 'images/default-headshot.jpg')
|
1471
1583
|
|
1472
|
-
workdir = (workdir = doc.attr 'docdir').nil_or_empty?
|
1584
|
+
workdir = '.' if (workdir = doc.attr 'docdir').nil_or_empty?
|
1473
1585
|
|
1474
1586
|
usernames.each do |username|
|
1475
1587
|
avatar = %(#{imagesdir}avatars/#{username}.jpg)
|
1476
|
-
if ::File.readable?
|
1588
|
+
if ::File.readable?(resolved_avatar = (::File.join workdir, avatar))
|
1477
1589
|
@book.add_item avatar, content: resolved_avatar
|
1478
1590
|
else
|
1479
1591
|
logger.error %(avatar for #{username} not found or readable: #{avatar}; falling back to default avatar)
|
@@ -1481,7 +1593,7 @@ body > svg {
|
|
1481
1593
|
end
|
1482
1594
|
|
1483
1595
|
headshot = %(#{imagesdir}headshots/#{username}.jpg)
|
1484
|
-
if ::File.readable?
|
1596
|
+
if ::File.readable?(resolved_headshot = (::File.join workdir, headshot))
|
1485
1597
|
@book.add_item headshot, content: resolved_headshot
|
1486
1598
|
elsif doc.attr? 'builder', 'editions'
|
1487
1599
|
logger.error %(headshot for #{username} not found or readable: #{headshot}; falling back to default headshot)
|
@@ -1491,11 +1603,12 @@ body > svg {
|
|
1491
1603
|
nil
|
1492
1604
|
end
|
1493
1605
|
|
1494
|
-
def nav_doc
|
1495
|
-
lines = [%(
|
1496
|
-
|
1606
|
+
def nav_doc(doc, items, landmarks, depth)
|
1607
|
+
lines = [%(<?xml version='1.0' encoding='utf-8'?>
|
1608
|
+
<!DOCTYPE html>
|
1609
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = doc.attr 'lang',
|
1610
|
+
'en'}" lang="#{lang}">
|
1497
1611
|
<head>
|
1498
|
-
<meta charset="UTF-8"/>
|
1499
1612
|
<title>#{sanitize_doctitle_xml doc, :cdata}</title>
|
1500
1613
|
<link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
|
1501
1614
|
<link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
|
@@ -1527,133 +1640,121 @@ body > svg {
|
|
1527
1640
|
lines * LF
|
1528
1641
|
end
|
1529
1642
|
|
1530
|
-
def nav_level
|
1643
|
+
def nav_level(items, depth, state = {})
|
1531
1644
|
lines = []
|
1532
1645
|
lines << '<ol>'
|
1533
1646
|
items.each do |item|
|
1534
|
-
#index = (state[:index] = (state.fetch :index, 0) + 1)
|
1535
|
-
if (
|
1647
|
+
# index = (state[:index] = (state.fetch :index, 0) + 1)
|
1648
|
+
if (chapter_filename = get_chapter_filename item).nil?
|
1536
1649
|
item_label = sanitize_xml get_numbered_title(item), :pcdata
|
1537
1650
|
item_href = %(#{state[:content_doc_href]}##{item.id})
|
1538
1651
|
else
|
1539
|
-
# NOTE we sanitize the chapter titles because we use formatting to control layout
|
1540
|
-
if item.context == :document
|
1541
|
-
|
1542
|
-
|
1543
|
-
|
1544
|
-
|
1545
|
-
item_href = (state[:content_doc_href] = %(#{
|
1652
|
+
# NOTE: we sanitize the chapter titles because we use formatting to control layout
|
1653
|
+
item_label = if item.context == :document
|
1654
|
+
sanitize_doctitle_xml item, :cdata
|
1655
|
+
else
|
1656
|
+
sanitize_xml get_numbered_title(item), :cdata
|
1657
|
+
end
|
1658
|
+
item_href = (state[:content_doc_href] = %(#{chapter_filename}.xhtml))
|
1546
1659
|
end
|
1547
1660
|
lines << %(<li><a href="#{item_href}">#{item_label}</a>)
|
1548
|
-
if depth
|
1661
|
+
if depth.zero? || (child_sections = item.sections).empty?
|
1549
1662
|
lines[-1] = %(#{lines[-1]}</li>)
|
1550
1663
|
else
|
1551
1664
|
lines << (nav_level child_sections, depth - 1, state)
|
1552
1665
|
lines << '</li>'
|
1553
1666
|
end
|
1554
|
-
state.delete :content_doc_href unless
|
1667
|
+
state.delete :content_doc_href unless chapter_filename.nil?
|
1555
1668
|
end
|
1556
1669
|
lines << '</ol>'
|
1557
1670
|
lines * LF
|
1558
1671
|
end
|
1559
1672
|
|
1560
|
-
def ncx_doc
|
1673
|
+
def ncx_doc(doc, items, depth)
|
1561
1674
|
# TODO: populate docAuthor element based on unique authors in work
|
1562
1675
|
lines = [%(<?xml version="1.0" encoding="utf-8"?>
|
1563
1676
|
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="#{doc.attr 'lang', 'en'}">
|
1564
1677
|
<head>
|
1565
1678
|
<meta name="dtb:uid" content="#{@book.identifier}"/>
|
1566
|
-
|
1679
|
+
%<depth>s
|
1567
1680
|
<meta name="dtb:totalPageCount" content="0"/>
|
1568
1681
|
<meta name="dtb:maxPageNumber" content="0"/>
|
1569
1682
|
</head>
|
1570
1683
|
<docTitle><text>#{sanitize_doctitle_xml doc, :cdata}</text></docTitle>
|
1571
1684
|
<navMap>)]
|
1572
1685
|
lines << (ncx_level items, depth, state = {})
|
1573
|
-
lines[0] = lines[0].sub '
|
1686
|
+
lines[0] = lines[0].sub '%<depth>s', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>)
|
1574
1687
|
lines << %(</navMap>
|
1575
1688
|
</ncx>)
|
1576
1689
|
lines * LF
|
1577
1690
|
end
|
1578
1691
|
|
1579
|
-
def ncx_level
|
1692
|
+
def ncx_level(items, depth, state = {})
|
1580
1693
|
lines = []
|
1581
1694
|
state[:max_depth] = (state.fetch :max_depth, 0) + 1
|
1582
1695
|
items.each do |item|
|
1583
1696
|
index = (state[:index] = (state.fetch :index, 0) + 1)
|
1584
1697
|
item_id = %(nav_#{index})
|
1585
|
-
if (
|
1698
|
+
if (chapter_filename = get_chapter_filename item).nil?
|
1586
1699
|
item_label = sanitize_xml get_numbered_title(item), :cdata
|
1587
1700
|
item_href = %(#{state[:content_doc_href]}##{item.id})
|
1588
1701
|
else
|
1589
|
-
if item.context == :document
|
1590
|
-
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
item_href = (state[:content_doc_href] = %(#{
|
1702
|
+
item_label = if item.context == :document
|
1703
|
+
sanitize_doctitle_xml item, :cdata
|
1704
|
+
else
|
1705
|
+
sanitize_xml get_numbered_title(item), :cdata
|
1706
|
+
end
|
1707
|
+
item_href = (state[:content_doc_href] = %(#{chapter_filename}.xhtml))
|
1595
1708
|
end
|
1596
1709
|
lines << %(<navPoint id="#{item_id}" playOrder="#{index}">)
|
1597
1710
|
lines << %(<navLabel><text>#{item_label}</text></navLabel>)
|
1598
1711
|
lines << %(<content src="#{item_href}"/>)
|
1599
|
-
unless depth
|
1712
|
+
unless depth.zero? || (child_sections = item.sections).empty?
|
1600
1713
|
lines << (ncx_level child_sections, depth - 1, state)
|
1601
1714
|
end
|
1602
1715
|
lines << %(</navPoint>)
|
1603
|
-
state.delete :content_doc_href unless
|
1716
|
+
state.delete :content_doc_href unless chapter_filename.nil?
|
1604
1717
|
end
|
1605
1718
|
lines * LF
|
1606
1719
|
end
|
1607
1720
|
|
1608
1721
|
# Swap fonts in CSS based on the value of the document attribute 'scripts',
|
1609
1722
|
# then return the list of fonts as well as the font CSS.
|
1610
|
-
def select_fonts
|
1611
|
-
font_css = ::File.read filename
|
1723
|
+
def select_fonts(font_css, scripts = 'latin')
|
1612
1724
|
font_css = font_css.gsub(/(?<=-)latin(?=\.ttf\))/, scripts) unless scripts == 'latin'
|
1613
1725
|
|
1614
1726
|
# match CSS font urls in the forms of:
|
1615
1727
|
# src: url(../fonts/notoserif-regular-latin.ttf);
|
1616
1728
|
# src: url(../fonts/notoserif-regular-latin.ttf) format("truetype");
|
1617
|
-
font_list = font_css.scan(
|
1618
|
-
|
1619
|
-
[font_list, font_css.to_ios]
|
1620
|
-
end
|
1729
|
+
font_list = font_css.scan(%r{url\(\.\./([^)]+?\.ttf)\)}).flatten
|
1621
1730
|
|
1622
|
-
|
1623
|
-
return filename unless format == :kf8
|
1624
|
-
postprocess_css ::File.read(filename), format
|
1731
|
+
[font_list, font_css]
|
1625
1732
|
end
|
1626
1733
|
|
1627
|
-
def
|
1628
|
-
|
1629
|
-
|
1630
|
-
|
1631
|
-
|
1632
|
-
.gsub(/^ max-width: .*\n/, '')
|
1633
|
-
.to_ios
|
1734
|
+
def load_css_file(filename)
|
1735
|
+
template = File.read filename
|
1736
|
+
load_paths = [File.dirname(filename)]
|
1737
|
+
sass_engine = Sass::Engine.new template, syntax: :scss, cache: false, load_paths: load_paths, style: :compressed
|
1738
|
+
sass_engine.render
|
1634
1739
|
end
|
1635
1740
|
|
1636
|
-
|
1637
|
-
# <meta charset="utf-8"/>
|
1638
|
-
# be converted to
|
1639
|
-
# <meta http-equiv="Content-Type" content="application/xml+xhtml; charset=UTF-8"/>
|
1640
|
-
def postprocess_xhtml content
|
1741
|
+
def postprocess_xhtml(content)
|
1641
1742
|
return content.to_ios unless @format == :kf8
|
1743
|
+
|
1642
1744
|
# TODO: convert regular expressions to constants
|
1643
1745
|
content
|
1644
|
-
.gsub(/<meta charset="(.+?)"\/>/, '<meta http-equiv="Content-Type" content="application/xml+xhtml; charset=\1"/>')
|
1645
1746
|
.gsub(/<img([^>]+) style="width: (\d\d)%;"/, '<img\1 style="width: \2%; height: \2%;"')
|
1646
|
-
.gsub(
|
1747
|
+
.gsub(%r{<script type="text/javascript">.*?</script>\n?}m, '')
|
1647
1748
|
.to_ios
|
1648
1749
|
end
|
1649
1750
|
|
1650
|
-
def
|
1751
|
+
def build_kindlegen_command
|
1651
1752
|
unless @kindlegen_path.nil?
|
1652
1753
|
logger.debug %(Using ebook-kindlegen-path attribute: #{@kindlegen_path})
|
1653
1754
|
return [@kindlegen_path]
|
1654
1755
|
end
|
1655
1756
|
|
1656
|
-
unless (result = ENV
|
1757
|
+
unless (result = ENV.fetch('KINDLEGEN', nil)).nil?
|
1657
1758
|
logger.debug %(Using KINDLEGEN env variable: #{result})
|
1658
1759
|
return [result]
|
1659
1760
|
end
|
@@ -1669,18 +1770,23 @@ body > svg {
|
|
1669
1770
|
end
|
1670
1771
|
end
|
1671
1772
|
|
1672
|
-
def distill_epub_to_mobi
|
1673
|
-
mobi_file = ::File.basename target.sub(
|
1674
|
-
compress_flag =
|
1773
|
+
def distill_epub_to_mobi(epub_file, target, compress)
|
1774
|
+
mobi_file = ::File.basename target.sub(EPUB_EXTENSION_RX, '.mobi')
|
1775
|
+
compress_flag = KINDLEGEN_COMPRESSION[if compress
|
1776
|
+
compress.empty? ? '1' : compress.to_s
|
1777
|
+
else
|
1778
|
+
'0'
|
1779
|
+
end]
|
1675
1780
|
|
1676
|
-
argv =
|
1781
|
+
argv = build_kindlegen_command + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
|
1677
1782
|
begin
|
1678
1783
|
# This duplicates Kindlegen.run, but we want to override executable
|
1679
1784
|
out, err, res = Open3.capture3(*argv) do |r|
|
1680
1785
|
r.force_encoding 'UTF-8' if ::Gem.win_platform? && r.respond_to?(:force_encoding)
|
1681
1786
|
end
|
1682
1787
|
rescue Errno::ENOENT => e
|
1683
|
-
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',
|
1788
|
+
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',
|
1789
|
+
cause: e
|
1684
1790
|
end
|
1685
1791
|
|
1686
1792
|
out.each_line do |line|
|
@@ -1698,13 +1804,13 @@ body > svg {
|
|
1698
1804
|
end
|
1699
1805
|
end
|
1700
1806
|
|
1701
|
-
def
|
1807
|
+
def build_epubcheck_command
|
1702
1808
|
unless @epubcheck_path.nil?
|
1703
1809
|
logger.debug %(Using ebook-epubcheck-path attribute: #{@epubcheck_path})
|
1704
1810
|
return [@epubcheck_path]
|
1705
1811
|
end
|
1706
1812
|
|
1707
|
-
unless (result = ENV
|
1813
|
+
unless (result = ENV.fetch('EPUBCHECK', nil)).nil?
|
1708
1814
|
logger.debug %(Using EPUBCHECK env variable: #{result})
|
1709
1815
|
return [result]
|
1710
1816
|
end
|
@@ -1719,12 +1825,13 @@ body > svg {
|
|
1719
1825
|
end
|
1720
1826
|
end
|
1721
1827
|
|
1722
|
-
def validate_epub
|
1723
|
-
argv =
|
1828
|
+
def validate_epub(epub_file)
|
1829
|
+
argv = build_epubcheck_command + ['-w', epub_file]
|
1724
1830
|
begin
|
1725
1831
|
out, err, res = Open3.capture3(*argv)
|
1726
1832
|
rescue Errno::ENOENT => e
|
1727
|
-
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',
|
1833
|
+
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',
|
1834
|
+
cause: e
|
1728
1835
|
end
|
1729
1836
|
|
1730
1837
|
out.each_line do |line|
|
@@ -1737,14 +1844,15 @@ body > svg {
|
|
1737
1844
|
logger.error %(EPUB validation failed: #{epub_file}) unless res.success?
|
1738
1845
|
end
|
1739
1846
|
|
1740
|
-
def log_line
|
1847
|
+
def log_line(line)
|
1741
1848
|
line = line.strip
|
1742
1849
|
|
1743
|
-
|
1850
|
+
case line
|
1851
|
+
when /^fatal/i
|
1744
1852
|
logger.fatal line
|
1745
|
-
|
1853
|
+
when /^error/i
|
1746
1854
|
logger.error line
|
1747
|
-
|
1855
|
+
when /^warning/i
|
1748
1856
|
logger.warn line
|
1749
1857
|
else
|
1750
1858
|
logger.info line
|
@@ -1753,7 +1861,7 @@ body > svg {
|
|
1753
1861
|
|
1754
1862
|
private
|
1755
1863
|
|
1756
|
-
def class_string
|
1864
|
+
def class_string(node)
|
1757
1865
|
role = node.role
|
1758
1866
|
|
1759
1867
|
return '' unless role_valid_class? role
|
@@ -1762,66 +1870,28 @@ body > svg {
|
|
1762
1870
|
end
|
1763
1871
|
|
1764
1872
|
# Handles asciidoctor 1.5.6 quirk when role can be parent
|
1765
|
-
def role_valid_class?
|
1873
|
+
def role_valid_class?(role)
|
1766
1874
|
role.is_a? String
|
1767
1875
|
end
|
1768
1876
|
end
|
1769
1877
|
|
1770
|
-
class
|
1771
|
-
|
1772
|
-
|
1773
|
-
|
1774
|
-
|
1775
|
-
|
1776
|
-
|
1777
|
-
|
1778
|
-
|
1779
|
-
|
1780
|
-
|
1781
|
-
|
1782
|
-
|
1783
|
-
|
1784
|
-
|
1785
|
-
|
1786
|
-
|
1787
|
-
sep = sep ? sep.chr : '_'
|
1788
|
-
if doc.header?
|
1789
|
-
id = doc.doctitle sanitize: true
|
1790
|
-
id = id.gsub CharRefRx do
|
1791
|
-
$1 ? ($1 == 'amp' ? 'and' : sep) : ((d = $2 ? $2.to_i : $3.hex) == 8217 ? '' : ([d].pack 'U*'))
|
1792
|
-
end if id.include? '&'
|
1793
|
-
id = id.downcase.gsub InvalidIdCharsRx, sep
|
1794
|
-
if id.empty?
|
1795
|
-
id, synthetic = nil, true
|
1796
|
-
else
|
1797
|
-
unless sep.empty?
|
1798
|
-
if (id = id.tr_s sep, sep).end_with? sep
|
1799
|
-
if id == sep
|
1800
|
-
id, synthetic = nil, true
|
1801
|
-
else
|
1802
|
-
id = (id.start_with? sep) ? id[1..-2] : id.chop
|
1803
|
-
end
|
1804
|
-
elsif id.start_with? sep
|
1805
|
-
id = id[1..-1]
|
1806
|
-
end
|
1807
|
-
end
|
1808
|
-
unless synthetic
|
1809
|
-
if pre.empty?
|
1810
|
-
id = %(_#{id}) if LeadingDigitRx =~ id
|
1811
|
-
elsif !(id.start_with? pre)
|
1812
|
-
id = %(#{pre}#{id})
|
1813
|
-
end
|
1814
|
-
end
|
1815
|
-
end
|
1816
|
-
elsif (first_section = doc.first_section)
|
1817
|
-
id = first_section.id
|
1818
|
-
else
|
1819
|
-
synthetic = true
|
1820
|
-
end
|
1821
|
-
id = %(#{pre}document#{sep}#{doc.object_id}) if synthetic
|
1822
|
-
end
|
1823
|
-
logger.error %(chapter uses a reserved ID: #{id}) if !synthetic && (ReservedIds.include? id)
|
1824
|
-
id
|
1878
|
+
class NumericIdGenerator
|
1879
|
+
def initialize
|
1880
|
+
@counter = 1
|
1881
|
+
end
|
1882
|
+
|
1883
|
+
# @param node [Asciidoctor::AbstractNode]
|
1884
|
+
# @return [void]
|
1885
|
+
def generate_id(node)
|
1886
|
+
if node.chapter? || node.is_a?(Asciidoctor::Document)
|
1887
|
+
node.id = %(_generated_id_#{@counter})
|
1888
|
+
@counter += 1
|
1889
|
+
end
|
1890
|
+
|
1891
|
+
# Recurse
|
1892
|
+
node.blocks.each do |subnode|
|
1893
|
+
# dlist contains array of *arrays* of blocks, so just skip them
|
1894
|
+
generate_id subnode if subnode.is_a?(Asciidoctor::AbstractBlock)
|
1825
1895
|
end
|
1826
1896
|
end
|
1827
1897
|
end
|
@@ -1834,27 +1904,33 @@ body > svg {
|
|
1834
1904
|
document.set_attribute 'pygments-style', 'bw' unless document.attr? 'pygments-style'
|
1835
1905
|
document.set_attribute 'rouge-style', 'bw' unless document.attr? 'rouge-style'
|
1836
1906
|
|
1837
|
-
# Old asciidoctor versions do not have public API for writing highlighter CSS file
|
1838
|
-
# So just use inline CSS there.
|
1839
|
-
unless Document.supports_syntax_highlighter?
|
1840
|
-
document.set_attribute 'coderay-css', 'style'
|
1841
|
-
document.set_attribute 'pygments-css', 'style'
|
1842
|
-
document.set_attribute 'rouge-css', 'style'
|
1843
|
-
end
|
1844
|
-
|
1845
1907
|
case (ebook_format = document.attributes['ebook-format'])
|
1846
1908
|
when 'epub3', 'kf8'
|
1847
1909
|
# all good
|
1848
1910
|
when 'mobi'
|
1849
1911
|
ebook_format = document.attributes['ebook-format'] = 'kf8'
|
1850
1912
|
else
|
1851
|
-
# QUESTION should we display a warning?
|
1913
|
+
# QUESTION: should we display a warning?
|
1852
1914
|
ebook_format = document.attributes['ebook-format'] = 'epub3'
|
1853
1915
|
end
|
1854
1916
|
document.attributes[%(ebook-format-#{ebook_format})] = ''
|
1917
|
+
|
1918
|
+
# Enable generation of section ids because we use them for chapter filenames
|
1919
|
+
document.set_attribute 'sectids'
|
1855
1920
|
treeprocessor do
|
1856
1921
|
process do |doc|
|
1857
|
-
|
1922
|
+
if ebook_format == 'kf8'
|
1923
|
+
# Kindlegen doesn't support unicode ids
|
1924
|
+
NumericIdGenerator.new.generate_id doc
|
1925
|
+
else
|
1926
|
+
# :sectids: doesn't generate id for top-level section (why?), do it manually
|
1927
|
+
doc.id = Section.generate_id(doc.first_section&.title || doc.attr('docname') || 'document', doc) if doc.id.nil_or_empty?
|
1928
|
+
|
1929
|
+
if (preamble = doc.blocks[0]) && preamble.context == :preamble && preamble.id.nil_or_empty?
|
1930
|
+
# :sectids: doesn't generate id for preamble (because it is not a section), do it manually
|
1931
|
+
preamble.id = Section.generate_id(preamble.title || 'preamble', doc)
|
1932
|
+
end
|
1933
|
+
end
|
1858
1934
|
nil
|
1859
1935
|
end
|
1860
1936
|
end
|