@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,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry points for the layered diagrams (flowchart, state, class, ER):
|
|
3
|
+
* choose what each node box holds, lay the graph out, paint it, orient it.
|
|
4
|
+
* Subgraphs recurse: each becomes a framed box holding its own canvas.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Canvas } from './canvas.ts'
|
|
8
|
+
import type { Edge, Node } from './graph.ts'
|
|
9
|
+
import { Graph } from './graph.ts'
|
|
10
|
+
import type { Limits } from './labels.ts'
|
|
11
|
+
import { layout, type NodeExtra } from './layout.ts'
|
|
12
|
+
import { orient, paint } from './paint.ts'
|
|
13
|
+
|
|
14
|
+
/** A laid-out canvas, or `null` when the diagram is empty or over the cell cap. */
|
|
15
|
+
export type CanvasResult = Canvas | null
|
|
16
|
+
|
|
17
|
+
/** Flowchart and state diagrams: plain boxes, no extra content. */
|
|
18
|
+
export function layoutFlowchart(graph: Graph, limits: Limits): CanvasResult {
|
|
19
|
+
const extras: NodeExtra[] = graph.nodes.map(() => ({ kind: 'plain' }))
|
|
20
|
+
const canvas = layoutCanvas(graph, extras, limits)
|
|
21
|
+
return canvas && orient(canvas, graph)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Class and ER diagrams: boxes divided into title / attribute / method rows. */
|
|
25
|
+
export function layoutClass(graph: Graph, limits: Limits): CanvasResult {
|
|
26
|
+
const extras: NodeExtra[] = graph.nodes.map((node) => ({
|
|
27
|
+
kind: 'compartments',
|
|
28
|
+
sections: node.sections ?? [[node.label]],
|
|
29
|
+
}))
|
|
30
|
+
const canvas = layoutCanvas(graph, extras, limits)
|
|
31
|
+
return canvas && orient(canvas, graph)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// -------------------------------------------------------------------- groups
|
|
35
|
+
|
|
36
|
+
/** An endpoint inside a scope: a plain node or a (proxied) subgraph. */
|
|
37
|
+
interface ScopeItem {
|
|
38
|
+
group: boolean
|
|
39
|
+
i: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Lay out a flowchart that uses `subgraph`.
|
|
44
|
+
*
|
|
45
|
+
* Each subgraph becomes a framed box holding its own independently laid-out
|
|
46
|
+
* canvas. An edge is drawn in the innermost scope containing both endpoints;
|
|
47
|
+
* one crossing a subgraph boundary attaches to the frame instead of the node.
|
|
48
|
+
*/
|
|
49
|
+
export function layoutGrouped(graph: Graph, limits: Limits): CanvasResult {
|
|
50
|
+
// A node whose id matches a subgraph id stands in for that subgraph.
|
|
51
|
+
const proxy = new Map<number, number>()
|
|
52
|
+
graph.groups.forEach((g, gi) => {
|
|
53
|
+
const ni = graph.index.get(g.id)
|
|
54
|
+
if (ni !== undefined) proxy.set(ni, gi)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const groupChain = (g: number | null): number[] => {
|
|
58
|
+
const chain: number[] = []
|
|
59
|
+
let cur = g
|
|
60
|
+
while (cur !== null) {
|
|
61
|
+
chain.push(cur)
|
|
62
|
+
cur = graph.groups[cur].parent
|
|
63
|
+
}
|
|
64
|
+
return chain.reverse()
|
|
65
|
+
}
|
|
66
|
+
const endpoint = (n: number): { item: ScopeItem; chain: number[] } => {
|
|
67
|
+
const gi = proxy.get(n)
|
|
68
|
+
return gi === undefined
|
|
69
|
+
? { item: { group: false, i: n }, chain: groupChain(graph.nodeGroup[n]) }
|
|
70
|
+
: { item: { group: true, i: gi }, chain: groupChain(graph.groups[gi].parent) }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Edges bucketed by the scope that draws them; `null` is the top level. */
|
|
74
|
+
const scopeEdges = new Map<number | null, [ScopeItem, ScopeItem, number][]>()
|
|
75
|
+
const referenced = new Array<boolean>(graph.groups.length).fill(false)
|
|
76
|
+
graph.edges.forEach((e, ei) => {
|
|
77
|
+
const f = endpoint(e.from)
|
|
78
|
+
const t = endpoint(e.to)
|
|
79
|
+
let k = 0
|
|
80
|
+
while (k < f.chain.length && k < t.chain.length && f.chain[k] === t.chain[k]) k++
|
|
81
|
+
const scope = k === 0 ? null : f.chain[k - 1]
|
|
82
|
+
const fItem = f.chain.length > k ? { group: true, i: f.chain[k] } : f.item
|
|
83
|
+
const tItem = t.chain.length > k ? { group: true, i: t.chain[k] } : t.item
|
|
84
|
+
for (const item of [fItem, tItem]) {
|
|
85
|
+
if (item.group) referenced[item.i] = true
|
|
86
|
+
}
|
|
87
|
+
const list = scopeEdges.get(scope)
|
|
88
|
+
if (list) list.push([fItem, tItem, ei])
|
|
89
|
+
else scopeEdges.set(scope, [[fItem, tItem, ei]])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const directNodes = new Map<number | null, number[]>()
|
|
93
|
+
graph.nodeGroup.forEach((g, ni) => {
|
|
94
|
+
if (proxy.has(ni)) return
|
|
95
|
+
const list = directNodes.get(g)
|
|
96
|
+
if (list) list.push(ni)
|
|
97
|
+
else directNodes.set(g, [ni])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Drop empty subgraphs, but keep any that an edge attaches to. Walked by
|
|
101
|
+
// the actual child relation: state `--` regions reparent earlier groups
|
|
102
|
+
// under later ones, so index order says nothing about depth.
|
|
103
|
+
const childGroups: number[][] = graph.groups.map(() => [])
|
|
104
|
+
graph.groups.forEach((g, gi) => {
|
|
105
|
+
if (g.parent !== null) childGroups[g.parent].push(gi)
|
|
106
|
+
})
|
|
107
|
+
const keep = new Array<boolean>(graph.groups.length).fill(false)
|
|
108
|
+
const visit = (gi: number): boolean => {
|
|
109
|
+
let kept = referenced[gi] || (directNodes.get(gi) ?? []).length > 0
|
|
110
|
+
for (const c of childGroups[gi]) if (visit(c)) kept = true
|
|
111
|
+
keep[gi] = kept
|
|
112
|
+
return kept
|
|
113
|
+
}
|
|
114
|
+
graph.groups.forEach((g, gi) => {
|
|
115
|
+
if (g.parent === null) visit(gi)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const canvas = buildScope(graph, null, scopeEdges, directNodes, keep, limits)
|
|
119
|
+
return canvas && orient(canvas, graph)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildScope(
|
|
123
|
+
graph: Graph,
|
|
124
|
+
scope: number | null,
|
|
125
|
+
scopeEdges: Map<number | null, [ScopeItem, ScopeItem, number][]>,
|
|
126
|
+
directNodes: Map<number | null, number[]>,
|
|
127
|
+
keep: boolean[],
|
|
128
|
+
limits: Limits,
|
|
129
|
+
): CanvasResult {
|
|
130
|
+
const items: ScopeItem[] = (directNodes.get(scope) ?? []).map((i) => ({ group: false, i }))
|
|
131
|
+
const childGroups = graph.groups
|
|
132
|
+
.map((_, gi) => gi)
|
|
133
|
+
.filter((gi) => graph.groups[gi].parent === scope && keep[gi])
|
|
134
|
+
items.push(...childGroups.map((i) => ({ group: true, i })))
|
|
135
|
+
|
|
136
|
+
if (items.length === 0) return new Canvas(1, 1)
|
|
137
|
+
|
|
138
|
+
const nodeAt = new Map<number, number>()
|
|
139
|
+
const groupAt = new Map<number, number>()
|
|
140
|
+
const nodes: Node[] = []
|
|
141
|
+
const extras: NodeExtra[] = []
|
|
142
|
+
for (const item of items) {
|
|
143
|
+
;(item.group ? groupAt : nodeAt).set(item.i, nodes.length)
|
|
144
|
+
if (!item.group) {
|
|
145
|
+
nodes.push({
|
|
146
|
+
label: graph.nodes[item.i].label,
|
|
147
|
+
shape: graph.nodes[item.i].shape,
|
|
148
|
+
classes: graph.nodes[item.i].classes,
|
|
149
|
+
href: graph.nodes[item.i].href,
|
|
150
|
+
})
|
|
151
|
+
extras.push({ kind: 'plain' })
|
|
152
|
+
} else {
|
|
153
|
+
const sub = buildScope(graph, item.i, scopeEdges, directNodes, keep, limits)
|
|
154
|
+
if (sub === null) return null
|
|
155
|
+
nodes.push({ label: graph.groups[item.i].label, shape: 'rect' })
|
|
156
|
+
extras.push({ kind: 'frame', sub })
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const edges: Edge[] = []
|
|
161
|
+
for (const [f, t, ei] of scopeEdges.get(scope) ?? []) {
|
|
162
|
+
const fi = (f.group ? groupAt : nodeAt).get(f.i)
|
|
163
|
+
const ti = (t.group ? groupAt : nodeAt).get(t.i)
|
|
164
|
+
if (fi === undefined || ti === undefined) continue
|
|
165
|
+
const e = graph.edges[ei]
|
|
166
|
+
const collapsed: Edge = {
|
|
167
|
+
from: fi,
|
|
168
|
+
to: ti,
|
|
169
|
+
label: e.label,
|
|
170
|
+
headTo: e.headTo,
|
|
171
|
+
headFrom: e.headFrom,
|
|
172
|
+
line: e.line,
|
|
173
|
+
}
|
|
174
|
+
// Edges from (or to) different nodes inside one frame collapse onto
|
|
175
|
+
// the frame and become indistinguishable; draw them once.
|
|
176
|
+
const twin = (a: Edge, b: Edge): boolean =>
|
|
177
|
+
a.from === b.from && a.to === b.to && a.label === b.label && a.headTo === b.headTo && a.headFrom === b.headFrom && a.line === b.line
|
|
178
|
+
if ((f.group || t.group) && edges.some((x) => twin(x, collapsed))) continue
|
|
179
|
+
edges.push(collapsed)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Layout only reads nodes/edges/dir, so a bare Graph carrying those is enough.
|
|
183
|
+
const synth = new Graph(graph.dir)
|
|
184
|
+
synth.nodes = nodes
|
|
185
|
+
synth.edges = edges
|
|
186
|
+
return layoutCanvas(synth, extras, limits)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Lay out and paint one scope. */
|
|
190
|
+
function layoutCanvas(graph: Graph, extras: NodeExtra[], limits: Limits): CanvasResult {
|
|
191
|
+
const lay = layout(graph, extras, limits)
|
|
192
|
+
return lay === null ? null : paint(graph, extras, lay)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ------------------------------------------------------------------- drawing
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared diagram model. Flowchart, state, class and ER sources all parse
|
|
3
|
+
* into a `Graph`; only sequence diagrams have their own model.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { asciiUpper } from './labels.ts'
|
|
7
|
+
|
|
8
|
+
/** Caps that keep layout bounded; exceeding one drops the diagram to fallback. */
|
|
9
|
+
export const MAX_NODES = 128
|
|
10
|
+
export const MAX_EDGES = 512
|
|
11
|
+
export const MAX_GROUPS = 24
|
|
12
|
+
export const MAX_GROUP_DEPTH = 6
|
|
13
|
+
/** Class members / ER attributes listed per box before eliding with `…`. */
|
|
14
|
+
export const MAX_MEMBERS = 8
|
|
15
|
+
|
|
16
|
+
export type Shape = 'rect' | 'round' | 'diamond'
|
|
17
|
+
|
|
18
|
+
/** Decoration at one end of an edge. */
|
|
19
|
+
export type Head =
|
|
20
|
+
| 'none'
|
|
21
|
+
| 'arrow'
|
|
22
|
+
| 'circle'
|
|
23
|
+
| 'cross'
|
|
24
|
+
| 'triangle'
|
|
25
|
+
| 'diamondFill'
|
|
26
|
+
| 'diamondOpen'
|
|
27
|
+
|
|
28
|
+
export type LineKind = 'solid' | 'dotted' | 'thick'
|
|
29
|
+
|
|
30
|
+
type Dir = 'down' | 'up' | 'right' | 'left'
|
|
31
|
+
|
|
32
|
+
export interface Node {
|
|
33
|
+
label: string
|
|
34
|
+
shape: Shape
|
|
35
|
+
/**
|
|
36
|
+
* Compartment content for class and ER boxes, pre-formatted by the parser:
|
|
37
|
+
* one string per row, one array per compartment (title, attributes,
|
|
38
|
+
* methods). Absent on plain nodes; layout draws whatever is here verbatim,
|
|
39
|
+
* separated by horizontal rules.
|
|
40
|
+
*/
|
|
41
|
+
sections?: string[][]
|
|
42
|
+
/**
|
|
43
|
+
* Author-assigned class names, from `:::name` or a `class A,B name`
|
|
44
|
+
* statement. The renderer never interprets them; the cells the node paints
|
|
45
|
+
* carry them out through `Span.classes`.
|
|
46
|
+
*/
|
|
47
|
+
classes?: string[]
|
|
48
|
+
/**
|
|
49
|
+
* Link target from a `click A "url"` / `link A "url"` statement, carried
|
|
50
|
+
* out through `Span.href` the way classes are; `toAnsi` emits it as an
|
|
51
|
+
* OSC 8 hyperlink.
|
|
52
|
+
*/
|
|
53
|
+
href?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Edge {
|
|
57
|
+
from: number
|
|
58
|
+
to: number
|
|
59
|
+
label: string | null
|
|
60
|
+
/**
|
|
61
|
+
* Cardinalities (ER crow's-foot, class multiplicities), painted at their
|
|
62
|
+
* own end of the edge so position says which side a number belongs to.
|
|
63
|
+
*/
|
|
64
|
+
cardFrom?: string
|
|
65
|
+
cardTo?: string
|
|
66
|
+
headTo: Head
|
|
67
|
+
headFrom: Head
|
|
68
|
+
line: LineKind
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Group {
|
|
72
|
+
id: string
|
|
73
|
+
label: string
|
|
74
|
+
parent: number | null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `LR`/`RL`/`BT` as written in a header or `direction` statement; else `down`. */
|
|
78
|
+
export function parseDir(token: string): Dir {
|
|
79
|
+
switch (asciiUpper(token)) {
|
|
80
|
+
case 'LR':
|
|
81
|
+
return 'right'
|
|
82
|
+
case 'RL':
|
|
83
|
+
return 'left'
|
|
84
|
+
case 'BT':
|
|
85
|
+
return 'up'
|
|
86
|
+
default:
|
|
87
|
+
return 'down'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class Graph {
|
|
92
|
+
nodes: Node[] = []
|
|
93
|
+
edges: Edge[] = []
|
|
94
|
+
index = new Map<string, number>()
|
|
95
|
+
groups: Group[] = []
|
|
96
|
+
/** Innermost subgraph each node was declared in, parallel to `nodes`. */
|
|
97
|
+
nodeGroup: (number | null)[] = []
|
|
98
|
+
curGroup: number | null = null
|
|
99
|
+
/**
|
|
100
|
+
* Set when a size cap was hit, naming the cap. The parser stops consuming
|
|
101
|
+
* statements and renders the prefix, warning about the truncation — a
|
|
102
|
+
* streamed diagram that outgrows a cap can never shrink back under it, so
|
|
103
|
+
* a stable truncated render beats flipping to the source box for good.
|
|
104
|
+
*/
|
|
105
|
+
truncated: string | null = null
|
|
106
|
+
/**
|
|
107
|
+
* Source the grammar could not read and dropped. Parsing is lenient in
|
|
108
|
+
* every grammar: a statement either contributes what parsed or is dropped
|
|
109
|
+
* and recorded here, so the reader can tell a clean diagram from one that
|
|
110
|
+
* is missing something they wrote.
|
|
111
|
+
*/
|
|
112
|
+
warnings: string[] = []
|
|
113
|
+
/** Parsed `classDef` declarations: name -> property map. */
|
|
114
|
+
classDefs: Record<string, Record<string, string>> = {}
|
|
115
|
+
dir: Dir = 'down'
|
|
116
|
+
|
|
117
|
+
constructor(dir: Dir = 'down') {
|
|
118
|
+
this.dir = dir
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Index of `id`, creating the node if new. A later declaration carrying a
|
|
123
|
+
* label overwrites the placeholder one an edge created. Returns `null` once
|
|
124
|
+
* `MAX_NODES` is reached, which truncates the parse.
|
|
125
|
+
*/
|
|
126
|
+
nodeIndex(id: string, label: string | null, shape: Shape): number | null {
|
|
127
|
+
const existing = this.index.get(id)
|
|
128
|
+
if (existing !== undefined) {
|
|
129
|
+
if (label !== null) {
|
|
130
|
+
this.nodes[existing].label = label
|
|
131
|
+
this.nodes[existing].shape = shape
|
|
132
|
+
}
|
|
133
|
+
return existing
|
|
134
|
+
}
|
|
135
|
+
if (this.nodes.length >= MAX_NODES) {
|
|
136
|
+
this.truncated ??= `node cap (${MAX_NODES}) reached`
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
this.index.set(id, this.nodes.length)
|
|
140
|
+
this.nodes.push({ label: label ?? id, shape })
|
|
141
|
+
this.nodeGroup.push(this.curGroup)
|
|
142
|
+
return this.nodes.length - 1
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Set a node's label without disturbing its shape, creating it if new. */
|
|
146
|
+
nodeLabel(id: string, label: string): number | null {
|
|
147
|
+
const existing = this.index.get(id)
|
|
148
|
+
if (existing !== undefined) {
|
|
149
|
+
this.nodes[existing].label = label
|
|
150
|
+
return existing
|
|
151
|
+
}
|
|
152
|
+
return this.nodeIndex(id, label, 'round')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Attach an author class name to a node, ignoring a repeat. */
|
|
156
|
+
addClass(idx: number, name: string): void {
|
|
157
|
+
const node = this.nodes[idx]
|
|
158
|
+
node.classes ??= []
|
|
159
|
+
if (!node.classes.includes(name)) node.classes.push(name)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Apply collected `[ids, names]` class assignments. Run after the statement
|
|
164
|
+
* walk so a `class A,B name` (or `:::` tag) may precede the nodes it names;
|
|
165
|
+
* unknown ids are ignored.
|
|
166
|
+
*/
|
|
167
|
+
applyClasses(assignments: [string[], string[]][]): void {
|
|
168
|
+
for (const [ids, names] of assignments) {
|
|
169
|
+
for (const id of ids) {
|
|
170
|
+
const idx = this.index.get(id.trim())
|
|
171
|
+
if (idx === undefined) continue
|
|
172
|
+
for (const name of names) {
|
|
173
|
+
if (name.trim() !== '') this.addClass(idx, name.trim())
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Apply `[id, url]` link targets; the last one per id wins, unknown ids
|
|
180
|
+
* are ignored. Deferred like `applyClasses`, for the same ordering reason. */
|
|
181
|
+
applyHrefs(hrefs: [string, string][]): void {
|
|
182
|
+
for (const [id, url] of hrefs) {
|
|
183
|
+
const idx = this.index.get(id.trim())
|
|
184
|
+
if (idx !== undefined) this.nodes[idx].href = url
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Record an unreadable statement; the diagram renders without it. Skipped
|
|
190
|
+
* once truncated — a cap-caused parse failure is not the statement's fault.
|
|
191
|
+
*/
|
|
192
|
+
drop(st: string): void {
|
|
193
|
+
if (this.truncated === null) this.warnings.push(`dropped, unreadable statement: "${st}"`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Append an edge, or flag `truncated` when `MAX_EDGES` is reached. */
|
|
197
|
+
pushEdge(edge: Edge): boolean {
|
|
198
|
+
if (this.edges.length >= MAX_EDGES) {
|
|
199
|
+
this.truncated ??= `edge cap (${MAX_EDGES}) reached`
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
this.edges.push(edge)
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Canvas } from './canvas.ts'
|
|
2
|
+
import { LIMITS, stripControls } from './labels.ts'
|
|
3
|
+
import { type Diagram, diagramFor } from './registry.ts'
|
|
4
|
+
import { frontmatterTitle } from './statements.ts'
|
|
5
|
+
import type { MermaidArt } from './types.ts'
|
|
6
|
+
import { stringWidth } from './width.ts'
|
|
7
|
+
|
|
8
|
+
export { type AnsiTheme, classSgr, DEFAULT_THEME, toAnsi } from './ansi.ts'
|
|
9
|
+
export { type ClassStyle, contrastOn, resolveClassStyle } from './class-style.ts'
|
|
10
|
+
export { type DiagramKind, diagramKind } from './registry.ts'
|
|
11
|
+
export { sourceBox } from './source-box.ts'
|
|
12
|
+
export type { MermaidArt, Role, Span } from './types.ts'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Render a Mermaid source block as Unicode box-drawing art.
|
|
16
|
+
*
|
|
17
|
+
* Supported: `graph`/`flowchart` (including `subgraph`), `stateDiagram`,
|
|
18
|
+
* `classDiagram`, `erDiagram`, `sequenceDiagram`, `pie`, `mindmap`,
|
|
19
|
+
* `timeline` and `gitGraph`.
|
|
20
|
+
*
|
|
21
|
+
* The diagram is laid out at whatever size it needs; `art.width` reports the
|
|
22
|
+
* columns that turned out to be. Given `maxWidth`, a diagram wider than that
|
|
23
|
+
* is laid out again with progressively tighter label limits and the first
|
|
24
|
+
* fit is returned. Deciding what to do when even the tightest exceeds the
|
|
25
|
+
* space at hand is the caller's — `sourceBox` is the usual answer:
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* const art = render(src, { maxWidth: cols })
|
|
29
|
+
* show(art && art.width <= cols ? art : sourceBox(src, cols))
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* `null` means there is no art to show: blank input, a diagram type this
|
|
33
|
+
* renderer does not draw, a source in which not one statement parsed, or a
|
|
34
|
+
* diagram large enough that laying it out is refused. `diagramKind` separates
|
|
35
|
+
* the middle two.
|
|
36
|
+
*
|
|
37
|
+
* Rendering is best-effort in every grammar: a statement either contributes
|
|
38
|
+
* what parsed or is dropped, and a diagram over a size cap renders its prefix.
|
|
39
|
+
* Everything given up on is listed in `art.warnings` — advisory only, never a
|
|
40
|
+
* reason to withhold the art.
|
|
41
|
+
*/
|
|
42
|
+
export function render(src: string, options: { maxWidth?: number } = {}): MermaidArt | null {
|
|
43
|
+
src = stripControls(src)
|
|
44
|
+
if (src.trim() === '') return null
|
|
45
|
+
const diagram = diagramFor(src)
|
|
46
|
+
if (diagram === null) return null
|
|
47
|
+
// Too wide for the space given: lay out again with shorter labels and
|
|
48
|
+
// tighter wrapping, tightest last, and keep the first that fits (else
|
|
49
|
+
// the tightest, for the caller to judge against `art.width`).
|
|
50
|
+
let drawn: ReturnType<Diagram['render']> = null
|
|
51
|
+
let art: ReturnType<Canvas['toLines']> = { plain: [], styled: [], width: 0 }
|
|
52
|
+
for (const limits of LIMITS) {
|
|
53
|
+
drawn = diagram.render(src, limits)
|
|
54
|
+
if (drawn === null) return null
|
|
55
|
+
art = drawn.canvas.toLines()
|
|
56
|
+
if (options.maxWidth === undefined || art.width <= options.maxWidth) break
|
|
57
|
+
}
|
|
58
|
+
if (drawn === null) return null
|
|
59
|
+
|
|
60
|
+
// A frontmatter `title:` is centred above the art, in the `title` role.
|
|
61
|
+
const title = frontmatterTitle(src)
|
|
62
|
+
if (title !== null) {
|
|
63
|
+
const tw = stringWidth(title)
|
|
64
|
+
art.width = Math.max(art.width, tw)
|
|
65
|
+
const pad = ' '.repeat(Math.floor((art.width - tw) / 2))
|
|
66
|
+
art.plain.unshift(pad + title, '')
|
|
67
|
+
art.styled.unshift(
|
|
68
|
+
pad === ''
|
|
69
|
+
? [{ text: title, role: 'title' }]
|
|
70
|
+
: [
|
|
71
|
+
{ text: pad, role: 'none' },
|
|
72
|
+
{ text: title, role: 'title' },
|
|
73
|
+
],
|
|
74
|
+
[],
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return { ...art, classDefs: drawn.classDefs, warnings: drawn.warnings }
|
|
78
|
+
}
|