世の中にはシンタックスハイライトを行うツールが既に多数存在しています。例えば以下のようなものがあります:
メジャーな言語やフォーマットなら標準でシンタックスハイライトの設定が同梱されていますが、
ニッチな言語やフォーマットだとまずそのような設定は存在しません。
それならば独自に設定を書けばいいのですが、
大抵のツールでは構文の定義方法が特定のパーツに該当する正規表現を並べるだけなので、
言語によっては構文の妥当な記述が不可能な場合もあります。
となると独自に実装せざるを得ません。
例えば
Vim の :help ドキュメントを良い感じに Web ブラウザ上で見るためのツール
を作って Heroku で動かそうと思った場合、
Vim の :help ドキュメントの構文のいい加減さ
から、まず既存のツールを利用してのシンタックスハイライトはできません。
さらにこのツールの場合はリンク周りもあれこれ面倒を見る必要があるため、
ますます既存のツールの利用はできません。
という訳で元のソースをパースしていい感じにシンタックスハイライトする仕組みを作る必要があります。
ことパースに関して言えば Ruby には様々な
gem が存在しているので、そのどれかを使うことになります。
パース関連で言うと以下のような gem があるのですが:
という感じなので parslet 一択という状態です。
そういう訳で parslet を使って Ruby でシンタックスハイライトを実装してみましょう。
require 'parslet'
class VimHelpParser < Parslet::Parser
# TODO: ここにパースのルールをいろいろ書こう!
end
rule
# 「指定した文字列が来る」形は str を使う。
rule(:rule_name) {
# TODO: ここに入力規則を書こう!
}
str
rule(:note) {
str('NOTE')
}
match
rule(:space) {
match('[ \t]')
}
match('[a-z]+')
などとは書けない。match('[a-z]').repeat(1)
と書く。>>
rule(:vimscript_link) {
str('vimscript#') >>
match('[0-9]')
}
repeat
rule(:vimscript_link) {
str('vimscript#') >>
match('[0-9]').repeat(1)
}
match('[0-9]').repeat(1, 3)
のように最大繰り返し回数も指定可能。as
rule(: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('.')
)
}
any
rule(:special_key) {
str('CTRL-') >>
(
str('{char}') |
match('[A-Za-z0-9]').repeat(1) |
any
)
}
match('.')
でも同じ意味ですが、 any
の方が読み易いです。maybe
rule(: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? >> any
any.abscent?
root
root(: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 は非常によくできているので、ちょろっと何かをパースする必要に迫られたときでも、上記のポイントを押さえていれば何とかなるでしょう。