Class: Asciidoctor::FB2::Converter

Inherits:
Converter::Base
  • Object
show all
Includes:
Writer
Defined in:
lib/asciidoctor_fb2.rb

Overview

Converts AsciiDoc documents to FB2 e-book formats

Constant Summary collapse

CSV_DELIMITER_REGEX =
/\s*,\s*/.freeze
IMAGE_ATTRIBUTE_VALUE_RX =
/^image:{1,2}(.*?)\[(.*?)\]$/.freeze
ADMONITION_ICONS =
{
  'caution' => '🔥',
  'important' => '',
  'note' => 'ℹ️',
  'tip' => '💡',
  'warning' => '⚠️'
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(backend, opts = {}) ⇒ Converter

Returns a new instance of Converter.



24
25
26
27
# File 'lib/asciidoctor_fb2.rb', line 24

def initialize(backend, opts = {})
  super
  outfilesuffix '.fb2.zip'
end

Instance Attribute Details

#bookFB2rb::Book (readonly)

Returns:

  • (FB2rb::Book)


22
23
24
# File 'lib/asciidoctor_fb2.rb', line 22

def book
  @book
end

Instance Method Details

#convert_admonition(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/asciidoctor_fb2.rb', line 371

def convert_admonition(node)
  lines = ['<p>']

  lines << if node.document.attr?('icons', 'font') && (icon = ADMONITION_ICONS[node.attr 'name'])
             %(#{icon} )
           else
             %(<strong>#{node.title || node.caption}:</strong>)
           end

  lines << node.content
  lines << '</p>'
  lines << '<empty-line/>' unless node.has_role?('last')
  lines * "\n"
end

#convert_colist(node) ⇒ Object

Parameters:

  • node (Asciidoctor::List)


299
300
301
# File 'lib/asciidoctor_fb2.rb', line 299

def convert_colist(node)
  convert_olist(node)
end

#convert_dlist(node) ⇒ Object

Parameters:

  • node (Asciidoctor::List)


424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/asciidoctor_fb2.rb', line 424

def convert_dlist(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
  lines = ['<table>']
  node.items.each do |terms, dd|
    lines << '<tr>'
    lines << '<td>'
    first_term = true
    terms.each do |dt|
      lines << %(<empty-line/>) unless first_term
      lines << '<p>'
      lines << '<strong>' if node.option?('strong')
      lines << dt.text
      lines << '</strong>' if node.option?('strong')
      lines << '</p>'
      first_term = false
    end
    lines << '</td>'
    lines << '<td>'
    if dd
      lines << %(<p>#{dd.text}</p>) if dd.text?
      lines << dd.content if dd.blocks?
    end
    lines << '</td>'
    lines << '</tr>'
  end
  lines << '</table>'
  lines << '<empty-line/>' unless node.has_role?('last')
  lines * "\n"
end

#convert_document(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Document)


30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/asciidoctor_fb2.rb', line 30

def convert_document(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
  @book = FB2rb::Book.new
  @book.add_stylesheet('text/css', File.join(DATA_DIR, 'fb2.css'))

  document_info = @book.description.document_info
  title_info = @book.description.title_info

  title_info.book_title = node.doctitle
  title_info.lang = node.attr('lang', 'en')
  (node.attr 'keywords', '').split(CSV_DELIMITER_REGEX).each do |s|
    title_info.keywords << s
  end
  (node.attr 'genres', '').split(CSV_DELIMITER_REGEX).each do |s|
    title_info.genres << s
  end
  node.authors.each do |author|
    title_info.authors << FB2rb::Author.new(
      first_name: author.firstname,
      middle_name: author.middlename,
      last_name: author.lastname,
      emails: author.email.nil? ? [] : [author.email]
    )
  end

  if node.attr? 'series-name'
    series_name = node.attr 'series-name'
    series_volume = node.attr 'series-volume', 1
    title_info.sequences << FB2rb::Sequence.new(name: series_name, number: series_volume)
  end

  date = node.attr('revdate') || node.attr('docdate')
  fb2date = FB2rb::FB2Date.new(display_value: date, value: Date.parse(date))
  title_info.date = document_info.date = fb2date

  unless (cover_image = node.attr('front-cover-image')).nil?
    cover_image = Regexp.last_match(1) if cover_image =~ IMAGE_ATTRIBUTE_VALUE_RX
    cover_image_path = node.image_uri(cover_image)
    register_binary(node, cover_image_path, 'image')
    title_info.coverpage = FB2rb::Coverpage.new(images: [%(##{cover_image_path})])
  end

  document_info.id = node.attr('uuid', '')
  document_info.version = node.attr('revnumber')
  document_info.program_used = %(Asciidoctor FB2 #{VERSION} using Asciidoctor #{node.attr('asciidoctor-version')})

  publisher = node.attr('publisher')
  document_info.publishers << publisher if publisher

  body = %(<section>
<title><p>#{node.doctitle}</p></title>
#{node.content}
</section>)
  @book.bodies << FB2rb::Body.new(content: body)
  unless node.document.footnotes.empty?
    notes = []
    node.document.footnotes.each do |footnote|
      notes << %(<section id="note-#{footnote.index}">
<title><p>#{footnote.index}</p></title>
<p>#{footnote.text}</p>
</section>)
    end
    @book.bodies << FB2rb::Body.new(name: 'notes', content: notes * "\n")
  end
  @book
end

#convert_example(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


239
240
241
242
243
244
# File 'lib/asciidoctor_fb2.rb', line 239

def convert_example(node)
  lines = []
  lines << %(<p><strong>#{node.title}:</strong></p>) if node.title?
  lines << node.content
  lines * "\n"
end

#convert_floating_title(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


132
133
134
# File 'lib/asciidoctor_fb2.rb', line 132

def convert_floating_title(node)
  %(<subtitle id="#{node.id}">#{node.title}</subtitle>)
end

#convert_image(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/asciidoctor_fb2.rb', line 304

def convert_image(node) # rubocop:disable Metrics/AbcSize
  image_attrs = register_binary(node, node.image_uri(node.attr('target')), 'image')
  image_attrs << %(title="#{node.captioned_title}") if node.title?
  image_attrs << %(id="#{node.id}") if node.id

  p_style = []
  p_style << %(float: #{node.attr 'float'}) if node.attr? 'float'
  p_style << %(text-align: #{node.attr 'align'}) if node.attr? 'align'

  p_attrs = []
  p_attrs << %(style="#{p_style.sort! * '; '}") unless p_style.empty?
  %(<p #{p_attrs.sort! * ' '}><image #{image_attrs.sort! * ' '}/></p>)
end

#convert_inline_anchor(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Inline)


257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/asciidoctor_fb2.rb', line 257

def convert_inline_anchor(node) # rubocop:disable Metrics/MethodLength
  case node.type
  when :xref
    %(<a l:href="#{node.target}">#{node.text}</a>)
  when :link
    %(<a l:href="#{node.target}">#{node.text}</a>)
  when :ref
    %(<a id="#{node.id}"></a>)
  when :bibref
    unless (reftext = node.reftext)
      reftext = %([#{node.id}])
    end
    %(<a id="#{node.id}"></a>#{reftext})
  else
    logger.warn %(unknown anchor type: #{node.type.inspect})
    nil
  end
end

#convert_inline_break(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Inline)


234
235
236
# File 'lib/asciidoctor_fb2.rb', line 234

def convert_inline_break(node)
  node.text
end

#convert_inline_button(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Inline)


247
248
249
# File 'lib/asciidoctor_fb2.rb', line 247

def convert_inline_button(node)
  %([<strong>#{node.text}</strong>])
end

#convert_inline_callout(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Inline)


294
295
296
# File 'lib/asciidoctor_fb2.rb', line 294

def convert_inline_callout(node)
  %(<strong>(#{node.text})</strong>)
end

#convert_inline_footnote(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Inline)


277
278
279
280
# File 'lib/asciidoctor_fb2.rb', line 277

def convert_inline_footnote(node)
  index = node.attr('index')
  %(<a l:href="#note-#{index}" type="note">[#{index}]</a>)
end

#convert_inline_image(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Inline)


283
284
285
286
# File 'lib/asciidoctor_fb2.rb', line 283

def convert_inline_image(node)
  image_attrs = register_binary(node, node.image_uri(node.target), 'image')
  %(<image #{image_attrs.sort! * ' '}/>)
end

#convert_inline_indexterm(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Inline)


289
290
291
# File 'lib/asciidoctor_fb2.rb', line 289

def convert_inline_indexterm(node)
  node.type == :visible ? node.text : ''
end

#convert_inline_kbd(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Inline)


252
253
254
# File 'lib/asciidoctor_fb2.rb', line 252

def convert_inline_kbd(node)
  %(<strong>#{node.attr('keys') * '</strong>+<strong>'}</strong>)
end

#convert_inline_menu(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Inline)


220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/asciidoctor_fb2.rb', line 220

def convert_inline_menu(node)
  caret = '&#160;<strong>&#8250;</strong> '
  menu = node.attr('menu')
  menuitem = node.attr('menuitem')
  submenus = node.attr('submenus') * %(</b>#{caret}<b>)

  result = %(<strong>#{menu}</strong>)
  result += %(#{caret}<strong>#{submenus}</strong>) unless submenus.nil_or_empty?
  result += %(#{caret}<strong>#{menuitem}</strong>) unless menuitem.nil_or_empty?

  result
end

#convert_inline_quoted(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Inline)


214
215
216
217
# File 'lib/asciidoctor_fb2.rb', line 214

def convert_inline_quoted(node)
  open, close = QUOTE_TAGS[node.type]
  %(#{open}#{node.text}#{close})
end

#convert_listing(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


187
188
189
# File 'lib/asciidoctor_fb2.rb', line 187

def convert_listing(node)
  convert_literal(node)
end

#convert_literal(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


192
193
194
195
196
197
198
199
# File 'lib/asciidoctor_fb2.rb', line 192

def convert_literal(node)
  lines = []
  node.content.split("\n").each do |line|
    lines << %(<p><code>#{line}</code></p>)
  end
  lines << '<empty-line/>' unless node.has_role?('last')
  lines * "\n"
end

#convert_olist(node) ⇒ Object

Parameters:

  • node (Asciidoctor::List)


410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/asciidoctor_fb2.rb', line 410

def convert_olist(node) # rubocop:disable Metrics/AbcSize
  lines = []
  @stack ||= []
  node.items.each_with_index do |item, index|
    @stack << %(#{index + 1}.)
    lines << %(<p>#{@stack * ' '} #{item.text}</p>)
    lines << %(<p>#{item.content}</p>) if item.blocks?
    @stack.pop
  end
  lines << '<empty-line/>' unless node.has_role?('last') || !@stack.empty?
  lines * "\n"
end

#convert_open(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


319
320
321
# File 'lib/asciidoctor_fb2.rb', line 319

def convert_open(node)
  convert_paragraph(node)
end

#convert_page_break(_node) ⇒ Object

Parameters:

  • _node (Asciidoctor::Block)


122
123
124
# File 'lib/asciidoctor_fb2.rb', line 122

def convert_page_break(_node)
  ''
end

#convert_paragraph(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


142
143
144
145
146
147
148
149
150
# File 'lib/asciidoctor_fb2.rb', line 142

def convert_paragraph(node)
  lines = [
    '<p>',
    node.content,
    '</p>'
  ]
  lines << '<empty-line/>' unless node.has_role?('last')
  lines * "\n"
end

#convert_preamble(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Section)


97
98
99
100
# File 'lib/asciidoctor_fb2.rb', line 97

def convert_preamble(node)
  mark_last_paragraph(node)
  node.content
end

#convert_quote(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/asciidoctor_fb2.rb', line 153

def convert_quote(node)
  citetitle = node.attr('citetitle')
  citetitle_tag = citetitle.nil_or_empty? ? '' : %(<subtitle>#{citetitle}</subtitle>)

  author = node.attr('attribution')
  author_tag = author.nil_or_empty? ? '' : %(<text-author>#{node.attr('attribution')}</text-author>)

  %(<cite>
#{citetitle_tag}
<p>#{node.content}</p>
#{author_tag}
</cite>)
end

#convert_section(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Section)


103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/asciidoctor_fb2.rb', line 103

def convert_section(node)
  mark_last_paragraph(node)
  if node.parent == node.document && node.document.doctype == 'book'
    %(<section id="#{node.id}">
<title><p>#{node.title}</p></title>
#{node.content}
</section>)
  else
    %(<subtitle id="#{node.id}">#{node.title}</subtitle>
#{node.content})
  end
end

#convert_sidebar(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


387
388
389
390
391
# File 'lib/asciidoctor_fb2.rb', line 387

def convert_sidebar(node)
  title_tag = node.title.nil_or_empty? ? '' : %(<p><strong>#{node.title}</strong></p>)
  %(#{title_tag}
#{node.content})
end

#convert_stem(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


127
128
129
# File 'lib/asciidoctor_fb2.rb', line 127

def convert_stem(node)
  %(<p><code>#{node.content}</code></p>)
end

#convert_table(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Table)


472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'lib/asciidoctor_fb2.rb', line 472

def convert_table(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
  lines = []
  lines << %(<subtitle>#{node.captioned_title}</subtitle>) if node.title?
  lines << '<table>'
  node.rows.to_h.each do |tsec, rows|
    next if rows.empty?

    rows.each do |row|
      lines << '<tr>'
      row.each do |cell|
        cell_content = get_cell_content(cell)
        cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')
        cell_attrs = [
          %(halign="#{cell.attr 'halign'}"),
          %(valign="#{cell.attr 'valign'}")
        ]
        cell_attrs << %(colspan="#{cell.colspan}") if cell.colspan
        cell_attrs << %(rowspan="#{cell.rowspan}") if cell.rowspan
        lines << %(<#{cell_tag_name} #{cell_attrs.sort! * ' '}>#{cell_content}</#{cell_tag_name}>)
      end
      lines << '</tr>'
    end
  end
  lines << '</table>'
  lines << '<empty-line/>' unless node.has_role?('last')
  lines * "\n"
end

#convert_thematic_break(_node) ⇒ Object

Parameters:

  • _node (Asciidoctor::Block)


137
138
139
# File 'lib/asciidoctor_fb2.rb', line 137

def convert_thematic_break(_node)
  ''
end

#convert_toc(_node) ⇒ Object

Parameters:

  • _node (Asciidoctor::Block)


117
118
119
# File 'lib/asciidoctor_fb2.rb', line 117

def convert_toc(_node)
  ''
end

#convert_ulist(node) ⇒ Object

Parameters:

  • node (Asciidoctor::List)


394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/asciidoctor_fb2.rb', line 394

def convert_ulist(node)
  lines = []
  @stack ||= []

  node.items.each do |item|
    @stack << ''
    lines << %(<p>#{@stack * ' '} #{item.text}</p>)
    lines << %(<p>#{item.content}</p>) if item.blocks?
    @stack.pop
  end

  lines << '<empty-line/>' unless node.has_role?('last') || !@stack.empty?
  lines * "\n"
end

#convert_verse(node) ⇒ Object

Parameters:

  • node (Asciidoctor::Block)


168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/asciidoctor_fb2.rb', line 168

def convert_verse(node)
  body = node.content&.split("\n\n")&.map do |stanza|
    %(<stanza>\n<v>#{stanza.split("\n") * "</v>\n<v>"}</v>\n</stanza>)
  end&.join("\n")

  citetitle = node.attr('citetitle')
  citetitle_tag = citetitle.nil_or_empty? ? '' : %(<title>#{citetitle}</title>)

  author = node.attr('attribution')
  author_tag = author.nil_or_empty? ? '' : %(<text-author>#{node.attr('attribution')}</text-author>)

  %(<poem>
#{citetitle_tag}
#{body}
#{author_tag}
</poem>)
end

#determine_mime_type(filename, media_type) ⇒ Object



330
331
332
333
334
# File 'lib/asciidoctor_fb2.rb', line 330

def determine_mime_type(filename, media_type)
  mime_types = MIME::Types.type_for(filename)
  mime_types.delete_if { |x| x.media_type != media_type }
  mime_types.empty? ? nil : mime_types[0].content_type
end

#get_cell_content(cell) ⇒ Object

Parameters:

  • cell (Asciidoctor::Table::Cell)


454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/asciidoctor_fb2.rb', line 454

def get_cell_content(cell) # rubocop:disable Metrics/MethodLength
  case cell.style
  when :asciidoc
    cell.content
  when :emphasis
    %(<emphasis>#{cell.text}</emphasis>)
  when :literal
    %(<code>#{cell.text}</code>)
  when :monospaced
    %(<code>#{cell.text}</code>)
  when :strong
    %(<strong>#{cell.text}</strong>)
  else
    cell.text
  end
end

#mark_last_paragraph(root) ⇒ Object

Parameters:

  • root (Asciidoctor::AbstractNode)


501
502
503
504
505
506
507
# File 'lib/asciidoctor_fb2.rb', line 501

def mark_last_paragraph(root)
  return unless (last_block = root.blocks[-1])

  last_block = last_block.blocks[-1] while last_block.context == :section && last_block.blocks?
  last_block.add_role('last') if last_block.context == :paragraph
  nil
end

#register_binary(node, target, media_type) ⇒ Object

Parameters:

  • node (Asciidoctor::AbstractNode)
  • target (String)


338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/asciidoctor_fb2.rb', line 338

def register_binary(node, target, media_type) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
  unless Asciidoctor::Helpers.uriish?(target)
    out_dir = node.attr('outdir', nil, true) || doc_option(node.document, :to_dir)
    fs_path = File.join(out_dir, target)
    unless File.readable?(fs_path)
      base_dir = root_document(node.document).base_dir
      fs_path = File.join(base_dir, target)
    end

    if File.readable?(fs_path)
      # Calibre fails to load images if they contain path separators
      target.gsub!('/', '_')
      target.gsub!('\\', '_')

      mime_type = determine_mime_type(target, media_type)
      @book.add_binary(target, fs_path, mime_type)
      target = %(##{target})
    end
  end

  image_attrs = [%(l:href="#{target}")]
  image_attrs << %(alt="#{node.attr('alt')}") if node.attr? 'alt'
end

#root_document(doc) ⇒ Asciidoctor::Document

Parameters:

  • doc (Asciidoctor::Document)

Returns:

  • (Asciidoctor::Document)


325
326
327
328
# File 'lib/asciidoctor_fb2.rb', line 325

def root_document(doc)
  doc = doc.parent_document until doc.parent_document.nil?
  doc
end

#write(output, target) ⇒ Object

Parameters:

  • output (FB2rb::Book)


510
511
512
513
514
515
516
# File 'lib/asciidoctor_fb2.rb', line 510

def write(output, target)
  if target.respond_to?(:end_with?) && target.end_with?('.zip')
    output.write_compressed(target)
  else
    output.write_uncompressed(target)
  end
end