XML (HTML) の特殊文字をエスケープするVimスクリプトを書いた
- 2009-09-29
- カテゴリ: Client Side
- タグ: Vim Tips
XMLやHTMLの5つの特殊文字 (&, <, >, ', ") を定義済みのエンティティ (&, <, >, ', ") に変換するVimスクリプトを書いてみた。
5つの文字を置換するだけだから簡単そうに思えるけど、選択範囲だけを対象にしようとするとなかなか難しくて、今まで納得のいくものを作れずにいた。それが、たまたま今日挑戦してみたら、そこそこいい感じのスクリプトが書けたので、記念にブログポストしてみる。
vnoremap <Leader>e "xx:call <SID>EscapeXml('x')<CR>"xP
function s:EscapeXml(regname)
let x = getreg(a:regname)
let x = substitute(x, '&', '\&', 'g')
let x = substitute(x, '<', '\<', 'g')
let x = substitute(x, '>', '\>', 'g')
let x = substitute(x, "'", '\'', 'g')
let x = substitute(x, '"', '\"', 'g')
call setreg(a:regname, x)
endfunction
使い方やコードの解説は続きで。
使い方
前述のコードをvimrcもしくはFiletype Pluginの中に貼り付けます。
Filetype Pluginに貼り付ける場合は、マッピングの設定を
vnoremap <buffer> <LocalLeader>e "xx:call <SID>EscapeXml('x')<CR>"xP
にした方がいいでしょう。
VimでXMLファイルを開いたら、特殊文字をエスケープしたい部分を選択し、

\e
を入力します (マップリーダーがデフォルトのバックスラッシュの場合)。

選択箇所が複数行にまたがった場合や、行選択モード (<S-V>) で選択した場合でも問題なく変換することができます。ただし、矩形選択モード (<C-V>) で選択した場合は正しく動作しません。
コード解説
"xx:call <SID>EscapeXml('x')<CR>"xP
選択領域を削除し、内容を x レジスタに保存する (
"xx)-
利用するレジスタの名前 ('x') を引数として、s:EscapeXml()関数を呼び出す (
:call <SID>EscapeXml('x')<CR>)s:EscapeXml()関数では、引数で指定されたレジスタから値を取得し (
let x = getreg(a:regname))、substitute()関数でXML特殊文字をすべて置換し (let x = substitute(x, ...) た後に、置換結果をレジスタに戻す (call setreg(a:regname, x))。 - s:EscapeXml()関数で書き換えた x レジスタの内容を、改めてバッファにペーストする (
"xP)
試行錯誤の跡
↑のスクリプトを作るときに、僕がはじめに書いた関数が↓だった。
function EscapeXml() s/&/\&/eg s/</\</eg s/>/\>/eg s/'/\'/eg s/"/\"/eg endfunction
この関数では望む動作をしないことは明らかで、↓のように選択して関数を呼び出しても、

選択範囲を含む行全てが対象になって↓のような結果になってしまう。

ということで、次に書いたのが↓のような関数。
function EscapeXml() s/\%V&/\&/eg s/\%V</\</eg s/\%V>/\>/eg s/\%V'/\'/eg s/\%V"/\"/eg endfunction
選択したテキストにマッチするアトム (\%V) を使ってみたもの。
これでうまく行くかと思っていたが、同様に↓のように選択して関数を呼び出してみたら、

↓のような結果になってしまった。理由は、置換によって選択されたテキストが長くなっても、それに伴って選択範囲が拡張されないため。

最終的には、xmleditというプラグインで使われていたアプローチを採用して、選択範囲を削除して内容をレジスタに送ってみることにした。で、書きあがったのが↓。
vnoremap <buffer> <LocalLeader>e "xx:call <SID>EscapeXml()<CR>"xP function s:EscapeXml() let @x = substitute(@x, '&', '\&', 'g') let @x = substitute(@x, '<', '\<', 'g') let @x = substitute(@x, '>', '\>', 'g') let @x = substitute(@x, "'", '\'', 'g') let @x = substitute(@x, '"', '\"', 'g') endfunction
↑を元に、利用するレジスタ名を引数で指定できるように改良したのが、冒頭で紹介した完成版のスクリプト。
本当は、関数単独で使えるような形にしたかったんだけど、結局マッピングと組み合わせないと目的の機能は実現できなかった。もう少し改良の余地があるかも。
トラックバックURL
- http://liosk.blog103.fc2.com/tb.php/187-f3187f34

