Draws mermaid code blocks in Claude's replies as text diagrams in the terminal, with the source folded below

A Claude Code mod that draws the `mermaid code blocks in Claude's replies as text diagrams, right in the terminal. The mermaid source is folded under each diagram, one click away.
Claude replies with:
flowchart LR A[Create order] --> B{Paid?} B -->|yes| C[Ship] B -->|no| D[Cancel]
The transcript shows:
┌──────────────┐ ◇───────◇ ┌────────┐
│ │ │ │ │ │
│ Create order ├────►│ Paid? │ ├─yes─►│ Ship │
│ │ │ │ │ │
└──────────────┘ ◇───┬───◇ └────────┘
│
│
│
│
│
│ ┌────────┐
│ │ │
└────no─────►│ Cancel │
│ │
└────────┘
▸ show mermaid source
ER diagrams keep their columns, keys and cardinality:
erDiagram USER ||--o{ ORDER : places USER { bigint id PK string email } ORDER { bigint id PK bigint user_id FK datetime created_at }
┌─────────────────┐ ┌────────────────────────┐
│ USER │ │ ORDER │
├─────────────────┤ ├────────────────────────┤
│ PK bigint id ││───○╟│ PK bigint id │
│ string email │places│ FK bigint user_id │
└─────────────────┘ │ datetime created_at │
└────────────────────────┘
Korean, Chinese and Japanese labels and emoji are laid out at their real terminal width (two columns), so boxes stay aligned:
┌────────┐ ┌──────┐
│ 사용자 │ │ 서버 │
└────┬───┘ └───┬──┘
│ │
│ 로그인 요청 │
│────────────────▶
│ │
│ 토큰 │
◀╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌│
(A browser font may not draw these characters at exactly two columns, so this example can look uneven on GitHub. It lines up in a terminal.)
At the prompt of a Claude Code terminal session:
/plugin install mermaid-view --marketplace jo-minjun/claude-mermaid-view
Answer y to add the marketplace, then pick a scope. From a shell:
claude plugin install mermaid-view --marketplace jo-minjun/claude-mermaid-view
Nothing to set up: mermaid blocks in replies are drawn as diagrams from then on.
▸ show mermaid source right under it, and ▾ hide mermaid source to fold it again. Each diagram in a reply has its own button. (Clicking needs a terminal layout that takes mouse input, such as Claude Code's fullscreen mode.)/mermaid-source.A block is left as its source when:
Rendering is done by beautiful-mermaid's text renderer: flowchart/graph, sequenceDiagram, classDiagram, erDiagram, stateDiagram-v2 and xychart. Other types (pie, gantt, mindmap, …) stay as source.
Known limits, from the renderer:
ordered in → ordere).A[주문] works, 주문[주문] does not); labels can be any language.The mod hooks Claude Code's ui.render for AssistantMessage rows. For each closed mermaid fence it renders the source with the bundled renderer and passes the rewritten markdown on to Claude Code's own reply renderer. The reply is drawn in pieces split after each diagram, so each diagram's toggle button sits right under it.
The renderer counts every character as one column. To lay out wide characters correctly, the mod appends a one-column filler letter (U+A4D0) after each wide character before rendering and strips it from the output.
claude plugin validate .
claude plugin test .
hooks/vendor/beautiful-mermaid.js is the renderer's src/ascii entry bundled into one ES module, because a hooks module cannot import npm packages. To rebuild it (needs npm and bun):
scripts/build-vendor.sh 1.1.3
MIT. The bundled renderer keeps its own licenses: beautiful-mermaid (MIT) and entities (BSD-2-Clause), in hooks/vendor/.
hooks/register.tsx 91 lines1import { atom, memberOf, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { rewriteMermaid } from './mermaid'
5
6const COMMAND = 'mermaid-source'
7// 답변 앞 글머리(● )와 코드 블록 들여쓰기가 차지하는 칸
8const REPLY_GUTTER_COLUMNS = 4
9// 터미널이 답변 본문을 글머리 뒤로 들이는 칸
10const REPLY_TEXT_INDENT = 2
11
12const openSources = atom({ plugin: 'mermaid-view', key: 'openSources' } as const, [])
13const showAllSources = atom({ plugin: 'mermaid-view', key: 'showAllSources' } as const, false)
14
15export const register: Register = on => {
16 on('session.start', async ($, e, next) => {
17 await $.command.register({
18 name: COMMAND,
19 description: 'Show or fold the mermaid source under every diagram',
20 })
21
22 return next(e)
23 })
24
25 on('command.run', { command: COMMAND }, async $ => {
26 const isShown = await update($, showAllSources, shown => !shown)
27
28 return { text: isShown ? 'Showing every mermaid source.' : 'Folded every mermaid source.' }
29 })
30
31 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
32 if (e.props.isSummary || !e.props.text.includes('mermaid')) {
33 return next(e)
34 }
35
36 const member = memberOf(openSources, e)
37 const open = await read($, member)
38 const isAllShown = await read($, showAllSources)
39 const columns = e.viewport?.columns
40 const { text, parts, rendered } = rewriteMermaid(e.props.text, {
41 maxColumns: columns === undefined ? undefined : columns - REPLY_GUTTER_COLUMNS,
42 isSourceOpen: index => isAllShown || open.includes(index),
43 })
44 if (rendered.length === 0) {
45 return next(e)
46 }
47 if (isAllShown) {
48 return next({ ...e, props: { ...e.props, text } })
49 }
50
51 // 버튼을 각 다이어그램 바로 밑에 두려고 조각마다 엔진의 답변 렌더러로 그린다.
52 // 글머리(●)는 첫 조각에만 남긴다.
53 const drawn = await Promise.all(
54 parts.map((part, order) =>
55 next({
56 ...e,
57 props: { ...e.props, text: part.text, isFirstOfReply: e.props.isFirstOfReply && order === 0 },
58 }),
59 ),
60 )
61
62 const { Box, Button } = $.ui.resolve(e)
63 const indent = e.surface === 'terminal' ? REPLY_TEXT_INDENT : 0
64 const toggle = (index: number) =>
65 update($, member, list =>
66 list.includes(index) ? list.filter(one => one !== index) : [...list, index],
67 )
68
69 return (
70 <Box flexDirection="column">
71 {parts.flatMap(({ diagram }, order) =>
72 diagram === undefined
73 ? [drawn[order]]
74 : [
75 drawn[order],
76 <Box paddingLeft={indent}>
77 <Button
78 key={`source-${diagram}`}
79 plain
80 dimColor
81 label={open.includes(diagram) ? '▾ hide mermaid source' : '▸ show mermaid source'}
82 onPress={() => toggle(diagram)}
83 />
84 </Box>,
85 ],
86 )}
87 </Box>
88 )
89 })
90}
91hooks/mermaid.ts 133 lines1import { renderMermaidASCII } from './vendor/beautiful-mermaid.js'
2
3export type MermaidBlock = {
4 start: number
5 end: number
6 indent: string
7 source: string
8}
9
10/** 답변을 다이어그램마다 끊은 조각. `diagram`이 있으면 그 다이어그램으로 끝난다 */
11export type ReplyPart = {
12 text: string
13 diagram?: number
14}
15
16export type RewriteResult = {
17 text: string
18 parts: ReplyPart[]
19 /** 다이어그램으로 바뀐 블록의 순번(메시지 안에서 0부터) */
20 rendered: number[]
21}
22
23const MERMAID_FENCE =
24 /^([ \t]*)(`{3,}|~{3,})[ \t]*mermaid[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*\2[ \t]*$/gm
25
26// 터미널에서 2칸을 차지하는 글자: 한글, CJK, 전각 기호, 이모지
27const WIDE_CHAR =
28 /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꥠ-가-힣豈-︰-﹏-⦆¢-₩]|[\u{1F300}-\u{1FAFF}]/gu
29
30// 렌더러는 모든 글자를 1칸으로 계산하므로, 넓은 글자 뒤에 1칸짜리 채움 글자를 붙여
31// 자리를 잡게 한 뒤 출력에서 지운다. state 다이어그램의 이름이 \p{L} 만 받아서
32// 채움 글자도 문자(Lisu, U+A4D0)여야 하고, 실제 다이어그램에 나올 일이 거의 없는 것을 고른다.
33const WIDTH_PAD = 'ꓐ'
34
35const CACHE_LIMIT = 64
36const cache = new Map<string, string | null>()
37
38export function findMermaidBlocks(text: string): MermaidBlock[] {
39 return [...text.matchAll(MERMAID_FENCE)].map(match => {
40 const indent = match[1] ?? ''
41 const source = (match[3] ?? '')
42 .split('\n')
43 .map(line => (line.startsWith(indent) ? line.slice(indent.length) : line))
44 .join('\n')
45
46 return { start: match.index, end: match.index + match[0].length, indent, source }
47 })
48}
49
50export function displayWidth(line: string): number {
51 return [...line].length + (line.match(WIDE_CHAR)?.length ?? 0)
52}
53
54export function renderDiagram(source: string): string | null {
55 const cached = cache.get(source)
56 if (cached !== undefined) {
57 return cached
58 }
59
60 const art = renderUncached(source)
61 if (cache.size >= CACHE_LIMIT) {
62 cache.delete(cache.keys().next().value!)
63 }
64 cache.set(source, art)
65
66 return art
67}
68
69function renderUncached(source: string): string | null {
70 try {
71 const padded = source.replace(WIDE_CHAR, char => char + WIDTH_PAD)
72 const art = renderMermaidASCII(padded, { colorMode: 'none' })
73 .split(WIDTH_PAD)
74 .join('')
75 .split('\n')
76 .map(line => line.trimEnd())
77 .join('\n')
78 .replace(/^\n+|\n+$/g, '')
79
80 // 문법을 못 읽으면 예외 대신 빈 출력을 내는 경우가 있다
81 return art === '' ? null : art
82 } catch {
83 return null
84 }
85}
86
87/**
88 * mermaid 블록을 텍스트 다이어그램 코드 블록으로 바꾼다. 변환에 실패했거나
89 * `maxColumns`보다 넓은 블록은 원문 그대로 둔다.
90 */
91export function rewriteMermaid(
92 text: string,
93 options: { maxColumns?: number; isSourceOpen: (index: number) => boolean },
94): RewriteResult {
95 const blocks = findMermaidBlocks(text)
96 const parts: ReplyPart[] = []
97 let cursor = 0
98
99 blocks.forEach((block, index) => {
100 const art = renderDiagram(block.source)
101 const fits =
102 art !== null &&
103 (options.maxColumns === undefined ||
104 Math.max(...art.split('\n').map(displayWidth)) <= options.maxColumns)
105 if (!fits) {
106 return
107 }
108
109 let part = text.slice(cursor, block.start) + fence(block.indent, '', art)
110 if (options.isSourceOpen(index)) {
111 part += '\n\n' + fence(block.indent, 'mermaid', block.source)
112 }
113 parts.push({ text: part, diagram: index })
114 cursor = block.end
115 })
116 parts.push({ text: text.slice(cursor) })
117
118 return {
119 text: parts.map(part => part.text).join(''),
120 // 조각 사이의 빈 줄은 조각을 따로 그릴 때의 간격과 겹치므로 덜어 낸다
121 parts: parts
122 .map((part, order) => (order === 0 ? part : { ...part, text: part.text.replace(/^\s*\n/, '') }))
123 .filter(part => part.diagram !== undefined || part.text.trim() !== ''),
124 rendered: parts.flatMap(part => (part.diagram === undefined ? [] : [part.diagram])),
125 }
126}
127
128function fence(indent: string, language: string, body: string): string {
129 const lines = body.split('\n').map(line => indent + line)
130
131 return [indent + '```' + language, ...lines, indent + '```'].join('\n')
132}
133hooks/vendor/beautiful-mermaid.js 5809 lines1// beautiful-mermaid 1.1.3 (src/ascii) bundled with entities 7.0.1 by scripts/build-vendor.sh. Licenses: LICENSE-beautiful-mermaid, LICENSE-entities.
2// node_modules/beautiful-mermaid/src/text-metrics.ts
3var NARROW_CHARS = new Set(["i", "l", "t", "f", "j", "I", "1", "!", "|", ".", ",", ":", ";", "'"]);
4var WIDE_CHARS = new Set(["W", "M", "w", "m", "@", "%"]);
5var VERY_WIDE_CHARS = new Set(["W", "M"]);
6var SEMI_NARROW_PUNCT = new Set(["(", ")", "[", "]", "{", "}", "/", "\\", "-", '"', "`"]);
7
8// node_modules/beautiful-mermaid/src/multiline-utils.ts
9function normalizeBrTags(label) {
10 const unquoted = label.startsWith('"') && label.endsWith('"') ? label.slice(1, -1) : label;
11 return unquoted.replace(/<br\s*\/?>/gi, `
12`).replace(/\\n/g, `
13`).replace(/<\/?(?:sub|sup|small|mark)\s*>/gi, "").replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g, "<i>$1</i>").replace(/~~(.+?)~~/g, "<s>$1</s>");
14}
15
16// node_modules/beautiful-mermaid/src/parser.ts
17function parseMermaid(text) {
18 const lines = text.split(`
19`).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("%%"));
20 if (lines.length === 0) {
21 throw new Error("Empty mermaid diagram");
22 }
23 const header = lines[0];
24 if (/^stateDiagram(-v2)?\s*$/i.test(header)) {
25 return parseStateDiagram(lines);
26 }
27 return parseFlowchart(lines);
28}
29function parseFlowchart(lines) {
30 const headerMatch = lines[0].match(/^(?:graph|flowchart)\s+(TD|TB|LR|BT|RL)\s*$/i);
31 if (!headerMatch) {
32 throw new Error(`Invalid mermaid header: "${lines[0]}". Expected "graph TD", "flowchart LR", "stateDiagram-v2", etc.`);
33 }
34 const direction = headerMatch[1].toUpperCase();
35 const graph = {
36 direction,
37 nodes: new Map,
38 edges: [],
39 subgraphs: [],
40 classDefs: new Map,
41 classAssignments: new Map,
42 nodeStyles: new Map,
43 linkStyles: new Map
44 };
45 const subgraphStack = [];
46 for (let i = 1;i < lines.length; i++) {
47 const line = lines[i];
48 const classDefMatch = line.match(/^classDef\s+(\w+)\s+(.+)$/);
49 if (classDefMatch) {
50 const name = classDefMatch[1];
51 const propsStr = classDefMatch[2];
52 const props = parseStyleProps(propsStr);
53 graph.classDefs.set(name, props);
54 continue;
55 }
56 const classAssignMatch = line.match(/^class\s+([\w,-]+)\s+(\w+)$/);
57 if (classAssignMatch) {
58 const nodeIds = classAssignMatch[1].split(",").map((s) => s.trim());
59 const className = classAssignMatch[2];
60 for (const id of nodeIds) {
61 graph.classAssignments.set(id, className);
62 }
63 continue;
64 }
65 const styleMatch = line.match(/^style\s+([\w,-]+)\s+(.+)$/);
66 if (styleMatch) {
67 const nodeIds = styleMatch[1].split(",").map((s) => s.trim());
68 const props = parseStyleProps(styleMatch[2]);
69 for (const id of nodeIds) {
70 graph.nodeStyles.set(id, { ...graph.nodeStyles.get(id), ...props });
71 }
72 continue;
73 }
74 const linkStyleMatch = line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/);
75 if (linkStyleMatch) {
76 const target = linkStyleMatch[1].trim();
77 const props = parseStyleProps(linkStyleMatch[2]);
78 if (target === "default") {
79 graph.linkStyles.set("default", { ...graph.linkStyles.get("default"), ...props });
80 } else {
81 const indices = target.split(",").map((s) => parseInt(s.trim(), 10));
82 for (const idx of indices) {
83 if (!isNaN(idx)) {
84 graph.linkStyles.set(idx, { ...graph.linkStyles.get(idx), ...props });
85 }
86 }
87 }
88 continue;
89 }
90 const dirMatch = line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i);
91 if (dirMatch && subgraphStack.length > 0) {
92 subgraphStack[subgraphStack.length - 1].direction = dirMatch[1].toUpperCase();
93 continue;
94 }
95 const subgraphMatch = line.match(/^subgraph\s+(.+)$/);
96 if (subgraphMatch) {
97 const rest = subgraphMatch[1].trim();
98 const bracketMatch = rest.match(/^([\w-]+)\s*\[(.+)\]$/);
99 let id;
100 let label;
101 if (bracketMatch) {
102 id = bracketMatch[1];
103 label = normalizeBrTags(bracketMatch[2]);
104 } else {
105 label = normalizeBrTags(rest);
106 id = rest.replace(/\s+/g, "_").replace(/[^\w]/g, "");
107 }
108 const sg = { id, label, nodeIds: [], children: [] };
109 subgraphStack.push(sg);
110 continue;
111 }
112 if (line === "end") {
113 const completed = subgraphStack.pop();
114 if (completed) {
115 if (subgraphStack.length > 0) {
116 subgraphStack[subgraphStack.length - 1].children.push(completed);
117 } else {
118 graph.subgraphs.push(completed);
119 }
120 }
121 continue;
122 }
123 parseEdgeLine(line, graph, subgraphStack);
124 }
125 return graph;
126}
127function parseStateDiagram(lines) {
128 const graph = {
129 direction: "TD",
130 nodes: new Map,
131 edges: [],
132 subgraphs: [],
133 classDefs: new Map,
134 classAssignments: new Map,
135 nodeStyles: new Map,
136 linkStyles: new Map
137 };
138 const compositeStack = [];
139 const compositeStateIds = new Set;
140 let startCount = 0;
141 let endCount = 0;
142 for (let i = 1;i < lines.length; i++) {
143 const line = lines[i];
144 const dirMatch = line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i);
145 if (dirMatch) {
146 if (compositeStack.length > 0) {
147 compositeStack[compositeStack.length - 1].direction = dirMatch[1].toUpperCase();
148 } else {
149 graph.direction = dirMatch[1].toUpperCase();
150 }
151 continue;
152 }
153 const linkStyleMatch = line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/);
154 if (linkStyleMatch) {
155 const target = linkStyleMatch[1].trim();
156 const props = parseStyleProps(linkStyleMatch[2]);
157 if (target === "default") {
158 graph.linkStyles.set("default", { ...graph.linkStyles.get("default"), ...props });
159 } else {
160 const indices = target.split(",").map((s) => parseInt(s.trim(), 10));
161 for (const idx of indices) {
162 if (!isNaN(idx)) {
163 graph.linkStyles.set(idx, { ...graph.linkStyles.get(idx), ...props });
164 }
165 }
166 }
167 continue;
168 }
169 const compositeMatch = line.match(/^state\s+(?:"([^"]+)"\s+as\s+)?([\w\p{L}]+)\s*\{$/u);
170 if (compositeMatch) {
171 const label = compositeMatch[1] ?? compositeMatch[2];
172 const id = compositeMatch[2];
173 const sg = { id, label, nodeIds: [], children: [] };
174 compositeStack.push(sg);
175 compositeStateIds.add(id);
176 graph.nodes.delete(id);
177 continue;
178 }
179 if (line === "}") {
180 const completed = compositeStack.pop();
181 if (completed) {
182 if (compositeStack.length > 0) {
183 compositeStack[compositeStack.length - 1].children.push(completed);
184 } else {
185 graph.subgraphs.push(completed);
186 }
187 }
188 continue;
189 }
190 const stateAliasMatch = line.match(/^state\s+"([^"]+)"\s+as\s+([\w\p{L}]+)\s*$/u);
191 if (stateAliasMatch) {
192 const label = normalizeBrTags(stateAliasMatch[1]);
193 const id = stateAliasMatch[2];
194 registerStateNode(graph, compositeStack, { id, label, shape: "rounded" });
195 continue;
196 }
197 const transitionMatch = line.match(/^(\[\*\]|[\w\p{L}-]+)\s*(-->)\s*(\[\*\]|[\w\p{L}-]+)(?:\s*:\s*(.+))?$/u);
198 if (transitionMatch) {
199 let sourceId = transitionMatch[1];
200 let targetId = transitionMatch[3];
201 const rawTransitionLabel = transitionMatch[4]?.trim();
202 const edgeLabel = rawTransitionLabel ? normalizeBrTags(rawTransitionLabel) : undefined;
203 if (sourceId === "[*]") {
204 startCount++;
205 sourceId = `_start${startCount > 1 ? startCount : ""}`;
206 registerStateNode(graph, compositeStack, { id: sourceId, label: "", shape: "state-start" });
207 } else if (!compositeStateIds.has(sourceId)) {
208 ensureStateNode(graph, compositeStack, sourceId);
209 }
210 if (targetId === "[*]") {
211 endCount++;
212 targetId = `_end${endCount > 1 ? endCount : ""}`;
213 registerStateNode(graph, compositeStack, { id: targetId, label: "", shape: "state-end" });
214 } else if (!compositeStateIds.has(targetId)) {
215 ensureStateNode(graph, compositeStack, targetId);
216 }
217 graph.edges.push({
218 source: sourceId,
219 target: targetId,
220 label: edgeLabel,
221 style: "solid",
222 hasArrowStart: false,
223 hasArrowEnd: true
224 });
225 continue;
226 }
227 const stateDescMatch = line.match(/^([\w\p{L}-]+)\s*:\s*(.+)$/u);
228 if (stateDescMatch) {
229 const id = stateDescMatch[1];
230 const label = normalizeBrTags(stateDescMatch[2].trim());
231 registerStateNode(graph, compositeStack, { id, label, shape: "rounded" });
232 continue;
233 }
234 }
235 return graph;
236}
237function registerStateNode(graph, compositeStack, node) {
238 const isNew = !graph.nodes.has(node.id);
239 if (isNew) {
240 graph.nodes.set(node.id, node);
241 }
242 if (compositeStack.length > 0) {
243 const current = compositeStack[compositeStack.length - 1];
244 if (!current.nodeIds.includes(node.id)) {
245 current.nodeIds.push(node.id);
246 }
247 }
248}
249function ensureStateNode(graph, compositeStack, id) {
250 if (!graph.nodes.has(id)) {
251 registerStateNode(graph, compositeStack, { id, label: id, shape: "rounded" });
252 } else {
253 if (compositeStack.length > 0) {
254 const current = compositeStack[compositeStack.length - 1];
255 if (!current.nodeIds.includes(id)) {
256 current.nodeIds.push(id);
257 }
258 }
259 }
260}
261function parseStyleProps(propsStr) {
262 const cleaned = propsStr.replace(/;\s*$/, "");
263 const props = {};
264 for (const pair of cleaned.split(",")) {
265 const colonIdx = pair.indexOf(":");
266 if (colonIdx > 0) {
267 const key = pair.slice(0, colonIdx).trim();
268 const val = pair.slice(colonIdx + 1).trim();
269 if (key && val) {
270 props[key] = val;
271 }
272 }
273 }
274 return props;
275}
276var ARROW_REGEX = /^(<)?(-->|-.->|==>|---|-\.-|===)(?:\|([^|]*)\|)?/;
277var TEXT_ARROW_REGEX = /^(<)?(--|-\.|==)\s+(.+?)\s+(-->|---|\.\->|-\.\-|==>|===)/;
278var NODE_PATTERNS = [
279 { regex: /^([\w-]+)\(\(\((.+?)\)\)\)/, shape: "doublecircle" },
280 { regex: /^([\w-]+)\(\[(.+?)\]\)/, shape: "stadium" },
281 { regex: /^([\w-]+)\(\((.+?)\)\)/, shape: "circle" },
282 { regex: /^([\w-]+)\[\[(.+?)\]\]/, shape: "subroutine" },
283 { regex: /^([\w-]+)\[\((.+?)\)\]/, shape: "cylinder" },
284 { regex: /^([\w-]+)\[\/(.+?)\\\]/, shape: "trapezoid" },
285 { regex: /^([\w-]+)\[\\(.+?)\/\]/, shape: "trapezoid-alt" },
286 { regex: /^([\w-]+)>(.+?)\]/, shape: "asymmetric" },
287 { regex: /^([\w-]+)\{\{(.+?)\}\}/, shape: "hexagon" },
288 { regex: /^([\w-]+)\[(.+?)\]/, shape: "rectangle" },
289 { regex: /^([\w-]+)\((.+?)\)/, shape: "rounded" },
290 { regex: /^([\w-]+)\{(.+?)\}/, shape: "diamond" }
291];
292var BARE_NODE_REGEX = /^([\w-]+)/;
293var CLASS_SHORTHAND_REGEX = /^:::([\w][\w-]*)/;
294function parseEdgeLine(line, graph, subgraphStack) {
295 let remaining = line.trim();
296 const firstGroup = consumeNodeGroup(remaining, graph, subgraphStack);
297 if (!firstGroup || firstGroup.ids.length === 0)
298 return;
299 remaining = firstGroup.remaining.trim();
300 let prevGroupIds = firstGroup.ids;
301 while (remaining.length > 0) {
302 let hasArrowStart;
303 let style;
304 let hasArrowEnd;
305 let edgeLabel;
306 const arrowMatch = remaining.match(ARROW_REGEX);
307 if (arrowMatch) {
308 hasArrowStart = Boolean(arrowMatch[1]);
309 const arrowOp = arrowMatch[2];
310 const rawEdgeLabel = arrowMatch[3]?.trim();
311 edgeLabel = rawEdgeLabel ? normalizeBrTags(rawEdgeLabel) : undefined;
312 remaining = remaining.slice(arrowMatch[0].length).trim();
313 style = arrowStyleFromOp(arrowOp);
314 hasArrowEnd = arrowOp.endsWith(">");
315 } else {
316 const textMatch = remaining.match(TEXT_ARROW_REGEX);
317 if (!textMatch)
318 break;
319 hasArrowStart = Boolean(textMatch[1]);
320 const rawLabel = textMatch[3].trim();
321 edgeLabel = rawLabel ? normalizeBrTags(rawLabel) : undefined;
322 const openOp = textMatch[2];
323 const closeOp = textMatch[4];
324 remaining = remaining.slice(textMatch[0].length).trim();
325 style = textArrowStyleFromOps(openOp, closeOp);
326 hasArrowEnd = closeOp.endsWith(">");
327 }
328 const nextGroup = consumeNodeGroup(remaining, graph, subgraphStack);
329 if (!nextGroup || nextGroup.ids.length === 0)
330 break;
331 remaining = nextGroup.remaining.trim();
332 for (const sourceId of prevGroupIds) {
333 for (const targetId of nextGroup.ids) {
334 graph.edges.push({
335 source: sourceId,
336 target: targetId,
337 label: edgeLabel,
338 style,
339 hasArrowStart,
340 hasArrowEnd
341 });
342 }
343 }
344 prevGroupIds = nextGroup.ids;
345 }
346}
347function consumeNodeGroup(text, graph, subgraphStack) {
348 const first = consumeNode(text, graph, subgraphStack);
349 if (!first)
350 return null;
351 const ids = [first.id];
352 let remaining = first.remaining.trim();
353 while (remaining.startsWith("&")) {
354 remaining = remaining.slice(1).trim();
355 const next = consumeNode(remaining, graph, subgraphStack);
356 if (!next)
357 break;
358 ids.push(next.id);
359 remaining = next.remaining.trim();
360 }
361 return { ids, remaining };
362}
363function consumeNode(text, graph, subgraphStack) {
364 let id = null;
365 let remaining = text;
366 for (const { regex, shape } of NODE_PATTERNS) {
367 const match = text.match(regex);
368 if (match) {
369 id = match[1];
370 const label = normalizeBrTags(match[2]);
371 registerNode(graph, subgraphStack, { id, label, shape });
372 remaining = text.slice(match[0].length);
373 break;
374 }
375 }
376 if (id === null) {
377 const bareMatch = text.match(BARE_NODE_REGEX);
378 if (bareMatch) {
379 id = bareMatch[1];
380 if (!graph.nodes.has(id)) {
381 registerNode(graph, subgraphStack, { id, label: id, shape: "rectangle" });
382 }
383 remaining = text.slice(bareMatch[0].length);
384 }
385 }
386 if (id === null)
387 return null;
388 const classMatch = remaining.match(CLASS_SHORTHAND_REGEX);
389 if (classMatch) {
390 graph.classAssignments.set(id, classMatch[1]);
391 remaining = remaining.slice(classMatch[0].length);
392 }
393 return { id, remaining };
394}
395function registerNode(graph, subgraphStack, node) {
396 const isNew = !graph.nodes.has(node.id);
397 if (isNew) {
398 graph.nodes.set(node.id, node);
399 }
400 trackInSubgraph(subgraphStack, node.id);
401}
402function trackInSubgraph(subgraphStack, nodeId) {
403 if (subgraphStack.length > 0) {
404 const current = subgraphStack[subgraphStack.length - 1];
405 if (!current.nodeIds.includes(nodeId)) {
406 current.nodeIds.push(nodeId);
407 }
408 }
409}
410function arrowStyleFromOp(op) {
411 if (op === "-.->")
412 return "dotted";
413 if (op === "-.-")
414 return "dotted";
415 if (op === "==>")
416 return "thick";
417 if (op === "===")
418 return "thick";
419 return "solid";
420}
421function textArrowStyleFromOps(openOp, closeOp) {
422 if (openOp === "-." || closeOp === ".->" || closeOp === "-.-")
423 return "dotted";
424 if (openOp === "==" || closeOp === "==>" || closeOp === "===")
425 return "thick";
426 return "solid";
427}
428
429// node_modules/beautiful-mermaid/src/ascii/types.ts
430var Up = { x: 1, y: 0 };
431var Down = { x: 1, y: 2 };
432var Left = { x: 0, y: 1 };
433var Right = { x: 2, y: 1 };
434var UpperRight = { x: 2, y: 0 };
435var UpperLeft = { x: 0, y: 0 };
436var LowerRight = { x: 2, y: 2 };
437var LowerLeft = { x: 0, y: 2 };
438var Middle = { x: 1, y: 1 };
439function gridCoordEquals(a, b) {
440 return a.x === b.x && a.y === b.y;
441}
442function drawingCoordEquals(a, b) {
443 return a.x === b.x && a.y === b.y;
444}
445function gridCoordDirection(c, dir) {
446 return { x: c.x + dir.x, y: c.y + dir.y };
447}
448function gridKey(c) {
449 return `${c.x},${c.y}`;
450}
451var EMPTY_STYLE = { name: "", styles: {} };
452
453// node_modules/beautiful-mermaid/src/ascii/ansi.ts
454var DEFAULT_ASCII_THEME = {
455 fg: "#27272a",
456 border: "#a1a1aa",
457 line: "#71717a",
458 arrow: "#52525b",
459 corner: "#71717a",
460 junction: "#a1a1aa"
461};
462function detectColorMode() {
463 const proc = globalThis.process;
464 if (proc) {
465 if (!proc.stdout?.isTTY) {
466 return "none";
467 }
468 const colorTerm = proc.env?.COLORTERM?.toLowerCase() ?? "";
469 const term = proc.env?.TERM?.toLowerCase() ?? "";
470 if (colorTerm === "truecolor" || colorTerm === "24bit") {
471 return "truecolor";
472 }
473 if (term.includes("256color") || term.includes("256")) {
474 return "ansi256";
475 }
476 if (term && term !== "dumb") {
477 return "ansi16";
478 }
479 return "none";
480 }
481 if (typeof document !== "undefined") {
482 return "html";
483 }
484 return "none";
485}
486function parseHex(hex) {
487 const h = hex.replace("#", "");
488 if (h.length === 3) {
489 return {
490 r: parseInt(h[0] + h[0], 16),
491 g: parseInt(h[1] + h[1], 16),
492 b: parseInt(h[2] + h[2], 16)
493 };
494 }
495 return {
496 r: parseInt(h.substring(0, 2), 16),
497 g: parseInt(h.substring(2, 4), 16),
498 b: parseInt(h.substring(4, 6), 16)
499 };
500}
501var ESC = "\x1B[";
502var RESET = `${ESC}0m`;
503function truecolorFg(hex) {
504 const { r, g, b } = parseHex(hex);
505 return `${ESC}38;2;${r};${g};${b}m`;
506}
507function rgbTo256(r, g, b) {
508 const avg = (r + g + b) / 3;
509 const maxDiff = Math.max(Math.abs(r - avg), Math.abs(g - avg), Math.abs(b - avg));
510 if (maxDiff < 10) {
511 const gray = Math.round(avg / 255 * 23);
512 return 232 + Math.min(23, Math.max(0, gray));
513 }
514 const toIndex = (v) => {
515 if (v < 48)
516 return 0;
517 if (v < 115)
518 return 1;
519 return Math.min(5, Math.floor((v - 35) / 40));
520 };
521 const ri = toIndex(r);
522 const gi = toIndex(g);
523 const bi = toIndex(b);
524 return 16 + 36 * ri + 6 * gi + bi;
525}
526function ansi256Fg(hex) {
527 const { r, g, b } = parseHex(hex);
528 const index = rgbTo256(r, g, b);
529 return `${ESC}38;5;${index}m`;
530}
531function ansi16Fg(hex) {
532 const { r, g, b } = parseHex(hex);
533 const luma = 0.299 * r + 0.587 * g + 0.114 * b;
534 const bright = luma > 100 ? 0 : 60;
535 let code;
536 if (r > 180 && g < 100 && b < 100)
537 code = 31;
538 else if (g > 180 && r < 100 && b < 100)
539 code = 32;
540 else if (r > 150 && g > 150 && b < 100)
541 code = 33;
542 else if (b > 180 && r < 100 && g < 100)
543 code = 34;
544 else if (r > 150 && b > 150 && g < 100)
545 code = 35;
546 else if (g > 150 && b > 150 && r < 100)
547 code = 36;
548 else if (luma > 200)
549 code = 37;
550 else if (luma < 50)
551 code = 30;
552 else
553 code = 37;
554 return `${ESC}${code + bright}m`;
555}
556function escapeHtml(text) {
557 return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
558}
559function htmlSpan(hex, text) {
560 return `<span style="color:${hex}">${escapeHtml(text)}</span>`;
561}
562function getRoleColor(role, theme) {
563 switch (role) {
564 case "text":
565 return theme.fg;
566 case "border":
567 return theme.border;
568 case "line":
569 return theme.line;
570 case "arrow":
571 return theme.arrow;
572 case "corner":
573 return theme.corner ?? theme.line;
574 case "junction":
575 return theme.junction ?? theme.border;
576 default:
577 return theme.fg;
578 }
579}
580function getAnsiColor(role, theme, mode) {
581 if (mode === "none")
582 return "";
583 const hex = getRoleColor(role, theme);
584 switch (mode) {
585 case "truecolor":
586 return truecolorFg(hex);
587 case "ansi256":
588 return ansi256Fg(hex);
589 case "ansi16":
590 return ansi16Fg(hex);
591 default:
592 return "";
593 }
594}
595function colorizeLine(chars, roles, theme, mode) {
596 if (mode === "none") {
597 return chars.join("");
598 }
599 if (mode === "html") {
600 return colorizeLineHtml(chars, roles, theme);
601 }
602 let result = "";
603 let currentRole = null;
604 let buffer = "";
605 for (let i = 0;i < chars.length; i++) {
606 const char = chars[i];
607 const role = roles[i] ?? null;
608 if (char === " ") {
609 if (buffer.length > 0) {
610 if (currentRole !== null) {
611 result += getAnsiColor(currentRole, theme, mode) + buffer + RESET;
612 } else {
613 result += buffer;
614 }
615 buffer = "";
616 currentRole = null;
617 }
618 result += char;
619 continue;
620 }
621 if (role === currentRole) {
622 buffer += char;
623 continue;
624 }
625 if (buffer.length > 0) {
626 if (currentRole !== null) {
627 result += getAnsiColor(currentRole, theme, mode) + buffer + RESET;
628 } else {
629 result += buffer;
630 }
631 }
632 buffer = char;
633 currentRole = role;
634 }
635 if (buffer.length > 0 && currentRole !== null) {
636 result += getAnsiColor(currentRole, theme, mode) + buffer + RESET;
637 } else if (buffer.length > 0) {
638 result += buffer;
639 }
640 return result;
641}
642function colorizeLineHtml(chars, roles, theme) {
643 let result = "";
644 let currentRole = null;
645 let buffer = "";
646 const flush = () => {
647 if (buffer.length === 0)
648 return;
649 if (currentRole !== null) {
650 result += htmlSpan(getRoleColor(currentRole, theme), buffer);
651 } else {
652 result += escapeHtml(buffer);
653 }
654 buffer = "";
655 currentRole = null;
656 };
657 for (let i = 0;i < chars.length; i++) {
658 const char = chars[i];
659 const role = roles[i] ?? null;
660 if (char === " ") {
661 flush();
662 result += " ";
663 continue;
664 }
665 if (role === currentRole) {
666 buffer += char;
667 continue;
668 }
669 flush();
670 buffer = char;
671 currentRole = role;
672 }
673 flush();
674 return result;
675}
676function colorizeText(text, hex, mode) {
677 if (mode === "none" || text.length === 0)
678 return text;
679 if (mode === "html")
680 return htmlSpan(hex, text);
681 let code;
682 switch (mode) {
683 case "truecolor":
684 code = truecolorFg(hex);
685 break;
686 case "ansi256":
687 code = ansi256Fg(hex);
688 break;
689 case "ansi16":
690 code = ansi16Fg(hex);
691 break;
692 default:
693 return text;
694 }
695 return `${code}${text}${RESET}`;
696}
697
698// node_modules/beautiful-mermaid/src/ascii/canvas.ts
699function mkCanvas(x, y) {
700 const canvas = [];
701 for (let i = 0;i <= x; i++) {
702 const col = [];
703 for (let j = 0;j <= y; j++) {
704 col.push(" ");
705 }
706 canvas.push(col);
707 }
708 return canvas;
709}
710function copyCanvas(source) {
711 const [maxX, maxY] = getCanvasSize(source);
712 return mkCanvas(maxX, maxY);
713}
714function mkRoleCanvas(x, y) {
715 const roleCanvas = [];
716 for (let i = 0;i <= x; i++) {
717 const col = [];
718 for (let j = 0;j <= y; j++) {
719 col.push(null);
720 }
721 roleCanvas.push(col);
722 }
723 return roleCanvas;
724}
725function increaseRoleCanvasSize(roleCanvas, newX, newY) {
726 const currX = roleCanvas.length - 1;
727 const currY = (roleCanvas[0]?.length ?? 1) - 1;
728 const targetX = Math.max(newX, currX);
729 const targetY = Math.max(newY, currY);
730 const grown = mkRoleCanvas(targetX, targetY);
731 for (let x = 0;x < grown.length; x++) {
732 for (let y = 0;y < grown[0].length; y++) {
733 if (x < roleCanvas.length && y < roleCanvas[0].length) {
734 grown[x][y] = roleCanvas[x][y];
735 }
736 }
737 }
738 roleCanvas.length = 0;
739 roleCanvas.push(...grown);
740 return roleCanvas;
741}
742function setRole(roleCanvas, x, y, role) {
743 if (x >= roleCanvas.length || y >= (roleCanvas[0]?.length ?? 0)) {
744 increaseRoleCanvasSize(roleCanvas, x, y);
745 }
746 roleCanvas[x][y] = role;
747}
748function getCanvasSize(canvas) {
749 return [canvas.length - 1, (canvas[0]?.length ?? 1) - 1];
750}
751function increaseSize(canvas, newX, newY) {
752 const [currX, currY] = getCanvasSize(canvas);
753 const targetX = Math.max(newX, currX);
754 const targetY = Math.max(newY, currY);
755 const grown = mkCanvas(targetX, targetY);
756 for (let x = 0;x < grown.length; x++) {
757 for (let y = 0;y < grown[0].length; y++) {
758 if (x < canvas.length && y < canvas[0].length) {
759 grown[x][y] = canvas[x][y];
760 }
761 }
762 }
763 canvas.length = 0;
764 canvas.push(...grown);
765 return canvas;
766}
767var JUNCTION_CHARS = new Set([
768 "─",
769 "│",
770 "┌",
771 "┐",
772 "└",
773 "┘",
774 "├",
775 "┤",
776 "┬",
777 "┴",
778 "┼",
779 "╴",
780 "╵",
781 "╶",
782 "╷"
783]);
784function isJunctionChar(c) {
785 return JUNCTION_CHARS.has(c);
786}
787function isAlphanumeric(c) {
788 return /^[a-zA-Z0-9]$/.test(c);
789}
790var JUNCTION_MAP = {
791 "─": { "│": "┼", "┌": "┬", "┐": "┬", "└": "┴", "┘": "┴", "├": "┼", "┤": "┼", "┬": "┬", "┴": "┴" },
792 "│": { "─": "┼", "┌": "├", "┐": "┤", "└": "├", "┘": "┤", "├": "├", "┤": "┤", "┬": "┼", "┴": "┼" },
793 "┌": { "─": "┬", "│": "├", "┐": "┬", "└": "├", "┘": "┼", "├": "├", "┤": "┼", "┬": "┬", "┴": "┼" },
794 "┐": { "─": "┬", "│": "┤", "┌": "┬", "└": "┼", "┘": "┤", "├": "┼", "┤": "┤", "┬": "┬", "┴": "┼" },
795 "└": { "─": "┴", "│": "├", "┌": "├", "┐": "┼", "┘": "┴", "├": "├", "┤": "┼", "┬": "┼", "┴": "┴" },
796 "┘": { "─": "┴", "│": "┤", "┌": "┼", "┐": "┤", "└": "┴", "├": "┼", "┤": "┤", "┬": "┼", "┴": "┴" },
797 "├": { "─": "┼", "│": "├", "┌": "├", "┐": "┼", "└": "├", "┘": "┼", "┤": "┼", "┬": "┼", "┴": "┼" },
798 "┤": { "─": "┼", "│": "┤", "┌": "┼", "┐": "┤", "└": "┼", "┘": "┤", "├": "┼", "┬": "┼", "┴": "┼" },
799 "┬": { "─": "┬", "│": "┼", "┌": "┬", "┐": "┬", "└": "┼", "┘": "┼", "├": "┼", "┤": "┼", "┴": "┼" },
800 "┴": { "─": "┴", "│": "┼", "┌": "┼", "┐": "┼", "└": "┴", "┘": "┴", "├": "┼", "┤": "┼", "┬": "┼" }
801};
802function mergeJunctions(c1, c2) {
803 return JUNCTION_MAP[c1]?.[c2] ?? c1;
804}
805function mergeCanvases(base, offset, useAscii, ...overlays) {
806 let [maxX, maxY] = getCanvasSize(base);
807 for (const overlay of overlays) {
808 const [oX, oY] = getCanvasSize(overlay);
809 maxX = Math.max(maxX, oX + offset.x);
810 maxY = Math.max(maxY, oY + offset.y);
811 }
812 const merged = mkCanvas(maxX, maxY);
813 for (let x = 0;x <= maxX; x++) {
814 for (let y = 0;y <= maxY; y++) {
815 if (x < base.length && y < base[0].length) {
816 merged[x][y] = base[x][y];
817 }
818 }
819 }
820 for (const overlay of overlays) {
821 for (let x = 0;x < overlay.length; x++) {
822 for (let y = 0;y < overlay[0].length; y++) {
823 const c = overlay[x][y];
824 if (c !== " ") {
825 const mx = x + offset.x;
826 const my = y + offset.y;
827 const current = merged[mx][my];
828 if (!useAscii && isJunctionChar(c) && isJunctionChar(current)) {
829 merged[mx][my] = mergeJunctions(current, c);
830 } else if (isAlphanumeric(current) && isAlphanumeric(c)) {} else {
831 merged[mx][my] = c;
832 }
833 }
834 }
835 }
836 }
837 return merged;
838}
839function canvasToString(canvas, options) {
840 const [maxX, maxY] = getCanvasSize(canvas);
841 const lines = [];
842 const roleCanvas = options?.roleCanvas;
843 const colorMode = options?.colorMode ?? "none";
844 const theme = options?.theme ?? DEFAULT_ASCII_THEME;
845 for (let y = 0;y <= maxY; y++) {
846 if (colorMode === "none" || !roleCanvas) {
847 let line = "";
848 for (let x = 0;x <= maxX; x++) {
849 line += canvas[x][y];
850 }
851 lines.push(line);
852 } else {
853 const chars = [];
854 const roles = [];
855 for (let x = 0;x <= maxX; x++) {
856 chars.push(canvas[x][y]);
857 roles.push(roleCanvas[x]?.[y] ?? null);
858 }
859 lines.push(colorizeLine(chars, roles, theme, colorMode));
860 }
861 }
862 return lines.join(`
863`);
864}
865var VERTICAL_FLIP_MAP = {
866 "▲": "▼",
867 "▼": "▲",
868 "◤": "◣",
869 "◣": "◤",
870 "◥": "◢",
871 "◢": "◥",
872 "^": "v",
873 v: "^",
874 "┌": "└",
875 "└": "┌",
876 "┐": "┘",
877 "┘": "┐",
878 "┬": "┴",
879 "┴": "┬",
880 "╵": "╷",
881 "╷": "╵"
882};
883function flipCanvasVertically(canvas) {
884 for (const col of canvas) {
885 col.reverse();
886 }
887 for (const col of canvas) {
888 for (let y = 0;y < col.length; y++) {
889 const flipped = VERTICAL_FLIP_MAP[col[y]];
890 if (flipped)
891 col[y] = flipped;
892 }
893 }
894 return canvas;
895}
896function flipRoleCanvasVertically(roleCanvas) {
897 for (const col of roleCanvas) {
898 col.reverse();
899 }
900 return roleCanvas;
901}
902function drawText(canvas, start, text, forceOverwrite = false) {
903 increaseSize(canvas, start.x + text.length, start.y);
904 for (let i = 0;i < text.length; i++) {
905 const x = start.x + i;
906 const current = canvas[x][start.y];
907 if (forceOverwrite || current === " ") {
908 canvas[x][start.y] = text[i];
909 }
910 }
911}
912function setCanvasSizeToGrid(canvas, columnWidth, rowHeight) {
913 let maxX = 0;
914 let maxY = 0;
915 for (const w of columnWidth.values())
916 maxX += w;
917 for (const h of rowHeight.values())
918 maxY += h;
919 increaseSize(canvas, maxX - 1, maxY - 1);
920}
921function setRoleCanvasSizeToGrid(roleCanvas, columnWidth, rowHeight) {
922 let maxX = 0;
923 let maxY = 0;
924 for (const w of columnWidth.values())
925 maxX += w;
926 for (const h of rowHeight.values())
927 maxY += h;
928 increaseRoleCanvasSize(roleCanvas, maxX - 1, maxY - 1);
929}
930
931// node_modules/beautiful-mermaid/src/ascii/converter.ts
932function convertToAsciiGraph(parsed, config) {
933 const nodeMap = new Map;
934 let index = 0;
935 for (const [id, mNode] of parsed.nodes) {
936 const asciiNode = {
937 name: id,
938 displayLabel: mNode.label,
939 shape: mNode.shape,
940 index,
941 gridCoord: null,
942 drawingCoord: null,
943 drawing: null,
944 drawn: false,
945 styleClassName: "",
946 styleClass: EMPTY_STYLE
947 };
948 nodeMap.set(id, asciiNode);
949 index++;
950 }
951 const nodes = [...nodeMap.values()];
952 const edges = [];
953 for (const mEdge of parsed.edges) {
954 const from = nodeMap.get(mEdge.source);
955 const to = nodeMap.get(mEdge.target);
956 if (!from || !to)
957 continue;
958 edges.push({
959 from,
960 to,
961 text: mEdge.label ?? "",
962 path: [],
963 labelLine: [],
964 startDir: { x: 0, y: 0 },
965 endDir: { x: 0, y: 0 },
966 style: mEdge.style,
967 hasArrowStart: mEdge.hasArrowStart,
968 hasArrowEnd: mEdge.hasArrowEnd
969 });
970 }
971 const subgraphs = [];
972 for (const mSg of parsed.subgraphs) {
973 convertSubgraph(mSg, null, nodeMap, subgraphs);
974 }
975 deduplicateSubgraphNodes(parsed.subgraphs, subgraphs, nodeMap, parsed);
976 for (const [nodeId, className] of parsed.classAssignments) {
977 const node = nodeMap.get(nodeId);
978 const classDef = parsed.classDefs.get(className);
979 if (node && classDef) {
980 node.styleClassName = className;
981 node.styleClass = { name: className, styles: classDef };
982 }
983 }
984 return {
985 nodes,
986 edges,
987 canvas: mkCanvas(0, 0),
988 roleCanvas: mkRoleCanvas(0, 0),
989 grid: new Map,
990 columnWidth: new Map,
991 rowHeight: new Map,
992 subgraphs,
993 config,
994 offsetX: 0,
995 offsetY: 0,
996 bundles: []
997 };
998}
999function convertSubgraph(mSg, parent, nodeMap, allSubgraphs) {
1000 let normalizedDirection;
1001 if (mSg.direction) {
1002 normalizedDirection = mSg.direction === "LR" || mSg.direction === "RL" ? "LR" : "TD";
1003 }
1004 const sg = {
1005 name: mSg.label,
1006 nodes: [],
1007 parent,
1008 children: [],
1009 minX: 0,
1010 minY: 0,
1011 maxX: 0,
1012 maxY: 0,
1013 direction: normalizedDirection
1014 };
1015 for (const nodeId of mSg.nodeIds) {
1016 const node = nodeMap.get(nodeId);
1017 if (node)
1018 sg.nodes.push(node);
1019 }
1020 allSubgraphs.push(sg);
1021 for (const childMSg of mSg.children) {
1022 const child = convertSubgraph(childMSg, sg, nodeMap, allSubgraphs);
1023 sg.children.push(child);
1024 for (const childNode of child.nodes) {
1025 if (!sg.nodes.includes(childNode)) {
1026 sg.nodes.push(childNode);
1027 }
1028 }
1029 }
1030 return sg;
1031}
1032function deduplicateSubgraphNodes(mermaidSubgraphs, asciiSubgraphs, nodeMap, parsed) {
1033 const sgMap = new Map;
1034 buildSgMap(mermaidSubgraphs, asciiSubgraphs, sgMap);
1035 const nodeOwner = new Map;
1036 function claimNodes(mSg) {
1037 const asciiSg = sgMap.get(mSg);
1038 if (!asciiSg)
1039 return;
1040 for (const child of mSg.children) {
1041 claimNodes(child);
1042 }
1043 for (const nodeId of mSg.nodeIds) {
1044 if (!nodeOwner.has(nodeId)) {
1045 nodeOwner.set(nodeId, asciiSg);
1046 }
1047 }
1048 }
1049 for (const mSg of mermaidSubgraphs) {
1050 claimNodes(mSg);
1051 }
1052 for (const asciiSg of asciiSubgraphs) {
1053 asciiSg.nodes = asciiSg.nodes.filter((node) => {
1054 let nodeId;
1055 for (const [id, n] of nodeMap) {
1056 if (n === node) {
1057 nodeId = id;
1058 break;
1059 }
1060 }
1061 if (!nodeId)
1062 return false;
1063 const owner = nodeOwner.get(nodeId);
1064 if (!owner)
1065 return true;
1066 return isAncestorOrSelf(asciiSg, owner);
1067 });
1068 }
1069}
1070function isAncestorOrSelf(candidate, target) {
1071 let current = target;
1072 while (current !== null) {
1073 if (current === candidate)
1074 return true;
1075 current = current.parent;
1076 }
1077 return false;
1078}
1079function buildSgMap(mSgs, aSgs, result) {
1080 const flatMermaid = [];
1081 function flatten(sgs) {
1082 for (const sg of sgs) {
1083 flatMermaid.push(sg);
1084 flatten(sg.children);
1085 }
1086 }
1087 flatten(mSgs);
1088 for (let i = 0;i < flatMermaid.length && i < aSgs.length; i++) {
1089 result.set(flatMermaid[i], aSgs[i]);
1090 }
1091}
1092
1093// node_modules/beautiful-mermaid/src/ascii/pathfinder.ts
1094class MinHeap {
1095 items = [];
1096 get length() {
1097 return this.items.length;
1098 }
1099 push(item) {
1100 this.items.push(item);
1101 this.bubbleUp(this.items.length - 1);
1102 }
1103 pop() {
1104 if (this.items.length === 0)
1105 return;
1106 const top = this.items[0];
1107 const last = this.items.pop();
1108 if (this.items.length > 0) {
1109 this.items[0] = last;
1110 this.sinkDown(0);
1111 }
1112 return top;
1113 }
1114 bubbleUp(i) {
1115 while (i > 0) {
1116 const parent = i - 1 >> 1;
1117 if (this.items[i].priority < this.items[parent].priority) {
1118 [this.items[i], this.items[parent]] = [this.items[parent], this.items[i]];
1119 i = parent;
1120 } else {
1121 break;
1122 }
1123 }
1124 }
1125 sinkDown(i) {
1126 const n = this.items.length;
1127 while (true) {
1128 let smallest = i;
1129 const left = 2 * i + 1;
1130 const right = 2 * i + 2;
1131 if (left < n && this.items[left].priority < this.items[smallest].priority) {
1132 smallest = left;
1133 }
1134 if (right < n && this.items[right].priority < this.items[smallest].priority) {
1135 smallest = right;
1136 }
1137 if (smallest !== i) {
1138 [this.items[i], this.items[smallest]] = [this.items[smallest], this.items[i]];
1139 i = smallest;
1140 } else {
1141 break;
1142 }
1143 }
1144 }
1145}
1146function heuristic(a, b) {
1147 const absX = Math.abs(a.x - b.x);
1148 const absY = Math.abs(a.y - b.y);
1149 if (absX === 0 || absY === 0) {
1150 return absX + absY;
1151 }
1152 return absX + absY + 1;
1153}
1154var MOVE_DIRS = [
1155 { x: 1, y: 0 },
1156 { x: -1, y: 0 },
1157 { x: 0, y: 1 },
1158 { x: 0, y: -1 }
1159];
1160function isFreeInGrid(grid, c) {
1161 if (c.x < 0 || c.y < 0)
1162 return false;
1163 return !grid.has(gridKey(c));
1164}
1165function getPath(grid, from, to) {
1166 const pq = new MinHeap;
1167 pq.push({ coord: from, priority: 0 });
1168 const costSoFar = new Map;
1169 costSoFar.set(gridKey(from), 0);
1170 const cameFrom = new Map;
1171 cameFrom.set(gridKey(from), null);
1172 while (pq.length > 0) {
1173 const current = pq.pop().coord;
1174 if (gridCoordEquals(current, to)) {
1175 const path = [];
1176 let c = current;
1177 while (c !== null) {
1178 path.unshift(c);
1179 c = cameFrom.get(gridKey(c)) ?? null;
1180 }
1181 return path;
1182 }
1183 const currentCost = costSoFar.get(gridKey(current));
1184 for (const dir of MOVE_DIRS) {
1185 const next = { x: current.x + dir.x, y: current.y + dir.y };
1186 if (!isFreeInGrid(grid, next) && !gridCoordEquals(next, to)) {
1187 continue;
1188 }
1189 const newCost = currentCost + 1;
1190 const nextKey = gridKey(next);
1191 const existingCost = costSoFar.get(nextKey);
1192 if (existingCost === undefined || newCost < existingCost) {
1193 costSoFar.set(nextKey, newCost);
1194 const priority = newCost + heuristic(next, to);
1195 pq.push({ coord: next, priority });
1196 cameFrom.set(nextKey, current);
1197 }
1198 }
1199 }
1200 return null;types/index.d.ts 10 lines1declare module 'claude-code' {
2 interface PluginState {
3 'mermaid-view': {
4 // 메시지(requestId)별로 원문을 펼친 다이어그램의 순번
5 openSources: StateFamily<number[]>
6 showAllSources: boolean
7 }
8 }
9}
10