dsh-model-everything 0.4.0 → 0.5.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/README.en-US.md +63 -19
- package/README.md +373 -345
- package/cordis.patch.yml +9 -0
- package/diagram/package.json +5 -0
- package/lib/client.js +1273 -0
- package/lib/diagram-diff-tool.js +90 -0
- package/lib/diagram-render-tool.js +102 -0
- package/lib/diagram-review-tool.js +99 -0
- package/lib/diagram.js +43 -0
- package/lib/gate.js +14 -12
- package/lib/presentation.js +10 -0
- package/lib/render/budget.js +99 -0
- package/lib/render/collapse.js +163 -0
- package/lib/render/diff.js +138 -0
- package/lib/render/from-mermaid.js +211 -0
- package/lib/render/from-plantuml.js +158 -0
- package/lib/render/index.js +42 -0
- package/lib/render/layout.js +52 -0
- package/lib/render/model.js +17 -0
- package/lib/render/paint.js +194 -0
- package/lib/render/palette.js +127 -0
- package/lib/render/place.js +210 -0
- package/lib/render/svg.js +107 -0
- package/lib/render/viewmodel.js +126 -0
- package/lib/render/write.js +14 -0
- package/lib/scan.js +248 -8
- package/lib/structure-tool.js +2 -2
- package/lib/types/client.d.ts +83 -0
- package/lib/types/diagram-diff-tool.d.ts +8 -0
- package/lib/types/diagram-render-tool.d.ts +8 -0
- package/lib/types/diagram-review-tool.d.ts +8 -0
- package/lib/types/diagram.d.ts +34 -0
- package/lib/types/presentation.d.ts +7 -0
- package/lib/types/render/budget.d.ts +55 -0
- package/lib/types/render/collapse.d.ts +23 -0
- package/lib/types/render/diff.d.ts +44 -0
- package/lib/types/render/from-mermaid.d.ts +27 -0
- package/lib/types/render/from-plantuml.d.ts +27 -0
- package/lib/types/render/index.d.ts +38 -0
- package/lib/types/render/layout.d.ts +22 -0
- package/lib/types/render/model.d.ts +56 -0
- package/lib/types/render/paint.d.ts +94 -0
- package/lib/types/render/palette.d.ts +41 -0
- package/lib/types/render/place.d.ts +39 -0
- package/lib/types/render/svg.d.ts +19 -0
- package/lib/types/render/viewmodel.d.ts +20 -0
- package/lib/types/render/write.d.ts +14 -0
- package/lib/types/scan.d.ts +10 -2
- package/lib/types/structure.d.ts +1 -0
- package/package.json +148 -131
- package/skills/model-everything/SKILL.md +454 -454
- package/skills/model-everything/references/architecture-domains.md +137 -0
- package/skills/model-everything/references/architecture-views.md +119 -0
- package/skills/model-everything-diagram/SKILL.md +82 -0
- package/src/client.ts +307 -0
- package/src/diagram-diff-tool.ts +94 -0
- package/src/diagram-render-tool.ts +108 -0
- package/src/diagram-review-tool.ts +109 -0
- package/src/diagram.ts +61 -0
- package/src/gate.ts +14 -12
- package/src/presentation.ts +12 -0
- package/src/render/budget.ts +135 -0
- package/src/render/collapse.ts +155 -0
- package/src/render/diff.ts +162 -0
- package/src/render/from-mermaid.ts +236 -0
- package/src/render/from-plantuml.ts +178 -0
- package/src/render/index.ts +73 -0
- package/src/render/layout.ts +62 -0
- package/src/render/model.ts +67 -0
- package/src/render/paint.ts +290 -0
- package/src/render/palette.ts +164 -0
- package/src/render/place.ts +234 -0
- package/src/render/svg.ts +127 -0
- package/src/render/viewmodel.ts +136 -0
- package/src/render/write.ts +17 -0
- package/src/scan.ts +807 -564
- package/src/structure-tool.ts +2 -2
- package/src/structure.ts +1 -1
package/lib/client.js
ADDED
|
@@ -0,0 +1,1273 @@
|
|
|
1
|
+
// Generated by scripts/build-client.mjs - do not edit. Source: src/client.ts and src/render/*.ts.
|
|
2
|
+
window.__ModuleLoader__.load({
|
|
3
|
+
id: "dsh-model-everything",
|
|
4
|
+
factory(require) {
|
|
5
|
+
const module = { exports: {} };
|
|
6
|
+
const exports = module.exports;
|
|
7
|
+
"use strict";
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
10
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
|
+
|
|
26
|
+
// src/client.ts
|
|
27
|
+
var client_exports = {};
|
|
28
|
+
__export(client_exports, {
|
|
29
|
+
TOOL_NAMES: () => TOOL_NAMES,
|
|
30
|
+
WHITEBOARD_ID: () => WHITEBOARD_ID,
|
|
31
|
+
WHITEBOARD_SAMPLE: () => WHITEBOARD_SAMPLE,
|
|
32
|
+
default: () => factory,
|
|
33
|
+
mountWhiteboard: () => mountWhiteboard,
|
|
34
|
+
themeMode: () => themeMode,
|
|
35
|
+
viewFor: () => viewFor,
|
|
36
|
+
whiteboardFor: () => whiteboardFor
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(client_exports);
|
|
39
|
+
|
|
40
|
+
// src/render/layout.ts
|
|
41
|
+
function rankStates(nodes, edges, init) {
|
|
42
|
+
const ids = nodes.map((node) => node.id);
|
|
43
|
+
const known = new Set(ids);
|
|
44
|
+
const start = init !== void 0 && known.has(init) ? init : ids[0];
|
|
45
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
46
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
47
|
+
for (const edge of edges) {
|
|
48
|
+
if (!known.has(edge.from) || !known.has(edge.to)) continue;
|
|
49
|
+
outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
|
|
50
|
+
incoming.set(edge.to, [...incoming.get(edge.to) ?? [], edge.from]);
|
|
51
|
+
}
|
|
52
|
+
const depth = /* @__PURE__ */ new Map();
|
|
53
|
+
if (start !== void 0) {
|
|
54
|
+
depth.set(start, 0);
|
|
55
|
+
const queue = [start];
|
|
56
|
+
while (queue.length > 0) {
|
|
57
|
+
const id = queue.shift();
|
|
58
|
+
const level = depth.get(id);
|
|
59
|
+
for (const target of outgoing.get(id) ?? []) {
|
|
60
|
+
if (depth.has(target)) continue;
|
|
61
|
+
depth.set(target, level + 1);
|
|
62
|
+
queue.push(target);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const rows = [];
|
|
67
|
+
for (const id of ids) {
|
|
68
|
+
const level = depth.get(id);
|
|
69
|
+
if (level === void 0) continue;
|
|
70
|
+
while (rows.length <= level) rows.push([]);
|
|
71
|
+
rows[level].push(id);
|
|
72
|
+
}
|
|
73
|
+
return { rows, unreachable: ids.filter((id) => !depth.has(id)) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/render/place.ts
|
|
77
|
+
var MARGIN = 20;
|
|
78
|
+
var NODE_HEIGHT = 34;
|
|
79
|
+
var ROW_GAP = 58;
|
|
80
|
+
var COLUMN_GAP = 30;
|
|
81
|
+
var MIN_NODE_WIDTH = 88;
|
|
82
|
+
var MAX_NODE_WIDTH = 220;
|
|
83
|
+
var CHAR_WIDTH = 7.2;
|
|
84
|
+
var GROUP_PADDING = 10;
|
|
85
|
+
var GROUP_HEADER = 16;
|
|
86
|
+
var BLOCK_GAP = 14;
|
|
87
|
+
var nodeWidth = (label) => Math.min(MAX_NODE_WIDTH, Math.max(MIN_NODE_WIDTH, label.length * CHAR_WIDTH + 26));
|
|
88
|
+
function placeByRanks(diagram, nodes, top) {
|
|
89
|
+
const ranked = rankStates(diagram.nodes, diagram.edges, diagram.init);
|
|
90
|
+
const rows = [...ranked.rows];
|
|
91
|
+
if (ranked.unreachable.length > 0) rows.push(ranked.unreachable);
|
|
92
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
93
|
+
let widest = 0;
|
|
94
|
+
rows.forEach((row, index) => {
|
|
95
|
+
const widths = row.map((id) => nodeWidth(id));
|
|
96
|
+
widest = Math.max(widest, widths.reduce((total, width) => total + width, 0) + Math.max(0, row.length - 1) * COLUMN_GAP);
|
|
97
|
+
let x = 0;
|
|
98
|
+
row.forEach((id, position) => {
|
|
99
|
+
boxes.set(id, { x, y: top + index * (NODE_HEIGHT + ROW_GAP), width: widths[position], height: NODE_HEIGHT });
|
|
100
|
+
x += widths[position] + COLUMN_GAP;
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
rows.forEach((row) => {
|
|
104
|
+
const rowWidth = row.reduce((total, id) => total + boxes.get(id).width, 0) + Math.max(0, row.length - 1) * COLUMN_GAP;
|
|
105
|
+
const offset = (widest - rowWidth) / 2;
|
|
106
|
+
for (const id of row) boxes.get(id).x += offset + MARGIN;
|
|
107
|
+
});
|
|
108
|
+
return { boxes, groupRects: /* @__PURE__ */ new Map(), width: widest + MARGIN * 2 };
|
|
109
|
+
}
|
|
110
|
+
function groupTree(groups) {
|
|
111
|
+
const byId = new Map(groups.map((group) => [group.id, group]));
|
|
112
|
+
const owner = /* @__PURE__ */ new Map();
|
|
113
|
+
const bySize = [...groups].sort((left, right) => left.members.length - right.members.length);
|
|
114
|
+
for (const group of bySize) {
|
|
115
|
+
for (const member of group.members) if (!owner.has(member)) owner.set(member, group.id);
|
|
116
|
+
}
|
|
117
|
+
const declared = (group) => {
|
|
118
|
+
const parent = group.parent;
|
|
119
|
+
if (parent === void 0 || parent === group.id) return void 0;
|
|
120
|
+
return byId.has(parent) ? parent : void 0;
|
|
121
|
+
};
|
|
122
|
+
const members = new Map(groups.map((group) => [group.id, new Set(group.members)]));
|
|
123
|
+
const infer = (group) => {
|
|
124
|
+
let best;
|
|
125
|
+
for (const other of groups) {
|
|
126
|
+
if (other.id === group.id) continue;
|
|
127
|
+
const outer = members.get(other.id);
|
|
128
|
+
const inner = members.get(group.id);
|
|
129
|
+
if (inner.size === 0 || inner.size >= outer.size) continue;
|
|
130
|
+
let contained = true;
|
|
131
|
+
for (const member of inner) {
|
|
132
|
+
if (!outer.has(member)) {
|
|
133
|
+
contained = false;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (contained && (best === void 0 || outer.size < best.size)) best = { id: other.id, size: outer.size };
|
|
138
|
+
}
|
|
139
|
+
return best?.id;
|
|
140
|
+
};
|
|
141
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
142
|
+
for (const group of groups) {
|
|
143
|
+
const parent = declared(group) ?? infer(group);
|
|
144
|
+
if (parent !== void 0) parentOf.set(group.id, parent);
|
|
145
|
+
}
|
|
146
|
+
for (const group of groups) {
|
|
147
|
+
const seen = /* @__PURE__ */ new Set([group.id]);
|
|
148
|
+
let cursor = parentOf.get(group.id);
|
|
149
|
+
while (cursor !== void 0) {
|
|
150
|
+
if (seen.has(cursor)) {
|
|
151
|
+
parentOf.delete(cursor);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
seen.add(cursor);
|
|
155
|
+
cursor = parentOf.get(cursor);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const children = /* @__PURE__ */ new Map();
|
|
159
|
+
const roots = [];
|
|
160
|
+
for (const group of groups) {
|
|
161
|
+
const parent = parentOf.get(group.id);
|
|
162
|
+
if (parent === void 0) roots.push(group.id);
|
|
163
|
+
else children.set(parent, [...children.get(parent) ?? [], group.id]);
|
|
164
|
+
}
|
|
165
|
+
return { owner, children, roots };
|
|
166
|
+
}
|
|
167
|
+
function placeByGroups(diagram, nodes, top) {
|
|
168
|
+
const groups = diagram.groups;
|
|
169
|
+
const byId = new Map(groups.map((group) => [group.id, group]));
|
|
170
|
+
const { owner, children, roots } = groupTree(groups);
|
|
171
|
+
const order = new Map(nodes.map((node, index) => [node.id, index]));
|
|
172
|
+
const loose = nodes.filter((node) => !owner.has(node.id)).map((node) => node.id);
|
|
173
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
174
|
+
const rawContents = (groupId) => {
|
|
175
|
+
const group = byId.get(groupId);
|
|
176
|
+
const own = [...new Set(group.members)].filter((member) => owner.get(member) === groupId && nodeIds.has(member));
|
|
177
|
+
const nested = children.get(groupId) ?? [];
|
|
178
|
+
return [...own, ...nested];
|
|
179
|
+
};
|
|
180
|
+
const contents = (groupId) => rawContents(groupId).filter((child) => !byId.has(child) || rawContents(child).length > 0);
|
|
181
|
+
const measure = (id) => {
|
|
182
|
+
if (!byId.has(id)) return { width: nodeWidth(id), height: NODE_HEIGHT };
|
|
183
|
+
const items = contents(id);
|
|
184
|
+
const sizes2 = items.map(measure);
|
|
185
|
+
return {
|
|
186
|
+
width: Math.max(MIN_NODE_WIDTH, ...sizes2.map((size) => size.width)) + GROUP_PADDING * 2,
|
|
187
|
+
height: GROUP_HEADER + GROUP_PADDING * 2 + sizes2.reduce((total, size) => total + size.height, 0) + Math.max(0, items.length - 1) * BLOCK_GAP
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
191
|
+
const groupRects = /* @__PURE__ */ new Map();
|
|
192
|
+
const place2 = (id, x, y) => {
|
|
193
|
+
const size = measure(id);
|
|
194
|
+
if (!byId.has(id)) {
|
|
195
|
+
boxes.set(id, { x, y, width: size.width, height: NODE_HEIGHT });
|
|
196
|
+
return size;
|
|
197
|
+
}
|
|
198
|
+
groupRects.set(id, { x, y, width: size.width, height: size.height });
|
|
199
|
+
let cursor2 = y + GROUP_HEADER + GROUP_PADDING;
|
|
200
|
+
const inner = size.width - GROUP_PADDING * 2;
|
|
201
|
+
const ordered = [...contents(id)].sort((left, right) => firstIndex(left) - firstIndex(right));
|
|
202
|
+
for (const child of ordered) {
|
|
203
|
+
const childSize = measure(child);
|
|
204
|
+
place2(child, x + GROUP_PADDING + (inner - childSize.width) / 2, cursor2);
|
|
205
|
+
cursor2 += childSize.height + BLOCK_GAP;
|
|
206
|
+
}
|
|
207
|
+
return size;
|
|
208
|
+
};
|
|
209
|
+
const visibleRoots = roots.filter((id) => contents(id).length > 0);
|
|
210
|
+
const firstIndex = (id) => {
|
|
211
|
+
if (!byId.has(id)) return order.get(id);
|
|
212
|
+
return Math.min(...contents(id).map(firstIndex));
|
|
213
|
+
};
|
|
214
|
+
const topItems = [...loose, ...visibleRoots].sort((left, right) => firstIndex(left) - firstIndex(right));
|
|
215
|
+
const sizes = topItems.map(measure);
|
|
216
|
+
const width = Math.max(MIN_NODE_WIDTH, ...sizes.map((size) => size.width));
|
|
217
|
+
let cursor = top;
|
|
218
|
+
for (const item of topItems) {
|
|
219
|
+
const size = measure(item);
|
|
220
|
+
place2(item, MARGIN + (width - size.width) / 2, cursor);
|
|
221
|
+
cursor += size.height + BLOCK_GAP;
|
|
222
|
+
}
|
|
223
|
+
return { boxes, groupRects, width: width + MARGIN * 2 };
|
|
224
|
+
}
|
|
225
|
+
function place(diagram, top) {
|
|
226
|
+
const groups = diagram.groups ?? [];
|
|
227
|
+
return groups.length > 0 ? placeByGroups(diagram, diagram.nodes, top) : placeByRanks(diagram, diagram.nodes, top);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/render/palette.ts
|
|
231
|
+
var NEUTRAL_LIGHT = {
|
|
232
|
+
pass: "#2f8f5b",
|
|
233
|
+
findings: "#b7791f",
|
|
234
|
+
fail: "#c0392b",
|
|
235
|
+
undecided: "#6b7280",
|
|
236
|
+
muted: "#9aa3ad",
|
|
237
|
+
text: "#1f2933",
|
|
238
|
+
edge: "#6b7280",
|
|
239
|
+
surface: "transparent",
|
|
240
|
+
background: "#ffffff"
|
|
241
|
+
};
|
|
242
|
+
var NEUTRAL_DARK = {
|
|
243
|
+
pass: "#4ac486",
|
|
244
|
+
findings: "#e0b050",
|
|
245
|
+
fail: "#f2705f",
|
|
246
|
+
undecided: "#9aa3ad",
|
|
247
|
+
muted: "#7d8794",
|
|
248
|
+
text: "#e6e8eb",
|
|
249
|
+
edge: "#8b949e",
|
|
250
|
+
surface: "transparent",
|
|
251
|
+
background: "#0d1117"
|
|
252
|
+
};
|
|
253
|
+
var GITHUB_LIGHT = {
|
|
254
|
+
pass: "#1a7f37",
|
|
255
|
+
findings: "#9a6700",
|
|
256
|
+
fail: "#cf222e",
|
|
257
|
+
undecided: "#656d76",
|
|
258
|
+
muted: "#8c959f",
|
|
259
|
+
text: "#1f2328",
|
|
260
|
+
edge: "#656d76",
|
|
261
|
+
surface: "transparent",
|
|
262
|
+
background: "#ffffff"
|
|
263
|
+
};
|
|
264
|
+
var GITHUB_DARK = {
|
|
265
|
+
pass: "#3fb950",
|
|
266
|
+
findings: "#d29922",
|
|
267
|
+
fail: "#f85149",
|
|
268
|
+
undecided: "#7d8590",
|
|
269
|
+
muted: "#6e7681",
|
|
270
|
+
text: "#e6edf3",
|
|
271
|
+
edge: "#7d8590",
|
|
272
|
+
surface: "transparent",
|
|
273
|
+
background: "#0d1117"
|
|
274
|
+
};
|
|
275
|
+
var HIGH_CONTRAST = {
|
|
276
|
+
pass: "#0a5c35",
|
|
277
|
+
findings: "#7a4300",
|
|
278
|
+
fail: "#a30000",
|
|
279
|
+
undecided: "#333333",
|
|
280
|
+
muted: "#444444",
|
|
281
|
+
text: "#000000",
|
|
282
|
+
edge: "#000000",
|
|
283
|
+
surface: "transparent",
|
|
284
|
+
background: "#ffffff"
|
|
285
|
+
};
|
|
286
|
+
var PRINT = {
|
|
287
|
+
pass: "#333333",
|
|
288
|
+
findings: "#555555",
|
|
289
|
+
fail: "#000000",
|
|
290
|
+
undecided: "#666666",
|
|
291
|
+
muted: "#777777",
|
|
292
|
+
text: "#000000",
|
|
293
|
+
edge: "#333333",
|
|
294
|
+
surface: "transparent",
|
|
295
|
+
background: "#ffffff"
|
|
296
|
+
};
|
|
297
|
+
var SCHEMES = {
|
|
298
|
+
default: { light: NEUTRAL_LIGHT, dark: NEUTRAL_DARK },
|
|
299
|
+
github: { light: GITHUB_LIGHT, dark: GITHUB_DARK },
|
|
300
|
+
"high-contrast": { light: HIGH_CONTRAST },
|
|
301
|
+
print: { light: PRINT }
|
|
302
|
+
};
|
|
303
|
+
function palette(overrides = {}, options = {}) {
|
|
304
|
+
const scheme = options.scheme ?? "default";
|
|
305
|
+
const mode = options.mode ?? "light";
|
|
306
|
+
const chosen = SCHEMES[scheme] ?? SCHEMES.default;
|
|
307
|
+
const base = mode === "dark" ? chosen.dark ?? chosen.light : chosen.light;
|
|
308
|
+
return { ...base, ...overrides };
|
|
309
|
+
}
|
|
310
|
+
function roleForVerdict(verdict) {
|
|
311
|
+
if (verdict === "pass") return "pass";
|
|
312
|
+
if (verdict === "pass_with_findings") return "findings";
|
|
313
|
+
if (verdict === "fail") return "fail";
|
|
314
|
+
return "undecided";
|
|
315
|
+
}
|
|
316
|
+
function schemeNames() {
|
|
317
|
+
return Object.keys(SCHEMES);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/render/model.ts
|
|
321
|
+
function emptyDiagram(title, caption) {
|
|
322
|
+
return { kind: "none", title, nodes: [], edges: [], caption };
|
|
323
|
+
}
|
|
324
|
+
function labelOf(node) {
|
|
325
|
+
return node.label !== void 0 && node.label !== "" ? node.label : node.id;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/render/paint.ts
|
|
329
|
+
var TITLE_HEIGHT = 26;
|
|
330
|
+
var NODE_HEIGHT2 = 34;
|
|
331
|
+
var CHANNEL_GAP = 16;
|
|
332
|
+
var CAPTION_CHARS = 78;
|
|
333
|
+
var CAPTION_LINE = 15;
|
|
334
|
+
var CAPTION_GAP = 16;
|
|
335
|
+
var CAPTION_CHAR_WIDTH = 6.2;
|
|
336
|
+
function roleOfNode(node) {
|
|
337
|
+
if (node.role === "error") return "fail";
|
|
338
|
+
if (node.role === "warning") return "findings";
|
|
339
|
+
if (node.role === "muted") return "muted";
|
|
340
|
+
return "edge";
|
|
341
|
+
}
|
|
342
|
+
function wrap(text2, limit) {
|
|
343
|
+
const lines = [];
|
|
344
|
+
let line = "";
|
|
345
|
+
for (const word of text2.split(/\s+/).filter((part) => part !== "")) {
|
|
346
|
+
const candidate = line === "" ? word : line + " " + word;
|
|
347
|
+
if (candidate.length <= limit) {
|
|
348
|
+
line = candidate;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (line !== "") lines.push(line);
|
|
352
|
+
line = "";
|
|
353
|
+
let rest = word;
|
|
354
|
+
while (rest.length > limit) {
|
|
355
|
+
lines.push(rest.slice(0, limit));
|
|
356
|
+
rest = rest.slice(limit);
|
|
357
|
+
}
|
|
358
|
+
line = rest;
|
|
359
|
+
}
|
|
360
|
+
if (line !== "") lines.push(line);
|
|
361
|
+
return lines.length === 0 ? [""] : lines;
|
|
362
|
+
}
|
|
363
|
+
function captionRole(caption) {
|
|
364
|
+
return roleForVerdict(caption.split(" \u2014 ")[0]);
|
|
365
|
+
}
|
|
366
|
+
function paintWith(diagram, overrides = {}, options = {}) {
|
|
367
|
+
const colours = palette(overrides, options);
|
|
368
|
+
const shapes = [];
|
|
369
|
+
const groups = diagram.groups ?? [];
|
|
370
|
+
const topOffset = MARGIN + TITLE_HEIGHT;
|
|
371
|
+
const placement = place(diagram, topOffset);
|
|
372
|
+
const { boxes, groupRects } = placement;
|
|
373
|
+
for (const group of groups) {
|
|
374
|
+
const rect = groupRects.get(group.id);
|
|
375
|
+
if (rect === void 0) continue;
|
|
376
|
+
shapes.push({ kind: "group", id: group.id, x: rect.x, y: rect.y, width: rect.width, height: rect.height, label: group.label, colour: "muted" });
|
|
377
|
+
}
|
|
378
|
+
const drawn = /* @__PURE__ */ new Set();
|
|
379
|
+
for (const node of diagram.nodes) {
|
|
380
|
+
if (drawn.has(node.id)) continue;
|
|
381
|
+
const box = boxes.get(node.id);
|
|
382
|
+
drawn.add(node.id);
|
|
383
|
+
shapes.push({
|
|
384
|
+
kind: "box",
|
|
385
|
+
x: box.x,
|
|
386
|
+
y: box.y,
|
|
387
|
+
width: box.width,
|
|
388
|
+
height: box.height,
|
|
389
|
+
label: labelOf(node),
|
|
390
|
+
colour: roleOfNode(node),
|
|
391
|
+
initial: diagram.init === node.id
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const roleById = new Map(diagram.nodes.map((node) => [node.id, roleOfNode(node)]));
|
|
395
|
+
let channel = Math.max(placement.width, ...Array.from(boxes.values(), (box) => box.x + box.width)) + CHANNEL_GAP;
|
|
396
|
+
let channelUses = 0;
|
|
397
|
+
for (const edge of diagram.edges) {
|
|
398
|
+
const source = boxes.get(edge.from);
|
|
399
|
+
const target = boxes.get(edge.to);
|
|
400
|
+
if (source === void 0 || target === void 0) continue;
|
|
401
|
+
const colour = roleById.get(edge.from);
|
|
402
|
+
const sameRow = Math.abs(target.y - source.y) < NODE_HEIGHT2 / 2;
|
|
403
|
+
if (edge.from === edge.to) {
|
|
404
|
+
const x = source.x + source.width;
|
|
405
|
+
const y = source.y + source.height / 2;
|
|
406
|
+
shapes.push({
|
|
407
|
+
kind: "path",
|
|
408
|
+
points: [
|
|
409
|
+
{ x, y: y - 9 },
|
|
410
|
+
{ x: x + 14, y: y - 9 },
|
|
411
|
+
{ x: x + 14, y: y + 9 },
|
|
412
|
+
{ x, y: y + 9 }
|
|
413
|
+
],
|
|
414
|
+
label: edge.label,
|
|
415
|
+
labelX: x + 20,
|
|
416
|
+
labelY: y,
|
|
417
|
+
colour
|
|
418
|
+
});
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (target.y > source.y && !sameRow) {
|
|
422
|
+
const x1 = source.x + source.width / 2;
|
|
423
|
+
const y1 = source.y + source.height;
|
|
424
|
+
const x2 = target.x + target.width / 2;
|
|
425
|
+
const y2 = target.y;
|
|
426
|
+
shapes.push({ kind: "link", x1, y1, x2, y2, label: edge.label, labelX: (x1 + x2) / 2 + 7, labelY: (y1 + y2) / 2, colour });
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (sameRow) {
|
|
430
|
+
const right = source.x + source.width;
|
|
431
|
+
const left = target.x;
|
|
432
|
+
const y = source.y + source.height / 2;
|
|
433
|
+
const forward = right <= left;
|
|
434
|
+
shapes.push({
|
|
435
|
+
kind: "link",
|
|
436
|
+
x1: forward ? right : source.x,
|
|
437
|
+
y1: y,
|
|
438
|
+
x2: forward ? left : target.x + target.width,
|
|
439
|
+
y2: y,
|
|
440
|
+
label: edge.label,
|
|
441
|
+
labelX: (right + left) / 2,
|
|
442
|
+
labelY: y - 7,
|
|
443
|
+
colour
|
|
444
|
+
});
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const lane = channel + channelUses % 3 * 8;
|
|
448
|
+
channelUses += 1;
|
|
449
|
+
channel = Math.max(channel, lane);
|
|
450
|
+
const startY = source.y + source.height / 2;
|
|
451
|
+
const endY = target.y + target.height / 2;
|
|
452
|
+
shapes.push({
|
|
453
|
+
kind: "path",
|
|
454
|
+
points: [
|
|
455
|
+
{ x: source.x + source.width, y: startY },
|
|
456
|
+
{ x: lane, y: startY },
|
|
457
|
+
{ x: lane, y: endY },
|
|
458
|
+
{ x: target.x + target.width, y: endY }
|
|
459
|
+
],
|
|
460
|
+
label: edge.label,
|
|
461
|
+
labelX: lane + 6,
|
|
462
|
+
labelY: (startY + endY) / 2,
|
|
463
|
+
colour
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
shapes.push({ kind: "title", x: MARGIN, y: MARGIN + 12, text: diagram.title, colour: "text" });
|
|
467
|
+
const contentRight = Math.max(
|
|
468
|
+
MARGIN,
|
|
469
|
+
...Array.from(boxes.values(), (box) => box.x + box.width),
|
|
470
|
+
...Array.from(groupRects.values(), (rect) => rect.x + rect.width)
|
|
471
|
+
);
|
|
472
|
+
const captionLines = wrap(diagram.caption, CAPTION_CHARS);
|
|
473
|
+
const captionWidth = Math.max(...captionLines.map((line) => line.length * CAPTION_CHAR_WIDTH)) + MARGIN * 2;
|
|
474
|
+
const width = Math.max(contentRight + MARGIN + (channelUses > 0 ? CHANNEL_GAP * 3 : 0), captionWidth, 240);
|
|
475
|
+
const contentBottom = Math.max(
|
|
476
|
+
topOffset,
|
|
477
|
+
...Array.from(boxes.values(), (box) => box.y + box.height),
|
|
478
|
+
...Array.from(groupRects.values(), (rect) => rect.y + rect.height)
|
|
479
|
+
);
|
|
480
|
+
const captionTop = contentBottom + CAPTION_GAP;
|
|
481
|
+
shapes.push({ kind: "caption", x: MARGIN, y: captionTop, lines: captionLines, colour: captionRole(diagram.caption) });
|
|
482
|
+
const height = captionTop + captionLines.length * CAPTION_LINE + MARGIN / 2;
|
|
483
|
+
return { canvas: { width: Math.round(width), height: Math.round(height), shapes }, colours };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/render/from-mermaid.ts
|
|
487
|
+
var CURVED_EDGE = /^(.+?)\s*(-->|---|-\.->|==>)\s*(?:\|([^|]*)\|\s*)?(.+)$/;
|
|
488
|
+
var LABELLED_EDGE = /^(.+?)\s*--\s*([^->]+?)\s*-->\s*(.+)$/;
|
|
489
|
+
var NODE = /^([A-Za-z_][\w-]*)\s*(?:\[\((.*)\)\]|\[\[(.*)\]\]|\[(.*)\]|\((.*)\)|\{(.*)\}|<<(.*)>>)?$/;
|
|
490
|
+
var TRANSITION = /^([A-Za-z_][\w-]*|\[\*\])\s*-->\s*([A-Za-z_][\w-]*|\[\*\])\s*(?::\s*(.*))?$/;
|
|
491
|
+
var STATE_ALIAS = /^state\s+["'](.+?)["']\s+as\s+([A-Za-z_][\w-]*)$/;
|
|
492
|
+
var DIRECTIVE = /^(click|style|classDef|class|linkStyle|accTitle|accDescr)\b/;
|
|
493
|
+
var UNSUPPORTED = /* @__PURE__ */ new Set([
|
|
494
|
+
"sequencediagram",
|
|
495
|
+
"classdiagram",
|
|
496
|
+
"erdiagram",
|
|
497
|
+
"gantt",
|
|
498
|
+
"pie",
|
|
499
|
+
"journey",
|
|
500
|
+
"mindmap",
|
|
501
|
+
"timeline",
|
|
502
|
+
"quadrantchart",
|
|
503
|
+
"gitgraph",
|
|
504
|
+
"c4context",
|
|
505
|
+
"requirementdiagram",
|
|
506
|
+
"sankey-beta"
|
|
507
|
+
]);
|
|
508
|
+
function ensure(builder, id, label) {
|
|
509
|
+
const existing = builder.nodes.get(id);
|
|
510
|
+
if (existing === void 0) {
|
|
511
|
+
builder.nodes.set(id, { id, role: "normal", ...label === void 0 ? {} : { label } });
|
|
512
|
+
const innermost = builder.open[builder.open.length - 1];
|
|
513
|
+
if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (label !== void 0 && (existing.label === void 0 || existing.label === "")) existing.label = label;
|
|
517
|
+
}
|
|
518
|
+
function nodeFrom(token) {
|
|
519
|
+
const match = NODE.exec(token.trim());
|
|
520
|
+
if (match === null) return void 0;
|
|
521
|
+
const id = match[1];
|
|
522
|
+
const label = [match[2], match[3], match[4], match[5], match[6], match[7]].find((value) => value !== void 0 && value.trim() !== "");
|
|
523
|
+
return { id, ...label === void 0 ? {} : { label: label.trim().replace(/^["']|["']$/g, "") } };
|
|
524
|
+
}
|
|
525
|
+
function fromMermaid(source, title = "mermaid") {
|
|
526
|
+
const lines = source.split(/\r?\n/);
|
|
527
|
+
const refused = [];
|
|
528
|
+
const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], groups: [], open: [] };
|
|
529
|
+
let mode = "none";
|
|
530
|
+
let startId;
|
|
531
|
+
let endCount = 0;
|
|
532
|
+
const push = (edge) => {
|
|
533
|
+
builder.edges.push(edge);
|
|
534
|
+
};
|
|
535
|
+
lines.forEach((raw, index) => {
|
|
536
|
+
const line = raw.replace(/%%(.*)$/, "").trim();
|
|
537
|
+
if (line === "") return;
|
|
538
|
+
const where = "line " + String(index + 1) + ": ";
|
|
539
|
+
const lower = line.toLowerCase();
|
|
540
|
+
const keyword = lower.split(/[\s{}]/)[0] ?? "";
|
|
541
|
+
if (UNSUPPORTED.has(keyword)) {
|
|
542
|
+
refused.push(where + "`" + keyword + "` is not a diagram this renderer draws");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (DIRECTIVE.test(line)) {
|
|
546
|
+
refused.push(where + "`" + (line.split(/\s/)[0] ?? "") + "` is a directive this renderer ignores");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (/^(graph|flowchart)\b/.test(lower)) {
|
|
550
|
+
mode = "graph";
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (/^statediagram(-v2)?\b/.test(lower)) {
|
|
554
|
+
mode = "state";
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (/^subgraph\b/.test(lower)) {
|
|
558
|
+
const label = line.replace(/^subgraph\b/i, "").trim().replace(/^["']|["']$/g, "");
|
|
559
|
+
const id = "subgraph-" + String(++endCount);
|
|
560
|
+
builder.open.push({ id, label: label === "" ? id : label, members: [] });
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (lower === "end") {
|
|
564
|
+
const closed = builder.open.pop();
|
|
565
|
+
if (closed !== void 0) {
|
|
566
|
+
const container = builder.open[builder.open.length - 1];
|
|
567
|
+
if (container !== void 0) closed.parent = container.id;
|
|
568
|
+
builder.groups.push(closed);
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (mode === "state") {
|
|
573
|
+
const alias = STATE_ALIAS.exec(line);
|
|
574
|
+
if (alias !== null) {
|
|
575
|
+
ensure(builder, alias[2], alias[1]);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const transition = TRANSITION.exec(line);
|
|
579
|
+
if (transition === null) {
|
|
580
|
+
const bare = /^state\s+([A-Za-z_][\w-]*)$/.exec(line);
|
|
581
|
+
if (bare !== null) {
|
|
582
|
+
ensure(builder, bare[1]);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (/^[A-Za-z_][\w-]*$/.test(line)) {
|
|
586
|
+
ensure(builder, line);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
refused.push(where + "`" + line + "` is not a state transition this renderer draws");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const from = transition[1];
|
|
593
|
+
const to = transition[2];
|
|
594
|
+
const label = (transition[3] ?? "").trim();
|
|
595
|
+
if (from === "[*]") {
|
|
596
|
+
ensure(builder, to);
|
|
597
|
+
if (startId === void 0) startId = to;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (to === "[*]") {
|
|
601
|
+
ensure(builder, from);
|
|
602
|
+
push({ from, to: from, label: label === "" ? "end" : label, dashed: true });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
ensure(builder, from);
|
|
606
|
+
ensure(builder, to);
|
|
607
|
+
push({ from, to, label });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const labelled = LABELLED_EDGE.exec(line);
|
|
611
|
+
if (labelled !== null) {
|
|
612
|
+
const left = nodeFrom(labelled[1]);
|
|
613
|
+
const right = nodeFrom(labelled[3]);
|
|
614
|
+
if (left === void 0 || right === void 0) {
|
|
615
|
+
refused.push(where + "`" + line + "` does not name two nodes");
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
ensure(builder, left.id, left.label);
|
|
619
|
+
ensure(builder, right.id, right.label);
|
|
620
|
+
push({ from: left.id, to: right.id, label: labelled[2].trim() });
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const edge = CURVED_EDGE.exec(line);
|
|
624
|
+
if (edge !== null) {
|
|
625
|
+
const left = nodeFrom(edge[1]);
|
|
626
|
+
const right = nodeFrom(edge[4]);
|
|
627
|
+
if (left === void 0 || right === void 0) {
|
|
628
|
+
refused.push(where + "`" + line + "` does not name two nodes");
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
ensure(builder, left.id, left.label);
|
|
632
|
+
ensure(builder, right.id, right.label);
|
|
633
|
+
push({
|
|
634
|
+
from: left.id,
|
|
635
|
+
to: right.id,
|
|
636
|
+
label: (edge[3] ?? "").trim(),
|
|
637
|
+
...edge[2] === "-.->" || edge[2] === "==>" ? { dashed: true } : {}
|
|
638
|
+
});
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const single = nodeFrom(line);
|
|
642
|
+
if (single !== void 0) {
|
|
643
|
+
ensure(builder, single.id, single.label);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
refused.push(where + "`" + line + "` is not something this renderer draws");
|
|
647
|
+
});
|
|
648
|
+
if (mode === "none") {
|
|
649
|
+
return { diagram: emptyDiagram(title, "no mermaid diagram found"), refused };
|
|
650
|
+
}
|
|
651
|
+
if (builder.nodes.size === 0) {
|
|
652
|
+
return { diagram: emptyDiagram(title, "no nodes found"), refused };
|
|
653
|
+
}
|
|
654
|
+
const diagram = {
|
|
655
|
+
kind: mode === "state" ? "machine" : "graph",
|
|
656
|
+
title,
|
|
657
|
+
...startId === void 0 ? {} : { init: startId },
|
|
658
|
+
nodes: [...builder.nodes.values()],
|
|
659
|
+
edges: builder.edges,
|
|
660
|
+
...builder.groups.length === 0 ? {} : { groups: builder.groups },
|
|
661
|
+
caption: mode + ", " + String(builder.nodes.size) + " box" + (builder.nodes.size === 1 ? "" : "es") + ", " + String(builder.edges.length) + " arrow" + (builder.edges.length === 1 ? "" : "s") + (refused.length === 0 ? "" : ", " + String(refused.length) + " line(s) refused")
|
|
662
|
+
};
|
|
663
|
+
return { diagram, refused };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/render/collapse.ts
|
|
667
|
+
function subtreeSize(diagram, groupId, seen) {
|
|
668
|
+
if (seen.has(groupId)) return 0;
|
|
669
|
+
seen.add(groupId);
|
|
670
|
+
const groups = diagram.groups ?? [];
|
|
671
|
+
const group = groups.find((entry) => entry.id === groupId);
|
|
672
|
+
if (group === void 0) return 0;
|
|
673
|
+
const children = groups.filter((entry) => entry.parent === groupId);
|
|
674
|
+
return group.members.length + children.reduce((total, child) => total + subtreeSize(diagram, child.id, seen), 0);
|
|
675
|
+
}
|
|
676
|
+
function ownerOf(diagram, nodeId) {
|
|
677
|
+
return (diagram.groups ?? []).find((group) => group.members.includes(nodeId))?.id;
|
|
678
|
+
}
|
|
679
|
+
function collapse(diagram, folded) {
|
|
680
|
+
const asked = new Set(folded);
|
|
681
|
+
const groups = diagram.groups ?? [];
|
|
682
|
+
if (asked.size === 0 || groups.length === 0) return diagram;
|
|
683
|
+
const byId = new Map(groups.map((group) => [group.id, group]));
|
|
684
|
+
const targets = /* @__PURE__ */ new Set();
|
|
685
|
+
for (const id of asked) {
|
|
686
|
+
const group = byId.get(id);
|
|
687
|
+
if (group !== void 0 && subtreeSize(diagram, id, /* @__PURE__ */ new Set()) > 0) targets.add(id);
|
|
688
|
+
}
|
|
689
|
+
if (targets.size === 0) return diagram;
|
|
690
|
+
for (const id of [...targets]) {
|
|
691
|
+
let cursor = byId.get(id)?.parent;
|
|
692
|
+
const seen = /* @__PURE__ */ new Set();
|
|
693
|
+
while (cursor !== void 0 && !seen.has(cursor)) {
|
|
694
|
+
seen.add(cursor);
|
|
695
|
+
if (targets.has(cursor)) {
|
|
696
|
+
targets.delete(id);
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
cursor = byId.get(cursor)?.parent;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const removedNodes = /* @__PURE__ */ new Set();
|
|
703
|
+
const removedGroups = /* @__PURE__ */ new Set();
|
|
704
|
+
const descend = (groupId, seen) => {
|
|
705
|
+
if (seen.has(groupId)) return;
|
|
706
|
+
seen.add(groupId);
|
|
707
|
+
removedGroups.add(groupId);
|
|
708
|
+
const group = byId.get(groupId);
|
|
709
|
+
if (group === void 0) return;
|
|
710
|
+
for (const member of group.members) removedNodes.add(member);
|
|
711
|
+
for (const child of groups.filter((entry) => entry.parent === groupId)) descend(child.id, seen);
|
|
712
|
+
};
|
|
713
|
+
for (const id of targets) descend(id, /* @__PURE__ */ new Set());
|
|
714
|
+
const foldOf = (id) => {
|
|
715
|
+
for (const target of targets) {
|
|
716
|
+
const inSubtree = (groupId, seen) => {
|
|
717
|
+
if (seen.has(groupId)) return false;
|
|
718
|
+
seen.add(groupId);
|
|
719
|
+
if (groupId === target) return true;
|
|
720
|
+
return groups.filter((entry) => entry.parent === groupId).some((child) => inSubtree(child.id, seen));
|
|
721
|
+
};
|
|
722
|
+
const group = byId.get(target);
|
|
723
|
+
if (group.members.includes(id) || inSubtree(ownerOf(diagram, id) ?? "", /* @__PURE__ */ new Set())) return target;
|
|
724
|
+
}
|
|
725
|
+
return void 0;
|
|
726
|
+
};
|
|
727
|
+
const nodes = [];
|
|
728
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
729
|
+
for (const node of diagram.nodes) {
|
|
730
|
+
if (!removedNodes.has(node.id)) {
|
|
731
|
+
nodes.push(node);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const fold = foldOf(node.id);
|
|
735
|
+
if (fold === void 0 || emitted.has(fold)) continue;
|
|
736
|
+
emitted.add(fold);
|
|
737
|
+
const group = byId.get(fold);
|
|
738
|
+
nodes.push({ id: fold, label: group.label + " (" + String(subtreeSize(diagram, fold, /* @__PURE__ */ new Set())) + ")", role: "normal" });
|
|
739
|
+
}
|
|
740
|
+
for (const id of targets) {
|
|
741
|
+
if (emitted.has(id)) continue;
|
|
742
|
+
const group = byId.get(id);
|
|
743
|
+
nodes.push({ id, label: group.label + " (" + String(subtreeSize(diagram, id, /* @__PURE__ */ new Set())) + ")", role: "normal" });
|
|
744
|
+
}
|
|
745
|
+
const edges = [];
|
|
746
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
747
|
+
for (const edge of diagram.edges) {
|
|
748
|
+
const from = removedNodes.has(edge.from) ? foldOf(edge.from) : edge.from;
|
|
749
|
+
const to = removedNodes.has(edge.to) ? foldOf(edge.to) : edge.to;
|
|
750
|
+
if (from === void 0 || to === void 0) continue;
|
|
751
|
+
if (from === to) continue;
|
|
752
|
+
const key = from + ">" + to + ":" + edge.label;
|
|
753
|
+
if (seenEdges.has(key)) continue;
|
|
754
|
+
seenEdges.add(key);
|
|
755
|
+
edges.push({ ...edge, from, to });
|
|
756
|
+
}
|
|
757
|
+
const adopted = /* @__PURE__ */ new Map();
|
|
758
|
+
for (const id of targets) {
|
|
759
|
+
const parent = byId.get(id)?.parent;
|
|
760
|
+
if (parent === void 0 || targets.has(parent) || removedGroups.has(parent)) continue;
|
|
761
|
+
adopted.set(parent, [...adopted.get(parent) ?? [], id]);
|
|
762
|
+
}
|
|
763
|
+
const remaining = groups.filter((group) => !removedGroups.has(group.id) && targets.has(group.id) === false).map((group) => {
|
|
764
|
+
const extra = adopted.get(group.id);
|
|
765
|
+
if (extra === void 0) return group;
|
|
766
|
+
return { ...group, members: [...group.members.filter((member) => !removedNodes.has(member)), ...extra] };
|
|
767
|
+
});
|
|
768
|
+
const caption = diagram.caption + (targets.size === 1 ? ", 1 group folded" : ", " + String(targets.size) + " groups folded");
|
|
769
|
+
return {
|
|
770
|
+
...diagram,
|
|
771
|
+
nodes,
|
|
772
|
+
edges,
|
|
773
|
+
...remaining.length === 0 ? { groups: void 0 } : { groups: remaining },
|
|
774
|
+
caption
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/render/from-plantuml.ts
|
|
779
|
+
var ARROW = /^(.+?)\s*(<?)(-{2,}|\.{2,})(>?)\s*(.+?)(?:\s*:\s*(.+))?$/;
|
|
780
|
+
var DECLARED = /^(component|package|node|folder|cloud|database|state|artifact|frame|rectangle)\s+(.+)$/;
|
|
781
|
+
var QUOTED_ALIAS = /^["'](.+?)["']\s+as\s+([A-Za-z_][\w-]*)$/;
|
|
782
|
+
var BRACKET_ALIAS = /^\[(.+?)\]\s+as\s+([A-Za-z_][\w-]*)$/;
|
|
783
|
+
var BRACKET = /^\[(.+?)\]$/;
|
|
784
|
+
var UNSUPPORTED2 = /^(actor|participant|usecase|class|object|note|legend|alt|opt|loop|par|group\s|ref\s|activate|deactivate|destroy|create|newpage)\b/;
|
|
785
|
+
var IGNORED = /^(skinparam|!theme|!define|!include|hide|show|scale|left to right direction|top to bottom direction|together)\b/;
|
|
786
|
+
function ensure2(builder, id, label) {
|
|
787
|
+
const existing = builder.nodes.get(id);
|
|
788
|
+
if (existing === void 0) {
|
|
789
|
+
builder.nodes.set(id, { id, role: "normal", ...label === void 0 ? {} : { label } });
|
|
790
|
+
const innermost = builder.open[builder.open.length - 1];
|
|
791
|
+
if (innermost !== void 0 && !innermost.members.includes(id)) innermost.members.push(id);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (label !== void 0 && (existing.label === void 0 || existing.label === "")) existing.label = label;
|
|
795
|
+
}
|
|
796
|
+
function reference(token) {
|
|
797
|
+
const text2 = token.trim();
|
|
798
|
+
const bracket = BRACKET.exec(text2);
|
|
799
|
+
if (bracket !== null) {
|
|
800
|
+
const label = bracket[1].trim();
|
|
801
|
+
return { id: label, label };
|
|
802
|
+
}
|
|
803
|
+
const quoted = /^["'](.+?)["']$/.exec(text2);
|
|
804
|
+
if (quoted !== null) return { id: quoted[1], label: quoted[1] };
|
|
805
|
+
return { id: text2 };
|
|
806
|
+
}
|
|
807
|
+
function fromPlantUml(source, title = "plantuml") {
|
|
808
|
+
const lines = source.split(/\r?\n/);
|
|
809
|
+
const refused = [];
|
|
810
|
+
const builder = { nodes: /* @__PURE__ */ new Map(), edges: [], groups: [], open: [] };
|
|
811
|
+
let startId;
|
|
812
|
+
let seenAny = false;
|
|
813
|
+
let groupCount = 0;
|
|
814
|
+
lines.forEach((raw, index) => {
|
|
815
|
+
const line = raw.replace(/'.*$/, "").trim();
|
|
816
|
+
if (line === "") return;
|
|
817
|
+
const where = "line " + String(index + 1) + ": ";
|
|
818
|
+
if (/^@(start|end)uml\b/.test(line)) return;
|
|
819
|
+
seenAny = true;
|
|
820
|
+
if (/^title\b/.test(line)) return;
|
|
821
|
+
if (IGNORED.test(line)) return;
|
|
822
|
+
if (UNSUPPORTED2.test(line)) {
|
|
823
|
+
refused.push(where + "`" + (line.split(/\s/)[0] ?? "") + "` is not a diagram this renderer draws");
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (line === "}") {
|
|
827
|
+
const closing = builder.open.pop();
|
|
828
|
+
if (closing !== void 0) {
|
|
829
|
+
const container = builder.open[builder.open.length - 1];
|
|
830
|
+
if (container !== void 0) closing.parent = container.id;
|
|
831
|
+
builder.groups.push(closing);
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (line.endsWith("{")) {
|
|
836
|
+
const declaration = line.slice(0, -1).trim();
|
|
837
|
+
const declared2 = DECLARED.exec(declaration);
|
|
838
|
+
const label = declared2 === null ? declaration.replace(/^["']|["']$/g, "") : declared2[2].replace(/^["']|["']$/g, "");
|
|
839
|
+
groupCount += 1;
|
|
840
|
+
builder.open.push({ id: "group-" + String(groupCount), label, members: [] });
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const declared = DECLARED.exec(line);
|
|
844
|
+
if (declared !== null) {
|
|
845
|
+
const rest = declared[2].trim();
|
|
846
|
+
const quoted = QUOTED_ALIAS.exec(rest);
|
|
847
|
+
if (quoted !== null) {
|
|
848
|
+
ensure2(builder, quoted[2], quoted[1]);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const bracketed = BRACKET_ALIAS.exec(rest);
|
|
852
|
+
if (bracketed !== null) {
|
|
853
|
+
ensure2(builder, bracketed[2], bracketed[1]);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (/^["'](.+?)["']$/.test(rest)) return;
|
|
857
|
+
const reference_ = reference(rest);
|
|
858
|
+
ensure2(builder, reference_.id, reference_.label);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (line === "[*] --> [*]") return;
|
|
862
|
+
const startMarker = /^\[\*\]\s*-->\s*(.+)$/.exec(line);
|
|
863
|
+
if (startMarker !== null) {
|
|
864
|
+
const target = reference(startMarker[1]);
|
|
865
|
+
ensure2(builder, target.id, target.label);
|
|
866
|
+
if (startId === void 0) startId = target.id;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const arrow = ARROW.exec(line);
|
|
870
|
+
if (arrow !== null) {
|
|
871
|
+
const left = arrow[2] === "<" ? arrow[5] : arrow[1];
|
|
872
|
+
const right = arrow[2] === "<" ? arrow[1] : arrow[5];
|
|
873
|
+
const from = reference(left);
|
|
874
|
+
const to = reference(right);
|
|
875
|
+
ensure2(builder, from.id, from.label);
|
|
876
|
+
ensure2(builder, to.id, to.label);
|
|
877
|
+
builder.edges.push({ from: from.id, to: to.id, label: (arrow[6] ?? "").trim(), ...arrow[3].startsWith(".") ? { dashed: true } : {} });
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const single = BRACKET.exec(line);
|
|
881
|
+
if (single !== null) {
|
|
882
|
+
ensure2(builder, single[1].trim(), single[1].trim());
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
refused.push(where + "`" + line + "` is not something this renderer draws");
|
|
886
|
+
});
|
|
887
|
+
if (!seenAny || builder.nodes.size === 0) {
|
|
888
|
+
return { diagram: emptyDiagram(title, "no nodes found"), refused };
|
|
889
|
+
}
|
|
890
|
+
const diagram = {
|
|
891
|
+
kind: startId === void 0 ? "graph" : "machine",
|
|
892
|
+
title,
|
|
893
|
+
...startId === void 0 ? {} : { init: startId },
|
|
894
|
+
nodes: [...builder.nodes.values()],
|
|
895
|
+
edges: builder.edges,
|
|
896
|
+
...builder.groups.length === 0 ? {} : { groups: builder.groups },
|
|
897
|
+
caption: "plantuml, " + String(builder.nodes.size) + " box" + (builder.nodes.size === 1 ? "" : "es") + ", " + String(builder.edges.length) + " arrow" + (builder.edges.length === 1 ? "" : "s") + (refused.length === 0 ? "" : ", " + String(refused.length) + " line(s) refused")
|
|
898
|
+
};
|
|
899
|
+
return { diagram, refused };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/render/svg.ts
|
|
903
|
+
var n = (value) => {
|
|
904
|
+
const rounded = Math.round(value * 10) / 10;
|
|
905
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
906
|
+
};
|
|
907
|
+
var escape = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
908
|
+
var writers = {
|
|
909
|
+
// A boundary is a dashed rectangle with its name in the top-left corner, drawn under everything else.
|
|
910
|
+
group: (shape, colours) => {
|
|
911
|
+
const stroke = colours[shape.colour];
|
|
912
|
+
return '<g data-group-id="' + escape(shape.id) + '" style="cursor:pointer"><rect x="' + n(shape.x) + '" y="' + n(shape.y) + '" width="' + n(shape.width) + '" height="' + n(shape.height) + '" rx="8" fill="none" stroke="' + stroke + '" stroke-width="1" stroke-dasharray="4 3" /><text x="' + n(shape.x + 6) + '" y="' + n(shape.y + 12) + '" font-size="10" fill="' + stroke + '">' + escape(shape.label) + "</text></g>";
|
|
913
|
+
},
|
|
914
|
+
box: (shape, colours) => {
|
|
915
|
+
const stroke = colours[shape.colour];
|
|
916
|
+
const width = shape.initial ? 2.5 : 1;
|
|
917
|
+
return '<rect x="' + n(shape.x) + '" y="' + n(shape.y) + '" width="' + n(shape.width) + '" height="' + n(shape.height) + '" rx="6" fill="' + colours.surface + '" stroke="' + stroke + '" stroke-width="' + n(width) + '" /><text x="' + n(shape.x + shape.width / 2) + '" y="' + n(shape.y + shape.height / 2 + 4) + '" font-size="11" text-anchor="middle" fill="' + stroke + '">' + escape(shape.label) + "</text>";
|
|
918
|
+
},
|
|
919
|
+
link: (shape, colours) => {
|
|
920
|
+
const stroke = colours[shape.colour];
|
|
921
|
+
const arrow = arrowHead(shape.x1, shape.y1, shape.x2, shape.y2, stroke);
|
|
922
|
+
return '<line x1="' + n(shape.x1) + '" y1="' + n(shape.y1) + '" x2="' + n(shape.x2) + '" y2="' + n(shape.y2) + '" stroke="' + stroke + '" stroke-width="1" />' + arrow + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
|
|
923
|
+
},
|
|
924
|
+
path: (shape, colours) => {
|
|
925
|
+
const stroke = colours[shape.colour];
|
|
926
|
+
const points = shape.points.map((point) => n(point.x) + "," + n(point.y)).join(" ");
|
|
927
|
+
const last = shape.points[shape.points.length - 1];
|
|
928
|
+
const before = shape.points[shape.points.length - 2] ?? last;
|
|
929
|
+
return '<polyline points="' + points + '" fill="none" stroke="' + stroke + '" stroke-width="1" stroke-linejoin="round" />' + arrowHead(before.x, before.y, last.x, last.y, stroke) + edgeLabel(shape.label, shape.labelX, shape.labelY, colours);
|
|
930
|
+
},
|
|
931
|
+
title: (shape, colours) => {
|
|
932
|
+
return '<text x="' + n(shape.x) + '" y="' + n(shape.y) + '" font-size="12" fill="' + colours[shape.colour] + '">' + escape(shape.text) + "</text>";
|
|
933
|
+
},
|
|
934
|
+
caption: (shape, colours) => {
|
|
935
|
+
const colour = colours[shape.colour];
|
|
936
|
+
const lines = shape.lines.map((line, index) => '<tspan x="' + n(shape.x) + '" dy="' + (index === 0 ? 0 : 15) + '">' + escape(line) + "</tspan>").join("");
|
|
937
|
+
return '<text x="' + n(shape.x) + '" y="' + n(shape.y + 11) + '" font-size="10" fill="' + colour + '">' + lines + "</text>";
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
function arrowHead(x1, y1, x2, y2, stroke) {
|
|
941
|
+
const length = Math.hypot(x2 - x1, y2 - y1);
|
|
942
|
+
if (length < 1) return "";
|
|
943
|
+
const ux = (x2 - x1) / length;
|
|
944
|
+
const uy = (y2 - y1) / length;
|
|
945
|
+
const size = 7;
|
|
946
|
+
const half = 3.2;
|
|
947
|
+
const baseX = x2 - ux * size;
|
|
948
|
+
const baseY = y2 - uy * size;
|
|
949
|
+
const left = n(baseX - uy * half) + "," + n(baseY + ux * half);
|
|
950
|
+
const right = n(baseX + uy * half) + "," + n(baseY - ux * half);
|
|
951
|
+
return '<polygon points="' + n(x2) + "," + n(y2) + " " + left + " " + right + '" fill="' + stroke + '" />';
|
|
952
|
+
}
|
|
953
|
+
function edgeLabel(label, x, y, colours) {
|
|
954
|
+
if (typeof label !== "string" || label === "") return "";
|
|
955
|
+
return '<text x="' + n(x) + '" y="' + n(y) + '" font-size="10" fill="' + colours.muted + '">' + escape(label) + "</text>";
|
|
956
|
+
}
|
|
957
|
+
function toSvg(canvas, colours, label) {
|
|
958
|
+
let dropped = 0;
|
|
959
|
+
const body = [];
|
|
960
|
+
for (const shape of canvas.shapes) {
|
|
961
|
+
const writer = writers[shape.kind];
|
|
962
|
+
if (writer === void 0) {
|
|
963
|
+
dropped += 1;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
body.push(writer(shape, colours));
|
|
967
|
+
}
|
|
968
|
+
const header = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + n(canvas.width) + " " + n(canvas.height) + '" width="' + n(canvas.width) + '" height="' + n(canvas.height) + '" role="img"' + (label === void 0 ? "" : ' aria-label="' + escape(label) + '"') + ' font-family="system-ui, -apple-system, Segoe UI, sans-serif">';
|
|
969
|
+
const background = '<rect x="0" y="0" width="' + n(canvas.width) + '" height="' + n(canvas.height) + '" fill="' + colours.background + '" />';
|
|
970
|
+
return { markup: header + background + body.join("") + "</svg>", dropped };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/render/index.ts
|
|
974
|
+
function detectNotation(source) {
|
|
975
|
+
const head = source.split(/\r?\n/).map((line) => line.trim()).find((line) => line !== "" && !line.startsWith("'") && !line.startsWith("//"));
|
|
976
|
+
if (head === void 0) return "mermaid";
|
|
977
|
+
return /^@start/.test(head) ? "plantuml" : "mermaid";
|
|
978
|
+
}
|
|
979
|
+
function render(input, overrides = {}, options = {}) {
|
|
980
|
+
const notation = input.notation === void 0 || input.notation === "auto" ? detectNotation(input.source) : input.notation;
|
|
981
|
+
const parsed = notation === "plantuml" ? fromPlantUml(input.source, input.title ?? "plantuml") : fromMermaid(input.source, input.title ?? "mermaid");
|
|
982
|
+
const diagram = input.folded === void 0 ? parsed.diagram : collapse(parsed.diagram, input.folded);
|
|
983
|
+
const { canvas, colours } = paintWith(diagram, overrides, options);
|
|
984
|
+
const written = toSvg(canvas, colours, diagram.title + ": " + diagram.caption);
|
|
985
|
+
return {
|
|
986
|
+
diagram,
|
|
987
|
+
canvas,
|
|
988
|
+
colours,
|
|
989
|
+
markup: written.markup,
|
|
990
|
+
notation,
|
|
991
|
+
refused: parsed.refused,
|
|
992
|
+
dropped: written.dropped
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/render/viewmodel.ts
|
|
997
|
+
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
998
|
+
var array = (value) => Array.isArray(value) ? value : [];
|
|
999
|
+
var text = (value) => typeof value === "string" && value !== "" ? value : void 0;
|
|
1000
|
+
function reportText(content) {
|
|
1001
|
+
const parts = [];
|
|
1002
|
+
for (const entry of array(content)) {
|
|
1003
|
+
const block = record(entry);
|
|
1004
|
+
const body = text(block?.text);
|
|
1005
|
+
if (body !== void 0) parts.push(body);
|
|
1006
|
+
}
|
|
1007
|
+
return parts.join(String.fromCharCode(10));
|
|
1008
|
+
}
|
|
1009
|
+
function readReport(content) {
|
|
1010
|
+
const body = reportText(content);
|
|
1011
|
+
if (body === "") return {};
|
|
1012
|
+
const verdict = /verdict=(\w+)/.exec(body)?.[1];
|
|
1013
|
+
const named = /\[error\/([A-Z0-9_]+)\]\s*(.*)/.exec(body);
|
|
1014
|
+
if (named !== null) return { verdict, code: named[1], message: named[2] };
|
|
1015
|
+
const first = /first error:\s*([A-Z0-9_]+)/.exec(body);
|
|
1016
|
+
return first === null ? { verdict } : { verdict, code: first[1] };
|
|
1017
|
+
}
|
|
1018
|
+
function readModel(args) {
|
|
1019
|
+
const view = record(args);
|
|
1020
|
+
if (view === void 0) return void 0;
|
|
1021
|
+
const read = view.value;
|
|
1022
|
+
if (typeof read === "function") return record(read("model"));
|
|
1023
|
+
return record(view.model);
|
|
1024
|
+
}
|
|
1025
|
+
function machineOf(model) {
|
|
1026
|
+
if (model === void 0) return void 0;
|
|
1027
|
+
const states = array(model.states).map(record).filter((entry) => entry !== void 0 && typeof entry.id === "string");
|
|
1028
|
+
if (states.length === 0) return void 0;
|
|
1029
|
+
const edges = array(model.transitions).map(record).filter((entry) => entry !== void 0 && typeof entry.from === "string" && typeof entry.to === "string").map((entry) => ({ from: String(entry.from), to: String(entry.to), label: text(entry.event) ?? "" }));
|
|
1030
|
+
return {
|
|
1031
|
+
nodes: states.map((state) => ({ id: String(state.id), role: "normal" })),
|
|
1032
|
+
edges,
|
|
1033
|
+
...typeof model.init === "string" ? { init: model.init } : {}
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function captionOf(report) {
|
|
1037
|
+
const verdict = report.verdict ?? "no report yet";
|
|
1038
|
+
if (report.code === void 0) return verdict;
|
|
1039
|
+
const message = report.message ?? "";
|
|
1040
|
+
return verdict + " \u2014 " + report.code + (message === "" ? "" : ": " + message);
|
|
1041
|
+
}
|
|
1042
|
+
function blame(nodes, report) {
|
|
1043
|
+
if (report.message === void 0) return nodes;
|
|
1044
|
+
const message = report.message;
|
|
1045
|
+
const named = nodes.filter((node) => {
|
|
1046
|
+
const escaped = node.id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1047
|
+
return new RegExp("(?<![A-Za-z0-9_])" + escaped + "(?![A-Za-z0-9_])").test(message);
|
|
1048
|
+
}).map((node) => node.id);
|
|
1049
|
+
if (named.length === 0) return nodes;
|
|
1050
|
+
return nodes.map((node) => named.includes(node.id) ? { ...node, role: "error" } : node);
|
|
1051
|
+
}
|
|
1052
|
+
function viewModel(input) {
|
|
1053
|
+
const block = record(input.block);
|
|
1054
|
+
const title = text(block?.name) ?? text(input.toolName) ?? "tool";
|
|
1055
|
+
const report = readReport(block?.content);
|
|
1056
|
+
const parsed = record(block?.result);
|
|
1057
|
+
const findings = array(parsed?.findings).map(record);
|
|
1058
|
+
const firstError = findings.find((finding) => finding?.severity === "error");
|
|
1059
|
+
const merged = {
|
|
1060
|
+
verdict: text(parsed?.verdict) ?? report.verdict,
|
|
1061
|
+
...report.code === void 0 && typeof firstError?.code === "string" ? { code: firstError.code } : { code: report.code },
|
|
1062
|
+
...report.message === void 0 && typeof firstError?.message === "string" ? { message: firstError.message } : { message: report.message }
|
|
1063
|
+
};
|
|
1064
|
+
const caption = captionOf(merged);
|
|
1065
|
+
const machine = machineOf(readModel(block?.args));
|
|
1066
|
+
if (machine === void 0) {
|
|
1067
|
+
return { kind: "none", title, nodes: [], edges: [], caption };
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
kind: "machine",
|
|
1071
|
+
title,
|
|
1072
|
+
...machine.init === void 0 ? {} : { init: machine.init },
|
|
1073
|
+
nodes: blame(machine.nodes, merged),
|
|
1074
|
+
edges: machine.edges,
|
|
1075
|
+
caption
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/client.ts
|
|
1080
|
+
var TOOL_NAMES = ["model_verify"];
|
|
1081
|
+
function themeMode() {
|
|
1082
|
+
try {
|
|
1083
|
+
const scope = globalThis;
|
|
1084
|
+
const body = scope.document?.body;
|
|
1085
|
+
const computed = typeof scope.getComputedStyle === "function" && body !== void 0 ? scope.getComputedStyle(body) : void 0;
|
|
1086
|
+
const colour = computed?.backgroundColor ?? "";
|
|
1087
|
+
const rgb = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(colour);
|
|
1088
|
+
if (rgb !== null) {
|
|
1089
|
+
const [r, g, b] = [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
|
|
1090
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 < 0.5 ? "dark" : "light";
|
|
1091
|
+
}
|
|
1092
|
+
} catch {
|
|
1093
|
+
}
|
|
1094
|
+
return "light";
|
|
1095
|
+
}
|
|
1096
|
+
function surfaceStyle(colours) {
|
|
1097
|
+
return { background: colours.background, color: colours.text, padding: "4px 0", overflowX: "auto" };
|
|
1098
|
+
}
|
|
1099
|
+
var WHITEBOARD_ID = "model-everything-diagram";
|
|
1100
|
+
var WHITEBOARD_SAMPLE = ["graph TD", " a[Idea] --> b{Feasible?}", " b -->|yes| c[Build]", " b -->|no| d[Revise]", " d --> a"].join("\n");
|
|
1101
|
+
function viewFor(React, overrides = {}) {
|
|
1102
|
+
return (props) => {
|
|
1103
|
+
const diagram = viewModel({ phase: props.phase, toolName: props.toolName, block: props.block });
|
|
1104
|
+
const { canvas, colours } = paintWith(diagram, overrides, { mode: themeMode() });
|
|
1105
|
+
const written = toSvg(canvas, colours, diagram.kind === "none" ? diagram.caption : diagram.title + ": " + diagram.caption);
|
|
1106
|
+
return React.createElement("div", {
|
|
1107
|
+
style: surfaceStyle(colours),
|
|
1108
|
+
dangerouslySetInnerHTML: { __html: written.markup }
|
|
1109
|
+
});
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
function whiteboardFor(React, overrides = {}) {
|
|
1113
|
+
const h = (type, props, ...children) => React.createElement(type, props, ...children);
|
|
1114
|
+
return function Whiteboard() {
|
|
1115
|
+
const useState = React.useState;
|
|
1116
|
+
if (useState === void 0) {
|
|
1117
|
+
return h(
|
|
1118
|
+
"div",
|
|
1119
|
+
{ style: { padding: "8px" } },
|
|
1120
|
+
h("textarea", {
|
|
1121
|
+
value: WHITEBOARD_SAMPLE,
|
|
1122
|
+
readOnly: true,
|
|
1123
|
+
spellcheck: false,
|
|
1124
|
+
rows: WHITEBOARD_SAMPLE.split("\n").length,
|
|
1125
|
+
style: { width: "100%", font: "12px ui-monospace, SFMono-Regular, Menlo, monospace" }
|
|
1126
|
+
}),
|
|
1127
|
+
// The picker is present but inert: a host without hooks cannot hold the choice, and hiding the
|
|
1128
|
+
// option would make the board look like it has none.
|
|
1129
|
+
h(
|
|
1130
|
+
"select",
|
|
1131
|
+
{ disabled: true, style: { margin: "4px 0", fontSize: "12px" } },
|
|
1132
|
+
...schemeNames().map((name) => h("option", { value: name, selected: name === "default" }, name))
|
|
1133
|
+
),
|
|
1134
|
+
whiteboardBody(React, overrides, WHITEBOARD_SAMPLE, 1)
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
const [source, setSource] = useState(WHITEBOARD_SAMPLE);
|
|
1138
|
+
const [zoom, setZoom] = useState(1);
|
|
1139
|
+
const [scheme, setScheme] = useState("default");
|
|
1140
|
+
const [fit, setFit] = useState(true);
|
|
1141
|
+
const [folded, setFolded] = useState([]);
|
|
1142
|
+
const drawn = drawSample(React, overrides, source, fit ? 0 : zoom, scheme, folded);
|
|
1143
|
+
const textarea = h("textarea", {
|
|
1144
|
+
value: source,
|
|
1145
|
+
spellcheck: false,
|
|
1146
|
+
rows: Math.min(12, Math.max(4, String(source).split("\n").length)),
|
|
1147
|
+
style: { width: "100%", font: "12px ui-monospace, SFMono-Regular, Menlo, monospace", padding: "6px", resize: "vertical" },
|
|
1148
|
+
onInput: (event) => setSource(String(event?.target?.value ?? ""))
|
|
1149
|
+
});
|
|
1150
|
+
const controls = h(
|
|
1151
|
+
"div",
|
|
1152
|
+
{ style: { display: "flex", gap: "6px", alignItems: "center", padding: "4px 0", flexWrap: "wrap" } },
|
|
1153
|
+
h("button", { type: "button", onClick: () => {
|
|
1154
|
+
setFit(false);
|
|
1155
|
+
setZoom(Math.max(0.5, Math.round((Number(zoom) - 0.25) * 100) / 100));
|
|
1156
|
+
} }, "\u2212"),
|
|
1157
|
+
h("span", { style: { fontSize: "12px", opacity: 0.7 } }, fit ? "fit" : String(Math.round(Number(zoom) * 100)) + "%"),
|
|
1158
|
+
h("button", { type: "button", onClick: () => {
|
|
1159
|
+
setFit(false);
|
|
1160
|
+
setZoom(Math.min(3, Math.round((Number(zoom) + 0.25) * 100) / 100));
|
|
1161
|
+
} }, "+"),
|
|
1162
|
+
h("button", { type: "button", onClick: () => {
|
|
1163
|
+
setFit(false);
|
|
1164
|
+
setZoom(1);
|
|
1165
|
+
} }, "reset"),
|
|
1166
|
+
h("button", { type: "button", title: "Scale to the pane width", onClick: () => setFit(!fit) }, fit ? "fit: on" : "fit: off"),
|
|
1167
|
+
folded.length === 0 ? null : h("button", { type: "button", title: "Unfold every group", onClick: () => setFolded([]) }, "unfold " + String(folded.length)),
|
|
1168
|
+
h(
|
|
1169
|
+
"select",
|
|
1170
|
+
{
|
|
1171
|
+
value: scheme,
|
|
1172
|
+
title: "Colour scheme",
|
|
1173
|
+
style: { marginLeft: "auto", fontSize: "12px" },
|
|
1174
|
+
onChange: (event) => setScheme(String(event?.target?.value ?? "default"))
|
|
1175
|
+
},
|
|
1176
|
+
...schemeNames().map((name) => h("option", { value: name, selected: name === scheme }, name))
|
|
1177
|
+
)
|
|
1178
|
+
);
|
|
1179
|
+
return h(
|
|
1180
|
+
"div",
|
|
1181
|
+
{ style: { display: "flex", flexDirection: "column", gap: "4px", height: "100%", boxSizing: "border-box", padding: "8px" } },
|
|
1182
|
+
textarea,
|
|
1183
|
+
controls,
|
|
1184
|
+
// The drawing takes the rest of the pane and scrolls inside it, so an expanded tab uses its room.
|
|
1185
|
+
h(
|
|
1186
|
+
"div",
|
|
1187
|
+
{
|
|
1188
|
+
style: { flex: "1 1 auto", minHeight: "0", overflow: "auto" },
|
|
1189
|
+
// A boundary carries its id in the markup, so one listener on the container is enough: no per-rect
|
|
1190
|
+
// handler, and the handling survives a re-render that replaced the whole SVG.
|
|
1191
|
+
onClick: (event) => {
|
|
1192
|
+
const hit = event?.target?.closest?.("[data-group-id]");
|
|
1193
|
+
const id = hit?.getAttribute?.("data-group-id");
|
|
1194
|
+
if (typeof id !== "string" || id === "") return;
|
|
1195
|
+
setFolded(folded.includes(id) ? folded.filter((entry) => entry !== id) : [...folded, id]);
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
drawn
|
|
1199
|
+
)
|
|
1200
|
+
);
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function drawSample(React, overrides, source, zoom, scheme, folded = []) {
|
|
1204
|
+
return whiteboardBody(React, overrides, source, zoom, scheme, folded);
|
|
1205
|
+
}
|
|
1206
|
+
function whiteboardBody(React, overrides, source, zoom, scheme = "default", folded = []) {
|
|
1207
|
+
if (String(source).trim() === "") {
|
|
1208
|
+
return React.createElement("p", { style: { fontSize: "12px", opacity: 0.7 } }, "Paste Mermaid or PlantUML above.");
|
|
1209
|
+
}
|
|
1210
|
+
const drawn = render({ source: String(source), folded }, overrides, { mode: themeMode(), scheme });
|
|
1211
|
+
const zoomed = zoom === 0 ? drawn.markup.replace("<svg ", '<svg style="width: 100%; height: auto; display: block" ') : drawn.markup.replace("<svg ", '<svg style="zoom: ' + String(zoom) + '; display: block" ');
|
|
1212
|
+
const refused = drawn.refused.length === 0 ? null : React.createElement("pre", {
|
|
1213
|
+
style: { fontSize: "11px", color: "#b7791f", whiteSpace: "pre-wrap", margin: "6px 0" }
|
|
1214
|
+
}, drawn.refused.join("\n"));
|
|
1215
|
+
return React.createElement(
|
|
1216
|
+
"div",
|
|
1217
|
+
{ style: { background: drawn.colours.background, color: drawn.colours.text, borderRadius: "6px" } },
|
|
1218
|
+
React.createElement("div", { dangerouslySetInnerHTML: { __html: zoomed } }),
|
|
1219
|
+
refused
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
var SIDEBAR_POLL_MS = 250;
|
|
1223
|
+
var SIDEBAR_POLL_ATTEMPTS = 40;
|
|
1224
|
+
function mountWhiteboard(React, ctx, options = {}) {
|
|
1225
|
+
const schedule = options.schedule ?? ((run, delay) => {
|
|
1226
|
+
const timer = globalThis.setTimeout;
|
|
1227
|
+
return typeof timer === "function" ? timer(run, delay) : void 0;
|
|
1228
|
+
});
|
|
1229
|
+
let attempts = 0;
|
|
1230
|
+
const attempt = () => {
|
|
1231
|
+
attempts += 1;
|
|
1232
|
+
const tabs = typeof ctx.get === "function" ? ctx.get("sidebarRightTabs") : void 0;
|
|
1233
|
+
if (tabs === void 0 || typeof tabs.register !== "function") {
|
|
1234
|
+
if (attempts < SIDEBAR_POLL_ATTEMPTS) schedule(attempt, SIDEBAR_POLL_MS);
|
|
1235
|
+
return void 0;
|
|
1236
|
+
}
|
|
1237
|
+
tabs.register({
|
|
1238
|
+
id: WHITEBOARD_ID,
|
|
1239
|
+
kind: "panel",
|
|
1240
|
+
keepMounted: true,
|
|
1241
|
+
title: () => "Diagram",
|
|
1242
|
+
guide: [
|
|
1243
|
+
{
|
|
1244
|
+
id: "diagram-whiteboard",
|
|
1245
|
+
order: 120,
|
|
1246
|
+
title: () => "Diagram",
|
|
1247
|
+
description: () => "Paste Mermaid or PlantUML and read the drawing"
|
|
1248
|
+
}
|
|
1249
|
+
]
|
|
1250
|
+
});
|
|
1251
|
+
return ctx.slots.register({ name: "sidebar.right.pane.tab", key: WHITEBOARD_ID }, whiteboardFor(React));
|
|
1252
|
+
};
|
|
1253
|
+
return attempt();
|
|
1254
|
+
}
|
|
1255
|
+
function factory(require2) {
|
|
1256
|
+
const hostRequire = require2;
|
|
1257
|
+
const React = hostRequire("react");
|
|
1258
|
+
return {
|
|
1259
|
+
inject: ["slots"],
|
|
1260
|
+
mount: (ctx, options = {}) => mountWhiteboard(React, ctx, options),
|
|
1261
|
+
apply(ctx) {
|
|
1262
|
+
for (const name of TOOL_NAMES) {
|
|
1263
|
+
ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({ name: "tool.call.toolview", key: name }, viewFor(React)));
|
|
1264
|
+
}
|
|
1265
|
+
ctx.slots.inject("sidebar.right.pane.tab", () => mountWhiteboard(React, ctx));
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const create = module.exports.default ?? module.exports;
|
|
1271
|
+
return create(require);
|
|
1272
|
+
},
|
|
1273
|
+
});
|