# frozen_string_literal: true module Asciidoctor # A built-in {Converter} implementation that generates HTML 5 output # consistent with the html5 backend from AsciiDoc Python. class Converter::Html5Converter < Converter::Base register_for 'html5' (QUOTE_TAGS = { monospaced: ['', '', true], emphasis: ['', '', true], strong: ['', '', true], double: ['“', '”'], single: ['‘', '’'], mark: ['', '', true], superscript: ['', '', true], subscript: ['', '', true], asciimath: ['\$', '\$'], latexmath: ['\(', '\)'], # Opal can't resolve these constants when referenced here #asciimath: INLINE_MATH_DELIMITERS[:asciimath] + [false], #latexmath: INLINE_MATH_DELIMITERS[:latexmath] + [false], }).default = ['', ''] DropAnchorRx = /<(?:a[^>+]+|\/a)>/ StemBreakRx = / *\\\n(?:\\?\n)*|\n\n+/ SvgPreambleRx = /\A.*?(?=]*>/ DimensionAttributeRx = /\s(?:width|height|style)=(["']).*?\1/ def initialize backend, opts = {} @backend = backend if opts[:htmlsyntax] == 'xml' syntax = 'xml' @xml_mode = true @void_element_slash = '/' else syntax = 'html' @xml_mode = nil @void_element_slash = '' end init_backend_traits basebackend: 'html', filetype: 'html', htmlsyntax: syntax, outfilesuffix: '.html', supports_templates: true end def convert node, transform = node.node_name, opts = nil if transform == 'inline_quoted'; return convert_inline_quoted node elsif transform == 'paragraph'; return convert_paragraph node elsif transform == 'inline_anchor'; return convert_inline_anchor node elsif transform == 'section'; return convert_section node elsif transform == 'listing'; return convert_listing node elsif transform == 'literal'; return convert_literal node elsif transform == 'ulist'; return convert_ulist node elsif transform == 'olist'; return convert_olist node elsif transform == 'dlist'; return convert_dlist node elsif transform == 'admonition'; return convert_admonition node elsif transform == 'colist'; return convert_colist node elsif transform == 'embedded'; return convert_embedded node elsif transform == 'example'; return convert_example node elsif transform == 'floating_title'; return convert_floating_title node elsif transform == 'image'; return convert_image node elsif transform == 'inline_break'; return convert_inline_break node elsif transform == 'inline_button'; return convert_inline_button node elsif transform == 'inline_callout'; return convert_inline_callout node elsif transform == 'inline_footnote'; return convert_inline_footnote node elsif transform == 'inline_image'; return convert_inline_image node elsif transform == 'inline_indexterm'; return convert_inline_indexterm node elsif transform == 'inline_kbd'; return convert_inline_kbd node elsif transform == 'inline_menu'; return convert_inline_menu node elsif transform == 'open'; return convert_open node elsif transform == 'page_break'; return convert_page_break node elsif transform == 'preamble'; return convert_preamble node elsif transform == 'quote'; return convert_quote node elsif transform == 'sidebar'; return convert_sidebar node elsif transform == 'stem'; return convert_stem node elsif transform == 'table'; return convert_table node elsif transform == 'thematic_break'; return convert_thematic_break node elsif transform == 'verse'; return convert_verse node elsif transform == 'video'; return convert_video node elsif transform == 'document'; return convert_document node elsif transform == 'toc'; return convert_toc node elsif transform == 'pass'; return convert_pass node elsif transform == 'audio'; return convert_audio node else; return super end end def convert_document node br = %() unless (asset_uri_scheme = (node.attr 'asset-uri-scheme', 'https')).empty? asset_uri_scheme = %(#{asset_uri_scheme}:) end cdn_base_url = %(#{asset_uri_scheme}//cdnjs.cloudflare.com/ajax/libs) linkcss = node.attr? 'linkcss' result = [''] lang_attribute = (node.attr? 'nolang') ? '' : %( lang="#{node.attr 'lang', 'en'}") result << %() result << %( ) result << %() if node.attr? 'app-name' result << %() if node.attr? 'description' result << %() if node.attr? 'keywords' result << %() if node.attr? 'authors' result << %() if node.attr? 'copyright' if node.attr? 'favicon' if (icon_href = node.attr 'favicon').empty? icon_href, icon_type = 'favicon.ico', 'image/x-icon' else icon_type = (icon_ext = ::File.extname icon_href) == '.ico' ? 'image/x-icon' : %(image/#{icon_ext.slice 1, icon_ext.length}) end result << %() end result << %(#{node.doctitle sanitize: true, use_fallback: true}) if DEFAULT_STYLESHEET_KEYS.include?(node.attr 'stylesheet') if (webfonts = node.attr 'webfonts') result << %() end if linkcss result << %() else result << %() end elsif node.attr? 'stylesheet' if linkcss result << %() else result << %() end end if node.attr? 'icons', 'font' if node.attr? 'iconfont-remote' result << %() else iconfont_stylesheet = %(#{node.attr 'iconfont-name', 'font-awesome'}.css) result << %() end end if (syntax_hl = node.syntax_highlighter) && (syntax_hl.docinfo? :head) result << (syntax_hl.docinfo :head, node, linkcss: linkcss) end unless (docinfo_content = node.docinfo).empty? result << docinfo_content end result << '' body_attrs = node.id ? [%(id="#{node.id}")] : [] if (sectioned = node.sections?) && (node.attr? 'toc-class') && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto') classes = [node.doctype, (node.attr 'toc-class'), %(toc-#{node.attr 'toc-position', 'header'})] else classes = [node.doctype] end classes << node.role if node.role? body_attrs << %(class="#{classes.join ' '}") body_attrs << %(style="max-width: #{node.attr 'max-width'};") if node.attr? 'max-width' result << %() unless (docinfo_content = node.docinfo :header).empty? result << docinfo_content end unless node.noheader result << '' if node.doctype == 'manpage' result << %(#{node.doctitle} Manual Page) if sectioned && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto') result << %( #{node.attr 'toc-title'} #{convert_outline node} ) end result << (generate_manname_section node) if node.attr? 'manpurpose' else if node.header? result << %(#{node.header.title}) unless node.notitle details = [] idx = 1 node.authors.each do |author| details << %(#{node.sub_replacements author.name}#{br}) details << %(#{node.sub_macros author.email}#{br}) if author.email idx += 1 end if node.attr? 'revnumber' details << %(#{((node.attr 'version-label') || '').downcase} #{node.attr 'revnumber'}#{(node.attr? 'revdate') ? ',' : ''}) end if node.attr? 'revdate' details << %(#{node.attr 'revdate'}) end if node.attr? 'revremark' details << %(#{br}#{node.attr 'revremark'}) end unless details.empty? result << '' result.concat details result << '' end end if sectioned && (node.attr? 'toc') && (node.attr? 'toc-placement', 'auto') result << %( #{node.attr 'toc-title'} #{convert_outline node} ) end end result << '' end result << %( #{node.content} ) if node.footnotes? && !(node.attr? 'nofootnotes') result << %( ) node.footnotes.each do |footnote| result << %( #{footnote.index}. #{footnote.text} ) end result << '' end unless node.nofooter result << '' end # JavaScript (and auxiliary stylesheets) loaded at the end of body for performance reasons # See http://www.html5rocks.com/en/tutorials/speed/script-loading/ if syntax_hl && (syntax_hl.docinfo? :footer) result << (syntax_hl.docinfo :footer, node, cdn_base_url: cdn_base_url, linkcss: linkcss, self_closing_tag_slash: slash) end if node.attr? 'stem' eqnums_val = node.attr 'eqnums', 'none' eqnums_val = 'AMS' if eqnums_val.empty? eqnums_opt = %( equationNumbers: { autoNumber: "#{eqnums_val}" } ) # IMPORTANT inspect calls on delimiter arrays are intentional for JavaScript compat (emulates JSON.stringify) result << %( ) end unless (docinfo_content = node.docinfo :footer).empty? result << docinfo_content end result << '' result << '' result.join LF end def convert_embedded node result = [] if node.doctype == 'manpage' # QUESTION should notitle control the manual page title? unless node.notitle id_attr = node.id ? %( id="#{node.id}") : '' result << %(#{node.doctitle} Manual Page) end result << (generate_manname_section node) if node.attr? 'manpurpose' elsif node.header? && !node.notitle id_attr = node.id ? %( id="#{node.id}") : '' result << %(#{node.header.title}) end if node.sections? && (node.attr? 'toc') && (toc_p = node.attr 'toc-placement') != 'macro' && toc_p != 'preamble' result << %( #{node.attr 'toc-title'} #{convert_outline node} ) end result << node.content if node.footnotes? && !(node.attr? 'nofootnotes') result << %( ) node.footnotes.each do |footnote| result << %( #{footnote.index}. #{footnote.text} ) end result << '' end result.join LF end def convert_outline node, opts = {} return unless node.sections? sectnumlevels = opts[:sectnumlevels] || (node.document.attributes['sectnumlevels'] || 3).to_i toclevels = opts[:toclevels] || (node.document.attributes['toclevels'] || 2).to_i sections = node.sections # FIXME top level is incorrect if a multipart book starts with a special section defined at level 0 result = [%()] sections.each do |section| slevel = section.level if section.caption stitle = section.captioned_title elsif section.numbered && slevel <= sectnumlevels if slevel < 2 && node.document.doctype == 'book' if section.sectname == 'chapter' stitle = %(#{(signifier = node.document.attributes['chapter-signifier']) ? "#{signifier} " : ''}#{section.sectnum} #{section.title}) elsif section.sectname == 'part' stitle = %(#{(signifier = node.document.attributes['part-signifier']) ? "#{signifier} " : ''}#{section.sectnum nil, ':'} #{section.title}) else stitle = %(#{section.sectnum} #{section.title}) end else stitle = %(#{section.sectnum} #{section.title}) end else stitle = section.title end stitle = stitle.gsub DropAnchorRx, '' if stitle.include? '#{stitle}) result << child_toc_level result << '' else result << %(#{stitle}) end end result << '' result.join LF end def convert_section node doc_attrs = node.document.attributes level = node.level if node.caption title = node.captioned_title elsif node.numbered && level <= (doc_attrs['sectnumlevels'] || 3).to_i if level < 2 && node.document.doctype == 'book' if node.sectname == 'chapter' title = %(#{(signifier = doc_attrs['chapter-signifier']) ? "#{signifier} " : ''}#{node.sectnum} #{node.title}) elsif node.sectname == 'part' title = %(#{(signifier = doc_attrs['part-signifier']) ? "#{signifier} " : ''}#{node.sectnum nil, ':'} #{node.title}) else title = %(#{node.sectnum} #{node.title}) end else title = %(#{node.sectnum} #{node.title}) end else title = node.title end if node.id id_attr = %( id="#{id = node.id}") if doc_attrs['sectlinks'] title = %(#{title}) end if doc_attrs['sectanchors'] # QUESTION should we add a font-based icon in anchor if icons=font? if doc_attrs['sectanchors'] == 'after' title = %(#{title}) else title = %(#{title}) end end else id_attr = '' end if level == 0 %(#{title} #{node.content}) else %( #{title} #{level == 1 ? %[ #{node.content} ] : node.content} ) end end def convert_admonition node id_attr = node.id ? %( id="#{node.id}") : '' name = node.attr 'name' title_element = node.title? ? %(#{node.title}\n) : '' if node.document.attr? 'icons' if (node.document.attr? 'icons', 'font') && !(node.attr? 'icon') label = %() else label = %() end else label = %(#{node.attr 'textlabel'}) end %( #{label} #{title_element}#{node.content} ) end def convert_audio node xml = @xml_mode id_attribute = node.id ? %( id="#{node.id}") : '' classes = ['audioblock', node.role].compact class_attribute = %( class="#{classes.join ' '}") title_element = node.title? ? %(#{node.title}\n) : '' start_t = node.attr 'start' end_t = node.attr 'end' time_anchor = (start_t || end_t) ? %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''}) : '' %( #{title_element} Your browser does not support the audio tag. ) end def convert_colist node result = [] id_attribute = node.id ? %( id="#{node.id}") : '' classes = ['colist', node.style, node.role].compact class_attribute = %( class="#{classes.join ' '}") result << %() result << %(#{node.title}) if node.title? if node.document.attr? 'icons' result << '' font_icons, num = (node.document.attr? 'icons', 'font'), 0 node.items.each do |item| num += 1 if font_icons num_label = %(#{num}) else num_label = %() end result << %( #{num_label} #{item.text}#{item.blocks? ? LF + item.content : ''} ) end result << '' else result << '' node.items.each do |item| result << %( #{item.text}#{item.blocks? ? LF + item.content : ''} ) end result << '' end result << '' result.join LF end def convert_dlist node result = [] id_attribute = node.id ? %( id="#{node.id}") : '' classes = case node.style when 'qanda' ['qlist', 'qanda', node.role] when 'horizontal' ['hdlist', node.role] else ['dlist', node.style, node.role] end.compact class_attribute = %( class="#{classes.join ' '}") result << %() result << %(#{node.title}) if node.title? case node.style when 'qanda' result << '' node.items.each do |terms, dd| result << '' terms.each do |dt| result << %(#{dt.text}) end if dd result << %(#{dd.text}) if dd.text? result << dd.content if dd.blocks? end result << '' end result << '' when 'horizontal' slash = @void_element_slash result << '' if (node.attr? 'labelwidth') || (node.attr? 'itemwidth') result << '' col_style_attribute = (node.attr? 'labelwidth') ? %( style="width: #{(node.attr 'labelwidth').chomp '%'}%;") : '' result << %() col_style_attribute = (node.attr? 'itemwidth') ? %( style="width: #{(node.attr 'itemwidth').chomp '%'}%;") : '' result << %() result << '' end node.items.each do |terms, dd| result << '' result << %() first_term = true terms.each do |dt| result << %() unless first_term result << dt.text first_term = nil end result << '' result << '' if dd result << %(#{dd.text}) if dd.text? result << dd.content if dd.blocks? end result << '' result << '' end result << '' else result << '' dt_style_attribute = node.style ? '' : ' class="hdlist1"' node.items.each do |terms, dd| terms.each do |dt| result << %(#{dt.text}) end if dd result << '' result << %(#{dd.text}) if dd.text? result << dd.content if dd.blocks? result << '' end end result << '' end result << '' result.join LF end def convert_example node id_attribute = node.id ? %( id="#{node.id}") : '' if node.option? 'collapsible' class_attribute = node.role ? %( class="#{node.role}") : '' summary_element = node.title? ? %(#{node.title}) : 'Details' %( #{summary_element} #{node.content} ) else title_element = node.title? ? %(#{node.captioned_title}\n) : '' %( #{title_element} #{node.content} ) end end def convert_floating_title node tag_name = %(h#{node.level + 1}) id_attribute = node.id ? %( id="#{node.id}") : '' classes = [node.style, node.role].compact %(<#{tag_name}#{id_attribute} class="#{classes.join ' '}">#{node.title}#{tag_name}>) end def convert_image node target = node.attr 'target' width_attr = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : '' height_attr = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : '' if ((node.attr? 'format', 'svg') || (target.include? '.svg')) && node.document.safe < SafeMode::SECURE && ((svg = (node.option? 'inline')) || (obj = (node.option? 'interactive'))) if svg img = (read_svg_contents node, target) || %(#{node.alt}) elsif obj fallback = (node.attr? 'fallback') ? %() : %(#{node.alt}) img = %(#{fallback}) end end img ||= %() if node.attr? 'link' img = %(#{img}) end id_attr = node.id ? %( id="#{node.id}") : '' classes = ['imageblock'] classes << (node.attr 'float') if node.attr? 'float' classes << %(text-#{node.attr 'align'}) if node.attr? 'align' classes << node.role if node.role class_attr = %( class="#{classes.join ' '}") title_el = node.title? ? %(\n#{node.captioned_title}) : '' %( #{img} #{title_el} ) end def convert_listing node nowrap = (node.option? 'nowrap') || !(node.document.attr? 'prewrap') if node.style == 'source' lang = node.attr 'language' if (syntax_hl = node.document.syntax_highlighter) opts = syntax_hl.highlight? ? { css_mode: ((doc_attrs = node.document.attributes)[%(#{syntax_hl.name}-css)] || :class).to_sym, style: doc_attrs[%(#{syntax_hl.name}-style)], } : {} opts[:nowrap] = nowrap else pre_open = %() pre_close = '' end else pre_open = %() pre_close = '' end id_attribute = node.id ? %( id="#{node.id}") : '' title_element = node.title? ? %(#{node.captioned_title}\n) : '' %( #{title_element} #{syntax_hl ? (syntax_hl.format node, lang, opts) : pre_open + (node.content || '') + pre_close} ) end def convert_literal node id_attribute = node.id ? %( id="#{node.id}") : '' title_element = node.title? ? %(#{node.title}\n) : '' nowrap = !(node.document.attr? 'prewrap') || (node.option? 'nowrap') %( #{title_element} #{node.content} ) end def convert_stem node id_attribute = node.id ? %( id="#{node.id}") : '' title_element = node.title? ? %(#{node.title}\n) : '' open, close = BLOCK_MATH_DELIMITERS[style = node.style.to_sym] if (equation = node.content) if style == :asciimath && (equation.include? LF) br = %(#{LF}) equation = equation.gsub(StemBreakRx) { %(#{close}#{br * ($&.count LF)}#{open}) } end unless (equation.start_with? open) && (equation.end_with? close) equation = %(#{open}#{equation}#{close}) end else equation = '' end %( #{title_element} #{equation} ) end def convert_olist node result = [] id_attribute = node.id ? %( id="#{node.id}") : '' classes = ['olist', node.style, node.role].compact class_attribute = %( class="#{classes.join ' '}") result << %() result << %(#{node.title}) if node.title? type_attribute = (keyword = node.list_marker_keyword) ? %( type="#{keyword}") : '' start_attribute = (node.attr? 'start') ? %( start="#{node.attr 'start'}") : '' reversed_attribute = (node.option? 'reversed') ? (append_boolean_attribute 'reversed', @xml_mode) : '' result << %() node.items.each do |item| if item.id result << %() elsif item.role result << %() else result << '' end result << %(#{item.text}) result << item.content if item.blocks? result << '' end result << '' result << '' result.join LF end def convert_open node if (style = node.style) == 'abstract' if node.parent == node.document && node.document.doctype == 'book' logger.warn 'abstract block cannot be used in a document without a title when doctype is book. Excluding block content.' '' else id_attr = node.id ? %( id="#{node.id}") : '' title_el = node.title? ? %(#{node.title}\n) : '' %( #{title_el} #{node.content} ) end elsif style == 'partintro' && (node.level > 0 || node.parent.context != :section || node.document.doctype != 'book') logger.error 'partintro block can only be used when doctype is book and must be a child of a book part. Excluding block content.' '' else id_attr = node.id ? %( id="#{node.id}") : '' title_el = node.title? ? %(#{node.title}\n) : '' %( #{title_el} #{node.content} ) end end def convert_page_break node '' end def convert_paragraph node if node.role attributes = %(#{node.id ? %[ id="#{node.id}"] : ''} class="paragraph #{node.role}") elsif node.id attributes = %( id="#{node.id}" class="paragraph") else attributes = ' class="paragraph"' end if node.title? %( #{node.title} #{node.content} ) else %( #{node.content} ) end end alias convert_pass content_only def convert_preamble node if (doc = node.document).attr?('toc-placement', 'preamble') && doc.sections? && (doc.attr? 'toc') toc = %( #{doc.attr 'toc-title'} #{convert_outline doc} ) else toc = '' end %( #{node.content} #{toc} ) end def convert_quote node id_attribute = node.id ? %( id="#{node.id}") : '' classes = ['quoteblock', node.role].compact class_attribute = %( class="#{classes.join ' '}") title_element = node.title? ? %(\n#{node.title}) : '' attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil if attribution || citetitle cite_element = citetitle ? %(#{citetitle}) : '' attribution_text = attribution ? %(— #{attribution}#{citetitle ? "\n" : ''}) : '' attribution_element = %(\n\n#{attribution_text}#{cite_element}\n) else attribution_element = '' end %(#{title_element} #{node.content} #{attribution_element} ) end def convert_thematic_break node %() end def convert_sidebar node id_attribute = node.id ? %( id="#{node.id}") : '' title_element = node.title? ? %(#{node.title}\n) : '' %( #{title_element}#{node.content} ) end def convert_table node result = [] id_attribute = node.id ? %( id="#{node.id}") : '' classes = ['tableblock', %(frame-#{node.attr 'frame', 'all', 'table-frame'}), %(grid-#{node.attr 'grid', 'all', 'table-grid'})] if (stripes = node.attr 'stripes', nil, 'table-stripes') classes << %(stripes-#{stripes}) end styles = [] if (autowidth = node.option? 'autowidth') && !(node.attr? 'width') classes << 'fit-content' elsif (tablewidth = node.attr 'tablepcwidth') == 100 classes << 'stretch' else styles << %(width: #{tablewidth}%;) end classes << (node.attr 'float') if node.attr? 'float' if (role = node.role) classes << role end class_attribute = %( class="#{classes.join ' '}") style_attribute = styles.empty? ? '' : %( style="#{styles.join ' '}") result << %() result << %(#{node.captioned_title}) if node.title? if (node.attr 'rowcount') > 0 slash = @void_element_slash result << '' if autowidth result += (Array.new node.columns.size, %()) else node.columns.each do |col| result << ((col.option? 'autowidth') ? %() : %()) end end result << '' node.rows.to_h.each do |tsec, rows| next if rows.empty? result << %() rows.each do |row| result << '' row.each do |cell| if tsec == :head cell_content = cell.text else case cell.style when :asciidoc cell_content = %(#{cell.content}) when :literal cell_content = %(#{cell.text}) else cell_content = (cell_content = cell.content).empty? ? '' : %(#{cell_content.join ' '}) end end cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td') cell_class_attribute = %( class="tableblock halign-#{cell.attr 'halign'} valign-#{cell.attr 'valign'}") cell_colspan_attribute = cell.colspan ? %( colspan="#{cell.colspan}") : '' cell_rowspan_attribute = cell.rowspan ? %( rowspan="#{cell.rowspan}") : '' cell_style_attribute = (node.document.attr? 'cellbgcolor') ? %( style="background-color: #{node.document.attr 'cellbgcolor'};") : '' result << %(<#{cell_tag_name}#{cell_class_attribute}#{cell_colspan_attribute}#{cell_rowspan_attribute}#{cell_style_attribute}>#{cell_content}#{cell_tag_name}>) end result << '' end result << %() end end result << '' result.join LF end def convert_toc node unless (doc = node.document).attr?('toc-placement', 'macro') && doc.sections? && (doc.attr? 'toc') return '' end if node.id id_attr = %( id="#{node.id}") title_id_attr = %( id="#{node.id}title") else id_attr = ' id="toc"' title_id_attr = ' id="toctitle"' end title = node.title? ? node.title : (doc.attr 'toc-title') levels = (node.attr? 'levels') ? (node.attr 'levels').to_i : nil role = node.role? ? node.role : (doc.attr 'toc-class', 'toc') %( #{title} #{convert_outline doc, toclevels: levels} ) end def convert_ulist node result = [] id_attribute = node.id ? %( id="#{node.id}") : '' div_classes = ['ulist', node.style, node.role].compact marker_checked = marker_unchecked = '' if (checklist = node.option? 'checklist') div_classes.unshift div_classes.shift, 'checklist' ul_class_attribute = ' class="checklist"' if node.option? 'interactive' if @xml_mode marker_checked = ' ' marker_unchecked = ' ' else marker_checked = ' ' marker_unchecked = ' ' end elsif node.document.attr? 'icons', 'font' marker_checked = ' ' marker_unchecked = ' ' else marker_checked = '✓ ' marker_unchecked = '❏ ' end else ul_class_attribute = node.style ? %( class="#{node.style}") : '' end result << %() result << %(#{node.title}) if node.title? result << %() node.items.each do |item| if item.id result << %() elsif item.role result << %() else result << '' end if checklist && (item.attr? 'checkbox') result << %(#{(item.attr? 'checked') ? marker_checked : marker_unchecked}#{item.text}) else result << %(#{item.text}) end result << item.content if item.blocks? result << '' end result << '' result << '' result.join LF end def convert_verse node id_attribute = node.id ? %( id="#{node.id}") : '' classes = ['verseblock', node.role].compact class_attribute = %( class="#{classes.join ' '}") title_element = node.title? ? %(\n#{node.title}) : '' attribution = (node.attr? 'attribution') ? (node.attr 'attribution') : nil citetitle = (node.attr? 'citetitle') ? (node.attr 'citetitle') : nil if attribution || citetitle cite_element = citetitle ? %(#{citetitle}) : '' attribution_text = attribution ? %(— #{attribution}#{citetitle ? "\n" : ''}) : '' attribution_element = %(\n\n#{attribution_text}#{cite_element}\n) else attribution_element = '' end %(#{title_element} #{node.content}#{attribution_element} ) end def convert_video node xml = @xml_mode id_attribute = node.id ? %( id="#{node.id}") : '' classes = ['videoblock'] classes << (node.attr 'float') if node.attr? 'float' classes << %(text-#{node.attr 'align'}) if node.attr? 'align' classes << node.role if node.role class_attribute = %( class="#{classes.join ' '}") title_element = node.title? ? %(\n#{node.title}) : '' width_attribute = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : '' height_attribute = (node.attr? 'height') ? %( height="#{node.attr 'height'}") : '' case node.attr 'poster' when 'vimeo' unless (asset_uri_scheme = (node.document.attr 'asset-uri-scheme', 'https')).empty? asset_uri_scheme = %(#{asset_uri_scheme}:) end start_anchor = (node.attr? 'start') ? %(#at=#{node.attr 'start'}) : '' delimiter = ['?'] autoplay_param = (node.option? 'autoplay') ? %(#{delimiter.pop || '&'}autoplay=1) : '' loop_param = (node.option? 'loop') ? %(#{delimiter.pop || '&'}loop=1) : '' muted_param = (node.option? 'muted') ? %(#{delimiter.pop || '&'}muted=1) : '' %(#{title_element} ) when 'youtube' unless (asset_uri_scheme = (node.document.attr 'asset-uri-scheme', 'https')).empty? asset_uri_scheme = %(#{asset_uri_scheme}:) end rel_param_val = (node.option? 'related') ? 1 : 0 # NOTE start and end must be seconds (t parameter allows XmYs where X is minutes and Y is seconds) start_param = (node.attr? 'start') ? %(&start=#{node.attr 'start'}) : '' end_param = (node.attr? 'end') ? %(&end=#{node.attr 'end'}) : '' autoplay_param = (node.option? 'autoplay') ? '&autoplay=1' : '' loop_param = (has_loop_param = node.option? 'loop') ? '&loop=1' : '' mute_param = (node.option? 'muted') ? '&mute=1' : '' controls_param = (node.option? 'nocontrols') ? '&controls=0' : '' # cover both ways of controlling fullscreen option if node.option? 'nofullscreen' fs_param = '&fs=0' fs_attribute = '' else fs_param = '' fs_attribute = append_boolean_attribute 'allowfullscreen', xml end modest_param = (node.option? 'modest') ? '&modestbranding=1' : '' theme_param = (node.attr? 'theme') ? %(&theme=#{node.attr 'theme'}) : '' hl_param = (node.attr? 'lang') ? %(&hl=#{node.attr 'lang'}) : '' # parse video_id/list_id syntax where list_id (i.e., playlist) is optional target, list = (node.attr 'target').split '/', 2 if (list ||= (node.attr 'list')) list_param = %(&list=#{list}) else # parse dynamic playlist syntax: video_id1,video_id2,... target, playlist = target.split ',', 2 if (playlist ||= (node.attr 'playlist')) # INFO playlist bar doesn't appear in Firefox unless showinfo=1 and modestbranding=1 list_param = %(&playlist=#{playlist}) else # NOTE for loop to work, playlist must be specified; use VIDEO_ID if there's no explicit playlist list_param = has_loop_param ? %(&playlist=#{target}) : '' end end %(#{title_element} ) else poster_attribute = (val = node.attr 'poster').nil_or_empty? ? '' : %( poster="#{node.media_uri val}") preload_attribute = (val = node.attr 'preload').nil_or_empty? ? '' : %( preload="#{val}") start_t = node.attr 'start' end_t = node.attr 'end' time_anchor = (start_t || end_t) ? %(#t=#{start_t || ''}#{end_t ? ",#{end_t}" : ''}) : '' %(#{title_element} Your browser does not support the video tag. ) end end def convert_inline_anchor node case node.type when :xref if (path = node.attributes['path']) attrs = (append_link_constraint_attrs node, node.role ? [%( class="#{node.role}")] : []).join text = node.text || path else attrs = node.role ? %( class="#{node.role}") : '' unless (text = node.text) refid = node.attributes['refid'] if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid]) text = (ref.xreftext node.attr('xrefstyle', nil, true)) || %([#{refid}]) else text = %([#{refid}]) end end end %(#{text}) when :ref %() when :link attrs = node.id ? [%( id="#{node.id}")] : [] attrs << %( class="#{node.role}") if node.role attrs << %( title="#{node.attr 'title'}") if node.attr? 'title' %(#{node.text}) when :bibref %([#{node.reftext || node.id}]) else logger.warn %(unknown anchor type: #{node.type.inspect}) nil end end def convert_inline_break node %(#{node.text}) end def convert_inline_button node %(#{node.text}) end def convert_inline_callout node if node.document.attr? 'icons', 'font' %((#{node.text})) elsif node.document.attr? 'icons' src = node.icon_uri("callouts/#{node.text}") %() else %(#{node.attributes['guard']}(#{node.text})) end end def convert_inline_footnote node if (index = node.attr 'index') if node.type == :xref %([#{index}]) else id_attr = node.id ? %( id="_footnote_#{node.id}") : '' %([#{index}]) end elsif node.type == :xref %([#{node.text}]) end end def convert_inline_image node if (type = node.type) == 'icon' && (node.document.attr? 'icons', 'font') class_attr_val = %(fa fa-#{node.target}) { 'size' => 'fa-', 'rotate' => 'fa-rotate-', 'flip' => 'fa-flip-' }.each do |key, prefix| class_attr_val = %(#{class_attr_val} #{prefix}#{node.attr key}) if node.attr? key end title_attr = (node.attr? 'title') ? %( title="#{node.attr 'title'}") : '' img = %() elsif type == 'icon' && !(node.document.attr? 'icons') img = %([#{node.alt}]) else target = node.target attrs = ['width', 'height', 'title'].map {|name| (node.attr? name) ? %( #{name}="#{node.attr name}") : '' }.join if type != 'icon' && ((node.attr? 'format', 'svg') || (target.include? '.svg')) && node.document.safe < SafeMode::SECURE && ((svg = (node.option? 'inline')) || (obj = (node.option? 'interactive'))) if svg img = (read_svg_contents node, target) || %(#{node.alt}) elsif obj fallback = (node.attr? 'fallback') ? %() : %(#{node.alt}) img = %(#{fallback}) end end img ||= %() end if node.attr? 'link' img = %(#{img}) end if (role = node.role) if node.attr? 'float' class_attr_val = %(#{type} #{node.attr 'float'} #{role}) else class_attr_val = %(#{type} #{role}) end elsif node.attr? 'float' class_attr_val = %(#{type} #{node.attr 'float'}) else class_attr_val = type end %(#{img}) end def convert_inline_indexterm node node.type == :visible ? node.text : '' end def convert_inline_kbd node if (keys = node.attr 'keys').size == 1 %(#{keys[0]}) else %(#{keys.join '+'}) end end def convert_inline_menu node caret = (node.document.attr? 'icons', 'font') ? ' ' : ' › ' submenu_joiner = %(#{caret}) menu = node.attr 'menu' if (submenus = node.attr 'submenus').empty? if (menuitem = node.attr 'menuitem') %(#{menu}#{caret}#{menuitem}) else %(#{menu}) end else %(#{menu}#{caret}#{submenus.join submenu_joiner}#{caret}#{node.attr 'menuitem'}) end end def convert_inline_quoted node open, close, tag = QUOTE_TAGS[node.type] if node.id class_attr = node.role ? %( class="#{node.role}") : '' if tag %(#{open.chop} id="#{node.id}"#{class_attr}>#{node.text}#{close}) else %(#{open}#{node.text}#{close}) end elsif node.role if tag %(#{open.chop} class="#{node.role}">#{node.text}#{close}) else %(#{open}#{node.text}#{close}) end else %(#{open}#{node.text}#{close}) end end # NOTE expose read_svg_contents for Bespoke converter def read_svg_contents node, target if (svg = node.read_contents target, start: (node.document.attr 'imagesdir'), normalize: true, label: 'SVG') svg = svg.sub SvgPreambleRx, '' unless svg.start_with? ') end end svg = %(#{new_start_tag}#{svg[old_start_tag.length..-1]}) if new_start_tag end svg end private def append_boolean_attribute name, xml xml ? %( #{name}="#{name}") : %( #{name}) end def append_link_constraint_attrs node, attrs = [] rel = 'nofollow' if node.option? 'nofollow' if (window = node.attributes['window']) attrs << %( target="#{window}") attrs << (rel ? %( rel="#{rel} noopener") : ' rel="noopener"') if window == '_blank' || (node.option? 'noopener') elsif rel attrs << %( rel="#{rel}") end attrs end def encode_attribute_value val (val.include? '"') ? (val.gsub '"', '"') : val end def generate_manname_section node manname_title = node.attr 'manname-title', 'Name' if (next_section = node.sections[0]) && (next_section_title = next_section.title) == next_section_title.upcase manname_title = manname_title.upcase end manname_id_attr = (manname_id = node.attr 'manname-id') ? %( id="#{manname_id}") : '' %(#{manname_title} #{node.attr 'manname'} - #{node.attr 'manpurpose'} ) end # NOTE adapt to older converters that relied on unprefixed method names def method_missing id, *params !((name = id.to_s).start_with? 'convert_') && (handles? name) ? (send %(convert_#{name}), *params) : super end end end
', '
#{item.text}
#{dt.text}
#{dd.text}
) pre_close = '
#{node.content}
#{cell.text}
#{cell_content.join '
'}
#{(item.attr? 'checked') ? marker_checked : marker_unchecked}#{item.text}
#{node.attr 'manname'} - #{node.attr 'manpurpose'}