class Asciidoctor::Converter::ManPageConverter
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 www.gnu.org/software/groff/manual/html_node/Man-usage.html#Man-usage
Constants
- ESC
- ESC_BS
- ESC_FS
- ET
- EllipsisCharRefRx
- EmDashCharRefRx
- EscapedMacroRx
- LF
- LeadingPeriodRx
- LiteralBackslashRx
- MockBoundaryRx
- TAB
- WHITESPACE
Public Instance Methods
# File lib/asciidoctor/converter/manpage.rb, line 194 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.attr 'textlabel'}#{node.title? ? "\\fP: #{manify node.title}" : ''} .ps -1 .br #{resolve_content node} .sp .5v .RE) result * LF end
# File lib/asciidoctor/converter/manpage.rb, line 214 def colist node result = [] result << %(.sp .B #{manify node.title} .br) if node.title? result << '.TS tab(:); r lw(\n(.lu*75u/100u).' num = 0 node.items.each do |item| result << %(\\fB(#{num += 1})\\fP\\h'-2n':T{) result << (manify item.text) result << item.content if item.blocks? result << 'T}' end result << '.TE' result * LF end
TODO implement horizontal (if it makes sense)
# File lib/asciidoctor/converter/manpage.rb, line 235 def dlist node result = [] result << %(.sp .B #{manify node.title} .br) if node.title? counter = 0 node.items.each do |terms, dd| counter += 1 case node.style when 'qanda' result << %(.sp #{counter}. #{manify([*terms].map {|dt| dt.text } * ' ')} .RS 4) else result << %(.sp #{manify([*terms].map {|dt| dt.text } * ', ')} .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
# File lib/asciidoctor/converter/manpage.rb, line 74 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 docdate = (node.attr? 'reproducible') ? nil : (node.attr 'docdate') # NOTE the first line enables the table (tbl) preprocessor, necessary for non-Linux systems result = [%('\\" t .\\" Title: #{mantitle} .\\" Author: #{(node.attr? 'authors') ? (node.attr 'authors') : '[see the "AUTHOR(S)" section]'} .\\" Generator: Asciidoctor #{node.attr 'asciidoctor-version'})] result << %(.\\" Date: #{docdate}) if docdate result << %(.\\" Manual: #{(manual = node.attr 'manmanual') || '\ \&'} .\\" Source: #{(source = node.attr 'mansource') || '\ \&'} .\\" Language: English .\\") # TODO add document-level setting to disable capitalization of manname result << %(.TH "#{manify manname.upcase}" "#{manvolnum}" "#{docdate}" "#{source ? (manify source) : '\ \&'}" "#{manual ? (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 # # Usage # # .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 \\fI\\\\$2\\fP <\\\\$1>\\\\$3 .. .als MTO URL .if \n[.g] \{\ . mso www.tmac . am URL . ad l . . . am MTO . ad l . .' result << %(. LINKSTYLE #{node.attr 'man-linkstyle', 'blue R < >'}) result << '.\}' unless node.noheader if node.attr? 'manpurpose' mannames = node.attr 'mannames', [manname] result << %(.SH "#{(node.attr 'manname-title').upcase}" #{mannames.map {|n| manify n } * ', '} \\- #{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 we really need an API that returns the authors as an array if (num_authors = (node.attr 'authorcount') || 0) > 0 if num_authors == 1 result << %(.SH "AUTHOR" .sp #{node.attr 'author'}) else result << '.SH "AUTHORS"' (1.upto num_authors).each do |i| result << %(.sp #{node.attr "author_#{i}"}) end end end result * LF end
NOTE embedded doesn't really make sense in the manpage backend
# File lib/asciidoctor/converter/manpage.rb, line 166 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
# File lib/asciidoctor/converter/manpage.rb, line 262 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
# File lib/asciidoctor/converter/manpage.rb, line 273 def floating_title node %(.SS "#{manify node.title}") end
# File lib/asciidoctor/converter/manpage.rb, line 596 def inline_anchor node target = node.target case node.type when :link if target.start_with? 'mailto:' macro = 'MTO' target = target.slice 7, target.length else macro = 'URL' end if (text = node.text) == target text = '' else text = text.gsub '"', %[#{ESC_BS}(dq] end target = target.sub '@', %[#{ESC_BS}(at] if macro == 'MTO' %(#{ESC_BS}c#{LF}#{ESC_FS}#{macro} "#{target}" "#{text}" ) when :xref refid = (node.attr 'refid') || target node.text || (node.document.catalog[:ids][refid] || %([#{refid}])) when :ref, :bibref # These are anchor points, which shouldn't be visible '' else logger.warn %(unknown anchor type: #{node.type.inspect}) nil end end
# File lib/asciidoctor/converter/manpage.rb, line 625 def inline_break node %(#{node.text}#{LF}#{ESC_FS}br) end
# File lib/asciidoctor/converter/manpage.rb, line 633 def inline_callout node %(#{ESC_BS}fB(#{node.text})#{ESC_BS}fP) end
TODO supposedly groff has footnotes, but we're in search of an example
# File lib/asciidoctor/converter/manpage.rb, line 638 def inline_footnote node if (index = node.attr 'index') %([#{index}]) elsif node.type == :xref %([#{node.text}]) end end
# File lib/asciidoctor/converter/manpage.rb, line 646 def inline_image node (node.attr? 'link') ? %([#{node.alt}] <#{node.attr 'link'}>) : %([#{node.alt}]) end
# File lib/asciidoctor/converter/manpage.rb, line 650 def inline_indexterm node node.type == :visible ? node.text : '' end
# File lib/asciidoctor/converter/manpage.rb, line 654 def inline_kbd node if (keys = node.attr 'keys').size == 1 keys[0] else keys * %(#{ESC_BS}0+#{ESC_BS}0) end end
NOTE use fake <BOUNDARY> element to prevent creating artificial word boundaries
# File lib/asciidoctor/converter/manpage.rb, line 676 def inline_quoted node case node.type when :emphasis %(#{ESC_BS}fI<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP) when :strong %(#{ESC_BS}fB<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP) when :monospaced %[#{ESC_BS}f(CR<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}fP] when :single %[#{ESC_BS}(oq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(cq] when :double %[#{ESC_BS}(lq<BOUNDARY>#{node.text}</BOUNDARY>#{ESC_BS}(rq] else node.text end end
# File lib/asciidoctor/converter/manpage.rb, line 279 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
# File lib/asciidoctor/converter/manpage.rb, line 293 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
Converts HTML entity references back to their original form, escapes special man characters and strips trailing whitespace.
It's crucial that text only ever pass through manify once.
str - the String to convert opts - an Hash of options to control processing (default: {})
* :preserve_space a Boolean that indicates whether to preserve spaces (only expanding tabs) if true or to collapse all adjacent whitespace to a single space if false (default: true) * :append_newline a Boolean that indicates whether to append an endline to the result (default: false)
# File lib/asciidoctor/converter/manpage.rb, line 34 def manify str, opts = {} str = ((opts.fetch :preserve_space, true) ? (str.gsub TAB, ET) : (str.tr_s WHITESPACE, ' ')). gsub(LiteralBackslashRx, '\&(rs'). # literal backslash (not a troff escape sequence) gsub(LeadingPeriodRx, '\\\&.'). # leading . is used in troff for macro call or other formatting; replace with \&. # drop orphaned \c escape lines, unescape troff macro, quote adjacent character, isolate macro line gsub(EscapedMacroRx) { (rest = $3.lstrip).empty? ? %(.#$1"#$2") : %(.#$1"#$2"#{LF}#{rest}) }. gsub('-', '\-'). gsub('<', '<'). gsub('>', '>'). gsub(' ', '\~'). # non-breaking space gsub('©', '\(co'). # copyright sign gsub('®', '\(rg'). # registered sign gsub('™', '\(tm'). # trademark sign gsub(' ', ' '). # thin space gsub('–', '\(en'). # en dash gsub(EmDashCharRefRx, '\(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(EllipsisCharRefRx, '...'). # horizontal ellipsis gsub('←', '\(<-'). # leftwards arrow gsub('→', '\(->'). # rightwards arrow gsub('⇐', '\(lA'). # leftwards double arrow gsub('⇒', '\(rA'). # rightwards double arrow gsub('​', '\:'). # zero width space gsub('&','&'). # literal ampersand (NOTE must take place after any other replacement that includes &) gsub('\'', '\(aq'). # apostrophe-quote gsub(MockBoundaryRx, ''). # mock boundary gsub(ESC_BS, '\\'). # unescape troff backslash (NOTE update if more escapes are added) gsub(ESC_FS, '.'). # unescape full stop in troff commands (NOTE must take place after gsub(LeadingPeriodRx)) rstrip # strip trailing space opts[:append_newline] ? %(#{str}#{LF}) : str end
# File lib/asciidoctor/converter/manpage.rb, line 307 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
# File lib/asciidoctor/converter/manpage.rb, line 330 def open node case node.style when 'abstract', 'partintro' resolve_content node else node.content end end
# File lib/asciidoctor/converter/manpage.rb, line 342 def paragraph node if node.title? %(.sp .B #{manify node.title} .br #{manify node.content}) else %(.sp #{manify node.content}) end end
# File lib/asciidoctor/converter/manpage.rb, line 356 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
# File lib/asciidoctor/converter/manpage.rb, line 693 def resolve_content node node.content_model == :compound ? node.content : %(.sp#{LF}#{manify node.content}) end
# File lib/asciidoctor/converter/manpage.rb, line 179 def section node result = [] if node.level > 1 macro = 'SS' # QUESTION why captioned title? why not when level == 1? stitle = node.captioned_title else macro = 'SH' stitle = node.title.upcase end result << %(.#{macro} "#{manify stitle}" #{node.content}) result * LF end
# File lib/asciidoctor/converter/manpage.rb, line 69 def skip_with_warning node, name = nil logger.warn %(converter missing for #{name || node.node_name} node in manpage backend) nil end
# File lib/asciidoctor/converter/manpage.rb, line 387 def stem node title_element = node.title? ? %(.sp .B #{manify node.title} .br) : '' 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.
# File lib/asciidoctor/converter/manpage.rb, line 406 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 node.rows.by_section.each do |tsec, rows| rows.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').chr 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] << %(#{manify 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 :literal cell_content = %(.nf#{LF}#{manify cell.text}#{LF}.fi) when :verse cell_content = %(.nf#{LF}#{manify cell.text}#{LF}.fi) else cell_content = manify 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] << %(#{manify 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 unless rows.empty? end #row_header.each do |row| # result << LF # row.each_with_index do |cell, i| # result << (cell * ' ') # result << ' ' if row.size > i + 1 # end #end # FIXME temporary fix to get basic table to display result << LF result << row_header[0].map { 'lt' } * ' ' result << %(.#{LF}) row_text.each do |row| result << row.join end result << %(.TE#{LF}.sp) result.join end
# File lib/asciidoctor/converter/manpage.rb, line 530 def thematic_break node '.sp .ce \l\'\n(.lu*25u/100u\(ap\'' end
# File lib/asciidoctor/converter/manpage.rb, line 538 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
# File lib/asciidoctor/converter/manpage.rb, line 561 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
# File lib/asciidoctor/converter/manpage.rb, line 585 def video node start_param = (node.attr? 'start', nil, false) ? %(&start=#{node.attr 'start'}) : '' end_param = (node.attr? 'end', nil, false) ? %(&end=#{node.attr 'end'}) : '' result = [] result << %(.sp .B #{manify node.title} .br) if node.title? result << %(<#{node.media_uri(node.attr 'target')}#{start_param}#{end_param}> (video)) result * LF end
# File lib/asciidoctor/converter/manpage.rb, line 697 def write_alternate_pages mannames, manvolnum, target if mannames && mannames.size > 1 mannames.shift manvolext = %(.#{manvolnum}) dir, basename = ::File.split target mannames.each do |manname| ::IO.write ::File.join(dir, %(#{manname}#{manvolext})), %(.so #{basename}) end end end