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.
- package/LICENSE +21 -0
- package/NOTICE +27 -0
- package/README.md +223 -2
- package/bin/otmd.tsx +99 -0
- package/native/Cargo.lock +3708 -0
- package/native/Cargo.toml +28 -0
- package/native/build.rs +3 -0
- package/native/src/lib.rs +880 -0
- package/package.json +67 -4
- package/prebuilds/linux-x64-gnu/opentui_markdown_render.node +4 -0
- package/src/app.tsx +180 -0
- package/src/cli.ts +63 -0
- package/src/config/index.ts +46 -0
- package/src/config/load.ts +337 -0
- package/src/config/resolve.ts +36 -0
- package/src/config/theme.ts +110 -0
- package/src/config/types.ts +105 -0
- package/src/core/document.ts +44 -0
- package/src/core/frame.ts +144 -0
- package/src/core/image.ts +9 -0
- package/src/core/index.ts +6 -0
- package/src/core/layout.ts +27 -0
- package/src/core/math.ts +170 -0
- package/src/core/parse.ts +208 -0
- package/src/core/tables.ts +58 -0
- package/src/graphics.ts +85 -0
- package/src/headings.ts +77 -0
- package/src/index.ts +107 -0
- package/src/math/index.ts +86 -0
- package/src/math/native.ts +39 -0
- package/src/math/renderer.ts +84 -0
- package/src/math/validate.ts +44 -0
- package/src/media.tsx +267 -0
- package/src/merman/class/dagre-layout.ts +171 -0
- package/src/merman/class/diagram.ts +8 -0
- package/src/merman/class/drawing.ts +343 -0
- package/src/merman/class/labels.ts +38 -0
- package/src/merman/class/layout.ts +531 -0
- package/src/merman/class/parser.ts +308 -0
- package/src/merman/class/render-grid.ts +18 -0
- package/src/merman/class/style.ts +27 -0
- package/src/merman/class/types.ts +57 -0
- package/src/merman/core/canvas.ts +228 -0
- package/src/merman/core/color/style.ts +56 -0
- package/src/merman/core/drawing.ts +307 -0
- package/src/merman/core/geometry.ts +347 -0
- package/src/merman/core/mermaid.ts +38 -0
- package/src/merman/core/render-grid.ts +51 -0
- package/src/merman/core/spatial.ts +243 -0
- package/src/merman/core/text-lines.ts +32 -0
- package/src/merman/core/text.ts +44 -0
- package/src/merman/detect.ts +19 -0
- package/src/merman/diagnostics.ts +16 -0
- package/src/merman/diagram.ts +268 -0
- package/src/merman/flowchart/drawing.ts +433 -0
- package/src/merman/flowchart/labels.ts +144 -0
- package/src/merman/flowchart/layout.ts +1057 -0
- package/src/merman/flowchart/options.ts +12 -0
- package/src/merman/flowchart/parser.ts +477 -0
- package/src/merman/flowchart/render.ts +10 -0
- package/src/merman/flowchart/routing.ts +1429 -0
- package/src/merman/flowchart/style.ts +79 -0
- package/src/merman/flowchart/types.ts +63 -0
- package/src/merman/gantt/diagram.ts +8 -0
- package/src/merman/gantt/drawing.ts +193 -0
- package/src/merman/gantt/parser.ts +198 -0
- package/src/merman/gantt/render-grid.ts +17 -0
- package/src/merman/gantt/style.ts +36 -0
- package/src/merman/gantt/types.ts +45 -0
- package/src/merman/gitgraph/diagram.ts +8 -0
- package/src/merman/gitgraph/drawing.ts +234 -0
- package/src/merman/gitgraph/parser.ts +234 -0
- package/src/merman/gitgraph/render-grid.ts +17 -0
- package/src/merman/gitgraph/style.ts +37 -0
- package/src/merman/gitgraph/types.ts +36 -0
- package/src/merman/palette.ts +60 -0
- package/src/merman/sequence/diagram.ts +8 -0
- package/src/merman/sequence/drawing.ts +288 -0
- package/src/merman/sequence/options.ts +8 -0
- package/src/merman/sequence/parser.ts +303 -0
- package/src/merman/sequence/placement.ts +692 -0
- package/src/merman/sequence/render-grid.ts +22 -0
- package/src/merman/sequence/style.ts +81 -0
- package/src/merman/sequence/types.ts +74 -0
- package/src/merman/state/diagram.ts +8 -0
- package/src/merman/state/drawing.ts +363 -0
- package/src/merman/state/endpoint.ts +15 -0
- package/src/merman/state/layout.ts +1134 -0
- package/src/merman/state/note.ts +59 -0
- package/src/merman/state/options.ts +11 -0
- package/src/merman/state/parser.ts +245 -0
- package/src/merman/state/render-grid.ts +22 -0
- package/src/merman/state/routing.ts +1522 -0
- package/src/merman/state/search.ts +103 -0
- package/src/merman/state/style.ts +52 -0
- package/src/merman/state/types.ts +63 -0
- package/src/merman/state/visible-model.ts +77 -0
- package/src/merman/timeline/diagram.ts +8 -0
- package/src/merman/timeline/drawing.ts +109 -0
- package/src/merman/timeline/parser.ts +119 -0
- package/src/merman/timeline/render-grid.ts +17 -0
- package/src/merman/timeline/style.ts +36 -0
- package/src/merman/timeline/types.ts +31 -0
- package/src/merman.ts +3 -0
- package/src/statusline.tsx +183 -0
- package/src/theme/colors.ts +11 -0
- package/src/theme/defaults.ts +190 -0
- package/src/theme/index.ts +7 -0
- package/src/theme/load.ts +39 -0
- package/src/theme/resolve.ts +167 -0
- package/src/theme/syntax.ts +96 -0
- package/src/theme/types.ts +151 -0
- package/src/view/code.tsx +27 -0
- package/src/view/document-slots.ts +111 -0
- package/src/view/document-table.tsx +98 -0
- package/src/view/document.tsx +195 -0
- package/src/view/frame.tsx +83 -0
- package/src/view/inline-flow.tsx +113 -0
- package/src/view/inline-layout.ts +149 -0
- package/src/view/inline-math.ts +42 -0
- package/src/view/inline-style.ts +103 -0
- package/src/view/math.tsx +85 -0
- package/src/view/mermaid.tsx +128 -0
- package/src/view/table.ts +100 -0
- package/themes/dark.json +32 -0
- package/themes/example.jsonc +98 -0
- package/themes/light.json +32 -0
- package/themes/solarized-dark.json +32 -0
- 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
|
+
}
|