# 機能設計書 78-エディタコンポーネント

## 概要

本ドキュメントは、Markdownエディタにカスタムブロック（エディタコンポーネント）を追加登録する機能の設計を記述する。

### 本機能の処理概要

本機能は、Markdownエディタ内で使用できるカスタムブロックコンポーネント（ショートコード、コードブロック等）をグローバルレジストリに登録・取得する仕組みを提供する。登録されたエディタコンポーネントは、Markdownコンテンツ内でパターンマッチングにより検出され、専用のUIで編集・プレビューできる。

**業務上の目的・背景**：MarkdownエディタではプレーンテキストやHTMLだけでなく、YouTube埋め込み、画像ギャラリー、カスタムショートコード等のリッチなコンテンツブロックを扱いたい要件がある。エディタコンポーネントにより、これらのカスタムブロックを定義し、Markdownコンテンツ内でWYSIWYG編集できるようにする。

**機能の利用シーン**：
- Markdownエディタでカスタムショートコード（例: `{{< youtube id="xxx" >}}`）を認識し、専用の入力UIで編集する
- コードブロックをカスタムコードエディタ（CodeMirror等）で編集する
- 画像挿入をカスタムUIで行う

**主要な処理内容**：
1. **エディタコンポーネント登録（registerEditorComponent）**: コンフィグオブジェクトをEditorComponentバリューオブジェクトで正規化し、ImmutableMapに登録
2. **エディタコンポーネント取得（getEditorComponents）**: 登録済みの全エディタコンポーネントをImmutableMapとして返す
3. **EditorComponent正規化**: コンフィグオブジェクトからid、label、type、icon、widget、pattern、fields、fromBlock、toBlock、toPreview等のプロパティを正規化し、デフォルト値を設定
4. **code-blockタイプの唯一性制約**: type='code-block'のエディタコンポーネントは1つのみ登録可能。既存がある場合は警告して上書き

**関連システム・外部連携**：レジストリ（registry.js）、EditorComponentバリューオブジェクト（valueObjects/EditorComponent.js）、Immutable.js（Mapデータ構造）と連携する。

**権限による制御**：エディタコンポーネント登録はグローバルAPIとして公開されており、権限制御はない。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 6 | エントリ編集画面 | 主画面 | Markdownエディタ内でエディタコンポーネントが使用される |
| 7 | エントリ新規作成画面 | 参照画面 | 新規エントリのMarkdown編集時にエディタコンポーネントが使用される |

## 機能種別

プラグイン登録 / エディタ拡張

## 入力仕様

### 入力パラメータ

**registerEditorComponent**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| component | object | Yes | エディタコンポーネント設定オブジェクト | - |

**component設定オブジェクト**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string | No | コンポーネントID（デフォルト: labelから生成） | - |
| label | string | No | 表示ラベル（デフォルト: 'unnamed component'） | - |
| icon | string | No | アイコン名（デフォルト: 'exclamation-triangle'） | - |
| type | string | No | タイプ（'shortcode' or 'code-block'、デフォルト: 'shortcode'） | - |
| widget | string | No | 使用するウィジェット名（デフォルト: 'object'） | - |
| pattern | RegExp | No | Markdownテキスト内でのマッチングパターン（デフォルト: /.^/ = マッチなし） | - |
| fields | array | No | フィールド定義配列（Immutable.jsのListに変換される） | - |
| fromBlock | function | No | Markdownブロックからフィールド値へのデシリアライズ関数 | - |
| toBlock | function | No | フィールド値からMarkdownブロックへのシリアライズ関数 | - |
| toPreview | function | No | フィールド値からプレビューHTMLへの変換関数 | - |

### 入力データソース

- CMS.registerEditorComponent() API経由で登録される
- decap-cms-editor-component-imageパッケージ（画像エディタコンポーネント）等の標準パッケージから登録される

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| editorComponents | ImmutableMap | id -> EditorComponentオブジェクトのImmutableMap |

### 出力先

registry.editorComponents（ImmutableMap）に格納。getEditorComponents()で取得可能。

## 処理フロー

### 処理シーケンス

```
1. registerEditorComponent(component)呼び出し
   └─ EditorComponent(component)でコンフィグを正規化
2. 正規化処理
   └─ id生成（labelから非英数字を_に置換）
   └─ fromBlock/toBlock/toPreviewのbind(null)処理
   └─ fieldsをfromJS(fields)でImmutable.jsのListに変換
3. code-blockタイプチェック
   └─ type === 'code-block'の場合、既存のcode-blockを検索
   └─ 既存あれば警告してdeleteし、新規をsetで登録
4. ImmutableMapへの登録
   └─ registry.editorComponents.set(plugin.id, plugin)
```

### フローチャート

```mermaid
flowchart TD
    A[registerEditorComponent] --> B[EditorComponent: コンフィグ正規化]
    B --> C{type === 'code-block'?}
    C -->|Yes| D{既存code-block存在?}
    C -->|No| F[ImmutableMapにset]
    D -->|Yes| E[console.warn + 既存をdelete]
    D -->|No| F
    E --> F
    F --> G[registry.editorComponents 更新完了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-78-01 | code-block唯一性 | type='code-block'のエディタコンポーネントは1つのみ登録可能 | registerEditorComponent時 |
| BR-78-02 | ID自動生成 | idが未指定の場合、labelの非英数字を_に置換して生成 | idがnullの場合 |
| BR-78-03 | デフォルトtype | typeのデフォルト値は'shortcode' | type未指定時 |
| BR-78-04 | デフォルトwidget | widgetのデフォルト値は'object' | widget未指定時 |
| BR-78-05 | デフォルトpattern | patternのデフォルト値は/.^/（何にもマッチしない） | pattern未指定時 |
| BR-78-06 | fromBlock/toBlockのbind | fromBlock/toBlockがfunctionの場合、bind(null)で関数をラップ。undefinedの場合はデフォルトの空関数を設定 | 常時 |
| BR-78-07 | toPreviewフォールバック | toPreviewが未指定かつwidgetも未設定の場合、toBlockをtoPreviewとして使用 | toPreview未指定時 |
| BR-78-08 | fieldsのImmutable化 | fields配列はfromJS()でImmutable.jsオブジェクトに変換される | 常時 |

### 計算ロジック

**ID生成ロジック**（EditorComponent.js）:
- `id = label.replace(/[^A-Z0-9]+/gi, '_')`
- 例: "YouTube Video" -> "YouTube_Video"

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

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

本機能はインメモリのImmutableMapへの登録であり、データベース操作は行わない。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | console.warn | code-blockタイプの二重登録 | 既存を上書きし警告メッセージを出力 |

### リトライ仕様

リトライは不要。

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

ImmutableMapの操作（set/delete）はイミュータブルであり、新しいMapオブジェクトが生成される。

## パフォーマンス要件

- エディタコンポーネントの登録・取得はImmutableMapのO(log n)操作
- Markdownテキスト内でのパターンマッチングはテキスト量に比例

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

- toPreview関数でHTMLを生成する場合、XSSリスクに注意が必要
- fromBlock/toBlock関数はMarkdownコンテンツの解析・生成を行うため、悪意のあるパターンによるReDoS攻撃の可能性がある

## 備考

- EditorComponentバリューオブジェクトはcreateEditorComponent関数として実装されている
- fromBlock/toBlockのbind(null)処理は、関数のthisコンテキストをnullに固定する
- isFunction(fn) && fn.bind(null) のパターンにより、関数でない値はfalseを返す（デフォルト関数にフォールバック）
- デフォルトのfromBlockは空オブジェクトを返し、デフォルトのtoBlockは'Plugin'文字列を返す

---

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

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

### 推奨読解順序

#### Step 1: EditorComponentバリューオブジェクトを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | EditorComponent.js | `packages/decap-cms-core/src/valueObjects/EditorComponent.js` | createEditorComponent関数によるコンフィグ正規化ロジック |

**読解のコツ**: bind関数のヘルパー（6-8行目）は、関数をnullコンテキストにバインドするか、関数でない場合はfalseを返す。toPreviewの条件式（34行目）は短絡評価を活用した複雑なフォールバック。

**主要処理フロー**:
- **4行目**: catchesNothing = /.^/ - 何にもマッチしない正規表現（デフォルトパターン）
- **6-8行目**: bind(fn) - isFunction(fn) && fn.bind(null)ヘルパー
- **10行目**: export default function createEditorComponent(config)
- **11-22行目**: destructuringでid, label, icon, type, widget, pattern, fields, fromBlock, toBlock, toPreviewを抽出（デフォルト値設定）
- **25-38行目**: 正規化オブジェクトの構築
  - **26行目**: id生成 - label.replace(/[^A-Z0-9]+/gi, '_')
  - **32行目**: fromBlock - bind(fromBlock) || (() => ({}))
  - **33行目**: toBlock - bind(toBlock) || (() => 'Plugin')
  - **34行目**: toPreview - bind(toPreview) || (!widget && (bind(toBlock) || (() => 'Plugin')))
  - **35行目**: fields - fromJS(fields)でImmutable化

#### Step 2: レジストリ登録ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | registerEditorComponent/getEditorComponentsのImmutableMap操作 |

**主要処理フロー**:
- **28行目**: editorComponents: Map() - Immutable.jsのMapで初期化
- **154-169行目**: registerEditorComponent
  - **155行目**: const plugin = EditorComponent(component) - 正規化
  - **156-164行目**: code-blockタイプの唯一性チェック
    - **157行目**: registry.editorComponents.find(c => c.type === 'code-block')
    - **159-163行目**: 既存あれば警告 + delete
  - **168行目**: registry.editorComponents = registry.editorComponents.set(plugin.id, plugin)
- **170-172行目**: getEditorComponents - registry.editorComponentsをそのまま返す

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

```
CMS.registerEditorComponent(config)
    |
    +-- EditorComponent(config) [valueObjects/EditorComponent.js]
    |       +-- id生成 (label.replace)
    |       +-- bind(fromBlock), bind(toBlock), bind(toPreview)
    |       +-- fromJS(fields) [Immutable.js]
    |       +-- { id, label, type, icon, widget, pattern, fields, fromBlock, toBlock, toPreview }
    |
    +-- registerEditorComponent(plugin) [registry.js]
            +-- code-block唯一性チェック
            |       +-- registry.editorComponents.find(c => c.type === 'code-block')
            |       +-- 既存あれば console.warn + delete
            |
            +-- registry.editorComponents.set(plugin.id, plugin)
```

### データフロー図

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

コンフィグオブジェクト      -->  EditorComponent()             -->  正規化されたプラグイン
  { id, label, type,             (デフォルト値設定、                { id, label, type,
    pattern, fromBlock,           bind処理、fromJS)                 pattern, fromBlock,
    toBlock, toPreview,                |                            toBlock, toPreview,
    fields }                           v                            fields: List }
                              registerEditorComponent()              |
                                   |                                 v
                              code-block                       ImmutableMap
                              唯一性チェック                     (id -> plugin)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| registry.js | `packages/decap-cms-core/src/lib/registry.js` | ソース | registerEditorComponent/getEditorComponents実装 |
| EditorComponent.js | `packages/decap-cms-core/src/valueObjects/EditorComponent.js` | ソース | エディタコンポーネント正規化 |
| index.js | `packages/decap-cms-editor-component-image/src/index.js` | ソース | 画像エディタコンポーネント（標準提供） |
