zombie-mermaid 5.10.0 → 5.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +72 -99
- package/package.json +6 -6
- package/dist/src-BRkxY4xp.js +0 -439
- package/dist/src-CvLXAGlZ.js +0 -431
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire as e } from "node:module";
|
|
3
3
|
import { extname as t, format as n, parse as r } from "node:path";
|
|
4
|
-
import { MermaidParseError as i, THEMES as a,
|
|
5
|
-
import { access as
|
|
6
|
-
import { diagramColorsToAsciiTheme as
|
|
4
|
+
import { MermaidParseError as i, THEMES as a, escapeXml as o, isDirection as s, isWideChar as c } from "@zombie-mermaid/core";
|
|
5
|
+
import { access as l, readFile as u, writeFile as d } from "node:fs/promises";
|
|
6
|
+
import { diagramColorsToAsciiTheme as f, renderMermaidASCII as p } from "@zombie-mermaid/ascii-renderer";
|
|
7
7
|
import "@zombie-mermaid/mermaid-parser";
|
|
8
|
-
import { registerElk as
|
|
8
|
+
import { registerElk as m, renderMermaidSVG as h, validateMermaid as g } from "@zombie-mermaid/svg-renderer";
|
|
9
|
+
import { renderMermaidASCIIWithLayout as _ } from "./ascii-layout.js";
|
|
9
10
|
import { createServer as v } from "node:http";
|
|
10
11
|
import { StdioServerTransport as y } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
12
|
import { createMcpServer as b } from "@zombie-mermaid/mcp";
|
|
@@ -88,7 +89,7 @@ function j(e, t, n) {
|
|
|
88
89
|
let r = e[t + 1];
|
|
89
90
|
if (t + 1 >= e.length || r === void 0) throw Error(`${n} requires a value (one of TD, TB, BT, LR, RL)`);
|
|
90
91
|
let i = r.toUpperCase();
|
|
91
|
-
if (!
|
|
92
|
+
if (!s(i)) throw Error(`${n} requires one of TD, TB, BT, LR, RL, got: "${r}"`);
|
|
92
93
|
return i;
|
|
93
94
|
}
|
|
94
95
|
function M(e) {
|
|
@@ -231,7 +232,7 @@ function ae(e) {
|
|
|
231
232
|
function oe(e) {
|
|
232
233
|
if (e.includes(ie)) return 2;
|
|
233
234
|
let t = !1, n = !1;
|
|
234
|
-
for (let r of e)
|
|
235
|
+
for (let r of e) c(r) && (t = !0), ne(r) || (n = !0);
|
|
235
236
|
return t ? 2 : +!!n;
|
|
236
237
|
}
|
|
237
238
|
function se(e) {
|
|
@@ -246,39 +247,11 @@ function le(e) {
|
|
|
246
247
|
return e.replace(ce, "");
|
|
247
248
|
}
|
|
248
249
|
//#endregion
|
|
249
|
-
//#region src/ascii-layout.ts
|
|
250
|
-
async function ue(e, t = {}) {
|
|
251
|
-
let { layout: n, maxCols: r, ...i } = t, a = (t) => ({
|
|
252
|
-
text: m(e, i),
|
|
253
|
-
path: "standard",
|
|
254
|
-
...t === void 0 ? {} : { fallbackReason: t }
|
|
255
|
-
});
|
|
256
|
-
if (n !== "mermaid") return a();
|
|
257
|
-
let s = o(e);
|
|
258
|
-
if (s !== "flowchart") return a(`not applicable to ${s} diagrams (flowcharts only)`);
|
|
259
|
-
let c, l;
|
|
260
|
-
try {
|
|
261
|
-
[c, l] = await Promise.all([import("./src-CvLXAGlZ.js"), import("./src-BRkxY4xp.js")]);
|
|
262
|
-
} catch {
|
|
263
|
-
return a("elkjs 0.9.3 is not installed (pnpm add elkjs-0.9.3@npm:elkjs@0.9.3)");
|
|
264
|
-
}
|
|
265
|
-
try {
|
|
266
|
-
let t = await l.renderFlowchartAscii(e, r === void 0 ? void 0 : { maxCols: r });
|
|
267
|
-
return t.capped ? {
|
|
268
|
-
text: t.text,
|
|
269
|
-
path: "layout-faithful",
|
|
270
|
-
note: t.note
|
|
271
|
-
} : a(`layout-faithful output is ${t.stats.naturalCols} columns, over the ${r ?? l.DEFAULT_MAX_COLS}-column cap`);
|
|
272
|
-
} catch (e) {
|
|
273
|
-
return e instanceof c.UnsupportedFlowchartError ? a(`unsupported (${e.reasons.join("; ")})`) : a(`layout failed (${e instanceof Error ? e.message : String(e)})`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
//#endregion
|
|
277
250
|
//#region packages/mcp/src/tools/parse-diagnostics.ts
|
|
278
|
-
var
|
|
279
|
-
function
|
|
251
|
+
var ue = /^Line (\d+): ([\s\S]*)$/;
|
|
252
|
+
function N(e, t) {
|
|
280
253
|
if (!(e instanceof i) || e.line === void 0) return;
|
|
281
|
-
let { line: n, message: r } = e, a =
|
|
254
|
+
let { line: n, message: r } = e, a = ue.exec(r), o = t.split(/\r?\n/)[n - 1];
|
|
282
255
|
if (!(n < 1 || o === void 0)) return {
|
|
283
256
|
line: n,
|
|
284
257
|
sourceLine: o,
|
|
@@ -287,24 +260,24 @@ function P(e, t) {
|
|
|
287
260
|
}
|
|
288
261
|
//#endregion
|
|
289
262
|
//#region src/cli/diagnostics.ts
|
|
290
|
-
function
|
|
263
|
+
function P(e, t) {
|
|
291
264
|
if (!(e instanceof Error)) return e;
|
|
292
|
-
let n =
|
|
265
|
+
let n = N(e, t);
|
|
293
266
|
return n === void 0 ? e : /* @__PURE__ */ Error(`${e.message}\n ${n.line} | ${n.sourceLine}`);
|
|
294
267
|
}
|
|
295
268
|
//#endregion
|
|
296
269
|
//#region src/cli/html-viewer-client-source.ts
|
|
297
|
-
var I = "// @ts-check\n/// <reference lib=\"dom\" />\n// ============================================================================\n// Client script for the CLI's self-contained HTML viewer (`render --html`).\n//\n// Inlined verbatim into the viewer page by src/cli/html-viewer.ts (imported\n// with Vite's `?raw`), so this file must stay plain browser JavaScript: no\n// imports, no TypeScript syntax, no build step. It is checked by tsc via\n// `// @ts-check` + JSDoc and linted like any other source file.\n//\n// This script ships only inside the HTML *viewer file* the CLI writes. It is\n// not — and must never become — part of the library's SVG output, which\n// stays script-free by decision (docs/decisions/no-script-interactivity.md).\n// ============================================================================\n\n;(() => {\n const stage = document.getElementById('stage')\n const diagram = document.getElementById('diagram')\n const svg = diagram ? diagram.querySelector('svg') : null\n const zoomLabel = document.getElementById('zoom')\n const themeButton = document.getElementById('theme')\n if (!stage || !diagram || !svg || !zoomLabel || !themeButton) return\n\n const MIN_SCALE = 0.05\n const MAX_SCALE = 40\n const KEY_PAN_STEP = 40\n const KEY_PAN_STEP_FAST = 160\n\n let scale = 1\n let tx = 0\n let ty = 0\n\n // -- Geometry ---------------------------------------------------------\n\n // Assigned as `const ... = () => {}` rather than `function` declarations\n // from here through stageCenter (and release/setMode further below):\n // these all read stage/diagram/svg/zoomLabel/themeButton, which the\n // guard above narrowed from `T | null` to `T` — a narrowing that only\n // survives for a function *expression* defined after the guard, not a\n // hoisted `function` declaration (tsc can't rule out the declaration\n // being invoked, in theory, before the guard runs).\n\n /** Natural (unscaled) size of the diagram, from the root's width/height or viewBox. */\n const naturalSize = () => {\n const w = Number(svg.getAttribute('width'))\n const h = Number(svg.getAttribute('height'))\n if (w > 0 && h > 0) return { w, h }\n const vb = svg.viewBox.baseVal\n if (vb && vb.width > 0 && vb.height > 0)\n return { w: vb.width, h: vb.height }\n const r = diagram.getBoundingClientRect()\n return { w: r.width / scale || 1, h: r.height / scale || 1 }\n }\n\n /** @param {number} s */\n function clampScale(s) {\n return Math.min(MAX_SCALE, Math.max(MIN_SCALE, s))\n }\n\n const apply = () => {\n diagram.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`\n zoomLabel.textContent = `${Math.round(scale * 100)}%`\n }\n\n /**\n * Zoom by `factor` keeping the stage point (cx, cy) fixed under the cursor.\n * @param {number} factor\n * @param {number} cx\n * @param {number} cy\n */\n function zoomAt(factor, cx, cy) {\n const next = clampScale(scale * factor)\n const k = next / scale\n tx = cx - (cx - tx) * k\n ty = cy - (cy - ty) * k\n scale = next\n apply()\n }\n\n const fit = () => {\n const r = stage.getBoundingClientRect()\n const { w, h } = naturalSize()\n scale = clampScale(Math.min(r.width / w, r.height / h) * 0.95)\n tx = (r.width - w * scale) / 2\n ty = (r.height - h * scale) / 2\n apply()\n }\n\n const oneToOne = () => {\n const r = stage.getBoundingClientRect()\n const { w, h } = naturalSize()\n scale = 1\n tx = Math.max(0, (r.width - w) / 2)\n ty = Math.max(0, (r.height - h) / 2)\n apply()\n }\n\n const stageCenter = () => {\n const r = stage.getBoundingClientRect()\n return { x: r.width / 2, y: r.height / 2 }\n }\n\n // -- Wheel: plain scroll pans; ctrl/cmd + wheel (which is also what a\n // trackpad pinch produces) zooms around the cursor. -------------------\n\n stage.addEventListener(\n 'wheel',\n (e) => {\n e.preventDefault()\n const r = stage.getBoundingClientRect()\n if (e.ctrlKey || e.metaKey) {\n zoomAt(\n Math.exp(-e.deltaY * 0.01),\n e.clientX - r.left,\n e.clientY - r.top,\n )\n } else {\n tx -= e.deltaX\n ty -= e.deltaY\n apply()\n }\n },\n { passive: false },\n )\n\n // -- Pointers: one pointer drags, two pinch-zoom around their midpoint. --\n\n /** @type {Map<number, {x: number, y: number}>} */\n const pointers = new Map()\n /** @type {{dist: number, mid: {x: number, y: number}} | null} */\n let lastPinch = null\n\n stage.addEventListener('pointerdown', (e) => {\n if (e.pointerType === 'mouse' && e.button !== 0) return\n stage.setPointerCapture(e.pointerId)\n pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })\n stage.classList.add('dragging')\n })\n\n stage.addEventListener('pointermove', (e) => {\n const prev = pointers.get(e.pointerId)\n if (!prev) return\n const current = { x: e.clientX, y: e.clientY }\n pointers.set(e.pointerId, current)\n if (pointers.size === 1) {\n tx += current.x - prev.x\n ty += current.y - prev.y\n apply()\n return\n }\n if (pointers.size === 2) {\n const [a, b] = [...pointers.values()]\n if (!a || !b) return\n const dist = Math.hypot(a.x - b.x, a.y - b.y)\n const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }\n if (lastPinch && lastPinch.dist > 0) {\n const r = stage.getBoundingClientRect()\n zoomAt(dist / lastPinch.dist, mid.x - r.left, mid.y - r.top)\n tx += mid.x - lastPinch.mid.x\n ty += mid.y - lastPinch.mid.y\n apply()\n }\n lastPinch = { dist, mid }\n }\n })\n\n /** @param {PointerEvent} e */\n const release = (e) => {\n pointers.delete(e.pointerId)\n if (pointers.size < 2) lastPinch = null\n if (pointers.size === 0) stage.classList.remove('dragging')\n }\n stage.addEventListener('pointerup', release)\n stage.addEventListener('pointercancel', release)\n\n stage.addEventListener('dblclick', (e) => {\n const r = stage.getBoundingClientRect()\n zoomAt(scale < 1 ? 1 / scale : 2, e.clientX - r.left, e.clientY - r.top)\n })\n\n // -- Theme: follows prefers-color-scheme until the user toggles, unless\n // the page was generated with an explicit --theme (data-theme preset). --\n\n const root = document.documentElement\n const darkMedia = matchMedia('(prefers-color-scheme: dark)')\n let userChose = root.dataset.themeSource === 'theme'\n\n function currentMode() {\n const preset = root.dataset.theme\n if (preset === 'dark' || preset === 'light') return preset\n return darkMedia.matches ? 'dark' : 'light'\n }\n\n /** @param {'light' | 'dark'} mode */\n const setMode = (mode) => {\n root.dataset.theme = mode\n themeButton.textContent = mode === 'dark' ? 'Light' : 'Dark'\n themeButton.setAttribute('aria-pressed', String(mode === 'dark'))\n themeButton.setAttribute(\n 'aria-label',\n `Switch to ${mode === 'dark' ? 'light' : 'dark'} mode (T)`,\n )\n }\n\n function toggleMode() {\n userChose = true\n setMode(currentMode() === 'dark' ? 'light' : 'dark')\n }\n\n darkMedia.addEventListener('change', () => {\n if (!userChose) {\n delete root.dataset.theme\n setMode(currentMode())\n delete root.dataset.theme\n }\n })\n // Reflect the initial mode on the button without pinning it: leave\n // data-theme unset for system mode so a later OS change still applies.\n themeButton.textContent = currentMode() === 'dark' ? 'Light' : 'Dark'\n themeButton.setAttribute('aria-pressed', String(currentMode() === 'dark'))\n\n // -- Buttons -------------------------------------------------------------\n\n /**\n * @param {string} id\n * @param {() => void} handler\n */\n function onClick(id, handler) {\n const el = document.getElementById(id)\n if (el) el.addEventListener('click', handler)\n }\n onClick('zoom-in', () => {\n const c = stageCenter()\n zoomAt(1.25, c.x, c.y)\n })\n onClick('zoom-out', () => {\n const c = stageCenter()\n zoomAt(0.8, c.x, c.y)\n })\n onClick('fit', fit)\n onClick('one', oneToOne)\n onClick('theme', toggleMode)\n\n // -- Keyboard (stage must be focused; it has tabindex=0) -----------------\n\n stage.addEventListener('keydown', (e) => {\n const c = stageCenter()\n const step = e.shiftKey ? KEY_PAN_STEP_FAST : KEY_PAN_STEP\n switch (e.key) {\n case '+':\n case '=':\n zoomAt(1.25, c.x, c.y)\n break\n case '-':\n case '_':\n zoomAt(0.8, c.x, c.y)\n break\n case '0':\n fit()\n break\n case '1':\n oneToOne()\n break\n case 'ArrowLeft':\n tx += step\n apply()\n break\n case 'ArrowRight':\n tx -= step\n apply()\n break\n case 'ArrowUp':\n ty += step\n apply()\n break\n case 'ArrowDown':\n ty -= step\n apply()\n break\n case 't':\n case 'T':\n toggleMode()\n break\n default:\n return\n }\n e.preventDefault()\n })\n\n fit()\n stage.focus({ preventScroll: true })\n})()\n", L = ":root {\n color-scheme: light dark;\n --bg: #f4f4f5;\n --fg: #18181b;\n --chrome-bg: #ffffff;\n --chrome-border: #e4e4e7;\n --chrome-fg: #3f3f46;\n --chrome-fg-hover: #18181b;\n --focus-ring: #2563eb;\n }\n :root[data-theme='dark'] {\n --bg: #09090b;\n --fg: #fafafa;\n --chrome-bg: #18181b;\n --chrome-border: #3f3f46;\n --chrome-fg: #d4d4d8;\n --chrome-fg-hover: #fafafa;\n --focus-ring: #60a5fa;\n }\n @media (prefers-color-scheme: dark) {\n :root:not([data-theme='light']) {\n --bg: #09090b;\n --fg: #fafafa;\n --chrome-bg: #18181b;\n --chrome-border: #3f3f46;\n --chrome-fg: #d4d4d8;\n --chrome-fg-hover: #fafafa;\n --focus-ring: #60a5fa;\n }\n }\n * { box-sizing: border-box; }\n html, body {\n margin: 0;\n height: 100%;\n background: var(--bg);\n color: var(--fg);\n font: 14px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n }\n #stage {\n position: fixed;\n inset: 0;\n overflow: hidden;\n cursor: grab;\n touch-action: none;\n outline: none;\n }\n #stage.dragging { cursor: grabbing; }\n #diagram {\n position: absolute;\n top: 0;\n left: 0;\n transform-origin: 0 0;\n will-change: transform;\n }\n #diagram svg { display: block; }\n #toolbar {\n position: fixed;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 6px;\n border-radius: 10px;\n background: var(--chrome-bg);\n border: 1px solid var(--chrome-border);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);\n }\n #toolbar button {\n appearance: none;\n border: none;\n background: transparent;\n color: var(--chrome-fg);\n font: inherit;\n font-size: 13px;\n padding: 6px 10px;\n border-radius: 6px;\n cursor: pointer;\n min-width: 32px;\n }\n #toolbar button:hover { color: var(--chrome-fg-hover); background: var(--chrome-border); }\n #toolbar button:focus-visible {\n outline: 2px solid var(--focus-ring);\n outline-offset: 1px;\n }\n #zoom {\n min-width: 3.5em;\n text-align: center;\n color: var(--chrome-fg);\n font-variant-numeric: tabular-nums;\n }\n #stage:focus-visible {\n box-shadow: inset 0 0 0 2px var(--focus-ring);\n }\n @media print {\n #toolbar { display: none; }\n #stage { position: static; overflow: visible; }\n /* Genuinely needs !important (zombie-mermaid#1080 audit): the client\n * script's apply() sets #diagram's transform inline on every pan/zoom,\n * so whatever pan/zoom state the viewer was left in is a live inline\n * style this print rule must beat -- there's no static default to\n * hoist it to instead. */\n #diagram { position: static; transform: none !important; }\n }";
|
|
298
|
-
function
|
|
270
|
+
var F = "// @ts-check\n/// <reference lib=\"dom\" />\n// ============================================================================\n// Client script for the CLI's self-contained HTML viewer (`render --html`).\n//\n// Inlined verbatim into the viewer page by src/cli/html-viewer.ts (imported\n// with Vite's `?raw`), so this file must stay plain browser JavaScript: no\n// imports, no TypeScript syntax, no build step. It is checked by tsc via\n// `// @ts-check` + JSDoc and linted like any other source file.\n//\n// This script ships only inside the HTML *viewer file* the CLI writes. It is\n// not — and must never become — part of the library's SVG output, which\n// stays script-free by decision (docs/decisions/no-script-interactivity.md).\n// ============================================================================\n\n;(() => {\n const stage = document.getElementById('stage')\n const diagram = document.getElementById('diagram')\n const svg = diagram ? diagram.querySelector('svg') : null\n const zoomLabel = document.getElementById('zoom')\n const themeButton = document.getElementById('theme')\n if (!stage || !diagram || !svg || !zoomLabel || !themeButton) return\n\n const MIN_SCALE = 0.05\n const MAX_SCALE = 40\n const KEY_PAN_STEP = 40\n const KEY_PAN_STEP_FAST = 160\n\n let scale = 1\n let tx = 0\n let ty = 0\n\n // -- Geometry ---------------------------------------------------------\n\n // Assigned as `const ... = () => {}` rather than `function` declarations\n // from here through stageCenter (and release/setMode further below):\n // these all read stage/diagram/svg/zoomLabel/themeButton, which the\n // guard above narrowed from `T | null` to `T` — a narrowing that only\n // survives for a function *expression* defined after the guard, not a\n // hoisted `function` declaration (tsc can't rule out the declaration\n // being invoked, in theory, before the guard runs).\n\n /** Natural (unscaled) size of the diagram, from the root's width/height or viewBox. */\n const naturalSize = () => {\n const w = Number(svg.getAttribute('width'))\n const h = Number(svg.getAttribute('height'))\n if (w > 0 && h > 0) return { w, h }\n const vb = svg.viewBox.baseVal\n if (vb && vb.width > 0 && vb.height > 0)\n return { w: vb.width, h: vb.height }\n const r = diagram.getBoundingClientRect()\n return { w: r.width / scale || 1, h: r.height / scale || 1 }\n }\n\n /** @param {number} s */\n function clampScale(s) {\n return Math.min(MAX_SCALE, Math.max(MIN_SCALE, s))\n }\n\n const apply = () => {\n diagram.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`\n zoomLabel.textContent = `${Math.round(scale * 100)}%`\n }\n\n /**\n * Zoom by `factor` keeping the stage point (cx, cy) fixed under the cursor.\n * @param {number} factor\n * @param {number} cx\n * @param {number} cy\n */\n function zoomAt(factor, cx, cy) {\n const next = clampScale(scale * factor)\n const k = next / scale\n tx = cx - (cx - tx) * k\n ty = cy - (cy - ty) * k\n scale = next\n apply()\n }\n\n const fit = () => {\n const r = stage.getBoundingClientRect()\n const { w, h } = naturalSize()\n scale = clampScale(Math.min(r.width / w, r.height / h) * 0.95)\n tx = (r.width - w * scale) / 2\n ty = (r.height - h * scale) / 2\n apply()\n }\n\n const oneToOne = () => {\n const r = stage.getBoundingClientRect()\n const { w, h } = naturalSize()\n scale = 1\n tx = Math.max(0, (r.width - w) / 2)\n ty = Math.max(0, (r.height - h) / 2)\n apply()\n }\n\n const stageCenter = () => {\n const r = stage.getBoundingClientRect()\n return { x: r.width / 2, y: r.height / 2 }\n }\n\n // -- Wheel: plain scroll pans; ctrl/cmd + wheel (which is also what a\n // trackpad pinch produces) zooms around the cursor. -------------------\n\n stage.addEventListener(\n 'wheel',\n (e) => {\n e.preventDefault()\n const r = stage.getBoundingClientRect()\n if (e.ctrlKey || e.metaKey) {\n zoomAt(\n Math.exp(-e.deltaY * 0.01),\n e.clientX - r.left,\n e.clientY - r.top,\n )\n } else {\n tx -= e.deltaX\n ty -= e.deltaY\n apply()\n }\n },\n { passive: false },\n )\n\n // -- Pointers: one pointer drags, two pinch-zoom around their midpoint. --\n\n /** @type {Map<number, {x: number, y: number}>} */\n const pointers = new Map()\n /** @type {{dist: number, mid: {x: number, y: number}} | null} */\n let lastPinch = null\n\n stage.addEventListener('pointerdown', (e) => {\n if (e.pointerType === 'mouse' && e.button !== 0) return\n stage.setPointerCapture(e.pointerId)\n pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })\n stage.classList.add('dragging')\n })\n\n stage.addEventListener('pointermove', (e) => {\n const prev = pointers.get(e.pointerId)\n if (!prev) return\n const current = { x: e.clientX, y: e.clientY }\n pointers.set(e.pointerId, current)\n if (pointers.size === 1) {\n tx += current.x - prev.x\n ty += current.y - prev.y\n apply()\n return\n }\n if (pointers.size === 2) {\n const [a, b] = [...pointers.values()]\n if (!a || !b) return\n const dist = Math.hypot(a.x - b.x, a.y - b.y)\n const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }\n if (lastPinch && lastPinch.dist > 0) {\n const r = stage.getBoundingClientRect()\n zoomAt(dist / lastPinch.dist, mid.x - r.left, mid.y - r.top)\n tx += mid.x - lastPinch.mid.x\n ty += mid.y - lastPinch.mid.y\n apply()\n }\n lastPinch = { dist, mid }\n }\n })\n\n /** @param {PointerEvent} e */\n const release = (e) => {\n pointers.delete(e.pointerId)\n if (pointers.size < 2) lastPinch = null\n if (pointers.size === 0) stage.classList.remove('dragging')\n }\n stage.addEventListener('pointerup', release)\n stage.addEventListener('pointercancel', release)\n\n stage.addEventListener('dblclick', (e) => {\n const r = stage.getBoundingClientRect()\n zoomAt(scale < 1 ? 1 / scale : 2, e.clientX - r.left, e.clientY - r.top)\n })\n\n // -- Theme: follows prefers-color-scheme until the user toggles, unless\n // the page was generated with an explicit --theme (data-theme preset). --\n\n const root = document.documentElement\n const darkMedia = matchMedia('(prefers-color-scheme: dark)')\n let userChose = root.dataset.themeSource === 'theme'\n\n function currentMode() {\n const preset = root.dataset.theme\n if (preset === 'dark' || preset === 'light') return preset\n return darkMedia.matches ? 'dark' : 'light'\n }\n\n /** @param {'light' | 'dark'} mode */\n const setMode = (mode) => {\n root.dataset.theme = mode\n themeButton.textContent = mode === 'dark' ? 'Light' : 'Dark'\n themeButton.setAttribute('aria-pressed', String(mode === 'dark'))\n themeButton.setAttribute(\n 'aria-label',\n `Switch to ${mode === 'dark' ? 'light' : 'dark'} mode (T)`,\n )\n }\n\n function toggleMode() {\n userChose = true\n setMode(currentMode() === 'dark' ? 'light' : 'dark')\n }\n\n darkMedia.addEventListener('change', () => {\n if (!userChose) {\n delete root.dataset.theme\n setMode(currentMode())\n delete root.dataset.theme\n }\n })\n // Reflect the initial mode on the button without pinning it: leave\n // data-theme unset for system mode so a later OS change still applies.\n themeButton.textContent = currentMode() === 'dark' ? 'Light' : 'Dark'\n themeButton.setAttribute('aria-pressed', String(currentMode() === 'dark'))\n\n // -- Buttons -------------------------------------------------------------\n\n /**\n * @param {string} id\n * @param {() => void} handler\n */\n function onClick(id, handler) {\n const el = document.getElementById(id)\n if (el) el.addEventListener('click', handler)\n }\n onClick('zoom-in', () => {\n const c = stageCenter()\n zoomAt(1.25, c.x, c.y)\n })\n onClick('zoom-out', () => {\n const c = stageCenter()\n zoomAt(0.8, c.x, c.y)\n })\n onClick('fit', fit)\n onClick('one', oneToOne)\n onClick('theme', toggleMode)\n\n // -- Keyboard (stage must be focused; it has tabindex=0) -----------------\n\n stage.addEventListener('keydown', (e) => {\n const c = stageCenter()\n const step = e.shiftKey ? KEY_PAN_STEP_FAST : KEY_PAN_STEP\n switch (e.key) {\n case '+':\n case '=':\n zoomAt(1.25, c.x, c.y)\n break\n case '-':\n case '_':\n zoomAt(0.8, c.x, c.y)\n break\n case '0':\n fit()\n break\n case '1':\n oneToOne()\n break\n case 'ArrowLeft':\n tx += step\n apply()\n break\n case 'ArrowRight':\n tx -= step\n apply()\n break\n case 'ArrowUp':\n ty += step\n apply()\n break\n case 'ArrowDown':\n ty -= step\n apply()\n break\n case 't':\n case 'T':\n toggleMode()\n break\n default:\n return\n }\n e.preventDefault()\n })\n\n fit()\n stage.focus({ preventScroll: true })\n})()\n", I = ":root {\n color-scheme: light dark;\n --bg: #f4f4f5;\n --fg: #18181b;\n --chrome-bg: #ffffff;\n --chrome-border: #e4e4e7;\n --chrome-fg: #3f3f46;\n --chrome-fg-hover: #18181b;\n --focus-ring: #2563eb;\n }\n :root[data-theme='dark'] {\n --bg: #09090b;\n --fg: #fafafa;\n --chrome-bg: #18181b;\n --chrome-border: #3f3f46;\n --chrome-fg: #d4d4d8;\n --chrome-fg-hover: #fafafa;\n --focus-ring: #60a5fa;\n }\n @media (prefers-color-scheme: dark) {\n :root:not([data-theme='light']) {\n --bg: #09090b;\n --fg: #fafafa;\n --chrome-bg: #18181b;\n --chrome-border: #3f3f46;\n --chrome-fg: #d4d4d8;\n --chrome-fg-hover: #fafafa;\n --focus-ring: #60a5fa;\n }\n }\n * { box-sizing: border-box; }\n html, body {\n margin: 0;\n height: 100%;\n background: var(--bg);\n color: var(--fg);\n font: 14px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n }\n #stage {\n position: fixed;\n inset: 0;\n overflow: hidden;\n cursor: grab;\n touch-action: none;\n outline: none;\n }\n #stage.dragging { cursor: grabbing; }\n #diagram {\n position: absolute;\n top: 0;\n left: 0;\n transform-origin: 0 0;\n will-change: transform;\n }\n #diagram svg { display: block; }\n #toolbar {\n position: fixed;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 6px;\n border-radius: 10px;\n background: var(--chrome-bg);\n border: 1px solid var(--chrome-border);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);\n }\n #toolbar button {\n appearance: none;\n border: none;\n background: transparent;\n color: var(--chrome-fg);\n font: inherit;\n font-size: 13px;\n padding: 6px 10px;\n border-radius: 6px;\n cursor: pointer;\n min-width: 32px;\n }\n #toolbar button:hover { color: var(--chrome-fg-hover); background: var(--chrome-border); }\n #toolbar button:focus-visible {\n outline: 2px solid var(--focus-ring);\n outline-offset: 1px;\n }\n #zoom {\n min-width: 3.5em;\n text-align: center;\n color: var(--chrome-fg);\n font-variant-numeric: tabular-nums;\n }\n #stage:focus-visible {\n box-shadow: inset 0 0 0 2px var(--focus-ring);\n }\n @media print {\n #toolbar { display: none; }\n #stage { position: static; overflow: visible; }\n /* Genuinely needs !important (zombie-mermaid#1080 audit): the client\n * script's apply() sets #diagram's transform inline on every pan/zoom,\n * so whatever pan/zoom state the viewer was left in is a live inline\n * style this print rule must beat -- there's no static default to\n * hoist it to instead. */\n #diagram { position: static; transform: none !important; }\n }";
|
|
271
|
+
function L(e) {
|
|
299
272
|
let t = e.title?.trim() || "Mermaid diagram";
|
|
300
273
|
return `<!doctype html>
|
|
301
274
|
<html lang="en">
|
|
302
275
|
<head>
|
|
303
276
|
<meta charset="utf-8" />
|
|
304
277
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
305
|
-
<title>${
|
|
278
|
+
<title>${o(t)}</title>
|
|
306
279
|
<style>
|
|
307
|
-
${
|
|
280
|
+
${I}
|
|
308
281
|
</style>
|
|
309
282
|
</head>
|
|
310
283
|
<body>
|
|
@@ -322,7 +295,7 @@ ${e.svg}
|
|
|
322
295
|
<button id="theme" type="button" aria-pressed="false" aria-label="Switch to dark mode (T)">Dark</button>
|
|
323
296
|
</div>
|
|
324
297
|
<script>
|
|
325
|
-
${
|
|
298
|
+
${F}
|
|
326
299
|
<\/script>
|
|
327
300
|
</body>
|
|
328
301
|
</html>
|
|
@@ -330,7 +303,7 @@ ${I}
|
|
|
330
303
|
}
|
|
331
304
|
//#endregion
|
|
332
305
|
//#region src/cli/png.ts
|
|
333
|
-
async function
|
|
306
|
+
async function R(e) {
|
|
334
307
|
let t;
|
|
335
308
|
try {
|
|
336
309
|
t = await import("@resvg/resvg-js");
|
|
@@ -342,16 +315,16 @@ async function z(e) {
|
|
|
342
315
|
}
|
|
343
316
|
//#endregion
|
|
344
317
|
//#region src/cli/render.ts
|
|
345
|
-
var
|
|
318
|
+
var z = 80, B = 1, V = 1, H = 0, U = /\x1b\[[0-9;]*m/g;
|
|
346
319
|
function W(e) {
|
|
347
|
-
return e.split("\n").reduce((e, t) => Math.max(e, se(le(t.replace(
|
|
320
|
+
return e.split("\n").reduce((e, t) => Math.max(e, se(le(t.replace(U, "")))), 0);
|
|
348
321
|
}
|
|
349
322
|
function G(e) {
|
|
350
|
-
return e === "auto" ? process.stdout.columns ??
|
|
323
|
+
return e === "auto" ? process.stdout.columns ?? z : e;
|
|
351
324
|
}
|
|
352
|
-
async function
|
|
325
|
+
async function de(e, t, n, r) {
|
|
353
326
|
let i = t ?? process.stdout, a = r ?? process.stderr, o;
|
|
354
|
-
if (e.input !== void 0) o = await
|
|
327
|
+
if (e.input !== void 0) o = await u(e.input, "utf-8");
|
|
355
328
|
else if (n !== void 0) o = n;
|
|
356
329
|
else {
|
|
357
330
|
if (process.stdin.isTTY) throw Error("No input file specified and stdin is a terminal. Pipe a diagram or pass a file path.");
|
|
@@ -359,45 +332,45 @@ async function fe(e, t, n, r) {
|
|
|
359
332
|
}
|
|
360
333
|
if (o.trim().length === 0) throw Error("Empty input — provide a Mermaid diagram via file or stdin");
|
|
361
334
|
try {
|
|
362
|
-
await
|
|
335
|
+
await fe(e, o, i, a);
|
|
363
336
|
} catch (e) {
|
|
364
|
-
throw
|
|
337
|
+
throw P(e, o);
|
|
365
338
|
}
|
|
366
339
|
}
|
|
367
|
-
async function
|
|
340
|
+
async function fe(e, t, n, i) {
|
|
368
341
|
let o;
|
|
369
342
|
if (e.theme !== void 0 && (o = a[e.theme], o === void 0)) {
|
|
370
343
|
let t = Object.keys(a).join(", ");
|
|
371
344
|
throw Error(`Unknown theme: "${e.theme}". Available themes: ${t}`);
|
|
372
345
|
}
|
|
373
|
-
let s = e.svg && e.output === "-", c = e.html && e.output === "-", l = e.png && e.output === "-", u = e.ascii && !e.svg && !e.html && !e.png && e.output !== void 0 && e.output !== "-" ? e.output : void 0, d = e.svg && !s ? e.output : void 0,
|
|
346
|
+
let s = e.svg && e.output === "-", c = e.html && e.output === "-", l = e.png && e.output === "-", u = e.ascii && !e.svg && !e.html && !e.png && e.output !== void 0 && e.output !== "-" ? e.output : void 0, d = e.svg && !s ? e.output : void 0, m = e.html && !c ? e.output : void 0, g = e.png && !l ? e.output : void 0;
|
|
374
347
|
for (let t of [
|
|
375
348
|
u,
|
|
376
349
|
d,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
]) t !== void 0 && await
|
|
350
|
+
m,
|
|
351
|
+
g
|
|
352
|
+
]) t !== void 0 && await pe(t, e.force);
|
|
380
353
|
if (e.ascii) {
|
|
381
354
|
let r = o && u === void 0 ? {
|
|
382
355
|
colorMode: "auto",
|
|
383
|
-
theme:
|
|
356
|
+
theme: f(o)
|
|
384
357
|
} : { colorMode: "none" };
|
|
385
358
|
e.paddingX !== void 0 && (r.paddingX = e.paddingX), e.paddingY !== void 0 && (r.paddingY = e.paddingY), e.borderPadding !== void 0 && (r.boxBorderPadding = e.borderPadding), e.coords && (r.showCoords = !0), e.profile !== void 0 && (r.profile = e.profile), e.direction !== void 0 && (r.direction = e.direction), e.hyperlinks && u === void 0 && (r.hyperlinks = !0);
|
|
386
359
|
let a, s = !1;
|
|
387
360
|
if (e.layout !== void 0) {
|
|
388
|
-
let n = e.layout === "mermaid-wide" ? Infinity : e.maxWidth === void 0 ? void 0 : G(e.maxWidth), o = await
|
|
361
|
+
let n = e.layout === "mermaid-wide" ? Infinity : e.maxWidth === void 0 ? void 0 : G(e.maxWidth), o = await _(t, {
|
|
389
362
|
...r,
|
|
390
363
|
layout: "mermaid",
|
|
391
364
|
maxCols: n
|
|
392
365
|
});
|
|
393
366
|
a = o.text, s = o.path === "layout-faithful", i.write(s ? `layout-faithful: used mermaid's ELK layout (mermaid 12.1.0 + elkjs 0.9.3). ${o.note ?? ""}`.trimEnd() + "\n" : `layout-faithful: ${o.fallbackReason ?? "unavailable"}; used standard renderer\n`);
|
|
394
|
-
} else a =
|
|
367
|
+
} else a = p(t, r);
|
|
395
368
|
if (e.maxWidth !== void 0) {
|
|
396
369
|
let n = G(e.maxWidth), o = W(a), c = !1;
|
|
397
370
|
if (o > n && !s) {
|
|
398
|
-
let e = r.paddingX ?? 5, n = r.paddingY ?? 5, i = r.boxBorderPadding ?? 1, s = Math.min(e,
|
|
371
|
+
let e = r.paddingX ?? 5, n = r.paddingY ?? 5, i = r.boxBorderPadding ?? 1, s = Math.min(e, B), l = Math.min(n, V), u = Math.min(i, H);
|
|
399
372
|
if (s < e || l < n || u < i) {
|
|
400
|
-
let e =
|
|
373
|
+
let e = p(t, {
|
|
401
374
|
...r,
|
|
402
375
|
paddingX: s,
|
|
403
376
|
paddingY: l,
|
|
@@ -407,9 +380,9 @@ async function pe(e, t, n, i) {
|
|
|
407
380
|
}
|
|
408
381
|
}
|
|
409
382
|
if (o > n) {
|
|
410
|
-
let t = e.maxWidth === "auto" ? "detected terminal width" : "--max-width", r = c ? `Compact spacing (-x ${
|
|
383
|
+
let t = e.maxWidth === "auto" ? "detected terminal width" : "--max-width", r = c ? `Compact spacing (-x ${B} -y ${V} -p ${H}) was already applied automatically. ` : "", a = c ? "Try a narrower direction (LR vs TD), shorter labels, or widen your terminal." : "Try -x/-y/-p for tighter spacing, a narrower direction (LR vs TD), or widen your terminal.";
|
|
411
384
|
i.write(`Warning: ASCII output is ${o} columns wide, exceeding ${t} of ${n}. ${r}zombie-mermaid does not reflow diagram structure to fit (no label wrapping or direction-flip cascade — see https://github.com/dfadler/zombie-mermaid/issues/335). ${a}\n`);
|
|
412
|
-
} else c && i.write(`Note: ASCII output exceeded ${n} columns at the requested spacing; applied compact spacing automatically (-x ${
|
|
385
|
+
} else c && i.write(`Note: ASCII output exceeded ${n} columns at the requested spacing; applied compact spacing automatically (-x ${B} -y ${V} -p ${H}) to fit.\n`);
|
|
413
386
|
}
|
|
414
387
|
await q(K(u === void 0, u), a + "\n", n, e.force);
|
|
415
388
|
}
|
|
@@ -419,15 +392,15 @@ async function pe(e, t, n, i) {
|
|
|
419
392
|
resolveColors: e.resolveColors
|
|
420
393
|
};
|
|
421
394
|
e.direction !== void 0 && (r.direction = e.direction);
|
|
422
|
-
let i =
|
|
395
|
+
let i = h(t, r);
|
|
423
396
|
await q(K(s, d), i, n, e.force);
|
|
424
397
|
}
|
|
425
398
|
if (e.html) {
|
|
426
|
-
let i =
|
|
427
|
-
svg:
|
|
399
|
+
let i = L({
|
|
400
|
+
svg: h(t, o ?? {}),
|
|
428
401
|
title: e.input ? r(e.input).name : void 0
|
|
429
402
|
});
|
|
430
|
-
await q(K(c,
|
|
403
|
+
await q(K(c, m), i, n, e.force);
|
|
431
404
|
}
|
|
432
405
|
if (e.png) {
|
|
433
406
|
let r = {
|
|
@@ -435,8 +408,8 @@ async function pe(e, t, n, i) {
|
|
|
435
408
|
resolveColors: !0
|
|
436
409
|
};
|
|
437
410
|
e.direction !== void 0 && (r.direction = e.direction);
|
|
438
|
-
let i = await
|
|
439
|
-
await q(K(l,
|
|
411
|
+
let i = await R(h(t, r));
|
|
412
|
+
await q(K(l, g), i, n, e.force);
|
|
440
413
|
}
|
|
441
414
|
}
|
|
442
415
|
function K(e, t) {
|
|
@@ -450,25 +423,25 @@ async function q(e, t, n, r) {
|
|
|
450
423
|
case "stdout":
|
|
451
424
|
n.write(t);
|
|
452
425
|
break;
|
|
453
|
-
case "file": await
|
|
426
|
+
case "file": await me(e.path, t, r);
|
|
454
427
|
}
|
|
455
428
|
}
|
|
456
429
|
function J(e) {
|
|
457
430
|
return /* @__PURE__ */ Error(`Refusing to overwrite existing file "${e}" — pass --force to replace it`);
|
|
458
431
|
}
|
|
459
|
-
async function
|
|
432
|
+
async function pe(e, t) {
|
|
460
433
|
if (t) return;
|
|
461
434
|
let n = !0;
|
|
462
435
|
try {
|
|
463
|
-
await
|
|
436
|
+
await l(e);
|
|
464
437
|
} catch {
|
|
465
438
|
n = !1;
|
|
466
439
|
}
|
|
467
440
|
if (n) throw J(e);
|
|
468
441
|
}
|
|
469
|
-
async function
|
|
442
|
+
async function me(e, t, n) {
|
|
470
443
|
try {
|
|
471
|
-
await
|
|
444
|
+
await d(e, t, {
|
|
472
445
|
encoding: "utf-8",
|
|
473
446
|
flag: n ? "w" : "wx"
|
|
474
447
|
});
|
|
@@ -484,9 +457,9 @@ async function Y() {
|
|
|
484
457
|
}
|
|
485
458
|
//#endregion
|
|
486
459
|
//#region src/cli/validate.ts
|
|
487
|
-
async function
|
|
460
|
+
async function he(e, t) {
|
|
488
461
|
let n;
|
|
489
|
-
if (e.input !== void 0) n = await
|
|
462
|
+
if (e.input !== void 0) n = await u(e.input, "utf-8");
|
|
490
463
|
else if (t !== void 0) n = t;
|
|
491
464
|
else {
|
|
492
465
|
if (process.stdin.isTTY) throw Error("No input file specified and stdin is a terminal. Pipe a diagram or pass a file path.");
|
|
@@ -495,20 +468,20 @@ async function ge(e, t) {
|
|
|
495
468
|
let r = n;
|
|
496
469
|
if (r.trim().length === 0) throw Error("Empty input — provide a Mermaid diagram via file or stdin");
|
|
497
470
|
try {
|
|
498
|
-
|
|
471
|
+
g(r);
|
|
499
472
|
} catch (e) {
|
|
500
|
-
throw
|
|
473
|
+
throw P(e, r);
|
|
501
474
|
}
|
|
502
475
|
console.log("valid");
|
|
503
476
|
}
|
|
504
477
|
//#endregion
|
|
505
478
|
//#region src/cli/web.ts
|
|
506
|
-
var
|
|
507
|
-
function
|
|
479
|
+
var ge = 1e6;
|
|
480
|
+
function _e(e) {
|
|
508
481
|
return new Promise((t, n) => {
|
|
509
482
|
let r = [], i = 0;
|
|
510
483
|
e.on("data", (t) => {
|
|
511
|
-
if (i += t.length, i >
|
|
484
|
+
if (i += t.length, i > ge) {
|
|
512
485
|
n(/* @__PURE__ */ Error("Request body too large")), e.destroy();
|
|
513
486
|
return;
|
|
514
487
|
}
|
|
@@ -519,7 +492,7 @@ function ve(e) {
|
|
|
519
492
|
function X(e) {
|
|
520
493
|
return typeof e == "object" && !!e;
|
|
521
494
|
}
|
|
522
|
-
function
|
|
495
|
+
function ve(e) {
|
|
523
496
|
let t;
|
|
524
497
|
try {
|
|
525
498
|
t = JSON.parse(e);
|
|
@@ -541,27 +514,27 @@ function Z(e, t, n) {
|
|
|
541
514
|
"Content-Length": Buffer.byteLength(r)
|
|
542
515
|
}), e.end(r);
|
|
543
516
|
}
|
|
544
|
-
async function
|
|
517
|
+
async function ye(e, t) {
|
|
545
518
|
try {
|
|
546
|
-
let { source: n, format: r } =
|
|
519
|
+
let { source: n, format: r } = ve(await _e(e));
|
|
547
520
|
Z(t, 200, {
|
|
548
|
-
output: r === "svg" ?
|
|
521
|
+
output: r === "svg" ? h(n) : p(n),
|
|
549
522
|
format: r
|
|
550
523
|
});
|
|
551
524
|
} catch (e) {
|
|
552
525
|
Z(t, 400, { error: e instanceof Error ? e.message : String(e) });
|
|
553
526
|
}
|
|
554
527
|
}
|
|
555
|
-
var
|
|
556
|
-
function
|
|
528
|
+
var be = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>zombie-mermaid</title>\n<style>\n body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 900px; }\n h1 { font-size: 1.25rem; }\n textarea { width: 100%; height: 200px; font-family: monospace; font-size: 14px; box-sizing: border-box; }\n pre { white-space: pre; overflow: auto; background: #111; color: #eee; padding: 1rem; border-radius: 4px; }\n .row { display: flex; gap: 1rem; align-items: center; margin: 0.75rem 0; }\n #error { color: #c00; white-space: pre-wrap; }\n #output svg { max-width: 100%; }\n</style>\n</head>\n<body>\n<h1>zombie-mermaid</h1>\n<textarea id=\"source\" placeholder=\"graph LR\n A --> B\">graph LR\n A --> B --> C</textarea>\n<div class=\"row\">\n <label><input type=\"radio\" name=\"format\" value=\"ascii\" checked /> ASCII</label>\n <label><input type=\"radio\" name=\"format\" value=\"svg\" /> SVG</label>\n <button id=\"render\" type=\"button\">Render</button>\n</div>\n<div id=\"error\"></div>\n<div id=\"output\"></div>\n<script>\n const sourceEl = document.getElementById('source')\n const outputEl = document.getElementById('output')\n const errorEl = document.getElementById('error')\n\n async function render() {\n errorEl.textContent = ''\n outputEl.innerHTML = ''\n const formatInput = document.querySelector('input[name=\"format\"]:checked')\n const format = formatInput ? formatInput.value : 'ascii'\n try {\n const res = await fetch('/render', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ source: sourceEl.value, format }),\n })\n const data = await res.json()\n if (!res.ok) {\n errorEl.textContent = data.error || 'Render failed'\n return\n }\n if (format === 'svg') {\n outputEl.innerHTML = data.output\n } else {\n const pre = document.createElement('pre')\n pre.textContent = data.output\n outputEl.appendChild(pre)\n }\n } catch (err) {\n errorEl.textContent = String(err)\n }\n }\n\n document.getElementById('render').addEventListener('click', render)\n<\/script>\n</body>\n</html>\n";
|
|
529
|
+
function xe() {
|
|
557
530
|
return v((e, t) => {
|
|
558
531
|
let n = (e.url ?? "/").split("?")[0];
|
|
559
532
|
if (e.method === "GET" && (n === "/" || n === "")) {
|
|
560
|
-
t.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), t.end(
|
|
533
|
+
t.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), t.end(be);
|
|
561
534
|
return;
|
|
562
535
|
}
|
|
563
536
|
if (e.method === "POST" && n === "/render") {
|
|
564
|
-
|
|
537
|
+
ye(e, t);
|
|
565
538
|
return;
|
|
566
539
|
}
|
|
567
540
|
t.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }), t.end("Not found");
|
|
@@ -569,7 +542,7 @@ function Q() {
|
|
|
569
542
|
}
|
|
570
543
|
function Se(e) {
|
|
571
544
|
return new Promise((t, n) => {
|
|
572
|
-
let r =
|
|
545
|
+
let r = xe();
|
|
573
546
|
r.once("error", n), r.listen(e, "127.0.0.1", () => {
|
|
574
547
|
r.removeListener("error", n), t(r);
|
|
575
548
|
});
|
|
@@ -587,14 +560,14 @@ async function we(e = (e) => console.error(e)) {
|
|
|
587
560
|
}
|
|
588
561
|
//#endregion
|
|
589
562
|
//#region src/package-info.ts
|
|
590
|
-
function
|
|
563
|
+
function Q() {
|
|
591
564
|
let t = e(import.meta.url)("../package.json");
|
|
592
565
|
return typeof t == "object" && t && "version" in t && typeof t.version == "string" ? t.version : "unknown";
|
|
593
566
|
}
|
|
594
567
|
//#endregion
|
|
595
568
|
//#region src/cli.ts
|
|
596
|
-
|
|
597
|
-
var
|
|
569
|
+
m(x);
|
|
570
|
+
var Te = Q();
|
|
598
571
|
async function $() {
|
|
599
572
|
let e = process.argv.slice(2), t;
|
|
600
573
|
try {
|
|
@@ -604,10 +577,10 @@ async function $() {
|
|
|
604
577
|
}
|
|
605
578
|
switch (t.command) {
|
|
606
579
|
case "help":
|
|
607
|
-
|
|
580
|
+
Ee();
|
|
608
581
|
break;
|
|
609
582
|
case "version":
|
|
610
|
-
console.log(`zombie-mermaid ${
|
|
583
|
+
console.log(`zombie-mermaid ${Te}`);
|
|
611
584
|
break;
|
|
612
585
|
case "themes":
|
|
613
586
|
console.log("Available themes:\n");
|
|
@@ -615,14 +588,14 @@ async function $() {
|
|
|
615
588
|
break;
|
|
616
589
|
case "render":
|
|
617
590
|
try {
|
|
618
|
-
await
|
|
591
|
+
await de(t);
|
|
619
592
|
} catch (e) {
|
|
620
593
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`), process.exit(1);
|
|
621
594
|
}
|
|
622
595
|
break;
|
|
623
596
|
case "validate":
|
|
624
597
|
try {
|
|
625
|
-
await
|
|
598
|
+
await he(t);
|
|
626
599
|
} catch (e) {
|
|
627
600
|
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`), process.exit(1);
|
|
628
601
|
}
|
|
@@ -641,7 +614,7 @@ async function $() {
|
|
|
641
614
|
}
|
|
642
615
|
}
|
|
643
616
|
}
|
|
644
|
-
function
|
|
617
|
+
function Ee() {
|
|
645
618
|
console.log("zombie-mermaid — render Mermaid diagrams from the command line\n\nUsage:\n zombie-mermaid render <file> --ascii Render to ASCII in terminal\n zombie-mermaid render <file> --svg Render to <file stem>.svg\n zombie-mermaid render <file> --svg -o <out.svg> Render to a named SVG file\n zombie-mermaid render <file> -o out.svg Format inferred from the extension\n zombie-mermaid render <file> --svg -o - Write SVG to stdout\n zombie-mermaid render <file> --ascii --svg -o <out.svg> Both (ASCII to terminal)\n zombie-mermaid render <file> --html Self-contained pan/zoom HTML viewer\n zombie-mermaid render <file> --png Rasterize to <file stem>.png\n cat file.mmd | zombie-mermaid render --ascii Read from stdin\n zombie-mermaid validate [file] Parse only; exit 1 with line diagnostics if invalid\n zombie-mermaid themes List available themes\n zombie-mermaid web [--port <n>] Start a local web UI (default port: 3000)\n zombie-mermaid mcp Start an MCP server on stdio (render tools)\n zombie-mermaid --help Show this help\n zombie-mermaid --version Show version\n\nOptions:\n --ascii Print ASCII/Unicode diagram to terminal, or to -o <file.txt>\n when it is the only format\n --svg Render SVG to -o <path> (default: <input stem>.svg;\n stdin input must give -o)\n --resolve-colors Replace CSS var()/color-mix() in the SVG with computed\n sRGB values, for rasterizers and other non-browser SVG\n consumers (resvg, librsvg, Inkscape) that don't\n evaluate them. The default output stays a live function\n of its CSS variables. Requires --svg.\n --html Render a self-contained HTML pan/zoom viewer to -o\n <path> (default: <input stem>.html). Embeds the SVG;\n no server, no network, opens from disk. Cannot be\n combined with --svg/--png (different content, same\n slot).\n --png Rasterize to PNG at -o <path> (default: <input\n stem>.png), via the optional @resvg/resvg-js native\n dependency — install it with `pnpm add\n @resvg/resvg-js` if it's missing. Always resolves\n CSS var()/color-mix() first (as --resolve-colors\n does for --svg), since a rasterizer can't evaluate\n them. 1:1 with the SVG's own pixel size, no scaling.\n Cannot be combined with --svg/--html.\n -o, --output <path> Output file for the SVG/HTML/PNG (or for ASCII when\n none of those is given). '-' writes to stdout. A\n .svg, .html, .txt, or .png extension selects the\n format when no --svg/--html/--png/--ascii flag is\n given; one that contradicts the flags is an error.\n Existing files are never overwritten without --force.\n -f, --force Overwrite an existing output file\n --theme <name> Apply a built-in theme (see 'themes' command)\n --direction <dir> Override the diagram's layout direction: TD, TB, BT, LR, or RL\n (flowchart, state, and ER diagrams; nested subgraph\n directions still apply on top of it)\n -x, --paddingX <n> Horizontal spacing between nodes (ASCII, default: 5)\n -y, --paddingY <n> Vertical spacing between nodes (ASCII, default: 5)\n -p, --borderPadding <n> Padding inside node boxes (ASCII, default: 1)\n --coords Overlay row/column index rulers (ASCII, debug layout)\n --hyperlinks Emit OSC 8 terminal hyperlinks for `click` hrefs\n (ASCII). Off by default: terminal support varies —\n iTerm2, WezTerm, kitty, Windows Terminal, and\n VTE-based terminals render them as clickable links;\n others may show the URL or nothing. Pagers need\n OSC 8 support too (`less -R`, recent versions).\n --profile <name> ASCII preset: 'markdown' = plain ASCII characters, no\n color, for pasting into issues/commits/fenced blocks\n (explicit flags like --coords still win). Requires\n --ascii.\n -w, --max-width <n|auto> Fit ASCII output within this many columns\n ('auto' detects the terminal width): applies compact\n spacing automatically when needed, and warns on\n stderr if it still doesn't fit (no label wrapping or\n direction-flip — see issue #335).\n --layout mermaid Opt in (flowcharts, --ascii): draw mermaid's own ELK layout\n instead of the grid renderer. Subgraphs, some shapes,\n other diagram types, layout failures and drawings that\n cannot fit 120 columns (or -w <n>, used as the cap)\n fall back to the default renderer; stderr says which\n one ran. --layout mermaid-wide drops the cap and always\n draws the (possibly wide) layout-faithful text. Merged\n parallel edges hide edge multiplicity. Depends on\n mermaid 12.1.0 + elkjs 0.9.3: install the optional peer\n with `pnpm add elkjs-0.9.3@npm:elkjs@0.9.3`. -x/-y/-p\n and --coords do not apply to the layout-faithful drawing.\n --port <n> Port for the 'web' command (default: 3000)\n -h, --help Show help\n -v, --version Show version");
|
|
646
619
|
}
|
|
647
620
|
$();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zombie-mermaid",
|
|
3
|
-
"version": "5.10.
|
|
3
|
+
"version": "5.10.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"mcpName": "io.github.dfadler/zombie-mermaid",
|
|
6
6
|
"description": "Render Mermaid diagrams as beautiful SVGs or ASCII art. Ultra-fast, fully themeable, zero DOM dependencies. Maintained fork of beautiful-mermaid.",
|
|
@@ -115,11 +115,11 @@
|
|
|
115
115
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
116
116
|
"elkjs": "^0.12.0",
|
|
117
117
|
"entities": "^7.0.1",
|
|
118
|
-
"@zombie-mermaid/
|
|
119
|
-
"@zombie-mermaid/
|
|
120
|
-
"@zombie-mermaid/
|
|
121
|
-
"@zombie-mermaid/
|
|
122
|
-
"@zombie-mermaid/
|
|
118
|
+
"@zombie-mermaid/ascii-renderer": "5.10.2",
|
|
119
|
+
"@zombie-mermaid/mcp": "5.10.2",
|
|
120
|
+
"@zombie-mermaid/core": "5.10.2",
|
|
121
|
+
"@zombie-mermaid/mermaid-parser": "5.10.2",
|
|
122
|
+
"@zombie-mermaid/svg-renderer": "5.10.2"
|
|
123
123
|
},
|
|
124
124
|
"devDependencies": {
|
|
125
125
|
"@arethetypeswrong/cli": "0.18.5",
|