「FlaskでMermaid簡易エディタを作る|テンプレート機能をデータ駆動で実装する」の内容を表す技術イラスト
UNIT 00LAB NOTES FIG.00 · EYECATCH

FlaskでMermaid簡易エディタを作る|テンプレート機能をデータ駆動で実装する

FlaskとMermaid.jsで作る簡易エディタに、フローチャート・シーケンス図・状態遷移図のテンプレート機能を追加します。JavaScriptのデータと処理を分離し、選択UIの自動生成、上書き確認、既存描画処理との統合から将来のJSON・DB化まで解説します。

DOC NO.EKP-00-1030
DATE
REVB
READ約 17 分
目次

今回の到達点

Day 5までに、Flaskで配信するMermaid簡易エディタは、コード入力・リアルタイムプレビュー・構文エラー表示・.mmd保存と読込・SVG出力・クリップボードコピーに対応しました。ところが、新しい図を描くたびに、flowchartやsequenceDiagramを最初から入力するのは手間です。

Day 6では、フローチャート・シーケンス図・状態遷移図を選択するだけで、編集可能なひな形を呼び出す機能を追加します。目標は単なる選択メニューではありません。図の定義を「データ」として独立させ、将来のJSON・データベース管理にもつながる構造を作ります。

flowchart LR
    A[テンプレートデータ] --> B[選択メニュー]
    B --> C[textarea]
    C --> D[scheduleRender]
    D --> E[Mermaidプレビュー]

この図のポイントは、テンプレートからプレビューへ直接描画しないことです。入力経路を増やしても、描画処理はDay 3から使っているscheduleRender()に集約します。

なぜデータ駆動にするのか

条件分岐で増やす方法の限界

最初は次のように書きたくなります。

if (selected === "flowchart") {
    inputElement.value = "flowchart LR\nA --> B";
} else if (selected === "sequence") {
    inputElement.value = "sequenceDiagram\nA->>B: Hello";
}

2種類なら動きます。しかし図が10種類、30種類と増えると、テンプレートを追加するたびに処理の条件分岐を編集することになります。図の内容と、選択・反映という操作が混ざっているためです。

データ駆動では、図の種類をオブジェクトの配列に登録し、処理側はどのテンプレートでも同じ関数を使います。

責務 担当 変更する場面
テンプレートの内容 templates.js 図の種類を追加・修正する
選択UI index.htmlとeditor.js 選び方を変更する
入力欄の更新 editor.js 適用条件を変更する
構文検証・描画 既存のscheduleRender() 描画仕様を変更する
ファイル保存・SVG出力 Day 4・5の既存関数 出力仕様を変更する

テンプレートを1件追加しても、選択イベントのロジックを変更しなくてよいことが、今回の設計目標です。

既存プロジェクトへの追加箇所

Day 5のプロジェクトを引き継ぎ、JavaScriptファイルを1つ追加します。

mermaid-editor/
├── app.py
├── requirements.txt
├── templates/
│   └── index.html
└── static/
    ├── css/
    │   └── style.css
    └── js/
        ├── editor.js
        └── templates.js  ← 今回追加

Flask側のapp.pyは変更しません。Flaskは従来どおりHTMLと静的ファイルを配信し、テンプレートの適用とMermaid描画はブラウザで処理します。Flaskの公式ドキュメントでも、JavaScriptはstaticディレクトリから配信する静的ファイルの例として扱われています。

テンプレートをデータとして定義する

templates.jsを新規作成する

static/js/templates.jsを作成し、次の内容を保存します。

export const templates = [
    {
        id: "flowchart-basic",
        title: "フローチャート",
        description: "処理と条件分岐の基本形です。",
        source: `flowchart LR
    A[Start] --> B{Ready?}
    B -->|Yes| C[Finish]
    B -->|No| D[Review]
    D --> B`
    },
    {
        id: "sequence-basic",
        title: "シーケンス図",
        description: "利用者とWebアプリの通信順序を表します。",
        source: `sequenceDiagram
    participant U as User
    participant W as Web App
    U->>W: Request
    W-->>U: Response`
    },
    {
        id: "state-basic",
        title: "状態遷移図",
        description: "待機・動作・停止の状態変化を表します。",
        source: `stateDiagram-v2
    [*] --> Idle
    Idle --> Running: start
    Running --> Idle: reset
    Running --> [*]: stop`
    }
];

export function getTemplateById(id) {
    return templates.find((template) => template.id === id) ?? null;
}

idは内部識別子、titleはメニューに表示する名前、descriptionは用途の説明、sourceはMermaidのコード全文です。複数行文字列にはJavaScriptのテンプレートリテラル(バッククォート)を使っています。

getTemplateById()は配列のfind()で該当データを検索し、見つからなければnullを返します。idは重複しないように管理します。画面に表示する日本語名を変更しても、内部識別子は維持できる設計です。

Mermaid公式資料では、flowchart、sequenceDiagram、stateDiagram-v2がそれぞれの図の記法として案内されています。今回の3例は、その基本構文を使っています。

JSON化しやすい構造にする

この配列の各要素は、文字列だけで構成された通常のJavaScriptオブジェクトです。たとえば、将来は次のようなJSONレコードとして管理できます。

{
  "id": "flowchart-basic",
  "title": "フローチャート",
  "description": "処理と条件分岐の基本形です。",
  "source": "flowchart LR\n    A[Start] --> B[Finish]"
}

JSONでは改行を\nとして表現します。今はビルドツールもAPIも増やさず、ES Modulesで読み込めるtemplates.jsを採用します。後日、データの取得元をJSONやDBへ変更しても、id・title・description・sourceという契約を維持すればUI側の変更を抑えられます。

HTMLに選択メニューを追加する

Day 5の操作ボタンは残す

templates/index.htmlの<div class="file-actions">内で、Copy Codeボタンの後、閉じる</div>の前に次を追加します。既存のOpen・Save・Export・Copyボタンは削除しません。

<div class="template-controls">
    <label for="template-select">図のひな形</label>
    <select id="template-select">
        <option value="">テンプレートを選択</option>
    </select>
</div>

<select>は候補を選ぶ標準的なHTML要素です。候補の<option>は後でJavaScriptから生成するため、HTMLには初期案内だけを書きます。

また、Day 5で追加したaction-statusの直後に説明表示欄を追加します。

<p id="template-description"
   class="template-description"
   aria-live="polite"></p>

action-statusは操作結果、template-descriptionは選択したひな形の用途を表示するための領域です。役割を分けることで、エラーメッセージや保存結果と混同しません。

CSSを追加する

static/css/style.cssの末尾に追記します。

.template-controls {
    display: flex;
    align-items: center;
    gap: 8px;
}

.template-controls label {
    white-space: nowrap;
    font-size: 13px;
}

.template-controls select {
    max-width: 220px;
    padding: 8px;
    border: 1px solid #cccccc;
    border-radius: 6px;
    background: #ffffff;
}

.template-description {
    margin: 0;
    padding: 0 24px 8px;
    font-size: 13px;
    color: #555555;
}

Day 4で設定した.file-actions { flex-wrap: wrap; }もそのまま残します。画面幅が狭いとき、ボタンと選択メニューが折り返せる構成です。

editor.jsへテンプレート処理を追加する

モジュールを読み込む

Day 5のstatic/js/editor.jsの先頭にあるMermaidのimportと並べて、次の1行を追加します。

import { templates, getTemplateById } from "./templates.js";

./templates.jsはeditor.jsから見た相対パスです。既存のindex.htmlでは、editor.jsをtype="module"で読み込んでいる前提です。通常の<script>として読み込むとimportが使えないため、ここを確認してください。

DOM要素を取得する

Day 5のDOM取得処理の近くに、次を追加します。

const templateSelect = document.getElementById("template-select");
const templateDescription = document.getElementById("template-description");

データから選択肢を生成する

次の関数をeditor.jsへ追加します。

function populateTemplateSelect() {
    for (const template of templates) {
        const option = document.createElement("option");
        option.value = template.id;
        option.textContent = template.title;
        templateSelect.appendChild(option);
    }
}

配列をfor...ofで走査し、1件につき1つのoptionを作ります。表示名にはtextContentを使い、テンプレートの文字列をHTMLとして解釈させません。

これにより、テンプレートを追加する際は**templates.jsへデータを1件追加するだけ**で、メニューにも反映されます。

選択時に既存コードを保護する

テンプレートを選ぶと、現在のtextareaを置き換えます。入力途中の図を誤って消さないよう、既存コードがある場合は確認ダイアログを出します。

function handleTemplateSelection() {
    const id = templateSelect.value;
    templateSelect.value = "";

    if (!id) {
        return;
    }

    const template = getTemplateById(id);

    if (!template) {
        setStatus("指定したテンプレートが見つかりません。");
        return;
    }

    const currentSource = inputElement.value;

    if (currentSource.trim() && currentSource !== template.source) {
        const accepted = window.confirm(
            "現在のコードをテンプレートで置き換えます。" +
            "必要なら先に.mmdとして保存してください。続行しますか?"
        );

        if (!accepted) {
            setStatus("テンプレートの適用を中止しました。");
            return;
        }
    }

    inputElement.value = template.source;
    templateDescription.textContent = template.description;
    scheduleRender();
    setStatus(`「${template.title}」を適用しました。`);
    inputElement.focus();
}

window.confirm()はユーザーの選択に応じてtrueまたはfalseを返します。キャンセルした場合、入力欄は変更しません。なお、これは未保存データを自動復旧する機能ではありません。重要な図は先に.mmdとして保存してください。

templateSelect.value = ""で選択状態を戻しているため、同じテンプレートを後から再選択できます。選択直後にinputElement.valueを更新してもinputイベントは自動発火しないため、scheduleRender()を明示的に呼ぶことが重要です。

初期化とイベント登録

editor.jsの末尾にある既存イベント登録と初期描画の近くへ、次の2行を追加します。populateTemplateSelect()はページ読込時に1回だけ呼びます。

populateTemplateSelect();
templateSelect.addEventListener("change", handleTemplateSelection);

changeイベントは、ユーザーが選択内容を確定したときに発生します。Day 5のinput、click、ファイル読込のイベント登録は維持してください。

実装後のデータの流れ

今回追加したのは、入力欄へ文字列を渡す入口です。既存の保存・出力処理はそのまま使えます。

テンプレート選択
    ↓
getTemplateById(id)
    ↓
上書き確認
    ↓
inputElement.value = template.source
    ↓
scheduleRender()
    ↓
構文検証 → SVGプレビュー
    ├── .mmd保存
    ├── SVG出力
    └── コードコピー

Day 3のrenderSequenceによる古い描画結果の排除、Day 5のrenderedSourceによる古いSVGの誤出力防止も引き続き有効です。テンプレート専用の描画関数を作らないことが、機能追加時の不具合を減らします。

実行方法と確認手順

Flaskを起動します。

python -m flask --app app run --debug

ブラウザでhttp://127.0.0.1:5000/を開き、次の順に確認してください。

  1. 選択メニューに「フローチャート」「シーケンス図」「状態遷移図」が並ぶ。
  2. 入力欄が空なら、テンプレート選択後に確認なしでコードが入る。
  3. 既存コードがある場合、確認ダイアログでキャンセルすると内容が保持される。
  4. 続行するとコードが置き換わり、少し待つとプレビューが更新される。
  5. 適用後も.mmd保存、SVG出力、Copy Codeが利用できる。
  6. 同じテンプレートをもう一度選べる。

この実行環境ではJavaScriptの構文とデータ操作を検証していますが、実ブラウザでのMermaid描画と確認ダイアログの操作は未実施です。実機で上記の往復テストを行ってください。

よくある失敗と対処

Cannot use import statement outside a moduleと表示される

index.htmlでeditor.jsを通常のスクリプトとして読み込んでいる可能性があります。既存の読み込みを次のように確認します。

<script type="module"
        src="{{ url_for('static', filename='js/editor.js') }}"></script>

Flaskのurl_for('static', ...)は静的ファイルのURLを生成します。templates.jsを別のscriptタグで二重読込する必要はありません。

メニューは表示されるが選択肢が増えない

populateTemplateSelect()を呼んだか、template-selectのIDが一致しているか確認します。ブラウザの開発者ツールでJavaScriptの読み込みエラーも確認してください。

選択してもプレビューが更新されない

inputElement.valueを変更しただけでは、キーボード入力用のinputイベントは発生しません。scheduleRender()を呼び、Day 3の描画パイプラインへ渡してください。

テンプレートを選ぶと作業中の図が消える

上書き確認をキャンセルした場合、コードは変更されない設計です。確認を受け入れた場合は置き換わるため、必要なら先に.mmdとして保存します。ブラウザを閉じても自動保存される仕組みは、今回まだ実装していません。

図が構文エラーになる

sourceの先頭行が図の種類に合っているか、改行・矢印・コロンが欠けていないか確認します。テンプレートの内容もMermaidの構文検証対象です。JavaScriptの構文が正しくても、Mermaidの記法が正しいとは限りません。

練習問題

練習1:テンプレートを1件追加する

templates.jsへ、次のような簡単なフローチャートを追加してください。

{
    id: "review-flow",
    title: "レビュー工程",
    description: "設計レビューの流れを表します。",
    source: `flowchart TD
    A[Design] --> B[Review]
    B --> C[Release]`
}

確認観点: editor.jsの条件分岐を変更せず、選択メニューに新しい項目が増えること。追加時は既存配列の要素間にカンマが必要です。

練習2:上書き防止を確認する

入力欄へ任意のMermaidコードを入力し、別のテンプレートを選択して確認ダイアログをキャンセルしてください。

解答・確認観点: 入力内容は変更されず、ステータスに中止メッセージが表示されます。必要なら.mmdへ保存してから適用します。

練習3:データとロジックを区別する

titleだけを変更した場合と、sourceだけを変更した場合で、どこに変化が現れるか予想してください。

解答例: titleは選択メニューの表示名に影響し、sourceはtextareaに反映されるコードと生成される図に影響します。イベント処理そのものは変更しません。

実務への接続と今後の拡張

テンプレート機能は、Mermaid以外の小さな業務ツールにも応用できます。たとえば、設計レビュー手順、API通信フロー、設備の状態遷移を、同じid・title・description・sourceの形式で登録できます。

将来、テンプレート数が増えたら、カテゴリ、バージョン、更新日などの項目を追加し、JSONファイルやデータベースへ移せます。ただし、データを増やす前に識別子の一意性、入力上書き時の確認、構文検証、保存との整合性をテストすることが重要です。

今回の到達点は「選択肢が3つ増えたこと」ではなく、機能の追加をロジックの修正からデータの追加へ移したことです。

まとめ

Day 6では、テンプレート定義をtemplates.jsへ分離し、選択メニューをデータから自動生成しました。テンプレートを適用するときは、既存コードを保護する確認を挟み、Day 3から続くscheduleRender()へ渡します。

Flask側は変更せず、Day 4の.mmd保存・読込、Day 5のSVG出力・コピーも再利用します。これで、図の種類を追加するために描画ロジックを触る必要がなくなりました。

参考資料

  • Mermaid公式:フローチャートの構文
  • Mermaid公式:シーケンス図の構文
  • Mermaid公式:状態遷移図の構文
  • Flask公式:静的ファイルの配信
  • MDN:changeイベントとHTMLSelectElement
参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次