asciidoctor-epub3 1.5.0.alpha.14 → 1.5.0.alpha.19

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'mime/types'
3
4
  require 'open3'
4
5
  require_relative 'font_icon_map'
5
6
 
@@ -29,7 +30,7 @@ module Asciidoctor
29
30
  unless (entry_dir = ::File.dirname entry.name) == '.' || (::File.directory? entry_dir)
30
31
  ::FileUtils.mkdir_p entry_dir
31
32
  end
32
- entry.extract
33
+ entry.extract entry.name
33
34
  end
34
35
  end
35
36
  end
@@ -38,9 +39,9 @@ module Asciidoctor
38
39
 
39
40
  if @format == :kf8
40
41
  # QUESTION shouldn't we validate this epub file too?
41
- distill_epub_to_mobi epub_file, target, @compress, @kindlegen_path
42
+ distill_epub_to_mobi epub_file, target, @compress
42
43
  elsif @validate
43
- validate_epub epub_file, @epubcheck_path
44
+ validate_epub epub_file
44
45
  end
45
46
  end
46
47
 
@@ -105,16 +106,45 @@ module Asciidoctor
105
106
  send method_name, node
106
107
  else
107
108
  logger.warn %(conversion missing in backend #{@backend} for #{name})
109
+ nil
108
110
  end
109
111
  end
110
112
 
111
113
  # See https://asciidoctor.org/docs/user-manual/#book-parts-and-chapters
112
114
  def get_chapter_name node
113
115
  if node.document.doctype != 'book'
114
- return Asciidoctor::Document === node ? node.attr('docname') : nil
116
+ return Asciidoctor::Document === node ? node.attr('docname') || node.id : nil
115
117
  end
116
118
  return (node.id || 'preamble') if node.context == :preamble && node.level == 0
117
- Asciidoctor::Section === node && node.level <= 1 ? node.id : nil
119
+ chapter_level = [node.document.attr('epub-chapter-level', 1).to_i, 1].max
120
+ Asciidoctor::Section === node && node.level <= chapter_level ? node.id : nil
121
+ end
122
+
123
+ def get_numbered_title node
124
+ doc_attrs = node.document.attributes
125
+ level = node.level
126
+ if node.caption
127
+ title = node.captioned_title
128
+ elsif node.respond_to?(:numbered) && node.numbered && level <= (doc_attrs['sectnumlevels'] || 3).to_i
129
+ if level < 2 && node.document.doctype == 'book'
130
+ if node.sectname == 'chapter'
131
+ title = %(#{(signifier = doc_attrs['chapter-signifier']) ? "#{signifier} " : ''}#{node.sectnum} #{node.title})
132
+ elsif node.sectname == 'part'
133
+ title = %(#{(signifier = doc_attrs['part-signifier']) ? "#{signifier} " : ''}#{node.sectnum nil, ':'} #{node.title})
134
+ else
135
+ title = %(#{node.sectnum} #{node.title})
136
+ end
137
+ else
138
+ title = %(#{node.sectnum} #{node.title})
139
+ end
140
+ else
141
+ title = node.title
142
+ end
143
+ title
144
+ end
145
+
146
+ def icon_names
147
+ @icon_names ||= []
118
148
  end
119
149
 
120
150
  def convert_document node
@@ -126,8 +156,7 @@ module Asciidoctor
126
156
  @kindlegen_path = node.attr 'ebook-kindlegen-path'
127
157
  @epubcheck_path = node.attr 'ebook-epubcheck-path'
128
158
  @xrefs_seen = ::Set.new
129
- @icon_names = []
130
- @images = []
159
+ @media_files = {}
131
160
  @footnotes = []
132
161
 
133
162
  @book = GEPUB::Book.new 'EPUB/package.opf'
@@ -182,41 +211,75 @@ module Asciidoctor
182
211
  @book.metadata.add_metadata 'subject', s
183
212
  end
184
213
 
185
- add_cover_image node
186
- add_front_matter_page node
214
+ if node.attr? 'series-name'
215
+ series_name = node.attr 'series-name'
216
+ series_volume = node.attr 'series-volume', 1
217
+ series_id = node.attr 'series-id'
218
+
219
+ series_meta = @book.metadata.add_metadata 'meta', series_name, id: 'pub-collection', group_position: series_volume
220
+ series_meta['property'] = 'belongs-to-collection'
221
+ series_meta.refine 'dcterms:identifier', series_id unless series_id.nil?
222
+ # Calibre only understands 'series'
223
+ series_meta.refine 'collection-type', 'series'
224
+ end
225
+
226
+ # For list of supported landmark types see
227
+ # https://idpf.github.io/epub-vocabs/structure/
228
+ landmarks = []
229
+
230
+ cover_page = add_cover_page node
231
+ landmarks << { type: 'cover', href: cover_page.href, title: 'Cover' } unless cover_page.nil?
232
+
233
+ 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?
235
+
236
+ nav_item = @book.add_item('nav.xhtml', id: 'nav').nav
237
+
238
+ toclevels = [(node.attr 'toclevels', 1).to_i, 0].max
239
+ outlinelevels = [(node.attr 'outlinelevels', toclevels).to_i, 0].max
240
+
241
+ if node.attr? 'toc'
242
+ toc_item = @book.add_ordered_item 'toc.xhtml', id: 'toc'
243
+ landmarks << { type: 'toc', href: toc_item.href, title: node.attr('toc-title') }
244
+ else
245
+ toc_item = nil
246
+ end
187
247
 
188
248
  if node.doctype == 'book'
189
- toc_items = []
190
- node.sections.each do |section|
191
- toc_items << section
192
- section.sections.each do |subsection|
193
- next if get_chapter_name(node).nil?
194
- toc_items << subsection
195
- end
196
- end
249
+ toc_items = node.sections
197
250
  node.content
198
251
  else
199
252
  toc_items = [node]
200
253
  add_chapter node
201
254
  end
202
255
 
203
- nav_xhtml = @book.add_item 'nav.xhtml', content: postprocess_xhtml(nav_doc(node, toc_items)), id: 'nav'
204
- nav_xhtml.nav
256
+ landmarks << { type: 'bodymatter', href: %(#{get_chapter_name toc_items[0]}.xhtml), title: 'Start of Content' } unless toc_items.empty?
257
+
258
+ toc_items.each do |item|
259
+ landmarks << { type: item.style, href: %(#{get_chapter_name item}.xhtml), title: item.title } if %w(appendix bibliography glossary index preface).include? item.style
260
+ end
261
+
262
+ nav_item.add_content postprocess_xhtml(nav_doc(node, toc_items, landmarks, outlinelevels))
263
+ # User is not supposed to see landmarks, so pass empty array here
264
+ toc_item&.add_content postprocess_xhtml(nav_doc(node, toc_items, [], toclevels))
205
265
 
206
266
  # NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
207
- toc_ncx = ncx_doc node, toc_items
267
+ toc_ncx = ncx_doc node, toc_items, outlinelevels
208
268
  @book.add_item 'toc.ncx', content: toc_ncx.to_ios, id: 'ncx'
209
269
 
210
270
  docimagesdir = (node.attr 'imagesdir', '.').chomp '/'
211
271
  docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/))
212
272
 
213
- @images.each do |image|
214
- if image[:name].start_with? %(#{docimagesdir}jacket/cover.)
215
- logger.warn %(image path is reserved for cover artwork: #{image[:name]}; skipping image found in content)
216
- elsif ::File.readable? image[:path]
217
- @book.add_item image[:name], content: image[:path]
273
+ @media_files.each do |name, file|
274
+ if name.start_with? %(#{docimagesdir}jacket/cover.)
275
+ logger.warn %(path is reserved for cover artwork: #{name}; skipping file found in content)
276
+ elsif file[:path].nil? || File.readable?(file[:path])
277
+ mime_types = MIME::Types.type_for name
278
+ mime_types.delete_if {|x| x.media_type != file[:media_type] }
279
+ preferred_mime_type = mime_types.empty? ? nil : mime_types[0].content_type
280
+ @book.add_item name, content: file[:path], media_type: preferred_mime_type
218
281
  else
219
- logger.error %(#{File.basename node.attr('docfile')}: image not found or not readable: #{image[:path]})
282
+ logger.error %(#{File.basename node.attr('docfile')}: media file not found or not readable: #{file[:path]})
220
283
  end
221
284
  end
222
285
 
@@ -267,25 +330,22 @@ module Asciidoctor
267
330
 
268
331
  chapter_item = @book.add_ordered_item %(#{docid}.xhtml)
269
332
 
270
- if node.context == :document && (doctitle = node.doctitle partition: true, use_fallback: true).subtitle?
333
+ doctitle = node.document.doctitle partition: true, use_fallback: true
334
+ chapter_title = doctitle.combined
335
+
336
+ if node.context == :document && doctitle.subtitle?
271
337
  title = %(#{doctitle.main} )
272
338
  subtitle = doctitle.subtitle
273
339
  elsif node.title
274
340
  # HACK: until we get proper handling of title-only in CSS
275
341
  title = ''
276
- subtitle = node.title
342
+ subtitle = get_numbered_title node
343
+ chapter_title = subtitle
277
344
  else
278
345
  title = nil
279
346
  subtitle = nil
280
347
  end
281
348
 
282
- doctitle_sanitized = (node.document.doctitle sanitize: true, use_fallback: true).to_s
283
-
284
- # By default, Kindle does not allow the line height to be adjusted.
285
- # But if you float the elements, then the line height disappears and can be restored manually using margins.
286
- # See https://github.com/asciidoctor/asciidoctor-epub3/issues/123
287
- subtitle_formatted = subtitle ? subtitle.split.map {|w| %(<b>#{w}</b>) } * ' ' : nil
288
-
289
349
  if node.document.doctype == 'book'
290
350
  byline = ''
291
351
  else
@@ -303,10 +363,10 @@ module Asciidoctor
303
363
 
304
364
  # NOTE must run after content is resolved
305
365
  # TODO perhaps create dynamic CSS file?
306
- if @icon_names.empty?
366
+ if icon_names.empty?
307
367
  icon_css_head = ''
308
368
  else
309
- icon_defs = @icon_names.map {|name|
369
+ icon_defs = icon_names.map {|name|
310
370
  %(.i-#{name}::before { content: "#{FontIconMap.unicode name}"; })
311
371
  } * LF
312
372
  icon_css_head = %(<style>
@@ -317,16 +377,20 @@ module Asciidoctor
317
377
 
318
378
  header = (title || subtitle) ? %(<header>
319
379
  <div class="chapter-header">
320
- #{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle_formatted}</small>) : ''}</h1>
380
+ #{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle}</small>) : ''}</h1>
321
381
  </div>
322
382
  </header>) : ''
323
383
 
384
+ # We want highlighter CSS to be stored in a separate file
385
+ # in order to avoid style duplication across chapter files
386
+ linkcss = true
387
+
324
388
  # NOTE kindlegen seems to mangle the <header> element, so we wrap its content in a div
325
389
  lines = [%(<!DOCTYPE html>
326
- <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="#{lang = node.document.attr 'lang', 'en'}" lang="#{lang}">
390
+ <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}">
327
391
  <head>
328
392
  <meta charset="UTF-8"/>
329
- <title>#{doctitle_sanitized}</title>
393
+ <title>#{chapter_title}</title>
330
394
  <link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
331
395
  <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
332
396
  #{icon_css_head}<script type="text/javascript"><![CDATA[
@@ -337,12 +401,18 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
337
401
  }
338
402
  document.body.setAttribute('class', reader.name.toLowerCase().replace(/ /g, '-'));
339
403
  });
340
- ]]></script>
341
- </head>
404
+ ]]></script>)]
405
+
406
+ syntax_hl = node.document.syntax_highlighter
407
+ epub_type_attr = node.respond_to?(:section) && node.sectname != 'section' ? %( epub:type="#{node.sectname}") : ''
408
+
409
+ lines << (syntax_hl.docinfo :head, node, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :head
410
+
411
+ lines << %(</head>
342
412
  <body>
343
- <section class="chapter" title="#{doctitle_sanitized.gsub '"', '&quot;'}" epub:type="chapter" id="#{docid}">
413
+ <section class="chapter" title=#{chapter_title.encode xml: :attr}#{epub_type_attr} id="#{docid}">
344
414
  #{header}
345
- #{content})]
415
+ #{content})
346
416
 
347
417
  unless (fns = node.document.footnotes - @footnotes).empty?
348
418
  @footnotes += fns
@@ -361,8 +431,11 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
361
431
  </footer>'
362
432
  end
363
433
 
364
- lines << '</section>
365
- </body>
434
+ lines << '</section>'
435
+
436
+ lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :footer
437
+
438
+ lines << '</body>
366
439
  </html>'
367
440
 
368
441
  chapter_item.add_content postprocess_xhtml lines * LF
@@ -378,11 +451,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
378
451
  def convert_section node
379
452
  if add_chapter(node).nil?
380
453
  hlevel = node.level
381
- epub_type_attr = node.special ? %( epub:type="#{node.sectname}") : ''
454
+ epub_type_attr = node.sectname != 'section' ? %( epub:type="#{node.sectname}") : ''
382
455
  div_classes = [%(sect#{node.level}), node.role].compact
383
- title = node.title
384
- title_sanitized = xml_sanitize title
385
- %(<section class="#{div_classes * ' '}" title="#{title_sanitized}"#{epub_type_attr}>
456
+ title = get_numbered_title node
457
+ %(<section class="#{div_classes * ' '}" title=#{title.encode xml: :attr}#{epub_type_attr}>
386
458
  <h#{hlevel} id="#{node.id}">#{title}</h#{hlevel}>#{(content = node.content).empty? ? '' : %(
387
459
  #{content})}
388
460
  </section>)
@@ -427,15 +499,16 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
427
499
  end
428
500
 
429
501
  def convert_paragraph node
502
+ id_attr = node.id ? %( id="#{node.id}") : ''
430
503
  role = node.role
431
504
  # stack-head is the alternative to the default, inline-head (where inline means "run-in")
432
505
  head_stop = node.attr 'head-stop', (role && (node.has_role? 'stack-head') ? nil : '.')
433
506
  head = node.title? ? %(<strong class="head">#{title = node.title}#{head_stop && title !~ TrailingPunctRx ? head_stop : ''}</strong> ) : ''
434
507
  if role
435
508
  node.set_option 'hardbreaks' if node.has_role? 'signature'
436
- %(<p class="#{role}">#{head}#{node.content}</p>)
509
+ %(<p#{id_attr} class="#{role}">#{head}#{node.content}</p>)
437
510
  else
438
- %(<p>#{head}#{node.content}</p>)
511
+ %(<p#{id_attr}>#{head}#{node.content}</p>)
439
512
  end
440
513
  end
441
514
 
@@ -464,14 +537,12 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
464
537
  type = node.attr 'name'
465
538
  epub_type = case type
466
539
  when 'tip'
467
- 'help'
468
- when 'note'
469
- 'note'
470
- when 'important', 'warning', 'caution'
471
- 'warning'
540
+ 'tip'
541
+ when 'important', 'warning', 'caution', 'note'
542
+ 'notice'
472
543
  else
473
544
  logger.warn %(unknown admonition type: #{type})
474
- 'note'
545
+ 'notice'
475
546
  end
476
547
  %(<aside#{id_attr} class="admonition #{type}"#{title_attr} epub:type="#{epub_type}">
477
548
  #{title_el}<div class="content">
@@ -498,22 +569,64 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
498
569
  end
499
570
 
500
571
  def convert_listing node
572
+ id_attribute = node.id ? %( id="#{node.id}") : ''
573
+ nowrap = (node.option? 'nowrap') || !(node.document.attr? 'prewrap')
574
+ if node.style == 'source'
575
+ lang = node.attr 'language'
576
+ syntax_hl = node.document.syntax_highlighter
577
+ if syntax_hl
578
+ opts = syntax_hl.highlight? ? {
579
+ css_mode: ((doc_attrs = node.document.attributes)[%(#{syntax_hl.name}-css)] || :class).to_sym,
580
+ style: doc_attrs[%(#{syntax_hl.name}-style)],
581
+ } : {}
582
+ opts[:nowrap] = nowrap
583
+ else
584
+ pre_open = %(<pre class="highlight#{nowrap ? ' nowrap' : ''}"><code#{lang ? %( class="language-#{lang}" data-lang="#{lang}") : ''}>)
585
+ pre_close = '</code></pre>'
586
+ end
587
+ else
588
+ pre_open = %(<pre#{nowrap ? ' class="nowrap"' : ''}>)
589
+ pre_close = '</pre>'
590
+ syntax_hl = nil
591
+ end
501
592
  figure_classes = ['listing']
502
593
  figure_classes << 'coalesce' if node.option? 'unbreakable'
503
- pre_classes = node.style == 'source' ? ['source', %(language-#{node.attr 'language'})] : ['screen']
504
- title_div = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>
505
- ) : ''
506
- %(<figure class="#{figure_classes * ' '}">
507
- #{title_div}<pre class="#{pre_classes * ' '}"><code>#{node.content}</code></pre>
594
+ title_div = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
595
+ %(<figure#{id_attribute} class="#{figure_classes * ' '}">#{title_div}
596
+ #{syntax_hl ? (syntax_hl.format node, lang, opts) : pre_open + (node.content || '') + pre_close}
597
+ </figure>)
598
+ end
599
+
600
+ def convert_stem node
601
+ return convert_listing node if node.style != 'asciimath' || !asciimath_available?
602
+
603
+ id_attr = node.id ? %( id="#{node.id}") : ''
604
+ title_element = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
605
+ equation_data = AsciiMath.parse(node.content).to_mathml 'mml:'
606
+
607
+ %(<figure#{id_attr} class="#{prepend_space node.role}">
608
+ #{title_element}
609
+ <div class="content">
610
+ #{equation_data}
611
+ </div>
508
612
  </figure>)
509
613
  end
510
614
 
511
- # TODO: implement proper stem support. See https://github.com/asciidoctor/asciidoctor-epub3/issues/10
512
- alias convert_stem convert_listing
615
+ def asciimath_available?
616
+ (@asciimath_status ||= load_asciimath) == :loaded
617
+ end
618
+
619
+ def load_asciimath
620
+ Helpers.require_library('asciimath', true, :warn).nil? ? :unavailable : :loaded
621
+ end
513
622
 
514
- # QUESTION should we wrap the <pre> in either <div> or <figure>?
515
623
  def convert_literal node
516
- %(<pre class="screen">#{node.content}</pre>)
624
+ id_attribute = node.id ? %( id="#{node.id}") : ''
625
+ title_element = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
626
+ %(<figure#{id_attribute} class="literalblock#{prepend_space node.role}">
627
+ #{title_element}
628
+ <div class="content"><pre class="screen">#{node.content}</pre></div>
629
+ </figure>)
517
630
  end
518
631
 
519
632
  def convert_page_break _node
@@ -595,41 +708,34 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
595
708
  lines = [%(<div class="table">)]
596
709
  lines << %(<div class="content">)
597
710
  table_id_attr = node.id ? %( id="#{node.id}") : ''
598
- frame_class = {
599
- 'all' => 'table-framed',
600
- 'topbot' => 'table-framed-topbot',
601
- 'sides' => 'table-framed-sides',
602
- 'none' => '',
603
- }
604
- grid_class = {
605
- 'all' => 'table-grid',
606
- 'rows' => 'table-grid-rows',
607
- 'cols' => 'table-grid-cols',
608
- 'none' => '',
609
- }
610
- table_classes = %W[table #{frame_class[node.attr 'frame'] || frame_class['topbot']} #{grid_class[node.attr 'grid'] || grid_class['rows']}]
711
+ table_classes = [
712
+ 'table',
713
+ %(table-framed-#{node.attr 'frame', 'rows', 'table-frame'}),
714
+ %(table-grid-#{node.attr 'grid', 'rows', 'table-grid'}),
715
+ ]
611
716
  if (role = node.role)
612
717
  table_classes << role
613
718
  end
614
- table_class_attr = %( class="#{table_classes * ' '}")
615
719
  table_styles = []
616
- table_styles << %(width: #{node.attr 'tablepcwidth'}%) unless (node.option? 'autowidth') && !(node.attr? 'width', nil, false)
720
+ if (autowidth = node.option? 'autowidth') && !(node.attr? 'width')
721
+ table_classes << 'fit-content'
722
+ else
723
+ table_styles << %(width: #{node.attr 'tablepcwidth'}%;)
724
+ end
725
+ table_class_attr = %( class="#{table_classes * ' '}")
617
726
  table_style_attr = !table_styles.empty? ? %( style="#{table_styles * '; '}") : ''
618
727
 
619
728
  lines << %(<table#{table_id_attr}#{table_class_attr}#{table_style_attr}>)
620
729
  lines << %(<caption>#{node.captioned_title}</caption>) if node.title?
621
730
  if (node.attr 'rowcount') > 0
622
731
  lines << '<colgroup>'
623
- #if node.option? 'autowidth'
624
- tag = %(<col/>)
625
- node.columns.size.times do
626
- lines << tag
732
+ if autowidth
733
+ lines += (Array.new node.columns.size, %(<col/>))
734
+ else
735
+ node.columns.each do |col|
736
+ lines << ((col.option? 'autowidth') ? %(<col/>) : %(<col style="width: #{col.attr 'colpcwidth'}%;" />))
737
+ end
627
738
  end
628
- #else
629
- # node.columns.each do |col|
630
- # lines << %(<col style="width: #{col.attr 'colpcwidth'}%"/>)
631
- # end
632
- #end
633
739
  lines << '</colgroup>'
634
740
  [:head, :body, :foot].reject {|tsec| node.rows[tsec].empty? }.each do |tsec|
635
741
  lines << %(<t#{tsec}>)
@@ -649,19 +755,16 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
649
755
  else
650
756
  cell_content = ''
651
757
  cell.content.each do |text|
652
- cell_content = %(#{cell_content}<p>#{text}</p>)
758
+ cell_content = %(#{cell_content}<p class="tableblock">#{text}</p>)
653
759
  end
654
760
  end
655
761
  end
656
762
 
657
763
  cell_tag_name = tsec == :head || cell.style == :header ? 'th' : 'td'
658
- cell_classes = []
659
- if (halign = cell.attr 'halign') && halign != 'left'
660
- cell_classes << 'halign-left'
661
- end
662
- if (halign = cell.attr 'valign') && halign != 'top'
663
- cell_classes << 'valign-top'
664
- end
764
+ cell_classes = [
765
+ "halign-#{cell.attr 'halign'}",
766
+ "valign-#{cell.attr 'valign'}",
767
+ ]
665
768
  cell_class_attr = !cell_classes.empty? ? %( class="#{cell_classes * ' '}") : ''
666
769
  cell_colspan_attr = cell.colspan ? %( colspan="#{cell.colspan}") : ''
667
770
  cell_rowspan_attr = cell.rowspan ? %( rowspan="#{cell.rowspan}") : ''
@@ -694,16 +797,30 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
694
797
  # TODO: add complex class if list has nested blocks
695
798
  def convert_dlist node
696
799
  lines = []
800
+ id_attribute = node.id ? %( id="#{node.id}") : ''
801
+
802
+ classes = case node.style
803
+ when 'horizontal'
804
+ ['hdlist', node.role]
805
+ when 'itemized', 'ordered'
806
+ # QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
807
+ ['dlist', %(#{node.style}-list), node.role]
808
+ else
809
+ ['description-list']
810
+ end.compact
811
+
812
+ class_attribute = %( class="#{classes.join ' '}")
813
+
814
+ lines << %(<div#{id_attribute}#{class_attribute}>)
815
+ lines << %(<div class="title">#{node.title}</div>) if node.title?
816
+
697
817
  case (style = node.style)
698
818
  when 'itemized', 'ordered'
699
819
  list_tag_name = style == 'itemized' ? 'ul' : 'ol'
700
820
  role = node.role
701
821
  subject_stop = node.attr 'subject-stop', (role && (node.has_role? 'stack') ? nil : ':')
702
- # QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
703
- div_classes = [%(#{style}-list), role].compact
704
822
  list_class_attr = (node.option? 'brief') ? ' class="brief"' : ''
705
- lines << %(<div class="#{div_classes * ' '}">
706
- <#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
823
+ lines << %(<#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
707
824
  node.items.each do |subjects, dd|
708
825
  # consists of one term (a subject) and supporting content
709
826
  subject = [*subjects].first.text
@@ -719,11 +836,40 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
719
836
  end
720
837
  lines << '</li>'
721
838
  end
722
- lines << %(</#{list_tag_name}>
723
- </div>)
839
+ lines << %(</#{list_tag_name}>)
840
+ when 'horizontal'
841
+ lines << '<table>'
842
+ if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
843
+ lines << '<colgroup>'
844
+ col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : ''
845
+ lines << %(<col#{col_style_attribute} />)
846
+ col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : ''
847
+ lines << %(<col#{col_style_attribute} />)
848
+ lines << '</colgroup>'
849
+ end
850
+ node.items.each do |terms, dd|
851
+ lines << '<tr>'
852
+ lines << %(<td class="hdlist1#{(node.option? 'strong') ? ' strong' : ''}">)
853
+ first_term = true
854
+ terms.each do |dt|
855
+ lines << %(<br />) unless first_term
856
+ lines << '<p>'
857
+ lines << dt.text
858
+ lines << '</p>'
859
+ first_term = nil
860
+ end
861
+ lines << '</td>'
862
+ lines << '<td class="hdlist2">'
863
+ if dd
864
+ lines << %(<p>#{dd.text}</p>) if dd.text?
865
+ lines << dd.content if dd.blocks?
866
+ end
867
+ lines << '</td>'
868
+ lines << '</tr>'
869
+ end
870
+ lines << '</table>'
724
871
  else
725
- lines << '<div class="description-list">
726
- <dl>'
872
+ lines << '<dl>'
727
873
  node.items.each do |terms, dd|
728
874
  [*terms].each do |dt|
729
875
  lines << %(<dt>
@@ -740,9 +886,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
740
886
  end
741
887
  lines << '</dd>'
742
888
  end
743
- lines << '</dl>
744
- </div>'
889
+ lines << '</dl>'
745
890
  end
891
+
892
+ lines << '</div>'
746
893
  lines * LF
747
894
  end
748
895
 
@@ -816,22 +963,29 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
816
963
  document
817
964
  end
818
965
 
819
- def register_image node, target
820
- if target.end_with? '.svg'
966
+ def register_media_file node, target, media_type
967
+ if target.end_with?('.svg') || target.start_with?('data:image/svg+xml')
821
968
  chapter = get_enclosing_chapter node
822
969
  chapter.set_attr 'epub-properties', [] unless chapter.attr? 'epub-properties'
823
970
  epub_properties = chapter.attr 'epub-properties'
824
971
  epub_properties << 'svg' unless epub_properties.include? 'svg'
825
972
  end
826
973
 
827
- out_dir = node.attr('outdir', nil, true) || doc_option(node.document, :to_dir)
828
- fs_path = (::File.join out_dir, target)
829
- unless ::File.exist? fs_path
830
- base_dir = root_document(node.document).base_dir
831
- fs_path = ::File.join base_dir, target
974
+ return if target.start_with? 'data:'
975
+
976
+ if Asciidoctor::Helpers.uriish? target
977
+ # We need to add both local and remote media files to manifest
978
+ fs_path = nil
979
+ else
980
+ out_dir = node.attr('outdir', nil, true) || doc_option(node.document, :to_dir)
981
+ fs_path = (::File.join out_dir, target)
982
+ unless ::File.exist? fs_path
983
+ base_dir = root_document(node.document).base_dir
984
+ fs_path = ::File.join base_dir, target
985
+ end
832
986
  end
833
987
  # We need *both* virtual and physical image paths. Unfortunately, references[:images] only has one of them.
834
- @images << { name: target, path: fs_path }
988
+ @media_files[target] ||= { path: fs_path, media_type: media_type }
835
989
  end
836
990
 
837
991
  def resolve_image_attrs node
@@ -848,16 +1002,81 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
848
1002
  img_attrs
849
1003
  end
850
1004
 
1005
+ def convert_audio node
1006
+ id_attr = node.id ? %( id="#{node.id}") : ''
1007
+ target = node.media_uri node.attr 'target'
1008
+ register_media_file node, target, 'audio'
1009
+ title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
1010
+
1011
+ autoplay_attr = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
1012
+ controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
1013
+ loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
1014
+
1015
+ start_t = node.attr 'start'
1016
+ end_t = node.attr 'end'
1017
+ if start_t || end_t
1018
+ time_anchor = %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
1019
+ else
1020
+ time_anchor = ''
1021
+ end
1022
+
1023
+ %(<figure#{id_attr} class="audioblock#{prepend_space node.role}">#{title_element}
1024
+ <div class="content">
1025
+ <audio src="#{target}#{time_anchor}"#{autoplay_attr}#{controls_attr}#{loop_attr}>
1026
+ <div>Your Reading System does not support (this) audio.</div>
1027
+ </audio>
1028
+ </div>
1029
+ </figure>)
1030
+ end
1031
+
1032
+ # TODO: Support multiple video files in different formats for a single video
1033
+ def convert_video node
1034
+ id_attr = node.id ? %( id="#{node.id}") : ''
1035
+ target = node.media_uri node.attr 'target'
1036
+ register_media_file node, target, 'video'
1037
+ title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
1038
+
1039
+ width_attr = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : ''
1040
+ height_attr = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : ''
1041
+ autoplay_attr = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
1042
+ controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
1043
+ loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
1044
+
1045
+ start_t = node.attr 'start'
1046
+ end_t = node.attr 'end'
1047
+ if start_t || end_t
1048
+ time_anchor = %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''})
1049
+ else
1050
+ time_anchor = ''
1051
+ end
1052
+
1053
+ if (poster = node.attr 'poster').nil_or_empty?
1054
+ poster_attr = ''
1055
+ else
1056
+ poster = node.media_uri poster
1057
+ register_media_file node, poster, 'image'
1058
+ poster_attr = %( poster="#{poster}")
1059
+ end
1060
+
1061
+ %(<figure#{id_attr} class="video#{prepend_space node.role}">#{title_element}
1062
+ <div class="content">
1063
+ <video src="#{target}#{time_anchor}"#{width_attr}#{height_attr}#{autoplay_attr}#{poster_attr}#{controls_attr}#{loop_attr}>
1064
+ <div>Your Reading System does not support (this) video.</div>
1065
+ </video>
1066
+ </div>
1067
+ </figure>)
1068
+ end
1069
+
851
1070
  def convert_image node
852
1071
  target = node.image_uri node.attr 'target'
853
- register_image node, target
1072
+ register_media_file node, target, 'image'
854
1073
  id_attr = node.id ? %( id="#{node.id}") : ''
1074
+ title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
855
1075
  img_attrs = resolve_image_attrs node
856
1076
  %(<figure#{id_attr} class="image#{prepend_space node.role}">
857
1077
  <div class="content">
858
1078
  <img src="#{target}"#{prepend_space img_attrs * ' '} />
859
- </div>#{node.title? ? %(
860
- <figcaption>#{node.captioned_title}</figcaption>) : ''}
1079
+ </div>#{title_element}
861
1080
  </figure>)
862
1081
  end
863
1082
 
@@ -901,7 +1120,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
901
1120
  end
902
1121
 
903
1122
  id_attr = '' unless @xrefs_seen.add? refid
904
- text = (ref.xreftext node.attr('xrefstyle', nil, true))
1123
+ text ||= (ref.xreftext node.attr('xrefstyle', nil, true))
905
1124
  else
906
1125
  logger.warn %(#{::File.basename doc.attr('docfile')}: invalid reference to unknown anchor: #{refid})
907
1126
  end
@@ -953,7 +1172,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
953
1172
 
954
1173
  def convert_inline_image node
955
1174
  if node.type == 'icon'
956
- @icon_names << (icon_name = node.target)
1175
+ icon_names << (icon_name = node.target)
957
1176
  i_classes = ['icon', %(i-#{icon_name})]
958
1177
  i_classes << %(icon-#{node.attr 'size'}) if node.attr? 'size'
959
1178
  i_classes << %(icon-flip-#{(node.attr 'flip')[0]}) if node.attr? 'flip'
@@ -962,7 +1181,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
962
1181
  %(<i class="#{i_classes * ' '}"></i>)
963
1182
  else
964
1183
  target = node.image_uri node.target
965
- register_image node, target
1184
+ register_media_file node, target, 'image'
966
1185
 
967
1186
  img_attrs = resolve_image_attrs node
968
1187
  img_attrs << %(class="inline#{prepend_space node.role}")
@@ -1000,25 +1219,30 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
1000
1219
  def convert_inline_quoted node
1001
1220
  open, close, tag = QUOTE_TAGS[node.type]
1002
1221
 
1003
- # TODO: implement proper stem support. See https://github.com/asciidoctor/asciidoctor-epub3/issues/10
1222
+ if node.type == :asciimath && asciimath_available?
1223
+ content = AsciiMath.parse(node.text).to_mathml 'mml:'
1224
+ else
1225
+ content = node.text
1226
+ end
1227
+
1004
1228
  node.add_role 'literal' if [:monospaced, :asciimath, :latexmath].include? node.type
1005
1229
 
1006
1230
  if node.id
1007
1231
  class_attr = class_string node
1008
1232
  if tag
1009
- %(#{open.chop} id="#{node.id}"#{class_attr}>#{node.text}#{close})
1233
+ %(#{open.chop} id="#{node.id}"#{class_attr}>#{content}#{close})
1010
1234
  else
1011
- %(<span id="#{node.id}"#{class_attr}>#{open}#{node.text}#{close}</span>)
1235
+ %(<span id="#{node.id}"#{class_attr}>#{open}#{content}#{close}</span>)
1012
1236
  end
1013
1237
  elsif role_valid_class? node.role
1014
1238
  class_attr = class_string node
1015
1239
  if tag
1016
- %(#{open.chop}#{class_attr}>#{node.text}#{close})
1240
+ %(#{open.chop}#{class_attr}>#{content}#{close})
1017
1241
  else
1018
- %(<span#{class_attr}>#{open}#{node.text}#{close}</span>)
1242
+ %(<span#{class_attr}>#{open}#{content}#{close}</span>)
1019
1243
  end
1020
1244
  else
1021
- %(#{open}#{node.text}#{close})
1245
+ %(#{open}#{content}#{close})
1022
1246
  end
1023
1247
  end
1024
1248
 
@@ -1082,6 +1306,19 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
1082
1306
  @book.add_item 'styles/epub3-css3-only.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
1083
1307
  end
1084
1308
 
1309
+ syntax_hl = doc.syntax_highlighter
1310
+ if syntax_hl&.write_stylesheet? doc
1311
+ Dir.mktmpdir do |dir|
1312
+ syntax_hl.write_stylesheet doc, dir
1313
+ Pathname.glob(dir + '/**/*').map do |filename|
1314
+ # Workaround for https://github.com/skoji/gepub/pull/117
1315
+ filename.open do |f|
1316
+ @book.add_item filename.basename.to_s, content: f
1317
+ end if filename.file?
1318
+ end
1319
+ end
1320
+ end
1321
+
1085
1322
  font_files, font_css = select_fonts ::File.join(DATA_DIR, 'styles/epub3-fonts.css'), (doc.attr 'scripts', 'latin')
1086
1323
  @book.add_item 'styles/epub3-fonts.css', content: font_css
1087
1324
  unless font_files.empty?
@@ -1101,8 +1338,8 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
1101
1338
  nil
1102
1339
  end
1103
1340
 
1104
- def add_cover_image doc
1105
- return if (image_path = doc.attr 'front-cover-image').nil?
1341
+ def add_cover_page doc
1342
+ return nil if (image_path = doc.attr 'front-cover-image').nil?
1106
1343
 
1107
1344
  imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
1108
1345
  imagesdir = (imagesdir == '.' ? '' : %(#{imagesdir}/))
@@ -1119,20 +1356,21 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
1119
1356
  workdir = doc.attr 'docdir'
1120
1357
  workdir = '.' if workdir.nil_or_empty?
1121
1358
 
1122
- unless ::File.readable? ::File.join(workdir, image_path)
1123
- logger.error %(#{::File.basename doc.attr('docfile')}: front cover image not found or readable: #{::File.expand_path image_path, workdir})
1124
- return
1359
+ begin
1360
+ @book.add_item(image_href, content: File.join(workdir, image_path)).cover_image
1361
+ rescue => e
1362
+ logger.error %(#{::File.basename doc.attr('docfile')}: error adding front cover image. Make sure that :front-cover-image: attribute points to a valid image file. #{e})
1363
+ return nil
1125
1364
  end
1126
1365
 
1366
+ return nil if @format == :kf8
1367
+
1127
1368
  unless !image_attrs.empty? && (width = image_attrs['width']) && (height = image_attrs['height'])
1128
1369
  width, height = 1050, 1600
1129
1370
  end
1130
1371
 
1131
- @book.add_item(image_href, content: File.join(workdir, image_path)).cover_image
1132
-
1133
- unless @format == :kf8
1134
- # NOTE SVG wrapper maintains aspect ratio and confines image to view box
1135
- content = %(<!DOCTYPE html>
1372
+ # NOTE SVG wrapper maintains aspect ratio and confines image to view box
1373
+ content = %(<!DOCTYPE html>
1136
1374
  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
1137
1375
  <head>
1138
1376
  <meta charset="UTF-8"/>
@@ -1162,10 +1400,7 @@ body > svg {
1162
1400
  </svg></body>
1163
1401
  </html>).to_ios
1164
1402
 
1165
- # Gitden expects a cover.xhtml, so add it to the spine
1166
- @book.add_ordered_item 'cover.xhtml', content: content, id: 'cover'
1167
- end
1168
- nil
1403
+ @book.add_ordered_item 'cover.xhtml', content: content, id: 'cover'
1169
1404
  end
1170
1405
 
1171
1406
  def get_frontmatter_files doc, workdir
@@ -1195,19 +1430,22 @@ body > svg {
1195
1430
  workdir = doc.attr 'docdir'
1196
1431
  workdir = '.' if workdir.nil_or_empty?
1197
1432
 
1433
+ result = nil
1198
1434
  get_frontmatter_files(doc, workdir).each do |front_matter|
1199
1435
  front_matter_content = ::File.read front_matter
1200
1436
 
1201
1437
  front_matter_file = File.basename front_matter, '.html'
1202
1438
  item = @book.add_ordered_item "#{front_matter_file}.xhtml", content: (postprocess_xhtml front_matter_content)
1203
1439
  item.add_property 'svg' if SvgImgSniffRx =~ front_matter_content
1440
+ # Store link to first frontmatter page
1441
+ result = item if result.nil?
1204
1442
 
1205
1443
  front_matter_content.scan ImgSrcScanRx do
1206
1444
  @book.add_item $1, content: File.join(File.dirname(front_matter), $1)
1207
1445
  end
1208
1446
  end
1209
1447
 
1210
- nil
1448
+ result
1211
1449
  end
1212
1450
 
1213
1451
  def add_profile_images doc, usernames
@@ -1239,8 +1477,7 @@ body > svg {
1239
1477
  nil
1240
1478
  end
1241
1479
 
1242
- # TODO: aggregate authors of chapters into authors attribute(s) on main document
1243
- def nav_doc doc, items
1480
+ def nav_doc doc, items, landmarks, depth
1244
1481
  lines = [%(<!DOCTYPE html>
1245
1482
  <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}">
1246
1483
  <head>
@@ -1250,13 +1487,29 @@ body > svg {
1250
1487
  <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
1251
1488
  </head>
1252
1489
  <body>
1253
- <h1>#{sanitize_doctitle_xml doc, :pcdata}</h1>
1254
- <nav epub:type="toc" id="toc">
1255
- <h2>#{doc.attr 'toc-title'}</h2>)]
1256
- lines << (nav_level items, [(doc.attr 'toclevels', 1).to_i, 0].max)
1257
- lines << %(</nav>
1490
+ <section class="chapter">
1491
+ <header>
1492
+ <div class="chapter-header"><h1 class="chapter-title"><small class="subtitle">#{doc.attr 'toc-title'}</small></h1></div>
1493
+ </header>
1494
+ <nav epub:type="toc" id="toc">)]
1495
+ lines << (nav_level items, [depth, 0].max)
1496
+ lines << '</nav>'
1497
+
1498
+ unless landmarks.empty?
1499
+ lines << '
1500
+ <nav epub:type="landmarks" id="landmarks" hidden="hidden">
1501
+ <ol>'
1502
+ landmarks.each do |landmark|
1503
+ lines << %(<li><a epub:type="#{landmark[:type]}" href="#{landmark[:href]}">#{landmark[:title]}</a></li>)
1504
+ end
1505
+ lines << '
1506
+ </ol>
1507
+ </nav>'
1508
+ end
1509
+ lines << '
1510
+ </section>
1258
1511
  </body>
1259
- </html>)
1512
+ </html>'
1260
1513
  lines * LF
1261
1514
  end
1262
1515
 
@@ -1266,14 +1519,14 @@ body > svg {
1266
1519
  items.each do |item|
1267
1520
  #index = (state[:index] = (state.fetch :index, 0) + 1)
1268
1521
  if (chapter_name = get_chapter_name item).nil?
1269
- item_label = sanitize_xml item.title, :pcdata
1522
+ item_label = sanitize_xml get_numbered_title(item), :pcdata
1270
1523
  item_href = %(#{state[:content_doc_href]}##{item.id})
1271
1524
  else
1272
1525
  # NOTE we sanitize the chapter titles because we use formatting to control layout
1273
1526
  if item.context == :document
1274
1527
  item_label = sanitize_doctitle_xml item, :cdata
1275
1528
  else
1276
- item_label = sanitize_xml item.title, :cdata
1529
+ item_label = sanitize_xml get_numbered_title(item), :cdata
1277
1530
  end
1278
1531
  item_href = (state[:content_doc_href] = %(#{chapter_name}.xhtml))
1279
1532
  end
@@ -1290,7 +1543,7 @@ body > svg {
1290
1543
  lines * LF
1291
1544
  end
1292
1545
 
1293
- def ncx_doc doc, items
1546
+ def ncx_doc doc, items, depth
1294
1547
  # TODO: populate docAuthor element based on unique authors in work
1295
1548
  lines = [%(<?xml version="1.0" encoding="utf-8"?>
1296
1549
  <ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="#{doc.attr 'lang', 'en'}">
@@ -1302,7 +1555,7 @@ body > svg {
1302
1555
  </head>
1303
1556
  <docTitle><text>#{sanitize_doctitle_xml doc, :cdata}</text></docTitle>
1304
1557
  <navMap>)]
1305
- lines << (ncx_level items, [(doc.attr 'toclevels', 1).to_i, 0].max, state = {})
1558
+ lines << (ncx_level items, depth, state = {})
1306
1559
  lines[0] = lines[0].sub '%{depth}', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>)
1307
1560
  lines << %(</navMap>
1308
1561
  </ncx>)
@@ -1316,13 +1569,13 @@ body > svg {
1316
1569
  index = (state[:index] = (state.fetch :index, 0) + 1)
1317
1570
  item_id = %(nav_#{index})
1318
1571
  if (chapter_name = get_chapter_name item).nil?
1319
- item_label = sanitize_xml item.title, :cdata
1572
+ item_label = sanitize_xml get_numbered_title(item), :cdata
1320
1573
  item_href = %(#{state[:content_doc_href]}##{item.id})
1321
1574
  else
1322
1575
  if item.context == :document
1323
1576
  item_label = sanitize_doctitle_xml item, :cdata
1324
1577
  else
1325
- item_label = sanitize_xml item.title, :cdata
1578
+ item_label = sanitize_xml get_numbered_title(item), :cdata
1326
1579
  end
1327
1580
  item_href = (state[:content_doc_href] = %(#{chapter_name}.xhtml))
1328
1581
  end
@@ -1380,10 +1633,10 @@ body > svg {
1380
1633
  .to_ios
1381
1634
  end
1382
1635
 
1383
- def get_kindlegen_command kindlegen_path
1384
- unless kindlegen_path.nil?
1385
- logger.debug %(Using ebook-kindlegen-path attribute: #{kindlegen_path})
1386
- return [kindlegen_path]
1636
+ def get_kindlegen_command
1637
+ unless @kindlegen_path.nil?
1638
+ logger.debug %(Using ebook-kindlegen-path attribute: #{@kindlegen_path})
1639
+ return [@kindlegen_path]
1387
1640
  end
1388
1641
 
1389
1642
  unless (result = ENV['KINDLEGEN']).nil?
@@ -1391,22 +1644,15 @@ body > svg {
1391
1644
  return [result]
1392
1645
  end
1393
1646
 
1394
- begin
1395
- require 'kindlegen' unless defined? ::Kindlegen
1396
- result = ::Kindlegen.command.to_s
1397
- logger.debug %(Using KindleGen from gem: #{result})
1398
- [result]
1399
- rescue LoadError => e
1400
- logger.debug %(#{e}; Using KindleGen from PATH)
1401
- [%(kindlegen#{::Gem.win_platform? ? '.exe' : ''})]
1402
- end
1647
+ logger.debug 'Using KindleGen from PATH'
1648
+ [%(kindlegen#{::Gem.win_platform? ? '.exe' : ''})]
1403
1649
  end
1404
1650
 
1405
- def distill_epub_to_mobi epub_file, target, compress, kindlegen_path
1651
+ def distill_epub_to_mobi epub_file, target, compress
1406
1652
  mobi_file = ::File.basename target.sub(EpubExtensionRx, '.mobi')
1407
1653
  compress_flag = KindlegenCompression[compress ? (compress.empty? ? '1' : compress.to_s) : '0']
1408
1654
 
1409
- argv = get_kindlegen_command(kindlegen_path) + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
1655
+ argv = get_kindlegen_command + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
1410
1656
  begin
1411
1657
  # This duplicates Kindlegen.run, but we want to override executable
1412
1658
  out, err, res = Open3.capture3(*argv) do |r|
@@ -1431,10 +1677,10 @@ body > svg {
1431
1677
  end
1432
1678
  end
1433
1679
 
1434
- def get_epubcheck_command epubcheck_path
1435
- unless epubcheck_path.nil?
1436
- logger.debug %(Using ebook-epubcheck-path attribute: #{epubcheck_path})
1437
- return [epubcheck_path]
1680
+ def get_epubcheck_command
1681
+ unless @epubcheck_path.nil?
1682
+ logger.debug %(Using ebook-epubcheck-path attribute: #{@epubcheck_path})
1683
+ return [@epubcheck_path]
1438
1684
  end
1439
1685
 
1440
1686
  unless (result = ENV['EPUBCHECK']).nil?
@@ -1452,8 +1698,8 @@ body > svg {
1452
1698
  end
1453
1699
  end
1454
1700
 
1455
- def validate_epub epub_file, epubcheck_path
1456
- argv = get_epubcheck_command(epubcheck_path) + ['-w', epub_file]
1701
+ def validate_epub epub_file
1702
+ argv = get_epubcheck_command + ['-w', epub_file]
1457
1703
  begin
1458
1704
  out, err, res = Open3.capture3(*argv)
1459
1705
  rescue Errno::ENOENT => e
@@ -1510,6 +1756,7 @@ body > svg {
1510
1756
  InvalidIdCharsRx = /[^[:word:]]+/
1511
1757
  LeadingDigitRx = /^[[:digit:]]/
1512
1758
  end
1759
+
1513
1760
  class << self
1514
1761
  def generate_id doc, pre = nil, sep = nil
1515
1762
  synthetic = false
@@ -1561,13 +1808,19 @@ body > svg {
1561
1808
  Extensions.register do
1562
1809
  if (document = @document).backend == 'epub3'
1563
1810
  document.set_attribute 'listing-caption', 'Listing'
1564
- # pygments.rb hangs on JRuby for Windows, see https://github.com/asciidoctor/asciidoctor-epub3/issues/253
1565
- if !(::RUBY_ENGINE == 'jruby' && Gem.win_platform?) && (Gem.try_activate 'pygments.rb')
1566
- if document.set_attribute 'source-highlighter', 'pygments'
1567
- document.set_attribute 'pygments-css', 'style'
1568
- document.set_attribute 'pygments-style', 'bw'
1569
- end
1811
+
1812
+ # TODO: bw theme for CodeRay
1813
+ document.set_attribute 'pygments-style', 'bw' unless document.attr? 'pygments-style'
1814
+ document.set_attribute 'rouge-style', 'bw' unless document.attr? 'rouge-style'
1815
+
1816
+ # Old asciidoctor versions do not have public API for writing highlighter CSS file
1817
+ # So just use inline CSS there.
1818
+ unless Document.supports_syntax_highlighter?
1819
+ document.set_attribute 'coderay-css', 'style'
1820
+ document.set_attribute 'pygments-css', 'style'
1821
+ document.set_attribute 'rouge-css', 'style'
1570
1822
  end
1823
+
1571
1824
  case (ebook_format = document.attributes['ebook-format'])
1572
1825
  when 'epub3', 'kf8'
1573
1826
  # all good