ラック
Home > ブログ > 記事 > 2018年8月 > JavaScriptの正規表現を使った文字列置換でマッチした文字列ごとに置換を行う

JavaScriptの正規表現を使った文字列置換でマッチした文字列ごとに置換を行う

カテゴリ: プログラム

ejsで色々やっているうちに、「記事の本文に該当するHTMLのうち、条件に合致するaタグやimgタグのパスを書き換えたい」ということを実装したくなりました。

それを実装するためにやや苦労したので、おそらく時間が経過すると忘れて分からなくなってしまいそうな気がしたので、記憶が確かなうちにきちんとした形で残しておきたいと思い、記事にした次第です。

前提

上記の通り、今回はブラウザで動かすJSではなくejsのコンパイル処理の中の話なので、それぞれのバージョンを。

  • node.js: v8.11.3
  • gulp: 3.9.1
  • gulp-ejs: 3.1.3

経緯

基本的には上記の通りですが、全てのaやimgタグではなく、上記の通り条件に合致したものに限定としたいです。その条件とは、「ローカルのリソースを参照するもののみ」、つまり外部サイトへのリンクやCDNではなく、自サイト内の他のページや画像を参照しているものだけということです。

もっと言うと、ejsのテンプレートと、展開されるhtmlファイルの階層が異なるため、自動で相対パスをhtml、つまり実際のサイトのパスに変換させたいということです。これができなければ、パスがずれてしまうためリソースが参照できなくなってしまうので、何とかして実装したいという状況でした。

実装のための整理

上記の条件を整理すると

  • マッチする条件:
    • src属性、またはhref属性のうち
    • 各属性の値が相対パス表記→絶対パス、http://https:////から始まっていないもの
    • 本文記事に該当する文字列全体を検索
  • マッチした場合の動作:
    • src属性、またはhref属性の値の先頭に../../など、相対パスの階層を付け足す

といった形になりますね。

実装1(失敗)

今回は自分が厄介だと感じる点があり、案の定そこで躓いたので苦労しました。その点というのは、

  • 「絶対パスではない」という条件でマッチするかどうかを判定するため、正規表現を用いる
  • 条件にマッチした場合の置換する文字列が、srchrefでその都度文字数が異なるため、「マッチする文字列のうち先頭何文字目で前後に切って……」といったような文字数による固定の処理は難しい

前者については問題はなく、replaceメソッドの第一引数に正規表現を渡せば良いでしょう。

問題は後者。前者の通り正規表現を使うとして、後者をどうやって実装するか。

方法としては、「後方参照」を用いることが考えられます。後方参照とは、正規表現にマッチしたパターンの一部を、処理の中で利用するための仕組みです。

JavaScriptの場合、RegExpオブジェクトの後ろに.$1などと付けることで、マッチした文字列を利用することができます。ただし、数字の部分は正規表現のパターンのうち、括弧で括られた部分の順番になります。例えば、

var reg = new RegExp("(\d{3})-(\d{4})");

とした場合、(\d{3})にマッチした部分がRegExp.$1に格納され、、(\d{4})にマッチした部分がRegExp.$2に格納される、と。

ただし、"g"オプションが付くと話がやや変わって(最初の1個だけではなく、文字列の最後まで検索するため)、RegExp.$1には最後にマッチしたパターンが保持されてしまうようです。今回の場合、文字列の途中でsrcだったりhrefだったりとバラバラなので、最後にマッチしたパターンだけで全部置換されてしまうと、imgタグなのにhref属性になってしまったり、逆にaタグでsrc属性になってしまったりするかもしれない、ということに。それはまずい。

そこで、replaceメソッドの第二引数を無名関数でコールバックする、という手法があるようです(末尾「参考」の「コールバック」を参照)。

前置きが長くなりましたが、まずはこの手法を使って実装してみました。

<%
//記事中のimg srcパスやa hrefパスに相対パスを追加
//引数:
//    content: 文字列(記事本文)
//    relPath: 先頭に付与したい相対パス
//戻り値: srcやhrefに階層の相対パスを付与、置換したcontent
funcURLConverter = (content, relPath) => {
    return content.replace(/(src=\"|href=\")(?!(http(s)?:)?\/\/)/gim, function() {
        return `${RegExp.$1}${relPath}`
    });
}
%>

ただし、この方法だと上記の通りマッチした最後のパターンで置換する問題を回避できると思ったのですが、変換した記事本文は問題を回避できていませんでした。

最終的な実装

さらに調査すると、コールバック関数の中ではargumentsという配列が使えるという情報を手に入れました。特にarguments[0]は、当初意図していた「都度マッチした文字列」を保持しているということで、上記を書き換えました。

<%
//記事中のimg srcパスやa hrefパスに相対パスを追加
//引数:
//    content: 文字列(記事本文)
//    relPath: 先頭に付与したい相対パス
funcURLConverter = (content, relPath) => {
    return content.replace(/(src=\"|href=\")(?!(http(s)?:)?\/\/)/gim, function() {
        return `${arguments[0]}${relPath}`
    });
}
%>

変更したのはコールバック関数の中身だけです。これで無事に意図していた動作を実装することができました。

……分かってしまえばほんの少しの差なのですが、そこに到達できずに色々試して、while ((array = regexp.exec(content)) != null) {}のループでごにょごにょしてみたり……と紆余曲折を経たので、備忘録のために筆を執った次第でした。

参考

replaceメソッド

後方参照

コールバック

arguments

タグ: javascript,gulp,ejs,KiribiUsusama,Node.js

 



関連する記事一覧