# frozen_string_literal: true
module Asciidoctor
# A built-in {Converter} implementation that generates DocBook 5 output. The output is inspired by the output produced
# by the docbook45 backend from AsciiDoc.py, except it has been migrated to the DocBook 5 specification.
class Converter::DocBook5Converter < Converter::Base
register_for 'docbook5'
# default represents variablelist
(DLIST_TAGS = {
'qanda' => { list: 'qandaset', entry: 'qandaentry', label: 'question', term: 'simpara', item: 'answer' },
'glossary' => { list: nil, entry: 'glossentry', term: 'glossterm', item: 'glossdef' },
}).default = { list: 'variablelist', entry: 'varlistentry', term: 'term', item: 'listitem' }
(QUOTE_TAGS = {
monospaced: ['', ''],
emphasis: ['', '', true],
strong: ['', '', true],
double: ['', '', true],
single: ['', '', true],
mark: ['', ''],
superscript: ['', ''],
subscript: ['', ''],
}).default = ['', '', true]
MANPAGE_SECTION_TAGS = { 'section' => 'refsection', 'synopsis' => 'refsynopsisdiv' }
TABLE_PI_NAMES = ['dbhtml', 'dbfo', 'dblatex']
CopyrightRx = /^(#{CC_ANY}+?)(?: ((?:\d{4}-)?\d{4}))?$/
ImageMacroRx = /^image::?(\S|\S#{CC_ANY}*?\S)\[(#{CC_ANY}+)?\]$/
def initialize backend, opts = {}
@backend = backend
init_backend_traits basebackend: 'docbook', filetype: 'xml', outfilesuffix: '.xml', supports_templates: true
end
def convert_document node
result = ['']
result << ((node.attr? 'toclevels') ? %() : '') if node.attr? 'toc'
result << ((node.attr? 'sectnumlevels') ? %() : '') if node.attr? 'sectnums'
lang_attribute = (node.attr? 'nolang') ? '' : %( xml:lang="#{node.attr 'lang', 'en'}")
if (root_tag_name = node.doctype) == 'manpage'
manpage = true
root_tag_name = 'article'
end
root_tag_idx = result.size
id = node.id
result << (document_info_tag node) unless node.noheader
if manpage
result << ''
result << ''
result << %(#{node.apply_reftext_subs node.attr 'mantitle'}) if node.attr? 'mantitle'
result << %(#{node.attr 'manvolnum'}) if node.attr? 'manvolnum'
result << %(#{node.attr 'mansource', ' '})
result << %(#{node.attr 'manmanual', ' '})
result << ''
result << ''
result += (node.attr 'mannames').map {|n| %(#{n}) } if node.attr? 'mannames'
result << %(#{node.attr 'manpurpose'}) if node.attr? 'manpurpose'
result << ''
end
unless (docinfo_content = node.docinfo :header).empty?
result << docinfo_content
end
result << node.content if node.blocks?
unless (docinfo_content = node.docinfo :footer).empty?
result << docinfo_content
end
result << '' if manpage
id, node.id = node.id, nil unless id
# defer adding root tag in case document ID is auto-generated on demand
result.insert root_tag_idx, %(<#{root_tag_name} xmlns="http://docbook.org/ns/docbook" xmlns:xl="http://www.w3.org/1999/xlink" version="5.0"#{lang_attribute}#{common_attributes id}>)
result << %(#{root_tag_name}>)
result.join LF
end
alias convert_embedded content_only
def convert_section node
if node.document.doctype == 'manpage'
tag_name = MANPAGE_SECTION_TAGS[tag_name = node.sectname] || tag_name
else
tag_name = node.sectname
end
title_el = node.special && ((node.option? 'notitle') || (node.option? 'untitled')) ? '' : %(
#{node.title}\n)
%(<#{tag_name}#{common_attributes node.id, node.role, node.reftext}>
#{title_el}#{node.content}
#{tag_name}>)
end
def convert_admonition node
%(<#{tag_name = node.attr 'name'}#{common_attributes node.id, node.role, node.reftext}>
#{title_tag node}#{enclose_content node}
#{tag_name}>)
end
alias convert_audio skip
def convert_colist node
result = []
result << %()
result << %(#{node.title}) if node.title?
node.items.each do |item|
result << %()
result << %(#{item.text})
result << item.content if item.blocks?
result << ''
end
result << %()
result.join LF
end
def convert_dlist node
result = []
if node.style == 'horizontal'
result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext} tabstyle="horizontal" frame="none" colsep="0" rowsep="0">
#{title_tag node}
)
node.items.each do |terms, dd|
result << %()
terms.each {|dt| result << %(#{dt.text}) }
result << %()
if dd
result << %(#{dd.text}) if dd.text?
result << dd.content if dd.blocks?
end
result << %()
end
result << %(
#{tag_name}>)
else
tags = DLIST_TAGS[node.style]
list_tag = tags[:list]
entry_tag = tags[:entry]
label_tag = tags[:label]
term_tag = tags[:term]
item_tag = tags[:item]
if list_tag
result << %(<#{list_tag}#{common_attributes node.id, node.role, node.reftext}>)
result << %(#{node.title}) if node.title?
end
node.items.each do |terms, dd|
result << %(<#{entry_tag}>)
result << %(<#{label_tag}>) if label_tag
terms.each {|dt| result << %(<#{term_tag}>#{dt.text}#{term_tag}>) }
result << %(#{label_tag}>) if label_tag
result << %(<#{item_tag}>)
if dd
result << %(#{dd.text}) if dd.text?
result << dd.content if dd.blocks?
end
result << %(#{item_tag}>)
result << %(#{entry_tag}>)
end
result << %(#{list_tag}>) if list_tag
end
result.join LF
end
def convert_example node
if node.title?
%(#{node.title}
#{enclose_content node}
)
else
%(
#{enclose_content node}
)
end
end
def convert_floating_title node
%(#{node.title})
end
def convert_image node
# NOTE according to the DocBook spec, content area, scaling, and scaling to fit are mutually exclusive
# See http://tdg.docbook.org/tdg/4.5/imagedata-x.html#d0e79635
if node.attr? 'scaledwidth'
width_attribute = %( width="#{node.attr 'scaledwidth'}")
depth_attribute = ''
scale_attribute = ''
elsif node.attr? 'scale'
# QUESTION should we set the viewport using width and depth? (the scaled image would be contained within this box)
#width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : ''
#depth_attribute = (node.attr? 'height') ? %( depth="#{node.attr 'height'}") : ''
scale_attribute = %( scale="#{node.attr 'scale'}")
else
width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : ''
depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : ''
scale_attribute = ''
end
align_attribute = (node.attr? 'align') ? %( align="#{node.attr 'align'}") : ''
mediaobject = %(#{node.alt})
if node.title?
%()
else
%(
#{mediaobject}
)
end
end
def convert_listing node
informal = !node.title?
common_attrs = common_attributes node.id, node.role, node.reftext
if node.style == 'source'
if (attrs = node.attributes).key? 'linenums'
numbering_attrs = (attrs.key? 'start') ? %( linenumbering="numbered" startinglinenumber="#{attrs['start'].to_i}") : ' linenumbering="numbered"'
else
numbering_attrs = ' linenumbering="unnumbered"'
end
if attrs.key? 'language'
wrapped_content = %(#{node.content})
else
wrapped_content = %(#{node.content})
end
else
wrapped_content = %(#{node.content})
end
informal ? wrapped_content : %(#{node.title}
#{wrapped_content}
)
end
def convert_literal node
if node.title?
%(#{node.title}#{node.content})
else
%(#{node.content})
end
end
alias convert_pass content_only
def convert_stem node
if (idx = node.subs.index :specialcharacters)
node.subs.delete_at idx
equation = node.content
idx > 0 ? (node.subs.insert idx, :specialcharacters) : (node.subs.unshift :specialcharacters)
else
equation = node.content
end
if node.style == 'asciimath'
# NOTE fop requires jeuclid to process mathml markup
equation_data = asciimath_available? ? ((::AsciiMath.parse equation).to_mathml 'mml:', 'xmlns:mml' => 'http://www.w3.org/1998/Math/MathML') : %()
else
# unhandled math; pass source to alt and required mathphrase element; dblatex will process alt as LaTeX math
equation_data = %()
end
if node.title?
%(#{node.title}
#{equation_data}
)
else
# WARNING dblatex displays the element inline instead of block as documented (except w/ mathml)
%(
#{equation_data}
)
end
end
def convert_olist node
result = []
num_attribute = node.style ? %( numeration="#{node.style}") : ''
start_attribute = (node.attr? 'start') ? %( startingnumber="#{node.attr 'start'}") : ''
result << %()
result << %(#{node.title}) if node.title?
node.items.each do |item|
result << %()
result << %(#{item.text})
result << item.content if item.blocks?
result << ''
end
result << %()
result.join LF
end
def convert_open node
case node.style
when 'abstract'
if node.parent == node.document && node.document.doctype == 'book'
logger.warn 'abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
''
else
%(
#{title_tag node}#{enclose_content node}
)
end
when 'partintro'
if node.level == 0 && node.parent.context == :section && node.document.doctype == 'book'
%(
#{title_tag node}#{enclose_content node}
)
else
logger.error 'partintro block can only be used when doctype is book and must be a child of a book part. Excluding block content.'
''
end
else
reftext = node.reftext if (id = node.id)
role = node.role
if node.title?
%(#{node.title}#{content_spacer = node.content_model == :compound ? LF : ''}#{node.content}#{content_spacer})
elsif id || role
if node.content_model == :compound
%(
#{node.content}
)
else
%(#{node.content})
end
else
enclose_content node
end
end
end
def convert_page_break node
''
end
def convert_paragraph node
if node.title?
%(#{node.title}#{node.content})
else
%(#{node.content})
end
end
def convert_preamble node
if node.document.doctype == 'book'
%(
#{title_tag node, false}#{node.content}
)
else
node.content
end
end
def convert_quote node
blockquote_tag(node, (node.has_role? 'epigraph') && 'epigraph') { enclose_content node }
end
def convert_thematic_break node
''
end
def convert_sidebar node
%(
#{title_tag node}#{enclose_content node}
)
end
def convert_table node
has_body = false
result = []
pgwide_attribute = (node.option? 'pgwide') ? ' pgwide="1"' : ''
frame = 'topbot' if (frame = node.attr 'frame', 'all', 'table-frame') == 'ends'
grid = node.attr 'grid', nil, 'table-grid'
result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext}#{pgwide_attribute} frame="#{frame}" rowsep="#{['none', 'cols'].include?(grid) ? 0 : 1}" colsep="#{['none', 'rows'].include?(grid) ? 0 : 1}"#{(node.attr? 'orientation', 'landscape', 'table-orientation') ? ' orient="land"' : ''}>)
if node.option? 'unbreakable'
result << ''
elsif node.option? 'breakable'
result << ''
end
result << %(#{node.title}) if tag_name == 'table'
if (width = (node.attr? 'width') ? (node.attr 'width') : nil)
TABLE_PI_NAMES.each do |pi_name|
result << %(#{pi_name} table-width="#{width}"?>)
end
col_width_key = 'colabswidth'
else
col_width_key = 'colpcwidth'
end
result << %()
node.columns.each do |col|
result << %()
end
node.rows.to_h.each do |tsec, rows|
next if rows.empty?
has_body = true if tsec == :body
result << %()
rows.each do |row|
result << ''
row.each do |cell|
colspan_attribute = cell.colspan ? %( namest="col_#{colnum = cell.column.attr 'colnumber'}" nameend="col_#{colnum + cell.colspan - 1}") : ''
rowspan_attribute = cell.rowspan ? %( morerows="#{cell.rowspan - 1}") : ''
# NOTE may not have whitespace (e.g., line breaks) as a direct descendant according to DocBook rules
entry_start = %()
if tsec == :head
cell_content = cell.text
else
case cell.style
when :asciidoc
cell_content = cell.content
when :literal
cell_content = %(#{cell.text})
when :header
cell_content = (cell_content = cell.content).empty? ? '' : %(#{cell_content.join ''})
else
cell_content = (cell_content = cell.content).empty? ? '' : %(#{cell_content.join ''})
end
end
entry_end = (node.document.attr? 'cellbgcolor') ? %() : ''
result << %(#{entry_start}#{cell_content}#{entry_end})
end
result << ''
end
result << %()
end
result << ''
result << %(#{tag_name}>)
logger.warn 'tables must have at least one body row' unless has_body
result.join LF
end
alias convert_toc skip
def convert_ulist node
result = []
if node.style == 'bibliography'
result << %()
result << %(#{node.title}) if node.title?
node.items.each do |item|
result << ''
result << %(#{item.text})
result << item.content if item.blocks?
result << ''
end
result << ''
else
mark_type = (checklist = node.option? 'checklist') ? 'none' : node.style
mark_attribute = mark_type ? %( mark="#{mark_type}") : ''
result << %()
result << %(#{node.title}) if node.title?
node.items.each do |item|
text_marker = (item.attr? 'checked') ? '✓ ' : '❏ ' if checklist && (item.attr? 'checkbox')
result << %()
result << %(#{text_marker || ''}#{item.text})
result << item.content if item.blocks?
result << ''
end
result << ''
end
result.join LF
end
def convert_verse node
blockquote_tag(node, (node.has_role? 'epigraph') && 'epigraph') { %(#{node.content}) }
end
alias convert_video skip
def convert_inline_anchor node
case node.type
when :ref
%()
when :xref
if (path = node.attributes['path'])
%(#{node.text || path})
else
if (linkend = node.attributes['refid']).nil_or_empty?
root_doc = get_root_document node
# Q: should we warn instead of generating a document ID on demand?
linkend = (root_doc.id ||= generate_document_id root_doc)
end
# NOTE the xref tag in DocBook does not support explicit link text, so the link tag must be used instead
# The section at http://www.sagehill.net/docbookxsl/CrossRefs.html#IdrefLinks gives an explanation for this choice
# "link - a cross reference where you supply the text of the reference as the content of the link element."
(text = node.text) ? %(#{text}) : %()
end
when :link
%(#{node.text})
when :bibref
%(#{text})
else
logger.warn %(unknown anchor type: #{node.type.inspect})
nil
end
end
def convert_inline_break node
%(#{node.text})
end
def convert_inline_button node
%(#{node.text})
end
def convert_inline_callout node
%()
end
def convert_inline_footnote node
if node.type == :xref
%()
else
%(#{node.text})
end
end
def convert_inline_image node
width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : ''
depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : ''
%(#{node.alt})
end
def convert_inline_indexterm node
if (see = node.attr 'see')
rel = %(\n#{see})
elsif (see_also_list = node.attr 'see-also')
rel = see_also_list.map {|see_also| %(\n#{see_also}) }.join
else
rel = ''
end
if node.type == :visible
%(#{node.text}#{rel}
#{node.text})
elsif (numterms = (terms = node.attr 'terms').size) > 2
%(#{terms[0]}#{terms[1]}#{terms[2]}#{rel}
#{(node.document.option? 'indexterm-promotion') ? %[
#{terms[1]}#{terms[2]}#{terms[2]}] : ''})
elsif numterms > 1
%(#{terms[0]}#{terms[1]}#{rel}
#{(node.document.option? 'indexterm-promotion') ? %[
#{terms[1]}] : ''})
else
%(#{terms[0]}#{rel}
)
end
end
def convert_inline_kbd node
if (keys = node.attr 'keys').size == 1
%(#{keys[0]})
else
%(#{keys.join ''})
end
end
def convert_inline_menu node
menu = node.attr 'menu'
if (submenus = node.attr 'submenus').empty?
if (menuitem = node.attr 'menuitem')
%(#{menu}#{menuitem})
else
%(#{menu})
end
else
%(#{menu}#{submenus.join ''}#{node.attr 'menuitem'})
end
end
def convert_inline_quoted node
if (type = node.type) == :asciimath
# NOTE fop requires jeuclid to process mathml markup
asciimath_available? ? %(#{(::AsciiMath.parse node.text).to_mathml 'mml:', 'xmlns:mml' => 'http://www.w3.org/1998/Math/MathML'}) : %()
elsif type == :latexmath
# unhandled math; pass source to alt and required mathphrase element; dblatex will process alt as LaTeX math
%()
else
open, close, supports_phrase = QUOTE_TAGS[type]
text = node.text
if node.role
if supports_phrase
quoted_text = %(#{open}#{text}#{close})
else
quoted_text = %(#{open.chop} role="#{node.role}">#{text}#{close})
end
else
quoted_text = %(#{open}#{text}#{close})
end
node.id ? %(#{quoted_text}) : quoted_text
end
end
private
def common_attributes id, role = nil, reftext = nil
if id
attrs = %( xml:id="#{id}"#{role ? %[ role="#{role}"] : ''})
elsif role
attrs = %( role="#{role}")
else
attrs = ''
end
if reftext
if (reftext.include? '<') && ((reftext = reftext.gsub XmlSanitizeRx, '').include? ' ')
reftext = (reftext.squeeze ' ').strip
end
reftext = reftext.gsub '"', '"' if reftext.include? '"'
%(#{attrs} xreflabel="#{reftext}")
else
attrs
end
end
def author_tag doc, author
result = []
result << ''
result << ''
result << %(#{doc.sub_replacements author.firstname}) if author.firstname
result << %(#{doc.sub_replacements author.middlename}) if author.middlename
result << %(#{doc.sub_replacements author.lastname}) if author.lastname
result << ''
result << %(#{author.email}) if author.email
result << ''
result.join LF
end
def document_info_tag doc
result = ['']
unless doc.notitle
if (title = doc.doctitle partition: true, use_fallback: true).subtitle?
result << %(#{title.main}#{title.subtitle})
else
result << %(#{title})
end
end
if (date = (doc.attr? 'revdate') ? (doc.attr 'revdate') : ((doc.attr? 'reproducible') ? nil : (doc.attr 'docdate')))
result << %(#{date})
end
if doc.attr? 'copyright'
CopyrightRx =~ (doc.attr 'copyright')
result << ''
result << %(#{$1})
result << %(#{$2}) if $2
result << ''
end
if doc.header?
unless (authors = doc.authors).empty?
if authors.size > 1
result << ''
authors.each {|author| result << (author_tag doc, author) }
result << ''
else
result << (author_tag doc, (author = authors[0]))
result << %(#{author.initials}) if author.initials
end
end
if (doc.attr? 'revdate') && ((doc.attr? 'revnumber') || (doc.attr? 'revremark'))
result << %()
result << %(#{doc.attr 'revnumber'}) if doc.attr? 'revnumber'
result << %(#{doc.attr 'revdate'}) if doc.attr? 'revdate'
result << %(#{doc.attr 'authorinitials'}) if doc.attr? 'authorinitials'
result << %(#{doc.attr 'revremark'}) if doc.attr? 'revremark'
result << %()
end
if (doc.attr? 'front-cover-image') || (doc.attr? 'back-cover-image')
if (back_cover_tag = cover_tag doc, 'back')
result << (cover_tag doc, 'front', true)
result << back_cover_tag
elsif (front_cover_tag = cover_tag doc, 'front')
result << front_cover_tag
end
end
result << %(#{doc.attr 'orgname'}) if doc.attr? 'orgname'
unless (docinfo_content = doc.docinfo).empty?
result << docinfo_content
end
end
result << ''
result.join LF
end
def get_root_document node
while (node = node.document).nested?
node = node.parent_document
end
node
end
def generate_document_id doc
%(__#{doc.doctype}-root__)
end
# FIXME this should be handled through a template mechanism
def enclose_content node
node.content_model == :compound ? node.content : %(#{node.content})
end
def title_tag node, optional = true
!optional || node.title? ? %(#{node.title}\n) : ''
end
def cover_tag doc, face, use_placeholder = false
if (cover_image = doc.attr %(#{face}-cover-image))
width_attr = ''
depth_attr = ''
if (cover_image.include? ':') && ImageMacroRx =~ cover_image
attrlist = $2
cover_image = doc.image_uri $1
if attrlist
attrs = (AttributeList.new attrlist).parse ['alt', 'width', 'height']
if attrs.key? 'scaledwidth'
# NOTE scalefit="1" is the default in this case
width_attr = %( width="#{attrs['scaledwidth']}")
else
width_attr = %( contentwidth="#{attrs['width']}") if attrs.key? 'width'
depth_attr = %( contentdepth="#{attrs['height']}") if attrs.key? 'height'
end
end
end
%()
elsif use_placeholder
%()
end
end
def blockquote_tag node, tag_name = nil
if tag_name
start_tag, end_tag = %(<#{tag_name}), %(#{tag_name}>)
else
start_tag, end_tag = '
'
end
result = [%(#{start_tag}#{common_attributes node.id, node.role, node.reftext}>)]
result << %(#{node.title}) if node.title?
if (node.attr? 'attribution') || (node.attr? 'citetitle')
result << ''
result << (node.attr 'attribution') if node.attr? 'attribution'
result << %(#{node.attr 'citetitle'}) if node.attr? 'citetitle'
result << ''
end
result << yield
result << end_tag
result.join LF
end
def asciimath_available?
(@asciimath_status ||= load_asciimath) == :loaded
end
def load_asciimath
(defined? ::AsciiMath.parse) ? :loaded : (Helpers.require_library 'asciimath', true, :warn).nil? ? :unavailable : :loaded
end
end
end