# 帳票設計書 2-テキストエクスポート

## 概要

本ドキュメントは、Etherpadのパッド（共同編集ドキュメント）をプレーンテキスト形式でエクスポートする機能に関する設計書である。書式情報を除去し、純粋なテキストデータとして取得するための仕様を定義する。

### 本帳票の処理概要

テキストエクスポート機能は、Etherpadのパッドに含まれるリッチテキストコンテンツから書式情報を除去し、プレーンテキスト形式に変換してダウンロード可能なファイルとして提供する処理を行う。

**業務上の目的・背景**：多くのシステムやアプリケーションではプレーンテキストのみを受け付ける場合がある。メールの本文、コマンドライン処理、テキストエディタでの編集、データ移行など、書式情報が不要または障害となる場面において、シンプルなテキスト形式でのエクスポートが必要となる。また、ファイルサイズを最小化したい場合や、長期保存・アーカイブ目的でも利用される。

**帳票の利用シーン**：ユーザーがパッドの内容を他のテキストベースのシステムに取り込みたい場合、メモ帳やVimなどのテキストエディタで開きたい場合、プログラムで処理したい場合、またはシンプルな形式でバックアップを取りたい場合に利用される。議事録のテキスト版作成、ログ記録、Wikiへの転記などの用途がある。

**主要な出力内容**：
1. パッドのテキストコンテンツ（全文、書式情報なし）
2. リスト構造のテキスト表現（タブインデント+番号/箇条書き記号）
3. 箇条書きリストのアスタリスク表現
4. 番号付きリストの階層番号表現（例：1.、1.1.、1.1.1.）

**帳票の出力タイミング**：ユーザーがパッドページにアクセスし、エクスポートメニューからテキスト形式を選択した時点で即座に生成・ダウンロードされる。特定のリビジョン（過去バージョン）を指定してエクスポートすることも可能。

**帳票の利用者**：パッドにアクセス権限を持つ全てのユーザー（編集者、閲覧者）が利用可能。読み取り専用アクセスの場合も、readOnlyIdを使用してエクスポートできる。

## 帳票種別

ドキュメントエクスポート（プレーンテキスト形式）

## 利用画面

| 画面No | 画面名 | URL/ルーティング | 出力操作 |
|--------|--------|-----------------|---------|
| - | パッド編集画面 | `/p/:pad/export/txt` | エクスポートメニューから「Plain text」を選択 |
| - | パッド編集画面（リビジョン指定） | `/p/:pad/:rev/export/txt` | エクスポートメニューから「Plain text」を選択（リビジョン指定） |

## 出力形式

### 基本仕様

| 項目 | 内容 |
|-----|------|
| ファイル形式 | TXT（プレーンテキスト） |
| 用紙サイズ | 該当なし |
| 向き | 該当なし |
| ファイル名 | `{padId}.txt` または `{readOnlyId}.txt`（フックで上書き可能） |
| 出力方法 | ダウンロード（`Content-Disposition: attachment`） |
| 文字コード | UTF-8 |
| 改行コード | LF（`\n`） |

### テキスト固有設定

| 項目 | 内容 |
|-----|------|
| 書式情報 | 完全に除去 |
| インデント | タブ文字（`\t`）で表現 |
| 箇条書き | `* ` で表現 |
| 番号付きリスト | `1. `、`1.1. `、`1.1.1. ` 形式 |

## 帳票レイアウト

### レイアウト概要

テキストファイルは純粋なテキストデータのみで構成され、HTMLのような構造的要素は含まない。

```
┌─────────────────────────────────────┐
│         プレーンテキスト本文          │
│                                     │
│  通常のテキスト行                     │
│  通常のテキスト行                     │
│  	* 箇条書き項目（タブ+アスタリスク）  │
│  	1. 番号付き項目                    │
│  		1.1. ネストされた番号付き項目   │
│                                     │
└─────────────────────────────────────┘
```

### 本文部

| No | 項目名 | 説明 | データ取得元 | 表示形式 |
|----|-------|------|-------------|---------|
| 1 | 通常行 | 書式なしテキスト行 | atext.text | `テキスト\n` |
| 2 | 箇条書き | 箇条書きリスト項目 | list属性(bullet) | `\t* テキスト\n` |
| 3 | 番号リスト（レベル1） | 番号付きリスト項目 | list属性(number1) | `\t1. テキスト\n` |
| 4 | 番号リスト（レベル2） | ネストされた番号付き | list属性(number2) | `\t\t1.1. テキスト\n` |
| 5 | 番号リスト（レベル3） | 深いネスト | list属性(number3) | `\t\t\t1.1.1. テキスト\n` |

## 出力条件

### 抽出条件

| 条件名 | 説明 | 必須 |
|-------|------|-----|
| パッドID | エクスポート対象のパッド識別子 | Yes |
| リビジョン番号 | 特定バージョンを指定（省略時は最新） | No |
| アクセス権限 | パッドへの読み取り権限 | Yes |

### ソート順

| 優先度 | 項目 | 昇順/降順 |
|-------|------|---------|
| 1 | 行番号 | 昇順（テキスト出現順） |

### 改ページ条件

該当なし（テキストファイルにはページ概念がない）

## データベース参照仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 結合条件 |
|-----------|------|---------|
| pad:{padId} | パッドメタ情報 | キー直接参照 |
| pad:{padId}:revs:{revNum} | リビジョン履歴 | リビジョン番号指定時 |

### テーブル別参照項目詳細

#### pad:{padId}

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| atext.text | 本文テキスト | 常時 | 最新テキスト |
| atext.attribs | 属性情報 | 常時 | リスト情報のみ使用 |
| pool | 属性プール | 常時 | 属性ID→属性値マッピング |

## 計算仕様

### 計算項目一覧

| 項目名 | 計算式 | 端数処理 | 備考 |
|-------|-------|---------|------|
| リスト番号 | listNumbers[level]++ | - | レベル毎にカウント |
| ネストレベル | list属性の末尾数字を解析 | - | 正規表現 `([a-z]+)([0-9]+)` |
| 階層番号文字列 | `listNumbers[1].listNumbers[2]....` | - | 例: `1.2.3.` |

## 処理フロー

### 出力フロー

```mermaid
flowchart TD
    A[エクスポートリクエスト<br>/p/:pad/export/txt] --> B{ファイル形式チェック}
    B -->|txt| C[アクセス権限確認]
    B -->|サポート外| Z[404エラー]
    C -->|権限なし| Y[アクセス拒否]
    C -->|権限あり| D{readOnlyId?}
    D -->|Yes| E[readOnlyIdからpadId取得]
    D -->|No| F[padIdを使用]
    E --> G[パッド存在確認]
    F --> G
    G -->|存在しない| X[404エラー]
    G -->|存在する| H{リビジョン指定?}
    H -->|Yes| I[指定リビジョンのatext取得]
    H -->|No| J[最新atextを使用]
    I --> K[getTXTFromAtext実行]
    J --> K
    K --> L[各行を解析]
    L --> M{リスト行?}
    M -->|Yes| N[リスト形式に変換]
    M -->|No| O[そのまま出力]
    N --> P[インデント追加]
    P --> Q[番号/箇条書き記号追加]
    Q --> R[テキスト結合]
    O --> R
    R --> S[Content-Disposition設定]
    S --> T[HTTPレスポンス送信]
```

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 表示メッセージ | 対処方法 |
|----------|---------|--------------|---------|
| パッド不存在 | 指定されたpadIdが存在しない | 404 Not Found | ログ出力後、404レスポンス |
| アクセス拒否 | パッドへの読み取り権限がない | アクセス権に応じた処理 | hasPadAccess関数で判定 |
| 無効なリビジョン | 存在しないリビジョン番号 | エラー | checkValidRevで検証 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定データ件数 | 1パッド（行数上限なし） |
| 目標出力時間 | 1秒以内（テキスト処理のため高速） |
| 同時出力数上限 | レートリミッター設定による（settings.importExportRateLimiting） |

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

- **アクセス制御**: hasPadAccess()によるパッド単位のアクセス権限チェック
- **レートリミッター**: DoS攻撃防止のためエクスポートリクエスト数を制限
- **特殊文字処理**: 制御文字（コードポイント12等）の除去は実装されていない（コメントアウト）
- **出力データ**: プレーンテキストのためXSS等のリスクは低い

## 備考

- HTMLエクスポートと異なり、書式情報（太字、斜体等）は完全に除去される
- リストのネストレベルが下がる際は、上位レベルのカウンタがリセットされる
- 箇条書きリストと番号付きリストは別々に処理され、切り替わり時にカウンタがリセットされる
- コード内のコメントによると、特殊文字除去処理は意図的にコメントアウトされている

---

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

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

### 推奨読解順序

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

パッドのテキストと属性情報の構造を理解することが最優先。テキストエクスポートではリスト属性のみを参照する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | PadType.ts | `src/node/types/PadType.ts` | PadType型定義、atext(text+attribs)構造 |
| 1-2 | ExportHelper.ts | `src/node/utils/ExportHelper.ts` | _analyzeLine関数、list属性の解析方法 |

**読解のコツ**: テキストエクスポートでは書式属性（bold等）は無視され、list属性のみがインデントと番号生成に使用される。

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

HTTPリクエストからエクスポート処理への流れを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | importexport.ts | `src/node/hooks/express/importexport.ts` | Expressルーティング、形式判定 |
| 2-2 | ExportHandler.ts | `src/node/handler/ExportHandler.ts` | doExport関数、`type === 'txt'` 分岐 |

**主要処理フロー**:
1. **29行目**: ルート定義 `/p/:pad{/:rev}/export/:type`
2. **72-74行目**: txtの場合は直接exporttxt.getPadTXTDocumentを呼び出し
3. **74行目**: res.send(txt) でレスポンス返却

#### Step 3: テキスト生成ロジックを理解する

ExportTxt.tsがテキストエクスポートのコアロジック。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | ExportTxt.ts | `src/node/utils/ExportTxt.ts` | getPadTXTDocument、getTXTFromAtext関数 |

**主要処理フロー**:
- **33-43行目**: getPadTXT - atextをテキストに変換
- **47-264行目**: getTXTFromAtext - メイン変換ロジック
- **52行目**: props配列（heading1〜strikethroughは参照のみ、出力には使用しない）
- **64-185行目**: getLineTXT - 1行のテキスト生成（書式タグ生成処理はあるが結果に反映されない）
- **198-261行目**: リスト番号生成ロジック

#### Step 4: リスト番号生成ロジックを理解する

番号付きリストの階層番号表現が特徴的。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | ExportTxt.ts | `src/node/utils/ExportTxt.ts` | listNumbers変数、階層番号文字列生成 |

**主要処理フロー**:
- **198行目**: listNumbers オブジェクト（レベル別カウンタ）
- **205-207行目**: bulletリストは `* ` プレフィックス
- **209-214行目**: numberリスト以外ではカウンタリセット
- **238-239行目**: レベルが下がった時のカウンタ削除
- **242-253行目**: 階層番号文字列生成（例: `1.2.3. `）

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

```
HTTPリクエスト /p/:pad/export/txt
    │
    ├─ importexport.ts (expressCreateServer)
    │      └─ args.app.get('/p/:pad{/:rev}/export/:type')
    │             │
    │             ├─ hasPadAccess() - アクセス権確認
    │             ├─ readOnlyManager.getPadId() - ID解決
    │             ├─ padManager.doesPadExists() - 存在確認
    │             └─ exportHandler.doExport()
    │
    └─ ExportHandler.ts (doExport)
           │
           ├─ hooks.aCallFirst('exportFileName') - ファイル名フック
           ├─ res.attachment() - ダウンロード設定
           │
           └─ (type === 'txt') の場合:
                  │
                  └─ ExportTxt.ts
                         │
                         └─ getPadTXTDocument()
                                ├─ padManager.getPad()
                                └─ getPadTXT()
                                       └─ getTXTFromAtext()
                                              ├─ splitAttributionLines()
                                              ├─ _analyzeLine() [ExportHelper.ts]
                                              ├─ getLineTXT() - 各行変換
                                              └─ リスト番号生成ロジック
```

### データフロー図

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

pad:{padId}          ┌─────────────────┐
  │                  │                 │
  ├─ atext.text ────▶│  getTXTFrom     │
  │                  │    Atext()      │
  └─ atext.attribs ─▶│                 │
                     │  ┌───────────┐  │
AttributePool ──────▶│  │_analyzeLine│  │──▶ プレーンテキスト
                     │  │ リスト解析 │  │     （書式情報なし）
                     │  └───────────┘  │
                     │  ┌───────────┐  │
                     │  │getLineTXT │  │
                     │  │ 文字抽出  │  │
                     │  └───────────┘  │
                     │  ┌───────────┐  │
                     │  │リスト番号  │  │
                     │  │ 生成     │  │
                     │  └───────────┘  │
                     └─────────────────┘
                              │
                              ▼
                     ┌─────────────────┐
                     │  pieces.join('')  │──▶ .txt ファイル
                     │  (文字列結合)     │
                     └─────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| importexport.ts | `src/node/hooks/express/importexport.ts` | ソース | Expressルーティング定義 |
| ExportHandler.ts | `src/node/handler/ExportHandler.ts` | ソース | エクスポート処理の振り分け |
| ExportTxt.ts | `src/node/utils/ExportTxt.ts` | ソース | テキスト変換ロジック本体 |
| ExportHelper.ts | `src/node/utils/ExportHelper.ts` | ソース | 行解析ヘルパー関数 |
| PadType.ts | `src/node/types/PadType.ts` | 型定義 | パッドデータ型 |
| PadManager.ts | `src/node/db/PadManager.ts` | ソース | パッドデータ取得 |
| Changeset.ts | `src/static/js/Changeset.ts` | ソース | 属性文字列パーサー |
| StringIterator.ts | `src/static/js/StringIterator.ts` | ソース | 文字列イテレータ |
| StringAssembler.ts | `src/static/js/StringAssembler.ts` | ソース | 文字列結合ユーティリティ |
| Settings.ts | `src/node/utils/Settings.ts` | 設定 | レートリミッター設定等 |
