diff --git a/lib/asciidoctor/converter/factory.rb b/lib/asciidoctor/converter/factory.rb
index 9719659bda..81b693dc30 100644
--- a/lib/asciidoctor/converter/factory.rb
+++ b/lib/asciidoctor/converter/factory.rb
@@ -207,6 +207,11 @@ def create backend, opts = {}
require 'asciidoctor/converter/docbook45'.to_s
end
DocBook45Converter.new backend, opts
+ when 'manpage'
+ unless defined? ::Asciidoctor::Converter::ManPageConverter
+ require 'asciidoctor/converter/manpage'.to_s
+ end
+ ManPageConverter.new backend, opts
end
return base_converter unless opts.key? :template_dirs
diff --git a/lib/asciidoctor/converter/manpage.rb b/lib/asciidoctor/converter/manpage.rb
new file mode 100644
index 0000000000..abaff8428f
--- /dev/null
+++ b/lib/asciidoctor/converter/manpage.rb
@@ -0,0 +1,707 @@
+module Asciidoctor
+ # A built-in {Converter} implementation that generates Troff Manpage output
+ # The output follows groff man page definition:
+ # http://www.gnu.org/software/groff/manual/html_node/Man-usage.html#Man-usage
+ # but also tries to be consistent with the a2x tool from AsciiDoc Python.
+ class Converter::ManPageConverter < Converter::BuiltIn
+ QUOTE_TAGS = {
+ :emphasis => ['\fI', '\fR', true],
+ :strong => ['\fB', '\fR', true],
+ # :monospaced => ['', '
', true],
+ # :superscript => ['', '', true],
+ # :subscript => ['', '', true],
+ :double => ['"', '"', false],
+ :single => ["'", "'", false],
+ # :mark => ['', '', true],
+ :asciimath => ['\\$', '\\$', false],
+ :latexmath => ['\\(', '\\)', false]
+ # Opal can't resolve these constants when referenced here
+ #:asciimath => INLINE_MATH_DELIMITERS[:asciimath] + [false],
+ #:latexmath => INLINE_MATH_DELIMITERS[:latexmath] + [false]
+ }
+ QUOTE_TAGS.default = [nil, nil, nil]
+
+ def initialize backend, opts = {}
+ @xml_mode = opts[:htmlsyntax] == 'xml'
+ @void_element_slash = @xml_mode ? '/' : nil
+ @stylesheets = Stylesheets.instance
+ # TODO:
+ # Use this list to make sure manify changes '.' at the beginning of line
+ # except when used in one of these commands.
+ # TODO: Complete the list
+ @used_troff_dot_commands = ['.SH ', '.SS ', '.PP', '.RS', '.RE', '.de', '.if']
+ end
+
+ # optionally folds each endline into a single space, escapes special man characters,
+ # reverts HTML entity references back to their original form, strips trailing
+ # whitespace and, optionally, appends a newline
+ def manify(str, append_newline = false, preserve_space = true)
+ if preserve_space
+ str = str.gsub("\t", (' ' * 8))
+ else
+ str = str.tr_s("\n\t ", ' ')
+ end
+ # TODO:
+ # Do not manify . everywhere since it is used for troff macros
+ # To begin a line with a literal period, use the zero-width non-printing
+ # escape sequence \& to get the period away from the beginning of the
+ # line, which is the only place it is treated specially: \&. This line
+ # begins with a dot.
+ # http://web.archive.org/web/20060102165607/http://people.debian.org/~branden/talks/wtfm/wtfm.pdf
+ # .gsub(/\./, '\\\&.')
+ str.
+ gsub('^.$', '\\\&.'). # lone . is also used in troff to indicate paragraph continuation with visual separator
+ gsub('-', '\\-').
+ gsub('<', '<').
+ gsub('>', '>').
+ gsub('©', '\\(co'). # copyright sign
+ gsub('®', '\\(rg'). # registered sign
+ gsub('™', '\\(tm'). # trademark sign
+ gsub(' ', ' '). # thin space #gsub(' — ', ' \\(em ').
+ gsub('', ''). # zero width 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('…', '\\\&...').
+ gsub('←', '\\(<-'). # leftwards arrow
+ gsub('→', '\\(->'). # rightwards arrow
+ gsub('⇐', '\\(lA'). # leftwards double arrow
+ gsub('⇒', '\\(rA'). # rightwards double arrow
+ gsub('\'', '\\(aq'). # apostrophe-quote
+ rstrip + (append_newline ? EOL : '')
+ end
+
+ def document node
+ result = []
+ # TODO
+ # lang_attribute = (node.attr? 'nolang') ? nil : %( lang="#{node.attr 'lang', 'en'}")
+ header_title = (node.attr? 'mantitle') ? (node.attr 'mantitle') : node.doctitle.split('(').first
+ header_number = node.doctitle.split('(').last.chop
+ header_author = (node.attr? 'authors') ? (node.attr 'authors') : '[see the "AUTHORS" section]'
+ header_generator = "Asciidoctor #{ node.attr 'asciidoctor-version' }"
+ header_date = node.attr 'docdate'
+ header_manual = (node.attr? 'manmanual') ? (node.attr 'manmanual') : '\ \&'
+ header_source = (node.attr? 'mansource') ? (node.attr 'mansource') : '\ \&'
+ result << %Q('\\" t
+.\\" Title: #{ header_title.downcase }
+.\\" Author: #{ header_author }
+.\\" Generator: #{ header_generator }
+.\\" Date: #{ header_date }
+.\\" Manual: #{ header_manual }
+.\\" Source: #{ header_source }
+.\\" Language: English
+.\\")
+ # .TH name section(1-8) date version
+ # Don't capitalize name, that's a presentation decision.
+ result << %Q(.TH "#{ manify header_title, false }" "#{ header_number }" "#{ header_date }" "#{manify header_source, false}" "#{manify header_manual, false}")
+ # http://bugs.debian.org/507673
+ # http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+ result << %(.ie \\n\(.g .ds Aq \\\(aq
+.el .ds Aq ')
+ # disable hyphenation
+ result << %Q(.nh)
+ # disable justification (adjust text to left margin only)
+ result << %Q(.ad l)
+ # URL portability. Taken from:
+ # http://web.archive.org/web/20060102165607/http://people.debian.org/~branden/talks/wtfm/wtfm.pdf
+ # Use: .URL "http://www.debian.org" "Debian" "*"
+ # The first argument is the URL, the second is the text to be
+ # hyperlinked, and the third (optional) argument is any text that needs
+ # to immediately trail the hyperlink without intervening whitespace
+ # GNU roff defines a URL macro; what the above does is test for the
+ # presence of GNU roff, and source the www.tmac macro definition file
+ # (which itself also defines URL) if it is — this overrides the
+ # definition just made, but leaves it intact for non-GNU roff
+ # implementations.
+ result << %(.\\" URL Portability
+.de URL
+\\\\$2 \\\(laURL: \\\\$1 \\\(ra\\\\$3
+..
+.if \\n[.g] .mso www.tmac)
+
+ unless node.noheader
+ if node.attr? 'manpurpose'
+ #.SH Unnumbered section heading
+ result << %(.SH "#{manify node.attr 'manname-title'}")
+ result << %(#{manify node.attr 'mantitle'} \\- #{manify node.attr 'manpurpose'})
+ end
+ end
+
+ result << %(#{node.content})
+
+ if node.footnotes? && !(node.attr? 'nofootnotes')
+ result << %(.SH "NOTES")
+ node.footnotes.each do |footnote|
+ result << %(#{footnote.index}". #{footnote.text})
+ end
+ end
+ if node.attr? 'authors'
+ result << %Q(.SH "AUTHOR(S)"
+.PP
+\\fB#{ node.attr 'authors' }\\fR
+.RS 4
+Author(s).
+.RE
+)
+ end
+ result * EOL
+ end
+
+ def embedded node
+ result = []
+ if !node.notitle && node.has_header?
+ result << %(.SH #{node.header.title}
+.sp)
+ end
+
+ result << node.content
+
+ if node.footnotes? && !(node.attr? 'nofootnotes')
+ result << %(.SH "NOTES")
+ node.footnotes.each do |footnote|
+ result << %(#{footnote.index}. #{footnote.text})
+ end
+ end
+
+ result * EOL
+ 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
+ result << %(.SH "#{manify node.title}"
+#{node.content})
+ else
+ result << %(.SS "#{manify node.captioned_title}")
+ result << node.content
+ end
+ result * EOL
+ end
+
+ def admonition node
+ result = []
+ result << %(\\fB#{node.title}\\fR\n.br) if node.title?
+ result << %(.if n \\{\\
+.sp
+.\\}
+.RS 4
+.it 1 an-trap
+.nr an-no-space-flag 1
+.nr an-break-flag 1
+.br
+.ps +1
+\\fB#{ node.caption }\\fR
+.ps -1
+.br
+#{ manify node.content }
+.sp .5v
+.RE)
+ result * EOL
+ end
+
+ def audio node
+ ''
+ end
+
+ def colist node
+ result = []
+ result << %(.B #{manify node.title}) if node.title?
+ result << %(.TS
+tab\(:\);
+r lw\(\\n\(.lu*75u/100u\).)
+
+ node.items.each_with_index do |item, index|
+ result << %(\\fB#{index + 1}.\\fR\\h'-2n':T{
+#{manify item.text}
+T})
+ end
+ result << ".TE"
+ result * EOL
+ end
+
+ def dlist node
+ result = []
+ counter = 0
+ node.items.each do |terms, dd|
+ case node.style
+ when 'qanda'
+ counter += 1
+ [*terms].each do |dt|
+ result << ".sp"
+ result << %(#{counter}. #{manify dt.text}\n.RS 4)
+ end
+ when 'horizontal'
+ counter += 1
+ [*terms].each do |dt|
+ result << ".sp"
+ result << %(#{manify dt.text}\n.RS 4)
+ end
+ else
+ counter += 1
+ [*terms].each do |dt|
+ result << ".sp"
+ result << %(.B #{manify dt.text}\n.RS 4)
+ end
+ end
+ if dd
+ result << %(#{manify dd.text}) if dd.text?
+ result << ".sp" if dd.text? && dd.blocks?
+ result << %(#{dd.content}) if dd.blocks?
+ end
+ result << ".RE"
+ end
+ result * EOL
+ end
+
+ def example node
+ result = []
+ result << %(.B #{node.captioned_title}\n.br) if node.title?
+ result << %(.if n \\{\\
+.sp
+.\\}
+.RS 4
+.it 1 an-trap
+.nr an-no-space-flag 1
+.nr an-break-flag 1
+.br
+#{ manify node.content }
+.sp .5v
+.RE)
+ result * EOL
+ end
+
+ def floating_title node
+ %(.SS "#{manify node.title}"\n.sp\n)
+ end
+
+ def image node
+ ''
+ end
+
+ def listing node
+ result = []
+ result << %(\\fB#{node.title}\\fR\n.br) if node.title?
+ result << %(.sp
+.if n \\{\\
+.RS 4
+.\\}
+.nf
+#{manify node.content, true, true}
+.fi
+.if n \\{\\
+.RE
+.\\})
+ result * EOL
+ end
+
+ def literal node
+ result = []
+ result << %(\\fB#{node.title}\\fR\n.br) if node.title?
+ result << %(.sp
+.if n \\{\\
+.RS 4
+.\\}
+.nf
+#{manify node.content, true, true}
+.fi
+.if n \\{\\
+.RE
+.\\}
+)
+ result * EOL
+ end
+
+ def olist node
+ result = []
+ result << %(\\fB#{node.title}\\fR\n.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 * EOL
+ end
+
+ def open node
+ node.content
+ end
+
+ def page_break node
+ ''
+ end
+
+ def paragraph node
+ if node.title?
+ %(.B #{manify node.title}
+.br
+#{manify node.content}
+)
+ else
+ # Change paragraph separator to .sp instead of .PP
+ # .PP breaks indentation of outher blocks.
+ %(.sp
+#{manify node.content})
+ end
+ end
+
+ def pass node
+ node.content
+ end
+
+ def preamble node
+ node.content
+ end
+
+ def quote node
+ result = []
+ title_element = node.title? ? %(.in +.3i\n\\fB#{node.title}\\fR\n.br\n.in\n) : nil
+ attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil
+ attribution_line = (node.attr? 'attribution') ? %(#{attribution_line}\\\(em #{node.attr 'attribution'}) : nil
+ result << %(#{title_element}.in +.5i
+.ll -.5i
+.nf
+#{node.content}
+.fi
+.br
+.in
+.ll)
+ if attribution_line
+ result << %(.in +.3i
+.ll -.3i
+#{attribution_line}
+.in
+.ll)
+ end
+ result * EOL
+ end
+
+ def sidebar node
+ ''
+ end
+
+ def stem node
+ title_element = node.title? ? %(\\fB#{node.title}\\fR\n.br\n) : 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 += %Q(.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 += "\n"
+ # 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] << %Q(T{\n.sp\nT}:)
+ end
+ row_text[row_index] << %Q(T{\n.sp\n)
+ cell_halign = case cell.attr 'halign'
+ when 'right'
+ 'r'
+ when 'left'
+ 'l'
+ when 'center'
+ 'c'
+ else
+ 'l'
+ end
+ 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 + "\n")
+ 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
+ cell_content = %(#{cell.text})
+ when :literal
+ cell_content = %(#{cell.text})
+ else
+ cell_content = ''
+ cell.content.each do |text|
+ cell_content = %(#{cell_content}#{text})
+ end
+ end
+ row_text[row_index] << (cell_content + "\n")
+ 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 + "\n")
+ 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] << %Q(T}:)
+ else
+ row_text[row_index] << %Q(T}\n)
+ end
+ end
+ row_index += 1
+ end
+ end
+ row_header.each do |row|
+ result += "\n"
+ row.each_with_index do |cell, i|
+ result += cell.join(' ')
+ result += ' ' if row.size > i+1
+ end
+ end
+ result += ".\n"
+ row_text.each do |row|
+ result += row.join('')
+ end
+ result += %(.TE\n.sp 1\n)
+ end
+
+ def thematic_break node
+ ''
+ end
+
+ def toc node
+ ''
+ end
+
+ def ulist node
+ result = []
+ result << %(\\fB#{node.title}\\fR) 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 * EOL
+ end
+
+ def verse node
+ result = []
+ title_element = node.title? ? %(.in +.3i\n\\fB#{node.title}\\fR\n.br\n.in\n) : nil
+ attribution_line = (node.attr? 'citetitle') ? %(#{node.attr 'citetitle'} ) : nil
+ attribution_line = (node.attr? 'attribution') ? %(#{attribution_line}\\\(em #{node.attr 'attribution'}) : nil
+ result << %(#{title_element}.in +.5i
+.ll -.5i
+.nf
+#{node.content}
+.fi
+.br
+.in
+.ll)
+ if attribution_line
+ result << %(.in +.3i
+.ll -.3i
+#{attribution_line}
+.in
+.ll)
+ end
+ result * EOL
+ 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
+ %(.URL "#{node.media_uri(node.attr 'target')}#{start_param}#{end_param}" "#{node.captioned_title}")
+ end
+
+ def inline_anchor node
+ target = node.target
+ case node.type
+ when :xref
+ refid = (node.attr 'refid') || target
+ # NOTE we lookup text in converter because DocBook doesn't need this logic
+ text = node.text || (node.document.references[:ids][refid] || %([#{refid}]))
+ # FIXME shouldn't target be refid? logic seems confused here
+ %(\n.URL "#{target}" "#{manify text}"\n)
+ when :ref
+ %(\n.URL "#{target}"\n)
+ when :link
+ %(\n.URL "#{target}" "#{manify node.text}"\n)
+ when :bibref
+ %(\n.URL "#{target}"\n)
+ else
+ %(\n.URL "#{target}"\n)
+ end
+ end
+
+ def inline_break node
+ %(#{node.text}\n.br)
+ end
+
+ def inline_button node
+ %([#{node.text}])
+ end
+
+ def inline_callout node
+ %(\\fB#{node.text}\\fR\n)
+ end
+
+ def inline_footnote node
+ if (index = node.attr 'index')
+ %(\n.URL "#{index}" "View footnote."\n)
+ elsif node.type == :xref
+ %(\n.URL "[#{node.text}]" "Unresolved footnote reference."\n)
+ end
+ end
+
+ def inline_image node
+ if (type = node.type) == 'icon' && (node.document.attr? 'icons', 'font')
+ img = node.attr 'title'
+ elsif type == 'icon' && !(node.document.attr? 'icons')
+ img = node.attr 'alt'
+ else
+ if node.attr? 'title'
+ img = (node.attr? 'alt') ? %(#{node.attr 'alt'} > #{node.attr 'title'}) : node.attr('title')
+ elsif node.attr? 'alt'
+ img = node.attr 'alt'
+ end
+ end
+
+ if node.attr? 'link'
+ (img != nil) ? %(\n.URL "#{node.attr 'link'}" "#{img}"\n) : node.attr('link')
+ else
+ (img != nil) ? %(\\fB[#{img}]\\fR\n) : ''
+ end
+ 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
+ key_combo = keys.join(' + ')
+ %([#{key_combo}])
+ end
+ end
+
+ def inline_menu node
+ menu = node.attr 'menu'
+ if !(submenus = node.attr 'submenus').empty?
+ submenu_path = submenus.join(' > ')
+ %(\\fB#{menu} > #{submenu_path} > #{node.attr 'menuitem'}\\fR\n)
+ elsif (menuitem = node.attr 'menuitem')
+ %(\\fB#{menu} > #{menuitem}\\fR\n)
+ else
+ %(\\fB #{menu}\\fR\n)
+ end
+ end
+
+ def inline_quoted node
+ case node.type
+ when :emphasis
+ %[\\fI#{node.text}\\fR]
+ when :strong
+ %[\\fB#{node.text}\\fR]
+ when :single
+ %[\\(oq#{node.text}\\(cq]
+ when :double
+ %[\\(lq#{node.text}\\(rq]
+ else
+ node.text
+ end
+ end
+ end
+end
diff --git a/lib/asciidoctor/document.rb b/lib/asciidoctor/document.rb
index 5600219a0c..92f0097324 100644
--- a/lib/asciidoctor/document.rb
+++ b/lib/asciidoctor/document.rb
@@ -898,6 +898,8 @@ def update_backend_attributes new_backend, force = false
new_backend = new_backend[1..-1]
elsif new_backend.start_with? 'html'
attrs['htmlsyntax'] = 'html'
+ elsif new_backend.start_with? 'manpage'
+ attrs['htmlsyntax'] = 'html'
end
if (resolved_name = BACKEND_ALIASES[new_backend])
new_backend = resolved_name