はじめに
JavaScriptには文字列置換の方法として replace()
がありますが、これは正規表現に「マッチした部分」しか置換できません。では以下の場合どうすればいいでしょうか?
文字列をHTMLエスケープせよ。ただし文字列中の文字参照はエスケープしてはならない。
ここで文字参照とは
- 文字実体参照 (
©
など) - 数値文字参照 (
♪
、♪
など)
を指し、HTMLエスケープは escapeHTML()
で行えるものとします。
例えば、
"©<♪♪>"
は
"©<♪♪>"
に置換されます。
マッチする部分の正規表現
文字参照の正規表現は、/&\w+;|&#\d+;|&#x[0-9a-fA-F]+;/
と表せますので、マッチする部分を置換するのであれば簡単です。例えば文字参照をそれが指す実際の文字に置き換えるのなら、
const str = "©<♪♪>";
str.replace(/&\w+;|&#\d+;|&#x[0-9a-fA-F]+;/, toChar)
のようにすればよいでしょう。(ただし文字参照を実際の文字に変換する関数toChar()
は別途要定義)
マッチしない部分の正規表現
マッチしない部分の正規表現が書ければ問題は解決ですが、ネットで探しても例がなく、なかなか難しそうです。マッチしない部分 + マッチした部分 の正規表現であれば、
/(?:(?!${pattern}).)*(?:${pattern})?/g
と書けますので、これを利用することにします(上記はPerl風に正規表現に変数展開していますが、javascriptではこれはできないので一工夫必要です)。実際のケースでは「マッチする部分とマッチしない部分をそれぞれ置換する」という局面が多いのですが、それにも対応できそうです。
マッチしない部分 + マッチした部分 が取得できれば、次は
/^(.*?)(${pattern})?$/
で、マッチしない部分 と マッチした部分 に分けることができます。
JavaScriptでの実装
これを実際にJavaScriptで実装すると以下のようになるでしょう。
const pattern = "&\\w+;|&#\\d+;|&#x[0-9a-fA-F]+;";
const regexp1 = new RegExp(`(?:(?!${pattern}).)*(?:${pattern})?`,'gi');
const regexp2 = new RegExp(`^(.*?)(${pattern})?$`,'i');
function replace(str = '') {
let rv = '';
for (let chunk of str.match(regexp1)) {
let [ , u, m ] = chunk.match(regexp2);
if (u) rv += unmatch ? unmatch(u) : u;
if (m) rv += match ? match(m) : m;
}
return rv;
}
pattern
は文字列リテラルですので、\
が2つ必要なことに注意してください。
match()
はマッチしたとき、unmatch()
はマッチなかったときの置換関数です。
モジュール化
この問題は度々発生するのでモジュール化しておきましょう。
module.exports = function(pattern, match, unmatch, opt = '') {
const regexp1 = new RegExp(`(?:(?!${pattern}).)*(?:${pattern})?`,'g'+opt);
const regexp2 = new RegExp(`^(.*?)(${pattern})?$`,opt);
return function(str = '') {
let rv = '';
for (let chunk of str.match(regexp1)) {
let [ , u, m ] = chunk.match(regexp2);
if (u) rv += unmatch ? unmatch(u) : u;
if (m) rv += match ? match(m) : m;
}
return rv;
}
}
元々の問題は、
const str = "©<♪♪>";
const replace
= require('./replacer.js')("&\\w+;|&#\\d+;|&#x[0-9a-fA-F]+;",
null, escapeHTML, 'i');
replace(str);
で解くことができます。