"Fossies" - the Fresh Open Source Software Archive

Member "asciidoctor-2.0.10/lib/asciidoctor/converter/html5.rb" (1 Jun 2019, 52859 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 latest Fossies "Diffs" side-by-side code changes report for "html5.rb": 2.0.9_vs_2.0.10.

    1 # frozen_string_literal: true
    2 module Asciidoctor
    3 # A built-in {Converter} implementation that generates HTML 5 output
    4 # consistent with the html5 backend from AsciiDoc Python.
    5 class Converter::Html5Converter < Converter::Base
    6   register_for 'html5'
    7 
    8   (QUOTE_TAGS = {
    9     monospaced:  ['<code>', '</code>', true],
   10     emphasis:    ['<em>', '</em>', true],
   11     strong:      ['<strong>', '</strong>', true],
   12     double:      ['&#8220;', '&#8221;'],
   13     single:      ['&#8216;', '&#8217;'],
   14     mark:        ['<mark>', '</mark>', true],
   15     superscript: ['<sup>', '</sup>', true],
   16     subscript:   ['<sub>', '</sub>', true],
   17     asciimath:   ['\$', '\$'],
   18     latexmath:   ['\(', '\)'],
   19     # Opal can't resolve these constants when referenced here
   20     #asciimath:  INLINE_MATH_DELIMITERS[:asciimath] + [false],
   21     #latexmath:  INLINE_MATH_DELIMITERS[:latexmath] + [false],
   22   }).default = ['', '']
   23 
   24   DropAnchorRx = /<(?:a[^>+]+|\/a)>/
   25   StemBreakRx = / *\\\n(?:\\?\n)*|\n\n+/
   26   if RUBY_ENGINE == 'opal'
   27     # NOTE In JavaScript, ^ matches the start of the string when the m flag is not set
   28     SvgPreambleRx = /^#{CC_ALL}*?(?=<svg\b)/
   29     SvgStartTagRx = /^<svg[^>]*>/
   30   else
   31     SvgPreambleRx = /\A.*?(?=<svg\b)/m
   32     SvgStartTagRx = /\A<svg[^>]*>/
   33   end
   34   DimensionAttributeRx = /\s(?:width|height|style)=(["'])#{CC_ANY}*?\1/
   35 
   36   def initialize backend, opts = {}
   37     @backend = backend
   38     if opts[:htmlsyntax] == 'xml'
   39       syntax = 'xml'
   40       @xml_mode = true
   41       @void_element_slash = '/'
   42     else
   43       syntax = 'html'
   44       @xml_mode = nil
   45       @void_element_slash = ''
   46     end
   47     init_backend_traits basebackend: 'html', filetype: 'html', htmlsyntax: syntax, outfilesuffix: '.html', supports_templates: true
   48   end
   49 
   50   def convert node, transform = node.node_name, opts = nil
   51     if transform == 'inline_quoted'; return convert_inline_quoted node
   52     elsif transform == 'paragraph'; return convert_paragraph node
   53     elsif transform == 'inline_anchor'; return convert_inline_anchor node
   54     elsif transform == 'section'; return convert_section node
   55     elsif transform == 'listing'; return convert_listing node
   56     elsif transform == 'literal'; return convert_literal node
   57     elsif transform == 'ulist'; return convert_ulist node
   58     elsif transform == 'olist'; return convert_olist node
   59     elsif transform == 'dlist'; return convert_dlist node
   60     elsif transform == 'admonition'; return convert_admonition node
   61     elsif transform == 'colist'; return convert_colist node
   62     elsif transform == 'embedded'; return convert_embedded node
   63     elsif transform == 'example'; return convert_example node
   64     elsif transform == 'floating_title'; return convert_floating_title node
   65     elsif transform == 'image'; return convert_image node
   66     elsif transform == 'inline_break'; return convert_inline_break node
   67     elsif transform == 'inline_button'; return convert_inline_button node
   68     elsif transform == 'inline_callout'; return convert_inline_callout node
   69     elsif transform == 'inline_footnote'; return convert_inline_footnote node
   70     elsif transform == 'inline_image'; return convert_inline_image node
   71     elsif transform == 'inline_indexterm'; return convert_inline_indexterm node
   72     elsif transform == 'inline_kbd'; return convert_inline_kbd node
   73     elsif transform == 'inline_menu'; return convert_inline_menu node
   74     elsif transform == 'open'; return convert_open node
   75     elsif transform == 'page_break'; return convert_page_break node
   76     elsif transform == 'preamble'; return convert_preamble node
   77     elsif transform == 'quote'; return convert_quote node
   78     elsif transform == 'sidebar'; return convert_sidebar node
   79     elsif transform == 'stem'; return convert_stem node
   80     elsif transform == 'table'; return convert_table node
   81     elsif transform == 'thematic_break'; return convert_thematic_break node
   82     elsif transform == 'verse'; return convert_verse node
   83     elsif transform == 'video'; return convert_video node
   84     elsif transform == 'document'; return convert_document node
   85     elsif transform == 'toc'; return convert_toc node
   86     elsif transform == 'pass'; return convert_pass node
   87     elsif transform == 'audio'; return convert_audio node
   88     else; return super
   89     end
   90   end
   91 
   92   def convert_document node
   93     br = %(<br#{slash = @void_element_slash}>)
   94     unless (asset_uri_scheme = (node.attr 'asset-uri-scheme', 'https')).empty?
   95       asset_uri_scheme = %(#{asset_uri_scheme}:)
   96     end
   97     cdn_base_url = %(#{asset_uri_scheme}//cdnjs.cloudflare.com/ajax/libs)
   98     linkcss = node.attr? 'linkcss'
   99     result = ['<!DOCTYPE html>']
  100     lang_attribute = (node.attr? 'nolang') ? '' : %( lang="#{node.attr 'lang', 'en'}")
  101     result << %(<html#{@xml_mode ? ' xmlns="http://www.w3.org/1999/xhtml"' : ''}#{lang_attribute}>)
  102     result << %(<head>
  103 <meta charset="#{node.attr 'encoding', 'UTF-8'}"#{slash}>
  104 <meta http-equiv="X-UA-Compatible" content="IE=edge"#{slash}>
  105 <meta name="viewport" content="width=device-width, initial-scale=1.0"#{slash}>
  106 <meta name="generator" content="Asciidoctor #{node.attr 'asciidoctor-version'}"#{slash}>)
  107     result << %(<meta name="application-name" content="#{node.attr 'app-name'}"#{slash}>) if node.attr? 'app-name'
  108     result << %(<meta name="description" content="#{node.attr 'description'}"#{slash}>) if node.attr? 'description'
  109     result << %(<meta name="keywords" content="#{node.attr 'keywords'}"#{slash}>) if node.attr? 'keywords'
  110     result << %(<meta name="author" content="#{((authors = node.sub_replacements node.attr 'authors').include? '<') ? (authors.gsub XmlSanitizeRx, '') : authors}"#{slash}>) if node.attr? 'authors'
  111     result << %(<meta name="copyright" content="#{node.attr 'copyright'}"#{slash}>) if node.attr? 'copyright'
  112     if node.attr? 'favicon'
  113       if (icon_href = node.attr 'favicon').empty?
  114         icon_href = 'favicon.ico'
  115         icon_type = 'image/x-icon'
  116       elsif (icon_ext = Helpers.extname icon_href, nil)
  117         icon_type = icon_ext == '.ico' ? 'image/x-icon' : %(image/#{icon_ext.slice 1, icon_ext.length})
  118       else
  119         icon_type = 'image/x-icon'
  120       end
  121       result << %(<link rel="icon" type="#{icon_type}" href="#{icon_href}"#{slash}>)
  122     end
  123     result << %(<title>#{node.doctitle sanitize: true, use_fallback: true}</title>)
  124 
  125     if DEFAULT_STYLESHEET_KEYS.include?(node.attr 'stylesheet')
  126       if (webfonts = node.attr 'webfonts')
  127         result << %(<link rel="stylesheet" href="#{asset_uri_scheme}//fonts.googleapis.com/css?family=#{webfonts.empty? ? 'Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700' : webfonts}"#{slash}>)
  128       end
  129       if linkcss
  130         result << %(<link rel="stylesheet" href="#{node.normalize_web_path DEFAULT_STYLESHEET_NAME, (node.attr 'stylesdir', ''), false}"#{slash}>)
  131       else
  132         result << %(<style>
  133 #{Stylesheets.instance.primary_stylesheet_data}
  134 </style>)
  135       end
  136     elsif node.attr? 'stylesheet'
  137       if linkcss
  138         result << %(<link rel="stylesheet" href="#{node.normalize_web_path((node.attr 'stylesheet'), (node.attr 'stylesdir', ''))}"#{slash}>)
  139       else
  140         result << %(<style>
  141 #{node.read_asset node.normalize_system_path((node.attr 'stylesheet'), (node.attr 'stylesdir', '')), warn_on_failure: true, label: 'stylesheet'}
  142 </style>)
  143       end
  144     end
  145 
  146     if node.attr? 'icons', 'font'
  147       if node.attr? 'iconfont-remote'
  148         result << %(<link rel="stylesheet" href="#{node.attr 'iconfont-cdn', %[#{cdn_base_url}/font-awesome/#{FONT_AWESOME_VERSION}/css/font-awesome.min.css]}"#{slash}>)
  149       else
  150         iconfont_stylesheet = %(#{node.attr 'iconfont-name', 'font-awesome'}.css)
  151         result << %(<link rel="stylesheet" href="#{node.normalize_web_path iconfont_stylesheet, (node.attr 'stylesdir', ''), false}"#{slash}>)
  152       end
  153     end
  154 
  155     if (syntax_hl = node.syntax_highlighter) && (syntax_hl.docinfo? :head)
  156       result << (syntax_hl.docinfo :head, node, cdn_base_url: cdn_base_url, linkcss: linkcss, self_closing_tag_slash: slash)
  157     end
  158 
  159     unless (docinfo_content = node.docinfo).empty?
  160       result << docinfo_content
  161     end
  162 
  163     result << '</head>'
  164     body_attrs = node.id ? [%(id="#{node.id}")] : []
  165     if (sectioned = node.sections?) && (node.attr? 'toc-class') && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
  166       classes = [node.doctype, (node.attr 'toc-class'), %(toc-#{node.attr 'toc-position', 'header'})]
  167     else
  168       classes = [node.doctype]
  169     end
  170     classes << node.role if node.role?
  171     body_attrs << %(class="#{classes.join ' '}")
  172     body_attrs << %(style="max-width: #{node.attr 'max-width'};") if node.attr? 'max-width'
  173     result << %(<body #{body_attrs.join ' '}>)
  174 
  175     unless (docinfo_content = node.docinfo :header).empty?
  176       result << docinfo_content
  177     end
  178 
  179     unless node.noheader
  180       result << '<div id="header">'
  181       if node.doctype == 'manpage'
  182         result << %(<h1>#{node.doctitle} Manual Page</h1>)
  183         if sectioned && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
  184           result << %(<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
  185 <div id="toctitle">#{node.attr 'toc-title'}</div>
  186 #{convert_outline node}
  187 </div>)
  188         end
  189         result << (generate_manname_section node) if node.attr? 'manpurpose'
  190       else
  191         if node.header?
  192           result << %(<h1>#{node.header.title}</h1>) unless node.notitle
  193           details = []
  194           idx = 1
  195           node.authors.each do |author|
  196             details << %(<span id="author#{idx > 1 ? idx : ''}" class="author">#{node.sub_replacements author.name}</span>#{br})
  197             details << %(<span id="email#{idx > 1 ? idx : ''}" class="email">#{node.sub_macros author.email}</span>#{br}) if author.email
  198             idx += 1
  199           end
  200           if node.attr? 'revnumber'
  201             details << %(<span id="revnumber">#{((node.attr 'version-label') || '').downcase} #{node.attr 'revnumber'}#{(node.attr? 'revdate') ? ',' : ''}</span>)
  202           end
  203           if node.attr? 'revdate'
  204             details << %(<span id="revdate">#{node.attr 'revdate'}</span>)
  205           end
  206           if node.attr? 'revremark'
  207             details << %(#{br}<span id="revremark">#{node.attr 'revremark'}</span>)
  208           end
  209           unless details.empty?
  210             result << '<div class="details">'
  211             result.concat details
  212             result << '</div>'
  213           end
  214         end
  215 
  216         if sectioned && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto')
  217           result << %(<div id="toc" class="#{node.attr 'toc-class', 'toc'}">
  218 <div id="toctitle">#{node.attr 'toc-title'}</div>
  219 #{convert_outline node}
  220 </div>)
  221         end
  222       end
  223       result << '</div>'
  224     end
  225 
  226     result << %(<div id="content">
  227 #{node.content}
  228 </div>)
  229 
  230     if node.footnotes? && !(node.attr? 'nofootnotes')
  231       result << %(<div id="footnotes">
  232 <hr#{slash}>)
  233       node.footnotes.each do |footnote|
  234         result << %(<div class="footnote" id="_footnotedef_#{footnote.index}">
  235 <a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a>. #{footnote.text}
  236 </div>)
  237       end
  238       result << '</div>'
  239     end
  240 
  241     unless node.nofooter
  242       result << '<div id="footer">'
  243       result << '<div id="footer-text">'
  244       result << %(#{node.attr 'version-label'} #{node.attr 'revnumber'}#{br}) if node.attr? 'revnumber'
  245       result << %(#{node.attr 'last-update-label'} #{node.attr 'docdatetime'}) if (node.attr? 'last-update-label') && !(node.attr? 'reproducible')
  246       result << '</div>'
  247       result << '</div>'
  248     end
  249 
  250     # JavaScript (and auxiliary stylesheets) loaded at the end of body for performance reasons
  251     # See http://www.html5rocks.com/en/tutorials/speed/script-loading/
  252 
  253     if syntax_hl && (syntax_hl.docinfo? :footer)
  254       result << (syntax_hl.docinfo :footer, node, cdn_base_url: cdn_base_url, linkcss: linkcss, self_closing_tag_slash: slash)
  255     end
  256 
  257     if node.attr? 'stem'
  258       eqnums_val = node.attr 'eqnums', 'none'
  259       eqnums_val = 'AMS' if eqnums_val.empty?
  260       eqnums_opt = %( equationNumbers: { autoNumber: "#{eqnums_val}" } )
  261       # IMPORTANT inspect calls on delimiter arrays are intentional for JavaScript compat (emulates JSON.stringify)
  262       result << %(<script type="text/x-mathjax-config">
  263 MathJax.Hub.Config({
  264   messageStyle: "none",
  265   tex2jax: {
  266     inlineMath: [#{INLINE_MATH_DELIMITERS[:latexmath].inspect}],
  267     displayMath: [#{BLOCK_MATH_DELIMITERS[:latexmath].inspect}],
  268     ignoreClass: "nostem|nolatexmath"
  269   },
  270   asciimath2jax: {
  271     delimiters: [#{BLOCK_MATH_DELIMITERS[:asciimath].inspect}],
  272     ignoreClass: "nostem|noasciimath"
  273   },
  274   TeX: {#{eqnums_opt}}
  275 })
  276 MathJax.Hub.Register.StartupHook("AsciiMath Jax Ready", function () {
  277   MathJax.InputJax.AsciiMath.postfilterHooks.Add(function (data, node) {
  278     if ((node = data.script.parentNode) && (node = node.parentNode) && node.classList.contains('stemblock')) {
  279       data.math.root.display = "block"
  280     }
  281     return data
  282   })
  283 })
  284 </script>
  285 <script src="#{cdn_base_url}/mathjax/#{MATHJAX_VERSION}/MathJax.js?config=TeX-MML-AM_HTMLorMML"></script>)
  286     end
  287 
  288     unless (docinfo_content = node.docinfo :footer).empty?
  289       result << docinfo_content
  290     end
  291 
  292     result << '</body>'
  293     result << '</html>'
  294     result.join LF
  295   end
  296 
  297   def convert_embedded node
  298     result = []
  299     if node.doctype == 'manpage'
  300       # QUESTION should notitle control the manual page title?
  301       unless node.notitle
  302         id_attr = node.id ? %( id="#{node.id}") : ''
  303         result << %(<h1#{id_attr}>#{node.doctitle} Manual Page</h1>)
  304       end
  305       result << (generate_manname_section node) if node.attr? 'manpurpose'
  306     elsif node.header? && !node.notitle
  307       id_attr = node.id ? %( id="#{node.id}") : ''
  308       result << %(<h1#{id_attr}>#{node.header.title}</h1>)
  309     end
  310 
  311     if node.sections? && (node.attr? 'toc') && (toc_p = node.attr 'toc-placement') != 'macro' && toc_p != 'preamble'
  312       result << %(<div id="toc" class="toc">
  313 <div id="toctitle">#{node.attr 'toc-title'}</div>
  314 #{convert_outline node}
  315 </div>)
  316     end
  317 
  318     result << node.content
  319 
  320     if node.footnotes? && !(node.attr? 'nofootnotes')
  321       result << %(<div id="footnotes">
  322 <hr#{@void_element_slash}>)
  323       node.footnotes.each do |footnote|
  324         result << %(<div class="footnote" id="_footnotedef_#{footnote.index}">
  325 <a href="#_footnoteref_#{footnote.index}">#{footnote.index}</a>. #{footnote.text}
  326 </div>)
  327       end
  328       result << '</div>'
  329     end
  330 
  331     result.join LF
  332   end
  333 
  334   def convert_outline node, opts = {}
  335     return unless node.sections?
  336     sectnumlevels = opts[:sectnumlevels] || (node.document.attributes['sectnumlevels'] || 3).to_i
  337     toclevels = opts[:toclevels] || (node.document.attributes['toclevels'] || 2).to_i
  338     sections = node.sections
  339     # FIXME top level is incorrect if a multipart book starts with a special section defined at level 0
  340     result = [%(<ul class="sectlevel#{sections[0].level}">)]
  341     sections.each do |section|
  342       slevel = section.level
  343       if section.caption
  344         stitle = section.captioned_title
  345       elsif section.numbered && slevel <= sectnumlevels
  346         if slevel < 2 && node.document.doctype == 'book'
  347           if section.sectname == 'chapter'
  348             stitle =  %(#{(signifier = node.document.attributes['chapter-signifier']) ? "#{signifier} " : ''}#{section.sectnum} #{section.title})
  349           elsif section.sectname == 'part'
  350             stitle =  %(#{(signifier = node.document.attributes['part-signifier']) ? "#{signifier} " : ''}#{section.sectnum nil, ':'} #{section.title})
  351           else
  352             stitle = %(#{section.sectnum} #{section.title})
  353           end
  354         else
  355           stitle = %(#{section.sectnum} #{section.title})
  356         end
  357       else
  358         stitle = section.title
  359       end
  360       stitle = stitle.gsub DropAnchorRx, '' if stitle.include? '<a'
  361       if slevel < toclevels && (child_toc_level = convert_outline section, toclevels: toclevels, sectnumlevels: sectnumlevels)
  362         result << %(<li><a href="##{section.id}">#{stitle}</a>)
  363         result << child_toc_level
  364         result << '</li>'
  365       else
  366         result << %(<li><a href="##{section.id}">#{stitle}</a></li>)
  367       end
  368     end
  369     result << '</ul>'
  370     result.join LF
  371   end
  372 
  373   def convert_section node
  374     doc_attrs = node.document.attributes
  375     level = node.level
  376     if node.caption
  377       title = node.captioned_title
  378     elsif node.numbered && level <= (doc_attrs['sectnumlevels'] || 3).to_i
  379       if level < 2 && node.document.doctype == 'book'
  380         if node.sectname == 'chapter'
  381           title = %(#{(signifier = doc_attrs['chapter-signifier']) ? "#{signifier} " : ''}#{node.sectnum} #{node.title})
  382         elsif node.sectname == 'part'
  383           title = %(#{(signifier = doc_attrs['part-signifier']) ? "#{signifier} " : ''}#{node.sectnum nil, ':'} #{node.title})
  384         else
  385           title = %(#{node.sectnum} #{node.title})
  386         end
  387       else
  388         title = %(#{node.sectnum} #{node.title})
  389       end
  390     else
  391       title = node.title
  392     end
  393     if node.id
  394       id_attr = %( id="#{id = node.id}")
  395       if doc_attrs['sectlinks']
  396         title = %(<a class="link" href="##{id}">#{title}</a>)
  397       end
  398       if doc_attrs['sectanchors']
  399         # QUESTION should we add a font-based icon in anchor if icons=font?
  400         if doc_attrs['sectanchors'] == 'after'
  401           title = %(#{title}<a class="anchor" href="##{id}"></a>)
  402         else
  403           title = %(<a class="anchor" href="##{id}"></a>#{title})
  404         end
  405       end
  406     else
  407       id_attr = ''
  408     end
  409     if level == 0
  410       %(<h1#{id_attr} class="sect0#{(role = node.role) ? " #{role}" : ''}">#{title}</h1>
  411 #{node.content})
  412     else
  413       %(<div class="sect#{level}#{(role = node.role) ? " #{role}" : ''}">
  414 <h#{level + 1}#{id_attr}>#{title}</h#{level + 1}>
  415 #{level == 1 ? %[<div class="sectionbody">
  416 #{node.content}
  417 </div>] : node.content}
  418 </div>)
  419     end
  420   end
  421 
  422   def convert_admonition node
  423     id_attr = node.id ? %( id="#{node.id}") : ''
  424     name = node.attr 'name'
  425     title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : ''
  426     if node.document.attr? 'icons'
  427       if (node.document.attr? 'icons', 'font') && !(node.attr? 'icon')
  428         label = %(<i class="fa icon-#{name}" title="#{node.attr 'textlabel'}"></i>)
  429       else
  430         label = %(<img src="#{node.icon_uri name}" alt="#{node.attr 'textlabel'}"#{@void_element_slash}>)
  431       end
  432     else
  433       label = %(<div class="title">#{node.attr 'textlabel'}</div>)
  434     end
  435     %(<div#{id_attr} class="admonitionblock #{name}#{(role = node.role) ? " #{role}" : ''}">
  436 <table>
  437 <tr>
  438 <td class="icon">
  439 #{label}
  440 </td>
  441 <td class="content">
  442 #{title_element}#{node.content}
  443 </td>
  444 </tr>
  445 </table>
  446 </div>)
  447   end
  448 
  449   def convert_audio node
  450     xml = @xml_mode
  451     id_attribute = node.id ? %( id="#{node.id}") : ''
  452     classes = ['audioblock', node.role].compact
  453     class_attribute = %( class="#{classes.join ' '}")
  454     title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : ''
  455     start_t = node.attr 'start'
  456     end_t = node.attr 'end'
  457     time_anchor = (start_t || end_t) ? %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''}) : ''
  458     %(<div#{id_attribute}#{class_attribute}>
  459 #{title_element}<div class="content">
  460 <audio src="#{node.media_uri(node.attr 'target')}#{time_anchor}"#{(node.option? 'autoplay') ? (append_boolean_attribute 'autoplay', xml) : ''}#{(node.option? 'nocontrols') ? '' : (append_boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (append_boolean_attribute 'loop', xml) : ''}>
  461 Your browser does not support the audio tag.
  462 </audio>
  463 </div>
  464 </div>)
  465   end
  466 
  467   def convert_colist node
  468     result = []
  469     id_attribute = node.id ? %( id="#{node.id}") : ''
  470     classes = ['colist', node.style, node.role].compact
  471     class_attribute = %( class="#{classes.join ' '}")
  472 
  473     result << %(<div#{id_attribute}#{class_attribute}>)
  474     result << %(<div class="title">#{node.title}</div>) if node.title?
  475 
  476     if node.document.attr? 'icons'
  477       result << '<table>'
  478       font_icons, num = (node.document.attr? 'icons', 'font'), 0
  479       node.items.each do |item|
  480         num += 1
  481         if font_icons
  482           num_label = %(<i class="conum" data-value="#{num}"></i><b>#{num}</b>)
  483         else
  484           num_label = %(<img src="#{node.icon_uri "callouts/#{num}"}" alt="#{num}"#{@void_element_slash}>)
  485         end
  486         result << %(<tr>
  487 <td>#{num_label}</td>
  488 <td>#{item.text}#{item.blocks? ? LF + item.content : ''}</td>
  489 </tr>)
  490       end
  491       result << '</table>'
  492     else
  493       result << '<ol>'
  494       node.items.each do |item|
  495         result << %(<li>
  496 <p>#{item.text}</p>#{item.blocks? ? LF + item.content : ''}
  497 </li>)
  498       end
  499       result << '</ol>'
  500     end
  501 
  502     result << '</div>'
  503     result.join LF
  504   end
  505 
  506   def convert_dlist node
  507     result = []
  508     id_attribute = node.id ? %( id="#{node.id}") : ''
  509 
  510     classes = case node.style
  511     when 'qanda'
  512       ['qlist', 'qanda', node.role]
  513     when 'horizontal'
  514       ['hdlist', node.role]
  515     else
  516       ['dlist', node.style, node.role]
  517     end.compact
  518 
  519     class_attribute = %( class="#{classes.join ' '}")
  520 
  521     result << %(<div#{id_attribute}#{class_attribute}>)
  522     result << %(<div class="title">#{node.title}</div>) if node.title?
  523     case node.style
  524     when 'qanda'
  525       result << '<ol>'
  526       node.items.each do |terms, dd|
  527         result << '<li>'
  528         terms.each do |dt|
  529           result << %(<p><em>#{dt.text}</em></p>)
  530         end
  531         if dd
  532           result << %(<p>#{dd.text}</p>) if dd.text?
  533           result << dd.content if dd.blocks?
  534         end
  535         result << '</li>'
  536       end
  537       result << '</ol>'
  538     when 'horizontal'
  539       slash = @void_element_slash
  540       result << '<table>'
  541       if (node.attr? 'labelwidth') || (node.attr? 'itemwidth')
  542         result << '<colgroup>'
  543         col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : ''
  544         result << %(<col#{col_style_attribute}#{slash}>)
  545         col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : ''
  546         result << %(<col#{col_style_attribute}#{slash}>)
  547         result << '</colgroup>'
  548       end
  549       node.items.each do |terms, dd|
  550         result << '<tr>'
  551         result << %(<td class="hdlist1#{(node.option? 'strong') ? ' strong' : ''}">)
  552         first_term = true
  553         terms.each do |dt|
  554           result << %(<br#{slash}>) unless first_term
  555           result << dt.text
  556           first_term = nil
  557         end
  558         result << '</td>'
  559         result << '<td class="hdlist2">'
  560         if dd
  561           result << %(<p>#{dd.text}</p>) if dd.text?
  562           result << dd.content if dd.blocks?
  563         end
  564         result << '</td>'
  565         result << '</tr>'
  566       end
  567       result << '</table>'
  568     else
  569       result << '<dl>'
  570       dt_style_attribute = node.style ? '' : ' class="hdlist1"'
  571       node.items.each do |terms, dd|
  572         terms.each do |dt|
  573           result << %(<dt#{dt_style_attribute}>#{dt.text}</dt>)
  574         end
  575         if dd
  576           result << '<dd>'
  577           result << %(<p>#{dd.text}</p>) if dd.text?
  578           result << dd.content if dd.blocks?
  579           result << '</dd>'
  580         end
  581       end
  582       result << '</dl>'
  583     end
  584 
  585     result << '</div>'
  586     result.join LF
  587   end
  588 
  589   def convert_example node
  590     id_attribute = node.id ? %( id="#{node.id}") : ''
  591     if node.option? 'collapsible'
  592       class_attribute = node.role ? %( class="#{node.role}") : ''
  593       summary_element = node.title? ? %(<summary class="title">#{node.title}</summary>) : '<summary class="title">Details</summary>'
  594       %(<details#{id_attribute}#{class_attribute}#{(node.option? 'open') ? ' open' : ''}>
  595 #{summary_element}
  596 <div class="content">
  597 #{node.content}
  598 </div>
  599 </details>)
  600     else
  601       title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : ''
  602       %(<div#{id_attribute} class="exampleblock#{(role = node.role) ? " #{role}" : ''}">
  603 #{title_element}<div class="content">
  604 #{node.content}
  605 </div>
  606 </div>)
  607     end
  608   end
  609 
  610   def convert_floating_title node
  611     tag_name = %(h#{node.level + 1})
  612     id_attribute = node.id ? %( id="#{node.id}") : ''
  613     classes = [node.style, node.role].compact
  614     %(<#{tag_name}#{id_attribute} class="#{classes.join ' '}">#{node.title}</#{tag_name}>)
  615   end
  616 
  617   def convert_image node
  618     target = node.attr 'target'
  619     width_attr = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : ''
  620     height_attr = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : ''
  621     if ((node.attr? 'format', 'svg') || (target.include? '.svg')) && node.document.safe < SafeMode::SECURE &&
  622         ((svg = (node.option? 'inline')) || (obj = (node.option? 'interactive')))
  623       if svg
  624         img = (read_svg_contents node, target) || %(<span class="alt">#{node.alt}</span>)
  625       elsif obj
  626         fallback = (node.attr? 'fallback') ? %(<img src="#{node.image_uri(node.attr 'fallback')}" alt="#{encode_attribute_value node.alt}"#{width_attr}#{height_attr}#{@void_element_slash}>) : %(<span class="alt">#{node.alt}</span>)
  627         img = %(<object type="image/svg+xml" data="#{node.image_uri target}"#{width_attr}#{height_attr}>#{fallback}</object>)
  628       end
  629     end
  630     img ||= %(<img src="#{node.image_uri target}" alt="#{encode_attribute_value node.alt}"#{width_attr}#{height_attr}#{@void_element_slash}>)
  631     if node.attr? 'link'
  632       img = %(<a class="image" href="#{node.attr 'link'}"#{(append_link_constraint_attrs node).join}>#{img}</a>)
  633     end
  634     id_attr = node.id ? %( id="#{node.id}") : ''
  635     classes = ['imageblock']
  636     classes << (node.attr 'float') if node.attr? 'float'
  637     classes << %(text-#{node.attr 'align'}) if node.attr? 'align'
  638     classes << node.role if node.role
  639     class_attr = %( class="#{classes.join ' '}")
  640     title_el = node.title? ? %(\n<div class="title">#{node.captioned_title}</div>) : ''
  641     %(<div#{id_attr}#{class_attr}>
  642 <div class="content">
  643 #{img}
  644 </div>#{title_el}
  645 </div>)
  646   end
  647 
  648   def convert_listing node
  649     nowrap = (node.option? 'nowrap') || !(node.document.attr? 'prewrap')
  650     if node.style == 'source'
  651       lang = node.attr 'language'
  652       if (syntax_hl = node.document.syntax_highlighter)
  653         opts = syntax_hl.highlight? ? {
  654           css_mode: ((doc_attrs = node.document.attributes)[%(#{syntax_hl.name}-css)] || :class).to_sym,
  655           style: doc_attrs[%(#{syntax_hl.name}-style)],
  656         } : {}
  657         opts[:nowrap] = nowrap
  658       else
  659         pre_open = %(<pre class="highlight#{nowrap ? ' nowrap' : ''}"><code#{lang ? %[ class="language-#{lang}" data-lang="#{lang}"] : ''}>)
  660         pre_close = '</code></pre>'
  661       end
  662     else
  663       pre_open = %(<pre#{nowrap ? ' class="nowrap"' : ''}>)
  664       pre_close = '</pre>'
  665     end
  666     id_attribute = node.id ? %( id="#{node.id}") : ''
  667     title_element = node.title? ? %(<div class="title">#{node.captioned_title}</div>\n) : ''
  668     %(<div#{id_attribute} class="listingblock#{(role = node.role) ? " #{role}" : ''}">
  669 #{title_element}<div class="content">
  670 #{syntax_hl ? (syntax_hl.format node, lang, opts) : pre_open + (node.content || '') + pre_close}
  671 </div>
  672 </div>)
  673   end
  674 
  675   def convert_literal node
  676     id_attribute = node.id ? %( id="#{node.id}") : ''
  677     title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : ''
  678     nowrap = !(node.document.attr? 'prewrap') || (node.option? 'nowrap')
  679     %(<div#{id_attribute} class="literalblock#{(role = node.role) ? " #{role}" : ''}">
  680 #{title_element}<div class="content">
  681 <pre#{nowrap ? ' class="nowrap"' : ''}>#{node.content}</pre>
  682 </div>
  683 </div>)
  684   end
  685 
  686   def convert_stem node
  687     id_attribute = node.id ? %( id="#{node.id}") : ''
  688     title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : ''
  689     open, close = BLOCK_MATH_DELIMITERS[style = node.style.to_sym]
  690     if (equation = node.content)
  691       if style == :asciimath && (equation.include? LF)
  692         br = %(<br#{@void_element_slash}>#{LF})
  693         equation = equation.gsub(StemBreakRx) { %(#{close}#{br * ($&.count LF)}#{open}) }
  694       end
  695       unless (equation.start_with? open) && (equation.end_with? close)
  696         equation = %(#{open}#{equation}#{close})
  697       end
  698     else
  699       equation = ''
  700     end
  701     %(<div#{id_attribute} class="stemblock#{(role = node.role) ? " #{role}" : ''}">
  702 #{title_element}<div class="content">
  703 #{equation}
  704 </div>
  705 </div>)
  706   end
  707 
  708   def convert_olist node
  709     result = []
  710     id_attribute = node.id ? %( id="#{node.id}") : ''
  711     classes = ['olist', node.style, node.role].compact
  712     class_attribute = %( class="#{classes.join ' '}")
  713 
  714     result << %(<div#{id_attribute}#{class_attribute}>)
  715     result << %(<div class="title">#{node.title}</div>) if node.title?
  716 
  717     type_attribute = (keyword = node.list_marker_keyword) ? %( type="#{keyword}") : ''
  718     start_attribute = (node.attr? 'start') ? %( start="#{node.attr 'start'}") : ''
  719     reversed_attribute = (node.option? 'reversed') ? (append_boolean_attribute 'reversed', @xml_mode) : ''
  720     result << %(<ol class="#{node.style}"#{type_attribute}#{start_attribute}#{reversed_attribute}>)
  721 
  722     node.items.each do |item|
  723       if item.id
  724         result << %(<li id="#{item.id}"#{item.role ? %[ class="#{item.role}"] : ''}>)
  725       elsif item.role
  726         result << %(<li class="#{item.role}">)
  727       else
  728         result << '<li>'
  729       end
  730       result << %(<p>#{item.text}</p>)
  731       result << item.content if item.blocks?
  732       result << '</li>'
  733     end
  734 
  735     result << '</ol>'
  736     result << '</div>'
  737     result.join LF
  738   end
  739 
  740   def convert_open node
  741     if (style = node.style) == 'abstract'
  742       if node.parent == node.document && node.document.doctype == 'book'
  743         logger.warn 'abstract block cannot be used in a document without a title when doctype is book. Excluding block content.'
  744         ''
  745       else
  746         id_attr = node.id ? %( id="#{node.id}") : ''
  747         title_el = node.title? ? %(<div class="title">#{node.title}</div>\n) : ''
  748         %(<div#{id_attr} class="quoteblock abstract#{(role = node.role) ? " #{role}" : ''}">
  749 #{title_el}<blockquote>
  750 #{node.content}
  751 </blockquote>
  752 </div>)
  753       end
  754     elsif style == 'partintro' && (node.level > 0 || node.parent.context != :section || node.document.doctype != 'book')
  755       logger.error 'partintro block can only be used when doctype is book and must be a child of a book part. Excluding block content.'
  756       ''
  757     else
  758         id_attr = node.id ? %( id="#{node.id}") : ''
  759         title_el = node.title? ? %(<div class="title">#{node.title}</div>\n) : ''
  760       %(<div#{id_attr} class="openblock#{style && style != 'open' ? " #{style}" : ''}#{(role = node.role) ? " #{role}" : ''}">
  761 #{title_el}<div class="content">
  762 #{node.content}
  763 </div>
  764 </div>)
  765     end
  766   end
  767 
  768   def convert_page_break node
  769     '<div style="page-break-after: always;"></div>'
  770   end
  771 
  772   def convert_paragraph node
  773     if node.role
  774       attributes = %(#{node.id ? %[ id="#{node.id}"] : ''} class="paragraph #{node.role}")
  775     elsif node.id
  776       attributes = %( id="#{node.id}" class="paragraph")
  777     else
  778       attributes = ' class="paragraph"'
  779     end
  780     if node.title?
  781       %(<div#{attributes}>
  782 <div class="title">#{node.title}</div>
  783 <p>#{node.content}</p>
  784 </div>)
  785     else
  786       %(<div#{attributes}>
  787 <p>#{node.content}</p>
  788 </div>)
  789     end
  790   end
  791 
  792   alias convert_pass content_only
  793 
  794   def convert_preamble node
  795     if (doc = node.document).attr?('toc-placement', 'preamble') && doc.sections? && (doc.attr? 'toc')
  796       toc = %(
  797 <div id="toc" class="#{doc.attr 'toc-class', 'toc'}">
  798 <div id="toctitle">#{doc.attr 'toc-title'}</div>
  799 #{convert_outline doc}
  800 </div>)
  801     else
  802       toc = ''
  803     end
  804 
  805     %(<div id="preamble">
  806 <div class="sectionbody">
  807 #{node.content}
  808 </div>#{toc}
  809 </div>)
  810   end
  811 
  812   def convert_quote node
  813     id_attribute = node.id ? %( id="#{node.id}") : ''
  814     classes = ['quoteblock', node.role].compact
  815     class_attribute = %( class="#{classes.join ' '}")
  816     title_element = node.title? ? %(\n<div class="title">#{node.title}</div>) : ''
  817     attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil
  818     citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil
  819     if attribution || citetitle
  820       cite_element = citetitle ? %(<cite>#{citetitle}</cite>) : ''
  821       attribution_text = attribution ? %(&#8212; #{attribution}#{citetitle ? "<br#{@void_element_slash}>\n" : ''}) : ''
  822       attribution_element = %(\n<div class="attribution">\n#{attribution_text}#{cite_element}\n</div>)
  823     else
  824       attribution_element = ''
  825     end
  826 
  827     %(<div#{id_attribute}#{class_attribute}>#{title_element}
  828 <blockquote>
  829 #{node.content}
  830 </blockquote>#{attribution_element}
  831 </div>)
  832   end
  833 
  834   def convert_thematic_break node
  835     %(<hr#{@void_element_slash}>)
  836   end
  837 
  838   def convert_sidebar node
  839     id_attribute = node.id ? %( id="#{node.id}") : ''
  840     title_element = node.title? ? %(<div class="title">#{node.title}</div>\n) : ''
  841     %(<div#{id_attribute} class="sidebarblock#{(role = node.role) ? " #{role}" : ''}">
  842 <div class="content">
  843 #{title_element}#{node.content}
  844 </div>
  845 </div>)
  846   end
  847 
  848   def convert_table node
  849     result = []
  850     id_attribute = node.id ? %( id="#{node.id}") : ''
  851     classes = ['tableblock', %(frame-#{node.attr 'frame', 'all', 'table-frame'}), %(grid-#{node.attr 'grid', 'all', 'table-grid'})]
  852     if (stripes = node.attr 'stripes', nil, 'table-stripes')
  853       classes << %(stripes-#{stripes})
  854     end
  855     styles = []
  856     if (autowidth = node.option? 'autowidth') && !(node.attr? 'width')
  857       classes << 'fit-content'
  858     elsif (tablewidth = node.attr 'tablepcwidth') == 100
  859       classes << 'stretch'
  860     else
  861       styles << %(width: #{tablewidth}%;)
  862     end
  863     classes << (node.attr 'float') if node.attr? 'float'
  864     if (role = node.role)
  865       classes << role
  866     end
  867     class_attribute = %( class="#{classes.join ' '}")
  868     style_attribute = styles.empty? ? '' : %( style="#{styles.join ' '}")
  869 
  870     result << %(<table#{id_attribute}#{class_attribute}#{style_attribute}>)
  871     result << %(<caption class="title">#{node.captioned_title}</caption>) if node.title?
  872     if (node.attr 'rowcount') > 0
  873       slash = @void_element_slash
  874       result << '<colgroup>'
  875       if autowidth
  876         result += (Array.new node.columns.size, %(<col#{slash}>))
  877       else
  878         node.columns.each do |col|
  879           result << ((col.option? 'autowidth') ? %(<col#{slash}>) : %(<col style="width: #{col.attr 'colpcwidth'}%;"#{slash}>))
  880         end
  881       end
  882       result << '</colgroup>'
  883       node.rows.to_h.each do |tsec, rows|
  884         next if rows.empty?
  885         result << %(<t#{tsec}>)
  886         rows.each do |row|
  887           result << '<tr>'
  888           row.each do |cell|
  889             if tsec == :head
  890               cell_content = cell.text
  891             else
  892               case cell.style
  893               when :asciidoc
  894                 cell_content = %(<div class="content">#{cell.content}</div>)
  895               when :literal
  896                 cell_content = %(<div class="literal"><pre>#{cell.text}</pre></div>)
  897               else
  898                 cell_content = (cell_content = cell.content).empty? ? '' : %(<p class="tableblock">#{cell_content.join '</p>
  899 <p class="tableblock">'}</p>)
  900               end
  901             end
  902 
  903             cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')
  904             cell_class_attribute = %( class="tableblock halign-#{cell.attr 'halign'} valign-#{cell.attr 'valign'}")
  905             cell_colspan_attribute = cell.colspan ? %( colspan="#{cell.colspan}") : ''
  906             cell_rowspan_attribute = cell.rowspan ? %( rowspan="#{cell.rowspan}") : ''
  907             cell_style_attribute = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'};") : ''
  908             result << %(<#{cell_tag_name}#{cell_class_attribute}#{cell_colspan_attribute}#{cell_rowspan_attribute}#{cell_style_attribute}>#{cell_content}</#{cell_tag_name}>)
  909           end
  910           result << '</tr>'
  911         end
  912         result << %(</t#{tsec}>)
  913       end
  914     end
  915     result << '</table>'
  916     result.join LF
  917   end
  918 
  919   def convert_toc node
  920     unless (doc = node.document).attr?('toc-placement', 'macro') && doc.sections? && (doc.attr? 'toc')
  921       return '<!-- toc disabled -->'
  922     end
  923 
  924     if node.id
  925       id_attr = %( id="#{node.id}")
  926       title_id_attr = %( id="#{node.id}title")
  927     else
  928       id_attr = ' id="toc"'
  929       title_id_attr = ' id="toctitle"'
  930     end
  931     title = node.title? ? node.title : (doc.attr 'toc-title')
  932     levels = (node.attr? 'levels') ? (node.attr 'levels').to_i : nil
  933     role = node.role? ? node.role : (doc.attr 'toc-class', 'toc')
  934 
  935     %(<div#{id_attr} class="#{role}">
  936 <div#{title_id_attr} class="title">#{title}</div>
  937 #{convert_outline doc, toclevels: levels}
  938 </div>)
  939   end
  940 
  941   def convert_ulist node
  942     result = []
  943     id_attribute = node.id ? %( id="#{node.id}") : ''
  944     div_classes = ['ulist', node.style, node.role].compact
  945     marker_checked = marker_unchecked = ''
  946     if (checklist = node.option? 'checklist')
  947       div_classes.unshift div_classes.shift, 'checklist'
  948       ul_class_attribute = ' class="checklist"'
  949       if node.option? 'interactive'
  950         if @xml_mode
  951           marker_checked = '<input type="checkbox" data-item-complete="1" checked="checked"/> '
  952           marker_unchecked = '<input type="checkbox" data-item-complete="0"/> '
  953         else
  954           marker_checked = '<input type="checkbox" data-item-complete="1" checked> '
  955           marker_unchecked = '<input type="checkbox" data-item-complete="0"> '
  956         end
  957       elsif node.document.attr? 'icons', 'font'
  958         marker_checked = '<i class="fa fa-check-square-o"></i> '
  959         marker_unchecked = '<i class="fa fa-square-o"></i> '
  960       else
  961         marker_checked = '&#10003; '
  962         marker_unchecked = '&#10063; '
  963       end
  964     else
  965       ul_class_attribute = node.style ? %( class="#{node.style}") : ''
  966     end
  967     result << %(<div#{id_attribute} class="#{div_classes.join ' '}">)
  968     result << %(<div class="title">#{node.title}</div>) if node.title?
  969     result << %(<ul#{ul_class_attribute}>)
  970 
  971     node.items.each do |item|
  972       if item.id
  973         result << %(<li id="#{item.id}"#{item.role ? %[ class="#{item.role}"] : ''}>)
  974       elsif item.role
  975         result << %(<li class="#{item.role}">)
  976       else
  977         result << '<li>'
  978       end
  979       if checklist && (item.attr? 'checkbox')
  980         result << %(<p>#{(item.attr? 'checked') ? marker_checked : marker_unchecked}#{item.text}</p>)
  981       else
  982         result << %(<p>#{item.text}</p>)
  983       end
  984       result << item.content if item.blocks?
  985       result << '</li>'
  986     end
  987 
  988     result << '</ul>'
  989     result << '</div>'
  990     result.join LF
  991   end
  992 
  993   def convert_verse node
  994     id_attribute = node.id ? %( id="#{node.id}") : ''
  995     classes = ['verseblock', node.role].compact
  996     class_attribute = %( class="#{classes.join ' '}")
  997     title_element = node.title? ? %(\n<div class="title">#{node.title}</div>) : ''
  998     attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil
  999     citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil
 1000     if attribution || citetitle
 1001       cite_element = citetitle ? %(<cite>#{citetitle}</cite>) : ''
 1002       attribution_text = attribution ? %(&#8212; #{attribution}#{citetitle ? "<br#{@void_element_slash}>\n" : ''}) : ''
 1003       attribution_element = %(\n<div class="attribution">\n#{attribution_text}#{cite_element}\n</div>)
 1004     else
 1005       attribution_element = ''
 1006     end
 1007 
 1008     %(<div#{id_attribute}#{class_attribute}>#{title_element}
 1009 <pre class="content">#{node.content}</pre>#{attribution_element}
 1010 </div>)
 1011   end
 1012 
 1013   def convert_video node
 1014     xml = @xml_mode
 1015     id_attribute = node.id ? %( id="#{node.id}") : ''
 1016     classes = ['videoblock']
 1017     classes << (node.attr 'float') if node.attr? 'float'
 1018     classes << %(text-#{node.attr 'align'}) if node.attr? 'align'
 1019     classes << node.role if node.role
 1020     class_attribute = %( class="#{classes.join ' '}")
 1021     title_element = node.title? ? %(\n<div class="title">#{node.title}</div>) : ''
 1022     width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : ''
 1023     height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : ''
 1024     case node.attr 'poster'
 1025     when 'vimeo'
 1026       unless (asset_uri_scheme = (node.document.attr 'asset-uri-scheme', 'https')).empty?
 1027         asset_uri_scheme = %(#{asset_uri_scheme}:)
 1028       end
 1029       start_anchor = (node.attr? 'start') ? %(#at=#{node.attr 'start'}) : ''
 1030       delimiter = ['?']
 1031       autoplay_param = (node.option? 'autoplay') ? %(#{delimiter.pop || '&amp;'}autoplay=1) : ''
 1032       loop_param = (node.option? 'loop') ? %(#{delimiter.pop || '&amp;'}loop=1) : ''
 1033       muted_param = (node.option? 'muted') ? %(#{delimiter.pop || '&amp;'}muted=1) : ''
 1034       %(<div#{id_attribute}#{class_attribute}>#{title_element}
 1035 <div class="content">
 1036 <iframe#{width_attribute}#{height_attribute} src="#{asset_uri_scheme}//player.vimeo.com/video/#{node.attr 'target'}#{autoplay_param}#{loop_param}#{muted_param}#{start_anchor}" frameborder="0"#{(node.option? 'nofullscreen') ? '' : (append_boolean_attribute 'allowfullscreen', xml)}></iframe>
 1037 </div>
 1038 </div>)
 1039     when 'youtube'
 1040       unless (asset_uri_scheme = (node.document.attr 'asset-uri-scheme', 'https')).empty?
 1041         asset_uri_scheme = %(#{asset_uri_scheme}:)
 1042       end
 1043       rel_param_val = (node.option? 'related') ? 1 : 0
 1044       # NOTE start and end must be seconds (t parameter allows XmYs where X is minutes and Y is seconds)
 1045       start_param = (node.attr? 'start') ? %(&amp;start=#{node.attr 'start'}) : ''
 1046       end_param = (node.attr? 'end') ? %(&amp;end=#{node.attr 'end'}) : ''
 1047       autoplay_param = (node.option? 'autoplay') ? '&amp;autoplay=1' : ''
 1048       loop_param = (has_loop_param = node.option? 'loop') ? '&amp;loop=1' : ''
 1049       mute_param = (node.option? 'muted') ? '&amp;mute=1' : ''
 1050       controls_param = (node.option? 'nocontrols') ? '&amp;controls=0' : ''
 1051       # cover both ways of controlling fullscreen option
 1052       if node.option? 'nofullscreen'
 1053         fs_param = '&amp;fs=0'
 1054         fs_attribute = ''
 1055       else
 1056         fs_param = ''
 1057         fs_attribute = append_boolean_attribute 'allowfullscreen', xml
 1058       end
 1059       modest_param = (node.option? 'modest') ? '&amp;modestbranding=1' : ''
 1060       theme_param = (node.attr? 'theme') ? %(&amp;theme=#{node.attr 'theme'}) : ''
 1061       hl_param = (node.attr? 'lang') ? %(&amp;hl=#{node.attr 'lang'}) : ''
 1062 
 1063       # parse video_id/list_id syntax where list_id (i.e., playlist) is optional
 1064       target, list = (node.attr 'target').split '/', 2
 1065       if (list ||= (node.attr 'list'))
 1066         list_param = %(&amp;list=#{list})
 1067       else
 1068         # parse dynamic playlist syntax: video_id1,video_id2,...
 1069         target, playlist = target.split ',', 2
 1070         if (playlist ||= (node.attr 'playlist'))
 1071           # INFO playlist bar doesn't appear in Firefox unless showinfo=1 and modestbranding=1
 1072           list_param = %(&amp;playlist=#{playlist})
 1073         else
 1074           # NOTE for loop to work, playlist must be specified; use VIDEO_ID if there's no explicit playlist
 1075           list_param = has_loop_param ? %(&amp;playlist=#{target}) : ''
 1076         end
 1077       end
 1078 
 1079       %(<div#{id_attribute}#{class_attribute}>#{title_element}
 1080 <div class="content">
 1081 <iframe#{width_attribute}#{height_attribute} src="#{asset_uri_scheme}//www.youtube.com/embed/#{target}?rel=#{rel_param_val}#{start_param}#{end_param}#{autoplay_param}#{loop_param}#{mute_param}#{controls_param}#{list_param}#{fs_param}#{modest_param}#{theme_param}#{hl_param}" frameborder="0"#{fs_attribute}></iframe>
 1082 </div>
 1083 </div>)
 1084     else
 1085       poster_attribute = (val = node.attr 'poster').nil_or_empty? ? '' : %( poster="#{node.media_uri val}")
 1086       preload_attribute = (val = node.attr 'preload').nil_or_empty? ? '' : %( preload="#{val}")
 1087       start_t = node.attr 'start'
 1088       end_t = node.attr 'end'
 1089       time_anchor = (start_t || end_t) ? %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''}) : ''
 1090       %(<div#{id_attribute}#{class_attribute}>#{title_element}
 1091 <div class="content">
 1092 <video src="#{node.media_uri(node.attr 'target')}#{time_anchor}"#{width_attribute}#{height_attribute}#{poster_attribute}#{(node.option? 'autoplay') ? (append_boolean_attribute 'autoplay', xml) : ''}#{(node.option? 'nocontrols') ? '' : (append_boolean_attribute 'controls', xml)}#{(node.option? 'loop') ? (append_boolean_attribute 'loop', xml) : ''}#{preload_attribute}>
 1093 Your browser does not support the video tag.
 1094 </video>
 1095 </div>
 1096 </div>)
 1097     end
 1098   end
 1099 
 1100   def convert_inline_anchor node
 1101     case node.type
 1102     when :xref
 1103       if (path = node.attributes['path'])
 1104         attrs = (append_link_constraint_attrs node, node.role ? [%( class="#{node.role}")] : []).join
 1105         text = node.text || path
 1106       else
 1107         attrs = node.role ? %( class="#{node.role}") : ''
 1108         unless (text = node.text)
 1109           refid = node.attributes['refid']
 1110           if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid])
 1111             text = (ref.xreftext node.attr('xrefstyle', nil, true)) || %([#{refid}])
 1112           else
 1113             text = %([#{refid}])
 1114           end
 1115         end
 1116       end
 1117       %(<a href="#{node.target}"#{attrs}>#{text}</a>)
 1118     when :ref
 1119       %(<a id="#{node.id}"></a>)
 1120     when :link
 1121       attrs = node.id ? [%( id="#{node.id}")] : []
 1122       attrs << %( class="#{node.role}") if node.role
 1123       attrs << %( title="#{node.attr 'title'}") if node.attr? 'title'
 1124       %(<a href="#{node.target}"#{(append_link_constraint_attrs node, attrs).join}>#{node.text}</a>)
 1125     when :bibref
 1126       %(<a id="#{node.id}"></a>[#{node.reftext || node.id}])
 1127     else
 1128       logger.warn %(unknown anchor type: #{node.type.inspect})
 1129       nil
 1130     end
 1131   end
 1132 
 1133   def convert_inline_break node
 1134     %(#{node.text}<br#{@void_element_slash}>)
 1135   end
 1136 
 1137   def convert_inline_button node
 1138     %(<b class="button">#{node.text}</b>)
 1139   end
 1140 
 1141   def convert_inline_callout node
 1142     if node.document.attr? 'icons', 'font'
 1143       %(<i class="conum" data-value="#{node.text}"></i><b>(#{node.text})</b>)
 1144     elsif node.document.attr? 'icons'
 1145       src = node.icon_uri("callouts/#{node.text}")
 1146       %(<img src="#{src}" alt="#{node.text}"#{@void_element_slash}>)
 1147     else
 1148       %(#{node.attributes['guard']}<b class="conum">(#{node.text})</b>)
 1149     end
 1150   end
 1151 
 1152   def convert_inline_footnote node
 1153     if (index = node.attr 'index')
 1154       if node.type == :xref
 1155         %(<sup class="footnoteref">[<a class="footnote" href="#_footnotedef_#{index}" title="View footnote.">#{index}</a>]</sup>)
 1156       else
 1157         id_attr = node.id ? %( id="_footnote_#{node.id}") : ''
 1158         %(<sup class="footnote"#{id_attr}>[<a id="_footnoteref_#{index}" class="footnote" href="#_footnotedef_#{index}" title="View footnote.">#{index}</a>]</sup>)
 1159       end
 1160     elsif node.type == :xref
 1161       %(<sup class="footnoteref red" title="Unresolved footnote reference.">[#{node.text}]</sup>)
 1162     end
 1163   end
 1164 
 1165   def convert_inline_image node
 1166     if (type = node.type || 'image') == 'icon' && (node.document.attr? 'icons', 'font')
 1167       class_attr_val = %(fa fa-#{node.target})
 1168       { 'size' => 'fa-', 'rotate' => 'fa-rotate-', 'flip' => 'fa-flip-' }.each do |key, prefix|
 1169         class_attr_val = %(#{class_attr_val} #{prefix}#{node.attr key}) if node.attr? key
 1170       end
 1171       title_attr = (node.attr? 'title') ? %( title="#{node.attr 'title'}") : ''
 1172       img = %(<i class="#{class_attr_val}"#{title_attr}></i>)
 1173     elsif type == 'icon' && !(node.document.attr? 'icons')
 1174       img = %([#{node.alt}])
 1175     else
 1176       target = node.target
 1177       attrs = ['width', 'height', 'title'].map {|name| (node.attr? name) ? %( #{name}="#{node.attr name}") : '' }.join
 1178       if type != 'icon' && ((node.attr? 'format', 'svg') || (target.include? '.svg')) &&
 1179           node.document.safe < SafeMode::SECURE && ((svg = (node.option? 'inline')) || (obj = (node.option? 'interactive')))
 1180         if svg
 1181           img = (read_svg_contents node, target) || %(<span class="alt">#{node.alt}</span>)
 1182         elsif obj
 1183           fallback = (node.attr? 'fallback') ? %(<img src="#{node.image_uri(node.attr 'fallback')}" alt="#{encode_attribute_value node.alt}"#{attrs}#{@void_element_slash}>) : %(<span class="alt">#{node.alt}</span>)
 1184           img = %(<object type="image/svg+xml" data="#{node.image_uri target}"#{attrs}>#{fallback}</object>)
 1185         end
 1186       end
 1187       img ||= %(<img src="#{type == 'icon' ? (node.icon_uri target) : (node.image_uri target)}" alt="#{encode_attribute_value node.alt}"#{attrs}#{@void_element_slash}>)
 1188     end
 1189     if node.attr? 'link'
 1190       img = %(<a class="image" href="#{node.attr 'link'}"#{(append_link_constraint_attrs node).join}>#{img}</a>)
 1191     end
 1192     if (role = node.role)
 1193       if node.attr? 'float'
 1194         class_attr_val = %(#{type} #{node.attr 'float'} #{role})
 1195       else
 1196         class_attr_val = %(#{type} #{role})
 1197       end
 1198     elsif node.attr? 'float'
 1199       class_attr_val = %(#{type} #{node.attr 'float'})
 1200     else
 1201       class_attr_val = type
 1202     end
 1203     %(<span class="#{class_attr_val}">#{img}</span>)
 1204   end
 1205 
 1206   def convert_inline_indexterm node
 1207     node.type == :visible ? node.text : ''
 1208   end
 1209 
 1210   def convert_inline_kbd node
 1211     if (keys = node.attr 'keys').size == 1
 1212       %(<kbd>#{keys[0]}</kbd>)
 1213     else
 1214       %(<span class="keyseq"><kbd>#{keys.join '</kbd>+<kbd>'}</kbd></span>)
 1215     end
 1216   end
 1217 
 1218   def convert_inline_menu node
 1219     caret = (node.document.attr? 'icons', 'font') ? '&#160;<i class="fa fa-angle-right caret"></i> ' : '&#160;<b class="caret">&#8250;</b> '
 1220     submenu_joiner = %(</b>#{caret}<b class="submenu">)
 1221     menu = node.attr 'menu'
 1222     if (submenus = node.attr 'submenus').empty?
 1223       if (menuitem = node.attr 'menuitem')
 1224         %(<span class="menuseq"><b class="menu">#{menu}</b>#{caret}<b class="menuitem">#{menuitem}</b></span>)
 1225       else
 1226         %(<b class="menuref">#{menu}</b>)
 1227       end
 1228     else
 1229       %(<span class="menuseq"><b class="menu">#{menu}</b>#{caret}<b class="submenu">#{submenus.join submenu_joiner}</b>#{caret}<b class="menuitem">#{node.attr 'menuitem'}</b></span>)
 1230     end
 1231   end
 1232 
 1233   def convert_inline_quoted node
 1234     open, close, tag = QUOTE_TAGS[node.type]
 1235     if node.id
 1236       class_attr = node.role ? %( class="#{node.role}") : ''
 1237       if tag
 1238         %(#{open.chop} id="#{node.id}"#{class_attr}>#{node.text}#{close})
 1239       else
 1240         %(<span id="#{node.id}"#{class_attr}>#{open}#{node.text}#{close}</span>)
 1241       end
 1242     elsif node.role
 1243       if tag
 1244         %(#{open.chop} class="#{node.role}">#{node.text}#{close})
 1245       else
 1246         %(<span class="#{node.role}">#{open}#{node.text}#{close}</span>)
 1247       end
 1248     else
 1249       %(#{open}#{node.text}#{close})
 1250     end
 1251   end
 1252 
 1253   # NOTE expose read_svg_contents for Bespoke converter
 1254   def read_svg_contents node, target
 1255     if (svg = node.read_contents target, start: (node.document.attr 'imagesdir'), normalize: true, label: 'SVG')
 1256       svg = svg.sub SvgPreambleRx, '' unless svg.start_with? '<svg'
 1257       old_start_tag = new_start_tag = nil
 1258       # NOTE width, height and style attributes are removed if either width or height is specified
 1259       ['width', 'height'].each do |dim|
 1260         if node.attr? dim
 1261           new_start_tag = (old_start_tag = (svg.match SvgStartTagRx)[0]).gsub DimensionAttributeRx, '' unless new_start_tag
 1262           # QUESTION should we add px since it's already the default?
 1263           new_start_tag = %(#{new_start_tag.chop} #{dim}="#{node.attr dim}px">)
 1264         end
 1265       end
 1266       svg = %(#{new_start_tag}#{svg[old_start_tag.length..-1]}) if new_start_tag
 1267     end
 1268     svg
 1269   end
 1270 
 1271   private
 1272 
 1273   def append_boolean_attribute name, xml
 1274     xml ? %( #{name}="#{name}") : %( #{name})
 1275   end
 1276 
 1277   def append_link_constraint_attrs node, attrs = []
 1278     rel = 'nofollow' if node.option? 'nofollow'
 1279     if (window = node.attributes['window'])
 1280       attrs << %( target="#{window}")
 1281       attrs << (rel ? %( rel="#{rel} noopener") : ' rel="noopener"') if window == '_blank' || (node.option? 'noopener')
 1282     elsif rel
 1283       attrs << %( rel="#{rel}")
 1284     end
 1285     attrs
 1286   end
 1287 
 1288   def encode_attribute_value val
 1289     (val.include? '"') ? (val.gsub '"', '&quot;') : val
 1290   end
 1291 
 1292   def generate_manname_section node
 1293     manname_title = node.attr 'manname-title', 'Name'
 1294     if (next_section = node.sections[0]) && (next_section_title = next_section.title) == next_section_title.upcase
 1295       manname_title = manname_title.upcase
 1296     end
 1297     manname_id_attr = (manname_id = node.attr 'manname-id') ? %( id="#{manname_id}") : ''
 1298     %(<h2#{manname_id_attr}>#{manname_title}</h2>
 1299 <div class="sectionbody">
 1300 <p>#{node.attr 'manname'} - #{node.attr 'manpurpose'}</p>
 1301 </div>)
 1302   end
 1303 
 1304   # NOTE adapt to older converters that relied on unprefixed method names
 1305   def method_missing id, *params
 1306     !((name = id.to_s).start_with? 'convert_') && (handles? name) ? (send %(convert_#{name}), *params) : super
 1307   end
 1308 end
 1309 end