私はパーサの書き方に本当に詳しくないのですが、興味深い題材でしたので少し試してみました。
処理の流れは次のようになります:
- LaTeX の数式をパースする。木構造を得る
- パースしてできた木構造を整形して、より扱いやすい木構造に変換する。文法エラーがあればここで失敗する
- 木構造を畳み込んで最終的な文字列を得る
以下 Rust で書くことを考えます。他の言語であっても、おおよその流れは共通しているはずです。
パーサを自作するとき、恐らくパーサジェネレータとパーサコンビネータという2つが二大勢力なのではないでしょうか。後者では nom
というクレートが大変人気のようです。前者で私が最近触っているのが pest
なので以下こちらを使います。PEG で書けるので、可搬性が高そうだと思いました。
一般の LaTeX、それも TeX on LaTeX を含めた世界を pest
でカバーするのは困難極まりないと思います(\def
やカテゴリコードの変更があると、たまったものではありません)。(お行儀の良い)数式に範囲を絞るとしても、基礎的な文法でさえ完璧に網羅するのは簡単ではないと思います。まして、amsmath
を始めとして数学のパッケージは沢山提供されています。そこで、ここでは 2^{32}
を変換すると Math.pow(2, 32)
を得るというテストを通すことだけを考えます。
rust
1#[test]
2fn test_convert() {
3 assert_eq!(convert("2^{32}"), "Math.pow(2, 32)");
4}
さて、以下は実際のコード例です。
ファイル構造は次の通り:
$ exa -T -I target
.
├── Cargo.lock
├── Cargo.toml
└── src
├── latex_math.pest
└── lib.rs
Cargo.toml
:
toml
1[package]
2name = "latex-math-to-javascript"
3version = "0.1.0"
4authors = ["gemmaro <***>"]
5edition = "2018"
6
7[dependencies]
8pest = "2.1.3"
9pest_derive = "2.1.0"
パース
pest
で今回のテストを通すためだけの非常に簡素な文法を書きます。
(書きながら気付きましたが、この部分については MathJax 関連でよい資料がありそうですね。)
src/latex_math.pest
:
pest
1input =
2 { SOI ~ stem ~EOI }
3
4stem =
5 { exponential }
6
7exponential =
8 { value ~ "^" ~ value }
9
10value =
11 { number
12 | "{" ~ number ~ "}"
13 }
14
15number =
16 { ASCII_DIGIT+ }
整形
実は今回整形していません。
複雑な文法になってくると、ある程度解釈を変えたほうがいいかもしれません。
畳み込み
ここで src/lib.rs
を掲げます。
src/lib.rs
:
rust
1#[macro_use]
2extern crate pest_derive;
3
4use pest::{iterators::Pair, Parser};
5
6#[derive(Parser)]
7#[grammar = "latex_math.pest"]
8struct LaTeXMathParser;
9
10fn convert(latex: &str) -> String {
11 let pairs = LaTeXMathParser::parse(Rule::input, latex)
12 .unwrap()
13 .next()
14 .unwrap();
15
16 for pair in pairs.into_inner() {
17 match pair.as_rule() {
18 Rule::stem => {
19 return from_stem(pair);
20 }
21 _ => unimplemented!(),
22 }
23 }
24
25 unimplemented!();
26}
27
28#[test]
29fn test_convert() {
30 assert_eq!(convert("2^{32}"), "Math.pow(2, 32)");
31}
32
33fn from_stem(stem: Pair<Rule>) -> String {
34 for pair in stem.into_inner() {
35 match pair.as_rule() {
36 Rule::exponential => {
37 return from_exponential(pair);
38 }
39 _ => unimplemented!(),
40 }
41 }
42 unimplemented!()
43}
44
45fn from_exponential(exp: Pair<Rule>) -> String {
46 let mut result: Vec<String> = Vec::new();
47 for pair in exp.into_inner() {
48 match pair.as_rule() {
49 Rule::value => {
50 result.push(from_value(pair));
51 }
52 _ => unimplemented!(),
53 }
54 }
55 format!("Math.pow({}, {})", result[0], result[1])
56}
57
58fn from_value(val: Pair<Rule>) -> String {
59 for pair in val.into_inner() {
60 match pair.as_rule() {
61 Rule::number => {
62 return from_number(pair);
63 }
64 _ => unimplemented!(),
65 }
66 }
67 unimplemented!()
68}
69
70fn from_number(num: Pair<Rule>) -> String {
71 num.as_str().into()
72}
木構造を巡回し、ルールに一致したものについて再帰的に処理しています。
from_*
という名前の函数は失敗する可能性を考えて Result
を返すべきなのですが、簡単のため省略しています。
以上について $ cargo test
するとテストは通ります。