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
- LeadingPeriodRx
- LiteralBackslashRx
- MockBoundaryRx
- WHITESPACE
- WrappedIndentRx
Public Class Methods
# File lib/asciidoctor/converter/manpage.rb, line 26 def initialize backend, opts = {} @backend = backend init_backend_traits basebackend: 'manpage', filetype: 'man', outfilesuffix: '.man', supports_templates: true end
# File lib/asciidoctor/converter/manpage.rb, line 668 def self.write_alternate_pages mannames, manvolnum, target if mannames && mannames.size > 1 mannames.shift manvolext = %(.#{manvolnum}) dir, basename = ::File.split target mannames.each do |manname| ::File.write ::File.join(dir, %(#{manname}#{manvolext})), %(.so #{basename}), mode: FILE_WRITE_MODE end end end
Public Instance Methods
# File lib/asciidoctor/converter/manpage.rb, line 152 def convert_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 #{enclose_content node} .sp .5v .RE) result.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 170 def convert_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, whitespace: :normalize) result << item.content if item.blocks? result << 'T}' end result << '.TE' result.join LF end
TODO implement horizontal (if it makes sense)
# File lib/asciidoctor/converter/manpage.rb, line 191 def convert_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 }.join ' '} .RS 4) else result << %(.sp #{manify terms.map {|dt| dt.text }.join(', '), whitespace: :normalize} .RS 4) end if dd result << (manify dd.text, whitespace: :normalize) if dd.text? result << dd.content if dd.blocks? end result << '.RE' end result.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 31 def convert_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 manmanual = node.attr 'manmanual' mansource = node.attr 'mansource' 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: #{manmanual ? (manmanual.tr_s WHITESPACE, ' ') : '\ \&'} .\\" Source: #{mansource ? (mansource.tr_s WHITESPACE, ' ') : '\ \&'} .\\" Language: English .\\") # TODO add document-level setting to disable capitalization of manname result << %(.TH "#{manify manname.upcase}" "#{manvolnum}" "#{docdate}" "#{mansource ? (manify mansource) : '\ \&'}" "#{manmanual ? (manify manmanual) : '\ \&'}") # 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', 'NAME').upcase}" #{mannames.map {|n| manify n }.join ', '} \\- #{manify node.attr('manpurpose'), whitespace: :normalize}) 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 unless (authors = node.authors).empty? if authors.size > 1 result << '.SH "AUTHORS"' authors.each do |author| result << %(.sp #{author.name}) end else result << %(.SH "AUTHOR" .sp #{authors[0].name}) end end result.join LF end
NOTE embedded doesn't really make sense in the manpage backend
# File lib/asciidoctor/converter/manpage.rb, line 124 def convert_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.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 218 def convert_example node result = [] result << (node.title? ? %(.sp .B #{manify node.captioned_title} .br) : '.sp') result << %(.RS 4 #{enclose_content node} .RE) result.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 229 def convert_floating_title node %(.SS "#{manify node.title}") end
# File lib/asciidoctor/converter/manpage.rb, line 233 def convert_image node result = [] result << (node.title? ? %(.sp .B #{manify node.captioned_title} .br) : '.sp') result << %([#{node.alt}]) result.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 568 def convert_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 unless (text = node.text) refid = node.attributes['refid'] text = %([#{refid}]) unless AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid]) && (@resolving_xref ||= outer = true) && outer && (text = ref.xreftext node.attr 'xrefstyle', nil, true) end text 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 600 def convert_inline_break node %(#{node.text}#{LF}#{ESC_FS}br) end
# File lib/asciidoctor/converter/manpage.rb, line 608 def convert_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 613 def convert_inline_footnote node if (index = node.attr 'index') %([#{index}]) elsif node.type == :xref %([#{node.text}]) end end
# File lib/asciidoctor/converter/manpage.rb, line 621 def convert_inline_image node (node.attr? 'link') ? %([#{node.alt}] <#{node.attr 'link'}>) : %([#{node.alt}]) end
# File lib/asciidoctor/converter/manpage.rb, line 625 def convert_inline_indexterm node node.type == :visible ? node.text : '' end
# File lib/asciidoctor/converter/manpage.rb, line 629 def convert_inline_kbd node if (keys = node.attr 'keys').size == 1 keys[0] else keys.join %(#{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 651 def convert_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 242 def convert_listing node result = [] result << %(.sp .B #{manify node.captioned_title} .br) if node.title? result << %(.sp .if n .RS 4 .nf .fam C #{manify node.content, whitespace: :preserve} .fam .fi .if n .RE) result.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 258 def convert_literal node result = [] result << %(.sp .B #{manify node.title} .br) if node.title? result << %(.sp .if n .RS 4 .nf .fam C #{manify node.content, whitespace: :preserve} .fam .fi .if n .RE) result.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 285 def convert_olist node result = [] result << %(.sp .B #{manify node.title} .br) if node.title? start = (node.attr 'start', 1).to_i node.items.each_with_index do |item, idx| result << %(.sp .RS 4 .ie n \\{\\ \\h'-04' #{numeral = idx + start}.\\h'+01'\\c .\\} .el \\{\\ . sp -1 . IP " #{numeral}." 4.2 .\\} #{manify item.text, whitespace: :normalize}) result << item.content if item.blocks? result << '.RE' end result.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 309 def convert_open node case node.style when 'abstract', 'partintro' enclose_content node else node.content end end
# File lib/asciidoctor/converter/manpage.rb, line 321 def convert_paragraph node if node.title? %(.sp .B #{manify node.title} .br #{manify node.content, whitespace: :normalize}) else %(.sp #{manify node.content, whitespace: :normalize}) end end
# File lib/asciidoctor/converter/manpage.rb, line 336 def convert_quote node result = [] if node.title? result << %(.sp .RS 3 .B #{manify node.title} .br .RE) end attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil attribution_line = (node.attr? 'attribution') ? %[#{attribution_line}\\(em #{node.attr 'attribution'}] : nil result << %(.RS 3 .ll -.6i #{enclose_content node} .br .RE .ll) if attribution_line result << %(.RS 5 .ll -.10i #{attribution_line} .RE .ll) end result.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 137 def convert_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.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 363 def convert_stem node result = [] result << (node.title? ? %(.sp .B #{manify node.title} .br) : '.sp') open, close = BLOCK_MATH_DELIMITERS[node.style.to_sym] if ((equation = node.content).start_with? open) && (equation.end_with? close) equation = equation.slice open.length, equation.length - open.length - close.length end result << %(#{manify equation, whitespace: :preserve} (#{node.style})) result.join LF 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 382 def convert_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.to_h.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, whitespace: :normalize}#{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, whitespace: :preserve}#{LF}.fi) else cell_content = manify cell.content.join, whitespace: :normalize 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, whitespace: :normalize}#{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.join ' ') # result << ' ' if row.size > i + 1 # end #end # FIXME temporary fix to get basic table to display result << LF result << ('lt ' * row_header[0].size).chop 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 504 def convert_thematic_break node '.sp .ce \l\'\n(.lu*25u/100u\(ap\'' end
# File lib/asciidoctor/converter/manpage.rb, line 512 def convert_ulist node result = [] result << %(.sp .B #{manify node.title} .br) if node.title? node.items.map do |item| result << %[.sp .RS 4 .ie n \\{\\ \\h'-04'\\(bu\\h'+03'\\c .\\} .el \\{\\ . sp -1 . IP \\(bu 2.3 .\\} #{manify item.text, whitespace: :normalize}] result << item.content if item.blocks? result << '.RE' end result.join LF end
FIXME git uses [verse] for the synopsis; detect this special case
# File lib/asciidoctor/converter/manpage.rb, line 535 def convert_verse node result = [] result << (node.title? ? %(.sp .B #{manify node.title} .br) : '.sp') 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, whitespace: :preserve} .fi .br) if attribution_line result << %(.in +.5i .ll -.5i #{attribution_line} .in .ll) end result.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 557 def convert_video node start_param = (node.attr? 'start') ? %(&start=#{node.attr 'start'}) : '' end_param = (node.attr? 'end') ? %(&end=#{node.attr 'end'}) : '' result = [] result << (node.title? ? %(.sp .B #{manify node.title} .br) : '.sp') result << %(<#{node.media_uri(node.attr 'target')}#{start_param}#{end_param}> (video)) result.join LF end
Private Instance Methods
# File lib/asciidoctor/converter/manpage.rb, line 736 def enclose_content node node.content_model == :compound ? node.content : %(.sp#{LF}#{manify node.content, whitespace: :normalize}) 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: {})
* :whitespace an enum that indicates how to handle whitespace; supported options are: :preserve - preserve spaces (only expanding tabs); :normalize - normalize whitespace (remove spaces around newlines); :collapse - collapse adjacent whitespace to a single space (default: :collapse) * :append_newline a Boolean that indicates whether to append a newline to the result (default: false)
# File lib/asciidoctor/converter/manpage.rb, line 693 def manify str, opts = {} case opts.fetch :whitespace, :collapse when :preserve str = str.gsub TAB, ET when :normalize str = str.gsub WrappedIndentRx, LF else str = str.tr_s WHITESPACE, ' ' end str = str. gsub(LiteralBackslashRx) { $1 ? $& : '\\(rs' }. # literal backslash (not a troff escape sequence) gsub(EllipsisCharRefRx, '...'). # horizontal ellipsis 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('←', '\(<-'). # 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