ruby--ruby/tool/mdoc2man.rb

506 lines
10 KiB
Ruby
Executable File

#!/usr/bin/env ruby
###
### mdoc2man - mdoc to man converter
###
### Quick usage: mdoc2man.rb < mdoc_manpage.8 > man_manpage.8
###
### Ported from Perl by Akinori MUSHA.
###
### Copyright (c) 2001 University of Illinois Board of Trustees
### Copyright (c) 2001 Mark D. Roth
### Copyright (c) 2002, 2003 Akinori MUSHA
### All rights reserved.
###
### Redistribution and use in source and binary forms, with or without
### modification, are permitted provided that the following conditions
### are met:
### 1. Redistributions of source code must retain the above copyright
### notice, this list of conditions and the following disclaimer.
### 2. Redistributions in binary form must reproduce the above copyright
### notice, this list of conditions and the following disclaimer in the
### documentation and/or other materials provided with the distribution.
### 3. All advertising materials mentioning features or use of this software
### must display the following acknowledgement:
### This product includes software developed by the University of
### Illinois at Urbana, and their contributors.
### 4. The University nor the names of their
### contributors may be used to endorse or promote products derived from
### this software without specific prior written permission.
###
### THIS SOFTWARE IS PROVIDED BY THE TRUSTEES AND CONTRIBUTORS ``AS IS'' AND
### ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
### IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
### ARE DISCLAIMED. IN NO EVENT SHALL THE TRUSTEES OR CONTRIBUTORS BE LIABLE
### FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
### DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
### OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
### HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
### LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
### OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
### SUCH DAMAGE.
###
### $Id$
###
class Mdoc2Man
ANGLE = 1
OPTION = 2
PAREN = 3
RE_PUNCT = /^[!"'),\.\/:;>\?\]`]$/
def initialize
@name = @date = @id = nil
@refauthors = @reftitle = @refissue = @refdate = @refopt = nil
@optlist = 0 ### 1 = bullet, 2 = enum, 3 = tag, 4 = item
@oldoptlist = 0
@nospace = 0 ### 0, 1, 2
@enum = 0
@synopsis = true
@reference = false
@ext = false
@extopt = false
@literal = false
end
def mdoc2man(i, o)
i.each { |line|
if /^\./ !~ line
o.print line
o.print ".br\n" if @literal
next
end
line.slice!(0, 1)
next if /\\"/ =~ line
line = parse_macro(line) and o.print line
}
initialize
end
def shift_arg(words)
case words[0]
when nil, RE_PUNCT
nil
when /\A"(.+)/
words.shift
word = $1
loop {
break if word.chomp!('"')
token = words.shift or break
word << ' ' << token
}
word
else
words.shift
end
end
def parse_macro(line)
words = line.split
retval = ''
quote = []
dl = false
while word = words.shift
case word
when RE_PUNCT
next retval << word if word == ':'
while q = quote.pop
case q
when OPTION
retval << ']'
when PAREN
retval << ')'
when ANGLE
retval << '>'
end
end
retval << word
next
when 'Li', 'Pf'
@nospace = 1
next
when 'Xo'
@ext = true
retval << ' ' unless retval.empty? || /[\n ]\z/ =~ retval
next
when 'Xc'
@ext = false
retval << "\n" unless @extopt
break
when 'Bd'
@literal = true if words[0] == '-literal'
retval << "\n"
break
when 'Ed'
@literal = false
break
when 'Ns'
@nospace = 1 if @nospace == 0
retval.chomp!(' ')
next
when 'No'
retval.chomp!(' ')
retval << words.shift
next
when 'Dq'
retval << '``'
begin
retval << words.shift << ' '
end until words.empty? || RE_PUNCT =~ words[0]
retval.chomp!(' ')
retval << '\'\''
@nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
next
when 'Sq', 'Ql'
retval << '`' << words.shift << '\''
@nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
next
# when 'Ic'
# retval << '\\fB' << words.shift << '\\fP'
# next
when 'Oo'
#retval << "[\\c\n"
@extopt = true
@nospace = 1 if @nospace == 0
retval << '['
next
when 'Oc'
@extopt = false
retval << ']'
next
when 'Ao'
@nospace = 1 if @nospace == 0
retval << '<'
next
when 'Ac'
retval << '>'
next
end
retval << ' ' if @nospace == 0 && !(retval.empty? || /[\n ]\z/ =~ retval)
@nospace = 0 if @nospace == 1
case word
when 'Dd'
@date = words.join(' ')
return nil
when 'Dt'
if words.size >= 2 && words[1] == '""' &&
/^(.*)\(([0-9])\)$/ =~ words[0]
words[0] = $1
words[1] = $2
end
@id = words.join(' ')
return nil
when 'Os'
retval << '.TH ' << @id << ' "' << @date << '" "' <<
words.join(' ') << '"'
break
when 'Sh'
retval << '.SH'
@synopsis = (words[0] == 'SYNOPSIS')
next
when 'Xr'
retval << '\\fB' << words.shift <<
'\\fP(' << words.shift << ')' << (words.shift||'')
break
when 'Rs'
@refauthors = []
@reftitle = ''
@refissue = ''
@refdate = ''
@refopt = ''
@reference = true
break
when 'Re'
retval << "\n"
# authors
while @refauthors.size > 1
retval << @refauthors.shift << ', '
end
retval << 'and ' unless retval.empty?
retval << @refauthors.shift
# title
retval << ', \\fI' << @reftitle << '\\fP'
# issue
retval << ', ' << @refissue unless @refissue.empty?
# date
retval << ', ' << @refdate unless @refdate.empty?
# optional info
retval << ', ' << @refopt unless @refopt.empty?
retval << ".\n"
@reference = false
break
when 'An'
next
when 'Dl'
retval << ".nf\n" << '\\& '
dl = true
next
when 'Ux'
retval << "UNIX"
next
when 'Bro'
retval << '{'
@nospace = 1 if @nospace == 0
next
when 'Brc'
retval.sub!(/ *\z/, '}')
next
end
if @reference
case word
when '%A'
@refauthors.unshift(words.join(' '))
break
when '%T'
@reftitle = words.join(' ')
@reftitle.sub!(/^"/, '')
@reftitle.sub!(/"$/, '')
break
when '%N'
@refissue = words.join(' ')
break
when '%D'
@refdate = words.join(' ')
break
when '%O'
@refopt = words.join(' ')
break
end
end
case word
when 'Nm'
name = words.empty? ? @name : words.shift
@name ||= name
retval << ".br\n" if @synopsis
retval << "\\fB" << name << "\\fP"
@nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
next
when 'Nd'
retval << '\\-'
next
when 'Fl'
retval << '\\fB\\-' << words.shift << '\\fP'
@nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
next
when 'Ar'
retval << '\\fI'
if words.empty?
retval << 'file ...\\fP'
else
retval << words.shift << '\\fP'
while words[0] == '|'
retval << ' ' << words.shift << ' \\fI' << words.shift << '\\fP'
end
@nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
next
end
when 'Cm'
retval << '\\fB' << words.shift << '\\fP'
while RE_PUNCT =~ words[0]
retval << words.shift
end
next
when 'Op'
quote << OPTION
@nospace = 1 if @nospace == 0
retval << '['
# words.push(words.pop + ']')
next
when 'Aq'
quote << ANGLE
@nospace = 1 if @nospace == 0
retval << '<'
# words.push(words.pop + '>')
next
when 'Pp'
retval << "\n"
next
when 'Ss'
retval << '.SS'
next
end
case word
when 'Pa'
if !quote.include?(OPTION)
retval << '\\fI'
retval << '\\&' if /^\./ =~ words[0]
retval << words.shift << '\\fP'
while RE_PUNCT =~ words[0]
retval << words.shift
end
# @nospace = 1 if @nospace == 0 && RE_PUNCT =~ words[0]
next
end
when 'Lk'
if !quote.include?(OPTION)
url = words.shift
if name = shift_arg(words)
retval << '\\fI' << name << ':\\fP '
end
retval << '\\fB'
retval << '\\&' if /\A\./ =~ url
retval << url << '\\fP'
next
end
end
case word
when 'Dv'
retval << '.BR'
next
when 'Em', 'Ev'
retval << '.IR'
next
when 'Pq'
retval << '('
@nospace = 1
quote << PAREN
next
when 'Sx', 'Sy'
retval << '.B ' << words.join(' ')
break
when 'Ic'
retval << '\\fB'
until words.empty? || RE_PUNCT =~ words[0]
case words[0]
when 'Op'
words.shift
retval << '['
words.push(words.pop + ']')
next
when 'Aq'
words.shift
retval << '<'
words.push(words.pop + '>')
next
when 'Ar'
words.shift
retval << '\\fI' << words.shift << '\\fP'
else
retval << words.shift
end
retval << ' ' if @nospace == 0
end
retval.chomp!(' ')
retval << '\\fP'
retval << words.shift unless words.empty?
break
when 'Bl'
@oldoptlist = @optlist
case words[0]
when '-bullet'
@optlist = 1
when '-enum'
@optlist = 2
@enum = 0
when '-tag'
@optlist = 3
when '-item'
@optlist = 4
end
break
when 'El'
@optlist = @oldoptlist
next
end
if @optlist != 0 && word == 'It'
case @optlist
when 1
# bullets
retval << '.IP \\(bu'
when 2
# enum
@enum += 1
retval << '.IP ' << @enum << '.'
when 3
# tags
retval << ".TP\n"
case words[0]
when 'Pa', 'Ev', 'Lk'
words.shift
retval << '.B'
end
when 4
# item
retval << ".IP\n"
end
next
end
case word
when 'Sm'
case words[0]
when 'off'
@nospace = 2
when 'on'
# retval << "\n"
@nospace = 0
end
words.shift
next
end
retval << word
end
return nil if retval == '.'
retval.sub!(/\A\.([^a-zA-Z])/, "\\1")
# retval.chomp!(' ')
while q = quote.pop
case q
when OPTION
retval << ']'
when PAREN
retval << ')'
when ANGLE
retval << '>'
end
end
# retval << ' ' unless @nospace == 0 || retval.empty? || /\n\z/ =~ retval
retval << ' ' unless !@ext || @extopt || / $/ =~ retval
retval << "\n" unless @ext || @extopt || retval.empty? || /\n\z/ =~ retval
retval << ".fi\n" if dl
return retval
end
def self.mdoc2man(i, o)
new.mdoc2man(i, o)
end
end
if $0 == __FILE__
Mdoc2Man.mdoc2man(ARGF, STDOUT)
end