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

WikiFormeを改良中

少し前にプロトタイプを紹介したWikiFormeを改良中です。

「手軽さ」と「柔軟さ」(カスタマイズ性)を両立するのはいつの時代も難しいと思うのですが、WikiForme的にはどちらを重視した方がいいのか。両方あればいいんですが…。


やっぱりWikiFormeの存在理由自体はカスタマイズ性にあって、Rubyが分からないと記法を作れないというコストを払ってでもカスタマイズ性があった方がいいはず、というわけで作ったのですが、どこまでも柔軟性があってもしょうがない。
行頭の記号(別に記号でなくても良いですが)を自分で決められるので、PukiWikiのプラグインのように後付けする要素でも、外付け感なく使えるのが良い感じだと思っています。それからWikiシステム全体を含んでいなくてパーサだけなので、ローカルPC内でもちょっとしたプリプロセッサ的にも使えるのも良い感じ。


でも記号を変えられるだけではなくて、自分で要素自体を作れるのがポイントだと思うのです。そこで、今のところRubyのクラスと記法の要素が一対一で対応しているので、要素Bから要素Aで定義しておいたメソッドを呼び出すとか、要素Bの中で要素Aをnewして使うとか、module_evalなどを駆使して黒魔術を行使することもできます。しかし、これがために要素の定義が冗長になったり、実装が複雑になったりしています。


たとえば今の要素の定義方法は、露骨にclassが出てくる↓こんな方法ですが、

class Hoge
  block_element "hoge"
  def containable?(instance)
    [Fuga, :Toplevel]
  end
  def process
    "<hoge>#{@arg}</hoge>"
  end
end

単純な記述をするには、よりDSLっぽい↓の方が簡潔だと思うわけです。

block Hoge => "hoge" do
  contains  :Fuga, "Toplevel"
  process {|arg, child, combination|
    "<hoge>#{arg}</hoge>"
  }
end

しかし後者だとRubyのクラスの機能をすべて使えない(メソッドを定義するとか)ので、カスタマイズ性としては悪くなります。書式定義としては直感的でも、Ruby的には直感的でない。
ちなみに今の改良案だと↓こんな感じにしています。

class Hoge
  block "hoge", :toplevel, :transparent
  combines :Hoge
  contains :Fuga, :toplevel
  # または contains {[ Fuga, :toplevel ]}
  # または def containable?(instance) [Fuga].include?(instance.class) end
  def process
    "<hoge>#{@text}</hoge>"
  end
end

うーむ。



細かいところでも、包含可能要素を定義するのにも、前者のようにcontainable?メソッドを定義することで定義するのか、containsメソッドを呼び出して定義するのかも悩みどころです。containable?メソッドで配列を返しているのは実はシンタックスシュガー的なもので、実際には

  def containable?(instandce)
    [Fuga, Hoge].include?(instance.class)
  end

と言うように、渡された要素(instance)を包含可能なのかどうか、trueかfalseを返すメソッドとして定義できます。この方法だと、文脈によって包含可能になったりならなかったりする要素を作ることができます。そうすると、↓こんな記法を作れます。

*1. Aについて
*1.1. Aとは
AとはAであってAである
*1.2. Aの用法
好きなように使う
*1. Bについて

見出しも小見出しも「*」になっていますが、与えられた引数(「1. Aについて」など)の先頭につけられた数字から、見出しなのか小見出しなのかを判断して次の要素を包含可能かどうか決定します。実際にこんな気分屋な要素が必要になるかは良く分からないですが、カスタマイズできる余地としては欲しいところ。


実はWikiFormeを作り始めた最初のきっかけは、「XMLを手で書きやすくするためのツールを作りたい」と思ったからで、手で書くので極限まで手抜きができないとイヤなのですが、一方であらゆる書式を表現したいのです。(DTDを渡すと自動的にWikiForme記法が出てきたらいいなーとか思っていたりいなかったり)
個人的にはhttpd.conf記法やiptables記法などがあったら嬉しいなーなんて思っています。


それから、テンプレートの扱いを変えようと思っています。プロトタイプで作ったテンプレート(BlockTemplate::RecursiveListClassなど)は、Wiki風記法からの変換先としてHTMLをかなり意識していたのですが、HTML以外でも使えるテンプレートを作っています。とはいえ、HTMLも当然使うので、テンプレートをレイヤー構造にして、下位レイヤーで基本構造を、上位レイヤーでHTMLを提供しようと思っています。
まだHTMLの上位レイヤーは作っている途中ですが、下位レイヤーは↓こんな感じになっています。

ul = BlockTemplate::ListGenerator.new(
  "<ul class=\"list<%= config %>\"><%= enumerate %></ul>",
  "<li><%= @arg %></li>",
  "<li><%= reflex %></li>"
)
class UnorderedList1 < ul.generate(1)
  block "ul1", :toplevel, :list
  combines :UnordredList1
  contains :UnordredList2, :toplevel
  uncontains :UnorderedList1, :headline
end
class UnorderedList2 < ul.generate(2)
  block_element "ul2"
  combines :UnorderedList2
  contains :UnorderedList3, :toplevel
  uncontains :headline
end
table = BlockTemplate::TableGenerator.new(
  "<table>
<% if !caption.empty? %><caption><%= caption %></caption><% end %>
<%= row_enumerate %>
</table>",
  "<tr><%= col_enumerate %></tr>",
  "<td><%= @arg %></td>",
  "<td><%= reflex %></td>",
  Proc.new {|line| line.split("|") }
)
class Table1 < table.frame
  block "table", :toplevel, :table
  combines :Table1
  contains :Table2
end
class Table2 < table.row
  block "tr"
  combines :Table2
  contains :Table3, :toplevel
  notcontains :Table1, :headline
end
class Table3 < table.cell
  block "td"
  combines :Table3
  contains :toplevel
  notcontains :Table1
end

これもListGenerator.newやTableGenerator.newの引数に渡すものをProcオブジェクトにするかerubyにするか迷ったのですが、今のところはerubyになっています。