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

WikiForme 0.5 開発裏話 3

第2回に続いて第3回。

文法カスタマイズと理想の限界

前回書いた、

この文法カスタマイズ機能はWikiFormeの開発当初からの目標だったのだが、これを実装するまでにはいろいろと紆余曲折があった。今でもこの設計で良かったのかと悩ましい。

の続きから。 


文法カスタマイズと言うと、カスタマイズしたくなる箇所がいろいろある。ブロック要素の行頭マークと、インライン要素の開始マークと終了マークは当然だが、表組みを書くときの区切り文字、リンク書き方などである。

|表組みを|このように|書くか
,表組みを,このように,書くか

[リンクは?>http://example.org]
[http://example.org/:title=リンクは?]

WikiForme 0.3では、このような細かい文法までもカスタマイズ可能にしようとしていた。その上、ある文法から他の文法へ、文章を変換できるようにしようとしていた。

つまり、表を「|」区切りで書いた文章から、「,」区切りで書いた文章に変換できるようにしようとしていた。

文法を変換できるようにすることで、過去に書いた文章のことを気にせずに気軽に文法をカスタマイズすることができる。


しかし現実には、これを実現するのは非常に困難である。理由は以下の2点。

  • 記法プラグインに、通常のフォーマット変換メソッド(process_htmlなど)に加えて、文法を変換するメソッドを実装する必要がある
  • フォーマット変換メソッドを文法に依存しない形で書く必要がある

実際にコードを書いてみると分かりやすい。

# 文法のカスタマイズができない実装(WikiForme 0.5)
f.module_eval {
  def process_html
    @text.split('|').each {|cell|
      # ...
    }
  end
}

# 文法のカスタマイズができる実装(WikiForme 0.3)
f.module_eval {
  def process_html
    split_char = @syntax['split_char'] || '|'
    @text.split(split_char).each {|cell|
      # ...
    }
  end
  def self.syntax_convert(syntax1, syntax2)
    split_char1 = syntax1['split_char'] || '|'
    split_char2 = syntax2['split_char'] || '|'
    @text.split(split_char1).join(split_char2)
  end
}

文法のカスタマイズができるようにすると、ただでさえ頭を使わないと書けない記法プラグインの実装がさらに難しくなる。構造化Wiki記法の表現力は高いと言っても、記法プラグインが無ければ意味がない。

そこで、そもそも文法のカスタマイズはほとんどできなくても良いのではないかと考えた。できないモノはできないと言われた方がずっとお気楽である。


というわけで、WikiForme 0.5では細かい文法のカスタマイズはできない。できるようにWikiFormeを改造するのも、私の経験からするとやめた方がいい。WikiForme本体の実装をこれ以上複雑にするのはムリがあるような…


もう一つの理想、記法バンドルのキャッシュ

文法の詳細なカスタマイズは諦めたが、記法バンドル(article.4me)のキャッシュはまだ諦めていない。しかし、諦めるべきかもしれないとも思っている。


現在のWikiFormeは一度記法バンドルを読み込むと、その記法バンドルを使って複数のWiki文章を変換することができる。記法バンドルをキャッシュすることで、一度にたくさんのWiki文章を変換したいときや、WikiFormeをFastCGIで動かすときに処理が高速になる。

ではグローバル変数を使うとどうなるか。グローバル変数を使いたくなるのは、章番号を自動的に振ったり、図への参照を自動的にタイトルに置き換えたいときなどである。


たとえば、グローバル変数を使って自動的に章番号を振る記法プラグインを考えてみる。

$chapter = 0    # $chapterの初期値
f = Format.block :chapter
f.process_html {|text, children|
  $chapter += 1  # 章ごとに1ずつ増やす
  XML[$chapter + '. ' + text.process] << children
}

このように普通にグローバル変数を使うと、2つ目のWiki文章を変換しようとしたときに章番号が間違った番号から振られてしまう。


そこで現在は、$varsという特別なグローバル変数を用意している。$varsは文章を変換する前に初期値にリセットされるようになっている。$chapterの代わりに$vars[:chapter]を使うだけで、通常通りグローバル変数を使うことができる。

使う側としてはそれでハッピーなのだが、実は$varsの実装はトリッキー極まりない。$varsが無くなるだけで、WikiFormeの実装はかなりクリーンになるのだ。それに実は使う側にも落とし穴があって、うっかりクロージャで$varsの中身を閉じ込んでしまうとおかしなことになる。



…と書いている内に、この問題は解決した。グローバル変数を使うときは、かならず Format.initialize{}ブロックの中で初期化してから使うようにする。(WikiForme 0.6で実装中)

Format.initialize {
  $chapter = 0    # $chapterの初期値
}
f = Format.block :chapter
f.process_html {|text, children|
  $chapter += 1  # 章ごとに1ずつ増やす
  XML[$chapter + '. ' + text.process] << children
}

Format.initialize{}ブロックは、新しい文章を処理するたびに呼ばれる。これで1つの記法バンドルで複数の文章を処理しても問題なくなった。

グローバル変数を宣言する意味にもなって、なかなか良いと思う。




ネストされたインライン要素

構造化や親要素補完、action要素と言った話は、すべてブロック要素に関するものだったが、ここではインライン要素について。


ブロック要素に比べて考えることが少ないかと思いきや、実はインライン要素がネストしたときになかなか厄介だったりする。

たとえば、「''」から「''」までの間を太字にする(仮に<b>テキスト</b>にする)bold要素と、「'''」から「'''」までを斜体にするitalic要素(仮に<i>テキスト</i>にする)があったとする。このときに、「'''''テキスト'''''」と書くとどうなるか。


何も考えずに実装してしまうと、「<b>'''テキスト'''[]」となってしまう。悲しい。


インライン要素を処理するときは、以下の2つを考えて実装する必要がある。

開始マークが長いものからマッチする
終了マークが連続するときは、連続する分だけ後ろにずらしてマッチする
よく分からないので正規表現で書くと、bold要素とitalic要素の正規表現はそれぞれ以下のようになる。

  • ''(.+?'*)''
  • '''(.+?'*)'''

開始マークが長いものからマッチするので、italic要素(開始マークは3文字)から試し、マッチしなかったらbold要素(開始マークは2文字)を試す。

まずitalic要素を試すと、「<i>''テキスト''</i>」このようになる。ネストしたテキストをさらに展開すると、「<i><b>テキスト</b></i>」となる。めでたしめでたし。



processとexpand

WikiFormeは1つのWiki記法文章を、HTMLやSmartDocなどの複数のフォーマットに変換する。SmartDocではできるがHTMLではできない表現や、HTMLではできるがSmartDocではできない表現があるので、そこのところをうまく処理してやらないといけない。

たとえば、HTMLだと章のタイトルは<h2>要素で指定するが、SmartDocだと<chapter>要素のtitle=属性で指定する。属性で指定すると言うことは、タグが使えない。つまり、HTMLでは章のタイトルにインライン要素を使えるが、SmartDocだと使えないことになる。


これを踏まえて、以下のWiki記法はどのように変換すれば良いか。

*ここだけ''太字''のタイトル

HTMLでは↓これで良いが、

<h2>ここだけ<strong>太字</strong>のタイトル</h2>

SmartDocでは↓こうが良いだろうか。

<chapter title="ここだけ太字のタイトル" />

↓これはダメだと思う。

<chapter title="ここだけ''太字''のタイトル" />

タグが使えないからと言って、インライン要素を展開しないのはダメなのである。展開はするが、タグを付けてはいけない。


そこで、インライン要素の処理方法にはprocess、expand、escapeの3つがある。processは、インライン要素を展開して、スタイルも付ける。expandはインライン要素を展開するが、スタイルを付けずにプレインテキストを返す。escapeはインライン要素を展開しない。



WikiForme Web インターフェイス

やっぱりWiki記法と言えばWebで動いて欲しい。そこでWikiForme Web版を使うと、Wikiっぽく使うことができる。

しかしWikiForme Web版は普通のWikiクローンではない。エンジニア好み…と言うか私好みの構造になっている。


一番のポイントは、ページがそのままファイルシステムに保存されていると言う点にある。

たとえばPukiWikiだと、サーバー上のファイルは以下のようなディレクトリ構造になる。

...
wiki/
wiki/3A52656E616D654C6F67.txt
wiki/3A636F6E666967.txt
wiki/3A636F6E6669672F5061676552656164696E67.txt
...

wiki/ディレクトリ以下にページ名をハッシュ関数を掛けたテキストファイルが並んでいる。

もちろんこれはこれで良いのだが、私はあまり好きではない。lsしてもページ名が分からなくて、「ページがあるぞー」という実感が湧かない。ディレクトリ構造が見えないのも不安になってしまう…普通はそんなことは考えないのだろうが…


一方でWikiForme Webは、以下のようなディレクトリ構造になる。

index.cgi
edit.cgi
index.txt
index.html
hogedir/
hogedir/index.txt
hogedir/index.html
hogedir/hogepage.txt
hogedir/hogepage.html

lsで見たまま、ページがそのまま.txtファイルとして並んでいる。ディレクトリを作れば、そのままURLでもディレクトリになる。とても分かりやすい。findやらgrepやらもいつものように使える。画像やCSSなどもそのままディレクトリに置けばいい。


index.cgiは、WikiFormeを使って.txtを.htmlに変換するCGiである。mod_rewriteなどを使って.htmlへのアクセスをindex.cgiへ流してやれば、アクセスがあったときに.txtが.htmlに変換されるという仕組みである。とても分かりやすい。

一度変換した.htmlは、.txtの方が更新されるまでキャッシュしておく。If-Modified-Sinceにも対応している。


edit.cgiは、.txtを編集するためのCGIである。ブラウザからテキストを受け取って、.txtファイルに書き込むだけ。とても分かりやすい。


さらに言えば、edit.cgiにはパスワード管理機能があるが、これは.htdigestファイルを書いているだけである。認証はApacheがやる。とても分かりやすい。


こういう単純な仕組みになっているので、ウェブサイトを自分好みにカスタマイズするのも簡単である。

edit.cgiは.txtを編集しているだけだから、サーバー上でvimを使って直接.txtファイルを書き換えても何ら問題ない。SVNで同期するのも良い。





そろそろ続きはまた今度。そろそろ終わるかと思ったら、また新しい機能ができてしまったので困る。

また今度のネタ:

  • XML自動インデントの実装
  • WikiForme Doc