Draws Mermaid diagrams in Claude's answers as coloured terminal text: flowcharts as filled cards, plus sequence, class, ER and state diagrams and simple charts.

A Claude Code mod that draws the `mermaid blocks of Claude's answers as terminal text — no pictures, so it is instant, copyable, searchable and works in any terminal.
| Diagram | Drawn as |
|---|---|
flowchart / graph (TD, LR) | filled cards with light rounded outlines, coloured by :::colour or by shape; subgraph groups on a tinted background; edges that skip layers or go back are routed round the outside |
sequenceDiagram, classDiagram, erDiagram, stateDiagram-v2, xychart-beta | plain box drawing by beautiful-mermaid's terminal renderer |
Wide (CJK) characters are measured as two cells, so boxes stay aligned around Japanese labels. A diagram wider than the terminal is shown as its Mermaid source.
/plugin install mermaid-view --marketplace Kenyan822/figure-view
Claude Code 2.1.298 or later. No other dependency.
| Command | What it does |
|---|---|
/mermaid on / off | Turns the drawing on or off |
/mermaid dark / light | Flowchart colours for a dark or a light terminal |
A[text], A(text), A{decision}, A[(database)], a second line with <br> (A["Title<br>detail"]), :::blue / green / orange / red / purple / yellow / teal / gray (or classDef … fill:#hex), subgraph id[Title] … end, edges -->, -.->, ==>, -->|label|, A & B --> C.
`mermaid block and math or a `typst block is drawn by only one of the two mods.MIT. The vendored renderer keeps its own licence: see THIRD-PARTY-NOTICES.md.
hooks/register.tsx 151 lines1import type { Register } from 'claude-code'
2
3import { drawDiagram } from './lib/diagram'
4import { drawFlow, parseFlow, type Cell, type Colour } from './lib/flow'
5import { splitSegments } from './lib/segments'
6
7const PROMPT = `In this terminal, fenced \`\`\`mermaid blocks in your replies are drawn as coloured terminal text (no picture), instantly and at any length.
8- flowchart TD / LR is drawn as filled cards: nodes A[text], A(text), A{decision}, A[(database)]; a second line with <br> (A["Title<br>short detail"]); a colour with :::blue / green / orange / red / purple / yellow / teal / gray; groups with subgraph id[Title] … end; edges -->, -.->, ==>, -->|label|, A & B --> C.
9- sequenceDiagram, classDiagram, erDiagram, stateDiagram-v2 and xychart-beta (bar / line) are drawn too. In stateDiagram use ASCII ids with quoted labels (state "受付" as S1).
10- Keep labels short; a diagram wider than the terminal is shown as code.`
11
12// The module's own state: a reload starts it over.
13const state = { isOn: true, isDark: true }
14
15export const register: Register = on => {
16 on('session.start', async ($, e, next) => {
17 await $.command.register({
18 name: 'mermaid',
19 description: 'Mermaid の図を文字で描く表示: on / off / dark / light',
20 argumentHint: '[on|off|dark|light]',
21 })
22 return next(e)
23 })
24
25 on('command.run', { command: 'mermaid' }, async ($, e) => {
26 const arg = e.args.trim()
27 if (arg === 'on' || arg === 'off') state.isOn = arg === 'on'
28 else if (arg === 'dark' || arg === 'light') state.isDark = arg === 'dark'
29 else if (arg) return { text: '使い方: /mermaid on | off | dark | light' }
30 $.ui.invalidate('ui.render')
31 return { text: `mermaid-view: ${state.isOn ? 'on' : 'off'}(${state.isDark ? 'dark' : 'light'})` }
32 })
33
34 on('prompt.compose', async ($, e, next) => {
35 const composed = await next(e)
36 if (!state.isOn || !e.surfaces.includes('terminal')) return composed
37 return { sections: [...composed.sections, { id: 'mermaid-view:prompt', text: PROMPT, scope: 'session' as const }] }
38 })
39
40 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
41 if (!state.isOn || e.surface !== 'terminal' || e.props.isSummary) return next(e)
42 const segments = splitSegments(e.props.text)
43 if (!segments.some(s => s.kind === 'mermaid')) return next(e)
44
45 const { Box, Markdown, Text } = $.ui.resolve(e)
46 // Room beside the bullet column the engine draws before a reply.
47 const columns = Math.max(10, (e.viewport?.columns ?? 80) - 4)
48
49 const parts = segments.map((seg, i) => {
50 if (seg.kind === 'md') return <Markdown key={`md${i}`} text={seg.text} />
51 const flow = parseFlow(seg.src)
52 const drawn = flow && drawFlow(flow, columns)
53 const diagram = drawn ? null : drawDiagram(seg.src, columns)
54 const grid = drawn?.grid ?? diagram
55 if (!grid) return <Markdown key={`src${i}`} text={'```mermaid\n' + seg.src + '\n```'} />
56 // Diagrams other than flowcharts stay plain: the terminal's own colours.
57 if (diagram) {
58 return (
59 <Box key={`fig${i}`} flexDirection="column" marginY={1}>
60 {diagram.map((row, y) => <Text key={`r${y}`} wrap="truncate-end">{row.map(c => c.ch).join('')}</Text>)}
61 </Box>
62 )
63 }
64 return (
65 <Box key={`fig${i}`} flexDirection="column" marginY={1}>
66 {grid.map((row, y) => (
67 <Text key={`r${y}`} wrap="truncate-end">
68 {runsOf(row).map((run, j) => (
69 <Text key={`c${j}`} {...styleOf(run.cell)}>{run.text}</Text>
70 ))}
71 </Text>
72 ))}
73 {(drawn?.notes ?? []).map((note, j) => <Text key={`n${j}`} dimColor>{note}</Text>)}
74 </Box>
75 )
76 })
77
78 const body = <Box flexDirection="column" flexGrow={1}>{parts}</Box>
79 if (!e.props.isFirstOfReply) return <Box paddingLeft={2}>{body}</Box>
80 return (
81 <Box flexDirection="row">
82 <Text>⏺ </Text>
83 {body}
84 </Box>
85 )
86 })
87}
88
89// Colours. Cards are filled by their `:::colour`, else by shape (decisions
90// and databases stand out); lines and arrows are a soft accent, labels small
91// pills, subgraphs a faint tint, chart series their own colour.
92type Palette = Record<Colour, string> & { text: string; sub: string; border: string; line: string; arrow: string; label: string; labelText: string; groups: string[]; title: string }
93const PALETTE: { dark: Palette; light: Palette } = {
94 dark: {
95 blue: '#2d4a6e', green: '#2f5a3c', orange: '#6e4a22', red: '#6b2f35', purple: '#4f3a6e', yellow: '#5f5420', teal: '#22585a', gray: '#3e434d',
96 text: '#f5f5f5', sub: '#c9d1dc', border: '#f0f0f0', line: '#7f93ad', arrow: '#c8d6ea', label: '#3a3f4b', labelText: '#e8edf5',
97 groups: ['#23262d', '#262329', '#22282a', '#28261f'], title: '#9aa6b8',
98 },
99 light: {
100 blue: '#dbe7f7', green: '#dcefe0', orange: '#f6e3cc', red: '#f6d9db', purple: '#ece0f5', yellow: '#f4ecc6', teal: '#d5eeee', gray: '#e6e8ec',
101 text: '#1f2328', sub: '#4a5361', border: '#3a4250', line: '#8a9bb0', arrow: '#3b5578', label: '#e8ecf2', labelText: '#2a3442',
102 groups: ['#f3f5f8', '#f6f3f8', '#f2f7f7', '#f8f6ef'], title: '#5b6676',
103 },
104}
105const SHAPE_COLOUR: Record<string, Colour> = { box: 'blue', round: 'green', diamond: 'yellow', db: 'purple' }
106
107type Run = { text: string; cell: Cell }
108
109// Consecutive cells drawn alike become one run of text.
110function runsOf(row: Cell[]): Run[] {
111 const runs: Run[] = []
112 for (const cell of row) {
113 if (cell.ch === '') continue
114 const last = runs[runs.length - 1]
115 const same =
116 last && last.cell.kind === cell.kind && last.cell.node === cell.node && last.cell.head === cell.head &&
117 last.cell.group === cell.group && last.cell.hex === cell.hex
118 if (last && same) last.text += cell.ch
119 else runs.push({ text: cell.ch, cell })
120 }
121 return runs
122}
123
124function styleOf(cell: Cell): { color?: string; backgroundColor?: string; bold?: boolean } {
125 const c = state.isDark ? PALETTE.dark : PALETTE.light
126 const tint = cell.group === undefined ? undefined : c.groups[cell.group % c.groups.length]
127 // A box the vendored renderer drew has no node: the default fill.
128 const fill = cell.node ? c[cell.node.colour ?? SHAPE_COLOUR[cell.node.shape] ?? 'blue'] : c.blue
129 if (cell.hex) return { color: cell.hex, backgroundColor: tint }
130 switch (cell.kind) {
131 case 'node':
132 return { color: cell.head || !cell.node ? c.text : c.sub, backgroundColor: fill, bold: cell.head || !cell.node }
133 case 'fill':
134 return { backgroundColor: fill }
135 case 'border':
136 // The outline sits on the background: a filled cell behind a corner
137 // would show the fill outside the rounded frame.
138 return { color: c.border, backgroundColor: tint }
139 case 'arrow':
140 return { color: c.arrow, backgroundColor: tint }
141 case 'line':
142 return { color: c.line, backgroundColor: tint }
143 case 'label':
144 return { color: c.labelText, backgroundColor: c.label }
145 case 'title':
146 return { color: c.title, backgroundColor: tint, bold: true }
147 default:
148 return { backgroundColor: tint }
149 }
150}
151hooks/lib/diagram.ts 82 lines1// Mermaid diagrams other than flowcharts (sequence, class, ER, state, xychart)
2// drawn as terminal text by the vendored beautiful-mermaid renderer, then
3// styled like the flowcharts: boxes become filled cards with a light outline,
4// labels outside boxes become pills, chart series keep their colours.
5
6import { renderMermaidASCII } from '../vendor/beautiful-mermaid-ascii.js'
7
8import { charWidth, type Cell } from './flow'
9
10// The renderer gives every character one cell. A marker after each wide
11// character reserves its second cell, and is taken out of the result.
12const MARK = ''
13
14// Colours that stand for the renderer's roles in its HTML output.
15const ROLE_BY_HEX: Record<string, Cell['kind']> = { '#000001': 'label', '#000002': 'border', '#000003': 'line', '#000004': 'arrow' }
16const THEME = { fg: '#000001', border: '#000002', line: '#000003', arrow: '#000004' }
17
18const ROUND: Record<string, string> = { '┌': '╭', '┐': '╮', '└': '╰', '┘': '╯' }
19const ENTITIES: Record<string, string> = { '<': '<', '>': '>', '&': '&', '"': '"', ''': "'" }
20
21type Raw = { ch: string; kind: Cell['kind']; hex?: string }
22
23// The diagram as rows of styled cells at most `maxWidth` wide; null when it
24// is not a diagram the renderer reads, or does not fit.
25export function drawDiagram(src: string, maxWidth: number): Cell[][] | null {
26 const padded = [...src.replace(/[︎️]/g, '')].map(ch => (charWidth(ch) === 2 ? ch + MARK : ch)).join('')
27 let html: string
28 try {
29 html = renderMermaidASCII(padded, { colorMode: 'html', theme: THEME })
30 } catch {
31 return null
32 }
33 const rows = html.split('\n').map(parseLine)
34 while (rows.length && rows[rows.length - 1]?.every(c => c.ch === ' ')) rows.pop()
35 if (!rows.some(r => r.some(c => c.ch.trim()))) return null
36 fillBoxes(rows)
37 const grid = rows.map(r => r.map((c): Cell => (c.ch === MARK ? { ...c, ch: '' } : c)))
38 const widths = grid.map(r => r.reduce((w, c) => w + (c.ch === '' ? 0 : charWidth(c.ch)), 0))
39 if (widths.some(w => w > maxWidth)) return null
40 // Trailing spaces would carry a background across the terminal.
41 for (const r of grid) while (r.length && r[r.length - 1]?.kind === 'space') r.pop()
42 return grid
43}
44
45function parseLine(line: string): Raw[] {
46 const out: Raw[] = []
47 for (const m of line.matchAll(/<span style="color:(#[0-9a-fA-F]{6})">(.*?)<\/span>|([^<]+)/g)) {
48 const hex = m[1]?.toLowerCase()
49 const text = (m[2] ?? m[3] ?? '').replace(/&(lt|gt|amp|quot|#39);/g, e => ENTITIES[e] ?? e)
50 const kind: Cell['kind'] = hex ? ROLE_BY_HEX[hex] ?? 'line' : 'space'
51 for (const ch of text) {
52 if (ch === ' ' && kind === 'space') out.push({ ch, kind: 'space' })
53 else out.push({ ch: kind === 'border' ? ROUND[ch] ?? ch : ch, kind, hex: hex && !ROLE_BY_HEX[hex] ? hex : undefined })
54 }
55 }
56 return out
57}
58
59// Every box (a border corner ╭ with its ╮ to the right and ╰ below) gets its
60// inside filled: the text there is a card's title, the blanks its fill.
61function fillBoxes(rows: Raw[][]): void {
62 const at = (x: number, y: number) => rows[y]?.[x]
63 for (let y = 0; y < rows.length; y++) {
64 const row = rows[y] ?? []
65 for (let x = 0; x < row.length; x++) {
66 if (at(x, y)?.ch !== '╭' || at(x, y)?.kind !== 'border') continue
67 let x2 = x + 1
68 while (x2 < row.length && at(x2, y)?.kind === 'border' && at(x2, y)?.ch !== '╮') x2++
69 let y2 = y + 1
70 while (y2 < rows.length && at(x, y2)?.kind === 'border' && at(x, y2)?.ch !== '╰') y2++
71 if (at(x2, y)?.ch !== '╮' || at(x, y2)?.ch !== '╰') continue
72 for (let yy = y + 1; yy < y2; yy++) {
73 for (let xx = x + 1; xx < x2; xx++) {
74 const c = at(xx, yy)
75 // Dividers inside a box (class members, ER attributes) stay lines.
76 if (c && (c.kind === 'space' || c.kind === 'label')) c.kind = c.kind === 'label' ? 'node' : 'fill'
77 }
78 }
79 }
80 }
81}
82hooks/lib/flow.ts 461 lines1// Mermaid flowcharts drawn as terminal text: filled cards, rounded lines and
2// arrows laid out on a grid of cells, no picture. Read: `flowchart`/`graph`
3// TD/TB/LR, nodes A[..] A(..) A([..]) A{..} A[(..)], `<br>` for a second line,
4// `:::colour` classes, edges --> -.-> ==> with |labels|, and subgraph … end.
5// Anything else returns null and stays a code block.
6
7export const COLOURS = ['blue', 'green', 'orange', 'red', 'purple', 'yellow', 'teal', 'gray'] as const
8export type Colour = (typeof COLOURS)[number]
9
10export type FlowNode = { id: string; lines: string[]; shape: 'box' | 'round' | 'diamond' | 'db'; colour?: Colour; group?: number }
11export type FlowEdge = { from: string; to: string; label: string; style: 'solid' | 'dotted' | 'thick' }
12export type FlowGroup = { title: string }
13export type Flow = { dir: 'TD' | 'LR'; nodes: FlowNode[]; edges: FlowEdge[]; groups: FlowGroup[] }
14
15// One cell of the drawing: its character and what it belongs to. A wide
16// character takes two cells; the second holds ''. `head` marks a card's
17// first text line, `group` the subgraph whose background the cell is on.
18export type Cell = {
19 ch: string
20 kind: 'space' | 'line' | 'arrow' | 'node' | 'border' | 'fill' | 'label' | 'title'
21 node?: FlowNode
22 // A colour of its own (a chart series), kept as drawn.
23 hex?: string
24 head?: boolean
25 group?: number
26}
27
28const HEADER = /^\s*(flowchart|graph)\s+(TD|TB|LR|BT|RL)\b/i
29const NODE = /^([A-Za-z_][\w-]*)\s*(\[\[|\[\(|\(\[|\(\(|\[|\(|\{\{|\{|>)?\s*(.*?)\s*(?::::\s*([\w-]+))?$/
30const CLOSE: Record<string, string> = { '[[': ']]', '[(': ')]', '([': '])', '((': '))', '[': ']', '(': ')', '{{': '}}', '{': '}', '>': ']' }
31const ARROW = /\s*(-->|---|-\.->|-\.-|==>|===|--\s*([^-|>][^>]*?)\s*-->|==\s*([^=][^>]*?)\s*==>|-\.\s*([^.]+?)\s*\.->)\s*(?:\|([^|]*)\|)?\s*/
32
33export function parseFlow(src: string): Flow | null {
34 const lines = src.split('\n').map(l => l.replace(/%%.*$/, '').trim()).filter(Boolean)
35 const head = HEADER.exec(lines[0] ?? '')
36 if (!head) return null
37 const d = (head[2] ?? '').toUpperCase()
38 if (d !== 'TD' && d !== 'TB' && d !== 'LR') return null
39 const flow: Flow = { dir: d === 'LR' ? 'LR' : 'TD', nodes: [], edges: [], groups: [] }
40 const byId = new Map<string, FlowNode>()
41 const classes = new Map<string, Colour>()
42 let group: number | undefined
43
44 const node = (raw: string): string | null => {
45 const m = NODE.exec(raw.trim())
46 if (!m) return null
47 const id = m[1] ?? ''
48 const open = m[2]
49 const colour = colourOf(m[4], classes)
50 let text: string | null = null
51 let shape: FlowNode['shape'] = 'box'
52 if (open) {
53 const close = CLOSE[open] ?? ']'
54 const body = (m[3] ?? '').trim()
55 if (!body.endsWith(close)) return null
56 text = body.slice(0, -close.length).trim().replace(/^"(.*)"$/, '$1')
57 shape = open === '{' || open === '{{' ? 'diamond' : open === '[(' ? 'db' : open.includes('(') ? 'round' : 'box'
58 } else if ((m[3] ?? '').trim()) {
59 return null
60 }
61 const known = byId.get(id)
62 // Presentation selectors and joiners change a glyph's width from terminal
63 // to terminal; without them each character's width is known.
64 const lines = (text ?? id).replace(/[\uFE0E\uFE0F\u200D]/g, '').split(/<br\s*\/?>|\\n/i).map(l => l.trim()).filter(Boolean)
65 if (!known) {
66 const n: FlowNode = { id, lines: lines.length ? lines : [id], shape, colour, group }
67 byId.set(id, n)
68 flow.nodes.push(n)
69 } else {
70 if (text !== null) {
71 known.lines = lines
72 known.shape = shape
73 }
74 if (colour) known.colour = colour
75 if (known.group === undefined) known.group = group
76 }
77 return id
78 }
79
80 for (const line of lines.slice(1)) {
81 const sub = /^subgraph\s+(?:([\w-]+)\s*\[(.*)\]|"?(.*?)"?)\s*$/.exec(line)
82 if (sub) {
83 flow.groups.push({ title: (sub[2] ?? sub[3] ?? sub[1] ?? '').trim() })
84 group = flow.groups.length - 1
85 continue
86 }
87 if (/^end\b/.test(line)) {
88 group = undefined
89 continue
90 }
91 const def = /^classDef\s+([\w-]+)\s+(.*)$/.exec(line)
92 if (def) {
93 const c = colourOfStyle(def[2] ?? '')
94 if (c) classes.set(def[1] ?? '', c)
95 continue
96 }
97 const cls = /^class\s+([\w,\s-]+?)\s+([\w-]+)$/.exec(line)
98 if (cls) {
99 const c = colourOf(cls[2], classes)
100 for (const id of (cls[1] ?? '').split(',').map(x => x.trim())) {
101 const n = byId.get(id)
102 if (n && c) n.colour = c
103 }
104 continue
105 }
106 if (/^(style|linkStyle|click|direction)\b/.test(line)) continue
107
108 // A chain: node (arrow node)*. Arrows inside a node's brackets are text.
109 const parts: string[] = []
110 const arrows: RegExpExecArray[] = []
111 let rest = line.replace(/;$/, '')
112 for (;;) {
113 const at = findArrow(rest)
114 if (!at) break
115 parts.push(rest.slice(0, at.index))
116 arrows.push(at)
117 rest = rest.slice(at.index + at[0].length)
118 }
119 parts.push(rest)
120 // `A & B --> C` fans out: each side may list several nodes.
121 const sides = parts.map(p => p.split(/\s+&\s+/).map(x => (x.trim() ? node(x) : null)))
122 if (sides.some(ids => ids.some(id => id === null))) return null
123 arrows.forEach((a, i) => {
124 const op = a[1] ?? ''
125 const label = (a[5] ?? a[2] ?? a[3] ?? a[4] ?? '').trim()
126 const style = op.startsWith('=') ? 'thick' : op.includes('.') ? 'dotted' : 'solid'
127 for (const from of sides[i] ?? []) for (const to of sides[i + 1] ?? []) flow.edges.push({ from: from as string, to: to as string, label, style })
128 })
129 }
130 return flow.nodes.length ? flow : null
131}
132
133function colourOf(name: string | undefined, classes: Map<string, Colour>): Colour | undefined {
134 if (!name) return undefined
135 if ((COLOURS as readonly string[]).includes(name)) return name as Colour
136 return classes.get(name)
137}
138
139// A classDef's fill named by its nearest palette colour (fill:#hex), or by a
140// palette name written as the fill.
141function colourOfStyle(style: string): Colour | undefined {
142 const fill = /fill\s*:\s*([#\w]+)/.exec(style)?.[1]
143 if (!fill) return undefined
144 if ((COLOURS as readonly string[]).includes(fill)) return fill as Colour
145 const hex = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(fill)?.[1]
146 if (!hex) return undefined
147 const full = hex.length === 3 ? [...hex].map(c => c + c).join('') : hex
148 const [r, g, b] = [0, 2, 4].map(i => parseInt(full.slice(i, i + 2), 16))
149 const hues: [Colour, number, number, number][] = [
150 ['blue', 70, 120, 200], ['green', 70, 160, 90], ['orange', 220, 140, 50], ['red', 200, 70, 70],
151 ['purple', 150, 90, 200], ['yellow', 210, 190, 70], ['teal', 60, 160, 160], ['gray', 128, 128, 128],
152 ]
153 let best: Colour = 'gray'
154 let dist = Infinity
155 for (const [name, hr, hg, hb] of hues) {
156 const dd = ((r ?? 0) - hr) ** 2 + ((g ?? 0) - hg) ** 2 + ((b ?? 0) - hb) ** 2
157 if (dd < dist) [best, dist] = [name, dd]
158 }
159 return best
160}
161
162// The first arrow in `s` that is not inside a node's brackets.
163function findArrow(s: string): RegExpExecArray | null {
164 let depth = 0
165 for (let i = 0; i < s.length; i++) {
166 const c = s[i] ?? ''
167 if ('[({'.includes(c)) depth++
168 else if (')]}'.includes(c)) depth = Math.max(0, depth - 1)
169 else if (depth === 0 && (c === '-' || c === '=')) {
170 const m = ARROW.exec(s.slice(i))
171 if (m && m.index === 0) {
172 m.index = i
173 return m
174 }
175 }
176 }
177 return null
178}
179
180export function charWidth(ch: string): number {
181 const c = ch.codePointAt(0) ?? 0
182 return (c >= 0x1100 && c <= 0x115f) || (c >= 0x2e80 && c <= 0xa4cf) || (c >= 0xac00 && c <= 0xd7a3) ||
183 (c >= 0xf900 && c <= 0xfaff) || (c >= 0xfe30 && c <= 0xfe4f) || (c >= 0xff00 && c <= 0xff60) ||
184 (c >= 0xffe0 && c <= 0xffe6) || (c >= 0x1f300 && c <= 0x1faff)
185 ? 2
186 : 1
187}
188
189export const textWidth = (s: string) => [...s].reduce((w, ch) => w + charWidth(ch), 0)
190
191// Layers by the longest path from a source; an edge that closes a cycle is
192// drawn but does not move its target down.
193function layersOf(flow: Flow): Map<string, number> {
194 const out = new Map<string, string[]>()
195 for (const e of flow.edges) out.set(e.from, [...(out.get(e.from) ?? []), e.to])
196 const layer = new Map<string, number>()
197 const onPath = new Set<string>()
198 const visit = (id: string, depth: number) => {
199 if (onPath.has(id) || (layer.get(id) ?? -1) >= depth) return
200 layer.set(id, depth)
201 onPath.add(id)
202 for (const to of out.get(id) ?? []) visit(to, depth + 1)
203 onPath.delete(id)
204 }
205 const hasIn = new Set(flow.edges.map(e => e.to))
206 for (const n of flow.nodes) if (!hasIn.has(n.id)) visit(n.id, 0)
207 for (const n of flow.nodes) if (!layer.has(n.id)) visit(n.id, 0)
208 return layer
209}
210
211type Placed = { x: number; y: number; w: number; h: number }
212
213// Line directions of a cell, combined when edges cross or meet; corners are
214// rounded.
215const U = 1, DN = 2, L = 4, R = 8
216const LINE: Record<number, string> = {
217 [U]: '│', [DN]: '│', [U | DN]: '│', [L]: '─', [R]: '─', [L | R]: '─',
218 [DN | R]: '╭', [DN | L]: '╮', [U | R]: '╰', [U | L]: '╯',
219 [U | DN | R]: '├', [U | DN | L]: '┤', [DN | L | R]: '┬', [U | L | R]: '┴', [U | DN | L | R]: '┼',
220}
221
222const ICON: Partial<Record<FlowNode['shape'], string>> = { diamond: '◆ ', db: '◉ ' }
223const iconOf = (n: FlowNode) => (/^\p{Extended_Pictographic}/u.test(n.lines[0] ?? '') ? '' : ICON[n.shape] ?? '')
224
225const nodeW = (n: FlowNode, max: number) => Math.min(max, Math.max(...n.lines.map((l, i) => textWidth((i === 0 ? iconOf(n) : '') + l))) + 4)
226const nodeH = (n: FlowNode) => n.lines.length + 2
227
228// Lays the flow out and draws it into a grid of cells, at most `maxWidth`
229// wide; null when it does not fit. Edge labels that find no free room are
230// returned as `notes` ("A → B: label") to print under the drawing.
231export function drawFlow(flow: Flow, maxWidth: number): { grid: Cell[][]; notes: string[] } | null {
232 const layer = layersOf(flow)
233 const depth = Math.max(0, ...layer.values()) + 1
234 const rows: FlowNode[][] = Array.from({ length: depth }, () => [])
235 for (const n of flow.nodes) rows[layer.get(n.id) ?? 0]?.push(n)
236 // Members of a subgraph side by side, so its background is one block.
237 for (const r of rows) r.sort((a, b) => (a.group ?? -1) - (b.group ?? -1))
238 // An edge to an earlier layer, or one that skips layers, would cross the
239 // cards between: it goes round the outside instead.
240 const isAround = (e: FlowEdge) => {
241 const span = (layer.get(e.to) ?? 0) - (layer.get(e.from) ?? 0)
242 return span <= 0 || span > 1
243 }
244 const backs = flow.edges.filter(isAround)
245 const titled = flow.groups.length > 0
246 const placed = new Map<FlowNode, Placed>()
247 let width = 0
248 let height = 0
249 // The width of the cards alone (TD), left of the lanes that go round.
250 let body = 0
251 if (flow.dir === 'TD') {
252 const gapX = 4
253 const gapY = titled ? 4 : 3
254 const rowW = rows.map(r => r.reduce((w, n) => w + nodeW(n, maxWidth - 2), 0) + gapX * Math.max(0, r.length - 1))
255 const rowH = rows.map(r => Math.max(3, ...r.map(nodeH)))
256 body = Math.max(...rowW) + (titled ? 2 : 0)
257 let y = titled ? 2 : 0
258 rows.forEach((r, i) => {
259 let x = Math.floor((body - (rowW[i] ?? 0)) / 2)
260 for (const n of r) {
261 const h = nodeH(n)
262 placed.set(n, { x, y: y + Math.floor(((rowH[i] ?? h) - h) / 2), w: nodeW(n, maxWidth - 2), h })
263 x += nodeW(n, maxWidth - 2) + gapX
264 }
265 y += (rowH[i] ?? 3) + gapY
266 })
267 const aroundLabel = Math.max(0, ...backs.map(e => textWidth(e.label)))
268 width = body + (backs.length ? 2 + backs.length * 2 + (aroundLabel ? aroundLabel + 4 : 0) : 0)
269 height = y - gapY + (titled ? 1 : 0)
270 } else {
271 const colW = rows.map(r => Math.max(...r.map(n => nodeW(n, maxWidth - 2))))
272 // Room between two columns for the labels of the edges that cross it.
273 const gapOf = (i: number) =>
274 Math.max(6, ...flow.edges.filter(e => !isAround(e) && layer.get(e.from) === i).map(e => textWidth(e.label) + 6))
275 const colH = rows.map(r => r.reduce((h, n) => h + nodeH(n), 0) + Math.max(0, r.length - 1) * (titled ? 3 : 1))
276 const tall = Math.max(...colH)
277 const top = titled ? 2 : 1
278 let x = titled ? 1 : 0
279 rows.forEach((r, i) => {
280 let y = Math.floor((tall - (colH[i] ?? 0)) / 2) + top
281 for (const n of r) {
282 const w = nodeW(n, maxWidth - 2)
283 placed.set(n, { x: x + Math.floor(((colW[i] ?? w) - w) / 2), y, w, h: nodeH(n) })
284 y += nodeH(n) + (titled ? 3 : 1)
285 }
286 x += (colW[i] ?? 0) + gapOf(i)
287 })
288 width = x - gapOf(rows.length - 1) + (titled ? 1 : 0)
289 height = tall + top + (titled ? 1 : 0) + (backs.length ? 1 + backs.length * 2 : 0)
290 }
291 // A left-to-right chart too wide for the terminal is laid out top-down.
292 if (width > maxWidth) return flow.dir === 'LR' ? drawFlow({ ...flow, dir: 'TD' }, maxWidth) : null
293
294 const masks: number[][] = Array.from({ length: height }, () => Array(width).fill(0))
295 const styles: (FlowEdge['style'] | undefined)[][] = Array.from({ length: height }, () => Array(width).fill(undefined))
296 const arrows = new Map<string, string>()
297 const mark = (x: number, y: number, m: number, style: FlowEdge['style']) => {
298 const row = masks[y]
299 if (!row || x < 0 || x >= width) return
300 row[x] = (row[x] ?? 0) | m
301 const st = styles[y]
302 if (st && style !== 'solid') st[x] = style
303 }
304 // A path of corner points, each pair horizontal or vertical.
305 const path = (pts: [number, number][], style: FlowEdge['style']) => {
306 for (let i = 0; i + 1 < pts.length; i++) {
307 const [ax, ay] = pts[i] ?? [0, 0]
308 const [bx, by] = pts[i + 1] ?? [0, 0]
309 if (ay === by) {
310 const [lo, hi] = ax < bx ? [ax, bx] : [bx, ax]
311 for (let x = lo; x <= hi; x++) mark(x, ay, (x > lo ? L : 0) | (x < hi ? R : 0), style)
312 } else {
313 const [lo, hi] = ay < by ? [ay, by] : [by, ay]
314 for (let y = lo; y <= hi; y++) mark(ax, y, (y > lo ? U : 0) | (y < hi ? DN : 0), style)
315 }
316 }
317 }
318 const labels: { x: number; y: number; text: string; note: string }[] = []
319 const byId = new Map(flow.nodes.map(n => [n.id, n]))
320 const nameOf = (id: string) => byId.get(id)?.lines.join(' ') ?? id
321
322 let lane = 0
323 for (const e of flow.edges) {
324 const an = byId.get(e.from)
325 const bn = byId.get(e.to)
326 const a = an && placed.get(an)
327 const b = bn && placed.get(bn)
328 if (!a || !b) continue
329 const note = `${nameOf(e.from)} → ${nameOf(e.to)}: ${e.label}`
330 if (flow.dir === 'TD') {
331 if (!isAround(e)) {
332 const x1 = a.x + Math.floor(a.w / 2)
333 // Straight down when the source's middle is over the target.
334 const x2 = x1 > b.x && x1 < b.x + b.w - 1 ? x1 : b.x + Math.floor(b.w / 2)
335 const y1 = a.y + a.h
336 const y2 = b.y - 1
337 const turn = Math.min(y1 + 1, y2)
338 path([[x1, y1], [x1, turn], [x2, turn], [x2, y2]], e.style)
339 arrows.set(`${x2},${y2}`, '▼')
340 if (e.label) labels.push({ x: x2 + 2, y: y2, text: e.label, note })
341 } else {
342 const xr = body + 1 + (backs.length - lane) * 2
343 lane++
344 const ya = a.y + Math.floor(a.h / 2)
345 const yb = b.y + Math.floor(b.h / 2)
346 path([[a.x + a.w, ya], [xr, ya], [xr, yb], [b.x + b.w, yb]], e.style)
347 arrows.set(`${b.x + b.w},${yb}`, '◀')
348 if (e.label) labels.push({ x: body + 3 + backs.length * 2, y: Math.floor((ya + yb) / 2), text: e.label, note })
349 }
350 } else if (!isAround(e)) {
351 const y1 = a.y + Math.floor(a.h / 2)
352 const y2 = y1 > b.y && y1 < b.y + b.h - 1 ? y1 : b.y + Math.floor(b.h / 2)
353 const x1 = a.x + a.w
354 const x2 = b.x - 1
355 path([[x1, y1], [x1 + 1, y1], [x1 + 1, y2], [x2, y2]], e.style)
356 arrows.set(`${x2},${y2}`, '▶')
357 if (e.label) labels.push({ x: x1 + 3, y: y2 - 1, text: e.label, note })
358 } else {
359 const yb = height - 1 - lane * 2
360 lane++
361 const xa = a.x + Math.floor(a.w / 2)
362 const xb = b.x + Math.floor(b.w / 2)
363 path([[xa, a.y + a.h], [xa, yb], [xb, yb], [xb, b.y + b.h]], e.style)
364 arrows.set(`${xb},${b.y + b.h}`, '▲')
365 if (e.label) labels.push({ x: Math.min(xa, xb) + 2, y: yb - 1, text: e.label, note })
366 }
367 }
368
369 const grid: Cell[][] = masks.map((row, y) =>
370 row.map((m, x): Cell => {
371 const arrow = arrows.get(`${x},${y}`)
372 if (arrow) return { ch: arrow, kind: 'arrow' }
373 if (!m) return { ch: ' ', kind: 'space' }
374 let ch = LINE[m] ?? '┼'
375 const st = styles[y]?.[x]
376 if (st === 'dotted') ch = ch === '│' ? '┆' : ch === '─' ? '┄' : ch
377 if (st === 'thick') ch = ch === '│' ? '┃' : ch === '─' ? '━' : ch
378 return { ch, kind: 'line' }
379 }),
380 )
381
382 // Subgraphs: a tinted block round their members, titled at its top.
383 flow.groups.forEach((g, gi) => {
384 const members = [...placed].filter(([n]) => n.group === gi).map(([, p]) => p)
385 if (!members.length) return
386 const x0 = Math.max(0, Math.min(...members.map(p => p.x)) - 2)
387 const x1 = Math.min(width - 1, Math.max(...members.map(p => p.x + p.w)) + 1)
388 const y0 = Math.max(0, Math.min(...members.map(p => p.y)) - 2)
389 const y1 = Math.min(height - 1, Math.max(...members.map(p => p.y + p.h)))
390 for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) {
391 const c = grid[y]?.[x]
392 if (c && c.group === undefined) c.group = gi
393 }
394 // The title goes on the first free stretch of the block's top row, so no
395 // line is written over.
396 const r0 = grid[y0] ?? []
397 const tw = textWidth(g.title)
398 let x = x0 + 1
399 while (x + tw <= width && !r0.slice(x - 1, x + tw + 1).every(c => c.kind === 'space')) x++
400 if (x + tw > width) return
401 for (const ch of g.title) {
402 const r = grid[y0]
403 if (r) {
404 r[x] = { ch, kind: 'title', group: gi }
405 if (charWidth(ch) === 2) r[x + 1] = { ch: '', kind: 'title', group: gi }
406 }
407 x += charWidth(ch)
408 }
409 })
410
411 // A card: filled, with a rounded light outline; the first line is the
412 // title, the others a quieter description.
413 for (const [n, p] of placed) {
414 const fill = (y: number, s: string[], kinds: Cell['kind'][]) => {
415 const r = grid[y]
416 if (r) for (let i = 0; i < p.w; i++) r[p.x + i] = { ch: s[i] ?? ' ', kind: kinds[i] ?? 'node', node: n }
417 }
418 const inner = p.w - 2
419 const sides: Cell['kind'][] = ['border', ...Array<Cell['kind']>(inner).fill('node'), 'border']
420 fill(p.y, ['╭', ...Array<string>(inner).fill('─'), '╮'], Array(p.w).fill('border'))
421 for (let i = 1; i < p.h - 1; i++) fill(p.y + i, ['│', ...Array<string>(inner).fill(' '), '│'], sides)
422 fill(p.y + p.h - 1, ['╰', ...Array<string>(inner).fill('─'), '╯'], Array(p.w).fill('border'))
423 n.lines.forEach((line, i) => {
424 let text = (i === 0 ? iconOf(n) : '') + line
425 while (textWidth(text) > p.w - 2 && text.length > 1) text = text.slice(0, -1)
426 let x = p.x + Math.floor((p.w - textWidth(text)) / 2)
427 const r = grid[p.y + 1 + i]
428 for (const ch of text) {
429 if (r) {
430 r[x] = { ch, kind: 'node', node: n, head: i === 0 }
431 if (charWidth(ch) === 2) r[x + 1] = { ch: '', kind: 'node', node: n, head: i === 0 }
432 }
433 x += charWidth(ch)
434 }
435 })
436 }
437
438 // A label goes, as a small pill, where its cells are all free; else it is
439 // a note below the drawing.
440 const notes: string[] = []
441 for (const l of labels) {
442 const w = textWidth(l.text) + 2
443 const row = grid[l.y]
444 const fits = (x: number) => !!row && x >= 0 && x + w <= width && row.slice(Math.max(0, x - 1), x + w + 1).every(c => c.kind === 'space')
445 // Right of the arrow, else left of it.
446 const at = fits(l.x) ? l.x : fits(l.x - w - 3) ? l.x - w - 3 : -1
447 if (at < 0 || !row) {
448 notes.push(l.note)
449 continue
450 }
451 let x = at
452 for (const ch of ` ${l.text} `) {
453 const g = row[x]?.group
454 row[x] = { ch, kind: 'label', group: g }
455 if (charWidth(ch) === 2) row[x + 1] = { ch: '', kind: 'label', group: g }
456 x += charWidth(ch)
457 }
458 }
459 return { grid, notes }
460}
461hooks/lib/segments.ts 49 lines1// Splits an answer's markdown into text and ```mermaid fences. A fence still
2// open at the end (the answer is streaming) stays text.
3
4export type Segment = { kind: 'md'; text: string } | { kind: 'mermaid'; src: string }
5
6const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^`\s]*)/
7
8export function splitSegments(text: string): Segment[] {
9 const lines = text.split('\n')
10 const out: Segment[] = []
11 let md: string[] = []
12 const flushMd = () => {
13 if (md.length) out.push({ kind: 'md', text: md.join('\n') })
14 md = []
15 }
16 let i = 0
17 while (i < lines.length) {
18 const line = lines[i] ?? ''
19 const fence = FENCE.exec(line)
20 if (!fence) {
21 md.push(line)
22 i++
23 continue
24 }
25 const mark = fence[1] ?? '```'
26 let close = -1
27 for (let j = i + 1; j < lines.length; j++) {
28 const l = lines[j] ?? ''
29 if (l.trimStart().startsWith(mark) && l.trim().replace(/[`~]/g, '') === '') {
30 close = j
31 break
32 }
33 }
34 if (close < 0) {
35 md.push(...lines.slice(i))
36 break
37 }
38 if ((fence[2] ?? '').toLowerCase() === 'mermaid') {
39 flushMd()
40 out.push({ kind: 'mermaid', src: lines.slice(i + 1, close).join('\n') })
41 } else {
42 md.push(...lines.slice(i, close + 1))
43 }
44 i = close + 1
45 }
46 flushMd()
47 return out
48}
49hooks/vendor/beautiful-mermaid-ascii.js 5615 lines1// beautiful-mermaid 1.1.3 (https://github.com/lukilabs/beautiful-mermaid), MIT,
2// src/ascii bundled unchanged with esbuild 0.24.0 (--format=esm --platform=neutral).
3// See THIRD-PARTY-NOTICES.md for its licence.
4// node_modules/beautiful-mermaid/src/multiline-utils.ts
5function normalizeBrTags(label) {
6 const unquoted = label.startsWith('"') && label.endsWith('"') ? label.slice(1, -1) : label;
7 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>");
8}
9
10// node_modules/beautiful-mermaid/src/parser.ts
11function parseMermaid(text) {
12 const lines = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("%%"));
13 if (lines.length === 0) {
14 throw new Error("Empty mermaid diagram");
15 }
16 const header = lines[0];
17 if (/^stateDiagram(-v2)?\s*$/i.test(header)) {
18 return parseStateDiagram(lines);
19 }
20 return parseFlowchart(lines);
21}
22function parseFlowchart(lines) {
23 const headerMatch = lines[0].match(/^(?:graph|flowchart)\s+(TD|TB|LR|BT|RL)\s*$/i);
24 if (!headerMatch) {
25 throw new Error(`Invalid mermaid header: "${lines[0]}". Expected "graph TD", "flowchart LR", "stateDiagram-v2", etc.`);
26 }
27 const direction = headerMatch[1].toUpperCase();
28 const graph = {
29 direction,
30 nodes: /* @__PURE__ */ new Map(),
31 edges: [],
32 subgraphs: [],
33 classDefs: /* @__PURE__ */ new Map(),
34 classAssignments: /* @__PURE__ */ new Map(),
35 nodeStyles: /* @__PURE__ */ new Map(),
36 linkStyles: /* @__PURE__ */ new Map()
37 };
38 const subgraphStack = [];
39 for (let i = 1; i < lines.length; i++) {
40 const line = lines[i];
41 const classDefMatch = line.match(/^classDef\s+(\w+)\s+(.+)$/);
42 if (classDefMatch) {
43 const name = classDefMatch[1];
44 const propsStr = classDefMatch[2];
45 const props = parseStyleProps(propsStr);
46 graph.classDefs.set(name, props);
47 continue;
48 }
49 const classAssignMatch = line.match(/^class\s+([\w,-]+)\s+(\w+)$/);
50 if (classAssignMatch) {
51 const nodeIds = classAssignMatch[1].split(",").map((s) => s.trim());
52 const className = classAssignMatch[2];
53 for (const id of nodeIds) {
54 graph.classAssignments.set(id, className);
55 }
56 continue;
57 }
58 const styleMatch = line.match(/^style\s+([\w,-]+)\s+(.+)$/);
59 if (styleMatch) {
60 const nodeIds = styleMatch[1].split(",").map((s) => s.trim());
61 const props = parseStyleProps(styleMatch[2]);
62 for (const id of nodeIds) {
63 graph.nodeStyles.set(id, { ...graph.nodeStyles.get(id), ...props });
64 }
65 continue;
66 }
67 const linkStyleMatch = line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/);
68 if (linkStyleMatch) {
69 const target = linkStyleMatch[1].trim();
70 const props = parseStyleProps(linkStyleMatch[2]);
71 if (target === "default") {
72 graph.linkStyles.set("default", { ...graph.linkStyles.get("default"), ...props });
73 } else {
74 const indices = target.split(",").map((s) => parseInt(s.trim(), 10));
75 for (const idx of indices) {
76 if (!isNaN(idx)) {
77 graph.linkStyles.set(idx, { ...graph.linkStyles.get(idx), ...props });
78 }
79 }
80 }
81 continue;
82 }
83 const dirMatch = line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i);
84 if (dirMatch && subgraphStack.length > 0) {
85 subgraphStack[subgraphStack.length - 1].direction = dirMatch[1].toUpperCase();
86 continue;
87 }
88 const subgraphMatch = line.match(/^subgraph\s+(.+)$/);
89 if (subgraphMatch) {
90 const rest = subgraphMatch[1].trim();
91 const bracketMatch = rest.match(/^([\w-]+)\s*\[(.+)\]$/);
92 let id;
93 let label;
94 if (bracketMatch) {
95 id = bracketMatch[1];
96 label = normalizeBrTags(bracketMatch[2]);
97 } else {
98 label = normalizeBrTags(rest);
99 id = rest.replace(/\s+/g, "_").replace(/[^\w]/g, "");
100 }
101 const sg = { id, label, nodeIds: [], children: [] };
102 subgraphStack.push(sg);
103 continue;
104 }
105 if (line === "end") {
106 const completed = subgraphStack.pop();
107 if (completed) {
108 if (subgraphStack.length > 0) {
109 subgraphStack[subgraphStack.length - 1].children.push(completed);
110 } else {
111 graph.subgraphs.push(completed);
112 }
113 }
114 continue;
115 }
116 parseEdgeLine(line, graph, subgraphStack);
117 }
118 return graph;
119}
120function parseStateDiagram(lines) {
121 const graph = {
122 direction: "TD",
123 nodes: /* @__PURE__ */ new Map(),
124 edges: [],
125 subgraphs: [],
126 classDefs: /* @__PURE__ */ new Map(),
127 classAssignments: /* @__PURE__ */ new Map(),
128 nodeStyles: /* @__PURE__ */ new Map(),
129 linkStyles: /* @__PURE__ */ new Map()
130 };
131 const compositeStack = [];
132 const compositeStateIds = /* @__PURE__ */ new Set();
133 let startCount = 0;
134 let endCount = 0;
135 for (let i = 1; i < lines.length; i++) {
136 const line = lines[i];
137 const dirMatch = line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i);
138 if (dirMatch) {
139 if (compositeStack.length > 0) {
140 compositeStack[compositeStack.length - 1].direction = dirMatch[1].toUpperCase();
141 } else {
142 graph.direction = dirMatch[1].toUpperCase();
143 }
144 continue;
145 }
146 const linkStyleMatch = line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/);
147 if (linkStyleMatch) {
148 const target = linkStyleMatch[1].trim();
149 const props = parseStyleProps(linkStyleMatch[2]);
150 if (target === "default") {
151 graph.linkStyles.set("default", { ...graph.linkStyles.get("default"), ...props });
152 } else {
153 const indices = target.split(",").map((s) => parseInt(s.trim(), 10));
154 for (const idx of indices) {
155 if (!isNaN(idx)) {
156 graph.linkStyles.set(idx, { ...graph.linkStyles.get(idx), ...props });
157 }
158 }
159 }
160 continue;
161 }
162 const compositeMatch = line.match(/^state\s+(?:"([^"]+)"\s+as\s+)?([\w\p{L}]+)\s*\{$/u);
163 if (compositeMatch) {
164 const label = compositeMatch[1] ?? compositeMatch[2];
165 const id = compositeMatch[2];
166 const sg = { id, label, nodeIds: [], children: [] };
167 compositeStack.push(sg);
168 compositeStateIds.add(id);
169 graph.nodes.delete(id);
170 continue;
171 }
172 if (line === "}") {
173 const completed = compositeStack.pop();
174 if (completed) {
175 if (compositeStack.length > 0) {
176 compositeStack[compositeStack.length - 1].children.push(completed);
177 } else {
178 graph.subgraphs.push(completed);
179 }
180 }
181 continue;
182 }
183 const stateAliasMatch = line.match(/^state\s+"([^"]+)"\s+as\s+([\w\p{L}]+)\s*$/u);
184 if (stateAliasMatch) {
185 const label = normalizeBrTags(stateAliasMatch[1]);
186 const id = stateAliasMatch[2];
187 registerStateNode(graph, compositeStack, { id, label, shape: "rounded" });
188 continue;
189 }
190 const transitionMatch = line.match(/^(\[\*\]|[\w\p{L}-]+)\s*(-->)\s*(\[\*\]|[\w\p{L}-]+)(?:\s*:\s*(.+))?$/u);
191 if (transitionMatch) {
192 let sourceId = transitionMatch[1];
193 let targetId = transitionMatch[3];
194 const rawTransitionLabel = transitionMatch[4]?.trim();
195 const edgeLabel = rawTransitionLabel ? normalizeBrTags(rawTransitionLabel) : void 0;
196 if (sourceId === "[*]") {
197 startCount++;
198 sourceId = `_start${startCount > 1 ? startCount : ""}`;
199 registerStateNode(graph, compositeStack, { id: sourceId, label: "", shape: "state-start" });
200 } else if (!compositeStateIds.has(sourceId)) {
201 ensureStateNode(graph, compositeStack, sourceId);
202 }
203 if (targetId === "[*]") {
204 endCount++;
205 targetId = `_end${endCount > 1 ? endCount : ""}`;
206 registerStateNode(graph, compositeStack, { id: targetId, label: "", shape: "state-end" });
207 } else if (!compositeStateIds.has(targetId)) {
208 ensureStateNode(graph, compositeStack, targetId);
209 }
210 graph.edges.push({
211 source: sourceId,
212 target: targetId,
213 label: edgeLabel,
214 style: "solid",
215 hasArrowStart: false,
216 hasArrowEnd: true
217 });
218 continue;
219 }
220 const stateDescMatch = line.match(/^([\w\p{L}-]+)\s*:\s*(.+)$/u);
221 if (stateDescMatch) {
222 const id = stateDescMatch[1];
223 const label = normalizeBrTags(stateDescMatch[2].trim());
224 registerStateNode(graph, compositeStack, { id, label, shape: "rounded" });
225 continue;
226 }
227 }
228 return graph;
229}
230function registerStateNode(graph, compositeStack, node) {
231 const isNew = !graph.nodes.has(node.id);
232 if (isNew) {
233 graph.nodes.set(node.id, node);
234 }
235 if (compositeStack.length > 0) {
236 const current = compositeStack[compositeStack.length - 1];
237 if (!current.nodeIds.includes(node.id)) {
238 current.nodeIds.push(node.id);
239 }
240 }
241}
242function ensureStateNode(graph, compositeStack, id) {
243 if (!graph.nodes.has(id)) {
244 registerStateNode(graph, compositeStack, { id, label: id, shape: "rounded" });
245 } else {
246 if (compositeStack.length > 0) {
247 const current = compositeStack[compositeStack.length - 1];
248 if (!current.nodeIds.includes(id)) {
249 current.nodeIds.push(id);
250 }
251 }
252 }
253}
254function parseStyleProps(propsStr) {
255 const cleaned = propsStr.replace(/;\s*$/, "");
256 const props = {};
257 for (const pair of cleaned.split(",")) {
258 const colonIdx = pair.indexOf(":");
259 if (colonIdx > 0) {
260 const key = pair.slice(0, colonIdx).trim();
261 const val = pair.slice(colonIdx + 1).trim();
262 if (key && val) {
263 props[key] = val;
264 }
265 }
266 }
267 return props;
268}
269var ARROW_REGEX = /^(<)?(-->|-.->|==>|---|-\.-|===)(?:\|([^|]*)\|)?/;
270var TEXT_ARROW_REGEX = /^(<)?(--|-\.|==)\s+(.+?)\s+(-->|---|\.\->|-\.\-|==>|===)/;
271var NODE_PATTERNS = [
272 // Triple delimiters (must be first)
273 { regex: /^([\w-]+)\(\(\((.+?)\)\)\)/, shape: "doublecircle" },
274 // A(((text)))
275 // Double delimiters with mixed brackets
276 { regex: /^([\w-]+)\(\[(.+?)\]\)/, shape: "stadium" },
277 // A([text])
278 { regex: /^([\w-]+)\(\((.+?)\)\)/, shape: "circle" },
279 // A((text))
280 { regex: /^([\w-]+)\[\[(.+?)\]\]/, shape: "subroutine" },
281 // A[[text]]
282 { regex: /^([\w-]+)\[\((.+?)\)\]/, shape: "cylinder" },
283 // A[(text)]
284 // Trapezoid variants — must come before plain [text]
285 { regex: /^([\w-]+)\[\/(.+?)\\\]/, shape: "trapezoid" },
286 // A[/text\]
287 { regex: /^([\w-]+)\[\\(.+?)\/\]/, shape: "trapezoid-alt" },
288 // A[\text/]
289 // Asymmetric flag shape
290 { regex: /^([\w-]+)>(.+?)\]/, shape: "asymmetric" },
291 // A>text]
292 // Double curly braces (hexagon) — must come before single {text}
293 { regex: /^([\w-]+)\{\{(.+?)\}\}/, shape: "hexagon" },
294 // A{{text}}
295 // Single-char delimiters (last — most common, least specific)
296 { regex: /^([\w-]+)\[(.+?)\]/, shape: "rectangle" },
297 // A[text]
298 { regex: /^([\w-]+)\((.+?)\)/, shape: "rounded" },
299 // A(text)
300 { regex: /^([\w-]+)\{(.+?)\}/, shape: "diamond" }
301 // A{text}
302];
303var BARE_NODE_REGEX = /^([\w-]+)/;
304var CLASS_SHORTHAND_REGEX = /^:::([\w][\w-]*)/;
305function parseEdgeLine(line, graph, subgraphStack) {
306 let remaining = line.trim();
307 const firstGroup = consumeNodeGroup(remaining, graph, subgraphStack);
308 if (!firstGroup || firstGroup.ids.length === 0) return;
309 remaining = firstGroup.remaining.trim();
310 let prevGroupIds = firstGroup.ids;
311 while (remaining.length > 0) {
312 let hasArrowStart;
313 let style;
314 let hasArrowEnd;
315 let edgeLabel;
316 const arrowMatch = remaining.match(ARROW_REGEX);
317 if (arrowMatch) {
318 hasArrowStart = Boolean(arrowMatch[1]);
319 const arrowOp = arrowMatch[2];
320 const rawEdgeLabel = arrowMatch[3]?.trim();
321 edgeLabel = rawEdgeLabel ? normalizeBrTags(rawEdgeLabel) : void 0;
322 remaining = remaining.slice(arrowMatch[0].length).trim();
323 style = arrowStyleFromOp(arrowOp);
324 hasArrowEnd = arrowOp.endsWith(">");
325 } else {
326 const textMatch = remaining.match(TEXT_ARROW_REGEX);
327 if (!textMatch) break;
328 hasArrowStart = Boolean(textMatch[1]);
329 const rawLabel = textMatch[3].trim();
330 edgeLabel = rawLabel ? normalizeBrTags(rawLabel) : void 0;
331 const openOp = textMatch[2];
332 const closeOp = textMatch[4];
333 remaining = remaining.slice(textMatch[0].length).trim();
334 style = textArrowStyleFromOps(openOp, closeOp);
335 hasArrowEnd = closeOp.endsWith(">");
336 }
337 const nextGroup = consumeNodeGroup(remaining, graph, subgraphStack);
338 if (!nextGroup || nextGroup.ids.length === 0) break;
339 remaining = nextGroup.remaining.trim();
340 for (const sourceId of prevGroupIds) {
341 for (const targetId of nextGroup.ids) {
342 graph.edges.push({
343 source: sourceId,
344 target: targetId,
345 label: edgeLabel,
346 style,
347 hasArrowStart,
348 hasArrowEnd
349 });
350 }
351 }
352 prevGroupIds = nextGroup.ids;
353 }
354}
355function consumeNodeGroup(text, graph, subgraphStack) {
356 const first = consumeNode(text, graph, subgraphStack);
357 if (!first) return null;
358 const ids = [first.id];
359 let remaining = first.remaining.trim();
360 while (remaining.startsWith("&")) {
361 remaining = remaining.slice(1).trim();
362 const next = consumeNode(remaining, graph, subgraphStack);
363 if (!next) break;
364 ids.push(next.id);
365 remaining = next.remaining.trim();
366 }
367 return { ids, remaining };
368}
369function consumeNode(text, graph, subgraphStack) {
370 let id = null;
371 let remaining = text;
372 for (const { regex, shape } of NODE_PATTERNS) {
373 const match = text.match(regex);
374 if (match) {
375 id = match[1];
376 const label = normalizeBrTags(match[2]);
377 registerNode(graph, subgraphStack, { id, label, shape });
378 remaining = text.slice(match[0].length);
379 break;
380 }
381 }
382 if (id === null) {
383 const bareMatch = text.match(BARE_NODE_REGEX);
384 if (bareMatch) {
385 id = bareMatch[1];
386 if (!graph.nodes.has(id)) {
387 registerNode(graph, subgraphStack, { id, label: id, shape: "rectangle" });
388 }
389 remaining = text.slice(bareMatch[0].length);
390 }
391 }
392 if (id === null) return null;
393 const classMatch = remaining.match(CLASS_SHORTHAND_REGEX);
394 if (classMatch) {
395 graph.classAssignments.set(id, classMatch[1]);
396 remaining = remaining.slice(classMatch[0].length);
397 }
398 return { id, remaining };
399}
400function registerNode(graph, subgraphStack, node) {
401 const isNew = !graph.nodes.has(node.id);
402 if (isNew) {
403 graph.nodes.set(node.id, node);
404 }
405 trackInSubgraph(subgraphStack, node.id);
406}
407function trackInSubgraph(subgraphStack, nodeId) {
408 if (subgraphStack.length > 0) {
409 const current = subgraphStack[subgraphStack.length - 1];
410 if (!current.nodeIds.includes(nodeId)) {
411 current.nodeIds.push(nodeId);
412 }
413 }
414}
415function arrowStyleFromOp(op) {
416 if (op === "-.->") return "dotted";
417 if (op === "-.-") return "dotted";
418 if (op === "==>") return "thick";
419 if (op === "===") return "thick";
420 return "solid";
421}
422function textArrowStyleFromOps(openOp, closeOp) {
423 if (openOp === "-." || closeOp === ".->" || closeOp === "-.-") return "dotted";
424 if (openOp === "==" || closeOp === "==>" || closeOp === "===") return "thick";
425 return "solid";
426}
427
428// node_modules/beautiful-mermaid/src/ascii/types.ts
429var Up = { x: 1, y: 0 };
430var Down = { x: 1, y: 2 };
431var Left = { x: 0, y: 1 };
432var Right = { x: 2, y: 1 };
433var UpperRight = { x: 2, y: 0 };
434var UpperLeft = { x: 0, y: 0 };
435var LowerRight = { x: 2, y: 2 };
436var LowerLeft = { x: 0, y: 2 };
437var Middle = { x: 1, y: 1 };
438function gridCoordEquals(a, b) {
439 return a.x === b.x && a.y === b.y;
440}
441function drawingCoordEquals(a, b) {
442 return a.x === b.x && a.y === b.y;
443}
444function gridCoordDirection(c, dir) {
445 return { x: c.x + dir.x, y: c.y + dir.y };
446}
447function gridKey(c) {
448 return `${c.x},${c.y}`;
449}
450var EMPTY_STYLE = { name: "", styles: {} };
451
452// node_modules/beautiful-mermaid/src/theme.ts
453var MIX = {
454 /** Primary text: near-full fg */
455 text: 100,
456 // just use --fg directly
457 /** Secondary text (group headers): fg mixed at 60% */
458 textSec: 60,
459 /** Muted text (edge labels, notes): fg mixed at 40% */
460 textMuted: 40,
461 /** Faint text (de-emphasized): fg mixed at 25% */
462 textFaint: 25,
463 /** Edge/connector lines: fg mixed at 50% for clear visibility */
464 line: 50,
465 /** Arrow head fill: fg mixed at 85% for clear visibility */
466 arrow: 85,
467 /** Node fill tint: fg mixed at 3% */
468 nodeFill: 3,
469 /** Node/group stroke: fg mixed at 20% */
470 nodeStroke: 20,
471 /** Group header band tint: fg mixed at 5% */
472 groupHeader: 5,
473 /** Inner divider strokes: fg mixed at 12% */
474 innerStroke: 12,
475 /** Key badge background opacity (ER diagrams) */
476 keyBadge: 10
477};
478
479// node_modules/beautiful-mermaid/src/ascii/ansi.ts
480var DEFAULT_ASCII_THEME = {
481 fg: "#27272a",
482 // zinc-800 — primary text
483 border: "#a1a1aa",
484 // zinc-400 — node borders (12% mix)
485 line: "#71717a",
486 // zinc-500 — edge lines (35% mix)
487 arrow: "#52525b",
488 // zinc-600 — arrowheads (60% mix)
489 corner: "#71717a",
490 // same as line
491 junction: "#a1a1aa"
492 // same as border
493};
494function mixColors(fg, bg, pct) {
495 const f = parseHex(fg), b = parseHex(bg);
496 const mix = (a, z) => Math.round(a * (pct / 100) + z * (1 - pct / 100));
497 const r = mix(f.r, b.r), g = mix(f.g, b.g), bl = mix(f.b, b.b);
498 return "#" + [r, g, bl].map((c) => c.toString(16).padStart(2, "0")).join("");
499}
500function diagramColorsToAsciiTheme(colors) {
501 const line = colors.line ?? mixColors(colors.fg, colors.bg, MIX.line);
502 const border = colors.border ?? mixColors(colors.fg, colors.bg, MIX.nodeStroke);
503 return {
504 fg: colors.fg,
505 border,
506 line,
507 arrow: colors.accent ?? mixColors(colors.fg, colors.bg, MIX.arrow),
508 accent: colors.accent,
509 bg: colors.bg,
510 corner: line,
511 junction: border
512 };
513}
514function detectColorMode() {
515 const proc = globalThis.process;
516 if (proc) {
517 if (!proc.stdout?.isTTY) {
518 return "none";
519 }
520 const colorTerm = proc.env?.COLORTERM?.toLowerCase() ?? "";
521 const term = proc.env?.TERM?.toLowerCase() ?? "";
522 if (colorTerm === "truecolor" || colorTerm === "24bit") {
523 return "truecolor";
524 }
525 if (term.includes("256color") || term.includes("256")) {
526 return "ansi256";
527 }
528 if (term && term !== "dumb") {
529 return "ansi16";
530 }
531 return "none";
532 }
533 if (typeof document !== "undefined") {
534 return "html";
535 }
536 return "none";
537}
538function parseHex(hex) {
539 const h = hex.replace("#", "");
540 if (h.length === 3) {
541 return {
542 r: parseInt(h[0] + h[0], 16),
543 g: parseInt(h[1] + h[1], 16),
544 b: parseInt(h[2] + h[2], 16)
545 };
546 }
547 return {
548 r: parseInt(h.substring(0, 2), 16),
549 g: parseInt(h.substring(2, 4), 16),
550 b: parseInt(h.substring(4, 6), 16)
551 };
552}
553var ESC = "\x1B[";
554var RESET = `${ESC}0m`;
555function truecolorFg(hex) {
556 const { r, g, b } = parseHex(hex);
557 return `${ESC}38;2;${r};${g};${b}m`;
558}
559function rgbTo256(r, g, b) {
560 const avg = (r + g + b) / 3;
561 const maxDiff = Math.max(Math.abs(r - avg), Math.abs(g - avg), Math.abs(b - avg));
562 if (maxDiff < 10) {
563 const gray = Math.round(avg / 255 * 23);
564 return 232 + Math.min(23, Math.max(0, gray));
565 }
566 const toIndex = (v) => {
567 if (v < 48) return 0;
568 if (v < 115) return 1;
569 return Math.min(5, Math.floor((v - 35) / 40));
570 };
571 const ri = toIndex(r);
572 const gi = toIndex(g);
573 const bi = toIndex(b);
574 return 16 + 36 * ri + 6 * gi + bi;
575}
576function ansi256Fg(hex) {
577 const { r, g, b } = parseHex(hex);
578 const index = rgbTo256(r, g, b);
579 return `${ESC}38;5;${index}m`;
580}
581function ansi16Fg(hex) {
582 const { r, g, b } = parseHex(hex);
583 const luma = 0.299 * r + 0.587 * g + 0.114 * b;
584 const bright = luma > 100 ? 0 : 60;
585 let code;
586 if (r > 180 && g < 100 && b < 100) code = 31;
587 else if (g > 180 && r < 100 && b < 100) code = 32;
588 else if (r > 150 && g > 150 && b < 100) code = 33;
589 else if (b > 180 && r < 100 && g < 100) code = 34;
590 else if (r > 150 && b > 150 && g < 100) code = 35;
591 else if (g > 150 && b > 150 && r < 100) code = 36;
592 else if (luma > 200) code = 37;
593 else if (luma < 50) code = 30;
594 else code = 37;
595 return `${ESC}${code + bright}m`;
596}
597function escapeHtml(text) {
598 return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
599}
600function htmlSpan(hex, text) {
601 return `<span style="color:${hex}">${escapeHtml(text)}</span>`;
602}
603function getRoleColor(role, theme) {
604 switch (role) {
605 case "text":
606 return theme.fg;
607 case "border":
608 return theme.border;
609 case "line":
610 return theme.line;
611 case "arrow":
612 return theme.arrow;
613 case "corner":
614 return theme.corner ?? theme.line;
615 case "junction":
616 return theme.junction ?? theme.border;
617 default:
618 return theme.fg;
619 }
620}
621function getAnsiColor(role, theme, mode) {
622 if (mode === "none") return "";
623 const hex = getRoleColor(role, theme);
624 switch (mode) {
625 case "truecolor":
626 return truecolorFg(hex);
627 case "ansi256":
628 return ansi256Fg(hex);
629 case "ansi16":
630 return ansi16Fg(hex);
631 default:
632 return "";
633 }
634}
635function colorizeLine(chars, roles, theme, mode) {
636 if (mode === "none") {
637 return chars.join("");
638 }
639 if (mode === "html") {
640 return colorizeLineHtml(chars, roles, theme);
641 }
642 let result = "";
643 let currentRole = null;
644 let buffer = "";
645 for (let i = 0; i < chars.length; i++) {
646 const char = chars[i];
647 const role = roles[i] ?? null;
648 if (char === " ") {
649 if (buffer.length > 0) {
650 if (currentRole !== null) {
651 result += getAnsiColor(currentRole, theme, mode) + buffer + RESET;
652 } else {
653 result += buffer;
654 }
655 buffer = "";
656 currentRole = null;
657 }
658 result += char;
659 continue;
660 }
661 if (role === currentRole) {
662 buffer += char;
663 continue;
664 }
665 if (buffer.length > 0) {
666 if (currentRole !== null) {
667 result += getAnsiColor(currentRole, theme, mode) + buffer + RESET;
668 } else {
669 result += buffer;
670 }
671 }
672 buffer = char;
673 currentRole = role;
674 }
675 if (buffer.length > 0 && currentRole !== null) {
676 result += getAnsiColor(currentRole, theme, mode) + buffer + RESET;
677 } else if (buffer.length > 0) {
678 result += buffer;
679 }
680 return result;
681}
682function colorizeLineHtml(chars, roles, theme) {
683 let result = "";
684 let currentRole = null;
685 let buffer = "";
686 const flush = () => {
687 if (buffer.length === 0) return;
688 if (currentRole !== null) {
689 result += htmlSpan(getRoleColor(currentRole, theme), buffer);
690 } else {
691 result += escapeHtml(buffer);
692 }
693 buffer = "";
694 currentRole = null;
695 };
696 for (let i = 0; i < chars.length; i++) {
697 const char = chars[i];
698 const role = roles[i] ?? null;
699 if (char === " ") {
700 flush();
701 result += " ";
702 continue;
703 }
704 if (role === currentRole) {
705 buffer += char;
706 continue;
707 }
708 flush();
709 buffer = char;
710 currentRole = role;
711 }
712 flush();
713 return result;
714}
715function colorizeText(text, hex, mode) {
716 if (mode === "none" || text.length === 0) return text;
717 if (mode === "html") return htmlSpan(hex, text);
718 let code;
719 switch (mode) {
720 case "truecolor":
721 code = truecolorFg(hex);
722 break;
723 case "ansi256":
724 code = ansi256Fg(hex);
725 break;
726 case "ansi16":
727 code = ansi16Fg(hex);
728 break;
729 default:
730 return text;
731 }
732 return `${code}${text}${RESET}`;
733}
734
735// node_modules/beautiful-mermaid/src/ascii/canvas.ts
736function mkCanvas(x, y) {
737 const canvas = [];
738 for (let i = 0; i <= x; i++) {
739 const col = [];
740 for (let j = 0; j <= y; j++) {
741 col.push(" ");
742 }
743 canvas.push(col);
744 }
745 return canvas;
746}
747function copyCanvas(source) {
748 const [maxX, maxY] = getCanvasSize(source);
749 return mkCanvas(maxX, maxY);
750}
751function mkRoleCanvas(x, y) {
752 const roleCanvas = [];
753 for (let i = 0; i <= x; i++) {
754 const col = [];
755 for (let j = 0; j <= y; j++) {
756 col.push(null);
757 }
758 roleCanvas.push(col);
759 }
760 return roleCanvas;
761}
762function increaseRoleCanvasSize(roleCanvas, newX, newY) {
763 const currX = roleCanvas.length - 1;
764 const currY = (roleCanvas[0]?.length ?? 1) - 1;
765 const targetX = Math.max(newX, currX);
766 const targetY = Math.max(newY, currY);
767 const grown = mkRoleCanvas(targetX, targetY);
768 for (let x = 0; x < grown.length; x++) {
769 for (let y = 0; y < grown[0].length; y++) {
770 if (x < roleCanvas.length && y < roleCanvas[0].length) {
771 grown[x][y] = roleCanvas[x][y];
772 }
773 }
774 }
775 roleCanvas.length = 0;
776 roleCanvas.push(...grown);
777 return roleCanvas;
778}
779function setRole(roleCanvas, x, y, role) {
780 if (x >= roleCanvas.length || y >= (roleCanvas[0]?.length ?? 0)) {
781 increaseRoleCanvasSize(roleCanvas, x, y);
782 }
783 roleCanvas[x][y] = role;
784}
785function getCanvasSize(canvas) {
786 return [canvas.length - 1, (canvas[0]?.length ?? 1) - 1];
787}
788function increaseSize(canvas, newX, newY) {
789 const [currX, currY] = getCanvasSize(canvas);
790 const targetX = Math.max(newX, currX);
791 const targetY = Math.max(newY, currY);
792 const grown = mkCanvas(targetX, targetY);
793 for (let x = 0; x < grown.length; x++) {
794 for (let y = 0; y < grown[0].length; y++) {
795 if (x < canvas.length && y < canvas[0].length) {
796 grown[x][y] = canvas[x][y];
797 }
798 }
799 }
800 canvas.length = 0;
801 canvas.push(...grown);
802 return canvas;
803}
804var JUNCTION_CHARS = /* @__PURE__ */ new Set([
805 "\u2500",
806 "\u2502",
807 "\u250C",
808 "\u2510",
809 "\u2514",
810 "\u2518",
811 "\u251C",
812 "\u2524",
813 "\u252C",
814 "\u2534",
815 "\u253C",
816 "\u2574",
817 "\u2575",
818 "\u2576",
819 "\u2577"
820]);
821function isJunctionChar(c) {
822 return JUNCTION_CHARS.has(c);
823}
824function isAlphanumeric(c) {
825 return /^[a-zA-Z0-9]$/.test(c);
826}
827var JUNCTION_MAP = {
828 "\u2500": { "\u2502": "\u253C", "\u250C": "\u252C", "\u2510": "\u252C", "\u2514": "\u2534", "\u2518": "\u2534", "\u251C": "\u253C", "\u2524": "\u253C", "\u252C": "\u252C", "\u2534": "\u2534" },
829 "\u2502": { "\u2500": "\u253C", "\u250C": "\u251C", "\u2510": "\u2524", "\u2514": "\u251C", "\u2518": "\u2524", "\u251C": "\u251C", "\u2524": "\u2524", "\u252C": "\u253C", "\u2534": "\u253C" },
830 "\u250C": { "\u2500": "\u252C", "\u2502": "\u251C", "\u2510": "\u252C", "\u2514": "\u251C", "\u2518": "\u253C", "\u251C": "\u251C", "\u2524": "\u253C", "\u252C": "\u252C", "\u2534": "\u253C" },
831 "\u2510": { "\u2500": "\u252C", "\u2502": "\u2524", "\u250C": "\u252C", "\u2514": "\u253C", "\u2518": "\u2524", "\u251C": "\u253C", "\u2524": "\u2524", "\u252C": "\u252C", "\u2534": "\u253C" },
832 "\u2514": { "\u2500": "\u2534", "\u2502": "\u251C", "\u250C": "\u251C", "\u2510": "\u253C", "\u2518": "\u2534", "\u251C": "\u251C", "\u2524": "\u253C", "\u252C": "\u253C", "\u2534": "\u2534" },
833 "\u2518": { "\u2500": "\u2534", "\u2502": "\u2524", "\u250C": "\u253C", "\u2510": "\u2524", "\u2514": "\u2534", "\u251C": "\u253C", "\u2524": "\u2524", "\u252C": "\u253C", "\u2534": "\u2534" },
834 "\u251C": { "\u2500": "\u253C", "\u2502": "\u251C", "\u250C": "\u251C", "\u2510": "\u253C", "\u2514": "\u251C", "\u2518": "\u253C", "\u2524": "\u253C", "\u252C": "\u253C", "\u2534": "\u253C" },
835 "\u2524": { "\u2500": "\u253C", "\u2502": "\u2524", "\u250C": "\u253C", "\u2510": "\u2524", "\u2514": "\u253C", "\u2518": "\u2524", "\u251C": "\u253C", "\u252C": "\u253C", "\u2534": "\u253C" },
836 "\u252C": { "\u2500": "\u252C", "\u2502": "\u253C", "\u250C": "\u252C", "\u2510": "\u252C", "\u2514": "\u253C", "\u2518": "\u253C", "\u251C": "\u253C", "\u2524": "\u253C", "\u2534": "\u253C" },
837 "\u2534": { "\u2500": "\u2534", "\u2502": "\u253C", "\u250C": "\u253C", "\u2510": "\u253C", "\u2514": "\u2534", "\u2518": "\u2534", "\u251C": "\u253C", "\u2524": "\u253C", "\u252C": "\u253C" }
838};
839function mergeJunctions(c1, c2) {
840 return JUNCTION_MAP[c1]?.[c2] ?? c1;
841}
842function mergeCanvases(base, offset, useAscii, ...overlays) {
843 let [maxX, maxY] = getCanvasSize(base);
844 for (const overlay of overlays) {
845 const [oX, oY] = getCanvasSize(overlay);
846 maxX = Math.max(maxX, oX + offset.x);
847 maxY = Math.max(maxY, oY + offset.y);
848 }
849 const merged = mkCanvas(maxX, maxY);
850 for (let x = 0; x <= maxX; x++) {
851 for (let y = 0; y <= maxY; y++) {
852 if (x < base.length && y < base[0].length) {
853 merged[x][y] = base[x][y];
854 }
855 }
856 }
857 for (const overlay of overlays) {
858 for (let x = 0; x < overlay.length; x++) {
859 for (let y = 0; y < overlay[0].length; y++) {
860 const c = overlay[x][y];
861 if (c !== " ") {
862 const mx = x + offset.x;
863 const my = y + offset.y;
864 const current = merged[mx][my];
865 if (!useAscii && isJunctionChar(c) && isJunctionChar(current)) {
866 merged[mx][my] = mergeJunctions(current, c);
867 } else if (isAlphanumeric(current) && isAlphanumeric(c)) {
868 } else {
869 merged[mx][my] = c;
870 }
871 }
872 }
873 }
874 }
875 return merged;
876}
877function canvasToString(canvas, options) {
878 const [maxX, maxY] = getCanvasSize(canvas);
879 const lines = [];
880 const roleCanvas = options?.roleCanvas;
881 const colorMode = options?.colorMode ?? "none";
882 const theme = options?.theme ?? DEFAULT_ASCII_THEME;
883 for (let y = 0; y <= maxY; y++) {
884 if (colorMode === "none" || !roleCanvas) {
885 let line = "";
886 for (let x = 0; x <= maxX; x++) {
887 line += canvas[x][y];
888 }
889 lines.push(line);
890 } else {
891 const chars = [];
892 const roles = [];
893 for (let x = 0; x <= maxX; x++) {
894 chars.push(canvas[x][y]);
895 roles.push(roleCanvas[x]?.[y] ?? null);
896 }
897 lines.push(colorizeLine(chars, roles, theme, colorMode));
898 }
899 }
900 return lines.join("\n");
901}
902var VERTICAL_FLIP_MAP = {
903 // Unicode arrows
904 "\u25B2": "\u25BC",
905 "\u25BC": "\u25B2",
906 "\u25E4": "\u25E3",
907 "\u25E3": "\u25E4",
908 "\u25E5": "\u25E2",
909 "\u25E2": "\u25E5",
910 // ASCII arrows
911 "^": "v",
912 "v": "^",
913 // Unicode corners
914 "\u250C": "\u2514",
915 "\u2514": "\u250C",
916 "\u2510": "\u2518",
917 "\u2518": "\u2510",
918 // Unicode junctions (T-pieces flip vertically)
919 "\u252C": "\u2534",
920 "\u2534": "\u252C",
921 // Box-start junctions (exit points from node boxes)
922 "\u2575": "\u2577",
923 "\u2577": "\u2575"
924};
925function flipCanvasVertically(canvas) {
926 for (const col of canvas) {
927 col.reverse();
928 }
929 for (const col of canvas) {
930 for (let y = 0; y < col.length; y++) {
931 const flipped = VERTICAL_FLIP_MAP[col[y]];
932 if (flipped) col[y] = flipped;
933 }
934 }
935 return canvas;
936}
937function flipRoleCanvasVertically(roleCanvas) {
938 for (const col of roleCanvas) {
939 col.reverse();
940 }
941 return roleCanvas;
942}
943function drawText(canvas, start, text, forceOverwrite = false) {
944 increaseSize(canvas, start.x + text.length, start.y);
945 for (let i = 0; i < text.length; i++) {
946 const x = start.x + i;
947 const current = canvas[x][start.y];
948 if (forceOverwrite || current === " ") {
949 canvas[x][start.y] = text[i];
950 }
951 }
952}
953function setCanvasSizeToGrid(canvas, columnWidth, rowHeight) {
954 let maxX = 0;
955 let maxY = 0;
956 for (const w of columnWidth.values()) maxX += w;
957 for (const h of rowHeight.values()) maxY += h;
958 increaseSize(canvas, maxX - 1, maxY - 1);
959}
960function setRoleCanvasSizeToGrid(roleCanvas, columnWidth, rowHeight) {
961 let maxX = 0;
962 let maxY = 0;
963 for (const w of columnWidth.values()) maxX += w;
964 for (const h of rowHeight.values()) maxY += h;
965 increaseRoleCanvasSize(roleCanvas, maxX - 1, maxY - 1);
966}
967
968// node_modules/beautiful-mermaid/src/ascii/converter.ts
969function convertToAsciiGraph(parsed, config) {
970 const nodeMap = /* @__PURE__ */ new Map();
971 let index = 0;
972 for (const [id, mNode] of parsed.nodes) {
973 const asciiNode = {
974 // Use the parser ID as the unique identity key to avoid collisions
975 // when multiple nodes share the same label (e.g. A[Web Server], C[Web Server]).
976 name: id,
977 // The label is used for rendering inside the box.
978 displayLabel: mNode.label,
979 // Preserve shape from parser for shape-aware rendering
980 shape: mNode.shape,
981 index,
982 gridCoord: null,
983 drawingCoord: null,
984 drawing: null,
985 drawn: false,
986 styleClassName: "",
987 styleClass: EMPTY_STYLE
988 };
989 nodeMap.set(id, asciiNode);
990 index++;
991 }
992 const nodes = [...nodeMap.values()];
993 const edges = [];
994 for (const mEdge of parsed.edges) {
995 const from = nodeMap.get(mEdge.source);
996 const to = nodeMap.get(mEdge.target);
997 if (!from || !to) continue;
998 edges.push({
999 from,
1000 to,
1001 text: mEdge.label ?? "",
1002 path: [],
1003 labelLine: [],
1004 startDir: { x: 0, y: 0 },
1005 endDir: { x: 0, y: 0 },
1006 style: mEdge.style,
1007 hasArrowStart: mEdge.hasArrowStart,
1008 hasArrowEnd: mEdge.hasArrowEnd
1009 });
1010 }
1011 const subgraphs = [];
1012 for (const mSg of parsed.subgraphs) {
1013 convertSubgraph(mSg, null, nodeMap, subgraphs);
1014 }
1015 deduplicateSubgraphNodes(parsed.subgraphs, subgraphs, nodeMap, parsed);
1016 for (const [nodeId, className] of parsed.classAssignments) {
1017 const node = nodeMap.get(nodeId);
1018 const classDef = parsed.classDefs.get(className);
1019 if (node && classDef) {
1020 node.styleClassName = className;
1021 node.styleClass = { name: className, styles: classDef };
1022 }
1023 }
1024 return {
1025 nodes,
1026 edges,
1027 canvas: mkCanvas(0, 0),
1028 roleCanvas: mkRoleCanvas(0, 0),
1029 grid: /* @__PURE__ */ new Map(),
1030 columnWidth: /* @__PURE__ */ new Map(),
1031 rowHeight: /* @__PURE__ */ new Map(),
1032 subgraphs,
1033 config,
1034 offsetX: 0,
1035 offsetY: 0,
1036 bundles: []
1037 // Populated by analyzeEdgeBundles() during layout
1038 };
1039}
1040function convertSubgraph(mSg, parent, nodeMap, allSubgraphs) {
1041 let normalizedDirection;
1042 if (mSg.direction) {
1043 normalizedDirection = mSg.direction === "LR" || mSg.direction === "RL" ? "LR" : "TD";
1044 }
1045 const sg = {
1046 name: mSg.label,
1047 nodes: [],
1048 parent,
1049 children: [],
1050 minX: 0,
1051 minY: 0,
1052 maxX: 0,
1053 maxY: 0,
1054 direction: normalizedDirection
1055 };
1056 for (const nodeId of mSg.nodeIds) {
1057 const node = nodeMap.get(nodeId);
1058 if (node) sg.nodes.push(node);
1059 }
1060 allSubgraphs.push(sg);
1061 for (const childMSg of mSg.children) {
1062 const child = convertSubgraph(childMSg, sg, nodeMap, allSubgraphs);
1063 sg.children.push(child);
1064 for (const childNode of child.nodes) {
1065 if (!sg.nodes.includes(childNode)) {
1066 sg.nodes.push(childNode);
1067 }
1068 }
1069 }
1070 return sg;
1071}
1072function deduplicateSubgraphNodes(mermaidSubgraphs, asciiSubgraphs, nodeMap, parsed) {
1073 const sgMap = /* @__PURE__ */ new Map();
1074 buildSgMap(mermaidSubgraphs, asciiSubgraphs, sgMap);
1075 const nodeOwner = /* @__PURE__ */ new Map();
1076 function claimNodes(mSg) {
1077 const asciiSg = sgMap.get(mSg);
1078 if (!asciiSg) return;
1079 for (const child of mSg.children) {
1080 claimNodes(child);
1081 }
1082 for (const nodeId of mSg.nodeIds) {
1083 if (!nodeOwner.has(nodeId)) {
1084 nodeOwner.set(nodeId, asciiSg);
1085 }
1086 }
1087 }
1088 for (const mSg of mermaidSubgraphs) {
1089 claimNodes(mSg);
1090 }
1091 for (const asciiSg of asciiSubgraphs) {
1092 asciiSg.nodes = asciiSg.nodes.filter((node) => {
1093 let nodeId;
1094 for (const [id, n] of nodeMap) {
1095 if (n === node) {
1096 nodeId = id;
1097 break;
1098 }
1099 }
1100 if (!nodeId) return false;
1101 const owner = nodeOwner.get(nodeId);
1102 if (!owner) return true;
1103 return isAncestorOrSelf(asciiSg, owner);
1104 });
1105 }
1106}
1107function isAncestorOrSelf(candidate, target) {
1108 let current = target;
1109 while (current !== null) {
1110 if (current === candidate) return true;
1111 current = current.parent;
1112 }
1113 return false;
1114}
1115function buildSgMap(mSgs, aSgs, result) {
1116 const flatMermaid = [];
1117 function flatten(sgs) {
1118 for (const sg of sgs) {
1119 flatMermaid.push(sg);
1120 flatten(sg.children);
1121 }
1122 }
1123 flatten(mSgs);
1124 for (let i = 0; i < flatMermaid.length && i < aSgs.length; i++) {
1125 result.set(flatMermaid[i], aSgs[i]);
1126 }
1127}
1128
1129// node_modules/beautiful-mermaid/src/ascii/pathfinder.ts
1130var MinHeap = class {
1131 items = [];
1132 get length() {
1133 return this.items.length;
1134 }
1135 push(item) {
1136 this.items.push(item);
1137 this.bubbleUp(this.items.length - 1);
1138 }
1139 pop() {
1140 if (this.items.length === 0) return void 0;
1141 const top = this.items[0];
1142 const last = this.items.pop();
1143 if (this.items.length > 0) {
1144 this.items[0] = last;
1145 this.sinkDown(0);
1146 }
1147 return top;
1148 }
1149 bubbleUp(i) {
1150 while (i > 0) {
1151 const parent = i - 1 >> 1;
1152 if (this.items[i].priority < this.items[parent].priority) {
1153 ;
1154 [this.items[i], this.items[parent]] = [this.items[parent], this.items[i]];
1155 i = parent;
1156 } else {
1157 break;
1158 }
1159 }
1160 }
1161 sinkDown(i) {
1162 const n = this.items.length;
1163 while (true) {
1164 let smallest = i;
1165 const left = 2 * i + 1;
1166 const right = 2 * i + 2;
1167 if (left < n && this.items[left].priority < this.items[smallest].priority) {
1168 smallest = left;
1169 }
1170 if (right < n && this.items[right].priority < this.items[smallest].priority) {
1171 smallest = right;
1172 }
1173 if (smallest !== i) {
1174 ;
1175 [this.items[i], this.items[smallest]] = [this.items[smallest], this.items[i]];
1176 i = smallest;
1177 } else {
1178 break;
1179 }
1180 }
1181 }
1182};
1183function heuristic(a, b) {
1184 const absX = Math.abs(a.x - b.x);
1185 const absY = Math.abs(a.y - b.y);
1186 if (absX === 0 || absY === 0) {
1187 return absX + absY;
1188 }
1189 return absX + absY + 1;
1190}
1191var MOVE_DIRS = [
1192 { x: 1, y: 0 },
1193 { x: -1, y: 0 },
1194 { x: 0, y: 1 },
1195 { x: 0, y: -1 }
1196];
1197function isFreeInGrid(grid, c) {
1198 if (c.x < 0 || c.y < 0) return false;
1199 return !grid.has(gridKey(c));
1200}