GASとD2.jsとCodeMirrorで実現する簡易ダイアグラムエディタ

# はじめに

[前回の記事では Google Apps Script (GAS) と D2.js で実現するダイアグラムビューア](https://qiita.com/rairaii/items/ea38a9ad6e7c4d31dd8a)を作成しました。

今回はその発展として、ウェブブラウザ上で **D2コード** を書いた瞬間に **SVG** が更新される **簡易 D2 エディタ** を実装します。

エディタはすべて ウェブブラウザ と GAS だけで完結するため、その他のサーバーは不要です。


# ゴール

![d2js_editor.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/f92d000d-223f-47f0-a023-8d33d7254441.gif)

左にコード、右に即時レンダリング結果が表示されるシンプルな構成です。

# 技術スタックの役割

| 技術                           | レイヤー            | 主な役割                                           |
| ---------------------------- | --------------- | ---------------------------------------------- |
| **D2.js**           | クライアント（描画エンジン）  | D2 言語の記述をリアルタイムで SVG ダイアグラムに変換                 |
| **CodeMirror 6**             | クライアント（UI・エディタ） | D2 スクリプトを記述・編集・リアルタイムプレビューのトリガー                |
| **GAS** | サーバー（HTML 配信）   | Web UI を提供し、必要に応じて Google ドキュメントなどから D2 コードを取得 |



# プロジェクト作成手順

1. **GAS プロジェクトを新規作成**  
   - Apps Script エディタで「新しいプロジェクト」を選択
2. **`Code.gs` を追加**  
   ```javascript:Code.gs
   function doGet() {
     return HtmlService.createHtmlOutputFromFile('index');
   }
   ```
3. **`index.html` を追加**（次章参照）  
4. **[デプロイ] → [ウェブアプリ]**  
   - 「新しいデプロイ」「アクセスを全員」に設定し URL を取得  
   - これでブラウザからアクセス可能

# 作成した index.html ファイル

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>D2.js エディタ</title>
  <style>
    /* 全体のフォントと余白設定 */
    body {
      font-family: sans-serif;
      padding: 1em;
    }

    /* エディタと描画結果を左右に並べる */
    .container {
      display: flex;
      gap: 1em;
      align-items: flex-start;
    }

    /* 左側：CodeMirror エディタ */
    #editor {
      width: 50%;
      border: 1px solid #ccc;
    }

    /* 右側：SVG描画エリア */
    #diagram {
      width: 50%;
      overflow: hidden;
    }

    /* SVGにボーダーを付けて見やすく、サイズをウィンドウに対して自動調整 */
    #diagram svg {
      box-sizing: border-box;
      width: 100%;
      height: 80vh;
      display: block;
      border: 1px solid #ccc;
    }
  </style>

  <!-- CodeMirror の状態管理モジュールをインポート -->
  <script type="importmap">
  {
    "imports": {
      "@codemirror/state": "https://esm.sh/@codemirror/state@6.4.1"
    }
  }
  </script>
</head>
<body>
  <h2>D2.js 簡易エディタ</h2>
  <div class="container">
    <div id="editor"></div>
    <div id="diagram"></div>
  </div>

  <script type="module">
    // CodeMirror 用のモジュールを読み込み
    import { EditorView, basicSetup } from "https://esm.sh/@codemirror/basic-setup@0.20.0?external=@codemirror/state";
    import { EditorState } from "https://esm.sh/@codemirror/state@6.4.1";

    // D2.js 本体を読み込み（WASMベースの図表エンジン）
    import { D2 } from "https://esm.sh/@terrastruct/d2@0.1.23";

    const diagramDiv = document.getElementById("diagram");

    // 初期図表コード：x → y → z の簡単なフロー
    const initialCode = `x -> y
y -> z
x: 開始
y: 処理
z: 終了`;

    // エディタ内容が変更されたら再描画
    const updateListener = EditorView.updateListener.of((update) => {
      if (update.docChanged) {
        renderDiagram(update.state.doc.toString());
      }
    });

    // CodeMirror エディタを初期化
    const editor = new EditorView({
      state: EditorState.create({
        doc: initialCode,
        extensions: [basicSetup, updateListener],
      }),
      parent: document.getElementById("editor"),
    });

    const d2 = new D2(); // D2.js インスタンス生成

    // SVG描画を行う関数
    async function renderDiagram(code) {
      try {
        const result = await d2.compile(code); // D2コードをコンパイル
        const svg = await d2.render(result.diagram, result.renderOptions); // SVG出力
        diagramDiv.innerHTML = svg; // HTMLに表示
      } catch (err) {
        diagramDiv.innerHTML = `<pre style="color:red;">${err.message}</pre>`; // エラー表示
      }
    }

    // 初回描画
    renderDiagram(initialCode);
  </script>
</body>
</html>
```


:::note info
**ポイント**  
- `importmap` を使うことでモジュール同士の依存解決が簡潔  
- エディタ更新を `updateListener` でフックし `renderDiagram()` を呼ぶだけ  
- 失敗時はエラーメッセージを SVG 領域に赤字表示
:::

# サンプル D2 コードと結果

以下のコードをエディタに貼ると即座にフロー図が再描画されます。

```text: サンプル
Start: 開始
Input: 入力確認
Decision: 正常？
ProcessA: データ登録
ProcessB: エラー処理
End: 終了

Start -> Input -> Decision
Decision -> ProcessA: はい
Decision -> ProcessB: いいえ
ProcessA -> End
ProcessB -> End
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/883ddd62-361b-4fdd-8304-08694a67254d.png)


# まとめ

これで、GAS とブラウザだけで完結する **リアルタイム D2 ダイアグラムエディタ** が完成しました

次のステップとして編集したダイアグラムをロード・セーブする機能を追加します

https://qiita.com/rairaii/items/147f89804179319f8f21