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,433 @@
1
+ import { BorderChars, TextAttributes, type BorderCharacters, type BorderStyle } from "@opentui/core"
2
+ import { walkOrthogonalSegment, type DiagramDirection } from "../core/geometry.js"
3
+ import { DiagramCanvas, type DiagramCanvasCell } from "../core/canvas.js"
4
+ import { DIAGRAM_LABEL_PADDING_X, parseDiagramTextLines } from "../core/text.js"
5
+ import type { DiagramTextRun } from "../core/text-lines.js"
6
+ import {
7
+ DIAGRAM_ARROW_HEADS,
8
+ diagramArrowHeadBetween,
9
+ diagramDiamondCharactersFromBorder,
10
+ diagramLineGlyph,
11
+ drawDiagramDiamond,
12
+ drawDiagramFrame,
13
+ fillDiagramFrameInterior,
14
+ drawOrthogonalPath,
15
+ mergeDiagramLineGlyph,
16
+ } from "../core/drawing.js"
17
+ import { layoutFlowchartDiagram, visualLength } from "./layout.js"
18
+ import { flowchartRouteLabelLayout } from "./labels.js"
19
+ import type { FlowchartDiagramRenderOptions } from "./options.js"
20
+ import { flowchartDirectionBetween, flowchartSourceConnector } from "./routing.js"
21
+ import {
22
+ DATABASE_EDGE_FADE_STYLES,
23
+ NODE_EDGE_FADE_STYLES,
24
+ type FlowchartCellStyle,
25
+ type FlowchartCellMetadata,
26
+ type FlowchartEdgeFadeStyle,
27
+ type FlowchartGrid,
28
+ } from "./style.js"
29
+ import type {
30
+ FlowchartDiagram,
31
+ FlowchartEdgeRoute,
32
+ FlowchartNode,
33
+ FlowchartNodeBounds,
34
+ FlowchartPoint,
35
+ FlowchartSubgraphBounds,
36
+ } from "./types.js"
37
+
38
+ export const DEFAULT_BORDER_STYLE = "rounded" satisfies BorderStyle
39
+ function mergeFlowchartCell(
40
+ existing: DiagramCanvasCell<FlowchartCellStyle>,
41
+ incoming: DiagramCanvasCell<FlowchartCellStyle>,
42
+ ): DiagramCanvasCell<FlowchartCellStyle> {
43
+ if (incoming.style !== "edge") return incoming
44
+ if (existing.style === "label") return existing
45
+ if (incoming.char === " ") return existing
46
+ if (existing.style !== "edge" || existing.char === " ") return incoming
47
+ if (DIAGRAM_ARROW_HEADS.has(existing.char)) return existing
48
+ if (DIAGRAM_ARROW_HEADS.has(incoming.char)) return incoming
49
+
50
+ return {
51
+ ...incoming,
52
+ char: mergeDiagramLineGlyph(existing.char, incoming.char, "rounded") ?? incoming.char,
53
+ } as DiagramCanvasCell<FlowchartCellStyle>
54
+ }
55
+
56
+ function setRichText(
57
+ grid: FlowchartGrid,
58
+ x: number,
59
+ y: number,
60
+ runs: readonly DiagramTextRun[],
61
+ style: FlowchartCellStyle,
62
+ ): number {
63
+ let offset = 0
64
+ for (const run of runs) {
65
+ grid.setText(x + offset, y, run.text, style, run.italic ? { attributes: TextAttributes.ITALIC } : undefined)
66
+ offset += visualLength(run.text)
67
+ }
68
+ return offset
69
+ }
70
+
71
+ function drawNode(
72
+ grid: FlowchartGrid,
73
+ node: FlowchartNode,
74
+ bounds: FlowchartNodeBounds,
75
+ borderStyle: BorderStyle,
76
+ ): void {
77
+ const chars = BorderChars[borderStyle]
78
+ const style: FlowchartCellStyle = node.shape === "database" ? "database" : "node"
79
+ const border: FlowchartCellStyle = node.shape === "database" ? "databaseBorder" : "nodeBorder"
80
+
81
+ if (node.shape === "decision") {
82
+ drawDiagramDiamond(
83
+ bounds,
84
+ (x, y, char) => grid.setCell(x, y, char, border),
85
+ diagramDiamondCharactersFromBorder(chars),
86
+ )
87
+ } else if (node.shape === "circle") {
88
+ fillDiagramFrameInterior(bounds, (x, y) => grid.setCell(x, y, " ", style))
89
+ drawCircleNode(grid, bounds, chars, border)
90
+ } else if (node.shape === "hexagon") {
91
+ fillDiagramFrameInterior(bounds, (x, y) => grid.setCell(x, y, " ", style))
92
+ drawHexagonNode(grid, bounds, chars, border)
93
+ } else if (node.shape === "parallelogram") {
94
+ fillDiagramFrameInterior(bounds, (x, y) => grid.setCell(x, y, " ", style))
95
+ drawParallelogramNode(grid, bounds, chars, border)
96
+ } else if (node.shape === "subroutine") {
97
+ fillDiagramFrameInterior(bounds, (x, y) => grid.setCell(x, y, " ", style))
98
+ drawSubroutineNode(grid, bounds, chars, border)
99
+ } else if (node.shape === "database") {
100
+ fillDiagramFrameInterior(bounds, (x, y) => grid.setCell(x, y, " ", style))
101
+ drawDatabaseNode(grid, bounds, chars, border)
102
+ } else if (node.shape === "rounded") {
103
+ fillDiagramFrameInterior(bounds, (x, y) => grid.setCell(x, y, " ", style))
104
+ drawRoundedNode(grid, bounds, border)
105
+ } else {
106
+ fillDiagramFrameInterior(bounds, (x, y) => grid.setCell(x, y, " ", style))
107
+ drawDiagramFrame(bounds, chars, (x, y, char) => grid.setCell(x, y, char, border))
108
+ }
109
+
110
+ const textTop =
111
+ node.shape === "decision" || node.shape === "circle" || node.shape === "hexagon"
112
+ ? bounds.top + Math.floor((bounds.height - bounds.lines.length) / 2)
113
+ : node.shape === "database"
114
+ ? bounds.top + 2
115
+ : bounds.top + 1
116
+ const lines = parseDiagramTextLines(node.label)
117
+ for (const [index, line] of bounds.lines.entries()) {
118
+ const lineX =
119
+ node.shape === "subroutine"
120
+ ? bounds.left + 3
121
+ : bounds.left + Math.max(1, Math.floor((bounds.width - visualLength(line)) / 2))
122
+ setRichText(grid, lineX, textTop + index, lines[index]!.runs, style)
123
+ }
124
+ }
125
+
126
+ function drawRoundedNode(grid: FlowchartGrid, bounds: FlowchartNodeBounds, style: FlowchartCellStyle): void {
127
+ const { left, top, width, height } = bounds
128
+ const right = left + width - 1
129
+ const bottom = top + height - 1
130
+ const chars = BorderChars.rounded
131
+ // Inset the caps to distinguish the pill from an ordinary rounded frame.
132
+ for (const y of [top, bottom]) {
133
+ grid.setCell(left + 1, y, y === top ? chars.topLeft : chars.bottomLeft, style)
134
+ grid.setCell(right - 1, y, y === top ? chars.topRight : chars.bottomRight, style)
135
+ for (let x = left + 2; x < right - 1; x++) grid.setCell(x, y, chars.horizontal, style)
136
+ }
137
+ for (let y = top + 1; y < bottom; y++) {
138
+ grid.setCell(left, y, chars.vertical, style)
139
+ grid.setCell(right, y, chars.vertical, style)
140
+ }
141
+ }
142
+
143
+ function drawCircleNode(
144
+ grid: FlowchartGrid,
145
+ bounds: FlowchartNodeBounds,
146
+ chars: BorderCharacters,
147
+ style: FlowchartCellStyle,
148
+ ): void {
149
+ const right = bounds.left + bounds.width - 1
150
+ const bottom = bounds.top + bounds.height - 1
151
+ // Inset the caps while keeping the sides on the routing bounds.
152
+ for (const y of [bounds.top, bottom]) {
153
+ grid.setCell(bounds.left + 1, y, y === bounds.top ? chars.topLeft : chars.bottomLeft, style)
154
+ grid.setCell(right - 1, y, y === bounds.top ? chars.topRight : chars.bottomRight, style)
155
+ for (let x = bounds.left + 2; x < right - 1; x++) grid.setCell(x, y, chars.horizontal, style)
156
+ }
157
+ for (let y = bounds.top + 1; y < bottom; y++) {
158
+ grid.setCell(bounds.left, y, y === bounds.top + 1 ? "╱" : y === bottom - 1 ? "╲" : chars.vertical, style)
159
+ grid.setCell(right, y, y === bounds.top + 1 ? "╲" : y === bottom - 1 ? "╱" : chars.vertical, style)
160
+ }
161
+ }
162
+
163
+ function drawHexagonNode(
164
+ grid: FlowchartGrid,
165
+ bounds: FlowchartNodeBounds,
166
+ chars: BorderCharacters,
167
+ style: FlowchartCellStyle,
168
+ ): void {
169
+ const right = bounds.left + bounds.width - 1
170
+ const bottom = bounds.top + bounds.height - 1
171
+ // One bevel at each end and straight sides between them: extra slashes on
172
+ // the rows beside the caps make the outline zigzag instead of hexagonal.
173
+ for (const y of [bounds.top, bottom]) {
174
+ const top = y === bounds.top
175
+ grid.setCell(bounds.left + 1, y, top ? "╱" : "╲", style)
176
+ grid.setCell(right - 1, y, top ? "╲" : "╱", style)
177
+ for (let x = bounds.left + 2; x < right - 1; x++) grid.setCell(x, y, chars.horizontal, style)
178
+ }
179
+ for (let y = bounds.top + 1; y < bottom; y++) {
180
+ grid.setCell(bounds.left, y, chars.vertical, style)
181
+ grid.setCell(right, y, chars.vertical, style)
182
+ }
183
+ }
184
+
185
+ function drawParallelogramNode(
186
+ grid: FlowchartGrid,
187
+ bounds: FlowchartNodeBounds,
188
+ chars: BorderCharacters,
189
+ style: FlowchartCellStyle,
190
+ ): void {
191
+ const left = bounds.left
192
+ const right = bounds.left + bounds.width - 1
193
+ const top = bounds.top
194
+ const bottom = bounds.top + bounds.height - 1
195
+ const height = bottom - top
196
+ const slant = Math.max(1, Math.min(2, Math.floor((bounds.width - 3) / 4)))
197
+ for (let y = top; y <= bottom; y++) {
198
+ const progress = height === 0 ? 1 : (bottom - y) / height
199
+ const leftX = left + Math.round(slant * progress)
200
+ const rightX = right - Math.round(slant * (1 - progress))
201
+ grid.setCell(leftX, y, "╱", style)
202
+ grid.setCell(rightX, y, "╱", style)
203
+ if (y === top || y === bottom) {
204
+ for (let x = leftX + 1; x < rightX; x++) grid.setCell(x, y, chars.horizontal, style)
205
+ }
206
+ }
207
+ }
208
+
209
+ function drawSubroutineNode(
210
+ grid: FlowchartGrid,
211
+ bounds: FlowchartNodeBounds,
212
+ chars: BorderCharacters,
213
+ style: FlowchartCellStyle,
214
+ ): void {
215
+ drawDiagramFrame(bounds, chars, (x, y, char) => grid.setCell(x, y, char, style))
216
+ const leftRailX = bounds.left + 2
217
+ const rightRailX = bounds.left + bounds.width - 3
218
+ grid.setCell(leftRailX, bounds.top, chars.topT, style)
219
+ grid.setCell(rightRailX, bounds.top, chars.topT, style)
220
+ grid.setCell(leftRailX, bounds.top + bounds.height - 1, chars.bottomT, style)
221
+ grid.setCell(rightRailX, bounds.top + bounds.height - 1, chars.bottomT, style)
222
+ for (let y = bounds.top + 1; y < bounds.top + bounds.height - 1; y++) {
223
+ grid.setCell(leftRailX, y, chars.vertical, style)
224
+ grid.setCell(rightRailX, y, chars.vertical, style)
225
+ }
226
+ }
227
+
228
+ function drawDatabaseNode(
229
+ grid: FlowchartGrid,
230
+ bounds: FlowchartNodeBounds,
231
+ chars: BorderCharacters,
232
+ style: FlowchartCellStyle,
233
+ ): void {
234
+ drawDiagramFrame(bounds, chars, (x, y, char) => grid.setCell(x, y, char, style))
235
+ const topRailY = bounds.top + 1
236
+ const bottomRailY = bounds.top + bounds.height - 2
237
+ for (const y of [topRailY, bottomRailY]) {
238
+ grid.setCell(bounds.left, y, chars.leftT, style)
239
+ grid.setCell(bounds.left + bounds.width - 1, y, chars.rightT, style)
240
+ for (let x = bounds.left + 1; x < bounds.left + bounds.width - 1; x++) {
241
+ grid.setCell(x, y, chars.horizontal, style)
242
+ }
243
+ }
244
+ }
245
+
246
+ function drawSubgraphFrame(grid: FlowchartGrid, bounds: FlowchartSubgraphBounds, borderStyle: BorderStyle): void {
247
+ const chars = BorderChars[borderStyle]
248
+ drawDiagramFrame(bounds, chars, (x, y, char) => grid.setCell(x, y, char, "group"))
249
+ }
250
+
251
+ function drawSubgraphLabel(grid: FlowchartGrid, bounds: FlowchartSubgraphBounds): void {
252
+ if (bounds.label) {
253
+ const lines = parseDiagramTextLines(bounds.label)
254
+ const labelY = bounds.labelSide === "top" ? bounds.top : bounds.top + bounds.height - lines.length
255
+ for (const [index, line] of lines.entries()) {
256
+ grid.setText(bounds.left + 2, labelY + index, " ", "groupLabel")
257
+ const width = setRichText(grid, bounds.left + 3, labelY + index, line.runs, "groupLabel")
258
+ grid.setText(bounds.left + width + 3, labelY + index, " ", "groupLabel")
259
+ }
260
+ }
261
+ }
262
+
263
+ function drawEdgeLabel(grid: FlowchartGrid, route: FlowchartEdgeRoute, style: FlowchartCellStyle): void {
264
+ const label = flowchartRouteLabelLayout(route, visualLength)
265
+ const padding = " ".repeat(DIAGRAM_LABEL_PADDING_X)
266
+ for (const [index, line] of parseDiagramTextLines(route.edge.label).entries()) {
267
+ grid.setText(label.point.x, label.point.y + index, padding, style)
268
+ const width = setRichText(grid, label.point.x + DIAGRAM_LABEL_PADDING_X, label.point.y + index, line.runs, style)
269
+ grid.setText(label.point.x + width + DIAGRAM_LABEL_PADDING_X, label.point.y + index, padding, style)
270
+ }
271
+ }
272
+
273
+ function drawRoutedEdge(grid: FlowchartGrid, route: FlowchartEdgeRoute): void {
274
+ const { edge, points } = route
275
+ if (points.length < 2) return
276
+ const style: FlowchartCellStyle = "edge"
277
+
278
+ drawOrthogonalPath(points, (x, y, char) => grid.setCell(x, y, char, style), {
279
+ cornerStyle: "rounded",
280
+ lineStyle: edge.style === "thick" ? "heavy" : "single",
281
+ })
282
+ if (edge.arrowhead !== false) {
283
+ const end = points[points.length - 1]!
284
+ const arrowFrom = points[points.length - 2]!
285
+ grid.setCell(end.x, end.y, diagramArrowHeadBetween(arrowFrom, end), style)
286
+ } else {
287
+ const end = points[points.length - 1]!
288
+ const endDirection = flowchartDirectionBetween(points[points.length - 2]!, end)
289
+ if (endDirection) grid.setCell(end.x, end.y, diagramLineGlyph(new Set([endDirection])), style)
290
+ }
291
+ if (edge.label) {
292
+ drawEdgeLabel(grid, route, "label")
293
+ }
294
+ }
295
+
296
+ function flowchartNodeStyle(node: FlowchartNode | undefined): "node" | "database" {
297
+ return node?.shape === "database" ? "database" : "node"
298
+ }
299
+
300
+ function sourceFadeStyles(sourceStyle: "node" | "database"): readonly FlowchartEdgeFadeStyle[] {
301
+ return sourceStyle === "database" ? DATABASE_EDGE_FADE_STYLES : NODE_EDGE_FADE_STYLES
302
+ }
303
+
304
+ function styleExistingEdgeCell(grid: FlowchartGrid, x: number, y: number, style: FlowchartEdgeFadeStyle): void {
305
+ const cell = grid.getCell(x, y)
306
+ if (cell) grid.setCell(x, y, cell.char, style)
307
+ }
308
+
309
+ function routeCellOccupancy(routes: readonly FlowchartEdgeRoute[]): Map<string, number> {
310
+ const occupancy = new Map<string, number>()
311
+ for (const route of routes) {
312
+ const routeCells = new Set<string>()
313
+ for (let index = 1; index < route.points.length; index++) {
314
+ walkOrthogonalSegment(route.points[index - 1]!, route.points[index]!, index === 1, (point) => {
315
+ routeCells.add(`${point.x}:${point.y}`)
316
+ })
317
+ }
318
+ for (const key of routeCells) occupancy.set(key, (occupancy.get(key) ?? 0) + 1)
319
+ }
320
+ return occupancy
321
+ }
322
+
323
+ function fadeSourcePath(
324
+ grid: FlowchartGrid,
325
+ connector: FlowchartPoint,
326
+ points: FlowchartPoint[],
327
+ styles: readonly FlowchartEdgeFadeStyle[],
328
+ occupancy: ReadonlyMap<string, number>,
329
+ ): void {
330
+ const from = points[0]
331
+ const to = points[1]
332
+ if (!from || !to) return
333
+ const privateCells = [connector]
334
+
335
+ walkOrthogonalSegment(from, to, true, (point) => {
336
+ const key = `${point.x}:${point.y}`
337
+ const cell = grid.getCell(point.x, point.y)
338
+ if (occupancy.get(key) !== 1 || !cell || !"─━│┃".includes(cell.char) || cell.style === "label") return false
339
+ privateCells.push(point)
340
+ })
341
+
342
+ for (const [index, point] of privateCells.entries()) {
343
+ const styleIndex = Math.min(
344
+ styles.length - 1,
345
+ Math.floor(((index + 1) * styles.length) / (privateCells.length + 1)),
346
+ )
347
+ styleExistingEdgeCell(grid, point.x, point.y, styles[styleIndex]!)
348
+ }
349
+ }
350
+
351
+ function drawSourceConnectors(
352
+ grid: FlowchartGrid,
353
+ diagram: FlowchartDiagram,
354
+ bounds: Map<string, FlowchartNodeBounds>,
355
+ routes: readonly FlowchartEdgeRoute[],
356
+ ): void {
357
+ const nodesById = new Map(diagram.nodes.map((node) => [node.id, node]))
358
+ const occupancy = routeCellOccupancy(routes)
359
+ const sourceJunctions = new Map<string, { directions: Set<DiagramDirection>; heavy: boolean }>()
360
+ for (const route of routes) {
361
+ const sourcePoint = route.points[0]
362
+ const next = route.points[1]
363
+ if (!sourcePoint || !next) continue
364
+ const key = `${route.edge.from}:${sourcePoint.x}:${sourcePoint.y}`
365
+ const direction = flowchartDirectionBetween(sourcePoint, next)
366
+ const junction = sourceJunctions.get(key) ?? { directions: new Set<DiagramDirection>(), heavy: true }
367
+ if (direction) junction.directions.add(direction)
368
+ junction.heavy &&= route.edge.style === "thick"
369
+ sourceJunctions.set(key, junction)
370
+ }
371
+
372
+ for (const route of routes) {
373
+ const from = bounds.get(route.edge.from)
374
+ const sourcePoint = route.points[0]
375
+ if (!from || !sourcePoint) continue
376
+ const styles = sourceFadeStyles(flowchartNodeStyle(nodesById.get(route.edge.from)))
377
+ const connector = flowchartSourceConnector(from, sourcePoint)
378
+ grid.setCell(connector.x, connector.y, connector.char, "edge")
379
+ const connectorDirection = flowchartDirectionBetween(sourcePoint, connector)
380
+ if (connectorDirection) {
381
+ const cell = grid.getCell(sourcePoint.x, sourcePoint.y)
382
+ if (cell && cell.style !== "label" && !DIAGRAM_ARROW_HEADS.has(cell.char)) {
383
+ const key = `${route.edge.from}:${sourcePoint.x}:${sourcePoint.y}`
384
+ const junction = sourceJunctions.get(key)
385
+ const directions = new Set(junction?.directions)
386
+ directions.add(connectorDirection)
387
+ grid.replaceCell(
388
+ sourcePoint.x,
389
+ sourcePoint.y,
390
+ diagramLineGlyph(
391
+ directions,
392
+ "rounded",
393
+ (junction?.heavy ?? route.edge.style === "thick") ? "heavy" : "single",
394
+ ),
395
+ "edge",
396
+ )
397
+ }
398
+ }
399
+ fadeSourcePath(grid, connector, route.points, styles, occupancy)
400
+ if (route.edge.sourceArrowhead && route.points[1]) {
401
+ grid.setCell(sourcePoint.x, sourcePoint.y, diagramArrowHeadBetween(route.points[1], sourcePoint), "edge")
402
+ }
403
+ }
404
+ }
405
+
406
+ export function drawFlowchartDiagramGrid(
407
+ diagram: FlowchartDiagram,
408
+ options: FlowchartDiagramRenderOptions = {},
409
+ ): FlowchartGrid {
410
+ const borderStyle = options.borderStyle ?? DEFAULT_BORDER_STYLE
411
+ const layout = layoutFlowchartDiagram(diagram, options)
412
+ const { bounds, routes, subgraphBounds, width, height } = layout
413
+ diagram = layout.diagram
414
+ const grid = new DiagramCanvas<FlowchartCellStyle, FlowchartCellMetadata>(width, height, {
415
+ mergeCell: mergeFlowchartCell,
416
+ })
417
+ for (const subgraph of diagram.subgraphs ?? []) {
418
+ const bound = subgraphBounds.get(subgraph.id)
419
+ if (bound) drawSubgraphFrame(grid, bound, borderStyle)
420
+ }
421
+ for (const route of routes) drawRoutedEdge(grid, route)
422
+ for (const node of diagram.nodes) {
423
+ const bound = bounds.get(node.id)
424
+ if (bound) drawNode(grid, node, bound, borderStyle)
425
+ }
426
+ drawSourceConnectors(grid, diagram, bounds, routes)
427
+ for (const subgraph of diagram.subgraphs ?? []) {
428
+ const bound = subgraphBounds.get(subgraph.id)
429
+ if (bound) drawSubgraphLabel(grid, bound)
430
+ }
431
+
432
+ return grid
433
+ }
@@ -0,0 +1,144 @@
1
+ import {
2
+ clampPoint,
3
+ centeredSpanStart,
4
+ insetSpan,
5
+ midpoint,
6
+ point,
7
+ pointOnSegment,
8
+ segmentBetween,
9
+ segmentSpan,
10
+ shiftPoint,
11
+ spanCapacity,
12
+ type DiagramSegment,
13
+ } from "../core/geometry.js"
14
+ import { measureDiagramLabelWidth, padDiagramLabelLine, splitDiagramLines } from "../core/text.js"
15
+ import type { FlowchartEdgeRoute, FlowchartPoint } from "./types.js"
16
+
17
+ const LABEL_BUS_CLEARANCE = 3
18
+ const LABEL_NODE_CLEARANCE = 2
19
+ const LABEL_LINE_CLEARANCE = 2
20
+ const LABEL_TERMINAL_CLEARANCE = 1
21
+
22
+ export interface FlowchartEdgeLabelLayout {
23
+ lines: string[]
24
+ point: FlowchartPoint
25
+ width: number
26
+ height: number
27
+ }
28
+
29
+ export function flowchartLabelText(label: string): string {
30
+ return padDiagramLabelLine(label)
31
+ }
32
+
33
+ export function flowchartLabelWidth(label: string, measure: (text: string) => number): number {
34
+ return measureDiagramLabelWidth(label, measure)
35
+ }
36
+
37
+ function minimumInlineLabelLength(labelWidth: number): number {
38
+ return labelWidth + LABEL_LINE_CLEARANCE * 2 - 1
39
+ }
40
+
41
+ export function flowchartHorizontalLabelRankGap(labelWidth: number): number {
42
+ return minimumInlineLabelLength(labelWidth) + LABEL_BUS_CLEARANCE + 1
43
+ }
44
+
45
+ export function flowchartVerticalBranchLabelGap(labelWidth: number): number {
46
+ return minimumInlineLabelLength(labelWidth) + LABEL_BUS_CLEARANCE + LABEL_NODE_CLEARANCE
47
+ }
48
+
49
+ function inlineLabelSlot(segment: DiagramSegment, labelWidth: number): { x: number; fits: boolean } {
50
+ const slot = insetSpan(segmentSpan(segment), LABEL_LINE_CLEARANCE)
51
+ return { x: centeredSpanStart(slot, labelWidth), fits: spanCapacity(slot) >= labelWidth }
52
+ }
53
+
54
+ function segmentLabelPoint(segment: DiagramSegment, labelWidth: number, labelHeight: number): FlowchartPoint {
55
+ if (segment.axis === "x") {
56
+ const slot = inlineLabelSlot(segment, labelWidth)
57
+ if (labelHeight === 1 && slot.fits) return point(slot.x, segment.from.y)
58
+
59
+ if (labelHeight > 1) {
60
+ return shiftPoint(point(slot.x, segment.from.y), "up", labelHeight)
61
+ }
62
+
63
+ return clampPoint(shiftPoint(shiftPoint(segment.from, segment.direction, LABEL_LINE_CLEARANCE), "up", labelHeight))
64
+ }
65
+
66
+ const slot = insetSpan(segmentSpan(segment), LABEL_TERMINAL_CLEARANCE)
67
+ if (spanCapacity(slot) >= labelHeight) {
68
+ return clampPoint(point(segment.from.x + 1, centeredSpanStart(slot, labelHeight)))
69
+ }
70
+
71
+ const center = shiftPoint(pointOnSegment(segment, midpoint(segmentSpan(segment))), "right")
72
+ return clampPoint(shiftPoint(center, "up", Math.floor((labelHeight - 1) / 2)))
73
+ }
74
+
75
+ function bestLabelSegment(
76
+ points: readonly FlowchartPoint[],
77
+ labelWidth: number,
78
+ preferredAxis?: DiagramSegment["axis"],
79
+ preferredSegment?: number,
80
+ ): DiagramSegment | undefined {
81
+ const segments = points.slice(1).flatMap((to, index) => {
82
+ const segment = segmentBetween(points[index]!, to)
83
+ return segment ? [segment] : []
84
+ })
85
+ if (preferredSegment !== undefined && segments[preferredSegment]) return segments[preferredSegment]
86
+ const preferred = preferredAxis ? segments.find((segment) => segment.axis === preferredAxis) : undefined
87
+ if (preferred) return preferred
88
+
89
+ let roomyHorizontal: DiagramSegment | undefined
90
+ let verticalBus: DiagramSegment | undefined
91
+ let longest: DiagramSegment | undefined
92
+
93
+ for (const segment of segments) {
94
+ if (!roomyHorizontal && segment.axis === "x" && inlineLabelSlot(segment, labelWidth).fits) roomyHorizontal = segment
95
+ if (!verticalBus && segment.axis === "y") verticalBus = segment
96
+ if (!longest || segment.length > longest.length) longest = segment
97
+ }
98
+
99
+ return roomyHorizontal ?? verticalBus ?? longest
100
+ }
101
+
102
+ function flowchartLabelPoint(
103
+ points: readonly FlowchartPoint[],
104
+ labelWidth: number,
105
+ labelHeight: number,
106
+ preferredAxis?: DiagramSegment["axis"],
107
+ preferredSegment?: number,
108
+ ): FlowchartPoint {
109
+ const segment = bestLabelSegment(points, labelWidth, preferredAxis, preferredSegment)
110
+ return segment ? segmentLabelPoint(segment, labelWidth, labelHeight) : (points[0] ?? point(0, 0))
111
+ }
112
+
113
+ export function flowchartEdgeLabelLayout(
114
+ points: readonly FlowchartPoint[],
115
+ label: string,
116
+ measure: (text: string) => number,
117
+ preferredAxis?: DiagramSegment["axis"],
118
+ preferredSegment?: number,
119
+ ): FlowchartEdgeLabelLayout {
120
+ const lines = splitDiagramLines(label).map(flowchartLabelText)
121
+ const width = flowchartLabelWidth(label, measure)
122
+ const height = lines.length
123
+ return {
124
+ lines,
125
+ point: flowchartLabelPoint(points, width, height, preferredAxis, preferredSegment),
126
+ width,
127
+ height,
128
+ }
129
+ }
130
+
131
+ export function flowchartRouteLabelLayout(
132
+ route: Pick<FlowchartEdgeRoute, "edge" | "points" | "labelAxis" | "labelPoint">,
133
+ measure: (text: string) => number,
134
+ ): FlowchartEdgeLabelLayout {
135
+ const lines = splitDiagramLines(route.edge.label).map(flowchartLabelText)
136
+ const width = flowchartLabelWidth(route.edge.label, measure)
137
+ const height = lines.length
138
+ return {
139
+ lines,
140
+ point: route.labelPoint ?? flowchartLabelPoint(route.points, width, height, route.labelAxis),
141
+ width,
142
+ height,
143
+ }
144
+ }