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,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
|
+
}
|