世の中にはシンタックスハイライトを行うツールが既に多数存在しています。例えば以下のようなものがあります:
メジャーな言語やフォーマットなら標準でシンタックスハイライトの設定が同梱されていますが、
ニッチな言語やフォーマットだとまずそのような設定は存在しません。
それならば独自に設定を書けばいいのですが、
大抵のツールでは構文の定義方法が特定のパーツに該当する正規表現を並べるだけなので、
言語によっては構文の妥当な記述が不可能な場合もあります。
となると独自に実装せざるを得ません。
例えば
Vim の :help ドキュメントを良い感じに Web ブラウザ上で見るためのツール
を作って Heroku で動かそうと思った場合、
Vim の :help ドキュメントの構文のいい加減さ
から、まず既存のツールを利用してのシンタックスハイライトはできません。
さらにこのツールの場合はリンク周りもあれこれ面倒を見る必要があるため、
ますます既存のツールの利用はできません。
という訳で元のソースをパースしていい感じにシンタックスハイライトする仕組みを作る必要があります。
ことパースに関して言えば Ruby には様々な
gem が存在しているので、そのどれかを使うことになります。
パース関連で言うと以下のような gem があるのですが:
という感じなので parslet 一択という状態です。
そういう訳で parslet を使って Ruby でシンタックスハイライトを実装してみましょう。
require 'parslet'
class VimHelpParser < Parslet::Parser
# TODO: ここにパースのルールをいろいろ書こう!
endrule# 「指定した文字列が来る」形は str を使う。
rule(:rule_name) {
# TODO: ここに入力規則を書こう!
}strrule(:note) {
str('NOTE')
}matchrule(:space) {
match('[ \t]')
}match('[a-z]+') などとは書けない。match('[a-z]').repeat(1) と書く。>>rule(:vimscript_link) {
str('vimscript#') >>
match('[0-9]')
}repeatrule(:vimscript_link) {
str('vimscript#') >>
match('[0-9]').repeat(1)
}match('[0-9]').repeat(1, 3) のように最大繰り返し回数も指定可能。asrule(:vimscript_link) {
(
str('vimscript#') >>
match('[0-9]').repeat(1).as(:id)
).as(:vimscript_link)
}|rule(:special_key) {
str('CTRL-') >>
(
str('{char}') |
match('[A-Za-z0-9]').repeat(1) |
match('.')
)
}anyrule(:special_key) {
str('CTRL-') >>
(
str('{char}') |
match('[A-Za-z0-9]').repeat(1) |
any
)
}match('.') でも同じ意味ですが、 any の方が読み易いです。mayberule(:spaces?) {
match('[ \t]').repeat(1).maybe
}repeat(0, 1) でもほぼ同じ効果が得られますが、 maybe の方が読み易いです。repeat(0, 1) と maybe だとパース結果の表現が異なります。maybe の方が扱い易いパース結果になるので、敢えて repeat(0, 1) を使うことはないと思います。rule(:spaces) {
match('[ \t]').repeat(1)
}
rule(:spaces?) {
spaces.maybe
}present? / absent?rule(:tag_anchor) {
star.as(:begin) >>
((space | newline | star | pipe).absent? >> any).
repeat1.
as(:tag_anchor) >>
star.as(:end) >>
((space | newline).present? | any.absent?)
}a.abscent? >> anyany.abscent?rootroot(:help)
rule(:help) {
token.repeat
}
rule(:token) {
header |
option |
tag_anchor |
...
}VimHelpParser.
new().
parse("*arpeggio.txt* Vim plugin for ...")class VimHelpParser < Parslet::Parser
rule(:vimscript_link) {
(
str('vimscript#') >>
match('[0-9]').repeat(1).as(:id)
).as(:vimscript_link)
}
end
VimHelpParser.new().vimscript_link.parse("vimscript#2100")
#==> {:vimscript_link => {:id => '2100'}}str や match の結果はマッチした文字列(に入力元での位置情報が付加されたオブジェクト)になります。as を使うと結果は Hash になります。キーが as で指定したオブジェクト(普通はシンボル)で、対応する値がパース結果になります。repeat を使うと個々のパース結果を要素に持つ Array になります(repeat されたのがただの str や match の場合は Array ではなくマッチした文字列になります)。maybe を使うと、該当する入力があった場合はそのパース結果がそのまま maybe のパース結果になります。該当する入力がなかった場合は nil が maybe のパース結果になります。class VimHelpTransformer < Parslet::Transform
rule(:vimscript_link => {:id => simple(:id)}) {
base_uri = 'http://www.vim.org/scripts/script.php'
%Q[<a class="vimscript_link" href="#{base_uri}?script_id=#{id.to_s}">vimscript##{id.to_s}</a>]
}
end
VimHelpTransformer.new().apply({:vimscript_link => {:id => '2100'}})
# ==> %Q[<a class="vimscript_link" href="http://www.vim.org/scripts/script.php?script_id=2100">vimscript#2100</a>]rule(個々のパース結果を表すオブジェクト) {変換結果を導出する式} を書きます。rule のブロックが実行されます。simple(:id) は Array でも Hash でもないオブジェクトにマッチします。マッチしたオブジェクトはローカル変数 id に束縛されます。apply が受け取ったパース結果(普通はネストした Hash)の内容を適宜 rule に従って変換してくれます。一旦パーサーができてしまえば、
あとは Parslet::Transform を使って各種構文を span 要素で括って適切な class 属性を付けた HTML のスニペットへ変換してやり、
それっぽい CSS を用意してあげればシンタックスハイライトのできあがりという訳です。
やりましたね。
parslet は非常によくできているので、ちょろっと何かをパースする必要に迫られたときでも、上記のポイントを押さえていれば何とかなるでしょう。