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,692 @@
1
+ import { diagramTextWidth } from "../core/text.js"
2
+ import { normalizeSequenceMinParticipantGap } from "./options.js"
3
+ import type {
4
+ SequenceDiagram,
5
+ SequenceDiagramRenderOptions,
6
+ SequenceFragment,
7
+ SequenceMessage,
8
+ SequenceNote,
9
+ SequenceParticipant,
10
+ SequenceParticipantGroup,
11
+ SequenceStep,
12
+ } from "./types.js"
13
+
14
+ const NOTE_HORIZONTAL_PADDING = 2
15
+ const GROUP_HORIZONTAL_PADDING = 2
16
+ const FRAGMENT_HORIZONTAL_OVERHANG = 3
17
+
18
+ export interface SequenceHorizontalBounds {
19
+ leftX: number
20
+ rightX: number
21
+ }
22
+
23
+ export interface SequenceParticipantPlacement {
24
+ participant: SequenceParticipant
25
+ centerX: number
26
+ headerLeftX: number
27
+ headerRightX: number
28
+ labelLines: string[]
29
+ }
30
+
31
+ export interface SequenceGroupPlacement {
32
+ group: SequenceParticipantGroup
33
+ labelText: string
34
+ leftX: number
35
+ rightX: number
36
+ }
37
+
38
+ export interface SequenceWallPlacement {
39
+ bounds: SequenceHorizontalBounds
40
+ startY: number
41
+ endY: number
42
+ }
43
+
44
+ export interface SequenceActivationPlacement {
45
+ participant: string
46
+ centerX: number
47
+ startY: number
48
+ endY: number
49
+ depth: number
50
+ }
51
+
52
+ export type SequenceStepPlacement =
53
+ | { type: "note"; note: SequenceNote; textLines: string[]; textX: number; textY: number }
54
+ | {
55
+ type: "fragment"
56
+ fragment: SequenceFragment
57
+ labelText: string
58
+ bounds: SequenceHorizontalBounds
59
+ y: number
60
+ wallsBefore?: SequenceWallPlacement
61
+ }
62
+ | {
63
+ type: "message"
64
+ message: SequenceMessage
65
+ labelLines: string[]
66
+ labelX: number
67
+ labelY: number
68
+ arrowY: number
69
+ fromX: number
70
+ toX: number
71
+ leftX: number
72
+ rightX: number
73
+ headX: number
74
+ direction: 1 | -1
75
+ inlineLabel?: string
76
+ }
77
+ | {
78
+ type: "selfMessage"
79
+ message: SequenceMessage
80
+ labelLines: string[]
81
+ centerX: number
82
+ rightX: number
83
+ topY: number
84
+ bottomY: number
85
+ }
86
+
87
+ export interface SequencePlacementPlan {
88
+ width: number
89
+ height: number
90
+ rows: {
91
+ participantHeaderTopY: number
92
+ participantHeaderY: number
93
+ participantRuleY: number
94
+ lifelineStartY: number
95
+ lifelineEndY: number
96
+ }
97
+ participants: SequenceParticipantPlacement[]
98
+ groups: SequenceGroupPlacement[]
99
+ activations: SequenceActivationPlacement[]
100
+ steps: SequenceStepPlacement[]
101
+ }
102
+
103
+ interface SequenceGroupRange {
104
+ group: SequenceParticipantGroup
105
+ startIndex: number
106
+ endIndex: number
107
+ }
108
+
109
+ interface PendingFragmentFrame {
110
+ startIndex: number
111
+ bounds: SequenceHorizontalBounds
112
+ }
113
+
114
+ interface ActiveFragmentFrame {
115
+ bounds: SequenceHorizontalBounds
116
+ boundaryY: number
117
+ }
118
+
119
+ function visualLength(value: string): number {
120
+ return diagramTextWidth(value)
121
+ }
122
+
123
+ function centeredStart(center: number, text: string): number {
124
+ return center - Math.floor(visualLength(text) / 2)
125
+ }
126
+
127
+ function mermaidLabelLines(label: string): string[] {
128
+ const lines = label.split(/(?:<br\s*\/?\s*>|\\n)/i).map((line) => line.trimEnd())
129
+ return lines.length > 0 ? lines : [""]
130
+ }
131
+
132
+ function noteLabelLines(label: string): string[] {
133
+ const lines = mermaidLabelLines(label)
134
+ const width = labelLinesWidth(lines)
135
+ const padding = " ".repeat(NOTE_HORIZONTAL_PADDING)
136
+ return lines.map((line) => `${padding}${line}${" ".repeat(width - visualLength(line))}${padding}`)
137
+ }
138
+
139
+ function messageLabelText(message: SequenceMessage): string {
140
+ return message.number === undefined ? message.label : `${message.number}. ${message.label}`
141
+ }
142
+
143
+ function participantHeaderWidth(label: string, compact: boolean): number {
144
+ const width = labelLinesWidth(mermaidLabelLines(label))
145
+ return compact ? width : Math.max(3, width)
146
+ }
147
+
148
+ function fragmentLabelText(fragment: SequenceFragment): string {
149
+ if (fragment.kind === "end") return ""
150
+ const prefix = fragment.kind === "loop" ? "↻ loop" : fragment.kind
151
+ return fragment.label ? ` ${prefix}: ${fragment.label} ` : ` ${prefix} `
152
+ }
153
+
154
+ export function isFragmentStart(kind: SequenceFragment["kind"]): boolean {
155
+ return kind !== "else" && kind !== "and" && kind !== "option" && kind !== "end"
156
+ }
157
+
158
+ function messageLabelLines(label: string): string[] {
159
+ return mermaidLabelLines(label)
160
+ }
161
+
162
+ function labelLinesWidth(lines: string[]): number {
163
+ return lines.reduce((max, line) => Math.max(max, visualLength(line)), 0)
164
+ }
165
+
166
+ function messageWidth(message: SequenceMessage): number {
167
+ return labelLinesWidth(messageLabelLines(messageLabelText(message)))
168
+ }
169
+
170
+ function selfMessageLoopWidthForLines(labelLines: string[]): number {
171
+ return Math.max(10, labelLinesWidth(labelLines) + 4)
172
+ }
173
+
174
+ function selfMessageLoopWidth(message: SequenceMessage): number {
175
+ return selfMessageLoopWidthForLines(messageLabelLines(messageLabelText(message)))
176
+ }
177
+
178
+ function arrowHeadX(toX: number, direction: 1 | -1, head: SequenceMessage["head"]): number {
179
+ return head === undefined ? toX : toX - direction
180
+ }
181
+
182
+ function inlineMessageLabel(
183
+ message: SequenceMessage,
184
+ labelLines: string[],
185
+ fromX: number,
186
+ toX: number,
187
+ compact: boolean,
188
+ ): string | undefined {
189
+ if (!compact || message.from === message.to || labelLines.length !== 1) return undefined
190
+ const label = ` ${labelLines[0]} `
191
+ return visualLength(label) <= Math.abs(toX - fromX) - 3 ? label : undefined
192
+ }
193
+
194
+ function getStepHeight(
195
+ step: SequenceStep,
196
+ centers: number[],
197
+ participantIndexes: Map<string, number>,
198
+ compact: boolean,
199
+ ): number {
200
+ if (step.type === "note") return noteLabelLines(step.note.label).length + 2
201
+ if (step.type === "activation") return 0
202
+ if (step.type === "fragment") return 2
203
+ const labelLines = messageLabelLines(messageLabelText(step.message))
204
+ const fromX = centers[participantIndexes.get(step.message.from) ?? -1]
205
+ const toX = centers[participantIndexes.get(step.message.to) ?? -1]
206
+ if (fromX !== undefined && toX !== undefined && inlineMessageLabel(step.message, labelLines, fromX, toX, compact)) {
207
+ return 2
208
+ }
209
+ return labelLines.length + (step.message.from === step.message.to ? 3 : 2)
210
+ }
211
+
212
+ function createParticipantIndexMap(diagram: SequenceDiagram): Map<string, number> {
213
+ return new Map(diagram.participants.map((participant, index) => [participant.id, index]))
214
+ }
215
+
216
+ function getParticipantIndexes(participantIndexes: Map<string, number>, participantIds: string[]): number[] {
217
+ return participantIds.map((id) => participantIndexes.get(id) ?? -1).filter((index) => index >= 0)
218
+ }
219
+
220
+ function groupLabelText(group: SequenceParticipantGroup): string {
221
+ return group.label ? ` ${group.label} ` : ""
222
+ }
223
+
224
+ function getGroupRanges(diagram: SequenceDiagram, participantIndexes: Map<string, number>): SequenceGroupRange[] {
225
+ return diagram.groups.flatMap((group) => {
226
+ const indexes = getParticipantIndexes(participantIndexes, group.participantIds)
227
+ return indexes.length === 0 ? [] : [{ group, startIndex: Math.min(...indexes), endIndex: Math.max(...indexes) }]
228
+ })
229
+ }
230
+
231
+ function getStepParticipantIndexes(step: SequenceStep, participantIndexes: Map<string, number>): number[] {
232
+ if (step.type === "message") {
233
+ return getParticipantIndexes(participantIndexes, [step.message.from, step.message.to])
234
+ }
235
+ if (step.type === "note") return getParticipantIndexes(participantIndexes, step.note.over)
236
+ return []
237
+ }
238
+
239
+ function getStepContentBounds(
240
+ step: SequenceStep,
241
+ centers: number[],
242
+ participantIndexes: Map<string, number>,
243
+ ): SequenceHorizontalBounds | undefined {
244
+ if (step.type === "message") {
245
+ const fromIndex = participantIndexes.get(step.message.from) ?? -1
246
+ const toIndex = participantIndexes.get(step.message.to) ?? -1
247
+ if (fromIndex < 0 || toIndex < 0) return undefined
248
+ const fromX = centers[fromIndex]!
249
+ const toX = centers[toIndex]!
250
+ if (fromIndex === toIndex) return { leftX: fromX, rightX: fromX + selfMessageLoopWidth(step.message) }
251
+ const leftX = Math.min(fromX, toX)
252
+ const rightX = Math.max(fromX, toX)
253
+ const labelWidth = messageWidth(step.message)
254
+ const labelLeftX = leftX + 2
255
+ return { leftX: Math.min(leftX, labelLeftX), rightX: Math.max(rightX, labelLeftX + labelWidth - 1) }
256
+ }
257
+ if (step.type !== "note") return undefined
258
+ const indexes = getParticipantIndexes(participantIndexes, step.note.over)
259
+ if (indexes.length === 0) return undefined
260
+ const centerX = Math.floor((centers[Math.min(...indexes)]! + centers[Math.max(...indexes)]!) / 2)
261
+ const textLines = noteLabelLines(step.note.label)
262
+ const width = labelLinesWidth(textLines)
263
+ const leftX =
264
+ step.note.position === "left"
265
+ ? centerX - width - 2
266
+ : step.note.position === "right"
267
+ ? centerX + 2
268
+ : centerX - Math.floor(width / 2)
269
+ return { leftX, rightX: leftX + width - 1 }
270
+ }
271
+
272
+ function rangeContainsIndexes(range: SequenceGroupRange, indexes: readonly number[]): boolean {
273
+ return indexes.length > 0 && indexes.every((index) => index >= range.startIndex && index <= range.endIndex)
274
+ }
275
+
276
+ function resolveGroupBounds(
277
+ diagram: SequenceDiagram,
278
+ centers: number[],
279
+ participantIndexes: Map<string, number>,
280
+ groupRanges: SequenceGroupRange[],
281
+ compact: boolean,
282
+ ): SequenceGroupPlacement[] {
283
+ return groupRanges.map((range) => {
284
+ let contentLeftX = centers[range.startIndex]!
285
+ let contentRightX = centers[range.endIndex]!
286
+ for (let i = range.startIndex; i <= range.endIndex; i++) {
287
+ const headerWidth = participantHeaderWidth(diagram.participants[i]!.label, compact)
288
+ const headerStartX = centers[i]! - Math.floor(headerWidth / 2)
289
+ contentLeftX = Math.min(contentLeftX, headerStartX)
290
+ contentRightX = Math.max(contentRightX, headerStartX + headerWidth - 1)
291
+ }
292
+ for (const step of diagram.steps) {
293
+ const indexes = getStepParticipantIndexes(step, participantIndexes)
294
+ if (!rangeContainsIndexes(range, indexes)) continue
295
+ const bounds = getStepContentBounds(step, centers, participantIndexes)
296
+ if (bounds) {
297
+ contentLeftX = Math.min(contentLeftX, bounds.leftX)
298
+ contentRightX = Math.max(contentRightX, bounds.rightX)
299
+ }
300
+ }
301
+ const labelText = groupLabelText(range.group)
302
+ let leftX = contentLeftX - GROUP_HORIZONTAL_PADDING
303
+ let rightX = contentRightX + GROUP_HORIZONTAL_PADDING
304
+ const extraWidth = Math.max(0, visualLength(labelText) + 4 - (rightX - leftX + 1))
305
+ leftX -= Math.floor(extraWidth / 2)
306
+ rightX += Math.ceil(extraWidth / 2)
307
+ return { group: range.group, labelText, leftX, rightX }
308
+ })
309
+ }
310
+
311
+ function expandHorizontalBounds(bounds: SequenceHorizontalBounds, leftX: number, rightX: number): void {
312
+ bounds.leftX = Math.min(bounds.leftX, leftX)
313
+ bounds.rightX = Math.max(bounds.rightX, rightX)
314
+ }
315
+
316
+ function getDiagramContentBounds(
317
+ diagram: SequenceDiagram,
318
+ centers: number[],
319
+ participantIndexes: Map<string, number>,
320
+ compact: boolean,
321
+ ): SequenceHorizontalBounds {
322
+ const bounds = { leftX: 0, rightX: 0 }
323
+ for (let i = 0; i < diagram.participants.length; i++) {
324
+ const headerWidth = participantHeaderWidth(diagram.participants[i]!.label, compact)
325
+ const labelStartX = centers[i]! - Math.floor(headerWidth / 2)
326
+ expandHorizontalBounds(bounds, labelStartX, labelStartX + headerWidth - 1)
327
+ }
328
+ for (const step of diagram.steps) {
329
+ const content = getStepContentBounds(step, centers, participantIndexes)
330
+ if (content) expandHorizontalBounds(bounds, content.leftX, content.rightX)
331
+ }
332
+ return bounds
333
+ }
334
+
335
+ function getFragmentFrameBounds(
336
+ centers: number[],
337
+ fragment: SequenceFragment,
338
+ nestingDepth = 0,
339
+ ): SequenceHorizontalBounds | undefined {
340
+ const leftParticipantX = centers[0]
341
+ const rightParticipantX = centers[centers.length - 1]
342
+ if (leftParticipantX === undefined || rightParticipantX === undefined) return undefined
343
+ const leftX = leftParticipantX - FRAGMENT_HORIZONTAL_OVERHANG + nestingDepth
344
+ const participantRightX = rightParticipantX + FRAGMENT_HORIZONTAL_OVERHANG - nestingDepth
345
+ return { leftX, rightX: Math.max(participantRightX, leftX + 2 + visualLength(fragmentLabelText(fragment)) + 1) }
346
+ }
347
+
348
+ function getFragmentFrameBoundsByStep(
349
+ centers: number[],
350
+ steps: SequenceStep[],
351
+ participantIndexes: Map<string, number>,
352
+ ): Map<number, SequenceHorizontalBounds> {
353
+ const boundsByStep = new Map<number, SequenceHorizontalBounds>()
354
+ const activeFrames: PendingFragmentFrame[] = []
355
+ for (const [index, step] of steps.entries()) {
356
+ if (step.type !== "fragment") {
357
+ const content = getStepContentBounds(step, centers, participantIndexes)
358
+ if (content) {
359
+ for (const frame of activeFrames) {
360
+ expandHorizontalBounds(frame.bounds, content.leftX - 1, content.rightX + 1)
361
+ }
362
+ }
363
+ continue
364
+ }
365
+ const bounds = getFragmentFrameBounds(centers, step.fragment, activeFrames.length)
366
+ if (!bounds) continue
367
+ if (isFragmentStart(step.fragment.kind)) {
368
+ activeFrames.push({ startIndex: index, bounds: { ...bounds } })
369
+ continue
370
+ }
371
+ const frame = activeFrames[activeFrames.length - 1]
372
+ if (!frame) continue
373
+ expandHorizontalBounds(frame.bounds, bounds.leftX, bounds.rightX)
374
+ if (step.fragment.kind !== "end") continue
375
+ activeFrames.pop()
376
+ boundsByStep.set(frame.startIndex, frame.bounds)
377
+ const parent = activeFrames[activeFrames.length - 1]
378
+ if (parent) expandHorizontalBounds(parent.bounds, frame.bounds.leftX - 1, frame.bounds.rightX + 1)
379
+ }
380
+ for (const frame of activeFrames) boundsByStep.set(frame.startIndex, frame.bounds)
381
+ return boundsByStep
382
+ }
383
+
384
+ function resolveParticipantCenters(
385
+ diagram: SequenceDiagram,
386
+ participantIndexes: Map<string, number>,
387
+ minParticipantGap: number,
388
+ compact: boolean,
389
+ ): number[] {
390
+ const gaps = Array.from({ length: Math.max(0, diagram.participants.length - 1) }, (_, index) => {
391
+ const left = diagram.participants[index]!
392
+ const right = diagram.participants[index + 1]!
393
+ return Math.max(
394
+ minParticipantGap,
395
+ Math.ceil(participantHeaderWidth(left.label, compact) / 2) +
396
+ Math.ceil(participantHeaderWidth(right.label, compact) / 2) +
397
+ 6,
398
+ )
399
+ })
400
+ for (const message of diagram.messages) {
401
+ const fromIndex = participantIndexes.get(message.from) ?? -1
402
+ const toIndex = participantIndexes.get(message.to) ?? -1
403
+ if (fromIndex === toIndex && fromIndex >= 0 && fromIndex < diagram.participants.length - 1) {
404
+ gaps[fromIndex] = Math.max(
405
+ gaps[fromIndex]!,
406
+ selfMessageLoopWidth(message) +
407
+ Math.ceil(labelLinesWidth(mermaidLabelLines(diagram.participants[fromIndex + 1]!.label)) / 2) +
408
+ 2,
409
+ )
410
+ continue
411
+ }
412
+ if (fromIndex < 0 || toIndex < 0 || Math.abs(fromIndex - toIndex) !== 1) continue
413
+ const gapIndex = Math.min(fromIndex, toIndex)
414
+ gaps[gapIndex] = Math.max(gaps[gapIndex]!, messageWidth(message) + 6)
415
+ }
416
+ for (const step of diagram.steps) {
417
+ if (step.type !== "note") continue
418
+ const indexes = getParticipantIndexes(participantIndexes, step.note.over)
419
+ if (indexes.length === 1 && step.note.position && step.note.position !== "over") {
420
+ const participantIndex = indexes[0]!
421
+ const gapIndex = step.note.position === "left" ? participantIndex - 1 : participantIndex
422
+ if (gapIndex >= 0 && gapIndex < gaps.length) {
423
+ gaps[gapIndex] = Math.max(gaps[gapIndex]!, labelLinesWidth(noteLabelLines(step.note.label)) + 4)
424
+ }
425
+ continue
426
+ }
427
+ if (indexes.length !== 2 || Math.abs(indexes[0]! - indexes[1]!) !== 1) continue
428
+ const gapIndex = Math.min(indexes[0]!, indexes[1]!)
429
+ gaps[gapIndex] = Math.max(gaps[gapIndex]!, labelLinesWidth(noteLabelLines(step.note.label)) + 4)
430
+ }
431
+ const centers = [Math.max(1, Math.floor(participantHeaderWidth(diagram.participants[0]?.label ?? "", compact) / 2))]
432
+ for (let i = 1; i < diagram.participants.length; i++) centers[i] = centers[i - 1]! + gaps[i - 1]!
433
+ return centers
434
+ }
435
+
436
+ function separateExpandedGroupsFromExternalParticipants(
437
+ diagram: SequenceDiagram,
438
+ centers: number[],
439
+ participantIndexes: Map<string, number>,
440
+ ranges: SequenceGroupRange[],
441
+ compact: boolean,
442
+ ): number[] {
443
+ const adjusted = [...centers]
444
+ for (let boundary = 0; boundary < adjusted.length - 1; boundary++) {
445
+ const groups = resolveGroupBounds(diagram, adjusted, participantIndexes, ranges, compact)
446
+ const leftWidth = participantHeaderWidth(diagram.participants[boundary]!.label, compact)
447
+ const rightWidth = participantHeaderWidth(diagram.participants[boundary + 1]!.label, compact)
448
+ let leftRight = adjusted[boundary]! - Math.floor(leftWidth / 2) + leftWidth - 1
449
+ let rightLeft = adjusted[boundary + 1]! - Math.floor(rightWidth / 2)
450
+ let bordersGroup = false
451
+
452
+ for (const [index, range] of ranges.entries()) {
453
+ if (range.endIndex === boundary) {
454
+ leftRight = Math.max(leftRight, groups[index]!.rightX)
455
+ bordersGroup = true
456
+ }
457
+ if (range.startIndex === boundary + 1) {
458
+ rightLeft = Math.min(rightLeft, groups[index]!.leftX)
459
+ bordersGroup = true
460
+ }
461
+ }
462
+
463
+ if (!bordersGroup) continue
464
+ const shift = leftRight + GROUP_HORIZONTAL_PADDING + 1 - rightLeft
465
+ if (shift <= 0) continue
466
+ for (let participantIndex = boundary + 1; participantIndex < adjusted.length; participantIndex++) {
467
+ adjusted[participantIndex]! += shift
468
+ }
469
+ }
470
+ return adjusted
471
+ }
472
+
473
+ export function createSequencePlacementPlan(
474
+ diagram: SequenceDiagram,
475
+ options: Pick<SequenceDiagramRenderOptions, "compact" | "minParticipantGap"> = {},
476
+ ): SequencePlacementPlan {
477
+ if (diagram.participants.length === 0) {
478
+ return {
479
+ width: 0,
480
+ height: 0,
481
+ rows: {
482
+ participantHeaderTopY: 0,
483
+ participantHeaderY: 0,
484
+ participantRuleY: 0,
485
+ lifelineStartY: 0,
486
+ lifelineEndY: 0,
487
+ },
488
+ participants: [],
489
+ groups: [],
490
+ activations: [],
491
+ steps: [],
492
+ }
493
+ }
494
+ const indexes = createParticipantIndexMap(diagram)
495
+ const compact = options.compact ?? false
496
+ let centers = resolveParticipantCenters(
497
+ diagram,
498
+ indexes,
499
+ normalizeSequenceMinParticipantGap(options.minParticipantGap),
500
+ compact,
501
+ )
502
+ const ranges = getGroupRanges(diagram, indexes)
503
+ centers = separateExpandedGroupsFromExternalParticipants(diagram, centers, indexes, ranges, compact)
504
+ let groups = resolveGroupBounds(diagram, centers, indexes, ranges, compact)
505
+ let contentBounds = getDiagramContentBounds(diagram, centers, indexes, compact)
506
+ let frameBounds = getFragmentFrameBoundsByStep(centers, diagram.steps, indexes)
507
+ const fragmentBounds = (): SequenceHorizontalBounds => {
508
+ const result = { leftX: 0, rightX: 0 }
509
+ for (const bounds of frameBounds.values()) expandHorizontalBounds(result, bounds.leftX, bounds.rightX)
510
+ return result
511
+ }
512
+ let fragments = fragmentBounds()
513
+ const leftOverflow = Math.min(
514
+ groups.reduce((left, group) => Math.min(left, group.leftX), 0),
515
+ contentBounds.leftX,
516
+ fragments.leftX,
517
+ 0,
518
+ )
519
+ if (leftOverflow < 0) {
520
+ centers = centers.map((center) => center - leftOverflow)
521
+ groups = resolveGroupBounds(diagram, centers, indexes, ranges, compact)
522
+ contentBounds = getDiagramContentBounds(diagram, centers, indexes, compact)
523
+ frameBounds = getFragmentFrameBoundsByStep(centers, diagram.steps, indexes)
524
+ fragments = fragmentBounds()
525
+ }
526
+ const hasGroups = groups.length > 0
527
+ const participantLabelHeight = Math.max(
528
+ 1,
529
+ ...diagram.participants.map((participant) => mermaidLabelLines(participant.label).length),
530
+ )
531
+ const participantHeaderTopY = hasGroups ? 1 : 0
532
+ const participantHeaderY = participantHeaderTopY
533
+ const participantRuleY = participantHeaderTopY + (compact ? participantLabelHeight - 1 : participantLabelHeight)
534
+ const lifelineStartY = participantRuleY + 1
535
+ const stepStartY = lifelineStartY + 1
536
+ const width = Math.max(contentBounds.rightX + 1, ...groups.map((group) => group.rightX + 1), fragments.rightX + 1)
537
+ const baseHeight =
538
+ stepStartY + diagram.steps.reduce((total, step) => total + getStepHeight(step, centers, indexes, compact), 0)
539
+ const height = hasGroups ? Math.max(5, baseHeight + 1) : Math.max(3, baseHeight)
540
+ const lifelineEndY = hasGroups ? height - 2 : height - 1
541
+ const participants = diagram.participants.map((participant, index) => {
542
+ const centerX = centers[index]!
543
+ const width = participantHeaderWidth(participant.label, compact)
544
+ const headerLeftX = centerX - Math.floor(width / 2)
545
+ const labelLines = mermaidLabelLines(participant.label)
546
+ return {
547
+ participant,
548
+ centerX,
549
+ headerLeftX,
550
+ headerRightX: headerLeftX + width - 1,
551
+ labelLines,
552
+ }
553
+ })
554
+ const steps: SequenceStepPlacement[] = []
555
+ const activations: SequenceActivationPlacement[] = []
556
+ const activeByParticipant = new Map<string, Array<{ startY: number; depth: number }>>()
557
+ const lastEventYByParticipant = new Map<string, number>()
558
+ const openActivation = (participant: string, y: number): void => {
559
+ const active = activeByParticipant.get(participant) ?? []
560
+ active.push({ startY: y, depth: active.length })
561
+ activeByParticipant.set(participant, active)
562
+ }
563
+ const closeActivation = (participant: string, y: number): void => {
564
+ const active = activeByParticipant.get(participant)
565
+ const opened = active?.pop()
566
+ const participantIndex = indexes.get(participant)
567
+ if (!opened || participantIndex === undefined) return
568
+ activations.push({
569
+ participant,
570
+ centerX: centers[participantIndex]!,
571
+ startY: opened.startY,
572
+ endY: y,
573
+ depth: opened.depth,
574
+ })
575
+ }
576
+ let stepY = stepStartY
577
+ const activeFrames: ActiveFragmentFrame[] = []
578
+ for (const [stepIndex, step] of diagram.steps.entries()) {
579
+ if (step.type === "activation") {
580
+ const eventY = Math.min(lastEventYByParticipant.get(step.activation.participant) ?? stepY, lifelineEndY)
581
+ if (step.activation.active) openActivation(step.activation.participant, eventY)
582
+ else closeActivation(step.activation.participant, eventY)
583
+ continue
584
+ }
585
+ const stepHeight = getStepHeight(step, centers, indexes, compact)
586
+ if (step.type === "note") {
587
+ const noteIndexes = getParticipantIndexes(indexes, step.note.over)
588
+ if (noteIndexes.length > 0) {
589
+ const centerX = Math.floor((centers[Math.min(...noteIndexes)]! + centers[Math.max(...noteIndexes)]!) / 2)
590
+ const textLines = noteLabelLines(step.note.label)
591
+ const width = labelLinesWidth(textLines)
592
+ const textX =
593
+ step.note.position === "left"
594
+ ? centerX - width - 2
595
+ : step.note.position === "right"
596
+ ? centerX + 2
597
+ : centerX - Math.floor(width / 2)
598
+ steps.push({ type: "note", note: step.note, textLines, textX, textY: stepY + 1 })
599
+ }
600
+ stepY += stepHeight
601
+ continue
602
+ }
603
+ if (step.type === "fragment") {
604
+ let bounds = frameBounds.get(stepIndex) ?? getFragmentFrameBounds(centers, step.fragment)
605
+ let wallsBefore: SequenceWallPlacement | undefined
606
+ if (isFragmentStart(step.fragment.kind)) {
607
+ if (bounds) activeFrames.push({ bounds, boundaryY: stepY })
608
+ } else {
609
+ const frame = activeFrames[activeFrames.length - 1]
610
+ if (frame) {
611
+ bounds = frame.bounds
612
+ wallsBefore = { bounds, startY: frame.boundaryY + 1, endY: stepY - 1 }
613
+ if (step.fragment.kind === "end") activeFrames.pop()
614
+ else frame.boundaryY = stepY
615
+ }
616
+ }
617
+ if (bounds)
618
+ steps.push({
619
+ type: "fragment",
620
+ fragment: step.fragment,
621
+ labelText: fragmentLabelText(step.fragment),
622
+ bounds,
623
+ y: stepY,
624
+ wallsBefore,
625
+ })
626
+ stepY += stepHeight
627
+ continue
628
+ }
629
+ const fromIndex = indexes.get(step.message.from) ?? -1
630
+ const toIndex = indexes.get(step.message.to) ?? -1
631
+ if (fromIndex < 0 || toIndex < 0) continue
632
+ const labelLines = messageLabelLines(messageLabelText(step.message))
633
+ if (fromIndex === toIndex) {
634
+ const centerX = centers[fromIndex]!
635
+ const bottomY = stepY + labelLines.length + 1
636
+ steps.push({
637
+ type: "selfMessage",
638
+ message: step.message,
639
+ labelLines,
640
+ centerX,
641
+ rightX: centerX + selfMessageLoopWidthForLines(labelLines),
642
+ topY: stepY,
643
+ bottomY,
644
+ })
645
+ if (step.message.activate) openActivation(step.message.activate, bottomY)
646
+ if (step.message.deactivate) closeActivation(step.message.deactivate, bottomY)
647
+ lastEventYByParticipant.set(step.message.from, bottomY)
648
+ } else {
649
+ const fromX = centers[fromIndex]!
650
+ const toX = centers[toIndex]!
651
+ const direction: 1 | -1 = toX > fromX ? 1 : -1
652
+ const leftX = Math.min(fromX, toX)
653
+ const rightX = Math.max(fromX, toX)
654
+ const inlineLabel = inlineMessageLabel(step.message, labelLines, fromX, toX, compact)
655
+ const arrowY = inlineLabel ? stepY : stepY + labelLines.length
656
+ const renderedLabelWidth = inlineLabel ? visualLength(inlineLabel) : labelLinesWidth(labelLines)
657
+ const labelX = inlineLabel ? Math.floor((leftX + rightX - renderedLabelWidth) / 2) : leftX + 2
658
+ steps.push({
659
+ type: "message",
660
+ message: step.message,
661
+ labelLines,
662
+ labelX,
663
+ labelY: stepY,
664
+ arrowY,
665
+ fromX,
666
+ toX,
667
+ leftX,
668
+ rightX,
669
+ direction,
670
+ headX: arrowHeadX(toX, direction, step.message.head),
671
+ inlineLabel,
672
+ })
673
+ if (step.message.activate) openActivation(step.message.activate, arrowY)
674
+ if (step.message.deactivate) closeActivation(step.message.deactivate, arrowY)
675
+ lastEventYByParticipant.set(step.message.from, arrowY)
676
+ lastEventYByParticipant.set(step.message.to, arrowY)
677
+ }
678
+ stepY += stepHeight
679
+ }
680
+ for (const [participant, active] of activeByParticipant) {
681
+ while (active.length > 0) closeActivation(participant, lifelineEndY)
682
+ }
683
+ return {
684
+ width,
685
+ height,
686
+ rows: { participantHeaderTopY, participantHeaderY, participantRuleY, lifelineStartY, lifelineEndY },
687
+ participants,
688
+ groups,
689
+ activations,
690
+ steps,
691
+ }
692
+ }