HB DevTools

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つの変換を行います。
    1. 改行コードの統一: CRLF(\r\n)・CR(\r)を すべて LF(\n)に変換します。Windowsからのコピーテキストに多いCRLFを正規化します。
    2. 全角スペースの半角化(オプション): 日本語IMEで誤入力されやすい全角スペース( )を半角に変換します。 デフォルトはオフで、チェックボックスで切り替えられます。
    3. 行末スペース・タブの除去: Markdownで意図しない改行を引き起こす 行末の空白文字を全行から一括削除します。
    4. 見出し後の空行挿入: ####### で始まる 見出し行の直後に空行がない場合、自動で空行を挿入します。 Prettierは見出しと次の段落の間に空行を期待するため、この前処理なしでは 整形結果がずれることがあります。
  • Prettier standalone の動的インポート:prettier/standaloneprettier/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_KEYprocess.env.GEMINI_API_KEY で Server Action 内のみから参照します。NEXT_PUBLIC_ プレフィックスを 意図的に付けないことで、ブラウザバンドルへの露出を防いでいます。
  • タイムアウト制御:AbortSignal.timeout(30_000) で30秒のタイムアウトを設定しています。 Node.js API に依存しない Web 標準の API であるため Edge Runtime でも使用できます。
  • レート制限の精密な判定: Gemini API の 429 エラーには2種類があります。
    1. 分レート制限(一時的): エラーメッセージに"retry in Xs"(秒単位の待機指示)が含まれます。isPerMinuteRateLimit() 関数で検出し、待機秒数をユーザーに提示します。 ボタンは非活性化せず、待機後に再試行できます。
    2. 日次トークン上限(永続的): dailyper daytoken quota 等のキーワードが含まれます。isDailyTokenLimit() 関数で検出し、isRateLimited: true を返します。 クライアント側はこのフラグを受け取ってセッション中ボタンを非活性化します。
    2関数を分離することで、分制限を日次上限と誤判定してボタンが永続的に 非活性になる問題(以前の実装でのバグ)を防いでいます。

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):preprocessMarkdownformatMarkdown に対して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 の実際の応答に依存しない検証に限定しています。