0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Chrome拡張】Google検索のAIモードやGeminiの回答を、Markdownで一発コピーするツールを作った

0
Last updated at Posted at 2026-07-01

概要

日々の開発やリサーチで、Google検索の「AIモード(AI Overview)」や「Gemini」を使う機会がすっかり定着しました。
しかし、後から情報を見返したり、社内WikiやNotionにまとめようとしたとき、標準のコピー機能では**「リストのインデント階層が崩れる」「参考リンクのURLが消える」**といった問題があり、手作業でMarkdownを修正するのに地味なストレスを感じていました。

そこで、ワンクリックでAIの質疑応答を**リッチなMarkdown形式(階層構造やリンクを保持)**としてクリップボードにコピーできるChrome拡張機能「Gemini-Search-MD-Copier」を開発しました。

本記事では、ツールの紹介とあわせて、複雑怪奇なGoogleのDOMから美しいMarkdownを生成するための「泥臭い(だけど実用的な)ハック」について解説します。

主な機能と実際の動作

この拡張機能は、単に画面のテキストを抜いてくるだけではありません。以下のフォーマットを自動で解析・補正します。

1. 箇条書きの階層(インデント)を論理的に維持
2. Gemini特有の特殊タグ(<sequence>等)のステップ展開に対応
3. AIが提示する「関連リンク」を <details> タグで折りたたみ可能な形で抽出
4. コードブロックの言語名(js, pyなど)を付与

外部ライブラリにいっさい依存せず、Manifest V3 + Vanilla JavaScriptの軽量構成で作っています。


技術的なハイライト:どうやってDOMをMarkdownに翻訳しているか

GoogleのAI回答部分は、単純に .innerText で取得すると構造が破綻します。今回、パーサーを自作する上で特に苦労・工夫した2つのポイントをご紹介します。

1. リストの「見た目のインデント」を「論理階層」に再計算する

HTMLのネスト構造が複雑な場合、単純にDOMツリーを潜るだけでは箇条書きの階層(深さ)が正しく測れません。
そこで、reIndentMarkdownStyles 関数を実装し、「行頭のスペース数を計測し、辞書構造(indentMap)に記録して階層の深さを再計算する」というアプローチをとりました。


// インデントを論理階層に基づいて再調整するコア処理
let indentMap = { 0: 0 };
let currentMaxDepth = 0; // 「階層の深さ(0, 1, 2...)」を管理

for (let line of lines) {
  const originalIndent = line.search(/\S|$/);

  // 1. 辞書にない新しいインデントなら登録し、最大深度を更新
  if (indentMap[originalIndent] === undefined) {
    currentMaxDepth += 1; // 階層を掘り下げる
    indentMap[originalIndent] = currentMaxDepth;
  }

  // 2. 前行と比較してインデントが戻った(浅くなった)場合のクリーンアップ
  const lastOriginalIndent = processedLines.length > 0 ? getIndent(lines[processedLines.length - 1]) : 0;
  if (originalIndent < lastOriginalIndent) {
    // 現在より深い階層の辞書データを削除
    Object.keys(indentMap).forEach(key => {
      if (parseInt(key) > originalIndent) {
        delete indentMap[key];
      }
    });
    // 最大階層を再計算
    currentMaxDepth = indentMap[originalIndent] || 0;
  }

  // 3. 辞書に沿って新しいインデント(スペース)を適用
  const newIndentLevel = indentMap[originalIndent];
  const indentedLine = indentUnit.repeat(newIndentLevel) + content;
  processedLines.push(indentedLine);
}

これにより、画面上の見た目に近い形でインデントされMarkdownとして出力されます。

2. Gemini特有の特殊タグ <sequence> のパース手法

Geminiのチャット画面を解析していて直面したのが、Google特有のカスタムタグ(Web Components)の存在です。
たとえば、Geminiが段階的な手順やプロセスを回答する際、ただの <ul><ol> ではなく、<sequence> という独自のタグ構造が使われることがあります。
これを無視するとテキストが欠落してしまうため、パーサー内で明示的に sequence タグとその子要素を拾い上げる処理を追加しました。

JavaScript

// GeminiのDOMパース処理(switch文の一部抜粋)
case 'sequence':
  childContent = '';
  const sequencelines = element.querySelectorAll("div.sequence-event");

  if (sequencelines) {
    // インデントレベルの計算
    const totalLeftSpace = getLeftPixels(element);
    let indentLevel = Math.max(0, Math.floor(totalLeftSpace / 20));
    const indentStr = indentUnit.repeat(indentLevel);

    sequencelines.forEach(child => {
      // マーカー(番号など)、タイトル、サブタイトル、詳細をそれぞれ抽出
      const markerEl = child.querySelector("div.sequence-event-marker");
      const divmarker = markerEl ? markerEl.innerText.trim() : "";
      const divtitle = convertToMarkdownGemini(child.querySelector("div.sequence-event-title"));
      const divsubtitle = convertToMarkdownGemini(child.querySelector("div.sequence-event-subtitle"));
      const divdetail = convertToMarkdownGemini(child.querySelector("div.sequence-event-description>structured-node-sequence"));

      // Markdownリストのフォーマットに組み立て
      if (divmarker) {
        const zenkakuMarker = toZenkakuNumString(divmarker);
        childContent += `${indentStr}- ${zenkakuMarker} ${divtitle.trim()}\n`
          + `${indentStr}${indentUnit}*${divsubtitle.trim()}*\n`
          + `${indentStr}${indentUnit}${divdetail.trim()}\n`;
      } else {
        // マーカーがないパターンの処理...
      }
    });
  }
  return `\n${childContent}\n`;

見慣れないタグが動的に生成されるのはモダンなWebアプリならではですが、このようにクラス名やカスタムタグ名で確実にフックすることで、複雑なUIコンポーネントもMarkdownのリストと斜体(イタリック)を組み合わせたテキストに翻訳することができました。

ちょっと便利な副産物:「関連サイト」の折りたたみ

個人的に気に入っているのが、AI回答の末尾につく「関連サイト」リンク群の処理です。
そのままMarkdownに書き出すとリンクの羅列で縦に長くなりすぎるため、本拡張機能では HTMLの <details><summary> タグを用いて折りたたみ可能なアコーディオンとして出力するようにしました。NotionやGitHubのプレビューでもそのまま折りたためるので、ドキュメントがすっきりします。

まとめと今後の展望

今回、UIが頻繁に変わるGoogle検索やGeminiのDOM解析に挑みましたが、「属性(jsname等)」や「カスタムタグ(<sequence>等)」をベースにすることで、ある程度堅牢な抽出ツールを作ることができたと思います。
今後もGoogle側のUI変更を継続的にウォッチしつつ、さらにパースの精度を上げていきたいと考えています。
ソースコードはすべてGitHubにて公開しています。「もっとスマートにパースできるよ!」「この画面だと崩れた!」といったご意見があれば、ぜひスター(⭐)やIssue、Pull Requestをお待ちしています。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?