asciidoctor-epub3 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 output, target
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 EpubExtensionRx, ''
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
- CsvDelimiterRx = /\s*,\s*/
50
+ CSV_DELIMITED_RX = /\s*,\s*/.freeze
49
51
 
50
52
  DATA_DIR = ::File.expand_path ::File.join(__dir__, '..', '..', 'data')
51
- ImageMacroRx = /^image::?(.*?)\[(.*?)\]$/
52
- ImgSrcScanRx = /<img src="(.+?)"/
53
- SvgImgSniffRx = /<img src=".+?\.svg"/
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 = ?\n
56
- NoBreakSpace = '&#xa0;'
57
- RightAngleQuote = '&#x203a;'
58
- CalloutStartNum = %(\u2460)
57
+ LF = "\n"
58
+ NO_BREAK_SPACE = '&#xa0;'
59
+ RIGHT_ANGLE_QUOTE = '&#x203a;'
60
+ CALLOUT_START_NUM = %(\u2460)
59
61
 
60
- CharEntityRx = /&#(\d{2,6});/
61
- XmlElementRx = /<\/?.+?>/
62
- TrailingPunctRx = /[[:punct:]]$/
62
+ CHAR_ENTITY_RX = /&#(\d{2,6});/.freeze
63
+ XML_ELEMENT_RX = %r{</?.+?>}.freeze
64
+ TRAILING_PUNCT_RX = /[[:punct:]]$/.freeze
63
65
 
64
- FromHtmlSpecialCharsMap = {
66
+ FROM_HTML_SPECIAL_CHARS_MAP = {
65
67
  '&lt;' => '<',
66
68
  '&gt;' => '>',
67
- '&amp;' => '&',
68
- }
69
+ '&amp;' => '&'
70
+ }.freeze
69
71
 
70
- FromHtmlSpecialCharsRx = /(?:#{FromHtmlSpecialCharsMap.keys * '|'})/
72
+ FROM_HTML_SPECIAL_CHARS_RX = /(?:#{FROM_HTML_SPECIAL_CHARS_MAP.keys * '|'})/.freeze
71
73
 
72
- ToHtmlSpecialCharsMap = {
74
+ TO_HTML_SPECIAL_CHARS_MAP = {
73
75
  '&' => '&amp;',
74
76
  '<' => '&lt;',
75
- '>' => '&gt;',
76
- }
77
-
78
- ToHtmlSpecialCharsRx = /[#{ToHtmlSpecialCharsMap.keys.join}]/
79
-
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 = {}
77
+ '>' => '&gt;'
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 node, name = nil, _opts = {}
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
- # 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
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 node
132
+ def get_numbered_title(node)
124
133
  doc_attrs = node.document.attributes
125
134
  level = node.level
126
135
  if node.caption
127
- title = node.captioned_title
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
- 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})
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
- title = %(#{node.sectnum} #{node.title})
146
+ %(#{node.sectnum} #{node.title})
136
147
  end
137
148
  else
138
- title = %(#{node.sectnum} #{node.title})
149
+ %(#{node.sectnum} #{node.title})
139
150
  end
140
151
  else
141
- title = node.title
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 node
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(CsvDelimiterRx).each do |s|
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', group_position: series_volume
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
- landmarks << { type: 'frontmatter', href: front_matter_page.href, title: 'Front Matter' } unless front_matter_page.nil?
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
- landmarks << { type: 'bodymatter', href: %(#{get_chapter_name toc_items[0]}.xhtml), title: 'Start of Content' } unless toc_items.empty?
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
- landmarks << { type: item.style, href: %(#{get_chapter_name item}.xhtml), title: item.title } if %w(appendix bibliography glossary index preface).include? item.style
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 doc, content_spec
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 content, content_spec
308
- if content_spec != :pcdata && (content.include? '<')
309
- if (content = (content.gsub XmlElementRx, '').strip).include? ' '
310
- content = content.tr_s ' ', ' '
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(CharEntityRx) { [$1.to_i].pack 'U*' } if content.include? '&#'
322
- content = content.gsub FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap
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
- def add_chapter node
331
- docid = get_chapter_name node
332
- return nil if docid.nil?
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 %(#{docid}.xhtml)
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 = (title || subtitle) ? %(<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 = [%(<!DOCTYPE html>
393
- <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', 'en'}" lang="#{lang}">
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
- lines << (syntax_hl.docinfo :head, node, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :head
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="#{docid}">
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><sup class="noteref"><a href="#noteref-#{footnote.index}">#{footnote.index}</a></sup> #{footnote.text}</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
- lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :footer
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
- def convert_section node
455
- if add_chapter(node).nil?
456
- hlevel = node.level
457
- epub_type_attr = node.sectname != 'section' ? %( epub:type="#{node.sectname}") : ''
458
- div_classes = [%(sect#{node.level}), node.role].compact
459
- title = get_numbered_title node
460
- %(<section class="#{div_classes * ' '}" title=#{title.encode xml: :attr}#{epub_type_attr}>
461
- <h#{hlevel} id="#{node.id}">#{title}</h#{hlevel}>#{(content = node.content).empty? ? '' : %(
462
- #{content})}
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 node
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 node
474
- if add_chapter(node).nil?
475
- if (first_block = node.blocks[0]) && first_block.style == 'abstract'
476
- convert_abstract first_block
477
- # REVIEW: should we treat the preamble as an abstract in general?
478
- elsif first_block && node.blocks.size == 1
479
- convert_abstract first_block
480
- else
481
- node.content
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 node
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 node
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 node
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 = node.title? ? %(<strong class="head">#{title = node.title}#{head_stop && title !~ TrailingPunctRx ? head_stop : ''}</strong> ) : ''
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 node
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 node
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 node
603
+ def convert_example(node)
558
604
  id_attr = node.id ? %( id="#{node.id}") : ''
559
- title_div = node.title? ? %(<div class="example-title">#{node.title}</div>
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 node
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
- def convert_listing node
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
- css_mode: ((doc_attrs = node.document.attributes)[%(#{syntax_hl.name}-css)] || :class).to_sym,
583
- style: doc_attrs[%(#{syntax_hl.name}-style)],
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 node
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 node
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 _node
690
+ def convert_page_break(_node)
636
691
  '<hr epub:type="pagebreak" class="pagebreak"/>'
637
692
  end
638
693
 
639
- def convert_thematic_break _node
694
+ def convert_thematic_break(_node)
640
695
  '<hr class="thematicbreak"/>'
641
696
  end
642
697
 
643
- def convert_quote node
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 node
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 = !footer_content.empty? ? %(
684
- <span class="attribution">~ #{footer_content * ', '}</span>) : ''
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 node
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 node
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 = !table_styles.empty? ? %( style="#{table_styles * '; '}") : ''
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') > 0
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 << ((col.option? 'autowidth') ? %(<col/>) : %(<col style="width: #{col.attr 'colpcwidth'}%;" />))
805
+ lines << (col.option?('autowidth') ? %(<col/>) : %(<col style="width: #{col.attr 'colpcwidth'}%;" />))
740
806
  end
741
807
  end
742
808
  lines << '</colgroup>'
743
- [:head, :body, :foot].reject {|tsec| node.rows[tsec].empty? }.each do |tsec|
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 = !cell_classes.empty? ? %( class="#{cell_classes * ' '}") : ''
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 = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'}") : ''
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 node
854
+ def convert_colist(node)
789
855
  lines = ['<div class="callout-list">
790
856
  <ol>']
791
- num = CalloutStartNum
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 node
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 = (node.option? 'brief') ? ' class="brief"' : ''
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 = [*subjects].first.text
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 !~ TrailingPunctRx ? subject_stop : ''}</strong>)
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 = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : ''
913
+ col_style_attribute = node.attr?('labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : ''
848
914
  lines << %(<col#{col_style_attribute} />)
849
- col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : ''
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#{(node.option? 'strong') ? ' strong' : ''}">)
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
- [*terms].each do |dt|
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 node
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, ((node.option? 'brief') ? 'brief' : nil)].compact
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 = (node.attr? 'start') ? %( start="#{node.attr 'start'}") : ''
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}#{(node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
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 && ::Asciidoctor::List === item.blocks[0]
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 node
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, ((node.option? 'brief') ? 'brief' : nil)].compact
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 && ::Asciidoctor::List === item.blocks[0]
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 document, key
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 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 node, target, media_type
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
- def resolve_image_attrs node
1062
+ # @param node [Asciidoctor::Block]
1063
+ # @return [Array<String>]
1064
+ def resolve_image_attrs(node)
995
1065
  img_attrs = []
996
- img_attrs << %(alt="#{node.attr 'alt'}") if node.attr? 'alt'
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
- img_attrs << %(style="width: #{width}")
1009
- else
1010
- img_attrs << %(width="#{width}")
1011
- end
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 node
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 = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
1024
- controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
1025
- loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
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
- time_anchor = %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
1031
- else
1032
- time_anchor = ''
1033
- end
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 node
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 = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : ''
1052
- height_attr = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : ''
1053
- autoplay_attr = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
1054
- controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
1055
- loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
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
- time_anchor = %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
1061
- else
1062
- time_anchor = ''
1063
- end
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
- def convert_image node
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 node
1170
+ def get_enclosing_chapter(node)
1096
1171
  loop do
1097
1172
  return nil if node.nil?
1098
- return node unless get_chapter_name(node).nil?
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 node
1179
+ def convert_inline_anchor(node)
1104
1180
  case node.type
1105
1181
  when :xref
1106
- doc, refid, target, text = node.document, node.attr('refid'), node.target, node.text
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 = get_chapter_name ref_chapter
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 node
1241
+ def convert_inline_break(node)
1163
1242
  %(#{node.text}<br/>)
1164
1243
  end
1165
1244
 
1166
- def convert_inline_button node
1167
- %(<b class="button">[<span class="label">#{node.text}</span>]</b>)
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 node
1171
- num = CalloutStartNum
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
- def convert_inline_footnote node
1258
+ # @param node [Asciidoctor::Inline]
1259
+ # @return [String]
1260
+ def convert_inline_footnote(node)
1178
1261
  if (index = node.attr 'index')
1179
- %(<sup class="noteref">[<a id="noteref-#{index}" href="#note-#{index}" epub:type="noteref">#{index}</a>]</sup>)
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 node
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 node
1291
+ def convert_inline_indexterm(node)
1205
1292
  node.type == :visible ? node.text : ''
1206
1293
  end
1207
1294
 
1208
- def convert_inline_kbd node
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 node
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 = %(#{NoBreakSpace}<span class="caret">#{RightAngleQuote}</span> )
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 node
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
- content = AsciiMath.parse(node.text).to_mathml 'mml:'
1236
- else
1237
- content = node.text
1238
- end
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 [:monospaced, :asciimath, :latexmath].include? node.type
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 node
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 '"', '&quot;'
1354
+ end
1355
+
1265
1356
  # FIXME: merge into with xml_sanitize helper
1266
- def xml_sanitize value, target = :attribute
1267
- sanitized = (value.include? '<') ? value.gsub(XmlElementRx, '').strip.tr_s(' ', ' ') : value
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
- sanitized = sanitized.gsub(CharEntityRx) { [$1.to_i].pack 'U*' } if sanitized.include? '&#'
1270
- sanitized = sanitized.gsub FromHtmlSpecialCharsRx, FromHtmlSpecialCharsMap
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 '"', '&quot;' 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 root
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 value
1384
+ def prepend_space(value)
1289
1385
  value ? %( #{value}) : ''
1290
1386
  end
1291
1387
 
1292
- def add_theme_assets doc
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
- if format == :kf8
1311
- # NOTE add layer of indirection so Kindle Direct Publishing (KDP) doesn't strip font-related CSS rules
1312
- @book.add_item 'styles/epub3.css', content: '@import url("epub3-proxied.css");'.to_ios
1313
- @book.add_item 'styles/epub3-css3-only.css', content: '@import url("epub3-css3-only-proxied.css");'.to_ios
1314
- @book.add_item 'styles/epub3-proxied.css', content: (postprocess_css_file ::File.join(workdir, 'epub3.css'), format)
1315
- @book.add_item 'styles/epub3-css3-only-proxied.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
1316
- else
1317
- @book.add_item 'styles/epub3.css', content: (postprocess_css_file ::File.join(workdir, 'epub3.css'), format)
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 + '/**/*').map do |filename|
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 if filename.file?
1426
+ end
1330
1427
  end
1331
1428
  end
1332
1429
  end
1333
1430
 
1334
- font_files, font_css = select_fonts ::File.join(DATA_DIR, 'styles/epub3-fonts.css'), (doc.attr 'scripts', 'latin')
1335
- @book.add_item 'styles/epub3-fonts.css', content: font_css
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
- @book.add_optional_file 'META-INF/com.apple.ibooks.display-options.xml', '<?xml version="1.0" encoding="UTF-8"?>
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 unless format == :kf8
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
- def add_cover_page doc, name
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 =~ ImageMacroRx
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}#{$1})
1365
- (::Asciidoctor::AttributeList.new $2).parse_into image_attrs, %w(alt width height) unless $2.empty?
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: File.join(workdir, image_path)).cover_image
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, height = 1050, 1600
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 = %(<!DOCTYPE html>
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 doc, workdir
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 doc
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 SvgImgSniffRx =~ front_matter_content
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 ImgSrcScanRx do
1458
- @book.add_item $1, content: File.join(File.dirname(front_matter), $1)
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 doc, usernames
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), content: ::File.join(DATA_DIR, 'images/default-headshot.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? ? '.' : workdir
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? (resolved_avatar = (::File.join workdir, avatar))
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? (resolved_headshot = (::File.join workdir, headshot))
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 doc, items, landmarks, depth
1495
- lines = [%(<!DOCTYPE html>
1496
- <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}">
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 items, depth, state = {}
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 (chapter_name = get_chapter_name item).nil?
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
- item_label = sanitize_doctitle_xml item, :cdata
1542
- else
1543
- item_label = sanitize_xml get_numbered_title(item), :cdata
1544
- end
1545
- item_href = (state[:content_doc_href] = %(#{chapter_name}.xhtml))
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 == 0 || (child_sections = item.sections).empty?
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 chapter_name.nil?
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 doc, items, depth
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
- %{depth}
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 '%{depth}', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>)
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 items, depth, state = {}
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 (chapter_name = get_chapter_name item).nil?
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
- item_label = sanitize_doctitle_xml item, :cdata
1591
- else
1592
- item_label = sanitize_xml get_numbered_title(item), :cdata
1593
- end
1594
- item_href = (state[:content_doc_href] = %(#{chapter_name}.xhtml))
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 == 0 || (child_sections = item.sections).empty?
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 chapter_name.nil?
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 filename, scripts = 'latin'
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(/url\(\.\.\/([^)]+\.ttf)\)/).flatten
1618
-
1619
- [font_list, font_css.to_ios]
1620
- end
1729
+ font_list = font_css.scan(%r{url\(\.\./([^)]+?\.ttf)\)}).flatten
1621
1730
 
1622
- def postprocess_css_file filename, format
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 postprocess_css content, format
1628
- return content.to_ios unless format == :kf8
1629
- # TODO: convert regular expressions to constants
1630
- content
1631
- .gsub(/^ -webkit-column-break-.*\n/, '')
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
- # NOTE Kindle requires that
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(/<script type="text\/javascript">.*?<\/script>\n?/m, '')
1747
+ .gsub(%r{<script type="text/javascript">.*?</script>\n?}m, '')
1647
1748
  .to_ios
1648
1749
  end
1649
1750
 
1650
- def get_kindlegen_command
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['KINDLEGEN']).nil?
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 epub_file, target, compress
1673
- mobi_file = ::File.basename target.sub(EpubExtensionRx, '.mobi')
1674
- compress_flag = KindlegenCompression[compress ? (compress.empty? ? '1' : compress.to_s) : '0']
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 = get_kindlegen_command + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
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', cause: e
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 get_epubcheck_command
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['EPUBCHECK']).nil?
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 epub_file
1723
- argv = get_epubcheck_command + ['-w', epub_file]
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', cause: e
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 line
1847
+ def log_line(line)
1741
1848
  line = line.strip
1742
1849
 
1743
- if line =~ /^fatal/i
1850
+ case line
1851
+ when /^fatal/i
1744
1852
  logger.fatal line
1745
- elsif line =~ /^error/i
1853
+ when /^error/i
1746
1854
  logger.error line
1747
- elsif line =~ /^warning/i
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 node
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? role
1873
+ def role_valid_class?(role)
1766
1874
  role.is_a? String
1767
1875
  end
1768
1876
  end
1769
1877
 
1770
- class DocumentIdGenerator
1771
- ReservedIds = %w(cover nav ncx)
1772
- CharRefRx = /&(?:([a-zA-Z][a-zA-Z]+\d{0,2})|#(\d\d\d{0,4})|#x([\da-fA-F][\da-fA-F][\da-fA-F]{0,3}));/
1773
- if defined? __dir__
1774
- InvalidIdCharsRx = /[^\p{Word}]+/
1775
- LeadingDigitRx = /^\p{Nd}/
1776
- else
1777
- InvalidIdCharsRx = /[^[:word:]]+/
1778
- LeadingDigitRx = /^[[:digit:]]/
1779
- end
1780
-
1781
- class << self
1782
- def generate_id doc, pre = nil, sep = nil
1783
- synthetic = false
1784
- unless (id = doc.id)
1785
- # NOTE we assume pre is a valid ID prefix and that pre and sep only contain valid ID chars
1786
- pre ||= '_'
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
- doc.id = DocumentIdGenerator.generate_id doc, (doc.attr 'idprefix'), (doc.attr 'idseparator')
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