#!/usr/bin/env python
# -*- coding: utf-8 -*-

""" Deroff.py, ported to Python from the venerable deroff.c """

import sys, re, string

IS_PY3 = sys.version_info[0] >= 3

class Deroffer:

    g_specs_specletter = {
        # Output composed latin1 letters
        '-D': '\320',
        'Sd': '\360',
        'Tp': '\376',
        'TP': '\336',
        'AE': '\306',
        'ae': '\346',
        'OE': "OE",
        'oe': "oe",
        ':a': '\344',
        ':A': '\304',
        ':e': '\353',
        ':E': '\313',
        ':i': '\357',
        ':I': '\317',
        ':o': '\366',
        ':O': '\326',
        ':u': '\374',
        ':U': '\334',
        ':y': '\377',
        'ss': '\337',
        '\'A': '\301',
        '\'E': '\311',
        '\'I': '\315',
        '\'O': '\323',
        '\'U': '\332',
        '\'Y': '\335',
        '\'a': '\341',
        '\'e': '\351',
        '\'i': '\355',
        '\'o': '\363',
        '\'u': '\372',
        '\'y': '\375',
        '^A': '\302',
        '^E': '\312',
        '^I': '\316',
        '^O': '\324',
        '^U': '\333',
        '^a': '\342',
        '^e': '\352',
        '^i': '\356',
        '^o': '\364',
        '^u': '\373',
        '`A': '\300',
        '`E': '\310',
        '`I': '\314',
        '`O': '\322',
        '`U': '\331',
        '`a': '\340',
        '`e': '\350',
        '`i': '\354',
        '`o': '\362',
        '`u': '\371',
        '~A': '\303',
        '~N': '\321',
        '~O': '\325',
        '~a': '\343',
        '~n': '\361',
        '~o': '\365',
        ',C': '\307',
        ',c': '\347',
        '/l': "/l",
        '/L': "/L",
        '/o': '\370',
        '/O': '\330',
        'oA': '\305',
        'oa': '\345',

        # Ligatures
        'fi': 'fi',
        'ff': 'ff',
        'fl': 'fl',

        'Fi': 'ffi',
        'Ff': 'fff',
        'Fl': 'ffl'
    }

    g_specs = {
        'mi': '-',
        'en': '-',
        'hy': '-',
        'em': "--",
        'lq': '“',
        'rq': '”',
        'Bq': ",,",
        'oq': '`',
        'cq': '\'',
        'aq': '\'',
        'dq': '"',
        'or': '|',
        'at': '@',
        'sh': '#',
        'Eu': '\244',
        'eu': '\244',
        'Do': '$',
        'ct': '\242',
        'Fo': '\253',
        'Fc': '\273',
        'fo': '<',
        'fc': '>',
        'r!': '\241',
        'r?': '\277',
        'Of': '\252',
        'Om': '\272',
        'pc': '\267',
        'S1': '\271',
        'S2': '\262',
        'S3': '\263',
        '<-': "<-",
        '->': "->",
        '<>': "<->",
        'ua': '^',
        'da': 'v',
        'lA': "<=",
        'rA': "=>",
        'hA': "<=>",
        'uA': "^^",
        'dA': "vv",
        'ba': '|',
        'bb': '|',
        'br': '|',
        'bv': '|',
        'ru': '_',
        'ul': '_',
        'ci': 'O',
        'bu': 'o',
        'co': '\251',
        'rg': '\256',
        'tm': "(TM)",
        'dd': "||",
        'dg': '|',
        'ps': '\266',
        'sc': '\247',
        'de': '\260',
        '%0': "0/00",
        '14': '\274',
        '12': '\275',
        '34': '\276',
        'f/': '/',
        'sl': '/',
        'rs': '\\',
        'sq': "[]",
        'fm': '\'',
        'ha': '^',
        'ti': '~',
        'lB': '[',
        'rB': ']',
        'lC': '{',
        'rC': '}',
        'la': '<',
        'ra': '>',
        'lh': "<=",
        'rh': "=>",
        'tf': "therefore",
        '~~': "~~",
        '~=': "~=",
        '!=': "!=",
        '**': '*',
        '+-': '\261',
        '<=': "<=",
        '==': "==",
        '=~': "=~",
        '>=': ">=",
        'AN': "\\/",
        'OR': "/\\",
        'no': '\254',
        'te': "there exists",
        'fa': "for all",
        'Ah': "aleph",
        'Im': "imaginary",
        'Re': "real",
        'if': "infinity",
        'md': "\267",
        'mo': "member of",
        'mu': '\327',
        'nm': "not member of",
        'pl': '+',
        'eq': '=',
        'pt': "oc",
        'pp': "perpendicular",
        'sb': "(=",
        'sp': "=)",
        'ib': "(-",
        'ip': "-)",
        'ap': '~',
        'is': 'I',
        'sr': "root",
        'pd': 'd',
        'c*': "(x)",
        'c+': "(+)",
        'ca': "cap",
        'cu': 'U',
        'di': '\367',
        'gr': 'V',
        'es': "{}",
        'CR': "_|",
        'st': "such that",
        '/_': "/_",
        'lz': "<>",
        'an': '-',

        # Output Greek
        '*A': "Alpha",
        '*B': "Beta",
        '*C': "Xi",
        '*D': "Delta",
        '*E': "Epsilon",
        '*F': "Phi",
        '*G': "Gamma",
        '*H': "Theta",
        '*I': "Iota",
        '*K': "Kappa",
        '*L': "Lambda",
        '*M': "Mu",
        '*N': "Nu",
        '*O': "Omicron",
        '*P': "Pi",
        '*Q': "Psi",
        '*R': "Rho",
        '*S': "Sigma",
        '*T': "Tau",
        '*U': "Upsilon",
        '*W': "Omega",
        '*X': "Chi",
        '*Y': "Eta",
        '*Z': "Zeta",
        '*a': "alpha",
        '*b': "beta",
        '*c': "xi",
        '*d': "delta",
        '*e': "epsilon",
        '*f': "phi",
        '+f': "phi",
        '*g': "gamma",
        '*h': "theta",
        '+h': "theta",
        '*i': "iota",
        '*k': "kappa",
        '*l': "lambda",
        '*m': "\265",
        '*n': "nu",
        '*o': "omicron",
        '*p': "pi",
        '+p': "omega",
        '*q': "psi",
        '*r': "rho",
        '*s': "sigma",
        '*t': "tau",
        '*u': "upsilon",
        '*w': "omega",
        '*x': "chi",
        '*y': "eta",
        '*z': "zeta",
        'ts': "sigma",
    }

    g_re_word = re.compile(r'[a-zA-Z_]+') # equivalent to the word() method
    g_re_number = re.compile(r'[+-]?\d+') # equivalent to the number() method
    g_re_esc_char = re.compile(r"""([a-zA-Z_]) |   # Word
                                   ([+-]?\d)   |   # Number
                                   \\              # Backslash (for escape seq)
                               """, re.VERBOSE)

    g_re_not_backslash_or_whitespace = re.compile(r'[^ \t\n\r\f\v\\]+') # Match a sequence of not backslash or whitespace

    g_re_newline_collapse = re.compile(r'\n{3,}')

    g_re_font = re.compile(r"""\\f(         # Starts with backslash f
                               (\(\S{2}) |  # Open paren, then two printable chars
                               (\[\S*?\]) |  # Open bracket, zero or more printable characters, then close bracket
                               \S)          # Any printable character
                            """,  re.VERBOSE)

    # This gets filled in in __init__ below
    g_macro_dict = False

    def __init__(self):
        self.reg_table = {}
        self.tr_from = ''
        self.tr_to = ''
        self.tr = ''
        self.nls = 2
        self.specletter = False
        self.refer = False
        self.macro = 0
        self.nobody = False
        self.inlist = False
        self.inheader = False
        self.pic = False
        self.tbl = False
        self.tblstate = 0
        self.tblTab = ''
        self.eqn = False
        self.skipheaders = False
        self.skiplists = False
        self.ignore_sonx = False
        self.output = []
        self.name = ''

        self.OPTIONS = 0
        self.FORMAT = 1
        self.DATA = 2

        # words is uninteresting and should be treated as false

        if not Deroffer.g_macro_dict:
            Deroffer.g_macro_dict = {
                'SH': Deroffer.macro_sh,
                'SS': Deroffer.macro_ss_ip,
                'IP': Deroffer.macro_ss_ip,
                'H ': Deroffer.macro_ss_ip,
                'I ': Deroffer.macro_i_ir,
                'IR': Deroffer.macro_i_ir,
                'IB': Deroffer.macro_i_ir,
                'B ': Deroffer.macro_i_ir,
                'BR': Deroffer.macro_i_ir,
                'BI': Deroffer.macro_i_ir,
                'R ': Deroffer.macro_i_ir,
                'RB': Deroffer.macro_i_ir,
                'RI': Deroffer.macro_i_ir,
                'AB': Deroffer.macro_i_ir,
                'Nm': Deroffer.macro_Nm,
                '] ': Deroffer.macro_close_bracket,
                'PS': Deroffer.macro_ps,
                'PE': Deroffer.macro_pe,
                'TS': Deroffer.macro_ts,
                'T&': Deroffer.macro_t_and,
                'TE': Deroffer.macro_te,
                'EQ': Deroffer.macro_eq,
                'EN': Deroffer.macro_en,
                'R1': Deroffer.macro_r1,
                'R2': Deroffer.macro_r2,
                'de': Deroffer.macro_de,
                'BL': Deroffer.macro_bl_vl,
                'VL': Deroffer.macro_bl_vl,
                'AL': Deroffer.macro_bl_vl,
                'LB': Deroffer.macro_bl_vl,
                'RL': Deroffer.macro_bl_vl,
                'ML': Deroffer.macro_bl_vl,
                'DL': Deroffer.macro_bl_vl,
                'BV': Deroffer.macro_bv,
                'LE': Deroffer.macro_le,
                'LP': Deroffer.macro_lp_pp,
                'PP': Deroffer.macro_lp_pp,
                'P\n': Deroffer.macro_lp_pp,
                'ds': Deroffer.macro_ds,
                'so': Deroffer.macro_so_nx,
                'nx': Deroffer.macro_so_nx,
                'tr': Deroffer.macro_tr,
                'sp': Deroffer.macro_sp
            }

    def flush_output(self, where):
        if where:
            where.write(self.get_output())
        self.output[:] = []

    def get_output(self):
        res = ''.join(self.output)
        clean_res = Deroffer.g_re_newline_collapse.sub('\n', res)
        return clean_res

    def putchar(self, c):
        self.output.append(c)
        return c

    # This gets swapped in in place of condputs the first time tr gets modified
    def condputs_tr(self, str):
        special = self.pic or self.eqn or self.refer or self.macro or (self.skiplists and self.inlist) or (self.skipheaders and self.inheader)
        if not special:
            self.output.append(str.translate(self.tr))

    def condputs(self, str):
        special = self.pic or self.eqn or self.refer or self.macro or (self.skiplists and self.inlist) or (self.skipheaders and self.inheader)
        if not special:
            self.output.append(str)

    def str_at(self, idx):
        return self.s[idx:idx+1]

    def skip_char(self, amt=1):
        self.s = self.s[amt:]

    def skip_leading_whitespace(self):
        self.s = self.s.lstrip()

    def is_white(self, idx):
        # Note this returns false for empty strings (idx >= len(self.s))
        return self.s[idx:idx+1].isspace()

    def str_eq(offset, other, len):
        return self.s[offset:offset+len] == other[:len]

    def prch(self, idx):
        # Note that this return False for the empty string (idx >= len(self.s))
        ch = self.s[idx:idx+1]
        return ch not in ' \t\n'

    def font(self):
        match = Deroffer.g_re_font.match(self.s)
        if not match: return False
        self.skip_char(match.end())
        return True

    def font2(self):
        if self.s[0:2] == '\\f':
            c = self.str_at(2)
            if c == '(' and self.prch(3) and self.prch(4):
                self.skip_char(5)
                return True
            elif c == '[':
                self.skip_char(2)
                while self.prch(0) and self.str_at(0) != ']': self.skip_char()
                if self.str_at(0) == ']': self.skip_char()
            elif self.prch(2):
                self.skip_char(3)
                return True
        return False

    def comment(self):
        # Here we require that the string start with \"
        while self.str_at(0) and self.str_at(0) != '\n': self.skip_char()
        return True

    def numreq(self):
        # We require that the string starts with backslash
        if self.str_at(1) in 'hvwud' and self.str_at(2) == '\'':
            self.macro += 1
            self.skip_char(3)
            while self.str_at(0) != '\'' and self.esc_char():
                pass # Weird
            if self.str_at(0) == '\'':
                self.skip_char()
            self.macro -= 1
            return True
        return False

    def var(self):
        reg = ''
        s0s1 = self.s[0:2]
        if s0s1 == '\\n':
            if self.s[3:5] == 'dy':
                self.skip_char(5)
                return True
            elif self.str_at(2) == '(' and self.prch(3) and self.prch(4):
                self.skip_char(5)
                return True
            elif self.str_at(2) == '[' and self.prch(3):
                self.skip_char(3)
                while self.str_at(0) and self.str_at(0) != ']':
                    self.skip_char()
                return True
            elif self.prch(2):
                self.skip_char(3)
                return True
        elif s0s1 == '\\*':
            if self.str_at(2) == '(' and self.prch(3) and self.prch(4):
                reg = self.s[3:5]
                self.skip_char(5)
            elif self.str_at(2) == '[' and self.prch(3):
                self.skip_char(3)
                while self.str_at(0) and self.str_at(0) != ']':
                    reg = reg + self.str_at(0)
                    self.skip_char()
                if self.s[0:1] == ']':
                    self.skip_char()
                else:
                    return False
            elif self.prch(2):
                reg = self.str_at(2)
                self.skip_char(3)
            else:
                return False

            if reg in self.reg_table:
                old_s = self.s
                self.s = self.reg_table[reg]
                self.text_arg()
                return True
        return False

    def size(self):
        # We require that the string starts with \s
        if self.digit(2) or (self.str_at(2) in '-+' and self.digit(3)):
            self.skip_char(3)
            while self.digit(0): self.skip_char()
            return True
        return False

    def spec(self):
        self.specletter = False
        if self.s[0:2] == '\\(' and self.prch(2) and self.prch(3):
            key = self.s[2:4]
            if key in Deroffer.g_specs_specletter:
                self.condputs(Deroffer.g_specs_specletter[key])
                self.specletter = True
            elif key in Deroffer.g_specs:
                self.condputs(Deroffer.g_specs[key])
            self.skip_char(4)
            return True
        elif self.s.startswith('\\%'):
            self.specletter = True
            self.skip_char(2)
            return True
        else:
            return False

    def esc(self):
        # We require that the string start with backslash
        c = self.s[1:2]
        if not c: return False
        if c in 'eE':
            self.condputs('\\')
        elif c in 't':
            self.condputs('\t')
        elif c in '0~':
            self.condputs(' ')
        elif c in '|^&:':
            pass
        else:
            self.condputs(c)
        self.skip_char(2)
        return True

    def word(self):
        got_something = False
        while True:
            match = Deroffer.g_re_word.match(self.s)
            if not match: break
            got_something = True
            self.condputs(match.group(0))
            self.skip_char(match.end(0))

            # Consume all specials
            while self.spec():
                if not self.specletter: break

        return got_something


    def text(self):
        while True:
            idx = self.s.find('\\')
            if idx == -1:
                self.condputs(self.s)
                self.s = ''
                break
            else:
                self.condputs(self.s[:idx])
                self.skip_char(idx)
                if not self.esc_char_backslash():
                    self.condputs(self.str_at(0))
                    self.skip_char()
        return True

    def letter(self, idx):
        ch = self.str_at(idx)
        return ch.isalpha() or ch == '_' # underscore is used in C identifiers


    def digit(self, idx):
        ch = self.str_at(idx)
        return ch.isdigit()

    def number(self):
        match = Deroffer.g_re_number.match(self.s)
        if not match:
            return False
        else:
            self.condputs(match.group(0))
            self.skip_char(match.end())
            return True

    def esc_char_backslash(self):
        # Like esc_char, but we know the string starts with a backslash
        c = self.s[1:2]
        if c == '"':
            return self.comment()
        elif c == 'f':
            return self.font()
        elif c == 's':
            return self.size()
        elif c in 'hvwud':
            return self.numreq()
        elif c in 'n*':
            return self.var()
        elif c == '(':
            return self.spec()
        else:
            return self.esc()


    def esc_char(self):
        if self.s[0:1] == '\\':
            return self.esc_char_backslash()
        return self.word() or self.number()

    def quoted_arg(self):
        if self.str_at(0) == '"':
            self.skip_char()
            while self.s and self.str_at(0) != '"':
                if not self.esc_char():
                    if self.s:
                        self.condputs(self.str_at(0))
                        self.skip_char()
            return True
        else:
            return False

    def text_arg(self):
        # PCA: The deroff.c textArg() disallowed quotes at the start of an argument
        # I'm not sure if this was a bug or not
        got_something = False
        while True:
            match = Deroffer.g_re_not_backslash_or_whitespace.match(self.s)
            if match:
                # Output the characters in the match
                self.condputs(match.group(0))
                self.skip_char(match.end(0))
                got_something = True

            # Next is either an escape, or whitespace, or the end
            # If it's the whitespace or the end, we're done
            if not self.s or self.is_white(0):
                return got_something

            # Try an escape
            if not self.esc_char():
                # Some busted escape? Just output it
                self.condputs(self.str_at(0))
                self.skip_char()
                got_something = True



    def text_arg2(self):
        if not self.esc_char():
            if self.s and not self.is_white(0):
                self.condputs(self.str_at(0))
                self.skip_char()
            else:
                return False
        while True:
            if not self.esc_char():
                if self.s and not self.is_white(0):
                    self.condputs(self.str_at(0))
                    self.skip_char()
                else:
                    return True


    # Macro functions
    def macro_sh(self):
        for header_str in [' SYNOPSIS', ' "SYNOPSIS', ' ‹BERSICHT', ' "‹BERSICHT']:
            if self.s[2:].startswith(header_str):
                self.inheader = True
                break
        else:
            # Did not find a header string
            self.inheader = False
            self.nobody = True

    def macro_ss_ip(self):
        self.nobody = True
        return False

    def macro_i_ir(self):
        pass
        return False

    def macro_Nm(self):
        if self.s == 'Nm\n':
            self.condputs(self.name)
        else:
            self.name = self.s[3:].strip() + ' '
        return True

    def macro_close_bracket(self):
        self.refer = False
        return False

    def macro_ps(self):
        if self.is_white(2): self.pic = True
        self.condputs('\n')
        return True

    def macro_pe(self):
        if self.is_white(2): self.pic = False
        self.condputs('\n')
        return True

    def macro_ts(self):
        if self.is_white(2): self.tbl, self.tblstate = True, self.OPTIONS
        self.condputs('\n')
        return True

    def macro_t_and(self):
        if self.is_white(2): self.tbl, self.tblstate = True, self.FORMAT
        self.condputs('\n')
        return True

    def macro_te(self):
        if self.is_white(2): self.tbl = False
        self.condputs('\n')
        return True

    def macro_eq(self):
        if self.is_white(2): self.eqn = True
        self.condputs('\n')
        return True

    def macro_en(self):
        if self.is_white(2): self.eqn = False
        self.condputs('\n')
        return True

    def macro_r1(self):
        if self.is_white(2): self.refer2 = True
        self.condputs('\n')
        return True

    def macro_r2(self):
        if self.is_white(2): self.refer2 = False
        self.condputs('\n')
        return True

    def macro_de(self):
        macro=True
        self.condputs('\n')
        return True

    def macro_bl_vl(self):
        if self.is_white(2): self.inlist = True
        self.condputs('\n')
        return True

    def macro_bv(self):
        if self.str_at(2) == 'L' and self.white(self.str_at(3)): self.inlist = True
        self.condputs('\n')
        return True

    def macro_le(self):
        if self.is_white(2): self.inlist = False
        self.condputs('\n')
        return True

    def macro_lp_pp(self):
        self.condputs('\n')
        return True

    def macro_ds(self):
        self.skip_char(2)
        self.skip_leading_whitespace()
        if self.str_at(0):
            # Split at whitespace
            comps = self.s.split(None, 2)
            if len(comps) is 2:
                name, value = comps
                value = value.rstrip()
                self.reg_table[name] = value
        self.condputs('\n')
        return True

    def macro_so_nx(self):
        # We always ignore include directives
        # deroff.c for some reason allowed this to fall through to the 'tr' case
        # I think that was just a bug so I won't replicate it
        return True

    def macro_tr(self):
        self.skip_char(2)
        self.skip_leading_whitespace()
        while self.s and self.str_at(0) != '\n':
            c = self.str_at(0)
            ns = self.str_at(1)
            self.skip_char(2)
            if not ns or ns == '\n': ns = ' '
            self.tr_from += c
            self.tr_to += ns

        # Update our table, then swap in the slower tr-savvy condputs
        try: #Python2
            self.tr = string.maketrans(self.tr_from, self.tr_to)
        except AttributeError: #Python3
            self.tr = "".maketrans(self.tr_from, self.tr_to)
        self.condputs = self.condputs_tr
        return True

    def macro_sp(self):
        self.condputs('\n')
        return True

    def macro_other(self):
        self.condputs('\n')
        return True

    def request_or_macro(self):
        # s[0] is period or open single quote
        self.skip_char()
        s0 = self.s[1:2]
        if s0 == '\\':
            if self.str_at(1) == '"':
                self.condputs('\n')
                return True
            else:
                pass
        elif s0 == '[':
            self.refer = True
            self.condputs('\n')
            return True
        elif s0 == ']':
            self.refer = False
            self.skip_char()
            return self.text()
        elif s0 == '.':
            self.macro = False
            self.condputs('\n')
            return True

        self.nobody = False
        s0s1 = self.s[0:2]

        macro_func = Deroffer.g_macro_dict.get(s0s1, Deroffer.macro_other)
        if macro_func(self):
            return True

        if self.skipheaders and self.nobody: return True

        self.skip_leading_whitespace()
        while self.s and not self.is_white(0): self.skip_char()
        self.skip_leading_whitespace()
        while True:
            if not self.quoted_arg() and not self.text_arg():
                if self.s:
                    self.condputs(self.str_at(0))
                    self.skip_char()
                else:
                    return True


    def request_or_macro2(self):
        self.skip_char()
        s0 = self.s[0:1]
        if s0 == '\\':
            if self.str_at(1) == '"':
                self.condputs('\n')
                return True
            else:
                pass
        elif s0 == '[':
            self.refer = True
            self.condputs('\n')
            return True
        elif s0 == ']':
            self.refer = False
            self.skip_char()
            return self.text()
        elif s0 == '.':
            self.macro = False
            self.condputs('\n')
            return True

        self.nobody = False
        s0s1 = self.s[0:2]
        if s0s1 == 'SH':
            for header_str in [' SYNOPSIS', ' "SYNOPSIS', ' ‹BERSICHT', ' "‹BERSICHT']:
                if self.s[2:].startswith(header_str):
                    self.inheader = True
                    break
            else:
                # Did not find a header string
                self.inheader = False
                self.nobody = True
        elif s0s1 in ['SS', 'IP', 'H ']:
            self.nobody = True
        elif s0s1 in ['I ', 'IR', 'IB', 'B ', 'BR', 'BI', 'R ', 'RB', 'RI', 'AB']:
            pass
        elif s0s1 in ['] ']:
            self.refer = False
        elif s0s1 in ['PS']:
            if self.is_white(2): self.pic = True
            self.condputs('\n')
            return True
        elif s0s1 in ['PE']:
            if self.is_white(2): self.pic = False
            self.condputs('\n')
            return True
        elif s0s1 in ['TS']:
            if self.is_white(2): self.tbl, self.tblstate = True, self.OPTIONS
            self.condputs('\n')
            return True
        elif s0s1 in ['T&']:
            if self.is_white(2): self.tbl, self.tblstate = True, self.FORMAT
            self.condputs('\n')
            return True
        elif s0s1 in ['TE']:
            if self.is_white(2): self.tbl = False
            self.condputs('\n')
            return True
        elif s0s1 in ['EQ']:
            if self.is_white(2): self.eqn = True
            self.condputs('\n')
            return True
        elif s0s1 in ['EN']:
            if self.is_white(2): self.eqn = False
            self.condputs('\n')
            return True
        elif s0s1 in ['R1']:
            if self.is_white(2): self.refer2 = True
            self.condputs('\n')
            return True
        elif s0s1 in ['R2']:
            if self.is_white(2): self.refer2 = False
            self.condputs('\n')
            return True
        elif s0s1 in ['de']:
            macro=True
            self.condputs('\n')
            return True
        elif s0s1 in ['BL', 'VL', 'AL', 'LB', 'RL', 'ML', 'DL']:
            if self.is_white(2): self.inlist = True
            self.condputs('\n')
            return True
        elif s0s1 in ['BV']:
            if self.str_at(2) == 'L' and self.white(self.str_at(3)): self.inlist = True
            self.condputs('\n')
            return True
        elif s0s1 in ['LE']:
            if self.is_white(2): self.inlist = False
            self.condputs('\n')
            return True
        elif s0s1 in ['LP', 'PP', 'P\n']:
            self.condputs('\n')
            return True
        elif s0s1 in ['ds']:
            self.skip_char(2)
            self.skip_leading_whitespace()
            if self.str_at(0):
                # Split at whitespace
                comps = self.s.split(None, 2)
                if len(comps) is 2:
                    name, value = comps
                    value = value.rstrip()
                    self.reg_table[name] = value
            self.condputs('\n')
            return True
        elif s0s1 in ['so', 'nx']:
            # We always ignore include directives
            # deroff.c for some reason allowed this to fall through to the 'tr' case
            # I think that was just a bug so I won't replicate it
            return True
        elif s0s1 in ['tr']:
            self.skip_char(2)
            self.skip_leading_whitespace()
            while self.s and self.str_at(0) != '\n':
                c = self.str_at(0)
                ns = self.str_at(1)
                self.skip_char(2)
                if not ns or ns == '\n': ns = ' '
                self.tr_from += c
                self.tr_to += ns

            # Update our table, then swap in the slower tr-savvy condputs
            try: #Python2
                self.tr = string.maketrans(self.tr_from, self.tr_to)
            except AttributeError: #Python3
                self.tr = "".maketrans(self.tr_from, self.tr_to)
            self.condputs = self.condputs_tr

            return True
        elif s0s1 in ['sp']:
            self.condputs('\n')
            return True
        else:
            self.condputs('\n')
            return True

        if self.skipheaders and self.nobody: return True

        self.skip_leading_whitespace()
        while self.s and not self.is_white(0): self.skip_char()
        self.skip_leading_whitespace()
        while True:
            if not self.quoted_arg() and not self.text_arg():
                if self.s:
                    self.condputs(self.str_at(0))
                    self.skip_char()
                else:
                    return True


    def do_tbl(self):
        if self.tblstate == self.OPTIONS:
            while self.s and self.str_at(0) != ';' and self.str_at(0) != '\n':
                self.skip_leading_whitespace()
                if not self.str_at(0).isalpha():
                    # deroff.c has a bug where it can loop forever here...we try to work around it
                    self.skip_char()
                else: # Parse option

                    option = self.s
                    arg = ''

                    idx = 0
                    while option[idx:idx+1].isalpha():
                        idx += 1

                    if option[idx:idx+1] == '(':
                        option = option[:idx]
                        self.s = self.s[idx+1:]
                        arg = self.s
                    else:
                        self.s = ''

                    if arg:
                        idx = arg.find(')')
                        if idx != -1:
                            arg = arg[:idx]
                        self.s = self.s[idx+1:]
                    else:
                        #self.skip_char()
                        pass

                    if option.lower() == 'tab':
                        self.tblTab = arg[0:1]

            self.tblstate = self.FORMAT
            self.condputs('\n')

        elif self.tblstate == self.FORMAT:
            while self.s and self.str_at(0) != '.' and self.str_at(0) != '\n':
                self.skip_leading_whitespace()
                if self.str_at(0): self.skip_char()

            if self.str_at(0) == '.': self.tblstate = self.DATA
            self.condputs('\n')
        elif self.tblstate == self.DATA:
            if self.tblTab:
                self.s = self.s.replace(self.tblTab, '\t')
            self.text()
        return True

    def do_line(self):
        if self.s[0:1] in ".'":
            if not self.request_or_macro(): return False
        elif self.tbl:
            self.do_tbl()
        else:
            self.text()
        return True

    def deroff(self, str):
        lines = str.split('\n')
        for line in lines:
            self.s = line + '\n'
            if not self.do_line():
                break
            #self.putchar('\n')

def deroff_files(files):
    for arg in files:
        sys.stderr.write(arg + '\n')
        if arg.endswith('.gz'):
            f = gzip.open(arg, 'r')
            str = f.read()
            if IS_PY3: str = str.decode('latin-1')
        else:
            f = open(arg, 'r')
            str = f.read()
        d = Deroffer()
        d.deroff(str)
        d.flush_output(sys.stdout)
        f.close()



if __name__ == "__main__":
    import gzip
    paths = sys.argv[1:]
    if True:
        deroff_files(paths)
    else:
        import cProfile, profile, pstats
        profile.run('deroff_files(paths)', 'fooprof')
        p = pstats.Stats('fooprof')
        p.sort_stats('time').print_stats(100)
        #p.sort_stats('calls').print_callers(.5, 'startswith')