class Asciidoctor::FB2::Converter

Converts AsciiDoc documents to FB2 e-book formats

Constants

CSV_DELIMITER_REGEX
IMAGE_ATTRIBUTE_VALUE_RX
QUOTE_TAGS

Attributes

book[R]

@return [FB2rb::Book]

Public Class Methods

new(backend, opts = {}) click to toggle source
Calls superclass method
# File lib/asciidoctor_fb2.rb, line 24
def initialize(backend, opts = {})
  super
  outfilesuffix '.fb2.zip'
end

Public Instance Methods

convert_admonition(node) click to toggle source

@param node [Asciidoctor::Block]

# File lib/asciidoctor_fb2.rb, line 356
      def convert_admonition(node)
        lines = [%(<p><strong>#{node.title || node.caption}:</strong>
#{node.content}
</p>)]
        lines << '<empty-line/>' unless node.has_role?('last')
        lines * "\n"
      end
convert_colist(node) click to toggle source

@param node [Asciidoctor::List]

# File lib/asciidoctor_fb2.rb, line 299
def convert_colist(node)
  convert_olist(node)
end
convert_dlist(node) click to toggle source

@param node [Asciidoctor::List]

# File lib/asciidoctor_fb2.rb, line 402
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) click to toggle source

@param node [Asciidoctor::Document]

# 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) click to toggle source

@param node [Asciidoctor::Block]

# 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) click to toggle source

@param node [Asciidoctor::Block]

# File lib/asciidoctor_fb2.rb, line 132
def convert_floating_title(node)
  %(<subtitle id="#{node.id}">#{node.title}</subtitle>)
end
convert_image(node) click to toggle source

@param node [Asciidoctor::Block]

# File lib/asciidoctor_fb2.rb, line 304
def convert_image(node)
  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><image #{image_attrs * ' '}/></p>)
end
convert_inline_anchor(node) click to toggle source

@param node [Asciidoctor::Inline]

# 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) click to toggle source

@param node [Asciidoctor::Inline]

# File lib/asciidoctor_fb2.rb, line 234
def convert_inline_break(node)
  node.text
end
convert_inline_button(node) click to toggle source

@param node [Asciidoctor::Inline]

# File lib/asciidoctor_fb2.rb, line 247
def convert_inline_button(node)
  %([<strong>#{node.text}</strong>])
end
convert_inline_callout(node) click to toggle source

@param node [Asciidoctor::Inline]

# File lib/asciidoctor_fb2.rb, line 294
def convert_inline_callout(node)
  %(<strong>(#{node.text})</strong>)
end
convert_inline_footnote(node) click to toggle source

@param node [Asciidoctor::Inline]

# 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) click to toggle source

@param node [Asciidoctor::Inline]

# 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 * ' '}/>)
end
convert_inline_indexterm(node) click to toggle source

@param node [Asciidoctor::Inline]

# File lib/asciidoctor_fb2.rb, line 289
def convert_inline_indexterm(node)
  node.type == :visible ? node.text : ''
end
convert_inline_kbd(node) click to toggle source

@param node [Asciidoctor::Inline]

# File lib/asciidoctor_fb2.rb, line 252
def convert_inline_kbd(node)
  %(<strong>#{node.attr('keys') * '</strong>+<strong>'}</strong>)
end
convert_inline_menu(node) click to toggle source

@param node [Asciidoctor::Inline]

# 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) click to toggle source

@param node [Asciidoctor::Inline]

# 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) click to toggle source

@param node [Asciidoctor::Block]

# File lib/asciidoctor_fb2.rb, line 187
def convert_listing(node)
  convert_literal(node)
end
convert_literal(node) click to toggle source

@param node [Asciidoctor::Block]

# 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) click to toggle source

@param node [Asciidoctor::List]

# File lib/asciidoctor_fb2.rb, line 388
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) click to toggle source

@param node [Asciidoctor::Block]

# File lib/asciidoctor_fb2.rb, line 312
def convert_open(node)
  convert_paragraph(node)
end
convert_page_break(_node) click to toggle source

@param _node [Asciidoctor::Block]

# File lib/asciidoctor_fb2.rb, line 122
def convert_page_break(_node)
  ''
end
convert_paragraph(node) click to toggle source

@param node [Asciidoctor::Block]

# 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) click to toggle source

@param node [Asciidoctor::Section]

# File lib/asciidoctor_fb2.rb, line 97
def convert_preamble(node)
  mark_last_paragraph(node)
  node.content
end
convert_quote(node) click to toggle source

@param node [Asciidoctor::Block]

# 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) click to toggle source

@param node [Asciidoctor::Section]

# 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) click to toggle source

@param node [Asciidoctor::Block]

# File lib/asciidoctor_fb2.rb, line 365
      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) click to toggle source

@param node [Asciidoctor::Block]

# File lib/asciidoctor_fb2.rb, line 127
def convert_stem(node)
  %(<p><code>#{node.content}</code></p>)
end
convert_table(node) click to toggle source

@param node [Asciidoctor::Table]

# File lib/asciidoctor_fb2.rb, line 450
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 * ' '}>#{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) click to toggle source

@param _node [Asciidoctor::Block]

# File lib/asciidoctor_fb2.rb, line 137
def convert_thematic_break(_node)
  ''
end
convert_toc(_node) click to toggle source

@param _node [Asciidoctor::Block]

# File lib/asciidoctor_fb2.rb, line 117
def convert_toc(_node)
  ''
end
convert_ulist(node) click to toggle source

@param node [Asciidoctor::List]

# File lib/asciidoctor_fb2.rb, line 372
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) click to toggle source

@param node [Asciidoctor::Block]

# 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) click to toggle source
# File lib/asciidoctor_fb2.rb, line 323
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) click to toggle source

@param cell [Asciidoctor::Table::Cell]

# File lib/asciidoctor_fb2.rb, line 432
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) click to toggle source

@param root [Asciidoctor::AbstractNode]

# File lib/asciidoctor_fb2.rb, line 479
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) click to toggle source

@param node [Asciidoctor::AbstractNode] @param target [String]

# File lib/asciidoctor_fb2.rb, line 331
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.sub!('/', '_')
      target.sub!('\\', '_')

      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) click to toggle source

@param doc [Asciidoctor::Document] @return [Asciidoctor::Document]

# File lib/asciidoctor_fb2.rb, line 318
def root_document(doc)
  doc = doc.parent_document until doc.parent_document.nil?
  doc
end
write(output, target) click to toggle source

@param output [FB2rb::Book]

# File lib/asciidoctor_fb2.rb, line 488
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