"Fossies" - the Fresh Open Source Software Archive

Member "asciidoctor-2.0.10/lib/asciidoctor/converter/docbook5.rb" (1 Jun 2019, 29527 Bytes) of package /linux/www/asciidoctor-2.0.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "docbook5.rb": 2.0.7_vs_2.0.8.

    1 # frozen_string_literal: true
    2 module Asciidoctor
    3 # A built-in {Converter} implementation that generates DocBook 5 output. The output is inspired by the output produced
    4 # by the docbook45 backend from AsciiDoc Python, except it has been migrated to the DocBook 5 specification.
    5 class Converter::DocBook5Converter < Converter::Base
    6   register_for 'docbook5'
    7 
    8   # default represents variablelist
    9   (DLIST_TAGS = {
   10     'qanda' => { list:  'qandaset', entry: 'qandaentry', label: 'question', term:  'simpara', item:  'answer' },
   11     'glossary' => { list:  nil, entry: 'glossentry', term:  'glossterm', item:  'glossdef' },
   12   }).default = { list:  'variablelist', entry: 'varlistentry', term: 'term', item:  'listitem' }
   13 
   14   (QUOTE_TAGS = {
   15     monospaced:  ['<literal>', '</literal>'],
   16     emphasis:    ['<emphasis>', '</emphasis>', true],
   17     strong:      ['<emphasis role="strong">', '</emphasis>', true],
   18     double:      ['<quote>', '</quote>', true],
   19     single:      ['<quote>', '</quote>', true],
   20     mark:        ['<emphasis role="marked">', '</emphasis>'],
   21     superscript: ['<superscript>', '</superscript>'],
   22     subscript:   ['<subscript>', '</subscript>'],
   23   }).default = ['', '', true]
   24 
   25   MANPAGE_SECTION_TAGS = { 'section' => 'refsection', 'synopsis' => 'refsynopsisdiv' }
   26   TABLE_PI_NAMES = ['dbhtml', 'dbfo', 'dblatex']
   27 
   28   CopyrightRx = /^(#{CC_ANY}+?)(?: ((?:\d{4}\-)?\d{4}))?$/
   29   ImageMacroRx = /^image::?(\S|\S#{CC_ANY}*?\S)\[(#{CC_ANY}+)?\]$/
   30 
   31   def initialize backend, opts = {}
   32     @backend = backend
   33     init_backend_traits basebackend: 'docbook', filetype: 'xml', outfilesuffix: '.xml', supports_templates: true
   34   end
   35 
   36   def convert_document node
   37     result = ['<?xml version="1.0" encoding="UTF-8"?>']
   38     result << ((node.attr? 'toclevels') ? %(<?asciidoc-toc maxdepth="#{node.attr 'toclevels'}"?>) : '<?asciidoc-toc?>') if node.attr? 'toc'
   39     result << ((node.attr? 'sectnumlevels') ? %(<?asciidoc-numbered maxdepth="#{node.attr 'sectnumlevels'}"?>) : '<?asciidoc-numbered?>') if node.attr? 'sectnums'
   40     lang_attribute = (node.attr? 'nolang') ? '' : %( xml:lang="#{node.attr 'lang', 'en'}")
   41     if (root_tag_name = node.doctype) == 'manpage'
   42       root_tag_name = 'refentry'
   43     end
   44     result << %(<#{root_tag_name} xmlns="http://docbook.org/ns/docbook" xmlns:xl="http://www.w3.org/1999/xlink" version="5.0"#{lang_attribute}#{common_attributes node.id}>)
   45     result << (document_info_tag node) unless node.noheader
   46     unless (docinfo_content = node.docinfo :header).empty?
   47       result << docinfo_content
   48     end
   49     result << node.content if node.blocks?
   50     unless (docinfo_content = node.docinfo :footer).empty?
   51       result << docinfo_content
   52     end
   53     result << %(</#{root_tag_name}>)
   54     result.join LF
   55   end
   56 
   57   alias convert_embedded content_only
   58 
   59   def convert_section node
   60     if node.document.doctype == 'manpage'
   61       tag_name = MANPAGE_SECTION_TAGS[tag_name = node.sectname] || tag_name
   62     else
   63       tag_name = node.sectname
   64     end
   65     title_el = node.special && (node.option? 'untitled') ? '' : %(<title>#{node.title}</title>\n)
   66     %(<#{tag_name}#{common_attributes node.id, node.role, node.reftext}>
   67 #{title_el}#{node.content}
   68 </#{tag_name}>)
   69   end
   70 
   71   def convert_admonition node
   72     %(<#{tag_name = node.attr 'name'}#{common_attributes node.id, node.role, node.reftext}>
   73 #{title_tag node}#{enclose_content node}
   74 </#{tag_name}>)
   75   end
   76 
   77   alias convert_audio skip
   78 
   79   def convert_colist node
   80     result = []
   81     result << %(<calloutlist#{common_attributes node.id, node.role, node.reftext}>)
   82     result << %(<title>#{node.title}</title>) if node.title?
   83     node.items.each do |item|
   84       result << %(<callout arearefs="#{item.attr 'coids'}">)
   85       result << %(<para>#{item.text}</para>)
   86       result << item.content if item.blocks?
   87       result << '</callout>'
   88     end
   89     result << %(</calloutlist>)
   90     result.join LF
   91   end
   92 
   93   def convert_dlist node
   94     result = []
   95     if node.style == 'horizontal'
   96       result << %(<#{tag_name = node.title? ? 'table' : 'informaltable'}#{common_attributes node.id, node.role, node.reftext} tabstyle="horizontal" frame="none" colsep="0" rowsep="0">
   97 #{title_tag node}<tgroup cols="2">
   98 <colspec colwidth="#{node.attr 'labelwidth', 15}*"/>
   99 <colspec colwidth="#{node.attr 'itemwidth', 85}*"/>
  100 <tbody valign="top">)
  101       node.items.each do |terms, dd|
  102         result << %(<row>
  103 <entry>)
  104         terms.each {|dt| result << %(<simpara>#{dt.text}</simpara>) }
  105         result << %(</entry>
  106 <entry>)
  107         if dd
  108           result << %(<simpara>#{dd.text}</simpara>) if dd.text?
  109           result << dd.content if dd.blocks?
  110         end
  111         result << %(</entry>
  112 </row>)
  113       end
  114       result << %(</tbody>
  115 </tgroup>
  116 </#{tag_name}>)
  117     else
  118       tags = DLIST_TAGS[node.style]
  119       list_tag = tags[:list]
  120       entry_tag = tags[:entry]
  121       label_tag = tags[:label]
  122       term_tag = tags[:term]
  123       item_tag = tags[:item]
  124       if list_tag
  125         result << %(<#{list_tag}#{common_attributes node.id, node.role, node.reftext}>)
  126         result << %(<title>#{node.title}</title>) if node.title?
  127       end
  128 
  129       node.items.each do |terms, dd|
  130         result << %(<#{entry_tag}>)
  131         result << %(<#{label_tag}>) if label_tag
  132         terms.each {|dt| result << %(<#{term_tag}>#{dt.text}</#{term_tag}>) }
  133         result << %(</#{label_tag}>) if label_tag
  134         result << %(<#{item_tag}>)
  135         if dd
  136           result << %(<simpara>#{dd.text}</simpara>) if dd.text?
  137           result << dd.content if dd.blocks?
  138         end
  139         result << %(</#{item_tag}>)
  140         result << %(</#{entry_tag}>)
  141       end
  142 
  143       result << %(</#{list_tag}>) if list_tag
  144     end
  145 
  146     result.join LF
  147   end
  148 
  149   def convert_example node
  150     if node.title?
  151       %(<example#{common_attributes node.id, node.role, node.reftext}>
  152 <title>#{node.title}</title>
  153 #{enclose_content node}
  154 </example>)
  155     else
  156       %(<informalexample#{common_attributes node.id, node.role, node.reftext}>
  157 #{enclose_content node}
  158 </informalexample>)
  159     end
  160   end
  161 
  162   def convert_floating_title node
  163     %(<bridgehead#{common_attributes node.id, node.role, node.reftext} renderas="sect#{node.level}">#{node.title}</bridgehead>)
  164   end
  165 
  166   def convert_image node
  167     # NOTE according to the DocBook spec, content area, scaling, and scaling to fit are mutually exclusive
  168     # See http://tdg.docbook.org/tdg/4.5/imagedata-x.html#d0e79635
  169     if node.attr? 'scaledwidth'
  170       width_attribute = %( width="#{node.attr 'scaledwidth'}")
  171       depth_attribute = ''
  172       scale_attribute = ''
  173     elsif node.attr? 'scale'
  174       # QUESTION should we set the viewport using width and depth? (the scaled image would be contained within this box)
  175       #width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : ''
  176       #depth_attribute = (node.attr? 'height') ? %( depth="#{node.attr 'height'}") : ''
  177       scale_attribute = %( scale="#{node.attr 'scale'}")
  178     else
  179       width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : ''
  180       depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : ''
  181       scale_attribute = ''
  182     end
  183     align_attribute = (node.attr? 'align') ? %( align="#{node.attr 'align'}") : ''
  184 
  185     mediaobject = %(<mediaobject>
  186 <imageobject>
  187 <imagedata fileref="#{node.image_uri(node.attr 'target')}"#{width_attribute}#{depth_attribute}#{scale_attribute}#{align_attribute}/>
  188 </imageobject>
  189 <textobject><phrase>#{node.alt}</phrase></textobject>
  190 </mediaobject>)
  191 
  192     if node.title?
  193       %(<figure#{common_attributes node.id, node.role, node.reftext}>
  194 <title>#{node.title}</title>
  195 #{mediaobject}
  196 </figure>)
  197     else
  198       %(<informalfigure#{common_attributes node.id, node.role, node.reftext}>
  199 #{mediaobject}
  200 </informalfigure>)
  201     end
  202   end
  203 
  204   def convert_listing node
  205     informal = !node.title?
  206     common_attrs = common_attributes node.id, node.role, node.reftext
  207     if node.style == 'source'
  208       if (attrs = node.attributes).key? 'linenums'
  209         numbering_attrs = (attrs.key? 'start') ? %( linenumbering="numbered" startinglinenumber="#{attrs['start'].to_i}") : ' linenumbering="numbered"'
  210       else
  211         numbering_attrs = ' linenumbering="unnumbered"'
  212       end
  213       if attrs.key? 'language'
  214         wrapped_content = %(<programlisting#{informal ? common_attrs : ''} language="#{attrs['language']}"#{numbering_attrs}>#{node.content}</programlisting>)
  215       else
  216         wrapped_content = %(<screen#{informal ? common_attrs : ''}#{numbering_attrs}>#{node.content}</screen>)
  217       end
  218     else
  219       wrapped_content = %(<screen#{informal ? common_attrs : ''}>#{node.content}</screen>)
  220     end
  221     informal ? wrapped_content : %(<formalpara#{common_attrs}>
  222 <title>#{node.title}</title>
  223 <para>
  224 #{wrapped_content}
  225 </para>
  226 </formalpara>)
  227   end
  228 
  229   def convert_literal node
  230     if node.title?
  231       %(<formalpara#{common_attributes node.id, node.role, node.reftext}>
  232 <title>#{node.title}</title>
  233 <para>
  234 <literallayout class="monospaced">#{node.content}</literallayout>
  235 </para>
  236 </formalpara>)
  237     else
  238       %(<literallayout#{common_attributes node.id, node.role, node.reftext} class="monospaced">#{node.content}</literallayout>)
  239     end
  240   end
  241 
  242   alias convert_pass content_only
  243 
  244   def convert_stem node
  245     if (idx = node.subs.index :specialcharacters)
  246       node.subs.delete_at idx
  247       equation = node.content || ''
  248       idx > 0 ? (node.subs.insert idx, :specialcharacters) : (node.subs.unshift :specialcharacters)
  249     else
  250       equation = node.content || ''
  251     end
  252     if node.style == 'asciimath'
  253       # NOTE fop requires jeuclid to process mathml markup
  254       equation_data = asciimath_available? ? ((::AsciiMath.parse equation).to_mathml 'mml:', 'xmlns:mml' => 'http://www.w3.org/1998/Math/MathML') : %(<mathphrase><![CDATA[#{equation}]]></mathphrase>)
  255     else
  256       # unhandled math; pass source to alt and required mathphrase element; dblatex will process alt as LaTeX math
  257       equation_data = %(<alt><![CDATA[#{equation}]]></alt>
  258 <mathphrase><![CDATA[#{equation}]]></mathphrase>)
  259     end
  260     if node.title?
  261       %(<equation#{common_attributes node.id, node.role, node.reftext}>
  262 <title>#{node.title}</title>
  263 #{equation_data}
  264 </equation>)
  265     else
  266       # WARNING dblatex displays the <informalequation> element inline instead of block as documented (except w/ mathml)
  267       %(<informalequation#{common_attributes node.id, node.role, node.reftext}>
  268 #{equation_data}
  269 </informalequation>)
  270     end
  271   end
  272 
  273   def convert_olist node
  274     result = []
  275     num_attribute = node.style ? %( numeration="#{node.style}") : ''
  276     start_attribute = (node.attr? 'start') ? %( startingnumber="#{node.attr 'start'}") : ''
  277     result << %(<orderedlist#{common_attributes node.id, node.role, node.reftext}#{num_attribute}#{start_attribute}>)
  278     result << %(<title>#{node.title}</title>) if node.title?
  279     node.items.each do |item|
  280       result << %(<listitem#{common_attributes item.id, item.role}>)
  281       result << %(<simpara>#{item.text}</simpara>)
  282       result << item.content if item.blocks?
  283       result << '</listitem>'
  284     end
  285     result << %(</orderedlist>)
  286     result.join LF
  287   end
  288 
  289   def convert_open node
  290     case node.style
  291     when 'abstract'
  292       if node.parent == node.document && node.document.doctype == 'book'
  293         logger.warn 'abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
  294         ''
  295       else
  296         %(<abstract>
  297 #{title_tag node}#{enclose_content node}
  298 </abstract>)
  299       end
  300     when 'partintro'
  301       unless node.level == 0 && node.parent.context == :section && node.document.doctype == 'book'
  302         logger.error 'partintro block can only be used when doctype is book and must be a child of a book part. Excluding block content.'
  303         ''
  304       else
  305         %(<partintro#{common_attributes node.id, node.role, node.reftext}>
  306 #{title_tag node}#{enclose_content node}
  307 </partintro>)
  308       end
  309     else
  310       reftext = node.reftext if (id = node.id)
  311       role = node.role
  312       if node.title?
  313         %(<formalpara#{common_attributes id, role, reftext}>
  314 <title>#{node.title}</title>
  315 <para>#{content_spacer = node.content_model == :compound ? LF : ''}#{node.content}#{content_spacer}</para>
  316 </formalpara>)
  317       elsif id || role
  318         if node.content_model == :compound
  319           %(<para#{common_attributes id, role, reftext}>
  320 #{node.content}
  321 </para>)
  322         else
  323           %(<simpara#{common_attributes id, role, reftext}>#{node.content}</simpara>)
  324         end
  325       else
  326         enclose_content node
  327       end
  328     end
  329   end
  330 
  331   def convert_page_break node
  332     '<simpara><?asciidoc-pagebreak?></simpara>'
  333   end
  334 
  335   def convert_paragraph node
  336     if node.title?
  337       %(<formalpara#{common_attributes node.id, node.role, node.reftext}>
  338 <title>#{node.title}</title>
  339 <para>#{node.content}</para>
  340 </formalpara>)
  341     else
  342       %(<simpara#{common_attributes node.id, node.role, node.reftext}>#{node.content}</simpara>)
  343     end
  344   end
  345 
  346   def convert_preamble node
  347     if node.document.doctype == 'book'
  348       %(<preface#{common_attributes node.id, node.role, node.reftext}>
  349 #{title_tag node, false}#{node.content}
  350 </preface>)
  351     else
  352       node.content
  353     end
  354   end
  355 
  356   def convert_quote node
  357     blockquote_tag(node, (node.has_role? 'epigraph') && 'epigraph') { enclose_content node }
  358   end
  359 
  360   def convert_thematic_break node
  361     '<simpara><?asciidoc-hr?></simpara>'
  362   end
  363 
  364   def convert_sidebar node
  365     %(<sidebar#{common_attributes node.id, node.role, node.reftext}>
  366 #{title_tag node}#{enclose_content node}
  367 </sidebar>)
  368   end
  369 
  370   def convert_table node
  371     has_body = false
  372     result = []
  373     pgwide_attribute = (node.option? 'pgwide') ? ' pgwide="1"' : ''
  374     if (frame = node.attr 'frame', 'all', 'table-frame') == 'ends'
  375       frame = 'topbot'
  376     end
  377     grid = node.attr 'grid', nil, 'table-grid'
  378     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"' : ''}>)
  379     if (node.option? 'unbreakable')
  380       result << '<?dbfo keep-together="always"?>'
  381     elsif (node.option? 'breakable')
  382       result << '<?dbfo keep-together="auto"?>'
  383     end
  384     result << %(<title>#{node.title}</title>) if tag_name == 'table'
  385     col_width_key = if (width = (node.attr? 'width') ? (node.attr 'width') : nil)
  386       TABLE_PI_NAMES.each do |pi_name|
  387         result << %(<?#{pi_name} table-width="#{width}"?>)
  388       end
  389       'colabswidth'
  390     else
  391       'colpcwidth'
  392     end
  393     result << %(<tgroup cols="#{node.attr 'colcount'}">)
  394     node.columns.each do |col|
  395       result << %(<colspec colname="col_#{col.attr 'colnumber'}" colwidth="#{col.attr col_width_key}*"/>)
  396     end
  397     node.rows.to_h.each do |tsec, rows|
  398       next if rows.empty?
  399       has_body = true if tsec == :body
  400       result << %(<t#{tsec}>)
  401       rows.each do |row|
  402         result << '<row>'
  403         row.each do |cell|
  404           halign_attribute = (cell.attr? 'halign') ? %( align="#{cell.attr 'halign'}") : ''
  405           valign_attribute = (cell.attr? 'valign') ? %( valign="#{cell.attr 'valign'}") : ''
  406           colspan_attribute = cell.colspan ? %( namest="col_#{colnum = cell.column.attr 'colnumber'}" nameend="col_#{colnum + cell.colspan - 1}") : ''
  407           rowspan_attribute = cell.rowspan ? %( morerows="#{cell.rowspan - 1}") : ''
  408           # NOTE <entry> may not have whitespace (e.g., line breaks) as a direct descendant according to DocBook rules
  409           entry_start = %(<entry#{halign_attribute}#{valign_attribute}#{colspan_attribute}#{rowspan_attribute}>)
  410           if tsec == :head
  411             cell_content = cell.text
  412           else
  413             case cell.style
  414             when :asciidoc
  415               cell_content = cell.content
  416             when :literal
  417               cell_content = %(<literallayout class="monospaced">#{cell.text}</literallayout>)
  418             when :header
  419               cell_content = (cell_content = cell.content).empty? ? '' : %(<simpara><emphasis role="strong">#{cell_content.join '</emphasis></simpara><simpara><emphasis role="strong">'}</emphasis></simpara>)
  420             else
  421               cell_content = (cell_content = cell.content).empty? ? '' : %(<simpara>#{cell_content.join '</simpara><simpara>'}</simpara>)
  422             end
  423           end
  424           entry_end = (node.document.attr? 'cellbgcolor') ? %(<?dbfo bgcolor="#{node.document.attr 'cellbgcolor'}"?></entry>) : '</entry>'
  425           result << %(#{entry_start}#{cell_content}#{entry_end})
  426         end
  427         result << '</row>'
  428       end
  429       result << %(</t#{tsec}>)
  430     end
  431     result << '</tgroup>'
  432     result << %(</#{tag_name}>)
  433 
  434     logger.warn 'tables must have at least one body row' unless has_body
  435     result.join LF
  436   end
  437 
  438   alias convert_toc skip
  439 
  440   def convert_ulist node
  441     result = []
  442     if node.style == 'bibliography'
  443       result << %(<bibliodiv#{common_attributes node.id, node.role, node.reftext}>)
  444       result << %(<title>#{node.title}</title>) if node.title?
  445       node.items.each do |item|
  446         result << '<bibliomixed>'
  447         result << %(<bibliomisc>#{item.text}</bibliomisc>)
  448         result << item.content if item.blocks?
  449         result << '</bibliomixed>'
  450       end
  451       result << '</bibliodiv>'
  452     else
  453       mark_type = (checklist = node.option? 'checklist') ? 'none' : node.style
  454       mark_attribute = mark_type ? %( mark="#{mark_type}") : ''
  455       result << %(<itemizedlist#{common_attributes node.id, node.role, node.reftext}#{mark_attribute}>)
  456       result << %(<title>#{node.title}</title>) if node.title?
  457       node.items.each do |item|
  458         text_marker = (item.attr? 'checked') ? '&#10003; ' : '&#10063; ' if checklist && (item.attr? 'checkbox')
  459         result << %(<listitem#{common_attributes item.id, item.role}>)
  460         result << %(<simpara>#{text_marker || ''}#{item.text}</simpara>)
  461         result << item.content if item.blocks?
  462         result << '</listitem>'
  463       end
  464       result << '</itemizedlist>'
  465     end
  466     result.join LF
  467   end
  468 
  469   def convert_verse node
  470     blockquote_tag(node, (node.has_role? 'epigraph') && 'epigraph') { %(<literallayout>#{node.content}</literallayout>) }
  471   end
  472 
  473   alias convert_video skip
  474 
  475   def convert_inline_anchor node
  476     case node.type
  477     when :ref
  478       %(<anchor#{common_attributes((id = node.id), nil, node.reftext || %([#{id}]))}/>)
  479     when :xref
  480       if (path = node.attributes['path'])
  481         # QUESTION should we use refid as fallback text instead? (like the html5 backend?)
  482         %(<link xl:href="#{node.target}">#{node.text || path}</link>)
  483       else
  484         linkend = node.attributes['fragment'] || node.target
  485         (text = node.text) ? %(<link linkend="#{linkend}">#{text}</link>) : %(<xref linkend="#{linkend}"/>)
  486       end
  487     when :link
  488       %(<link xl:href="#{node.target}">#{node.text}</link>)
  489     when :bibref
  490       %(<anchor#{common_attributes node.id, nil, "[#{node.reftext || node.id}]"}/>#{text})
  491     else
  492       logger.warn %(unknown anchor type: #{node.type.inspect})
  493       nil
  494     end
  495   end
  496 
  497   def convert_inline_break node
  498     %(#{node.text}<?asciidoc-br?>)
  499   end
  500 
  501   def convert_inline_button node
  502     %(<guibutton>#{node.text}</guibutton>)
  503   end
  504 
  505   def convert_inline_callout node
  506     %(<co#{common_attributes node.id}/>)
  507   end
  508 
  509   def convert_inline_footnote node
  510     if node.type == :xref
  511       %(<footnoteref linkend="#{node.target}"/>)
  512     else
  513       %(<footnote#{common_attributes node.id}><simpara>#{node.text}</simpara></footnote>)
  514     end
  515   end
  516 
  517   def convert_inline_image node
  518     width_attribute = (node.attr? 'width') ? %( contentwidth="#{node.attr 'width'}") : ''
  519     depth_attribute = (node.attr? 'height') ? %( contentdepth="#{node.attr 'height'}") : ''
  520     %(<inlinemediaobject>
  521 <imageobject>
  522 <imagedata fileref="#{node.type == 'icon' ? (node.icon_uri node.target) : (node.image_uri node.target)}"#{width_attribute}#{depth_attribute}/>
  523 </imageobject>
  524 <textobject><phrase>#{node.alt}</phrase></textobject>
  525 </inlinemediaobject>)
  526   end
  527 
  528   def convert_inline_indexterm node
  529     if (see = node.attr 'see')
  530       rel = %(\n<see>#{see}</see>)
  531     elsif (see_also_list = node.attr 'see-also')
  532       rel = see_also_list.map {|see_also| %(\n<seealso>#{see_also}</seealso>) }.join
  533     else
  534       rel = ''
  535     end
  536     if node.type == :visible
  537       %(<indexterm>
  538 <primary>#{node.text}</primary>#{rel}
  539 </indexterm>#{node.text})
  540     else
  541       if (numterms = (terms = node.attr 'terms').size) > 2
  542         %(<indexterm>
  543 <primary>#{terms[0]}</primary><secondary>#{terms[1]}</secondary><tertiary>#{terms[2]}</tertiary>#{rel}
  544 </indexterm>#{(node.document.option? 'indexterm-promotion') ? %[
  545 <indexterm>
  546 <primary>#{terms[1]}</primary><secondary>#{terms[2]}</secondary>
  547 </indexterm>
  548 <indexterm>
  549 <primary>#{terms[2]}</primary>
  550 </indexterm>] : ''})
  551       elsif numterms > 1
  552         %(<indexterm>
  553 <primary>#{terms[0]}</primary><secondary>#{terms[1]}</secondary>#{rel}
  554 </indexterm>#{(node.document.option?  'indexterm-promotion') ? %[
  555 <indexterm>
  556 <primary>#{terms[1]}</primary>
  557 </indexterm>] : ''})
  558       else
  559         %(<indexterm>
  560 <primary>#{terms[0]}</primary>#{rel}
  561 </indexterm>)
  562       end
  563     end
  564   end
  565 
  566   def convert_inline_kbd node
  567     if (keys = node.attr 'keys').size == 1
  568       %(<keycap>#{keys[0]}</keycap>)
  569     else
  570       %(<keycombo><keycap>#{keys.join '</keycap><keycap>'}</keycap></keycombo>)
  571     end
  572   end
  573 
  574   def convert_inline_menu node
  575     menu = node.attr 'menu'
  576     if (submenus = node.attr 'submenus').empty?
  577       if (menuitem = node.attr 'menuitem')
  578         %(<menuchoice><guimenu>#{menu}</guimenu> <guimenuitem>#{menuitem}</guimenuitem></menuchoice>)
  579       else
  580         %(<guimenu>#{menu}</guimenu>)
  581       end
  582     else
  583       %(<menuchoice><guimenu>#{menu}</guimenu> <guisubmenu>#{submenus.join '</guisubmenu> <guisubmenu>'}</guisubmenu> <guimenuitem>#{node.attr 'menuitem'}</guimenuitem></menuchoice>)
  584     end
  585   end
  586 
  587   def convert_inline_quoted node
  588     if (type = node.type) == :asciimath
  589       # NOTE fop requires jeuclid to process mathml markup
  590       asciimath_available? ? %(<inlineequation>#{(::AsciiMath.parse node.text).to_mathml 'mml:', 'xmlns:mml' => 'http://www.w3.org/1998/Math/MathML'}</inlineequation>) : %(<inlineequation><mathphrase><![CDATA[#{node.text}]]></mathphrase></inlineequation>)
  591     elsif type == :latexmath
  592       # unhandled math; pass source to alt and required mathphrase element; dblatex will process alt as LaTeX math
  593       %(<inlineequation><alt><![CDATA[#{equation = node.text}]]></alt><mathphrase><![CDATA[#{equation}]]></mathphrase></inlineequation>)
  594     else
  595       open, close, supports_phrase = QUOTE_TAGS[type]
  596       text = node.text
  597       if node.role
  598         if supports_phrase
  599           quoted_text = %(#{open}<phrase role="#{node.role}">#{text}</phrase>#{close})
  600         else
  601           quoted_text = %(#{open.chop} role="#{node.role}">#{text}#{close})
  602         end
  603       else
  604         quoted_text = %(#{open}#{text}#{close})
  605       end
  606 
  607       node.id ? %(<anchor#{common_attributes node.id, nil, text}/>#{quoted_text}) : quoted_text
  608     end
  609   end
  610 
  611   private
  612 
  613   def common_attributes id, role = nil, reftext = nil
  614     if id
  615       attrs = %( xml:id="#{id}"#{role ? %[ role="#{role}"] : ''})
  616     elsif role
  617       attrs = %( role="#{role}")
  618     else
  619       attrs = ''
  620     end
  621     if reftext
  622       if (reftext.include? '<') && ((reftext = reftext.gsub XmlSanitizeRx, '').include? ' ')
  623         reftext = (reftext.squeeze ' ').strip
  624       end
  625       reftext = reftext.gsub '"', '&quot;' if reftext.include? '"'
  626       %(#{attrs} xreflabel="#{reftext}")
  627     else
  628       attrs
  629     end
  630   end
  631 
  632   def author_tag doc, author
  633     result = []
  634     result << '<author>'
  635     result << '<personname>'
  636     result << %(<firstname>#{doc.sub_replacements author.firstname}</firstname>) if author.firstname
  637     result << %(<othername>#{doc.sub_replacements author.middlename}</othername>) if author.middlename
  638     result << %(<surname>#{doc.sub_replacements author.lastname}</surname>) if author.lastname
  639     result << '</personname>'
  640     result << %(<email>#{author.email}</email>) if author.email
  641     result << '</author>'
  642     result.join LF
  643   end
  644 
  645   def document_info_tag doc
  646     result = ['<info>']
  647     unless doc.notitle
  648       if (title = doc.doctitle partition: true, use_fallback: true).subtitle?
  649         result << %(<title>#{title.main}</title>
  650 <subtitle>#{title.subtitle}</subtitle>)
  651       else
  652         result << %(<title>#{title}</title>)
  653       end
  654     end
  655     if (date = (doc.attr? 'revdate') ? (doc.attr 'revdate') : ((doc.attr? 'reproducible') ? nil : (doc.attr 'docdate')))
  656       result << %(<date>#{date}</date>)
  657     end
  658     if doc.attr? 'copyright'
  659       CopyrightRx =~ (doc.attr 'copyright')
  660       result << '<copyright>'
  661       result << %(<holder>#{$1}</holder>)
  662       result << %(<year>#{$2}</year>) if $2
  663       result << '</copyright>'
  664     end
  665     if doc.header?
  666       unless (authors = doc.authors).empty?
  667         if authors.size > 1
  668           result << '<authorgroup>'
  669           authors.each {|author| result << (author_tag doc, author) }
  670           result << '</authorgroup>'
  671         else
  672           result << (author_tag doc, (author = authors[0]))
  673           result << %(<authorinitials>#{author.initials}</authorinitials>) if author.initials
  674         end
  675       end
  676       if (doc.attr? 'revdate') && ((doc.attr? 'revnumber') || (doc.attr? 'revremark'))
  677         result << %(<revhistory>
  678 <revision>)
  679         result << %(<revnumber>#{doc.attr 'revnumber'}</revnumber>) if doc.attr? 'revnumber'
  680         result << %(<date>#{doc.attr 'revdate'}</date>) if doc.attr? 'revdate'
  681         result << %(<authorinitials>#{doc.attr 'authorinitials'}</authorinitials>) if doc.attr? 'authorinitials'
  682         result << %(<revremark>#{doc.attr 'revremark'}</revremark>) if doc.attr? 'revremark'
  683         result << %(</revision>
  684 </revhistory>)
  685       end
  686       if (doc.attr? 'front-cover-image') || (doc.attr? 'back-cover-image')
  687         if (back_cover_tag = cover_tag doc, 'back')
  688           result << (cover_tag doc, 'front', true)
  689           result << back_cover_tag
  690         elsif (front_cover_tag = cover_tag doc, 'front')
  691           result << front_cover_tag
  692         end
  693       end
  694       result << %(<orgname>#{doc.attr 'orgname'}</orgname>) if doc.attr? 'orgname'
  695       unless (docinfo_content = doc.docinfo).empty?
  696         result << docinfo_content
  697       end
  698     end
  699     result << '</info>'
  700 
  701     if doc.doctype == 'manpage'
  702       result << '<refmeta>'
  703       result << %(<refentrytitle>#{doc.attr 'mantitle'}</refentrytitle>) if doc.attr? 'mantitle'
  704       result << %(<manvolnum>#{doc.attr 'manvolnum'}</manvolnum>) if doc.attr? 'manvolnum'
  705       result << %(<refmiscinfo class="source">#{doc.attr 'mansource', '&#160;'}</refmiscinfo>)
  706       result << %(<refmiscinfo class="manual">#{doc.attr 'manmanual', '&#160;'}</refmiscinfo>)
  707       result << '</refmeta>'
  708       result << '<refnamediv>'
  709       result += (doc.attr 'mannames').map {|n| %(<refname>#{n}</refname>) } if doc.attr? 'mannames'
  710       result << %(<refpurpose>#{doc.attr 'manpurpose'}</refpurpose>) if doc.attr? 'manpurpose'
  711       result << '</refnamediv>'
  712     end
  713 
  714     result.join LF
  715   end
  716 
  717   # FIXME this should be handled through a template mechanism
  718   def enclose_content node
  719     node.content_model == :compound ? node.content : %(<simpara>#{node.content}</simpara>)
  720   end
  721 
  722   def title_tag node, optional = true
  723     !optional || node.title? ? %(<title>#{node.title}</title>\n) : ''
  724   end
  725 
  726   def cover_tag doc, face, use_placeholder = false
  727     if (cover_image = doc.attr %(#{face}-cover-image))
  728       width_attr = ''
  729       depth_attr = ''
  730       if (cover_image.include? ':') && ImageMacroRx =~ cover_image
  731         attrlist = $2
  732         cover_image = doc.image_uri $1
  733         if attrlist
  734           attrs = (AttributeList.new attrlist).parse ['alt', 'width', 'height']
  735           if attrs.key? 'scaledwidth'
  736             # NOTE scalefit="1" is the default in this case
  737             width_attr = %( width="#{attrs['scaledwidth']}")
  738           else
  739             width_attr = %( contentwidth="#{attrs['width']}") if attrs.key? 'width'
  740             depth_attr = %( contentdepth="#{attrs['height']}") if attrs.key? 'height'
  741           end
  742         end
  743       end
  744       %(<cover role="#{face}">
  745 <mediaobject>
  746 <imageobject>
  747 <imagedata fileref="#{cover_image}"#{width_attr}#{depth_attr}/>
  748 </imageobject>
  749 </mediaobject>
  750 </cover>)
  751     elsif use_placeholder
  752       %(<cover role="#{face}"/>)
  753     end
  754   end
  755 
  756   def blockquote_tag node, tag_name = nil
  757     if tag_name
  758       start_tag, end_tag = %(<#{tag_name}), %(</#{tag_name}>)
  759     else
  760       start_tag, end_tag = '<blockquote', '</blockquote>'
  761     end
  762     result = [%(#{start_tag}#{common_attributes node.id, node.role, node.reftext}>)]
  763     result << %(<title>#{node.title}</title>) if node.title?
  764     if (node.attr? 'attribution') || (node.attr? 'citetitle')
  765       result << '<attribution>'
  766       result << (node.attr 'attribution') if node.attr? 'attribution'
  767       result << %(<citetitle>#{node.attr 'citetitle'}</citetitle>) if node.attr? 'citetitle'
  768       result << '</attribution>'
  769     end
  770     result << yield
  771     result << end_tag
  772     result.join LF
  773   end
  774 
  775   def asciimath_available?
  776     (@asciimath_status ||= load_asciimath) == :loaded
  777   end
  778 
  779   def load_asciimath
  780     (defined? ::AsciiMath.parse) ? :loaded : (Helpers.require_library 'asciimath', true, :warn).nil? ? :unavailable : :loaded
  781   end
  782 end
  783 end