class Asciidoctor::Converter::ManPageConverter
See www.gnu.org/software/groff/manual/html_node/Man-usage.html#Man-usage<br><br>enhancements that have been added since, such as the customizable linkstyle.
using this converter. Though you’ll also get to enjoy some notable
from AsciiDoc.py, you should be able to achieve a very similar result
That means if you’ve previously been generating man pages using the a2x tool
groff and uses the manpage output of the DocBook toolchain as a foundation.
The output of this converter adheres to the man definition as defined by
A built-in {Converter} implementation that generates the man page (troff) format.
def self.write_alternate_pages mannames, manvolnum, target
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
def append_footnotes result, node
def append_footnotes result, node if node.footnotes? && !(node.attr? 'nofootnotes') result << '.SH "NOTES"' node.footnotes.each_with_index do |fn, idx| result << %(.IP [#{fn.index}]) # NOTE restore newline in escaped macro that gets removed by normalize_text in substitutor if (text = fn.text).include? %(#{ESC}\\c #{ESC}.) text = (manify %(#{text.gsub MalformedEscapedMacroRx, %(\\1#{LF}\\2)} ), whitespace: :normalize).chomp ' ' else text = manify text, whitespace: :normalize end result << text end end end
def convert_admonition node
def convert_admonition node result = [] result << %(.if n .sp S 4 t 1 an-trap r an-no-space-flag 1 r an-break-flag 1 r s +1 #{node.attr 'textlabel'}#{node.title? ? "\\fP: #{manify node.title}" : ''} s -1 r enclose_content node} p .5v E) result.join LF end
def convert_colist node
def convert_colist node result = [] result << %(.sp #{manify node.title} r) if node.title? result << '.TS b(:); 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
def convert_dlist node
def convert_dlist node result = [] result << %(.sp #{manify node.title} r) 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 ' '} S 4) else result << %(.sp manify terms.map {|dt| dt.text }.join(', '), whitespace: :normalize} S 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
def convert_document node
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 ls MTO URL f \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).gsub '\-', '-' }.join ', '} \\- #{manify node.attr('manpurpose'), whitespace: :normalize}) end end result << node.content # QUESTION should NOTES come after AUTHOR(S)? append_footnotes result, node 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" p authors[0].name}) end end result.join LF end
def convert_embedded node
def convert_embedded node result = [node.content] append_footnotes result, node # QUESTION should we add an AUTHOR(S) section? result.join LF end
def convert_example node
def convert_example node result = [] result << (node.title? ? %(.sp #{manify node.captioned_title} r) : '.sp') result << %(.RS 4 enclose_content node} E) result.join LF end
def convert_floating_title node
def convert_floating_title node %(.SS "#{manify node.title}") end
def convert_image node
def convert_image node result = [] result << (node.title? ? %(.sp #{manify node.captioned_title} r) : '.sp') result << %([#{node.alt}]) result.join LF end
def convert_inline_anchor node
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
def convert_inline_break node
def convert_inline_break node %(#{node.text}#{LF}#{ESC_FS}br) end
def convert_inline_button node
def convert_inline_button node %(#{ESC_BS}fB[#{ESC_BS}0#{node.text}#{ESC_BS}0]#{ESC_BS}fP) end
def convert_inline_callout node
def convert_inline_callout node %(#{ESC_BS}fB(#{node.text})#{ESC_BS}fP) end
def convert_inline_footnote node
def convert_inline_footnote node if (index = node.attr 'index') %([#{index}]) elsif node.type == :xref %([#{node.text}]) end end
def convert_inline_image node
def convert_inline_image node (node.attr? 'link') ? %([#{node.alt}] <#{node.attr 'link'}>) : %([#{node.alt}]) end
def convert_inline_indexterm node
def convert_inline_indexterm node node.type == :visible ? node.text : '' end
def convert_inline_kbd node
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
def convert_inline_menu node
def convert_inline_menu node caret = %[#{ESC_BS}0#{ESC_BS}(fc#{ESC_BS}0] menu = node.attr 'menu' if !(submenus = node.attr 'submenus').empty? submenu_path = submenus.map {|item| %(#{ESC_BS}fI#{item}#{ESC_BS}fP) }.join caret %(#{ESC_BS}fI#{menu}#{ESC_BS}fP#{caret}#{submenu_path}#{caret}#{ESC_BS}fI#{node.attr 'menuitem'}#{ESC_BS}fP) elsif (menuitem = node.attr 'menuitem') %(#{ESC_BS}fI#{menu}#{caret}#{menuitem}#{ESC_BS}fP) else %(#{ESC_BS}fI#{menu}#{ESC_BS}fP) end end
def convert_inline_quoted node
def convert_inline_quoted node case node.type when :emphasis %(<#{ESC_BS}fI>#{node.text}</#{ESC_BS}fP>) when :strong %(<#{ESC_BS}fB>#{node.text}</#{ESC_BS}fP>) when :monospaced %[<#{ESC_BS}f(CR>#{node.text}</#{ESC_BS}fP>] when :single %[<#{ESC_BS}(oq>#{node.text}</#{ESC_BS}(cq>] when :double %[<#{ESC_BS}(lq>#{node.text}</#{ESC_BS}(rq>] else node.text end end
def convert_listing node
def convert_listing node result = [] result << %(.sp #{manify node.captioned_title} r) if node.title? result << %(.sp f n .RS 4 f am C manify node.content, whitespace: :preserve} am i f n .RE) result.join LF end
def convert_literal node
def convert_literal node result = [] result << %(.sp #{manify node.title} r) if node.title? result << %(.sp f n .RS 4 f am C manify node.content, whitespace: :preserve} am i f n .RE) result.join LF end
def convert_olist node
def convert_olist node result = [] result << %(.sp #{manify node.title} r) if node.title? start = (node.attr 'start', 1).to_i node.items.each_with_index do |item, idx| result << %(.sp S 4 e n \\{\\ h'-04' #{numeral = idx + start}.\\h'+01'\\c \} l \\{\\ sp -1 IP " #{numeral}." 4.2 \} manify item.text, whitespace: :normalize}) result << item.content if item.blocks? result << '.RE' end result.join LF end
def convert_open node
def convert_open node case node.style when 'abstract', 'partintro' enclose_content node else node.content end end
def convert_page_break node
def convert_page_break node '.bp' end
def convert_paragraph node
def convert_paragraph node if node.title? %(.sp #{manify node.title} r manify node.content, whitespace: :normalize}) else %(.sp manify node.content, whitespace: :normalize}) end end
def convert_quote node
def convert_quote node result = [] if node.title? result << %(.sp S 3 #{manify node.title} r E) 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 l -.6i enclose_content node} r E l) if attribution_line result << %(.RS 5 l -.10i attribution_line} E l) end result.join LF end
def convert_section node
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 = uppercase_pcdata node.title end result << %(.#{macro} "#{manify stitle}" node.content}) result.join LF end
def convert_sidebar node
def convert_sidebar node result = [] result << (node.title? ? %(.sp #{manify node.title} r) : '.sp') result << %(.RS 4 enclose_content node} E) result.join LF end
def convert_stem node
def convert_stem node result = [] result << (node.title? ? %(.sp #{manify node.title} r) : '.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
def convert_table node
To fix this, asciidoctor needs to provide an API to tell the user if a
create empty cells as placeholders of the span.
method has to create a map of all cells and in the case of rowspans
receiving empty(marked) cells when there are colspans or rowspans. This
FIXME: The reason this method is so complicated is because we are not
def convert_table node result = [] if node.title? result << %(.sp t 1 an-trap r an-no-space-flag 1 r an-break-flag 1 r #{manify node.captioned_title} end result << '.TS lbox 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
def convert_thematic_break node
def convert_thematic_break node '.sp e \'\n(.lu*25u/100u\(ap\'' end
def convert_ulist node
def convert_ulist node result = [] result << %(.sp #{manify node.title} r) if node.title? node.items.map do |item| result << %[.sp S 4 e n \\{\\ h'-04'\\(bu\\h'+03'\\c \} l \\{\\ sp -1 IP \\(bu 2.3 \} manify item.text, whitespace: :normalize}] result << item.content if item.blocks? result << '.RE' end result.join LF end
def convert_verse node
def convert_verse node result = [] if node.title? result << %(.sp #{manify node.title} r) end attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil attribution_line = (node.attr? 'attribution') ? %[#{attribution_line}\\(em #{node.attr 'attribution'}] : nil result << %(.sp f manify node.content, whitespace: :preserve} i r) if attribution_line result << %(.in +.5i l -.5i attribution_line} n l) end result.join LF end
def convert_video node
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 #{manify node.title} r) : '.sp') result << %(<#{node.media_uri(node.attr 'target')}#{start_param}#{end_param}> (video)) result.join LF end
def enclose_content node
def enclose_content node node.content_model == :compound ? node.content : %(.sp#{LF}#{manify node.content, whitespace: :normalize}) end
def initialize backend, opts = {}
def initialize backend, opts = {} @backend = backend init_backend_traits basebackend: 'manpage', filetype: 'man', outfilesuffix: '.man', supports_templates: true end
def manify str, opts = {}
space (default: :collapse)
(remove spaces around newlines); :collapse - collapse adjacent whitespace to a single
:preserve - preserve spaces (only expanding tabs); :normalize - normalize whitespace
* :whitespace an enum that indicates how to handle whitespace; supported options are:
opts - an Hash of options to control processing (default: {})
str - the String to convert
It's crucial that text only ever pass through manify once.
special man characters and strips trailing whitespace.
Converts HTML entity references back to their original form, escapes
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.rstrip}"#{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(MockMacroRx, '\1'). # 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
def uppercase_pcdata string
def uppercase_pcdata string (XMLMarkupRx.match? string) ? string.gsub(PCDATAFilterRx) { $2 ? $2.upcase : $1 } : string.upcase end