module Asciidoctor # A built-in {Converter} implementation that generates the man page (troff) format. # # The output follows the groff man page definition while also trying to be # consistent with the output produced by the a2x tool from AsciiDoc Python. # # See http://www.gnu.org/software/groff/manual/html_node/Man-usage.html#Man-usage class Converter::ManPageConverter < Converter::BuiltIn LF = "\n" TAB = "\t" ETAB = ' ' * 8 # Converts HTML entity references back to their original form, escapes # special man characters and strips trailing whitespace. # # Optional features: # * fold each endline into a single space # * append a newline def manify str, opts = {} append_newline = opts[:append_newline] preserve_space = opts.fetch :preserve_space, true str = preserve_space ? str.gsub(TAB, ETAB) : str.tr_s(%(#{LF}#{TAB} ), ' ') str = str. gsub(/^\.$/, '\\\&.'). # a lone . is also used in troff to indicate paragraph continuation with visual separator gsub(/\\$/, '\\(rs'). # a literal backslash at the end of a line gsub(/^\.((?:URL|MTO) ".*?" ".*?" )( |[^\s]*)(.*?)( *)$/, ".\\1\"\\2\"#{LF}\\3"). # quote last URL argument gsub(/(?:\A\n|\n *(\n))^\.(URL|MTO) /, "\\1\.\\2 "). # strip blank lines in source that precede a URL gsub('-', '\\-'). gsub('<', '<'). gsub('>', '>'). gsub('©', '\\(co'). # copyright sign gsub('®', '\\(rg'). # registered sign gsub('™', '\\(tm'). # trademark sign gsub(' ', ' '). # thin space gsub('–', '\\(en'). # en-dash gsub(/—(?:;​)?/, '\\(em'). # em-dash gsub('‘', '\\(oq'). # left single quotation mark gsub('’', '\\(cq'). # right single quotation mark gsub('“', '\\(lq'). # left double quotation mark gsub('”', '\\(rq'). # right double quotation mark gsub(/…(?:​)?/, '...'). # horizontal ellipsis gsub('←', '\\(<-'). # leftwards arrow gsub('→', '\\(->'). # rightwards arrow gsub('⇐', '\\(lA'). # leftwards double arrow gsub('⇒', '\\(rA'). # rightwards double arrow gsub('​', '\:'). # zero width space gsub('\'', '\\(aq'). # apostrophe-quote gsub(/<\/?BOUNDARY>/, '').# artificial boundary rstrip # strip trailing space append_newline ? %(#{str}#{LF}) : str end def skip_with_warning node, name = nil warn %(asciidoctor: WARNING: converter missing for #{name || node.node_name} node in manpage backend) nil end def document node unless node.attr? 'mantitle' raise 'asciidoctor: ERROR: doctype must be set to manpage when using manpage backend' end mantitle = node.attr 'mantitle' manvolnum = node.attr 'manvolnum', '1' manname = node.attr 'manname', mantitle result = [%('\\" t .\\" Title: #{mantitle} .\\" Author: #{(node.attr? 'authors') ? (node.attr 'authors') : '[see the "AUTHORS" section]'} .\\" Generator: Asciidoctor #{node.attr 'asciidoctor-version'} .\\" Date: #{docdate = node.attr 'docdate'} .\\" Manual: #{manual = (node.attr? 'manmanual') ? (node.attr 'manmanual') : '\ \&'} .\\" Source: #{source = (node.attr? 'mansource') ? (node.attr 'mansource') : '\ \&'} .\\" Language: English .\\")] # TODO add document-level setting to disable capitalization of manname result << %(.TH "#{manify manname.upcase}" "#{manvolnum}" "#{docdate}" "#{manify source}" "#{manify manual}") # define portability settings # see http://bugs.debian.org/507673 # see http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html result << '.ie \n(.g .ds Aq \(aq' result << '.el .ds Aq \'' # set sentence_space_size to 0 to prevent extra space between sentences separated by a newline # the alternative is to add \& at the end of the line result << '.ss \n[.ss] 0' # disable hyphenation result << '.nh' # disable justification (adjust text to left margin only) result << '.ad l' # define URL macro for portability # see http://web.archive.org/web/20060102165607/http://people.debian.org/~branden/talks/wtfm/wtfm.pdf # # Use: .URL "http://www.debian.org" "Debian" "." # # * First argument: the URL # * Second argument: text to be hyperlinked # * Third (optional) argument: text that needs to immediately trail # the hyperlink without intervening whitespace result << '.de URL \\\\$2 \\(laURL: \\\\$1 \\(ra\\\\$3 .. .if \n[.g] .mso www.tmac .LINKSTYLE blue R < >' unless node.noheader if node.attr? 'manpurpose' result << %(.SH "#{node.attr 'manname-title'}" #{manify mantitle} \\- #{manify node.attr 'manpurpose'}) end end result << node.content # QUESTION should NOTES come after AUTHOR(S)? if node.footnotes? && !(node.attr? 'nofootnotes') result << '.SH "NOTES"' result.concat(node.footnotes.map {|fn| %(#{fn.index}. #{fn.text}) }) end # FIXME detect single author and use appropriate heading; itemize the authors if multiple if node.attr? 'authors' result << %(.SH "AUTHOR(S)" .sp \\fB#{node.attr 'authors'}\\fP .RS 4 Author(s). .RE) end result * LF end # NOTE embedded doesn't really make sense in the manpage backend def embedded node result = [node.content] if node.footnotes? && !(node.attr? 'nofootnotes') result << '.SH "NOTES"' result.concat(node.footnotes.map {|fn| %(#{fn.index}. #{fn.text}) }) end # QUESTION should we add an AUTHOR(S) section? result * LF end def section node slevel = node.level # QUESTION should the check for slevel be done in section? slevel = 1 if slevel == 0 && node.special result = [] if slevel > 1 macro = 'SS' # QUESTION why captioned title? why not for slevel == 1? stitle = node.captioned_title else macro = 'SH' stitle = node.title.upcase end result << %(.#{macro} "#{manify stitle}" #{node.content}) result * LF end def admonition node result = [] result << %(.if n \\{\\ .sp .\\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 .B #{node.caption}#{node.title? ? "\\fP #{manify node.title}" : nil} .ps -1 .br #{resolve_content node} .sp .5v .RE) result * LF end alias :audio :skip_with_warning def colist node result = [] result << %(.sp .B #{manify node.title} .br) if node.title? result << %(.TS tab\(:\); r lw\(\\n\(.lu*75u/100u\).) node.items.each_with_index do |item, index| result << %(\\fB(#{index + 1})\\fP\\h'-2n':T{ #{manify item.text} T}) end result << '.TE' result * LF end # TODO implement title for dlist # TODO implement horizontal (if it makes sense) def dlist node result = [] counter = 0 node.items.each do |terms, dd| counter += 1 case node.style when 'qanda' result << %(.sp #{counter}. #{manify([*terms].map {|dt| dt.text }.join ' ')} .RS 4) else result << %(.sp #{manify([*terms].map {|dt| dt.text }.join ', ')} .RS 4) end if dd result << (manify dd.text) if dd.text? result << dd.content if dd.blocks? end result << '.RE' end result * LF end def example node result = [] result << %(.sp .B #{manify node.captioned_title} .br) if node.title? result << %(.RS 4 #{resolve_content node} .RE) result * LF end def floating_title node %(.SS "#{manify node.title}") end alias :image :skip_with_warning def listing node result = [] result << %(.sp .B #{manify node.captioned_title} .br) if node.title? result << %(.sp .if n \\{\\ .RS 4 .\\} .nf #{manify node.content} .fi .if n \\{\\ .RE .\\}) result * LF end def literal node result = [] result << %(.sp .B #{manify node.title} .br) if node.title? result << %(.sp .if n \\{\\ .RS 4 .\\} .nf #{manify node.content} .fi .if n \\{\\ .RE .\\}) result * LF end def olist node result = [] result << %(.sp .B #{manify node.title} .br) if node.title? node.items.each_with_index do |item, idx| result << %(.sp .RS 4 .ie n \\{\\ \\h'-04' #{idx + 1}.\\h'+01'\\c .\\} .el \\{\\ .sp -1 .IP " #{idx + 1}." 4.2 .\\} #{manify item.text}) result << item.content if item.blocks? result << '.RE' end result * LF end def open node case node.style when 'abstract', 'partintro' resolve_content node else node.content end end # TODO use Page Control https://www.gnu.org/software/groff/manual/html_node/Page-Control.html#Page-Control alias :page_break :skip def paragraph node if node.title? %(.sp .B #{manify node.title} .br #{manify node.content}) else %(.sp #{manify node.content}) end end alias :preamble :content def quote node result = [] if node.title? result << %(.sp .in +.3i .B #{manify node.title} .br .in) end attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil attribution_line = (node.attr? 'attribution') ? %(#{attribution_line}\\\(em #{node.attr 'attribution'}) : nil result << %(.in +.3i .ll -.3i .nf #{resolve_content node} .fi .br .in .ll) if attribution_line result << %(.in +.5i .ll -.5i #{attribution_line} .in .ll) end result * LF end alias :sidebar :skip_with_warning def stem node title_element = node.title? ? %(.sp .B #{manify node.title} .br) : nil open, close = BLOCK_MATH_DELIMITERS[node.style.to_sym] unless ((equation = node.content).start_with? open) && (equation.end_with? close) equation = %(#{open}#{equation}#{close}) end %(#{title_element}#{equation}) end # FIXME: The reason this method is so complicated is because we are not # receiving empty(marked) cells when there are colspans or rowspans. This # method has to create a map of all cells and in the case of rowspans # create empty cells as placeholders of the span. # To fix this, asciidoctor needs to provide an API to tell the user if a # given cell is being used as a colspan or rowspan. def table node result = [] if node.title? result << %(.sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .B #{manify node.captioned_title}) end result << '.TS allbox tab(:);' row_header = [] row_text = [] row_index = 0 [:head, :body, :foot].each do |tsec| node.rows[tsec].each do |row| row_header[row_index] ||= [] row_text[row_index] ||= [] # result << LF # l left-adjusted # r right-adjusted # c centered-adjusted # n numerical align # a alphabetic align # s spanned # ^ vertically spanned remaining_cells = row.size row.each_with_index do |cell, cell_index| remaining_cells -= 1 row_header[row_index][cell_index] ||= [] # Add an empty cell if this is a rowspan cell if row_header[row_index][cell_index] == ['^t'] row_text[row_index] << %(T{#{LF}.sp#{LF}T}:) end row_text[row_index] << %(T{#{LF}.sp#{LF}) cell_halign = (cell.attr 'halign', 'left')[0..0] if tsec == :head if row_header[row_index].empty? || row_header[row_index][cell_index].empty? row_header[row_index][cell_index] << %(#{cell_halign}tB) else row_header[row_index][cell_index + 1] ||= [] row_header[row_index][cell_index + 1] << %(#{cell_halign}tB) end row_text[row_index] << %(#{cell.text}#{LF}) elsif tsec == :body if row_header[row_index].empty? || row_header[row_index][cell_index].empty? row_header[row_index][cell_index] << %(#{cell_halign}t) else row_header[row_index][cell_index + 1] ||= [] row_header[row_index][cell_index + 1] << %(#{cell_halign}t) end case cell.style when :asciidoc cell_content = cell.content when :verse, :literal cell_content = cell.text else cell_content = cell.content.join end row_text[row_index] << %(#{cell_content}#{LF}) elsif tsec == :foot if row_header[row_index].empty? || row_header[row_index][cell_index].empty? row_header[row_index][cell_index] << %(#{cell_halign}tB) else row_header[row_index][cell_index + 1] ||= [] row_header[row_index][cell_index + 1] << %(#{cell_halign}tB) end row_text[row_index] << %(#{cell.text}#{LF}) end if cell.colspan && cell.colspan > 1 (cell.colspan - 1).times do |i| if row_header[row_index].empty? || row_header[row_index][cell_index].empty? row_header[row_index][cell_index + i] << 'st' else row_header[row_index][cell_index + 1 + i] ||= [] row_header[row_index][cell_index + 1 + i] << 'st' end end end if cell.rowspan && cell.rowspan > 1 (cell.rowspan - 1).times do |i| row_header[row_index + 1 + i] ||= [] if row_header[row_index + 1 + i].empty? || row_header[row_index + 1 + i][cell_index].empty? row_header[row_index + 1 + i][cell_index] ||= [] row_header[row_index + 1 + i][cell_index] << '^t' else row_header[row_index + 1 + i][cell_index + 1] ||= [] row_header[row_index + 1 + i][cell_index + 1] << '^t' end end end if remaining_cells >= 1 row_text[row_index] << 'T}:' else row_text[row_index] << %(T}#{LF}) end end row_index += 1 end end #row_header.each do |row| # result << LF # row.each_with_index do |cell, i| # result << (cell.join ' ') # result << ' ' if row.size > i + 1 # end #end # FIXME temporary fix to get basic table to display result << LF result << row_header.first.map {|r| 'lt'}.join(' ') result << %(.#{LF}) row_text.each do |row| result << row.join end result << %(.TE#{LF}.sp) result.join end def thematic_break node '.sp .ce \l\'\n(.lu*25u/100u\(ap\'' end alias :toc :skip def ulist node result = [] result << %(.sp .B #{manify node.title} .br) if node.title? node.items.map {|item| result << %[.sp .RS 4 .ie n \\{\\ \\h'-04'\\(bu\\h'+03'\\c .\\} .el \\{\\ .sp -1 .IP \\(bu 2.3 .\\} #{manify item.text}] result << item.content if item.blocks? result << '.RE' } result * LF end # FIXME git uses [verse] for the synopsis; detect this special case def verse node result = [] if node.title? result << %(.sp .B #{manify node.title} .br) end attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil attribution_line = (node.attr? 'attribution') ? %(#{attribution_line}\\\(em #{node.attr 'attribution'}) : nil result << %(.sp .nf #{manify node.content} .fi .br) if attribution_line result << %(.in +.5i .ll -.5i #{attribution_line} .in .ll) end result * LF end def video node start_param = (node.attr? 'start', nil, false) ? %(&start=#{node.attr 'start'}) : nil end_param = (node.attr? 'end', nil, false) ? %(&end=#{node.attr 'end'}) : nil %(.sp #{manify node.captioned_title} (video) <#{node.media_uri(node.attr 'target')}#{start_param}#{end_param}>) end def inline_anchor node target = node.target case node.type when :link if (text = node.text) == target text = nil else text = text.gsub '"', '\\(dq' end if target.start_with? 'mailto:' macro = 'MTO' target = target[7..-1].sub('@', '\\(at') else macro = 'URL' end %(#{LF}.#{macro} "#{target}" "#{text}" ) when :xref refid = (node.attr 'refid') || target node.text || (node.document.references[:ids][refid] || %([#{refid}])) when :ref, :bibref # These are anchor points, which shouldn't be visual '' else warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect}) end end def inline_break node %(#{node.text} .br) end def inline_button node %(\\fB[\\ #{node.text}\\ ]\\fP) end def inline_callout node %[\\fB(#{node.text})\\fP] end # TODO supposedly groff has footnotes, but we're in search of an example def inline_footnote node if (index = node.attr 'index') %([#{index}]) elsif node.type == :xref %([#{node.text}]) end end def inline_image node # NOTE alt should always be set alt_text = (node.attr? 'alt') ? (node.attr 'alt') : node.target (node.attr? 'link') ? %([#{alt_text}] <#{node.attr 'link'}>) : %([#{alt_text}]) end def inline_indexterm node node.type == :visible ? node.text : '' end def inline_kbd node if (keys = node.attr 'keys').size == 1 keys[0] else keys.join '\ +\ ' end end def inline_menu node caret = '\ \(fc\ ' menu = node.attr 'menu' if !(submenus = node.attr 'submenus').empty? submenu_path = submenus.map {|item| %(\\fI#{item}\\fP) }.join caret %(\\fI#{menu}\\fP#{caret}#{submenu_path}#{caret}\\fI#{node.attr 'menuitem'}\\fP) elsif (menuitem = node.attr 'menuitem') %(\\fI#{menu}#{caret}#{menuitem}\\fP) else %(\\fI#{menu}\\fP) end end # NOTE use fake element to prevent creating artificial word boundaries def inline_quoted node case node.type when :emphasis %[\\fI#{node.text}\\fP] when :strong %[\\fB#{node.text}\\fP] when :monospaced %[\\f[CR]#{node.text}\\fP] when :single %[\\(oq#{node.text}\\(cq] when :double %[\\(lq#{node.text}\\(rq] else node.text end end def resolve_content node node.content_model == :compound ? node.content : %(.sp#{LF}#{manify node.content}) end end end