Colourful, themeable rendering for Claude Code replies, with animated diagrams, timelines, state machines and sparklines. A fork of prismantis.

claude plugin install lumen --marketplace sruthik27/claude-foundry
Requires Claude Code 2.1.296+. See installation and compatibility.
Colourful, themeable rendering for Claude Code replies, now with animated diagrams: a Claude Code mod that redraws markdown in the terminal with themes, highlighted code, boxed tables, mermaid diagrams and charts, and explains flows step by step with diagrams that play.
lumen is a fork of prismantis by Nahum Litvin (MIT). Everything prismantis does is still here; see "What lumen adds" for the new parts.
| Write this | You see |
|---|---|
%% step A->B "request" lines in a ``` `mermaid ``` flowchart | the diagram plays: a dot travels each step along the drawn lines, the step's boxes go bold, the caption shows below. Click or space pauses, ←/→ steps, r restarts |
%% animate in a sequenceDiagram (or a flowchart) | every message (or edge) plays in order |
a mermaid gantt block | coloured timeline bars: done ✓, active ◂, critical in red, milestones ◆, after dependencies, a dated axis |
a stateDiagram-v2 block | drawn as a clean flowchart with start and end circles and labelled transitions |
{spark: 3 5 9 4 12} in text or a table cell | an inline sparkline ▁▃▆▂█ |
| a 60-line code block | folded to 12 lines with a ▸ 48 more lines button |
| a flowchart or sequence diagram too wide for your terminal | redrawn top-down, or as a numbered message list, rather than raw source |
Claude is told about all of these with each prompt you type, along with your terminal width, so it uses them when a picture helps. Animations run only while the reply is on screen and stop after two plays; headless runs and copies get the still diagram.
flowchart LR
U[Client] --> G[Gateway] --> A[Auth]
G --> S[Service]
%% step U->G "request arrives"
%% step G->A "verify token"
%% step A->G "token ok"
%% step G->S "forward"
lumen is a local mod: put the folder in Claude Code's mod path.
cp -R lumen ~/.claude/mods/lumen
# add ~/.claude/mods/lumen to CLAUDE_CODE_PLUGIN_DIRS in ~/.claude/settings.json (install.sh in the repo root does both)
If you also have prismantis installed, disable it so the two don't both draw replies:
claude plugin disable prismantis@prismantis
Start a new session. /lumen shows the commands, /lumen demo the full showcase.
Dark: catppuccin-mocha (default), dracula, nord, tokyo-night, gruvbox-dark, rose-pine, everforest, github-dark, one-dark, solarized-dark.
Light: catppuccin-latte, gruvbox-light, rose-pine-dawn, github-light, solarized-light.
Switch on the spot with /lumen theme nord, or run /lumen for the help screen with every theme. mono uses no color, only bold and dim. Every palette is MIT licensed and credited in THIRD_PARTY_NOTICES.md.
Header cells take the tableHeader color, and every cell sits in a box with a double line under the header (tableStyle: box, or rules, grid and minimal for lighter looks), and :---:/---: alignment is honored. Numbers inside cells are colored like everywhere else. Columns shrink to fit the terminal.
Code blocks get a header row with the language on the left and a copy button on the right, and the code sits indented below with no frame, so selecting it with the mouse copies only the code.
bash, sh, zsh, unlabeled) color the command word, --flags, quoted strings and # comments, and restart after |, && and ;.Code blocks tagged mermaid draw as colored text art:
graph LR, graph TD, decisions), sequence, state, class and ER diagramsxychart-beta, sized to the terminal widthDiagrams too wide for the window, or over 80 lines, stay as code. mermaidAscii swaps box-drawing characters for + - |. Pie charts are not supported.
When tables and diagrams follow each other, they share a row and wrap to the next line once the terminal runs out of width. A wide terminal shows a table, a flowchart and two charts side by side.
[ ⧉ copy ], drawn in Claude Code's accent color, sits on code blocks, tables, lists and quotes and puts the raw markdown on your clipboard. Quotes copy without their > markers, ready to paste as a message. Diagrams get two: ⧉ source copies the mermaid code and ⧉ art copies the drawn art, ready to paste into a chat code block. Tables get ⧉ art too: a plain boxed table that reads right in Slack, where pasted markdown does not. Replies end with ⧉ copy reply, which copies the reply as Claude wrote it (one-line English narration gets none). Use it for Hebrew and Arabic: selecting right-to-left text on screen copies the letters in the order they are drawn, not the order they are read. Clicking works where the terminal passes clicks through (fullscreen mode does); terminals with copy-on-select, such as Warp, may grab the word "copy" instead. Without a mouse, /lumen copy copies the last reply and /lumen copy code its last code block. Since Claude Code 2.1.291 ctrl+x then tab focuses the area above the prompt, not the buttons in replies.
Each tool call draws as one line: a bold verb and its target, Ran with a colored shell command, Read and Edited with the path. The dot is green when done, hollow while running and red on failure. Output still draws below. Collapsed groups draw one line too, Ran 3 commands, read 2 files · last: npm test, with a red count when any call failed. Expand a group (ctrl+o or --verbose) and its calls draw with Claude Code's own rows, inline output included.
toolStyle keeps tool rows apart from what Claude says. The default, chat, puts them dimmed on the right, like the other side of a chat, and leaves Claude's sentences on the left. Rows are capped at 60% of the width.
tree-dim tucks them under the sentence with ⎿ and dims them.
tree-bold is tree-dim with one-line sentences in bold.
classic is the original look: a bold verb and a status dot, at full brightness.
The line that closes a turn keeps Claude Code's word and colors the duration: ✻ Baked for 6m 20s. Terminal only, since that's the only surface that draws it.
Output from slash commands, built-in or from other plugins, is parsed as markdown and drawn like a reply, copy buttons included. Errors keep Claude Code's own red line.
A mod that draws markdown in its own pane or band can have lumen draw it, in the user's theme, by calling $.lumen.markdown with the surface, the text and the columns it has.
undefined when lumen is disabled or the text holds nothing to draw.try.columns is the width of your content. Lumen draws at least 20 columns wide.let drawn
try {
drawn = await $.lumen.markdown({ surface: e.surface, text, columns: e.props.bodyColumns })
} catch {}
return drawn ?? <Markdown text={text} />
The types are in types/index.d.ts. List lumen under dependencies in your plugin.json to have Claude Code lay them into your .claude-plugin/types/. When lumen is optional, declare the noun in your own contract instead:
declare module 'claude-code' {
interface EngineInterface {
lumen: {
markdown: (args: { surface: RenderSurface; text: string; columns: number }) => Promise<RenderElement | undefined>
}
}
}
Claude rarely writes a chart unless it knows the terminal can draw one. With diagramHints on (the default), lumen attaches a short note to each prompt you type, read by the model and never shown, saying tables, alerts, code, mermaid diagrams and xychart-beta charts render here and to use one when a numeric series or a flow is easier to see than read. It also asks for commands in fenced blocks, since only those get a copy button. It costs about 190 tokens per prompt. It's off whenever mermaid is off, and skipped for headless claude -p runs and background notifications. Claude Code doesn't let installed plugins edit the system prompt (its built-in sec-default policy keeps that for the organization), so the note rides along with your prompt instead.
Bold, italic, strikethrough, inline code, links and bare URLs, clickable as terminal hyperlinks (where the terminal has none, the URL shows dimmed after the text). Numbers, versions (v2.14.0), durations (250ms, 3h), sizes (16Gi) and percentages (99.9%) take the number color, and paths like ~/src/app.ts the path color.
headingStyle picks banner (the default: a box around H1, a heavy rule under H2), bold, underline or uppercase. Terminals have one font size, so headings stand out through style and color. Lists keep their numbers and nest with • and ◦. Quotes get an accent bar, and GitHub alerts (> [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]) draw as colored boxes. Single-series bar charts print each value above its bar and color the tallest one.
Task lists draw as [ ] and [✓], with done items dimmed and struck through. taskStyle switches to ticks (○ ✓), box (□ ✓) or progress, which adds a done-count bar above each list.
What you type, at the prompt or through Remote Control, draws in the theme's colors. promptStyle picks the look, and off keeps Claude Code's own. Task notifications and teammate messages are left alone.
bubble (default) | bar | chevron |
|---|---|---|
| Rounded prompt bubble | Accent border | Bold chevron |
Hebrew and Arabic blocks are right aligned, with bullets, numbers and quote bars on the right and table columns mirrored, while code, numbers, paths and links stay left to right inside them. Terminals differ in how they treat right-to-left letters, so lumen detects yours and sends the letters the way it needs them. /lumen demo-rtl shows every element, and the rtl option forces a terminal's handling or turns it off.
Open /config and look for the lumen rows, or set values in ~/.claude/settings.json:
{
"pluginConfigs": {
"lumen": {
"theme": "tokyo-night",
"tableStyle": "grid",
"headingStyle": "banner",
"tableHeaderColor": "#ffcc00",
"numberColor": "cyan"
}
}
}
| Option | Values | Default |
|---|---|---|
enabled | true, false | true |
theme | see Themes | catppuccin-mocha |
tableStyle | box, rules, grid, minimal | box |
taskStyle | checks ([ ] [✓], done struck through), ticks (○ ✓), box (□ ✓), progress (ticks with a done-count bar) | checks |
promptStyle | bubble, bar, chevron, off | bubble |
headingStyle | banner, bold, underline, uppercase | banner |
highlightNumbers | true, false | true |
highlightPaths | true, false | true |
toolRows | true, false | true |
toolStyle | chat, tree-dim, tree-bold, classic | chat |
copyButtons | true, false | true |
diagramHints | true, false | true |
rtl | auto, a terminal (warp, kitty, apple-terminal, iterm, ghostty, wezterm, vscode, alacritty, windows-terminal, gnome, konsole), off | auto |
mermaid | true, false | true |
mermaidAscii | true, false | false |
<token>Color | any color, see below | theme |
A color is hex (#a6e3a1, #fc0), rgb(166,227,161), ansi256(114) or a name (green, cyanBright). Values that don't parse are ignored. Every token has a <token>Color option and a row in /config:
| Token | Colors |
|---|---|
accent | reply bullet, H3+ headings, quote bar, running tool dots |
heading | H1 and H2 |
strong | bold text |
emphasis | italic text, variables, attribute names |
inlineCode | inline code |
codeText | code block text |
codeCommand | shell commands, functions, class names, keys |
codeFlag | --flags, keywords, failures |
codeString | strings |
codeComment | comments, the code block's language label |
link | links, URLs, properties, tags |
path | file paths, regexes |
number | numbers, versions, durations, done dots |
quote | quote text |
rule | rules, chart gridlines |
tableHeader | table header cells |
tableRule | table rules |
bullet | list bullets and numbers |
diagram | diagram lines |
diagramText | diagram labels |
codeText.claude plugin validate .
claude plugin test .
hooks/extras.ts holds lumen's additions as pure functions (sparklines, gantt, state diagrams, animation frames); hooks/anim.tsx is the animation player, a Client surface module; tests/extras.test.tsx covers them. scripts/build-vendor.mjs rebuilds the vendored mermaid renderer and Prism (cd scripts && npm install first).
MIT. lumen is a fork of prismantis © Nahum Litvin; the original copyright notice is kept in LICENSE. Bundled third-party code is listed in docs/THIRD_PARTY_NOTICES.md.
hooks/register.tsx 267 lines1import { stripOutsideCode } from './metadata.js'
2import type { EngineInterface, Register, RenderElement } from 'claude-code'
3
4import { parse } from './markdown'
5import { boxArt, mermaidText, paint } from './mermaid'
6import { flowFrames, ganttRows, isSequence, segText, seqFrames, seqRows, stateToFlow, stepSpecs } from './extras'
7import type { Drawn } from './render'
8import { remember, renderBlocks, renderExpandedShell, renderToolGroup, renderToolRow, renderTurnDuration, renderUserPrompt, width } from './render'
9import { helpText, rtlShowcaseText, showcaseText } from './help'
10import { PRESET_NAMES } from './presets'
11import type { Style } from './theme'
12import { resolveStyle } from './theme'
13import type { Terminal } from './rtl'
14import { TERMINALS, hasRtl } from './rtl'
15
16const HINT = [
17 'Replies in this session are drawn by the lumen mod, which runs inside Claude Code and is not a command or tool to call: when the user asks to show something with lumen, write it as markdown in the reply.',
18 'Markdown tables, GitHub alerts (> [!WARNING], > [!NOTE]), fenced code with a language tag, and ```mermaid blocks render as colored terminal graphics:',
19 'flowcharts, sequence diagrams and xychart-beta bar or line charts.',
20 'When a reply carries a numeric series or a flow that is easier to see than read, add one small diagram or chart with short labels.',
21 'Also drawn here: ```mermaid gantt (timelines), stateDiagram-v2, and {spark: 3 5 9 4} for an inline sparkline in text or table cells.',
22 'To explain a flow step by step, animate it: add `%% step A->B "what happens"` lines to a flowchart (node ids, in order), or `%% animate` to a sequence diagram; the terminal plays it.',
23 'Diagrams must fit the terminal width: a sequence diagram needs about 14 columns per participant plus its longest message, so keep to 4-5 participants with short labels, or it is shown as a numbered list.',
24 'Skip diagrams for simple answers.',
25 'Put any command or snippet the user may run or copy in a fenced block with a language tag, never inline code: fenced blocks get a copy button, inline code does not.',
26].join(' ')
27
28const detectTerminal = async ($: EngineInterface): Promise<Terminal | null> => {
29 const program = await $.env.get('TERM_PROGRAM')
30 const term = await $.env.get('TERM')
31 if ((await $.env.get('KITTY_WINDOW_ID')) || term === 'xterm-kitty') return 'kitty'
32 if (program === 'Apple_Terminal') return 'apple-terminal'
33 if (program === 'WarpTerminal') return 'warp'
34 if (program === 'ghostty') return 'ghostty'
35 if (program === 'WezTerm') return 'wezterm'
36 if (program === 'vscode') return 'vscode'
37 if (program === 'iTerm.app') return 'iterm'
38 if (term === 'alacritty' || (await $.env.get('ALACRITTY_WINDOW_ID'))) return 'alacritty'
39 if (await $.env.get('WT_SESSION')) return 'windows-terminal'
40 if (await $.env.get('VTE_VERSION')) return 'gnome'
41 if (await $.env.get('KONSOLE_VERSION')) return 'konsole'
42 return null
43}
44
45const applyRtl = async ($: EngineInterface, style: Style): Promise<Terminal | null> => {
46 if (style.rtl === 'off') return null
47 if (style.rtl !== 'auto') return style.rtl
48 const terminal = await detectTerminal($)
49 style.reorder = terminal !== null
50 if (terminal) style.shape = TERMINALS[terminal]
51 return terminal
52}
53
54const expandedCalls = new Set<string>()
55/** local extension: prompt.submit carries no viewport, so the width is remembered from the last reply drawn. */
56const seen = { columns: 0 }
57/** local extension: long code blocks fold to FOLD_TO lines until expanded (keyed by their text). */
58const unfolded = new Set<string>()
59const FOLD_AT = 40
60const FOLD_TO = 12
61
62const drawMarkdown = ($: EngineInterface, el: ReturnType<EngineInterface['ui']['resolve']>, style: Style, blocks: ReturnType<typeof parse>, columns: number, reply?: string, live = false): RenderElement[] => {
63 const { Button } = el
64 const copy = (text: string | (() => string), key: string, label = '⧉ copy') =>
65 style.copyButtons ? (
66 <Button
67 key={key}
68 variant="primary"
69 label={label}
70 onPress={press => {
71 $.ui.copy({ text: typeof text === 'function' ? text() : text, surface: press.surface })
72 .then(r => $.ui.toast(r.isCopied ? 'Copied' : `Copy failed: ${r.reason}`))
73 .catch(() => $.ui.toast('Copy failed'))
74 }}
75 />
76 ) : null
77 const drawn: Drawn = new Map()
78 if (style.mermaid) {
79 for (const [i, block] of blocks.entries()) {
80 if (block.kind !== 'code' || block.lang.toLowerCase() !== 'mermaid') continue
81 const fits = (a: string | null): a is string => a !== null && a.split('\n').every(l => width(l) <= columns - 2)
82 const given = block.lines.join('\n')
83 // local extension: gantt timelines drawn as bars
84 const gantt = ganttRows(given, columns - 4, { heading: style.theme.heading, codeComment: style.theme.codeComment, codeFlag: style.theme.codeFlag, rule: style.theme.rule, palette: [style.theme.link, style.theme.number, style.theme.accent, style.theme.heading, style.theme.path].filter((c): c is string => !!c) })
85 if (gantt) {
86 drawn.set(i, { art: segText(gantt), element: (
87 <el.Box key={`b${i}`} flexDirection="column" paddingLeft={2}>
88 {gantt.map((row, r) => <el.Text key={`g${r}`}>{row.map((seg, k) => <el.Text key={`s${k}`} color={seg.color} bold={seg.bold} dimColor={seg.dim}>{seg.text}</el.Text>)}</el.Text>)}
89 </el.Box>
90 ) })
91 continue
92 }
93 // local extension: stateDiagram drawn as a flowchart (the renderer draws states poorly)
94 const src = stateToFlow(given) ?? given
95 let art = mermaidText(src, style.mermaidAscii, columns)
96 // local patch: a left-to-right flowchart too wide for the terminal is redrawn top-to-bottom
97 if (!fits(art) && /^\s*(flowchart|graph)\s+(LR|RL)\b/m.test(src)) art = mermaidText(src.replace(/^(\s*(?:flowchart|graph)\s+)(LR|RL)\b/m, '$1TD'), style.mermaidAscii, columns)
98 if (!fits(art)) {
99 // local extension: a sequence diagram too wide to draw becomes a numbered message list
100 const seq = seqRows(src)
101 if (seq) {
102 const t = style.theme
103 const w = Math.max(...seq.map(r => r.from.length))
104 const w2 = Math.max(...seq.map(r => r.to.length))
105 drawn.set(i, { art: seq.map(r => `${r.n}. ${r.from} ${r.reply ? '⇠' : '→'} ${r.to}: ${r.text}`).join('\n'), element: (
106 <el.Box key={`b${i}`} flexDirection="column" paddingLeft={2}>
107 {seq.map(r => (
108 <el.Box key={`q${r.n}`} flexDirection="row">
109 <el.Text color={t.codeComment}>{`${String(r.n).padStart(2)} `}</el.Text>
110 <el.Text color={t.link} bold>{r.from.padEnd(w)}</el.Text>
111 <el.Text color={r.reply ? t.codeComment : t.accent}>{r.reply ? ' ╌╌▷ ' : ' ──▶ '}</el.Text>
112 <el.Text color={t.number} bold>{r.to.padEnd(w2)}</el.Text>
113 <el.Text color={t.diagramText ?? t.codeText}>{` ${r.text}`}</el.Text>
114 </el.Box>
115 ))}
116 </el.Box>
117 ) })
118 }
119 continue
120 }
121 // local extension: `%% step` / `%% animate` play as an animation where a Client can run
122 const steps = live && el.Client ? stepSpecs(src) : null
123 const plain = art.replace(/[ \t]+$/gm, '')
124 const frames = steps ? (isSequence(src) ? seqFrames(plain, steps) : flowFrames(plain, src, steps)) : []
125 if (frames.length > 0 && frames.some(f => f.path.length > 0)) {
126 const lines = plain.split('\n')
127 const colors = paint(plain, style).map(row => row.map(c => c ?? null))
128 const t = style.theme
129 drawn.set(i, { art, element: <el.Client key={`b${i}`} module="./anim.tsx" props={{ lines, colors, frames, accent: t.accent ?? '#cba6f7', dim: t.codeComment ?? '#6c7086', text: t.diagramText ?? t.codeText ?? '#cdd6f4' }} /> })
130 } else drawn.set(i, { element: boxArt(el, style, art, `b${i}`), art })
131 }
132 }
133 // local extension: a long non-diagram code block is drawn folded, with a button to open it
134 for (const [i, block] of blocks.entries()) {
135 if (block.kind !== 'code' || drawn.has(i) || block.lines.length <= FOLD_AT || !el.Button) continue
136 const body = block.lines.join('\n')
137 const id = `${block.lang}:${block.lines.length}:${body.slice(0, 200)}:${body.slice(-200)}`
138 if (unfolded.has(id)) continue
139 const shown = { ...block, lines: block.lines.slice(0, FOLD_TO) }
140 const [head] = renderBlocks(el, style, [shown], columns, new Map(), copy && ((_t, k, l) => copy(body, k, l)))
141 drawn.set(i, { element: (
142 <el.Box key={`b${i}`} flexDirection="column" alignSelf="flex-start">
143 {head}
144 <el.Box paddingLeft={2}>
145 <el.Button key={`fold${i}`} plain label={`▸ ${block.lines.length - FOLD_TO} more lines`} onPress={() => { unfolded.add(id); $.ui.invalidate('ui.render') }} />
146 </el.Box>
147 </el.Box>
148 ) })
149 }
150 const elements = renderBlocks(el, style, blocks, columns, drawn, copy)
151 const button = reply === undefined ? null : copy(reply, 'reply', '⧉ copy reply')
152 return button ? [...elements, <el.Box key="reply" alignSelf="flex-end">{button}</el.Box>] : elements
153}
154
155export const register: Register = (on, options) => {
156 on('engine.create', async (_$, e, next) => ({ ...(await next(e)), lumen: { markdown: async () => undefined } }))
157 if (options.enabled === false) return
158 const style = resolveStyle(options)
159 const parsed = new Map<string, ReturnType<typeof parse>>()
160 const parseCached = (text: string, cache = parsed, limit?: number) => remember(cache, text, () => parse(text, { numbers: style.highlightNumbers, paths: style.highlightPaths }), limit)
161 const shared = new Map<string, ReturnType<typeof parse>>()
162 let terminal: Terminal | null = null
163 const fit = (viewport?: { isFullscreen?: boolean }): Style => (terminal === 'apple-terminal' && viewport?.isFullscreen ? { ...style, shape: 'inverse' } : style)
164
165 if (options.toolRows !== false) {
166 on('ui.render', { component: 'ToolGroup' }, ($, e, next) => {
167 if (e.props.isExpanded) {
168 for (const call of e.props.calls) if (call.tool_use_id) expandedCalls.add(call.tool_use_id)
169 return next(e)
170 }
171 return renderToolGroup($.ui.resolve(e), fit(e.viewport), e.props.calls, e.props.isActive, e.viewport?.columns)
172 })
173 on('ui.render', { component: 'ToolUse' }, ($, e, next) => {
174 if (!expandedCalls.has(e.props.tool_use_id)) return renderToolRow($.ui.resolve(e), fit(e.viewport), e.props, e.viewport?.columns)
175 return e.props.tool === 'Bash' || e.props.tool === 'PowerShell' ? renderExpandedShell($.ui.resolve(e), fit(e.viewport), e.props) : next(e)
176 })
177 }
178
179 on('session.start', async ($, e, next) => {
180 terminal = await applyRtl($, style)
181 const started = await next(e)
182 await $.command
183 .register({ name: 'lumen', description: 'Switch the lumen theme, copy the last reply, or show the demo', argumentHint: '[theme <name> | copy [code] | demo]' })
184 .catch(() => undefined)
185 return started
186 })
187
188 on('command.run', { command: 'lumen' }, async ($, e) => {
189 const [sub, name] = e.args.trim().split(/\s+/)
190 if (sub === 'demo') return { text: showcaseText(PRESET_NAMES) }
191 if (sub === 'copy') {
192 const reply = (await $.session.messages()).findLast(m => m.role === 'assistant' && m.text.trim())
193 if (!reply) return { text: 'Nothing to copy yet.' }
194 const code = name === 'code' ? parseCached(reply.text).findLast(b => b.kind === 'code') : undefined
195 if (name === 'code' && code?.kind !== 'code') return { text: 'The last reply has no code block.' }
196 const result = await $.ui.copy({ text: code?.kind === 'code' ? code.lines.join('\n') : reply.text })
197 return { text: result.isCopied ? `Copied the last ${code ? 'code block' : 'reply'}.` : `Copy failed: ${result.reason}` }
198 }
199 if (sub === 'demo-rtl') {
200 await applyRtl($, style)
201 return { text: rtlShowcaseText() }
202 }
203 if (sub !== 'theme' || !name) return { text: helpText(PRESET_NAMES) }
204 if (!(PRESET_NAMES as readonly string[]).includes(name)) return { text: `Unknown theme "${name}". Themes: ${PRESET_NAMES.join(', ')}` }
205 const result = await $.config.set({ key: `${$.plugin.name}.theme`, value: name })
206 return { text: result.deny ? `Could not switch theme: ${result.deny}` : `Theme set to ${name}.` }
207 })
208
209 on('ui.render', { component: 'TurnDuration' }, ($, e) => renderTurnDuration($.ui.resolve(e), style, e.props.word, e.props.durationMs))
210
211 on('prompt.submit', async ($, e, next) => {
212 await applyRtl($, style)
213 if (!style.diagramHints || (e.origin.kind !== 'composer' && e.origin.kind !== 'bridge')) return next(e)
214 const cols = seen.columns
215 return next({ ...e, context: [...(e.context ?? []), cols ? `${HINT} The terminal is ${cols} columns wide.` : HINT] })
216 })
217
218 on('ui.render', { component: 'CommandOutput' }, ($, e, next) => {
219 if (e.props.isErrored) return next(e)
220 const blocks = parseCached(e.props.text)
221 if (blocks.length === 0) return next(e)
222 const el = $.ui.resolve(e)
223 const { Box } = el
224 const columns = Math.max(20, (e.viewport?.columns ?? 100) - 4)
225 return <Box flexDirection="column" rowGap={1} {...(style.reorder && hasRtl(e.props.text) ? { width: '100%' } : {})}>{drawMarkdown($, el, fit(e.viewport), blocks, columns)}</Box>
226 })
227
228 on('ui.render', { component: 'UserMessage' }, ($, e, next) => {
229 const kind = e.props.origin.kind
230 const own = kind === 'composer' || kind === 'bridge' || (kind === 'unclassified' && !e.props.from && !e.props.task)
231 if (style.promptStyle === 'off' || !own) return next(e)
232 return renderUserPrompt($.ui.resolve(e), fit(e.viewport), e.props.text, Math.max(20, (e.viewport?.columns ?? 100) - 4))
233 })
234
235 on('ui.render', { component: 'AssistantMessage' }, ($, e, next) => {
236 e = { ...e, props: { ...e.props, text: stripOutsideCode(stripOutsideCode(stripOutsideCode(e.props.text,
237 /<insight\s+[^>]*?(?:\/>|>[\s\S]{0,4000}?<\/insight>)/g),
238 /<progress\b[^>]*?\/?>(?:<\/progress>)?/g), /<change-group\b[^>]*?\/>/g) } }
239
240 if (e.surface === 'terminal' && e.viewport?.columns) seen.columns = e.viewport.columns
241 const blocks = parseCached(e.props.text)
242 if (blocks.length === 0) return next(e)
243 const el = $.ui.resolve(e)
244 const { Box, Text } = el
245 const columns = Math.max(20, (e.viewport?.columns ?? 100) - 4)
246 const narration = style.toolStyle === 'tree-bold' && blocks.length === 1 && blocks[0]!.kind === 'paragraph'
247 return (
248 <Box flexDirection="row">
249 <Box width={2} flexShrink={0}>
250 <Text color={style.theme.accent}>{e.props.isFirstOfReply ? '●' : ' '}</Text>
251 </Box>
252 <Box flexDirection="column" rowGap={1} flexGrow={1}>
253 {drawMarkdown($, el, narration ? { ...fit(e.viewport), narration } : fit(e.viewport), blocks, columns, blocks.length > 1 || hasRtl(e.props.text) ? e.props.text : undefined, e.surface === 'terminal' && e.props.onScreen !== null)}
254 </Box>
255 </Box>
256 )
257 })
258
259 on('lumen.markdown', ($, e, next) => {
260 const blocks = parseCached(e.text, shared, 20)
261 if (blocks.length === 0) return next(e)
262 const el = $.ui.resolve({ surface: e.surface, component: 'AssistantMessage' })
263 const { Box } = el
264 return { value: <Box flexDirection="column" rowGap={1} {...(style.reorder && hasRtl(e.text) ? { width: '100%' } : {})}>{drawMarkdown($, el, { ...style, copyButtons: false }, blocks, Math.max(20, e.columns || 0))}</Box> }
265 })
266}
267hooks/metadata.ts 24 lines1/** Reporting tags inside Markdown code are examples. Preserve them verbatim. */
2export function codeSpans(text: string): readonly (readonly [number, number])[] {
3 const spans: [number, number][] = []
4 const re = /(`+|~{3,})/g
5 let m: RegExpExecArray | null
6 while ((m = re.exec(text))) {
7 const marker = m[0]
8 if (marker[0] === '~' && text.slice(text.lastIndexOf('\n', m.index) + 1, m.index).trim()) continue
9 const end = text.indexOf(marker, re.lastIndex)
10 // An unfinished code span/fence is still code while a reply streams.
11 const stop = end < 0 ? text.length : end + marker.length
12 spans.push([m.index, stop]); re.lastIndex = stop
13 }
14 return spans
15}
16export function outsideCode(text: string, tag: RegExp): RegExpMatchArray[] {
17 const spans = codeSpans(text)
18 return [...text.matchAll(tag)].filter(m => !spans.some(([a, b]) => m.index! >= a && m.index! < b))
19}
20export function stripOutsideCode(text: string, tag: RegExp): string {
21 for (const m of outsideCode(text, tag).reverse()) text = text.slice(0, m.index!) + text.slice(m.index! + m[0].length)
22 return text.replace(/\n{3,}/g, '\n\n')
23}
24hooks/markdown.ts 220 lines1import { SPARK, sparkValues, sparkline } from './extras'
2export type Inline =
3 | { kind: 'text'; text: string }
4 | { kind: 'strong'; children: Inline[] }
5 | { kind: 'emphasis'; children: Inline[] }
6 | { kind: 'strike'; children: Inline[] }
7 | { kind: 'code'; text: string }
8 | { kind: 'link'; text: string; href: string }
9 | { kind: 'number'; text: string }
10 | { kind: 'path'; text: string }
11 | { kind: 'dim'; text: string }
12
13export type Block = { raw: string } & (
14 | { kind: 'heading'; level: number; inline: Inline[] }
15 | { kind: 'paragraph'; inline: Inline[] }
16 | { kind: 'list'; ordered: boolean; items: { marker: string; depth: number; task?: boolean; inline: Inline[] }[] }
17 | { kind: 'code'; lang: string; lines: string[] }
18 | { kind: 'quote'; inline: Inline[] }
19 | { kind: 'alert'; level: AlertLevel; inline: Inline[] }
20 | { kind: 'rule' }
21 | { kind: 'table'; header: Inline[][]; align: ('left' | 'right' | 'center')[]; rows: Inline[][][] }
22)
23
24type Draft = Block extends infer B ? (B extends unknown ? Omit<B, 'raw'> : never) : never
25
26export type AlertLevel = 'note' | 'tip' | 'important' | 'warning' | 'caution'
27const ALERT = /^\[!(note|tip|important|warning|caution)\]\s*(.*)$/i
28
29export type Highlight = { numbers: boolean; paths: boolean }
30
31const MAX_INLINE = 4000
32const TABLE_SEP = /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/
33const LIST_ITEM = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/
34const FENCE = /^\s*(`{3,}|~{3,})\s*([\w+-]*)/
35
36const splitRow = (line: string): string[] => {
37 const trimmed = line.trim().replace(/^\|/, '').replace(/(?<!\\)\|$/, '')
38 const cells: string[] = []
39 let cell = ''
40 for (let i = 0; i < trimmed.length; i++) {
41 const ch = trimmed[i]
42 if (ch === '\\' && trimmed[i + 1] === '|') {
43 cell += '|'
44 i++
45 } else if (ch === '|') {
46 cells.push(cell.trim())
47 cell = ''
48 } else {
49 cell += ch
50 }
51 }
52 cells.push(cell.trim())
53 return cells
54}
55
56const INLINE = /(`+)(?!`)(.+?)(?<!`)\1(?!`)|\[([^\]]+)\]\(([^)\s]+)\)|\*\*([^*]+?)\*\*|(?<![\w_])__([^_]+?)__(?![\w_])|~~([^~]+?)~~|(?<![\w*])\*([^*\s][^*]*?)\*(?!\w)|(?<![\w_])_([^_\s][^_]*?)_(?!\w)|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g
57const NUMBER = /(?<![\w.#/-])(v?\d+(?:[.,:]\d+)*(?:%|ms|s|m|h|d|Gi|Mi|GB|MB|KB|x)?)(?![\w/])/g
58const PATH = /(?<![\w/.:])((?:~|\.{1,2})?\/[\w.@+-]+(?:\/[\w.@+-]*)*)/g
59
60const decorate = (text: string, hl: Highlight): Inline[] => {
61 // local extension: {spark: 3 5 9 4} → ▂▃▆▃, coloured like a number
62 if (text.includes('{spark:')) {
63 const out: Inline[] = []
64 let at = 0
65 for (const m of text.matchAll(SPARK)) {
66 if (m.index > at) out.push(...decorate(text.slice(at, m.index), hl))
67 out.push({ kind: 'number', text: sparkline(sparkValues(m[1]!)) })
68 at = m.index + m[0].length
69 }
70 if (out.length) { if (at < text.length) out.push(...decorate(text.slice(at), hl)); return out }
71 }
72 if (!hl.numbers && !hl.paths) return [{ kind: 'text', text }]
73 const marks: { start: number; end: number; kind: 'number' | 'path' }[] = []
74 if (hl.paths) for (const m of text.matchAll(PATH)) marks.push({ start: m.index, end: m.index + m[0].length, kind: 'path' })
75 if (hl.numbers) {
76 for (const m of text.matchAll(NUMBER)) {
77 const start = m.index
78 if (!marks.some(p => start < p.end && start + m[0].length > p.start)) {
79 marks.push({ start, end: start + m[0].length, kind: 'number' })
80 }
81 }
82 }
83 marks.sort((a, b) => a.start - b.start)
84 const out: Inline[] = []
85 let at = 0
86 for (const m of marks) {
87 if (m.start > at) out.push({ kind: 'text', text: text.slice(at, m.start) })
88 out.push({ kind: m.kind, text: text.slice(m.start, m.end) })
89 at = m.end
90 }
91 if (at < text.length) out.push({ kind: 'text', text: text.slice(at) })
92 return out
93}
94
95export const parseInline = (text: string, hl: Highlight): Inline[] => {
96 if (text.length > MAX_INLINE) return [{ kind: 'text', text }]
97 const out: Inline[] = []
98 let at = 0
99 for (const m of text.matchAll(INLINE)) {
100 if (m.index > at) out.push(...decorate(text.slice(at, m.index), hl))
101 if (m[2] !== undefined) out.push({ kind: 'code', text: m[2] })
102 else if (m[3] !== undefined) out.push({ kind: 'link', text: m[3], href: m[4] ?? "" })
103 else if (m[5] !== undefined || m[6] !== undefined) out.push({ kind: 'strong', children: parseInline(m[5] ?? m[6] ?? "", hl) })
104 else if (m[7] !== undefined) out.push({ kind: 'strike', children: parseInline(m[7], hl) })
105 else if (m[8] !== undefined || m[9] !== undefined) out.push({ kind: 'emphasis', children: parseInline(m[8] ?? m[9] ?? "", hl) })
106 else if (m[10] !== undefined) out.push({ kind: 'link', text: m[10], href: m[10] })
107 at = m.index + m[0].length
108 }
109 if (at < text.length) out.push(...decorate(text.slice(at), hl))
110 return out
111}
112
113export const inlineText = (inline: Inline[]): string =>
114 inline.map(n => ('children' in n ? inlineText(n.children) : n.text)).join('')
115
116export const parse = (source: string, hl: Highlight): Block[] => {
117 const lines = source.replace(/\r\n?/g, '\n').split('\n')
118 const at = (n: number) => lines[n] ?? ''
119 const blocks: Block[] = []
120 const add = (block: Draft, from: number, to: number) => blocks.push({ ...block, raw: lines.slice(from, to).join('\n') } as Block)
121 let paraStart = 0
122 let para: string[] = []
123
124 const flush = (end: number) => {
125 if (para.length) add({ kind: 'paragraph', inline: parseInline(para.join(' '), hl) }, paraStart, end)
126 para = []
127 }
128
129 for (let i = 0; i < lines.length; i++) {
130 const line = at(i)
131 const fence = FENCE.exec(line)
132 if (fence) {
133 flush(i)
134 const start = i
135 const run = fence[1] ?? '```'
136 const closer = new RegExp(`^\\s*\\${run[0]}{${run.length},}\\s*$`)
137 const body: string[] = []
138 i++
139 while (i < lines.length && !closer.test(at(i))) body.push(at(i++))
140 add({ kind: 'code', lang: fence[2] ?? '', lines: body }, start, i + 1)
141 continue
142 }
143 if (line.trim() === '') {
144 flush(i)
145 continue
146 }
147 const heading = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line)
148 if (heading) {
149 flush(i)
150 add({ kind: 'heading', level: (heading[1] ?? '#').length, inline: parseInline(heading[2] ?? '', hl) }, i, i + 1)
151 continue
152 }
153 if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
154 flush(i)
155 add({ kind: 'rule' }, i, i + 1)
156 continue
157 }
158 if (line.includes('|') && i + 1 < lines.length && TABLE_SEP.test(at(i + 1))) {
159 flush(i)
160 const start = i
161 const header = splitRow(line)
162 const align = splitRow(at(i + 1)).map(c =>
163 c.startsWith(':') && c.endsWith(':') ? 'center' : c.endsWith(':') ? 'right' : 'left',
164 )
165 i += 2
166 const rows: Inline[][][] = []
167 while (i < lines.length && at(i).includes('|') && at(i).trim() !== '') {
168 const cells = splitRow(at(i++))
169 rows.push(header.map((_, c) => parseInline(cells[c] ?? '', hl)))
170 }
171 add({ kind: 'table', header: header.map(c => parseInline(c, { numbers: false, paths: false })), align, rows }, start, i)
172 i--
173 continue
174 }
175 if (/^\s*>/.test(line)) {
176 flush(i)
177 const start = i
178 const body: string[] = []
179 while (i < lines.length && /^\s*>/.test(at(i))) body.push(at(i++).replace(/^\s*>\s?/, ''))
180 const alert = ALERT.exec(body[0] ?? '')
181 if (alert) add({ kind: 'alert', level: alert[1]!.toLowerCase() as AlertLevel, inline: parseInline([alert[2]!, ...body.slice(1)].join(' ').trim(), hl) }, start, i)
182 else add({ kind: 'quote', inline: parseInline(body.join(' '), hl) }, start, i)
183 i--
184 continue
185 }
186 const item = LIST_ITEM.exec(line)
187 if (item) {
188 flush(i)
189 const start = i
190 const ordered = /\d/.test(item[2] ?? '')
191 const items: { marker: string; depth: number; task?: boolean; inline: Inline[] }[] = []
192 const contentIndent: number[] = []
193 while (i < lines.length) {
194 const it = LIST_ITEM.exec(at(i))
195 if (it) {
196 contentIndent.push((it[1] ?? '').replace(/\t/g, ' ').length + (it[2] ?? '').length + 1)
197 const task = /^\[([ xX])\]\s+(.*)$/.exec(it[3] ?? '')
198 items.push({ marker: it[2] ?? '-', depth: Math.floor((it[1] ?? '').replace(/\t/g, ' ').length / 2), ...(task ? { task: task[1] !== ' ' } : {}), inline: parseInline(task ? task[2]! : it[3] ?? '', hl) })
199 } else if (/^\s{2,}\S/.test(at(i)) && items.length) {
200 const indent = (at(i).match(/^\s*/)?.[0] ?? '').replace(/\t/g, ' ').length
201 let owner = items.length - 1
202 while (owner > 0 && contentIndent[owner]! > indent) owner--
203 const target = items[owner]!
204 target.inline = [...target.inline, { kind: 'text', text: ' ' }, ...parseInline(at(i).trim(), hl)]
205 } else {
206 break
207 }
208 i++
209 }
210 add({ kind: 'list', ordered, items }, start, i)
211 i--
212 continue
213 }
214 if (!para.length) paraStart = i
215 para.push(line.trim())
216 }
217 flush(lines.length)
218 return blocks
219}
220hooks/mermaid.tsx 138 lines1import type { ElementTable, RenderElement } from 'claude-code'
2
3import { remember } from './render'
4import type { Style } from './theme'
5import { renderMermaidAscii, setChartSize } from './vendor/mermaid-text.js'
6
7const MAX_LINES = 80
8const textCache = new Map<string, string | null>()
9
10export const chartSize = (columns: number, source = '') => {
11 const labels = (/^\s*x-axis\b[^[\n]*\[([^\]\n]*)\]/m.exec(source)?.[1] ?? '').split(',').map(s => s.trim().replace(/^"|"$/g, ''))
12 const fit = labels.length * (Math.max(...labels.map(l => l.length)) + 2)
13 const width = Math.max(24, Math.min(60, Math.max(Math.floor(columns / 6), fit), columns - 12))
14 return { width, height: Math.max(8, Math.min(20, Math.round(width * 0.3))) }
15}
16
17const unquoteCategories = (source: string) =>
18 source.replace(/^(\s*x-axis\b[^[\n]*\[)([^\]\n]*)\]/m, (_, head: string, items: string) => `${head}${items.replace(/"([^"]*)"/g, (_q, s: string) => s.replaceAll(',', ' '))}]`)
19
20const labelBars = (art: string, source: string): string => {
21 const series = source.match(/^\s*(bar|line)\b.*$/gm) ?? []
22 const values = /^\s*bar\b[^[\n]*\[([^\]\n]*)\]/m.exec(source)?.[1]?.split(',').map(v => v.trim())
23 if (series.length !== 1 || !values || /^\s*xychart(-beta)?\s+horizontal/m.test(source)) return art
24 const grid = art.split('\n').map(l => [...l])
25 const axis = grid.findLastIndex(row => row.includes('┬'))
26 const ticks = grid[axis]?.flatMap((ch, x) => (ch === '┬' ? [x] : [])) ?? []
27 if (ticks.length !== values.length) return art
28 ticks.forEach((x, k) => {
29 const top = grid.findIndex(row => row[x] === '█')
30 const y = top === -1 ? axis - 1 : top - 1
31 const text = [...values[k]!]
32 const from = x - Math.floor((text.length - 1) / 2)
33 const row = grid[y]
34 if (!row || y < 0) return
35 while (row.length < from + text.length) row.push(' ')
36 if (!text.every((_, i) => /[ ·]/.test(row[from + i] ?? ' '))) return
37 text.forEach((ch, i) => (row[from + i] = ch))
38 })
39 return grid.map(row => row.join('').trimEnd()).join('\n')
40}
41
42export const mermaidText =(source: string, ascii: boolean, columns: number): string | null => {
43 if (source.length > 8000 || source.split('\n').length > MAX_LINES) return null
44 const isChart = /^\s*xychart/.test(source)
45 const size = chartSize(columns, isChart ? source : '')
46 const key = `${ascii}:${isChart ? size.width : 0}:${source}`
47 return remember(textCache, key, () => {
48 try {
49 if (isChart) setChartSize(size.width, size.height)
50 const art = renderMermaidAscii(unquoteCategories(source.replace(/^(\s*%%[^\n]*\n)+/, '')).replace(/(-->|-\.->|==>|---|-\.-|===)[ \t]+\|/g, '$1|'), { useAscii: ascii, colorMode: 'none', paddingX: 3, paddingY: 1 }).replace(/[ \t]+$/gm, '').trimEnd().replace(/▶/g, '►').replace(/◀/g, '◄')
51 return isChart ? labelBars(art, source) : art.split('\n').filter(l => !/^[\s│|]*$/.test(l)).join('\n')
52 } catch {
53 return null
54 }
55 })
56}
57
58const LINE = /[─-╿◇]/
59const ARROW = /[►◄▲▼]/
60
61export const paint = (art: string, style: Style): (string | undefined)[][] => {
62 const t = style.theme
63 const grid = art.split('\n').map(l => [...l])
64 const cell = (r: number, c: number) => grid[r]?.[c] ?? ''
65 const color: (string | undefined)[][] = grid.map(row => row.map(() => undefined))
66 const palette = [...new Set([t.link, t.number, t.heading, t.emphasis, t.path, t.codeFlag, t.accent])].filter((c): c is string => c !== undefined)
67 const next = (i: number) => palette[i % palette.length]
68 const labels = new Map<string, string | undefined>()
69
70 const rects: { r: number; c: number; r2: number; c2: number }[] = []
71 for (let r = 0; r < grid.length; r++) {
72 for (let c = 0; c < (grid[r]?.length ?? 0); c++) {
73 if (!/[┌╭(]/.test(cell(r, c))) continue
74 let c2 = c + 1
75 while (/[─┬┴┼▲▼]/.test(cell(r, c2))) c2++
76 if (!/[┐╮)]/.test(cell(r, c2)) || c2 === c + 1) continue
77 let r2 = r + 1
78 while (/[│├┤┼►◄]/.test(cell(r2, c))) r2++
79 if (!/[└╰(]/.test(cell(r2, c)) || !/[┘╯)]/.test(cell(r2, c2))) continue
80 rects.push({ r, c, r2, c2 })
81 }
82 }
83 const inside = (a: (typeof rects)[number], b: (typeof rects)[number]) => a !== b && b.r > a.r && b.r2 < a.r2 && b.c > a.c && b.c2 < a.c2
84 for (const box of rects.filter(a => !rects.some(b => inside(a, b)))) {
85 const { r, c, r2, c2 } = box
86 const label = grid.slice(r + 1, r2).map(row => row.slice(c + 1, c2).join('')).join(' ').trim()
87 if (!labels.has(label)) labels.set(label, next(labels.size))
88 const hue = labels.get(label)
89 for (let y = r; y <= r2; y++) for (let x = c; x <= c2; x++) if (cell(y, x).trim()) color[y]![x] = hue
90 }
91
92 const bars = [...new Set(grid.flatMap(row => row.flatMap((ch, c) => (ch === '█' && row[c - 1] !== '█' ? [c] : []))))].sort((a, b) => a - b)
93 const ticks = grid.findLast(row => row.includes('┬'))?.filter(ch => ch === '┬').length ?? 0
94 const tops = bars.map(x => grid.findIndex(row => row[x] === '█'))
95 const single = bars.length > 1 && bars.length <= ticks
96 const barColor = (i: number) => (single ? (tops[i] === Math.min(...tops) ? t.emphasis ?? t.accent : t.quote) : next(i))
97 grid.forEach((row, r) =>
98 row.forEach((ch, c) => {
99 if (color[r]![c] !== undefined) return
100 if (ch === '█') {
101 let start = c
102 while (row[start - 1] === '█') start--
103 color[r]![c] = barColor(bars.indexOf(start))
104 } else if (ch === '·') color[r]![c] = t.rule
105 else if (ARROW.test(ch)) color[r]![c] = t.accent
106 else if (LINE.test(ch)) color[r]![c] = t.diagram
107 else if (/^\d+[┤┼]/.test(row.slice(c).join(''))) color[r]![c] = t.number
108 else color[r]![c] = t.diagramText
109 }),
110 )
111 return color
112}
113
114const painted = new WeakMap<Style, Map<string, (string | undefined)[][]>>()
115
116export const boxArt = ({ Box, Text }: ElementTable, style: Style, art: string, key: string): RenderElement => {
117 const cache = painted.get(style) ?? new Map<string, (string | undefined)[][]>()
118 painted.set(style, cache)
119 const colors = remember(cache, art, () => paint(art, style))
120 return (
121 <Box key={key} flexDirection="column" paddingLeft={2}>
122 {art.split('\n').map((line, i) => {
123 const chars = [...line]
124 const parts: RenderElement[] = []
125 let at = 0
126 while (at < chars.length) {
127 const hue = colors[i]?.[at]
128 let end = at + 1
129 while (end < chars.length && colors[i]?.[end] === hue) end++
130 parts.push(<Text key={`t${parts.length}`} color={hue}>{chars.slice(at, end).join('')}</Text>)
131 at = end
132 }
133 return <Text key={`${key}.${i}`}>{parts.length ? parts : ' '}</Text>
134 })}
135 </Box>
136 )
137}
138hooks/extras.ts 325 lines1/* Local extensions to lumen (not upstream): inline sparklines, gantt timelines, state diagrams
2 drawn as flowcharts, and animated steps over a drawn mermaid diagram. Pure: no engine, no JSX. */
3
4// ── sparklines ────────────────────────────────────────────────────────────────
5export const SPARK = /\{spark:\s*(-?\d+(?:\.\d+)?(?:[\s,]+-?\d+(?:\.\d+)?)*)\s*\}/g
6const BARS = '▁▂▃▄▅▆▇█'
7export const sparkline = (values: readonly number[]): string => {
8 const v = values.slice(0, 200)
9 if (v.length === 0) return ''
10 const lo = Math.min(...v), hi = Math.max(...v)
11 return v.map(x => BARS[hi === lo ? 3 : Math.round(((x - lo) / (hi - lo)) * 7)]).join('')
12}
13export const sparkValues = (body: string): number[] => body.split(/[\s,]+/).filter(Boolean).map(Number).filter(Number.isFinite)
14
15// ── flowchart source: nodes, labels, edges ────────────────────────────────────
16export type Edge = { from: string; to: string; label: string }
17const NODE = /^([A-Za-z_][\w-]*)\s*(?:\(\(\s*(.*?)\s*\)\)|\(\[\s*(.*?)\s*\]\)|\[\[\s*(.*?)\s*\]\]|\[\(\s*(.*?)\s*\)\]|\[\s*(.*?)\s*\]|\(\s*(.*?)\s*\)|\{\{\s*(.*?)\s*\}\}|\{\s*(.*?)\s*\}|>\s*(.*?)\s*\])?/
18const ARROW = /^\s*(?:<?(?:-{2,}|-\.+-|={2,})[->ox]?|--\s*([^-|]+?)\s*--[->]?)\s*(?:\|\s*([^|]*?)\s*\|)?\s*/
19const SKIP = /^(subgraph|end|style|classDef|class|click|linkStyle|direction)\b/
20const unquote = (s: string) => s.replace(/^"(.*)"$/, '$1')
21
22export const flowGraph = (source: string): { labels: Map<string, string>; edges: Edge[] } => {
23 const labels = new Map<string, string>()
24 const edges: Edge[] = []
25 for (const raw of source.split('\n').slice(1)) {
26 const line = raw.replace(/%%.*$/, '').trim().replace(/;$/, '')
27 if (!line || SKIP.test(line) || line.includes('&')) continue
28 let rest = line
29 const node = (): string | null => {
30 const m = NODE.exec(rest)
31 if (!m) return null
32 const label = m.slice(2).find(x => x !== undefined)
33 if (label !== undefined) labels.set(m[1]!, unquote(label))
34 rest = rest.slice(m[0].length)
35 return m[1]!
36 }
37 let prev = node()
38 while (prev) {
39 const a = ARROW.exec(rest)
40 if (!a || a[0].trim() === '') break
41 rest = rest.slice(a[0].length)
42 const next = node()
43 if (!next) break
44 edges.push({ from: prev, to: next, label: unquote((a[2] ?? a[1] ?? '').trim()) })
45 prev = next
46 }
47 }
48 return { labels, edges }
49}
50
51// ── state diagrams → flowchart (the vendored renderer draws stateDiagram poorly) ─
52export const stateToFlow = (source: string): string | null => {
53 const lines = source.split('\n')
54 if (!/^\s*stateDiagram(-v2)?\s*$/.test(lines[0] ?? '')) return null
55 let dir = 'LR'
56 const labels = new Map<string, string>()
57 const order: string[] = []
58 const edges: string[] = []
59 const keep: string[] = []
60 const id = (s: string, side: 'from' | 'to') => (s === '[*]' ? (side === 'from' ? '_start' : '_end') : s)
61 const see = (s: string) => { if (!order.includes(s)) order.push(s) }
62 for (const raw of lines.slice(1)) {
63 const t = raw.trim()
64 if (!t) continue
65 if (t.startsWith('%%')) { keep.push(t.replace(/\[\*\]/g, '_start')); continue }
66 let m: RegExpExecArray | null
67 if ((m = /^direction\s+(LR|RL|TB|TD|BT)$/.exec(t))) { dir = m[1] === 'TB' ? 'TD' : m[1]!; continue }
68 if ((m = /^state\s+"([^"]+)"\s+as\s+([\w-]+)$/.exec(t))) { labels.set(m[2]!, m[1]!); see(m[2]!); continue }
69 if ((m = /^([\w-]+|\[\*\])\s*-->\s*([\w-]+|\[\*\])\s*(?::\s*(.+))?$/.exec(t))) {
70 const a = id(m[1]!, 'from'), b = id(m[2]!, 'to')
71 see(a); see(b)
72 edges.push(m[3] ? ` ${a} -->|${m[3].trim().replace(/\|/g, '/')}| ${b}` : ` ${a} --> ${b}`)
73 continue
74 }
75 if ((m = /^([\w-]+)\s*:\s*(.+)$/.exec(t))) { labels.set(m[1]!, m[2]!.trim()); see(m[1]!); continue }
76 return null // notes, composite states, forks: leave it to the stock renderer
77 }
78 if (edges.length === 0) return null
79 const decl = order.map(s => (s === '_start' ? ' _start((start))' : s === '_end' ? ' _end((end))' : ` ${s}[${(labels.get(s) ?? s).replace(/[[\]]/g, '')}]`))
80 return [`flowchart ${dir}`, ...decl, ...edges, ...keep].join('\n')
81}
82
83// ── steps: what an animation walks through ────────────────────────────────────
84export type StepSpec = { from: string; to: string; caption: string }
85const STEP = /^\s*%%\s*step\s+([\w-]+)\s*-+>+\s*([\w-]+)\s*(?:"([^"]*)"|:\s*(.*?))?\s*$/
86export const isSequence = (source: string) => /^\s*sequenceDiagram\b/.test(source)
87
88/** `%% step` lines, or with `%% animate` every edge (flowchart) in source order; null: not animated. */
89export const stepSpecs = (source: string): StepSpec[] | null => {
90 const lines = source.split('\n')
91 const explicit = lines.map(l => STEP.exec(l)).filter((m): m is RegExpExecArray => m !== null)
92 const auto = lines.some(l => /^\s*%%\s*animate\b/.test(l))
93 if (explicit.length === 0 && !auto) return null
94 if (isSequence(source)) return seqMessages(source)
95 if (explicit.length) return explicit.map(m => ({ from: m[1]!, to: m[2]!, caption: (m[3] ?? m[4] ?? '').trim() }))
96 const g = flowGraph(source)
97 return g.edges.map(e => ({ from: e.from, to: e.to, caption: e.label || `${g.labels.get(e.from) ?? e.from} → ${g.labels.get(e.to) ?? e.to}` }))
98}
99
100const seqMessages = (source: string): StepSpec[] =>
101 source.split('\n').map(l => /^\s*([\w-]+)\s*(?:-{1,2}>>|-{1,2}>|-{1,2}[x)])\s*[+-]?\s*([\w-]+)\s*:\s*(.*)$/.exec(l))
102 .filter((m): m is RegExpExecArray => m !== null)
103 .map(m => ({ from: m[1]!, to: m[2]!, caption: m[3]!.trim() }))
104
105// ── geometry of the drawn art ─────────────────────────────────────────────────
106export type Cell = [number, number]
107export type Rect = { r: number; c: number; r2: number; c2: number }
108export type Frame = { path: Cell[]; lit: Cell[]; rects: Rect[]; caption: string }
109
110const TL = '┌╭◯(', TR = '┐╮◯)', BL = '└╰◯(', BR = '┘╯◯)'
111export const findRects = (g: string[][]): Rect[] => {
112 const at = (r: number, c: number) => g[r]?.[c] ?? ''
113 const out: Rect[] = []
114 for (let r = 0; r < g.length; r++) for (let c = 0; c < (g[r]?.length ?? 0); c++) {
115 if (!TL.includes(at(r, c)) || at(r, c) === '') continue
116 let c2 = c + 1
117 while (/[─┬┴┼▲▼]/.test(at(r, c2))) c2++
118 if (c2 === c + 1 || !TR.includes(at(r, c2)) || at(r, c2) === '') continue
119 let r2 = r + 1
120 while (/[│├┤┼►◄]/.test(at(r2, c))) r2++
121 if (r2 === r + 1 || !BL.includes(at(r2, c)) || at(r2, c) === '' || !BR.includes(at(r2, c2)) || at(r2, c2) === '') continue
122 out.push({ r, c, r2, c2 })
123 }
124 return out
125}
126const inside = (x: Rect, r: number, c: number) => r >= x.r && r <= x.r2 && c >= x.c && c <= x.c2
127const interior = (g: string[][], x: Rect) =>
128 g.slice(x.r + 1, x.r2).map(row => row.slice(x.c + 1, x.c2).join('').trim()).filter(Boolean).join(' ')
129
130const rectFor = (g: string[][], rects: Rect[], label: string): Rect | undefined => {
131 const texts = rects.map(x => interior(g, x))
132 const i = texts.findIndex(t => t === label)
133 if (i >= 0) return rects[i]
134 const j = texts.findIndex(t => t.includes(label) || (label.length > 8 && t.includes(label.slice(0, 8))))
135 return j >= 0 ? rects[j] : undefined
136}
137const ring = (x: Rect): Cell[] => {
138 const out: Cell[] = []
139 for (let c = x.c; c <= x.c2; c++) out.push([x.r - 1, c], [x.r2 + 1, c])
140 for (let r = x.r; r <= x.r2; r++) out.push([r, x.c - 1], [r, x.c2 + 1])
141 return out
142}
143
144/** Shortest walk over drawn, non-box cells from beside box `a` to beside box `b`. */
145export const pathBetween = (g: string[][], rects: Rect[], a: Rect, b: Rect): Cell[] => {
146 const free = (r: number, c: number) => (g[r]?.[c] ?? ' ') !== ' ' && !rects.some(x => inside(x, r, c))
147 const goal = new Set(ring(b).filter(([r, c]) => free(r, c)).map(([r, c]) => `${r},${c}`))
148 const prev = new Map<string, string | null>()
149 const queue: Cell[] = []
150 for (const [r, c] of ring(a)) if (free(r, c) && !prev.has(`${r},${c}`)) { prev.set(`${r},${c}`, null); queue.push([r, c]) }
151 for (let i = 0; i < queue.length; i++) {
152 const [r, c] = queue[i]!
153 const k = `${r},${c}`
154 if (goal.has(k)) {
155 const path: Cell[] = []
156 for (let at: string | null | undefined = k; at; at = prev.get(at)) { const [y, x] = at.split(',').map(Number); path.push([y!, x!]) }
157 return path.reverse()
158 }
159 for (const [dr, dc] of [[0, 1], [0, -1], [1, 0], [-1, 0]] as const) {
160 const n = `${r + dr},${c + dc}`
161 if (!prev.has(n) && free(r + dr, c + dc)) { prev.set(n, k); queue.push([r + dr, c + dc]) }
162 }
163 }
164 return []
165}
166
167/** The frames of an animated flowchart: each step's boxes and the drawn path between them. */
168export const flowFrames = (art: string, source: string, steps: StepSpec[]): Frame[] => {
169 const g = art.split('\n').map(l => [...l])
170 const rects = findRects(g)
171 const { labels } = flowGraph(source)
172 return steps.map(s => {
173 const a = rectFor(g, rects, labels.get(s.from) ?? s.from)
174 const b = rectFor(g, rects, labels.get(s.to) ?? s.to)
175 return { path: a && b && a !== b ? pathBetween(g, rects, a, b) : [], lit: [], rects: [a, b].filter((x): x is Rect => x !== undefined), caption: s.caption }
176 })
177}
178
179/** The frames of an animated sequence diagram: each message's arrow row, tail to head, and its label. */
180export const seqFrames = (art: string, steps: StepSpec[]): Frame[] => {
181 const g = art.split('\n').map(l => [...l])
182 const rects = findRects(g)
183 const boxed = (r: number, c: number) => rects.some(x => inside(x, r, c))
184 const rows: number[] = []
185 for (let r = 0; r < g.length; r++) if (g[r]!.some((ch, c) => /[►◄]/.test(ch) && !boxed(r, c))) rows.push(r)
186 const top = Math.max(-1, ...rects.filter(x => x.r === Math.min(...rects.map(y => y.r))).map(x => x.r2))
187 return rows.slice(0, Math.max(steps.length, rows.length)).map((r, i) => {
188 const row = g[r]!
189 const head = row.findIndex((ch, c) => /[►◄]/.test(ch) && !boxed(r, c))
190 const path: Cell[] = []
191 const step = row[head] === '►' ? -1 : 1
192 for (let c = head; c >= 0 && c < row.length && /[─╌►◄┘┐]/.test(row[c]!); c += step) path.push([r, c])
193 path.reverse()
194 const lit: Cell[] = []
195 for (let y = (rows[i - 1] ?? top) + 1; y < r; y++) g[y]!.forEach((ch, c) => { if (/[^\s│├┤┬┴┼─┐┘╌]/.test(ch) && !boxed(y, c)) lit.push([y, c]) })
196 return { path, lit, rects: [], caption: steps[i]?.caption ?? '' }
197 })
198}
199
200// ── gantt timelines ───────────────────────────────────────────────────────────
201export type Seg = { text: string; color?: string; dim?: boolean; bold?: boolean }
202export type GanttTheme = { heading?: string; codeComment?: string; codeFlag?: string; rule?: string; palette: string[] }
203type Task = { label: string; section: number; start: number; end: number; tags: Set<string>; id?: string }
204const DAY = 86_400_000
205const UNIT: Record<string, number> = { ms: 1, s: 1e3, m: 6e4, h: 3.6e6, d: DAY, w: 7 * DAY }
206const DATE = /^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}(?::\d{2})?)?$/
207const TAGS = new Set(['done', 'active', 'crit', 'milestone'])
208const KEYWORDS = /^(title|dateFormat|axisFormat|tickInterval|excludes|includes|todayMarker|weekday|section)\b/
209
210export const parseGantt = (source: string): { title: string; sections: string[]; tasks: Task[]; dated: boolean } | null => {
211 const lines = source.split('\n')
212 if (!/^\s*gantt\s*$/.test(lines[0] ?? '')) return null
213 let title = ''
214 let dated = false
215 const sections: string[] = []
216 const tasks: Task[] = []
217 const byId = new Map<string, Task>()
218 const time = (x: string, fallback: number): number => {
219 const after = /^after\s+(.+)$/.exec(x)
220 if (after) return Math.max(...after[1]!.split(/\s+/).map(id => byId.get(id)?.end ?? NaN))
221 if (DATE.test(x)) { dated = true; return Date.parse(x.replace(' ', 'T') + (x.length > 10 ? 'Z' : 'T00:00Z')) }
222 if (/^\d+(\.\d+)?$/.test(x)) return Number(x) * DAY
223 return fallback
224 }
225 const endOf = (x: string, start: number): number => {
226 const d = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)$/.exec(x)
227 if (d) return start + Number(d[1]) * UNIT[d[2]!]!
228 if (DATE.test(x)) { dated = true; return Date.parse(x.replace(' ', 'T') + (x.length > 10 ? 'Z' : 'T00:00Z')) }
229 if (/^\d+(\.\d+)?$/.test(x)) return start + Number(x) * DAY
230 return NaN
231 }
232 for (const raw of lines.slice(1)) {
233 const t = raw.replace(/%%.*$/, '').trim()
234 if (!t) continue
235 let m: RegExpExecArray | null
236 if ((m = /^title\s+(.+)$/.exec(t))) { title = m[1]!; continue }
237 if ((m = /^section\s+(.+)$/.exec(t))) { sections.push(m[1]!); continue }
238 if (KEYWORDS.test(t)) continue
239 if (!(m = /^(.+?)\s*:\s*(.*)$/.exec(t))) continue
240 const items = m[2]!.split(',').map(s => s.trim()).filter(Boolean)
241 const tags = new Set<string>()
242 while (items.length && TAGS.has(items[0]!)) tags.add(items.shift()!)
243 const prevEnd = tasks.at(-1)?.end ?? 0
244 let id: string | undefined, start: number, end: number
245 if (items.length === 0) { start = prevEnd; end = start + DAY }
246 else if (items.length === 1) { start = prevEnd; end = endOf(items[0]!, start) }
247 else if (items.length === 2) { start = time(items[0]!, prevEnd); end = endOf(items[1]!, start) }
248 else { id = items[0]; start = time(items[1]!, prevEnd); end = endOf(items[2]!, start) }
249 if (tags.has('milestone')) end = start
250 if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) continue
251 const task: Task = { label: m[1]!.trim(), section: Math.max(0, sections.length - 1), start, end, tags, ...(id ? { id } : {}) }
252 tasks.push(task)
253 if (id) byId.set(id, task)
254 }
255 if (sections.length === 0) sections.push('')
256 return tasks.length ? { title, sections, tasks, dated } : null
257}
258
259const human = (ms: number): string => {
260 if (ms === 0) return ''
261 for (const [u, n] of [['w', 7 * DAY], ['d', DAY], ['h', 3.6e6], ['m', 6e4], ['s', 1e3]] as const)
262 if (ms >= n && ms % n === 0) return `${ms / n}${u}`
263 return ms >= DAY ? `${Math.round(ms / DAY)}d` : `${Math.round(ms / 3.6e6)}h`
264}
265const stamp = (t: number, dated: boolean, span: number): string => {
266 if (!dated) return `day ${Math.round(t / DAY)}`
267 const iso = new Date(t).toISOString()
268 return span < 2 * DAY ? iso.slice(11, 16) : iso.slice(5, 10)
269}
270
271/** Gantt rows as coloured segments, sized to `columns`; null when the source is not a gantt chart. */
272export const ganttRows = (source: string, columns: number, th: GanttTheme): Seg[][] | null => {
273 const g = parseGantt(source)
274 if (!g) return null
275 const t0 = Math.min(...g.tasks.map(t => t.start)), t1 = Math.max(...g.tasks.map(t => t.end))
276 const span = Math.max(1, t1 - t0)
277 const labelW = Math.max(6, Math.min(Math.max(...g.tasks.map(t => t.label.length)), Math.floor(columns * 0.35), 30))
278 const barW = Math.max(10, columns - labelW - 10)
279 const x = (t: number) => Math.round(((t - t0) / span) * barW)
280 const rows: Seg[][] = []
281 if (g.title) rows.push([{ text: g.title, color: th.heading, bold: true }])
282 let section = -1
283 for (const task of g.tasks) {
284 const hue = th.palette[task.section % th.palette.length]
285 if (task.section !== section && (g.sections.length > 1 || g.sections[0])) {
286 section = task.section
287 rows.push([{ text: `▌ ${g.sections[section]}`, color: hue, bold: true }])
288 }
289 const label = task.label.length > labelW ? task.label.slice(0, labelW - 1) + '…' : task.label.padEnd(labelW)
290 const a = Math.min(barW - 1, x(task.start))
291 const color = task.tags.has('crit') ? th.codeFlag : hue
292 const dim = task.tags.has('done')
293 if (task.tags.has('milestone')) {
294 rows.push([{ text: label + ' ' }, { text: ' '.repeat(a) }, { text: '◆', color, bold: true }, { text: ' ' + label.trim(), color: th.codeComment }])
295 continue
296 }
297 const b = Math.max(a + 1, Math.min(barW, x(task.end)))
298 rows.push([
299 { text: label + ' ' },
300 { text: ' '.repeat(a) },
301 { text: '█'.repeat(b - a), color, dim, bold: task.tags.has('active') },
302 { text: ' ' + human(task.end - task.start) + (task.tags.has('done') ? ' ✓' : task.tags.has('active') ? ' ◂' : ''), color: th.codeComment },
303 ])
304 }
305 const left = stamp(t0, g.dated, span), right = stamp(t1, g.dated, span)
306 rows.push([{ text: ' '.repeat(labelW + 1) }, { text: '└' + '─'.repeat(barW - 1), color: th.rule }])
307 rows.push([{ text: ' '.repeat(labelW + 1) }, { text: left + ' '.repeat(Math.max(1, barW - left.length - right.length)) + right, color: th.codeComment }])
308 return rows
309}
310export const segText = (rows: Seg[][]): string => rows.map(r => r.map(s => s.text).join('').trimEnd()).join('\n')
311
312// ── a sequence diagram too wide for the terminal, drawn as a numbered message list ─────────
313export type SeqRow = { n: number; from: string; to: string; text: string; reply: boolean }
314export const seqRows = (source: string): SeqRow[] | null => {
315 if (!isSequence(source)) return null
316 const names = new Map<string, string>()
317 for (const l of source.split('\n')) { const m = /^\s*(?:participant|actor)\s+([\w-]+)(?:\s+as\s+(.+))?$/.exec(l); if (m) names.set(m[1]!, (m[2] ?? m[1]!).trim()) }
318 const rows: SeqRow[] = []
319 for (const l of source.split('\n')) {
320 const m = /^\s*(\w+)\s*(--?)(>>|>|x|\))\s*[+-]?\s*(\w+)\s*:\s*(.*)$/.exec(l)
321 if (m) rows.push({ n: rows.length + 1, from: names.get(m[1]!) ?? m[1]!, to: names.get(m[4]!) ?? m[4]!, text: m[5]!.trim(), reply: m[2] === '--' })
322 }
323 return rows.length ? rows : null
324}
325hooks/render.tsx 756 lines1import type { ElementTable, RenderElement } from 'claude-code'
2
3import type { Block, Inline } from './markdown'
4import { inlineText } from './markdown'
5import { commentTail, commentVisual, flow, hasRtl, terminalLine } from './rtl'
6import type { Style, Theme } from './theme'
7import type { PrismToken } from './vendor/prism.js'
8import { languages, tokenize } from './vendor/prism.js'
9
10const WIDE = /[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]|\p{Extended_Pictographic}/u
11const segmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl ? new Intl.Segmenter() : undefined
12
13const graphemes = (s: string): string[] => (segmenter ? [...segmenter.segment(s)].map(g => g.segment) : [...s])
14
15export const width = (s: string): number =>
16 /^[ -~]*$/.test(s) ? s.length : graphemes(s).reduce((w, g) => (/^\p{M}+$/u.test(g) ? w : w + (WIDE.test(g) ? 2 : 1)), 0)
17
18const wrapRanges = (text: string, w: number): [number, number][] => {
19 if (width(text) <= w) return [[0, text.length]]
20 const out: [number, number][] = []
21 let line: [number, number] | undefined
22 for (const word of text.matchAll(/\S+/g)) {
23 let start = word.index
24 const end = start + word[0].length
25 while (width(text.slice(start, end)) > w) {
26 if (line) out.push(line)
27 line = undefined
28 let cut = start
29 for (const g of graphemes(text.slice(start, end))) {
30 if (cut > start && width(text.slice(start, cut + g.length)) > w) break
31 cut += g.length
32 }
33 out.push([start, cut])
34 start = cut
35 }
36 if (start === end) continue
37 if (line && width(text.slice(line[0], end)) <= w) line = [line[0], end]
38 else {
39 if (line) out.push(line)
40 line = [start, end]
41 }
42 }
43 if (line) out.push(line)
44 return out.length ? out : [[0, 0]]
45}
46
47const sliceInline = (nodes: Inline[], from: number, to: number): Inline[] => {
48 const out: Inline[] = []
49 let at = 0
50 for (const n of nodes) {
51 const length = inlineText([n]).length
52 const a = Math.max(from - at, 0)
53 const b = Math.min(to - at, length)
54 at += length
55 if (a >= b) continue
56 if ('children' in n) out.push({ ...n, children: sliceInline(n.children, a, b) })
57 else out.push({ ...n, text: n.text.slice(a, b) })
58 }
59 return out
60}
61
62const hasLink = (nodes: Inline[]): boolean => nodes.some(n => n.kind === 'link' || ('children' in n && hasLink(n.children)))
63
64const mostLines = (text: string, w: number): number => (text.match(/\S+/g)?.length ?? 0) + Math.ceil(width(text) / w)
65
66const wrapInline = (nodes: Inline[], w: number): Inline[][] => {
67 const text = inlineText(nodes)
68 return width(text) <= w ? [nodes] : wrapRanges(text, w).map(([a, b]) => sliceInline(nodes, a, b))
69}
70
71const flowOf = (style: Style, nodes: Inline[], columns: number) => (style.reorder ? flow(nodes, columns, width, style.shape) : null)
72
73const renderInline = (el: ElementTable, style: Style, nodes: Inline[], keyBase: string): RenderElement[] => {
74 const { Text } = el
75 const t = style.theme
76 return nodes.map((n, i) => {
77 const key = `${keyBase}.${i}`
78 switch (n.kind) {
79 case 'text':
80 return <Text key={key}>{n.text}</Text>
81 case 'strong':
82 return <Text key={key} bold color={t.strong}>{renderInline(el, style, n.children, key)}</Text>
83 case 'emphasis':
84 return <Text key={key} italic color={t.emphasis}>{renderInline(el, style, n.children, key)}</Text>
85 case 'strike':
86 return <Text key={key} strikethrough dimColor>{renderInline(el, style, n.children, key)}</Text>
87 case 'code':
88 return <Text key={key} color={t.inlineCode}>{n.text}</Text>
89 case 'link':
90 return /^[a-z][\w+.-]*:/i.test(n.href)
91 ? n.text === n.href ? <el.Link key={key} href={n.href} /> : <el.Link key={key} href={n.href}><Text color={t.link} underline>{n.text}</Text></el.Link>
92 : <Text key={key} color={t.link} underline>{n.text}</Text>
93 case 'number':
94 return <Text key={key} color={t.number}>{n.text}</Text>
95 case 'path':
96 return <Text key={key} color={t.path}>{n.text}</Text>
97 case 'dim':
98 return <Text key={key} dimColor>{n.text}</Text>
99 }
100 })
101}
102
103const renderFlow = (el: ElementTable, style: Style, lines: Inline[][], key: string, props: { italic?: boolean; color?: string; bold?: boolean } = {}) => {
104 const { Text } = el
105 return lines.map((line, i) => <Text key={`${key}.${i}`} {...props}>{renderInline(el, style, line, `${key}.${i}`)}</Text>)
106}
107
108const PRISM_COLORS: Record<string, keyof Theme> = {
109 comment: 'codeComment', prolog: 'codeComment', doctype: 'codeComment', cdata: 'codeComment',
110 string: 'codeString', char: 'codeString', 'template-string': 'codeString', 'attr-value': 'codeString', url: 'codeString',
111 number: 'number', boolean: 'number', constant: 'number', symbol: 'number', inserted: 'number',
112 keyword: 'codeFlag', important: 'codeFlag', atrule: 'codeFlag', rule: 'codeFlag', deleted: 'codeFlag',
113 function: 'codeCommand', 'class-name': 'codeCommand', builtin: 'codeCommand', key: 'codeCommand', selector: 'codeCommand',
114 property: 'link', tag: 'link', 'attr-name': 'emphasis', variable: 'emphasis', regex: 'path',
115}
116
117type Segment = { text: string; color?: string; italic: boolean }
118
119const flatten = (tokens: PrismToken[], style: Style, color?: string, italic = false): Segment[] =>
120 tokens.flatMap(token => {
121 if (typeof token === 'string') return [{ text: token, color, italic }]
122 const names = [token.type, ...(Array.isArray(token.alias) ? token.alias : token.alias ? [token.alias] : [])]
123 const slot = names.map(n => PRISM_COLORS[n]).find(Boolean)
124 const inner = Array.isArray(token.content) ? token.content : [token.type === 'comment' && typeof token.content === 'string' && style.reorder ? commentVisual(token.content, style.shape) : token.content]
125 return flatten(inner, style, slot ? style.theme[slot] : color, italic || token.type === 'comment')
126 })
127
128export const remember = <T,>(cache: Map<string, T>, key: string, make: () => T, limit = 200): T => {
129 const hit = cache.get(key)
130 if (hit !== undefined) return hit
131 const value = make()
132 cache.set(key, value)
133 if (cache.size > limit) cache.delete(cache.keys().next().value!)
134 return value
135}
136
137const highlighted = new WeakMap<Style, Map<string, Segment[][]>>()
138
139const grammarFor = (lang: string) => {
140 const name = lang.toLowerCase()
141 const grammar = Object.hasOwn(languages, name) ? languages[name] : undefined
142 return grammar !== null && typeof grammar === 'object' ? grammar : undefined
143}
144
145export const highlightBlock = ({ Text }: ElementTable, style: Style, lines: string[], lang: string, key: string): RenderElement[] | null => {
146 const grammar = grammarFor(lang)
147 if (!grammar) return null
148 const code = lines.join('\n')
149 const cache = highlighted.get(style) ?? new Map<string, Segment[][]>()
150 highlighted.set(style, cache)
151 const rows = remember(cache, `${lang}\0${code}`, () => {
152 const out: Segment[][] = [[]]
153 for (const seg of flatten(tokenize(code, grammar), style)) {
154 seg.text.split('\n').forEach((piece, i) => {
155 if (i > 0) out.push([])
156 if (piece) out[out.length - 1]!.push({ ...seg, text: piece })
157 })
158 }
159 return out
160 })
161 return rows.map((row, r) => (
162 <Text key={`${key}.${r}`} color={style.theme.codeText}>
163 {row.length ? row.map((s, i) => <Text key={`${key}.${r}.${i}`} color={s.color} italic={s.italic}>{s.text}</Text>) : ' '}
164 </Text>
165 ))
166}
167
168const isShellLang = (lang: string) => lang === '' || /^(sh|bash|zsh|shell|console|fish|powershell|ps1)$/i.test(lang)
169
170export const codeLine = (el: ElementTable, style: Style, line: string, lang: string, key: string): RenderElement => {
171 const { Text } = el
172 const t = style.theme
173 const isShell = isShellLang(lang)
174 if (/[\u2500-\u257F]/.test(line)) return <Text key={key} color={t.codeText}>{line}</Text>
175 const comment = style.reorder ? commentTail(line, style.shape) : null
176 if (comment) {
177 return (
178 <Text key={key} color={t.codeText}>
179 {comment.head ? codeLine(el, style, comment.head, lang, `${key}.h`) : null}
180 <Text color={t.codeComment}>{comment.marker + comment.tail}</Text>
181 </Text>
182 )
183 }
184 if (!isShell) return <Text key={key} color={t.codeText}>{line || ' '}</Text>
185 if (/^\s*#/.test(line)) return <Text key={key} color={t.codeComment}>{line}</Text>
186 const parts = line.split(/("[^"]*"|'[^']*'|\s+)/).filter(p => p !== '')
187 let seenCommand = false
188 return (
189 <Text key={key} color={t.codeText}>
190 {parts.map((p, i) => {
191 if (/^\s+$/.test(p)) return <Text key={`${key}.${i}`}>{p}</Text>
192 if (/^["']/.test(p)) return <Text key={`${key}.${i}`} color={t.codeString}>{p}</Text>
193 if (/^--?[\w-]/.test(p)) return <Text key={`${key}.${i}`} color={t.codeFlag}>{p}</Text>
194 if (!seenCommand && !/^[$>|&;]+$/.test(p)) {
195 seenCommand = true
196 return <Text key={`${key}.${i}`} color={t.codeCommand}>{p}</Text>
197 }
198 if (/^(\||&&|;|\|\|)$/.test(p)) seenCommand = false
199 return <Text key={`${key}.${i}`}>{p}</Text>
200 })}
201 </Text>
202 )
203}
204
205const longestWord = (text: string): number => Math.max(0, ...text.split(/\s+/).map(width))
206
207export const columnWidths = (natural: number[], available: number, gap: number, words: number[] = []): number[] => {
208 const room = Math.max(natural.length, available - gap * (natural.length - 1))
209 const total = natural.reduce((a, b) => a + b, 0)
210 if (total <= room) return natural
211 const minimum = natural.map((w, c) => Math.min(w, Math.max(1, words[c] ?? 1)))
212 const widths = natural.map(() => 0)
213 let open = natural.map((_, c) => c)
214 let left = room
215 for (let fixed = [-1]; fixed.length; ) {
216 const share = Math.floor(left / open.length)
217 const claim = (c: number) => (natural[c]! <= share ? natural[c]! : minimum[c]!)
218 const minimumsFit = open.reduce((a, c) => a + minimum[c]!, 0) <= left
219 const needed = open.reduce((a, c) => a + claim(c), 0)
220 fixed = open.filter(c => natural[c]! <= share)
221 if (minimumsFit && (!fixed.length || needed > left)) fixed = open.filter(c => minimum[c]! > share)
222 fixed.forEach(c => (widths[c] = claim(c), left -= widths[c]!))
223 open = open.filter(c => !fixed.includes(c))
224 }
225 open.forEach((c, i) => (widths[c] = Math.max(1, Math.floor(left / open.length) + (i < left % open.length ? 1 : 0))))
226 while (widths.reduce((a, b) => a + b, 0) > room) {
227 const widest = widths.indexOf(Math.max(...widths))
228 if (widths[widest]! <= 1) break
229 widths[widest]!--
230 }
231 return widths
232}
233
234const displayText = (inline: Inline[]): string =>
235 inline.map(n => (n.kind === 'link' && n.text !== n.href ? `${n.text} (${n.href})` : 'children' in n ? displayText(n.children) : n.text)).join('')
236
237const isRtlTable = (style: Style, block: Extract<Block, { kind: 'table' }>): boolean => {
238 const cells = [...block.header, ...block.rows.flat()].filter(cell => displayText(cell).trim() !== '')
239 return cells.filter(cell => flowOf(style, cell, Infinity)?.base === 'R').length * 2 > cells.length
240}
241
242const ART_WIDTH = 100
243
244export const tableArt = (block: Extract<Block, { kind: 'table' }>): string => {
245 const cells = [block.header, ...block.rows].map(r => block.header.map((_, c) => displayText(r[c] ?? [])))
246 const widths = columnWidths(
247 block.header.map((_, c) => Math.max(...cells.map(r => width(r[c]!)))),
248 ART_WIDTH - 4,
249 3,
250 block.header.map((_, c) => Math.max(...cells.map(r => longestWord(r[c]!)))),
251 )
252 const pad = (text: string, c: number, align: 'left' | 'right' | 'center') => {
253 const room = widths[c]! - width(text)
254 const left = align === 'right' ? room : align === 'center' ? Math.floor(room / 2) : 0
255 return ' '.repeat(left) + text + ' '.repeat(room - left)
256 }
257 const line = (l: string, m: string, r: string) => l + widths.map(w => '─'.repeat(w + 2)).join(m) + r
258 const row = (r: string[], header: boolean) => {
259 const lines = r.map((text, c) => wrapRanges(text, widths[c]!).map(([a, b]) => text.slice(a, b)))
260 return Array.from({ length: Math.max(...lines.map(l => l.length)) }, (_, i) =>
261 `│ ${lines.map((l, c) => pad(l[i] ?? '', c, header ? 'center' : block.align[c] ?? 'left')).join(' │ ')} │`)
262 }
263 const art = [line('┌', '┬', '┐'), ...row(cells[0]!, true), ...cells.slice(1).flatMap(r => [line('├', '┼', '┤'), ...row(r, false)]), line('└', '┴', '┘')]
264 return ['```', ...art, '```'].join('\n')
265}
266
267const renderTable = (el: ElementTable, style: Style, block: Extract<Block, { kind: 'table' }>, columns: number, key: string) => {
268 const { Box, Text } = el
269 const t = style.theme
270 const rtl = isRtlTable(style, block)
271 const box = style.tableStyle === 'box'
272 const gap = box ? 0 : style.tableStyle === 'grid' ? 3 : 2
273 const natural = block.header.map((h, c) =>
274 Math.max(width(displayText(h)), ...block.rows.map(r => width(displayText(r[c] ?? [])))),
275 )
276 const words = block.header.map((h, c) =>
277 Math.max(longestWord(displayText(h)), ...block.rows.map(r => longestWord(displayText(r[c] ?? [])))),
278 )
279 const widths = box ? columnWidths(natural, columns - 4, 3, words) : columnWidths(natural, columns, gap, words)
280 const order = natural.map((_, c) => c)
281 if (rtl) order.reverse()
282 const ruleChar = style.tableStyle === 'grid' ? '━' : '─'
283 const justify = (c: number) =>
284 block.align[c] === 'right' ? 'flex-end' : block.align[c] === 'center' ? 'center' : 'flex-start'
285
286 const rule = (k: string, heavy: boolean) => (
287 <Box key={k} flexDirection="row" columnGap={gap}>
288 {order.map(c => (
289 <Text key={`${k}.${c}`} color={t.tableRule} dimColor={!heavy && !t.tableRule}>
290 {(heavy ? ruleChar : '─').repeat(widths[c]!)}
291 </Text>
292 ))}
293 </Box>
294 )
295
296 const bar = (k: string, text: string, lines = 1) => (
297 <Text key={k} color={t.tableRule} dimColor={!t.tableRule}>{Array.from({ length: lines }, () => text).join('\n')}</Text>
298 )
299 const clipped = (lines: number) => (k: string, text: string) => (
300 <Box key={k} minWidth={width(text)}>
301 <Box position="absolute" top={0} bottom={0} left={0} minWidth={width(text)} overflow="hidden">
302 {bar(`${k}.b`, text, lines)}
303 </Box>
304 </Box>
305 )
306 const edge = (k: string, [left, fill, mid, right]: string) =>
307 bar(k, left + order.map(c => fill!.repeat(widths[c]! + 2)).join(mid) + right)
308
309 const terminalRow = (cells: Inline[][], k: string, isHeader: boolean) => {
310 const parts: Inline[] = box ? [{ kind: 'dim', text: '│ ' }] : []
311 order.forEach((c, i) => {
312 if (i > 0) parts.push(box ? { kind: 'dim', text: ' │ ' } : { kind: 'text', text: ' '.repeat(gap) })
313 const cell = flowOf({ ...style, shape: 'visual' }, cells[c] ?? [], Infinity)
314 const content = cell ? cell.lines[0]! : (cells[c] ?? [])
315 const pad = Math.max(0, widths[c]! - width(inlineText(content)))
316 const side = cell?.base === 'R' && block.align[c] !== 'center' ? 'flex-end' : justify(c)
317 const before = side === 'flex-end' ? pad : side === 'center' ? Math.floor(pad / 2) : 0
318 parts.push({ kind: 'text', text: ' '.repeat(before) }, ...(isHeader ? [{ kind: 'strong' as const, children: content }] : content), { kind: 'text', text: ' '.repeat(pad - before) })
319 })
320 if (box) parts.push({ kind: 'dim', text: ' │' })
321 return <Text key={k}>{renderInline(el, style, terminalLine(parts), k)}</Text>
322 }
323
324 const row = (cells: Inline[][], k: string, isHeader: boolean, border: (k: string, text: string) => RenderElement = bar) => rtl && style.shape === 'inverse' ? terminalRow(cells, k, isHeader) : (
325 <Box key={k} flexDirection="row" columnGap={gap} alignItems="stretch">
326 {box && border(`${k}.l`, '│ ')}
327 {order.map((c, i) => {
328 const w = widths[c]!
329 const cell = flowOf(style, cells[c] ?? [], Infinity)
330 const content = cell ? cell.lines[0]! : (cells[c] ?? [])
331 const side = cell?.base === 'R' && block.align[c] !== 'center' ? 'flex-end' : justify(c)
332 const cellBox = (
333 <Box key={`${k}.${c}`} width={w} flexShrink={0} justifyContent={side}>
334 {isHeader
335 ? <Text bold color={t.tableHeader}>{inlineText(content)}</Text>
336 : <Text>{renderInline(el, style, content, `${k}.${c}`)}</Text>}
337 </Box>
338 )
339 return box && i > 0 ? [border(`${k}.${c}s`, ' │ '), cellBox] : cellBox
340 })}
341 {box && border(`${k}.r`, ' │')}
342 </Box>
343 )
344
345 const wrappedRow = (cells: Inline[][], k: string, isHeader: boolean) => {
346 if (!isHeader && cells.some(hasLink)) return [row(cells, k, isHeader, clipped(Math.max(1, ...widths.map((w, c) => mostLines(displayText(cells[c] ?? []), w)))))]
347 const wrapped = widths.map((w, c) => {
348 const content = cells[c] ?? []
349 if (natural[c]! <= w) return [content]
350 return wrapInline(isHeader ? [{ kind: 'text', text: inlineText(content) }] : content, w)
351 })
352 return Array.from({ length: Math.max(...wrapped.map(l => l.length)) }, (_, i) =>
353 row(wrapped.map(l => l[i] ?? []), `${k}.${i}`, isHeader))
354 }
355
356 if (box) {
357 const lines: RenderElement[] = [edge(`${key}.t`, '┌─┬┐'), ...wrappedRow(block.header, `${key}.h`, true), edge(`${key}.hr`, '╞═╪╡')]
358 block.rows.forEach((r, i) => {
359 if (i > 0) lines.push(edge(`${key}.r${i}r`, '├─┼┤'))
360 lines.push(...wrappedRow(r, `${key}.r${i}`, false))
361 })
362 lines.push(edge(`${key}.b`, '└─┴┘'))
363 return <Box key={key} flexDirection="column" {...(rtl ? { alignSelf: 'flex-end' as const } : {})}>{lines}</Box>
364 }
365
366 const body: RenderElement[] = [row(block.header, `${key}.h`, true), rule(`${key}.hr`, true)]
367 block.rows.forEach((r, i) => {
368 body.push(row(r, `${key}.r${i}`, false))
369 if (style.tableStyle !== 'minimal' && i < block.rows.length - 1) body.push(rule(`${key}.r${i}r`, false))
370 })
371 return <Box key={key} flexDirection="column" {...(rtl ? { alignSelf: 'flex-end' as const } : {})}>{body}</Box>
372}
373
374const renderHeading = (el: ElementTable, style: Style, block: Extract<Block, { kind: 'heading' }>, key: string) => {
375 const rtl = flowOf(style, block.inline, Infinity)
376 if (!rtl) return drawHeading(el, style, block, block.inline, key)
377 const heading = drawHeading(el, style, block, rtl.lines[0]!, key)
378 return rtl.base === 'R' ? <el.Box key={key} alignSelf="flex-end">{heading}</el.Box> : heading
379}
380
381const drawHeading = (el: ElementTable, style: Style, block: Extract<Block, { kind: 'heading' }>, inline: Inline[], key: string) => {
382 const { Box, Text } = el
383 const t = style.theme
384 const color = block.level <= 2 ? t.heading : t.accent ?? t.heading
385 const label = inlineText(inline)
386 switch (style.headingStyle) {
387 case 'uppercase':
388 return <Text key={key} bold color={color}>{block.level === 1 ? label.toUpperCase() : label}</Text>
389 case 'underline':
390 return <Text key={key} bold underline={block.level <= 2} color={color}>{label}</Text>
391 case 'banner':
392 if (block.level === 1) return <Box key={key} alignSelf="flex-start" borderStyle="bold" borderColor={color} paddingX={1}><Text bold color={color}>{renderInline(el, style, inline, key)}</Text></Box>
393 return block.level === 2
394 ? <Box key={key} flexDirection="column" alignSelf="flex-start"><Text bold color={color}>{renderInline(el, style, inline, key)}</Text><Text color={color}>{'━'.repeat(width(label))}</Text></Box>
395 : <Text key={key} bold color={block.level === 3 ? color : t.strong}>{renderInline(el, style, inline, key)}</Text>
396 default:
397 return <Text key={key} bold color={color}>{renderInline(el, style, inline, key)}</Text>
398 }
399}
400
401const ALERT_COLOR = { note: 'blue', tip: 'green', important: 'magenta', warning: 'yellow', caution: 'red' } as const
402
403const renderParagraph = (el: ElementTable, style: Style, block: Extract<Block, { kind: 'paragraph' }>, columns: number, key: string) => {
404 const { Box, Text } = el
405 const rtl = flowOf(style, block.inline, columns)
406 if (rtl?.base === 'R') return <Box key={key} flexDirection="column" alignItems="flex-end">{renderFlow(el, style, rtl.lines, key)}</Box>
407 return <Text key={key} bold={style.narration}>{renderInline(el, style, rtl ? rtl.lines[0]! : block.inline, key)}</Text>
408}
409
410const renderQuote = (el: ElementTable, style: Style, block: Extract<Block, { kind: 'quote' }>, columns: number, key: string) => {
411 const { Box, Text } = el
412 const t = style.theme
413 const rtl = flowOf(style, block.inline, columns - 2)
414 if (rtl?.base === 'R') {
415 return (
416 <Box key={key} flexDirection="row" justifyContent="flex-end">
417 <Box flexDirection="column" alignItems="flex-end">{renderFlow(el, style, rtl.lines, key, { italic: true, color: t.quote })}</Box>
418 <Text color={t.accent}> │</Text>
419 </Box>
420 )
421 }
422 return (
423 <Box key={key} flexDirection="row">
424 <Text color={t.accent}>│ </Text>
425 <Text italic color={t.quote}>{renderInline(el, style, rtl ? rtl.lines[0]! : block.inline, key)}</Text>
426 </Box>
427 )
428}
429
430const renderAlert = (el: ElementTable, style: Style, block: Extract<Block, { kind: 'alert' }>, columns: number, key: string) => {
431 const { Box, Text } = el
432 const color = ALERT_COLOR[block.level]
433 const title = <Text bold color={color}>{block.level[0]!.toUpperCase() + block.level.slice(1)}</Text>
434 const rtl = flowOf(style, block.inline, columns - 4)
435 if (rtl?.base === 'R') {
436 return (
437 <Box key={key} flexDirection="column" alignSelf="flex-end" alignItems="flex-end" borderStyle="round" borderColor={color} paddingX={1}>
438 {title}
439 {renderFlow(el, style, rtl.lines, key)}
440 </Box>
441 )
442 }
443 const inline = rtl ? rtl.lines[0]! : block.inline
444 return (
445 <Box key={key} flexDirection="column" alignSelf="flex-start" borderStyle="round" borderColor={color} paddingX={1}>
446 {title}
447 {inline.length ? <Text>{renderInline(el, style, inline, key)}</Text> : null}
448 </Box>
449 )
450}
451
452const TASK_GLYPHS = { checks: ['[ ]', '[✓]'], ticks: ['○', '✓'], box: ['□', '✓'], progress: ['○', '✓'] } as const
453
454const renderList = (el: ElementTable, style: Style, block: Extract<Block, { kind: 'list' }>, columns: number, key: string) => {
455 const { Box, Text } = el
456 const t = style.theme
457 const tasks = block.items.filter(item => item.task !== undefined)
458 const done = tasks.filter(item => item.task).length
459 const filled = tasks.length ? Math.round((done / tasks.length) * 20) : 0
460 const strike = style.taskStyle === 'checks' || style.taskStyle === 'box'
461 const tick = style.taskStyle === 'ticks' || style.taskStyle === 'progress'
462 return (
463 <Box key={key} flexDirection="column">
464 {style.taskStyle === 'progress' && tasks.length > 0 && (
465 <Text>
466 <Text color={t.accent}>{'━'.repeat(filled)}</Text>
467 <Text color={t.bullet} dimColor>{'─'.repeat(20 - filled)}</Text>
468 <Text color={t.bullet}>{` ${done}/${tasks.length} done`}</Text>
469 </Text>
470 )}
471 {block.items.map((item, i) => {
472 const k = `${key}.${i}`
473 const glyph = item.task !== undefined ? TASK_GLYPHS[style.taskStyle][item.task ? 1 : 0] : /\d/.test(item.marker) ? item.marker : item.depth ? '◦' : '•'
474 const glyphColor = item.task && tick ? t.accent : t.bullet
475 const rtl = flowOf(style, item.inline, columns - item.depth * 2 - glyph.length - 1)
476 if (rtl?.base === 'R') {
477 return (
478 <Box key={k} flexDirection="row" justifyContent="flex-end" paddingRight={item.depth * 2}>
479 <Box flexDirection="column" alignItems="flex-end">{renderFlow(el, style, rtl.lines, k)}</Box>
480 <Text color={glyphColor}>{` ${glyph}`}</Text>
481 </Box>
482 )
483 }
484 return (
485 <Box key={k} flexDirection="row" paddingLeft={item.depth * 2}>
486 <Text color={glyphColor}>{`${glyph} `}</Text>
487 <Text dimColor={item.task === true} strikethrough={item.task === true && strike}>{renderInline(el, style, rtl ? rtl.lines[0]! : item.inline, k)}</Text>
488 </Box>
489 )
490 })}
491 </Box>
492 )
493}
494
495export type CopyButton = (text: string | (() => string), key: string, label?: string) => RenderElement | null
496export type Drawn = Map<number, { element: RenderElement; art?: string }>
497
498const copySource = (block: Block): string | undefined =>
499 block.kind === 'code' ? block.lines.join('\n') : block.kind === 'table' || block.kind === 'list' ? block.raw : block.kind === 'quote' || block.kind === 'alert' ? block.raw.split('\n').map(line => line.replace(/^\s*>\s?/, '')).join('\n') : undefined
500
501export const renderBlocks = (el: ElementTable, style: Style, blocks: Block[], columns: number, drawn: Drawn = new Map(), copy?: CopyButton): RenderElement[] => {
502 const { Box, Text } = el
503 const t = style.theme
504 const rendered = blocks.map((block, b) => {
505 const key = `b${b}`
506 switch (block.kind) {
507 case 'heading':
508 return renderHeading(el, style, block, key)
509 case 'paragraph':
510 return renderParagraph(el, style, block, columns, key)
511 case 'quote':
512 return renderQuote(el, style, block, columns, key)
513 case 'alert':
514 return renderAlert(el, style, block, columns, key)
515 case 'rule':
516 return <Text key={key} color={t.rule} dimColor={!t.rule}>{'─'.repeat(Math.max(8, Math.min(columns, 80)))}</Text>
517 case 'code':
518 return drawn.get(b)?.element ?? (
519 <Box key={key} flexDirection="column" alignSelf="flex-start">
520 <Box flexDirection="row" justifyContent="space-between" columnGap={4}>
521 <Text color={t.codeComment}>{`── ${block.lang || 'code'}`}</Text>
522 {copy?.(block.lines.join('\n'), `copy${b}`) ?? null}
523 </Box>
524 <Box flexDirection="column" paddingLeft={2}>
525 {(isShellLang(block.lang) ? null : highlightBlock(el, style, block.lines, block.lang, key)) ?? block.lines.map((line, i) => codeLine(el, style, line, block.lang, `${key}.${i}`))}
526 </Box>
527 </Box>
528 )
529 case 'list':
530 return renderList(el, style, block, columns, key)
531 case 'table':
532 return renderTable(el, style, block, columns, key)
533 }
534 })
535 const copied = rendered.map((element, b) => {
536 const block = blocks[b]
537 const text = block ? copySource(block) : undefined
538 const isPlainCode = block?.kind === 'code' && !drawn.has(b)
539 const art = drawn.get(b)?.art ?? (block?.kind === 'table' ? () => tableArt(block) : undefined)
540 const first = text === undefined || isPlainCode ? null : copy?.(text, `copy${b}`, art === undefined || block?.kind === 'table' ? undefined : '⧉ source')
541 const second = first && art !== undefined ? copy?.(art, `art${b}`, '⧉ art') : null
542 const button = second ? (
543 <el.Box key={`copies${b}`} flexDirection="row" columnGap={1}>
544 {first}
545 {second}
546 </el.Box>
547 ) : first
548 if (!button) return element
549 const { Box } = el
550 const rtl = style.reorder && block !== undefined && hasRtl(block.raw)
551 return block?.kind === 'quote' || block?.kind === 'alert' ? (
552 <Box key={`c${b}`} flexDirection="row" columnGap={2} {...(rtl ? { justifyContent: 'flex-end' as const } : {})}>
553 {element}
554 {button}
555 </Box>
556 ) : (
557 <Box key={`c${b}`} flexDirection="column" {...(rtl && (block?.kind === 'list' || (block?.kind === 'table' && isRtlTable(style, block))) ? {} : { alignSelf: 'flex-start' as const })}>
558 <Box justifyContent="flex-end">{button}</Box>
559 {element}
560 </Box>
561 )
562 })
563 const isFigure = (b: number) => blocks[b]?.kind === 'table' || drawn.has(b)
564 const out: RenderElement[] = []
565 for (let b = 0; b < rendered.length; b++) {
566 if (!isFigure(b) || !isFigure(b + 1)) {
567 out.push(copied[b]!)
568 continue
569 }
570 const start = b
571 while (isFigure(b + 1)) b++
572 out.push(
573 <Box key={`row${start}`} flexDirection="row" flexWrap="wrap" columnGap={4} rowGap={1}>
574 {copied.slice(start, b + 1).map((figure, i) => <Box key={`f${start + i}`} flexShrink={0}>{figure}</Box>)}
575 </Box>,
576 )
577 }
578 return out
579}
580
581export type ToolRow = { tool: string; input: unknown; isRunning: boolean; isErrored: boolean; isInterrupted: boolean }
582
583const VERBS: Record<string, string> = {
584 Bash: 'Ran', PowerShell: 'Ran', Read: 'Read', Write: 'Wrote', Edit: 'Edited', MultiEdit: 'Edited', NotebookEdit: 'Edited',
585 Grep: 'Searched', Glob: 'Listed', WebFetch: 'Fetched', WebSearch: 'Searched the web for', Agent: 'Delegated', Task: 'Delegated',
586}
587
588const field = (input: unknown, ...keys: string[]): string | undefined => {
589 if (input === null || typeof input !== 'object') return undefined
590 for (const k of keys) {
591 const v = (input as Record<string, unknown>)[k]
592 if (typeof v === 'string' && v.trim() !== '') return v
593 }
594 return undefined
595}
596
597const toolDim = (style: Style) => style.toolStyle !== "classic"
598
599const toolGutter = ({ Box, Text }: ElementTable, style: Style, color: string | undefined, running: boolean) =>
600 style.toolStyle.startsWith("tree")
601 ? <Box width={4} flexShrink={0}><Text color={color}>{' ⎿ '}</Text></Box>
602 : <Box width={2} flexShrink={0}><Text color={color}>{running ? '◌' : '●'}</Text></Box>
603
604const toolLayout = (el: ElementTable, style: Style, columns: number, label: string, color: string | undefined, running: boolean, text: RenderElement) => {
605 const { Box, Text } = el
606 if (style.toolStyle !== "chat") return <Box flexDirection="row">{toolGutter(el, style, color, running)}{text}</Box>
607 const w = Math.min(width(label) + 2, Math.max(20, Math.floor(columns * 0.6)))
608 return (
609 <Box flexDirection="row" justifyContent="flex-end" width="100%">
610 <Box width={w} flexDirection="row">{text}<Text color={color}>{running ? " ◌" : " ●"}</Text></Box>
611 </Box>
612 )
613}
614
615export const renderToolRow = (el: ElementTable, style: Style, row: ToolRow, columns = 100): RenderElement => {
616 const { Box, Text } = el
617 const t = style.theme
618 const isShell = row.tool === 'Bash' || row.tool === 'PowerShell'
619 const verb = VERBS[row.tool] ?? row.tool.replace(/^mcp__([^_]+)__/, '$1 ')
620 const target = isShell
621 ? field(row.input, 'command')?.split('\n')[0]
622 : field(row.input, 'file_path', 'notebook_path', 'path', 'pattern', 'url', 'query', 'description')
623 const dot = row.isErrored ? t.codeFlag : row.isInterrupted ? t.codeComment : row.isRunning ? t.accent : t.number
624 const isPath = target !== undefined && /^(~|\.{0,2}\/|[A-Za-z]:\\)/.test(target)
625
626 const label = `${verb}${target === undefined ? "" : ` ${target}`}${row.isInterrupted ? " interrupted" : row.isErrored ? " failed" : ""}`
627 return toolLayout(el, style, columns, label, dot, row.isRunning, (
628 <Text wrap="truncate-end" dimColor={toolDim(style)}>
629 <Text bold={!toolDim(style)} dimColor={toolDim(style)}>{verb}</Text>
630 {target === undefined ? null : <Text> </Text>}
631 {target === undefined ? null : isShell ? codeLine(el, style, target, 'bash', 'cmd') : <Text color={isPath ? t.path : t.inlineCode} dimColor={toolDim(style)}>{target}</Text>}
632 {row.isInterrupted ? <Text dimColor> interrupted</Text> : row.isErrored ? <Text color={t.codeFlag}> failed</Text> : null}
633 </Text>
634 ))
635}
636
637const OUTPUT_LINES = 120
638
639const lines = (value: unknown): string[] => (typeof value === 'string' && value !== '' ? value.replace(/\n$/, '').split('\n') : [])
640
641export const renderExpandedShell = (el: ElementTable, style: Style, row: ToolRow & { output?: unknown }): RenderElement => {
642 const { Box, Text } = el
643 const t = style.theme
644 const command = (field(row.input, 'command') ?? '').split('\n')
645 const out = row.output !== null && typeof row.output === 'object' ? (row.output as Record<string, unknown>) : {}
646 const stdout = lines(out.stdout)
647 const stderr = lines(out.stderr)
648 const shown = [...stdout.map(text => ({ text, color: undefined as string | undefined })), ...stderr.map(text => ({ text, color: t.codeFlag as string | undefined }))]
649 const visible = shown.slice(0, OUTPUT_LINES)
650 const dot = row.isErrored ? t.codeFlag : row.isInterrupted ? t.codeComment : row.isRunning ? t.accent : t.number
651 return (
652 <Box flexDirection="column">
653 <Box flexDirection="row">
654 <Box width={2} flexShrink={0}>
655 <Text color={dot}>{row.isRunning ? '◌' : '●'}</Text>
656 </Box>
657 <Box flexDirection="column">
658 {command.map((line, i) => (
659 <Text key={`c${i}`}>
660 {i === 0 ? <Text bold>{`${row.tool}(`}</Text> : null}
661 {codeLine(el, style, line, 'bash', `cmd${i}`)}
662 {i === command.length - 1 ? <Text bold>)</Text> : null}
663 </Text>
664 ))}
665 </Box>
666 </Box>
667 {row.isRunning ? null : (
668 <Box paddingLeft={2}>
669 <Box flexDirection="column" alignSelf="flex-start" borderStyle="round" borderColor={t.codeComment} paddingX={1}>
670 {visible.length === 0 ? <Text dimColor>(No output)</Text> : visible.map((l, i) => <Text key={`o${i}`} color={l.color}>{l.text === '' ? ' ' : l.text}</Text>)}
671 {shown.length > visible.length ? <Text dimColor>{`\u2026 +${shown.length - visible.length} lines`}</Text> : null}
672 </Box>
673 </Box>
674 )}
675 </Box>
676 )
677}
678
679const GROUPS: [RegExp, string, string][] = [
680 [/^(Bash|PowerShell)$/, 'ran', 'command'],
681 [/^Read$/, 'read', 'file'],
682 [/^(Write|Edit|MultiEdit|NotebookEdit)$/, 'edited', 'file'],
683 [/^(Grep|Glob)$/, 'searched', 'pattern'],
684 [/^(WebFetch|WebSearch)$/, 'fetched', 'page'],
685 [/^(Agent|Task)$/, 'delegated', 'task'],
686]
687
688export const groupSummary = (calls: readonly { tool: string }[]): string => {
689 const counts = new Map<string, number>()
690 for (const call of calls) {
691 const [, verb, noun] = GROUPS.find(([re]) => re.test(call.tool)) ?? [, 'used', call.tool.replace(/^mcp__([^_]+)__/, '$1 ')]
692 const label = `${verb} ${noun}`
693 counts.set(label, (counts.get(label) ?? 0) + 1)
694 }
695 const parts = [...counts].map(([label, n]) => {
696 const [verb, ...noun] = label.split(' ')
697 const name = noun.join(' ')
698 return `${verb} ${n} ${n === 1 ? name : name.endsWith('h') ? `${name}es` : `${name}s`}`
699 })
700 const text = parts.join(', ')
701 return text.charAt(0).toUpperCase() + text.slice(1)
702}
703
704export const renderToolGroup = (el: ElementTable, style: Style, calls: readonly ToolRow[], isActive: boolean, columns = 100): RenderElement => {
705 const { Box, Text } = el
706 const t = style.theme
707 const failed = calls.filter(c => c.isErrored).length
708 const running = isActive && calls.some(c => c.isRunning)
709 const dot = failed ? t.codeFlag : running ? t.accent : t.number
710 const last = calls[calls.length - 1]
711 const lastTarget = last ? field(last.input, 'command', 'file_path', 'notebook_path', 'path', 'pattern', 'url', 'query', 'description')?.split('\n')[0] : undefined
712 const label = `${groupSummary(calls)}${failed ? ` · ${failed} failed` : ""}${lastTarget ? ` · last: ${lastTarget}` : ""}`
713 return toolLayout(el, style, columns, label, dot, running, (
714 <Text wrap="truncate-end" dimColor={toolDim(style)}>
715 <Text bold={!toolDim(style)} dimColor={toolDim(style)}>{groupSummary(calls)}</Text>
716 {failed ? <Text color={t.codeFlag}>{` · ${failed} failed`}</Text> : null}
717 {lastTarget ? <Text dimColor>{` · last: ${lastTarget}`}</Text> : null}
718 </Text>
719 ))
720}
721
722export const formatDuration = (ms: number): string => {
723 const s = Math.round(ms / 1000)
724 if (s < 60) return `${Math.max(s, 0)}s`
725 if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`
726 return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`
727}
728
729export const renderTurnDuration = ({ Text }: ElementTable, style: Style, word: string, durationMs: number): RenderElement => (
730 <Text color={style.theme.codeComment}>
731 {`✻ ${word} for `}
732 <Text color={style.theme.number}>{formatDuration(durationMs)}</Text>
733 </Text>
734)
735
736export const renderUserPrompt = (el: ElementTable, style: Style, text: string, columns: number): RenderElement => {
737 const { Box, Text } = el
738 const t = style.theme
739 const color = style.promptStyle === 'chevron' ? t.accent : t.heading
740 const lines = text.split('\n').map(line => flowOf(style, [{ kind: 'text', text: line }], columns - 4))
741 const rtl = lines.some(l => l?.base === 'R')
742 const body = (
743 <Box flexDirection="column" {...(rtl ? { alignItems: 'flex-end' as const } : {})}>
744 {text.split('\n').map((line, i) => {
745 const flow = lines[i]
746 return flow ? renderFlow(el, style, flow.lines, `p${i}`, { color, bold: style.promptStyle === 'chevron' }) : <Text key={`p${i}`} color={color} bold={style.promptStyle === 'chevron'}>{line}</Text>
747 })}
748 </Box>
749 )
750 if (style.promptStyle === 'bubble') {
751 return <Box borderStyle="round" borderColor={t.accent} paddingX={1} alignSelf={rtl ? 'flex-end' : 'flex-start'}>{body}</Box>
752 }
753 const mark = <Text color={t.accent} bold>{style.promptStyle === 'bar' ? (rtl ? ' ▐' : '▌ ') : rtl ? ' ‹' : '› '}</Text>
754 return <Box flexDirection="row" {...(rtl ? { justifyContent: 'flex-end' as const } : {})}>{rtl ? body : mark}{rtl ? mark : body}</Box>
755}
756hooks/help.ts 175 lines1export const showcaseText = (themes: readonly string[]): string => `
2# lumen
3
4Colored markdown for Claude Code replies: **bold**, *italic*, ~~struck~~, \`inline code\`, a [link](https://claude.com/blog/claude-code-mods) and a bare URL https://claude.com/blog/claude-code-mods, both clickable, numbers like 99.9% and 250ms, paths like ~/src/app.ts.
5
6## Commands
7
8| Command | Does |
9|---------|------|
10| \`/lumen\` | This screen |
11| \`/lumen theme <name>\` | Switch theme on the spot |
12| \`/lumen copy\` | Copy the last reply, or \`copy code\` for its last code block |
13| \`/config\` | Edit any option |
14
15> [!NOTE]
16> ${themes.length} themes ship with it. \`mono\` uses no color, only bold and dim.
17
18### Pick a theme
19
20\`\`\`bash
21/lumen theme nord
22/lumen theme github-light
23\`\`\`
24
251. Dark: ${themes.filter(t => !/latte|light|dawn/.test(t) && t !== 'mono').join(', ')}
262. Light: ${themes.filter(t => /latte|light|dawn/.test(t)).join(', ')}
273. Plain
28 - mono
29
30> [!TIP]
31> Any color slot beats the theme. Set \`headingColor\` or \`numberColor\` to a hex value in \`/config\`.
32
33## Everything it draws
34
35### Alerts
36
37> [!IMPORTANT]
38> After updating the plugin, open sessions need \`/reload\`.
39
40> [!WARNING]
41> Terminals that copy on select (Warp) can turn a click on a copy button into a selection. Use the keyboard shortcut.
42
43> [!CAUTION]
44> Claude Code refuses trees over 20000 nodes. A code block past about 500 highlighted lines falls back to plain text.
45
46### Task lists
47
48- [x] Parse the reply
49- [x] Draw it
50 - [x] Tables and code
51 - [ ] Diagrams on the desktop app
52- [ ] Ship the next release
53
54### Your prompts
55
56Scroll up: what you typed draws in a bubble. \`promptStyle\` switches to \`bar\`, \`chevron\` or \`off\` in \`/config\`.
57
58### Tool rows
59
60Tool calls draw as one line, dimmed on the right by default. Set \`toolStyle\` in \`/config\` to \`chat\`, \`tree-dim\`, \`tree-bold\` or \`classic\`.
61
62### Quotes and rules
63
64> A quote keeps its text when you copy it, without the \`> \` markers.
65
66---
67
68### Code
69
70\`\`\`json
71{ "theme": "dracula", "headingStyle": "banner", "mermaid": true }
72\`\`\`
73
74#### Diagrams
75
76\`\`\`mermaid
77flowchart LR
78 R[Reply] --> P[Parse]
79 P --> D[Draw]
80 D --> S[Screen]
81\`\`\`
82
83\`\`\`mermaid
84sequenceDiagram
85 participant U as You
86 participant C as Claude
87 participant P as lumen
88 U->>C: prompt
89 C->>P: markdown
90 P-->>U: colored reply
91\`\`\`
92
93\`\`\`mermaid
94xychart-beta
95 title "Color options per group"
96 x-axis [text, head, num, code, diag]
97 y-axis "options" 0 --> 8
98 bar [7, 3, 2, 6, 2]
99\`\`\`
100`
101
102export const helpText = (themes: readonly string[]): string => `
103## Commands
104
105| Command | Does |
106|---------|------|
107| \`/lumen theme <name>\` | Switch theme on the spot |
108| \`/lumen copy\` | Copy the last reply, or \`copy code\` for its last code block |
109| \`/lumen demo\` | Full showcase, every element and diagram |
110| \`/lumen demo-rtl\` | Hebrew right-to-left showcase |
111
112### ${themes.length} themes
113
114- Dark: ${themes.filter(t => !/latte|light|dawn/.test(t) && t !== 'mono').join(', ')}
115- Light: ${themes.filter(t => /latte|light|dawn/.test(t)).join(', ')}
116- Plain: mono, no color, only bold and dim
117
118lumen is a fork of prismantis by Nahum Litvin: https://github.com/NahumLitvin/prismantis
119
120> [!TIP]
121> Any color slot beats the theme. Set \`headingColor\` or \`numberColor\` to a hex value in \`/config\`.
122
123### Task lists
124
125- [x] Tables, diagrams and charts drawn in the terminal
126- [x] A copy button on every block
127- [ ] Pick a \`taskStyle\` in \`/config\`: checks, ticks, box or progress
128
129\`\`\`mermaid
130flowchart LR
131 R[Reply] --> P[Parse]
132 P --> D[Draw]
133 D --> S[Screen]
134\`\`\`
135
136\`\`\`mermaid
137xychart-beta
138 title "Color options per group"
139 x-axis [text, head, num, code, diag]
140 y-axis "options" 0 --> 8
141 bar [7, 3, 2, 6, 2]
142\`\`\`
143
144> [!CAUTION]
145> Claude Code refuses trees over 20000 nodes. A code block past about 500 highlighted lines falls back to plain text.
146`
147
148export const rtlShowcaseText = (): string => `
149# עברית מימין לשמאל
150
151**שלום חברים**, זו הדגמה של עברית עם מונחים באנגלית כמו \`kubectl\`, מספרים כמו 99.9% ו-250ms, נתיב כמו ~/src/app.ts וגם [קישור](https://claude.com/blog/claude-code-mods).
152
153## רשימות
154
155- פרוסים בשני אזורים (us-east ו-eu-west)
156- מחליפים ערכת נושא עם \`/lumen theme nord\`
157
1581. מתקינים את התוסף
1592. שואלים שאלה בעברית
160
161> עברית נקראת מימין לשמאל, גם בטרמינל בלי תמיכה בכיווניות
162
163> [!TIP]
164> כל ערכת נושא עובדת גם בעברית
165
166| שירות | אזור | גרסה |
167| :--- | :--- | ---: |
168| שער | us-east | 2.14.0 |
169| חיוב | eu-west | 1.8.3 |
170
171\`\`\`bash
172ls -la # רשימת הקבצים בתיקייה
173\`\`\`
174`
175hooks/presets.ts 99 lines1import type { Theme } from './theme'
2
3export const PRESETS = {
4 'catppuccin-mocha': {
5 accent: '#cba6f7', heading: '#f9e2af', tableHeader: '#f9e2af', strong: '#ffffff', emphasis: '#f5c2e7',
6 inlineCode: '#89dceb', codeText: '#cdd6f4', codeCommand: '#89b4fa', codeFlag: '#f38ba8', codeString: '#a6e3a1',
7 codeComment: '#6c7086', link: '#89b4fa', path: '#89dceb', number: '#a6e3a1', quote: '#9399b2',
8 rule: '#45475a', tableRule: '#585b70', bullet: '#cba6f7', diagram: '#89b4fa', diagramText: '#cdd6f4',
9 },
10 'catppuccin-latte': {
11 accent: '#8839ef', heading: '#df8e1d', tableHeader: '#df8e1d', strong: '#11111b', emphasis: '#ea76cb',
12 inlineCode: '#04a5e5', codeText: '#4c4f69', codeCommand: '#1e66f5', codeFlag: '#d20f39', codeString: '#40a02b',
13 codeComment: '#9ca0b0', link: '#1e66f5', path: '#209fb5', number: '#40a02b', quote: '#6c6f85',
14 rule: '#bcc0cc', tableRule: '#acb0be', bullet: '#8839ef', diagram: '#1e66f5', diagramText: '#4c4f69',
15 },
16 dracula: {
17 accent: '#bd93f9', heading: '#bd93f9', tableHeader: '#f1fa8c', strong: '#f8f8f2', emphasis: '#ff79c6',
18 inlineCode: '#8be9fd', codeText: '#f8f8f2', codeCommand: '#50fa7b', codeFlag: '#ff79c6', codeString: '#f1fa8c',
19 codeComment: '#6272a4', link: '#8be9fd', path: '#ffb86c', number: '#bd93f9', quote: '#6272a4',
20 rule: '#44475a', tableRule: '#6272a4', bullet: '#ff79c6', diagram: '#bd93f9', diagramText: '#f8f8f2',
21 },
22 nord: {
23 accent: '#88c0d0', heading: '#88c0d0', tableHeader: '#ebcb8b', strong: '#eceff4', emphasis: '#b48ead',
24 inlineCode: '#8fbcbb', codeText: '#d8dee9', codeCommand: '#88c0d0', codeFlag: '#81a1c1', codeString: '#a3be8c',
25 codeComment: '#4c566a', link: '#88c0d0', path: '#8fbcbb', number: '#b48ead', quote: '#81a1c1',
26 rule: '#3b4252', tableRule: '#4c566a', bullet: '#88c0d0', diagram: '#81a1c1', diagramText: '#d8dee9',
27 },
28 'tokyo-night': {
29 accent: '#bb9af7', heading: '#7dcfff', tableHeader: '#e0af68', strong: '#c0caf5', emphasis: '#f7768e',
30 inlineCode: '#7dcfff', codeText: '#a9b1d6', codeCommand: '#7aa2f7', codeFlag: '#bb9af7', codeString: '#9ece6a',
31 codeComment: '#565f89', link: '#73daca', path: '#2ac3de', number: '#ff9e64', quote: '#9aa5ce',
32 rule: '#414868', tableRule: '#565f89', bullet: '#bb9af7', diagram: '#7aa2f7', diagramText: '#c0caf5',
33 },
34 'gruvbox-dark': {
35 accent: '#d3869b', heading: '#fabd2f', tableHeader: '#fabd2f', strong: '#fbf1c7', emphasis: '#d3869b',
36 inlineCode: '#8ec07c', codeText: '#ebdbb2', codeCommand: '#83a598', codeFlag: '#fe8019', codeString: '#b8bb26',
37 codeComment: '#928374', link: '#83a598', path: '#8ec07c', number: '#d3869b', quote: '#a89984',
38 rule: '#3c3836', tableRule: '#504945', bullet: '#fe8019', diagram: '#83a598', diagramText: '#ebdbb2',
39 },
40 'gruvbox-light': {
41 accent: '#8f3f71', heading: '#b57614', tableHeader: '#b57614', strong: '#282828', emphasis: '#8f3f71',
42 inlineCode: '#427b58', codeText: '#3c3836', codeCommand: '#076678', codeFlag: '#af3a03', codeString: '#79740e',
43 codeComment: '#928374', link: '#076678', path: '#427b58', number: '#8f3f71', quote: '#7c6f64',
44 rule: '#ebdbb2', tableRule: '#a89984', bullet: '#af3a03', diagram: '#076678', diagramText: '#3c3836',
45 },
46 'rose-pine': {
47 accent: '#c4a7e7', heading: '#c4a7e7', tableHeader: '#f6c177', strong: '#e0def4', emphasis: '#ebbcba',
48 inlineCode: '#9ccfd8', codeText: '#e0def4', codeCommand: '#9ccfd8', codeFlag: '#eb6f92', codeString: '#f6c177',
49 codeComment: '#6e6a86', link: '#c4a7e7', path: '#9ccfd8', number: '#ebbcba', quote: '#908caa',
50 rule: '#26233a', tableRule: '#6e6a86', bullet: '#eb6f92', diagram: '#c4a7e7', diagramText: '#e0def4',
51 },
52 'rose-pine-dawn': {
53 accent: '#907aa9', heading: '#907aa9', tableHeader: '#ea9d34', strong: '#464261', emphasis: '#d7827e',
54 inlineCode: '#56949f', codeText: '#464261', codeCommand: '#56949f', codeFlag: '#b4637a', codeString: '#ea9d34',
55 codeComment: '#9893a5', link: '#907aa9', path: '#286983', number: '#d7827e', quote: '#797593',
56 rule: '#f2e9e1', tableRule: '#9893a5', bullet: '#b4637a', diagram: '#907aa9', diagramText: '#464261',
57 },
58 everforest: {
59 accent: '#a7c080', heading: '#e69875', tableHeader: '#dbbc7f', strong: '#d3c6aa', emphasis: '#d699b6',
60 inlineCode: '#83c092', codeText: '#d3c6aa', codeCommand: '#7fbbb3', codeFlag: '#e67e80', codeString: '#a7c080',
61 codeComment: '#859289', link: '#7fbbb3', path: '#83c092', number: '#d699b6', quote: '#9da9a0',
62 rule: '#475258', tableRule: '#7a8478', bullet: '#e69875', diagram: '#7fbbb3', diagramText: '#d3c6aa',
63 },
64 'github-dark': {
65 accent: '#d2a8ff', heading: '#79c0ff', tableHeader: '#ffa657', strong: '#e6edf3', emphasis: '#d2a8ff',
66 inlineCode: '#79c0ff', codeText: '#e6edf3', codeCommand: '#d2a8ff', codeFlag: '#ff7b72', codeString: '#a5d6ff',
67 codeComment: '#7d8590', link: '#58a6ff', path: '#7ee787', number: '#79c0ff', quote: '#7d8590',
68 rule: '#30363d', tableRule: '#8b949e', bullet: '#ff7b72', diagram: '#58a6ff', diagramText: '#e6edf3',
69 },
70 'github-light': {
71 accent: '#8250df', heading: '#0550ae', tableHeader: '#953800', strong: '#1f2328', emphasis: '#8250df',
72 inlineCode: '#0550ae', codeText: '#1f2328', codeCommand: '#8250df', codeFlag: '#cf222e', codeString: '#0a3069',
73 codeComment: '#6e7781', link: '#0969da', path: '#116329', number: '#0550ae', quote: '#656d76',
74 rule: '#d0d7de', tableRule: '#6e7781', bullet: '#cf222e', diagram: '#0969da', diagramText: '#1f2328',
75 },
76 'one-dark': {
77 accent: '#c678dd', heading: '#e06c75', tableHeader: '#e5c07b', strong: '#d19a66', emphasis: '#c678dd',
78 inlineCode: '#56b6c2', codeText: '#abb2bf', codeCommand: '#61afef', codeFlag: '#e06c75', codeString: '#98c379',
79 codeComment: '#5c6370', link: '#61afef', path: '#56b6c2', number: '#d19a66', quote: '#828997',
80 rule: '#3a3f4b', tableRule: '#5c6370', bullet: '#c678dd', diagram: '#61afef', diagramText: '#abb2bf',
81 },
82 'solarized-dark': {
83 accent: '#6c71c4', heading: '#b58900', tableHeader: '#b58900', strong: '#eee8d5', emphasis: '#d33682',
84 inlineCode: '#2aa198', codeText: '#93a1a1', codeCommand: '#268bd2', codeFlag: '#cb4b16', codeString: '#859900',
85 codeComment: '#586e75', link: '#268bd2', path: '#2aa198', number: '#859900', quote: '#657b83',
86 rule: '#073642', tableRule: '#586e75', bullet: '#6c71c4', diagram: '#268bd2', diagramText: '#93a1a1',
87 },
88 'solarized-light': {
89 accent: '#6c71c4', heading: '#b58900', tableHeader: '#b58900', strong: '#073642', emphasis: '#d33682',
90 inlineCode: '#2aa198', codeText: '#586e75', codeCommand: '#268bd2', codeFlag: '#cb4b16', codeString: '#859900',
91 codeComment: '#93a1a1', link: '#268bd2', path: '#2aa198', number: '#859900', quote: '#839496',
92 rule: '#eee8d5', tableRule: '#93a1a1', bullet: '#6c71c4', diagram: '#268bd2', diagramText: '#586e75',
93 },
94 mono: {} as Theme,
95} satisfies Record<string, Theme>
96
97export type PresetName = keyof typeof PRESETS
98export const PRESET_NAMES = Object.keys(PRESETS) as PresetName[]
99hooks/theme.ts 66 lines1import type { PluginOptions } from 'claude-code'
2
3import { PRESETS } from './presets'
4import type { Shape, Terminal } from './rtl'
5import { TERMINALS } from './rtl'
6
7export const TOKENS = [
8 'accent', 'heading', 'strong', 'emphasis', 'inlineCode', 'codeText', 'codeCommand', 'codeFlag', 'codeString', 'codeComment',
9 'link', 'path', 'number', 'quote', 'rule', 'tableHeader', 'tableRule', 'bullet', 'diagram', 'diagramText',
10] as const
11
12export type Theme = Partial<Record<(typeof TOKENS)[number], string>>
13
14export type Style = {
15 theme: Theme
16 headingStyle: 'bold' | 'underline' | 'uppercase' | 'banner'
17 tableStyle: 'box' | 'rules' | 'grid' | 'minimal'
18 taskStyle: 'checks' | 'ticks' | 'box' | 'progress'
19 promptStyle: 'bubble' | 'bar' | 'chevron' | 'off'
20 toolStyle: 'chat' | 'tree-dim' | 'tree-bold' | 'classic'
21 narration?: boolean
22 highlightNumbers: boolean
23 highlightPaths: boolean
24 mermaid: boolean
25 mermaidAscii: boolean
26 copyButtons: boolean
27 diagramHints: boolean
28 rtl: 'auto' | Terminal | 'off'
29 reorder: boolean
30 shape: Shape
31}
32
33const COLOR = /^(#[0-9a-f]{3}|#[0-9a-f]{6}|rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)|ansi256\(\d{1,3}\)|(black|red|green|yellow|blue|magenta|cyan|white|gray|grey)(Bright)?)$/i
34
35export const isColor = (value: unknown): value is string => typeof value === 'string' && COLOR.test(value.trim())
36
37const pick = <T extends string>(value: unknown, allowed: readonly T[], fallback: T): T =>
38 allowed.includes(value as T) ? (value as T) : fallback
39
40export const resolveStyle = (options: PluginOptions): Style => {
41 const base: Theme = (PRESETS as Record<string, Theme>)[String(options.theme)] ?? PRESETS['catppuccin-mocha']
42 const fromFields = Object.fromEntries(
43 TOKENS.filter(k => isColor(options[`${k}Color`])).map(k => [k, String(options[`${k}Color`]).trim()]),
44 )
45
46 const rtl = pick(options.rtl, ['auto', 'off', ...(Object.keys(TERMINALS) as Terminal[])], 'auto')
47
48 return {
49 theme: { ...base, ...fromFields },
50 headingStyle: pick(options.headingStyle, ['bold', 'underline', 'uppercase', 'banner'] as const, 'banner'),
51 tableStyle: pick(options.tableStyle, ['box', 'rules', 'grid', 'minimal'] as const, 'box'),
52 taskStyle: pick(options.taskStyle, ['checks', 'ticks', 'box', 'progress'] as const, 'checks'),
53 promptStyle: pick(options.promptStyle, ['bubble', 'bar', 'chevron', 'off'] as const, 'bubble'),
54 toolStyle: pick(options.toolStyle, ['chat', 'tree-dim', 'tree-bold', 'classic'] as const, 'chat'),
55 highlightNumbers: options.highlightNumbers !== false,
56 highlightPaths: options.highlightPaths !== false,
57 mermaid: options.mermaid !== false,
58 mermaidAscii: options.mermaidAscii === true,
59 copyButtons: options.copyButtons !== false,
60 diagramHints: options.diagramHints !== false && options.mermaid !== false,
61 rtl,
62 reorder: rtl !== 'auto' && rtl !== 'off',
63 shape: rtl === 'auto' || rtl === 'off' ? 'visual' : TERMINALS[rtl],
64 }
65}
66hooks/rtl.ts 232 lines1import type { Inline } from './markdown'
2
3type Dir = 'R' | 'L'
4export type Flow = { base: Dir; lines: Inline[][] }
5export type Shape = 'visual' | 'words' | 'logical' | 'inverse'
6
7export const TERMINALS = {
8 warp: 'visual',
9 ghostty: 'visual',
10 wezterm: 'visual',
11 vscode: 'visual',
12 alacritty: 'visual',
13 'windows-terminal': 'visual',
14 'apple-terminal': 'logical',
15 kitty: 'words',
16 iterm: 'logical',
17 gnome: 'logical',
18 konsole: 'logical',
19} as const satisfies Record<string, Shape>
20
21export type Terminal = keyof typeof TERMINALS
22
23type Fmt = { wrap: ('strong' | 'emphasis' | 'strike')[]; leaf: 'text' | 'code' | 'number' | 'path' | 'link' | 'dim' }
24type Unit = { ch: string; fmt: Fmt }
25
26const R = /[--ٟ٪-ۯۺ-ݿࢠ-ࣿיִ-﷿ﹰ-]/
27const L = /[A-Za-z0-9À-ɏ٠-٩۰-۹]/
28const LATIN = /[A-Za-zÀ-ɏ]/
29const MARK = /\p{M}/u
30const MIRROR: Record<string, string> = { '(': ')', ')': '(', '[': ']', ']': '[', '{': '}', '}': '{', '<': '>', '>': '<' }
31const DIGIT = /[0-9\u0660-\u0669\u06F0-\u06F9]/
32const TERMINATOR = /[%\u2030\u00B0$\u20AC\u00A3\u00A5\u20AA]/
33const COMMENT = /^(.*?)((?:^|\s)(?:#|\/\/)\s+)(.*)$/
34
35const classify = (ch: string): Dir | null => (R.test(ch) ? 'R' : L.test(ch) ? 'L' : null)
36
37const units = (text: string, fmt: Fmt): Unit[] => {
38 const out: Unit[] = []
39 for (const cp of text) {
40 const last = out.at(-1)
41 if (last && MARK.test(cp)) last.ch += cp
42 else out.push({ ch: cp, fmt })
43 }
44 return out
45}
46
47const flatten = (nodes: Inline[], wrap: Fmt['wrap'] = []): Unit[] =>
48 nodes.flatMap(n => {
49 switch (n.kind) {
50 case 'strong':
51 case 'emphasis':
52 case 'strike':
53 return flatten(n.children, [...wrap, n.kind])
54 case 'link':
55 return n.text === n.href
56 ? units(n.text, { wrap, leaf: 'link' })
57 : [...units(n.text, { wrap, leaf: 'link' }), ...units(` (${n.href})`, { wrap, leaf: 'dim' })]
58 default:
59 return units(n.text, { wrap, leaf: n.kind })
60 }
61 })
62
63const sameFmt = (a: Fmt, b: Fmt) => a.leaf === b.leaf && a.wrap.length === b.wrap.length && a.wrap.every((k, i) => k === b.wrap[i])
64
65const leafNode = (leaf: Fmt['leaf'], text: string): Inline => (leaf === 'link' ? { kind: 'link', text, href: text } : { kind: leaf, text })
66
67const rebuild = (all: Unit[]): Inline[] => {
68 const out: Inline[] = []
69 let i = 0
70 while (i < all.length) {
71 const fmt = all[i]!.fmt
72 let text = ''
73 let j = i
74 while (j < all.length && sameFmt(all[j]!.fmt, fmt)) text += all[j++]!.ch
75 out.push(fmt.wrap.reduceRight<Inline>((inner, kind) => ({ kind, children: [inner] }), leafNode(fmt.leaf, text)))
76 i = j
77 }
78 return out
79}
80
81const baseOf = (all: Unit[]): Dir => {
82 const words = all.map(u => u.ch).join('').split(/\s+/)
83 const rw = words.filter(w => R.test(w)).length
84 const lw = words.filter(w => LATIN.test(w) && !R.test(w)).length
85 if (rw !== lw) return rw > lw ? 'R' : 'L'
86 return all.map(u => classify(u.ch)).find(Boolean) ?? 'R'
87}
88
89const reorder = (all: Unit[], base: Dir): Unit[] => {
90 const strong = all.map((u, i) => {
91 const own = classify(u.ch)
92 if (own) return own
93 if (u.fmt.leaf === 'code' || u.fmt.leaf === 'path') return 'L'
94 return TERMINATOR.test(u.ch) && (DIGIT.test(all[i - 1]?.ch ?? '') || DIGIT.test(all[i + 1]?.ch ?? '')) ? 'L' : null
95 })
96 const prev: (Dir | null)[] = []
97 let seen: Dir | null = null
98 strong.forEach((s, i) => {
99 seen = s ?? seen
100 prev[i] = seen
101 })
102 const next: (Dir | null)[] = []
103 seen = null
104 for (let i = strong.length - 1; i >= 0; i--) {
105 seen = strong[i] ?? seen
106 next[i] = seen
107 }
108 const dir = strong.map((s, i): Dir => s ?? ((prev[i] ?? base) === (next[i] ?? base) ? (prev[i] ?? base) : base))
109 const runs: { d: Dir; u: Unit[] }[] = []
110 all.forEach((u, i) => {
111 const last = runs.at(-1)
112 if (last && last.d === dir[i]) last.u.push(u)
113 else runs.push({ d: dir[i]!, u: [u] })
114 })
115 const out = runs.map(r => (r.d === 'R' ? r.u.map(u => ({ ...u, ch: MIRROR[u.ch] ?? u.ch })).reverse() : r.u))
116 return (base === 'R' ? out.reverse() : out).flat()
117}
118
119const bidiClass = (ch: string): string =>
120 R.test(ch) ? 'R' : /[٠-٩]/.test(ch) ? 'AN' : /[0-9۰-۹]/.test(ch) ? 'EN' : /\p{L}/u.test(ch) ? 'L' : /[+-]/.test(ch) ? 'ES' : /[#$%°£€¥₪‰¢]/.test(ch) ? 'ET' : /[,.:/]/.test(ch) ? 'CS' : 'N'
121
122const terminalBidi = (all: Unit[]): Unit[] => {
123 const t = all.map(u => bidiClass(u.ch))
124 t.forEach((c, i) => {
125 if ((c === 'ES' || c === 'CS') && t[i - 1] === 'EN' && t[i + 1] === 'EN') t[i] = 'EN'
126 else if (c === 'CS' && t[i - 1] === 'AN' && t[i + 1] === 'AN') t[i] = 'AN'
127 })
128 for (let i = 1; i < t.length; i++) if (t[i] === 'ET' && t[i - 1] === 'EN') t[i] = 'EN'
129 for (let i = t.length - 2; i >= 0; i--) if (t[i] === 'ET' && t[i + 1] === 'EN') t[i] = 'EN'
130 let strong = 'L'
131 const w = t.map(c => {
132 if (c === 'L' || c === 'R') strong = c
133 return c === 'EN' && strong === 'L' ? 'L' : c === 'ES' || c === 'ET' || c === 'CS' ? 'N' : c
134 })
135 const rtlSide = (c?: string) => c !== undefined && c !== 'L'
136 const levels = w.map((c, i) => {
137 if (c !== 'N') return c === 'L' ? 0 : c === 'R' ? 1 : 2
138 let a = i
139 while (w[a] === 'N') a--
140 let b = i
141 while (w[b] === 'N') b++
142 return rtlSide(w[a]) && rtlSide(w[b]) ? 1 : 0
143 })
144 let out = all.map((u, i) => ({ u: levels[i]! % 2 ? { ...u, ch: MIRROR[u.ch] ?? u.ch } : u, l: levels[i]! }))
145 for (let l = 2; l >= 1; l--) {
146 const done: typeof out = []
147 let run: typeof out = []
148 for (const x of out) {
149 if (x.l >= l) run.push(x)
150 else {
151 done.push(...run.reverse(), x)
152 run = []
153 }
154 }
155 out = [...done, ...run.reverse()]
156 }
157 return out.map(x => x.u)
158}
159
160export const terminalLine = (nodes: Inline[]): Inline[] => rebuild(terminalBidi(flatten(nodes)))
161
162const wrapUnits = (all: Unit[], max: number, measure: (s: string) => number): Unit[][] => {
163 const tokens: Unit[][] = []
164 all.forEach((u, i) => {
165 if (i === 0 || (u.ch !== ' ' && all[i - 1]!.ch === ' ')) tokens.push([])
166 tokens.at(-1)!.push(u)
167 })
168 const lines: Unit[][] = [[]]
169 let used = 0
170 const trim = (line: Unit[]) => {
171 while (line.at(-1)?.ch === ' ') line.pop()
172 }
173 for (const token of tokens) {
174 let core = token.length
175 while (core > 0 && token[core - 1]!.ch === ' ') core--
176 const w = token.slice(0, core).reduce((sum, u) => sum + measure(u.ch), 0)
177 if (lines.at(-1)!.length && used + w > max) {
178 trim(lines.at(-1)!)
179 lines.push([])
180 used = 0
181 }
182 lines.at(-1)!.push(...token)
183 used += w + (token.length - core)
184 }
185 trim(lines.at(-1)!)
186 return lines
187}
188
189const clusters = (text: string): string[] => units(text, { wrap: [], leaf: 'text' }).map(u => u.ch)
190
191const RUN = new RegExp(`(?:${R.source}|\\p{M})+`, 'gu')
192
193const shapeText = (text: string, shape: Shape): string =>
194 !R.test(text) ? text : shape === 'words' ? text.replace(RUN, run => clusters(run).reverse().join('')) : shape === 'inverse' ? terminalBidi(units(text, { wrap: [], leaf: 'text' })).map(u => u.ch).join('') : text
195
196const shapeNodes = (nodes: Inline[], shape: Shape): Inline[] =>
197 nodes.map(n => {
198 if ('children' in n) return { ...n, children: shapeNodes(n.children, shape) }
199 if (n.kind === 'link') return { ...n, text: shapeText(n.text, shape), href: shapeText(n.href, shape) }
200 return { ...n, text: shapeText(n.text, shape) }
201 })
202
203const containsRtl = (nodes: Inline[]): boolean => nodes.some(n => ('children' in n ? containsRtl(n.children) : R.test(n.text)))
204
205export const flow = (nodes: Inline[], columns: number, measure: (s: string) => number, shape: Shape = 'visual'): Flow | null => {
206 if (!containsRtl(nodes)) return null
207 const all = flatten(nodes)
208 const base = baseOf(all)
209 const logical = base === 'R' ? wrapUnits(all, columns, measure) : [all]
210 const lines = logical.map(line => rebuild(shape === 'logical' ? line : shape === 'inverse' ? terminalBidi(reorder(line, base)) : reorder(line, base)))
211 return { base, lines: shape === 'words' ? lines.map(line => shapeNodes(line, shape)) : lines }
212}
213
214const visualText = (text: string): string => {
215 if (!R.test(text)) return text
216 const all = units(text, { wrap: [], leaf: 'text' })
217 return reorder(all, baseOf(all)).map(u => u.ch).join('')
218}
219
220export const commentTail = (line: string, shape: Shape = 'visual'): { head: string; marker: string; tail: string } | null => {
221 if (shape === 'logical' || !R.test(line)) return null
222 const m = COMMENT.exec(line)
223 return m && R.test(m[3]!) ? { head: m[1]!, marker: m[2]!, tail: shapeText(visualText(m[3]!), shape) } : null
224}
225
226export const commentVisual = (text: string, shape: Shape = 'visual'): string => {
227 const c = commentTail(text, shape)
228 return c ? c.head + c.marker + c.tail : text
229}
230
231export const hasRtl = (text: string): boolean => R.test(text)
232hooks/anim.tsx 90 lines1/* Local extension: a Client surface module that plays an animated mermaid diagram. It walks a dot
2 along each step's drawn path, lights the step's boxes and labels (bold; the rest keeps its theme colours), and shows the caption. It plays
3 twice then rests on the last step; space pauses or replays, ←/→ step, r restarts, a click toggles. */
4import type { ClientKeyEvent, ClientModule, ClientSurface, RenderElement } from 'claude-code'
5
6export type AnimFrame = { path: [number, number][]; lit: [number, number][]; rects: { r: number; c: number; r2: number; c2: number }[]; caption: string }
7export type AnimProps = { lines: string[]; colors: (string | null)[][]; frames: AnimFrame[]; accent: string; dim: string; text: string }
8type State = { step: number; pos: number; hold: number; playing: boolean; loops: number; live: { p: AnimProps } }
9
10const HOLD = 10
11const LOOPS = 2
12
13const advance = (s: State): State => {
14 const f = s.live.p.frames[s.step]
15 if (!f) return s
16 const len = f.path.length
17 const pace = Math.max(1, Math.ceil(len / 12))
18 if (s.pos < len) return { ...s, pos: Math.min(len, s.pos + pace) }
19 if (s.hold < HOLD) return { ...s, hold: s.hold + 1 }
20 const last = s.step + 1 >= s.live.p.frames.length
21 if (!last) return { ...s, step: s.step + 1, pos: 0, hold: 0 }
22 const loops = s.loops + 1
23 return loops >= LOOPS ? { ...s, playing: false, loops } : { ...s, step: 0, pos: 0, hold: 0, loops }
24}
25
26const onKey = (s: State, k: ClientKeyEvent): State | null => {
27 const n = s.live.p.frames.length
28 const at = (step: number): State => ({ ...s, step: (step + n) % n, pos: s.live.p.frames[(step + n) % n]?.path.length ?? 0, hold: HOLD, playing: false })
29 if (k.key === ' ' || k.key === 'return') return s.playing ? { ...s, playing: false } : { ...s, playing: true, loops: 0, ...(s.loops >= LOOPS ? { step: 0, pos: 0, hold: 0 } : {}) }
30 if (k.key === 'right' || k.key === 'l') return at(s.step + 1)
31 if (k.key === 'left' || k.key === 'h') return at(s.step - 1)
32 if (k.key === 'r') return { ...s, step: 0, pos: 0, hold: 0, playing: true, loops: 0 }
33 return null
34}
35
36export const Anim: ClientModule<AnimProps, State> = (props, surface: ClientSurface<State>) => {
37 const { Box, Text } = surface.elements
38 let st = surface.state
39 if (!st) {
40 st = { step: 0, pos: 0, hold: 0, playing: true, loops: 0, live: { p: props } }
41 surface.setState(st)
42 surface.every(70, () => { const now = surface.state; if (now?.playing) surface.setState(advance(now)) })
43 surface.onKey(k => { const now = surface.state; const next = now && onKey(now, k); if (next) surface.setState(next) })
44 surface.onPointer(e => { const now = surface.state; if (now && e.type === 'down') { const next = onKey(now, { key: ' ' }); if (next) surface.setState(next) } })
45 }
46 st.live.p = props
47 const f = props.frames[st.step]
48 const hot = new Set<string>()
49 const trail = new Set<string>()
50 if (f) {
51 f.path.slice(0, st.pos).forEach(([r, c]) => trail.add(`${r},${c}`))
52 f.lit.forEach(([r, c]) => hot.add(`${r},${c}`))
53 for (const x of f.rects) for (let r = x.r; r <= x.r2; r++) for (let c = x.c; c <= x.c2; c++) hot.add(`${r},${c}`)
54 }
55 const head = f && st.pos > 0 && st.pos < f.path.length ? f.path[st.pos - 1] : undefined
56 const rows: RenderElement[] = props.lines.map((line, r) => {
57 const chars = [...line]
58 const parts: RenderElement[] = []
59 let at = 0
60 const look = (c: number) => {
61 const k = `${r},${c}`
62 if (head && head[0] === r && head[1] === c) return { ch: '●', color: props.accent, bold: true, dim: false }
63 if (trail.has(k)) return { ch: chars[c]!, color: props.accent, bold: true, dim: false }
64 if (hot.has(k)) return { ch: chars[c]!, color: props.colors[r]?.[c] ?? props.text, bold: true, dim: false }
65 return { ch: chars[c]!, color: props.colors[r]?.[c] ?? props.text, bold: false, dim: false }
66 }
67 while (at < chars.length) {
68 const a = look(at)
69 let text = a.ch
70 let end = at + 1
71 for (; end < chars.length; end++) { const b = look(end); if (b.color !== a.color || b.bold !== a.bold || b.dim !== a.dim) break; text += b.ch }
72 parts.push(<Text key={`p${at}`} color={a.color ?? undefined} bold={a.bold} dimColor={a.dim}>{text}</Text>)
73 at = end
74 }
75 return <Text key={`r${r}`}>{parts.length ? parts : ' '}</Text>
76 })
77 const n = props.frames.length
78 return (
79 <Box flexDirection="column" paddingLeft={2}>
80 {rows}
81 <Box flexDirection="row" columnGap={2} marginTop={1}>
82 <Text color={props.accent} bold>{`${st.playing ? '▶' : '❚❚'} ${st.step + 1}/${n}`}</Text>
83 <Text color={props.text} wrap="wrap">{f?.caption ?? ''}</Text>
84 </Box>
85 <Text color={props.dim}>{st.playing ? 'click or space pauses · ←/→ step · r restart' : 'click or space plays · ←/→ step · r restart'}</Text>
86 </Box>
87 )
88}
89export default Anim
90hooks/vendor/mermaid-text.js 13 lines1function normalizeBrTags(label){return(label.startsWith('"')&&label.endsWith('"')?label.slice(1,-1):label).replace(/<br\s*\/?>/gi,`
2`).replace(/\\n/g,`
3`).replace(/<\/?(?:sub|sup|small|mark)\s*>/gi,"").replace(/\*\*(.+?)\*\*/g,"<b>$1</b>").replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g,"<i>$1</i>").replace(/~~(.+?)~~/g,"<s>$1</s>")}function parseMermaid(text){let lines=text.split(`
4`).map(l=>l.trim()).filter(l=>l.length>0&&!l.startsWith("%%"));if(lines.length===0)throw new Error("Empty mermaid diagram");let header=lines[0];return/^stateDiagram(-v2)?\s*$/i.test(header)?parseStateDiagram(lines):parseFlowchart(lines)}function parseFlowchart(lines){let headerMatch=lines[0].match(/^(?:graph|flowchart)\s+(TD|TB|LR|BT|RL)\s*$/i);if(!headerMatch)throw new Error(`Invalid mermaid header: "${lines[0]}". Expected "graph TD", "flowchart LR", "stateDiagram-v2", etc.`);let graph={direction:headerMatch[1].toUpperCase(),nodes:new Map,edges:[],subgraphs:[],classDefs:new Map,classAssignments:new Map,nodeStyles:new Map,linkStyles:new Map},subgraphStack=[];for(let i=1;i<lines.length;i++){let line=lines[i],classDefMatch=line.match(/^classDef\s+(\w+)\s+(.+)$/);if(classDefMatch){let name=classDefMatch[1],propsStr=classDefMatch[2],props=parseStyleProps(propsStr);graph.classDefs.set(name,props);continue}let classAssignMatch=line.match(/^class\s+([\w,-]+)\s+(\w+)$/);if(classAssignMatch){let nodeIds=classAssignMatch[1].split(",").map(s=>s.trim()),className=classAssignMatch[2];for(let id of nodeIds)graph.classAssignments.set(id,className);continue}let styleMatch=line.match(/^style\s+([\w,-]+)\s+(.+)$/);if(styleMatch){let nodeIds=styleMatch[1].split(",").map(s=>s.trim()),props=parseStyleProps(styleMatch[2]);for(let id of nodeIds)graph.nodeStyles.set(id,{...graph.nodeStyles.get(id),...props});continue}let linkStyleMatch=line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/);if(linkStyleMatch){let target=linkStyleMatch[1].trim(),props=parseStyleProps(linkStyleMatch[2]);if(target==="default")graph.linkStyles.set("default",{...graph.linkStyles.get("default"),...props});else{let indices=target.split(",").map(s=>parseInt(s.trim(),10));for(let idx of indices)isNaN(idx)||graph.linkStyles.set(idx,{...graph.linkStyles.get(idx),...props})}continue}let dirMatch=line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i);if(dirMatch&&subgraphStack.length>0){subgraphStack[subgraphStack.length-1].direction=dirMatch[1].toUpperCase();continue}let subgraphMatch=line.match(/^subgraph\s+(.+)$/);if(subgraphMatch){let rest=subgraphMatch[1].trim(),bracketMatch=rest.match(/^([\w-]+)\s*\[(.+)\]$/),id,label;bracketMatch?(id=bracketMatch[1],label=normalizeBrTags(bracketMatch[2])):(label=normalizeBrTags(rest),id=rest.replace(/\s+/g,"_").replace(/[^\w]/g,""));let sg={id,label,nodeIds:[],children:[]};subgraphStack.push(sg);continue}if(line==="end"){let completed=subgraphStack.pop();completed&&(subgraphStack.length>0?subgraphStack[subgraphStack.length-1].children.push(completed):graph.subgraphs.push(completed));continue}parseEdgeLine(line,graph,subgraphStack)}return graph}function parseStateDiagram(lines){let graph={direction:"TD",nodes:new Map,edges:[],subgraphs:[],classDefs:new Map,classAssignments:new Map,nodeStyles:new Map,linkStyles:new Map},compositeStack=[],compositeStateIds=new Set,startCount=0,endCount=0;for(let i=1;i<lines.length;i++){let line=lines[i],dirMatch=line.match(/^direction\s+(TD|TB|LR|BT|RL)\s*$/i);if(dirMatch){compositeStack.length>0?compositeStack[compositeStack.length-1].direction=dirMatch[1].toUpperCase():graph.direction=dirMatch[1].toUpperCase();continue}let linkStyleMatch=line.match(/^linkStyle\s+(default|[\d,\s]+)\s+(.+)$/);if(linkStyleMatch){let target=linkStyleMatch[1].trim(),props=parseStyleProps(linkStyleMatch[2]);if(target==="default")graph.linkStyles.set("default",{...graph.linkStyles.get("default"),...props});else{let indices=target.split(",").map(s=>parseInt(s.trim(),10));for(let idx of indices)isNaN(idx)||graph.linkStyles.set(idx,{...graph.linkStyles.get(idx),...props})}continue}let compositeMatch=line.match(/^state\s+(?:"([^"]+)"\s+as\s+)?([\w\p{L}]+)\s*\{$/u);if(compositeMatch){let label=compositeMatch[1]??compositeMatch[2],id=compositeMatch[2],sg={id,label,nodeIds:[],children:[]};compositeStack.push(sg),compositeStateIds.add(id),graph.nodes.delete(id);continue}if(line==="}"){let completed=compositeStack.pop();completed&&(compositeStack.length>0?compositeStack[compositeStack.length-1].children.push(completed):graph.subgraphs.push(completed));continue}let stateAliasMatch=line.match(/^state\s+"([^"]+)"\s+as\s+([\w\p{L}]+)\s*$/u);if(stateAliasMatch){let label=normalizeBrTags(stateAliasMatch[1]),id=stateAliasMatch[2];registerStateNode(graph,compositeStack,{id,label,shape:"rounded"});continue}let transitionMatch=line.match(/^(\[\*\]|[\w\p{L}-]+)\s*(-->)\s*(\[\*\]|[\w\p{L}-]+)(?:\s*:\s*(.+))?$/u);if(transitionMatch){let sourceId=transitionMatch[1],targetId=transitionMatch[3],rawTransitionLabel=transitionMatch[4]?.trim(),edgeLabel=rawTransitionLabel?normalizeBrTags(rawTransitionLabel):void 0;sourceId==="[*]"?(startCount++,sourceId=`_start${startCount>1?startCount:""}`,registerStateNode(graph,compositeStack,{id:sourceId,label:"",shape:"state-start"})):compositeStateIds.has(sourceId)||ensureStateNode(graph,compositeStack,sourceId),targetId==="[*]"?(endCount++,targetId=`_end${endCount>1?endCount:""}`,registerStateNode(graph,compositeStack,{id:targetId,label:"",shape:"state-end"})):compositeStateIds.has(targetId)||ensureStateNode(graph,compositeStack,targetId),graph.edges.push({source:sourceId,target:targetId,label:edgeLabel,style:"solid",hasArrowStart:!1,hasArrowEnd:!0});continue}let stateDescMatch=line.match(/^([\w\p{L}-]+)\s*:\s*(.+)$/u);if(stateDescMatch){let id=stateDescMatch[1],label=normalizeBrTags(stateDescMatch[2].trim());registerStateNode(graph,compositeStack,{id,label,shape:"rounded"});continue}}return graph}function registerStateNode(graph,compositeStack,node){if(!graph.nodes.has(node.id)&&graph.nodes.set(node.id,node),compositeStack.length>0){let current=compositeStack[compositeStack.length-1];current.nodeIds.includes(node.id)||current.nodeIds.push(node.id)}}function ensureStateNode(graph,compositeStack,id){if(!graph.nodes.has(id))registerStateNode(graph,compositeStack,{id,label:id,shape:"rounded"});else if(compositeStack.length>0){let current=compositeStack[compositeStack.length-1];current.nodeIds.includes(id)||current.nodeIds.push(id)}}function parseStyleProps(propsStr){let cleaned=propsStr.replace(/;\s*$/,""),props={};for(let pair of cleaned.split(",")){let colonIdx=pair.indexOf(":");if(colonIdx>0){let key=pair.slice(0,colonIdx).trim(),val=pair.slice(colonIdx+1).trim();key&&val&&(props[key]=val)}}return props}var ARROW_REGEX=/^(<)?(-->|-.->|==>|---|-\.-|===)(?:\|([^|]*)\|)?/,TEXT_ARROW_REGEX=/^(<)?(--|-\.|==)\s+(.+?)\s+(-->|---|\.\->|-\.\-|==>|===)/,NODE_PATTERNS=[{regex:/^([\w-]+)\(\(\((.+?)\)\)\)/,shape:"doublecircle"},{regex:/^([\w-]+)\(\[(.+?)\]\)/,shape:"stadium"},{regex:/^([\w-]+)\(\((.+?)\)\)/,shape:"circle"},{regex:/^([\w-]+)\[\[(.+?)\]\]/,shape:"subroutine"},{regex:/^([\w-]+)\[\((.+?)\)\]/,shape:"cylinder"},{regex:/^([\w-]+)\[\/(.+?)\\\]/,shape:"trapezoid"},{regex:/^([\w-]+)\[\\(.+?)\/\]/,shape:"trapezoid-alt"},{regex:/^([\w-]+)>(.+?)\]/,shape:"asymmetric"},{regex:/^([\w-]+)\{\{(.+?)\}\}/,shape:"hexagon"},{regex:/^([\w-]+)\[(.+?)\]/,shape:"rectangle"},{regex:/^([\w-]+)\((.+?)\)/,shape:"rounded"},{regex:/^([\w-]+)\{(.+?)\}/,shape:"diamond"}],BARE_NODE_REGEX=/^([\w-]+)/,CLASS_SHORTHAND_REGEX=/^:::([\w][\w-]*)/;function parseEdgeLine(line,graph,subgraphStack){let remaining=line.trim(),firstGroup=consumeNodeGroup(remaining,graph,subgraphStack);if(!firstGroup||firstGroup.ids.length===0)return;remaining=firstGroup.remaining.trim();let prevGroupIds=firstGroup.ids;for(;remaining.length>0;){let hasArrowStart,style,hasArrowEnd,edgeLabel,arrowMatch=remaining.match(ARROW_REGEX);if(arrowMatch){hasArrowStart=!!arrowMatch[1];let arrowOp=arrowMatch[2],rawEdgeLabel=arrowMatch[3]?.trim();edgeLabel=rawEdgeLabel?normalizeBrTags(rawEdgeLabel):void 0,remaining=remaining.slice(arrowMatch[0].length).trim(),style=arrowStyleFromOp(arrowOp),hasArrowEnd=arrowOp.endsWith(">")}else{let textMatch=remaining.match(TEXT_ARROW_REGEX);if(!textMatch)break;hasArrowStart=!!textMatch[1];let rawLabel=textMatch[3].trim();edgeLabel=rawLabel?normalizeBrTags(rawLabel):void 0;let openOp=textMatch[2],closeOp=textMatch[4];remaining=remaining.slice(textMatch[0].length).trim(),style=textArrowStyleFromOps(openOp,closeOp),hasArrowEnd=closeOp.endsWith(">")}let nextGroup=consumeNodeGroup(remaining,graph,subgraphStack);if(!nextGroup||nextGroup.ids.length===0)break;remaining=nextGroup.remaining.trim();for(let sourceId of prevGroupIds)for(let targetId of nextGroup.ids)graph.edges.push({source:sourceId,target:targetId,label:edgeLabel,style,hasArrowStart,hasArrowEnd});prevGroupIds=nextGroup.ids}}function consumeNodeGroup(text,graph,subgraphStack){let first=consumeNode(text,graph,subgraphStack);if(!first)return null;let ids=[first.id],remaining=first.remaining.trim();for(;remaining.startsWith("&");){remaining=remaining.slice(1).trim();let next=consumeNode(remaining,graph,subgraphStack);if(!next)break;ids.push(next.id),remaining=next.remaining.trim()}return{ids,remaining}}function consumeNode(text,graph,subgraphStack){let id=null,remaining=text;for(let{regex,shape}of NODE_PATTERNS){let match=text.match(regex);if(match){id=match[1];let label=normalizeBrTags(match[2]);registerNode(graph,subgraphStack,{id,label,shape}),remaining=text.slice(match[0].length);break}}if(id===null){let bareMatch=text.match(BARE_NODE_REGEX);bareMatch&&(id=bareMatch[1],graph.nodes.has(id)||registerNode(graph,subgraphStack,{id,label:id,shape:"rectangle"}),remaining=text.slice(bareMatch[0].length))}if(id===null)return null;let classMatch=remaining.match(CLASS_SHORTHAND_REGEX);return classMatch&&(graph.classAssignments.set(id,classMatch[1]),remaining=remaining.slice(classMatch[0].length)),{id,remaining}}function registerNode(graph,subgraphStack,node){!graph.nodes.has(node.id)&&graph.nodes.set(node.id,node),trackInSubgraph(subgraphStack,node.id)}function trackInSubgraph(subgraphStack,nodeId){if(subgraphStack.length>0){let current=subgraphStack[subgraphStack.length-1];current.nodeIds.includes(nodeId)||current.nodeIds.push(nodeId)}}function arrowStyleFromOp(op){return op==="-.->"||op==="-.-"?"dotted":op==="==>"||op==="==="?"thick":"solid"}function textArrowStyleFromOps(openOp,closeOp){return openOp==="-."||closeOp===".->"||closeOp==="-.-"?"dotted":openOp==="=="||closeOp==="==>"||closeOp==="==="?"thick":"solid"}var Up={x:1,y:0},Down={x:1,y:2},Left={x:0,y:1},Right={x:2,y:1},UpperRight={x:2,y:0},UpperLeft={x:0,y:0},LowerRight={x:2,y:2},LowerLeft={x:0,y:2},Middle={x:1,y:1};function gridCoordEquals(a,b){return a.x===b.x&&a.y===b.y}function drawingCoordEquals(a,b){return a.x===b.x&&a.y===b.y}function gridCoordDirection(c,dir){return{x:c.x+dir.x,y:c.y+dir.y}}function gridKey(c){return`${c.x},${c.y}`}var EMPTY_STYLE={name:"",styles:{}};var DEFAULT_ASCII_THEME={fg:"#27272a",border:"#a1a1aa",line:"#71717a",arrow:"#52525b",corner:"#71717a",junction:"#a1a1aa"};function detectColorMode(){let proc=globalThis.process;if(proc){if(!proc.stdout?.isTTY)return"none";let colorTerm=proc.env?.COLORTERM?.toLowerCase()??"",term=proc.env?.TERM?.toLowerCase()??"";return colorTerm==="truecolor"||colorTerm==="24bit"?"truecolor":term.includes("256color")||term.includes("256")?"ansi256":term&&term!=="dumb"?"ansi16":"none"}return typeof document<"u"?"html":"none"}function parseHex(hex){let h=hex.replace("#","");return h.length===3?{r:parseInt(h[0]+h[0],16),g:parseInt(h[1]+h[1],16),b:parseInt(h[2]+h[2],16)}:{r:parseInt(h.substring(0,2),16),g:parseInt(h.substring(2,4),16),b:parseInt(h.substring(4,6),16)}}var ESC="\x1B[",RESET=`${ESC}0m`;function truecolorFg(hex){let{r,g,b}=parseHex(hex);return`${ESC}38;2;${r};${g};${b}m`}function rgbTo256(r,g,b){let avg=(r+g+b)/3;if(Math.max(Math.abs(r-avg),Math.abs(g-avg),Math.abs(b-avg))<10){let gray=Math.round(avg/255*23);return 232+Math.min(23,Math.max(0,gray))}let toIndex=v=>v<48?0:v<115?1:Math.min(5,Math.floor((v-35)/40)),ri=toIndex(r),gi=toIndex(g),bi=toIndex(b);return 16+36*ri+6*gi+bi}function ansi256Fg(hex){let{r,g,b}=parseHex(hex),index=rgbTo256(r,g,b);return`${ESC}38;5;${index}m`}function ansi16Fg(hex){let{r,g,b}=parseHex(hex),luma=.299*r+.587*g+.114*b,bright=luma>100?0:60,code;return r>180&&g<100&&b<100?code=31:g>180&&r<100&&b<100?code=32:r>150&&g>150&&b<100?code=33:b>180&&r<100&&g<100?code=34:r>150&&b>150&&g<100?code=35:g>150&&b>150&&r<100?code=36:luma>200?code=37:luma<50?code=30:code=37,`${ESC}${code+bright}m`}function escapeHtml(text){return text.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function htmlSpan(hex,text){return`<span style="color:${hex}">${escapeHtml(text)}</span>`}function getRoleColor(role,theme){switch(role){case"text":return theme.fg;case"border":return theme.border;case"line":return theme.line;case"arrow":return theme.arrow;case"corner":return theme.corner??theme.line;case"junction":return theme.junction??theme.border;default:return theme.fg}}function getAnsiColor(role,theme,mode){if(mode==="none")return"";let hex=getRoleColor(role,theme);switch(mode){case"truecolor":return truecolorFg(hex);case"ansi256":return ansi256Fg(hex);case"ansi16":return ansi16Fg(hex);default:return""}}function colorizeLine(chars,roles,theme,mode){if(mode==="none")return chars.join("");if(mode==="html")return colorizeLineHtml(chars,roles,theme);let result="",currentRole=null,buffer="";for(let i=0;i<chars.length;i++){let char=chars[i],role=roles[i]??null;if(char===" "){buffer.length>0&&(currentRole!==null?result+=getAnsiColor(currentRole,theme,mode)+buffer+RESET:result+=buffer,buffer="",currentRole=null),result+=char;continue}if(role===currentRole){buffer+=char;continue}buffer.length>0&&(currentRole!==null?result+=getAnsiColor(currentRole,theme,mode)+buffer+RESET:result+=buffer),buffer=char,currentRole=role}return buffer.length>0&¤tRole!==null?result+=getAnsiColor(currentRole,theme,mode)+buffer+RESET:buffer.length>0&&(result+=buffer),result}function colorizeLineHtml(chars,roles,theme){let result="",currentRole=null,buffer="",flush=()=>{buffer.length!==0&&(currentRole!==null?result+=htmlSpan(getRoleColor(currentRole,theme),buffer):result+=escapeHtml(buffer),buffer="",currentRole=null)};for(let i=0;i<chars.length;i++){let char=chars[i],role=roles[i]??null;if(char===" "){flush(),result+=" ";continue}if(role===currentRole){buffer+=char;continue}flush(),buffer=char,currentRole=role}return flush(),result}function colorizeText(text,hex,mode){if(mode==="none"||text.length===0)return text;if(mode==="html")return htmlSpan(hex,text);let code;switch(mode){case"truecolor":code=truecolorFg(hex);break;case"ansi256":code=ansi256Fg(hex);break;case"ansi16":code=ansi16Fg(hex);break;default:return text}return`${code}${text}${RESET}`}function mkCanvas(x,y){let canvas=[];for(let i=0;i<=x;i++){let col=[];for(let j=0;j<=y;j++)col.push(" ");canvas.push(col)}return canvas}function copyCanvas(source){let[maxX,maxY]=getCanvasSize(source);return mkCanvas(maxX,maxY)}function mkRoleCanvas(x,y){let roleCanvas=[];for(let i=0;i<=x;i++){let col=[];for(let j=0;j<=y;j++)col.push(null);roleCanvas.push(col)}return roleCanvas}function increaseRoleCanvasSize(roleCanvas,newX,newY){let currX=roleCanvas.length-1,currY=(roleCanvas[0]?.length??1)-1,targetX=Math.max(newX,currX),targetY=Math.max(newY,currY),grown=mkRoleCanvas(targetX,targetY);for(let x=0;x<grown.length;x++)for(let y=0;y<grown[0].length;y++)x<roleCanvas.length&&y<roleCanvas[0].length&&(grown[x][y]=roleCanvas[x][y]);return roleCanvas.length=0,roleCanvas.push(...grown),roleCanvas}function setRole(roleCanvas,x,y,role){(x>=roleCanvas.length||y>=(roleCanvas[0]?.length??0))&&increaseRoleCanvasSize(roleCanvas,x,y),roleCanvas[x][y]=role}function getCanvasSize(canvas){return[canvas.length-1,(canvas[0]?.length??1)-1]}function increaseSize(canvas,newX,newY){let[currX,currY]=getCanvasSize(canvas),targetX=Math.max(newX,currX),targetY=Math.max(newY,currY),grown=mkCanvas(targetX,targetY);for(let x=0;x<grown.length;x++)for(let y=0;y<grown[0].length;y++)x<canvas.length&&y<canvas[0].length&&(grown[x][y]=canvas[x][y]);return canvas.length=0,canvas.push(...grown),canvas}var JUNCTION_CHARS=new Set(["\u2500","\u2502","\u250C","\u2510","\u2514","\u2518","\u251C","\u2524","\u252C","\u2534","\u253C","\u2574","\u2575","\u2576","\u2577"]);function clearLabelGaps(canvas,label){for(let y=0;y<(label[0]?.length??0);y++){let xs=label.map((col,x)=>col[y]!==" "?x:-1).filter(x=>x>=0);if(xs.length!==0)for(let x=xs[0];x<=xs[xs.length-1];x++)label[x][y]===" "&&!isAlphanumeric(canvas[x][y])&&(canvas[x][y]=" ")}}function isJunctionChar(c){return JUNCTION_CHARS.has(c)}function isAlphanumeric(c){return/^[a-zA-Z0-9]$/.test(c)}var JUNCTION_MAP={"\u2500":{"\u2502":"\u253C","\u250C":"\u252C","\u2510":"\u252C","\u2514":"\u2534","\u2518":"\u2534","\u251C":"\u253C","\u2524":"\u253C","\u252C":"\u252C","\u2534":"\u2534"},"\u2502":{"\u2500":"\u253C","\u250C":"\u251C","\u2510":"\u2524","\u2514":"\u251C","\u2518":"\u2524","\u251C":"\u251C","\u2524":"\u2524","\u252C":"\u253C","\u2534":"\u253C"},"\u250C":{"\u2500":"\u252C","\u2502":"\u251C","\u2510":"\u252C","\u2514":"\u251C","\u2518":"\u253C","\u251C":"\u251C","\u2524":"\u253C","\u252C":"\u252C","\u2534":"\u253C"},"\u2510":{"\u2500":"\u252C","\u2502":"\u2524","\u250C":"\u252C","\u2514":"\u253C","\u2518":"\u2524","\u251C":"\u253C","\u2524":"\u2524","\u252C":"\u252C","\u2534":"\u253C"},"\u2514":{"\u2500":"\u2534","\u2502":"\u251C","\u250C":"\u251C","\u2510":"\u253C","\u2518":"\u2534","\u251C":"\u251C","\u2524":"\u253C","\u252C":"\u253C","\u2534":"\u2534"},"\u2518":{"\u2500":"\u2534","\u2502":"\u2524","\u250C":"\u253C","\u2510":"\u2524","\u2514":"\u2534","\u251C":"\u253C","\u2524":"\u2524","\u252C":"\u253C","\u2534":"\u2534"},"\u251C":{"\u2500":"\u253C","\u2502":"\u251C","\u250C":"\u251C","\u2510":"\u253C","\u2514":"\u251C","\u2518":"\u253C","\u2524":"\u253C","\u252C":"\u253C","\u2534":"\u253C"},"\u2524":{"\u2500":"\u253C","\u2502":"\u2524","\u250C":"\u253C","\u2510":"\u2524","\u2514":"\u253C","\u2518":"\u2524","\u251C":"\u253C","\u252C":"\u253C","\u2534":"\u253C"},"\u252C":{"\u2500":"\u252C","\u2502":"\u253C","\u250C":"\u252C","\u2510":"\u252C","\u2514":"\u253C","\u2518":"\u253C","\u251C":"\u253C","\u2524":"\u253C","\u2534":"\u253C"},"\u2534":{"\u2500":"\u2534","\u2502":"\u253C","\u250C":"\u253C","\u2510":"\u253C","\u2514":"\u2534","\u2518":"\u2534","\u251C":"\u253C","\u2524":"\u253C","\u252C":"\u253C"}};function mergeJunctions(c1,c2){return JUNCTION_MAP[c1]?.[c2]??c1}function mergeCanvases(base,offset,useAscii,...overlays){let[maxX,maxY]=getCanvasSize(base);for(let overlay of overlays){let[oX,oY]=getCanvasSize(overlay);maxX=Math.max(maxX,oX+offset.x),maxY=Math.max(maxY,oY+offset.y)}let merged=mkCanvas(maxX,maxY);for(let x=0;x<=maxX;x++)for(let y=0;y<=maxY;y++)x<base.length&&y<base[0].length&&(merged[x][y]=base[x][y]);for(let overlay of overlays)for(let x=0;x<overlay.length;x++)for(let y=0;y<overlay[0].length;y++){let c=overlay[x][y];if(c!==" "){let mx=x+offset.x,my=y+offset.y,current=merged[mx][my];!useAscii&&isJunctionChar(c)&&isJunctionChar(current)?merged[mx][my]=mergeJunctions(current,c):isAlphanumeric(current)&&isAlphanumeric(c)||(merged[mx][my]=c)}}return merged}function canvasToString(canvas,options){let[maxX,maxY]=getCanvasSize(canvas),lines=[],roleCanvas=options?.roleCanvas,colorMode=options?.colorMode??"none",theme=options?.theme??DEFAULT_ASCII_THEME;for(let y=0;y<=maxY;y++)if(colorMode==="none"||!roleCanvas){let line="";for(let x=0;x<=maxX;x++)line+=canvas[x][y];lines.push(line)}else{let chars=[],roles=[];for(let x=0;x<=maxX;x++)chars.push(canvas[x][y]),roles.push(roleCanvas[x]?.[y]??null);lines.push(colorizeLine(chars,roles,theme,colorMode))}return lines.join(`
5`)}var VERTICAL_FLIP_MAP={"\u25B2":"\u25BC","\u25BC":"\u25B2","\u25E4":"\u25E3","\u25E3":"\u25E4","\u25E5":"\u25E2","\u25E2":"\u25E5","^":"v",v:"^","\u250C":"\u2514","\u2514":"\u250C","\u2510":"\u2518","\u2518":"\u2510","\u252C":"\u2534","\u2534":"\u252C","\u2575":"\u2577","\u2577":"\u2575"};function flipCanvasVertically(canvas){for(let col of canvas)col.reverse();for(let col of canvas)for(let y=0;y<col.length;y++){let flipped=VERTICAL_FLIP_MAP[col[y]];flipped&&(col[y]=flipped)}return canvas}function flipRoleCanvasVertically(roleCanvas){for(let col of roleCanvas)col.reverse();return roleCanvas}function drawText(canvas,start,text,forceOverwrite=!1){increaseSize(canvas,start.x+text.length,start.y);for(let i=0;i<text.length;i++){let x=start.x+i,current=canvas[x][start.y];(forceOverwrite||current===" ")&&(canvas[x][start.y]=text[i])}}function setCanvasSizeToGrid(canvas,columnWidth,rowHeight){let maxX=0,maxY=0;for(let w of columnWidth.values())maxX+=w;for(let h of rowHeight.values())maxY+=h;increaseSize(canvas,maxX-1,maxY-1)}function setRoleCanvasSizeToGrid(roleCanvas,columnWidth,rowHeight){let maxX=0,maxY=0;for(let w of columnWidth.values())maxX+=w;for(let h of rowHeight.values())maxY+=h;increaseRoleCanvasSize(roleCanvas,maxX-1,maxY-1)}function convertToAsciiGraph(parsed,config){let nodeMap=new Map,index=0;for(let[id,mNode]of parsed.nodes){let asciiNode={name:id,displayLabel:mNode.label,shape:mNode.shape,index,gridCoord:null,drawingCoord:null,drawing:null,drawn:!1,styleClassName:"",styleClass:EMPTY_STYLE};nodeMap.set(id,asciiNode),index++}let nodes=[...nodeMap.values()],edges=[];for(let mEdge of parsed.edges){let from=nodeMap.get(mEdge.source),to=nodeMap.get(mEdge.target);!from||!to||edges.push({from,to,text:mEdge.label??"",path:[],labelLine:[],startDir:{x:0,y:0},endDir:{x:0,y:0},style:mEdge.style,hasArrowStart:mEdge.hasArrowStart,hasArrowEnd:mEdge.hasArrowEnd})}let subgraphs=[];for(let mSg of parsed.subgraphs)convertSubgraph(mSg,null,nodeMap,subgraphs);deduplicateSubgraphNodes(parsed.subgraphs,subgraphs,nodeMap,parsed);for(let[nodeId,className]of parsed.classAssignments){let node=nodeMap.get(nodeId),classDef=parsed.classDefs.get(className);node&&classDef&&(node.styleClassName=className,node.styleClass={name:className,styles:classDef})}return{nodes,edges,canvas:mkCanvas(0,0),roleCanvas:mkRoleCanvas(0,0),grid:new Map,columnWidth:new Map,rowHeight:new Map,subgraphs,config,offsetX:0,offsetY:0,bundles:[]}}function convertSubgraph(mSg,parent,nodeMap,allSubgraphs){let normalizedDirection;mSg.direction&&(normalizedDirection=mSg.direction==="LR"||mSg.direction==="RL"?"LR":"TD");let sg={name:mSg.label,nodes:[],parent,children:[],minX:0,minY:0,maxX:0,maxY:0,direction:normalizedDirection};for(let nodeId of mSg.nodeIds){let node=nodeMap.get(nodeId);node&&sg.nodes.push(node)}allSubgraphs.push(sg);for(let childMSg of mSg.children){let child=convertSubgraph(childMSg,sg,nodeMap,allSubgraphs);sg.children.push(child);for(let childNode of child.nodes)sg.nodes.includes(childNode)||sg.nodes.push(childNode)}return sg}function deduplicateSubgraphNodes(mermaidSubgraphs,asciiSubgraphs,nodeMap,parsed){let sgMap=new Map;buildSgMap(mermaidSubgraphs,asciiSubgraphs,sgMap);let nodeOwner=new Map;function claimNodes(mSg){let asciiSg=sgMap.get(mSg);if(asciiSg){for(let child of mSg.children)claimNodes(child);for(let nodeId of mSg.nodeIds)nodeOwner.has(nodeId)||nodeOwner.set(nodeId,asciiSg)}}for(let mSg of mermaidSubgraphs)claimNodes(mSg);for(let asciiSg of asciiSubgraphs)asciiSg.nodes=asciiSg.nodes.filter(node=>{let nodeId;for(let[id,n]of nodeMap)if(n===node){nodeId=id;break}if(!nodeId)return!1;let owner=nodeOwner.get(nodeId);return owner?isAncestorOrSelf(asciiSg,owner):!0})}function isAncestorOrSelf(candidate,target){let current=target;for(;current!==null;){if(current===candidate)return!0;current=current.parent}return!1}function buildSgMap(mSgs,aSgs,result){let flatMermaid=[];function flatten(sgs){for(let sg of sgs)flatMermaid.push(sg),flatten(sg.children)}flatten(mSgs);for(let i=0;i<flatMermaid.length&&i<aSgs.length;i++)result.set(flatMermaid[i],aSgs[i])}var MinHeap=class{items=[];get length(){return this.items.length}push(item){this.items.push(item),this.bubbleUp(this.items.length-1)}pop(){if(this.items.length===0)return;let top=this.items[0],last=this.items.pop();return this.items.length>0&&(this.items[0]=last,this.sinkDown(0)),top}bubbleUp(i){for(;i>0;){let parent=i-1>>1;if(this.items[i].priority<this.items[parent].priority)[this.items[i],this.items[parent]]=[this.items[parent],this.items[i]],i=parent;else break}}sinkDown(i){let n=this.items.length;for(;;){let smallest=i,left=2*i+1,right=2*i+2;if(left<n&&this.items[left].priority<this.items[smallest].priority&&(smallest=left),right<n&&this.items[right].priority<this.items[smallest].priority&&(smallest=right),smallest!==i)[this.items[i],this.items[smallest]]=[this.items[smallest],this.items[i]],i=smallest;else break}}};function heuristic(a,b){let absX=Math.abs(a.x-b.x),absY=Math.abs(a.y-b.y);return absX===0||absY===0?absX+absY:absX+absY+1}var MOVE_DIRS=[{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];function isFreeInGrid(grid,c){return c.x<0||c.y<0?!1:!grid.has(gridKey(c))}function getPath(grid,from,to){let pq=new MinHeap;pq.push({coord:from,priority:0});let costSoFar=new Map;costSoFar.set(gridKey(from),0);let cameFrom=new Map;for(cameFrom.set(gridKey(from),null);pq.length>0;){let current=pq.pop().coord;if(gridCoordEquals(current,to)){let path=[],c=current;for(;c!==null;)path.unshift(c),c=cameFrom.get(gridKey(c))??null;return path}let currentCost=costSoFar.get(gridKey(current));for(let dir of MOVE_DIRS){let next={x:current.x+dir.x,y:current.y+dir.y};if(!isFreeInGrid(grid,next)&&!gridCoordEquals(next,to))continue;let newCost=currentCost+1,nextKey=gridKey(next),existingCost=costSoFar.get(nextKey);if(existingCost===void 0||newCost<existingCost){costSoFar.set(nextKey,newCost);let priority=newCost+heuristic(next,to);pq.push({coord:next,priority}),cameFrom.set(nextKey,current)}}}return null}function mergePath(path){if(path.length<=2)return path;let toRemove=new Set,step0=path[0],step1=path[1];for(let idx=2;idx<path.length;idx++){let step2=path[idx],prevDx=step1.x-step0.x,prevDy=step1.y-step0.y,dx=step2.x-step1.x,dy=step2.y-step1.y;prevDx===dx&&prevDy===dy&&toRemove.add(idx-1),step0=step1,step1=step2}return path.filter((_,i)=>!toRemove.has(i))}function getOpposite(d){return d===Up?Down:d===Down?Up:d===Left?Right:d===Right?Left:d===UpperRight?LowerLeft:d===UpperLeft?LowerRight:d===LowerRight?UpperLeft:d===LowerLeft?UpperRight:Middle}function dirEquals(a,b){return a.x===b.x&&a.y===b.y}function determineDirection(from,to){return from.x===to.x?from.y<to.y?Down:Up:from.y===to.y?from.x<to.x?Right:Left:from.x<to.x?from.y<to.y?LowerRight:UpperRight:from.y<to.y?LowerLeft:UpperLeft}function selfReferenceDirection(graphDirection){return graphDirection==="LR"?[Right,Down,Down,Right]:[Down,Right,Right,Down]}function determineStartAndEndDir(edge,graphDirection){if(edge.from===edge.to)return selfReferenceDirection(graphDirection);let d=determineDirection(edge.from.gridCoord,edge.to.gridCoord),preferredDir,preferredOppositeDir,alternativeDir,alternativeOppositeDir,isBackwards=graphDirection==="LR"?dirEquals(d,Left)||dirEquals(d,UpperLeft)||dirEquals(d,LowerLeft):dirEquals(d,Up)||dirEquals(d,UpperLeft)||dirEquals(d,UpperRight);return dirEquals(d,LowerRight)?graphDirection==="LR"?(preferredDir=Down,preferredOppositeDir=Left,alternativeDir=Right,alternativeOppositeDir=Up):(preferredDir=Right,preferredOppositeDir=Up,alternativeDir=Down,alternativeOppositeDir=Left):dirEquals(d,UpperRight)?graphDirection==="LR"?(preferredDir=Up,preferredOppositeDir=Left,alternativeDir=Right,alternativeOppositeDir=Down):(preferredDir=Right,preferredOppositeDir=Down,alternativeDir=Up,alternativeOppositeDir=Left):dirEquals(d,LowerLeft)?graphDirection==="LR"?(preferredDir=Down,preferredOppositeDir=Down,alternativeDir=Left,alternativeOppositeDir=Up):(preferredDir=Left,preferredOppositeDir=Up,alternativeDir=Down,alternativeOppositeDir=Right):dirEquals(d,UpperLeft)?graphDirection==="LR"?(preferredDir=Down,preferredOppositeDir=Down,alternativeDir=Left,alternativeOppositeDir=Down):(preferredDir=Right,preferredOppositeDir=Right,alternativeDir=Up,alternativeOppositeDir=Right):isBackwards?graphDirection==="LR"&&dirEquals(d,Left)?(preferredDir=Down,preferredOppositeDir=Down,alternativeDir=Left,alternativeOppositeDir=Right):graphDirection==="TD"&&dirEquals(d,Up)?(preferredDir=Right,preferredOppositeDir=Right,alternativeDir=Up,alternativeOppositeDir=Down):(preferredDir=d,preferredOppositeDir=getOpposite(d),alternativeDir=d,alternativeOppositeDir=getOpposite(d)):(preferredDir=d,preferredOppositeDir=getOpposite(d),alternativeDir=d,alternativeOppositeDir=getOpposite(d)),[preferredDir,preferredOppositeDir,alternativeDir,alternativeOppositeDir]}function determinePath(graph,edge){let sourceSg=getNodeSubgraph(graph,edge.from),targetSg=getNodeSubgraph(graph,edge.to),effectiveDir=sourceSg&&sourceSg===targetSg&&sourceSg.direction?sourceSg.direction:graph.config.graphDirection,[preferredDir,preferredOppositeDir,alternativeDir,alternativeOppositeDir]=determineStartAndEndDir(edge,effectiveDir),prefFrom=gridCoordDirection(edge.from.gridCoord,preferredDir),prefTo=gridCoordDirection(edge.to.gridCoord,preferredOppositeDir),preferredPath=getPath(graph.grid,prefFrom,prefTo),altFrom=gridCoordDirection(edge.from.gridCoord,alternativeDir),altTo=gridCoordDirection(edge.to.gridCoord,alternativeOppositeDir),alternativePath=getPath(graph.grid,altFrom,altTo);if(preferredPath!==null&&alternativePath!==null){preferredPath=mergePath(preferredPath),alternativePath=mergePath(alternativePath),preferredPath.length<=alternativePath.length?(edge.startDir=preferredDir,edge.endDir=preferredOppositeDir,edge.path=preferredPath):(edge.startDir=alternativeDir,edge.endDir=alternativeOppositeDir,edge.path=alternativePath);return}if(preferredPath!==null){edge.startDir=preferredDir,edge.endDir=preferredOppositeDir,edge.path=mergePath(preferredPath);return}if(alternativePath!==null){edge.startDir=alternativeDir,edge.endDir=alternativeOppositeDir,edge.path=mergePath(alternativePath);return}edge.startDir=preferredDir,edge.endDir=preferredOppositeDir,edge.path=[prefFrom,prefTo]}function determineLabelLine(graph,edge){if(edge.text.length===0)return;let lenLabel=edge.text.length,pathLen=edge.path.length,isVerticalFlow=graph.config.graphDirection==="TD",segments=[];for(let i=1;i<pathLen;i++){let p1=edge.path[i-1],p2=edge.path[i],line=[p1,p2],width=calculateLineWidth(graph,line),isVertical=p1.x===p2.x;segments.push({line,width,index:i,isVertical})}let suitableSegments=segments.filter(s=>s.width>=lenLabel&&s.index>1),largestLine;if(suitableSegments.length>0)suitableSegments.sort((a,b)=>b.index-a.index),largestLine=suitableSegments[0].line;else{let fallbackSegments=segments.filter(s=>s.width>=lenLabel);fallbackSegments.length>0?(fallbackSegments.sort((a,b)=>b.index-a.index),largestLine=fallbackSegments[0].line):(segments.sort((a,b)=>b.width-a.width),largestLine=segments[0]?.line??[edge.path[0],edge.path[1]])}let minX=Math.min(largestLine[0].x,largestLine[1].x),maxX=Math.max(largestLine[0].x,largestLine[1].x),middleX=minX+Math.floor((maxX-minX)/2),current=graph.columnWidth.get(middleX)??0;graph.columnWidth.set(middleX,Math.max(current,lenLabel+2)),edge.labelLine=[largestLine[0],largestLine[1]]}function calculateLineWidth(graph,line){let total=0,startX=Math.min(line[0].x,line[1].x),endX=Math.max(line[0].x,line[1].x);for(let x=startX;x<=endX;x++)total+=graph.columnWidth.get(x)??0;return total}function analyzeEdgeBundles(graph){if(graph.config.graphDirection!=="TD")return[];let bundles=[],bundledEdges=new Set,edgesByTarget=new Map;for(let edge of graph.edges){if(edge.from===edge.to)continue;let existing=edgesByTarget.get(edge.to)??[];existing.push(edge),edgesByTarget.set(edge.to,existing)}for(let[target,edges]of edgesByTarget){if(edges.length<2||!canBundle(edges,graph)||edges.some(e=>bundledEdges.has(e)))continue;let bundle={type:"fan-in",edges:[...edges],sharedNode:target,otherNodes:edges.map(e=>e.from),junctionPoint:null,sharedPath:[],junctionDir:Middle,sharedNodeDir:Middle};for(let edge of edges)edge.bundle=bundle,bundledEdges.add(edge);bundles.push(bundle)}let edgesBySource=new Map;for(let edge of graph.edges){if(edge.from===edge.to||bundledEdges.has(edge))continue;let existing=edgesBySource.get(edge.from)??[];existing.push(edge),edgesBySource.set(edge.from,existing)}for(let[source,edges]of edgesBySource){if(edges.length<2||!canBundle(edges,graph))continue;let bundle={type:"fan-out",edges:[...edges],sharedNode:source,otherNodes:edges.map(e=>e.to),junctionPoint:null,sharedPath:[],junctionDir:Middle,sharedNodeDir:Middle};for(let edge of edges)edge.bundle=bundle,bundledEdges.add(edge);bundles.push(bundle)}return bundles}function canBundle(edges,graph){if(edges.length<2)return!1;let firstStyle=edges[0].style,firstFromSg=getNodeSubgraph(graph,edges[0].from),firstToSg=getNodeSubgraph(graph,edges[0].to);for(let edge of edges){if(edge.style!==firstStyle||edge.text.length>0)return!1;let fromSg=getNodeSubgraph(graph,edge.from),toSg=getNodeSubgraph(graph,edge.to);if(fromSg!==firstFromSg||toSg!==firstToSg||fromSg!==toSg)return!1}return!0}function calculateJunctionPoint(graph,bundle){let dir=graph.config.graphDirection,sharedCoord=bundle.sharedNode.gridCoord,otherCoords=bundle.otherNodes.map(n=>n.gridCoord);if(bundle.type==="fan-in"){let minX=Math.min(...otherCoords.map(c=>c.x)),maxX=Math.max(...otherCoords.map(c=>c.x)),minY=Math.min(...otherCoords.map(c=>c.y)),maxY=Math.max(...otherCoords.map(c=>c.y));if(dir==="TD"){let junctionY=sharedCoord.y-1,centerX=Math.floor((minX+maxX)/2)+1;return{x:sharedCoord.x+1,y:junctionY}}else{let junctionX=sharedCoord.x-1,junctionY=sharedCoord.y+1;return{x:junctionX,y:junctionY}}}else{let minX=Math.min(...otherCoords.map(c=>c.x)),maxX=Math.max(...otherCoords.map(c=>c.x)),minY=Math.min(...otherCoords.map(c=>c.y)),maxY=Math.max(...otherCoords.map(c=>c.y));if(dir==="TD"){let junctionY=sharedCoord.y+3;return{x:sharedCoord.x+1,y:junctionY}}else{let junctionX=sharedCoord.x+3,junctionY=sharedCoord.y+1;return{x:junctionX,y:junctionY}}}}function routeBundledEdges(graph,bundle){let dir=graph.config.graphDirection;bundle.junctionPoint=calculateJunctionPoint(graph,bundle);let junction=bundle.junctionPoint;if(bundle.type==="fan-in"){bundle.junctionDir=dir==="TD"?Up:Left,bundle.sharedNodeDir=dir==="TD"?Down:Right;let targetCoord=bundle.sharedNode.gridCoord,targetEntry=dir==="TD"?{x:targetCoord.x+1,y:targetCoord.y}:{x:targetCoord.x,y:targetCoord.y+1},sharedPath=getPath(graph.grid,junction,targetEntry);bundle.sharedPath=sharedPath?mergePath(sharedPath):[junction,targetEntry];for(let edge of bundle.edges){let sourceCoord=edge.from.gridCoord,sourceExit=dir==="TD"?{x:sourceCoord.x+1,y:sourceCoord.y+2}:{x:sourceCoord.x+2,y:sourceCoord.y+1},pathToJunction=getPath(graph.grid,sourceExit,junction);edge.pathToJunction=pathToJunction?mergePath(pathToJunction):[sourceExit,junction],edge.startDir=dir==="TD"?Down:Right,edge.endDir=dir==="TD"?Up:Left,edge.path=[...edge.pathToJunction,...bundle.sharedPath.slice(1)]}}else{bundle.junctionDir=dir==="TD"?Down:Right,bundle.sharedNodeDir=dir==="TD"?Up:Left;let sourceCoord=bundle.sharedNode.gridCoord,sourceExit=dir==="TD"?{x:sourceCoord.x+1,y:sourceCoord.y+2}:{x:sourceCoord.x+2,y:sourceCoord.y+1},sharedPath=getPath(graph.grid,sourceExit,junction);bundle.sharedPath=sharedPath?mergePath(sharedPath):[sourceExit,junction];for(let edge of bundle.edges){let targetCoord=edge.to.gridCoord,targetEntry=dir==="TD"?{x:targetCoord.x+1,y:targetCoord.y}:{x:targetCoord.x,y:targetCoord.y+1},pathToJunction=getPath(graph.grid,junction,targetEntry);edge.pathToJunction=pathToJunction?mergePath(pathToJunction):[junction,targetEntry],edge.startDir=dir==="TD"?Down:Right,edge.endDir=dir==="TD"?Up:Left,edge.path=[...bundle.sharedPath,...edge.pathToJunction.slice(1)]}}}function processBundles(graph){for(let bundle of graph.bundles)routeBundledEdges(graph,bundle)}function splitLines(label){return label.split(`
6`)}function maxLineWidth(label){let lines=splitLines(label);return Math.max(...lines.map(l=>l.length),0)}function lineCount(label){return splitLines(label).length}var SHAPE_CORNERS={rectangle:{unicode:{tl:"\u250C",tr:"\u2510",bl:"\u2514",br:"\u2518"},ascii:{tl:"+",tr:"+",bl:"+",br:"+"}},rounded:{unicode:{tl:"\u256D",tr:"\u256E",bl:"\u2570",br:"\u256F"},ascii:{tl:".",tr:".",bl:"'",br:"'"}},circle:{unicode:{tl:"\u25EF",tr:"\u25EF",bl:"\u25EF",br:"\u25EF"},ascii:{tl:"o",tr:"o",bl:"o",br:"o"}},doublecircle:{unicode:{tl:"\u25CE",tr:"\u25CE",bl:"\u25CE",br:"\u25CE"},ascii:{tl:"@",tr:"@",bl:"@",br:"@"}},diamond:{unicode:{tl:"\u25C7",tr:"\u25C7",bl:"\u25C7",br:"\u25C7"},ascii:{tl:"<",tr:">",bl:"<",br:">"}},hexagon:{unicode:{tl:"\u231C",tr:"\u231D",bl:"\u231E",br:"\u231F"},ascii:{tl:"*",tr:"*",bl:"*",br:"*"}},stadium:{unicode:{tl:"(",tr:")",bl:"(",br:")"},ascii:{tl:"(",tr:")",bl:"(",br:")"}},subroutine:{unicode:{tl:"\u255F",tr:"\u2562",bl:"\u255F",br:"\u2562"},ascii:{tl:"|",tr:"|",bl:"|",br:"|"}},cylinder:{unicode:{tl:"\u256D",tr:"\u256E",bl:"\u2570",br:"\u256F"},ascii:{tl:".",tr:".",bl:"'",br:"'"}},asymmetric:{unicode:{tl:"\u25B7",tr:"\u2510",bl:"\u25B7",br:"\u2518"},ascii:{tl:">",tr:"+",bl:">",br:"+"}},trapezoid:{unicode:{tl:"/",tr:"\\",bl:"\u2514",br:"\u2518"},ascii:{tl:"/",tr:"\\",bl:"+",br:"+"}},"trapezoid-alt":{unicode:{tl:"\u250C",tr:"\u2510",bl:"\\",br:"/"},ascii:{tl:"+",tr:"+",bl:"\\",br:"/"}},"state-start":{unicode:{tl:"\u25CF",tr:"\u25CF",bl:"\u25CF",br:"\u25CF"},ascii:{tl:"*",tr:"*",bl:"*",br:"*"}},"state-end":{unicode:{tl:"\u25C9",tr:"\u25C9",bl:"\u25C9",br:"\u25C9"},ascii:{tl:"@",tr:"@",bl:"@",br:"@"}}};function getCorners(shape,useAscii){let corners=SHAPE_CORNERS[shape]??SHAPE_CORNERS.rectangle;return useAscii?corners.ascii:corners.unicode}function getBoxDimensions(label,options){let lines=splitLines(label),maxLineWidth2=Math.max(...lines.map(l=>l.length),0),lineCount2=lines.length,innerWidth=2*options.padding+maxLineWidth2,width=innerWidth+2,rawInnerHeight=lineCount2+2*options.padding,innerHeight=rawInnerHeight%2===0?rawInnerHeight+1:rawInnerHeight,height=innerHeight+2;return{width,height,labelArea:{x:1+options.padding,y:1+options.padding,width:maxLineWidth2,height:lineCount2},gridColumns:[1,innerWidth,1],gridRows:[1,innerHeight,1]}}function renderBox(label,dimensions,corners,useAscii){let{width,height}=dimensions,canvas=mkCanvas(width-1,height-1),from={x:0,y:0},to={x:width-1,y:height-1},hLine=useAscii?"-":"\u2500",vLine=useAscii?"|":"\u2502";for(let x=from.x+1;x<to.x;x++)canvas[x][from.y]=hLine,canvas[x][to.y]=hLine;for(let y=from.y+1;y<to.y;y++)canvas[from.x][y]=vLine,canvas[to.x][y]=vLine;canvas[from.x][from.y]=corners.tl,canvas[to.x][from.y]=corners.tr,canvas[from.x][to.y]=corners.bl,canvas[to.x][to.y]=corners.br;let lines=splitLines(label),w=width-1,h=height-1,startY=Math.floor(h/2)-Math.floor((lines.length-1)/2);for(let i=0;i<lines.length;i++){let line=lines[i],textX=Math.floor(w/2)-Math.ceil(line.length/2)+1;for(let j=0;j<line.length;j++){let x=textX+j,y=startY+i;x>=0&&x<canvas.length&&y>=0&&y<canvas[0].length&&(canvas[x][y]=line[j])}}return canvas}function getBoxAttachmentPoint(dir,dimensions,baseCoord){let{width,height}=dimensions,centerX=baseCoord.x+Math.floor(width/2),centerY=baseCoord.y+Math.floor(height/2);return dirEquals(dir,Up)?{x:centerX,y:baseCoord.y}:dirEquals(dir,Down)?{x:centerX,y:baseCoord.y+height-1}:dirEquals(dir,Left)?{x:baseCoord.x,y:centerY}:dirEquals(dir,Right)?{x:baseCoord.x+width-1,y:centerY}:dirEquals(dir,UpperLeft)?{x:baseCoord.x,y:baseCoord.y}:dirEquals(dir,UpperRight)?{x:baseCoord.x+width-1,y:baseCoord.y}:dirEquals(dir,LowerLeft)?{x:baseCoord.x,y:baseCoord.y+height-1}:dirEquals(dir,LowerRight)?{x:baseCoord.x+width-1,y:baseCoord.y+height-1}:{x:centerX,y:centerY}}var rectangleRenderer={getDimensions:getBoxDimensions,render(label,dimensions,options){let corners=getCorners("rectangle",options.useAscii);return renderBox(label,dimensions,corners,options.useAscii)},getAttachmentPoint:getBoxAttachmentPoint};var diamondRenderer={getDimensions:getBoxDimensions,render(label,dimensions,options){let corners=getCorners("diamond",options.useAscii);return renderBox(label,dimensions,corners,options.useAscii)},getAttachmentPoint:getBoxAttachmentPoint};var circleRenderer={getDimensions:getBoxDimensions,render(label,dimensions,options){let corners=getCorners("circle",options.useAscii);return renderBox(label,dimensions,corners,options.useAscii)},getAttachmentPoint:getBoxAttachmentPoint};var stateStartRenderer={getDimensions(_label,_options){return{width:5,height:3,labelArea:{x:2,y:1,width:1,height:1},gridColumns:[1,3,1],gridRows:[1,1,1]}},render(_label,dimensions,options){let{width,height}=dimensions,canvas=mkCanvas(width-1,height-1),centerX=Math.floor(width/2);return options.useAscii?(canvas[0][0]=".",canvas[1][0]="-",canvas[2][0]="-",canvas[3][0]="-",canvas[4][0]=".",canvas[0][1]="|",canvas[centerX][1]="*",canvas[4][1]="|",canvas[0][2]="'",canvas[1][2]="-",canvas[2][2]="-",canvas[3][2]="-",canvas[4][2]="'"):(canvas[0][0]="\u256D",canvas[1][0]="\u2500",canvas[2][0]="\u2500",canvas[3][0]="\u2500",canvas[4][0]="\u256E",canvas[0][1]="\u2502",canvas[centerX][1]="\u25CF",canvas[4][1]="\u2502",canvas[0][2]="\u2570",canvas[1][2]="\u2500",canvas[2][2]="\u2500",canvas[3][2]="\u2500",canvas[4][2]="\u256F"),canvas},getAttachmentPoint(dir,dimensions,baseCoord){let{width,height}=dimensions,centerX=baseCoord.x+Math.floor(width/2),centerY=baseCoord.y+Math.floor(height/2);return dirEquals(dir,Up)?{x:centerX,y:baseCoord.y}:dirEquals(dir,Down)?{x:centerX,y:baseCoord.y+height-1}:dirEquals(dir,Left)?{x:baseCoord.x,y:centerY}:dirEquals(dir,Right)?{x:baseCoord.x+width-1,y:centerY}:{x:centerX,y:centerY}}},stateEndRenderer={getDimensions(_label,_options){return{width:5,height:3,labelArea:{x:2,y:1,width:1,height:1},gridColumns:[1,3,1],gridRows:[1,1,1]}},render(_label,dimensions,options){let{width,height}=dimensions,canvas=mkCanvas(width-1,height-1),centerX=Math.floor(width/2);return options.useAscii?(canvas[0][0]="#",canvas[1][0]="=",canvas[2][0]="=",canvas[3][0]="=",canvas[4][0]="#",canvas[0][1]="#",canvas[centerX][1]="*",canvas[4][1]="#",canvas[0][2]="#",canvas[1][2]="=",canvas[2][2]="=",canvas[3][2]="=",canvas[4][2]="#"):(canvas[0][0]="\u2554",canvas[1][0]="\u2550",canvas[2][0]="\u2550",canvas[3][0]="\u2550",canvas[4][0]="\u2557",canvas[0][1]="\u2551",canvas[centerX][1]="\u25CE",canvas[4][1]="\u2551",canvas[0][2]="\u255A",canvas[1][2]="\u2550",canvas[2][2]="\u2550",canvas[3][2]="\u2550",canvas[4][2]="\u255D"),canvas},getAttachmentPoint(dir,dimensions,baseCoord){let{width,height}=dimensions,centerX=baseCoord.x+Math.floor(width/2),centerY=baseCoord.y+Math.floor(height/2);return dirEquals(dir,Up)?{x:centerX,y:baseCoord.y}:dirEquals(dir,Down)?{x:centerX,y:baseCoord.y+height-1}:dirEquals(dir,Left)?{x:baseCoord.x,y:centerY}:dirEquals(dir,Right)?{x:baseCoord.x+width-1,y:centerY}:{x:centerX,y:centerY}}};var roundedRenderer={getDimensions:getBoxDimensions,render(label,dimensions,options){let corners=getCorners("rounded",options.useAscii);return renderBox(label,dimensions,corners,options.useAscii)},getAttachmentPoint:getBoxAttachmentPoint};var stadiumRenderer={getDimensions(label,options){let lines=splitLines(label),maxLineWidth2=Math.max(...lines.map(l=>l.length),0),lineCount2=lines.length,innerWidth=2*options.padding+maxLineWidth2,width=innerWidth+4,innerHeight=lineCount2+2*options.padding,height=Math.max(innerHeight+2,3);return{width,height,labelArea:{x:2+options.padding,y:1+options.padding,width:maxLineWidth2,height:lineCount2},gridColumns:[2,innerWidth,2],gridRows:[1,innerHeight,1]}},render(label,dimensions,options){let{width,height}=dimensions,canvas=mkCanvas(width-1,height-1),centerY=Math.floor(height/2),hChar=options.useAscii?"-":"\u2500";if(height===3)canvas[0][centerY]="(",canvas[width-1][centerY]=")";else if(options.useAscii){for(let y=0;y<height;y++)canvas[0][y]="(",canvas[width-1][y]=")";for(let x=1;x<width-1;x++)canvas[x][0]=hChar,canvas[x][height-1]=hChar}else{canvas[0][0]="\u256D";for(let x=1;x<width-1;x++)canvas[x][0]=hChar;canvas[width-1][0]="\u256E";for(let y=1;y<height-1;y++)canvas[0][y]="\u2502",canvas[width-1][y]="\u2502";canvas[0][height-1]="\u2570";for(let x=1;x<width-1;x++)canvas[x][height-1]=hChar;canvas[width-1][height-1]="\u256F"}let lines=splitLines(label),startY=centerY-Math.floor((lines.length-1)/2);for(let i=0;i<lines.length;i++){let line=lines[i],textX=Math.floor(width/2)-Math.floor(line.length/2);for(let j=0;j<line.length;j++){let x=textX+j,y=startY+i;x>0&&x<width-1&&y>=0&&y<height&&(canvas[x][y]=line[j])}}return canvas},getAttachmentPoint:getBoxAttachmentPoint};var hexagonRenderer={getDimensions:getBoxDimensions,render(label,dimensions,options){let corners=getCorners("hexagon",options.useAscii);return renderBox(label,dimensions,corners,options.useAscii)},getAttachmentPoint:getBoxAttachmentPoint};var subroutineRenderer={getDimensions(label,options){let lines=splitLines(label),maxLineWidth2=Math.max(...lines.map(l=>l.length),0),lineCount2=lines.length,innerWidth=2*options.padding+maxLineWidth2,width=innerWidth+4,innerHeight=lineCount2+2*options.padding,height=innerHeight+2;return{width,height,labelArea:{x:2+options.padding,y:1+options.padding,width:maxLineWidth2,height:lineCount2},gridColumns:[2,innerWidth,2],gridRows:[1,innerHeight,1]}},render(label,dimensions,options){let{width,height}=dimensions,canvas=mkCanvas(width-1,height-1),hChar=options.useAscii?"-":"\u2500",vChar=options.useAscii?"|":"\u2502";canvas[0][0]=options.useAscii?"+":"\u250C",canvas[1][0]=options.useAscii?"+":"\u252C";for(let x=2;x<width-2;x++)canvas[x][0]=hChar;canvas[width-2][0]=options.useAscii?"+":"\u252C",canvas[width-1][0]=options.useAscii?"+":"\u2510";for(let y=1;y<height-1;y++)canvas[0][y]=vChar,canvas[1][y]=vChar,canvas[width-2][y]=vChar,canvas[width-1][y]=vChar;canvas[0][height-1]=options.useAscii?"+":"\u2514",canvas[1][height-1]=options.useAscii?"+":"\u2534";for(let x=2;x<width-2;x++)canvas[x][height-1]=hChar;canvas[width-2][height-1]=options.useAscii?"+":"\u2534",canvas[width-1][height-1]=options.useAscii?"+":"\u2518";let lines=splitLines(label),startY=Math.floor(height/2)-Math.floor((lines.length-1)/2);for(let i=0;i<lines.length;i++){let line=lines[i],textX=Math.floor(width/2)-Math.floor(line.length/2);for(let j=0;j<line.length;j++){let x=textX+j,y=startY+i;x>1&&x<width-2&&y>0&&y<height-1&&(canvas[x][y]=line[j])}}return canvas},getAttachmentPoint:getBoxAttachmentPoint},doublecircleRenderer={getDimensions:getBoxDimensions,render(label,dimensions,options){let corners=getCorners("doublecircle",options.useAscii);return renderBox(label,dimensions,corners,options.useAscii)},getAttachmentPoint:getBoxAttachmentPoint},cylinderRenderer={getDimensions(label,options){let lines=splitLines(label),maxLineWidth2=Math.max(...lines.map(l=>l.length),0),lineCount2=lines.length,innerWidth=2*options.padding+maxLineWidth2,width=innerWidth+2,innerHeight=lineCount2+2*options.padding+2,height=innerHeight+2;return{width,height,labelArea:{x:1+options.padding,y:2+options.padding,width:maxLineWidth2,height:lineCount2},gridColumns:[1,innerWidth,1],gridRows:[2,innerHeight-2,2]}},render(label,dimensions,options){let{width,height}=dimensions,canvas=mkCanvas(width-1,height-1),hChar=options.useAscii?"-":"\u2500",vChar=options.useAscii?"|":"\u2502";canvas[0][0]=options.useAscii?".":"\u256D";for(let x=1;x<width-1;x++)canvas[x][0]=hChar;canvas[width-1][0]=options.useAscii?".":"\u256E",canvas[0][1]=vChar;for(let x=1;x<width-1;x++)canvas[x][1]=hChar;canvas[width-1][1]=vChar;for(let y=2;y<height-2;y++)canvas[0][y]=vChar,canvas[width-1][y]=vChar;canvas[0][height-2]=vChar;for(let x=1;x<width-1;x++)canvas[x][height-2]=hChar;canvas[width-1][height-2]=vChar,canvas[0][height-1]=options.useAscii?"'":"\u2570";for(let x=1;x<width-1;x++)canvas[x][height-1]=hChar;canvas[width-1][height-1]=options.useAscii?"'":"\u256F";let lines=splitLines(label),startY=Math.floor(height/2)-Math.floor((lines.length-1)/2);for(let i=0;i<lines.length;i++){let line=lines[i],textX=Math.floor(width/2)-Math.floor(line.length/2);for(let j=0;j<line.length;j++){let x=textX+j,y=startY+i;x>0&&x<width-1&&y>1&&y<height-2&&(canvas[x][y]=line[j])}}return canvas},getAttachmentPoint:getBoxAttachmentPoint},asymmetricRenderer={getDimensions:getBoxDimensions,render(label,dimensions,options){let corners=getCorners("asymmetric",options.useAscii);return renderBox(label,dimensions,corners,options.useAscii)},getAttachmentPoint:getBoxAttachmentPoint},trapezoidRenderer={getDimensions:getBoxDimensions,render(label,dimensions,options){let corners=getCorners("trapezoid",options.useAscii);return renderBox(label,dimensions,corners,options.useAscii)},getAttachmentPoint:getBoxAttachmentPoint},trapezoidAltRenderer={getDimensions:getBoxDimensions,render(label,dimensions,options){let corners=getCorners("trapezoid-alt",options.useAscii);return renderBox(label,dimensions,corners,options.useAscii)},getAttachmentPoint:getBoxAttachmentPoint};var shapeRegistry=new Map([["rectangle",rectangleRenderer],["rounded",roundedRenderer],["diamond",diamondRenderer],["stadium",stadiumRenderer],["circle",circleRenderer],["subroutine",subroutineRenderer],["doublecircle",doublecircleRenderer],["hexagon",hexagonRenderer],["cylinder",cylinderRenderer],["asymmetric",asymmetricRenderer],["trapezoid",trapezoidRenderer],["trapezoid-alt",trapezoidAltRenderer],["state-start",stateStartRenderer],["state-end",stateEndRenderer]]);function getShapeRenderer(shape){return shapeRegistry.get(shape)??rectangleRenderer}function getShapeDimensions(shape,label,options){return getShapeRenderer(shape).getDimensions(label,options)}function getShapeAttachmentPoint(shape,dir,dimensions,baseCoord){return getShapeRenderer(shape).getAttachmentPoint(dir,dimensions,baseCoord)}function drawNode(node,graph){return drawBoxWithGridDimensions(node,graph)}function drawBoxWithGridDimensions(node,graph){let gc=node.gridCoord,useAscii=graph.config.useAscii,w=0;for(let i=0;i<2;i++)w+=graph.columnWidth.get(gc.x+i)??0;let h=0;for(let i=0;i<2;i++)h+=graph.rowHeight.get(gc.y+i)??0;let from={x:0,y:0},to={x:w,y:h},box=mkCanvas(Math.max(from.x,to.x),Math.max(from.y,to.y)),corners=getCorners(node.shape,useAscii),isDoubleBox=node.shape==="state-end",hChar=useAscii?isDoubleBox?"=":"-":isDoubleBox?"\u2550":"\u2500",vChar=useAscii?isDoubleBox?"\u2016":"|":isDoubleBox?"\u2551":"\u2502",effectiveCorners=isDoubleBox?useAscii?{tl:"#",tr:"#",bl:"#",br:"#"}:{tl:"\u2554",tr:"\u2557",bl:"\u255A",br:"\u255D"}:corners;for(let x=from.x+1;x<to.x;x++)box[x][from.y]=hChar;for(let x=from.x+1;x<to.x;x++)box[x][to.y]=hChar;for(let y=from.y+1;y<to.y;y++)box[from.x][y]=vChar;for(let y=from.y+1;y<to.y;y++)box[to.x][y]=vChar;box[from.x][from.y]=effectiveCorners.tl,box[to.x][from.y]=effectiveCorners.tr,box[from.x][to.y]=effectiveCorners.bl,box[to.x][to.y]=effectiveCorners.br;let label=node.displayLabel,lines=splitLines(label),startY=from.y+Math.floor(h/2)-Math.floor((lines.length-1)/2);for(let i=0;i<lines.length;i++){let line=lines[i],textX=from.x+Math.floor(w/2)-Math.ceil(line.length/2)+1;for(let j=0;j<line.length;j++)textX+j>=0&&textX+j<box.length&&startY+i>=0&&startY+i<box[0].length&&(box[textX+j][startY+i]=line[j])}return box}function drawBox(node,graph){return drawNode(node,graph)}function drawMultiBox(sections,useAscii,padding=1){let maxTextWidth=0;for(let section of sections)for(let line of section)maxTextWidth=Math.max(maxTextWidth,line.length);let boxWidth=maxTextWidth+2*padding+2,totalLines=0;for(let section of sections)totalLines+=Math.max(section.length,1);let numDividers=sections.length-1,boxHeight=totalLines+numDividers+2,hLine=useAscii?"-":"\u2500",vLine=useAscii?"|":"\u2502",tl=useAscii?"+":"\u250C",tr=useAscii?"+":"\u2510",bl=useAscii?"+":"\u2514",br=useAscii?"+":"\u2518",divL=useAscii?"+":"\u251C",divR=useAscii?"+":"\u2524",canvas=mkCanvas(boxWidth-1,boxHeight-1);canvas[0][0]=tl;for(let x=1;x<boxWidth-1;x++)canvas[x][0]=hLine;canvas[boxWidth-1][0]=tr,canvas[0][boxHeight-1]=bl;for(let x=1;x<boxWidth-1;x++)canvas[x][boxHeight-1]=hLine;canvas[boxWidth-1][boxHeight-1]=br;for(let y=1;y<boxHeight-1;y++)canvas[0][y]=vLine,canvas[boxWidth-1][y]=vLine;let row=1;for(let s=0;s<sections.length;s++){let section=sections[s],lines=section.length>0?section:[""];for(let line of lines){let startX=1+padding;for(let i=0;i<line.length;i++)canvas[startX+i][row]=line[i];row++}if(s<sections.length-1){canvas[0][row]=divL;for(let x=1;x<boxWidth-1;x++)canvas[x][row]=hLine;canvas[boxWidth-1][row]=divR,row++}}return canvas}var LINE_CHARS={solid:{h:{unicode:"\u2500",ascii:"-"},v:{unicode:"\u2502",ascii:"|"}},dotted:{h:{unicode:"\u2504",ascii:"."},v:{unicode:"\u2506",ascii:":"}},thick:{h:{unicode:"\u2501",ascii:"="},v:{unicode:"\u2503",ascii:"\u2016"}}};function drawLine(canvas,from,to,offsetFrom,offsetTo,useAscii,style="solid"){let dir=determineDirection(from,to),drawnCoords=[],chars=LINE_CHARS[style],hChar=useAscii?chars.h.ascii:chars.h.unicode,vChar=useAscii?chars.v.ascii:chars.v.unicode;if(dirEquals(dir,Up))for(let y=from.y-offsetFrom;y>=to.y-offsetTo;y--)drawnCoords.push({x:from.x,y}),canvas[from.x][y]=vChar;else if(dirEquals(dir,Down))for(let y=from.y+offsetFrom;y<=to.y+offsetTo;y++)drawnCoords.push({x:from.x,y}),canvas[from.x][y]=vChar;else if(dirEquals(dir,Left))for(let x=from.x-offsetFrom;x>=to.x-offsetTo;x--)drawnCoords.push({x,y:from.y}),canvas[x][from.y]=hChar;else if(dirEquals(dir,Right))for(let x=from.x+offsetFrom;x<=to.x+offsetTo;x++)drawnCoords.push({x,y:from.y}),canvas[x][from.y]=hChar;else if(dirEquals(dir,UpperLeft)){for(let x=from.x-offsetFrom;x>=to.x;x--)drawnCoords.push({x,y:from.y}),canvas[x][from.y]=hChar;for(let y=from.y-1;y>=to.y-offsetTo;y--)drawnCoords.push({x:to.x,y}),canvas[to.x][y]=vChar}else if(dirEquals(dir,UpperRight)){for(let x=from.x+offsetFrom;x<=to.x;x++)drawnCoords.push({x,y:from.y}),canvas[x][from.y]=hChar;for(let y=from.y-1;y>=to.y-offsetTo;y--)drawnCoords.push({x:to.x,y}),canvas[to.x][y]=vChar}else if(dirEquals(dir,LowerLeft)){for(let x=from.x-offsetFrom;x>=to.x;x--)drawnCoords.push({x,y:from.y}),canvas[x][from.y]=hChar;for(let y=from.y+1;y<=to.y+offsetTo;y++)drawnCoords.push({x:to.x,y}),canvas[to.x][y]=vChar}else if(dirEquals(dir,LowerRight))if(to.x-from.x<=1)for(let y=from.y+offsetFrom;y<=to.y+offsetTo;y++)drawnCoords.push({x:from.x,y}),canvas[from.x][y]=vChar;else{for(let x=from.x+offsetFrom;x<=to.x;x++)drawnCoords.push({x,y:from.y}),canvas[x][from.y]=hChar;for(let y=from.y+1;y<=to.y+offsetTo;y++)drawnCoords.push({x:to.x,y}),canvas[to.x][y]=vChar}return drawnCoords}function drawArrow(graph,edge){if(edge.path.length===0){let empty=copyCanvas(graph.canvas);return[empty,empty,empty,empty,empty,empty]}let labelCanvas=drawArrowLabel(graph,edge),[pathCanvas,linesDrawn,lineDirs]=drawPath(graph,edge.path,edge.style),boxStartCanvas=drawBoxStart(graph,edge.path,linesDrawn[0],edge.from.shape),arrowHeadEndCanvas;edge.hasArrowEnd?arrowHeadEndCanvas=drawArrowHead(graph,linesDrawn[linesDrawn.length-1],lineDirs[lineDirs.length-1]):arrowHeadEndCanvas=copyCanvas(graph.canvas);let arrowHeadStartCanvas;if(edge.hasArrowStart&&linesDrawn.length>0){let firstPoint=linesDrawn[0][0],startDir=reverseDirection(lineDirs[0]),arrowPos={x:firstPoint.x,y:firstPoint.y};dirEquals(lineDirs[0],Right)?arrowPos.x=firstPoint.x-1:dirEquals(lineDirs[0],Left)?arrowPos.x=firstPoint.x+1:dirEquals(lineDirs[0],Down)?arrowPos.y=firstPoint.y-1:dirEquals(lineDirs[0],Up)&&(arrowPos.y=firstPoint.y+1),arrowHeadStartCanvas=drawArrowHead(graph,[firstPoint,arrowPos],startDir)}else arrowHeadStartCanvas=copyCanvas(graph.canvas);let cornersCanvas=drawCorners(graph,edge.path);return[pathCanvas,boxStartCanvas,arrowHeadEndCanvas,arrowHeadStartCanvas,cornersCanvas,labelCanvas]}function reverseDirection(dir){return dirEquals(dir,Up)?Down:dirEquals(dir,Down)?Up:dirEquals(dir,Left)?Right:dirEquals(dir,Right)?Left:dirEquals(dir,UpperLeft)?LowerRight:dirEquals(dir,UpperRight)?LowerLeft:dirEquals(dir,LowerLeft)?UpperRight:dirEquals(dir,LowerRight)?UpperLeft:Middle}function drawPath(graph,path,style="solid"){let canvas=copyCanvas(graph.canvas),previousCoord=path[0],linesDrawn=[],lineDirs=[];for(let i=1;i<path.length;i++){let nextCoord=path[i],prevDC=gridToDrawingCoord(graph,previousCoord),nextDC=gridToDrawingCoord(graph,nextCoord);if(drawingCoordEquals(prevDC,nextDC)){previousCoord=nextCoord;continue}let dir=determineDirection(previousCoord,nextCoord),segment=drawLine(canvas,prevDC,nextDC,1,-1,graph.config.useAscii,style);segment.length===0&&segment.push(prevDC),linesDrawn.push(segment),lineDirs.push(dir),previousCoord=nextCoord}return[canvas,linesDrawn,lineDirs]}function drawBoxStart(graph,path,firstLine,sourceShape){let canvas=copyCanvas(graph.canvas);if(graph.config.useAscii||sourceShape==="state-start"||sourceShape==="state-end")return canvas;let from=firstLine[0],dir=determineDirection(path[0],path[1]);return dirEquals(dir,Up)?canvas[from.x][from.y+1]="\u2534":dirEquals(dir,Down)?canvas[from.x][from.y-1]="\u252C":dirEquals(dir,Left)?canvas[from.x+1][from.y]="\u2524":dirEquals(dir,Right)&&(canvas[from.x-1][from.y]="\u251C"),canvas}function drawArrowHead(graph,lastLine,fallbackDir){let canvas=copyCanvas(graph.canvas);if(lastLine.length===0)return canvas;let from=lastLine[0],lastPos=lastLine[lastLine.length-1],dir=determineDirection(from,lastPos);(lastLine.length===1||dirEquals(dir,Middle))&&(dir=fallbackDir);let char;return graph.config.useAscii?dirEquals(dir,Up)?char="^":dirEquals(dir,Down)?char="v":dirEquals(dir,Left)?char="<":dirEquals(dir,Right)?char=">":dirEquals(fallbackDir,Up)?char="^":dirEquals(fallbackDir,Down)?char="v":dirEquals(fallbackDir,Left)?char="<":dirEquals(fallbackDir,Right)?char=">":char="*":dirEquals(dir,Up)?char="\u25B2":dirEquals(dir,Down)?char="\u25BC":dirEquals(dir,Left)?char="\u25C4":dirEquals(dir,Right)?char="\u25BA":dirEquals(dir,UpperRight)?char="\u25E5":dirEquals(dir,UpperLeft)?char="\u25E4":dirEquals(dir,LowerRight)?char="\u25E2":dirEquals(dir,LowerLeft)?char="\u25E3":dirEquals(fallbackDir,Up)?char="\u25B2":dirEquals(fallbackDir,Down)?char="\u25BC":dirEquals(fallbackDir,Left)?char="\u25C4":dirEquals(fallbackDir,Right)?char="\u25BA":dirEquals(fallbackDir,UpperRight)?char="\u25E5":dirEquals(fallbackDir,UpperLeft)?char="\u25E4":dirEquals(fallbackDir,LowerRight)?char="\u25E2":dirEquals(fallbackDir,LowerLeft)?char="\u25E3":char="\u25CF",canvas[lastPos.x][lastPos.y]=char,canvas}function drawCorners(graph,path){let canvas=copyCanvas(graph.canvas);for(let idx=1;idx<path.length-1;idx++){let coord=path[idx],dc=gridToDrawingCoord(graph,coord),prevDir=determineDirection(path[idx-1],coord),nextDir=determineDirection(coord,path[idx+1]),corner;graph.config.useAscii?corner="+":dirEquals(prevDir,Right)&&dirEquals(nextDir,Down)||dirEquals(prevDir,Up)&&dirEquals(nextDir,Left)?corner="\u2510":dirEquals(prevDir,Right)&&dirEquals(nextDir,Up)||dirEquals(prevDir,Down)&&dirEquals(nextDir,Left)?corner="\u2518":dirEquals(prevDir,Left)&&dirEquals(nextDir,Down)||dirEquals(prevDir,Up)&&dirEquals(nextDir,Right)?corner="\u250C":dirEquals(prevDir,Left)&&dirEquals(nextDir,Up)||dirEquals(prevDir,Down)&&dirEquals(nextDir,Right)?corner="\u2514":corner="+",canvas[dc.x][dc.y]=corner}return canvas}function drawArrowLabel(graph,edge){let canvas=copyCanvas(graph.canvas);if(edge.text.length===0)return canvas;let drawingLine=lineToDrawing(graph,edge.labelLine),isUpwardEdge;if(edge.path.length>=2){let startY=edge.path[0].y,endY=edge.path[edge.path.length-1].y;endY<startY?isUpwardEdge=!0:endY>startY&&(isUpwardEdge=!1)}return drawTextOnLine(canvas,drawingLine,edge.text,isUpwardEdge),canvas}function drawTextOnLine(canvas,line,label,isUpwardEdge){if(line.length<2)return;let minX=Math.min(line[0].x,line[1].x),maxX=Math.max(line[0].x,line[1].x),minY=Math.min(line[0].y,line[1].y),maxY=Math.max(line[0].y,line[1].y),middleX=minX+Math.floor((maxX-minX)/2),middleY=minY+Math.floor((maxY-minY)/2);if(isUpwardEdge!==void 0&&minX===maxX){let segmentHeight=maxY-minY,offset=Math.max(1,Math.floor(segmentHeight/4));isUpwardEdge?middleY=middleY+offset:middleY=middleY-offset}let lines=splitLines(label),startY=middleY-Math.floor((lines.length-1)/2);for(let i=0;i<lines.length;i++){let lineText=lines[i],startX=middleX-Math.floor(lineText.length/2);drawText(canvas,{x:startX,y:startY+i},lineText)}}function getNodeAttachmentPoint(graph,node,dir){let gc=node.gridCoord,w=0;for(let i=0;i<2;i++)w+=graph.columnWidth.get(gc.x+i)??0;let h=0;for(let i=0;i<2;i++)h+=graph.rowHeight.get(gc.y+i)??0;let gridDimensions={width:w+1,height:h+1,labelArea:{x:0,y:0,width:0,height:0},gridColumns:[0,0,0],gridRows:[0,0,0]},baseCoord=node.drawingCoord;return getShapeAttachmentPoint(node.shape,dir,gridDimensions,baseCoord)}function drawBundledEdgeSegment(graph,edge,bundle){let empty=copyCanvas(graph.canvas);if(!edge.pathToJunction||edge.pathToJunction.length===0)return[empty,empty,empty,empty,empty,empty];let pathCanvas=copyCanvas(graph.canvas),useAscii=graph.config.useAscii,drawingPath=edge.pathToJunction.map((gc,idx)=>bundle.type==="fan-in"&&idx===0?getNodeAttachmentPoint(graph,edge.from,edge.startDir):bundle.type==="fan-out"&&idx===edge.pathToJunction.length-1?getNodeAttachmentPoint(graph,edge.to,edge.endDir):gridToDrawingCoord(graph,gc));for(let i=1;i<drawingPath.length;i++){let from=drawingPath[i-1],to=drawingPath[i];drawingCoordEquals(from,to)||drawLine(pathCanvas,from,to,1,-1,useAscii,edge.style)}let cornersCanvas=copyCanvas(graph.canvas);for(let idx=1;idx<edge.pathToJunction.length-1;idx++){let coord=edge.pathToJunction[idx],dc=gridToDrawingCoord(graph,coord),prevDir=determineDirection(edge.pathToJunction[idx-1],coord),nextDir=determineDirection(coord,edge.pathToJunction[idx+1]),corner;useAscii?corner="+":dirEquals(prevDir,Right)&&dirEquals(nextDir,Down)||dirEquals(prevDir,Up)&&dirEquals(nextDir,Left)?corner="\u2510":dirEquals(prevDir,Right)&&dirEquals(nextDir,Up)||dirEquals(prevDir,Down)&&dirEquals(nextDir,Left)?corner="\u2518":dirEquals(prevDir,Left)&&dirEquals(nextDir,Down)||dirEquals(prevDir,Up)&&dirEquals(nextDir,Right)?corner="\u250C":dirEquals(prevDir,Left)&&dirEquals(nextDir,Up)||dirEquals(prevDir,Down)&&dirEquals(nextDir,Right)?corner="\u2514":corner="+",cornersCanvas[dc.x][dc.y]=corner}let boxStartCanvas=copyCanvas(graph.canvas);if(bundle.type==="fan-in"&&edge.pathToJunction.length>=2){let firstPoint=drawingPath[0],dir=determineDirection(edge.pathToJunction[0],edge.pathToJunction[1]);useAscii||(dirEquals(dir,Up)?boxStartCanvas[firstPoint.x][firstPoint.y]="\u2534":dirEquals(dir,Down)?boxStartCanvas[firstPoint.x][firstPoint.y]="\u252C":dirEquals(dir,Left)?boxStartCanvas[firstPoint.x][firstPoint.y]="\u2524":dirEquals(dir,Right)&&(boxStartCanvas[firstPoint.x][firstPoint.y]="\u251C"))}let labelCanvas=copyCanvas(graph.canvas);return[pathCanvas,boxStartCanvas,empty,empty,cornersCanvas,labelCanvas]}function drawBundleSharedPath(graph,bundle){let pathCanvas=copyCanvas(graph.canvas),cornersCanvas=copyCanvas(graph.canvas);if(bundle.sharedPath.length<2)return[pathCanvas,cornersCanvas];let useAscii=graph.config.useAscii,style=bundle.edges[0]?.style??"solid",graphDir=graph.config.graphDirection,drawingPath=bundle.sharedPath.map((gc,idx)=>{if(bundle.type==="fan-in"&&idx===bundle.sharedPath.length-1){let entryDir=graphDir==="TD"?Up:Left;return getNodeAttachmentPoint(graph,bundle.sharedNode,entryDir)}if(bundle.type==="fan-out"&&idx===0){let exitDir=graphDir==="TD"?Down:Right;return getNodeAttachmentPoint(graph,bundle.sharedNode,exitDir)}return gridToDrawingCoord(graph,gc)});for(let i=1;i<drawingPath.length;i++){let from=drawingPath[i-1],to=drawingPath[i];drawingCoordEquals(from,to)||drawLine(pathCanvas,from,to,1,-1,useAscii,style)}for(let idx=1;idx<bundle.sharedPath.length-1;idx++){let coord=bundle.sharedPath[idx],dc=gridToDrawingCoord(graph,coord),prevDir=determineDirection(bundle.sharedPath[idx-1],coord),nextDir=determineDirection(coord,bundle.sharedPath[idx+1]),corner;useAscii?corner="+":dirEquals(prevDir,Right)&&dirEquals(nextDir,Down)||dirEquals(prevDir,Up)&&dirEquals(nextDir,Left)?corner="\u2510":dirEquals(prevDir,Right)&&dirEquals(nextDir,Up)||dirEquals(prevDir,Down)&&dirEquals(nextDir,Left)?corner="\u2518":dirEquals(prevDir,Left)&&dirEquals(nextDir,Down)||dirEquals(prevDir,Up)&&dirEquals(nextDir,Right)?corner="\u250C":dirEquals(prevDir,Left)&&dirEquals(nextDir,Up)||dirEquals(prevDir,Down)&&dirEquals(nextDir,Right)?corner="\u2514":corner="+",cornersCanvas[dc.x][dc.y]=corner}return[pathCanvas,cornersCanvas]}function drawBundleArrowhead(graph,bundle){let canvas=copyCanvas(graph.canvas);if(bundle.sharedPath.length<2)return canvas;let lastIdx=bundle.sharedPath.length-1,secondLast=bundle.sharedPath[lastIdx-1],last=bundle.sharedPath[lastIdx],dir=determineDirection(secondLast,last),graphDir=graph.config.graphDirection,entryDir=graphDir==="TD"?Up:Left,dc=getNodeAttachmentPoint(graph,bundle.sharedNode,entryDir);graphDir==="TD"?dc.y-=1:dc.x-=1;let char;return graph.config.useAscii?dirEquals(dir,Up)?char="^":dirEquals(dir,Down)?char="v":dirEquals(dir,Left)?char="<":dirEquals(dir,Right)?char=">":char="v":dirEquals(dir,Up)?char="\u25B2":dirEquals(dir,Down)?char="\u25BC":dirEquals(dir,Left)?char="\u25C4":dirEquals(dir,Right)?char="\u25BA":char="\u25BC",canvas[dc.x][dc.y]=char,canvas}function drawBundledEdgeArrowhead(graph,edge){let canvas=copyCanvas(graph.canvas);if(!edge.pathToJunction||edge.pathToJunction.length<2)return canvas;let lastIdx=edge.pathToJunction.length-1,secondLast=edge.pathToJunction[lastIdx-1],last=edge.pathToJunction[lastIdx],dir=determineDirection(secondLast,last),graphDir=graph.config.graphDirection,entryDir=graphDir==="TD"?Up:Left,dc=getNodeAttachmentPoint(graph,edge.to,entryDir);graphDir==="TD"?dc.y-=1:dc.x-=1;let char;return graph.config.useAscii?dirEquals(dir,Up)?char="^":dirEquals(dir,Down)?char="v":dirEquals(dir,Left)?char="<":dirEquals(dir,Right)?char=">":char="v":dirEquals(dir,Up)?char="\u25B2":dirEquals(dir,Down)?char="\u25BC":dirEquals(dir,Left)?char="\u25C4":dirEquals(dir,Right)?char="\u25BA":char="\u25BC",canvas[dc.x][dc.y]=char,canvas}function drawJunctionCharacter(graph,bundle){let canvas=copyCanvas(graph.canvas);if(!bundle.junctionPoint)return canvas;let dc=gridToDrawingCoord(graph,bundle.junctionPoint),useAscii=graph.config.useAscii,hasUp=!1,hasDown=!1,hasLeft=!1,hasRight=!1;if(bundle.sharedPath.length>=2){let junctionIdx=bundle.type==="fan-in"?0:bundle.sharedPath.length-1,adjacentIdx=bundle.type==="fan-in"?1:bundle.sharedPath.length-2,sharedDir=determineDirection(bundle.sharedPath[junctionIdx],bundle.sharedPath[adjacentIdx]);dirEquals(sharedDir,Down)?hasDown=!0:dirEquals(sharedDir,Up)?hasUp=!0:dirEquals(sharedDir,Right)?hasRight=!0:dirEquals(sharedDir,Left)&&(hasLeft=!0)}for(let edge of bundle.edges)if(edge.pathToJunction&&edge.pathToJunction.length>=2){let junctionIdx=bundle.type==="fan-in"?edge.pathToJunction.length-1:0,adjacentIdx=bundle.type==="fan-in"?edge.pathToJunction.length-2:1,arrivalDir=determineDirection(edge.pathToJunction[adjacentIdx],edge.pathToJunction[junctionIdx]);dirEquals(arrivalDir,Down)?hasUp=!0:dirEquals(arrivalDir,Up)?hasDown=!0:dirEquals(arrivalDir,Right)?hasLeft=!0:dirEquals(arrivalDir,Left)&&(hasRight=!0)}let char;return useAscii?char="+":hasUp&&hasDown&&hasLeft&&hasRight?char="\u253C":hasDown&&hasLeft&&hasRight&&!hasUp?char="\u252C":hasUp&&hasLeft&&hasRight&&!hasDown?char="\u2534":hasUp&&hasDown&&hasRight&&!hasLeft?char="\u251C":hasUp&&hasDown&&hasLeft&&!hasRight?char="\u2524":hasLeft&&hasRight?char="\u2500":hasUp&&hasDown?char="\u2502":hasDown&&hasRight?char="\u250C":hasDown&&hasLeft?char="\u2510":hasUp&&hasRight?char="\u2514":hasUp&&hasLeft?char="\u2518":char="\u253C",canvas[dc.x][dc.y]=char,canvas}function drawSubgraphBox(sg,graph){let width=sg.maxX-sg.minX,height=sg.maxY-sg.minY;if(width<=0||height<=0)return mkCanvas(0,0);let from={x:0,y:0},to={x:width,y:height},canvas=mkCanvas(width,height);if(graph.config.useAscii){for(let x=from.x+1;x<to.x;x++)canvas[x][from.y]="-";for(let x=from.x+1;x<to.x;x++)canvas[x][to.y]="-";for(let y=from.y+1;y<to.y;y++)canvas[from.x][y]="|";for(let y=from.y+1;y<to.y;y++)canvas[to.x][y]="|";canvas[from.x][from.y]="+",canvas[to.x][from.y]="+",canvas[from.x][to.y]="+",canvas[to.x][to.y]="+"}else{for(let x=from.x+1;x<to.x;x++)canvas[x][from.y]="\u2500";for(let x=from.x+1;x<to.x;x++)canvas[x][to.y]="\u2500";for(let y=from.y+1;y<to.y;y++)canvas[from.x][y]="\u2502";for(let y=from.y+1;y<to.y;y++)canvas[to.x][y]="\u2502";canvas[from.x][from.y]="\u250C",canvas[to.x][from.y]="\u2510",canvas[from.x][to.y]="\u2514",canvas[to.x][to.y]="\u2518"}return canvas}function drawSubgraphLabel(sg,graph){let width=sg.maxX-sg.minX,height=sg.maxY-sg.minY;if(width<=0||height<=0)return[mkCanvas(0,0),{x:0,y:0}];let canvas=mkCanvas(width,height),lines=splitLines(sg.name);for(let i=0;i<lines.length;i++){let line=lines[i],labelY=1+i,labelX=Math.floor(width/2)-Math.floor(line.length/2);labelX<1&&(labelX=1);for(let j=0;j<line.length;j++)labelX+j<width&&labelY<height&&(canvas[labelX+j][labelY]=line[j])}return[canvas,{x:sg.minX,y:sg.minY}]}function sortSubgraphsByDepth(subgraphs){function getDepth(sg){return sg.parent===null?0:1+getDepth(sg.parent)}let sorted=[...subgraphs];return sorted.sort((a,b)=>getDepth(a)-getDepth(b)),sorted}function fillRolesFromCanvas(roleCanvas,canvas,offset,role){for(let x=0;x<canvas.length;x++)for(let y=0;y<(canvas[0]?.length??0);y++){let char=canvas[x]?.[y];if(char&&char!==" "){let rx=x+offset.x,ry=y+offset.y;rx>=0&&ry>=0&&setRole(roleCanvas,rx,ry,role)}}}function fillRolesFromCanvases(roleCanvas,canvases,offset,role){for(let canvas of canvases)fillRolesFromCanvas(roleCanvas,canvas,offset,role)}function fillRolesForNodeBox(roleCanvas,canvas,offset){let isBorderChar=c=>/^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|.':]$/.test(c);for(let x=0;x<canvas.length;x++)for(let y=0;y<(canvas[0]?.length??0);y++){let char=canvas[x]?.[y];if(char&&char!==" "){let rx=x+offset.x,ry=y+offset.y;rx>=0&&ry>=0&&setRole(roleCanvas,rx,ry,isBorderChar(char)?"border":"text")}}}function drawGraph(graph){let useAscii=graph.config.useAscii,zero={x:0,y:0},sortedSgs=sortSubgraphsByDepth(graph.subgraphs);for(let sg of sortedSgs){let sgCanvas=drawSubgraphBox(sg,graph),offset={x:sg.minX,y:sg.minY};graph.canvas=mergeCanvases(graph.canvas,offset,useAscii,sgCanvas),fillRolesFromCanvas(graph.roleCanvas,sgCanvas,offset,"border")}for(let node of graph.nodes)!node.drawn&&node.drawingCoord&&node.drawing&&(graph.canvas=mergeCanvases(graph.canvas,node.drawingCoord,useAscii,node.drawing),fillRolesForNodeBox(graph.roleCanvas,node.drawing,node.drawingCoord),node.drawn=!0);let lineCanvases=[],cornerCanvases=[],arrowHeadEndCanvases=[],arrowHeadStartCanvases=[],boxStartCanvases=[],labelCanvases=[],junctionCanvases=[],processedBundles=new Set;for(let edge of graph.edges)if(edge.bundle&&edge.pathToJunction){let bundle=edge.bundle,[pathC,boxStartC,,,cornersC,labelC]=drawBundledEdgeSegment(graph,edge,bundle);if(lineCanvases.push(pathC),cornerCanvases.push(cornersC),boxStartCanvases.push(boxStartC),labelCanvases.push(labelC),!processedBundles.has(bundle)){processedBundles.add(bundle);let[sharedPathC,sharedCornersC]=drawBundleSharedPath(graph,bundle);if(lineCanvases.push(sharedPathC),cornerCanvases.push(sharedCornersC),bundle.type==="fan-in"){let arrowHeadC=drawBundleArrowhead(graph,bundle);arrowHeadEndCanvases.push(arrowHeadC)}let junctionC=drawJunctionCharacter(graph,bundle);junctionCanvases.push(junctionC)}if(bundle.type==="fan-out"&&edge.hasArrowEnd){let arrowHeadC=drawBundledEdgeArrowhead(graph,edge);arrowHeadEndCanvases.push(arrowHeadC)}}else{let[pathC,boxStartC,arrowHeadEndC,arrowHeadStartC,cornersC,labelC]=drawArrow(graph,edge);lineCanvases.push(pathC),cornerCanvases.push(cornersC),arrowHeadEndCanvases.push(arrowHeadEndC),arrowHeadStartCanvases.push(arrowHeadStartC),boxStartCanvases.push(boxStartC),labelCanvases.push(labelC)}graph.canvas=mergeCanvases(graph.canvas,zero,useAscii,...lineCanvases),fillRolesFromCanvases(graph.roleCanvas,lineCanvases,zero,"line"),graph.canvas=mergeCanvases(graph.canvas,zero,useAscii,...cornerCanvases),fillRolesFromCanvases(graph.roleCanvas,cornerCanvases,zero,"corner"),graph.canvas=mergeCanvases(graph.canvas,zero,useAscii,...junctionCanvases),fillRolesFromCanvases(graph.roleCanvas,junctionCanvases,zero,"junction"),graph.canvas=mergeCanvases(graph.canvas,zero,useAscii,...arrowHeadEndCanvases),fillRolesFromCanvases(graph.roleCanvas,arrowHeadEndCanvases,zero,"arrow"),graph.canvas=mergeCanvases(graph.canvas,zero,useAscii,...boxStartCanvases),fillRolesFromCanvases(graph.roleCanvas,boxStartCanvases,zero,"junction"),graph.canvas=mergeCanvases(graph.canvas,zero,useAscii,...arrowHeadStartCanvases),fillRolesFromCanvases(graph.roleCanvas,arrowHeadStartCanvases,zero,"arrow"),graph.canvas=mergeCanvases(graph.canvas,zero,useAscii,...labelCanvases);for(let label of labelCanvases)clearLabelGaps(graph.canvas,label);fillRolesFromCanvases(graph.roleCanvas,labelCanvases,zero,"text");for(let sg of graph.subgraphs){if(sg.nodes.length===0)continue;let[labelCanvas,offset]=drawSubgraphLabel(sg,graph);graph.canvas=mergeCanvases(graph.canvas,offset,useAscii,labelCanvas),fillRolesFromCanvas(graph.roleCanvas,labelCanvas,offset,"text")}return graph.canvas}function gridToDrawingCoord(graph,c,dir){let target=dir?{x:c.x+dir.x,y:c.y+dir.y}:c,x=0;for(let col=0;col<target.x;col++)x+=graph.columnWidth.get(col)??0;let y=0;for(let row=0;row<target.y;row++)y+=graph.rowHeight.get(row)??0;let colW=graph.columnWidth.get(target.x)??0,rowH=graph.rowHeight.get(target.y)??0;return{x:x+Math.floor(colW/2)+graph.offsetX,y:y+Math.floor(rowH/2)+graph.offsetY}}function lineToDrawing(graph,line){return line.map(c=>gridToDrawingCoord(graph,c))}function reserveSpotInGrid(graph,node,requested,effectiveDir){let dir=effectiveDir??getEffectiveDirection2(graph,node);if(graph.grid.has(gridKey(requested)))return dir==="LR"?reserveSpotInGrid(graph,node,{x:requested.x,y:requested.y+4},dir):reserveSpotInGrid(graph,node,{x:requested.x+4,y:requested.y},dir);for(let dx=0;dx<3;dx++)for(let dy=0;dy<3;dy++){let reserved={x:requested.x+dx,y:requested.y+dy};graph.grid.set(gridKey(reserved),node)}return node.gridCoord=requested,requested}function setColumnWidth(graph,node){let gc=node.gridCoord,padding=graph.config.boxBorderPadding,shapeDims=getShapeDimensions(node.shape,node.displayLabel,{useAscii:graph.config.useAscii,padding}),colWidths=shapeDims.gridColumns,rowHeights=shapeDims.gridRows;for(let idx=0;idx<colWidths.length;idx++){let xCoord=gc.x+idx,current=graph.columnWidth.get(xCoord)??0;graph.columnWidth.set(xCoord,Math.max(current,colWidths[idx]))}for(let idx=0;idx<rowHeights.length;idx++){let yCoord=gc.y+idx,current=graph.rowHeight.get(yCoord)??0;graph.rowHeight.set(yCoord,Math.max(current,rowHeights[idx]))}if(gc.x>0){let current=graph.columnWidth.get(gc.x-1)??0;graph.columnWidth.set(gc.x-1,Math.max(current,graph.config.paddingX))}if(gc.y>0){let basePadding=graph.config.paddingY;hasIncomingEdgeFromOutsideSubgraph(graph,node)&&(basePadding+=4);let current=graph.rowHeight.get(gc.y-1)??0;graph.rowHeight.set(gc.y-1,Math.max(current,basePadding))}}function increaseGridSizeForPath(graph,path){for(let c of path)graph.columnWidth.has(c.x)||graph.columnWidth.set(c.x,Math.floor(graph.config.paddingX/2)),graph.rowHeight.has(c.y)||graph.rowHeight.set(c.y,Math.floor(graph.config.paddingY/2))}function isNodeInAnySubgraph(graph,node){return graph.subgraphs.some(sg=>sg.nodes.includes(node))}function getNodeSubgraph(graph,node){let innermost=null;for(let sg of graph.subgraphs)sg.nodes.includes(node)&&(!innermost||isAncestorOrSelf2(innermost,sg))&&(innermost=sg);return innermost}function isAncestorOrSelf2(candidate,target){let current=target;for(;current!==null;){if(current===candidate)return!0;current=current.parent}return!1}function getEffectiveDirection2(graph,node){let sg=getNodeSubgraph(graph,node);return sg?.direction?sg.direction:graph.config.graphDirection}function hasIncomingEdgeFromOutsideSubgraph(graph,node){let nodeSg=getNodeSubgraph(graph,node);if(!nodeSg)return!1;let hasExternalEdge=!1;for(let edge of graph.edges)if(edge.to===node&&getNodeSubgraph(graph,edge.from)!==nodeSg){hasExternalEdge=!0;break}if(!hasExternalEdge)return!1;for(let otherNode of nodeSg.nodes){if(otherNode===node||!otherNode.gridCoord)continue;let otherHasExternal=!1;for(let edge of graph.edges)if(edge.to===otherNode&&getNodeSubgraph(graph,edge.from)!==nodeSg){otherHasExternal=!0;break}if(otherHasExternal&&otherNode.gridCoord.y<node.gridCoord.y)return!1}return!0}function calculateSubgraphBoundingBox(graph,sg){if(sg.nodes.length===0)return;let minX=1e6,minY=1e6,maxX=-1e6,maxY=-1e6;for(let child of sg.children)calculateSubgraphBoundingBox(graph,child),child.nodes.length>0&&(minX=Math.min(minX,child.minX),minY=Math.min(minY,child.minY),maxX=Math.max(maxX,child.maxX),maxY=Math.max(maxY,child.maxY));for(let node of sg.nodes){if(!node.drawingCoord||!node.drawing)continue;let nodeMinX=node.drawingCoord.x,nodeMinY=node.drawingCoord.y,nodeMaxX=nodeMinX+node.drawing.length-1,nodeMaxY=nodeMinY+node.drawing[0].length-1;minX=Math.min(minX,nodeMinX),minY=Math.min(minY,nodeMinY),maxX=Math.max(maxX,nodeMaxX),maxY=Math.max(maxY,nodeMaxY)}let subgraphPadding=2,subgraphLabelSpace=2;sg.minX=minX-subgraphPadding,sg.minY=minY-subgraphPadding-subgraphLabelSpace,sg.maxX=maxX+subgraphPadding,sg.maxY=maxY+subgraphPadding}function ensureSubgraphSpacing(graph){let rootSubgraphs=graph.subgraphs.filter(sg=>sg.parent===null&&sg.nodes.length>0);for(let i=0;i<rootSubgraphs.length;i++)for(let j=i+1;j<rootSubgraphs.length;j++){let sg1=rootSubgraphs[i],sg2=rootSubgraphs[j];sg1.minX<sg2.maxX&&sg1.maxX>sg2.minX&&(sg1.maxY>=sg2.minY-1&&sg1.minY<sg2.minY?sg2.minY=sg1.maxY+1+1:sg2.maxY>=sg1.minY-1&&sg2.minY<sg1.minY&&(sg1.minY=sg2.maxY+1+1)),sg1.minY<sg2.maxY&&sg1.maxY>sg2.minY&&(sg1.maxX>=sg2.minX-1&&sg1.minX<sg2.minX?sg2.minX=sg1.maxX+1+1:sg2.maxX>=sg1.minX-1&&sg2.minX<sg1.minX&&(sg1.minX=sg2.maxX+1+1))}}function calculateSubgraphBoundingBoxes(graph){for(let sg of graph.subgraphs)calculateSubgraphBoundingBox(graph,sg);ensureSubgraphSpacing(graph)}function offsetDrawingForSubgraphs(graph){if(graph.subgraphs.length===0)return;let minX=0,minY=0;for(let sg of graph.subgraphs)minX=Math.min(minX,sg.minX),minY=Math.min(minY,sg.minY);let offsetX=-minX,offsetY=-minY;if(!(offsetX===0&&offsetY===0)){graph.offsetX=offsetX,graph.offsetY=offsetY;for(let sg of graph.subgraphs)sg.minX+=offsetX,sg.minY+=offsetY,sg.maxX+=offsetX,sg.maxY+=offsetY;for(let node of graph.nodes)node.drawingCoord&&(node.drawingCoord.x+=offsetX,node.drawingCoord.y+=offsetY)}}function createMapping(graph){let dir=graph.config.graphDirection,highestPositionPerLevel=new Array(100).fill(0),nodesFound=new Set,initialRoots=[];for(let node of graph.nodes){nodesFound.has(node.name)||initialRoots.push(node),nodesFound.add(node.name);for(let child of getChildren(graph,node))nodesFound.add(child.name)}let rootNodes=initialRoots.filter(node=>{let nodeSg=getNodeSubgraph(graph,node);if(!nodeSg)return!0;for(let edge of graph.edges)if(edge.to===node&&getNodeSubgraph(graph,edge.from)!==nodeSg)return!1;return!0}),hasExternalRoots=!1,hasSubgraphRootsWithEdges=!1;for(let node of rootNodes)isNodeInAnySubgraph(graph,node)?getChildren(graph,node).length>0&&(hasSubgraphRootsWithEdges=!0):hasExternalRoots=!0;let shouldSeparate=dir==="LR"&&hasExternalRoots&&hasSubgraphRootsWithEdges,externalRootNodes,subgraphRootNodes=[];shouldSeparate?(externalRootNodes=rootNodes.filter(n=>!isNodeInAnySubgraph(graph,n)),subgraphRootNodes=rootNodes.filter(n=>isNodeInAnySubgraph(graph,n))):externalRootNodes=rootNodes;for(let node of externalRootNodes){let requested=dir==="LR"?{x:0,y:highestPositionPerLevel[0]}:{x:highestPositionPerLevel[0],y:0};reserveSpotInGrid(graph,graph.nodes[node.index],requested),highestPositionPerLevel[0]=highestPositionPerLevel[0]+4}if(shouldSeparate&&subgraphRootNodes.length>0)for(let node of subgraphRootNodes){let requested=dir==="LR"?{x:4,y:highestPositionPerLevel[4]}:{x:highestPositionPerLevel[4],y:4};reserveSpotInGrid(graph,graph.nodes[node.index],requested),highestPositionPerLevel[4]=highestPositionPerLevel[4]+4}let placedCount=externalRootNodes.length+subgraphRootNodes.length;for(;placedCount<graph.nodes.length;){let prevCount=placedCount;for(let node of graph.nodes){if(node.gridCoord===null)continue;let gc=node.gridCoord;for(let child of getChildren(graph,node)){if(child.gridCoord!==null)continue;let parentSg=getNodeSubgraph(graph,node),childSg=getNodeSubgraph(graph,child),edgeDir=parentSg&&parentSg===childSg&&parentSg.direction?parentSg.direction:graph.config.graphDirection,childLevel=edgeDir==="LR"?gc.x+4:gc.y+4,highestPosition;edgeDir!==graph.config.graphDirection?highestPosition=edgeDir==="LR"?gc.y:gc.x:highestPosition=highestPositionPerLevel[childLevel];let requested=edgeDir==="LR"?{x:childLevel,y:highestPosition}:{x:highestPosition,y:childLevel};reserveSpotInGrid(graph,graph.nodes[child.index],requested,edgeDir),edgeDir===graph.config.graphDirection&&(highestPositionPerLevel[childLevel]=highestPosition+4),placedCount++}}if(placedCount===prevCount)break}for(let node of graph.nodes)setColumnWidth(graph,node);graph.bundles=analyzeEdgeBundles(graph),processBundles(graph);for(let edge of graph.edges){if(edge.bundle&&edge.path.length>0){increaseGridSizeForPath(graph,edge.path),determineLabelLine(graph,edge);continue}determinePath(graph,edge),increaseGridSizeForPath(graph,edge.path),determineLabelLine(graph,edge)}for(let node of graph.nodes)node.drawingCoord=gridToDrawingCoord(graph,node.gridCoord),node.drawing=drawBox(node,graph);setCanvasSizeToGrid(graph.canvas,graph.columnWidth,graph.rowHeight),setRoleCanvasSizeToGrid(graph.roleCanvas,graph.columnWidth,graph.rowHeight),calculateSubgraphBoundingBoxes(graph),offsetDrawingForSubgraphs(graph)}function getEdgesFromNode(graph,node){return graph.edges.filter(e=>e.from.name===node.name)}function getChildren(graph,node){return getEdgesFromNode(graph,node).map(e=>e.to)}function parseSequenceDiagram(lines){let diagram={actors:[],messages:[],blocks:[],notes:[]},actorIds=new Set,blockStack=[];for(let i=1;i<lines.length;i++){let line=lines[i],actorMatch=line.match(/^(participant|actor)\s+(\S+?)(?:\s+as\s+(.+))?$/);if(actorMatch){let type=actorMatch[1],id=actorMatch[2],rawLabel=actorMatch[3]?.trim()??id,label=normalizeBrTags(rawLabel);actorIds.has(id)||(actorIds.add(id),diagram.actors.push({id,label,type}));continue}let noteMatch=line.match(/^Note\s+(left of|right of|over)\s+([^:]+):\s*(.+)$/i);if(noteMatch){let posStr=noteMatch[1].toLowerCase(),actorsStr=noteMatch[2].trim(),text=normalizeBrTags(noteMatch[3].trim()),noteActorIds=actorsStr.split(",").map(s=>s.trim());for(let aid of noteActorIds)ensureActor(diagram,actorIds,aid);let position="over";posStr==="left of"?position="left":posStr==="right of"&&(position="right"),diagram.notes.push({actorIds:noteActorIds,text,position,afterIndex:diagram.messages.length-1});continue}let blockMatch=line.match(/^(loop|alt|opt|par|critical|break|rect)\s*(.*)$/);if(blockMatch){let blockType=blockMatch[1],rawBlockLabel=blockMatch[2]?.trim()??"",label=normalizeBrTags(rawBlockLabel);blockStack.push({type:blockType,label,startIndex:diagram.messages.length,dividers:[]});continue}let dividerMatch=line.match(/^(else|and)\s*(.*)$/);if(dividerMatch&&blockStack.length>0){let rawDividerLabel=dividerMatch[2]?.trim()??"",label=normalizeBrTags(rawDividerLabel);blockStack[blockStack.length-1].dividers.push({index:diagram.messages.length,label});continue}if(line==="end"&&blockStack.length>0){let completed=blockStack.pop();diagram.blocks.push({type:completed.type,label:completed.label,startIndex:completed.startIndex,endIndex:Math.max(diagram.messages.length-1,completed.startIndex),dividers:completed.dividers});continue}let msgMatch=line.match(/^(\S+?)\s*(--?>?>|--?[)x]|--?>>|--?>)\s*([+-]?)(\S+?)\s*:\s*(.+)$/);if(msgMatch){let from=msgMatch[1],arrow=msgMatch[2],activationMark=msgMatch[3],to=msgMatch[4],label=normalizeBrTags(msgMatch[5].trim());ensureActor(diagram,actorIds,from),ensureActor(diagram,actorIds,to);let lineStyle=arrow.startsWith("--")?"dashed":"solid",arrowHead=arrow.includes(">>")||arrow.includes("x")?"filled":"open",msg={from,to,label,lineStyle,arrowHead};activationMark==="+"&&(msg.activate=!0),activationMark==="-"&&(msg.deactivate=!0),diagram.messages.push(msg);continue}let simpleMsgMatch=line.match(/^(\S+?)\s*(->>|-->>|-\)|--\)|-x|--x|->|-->)\s*([+-]?)(\S+?)\s*:\s*(.+)$/);if(simpleMsgMatch){let from=simpleMsgMatch[1],arrow=simpleMsgMatch[2],activationMark=simpleMsgMatch[3],to=simpleMsgMatch[4],label=normalizeBrTags(simpleMsgMatch[5].trim());ensureActor(diagram,actorIds,from),ensureActor(diagram,actorIds,to);let lineStyle=arrow.startsWith("--")?"dashed":"solid",arrowHead=arrow.includes(">>")||arrow.includes("x")?"filled":"open",msg={from,to,label,lineStyle,arrowHead};activationMark==="+"&&(msg.activate=!0),activationMark==="-"&&(msg.deactivate=!0),diagram.messages.push(msg);continue}}return diagram}function ensureActor(diagram,actorIds,id){actorIds.has(id)||(actorIds.add(id),diagram.actors.push({id,label:id,type:"participant"}))}function renderSequenceAscii(text,config,colorMode,theme){let lines=text.split(`
7`).map(l=>l.trim()).filter(l=>l.length>0&&!l.startsWith("%%")),diagram=parseSequenceDiagram(lines);if(diagram.actors.length===0)return"";let useAscii=config.useAscii,H=useAscii?"-":"\u2500",V=useAscii?"|":"\u2502",TL=useAscii?"+":"\u250C",TR=useAscii?"+":"\u2510",BL=useAscii?"+":"\u2514",BR=useAscii?"+":"\u2518",JT=useAscii?"+":"\u252C",JB=useAscii?"+":"\u2534",JL=useAscii?"+":"\u251C",JR=useAscii?"+":"\u2524",actorIdx=new Map;diagram.actors.forEach((a,i)=>actorIdx.set(a.id,i));let boxPad=1,halfBox=diagram.actors.map(a=>maxLineWidth(a.label)+2*boxPad+2).map(w=>Math.ceil(w/2)),actorBoxHeights=diagram.actors.map(a=>lineCount(a.label)+2),actorBoxH=Math.max(...actorBoxHeights,3),adjMaxWidth=new Array(Math.max(diagram.actors.length-1,0)).fill(0);for(let msg of diagram.messages){let fi=actorIdx.get(msg.from),ti=actorIdx.get(msg.to);if(fi===ti)continue;let lo=Math.min(fi,ti),hi=Math.max(fi,ti),needed=maxLineWidth(msg.label)+4,numGaps=hi-lo,perGap=Math.ceil(needed/numGaps);for(let g=lo;g<hi;g++)adjMaxWidth[g]=Math.max(adjMaxWidth[g],perGap)}let llX=[halfBox[0]];for(let i=1;i<diagram.actors.length;i++){let gap=Math.max(halfBox[i-1]+halfBox[i]+2,adjMaxWidth[i-1]+2,10);llX[i]=llX[i-1]+gap}let msgArrowY=[],msgLabelY=[],blockStartY=new Map,blockEndY=new Map,divYMap=new Map,notePositions=[],curY=actorBoxH;for(let m=0;m<diagram.messages.length;m++){for(let b=0;b<diagram.blocks.length;b++)diagram.blocks[b].startIndex===m&&(curY+=2,blockStartY.set(b,curY-1));for(let b=0;b<diagram.blocks.length;b++)for(let d=0;d<diagram.blocks[b].dividers.length;d++)diagram.blocks[b].dividers[d].index===m&&(curY+=1,divYMap.set(`${b}:${d}`,curY),curY+=1);curY+=1;let msg=diagram.messages[m],isSelf=msg.from===msg.to,msgLineCount=lineCount(msg.label);isSelf?(msgLabelY[m]=curY+1,msgArrowY[m]=curY,curY+=2+msgLineCount):(msgLabelY[m]=curY,msgArrowY[m]=curY+msgLineCount,curY+=msgLineCount+1);for(let n=0;n<diagram.notes.length;n++)if(diagram.notes[n].afterIndex===m){curY+=1;let note=diagram.notes[n],nLines=splitLines(note.text),nWidth=Math.max(...nLines.map(l=>l.length))+4,nHeight=nLines.length+2,aIdx=actorIdx.get(note.actorIds[0])??0,nx;if(note.position==="left")nx=llX[aIdx]-nWidth-1;else if(note.position==="right")nx=llX[aIdx]+2;else if(note.actorIds.length>=2){let aIdx2=actorIdx.get(note.actorIds[1])??aIdx;nx=Math.floor((llX[aIdx]+llX[aIdx2])/2)-Math.floor(nWidth/2)}else nx=llX[aIdx]-Math.floor(nWidth/2);nx=Math.max(0,nx),notePositions.push({x:nx,y:curY,width:nWidth,height:nHeight,lines:nLines}),curY+=nHeight}for(let b=0;b<diagram.blocks.length;b++)diagram.blocks[b].endIndex===m&&(curY+=1,blockEndY.set(b,curY),curY+=1)}curY+=1;let footerY=curY,totalH=footerY+actorBoxH,lastLL=llX[llX.length-1]??0,lastHalf=halfBox[halfBox.length-1]??0,totalW=lastLL+lastHalf+2;for(let m=0;m<diagram.messages.length;m++){let msg=diagram.messages[m];if(msg.from===msg.to){let fi=actorIdx.get(msg.from),selfRight=llX[fi]+6+2+msg.label.length;totalW=Math.max(totalW,selfRight+1)}}for(let np of notePositions)totalW=Math.max(totalW,np.x+np.width+1);let canvas=mkCanvas(totalW,totalH-1),rc=mkRoleCanvas(totalW,totalH-1);function setC(x,y,ch,role){x>=0&&x<canvas.length&&y>=0&&y<(canvas[0]?.length??0)&&(canvas[x][y]=ch,setRole(rc,x,y,role))}function drawActorBox(cx,topY,label){let lines2=splitLines(label),maxW=maxLineWidth(label),w=maxW+2*boxPad+2,h=lines2.length+2,left=cx-Math.floor(w/2);setC(left,topY,TL,"border");for(let x=1;x<w-1;x++)setC(left+x,topY,H,"border");setC(left+w-1,topY,TR,"border");for(let i=0;i<lines2.length;i++){let row=topY+1+i;setC(left,row,V,"border"),setC(left+w-1,row,V,"border");let line=lines2[i],ls=left+1+boxPad+Math.floor((maxW-line.length)/2);for(let j=0;j<line.length;j++)setC(ls+j,row,line[j],"text")}let bottomY=topY+h-1;setC(left,bottomY,BL,"border");for(let x=1;x<w-1;x++)setC(left+x,bottomY,H,"border");setC(left+w-1,bottomY,BR,"border")}for(let i=0;i<diagram.actors.length;i++){let x=llX[i];for(let y=actorBoxH;y<=footerY;y++)setC(x,y,V,"line")}for(let i=0;i<diagram.actors.length;i++){let actor=diagram.actors[i];drawActorBox(llX[i],0,actor.label),drawActorBox(llX[i],footerY,actor.label),useAscii||(setC(llX[i],actorBoxH-1,JT,"junction"),setC(llX[i],footerY,JB,"junction"))}for(let m=0;m<diagram.messages.length;m++){let msg=diagram.messages[m],fi=actorIdx.get(msg.from),ti=actorIdx.get(msg.to),fromX=llX[fi],toX=llX[ti],isSelf=fi===ti,isDashed=msg.lineStyle==="dashed",isFilled=msg.arrowHead==="filled",lineChar=isDashed?useAscii?".":"\u254C":H;if(isSelf){let y0=msgArrowY[m],loopW=Math.max(4,4);setC(fromX,y0,JL,"junction");for(let x=fromX+1;x<fromX+loopW;x++)setC(x,y0,lineChar,"line");setC(fromX+loopW,y0,useAscii?"+":"\u2510","corner"),setC(fromX+loopW,y0+1,V,"line");let labelX=fromX+loopW+2;for(let i=0;i<msg.label.length;i++)labelX+i<totalW&&setC(labelX+i,y0+1,msg.label[i],"text");let arrowChar=isFilled?useAscii?"<":"\u25C0":useAscii?"<":"\u25C1";setC(fromX,y0+2,arrowChar,"arrow");for(let x=fromX+1;x<fromX+loopW;x++)setC(x,y0+2,lineChar,"line");setC(fromX+loopW,y0+2,useAscii?"+":"\u2518","corner")}else{let labelY=msgLabelY[m],arrowY=msgArrowY[m],leftToRight=fromX<toX,midX=Math.floor((fromX+toX)/2),msgLines=splitLines(msg.label);for(let lineIdx=0;lineIdx<msgLines.length;lineIdx++){let line=msgLines[lineIdx],labelStart=midX-Math.floor(line.length/2),y=labelY+lineIdx;for(let i=0;i<line.length;i++){let lx=labelStart+i;lx>=0&&lx<totalW&&setC(lx,y,line[i],"text")}}if(leftToRight){for(let x=fromX+1;x<toX;x++)setC(x,arrowY,lineChar,"line");setC(toX,arrowY,isFilled?useAscii?">":"\u25B6":useAscii?">":"\u25B7","arrow")}else{for(let x=toX+1;x<fromX;x++)setC(x,arrowY,lineChar,"line");setC(toX,arrowY,isFilled?useAscii?"<":"\u25C0":useAscii?"<":"\u25C1","arrow")}}}for(let b=0;b<diagram.blocks.length;b++){let block=diagram.blocks[b],topY=blockStartY.get(b),botY=blockEndY.get(b);if(topY===void 0||botY===void 0)continue;let minLX=totalW,maxLX=0;for(let m=block.startIndex;m<=block.endIndex&&!(m>=diagram.messages.length);m++){let msg=diagram.messages[m],f=actorIdx.get(msg.from)??0,t=actorIdx.get(msg.to)??0;minLX=Math.min(minLX,llX[Math.min(f,t)]),maxLX=Math.max(maxLX,llX[Math.max(f,t)])}let bLeft=Math.max(0,minLX-4),bRight=Math.min(totalW-1,maxLX+4);setC(bLeft,topY,TL,"border");for(let x=bLeft+1;x<bRight;x++)setC(x,topY,H,"border");setC(bRight,topY,TR,"border");let hdrLabel=block.label?`${block.type} [${block.label}]`:block.type,hdrLines=splitLines(hdrLabel);for(let lineIdx=0;lineIdx<hdrLines.length&&topY+lineIdx<botY;lineIdx++){let line=hdrLines[lineIdx];for(let i=0;i<line.length&&bLeft+1+i<bRight;i++)setC(bLeft+1+i,topY+lineIdx,line[i],"text")}setC(bLeft,botY,BL,"border");for(let x=bLeft+1;x<bRight;x++)setC(x,botY,H,"border");setC(bRight,botY,BR,"border");for(let y=topY+1;y<botY;y++)setC(bLeft,y,V,"border"),setC(bRight,y,V,"border");for(let d=0;d<block.dividers.length;d++){let dY=divYMap.get(`${b}:${d}`);if(dY===void 0)continue;let dashChar=isDashedH();setC(bLeft,dY,JL,"junction");for(let x=bLeft+1;x<bRight;x++)setC(x,dY,dashChar,"line");setC(bRight,dY,JR,"junction");let dLabel=block.dividers[d].label;if(dLabel){let dStr=`[${dLabel}]`;for(let i=0;i<dStr.length&&bLeft+1+i<bRight;i++)setC(bLeft+1+i,dY,dStr[i],"text")}}}for(let np of notePositions){increaseSize(canvas,np.x+np.width,np.y+np.height),increaseRoleCanvasSize(rc,np.x+np.width,np.y+np.height),setC(np.x,np.y,TL,"border");for(let x=1;x<np.width-1;x++)setC(np.x+x,np.y,H,"border");setC(np.x+np.width-1,np.y,TR,"border");for(let l=0;l<np.lines.length;l++){let ly=np.y+1+l;setC(np.x,ly,V,"border"),setC(np.x+np.width-1,ly,V,"border");for(let i=0;i<np.lines[l].length;i++)setC(np.x+2+i,ly,np.lines[l][i],"text")}let by=np.y+np.height-1;setC(np.x,by,BL,"border");for(let x=1;x<np.width-1;x++)setC(np.x+x,by,H,"border");setC(np.x+np.width-1,by,BR,"border")}return canvasToString(canvas,{roleCanvas:rc,colorMode,theme});function isDashedH(){return useAscii?"-":"\u254C"}}function parseClassDiagram(lines){let diagram={classes:[],relationships:[],namespaces:[]},classMap=new Map,currentNamespace=null,currentClass=null,braceDepth=0;for(let i=1;i<lines.length;i++){let line=lines[i];if(currentClass&&braceDepth>0){if(line==="}"){braceDepth--,braceDepth===0&&(currentClass=null);continue}let annotMatch=line.match(/^<<(\w+)>>$/);if(annotMatch){currentClass.annotation=annotMatch[1];continue}let member=parseMember(line);member&&(member.isMethod?currentClass.methods.push(member.member):currentClass.attributes.push(member.member));continue}let nsMatch=line.match(/^namespace\s+(\S+)\s*\{$/);if(nsMatch){currentNamespace={name:nsMatch[1],classIds:[]};continue}if(line==="}"&¤tNamespace){diagram.namespaces.push(currentNamespace),currentNamespace=null;continue}let classBlockMatch=line.match(/^class\s+(\S+?)(?:\s*~(\w+)~)?\s*\{$/);if(classBlockMatch){let id=classBlockMatch[1],generic=classBlockMatch[2],cls=ensureClass(classMap,id);generic&&(cls.label=`${id}<${generic}>`),currentClass=cls,braceDepth=1,currentNamespace&¤tNamespace.classIds.push(id);continue}let classOnlyMatch=line.match(/^class\s+(\S+?)(?:\s*~(\w+)~)?\s*$/);if(classOnlyMatch){let id=classOnlyMatch[1],generic=classOnlyMatch[2],cls=ensureClass(classMap,id);generic&&(cls.label=`${id}<${generic}>`),currentNamespace&¤tNamespace.classIds.push(id);continue}let inlineAnnotMatch=line.match(/^class\s+(\S+?)\s*\{\s*<<(\w+)>>\s*\}$/);if(inlineAnnotMatch){let cls=ensureClass(classMap,inlineAnnotMatch[1]);cls.annotation=inlineAnnotMatch[2];continue}let inlineAttrMatch=line.match(/^(\S+?)\s*:\s*(.+)$/);if(inlineAttrMatch){let rest=inlineAttrMatch[2];if(!rest.match(/<\|--|--|\*--|o--|-->|\.\.>|\.\.\|>/)){let cls=ensureClass(classMap,inlineAttrMatch[1]),member=parseMember(rest);member&&(member.isMethod?cls.methods.push(member.member):cls.attributes.push(member.member));continue}}let rel=parseRelationship(line);if(rel){ensureClass(classMap,rel.from),ensureClass(classMap,rel.to),diagram.relationships.push(rel);continue}}return diagram.classes=[...classMap.values()],diagram}function ensureClass(classMap,id){let cls=classMap.get(id);return cls||(cls={id,label:id,attributes:[],methods:[]},classMap.set(id,cls)),cls}function parseMember(line){let trimmed=line.trim().replace(/;$/,"");if(!trimmed)return null;let visibility="",rest=trimmed;/^[+\-#~]/.test(rest)&&(visibility=rest[0],rest=rest.slice(1).trim());let methodMatch=rest.match(/^(.+?)\(([^)]*)\)(?:\s*(.+))?$/);if(methodMatch){let name2=methodMatch[1].trim(),params=methodMatch[2]?.trim()||void 0,type2=methodMatch[3]?.trim(),isStatic2=name2.endsWith("$")||rest.includes("$"),isAbstract2=name2.endsWith("*")||rest.includes("*");return{member:{visibility,name:name2.replace(/[$*]$/,""),type:type2||void 0,isStatic:isStatic2,isAbstract:isAbstract2,isMethod:!0,params},isMethod:!0}}let parts=rest.split(/\s+/),name,type;parts.length>=2?(type=parts[0],name=parts.slice(1).join(" ")):name=parts[0]??rest;let isStatic=name.endsWith("$"),isAbstract=name.endsWith("*");return{member:{visibility,name:name.replace(/[$*]$/,""),type:type||void 0,isStatic,isAbstract,isMethod:!1},isMethod:!1}}function parseRelationship(line){let match=line.match(/^(\S+?)\s+(?:"([^"]*?)"\s+)?(<\|--|<\|\.\.|\*--|o--|-->|--\*|--o|--\|>|\.\.>|\.\.\|>|<--|<\.\.?|--)\s+(?:"([^"]*?)"\s+)?(\S+?)(?:\s*:\s*(.+))?$/);if(!match)return null;let from=match[1],rawFromCardinality=match[2],fromCardinality=rawFromCardinality?normalizeBrTags(rawFromCardinality):void 0,arrow=match[3].trim(),rawToCardinality=match[4],toCardinality=rawToCardinality?normalizeBrTags(rawToCardinality):void 0,to=match[5],rawLabel=match[6]?.trim(),label=rawLabel?normalizeBrTags(rawLabel):void 0,parsed=parseArrow(arrow);return parsed?{from,to,type:parsed.type,markerAt:parsed.markerAt,label,fromCardinality,toCardinality}:null}function parseArrow(arrow){switch(arrow.trim()){case"<|--":return{type:"inheritance",markerAt:"from"};case"--|>":return{type:"inheritance",markerAt:"to"};case"<|..":return{type:"realization",markerAt:"from"};case"..|>":return{type:"realization",markerAt:"to"};case"*--":return{type:"composition",markerAt:"from"};case"--*":return{type:"composition",markerAt:"to"};case"o--":return{type:"aggregation",markerAt:"from"};case"--o":return{type:"aggregation",markerAt:"to"};case"-->":return{type:"association",markerAt:"to"};case"<--":return{type:"association",markerAt:"from"};case"..>":return{type:"dependency",markerAt:"to"};case"<..":return{type:"dependency",markerAt:"from"};case"--":return{type:"association",markerAt:"to"};default:return null}}function classifyBoxChar(ch){return/^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|]$/.test(ch)?"border":"text"}function formatMember(m){let vis=m.visibility||"",type=m.type?`: ${m.type}`:"";return`${vis}${m.name}${type}`}function buildClassSections(cls){let header=[];cls.annotation&&header.push(`<<${cls.annotation}>>`);let nameLines=splitLines(cls.label);header.push(...nameLines);let attrs=cls.attributes.map(formatMember),methods=cls.methods.map(formatMember);return attrs.length===0&&methods.length===0?[header]:methods.length===0?[header,attrs]:[header,attrs,methods]}function getRelMarker(type,markerAt){return{type,markerAt,dashed:type==="dependency"||type==="realization"}}function getMarkerShape(type,useAscii,direction){switch(type){case"inheritance":case"realization":return direction==="down"?useAscii?"^":"\u25B3":direction==="up"?useAscii?"v":"\u25BD":direction==="left"?useAscii?">":"\u25C1":useAscii?"<":"\u25B7";case"composition":return useAscii?"*":"\u25C6";case"aggregation":return useAscii?"o":"\u25C7";case"association":case"dependency":return direction==="down"?useAscii?"v":"\u25BC":direction==="up"?useAscii?"^":"\u25B2":direction==="left"?useAscii?"<":"\u25C0":useAscii?">":"\u25B6"}}function renderClassAscii(text,config,colorMode,theme){let lines=text.split(`
8`).map(l=>l.trim()).filter(l=>l.length>0&&!l.startsWith("%%")),diagram=parseClassDiagram(lines);if(diagram.classes.length===0)return"";let useAscii=config.useAscii,hGap=4,vGap=3,classSections=new Map,classBoxW=new Map,classBoxH=new Map;for(let cls of diagram.classes){let sections=buildClassSections(cls);classSections.set(cls.id,sections);let maxTextW=0;for(let section of sections)for(let line of section)maxTextW=Math.max(maxTextW,line.length);let boxW=maxTextW+4,totalLines=0;for(let section of sections)totalLines+=Math.max(section.length,1);let boxH=totalLines+(sections.length-1)+2;classBoxW.set(cls.id,boxW),classBoxH.set(cls.id,boxH)}let classById=new Map;for(let cls of diagram.classes)classById.set(cls.id,cls);let parents=new Map,children=new Map;for(let rel of diagram.relationships){let isHierarchical=rel.type==="inheritance"||rel.type==="realization",parentId=isHierarchical&&rel.markerAt==="to"?rel.to:rel.from,childId=isHierarchical&&rel.markerAt==="to"?rel.from:rel.to;parents.has(childId)||parents.set(childId,new Set),parents.get(childId).add(parentId),children.has(parentId)||children.set(parentId,new Set),children.get(parentId).add(childId)}let level=new Map,queue=diagram.classes.filter(c=>!parents.has(c.id)||parents.get(c.id).size===0).map(c=>c.id);for(let id of queue)level.set(id,0);let levelCap=diagram.classes.length-1,qi=0;for(;qi<queue.length;){let id=queue[qi++],childSet=children.get(id);if(childSet)for(let childId of childSet){let newLevel=(level.get(id)??0)+1;newLevel>levelCap||(!level.has(childId)||level.get(childId)<newLevel)&&(level.set(childId,newLevel),queue.push(childId))}}for(let cls of diagram.classes)level.has(cls.id)||level.set(cls.id,0);let maxLevel=Math.max(...level.values(),0),levelGroups=Array.from({length:maxLevel+1},()=>[]);for(let cls of diagram.classes)levelGroups[level.get(cls.id)].push(cls.id);let placed=new Map,currentY=0;for(let lv=0;lv<=maxLevel;lv++){let group=levelGroups[lv];if(group.length===0)continue;let currentX=0,maxH=0;for(let id of group){let cls=classById.get(id),w=classBoxW.get(id),h=classBoxH.get(id);placed.set(id,{cls,sections:classSections.get(id),x:currentX,y:currentY,width:w,height:h}),currentX+=w+hGap,maxH=Math.max(maxH,h)}currentY+=maxH+vGap}let totalW=0,totalH=0;for(let p of placed.values())totalW=Math.max(totalW,p.x+p.width),totalH=Math.max(totalH,p.y+p.height);totalW+=4,totalH+=2;let canvas=mkCanvas(totalW-1,totalH-1),rc=mkRoleCanvas(totalW-1,totalH-1);function setC(x,y,ch,role){x>=0&&x<canvas.length&&y>=0&&y<(canvas[0]?.length??0)&&(canvas[x][y]=ch,setRole(rc,x,y,role))}for(let p of placed.values()){let boxCanvas=drawMultiBox(p.sections,useAscii);for(let bx=0;bx<boxCanvas.length;bx++)for(let by=0;by<boxCanvas[0].length;by++){let ch=boxCanvas[bx][by];if(ch!==" "){let cx=p.x+bx,cy=p.y+by;cx<totalW&&cy<totalH&&setC(cx,cy,ch,classifyBoxChar(ch))}}}let boxOccupancy=[];for(let p of placed.values())boxOccupancy.push({x1:p.x,x2:p.x+p.width-1,y1:p.y,y2:p.y+p.height-1});function isInsideBox(x,y,excludeIds){for(let[id,p]of placed.entries())if(!excludeIds?.has(id)&&x>=p.x&&x<=p.x+p.width-1&&y>=p.y&&y<=p.y+p.height-1)return!0;return!1}function findClearColumn(startX,y1,y2,excludeIds){let clear=!0;for(let y=Math.min(y1,y2);y<=Math.max(y1,y2);y++)if(isInsideBox(startX,y,excludeIds)){clear=!1;break}if(clear)return startX;for(let offset=1;offset<totalW+10;offset++){let rightX=startX+offset;clear=!0;for(let y=Math.min(y1,y2);y<=Math.max(y1,y2);y++)if(isInsideBox(rightX,y,excludeIds)){clear=!1;break}if(clear)return rightX;let leftX=startX-offset;if(leftX>=0){clear=!0;for(let y=Math.min(y1,y2);y<=Math.max(y1,y2);y++)if(isInsideBox(leftX,y,excludeIds)){clear=!1;break}if(clear)return leftX}}return totalW+2}let H=useAscii?"-":"\u2500",V=useAscii?"|":"\u2502",dashH=useAscii?".":"\u254C",dashV=useAscii?":":"\u250A";for(let rel of diagram.relationships){let fromP=placed.get(rel.from),toP=placed.get(rel.to);if(!fromP||!toP)continue;let marker=getRelMarker(rel.type,rel.markerAt),lineH=marker.dashed?dashH:H,lineV=marker.dashed?dashV:V,excludeIds=new Set([rel.from,rel.to]),fromCX=fromP.x+Math.floor(fromP.width/2),fromBY=fromP.y+fromP.height-1,toCX=toP.x+Math.floor(toP.width/2),toTY=toP.y;if(fromBY<toTY){let routeX=findClearColumn(fromCX,fromBY+1,toTY-1,excludeIds),needsDetour=routeX!==fromCX;if(routeX>=totalW&&increaseSize(canvas,routeX+2,totalH),needsDetour){let exitY=fromBY+1,entryY=toTY-1,lx1=Math.min(fromCX,routeX),rx1=Math.max(fromCX,routeX);for(let x=lx1;x<=rx1;x++)setC(x,exitY,lineH,"line");!useAscii&&exitY<(canvas[0]?.length??0)&&(fromCX<routeX?(setC(fromCX,exitY,"\u2514","corner"),setC(routeX,exitY,"\u2510","corner")):(setC(fromCX,exitY,"\u2518","corner"),setC(routeX,exitY,"\u250C","corner")));for(let y=exitY+1;y<=entryY;y++)setC(routeX,y,lineV,"line");if(routeX!==toCX){let lx2=Math.min(routeX,toCX),rx2=Math.max(routeX,toCX);for(let x=lx2;x<=rx2;x++)setC(x,entryY,lineH,"line");!useAscii&&entryY<(canvas[0]?.length??0)&&(routeX<toCX?(setC(routeX,entryY,"\u2514","corner"),setC(toCX,entryY,"\u2510","corner")):(setC(routeX,entryY,"\u2518","corner"),setC(toCX,entryY,"\u250C","corner")))}if(marker.markerAt==="to"){let markerChar=getMarkerShape(marker.type,useAscii,"down");setC(toCX,entryY,markerChar,"arrow")}if(marker.markerAt==="from"){let markerChar=getMarkerShape(marker.type,useAscii,"down");setC(fromCX,fromBY+1,markerChar,"arrow")}}else{let midY=fromBY+Math.floor((toTY-fromBY)/2);for(let y=fromBY+1;y<=midY;y++)setC(fromCX,y,lineV,"line");if(fromCX!==toCX&&midY<(canvas[0]?.length??0)){let lx=Math.min(fromCX,toCX),rx=Math.max(fromCX,toCX);for(let x=lx;x<=rx;x++)setC(x,midY,lineH,"line");useAscii||(setC(fromCX,midY,fromCX<toCX?"\u2514":"\u2518","corner"),setC(toCX,midY,fromCX<toCX?"\u2510":"\u250C","corner"))}for(let y=midY+1;y<toTY;y++)setC(toCX,y,lineV,"line");marker.markerAt==="to"&&setC(toCX,toTY-1,getMarkerShape(marker.type,useAscii,"down"),"arrow"),marker.markerAt==="from"&&setC(fromCX,fromBY+1,getMarkerShape(marker.type,useAscii,"down"),"arrow")}}else if(toP.y+toP.height-1<fromP.y){let fromTY=fromP.y,toBY=toP.y+toP.height-1,midY=toBY+Math.floor((fromTY-toBY)/2);for(let y=fromTY-1;y>=midY;y--)setC(fromCX,y,lineV,"line");if(fromCX!==toCX){let lx=Math.min(fromCX,toCX),rx=Math.max(fromCX,toCX);for(let x=lx;x<=rx;x++)setC(x,midY,lineH,"line");!useAscii&&midY>=0&&midY<totalH&&(setC(fromCX,midY,fromCX<toCX?"\u250C":"\u2510","corner"),setC(toCX,midY,fromCX<toCX?"\u2518":"\u2514","corner"))}for(let y=midY-1;y>toBY;y--)setC(toCX,y,lineV,"line");if(marker.markerAt==="from"){let markerChar=getMarkerShape(marker.type,useAscii,"up"),my=fromTY-1;for(let i=0;i<markerChar.length;i++)setC(fromCX-Math.floor(markerChar.length/2)+i,my,markerChar[i],"arrow")}if(marker.markerAt==="to"){let markerDir=marker.type==="inheritance"||marker.type==="realization"?"down":"up",markerChar=getMarkerShape(marker.type,useAscii,markerDir),my=toBY+1;for(let i=0;i<markerChar.length;i++)setC(toCX-Math.floor(markerChar.length/2)+i,my,markerChar[i],"arrow")}}else{let detourY=Math.max(fromBY,toP.y+toP.height-1)+2;increaseSize(canvas,totalW,detourY+1),increaseRoleCanvasSize(rc,totalW,detourY+1);for(let y=fromBY+1;y<=detourY;y++)setC(fromCX,y,lineV,"line");let lx=Math.min(fromCX,toCX),rx=Math.max(fromCX,toCX);for(let x=lx;x<=rx;x++)setC(x,detourY,lineH,"line");for(let y=detourY-1;y>=toP.y+toP.height;y--)setC(toCX,y,lineV,"line");if(marker.markerAt==="from"){let markerChar=getMarkerShape(marker.type,useAscii,"down"),my=fromBY+1;for(let i=0;i<markerChar.length;i++)setC(fromCX-Math.floor(markerChar.length/2)+i,my,markerChar[i],"arrow")}if(marker.markerAt==="to"){let markerChar=getMarkerShape(marker.type,useAscii,"up"),my=toP.y+toP.height;for(let i=0;i<markerChar.length;i++)setC(toCX-Math.floor(markerChar.length/2)+i,my,markerChar[i],"arrow")}}if(rel.label){let lines2=splitLines(rel.label),maxLabelWidth=Math.max(...lines2.map(l=>l.length))+2,baseMidY,idealMidX;if(fromBY<toTY)baseMidY=Math.floor((fromBY+1+toTY-1)/2),idealMidX=Math.floor((fromCX+toCX)/2);else if(toP.y+toP.height-1<fromP.y){let toBY=toP.y+toP.height-1;baseMidY=Math.floor((toBY+1+fromP.y-1)/2),idealMidX=Math.floor((fromCX+toCX)/2)}else baseMidY=Math.max(fromBY,toP.y+toP.height-1)+2,idealMidX=Math.floor((fromCX+toCX)/2);let labelY=baseMidY,halfHeight=Math.floor(lines2.length/2),labelInBox=!1;for(let i=0;i<lines2.length;i++){let y=labelY-halfHeight+i,idealLabelStart=idealMidX-Math.floor(maxLabelWidth/2),labelStart=Math.max(0,idealLabelStart);for(let x=labelStart;x<labelStart+maxLabelWidth;x++)if(isInsideBox(x,y,excludeIds)){labelInBox=!0;break}if(labelInBox)break}if(labelInBox){let gapTop=fromBY+1,gapBottom=toTY-1;for(let y=gapTop;y<=gapBottom;y++){let clearRow=!0,idealLabelStart=idealMidX-Math.floor(maxLabelWidth/2),labelStart=Math.max(0,idealLabelStart);for(let x=labelStart;x<labelStart+maxLabelWidth;x++)if(isInsideBox(x,y,excludeIds)){clearRow=!1;break}if(clearRow){labelY=y;break}}}let startY=labelY-halfHeight;for(let lineIdx=0;lineIdx<lines2.length;lineIdx++){let paddedLine=` ${lines2[lineIdx]} `,idealLabelStart=idealMidX-Math.floor(paddedLine.length/2),labelStart=Math.max(0,idealLabelStart),y=startY+lineIdx,labelEnd=labelStart+paddedLine.length;labelEnd>0&&y>=0&&(increaseSize(canvas,Math.max(labelEnd,1),Math.max(y+1,1)),increaseRoleCanvasSize(rc,Math.max(labelEnd,1),Math.max(y+1,1)));for(let i=0;i<paddedLine.length;i++){let lx=labelStart+i;lx>=0&&y>=0&&setC(lx,y,paddedLine[i],"text")}}}}return canvasToString(canvas,{roleCanvas:rc,colorMode,theme})}function parseErDiagram(lines){let diagram={entities:[],relationships:[]},entityMap=new Map,currentEntity=null;for(let i=1;i<lines.length;i++){let line=lines[i];if(currentEntity){if(line==="}"){currentEntity=null;continue}let attr=parseAttribute(line);attr&¤tEntity.attributes.push(attr);continue}let entityBlockMatch=line.match(/^(\S+)\s*\{$/);if(entityBlockMatch){let id=entityBlockMatch[1];currentEntity=ensureEntity(entityMap,id);continue}let rel=parseRelationshipLine(line);if(rel){ensureEntity(entityMap,rel.entity1),ensureEntity(entityMap,rel.entity2),diagram.relationships.push(rel);continue}}return diagram.entities=[...entityMap.values()],diagram}function ensureEntity(entityMap,id){let entity=entityMap.get(id);return entity||(entity={id,label:id,attributes:[]},entityMap.set(id,entity)),entity}function parseAttribute(line){let match=line.match(/^(\S+)\s+(\S+)(?:\s+(.+))?$/);if(!match)return null;let type=match[1],name=match[2],rest=match[3]?.trim()??"",keys=[],comment,commentMatch=rest.match(/"([^"]*)"/);commentMatch&&(comment=normalizeBrTags(commentMatch[1]));let restWithoutComment=rest.replace(/"[^"]*"/,"").trim();for(let part of restWithoutComment.split(/\s+/)){let upper=part.toUpperCase();(upper==="PK"||upper==="FK"||upper==="UK")&&keys.push(upper)}return{type,name,keys,comment}}function parseRelationshipLine(line){let match=line.match(/^(\S+)\s+([|o}{]+(?:--|\.\.)[|o}{]+)\s+(\S+)\s*:\s*(.+)$/);if(!match)return null;let entity1=match[1],cardinalityStr=match[2],entity2=match[3],rawLabel=match[4].trim().replace(/^["']|["']$/g,""),label=normalizeBrTags(rawLabel),lineMatch=cardinalityStr.match(/^([|o}{]+)(--|\.\.?)([|o}{]+)$/);if(!lineMatch)return null;let leftStr=lineMatch[1],lineStyle=lineMatch[2],rightStr=lineMatch[3],cardinality1=parseCardinality(leftStr),cardinality2=parseCardinality(rightStr),identifying=lineStyle==="--";return!cardinality1||!cardinality2?null:{entity1,entity2,cardinality1,cardinality2,label,identifying}}function parseCardinality(str){let sorted=str.split("").sort().join("");return sorted==="||"?"one":sorted==="o|"?"zero-one":sorted==="|}"||sorted==="{|"?"many":sorted==="{o"||sorted==="o{"?"zero-many":null}function classifyBoxChar2(ch){return/^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|]$/.test(ch)?"border":"text"}function formatAttribute(attr){return`${attr.keys.length>0?attr.keys.join(",")+" ":" "}${attr.type} ${attr.name}`}function buildEntitySections(entity){let header=splitLines(entity.label),attrs=entity.attributes.map(formatAttribute);return attrs.length===0?[header]:[header,attrs]}function getCrowsFootChars(card,useAscii,isRight=!1){if(useAscii)switch(card){case"one":return"|";case"zero-one":return"o|";case"many":return isRight?"<":">";case"zero-many":return isRight?"o<":">o"}else switch(card){case"one":return"\u2502";case"zero-one":return"\u25CB\u2502";case"many":return isRight?"\u255F":"\u2562";case"zero-many":return isRight?"\u25CB\u255F":"\u2562\u25CB"}}function findConnectedComponents(diagram){let visited=new Set,components=[],neighbors=new Map;for(let ent of diagram.entities)neighbors.set(ent.id,new Set);for(let rel of diagram.relationships)neighbors.get(rel.entity1)?.add(rel.entity2),neighbors.get(rel.entity2)?.add(rel.entity1);function dfs(startId,component){let stack=[startId];for(;stack.length>0;){let nodeId=stack.pop();if(!visited.has(nodeId)){visited.add(nodeId),component.add(nodeId);for(let neighbor of neighbors.get(nodeId)??[])visited.has(neighbor)||stack.push(neighbor)}}}for(let ent of diagram.entities)if(!visited.has(ent.id)){let component=new Set;dfs(ent.id,component),component.size>0&&components.push(component)}return components}function renderErAscii(text,config,colorMode,theme){let lines=text.split(`
9`).map(l=>l.trim()).filter(l=>l.length>0&&!l.startsWith("%%")),diagram=parseErDiagram(lines);if(diagram.entities.length===0)return"";let useAscii=config.useAscii,hGap=6,vGap=4,componentGap=6,entitySections=new Map,entityBoxW=new Map,entityBoxH=new Map,entityById=new Map;for(let ent of diagram.entities){entityById.set(ent.id,ent);let sections=buildEntitySections(ent);entitySections.set(ent.id,sections);let maxTextW=0;for(let section of sections)for(let line of section)maxTextW=Math.max(maxTextW,line.length);let boxW=maxTextW+4,totalLines=0;for(let section of sections)totalLines+=Math.max(section.length,1);let boxH=totalLines+(sections.length-1)+2;entityBoxW.set(ent.id,boxW),entityBoxH.set(ent.id,boxH)}let components=findConnectedComponents(diagram),placed=new Map,currentY=0;for(let component of components){let componentEntities=diagram.entities.filter(e=>component.has(e.id)),maxPerRow=Math.max(2,Math.ceil(Math.sqrt(componentEntities.length))),currentX=0,maxRowH=0,colCount=0,componentStartY=currentY;for(let ent of componentEntities){let w=entityBoxW.get(ent.id),h=entityBoxH.get(ent.id);colCount>=maxPerRow&&(currentY+=maxRowH+vGap,currentX=0,maxRowH=0,colCount=0),placed.set(ent.id,{entity:ent,sections:entitySections.get(ent.id),x:currentX,y:currentY,width:w,height:h}),currentX+=w+hGap,maxRowH=Math.max(maxRowH,h),colCount++}currentY+=maxRowH+componentGap}let totalW=0,totalH=0;for(let p of placed.values())totalW=Math.max(totalW,p.x+p.width),totalH=Math.max(totalH,p.y+p.height);totalW+=4,totalH+=2;let canvas=mkCanvas(totalW-1,totalH-1),rc=mkRoleCanvas(totalW-1,totalH-1);function setC(x,y,ch,role){x>=0&&x<canvas.length&&y>=0&&y<(canvas[0]?.length??0)&&(canvas[x][y]=ch,setRole(rc,x,y,role))}for(let p of placed.values()){let boxCanvas=drawMultiBox(p.sections,useAscii);for(let bx=0;bx<boxCanvas.length;bx++)for(let by=0;by<boxCanvas[0].length;by++){let ch=boxCanvas[bx][by];if(ch!==" "){let cx=p.x+bx,cy=p.y+by;cx<totalW&&cy<totalH&&setC(cx,cy,ch,classifyBoxChar2(ch))}}}let H=useAscii?"-":"\u2500",V=useAscii?"|":"\u2502",dashH=useAscii?".":"\u254C",dashV=useAscii?":":"\u250A";for(let rel of diagram.relationships){let e1=placed.get(rel.entity1),e2=placed.get(rel.entity2);if(!e1||!e2)continue;let lineH=rel.identifying?H:dashH,lineV=rel.identifying?V:dashV,e1CX=e1.x+Math.floor(e1.width/2),e1CY=e1.y+Math.floor(e1.height/2),e2CX=e2.x+Math.floor(e2.width/2),e2CY=e2.y+Math.floor(e2.height/2);if(Math.abs(e1CY-e2CY)<Math.max(e1.height,e2.height)){let[left,right]=e1CX<e2CX?[e1,e2]:[e2,e1],[leftCard,rightCard]=e1CX<e2CX?[rel.cardinality1,rel.cardinality2]:[rel.cardinality2,rel.cardinality1],startX=left.x+left.width,endX=right.x-1,lineY=left.y+Math.floor(left.height/2);for(let x=startX;x<=endX;x++)setC(x,lineY,lineH,"line");let leftChars=getCrowsFootChars(leftCard,useAscii,!1);for(let i=0;i<leftChars.length;i++)setC(startX+i,lineY,leftChars[i],"arrow");let rightChars=getCrowsFootChars(rightCard,useAscii,!0);for(let i=0;i<rightChars.length;i++)setC(endX-rightChars.length+1+i,lineY,rightChars[i],"arrow");if(rel.label){let lines2=splitLines(rel.label),gapMid=Math.floor((startX+endX)/2);for(let lineIdx=0;lineIdx<lines2.length;lineIdx++){let line=lines2[lineIdx],labelStart=Math.max(startX,gapMid-Math.floor(line.length/2)),labelY=lineY+1+lineIdx;increaseSize(canvas,Math.max(labelStart+line.length,1),Math.max(labelY+1,1)),increaseRoleCanvasSize(rc,Math.max(labelStart+line.length,1),Math.max(labelY+1,1));for(let i=0;i<line.length;i++){let lx=labelStart+i;lx>=startX&&lx<=endX&&setC(lx,labelY,line[i],"text")}}}}else{let[upper,lower]=e1CY<e2CY?[e1,e2]:[e2,e1],[upperCard,lowerCard]=e1CY<e2CY?[rel.cardinality1,rel.cardinality2]:[rel.cardinality2,rel.cardinality1],startY=upper.y+upper.height,endY=lower.y-1,lineX=upper.x+Math.floor(upper.width/2);for(let y=startY;y<=endY;y++)setC(lineX,y,lineV,"line");let lowerCX=lower.x+Math.floor(lower.width/2);if(lineX!==lowerCX){let midY=Math.floor((startY+endY)/2),lx=Math.min(lineX,lowerCX),rx=Math.max(lineX,lowerCX);for(let x=lx;x<=rx;x++)setC(x,midY,lineH,"line");for(let y=midY+1;y<=endY;y++)setC(lowerCX,y,lineV,"line")}let upperChars=getCrowsFootChars(upperCard,useAscii,!1);for(let i=0;i<upperChars.length;i++)setC(lineX-Math.floor(upperChars.length/2)+i,startY,upperChars[i],"arrow");let targetX=lineX!==lowerCX?lowerCX:lineX,lowerChars=getCrowsFootChars(lowerCard,useAscii,!0);for(let i=0;i<lowerChars.length;i++)setC(targetX-Math.floor(lowerChars.length/2)+i,endY,lowerChars[i],"arrow");if(rel.label){let lines2=splitLines(rel.label),startLabelY=Math.floor((startY+endY)/2)-Math.floor((lines2.length-1)/2);for(let lineIdx=0;lineIdx<lines2.length;lineIdx++){let line=lines2[lineIdx],labelX=lineX+2,y=startLabelY+lineIdx;if(y>=0)for(let i=0;i<line.length;i++){let lx=labelX+i;lx>=0&&(increaseSize(canvas,lx+1,y+1),increaseRoleCanvasSize(rc,lx+1,y+1),setC(lx,y,line[i],"text"))}}}}}return canvasToString(canvas,{roleCanvas:rc,colorMode,theme})}function parseXYChart(lines){let xAxis={},yAxis={},series=[],title,horizontal=!1;for(let line of lines){if(/^xychart(-beta)?\b/i.test(line)){/\bhorizontal\b/i.test(line)&&(horizontal=!0);continue}let titleMatch=line.match(/^title\s+"([^"]+)"/);if(titleMatch){title=titleMatch[1];continue}let xCatMatch=line.match(/^x-axis\s+(?:"([^"]*)"\s*)?\[([^\]]+)\]/);if(xCatMatch){xCatMatch[1]&&(xAxis.title=xCatMatch[1]),xAxis.categories=xCatMatch[2].split(",").map(s=>s.trim());continue}let xRangeMatch=line.match(/^x-axis\s+(?:"([^"]*)"\s+)?(-?\d+(?:\.\d+)?)\s*-->\s*(-?\d+(?:\.\d+)?)/);if(xRangeMatch){xRangeMatch[1]&&(xAxis.title=xRangeMatch[1]),xAxis.range={min:parseFloat(xRangeMatch[2]),max:parseFloat(xRangeMatch[3])};continue}let yRangeMatch=line.match(/^y-axis\s+(?:"([^"]*)"\s+)?(-?\d+(?:\.\d+)?)\s*-->\s*(-?\d+(?:\.\d+)?)/);if(yRangeMatch){yRangeMatch[1]&&(yAxis.title=yRangeMatch[1]),yAxis.range={min:parseFloat(yRangeMatch[2]),max:parseFloat(yRangeMatch[3])};continue}let yTitleOnly=line.match(/^y-axis\s+"([^"]+)"\s*$/);if(yTitleOnly){yAxis.title=yTitleOnly[1];continue}let barMatch=line.match(/^bar\s+\[([^\]]+)\]/);if(barMatch){series.push({type:"bar",data:parseNumericArray(barMatch[1])});continue}let lineMatch=line.match(/^line\s+\[([^\]]+)\]/);if(lineMatch){series.push({type:"line",data:parseNumericArray(lineMatch[1])});continue}}if(!yAxis.range&&series.length>0){let allValues=series.flatMap(s=>s.data),min=Math.min(...allValues),max=Math.max(...allValues),span=max-min||1;min=min-span*.1,max=max+span*.1,min>0&&min<span*.5&&(min=0),yAxis.range={min,max}}return yAxis.range||(yAxis.range={min:0,max:100}),{title,horizontal,xAxis,yAxis,series}}function parseNumericArray(str){return str.split(",").map(s=>parseFloat(s.trim()))}var CHART_ACCENT_FALLBACK="#3b82f6";function hexToHsl(hex){let h=hex.replace("#",""),ri=parseInt(h.substring(0,2),16)/255,gi=parseInt(h.substring(2,4),16)/255,bi=parseInt(h.substring(4,6),16)/255,max=Math.max(ri,gi,bi),min=Math.min(ri,gi,bi),l=(max+min)/2;if(max===min)return[0,0,l*100];let d=max-min,s=l>.5?d/(2-max-min):d/(max+min),hue;return max===ri?hue=((gi-bi)/d+(gi<bi?6:0))/6:max===gi?hue=((bi-ri)/d+2)/6:hue=((ri-gi)/d+4)/6,[hue*360,s*100,l*100]}function hslToHex(h,s,l){let si=s/100,li=l/100,c=(1-Math.abs(2*li-1))*si,x=c*(1-Math.abs(h/60%2-1)),m=li-c/2,r,g,b;h<60?(r=c,g=x,b=0):h<120?(r=x,g=c,b=0):h<180?(r=0,g=c,b=x):h<240?(r=0,g=x,b=c):h<300?(r=x,g=0,b=c):(r=c,g=0,b=x);let toHex=v=>Math.round((v+m)*255).toString(16).padStart(2,"0");return`#${toHex(r)}${toHex(g)}${toHex(b)}`}function isValidHex(color){return/^#[0-9a-fA-F]{6}$/.test(color)}function isDarkBackground(bgHex){return hexToHsl(bgHex)[2]<50}function getSeriesColor(index,accentColor,bgColor){if(index===0)return accentColor;let safeAccent=isValidHex(accentColor)?accentColor:CHART_ACCENT_FALLBACK,safeBg=bgColor&&isValidHex(bgColor)?bgColor:void 0,[h,s]=hexToHsl(safeAccent),chartS=Math.max(55,Math.min(85,s)),tier=Math.ceil(index/2),oddIndex=index%2===1,dark=safeBg&&isDarkBackground(safeBg)?!oddIndex:oddIndex,l=dark?Math.max(25,48-tier*13):Math.min(78,55+tier*11),hShift=(dark?-8:12)*tier,newH=((h+hShift)%360+360)%360;return hslToHex(newH,chartS,l)}var PLOT_WIDTH=60,PLOT_HEIGHT=20,UNI={hLine:"\u2500",vLine:"\u2502",origin:"\u253C",yTick:"\u2524",xTick:"\u252C",bar:"\u2588",grid:"\xB7",cornerTL:"\u256D",cornerTR:"\u256E",cornerBL:"\u2570",cornerBR:"\u256F"},ASC={hLine:"-",vLine:"|",origin:"+",yTick:"+",xTick:"+",bar:"#",grid:".",cornerTL:"+",cornerTR:"+",cornerBL:"+",cornerBR:"+"};function getSeriesColors(total,theme){let accent=theme.accent??CHART_ACCENT_FALLBACK;return total<=1?[accent]:Array.from({length:total},(_,i)=>getSeriesColor(i,accent,theme.bg))}function roleToHex(role,theme){switch(role){case"text":return theme.fg;case"border":return theme.border;case"line":return theme.line;case"arrow":return theme.arrow;case"corner":return theme.corner??theme.line;case"junction":return theme.junction??theme.border;default:return theme.fg}}function renderXYChartAscii(text,config,colorMode,theme){let lines=text.split(`
10`).map(l=>l.trim()).filter(l=>l.length>0&&!l.startsWith("%%")),chart=parseXYChart(lines),ch=config.useAscii?ASC:UNI;return chart.horizontal?renderHorizontal(chart,ch,colorMode,theme):renderVertical(chart,ch,colorMode,theme)}function renderVertical(chart,ch,colorMode,theme){let dataCount=getDataCount(chart);if(dataCount===0)return"";let yRange=chart.yAxis.range,yTicks=niceTickValues(yRange.min,yRange.max),yLabels=yTicks.map(v=>formatTickValue(v)),yGutter=Math.max(...yLabels.map(l=>l.length))+1,plotW=Math.max(PLOT_WIDTH,dataCount*6),plotH=PLOT_HEIGHT,bandW=Math.floor(plotW/dataCount),catLabels=getCategoryLabels(chart,dataCount),hasTitle=!!chart.title,hasXTitle=!!chart.xAxis.title,hasLegend=chart.series.length>1,titleRow=hasTitle?0:-1,plotTop=(hasTitle?2:0)+(hasLegend?1:0),plotLeft=yGutter+1,totalW=plotLeft+bandW*dataCount+2,xAxisRow=plotTop+plotH,xLabelRow=xAxisRow+1,xTitleRow=hasXTitle?xLabelRow+1:-1,totalH=xLabelRow+1+(hasXTitle?1:0)+0,canvas=createCanvas(totalW,totalH),roles=createRoleCanvas(totalW,totalH),hexColors=createHexCanvas(totalW,totalH),seriesColors=getSeriesColors(chart.series.length,theme),valueToRow=v=>{let t=(v-yRange.min)/(yRange.max-yRange.min||1);return Math.round(t*(plotH-1))},bandCenter=i=>plotLeft+Math.floor(bandW*(i+.5));hasTitle&&titleRow>=0&&writeText(canvas,roles,titleRow,Math.floor(totalW/2-chart.title.length/2),chart.title,"text"),hasLegend&&drawLegend(canvas,roles,hexColors,chart,hasTitle?1:0,totalW,ch,seriesColors);for(let row=0;row<plotH;row++){let displayRow=plotTop+(plotH-1-row);set(canvas,roles,displayRow,plotLeft-1,ch.vLine,"border")}set(canvas,roles,xAxisRow,plotLeft-1,ch.origin,"border");for(let tick of yTicks){let row=valueToRow(tick);if(row<0||row>=plotH)continue;let displayRow=plotTop+(plotH-1-row),label=formatTickValue(tick);set(canvas,roles,displayRow,plotLeft-1,row===0?ch.origin:ch.yTick,"border");let labelStart=yGutter-label.length;writeText(canvas,roles,displayRow,Math.max(0,labelStart),label,"text")}for(let c=plotLeft;c<plotLeft+bandW*dataCount;c++)set(canvas,roles,xAxisRow,c,ch.hLine,"border");for(let i=0;i<dataCount;i++){let cx=bandCenter(i);set(canvas,roles,xAxisRow,cx,ch.xTick,"border");let label=catLabels[i],labelStart=cx-Math.floor(label.length/2);writeText(canvas,roles,xLabelRow,Math.max(0,labelStart),label,"text")}if(hasXTitle&&xTitleRow>=0){let title=chart.xAxis.title;writeText(canvas,roles,xTitleRow,Math.floor(totalW/2-title.length/2),title,"text")}for(let tick of yTicks){let row=valueToRow(tick);if(row<0||row>=plotH)continue;let displayRow=plotTop+(plotH-1-row);for(let c=plotLeft;c<plotLeft+bandW*dataCount;c++)get(canvas,displayRow,c)===" "&&set(canvas,roles,displayRow,c,ch.grid,"line")}let barEntries=[];for(let si=0;si<chart.series.length;si++)chart.series[si].type==="bar"&&barEntries.push({data:chart.series[si].data,globalIdx:si});if(barEntries.length>0){let barCount=barEntries.length,usable=Math.max(1,bandW-2),singleBarW=Math.max(1,Math.min(Math.floor(usable/barCount),8)),groupW=singleBarW*barCount+(barCount-1),baseRow=valueToRow(Math.max(0,yRange.min));for(let bIdx=0;bIdx<barEntries.length;bIdx++){let entry=barEntries[bIdx],hexColor=seriesColors[entry.globalIdx];for(let i=0;i<entry.data.length;i++){let bx=bandCenter(i)-Math.floor(groupW/2)+bIdx*(singleBarW+1),valRow=valueToRow(entry.data[i]),fromRow=Math.min(baseRow,valRow),toRow=Math.max(baseRow,valRow);for(let row=fromRow;row<=toRow;row++){let displayRow=plotTop+(plotH-1-row);for(let c=bx;c<bx+singleBarW;c++)set(canvas,roles,displayRow,c,ch.bar,"arrow",hexColors,hexColor)}}}}let lineEntries=[];for(let si=0;si<chart.series.length;si++)chart.series[si].type==="line"&&lineEntries.push({data:chart.series[si].data,globalIdx:si});for(let entry of lineEntries){if(entry.data.length===0)continue;let hexColor=seriesColors[entry.globalIdx];drawStaircaseLine(canvas,roles,entry.data,bandCenter,valueToRow,plotTop,plotH,plotLeft,bandW*dataCount,ch,hexColors,hexColor)}return canvasToString2(canvas,roles,hexColors,colorMode,theme)}function renderHorizontal(chart,ch,colorMode,theme){let dataCount=getDataCount(chart);if(dataCount===0)return"";let yRange=chart.yAxis.range,valueTicks=niceTickValues(yRange.min,yRange.max),catLabels=getCategoryLabels(chart,dataCount),catGutter=Math.max(...catLabels.map(l=>l.length))+1,plotW=Math.max(PLOT_WIDTH,40),bandH=Math.max(2,Math.floor(PLOT_HEIGHT/dataCount)),plotH=bandH*dataCount,hasTitle=!!chart.title,hasYTitle=!!chart.yAxis.title,hasLegend=chart.series.length>1,plotTop=(hasTitle?2:0)+(hasLegend?1:0),plotLeft=catGutter+1,totalW=plotLeft+plotW+2,totalH=plotTop+plotH+2+(hasYTitle?1:0),xAxisRow=plotTop+plotH,canvas=createCanvas(totalW,totalH),roles=createRoleCanvas(totalW,totalH),hexColors=createHexCanvas(totalW,totalH),seriesColors=getSeriesColors(chart.series.length,theme),valueToCol=v=>{let t=(v-yRange.min)/(yRange.max-yRange.min||1);return plotLeft+Math.round(t*(plotW-1))},bandMid=i=>plotTop+Math.floor(bandH*(i+.5));hasTitle&&writeText(canvas,roles,0,Math.floor(totalW/2-chart.title.length/2),chart.title,"text"),hasLegend&&drawLegend(canvas,roles,hexColors,chart,hasTitle?1:0,totalW,ch,seriesColors);for(let r=plotTop;r<plotTop+plotH;r++)set(canvas,roles,r,plotLeft-1,ch.vLine,"border");set(canvas,roles,xAxisRow,plotLeft-1,ch.origin,"border");for(let i=0;i<dataCount;i++){let my=bandMid(i),label=catLabels[i],labelStart=catGutter-label.length;writeText(canvas,roles,my,Math.max(0,labelStart),label,"text")}for(let c=plotLeft;c<plotLeft+plotW;c++)set(canvas,roles,xAxisRow,c,ch.hLine,"border");for(let tick of valueTicks){let cx=valueToCol(tick);if(cx<plotLeft||cx>=plotLeft+plotW)continue;set(canvas,roles,xAxisRow,cx,ch.xTick,"border");let label=formatTickValue(tick);writeText(canvas,roles,xAxisRow+1,cx-Math.floor(label.length/2),label,"text")}if(hasYTitle){let title=chart.yAxis.title;writeText(canvas,roles,totalH-1,Math.floor(totalW/2-title.length/2),title,"text")}for(let tick of valueTicks){let cx=valueToCol(tick);if(!(cx<plotLeft||cx>=plotLeft+plotW))for(let r=plotTop;r<plotTop+plotH;r++)get(canvas,r,cx)===" "&&set(canvas,roles,r,cx,ch.grid,"line")}let barEntries=[];for(let si=0;si<chart.series.length;si++)chart.series[si].type==="bar"&&barEntries.push({data:chart.series[si].data,globalIdx:si});if(barEntries.length>0){let barCount=barEntries.length,singleBarH=1,groupH=singleBarH*barCount+(barCount-1),baseCol=valueToCol(Math.max(0,yRange.min));for(let bIdx=0;bIdx<barEntries.length;bIdx++){let entry=barEntries[bIdx],hexColor=seriesColors[entry.globalIdx];for(let i=0;i<entry.data.length;i++){let by=bandMid(i)-Math.floor(groupH/2)+bIdx*(singleBarH+1),valCol=valueToCol(entry.data[i]),fromCol=Math.min(baseCol,valCol),toCol=Math.max(baseCol,valCol);for(let r=by;r<by+singleBarH;r++)for(let c=fromCol;c<=toCol;c++)set(canvas,roles,r,c,ch.bar,"arrow",hexColors,hexColor)}}}let lineEntries=[];for(let si=0;si<chart.series.length;si++)chart.series[si].type==="line"&&lineEntries.push({data:chart.series[si].data,globalIdx:si});for(let entry of lineEntries){if(entry.data.length===0)continue;let hexColor=seriesColors[entry.globalIdx];drawHorizontalStaircaseLine(canvas,roles,entry.data,bandMid,valueToCol,plotTop,plotH,plotLeft,plotW,ch,hexColors,hexColor)}return canvasToString2(canvas,roles,hexColors,colorMode,theme)}function drawStaircaseLine(canvas,roles,data,bandCenter,valueToRow,plotTop,plotH,plotLeft,plotTotalW,ch,hexCanvas,hexColor){if(data.length===0)return;let points=data.map((v,i)=>({col:bandCenter(i),row:valueToRow(v)})),drawAt=(col,row,char)=>{let displayRow=plotTop+(plotH-1-row);displayRow>=0&&col>=plotLeft&&col<plotLeft+plotTotalW&&set(canvas,roles,displayRow,col,char,"arrow",hexCanvas,hexColor)};if(points.length===1){drawAt(points[0].col,points[0].row,ch.hLine);return}for(let i=0;i<points.length-1;i++){let p1=points[i],p2=points[i+1];if(p1.row===p2.row){for(let c=p1.col;c<=p2.col;c++)drawAt(c,p1.row,ch.hLine);continue}let midCol=Math.round((p1.col+p2.col)/2),goingUp=p2.row>p1.row;for(let c=p1.col;c<midCol;c++)drawAt(c,p1.row,ch.hLine);goingUp?drawAt(midCol,p1.row,ch.cornerBR):drawAt(midCol,p1.row,ch.cornerTR);let minRow=Math.min(p1.row,p2.row),maxRow=Math.max(p1.row,p2.row);for(let row=minRow+1;row<maxRow;row++)drawAt(midCol,row,ch.vLine);goingUp?drawAt(midCol,p2.row,ch.cornerTL):drawAt(midCol,p2.row,ch.cornerBL);for(let c=midCol+1;c<=p2.col;c++)drawAt(c,p2.row,ch.hLine);if(i===0){let leadStart=Math.max(plotLeft,p1.col-Math.floor((p2.col-p1.col)/4));for(let c=leadStart;c<p1.col;c++)drawAt(c,p1.row,ch.hLine)}if(i===points.length-2){let trailEnd=Math.min(plotLeft+plotTotalW-1,p2.col+Math.floor((p2.col-p1.col)/4));for(let c=p2.col+1;c<=trailEnd;c++)drawAt(c,p2.row,ch.hLine)}}}function drawHorizontalStaircaseLine(canvas,roles,data,bandMid,valueToCol,plotTop,plotH,plotLeft,plotW,ch,hexCanvas,hexColor){if(data.length===0)return;let points=data.map((v,i)=>({row:bandMid(i),col:valueToCol(v)})),drawAt=(row,col,char)=>{row>=plotTop&&row<plotTop+plotH&&col>=plotLeft&&col<plotLeft+plotW&&set(canvas,roles,row,col,char,"arrow",hexCanvas,hexColor)};if(points.length===1){drawAt(points[0].row,points[0].col,ch.vLine);return}for(let i=0;i<points.length-1;i++){let p1=points[i],p2=points[i+1];if(p1.col===p2.col){for(let r=p1.row;r<=p2.row;r++)drawAt(r,p1.col,ch.vLine);continue}let midRow=Math.round((p1.row+p2.row)/2),goingRight=p2.col>p1.col;for(let r=p1.row;r<midRow;r++)drawAt(r,p1.col,ch.vLine);goingRight?drawAt(midRow,p1.col,ch.cornerBL):drawAt(midRow,p1.col,ch.cornerBR);let minCol=Math.min(p1.col,p2.col),maxCol=Math.max(p1.col,p2.col);for(let c=minCol+1;c<maxCol;c++)drawAt(midRow,c,ch.hLine);goingRight?drawAt(midRow,p2.col,ch.cornerTR):drawAt(midRow,p2.col,ch.cornerTL);for(let r=midRow+1;r<=p2.row;r++)drawAt(r,p2.col,ch.vLine)}}function drawLegend(canvas,roles,hexCanvas,chart,row,totalW,ch,seriesColors){let items=[],barIdx=0,lineIdx=0;for(let si=0;si<chart.series.length;si++)chart.series[si].type==="bar"?(items.push({symbol:ch.bar,label:`Bar ${barIdx+1}`,globalIdx:si}),barIdx++):(items.push({symbol:ch.hLine,label:`Line ${lineIdx+1}`,globalIdx:si}),lineIdx++);let totalLen=0;for(let i=0;i<items.length;i++)i>0&&(totalLen+=2),totalLen+=2+items[i].label.length;let col=Math.max(0,Math.floor(totalW/2-totalLen/2));for(let i=0;i<items.length;i++){i>0&&(col+=2);let item=items[i];set(canvas,roles,row,col,item.symbol,"arrow",hexCanvas,seriesColors[item.globalIdx]),col+=1,col+=1,writeText(canvas,roles,row,col,item.label,"text"),col+=item.label.length}}function createCanvas(width,height){return Array.from({length:width},()=>Array.from({length:height},()=>" "))}function createRoleCanvas(width,height){return Array.from({length:width},()=>Array.from({length:height}).fill(null))}function createHexCanvas(width,height){return Array.from({length:width},()=>Array.from({length:height}).fill(null))}function set(canvas,roles,row,col,char,role,hexCanvas,hex){col>=0&&col<canvas.length&&row>=0&&row<canvas[0].length&&(canvas[col][row]=char,roles[col][row]=role,hexCanvas&&hex&&(hexCanvas[col][row]=hex))}function get(canvas,row,col){return col>=0&&col<canvas.length&&row>=0&&row<canvas[0].length?canvas[col][row]:" "}function writeText(canvas,roles,row,startCol,text,role){for(let i=0;i<text.length;i++)set(canvas,roles,row,startCol+i,text[i],role)}function canvasToString2(canvas,roles,hexCanvas,colorMode,theme){if(canvas.length===0)return"";let height=canvas[0].length,width=canvas.length,lines=[];for(let row=0;row<height;row++){let chars=[],rowRoles=[],rowHex=[];for(let col=0;col<width;col++)chars.push(canvas[col][row]),rowRoles.push(roles[col][row]),rowHex.push(hexCanvas[col][row]);let end=chars.length-1;for(;end>=0&&chars[end]===" ";)end--;end<0?lines.push(""):lines.push(colorizeRow(chars.slice(0,end+1),rowRoles.slice(0,end+1),rowHex.slice(0,end+1),theme,colorMode))}for(;lines.length>0&&lines[lines.length-1]==="";)lines.pop();return lines.join(`
11`)}function colorizeRow(chars,roles,hexOverrides,theme,mode){if(mode==="none")return chars.join("");let result="",currentColor=null,buffer="";for(let i=0;i<chars.length;i++){let char=chars[i];if(char===" "){buffer.length>0&&(result+=currentColor?colorizeText(buffer,currentColor,mode):buffer,buffer="",currentColor=null),result+=" ";continue}let hexOvr=hexOverrides[i]??null,roleVal=roles[i]??null,color=hexOvr??(roleVal?roleToHex(roleVal,theme):null);color===currentColor?buffer+=char:(buffer.length>0&&(result+=currentColor?colorizeText(buffer,currentColor,mode):buffer),buffer=char,currentColor=color)}return buffer.length>0&&(result+=currentColor?colorizeText(buffer,currentColor,mode):buffer),result}function getDataCount(chart){if(chart.xAxis.categories)return chart.xAxis.categories.length;for(let s of chart.series)if(s.data.length>0)return s.data.length;return 0}function getCategoryLabels(chart,count){if(chart.xAxis.categories)return chart.xAxis.categories;if(chart.xAxis.range){let{min,max}=chart.xAxis.range,step=count>1?(max-min)/(count-1):0;return Array.from({length:count},(_,i)=>formatTickValue(min+step*i))}return Array.from({length:count},(_,i)=>String(i+1))}function niceTickValues(min,max){let range=max-min;if(range<=0)return[min];let rawInterval=range/6,magnitude=Math.pow(10,Math.floor(Math.log10(rawInterval))),residual=rawInterval/magnitude,niceInterval;residual<=1.5?niceInterval=magnitude:residual<=3?niceInterval=2*magnitude:residual<=7?niceInterval=5*magnitude:niceInterval=10*magnitude;let start=Math.ceil(min/niceInterval)*niceInterval,ticks=[];for(let v=start;v<=max+niceInterval*.001;v+=niceInterval)ticks.push(Math.round(v*1e10)/1e10);return ticks}function formatTickValue(v){return Number.isInteger(v)?String(v):v.toFixed(Math.abs(v)<10?1:0)}var setChartSize=(width,height)=>{PLOT_WIDTH=width,PLOT_HEIGHT=height};function detectDiagramType(text){let firstLine=text.trim().split(`
12`)[0]?.trim().toLowerCase()??"";return/^xychart(-beta)?\b/.test(firstLine)?"xychart":/^sequencediagram\s*$/.test(firstLine)?"sequence":/^classdiagram\s*$/.test(firstLine)?"class":/^erdiagram\s*$/.test(firstLine)?"er":"flowchart"}function renderMermaidASCII(text,options={}){let config={useAscii:options.useAscii??!1,paddingX:options.paddingX??5,paddingY:options.paddingY??5,boxBorderPadding:options.boxBorderPadding??1,graphDirection:"TD"},colorMode=options.colorMode==="auto"||options.colorMode===void 0?detectColorMode():options.colorMode,theme={...DEFAULT_ASCII_THEME,...options.theme};switch(detectDiagramType(text)){case"xychart":return renderXYChartAscii(text,config,colorMode,theme);case"sequence":return renderSequenceAscii(text,config,colorMode,theme);case"class":return renderClassAscii(text,config,colorMode,theme);case"er":return renderErAscii(text,config,colorMode,theme);default:{let parsed=parseMermaid(text);parsed.direction==="LR"||parsed.direction==="RL"?config.graphDirection="LR":config.graphDirection="TD";let graph=convertToAsciiGraph(parsed,config);return createMapping(graph),drawGraph(graph),parsed.direction==="BT"&&(flipCanvasVertically(graph.canvas),flipRoleCanvasVertically(graph.roleCanvas)),canvasToString(graph.canvas,{roleCanvas:graph.roleCanvas,colorMode,theme})}}}var renderMermaidAscii=renderMermaidASCII;export{renderMermaidAscii,setChartSize};
13