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