@zombie-mermaid/ascii-renderer 3.0.0 → 3.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/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2484 -1467
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/ascii-bt-label-order.test.ts +97 -0
- package/src/__tests__/ascii-cluster-exit-anchoring-1148.test.ts +774 -0
- package/src/__tests__/ascii-cluster-exit-lanes-1182.test.ts +375 -0
- package/src/__tests__/ascii-fan-in-then-fan-out-upstream-63.test.ts +78 -0
- package/src/__tests__/ascii-fan-out-upstream-113.test.ts +101 -0
- package/src/__tests__/ascii-role-colors.test.ts +328 -0
- package/src/__tests__/ascii-subgraph-back-edge-1197.test.ts +212 -0
- package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +4 -2
- package/src/__tests__/ascii-subgraph-title-edge-1222.test.ts +106 -0
- package/src/__tests__/c4-ascii.test.ts +311 -0
- package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +21 -21
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +1 -1
- package/src/__tests__/xychart-ascii-ticks.test.ts +34 -0
- package/src/ansi.ts +16 -2
- package/src/architecture.ts +33 -0
- package/src/c4-diagram.ts +1087 -0
- package/src/canvas.ts +78 -8
- package/src/cluster-boundary.ts +429 -0
- package/src/converter.ts +18 -2
- package/src/draw-arrows.ts +120 -18
- package/src/draw-boxes.ts +1 -0
- package/src/draw-subgraphs.ts +102 -2
- package/src/draw.ts +57 -3
- package/src/edge-cell-styles.ts +20 -1
- package/src/edge-routing.ts +77 -1
- package/src/flowchart.ts +28 -3
- package/src/frame-avoidance.ts +235 -0
- package/src/grid.ts +78 -19
- package/src/pathfinder.ts +22 -2
- package/src/registry.ts +8 -0
- package/src/types.ts +69 -0
- package/src/xychart.ts +24 -5
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { splitStatements } from '@zombie-mermaid/core'
|
|
3
|
+
import { parseC4Diagram } from '@zombie-mermaid/mermaid-parser'
|
|
4
|
+
import { renderMermaidASCII } from '../../../../src/index.ts'
|
|
5
|
+
import { rankElements } from '../c4-diagram.ts'
|
|
6
|
+
|
|
7
|
+
function ranks(src: string): Record<string, number> {
|
|
8
|
+
const d = parseC4Diagram(splitStatements(src))
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
rankElements(
|
|
11
|
+
d.elements.map((e) => e.alias),
|
|
12
|
+
d.relationships,
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** [column, row] of the first occurrence of `needle`. */
|
|
18
|
+
function find(out: string, needle: string): [number, number] {
|
|
19
|
+
const lines = out.split('\n')
|
|
20
|
+
for (const [row, line] of lines.entries()) {
|
|
21
|
+
const col = line.indexOf(needle)
|
|
22
|
+
if (col >= 0) return [col, row]
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`"${needle}" not found in:\n${out}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('rankElements', () => {
|
|
28
|
+
it('ranks along relationships and lets Rel_U reverse them', () => {
|
|
29
|
+
expect(
|
|
30
|
+
ranks(`C4Context
|
|
31
|
+
System(a, "A")
|
|
32
|
+
System(b, "B")
|
|
33
|
+
System(c, "C")
|
|
34
|
+
Rel(a, b, "x")
|
|
35
|
+
Rel_U(b, c, "up")`),
|
|
36
|
+
).toEqual({ a: 0, b: 1, c: 0 })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('puts Rel_R / Rel_L pairs in one rank', () => {
|
|
40
|
+
expect(
|
|
41
|
+
ranks(`C4Context
|
|
42
|
+
System(a, "A")
|
|
43
|
+
System(b, "B")
|
|
44
|
+
System(c, "C")
|
|
45
|
+
Rel(a, b, "x")
|
|
46
|
+
Rel_R(b, c, "side")`),
|
|
47
|
+
).toEqual({ a: 0, b: 1, c: 1 })
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('survives cycles', () => {
|
|
51
|
+
const r = ranks(`C4Context
|
|
52
|
+
System(a, "A")
|
|
53
|
+
System(b, "B")
|
|
54
|
+
Rel(a, b, "x")
|
|
55
|
+
Rel(b, a, "y")`)
|
|
56
|
+
expect(r.a).toBe(0)
|
|
57
|
+
expect(r.b).toBe(1)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('ignores relationships to unknown or self endpoints', () => {
|
|
61
|
+
expect(
|
|
62
|
+
ranks('C4Context\nSystem(a, "A")\nRel(a, a, "loop")\nRel_R(a, a, "l")'),
|
|
63
|
+
).toEqual({ a: 0 })
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('C4 ASCII rendering', () => {
|
|
68
|
+
const SRC = `C4Container
|
|
69
|
+
title Shop
|
|
70
|
+
Person(u, "User")
|
|
71
|
+
System_Boundary(sys, "Shop") {
|
|
72
|
+
Container(web, "Web App", "React", "Storefront")
|
|
73
|
+
ContainerDb(db, "Database", "PostgreSQL")
|
|
74
|
+
ContainerQueue(q, "Events", "Kafka")
|
|
75
|
+
}
|
|
76
|
+
System_Ext(pay, "Payments")
|
|
77
|
+
Rel(u, web, "Browses", "HTTPS")
|
|
78
|
+
Rel(web, db, "Reads", "SQL")
|
|
79
|
+
BiRel(web, q, "Publishes")
|
|
80
|
+
Rel(web, pay, "Charges")`
|
|
81
|
+
|
|
82
|
+
it('draws a person glyph above the name', () => {
|
|
83
|
+
const out = renderMermaidASCII(SRC)
|
|
84
|
+
const [, glyphRow] = find(out, '/|\\')
|
|
85
|
+
const [, nameRow] = find(out, 'User')
|
|
86
|
+
expect(glyphRow).toBeLessThan(nameRow)
|
|
87
|
+
expect(renderMermaidASCII('C4Context\nSystem(s, "S")')).not.toContain(
|
|
88
|
+
'/|\\',
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('draws the diagram title, boundary title, type lines and technology text', () => {
|
|
93
|
+
const out = renderMermaidASCII(SRC)
|
|
94
|
+
for (const t of [
|
|
95
|
+
'Shop',
|
|
96
|
+
'[Person]',
|
|
97
|
+
'[Container: React]',
|
|
98
|
+
'[Container: PostgreSQL]',
|
|
99
|
+
'[Software System]',
|
|
100
|
+
'[HTTPS]',
|
|
101
|
+
'[SQL]',
|
|
102
|
+
'Storefront',
|
|
103
|
+
]) {
|
|
104
|
+
expect(out).toContain(t)
|
|
105
|
+
}
|
|
106
|
+
// The diagram title is the first text row, above everything else.
|
|
107
|
+
expect(find(out, 'Shop')[1]).toBeLessThan(find(out, 'User')[1])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('flows top to bottom and BT reverses the rows', () => {
|
|
111
|
+
const src = 'C4Context\nSystem(a, "Aaa")\nSystem(b, "Bbb")\nRel(a, b, "x")'
|
|
112
|
+
const td = renderMermaidASCII(src)
|
|
113
|
+
expect(find(td, 'Aaa')[1]).toBeLessThan(find(td, 'Bbb')[1])
|
|
114
|
+
const bt = renderMermaidASCII(src, { direction: 'BT' })
|
|
115
|
+
expect(find(bt, 'Bbb')[1]).toBeLessThan(find(bt, 'Aaa')[1])
|
|
116
|
+
// Arrowhead points at the target in both.
|
|
117
|
+
expect(td).toContain('▼')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('honors Rel_R: same row, source on the left, label between', () => {
|
|
121
|
+
const out = renderMermaidASCII(
|
|
122
|
+
'C4Context\nSystem(a, "Aaa")\nSystem(b, "Bbb")\nRel_R(a, b, "sends to")',
|
|
123
|
+
)
|
|
124
|
+
const [ax, ay] = find(out, 'Aaa')
|
|
125
|
+
const [bx, by] = find(out, 'Bbb')
|
|
126
|
+
expect(ay).toBe(by)
|
|
127
|
+
expect(ax).toBeLessThan(bx)
|
|
128
|
+
const [lx] = find(out, 'sends to')
|
|
129
|
+
expect(lx).toBeGreaterThan(ax)
|
|
130
|
+
expect(lx).toBeLessThan(bx)
|
|
131
|
+
expect(out).toContain('►')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('honors Rel_L and Rel_U', () => {
|
|
135
|
+
const l = renderMermaidASCII(
|
|
136
|
+
'C4Context\nSystem(a, "Aaa")\nSystem(b, "Bbb")\nRel_L(a, b, "x")',
|
|
137
|
+
)
|
|
138
|
+
expect(find(l, 'Bbb')[0]).toBeLessThan(find(l, 'Aaa')[0])
|
|
139
|
+
const u = renderMermaidASCII(
|
|
140
|
+
'C4Context\nSystem(a, "Aaa")\nSystem(b, "Bbb")\nRel_U(a, b, "x")',
|
|
141
|
+
)
|
|
142
|
+
expect(find(u, 'Bbb')[1]).toBeLessThan(find(u, 'Aaa')[1])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('draws BiRel arrowheads at both ends', () => {
|
|
146
|
+
const out = renderMermaidASCII(
|
|
147
|
+
'C4Context\nSystem(a, "Aaa")\nSystem(b, "Bbb")\nBiRel(a, b, "x")',
|
|
148
|
+
)
|
|
149
|
+
expect(out).toContain('▼')
|
|
150
|
+
expect(out).toContain('▲')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('uses only ASCII characters with useAscii', () => {
|
|
154
|
+
const out = renderMermaidASCII(SRC, { useAscii: true })
|
|
155
|
+
expect(out).toMatch(/^[\x00-\x7f]*$/)
|
|
156
|
+
expect(out).toContain('/|\\')
|
|
157
|
+
expect(out).toContain('v')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('marks external elements with a dashed border and queues with parentheses', () => {
|
|
161
|
+
const out = renderMermaidASCII(SRC)
|
|
162
|
+
expect(out).toContain('┄')
|
|
163
|
+
expect(out).toMatch(/\( +Events +\)/)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('draws a cylinder cap for databases', () => {
|
|
167
|
+
const out = renderMermaidASCII(SRC)
|
|
168
|
+
expect(out).toMatch(/├─+┤/)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('keeps every element box intact (no route runs through one)', () => {
|
|
172
|
+
const out = renderMermaidASCII(SRC)
|
|
173
|
+
const lines = out.split('\n')
|
|
174
|
+
for (const l of lines.filter((l) => l.includes('Web App'))) {
|
|
175
|
+
expect(l).toMatch(/│ +Web App +│/)
|
|
176
|
+
}
|
|
177
|
+
for (const name of ['Database', 'Events', 'Payments']) {
|
|
178
|
+
const row = lines.find((l) => l.includes(name))!
|
|
179
|
+
expect(row).toMatch(new RegExp(`[│(┆] +${name} +[│)┆]`))
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('nests boundaries and routes to a boundary', () => {
|
|
184
|
+
const out = renderMermaidASCII(`C4Deployment
|
|
185
|
+
Deployment_Node(dc, "DC", "Ubuntu") {
|
|
186
|
+
Deployment_Node(k, "K8s", "Kubernetes") {
|
|
187
|
+
Container(app, "App", "Java")
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
Deployment_Node(empty, "Empty", "x")
|
|
191
|
+
ContainerDb(db, "DB", "Pg")
|
|
192
|
+
Rel(app, db, "reads")
|
|
193
|
+
Rel(db, empty, "to empty")`)
|
|
194
|
+
expect(out).toContain('[Ubuntu]')
|
|
195
|
+
expect(out).toContain('[Kubernetes]')
|
|
196
|
+
const [, dcRow] = find(out, 'DC')
|
|
197
|
+
const [, k8sRow] = find(out, 'K8s')
|
|
198
|
+
const [, appRow] = find(out, 'App')
|
|
199
|
+
expect(dcRow).toBeLessThan(k8sRow)
|
|
200
|
+
expect(k8sRow).toBeLessThan(appRow)
|
|
201
|
+
// The empty node ranks below the database that points at it.
|
|
202
|
+
expect(find(out, 'DB')[1]).toBeLessThan(find(out, 'Empty')[1])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('renders a lone element, empty input and self relationships', () => {
|
|
206
|
+
expect(renderMermaidASCII('C4Context\nSystem(a, "Solo")')).toContain('Solo')
|
|
207
|
+
expect(renderMermaidASCII('C4Context')).toBe('')
|
|
208
|
+
expect(
|
|
209
|
+
renderMermaidASCII('C4Context\nSystem(a, "A")\nRel(a, a, "self")'),
|
|
210
|
+
).toContain('A')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('wraps long names and descriptions', () => {
|
|
214
|
+
const out = renderMermaidASCII(
|
|
215
|
+
'C4Context\nSystem(a, "A very long system name that must wrap", "and an equally long description that also wraps")',
|
|
216
|
+
)
|
|
217
|
+
const longest = Math.max(...out.split('\n').map((l) => l.length))
|
|
218
|
+
expect(longest).toBeLessThan(40)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('emits ANSI color when asked', () => {
|
|
222
|
+
const out = renderMermaidASCII(SRC, { colorMode: 'ansi16' })
|
|
223
|
+
expect(out).toMatch(/\x1b\[/)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('is deterministic', () => {
|
|
227
|
+
expect(renderMermaidASCII(SRC)).toBe(renderMermaidASCII(SRC))
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('relationship labels stay readable', () => {
|
|
232
|
+
/** The rows between the box holding `upper` and the next box below it. */
|
|
233
|
+
function between(out: string, upper: string): string[] {
|
|
234
|
+
const lines = out.split('\n')
|
|
235
|
+
const top = lines.findIndex((l) => l.includes(upper))
|
|
236
|
+
const bottom = lines.findIndex((l, i) => i > top && /└─/.test(l))
|
|
237
|
+
const next = lines.findIndex((l, i) => i > bottom && /┌─/.test(l))
|
|
238
|
+
return lines.slice(bottom + 1, next)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const DYNAMIC = `C4Dynamic
|
|
242
|
+
Person(u, "User")
|
|
243
|
+
System(a, "App")
|
|
244
|
+
System(b, "Backend")
|
|
245
|
+
Rel(u, a, "Clicks")
|
|
246
|
+
Rel(a, b, "Requests")
|
|
247
|
+
Rel_Back(b, a, "Responds")
|
|
248
|
+
BiRel(u, b, "Streams")`
|
|
249
|
+
|
|
250
|
+
it('gives every label on a crowded gap its own room and leaves the routes whole', () => {
|
|
251
|
+
const out = renderMermaidASCII(DYNAMIC)
|
|
252
|
+
for (const word of [
|
|
253
|
+
'1:',
|
|
254
|
+
'Clicks',
|
|
255
|
+
'2:',
|
|
256
|
+
'Requests',
|
|
257
|
+
'3:',
|
|
258
|
+
'Responds',
|
|
259
|
+
'4:',
|
|
260
|
+
'Streams',
|
|
261
|
+
]) {
|
|
262
|
+
expect(out, word).toContain(word)
|
|
263
|
+
}
|
|
264
|
+
// Two routes run side by side from App to Backend; a label drawn over
|
|
265
|
+
// either would break the pair on its row (arrowhead rows excepted).
|
|
266
|
+
const gap = between(out, 'App')
|
|
267
|
+
expect(gap.length).toBeGreaterThanOrEqual(6)
|
|
268
|
+
for (const row of gap.filter((l) => !/[▼▲]/.test(l))) {
|
|
269
|
+
expect(row, row).toContain('││')
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('wraps a long label beside its route instead of drawing it over the route', () => {
|
|
274
|
+
const out = renderMermaidASCII(`C4Container
|
|
275
|
+
Container(w, "Web", "React")
|
|
276
|
+
Container(a, "API", "Node")
|
|
277
|
+
Rel(w, a, "Calls the API with a long label", "JSON/HTTPS")`)
|
|
278
|
+
for (const word of [
|
|
279
|
+
'Calls',
|
|
280
|
+
'the',
|
|
281
|
+
'API',
|
|
282
|
+
'with',
|
|
283
|
+
'a',
|
|
284
|
+
'long',
|
|
285
|
+
'label',
|
|
286
|
+
'[JSON/HTTPS]',
|
|
287
|
+
]) {
|
|
288
|
+
expect(out, word).toContain(word)
|
|
289
|
+
}
|
|
290
|
+
// The route is one unbroken vertical line between the boxes: its column
|
|
291
|
+
// (where the arrowhead lands) holds a line or the arrowhead on every row.
|
|
292
|
+
const gap = between(out, 'Web').filter((l) => l.trim() !== '')
|
|
293
|
+
const col = gap.map((l) => l.indexOf('▼')).find((c) => c >= 0)!
|
|
294
|
+
expect(col).toBeGreaterThanOrEqual(0)
|
|
295
|
+
for (const row of gap) expect('│▼').toContain(row[col] ?? ' ')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('grows a gap with the labels that cross it', () => {
|
|
299
|
+
const rows = (labels: number): number => {
|
|
300
|
+
const rels = Array.from(
|
|
301
|
+
{ length: labels },
|
|
302
|
+
(_, i) => `Rel(a, b, "label ${i}")`,
|
|
303
|
+
)
|
|
304
|
+
const out = renderMermaidASCII(
|
|
305
|
+
`C4Context\n System(a, "A")\n System(b, "B")\n ${rels.join('\n ')}`,
|
|
306
|
+
)
|
|
307
|
+
return between(out, 'A').length
|
|
308
|
+
}
|
|
309
|
+
expect(rows(4)).toBeGreaterThan(rows(1))
|
|
310
|
+
})
|
|
311
|
+
})
|
|
@@ -16,27 +16,27 @@ B --> Y
|
|
|
16
16
|
|
|
|
17
17
|
|
|
|
18
18
|
|
|
|
19
|
-
|
|
20
|
-
|LR
|
|
21
|
-
| |
|
|
22
|
-
| v
|
|
23
|
-
| +---+
|
|
24
|
-
| | |
|
|
25
|
-
| | A |
|
|
26
|
-
| | |
|
|
27
|
-
| +-+-+
|
|
28
|
-
| |
|
|
29
|
-
| |
|
|
30
|
-
| |
|
|
31
|
-
| |
|
|
32
|
-
| v
|
|
33
|
-
| +---+
|
|
34
|
-
| | |
|
|
35
|
-
| | B |
|
|
36
|
-
| | |
|
|
37
|
-
| +-+-+
|
|
38
|
-
| |
|
|
39
|
-
|
|
19
|
+
+---|-----+
|
|
20
|
+
| LR|Group|
|
|
21
|
+
| | |
|
|
22
|
+
| v |
|
|
23
|
+
| +---+ |
|
|
24
|
+
| | | |
|
|
25
|
+
| | A | |
|
|
26
|
+
| | | |
|
|
27
|
+
| +-+-+ |
|
|
28
|
+
| | |
|
|
29
|
+
| | |
|
|
30
|
+
| | |
|
|
31
|
+
| | |
|
|
32
|
+
| v |
|
|
33
|
+
| +---+ |
|
|
34
|
+
| | | |
|
|
35
|
+
| | B | |
|
|
36
|
+
| | | |
|
|
37
|
+
| +-+-+ |
|
|
38
|
+
| | |
|
|
39
|
+
+---|-----+
|
|
40
40
|
|
|
|
41
41
|
|
|
|
42
42
|
v
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value-axis tick labels for narrow and degenerate ranges (#1234).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest'
|
|
5
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
6
|
+
|
|
7
|
+
function render(text: string): string {
|
|
8
|
+
return renderMermaidASCII(text, { colorMode: 'none' })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('xychart ASCII – narrow-range value-axis labels', () => {
|
|
12
|
+
it('labels a 99-102 range with fractional ticks, not repeated integers', () => {
|
|
13
|
+
const result = render(`xychart-beta
|
|
14
|
+
x-axis [A, B, C, D]
|
|
15
|
+
bar [100, 102, 99, 101]`)
|
|
16
|
+
expect(result).toContain('99.5')
|
|
17
|
+
expect(result).toContain('100.5')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('renders a single tick label for a zero-width explicit range', () => {
|
|
21
|
+
const result = render(`xychart-beta
|
|
22
|
+
x-axis [A]
|
|
23
|
+
y-axis 5 --> 5
|
|
24
|
+
bar [5]`)
|
|
25
|
+
expect(result).toContain('5')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('labels the value axis of a horizontal chart with fractional ticks', () => {
|
|
29
|
+
const result = render(`xychart-beta horizontal
|
|
30
|
+
x-axis [A, B]
|
|
31
|
+
bar [100, 102]`)
|
|
32
|
+
expect(result).toMatch(/\d\.\d/)
|
|
33
|
+
})
|
|
34
|
+
})
|
package/src/ansi.ts
CHANGED
|
@@ -253,9 +253,23 @@ function escapeHtml(text: string): string {
|
|
|
253
253
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
/**
|
|
256
|
+
/** A run made only of full blocks (U+2588), as drawn by bar and area fills. */
|
|
257
|
+
const SOLID_BLOCK_RUN = /^█+$/
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Wrap text in a <span> with an inline color style. A run of full blocks also
|
|
261
|
+
* gets a background of the same color: a browser draws each `█` as an
|
|
262
|
+
* anti-aliased glyph, and where a cell edge falls inside a device pixel (any
|
|
263
|
+
* fractional display scale, e.g. 125% or 150%) the two neighbouring glyphs
|
|
264
|
+
* each cover only part of that pixel and composite to 75-81% of the fill,
|
|
265
|
+
* leaving a hairline seam between cells. A background is snapped to whole
|
|
266
|
+
* device pixels, so the glyph edges land on a solid fill instead.
|
|
267
|
+
*/
|
|
257
268
|
function htmlSpan(hex: string, text: string): string {
|
|
258
|
-
|
|
269
|
+
const style = SOLID_BLOCK_RUN.test(text)
|
|
270
|
+
? `color:${hex};background:${hex}`
|
|
271
|
+
: `color:${hex}`
|
|
272
|
+
return `<span style="${style}">${escapeHtml(text)}</span>`
|
|
259
273
|
}
|
|
260
274
|
|
|
261
275
|
// ============================================================================
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — architecture diagram
|
|
3
|
+
//
|
|
4
|
+
// Lowers the parsed diagram to the flowchart model (`architectureToGraph`)
|
|
5
|
+
// and reuses the flowchart grid layout + drawing pipeline.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
import { splitStatements, withDirectionOverride } from '@zombie-mermaid/core'
|
|
9
|
+
import {
|
|
10
|
+
parseArchitecture,
|
|
11
|
+
architectureToGraph,
|
|
12
|
+
} from '@zombie-mermaid/mermaid-parser'
|
|
13
|
+
import { renderGraphAscii } from './flowchart.ts'
|
|
14
|
+
import type { FlowchartAsciiExtras } from './flowchart.ts'
|
|
15
|
+
import type { AsciiConfig, AsciiTheme, ColorMode } from './types.ts'
|
|
16
|
+
|
|
17
|
+
/** Render an `architecture-beta` diagram to ASCII/Unicode text art. */
|
|
18
|
+
export function renderArchitectureAscii(
|
|
19
|
+
text: string,
|
|
20
|
+
config: AsciiConfig,
|
|
21
|
+
colorMode: ColorMode,
|
|
22
|
+
theme: AsciiTheme,
|
|
23
|
+
extras: FlowchartAsciiExtras = {},
|
|
24
|
+
): string {
|
|
25
|
+
const graph = architectureToGraph(parseArchitecture(splitStatements(text)))
|
|
26
|
+
return renderGraphAscii(
|
|
27
|
+
withDirectionOverride(graph, extras.direction),
|
|
28
|
+
config,
|
|
29
|
+
colorMode,
|
|
30
|
+
theme,
|
|
31
|
+
extras,
|
|
32
|
+
)
|
|
33
|
+
}
|