LoginSignup
2
1

More than 3 years have passed since last update.

JavaScriptで正規表現にマッチしない部分を置換する

Last updated at Posted at 2020-08-01

はじめに

JavaScriptには文字列置換の方法として replace() がありますが、これは正規表現に「マッチした部分」しか置換できません。では以下の場合どうすればいいでしょうか?

文字列をHTMLエスケープせよ。ただし文字列中の文字参照はエスケープしてはならない。

ここで文字参照とは

  • 文字実体参照 (© など)
  • 数値文字参照 (♪♪など)

を指し、HTMLエスケープは escapeHTML() で行えるものとします。

例えば、

"&copy;<&#9834;&#x266A;>"

&quot;&copy;&lt;&#9834;&#x266A;&gt;&quot;

に置換されます。

マッチする部分の正規表現

文字参照の正規表現は、/&\w+;|&#\d+;|&#x[0-9a-fA-F]+;/ と表せますので、マッチする部分を置換するのであれば簡単です。例えば文字参照をそれが指す実際の文字に置き換えるのなら、

    const str = "&copy;<&#9834;&#x266A;>";
    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() はマッチなかったときの置換関数です。

モジュール化

この問題は度々発生するのでモジュール化しておきましょう。

replacer.js
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 = "&copy;<&#9834;&#x266A;>";
    const replace
            = require('./replacer.js')("&\\w+;|&#\\d+;|&#x[0-9a-fA-F]+;",
                                       null, escapeHTML, 'i');
    replace(str);

で解くことができます。

2
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1