@yassimba/pi-loom-mermaid 0.4.0 → 0.4.1
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/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `architecture-beta`: services and junctions inside nested groups.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* preserves endpoints and arrowheads while leaving port placement to it.
|
|
4
|
+
* Built-in icons use stable Unicode stand-ins; custom Iconify names remain
|
|
5
|
+
* visible as text because a terminal cannot draw their SVGs.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import { Graph, MAX_GROUP_DEPTH, MAX_GROUPS } from '../graph.ts'
|
|
8
|
+
import { Graph, MAX_GROUP_DEPTH, MAX_GROUPS, type PortSide } from '../graph.ts'
|
|
10
9
|
import { cleanLabel } from '../labels.ts'
|
|
11
10
|
import { layoutFlowchart, layoutGrouped } from '../graph-render.ts'
|
|
12
11
|
import type { Diagram } from '../registry.ts'
|
|
@@ -27,6 +26,14 @@ export const architecture: Diagram = {
|
|
|
27
26
|
const DECLARATION = /^(group|service)\s+([^\s()[\]:{}]+)\(([^)]*)\)\[([^\]]*)\](?:\s+in\s+([^\s]+))?$/i
|
|
28
27
|
const JUNCTION = /^junction\s+([^\s:{}]+)(?:\s+in\s+([^\s]+))?$/i
|
|
29
28
|
const EDGE = /^([^\s:{}]+)(?:\{group\})?:([TBLR])\s*(<)?--(>)?\s*([TBLR]):([^\s:{}]+)(?:\{group\})?$/i
|
|
29
|
+
const SIDES: Record<string, PortSide> = { T: 'top', B: 'bottom', L: 'left', R: 'right' }
|
|
30
|
+
const ICONS: Record<string, string> = {
|
|
31
|
+
cloud: '☁',
|
|
32
|
+
database: '◉',
|
|
33
|
+
disk: '▰',
|
|
34
|
+
internet: '◎',
|
|
35
|
+
server: '▣',
|
|
36
|
+
}
|
|
30
37
|
|
|
31
38
|
function parseArchitecture(src: string): Graph | null {
|
|
32
39
|
const statements = statementsOf(src)
|
|
@@ -43,7 +50,7 @@ function parseArchitecture(src: string): Graph | null {
|
|
|
43
50
|
const edge = st.match(EDGE)
|
|
44
51
|
|
|
45
52
|
if (declaration) {
|
|
46
|
-
const [, kind, id, , rawLabel, parentId] = declaration
|
|
53
|
+
const [, kind, id, icon, rawLabel, parentId] = declaration
|
|
47
54
|
const parent = parentId === undefined ? null : groupIndex.get(parentId)
|
|
48
55
|
if (parentId !== undefined && parent === undefined) {
|
|
49
56
|
graph.drop(st)
|
|
@@ -57,10 +64,10 @@ function parseArchitecture(src: string): Graph | null {
|
|
|
57
64
|
graph.truncated ??= `subgraph cap (${MAX_GROUPS} groups, depth ${MAX_GROUP_DEPTH}) reached`
|
|
58
65
|
} else {
|
|
59
66
|
groupIndex.set(id, graph.groups.length)
|
|
60
|
-
graph.groups.push({ id, label:
|
|
67
|
+
graph.groups.push({ id, label: iconLabel(icon, rawLabel || id), parent: parent ?? null })
|
|
61
68
|
}
|
|
62
69
|
} else {
|
|
63
|
-
addNode(graph, id,
|
|
70
|
+
addNode(graph, id, iconLabel(icon, rawLabel || id), parent ?? null, 'rect')
|
|
64
71
|
}
|
|
65
72
|
} else if (junction) {
|
|
66
73
|
const [, id, parentId] = junction
|
|
@@ -68,7 +75,7 @@ function parseArchitecture(src: string): Graph | null {
|
|
|
68
75
|
if (parentId !== undefined && parent === undefined) graph.drop(st)
|
|
69
76
|
else addNode(graph, id, '•', parent ?? null, 'round')
|
|
70
77
|
} else if (edge) {
|
|
71
|
-
const [, fromId, , leftArrow, rightArrow, , toId] = edge
|
|
78
|
+
const [, fromId, fromPort, leftArrow, rightArrow, toPort, toId] = edge
|
|
72
79
|
const from = graph.index.get(fromId)
|
|
73
80
|
const to = graph.index.get(toId)
|
|
74
81
|
if (from === undefined || to === undefined) {
|
|
@@ -81,6 +88,8 @@ function parseArchitecture(src: string): Graph | null {
|
|
|
81
88
|
headFrom: leftArrow ? 'arrow' : 'none',
|
|
82
89
|
headTo: rightArrow ? 'arrow' : 'none',
|
|
83
90
|
line: 'solid',
|
|
91
|
+
fromSide: SIDES[fromPort.toUpperCase()],
|
|
92
|
+
toSide: SIDES[toPort.toUpperCase()],
|
|
84
93
|
})
|
|
85
94
|
}
|
|
86
95
|
} else if (firstWord(st).toLowerCase() !== 'title') {
|
|
@@ -96,6 +105,12 @@ function parseArchitecture(src: string): Graph | null {
|
|
|
96
105
|
return graph.nodes.length === 0 ? null : graph
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
function iconLabel(icon: string, rawLabel: string): string {
|
|
109
|
+
const name = cleanLabel(icon)
|
|
110
|
+
const mark = ICONS[name.toLowerCase()] ?? `[${name.split(':').at(-1)}]`
|
|
111
|
+
return `${mark} ${cleanLabel(rawLabel)}`
|
|
112
|
+
}
|
|
113
|
+
|
|
99
114
|
function addNode(
|
|
100
115
|
graph: Graph,
|
|
101
116
|
id: string,
|
|
@@ -26,6 +26,7 @@ export type Head =
|
|
|
26
26
|
| 'diamondOpen'
|
|
27
27
|
|
|
28
28
|
export type LineKind = 'solid' | 'dotted' | 'thick'
|
|
29
|
+
export type PortSide = 'top' | 'bottom' | 'left' | 'right'
|
|
29
30
|
|
|
30
31
|
type Dir = 'down' | 'up' | 'right' | 'left'
|
|
31
32
|
|
|
@@ -66,6 +67,9 @@ export interface Edge {
|
|
|
66
67
|
headTo: Head
|
|
67
68
|
headFrom: Head
|
|
68
69
|
line: LineKind
|
|
70
|
+
/** Exact attachment sides requested by diagrams such as `architecture-beta`. */
|
|
71
|
+
fromSide?: PortSide
|
|
72
|
+
toSide?: PortSide
|
|
69
73
|
/**
|
|
70
74
|
* Set on an end that stands for a subgraph frame: the inner node the
|
|
71
75
|
* author actually named, as a box in the frame's sub-canvas coordinates.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Canvas } from './canvas.ts'
|
|
14
|
-
import type { Anchor, Edge, LineKind } from './graph.ts'
|
|
14
|
+
import type { Anchor, Edge, LineKind, PortSide } from './graph.ts'
|
|
15
15
|
import type { Graph } from './graph.ts'
|
|
16
16
|
import { fitLabel, type Limits, wrapLabel } from './labels.ts'
|
|
17
17
|
import { brandesKoepf, type LayeredGraph } from './placement.ts'
|
|
@@ -117,7 +117,7 @@ interface Port {
|
|
|
117
117
|
wanted: number
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
type Side =
|
|
120
|
+
type Side = PortSide
|
|
121
121
|
|
|
122
122
|
function framePort(
|
|
123
123
|
sub: Canvas,
|
|
@@ -1690,7 +1690,9 @@ function placeLr(
|
|
|
1690
1690
|
})
|
|
1691
1691
|
}
|
|
1692
1692
|
const forward = (e: Edge): boolean => e.from !== e.to && ranks[e.to] > ranks[e.from]
|
|
1693
|
-
|
|
1693
|
+
const requestedSides = (e: Edge): [Side, Side] | null =>
|
|
1694
|
+
e.fromSide !== undefined && e.toSide !== undefined ? [e.fromSide, e.toSide] : null
|
|
1695
|
+
let ends = resolve((i) => requestedSides(graph.edges[i]) ?? (forward(graph.edges[i]) ? ['right', 'left'] : null))
|
|
1694
1696
|
// A node whose incoming edges all leave their frames at one row off the
|
|
1695
1697
|
// frame's centre sits that far off its own aligned position, so the
|
|
1696
1698
|
// edges run straight rather than jog to it (`[*]` after a composite
|
|
@@ -1753,6 +1755,8 @@ function placeLr(
|
|
|
1753
1755
|
})
|
|
1754
1756
|
ends = resolve((i) => {
|
|
1755
1757
|
const e = graph.edges[i]
|
|
1758
|
+
const requested = requestedSides(e)
|
|
1759
|
+
if (requested !== null) return requested
|
|
1756
1760
|
if (e.from === e.to) return null
|
|
1757
1761
|
if (ranks[e.to] === ranks[e.from] + 1 || edgeStraight[i]) return ['right', 'left']
|
|
1758
1762
|
return ranks[e.to] < ranks[e.from] ? ['top', 'top'] : ['bottom', 'bottom']
|
|
@@ -1871,14 +1875,40 @@ function placeLr(
|
|
|
1871
1875
|
const [from, to] = endsOf(i)
|
|
1872
1876
|
const through = ends[i].flatMap((p, k) => (p === null ? [] : portAt(p, placed[k === 0 ? edge.from : edge.to]).through))
|
|
1873
1877
|
const route =
|
|
1874
|
-
|
|
1875
|
-
?
|
|
1876
|
-
: to.rank
|
|
1877
|
-
?
|
|
1878
|
-
:
|
|
1878
|
+
edge.fromSide !== undefined && edge.toSide !== undefined
|
|
1879
|
+
? portRoute(from, to, edge.fromSide, edge.toSide)
|
|
1880
|
+
: to.rank === from.rank + 1
|
|
1881
|
+
? forwardRouteLr(from, to, edge, bandEnd[from.rank] + 1 + edgeBus[i], max, bundleOf(i) !== undefined)
|
|
1882
|
+
: to.rank > from.rank && edgeStraight[i]
|
|
1883
|
+
? skipRouteLr(from, to, edge, skipRoute[i], max)
|
|
1884
|
+
: laneRoute(from, to, edge, onTop(i) ? edgeLane[i] : laneBase + edgeLane[i], max, onTop(i), laneEntry(i, from, to))
|
|
1879
1885
|
return through.length === 0 ? route : { ...route, through: [...(route.through ?? []), ...through] }
|
|
1880
1886
|
})
|
|
1881
|
-
|
|
1887
|
+
|
|
1888
|
+
if (!graph.edges.some((e) => e.fromSide !== undefined)) return { canvasW, canvasH, routes }
|
|
1889
|
+
// Exact side ports may face out of the outermost box. Two cells of margin
|
|
1890
|
+
// keep their first/last segments on-canvas at every nested group level.
|
|
1891
|
+
for (const p of placed) {
|
|
1892
|
+
p.x += 2
|
|
1893
|
+
p.y += 2
|
|
1894
|
+
p.cx += 2
|
|
1895
|
+
p.cy += 2
|
|
1896
|
+
}
|
|
1897
|
+
for (const route of routes) {
|
|
1898
|
+
route.points = route.points.map(([x, y]) => [x + 2, y + 2])
|
|
1899
|
+
route.labels = route.labels.map((label) => ({ ...label, row: label.row + 2, x: label.x + 2 }))
|
|
1900
|
+
if (route.laneLabel !== undefined) {
|
|
1901
|
+
route.laneLabel = {
|
|
1902
|
+
...route.laneLabel,
|
|
1903
|
+
y: route.laneLabel.y + 2,
|
|
1904
|
+
lo: route.laneLabel.lo + 2,
|
|
1905
|
+
hi: route.laneLabel.hi + 2,
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
if (route.through !== undefined)
|
|
1909
|
+
route.through = route.through.map(([x, y, kind]) => [x + 2, y + 2, kind])
|
|
1910
|
+
}
|
|
1911
|
+
return { canvasW: canvasW + 4, canvasH: canvasH + 4, routes }
|
|
1882
1912
|
}
|
|
1883
1913
|
|
|
1884
1914
|
// -------------------------------------------------------------------- canvas
|
|
@@ -2213,6 +2243,50 @@ function chainRoute(
|
|
|
2213
2243
|
return { points, labels: chainLabel(edge, headRow, entryX, labelLeft, labelAt, max) }
|
|
2214
2244
|
}
|
|
2215
2245
|
|
|
2246
|
+
/** An orthogonal route that leaves and enters the exact requested box sides. */
|
|
2247
|
+
function portRoute(from: Placed, to: Placed, fromSide: PortSide, toSide: PortSide): Route {
|
|
2248
|
+
const normal = (side: PortSide): [number, number] =>
|
|
2249
|
+
side === 'left' ? [-1, 0] : side === 'right' ? [1, 0] : side === 'top' ? [0, -1] : [0, 1]
|
|
2250
|
+
const border = (p: Placed, side: PortSide): [number, number] =>
|
|
2251
|
+
side === 'left'
|
|
2252
|
+
? [p.x, p.cy]
|
|
2253
|
+
: side === 'right'
|
|
2254
|
+
? [p.x + p.w - 1, p.cy]
|
|
2255
|
+
: side === 'top'
|
|
2256
|
+
? [p.cx, p.y]
|
|
2257
|
+
: [p.cx, p.y + p.h - 1]
|
|
2258
|
+
const move = ([x, y]: [number, number], [dx, dy]: [number, number]): [number, number] => [x + dx, y + dy]
|
|
2259
|
+
|
|
2260
|
+
const fromNormal = normal(fromSide)
|
|
2261
|
+
const toNormal = normal(toSide)
|
|
2262
|
+
const start = border(from, fromSide)
|
|
2263
|
+
const startOut = move(start, fromNormal)
|
|
2264
|
+
const head = move(border(to, toSide), toNormal)
|
|
2265
|
+
const targetOut = move(head, toNormal)
|
|
2266
|
+
const fromHorizontal = fromNormal[0] !== 0
|
|
2267
|
+
const toHorizontal = toNormal[0] !== 0
|
|
2268
|
+
let middle: [number, number][]
|
|
2269
|
+
|
|
2270
|
+
if (fromHorizontal !== toHorizontal) {
|
|
2271
|
+
middle = [fromHorizontal ? [targetOut[0], startOut[1]] : [startOut[0], targetOut[1]]]
|
|
2272
|
+
} else if (fromHorizontal) {
|
|
2273
|
+
const direction = Math.sign(targetOut[0] - startOut[0])
|
|
2274
|
+
const direct = direction === fromNormal[0] && direction === -toNormal[0]
|
|
2275
|
+
const lane = Math.max(from.y + from.h, to.y + to.h) + 1
|
|
2276
|
+
middle = direct ? [] : [[startOut[0], lane], [targetOut[0], lane]]
|
|
2277
|
+
} else {
|
|
2278
|
+
const direction = Math.sign(targetOut[1] - startOut[1])
|
|
2279
|
+
const direct = direction === fromNormal[1] && direction === -toNormal[1]
|
|
2280
|
+
const lane = Math.max(from.x + from.w, to.x + to.w) + 1
|
|
2281
|
+
middle = direct ? [] : [[lane, startOut[1]], [lane, targetOut[1]]]
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
const points = [start, startOut, ...middle, targetOut, head].filter(
|
|
2285
|
+
([x, y], i, all) => i === 0 || x !== all[i - 1][0] || y !== all[i - 1][1],
|
|
2286
|
+
) as [number, number][]
|
|
2287
|
+
return { points, labels: [] }
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2216
2290
|
/**
|
|
2217
2291
|
* Adjacent ranks, left-to-right: out the right side, jog on the bus
|
|
2218
2292
|
* column. The verb keeps its usual spot above the line; cardinalities hug
|