--- parent: HowItWorks title: パーサー date: 2019-11-23 tags: 構文解析, アルゴリズム --- OutlineTextのパーサ(Parser)について === # はじめに OutlineTextは,2018年4月から開発され,明確なパーサモデルがないまま, トライアル・アンド・エラーで構築,改良されていきました. 本稿で挙げられているパーサモデルはその過程で作られたものです. 特にモデルは理想や目標で語られることが多く^[注.model],実際のスクリプトがモデルに追い付いていない ことがあります(特にクラスの分離や,命名など).ご容赦ください. # 文法内の要素について OutlineTextの記法には,多くのほかの記法と同じように,大きく分けて二つの要素があります. 一つめは,インライン要素で,二つ目は,ブロック要素です. インライン要素: 行内にある記法.行をまたがない記法 ex) ```xml **強調**, //アクセント//, __アンダーライン__, [URL](url), など ``` ブロック要素: 複数行にわたる記法.行をまたぐ記法. ex) ```xml * リスト * リスト 段落 段落 段落 段落 上の段落を合わせて 一つのセクションというブロック要素 定義リスト: 定義用語 ``` ![要素の種類](CURRENT_DIR/Images/elements.jpg) # イベント駆動型モジュラーパーサ(Event-driven Modular Parser) OutlineTextのパーサは,一つのパーサがすべての文法を解釈するのではなく, 複数のパーサが組み合わさって動作します.パーサの種類は大きく分けて三種類あり, //メインパーサ//,//ブロック要素パーサ//,//インライン要素パーサ//があります. ブロック要素パーサとインライン要素パーサは, メインパーサが発火させる//イベントを受けて動作//します. ![Event-driven Modular Parser](CURRENT_DIR/Images/modular-parser.jpg) # パーサの種類 メインパーサ: 文書を読み込み,イベント(インデントが入った, 抜けた, 行の先頭にきたなど) を発火させる. ブロック要素パーサ: ブロック要素を担当するパーサ.メインパーサが発火させるイベントに反応して処理を実行する. この種類のパーサは,単体ではなく,ブロック要素ごとに一つのパーサが担当する. インライン要素パーサ: インライン要素を担当するパーサ. メインパーサとブロック要素から呼ばれる. インライン文法の変換テーブルを参照して,プレインテキストを変換する. 複数のインライン文法があっても,このパーサは一つ. # イベントの種類 メインパーサは,読み込まれた文章を上から読み,行の変化やインデントの変化 等があったときにイベントを発火します. ![Event の種類](CURRENT_DIR/Images/events.jpg) # 優先順位 イベント時,メインパーサが呼ぶブロック要素パーサには順番があります. そのイベントに対して優先度の高いパーサから順番に呼ばれていきます. 呼ばれたパーサは,処理を行いそのあとのパーサに処理を回すか選択できます. 処理を回すことを選択すると,メインパーサは,続けて次に優先度の高いパーサを呼び出します. 逆に処理を回さないことを選択すると,メインパーサは,それ以降のパーサを呼び出しません. 例えば,行頭のイベントでは,まず見出しに関するパーサが呼ばれたあと,段落に関するパーサが呼ばれます. 実際に,行頭が見出しの場合,見出しパーサがその行をデコードし,それより後のパーサには処理を回しません. ![優先順位](CURRENT_DIR/Images/priority.jpg) # 文脈(Context) 各パーサが独立しているため,あるパーサが持っている文書の情報をほかのパーサが参照できる 仕組みが必要です.そこで,文脈(Context)というデータが登場します. パーサは理解した文書の情報を文脈に書き込みます. ほかのパーサは,文脈を見て,しかるべき動作をします. ![文脈(Context)](CURRENT_DIR/Images/context.jpg) # 処理の流れ 1. メインパーサが文書読み込み 2. デコード単位(チャンク)に分ける 3. チャンクごとにデコード開始. 3.1. メインパーサがイベント発火 3.2. イベントを受けとったブロック要素パーサ,インライン要素パーサがデコードを行う. 4. 終了 # 文法の追加 各パーサが独立して動作する//イベント駆動型モジュラーパーサ//を採用しているため, 文法の追加は,難しくありません. # ブロック文法 1. ブロック要素パーサの基底クラスを継承してパーサの実装 ```php /** * 空行を線で区切る(誰得?) */ class SeparateEmptyParser extends ElementParser { public static function OnEmptyLine($context, &$output) { $output = '<hr>'; return false; } } ``` 2. メインパーサにイベントの登録 ```php private static $onEmptyLineParserList = [ 'HeadingElementParser', 'ParagraphElementParser', 'TableElementParser', 'ReferenceListParser', 'SeparateEmptyParser', // <- 追加 ]; ``` # インライン文法 1. インライン文法変換テーブルに追加 ```php 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], // <- 追加. 'です'を'ぽよ'に変換する ]; ``` # 注釈 [注.model]: 筆者独自解釈