[Experimental] Every ```mermaid block Claude writes drawn as plain box art in place in the transcript — flowchart, sequence, class, state and xychart; CJK…

Every ``` `mermaid `` block Claude writes is drawn as plain box art where the fence was in the transcript, as one Claude Mod (a function-hooks module, hooks/register.ts`). The plugin draws locally and costs no tokens; the stored message is unchanged.
[*] start and end are left out.Needs Claude Code 2.1.287 or later, where Claude Mods are on by default, and an interactive session. Built and tested against Claude Code 2.1.289.
/mermaid-inline answers the current settings./mermaid-inline ascii on|off switches to plain ASCII art, for a font without box-drawing characters. Default off./mermaid-inline lr on|off allows or forbids the sideways layout. Default on./mermaid-inline open writes the diagrams the transcript had to cut to an HTML page in $TMPDIR and opens it in the browser, where mermaid itself draws them (from the jsDelivr CDN)./mermaid-inline reset restores the defaults.A diagram wider than the transcript is drawn again with the renderer's tightest padding, which is often a third of the width and half the height. One that still does not fit is cut at the edge, and its last line points to /mermaid-inline open.
Settings are kept in the plugin's store under the key prefs and read at session start.
Forked from claude-mermaid by Gal Elmalah at commit b19a0f0d09bf214c010b15cc2849c0c1a11d7b0d (plugin version 0.3.1).
claude-mermaid fails claude plugin validate: names starting with claude- are reserved. The command is /mermaid-inline.ui.render rewrite that carries escape sequences, so the terminal showed the raw fence. The art is now plain text, and the color setting is gone.package.json, esbuild script), the tmux end-to-end harness, and the bun test runner; the unit tests run under claude plugin test.hooks/vendor/mermaid-ascii.js is upstream's file, unchanged: the ASCII path of beautiful-mermaid 1.1.3 (tag v1.1.3, commit 65f4e0ab) bundled by upstream's scripts/build-vendor.mjs, with the A* search capped at 40,000 expansions. sha256 ee4f9dbf4af6e2b8548d0e3fe33a0d38f19f2dcec0457d8e27c3a948f700150a.
From the repo root:
claude plugin test mermaid-inline
tests/render.test.ts mounts AssistantMessage through the engine on the terminal and desktop surfaces and checks what the engine would draw; tests/diagrams.test.ts covers the pure functions.
MIT. LICENSE is claude-mermaid's (Copyright (c) 2026 Gal Elmalah); hooks/vendor/LICENSE is beautiful-mermaid's (Copyright (c) 2026 Craft Docs).
hooks/register.ts 133 lines1import type { Register } from 'claude-code'
2import {
3 fitLines,
4 htmlOf,
5 inlineTextOf,
6 leftToRightOf,
7 mermaidBlocksOf,
8 pickLayout,
9 renderOf,
10 withoutPseudoStates,
11 type Rendered,
12 widthOf,
13} from './diagrams.ts'
14
15// Every ```mermaid block Claude writes is drawn as box art where the fence
16// was, in the transcript. /mermaid-inline sets the glyph set and whether
17// top-down diagrams may be laid out sideways. The art is plain text: the
18// engine skips a rewrite that carries escape sequences.
19
20const COMMAND = 'mermaid-inline'
21const PREFS_KEY = 'prefs'
22// the transcript's code block has a gutter and margins the art must clear
23const INLINE_MARGIN = 6
24
25type Prefs = { ascii: boolean; lr: boolean }
26const DEFAULT_PREFS: Prefs = { ascii: false, lr: true }
27
28let prefs: Prefs = DEFAULT_PREFS
29const cache = new Map<string, Rendered>()
30
31const isPrefs = (value: unknown): value is Partial<Prefs> => typeof value === 'object' && value !== null
32
33// sources the transcript had to cut, oldest first: what /mermaid-inline open shows
34const cut: string[] = []
35
36const rendered = (source: string, compact: boolean): Rendered => {
37 const key = `${prefs.ascii ? 'a' : 'u'}${compact ? 'c' : ''}:${source}`
38 let out = cache.get(key)
39 if (!out) {
40 out = renderOf(source, prefs.ascii, compact)
41 cache.set(key, out)
42 }
43 return out
44}
45
46const laidOut = (source: string, columns: number, compact: boolean): Rendered => {
47 const prepared = withoutPseudoStates(source)
48 const base = rendered(prepared, compact)
49 const sideways = prefs.lr ? leftToRightOf(prepared) : null
50 return sideways ? pickLayout(base, rendered(sideways, compact), columns) : base
51}
52
53// the default padding when it fits, else the compact one, which is never wider
54const drawn = (source: string, columns: number): Rendered => {
55 const roomy = laidOut(source, columns, false)
56 if (!('lines' in roomy) || widthOf(roomy.lines) <= columns) return roomy
57 const compact = laidOut(source, columns, true)
58 return 'lines' in compact && widthOf(compact.lines) < widthOf(roomy.lines) ? compact : roomy
59}
60
61const onOff = (word: string): boolean | undefined =>
62 word === 'on' || word === 'true' ? true : word === 'off' || word === 'false' ? false : undefined
63
64const status = () => `ascii ${prefs.ascii ? 'on' : 'off'} · lr ${prefs.lr ? 'on' : 'off'}`
65
66export const register: Register = on => {
67 on('session.start', async ($, e, next) => {
68 const r = await next(e)
69 const saved = await $.store.get(PREFS_KEY).catch(() => undefined)
70 if (isPrefs(saved)) prefs = { ...DEFAULT_PREFS, ...saved }
71 await $.command
72 .register({
73 name: COMMAND,
74 description: 'Mermaid diagrams drawn in the transcript: ascii|lr on|off, open, reset (mermaid-inline)',
75 argumentHint: '[ascii|lr on|off | open | reset]',
76 immediate: true,
77 })
78 .catch(err => $.ui.log(`mermaid-inline: /${COMMAND} not registered: ${err}`))
79 return r
80 })
81
82 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
83 const blocks = mermaidBlocksOf(e.props.text)
84 if (blocks.length === 0) return next(e)
85 const columns = (e.viewport?.columns ?? 80) - INLINE_MARGIN
86 const text = inlineTextOf(e.props.text, blocks, block => {
87 const room = columns - block.indent.length
88 const art = drawn(block.source, room)
89 if (!('lines' in art)) return null
90 const fit = fitLines(art.lines, room)
91 if (fit.overflow === 0) return fit.lines
92 if (!cut.includes(block.source)) cut.push(block.source)
93 return [...fit.lines, `… ${fit.overflow} columns cut · /mermaid-inline open shows it in the browser`]
94 })
95 return next({ ...e, props: { ...e.props, text } })
96 })
97
98 on('command.run', { command: COMMAND }, async ($, e, next) => {
99 const [word = '', value = ''] = e.args.trim().toLowerCase().split(/\s+/)
100 const save = async () => {
101 await $.store.set(PREFS_KEY, prefs).catch(err => $.ui.log(`mermaid-inline: store write failed: ${err}`))
102 cache.clear()
103 $.ui.invalidate('ui.render')
104 }
105 if (word === 'open') {
106 if (cut.length === 0) return { text: 'no diagram was cut in this session' }
107 const dir = ((await $.env.get('TMPDIR')) ?? '/tmp').replace(/\/+$/, '')
108 const file = `${dir}/mermaid-inline-${Date.now()}.html`
109 await $.fs.write(file, htmlOf([...cut].reverse()))
110 const opened = await $.process.run(['open', file]).catch(() => $.process.run(['xdg-open', file]))
111 return opened.exitCode === 0
112 ? { text: `opened ${file} (${cut.length} cut diagram${cut.length === 1 ? '' : 's'}, newest first)` }
113 : { text: `wrote ${file}; open it in a browser (${opened.stderr.trim() || `exit ${opened.exitCode}`})` }
114 }
115 if (word === 'reset') {
116 prefs = DEFAULT_PREFS
117 await save()
118 return { text: status() }
119 }
120 if (word === 'ascii' || word === 'lr') {
121 const flag = onOff(value)
122 prefs = { ...prefs, [word]: flag ?? !prefs[word] }
123 await save()
124 const why = {
125 ascii: 'plain ASCII art',
126 lr: 'top-down flowcharts and state diagrams laid out left to right when nothing is lost',
127 }[word]
128 return { text: `mermaid-inline ${word} ${prefs[word] ? 'on' : 'off'} · ${why}` }
129 }
130 return { text: `${status()} · /mermaid-inline ascii|lr on|off · open · reset` }
131 })
132}
133hooks/diagrams.ts 249 lines1import { renderMermaidAscii } from './vendor/mermaid-ascii.js'
2
3// Pure functions over mermaid blocks: finding them in a message's markdown,
4// drawing them as box art, fitting the art to a width, and
5// serializing it for the transcript. No `$`, so tests call them directly.
6
7export type MermaidBlock = {
8 /** the whole fence, opening line through closing line, its indentation included */
9 start: number
10 end: number
11 /** what the fence's lines are indented by (a fence inside a list item) */
12 indent: string
13 source: string
14}
15
16export type Rendered = { lines: string[] } | { error: string }
17
18export type Fitted = { lines: string[]; width: number; overflow: number }
19
20// closing fence must sit alone on its line; an unclosed fence (mid-stream)
21// is not a block yet. The info string is `mermaid` as a whole word, so
22// `mermaidjs` is some other language
23const FENCE = /^([ \t]*)(`{3,}|~{3,})[ \t]*mermaid(?![\w-])[^\n]*\n([\s\S]*?)\n[ \t]*\2[ \t]*$/gim
24
25export const mermaidBlocksOf = (text: string): MermaidBlock[] => {
26 const blocks: MermaidBlock[] = []
27 for (const match of text.matchAll(FENCE)) {
28 const source = (match[3] ?? '').replace(/\r\n?/g, '\n').trim()
29 if (source === '') continue
30 const start = match.index ?? 0
31 blocks.push({ start, end: start + match[0].length, indent: match[1] ?? '', source })
32 }
33 return blocks
34}
35
36const KINDS: [RegExp, string][] = [
37 [/^(flowchart|graph)\b/i, 'flowchart'],
38 [/^sequenceDiagram/i, 'sequence'],
39 [/^classDiagram/i, 'class'],
40 [/^stateDiagram/i, 'state'],
41 [/^erDiagram/i, 'er'],
42 [/^xychart/i, 'chart'],
43 [/^gantt/i, 'gantt'],
44 [/^pie\b/i, 'pie'],
45 [/^mindmap/i, 'mindmap'],
46 [/^gitGraph/i, 'gitgraph'],
47 [/^journey/i, 'journey'],
48 [/^timeline/i, 'timeline'],
49 [/^quadrantChart/i, 'quadrant'],
50 [/^requirementDiagram/i, 'requirement'],
51 [/^C4/, 'c4'],
52 [/^sankey/i, 'sankey'],
53 [/^block/i, 'block'],
54 [/^packet/i, 'packet'],
55 [/^kanban/i, 'kanban'],
56 [/^architecture/i, 'architecture'],
57]
58
59// what the renderer draws; the rest keep their fence. ER is left out: the
60// renderer mangles its relationship edges (a second relation detaches)
61export const DRAWN_KINDS: ReadonlySet<string> = new Set(['flowchart', 'sequence', 'class', 'state', 'chart'])
62
63// front matter and %% comments precede the header
64const headerOf = (source: string): string => {
65 const lines = source.split('\n').map(line => line.trim())
66 let i = 0
67 if (lines[0] === '---') {
68 i = lines.indexOf('---', 1) + 1
69 if (i === 0) i = lines.length
70 }
71 return lines.slice(i).find(line => line !== '' && !line.startsWith('%%')) ?? ''
72}
73
74export const kindOf = (source: string): string => {
75 const head = headerOf(source)
76 return KINDS.find(([pattern]) => pattern.test(head))?.[1] ?? 'diagram'
77}
78
79// a source past this is not drawn: the layout is quadratic in places
80export const MAX_SOURCE_CHARS = 12_000
81
82// the renderer draws a state diagram's `[*]` start and end as an empty
83// corner-dotted box; a diagram with other transitions reads better without
84export const withoutPseudoStates = (source: string): string => {
85 if (kindOf(source) !== 'state') return source
86 const kept = source.split('\n').filter(line => !line.includes('[*]'))
87 return kept.some(line => line.includes('-->')) ? kept.join('\n') : source
88}
89
90// Rows are dear in a terminal and columns are cheap: a top-down flowchart or
91// state diagram laid out left to right is a fraction of the height. Null
92// when the source already picks a sideways direction, or the kind ignores one
93export const leftToRightOf = (source: string): string | null => {
94 const kind = kindOf(source)
95 if (kind === 'flowchart') {
96 const header = /^(\s*(?:flowchart|graph))(?:\s+(TD|TB|BT|LR|RL))?\b([^\n]*)$/im.exec(source)
97 if (!header || header[2] === 'LR' || header[2] === 'RL') return null
98 return source.replace(header[0], `${header[1]} LR${header[3]}`)
99 }
100 if (kind === 'state') {
101 if (/^\s*direction\s+/im.test(source)) return null
102 return source.replace(/^([^\n]*stateDiagram[^\n]*)$/im, '$1\n direction LR')
103 }
104 return null
105}
106
107const tokensOf = (lines: readonly string[]): Map<string, number> => {
108 const out = new Map<string, number>()
109 for (const line of lines) for (const token of line.match(/[\p{L}\p{N}_+#-]+/gu) ?? []) out.set(token, (out.get(token) ?? 0) + 1)
110 return out
111}
112
113// the sideways layout wins when every word of the original survives (the
114// renderer can overwrite the label of an edge that runs back the other way)
115// and it fits, or is at least no wider than the original
116export const pickLayout = (base: Rendered, sideways: Rendered, columns: number): Rendered => {
117 if (!('lines' in base) || !('lines' in sideways)) return base
118 const have = tokensOf(sideways.lines)
119 for (const [token, count] of tokensOf(base.lines)) if ((have.get(token) ?? 0) < count) return base
120 const width = widthOf(sideways.lines)
121 return width <= columns || width <= widthOf(base.lines) ? sideways : base
122}
123
124// East Asian Wide and Fullwidth characters take two terminal columns
125const WIDE = /[\u1100-\u115f\u2e80-\u303e\u3041-\u33ff\u3400-\u4dbf\u4e00-\u9fff\ua000-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6\u{20000}-\u{3fffd}\p{Emoji_Presentation}]/u
126
127const cellsOf = (char: string): number => (WIDE.test(char) ? 2 : 1)
128
129export const displayWidth = (text: string): number => {
130 let width = 0
131 for (const char of text) width += cellsOf(char)
132 return width
133}
134
135// The renderer sizes boxes by string length, one column per character, so a
136// wide label overflows its box. Each wide character goes in as two narrow
137// letters (a marker naming it, then a filler) and comes back out after: the
138// box is sized for two columns. Letters, not private-use characters, because
139// state names must still parse as \p{L}. Canadian syllabics are the stand-ins.
140const MARKER_BASE = 0x1401
141const MARKER_COUNT = 0x166d - MARKER_BASE
142const FILLER = '\u166f'
143const STAND_IN = /[\u1401-\u167f]/
144const ENCODED = /([\u1401-\u166c])\u166f/g
145const WIDE_ALL = new RegExp(WIDE.source, 'gu')
146
147type Encoded = { source: string; decode: (art: string) => string | null }
148
149const encodedWide = (source: string): Encoded | null => {
150 if (!WIDE.test(source)) return { source, decode: art => art }
151 if (STAND_IN.test(source)) return null
152 const wide: string[] = []
153 const encoded = source.replace(WIDE_ALL, char => {
154 let i = wide.indexOf(char)
155 if (i < 0) i = wide.push(char) - 1
156 return String.fromCharCode(MARKER_BASE + i) + FILLER
157 })
158 if (wide.length > MARKER_COUNT) return null
159 return {
160 source: encoded,
161 decode: art => {
162 const out = art.replace(ENCODED, (_, marker: string) => wide[marker.charCodeAt(0) - MARKER_BASE]!)
163 return STAND_IN.test(out) ? null : out
164 },
165 }
166}
167
168// the renderer's tightest padding: a fraction of the default width and height,
169// for a diagram that does not fit the transcript at the default
170const COMPACT = { paddingX: 1, paddingY: 0, boxBorderPadding: 0 } as const
171
172export const renderOf = (source: string, useAscii: boolean, compact = false): Rendered => {
173 const kind = kindOf(source)
174 if (!DRAWN_KINDS.has(kind)) return { error: `${kind} diagrams are not drawn yet` }
175 if (source.length > MAX_SOURCE_CHARS) return { error: `too big to draw (${source.length} characters)` }
176 const encoded = encodedWide(source)
177 if (!encoded) return { error: 'wide characters could not be measured' }
178 try {
179 const art = encoded.decode(renderMermaidAscii(encoded.source, { useAscii, colorMode: 'none', ...(compact ? COMPACT : {}) }))
180 if (art === null) return { error: 'wide characters could not be measured' }
181 const lines = art.split('\n').map(line => line.replace(/ +$/, ''))
182 while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
183 while (lines.length > 0 && lines[0] === '') lines.shift()
184 return lines.length === 0 ? { error: 'nothing to draw' } : { lines }
185 } catch (err) {
186 return { error: err instanceof Error ? err.message : String(err) }
187 }
188}
189
190export const widthOf = (lines: readonly string[]): number => lines.reduce((max, line) => Math.max(max, displayWidth(line)), 0)
191
192// lines wider than `columns` are cut with an ellipsis; `overflow` says by how much
193export const fitLines = (lines: readonly string[], columns: number): Fitted => {
194 const width = widthOf(lines)
195 const room = Math.max(1, columns)
196 if (width <= room) return { lines: [...lines], width, overflow: 0 }
197 const fitted = lines.map(line => {
198 if (displayWidth(line) <= room) return line
199 let out = ''
200 let used = 0
201 for (const char of line) {
202 if (used + cellsOf(char) > room - 1) break
203 out += char
204 used += cellsOf(char)
205 }
206 return out + '…'
207 })
208 return { lines: fitted, width, overflow: width - room }
209}
210
211// the message's markdown with each mermaid fence swapped for a text fence of its
212// art, so the transcript draws the diagram in place; a block that fails to
213// render keeps its fence. The art keeps the fence's indentation, so a diagram
214// inside a list item stays in it
215export const inlineTextOf = (
216 text: string,
217 blocks: readonly MermaidBlock[],
218 artOf: (block: MermaidBlock) => string[] | null,
219): string => {
220 let out = ''
221 let cursor = 0
222 for (const block of blocks) {
223 const art = artOf(block)
224 out += text.slice(cursor, block.start)
225 out += art
226 ? [block.indent + '```text', ...art.map(line => block.indent + line), block.indent + '```'].join('\n')
227 : text.slice(block.start, block.end)
228 cursor = block.end
229 }
230 return out + text.slice(cursor)
231}
232
233const escapeHtml = (text: string): string => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
234
235// a page that draws each source with mermaid in the browser, for a diagram the
236// transcript had to cut
237export const htmlOf = (sources: readonly string[]): string =>
238 [
239 '<!doctype html>',
240 '<html><head><meta charset="utf-8"><title>mermaid-inline</title>',
241 '<style>body{font-family:system-ui,sans-serif;margin:24px} pre.mermaid{margin:0 0 48px}</style>',
242 '</head><body>',
243 ...sources.map(source => `<pre class="mermaid">\n${escapeHtml(source)}\n</pre>`),
244 // the single-file build: the ESM one loads each diagram kind as another chunk
245 '<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>',
246 '<script>mermaid.initialize({ startOnLoad: true })</script>',
247 '</body></html>',
248 ].join('\n')
249hooks/vendor/mermaid-ascii.js 5566 lines1// Generated by scripts/build-vendor.mjs from beautiful-mermaid (MIT), A* bounded. Do not edit.
2
3// node_modules/beautiful-mermaid/src/multiline-utils.ts
4function normalizeBrTags(label) {
5 const unquoted = label.startsWith('"') && label.endsWith('"') ? label.slice(1, -1) : label;
6 return unquoted.replace(/<br\s*\/?>/gi, "\n").replace(/\\n/g, "\n").replace(/<\/?(?:sub|sup|small|mark)\s*>/gi, "").replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g, "<i>$1</i>").replace(/~~(.+?)~~/g, "<s>$1</s>");
7}
8
9// node_modules/beautiful-mermaid/src/parser.ts
10function parseMermaid(text) {
11 const lines = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("%%"));
12 if (lines.length === 0) {
13 throw new Error("Empty mermaid diagram");
14 }
15 const header = lines[0];
16 if (/^stateDiagram(-v2)?\s*$/i.test(header)) {
17 return parseStateDiagram(lines);
18 }
19 return parseFlowchart(lines);
20}
21function parseFlowchart(lines) {
22 const headerMatch = lines[0].match(/^(?:graph|flowchart)\s+(TD|TB|LR|BT|RL)\s*$/i);
23 if (!headerMatch) {
24 throw new Error(`Invalid mermaid header: "${lines[0]}". Expected "graph TD", "flowchart LR", "stateDiagram-v2", etc.`);
25 }
26 const direction = headerMatch[1].toUpperCase();
27 const graph = {
28 direction,
29 nodes: /* @__PURE__ */ new Map(),
30 edges: [],
31 subgraphs: [],
32 classDefs: /* @__PURE__ */ new Map(),
33 classAssignments: /* @__PURE__ */ new Map(),
34 nodeStyles: /* @__PURE__ */ new Map(),
35 linkStyles: /* @__PURE__ */ new Map()
36 };
37 const subgraphStack = [];
38 for (let i = 1; i < lines.length; i++) {
39 const line = lines[i];
40 const classDefMatch = line.match(/^classDef\s+(\w+)\s+(.+)$/);
41 if (classDefMatch) {
42 const name = classDefMatch[1];
43 const propsStr = classDefMatch[2];
44 const props = parseStyleProps(propsStr);
45 graph.classDefs.set(name, props);
46 continue;
47 }
48 const classAssignMatch = line.match(/^class\s+([\w,-]+)\s+(\w+)$/);
49 if (classAssignMatch) {
50 const nodeIds = classAssignMatch[1].split(",").map((s) => s.trim());
51 const className = classAssignMatch[2];
52 for (const id of nodeIds) {
53 graph.classAssignments.set(id, className);
54 }
55 continue;
56 }
57 const styleMatch = line.match(/^style\s+([\w,-]+)\s+(.+)$/);
58 if (styleMatch) {
59 const nodeIds = styleMatch[1].split(",").map((s) => s.trim());
60 const props = parseStyleProps(styleMatch[2]);
61 for (const id of nodeIds) {
62 graph.nodeStyles.set(id, { ...graph.nodeStyles.get(id), ...props });
63 }
64 continue;
65 }
66 const linkStyleMatch = line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/);
67 if (linkStyleMatch) {
68 const target = linkStyleMatch[1].trim();
69 const props = parseStyleProps(linkStyleMatch[2]);
70 if (target === "default") {
71 graph.linkStyles.set("default", { ...graph.linkStyles.get("default"), ...props });
72 } else {
73 const indices = target.split(",").map((s) => parseInt(s.trim(), 10));
74 for (const idx of indices) {
75 if (!isNaN(idx)) {
76 graph.linkStyles.set(idx, { ...graph.linkStyles.get(idx), ...props });
77 }
78 }
79 }
80 continue;
81 }
82 const dirMatch = line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i);
83 if (dirMatch && subgraphStack.length > 0) {
84 subgraphStack[subgraphStack.length - 1].direction = dirMatch[1].toUpperCase();
85 continue;
86 }
87 const subgraphMatch = line.match(/^subgraph\s+(.+)$/);
88 if (subgraphMatch) {
89 const rest = subgraphMatch[1].trim();
90 const bracketMatch = rest.match(/^([\w-]+)\s*\[(.+)\]$/);
91 let id;
92 let label;
93 if (bracketMatch) {
94 id = bracketMatch[1];
95 label = normalizeBrTags(bracketMatch[2]);
96 } else {
97 label = normalizeBrTags(rest);
98 id = rest.replace(/\s+/g, "_").replace(/[^\w]/g, "");
99 }
100 const sg = { id, label, nodeIds: [], children: [] };
101 subgraphStack.push(sg);
102 continue;
103 }
104 if (line === "end") {
105 const completed = subgraphStack.pop();
106 if (completed) {
107 if (subgraphStack.length > 0) {
108 subgraphStack[subgraphStack.length - 1].children.push(completed);
109 } else {
110 graph.subgraphs.push(completed);
111 }
112 }
113 continue;
114 }
115 parseEdgeLine(line, graph, subgraphStack);
116 }
117 return graph;
118}
119function parseStateDiagram(lines) {
120 const graph = {
121 direction: "TD",
122 nodes: /* @__PURE__ */ new Map(),
123 edges: [],
124 subgraphs: [],
125 classDefs: /* @__PURE__ */ new Map(),
126 classAssignments: /* @__PURE__ */ new Map(),
127 nodeStyles: /* @__PURE__ */ new Map(),
128 linkStyles: /* @__PURE__ */ new Map()
129 };
130 const compositeStack = [];
131 const compositeStateIds = /* @__PURE__ */ new Set();
132 let startCount = 0;
133 let endCount = 0;
134 for (let i = 1; i < lines.length; i++) {
135 const line = lines[i];
136 const dirMatch = line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i);
137 if (dirMatch) {
138 if (compositeStack.length > 0) {
139 compositeStack[compositeStack.length - 1].direction = dirMatch[1].toUpperCase();
140 } else {
141 graph.direction = dirMatch[1].toUpperCase();
142 }
143 continue;
144 }
145 const linkStyleMatch = line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/);
146 if (linkStyleMatch) {
147 const target = linkStyleMatch[1].trim();
148 const props = parseStyleProps(linkStyleMatch[2]);
149 if (target === "default") {
150 graph.linkStyles.set("default", { ...graph.linkStyles.get("default"), ...props });
151 } else {
152 const indices = target.split(",").map((s) => parseInt(s.trim(), 10));
153 for (const idx of indices) {
154 if (!isNaN(idx)) {
155 graph.linkStyles.set(idx, { ...graph.linkStyles.get(idx), ...props });
156 }
157 }
158 }
159 continue;
160 }
161 const compositeMatch = line.match(/^state\s+(?:"([^"]+)"\s+as\s+)?([\w\p{L}]+)\s*\{$/u);
162 if (compositeMatch) {
163 const label = compositeMatch[1] ?? compositeMatch[2];
164 const id = compositeMatch[2];
165 const sg = { id, label, nodeIds: [], children: [] };
166 compositeStack.push(sg);
167 compositeStateIds.add(id);
168 graph.nodes.delete(id);
169 continue;
170 }
171 if (line === "}") {
172 const completed = compositeStack.pop();
173 if (completed) {
174 if (compositeStack.length > 0) {
175 compositeStack[compositeStack.length - 1].children.push(completed);
176 } else {
177 graph.subgraphs.push(completed);
178 }
179 }
180 continue;
181 }
182 const stateAliasMatch = line.match(/^state\s+"([^"]+)"\s+as\s+([\w\p{L}]+)\s*$/u);
183 if (stateAliasMatch) {
184 const label = normalizeBrTags(stateAliasMatch[1]);
185 const id = stateAliasMatch[2];
186 registerStateNode(graph, compositeStack, { id, label, shape: "rounded" });
187 continue;
188 }
189 const transitionMatch = line.match(/^(\[\*\]|[\w\p{L}-]+)\s*(-->)\s*(\[\*\]|[\w\p{L}-]+)(?:\s*:\s*(.+))?$/u);
190 if (transitionMatch) {
191 let sourceId = transitionMatch[1];
192 let targetId = transitionMatch[3];
193 const rawTransitionLabel = transitionMatch[4]?.trim();
194 const edgeLabel = rawTransitionLabel ? normalizeBrTags(rawTransitionLabel) : void 0;
195 if (sourceId === "[*]") {
196 startCount++;
197 sourceId = `_start${startCount > 1 ? startCount : ""}`;
198 registerStateNode(graph, compositeStack, { id: sourceId, label: "", shape: "state-start" });
199 } else if (!compositeStateIds.has(sourceId)) {
200 ensureStateNode(graph, compositeStack, sourceId);
201 }
202 if (targetId === "[*]") {
203 endCount++;
204 targetId = `_end${endCount > 1 ? endCount : ""}`;
205 registerStateNode(graph, compositeStack, { id: targetId, label: "", shape: "state-end" });
206 } else if (!compositeStateIds.has(targetId)) {
207 ensureStateNode(graph, compositeStack, targetId);
208 }
209 graph.edges.push({
210 source: sourceId,
211 target: targetId,
212 label: edgeLabel,
213 style: "solid",
214 hasArrowStart: false,
215 hasArrowEnd: true
216 });
217 continue;
218 }
219 const stateDescMatch = line.match(/^([\w\p{L}-]+)\s*:\s*(.+)$/u);
220 if (stateDescMatch) {
221 const id = stateDescMatch[1];
222 const label = normalizeBrTags(stateDescMatch[2].trim());
223 registerStateNode(graph, compositeStack, { id, label, shape: "rounded" });
224 continue;
225 }
226 }
227 return graph;
228}
229function registerStateNode(graph, compositeStack, node) {
230 const isNew = !graph.nodes.has(node.id);
231 if (isNew) {
232 graph.nodes.set(node.id, node);
233 }
234 if (compositeStack.length > 0) {
235 const current = compositeStack[compositeStack.length - 1];
236 if (!current.nodeIds.includes(node.id)) {
237 current.nodeIds.push(node.id);
238 }
239 }
240}
241function ensureStateNode(graph, compositeStack, id) {
242 if (!graph.nodes.has(id)) {
243 registerStateNode(graph, compositeStack, { id, label: id, shape: "rounded" });
244 } else {
245 if (compositeStack.length > 0) {
246 const current = compositeStack[compositeStack.length - 1];
247 if (!current.nodeIds.includes(id)) {
248 current.nodeIds.push(id);
249 }
250 }
251 }
252}
253function parseStyleProps(propsStr) {
254 const cleaned = propsStr.replace(/;\s*$/, "");
255 const props = {};
256 for (const pair of cleaned.split(",")) {
257 const colonIdx = pair.indexOf(":");
258 if (colonIdx > 0) {
259 const key = pair.slice(0, colonIdx).trim();
260 const val = pair.slice(colonIdx + 1).trim();
261 if (key && val) {
262 props[key] = val;
263 }
264 }
265 }
266 return props;
267}
268var ARROW_REGEX = /^(<)?(-->|-.->|==>|---|-\.-|===)(?:\|([^|]*)\|)?/;
269var TEXT_ARROW_REGEX = /^(<)?(--|-\.|==)\s+(.+?)\s+(-->|---|\.\->|-\.\-|==>|===)/;
270var NODE_PATTERNS = [
271 // Triple delimiters (must be first)
272 { regex: /^([\w-]+)\(\(\((.+?)\)\)\)/, shape: "doublecircle" },
273 // A(((text)))
274 // Double delimiters with mixed brackets
275 { regex: /^([\w-]+)\(\[(.+?)\]\)/, shape: "stadium" },
276 // A([text])
277 { regex: /^([\w-]+)\(\((.+?)\)\)/, shape: "circle" },
278 // A((text))
279 { regex: /^([\w-]+)\[\[(.+?)\]\]/, shape: "subroutine" },
280 // A[[text]]
281 { regex: /^([\w-]+)\[\((.+?)\)\]/, shape: "cylinder" },
282 // A[(text)]
283 // Trapezoid variants — must come before plain [text]
284 { regex: /^([\w-]+)\[\/(.+?)\\\]/, shape: "trapezoid" },
285 // A[/text\]
286 { regex: /^([\w-]+)\[\\(.+?)\/\]/, shape: "trapezoid-alt" },
287 // A[\text/]
288 // Asymmetric flag shape
289 { regex: /^([\w-]+)>(.+?)\]/, shape: "asymmetric" },
290 // A>text]
291 // Double curly braces (hexagon) — must come before single {text}
292 { regex: /^([\w-]+)\{\{(.+?)\}\}/, shape: "hexagon" },
293 // A{{text}}
294 // Single-char delimiters (last — most common, least specific)
295 { regex: /^([\w-]+)\[(.+?)\]/, shape: "rectangle" },
296 // A[text]
297 { regex: /^([\w-]+)\((.+?)\)/, shape: "rounded" },
298 // A(text)
299 { regex: /^([\w-]+)\{(.+?)\}/, shape: "diamond" }
300 // A{text}
301];
302var BARE_NODE_REGEX = /^([\w-]+)/;
303var CLASS_SHORTHAND_REGEX = /^:::([\w][\w-]*)/;
304function parseEdgeLine(line, graph, subgraphStack) {
305 let remaining = line.trim();
306 const firstGroup = consumeNodeGroup(remaining, graph, subgraphStack);
307 if (!firstGroup || firstGroup.ids.length === 0) return;
308 remaining = firstGroup.remaining.trim();
309 let prevGroupIds = firstGroup.ids;
310 while (remaining.length > 0) {
311 let hasArrowStart;
312 let style;
313 let hasArrowEnd;
314 let edgeLabel;
315 const arrowMatch = remaining.match(ARROW_REGEX);
316 if (arrowMatch) {
317 hasArrowStart = Boolean(arrowMatch[1]);
318 const arrowOp = arrowMatch[2];
319 const rawEdgeLabel = arrowMatch[3]?.trim();
320 edgeLabel = rawEdgeLabel ? normalizeBrTags(rawEdgeLabel) : void 0;
321 remaining = remaining.slice(arrowMatch[0].length).trim();
322 style = arrowStyleFromOp(arrowOp);
323 hasArrowEnd = arrowOp.endsWith(">");
324 } else {
325 const textMatch = remaining.match(TEXT_ARROW_REGEX);
326 if (!textMatch) break;
327 hasArrowStart = Boolean(textMatch[1]);
328 const rawLabel = textMatch[3].trim();
329 edgeLabel = rawLabel ? normalizeBrTags(rawLabel) : void 0;
330 const openOp = textMatch[2];
331 const closeOp = textMatch[4];
332 remaining = remaining.slice(textMatch[0].length).trim();
333 style = textArrowStyleFromOps(openOp, closeOp);
334 hasArrowEnd = closeOp.endsWith(">");
335 }
336 const nextGroup = consumeNodeGroup(remaining, graph, subgraphStack);
337 if (!nextGroup || nextGroup.ids.length === 0) break;
338 remaining = nextGroup.remaining.trim();
339 for (const sourceId of prevGroupIds) {
340 for (const targetId of nextGroup.ids) {
341 graph.edges.push({
342 source: sourceId,
343 target: targetId,
344 label: edgeLabel,
345 style,
346 hasArrowStart,
347 hasArrowEnd
348 });
349 }
350 }
351 prevGroupIds = nextGroup.ids;
352 }
353}
354function consumeNodeGroup(text, graph, subgraphStack) {
355 const first = consumeNode(text, graph, subgraphStack);
356 if (!first) return null;
357 const ids = [first.id];
358 let remaining = first.remaining.trim();
359 while (remaining.startsWith("&")) {
360 remaining = remaining.slice(1).trim();
361 const next = consumeNode(remaining, graph, subgraphStack);
362 if (!next) break;
363 ids.push(next.id);
364 remaining = next.remaining.trim();
365 }
366 return { ids, remaining };
367}
368function consumeNode(text, graph, subgraphStack) {
369 let id = null;
370 let remaining = text;
371 for (const { regex, shape } of NODE_PATTERNS) {
372 const match = text.match(regex);
373 if (match) {
374 id = match[1];
375 const label = normalizeBrTags(match[2]);
376 registerNode(graph, subgraphStack, { id, label, shape });
377 remaining = text.slice(match[0].length);
378 break;
379 }
380 }
381 if (id === null) {
382 const bareMatch = text.match(BARE_NODE_REGEX);
383 if (bareMatch) {
384 id = bareMatch[1];
385 if (!graph.nodes.has(id)) {
386 registerNode(graph, subgraphStack, { id, label: id, shape: "rectangle" });
387 }
388 remaining = text.slice(bareMatch[0].length);
389 }
390 }
391 if (id === null) return null;
392 const classMatch = remaining.match(CLASS_SHORTHAND_REGEX);
393 if (classMatch) {
394 graph.classAssignments.set(id, classMatch[1]);
395 remaining = remaining.slice(classMatch[0].length);
396 }
397 return { id, remaining };
398}
399function registerNode(graph, subgraphStack, node) {
400 const isNew = !graph.nodes.has(node.id);
401 if (isNew) {
402 graph.nodes.set(node.id, node);
403 }
404 trackInSubgraph(subgraphStack, node.id);
405}
406function trackInSubgraph(subgraphStack, nodeId) {
407 if (subgraphStack.length > 0) {
408 const current = subgraphStack[subgraphStack.length - 1];
409 if (!current.nodeIds.includes(nodeId)) {
410 current.nodeIds.push(nodeId);
411 }
412 }
413}
414function arrowStyleFromOp(op) {
415 if (op === "-.->") return "dotted";
416 if (op === "-.-") return "dotted";
417 if (op === "==>") return "thick";
418 if (op === "===") return "thick";
419 return "solid";
420}
421function textArrowStyleFromOps(openOp, closeOp) {
422 if (openOp === "-." || closeOp === ".->" || closeOp === "-.-") return "dotted";
423 if (openOp === "==" || closeOp === "==>" || closeOp === "===") return "thick";
424 return "solid";
425}
426
427// node_modules/beautiful-mermaid/src/ascii/types.ts
428var Up = { x: 1, y: 0 };
429var Down = { x: 1, y: 2 };
430var Left = { x: 0, y: 1 };
431var Right = { x: 2, y: 1 };
432var UpperRight = { x: 2, y: 0 };
433var UpperLeft = { x: 0, y: 0 };
434var LowerRight = { x: 2, y: 2 };
435var LowerLeft = { x: 0, y: 2 };
436var Middle = { x: 1, y: 1 };
437function gridCoordEquals(a, b) {
438 return a.x === b.x && a.y === b.y;
439}
440function drawingCoordEquals(a, b) {
441 return a.x === b.x && a.y === b.y;
442}
443function gridCoordDirection(c, dir) {
444 return { x: c.x + dir.x, y: c.y + dir.y };
445}
446function gridKey(c) {
447 return `${c.x},${c.y}`;
448}
449var EMPTY_STYLE = { name: "", styles: {} };
450
451// node_modules/beautiful-mermaid/src/ascii/ansi.ts
452var DEFAULT_ASCII_THEME = {
453 fg: "#27272a",
454 // zinc-800 — primary text
455 border: "#a1a1aa",
456 // zinc-400 — node borders (12% mix)
457 line: "#71717a",
458 // zinc-500 — edge lines (35% mix)
459 arrow: "#52525b",
460 // zinc-600 — arrowheads (60% mix)
461 corner: "#71717a",
462 // same as line
463 junction: "#a1a1aa"
464 // same as border
465};
466function detectColorMode() {
467 const proc = globalThis.process;
468 if (proc) {
469 if (!proc.stdout?.isTTY) {
470 return "none";
471 }
472 const colorTerm = proc.env?.COLORTERM?.toLowerCase() ?? "";
473 const term = proc.env?.TERM?.toLowerCase() ?? "";
474 if (colorTerm === "truecolor" || colorTerm === "24bit") {
475 return "truecolor";
476 }
477 if (term.includes("256color") || term.includes("256")) {
478 return "ansi256";
479 }
480 if (term && term !== "dumb") {
481 return "ansi16";
482 }
483 return "none";
484 }
485 if (typeof document !== "undefined") {
486 return "html";
487 }
488 return "none";
489}
490function parseHex(hex) {
491 const h = hex.replace("#", "");
492 if (h.length === 3) {
493 return {
494 r: parseInt(h[0] + h[0], 16),
495 g: parseInt(h[1] + h[1], 16),
496 b: parseInt(h[2] + h[2], 16)
497 };
498 }
499 return {
500 r: parseInt(h.substring(0, 2), 16),
501 g: parseInt(h.substring(2, 4), 16),
502 b: parseInt(h.substring(4, 6), 16)
503 };
504}
505var ESC = "\x1B[";
506var RESET = `${ESC}0m`;
507function truecolorFg(hex) {
508 const { r, g, b } = parseHex(hex);
509 return `${ESC}38;2;${r};${g};${b}m`;
510}
511function rgbTo256(r, g, b) {
512 const avg = (r + g + b) / 3;
513 const maxDiff = Math.max(Math.abs(r - avg), Math.abs(g - avg), Math.abs(b - avg));
514 if (maxDiff < 10) {
515 const gray = Math.round(avg / 255 * 23);
516 return 232 + Math.min(23, Math.max(0, gray));
517 }
518 const toIndex = (v) => {
519 if (v < 48) return 0;
520 if (v < 115) return 1;
521 return Math.min(5, Math.floor((v - 35) / 40));
522 };
523 const ri = toIndex(r);
524 const gi = toIndex(g);
525 const bi = toIndex(b);
526 return 16 + 36 * ri + 6 * gi + bi;
527}
528function ansi256Fg(hex) {
529 const { r, g, b } = parseHex(hex);
530 const index = rgbTo256(r, g, b);
531 return `${ESC}38;5;${index}m`;
532}
533function ansi16Fg(hex) {
534 const { r, g, b } = parseHex(hex);
535 const luma = 0.299 * r + 0.587 * g + 0.114 * b;
536 const bright = luma > 100 ? 0 : 60;
537 let code;
538 if (r > 180 && g < 100 && b < 100) code = 31;
539 else if (g > 180 && r < 100 && b < 100) code = 32;
540 else if (r > 150 && g > 150 && b < 100) code = 33;
541 else if (b > 180 && r < 100 && g < 100) code = 34;
542 else if (r > 150 && b > 150 && g < 100) code = 35;
543 else if (g > 150 && b > 150 && r < 100) code = 36;
544 else if (luma > 200) code = 37;
545 else if (luma < 50) code = 30;
546 else code = 37;
547 return `${ESC}${code + bright}m`;
548}
549function escapeHtml(text) {
550 return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
551}
552function htmlSpan(hex, text) {
553 return `<span style="color:${hex}">${escapeHtml(text)}</span>`;
554}
555function getRoleColor(role, theme) {
556 switch (role) {
557 case "text":
558 return theme.fg;
559 case "border":
560 return theme.border;
561 case "line":
562 return theme.line;
563 case "arrow":
564 return theme.arrow;
565 case "corner":
566 return theme.corner ?? theme.line;
567 case "junction":
568 return theme.junction ?? theme.border;
569 default:
570 return theme.fg;
571 }
572}
573function getAnsiColor(role, theme, mode) {
574 if (mode === "none") return "";
575 const hex = getRoleColor(role, theme);
576 switch (mode) {
577 case "truecolor":
578 return truecolorFg(hex);
579 case "ansi256":
580 return ansi256Fg(hex);
581 case "ansi16":
582 return ansi16Fg(hex);
583 default:
584 return "";
585 }
586}
587function colorizeLine(chars, roles, theme, mode) {
588 if (mode === "none") {
589 return chars.join("");
590 }
591 if (mode === "html") {
592 return colorizeLineHtml(chars, roles, theme);
593 }
594 let result = "";
595 let currentRole = null;
596 let buffer = "";
597 for (let i = 0; i < chars.length; i++) {
598 const char = chars[i];
599 const role = roles[i] ?? null;
600 if (char === " ") {
601 if (buffer.length > 0) {
602 if (currentRole !== null) {
603 result += getAnsiColor(currentRole, theme, mode) + buffer + RESET;
604 } else {
605 result += buffer;
606 }
607 buffer = "";
608 currentRole = null;
609 }
610 result += char;
611 continue;
612 }
613 if (role === currentRole) {
614 buffer += char;
615 continue;
616 }
617 if (buffer.length > 0) {
618 if (currentRole !== null) {
619 result += getAnsiColor(currentRole, theme, mode) + buffer + RESET;
620 } else {
621 result += buffer;
622 }
623 }
624 buffer = char;
625 currentRole = role;
626 }
627 if (buffer.length > 0 && currentRole !== null) {
628 result += getAnsiColor(currentRole, theme, mode) + buffer + RESET;
629 } else if (buffer.length > 0) {
630 result += buffer;
631 }
632 return result;
633}
634function colorizeLineHtml(chars, roles, theme) {
635 let result = "";
636 let currentRole = null;
637 let buffer = "";
638 const flush = () => {
639 if (buffer.length === 0) return;
640 if (currentRole !== null) {
641 result += htmlSpan(getRoleColor(currentRole, theme), buffer);
642 } else {
643 result += escapeHtml(buffer);
644 }
645 buffer = "";
646 currentRole = null;
647 };
648 for (let i = 0; i < chars.length; i++) {
649 const char = chars[i];
650 const role = roles[i] ?? null;
651 if (char === " ") {
652 flush();
653 result += " ";
654 continue;
655 }
656 if (role === currentRole) {
657 buffer += char;
658 continue;
659 }
660 flush();
661 buffer = char;
662 currentRole = role;
663 }
664 flush();
665 return result;
666}
667function colorizeText(text, hex, mode) {
668 if (mode === "none" || text.length === 0) return text;
669 if (mode === "html") return htmlSpan(hex, text);
670 let code;
671 switch (mode) {
672 case "truecolor":
673 code = truecolorFg(hex);
674 break;
675 case "ansi256":
676 code = ansi256Fg(hex);
677 break;
678 case "ansi16":
679 code = ansi16Fg(hex);
680 break;
681 default:
682 return text;
683 }
684 return `${code}${text}${RESET}`;
685}
686
687// node_modules/beautiful-mermaid/src/ascii/canvas.ts
688function mkCanvas(x, y) {
689 const canvas = [];
690 for (let i = 0; i <= x; i++) {
691 const col = [];
692 for (let j = 0; j <= y; j++) {
693 col.push(" ");
694 }
695 canvas.push(col);
696 }
697 return canvas;
698}
699function copyCanvas(source) {
700 const [maxX, maxY] = getCanvasSize(source);
701 return mkCanvas(maxX, maxY);
702}
703function mkRoleCanvas(x, y) {
704 const roleCanvas = [];
705 for (let i = 0; i <= x; i++) {
706 const col = [];
707 for (let j = 0; j <= y; j++) {
708 col.push(null);
709 }
710 roleCanvas.push(col);
711 }
712 return roleCanvas;
713}
714function increaseRoleCanvasSize(roleCanvas, newX, newY) {
715 const currX = roleCanvas.length - 1;
716 const currY = (roleCanvas[0]?.length ?? 1) - 1;
717 const targetX = Math.max(newX, currX);
718 const targetY = Math.max(newY, currY);
719 const grown = mkRoleCanvas(targetX, targetY);
720 for (let x = 0; x < grown.length; x++) {
721 for (let y = 0; y < grown[0].length; y++) {
722 if (x < roleCanvas.length && y < roleCanvas[0].length) {
723 grown[x][y] = roleCanvas[x][y];
724 }
725 }
726 }
727 roleCanvas.length = 0;
728 roleCanvas.push(...grown);
729 return roleCanvas;
730}
731function setRole(roleCanvas, x, y, role) {
732 if (x >= roleCanvas.length || y >= (roleCanvas[0]?.length ?? 0)) {
733 increaseRoleCanvasSize(roleCanvas, x, y);
734 }
735 roleCanvas[x][y] = role;
736}
737function getCanvasSize(canvas) {
738 return [canvas.length - 1, (canvas[0]?.length ?? 1) - 1];
739}
740function increaseSize(canvas, newX, newY) {
741 const [currX, currY] = getCanvasSize(canvas);
742 const targetX = Math.max(newX, currX);
743 const targetY = Math.max(newY, currY);
744 const grown = mkCanvas(targetX, targetY);
745 for (let x = 0; x < grown.length; x++) {
746 for (let y = 0; y < grown[0].length; y++) {
747 if (x < canvas.length && y < canvas[0].length) {
748 grown[x][y] = canvas[x][y];
749 }
750 }
751 }
752 canvas.length = 0;
753 canvas.push(...grown);
754 return canvas;
755}
756var JUNCTION_CHARS = /* @__PURE__ */ new Set([
757 "\u2500",
758 "\u2502",
759 "\u250C",
760 "\u2510",
761 "\u2514",
762 "\u2518",
763 "\u251C",
764 "\u2524",
765 "\u252C",
766 "\u2534",
767 "\u253C",
768 "\u2574",
769 "\u2575",
770 "\u2576",
771 "\u2577"
772]);
773function isJunctionChar(c) {
774 return JUNCTION_CHARS.has(c);
775}
776function isAlphanumeric(c) {
777 return /^[a-zA-Z0-9]$/.test(c);
778}
779var JUNCTION_MAP = {
780 "\u2500": { "\u2502": "\u253C", "\u250C": "\u252C", "\u2510": "\u252C", "\u2514": "\u2534", "\u2518": "\u2534", "\u251C": "\u253C", "\u2524": "\u253C", "\u252C": "\u252C", "\u2534": "\u2534" },
781 "\u2502": { "\u2500": "\u253C", "\u250C": "\u251C", "\u2510": "\u2524", "\u2514": "\u251C", "\u2518": "\u2524", "\u251C": "\u251C", "\u2524": "\u2524", "\u252C": "\u253C", "\u2534": "\u253C" },
782 "\u250C": { "\u2500": "\u252C", "\u2502": "\u251C", "\u2510": "\u252C", "\u2514": "\u251C", "\u2518": "\u253C", "\u251C": "\u251C", "\u2524": "\u253C", "\u252C": "\u252C", "\u2534": "\u253C" },
783 "\u2510": { "\u2500": "\u252C", "\u2502": "\u2524", "\u250C": "\u252C", "\u2514": "\u253C", "\u2518": "\u2524", "\u251C": "\u253C", "\u2524": "\u2524", "\u252C": "\u252C", "\u2534": "\u253C" },
784 "\u2514": { "\u2500": "\u2534", "\u2502": "\u251C", "\u250C": "\u251C", "\u2510": "\u253C", "\u2518": "\u2534", "\u251C": "\u251C", "\u2524": "\u253C", "\u252C": "\u253C", "\u2534": "\u2534" },
785 "\u2518": { "\u2500": "\u2534", "\u2502": "\u2524", "\u250C": "\u253C", "\u2510": "\u2524", "\u2514": "\u2534", "\u251C": "\u253C", "\u2524": "\u2524", "\u252C": "\u253C", "\u2534": "\u2534" },
786 "\u251C": { "\u2500": "\u253C", "\u2502": "\u251C", "\u250C": "\u251C", "\u2510": "\u253C", "\u2514": "\u251C", "\u2518": "\u253C", "\u2524": "\u253C", "\u252C": "\u253C", "\u2534": "\u253C" },
787 "\u2524": { "\u2500": "\u253C", "\u2502": "\u2524", "\u250C": "\u253C", "\u2510": "\u2524", "\u2514": "\u253C", "\u2518": "\u2524", "\u251C": "\u253C", "\u252C": "\u253C", "\u2534": "\u253C" },
788 "\u252C": { "\u2500": "\u252C", "\u2502": "\u253C", "\u250C": "\u252C", "\u2510": "\u252C", "\u2514": "\u253C", "\u2518": "\u253C", "\u251C": "\u253C", "\u2524": "\u253C", "\u2534": "\u253C" },
789 "\u2534": { "\u2500": "\u2534", "\u2502": "\u253C", "\u250C": "\u253C", "\u2510": "\u253C", "\u2514": "\u2534", "\u2518": "\u2534", "\u251C": "\u253C", "\u2524": "\u253C", "\u252C": "\u253C" }
790};
791function mergeJunctions(c1, c2) {
792 return JUNCTION_MAP[c1]?.[c2] ?? c1;
793}
794function mergeCanvases(base, offset, useAscii, ...overlays) {
795 let [maxX, maxY] = getCanvasSize(base);
796 for (const overlay of overlays) {
797 const [oX, oY] = getCanvasSize(overlay);
798 maxX = Math.max(maxX, oX + offset.x);
799 maxY = Math.max(maxY, oY + offset.y);
800 }
801 const merged = mkCanvas(maxX, maxY);
802 for (let x = 0; x <= maxX; x++) {
803 for (let y = 0; y <= maxY; y++) {
804 if (x < base.length && y < base[0].length) {
805 merged[x][y] = base[x][y];
806 }
807 }
808 }
809 for (const overlay of overlays) {
810 for (let x = 0; x < overlay.length; x++) {
811 for (let y = 0; y < overlay[0].length; y++) {
812 const c = overlay[x][y];
813 if (c !== " ") {
814 const mx = x + offset.x;
815 const my = y + offset.y;
816 const current = merged[mx][my];
817 if (!useAscii && isJunctionChar(c) && isJunctionChar(current)) {
818 merged[mx][my] = mergeJunctions(current, c);
819 } else if (isAlphanumeric(current) && isAlphanumeric(c)) {
820 } else {
821 merged[mx][my] = c;
822 }
823 }
824 }
825 }
826 }
827 return merged;
828}
829function canvasToString(canvas, options) {
830 const [maxX, maxY] = getCanvasSize(canvas);
831 const lines = [];
832 const roleCanvas = options?.roleCanvas;
833 const colorMode = options?.colorMode ?? "none";
834 const theme = options?.theme ?? DEFAULT_ASCII_THEME;
835 for (let y = 0; y <= maxY; y++) {
836 if (colorMode === "none" || !roleCanvas) {
837 let line = "";
838 for (let x = 0; x <= maxX; x++) {
839 line += canvas[x][y];
840 }
841 lines.push(line);
842 } else {
843 const chars = [];
844 const roles = [];
845 for (let x = 0; x <= maxX; x++) {
846 chars.push(canvas[x][y]);
847 roles.push(roleCanvas[x]?.[y] ?? null);
848 }
849 lines.push(colorizeLine(chars, roles, theme, colorMode));
850 }
851 }
852 return lines.join("\n");
853}
854var VERTICAL_FLIP_MAP = {
855 // Unicode arrows
856 "\u25B2": "\u25BC",
857 "\u25BC": "\u25B2",
858 "\u25E4": "\u25E3",
859 "\u25E3": "\u25E4",
860 "\u25E5": "\u25E2",
861 "\u25E2": "\u25E5",
862 // ASCII arrows
863 "^": "v",
864 "v": "^",
865 // Unicode corners
866 "\u250C": "\u2514",
867 "\u2514": "\u250C",
868 "\u2510": "\u2518",
869 "\u2518": "\u2510",
870 // Unicode junctions (T-pieces flip vertically)
871 "\u252C": "\u2534",
872 "\u2534": "\u252C",
873 // Box-start junctions (exit points from node boxes)
874 "\u2575": "\u2577",
875 "\u2577": "\u2575"
876};
877function flipCanvasVertically(canvas) {
878 for (const col of canvas) {
879 col.reverse();
880 }
881 for (const col of canvas) {
882 for (let y = 0; y < col.length; y++) {
883 const flipped = VERTICAL_FLIP_MAP[col[y]];
884 if (flipped) col[y] = flipped;
885 }
886 }
887 return canvas;
888}
889function flipRoleCanvasVertically(roleCanvas) {
890 for (const col of roleCanvas) {
891 col.reverse();
892 }
893 return roleCanvas;
894}
895function drawText(canvas, start, text, forceOverwrite = false) {
896 increaseSize(canvas, start.x + text.length, start.y);
897 for (let i = 0; i < text.length; i++) {
898 const x = start.x + i;
899 const current = canvas[x][start.y];
900 if (forceOverwrite || current === " ") {
901 canvas[x][start.y] = text[i];
902 }
903 }
904}
905function setCanvasSizeToGrid(canvas, columnWidth, rowHeight) {
906 let maxX = 0;
907 let maxY = 0;
908 for (const w of columnWidth.values()) maxX += w;
909 for (const h of rowHeight.values()) maxY += h;
910 increaseSize(canvas, maxX - 1, maxY - 1);
911}
912function setRoleCanvasSizeToGrid(roleCanvas, columnWidth, rowHeight) {
913 let maxX = 0;
914 let maxY = 0;
915 for (const w of columnWidth.values()) maxX += w;
916 for (const h of rowHeight.values()) maxY += h;
917 increaseRoleCanvasSize(roleCanvas, maxX - 1, maxY - 1);
918}
919
920// node_modules/beautiful-mermaid/src/ascii/converter.ts
921function convertToAsciiGraph(parsed, config) {
922 const nodeMap = /* @__PURE__ */ new Map();
923 let index = 0;
924 for (const [id, mNode] of parsed.nodes) {
925 const asciiNode = {
926 // Use the parser ID as the unique identity key to avoid collisions
927 // when multiple nodes share the same label (e.g. A[Web Server], C[Web Server]).
928 name: id,
929 // The label is used for rendering inside the box.
930 displayLabel: mNode.label,
931 // Preserve shape from parser for shape-aware rendering
932 shape: mNode.shape,
933 index,
934 gridCoord: null,
935 drawingCoord: null,
936 drawing: null,
937 drawn: false,
938 styleClassName: "",
939 styleClass: EMPTY_STYLE
940 };
941 nodeMap.set(id, asciiNode);
942 index++;
943 }
944 const nodes = [...nodeMap.values()];
945 const edges = [];
946 for (const mEdge of parsed.edges) {
947 const from = nodeMap.get(mEdge.source);
948 const to = nodeMap.get(mEdge.target);
949 if (!from || !to) continue;
950 edges.push({
951 from,
952 to,
953 text: mEdge.label ?? "",
954 path: [],
955 labelLine: [],
956 startDir: { x: 0, y: 0 },
957 endDir: { x: 0, y: 0 },
958 style: mEdge.style,
959 hasArrowStart: mEdge.hasArrowStart,
960 hasArrowEnd: mEdge.hasArrowEnd
961 });
962 }
963 const subgraphs = [];
964 for (const mSg of parsed.subgraphs) {
965 convertSubgraph(mSg, null, nodeMap, subgraphs);
966 }
967 deduplicateSubgraphNodes(parsed.subgraphs, subgraphs, nodeMap, parsed);
968 for (const [nodeId, className] of parsed.classAssignments) {
969 const node = nodeMap.get(nodeId);
970 const classDef = parsed.classDefs.get(className);
971 if (node && classDef) {
972 node.styleClassName = className;
973 node.styleClass = { name: className, styles: classDef };
974 }
975 }
976 return {
977 nodes,
978 edges,
979 canvas: mkCanvas(0, 0),
980 roleCanvas: mkRoleCanvas(0, 0),
981 grid: /* @__PURE__ */ new Map(),
982 columnWidth: /* @__PURE__ */ new Map(),
983 rowHeight: /* @__PURE__ */ new Map(),
984 subgraphs,
985 config,
986 offsetX: 0,
987 offsetY: 0,
988 bundles: []
989 // Populated by analyzeEdgeBundles() during layout
990 };
991}
992function convertSubgraph(mSg, parent, nodeMap, allSubgraphs) {
993 let normalizedDirection;
994 if (mSg.direction) {
995 normalizedDirection = mSg.direction === "LR" || mSg.direction === "RL" ? "LR" : "TD";
996 }
997 const sg = {
998 name: mSg.label,
999 nodes: [],
1000 parent,
1001 children: [],
1002 minX: 0,
1003 minY: 0,
1004 maxX: 0,
1005 maxY: 0,
1006 direction: normalizedDirection
1007 };
1008 for (const nodeId of mSg.nodeIds) {
1009 const node = nodeMap.get(nodeId);
1010 if (node) sg.nodes.push(node);
1011 }
1012 allSubgraphs.push(sg);
1013 for (const childMSg of mSg.children) {
1014 const child = convertSubgraph(childMSg, sg, nodeMap, allSubgraphs);
1015 sg.children.push(child);
1016 for (const childNode of child.nodes) {
1017 if (!sg.nodes.includes(childNode)) {
1018 sg.nodes.push(childNode);
1019 }
1020 }
1021 }
1022 return sg;
1023}
1024function deduplicateSubgraphNodes(mermaidSubgraphs, asciiSubgraphs, nodeMap, parsed) {
1025 const sgMap = /* @__PURE__ */ new Map();
1026 buildSgMap(mermaidSubgraphs, asciiSubgraphs, sgMap);
1027 const nodeOwner = /* @__PURE__ */ new Map();
1028 function claimNodes(mSg) {
1029 const asciiSg = sgMap.get(mSg);
1030 if (!asciiSg) return;
1031 for (const child of mSg.children) {
1032 claimNodes(child);
1033 }
1034 for (const nodeId of mSg.nodeIds) {
1035 if (!nodeOwner.has(nodeId)) {
1036 nodeOwner.set(nodeId, asciiSg);
1037 }
1038 }
1039 }
1040 for (const mSg of mermaidSubgraphs) {
1041 claimNodes(mSg);
1042 }
1043 for (const asciiSg of asciiSubgraphs) {
1044 asciiSg.nodes = asciiSg.nodes.filter((node) => {
1045 let nodeId;
1046 for (const [id, n] of nodeMap) {
1047 if (n === node) {
1048 nodeId = id;
1049 break;
1050 }
1051 }
1052 if (!nodeId) return false;
1053 const owner = nodeOwner.get(nodeId);
1054 if (!owner) return true;
1055 return isAncestorOrSelf(asciiSg, owner);
1056 });
1057 }
1058}
1059function isAncestorOrSelf(candidate, target) {
1060 let current = target;
1061 while (current !== null) {
1062 if (current === candidate) return true;
1063 current = current.parent;
1064 }
1065 return false;
1066}
1067function buildSgMap(mSgs, aSgs, result) {
1068 const flatMermaid = [];
1069 function flatten(sgs) {
1070 for (const sg of sgs) {
1071 flatMermaid.push(sg);
1072 flatten(sg.children);
1073 }
1074 }
1075 flatten(mSgs);
1076 for (let i = 0; i < flatMermaid.length && i < aSgs.length; i++) {
1077 result.set(flatMermaid[i], aSgs[i]);
1078 }
1079}
1080
1081// node_modules/beautiful-mermaid/src/ascii/pathfinder.ts
1082var MinHeap = class {
1083 items = [];
1084 get length() {
1085 return this.items.length;
1086 }
1087 push(item) {
1088 this.items.push(item);
1089 this.bubbleUp(this.items.length - 1);
1090 }
1091 pop() {
1092 if (this.items.length === 0) return void 0;
1093 const top = this.items[0];
1094 const last = this.items.pop();
1095 if (this.items.length > 0) {
1096 this.items[0] = last;
1097 this.sinkDown(0);
1098 }
1099 return top;
1100 }
1101 bubbleUp(i) {
1102 while (i > 0) {
1103 const parent = i - 1 >> 1;
1104 if (this.items[i].priority < this.items[parent].priority) {
1105 ;
1106 [this.items[i], this.items[parent]] = [this.items[parent], this.items[i]];
1107 i = parent;
1108 } else {
1109 break;
1110 }
1111 }
1112 }
1113 sinkDown(i) {
1114 const n = this.items.length;
1115 while (true) {
1116 let smallest = i;
1117 const left = 2 * i + 1;
1118 const right = 2 * i + 2;
1119 if (left < n && this.items[left].priority < this.items[smallest].priority) {
1120 smallest = left;
1121 }
1122 if (right < n && this.items[right].priority < this.items[smallest].priority) {
1123 smallest = right;
1124 }
1125 if (smallest !== i) {
1126 ;
1127 [this.items[i], this.items[smallest]] = [this.items[smallest], this.items[i]];
1128 i = smallest;
1129 } else {
1130 break;
1131 }
1132 }
1133 }
1134};
1135function heuristic(a, b) {
1136 const absX = Math.abs(a.x - b.x);
1137 const absY = Math.abs(a.y - b.y);
1138 if (absX === 0 || absY === 0) {
1139 return absX + absY;
1140 }
1141 return absX + absY + 1;
1142}
1143var MOVE_DIRS = [
1144 { x: 1, y: 0 },
1145 { x: -1, y: 0 },
1146 { x: 0, y: 1 },
1147 { x: 0, y: -1 }
1148];
1149function isFreeInGrid(grid, c) {
1150 if (c.x < 0 || c.y < 0) return false;
1151 return !grid.has(gridKey(c));
1152}
1153function getPath(grid, from, to) {
1154 const pq = new MinHeap();
1155 pq.push({ coord: from, priority: 0 });
1156 const costSoFar = /* @__PURE__ */ new Map();
1157 costSoFar.set(gridKey(from), 0);
1158 const cameFrom = /* @__PURE__ */ new Map();
1159 cameFrom.set(gridKey(from), null);
1160 const MAX_EXPANSIONS = 4e4;
1161 let expansions = 0;
1162 while (pq.length > 0) {
1163 if (++expansions > MAX_EXPANSIONS) return null;
1164 const current = pq.pop().coord;
1165 if (gridCoordEquals(current, to)) {
1166 const path = [];
1167 let c = current;
1168 while (c !== null) {
1169 path.unshift(c);
1170 c = cameFrom.get(gridKey(c)) ?? null;
1171 }
1172 return path;
1173 }
1174 const currentCost = costSoFar.get(gridKey(current));
1175 for (const dir of MOVE_DIRS) {
1176 const next = { x: current.x + dir.x, y: current.y + dir.y };
1177 if (!isFreeInGrid(grid, next) && !gridCoordEquals(next, to)) {
1178 continue;
1179 }
1180 const newCost = currentCost + 1;
1181 const nextKey = gridKey(next);
1182 const existingCost = costSoFar.get(nextKey);
1183 if (existingCost === void 0 || newCost < existingCost) {
1184 costSoFar.set(nextKey, newCost);
1185 const priority = newCost + heuristic(next, to);
1186 pq.push({ coord: next, priority });
1187 cameFrom.set(nextKey, current);
1188 }
1189 }
1190 }
1191 return null;
1192}
1193function mergePath(path) {
1194 if (path.length <= 2) return path;
1195 const toRemove = /* @__PURE__ */ new Set();
1196 let step0 = path[0];
1197 let step1 = path[1];
1198 for (let idx = 2; idx < path.length; idx++) {
1199 const step2 = path[idx];
1200 const prevDx = step1.x - step0.x;