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 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, detectDiagramType as o, escapeXml as s, isDirection as c, isWideChar as l } from "@zombie-mermaid/core";
5
- import { access as u, readFile as d, writeFile as f } from "node:fs/promises";
6
- import { diagramColorsToAsciiTheme as p, renderMermaidASCII as m } from "@zombie-mermaid/ascii-renderer";
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 h, renderMermaidSVG as g, validateMermaid as _ } from "@zombie-mermaid/svg-renderer";
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 (!c(i)) throw Error(`${n} requires one of TD, TB, BT, LR, RL, got: "${r}"`);
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) l(r) && (t = !0), ne(r) || (n = !0);
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 N = /^Line (\d+): ([\s\S]*)$/;
279
- function P(e, t) {
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 = N.exec(r), o = t.split(/\r?\n/)[n - 1];
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 F(e, t) {
263
+ function P(e, t) {
291
264
  if (!(e instanceof Error)) return e;
292
- let n = P(e, t);
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 R(e) {
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>${s(t)}</title>
278
+ <title>${o(t)}</title>
306
279
  <style>
307
- ${L}
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
- ${I}
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 z(e) {
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 B = 80, V = 1, H = 1, U = 0, de = /\x1b\[[0-9;]*m/g;
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(de, "")))), 0);
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 ?? B : e;
323
+ return e === "auto" ? process.stdout.columns ?? z : e;
351
324
  }
352
- async function fe(e, t, n, r) {
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 d(e.input, "utf-8");
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 pe(e, o, i, a);
335
+ await fe(e, o, i, a);
363
336
  } catch (e) {
364
- throw F(e, o);
337
+ throw P(e, o);
365
338
  }
366
339
  }
367
- async function pe(e, t, n, i) {
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, f = e.html && !c ? e.output : void 0, h = e.png && !l ? 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
- f,
378
- h
379
- ]) t !== void 0 && await me(t, e.force);
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: p(o)
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 ue(t, {
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 = m(t, r);
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, V), l = Math.min(n, H), u = Math.min(i, U);
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 = m(t, {
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 ${V} -y ${H} -p ${U}) 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.";
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 ${V} -y ${H} -p ${U}) to fit.\n`);
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 = g(t, r);
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 = R({
427
- svg: g(t, o ?? {}),
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, f), i, n, e.force);
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 z(g(t, r));
439
- await q(K(l, h), i, n, e.force);
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 he(e.path, t, r);
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 me(e, t) {
432
+ async function pe(e, t) {
460
433
  if (t) return;
461
434
  let n = !0;
462
435
  try {
463
- await u(e);
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 he(e, t, n) {
442
+ async function me(e, t, n) {
470
443
  try {
471
- await f(e, t, {
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 ge(e, t) {
460
+ async function he(e, t) {
488
461
  let n;
489
- if (e.input !== void 0) n = await d(e.input, "utf-8");
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
- _(r);
471
+ g(r);
499
472
  } catch (e) {
500
- throw F(e, r);
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 _e = 1e6;
507
- function ve(e) {
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 > _e) {
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 ye(e) {
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 be(e, t) {
517
+ async function ye(e, t) {
545
518
  try {
546
- let { source: n, format: r } = ye(await ve(e));
519
+ let { source: n, format: r } = ve(await _e(e));
547
520
  Z(t, 200, {
548
- output: r === "svg" ? g(n) : m(n),
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 xe = "<!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";
556
- function Q() {
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(xe);
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
- be(e, t);
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 = Q();
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 Te() {
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
- h(x);
597
- var Ee = Te();
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
- De();
580
+ Ee();
608
581
  break;
609
582
  case "version":
610
- console.log(`zombie-mermaid ${Ee}`);
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 fe(t);
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 ge(t);
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 De() {
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.0",
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/mcp": "5.10.0",
119
- "@zombie-mermaid/core": "5.10.0",
120
- "@zombie-mermaid/mermaid-parser": "5.10.0",
121
- "@zombie-mermaid/svg-renderer": "5.10.0",
122
- "@zombie-mermaid/ascii-renderer": "5.10.0"
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",