SLOPSHOPPER

mermaid-view

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

newrowscommand
v0.1.0MITupdated 2026-10-26jo-minjun/claude-mermaid-view
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mermaid-view
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /mermaid-source ⎿ mermaid-view: Showing every mermaid source. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-mermaid-view

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.

Example

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.)

Install

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

Usage

Nothing to set up: mermaid blocks in replies are drawn as diagrams from then on.

  • Show the source of one diagram: click ▸ 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.)
  • Show or fold every source: run /mermaid-source.
  • Original text: only the drawing changes. The stored message and what Claude reads back keep the mermaid source.

A block is left as its source when:

  • the renderer cannot read it (a diagram type or syntax it does not support, a typo),
  • the diagram is wider than the terminal,
  • the block is still streaming (no closing fence yet).

Supported diagrams

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:

  • Edge labels longer than the gap between two boxes are cut (ordered in → ordere).
  • Large graphs with many crossing edges get hard to read in text.
  • Flowchart node IDs must be ASCII (A[주문] works, 주문[주문] does not); labels can be any language.

How it works

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.

Development

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

License

MIT. The bundled renderer keeps its own licenses: beautiful-mermaid (MIT) and entities (BSD-2-Clause), in hooks/vendor/.

Source 4 files
hooks/register.tsx 91 lines
1import { 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}
91
hooks/mermaid.ts 133 lines
1import { 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}
133
hooks/vendor/beautiful-mermaid.js 5809 lines
1// 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'mermaid-view': {
4      // 메시지(requestId)별로 원문을 펼친 다이어그램의 순번
5      openSources: StateFamily<number[]>
6      showAllSources: boolean
7    }
8  }
9}
10