FlaskでMermaid簡易エディタを作る|SVG出力とクリップボード機能を実装する

「FlaskでMermaid簡易エディタを作る|SVG出力とクリップボード機能を実装する」の内容を表す技術イラスト
目次

今回の到達点

Day 4までで、Mermaid簡易エディタには次の機能が実装されました。

Mermaidコード入力
        ↓
    debounce
        ↓
    構文検証
        ↓
    SVG生成
        ↓
  リアルタイム表示

        +

.mmdファイル保存
.mmdファイル読込

しかし、まだ一つ重要な機能がありません。

作成した図を、ほかのアプリケーションで使える形式として出力する機能です。

Mermaidコードを保存できても、その図を資料やWebページで使用するには、別の手段が必要になります。

Day 5では次の2機能を追加します。

  1. プレビュー中の図をSVGファイルとしてダウンロードする。
  2. Mermaidコードをクリップボードへコピーする。

完成すると、次のようなデータの流れになります。

              Mermaid Editor
                     │
          ┌──────────┴──────────┐
          │                     │
     Mermaidコード          SVGプレビュー
          │                     │
     ┌────┴────┐                │
     │         │                │
   .mmd     Clipboard         .svg
     │         │                │
     ▼         ▼                ▼
  再編集    他のツール       資料・Web

ここで重要なのは、編集用データと表示用データを分離することです。

MermaidコードとSVGの違い

MermaidコードとSVGは、どちらもテキストで記述できる形式ですが、目的が異なります。

項目MermaidコードSVG
主な用途図の定義・編集図の表示・配布
ファイル形式.mmd.svg
人間による編集容易比較的難しい
図形情報Mermaid記法XML形式
拡大縮小描画後に可能ベクター形式
再描画Mermaid.jsが必要SVG対応環境で表示可能
バージョン管理適している可能だが差分が複雑

Mermaidコードは、図をどのように作るかを記述したソースデータです。

一方、SVGは描画された図形を記述した表示データです。

たとえば次のMermaidコードを考えます。

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

Mermaid.jsは、この記述を解析してSVGを生成します。

SVGはXMLを基礎とするベクター画像形式です。

概念的には次のような構造を持ちます。

Input
<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 200 100">

    <rect
        x="10"
        y="20"
        width="80"
        height="40"
        fill="white"
        stroke="black"
    />

    <text
        x="50"
        y="45"
        text-anchor="middle"
    >
        Input
    </text>

</svg>

実際にMermaid.jsが生成するSVGは、スタイルやグループ要素などを含むため、これより複雑です。

ただし基本原理は同じです。

SVGは画像でありながら、図形情報をテキストとして保持できる形式なのです。

Day 4の構成をそのまま利用する

今回もFlask側の構成は変更しません。

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

変更するファイルは次の3つです。

ファイル変更内容
index.htmlSVG出力・コピー用ボタン追加
style.cssボタン配置と操作結果表示
editor.jsSVG保存・クリップボード処理

app.pyは変更しません。

理由は、今回の処理がすべてブラウザ内で完結するためです。

ブラウザ
    │
    ├── Mermaid.js
    │       ↓
    │      SVG
    │       ↓
    │   ダウンロード
    │
    └── Mermaidコード
            ↓
       Clipboard API
            ↓
       クリップボード

SVG生成をPython側へ移す必要はありません。

HTMLに操作ボタンを追加する

Day 4では、OpenとSaveの2つのボタンを作りました。

今回はそこへ2つ追加します。

[Open .mmd] [Save .mmd] [Export SVG] [Copy Code]

templates/index.htmlのヘッダー部分を、次の内容へ変更します。

<header class="app-header">
    <h1>Mermaid Editor</h1>

    <div class="file-actions">
        <button id="open-button" type="button">
            Open .mmd
        </button>

        <button id="save-button" type="button">
            Save .mmd
        </button>

        <button id="svg-button" type="button">
            Export SVG
        </button>

        <button id="copy-button" type="button">
            Copy Code
        </button>

        <input
            id="file-input"
            type="file"
            accept=".mmd,text/plain"
            hidden
        >
    </div>
</header>

<p
    id="action-status"
    class="action-status"
    role="status"
    aria-live="polite"
></p>

この変更では、Day 4のOpen・Save機能を削除していません。

新しい操作を追加しているだけです。

action-statusは、操作結果を表示する領域です。

たとえば、

SVGファイルのダウンロードを開始しました。

または、

Mermaidコードをコピーしました。

などを表示します。

role="status"とaria-live="polite"は、操作結果を支援技術へ通知するための指定です。

Day 3で作った構文エラー表示とは役割を分けます。

#error-message
→ Mermaid構文エラー

#action-status
→ ファイル操作・コピー結果

CSSを追加する

static/css/style.cssの末尾へ次を追加します。

.file-actions {
    flex-wrap: wrap;
}

.action-status {
    min-height: 1.5em;
    margin: 0;
    padding: 6px 24px;
    background: #ffffff;
    color: #444444;
    font-size: 13px;
}

Day 4までのCSSは維持します。

今回追加したのは、ボタンが増えた場合の折り返しと操作結果表示だけです。

これでUI側の準備ができました。

SVG出力の原理

Day 3では、Mermaid.jsから返されたSVGを次のように表示していました。

previewElement.innerHTML = svg;

つまり、プレビュー領域の内部には、すでにSVGが存在しています。

今回必要なのは、そのSVGを取り出してファイルにすることです。

処理は次の順序になります。

Mermaidプレビュー
        ↓
     SVG要素取得
        ↓
     SVG要素複製
        ↓
    XMLSerializer
        ↓
      SVG文字列
        ↓
        Blob
        ↓
     Object URL
        ↓
     ダウンロード

ここでは新たにMermaidを描画し直しません。

現在表示されているSVGを出力する設計です。

SVG要素を取得する

まずプレビュー内のSVG要素を取得します。

const svgElement = previewElement.querySelector("svg");

querySelector()は、指定したCSSセレクターに一致する最初の要素を取得します。

今回は、

#mermaid-preview
    └── svg

という構造を想定しています。

SVGが存在しなければ、出力できません。

その場合はエラーではなく、操作結果として理由を表示します。

XMLSerializerとは何か

DOM上のSVG要素をファイルへ保存するには、文字列へ変換する必要があります。

そこで使用するのがXMLSerializerです。

const serializer = new XMLSerializer();

const svgText = serializer.serializeToString(svgElement);

serializeToString()はDOMツリーをXML文字列へ変換するブラウザAPIです。

SVGもXML形式なので、この方法で文字列化できます。

ただし、文字列化できることと、あらゆる環境で同じ見た目になることは別です。

外部フォントや外部CSSに依存したSVGでは、単独ファイルとして開いた際に表示が変わる可能性があります。

今回のMVPでは、Mermaidが生成したSVG構造をそのまま出力する方式を採用します。

SVG要素を複製する理由

今回は直接SVGを変更せず、複製してから保存します。

const copy = svgElement.cloneNode(true);

cloneNode(true)は、子要素を含めてDOM要素を複製します。

これにより、

画面表示用SVG
        │
        └── 複製
              ↓
          保存用SVG

という構成になります。

保存用SVGへ属性を追加しても、画面表示中のSVGへ影響しません。

続いて名前空間を指定します。

copy.setAttribute(
    "xmlns",
    "http://www.w3.org/2000/svg"
);

SVGを独立したXML文書として扱うための指定です。

viewBoxと出力寸法

SVGにはviewBoxという属性があります。

たとえば、

<svg viewBox="0 0 800 400">

なら、内部の座標範囲は幅800、高さ400です。

ただしviewBoxは、表示時の実寸を直接指定するものではありません。

そこで今回は、正しいviewBoxが存在する場合、その幅と高さを出力SVGへ設定します。

const viewBox = copy.getAttribute("viewBox");

if (viewBox) {
    const values = viewBox
        .trim()
        .split(/[\s,]+/)
        .map(Number);

    if (
        values.length === 4 &&
        values.every(Number.isFinite) &&
        values[2] > 0 &&
        values[3] > 0
    ) {
        copy.setAttribute("width", String(values[2]));
        copy.setAttribute("height", String(values[3]));
    }
}

この処理によって、出力SVGの幅と高さに具体的な値を与えます。

ただし、viewBoxの値は描画座標系の寸法です。CSSによる表示寸法や印刷時の物理寸法を保証するものではありません。

Blobによるダウンロードを共通化する

Day 4では、.mmd保存用にBlobを作成しました。

Day 5でも同じ仕組みを利用します。

ここで、ダウンロード処理を共通関数へ分離します。

function downloadBlob(blob, filename) {
    const objectUrl = URL.createObjectURL(blob);

    const link = document.createElement("a");

    link.href = objectUrl;
    link.download = filename;

    document.body.appendChild(link);

    link.click();
    link.remove();

    setTimeout(() => {
        URL.revokeObjectURL(objectUrl);
    }, 30_000);
}

この関数の入力は2つです。

blob
→ 保存するデータ

filename
→ 保存ファイル名

これだけで、異なるファイル形式を同じ処理でダウンロードできます。

downloadBlob(
    new Blob(["sample"], {
        type: "text/plain"
    }),
    "sample.txt"
);

あるいは、

downloadBlob(
    new Blob([svgText], {
        type: "image/svg+xml;charset=utf-8"
    }),
    "diagram.svg"
);

という使い方ができます。

Object URLの解放

URL.createObjectURL()で作成したURLは、不要になった時点で解放する必要があります。

そこで、

URL.revokeObjectURL(objectUrl);

を使用します。

ただし、ダウンロードリンクをクリックした直後にURLを解放すると、ブラウザによってはダウンロード処理とのタイミングが問題になる可能性があります。

今回の実装では30秒後に解放します。

これはMVP向けの簡易的な方法であり、ダウンロード完了を厳密に検知する仕組みではありません。

長時間稼働するアプリケーションでは、Object URLのライフサイクルをさらに管理する設計も検討できます。

クリップボード機能を実装する

次にMermaidコードをコピーする機能です。

今回はSVG画像ではなく、Mermaidのソースコードをテキストとしてコピーします。

理由は、コピー先で再編集できることを重視するためです。

Mermaid Editor
      ↓
Mermaidコード
      ↓
Clipboard API
      ↓
クリップボード
      ↓
Markdown・別エディタ

使用するAPIは次です。

navigator.clipboard.writeText()

たとえば、

await navigator.clipboard.writeText(
    "flowchart LR\nA --> B"
);

とすると、指定した文字列をクリップボードへ書き込めます。

writeText()は非同期APIであり、結果をPromiseとして返します。

また、利用にはセキュアコンテキストが必要です。

通常のHTTPSページに加え、ローカル開発で使用するlocalhostやループバックアドレスもセキュアコンテキストとして扱われます。

ただし、ブラウザによってはユーザー操作やアクセス許可などの追加条件があります。

そのため今回は、ボタンクリックから直接コピー処理を呼び出し、失敗した場合はメッセージを表示します。

Day 5完成版 editor.js

ここまでの内容を統合します。

今回はDay 3・Day 4の機能も維持した**完成版のeditor.js**を掲載します。

static/js/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 statusElement = document.getElementById("action-status");

const openButton = document.getElementById("open-button");
const saveButton = document.getElementById("save-button");
const svgButton = document.getElementById("svg-button");
const copyButton = document.getElementById("copy-button");
const fileInput = document.getElementById("file-input");

const RENDER_DELAY_MS = 300;

let debounceTimer = null;
let renderSequence = 0;
let renderedSource = null;


// エラー表示
function showError(message) {
    errorElement.textContent = message;
    errorElement.hidden = false;
}


// エラー解除
function clearError() {
    errorElement.textContent = "";
    errorElement.hidden = true;
}


// 操作結果表示
function setStatus(message) {
    statusElement.textContent = message;
}


// エラー文字列への変換
function getErrorMessage(error) {
    return error instanceof Error
        ? error.message
        : String(error);
}


// Mermaid描画
async function renderDiagram(definition, sequence) {
    try {
        await mermaid.parse(definition);

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

        const { svg, bindFunctions } = await mermaid.render(
            `mermaid-diagram-${sequence}`,
            definition
        );

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

        previewElement.innerHTML = svg;

        bindFunctions?.(previewElement);

        renderedSource = definition;

        clearError();

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

        showError(getErrorMessage(error));
    }
}


// 描画予約
function scheduleRender() {
    const sequence = ++renderSequence;

    renderedSource = null;

    clearTimeout(debounceTimer);

    const definition = inputElement.value.trim();

    if (!definition) {
        previewElement.replaceChildren();
        clearError();
        return;
    }

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

        void renderDiagram(definition, sequence);

    }, RENDER_DELAY_MS);
}


// 共通ダウンロード処理
function downloadBlob(blob, filename) {
    const objectUrl = URL.createObjectURL(blob);

    const link = document.createElement("a");

    link.href = objectUrl;
    link.download = filename;

    document.body.appendChild(link);

    link.click();
    link.remove();

    setTimeout(() => {
        URL.revokeObjectURL(objectUrl);
    }, 30_000);
}


// Mermaidコード保存
function saveMmdFile() {
    const source = inputElement.value;

    if (!source.trim()) {
        setStatus("保存するMermaidコードがありません。");
        return;
    }

    downloadBlob(
        new Blob([source], {
            type: "text/plain;charset=utf-8"
        }),
        "diagram.mmd"
    );

    setStatus(
        ".mmdファイルのダウンロードを開始しました。"
    );
}


// Mermaidファイル読込
async function handleFileSelection() {
    const file = fileInput.files?.[0];

    if (!file) {
        return;
    }

    try {
        if (!file.name.toLowerCase().endsWith(".mmd")) {
            throw new Error(
                ".mmdファイルを選択してください。"
            );
        }

        const source = await file.text();

        inputElement.value = source;

        scheduleRender();

        setStatus(
            `${file.name}を読み込みました。`
        );

    } catch (error) {
        setStatus(
            `読込失敗: ${getErrorMessage(error)}`
        );

    } finally {
        fileInput.value = "";
    }
}


// SVGファイル出力
function exportSvg() {
    if (
        renderedSource === null ||
        renderedSource !== inputElement.value.trim()
    ) {
        setStatus(
            "現在のコードを正常に描画してからSVGを出力してください。"
        );
        return;
    }

    const svgElement = previewElement.querySelector("svg");

    if (!svgElement) {
        setStatus("出力可能なSVGがありません。");
        return;
    }

    const copy = svgElement.cloneNode(true);

    copy.setAttribute(
        "xmlns",
        "http://www.w3.org/2000/svg"
    );

    const viewBox = copy.getAttribute("viewBox");

    if (viewBox) {
        const values = viewBox
            .trim()
            .split(/[\s,]+/)
            .map(Number);

        if (
            values.length === 4 &&
            values.every(Number.isFinite) &&
            values[2] > 0 &&
            values[3] > 0
        ) {
            copy.setAttribute(
                "width",
                String(values[2])
            );

            copy.setAttribute(
                "height",
                String(values[3])
            );
        }
    }

    const svgText = new XMLSerializer()
        .serializeToString(copy);

    downloadBlob(
        new Blob([svgText], {
            type: "image/svg+xml;charset=utf-8"
        }),
        "diagram.svg"
    );

    setStatus(
        "SVGファイルのダウンロードを開始しました。"
    );
}


// Mermaidコードをコピー
async function copyMermaidSource() {
    const source = inputElement.value;

    if (!source.trim()) {
        setStatus(
            "コピーするMermaidコードがありません。"
        );
        return;
    }

    if (!navigator.clipboard?.writeText) {
        setStatus(
            "クリップボードAPIを利用できません。HTTPSまたはlocalhostで開いてください。"
        );
        return;
    }

    try {
        await navigator.clipboard.writeText(source);

        setStatus(
            "Mermaidコードをコピーしました。"
        );

    } catch (error) {
        setStatus(
            `コピー失敗: ${getErrorMessage(error)}`
        );
    }
}


// イベント登録
inputElement.addEventListener(
    "input",
    scheduleRender
);

openButton.addEventListener(
    "click",
    () => fileInput.click()
);

saveButton.addEventListener(
    "click",
    saveMmdFile
);

fileInput.addEventListener(
    "change",
    handleFileSelection
);

svgButton.addEventListener(
    "click",
    exportSvg
);

copyButton.addEventListener(
    "click",
    copyMermaidSource
);


// 初期描画
scheduleRender();

このコードは、Day 3・Day 4で作成した機能を削除せず、SVG出力とコピー機能を追加したものです。

古いSVGを誤って出力しない仕組み

今回の実装で特に重要なのが、次の変数です。

let renderedSource = null;

これは、現在表示されているSVGが、どのMermaidコードから生成されたものかを管理します。

正常に描画できた場合だけ、

renderedSource = definition;

とします。

一方、入力内容が変更された時点で、

renderedSource = null;

へ戻します。

なぜ必要なのでしょうか。

リアルタイム描画には時間差がある

Day 3ではdebounceを導入しました。

そのため、ユーザーが入力してからSVGが更新されるまで、わずかな時間差があります。

たとえば、

時刻A
MermaidコードA
SVG-A表示

時刻B
MermaidコードBへ変更

時刻C
SVG-B生成中

という状態を考えます。

時刻BからCの間は、入力欄がBになっていても、画面には古いSVG-Aが残っています。

この状態でExport SVGを押すと、古い図を保存してしまう可能性があります。

そこで今回は、

if (
    renderedSource === null ||
    renderedSource !== inputElement.value.trim()
) {
    return;
}

という条件を追加しています。

これにより、

現在の入力
        ↓
正常描画済み?
        │
   ┌────┴────┐
   │         │
  Yes        No
   │         │
   ▼         ▼
 SVG保存   保存禁止

となります。

非同期処理の競合にも対応する

Day 3から使用しているrenderSequenceも維持しています。

今回のコードでは、入力が変更された時点で番号を更新します。

const sequence = ++renderSequence;

描画結果を受け取った際に、現在の番号と一致するか確認します。

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

これによって、古い描画要求の結果が後から返ってきても、画面へ反映しません。

また、描画待機中の新しい入力が存在する場合も、古い結果を無効化できます。

表示中の図と現在の入力が一致しているかを管理することは、SVG出力の正確性に直結します。

クリップボードはSVGではなくソースをコピーする

今回のCopy Codeは、次の文字列をコピーします。

flowchart LR
    A[Pump] --> B[Valve]
    B --> C[Cylinder]

これは図の画像をコピーする機能ではありません。

Mermaidコードそのものをコピーする機能です。

そのため、構文エラーが含まれていてもコピーできます。

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

たとえば編集途中のコードを別のエディタへ移したい場合、構文が完成している必要はありません。

一方、SVG出力では正常描画済みであることを要求します。

両者の入力条件は異なります。

Copy Code
→ テキストが存在すればコピー可能

Export SVG
→ 正常描画されたSVGが必要

この違いを明確にしておくことが重要です。

動作確認

Flaskを起動します。

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

ブラウザで次のアドレスを開きます。

http://127.0.0.1:5000/

Day 5の完成状態では、画面上部に4つの操作ボタンが表示されます。

┌────────────────────────────────────────────────┐
│ Mermaid Editor                                 │
│                                                │
│ [Open .mmd] [Save .mmd]                        │
│ [Export SVG] [Copy Code]                       │
├───────────────────────┬────────────────────────┤
│ Code                  │ Preview                │
│                       │                        │
│ flowchart LR          │   ┌─────┐              │
│ A --> B               │   │  A  │ → B          │
│                       │   └─────┘              │
└───────────────────────┴────────────────────────┘

実際の配置はブラウザ幅によって変わります。

SVG出力を確認する

次のコードを入力します。

flowchart LR
    A[Start] --> B[Process]
    B --> C[Finish]

プレビューが更新されたことを確認します。

続いてExport SVGを押します。

期待結果は次のとおりです。

diagram.svg

がダウンロードされます。

保存したSVGをブラウザなどのSVG対応アプリケーションで開き、図が表示されることを確認します。

クリップボードを確認する

Copy Codeを押します。

その後、テキストエディタを開いて貼り付けます。

期待する内容は次のとおりです。

flowchart LR
    A[Start] --> B[Process]
    B --> C[Finish]

改行とインデントが維持されていることも確認します。

古いSVGを保存できないことを確認する

正常な図が表示された状態から、Mermaidコードを変更します。

変更直後、まだ新しいSVGが完成していない間にExport SVGを押します。

期待結果は、

現在のコードを正常に描画してからSVGを出力してください。

というメッセージです。

その後、正常描画が完了すれば出力できるようになります。

構文エラー時の動作

次の不正なMermaidコードを入力します。

flowchart LR
    A[

構文検証が失敗した場合、Day 3で作成したエラー表示が動作します。

この状態ではSVG出力を禁止します。

ただしCopy Codeは利用できます。

これによって、編集途中のソースコードを失わずに別のツールへ移せます。

コード検証結果

今回のJavaScriptについて、Node.js v22.16.0で構文検証を実施しました。

結果は合格です。

さらに、DOMとMermaid APIを模擬したテスト環境で、次の項目を確認しました。

テスト項目結果
初期描画合格
SVG出力処理合格
クリップボードコピー合格
描画待機中の古いSVG出力防止合格
構文エラーの検出合格
構文エラーからの復帰合格
.mmdファイル読込合格
.mmdファイル保存合格

ただし、このテストはMermaid本体とブラウザDOMを模擬したものです。

実際のMermaid.jsによるSVG生成、ブラウザのダウンロード動作、OSのクリップボード操作までは確認していません。

したがって、実ブラウザでの完全な動作確認は未実施です。

よくある失敗

SVGを出力できない

まずプレビューが正常に表示されているか確認します。

今回のコードでは、描画中や構文エラー時のSVG出力を禁止しています。

入力内容が変更された直後なら、描画完了まで待ってください。

SVGファイルを開くと見た目が違う

SVG内部で使用しているフォントやスタイルが、保存先の環境で同じように利用できない場合があります。

特に外部フォントやCSSへ依存している場合は注意が必要です。

また、SVGを読み込むアプリケーションによっては、一部のSVG要素やHTMLを含むforeignObjectの扱いが異なります。

今回のMVPではSVGの基本的な出力までを扱います。

完全な自己完結型SVGや、あらゆるCAD・画像編集ソフトへの互換性までは保証しません。

Copy Codeが動かない

まずHTTPSまたはlocalhostでアクセスしているか確認します。

Clipboard APIはセキュアコンテキストを要求します。

また、ブラウザの権限設定やユーザー操作条件によってコピーが拒否される場合があります。

今回のコードでは、失敗した場合にメッセージを表示します。

コピーした内容に改行がない

次のようにtextareaのvalueを直接取得しているか確認します。

const source = inputElement.value;

textContentではありません。

また、コピー前に文字列を不要に加工していないか確認します。

今回のコードでは、コピー対象にtrim()を適用していません。

したがって元の文字列を保持します。

ダウンロードファイル名が毎回同じ

今回のMVPでは、

diagram.mmd
diagram.svg

という固定名を使用しています。

同名ファイルが存在する場合の動作は、ブラウザやダウンロード設定によって異なります。

自動的に連番が付く場合もあれば、上書き確認が表示される場合もあります。

ファイル名管理機能は、将来の拡張対象とします。

実務ツールへの応用

今回の実装は、Mermaid専用の技術ではありません。

構造を抽象化すると、

入力データ
    ↓
処理エンジン
    ↓
表示データ
    ↓
外部出力

となります。

これは設計計算ツールやCAD支援ツールにも適用できます。

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

設計パラメータ
      ↓
計算ロジック
      ↓
計算結果
      │
      ├── JSON保存
      ├── CSV保存
      └── SVGグラフ出力

という構成にできます。

あるいは、油圧回路を可視化するツールなら、

回路データ
      ↓
描画エンジン
      ↓
SVG
      │
      ├── ブラウザ表示
      ├── SVG保存
      └── 資料への組込み

という設計も考えられます。

ここで重要なのは、出力形式ごとに計算ロジックや描画ロジックを重複実装しないことです。

今回も、

Mermaidコード
      ↓
Mermaid.js
      ↓
SVG

という既存の処理をそのまま利用しています。

出力機能は、その結果を別の形式で外部へ渡しているだけです。

この責務分離によって、機能追加時の変更範囲を小さくできます。

練習問題

SVGファイル名を変更する

現在のコードでは、

"diagram.svg"

というファイル名を使用しています。

これを、

"my-flowchart.svg"

へ変更してください。

確認観点: 描画処理を変更せず、保存ファイル名だけを変更できることを確認します。

クリップボードへ空文字列を送らない

現在のコードでは、

if (!source.trim()) {
    return;
}

という判定を使用しています。

この判定を一時的に削除し、空のtextareaでCopy Codeを押した場合の動作を比較してください。

確認観点: ブラウザAPIが受け付ける値と、アプリケーションとして許可する値は必ずしも同じではないことを理解します。

SVG出力と.mmd保存の違いを確認する

同じMermaidコードから、

diagram.mmd
diagram.svg

の2ファイルを保存してください。

両方をテキストエディタで開いて内容を比較します。

確認観点: .mmdは図の定義、.svgは描画結果を表現していることを確認します。

Day 5のまとめ

Day 5では、Mermaid簡易エディタにSVG出力とクリップボード機能を追加しました。

今回の追加機能は次のとおりです。

Mermaid Editor
      │
      ├── .mmd保存
      │
      ├── .mmd読込
      │
      ├── SVG出力
      │
      └── コードコピー

これによって、エディタ内部で作成した図を外部へ持ち出せるようになりました。

特に重要なポイントは3つあります。

第一に、SVG生成とSVG保存を分離したことです。

Mermaid.jsが生成したSVGを再利用するため、保存機能のために新しい描画ロジックを作る必要がありません。

第二に、共通ダウンロード関数を作ったことです。

.mmdと.svgの保存で、同じBlobダウンロード処理を再利用しています。

第三に、描画状態と入力状態を区別したことです。

リアルタイムエディタでは、現在の入力と表示中の図が一時的に一致しない場合があります。

そのため、正常描画済みのSVGだけを出力する条件を追加しました。

ここまでの進捗を整理します。

Day 1
Flask最小構成
      ↓
Day 2
2ペインUI
      ↓
Day 3
リアルタイム描画
      ↓
Day 4
.mmd保存・読込
      ↓
Day 5
SVG出力・コピー

Day 5までで、入力・編集・表示・保存・読込・出力という基本的な機能がそろいました。

残り3回では、これらを再利用可能なMVPとして完成させます。

参考資料

今回の実装では、以下の公式資料を参照しました。


次回予告

Day 6|テンプレート機能をデータ駆動で作る

次回は、Mermaidエディタへテンプレート選択機能を追加します。

フローチャート、シーケンス図、状態遷移図などの初期コードをテンプレートとして登録し、ボタンや選択メニューから呼び出せるようにします。

特に重視するのは、テンプレートをJavaScriptの条件分岐へ直接埋め込まない設計です。

テンプレートデータ
        ↓
    選択UI
        ↓
    textarea
        ↓
  既存の描画処理
        ↓
     Preview

テンプレート定義と表示ロジックを分離し、将来的にJSONやデータベースへ移行できる構成を作ります。

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

この記事を書いた人

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

目次