opentui-markdown-render 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +27 -0
  3. package/README.md +223 -2
  4. package/bin/otmd.tsx +99 -0
  5. package/native/Cargo.lock +3708 -0
  6. package/native/Cargo.toml +28 -0
  7. package/native/build.rs +3 -0
  8. package/native/src/lib.rs +880 -0
  9. package/package.json +67 -4
  10. package/prebuilds/linux-x64-gnu/opentui_markdown_render.node +4 -0
  11. package/src/app.tsx +180 -0
  12. package/src/cli.ts +63 -0
  13. package/src/config/index.ts +46 -0
  14. package/src/config/load.ts +337 -0
  15. package/src/config/resolve.ts +36 -0
  16. package/src/config/theme.ts +110 -0
  17. package/src/config/types.ts +105 -0
  18. package/src/core/document.ts +44 -0
  19. package/src/core/frame.ts +144 -0
  20. package/src/core/image.ts +9 -0
  21. package/src/core/index.ts +6 -0
  22. package/src/core/layout.ts +27 -0
  23. package/src/core/math.ts +170 -0
  24. package/src/core/parse.ts +208 -0
  25. package/src/core/tables.ts +58 -0
  26. package/src/graphics.ts +85 -0
  27. package/src/headings.ts +77 -0
  28. package/src/index.ts +107 -0
  29. package/src/math/index.ts +86 -0
  30. package/src/math/native.ts +39 -0
  31. package/src/math/renderer.ts +84 -0
  32. package/src/math/validate.ts +44 -0
  33. package/src/media.tsx +267 -0
  34. package/src/merman/class/dagre-layout.ts +171 -0
  35. package/src/merman/class/diagram.ts +8 -0
  36. package/src/merman/class/drawing.ts +343 -0
  37. package/src/merman/class/labels.ts +38 -0
  38. package/src/merman/class/layout.ts +531 -0
  39. package/src/merman/class/parser.ts +308 -0
  40. package/src/merman/class/render-grid.ts +18 -0
  41. package/src/merman/class/style.ts +27 -0
  42. package/src/merman/class/types.ts +57 -0
  43. package/src/merman/core/canvas.ts +228 -0
  44. package/src/merman/core/color/style.ts +56 -0
  45. package/src/merman/core/drawing.ts +307 -0
  46. package/src/merman/core/geometry.ts +347 -0
  47. package/src/merman/core/mermaid.ts +38 -0
  48. package/src/merman/core/render-grid.ts +51 -0
  49. package/src/merman/core/spatial.ts +243 -0
  50. package/src/merman/core/text-lines.ts +32 -0
  51. package/src/merman/core/text.ts +44 -0
  52. package/src/merman/detect.ts +19 -0
  53. package/src/merman/diagnostics.ts +16 -0
  54. package/src/merman/diagram.ts +268 -0
  55. package/src/merman/flowchart/drawing.ts +433 -0
  56. package/src/merman/flowchart/labels.ts +144 -0
  57. package/src/merman/flowchart/layout.ts +1057 -0
  58. package/src/merman/flowchart/options.ts +12 -0
  59. package/src/merman/flowchart/parser.ts +477 -0
  60. package/src/merman/flowchart/render.ts +10 -0
  61. package/src/merman/flowchart/routing.ts +1429 -0
  62. package/src/merman/flowchart/style.ts +79 -0
  63. package/src/merman/flowchart/types.ts +63 -0
  64. package/src/merman/gantt/diagram.ts +8 -0
  65. package/src/merman/gantt/drawing.ts +193 -0
  66. package/src/merman/gantt/parser.ts +198 -0
  67. package/src/merman/gantt/render-grid.ts +17 -0
  68. package/src/merman/gantt/style.ts +36 -0
  69. package/src/merman/gantt/types.ts +45 -0
  70. package/src/merman/gitgraph/diagram.ts +8 -0
  71. package/src/merman/gitgraph/drawing.ts +234 -0
  72. package/src/merman/gitgraph/parser.ts +234 -0
  73. package/src/merman/gitgraph/render-grid.ts +17 -0
  74. package/src/merman/gitgraph/style.ts +37 -0
  75. package/src/merman/gitgraph/types.ts +36 -0
  76. package/src/merman/palette.ts +60 -0
  77. package/src/merman/sequence/diagram.ts +8 -0
  78. package/src/merman/sequence/drawing.ts +288 -0
  79. package/src/merman/sequence/options.ts +8 -0
  80. package/src/merman/sequence/parser.ts +303 -0
  81. package/src/merman/sequence/placement.ts +692 -0
  82. package/src/merman/sequence/render-grid.ts +22 -0
  83. package/src/merman/sequence/style.ts +81 -0
  84. package/src/merman/sequence/types.ts +74 -0
  85. package/src/merman/state/diagram.ts +8 -0
  86. package/src/merman/state/drawing.ts +363 -0
  87. package/src/merman/state/endpoint.ts +15 -0
  88. package/src/merman/state/layout.ts +1134 -0
  89. package/src/merman/state/note.ts +59 -0
  90. package/src/merman/state/options.ts +11 -0
  91. package/src/merman/state/parser.ts +245 -0
  92. package/src/merman/state/render-grid.ts +22 -0
  93. package/src/merman/state/routing.ts +1522 -0
  94. package/src/merman/state/search.ts +103 -0
  95. package/src/merman/state/style.ts +52 -0
  96. package/src/merman/state/types.ts +63 -0
  97. package/src/merman/state/visible-model.ts +77 -0
  98. package/src/merman/timeline/diagram.ts +8 -0
  99. package/src/merman/timeline/drawing.ts +109 -0
  100. package/src/merman/timeline/parser.ts +119 -0
  101. package/src/merman/timeline/render-grid.ts +17 -0
  102. package/src/merman/timeline/style.ts +36 -0
  103. package/src/merman/timeline/types.ts +31 -0
  104. package/src/merman.ts +3 -0
  105. package/src/statusline.tsx +183 -0
  106. package/src/theme/colors.ts +11 -0
  107. package/src/theme/defaults.ts +190 -0
  108. package/src/theme/index.ts +7 -0
  109. package/src/theme/load.ts +39 -0
  110. package/src/theme/resolve.ts +167 -0
  111. package/src/theme/syntax.ts +96 -0
  112. package/src/theme/types.ts +151 -0
  113. package/src/view/code.tsx +27 -0
  114. package/src/view/document-slots.ts +111 -0
  115. package/src/view/document-table.tsx +98 -0
  116. package/src/view/document.tsx +195 -0
  117. package/src/view/frame.tsx +83 -0
  118. package/src/view/inline-flow.tsx +113 -0
  119. package/src/view/inline-layout.ts +149 -0
  120. package/src/view/inline-math.ts +42 -0
  121. package/src/view/inline-style.ts +103 -0
  122. package/src/view/math.tsx +85 -0
  123. package/src/view/mermaid.tsx +128 -0
  124. package/src/view/table.ts +100 -0
  125. package/themes/dark.json +32 -0
  126. package/themes/example.jsonc +98 -0
  127. package/themes/light.json +32 -0
  128. package/themes/solarized-dark.json +32 -0
  129. package/themes/solarized-light.json +32 -0
@@ -0,0 +1,12 @@
1
+ import type { BorderStyle } from "@opentui/core"
2
+ import type { FlowchartDirection } from "./types.js"
3
+
4
+ export interface FlowchartDiagramRenderOptions {
5
+ compact?: boolean
6
+ direction?: FlowchartDirection
7
+ borderStyle?: BorderStyle
8
+ minNodeGap?: number
9
+ minRankGap?: number
10
+ /** Target rendered width. Oversized horizontal layouts fold vertically and broad vertical ranks wrap. */
11
+ layoutMaxWidth?: number
12
+ }
@@ -0,0 +1,477 @@
1
+ import type {
2
+ FlowchartDiagram,
3
+ FlowchartDirection,
4
+ FlowchartEdge,
5
+ FlowchartEdgeStyle,
6
+ FlowchartNode,
7
+ FlowchartSubgraph,
8
+ } from "./types.js"
9
+ import { MermaidSyntaxError } from "../diagnostics.js"
10
+ import {
11
+ decodeMermaidText,
12
+ firstMeaningfulMermaidLine,
13
+ meaningfulNumberedMermaidLines,
14
+ stripMermaidQuotes as stripQuotes,
15
+ type NumberedMermaidLine,
16
+ } from "../core/mermaid.js"
17
+
18
+ const DEFAULT_DIRECTION = "TD" satisfies FlowchartDirection
19
+ const FLOWCHART_HEADER_RE = /^(flowchart|graph)(?:\s+(TB|TD|BT|LR|RL))?$/i
20
+ const ID_RE = "[A-Za-z_][A-Za-z0-9_.-]*"
21
+ const SUBGRAPH_RE = /^subgraph\s+(.+)$/i
22
+ const SUBGRAPH_WITH_LABEL_RE = new RegExp(`^(${ID_RE})\\s*\\[(.+)\\]$`)
23
+ const SUBGRAPH_DIRECTION_RE = /^direction\s+(TB|TD|BT|LR|RL)$/i
24
+ const IGNORED_PRESENTATION_RE = /^(?:classDef|class|style|linkStyle)\b/i
25
+ const DATABASE_NODE_RE = new RegExp(`^(${ID_RE})\\[\\((.+)\\)\\]$`)
26
+ const SUBROUTINE_NODE_RE = new RegExp(`^(${ID_RE})\\[\\[(.+)\\]\\]$`)
27
+ const ROUNDED_BRACKET_NODE_RE = new RegExp(`^(${ID_RE})\\(\\[(.+)\\]\\)$`)
28
+ const ROUNDED_NODE_RE = new RegExp(`^(${ID_RE})\\((.+)\\)$`)
29
+ const HEXAGON_NODE_RE = new RegExp(`^(${ID_RE})\\{\\{(.+)\\}\\}$`)
30
+ const PARALLELOGRAM_NODE_RE = new RegExp(`^(${ID_RE})\\[/(.+)/\\]$`)
31
+ const DECISION_NODE_RE = new RegExp(`^(${ID_RE})\\{(.+)\\}$`)
32
+ const BOX_NODE_RE = new RegExp(`^(${ID_RE})\\[(.+)\\]$`)
33
+ const ID_ONLY_RE = new RegExp(`^${ID_RE}$`)
34
+ const EXPLICIT_NODE_SHAPE_RE = new RegExp(`^${ID_RE}(?:\\[|\\(|\\{)`)
35
+ const CIRCLE_NODE_RE = new RegExp(`^(${ID_RE})\\(\\((.+)\\)\\)$`)
36
+ const MAX_FLOWCHART_LINE_LENGTH = 10_000
37
+ const EDGE_OPERATOR_RE =
38
+ /(-\.(?!->)(.+?)\.(?:->|-))|(--|==|-\.)\s+(.+?)\s+(-->|==>|\.->|-\.->|\.-)|(<-->|-->|==>|-\.->|---|~~~)\s*(?:\|([^|]*)\|\s*)?/dg
39
+
40
+ function normalizeDirection(value?: string): FlowchartDirection {
41
+ const upper = value?.toUpperCase()
42
+ if (upper === "TB" || upper === "TD" || upper === "BT" || upper === "LR" || upper === "RL") return upper
43
+ return DEFAULT_DIRECTION
44
+ }
45
+
46
+ function normalizeSubgraphId(value: string, index: number): string {
47
+ const stripped = stripQuotes(value)
48
+ return ID_ONLY_RE.test(stripped) ? stripped : `subgraph_${index + 1}`
49
+ }
50
+
51
+ function parseSubgraphToken(token: string, index: number): Pick<FlowchartSubgraph, "id" | "label"> {
52
+ const trimmed = token
53
+ .trim()
54
+ .replace(/\s*:::.*$/, "")
55
+ .replace(/;$/, "")
56
+ const withLabel = trimmed.match(SUBGRAPH_WITH_LABEL_RE)
57
+ if (withLabel) {
58
+ return { id: withLabel[1]!, label: stripQuotes(withLabel[2]!) }
59
+ }
60
+
61
+ const label = stripQuotes(trimmed)
62
+ return { id: normalizeSubgraphId(trimmed, index), label }
63
+ }
64
+
65
+ function parseNodeToken(token: string): FlowchartNode {
66
+ const trimmed = token.trim().replace(/;$/, "")
67
+ const database = trimmed.match(DATABASE_NODE_RE)
68
+ if (database) return { id: database[1]!, label: stripQuotes(database[2]!), shape: "database" }
69
+
70
+ const subroutine = trimmed.match(SUBROUTINE_NODE_RE)
71
+ if (subroutine) return { id: subroutine[1]!, label: stripQuotes(subroutine[2]!), shape: "subroutine" }
72
+
73
+ const roundedBracket = trimmed.match(ROUNDED_BRACKET_NODE_RE)
74
+ if (roundedBracket) return { id: roundedBracket[1]!, label: stripQuotes(roundedBracket[2]!), shape: "rounded" }
75
+
76
+ const circle = trimmed.match(CIRCLE_NODE_RE)
77
+ if (circle) return { id: circle[1]!, label: stripQuotes(circle[2]!), shape: "circle" }
78
+
79
+ const rounded = trimmed.match(ROUNDED_NODE_RE)
80
+ if (rounded) return { id: rounded[1]!, label: stripQuotes(rounded[2]!), shape: "rounded" }
81
+
82
+ const hexagon = trimmed.match(HEXAGON_NODE_RE)
83
+ if (hexagon) return { id: hexagon[1]!, label: stripQuotes(hexagon[2]!), shape: "hexagon" }
84
+
85
+ const parallelogram = trimmed.match(PARALLELOGRAM_NODE_RE)
86
+ if (parallelogram) return { id: parallelogram[1]!, label: stripQuotes(parallelogram[2]!), shape: "parallelogram" }
87
+
88
+ const decision = trimmed.match(DECISION_NODE_RE)
89
+ if (decision) return { id: decision[1]!, label: stripQuotes(decision[2]!), shape: "decision" }
90
+
91
+ const box = trimmed.match(BOX_NODE_RE)
92
+ if (box) return { id: box[1]!, label: stripQuotes(box[2]!), shape: "box" }
93
+
94
+ return { id: trimmed, label: trimmed, shape: "box" }
95
+ }
96
+
97
+ function isSupportedNodeToken(token: string): boolean {
98
+ const trimmed = stripNodeToken(token)
99
+ return (
100
+ ID_ONLY_RE.test(trimmed) ||
101
+ DATABASE_NODE_RE.test(trimmed) ||
102
+ SUBROUTINE_NODE_RE.test(trimmed) ||
103
+ ROUNDED_BRACKET_NODE_RE.test(trimmed) ||
104
+ CIRCLE_NODE_RE.test(trimmed) ||
105
+ ROUNDED_NODE_RE.test(trimmed) ||
106
+ HEXAGON_NODE_RE.test(trimmed) ||
107
+ PARALLELOGRAM_NODE_RE.test(trimmed) ||
108
+ DECISION_NODE_RE.test(trimmed) ||
109
+ BOX_NODE_RE.test(trimmed)
110
+ )
111
+ }
112
+
113
+ function hasExplicitNodeShape(token: string): boolean {
114
+ return EXPLICIT_NODE_SHAPE_RE.test(token.trim())
115
+ }
116
+
117
+ function ensureNode(nodes: Map<string, FlowchartNode>, token: string): FlowchartNode {
118
+ const node = parseNodeToken(token)
119
+ const existing = nodes.get(node.id)
120
+ if (!existing) {
121
+ nodes.set(node.id, node)
122
+ return node
123
+ }
124
+
125
+ if (hasExplicitNodeShape(token)) {
126
+ existing.label = node.label
127
+ existing.shape = node.shape
128
+ }
129
+ return existing
130
+ }
131
+
132
+ function addNodeToSubgraph(subgraph: FlowchartSubgraph | undefined, nodeId: string): void {
133
+ if (!subgraph || subgraph.nodeIds.includes(nodeId)) return
134
+ subgraph.nodeIds.push(nodeId)
135
+ }
136
+
137
+ function stripNodeToken(token: string): string {
138
+ return token
139
+ .replace(/\s*:::.*$/, "")
140
+ .replace(/;$/, "")
141
+ .trim()
142
+ }
143
+
144
+ /** Split an `&`-joined node group, leaving `&` inside labels (brackets or quotes) untouched. */
145
+ function splitNodeGroup(token: string): string[] {
146
+ const groups: string[] = []
147
+ const stack: string[] = []
148
+ let quote: '"' | "'" | undefined
149
+ let start = 0
150
+ const closes: Record<string, string> = { "[": "]", "(": ")", "{": "}" }
151
+
152
+ for (let index = 0; index < token.length; index++) {
153
+ const character = token[index]!
154
+ if (quote) {
155
+ if (character === quote && token[index - 1] !== "\\") quote = undefined
156
+ continue
157
+ }
158
+ if (character === '"' || character === "'") {
159
+ quote = character
160
+ continue
161
+ }
162
+ if (character in closes) {
163
+ stack.push(character)
164
+ continue
165
+ }
166
+ if (stack.length > 0 && character === closes[stack.at(-1)!]) {
167
+ stack.pop()
168
+ continue
169
+ }
170
+ if (stack.length === 0 && character === "&") {
171
+ groups.push(token.slice(start, index))
172
+ start = index + 1
173
+ }
174
+ }
175
+ groups.push(token.slice(start))
176
+ return groups
177
+ }
178
+
179
+ function edgeStyleFromArrow(...arrows: string[]): FlowchartEdgeStyle | undefined {
180
+ if (arrows.some((arrow) => arrow.includes("=="))) return "thick"
181
+ if (arrows.some((arrow) => arrow.includes("."))) return "dashed"
182
+ return undefined
183
+ }
184
+
185
+ function createEdge(
186
+ from: string,
187
+ to: string,
188
+ label: string,
189
+ style: FlowchartEdgeStyle | undefined,
190
+ arrowhead: boolean,
191
+ sourceArrowhead: boolean,
192
+ ): FlowchartEdge {
193
+ const edge: FlowchartEdge = style ? { from, to, label, style } : { from, to, label }
194
+ if (!arrowhead) edge.arrowhead = false
195
+ if (sourceArrowhead) edge.sourceArrowhead = true
196
+ return edge
197
+ }
198
+
199
+ interface ParsedEdgeOperator {
200
+ index: number
201
+ end: number
202
+ label: string
203
+ style: FlowchartEdgeStyle | undefined
204
+ arrowhead: boolean
205
+ sourceArrowhead: boolean
206
+ orderOnly: boolean
207
+ }
208
+
209
+ function parseEdgeOperators(line: string): ParsedEdgeOperator[] {
210
+ return [...maskNodeLabelOperators(line).matchAll(EDGE_OPERATOR_RE)].map((match) => {
211
+ const inlineDashedArrow = match[1]
212
+ const startArrow = inlineDashedArrow ?? match[3] ?? match[6]!
213
+ const endArrow = inlineDashedArrow ?? match[5] ?? match[6]!
214
+ const labelGroup = match[2] ? 2 : match[4] ? 4 : match[7] ? 7 : undefined
215
+ const labelRange = labelGroup === undefined ? undefined : match.indices?.[labelGroup]
216
+ return {
217
+ index: match.index,
218
+ end: match.index + match[0].length,
219
+ label: stripQuotes(labelRange ? line.slice(labelRange[0], labelRange[1]) : ""),
220
+ style: edgeStyleFromArrow(startArrow, endArrow),
221
+ arrowhead: endArrow === "~~~" || endArrow.endsWith(">"),
222
+ sourceArrowhead: startArrow.startsWith("<"),
223
+ orderOnly: endArrow === "~~~",
224
+ }
225
+ })
226
+ }
227
+
228
+ function maskNodeLabelOperators(line: string): string {
229
+ const characters = line.split("")
230
+ const stack: string[] = []
231
+ let quote: '"' | "'" | undefined
232
+ const closes: Record<string, string> = { "[": "]", "(": ")", "{": "}" }
233
+
234
+ for (let index = 0; index < characters.length; index++) {
235
+ const character = characters[index]!
236
+ if (quote) {
237
+ if (character === quote && characters[index - 1] !== "\\") quote = undefined
238
+ else if (/[<>=.-]/.test(character)) characters[index] = " "
239
+ continue
240
+ }
241
+ if (character === '"' || character === "'") {
242
+ quote = character
243
+ continue
244
+ }
245
+ if (character in closes) {
246
+ stack.push(character)
247
+ continue
248
+ }
249
+ if (stack.length > 0 && character === closes[stack.at(-1)!]) {
250
+ stack.pop()
251
+ continue
252
+ }
253
+ if (stack.length > 0 && /[<>=.-]/.test(character)) characters[index] = " "
254
+ }
255
+ return characters.join("")
256
+ }
257
+
258
+ function splitStatements(line: string): string[] {
259
+ const statements: string[] = []
260
+ const stack: string[] = []
261
+ let quote: '"' | "'" | undefined
262
+ let edgeLabel = false
263
+ const closes: Record<string, string> = { "[": "]", "(": ")", "{": "}" }
264
+ let start = 0
265
+
266
+ for (let index = 0; index < line.length; index++) {
267
+ const character = line[index]!
268
+ if (quote) {
269
+ if (character === quote && line[index - 1] !== "\\") quote = undefined
270
+ continue
271
+ }
272
+ if (character === '"' || character === "'") {
273
+ quote = character
274
+ continue
275
+ }
276
+ if (character in closes) {
277
+ stack.push(character)
278
+ continue
279
+ }
280
+ if (stack.length > 0 && character === closes[stack.at(-1)!]) {
281
+ stack.pop()
282
+ continue
283
+ }
284
+ if (stack.length === 0 && character === "|") {
285
+ edgeLabel = !edgeLabel
286
+ continue
287
+ }
288
+ if (character === ";" && stack.length === 0 && !edgeLabel) {
289
+ const statement = line.slice(start, index).trim()
290
+ if (statement) statements.push(statement)
291
+ start = index + 1
292
+ }
293
+ }
294
+ const last = line.slice(start).trim()
295
+ if (last) statements.push(last)
296
+ return statements
297
+ }
298
+
299
+ function stripInlineComment(line: string): string {
300
+ const stack: string[] = []
301
+ let quote: '"' | "'" | undefined
302
+ let edgeLabel = false
303
+ const closes: Record<string, string> = { "[": "]", "(": ")", "{": "}" }
304
+ for (let index = 0; index < line.length - 1; index++) {
305
+ const character = line[index]!
306
+ if (quote) {
307
+ if (character === quote && line[index - 1] !== "\\") quote = undefined
308
+ continue
309
+ }
310
+ if (character === '"' || character === "'") {
311
+ quote = character
312
+ continue
313
+ }
314
+ if (character in closes) {
315
+ stack.push(character)
316
+ continue
317
+ }
318
+ if (stack.length && character === closes[stack.at(-1)!]) {
319
+ stack.pop()
320
+ continue
321
+ }
322
+ if (character === "|" && stack.length === 0) {
323
+ edgeLabel = !edgeLabel
324
+ continue
325
+ }
326
+ if (character === "%" && line[index + 1] === "%" && stack.length === 0 && !edgeLabel) {
327
+ return line.slice(0, index).trimEnd()
328
+ }
329
+ }
330
+ return line
331
+ }
332
+
333
+ function* flowchartStatements(content: string): Generator<NumberedMermaidLine> {
334
+ for (const source of meaningfulNumberedMermaidLines(content)) {
335
+ if (source.text.length > MAX_FLOWCHART_LINE_LENGTH) {
336
+ throw new MermaidSyntaxError("flowchart", source.lineNumber, source.text, "Flowchart statement is too long")
337
+ }
338
+ for (const text of splitStatements(stripInlineComment(source.text))) {
339
+ // The rest of a presentation directive may contain CSS semicolons, not
340
+ // Mermaid statements. Terminal colours come from the theme instead.
341
+ if (IGNORED_PRESENTATION_RE.test(text)) break
342
+ yield { ...source, text }
343
+ }
344
+ }
345
+ }
346
+
347
+ export function isMermaidFlowchartDiagram(content: string): boolean {
348
+ const first = stripInlineComment(firstMeaningfulMermaidLine(content) ?? "").trim()
349
+ return FLOWCHART_HEADER_RE.test(first) || (first.includes(";") && FLOWCHART_HEADER_RE.test(splitStatements(first)[0] ?? ""))
350
+ }
351
+
352
+ export function parseMermaidFlowchartDiagram(content: string): FlowchartDiagram {
353
+ const nodes = new Map<string, FlowchartNode>()
354
+ const edges: FlowchartEdge[] = []
355
+ const subgraphs: FlowchartSubgraph[] = []
356
+ const subgraphStack: Array<{ subgraph: FlowchartSubgraph; lineNumber: number; sourceLine: string }> = []
357
+ let direction: FlowchartDirection = DEFAULT_DIRECTION
358
+
359
+ for (const source of flowchartStatements(content)) {
360
+ const line = source.text
361
+ const header = line.match(FLOWCHART_HEADER_RE)
362
+ if (header) {
363
+ direction = normalizeDirection(header[2])
364
+ continue
365
+ }
366
+
367
+ // Mermaid CSS styling does not apply to terminal theme rendering.
368
+ if (IGNORED_PRESENTATION_RE.test(line)) continue
369
+
370
+ const subgraphMatch = line.match(SUBGRAPH_RE)
371
+ if (subgraphMatch) {
372
+ const parsed = parseSubgraphToken(subgraphMatch[1]!, subgraphs.length)
373
+ const subgraph: FlowchartSubgraph = {
374
+ ...parsed,
375
+ nodeIds: [],
376
+ parentId: subgraphStack[subgraphStack.length - 1]?.subgraph.id,
377
+ }
378
+ subgraphs.push(subgraph)
379
+ subgraphStack.push({ subgraph, lineNumber: source.lineNumber, sourceLine: line })
380
+ continue
381
+ }
382
+
383
+ if (/^end$/i.test(line)) {
384
+ if (subgraphStack.length === 0) {
385
+ throw new MermaidSyntaxError("flowchart", source.lineNumber, line, 'Unexpected "end" without an open subgraph')
386
+ }
387
+ subgraphStack.pop()
388
+ continue
389
+ }
390
+
391
+ const currentSubgraph = subgraphStack[subgraphStack.length - 1]?.subgraph
392
+
393
+ const subgraphDirection = line.match(SUBGRAPH_DIRECTION_RE)
394
+ if (subgraphDirection) {
395
+ if (!currentSubgraph) {
396
+ throw new MermaidSyntaxError(
397
+ "flowchart",
398
+ source.lineNumber,
399
+ line,
400
+ 'A "direction" statement requires an open subgraph',
401
+ )
402
+ }
403
+ currentSubgraph.direction = normalizeDirection(subgraphDirection[1])
404
+ continue
405
+ }
406
+
407
+ const edgeOperators = parseEdgeOperators(line)
408
+ if (edgeOperators.length > 0) {
409
+ // Each chain position may be an `&` group (`A & B --> C`), so endpoints are lists of node tokens.
410
+ const nodeGroups = [
411
+ line.slice(0, edgeOperators[0]!.index),
412
+ ...edgeOperators.map((operator, index) =>
413
+ line.slice(operator.end, edgeOperators[index + 1]?.index ?? line.length),
414
+ ),
415
+ ].map((group) => splitNodeGroup(group).map(stripNodeToken))
416
+
417
+ if (nodeGroups.every((group) => group.every((token) => token.length > 0))) {
418
+ const unsupportedEndpoint = nodeGroups.find((group, index) => {
419
+ const orderOnlyEndpoint = edgeOperators[index - 1]?.orderOnly || edgeOperators[index]?.orderOnly
420
+ return group.some(
421
+ (stripped) =>
422
+ !(orderOnlyEndpoint && subgraphs.some((subgraph) => subgraph.id === stripped)) &&
423
+ !isSupportedNodeToken(stripped),
424
+ )
425
+ })
426
+ if (unsupportedEndpoint) throw new MermaidSyntaxError("flowchart", source.lineNumber, line)
427
+ const chainNodeIds = nodeGroups.map((group, index) => {
428
+ const orderOnlyEndpoint = edgeOperators[index - 1]?.orderOnly || edgeOperators[index]?.orderOnly
429
+ return group.map((stripped) => {
430
+ if (orderOnlyEndpoint && subgraphs.some((subgraph) => subgraph.id === stripped)) return stripped
431
+ return ensureNode(nodes, stripped).id
432
+ })
433
+ })
434
+ for (const nodeId of chainNodeIds.flat()) {
435
+ if (nodes.has(nodeId)) addNodeToSubgraph(currentSubgraph, nodeId)
436
+ }
437
+ for (let index = 0; index < edgeOperators.length; index++) {
438
+ const operator = edgeOperators[index]!
439
+ for (const from of chainNodeIds[index]!) {
440
+ for (const to of chainNodeIds[index + 1]!) {
441
+ const edge = createEdge(
442
+ from,
443
+ to,
444
+ operator.label,
445
+ operator.style,
446
+ operator.arrowhead,
447
+ operator.sourceArrowhead,
448
+ )
449
+ edges.push(operator.orderOnly ? { ...edge, orderOnly: true } : edge)
450
+ }
451
+ }
452
+ }
453
+ continue
454
+ }
455
+ }
456
+
457
+ const nodeGroup = splitNodeGroup(line)
458
+ if (nodeGroup.every(isSupportedNodeToken)) {
459
+ for (const token of nodeGroup) addNodeToSubgraph(currentSubgraph, ensureNode(nodes, stripNodeToken(token)).id)
460
+ continue
461
+ }
462
+
463
+ throw new MermaidSyntaxError("flowchart", source.lineNumber, line)
464
+ }
465
+
466
+ const unclosedSubgraph = subgraphStack[subgraphStack.length - 1]
467
+ if (unclosedSubgraph) {
468
+ throw new MermaidSyntaxError(
469
+ "flowchart",
470
+ unclosedSubgraph.lineNumber,
471
+ unclosedSubgraph.sourceLine,
472
+ 'Unclosed subgraph; expected "end"',
473
+ )
474
+ }
475
+
476
+ return { direction, nodes: [...nodes.values()], edges, subgraphs }
477
+ }
@@ -0,0 +1,10 @@
1
+ import { drawFlowchartDiagramGrid } from "./drawing.js"
2
+ import type { FlowchartDiagramRenderOptions } from "./options.js"
3
+ import { parseMermaidFlowchartDiagram } from "./parser.js"
4
+
5
+ export function renderFlowchartDiagram(content: string, options: FlowchartDiagramRenderOptions = {}): string {
6
+ return drawFlowchartDiagramGrid(parseMermaidFlowchartDiagram(content), options).toString({
7
+ trimTop: true,
8
+ trimBottom: true,
9
+ })
10
+ }