@zenuml/core 3.46.0 → 3.46.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/.claude/skills/dia-scoring/SKILL.md +139 -0
- package/.claude/skills/dia-scoring/agents/openai.yaml +7 -0
- package/.claude/skills/dia-scoring/references/selectors-and-keys.md +253 -0
- package/CLAUDE.md +1 -1
- package/bun.lock +25 -11
- package/cy/canonical-history.html +908 -0
- package/cy/compare-case.html +357 -0
- package/cy/compare-cases.js +824 -0
- package/cy/compare.html +35 -0
- package/cy/diff-algorithm.js +199 -0
- package/cy/element-report.html +705 -0
- package/cy/icons-test.html +29 -0
- package/cy/legacy-vs-html.html +291 -0
- package/cy/native-diff-ext/background.js +60 -0
- package/cy/native-diff-ext/bridge.js +26 -0
- package/cy/native-diff-ext/content.js +194 -0
- package/cy/parity-test.html +122 -0
- package/cy/return-in-nested-if.html +29 -0
- package/cy/svg-preview.html +56 -0
- package/cy/svg-test.html +21 -0
- package/cy/theme-default-test.html +28 -0
- package/dist/stats.html +1 -1
- package/dist/zenuml.esm.mjs +16352 -15223
- package/dist/zenuml.js +701 -575
- package/docs/superpowers/plans/2026-03-23-svg-parity-features.md +283 -0
- package/index.html +568 -73
- package/package.json +15 -4
- package/scripts/analyze-compare-case/collect-data.mjs +991 -0
- package/scripts/analyze-compare-case/config.mjs +102 -0
- package/scripts/analyze-compare-case/geometry.mjs +101 -0
- package/scripts/analyze-compare-case/native-diff.mjs +224 -0
- package/scripts/analyze-compare-case/output.mjs +74 -0
- package/scripts/analyze-compare-case/panel-diff.mjs +114 -0
- package/scripts/analyze-compare-case/report.mjs +157 -0
- package/scripts/analyze-compare-case/residual-scopes.mjs +325 -0
- package/scripts/analyze-compare-case/scoring.mjs +816 -0
- package/scripts/analyze-compare-case.mjs +149 -0
- package/scripts/snapshot-dual.js +34 -34
- package/skills/dia-scoring/SKILL.md +129 -0
- package/skills/dia-scoring/agents/openai.yaml +7 -0
- package/skills/dia-scoring/references/selectors-and-keys.md +253 -0
- package/test-setup.ts +8 -0
- package/types/index.d.ts +56 -0
- package/vite.config.ts +4 -0
- package/dist/10029-icon-service-Function-Apps-ObflOLuF.js +0 -5
- package/dist/Res_AWS-Identity-Access-Management_IAM-Access-Analyzer_48-BPq60XMY.js +0 -11
- package/dist/Res_AWS-Lambda_Lambda-Function_48-Co38UB_2.js +0 -12
- package/dist/Res_Amazon-EC2_Instance_48-CRaqbNUl.js +0 -12
- package/dist/Res_Amazon-Simple-Notification-Service_Topic_48-q13mxUeM.js +0 -11
- package/dist/Res_Amazon-Simple-Queue-Service_Queue_48-D2-8gbFw.js +0 -11
- package/dist/Robustness_Diagram_Boundary-nYnmTPs8.js +0 -10
- package/dist/Robustness_Diagram_Control-DLNLoMxd.js +0 -11
- package/dist/Robustness_Diagram_Entity-Be3kcbIE.js +0 -11
- package/dist/actor-BMj_HFpo.js +0 -11
- package/dist/database-BKHQQWQK.js +0 -8
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What this file does:
|
|
3
|
+
* Extracts semantic layout data from the live compare-case page inside Playwright.
|
|
4
|
+
*
|
|
5
|
+
* High-level flow:
|
|
6
|
+
* - Runs in the page context after fonts are ready.
|
|
7
|
+
* - Finds HTML and SVG roots for the current compare-case.
|
|
8
|
+
* - Collects labels, per-letter glyph boxes, numbers, arrows, participant headers,
|
|
9
|
+
* icons, and participant boxes from both renderers.
|
|
10
|
+
* - Returns normalized geometry that later scoring modules can compare directly.
|
|
11
|
+
*
|
|
12
|
+
* This file does not score anything. Its only job is to turn the live page into
|
|
13
|
+
* structured measurement data.
|
|
14
|
+
*
|
|
15
|
+
* Example input:
|
|
16
|
+
* A Playwright `page` already loaded with
|
|
17
|
+
* `http://localhost:8080/cy/compare-case.html?case=async-2a`
|
|
18
|
+
*
|
|
19
|
+
* Example output:
|
|
20
|
+
* `{ htmlLabels, svgLabels, htmlNumbers, svgNumbers, htmlArrows, svgArrows, htmlParticipants, svgParticipants, ... }`
|
|
21
|
+
*/
|
|
22
|
+
export async function collectLabelData(page) {
|
|
23
|
+
return page.evaluate(async () => {
|
|
24
|
+
await document.fonts.ready;
|
|
25
|
+
|
|
26
|
+
function relRect(rect, rootRect) {
|
|
27
|
+
return {
|
|
28
|
+
x: rect.left - rootRect.left,
|
|
29
|
+
y: rect.top - rootRect.top,
|
|
30
|
+
w: rect.width,
|
|
31
|
+
h: rect.height,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function elementRect(el, rootRect) {
|
|
36
|
+
if (!el) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const rect = el.getBoundingClientRect();
|
|
40
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return relRect(rect, rootRect);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function strokedElementOuterRect(el, rootRect) {
|
|
47
|
+
const box = elementRect(el, rootRect);
|
|
48
|
+
if (!box) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const strokeWidth = parseFloat(getComputedStyle(el).strokeWidth || "0") || 0;
|
|
52
|
+
if (strokeWidth <= 0) {
|
|
53
|
+
return box;
|
|
54
|
+
}
|
|
55
|
+
const half = strokeWidth / 2;
|
|
56
|
+
return {
|
|
57
|
+
x: box.x - half,
|
|
58
|
+
y: box.y - half,
|
|
59
|
+
w: box.w + strokeWidth,
|
|
60
|
+
h: box.h + strokeWidth,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function pushBox(parts, part, box) {
|
|
65
|
+
if (!box || box.w <= 0 || box.h <= 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
parts.push({ part, box });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function unionRect(rects) {
|
|
72
|
+
if (!rects || rects.length === 0) {
|
|
73
|
+
return { x: 0, y: 0, w: 0, h: 0 };
|
|
74
|
+
}
|
|
75
|
+
const left = Math.min(...rects.map((rect) => rect.x));
|
|
76
|
+
const top = Math.min(...rects.map((rect) => rect.y));
|
|
77
|
+
const right = Math.max(...rects.map((rect) => rect.x + rect.w));
|
|
78
|
+
const bottom = Math.max(...rects.map((rect) => rect.y + rect.h));
|
|
79
|
+
return { x: left, y: top, w: right - left, h: bottom - top };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function arrowEndpointsFromBox(box) {
|
|
83
|
+
return {
|
|
84
|
+
left_x: box.x,
|
|
85
|
+
right_x: box.x + box.w,
|
|
86
|
+
width: box.w,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function collectTextNodes(el) {
|
|
91
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
92
|
+
const nodes = [];
|
|
93
|
+
let cursor = 0;
|
|
94
|
+
while (walker.nextNode()) {
|
|
95
|
+
const node = walker.currentNode;
|
|
96
|
+
const text = node.textContent ?? "";
|
|
97
|
+
if (!text) continue;
|
|
98
|
+
nodes.push({ node, start: cursor, end: cursor + text.length });
|
|
99
|
+
cursor += text.length;
|
|
100
|
+
}
|
|
101
|
+
return nodes;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function locateOffset(nodes, offset) {
|
|
105
|
+
for (const entry of nodes) {
|
|
106
|
+
if (offset >= entry.start && offset <= entry.end) {
|
|
107
|
+
return { node: entry.node, offset: offset - entry.start };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const last = nodes[nodes.length - 1];
|
|
111
|
+
return last ? { node: last.node, offset: last.node.textContent.length } : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function segmentText(text) {
|
|
115
|
+
if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
|
|
116
|
+
return Array.from(new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)).map(
|
|
117
|
+
(part) => ({ grapheme: part.segment, start: part.index, end: part.index + part.segment.length }),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const chars = Array.from(text);
|
|
121
|
+
let cursor = 0;
|
|
122
|
+
return chars.map((grapheme) => {
|
|
123
|
+
const entry = { grapheme, start: cursor, end: cursor + grapheme.length };
|
|
124
|
+
cursor += grapheme.length;
|
|
125
|
+
return entry;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function glyphBoxesForElement(el, rootRect) {
|
|
130
|
+
const text = (el.textContent ?? "").trim();
|
|
131
|
+
const sourceText = el.textContent ?? "";
|
|
132
|
+
const trimStart = sourceText.indexOf(text);
|
|
133
|
+
const trimOffset = trimStart >= 0 ? trimStart : 0;
|
|
134
|
+
const segments = segmentText(text);
|
|
135
|
+
const nodes = collectTextNodes(el);
|
|
136
|
+
const range = document.createRange();
|
|
137
|
+
const boxes = [];
|
|
138
|
+
|
|
139
|
+
for (const [index, segment] of segments.entries()) {
|
|
140
|
+
const start = locateOffset(nodes, trimOffset + segment.start);
|
|
141
|
+
const end = locateOffset(nodes, trimOffset + segment.end);
|
|
142
|
+
if (!start || !end) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
range.setStart(start.node, start.offset);
|
|
147
|
+
range.setEnd(end.node, end.offset);
|
|
148
|
+
} catch (_error) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const rects = Array.from(range.getClientRects());
|
|
152
|
+
if (rects.length === 0) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const left = Math.min(...rects.map((rect) => rect.left));
|
|
156
|
+
const top = Math.min(...rects.map((rect) => rect.top));
|
|
157
|
+
const right = Math.max(...rects.map((rect) => rect.right));
|
|
158
|
+
const bottom = Math.max(...rects.map((rect) => rect.bottom));
|
|
159
|
+
boxes.push({
|
|
160
|
+
index,
|
|
161
|
+
grapheme: segment.grapheme,
|
|
162
|
+
box: relRect({ left, top, width: right - left, height: bottom - top }, rootRect),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return boxes;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function glyphBoxesForElements(elements, rootRect) {
|
|
170
|
+
const boxes = [];
|
|
171
|
+
let indexOffset = 0;
|
|
172
|
+
for (const el of elements) {
|
|
173
|
+
const elementBoxes = glyphBoxesForElement(el, rootRect);
|
|
174
|
+
for (const box of elementBoxes) {
|
|
175
|
+
boxes.push({
|
|
176
|
+
...box,
|
|
177
|
+
index: box.index + indexOffset,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
indexOffset += elementBoxes.length;
|
|
181
|
+
}
|
|
182
|
+
return boxes;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function measureTextEntry(el, rootRect, fontEl = el) {
|
|
186
|
+
const letters = glyphBoxesForElement(el, rootRect);
|
|
187
|
+
return {
|
|
188
|
+
box: letters.length > 0 ? unionRect(letters.map((letter) => letter.box)) : relRect(el.getBoundingClientRect(), rootRect),
|
|
189
|
+
font: fontInfo(fontEl),
|
|
190
|
+
letters,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function measureTextEntryFromElements(elements, rootRect, fallbackEl, fontEl = fallbackEl) {
|
|
195
|
+
const letters = glyphBoxesForElements(elements, rootRect);
|
|
196
|
+
return {
|
|
197
|
+
box: letters.length > 0 ? unionRect(letters.map((letter) => letter.box)) : relRect(fallbackEl.getBoundingClientRect(), rootRect),
|
|
198
|
+
font: fontInfo(fontEl),
|
|
199
|
+
letters,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function fontInfo(el) {
|
|
204
|
+
const style = getComputedStyle(el);
|
|
205
|
+
return {
|
|
206
|
+
fontFamily: style.fontFamily,
|
|
207
|
+
fontSize: style.fontSize,
|
|
208
|
+
fontWeight: style.fontWeight,
|
|
209
|
+
lineHeight: style.lineHeight,
|
|
210
|
+
textAlign: style.textAlign,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function textOrEmpty(el, selector) {
|
|
215
|
+
return (el?.querySelector(selector)?.textContent ?? "").trim();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function fragmentOwnerText(fragmentEl) {
|
|
219
|
+
return (
|
|
220
|
+
textOrEmpty(fragmentEl, ":scope > .header .name") ||
|
|
221
|
+
textOrEmpty(fragmentEl, ":scope > .header .text-skin-fragment span") ||
|
|
222
|
+
textOrEmpty(fragmentEl, ":scope > .header .text-skin-fragment")
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function visibleChildren(el) {
|
|
227
|
+
return Array.from(el.children).filter((child) => getComputedStyle(child).display !== "none");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function textContentNormalized(el) {
|
|
231
|
+
return (el?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeColorValue(value) {
|
|
235
|
+
const normalized = (value ?? "").replace(/\s+/g, "").trim().toLowerCase();
|
|
236
|
+
return normalized || null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function boxOrNull(box) {
|
|
240
|
+
if (!box || box.w <= 0 || box.h <= 0) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
return box;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function paintedBox(el, rootRect) {
|
|
247
|
+
if (!el) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const shapeSelectors = "path, rect, circle, ellipse, polygon, polyline, line, use";
|
|
251
|
+
const rects = [];
|
|
252
|
+
for (const shape of el.querySelectorAll(shapeSelectors)) {
|
|
253
|
+
const rect = relRect(shape.getBoundingClientRect(), rootRect);
|
|
254
|
+
if (rect.w > 0 && rect.h > 0) {
|
|
255
|
+
rects.push(rect);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (rects.length > 0) {
|
|
259
|
+
return unionRect(rects);
|
|
260
|
+
}
|
|
261
|
+
return boxOrNull(relRect(el.getBoundingClientRect(), rootRect));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function topParticipantsByName(entries) {
|
|
265
|
+
const ordered = [...entries].sort((a, b) => (a.participantBox.y - b.participantBox.y) || (a.participantBox.x - b.participantBox.x));
|
|
266
|
+
const byName = new Map();
|
|
267
|
+
for (const entry of ordered) {
|
|
268
|
+
if (!entry.name || byName.has(entry.name)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
byName.set(entry.name, entry);
|
|
272
|
+
}
|
|
273
|
+
return Array.from(byName.values());
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function collectHtmlLabels(root, rootRect) {
|
|
277
|
+
const labels = [];
|
|
278
|
+
const selectorPairs = [
|
|
279
|
+
{
|
|
280
|
+
kind: "message",
|
|
281
|
+
selector:
|
|
282
|
+
".interaction:not(.return):not(.creation):not(.self-invocation):not(.self) > .message .editable-span-base",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
kind: "self",
|
|
286
|
+
selector:
|
|
287
|
+
".interaction.self-invocation > .message .editable-span-base, .interaction.self > .self-invocation .editable-span-base",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
kind: "return",
|
|
291
|
+
selector:
|
|
292
|
+
".interaction.return > .message .editable-span-base, .interaction.return > .flex.items-center > .name",
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
kind: "creation",
|
|
296
|
+
selector:
|
|
297
|
+
".interaction.creation .message .name",
|
|
298
|
+
},
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
for (const pair of selectorPairs) {
|
|
302
|
+
for (const labelEl of root.querySelectorAll(pair.selector)) {
|
|
303
|
+
const text = (labelEl.textContent ?? "").trim();
|
|
304
|
+
if (!text) continue;
|
|
305
|
+
const measured = measureTextEntry(labelEl, rootRect);
|
|
306
|
+
labels.push({
|
|
307
|
+
side: "html",
|
|
308
|
+
kind: pair.kind,
|
|
309
|
+
text,
|
|
310
|
+
box: measured.box,
|
|
311
|
+
font: measured.font,
|
|
312
|
+
letters: measured.letters,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for (const conditionWrap of root.querySelectorAll(".fragment .segment > .text-skin-fragment:not(.finally)")) {
|
|
318
|
+
const children = visibleChildren(conditionWrap);
|
|
319
|
+
if (children.length === 0) continue;
|
|
320
|
+
const text = children.map((child) => (child.textContent ?? "").trim()).join("").trim();
|
|
321
|
+
if (!text) continue;
|
|
322
|
+
const measured = measureTextEntryFromElements(children, rootRect, conditionWrap, children[0]);
|
|
323
|
+
labels.push({
|
|
324
|
+
side: "html",
|
|
325
|
+
kind: "fragment-condition",
|
|
326
|
+
text,
|
|
327
|
+
ownerText: fragmentOwnerText(conditionWrap.closest(".fragment")) || null,
|
|
328
|
+
box: measured.box,
|
|
329
|
+
font: measured.font,
|
|
330
|
+
letters: measured.letters,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const sectionSelectors = [
|
|
335
|
+
".fragment.fragment-tcf .segment > .header.inline-block.bg-skin-frame.opacity-65",
|
|
336
|
+
".fragment.fragment-tcf .segment > .header.finally",
|
|
337
|
+
];
|
|
338
|
+
for (const selector of sectionSelectors) {
|
|
339
|
+
for (const sectionEl of root.querySelectorAll(selector)) {
|
|
340
|
+
const children = visibleChildren(sectionEl);
|
|
341
|
+
if (children.length === 0) continue;
|
|
342
|
+
const text = children.map((child) => (child.textContent ?? "").trim()).join("").trim();
|
|
343
|
+
if (!text) continue;
|
|
344
|
+
const measured = measureTextEntryFromElements(children, rootRect, sectionEl, children[0]);
|
|
345
|
+
labels.push({
|
|
346
|
+
side: "html",
|
|
347
|
+
kind: "fragment-section",
|
|
348
|
+
text,
|
|
349
|
+
ownerText: fragmentOwnerText(sectionEl.closest(".fragment")) || null,
|
|
350
|
+
box: measured.box,
|
|
351
|
+
font: measured.font,
|
|
352
|
+
letters: measured.letters,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return labels;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function collectSvgLabels(root, rootRect) {
|
|
360
|
+
const labels = [];
|
|
361
|
+
const pairs = [
|
|
362
|
+
{ selector: "g.message:not(.self-call) > text.message-label", kind: "message" },
|
|
363
|
+
{ selector: "g.message.self-call > text.message-label", kind: "self" },
|
|
364
|
+
{ selector: "g.return > text.return-label", kind: "return" },
|
|
365
|
+
{ selector: "g.creation > text.message-label", kind: "creation" },
|
|
366
|
+
];
|
|
367
|
+
for (const pair of pairs) {
|
|
368
|
+
for (const labelEl of root.querySelectorAll(pair.selector)) {
|
|
369
|
+
const text = (labelEl.textContent ?? "").trim();
|
|
370
|
+
if (!text) continue;
|
|
371
|
+
const measured = measureTextEntry(labelEl, rootRect);
|
|
372
|
+
labels.push({
|
|
373
|
+
side: "svg",
|
|
374
|
+
kind: pair.kind,
|
|
375
|
+
text,
|
|
376
|
+
box: measured.box,
|
|
377
|
+
font: measured.font,
|
|
378
|
+
letters: measured.letters,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const labelEl of root.querySelectorAll("g.fragment > text.fragment-condition")) {
|
|
384
|
+
const text = (labelEl.textContent ?? "").trim();
|
|
385
|
+
if (!text) continue;
|
|
386
|
+
const measured = measureTextEntry(labelEl, rootRect);
|
|
387
|
+
labels.push({
|
|
388
|
+
side: "svg",
|
|
389
|
+
kind: "fragment-condition",
|
|
390
|
+
text,
|
|
391
|
+
ownerText: textOrEmpty(labelEl.closest("g.fragment"), ":scope > text.fragment-label") || null,
|
|
392
|
+
box: measured.box,
|
|
393
|
+
font: measured.font,
|
|
394
|
+
letters: measured.letters,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const fragmentEl of root.querySelectorAll("g.fragment")) {
|
|
399
|
+
for (const groupEl of fragmentEl.querySelectorAll(":scope > g")) {
|
|
400
|
+
const conditionTextEls = Array.from(groupEl.querySelectorAll(":scope > text.fragment-condition"));
|
|
401
|
+
if (conditionTextEls.length > 0) {
|
|
402
|
+
const text = conditionTextEls.map((el) => (el.textContent ?? "").trim()).join("").replace(/\s+\]/g, "]").trim();
|
|
403
|
+
if (!text) continue;
|
|
404
|
+
const measured = measureTextEntryFromElements(conditionTextEls, rootRect, groupEl, conditionTextEls[0]);
|
|
405
|
+
labels.push({
|
|
406
|
+
side: "svg",
|
|
407
|
+
kind: "fragment-condition",
|
|
408
|
+
text,
|
|
409
|
+
ownerText: textOrEmpty(fragmentEl, ":scope > text.fragment-label") || null,
|
|
410
|
+
box: measured.box,
|
|
411
|
+
font: measured.font,
|
|
412
|
+
letters: measured.letters,
|
|
413
|
+
});
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const textEls = Array.from(groupEl.querySelectorAll("text.fragment-section-label"));
|
|
418
|
+
if (textEls.length === 0) continue;
|
|
419
|
+
const text = textEls.map((el) => (el.textContent ?? "").trim()).join("").replace(/\s+\]/g, "]").trim();
|
|
420
|
+
if (!text) continue;
|
|
421
|
+
const measured = measureTextEntryFromElements(textEls, rootRect, groupEl, textEls[0]);
|
|
422
|
+
labels.push({
|
|
423
|
+
side: "svg",
|
|
424
|
+
kind: text.startsWith("[") ? "fragment-condition" : "fragment-section",
|
|
425
|
+
text,
|
|
426
|
+
ownerText: textOrEmpty(fragmentEl, ":scope > text.fragment-label") || null,
|
|
427
|
+
box: measured.box,
|
|
428
|
+
font: measured.font,
|
|
429
|
+
letters: measured.letters,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return labels;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function collectHtmlParticipants(root, rootRect) {
|
|
437
|
+
const participants = [];
|
|
438
|
+
for (const participantEl of root.querySelectorAll(".participant[data-participant-id]")) {
|
|
439
|
+
const name = (participantEl.getAttribute("data-participant-id") ?? "").trim();
|
|
440
|
+
if (!name) continue;
|
|
441
|
+
const participantBox = boxOrNull(relRect(participantEl.getBoundingClientRect(), rootRect));
|
|
442
|
+
if (!participantBox) continue;
|
|
443
|
+
|
|
444
|
+
const rowEl = participantEl.querySelector(":scope > .flex.items-center.justify-center, :scope > div:last-child");
|
|
445
|
+
const firstChild = rowEl?.firstElementChild ?? null;
|
|
446
|
+
const iconHost = firstChild && (
|
|
447
|
+
firstChild.matches("[aria-description]") ||
|
|
448
|
+
firstChild.querySelector("svg") ||
|
|
449
|
+
/\bh-6\b/.test(firstChild.className || "")
|
|
450
|
+
)
|
|
451
|
+
? firstChild
|
|
452
|
+
: null;
|
|
453
|
+
const labelEl = Array.from(participantEl.querySelectorAll(".name")).at(-1) ?? null;
|
|
454
|
+
const measuredLabel = labelEl ? measureTextEntry(labelEl, rootRect) : null;
|
|
455
|
+
const stereotypeEl = participantEl.querySelector("label.interface");
|
|
456
|
+
const measuredStereotype = stereotypeEl ? measureTextEntry(stereotypeEl, rootRect) : null;
|
|
457
|
+
const iconPaintRoot = iconHost?.querySelector("svg") ?? iconHost;
|
|
458
|
+
const participantStyle = getComputedStyle(participantEl);
|
|
459
|
+
|
|
460
|
+
participants.push({
|
|
461
|
+
side: "html",
|
|
462
|
+
name,
|
|
463
|
+
labelText: textContentNormalized(labelEl),
|
|
464
|
+
participantBox,
|
|
465
|
+
labelBox: measuredLabel?.box ?? null,
|
|
466
|
+
labelFont: measuredLabel?.font ?? null,
|
|
467
|
+
labelLetters: measuredLabel?.letters ?? [],
|
|
468
|
+
stereotypeText: textContentNormalized(stereotypeEl),
|
|
469
|
+
stereotypeBox: measuredStereotype?.box ?? null,
|
|
470
|
+
stereotypeFont: measuredStereotype?.font ?? null,
|
|
471
|
+
stereotypeLetters: measuredStereotype?.letters ?? [],
|
|
472
|
+
iconBox: paintedBox(iconPaintRoot, rootRect),
|
|
473
|
+
anchorKind: measuredLabel?.box ? "label" : "participant-box",
|
|
474
|
+
anchorBox: measuredLabel?.box ?? participantBox,
|
|
475
|
+
backgroundColor: normalizeColorValue(participantStyle.backgroundColor),
|
|
476
|
+
textColor: normalizeColorValue(labelEl ? getComputedStyle(labelEl).color : participantStyle.color),
|
|
477
|
+
stereotypeColor: normalizeColorValue(stereotypeEl ? getComputedStyle(stereotypeEl).color : null),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return topParticipantsByName(participants);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function collectSvgParticipants(root, rootRect) {
|
|
484
|
+
const participants = [];
|
|
485
|
+
for (const participantEl of root.querySelectorAll("g.participant[data-participant]")) {
|
|
486
|
+
if (participantEl.classList.contains("participant-bottom")) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const name = (participantEl.getAttribute("data-participant") ?? "").trim();
|
|
490
|
+
if (!name) continue;
|
|
491
|
+
const participantBoxEl = participantEl.querySelector(":scope > rect.participant-box");
|
|
492
|
+
const participantBox = boxOrNull(strokedElementOuterRect(participantBoxEl || participantEl, rootRect));
|
|
493
|
+
if (!participantBox) continue;
|
|
494
|
+
|
|
495
|
+
const labelEl = participantEl.querySelector(":scope > text.participant-label");
|
|
496
|
+
const measuredLabel = labelEl ? measureTextEntry(labelEl, rootRect) : null;
|
|
497
|
+
const stereotypeEl = participantEl.querySelector(":scope > text.stereotype-label")
|
|
498
|
+
|| Array.from(participantEl.querySelectorAll(":scope > text"))
|
|
499
|
+
.filter((textEl) => textEl !== labelEl)
|
|
500
|
+
.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)[0]
|
|
501
|
+
|| null;
|
|
502
|
+
const measuredStereotype = stereotypeEl ? measureTextEntry(stereotypeEl, rootRect) : null;
|
|
503
|
+
const iconEl = participantEl.querySelector(":scope > g[transform]");
|
|
504
|
+
const participantBoxStyle = participantBoxEl ? getComputedStyle(participantBoxEl) : null;
|
|
505
|
+
|
|
506
|
+
participants.push({
|
|
507
|
+
side: "svg",
|
|
508
|
+
name,
|
|
509
|
+
labelText: textContentNormalized(labelEl),
|
|
510
|
+
participantBox,
|
|
511
|
+
labelBox: measuredLabel?.box ?? null,
|
|
512
|
+
labelFont: measuredLabel?.font ?? null,
|
|
513
|
+
labelLetters: measuredLabel?.letters ?? [],
|
|
514
|
+
stereotypeText: textContentNormalized(stereotypeEl),
|
|
515
|
+
stereotypeBox: measuredStereotype?.box ?? null,
|
|
516
|
+
stereotypeFont: measuredStereotype?.font ?? null,
|
|
517
|
+
stereotypeLetters: measuredStereotype?.letters ?? [],
|
|
518
|
+
iconBox: paintedBox(iconEl, rootRect),
|
|
519
|
+
anchorKind: measuredLabel?.box ? "label" : "participant-box",
|
|
520
|
+
anchorBox: measuredLabel?.box ?? participantBox,
|
|
521
|
+
backgroundColor: normalizeColorValue(participantBoxStyle?.fill || participantBoxEl?.getAttribute("fill")),
|
|
522
|
+
textColor: normalizeColorValue(labelEl ? getComputedStyle(labelEl).fill : null),
|
|
523
|
+
stereotypeColor: normalizeColorValue(stereotypeEl ? getComputedStyle(stereotypeEl).fill : null),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return topParticipantsByName(participants);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function collectHtmlComments(root, rootRect) {
|
|
530
|
+
const comments = [];
|
|
531
|
+
for (const commentEl of root.querySelectorAll(".comments")) {
|
|
532
|
+
const text = textContentNormalized(commentEl);
|
|
533
|
+
if (!text) continue;
|
|
534
|
+
const measured = measureTextEntry(commentEl, rootRect, commentEl);
|
|
535
|
+
comments.push({
|
|
536
|
+
side: "html",
|
|
537
|
+
kind: "comment",
|
|
538
|
+
text,
|
|
539
|
+
box: measured.box,
|
|
540
|
+
font: measured.font,
|
|
541
|
+
letters: measured.letters,
|
|
542
|
+
color: normalizeColorValue(getComputedStyle(commentEl).color),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
return comments;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function collectSvgComments(root, rootRect) {
|
|
549
|
+
const comments = [];
|
|
550
|
+
for (const commentEl of root.querySelectorAll("text.comment-text")) {
|
|
551
|
+
const text = textContentNormalized(commentEl);
|
|
552
|
+
if (!text) continue;
|
|
553
|
+
const measured = measureTextEntry(commentEl, rootRect);
|
|
554
|
+
comments.push({
|
|
555
|
+
side: "svg",
|
|
556
|
+
kind: "comment",
|
|
557
|
+
text,
|
|
558
|
+
box: measured.box,
|
|
559
|
+
font: measured.font,
|
|
560
|
+
letters: measured.letters,
|
|
561
|
+
color: normalizeColorValue(getComputedStyle(commentEl).fill),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return comments;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function collectHtmlGroups(root, rootRect) {
|
|
568
|
+
const groups = [];
|
|
569
|
+
for (const groupEl of root.querySelectorAll(".lifeline-group-container")) {
|
|
570
|
+
const nameEl = groupEl.querySelector(".text-skin-lifeline-group-name");
|
|
571
|
+
const name = textContentNormalized(nameEl);
|
|
572
|
+
const box = boxOrNull(relRect(groupEl.getBoundingClientRect(), rootRect));
|
|
573
|
+
if (!box) continue;
|
|
574
|
+
const measuredName = nameEl ? measureTextEntry(nameEl, rootRect) : null;
|
|
575
|
+
groups.push({
|
|
576
|
+
side: "html",
|
|
577
|
+
name,
|
|
578
|
+
box,
|
|
579
|
+
nameBox: measuredName?.box ?? null,
|
|
580
|
+
nameFont: measuredName?.font ?? null,
|
|
581
|
+
nameLetters: measuredName?.letters ?? [],
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
return groups;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function collectSvgGroups(root, rootRect) {
|
|
588
|
+
const groups = [];
|
|
589
|
+
for (const groupEl of root.querySelectorAll("g.participant-group")) {
|
|
590
|
+
const nameEl = groupEl.querySelector(":scope > text");
|
|
591
|
+
const name = textContentNormalized(nameEl);
|
|
592
|
+
const box = boxOrNull(relRect(groupEl.getBoundingClientRect(), rootRect));
|
|
593
|
+
if (!box) continue;
|
|
594
|
+
const measuredName = nameEl ? measureTextEntry(nameEl, rootRect) : null;
|
|
595
|
+
groups.push({
|
|
596
|
+
side: "svg",
|
|
597
|
+
name,
|
|
598
|
+
box,
|
|
599
|
+
nameBox: measuredName?.box ?? null,
|
|
600
|
+
nameFont: measuredName?.font ?? null,
|
|
601
|
+
nameLetters: measuredName?.letters ?? [],
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
return groups;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function collectHtmlArrows(root, rootRect) {
|
|
608
|
+
const arrows = [];
|
|
609
|
+
|
|
610
|
+
function addArrow(kind, interaction, text, parts) {
|
|
611
|
+
if (!text || parts.length === 0) return;
|
|
612
|
+
const box = unionRect(parts.map((part) => part.box));
|
|
613
|
+
const labelText = (interaction.getAttribute("data-signature") || "").trim();
|
|
614
|
+
arrows.push({
|
|
615
|
+
side: "html",
|
|
616
|
+
kind,
|
|
617
|
+
text,
|
|
618
|
+
pairText: text,
|
|
619
|
+
box,
|
|
620
|
+
...arrowEndpointsFromBox(box),
|
|
621
|
+
labelText,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
for (const interaction of root.querySelectorAll(".interaction:not(.return):not(.creation):not(.self-invocation):not(.self)")) {
|
|
626
|
+
const text = (interaction.querySelector(":scope > .message > .absolute.text-xs")?.textContent || "").trim()
|
|
627
|
+
|| (interaction.getAttribute("data-signature") || "").trim();
|
|
628
|
+
const messageEl = interaction.querySelector(":scope > .message");
|
|
629
|
+
if (!messageEl) continue;
|
|
630
|
+
const svgChildren = Array.from(messageEl.children).filter((child) => child.tagName?.toLowerCase() === "svg");
|
|
631
|
+
const parts = [];
|
|
632
|
+
if (svgChildren[0]) {
|
|
633
|
+
parts.push({ part: "line", box: relRect(svgChildren[0].getBoundingClientRect(), rootRect) });
|
|
634
|
+
}
|
|
635
|
+
if (svgChildren[1]) {
|
|
636
|
+
parts.push({ part: "head", box: relRect(svgChildren[1].getBoundingClientRect(), rootRect) });
|
|
637
|
+
}
|
|
638
|
+
addArrow("message", interaction, text, parts);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
for (const interaction of root.querySelectorAll(".interaction.return")) {
|
|
642
|
+
const text = (interaction.querySelector(":scope > .message > .absolute.text-xs")?.textContent || "").trim()
|
|
643
|
+
|| (interaction.getAttribute("data-signature") || "").trim();
|
|
644
|
+
const messageEl = interaction.querySelector(":scope > .message");
|
|
645
|
+
if (!messageEl) continue;
|
|
646
|
+
const svgChildren = Array.from(messageEl.children).filter((child) => child.tagName?.toLowerCase() === "svg");
|
|
647
|
+
const parts = [];
|
|
648
|
+
if (svgChildren[0]) {
|
|
649
|
+
parts.push({ part: "line", box: relRect(svgChildren[0].getBoundingClientRect(), rootRect) });
|
|
650
|
+
}
|
|
651
|
+
if (svgChildren[1]) {
|
|
652
|
+
parts.push({ part: "head", box: relRect(svgChildren[1].getBoundingClientRect(), rootRect) });
|
|
653
|
+
}
|
|
654
|
+
addArrow("return", interaction, text, parts);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
for (const interaction of root.querySelectorAll(".interaction.self, .interaction.self-invocation")) {
|
|
658
|
+
const text = (interaction.querySelector(":scope > .message .absolute.text-xs, :scope > .self-invocation .absolute.text-xs")?.textContent || "").trim()
|
|
659
|
+
|| (interaction.getAttribute("data-signature") || "").trim();
|
|
660
|
+
const arrowSvg = interaction.querySelector(":scope > .message > svg.arrow, :scope > .self-invocation > svg.arrow");
|
|
661
|
+
if (!arrowSvg) continue;
|
|
662
|
+
const parts = [];
|
|
663
|
+
pushBox(parts, "loop", elementRect(arrowSvg.querySelector(":scope > path, :scope > polyline"), rootRect));
|
|
664
|
+
pushBox(parts, "head", elementRect(arrowSvg.querySelector(":scope > g path, :scope > g polyline"), rootRect));
|
|
665
|
+
if (parts.length === 0) {
|
|
666
|
+
pushBox(parts, "loop", elementRect(arrowSvg, rootRect));
|
|
667
|
+
}
|
|
668
|
+
addArrow("self", interaction, text, parts);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return arrows;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function collectSvgArrows(root, rootRect) {
|
|
675
|
+
const arrows = [];
|
|
676
|
+
|
|
677
|
+
function addArrow(kind, group, text, parts) {
|
|
678
|
+
if (!text || parts.length === 0) return;
|
|
679
|
+
const box = unionRect(parts.map((part) => part.box));
|
|
680
|
+
const labelText = (group.querySelector("text.message-label, text.return-label")?.textContent || "").trim();
|
|
681
|
+
arrows.push({
|
|
682
|
+
side: "svg",
|
|
683
|
+
kind,
|
|
684
|
+
text,
|
|
685
|
+
pairText: text,
|
|
686
|
+
box,
|
|
687
|
+
...arrowEndpointsFromBox(box),
|
|
688
|
+
labelText,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
for (const group of root.querySelectorAll("g.message:not(.self-call)")) {
|
|
693
|
+
const text = (group.querySelector("text.seq-number")?.textContent || "").trim()
|
|
694
|
+
|| (group.querySelector("text.message-label")?.textContent || "").trim();
|
|
695
|
+
const parts = [];
|
|
696
|
+
const lineEl = group.querySelector(":scope > line.message-line");
|
|
697
|
+
const headEl = group.querySelector(":scope > svg.arrow-head");
|
|
698
|
+
if (lineEl) parts.push({ part: "line", box: relRect(lineEl.getBoundingClientRect(), rootRect) });
|
|
699
|
+
if (headEl) parts.push({ part: "head", box: relRect(headEl.getBoundingClientRect(), rootRect) });
|
|
700
|
+
addArrow("message", group, text, parts);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
for (const group of root.querySelectorAll("g.return")) {
|
|
704
|
+
const text = (group.querySelector("text.seq-number")?.textContent || "").trim()
|
|
705
|
+
|| (group.querySelector("text.return-label")?.textContent || "").trim();
|
|
706
|
+
const parts = [];
|
|
707
|
+
const lineEl = group.querySelector(":scope > line.return-line");
|
|
708
|
+
const headEl = group.querySelector(":scope > polyline.return-arrow");
|
|
709
|
+
if (lineEl) parts.push({ part: "line", box: relRect(lineEl.getBoundingClientRect(), rootRect) });
|
|
710
|
+
if (headEl) parts.push({ part: "head", box: relRect(headEl.getBoundingClientRect(), rootRect) });
|
|
711
|
+
addArrow("return", group, text, parts);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
for (const group of root.querySelectorAll("g.message.self-call")) {
|
|
715
|
+
const text = (group.querySelector("text.seq-number")?.textContent || "").trim()
|
|
716
|
+
|| (group.querySelector("text.message-label")?.textContent || "").trim();
|
|
717
|
+
const loopEl = group.querySelector(":scope > svg");
|
|
718
|
+
if (!loopEl) continue;
|
|
719
|
+
const parts = [];
|
|
720
|
+
pushBox(parts, "loop", elementRect(loopEl.querySelector(":scope > path, :scope > polyline"), rootRect));
|
|
721
|
+
pushBox(parts, "head", elementRect(loopEl.querySelector(":scope > g path, :scope > g polyline"), rootRect));
|
|
722
|
+
if (parts.length === 0) {
|
|
723
|
+
const loopRect = loopEl.getBoundingClientRect();
|
|
724
|
+
const attrW = parseFloat(loopEl.getAttribute("width"));
|
|
725
|
+
const attrH = parseFloat(loopEl.getAttribute("height"));
|
|
726
|
+
const box = relRect(loopRect, rootRect);
|
|
727
|
+
if (attrW && attrH) {
|
|
728
|
+
box.w = attrW;
|
|
729
|
+
box.h = attrH;
|
|
730
|
+
}
|
|
731
|
+
pushBox(parts, "loop", box);
|
|
732
|
+
}
|
|
733
|
+
addArrow("self", group, text, parts);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return arrows;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function collectHtmlNumbers(root, rootRect) {
|
|
740
|
+
const numbers = [];
|
|
741
|
+
const selectorPairs = [
|
|
742
|
+
{
|
|
743
|
+
kind: "message",
|
|
744
|
+
selector:
|
|
745
|
+
".interaction:not(.return):not(.creation):not(.self-invocation):not(.self) > .message > .absolute.text-xs",
|
|
746
|
+
ownerText: (numberEl) => textOrEmpty(numberEl.closest(".interaction"), ":scope > .message .editable-span-base"),
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
kind: "self",
|
|
750
|
+
selector:
|
|
751
|
+
".interaction.self-invocation > .message .absolute.text-xs, .interaction.self > .self-invocation .absolute.text-xs",
|
|
752
|
+
ownerText: (numberEl) =>
|
|
753
|
+
textOrEmpty(numberEl.closest(".interaction"), ":scope > .message .editable-span-base, :scope > .self-invocation .editable-span-base"),
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
kind: "return",
|
|
757
|
+
selector:
|
|
758
|
+
".interaction.return > .message > .absolute.text-xs",
|
|
759
|
+
ownerText: (numberEl) =>
|
|
760
|
+
textOrEmpty(numberEl.closest(".interaction"), ":scope > .message .editable-span-base, :scope > .message .name"),
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
kind: "fragment",
|
|
764
|
+
selector:
|
|
765
|
+
".fragment > .header > .absolute.text-xs",
|
|
766
|
+
ownerText: (numberEl) => fragmentOwnerText(numberEl.closest(".fragment")),
|
|
767
|
+
},
|
|
768
|
+
];
|
|
769
|
+
|
|
770
|
+
for (const pair of selectorPairs) {
|
|
771
|
+
for (const numberEl of root.querySelectorAll(pair.selector)) {
|
|
772
|
+
const text = (numberEl.textContent ?? "").trim();
|
|
773
|
+
if (!text) continue;
|
|
774
|
+
numbers.push({
|
|
775
|
+
side: "html",
|
|
776
|
+
kind: pair.kind,
|
|
777
|
+
text,
|
|
778
|
+
pairText: pair.ownerText ? pair.ownerText(numberEl) || text : text,
|
|
779
|
+
ownerText: pair.ownerText ? pair.ownerText(numberEl) || null : null,
|
|
780
|
+
box: relRect(numberEl.getBoundingClientRect(), rootRect),
|
|
781
|
+
font: fontInfo(numberEl),
|
|
782
|
+
letters: glyphBoxesForElement(numberEl, rootRect),
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return numbers;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function collectSvgNumbers(root, rootRect) {
|
|
790
|
+
const numbers = [];
|
|
791
|
+
const pairs = [
|
|
792
|
+
{
|
|
793
|
+
selector: "g.message:not(.self-call) > text.seq-number",
|
|
794
|
+
kind: "message",
|
|
795
|
+
ownerText: (numberEl) => textOrEmpty(numberEl.closest("g.message"), ":scope > text.message-label"),
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
selector: "g.message.self-call > text.seq-number",
|
|
799
|
+
kind: "self",
|
|
800
|
+
ownerText: (numberEl) => textOrEmpty(numberEl.closest("g.message"), ":scope > text.message-label"),
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
selector: "g.return > text.seq-number",
|
|
804
|
+
kind: "return",
|
|
805
|
+
ownerText: (numberEl) => textOrEmpty(numberEl.closest("g.return"), ":scope > text.return-label"),
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
selector: "g.fragment > text.seq-number",
|
|
809
|
+
kind: "fragment",
|
|
810
|
+
ownerText: (numberEl) => textOrEmpty(numberEl.closest("g.fragment"), ":scope > text.fragment-label"),
|
|
811
|
+
},
|
|
812
|
+
];
|
|
813
|
+
for (const pair of pairs) {
|
|
814
|
+
for (const numberEl of root.querySelectorAll(pair.selector)) {
|
|
815
|
+
const text = (numberEl.textContent ?? "").trim();
|
|
816
|
+
if (!text) continue;
|
|
817
|
+
// SVG <text> getBoundingClientRect returns glyph bounds (height ~14px for 12px font).
|
|
818
|
+
// HTML <div> with line-height:16px adds 2px top padding ((16-12)/2).
|
|
819
|
+
// Adjust SVG box Y by -1 to align with HTML's line-height-padded top edge.
|
|
820
|
+
const rawBox = relRect(numberEl.getBoundingClientRect(), rootRect);
|
|
821
|
+
const adjustedBox = { ...rawBox, y: rawBox.y - 1, h: rawBox.h + 2 };
|
|
822
|
+
numbers.push({
|
|
823
|
+
side: "svg",
|
|
824
|
+
kind: pair.kind,
|
|
825
|
+
text,
|
|
826
|
+
pairText: pair.ownerText ? pair.ownerText(numberEl) || text : text,
|
|
827
|
+
ownerText: pair.ownerText ? pair.ownerText(numberEl) || null : null,
|
|
828
|
+
box: adjustedBox,
|
|
829
|
+
font: fontInfo(numberEl),
|
|
830
|
+
letters: glyphBoxesForElement(numberEl, rootRect),
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return numbers;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function collectHtmlOccurrences(root, rootRect) {
|
|
838
|
+
const occurrences = [];
|
|
839
|
+
for (const el of root.querySelectorAll('[data-el-type="occurrence"]')) {
|
|
840
|
+
const participant = (el.getAttribute("data-belongs-to") ?? "").trim();
|
|
841
|
+
const box = boxOrNull(relRect(el.getBoundingClientRect(), rootRect));
|
|
842
|
+
if (!box) continue;
|
|
843
|
+
occurrences.push({
|
|
844
|
+
side: "html",
|
|
845
|
+
participant,
|
|
846
|
+
idx: occurrences.length,
|
|
847
|
+
box,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
return occurrences;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function collectSvgOccurrences(root, rootRect) {
|
|
854
|
+
const occurrences = [];
|
|
855
|
+
for (const el of root.querySelectorAll("rect.occurrence")) {
|
|
856
|
+
const participant = (el.getAttribute("data-participant") ?? "").trim();
|
|
857
|
+
const box = boxOrNull(strokedElementOuterRect(el, rootRect));
|
|
858
|
+
if (!box) continue;
|
|
859
|
+
occurrences.push({
|
|
860
|
+
side: "svg",
|
|
861
|
+
participant,
|
|
862
|
+
idx: occurrences.length,
|
|
863
|
+
box,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
return occurrences;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function collectHtmlFragmentDividers(root, rootRect) {
|
|
870
|
+
const dividers = [];
|
|
871
|
+
// Alt/tcf dividers: .segment.border-t
|
|
872
|
+
for (const seg of root.querySelectorAll(".segment.border-t")) {
|
|
873
|
+
const r = seg.getBoundingClientRect();
|
|
874
|
+
const y = r.top - rootRect.top;
|
|
875
|
+
const x = r.left - rootRect.left;
|
|
876
|
+
const w = r.width;
|
|
877
|
+
const label = (seg.querySelector(".text-skin-fragment")?.textContent ?? "").trim();
|
|
878
|
+
dividers.push({ side: "html", idx: dividers.length, y, x, width: w, label, source: "segment" });
|
|
879
|
+
}
|
|
880
|
+
// Par dividers: .statement-container with computed border-top inside .par
|
|
881
|
+
for (const sc of root.querySelectorAll(".par .statement-container")) {
|
|
882
|
+
const style = getComputedStyle(sc);
|
|
883
|
+
if (parseFloat(style.borderTopWidth) < 1) continue;
|
|
884
|
+
const r = sc.getBoundingClientRect();
|
|
885
|
+
const y = r.top - rootRect.top;
|
|
886
|
+
const x = r.left - rootRect.left;
|
|
887
|
+
const w = r.width;
|
|
888
|
+
dividers.push({ side: "html", idx: dividers.length, y, x, width: w, label: "", source: "par" });
|
|
889
|
+
}
|
|
890
|
+
// Sort by Y position for consistent pairing
|
|
891
|
+
dividers.sort((a, b) => a.y - b.y);
|
|
892
|
+
dividers.forEach((d, i) => { d.idx = i; });
|
|
893
|
+
return dividers;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function collectSvgFragmentDividers(root, rootRect) {
|
|
897
|
+
const dividers = [];
|
|
898
|
+
for (const line of root.querySelectorAll("line.fragment-separator")) {
|
|
899
|
+
const lineRect = line.getBoundingClientRect();
|
|
900
|
+
const strokeWidth = parseFloat(getComputedStyle(line).strokeWidth || "0") || 0;
|
|
901
|
+
const half = strokeWidth / 2;
|
|
902
|
+
// The painted top edge of the stroke = center - half.
|
|
903
|
+
// This matches HTML border-top measurement (top edge of the 1px border).
|
|
904
|
+
const y = (lineRect.top - rootRect.top) - half;
|
|
905
|
+
const x = lineRect.left - rootRect.left;
|
|
906
|
+
const w = lineRect.width;
|
|
907
|
+
dividers.push({ side: "svg", idx: dividers.length, y, x, width: w, label: "" });
|
|
908
|
+
}
|
|
909
|
+
return dividers;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function collectHtmlDividers(root, rootRect) {
|
|
913
|
+
const dividers = [];
|
|
914
|
+
for (const el of root.querySelectorAll(".divider")) {
|
|
915
|
+
const nameEl = el.querySelector(".name");
|
|
916
|
+
if (!nameEl) continue;
|
|
917
|
+
const r = el.getBoundingClientRect();
|
|
918
|
+
const nr = nameEl.getBoundingClientRect();
|
|
919
|
+
dividers.push({
|
|
920
|
+
side: "html",
|
|
921
|
+
idx: dividers.length,
|
|
922
|
+
label: nameEl.textContent.trim(),
|
|
923
|
+
y: Math.round((r.top - rootRect.top + r.bottom - rootRect.top) / 2),
|
|
924
|
+
box: { x: Math.round(r.left - rootRect.left), y: Math.round(r.top - rootRect.top), w: Math.round(r.width), h: Math.round(r.height) },
|
|
925
|
+
label_box: { x: Math.round(nr.left - rootRect.left), y: Math.round(nr.top - rootRect.top), w: Math.round(nr.width), h: Math.round(nr.height) },
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
return dividers;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function collectSvgDividers(root, rootRect) {
|
|
932
|
+
const dividers = [];
|
|
933
|
+
for (const g of root.querySelectorAll("g.divider")) {
|
|
934
|
+
const label = g.querySelector(".divider-label");
|
|
935
|
+
const bg = g.querySelector(".divider-bg");
|
|
936
|
+
if (!label) continue;
|
|
937
|
+
const lr = label.getBoundingClientRect();
|
|
938
|
+
const bgRect = bg ? strokedElementOuterRect(bg, rootRect) : null;
|
|
939
|
+
dividers.push({
|
|
940
|
+
side: "svg",
|
|
941
|
+
idx: dividers.length,
|
|
942
|
+
label: label.textContent.trim(),
|
|
943
|
+
y: Math.round(lr.top - rootRect.top + lr.height / 2),
|
|
944
|
+
box: bgRect ? { x: Math.round(bgRect.x), y: Math.round(bgRect.y), w: Math.round(bgRect.w), h: Math.round(bgRect.h) } : null,
|
|
945
|
+
label_box: { x: Math.round(lr.left - rootRect.left), y: Math.round(lr.top - rootRect.top), w: Math.round(lr.width), h: Math.round(lr.height) },
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
return dividers;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const prepared = typeof window.prepareHtmlForCapture === "function"
|
|
952
|
+
? window.prepareHtmlForCapture()
|
|
953
|
+
: null;
|
|
954
|
+
const htmlOutput = document.getElementById("html-output");
|
|
955
|
+
const htmlRoot = htmlOutput.querySelector(".frame") || htmlOutput.querySelector(".sequence-diagram") || htmlOutput;
|
|
956
|
+
const svgRoot = document.querySelector("#svg-output > svg") || document.querySelector("#svg-output svg");
|
|
957
|
+
const htmlRootRect = htmlRoot.getBoundingClientRect();
|
|
958
|
+
const svgRootRect = svgRoot.getBoundingClientRect();
|
|
959
|
+
const svgFrameBorderEl = svgRoot.querySelector("rect.frame-border-inner, rect.frame-border, rect.frame-box");
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
caseName: new URLSearchParams(window.location.search).get("case") || "",
|
|
963
|
+
htmlRootSelector: htmlOutput.querySelector(".frame") ? "#html-output .frame" : "#html-output .sequence-diagram",
|
|
964
|
+
svgRootSelector: "#svg-output > svg",
|
|
965
|
+
prepared: Boolean(prepared),
|
|
966
|
+
htmlRoot: { width: htmlRootRect.width, height: htmlRootRect.height },
|
|
967
|
+
svgRoot: { width: svgRootRect.width, height: svgRootRect.height },
|
|
968
|
+
htmlRootBox: { x: 0, y: 0, w: htmlRootRect.width, h: htmlRootRect.height },
|
|
969
|
+
svgRootBox: { x: 0, y: 0, w: svgRootRect.width, h: svgRootRect.height },
|
|
970
|
+
svgFrameBorderBox: boxOrNull(strokedElementOuterRect(svgFrameBorderEl, svgRootRect)),
|
|
971
|
+
htmlLabels: collectHtmlLabels(htmlRoot, htmlRootRect),
|
|
972
|
+
svgLabels: collectSvgLabels(svgRoot, svgRootRect),
|
|
973
|
+
htmlNumbers: collectHtmlNumbers(htmlRoot, htmlRootRect),
|
|
974
|
+
svgNumbers: collectSvgNumbers(svgRoot, svgRootRect),
|
|
975
|
+
htmlArrows: collectHtmlArrows(htmlRoot, htmlRootRect),
|
|
976
|
+
svgArrows: collectSvgArrows(svgRoot, svgRootRect),
|
|
977
|
+
htmlParticipants: collectHtmlParticipants(htmlRoot, htmlRootRect),
|
|
978
|
+
svgParticipants: collectSvgParticipants(svgRoot, svgRootRect),
|
|
979
|
+
htmlComments: collectHtmlComments(htmlRoot, htmlRootRect),
|
|
980
|
+
svgComments: collectSvgComments(svgRoot, svgRootRect),
|
|
981
|
+
htmlGroups: collectHtmlGroups(htmlRoot, htmlRootRect),
|
|
982
|
+
svgGroups: collectSvgGroups(svgRoot, svgRootRect),
|
|
983
|
+
htmlOccurrences: collectHtmlOccurrences(htmlRoot, htmlRootRect),
|
|
984
|
+
svgOccurrences: collectSvgOccurrences(svgRoot, svgRootRect),
|
|
985
|
+
htmlFragmentDividers: collectHtmlFragmentDividers(htmlRoot, htmlRootRect),
|
|
986
|
+
svgFragmentDividers: collectSvgFragmentDividers(svgRoot, svgRootRect),
|
|
987
|
+
htmlDividers: collectHtmlDividers(htmlRoot, htmlRootRect),
|
|
988
|
+
svgDividers: collectSvgDividers(svgRoot, svgRootRect),
|
|
989
|
+
};
|
|
990
|
+
});
|
|
991
|
+
}
|