2017-03-29

tobijibu

VimでHTMLエスケープ用の処理を作る

ブログを書いていて、HTMLタグを書く場合等で、ちょこちょことHTMLエスケープが必要になる場合があります。 今までは、ほとんど使う機会が無かったですし、 必要になったら文字置換したり、HTMLエスケープを提供しているサイトを探して変換していました。

ブログの更新頻度が増えたこともあり、HTMLエスケープが必要な回数が増えてきました。 そこで、少しでも楽になるようにVimのコマンドとして、HTMLエスケープ処理を作成してみました。

&"'<>を16進数表記に変換します。

let s:replaceEntity = [
\  ['\(\(&\(#*\w\+;\)\@!\)\|&amp;\|&#38;\)', '\&#x26;'],
\  ['\(\"\|&quot;\|&#34;\)',                 '\&#x22;'],
\  ["\\('\\|&#39;\\)",                       '\&#x27;'],
\  ['\(<\|&lt;\|&#60;\)',                    '\&#x3C;'],
\  ['\(>\|&gt;\|&#62;\)',                    '\&#x3E;'],
\]

function! s:htmlesc() range
  for l:line in range(a:firstline, a:lastline)
    let l:_line = getline(l:line)
    for l:replace in s:replaceEntity
      let l:_line = substitute(l:_line, l:replace[0], l:replace[1], 'g')
    endfor
    call setline(l:line, l:_line)
  endfor
endfunction

command! -range HtmlEsc <line1>, <line2> call s:htmlesc()

使い方は、ソースをそのまま.vimrcに貼り付けるか、 ソースをファイルに保存して、.vimrcsource htmlesc.vim等というようにファイルを読み込みます。sourceで読み込むのではなく、runtimepathで指定されたディレクトリ内に入れる方法もあります。

置換したい行を選択して、:HtmlEscコマンドを打つと対象文字が置換されます。置換は1行ごとに処理しているので、"特定範囲だけ"という処理には対応していません。

対象文字の正規表現

このリストで置換対象文字と、置換後の文字を設定しています。&(アンパサンド)'(シングルクォーテーション)は少し特殊な指定をしています。

let s:replaceEntity = [
\  ['\(\(&\(#*\w\+;\)\@!\)\|&amp;\|&#38;\)', '\&#x26;'],
\  ['\(\"\|&quot;\|&#34;\)',                 '\&#x22;'],
\  ["\\('\\|&#39;\\)",                       '\&#x27;'],
\  ['\(<\|&lt;\|&#60;\)',                    '\&#x3C;'],
\  ['\(>\|&gt;\|&#62;\)',                    '\&#x3E;'],
\]

&は、置換後の文字にも含まれていますので、何度も繰り返し実行すると、文字がどんどん増えてしまいます。

そこで、&\(#*\w\+;\)\@!を指定し、&が1文字だけ独立している場合のみ、変換対象としています。

上記の(~)\@!と指定している箇所がポイントで、"否定先読み"と呼ばれる指定手法です。 この指定によって"&の後に、#*\w\+;が無い場合のみマッチする"ということにります。

具体的には、&nbsp;や、&#34&#x22といった文字が"マッチしない"ことになります。


'(シングルクォーテーション)に関しては、正規表現を'(シングルクォーテーション)で囲っていません。 '(シングルクォーテーション)で囲ってしまうと、''のようにシングルクォーテーション自体をシングルクォーテーションでエスケープしなければならず、正常に置換することができません。

'(シングルクォーテーション)の場合は正規表現を"(ダブルクォーテーション)で囲み、それに応じて各記号を\(バックスラッシュ)でエスケープしてやります。

置換処理

先ほどの正規表現を使って、文字を置換していきます。

function! s:htmlesc() range
  for l:line in range(a:firstline, a:lastline)
    let l:_line = getline(l:line)
    for l:replace in s:replaceEntity
      let l:_line = substitute(l:_line, l:replace[0], l:replace[1], 'g')
    endfor
    call setline(l:line, l:_line)
  endfor
endfunction

functionの右にある!は"関数の再定義"の指定です。 この指定が無いと、スクリプトを再読込した場合にエラーが発生してしまいます。

関数にrangeを指定すると、行範囲を取得することができます。 そして、関数内で行範囲を示すa:firstlinea:lastlineが使えるようになります。

s:replaceEntityに格納された正規表現と置換後の文字をsubstitute()に指定して1つずつ文字を置換します。



ちなみに、Vimならば:TOhtmlというコマンドもあります。:TOhtmlはVimに表示されている内容を、別ファイルとしてHTML化することができます。

改行や色も含めて出来る限りVimの見た目に近い形で出力してくれます。もちろん必要に応じてHTMLエスケープも対応しています。

範囲選択もできますので、ある程度の範囲をまとめてエスケープしつつ、綺麗に表示したい場合には便利かもしれないですね。

:TOhtml使った表示

 1 " 変換対象文字指定
 2 let s:replaceEntity = [
 3 \  ['\(\(&\(#*\w\+;\)\@!\)\|&amp;\|&#38;\)', '\&#x26;'],
 4 \  ['\(\"\|&quot;\|&#34;\)', '\&#x22;'],
 5 \  ["\\('\\|&#39;\\)",       '\&#x27;'],
 6 \  ['\(<\|&lt;\|&#60;\)',    '\&#x3C;'],
 7 \  ['\(>\|&gt;\|&#62;\)',    '\&#x3E;'],
 8 \]
 9 
10 function! s:htmlesc() range
11   for l:line in range(a:firstline, a:lastline)
12     let l:_line = getline(l:line)
13     for l:replace in s:replaceEntity
14       let l:_line = substitute(l:_line, l:replace[0], l:replace[1], 'g')
15     endfor
16     call setline(l:line, l:_line)
17   endfor
18 endfunction
19 
20 command! -range HtmlEsc <line1>, <line2> call s:htmlesc()

今回説明で利用したソースはこちらにあります。

参考サイト

Qiita - vimに自作コマンドを実装する
http://qiita.com/shimbaroid/items/f2ad60c203ccdff7da16

vim-users.jp - Hack #158: ユーザコマンドを定義する
http://vim-jp.org/vim-users-jp/2010/06/29/Hack-158.html

IBM developerWorks® - Vim エディターのスクリプトの作成: 第 2 回 ユーザー定義関数
https://www.ibm.com/developerworks/jp/linux/library/l-vim-script-2/