TeX用のパーサを書いてみた

帰りの電車を、確実に座れるところまで反対方向に進んでから帰ってくるって少し遠回りして、パーサを実装してみた。

残念ながらネットワーク接続がなかったので、PLYを入れてみるとかTeXの文法についての形式的な定義を調べるとかできないので気合とフィーリングで適当に。まあテストケースは全部通るように作ったけど、漏れがあるかもねぇ。一応{}によるグルーピングと、それの\でのエスケープまではサポートしてある。

"""
texparse.py: parse TeX
"""
from string import ascii_letters
SPACES = " \t"
def spaces(s, i):
    r"""
    >>> spaces("  a", 0)
    ('  ', 2)
    """
    start = i
    while s[i] in SPACES:
        i += 1
    return (s[start:i], i)


def term(s, i):
    r"""
    >>> term("aaa", 0)
    ('aaa', 3)
    >>> term("aaa  ", 0)
    ('aaa', 3)
    >>> term(r"aaa\aaa", 0)
    ('aaa', 3)
    >>> term(r"\aaa aaa", 0)
    ('\\aaa', 4)
    >>> term(r"\{aaa", 0)
    ('{', 2)
    """
    start = i
    escaped = False # T when \ appeared
    while i < len(s):
        if s[i] in SPACES:
            break
        elif s[i] in "{}":
            if escaped:
                return (s[i], i + 1)
            break
        elif s[i] == "\\":
            if i != start:
                break
            escaped = True
        elif s[i] in ascii_letters:
            if escaped: escaped = False
        else:
            if i != start: break
            i += 1
            break

        i += 1
    return (s[start:i], i)


def brace(s, i):
    assert s[i] == "{"
    tokens, i = parse_tokens(s, i + 1)
    return (tokens, i)


def parse_tokens(s, i):
    tokens = []
    while i < len(s):
        if s[i] in SPACES:
            tok, i = spaces(s, i)
            tokens.append(tok)
        elif s[i] == "{":
            tok, i = brace(s, i)
            tokens.append(tok)
        elif s[i] == "}":
            i += 1
            break
        else:
            tok, i = term(s, i)
            tokens.append(tok)
            
    return (tokens, i)


def parse(s, i=0):
    r"""
    >>> parse("hoge")
    ['hoge']
    >>> parse(r"\ho\ge")
    ['\\ho', '\\ge']
    >>> parse(r"  \hoge")
    ['  ', '\\hoge']
    >>> parse(r"a{ho ge}ga")
    ['a', ['ho', ' ', 'ge'], 'ga']
    >>> parse(r"\{aaa\}")
    ['{', 'aaa', '}']
    >>> parse(r"\frac{x^2}{2} + x")
    ['\\frac', ['x', '^', '2'], ['2'], ' ', '+', ' ', 'x']

    """
    tokens, i = parse_tokens(s, 0)
    return tokens

def _test():
    import doctest
    doctest.testmod()

if __name__ == "__main__":
    _test()