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,288 @@
1
+ import { BorderChars, type BorderStyle } from "@opentui/core"
2
+ import { DiagramCanvas } from "../core/canvas.js"
3
+ import { diagramTextWidth } from "../core/text.js"
4
+ import { DEFAULT_FRAGMENT_BORDER_STYLE } from "./options.js"
5
+ import {
6
+ createSequencePlacementPlan,
7
+ isFragmentStart,
8
+ type SequenceGroupPlacement,
9
+ type SequenceHorizontalBounds,
10
+ type SequenceStepPlacement,
11
+ } from "./placement.js"
12
+ import type { SequenceGrid } from "./render-grid.js"
13
+ import { SEQUENCE_FADE_STEPS as FADE_STEPS } from "./style.js"
14
+ import type {
15
+ MessageStyle,
16
+ SequenceArrowHead,
17
+ SequenceCellStyle,
18
+ SequenceDiagram,
19
+ SequenceDiagramRenderOptions,
20
+ } from "./types.js"
21
+
22
+ const SEQUENCE_BORDER = BorderChars.rounded
23
+
24
+ function centeredStart(center: number, text: string): number {
25
+ return center - Math.floor(diagramTextWidth(text) / 2)
26
+ }
27
+
28
+ function arrowHeadChar(head: SequenceArrowHead | undefined, direction: 1 | -1): string {
29
+ switch (head) {
30
+ case "open":
31
+ return direction === 1 ? ">" : "<"
32
+ case "cross":
33
+ return "✕"
34
+ case "async":
35
+ return direction === 1 ? ")" : "("
36
+ default:
37
+ return direction === 1 ? "►" : "◄"
38
+ }
39
+ }
40
+
41
+ function createGrid(width: number, height: number): SequenceGrid {
42
+ return new DiagramCanvas(width, height)
43
+ }
44
+
45
+ function setCell(grid: SequenceGrid, x: number, y: number, char: string, style?: SequenceCellStyle): void {
46
+ grid.setCell(x, y, char, style)
47
+ }
48
+
49
+ function setText(grid: SequenceGrid, x: number, y: number, text: string, style?: SequenceCellStyle): void {
50
+ grid.setText(Math.max(0, x), y, text, style)
51
+ }
52
+
53
+ function setArrowDepartureFade(
54
+ grid: SequenceGrid,
55
+ x: number,
56
+ y: number,
57
+ direction: 1 | -1,
58
+ style: SequenceCellStyle,
59
+ ): void {
60
+ setCell(
61
+ grid,
62
+ x,
63
+ y,
64
+ direction === 1 ? SEQUENCE_BORDER.leftT : SEQUENCE_BORDER.rightT,
65
+ `${style}Fade1` as SequenceCellStyle,
66
+ )
67
+ for (let step = 2; step <= 5; step++) {
68
+ setCell(grid, x + direction * (step - 1), y, SEQUENCE_BORDER.horizontal, `${style}Fade${step}` as SequenceCellStyle)
69
+ }
70
+ }
71
+
72
+ function groupVerticalChar(existing: string | undefined): string | undefined {
73
+ switch (existing) {
74
+ case undefined:
75
+ case " ":
76
+ return SEQUENCE_BORDER.vertical
77
+ case SEQUENCE_BORDER.vertical:
78
+ return SEQUENCE_BORDER.vertical
79
+ default:
80
+ return undefined
81
+ }
82
+ }
83
+
84
+ function setGroupVerticalCell(grid: SequenceGrid, x: number, y: number): void {
85
+ const existing = grid.getCell(x, y)?.char
86
+ const char = groupVerticalChar(existing)
87
+ if (char) setCell(grid, x, y, char, "group")
88
+ }
89
+
90
+ function renderParticipantGroups(
91
+ grid: SequenceGrid,
92
+ groupBounds: readonly SequenceGroupPlacement[],
93
+ bottomY: number,
94
+ ): void {
95
+ for (const bounds of groupBounds) {
96
+ for (let x = bounds.leftX; x <= bounds.rightX; x++) {
97
+ setCell(grid, x, 0, SEQUENCE_BORDER.horizontal, "group")
98
+ setCell(grid, x, bottomY, SEQUENCE_BORDER.horizontal, "group")
99
+ }
100
+
101
+ setCell(grid, bounds.leftX, 0, SEQUENCE_BORDER.topLeft, "group")
102
+ setCell(grid, bounds.rightX, 0, SEQUENCE_BORDER.topRight, "group")
103
+ setCell(grid, bounds.leftX, bottomY, SEQUENCE_BORDER.bottomLeft, "group")
104
+ setCell(grid, bounds.rightX, bottomY, SEQUENCE_BORDER.bottomRight, "group")
105
+
106
+ for (let y = 1; y < bottomY; y++) {
107
+ setGroupVerticalCell(grid, bounds.leftX, y)
108
+ setGroupVerticalCell(grid, bounds.rightX, y)
109
+ }
110
+
111
+ if (bounds.labelText) {
112
+ setText(grid, bounds.leftX + 2, 0, bounds.labelText, "group")
113
+ }
114
+ }
115
+ }
116
+
117
+ function drawFragmentWalls(
118
+ grid: SequenceGrid,
119
+ bounds: SequenceHorizontalBounds,
120
+ startY: number,
121
+ endY: number,
122
+ borderStyle: BorderStyle,
123
+ ): void {
124
+ if (endY < startY) return
125
+ const border = BorderChars[borderStyle]
126
+
127
+ for (let y = startY; y <= endY; y++) {
128
+ setCell(grid, bounds.leftX, y, border.vertical, "fragment")
129
+ setCell(grid, bounds.rightX, y, border.vertical, "fragment")
130
+ }
131
+ }
132
+
133
+ function renderFragment(
134
+ grid: SequenceGrid,
135
+ placement: Extract<SequenceStepPlacement, { type: "fragment" }>,
136
+ borderStyle: BorderStyle,
137
+ ): void {
138
+ const { bounds, fragment, labelText: label, y } = placement
139
+ const border = BorderChars[borderStyle]
140
+ const { leftX, rightX } = bounds
141
+
142
+ const leftChar =
143
+ isFragmentStart(fragment.kind)
144
+ ? border.topLeft
145
+ : fragment.kind !== "end"
146
+ ? border.leftT
147
+ : border.bottomLeft
148
+ const rightChar =
149
+ isFragmentStart(fragment.kind)
150
+ ? border.topRight
151
+ : fragment.kind !== "end"
152
+ ? border.rightT
153
+ : border.bottomRight
154
+
155
+ for (let x = leftX; x <= rightX; x++) {
156
+ setCell(grid, x, y, border.horizontal, "fragment")
157
+ }
158
+
159
+ setCell(grid, leftX, y, leftChar, "fragment")
160
+ setCell(grid, rightX, y, rightChar, "fragment")
161
+ if (label) {
162
+ setText(grid, leftX + 2, y, label, "fragmentLabel")
163
+ }
164
+ }
165
+
166
+ function renderSelfMessage(
167
+ grid: SequenceGrid,
168
+ placement: Extract<SequenceStepPlacement, { type: "selfMessage" }>,
169
+ style: MessageStyle,
170
+ ): void {
171
+ const { centerX, rightX, topY: topRow, bottomY: bottomRow, labelLines, message } = placement
172
+
173
+ setArrowDepartureFade(grid, centerX, topRow, 1, style)
174
+ for (let x = centerX + FADE_STEPS.length; x < rightX; x++) {
175
+ setCell(grid, x, topRow, SEQUENCE_BORDER.horizontal, style)
176
+ }
177
+ setCell(grid, rightX, topRow, SEQUENCE_BORDER.topRight, style)
178
+
179
+ for (let lineIndex = 0; lineIndex < labelLines.length; lineIndex++) {
180
+ const y = topRow + lineIndex + 1
181
+ setCell(grid, centerX, y, SEQUENCE_BORDER.vertical, "lifeline")
182
+ setText(grid, centerX + 2, y, labelLines[lineIndex]!, style)
183
+ setCell(grid, rightX, y, SEQUENCE_BORDER.vertical, style)
184
+ }
185
+
186
+ for (let x = centerX + 1; x < rightX; x++) {
187
+ setCell(grid, x, bottomRow, SEQUENCE_BORDER.horizontal, style)
188
+ }
189
+ const headX = message.head === undefined ? centerX : centerX + 1
190
+ setCell(grid, headX, bottomRow, arrowHeadChar(message.head, -1), style)
191
+ setCell(grid, rightX, bottomRow, SEQUENCE_BORDER.bottomRight, style)
192
+ }
193
+
194
+ function renderNote(grid: SequenceGrid, placement: Extract<SequenceStepPlacement, { type: "note" }>): void {
195
+ placement.textLines.forEach((line, index) =>
196
+ setText(grid, placement.textX, placement.textY + index, line, "noteBadge"),
197
+ )
198
+ }
199
+
200
+ export function drawSequenceDiagramGrid(
201
+ diagram: SequenceDiagram,
202
+ options: SequenceDiagramRenderOptions = {},
203
+ ): SequenceGrid {
204
+ const plan = createSequencePlacementPlan(diagram, options)
205
+ if (plan.width === 0 || plan.height === 0) return createGrid(0, 0)
206
+ const fragmentBorderStyle = options.fragmentBorderStyle ?? DEFAULT_FRAGMENT_BORDER_STYLE
207
+ const grid = createGrid(plan.width, plan.height)
208
+
209
+ if (plan.groups.length > 0) renderParticipantGroups(grid, plan.groups, plan.height - 1)
210
+
211
+ for (const placement of plan.participants) {
212
+ const { centerX: center, headerLeftX, headerRightX, labelLines } = placement
213
+ const { participantHeaderTopY, participantHeaderY, participantRuleY, lifelineStartY, lifelineEndY } = plan.rows
214
+
215
+ if (options.compact) {
216
+ labelLines.forEach((line, index) =>
217
+ setText(grid, centeredStart(center, line), participantHeaderY + index, line, "participant"),
218
+ )
219
+ } else {
220
+ labelLines.forEach((line, index) =>
221
+ setText(grid, centeredStart(center, line), participantHeaderTopY + index, line, "participant"),
222
+ )
223
+ for (let x = headerLeftX; x <= headerRightX; x++) {
224
+ setCell(grid, x, participantRuleY, SEQUENCE_BORDER.horizontal, "participant")
225
+ }
226
+ setCell(grid, center, participantRuleY, SEQUENCE_BORDER.topT, "participant")
227
+ }
228
+
229
+ for (let y = lifelineStartY; y <= lifelineEndY; y++) {
230
+ const distance = y - lifelineStartY
231
+ const style = !options.compact && distance < 3 ? (`lifelineRamp${distance + 1}` as SequenceCellStyle) : "lifeline"
232
+ setCell(grid, center, y, SEQUENCE_BORDER.vertical, style)
233
+ }
234
+ }
235
+
236
+ for (const placement of plan.steps) {
237
+ if (placement.type === "note") {
238
+ renderNote(grid, placement)
239
+ continue
240
+ }
241
+
242
+ if (placement.type === "fragment") {
243
+ if (placement.wallsBefore) {
244
+ drawFragmentWalls(
245
+ grid,
246
+ placement.wallsBefore.bounds,
247
+ placement.wallsBefore.startY,
248
+ placement.wallsBefore.endY,
249
+ fragmentBorderStyle,
250
+ )
251
+ }
252
+ renderFragment(grid, placement, fragmentBorderStyle)
253
+ continue
254
+ }
255
+
256
+ const message = placement.message
257
+ const messageStyle: MessageStyle = message.style === "dashed" ? "response" : "request"
258
+
259
+ if (placement.type === "selfMessage") {
260
+ renderSelfMessage(grid, placement, messageStyle)
261
+ continue
262
+ }
263
+
264
+ if (!placement.inlineLabel) {
265
+ for (let lineIndex = 0; lineIndex < placement.labelLines.length; lineIndex++) {
266
+ setText(grid, placement.labelX, placement.labelY + lineIndex, placement.labelLines[lineIndex]!, messageStyle)
267
+ }
268
+ }
269
+
270
+ for (let x = placement.leftX + 1; x < placement.rightX; x++) {
271
+ setCell(grid, x, placement.arrowY, SEQUENCE_BORDER.horizontal, messageStyle)
272
+ }
273
+
274
+ setArrowDepartureFade(grid, placement.fromX, placement.arrowY, placement.direction, messageStyle)
275
+ setCell(grid, placement.headX, placement.arrowY, arrowHeadChar(message.head, placement.direction), messageStyle)
276
+ if (placement.inlineLabel) setText(grid, placement.labelX, placement.labelY, placement.inlineLabel, messageStyle)
277
+ }
278
+
279
+ for (const activation of plan.activations) {
280
+ for (let y = activation.startY; y <= activation.endY; y++) {
281
+ if (grid.getCell(activation.centerX, y)?.char === SEQUENCE_BORDER.vertical) {
282
+ setCell(grid, activation.centerX, y, "┃", "lifeline")
283
+ }
284
+ }
285
+ }
286
+
287
+ return grid
288
+ }
@@ -0,0 +1,8 @@
1
+ import type { BorderStyle } from "@opentui/core"
2
+
3
+ export const DEFAULT_MIN_PARTICIPANT_GAP = 18
4
+ export const DEFAULT_FRAGMENT_BORDER_STYLE = "rounded" satisfies BorderStyle
5
+
6
+ export function normalizeSequenceMinParticipantGap(value: number | undefined): number {
7
+ return value === undefined || !Number.isFinite(value) ? DEFAULT_MIN_PARTICIPANT_GAP : Math.max(1, Math.floor(value))
8
+ }
@@ -0,0 +1,303 @@
1
+ import {
2
+ firstMeaningfulMermaidLine,
3
+ meaningfulNumberedMermaidLines,
4
+ stripMermaidQuotes as stripQuotes,
5
+ } from "../core/mermaid.js"
6
+ import { MermaidSyntaxError } from "../diagnostics.js"
7
+ import type {
8
+ SequenceArrowHead,
9
+ SequenceDiagram,
10
+ SequenceMessage,
11
+ SequenceParticipant,
12
+ SequenceParticipantGroup,
13
+ SequenceStep,
14
+ } from "./types.js"
15
+
16
+ const MESSAGE_RE = /^(.+?)\s*(-->>|->>|--x|-x|--\)|-\)|-->|->)([+-]?)\s*(.+?)\s*:\s*(.*)$/
17
+ const NOTE_RE = /^note\s+(over|left\s+of|right\s+of)\s+(.+?)\s*:\s*(.*)$/i
18
+ const PARTICIPANT_RE = /^(?:participant|actor)\s+(\S+)(?:\s+as\s+(.+))?$/i
19
+ const ACTIVATION_RE = /^(activate|deactivate)\s+(.+)$/i
20
+ const BOX_RE = /^box(?:\s+(.+))?$/i
21
+ const ALT_RE = /^alt\s+(.+)$/i
22
+ const ELSE_RE = /^else(?:\s+(.+))?$/i
23
+ const LOOP_RE = /^loop\s+(.+)$/i
24
+ const FRAGMENT_START_RE = /^(opt|par|critical|break)(?:\s+(.+))?$/i
25
+ const FRAGMENT_BRANCH_RE = /^(and|option)(?:\s+(.+))?$/i
26
+ const AUTONUMBER_RE = /^autonumber(?:\s+(\d+)(?:\s+(\d+))?)?$/i
27
+ const UNSUPPORTED_BIDIRECTIONAL_MESSAGE_RE = /<<-{1,2}>>/
28
+ const MESSAGE_OPERATOR_RE = /-->>|->>|--x|-x|--\)|-\)|-->|->/
29
+ const CSS_COLOR_NAMES = new Set([
30
+ "black",
31
+ "white",
32
+ "red",
33
+ "green",
34
+ "blue",
35
+ "yellow",
36
+ "cyan",
37
+ "magenta",
38
+ "silver",
39
+ "gray",
40
+ "grey",
41
+ "maroon",
42
+ "olive",
43
+ "lime",
44
+ "aqua",
45
+ "teal",
46
+ "navy",
47
+ "fuchsia",
48
+ "purple",
49
+ "orange",
50
+ "brightblack",
51
+ "brightred",
52
+ "brightgreen",
53
+ "brightblue",
54
+ "brightyellow",
55
+ "brightcyan",
56
+ "brightmagenta",
57
+ "brightwhite",
58
+ ])
59
+
60
+ function arrowHeadForSyntax(arrow: string): SequenceArrowHead | undefined {
61
+ if (arrow.endsWith("x")) return "cross"
62
+ if (arrow.endsWith(")")) return "async"
63
+ if (arrow.endsWith(">") && !arrow.endsWith(">>")) return "open"
64
+ return undefined
65
+ }
66
+
67
+ function isBoxColorToken(value: string): boolean {
68
+ const lowerValue = value.toLowerCase()
69
+ return (
70
+ lowerValue === "transparent" ||
71
+ CSS_COLOR_NAMES.has(lowerValue) ||
72
+ /^#[0-9a-f]{3,8}$/i.test(value) ||
73
+ /^rgba?\(.+\)$/i.test(value)
74
+ )
75
+ }
76
+
77
+ function splitLeadingBoxToken(value: string): { token: string; rest: string } {
78
+ if (/^rgba?\(/i.test(value)) {
79
+ const closeIndex = value.indexOf(")")
80
+ if (closeIndex >= 0) {
81
+ return { token: value.slice(0, closeIndex + 1), rest: value.slice(closeIndex + 1).trim() }
82
+ }
83
+ }
84
+
85
+ const firstSpace = value.search(/\s/)
86
+ return firstSpace < 0
87
+ ? { token: value, rest: "" }
88
+ : { token: value.slice(0, firstSpace), rest: value.slice(firstSpace + 1).trim() }
89
+ }
90
+
91
+ function boxLabelText(value: string | undefined): string {
92
+ const rawLabel = (value ?? "").trim()
93
+ if ((rawLabel.startsWith('"') && rawLabel.endsWith('"')) || (rawLabel.startsWith("'") && rawLabel.endsWith("'"))) {
94
+ return stripQuotes(rawLabel)
95
+ }
96
+
97
+ const label = stripQuotes(rawLabel)
98
+ if (!label) return ""
99
+ const { token, rest } = splitLeadingBoxToken(label)
100
+ return isBoxColorToken(token) ? stripQuotes(rest) : label
101
+ }
102
+
103
+ function addParticipantToGroup(group: SequenceParticipantGroup | undefined, participantId: string): void {
104
+ if (!group || group.participantIds.includes(participantId)) return
105
+ group.participantIds.push(participantId)
106
+ }
107
+
108
+ function ensureParticipant(
109
+ participants: SequenceParticipant[],
110
+ id: string,
111
+ label: string = id,
112
+ replaceExistingLabel: boolean = false,
113
+ ): void {
114
+ const existing = participants.find((participant) => participant.id === id)
115
+ if (existing) {
116
+ if (replaceExistingLabel) existing.label = label
117
+ return
118
+ }
119
+ participants.push({ id, label })
120
+ }
121
+
122
+ export function isMermaidSequenceDiagram(content: string): boolean {
123
+ return firstMeaningfulMermaidLine(content)?.toLowerCase() === "sequencediagram"
124
+ }
125
+
126
+ export function parseMermaidSequenceDiagram(content: string): SequenceDiagram {
127
+ const participants: SequenceParticipant[] = []
128
+ const messages: SequenceMessage[] = []
129
+ const steps: SequenceStep[] = []
130
+ const groups: SequenceParticipantGroup[] = []
131
+ const blockStack: Array<{ kind: "box" | "alt" | "loop" | "opt" | "par" | "critical" | "break"; lineNumber: number; sourceLine: string }> = []
132
+ const groupStack: SequenceParticipantGroup[] = []
133
+ let nextMessageNumber: number | undefined
134
+ let messageNumberIncrement = 1
135
+
136
+ for (const source of meaningfulNumberedMermaidLines(content)) {
137
+ const line = source.text
138
+ if (line.toLowerCase() === "sequencediagram") continue
139
+ if (UNSUPPORTED_BIDIRECTIONAL_MESSAGE_RE.test(line)) {
140
+ throw new MermaidSyntaxError("sequence", source.lineNumber, line)
141
+ }
142
+
143
+ const autonumberMatch = line.match(AUTONUMBER_RE)
144
+ if (autonumberMatch) {
145
+ nextMessageNumber = Number.parseInt(autonumberMatch[1] ?? "1", 10)
146
+ messageNumberIncrement = Number.parseInt(autonumberMatch[2] ?? "1", 10)
147
+ continue
148
+ }
149
+
150
+ const boxMatch = line.match(BOX_RE)
151
+ if (boxMatch) {
152
+ const group: SequenceParticipantGroup = { label: boxLabelText(boxMatch[1]), participantIds: [] }
153
+ groups.push(group)
154
+ groupStack.push(group)
155
+ blockStack.push({ kind: "box", lineNumber: source.lineNumber, sourceLine: line })
156
+ continue
157
+ }
158
+
159
+ const participantMatch = line.match(PARTICIPANT_RE)
160
+ if (participantMatch) {
161
+ const id = stripQuotes(participantMatch[1]!)
162
+ ensureParticipant(participants, id, stripQuotes(participantMatch[2] ?? id), true)
163
+ addParticipantToGroup(groupStack[groupStack.length - 1], id)
164
+ continue
165
+ }
166
+
167
+ const noteMatch = line.match(NOTE_RE)
168
+ if (noteMatch) {
169
+ const position = noteMatch[1]!.toLowerCase().replace(/\s+of$/, "") as "over" | "left" | "right"
170
+ const over = noteMatch[2]!
171
+ .split(",")
172
+ .map((participant) => stripQuotes(participant))
173
+ .filter((participant) => participant.length > 0)
174
+ for (const participant of over) ensureParticipant(participants, participant)
175
+ steps.push({
176
+ type: "note",
177
+ note: {
178
+ over,
179
+ label: stripQuotes(noteMatch[3]!),
180
+ ...(position === "over" ? {} : { position }),
181
+ },
182
+ })
183
+ continue
184
+ }
185
+
186
+ const activationMatch = line.match(ACTIVATION_RE)
187
+ if (activationMatch) {
188
+ const participant = stripQuotes(activationMatch[2]!)
189
+ ensureParticipant(participants, participant)
190
+ steps.push({
191
+ type: "activation",
192
+ activation: { participant, active: activationMatch[1]!.toLowerCase() === "activate" },
193
+ })
194
+ continue
195
+ }
196
+
197
+ const altMatch = line.match(ALT_RE)
198
+ if (altMatch) {
199
+ blockStack.push({ kind: "alt", lineNumber: source.lineNumber, sourceLine: line })
200
+ steps.push({ type: "fragment", fragment: { kind: "alt", label: stripQuotes(altMatch[1]!) } })
201
+ continue
202
+ }
203
+
204
+ const loopMatch = line.match(LOOP_RE)
205
+ if (loopMatch) {
206
+ blockStack.push({ kind: "loop", lineNumber: source.lineNumber, sourceLine: line })
207
+ steps.push({ type: "fragment", fragment: { kind: "loop", label: stripQuotes(loopMatch[1]!) } })
208
+ continue
209
+ }
210
+
211
+ const fragmentStart = line.match(FRAGMENT_START_RE)
212
+ if (fragmentStart) {
213
+ const kind = fragmentStart[1]!.toLowerCase() as "opt" | "par" | "critical" | "break"
214
+ blockStack.push({ kind, lineNumber: source.lineNumber, sourceLine: line })
215
+ steps.push({ type: "fragment", fragment: { kind, label: stripQuotes(fragmentStart[2] ?? "") } })
216
+ continue
217
+ }
218
+
219
+ const branchMatch = line.match(FRAGMENT_BRANCH_RE)
220
+ if (branchMatch) {
221
+ const kind = branchMatch[1]!.toLowerCase() as "and" | "option"
222
+ const expected = kind === "and" ? "par" : "critical"
223
+ if (blockStack[blockStack.length - 1]?.kind !== expected) {
224
+ throw new MermaidSyntaxError("sequence", source.lineNumber, line, `Unexpected "${kind}" without an open "${expected}" block`)
225
+ }
226
+ steps.push({ type: "fragment", fragment: { kind, label: stripQuotes(branchMatch[2] ?? "") } })
227
+ continue
228
+ }
229
+
230
+ const elseMatch = line.match(ELSE_RE)
231
+ if (elseMatch) {
232
+ if (blockStack[blockStack.length - 1]?.kind !== "alt") {
233
+ throw new MermaidSyntaxError(
234
+ "sequence",
235
+ source.lineNumber,
236
+ line,
237
+ 'Unexpected "else" without an open "alt" block',
238
+ )
239
+ }
240
+ steps.push({ type: "fragment", fragment: { kind: "else", label: stripQuotes(elseMatch[1] ?? "") } })
241
+ continue
242
+ }
243
+
244
+ if (line.toLowerCase() === "end") {
245
+ const block = blockStack.pop()
246
+ if (!block)
247
+ throw new MermaidSyntaxError("sequence", source.lineNumber, line, 'Unexpected "end" without an open block')
248
+ if (block.kind === "box") {
249
+ groupStack.pop()
250
+ continue
251
+ }
252
+ steps.push({ type: "fragment", fragment: { kind: "end", label: block.kind } })
253
+ continue
254
+ }
255
+
256
+ const messageMatch = line.match(MESSAGE_RE)
257
+ if (messageMatch) {
258
+ const from = stripQuotes(messageMatch[1]!)
259
+ const arrow = messageMatch[2]!
260
+ const activationMarker = messageMatch[3]!
261
+ const to = stripQuotes(messageMatch[4]!)
262
+ if (MESSAGE_OPERATOR_RE.test(from) || MESSAGE_OPERATOR_RE.test(to)) {
263
+ throw new MermaidSyntaxError("sequence", source.lineNumber, line)
264
+ }
265
+ const message: SequenceMessage = {
266
+ from,
267
+ to,
268
+ label: stripQuotes(messageMatch[5]!),
269
+ style: arrow.startsWith("--") ? "dashed" : "solid",
270
+ }
271
+ const activeGroup = groupStack[groupStack.length - 1]
272
+ ensureParticipant(participants, from)
273
+ ensureParticipant(participants, to)
274
+ addParticipantToGroup(activeGroup, from)
275
+ addParticipantToGroup(activeGroup, to)
276
+ const head = arrowHeadForSyntax(arrow)
277
+ if (head) message.head = head
278
+ if (nextMessageNumber !== undefined) {
279
+ message.number = nextMessageNumber
280
+ nextMessageNumber += messageNumberIncrement
281
+ }
282
+ if (activationMarker === "+") message.activate = to
283
+ else if (activationMarker === "-") message.deactivate = from
284
+ messages.push(message)
285
+ steps.push({ type: "message", message })
286
+ continue
287
+ }
288
+
289
+ throw new MermaidSyntaxError("sequence", source.lineNumber, line)
290
+ }
291
+
292
+ const unclosedBlock = blockStack[blockStack.length - 1]
293
+ if (unclosedBlock) {
294
+ throw new MermaidSyntaxError(
295
+ "sequence",
296
+ unclosedBlock.lineNumber,
297
+ unclosedBlock.sourceLine,
298
+ `Unclosed ${unclosedBlock.kind} block; expected "end"`,
299
+ )
300
+ }
301
+
302
+ return { participants, messages, steps, groups }
303
+ }