asciidoctor-epub3 1.5.0.alpha.15 → 1.5.0

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,6 +106,7 @@ 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
 
@@ -114,7 +116,8 @@ module Asciidoctor
114
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
118
121
  end
119
122
 
120
123
  def get_numbered_title node
@@ -140,6 +143,10 @@ module Asciidoctor
140
143
  title
141
144
  end
142
145
 
146
+ def icon_names
147
+ @icon_names ||= []
148
+ end
149
+
143
150
  def convert_document node
144
151
  @format = node.attr('ebook-format').to_sym
145
152
 
@@ -149,8 +156,7 @@ module Asciidoctor
149
156
  @kindlegen_path = node.attr 'ebook-kindlegen-path'
150
157
  @epubcheck_path = node.attr 'ebook-epubcheck-path'
151
158
  @xrefs_seen = ::Set.new
152
- @icon_names = []
153
- @images = []
159
+ @media_files = {}
154
160
  @footnotes = []
155
161
 
156
162
  @book = GEPUB::Book.new 'EPUB/package.opf'
@@ -217,41 +223,66 @@ module Asciidoctor
217
223
  series_meta.refine 'collection-type', 'series'
218
224
  end
219
225
 
220
- add_cover_image node
221
- add_front_matter_page node
226
+ # For list of supported landmark types see
227
+ # https://idpf.github.io/epub-vocabs/structure/
228
+ landmarks = []
229
+
230
+ front_cover = add_cover_page node, 'front-cover'
231
+ landmarks << { type: 'cover', href: front_cover.href, title: 'Front Cover' } unless front_cover.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
222
247
 
223
248
  if node.doctype == 'book'
224
- toc_items = []
225
- node.sections.each do |section|
226
- toc_items << section
227
- section.sections.each do |subsection|
228
- next if get_chapter_name(node).nil?
229
- toc_items << subsection
230
- end
231
- end
249
+ toc_items = node.sections
232
250
  node.content
233
251
  else
234
252
  toc_items = [node]
235
253
  add_chapter node
236
254
  end
237
255
 
238
- nav_xhtml = @book.add_item 'nav.xhtml', content: postprocess_xhtml(nav_doc(node, toc_items)), id: 'nav'
239
- nav_xhtml.nav
256
+ _back_cover = add_cover_page node, 'back-cover'
257
+ # TODO: add landmark for back cover? But what epub:type?
258
+
259
+ landmarks << { type: 'bodymatter', href: %(#{get_chapter_name toc_items[0]}.xhtml), title: 'Start of Content' } unless toc_items.empty?
260
+
261
+ 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
263
+ end
264
+
265
+ nav_item.add_content postprocess_xhtml(nav_doc(node, toc_items, landmarks, outlinelevels))
266
+ # User is not supposed to see landmarks, so pass empty array here
267
+ toc_item&.add_content postprocess_xhtml(nav_doc(node, toc_items, [], toclevels))
240
268
 
241
269
  # NOTE gepub doesn't support building a ncx TOC with depth > 1, so do it ourselves
242
- toc_ncx = ncx_doc node, toc_items
270
+ toc_ncx = ncx_doc node, toc_items, outlinelevels
243
271
  @book.add_item 'toc.ncx', content: toc_ncx.to_ios, id: 'ncx'
244
272
 
245
273
  docimagesdir = (node.attr 'imagesdir', '.').chomp '/'
246
274
  docimagesdir = (docimagesdir == '.' ? nil : %(#{docimagesdir}/))
247
275
 
248
- @images.each do |image|
249
- if image[:name].start_with? %(#{docimagesdir}jacket/cover.)
250
- logger.warn %(image path is reserved for cover artwork: #{image[:name]}; skipping image found in content)
251
- elsif ::File.readable? image[:path]
252
- @book.add_item image[:name], content: image[:path]
276
+ @media_files.each do |name, file|
277
+ if name.start_with? %(#{docimagesdir}jacket/cover.)
278
+ logger.warn %(path is reserved for cover artwork: #{name}; skipping file found in content)
279
+ elsif file[:path].nil? || File.readable?(file[:path])
280
+ mime_types = MIME::Types.type_for name
281
+ mime_types.delete_if {|x| x.media_type != file[:media_type] }
282
+ preferred_mime_type = mime_types.empty? ? nil : mime_types[0].content_type
283
+ @book.add_item name, content: file[:path], media_type: preferred_mime_type
253
284
  else
254
- logger.error %(#{File.basename node.attr('docfile')}: image not found or not readable: #{image[:path]})
285
+ logger.error %(#{File.basename node.attr('docfile')}: media file not found or not readable: #{file[:path]})
255
286
  end
256
287
  end
257
288
 
@@ -302,25 +333,22 @@ module Asciidoctor
302
333
 
303
334
  chapter_item = @book.add_ordered_item %(#{docid}.xhtml)
304
335
 
305
- if node.context == :document && (doctitle = node.doctitle partition: true, use_fallback: true).subtitle?
336
+ doctitle = node.document.doctitle partition: true, use_fallback: true
337
+ chapter_title = doctitle.combined
338
+
339
+ if node.context == :document && doctitle.subtitle?
306
340
  title = %(#{doctitle.main} )
307
341
  subtitle = doctitle.subtitle
308
342
  elsif node.title
309
343
  # HACK: until we get proper handling of title-only in CSS
310
344
  title = ''
311
345
  subtitle = get_numbered_title node
346
+ chapter_title = subtitle
312
347
  else
313
348
  title = nil
314
349
  subtitle = nil
315
350
  end
316
351
 
317
- doctitle_sanitized = (node.document.doctitle sanitize: true, use_fallback: true).to_s
318
-
319
- # By default, Kindle does not allow the line height to be adjusted.
320
- # But if you float the elements, then the line height disappears and can be restored manually using margins.
321
- # See https://github.com/asciidoctor/asciidoctor-epub3/issues/123
322
- subtitle_formatted = subtitle ? subtitle.split.map {|w| %(<b>#{w}</b>) } * ' ' : nil
323
-
324
352
  if node.document.doctype == 'book'
325
353
  byline = ''
326
354
  else
@@ -338,10 +366,10 @@ module Asciidoctor
338
366
 
339
367
  # NOTE must run after content is resolved
340
368
  # TODO perhaps create dynamic CSS file?
341
- if @icon_names.empty?
369
+ if icon_names.empty?
342
370
  icon_css_head = ''
343
371
  else
344
- icon_defs = @icon_names.map {|name|
372
+ icon_defs = icon_names.map {|name|
345
373
  %(.i-#{name}::before { content: "#{FontIconMap.unicode name}"; })
346
374
  } * LF
347
375
  icon_css_head = %(<style>
@@ -352,19 +380,20 @@ module Asciidoctor
352
380
 
353
381
  header = (title || subtitle) ? %(<header>
354
382
  <div class="chapter-header">
355
- #{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle_formatted}</small>) : ''}</h1>
383
+ #{byline}<h1 class="chapter-title">#{title}#{subtitle ? %(<small class="subtitle">#{subtitle}</small>) : ''}</h1>
356
384
  </div>
357
385
  </header>) : ''
358
386
 
359
- # TODO : support writing code highlighter CSS to a separate file
360
- linkcss = false
387
+ # We want highlighter CSS to be stored in a separate file
388
+ # in order to avoid style duplication across chapter files
389
+ linkcss = true
361
390
 
362
391
  # NOTE kindlegen seems to mangle the <header> element, so we wrap its content in a div
363
392
  lines = [%(<!DOCTYPE html>
364
- <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}">
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}">
365
394
  <head>
366
395
  <meta charset="UTF-8"/>
367
- <title>#{doctitle_sanitized}</title>
396
+ <title>#{chapter_title}</title>
368
397
  <link rel="stylesheet" type="text/css" href="styles/epub3.css"/>
369
398
  <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
370
399
  #{icon_css_head}<script type="text/javascript"><![CDATA[
@@ -377,13 +406,14 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
377
406
  });
378
407
  ]]></script>)]
379
408
 
380
- if self.class.supports_highlighter_docinfo? && (syntax_hl = node.document.syntax_highlighter) && (syntax_hl.docinfo? :head)
381
- lines << (syntax_hl.docinfo :head, node, linkcss: linkcss, self_closing_tag_slash: '/')
382
- end
409
+ syntax_hl = node.document.syntax_highlighter
410
+ epub_type_attr = node.respond_to?(:section) && node.sectname != 'section' ? %( epub:type="#{node.sectname}") : ''
411
+
412
+ lines << (syntax_hl.docinfo :head, node, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :head
383
413
 
384
414
  lines << %(</head>
385
415
  <body>
386
- <section class="chapter" title="#{doctitle_sanitized.gsub '"', '&quot;'}" epub:type="chapter" id="#{docid}">
416
+ <section class="chapter" title=#{chapter_title.encode xml: :attr}#{epub_type_attr} id="#{docid}">
387
417
  #{header}
388
418
  #{content})
389
419
 
@@ -406,7 +436,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
406
436
 
407
437
  lines << '</section>'
408
438
 
409
- lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl && (syntax_hl.docinfo? :footer)
439
+ lines << (syntax_hl.docinfo :footer, node.document, linkcss: linkcss, self_closing_tag_slash: '/') if syntax_hl&.docinfo? :footer
410
440
 
411
441
  lines << '</body>
412
442
  </html>'
@@ -424,11 +454,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
424
454
  def convert_section node
425
455
  if add_chapter(node).nil?
426
456
  hlevel = node.level
427
- epub_type_attr = node.special ? %( epub:type="#{node.sectname}") : ''
457
+ epub_type_attr = node.sectname != 'section' ? %( epub:type="#{node.sectname}") : ''
428
458
  div_classes = [%(sect#{node.level}), node.role].compact
429
459
  title = get_numbered_title node
430
- title_sanitized = xml_sanitize title
431
- %(<section class="#{div_classes * ' '}" title="#{title_sanitized}"#{epub_type_attr}>
460
+ %(<section class="#{div_classes * ' '}" title=#{title.encode xml: :attr}#{epub_type_attr}>
432
461
  <h#{hlevel} id="#{node.id}">#{title}</h#{hlevel}>#{(content = node.content).empty? ? '' : %(
433
462
  #{content})}
434
463
  </section>)
@@ -511,14 +540,12 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
511
540
  type = node.attr 'name'
512
541
  epub_type = case type
513
542
  when 'tip'
514
- 'help'
515
- when 'note'
516
- 'note'
517
- when 'important', 'warning', 'caution'
518
- 'warning'
543
+ 'tip'
544
+ when 'important', 'warning', 'caution', 'note'
545
+ 'notice'
519
546
  else
520
547
  logger.warn %(unknown admonition type: #{type})
521
- 'note'
548
+ 'notice'
522
549
  end
523
550
  %(<aside#{id_attr} class="admonition #{type}"#{title_attr} epub:type="#{epub_type}">
524
551
  #{title_el}<div class="content">
@@ -545,10 +572,12 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
545
572
  end
546
573
 
547
574
  def convert_listing node
575
+ id_attribute = node.id ? %( id="#{node.id}") : ''
548
576
  nowrap = (node.option? 'nowrap') || !(node.document.attr? 'prewrap')
549
577
  if node.style == 'source'
550
578
  lang = node.attr 'language'
551
- if self.class.supports_highlighter_docinfo? && (syntax_hl = node.document.syntax_highlighter)
579
+ syntax_hl = node.document.syntax_highlighter
580
+ if syntax_hl
552
581
  opts = syntax_hl.highlight? ? {
553
582
  css_mode: ((doc_attrs = node.document.attributes)[%(#{syntax_hl.name}-css)] || :class).to_sym,
554
583
  style: doc_attrs[%(#{syntax_hl.name}-style)],
@@ -565,18 +594,42 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
565
594
  end
566
595
  figure_classes = ['listing']
567
596
  figure_classes << 'coalesce' if node.option? 'unbreakable'
568
- title_div = node.title? ? %(<figcaption>#{get_numbered_title node}</figcaption>) : ''
569
- %(<figure class="#{figure_classes * ' '}">#{title_div}
597
+ title_div = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
598
+ %(<figure#{id_attribute} class="#{figure_classes * ' '}">#{title_div}
570
599
  #{syntax_hl ? (syntax_hl.format node, lang, opts) : pre_open + (node.content || '') + pre_close}
571
600
  </figure>)
572
601
  end
573
602
 
574
- # TODO: implement proper stem support. See https://github.com/asciidoctor/asciidoctor-epub3/issues/10
575
- alias convert_stem convert_listing
603
+ def convert_stem node
604
+ return convert_listing node if node.style != 'asciimath' || !asciimath_available?
605
+
606
+ id_attr = node.id ? %( id="#{node.id}") : ''
607
+ title_element = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
608
+ equation_data = AsciiMath.parse(node.content).to_mathml 'mml:'
609
+
610
+ %(<figure#{id_attr} class="#{prepend_space node.role}">
611
+ #{title_element}
612
+ <div class="content">
613
+ #{equation_data}
614
+ </div>
615
+ </figure>)
616
+ end
617
+
618
+ def asciimath_available?
619
+ (@asciimath_status ||= load_asciimath) == :loaded
620
+ end
621
+
622
+ def load_asciimath
623
+ Helpers.require_library('asciimath', true, :warn).nil? ? :unavailable : :loaded
624
+ end
576
625
 
577
- # QUESTION should we wrap the <pre> in either <div> or <figure>?
578
626
  def convert_literal node
579
- %(<pre class="screen">#{node.content}</pre>)
627
+ id_attribute = node.id ? %( id="#{node.id}") : ''
628
+ title_element = node.title? ? %(<figcaption>#{node.captioned_title}</figcaption>) : ''
629
+ %(<figure#{id_attribute} class="literalblock#{prepend_space node.role}">
630
+ #{title_element}
631
+ <div class="content"><pre class="screen">#{node.content}</pre></div>
632
+ </figure>)
580
633
  end
581
634
 
582
635
  def convert_page_break _node
@@ -658,41 +711,34 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
658
711
  lines = [%(<div class="table">)]
659
712
  lines << %(<div class="content">)
660
713
  table_id_attr = node.id ? %( id="#{node.id}") : ''
661
- frame_class = {
662
- 'all' => 'table-framed',
663
- 'topbot' => 'table-framed-topbot',
664
- 'sides' => 'table-framed-sides',
665
- 'none' => '',
666
- }
667
- grid_class = {
668
- 'all' => 'table-grid',
669
- 'rows' => 'table-grid-rows',
670
- 'cols' => 'table-grid-cols',
671
- 'none' => '',
672
- }
673
- table_classes = %W[table #{frame_class[node.attr 'frame'] || frame_class['topbot']} #{grid_class[node.attr 'grid'] || grid_class['rows']}]
714
+ table_classes = [
715
+ 'table',
716
+ %(table-framed-#{node.attr 'frame', 'rows', 'table-frame'}),
717
+ %(table-grid-#{node.attr 'grid', 'rows', 'table-grid'}),
718
+ ]
674
719
  if (role = node.role)
675
720
  table_classes << role
676
721
  end
677
- table_class_attr = %( class="#{table_classes * ' '}")
678
722
  table_styles = []
679
- table_styles << %(width: #{node.attr 'tablepcwidth'}%) unless (node.option? 'autowidth') && !(node.attr? 'width', nil, false)
723
+ if (autowidth = node.option? 'autowidth') && !(node.attr? 'width')
724
+ table_classes << 'fit-content'
725
+ else
726
+ table_styles << %(width: #{node.attr 'tablepcwidth'}%;)
727
+ end
728
+ table_class_attr = %( class="#{table_classes * ' '}")
680
729
  table_style_attr = !table_styles.empty? ? %( style="#{table_styles * '; '}") : ''
681
730
 
682
731
  lines << %(<table#{table_id_attr}#{table_class_attr}#{table_style_attr}>)
683
732
  lines << %(<caption>#{node.captioned_title}</caption>) if node.title?
684
733
  if (node.attr 'rowcount') > 0
685
734
  lines << '<colgroup>'
686
- #if node.option? 'autowidth'
687
- tag = %(<col/>)
688
- node.columns.size.times do
689
- lines << tag
735
+ if autowidth
736
+ lines += (Array.new node.columns.size, %(<col/>))
737
+ else
738
+ node.columns.each do |col|
739
+ lines << ((col.option? 'autowidth') ? %(<col/>) : %(<col style="width: #{col.attr 'colpcwidth'}%;" />))
740
+ end
690
741
  end
691
- #else
692
- # node.columns.each do |col|
693
- # lines << %(<col style="width: #{col.attr 'colpcwidth'}%"/>)
694
- # end
695
- #end
696
742
  lines << '</colgroup>'
697
743
  [:head, :body, :foot].reject {|tsec| node.rows[tsec].empty? }.each do |tsec|
698
744
  lines << %(<t#{tsec}>)
@@ -712,19 +758,16 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
712
758
  else
713
759
  cell_content = ''
714
760
  cell.content.each do |text|
715
- cell_content = %(#{cell_content}<p>#{text}</p>)
761
+ cell_content = %(#{cell_content}<p class="tableblock">#{text}</p>)
716
762
  end
717
763
  end
718
764
  end
719
765
 
720
766
  cell_tag_name = tsec == :head || cell.style == :header ? 'th' : 'td'
721
- cell_classes = []
722
- if (halign = cell.attr 'halign') && halign != 'left'
723
- cell_classes << 'halign-left'
724
- end
725
- if (halign = cell.attr 'valign') && halign != 'top'
726
- cell_classes << 'valign-top'
727
- end
767
+ cell_classes = [
768
+ "halign-#{cell.attr 'halign'}",
769
+ "valign-#{cell.attr 'valign'}",
770
+ ]
728
771
  cell_class_attr = !cell_classes.empty? ? %( class="#{cell_classes * ' '}") : ''
729
772
  cell_colspan_attr = cell.colspan ? %( colspan="#{cell.colspan}") : ''
730
773
  cell_rowspan_attr = cell.rowspan ? %( rowspan="#{cell.rowspan}") : ''
@@ -757,16 +800,30 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
757
800
  # TODO: add complex class if list has nested blocks
758
801
  def convert_dlist node
759
802
  lines = []
803
+ id_attribute = node.id ? %( id="#{node.id}") : ''
804
+
805
+ classes = case node.style
806
+ when 'horizontal'
807
+ ['hdlist', node.role]
808
+ when 'itemized', 'ordered'
809
+ # QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
810
+ ['dlist', %(#{node.style}-list), node.role]
811
+ else
812
+ ['description-list']
813
+ end.compact
814
+
815
+ class_attribute = %( class="#{classes.join ' '}")
816
+
817
+ lines << %(<div#{id_attribute}#{class_attribute}>)
818
+ lines << %(<div class="title">#{node.title}</div>) if node.title?
819
+
760
820
  case (style = node.style)
761
821
  when 'itemized', 'ordered'
762
822
  list_tag_name = style == 'itemized' ? 'ul' : 'ol'
763
823
  role = node.role
764
824
  subject_stop = node.attr 'subject-stop', (role && (node.has_role? 'stack') ? nil : ':')
765
- # QUESTION should we just use itemized-list and ordered-list as the class here? or just list?
766
- div_classes = [%(#{style}-list), role].compact
767
825
  list_class_attr = (node.option? 'brief') ? ' class="brief"' : ''
768
- lines << %(<div class="#{div_classes * ' '}">
769
- <#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
826
+ lines << %(<#{list_tag_name}#{list_class_attr}#{list_tag_name == 'ol' && (node.option? 'reversed') ? ' reversed="reversed"' : ''}>)
770
827
  node.items.each do |subjects, dd|
771
828
  # consists of one term (a subject) and supporting content
772
829
  subject = [*subjects].first.text
@@ -782,11 +839,40 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
782
839
  end
783
840
  lines << '</li>'
784
841
  end
785
- lines << %(</#{list_tag_name}>
786
- </div>)
842
+ lines << %(</#{list_tag_name}>)
843
+ when 'horizontal'
844
+ lines << '<table>'
845
+ if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
846
+ lines << '<colgroup>'
847
+ col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : ''
848
+ lines << %(<col#{col_style_attribute} />)
849
+ col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : ''
850
+ lines << %(<col#{col_style_attribute} />)
851
+ lines << '</colgroup>'
852
+ end
853
+ node.items.each do |terms, dd|
854
+ lines << '<tr>'
855
+ lines << %(<td class="hdlist1#{(node.option? 'strong') ? ' strong' : ''}">)
856
+ first_term = true
857
+ terms.each do |dt|
858
+ lines << %(<br />) unless first_term
859
+ lines << '<p>'
860
+ lines << dt.text
861
+ lines << '</p>'
862
+ first_term = nil
863
+ end
864
+ lines << '</td>'
865
+ lines << '<td class="hdlist2">'
866
+ if dd
867
+ lines << %(<p>#{dd.text}</p>) if dd.text?
868
+ lines << dd.content if dd.blocks?
869
+ end
870
+ lines << '</td>'
871
+ lines << '</tr>'
872
+ end
873
+ lines << '</table>'
787
874
  else
788
- lines << '<div class="description-list">
789
- <dl>'
875
+ lines << '<dl>'
790
876
  node.items.each do |terms, dd|
791
877
  [*terms].each do |dt|
792
878
  lines << %(<dt>
@@ -803,9 +889,10 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
803
889
  end
804
890
  lines << '</dd>'
805
891
  end
806
- lines << '</dl>
807
- </div>'
892
+ lines << '</dl>'
808
893
  end
894
+
895
+ lines << '</div>'
809
896
  lines * LF
810
897
  end
811
898
 
@@ -879,48 +966,129 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
879
966
  document
880
967
  end
881
968
 
882
- def register_image node, target
883
- if target.end_with? '.svg'
969
+ def register_media_file node, target, media_type
970
+ if target.end_with?('.svg') || target.start_with?('data:image/svg+xml')
884
971
  chapter = get_enclosing_chapter node
885
972
  chapter.set_attr 'epub-properties', [] unless chapter.attr? 'epub-properties'
886
973
  epub_properties = chapter.attr 'epub-properties'
887
974
  epub_properties << 'svg' unless epub_properties.include? 'svg'
888
975
  end
889
976
 
890
- out_dir = node.attr('outdir', nil, true) || doc_option(node.document, :to_dir)
891
- fs_path = (::File.join out_dir, target)
892
- unless ::File.exist? fs_path
893
- base_dir = root_document(node.document).base_dir
894
- fs_path = ::File.join base_dir, target
977
+ return if target.start_with? 'data:'
978
+
979
+ if Asciidoctor::Helpers.uriish? target
980
+ # We need to add both local and remote media files to manifest
981
+ fs_path = nil
982
+ else
983
+ out_dir = node.attr('outdir', nil, true) || doc_option(node.document, :to_dir)
984
+ fs_path = (::File.join out_dir, target)
985
+ unless ::File.exist? fs_path
986
+ base_dir = root_document(node.document).base_dir
987
+ fs_path = ::File.join base_dir, target
988
+ end
895
989
  end
896
990
  # We need *both* virtual and physical image paths. Unfortunately, references[:images] only has one of them.
897
- @images << { name: target, path: fs_path }
991
+ @media_files[target] ||= { path: fs_path, media_type: media_type }
898
992
  end
899
993
 
900
994
  def resolve_image_attrs node
901
995
  img_attrs = []
902
996
  img_attrs << %(alt="#{node.attr 'alt'}") if node.attr? 'alt'
903
997
 
904
- width = node.attr 'scaledwidth'
905
- width = node.attr 'width' if width.nil?
906
-
907
998
  # Unlike browsers, Calibre/Kindle *do* scale image if only height is specified
908
999
  # So, in order to match browser behavior, we just always omit height
909
- img_attrs << %(width="#{width}") unless width.nil?
1000
+
1001
+ if (scaledwidth = node.attr 'scaledwidth')
1002
+ img_attrs << %(style="width: #{scaledwidth}")
1003
+ elsif (width = node.attr 'width')
1004
+ # HTML5 spec (and EPUBCheck) only allows pixels in width, but browsers also accept percents
1005
+ # and there are multiple AsciiDoc files in the wild that have width=percents%
1006
+ # 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
1012
+ end
910
1013
 
911
1014
  img_attrs
912
1015
  end
913
1016
 
1017
+ def convert_audio node
1018
+ id_attr = node.id ? %( id="#{node.id}") : ''
1019
+ target = node.media_uri node.attr 'target'
1020
+ register_media_file node, target, 'audio'
1021
+ title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
1022
+
1023
+ autoplay_attr = (node.option? 'autoplay') ? ' autoplay="autoplay"' : ''
1024
+ controls_attr = (node.option? 'nocontrols') ? '' : ' controls="controls"'
1025
+ loop_attr = (node.option? 'loop') ? ' loop="loop"' : ''
1026
+
1027
+ start_t = node.attr 'start'
1028
+ 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
1034
+
1035
+ %(<figure#{id_attr} class="audioblock#{prepend_space node.role}">#{title_element}
1036
+ <div class="content">
1037
+ <audio src="#{target}#{time_anchor}"#{autoplay_attr}#{controls_attr}#{loop_attr}>
1038
+ <div>Your Reading System does not support (this) audio.</div>
1039
+ </audio>
1040
+ </div>
1041
+ </figure>)
1042
+ end
1043
+
1044
+ # TODO: Support multiple video files in different formats for a single video
1045
+ def convert_video node
1046
+ id_attr = node.id ? %( id="#{node.id}") : ''
1047
+ target = node.media_uri node.attr 'target'
1048
+ register_media_file node, target, 'video'
1049
+ title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
1050
+
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"' : ''
1056
+
1057
+ start_t = node.attr 'start'
1058
+ 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
1064
+
1065
+ if (poster = node.attr 'poster').nil_or_empty?
1066
+ poster_attr = ''
1067
+ else
1068
+ poster = node.media_uri poster
1069
+ register_media_file node, poster, 'image'
1070
+ poster_attr = %( poster="#{poster}")
1071
+ end
1072
+
1073
+ %(<figure#{id_attr} class="video#{prepend_space node.role}">#{title_element}
1074
+ <div class="content">
1075
+ <video src="#{target}#{time_anchor}"#{width_attr}#{height_attr}#{autoplay_attr}#{poster_attr}#{controls_attr}#{loop_attr}>
1076
+ <div>Your Reading System does not support (this) video.</div>
1077
+ </video>
1078
+ </div>
1079
+ </figure>)
1080
+ end
1081
+
914
1082
  def convert_image node
915
1083
  target = node.image_uri node.attr 'target'
916
- register_image node, target
1084
+ register_media_file node, target, 'image'
917
1085
  id_attr = node.id ? %( id="#{node.id}") : ''
1086
+ title_element = node.title? ? %(\n<figcaption>#{node.captioned_title}</figcaption>) : ''
918
1087
  img_attrs = resolve_image_attrs node
919
1088
  %(<figure#{id_attr} class="image#{prepend_space node.role}">
920
1089
  <div class="content">
921
1090
  <img src="#{target}"#{prepend_space img_attrs * ' '} />
922
- </div>#{node.title? ? %(
923
- <figcaption>#{node.captioned_title}</figcaption>) : ''}
1091
+ </div>#{title_element}
924
1092
  </figure>)
925
1093
  end
926
1094
 
@@ -1016,7 +1184,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
1016
1184
 
1017
1185
  def convert_inline_image node
1018
1186
  if node.type == 'icon'
1019
- @icon_names << (icon_name = node.target)
1187
+ icon_names << (icon_name = node.target)
1020
1188
  i_classes = ['icon', %(i-#{icon_name})]
1021
1189
  i_classes << %(icon-#{node.attr 'size'}) if node.attr? 'size'
1022
1190
  i_classes << %(icon-flip-#{(node.attr 'flip')[0]}) if node.attr? 'flip'
@@ -1025,7 +1193,7 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
1025
1193
  %(<i class="#{i_classes * ' '}"></i>)
1026
1194
  else
1027
1195
  target = node.image_uri node.target
1028
- register_image node, target
1196
+ register_media_file node, target, 'image'
1029
1197
 
1030
1198
  img_attrs = resolve_image_attrs node
1031
1199
  img_attrs << %(class="inline#{prepend_space node.role}")
@@ -1063,25 +1231,30 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
1063
1231
  def convert_inline_quoted node
1064
1232
  open, close, tag = QUOTE_TAGS[node.type]
1065
1233
 
1066
- # TODO: implement proper stem support. See https://github.com/asciidoctor/asciidoctor-epub3/issues/10
1234
+ if node.type == :asciimath && asciimath_available?
1235
+ content = AsciiMath.parse(node.text).to_mathml 'mml:'
1236
+ else
1237
+ content = node.text
1238
+ end
1239
+
1067
1240
  node.add_role 'literal' if [:monospaced, :asciimath, :latexmath].include? node.type
1068
1241
 
1069
1242
  if node.id
1070
1243
  class_attr = class_string node
1071
1244
  if tag
1072
- %(#{open.chop} id="#{node.id}"#{class_attr}>#{node.text}#{close})
1245
+ %(#{open.chop} id="#{node.id}"#{class_attr}>#{content}#{close})
1073
1246
  else
1074
- %(<span id="#{node.id}"#{class_attr}>#{open}#{node.text}#{close}</span>)
1247
+ %(<span id="#{node.id}"#{class_attr}>#{open}#{content}#{close}</span>)
1075
1248
  end
1076
1249
  elsif role_valid_class? node.role
1077
1250
  class_attr = class_string node
1078
1251
  if tag
1079
- %(#{open.chop}#{class_attr}>#{node.text}#{close})
1252
+ %(#{open.chop}#{class_attr}>#{content}#{close})
1080
1253
  else
1081
- %(<span#{class_attr}>#{open}#{node.text}#{close}</span>)
1254
+ %(<span#{class_attr}>#{open}#{content}#{close}</span>)
1082
1255
  end
1083
1256
  else
1084
- %(#{open}#{node.text}#{close})
1257
+ %(#{open}#{content}#{close})
1085
1258
  end
1086
1259
  end
1087
1260
 
@@ -1145,6 +1318,19 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
1145
1318
  @book.add_item 'styles/epub3-css3-only.css', content: (postprocess_css_file ::File.join(workdir, 'epub3-css3-only.css'), format)
1146
1319
  end
1147
1320
 
1321
+ syntax_hl = doc.syntax_highlighter
1322
+ if syntax_hl&.write_stylesheet? doc
1323
+ Dir.mktmpdir do |dir|
1324
+ syntax_hl.write_stylesheet doc, dir
1325
+ Pathname.glob(dir + '/**/*').map do |filename|
1326
+ # Workaround for https://github.com/skoji/gepub/pull/117
1327
+ filename.open do |f|
1328
+ @book.add_item filename.basename.to_s, content: f
1329
+ end if filename.file?
1330
+ end
1331
+ end
1332
+ end
1333
+
1148
1334
  font_files, font_css = select_fonts ::File.join(DATA_DIR, 'styles/epub3-fonts.css'), (doc.attr 'scripts', 'latin')
1149
1335
  @book.add_item 'styles/epub3-fonts.css', content: font_css
1150
1336
  unless font_files.empty?
@@ -1164,38 +1350,41 @@ document.addEventListener('DOMContentLoaded', function(event, reader) {
1164
1350
  nil
1165
1351
  end
1166
1352
 
1167
- def add_cover_image doc
1168
- return if (image_path = doc.attr 'front-cover-image').nil?
1353
+ def add_cover_page doc, name
1354
+ image_attr_name = %(#{name}-image)
1355
+
1356
+ return nil if (image_path = doc.attr image_attr_name).nil?
1169
1357
 
1170
1358
  imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
1171
1359
  imagesdir = (imagesdir == '.' ? '' : %(#{imagesdir}/))
1172
1360
 
1173
1361
  image_attrs = {}
1174
1362
  if (image_path.include? ':') && image_path =~ ImageMacroRx
1175
- logger.warn %(deprecated block macro syntax detected in front-cover-image attribute) if image_path.start_with? 'image::'
1363
+ logger.warn %(deprecated block macro syntax detected in :#{image_attr_name}: attribute) if image_path.start_with? 'image::'
1176
1364
  image_path = %(#{imagesdir}#{$1})
1177
1365
  (::Asciidoctor::AttributeList.new $2).parse_into image_attrs, %w(alt width height) unless $2.empty?
1178
1366
  end
1179
1367
 
1180
- image_href = %(#{imagesdir}jacket/cover#{::File.extname image_path})
1368
+ image_href = %(#{imagesdir}jacket/#{name}#{::File.extname image_path})
1181
1369
 
1182
1370
  workdir = doc.attr 'docdir'
1183
1371
  workdir = '.' if workdir.nil_or_empty?
1184
1372
 
1185
- unless ::File.readable? ::File.join(workdir, image_path)
1186
- logger.error %(#{::File.basename doc.attr('docfile')}: front cover image not found or readable: #{::File.expand_path image_path, workdir})
1187
- return
1373
+ begin
1374
+ @book.add_item(image_href, content: File.join(workdir, image_path)).cover_image
1375
+ rescue => e
1376
+ 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
+ return nil
1188
1378
  end
1189
1379
 
1380
+ return nil if @format == :kf8
1381
+
1190
1382
  unless !image_attrs.empty? && (width = image_attrs['width']) && (height = image_attrs['height'])
1191
1383
  width, height = 1050, 1600
1192
1384
  end
1193
1385
 
1194
- @book.add_item(image_href, content: File.join(workdir, image_path)).cover_image
1195
-
1196
- unless @format == :kf8
1197
- # NOTE SVG wrapper maintains aspect ratio and confines image to view box
1198
- content = %(<!DOCTYPE html>
1386
+ # NOTE SVG wrapper maintains aspect ratio and confines image to view box
1387
+ content = %(<!DOCTYPE html>
1199
1388
  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
1200
1389
  <head>
1201
1390
  <meta charset="UTF-8"/>
@@ -1225,10 +1414,7 @@ body > svg {
1225
1414
  </svg></body>
1226
1415
  </html>).to_ios
1227
1416
 
1228
- # Gitden expects a cover.xhtml, so add it to the spine
1229
- @book.add_ordered_item 'cover.xhtml', content: content, id: 'cover'
1230
- end
1231
- nil
1417
+ @book.add_ordered_item %(#{name}.xhtml), content: content, id: name
1232
1418
  end
1233
1419
 
1234
1420
  def get_frontmatter_files doc, workdir
@@ -1258,19 +1444,22 @@ body > svg {
1258
1444
  workdir = doc.attr 'docdir'
1259
1445
  workdir = '.' if workdir.nil_or_empty?
1260
1446
 
1447
+ result = nil
1261
1448
  get_frontmatter_files(doc, workdir).each do |front_matter|
1262
1449
  front_matter_content = ::File.read front_matter
1263
1450
 
1264
1451
  front_matter_file = File.basename front_matter, '.html'
1265
1452
  item = @book.add_ordered_item "#{front_matter_file}.xhtml", content: (postprocess_xhtml front_matter_content)
1266
1453
  item.add_property 'svg' if SvgImgSniffRx =~ front_matter_content
1454
+ # Store link to first frontmatter page
1455
+ result = item if result.nil?
1267
1456
 
1268
1457
  front_matter_content.scan ImgSrcScanRx do
1269
1458
  @book.add_item $1, content: File.join(File.dirname(front_matter), $1)
1270
1459
  end
1271
1460
  end
1272
1461
 
1273
- nil
1462
+ result
1274
1463
  end
1275
1464
 
1276
1465
  def add_profile_images doc, usernames
@@ -1302,8 +1491,7 @@ body > svg {
1302
1491
  nil
1303
1492
  end
1304
1493
 
1305
- # TODO: aggregate authors of chapters into authors attribute(s) on main document
1306
- def nav_doc doc, items
1494
+ def nav_doc doc, items, landmarks, depth
1307
1495
  lines = [%(<!DOCTYPE html>
1308
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}">
1309
1497
  <head>
@@ -1313,13 +1501,29 @@ body > svg {
1313
1501
  <link rel="stylesheet" type="text/css" href="styles/epub3-css3-only.css" media="(min-device-width: 0px)"/>
1314
1502
  </head>
1315
1503
  <body>
1316
- <h1>#{sanitize_doctitle_xml doc, :pcdata}</h1>
1317
- <nav epub:type="toc" id="toc">
1318
- <h2>#{doc.attr 'toc-title'}</h2>)]
1319
- lines << (nav_level items, [(doc.attr 'toclevels', 1).to_i, 0].max)
1320
- lines << %(</nav>
1504
+ <section class="chapter">
1505
+ <header>
1506
+ <div class="chapter-header"><h1 class="chapter-title"><small class="subtitle">#{doc.attr 'toc-title'}</small></h1></div>
1507
+ </header>
1508
+ <nav epub:type="toc" id="toc">)]
1509
+ lines << (nav_level items, [depth, 0].max)
1510
+ lines << '</nav>'
1511
+
1512
+ unless landmarks.empty?
1513
+ lines << '
1514
+ <nav epub:type="landmarks" id="landmarks" hidden="hidden">
1515
+ <ol>'
1516
+ landmarks.each do |landmark|
1517
+ lines << %(<li><a epub:type="#{landmark[:type]}" href="#{landmark[:href]}">#{landmark[:title]}</a></li>)
1518
+ end
1519
+ lines << '
1520
+ </ol>
1521
+ </nav>'
1522
+ end
1523
+ lines << '
1524
+ </section>
1321
1525
  </body>
1322
- </html>)
1526
+ </html>'
1323
1527
  lines * LF
1324
1528
  end
1325
1529
 
@@ -1353,7 +1557,7 @@ body > svg {
1353
1557
  lines * LF
1354
1558
  end
1355
1559
 
1356
- def ncx_doc doc, items
1560
+ def ncx_doc doc, items, depth
1357
1561
  # TODO: populate docAuthor element based on unique authors in work
1358
1562
  lines = [%(<?xml version="1.0" encoding="utf-8"?>
1359
1563
  <ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="#{doc.attr 'lang', 'en'}">
@@ -1365,7 +1569,7 @@ body > svg {
1365
1569
  </head>
1366
1570
  <docTitle><text>#{sanitize_doctitle_xml doc, :cdata}</text></docTitle>
1367
1571
  <navMap>)]
1368
- lines << (ncx_level items, [(doc.attr 'toclevels', 1).to_i, 0].max, state = {})
1572
+ lines << (ncx_level items, depth, state = {})
1369
1573
  lines[0] = lines[0].sub '%{depth}', %(<meta name="dtb:depth" content="#{state[:max_depth]}"/>)
1370
1574
  lines << %(</navMap>
1371
1575
  </ncx>)
@@ -1443,10 +1647,10 @@ body > svg {
1443
1647
  .to_ios
1444
1648
  end
1445
1649
 
1446
- def get_kindlegen_command kindlegen_path
1447
- unless kindlegen_path.nil?
1448
- logger.debug %(Using ebook-kindlegen-path attribute: #{kindlegen_path})
1449
- return [kindlegen_path]
1650
+ def get_kindlegen_command
1651
+ unless @kindlegen_path.nil?
1652
+ logger.debug %(Using ebook-kindlegen-path attribute: #{@kindlegen_path})
1653
+ return [@kindlegen_path]
1450
1654
  end
1451
1655
 
1452
1656
  unless (result = ENV['KINDLEGEN']).nil?
@@ -1465,11 +1669,11 @@ body > svg {
1465
1669
  end
1466
1670
  end
1467
1671
 
1468
- def distill_epub_to_mobi epub_file, target, compress, kindlegen_path
1672
+ def distill_epub_to_mobi epub_file, target, compress
1469
1673
  mobi_file = ::File.basename target.sub(EpubExtensionRx, '.mobi')
1470
1674
  compress_flag = KindlegenCompression[compress ? (compress.empty? ? '1' : compress.to_s) : '0']
1471
1675
 
1472
- argv = get_kindlegen_command(kindlegen_path) + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
1676
+ argv = get_kindlegen_command + ['-dont_append_source', compress_flag, '-o', mobi_file, epub_file].compact
1473
1677
  begin
1474
1678
  # This duplicates Kindlegen.run, but we want to override executable
1475
1679
  out, err, res = Open3.capture3(*argv) do |r|
@@ -1494,10 +1698,10 @@ body > svg {
1494
1698
  end
1495
1699
  end
1496
1700
 
1497
- def get_epubcheck_command epubcheck_path
1498
- unless epubcheck_path.nil?
1499
- logger.debug %(Using ebook-epubcheck-path attribute: #{epubcheck_path})
1500
- return [epubcheck_path]
1701
+ def get_epubcheck_command
1702
+ unless @epubcheck_path.nil?
1703
+ logger.debug %(Using ebook-epubcheck-path attribute: #{@epubcheck_path})
1704
+ return [@epubcheck_path]
1501
1705
  end
1502
1706
 
1503
1707
  unless (result = ENV['EPUBCHECK']).nil?
@@ -1515,8 +1719,8 @@ body > svg {
1515
1719
  end
1516
1720
  end
1517
1721
 
1518
- def validate_epub epub_file, epubcheck_path
1519
- argv = get_epubcheck_command(epubcheck_path) + ['-w', epub_file]
1722
+ def validate_epub epub_file
1723
+ argv = get_epubcheck_command + ['-w', epub_file]
1520
1724
  begin
1521
1725
  out, err, res = Open3.capture3(*argv)
1522
1726
  rescue Errno::ENOENT => e
@@ -1561,14 +1765,6 @@ body > svg {
1561
1765
  def role_valid_class? role
1562
1766
  role.is_a? String
1563
1767
  end
1564
-
1565
- class << self
1566
- def supports_highlighter_docinfo?
1567
- # Asciidoctor only got pluggable syntax highlighters since 2.0:
1568
- # https://github.com/asciidoctor/asciidoctor/commit/23ddbaed6818025cbe74365fec7e8101f34eadca
1569
- Asciidoctor::Document.method_defined? :syntax_highlighter
1570
- end
1571
- end
1572
1768
  end
1573
1769
 
1574
1770
  class DocumentIdGenerator
@@ -1581,6 +1777,7 @@ body > svg {
1581
1777
  InvalidIdCharsRx = /[^[:word:]]+/
1582
1778
  LeadingDigitRx = /^[[:digit:]]/
1583
1779
  end
1780
+
1584
1781
  class << self
1585
1782
  def generate_id doc, pre = nil, sep = nil
1586
1783
  synthetic = false
@@ -1636,7 +1833,10 @@ body > svg {
1636
1833
  # TODO: bw theme for CodeRay
1637
1834
  document.set_attribute 'pygments-style', 'bw' unless document.attr? 'pygments-style'
1638
1835
  document.set_attribute 'rouge-style', 'bw' unless document.attr? 'rouge-style'
1639
- unless Converter.supports_highlighter_docinfo?
1836
+
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?
1640
1840
  document.set_attribute 'coderay-css', 'style'
1641
1841
  document.set_attribute 'pygments-css', 'style'
1642
1842
  document.set_attribute 'rouge-css', 'style'