FlaskでMermaid簡易エディタを作る|2ペインUIとプレビューを実装する

「FlaskでMermaid簡易エディタを作る|2ペインUIとプレビューを実装する」の内容を表す技術イラスト
目次

今回の到達点

Day 1では、Flaskを使って次の最小構成を作りました。

ブラウザ
↓
Flask
↓
templates/index.html

Day 2では、この最小アプリを初めて「エディタらしい画面」へ拡張します。

今回作るのは次の構成です。

┌─────────────────────┬─────────────────────┐
│ Mermaidコード入力   │ プレビュー          │
│                     │                     │
│ flowchart LR         │   ┌───┐             │
│ A --> B              │   │ A │ → │ B │     │
│                     │   └───┘             │
└─────────────────────┴─────────────────────┘

今回の到達点は次の4つです。

  • 左側にMermaidコード入力欄を作る
  • 右側にプレビュー領域を作る
  • Mermaid.jsを読み込む
  • 初期サンプルのフローチャートを表示する

まだ入力変更に追従するリアルタイム描画は実装しません。

それはDay 3で扱います。

Day 2では、

UIの構造とMermaid描画の最小経路を確立する

ことに集中します。

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

Day 1の構成は次のとおりでした。

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

今回はstaticディレクトリを実際に使用します。

完成後は次のようになります。

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

Python側のapp.pyは、基本的に変更しません。

ここが重要です。

Day 2で追加する機能は、

画面構造
見た目
Mermaid描画

であり、Flask側のルーティングを変更する必要がないからです。

Day 1で決めた責務分離が、すでに機能し始めています。

今回の処理構造

今回のアプリは次のように動きます。

ブラウザ
↓
GET /
↓
Flask
↓
index.html
↓
style.css
↓
editor.js
↓
Mermaid.js
↓
SVGとして図を描画

Mermaid公式ドキュメントでは、MermaidをJavaScript APIとして利用でき、図の定義文字列からSVGを生成するrender() APIが提供されています。より複雑な統合ではmermaid.run()も推奨されています。

今回のエディタでは、最終的に入力欄の文字列をmermaid.render()へ渡す構成へ発展させます。

Day 2ではまず、

固定された初期コード
↓
Mermaid.js
↓
プレビュー表示

までを作ります。

HTML・CSS・JavaScriptを分離する理由

小さなWebツールでは、すべてをindex.htmlへ書くこともできます。

たとえば、

<style>
    ...
</style>

<script>
    ...
</script>

とすれば、1ファイルでも動きます。

しかし、今回は最初から分離します。

index.html
→ ページ構造

style.css
→ レイアウト・見た目

editor.js
→ エディタの動作

理由は、変更理由が異なるからです。

画面の幅を変更したいならCSS。

Mermaidの描画方式を変更したいならJavaScript。

ページ内の要素構造を変更したいならHTML。

この境界を維持すると、後半で機能が増えても修正箇所を探しやすくなります。

Flask公式でも、CSSやJavaScriptなどの静的ファイルはstaticディレクトリに置き、url_for('static', filename=...)で参照する標準構成が示されています。

index.htmlを2ペイン構成へ変更する

Day 1のindex.htmlを次の内容へ変更します。

<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Mermaid Editor</title>

    <link
        rel="stylesheet"
        href="{{ url_for('static', filename='css/style.css') }}"
    >
</head>
<body>
    <header class="app-header">
        <h1>Mermaid Editor</h1>
    </header>

    <main class="editor-layout">
        <section class="editor-pane">
            <h2>Code</h2>

            <textarea id="mermaid-input" spellcheck="false">flowchart LR
    A[Input] --> B[Process]
    B --> C[Output]</textarea>
        </section>

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

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

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

ここで新しく登場した要素を順に見ていきます。

url_for('static', ...)で静的ファイルを読む

CSSの読み込みでは、

<link
    rel="stylesheet"
    href="{{ url_for('static', filename='css/style.css') }}"
>

としています。

これはFlaskのJinjaテンプレート構文です。

実際にはブラウザへ、

/static/css/style.css

のようなURLとして出力されます。

JavaScriptも同様です。

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

Flask公式ではstaticという専用エンドポイントを通して静的ファイルURLを生成する方法が示されています。

URLをHTMLへ直接、

/static/css/style.css

と書いても動く場合はあります。

ただしurl_for()を使っておくほうが、Flaskアプリ側のURL構成変更へ追従しやすくなります。

textareaをコード入力欄として使う

Mermaidコード入力にはtextareaを使用します。

<textarea id="mermaid-input" spellcheck="false">flowchart LR
    A[Input] --> B[Process]
    B --> C[Output]</textarea>

初期値として次のMermaidコードを入れています。

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

MermaidのFlowchartでは、

flowchart LR

によって左から右へ進むフローを定義できます。

A --> Bのような記法でノード間の接続を表現します。Mermaid公式のFlowchart Syntaxでも、ノードとエッジをテキストで定義する基本構造が示されています。

spellcheck="false"を付けているのは、コード入力中にブラウザのスペルチェック表示が出るのを避けるためです。

プレビュー領域を用意する

右側には、

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

を置きます。

この中へJavaScriptから生成されたSVGを挿入します。

最終的な流れは、

textarea
↓
文字列取得
↓
mermaid.render()
↓
SVG文字列
↓
#mermaid-preview

になります。

これをDay 3でリアルタイム化します。

CSSで2ペインレイアウトを作る

static/css/style.cssを作ります。

* {
    box-sizing: border-box;
}

html,
body {
    margin: 0;
    min-height: 100%;
}

body {
    font-family:
        system-ui,
        -apple-system,
        BlinkMacSystemFont,
        "Segoe UI",
        sans-serif;
    background: #f5f5f5;
    color: #222;
}

.app-header {
    padding: 16px 24px;
    background: #ffffff;
    border-bottom: 1px solid #dddddd;
}

.app-header h1 {
    margin: 0;
    font-size: 1.4rem;
}

.editor-layout {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    min-height: calc(100vh - 65px);
    padding: 16px;
}

.editor-pane,
.preview-pane {
    display: flex;
    flex-direction: column;
    min-width: 0;
    padding: 16px;
    background: #ffffff;
    border: 1px solid #dddddd;
    border-radius: 8px;
}

.editor-pane h2,
.preview-pane h2 {
    margin-top: 0;
    font-size: 1rem;
}

#mermaid-input {
    flex: 1;
    width: 100%;
    min-height: 400px;
    padding: 12px;
    resize: vertical;
    border: 1px solid #cccccc;
    border-radius: 6px;
    font-family:
        Consolas,
        "Courier New",
        monospace;
    font-size: 14px;
    line-height: 1.5;
}

#mermaid-preview {
    display: flex;
    flex: 1;
    align-items: center;
    justify-content: center;
    min-height: 400px;
    overflow: auto;
}

#mermaid-preview svg {
    max-width: 100%;
    height: auto;
}

@media (max-width: 800px) {
    .editor-layout {
        grid-template-columns: 1fr;
    }
}

CSS Gridで左右を分割する

2ペインを作っている中心部分はここです。

.editor-layout {
    display: grid;
    grid-template-columns: 1fr 1fr;
}

1fr 1frによって、使用可能な横幅を左右へ均等に分けています。

概念的には、

全体幅
├── 50% Code
└── 50% Preview

です。

厳密にはfrはGridコンテナ内の利用可能スペースの割合を表します。

今回のような2ペインUIには非常に扱いやすい指定です。

min-width: 0を入れる理由

GridやFlexboxの子要素は、内容によって意図した幅より広がる場合があります。

そこで、

min-width: 0;

を入れています。

この指定がないと、長いコードや大きなSVGなどによってペインが横方向へ押し広げられる場合があります。

小さな指定ですが、エディタのように横幅を制御したいUIでは有効です。

狭い画面では縦並びにする

今回は最低限のレスポンシブ対応も入れます。

@media (max-width: 800px) {
    .editor-layout {
        grid-template-columns: 1fr;
    }
}

画面が狭くなると、

Code | Preview

ではなく、

Code

Preview

へ切り替わります。

PC用の開発ツールが主目的でも、ブラウザ幅を狭くしたときに完全に崩れる状態は避けておいたほうが扱いやすくなります。

Mermaid.jsを読み込む

次にstatic/js/editor.jsを作ります。

今回はMermaidをES Moduleとして読み込みます。

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

mermaid.initialize({
    startOnLoad: false,
});

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

async function renderInitialDiagram() {
    const definition = inputElement.value;

    const { svg } = await mermaid.render(
        "initial-mermaid-diagram",
        definition
    );

    previewElement.innerHTML = svg;
}

renderInitialDiagram();

Mermaid公式のUsageでは、ES ModuleとしてMermaidを読み込み、

mermaid.initialize({
    startOnLoad: false,
});

とした上で、JavaScript APIからmermaid.render()を呼び出す方法が案内されています。render()は図の定義文字列を受け取り、生成したSVGを返します。

startOnLoad: falseにする理由

MermaidにはHTML上の対象要素を自動的に探して描画する使い方もあります。

しかし今回作っているのはエディタです。

最終的には、

ユーザーが入力
↓
JavaScriptが検知
↓
必要なタイミングで描画

としたいため、自動描画へ任せません。

そこで、

mermaid.initialize({
    startOnLoad: false,
});

とします。

描画タイミングをアプリケーション側で管理する設計です。

mermaid.render()の役割

中心となるコードはここです。

const { svg } = await mermaid.render(
    "initial-mermaid-diagram",
    definition
);

definitionには、

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

というMermaidコードが入っています。

Mermaidがこれを解析し、SVGを生成します。

返されたSVGを、

previewElement.innerHTML = svg;

でプレビュー領域へ入れます。

処理全体は、

textarea.value
↓
definition
↓
mermaid.render()
↓
svg
↓
innerHTML

です。

この5段階が、Mermaidエディタの中核になります。

Day 3では最初の、

textarea.value

が変更されるたびに再実行できるようにします。

なぜ初期表示だけにするのか

今回のコードでは、ページを開いたときに一度だけ、

renderInitialDiagram();

を実行しています。

したがってtextareaを書き換えても、まだ図は更新されません。

これは意図した仕様です。

Day 2では、

Mermaidを読み込める
↓
入力値を取得できる
↓
SVGへ変換できる
↓
プレビューへ表示できる

という最小経路だけを確認します。

ここへ同時に、

  • inputイベント
  • debounce
  • 構文エラー処理
  • 再描画
  • エラーからの復帰

まで追加すると、問題発生時の切り分け対象が一気に増えてしまいます。

正常状態を1段ずつ確定するほうが安全です。

app.pyは変更しない

Day 1のapp.pyをそのまま使います。

from flask import Flask, render_template


app = Flask(__name__)


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

今回追加したUIやMermaid.jsについて、Pythonは何も知りません。

これは望ましい状態です。

Flaskは、

index.htmlを返す
staticファイルを配信する

ところまで。

エディタの描画処理はブラウザへ任せています。

Day 2完成時のディレクトリ構成

完成状態を整理します。

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

重要なのは、役割とファイルが対応していることです。

app.py
→ Flask

index.html
→ ページ構造

style.css
→ UI

editor.js
→ Mermaid制御

これならDay 3以降でJavaScriptが増えても、Flask側のコードは必要以上に複雑になりません。

起動方法

仮想環境を有効にします。

.\.venv\Scripts\Activate.ps1

Flaskアプリを起動します。

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

ブラウザで開きます。

http://127.0.0.1:5000/

期待する状態は次のとおりです。

  • 左にCodeペインが表示される
  • 右にPreviewペインが表示される
  • textareaに初期Mermaidコードが入っている
  • 右側にフローチャートが表示される
  • ブラウザ幅を狭くすると縦並びになる

なお、この記事生成環境では実ブラウザでの描画確認を行っていないため、上記は実装後に確認すべき期待結果です。

うまく表示されない場合

CSSが反映されない

まずブラウザの開発者ツールで、

/static/css/style.css

が正常に取得されているか確認します。

404ならディレクトリ構造を確認します。

static/
└── css/
    └── style.css

HTML側は、

{{ url_for('static', filename='css/style.css') }}

になっている必要があります。

JavaScriptが動かない

editor.jsが取得できているか確認します。

/static/js/editor.js

ブラウザのConsoleにJavaScriptエラーが出ていないかも確認します。

特に、

type="module"

を付け忘れると、ES Moduleとしてのimportが使えません。

HTML側は、

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

とします。

Mermaidが読み込めない

CDNへのアクセスがネットワークや社内環境で制限されている可能性があります。

今回のコードでは外部CDNを利用しています。

そのため、

ブラウザ
↓
jsDelivr
↓
Mermaidモジュール取得

が成立する必要があります。

将来的に完全ローカル化する場合は、Mermaidをnpmなどでローカルへ導入する構成も考えられます。

ただしDay 2では、構成を増やさないためCDNを使います。

Mermaidコードが描画されない

まず初期コードを最小化します。

flowchart LR
    A --> B

MermaidのFlowchart構文では、flowchart LRなどで方向を指定し、-->でノードを接続できます。

複雑な図で失敗した場合は、一度ここまで戻して切り分けます。

Consoleに構文エラーが出る

現段階ではエラー表示UIを作っていません。

そのためMermaidの解析エラーは、主にConsoleへ現れる可能性があります。

これはDay 2では正常です。

Day 3で、

構文エラー
↓
catch
↓
画面にエラー表示

する仕組みを追加します。

今回の設計で重要な点

Day 2の本質は「左右に画面を並べたこと」ではありません。

重要なのは、

入力
↓
JavaScript
↓
Mermaid
↓
SVG
↓
表示

という描画パイプラインを作ったことです。

この流れが成立すれば、今後は入口と出口へ機能を追加できます。

たとえば入口側には、

入力イベント
ファイル読込
テンプレート

を追加できます。

出口側には、

SVG保存
コピー
エラー表示

を追加できます。

つまり、今回作った描画経路がMVP全体の中心になります。

実務的なWebツール設計への接続

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

たとえば技術計算ツールなら、

入力フォーム
↓
JavaScript
↓
計算API
↓
結果表示

に置き換えられます。

CAD支援ツールなら、

パラメータ入力
↓
JavaScript
↓
Flask
↓
Python処理
↓
JSON
↓
画面表示

という形にもできます。

重要なのは、

入力・処理・表示の境界を明確にする

ことです。

今回のMermaidエディタでは、

入力
textarea

処理
Mermaid.js

表示
SVG preview

が明確に分かれています。

これは小さなWebツールを拡張可能にする基本構造です。

練習問題

初期フローチャートを変更する

textareaの初期値を次のように変更してください。

flowchart TD
    Start --> Check
    Check --> Finish

LRをTDへ変更すると、図の方向が変わります。

確認観点

Mermaidコードだけを変更し、FlaskやCSSを変更しなくても図の内容を変更できることを確認します。

ペインの比率を変更する

現在は、

grid-template-columns: 1fr 1fr;

です。

これを、

grid-template-columns: 2fr 3fr;

へ変更してください。

確認観点

CodeペインよりPreviewペインのほうが広くなることを確認します。

UIの見た目だけならCSSだけで変更できることも確認してください。

サンプルノードを増やす

初期Mermaidコードを次のように変更します。

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

確認観点

JavaScriptコードを変更せず、入力データだけを変更して表示結果を変えられることを確認します。

これは「データと処理を分ける」という今回の設計方針にもつながります。

Day 2のまとめ

Day 2ではFlaskの最小アプリを、実際のエディタUIへ一段階進めました。

今回追加した構成は、

index.html
↓
2ペイン画面

style.css
↓
レイアウト

editor.js
↓
Mermaid描画

Mermaid.js
↓
SVG生成

です。

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

それでもアプリの機能は大きく増えました。

これは、

Flaskとブラウザ側ロジックの責務を分離した効果

です。

今回の最重要ポイントは次のとおりです。

  • Flaskはページと静的ファイルを配信する
  • HTMLは画面構造を担当する
  • CSSは2ペインレイアウトを担当する
  • JavaScriptがMermaid.jsを制御する
  • MermaidコードをSVGへ変換する経路を作る
  • Day 2では描画を一度だけ行う
  • リアルタイム再描画とエラー処理はDay 3へ分離する

現時点で、

入力
↓
Mermaid
↓
SVG
↓
プレビュー

というMVPの中核ができました。

次はこの処理を、ユーザーの入力へ追従させます。

参考情報

Flaskのtemplates、static、render_template()、url_for()の基本構成についてはFlask公式Quickstartを参照してください。

Mermaidの初期化、JavaScript API、mermaid.render()、mermaid.run()についてはMermaid公式Usageを参照してください。

Flowchartの基本構文についてはMermaid公式Flowchart Syntaxを参照してください。

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

この記事を書いた人

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

目次