家計簿アプリを作る #63:検索キーワードのハイライト表示

家計簿アプリ作成シリーズの第63回です。収支一覧でキーワード検索した結果、テーブル内のどこにマッチしたのかが一目で分かるように、該当箇所をハイライト表示します。

テキストをマッチ部分で分割する

文字列をキーワードで分割し、マッチした部分だけ別要素で囲む、というアプローチにしました。

function highlightParts(text: string, keyword: string): { text: string; match: boolean }[] {
  if (!keyword) return [{ text, match: false }];
  const parts = text.split(new RegExp(`(${keyword})`, 'gi'));
  return parts.map(part => ({
    text: part,
    match: part.toLowerCase() === keyword.toLowerCase(),
  }));
}
{#each highlightParts(transaction.memo, data.searchQuery) as part}
  {#if part.match}
    <mark class="bg-yellow-200 dark:bg-yellow-600">{part.text}</mark>
  {:else}
    {part.text}
  {/if}
{/each}

split()にキャプチャグループを渡すとマッチ部分も残る

JavaScriptの String.split() は通常「区切り文字を取り除いて分割する」だけですが、正規表現に () のキャプチャグループがあると、マッチした部分も結果の配列に含めて返すという特殊な挙動をします。

"スーパーで買い物".split(/(スー)/gi);
// ["", "スー", "パーで買い物"]

g(global)フラグを付けているので、文字列内に複数回マッチしても全て区切り点として扱われます。

"スーパーで買い物スーパーで買い物".split(/(スー)/gi);
// ["", "スー", "パーで買い物", "スー", "パーで買い物"]

分割された配列を .map() で「その要素自体がキーワードと完全一致するか」判定し、{ text, match } の形に変換することで、マッチ部分だけを特定できるようになっています。

ハマりポイント:キーワードの特殊文字でクラッシュする

動作確認していると、検索キーワードに (. のような正規表現の特殊文字が含まれると、new RegExp(...) が不正なパターンとしてエラーを投げ、ページ全体がクラッシュする問題がありました。

// ❌ keywordに特殊文字が含まれると不正な正規表現になる
const parts = text.split(new RegExp(`(${keyword})`, "gi"));

正規表現で特別な意味を持つ文字(. * + ? ^ $ { } ( ) | [ ] \)をエスケープする関数を用意して解決しました。

function escapeRegExp(str: string): string {
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function highlightParts(text: string, keyword: string): { text: string; match: boolean }[] {
  if (!keyword) return [{ text, match: false }];
  const escapedKeyword = escapeRegExp(keyword);
  const parts = text.split(new RegExp(`(${escapedKeyword})`, "gi"));
  return parts.map(part => ({
    text: part,
    match: part.toLowerCase() === keyword.toLowerCase(),
  }));
}

replace(/[.*+?^${}()|[\]\\]/g, "\\$&") は、正規表現で特別な意味を持つ文字の直前にバックスラッシュを挿入し、「ただの文字」として扱わせるための定番のイディオムです。ユーザーが自由に入力するキーワードを正規表現に組み込む場合は、必ずこのエスケープ処理が必要になることを再確認しました。

まとめ

ポイント 内容
ハイライトの実装方法 キャプチャグループ付きの正規表現でsplit()し、マッチ部分だけ<mark>で囲む
split()の特殊な挙動 キャプチャグループがあるとマッチ部分も結果配列に含まれる
ユーザー入力の正規表現化 特殊文字のエスケープを忘れるとクラッシュしうるので必須
← トップページに戻る