# frozen_string_literal: true require 'asciidoctor' require 'asciidoctor/converter' require 'fb2rb' module Asciidoctor module FB2 # Converts AsciiDoc documents to FB2 e-book formats class Converter < Asciidoctor::Converter::Base # rubocop:disable Metrics/ClassLength include ::Asciidoctor::Writer CSV_DELIMITER_REGEX = /\s*,\s*/.freeze register_for 'fb2' # @return [FB2rb::Book] attr_reader(:book) def initialize(backend, opts = {}) super outfilesuffix '.fb2.zip' end # @param node [Asciidoctor::Document] def convert_document(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength @book = FB2rb::Book.new 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( author.firstname, author.middlename, author.lastname, nil, [], 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(series_name, series_volume) end date = node.attr('revdate') || node.attr('docdate') fb2date = FB2rb::FB2Date.new(date, Date.parse(date)) title_info.date = document_info.date = fb2date 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 = %(
<p>#{node.doctitle}</p> #{node.content}
) @book.bodies << FB2rb::Body.new(nil, body) if node.document.footnotes notes = [] node.document.footnotes.each do |footnote| notes << %(
<p>#{footnote.index}</p>

#{footnote.text}

) end @book.bodies << FB2rb::Body.new('notes', notes * "\n") end @book end # @param node [Asciidoctor::Section] def convert_preamble(node) mark_last_paragraph(node) node.content end # @param node [Asciidoctor::Section] def convert_section(node) mark_last_paragraph(node) if node.parent == node.document && node.document.doctype == 'book' %(
<p>#{node.title}</p> #{node.content}
) else %(#{node.title} #{node.content}) end end # @param node [Asciidoctor::Block] def convert_paragraph(node) lines = [ '

', node.content, '

' ] lines << '' unless node.has_role?('last') lines * "\n" end # @param node [Asciidoctor::Block] def convert_listing(node) lines = [] node.content.split("\n").each do |line| lines << %(

#{line}

) end lines << '' unless node.has_role?('last') lines * "\n" end (QUOTE_TAGS = { # rubocop:disable Style/MutableConstant monospaced: ['', ''], emphasis: ['', ''], strong: ['', ''], double: ['“', '”'], single: ['‘', '’'], superscript: ['', ''], subscript: ['', ''], asciimath: ['', ''], latexmath: ['', ''] }).default = ['', ''] # @param node [Asciidoctor::Inline] def convert_inline_quoted(node) open, close = QUOTE_TAGS[node.type] %(#{open}#{node.text}#{close}) end # @param node [Asciidoctor::Inline] def convert_inline_anchor(node) # rubocop:disable Metrics/MethodLength case node.type when :xref %(#{node.text}) when :link %(#{node.text}) when :ref %() when :bibref unless (reftext = node.reftext) reftext = %([#{node.id}]) end %(#{reftext}) else logger.warn %(unknown anchor type: #{node.type.inspect}) nil end end # @param node [Asciidoctor::Inline] def convert_inline_footnote(node) index = node.attr('index') %([#{index}]) end # @param node [Asciidoctor::Inline] def convert_inline_image(node) image_attrs = resolve_image_attrs(node, node.target) %() end # @param node [Asciidoctor::Block] def convert_image(node) image_attrs = resolve_image_attrs(node, node.attr('target')) image_attrs << %(title="#{node.captioned_title}") if node.title? image_attrs << %(id="#{node.id}") if node.id %(

) end # @param doc [Asciidoctor::Document] # @return [Asciidoctor::Document] def root_document(doc) doc = doc.parent_document until doc.parent_document.nil? doc end # @param node [Asciidoctor::AbstractNode] # @param target [String] def resolve_image_attrs(node, target) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength target = node.image_uri(target) 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) @book.add_binary(target, fs_path) target = %(##{target}) end end image_attrs = [%(l:href="#{target}")] image_attrs << %(alt="#{node.attr('alt')}") if node.attr? 'alt' end # @param node [Asciidoctor::Block] def convert_admonition(node) %(

#{node.title || node.caption}: #{node.content}

) end # @param node [Asciidoctor::List] def convert_ulist(node) lines = [] node.items.each do |item| lines << %(

• #{item.text}

) lines << %(

#{item.content}

) if item.blocks? end lines << '' unless node.has_role?('last') lines * "\n" end # @param node [Asciidoctor::List] def convert_olist(node) lines = [] node.items.each_with_index do |item, index| lines << %(

#{index + 1}. #{item.text}

) lines << %(

#{item.content}

) if item.blocks? end lines << '' unless node.has_role?('last') lines * "\n" end # @param node [Asciidoctor::List] def convert_dlist(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity lines = [''] node.items.each do |terms, dd| lines << '' lines << '' lines << '' lines << '' end lines << '
' first_term = true terms.each do |dt| lines << %() unless first_term lines << '

' lines << '' if node.option?('strong') lines << dt.text lines << '' if node.option?('strong') lines << '

' first_term = false end lines << '
' if dd lines << %(

#{dd.text}

) if dd.text? lines << dd.content if dd.blocks? end lines << '
' lines << '' unless node.has_role?('last') lines * "\n" end # @param node [Asciidoctor::Table] def convert_table(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity lines = [''] node.rows.to_h.each do |tsec, rows| # rubocop:disable Metrics/BlockLength next if rows.empty? rows.each do |row| lines << '' row.each do |cell| cell_content = if tsec == :head cell.text else case cell.style when :asciidoc cell.content when :literal %(

#{cell.text}

) else (cell_content = cell.content).empty? ? '' : %(

#{cell_content.join "

\n

"}

) end end 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}) end lines << '' end end lines << '
' lines << '' unless node.has_role?('last') lines * "\n" end # @param root [Asciidoctor::AbstractNode] 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 # @param output [FB2rb::Book] def write(output, target) output.write(target) end end end end