ピュアJS: HTMLの該当文字列をmarkタグで囲う方法

外部ライブラリを使わず生JSだけでHTML内の特定の文字列にmarkタグで囲う(マーカーを引く)方法です。

目次

マーカーを引くための関数を自作

次の2つの関数を自作します。 検索文字列にマーカーを引くmarkText()とマーカーを除去するunmarkText()です。

  • markText(node, word)

    • 第一引数に親要素を、第二引数には検索文字列を指定します。この関数は、検索文字列にヒットしたTextNodeの箇所をmarkタグで囲います。
  • unmarkText(node)

    • 第一引数に親要素を指定します。この関数は、指定された要素の子要素のmarkタグを全て外します。

それぞれの関数の実際のコードは次のサンプルコードで示しています。

サンプルコード

<style type="text/css">
/* マーカーの色変更はお好みで。 */
mark {
  background: yellow;
}
</style>
...
<input type="text" id="ipt-word" value="マーク">
<button id="btn-markText">テキストにマーカー</button>
<div>
  <div class="item">マークあいうえお</div>
  <div class="item">かきくけこ</div>
  <div class="item">さしすマークせそ</div>
  <div class="item">なにぬねのマーク</div>
  <div class="item"><code>mark</code>タグで囲います。</div>
  <div class="item"><b>注意:</b> 上記メッセージは「mark」というワードではマーカーを引けますが、「markタグ」というワードでは引けません。これは「<code>mark</code>タグで囲います。」の実体が「&lt;code&gt;mark&lt;/code&gt;」(code要素)と「タグで囲います。」(テキストノード要素)という2つの要素で構成されているからです。</div>
</div>
<script type="text/javascript">
document.querySelector("#btn-markText").onclick = function() {
  // 一旦全てのマーカーを消す:
  unmarkText(document.querySelector("body"));
  const word = document.querySelector("#ipt-word").value;
  // マーカーを引く:
  const items = document.querySelectorAll("body [class=item]");
  for (let len = items.length, i = 0; i < len; i++) {
    const item = items[i];
    markText(item, word);
  }
};

/**
 * マッチテキストにマーカーを引く(markタグを付与)
 * @param {HTMLElement} node - 親要素
 * @param {String} word - 検索文字列
 *  空文字列をセットするとマーカーが除去されます。
 */
function markText(node, word) {
  if (node instanceof HTMLElement !== true) {
    return console.warn("markText: node must be an HTMLElement");
  }
  if (!word || typeof word !== "string") return;
  _markText(node, new RegExp(word, "g"), function(node, match, offset) {
    const mark = document.createElement("mark");
    // mark.className = "search-term";
    mark.textContent = match;
    return mark;
  });

  // @see: https://stackoverflow.com/a/29301739
  function _markText(node, regex, callback, excludeElements) {
    excludeElements || (excludeElements = ['script', 'style', 'iframe', 'canvas']);
    let child = node.firstChild;
    while (child) {
      switch (child.nodeType) {
        case 1:
          {
            if (excludeElements.indexOf(child.tagName.toLowerCase()) > -1)
              break;
            _markText(child, regex, callback, excludeElements);
            break;
          }
        case 3:
          {
            let bk = 0;
            child.data.replace(regex, function(all) {
              let args = [].slice.call(arguments),
                offset = args[args.length - 2],
                newTextNode = child.splitText(offset + bk),
                tag;
              bk -= child.data.length + all.length;
              newTextNode.data = newTextNode.data.substr(all.length);
              tag = callback.apply(window, [child].concat(args));
              child.parentNode.insertBefore(tag, newTextNode);
              child = newTextNode;
            });
            regex.lastIndex = 0;
            break;
          }
      }
      child = child.nextSibling;
    }
    return node;
  }
}
/**
 * 指定された要素からマーカーを除去(markタグを外す)
 * @param {HTMLElement} node - ノード
 */
function unmarkText(node) {
  if (node instanceof HTMLElement !== true) {
    console.warn("unmarkText: node must be an HTMLElement.");
    return;
  }
  const items = node.querySelectorAll("mark");
  for (let len = items.length, i = 0; i < len; i++) {
    const node = items[i];
    if (node.nodeName === "MARK" || node.nodeName === "mark") {
      const pn = node.parentNode;
      _unmarkText(node);
      // NOTE: タグの付与でバラけてしまったTextNodeをまとめたりする:
      pn.normalize();
    }
  }

  function _unmarkText(node) {
    const txtn = document.createTextNode(node.textContent);
    node.parentNode.replaceChild(txtn, node);
  }
}
</script>
...

サンプルを実行

サンプルコードを実行してみましょう。 入力BOXに文字を入れて[テキストにマーカー]ボタンを押すと該当箇所の背景色が変わります。 また、空入力でボタンを押すとマーカーが外れます。

マークあいうえお
かきくけこ
さしすマークせそ
なにぬねのマーク
markタグで囲います。
注意: 上記メッセージは「mark」というワードではマーカーを引けますが、「markタグ」というワードでは引けません。これは「markタグで囲います。」の実体が「<code>mark</code>」(code要素)と「タグで囲います。」(テキストノード要素)という2つの要素で構成されているからです。

ひとこと

どうぞご自由にコピペしてお使いください。

Enjoy!

芽萌丸プログラミング部 @programming
プログラミング関連アカウント。Web標準技術を中心に書いていきます。フロントエンドからサーバサイドまで JavaScript だけで済ませたい人たちの集いです。