大筋は reiny - Reactに最適化したテンプレートエンジンを作り始めた - Qiita
内部的処理のなメモ
JSXへの問題意識
- nodeの変数代入で、コードによる出現順と実際に埋め込まれる位置が異なるのが可読性を落としている
- listをmapしてelementに変換する処理が直感的ではない
react-jadeの拡張を検討
最初はreact-jadeを拡張するのを検討した。調べた感じ、react-jadeは元のjadeをhackyに拡張している。作者が同じだからこそできる感じ。外部からPR出すのも面倒だったので、全く異なる一つの処理系を作った
グランドデザイン
- HTMLツリーをインデントブロックによる閉じタグ省略で生産的に書けるようにする
- インラインスタイルを書きやすくする
- インラインコードを書きやすくする
- reactで表現できることを制限しない
最初はslimのように()なしで foo=bar とできるようにしようとも試みたが、 foo(a=3){fontSize=13}
みたいに書けると嬉しい気がして、その時に複雑になりそうだったので()を使うデザインは継承した
ライブラリ選定
パーサジェネレータ
- pegjs
- pegcoffee
将来的には parser api による javascript-ast から生成すべきだと思っているが、現在ではそれによって得られる抽象度はオーバースペックなので現時点においてはコードを手作業で生成することにする
nodeとインラインコードを交互に書けるようにするため、そこを抽象化したランタイムを自作した mizchi/coppe。 昔作ったmizchi/vk をシンプルにしたもの。これはbrowserifyを経由してruntimeとして使うことを意図するので、できるだけ軽量にする
文法
最初にインデントブロックを設計する javascript - Parse indentation level with PEG.js - Stack Overflow
ASTの設計
大雑把に
- program =
line*
- line = statement
- statement = ifStatement / forStatement / element / text / inlineCode / comment
- expression = primitive / inlineExpression
まあ普通の言語設計と同じ
AST実装をさぼる
if expr span
みたいなコードを実装するには、完全なJavaScriptのexpressionを表現できる必要があるのだが、それを実装しきるのはかなり面倒。なので、最小限実装した expression ノードの他に、次のようなjs埋め込み式を用意した。jsをそのまま通す。なにもしない。ここだけはbabelを通さず生JSを返すのを要求するが、expressionとしてパースする方法が思いつかないのでこうした。acornとか使えば出来るかもしれない。react-jadeは中でacornを呼んでいた。
if { a > 1 } span
または
if {- deepEqual({a:1}, {a:1}) -} span
とする。 {- -} を用意したのは、 {.*}
では 中でオブジェクトリテラルが使うのが難しいため。
インラインHTMLはサポートしない。そもそもReactで最適化できないし、どうせdangerouslySetInnerHTML を使うことになる。
処理ステップ
- パーサジェネレータで文法定義からパーサを生成
- プリプロセス: インデントブロックの為に、与えられた文字列から空白行を除去
- パース: パーサに文字列を食わせてASTを生成
- コンパイル: ASTをトラバースしてReactElementを生成するコードを生成
実行ステップ
- foo.reiny をコンパイルし foo.js を生成
- foo.js をrequireして得られた関数を実行すると ReactElementが得られる
- Reactで↑で得られたElementをよしなに使う