Technical Docs
Markdown Formatter の設計
Prettier standalone による Markdown 整形パイプラインと、Gemini AI を用いた平文→Markdown 変換(MD変換)機能の設計・実装を解説します。
1. 設計思想 — 2つの機能を1ツールに統合する理由
このツールは、Markdownに関する2つの異なるユースケースを1画面で解決します。
- 整形する(Prettier整形): すでにMarkdown記法で書かれているが、AIの出力やコピー&ペーストにより空白・改行が崩れたテキストを Prettier標準に合わせて正規化します。 見出し後の空行挿入、行末スペースの除去、改行コードの統一など、 機械的に検出・修正できる「崩れ」を一括で直します。
- MD変換(AI変換): Markdownの知識がないユーザーが書いた普通の文章や、AI応答の生テキストなど、 Markdown記法が一切含まれない平文を、Gemini AIが自動的に 見出し・リスト・テーブル・コードブロック付きのMarkdown文書へ変換します。
2つの機能を分離せず同一ページに置くことで、 「整形したいのかAI変換したいのかわからない」というユーザーの迷いをなくし、 どちらの用途でも最短の操作で結果を得られるようにしています。
2. 技術アーキテクチャ
本ツールは3層で構成されています。
- 純粋関数層(
src/lib/tools/markdown-formatter.ts): 前処理(preprocessMarkdown)と Prettier 整形(formatMarkdown)を 副作用なしの非同期純粋関数として実装します。 エラーは例外ではなく{ success: false, message: string }の Union 型で返します。 - Server Action層(
app/tools/markdown-formatter/_actions/gemini-format.ts): Gemini REST APIの呼び出しをサーバーサイドに集約します。'use server'ディレクティブにより Cloudflare Workers(Edge Runtime)上で実行され、 APIキーがブラウザに露出しません。 - Client Component層(
MarkdownFormatterClient.tsx): Prettier整形(isPending)と AI変換(isAiPending)を それぞれ独立したuseTransitionで管理し、 2つの処理が干渉せず独立してローディング状態を追跡できます。
ページルート(app/tools/markdown-formatter/page.tsx)は ファイル冒頭に export const runtime = 'edge' を宣言しており、 Server Action はこの設定を自動継承して Edge Runtime で実行されます。 Server Action 側への runtime 設定は不要(むしろ 'use server' と共存させると モジュール exports が壊れる)ため、ページ側のみに記述しています。
3. Prettier整形の仕組み
Prettier整形は「前処理 → Prettier本体による整形」の2ステップで実行されます。
- 前処理(preprocessMarkdown): Prettier が正しく整形するための下準備として4つの変換を行います。
- 改行コードの統一: CRLF(
\r\n)・CR(\r)を すべて LF(\n)に変換します。Windowsからのコピーテキストに多いCRLFを正規化します。 - 全角スペースの半角化(オプション): 日本語IMEで誤入力されやすい全角スペース(
)を半角に変換します。 デフォルトはオフで、チェックボックスで切り替えられます。 - 行末スペース・タブの除去: Markdownで意図しない改行を引き起こす 行末の空白文字を全行から一括削除します。
- 見出し後の空行挿入:
#〜######で始まる 見出し行の直後に空行がない場合、自動で空行を挿入します。 Prettierは見出しと次の段落の間に空行を期待するため、この前処理なしでは 整形結果がずれることがあります。
- 改行コードの統一: CRLF(
- Prettier standalone の動的インポート:
prettier/standaloneとprettier/plugins/markdownは バンドルサイズが大きいため、formatMarkdown関数内でPromise.all([import(...), import(...)])により実際に整形が実行されるタイミングまでロードを遅延させています(bundle-conditionalパターン)。 ページ初回ロード時のJavaScriptコストを削減できます。 - proseWrap: 'preserve': Prettierのデフォルト設定ではテキストが
printWidth(80文字)で 自動折り返されますが、AI生成テキストやコピーペースト文章では 意図しない折り返しが問題になります。proseWrap: 'preserve'を指定することで、既存の改行位置を 変更せず、空白・見出し・リストの正規化のみを行います。
4. AI MD変換の仕組み(Gemini連携)
MD変換機能は、Gemini REST API(gemini-3-flash-preview)を Server Action から呼び出すことで実現しています。 AI変換に成功した後、さらに Prettier 整形を1回適用することで 出力品質を安定させます。
- プロンプト設計: 生テキストのみ返す・HTMLに変換しない・コードブロック全体で囲まない、 などの指示を明示したシステムプロンプトを事前定義しています。
temperature: 0.1(低温)を指定して出力の創造性より安定性を優先します。 - APIキーのサーバーサイド管理:
GEMINI_API_KEYはprocess.env.GEMINI_API_KEYで Server Action 内のみから参照します。NEXT_PUBLIC_プレフィックスを 意図的に付けないことで、ブラウザバンドルへの露出を防いでいます。 - タイムアウト制御:
AbortSignal.timeout(30_000)で30秒のタイムアウトを設定しています。 Node.js API に依存しない Web 標準の API であるため Edge Runtime でも使用できます。 - レート制限の精密な判定: Gemini API の 429 エラーには2種類があります。
- 分レート制限(一時的): エラーメッセージに
"retry in Xs"(秒単位の待機指示)が含まれます。isPerMinuteRateLimit()関数で検出し、待機秒数をユーザーに提示します。 ボタンは非活性化せず、待機後に再試行できます。 - 日次トークン上限(永続的):
daily・per day・token quota等のキーワードが含まれます。isDailyTokenLimit()関数で検出し、isRateLimited: trueを返します。 クライアント側はこのフラグを受け取ってセッション中ボタンを非活性化します。
- 分レート制限(一時的): エラーメッセージに
5. Edge Runtime制約と実装上の注意点
本プロジェクトは Cloudflare Pages(Cloudflare Workers)にデプロイされており、 Server Actions は Edge Runtime 上で動作します。この環境にはいくつかの制約があります。
- Node.js API 非対応: Edge Runtime は
Buffer・ストリーム・fs等の Node.js API を サポートしません。Gemini連携にはfetchのみを使用しており、 Node.js API に依存するライブラリは一切使用していません。 - Prettier standalone はクライアントサイドで実行:
prettier/standaloneは Server Action ではなく Client Component 内で呼び出されます(formatMarkdownはブラウザで実行)。 Edge Runtime 制約の対象外ですが、バンドルコスト削減のため動的インポートを適用しています。 - 2つの useTransition の独立管理: Prettier整形(
isPending / startTransition)と AI変換(isAiPending / startAiTransition)を 別のuseTransitionで管理しています。 共通化すると「整形中はMD変換ボタンも非活性」という挙動になりますが、 実際には独立した操作であるため、それぞれ独立したローディング状態を持ちます。 一方が処理中のときはisBusy = isPending || isAiPendingで もう一方のボタンを非活性にします。
6. 品質管理
Markdown Formatter は純粋関数テストと E2E テストの両方でカバーしています。
- 純粋関数テスト(Vitest)(
src/lib/tools/__tests__/markdown-formatter.test.ts):preprocessMarkdownとformatMarkdownに対して25件のテストを実装しています。 改行コード統一・全角スペース変換・見出し後空行挿入・Prettier整形結果・ 空入力エラー返却などを網羅します。 - E2Eテスト(Playwright)(
e2e/markdown-formatter.spec.ts): 15件のシナリオで主要なUIフローを検証しています。 整形ボタンの活性/非活性・貼り付け時自動整形・プレビュータブ切り替え・ シンタックスハイライト・コピーToast通知・クリア動作などをカバーします。 - Server Actionのモック制約: Playwright の
page.routeはブラウザ発のリクエストのみ傍受できます。 Server Action 内で行う Gemini API 呼び出しはサーバーサイドで実行されるためpage.routeでは傍受・モックできません。 そのため AI変換系のE2Eテストは「ローディング状態の観察」や「デフォルト非表示の確認」など、 Gemini API の実際の応答に依存しない検証に限定しています。