class Asciidoctor::Converter::ManPageConverter
A built-in {Converter} implementation that generates the man page (troff) format.
The output of this converter adheres to the man definition as defined by groff and uses the manpage output of the DocBook toolchain as a foundation. That means if you’ve previously been generating man pages using the a2x tool from AsciiDoc.py, you should be able to achieve a very similar result using this converter. Though you’ll also get to enjoy some notable enhancements that have been added since, such as the customizable linkstyle.
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
- MalformedEscapedMacroRx
- MockMacroRx
- PCDATAFilterRx
- WHITESPACE
- WrappedIndentRx
- XMLMarkupRx
Public Class Methods
# File lib/asciidoctor/converter/manpage.rb, line 33 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 666 def self.write_alternate_pages mannames, manvolnum, target return unless 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
Public Instance Methods
# File lib/asciidoctor/converter/manpage.rb, line 153 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 171 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 192 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 38 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).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" .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 128 def convert_embedded node result = [node.content] append_footnotes result, node # QUESTION should we add an AUTHOR(S) section? result.join LF end
# File lib/asciidoctor/converter/manpage.rb, line 219 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 230 def convert_floating_title node %(.SS "#{manify node.title}") end
# File lib/asciidoctor/converter/manpage.rb, line 234 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 563 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) if AbstractNode === (ref = (@refs ||= node.document.catalog[:refs])[refid = node.attributes['refid']] || (refid.nil_or_empty? ? (top = get_root_document node) : nil)) if (@resolving_xref ||= (outer = true)) && outer && (text = ref.xreftext node.attr 'xrefstyle', nil, true) text = uppercase_pcdata text if ref.context === :section && ref.level < 2 && text == ref.title else text = top ? '[^top]' : %([#{refid}]) end @resolving_xref = nil if outer else text = %([#{refid}]) end 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 603 def convert_inline_break node %(#{node.text}#{LF}#{ESC_FS}br) end
# File lib/asciidoctor/converter/manpage.rb, line 611 def convert_inline_callout node %(<#{ESC_BS}fB>(#{node.text})<#{ESC_BS}fP>) end
# File lib/asciidoctor/converter/manpage.rb, line 615 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 623 def convert_inline_image node (node.attr? 'link') ? %([#{node.alt}] <#{node.attr 'link'}>) : %([#{node.alt}]) end
# File lib/asciidoctor/converter/manpage.rb, line 627 def convert_inline_indexterm node node.type == :visible ? node.text : '' end
# File lib/asciidoctor/converter/manpage.rb, line 631 def convert_inline_kbd node %[<#{ESC_BS}f(CR>#{(keys = node.attr 'keys').size == 1 ? keys[0] : (keys.join "#{ESC_BS}0+#{ESC_BS}0")}</#{ESC_BS}fP>] end
NOTE use fake XML elements to prevent creating artificial word boundaries
# File lib/asciidoctor/converter/manpage.rb, line 649 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
# File lib/asciidoctor/converter/manpage.rb, line 243 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 259 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 286 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 310 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 319 def convert_page_break node '.bp' end
# File lib/asciidoctor/converter/manpage.rb, line 323 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 338 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 138 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
# File lib/asciidoctor/converter/manpage.rb, line 365 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 384 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 == :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}) else # tsec == :head || 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 498 def convert_thematic_break node '.sp .ce \l\'\n(.lu*25u/100u\(ap\'' end
# File lib/asciidoctor/converter/manpage.rb, line 506 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
# File lib/asciidoctor/converter/manpage.rb, line 528 def convert_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, 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 552 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