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,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"
|