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,343 @@
1
+ import { BorderChars } from "@opentui/core"
2
+ import { DiagramCanvas, type DiagramCanvasCell } from "../core/canvas.js"
3
+ import { diagramLineGlyph, drawDiagramFrame, drawOrthogonalPath, mergeDiagramLineGlyph } from "../core/drawing.js"
4
+ import { directionBetween, type DiagramDirection, type DiagramPoint } from "../core/geometry.js"
5
+ import { diagramTextWidth, splitDiagramLines } from "../core/text.js"
6
+ import { layoutDagreClassDiagram } from "./dagre-layout.js"
7
+ import { classLabelAnchor } from "./labels.js"
8
+ import {
9
+ CLASS_CONTENT_OFFSET_X,
10
+ layoutClassDiagram,
11
+ type ClassBox,
12
+ type ClassDiagramLayout,
13
+ type ClassEdge,
14
+ } from "./layout.js"
15
+ import type { ClassGrid } from "./render-grid.js"
16
+ import type {
17
+ ClassCellStyle,
18
+ ClassDiagram,
19
+ ClassDiagramDirection,
20
+ ClassDiagramRenderOptions,
21
+ ClassRelationshipKind,
22
+ } from "./types.js"
23
+
24
+ type ClassCell = DiagramCanvasCell<ClassCellStyle>
25
+
26
+ const HOLLOW_TRIANGLE: Record<DiagramDirection, string> = { up: "△", down: "▽", left: "◁", right: "▷" }
27
+ const OPEN_ARROW: Record<DiagramDirection, string> = { up: "↑", down: "↓", left: "←", right: "→" }
28
+ const FILLED_DIAMOND = "◆"
29
+ const HOLLOW_DIAMOND = "◇"
30
+
31
+ export function drawClassDiagramGrid(diagram: ClassDiagram, options: ClassDiagramRenderOptions = {}): ClassGrid {
32
+ return drawClassDiagramLayout(createClassDiagramLayout(diagram, options))
33
+ }
34
+
35
+ export function createClassDiagramLayout(
36
+ diagram: ClassDiagram,
37
+ options: ClassDiagramRenderOptions = {},
38
+ ): ClassDiagramLayout {
39
+ const place = options.classLayout === "dagre" ? layoutDagreClassDiagram : layoutClassDiagram
40
+ const requested = diagram.direction
41
+ const maxWidth =
42
+ options.layoutMaxWidth !== undefined && Number.isFinite(options.layoutMaxWidth)
43
+ ? Math.max(1, Math.trunc(options.layoutMaxWidth))
44
+ : undefined
45
+ if (maxWidth === undefined || (requested !== "LR" && requested !== "RL")) {
46
+ return place(diagram, requested)
47
+ }
48
+ const wide = place(diagram, requested)
49
+ if (wide.width <= maxWidth) return wide
50
+ const fallbackDirection: ClassDiagramDirection = requested === "RL" ? "BT" : "TB"
51
+ const fallback = place(diagram, fallbackDirection)
52
+ return fallback.width < wide.width ? fallback : wide
53
+ }
54
+
55
+ function drawClassDiagramLayout(layout: ClassDiagramLayout): ClassGrid {
56
+ if (layout.boxes.length === 0) return new DiagramCanvas(0, 0)
57
+ const grid = makeGrid(Math.max(1, layout.width), Math.max(1, layout.height))
58
+ const vertical = layout.direction === "TB" || layout.direction === "BT"
59
+ for (const box of layout.boxes) drawBox(grid, box)
60
+ for (const edge of layout.edges) drawEdgeLine(grid, edge)
61
+ for (const edge of layout.edges) drawEdgeEndpoints(grid, edge)
62
+ bridgeIndependentCrossings(grid, layout.edges)
63
+ for (const edge of layout.edges) drawEdgeLabels(grid, edge, vertical)
64
+ return grid
65
+ }
66
+
67
+ function makeGrid(width: number, height: number): ClassGrid {
68
+ return new DiagramCanvas(width, height, {
69
+ mergeCell: (existing, incoming): ClassCell => ({
70
+ ...incoming,
71
+ char:
72
+ existing.style === "edge" && incoming.style === "edge"
73
+ ? (mergeDiagramLineGlyph(existing.char, incoming.char) ?? incoming.char)
74
+ : incoming.char,
75
+ }),
76
+ })
77
+ }
78
+
79
+ function setCell(grid: ClassGrid, x: number, y: number, char: string, style?: ClassCellStyle): void {
80
+ grid.setCell(x, y, char, style)
81
+ }
82
+
83
+ function drawBox(grid: ClassGrid, box: ClassBox): void {
84
+ const bounds = box.bounds
85
+ drawDiagramFrame(bounds, BorderChars.single, (x, y, char) => setCell(grid, x, y, char, "border"))
86
+
87
+ let y = bounds.top + 1
88
+ box.titleLines.forEach((line, index) => {
89
+ const x = bounds.left + CLASS_CONTENT_OFFSET_X + Math.floor((box.contentWidth - diagramTextWidth(line)) / 2)
90
+ setText(grid, x, y + index, line, box.definition.stereotype && index === 0 ? "stereotype" : "title")
91
+ })
92
+ y += box.titleLines.length
93
+
94
+ if (box.attributeLines.length) {
95
+ drawSeparator(grid, bounds, y)
96
+ y += 1
97
+ box.attributeLines.forEach((line, index) => setText(grid, bounds.left + CLASS_CONTENT_OFFSET_X, y + index, line, "member"))
98
+ y += box.attributeLines.length
99
+ }
100
+ if (box.methodLines.length) {
101
+ drawSeparator(grid, bounds, y)
102
+ y += 1
103
+ box.methodLines.forEach((line, index) => setText(grid, bounds.left + CLASS_CONTENT_OFFSET_X, y + index, line, "member"))
104
+ }
105
+ }
106
+
107
+ function drawSeparator(grid: ClassGrid, bounds: ClassBox["bounds"], y: number): void {
108
+ const left = bounds.left
109
+ const right = bounds.left + bounds.width - 1
110
+ setCell(grid, left, y, "├", "border")
111
+ for (let x = left + 1; x < right; x++) setCell(grid, x, y, "─", "border")
112
+ setCell(grid, right, y, "┤", "border")
113
+ }
114
+
115
+ function drawEdgeLine(grid: ClassGrid, edge: ClassEdge): void {
116
+ const { relationship, path } = edge
117
+ if (path.length < 2) return
118
+ drawOrthogonalPath(path, (x, y, char) => setCell(grid, x, y, char, "edge"), {
119
+ cornerStyle: "square",
120
+ lineStyle: relationship.dashed ? "dashed" : "single",
121
+ })
122
+ }
123
+
124
+ interface CrossingAxis {
125
+ through: number[]
126
+ ends: number[]
127
+ }
128
+
129
+ interface CrossingCell {
130
+ horizontal: CrossingAxis
131
+ vertical: CrossingAxis
132
+ }
133
+
134
+ function rememberOwner(owners: number[], owner: number): void {
135
+ // Two distinct owners suffice to determine whether the other axis is independent.
136
+ if (owners.length < 2 && !owners.includes(owner)) owners.push(owner)
137
+ }
138
+
139
+ function independent(first: readonly number[], second: readonly number[]): boolean {
140
+ return first.length > 0 && second.length > 0 &&
141
+ (first.length > 1 || second.length > 1 || first[0] !== second[0])
142
+ }
143
+
144
+ function bridgeIndependentCrossings(grid: ClassGrid, edges: readonly ClassEdge[]): void {
145
+ const cells = new Map<number, CrossingCell>()
146
+ const seenPaths = new Set<string>()
147
+
148
+ edges.forEach((edge, edgeIndex) => {
149
+ // Repeated relationships can have identical routes; their geometry only
150
+ // needs to be recorded once, regardless of how many times it was drawn.
151
+ const pathKey = edge.path.map(({ x, y }) => `${x}:${y}`).join(";")
152
+ if (seenPaths.has(pathKey)) return
153
+ seenPaths.add(pathKey)
154
+
155
+ for (let index = 1; index < edge.path.length; index++) {
156
+ const from = edge.path[index - 1]!
157
+ const to = edge.path[index]!
158
+ const horizontal = from.y === to.y
159
+ if (!horizontal && from.x !== to.x) continue
160
+ const start = horizontal ? from.x : from.y
161
+ const end = horizontal ? to.x : to.y
162
+ for (let position = Math.min(start, end); position <= Math.max(start, end); position++) {
163
+ const x = horizontal ? position : from.x
164
+ const y = horizontal ? from.y : position
165
+ const cellKey = y * grid.width + x
166
+ const cell = cells.get(cellKey) ?? {
167
+ horizontal: { through: [], ends: [] },
168
+ vertical: { through: [], ends: [] },
169
+ }
170
+ const axis = horizontal ? cell.horizontal : cell.vertical
171
+ if (position !== start && position !== end) rememberOwner(axis.through, edgeIndex)
172
+ else if ((position === start && index === 1) || (position === end && index === edge.path.length - 1)) {
173
+ rememberOwner(axis.ends, edgeIndex)
174
+ }
175
+ cells.set(cellKey, cell)
176
+ }
177
+ }
178
+ })
179
+
180
+ for (const [cellKey, cell] of cells) {
181
+ const x = cellKey % grid.width
182
+ const y = Math.floor(cellKey / grid.width)
183
+ if (grid.getCell(x, y)?.char !== "┼") continue
184
+ if (independent(cell.horizontal.through, cell.vertical.through) ||
185
+ independent(cell.horizontal.ends, cell.vertical.through)) {
186
+ grid.replaceCell(x, y, "─", "edge")
187
+ } else if (independent(cell.horizontal.through, cell.vertical.ends)) {
188
+ // A vertical endpoint must still reach its box; break the passing line instead.
189
+ grid.replaceCell(x, y, "│", "edge")
190
+ }
191
+ }
192
+ }
193
+
194
+ function drawEdgeEndpoints(grid: ClassGrid, edge: ClassEdge): void {
195
+ const { relationship, path } = edge
196
+ if (path.length < 2) return
197
+ drawStartCell(grid, path, relationship.headAt === "from" ? relationship.kind : undefined)
198
+ drawEndCell(grid, path, relationship.headAt === "to" ? relationship.kind : undefined)
199
+ }
200
+
201
+ function drawStartCell(grid: ClassGrid, path: DiagramPoint[], markerKind: ClassRelationshipKind | undefined): void {
202
+ const start = path[0]!
203
+ const next = path[1]!
204
+ const direction = directionBetween(start, next) ?? "right"
205
+ const marker = markerKind ? relationshipMarker(markerKind, opposite(direction)) : undefined
206
+ if (marker) setCell(grid, start.x, start.y, marker, "edge")
207
+ else setCell(grid, start.x, start.y, diagramLineGlyph(new Set([direction])), "edge")
208
+ }
209
+
210
+ function drawEndCell(grid: ClassGrid, path: DiagramPoint[], markerKind: ClassRelationshipKind | undefined): void {
211
+ const end = path[path.length - 1]!
212
+ const previous = path[path.length - 2]!
213
+ const direction = directionBetween(previous, end) ?? "right"
214
+ const marker = markerKind ? relationshipMarker(markerKind, direction) : undefined
215
+ if (marker) setCell(grid, end.x, end.y, marker, "edge")
216
+ else setCell(grid, end.x, end.y, diagramLineGlyph(new Set([direction])), "edge")
217
+ }
218
+
219
+ function relationshipMarker(kind: ClassRelationshipKind, direction: DiagramDirection): string | undefined {
220
+ switch (kind) {
221
+ case "inheritance":
222
+ case "realization":
223
+ return HOLLOW_TRIANGLE[direction]
224
+ case "composition":
225
+ return FILLED_DIAMOND
226
+ case "aggregation":
227
+ return HOLLOW_DIAMOND
228
+ case "association":
229
+ case "dependency":
230
+ return OPEN_ARROW[direction]
231
+ case "link":
232
+ return undefined
233
+ }
234
+ }
235
+
236
+ function opposite(direction: DiagramDirection): DiagramDirection {
237
+ switch (direction) {
238
+ case "up":
239
+ return "down"
240
+ case "down":
241
+ return "up"
242
+ case "left":
243
+ return "right"
244
+ case "right":
245
+ return "left"
246
+ }
247
+ }
248
+
249
+ function drawEdgeLabels(grid: ClassGrid, edge: ClassEdge, vertical: boolean): void {
250
+ const { relationship, path } = edge
251
+ let labelWidth = 0
252
+ let labelHeight = 0
253
+ let labelX = 0
254
+ let labelY = 0
255
+ if (relationship.label) {
256
+ const lines = splitDiagramLines(relationship.label)
257
+ labelWidth = Math.max(...lines.map(diagramTextWidth))
258
+ labelHeight = lines.length
259
+ const anchor = classLabelAnchor(path, lines.length, labelWidth, vertical)
260
+ labelX = anchor.x
261
+ labelY = anchor.y
262
+ drawTextTag(grid, anchor.x, anchor.y, lines)
263
+ }
264
+ if (relationship.fromCardinality) {
265
+ const width = diagramTextWidth(relationship.fromCardinality)
266
+ const anchor = cardinalityAnchor(path, width, true, labelX, labelY, labelWidth, labelHeight)
267
+ drawTextTag(grid, anchor.x, anchor.y, [relationship.fromCardinality])
268
+ }
269
+ if (relationship.toCardinality) {
270
+ const width = diagramTextWidth(relationship.toCardinality)
271
+ const anchor = cardinalityAnchor(path, width, false, labelX, labelY, labelWidth, labelHeight)
272
+ drawTextTag(grid, anchor.x, anchor.y, [relationship.toCardinality])
273
+ }
274
+ }
275
+
276
+ function cardinalityAnchor(
277
+ path: DiagramPoint[],
278
+ width: number,
279
+ atStart: boolean,
280
+ labelX: number,
281
+ labelY: number,
282
+ labelWidth: number,
283
+ labelHeight: number,
284
+ ): DiagramPoint {
285
+ const point = atStart ? path[0]! : path[path.length - 1]!
286
+ const neighbor = atStart ? path[1]! : path[path.length - 2]!
287
+ // Direction points from the endpoint into the line, so one step lands just inside it.
288
+ const direction = directionBetween(point, neighbor) ?? "right"
289
+ const horizontal = direction === "left" || direction === "right"
290
+ const baseX = point.x + (horizontal ? (direction === "right" ? 1 : -1) : 0)
291
+ const baseY = point.y + (horizontal ? 0 : direction === "down" ? 1 : -1)
292
+ const candidates: DiagramPoint[] = horizontal
293
+ ? [
294
+ { x: baseX, y: baseY - 1 },
295
+ { x: baseX, y: baseY + 1 },
296
+ { x: baseX, y: baseY - 2 },
297
+ { x: baseX, y: baseY + 2 },
298
+ ]
299
+ : [
300
+ { x: baseX + 1, y: baseY },
301
+ { x: baseX - 1, y: baseY },
302
+ { x: baseX + 1, y: baseY - 1 },
303
+ { x: baseX + 1, y: baseY + 1 },
304
+ { x: baseX - 1, y: baseY - 1 },
305
+ { x: baseX - 1, y: baseY + 1 },
306
+ ]
307
+ for (const candidate of candidates) {
308
+ if (!touchesLabel(candidate.x, width, candidate.y, 1, labelX, labelY, labelWidth, labelHeight)) return candidate
309
+ }
310
+ return candidates[0]!
311
+ }
312
+
313
+ function touchesLabel(
314
+ x: number,
315
+ width: number,
316
+ y: number,
317
+ height: number,
318
+ labelX: number,
319
+ labelY: number,
320
+ labelWidth: number,
321
+ labelHeight: number,
322
+ ): boolean {
323
+ if (labelWidth <= 0 || labelHeight <= 0) return false
324
+ const rowOverlap = y <= labelY + labelHeight - 1 && y + height - 1 >= labelY
325
+ if (!rowOverlap) return false
326
+ // On a shared row keep at least one blank column so the two tags stay legible.
327
+ const clearX = x + width < labelX || x > labelX + labelWidth
328
+ return !clearX
329
+ }
330
+
331
+ function drawTextTag(grid: ClassGrid, x: number, y: number, lines: string[]): void {
332
+ lines.forEach((line, index) => {
333
+ const row = y + index
334
+ if (grid.getCell(x - 1, row)?.char === " ") setCell(grid, x - 1, row, " ", "label")
335
+ setText(grid, x, row, line, "label")
336
+ const rightX = x + diagramTextWidth(line)
337
+ if (grid.getCell(rightX, row)?.char === " ") setCell(grid, rightX, row, " ", "label")
338
+ })
339
+ }
340
+
341
+ function setText(grid: ClassGrid, x: number, y: number, text: string, style?: ClassCellStyle): void {
342
+ grid.setText(x, y, text, style)
343
+ }
@@ -0,0 +1,38 @@
1
+ import type { DiagramPoint } from "../core/geometry.js"
2
+
3
+ export function classLabelAnchor(path: readonly DiagramPoint[], lineCount: number, width: number, vertical: boolean): DiagramPoint {
4
+ let bestFrom = path[0]!
5
+ let bestTo = path[path.length - 1]!
6
+ let bestLength = -1
7
+ const preferredLength = (from: DiagramPoint, to: DiagramPoint): number => {
8
+ const horizontal = from.y === to.y
9
+ return horizontal === vertical ? Math.abs(from.x - to.x) + Math.abs(from.y - to.y) : -1
10
+ }
11
+ for (let index = 1; index < path.length; index++) {
12
+ const from = path[index - 1]!
13
+ const to = path[index]!
14
+ const length = preferredLength(from, to)
15
+ if (length > bestLength) {
16
+ bestLength = length
17
+ bestFrom = from
18
+ bestTo = to
19
+ }
20
+ }
21
+ if (bestLength < 0) {
22
+ for (let index = 1; index < path.length; index++) {
23
+ const from = path[index - 1]!
24
+ const to = path[index]!
25
+ const length = Math.abs(from.x - to.x) + Math.abs(from.y - to.y)
26
+ if (length > bestLength) {
27
+ bestLength = length
28
+ bestFrom = from
29
+ bestTo = to
30
+ }
31
+ }
32
+ }
33
+ const midX = Math.floor((bestFrom.x + bestTo.x) / 2)
34
+ const midY = Math.floor((bestFrom.y + bestTo.y) / 2)
35
+ if (bestFrom.y === bestTo.y) return { x: midX - Math.floor(width / 2), y: bestFrom.y - lineCount }
36
+ if (bestFrom.x === bestTo.x) return { x: bestFrom.x + 2, y: midY - Math.floor(lineCount / 2) }
37
+ return { x: midX, y: midY }
38
+ }