# 機能設計書 54-Markdownウィジェット

## 概要

本ドキュメントは、Decap CMSにおけるMarkdownウィジェットの設計を記述する。リッチテキストエディタ（ビジュアルモード）とRawテキストエディタの2つのモードを提供するMarkdown編集ウィジェットである。

### 本機能の処理概要

**業務上の目的・背景**：CMSのコンテンツ編集において、Markdown形式の本文入力は最も重要な機能の一つである。本ウィジェットは、Markdownに不慣れなユーザーのためにWYSIWYG的なリッチテキストエディタ（ビジュアルモード）を提供しつつ、上級ユーザーには生のMarkdownを直接編集できるRawモードも用意している。カスタムエディタコンポーネント、Remarkプラグインにも対応し、拡張性が高い。

**機能の利用シーン**：エントリ編集画面・エントリ新規作成画面において、`widget: markdown`が指定されたフィールドに対してMarkdownエディタをレンダリングする。ブログ記事の本文やドキュメントの内容編集に広く利用される。

**主要な処理内容**：
1. ビジュアルエディタ（Slateベース）とRawエディタの切り替え
2. Markdown文字列とSlate AST間の双方向シリアライズ
3. ツールバーによるインライン書式（太字・斜体・リンク等）とブロック書式（見出し・リスト・引用等）の適用
4. ショートコード（カスタムエディタコンポーネント）の挿入・管理
5. HTML貼り付け時の自動変換（HTML → Slate AST）
6. プレビューコンポーネントでのMarkdown → HTML変換表示

**関連システム・外部連携**：Slate（リッチテキストエディタフレームワーク）、Unified/Remark/Rehype（Markdownパーサー/シリアライザー）、DOMPurify（HTMLサニタイズ）

**権限による制御**：なし。ただし`isDisabled`プロップで読み取り専用化が可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 6 | エントリ編集画面 | 主機能 | Markdown形式のリッチテキスト編集 |
| 7 | エントリ新規作成画面 | 主機能 | Markdown形式のリッチテキスト編集 |

## 機能種別

UI入力コンポーネント（リッチテキストエディタ）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| onChange | function | Yes | 値変更時のコールバック | - |
| onAddAsset | function | Yes | アセット追加関数 | - |
| getAsset | function | Yes | アセット取得関数 | - |
| classNameWrapper | string | Yes | ラッパーのCSSクラス名 | - |
| editorControl | elementType | Yes | ネストウィジェット用のコントロールコンポーネント | - |
| value | string | No | Markdown文字列（デフォルト: ''） | - |
| field | ImmutableMap | Yes | フィールド設定（buttons, modes, minimal, editor_components等） | - |
| getEditorComponents | function | No | エディタコンポーネント取得関数 | - |
| getRemarkPlugins | function | No | Remarkプラグイン取得関数 | - |
| resolveWidget | function | No | ウィジェット解決関数 | - |
| t | function | Yes | ローカライズ関数 | - |
| isDisabled | boolean | No | 無効化フラグ | - |

### 入力データソース

- エディタのコントロールペインから提供されるprops
- Reduxストアのエントリドラフトから値が供給
- レジストリから取得されるエディタコンポーネント・Remarkプラグイン

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| value | string | 編集後のMarkdown文字列 |

### 出力先

onChangeコールバック経由でReduxストアのエントリドラフトに反映される。

## 処理フロー

### 処理シーケンス

```
1. コンポーネントマウント
   └─ localStorageからモード設定を読み取り（KEY: 'cms.md-mode'）
   └─ fieldのmodes設定で利用可能モードを確認
2. ビジュアルモード選択時
   └─ markdownToSlateでMarkdown文字列をSlate ASTに変換
   └─ Slateエディタを初期化（withInlines > withLists > withBlocks > withShortcodes > withHistory > withReact > withHtml）
   └─ ツールバーとEditable領域をレンダリング
3. Rawモード選択時
   └─ Markdown文字列を行ごとに分割しSlateの単純テキストノードとして表示
4. ユーザー編集
   └─ ビジュアル: Slate ASTを変更 → slateToMarkdownでMarkdown文字列に変換 → onChange
   └─ Raw: テキスト行を結合 → onChange
5. モード切替
   └─ localStorageにモードを保存
   └─ Stateを更新してビジュアル/Rawコンポーネントを切替
6. プレビュー表示
   └─ markdownToHtmlでMarkdownをHTMLに変換
   └─ sanitize_preview設定時はDOMPurifyでサニタイズ
```

### フローチャート

```mermaid
flowchart TD
    A[MarkdownControl初期化] --> B{モード判定}
    B -->|rich_text| C[VisualEditor]
    B -->|raw| D[RawEditor]
    C --> E[markdownToSlate変換]
    E --> F[Slateエディタ表示]
    F --> G[ユーザー編集]
    G --> H[slateToMarkdown変換]
    H --> I[onChange呼び出し]
    D --> J[テキスト行分割表示]
    J --> K[ユーザー編集]
    K --> L[行結合]
    L --> I
    I --> M[Reduxストア更新]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-54-01 | モード記憶 | エディタモード（rich_text/raw）をlocalStorageに記憶し、次回起動時に復元する | 常時 |
| BR-54-02 | 許可モード制限 | fieldのmodes設定で利用可能なモードを制限できる | modes設定あり時 |
| BR-54-03 | デフォルトモード | デフォルトモードはrich_text（ビジュアルモード） | modes未設定時 |
| BR-54-04 | コードブロック | code-blockエディタコンポーネントが未登録の場合は自動的にデフォルトのcode-blockを追加 | 常時 |
| BR-54-05 | メディア設定マージ | Markdownフィールドのmedia_library, media_folder, public_folder設定を画像エディタコンポーネントにマージ | image コンポーネント存在時 |
| BR-54-06 | HTMLサニタイズ | sanitize_previewがtrueの場合、プレビューHTMLをDOMPurifyでサニタイズ | sanitize_preview: true |

### 計算ロジック

- Markdown → Slate AST: `markdownToRemark`（Unified/Remark）→ `remarkToSlate`
- Slate AST → Markdown: `slateToRemark`（手動変換）→ `remarkToMarkdown`（Unified/Remark）
- Markdown → HTML: `markdownToRemark` → `remarkToRehype` → `rehypeToHtml`

## データベース操作仕様

### 操作別データベース影響一覧

該当なし。本ウィジェットはUIコンポーネントであり、データベースを直接操作しない。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | パース失敗 | 不正なMarkdown文字列のパース | Unified/Remarkの内部エラーハンドリングに依存 |
| - | PropType警告 | 必須propsが未提供の場合 | React 19対応のためcomponentDidMountで手動バリデーション |

### リトライ仕様

該当なし。

## トランザクション仕様

該当なし。

## パフォーマンス要件

- Slate ASTとMarkdown文字列間の変換は編集のたびに実行されるため、大きなドキュメントではパフォーマンスに影響が出る可能性がある
- ツールバーの再レンダリングはtoolbarKeyの更新により毎回発生する

## セキュリティ考慮事項

- プレビュー表示時に`dangerouslySetInnerHTML`を使用してHTMLをレンダリングするため、XSSリスクがある
- `sanitize_preview: true`設定時はDOMPurifyによるサニタイズが適用される
- Remarkのカスタムプラグインにより任意のHTML生成が可能なため、プラグインの信頼性確認が重要

## 備考

- Slate v0.x系のAPIを使用（createEditor, Transforms, Editor等）
- slate-react、slate-historyに依存
- プラグインはwithInlines → withLists → withBlocks → withShortcodes → withHistory → withReact → withHtml の順でチェイニングされる

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

Markdown ↔ Slate AST ↔ MDAST ↔ HTML のデータ変換チェインを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | serializers/index.js | `packages/decap-cms-widget-markdown/src/serializers/index.js` | 変換関数群の全体像（markdownToSlate, slateToMarkdown, markdownToHtml, htmlToSlate）。25-57行目のコメントに4つのデータ形式の説明あり |

**読解のコツ**: Unifiedエコシステム（Remark = MDAST, Rehype = HAST）の理解が前提。`unified().use(plugin).parse()`/`.runSync()`/`.stringify()`のパイプラインパターンが全変換で使われている。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | MarkdownControl/index.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/index.js` | モード切替ロジック（48-55行目）、getAllowedModes（73行目）、localStorageへのモード保存（62-65行目） |

**主要処理フロー**:
1. **9行目**: MODE_STORAGE_KEY = 'cms.md-mode'
2. **42-55行目**: コンストラクタ - editorControlの設定、localStorageからモード読み取り
3. **62-65行目**: handleMode - モード変更とlocalStorage保存
4. **73行目**: getAllowedModes - fieldのmodes設定から許可モード取得
5. **79-133行目**: render - mode === 'rich_text' ? visualEditor : rawEditor

#### Step 3: ビジュアルエディタを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | VisualEditor.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js` | Slateエディタのプラグインチェイン（99-105行目）、markdownToSlate/slateToMarkdownの使用（120-124行目、188-194行目） |

**主要処理フロー**:
- **99-105行目**: Slateエディタ初期化（withHtml > withReact > withHistory > withShortcodes > withBlocks > withLists > withInlines）
- **118-125行目**: 初期値のmarkdownToSlateによるSlate AST変換
- **186-197行目**: handleChange - editorValue変更時にslateToMarkdownでMarkdown化してonChangeに渡す
- **53-83行目**: mergeMediaConfig - エディタコンポーネントのメディア設定マージ

#### Step 4: Rawエディタを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | RawEditor.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js` | 行単位のテキスト処理（37-41行目初期化、54-57行目onChange） |

#### Step 5: プレビューを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | MarkdownPreview.js | `packages/decap-cms-widget-markdown/src/MarkdownPreview.js` | markdownToHtmlでHTML変換、DOMPurifyサニタイズ（25-26行目）、dangerouslySetInnerHTMLでレンダリング（28行目） |

### プログラム呼び出し階層図

```
MarkdownControl (MarkdownControl/index.js)
    |
    +-- VisualEditor (VisualEditor.js)
    |       +-- createEditor() with plugins chain
    |       |       +-- withInlines
    |       |       +-- withLists
    |       |       +-- withBlocks
    |       |       +-- withShortcodes
    |       |       +-- withHistory
    |       |       +-- withReact
    |       |       └── withHtml
    |       +-- markdownToSlate (serializers/index.js)
    |       |       +-- markdownToRemark
    |       |       └── remarkToSlate
    |       +-- slateToMarkdown (serializers/index.js)
    |       |       +-- slateToRemark
    |       |       └── remarkToMarkdown
    |       +-- Toolbar (Toolbar.js)
    |       └── Editable (slate-react)
    |
    +-- RawEditor (RawEditor.js)
    |       +-- createEditor() (basic Slate)
    |       └── Editable (slate-react)
    |
    └── MarkdownPreview (MarkdownPreview.js)
            +-- markdownToHtml (serializers/index.js)
            |       +-- markdownToRemark
            |       +-- remarkToRehype
            |       └── rehypeToHtml
            └── DOMPurify.sanitize (optional)
```

### データフロー図

```
[入力]                   [処理]                        [出力]

Markdown文字列  ──> markdownToSlate ──>  Slate AST
                                            |
                                     ユーザー編集
                                            |
                                     slateToMarkdown
                                            |
                                            v
                                     Markdown文字列 ──> onChange

Markdown文字列  ──> markdownToHtml  ──>  HTML文字列
                                            |
                                     DOMPurify (optional)
                                            |
                                            v
                                     プレビュー表示
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.js | `packages/decap-cms-widget-markdown/src/index.js` | ソース | ウィジェットエクスポート定義 |
| MarkdownControl/index.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/index.js` | ソース | メインコントロール（モード切替） |
| VisualEditor.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/VisualEditor.js` | ソース | ビジュアルエディタ |
| RawEditor.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/RawEditor.js` | ソース | Rawエディタ |
| Toolbar.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/Toolbar.js` | ソース | ツールバー |
| renderers.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/renderers.js` | ソース | Slateの要素・リーフレンダラー |
| serializers/index.js | `packages/decap-cms-widget-markdown/src/serializers/index.js` | ソース | シリアライザー群（変換関数） |
| serializers/slateRemark.js | `packages/decap-cms-widget-markdown/src/serializers/slateRemark.js` | ソース | Slate AST → MDAST変換 |
| serializers/remarkSlate.js | `packages/decap-cms-widget-markdown/src/serializers/remarkSlate.js` | ソース | MDAST → Slate AST変換 |
| MarkdownPreview.js | `packages/decap-cms-widget-markdown/src/MarkdownPreview.js` | ソース | プレビューコンポーネント |
| plugins/blocks/withBlocks.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/blocks/withBlocks.js` | ソース | ブロック要素プラグイン |
| plugins/lists/withLists.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/lists/withLists.js` | ソース | リスト操作プラグイン |
| plugins/inlines/withInlines.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/inlines/withInlines.js` | ソース | インライン書式プラグイン |
| plugins/shortcodes/withShortcodes.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/shortcodes/withShortcodes.js` | ソース | ショートコードプラグイン |
| plugins/html/withHtml.js | `packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/html/withHtml.js` | ソース | HTMLペーストプラグイン |
