@yassimba/pi-loom-mermaid 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/README.md +28 -0
- package/index.ts +1 -0
- package/package.json +57 -0
- package/src/index.ts +103 -0
- package/src/loom-mermaid/ansi.ts +65 -0
- package/src/loom-mermaid/canvas.ts +466 -0
- package/src/loom-mermaid/class-style.ts +65 -0
- package/src/loom-mermaid/css-colors.ts +153 -0
- package/src/loom-mermaid/diagrams/class.ts +297 -0
- package/src/loom-mermaid/diagrams/er.ts +189 -0
- package/src/loom-mermaid/diagrams/flowchart.ts +519 -0
- package/src/loom-mermaid/diagrams/gitgraph.ts +230 -0
- package/src/loom-mermaid/diagrams/mindmap.ts +107 -0
- package/src/loom-mermaid/diagrams/pie.ts +121 -0
- package/src/loom-mermaid/diagrams/sequence.ts +305 -0
- package/src/loom-mermaid/diagrams/state.ts +279 -0
- package/src/loom-mermaid/diagrams/timeline.ts +117 -0
- package/src/loom-mermaid/graph-render.ts +195 -0
- package/src/loom-mermaid/graph.ts +205 -0
- package/src/loom-mermaid/index.ts +78 -0
- package/src/loom-mermaid/labels.ts +353 -0
- package/src/loom-mermaid/layout-seq.ts +234 -0
- package/src/loom-mermaid/layout.ts +1749 -0
- package/src/loom-mermaid/paint.ts +325 -0
- package/src/loom-mermaid/placement.ts +255 -0
- package/src/loom-mermaid/registry.ts +88 -0
- package/src/loom-mermaid/source-box.ts +111 -0
- package/src/loom-mermaid/statements.ts +228 -0
- package/src/loom-mermaid/types.ts +64 -0
- package/src/loom-mermaid/width-data.ts +993 -0
- package/src/loom-mermaid/width.ts +69 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import type { Role, Span } from './types.ts'
|
|
2
|
+
import { measured } from './width.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sentinel occupying the trailing column of a wide glyph. Never emitted: the
|
|
6
|
+
* line builder skips it so a CJK character claims two cells of layout but
|
|
7
|
+
* contributes one character of output.
|
|
8
|
+
*/
|
|
9
|
+
export const CONT = String.fromCharCode(0)
|
|
10
|
+
|
|
11
|
+
/** Connection direction bits, combined into a box-drawing glyph by `maskChar`. */
|
|
12
|
+
export const U = 1
|
|
13
|
+
export const D = 2
|
|
14
|
+
export const L = 4
|
|
15
|
+
export const R = 8
|
|
16
|
+
|
|
17
|
+
/** Line styles, tracked per cell so crossing edges keep their own stroke. */
|
|
18
|
+
export const STY_DOT = 1
|
|
19
|
+
export const STY_THICK = 2
|
|
20
|
+
export const STY_SOLID = 4
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A grid of cells. Edges accumulate as direction bits rather than glyphs so
|
|
24
|
+
* that crossings and junctions resolve correctly whatever order they are drawn
|
|
25
|
+
* in; `finalizeMask` turns the accumulated bits into characters at the end.
|
|
26
|
+
*
|
|
27
|
+
* `occupied` marks cells claimed by a box, which edge bits must not overwrite.
|
|
28
|
+
*
|
|
29
|
+
* `pass` remembers how each cell was reached: a vertical run passing
|
|
30
|
+
* through, a horizontal run passing through, or a turn, end or junction.
|
|
31
|
+
* A cell crossed by one vertical and one horizontal run and nothing else
|
|
32
|
+
* is two edges crossing, drawn as a hop (`╫`) rather than a junction
|
|
33
|
+
* (`┼`), so an edge can be followed through a dense band.
|
|
34
|
+
*/
|
|
35
|
+
const PASS_V = 1
|
|
36
|
+
const PASS_H = 2
|
|
37
|
+
const JOINED = 4
|
|
38
|
+
const HOP = '╫'
|
|
39
|
+
export class Canvas {
|
|
40
|
+
readonly w: number
|
|
41
|
+
readonly h: number
|
|
42
|
+
ch: string[]
|
|
43
|
+
role: Role[]
|
|
44
|
+
/** Space-joined author classes per cell, or undefined; see `Span.classes`. */
|
|
45
|
+
tag: (string | undefined)[]
|
|
46
|
+
/** Link target per cell, or undefined; see `Span.href`. */
|
|
47
|
+
href: (string | undefined)[]
|
|
48
|
+
mask: Uint8Array
|
|
49
|
+
style: Uint8Array
|
|
50
|
+
occupied: Uint8Array
|
|
51
|
+
pass: Uint8Array
|
|
52
|
+
/** Edge labels queued by the layout, written after every line. */
|
|
53
|
+
labels: { label: string; row: number; x: number }[] = []
|
|
54
|
+
curStyle: number = STY_SOLID
|
|
55
|
+
/** Author classes stamped on cells painted while set, like `curStyle`. */
|
|
56
|
+
curTag: string | undefined
|
|
57
|
+
/** Link target stamped on cells painted while set, like `curTag`. */
|
|
58
|
+
curHref: string | undefined
|
|
59
|
+
|
|
60
|
+
constructor(w: number, h: number) {
|
|
61
|
+
const n = w * h
|
|
62
|
+
this.w = w
|
|
63
|
+
this.h = h
|
|
64
|
+
this.ch = new Array(n).fill(' ')
|
|
65
|
+
this.role = new Array(n).fill('none')
|
|
66
|
+
this.tag = new Array(n).fill(undefined)
|
|
67
|
+
this.href = new Array(n).fill(undefined)
|
|
68
|
+
this.mask = new Uint8Array(n)
|
|
69
|
+
this.style = new Uint8Array(n)
|
|
70
|
+
this.occupied = new Uint8Array(n)
|
|
71
|
+
this.pass = new Uint8Array(n)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
idx(x: number, y: number): number {
|
|
75
|
+
return y * this.w + x
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
set(x: number, y: number, c: string, role: Role): void {
|
|
79
|
+
if (x >= this.w || y >= this.h) return
|
|
80
|
+
const i = this.idx(x, y)
|
|
81
|
+
// A literal tab measures one cell here but jumps to the terminal's tab
|
|
82
|
+
// stop there, desyncing every column after it (the source box expands
|
|
83
|
+
// tabs for the same reason). Same width, safe glyph.
|
|
84
|
+
this.ch[i] = c === '\t' ? ' ' : c
|
|
85
|
+
this.role[i] = role
|
|
86
|
+
if (this.curTag !== undefined) this.tag[i] = this.curTag
|
|
87
|
+
if (this.curHref !== undefined) this.href[i] = this.curHref
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Accumulate direction bits on a free cell.
|
|
92
|
+
*
|
|
93
|
+
* `role` is the role to claim the cell for; `border` cells are never
|
|
94
|
+
* reclassified, so a connector meeting a box keeps the box's styling.
|
|
95
|
+
*/
|
|
96
|
+
addBits(x: number, y: number, bits: number, role: Role = 'edge'): void {
|
|
97
|
+
if (x >= this.w || y >= this.h) return
|
|
98
|
+
const i = this.idx(x, y)
|
|
99
|
+
if (this.occupied[i]) return
|
|
100
|
+
this.mask[i] |= bits
|
|
101
|
+
this.pass[i] |= bits === (U | D) ? PASS_V : bits === (L | R) ? PASS_H : JOINED
|
|
102
|
+
this.style[i] |= this.curStyle
|
|
103
|
+
if (this.role[i] !== 'border') this.role[i] = role
|
|
104
|
+
if (this.curTag !== undefined) this.tag[i] = this.curTag
|
|
105
|
+
if (this.curHref !== undefined) this.href[i] = this.curHref
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Stamp a finished sub-canvas (a subgraph frame's contents) at an offset. */
|
|
109
|
+
blit(sub: Canvas, ox: number, oy: number): void {
|
|
110
|
+
for (let sy = 0; sy < sub.h; sy++) {
|
|
111
|
+
for (let sx = 0; sx < sub.w; sx++) {
|
|
112
|
+
const x = ox + sx
|
|
113
|
+
const y = oy + sy
|
|
114
|
+
if (x >= this.w || y >= this.h) continue
|
|
115
|
+
const si = sub.idx(sx, sy)
|
|
116
|
+
const di = this.idx(x, y)
|
|
117
|
+
this.ch[di] = sub.ch[si]
|
|
118
|
+
this.role[di] = sub.role[si]
|
|
119
|
+
this.tag[di] = sub.tag[si]
|
|
120
|
+
this.href[di] = sub.href[si]
|
|
121
|
+
this.style[di] = sub.style[si]
|
|
122
|
+
this.pass[di] = sub.pass[si]
|
|
123
|
+
this.occupied[di] = 1
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Add direction bits even to an occupied cell, so an edge can meet a border. */
|
|
129
|
+
junction(x: number, y: number, bits: number): void {
|
|
130
|
+
if (x >= this.w || y >= this.h) return
|
|
131
|
+
const i = this.idx(x, y)
|
|
132
|
+
this.mask[i] |= bits
|
|
133
|
+
this.pass[i] |= JOINED
|
|
134
|
+
if (this.role[i] !== 'border') this.role[i] = 'edge'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
segV(x: number, y0: number, y1: number): void {
|
|
138
|
+
const a = Math.min(y0, y1)
|
|
139
|
+
const b = Math.max(y0, y1)
|
|
140
|
+
for (let y = a; y <= b; y++) {
|
|
141
|
+
let bits = 0
|
|
142
|
+
if (y > a) bits |= U
|
|
143
|
+
if (y < b) bits |= D
|
|
144
|
+
this.addBits(x, y, bits)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
segH(y: number, x0: number, x1: number): void {
|
|
149
|
+
const a = Math.min(x0, x1)
|
|
150
|
+
const b = Math.max(x0, x1)
|
|
151
|
+
for (let x = a; x <= b; x++) {
|
|
152
|
+
let bits = 0
|
|
153
|
+
if (x > a) bits |= L
|
|
154
|
+
if (x < b) bits |= R
|
|
155
|
+
this.addBits(x, y, bits)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Resolve accumulated direction bits into glyphs, honouring line style. */
|
|
160
|
+
finalizeMask(): void {
|
|
161
|
+
for (let i = 0; i < this.ch.length; i++) {
|
|
162
|
+
if (this.mask[i] === 0) continue
|
|
163
|
+
if (this.ch[i] === ' ') {
|
|
164
|
+
const c = this.pass[i] === (PASS_V | PASS_H) ? HOP : maskChar(this.mask[i])
|
|
165
|
+
this.ch[i] =
|
|
166
|
+
this.style[i] === STY_DOT ? dottedChar(c) : this.style[i] === STY_THICK ? thickChar(c) : c
|
|
167
|
+
} else if (this.ch[i] === '═' || this.ch[i] === '║') {
|
|
168
|
+
this.ch[i] = doubleTee(this.ch[i], this.mask[i])
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Mirror top-to-bottom for `BT`. Rows reorder but within-row text does not,
|
|
175
|
+
* so labels stay readable; box-drawing glyphs flip to match.
|
|
176
|
+
*/
|
|
177
|
+
flipVertical(): void {
|
|
178
|
+
for (let y = 0; y < Math.floor(this.h / 2); y++) {
|
|
179
|
+
const y2 = this.h - 1 - y
|
|
180
|
+
for (let x = 0; x < this.w; x++) {
|
|
181
|
+
const i = this.idx(x, y)
|
|
182
|
+
const j = this.idx(x, y2)
|
|
183
|
+
;[this.ch[i], this.ch[j]] = [this.ch[j], this.ch[i]]
|
|
184
|
+
;[this.role[i], this.role[j]] = [this.role[j], this.role[i]]
|
|
185
|
+
;[this.tag[i], this.tag[j]] = [this.tag[j], this.tag[i]]
|
|
186
|
+
;[this.href[i], this.href[j]] = [this.href[j], this.href[i]]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (let i = 0; i < this.ch.length; i++) {
|
|
190
|
+
if (!textRole(this.role[i])) this.ch[i] = flipGlyphV(this.ch[i])
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Mirror left-to-right for `RL`. Mirroring reverses each row, so after
|
|
196
|
+
* flipping glyphs each text/label run is reversed back to reading order.
|
|
197
|
+
*/
|
|
198
|
+
flipHorizontal(): void {
|
|
199
|
+
for (let y = 0; y < this.h; y++) {
|
|
200
|
+
for (let x = 0; x < Math.floor(this.w / 2); x++) {
|
|
201
|
+
const x2 = this.w - 1 - x
|
|
202
|
+
const i = this.idx(x, y)
|
|
203
|
+
const j = this.idx(x2, y)
|
|
204
|
+
;[this.ch[i], this.ch[j]] = [this.ch[j], this.ch[i]]
|
|
205
|
+
;[this.role[i], this.role[j]] = [this.role[j], this.role[i]]
|
|
206
|
+
;[this.tag[i], this.tag[j]] = [this.tag[j], this.tag[i]]
|
|
207
|
+
;[this.href[i], this.href[j]] = [this.href[j], this.href[i]]
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (let i = 0; i < this.ch.length; i++) {
|
|
211
|
+
if (!textRole(this.role[i])) this.ch[i] = flipGlyphH(this.ch[i])
|
|
212
|
+
}
|
|
213
|
+
for (let y = 0; y < this.h; y++) {
|
|
214
|
+
let x = 0
|
|
215
|
+
while (x < this.w) {
|
|
216
|
+
const role = this.role[this.idx(x, y)]
|
|
217
|
+
if (role === 'text' || role === 'edgeLabel') {
|
|
218
|
+
const start = this.idx(x, y)
|
|
219
|
+
while (x < this.w && this.role[this.idx(x, y)] === role) x++
|
|
220
|
+
const end = this.idx(x, y)
|
|
221
|
+
reverseSlice(this.ch, start, end)
|
|
222
|
+
} else {
|
|
223
|
+
x++
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Group each row into runs of one role and tag, dropping continuations. */
|
|
230
|
+
toLines(): { plain: string[]; styled: Span[][]; width: number } {
|
|
231
|
+
const plain: string[] = []
|
|
232
|
+
const styled: Span[][] = []
|
|
233
|
+
let width = 0
|
|
234
|
+
for (let y = 0; y < this.h; y++) {
|
|
235
|
+
// A trailing CONT counts as painted: it is the second cell of a wide
|
|
236
|
+
// glyph, so the row really does reach that column.
|
|
237
|
+
let last = 0
|
|
238
|
+
for (let x = this.w - 1; x >= 0; x--) {
|
|
239
|
+
if (this.ch[this.idx(x, y)] !== ' ') {
|
|
240
|
+
last = x + 1
|
|
241
|
+
break
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
width = Math.max(width, last)
|
|
245
|
+
const spans: Span[] = []
|
|
246
|
+
const push = (text: string, role: Role, tag: string | undefined, href?: string): void => {
|
|
247
|
+
if (text === '') return
|
|
248
|
+
const span: Span = { text, role }
|
|
249
|
+
if (tag !== undefined) span.classes = tag.split(' ')
|
|
250
|
+
if (href !== undefined) span.href = href
|
|
251
|
+
spans.push(span)
|
|
252
|
+
}
|
|
253
|
+
let plainRow = ''
|
|
254
|
+
let run = ''
|
|
255
|
+
let runRole: Role = 'none'
|
|
256
|
+
let runTag: string | undefined
|
|
257
|
+
let runHref: string | undefined
|
|
258
|
+
for (let x = 0; x < last; x++) {
|
|
259
|
+
const i = this.idx(x, y)
|
|
260
|
+
const c = this.ch[i]
|
|
261
|
+
if (c === CONT) continue
|
|
262
|
+
plainRow += c
|
|
263
|
+
if (
|
|
264
|
+
(this.role[i] !== runRole || this.tag[i] !== runTag || this.href[i] !== runHref) &&
|
|
265
|
+
run !== ''
|
|
266
|
+
) {
|
|
267
|
+
push(run, runRole, runTag, runHref)
|
|
268
|
+
run = ''
|
|
269
|
+
}
|
|
270
|
+
runRole = this.role[i]
|
|
271
|
+
runTag = this.tag[i]
|
|
272
|
+
runHref = this.href[i]
|
|
273
|
+
run += c
|
|
274
|
+
}
|
|
275
|
+
push(run, runRole, runTag, runHref)
|
|
276
|
+
styled.push(spans)
|
|
277
|
+
// Only ASCII spaces, which is all a blank cell ever holds. Trimming `\s`
|
|
278
|
+
// would eat a trailing NBSP that `styled` keeps, desyncing the two.
|
|
279
|
+
// (A ` +$` regex backtracks quadratically on a row of mostly spaces:
|
|
280
|
+
// it was more than half the render time of a 500-edge diagram.)
|
|
281
|
+
let cut = plainRow.length
|
|
282
|
+
while (cut > 0 && plainRow[cut - 1] === ' ') cut--
|
|
283
|
+
plain.push(plainRow.slice(0, cut))
|
|
284
|
+
}
|
|
285
|
+
let first = 0
|
|
286
|
+
while (first < plain.length && plain[first] === '') first++
|
|
287
|
+
let end = plain.length
|
|
288
|
+
while (end > first && plain[end - 1] === '') end--
|
|
289
|
+
return { plain: plain.slice(first, end), styled: styled.slice(first, end), width }
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function reverseSlice(arr: string[], start: number, end: number): void {
|
|
294
|
+
for (let i = start, j = end - 1; i < j; i++, j--) {
|
|
295
|
+
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Paint `text` at `x, y`, one grapheme cluster per cell.
|
|
301
|
+
*
|
|
302
|
+
* A wide cluster claims a second cell, marked with `CONT` so the line builder
|
|
303
|
+
* emits one character for it rather than a stray space.
|
|
304
|
+
*/
|
|
305
|
+
export function drawText(canvas: Canvas, text: string, x: number, y: number, role: Role): void {
|
|
306
|
+
let cur = x
|
|
307
|
+
for (const [cluster, cw] of measured(text)) {
|
|
308
|
+
if (cw === 0) continue
|
|
309
|
+
canvas.set(cur, y, cluster, role)
|
|
310
|
+
for (let k = 1; k < cw; k++) canvas.set(cur + k, y, CONT, role)
|
|
311
|
+
cur += cw
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Paint `text` at `x, y`, clearing any edge bits underneath first.
|
|
317
|
+
*
|
|
318
|
+
* Used where text sits on top of a drawn line (sequence messages, dividers,
|
|
319
|
+
* compartment rows) and must win over it.
|
|
320
|
+
*/
|
|
321
|
+
export function drawTextOverEdges(
|
|
322
|
+
canvas: Canvas,
|
|
323
|
+
text: string,
|
|
324
|
+
x: number,
|
|
325
|
+
y: number,
|
|
326
|
+
role: Role,
|
|
327
|
+
): void {
|
|
328
|
+
let cur = x
|
|
329
|
+
for (const [cluster, cw] of measured(text)) {
|
|
330
|
+
if (cw === 0) continue
|
|
331
|
+
for (let k = 0; k < cw; k++) {
|
|
332
|
+
if (cur + k < canvas.w && y < canvas.h) canvas.mask[canvas.idx(cur + k, y)] = 0
|
|
333
|
+
canvas.set(cur + k, y, k === 0 ? cluster : CONT, role)
|
|
334
|
+
}
|
|
335
|
+
cur += cw
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function maskChar(mask: number): string {
|
|
340
|
+
switch (mask) {
|
|
341
|
+
case 0:
|
|
342
|
+
return ' '
|
|
343
|
+
case U:
|
|
344
|
+
case D:
|
|
345
|
+
case U | D:
|
|
346
|
+
return '│'
|
|
347
|
+
case L:
|
|
348
|
+
case R:
|
|
349
|
+
case L | R:
|
|
350
|
+
return '─'
|
|
351
|
+
case D | R:
|
|
352
|
+
return '┌'
|
|
353
|
+
case D | L:
|
|
354
|
+
return '┐'
|
|
355
|
+
case U | R:
|
|
356
|
+
return '└'
|
|
357
|
+
case U | L:
|
|
358
|
+
return '┘'
|
|
359
|
+
case U | D | R:
|
|
360
|
+
return '├'
|
|
361
|
+
case U | D | L:
|
|
362
|
+
return '┤'
|
|
363
|
+
case D | L | R:
|
|
364
|
+
return '┬'
|
|
365
|
+
case U | L | R:
|
|
366
|
+
return '┴'
|
|
367
|
+
default:
|
|
368
|
+
return '┼'
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** An edge teeing into a double-line border: the mixed single/double glyphs. */
|
|
373
|
+
function doubleTee(c: string, mask: number): string {
|
|
374
|
+
if (c === '═') {
|
|
375
|
+
if (mask & U && mask & D) return '╪'
|
|
376
|
+
if (mask & D) return '╤'
|
|
377
|
+
if (mask & U) return '╧'
|
|
378
|
+
} else {
|
|
379
|
+
if (mask & L && mask & R) return '╫'
|
|
380
|
+
if (mask & R) return '╟'
|
|
381
|
+
if (mask & L) return '╢'
|
|
382
|
+
}
|
|
383
|
+
return c
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const DOTTED: Record<string, string> = { '─': '╌', '│': '╎' }
|
|
387
|
+
|
|
388
|
+
const THICK: Record<string, string> = {
|
|
389
|
+
'─': '━',
|
|
390
|
+
'│': '┃',
|
|
391
|
+
'┌': '┏',
|
|
392
|
+
'┐': '┓',
|
|
393
|
+
'└': '┗',
|
|
394
|
+
'┘': '┛',
|
|
395
|
+
'├': '┣',
|
|
396
|
+
'┤': '┫',
|
|
397
|
+
'┬': '┳',
|
|
398
|
+
'┴': '┻',
|
|
399
|
+
'┼': '╋',
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const FLIP_V: Record<string, string> = {
|
|
403
|
+
'╔': '╚',
|
|
404
|
+
'╚': '╔',
|
|
405
|
+
'╗': '╝',
|
|
406
|
+
'╝': '╗',
|
|
407
|
+
'╤': '╧',
|
|
408
|
+
'╧': '╤',
|
|
409
|
+
'┌': '└',
|
|
410
|
+
'└': '┌',
|
|
411
|
+
'┐': '┘',
|
|
412
|
+
'┘': '┐',
|
|
413
|
+
'┏': '┗',
|
|
414
|
+
'┗': '┏',
|
|
415
|
+
'┓': '┛',
|
|
416
|
+
'┛': '┓',
|
|
417
|
+
'╭': '╰',
|
|
418
|
+
'╰': '╭',
|
|
419
|
+
'╮': '╯',
|
|
420
|
+
'╯': '╮',
|
|
421
|
+
'┬': '┴',
|
|
422
|
+
'┴': '┬',
|
|
423
|
+
'┳': '┻',
|
|
424
|
+
'┻': '┳',
|
|
425
|
+
'▼': '▲',
|
|
426
|
+
'▲': '▼',
|
|
427
|
+
'▽': '△',
|
|
428
|
+
'△': '▽',
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const FLIP_H: Record<string, string> = {
|
|
432
|
+
'╔': '╗',
|
|
433
|
+
'╗': '╔',
|
|
434
|
+
'╚': '╝',
|
|
435
|
+
'╝': '╚',
|
|
436
|
+
'╟': '╢',
|
|
437
|
+
'╢': '╟',
|
|
438
|
+
'┌': '┐',
|
|
439
|
+
'┐': '┌',
|
|
440
|
+
'└': '┘',
|
|
441
|
+
'┘': '└',
|
|
442
|
+
'┏': '┓',
|
|
443
|
+
'┓': '┏',
|
|
444
|
+
'┗': '┛',
|
|
445
|
+
'┛': '┗',
|
|
446
|
+
'╭': '╮',
|
|
447
|
+
'╮': '╭',
|
|
448
|
+
'╰': '╯',
|
|
449
|
+
'╯': '╰',
|
|
450
|
+
'├': '┤',
|
|
451
|
+
'┤': '├',
|
|
452
|
+
'┣': '┫',
|
|
453
|
+
'┫': '┣',
|
|
454
|
+
'▶': '◄',
|
|
455
|
+
'◄': '▶',
|
|
456
|
+
'▷': '◁',
|
|
457
|
+
'◁': '▷',
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const dottedChar = (c: string): string => DOTTED[c] ?? c
|
|
461
|
+
const thickChar = (c: string): string => THICK[c] ?? c
|
|
462
|
+
const flipGlyphV = (c: string): string => FLIP_V[c] ?? c
|
|
463
|
+
const flipGlyphH = (c: string): string => FLIP_H[c] ?? c
|
|
464
|
+
|
|
465
|
+
/** User-authored cells: flips reorder them but must not remap their glyphs. */
|
|
466
|
+
const textRole = (r: Role): boolean => r === 'text' || r === 'edgeLabel' || r === 'title'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort interpretation of `classDef` styles for a cell grid.
|
|
3
|
+
*
|
|
4
|
+
* A terminal cell can express a foreground, a background and boldness —
|
|
5
|
+
* nothing else. `fill` is the node background, `stroke` its border,
|
|
6
|
+
* `color` its text; every other property is silently ignored.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { NAMED_COLORS } from './css-colors.ts'
|
|
10
|
+
|
|
11
|
+
/** The terminal-expressible subset of a classDef; colors as `#rrggbb`. */
|
|
12
|
+
export interface ClassStyle {
|
|
13
|
+
fill?: string
|
|
14
|
+
stroke?: string
|
|
15
|
+
color?: string
|
|
16
|
+
bold?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `#rgb`, `#rrggbb`, `rgb(r,g,b)` or a CSS color name → `#rrggbb`; else null. */
|
|
20
|
+
function normalizeColor(v: string): string | null {
|
|
21
|
+
const s = v.trim().toLowerCase()
|
|
22
|
+
if (/^#[0-9a-f]{6}$/.test(s)) return s
|
|
23
|
+
if (/^#[0-9a-f]{3}$/.test(s)) return `#${s[1]}${s[1]}${s[2]}${s[2]}${s[3]}${s[3]}`
|
|
24
|
+
const rgb = s.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/)
|
|
25
|
+
if (rgb !== null) {
|
|
26
|
+
const hex = (n: string) => Math.min(255, Number(n)).toString(16).padStart(2, '0')
|
|
27
|
+
return `#${hex(rgb[1])}${hex(rgb[2])}${hex(rgb[3])}`
|
|
28
|
+
}
|
|
29
|
+
return NAMED_COLORS[s] ?? null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The merged style of a span's classes (later classes win), or null when
|
|
34
|
+
* nothing terminal-expressible was declared.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveClassStyle(
|
|
37
|
+
classes: string[] | undefined,
|
|
38
|
+
classDefs: Record<string, Record<string, string>>,
|
|
39
|
+
): ClassStyle | null {
|
|
40
|
+
if (classes === undefined) return null
|
|
41
|
+
const out: ClassStyle = {}
|
|
42
|
+
for (const name of classes) {
|
|
43
|
+
const props = classDefs[name]
|
|
44
|
+
if (props === undefined) continue
|
|
45
|
+
for (const [k, v] of Object.entries(props)) {
|
|
46
|
+
if (k === 'fill' || k === 'stroke' || k === 'color') {
|
|
47
|
+
const c = normalizeColor(v)
|
|
48
|
+
if (c !== null) out[k] = c
|
|
49
|
+
} else if (k === 'font-weight') {
|
|
50
|
+
out.bold = v.trim() === 'bold' || v.trim() === 'bolder'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Object.keys(out).length > 0 ? out : null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Black or white, whichever reads on the given `#rrggbb` background — the
|
|
59
|
+
* guard that keeps `fill:#eee` legible on a dark terminal theme.
|
|
60
|
+
*/
|
|
61
|
+
export function contrastOn(fill: string): '#000000' | '#ffffff' {
|
|
62
|
+
const ch = (i: number) => Number.parseInt(fill.slice(i, i + 2), 16)
|
|
63
|
+
const yiq = (ch(1) * 299 + ch(3) * 587 + ch(5) * 114) / 1000
|
|
64
|
+
return yiq >= 128 ? '#000000' : '#ffffff'
|
|
65
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Generated by scripts/gen-css-colors.ts from https://drafts.csswg.org/css-color-4/ — do not edit.
|
|
2
|
+
|
|
3
|
+
/** The CSS Color 4 named-color table: name → `#rrggbb`. */
|
|
4
|
+
export const NAMED_COLORS: Record<string, string> = {
|
|
5
|
+
aliceblue: '#f0f8ff',
|
|
6
|
+
antiquewhite: '#faebd7',
|
|
7
|
+
aqua: '#00ffff',
|
|
8
|
+
aquamarine: '#7fffd4',
|
|
9
|
+
azure: '#f0ffff',
|
|
10
|
+
beige: '#f5f5dc',
|
|
11
|
+
bisque: '#ffe4c4',
|
|
12
|
+
black: '#000000',
|
|
13
|
+
blanchedalmond: '#ffebcd',
|
|
14
|
+
blue: '#0000ff',
|
|
15
|
+
blueviolet: '#8a2be2',
|
|
16
|
+
brown: '#a52a2a',
|
|
17
|
+
burlywood: '#deb887',
|
|
18
|
+
cadetblue: '#5f9ea0',
|
|
19
|
+
chartreuse: '#7fff00',
|
|
20
|
+
chocolate: '#d2691e',
|
|
21
|
+
coral: '#ff7f50',
|
|
22
|
+
cornflowerblue: '#6495ed',
|
|
23
|
+
cornsilk: '#fff8dc',
|
|
24
|
+
crimson: '#dc143c',
|
|
25
|
+
cyan: '#00ffff',
|
|
26
|
+
darkblue: '#00008b',
|
|
27
|
+
darkcyan: '#008b8b',
|
|
28
|
+
darkgoldenrod: '#b8860b',
|
|
29
|
+
darkgray: '#a9a9a9',
|
|
30
|
+
darkgreen: '#006400',
|
|
31
|
+
darkgrey: '#a9a9a9',
|
|
32
|
+
darkkhaki: '#bdb76b',
|
|
33
|
+
darkmagenta: '#8b008b',
|
|
34
|
+
darkolivegreen: '#556b2f',
|
|
35
|
+
darkorange: '#ff8c00',
|
|
36
|
+
darkorchid: '#9932cc',
|
|
37
|
+
darkred: '#8b0000',
|
|
38
|
+
darksalmon: '#e9967a',
|
|
39
|
+
darkseagreen: '#8fbc8f',
|
|
40
|
+
darkslateblue: '#483d8b',
|
|
41
|
+
darkslategray: '#2f4f4f',
|
|
42
|
+
darkslategrey: '#2f4f4f',
|
|
43
|
+
darkturquoise: '#00ced1',
|
|
44
|
+
darkviolet: '#9400d3',
|
|
45
|
+
deeppink: '#ff1493',
|
|
46
|
+
deepskyblue: '#00bfff',
|
|
47
|
+
dimgray: '#696969',
|
|
48
|
+
dimgrey: '#696969',
|
|
49
|
+
dodgerblue: '#1e90ff',
|
|
50
|
+
firebrick: '#b22222',
|
|
51
|
+
floralwhite: '#fffaf0',
|
|
52
|
+
forestgreen: '#228b22',
|
|
53
|
+
fuchsia: '#ff00ff',
|
|
54
|
+
gainsboro: '#dcdcdc',
|
|
55
|
+
ghostwhite: '#f8f8ff',
|
|
56
|
+
gold: '#ffd700',
|
|
57
|
+
goldenrod: '#daa520',
|
|
58
|
+
gray: '#808080',
|
|
59
|
+
green: '#008000',
|
|
60
|
+
greenyellow: '#adff2f',
|
|
61
|
+
grey: '#808080',
|
|
62
|
+
honeydew: '#f0fff0',
|
|
63
|
+
hotpink: '#ff69b4',
|
|
64
|
+
indianred: '#cd5c5c',
|
|
65
|
+
indigo: '#4b0082',
|
|
66
|
+
ivory: '#fffff0',
|
|
67
|
+
khaki: '#f0e68c',
|
|
68
|
+
lavender: '#e6e6fa',
|
|
69
|
+
lavenderblush: '#fff0f5',
|
|
70
|
+
lawngreen: '#7cfc00',
|
|
71
|
+
lemonchiffon: '#fffacd',
|
|
72
|
+
lightblue: '#add8e6',
|
|
73
|
+
lightcoral: '#f08080',
|
|
74
|
+
lightcyan: '#e0ffff',
|
|
75
|
+
lightgoldenrodyellow: '#fafad2',
|
|
76
|
+
lightgray: '#d3d3d3',
|
|
77
|
+
lightgreen: '#90ee90',
|
|
78
|
+
lightgrey: '#d3d3d3',
|
|
79
|
+
lightpink: '#ffb6c1',
|
|
80
|
+
lightsalmon: '#ffa07a',
|
|
81
|
+
lightseagreen: '#20b2aa',
|
|
82
|
+
lightskyblue: '#87cefa',
|
|
83
|
+
lightslategray: '#778899',
|
|
84
|
+
lightslategrey: '#778899',
|
|
85
|
+
lightsteelblue: '#b0c4de',
|
|
86
|
+
lightyellow: '#ffffe0',
|
|
87
|
+
lime: '#00ff00',
|
|
88
|
+
limegreen: '#32cd32',
|
|
89
|
+
linen: '#faf0e6',
|
|
90
|
+
magenta: '#ff00ff',
|
|
91
|
+
maroon: '#800000',
|
|
92
|
+
mediumaquamarine: '#66cdaa',
|
|
93
|
+
mediumblue: '#0000cd',
|
|
94
|
+
mediumorchid: '#ba55d3',
|
|
95
|
+
mediumpurple: '#9370db',
|
|
96
|
+
mediumseagreen: '#3cb371',
|
|
97
|
+
mediumslateblue: '#7b68ee',
|
|
98
|
+
mediumspringgreen: '#00fa9a',
|
|
99
|
+
mediumturquoise: '#48d1cc',
|
|
100
|
+
mediumvioletred: '#c71585',
|
|
101
|
+
midnightblue: '#191970',
|
|
102
|
+
mintcream: '#f5fffa',
|
|
103
|
+
mistyrose: '#ffe4e1',
|
|
104
|
+
moccasin: '#ffe4b5',
|
|
105
|
+
navajowhite: '#ffdead',
|
|
106
|
+
navy: '#000080',
|
|
107
|
+
oldlace: '#fdf5e6',
|
|
108
|
+
olive: '#808000',
|
|
109
|
+
olivedrab: '#6b8e23',
|
|
110
|
+
orange: '#ffa500',
|
|
111
|
+
orangered: '#ff4500',
|
|
112
|
+
orchid: '#da70d6',
|
|
113
|
+
palegoldenrod: '#eee8aa',
|
|
114
|
+
palegreen: '#98fb98',
|
|
115
|
+
paleturquoise: '#afeeee',
|
|
116
|
+
palevioletred: '#db7093',
|
|
117
|
+
papayawhip: '#ffefd5',
|
|
118
|
+
peachpuff: '#ffdab9',
|
|
119
|
+
peru: '#cd853f',
|
|
120
|
+
pink: '#ffc0cb',
|
|
121
|
+
plum: '#dda0dd',
|
|
122
|
+
powderblue: '#b0e0e6',
|
|
123
|
+
purple: '#800080',
|
|
124
|
+
rebeccapurple: '#663399',
|
|
125
|
+
red: '#ff0000',
|
|
126
|
+
rosybrown: '#bc8f8f',
|
|
127
|
+
royalblue: '#4169e1',
|
|
128
|
+
saddlebrown: '#8b4513',
|
|
129
|
+
salmon: '#fa8072',
|
|
130
|
+
sandybrown: '#f4a460',
|
|
131
|
+
seagreen: '#2e8b57',
|
|
132
|
+
seashell: '#fff5ee',
|
|
133
|
+
sienna: '#a0522d',
|
|
134
|
+
silver: '#c0c0c0',
|
|
135
|
+
skyblue: '#87ceeb',
|
|
136
|
+
slateblue: '#6a5acd',
|
|
137
|
+
slategray: '#708090',
|
|
138
|
+
slategrey: '#708090',
|
|
139
|
+
snow: '#fffafa',
|
|
140
|
+
springgreen: '#00ff7f',
|
|
141
|
+
steelblue: '#4682b4',
|
|
142
|
+
tan: '#d2b48c',
|
|
143
|
+
teal: '#008080',
|
|
144
|
+
thistle: '#d8bfd8',
|
|
145
|
+
tomato: '#ff6347',
|
|
146
|
+
turquoise: '#40e0d0',
|
|
147
|
+
violet: '#ee82ee',
|
|
148
|
+
wheat: '#f5deb3',
|
|
149
|
+
white: '#ffffff',
|
|
150
|
+
whitesmoke: '#f5f5f5',
|
|
151
|
+
yellow: '#ffff00',
|
|
152
|
+
yellowgreen: '#9acd32',
|
|
153
|
+
}
|