Geminiの新しいインターフェース「Canvas」。発表されたときは震えましたよね。ついにAIと対話しながらドキュメントを作り込める、理想のエディタが来たと。
意気揚々と技術記事を書き、LaTeXで数式もバッチリ決めて、いざブログやNotionに投稿しようとしたその瞬間。
……あれ? Markdownエクスポート、どこ?
画面のどこを探しても見当たらない。恐る恐るコピペしてみると、数式は崩れ、テーブルは無惨な姿に。
「Googleなら標準で実装しているはず」という勝手な期待は見事に裏切られました。
一応、回避策はあります。「Googleドキュメントにエクスポート」してから、そこ経由でMarkdownとしてダウンロードする方法です。
しかし、試してすぐに諦めました。記事を書くたびにドライブに不要な一時ファイルが増え続けるのは精神衛生上よくありません。しかも、生成されたMarkdownを見ると、なぜか見出しに余計な太字(**)が混入しており、結局手作業での修正を強いられます。数式も画像に変換されてしまい、手動での元ソースからのコピペを強いられます。
Qiitaやブログ記事を効率よく量産したいのに、エクスポートのたびに「手作業」が発生するのは致命的なボトルネックです。
「ないなら、作るしかない」
勢いのまま、CanvasのDOM構造を解析し、見た目そのままにMarkdownとして抽出するChrome拡張機能「Canvas to Markdown for Gemini」を開発しました。
- Chrome Web Store: Canvas to Markdown for Gemini
- Google Gemini: テスト用のキャンバス
なぜ作ったのか:ProseMirrorとの戦い
Gemini Canvasのエディタ部分は、クラス名から推測するに ProseMirror ベースで構築されているようです。通常の textarea ではなく contenteditable な div 要素としてレンダリングされるため、単純な innerText の取得では以下の情報が欠落します。
-
数式 (LaTeX):
math-blockやmath-inlineといったカスタムタグで管理されており、単純なテキスト取得では数式として機能しなくなる。 - 構造化データ: ネストされたリストやテーブルのセパレータなど、Markdown特有の表現が失われる。
-
コードブロック: 言語指定(
language-python等)が属性として埋め込まれている。
これらを解決するため、DOMツリーを再帰的に走査し、各ノードを適切なMarkdown構文にマッピングするパーサーを実装する必要がありました。
実装のアプローチ
開発にあたっては、保守性と拡張性を重視して設計しました。
1. Strategyパターンによるタグ処理の分離
HTMLタグごとの変換ロジックを巨大な switch 文で書くのは悪手です。今回は strategies.js にタグごとのハンドラを定義し、Strategyパターンを採用しました。
const tagHandlers = {
'math-block': (node) => {
const latex = node.getAttribute('data-math');
return latex ? `\n\n$$${latex}$$` : '';
},
'h1': (n, t) => `\n\n# ${traverseChildren(n, t)}`,
'pre': (node) => {
// 言語判定ロジック
return `\n\n\`\`\`${lang}\n${codeContent}\n\`\`\``;
},
// ...他多数
};
これにより、Gemini側の仕様変更で新しいタグが追加された場合でも、ハンドラを追加するだけで対応できます。
2. メインスレッドをブロックしない非同期パース
長文のドキュメントを再帰的に処理すると、ブラウザのUIスレッドをブロックしてしまうリスクがあります。これを防ぐため、parser.js では処理をチャンク(塊)に分割し、requestAnimationFrame を挟むことで描画の譲渡を行っています。
async parseAsync(nodes, total, uiCallback) {
for (let i = 0; i < total; i += this.CHUNK_SIZE) {
// チャンク単位で同期処理
const chunk = nodes.slice(i, i + this.CHUNK_SIZE);
chunk.forEach(node => { markdown += this.traverse(node); });
// UI更新と描画フレームの譲渡
if (uiCallback) uiCallback(percent);
await new Promise(resolve => requestAnimationFrame(resolve));
}
return this.format(markdown);
}
この実装により、数万文字レベルのドキュメントであっても、進捗バーを表示しながらスムーズに変換処理を実行できます。
こだわりの機能
単なるテキスト抽出ではなく、「エンジニアがそのまま使える」品質を目指しました。
LaTeX数式の完全維持
Geminiが生成する数式は data-math 属性にLaTeX形式で格納されています。これを解析し、$$...$$ ブロックとして再構築します。ZennやQiita、Notionに貼り付けた瞬間に、美しい数式がレンダリングされます。
複雑なリスト構造の再現
Markdownのリストで意外と面倒なのが「ネスト」の扱いです。インデントのスペース数を適切に計算し、チェックボックス(- [ ])の状態も DOM から取得して反映させています。
Mermaid記法への対応
Canvas内で描画された図表についても、テキストベースの構造を維持するように配慮しました。
技術スタックとセキュリティ
- Manifest V3: 最新のChrome拡張仕様に準拠。
- Vanilla JS: 依存関係を減らし、軽量化するためにフレームワークは未使用。
- 完全ローカル処理: 取得したデータの解析、Markdown生成、ファイル保存(Blob利用)はすべてブラウザ内で完結します。外部サーバーへの送信は一切行っていません。
まとめ
「Canvas to Markdown for Gemini」は、Gemini Canvasという強力な思考ツールを、既存のテキストワークフロー(VS Code, Obsidian, GitHubなど)に接続するためのミッシングリンクです。
もしGeminiでのドキュメント作成に可能性を感じつつも、エクスポートの弱さにストレスを感じている方がいれば、ぜひ試してみてください。ソースは
法的免責事項
Geminiは Google LLC の商標です。本拡張機能内で言及される「Canvas」は、Gemini の特定の機能名称を指すものです。本拡張機能は非公式のサードパーティ製ツールであり、Google LLC と提携、スポンサー、または推奨されているものではありません。