"Fossies" - the Fresh Open Source Software Archive

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


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

    1 # frozen_string_literal: true
    2 module Asciidoctor
    3 # A built-in {Converter} implementation that generates the man page (troff) format.
    4 #
    5 # The output follows the groff man page definition while also trying to be
    6 # consistent with the output produced by the a2x tool from AsciiDoc Python.
    7 #
    8 # See http://www.gnu.org/software/groff/manual/html_node/Man-usage.html#Man-usage
    9 class Converter::ManPageConverter < Converter::Base
   10   register_for 'manpage'
   11 
   12   WHITESPACE = %(#{LF}#{TAB} )
   13   ET = ' ' * 8
   14   ESC = ?\u001b # troff leader marker
   15   ESC_BS = %(#{ESC}\\) # escaped backslash (indicates troff formatting sequence)
   16   ESC_FS = %(#{ESC}.)  # escaped full stop (indicates troff macro)
   17 
   18   LiteralBackslashRx = /(?:\A|[^#{ESC}])\\/
   19   LeadingPeriodRx = /^\./
   20   EscapedMacroRx = /^(?:#{ESC}\\c\n)?#{ESC}\.((?:URL|MTO) "#{CC_ANY}*?" "#{CC_ANY}*?" )( |[^\s]*)(#{CC_ANY}*?)(?: *#{ESC}\\c)?$/
   21   MockBoundaryRx = /<\/?BOUNDARY>/
   22   EmDashCharRefRx = /&#8212;(?:&#8203;)?/
   23   EllipsisCharRefRx = /&#8230;(?:&#8203;)?/
   24   WrappedIndentRx = /#{CG_BLANK}*#{LF}#{CG_BLANK}*/
   25 
   26   def initialize backend, opts = {}
   27     @backend = backend
   28     init_backend_traits basebackend: 'manpage', filetype: 'man', outfilesuffix: '.man', supports_templates: true
   29   end
   30 
   31   def convert_document node
   32     unless node.attr? 'mantitle'
   33       raise 'asciidoctor: ERROR: doctype must be set to manpage when using manpage backend'
   34     end
   35     mantitle = node.attr 'mantitle'
   36     manvolnum = node.attr 'manvolnum', '1'
   37     manname = node.attr 'manname', mantitle
   38     manmanual = node.attr 'manmanual'
   39     mansource = node.attr 'mansource'
   40     docdate = (node.attr? 'reproducible') ? nil : (node.attr 'docdate')
   41     # NOTE the first line enables the table (tbl) preprocessor, necessary for non-Linux systems
   42     result = [%('\\" t
   43 .\\"     Title: #{mantitle}
   44 .\\"    Author: #{(node.attr? 'authors') ? (node.attr 'authors') : '[see the "AUTHOR(S)" section]'}
   45 .\\" Generator: Asciidoctor #{node.attr 'asciidoctor-version'})]
   46     result << %(.\\"      Date: #{docdate}) if docdate
   47     result << %(.\\"    Manual: #{manmanual ? (manmanual.tr_s WHITESPACE, ' ') : '\ \&'}
   48 .\\"    Source: #{mansource ? (mansource.tr_s WHITESPACE, ' ') : '\ \&'}
   49 .\\"  Language: English
   50 .\\")
   51     # TODO add document-level setting to disable capitalization of manname
   52     result << %(.TH "#{manify manname.upcase}" "#{manvolnum}" "#{docdate}" "#{mansource ? (manify mansource) : '\ \&'}" "#{manmanual ? (manify manmanual) : '\ \&'}")
   53     # define portability settings
   54     # see http://bugs.debian.org/507673
   55     # see http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
   56     result << '.ie \n(.g .ds Aq \(aq'
   57     result << '.el       .ds Aq \''
   58     # set sentence_space_size to 0 to prevent extra space between sentences separated by a newline
   59     # the alternative is to add \& at the end of the line
   60     result << '.ss \n[.ss] 0'
   61     # disable hyphenation
   62     result << '.nh'
   63     # disable justification (adjust text to left margin only)
   64     result << '.ad l'
   65     # define URL macro for portability
   66     # see http://web.archive.org/web/20060102165607/http://people.debian.org/~branden/talks/wtfm/wtfm.pdf
   67     #
   68     # Usage
   69     #
   70     # .URL "http://www.debian.org" "Debian" "."
   71     #
   72     # * First argument: the URL
   73     # * Second argument: text to be hyperlinked
   74     # * Third (optional) argument: text that needs to immediately trail the hyperlink without intervening whitespace
   75     result << '.de URL
   76 \\fI\\\\$2\\fP <\\\\$1>\\\\$3
   77 ..
   78 .als MTO URL
   79 .if \n[.g] \{\
   80 .  mso www.tmac
   81 .  am URL
   82 .    ad l
   83 .  .
   84 .  am MTO
   85 .    ad l
   86 .  .'
   87     result << %(.  LINKSTYLE #{node.attr 'man-linkstyle', 'blue R < >'})
   88     result << '.\}'
   89 
   90     unless node.noheader
   91       if node.attr? 'manpurpose'
   92         mannames = node.attr 'mannames', [manname]
   93         result << %(.SH "#{(node.attr 'manname-title', 'NAME').upcase}"
   94 #{mannames.map {|n| manify n }.join ', '} \\- #{manify node.attr('manpurpose'), whitespace: :normalize})
   95       end
   96     end
   97 
   98     result << node.content
   99 
  100     # QUESTION should NOTES come after AUTHOR(S)?
  101     if node.footnotes? && !(node.attr? 'nofootnotes')
  102       result << '.SH "NOTES"'
  103       result.concat(node.footnotes.map {|fn| %(#{fn.index}. #{fn.text}) })
  104     end
  105 
  106     unless (authors = node.authors).empty?
  107       if authors.size > 1
  108         result << '.SH "AUTHORS"'
  109         authors.each do |author|
  110           result << %(.sp
  111 #{author.name})
  112         end
  113       else
  114         result << %(.SH "AUTHOR"
  115 .sp
  116 #{authors[0].name})
  117       end
  118     end
  119 
  120     result.join LF
  121   end
  122 
  123   # NOTE embedded doesn't really make sense in the manpage backend
  124   def convert_embedded node
  125     result = [node.content]
  126 
  127     if node.footnotes? && !(node.attr? 'nofootnotes')
  128       result << '.SH "NOTES"'
  129       result.concat(node.footnotes.map {|fn| %(#{fn.index}. #{fn.text}) })
  130     end
  131 
  132     # QUESTION should we add an AUTHOR(S) section?
  133 
  134     result.join LF
  135   end
  136 
  137   def convert_section node
  138     result = []
  139     if node.level > 1
  140       macro = 'SS'
  141       # QUESTION why captioned title? why not when level == 1?
  142       stitle = node.captioned_title
  143     else
  144       macro = 'SH'
  145       stitle = node.title.upcase
  146     end
  147     result << %(.#{macro} "#{manify stitle}"
  148 #{node.content})
  149     result.join LF
  150   end
  151 
  152   def convert_admonition node
  153     result = []
  154     result << %(.if n .sp
  155 .RS 4
  156 .it 1 an-trap
  157 .nr an-no-space-flag 1
  158 .nr an-break-flag 1
  159 .br
  160 .ps +1
  161 .B #{node.attr 'textlabel'}#{node.title? ? "\\fP: #{manify node.title}" : ''}
  162 .ps -1
  163 .br
  164 #{enclose_content node}
  165 .sp .5v
  166 .RE)
  167     result.join LF
  168   end
  169 
  170   def convert_colist node
  171     result = []
  172     result << %(.sp
  173 .B #{manify node.title}
  174 .br) if node.title?
  175     result << '.TS
  176 tab(:);
  177 r lw(\n(.lu*75u/100u).'
  178 
  179     num = 0
  180     node.items.each do |item|
  181       result << %(\\fB(#{num += 1})\\fP\\h'-2n':T{)
  182       result << (manify item.text, whitespace: :normalize)
  183       result << item.content if item.blocks?
  184       result << 'T}'
  185     end
  186     result << '.TE'
  187     result.join LF
  188   end
  189 
  190   # TODO implement horizontal (if it makes sense)
  191   def convert_dlist node
  192     result = []
  193     result << %(.sp
  194 .B #{manify node.title}
  195 .br) if node.title?
  196     counter = 0
  197     node.items.each do |terms, dd|
  198       counter += 1
  199       case node.style
  200       when 'qanda'
  201         result << %(.sp
  202 #{counter}. #{manify terms.map {|dt| dt.text }.join ' '}
  203 .RS 4)
  204       else
  205         result << %(.sp
  206 #{manify terms.map {|dt| dt.text }.join(', '), whitespace: :normalize}
  207 .RS 4)
  208       end
  209       if dd
  210         result << (manify dd.text, whitespace: :normalize) if dd.text?
  211         result << dd.content if dd.blocks?
  212       end
  213       result << '.RE'
  214     end
  215     result.join LF
  216   end
  217 
  218   def convert_example node
  219     result = []
  220     result << (node.title? ? %(.sp
  221 .B #{manify node.captioned_title}
  222 .br) : '.sp')
  223     result << %(.RS 4
  224 #{enclose_content node}
  225 .RE)
  226     result.join LF
  227   end
  228 
  229   def convert_floating_title node
  230     %(.SS "#{manify node.title}")
  231   end
  232 
  233   def convert_image node
  234     result = []
  235     result << (node.title? ? %(.sp
  236 .B #{manify node.captioned_title}
  237 .br) : '.sp')
  238     result << %([#{node.alt}])
  239     result.join LF
  240   end
  241 
  242   def convert_listing node
  243     result = []
  244     result << %(.sp
  245 .B #{manify node.captioned_title}
  246 .br) if node.title?
  247     result << %(.sp
  248 .if n .RS 4
  249 .nf
  250 #{manify node.content, whitespace: :preserve}
  251 .fi
  252 .if n .RE)
  253     result.join LF
  254   end
  255 
  256   def convert_literal node
  257     result = []
  258     result << %(.sp
  259 .B #{manify node.title}
  260 .br) if node.title?
  261     result << %(.sp
  262 .if n .RS 4
  263 .nf
  264 #{manify node.content, whitespace: :preserve}
  265 .fi
  266 .if n .RE)
  267     result.join LF
  268   end
  269 
  270   def convert_sidebar node
  271     result = []
  272     result << (node.title? ? %(.sp
  273 .B #{manify node.title}
  274 .br) : '.sp')
  275     result << %(.RS 4
  276 #{enclose_content node}
  277 .RE)
  278     result.join LF
  279   end
  280 
  281   def convert_olist node
  282     result = []
  283     result << %(.sp
  284 .B #{manify node.title}
  285 .br) if node.title?
  286 
  287     node.items.each_with_index do |item, idx|
  288       result << %(.sp
  289 .RS 4
  290 .ie n \\{\\
  291 \\h'-04' #{idx + 1}.\\h'+01'\\c
  292 .\\}
  293 .el \\{\\
  294 .  sp -1
  295 .  IP " #{idx + 1}." 4.2
  296 .\\}
  297 #{manify item.text, whitespace: :normalize})
  298       result << item.content if item.blocks?
  299       result << '.RE'
  300     end
  301     result.join LF
  302   end
  303 
  304   def convert_open node
  305     case node.style
  306     when 'abstract', 'partintro'
  307       enclose_content node
  308     else
  309       node.content
  310     end
  311   end
  312 
  313   # TODO use Page Control https://www.gnu.org/software/groff/manual/html_node/Page-Control.html#Page-Control
  314   alias convert_page_break skip
  315 
  316   def convert_paragraph node
  317     if node.title?
  318       %(.sp
  319 .B #{manify node.title}
  320 .br
  321 #{manify node.content, whitespace: :normalize})
  322     else
  323       %(.sp
  324 #{manify node.content, whitespace: :normalize})
  325     end
  326   end
  327 
  328   alias convert_pass content_only
  329   alias convert_preamble content_only
  330 
  331   def convert_quote node
  332     result = []
  333     if node.title?
  334       result << %(.sp
  335 .RS 3
  336 .B #{manify node.title}
  337 .br
  338 .RE)
  339     end
  340     attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil
  341     attribution_line = (node.attr? 'attribution') ? %[#{attribution_line}\\(em #{node.attr 'attribution'}] : nil
  342     result << %(.RS 3
  343 .ll -.6i
  344 #{enclose_content node}
  345 .br
  346 .RE
  347 .ll)
  348     if attribution_line
  349       result << %(.RS 5
  350 .ll -.10i
  351 #{attribution_line}
  352 .RE
  353 .ll)
  354     end
  355     result.join LF
  356   end
  357 
  358   def convert_stem node
  359     result = []
  360     result << (node.title? ? %(.sp
  361 .B #{manify node.title}
  362 .br) : '.sp')
  363     open, close = BLOCK_MATH_DELIMITERS[node.style.to_sym]
  364     if ((equation = node.content).start_with? open) && (equation.end_with? close)
  365       equation = equation.slice open.length, equation.length - open.length - close.length
  366     end
  367     result << %(#{manify equation, whitespace: :preserve} (#{node.style}))
  368     result.join LF
  369   end
  370 
  371   # FIXME: The reason this method is so complicated is because we are not
  372   # receiving empty(marked) cells when there are colspans or rowspans. This
  373   # method has to create a map of all cells and in the case of rowspans
  374   # create empty cells as placeholders of the span.
  375   # To fix this, asciidoctor needs to provide an API to tell the user if a
  376   # given cell is being used as a colspan or rowspan.
  377   def convert_table node
  378     result = []
  379     if node.title?
  380       result << %(.sp
  381 .it 1 an-trap
  382 .nr an-no-space-flag 1
  383 .nr an-break-flag 1
  384 .br
  385 .B #{manify node.captioned_title}
  386 )
  387     end
  388     result << '.TS
  389 allbox tab(:);'
  390     row_header = []
  391     row_text = []
  392     row_index = 0
  393     node.rows.to_h.each do |tsec, rows|
  394       rows.each do |row|
  395         row_header[row_index] ||= []
  396         row_text[row_index] ||= []
  397         # result << LF
  398         # l left-adjusted
  399         # r right-adjusted
  400         # c centered-adjusted
  401         # n numerical align
  402         # a alphabetic align
  403         # s spanned
  404         # ^ vertically spanned
  405         remaining_cells = row.size
  406         row.each_with_index do |cell, cell_index|
  407           remaining_cells -= 1
  408           row_header[row_index][cell_index] ||= []
  409           # Add an empty cell if this is a rowspan cell
  410           if row_header[row_index][cell_index] == ['^t']
  411             row_text[row_index] << %(T{#{LF}.sp#{LF}T}:)
  412           end
  413           row_text[row_index] << %(T{#{LF}.sp#{LF})
  414           cell_halign = (cell.attr 'halign', 'left').chr
  415           if tsec == :head
  416             if row_header[row_index].empty? || row_header[row_index][cell_index].empty?
  417               row_header[row_index][cell_index] << %(#{cell_halign}tB)
  418             else
  419               row_header[row_index][cell_index + 1] ||= []
  420               row_header[row_index][cell_index + 1] << %(#{cell_halign}tB)
  421             end
  422             row_text[row_index] << %(#{manify cell.text, whitespace: :normalize}#{LF})
  423           elsif tsec == :body
  424             if row_header[row_index].empty? || row_header[row_index][cell_index].empty?
  425               row_header[row_index][cell_index] << %(#{cell_halign}t)
  426             else
  427               row_header[row_index][cell_index + 1] ||= []
  428               row_header[row_index][cell_index + 1] << %(#{cell_halign}t)
  429             end
  430             case cell.style
  431             when :asciidoc
  432               cell_content = cell.content
  433             when :literal
  434               cell_content = %(.nf#{LF}#{manify cell.text, whitespace: :preserve}#{LF}.fi)
  435             else
  436               cell_content = manify cell.content.join, whitespace: :normalize
  437             end
  438             row_text[row_index] << %(#{cell_content}#{LF})
  439           elsif tsec == :foot
  440             if row_header[row_index].empty? || row_header[row_index][cell_index].empty?
  441               row_header[row_index][cell_index] << %(#{cell_halign}tB)
  442             else
  443               row_header[row_index][cell_index + 1] ||= []
  444               row_header[row_index][cell_index + 1] << %(#{cell_halign}tB)
  445             end
  446             row_text[row_index] << %(#{manify cell.text, whitespace: :normalize}#{LF})
  447           end
  448           if cell.colspan && cell.colspan > 1
  449             (cell.colspan - 1).times do |i|
  450               if row_header[row_index].empty? || row_header[row_index][cell_index].empty?
  451                 row_header[row_index][cell_index + i] << 'st'
  452               else
  453                 row_header[row_index][cell_index + 1 + i] ||= []
  454                 row_header[row_index][cell_index + 1 + i] << 'st'
  455               end
  456             end
  457           end
  458           if cell.rowspan && cell.rowspan > 1
  459             (cell.rowspan - 1).times do |i|
  460               row_header[row_index + 1 + i] ||= []
  461               if row_header[row_index + 1 + i].empty? || row_header[row_index + 1 + i][cell_index].empty?
  462                 row_header[row_index + 1 + i][cell_index] ||= []
  463                 row_header[row_index + 1 + i][cell_index] << '^t'
  464               else
  465                 row_header[row_index + 1 + i][cell_index + 1] ||= []
  466                 row_header[row_index + 1 + i][cell_index + 1] << '^t'
  467               end
  468             end
  469           end
  470           if remaining_cells >= 1
  471             row_text[row_index] << 'T}:'
  472           else
  473             row_text[row_index] << %(T}#{LF})
  474           end
  475         end
  476         row_index += 1
  477       end unless rows.empty?
  478     end
  479 
  480     #row_header.each do |row|
  481     #  result << LF
  482     #  row.each_with_index do |cell, i|
  483     #    result << (cell.join ' ')
  484     #    result << ' ' if row.size > i + 1
  485     #  end
  486     #end
  487     # FIXME temporary fix to get basic table to display
  488     result << LF
  489     result << ('lt ' * row_header[0].size).chop
  490 
  491     result << %(.#{LF})
  492     row_text.each do |row|
  493       result << row.join
  494     end
  495     result << %(.TE#{LF}.sp)
  496     result.join
  497   end
  498 
  499   def convert_thematic_break node
  500     '.sp
  501 .ce
  502 \l\'\n(.lu*25u/100u\(ap\''
  503   end
  504 
  505   alias convert_toc skip
  506 
  507   def convert_ulist node
  508     result = []
  509     result << %(.sp
  510 .B #{manify node.title}
  511 .br) if node.title?
  512     node.items.map do |item|
  513       result << %[.sp
  514 .RS 4
  515 .ie n \\{\\
  516 \\h'-04'\\(bu\\h'+03'\\c
  517 .\\}
  518 .el \\{\\
  519 .  sp -1
  520 .  IP \\(bu 2.3
  521 .\\}
  522 #{manify item.text, whitespace: :normalize}]
  523       result << item.content if item.blocks?
  524       result << '.RE'
  525     end
  526     result.join LF
  527   end
  528 
  529   # FIXME git uses [verse] for the synopsis; detect this special case
  530   def convert_verse node
  531     result = []
  532     result << (node.title? ? %(.sp
  533 .B #{manify node.title}
  534 .br) : '.sp')
  535     attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil
  536     attribution_line = (node.attr? 'attribution') ? %[#{attribution_line}\\(em #{node.attr 'attribution'}] : nil
  537     result << %(.sp
  538 .nf
  539 #{manify node.content, whitespace: :preserve}
  540 .fi
  541 .br)
  542     if attribution_line
  543       result << %(.in +.5i
  544 .ll -.5i
  545 #{attribution_line}
  546 .in
  547 .ll)
  548     end
  549     result.join LF
  550   end
  551 
  552   def convert_video node
  553     start_param = (node.attr? 'start') ? %(&start=#{node.attr 'start'}) : ''
  554     end_param = (node.attr? 'end') ? %(&end=#{node.attr 'end'}) : ''
  555     result = []
  556     result << (node.title? ? %(.sp
  557 .B #{manify node.title}
  558 .br) : '.sp')
  559     result << %(<#{node.media_uri(node.attr 'target')}#{start_param}#{end_param}> (video))
  560     result.join LF
  561   end
  562 
  563   def convert_inline_anchor node
  564     target = node.target
  565     case node.type
  566     when :link
  567       if target.start_with? 'mailto:'
  568         macro = 'MTO'
  569         target = target.slice 7, target.length
  570       else
  571         macro = 'URL'
  572       end
  573       if (text = node.text) == target
  574         text = ''
  575       else
  576         text = text.gsub '"', %[#{ESC_BS}(dq]
  577       end
  578       target = target.sub '@', %[#{ESC_BS}(at] if macro == 'MTO'
  579       %(#{ESC_BS}c#{LF}#{ESC_FS}#{macro} "#{target}" "#{text}" )
  580     when :xref
  581       unless (text = node.text)
  582         refid = node.attributes['refid']
  583         if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid])
  584           text = (ref.xreftext node.attr('xrefstyle', nil, true)) || %([#{refid}])
  585         else
  586           text = %([#{refid}])
  587         end
  588       end
  589       text
  590     when :ref, :bibref
  591       # These are anchor points, which shouldn't be visible
  592       ''
  593     else
  594       logger.warn %(unknown anchor type: #{node.type.inspect})
  595       nil
  596     end
  597   end
  598 
  599   def convert_inline_break node
  600     %(#{node.text}#{LF}#{ESC_FS}br)
  601   end
  602 
  603   def convert_inline_button node
  604     %(#{ESC_BS}fB[#{ESC_BS}0#{node.text}#{ESC_BS}0]#{ESC_BS}fP)
  605   end
  606 
  607   def convert_inline_callout node
  608     %(#{ESC_BS}fB(#{node.text})#{ESC_BS}fP)
  609   end
  610 
  611   # TODO supposedly groff has footnotes, but we're in search of an example
  612   def convert_inline_footnote node
  613     if (index = node.attr 'index')
  614       %([#{index}])
  615     elsif node.type == :xref
  616       %([#{node.text}])
  617     end
  618   end
  619 
  620   def convert_inline_image node
  621     (node.attr? 'link') ? %([#{node.alt}] <#{node.attr 'link'}>) : %([#{node.alt}])
  622   end
  623 
  624   def convert_inline_indexterm node
  625     node.type == :visible ? node.text : ''
  626   end
  627 
  628   def convert_inline_kbd node
  629     if (keys = node.attr 'keys').size == 1
  630       keys[0]
  631     else
  632       keys.join %(#{ESC_BS}0+#{ESC_BS}0)
  633     end
  634   end
  635 
  636   def convert_inline_menu node
  637     caret = %[#{ESC_BS}0#{ESC_BS}(fc#{ESC_BS}0]
  638     menu = node.attr 'menu'
  639     if !(submenus = node.attr 'submenus').empty?
  640       submenu_path = submenus.map {|item| %(#{ESC_BS}fI#{item}#{ESC_BS}fP) }.join caret
  641       %(#{ESC_BS}fI#{menu}#{ESC_BS}fP#{caret}#{submenu_path}#{caret}#{ESC_BS}fI#{node.attr 'menuitem'}#{ESC_BS}fP)
  642     elsif (menuitem = node.attr 'menuitem')
  643       %(#{ESC_BS}fI#{menu}#{caret}#{menuitem}#{ESC_BS}fP)
  644     else
  645       %(#{ESC_BS}fI#{menu}#{ESC_BS}fP)
  646     end
  647   end
  648 
  649   # NOTE use fake <BOUNDARY> element to prevent creating artificial word boundaries
  650   def convert_inline_quoted node
  651     case node.type
  652     when :emphasis
  653       %(#{ESC_BS}fI<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP)
  654     when :strong
  655       %(#{ESC_BS}fB<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP)
  656     when :monospaced
  657       %[#{ESC_BS}f(CR<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP]
  658     when :single
  659       %[#{ESC_BS}(oq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(cq]
  660     when :double
  661       %[#{ESC_BS}(lq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(rq]
  662     else
  663       node.text
  664     end
  665   end
  666 
  667   def self.write_alternate_pages mannames, manvolnum, target
  668     if mannames && mannames.size > 1
  669       mannames.shift
  670       manvolext = %(.#{manvolnum})
  671       dir, basename = ::File.split target
  672       mannames.each do |manname|
  673         ::File.write ::File.join(dir, %(#{manname}#{manvolext})), %(.so #{basename}), mode: FILE_WRITE_MODE
  674       end
  675     end
  676   end
  677 
  678   private
  679 
  680   # Converts HTML entity references back to their original form, escapes
  681   # special man characters and strips trailing whitespace.
  682   #
  683   # It's crucial that text only ever pass through manify once.
  684   #
  685   # str  - the String to convert
  686   # opts - an Hash of options to control processing (default: {})
  687   #        * :whitespace an enum that indicates how to handle whitespace; supported options are:
  688   #          :preserve - preserve spaces (only expanding tabs); :normalize - normalize whitespace
  689   #          (remove spaces around newlines); :collapse - collapse adjacent whitespace to a single
  690   #          space (default: :collapse)
  691   #        * :append_newline a Boolean that indicates whether to append a newline to the result (default: false)
  692   def manify str, opts = {}
  693     case opts.fetch :whitespace, :collapse
  694     when :preserve
  695       str = str.gsub TAB, ET
  696     when :normalize
  697       str = str.gsub WrappedIndentRx, LF
  698     else
  699       str = str.tr_s WHITESPACE, ' '
  700     end
  701     str = str.
  702       gsub(LiteralBackslashRx, '\&(rs'). # literal backslash (not a troff escape sequence)
  703       gsub(LeadingPeriodRx, '\\\&.'). # leading . is used in troff for macro call or other formatting; replace with \&.
  704       # drop orphaned \c escape lines, unescape troff macro, quote adjacent character, isolate macro line
  705       gsub(EscapedMacroRx) { (rest = $3.lstrip).empty? ? %(.#$1"#$2") : %(.#$1"#$2"#{LF}#{rest}) }.
  706       gsub('-', '\-').
  707       gsub('&lt;', '<').
  708       gsub('&gt;', '>').
  709       gsub('&#160;', '\~').     # non-breaking space
  710       gsub('&#169;', '\(co').   # copyright sign
  711       gsub('&#174;', '\(rg').   # registered sign
  712       gsub('&#8482;', '\(tm').  # trademark sign
  713       gsub('&#8201;', ' ').     # thin space
  714       gsub('&#8211;', '\(en').  # en dash
  715       gsub(EmDashCharRefRx, '\(em'). # em dash
  716       gsub('&#8216;', '\(oq').  # left single quotation mark
  717       gsub('&#8217;', '\(cq').  # right single quotation mark
  718       gsub('&#8220;', '\(lq').  # left double quotation mark
  719       gsub('&#8221;', '\(rq').  # right double quotation mark
  720       gsub(EllipsisCharRefRx, '...'). # horizontal ellipsis
  721       gsub('&#8592;', '\(<-').  # leftwards arrow
  722       gsub('&#8594;', '\(->').  # rightwards arrow
  723       gsub('&#8656;', '\(lA').  # leftwards double arrow
  724       gsub('&#8658;', '\(rA').  # rightwards double arrow
  725       gsub('&#8203;', '\:').    # zero width space
  726       gsub('&amp;','&').        # literal ampersand (NOTE must take place after any other replacement that includes &)
  727       gsub('\'', '\(aq').       # apostrophe-quote
  728       gsub(MockBoundaryRx, ''). # mock boundary
  729       gsub(ESC_BS, '\\').       # unescape troff backslash (NOTE update if more escapes are added)
  730       gsub(ESC_FS, '.').        # unescape full stop in troff commands (NOTE must take place after gsub(LeadingPeriodRx))
  731       rstrip                    # strip trailing space
  732     opts[:append_newline] ? %(#{str}#{LF}) : str
  733   end
  734 
  735   def enclose_content node
  736     node.content_model == :compound ? node.content : %(.sp#{LF}#{manify node.content, whitespace: :normalize})
  737   end
  738 end
  739 end