晴耕雨読

working in the fields on fine days and reading books on rainy days

[Perl6] Raku で LaTeX 文章の構文解析をする

Perl6 (Raku) では新しい機能として Grammar が導入されたので、ちょっと遊んでみました。

Grammar による構文解析

例えば、LaTeX は

\foo{bar}

のような命令と、

\begin{foo}
    bar
\end{foo}

のようなブロック命令1があり、それ以外は文章となります。 また例外的に、

{\Large Emergency request}

のときには { } は表示されず、プログラムでいうところのスコープなるものが作られるので、注意が必要です。

これらを踏まえて、LaTeX の構造を拡張 BNF で示すと次のようになります2

TOP           ::= <exp>*
<exp>         ::= <curlybrace> | <block> | <command> | <text>
<curlybrace>  ::= { <exp>* }
<block>       ::= \\begin{ <blockname> }
                  <exp>*
                  \\end{ $<blockname> }
<command>     ::= \\ <name> <curlybrace>
<name>        ::= [\w_]+ \*?
<text>        ::= [^\\\{\}$]+

Grammar

詳しい話は公式のドキュメント Grammar Tutorial に書いてありますが、改めて説明すると grammar の宣言はクラスの宣言に似ており、parse というメソッドが自動的に定義されます。 この parse は与えられた文字列に対して、TOP という特別なトークンから解析を開始します。 TOP トークン以外は、regex, token, rule のいずれかを使って、トークンを定義します。 この3つの定義方法の違いは以下の通りです。

  • regex は普通の正規表現(どのオプションも指定されていない)
  • token はオプション「Ratchet」が指定された正規表現
    • token name { ... }regex name { :r ... } と等しい
    • オプション「Ratchet」は、正規表現によるバックトラックを禁止することができる
  • rule はオプション「Ratchet」と「Sigspace」が指定された正規表現
    • rule name { ... }regex name { :r :s ... } と等しい
    • オプション「Sigspace」は、空白も正規表現でマッチするようにすることができる

Perl6 の正規表現は Perl5 の正規表現と全く違うので、もし Perl6 の正規表現がわからないという方は Regexes をご一読ください。

先ほどの拡張 BNF を Perl6 の Grammar で表すと次のようになります。

use v6;

grammar Grammar {
    token name { <[ \w _ ]>+ \*? }

    # 式は 中かっこ か ブロック命令 か 命令 か それ以外は文章
    token exp  { <curlybrace> || <block> || <command> || <text> }

    # 文章は \ { } 以外の全ての文字
    token text { ( <-[ \\ \{ \} ]>+ ) }

    # ブロック
    rule block {
        '\begin{' $<blockname>=[<name>] '}'
        [ <exp> ]*?
        '\end{' $<blockname> '}'
    }

    # 命令
    token command {
        '\\' <name> <curlybrace>
    }

    # 中かっこ
    rule curlybrace {
        '{' <exp>* '}'
    }

    # 構文解析の開始トークン
    token TOP {
        ^
        \n*
        [ <exp> ]*
        $
    }
}

my $contents = q:to/EOS/;
\documentclass{jsarticle}
\lstset{ language = c, numbers = left }

\begin{foo}
  \begin{bar}
    nested block test
  \end{bar}

  \lstinputlisting{../src/abc.c}
\end{foo}
EOS

my $result = Grammar.parse($contents);
say $result;

上のコードを実行すると改行が多くて読みにくいが、階層構造が正しく解析されているのが確認できます。

exp => 「\documentclass{jsarticle}
」
  command => 「\documentclass{jsarticle}
」
   name => 「documentclass」
   curlybrace => 「{jsarticle}
」
    exp => 「jsarticle」
     text => 「jsarticle」
      0 => 「jsarticle」

上は出力の一部ですが、コマンド「\documentclass{jsarticle}」の名前は「documentclass」で、引数は「{jsarticle}」であることがわかります。

解析結果の JSON 化

このままの結果を使うのも良いですが、JSON にして必要な情報だけにする方法についても説明します。

まず、流れとしては、

LaTeX文字列 ==> Grammarで解析 ==> Actionで必要な情報だけ抜き取る ==> to-jsonでJSON化

という感じで進めていきます。

とりあえず次のような Action を作成しました。

class Latex::Action {
    method TOP($/) {
        make $<exp>».ast;
    }
    method exp($/) {
        make $/.values[0].ast;
    }
    method text($/) {
        make $0.Str.trim;
    }
    method command($/) {
        my @arguments = $<curlybrace>;

        my %node = %{ command => $<name>.Str };
        %node<args> = @arguments».ast if @arguments.elems > 0;
        make %node;
    }
    method block($/) {
        my %node = %{ block => $<name>.Str.trim };
        %node<contents> = $<exp>».ast;
        make %node;
    }
    method curlybrace($/) {
        make { contents => $/.values.Array».ast };
    }
}

マッチオブジェクトには ast という木構造を作るためのメソッド(?)があるので、これを呼ぶとその下に続く構造を作る(make)することができる。

  • TOP の下には、複数の exp をもつため、それぞれの exp に対して ast を呼んでいる
  • exp の下には、中かっこ、ブロック命令、命令、文章のいずれかがあるため、それに対して ast を呼んでいる
  • text は文章であるため、前後の空白を取り除いた値を返している
  • command は、命令名(name)と引数(curlybrace)の値をもつハッシュを返している
  • block は、命令名(name)とブロックの中身(複数の exp)の値をもつハッシュを返している

この Action を Grammar でパースするときの引数に渡してあげ、made メソッドを呼び出すと Action によって作成されたオブジェクトを得るとこができます。 また、そのままでは読みにくいので、JSON::Fast の to-json を使って、整形した出力を得るには、次のようにすれば良いです。

use JSON::Fast;

my $actions = Latex::Action;
my $json = Grammar.parse($contents, :$actions).made;
say to-json($json, :pretty);

このプログラムを実行すると次のJSONを得るとこができます。

[
  {
    "command": "documentclass",
    "args": [
      {
        "contents": [
          "jsarticle"
        ]
      }
    ]
  },
  {
    "command": "lstset",
    "args": [
      {
        "contents": [
          "language = c, numbers = left"
        ]
      }
    ]
  },
  {
    "block": "foo",
    "contents": [
      {
        "block": "bar",
        "contents": [
          "nested block test"
        ]
      },
      {
        "command": "lstinputlisting",
        "args": [
          {
            "contents": [
              "../src/abc.c"
            ]
          }
        ]
      }
    ]
  }
]

まとめ

ここまで示したコードは LaTeX の構文解析を行うための簡単な例です。 この拡張版として yalp という Perl6 で書いた LaTeX の構文解析ツールを作りましたが、これは普通の LaTeX には正しく動作しますが、命令の引数が3つ4つになると正しく動作しないので、使用時は注意して使ってください。

以上です。


  1. 正式名称は環境 Environment 

  2. この BNF では不十分で、実際の LaTeX にはコメント % や数式モード $ や命令のオプション [ ] などもある。