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に変換することが目的とされていたので、「
*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: ">"
preとpre 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ディレクトリ)が、記法定義の単位になります。このディレクトリにxhtmlやSmartDocなど、変換先のフォーマットごとにディレクトリを作って、その中に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
変換フォーマットの名前(xhtml、SmartDocなど)は新たに作っても構いませんが、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
BLANKとTEXTは特殊な要素です。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.yamlでtype: 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.yamlでtype: 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 }