WikiForme - 自分用Wiki記法パーサ バージョン0.1!
自分用のWikiForme記法を簡単に作れるカスタマイザブルパーサWikiFormeの新バージョンを作りました。
※2007/09/23: バージョン 0.3をリリースしました > WikiForme 0.3 リリース! - 構造化Wiki記法パーサ
※2007/09/13: バージョン 0.2をリリースしました > WikiForme 0.2 - 構造化Wiki記法パーサ
wikiforme-0.1.0.tar.gz
test.shを実行してみてください。test.txtに書かれたWiki風記法がHTMLに変換されます。実行にはRuby 1.8が必要です。
WikiFormeの概要は前回のエントリーをご覧ください。(記法の書き方は新バージョンで変わっているので注意してください)
はてな記法、PukiWiki記法、tDiary記法などなど、世の中「なんとか記法」が溢れているわけですが、往々にして「自分にぴったり合う記法なんてどこにも無い!」という結論に達する場合が多く、結果として「なんとか記法」の乱立を生んでいるのではないでしょうか。
前回のプロトタイプバージョンと比べて、記法の定義が簡潔に出来るようになり、またテンプレートを使って新しい記法を定義しやすくなりました。
記法はRubyで定義し、一つの要素が一つのクラスの対応します。たとえば↓このように記法を定義します。
require 'wikiforme' require 'wikiforme/html' # ブロック要素の定義 module MyWikiBlock # テンプレートを使う Headline1 = HTMLTemplate::makeHeadline1() Headline2 = HTMLTemplate::makeHeadline2() Headline3 = HTMLTemplate::makeHeadline3() ol = HTMLTemplate::OrderedListGenerator.new OrderedList1 = ol.level1 OrderedList2 = ol.level2 OrderedList3 = ol.level3 OrderedList4 = ol.level4 OrderedList5 = ol.level5 ul = HTMLTemplate::UnorderedListGenerator.new UnorderedList1 = ul.level1 UnorderedList2 = ul.level2 UnorderedList3 = ul.level3 UnorderedList4 = ul.level4 UnorderedList5 = ul.level5 table = HTMLTemplate::TableGenerator.new TableFrame = table.table TableRow = table.tr TableCell = table.td # 自分で作る class Text block "text", :toplevel def process # @textだと指定された文字列そのまま # @text.processだとインライン要素が展開される return "<p>#{@text.process}</p>" end end class TextBox block "textbox", :toplevel contains :toplevel # 包含可能要素 uncontains self def process "<div class=\"textbox\">#{@children.process}</div>" end end class Document block "mywiki" contains :Headline1, :transparent # 包含可能要素 def process return "<html>" + "<head><title>#{@text}</title></head>" + "<body>#{@children.process}</body>" + "</html>" end end end # インライン要素の定義 module MyWikiInline include WikiForme Emphasis = HTMLTemplate::SimpleInline("em") Strong = HTMLTemplate::SimpleInline("strong") Small = HTMLTemplate::SimpleInline("span", "small", :class => "small") URL = HTMLTemplate::URLAutoLink() class Link inline "link" # ↑"inline"または"inline_regex"を書くと(必須)、 # WikiForme::InlineParser::InlineElementがincludeされる def process # @textはsettings[:escape]で自動的にエスケープされる # エスケープ前の文字列を取り出すには、@text.raw title, href = @text.raw.split(">", 2) title = CGI.escapeHTML(title) href = CGI.escapeHTML(href) return "<a href=\"#{href}\">#{title}</a>" end end end include MyWikiBlock include MyWikiInline settings = { :blocksugar => { "*" => Headline1, "**" => Headline2, "***" => Headline3, "//" => Comment, "+" => OrderedList1, "++" => OrderedList2, "+++" => OrderedList3, "++++" => OrderedList4, "+++++" => OrderedList5, "-" => UnorderedList1, "--" => UnorderedList2, "---" => UnorderedList3, "----" => UnorderedList4, "-----" => UnorderedList5, "|" => TableFrame, "||" => TableRow, "|||" => TableCell, }, # ↑ブロック要素のシンタックスシュガーは「行頭マーク」 :text => Text, # 行頭マークがないテキスト :blank => BlankLine, # 空行 :document => Document, # 一番親の要素 :inlinesugar => { ["'''", "'''"] => Emphasis, ["''", "''"] => Strong, ["((", "))"] => Small, ["[[", "]]"] => Link, }, # ↑インライン要素のシンタックスシュガーは「開始マーク」と「終了マーク」 :escape => CGI.method(:escapeHTML), # ↑@textは:escapeで指定したメソッドで自動的にエスケープされる # (エスケープされていない文字列は@text.rawで取り出せる) } parser = WikiForme::Parser.new(MyWikiBlock, MyWikiInline, settings) File.open("test.txt") {|file| puts parser.process(file, "test document") # ↑"test document"は一番親の要素(Document)の@textになる }
ブロック要素のシンタックスシュガーは、「行頭マーク」を指定します。シンタックスシュガーを指定しないでも、「\name text」という記法で使えます。
同様にインライン要素では、「開始マーク」と「終了マーク」を指定します。シンタックスシュガーを指定しない場合は、「\name{text}」という記法で使えます。
「行頭マーク」や「開始マーク」「終了マーク」には、記号以外に英数字も使えます。
?←行頭マークはてな |A|←行頭マークが ''←開始マーク 異種混合 終了マーク→'' ←行頭マークが半角スペース [id:←開始マーク 終了マーク→] などなど
WikiFormeの仕組み
テンプレートを使うと少し分かりにくいですが、すべてのブロック要素には「包含可能要素」と「結合可能要素」を定義します。ソースの文章を1行ずつ読み込んでいき、見つかった要素が前の要素の包含可能要素であれば前の要素の子にし(配列@childrenに追加される)、包含可能でなければ前の要素を閉じます。同様に結合可能であれば前の要素と結合し(配列@combinationに追加される)、結合可能でなければ前の要素を閉じます。
文章構造が完成したら、最も親(ドキュメント自体)のprocessメソッドを実行します。後は再帰的にprocessメソッドを呼び出していけば、全体が目的の文章形式(HTMLなど)に変換されるというわけです。
包含可能と結合可能
「包含可能と結合可能の2つの要素で文章構造を作る」という点が、WikiFormeの本質的な部分です。
<section> <paragraph> テキスト </paragraph> </section>
このように入れ子になった構造を書くには、いわゆる「閉じタグ」を書かなくてはなりません。しかし、閉じタグを書くのは非常に面倒なので、「sectionの中にはsectionは入らない、paragraphの中にはsectionは入らない」などのように包含関係を決めておきます。そうすると閉じタグを書かなくても入れ子構造を書けるようになります。
# ここでdocument開始 \section # section開始 \paragraph # paragraphはsectionの中に入れられるので、sectionの中に入れる テキスト # テキストはparagraphの中に入れられるので、paragraphの中に入れる \section # sectionはテキストの中に入れられない、 # sectionはparagraphの中に入れられない、 # sectionはdocumentの中に入れられるので、documentの中に入れる
(ちなみに最後まで含められない場合はエラーになります。)
これでだいたいOKなのですが、これだけではリスト(箇条書き)を書こうとしたときに困ります。
-こういう風に -リストを -書きたい
↑このような書式を、↓このように変換したいわけです。
<list> <item>こういう風に</item> <item>リストを</item> <item>書きたい</item> </list>
しかし、これは含められる/含められないの関係だけでは実現できません。
↓ダメな例1(「listはlistを含められない」にした場合)
<list><item>こういう風に</item></list> <list><item>リストを</item></list> <list><item>書きたい</item></list>
↓ダメな例2(「listはlistを含められる」にした場合)
<list> <item>こういう風に</item> <list> <item>リストを</item> <list> <item>書きたい</item> </list> </list> </list>
そこで、「結合可能要素」を導入します。「listはlistに含められないが、listとlistは結合可能」と定義します。
<list> # 結合グループ開始 <item>こういう風に</item> <item>リストを</item> # 前のlistと結合 <item>書きたい</item> # 前のlistと結合 </list> # 結合グループおわり
以上を踏まえて記法を作っていくわけですが、多くの場合は包含可能だけで作れるので、結合可能は凝った記法を作ろうとしなければ深く考えなくてもOKです。包含可能も、実は「文章」「章」「節」「項」「本文」(「本文」に相当する要素がたくさんある)だけあればだいたい足ります。
なお、包含可能や結合可能という仕組みがあるのはブロック要素だけで、インライン要素にはありません。
要素のグループ化
包含可能要素や結合可能要素には、要素の名前(=Rubyのクラス名)を指定するわけですが、一つ一つ指定していくのはやってられないので、グループにして扱うことができます。
class Chapter block "chapter" # ←ここで指定する文字列は先頭マークの指定なので、 # 包含可能/結合可能とは関係ない contains :Section, :toplevel # Section要素と、toplevelグループを含められる # … end class Section block "section" contains :Subsectoin, :toplevel # Subsection要素と、toplevelグループを含められる # … end class Table block "table" add_block_group :toplevel # toplevelグループに属する # … end class Image block "image", :toplevel # ↑ block "image"; add_block_group :toplevel と同じ # … end class ImageBox block "imagebox", :toplevel contains Image # ←前に定義された要素であれば、シンボルではなくクラス定数で指定可能 # … end
要素の名前(=Rubyのクラス名)は、先頭が大文字でなければなりません。グループ名は先頭が小文字でなければなりません。
"block"で指定する先頭マークの指定はシンタックスシュガーとは別です。このマークを""やnilにしておくと、シンタックスシュガーでのみ使えるようになります。(\name text で使えなくなる)
自分の記法を作る
まずは「HTMLTemplate」を使って記法を作ってみると簡単です。HTMLTemplateはWiki風記法をHTMLに変換する記法を作るためのテンプレートで、包含可能など定義もだいたいできています。
ブロック要素を作る
HTMLTemplateを使ってブロック要素を追加する場合は、:toplevelグループに属した要素を作っていけばOKです。
普通のブロック要素を作るには(普通でないブロック要素は後述)、以下のように書きます。
class BlockElementName block "マーク", :所属するグループ1, :所属するグループ2 contains :包含可能要素名1, :包含可能要素名2, 包含可能要素1, :包含可能グループ1 uncointains :包含可能でない要素1, :包含可能でないグループ1 # contains/uncontains と同様に combines/uncombines で結合可能要素を定義 def process return "processは変換後の文字列を返すメソッド" # 例: return "<p>#{@text.process}</p>" # @textの他に、包含した要素が@children配列に、 # 結合した要素が@combinationに入っています。 # パーサの情報が@parserに入っています。 end end
uncontainsは、「Textが:toplevelに属していて、containsに:toplevelを指定したが、Textは包含可能にしたくない」というときに、「uncontains Text」というように使います。
contains/uncontains/combines/uncombinesには、「要素名のシンボルか文字列(先頭が大文字)」、「要素名の定数」、「グループ名のシンボルか文字列(先頭が小文字)」を指定できます。
要素名の定数を指定するには、その定義を書く行より前に定数が定義されていないと、Rubyインタプリタでエラーになります。要素名の定数を指定した場合は、その要素とis_a?の関係にある要素も含まれます。(つまりcontainsにObjectを指定すると、すべての要素が包含可能になります)
processメソッドでは、行頭マークに続く文字列が@textに、子要素が@children(Array)に、結合した要素が@combination(Array)に入っています。@childrenと@combinationにはprocessメソッドが、@combinationにはprocessメソッドに加えてcombination_processメソッドが特異メソッドとして定義されています。
参考までに、@children.processの定義は以下です。
def @children.process block = "" each {|c| body << c.process } return body end
@textにもprocessメソッドが定義されています。@text.processはインライン要素を展開します。
@textは、そのままStringクラスのインスタンスとして操作できますが、パーサを作るときに登録するメソッドでエスケープされています。HTMLに変換する記法を作るときは、このメソッドにCGI.escapeHTMLを登録してしておくと便利です。エスケープ前の文字列を取り出すには、@text.rawを使います。
インライン要素を作る
インライン要素には、包含可能や結合可能などの仕組みはありません。"inline"(または"inlnie_regex")と、processメソッドだけです。
class InlineElementName inline "マーク" def process return "変換後の文字列を返すメソッド # 例: return "<em>#{@text}</em>" end end
inline_regexは、「\name{text}」の形ではなく、正規表現でマッチします。
class InlineElementName inline_regex /括弧を1つ含んだを正規表現/ def process return "文字列を返す" end end
正規表現は、括弧を必ず1つ含まなければなりません。2つではダメです。括弧内にマッチした文字列が@textに入ります。
たとえばURLを自動的にリンクにするinline_regex要素は、以下のように書けます。
class URL inline_regex /((?:(?:https?|ftp|itunes):\/\/|mailto:)[\w\/\@\$()!?&%#:;.,~'=*+-]+)/ def process "<a href=\"#{@text}\">#{@text}</a>" end end
インライン要素でも@textは自動的にエスケープされています。エスケープ前の文字列を取り出すには、@text.rawを使ってください。インライン要素の@textにもprocessメソッドが定義されています。@text.processはさらに再帰的にインライン要素を展開します。
BlockTemplateを使う
よく使うが定義が複雑な「表」と、「リスト」にはテンプレートを用意してあります。テンプレートを使うと、簡単に表組み要素とリスト要素を作れます。HTMLTemplateはHTMLに特化したテンプレートですが、BlockTemplateはHTMLに限らず一般的に使えます。
BlockTemplateはHTMLTemplateと違って包含可能や結合可能などの定義は行いません。
ListGenerator
ListGeneratorを使うと、リストを簡単に作れます。
require 'wikiforme/template' module MyBlock ol = WikiForme::BlockTemplate::ListGenerator.new( '<ol class="<%= config %>"><%= enumerate %></ol>', '<li><%= @text.process %><%= reflex %></li>' ) class OrderedList1 < ol.generate("list1") block "ol1", :toplevel contains :OrderedList2 # processメソッドの定義は不要 end class OrderedList2 < ol.generate("list2") block "ol2", :toplevel contains :OrderedList3 end class OrderedList3 < ol.generate("list3") block "ol3", :toplevel end end
ListGeneratorクラスのインスタンスを、eRubyスクリプトを2つ渡して作成します。
generateの引数に渡したオブジェクトは、eRubyスクリプトの中で"config"として参照できます。この例の場合はそのまま"class="の指定になっています。
1つ目のeRubyスクリプトでは、"config"と"enumerate"を使えます。enumerateはリストの要素(2つ目のeRubyスクリプト)を並べていきます。
2つ目のeRubyスクリプトでは、"config"と"@text"と"reflex"を使えます。reflexは、子リストや子要素をそこに展開します。
TableGenerator
TableGeneratorを使うと、表組みを簡単に作れます。
require 'wikiforme/template' module MyBlock table = WikiForme::BlockTemplate::TableGenerator.new( generator = WikiForme::BlockTemplate::TableGenerator.new( '<table>' + '<% if !caption.empty? %><caption><%= caption %></caption><% end %>' + '<%= row_enumerate %>' + '</table>', '<tr><%= col_enumerate %></tr>', '<td><%= @text.process %><%= reflex %></td>', Proc.new {|line| line.split("|") } ) ) # この場合、TableFrame, TableRow, TableCellのシンタックスシュガーは # それぞれ "|", "||", "|||" がオススメ class TableFrame < table.table block "table", :toplevel combines self # 必要 contains :TableRow # 必要 # processメソッドの定義は不要 end class TableRow < table.row block "tr" combines self # 必要 contains :TableCell # 必要 # processメソッドの定義は不要 end class TableCell < table.cell block "td" combines self # 必要 # processメソッドの定義は不要 end end
TableGeneratorクラスのインスタンスを、3つのeRubyスクリプト(frame, row, cell)と1つのProcオブジェクト(splitter)を渡して作成します。
(最後にもう一つ任意のオブジェクトを渡すことができ、eRubyスクリプトの中から<% config %>として参照できます)
上のようにクラスを作ると、↓以下の3つの記法で表を書けます(シンタックスシュガーに"|", "||", "|||"を指定した場合)
|表のキャプション || |||1a |||1b |||1c || |||2a |||2b |||2c
|表のキャプション ||1a ||1b ||1c | ||2a ||2b ||2c
|1a|1b|1c |2a|2b|2c
1番目の記法では、TableCellの包含可能要素に:toplevelを加えておけば、表の中にブロック要素を入れることもできます。
TableGeneratorの作成時に渡すProcオブジェクト(splitter)は、3番目の記法でセルとセルを区切るために使います。splitterは、文字列をブロック引数に取って、配列を返してください。
1つ目のeRubyスクリプトでは、"config"と"caption"と"row_enumerate"を使えます。row_enumerateは2つ目のeRubyスクリプトを並べていきます。
2つ目のeRubyスクリプトでは、"config"と"col_enumerate"を使えます。col_enumerateは3つ目のeRubyスクリプトを並べていきます。
3つ目のeRubyスクリプトでは、"config"と"@text"と"reflex"を使えます。reflexは、子要素(@children)があればそれを展開します。
その他の仕様
containable? combinable?のオーバーライド
ブロック要素の"contains"や"uncontains"は、実際にはcontainable?メソッドの挙動を操作しているものです。このcontainable?メソッドはオーバーライドできます。containable?メソッドは、要素のインスタンスを一つ受け取り、その要素を包含可能かどうかをtrueかfalseで返すメソッドです。
たとえば↓こんな風に使えます。Quotaion要素は、QuotationEndMarkが現れるまではtoplevelグループとtransparentグループを包含可能で、QuotationEndMarkが現れたら、その次の要素から何も包含しなくなります。
module MyBlock class Quotation block "quote", :toplevel contains :toplevel, :transparent uncontains self, :headline def process "<blockquote>#{@children.process}</blockquote>" end def containable?(instance) if !@quote_end.nil? return false elsif instance.class == QuotationEndMark @quote_end = true return true else # ↓"contains"と"uncontains"で判断 return super(instance) end end end class QuotationEndMark block "quote_end" def process return "" end end end
multiline
multilineは、テキストを複数行取る要素です。整形済み要素のような要素を作るために使えます。
テキストは@lines変数に配列として入ります。
class Pre block "pre", :toplevel multiline "EOF" def process return "<pre>#{@lines.join("\n")}</pre>" end end
"multiline"の引数には、文の終わりを示すマークを書きます。この例の場合は以下のような記法が書けます。
\pre 整形済み テキスト EOF こから先は通常
"multiline"は、実際にはmultiline_end?メソッドを定義しています。multiline_end?メソッドはオーバーライドできます。multiline_end?メソッドは、行を一つ受け取り、その行で文を終えるかどうかをtrueかfalseで返すメソッドです。
たとえばヒアドキュメントのような記法を以下のようにして作れます。
class Pre block "pre", :toplevel multiline nil def multiline_end?(line) line == @text.raw end def process return "<pre>{@lines.join("\n")}</pre>" end end
BlankLineとcloser
包含可能要素の定義によっては、なかなか閉じられないブロック要素ができてしまう可能性があります。たとえば他のブロック要素を含められる表は、表の終わりを明示的に示さないと、どこまでが表なのか、どこから通常に戻るのかが分からなくなってしまいます。
BlankLine(空行)を包含可能にしないことでだいたいは解決できますが、空行も含めたいときは、"closer"を使うと便利です。
closerは、前の要素が次の要素を含められる場合に、その要素を含めなくする効果があります。closerは以下のように定義します。
class Closer block "close", :transparent closer # ←これが重要 def process "" end end
ちなみに、空行は以下のような定義で良いでしょう。
class BlankLine block "blank", :transparent def process "" end end
structure
これはまだ実験段階なのですが、包含可能・結合可能の定義と、processメソッドの定義を別のモジュールに分けることで、一つの記法から複数のフォーマットに変換するプログラムを作りやすくすることを狙っています。
1. 包含可能・結合可能の定義
module Structure module Section CONTAINS = {[ Subsection ]} end module Subsection CONTAINS = {[ Body ]} end module Body CONTAINS = [:toplevel] end end
2. processメソッドの定義
module HTMLMode class Section block nil structure Structure::Section def process "<h2>#{@text}</h2>#{@children.process}" end end class Subsection block nil structure Structure::Subsection def process "<h3>#{@text}</h3>#{@children.process}" end end end
module TeXMode class Section block nil structure Structure::Section def process "\section{#{@text}}\n#{@children.process}\n" end end class Subsection block nil structure Structure::Subsection def process "\subsection{#{@text}}\n#{@children.process}\n" end end end
パーサもRubyで書いてあって、オープンソースなので、パーサもいじってしまってください。要素とRubyのクラスと対応させるためにリフレクションが大量に使われていますが、それほどおかしなことはやっていないと思います。
WikiFormeで目指すモノ
なにやら作り込んでいたら、なかなか大きな仕様になってしまったわけですが、私はWikiFormeを「一次文章(←人間が書く)を書くためのツール」として捉えています。つまり、一度Wiki風記法で一次文章を書いたら、HTMLにもできるし、PDFにもできるし、はてな記法に変換することもできるし…ということをやりたいと思っています。
XMLを知った頃には、一次文章をXMLで書けば何にでもできてスバラシイ!と思っていたのですが、XMLは手で書くにはあまりに煩雑で挫折しました。
XMLを手で書きたくない元凶は「閉じタグ」であることは間違いありません。開始タグは「これから書くぞー」というモチベーションがあるのでまだ良いのですが、もう書き終わったのに閉じタグなんて書いてられない。
それから、どんな要素を書くにもほぼ同等のコスト(タイプ数)がかかるのも問題です。よく使う要素は短いタイプ数にするべきです。<や>をエスケープしないといけないのもいけない。それは人間の仕事じゃない。
要するに、XMLは自然言語を手でマークアップするには明らかに適していないですよね。
Wiki記法は自然言語をマークアップするために作られたので、なかなか書きやすい。しかしHTMLに変換することが前提とされているので、章や節といった基本的な構造も定義できないため、HTML以外の形式には変換しにくい。
これはとんでもなく問題で、つまり「再利用可能」で、「自然言語をマークアップ」できて、「手で書ける」という3つが揃った方法がないわけです。結果として、せっかくがんばって書いた一次文章が特定の形式向けにしかならなくなってしまいます。今この瞬間にも様々な一次文章が書かれていて、それが本になったり論文になったりしているわけですが、それをWebに載せて検索可能にするにはHTMLにしないといけないけど、それは面倒だからやれない、というのは、非常にもったいない。
ブログ <=> PDF という変換や、そもそも ブログA用文章 <=> ブログB用文章 という変換でさえ困難で、せっかく電子データなのに、これは何かおかしい。コンピュータもっとがんばれよーと思うわけです。
一方、自然言語も、一定の体裁を持った文章は、一定の構造を持っています。その体裁を記述するときに、何でも出来る多機能ワープロソフトを使うのも牛刀であるわけですが、テキストエディタ(プレインテキスト)だとまったく体裁を記述できないので、後で見てどこが見出しだったか分かるようにしておくのに困ります。それでも多くの場合は文章はテキストエディタで文章を書いて、だいたい文章が出来上がった後でWYSIWYGなソフトに流し込んでいると思います。
そこを考えれば、現時点で一次文章を書く方法もさほどうまくいっていないので、そこのところに新たな仕組みを導入するのは、さほど高く付かないと持っています。
そこで、そこにXMLのような再利用可能な形式を導入したいけど、XMLは手で書けないので、Wiki風記法、でも章や節などの入れ子構造を書けるWiki記法だ、というわけです。
というわけで、実はWikiFormeを作ったきっかけは「今までのWiki記法がカスタマイズできなくてイヤだったから」ではなくて、どちらかというとXMLをどうにかしようと思った、既存のWiki風記法では入れ子構造を書けないというところにあります。
後はWYSIWYGで書けると良いのですが、左にWiki記法、右に変換後という形で表示するのでも良いかな、と思っています。Ajaxだとリアルタイム性に欠けて快適でなくなる→結局使わなくなるというオチだと思うので、デスクトップアプリケーションか、今流行のAIRなどで高速表示できると嬉しいなーと思っています。必ずしもブラウザで動く必要はないと思っています。ローカルでもプレビューさえ出来るなら、最後に1回スクリプトでアップロードしたり同期したりすれば良いので。
それからページ物も書けるように、CMS的に複数のページを統括するマネジメントシステムも欲しいところです。これもブラウザで鈍重になるよりは、ローカルで高速に動いて欲しい。
さらにバージョン管理システムもあればとっても嬉しい。