mdoc2man.rb   [plain text]


#!/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: mdoc2man.rb 11708 2007-02-12 23:01:19Z shyouhei $
###

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 parse_macro(line)
    words = line.split
    retval = ''

    quote = []
    dl = false

    while word = words.shift
      case word
      when RE_PUNCT
	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
      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

      if word == 'Pa' && !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

      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'
	    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