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

WikiForme 0.5 開発裏話 2

大変ご好評をいただいた(?)第1回に続きまして、第2回。
こんなモノを書いる暇があったら記法を充実させろと言う話もあるわけですが、ちょっと書いてみたい気分なのです。WikiFormeデモサイトにこんな記法が欲しいというページを作ってみたので、こんな記法があったら嬉しいなーという案があればサクサク書いてみてください。 WikiForme Wikiができるかもしれません。

記法定義とリフレクション

さて、前回は「要素はクラスである」というところの途中で終わってしまった。
まずはWikiFormeの要素はどのように定義するのか、実際のコードを見てみる。要素はRubyを使って定義する。

# chapter要素を作る
f = Format.block :chapter

# 文法は *
f.default_syntax '*'

# @contentsグループ、@blankグループに属している要素と、section要素を包含可能
f.contain :@contents, :section, :@blank

f.process_html {|text, children|
  # <h2>ここにテキスト</h2>ここに子要素
  XML[:h2, text.process] + children
}

f.process_smartdoc {|text, children|
  # <chapter title="ここにテキスト">ここに子要素</chapter>
  XML[:chapter, {:title => text.expand}, children]
}

1行目のFormat.blockメソッドは、Classクラスのインスタンスを作って返す。つまり新しいクラスを作っているのである。
新しいクラスを作るとき、WikiForme::BlockElementクラス(すべてのブロック要素の基底クラス)を継承しており*1、 default_syntaxメソッドやcontainメソッドは、BlockElementクラスに定義された特異メソッドである。要するにコードで書けば以下のようになる。

# すべてのブロック要素の基底クラス
class WikiForme::BlockElement
  def self.default_syntax(syntax)
    # ...
  end
  def self.contain(*names)
    # ...
  end
  # ...
end

# 新しい要素を作る(※実際にはこれでは足りない)
class ChapterClass < WikiForme::BlockElement
end

ChapterClass.default_syntax '*'
ChapterClass.contain :@contents, :section, :@blank

# process_htmlメソッドは?

process_htmlメソッドはどうなっているのか。これはもう1段階だけ複雑になる。WikiForme::BlockElementクラスには、process_htmlという特異メソッドは定義されていない。代わりにmethod_missingというメソッドが定義されている。
method_missingはRubyの非常に便利な機能の一つで、定義されていないメソッドが呼ばれたときに呼ばれるメソッドである(PerlPythonにも同じような機能があるらしい)。method_missingは name と 可変長の args を引数に取るメソッドで、name には呼ばれた(そして定義されていなかった)メソッドの名前、argsには引数が入る。


実際に定義されているmethod_missingメソッドを見てみると、↓このようになっている。

class WikiForme::BlockElement
  # ...
  # process_*の定義
  def self.method_missing(name, *args)
    if block_given? && name.to_s =~ /process_([^=]+)/
      # process_*の定義
      define_method(name) {
        yield @text, @children, @lines
      }
      return
    end
    raise NameError, "undefined method `#{name}' for #{self.inspect}:#{self.class}"
  end
  # ...
end

define_methodという、これもまた余り使われていないであろうメソッドが登場しているが、これはインスタンスメソッドを定義するメソッドである。つまり、定義されていないメソッドが呼ばれて、呼ばれたメソッドの名前が /process_([^=]+)/ という正規表現にマッチしたら、呼ばれたメソッドの名前と同じ名前のインスタンスメソッドを定義している。


以上をまとめると、一番最初に挙げた要素の定義は、以下のように書くことができる。(これはちゃんと動く)

# クラスを作成
f = Format.block :chapter

# 特異メソッドの呼び出し
f.default_syntax '*'
f.contain :@contents, :section, :@blank

# インスタンスメソッドを定義
f.module_eval {
  # process_htmlという名前のインスタンスメソッドを定義
  def process_html
    XML[:h2, @text.process] + @children
  end

  # process_smartdocという名前のインスタンスメソッドを定義
  def process_smartdoc
    XML[:chapter, {:title => @text.expand}, @children]
  end
}


Rubyのリフレクション機能に慣れていないと少し取っつきにくいコードかもしれないが、要するに要素はクラスなのである。それが分かっていただけるだけでもとても嬉しい。


action要素

要素はクラスであるから、当然いくらでもインスタンスメソッドを定義できる。↓このように process_* 以外のメソッドを定義した要素を作ることもできる。

# クラスを作成
f = Format.block :image

# 文法は @image
f.default_syntax '@image'

f.group :@contents    # ←@contentsグループに属している要素

# インスタンスメソッドを定義
f.module_eval {
  def process_html
    # ...
  end

  # タイトルをセット
  def title=(title)
    @title = title
  end

  # タイトルをゲット
  def title
    return @title
  end
  
  # ちなみに attr_accessor :title と同じ
}


先ほどの要素で「@text」(行頭マークに続いて書かれた文字列)や「@children」(子要素)というインスタンス変数を使っていたが、実は他にもあらかじめ定義されている変数があり、その中に「@parent」というのがある。これは親要素を指しており、これはズバリ先ほど作ったクラスのインスタンスである。

というわけで、↓こういう要素を作ってみる。

# title要素を作る
f = Format.block :title

# 文法は ^title
f.default_syntax = '^title'

f.module_eval {
  def process_html
    # 親要素のtitle=メソッドを呼ぶ
    @parent.title = @text
  end
}

この^title要素を使うと、

@image http://example.com/hoge.png
^title ほげの図

このようなWiki記法が書ける! ^titleは親要素にtitle=メソッドが定義されてさえいれば、どこにでも書けるのである。とても記法を覚えやすい上に、表にも箇条書きにもどこにでもタイトルを付けて表示できる、素晴らしい表現力!


…と言いたいところだが、少し問題がある。構造化に関する問題である。^title要素は文章構造の中でどこに位置すれば良いのか?
^title要素自体は、親要素にタイトルを付加するというメタな要素であって、「それ自体は文章の一部ではない」と考えた。つまり、文章構造の中に^title要素が存在できる場所はない。


そこで用意したのが、構文木の中には入らずに、親要素(や親の親、親の親の親…)に影響を与える「action要素」である。
action要素は文章ではないから、htmlやsmartdocといった区別はない。構文木の中に入らないため、子要素もいない。親要素もおらず、代わりに現在の構文スタックを指している「context」が与えられる。定義は以下のようになる。

f = Format.block :title

# 文法は ^title
f.default_syntax = '^title'

f.action {|text, context|
  # 現在の構文スタックのトップに位置する要素にタイトルをセット
  context.stack.last.title = text
}

# action {|text, context| ... } は以下と同等
f.module_eval {
  def action
    @context.stack.last.title = @text
  end
}

action要素 2、アセンブルとプロセス

以上はaction要素の思想面から見た存在理由である。だが実際のところ、action要素は実装上あったほうが自然だから存在する、という理由の方が大きい。構文木に入らないで親要素に影響を与える要素というのは、その利用例の1つに過ぎない。

WikiFormeがWiki記法の文章を各種フォーマットに変換する過程は、3つの段階に分けられる。

  1. 字句解析(行頭マークを識別する)
  2. アセンブル(構造化、親要素補完)
  3. プロセス(各種フォーマットへ変換)

通常のブロック要素やインライン要素は、プロセスの段階で働く要素である。一方action要素は、アセンブルの段階で動作する。action要素は構造化をプログラマブルに操作できる要素なのである。action要素にhtmlやsmartdocといった区別がないのは、変換フォーマットの種別はプロセスの段階に影響するオプションだからである。


構文木を操作するaction要素として、表がある。表の実装はWikiFormeの特性を最大限に生かしていて面白い。

前回、表は親要素補完を使って↓このように書けると紹介したが、

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

親要素を補完を使わないと、↓このように書くことになる。

|||?        /* table   */
||:         /*  tbody  */
|=          /*   tr    */
|[]日本語名 /*    td   */
|[]英語名   /*    td   */
|=          /*   tr    */
|[]ほげ     /*    td   */
|[]hoge     /*    td   */
|=          /*   tr    */
|[]ふが     /*    td   */
|[]fuga     /*    td   */

注目すべきは、省略記法の方で使っている「|」要素がどこにも存在しないことである。そう、「|」要素はaction要素である。


「|」要素の定義を見てみると、以下のようになっている。

# 名前はtable_split要素
f = Format.block :table_split

# 文法は |
f.default_syntax '|'

# action要素
f.action {|text, context|
  tr_element = context.format.block[:tr]
  td_element = context.format.block[:td]

  # tr要素を生成して構文スタックに追加
  context.push(tr_element, '')

  # テキストを | で区切って…
  text.split('|').each {|cell|
    # td要素を生成して構文スタックに追加
    context.push(td_element, cell)
  }
}

table_split要素は、構文スタックにtr要素(つまり行)を1つ追加して、その後テキストを「|」で区切ってtd要素を追加しているだけである。構文スタックに要素を追加するときに親要素補完も行われるので、tbody要素やtable要素はtr要素を追加したときに自動的に補完される。スマート!


title要素やtable_split要素の他に、定義リスト(:タイトル:内容)を書くdl_split要素、他のファイルをインクルードする@include要素もaction要素である。


文法カスタマイズ

ところで、文法(「|」や「*」)を定義するところで、

f.default_syntax '|'

と、「default_」と付いていることに気が付いていただけただろうか。この文法はデフォルト値であり、自由にカスタマイズできる。


WikiForme Web版であれば、wikiforme/format.4me/syntax.yamlというファイルが入っている。Windows GUI版でもコマンドライン版でも、article.4me(フォーマットバンドル)の中に「syntax.yaml」という名前のファイルを作れば文法をカスタマイズできる。

syntax.yamlファイルの書式はとても簡単で、↓このように書く。

block:
  webtitle:          "*?"
  chapter:           "*"
  section:           "**"
  subsection:        "***"
  ul1:               "-"
  ul2:               "--"
  ol1:               "+"
  ol2:               "++"

inline:
  bold:              ["''", "''"]
  italic:            ["'''", "'''"]
  del:               ["%%", "%%"]
  link:              ["[", "]"]

キー(「:」の左側の文字列)に要素名を、値(「:」の右側)に行頭マークを書く。インライン要素の場合は、行頭マークの代わりに開始マークと終了マークを書く。空白の代わりにタブを使ってはいけない点には注意。


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



…と、そろそろ疲れてきたので、続きはまた今度。
また今度のネタ:

  • Wiki文法のカスタマイズ、理想を諦めたこと
  • ネストしたインライン要素のパース
  • 記法バンドルのロードとキャッシュ
  • WikiForme Web UIの設計について
  • XML自動インデントの実装

*1:正確にはBlockElementクラスを継承したクラスを継承している。BlockElementを継承したクラスは、記法バンドルごとに変更する。これによって1つのrubyプロセスで異なる記法を読み込んだときでも、別々のクラス変数を使うことができる。