【jQuery】構造を保ったままテキストを置換する

jQuery で、マークアップされている部分を含むテキストを置換してみたいと思ったら、この記事を読むといいと思うよ。

## 一般的な手法とその限界

jQuery を用いて、JavaScript からページの文章の一部を置換したいと思ったとします。

このとき、普通にググると、大抵「jQuery オブジェクトの`text()`メソッドで得た文字列に対して`replace(substr, newSubstr)`メソッドを呼んで、返却された文字列を元の jQuery オブジェクトの`text(val)`メソッドに渡してやればいい」という解決策を提案されるでしょう。

```javascript
function replaceText($obj, substr, newSubstr) {
  $obj.text($obj.text().replace(substr, newSubstr));
}
```

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="js,result" data-user="BlueRayi" data-slug-hash="gObRgZj" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="jQueryテキスト置換(通常)">
  <span>See the Pen <a href="https://codepen.io/BlueRayi/pen/gObRgZj">
  jQueryテキスト置換(通常)</a> by Aᴏɪ Hikari (<a href="https://codepen.io/BlueRayi">@BlueRayi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

この方法は確かに大抵の場合うまく行きます。 しかし、もし置換したい DOM が子要素を含んでいる、つまり内部のテキストがさらにマークアップされているとどうなるでしょう。

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="js,result" data-user="BlueRayi" data-slug-hash="yLyXgdN" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="jQueryテキスト置換(通常: 失敗)">
  <span>See the Pen <a href="https://codepen.io/BlueRayi/pen/yLyXgdN">
  jQueryテキスト置換(通常: 失敗)</a> by Aᴏɪ Hikari (<a href="https://codepen.io/BlueRayi">@BlueRayi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

見ての通り、マークアップの効果が完全に消えてしまいます。 これは`text()`メソッドおよび`text(val)`メソッドの特性によるもので、`text()`メソッドは子要素のテキスト部のみを単に結合して返し、`text(val)`メソッドは既存の構造を完全に無視して単なるテキストノードとして再配置するためです。

このような場合、どうすれば目標を達成することができるでしょうか。

## 構造を保ったまま置換する

### 理論

まず、対象の jQuery オブジェクトを単なるテキストノードと子の DOM 要素に分解します。 以下に記述する`splitChildren($obj)`関数は引数として与えられた jQuery オブジェクト(長さ 1 を想定しています)の子ノードの jQuery オブジェクトの配列を返却します。

```javascript
function splitChildren($obj) {
  let ret = [];
  let children = $obj[0].childNodes;
  for (let child of children) {
    ret.push($(child));
  }
  return ret;
}
```

そして、そのうちのテキストノードの jQuery オブジェクトのみを置換していきます。 このとき、テキストノードの jQuery オブジェクトは`text()`および`text(val)`メソッドで期待する動作を得られませんので、直接、内部のノードの`nodeValue`属性を書き換えています。

```javascript
function replaceText($obj, substr, newSubstr, isRecursive) {
  var isRecursive = (isRecursive !== undefined) ? isRecursive : false;
  $obj.each(function() {
    let $children = splitChildren($(this));
    for (let $child of $children) {
      if ($child.prop('tagName')) {
        if (isRecursive) {
          replaceText($child, substr, newSubstr, true);
        } else {
          /* NOOP */
        }
      } else {
        $child[0].nodeValue = $child[0].nodeValue.replace(substr, newSubstr);
      }
    }
  });
}
```

### 実践

実際に実行してみます。

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="js,result" data-user="BlueRayi" data-slug-hash="ZEYyexK" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="jQueryテキスト置換(構造維持)">
  <span>See the Pen <a href="https://codepen.io/BlueRayi/pen/ZEYyexK">
  jQueryテキスト置換(構造維持)</a> by Aᴏɪ Hikari (<a href="https://codepen.io/BlueRayi">@BlueRayi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

このようにマークアップされた子要素をそのままにしてテキストが置換できています。

第 4 引数である`isRecursive`に Truthy な値を与えると、テキストノードでない jQuery オブジェクトに対して再帰的に呼び出されるため、子要素を含めて置換できます。

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="js,result" data-user="BlueRayi" data-slug-hash="povweKN" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="jQueryテキスト置換(構造維持: 再帰的)">
  <span>See the Pen <a href="https://codepen.io/BlueRayi/pen/povweKN">
  jQueryテキスト置換(構造維持: 再帰的)</a> by Aᴏɪ Hikari (<a href="https://codepen.io/BlueRayi">@BlueRayi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>