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

WikiForme 0.2 - 構造化Wiki記法パーサ

はてな記法PukiWiki記法、tDiary記法などなど、世の中「なんとか記法」が溢れているわけですが、往々にして「自分にぴったり合う記法なんてどこにも無い!」という結論に達する場合が多く、結果として「なんとか記法」の乱立を生んでいるのではないでしょうか。


というわけで、自分専用のWiki記法を簡単に作れるカスタマイザブルパーサ WikiForme を作ってみました。乱立乱立!

記法を統一しようなんてムリですよね。もはや宗教論争です。自分専用の記法があればいいんです。


と、このバージョン0.0.1から約2ヶ月、大きくパワーアップしたWikiForme 0.2を公開します。


※2007/09/23: バージョン 0.3をリリースしました > WikiForme 0.3 リリース! - 構造化Wiki記法パーサ
wikiforme-0.2.0.tar.gz
ダウンロードして展開したら、./example.shを実行してみてください。example/test.txtに書かれたWiki記法の文章がHTMLに変換されます(実行にはRuby 1.8が必要です)。
example/test.htmlに変換済みのHTMLがあります。




構造化Wiki記法とは?

構造化Wiki記法とは、入れ子構造を作れるWiki記法です。 (私が勝手に命名しました :-p)
今までのWiki記法はHTMLに変換することが目的とされていたので、「の中に

が入る」と言った、入れ子構造を作ることができませんでした。WikiFormeは、今までのWiki記法と同じように簡単に書くことができるにもかかわらず、入れ子構造を作ることができます。

*1. サンプルWiki記法
**1.1. 小見出し
段落
*2. 変換の例
この文章を[[SmartDoc>http://www.smartdoc.jp/]]風XMLに変換
<section title="1. サンプルWiki記法">
    <subsection title="1.1. 小見出し">
        <p>段落</p>
    </subsection>
</section>
<section title="2. 変換の例">
    <p>この文章を<a href="http://www.smartdoc.jp/">SmartDoc</a>風XMLに変換</p>
</section>


入れ子構造を作ることができるので、一つのWiki記法の文章をいろいろなフォーマットの文章に変換することが(比較的簡単に)できます。たとえば、論文用に書いたWiki記法の文章を、TeXに変換したり、HTMLに変換してWebに載せたり、はてな記法に変換してブログに載せたり、といったストーリーが考えられます。(まだそこまで実装できていませんが ^_^;)


記法のカスタマイズ

見出しを「*」にするのか「!」にするのかといった話題は宗教論争ですが、WikiFormeでは記法は自由にカスタマイズできます。
見出しなどのブロック要素行頭マークを、太字やリンクなどのインライン要素開始マーク終了マークを指定します。

*こんな見出し
!あんな見出し
%←行頭マーク
//←2文字以上でも
@image ←記号でも英数字でもOK
'' ←開始マーク インライン要素 終了マーク→ ''
<em>←開始マーク HTML風 終了マーク→</em>
[id: ←開始マーク はてなid記法風 終了マーク→ ]

入れ子の仕組み

WikiFormeは、それぞれの要素(見出しなど)に「入れ子にすることが可能な要素」(包含可能要素)を決めておきます。包含可能な要素が現れたら、その要素を入れ子にします。包含可能でなければ、入れ子にしません。


たとえば\chapterは\sectionを包含可能だ!と指定しておくと、

\chapter 第1章
\section 第1節
\chapter 第2章

という文章は、

<chapter text="第1章">
    <section text="第1節"/>
</chapter>
<chapter text="第2章"/>

と言った具合に入れ子構造になります。


この入れ子の指定は、wikiforme-0.2.0.tar.gzの、article/base-devel/structure.yamlファイルに書いてあります。もちろん自分で新しい要素を追加することもできます。



Rubyで変換方法を書く

入れ子構造になった要素をどのように変換するか(HTMLに変換するのか、TeXに変換するのか、etc...)は、Rubyでプログラムします。
たとえば↓こんな感じです。(先のSmartDocの例)

block["section"].process {|text,children|
  "<section title=\"#{text}\">#{children.process}</section>"
}
block["paragraph"].process {|text,children|
  "<p>#{text.process}</p>"
}

とっても簡単ですね!


少し補足すると、#{text.process}は、インライン要素を展開します。そのまま#{text}と書くと、インライン要素は展開されません。#{children.process}は、入れ子になった子の要素を展開します。


上の例はブロック要素ですが、インライン要素では↓こうなります。

inline["bold"].process {|text|
  "<strong>#{text.process}</strong>"
}

ここでも#{text.process}と書くと、インライン要素を展開します。これで「太字の中に斜体がある」という場合でも問題なく展開できます。



親要素補完

親要素補完は、ちょっと複雑なWikiForme 0.2の新機能です。(ちなみにWikiForme 0.1の「結合可能」は無くなりました)
ご存じの通り、HTMLの表(table)は、table→tbody→tr→tdと入れ子になっています。これをそのままWiki記法にしようと思うと、↓のように書くことになります。

\table
\tbody
\tr
\td AA
\td AB
\tr
\td BA
\td BB

これは面倒です。知らぬ事情のために\tableと書くなど耐えられません。そこで、「tbodyの親はtableである」「trの親はtbodyである」と決めておくと、\tableや\tbodyを書かなくても、自動的に補完してくれます。つまり、↓このように書くことができます。

\tr
\td AA
\td AB
\tr
\td BA
\td BB

もちろん行頭マークはカスタマイズできるので、お好みにより↓このように書くこともできます。

|=
|AA
|AB
|=
|BA
|BB

もっと簡単に書きたければ、\tr要素にカンマ区切りのテキストを渡すと、自動的にtdに分割してくれことを考えるでしょう。そうすると↓このように書けるようになります。(これはRubyでどのようにプログラムするかに依ります)

,AA,AB
,BA,BB

記法のカスタマイズファイルの書き方

「*」や「,」などのカスタマイズは、wikiforme-0.2.0.tar.gzの中のexample/syntax.yamlにサンプルがあります。

block:
  chapter:              "*"
  section:              "**"
  subsection:           "***"

  tr:                   ","
  tr splitter:          ","

  pre:                  ">||"
  pre END:              "||<"

inline:
  bold:                 ["''",  "''"]
  link:                 ["[[",  "]]"]
  link anchor:          ">"

prepre ENDは、multiline要素(テキストを複数行取る要素。後述)で、開始行の行頭マークと、終了行のマークを指定しています。

pre ENDと同様に、要素名 <半角スペース> 引数: マークと書くと、Rubyの変換プログラムの動きを変えることができます。




要素の追加とオーバーライド

新しい要素はプラグイン的に追加したり、配布したりすることができます。

wikiforme-0.2.0.tar.gzを展開してみると、articleというディレクトリがあります。この中に先ほどの「包含可能」の定義と、Rubyで書かれた変換プログラムが入っています。

article/
article/base-devel
article/base-devel/structure.yaml
article/base-devel/COMMON
article/base-devel/COMMON/attribute.rb
article/base-devel/COMMON/comment.rb
…
article/base-devel/xhtml
article/base-devel/xhtml/list.rb
article/base-devel/xhtml/inline.rb
article/base-devel/xhtml/table.rb
…
article/base-devel/SmartDoc
article/base-devel/SmartDoc/list.rb
…
article/user

まず、このarticleディレクトリの中に記法の定義がすべて入っているので、これをzipなどで固めれば、記法を配布することができます。


続いて、article/base-devel/structure.yamlが、「包含可能」の定義ファイルです。このstructure.yamlが入っているディレクトリ(この場合ではbase-develディレクトリ)が、記法定義の単位になります。このディレクトリにxhtmlSmartDocなど、変換先のフォーマットごとにディレクトリを作って、その中にRubyのプログラム(*.rb)を置いていきます。


articleディレクトリの中には、structure.yamlがいくつあっても構いません。名前順で後ろのディレクトリほど、階層が深いディレクトリほど優先され、前に定義された要素を上書きすることができます。
つまり、新たに要素を追加したい場合は、article/user/ディレクトリにstructure.yamlファイルを作れば、base-develを変更せず要素を追加できます。

article/user/structure.yaml
article/user/xhtml
article/user/xhtml/mylist.rb
article/user/SmartDoc/mylist.rb


userディレクトリの下にさらにディレクトリを作ってもOKです。structure.yamlが鍵です。

article/user/viver
article/user/viver/structure.yaml
article/user/viver/xhtml
article/user/viver/xhtml/list_override.rb

変換フォーマットの名前(xhtmlSmartDocなど)は新たに作っても構いませんが、COMMONだけは特殊で、どの変換フォーマットを指定されたときでも共通して読み込まれます。




structure.yamlの書き方

structure.yamlは、「包含可能」の定義ファイルです。どの要素がどの要素を包含可能なのか(など)を定義します。
多少複雑ですが、大したことはありません。

block:
  contents:
    type: group

  section:
    contain: [subsection, contents, blank]

  subsection:
    contain: [contents, blank]

  paragraph:
    group: [contents]

  blank:

  comment:
    extend: blank

  BLANK:
    extend: blank

  TEXT:
    extend: paragraph
contain

contain:には、包含可能な要素かグループを配列で指定します。グループとは、type: groupと指定されている要素です(ここではcontents)。

ここではsectionのところで、「contain: [subsection, contents, blank]」 と書きました。これは、【sectionは「subsectionと、subsectionを継承した要素」、「contentsグループに属する要素」、「blankと、blankを継承した要素」を含むことができる】ということを意味します。つまり、この場合にsectionが包含可能な要素は、「subsection」「paragraph」「blank」「comment」「BLANK」「TEXT」ということになります。


なお、group:には複数の要素を指定できますが、extend:には1つの要素しか指定できません。


BLANKとTEXT

BLANKTEXTは特殊な要素です。BLANKは空行、TEXTは行頭マークの無いテキストです。
ここでは、TEXTはparagraphを継承しているので、基本的にTEXTはparagraphと同じになります。しかしTEXTに独自の変換プログラムを書くと、paragraphの変換プログラムをオーバーライドすることになります。


parent

親要素補完もstructure.yamlで指定します。

block:
  table:
    group: [contents]
    contain: [tbody, thead]

  tbody:
    parent: table
    contain: [tr]

  thead:
    parent: table
    extend: tbody
    contain: [tr]

  tr:
    parent: tbody
    contain: [td]

  tr_split:
    extend: tr
    parent: tbody

  td:
    parent: tr
    contain: [contents, no table]


trには、parent: tbodyと書いてあります。これは、「trが現れたとき、親の要素にtbodyかtbodyを継承した要素がなければ、自動的にtbodyを補完する」ということを意味します。つまり、突然trが現れたときには自動的にtbodyが補完され(さらにtableも補完される)、tbodyの次にtrが現れたときには補完されません。theadの次にtrが来たときも補完されません(theadはtbodyを継承しているため)。

contain:にno tableと指定すると、「tableとtableを継承した要素は包含可能ではない」という意味になります。


type

type:に指定できる種類には、以下の3つがあります。

  • group
  • multiline
  • action

groupは既に紹介しました。後の二つは後述します。


インライン要素

以上の「contain」「extend」「parent」「type」は、すべてブロック要素のものです。インライン要素には、regexpしかありません。

inline:
  bold:

  italic:

  url:
    regexp: "((?:(?:https?|ftp|itunes):\/\/|mailto:)[\w\/\@\$()!?&%#:;.,~'=*+-]+)"


regexp:には、括弧を1つ含んだ正規表現を指定します。括弧は必ず1つでなければいけません。0コや2コではダメです。

regexp:を指定すると、開始マークと終了マークではなく、正規表現でマッチするようになります。URLを自動的にリンクにしたい場合などに使います。



変換プログラムで使える変数

Rubyで書く変換プログラム(user/xhtml/mylist.rbなど)は、↓こう書けると紹介しました。

block = WikiFormeMethod.block
inline = WikiFormeMethod.inline

block["section"].process {|text,children|
  "<section title=\"#{text}\">#{children.process}</section>"
}
block["paragraph"].process {|text,children|
  "<p>#{text.process}</p>"
}


これは実は簡略化した書き方で、↓このように書くこともできます。

block = WikiFormeMethod.block
inline = WikiFormeMethod.inline

block["section"].module_elval {
  def process
    "<section title=\"#{@text}\">#{@children.process}</section>"
  end
}


この書き方の場合は、簡略化した書き方では使えない変数を使うことができます。

  • ブロック要素の場合
    • @text
    • @children
    • @parent
    • @syntax
    • @parser["block"]
    • @parser["make_inline"]
    • @parser["tree"]
  • インライン要素の場合
    • @text
    • @parser
    • @syntax
    • @parser["inline"]
    • @parser["make_inline"]


このなかで、@syntaxが重要です。先に紹介した記法のカスタマイズファイルの書き方で、「要素名 <半角スペース> 引数: マーク」と書かれたものを参照できます。「tr_split splitter: ","」と書かれていれば、

block["tr_split"].module_eval {
  def process
    p @syntax["splitter"]    #=> ","
  end
}

となります。


特殊なブロック要素

multiline

structure.yamltype: multilineと指定すると、multiline要素になります。multiline要素は、テキストを複数取ることができます。(通常は行頭マークから行末までの1行)
multiline要素では、lines変数を使うことができます。

block = WikiFormeMethod.block
inline = WikiFormeMethod.inline

block["pre"].process {|text,children,lines|
  "<pre>#{lines.join("\n")}</pre>"
}
action

structure.yamltype: actionと指定すると、action要素になります。action要素は、「包含可能/不可能」に関わらず動作し、構文スタックに直接働きかけます。
たぶん特殊な用途にしか使いません。他のファイルを読み込む「include」要素は、action要素として実装しています。

block["include"].action {|text,stack,syntax,parser|
  savedir = Dir.pwd
  Dir.chdir( File.dirname(text) )
  begin
    File.open( File.basename(text) ) {|file|
      parser["block"].parse_continue(file, stack)
    }
  ensure
    Dir.chdir(savedir)
  end
}

バグ

  • 変換プログラムが中途半端にしか実装されていない
  • ドキュメントが揃っていない。ソースコードを読まないと分からない
  • ソースコードはリフレクションの嵐
  • エラーメッセージが分かりにくい
  • テストが十分にされていない

というわけで皆さん、いじり倒しましょう!