FlaskでMermaid簡易エディタを作る|リアルタイム描画と構文エラー処理

「FlaskでMermaid簡易エディタを作る|リアルタイム描画と構文エラー処理」の内容を表す技術イラスト
目次

今回の到達点

Day 2では、左側にMermaidコード、右側にプレビューを配置する2ペインUIを作りました。

処理の流れは次の状態です。

textarea
↓
Mermaidコード
↓
mermaid.render()
↓
SVG
↓
Preview

ただし、Day 2ではページを開いたときに一度だけ描画していました。

今回はこれを、

ユーザーが入力
↓
少し待つ
↓
構文を検証
↓
正しければ描画
↓
間違っていればエラー表示
↓
修正すれば正常状態へ復帰

というリアルタイムエディタへ進化させます。

今回の重要なテーマは単なるリアルタイム表示ではありません。

入力途中の不完全なコードを、アプリケーション全体の故障として扱わないこと

です。

Mermaidエディタでは、ユーザーが入力している途中に構文が一時的に壊れるのは正常な状態です。

したがって、

構文エラー
≠
アプリケーションエラー

として設計する必要があります。

Day 2から何を変更するのか

Day 2終了時の構成は次のとおりです。

mermaid-editor/
├── .venv/
├── static/
│   ├── css/
│   │   └── style.css
│   └── js/
│       └── editor.js
├── templates/
│   └── index.html
├── app.py
└── requirements.txt

Day 3でも、この構造は変更しません。

主に変更するのは、

static/js/editor.js

です。

さらにエラー表示用として、

templates/index.html

と、

static/css/style.css

へ小さな変更を加えます。

Python側のapp.pyは変更しません。

これもDay 1から続けている責務分離の結果です。

リアルタイム入力はブラウザ内の処理なので、Flaskへ仕事を追加する必要はありません。

なぜ入力するたびに即描画しないのか

最も単純な実装なら、次のようにできます。

inputElement.addEventListener("input", renderDiagram);

inputイベントは、textareaの値が変更されるたびに発生します。

したがってユーザーが、

flowchart LR

と入力するだけでも、1文字ごとに描画処理が走ります。

概念的には、

f
↓
描画

fl
↓
描画

flo
↓
描画

flow
↓
描画

となります。

これは効率がよくありません。

さらに入力途中のMermaidコードは、ほとんどの場合一時的に不完全です。

そこで今回はdebounceを使います。

debounceとは何か

debounceは、イベントが連続して発生している間は処理を実行せず、イベントが止まって一定時間経過してから一度だけ実行する方法です。

たとえば待ち時間を300msにすると、

入力
↓
100ms
入力
↓
100ms
入力
↓
300ms入力なし
↓
描画

となります。

ユーザーが連続して入力している間は描画しません。

入力が少し止まったところで一度だけ描画します。

リアルタイムエディタでは、

毎キー入力で即処理する必要はないが、操作感としてはリアルタイムに見える

程度の待ち時間を設定するのが実用的です。

今回は、

const RENDER_DELAY_MS = 300;

とします。

この値を定数として外へ出していることにも意味があります。

後から、

150
300
500

などへ調整するときに、ロジック内部を探す必要がありません。

エラー表示領域をHTMLへ追加する

templates/index.htmlのPreview側を変更します。

<section class="preview-pane">
    <h2>Preview</h2>

    <div
        id="error-message"
        class="error-message"
        role="alert"
        hidden
    ></div>

    <div id="mermaid-preview"></div>
</section>

新しく追加したのは、

<div
    id="error-message"
    class="error-message"
    role="alert"
    hidden
></div>

です。

正常時にはhidden属性によって表示しません。

構文エラーが発生した場合だけJavaScriptから表示します。

役割は明確です。

#mermaid-preview
→ 正常な図

#error-message
→ エラー情報

SVG表示領域へエラー文章まで混ぜないようにします。

エラー表示用CSSを追加する

style.cssへ次を追加します。

.error-message {
    margin-bottom: 12px;
    padding: 12px;
    border: 1px solid currentColor;
    border-radius: 6px;
    white-space: pre-wrap;
    font-family:
        Consolas,
        "Courier New",
        monospace;
    font-size: 13px;
    line-height: 1.5;
}

.error-message[hidden] {
    display: none;
}

今回はエラー表示の構造を作ることが目的なので、色には依存させていません。

重要なのは、

正常
→ 非表示

異常
→ 表示

再び正常
→ 非表示

という状態遷移です。

Mermaidの構文検証と描画を分ける

Day 2では直接、

mermaid.render(...)

を呼び出しました。

Day 3では、その前に、

mermaid.parse(...)

を使います。

Mermaid公式APIではparse()は図を描画せず、Mermaid定義の構文を検証できます。正しい場合は図の種類を含む結果を返し、無効な構文では、エラー抑制を指定していなければ例外を投げます。Mermaid

したがって処理を、

入力
↓
mermaid.parse()
↓
構文OK?
├─ No → エラー表示
└─ Yes
    ↓
    mermaid.render()
    ↓
    SVG表示

と分離できます。

これはエディタとして非常に扱いやすい構造です。

Mermaidの設定を変更する

editor.js冒頭は次のようにします。

import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@12/dist/mermaid.esm.min.mjs";

mermaid.initialize({
    startOnLoad: false,
    securityLevel: "strict",
    suppressErrorRendering: true,
});

2026年10月時点のMermaid公式Usageでは、CDN利用例はv12系を示しています。また、initialize()による設定が推奨されています。Mermaid

startOnLoad: falseはDay 2と同じです。

描画タイミングをアプリ側で管理します。

今回はさらに、

securityLevel: "strict"

を明示します。

Mermaid公式ではstrictがデフォルトのセキュリティレベルで、HTMLタグをエンコードし、クリック機能を無効化します。ユーザー入力を扱うエディタの初期設計として、理由なくlooseへ下げる必要はありません。Mermaid

そして、

suppressErrorRendering: true

を指定します。

Mermaid公式Config Schemaでは、この設定はMermaid自身がDOMへ「Syntax error」図を挿入するのを抑止するための設定として定義されています。独自UIで構文エラーを扱いたい場合に適しています。Mermaid

今回はエラー表示を自前で管理するため、この設定を使います。

DOM要素を取得する

必要な要素を取得します。

const inputElement = document.getElementById("mermaid-input");
const previewElement = document.getElementById("mermaid-preview");
const errorElement = document.getElementById("error-message");

const RENDER_DELAY_MS = 300;

let debounceTimerId = null;
let renderSequence = 0;

新しく、

debounceTimerId

と、

renderSequence

を持っています。

debounceTimerIdは待機中のタイマー管理用です。

renderSequenceは、非同期描画の競合を防ぐために使います。

エラー表示を関数化する

エラー表示処理を分離します。

function showError(message) {
    errorElement.textContent = message;
    errorElement.hidden = false;
}

正常状態へ戻す処理も分離します。

function clearError() {
    errorElement.textContent = "";
    errorElement.hidden = true;
}

これにより描画処理側では、

showError(...)

または、

clearError()

と呼ぶだけで済みます。

DOM操作の詳細を描画ロジックへ混ぜません。

エラー内容を文字列へ変換する

JavaScriptのcatchへ渡される値が、必ずしもErrorオブジェクトとは限りません。

そこで小さな変換関数を用意します。

function getErrorMessage(error) {
    if (error instanceof Error) {
        return error.message;
    }

    return String(error);
}

これによって、

catch (error) {
    showError(getErrorMessage(error));
}

とできます。

描画処理を作る

中心となる処理です。

async function renderDiagram(definition) {
    const currentSequence = ++renderSequence;

    try {
        await mermaid.parse(definition);

        const renderId = `mermaid-diagram-${currentSequence}`;

        const { svg, bindFunctions } = await mermaid.render(
            renderId,
            definition
        );

        if (currentSequence !== renderSequence) {
            return;
        }

        previewElement.innerHTML = svg;
        bindFunctions?.(previewElement);

        clearError();
    } catch (error) {
        if (currentSequence !== renderSequence) {
            return;
        }

        showError(getErrorMessage(error));
    }
}

順番に見ていきます。

まず構文を検証する

最初に、

await mermaid.parse(definition);

を実行します。

構文が正しければ次へ進みます。

構文が間違っていれば例外が発生し、catchへ移動します。

重要なのは、この段階ではまだPreviewを書き換えていないことです。

したがって入力途中で構文エラーになっても、直前の正常な図を残せます。

これはエディタとして使いやすい挙動です。

正常な図A
↓
入力途中で構文エラー
↓
図Aを保持
+
エラー表示

図を空白にしてしまうより、ユーザーは「どこまで正常だったか」を把握できます。

render IDを毎回変える

描画では、

const renderId = `mermaid-diagram-${currentSequence}`;

としています。

そして、

const { svg, bindFunctions } = await mermaid.render(
    renderId,
    definition
);

とします。

Mermaidのrender()は識別用IDと図定義を受け取り、SVGなどを返します。API利用時には返されたbindFunctionsが存在する場合、SVGをDOMへ挿入した後に呼び出す構成が公式Usageで示されています。Mermaid

今回も、

previewElement.innerHTML = svg;
bindFunctions?.(previewElement);

としています。

現在の単純なFlowchartではイベントバインドが必要ない場合もありますが、後からインタラクティブな図を扱える構造を維持します。

なぜrenderSequenceが必要なのか

ここはリアルタイム描画で重要な部分です。

mermaid.render()は非同期処理です。

そのため理論上、

入力A
↓
描画A開始

入力B
↓
描画B開始

描画B完了
↓
画面B

描画A完了
↓
画面A

という順序逆転が起きる可能性があります。

ユーザーの最新入力はBなのに、古いAが最後に表示されてしまいます。

そこで、

const currentSequence = ++renderSequence;

として各描画要求へ番号を付けます。

描画終了時に、

if (currentSequence !== renderSequence) {
    return;
}

を確認します。

古い描画ならDOMへ反映しません。

これにより、

古い非同期結果
→ 捨てる

最新の非同期結果
→ 表示する

というルールになります。

これはMermaid専用の考え方ではありません。

検索候補、API呼び出し、計算処理など、入力に追従する非同期UI全般で使える設計です。

debounce処理を作る

次に入力イベントから直接renderDiagram()を呼ばず、debounceを挟みます。

function scheduleRender() {
    if (debounceTimerId !== null) {
        clearTimeout(debounceTimerId);
    }

    debounceTimerId = setTimeout(() => {
        debounceTimerId = null;

        const definition = inputElement.value.trim();

        if (definition === "") {
            previewElement.innerHTML = "";
            clearError();
            return;
        }

        renderDiagram(definition);
    }, RENDER_DELAY_MS);
}

入力されるたびに、

clearTimeout(debounceTimerId);

で前回の予約をキャンセルします。

そして新しく、

setTimeout(...)

を予約します。

結果として、入力が300ms止まったときだけ描画処理が始まります。

空入力を特別扱いする

次の処理も重要です。

if (definition === "") {
    previewElement.innerHTML = "";
    clearError();
    return;
}

空文字列をMermaidへ渡して、

構文エラー

として扱う必要はありません。

ユーザーがコードを全部消した状態は、アプリケーションとして正常です。

したがって、

空入力
→ Previewを空にする
→ エラーなし

とします。

これは入力値の意味をアプリ側で定義する例です。

ライブラリへ何でも渡すのではなく、その前にアプリケーションのルールを置きます。

inputイベントへ接続する

最後にtextareaへイベントを登録します。

inputElement.addEventListener("input", scheduleRender);

ページ初期表示時にも一度描画します。

scheduleRender();

これで、

ページを開く
↓
初期コードを描画

ユーザーが入力
↓
300ms待機
↓
構文確認
↓
再描画

という流れになります。

Day 3の完成版editor.js

ここまでをまとめます。

import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@12/dist/mermaid.esm.min.mjs";

mermaid.initialize({
    startOnLoad: false,
    securityLevel: "strict",
    suppressErrorRendering: true,
});

const inputElement = document.getElementById("mermaid-input");
const previewElement = document.getElementById("mermaid-preview");
const errorElement = document.getElementById("error-message");

const RENDER_DELAY_MS = 300;

let debounceTimerId = null;
let renderSequence = 0;


function showError(message) {
    errorElement.textContent = message;
    errorElement.hidden = false;
}


function clearError() {
    errorElement.textContent = "";
    errorElement.hidden = true;
}


function getErrorMessage(error) {
    if (error instanceof Error) {
        return error.message;
    }

    return String(error);
}


async function renderDiagram(definition) {
    const currentSequence = ++renderSequence;

    try {
        await mermaid.parse(definition);

        const renderId = `mermaid-diagram-${currentSequence}`;

        const { svg, bindFunctions } = await mermaid.render(
            renderId,
            definition
        );

        if (currentSequence !== renderSequence) {
            return;
        }

        previewElement.innerHTML = svg;
        bindFunctions?.(previewElement);

        clearError();
    } catch (error) {
        if (currentSequence !== renderSequence) {
            return;
        }

        showError(getErrorMessage(error));
    }
}


function scheduleRender() {
    if (debounceTimerId !== null) {
        clearTimeout(debounceTimerId);
    }

    debounceTimerId = setTimeout(() => {
        debounceTimerId = null;

        const definition = inputElement.value.trim();

        if (definition === "") {
            previewElement.innerHTML = "";
            clearError();
            return;
        }

        renderDiagram(definition);
    }, RENDER_DELAY_MS);
}


inputElement.addEventListener("input", scheduleRender);

scheduleRender();

今回のJavaScriptはDay 2より長くなりました。

しかし、役割を見ると明確に分かれています。

showError()
→ エラー表示

clearError()
→ エラー解除

getErrorMessage()
→ エラー値の正規化

renderDiagram()
→ 検証と描画

scheduleRender()
→ 描画タイミング制御

一つの巨大なイベントハンドラへ全部書いていないことが重要です。

app.pyは今回も変更しない

Flask側はDay 2と同じです。

from flask import Flask, render_template


app = Flask(__name__)


@app.get("/")
def index():
    return render_template("index.html")

Day 3で追加した、

  • inputイベント
  • debounce
  • Mermaid構文検証
  • SVG再描画
  • エラー表示

はすべてブラウザ内で完結しています。

Flask公式の標準構成でも、CSSやJavaScriptはstatic配下へ置き、テンプレートはtemplatesから描画できます。Flask Documentation

Python側へ不要な責務を持ち込まない構成を維持します。

動作確認

Flaskを起動します。

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

ブラウザで、

http://127.0.0.1:5000/

を開きます。

まず正常なMermaidコードを入力します。

flowchart LR
    A[Input] --> B[Process]
    B --> C[Output]

期待結果は、

入力
↓
約300ms
↓
Preview更新

です。

次に、意図的に構文を壊します。

たとえば途中まで入力して、

flowchart LR
    A[

のような状態を作ります。

期待する状態は、

直前の正常な図
→ 残る

エラー領域
→ 表示される

です。

その後、コードを修正します。

flowchart LR
    A[Fixed] --> B[OK]

期待する状態は、

新しい図
→ 表示

エラー領域
→ 消える

です。

この、

正常
↓
異常
↓
正常

の往復ができることがDay 3の重要な完成条件です。

なぜ構文エラーでPreviewを消さないのか

構文エラー発生時に、

previewElement.innerHTML = "";

とする設計も可能です。

しかし今回は行いません。

理由は、Mermaidエディタでは構文エラーの多くが入力途中の一時状態だからです。

たとえば、

A[Pump]

を、

A[Hydraulic Pump]

へ変更している途中で一時的に括弧が閉じていない状態が発生することがあります。

そのたびに図が消えると、画面が頻繁に点滅します。

そこで、

最後に正常だったSVG
+
現在のエラー情報

を表示します。

これはエディタとしてのUXを改善します。

Mermaid自身のエラー図を使わない理由

Mermaidにはエラー発生時に独自のSyntax error表示をDOMへ挿入する仕組みがあります。

しかし今回は、

suppressErrorRendering: true

としています。

Mermaid公式Config Schemaでも、この設定はアプリ側で独自に構文エラーを処理したいケースを想定しています。Mermaid

理由は責務を分けるためです。

Mermaid
→ 構文を判断する

エディタ
→ エラーをどう見せるか判断する

ライブラリのデフォルトUIへアプリ全体のUXを依存させません。

失敗しやすいポイント

awaitを付け忘れる

mermaid.parse()とmermaid.render()は非同期処理として扱います。

したがって、

await mermaid.parse(definition);

とします。

呼び出し元の関数も、

async function renderDiagram(definition)

とする必要があります。

try...catchの外で描画する

次のような構造では、Mermaidの構文エラーをアプリ側で適切に扱えません。

const { svg } = await mermaid.render(...);

今回は、

try {
    ...
} catch (error) {
    ...
}

の中へ構文検証と描画を入れます。

エラー後に正常状態へ戻らない

エラーを表示するだけでは不十分です。

正常描画時には必ず、

clearError();

を呼びます。

エラー処理では、

エラーを出せる

だけでなく、

エラーから復帰できる

ことまで確認します。

debounce時間を長くしすぎる

たとえば、

const RENDER_DELAY_MS = 2000;

とすると、入力してから2秒待たなければ図が変わりません。

リアルタイムエディタとしては重く感じます。

逆に、

10

のように極端に短くすると、debounceの効果が小さくなります。

今回は初期値として300msを使います。

これは絶対値ではありません。

最終的には実際の操作感で調整します。

Day 3でできるようになったこと

Day 2までは、

ページロード
↓
描画

でした。

Day 3では、

入力
↓
debounce
↓
構文検証
↓
描画
↓
表示

になりました。

さらに異常系も、

不正入力
↓
構文検証失敗
↓
例外捕捉
↓
最後の正常図を保持
↓
エラー表示
↓
ユーザーが修正
↓
再検証
↓
正常描画
↓
エラー解除

として設計しました。

ここで初めて、単なるMermaid表示ページからエディタへ近づきました。

実務ツールにも使える設計

今回の構造はMermaid専用ではありません。

たとえば設計計算ツールで、

数値入力
↓
毎キー入力で計算

すると、不要な計算が大量に発生します。

そこで、

入力
↓
debounce
↓
入力値検証
↓
計算
↓
結果表示

とできます。

さらに計算処理が非同期なら、

request 10
request 11
request 12

のうち最新の12だけを採用する仕組みが必要になります。

今回の、

renderSequence

と同じ考え方です。

つまりDay 3で扱っているのは、

Mermaidの使い方だけではなく、入力追従型Webツールの基本設計

でもあります。

データとロジックを分離する

今回、

const RENDER_DELAY_MS = 300;

をロジックから分離しました。

今後さらに設定値が増えれば、

const editorConfig = {
    renderDelayMs: 300,
};

のような構造へ発展させられます。

将来的には、

{
    "renderDelayMs": 300
}

のように外部データ化することもできます。

ただし現時点では設定項目が1つしかありません。

Day 3で無理にJSONファイルへ分離すると、構成だけが複雑になります。

将来分離できる構造にしておくことと、今すぐ分離することは別です。

小さなMVPではこの判断も重要です。

練習問題

debounce時間を変更する

次の値を、

const RENDER_DELAY_MS = 300;

から、

const RENDER_DELAY_MS = 1000;

へ変更してください。

実際にMermaidコードを入力します。

確認観点

入力停止から描画までの遅延が明確に感じられるはずです。

その後、

150

も試してください。

どの程度なら「リアルタイム」と感じるか比較します。

空入力の処理を確認する

textareaの内容をすべて削除します。

期待結果は、

Preview
→ 空

Error
→ 非表示

です。

確認観点

「入力がない」と「構文が間違っている」を別状態として扱えているか確認します。

正常→異常→正常を試す

最初に、

flowchart LR
    A --> B

を入力します。

次に意図的に壊します。

flowchart LR
    A[

最後に修正します。

flowchart LR
    A --> C

確認観点

次の状態遷移を確認します。

正常SVG
↓
エラー表示+直前SVG保持
↓
新しい正常SVG+エラー解除

この往復ができれば、Day 3の主要機能は成立しています。

Day 3のまとめ

Day 3では、Mermaidエディタの描画処理をリアルタイム化しました。

今回追加した中心要素は、

inputイベント
↓
debounce
↓
mermaid.parse()
↓
mermaid.render()
↓
SVG更新

です。

さらに、

try...catch
renderSequence
エラー表示
正常状態への復帰

を追加しました。

特に重要なのは、構文エラーをアプリケーションの異常終了として扱わなかったことです。

エディタでは入力途中の不完全なデータは通常状態です。

したがって、

Valid
Invalid
Valid

を安全に往復できなければなりません。

Day 1では、

Flask → HTML

を作りました。

Day 2では、

入力 → Mermaid → SVG

を作りました。

そしてDay 3では、

入力変更
↓
検証
↓
描画
↓
失敗
↓
修正
↓
再描画

という編集ループが成立しました。

これでMermaid簡易エディタの中核部分はかなり形になっています。

参考情報

MermaidのJavaScript APIでは、render()によるSVG生成、parse()による描画前の構文検証、bindFunctionsによる描画後イベント設定が公式に提供されています。Mermaid

2026年10月時点のMermaid公式Usageではv12系CDN例が掲載され、initialize()による設定が推奨されています。古いmermaid.init()は非推奨なので、新規コードでは使用しません。Mermaid

また、独自エラーUIを実装する場合にはsuppressErrorRenderingでMermaid自身のエラー図挿入を抑制できます。Mermaid

次回予告

Day 4は**「.mmdファイルの保存と読込」**です。

現在textarea内にしか存在しないMermaidコードを.mmdファイルとして保存できるようにし、保存したファイルを再び読み込んで編集・描画できるようにします。ここではBlob、ダウンロード、FileReaderまたは現在のブラウザAPIによるファイル読込を扱い、「ブラウザ側で完結させる処理」と「Flaskへ持たせるべき処理」の境界も整理します。

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

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

目次