asciidoctor-epub3 1.5.0.alpha.12 → 1.5.0.alpha.17

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