読者です 読者をやめる 読者になる 読者になる

WikiForme 0.5 開発裏話 1

ブログに書いている時点で裏ではないんじゃないか。
昨日リリースしたWikiForme 0.5の開発と実装について。「WikiFormeのココが普通じゃない」という話。

構造化Wiki記法

これがWikiFormeの最重要ポイント。Wiki記法で書かれた文章をツリー構造に起こしてから、各種出力フォーマットに変換する。
普通のWiki記法は構造化しないので、通常は完全にフラットか、1段階くらいネストしたフォーマットにしか出力できない。これが「箇条書きの中に表を入れる」とか「番号付きリストの中に箇条書きを入れる」というような、複雑にネストした構造が書けない理由でもある。
しかしそれも当然で、そもそもWiki記法自体がネストした構造を書けないようになっている。Wiki記法は行頭にマークを付けることでテキストを整形するので、基本的に「1行=1要素」とした1対1の対応でしか整形できない。「!=見出し」「-=見出しレベル1」「--=見出しレベル2」といった具合。


ネストした構造を書くには、XMLのように閉じタグを書くか、YAMLのようにインデントを使わないといけない。しかしそのどちらも、人間が自然言語を書くには適していないと思う。いちいち閉じタグを書くのは面倒だし、きっちり間違わずにインデントし続けるのは少々キツイ。
そこでWikiFormeは、「節の中には章は入らない」「箇条書きレベル2の中には箇条書きレベル1は入らない」といった「包含関係」をすべての要素に定義することで、閉じタグもインデントも使わずに構造化された文章を書けるようなっている。
たとえば↓こんな擬似Wiki記法があったときに、

章  第1章
段落 本文1
章  第2章
段落 本文2

WikiFormeは以下のように構造化を行う。

  1. ルート要素として「記事」要素を設定
  2. 「章  第1章」がきた:「記事」は「章」を含められるので、記事の中に章を入れる
  3. 「段落 本文1」がきた:「章」は「段落」を含められるので、章の中に段落を入れる
  4. 「章  第2章」がきた:「段落」は「章」を含められない。1つ親へ進む
  5. 「章」は「章」含められない。もう1つ親へ進む
  6. 「記事」は「章」を含められるので、記事の中に章を入れる
  7. 「章」は「段落」を含められるので、章の中に段落を入れる


これによって、以下のような構文木が作られる。

  • 章 第1章
    • 段落 本文1
  • 章 第2章
    • 段落 本文2

「構造化することで何が嬉しいのか?」と思われるに違いない。WikiFormeを使う分には「表の中に箇条書きを入れられる」と言うように表現力が増すというくらいの利点しかないのだが(これはこれで嬉しいが)、実は各種フォーマットに書き出すときに非常に嬉しいのである。

たとえば「箇条書き」をHTMLに変換することを考えてみる。ご存じの通り、HTMLではol要素とli要素を使って箇条書きを書く。レベル1,レベル2とネストするには、li要素の中にol要素をネストさせる。

<ol>
  <li>VIVER
    <ol>
      <li>V-FIELD</li>
      <li>RUNES</li>
    </ol>
  </li>
  <li>VIVER CORE Server</li>
  <li>WikiForme</li>
</ol>

↑このHTMLを↓このような擬似Wiki記法で書きたいとする。

箇条書きレベル1 VIVER
箇条書きレベル2 V-FIELD
箇条書きレベル2 RUNES
箇条書きレベル1 VIVER CORE Server
箇条書きレベル1 WikiForme

レベル1を「-」、レベル2を「--」に置き換えれば、見覚えのあるWiki記法である。
この箇条書きは1対1の対応では表現することができない。こういうときに普通のWiki記法は困る。ネストした構造用には、他の要素とは違う特別な処理をしないといけない。
HTMLに変換するならまだ個別に対応できるが、SmartDocやDocBookに変換しようと思うと、「見出し」でさえ「章」や「節」と言った形でネストしないといけないので、かなり辛い。


これがWikiFormeであれば、「箇条書きレベル1には箇条書きレベル2を入れられる」という定義を書いておけば、以下のようなツリー構造を作ってくれる。

  • 箇条書きレベル1 VIVER
    • 箇条書きレベル2 V-FIELD
    • 箇条書きレベル2 RUNES
  • 箇条書きレベル1 VIVER CORE Server
  • 箇条書きレベル1 WikiForme


さらに、元のWiki記法を↓こう書くように変更した上で、

箇条書きレベル1の枠
箇条書きレベル1 VIVER
箇条書きレベル2の枠
箇条書きレベル2 V-FIELD
箇条書きレベル2 RUNES
箇条書きレベル1 VIVER CORE Server
箇条書きレベル1 WikiForme

包含関係を設定すれば、↓このようなツリー構造を作ってくれる。

  • 箇条書きレベル1の枠
    • 箇条書きレベル1 VIVER
    • 箇条書きレベル2の枠
      • 箇条書きレベル2 V-FIELD
      • 箇条書きレベル2 RUNES
    • 箇条書きレベル1 VIVER CORE Server
    • 箇条書きレベル1 WikiForme

これを先のHTMLに変換するには、「箇条書きレベル1の枠=ol」「箇条書きレベル1=li」「箇条書きレベル2の枠=ol」「箇条書きレベル3=li」というように、1対1で変換するだけでいい。構造化はWikiFormeがやってくれるので、変換が非常に楽になるというわけである。

しかし、「箇条書きレベル1の枠」というワケの分からないものを書かないといけなくなってしまうのは納得できない。そこで登場するのが「親要素補完」である。



親要素補完

親要素補完は、「箇条書きレベル1の親は箇条書きレベル1の枠である」という定義を書いておくと、箇条書きレベル1の枠を自動的に補完してくれる機能である。
親要素補完の定義を書いておくと、↓このWiki記法が渡されても、

箇条書きレベル1 VIVER
箇条書きレベル2 V-FIELD
箇条書きレベル2 RUNES
箇条書きレベル1 VIVER CORE Server
箇条書きレベル1 WikiForme

↓このように構造化される。箇条書きレベル1の枠や箇条書きレベル2の枠は、自動的に補完されたものである。

  • 箇条書きレベル1の枠
    • 箇条書きレベル1 VIVER
    • 箇条書きレベル2の枠
      • 箇条書きレベル2 V-FIELD
      • 箇条書きレベル2 RUNES
    • 箇条書きレベル1 VIVER CORE Server
    • 箇条書きレベル1 WikiForme


親要素が自動的に補完されるのでWiki記法を書く手間が減って、構造化されるので変換する手間も減る。スバラシイ。



親要素補完の過程を見てみると、以下のようになる。

  1. ルート要素として「章」要素を設定(これは別に設定しておく)
  2. 「箇条書きレベル1」がきた:「章」は「箇条書きレベル1」を含められない。1つ親へ進む
  3. 親が存在しない!「箇条書きレベル1」を保留し、「箇条書きレベル1の枠」を生成して補完する
  4. 「箇条書きレベル1の枠」がきた:「記事」は「箇条書きレベル1の枠」を含められるので、記事の中に箇条書きレベル1の枠を入れる
  5. 保留されていた「箇条書きレベル1」:「箇条書きレベル1の枠」は「箇条書きレベル1」を含められるので、箇条書きレベル1の枠の中に箇条書きレベル1を入れる
  6. 「箇条書きレベル1」がきた:「箇条書きレベル1」は「箇条書きレベル1」を含められないので、1つ親へ進む
  7. 「箇条書きレベル1の枠」は「箇条書きレベル1」を含められるので、箇条書きレベル1の枠の中に箇条書きレベル1を入れる
  8. 「箇条書きレベル2」がきた:「箇条書きレベル1」は「箇条書きレベル2」を含められないので、1つ親へ進む
  9. 「箇条書きレベル1の枠」は「箇条書きレベル2」を含められないので、もう1つ親へ進む
  10. 「章」は「箇条書きレベル2」を含められないので、もう一つ親へ進む
  11. 親が存在しない!「箇条書きレベル2」を保留し、「箇条書きレベル2の枠」を生成して補完する
  12. …以下略

かなり複雑な処理になるが、もう実装されているので心配する必要はない。包含関係と補完して欲しい親要素を定義しておくだけで良いのである。




ちなみにWikiFormeの「表」は、親要素補完を存分に活用して実装されている。デフォルトでは表は↓このようにして書くことができる。

|日本語名|英語名|
|ほげ|hoge|
|ふが|fuga|

これがWikiFormeによって構造化・親要素補完されると、↓このようになる。

  • table
    • tbody
      • tr
        • td 日本語名
        • td 英語名
      • tr
        • td ほげ
        • td hoge
      • tr
        • td ふが
        • td fuga

包含関係と親要素補完の設定だけで、HTMLの表と同じ構造がWikiFormeによって作成されてしまうのである。あとは要素を1対1で変換されるように定義してやるだけでHTMLに変換できる。
さらに次に紹介する「継承」を使えば、tbody要素を継承してthead要素を作成できる。明示的にthead要素を指定してやれば、明示的にヘッダを指定した表をWiki記法で書くことができる。


継承とグループ

WikiFormeでは包含関係をすべての要素に設定する必要があるが、新しい要素を作ろうと思ったときに困ったことになる。
要素Aは要素Bを含められる、と定義されているときに、要素Bに似ているけど少し違う新しく要素Cを作りたくなったとする。しかし要素Cも要素Aに含められるようにするには、要素Aの設定を変更しないといけない。これでは少しだけ違う要素を気軽に作れない。


そこで、要素Cを要素Bを「継承」して作成する。これによって要素Bの属性が要素Cに引き継がれ、要素Aは要素Cも含められるようになる。これで要素Aの設定を変更しなくても良くなった。ハッピーである。


それから、いくら継承があるからと言っても、「章」は「節」と「箇条書きレベル1の枠」と「段落」と「表」と…を含めることができる、などとすべての要素を設定するのは手間がかかりすぎる。そこで要素をグループにしてまとめられるようにする。
こうすれば、「段落」や「表」を「@contents」グループに属させておいて、「章」には「節と@contentsグループを含むことができる」と設定しておけば良い。まったく新しく要素を作りたいときでも、とりあえず「@contents」グループに属させておけば、自動的に「章」の中に含められるのである。逆に@contentsグループに属している要素をすべて含められる要素を作るのも簡単になる。ハッピーである。


結果だけ見ても単純にハッピーなだけであまり面白くない。面白さはその実装にある。


要素=クラス

他のWiki記法パーサーでは、1つの要素(あるいはプラグイン)=1つの関数で定義されていることが多い。しかしWikiFormeは1つの要素を1つのクラスで定義する。
そして先ほど紹介した「継承」は、まさにオブジェクト指向言語の「継承」で実装されているのである。「グループ」はMix-inで実装されている。ある要素が他の要素を包含可能かどうかは、包含可能な要素を列挙したリストの中に、is_a?の関係にある要素が入っているかどうかを調べることで判別できる。


「包含関係」「親要素補完」「継承」まで構造化の仕組みが分かってくると、フラットに書かれたWiki記法も構造化されて見えてくる。Wiki記法はメモ用にフラットな文章を書くためだけの物ではなく、キッチリと文章構造を記述できるものだったのである。





長くなってきたので、続きはまた今度。
また今度のネタ:

  • リフレクションによる記法定義。要素=クラスについてもう少し詳しく。action要素についても
  • ネストしたインライン要素のパース
  • 記法バンドルのロードとキャッシュ
  • Wiki文法のカスタマイズ、理想を諦めたこと
  • WikiForme Web UIの設計について
  • XML自動インデントの実装