zombie-mermaid 5.4.0 → 5.5.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +575 -1122
  2. package/package.json +6 -7
package/dist/cli.js CHANGED
@@ -1,683 +1,276 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire as q } from "node:module";
3
- import { extname as H, format as j, parse as F } from "node:path";
4
- import { MermaidParseError as K, THEMES as O, escapeXml as J, isDirection as Z, isWideChar as Q } from "@zombie-mermaid/core";
5
- import { access as ee, readFile as G, writeFile as te } from "node:fs/promises";
6
- import { diagramColorsToAsciiTheme as ne, renderMermaidASCII as _ } from "@zombie-mermaid/ascii-renderer";
2
+ import { createRequire as e } from "node:module";
3
+ import { extname as t, format as n, parse as r } from "node:path";
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 re, renderMermaidSVG as z, validateMermaid as oe } from "@zombie-mermaid/svg-renderer";
9
- import { createServer as ie } from "node:http";
10
- import { StdioServerTransport as ae } from "@modelcontextprotocol/sdk/server/stdio.js";
11
- import { createMcpServer as se } from "@zombie-mermaid/mcp";
12
- import de from "elkjs/lib/elk.bundled.js";
13
- var ce = 3e3;
14
- var W = {
15
- ".svg": "svg",
16
- ".txt": "ascii",
17
- ".html": "html",
18
- ".htm": "html",
19
- ".png": "png"
8
+ import { registerElk as m, renderMermaidSVG as h, validateMermaid as g } from "@zombie-mermaid/svg-renderer";
9
+ import { createServer as _ } from "node:http";
10
+ import { StdioServerTransport as v } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import { createMcpServer as y } from "@zombie-mermaid/mcp";
12
+ import b from "elkjs/lib/elk.bundled.js";
13
+ //#region src/cli/parse-args.ts
14
+ var x = 3e3, S = {
15
+ ".svg": "svg",
16
+ ".txt": "ascii",
17
+ ".html": "html",
18
+ ".htm": "html",
19
+ ".png": "png"
20
20
  };
21
- function le(e) {
22
- if (!(e === void 0 || e === "-"))
23
- return W[H(e).toLowerCase()];
24
- }
25
- var b = {
26
- svg: {
27
- flag: "--svg",
28
- ext: ".svg",
29
- article: "a",
30
- display: "SVG",
31
- label: "raw SVG output"
32
- },
33
- html: {
34
- flag: "--html",
35
- ext: ".html",
36
- article: "an",
37
- display: "HTML",
38
- label: "the HTML viewer"
39
- },
40
- png: {
41
- flag: "--png",
42
- ext: ".png",
43
- article: "a",
44
- display: "PNG",
45
- label: "the PNG rasterization"
46
- }
47
- }, I = Object.keys(b);
48
- function me(e, n) {
49
- const t = F(e);
50
- return j({
51
- dir: t.dir,
52
- name: t.name,
53
- ext: n
54
- });
55
- }
56
- function ue(e) {
57
- const [n, ...t] = e;
58
- if (n === void 0) return { command: "help" };
59
- if (n === "--help" || n === "-h") return { command: "help" };
60
- if (n === "--version" || n === "-v") return { command: "version" };
61
- if (n === "themes") return { command: "themes" };
62
- if (n === "render") return he(t);
63
- if (n === "validate") {
64
- const r = t.find((a, d) => d > 0 || a.startsWith("-"));
65
- if (r !== void 0) throw new Error(`Unexpected argument: ${r}`);
66
- return {
67
- command: "validate",
68
- input: t[0]
69
- };
70
- }
71
- if (n === "web") return ge(t);
72
- if (n === "mcp") return { command: "mcp" };
73
- throw new Error(`Unknown command: ${n}`);
74
- }
75
- function $(e, n, t) {
76
- const r = e[n + 1];
77
- if (n + 1 >= e.length || r === void 0) throw new Error(`${t} requires a numeric value`);
78
- if (!/^\d+$/.test(r)) throw new Error(`${t} requires a non-negative integer, got: "${r}"`);
79
- return Number(r);
80
- }
81
- function pe(e, n, t) {
82
- const r = e[n + 1];
83
- if (n + 1 >= e.length || r === void 0) throw new Error(`${t} requires a value ("auto" or a positive integer)`);
84
- if (r === "auto") return "auto";
85
- const a = Number(r);
86
- if (!/^\d+$/.test(r) || a === 0 || !Number.isSafeInteger(a)) throw new Error(`${t} requires "auto" or a positive integer, got: "${r}"`);
87
- return a;
88
- }
89
- function fe(e, n, t) {
90
- const r = e[n + 1];
91
- if (n + 1 >= e.length || r === void 0) throw new Error(`${t} requires a value (one of TD, TB, BT, LR, RL)`);
92
- const a = r.toUpperCase();
93
- if (!Z(a)) throw new Error(`${t} requires one of TD, TB, BT, LR, RL, got: "${r}"`);
94
- return a;
95
- }
96
- function he(e) {
97
- let n, t = !1, r = !1, a = !1, d = !1, l = !1, m, g = !1, E, S, k, s, u = !1, p = !1, h, v, y, i = 0;
98
- for (; i < e.length; ) {
99
- const o = e[i];
100
- if (o === void 0) throw new Error(`parseArgs: index ${i} out of range while parsing arguments`);
101
- if (o === "--ascii")
102
- t = !0, i++;
103
- else if (o === "--svg")
104
- r = !0, i++;
105
- else if (o === "--resolve-colors")
106
- a = !0, i++;
107
- else if (o === "--html")
108
- d = !0, i++;
109
- else if (o === "--png")
110
- l = !0, i++;
111
- else if (o === "-o" || o === "--output") {
112
- if (i + 1 >= e.length) throw new Error("-o requires a file path (or - for stdout)");
113
- m = e[i + 1], i += 2;
114
- } else if (o === "-f" || o === "--force")
115
- g = !0, i++;
116
- else if (o === "--theme") {
117
- if (i + 1 >= e.length) throw new Error("--theme requires a theme name");
118
- E = e[i + 1], i += 2;
119
- } else if (o === "-x" || o === "--paddingX")
120
- S = $(e, i, o), i += 2;
121
- else if (o === "-y" || o === "--paddingY")
122
- k = $(e, i, o), i += 2;
123
- else if (o === "-p" || o === "--borderPadding")
124
- s = $(e, i, o), i += 2;
125
- else if (o === "--coords")
126
- u = !0, i++;
127
- else if (o === "--hyperlinks")
128
- p = !0, i++;
129
- else if (o === "-w" || o === "--max-width")
130
- h = pe(e, i, o), i += 2;
131
- else if (o === "--profile") {
132
- const c = e[i + 1];
133
- if (c !== "markdown") throw new Error(`--profile requires one of: markdown, got: "${c ?? ""}"`);
134
- v = c, i += 2;
135
- } else if (o === "--direction")
136
- y = fe(e, i, o), i += 2;
137
- else if (!o.startsWith("-") || o === "-") {
138
- if (o === "-") throw new Error("Unexpected argument: - (use -o - to write output to stdout; omit the input file to read stdin)");
139
- if (n !== void 0) throw new Error(`Unexpected argument: ${o} (input file already set to "${n}")`);
140
- n = o, i++;
141
- } else throw new Error(`Unknown flag: ${o}`);
142
- }
143
- const w = {
144
- svg: r,
145
- html: d,
146
- png: l
147
- }, C = I.filter((o) => w[o]);
148
- if (C.length > 1) {
149
- const [o, c] = C;
150
- throw new Error(`${b[o].flag} and ${b[c].flag} cannot both be set — they would write different content to the same output path. Run the command twice with different -o paths for each.`);
151
- }
152
- const f = le(m);
153
- if (f !== void 0 && f !== "ascii") {
154
- const o = I.find((c) => c !== f && w[c]);
155
- if (o !== void 0) {
156
- const c = b[f], P = b[o];
157
- throw new Error(`-o ${m} has ${c.article} ${c.ext} extension, but ${P.flag} output would be written to it. Use a ${P.ext} path (or -o - for stdout) for ${P.label}.`);
158
- }
159
- f === "svg" ? r = !0 : f === "html" ? d = !0 : l = !0;
160
- } else if (f === "ascii") {
161
- const o = I.find((c) => w[c]);
162
- if (o !== void 0) {
163
- const c = b[o];
164
- throw new Error(`-o ${m} has a .txt extension, but ${c.flag} output would be written to it. Use a ${c.ext} path (or -o - for stdout) instead.`);
165
- }
166
- t = !0;
167
- }
168
- if (!t && !r && !d && !l) {
169
- if (m !== void 0 && m !== "-") {
170
- const o = Object.keys(W).join(", ");
171
- throw new Error(`Cannot infer an output format from "${m}" — pass --ascii, --svg, --html, or --png, or use a recognised extension (${o})`);
172
- }
173
- throw new Error("Specify --ascii, --svg, --html, and/or --png (or -o <path> with a .svg/.txt/.html/.png extension)");
174
- }
175
- const T = {
176
- svg: r,
177
- html: d,
178
- png: l
179
- }, x = I.find((o) => T[o]);
180
- if (m === "-" && t && x !== void 0) {
181
- const o = b[x].display;
182
- throw new Error(`-o - would send both ASCII and ${o} to stdout; drop --ascii, or write the ${o} to a file path`);
183
- }
184
- if (x !== void 0 && m === void 0) {
185
- const { flag: o, ext: c } = b[x];
186
- if (n === void 0) throw new Error(`${o} needs -o <path> (or -o - for stdout) when reading from stdin — there is no input file name to derive an output name from`);
187
- m = me(n, c);
188
- }
189
- if (h !== void 0 && !t) throw new Error("-w/--max-width requires --ascii");
190
- if (v !== void 0 && !t) throw new Error("--profile requires --ascii");
191
- if (p && !t) throw new Error("--hyperlinks requires --ascii");
192
- if (a && !r) throw new Error("--resolve-colors requires --svg");
193
- return {
194
- command: "render",
195
- input: n,
196
- ascii: t,
197
- svg: r,
198
- resolveColors: a,
199
- html: d,
200
- png: l,
201
- output: m,
202
- force: g,
203
- theme: E,
204
- paddingX: S,
205
- paddingY: k,
206
- borderPadding: s,
207
- coords: u,
208
- hyperlinks: p,
209
- maxWidth: h,
210
- profile: v,
211
- direction: y
212
- };
213
- }
214
- function ge(e) {
215
- let n = ce, t = 0;
216
- for (; t < e.length; ) {
217
- const r = e[t];
218
- if (r === void 0) throw new Error(`parseArgs: index ${t} out of range while parsing arguments`);
219
- if (r === "--port")
220
- n = $(e, t, r), t += 2;
221
- else throw new Error(`Unknown flag: ${r}`);
222
- }
223
- return {
224
- command: "web",
225
- port: n
226
- };
227
- }
228
- var ve = /\p{Mn}|\p{Me}/u;
229
- function we(e) {
230
- return ve.test(e);
231
- }
232
- var be = new Intl.Segmenter(void 0, { granularity: "grapheme" }), ye = "️";
233
- function xe(e) {
234
- if (/^[\x20-\x7e]*$/.test(e)) return e.split("");
235
- const n = [];
236
- for (const { segment: t } of be.segment(e)) n.push(t);
237
- return n;
238
- }
239
- function Ee(e) {
240
- if (e.includes(ye)) return 2;
241
- let n = !1, t = !1;
242
- for (const r of e)
243
- Q(r) && (n = !0), we(r) || (t = !0);
244
- return n ? 2 : t ? 1 : 0;
245
- }
246
- function Se(e) {
247
- let n = 0;
248
- for (const t of xe(e)) n += Ee(t);
249
- return n;
250
- }
251
- var ke = /\x1b\]8;[^\x07\x1b]*(?:\x1b\\|\x07)/g;
252
- function Ce(e) {
253
- return e.replace(ke, "");
254
- }
255
- var Te = /^Line (\d+): ([\s\S]*)$/;
256
- function Ie(e, n) {
257
- if (!(e instanceof K) || e.line === void 0) return;
258
- const { line: t, message: r } = e, a = Te.exec(r), d = n.split(/\r?\n/)[t - 1];
259
- if (!(t < 1 || d === void 0))
260
- return {
261
- line: t,
262
- sourceLine: d,
263
- message: a?.[2] ?? r
264
- };
265
- }
266
- function X(e, n) {
267
- if (!(e instanceof Error)) return e;
268
- const t = Ie(e, n);
269
- return t === void 0 ? e : /* @__PURE__ */ new Error(`${e.message}
270
- ${t.line} | ${t.sourceLine}`);
271
- }
272
- var Ae = `// @ts-check
273
- /// <reference lib="dom" />
274
- // ============================================================================
275
- // Client script for the CLI's self-contained HTML viewer (\`render --html\`).
276
- //
277
- // Inlined verbatim into the viewer page by src/cli/html-viewer.ts (imported
278
- // with Vite's \`?raw\`), so this file must stay plain browser JavaScript: no
279
- // imports, no TypeScript syntax, no build step. It is checked by tsc via
280
- // \`// @ts-check\` + JSDoc and linted like any other source file.
281
- //
282
- // This script ships only inside the HTML *viewer file* the CLI writes. It is
283
- // not — and must never become — part of the library's SVG output, which
284
- // stays script-free by decision (docs/decisions/no-script-interactivity.md).
285
- // ============================================================================
286
-
287
- ;(() => {
288
- const stage = document.getElementById('stage')
289
- const diagram = document.getElementById('diagram')
290
- const svg = diagram ? diagram.querySelector('svg') : null
291
- const zoomLabel = document.getElementById('zoom')
292
- const themeButton = document.getElementById('theme')
293
- if (!stage || !diagram || !svg || !zoomLabel || !themeButton) return
294
-
295
- const MIN_SCALE = 0.05
296
- const MAX_SCALE = 40
297
- const KEY_PAN_STEP = 40
298
- const KEY_PAN_STEP_FAST = 160
299
-
300
- let scale = 1
301
- let tx = 0
302
- let ty = 0
303
-
304
- // -- Geometry ---------------------------------------------------------
305
-
306
- // Assigned as \`const ... = () => {}\` rather than \`function\` declarations
307
- // from here through stageCenter (and release/setMode further below):
308
- // these all read stage/diagram/svg/zoomLabel/themeButton, which the
309
- // guard above narrowed from \`T | null\` to \`T\` — a narrowing that only
310
- // survives for a function *expression* defined after the guard, not a
311
- // hoisted \`function\` declaration (tsc can't rule out the declaration
312
- // being invoked, in theory, before the guard runs).
313
-
314
- /** Natural (unscaled) size of the diagram, from the root's width/height or viewBox. */
315
- const naturalSize = () => {
316
- const w = Number(svg.getAttribute('width'))
317
- const h = Number(svg.getAttribute('height'))
318
- if (w > 0 && h > 0) return { w, h }
319
- const vb = svg.viewBox.baseVal
320
- if (vb && vb.width > 0 && vb.height > 0)
321
- return { w: vb.width, h: vb.height }
322
- const r = diagram.getBoundingClientRect()
323
- return { w: r.width / scale || 1, h: r.height / scale || 1 }
324
- }
325
-
326
- /** @param {number} s */
327
- function clampScale(s) {
328
- return Math.min(MAX_SCALE, Math.max(MIN_SCALE, s))
329
- }
330
-
331
- const apply = () => {
332
- diagram.style.transform = \`translate(\${tx}px, \${ty}px) scale(\${scale})\`
333
- zoomLabel.textContent = \`\${Math.round(scale * 100)}%\`
334
- }
335
-
336
- /**
337
- * Zoom by \`factor\` keeping the stage point (cx, cy) fixed under the cursor.
338
- * @param {number} factor
339
- * @param {number} cx
340
- * @param {number} cy
341
- */
342
- function zoomAt(factor, cx, cy) {
343
- const next = clampScale(scale * factor)
344
- const k = next / scale
345
- tx = cx - (cx - tx) * k
346
- ty = cy - (cy - ty) * k
347
- scale = next
348
- apply()
349
- }
350
-
351
- const fit = () => {
352
- const r = stage.getBoundingClientRect()
353
- const { w, h } = naturalSize()
354
- scale = clampScale(Math.min(r.width / w, r.height / h) * 0.95)
355
- tx = (r.width - w * scale) / 2
356
- ty = (r.height - h * scale) / 2
357
- apply()
358
- }
359
-
360
- const oneToOne = () => {
361
- const r = stage.getBoundingClientRect()
362
- const { w, h } = naturalSize()
363
- scale = 1
364
- tx = Math.max(0, (r.width - w) / 2)
365
- ty = Math.max(0, (r.height - h) / 2)
366
- apply()
367
- }
368
-
369
- const stageCenter = () => {
370
- const r = stage.getBoundingClientRect()
371
- return { x: r.width / 2, y: r.height / 2 }
372
- }
373
-
374
- // -- Wheel: plain scroll pans; ctrl/cmd + wheel (which is also what a
375
- // trackpad pinch produces) zooms around the cursor. -------------------
376
-
377
- stage.addEventListener(
378
- 'wheel',
379
- (e) => {
380
- e.preventDefault()
381
- const r = stage.getBoundingClientRect()
382
- if (e.ctrlKey || e.metaKey) {
383
- zoomAt(
384
- Math.exp(-e.deltaY * 0.01),
385
- e.clientX - r.left,
386
- e.clientY - r.top,
387
- )
388
- } else {
389
- tx -= e.deltaX
390
- ty -= e.deltaY
391
- apply()
392
- }
393
- },
394
- { passive: false },
395
- )
396
-
397
- // -- Pointers: one pointer drags, two pinch-zoom around their midpoint. --
398
-
399
- /** @type {Map<number, {x: number, y: number}>} */
400
- const pointers = new Map()
401
- /** @type {{dist: number, mid: {x: number, y: number}} | null} */
402
- let lastPinch = null
403
-
404
- stage.addEventListener('pointerdown', (e) => {
405
- if (e.pointerType === 'mouse' && e.button !== 0) return
406
- stage.setPointerCapture(e.pointerId)
407
- pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
408
- stage.classList.add('dragging')
409
- })
410
-
411
- stage.addEventListener('pointermove', (e) => {
412
- const prev = pointers.get(e.pointerId)
413
- if (!prev) return
414
- const current = { x: e.clientX, y: e.clientY }
415
- pointers.set(e.pointerId, current)
416
- if (pointers.size === 1) {
417
- tx += current.x - prev.x
418
- ty += current.y - prev.y
419
- apply()
420
- return
421
- }
422
- if (pointers.size === 2) {
423
- const [a, b] = [...pointers.values()]
424
- if (!a || !b) return
425
- const dist = Math.hypot(a.x - b.x, a.y - b.y)
426
- const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }
427
- if (lastPinch && lastPinch.dist > 0) {
428
- const r = stage.getBoundingClientRect()
429
- zoomAt(dist / lastPinch.dist, mid.x - r.left, mid.y - r.top)
430
- tx += mid.x - lastPinch.mid.x
431
- ty += mid.y - lastPinch.mid.y
432
- apply()
433
- }
434
- lastPinch = { dist, mid }
435
- }
436
- })
437
-
438
- /** @param {PointerEvent} e */
439
- const release = (e) => {
440
- pointers.delete(e.pointerId)
441
- if (pointers.size < 2) lastPinch = null
442
- if (pointers.size === 0) stage.classList.remove('dragging')
443
- }
444
- stage.addEventListener('pointerup', release)
445
- stage.addEventListener('pointercancel', release)
446
-
447
- stage.addEventListener('dblclick', (e) => {
448
- const r = stage.getBoundingClientRect()
449
- zoomAt(scale < 1 ? 1 / scale : 2, e.clientX - r.left, e.clientY - r.top)
450
- })
451
-
452
- // -- Theme: follows prefers-color-scheme until the user toggles, unless
453
- // the page was generated with an explicit --theme (data-theme preset). --
454
-
455
- const root = document.documentElement
456
- const darkMedia = matchMedia('(prefers-color-scheme: dark)')
457
- let userChose = root.dataset.themeSource === 'theme'
458
-
459
- function currentMode() {
460
- const preset = root.dataset.theme
461
- if (preset === 'dark' || preset === 'light') return preset
462
- return darkMedia.matches ? 'dark' : 'light'
463
- }
464
-
465
- /** @param {'light' | 'dark'} mode */
466
- const setMode = (mode) => {
467
- root.dataset.theme = mode
468
- themeButton.textContent = mode === 'dark' ? 'Light' : 'Dark'
469
- themeButton.setAttribute('aria-pressed', String(mode === 'dark'))
470
- themeButton.setAttribute(
471
- 'aria-label',
472
- \`Switch to \${mode === 'dark' ? 'light' : 'dark'} mode (T)\`,
473
- )
474
- }
475
-
476
- function toggleMode() {
477
- userChose = true
478
- setMode(currentMode() === 'dark' ? 'light' : 'dark')
479
- }
480
-
481
- darkMedia.addEventListener('change', () => {
482
- if (!userChose) {
483
- delete root.dataset.theme
484
- setMode(currentMode())
485
- delete root.dataset.theme
486
- }
487
- })
488
- // Reflect the initial mode on the button without pinning it: leave
489
- // data-theme unset for system mode so a later OS change still applies.
490
- themeButton.textContent = currentMode() === 'dark' ? 'Light' : 'Dark'
491
- themeButton.setAttribute('aria-pressed', String(currentMode() === 'dark'))
492
-
493
- // -- Buttons -------------------------------------------------------------
494
-
495
- /**
496
- * @param {string} id
497
- * @param {() => void} handler
498
- */
499
- function onClick(id, handler) {
500
- const el = document.getElementById(id)
501
- if (el) el.addEventListener('click', handler)
502
- }
503
- onClick('zoom-in', () => {
504
- const c = stageCenter()
505
- zoomAt(1.25, c.x, c.y)
506
- })
507
- onClick('zoom-out', () => {
508
- const c = stageCenter()
509
- zoomAt(0.8, c.x, c.y)
510
- })
511
- onClick('fit', fit)
512
- onClick('one', oneToOne)
513
- onClick('theme', toggleMode)
514
-
515
- // -- Keyboard (stage must be focused; it has tabindex=0) -----------------
516
-
517
- stage.addEventListener('keydown', (e) => {
518
- const c = stageCenter()
519
- const step = e.shiftKey ? KEY_PAN_STEP_FAST : KEY_PAN_STEP
520
- switch (e.key) {
521
- case '+':
522
- case '=':
523
- zoomAt(1.25, c.x, c.y)
524
- break
525
- case '-':
526
- case '_':
527
- zoomAt(0.8, c.x, c.y)
528
- break
529
- case '0':
530
- fit()
531
- break
532
- case '1':
533
- oneToOne()
534
- break
535
- case 'ArrowLeft':
536
- tx += step
537
- apply()
538
- break
539
- case 'ArrowRight':
540
- tx -= step
541
- apply()
542
- break
543
- case 'ArrowUp':
544
- ty += step
545
- apply()
546
- break
547
- case 'ArrowDown':
548
- ty -= step
549
- apply()
550
- break
551
- case 't':
552
- case 'T':
553
- toggleMode()
554
- break
555
- default:
556
- return
557
- }
558
- e.preventDefault()
559
- })
560
-
561
- fit()
562
- stage.focus({ preventScroll: true })
563
- })()
564
- `, Me = `
565
- :root {
566
- color-scheme: light dark;
567
- --bg: #f4f4f5;
568
- --fg: #18181b;
569
- --chrome-bg: #ffffff;
570
- --chrome-border: #e4e4e7;
571
- --chrome-fg: #3f3f46;
572
- --chrome-fg-hover: #18181b;
573
- --focus-ring: #2563eb;
574
- }
575
- :root[data-theme='dark'] {
576
- --bg: #09090b;
577
- --fg: #fafafa;
578
- --chrome-bg: #18181b;
579
- --chrome-border: #3f3f46;
580
- --chrome-fg: #d4d4d8;
581
- --chrome-fg-hover: #fafafa;
582
- --focus-ring: #60a5fa;
583
- }
584
- @media (prefers-color-scheme: dark) {
585
- :root:not([data-theme='light']) {
586
- --bg: #09090b;
587
- --fg: #fafafa;
588
- --chrome-bg: #18181b;
589
- --chrome-border: #3f3f46;
590
- --chrome-fg: #d4d4d8;
591
- --chrome-fg-hover: #fafafa;
592
- --focus-ring: #60a5fa;
593
- }
594
- }
595
- * { box-sizing: border-box; }
596
- html, body {
597
- margin: 0;
598
- height: 100%;
599
- background: var(--bg);
600
- color: var(--fg);
601
- font: 14px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
602
- }
603
- #stage {
604
- position: fixed;
605
- inset: 0;
606
- overflow: hidden;
607
- cursor: grab;
608
- touch-action: none;
609
- outline: none;
610
- }
611
- #stage.dragging { cursor: grabbing; }
612
- #diagram {
613
- position: absolute;
614
- top: 0;
615
- left: 0;
616
- transform-origin: 0 0;
617
- will-change: transform;
618
- }
619
- #diagram svg { display: block; }
620
- #toolbar {
621
- position: fixed;
622
- bottom: 16px;
623
- left: 50%;
624
- transform: translateX(-50%);
625
- display: flex;
626
- align-items: center;
627
- gap: 4px;
628
- padding: 6px;
629
- border-radius: 10px;
630
- background: var(--chrome-bg);
631
- border: 1px solid var(--chrome-border);
632
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
633
- }
634
- #toolbar button {
635
- appearance: none;
636
- border: none;
637
- background: transparent;
638
- color: var(--chrome-fg);
639
- font: inherit;
640
- font-size: 13px;
641
- padding: 6px 10px;
642
- border-radius: 6px;
643
- cursor: pointer;
644
- min-width: 32px;
645
- }
646
- #toolbar button:hover { color: var(--chrome-fg-hover); background: var(--chrome-border); }
647
- #toolbar button:focus-visible {
648
- outline: 2px solid var(--focus-ring);
649
- outline-offset: 1px;
650
- }
651
- #zoom {
652
- min-width: 3.5em;
653
- text-align: center;
654
- color: var(--chrome-fg);
655
- font-variant-numeric: tabular-nums;
656
- }
657
- #stage:focus-visible {
658
- box-shadow: inset 0 0 0 2px var(--focus-ring);
659
- }
660
- @media print {
661
- #toolbar { display: none; }
662
- #stage { position: static; overflow: visible; }
663
- /* Genuinely needs !important (zombie-mermaid#1080 audit): the client
664
- * script's apply() sets #diagram's transform inline on every pan/zoom,
665
- * so whatever pan/zoom state the viewer was left in is a live inline
666
- * style this print rule must beat -- there's no static default to
667
- * hoist it to instead. */
668
- #diagram { position: static; transform: none !important; }
669
- }
670
- `.trim();
671
- function ze(e) {
672
- const n = e.title?.trim() || "Mermaid diagram";
673
- return `<!doctype html>
21
+ function C(e) {
22
+ if (e !== void 0 && e !== "-") return S[t(e).toLowerCase()];
23
+ }
24
+ var w = {
25
+ svg: {
26
+ flag: "--svg",
27
+ ext: ".svg",
28
+ article: "a",
29
+ display: "SVG",
30
+ label: "raw SVG output"
31
+ },
32
+ html: {
33
+ flag: "--html",
34
+ ext: ".html",
35
+ article: "an",
36
+ display: "HTML",
37
+ label: "the HTML viewer"
38
+ },
39
+ png: {
40
+ flag: "--png",
41
+ ext: ".png",
42
+ article: "a",
43
+ display: "PNG",
44
+ label: "the PNG rasterization"
45
+ }
46
+ }, T = Object.keys(w);
47
+ function E(e, t) {
48
+ let i = r(e);
49
+ return n({
50
+ dir: i.dir,
51
+ name: i.name,
52
+ ext: t
53
+ });
54
+ }
55
+ function D(e) {
56
+ let [t, ...n] = e;
57
+ if (t === void 0 || t === "--help" || t === "-h") return { command: "help" };
58
+ if (t === "--version" || t === "-v") return { command: "version" };
59
+ if (t === "themes") return { command: "themes" };
60
+ if (t === "render") return j(n);
61
+ if (t === "validate") {
62
+ let e = n.find((e, t) => t > 0 || e.startsWith("-"));
63
+ if (e !== void 0) throw Error(`Unexpected argument: ${e}`);
64
+ return {
65
+ command: "validate",
66
+ input: n[0]
67
+ };
68
+ }
69
+ if (t === "web") return M(n);
70
+ if (t === "mcp") return { command: "mcp" };
71
+ throw Error(`Unknown command: ${t}`);
72
+ }
73
+ function O(e, t, n) {
74
+ let r = e[t + 1];
75
+ if (t + 1 >= e.length || r === void 0) throw Error(`${n} requires a numeric value`);
76
+ if (!/^\d+$/.test(r)) throw Error(`${n} requires a non-negative integer, got: "${r}"`);
77
+ return Number(r);
78
+ }
79
+ function k(e, t, n) {
80
+ let r = e[t + 1];
81
+ if (t + 1 >= e.length || r === void 0) throw Error(`${n} requires a value ("auto" or a positive integer)`);
82
+ if (r === "auto") return "auto";
83
+ let i = Number(r);
84
+ if (!/^\d+$/.test(r) || i === 0 || !Number.isSafeInteger(i)) throw Error(`${n} requires "auto" or a positive integer, got: "${r}"`);
85
+ return i;
86
+ }
87
+ function A(e, t, n) {
88
+ let r = e[t + 1];
89
+ if (t + 1 >= e.length || r === void 0) throw Error(`${n} requires a value (one of TD, TB, BT, LR, RL)`);
90
+ let i = r.toUpperCase();
91
+ if (!s(i)) throw Error(`${n} requires one of TD, TB, BT, LR, RL, got: "${r}"`);
92
+ return i;
93
+ }
94
+ function j(e) {
95
+ let t, n = !1, r = !1, i = !1, a = !1, o = !1, s, c = !1, l, u, d, f, p = !1, m = !1, h, g, _, v = 0;
96
+ for (; v < e.length;) {
97
+ let y = e[v];
98
+ if (y === void 0) throw Error(`parseArgs: index ${v} out of range while parsing arguments`);
99
+ if (y === "--ascii") n = !0, v++;
100
+ else if (y === "--svg") r = !0, v++;
101
+ else if (y === "--resolve-colors") i = !0, v++;
102
+ else if (y === "--html") a = !0, v++;
103
+ else if (y === "--png") o = !0, v++;
104
+ else if (y === "-o" || y === "--output") {
105
+ if (v + 1 >= e.length) throw Error("-o requires a file path (or - for stdout)");
106
+ s = e[v + 1], v += 2;
107
+ } else if (y === "-f" || y === "--force") c = !0, v++;
108
+ else if (y === "--theme") {
109
+ if (v + 1 >= e.length) throw Error("--theme requires a theme name");
110
+ l = e[v + 1], v += 2;
111
+ } else if (y === "-x" || y === "--paddingX") u = O(e, v, y), v += 2;
112
+ else if (y === "-y" || y === "--paddingY") d = O(e, v, y), v += 2;
113
+ else if (y === "-p" || y === "--borderPadding") f = O(e, v, y), v += 2;
114
+ else if (y === "--coords") p = !0, v++;
115
+ else if (y === "--hyperlinks") m = !0, v++;
116
+ else if (y === "-w" || y === "--max-width") h = k(e, v, y), v += 2;
117
+ else if (y === "--profile") {
118
+ let t = e[v + 1];
119
+ if (t !== "markdown") throw Error(`--profile requires one of: markdown, got: "${t ?? ""}"`);
120
+ g = t, v += 2;
121
+ } else if (y === "--direction") _ = A(e, v, y), v += 2;
122
+ else if (!y.startsWith("-") || y === "-") {
123
+ if (y === "-") throw Error("Unexpected argument: - (use -o - to write output to stdout; omit the input file to read stdin)");
124
+ if (t !== void 0) throw Error(`Unexpected argument: ${y} (input file already set to "${t}")`);
125
+ t = y, v++;
126
+ } else throw Error(`Unknown flag: ${y}`);
127
+ }
128
+ let y = {
129
+ svg: r,
130
+ html: a,
131
+ png: o
132
+ }, b = T.filter((e) => y[e]);
133
+ if (b.length > 1) {
134
+ let [e, t] = b;
135
+ throw Error(`${w[e].flag} and ${w[t].flag} cannot both be set — they would write different content to the same output path. Run the command twice with different -o paths for each.`);
136
+ }
137
+ let x = C(s);
138
+ if (x !== void 0 && x !== "ascii") {
139
+ let e = T.find((e) => e !== x && y[e]);
140
+ if (e !== void 0) {
141
+ let t = w[x], n = w[e];
142
+ throw Error(`-o ${s} has ${t.article} ${t.ext} extension, but ${n.flag} output would be written to it. Use a ${n.ext} path (or -o - for stdout) for ${n.label}.`);
143
+ }
144
+ x === "svg" ? r = !0 : x === "html" ? a = !0 : o = !0;
145
+ } else if (x === "ascii") {
146
+ let e = T.find((e) => y[e]);
147
+ if (e !== void 0) {
148
+ let t = w[e];
149
+ throw Error(`-o ${s} has a .txt extension, but ${t.flag} output would be written to it. Use a ${t.ext} path (or -o - for stdout) instead.`);
150
+ }
151
+ n = !0;
152
+ }
153
+ if (!n && !r && !a && !o) {
154
+ if (s !== void 0 && s !== "-") {
155
+ let e = Object.keys(S).join(", ");
156
+ throw Error(`Cannot infer an output format from "${s}" — pass --ascii, --svg, --html, or --png, or use a recognised extension (${e})`);
157
+ }
158
+ throw Error("Specify --ascii, --svg, --html, and/or --png (or -o <path> with a .svg/.txt/.html/.png extension)");
159
+ }
160
+ let D = {
161
+ svg: r,
162
+ html: a,
163
+ png: o
164
+ }, j = T.find((e) => D[e]);
165
+ if (s === "-" && n && j !== void 0) {
166
+ let e = w[j].display;
167
+ throw Error(`-o - would send both ASCII and ${e} to stdout; drop --ascii, or write the ${e} to a file path`);
168
+ }
169
+ if (j !== void 0 && s === void 0) {
170
+ let { flag: e, ext: n } = w[j];
171
+ if (t === void 0) throw Error(`${e} needs -o <path> (or -o - for stdout) when reading from stdin — there is no input file name to derive an output name from`);
172
+ s = E(t, n);
173
+ }
174
+ if (h !== void 0 && !n) throw Error("-w/--max-width requires --ascii");
175
+ if (g !== void 0 && !n) throw Error("--profile requires --ascii");
176
+ if (m && !n) throw Error("--hyperlinks requires --ascii");
177
+ if (i && !r) throw Error("--resolve-colors requires --svg");
178
+ return {
179
+ command: "render",
180
+ input: t,
181
+ ascii: n,
182
+ svg: r,
183
+ resolveColors: i,
184
+ html: a,
185
+ png: o,
186
+ output: s,
187
+ force: c,
188
+ theme: l,
189
+ paddingX: u,
190
+ paddingY: d,
191
+ borderPadding: f,
192
+ coords: p,
193
+ hyperlinks: m,
194
+ maxWidth: h,
195
+ profile: g,
196
+ direction: _
197
+ };
198
+ }
199
+ function M(e) {
200
+ let t = x, n = 0;
201
+ for (; n < e.length;) {
202
+ let r = e[n];
203
+ if (r === void 0) throw Error(`parseArgs: index ${n} out of range while parsing arguments`);
204
+ if (r === "--port") t = O(e, n, r), n += 2;
205
+ else throw Error(`Unknown flag: ${r}`);
206
+ }
207
+ return {
208
+ command: "web",
209
+ port: t
210
+ };
211
+ }
212
+ //#endregion
213
+ //#region packages/ascii-renderer/src/display-width.ts
214
+ var ee = /\p{Mn}|\p{Me}/u;
215
+ function te(e) {
216
+ return ee.test(e);
217
+ }
218
+ var ne = new Intl.Segmenter(void 0, { granularity: "grapheme" }), re = "️";
219
+ function ie(e) {
220
+ if (/^[\x20-\x7e]*$/.test(e)) return e.split("");
221
+ let t = [];
222
+ for (let { segment: n } of ne.segment(e)) t.push(n);
223
+ return t;
224
+ }
225
+ function ae(e) {
226
+ if (e.includes(re)) return 2;
227
+ let t = !1, n = !1;
228
+ for (let r of e) c(r) && (t = !0), te(r) || (n = !0);
229
+ return t ? 2 : +!!n;
230
+ }
231
+ function oe(e) {
232
+ let t = 0;
233
+ for (let n of ie(e)) t += ae(n);
234
+ return t;
235
+ }
236
+ //#endregion
237
+ //#region packages/ascii-renderer/src/hyperlinks.ts
238
+ var se = /\x1b\]8;[^\x07\x1b]*(?:\x1b\\|\x07)/g;
239
+ function ce(e) {
240
+ return e.replace(se, "");
241
+ }
242
+ //#endregion
243
+ //#region packages/mcp/src/tools/parse-diagnostics.ts
244
+ var N = /^Line (\d+): ([\s\S]*)$/;
245
+ function P(e, t) {
246
+ if (!(e instanceof i) || e.line === void 0) return;
247
+ let { line: n, message: r } = e, a = N.exec(r), o = t.split(/\r?\n/)[n - 1];
248
+ if (!(n < 1 || o === void 0)) return {
249
+ line: n,
250
+ sourceLine: o,
251
+ message: a?.[2] ?? r
252
+ };
253
+ }
254
+ //#endregion
255
+ //#region src/cli/diagnostics.ts
256
+ function F(e, t) {
257
+ if (!(e instanceof Error)) return e;
258
+ let n = P(e, t);
259
+ return n === void 0 ? e : /* @__PURE__ */ Error(`${e.message}\n ${n.line} | ${n.sourceLine}`);
260
+ }
261
+ //#endregion
262
+ //#region src/cli/html-viewer-client-source.ts
263
+ 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 }";
264
+ function R(e) {
265
+ let t = e.title?.trim() || "Mermaid diagram";
266
+ return `<!doctype html>
674
267
  <html lang="en">
675
268
  <head>
676
269
  <meta charset="utf-8" />
677
270
  <meta name="viewport" content="width=device-width, initial-scale=1" />
678
- <title>${J(n)}</title>
271
+ <title>${o(t)}</title>
679
272
  <style>
680
- ${Me}
273
+ ${L}
681
274
  </style>
682
275
  </head>
683
276
  <body>
@@ -695,460 +288,320 @@ ${e.svg}
695
288
  <button id="theme" type="button" aria-pressed="false" aria-label="Switch to dark mode (T)">Dark</button>
696
289
  </div>
697
290
  <script>
698
- ${Ae}
699
- <\/script>
700
- </body>
701
- </html>
702
- `;
703
- }
704
- async function $e(e) {
705
- let n;
706
- try {
707
- n = await import("@resvg/resvg-js");
708
- } catch (t) {
709
- const r = t instanceof Error ? t.message : String(t);
710
- throw new Error(`--png requires the optional dependency "@resvg/resvg-js", which is not installed (it may have failed to build for this platform, or the install ran with --no-optional). Install it with \`pnpm add @resvg/resvg-js\` (or the npm/yarn equivalent) and try again.
711
- ${r}`);
712
- }
713
- return (await n.renderAsync(e)).asPng();
714
- }
715
- var Pe = 80, L = 1, R = 1, B = 0, Le = /\x1b\[[0-9;]*m/g;
716
- function N(e) {
717
- return e.split(`
718
- `).reduce((n, t) => Math.max(n, Se(Ce(t.replace(Le, "")))), 0);
719
- }
720
- function Re(e) {
721
- return e !== "auto" ? e : process.stdout.columns ?? Pe;
722
- }
723
- async function Be(e, n, t, r) {
724
- const a = n ?? process.stdout, d = r ?? process.stderr;
725
- let l;
726
- if (e.input !== void 0) l = await G(e.input, "utf-8");
727
- else if (t !== void 0) l = t;
728
- else {
729
- if (process.stdin.isTTY) throw new Error("No input file specified and stdin is a terminal. Pipe a diagram or pass a file path.");
730
- l = await U();
731
- }
732
- if (l.trim().length === 0) throw new Error("Empty input — provide a Mermaid diagram via file or stdin");
733
- try {
734
- await Oe(e, l, a, d);
735
- } catch (m) {
736
- throw X(m, l);
737
- }
738
- }
739
- async function Oe(e, n, t, r) {
740
- let a;
741
- if (e.theme !== void 0 && (a = O[e.theme], a === void 0)) {
742
- const s = Object.keys(O).join(", ");
743
- throw new Error(`Unknown theme: "${e.theme}". Available themes: ${s}`);
744
- }
745
- const d = e.svg && e.output === "-", l = e.html && e.output === "-", m = e.png && e.output === "-", g = e.ascii && !e.svg && !e.html && !e.png && e.output !== void 0 && e.output !== "-" ? e.output : void 0, E = e.svg && !d ? e.output : void 0, S = e.html && !l ? e.output : void 0, k = e.png && !m ? e.output : void 0;
746
- for (const s of [
747
- g,
748
- E,
749
- S,
750
- k
751
- ]) s !== void 0 && await _e(s, e.force);
752
- if (e.ascii) {
753
- const s = a && g === void 0 ? {
754
- colorMode: "auto",
755
- theme: ne(a)
756
- } : { colorMode: "none" };
757
- e.paddingX !== void 0 && (s.paddingX = e.paddingX), e.paddingY !== void 0 && (s.paddingY = e.paddingY), e.borderPadding !== void 0 && (s.boxBorderPadding = e.borderPadding), e.coords && (s.showCoords = !0), e.profile !== void 0 && (s.profile = e.profile), e.direction !== void 0 && (s.direction = e.direction), e.hyperlinks && g === void 0 && (s.hyperlinks = !0);
758
- let u = _(n, s);
759
- if (e.maxWidth !== void 0) {
760
- const p = Re(e.maxWidth);
761
- let h = N(u), v = !1;
762
- if (h > p) {
763
- const y = s.paddingX ?? 5, i = s.paddingY ?? 5, w = s.boxBorderPadding ?? 1, C = Math.min(y, L), f = Math.min(i, R), T = Math.min(w, B);
764
- if (C < y || f < i || T < w) {
765
- const x = _(n, {
766
- ...s,
767
- paddingX: C,
768
- paddingY: f,
769
- boxBorderPadding: T
770
- }), o = N(x);
771
- o < h && (u = x, h = o, v = !0);
772
- }
773
- }
774
- if (h > p) {
775
- const y = e.maxWidth === "auto" ? "detected terminal width" : "--max-width", i = v ? `Compact spacing (-x ${L} -y ${R} -p ${B}) was already applied automatically. ` : "", w = v ? "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.";
776
- r.write(`Warning: ASCII output is ${h} columns wide, exceeding ${y} of ${p}. ${i}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). ${w}
777
- `);
778
- } else v && r.write(`Note: ASCII output exceeded ${p} columns at the requested spacing; applied compact spacing automatically (-x ${L} -y ${R} -p ${B}) to fit.
779
- `);
780
- }
781
- await M(A(g === void 0, g), u + `
782
- `, t, e.force);
783
- }
784
- if (e.svg) {
785
- const s = {
786
- ...a,
787
- resolveColors: e.resolveColors
788
- };
789
- e.direction !== void 0 && (s.direction = e.direction);
790
- const u = z(n, s);
791
- await M(A(d, E), u, t, e.force);
792
- }
793
- if (e.html) {
794
- const s = z(n, a ?? {}), u = e.input ? F(e.input).name : void 0, p = ze({
795
- svg: s,
796
- title: u
797
- });
798
- await M(A(l, S), p, t, e.force);
799
- }
800
- if (e.png) {
801
- const s = {
802
- ...a,
803
- resolveColors: !0
804
- };
805
- e.direction !== void 0 && (s.direction = e.direction);
806
- const u = z(n, s), p = await $e(u);
807
- await M(A(m, k), p, t, e.force);
808
- }
809
- }
810
- function A(e, n) {
811
- return e ? { kind: "stdout" } : n !== void 0 ? {
812
- kind: "file",
813
- path: n
814
- } : { kind: "skip" };
815
- }
816
- async function M(e, n, t, r) {
817
- switch (e.kind) {
818
- case "stdout":
819
- t.write(n);
820
- break;
821
- case "file":
822
- await Ne(e.path, n, r);
823
- }
824
- }
825
- function Y(e) {
826
- return /* @__PURE__ */ new Error(`Refusing to overwrite existing file "${e}" — pass --force to replace it`);
827
- }
828
- async function _e(e, n) {
829
- if (n) return;
830
- let t = !0;
831
- try {
832
- await ee(e);
833
- } catch {
834
- t = !1;
835
- }
836
- if (t) throw Y(e);
837
- }
838
- async function Ne(e, n, t) {
839
- try {
840
- await te(e, n, {
841
- encoding: "utf-8",
842
- flag: t ? "w" : "wx"
843
- });
844
- } catch (r) {
845
- throw !t && r instanceof Error && "code" in r && r.code === "EEXIST" ? Y(e) : r;
846
- }
847
- }
848
- async function U() {
849
- const e = [], n = process.stdin;
850
- return n.setEncoding("utf-8"), new Promise((t, r) => {
851
- n.on("data", (a) => e.push(a)), n.on("end", () => t(e.join(""))), n.on("error", r);
852
- });
853
- }
854
- async function De(e, n) {
855
- let t;
856
- if (e.input !== void 0) t = await G(e.input, "utf-8");
857
- else if (n !== void 0) t = n;
858
- else {
859
- if (process.stdin.isTTY) throw new Error("No input file specified and stdin is a terminal. Pipe a diagram or pass a file path.");
860
- t = await U();
861
- }
862
- const r = t;
863
- if (r.trim().length === 0) throw new Error("Empty input — provide a Mermaid diagram via file or stdin");
864
- try {
865
- oe(r);
866
- } catch (a) {
867
- throw X(a, r);
868
- }
869
- console.log("valid");
870
- }
871
- var Fe = 1e6;
872
- function Ge(e) {
873
- return new Promise((n, t) => {
874
- const r = [];
875
- let a = 0;
876
- e.on("data", (d) => {
877
- if (a += d.length, a > Fe) {
878
- t(/* @__PURE__ */ new Error("Request body too large")), e.destroy();
879
- return;
880
- }
881
- r.push(d);
882
- }), e.on("end", () => n(Buffer.concat(r).toString("utf-8"))), e.on("error", t);
883
- });
884
- }
885
- function V(e) {
886
- return typeof e == "object" && e !== null;
887
- }
888
- function We(e) {
889
- let n;
890
- try {
891
- n = JSON.parse(e);
892
- } catch {
893
- throw new Error("Invalid JSON in request body");
894
- }
895
- if (!V(n)) throw new Error("Request body must be a JSON object");
896
- const t = n.source;
897
- if (typeof t != "string" || t.trim().length === 0) throw new Error('Provide Mermaid source as a non-empty "source" string');
898
- return {
899
- source: t,
900
- format: n.format === "svg" ? "svg" : "ascii"
901
- };
902
- }
903
- function D(e, n, t) {
904
- const r = JSON.stringify(t);
905
- e.writeHead(n, {
906
- "Content-Type": "application/json; charset=utf-8",
907
- "Content-Length": Buffer.byteLength(r)
908
- }), e.end(r);
909
- }
910
- async function Xe(e, n) {
911
- try {
912
- const { source: t, format: r } = We(await Ge(e));
913
- D(n, 200, {
914
- output: r === "svg" ? z(t) : _(t),
915
- format: r
916
- });
917
- } catch (t) {
918
- D(n, 400, { error: t instanceof Error ? t.message : String(t) });
919
- }
920
- }
921
- var Ye = `<!doctype html>
922
- <html lang="en">
923
- <head>
924
- <meta charset="utf-8" />
925
- <meta name="viewport" content="width=device-width, initial-scale=1" />
926
- <title>zombie-mermaid</title>
927
- <style>
928
- body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 900px; }
929
- h1 { font-size: 1.25rem; }
930
- textarea { width: 100%; height: 200px; font-family: monospace; font-size: 14px; box-sizing: border-box; }
931
- pre { white-space: pre; overflow: auto; background: #111; color: #eee; padding: 1rem; border-radius: 4px; }
932
- .row { display: flex; gap: 1rem; align-items: center; margin: 0.75rem 0; }
933
- #error { color: #c00; white-space: pre-wrap; }
934
- #output svg { max-width: 100%; }
935
- </style>
936
- </head>
937
- <body>
938
- <h1>zombie-mermaid</h1>
939
- <textarea id="source" placeholder="graph LR
940
- A --> B">graph LR
941
- A --> B --> C</textarea>
942
- <div class="row">
943
- <label><input type="radio" name="format" value="ascii" checked /> ASCII</label>
944
- <label><input type="radio" name="format" value="svg" /> SVG</label>
945
- <button id="render" type="button">Render</button>
946
- </div>
947
- <div id="error"></div>
948
- <div id="output"></div>
949
- <script>
950
- const sourceEl = document.getElementById('source')
951
- const outputEl = document.getElementById('output')
952
- const errorEl = document.getElementById('error')
953
-
954
- async function render() {
955
- errorEl.textContent = ''
956
- outputEl.innerHTML = ''
957
- const formatInput = document.querySelector('input[name="format"]:checked')
958
- const format = formatInput ? formatInput.value : 'ascii'
959
- try {
960
- const res = await fetch('/render', {
961
- method: 'POST',
962
- headers: { 'Content-Type': 'application/json' },
963
- body: JSON.stringify({ source: sourceEl.value, format }),
964
- })
965
- const data = await res.json()
966
- if (!res.ok) {
967
- errorEl.textContent = data.error || 'Render failed'
968
- return
969
- }
970
- if (format === 'svg') {
971
- outputEl.innerHTML = data.output
972
- } else {
973
- const pre = document.createElement('pre')
974
- pre.textContent = data.output
975
- outputEl.appendChild(pre)
976
- }
977
- } catch (err) {
978
- errorEl.textContent = String(err)
979
- }
980
- }
981
-
982
- document.getElementById('render').addEventListener('click', render)
291
+ ${I}
983
292
  <\/script>
984
293
  </body>
985
294
  </html>
986
295
  `;
987
- function Ue() {
988
- return ie((e, n) => {
989
- const t = (e.url ?? "/").split("?")[0];
990
- if (e.method === "GET" && (t === "/" || t === "")) {
991
- n.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), n.end(Ye);
992
- return;
993
- }
994
- if (e.method === "POST" && t === "/render") {
995
- Xe(e, n);
996
- return;
997
- }
998
- n.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }), n.end("Not found");
999
- });
1000
- }
1001
- function Ve(e) {
1002
- return new Promise((n, t) => {
1003
- const r = Ue();
1004
- r.once("error", t), r.listen(e, "127.0.0.1", () => {
1005
- r.removeListener("error", t), n(r);
1006
- });
1007
- });
1008
296
  }
1009
- async function qe(e, n = console.log) {
1010
- const t = await Ve(e.port), r = t.address();
1011
- return n(`zombie-mermaid web running at http://localhost:${V(r) && typeof r.port == "number" ? r.port : e.port}`), t;
297
+ //#endregion
298
+ //#region src/cli/png.ts
299
+ async function z(e) {
300
+ let t;
301
+ try {
302
+ t = await import("@resvg/resvg-js");
303
+ } catch (e) {
304
+ let t = e instanceof Error ? e.message : String(e);
305
+ throw Error(`--png requires the optional dependency "@resvg/resvg-js", which is not installed (it may have failed to build for this platform, or the install ran with --no-optional). Install it with \`pnpm add @resvg/resvg-js\` (or the npm/yarn equivalent) and try again.\n${t}`);
306
+ }
307
+ return (await t.renderAsync(e)).asPng();
308
+ }
309
+ //#endregion
310
+ //#region src/cli/render.ts
311
+ var B = 80, V = 1, H = 1, U = 0, W = /\x1b\[[0-9;]*m/g;
312
+ function G(e) {
313
+ return e.split("\n").reduce((e, t) => Math.max(e, oe(ce(t.replace(W, "")))), 0);
1012
314
  }
1013
- async function He(e = (n) => console.error(n)) {
1014
- const n = se(), t = new ae();
1015
- return await n.connect(t), e("zombie-mermaid MCP server running on stdio (experimental — feedback welcome: https://github.com/dfadler/zombie-mermaid/issues)"), n;
1016
- }
1017
- function je() {
1018
- const e = q(import.meta.url)("../package.json");
1019
- return typeof e == "object" && e !== null && "version" in e && typeof e.version == "string" ? e.version : "unknown";
1020
- }
1021
- re(de);
1022
- var Ke = je();
1023
- async function Je() {
1024
- const e = process.argv.slice(2);
1025
- let n;
1026
- try {
1027
- n = ue(e);
1028
- } catch (t) {
1029
- console.error(`Error: ${t instanceof Error ? t.message : String(t)}`), process.exit(1);
1030
- }
1031
- switch (n.command) {
1032
- case "help":
1033
- Ze();
1034
- break;
1035
- case "version":
1036
- console.log(`zombie-mermaid ${Ke}`);
1037
- break;
1038
- case "themes":
1039
- console.log(`Available themes:
1040
- `);
1041
- for (const t of Object.keys(O)) console.log(` ${t}`);
1042
- break;
1043
- case "render":
1044
- try {
1045
- await Be(n);
1046
- } catch (t) {
1047
- console.error(`Error: ${t instanceof Error ? t.message : String(t)}`), process.exit(1);
1048
- }
1049
- break;
1050
- case "validate":
1051
- try {
1052
- await De(n);
1053
- } catch (t) {
1054
- console.error(`Error: ${t instanceof Error ? t.message : String(t)}`), process.exit(1);
1055
- }
1056
- break;
1057
- case "web":
1058
- try {
1059
- await qe(n);
1060
- } catch (t) {
1061
- console.error(`Error: ${t instanceof Error ? t.message : String(t)}`), process.exit(1);
1062
- }
1063
- break;
1064
- case "mcp":
1065
- try {
1066
- await He();
1067
- } catch (t) {
1068
- console.error(`Error: ${t instanceof Error ? t.message : String(t)}`), process.exit(1);
1069
- }
1070
- }
1071
- }
1072
- function Ze() {
1073
- console.log(`
1074
- zombie-mermaid — render Mermaid diagrams from the command line
1075
-
1076
- Usage:
1077
- zombie-mermaid render <file> --ascii Render to ASCII in terminal
1078
- zombie-mermaid render <file> --svg Render to <file stem>.svg
1079
- zombie-mermaid render <file> --svg -o <out.svg> Render to a named SVG file
1080
- zombie-mermaid render <file> -o out.svg Format inferred from the extension
1081
- zombie-mermaid render <file> --svg -o - Write SVG to stdout
1082
- zombie-mermaid render <file> --ascii --svg -o <out.svg> Both (ASCII to terminal)
1083
- zombie-mermaid render <file> --html Self-contained pan/zoom HTML viewer
1084
- zombie-mermaid render <file> --png Rasterize to <file stem>.png
1085
- cat file.mmd | zombie-mermaid render --ascii Read from stdin
1086
- zombie-mermaid validate [file] Parse only; exit 1 with line diagnostics if invalid
1087
- zombie-mermaid themes List available themes
1088
- zombie-mermaid web [--port <n>] Start a local web UI (default port: 3000)
1089
- zombie-mermaid mcp Start an MCP server on stdio (render tools)
1090
- zombie-mermaid --help Show this help
1091
- zombie-mermaid --version Show version
1092
-
1093
- Options:
1094
- --ascii Print ASCII/Unicode diagram to terminal, or to -o <file.txt>
1095
- when it is the only format
1096
- --svg Render SVG to -o <path> (default: <input stem>.svg;
1097
- stdin input must give -o)
1098
- --resolve-colors Replace CSS var()/color-mix() in the SVG with computed
1099
- sRGB values, for rasterizers and other non-browser SVG
1100
- consumers (resvg, librsvg, Inkscape) that don't
1101
- evaluate them. The default output stays a live function
1102
- of its CSS variables. Requires --svg.
1103
- --html Render a self-contained HTML pan/zoom viewer to -o
1104
- <path> (default: <input stem>.html). Embeds the SVG;
1105
- no server, no network, opens from disk. Cannot be
1106
- combined with --svg/--png (different content, same
1107
- slot).
1108
- --png Rasterize to PNG at -o <path> (default: <input
1109
- stem>.png), via the optional @resvg/resvg-js native
1110
- dependency — install it with \`pnpm add
1111
- @resvg/resvg-js\` if it's missing. Always resolves
1112
- CSS var()/color-mix() first (as --resolve-colors
1113
- does for --svg), since a rasterizer can't evaluate
1114
- them. 1:1 with the SVG's own pixel size, no scaling.
1115
- Cannot be combined with --svg/--html.
1116
- -o, --output <path> Output file for the SVG/HTML/PNG (or for ASCII when
1117
- none of those is given). '-' writes to stdout. A
1118
- .svg, .html, .txt, or .png extension selects the
1119
- format when no --svg/--html/--png/--ascii flag is
1120
- given; one that contradicts the flags is an error.
1121
- Existing files are never overwritten without --force.
1122
- -f, --force Overwrite an existing output file
1123
- --theme <name> Apply a built-in theme (see 'themes' command)
1124
- --direction <dir> Override the diagram's layout direction: TD, TB, BT, LR, or RL
1125
- (flowchart, state, and ER diagrams; nested subgraph
1126
- directions still apply on top of it)
1127
- -x, --paddingX <n> Horizontal spacing between nodes (ASCII, default: 5)
1128
- -y, --paddingY <n> Vertical spacing between nodes (ASCII, default: 5)
1129
- -p, --borderPadding <n> Padding inside node boxes (ASCII, default: 1)
1130
- --coords Overlay row/column index rulers (ASCII, debug layout)
1131
- --hyperlinks Emit OSC 8 terminal hyperlinks for \`click\` hrefs
1132
- (ASCII). Off by default: terminal support varies —
1133
- iTerm2, WezTerm, kitty, Windows Terminal, and
1134
- VTE-based terminals render them as clickable links;
1135
- others may show the URL or nothing. Pagers need
1136
- OSC 8 support too (\`less -R\`, recent versions).
1137
- --profile <name> ASCII preset: 'markdown' = plain ASCII characters, no
1138
- color, for pasting into issues/commits/fenced blocks
1139
- (explicit flags like --coords still win). Requires
1140
- --ascii.
1141
- -w, --max-width <n|auto> Fit ASCII output within this many columns
1142
- ('auto' detects the terminal width): applies compact
1143
- spacing automatically when needed, and warns on
1144
- stderr if it still doesn't fit (no label wrapping or
1145
- direction-flip — see issue #335).
1146
- --port <n> Port for the 'web' command (default: 3000)
1147
- -h, --help Show help
1148
- -v, --version Show version
1149
- `.trim());
315
+ function le(e) {
316
+ return e === "auto" ? process.stdout.columns ?? B : e;
317
+ }
318
+ async function ue(e, t, n, r) {
319
+ let i = t ?? process.stdout, a = r ?? process.stderr, o;
320
+ if (e.input !== void 0) o = await u(e.input, "utf-8");
321
+ else if (n !== void 0) o = n;
322
+ else {
323
+ if (process.stdin.isTTY) throw Error("No input file specified and stdin is a terminal. Pipe a diagram or pass a file path.");
324
+ o = await Y();
325
+ }
326
+ if (o.trim().length === 0) throw Error("Empty input — provide a Mermaid diagram via file or stdin");
327
+ try {
328
+ await de(e, o, i, a);
329
+ } catch (e) {
330
+ throw F(e, o);
331
+ }
332
+ }
333
+ async function de(e, t, n, i) {
334
+ let o;
335
+ if (e.theme !== void 0 && (o = a[e.theme], o === void 0)) {
336
+ let t = Object.keys(a).join(", ");
337
+ throw Error(`Unknown theme: "${e.theme}". Available themes: ${t}`);
338
+ }
339
+ 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;
340
+ for (let t of [
341
+ u,
342
+ d,
343
+ m,
344
+ g
345
+ ]) t !== void 0 && await fe(t, e.force);
346
+ if (e.ascii) {
347
+ let r = o && u === void 0 ? {
348
+ colorMode: "auto",
349
+ theme: f(o)
350
+ } : { colorMode: "none" };
351
+ 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);
352
+ let a = p(t, r);
353
+ if (e.maxWidth !== void 0) {
354
+ let n = le(e.maxWidth), o = G(a), s = !1;
355
+ if (o > n) {
356
+ let e = r.paddingX ?? 5, n = r.paddingY ?? 5, i = r.boxBorderPadding ?? 1, c = Math.min(e, V), l = Math.min(n, H), u = Math.min(i, U);
357
+ if (c < e || l < n || u < i) {
358
+ let e = p(t, {
359
+ ...r,
360
+ paddingX: c,
361
+ paddingY: l,
362
+ boxBorderPadding: u
363
+ }), n = G(e);
364
+ n < o && (a = e, o = n, s = !0);
365
+ }
366
+ }
367
+ if (o > n) {
368
+ let t = e.maxWidth === "auto" ? "detected terminal width" : "--max-width", r = s ? `Compact spacing (-x ${V} -y ${H} -p ${U}) was already applied automatically. ` : "", a = s ? "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.";
369
+ 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`);
370
+ } else s && 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`);
371
+ }
372
+ await q(K(u === void 0, u), a + "\n", n, e.force);
373
+ }
374
+ if (e.svg) {
375
+ let r = {
376
+ ...o,
377
+ resolveColors: e.resolveColors
378
+ };
379
+ e.direction !== void 0 && (r.direction = e.direction);
380
+ let i = h(t, r);
381
+ await q(K(s, d), i, n, e.force);
382
+ }
383
+ if (e.html) {
384
+ let i = R({
385
+ svg: h(t, o ?? {}),
386
+ title: e.input ? r(e.input).name : void 0
387
+ });
388
+ await q(K(c, m), i, n, e.force);
389
+ }
390
+ if (e.png) {
391
+ let r = {
392
+ ...o,
393
+ resolveColors: !0
394
+ };
395
+ e.direction !== void 0 && (r.direction = e.direction);
396
+ let i = await z(h(t, r));
397
+ await q(K(l, g), i, n, e.force);
398
+ }
399
+ }
400
+ function K(e, t) {
401
+ return e ? { kind: "stdout" } : t === void 0 ? { kind: "skip" } : {
402
+ kind: "file",
403
+ path: t
404
+ };
405
+ }
406
+ async function q(e, t, n, r) {
407
+ switch (e.kind) {
408
+ case "stdout":
409
+ n.write(t);
410
+ break;
411
+ case "file": await pe(e.path, t, r);
412
+ }
413
+ }
414
+ function J(e) {
415
+ return /* @__PURE__ */ Error(`Refusing to overwrite existing file "${e}" — pass --force to replace it`);
416
+ }
417
+ async function fe(e, t) {
418
+ if (t) return;
419
+ let n = !0;
420
+ try {
421
+ await l(e);
422
+ } catch {
423
+ n = !1;
424
+ }
425
+ if (n) throw J(e);
426
+ }
427
+ async function pe(e, t, n) {
428
+ try {
429
+ await d(e, t, {
430
+ encoding: "utf-8",
431
+ flag: n ? "w" : "wx"
432
+ });
433
+ } catch (t) {
434
+ throw !n && t instanceof Error && "code" in t && t.code === "EEXIST" ? J(e) : t;
435
+ }
436
+ }
437
+ async function Y() {
438
+ let e = [], t = process.stdin;
439
+ return t.setEncoding("utf-8"), new Promise((n, r) => {
440
+ t.on("data", (t) => e.push(t)), t.on("end", () => n(e.join(""))), t.on("error", r);
441
+ });
442
+ }
443
+ //#endregion
444
+ //#region src/cli/validate.ts
445
+ async function me(e, t) {
446
+ let n;
447
+ if (e.input !== void 0) n = await u(e.input, "utf-8");
448
+ else if (t !== void 0) n = t;
449
+ else {
450
+ if (process.stdin.isTTY) throw Error("No input file specified and stdin is a terminal. Pipe a diagram or pass a file path.");
451
+ n = await Y();
452
+ }
453
+ let r = n;
454
+ if (r.trim().length === 0) throw Error("Empty input — provide a Mermaid diagram via file or stdin");
455
+ try {
456
+ g(r);
457
+ } catch (e) {
458
+ throw F(e, r);
459
+ }
460
+ console.log("valid");
461
+ }
462
+ //#endregion
463
+ //#region src/cli/web.ts
464
+ var he = 1e6;
465
+ function ge(e) {
466
+ return new Promise((t, n) => {
467
+ let r = [], i = 0;
468
+ e.on("data", (t) => {
469
+ if (i += t.length, i > he) {
470
+ n(/* @__PURE__ */ Error("Request body too large")), e.destroy();
471
+ return;
472
+ }
473
+ r.push(t);
474
+ }), e.on("end", () => t(Buffer.concat(r).toString("utf-8"))), e.on("error", n);
475
+ });
476
+ }
477
+ function X(e) {
478
+ return typeof e == "object" && !!e;
479
+ }
480
+ function _e(e) {
481
+ let t;
482
+ try {
483
+ t = JSON.parse(e);
484
+ } catch {
485
+ throw Error("Invalid JSON in request body");
486
+ }
487
+ if (!X(t)) throw Error("Request body must be a JSON object");
488
+ let n = t.source;
489
+ if (typeof n != "string" || n.trim().length === 0) throw Error("Provide Mermaid source as a non-empty \"source\" string");
490
+ return {
491
+ source: n,
492
+ format: t.format === "svg" ? "svg" : "ascii"
493
+ };
494
+ }
495
+ function Z(e, t, n) {
496
+ let r = JSON.stringify(n);
497
+ e.writeHead(t, {
498
+ "Content-Type": "application/json; charset=utf-8",
499
+ "Content-Length": Buffer.byteLength(r)
500
+ }), e.end(r);
501
+ }
502
+ async function ve(e, t) {
503
+ try {
504
+ let { source: n, format: r } = _e(await ge(e));
505
+ Z(t, 200, {
506
+ output: r === "svg" ? h(n) : p(n),
507
+ format: r
508
+ });
509
+ } catch (e) {
510
+ Z(t, 400, { error: e instanceof Error ? e.message : String(e) });
511
+ }
512
+ }
513
+ var ye = "<!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";
514
+ function be() {
515
+ return _((e, t) => {
516
+ let n = (e.url ?? "/").split("?")[0];
517
+ if (e.method === "GET" && (n === "/" || n === "")) {
518
+ t.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), t.end(ye);
519
+ return;
520
+ }
521
+ if (e.method === "POST" && n === "/render") {
522
+ ve(e, t);
523
+ return;
524
+ }
525
+ t.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }), t.end("Not found");
526
+ });
1150
527
  }
1151
- Je();
1152
- export {
1153
- Je as main
1154
- };
528
+ function xe(e) {
529
+ return new Promise((t, n) => {
530
+ let r = be();
531
+ r.once("error", n), r.listen(e, "127.0.0.1", () => {
532
+ r.removeListener("error", n), t(r);
533
+ });
534
+ });
535
+ }
536
+ async function Q(e, t = console.log) {
537
+ let n = await xe(e.port), r = n.address();
538
+ return t(`zombie-mermaid web running at http://localhost:${X(r) && typeof r.port == "number" ? r.port : e.port}`), n;
539
+ }
540
+ //#endregion
541
+ //#region src/cli/mcp.ts
542
+ async function Se(e = (e) => console.error(e)) {
543
+ let t = y(), n = new v();
544
+ return await t.connect(n), e("zombie-mermaid MCP server running on stdio (experimental — feedback welcome: https://github.com/dfadler/zombie-mermaid/issues)"), t;
545
+ }
546
+ //#endregion
547
+ //#region src/package-info.ts
548
+ function Ce() {
549
+ let t = e(import.meta.url)("../package.json");
550
+ return typeof t == "object" && t && "version" in t && typeof t.version == "string" ? t.version : "unknown";
551
+ }
552
+ //#endregion
553
+ //#region src/cli.ts
554
+ m(b);
555
+ var we = Ce();
556
+ async function $() {
557
+ let e = process.argv.slice(2), t;
558
+ try {
559
+ t = D(e);
560
+ } catch (e) {
561
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`), process.exit(1);
562
+ }
563
+ switch (t.command) {
564
+ case "help":
565
+ Te();
566
+ break;
567
+ case "version":
568
+ console.log(`zombie-mermaid ${we}`);
569
+ break;
570
+ case "themes":
571
+ console.log("Available themes:\n");
572
+ for (let e of Object.keys(a)) console.log(` ${e}`);
573
+ break;
574
+ case "render":
575
+ try {
576
+ await ue(t);
577
+ } catch (e) {
578
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`), process.exit(1);
579
+ }
580
+ break;
581
+ case "validate":
582
+ try {
583
+ await me(t);
584
+ } catch (e) {
585
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`), process.exit(1);
586
+ }
587
+ break;
588
+ case "web":
589
+ try {
590
+ await Q(t);
591
+ } catch (e) {
592
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`), process.exit(1);
593
+ }
594
+ break;
595
+ case "mcp": try {
596
+ await Se();
597
+ } catch (e) {
598
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`), process.exit(1);
599
+ }
600
+ }
601
+ }
602
+ function Te() {
603
+ 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 --port <n> Port for the 'web' command (default: 3000)\n -h, --help Show help\n -v, --version Show version");
604
+ }
605
+ $();
606
+ //#endregion
607
+ export { $ as main };