パーサー
OutlineTextのパーサ(Parser)について
はじめに
OutlineTextは,2018年4月から開発され,明確なパーサモデルがないまま,トライアル・アンド・エラーで構築,改良されていきました.本稿で挙げられているパーサモデルはその過程で作られたものです.特にモデルは理想や目標で語られることが多く[注 1],実際のスクリプトがモデルに追い付いていないことがあります(特にクラスの分離や,命名など).ご容赦ください.
文法内の要素について
OutlineTextの記法には,多くのほかの記法と同じように,大きく分けて二つの要素があります.一つめは,インライン要素で,二つ目は,ブロック要素です.
- インライン要素
行内にある記法.行をまたがない記法
ex)
**強調**, //アクセント//, __アンダーライン__, [URL](url), など
- ブロック要素
複数行にわたる記法.行をまたぐ記法.
ex)
* リスト * リスト 段落 段落 段落 段落 上の段落を合わせて 一つのセクションというブロック要素 定義リスト: 定義用語

イベント駆動型モジュラーパーサ(Event-driven Modular Parser)
OutlineTextのパーサは,一つのパーサがすべての文法を解釈するのではなく,複数のパーサが組み合わさって動作します.パーサの種類は大きく分けて三種類あり,メインパーサ,ブロック要素パーサ,インライン要素パーサがあります.ブロック要素パーサとインライン要素パーサは,メインパーサが発火させるイベントを受けて動作します.

パーサの種類
- メインパーサ
文書を読み込み,イベント(インデントが入った, 抜けた, 行の先頭にきたなど) を発火させる.
- ブロック要素パーサ
ブロック要素を担当するパーサ.メインパーサが発火させるイベントに反応して処理を実行する.この種類のパーサは,単体ではなく,ブロック要素ごとに一つのパーサが担当する.
- インライン要素パーサ
インライン要素を担当するパーサ.メインパーサとブロック要素から呼ばれる.インライン文法の変換テーブルを参照して,プレインテキストを変換する.複数のインライン文法があっても,このパーサは一つ.
イベントの種類
優先順位
イベント時,メインパーサが呼ぶブロック要素パーサには順番があります.そのイベントに対して優先度の高いパーサから順番に呼ばれていきます.呼ばれたパーサは,処理を行いそのあとのパーサに処理を回すか選択できます.処理を回すことを選択すると,メインパーサは,続けて次に優先度の高いパーサを呼び出します.逆に処理を回さないことを選択すると,メインパーサは,それ以降のパーサを呼び出しません.
例えば,行頭のイベントでは,まず見出しに関するパーサが呼ばれたあと,段落に関するパーサが呼ばれます.実際に,行頭が見出しの場合,見出しパーサがその行をデコードし,それより後のパーサには処理を回しません.

文脈(Context)
処理の流れ
- メインパーサが文書読み込み
- デコード単位(チャンク)に分ける
- チャンクごとにデコード開始.
- メインパーサがイベント発火
- イベントを受けとったブロック要素パーサ,インライン要素パーサがデコードを行う.
- 終了
文法の追加
各パーサが独立して動作するイベント駆動型モジュラーパーサを採用しているため,文法の追加は,難しくありません.
ブロック文法
- ブロック要素パーサの基底クラスを継承してパーサの実装
/** * 空行を線で区切る(誰得?) */ class SeparateEmptyParser extends ElementParser { public static function OnEmptyLine($context, &$output) { $output = '<hr>'; return false; } }
- メインパーサにイベントの登録
private static $onEmptyLineParserList = [ 'HeadingElementParser', 'ParagraphElementParser', 'TableElementParser', 'ReferenceListParser', 'SeparateEmptyParser', // <- 追加 ];
インライン文法
- インライン文法変換テーブルに追加
private static $spanElementPatternTable = [ ["/\[\[ *(.*?) *\]\]/", '<a name="{0}"></a>', null], ["/\[(.*?)\]\((.*?)\)/", null, 'DecodeLinkElementCallback'], ["/\*\*(.*?)\*\*/", '<strong>{0}</strong>', null], ["/\/\/(.*?)\/\//", '<em>{0}</em>', null], ["/__(.*?)__/", '<mark>{0}</mark>', null], ["/~~(.*?)~~/", '<del>{0}</del>', null], ["/\^\[(.*?)\]/", null, 'DecodeReferenceElementCallback'], ["/<((http|https):\/\/[0-9a-z\-\._~%\:\/\?\#\[\]@\!\$&'\(\)\*\+,;\=]+)>/i", '<a href="{0}">{0}</a>', null], ["/<(([a-zA-Z0-9])+([a-zA-Z0-9\?\*\[|\]%'=~^\{\}\/\+!#&\$\._-])*@([a-zA-Z0-9_-])+\.([a-zA-Z0-9\._-]+)+)>/", '<a href="mailto:{0}">{0}</a>', null], ["/->/", '→', null], ["/<-/", '←', null], ["/=>/", '⇒', null], ["/<=/", '⇐', null], ["/\.\.\./", '…', null], ["/--/", '—', null], ["/\(TM\)/", '™', null], ["/\(R\)/", '®', null], ["/\(C\)/", '©', null], ["/'/", '’', null], ["/です/", 'ぽよ', null], // <- 追加. 'です'を'ぽよ'に変換する ];
注釈
- ^ 筆者独自解釈