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,307 @@
1
+ import { BorderChars, type BorderCharacters } from "@opentui/core"
2
+ import {
3
+ directionBetween,
4
+ walkOrthogonalSegment,
5
+ type DiagramBounds,
6
+ type DiagramDirection,
7
+ type DiagramPoint,
8
+ } from "./geometry.js"
9
+
10
+ export type DiagramLineCornerStyle = "square" | "rounded"
11
+ export type DiagramLineStyle = "single" | "heavy" | "dashed"
12
+ export type DiagramArrowHeadStyle = "filled" | "line"
13
+
14
+ export interface DiagramDiamondCharacters {
15
+ topLeft: string
16
+ topRight: string
17
+ upperLeft: string
18
+ upperLeftJoin: string
19
+ upperRightJoin: string
20
+ upperRight: string
21
+ vertical: string
22
+ lowerLeft: string
23
+ lowerLeftJoin: string
24
+ lowerRightJoin: string
25
+ lowerRight: string
26
+ bottomLeft: string
27
+ bottomRight: string
28
+ horizontal: string
29
+ }
30
+
31
+ export const DIAGRAM_ARROW_HEADS = new Set(["▶", "◀", "▼", "▲", "→", "←", "↓", "↑"])
32
+ const HEAVY_LINE_GLYPHS = new Set(Object.values(BorderChars.heavy))
33
+
34
+ export const DIAGRAM_DIAMOND_CHARS = {
35
+ topLeft: "╭",
36
+ topRight: "╮",
37
+ upperLeft: "╭",
38
+ upperLeftJoin: "╯",
39
+ upperRightJoin: "╰",
40
+ upperRight: "╮",
41
+ vertical: "│",
42
+ lowerLeft: "╰",
43
+ lowerLeftJoin: "╮",
44
+ lowerRightJoin: "╭",
45
+ lowerRight: "╯",
46
+ bottomLeft: "╰",
47
+ bottomRight: "╯",
48
+ horizontal: "─",
49
+ } as const satisfies DiagramDiamondCharacters
50
+
51
+ export function diagramDiamondCharactersFromBorder(chars: BorderCharacters): DiagramDiamondCharacters {
52
+ return {
53
+ topLeft: chars.topLeft,
54
+ topRight: chars.topRight,
55
+ upperLeft: chars.topLeft,
56
+ upperLeftJoin: chars.bottomRight,
57
+ upperRightJoin: chars.bottomLeft,
58
+ upperRight: chars.topRight,
59
+ vertical: chars.vertical,
60
+ lowerLeft: chars.bottomLeft,
61
+ lowerLeftJoin: chars.topRight,
62
+ lowerRightJoin: chars.topLeft,
63
+ lowerRight: chars.bottomRight,
64
+ bottomLeft: chars.bottomLeft,
65
+ bottomRight: chars.bottomRight,
66
+ horizontal: chars.horizontal,
67
+ }
68
+ }
69
+
70
+ function lineDirections(char: string): readonly DiagramDirection[] | undefined {
71
+ switch (char) {
72
+ case "─":
73
+ return ["left", "right"]
74
+ case "│":
75
+ return ["up", "down"]
76
+ case "━":
77
+ return ["left", "right"]
78
+ case "┃":
79
+ return ["up", "down"]
80
+ case "┌":
81
+ case "╭":
82
+ case "┏":
83
+ return ["right", "down"]
84
+ case "┐":
85
+ case "╮":
86
+ case "┓":
87
+ return ["left", "down"]
88
+ case "└":
89
+ case "╰":
90
+ case "┗":
91
+ return ["up", "right"]
92
+ case "┘":
93
+ case "╯":
94
+ case "┛":
95
+ return ["up", "left"]
96
+ case "├":
97
+ case "┣":
98
+ return ["up", "down", "right"]
99
+ case "┤":
100
+ case "┫":
101
+ return ["up", "down", "left"]
102
+ case "┬":
103
+ case "┳":
104
+ return ["left", "right", "down"]
105
+ case "┴":
106
+ case "┻":
107
+ return ["left", "right", "up"]
108
+ case "┼":
109
+ case "╋":
110
+ return ["up", "down", "left", "right"]
111
+ default:
112
+ return undefined
113
+ }
114
+ }
115
+
116
+ export function diagramLineGlyph(
117
+ directions: ReadonlySet<DiagramDirection>,
118
+ cornerStyle: DiagramLineCornerStyle = "square",
119
+ lineStyle: DiagramLineStyle = "single",
120
+ ): string {
121
+ const up = directions.has("up")
122
+ const down = directions.has("down")
123
+ const left = directions.has("left")
124
+ const right = directions.has("right")
125
+ if (lineStyle === "heavy") {
126
+ const chars = BorderChars.heavy
127
+ if (up && down && left && right) return chars.cross
128
+ if (up && down && right) return chars.leftT
129
+ if (up && down && left) return chars.rightT
130
+ if (left && right && down) return chars.topT
131
+ if (left && right && up) return chars.bottomT
132
+ if (up && right) return chars.bottomLeft
133
+ if (up && left) return chars.bottomRight
134
+ if (down && right) return chars.topLeft
135
+ if (down && left) return chars.topRight
136
+ if (up || down) return chars.vertical
137
+ return chars.horizontal
138
+ }
139
+ if (up && down && left && right) return "┼"
140
+ if (up && down && right) return "├"
141
+ if (up && down && left) return "┤"
142
+ if (left && right && down) return "┬"
143
+ if (left && right && up) return "┴"
144
+ if (up && right) return cornerStyle === "rounded" ? "╰" : "└"
145
+ if (up && left) return cornerStyle === "rounded" ? "╯" : "┘"
146
+ if (down && right) return cornerStyle === "rounded" ? "╭" : "┌"
147
+ if (down && left) return cornerStyle === "rounded" ? "╮" : "┐"
148
+ if (up || down) return "│"
149
+ return "─"
150
+ }
151
+
152
+ function isHeavyLineGlyph(char: string): boolean {
153
+ return HEAVY_LINE_GLYPHS.has(char)
154
+ }
155
+
156
+ function segmentGlyph(direction: DiagramDirection, lineStyle: DiagramLineStyle | undefined): string {
157
+ const directions = new Set<DiagramDirection>(
158
+ direction === "left" || direction === "right" ? ["left", "right"] : ["up", "down"],
159
+ )
160
+ return diagramLineGlyph(directions, "square", lineStyle === "heavy" ? "heavy" : "single")
161
+ }
162
+
163
+ export function mergeDiagramLineGlyph(
164
+ existing: string,
165
+ incoming: string,
166
+ cornerStyle: DiagramLineCornerStyle = "square",
167
+ ): string | undefined {
168
+ const existingDirections = lineDirections(existing)
169
+ const incomingDirections = lineDirections(incoming)
170
+ if (!existingDirections || !incomingDirections) return undefined
171
+
172
+ return diagramLineGlyph(
173
+ new Set([...existingDirections, ...incomingDirections]),
174
+ cornerStyle,
175
+ isHeavyLineGlyph(existing) && isHeavyLineGlyph(incoming) ? "heavy" : "single",
176
+ )
177
+ }
178
+
179
+ export function diagramArrowHead(direction: DiagramDirection, style: DiagramArrowHeadStyle = "filled"): string {
180
+ if (style === "line") {
181
+ if (direction === "right") return "→"
182
+ if (direction === "left") return "←"
183
+ if (direction === "up") return "↑"
184
+ return "↓"
185
+ }
186
+
187
+ if (direction === "right") return "▶"
188
+ if (direction === "left") return "◀"
189
+ if (direction === "up") return "▲"
190
+ return "▼"
191
+ }
192
+
193
+ export function diagramArrowHeadBetween(
194
+ from: DiagramPoint,
195
+ to: DiagramPoint,
196
+ style: DiagramArrowHeadStyle = "filled",
197
+ ): string {
198
+ const direction = directionBetween(from, to)
199
+ return direction ? diagramArrowHead(direction, style) : diagramArrowHead("right", style)
200
+ }
201
+
202
+ export function drawDiagramFrame(
203
+ bounds: DiagramBounds,
204
+ chars: BorderCharacters,
205
+ setCell: (x: number, y: number, char: string) => void,
206
+ ): void {
207
+ setCell(bounds.left, bounds.top, chars.topLeft)
208
+ setCell(bounds.left + bounds.width - 1, bounds.top, chars.topRight)
209
+ setCell(bounds.left, bounds.top + bounds.height - 1, chars.bottomLeft)
210
+ setCell(bounds.left + bounds.width - 1, bounds.top + bounds.height - 1, chars.bottomRight)
211
+ for (let x = bounds.left + 1; x < bounds.left + bounds.width - 1; x++) {
212
+ setCell(x, bounds.top, chars.horizontal)
213
+ setCell(x, bounds.top + bounds.height - 1, chars.horizontal)
214
+ }
215
+ for (let y = bounds.top + 1; y < bounds.top + bounds.height - 1; y++) {
216
+ setCell(bounds.left, y, chars.vertical)
217
+ setCell(bounds.left + bounds.width - 1, y, chars.vertical)
218
+ }
219
+ }
220
+
221
+ export function fillDiagramFrameInterior(bounds: DiagramBounds, setCell: (x: number, y: number) => void): void {
222
+ for (let y = bounds.top + 1; y < bounds.top + bounds.height - 1; y++) {
223
+ for (let x = bounds.left + 1; x < bounds.left + bounds.width - 1; x++) setCell(x, y)
224
+ }
225
+ }
226
+
227
+ export function drawDiagramDiamond(
228
+ bounds: DiagramBounds,
229
+ setCell: (x: number, y: number, char: string) => void,
230
+ chars: DiagramDiamondCharacters = DIAGRAM_DIAMOND_CHARS,
231
+ ): void {
232
+ const left = bounds.left
233
+ const right = bounds.left + bounds.width - 1
234
+ const top = bounds.top
235
+ const bottom = bounds.top + bounds.height - 1
236
+ const capInset = Math.min(2, Math.max(1, Math.floor((bounds.width - 1) / 2)))
237
+ const capLeft = left + capInset
238
+ const capRight = right - capInset
239
+
240
+ setCell(capLeft, top, chars.topLeft)
241
+ for (let x = capLeft + 1; x < capRight; x++) setCell(x, top, chars.horizontal)
242
+ setCell(capRight, top, chars.topRight)
243
+
244
+ setCell(left, top + 1, chars.upperLeft)
245
+ for (let x = left + 1; x < capLeft; x++) setCell(x, top + 1, chars.horizontal)
246
+ setCell(capLeft, top + 1, chars.upperLeftJoin)
247
+ setCell(capRight, top + 1, chars.upperRightJoin)
248
+ for (let x = capRight + 1; x < right; x++) setCell(x, top + 1, chars.horizontal)
249
+ setCell(right, top + 1, chars.upperRight)
250
+
251
+ for (let y = top + 2; y < bottom - 1; y++) {
252
+ setCell(left, y, chars.vertical)
253
+ setCell(right, y, chars.vertical)
254
+ }
255
+
256
+ setCell(left, bottom - 1, chars.lowerLeft)
257
+ for (let x = left + 1; x < capLeft; x++) setCell(x, bottom - 1, chars.horizontal)
258
+ setCell(capLeft, bottom - 1, chars.lowerLeftJoin)
259
+ setCell(capRight, bottom - 1, chars.lowerRightJoin)
260
+ for (let x = capRight + 1; x < right; x++) setCell(x, bottom - 1, chars.horizontal)
261
+ setCell(right, bottom - 1, chars.lowerRight)
262
+
263
+ setCell(capLeft, bottom, chars.bottomLeft)
264
+ for (let x = capLeft + 1; x < capRight; x++) setCell(x, bottom, chars.horizontal)
265
+ setCell(capRight, bottom, chars.bottomRight)
266
+ }
267
+
268
+ export function drawOrthogonalPath(
269
+ points: readonly DiagramPoint[],
270
+ setCell: (x: number, y: number, char: string) => void,
271
+ options: { cornerStyle?: DiagramLineCornerStyle; lineStyle?: DiagramLineStyle } = {},
272
+ ): void {
273
+ for (let index = 1; index < points.length; index++) {
274
+ const from = points[index - 1]!
275
+ const to = points[index]!
276
+ const direction = directionBetween(from, to)
277
+ if (!direction) continue
278
+ const glyph = segmentGlyph(direction, options.lineStyle)
279
+ const horizontal = direction === "left" || direction === "right"
280
+ // Terminal cells are about twice as tall as they are wide. A two-cell
281
+ // horizontal dash matches the visual length of a one-row vertical dash,
282
+ // and the same ratio keeps the gaps balanced.
283
+ const dashOn = horizontal ? 2 : 1
284
+ const dashPeriod = horizontal ? 4 : 2
285
+ let step = index === 1 ? 0 : 1
286
+ walkOrthogonalSegment(from, to, index === 1, (point) => {
287
+ if (options.lineStyle !== "dashed" || step % dashPeriod < dashOn) setCell(point.x, point.y, glyph)
288
+ step += 1
289
+ })
290
+ }
291
+
292
+ for (let index = 1; index < points.length - 1; index++) {
293
+ const previous = points[index - 1]!
294
+ const current = points[index]!
295
+ const next = points[index + 1]!
296
+ const fromDirection = directionBetween(current, previous)
297
+ const toDirection = directionBetween(current, next)
298
+ const directions = new Set<DiagramDirection>()
299
+ if (fromDirection) directions.add(fromDirection)
300
+ if (toDirection) directions.add(toDirection)
301
+ setCell(
302
+ current.x,
303
+ current.y,
304
+ diagramLineGlyph(directions, options.cornerStyle, options.lineStyle === "heavy" ? "heavy" : "single"),
305
+ )
306
+ }
307
+ }
@@ -0,0 +1,347 @@
1
+ export type DiagramAxis = "x" | "y"
2
+ export type DiagramDirection = "up" | "down" | "left" | "right"
3
+ export type DiagramSide = "left" | "right" | "top" | "bottom"
4
+
5
+ export interface DiagramPoint {
6
+ x: number
7
+ y: number
8
+ }
9
+
10
+ export interface DiagramBounds {
11
+ left: number
12
+ top: number
13
+ width: number
14
+ height: number
15
+ centerX: number
16
+ centerY: number
17
+ }
18
+
19
+ export interface DiagramSegment {
20
+ from: DiagramPoint
21
+ to: DiagramPoint
22
+ axis: DiagramAxis
23
+ direction: DiagramDirection
24
+ length: number
25
+ }
26
+
27
+ export interface DiagramLane {
28
+ axis: DiagramAxis
29
+ coordinate: number
30
+ }
31
+
32
+ export interface DiagramSpan {
33
+ start: number
34
+ end: number
35
+ }
36
+
37
+ const DIRECTION_AXIS = {
38
+ left: "x",
39
+ right: "x",
40
+ up: "y",
41
+ down: "y",
42
+ } as const satisfies Record<DiagramDirection, DiagramAxis>
43
+
44
+ const DIRECTION_SIGN = {
45
+ left: -1,
46
+ right: 1,
47
+ up: -1,
48
+ down: 1,
49
+ } as const satisfies Record<DiagramDirection, -1 | 1>
50
+
51
+ const DIRECTION_SIDE = {
52
+ left: "left",
53
+ right: "right",
54
+ up: "top",
55
+ down: "bottom",
56
+ } as const satisfies Record<DiagramDirection, DiagramSide>
57
+
58
+ const OPPOSITE_SIDE = {
59
+ left: "right",
60
+ right: "left",
61
+ top: "bottom",
62
+ bottom: "top",
63
+ } as const satisfies Record<DiagramSide, DiagramSide>
64
+
65
+ export function point(x: number, y: number): DiagramPoint {
66
+ return { x, y }
67
+ }
68
+
69
+ export function diagramBoundsFromRect(left: number, top: number, width: number, height: number): DiagramBounds {
70
+ return {
71
+ left,
72
+ top,
73
+ width,
74
+ height,
75
+ centerX: left + Math.floor(width / 2),
76
+ centerY: top + Math.floor(height / 2),
77
+ }
78
+ }
79
+
80
+ export function translateDiagramBounds(bounds: DiagramBounds, dx: number, dy: number): void {
81
+ bounds.left += dx
82
+ bounds.top += dy
83
+ bounds.centerX += dx
84
+ bounds.centerY += dy
85
+ }
86
+
87
+ export function diagramBoundsFromBounds(bounds: readonly DiagramBounds[]): DiagramBounds | undefined {
88
+ if (bounds.length === 0) return undefined
89
+ const left = Math.min(...bounds.map((bound) => bound.left))
90
+ const top = Math.min(...bounds.map((bound) => bound.top))
91
+ const right = Math.max(...bounds.map((bound) => bound.left + bound.width))
92
+ const bottom = Math.max(...bounds.map((bound) => bound.top + bound.height))
93
+ return diagramBoundsFromRect(left, top, right - left, bottom - top)
94
+ }
95
+
96
+ export function diagramBoundsFromPoints(points: readonly DiagramPoint[]): DiagramBounds | undefined {
97
+ if (points.length === 0) return undefined
98
+ const left = Math.min(...points.map((point) => point.x))
99
+ const top = Math.min(...points.map((point) => point.y))
100
+ const right = Math.max(...points.map((point) => point.x))
101
+ const bottom = Math.max(...points.map((point) => point.y))
102
+ return diagramBoundsFromRect(left, top, right - left + 1, bottom - top + 1)
103
+ }
104
+
105
+ export function coordinate(point: DiagramPoint, axis: DiagramAxis): number {
106
+ return point[axis]
107
+ }
108
+
109
+ export function withCoordinate(point: DiagramPoint, axis: DiagramAxis, value: number): DiagramPoint {
110
+ return axis === "x" ? { x: value, y: point.y } : { x: point.x, y: value }
111
+ }
112
+
113
+ export function shiftPoint(point: DiagramPoint, direction: DiagramDirection, distance = 1): DiagramPoint {
114
+ switch (direction) {
115
+ case "left":
116
+ return { x: point.x - distance, y: point.y }
117
+ case "right":
118
+ return { x: point.x + distance, y: point.y }
119
+ case "up":
120
+ return { x: point.x, y: point.y - distance }
121
+ case "down":
122
+ return { x: point.x, y: point.y + distance }
123
+ }
124
+ }
125
+
126
+ export function clampPoint(point: DiagramPoint, min: DiagramPoint = { x: 0, y: 0 }): DiagramPoint {
127
+ return { x: Math.max(min.x, point.x), y: Math.max(min.y, point.y) }
128
+ }
129
+
130
+ export function samePoint(left: DiagramPoint, right: DiagramPoint): boolean {
131
+ return left.x === right.x && left.y === right.y
132
+ }
133
+
134
+ export function directionAxis(direction: DiagramDirection): DiagramAxis {
135
+ return DIRECTION_AXIS[direction]
136
+ }
137
+
138
+ export function directionSign(direction: DiagramDirection): -1 | 1 {
139
+ return DIRECTION_SIGN[direction]
140
+ }
141
+
142
+ export function sideForDirection(direction: DiagramDirection): DiagramSide {
143
+ return DIRECTION_SIDE[direction]
144
+ }
145
+
146
+ export function oppositeSide(side: DiagramSide): DiagramSide {
147
+ return OPPOSITE_SIDE[side]
148
+ }
149
+
150
+ export function directionBetween(from: DiagramPoint, to: DiagramPoint): DiagramDirection | undefined {
151
+ if (from.y === to.y) {
152
+ if (to.x > from.x) return "right"
153
+ if (to.x < from.x) return "left"
154
+ }
155
+ if (from.x === to.x) {
156
+ if (to.y > from.y) return "down"
157
+ if (to.y < from.y) return "up"
158
+ }
159
+ return undefined
160
+ }
161
+
162
+ export function boundsCenter(bounds: DiagramBounds): DiagramPoint {
163
+ return point(bounds.centerX, bounds.centerY)
164
+ }
165
+
166
+ export function boundsSidePoint(
167
+ bounds: DiagramBounds,
168
+ side: DiagramSide,
169
+ surface: "border" | "outside" = "outside",
170
+ ): DiagramPoint {
171
+ switch (side) {
172
+ case "left":
173
+ return point(bounds.left - (surface === "outside" ? 1 : 0), bounds.centerY)
174
+ case "right":
175
+ return point(bounds.left + bounds.width - (surface === "outside" ? 0 : 1), bounds.centerY)
176
+ case "top":
177
+ return point(bounds.centerX, bounds.top - (surface === "outside" ? 1 : 0))
178
+ case "bottom":
179
+ return point(bounds.centerX, bounds.top + bounds.height - (surface === "outside" ? 0 : 1))
180
+ }
181
+ }
182
+
183
+ export function centerCoordinate(bounds: DiagramBounds, axis: DiagramAxis): number {
184
+ return axis === "x" ? bounds.centerX : bounds.centerY
185
+ }
186
+
187
+ export function snapCoordinate(source: number, target: number, tolerance: number): number {
188
+ return Math.abs(source - target) <= tolerance ? target : source
189
+ }
190
+
191
+ export function pathThrough(points: readonly DiagramPoint[]): DiagramPoint[] {
192
+ const path: DiagramPoint[] = []
193
+ for (const next of points) {
194
+ if (!path.length || !samePoint(path[path.length - 1]!, next)) path.push(point(next.x, next.y))
195
+ }
196
+ return path
197
+ }
198
+
199
+ export function lane(axis: DiagramAxis, coordinate: number): DiagramLane {
200
+ return { axis, coordinate }
201
+ }
202
+
203
+ export function pathViaLane(start: DiagramPoint, routeLane: DiagramLane, end: DiagramPoint): DiagramPoint[] {
204
+ return pathThrough([
205
+ start,
206
+ withCoordinate(start, routeLane.axis, routeLane.coordinate),
207
+ withCoordinate(end, routeLane.axis, routeLane.coordinate),
208
+ end,
209
+ ])
210
+ }
211
+
212
+ function dominantAxis(start: DiagramPoint, end: DiagramPoint): DiagramAxis {
213
+ return Math.abs(end.x - start.x) >= Math.abs(end.y - start.y) ? "x" : "y"
214
+ }
215
+
216
+ function terminalLane(start: DiagramPoint, end: DiagramPoint, axis: DiagramAxis, terminalClearance: number): number {
217
+ const startCoordinate = coordinate(start, axis)
218
+ const endCoordinate = coordinate(end, axis)
219
+ const delta = endCoordinate - startCoordinate
220
+ const sign = Math.sign(delta)
221
+ if (sign === 0) return endCoordinate
222
+ return endCoordinate - sign * Math.min(terminalClearance, Math.max(1, Math.abs(delta) - 1))
223
+ }
224
+
225
+ export function orthogonalPath(
226
+ start: DiagramPoint,
227
+ end: DiagramPoint,
228
+ options: { preferredAxis?: DiagramAxis; terminalClearance?: number } = {},
229
+ ): DiagramPoint[] {
230
+ if (start.x === end.x || start.y === end.y) return pathThrough([start, end])
231
+
232
+ const laneAxis = options.preferredAxis ?? dominantAxis(start, end)
233
+ return pathViaLane(start, lane(laneAxis, terminalLane(start, end, laneAxis, options.terminalClearance ?? 4)), end)
234
+ }
235
+
236
+ export function segmentBetween(from: DiagramPoint, to: DiagramPoint): DiagramSegment | undefined {
237
+ const direction = directionBetween(from, to)
238
+ if (!direction) return undefined
239
+ return {
240
+ from,
241
+ to,
242
+ axis: directionAxis(direction),
243
+ direction,
244
+ length: Math.abs(coordinate(to, directionAxis(direction)) - coordinate(from, directionAxis(direction))),
245
+ }
246
+ }
247
+
248
+ export function walkOrthogonalSegment(
249
+ from: DiagramPoint,
250
+ to: DiagramPoint,
251
+ includeStart: boolean,
252
+ visit: (point: DiagramPoint) => boolean | void,
253
+ ): void {
254
+ const direction = directionBetween(from, to)
255
+ if (!direction) return
256
+
257
+ const dx = direction === "right" ? 1 : direction === "left" ? -1 : 0
258
+ const dy = direction === "down" ? 1 : direction === "up" ? -1 : 0
259
+ let cursor = includeStart ? from : point(from.x + dx, from.y + dy)
260
+
261
+ while (!samePoint(cursor, to)) {
262
+ if (visit(cursor) === false) return
263
+ cursor = point(cursor.x + dx, cursor.y + dy)
264
+ }
265
+ }
266
+
267
+ export function orthogonalPathPoints(points: readonly DiagramPoint[]): DiagramPoint[] {
268
+ const path: DiagramPoint[] = []
269
+ for (let index = 1; index < points.length; index++) {
270
+ const from = points[index - 1]!
271
+ const to = points[index]!
272
+ const direction = directionBetween(from, to)
273
+ if (!direction) continue
274
+
275
+ const dx = direction === "right" ? 1 : direction === "left" ? -1 : 0
276
+ const dy = direction === "down" ? 1 : direction === "up" ? -1 : 0
277
+ let cursor = path.length === 0 ? point(from.x, from.y) : point(from.x + dx, from.y + dy)
278
+ while (true) {
279
+ path.push(cursor)
280
+ if (samePoint(cursor, to)) break
281
+ cursor = point(cursor.x + dx, cursor.y + dy)
282
+ }
283
+ }
284
+ return path
285
+ }
286
+
287
+ export function orderedSpan(left: number, right: number): DiagramSpan {
288
+ return left <= right ? { start: left, end: right } : { start: right, end: left }
289
+ }
290
+
291
+ export function segmentSpan(segment: DiagramSegment): DiagramSpan {
292
+ return orderedSpan(coordinate(segment.from, segment.axis), coordinate(segment.to, segment.axis))
293
+ }
294
+
295
+ export function pointOnSegment(segment: DiagramSegment, coordinateValue: number): DiagramPoint {
296
+ return withCoordinate(segment.from, segment.axis, coordinateValue)
297
+ }
298
+
299
+ export function insetSpan(span: DiagramSpan, amount: number): DiagramSpan {
300
+ return { start: span.start + amount, end: span.end - amount }
301
+ }
302
+
303
+ export function spanCapacity(span: DiagramSpan): number {
304
+ return Math.max(0, span.end - span.start + 1)
305
+ }
306
+
307
+ export function centeredSpanStart(span: DiagramSpan, width: number): number {
308
+ return span.start + Math.floor((spanCapacity(span) - width) / 2)
309
+ }
310
+
311
+ export function midpoint(span: DiagramSpan): number {
312
+ return Math.round((span.start + span.end) / 2)
313
+ }
314
+
315
+ export function advanceCoordinate(origin: number, direction: DiagramDirection, distance: number): number {
316
+ return origin + directionSign(direction) * distance
317
+ }
318
+
319
+ export function beforeNearestCoordinate(
320
+ points: readonly DiagramPoint[],
321
+ axis: DiagramAxis,
322
+ direction: DiagramDirection,
323
+ clearance: number,
324
+ ): number {
325
+ const coordinates = points.map((point) => coordinate(point, axis))
326
+ const nearest = directionSign(direction) > 0 ? Math.min(...coordinates) : Math.max(...coordinates)
327
+ return advanceCoordinate(nearest, direction, -clearance)
328
+ }
329
+
330
+ export function afterFarthestCoordinate(
331
+ points: readonly DiagramPoint[],
332
+ axis: DiagramAxis,
333
+ direction: DiagramDirection,
334
+ clearance: number,
335
+ ): number {
336
+ const coordinates = points.map((point) => coordinate(point, axis))
337
+ const farthest = directionSign(direction) > 0 ? Math.max(...coordinates) : Math.min(...coordinates)
338
+ return advanceCoordinate(farthest, direction, clearance)
339
+ }
340
+
341
+ export function keepBefore(preferred: number, boundary: number, direction: DiagramDirection): number {
342
+ return directionSign(direction) > 0 ? Math.min(preferred, boundary) : Math.max(preferred, boundary)
343
+ }
344
+
345
+ export function keepAfter(preferred: number, boundary: number, direction: DiagramDirection): number {
346
+ return directionSign(direction) > 0 ? Math.max(preferred, boundary) : Math.min(preferred, boundary)
347
+ }
@@ -0,0 +1,38 @@
1
+ export interface NumberedMermaidLine {
2
+ readonly lineNumber: number
3
+ readonly text: string
4
+ }
5
+
6
+ export function* numberedMermaidLines(content: string): Generator<NumberedMermaidLine> {
7
+ const lines = content.split(/\r?\n/)
8
+ for (let index = 0; index < lines.length; index += 1) {
9
+ yield { lineNumber: index + 1, text: lines[index]!.trim() }
10
+ }
11
+ }
12
+
13
+ export function* meaningfulNumberedMermaidLines(content: string): Generator<NumberedMermaidLine> {
14
+ for (const line of numberedMermaidLines(content)) {
15
+ if (line.text && !line.text.startsWith("%%")) yield line
16
+ }
17
+ }
18
+
19
+ export function firstMeaningfulMermaidLine(content: string): string | undefined {
20
+ for (const rawLine of content.split(/\r?\n/)) {
21
+ const line = rawLine.trim()
22
+ if (line && !line.startsWith("%%")) return line
23
+ }
24
+ return undefined
25
+ }
26
+
27
+ export function stripMermaidQuotes(value: string): string {
28
+ const trimmed = value.trim()
29
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
30
+ return decodeMermaidText(trimmed.slice(1, -1))
31
+ }
32
+ return decodeMermaidText(trimmed)
33
+ }
34
+
35
+ export function decodeMermaidText(value: string): string {
36
+ return decodeHTMLStrict(value)
37
+ }
38
+ import { decodeHTMLStrict } from "entities"