@zenuml/core 3.47.9 → 3.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cloud-icons-eHuugVSv.js.map +1 -0
- package/dist/zenuml.esm.mjs +2153 -2156
- package/dist/zenuml.esm.mjs.map +1 -0
- package/dist/zenuml.js +82 -82
- package/dist/zenuml.js.map +1 -0
- package/package.json +11 -1
- package/src/cli/zenuml.ts +1164 -0
- package/.agents/skills/babysit-pr/SKILL.md +0 -223
- package/.agents/skills/babysit-pr/agents/openai.yaml +0 -7
- package/.agents/skills/dia-scoring/SKILL.md +0 -139
- package/.agents/skills/dia-scoring/agents/openai.yaml +0 -7
- package/.agents/skills/dia-scoring/references/selectors-and-keys.md +0 -253
- package/.agents/skills/land-pr/SKILL.md +0 -120
- package/.agents/skills/propagate-core-release/SKILL.md +0 -205
- package/.agents/skills/propagate-core-release/agents/openai.yaml +0 -7
- package/.agents/skills/propagate-core-release/references/downstreams.md +0 -42
- package/.agents/skills/ship-branch/SKILL.md +0 -105
- package/.agents/skills/submit-branch/SKILL.md +0 -76
- package/.agents/skills/validate-branch/SKILL.md +0 -72
- package/.claude/commands/README.md +0 -162
- package/.claude/commands/analyze.md +0 -101
- package/.claude/commands/clarify.md +0 -158
- package/.claude/commands/code-review.md +0 -322
- package/.claude/commands/constitution.md +0 -73
- package/.claude/commands/create-docs.md +0 -309
- package/.claude/commands/full-context.md +0 -121
- package/.claude/commands/gemini-consult.md +0 -164
- package/.claude/commands/handoff.md +0 -146
- package/.claude/commands/implement.md +0 -56
- package/.claude/commands/plan.md +0 -43
- package/.claude/commands/refactor.md +0 -188
- package/.claude/commands/specify.md +0 -21
- package/.claude/commands/tasks.md +0 -62
- package/.claude/commands/update-docs.md +0 -314
- package/.claude/hooks/README.md +0 -270
- package/.claude/hooks/config/sensitive-patterns.json +0 -86
- package/.claude/hooks/gemini-context-injector.sh +0 -129
- package/.claude/hooks/mcp-security-scan.sh +0 -147
- package/.claude/hooks/notify.sh +0 -103
- package/.claude/hooks/setup/hook-setup.md +0 -96
- package/.claude/hooks/setup/settings.json.template +0 -63
- package/.claude/hooks/sounds/complete.wav +0 -0
- package/.claude/hooks/sounds/input-needed.wav +0 -0
- package/.claude/hooks/subagent-context-injector.sh +0 -65
- package/.claude/skills/babysit-pr/SKILL.md +0 -223
- package/.claude/skills/babysit-pr/agents/openai.yaml +0 -7
- package/.claude/skills/dia-scoring/SKILL.md +0 -139
- package/.claude/skills/dia-scoring/agents/openai.yaml +0 -7
- package/.claude/skills/dia-scoring/references/selectors-and-keys.md +0 -253
- package/.claude/skills/emoji-eval/SKILL.md +0 -187
- package/.claude/skills/land-pr/SKILL.md +0 -120
- package/.claude/skills/propagate-core-release/SKILL.md +0 -205
- package/.claude/skills/propagate-core-release/agents/openai.yaml +0 -7
- package/.claude/skills/propagate-core-release/references/downstreams.md +0 -42
- package/.claude/skills/ship-branch/SKILL.md +0 -105
- package/.claude/skills/submit-branch/SKILL.md +0 -76
- package/.claude/skills/validate-branch/SKILL.md +0 -72
- package/.claude/skills/zenuml-ux-research/SKILL.md +0 -183
- package/.claude/skills/zenuml-ux-research/references/assertion-catalog.md +0 -261
- package/.claude/skills/zenuml-ux-research/references/best-practices-overview.md +0 -56
- package/.claude/skills/zenuml-ux-research/references/report-template.md +0 -89
- package/.claude/skills/zenuml-ux-research/references/scenarios/edit-message-label.md +0 -37
- package/.claude/skills/zenuml-ux-research/references/scenarios/insert-message.md +0 -36
- package/.claude/skills/zenuml-ux-research/references/scenarios/insert-participant.md +0 -31
- package/.claude/skills/zenuml-ux-research/references/scenarios/rename-participant.md +0 -33
- package/.claude/skills/zenuml-ux-research/references/scenarios/undo-insert.md +0 -35
- package/.devcontainer/devcontainer.json +0 -21
- package/.dockerignore +0 -19
- package/.eslintrc.js +0 -39
- package/.git-blame-ignore-revs +0 -6
- package/.kiro/hooks/README.md +0 -38
- package/.kiro/hooks/session-sound-notification.js +0 -44
- package/.kiro/hooks/session-sound-notification.json +0 -23
- package/.mcp.json.example +0 -17
- package/.nvmrc +0 -1
- package/.prettierignore +0 -4
- package/.prettierrc +0 -1
- package/.specify/memory/constitution.md +0 -33
- package/.specify/scripts/bash/check-prerequisites.sh +0 -166
- package/.specify/scripts/bash/common.sh +0 -113
- package/.specify/scripts/bash/create-new-feature.sh +0 -97
- package/.specify/scripts/bash/setup-plan.sh +0 -60
- package/.specify/scripts/bash/update-agent-context.sh +0 -728
- package/.specify/templates/agent-file-template.md +0 -23
- package/.specify/templates/plan-template.md +0 -219
- package/.specify/templates/spec-template.md +0 -116
- package/.specify/templates/tasks-template.md +0 -127
- package/.storybook/main.ts +0 -25
- package/.storybook/preview.ts +0 -29
- package/.watchmanconfig +0 -3
- package/AGENTS.md +0 -26
- package/CLAUDE.md +0 -124
- package/DEPLOYMENT.md +0 -62
- package/Dockerfile +0 -36
- package/IMPLEMENTATION_PLAN.md +0 -163
- package/Integration/vanilla-js/index.html +0 -42
- package/MCP-ASSISTANT-RULES.md +0 -85
- package/README_CN.md +0 -15
- package/TUTORIAL.md +0 -116
- package/antlr/antlr-4.11.1-complete.jar +0 -0
- package/bun.lock +0 -1544
- package/bunfig.toml +0 -52
- package/docs/UNICODE_SUPPORT.md +0 -179
- package/docs/ai-context/deployment-infrastructure.md +0 -21
- package/docs/ai-context/docs-overview.md +0 -89
- package/docs/ai-context/handoff.md +0 -174
- package/docs/ai-context/project-structure.md +0 -160
- package/docs/ai-context/system-integration.md +0 -21
- package/docs/asciidoc/contributor.adoc +0 -54
- package/docs/asciidoc/create-my-own-theme.adoc +0 -149
- package/docs/asciidoc/images/creation-component.png +0 -0
- package/docs/asciidoc/images/creation-rtl.png +0 -0
- package/docs/asciidoc/images/message-arrow-rtl.png +0 -0
- package/docs/asciidoc/images/occurrence.png +0 -0
- package/docs/asciidoc/images/return-message-conflict.png +0 -0
- package/docs/asciidoc/images/shift-up-half-the-height.png +0 -0
- package/docs/asciidoc/images/three-layer-info-arch.png +0 -0
- package/docs/asciidoc/images/vertical-alignment.svg +0 -1
- package/docs/asciidoc/images/vertically-aligning.png +0 -0
- package/docs/asciidoc/index.adoc +0 -277
- package/docs/asciidoc/theme-debug-web-app.png +0 -0
- package/docs/asciidoc/tutorial.adoc +0 -22
- package/docs/asciidoc/user-css.png +0 -0
- package/docs/async-vs-sync-parser-rules.md +0 -81
- package/docs/divider-parser-allow-spaces.md +0 -38
- package/docs/highlighting-messages.md +0 -52
- package/docs/images/editor-sample.png +0 -0
- package/docs/inherited-vs-provided-from.md +0 -64
- package/docs/parser/Assignment.md +0 -8
- package/docs/parser/PARSER_IMPROVEMENTS_CC.md +0 -425
- package/docs/parser/grammar_review_gemini.md +0 -116
- package/docs/participants-function.md +0 -25
- package/docs/responsive-participant-margin.md +0 -52
- package/docs/starter.md +0 -9
- package/docs/superpowers/plans/2026-03-27-e2e-test-reorg.md +0 -698
- package/docs/superpowers/plans/2026-03-30-emoji-support.md +0 -1220
- package/docs/superpowers/plans/2026-03-30-self-correcting-scoring.md +0 -206
- package/docs/superpowers/plans/2026-04-15-keyboard-editing-on-diagram.md +0 -1992
- package/docs/superpowers/plans/2026-04-15-zenuml-ux-research-skill.md +0 -1452
- package/docs/ux-research/.gitkeep +0 -0
- package/docs/ux-research/2026-04-15-rename-participant.md +0 -156
- package/docs/ux-research/2026-04-18-insert-participant.md +0 -151
- package/docs/width-translate-and-offsets.md +0 -62
- package/docs/xss.md +0 -59
- package/e2e/data/compare-cases.js +0 -1090
- package/e2e/data/diff-algorithm.js +0 -199
- package/e2e/fixtures/create-message.html +0 -26
- package/e2e/fixtures/editable-label.html +0 -35
- package/e2e/fixtures/editable-span.html +0 -122
- package/e2e/fixtures/empty-diagram.html +0 -23
- package/e2e/fixtures/fixture.html +0 -31
- package/e2e/fixtures/insert-participant.html +0 -23
- package/e2e/fixtures/reorder-cross-fragment.html +0 -31
- package/e2e/fixtures/reorder-fragment.html +0 -29
- package/e2e/fixtures/reorder-message.html +0 -27
- package/e2e/fixtures/svg-test.html +0 -21
- package/e2e/fixtures/type-switch.html +0 -29
- package/e2e/tools/canonical-history.html +0 -908
- package/e2e/tools/compare-case.html +0 -371
- package/e2e/tools/compare.html +0 -35
- package/e2e/tools/native-diff-ext/background.js +0 -60
- package/e2e/tools/native-diff-ext/bridge.js +0 -26
- package/e2e/tools/native-diff-ext/content.js +0 -194
- package/e2e/tools/svg-preview.html +0 -56
- package/embed.html +0 -193
- package/eslint.config.mjs +0 -35
- package/firebase-debug.log +0 -108
- package/iframe-container-demo/diagram.html +0 -124
- package/iframe-container-demo/host.html +0 -817
- package/index.html +0 -771
- package/mermaid-zenuml-async-spa-auth.png +0 -0
- package/mermaid-zenuml-async-spa-auth.snapshot.md +0 -96
- package/newsletter/unicode-support-announcement.md +0 -134
- package/playground/creation.html +0 -53
- package/playground/message.html +0 -63
- package/playwright.config.ts +0 -40
- package/renderer.html +0 -366
- package/scripts/analyze-compare-case/collect-data.mjs +0 -1134
- package/scripts/analyze-compare-case/config.mjs +0 -102
- package/scripts/analyze-compare-case/geometry.mjs +0 -101
- package/scripts/analyze-compare-case/native-diff.mjs +0 -224
- package/scripts/analyze-compare-case/output.mjs +0 -74
- package/scripts/analyze-compare-case/panel-diff.mjs +0 -114
- package/scripts/analyze-compare-case/report.mjs +0 -162
- package/scripts/analyze-compare-case/residual-scopes.mjs +0 -347
- package/scripts/analyze-compare-case/scoring.mjs +0 -829
- package/scripts/analyze-compare-case.mjs +0 -149
- package/scripts/bump-version.js +0 -117
- package/scripts/snapshot-dual.js +0 -173
- package/scripts/update-snapshots.js +0 -70
- package/skills/dia-scoring/SKILL.md +0 -129
- package/skills/dia-scoring/agents/openai.yaml +0 -7
- package/skills/dia-scoring/references/selectors-and-keys.md +0 -253
- package/tailwind.config.js +0 -126
- package/test-compression.html +0 -274
- package/test-mermaid-zenuml.html +0 -57
- package/test-setup.ts +0 -124
- package/test-url-params.html +0 -192
- package/tsconfig.app.json +0 -31
- package/tsconfig.node.json +0 -24
- package/tsconfig.test.json +0 -9
- package/vite.config.lib.ts +0 -93
- package/vite.config.ts +0 -84
- package/wrangler.toml +0 -18
|
@@ -1,1134 +0,0 @@
|
|
|
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/e2e/tools/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 collectHtmlTitle(root, rootRect) {
|
|
277
|
-
const titleEl = root.querySelector(".title");
|
|
278
|
-
if (!titleEl) return null;
|
|
279
|
-
const text = (titleEl.textContent ?? "").trim();
|
|
280
|
-
if (!text) return null;
|
|
281
|
-
const measured = measureTextEntry(titleEl, rootRect);
|
|
282
|
-
return { side: "html", kind: "title", text, box: measured.box, font: measured.font, letters: measured.letters };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function collectSvgTitle(root, rootRect) {
|
|
286
|
-
const titleEl = root.querySelector("text.frame-title");
|
|
287
|
-
if (!titleEl) return null;
|
|
288
|
-
const text = (titleEl.textContent ?? "").trim();
|
|
289
|
-
if (!text) return null;
|
|
290
|
-
const measured = measureTextEntry(titleEl, rootRect);
|
|
291
|
-
return { side: "svg", kind: "title", text, box: measured.box, font: measured.font, letters: measured.letters };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function collectHtmlLabels(root, rootRect) {
|
|
295
|
-
const labels = [];
|
|
296
|
-
const selectorPairs = [
|
|
297
|
-
{
|
|
298
|
-
kind: "message",
|
|
299
|
-
selector:
|
|
300
|
-
".interaction:not(.return):not(.creation):not(.self-invocation):not(.self) > .message .editable-span-base",
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
kind: "self",
|
|
304
|
-
selector:
|
|
305
|
-
".interaction.self-invocation > .message .editable-span-base, .interaction.self > .self-invocation .editable-span-base",
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
kind: "return",
|
|
309
|
-
selector:
|
|
310
|
-
".interaction.return > .message .editable-span-base, .interaction.return > .flex.items-center > .name",
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
kind: "creation",
|
|
314
|
-
selector:
|
|
315
|
-
".interaction.creation .message .name",
|
|
316
|
-
},
|
|
317
|
-
];
|
|
318
|
-
|
|
319
|
-
for (const pair of selectorPairs) {
|
|
320
|
-
for (const labelEl of root.querySelectorAll(pair.selector)) {
|
|
321
|
-
const text = (labelEl.textContent ?? "").trim();
|
|
322
|
-
if (!text) continue;
|
|
323
|
-
const measured = measureTextEntry(labelEl, rootRect);
|
|
324
|
-
labels.push({
|
|
325
|
-
side: "html",
|
|
326
|
-
kind: pair.kind,
|
|
327
|
-
text,
|
|
328
|
-
box: measured.box,
|
|
329
|
-
font: measured.font,
|
|
330
|
-
letters: measured.letters,
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
for (const conditionWrap of root.querySelectorAll(".fragment .segment > .text-skin-fragment:not(.finally)")) {
|
|
336
|
-
const children = visibleChildren(conditionWrap);
|
|
337
|
-
if (children.length === 0) continue;
|
|
338
|
-
const text = children.map((child) => (child.textContent ?? "").trim()).join("").trim();
|
|
339
|
-
if (!text) continue;
|
|
340
|
-
const measured = measureTextEntryFromElements(children, rootRect, conditionWrap, children[0]);
|
|
341
|
-
labels.push({
|
|
342
|
-
side: "html",
|
|
343
|
-
kind: "fragment-condition",
|
|
344
|
-
text,
|
|
345
|
-
ownerText: fragmentOwnerText(conditionWrap.closest(".fragment")) || null,
|
|
346
|
-
box: measured.box,
|
|
347
|
-
font: measured.font,
|
|
348
|
-
letters: measured.letters,
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const sectionSelectors = [
|
|
353
|
-
".fragment.fragment-tcf .segment > .header.inline-block.bg-skin-frame.opacity-65",
|
|
354
|
-
".fragment.fragment-tcf .segment > .header.finally",
|
|
355
|
-
];
|
|
356
|
-
for (const selector of sectionSelectors) {
|
|
357
|
-
for (const sectionEl of root.querySelectorAll(selector)) {
|
|
358
|
-
const children = visibleChildren(sectionEl);
|
|
359
|
-
if (children.length === 0) continue;
|
|
360
|
-
const text = children.map((child) => (child.textContent ?? "").trim()).join("").trim();
|
|
361
|
-
if (!text) continue;
|
|
362
|
-
const measured = measureTextEntryFromElements(children, rootRect, sectionEl, children[0]);
|
|
363
|
-
labels.push({
|
|
364
|
-
side: "html",
|
|
365
|
-
kind: "fragment-section",
|
|
366
|
-
text,
|
|
367
|
-
ownerText: fragmentOwnerText(sectionEl.closest(".fragment")) || null,
|
|
368
|
-
box: measured.box,
|
|
369
|
-
font: measured.font,
|
|
370
|
-
letters: measured.letters,
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return labels;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function collectSvgLabels(root, rootRect) {
|
|
378
|
-
const labels = [];
|
|
379
|
-
const pairs = [
|
|
380
|
-
{ selector: "g.message:not(.self-call) > text.message-label", kind: "message" },
|
|
381
|
-
{ selector: "g.message.self-call > text.message-label", kind: "self" },
|
|
382
|
-
{ selector: "g.return > text.return-label", kind: "return" },
|
|
383
|
-
{ selector: "g.creation > text.message-label", kind: "creation" },
|
|
384
|
-
];
|
|
385
|
-
for (const pair of pairs) {
|
|
386
|
-
for (const labelEl of root.querySelectorAll(pair.selector)) {
|
|
387
|
-
const text = (labelEl.textContent ?? "").trim();
|
|
388
|
-
if (!text) continue;
|
|
389
|
-
const measured = measureTextEntry(labelEl, rootRect);
|
|
390
|
-
labels.push({
|
|
391
|
-
side: "svg",
|
|
392
|
-
kind: pair.kind,
|
|
393
|
-
text,
|
|
394
|
-
box: measured.box,
|
|
395
|
-
font: measured.font,
|
|
396
|
-
letters: measured.letters,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
for (const labelEl of root.querySelectorAll("g.fragment > text.fragment-condition")) {
|
|
402
|
-
const text = (labelEl.textContent ?? "").trim();
|
|
403
|
-
if (!text) continue;
|
|
404
|
-
const measured = measureTextEntry(labelEl, rootRect);
|
|
405
|
-
labels.push({
|
|
406
|
-
side: "svg",
|
|
407
|
-
kind: "fragment-condition",
|
|
408
|
-
text,
|
|
409
|
-
ownerText: textOrEmpty(labelEl.closest("g.fragment"), ":scope > text.fragment-label") || null,
|
|
410
|
-
box: measured.box,
|
|
411
|
-
font: measured.font,
|
|
412
|
-
letters: measured.letters,
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
for (const fragmentEl of root.querySelectorAll("g.fragment")) {
|
|
417
|
-
// Detect direct text.fragment-section-label children (e.g. [else] rendered without a <g> wrapper)
|
|
418
|
-
for (const directLabel of fragmentEl.querySelectorAll(":scope > text.fragment-section-label")) {
|
|
419
|
-
const text = (directLabel.textContent ?? "").trim();
|
|
420
|
-
if (!text) continue;
|
|
421
|
-
const measured = measureTextEntry(directLabel, rootRect);
|
|
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
|
-
for (const groupEl of fragmentEl.querySelectorAll(":scope > g")) {
|
|
433
|
-
const conditionTextEls = Array.from(groupEl.querySelectorAll(":scope > text.fragment-condition"));
|
|
434
|
-
if (conditionTextEls.length > 0) {
|
|
435
|
-
const text = conditionTextEls.map((el) => (el.textContent ?? "").trim()).join("").replace(/\s+\]/g, "]").trim();
|
|
436
|
-
if (!text) continue;
|
|
437
|
-
const measured = measureTextEntryFromElements(conditionTextEls, rootRect, groupEl, conditionTextEls[0]);
|
|
438
|
-
labels.push({
|
|
439
|
-
side: "svg",
|
|
440
|
-
kind: "fragment-condition",
|
|
441
|
-
text,
|
|
442
|
-
ownerText: textOrEmpty(fragmentEl, ":scope > text.fragment-label") || null,
|
|
443
|
-
box: measured.box,
|
|
444
|
-
font: measured.font,
|
|
445
|
-
letters: measured.letters,
|
|
446
|
-
});
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const textEls = Array.from(groupEl.querySelectorAll("text.fragment-section-label"));
|
|
451
|
-
if (textEls.length === 0) continue;
|
|
452
|
-
const text = textEls.map((el) => (el.textContent ?? "").trim()).join("").replace(/\s+\]/g, "]").trim();
|
|
453
|
-
if (!text) continue;
|
|
454
|
-
const measured = measureTextEntryFromElements(textEls, rootRect, groupEl, textEls[0]);
|
|
455
|
-
labels.push({
|
|
456
|
-
side: "svg",
|
|
457
|
-
kind: text.startsWith("[") ? "fragment-condition" : "fragment-section",
|
|
458
|
-
text,
|
|
459
|
-
ownerText: textOrEmpty(fragmentEl, ":scope > text.fragment-label") || null,
|
|
460
|
-
box: measured.box,
|
|
461
|
-
font: measured.font,
|
|
462
|
-
letters: measured.letters,
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
return labels;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function collectHtmlParticipants(root, rootRect) {
|
|
470
|
-
const participants = [];
|
|
471
|
-
for (const participantEl of root.querySelectorAll(".participant[data-participant-id]")) {
|
|
472
|
-
const name = (participantEl.getAttribute("data-participant-id") ?? "").trim();
|
|
473
|
-
if (!name) continue;
|
|
474
|
-
const participantBox = boxOrNull(relRect(participantEl.getBoundingClientRect(), rootRect));
|
|
475
|
-
if (!participantBox) continue;
|
|
476
|
-
|
|
477
|
-
const rowEl = participantEl.querySelector(":scope > .flex.items-center.justify-center, :scope > div:last-child");
|
|
478
|
-
const firstChild = rowEl?.firstElementChild ?? null;
|
|
479
|
-
// Find emoji span (span.mr-1.flex-shrink-0 containing emoji text)
|
|
480
|
-
const emojiSpan = participantEl.querySelector("span.mr-1.flex-shrink-0, span[data-testid='participant-emoji']");
|
|
481
|
-
const emojiText = emojiSpan ? emojiSpan.textContent.trim() : null;
|
|
482
|
-
// Find type icon div (div with aria-description or h-6.w-6 containing an SVG icon)
|
|
483
|
-
const typeIconDiv = firstChild && (
|
|
484
|
-
firstChild.matches("[aria-description]") ||
|
|
485
|
-
firstChild.querySelector("svg") ||
|
|
486
|
-
/\bh-6\b/.test(firstChild.className || "")
|
|
487
|
-
) ? firstChild : null;
|
|
488
|
-
const iconHost = typeIconDiv || emojiSpan || null;
|
|
489
|
-
const labelEl = Array.from(participantEl.querySelectorAll(".name")).at(-1) ?? null;
|
|
490
|
-
const measuredLabel = labelEl ? measureTextEntry(labelEl, rootRect) : null;
|
|
491
|
-
const stereotypeEl = participantEl.querySelector("label.interface");
|
|
492
|
-
const measuredStereotype = stereotypeEl ? measureTextEntry(stereotypeEl, rootRect) : null;
|
|
493
|
-
// For icon measurement: prefer type icon (so we compare type icon vs type icon across renderers).
|
|
494
|
-
// Fall back to emoji span for emoji-only participants.
|
|
495
|
-
const iconPaintRoot = typeIconDiv ? (typeIconDiv.querySelector("svg") ?? typeIconDiv) : (emojiSpan || null);
|
|
496
|
-
const participantStyle = getComputedStyle(participantEl);
|
|
497
|
-
|
|
498
|
-
participants.push({
|
|
499
|
-
side: "html",
|
|
500
|
-
name,
|
|
501
|
-
labelText: textContentNormalized(labelEl),
|
|
502
|
-
participantBox,
|
|
503
|
-
labelBox: measuredLabel?.box ?? null,
|
|
504
|
-
labelFont: measuredLabel?.font ?? null,
|
|
505
|
-
labelLetters: measuredLabel?.letters ?? [],
|
|
506
|
-
stereotypeText: textContentNormalized(stereotypeEl),
|
|
507
|
-
stereotypeBox: measuredStereotype?.box ?? null,
|
|
508
|
-
stereotypeFont: measuredStereotype?.font ?? null,
|
|
509
|
-
stereotypeLetters: measuredStereotype?.letters ?? [],
|
|
510
|
-
iconBox: paintedBox(iconPaintRoot, rootRect),
|
|
511
|
-
emojiText: emojiText || null,
|
|
512
|
-
anchorKind: measuredLabel?.box ? "label" : "participant-box",
|
|
513
|
-
anchorBox: measuredLabel?.box ?? participantBox,
|
|
514
|
-
backgroundColor: normalizeColorValue(participantStyle.backgroundColor),
|
|
515
|
-
textColor: normalizeColorValue(labelEl ? getComputedStyle(labelEl).color : participantStyle.color),
|
|
516
|
-
stereotypeColor: normalizeColorValue(stereotypeEl ? getComputedStyle(stereotypeEl).color : null),
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
return topParticipantsByName(participants);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function collectSvgParticipants(root, rootRect) {
|
|
523
|
-
const participants = [];
|
|
524
|
-
for (const participantEl of root.querySelectorAll("g.participant[data-participant]")) {
|
|
525
|
-
if (participantEl.classList.contains("participant-bottom")) {
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
const name = (participantEl.getAttribute("data-participant") ?? "").trim();
|
|
529
|
-
if (!name) continue;
|
|
530
|
-
const participantBoxEl = participantEl.querySelector(":scope > rect.participant-box");
|
|
531
|
-
const participantBox = boxOrNull(strokedElementOuterRect(participantBoxEl || participantEl, rootRect));
|
|
532
|
-
if (!participantBox) continue;
|
|
533
|
-
|
|
534
|
-
const labelEl = participantEl.querySelector(":scope > text.participant-label");
|
|
535
|
-
const measuredLabel = labelEl ? measureTextEntry(labelEl, rootRect) : null;
|
|
536
|
-
const stereotypeEl = participantEl.querySelector(":scope > text.stereotype-label")
|
|
537
|
-
|| Array.from(participantEl.querySelectorAll(":scope > text"))
|
|
538
|
-
.filter((textEl) => textEl !== labelEl)
|
|
539
|
-
.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)[0]
|
|
540
|
-
|| null;
|
|
541
|
-
const measuredStereotype = stereotypeEl ? measureTextEntry(stereotypeEl, rootRect) : null;
|
|
542
|
-
const iconEl = participantEl.querySelector(":scope > g.participant-icon[transform]");
|
|
543
|
-
// Detect emoji: separate text.participant-emoji element (present for both emoji-only AND icon+emoji participants)
|
|
544
|
-
// or first tspan in participant-label containing emoji codepoints (legacy inline-tspan format)
|
|
545
|
-
const emojiPattern = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE00}-\u{FE0F}\u{200D}]/u;
|
|
546
|
-
const emojiTextEl = participantEl.querySelector(":scope > text.participant-emoji") ?? null;
|
|
547
|
-
const emojiTspan = !emojiTextEl && labelEl
|
|
548
|
-
? Array.from(labelEl.querySelectorAll("tspan")).find((ts) => emojiPattern.test(ts.textContent))
|
|
549
|
-
: null;
|
|
550
|
-
const svgEmojiText = (emojiTextEl || emojiTspan)
|
|
551
|
-
? (emojiTextEl || emojiTspan).textContent.trim()
|
|
552
|
-
: null;
|
|
553
|
-
// For icon measurement: prefer type icon so we compare type icon vs type icon across renderers.
|
|
554
|
-
// Fall back to emoji element for emoji-only participants.
|
|
555
|
-
const iconTarget = iconEl || emojiTextEl || emojiTspan;
|
|
556
|
-
const participantBoxStyle = participantBoxEl ? getComputedStyle(participantBoxEl) : null;
|
|
557
|
-
|
|
558
|
-
participants.push({
|
|
559
|
-
side: "svg",
|
|
560
|
-
name,
|
|
561
|
-
labelText: textContentNormalized(labelEl),
|
|
562
|
-
participantBox,
|
|
563
|
-
labelBox: measuredLabel?.box ?? null,
|
|
564
|
-
labelFont: measuredLabel?.font ?? null,
|
|
565
|
-
labelLetters: measuredLabel?.letters ?? [],
|
|
566
|
-
stereotypeText: textContentNormalized(stereotypeEl),
|
|
567
|
-
stereotypeBox: measuredStereotype?.box ?? null,
|
|
568
|
-
stereotypeFont: measuredStereotype?.font ?? null,
|
|
569
|
-
stereotypeLetters: measuredStereotype?.letters ?? [],
|
|
570
|
-
iconBox: paintedBox(iconTarget, rootRect),
|
|
571
|
-
emojiText: svgEmojiText || null,
|
|
572
|
-
anchorKind: measuredLabel?.box ? "label" : "participant-box",
|
|
573
|
-
anchorBox: measuredLabel?.box ?? participantBox,
|
|
574
|
-
backgroundColor: normalizeColorValue(participantBoxStyle?.fill || participantBoxEl?.getAttribute("fill")),
|
|
575
|
-
textColor: normalizeColorValue(labelEl ? getComputedStyle(labelEl).fill : null),
|
|
576
|
-
stereotypeColor: normalizeColorValue(stereotypeEl ? getComputedStyle(stereotypeEl).fill : null),
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
return topParticipantsByName(participants);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function collectHtmlComments(root, rootRect) {
|
|
583
|
-
const comments = [];
|
|
584
|
-
for (const commentEl of root.querySelectorAll(".comments")) {
|
|
585
|
-
const text = textContentNormalized(commentEl);
|
|
586
|
-
if (!text) continue;
|
|
587
|
-
const measured = measureTextEntry(commentEl, rootRect, commentEl);
|
|
588
|
-
comments.push({
|
|
589
|
-
side: "html",
|
|
590
|
-
kind: "comment",
|
|
591
|
-
text,
|
|
592
|
-
box: measured.box,
|
|
593
|
-
font: measured.font,
|
|
594
|
-
letters: measured.letters,
|
|
595
|
-
color: normalizeColorValue(getComputedStyle(commentEl).color),
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
return comments;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function collectSvgComments(root, rootRect) {
|
|
602
|
-
const comments = [];
|
|
603
|
-
for (const commentEl of root.querySelectorAll("text.comment-text")) {
|
|
604
|
-
const text = textContentNormalized(commentEl);
|
|
605
|
-
if (!text) continue;
|
|
606
|
-
const measured = measureTextEntry(commentEl, rootRect);
|
|
607
|
-
comments.push({
|
|
608
|
-
side: "svg",
|
|
609
|
-
kind: "comment",
|
|
610
|
-
text,
|
|
611
|
-
box: measured.box,
|
|
612
|
-
font: measured.font,
|
|
613
|
-
letters: measured.letters,
|
|
614
|
-
color: normalizeColorValue(getComputedStyle(commentEl).fill),
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
return comments;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function collectHtmlGroups(root, rootRect) {
|
|
621
|
-
// Group containers appear twice: once in participant layer (has name label, no overlay)
|
|
622
|
-
// and once in lifeline layer (has overlay rect, no name label).
|
|
623
|
-
// Collect name data from participant-layer containers and outline boxes from
|
|
624
|
-
// lifeline-layer containers, then merge by index order.
|
|
625
|
-
const nameEntries = [];
|
|
626
|
-
const boxEntries = [];
|
|
627
|
-
for (const groupEl of root.querySelectorAll(".lifeline-group-container")) {
|
|
628
|
-
const nameEl = groupEl.querySelector(".text-skin-lifeline-group-name");
|
|
629
|
-
const outlineRect = groupEl.querySelector("[data-group-overlay] rect");
|
|
630
|
-
if (nameEl) {
|
|
631
|
-
nameEntries.push({
|
|
632
|
-
name: textContentNormalized(nameEl),
|
|
633
|
-
measuredName: measureTextEntry(nameEl, rootRect),
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
if (outlineRect) {
|
|
637
|
-
boxEntries.push(boxOrNull(relRect(outlineRect.getBoundingClientRect(), rootRect)));
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
const groups = [];
|
|
641
|
-
for (let i = 0; i < nameEntries.length; i++) {
|
|
642
|
-
const box = boxEntries[i] || null;
|
|
643
|
-
if (!box) continue;
|
|
644
|
-
groups.push({
|
|
645
|
-
side: "html",
|
|
646
|
-
name: nameEntries[i].name,
|
|
647
|
-
box,
|
|
648
|
-
nameBox: nameEntries[i].measuredName?.box ?? null,
|
|
649
|
-
nameFont: nameEntries[i].measuredName?.font ?? null,
|
|
650
|
-
nameLetters: nameEntries[i].measuredName?.letters ?? [],
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
return groups;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function collectSvgGroups(root, rootRect) {
|
|
657
|
-
const groups = [];
|
|
658
|
-
for (const groupEl of root.querySelectorAll("g.participant-group")) {
|
|
659
|
-
const nameEl = groupEl.querySelector(":scope > text");
|
|
660
|
-
const name = textContentNormalized(nameEl);
|
|
661
|
-
// Measure the outline <rect> directly — consistent with HTML side measurement.
|
|
662
|
-
const outlineRect = groupEl.querySelector("rect.group-outline");
|
|
663
|
-
const measureEl = outlineRect || groupEl;
|
|
664
|
-
const box = boxOrNull(relRect(measureEl.getBoundingClientRect(), rootRect));
|
|
665
|
-
if (!box) continue;
|
|
666
|
-
const measuredName = nameEl ? measureTextEntry(nameEl, rootRect) : null;
|
|
667
|
-
groups.push({
|
|
668
|
-
side: "svg",
|
|
669
|
-
name,
|
|
670
|
-
box,
|
|
671
|
-
nameBox: measuredName?.box ?? null,
|
|
672
|
-
nameFont: measuredName?.font ?? null,
|
|
673
|
-
nameLetters: measuredName?.letters ?? [],
|
|
674
|
-
});
|
|
675
|
-
}
|
|
676
|
-
return groups;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function collectHtmlArrows(root, rootRect) {
|
|
680
|
-
const arrows = [];
|
|
681
|
-
|
|
682
|
-
function addArrow(kind, interaction, text, parts) {
|
|
683
|
-
if (!text || parts.length === 0) return;
|
|
684
|
-
const box = unionRect(parts.map((part) => part.box));
|
|
685
|
-
const labelText = (interaction.getAttribute("data-signature") || "").trim();
|
|
686
|
-
arrows.push({
|
|
687
|
-
side: "html",
|
|
688
|
-
kind,
|
|
689
|
-
text,
|
|
690
|
-
pairText: text,
|
|
691
|
-
box,
|
|
692
|
-
...arrowEndpointsFromBox(box),
|
|
693
|
-
labelText,
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
for (const interaction of root.querySelectorAll(".interaction:not(.return):not(.creation):not(.self-invocation):not(.self)")) {
|
|
698
|
-
const text = (interaction.querySelector(":scope > .message > .absolute.text-xs")?.textContent || "").trim()
|
|
699
|
-
|| (interaction.getAttribute("data-signature") || "").trim();
|
|
700
|
-
const messageEl = interaction.querySelector(":scope > .message");
|
|
701
|
-
if (!messageEl) continue;
|
|
702
|
-
const svgChildren = Array.from(messageEl.children).filter((child) => child.tagName?.toLowerCase() === "svg");
|
|
703
|
-
const parts = [];
|
|
704
|
-
if (svgChildren[0]) {
|
|
705
|
-
parts.push({ part: "line", box: relRect(svgChildren[0].getBoundingClientRect(), rootRect) });
|
|
706
|
-
}
|
|
707
|
-
if (svgChildren[1]) {
|
|
708
|
-
parts.push({ part: "head", box: relRect(svgChildren[1].getBoundingClientRect(), rootRect) });
|
|
709
|
-
}
|
|
710
|
-
addArrow("message", interaction, text, parts);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
for (const interaction of root.querySelectorAll(".interaction.return")) {
|
|
714
|
-
const text = (interaction.querySelector(":scope > .message > .absolute.text-xs")?.textContent || "").trim()
|
|
715
|
-
|| (interaction.getAttribute("data-signature") || "").trim();
|
|
716
|
-
const messageEl = interaction.querySelector(":scope > .message");
|
|
717
|
-
if (!messageEl) continue;
|
|
718
|
-
const svgChildren = Array.from(messageEl.children).filter((child) => child.tagName?.toLowerCase() === "svg");
|
|
719
|
-
const parts = [];
|
|
720
|
-
if (svgChildren[0]) {
|
|
721
|
-
parts.push({ part: "line", box: relRect(svgChildren[0].getBoundingClientRect(), rootRect) });
|
|
722
|
-
}
|
|
723
|
-
if (svgChildren[1]) {
|
|
724
|
-
parts.push({ part: "head", box: relRect(svgChildren[1].getBoundingClientRect(), rootRect) });
|
|
725
|
-
}
|
|
726
|
-
addArrow("return", interaction, text, parts);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
for (const interaction of root.querySelectorAll(".interaction.self, .interaction.self-invocation")) {
|
|
730
|
-
const text = (interaction.querySelector(":scope > .message .absolute.text-xs, :scope > .self-invocation .absolute.text-xs")?.textContent || "").trim()
|
|
731
|
-
|| (interaction.getAttribute("data-signature") || "").trim();
|
|
732
|
-
const arrowSvg = interaction.querySelector(":scope > .message > svg.arrow, :scope > .self-invocation > svg.arrow");
|
|
733
|
-
if (!arrowSvg) continue;
|
|
734
|
-
const parts = [];
|
|
735
|
-
pushBox(parts, "loop", elementRect(arrowSvg.querySelector(":scope > path, :scope > polyline"), rootRect));
|
|
736
|
-
pushBox(parts, "head", elementRect(arrowSvg.querySelector(":scope > g path, :scope > g polyline"), rootRect));
|
|
737
|
-
if (parts.length === 0) {
|
|
738
|
-
pushBox(parts, "loop", elementRect(arrowSvg, rootRect));
|
|
739
|
-
}
|
|
740
|
-
addArrow("self", interaction, text, parts);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
return arrows;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function collectSvgArrows(root, rootRect) {
|
|
747
|
-
const arrows = [];
|
|
748
|
-
|
|
749
|
-
function addArrow(kind, group, text, parts) {
|
|
750
|
-
if (!text || parts.length === 0) return;
|
|
751
|
-
const box = unionRect(parts.map((part) => part.box));
|
|
752
|
-
const labelText = (group.querySelector("text.message-label, text.return-label")?.textContent || "").trim();
|
|
753
|
-
arrows.push({
|
|
754
|
-
side: "svg",
|
|
755
|
-
kind,
|
|
756
|
-
text,
|
|
757
|
-
pairText: text,
|
|
758
|
-
box,
|
|
759
|
-
...arrowEndpointsFromBox(box),
|
|
760
|
-
labelText,
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
for (const group of root.querySelectorAll("g.message:not(.self-call)")) {
|
|
765
|
-
const text = (group.querySelector("text.seq-number")?.textContent || "").trim()
|
|
766
|
-
|| (group.querySelector("text.message-label")?.textContent || "").trim();
|
|
767
|
-
const parts = [];
|
|
768
|
-
const lineEl = group.querySelector(":scope > line.message-line");
|
|
769
|
-
const headEl = group.querySelector(":scope > svg.arrow-head");
|
|
770
|
-
if (lineEl) parts.push({ part: "line", box: relRect(lineEl.getBoundingClientRect(), rootRect) });
|
|
771
|
-
if (headEl) parts.push({ part: "head", box: relRect(headEl.getBoundingClientRect(), rootRect) });
|
|
772
|
-
addArrow("message", group, text, parts);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
for (const group of root.querySelectorAll("g.return")) {
|
|
776
|
-
const text = (group.querySelector("text.seq-number")?.textContent || "").trim()
|
|
777
|
-
|| (group.querySelector("text.return-label")?.textContent || "").trim();
|
|
778
|
-
const parts = [];
|
|
779
|
-
const lineEl = group.querySelector(":scope > line.return-line");
|
|
780
|
-
const headEl = group.querySelector(":scope > polyline.return-arrow");
|
|
781
|
-
if (lineEl) parts.push({ part: "line", box: relRect(lineEl.getBoundingClientRect(), rootRect) });
|
|
782
|
-
if (headEl) parts.push({ part: "head", box: relRect(headEl.getBoundingClientRect(), rootRect) });
|
|
783
|
-
addArrow("return", group, text, parts);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
for (const group of root.querySelectorAll("g.message.self-call")) {
|
|
787
|
-
const text = (group.querySelector("text.seq-number")?.textContent || "").trim()
|
|
788
|
-
|| (group.querySelector("text.message-label")?.textContent || "").trim();
|
|
789
|
-
const loopEl = group.querySelector(":scope > svg");
|
|
790
|
-
if (!loopEl) continue;
|
|
791
|
-
const parts = [];
|
|
792
|
-
pushBox(parts, "loop", elementRect(loopEl.querySelector(":scope > path, :scope > polyline"), rootRect));
|
|
793
|
-
pushBox(parts, "head", elementRect(loopEl.querySelector(":scope > g path, :scope > g polyline"), rootRect));
|
|
794
|
-
if (parts.length === 0) {
|
|
795
|
-
const loopRect = loopEl.getBoundingClientRect();
|
|
796
|
-
const attrW = parseFloat(loopEl.getAttribute("width"));
|
|
797
|
-
const attrH = parseFloat(loopEl.getAttribute("height"));
|
|
798
|
-
const box = relRect(loopRect, rootRect);
|
|
799
|
-
if (attrW && attrH) {
|
|
800
|
-
box.w = attrW;
|
|
801
|
-
box.h = attrH;
|
|
802
|
-
}
|
|
803
|
-
pushBox(parts, "loop", box);
|
|
804
|
-
}
|
|
805
|
-
addArrow("self", group, text, parts);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
return arrows;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function collectHtmlNumbers(root, rootRect) {
|
|
812
|
-
const numbers = [];
|
|
813
|
-
const selectorPairs = [
|
|
814
|
-
{
|
|
815
|
-
kind: "message",
|
|
816
|
-
selector:
|
|
817
|
-
".interaction:not(.return):not(.creation):not(.self-invocation):not(.self) > .message > .absolute.text-xs",
|
|
818
|
-
ownerText: (numberEl) => textOrEmpty(numberEl.closest(".interaction"), ":scope > .message .editable-span-base"),
|
|
819
|
-
},
|
|
820
|
-
{
|
|
821
|
-
kind: "self",
|
|
822
|
-
selector:
|
|
823
|
-
".interaction.self-invocation > .message .absolute.text-xs, .interaction.self > .self-invocation .absolute.text-xs",
|
|
824
|
-
ownerText: (numberEl) =>
|
|
825
|
-
textOrEmpty(numberEl.closest(".interaction"), ":scope > .message .editable-span-base, :scope > .self-invocation .editable-span-base"),
|
|
826
|
-
},
|
|
827
|
-
{
|
|
828
|
-
kind: "return",
|
|
829
|
-
selector:
|
|
830
|
-
".interaction.return > .message > .absolute.text-xs",
|
|
831
|
-
ownerText: (numberEl) =>
|
|
832
|
-
textOrEmpty(numberEl.closest(".interaction"), ":scope > .message .editable-span-base, :scope > .message .name"),
|
|
833
|
-
},
|
|
834
|
-
{
|
|
835
|
-
kind: "fragment",
|
|
836
|
-
selector:
|
|
837
|
-
".fragment > .header > .absolute.text-xs",
|
|
838
|
-
ownerText: (numberEl) => fragmentOwnerText(numberEl.closest(".fragment")),
|
|
839
|
-
},
|
|
840
|
-
];
|
|
841
|
-
|
|
842
|
-
for (const pair of selectorPairs) {
|
|
843
|
-
for (const numberEl of root.querySelectorAll(pair.selector)) {
|
|
844
|
-
const text = (numberEl.textContent ?? "").trim();
|
|
845
|
-
if (!text) continue;
|
|
846
|
-
numbers.push({
|
|
847
|
-
side: "html",
|
|
848
|
-
kind: pair.kind,
|
|
849
|
-
text,
|
|
850
|
-
pairText: pair.ownerText ? pair.ownerText(numberEl) || text : text,
|
|
851
|
-
ownerText: pair.ownerText ? pair.ownerText(numberEl) || null : null,
|
|
852
|
-
box: relRect(numberEl.getBoundingClientRect(), rootRect),
|
|
853
|
-
font: fontInfo(numberEl),
|
|
854
|
-
letters: glyphBoxesForElement(numberEl, rootRect),
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
return numbers;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function collectSvgNumbers(root, rootRect) {
|
|
862
|
-
const numbers = [];
|
|
863
|
-
const pairs = [
|
|
864
|
-
{
|
|
865
|
-
selector: "g.message:not(.self-call) > text.seq-number",
|
|
866
|
-
kind: "message",
|
|
867
|
-
ownerText: (numberEl) => textOrEmpty(numberEl.closest("g.message"), ":scope > text.message-label"),
|
|
868
|
-
},
|
|
869
|
-
{
|
|
870
|
-
selector: "g.message.self-call > text.seq-number",
|
|
871
|
-
kind: "self",
|
|
872
|
-
ownerText: (numberEl) => textOrEmpty(numberEl.closest("g.message"), ":scope > text.message-label"),
|
|
873
|
-
},
|
|
874
|
-
{
|
|
875
|
-
selector: "g.return > text.seq-number",
|
|
876
|
-
kind: "return",
|
|
877
|
-
ownerText: (numberEl) => textOrEmpty(numberEl.closest("g.return"), ":scope > text.return-label"),
|
|
878
|
-
},
|
|
879
|
-
{
|
|
880
|
-
selector: "g.fragment > text.seq-number",
|
|
881
|
-
kind: "fragment",
|
|
882
|
-
ownerText: (numberEl) => textOrEmpty(numberEl.closest("g.fragment"), ":scope > text.fragment-label"),
|
|
883
|
-
},
|
|
884
|
-
];
|
|
885
|
-
for (const pair of pairs) {
|
|
886
|
-
for (const numberEl of root.querySelectorAll(pair.selector)) {
|
|
887
|
-
const text = (numberEl.textContent ?? "").trim();
|
|
888
|
-
if (!text) continue;
|
|
889
|
-
// SVG <text> getBoundingClientRect returns glyph bounds (height ~14px for 12px font).
|
|
890
|
-
// HTML <div> with line-height:16px adds 2px top padding ((16-12)/2).
|
|
891
|
-
// Adjust SVG box Y by -1 to align with HTML's line-height-padded top edge.
|
|
892
|
-
const rawBox = relRect(numberEl.getBoundingClientRect(), rootRect);
|
|
893
|
-
const adjustedBox = { ...rawBox, y: rawBox.y - 1, h: rawBox.h + 2 };
|
|
894
|
-
numbers.push({
|
|
895
|
-
side: "svg",
|
|
896
|
-
kind: pair.kind,
|
|
897
|
-
text,
|
|
898
|
-
pairText: pair.ownerText ? pair.ownerText(numberEl) || text : text,
|
|
899
|
-
ownerText: pair.ownerText ? pair.ownerText(numberEl) || null : null,
|
|
900
|
-
box: adjustedBox,
|
|
901
|
-
font: fontInfo(numberEl),
|
|
902
|
-
letters: glyphBoxesForElement(numberEl, rootRect),
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
return numbers;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
function collectHtmlOccurrences(root, rootRect) {
|
|
910
|
-
const occurrences = [];
|
|
911
|
-
for (const el of root.querySelectorAll('[data-el-type="occurrence"]')) {
|
|
912
|
-
const participant = (el.getAttribute("data-belongs-to") ?? "").trim();
|
|
913
|
-
const box = boxOrNull(relRect(el.getBoundingClientRect(), rootRect));
|
|
914
|
-
if (!box) continue;
|
|
915
|
-
occurrences.push({
|
|
916
|
-
side: "html",
|
|
917
|
-
participant,
|
|
918
|
-
idx: occurrences.length,
|
|
919
|
-
box,
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
return occurrences;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
function collectSvgOccurrences(root, rootRect) {
|
|
926
|
-
const occurrences = [];
|
|
927
|
-
for (const el of root.querySelectorAll("rect.occurrence")) {
|
|
928
|
-
const participant = (el.getAttribute("data-participant") ?? "").trim();
|
|
929
|
-
const box = boxOrNull(strokedElementOuterRect(el, rootRect));
|
|
930
|
-
if (!box) continue;
|
|
931
|
-
occurrences.push({
|
|
932
|
-
side: "svg",
|
|
933
|
-
participant,
|
|
934
|
-
idx: occurrences.length,
|
|
935
|
-
box,
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
return occurrences;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function collectHtmlFragmentDividers(root, rootRect) {
|
|
942
|
-
const dividers = [];
|
|
943
|
-
// Alt/tcf dividers: .segment.border-t
|
|
944
|
-
for (const seg of root.querySelectorAll(".segment.border-t")) {
|
|
945
|
-
const r = seg.getBoundingClientRect();
|
|
946
|
-
const y = r.top - rootRect.top;
|
|
947
|
-
const x = r.left - rootRect.left;
|
|
948
|
-
const w = r.width;
|
|
949
|
-
const label = (seg.querySelector(".text-skin-fragment")?.textContent ?? "").trim();
|
|
950
|
-
dividers.push({ side: "html", idx: dividers.length, y, x, width: w, label, source: "segment" });
|
|
951
|
-
}
|
|
952
|
-
// Par dividers: .statement-container with computed border-top inside .par
|
|
953
|
-
for (const sc of root.querySelectorAll(".par .statement-container")) {
|
|
954
|
-
const style = getComputedStyle(sc);
|
|
955
|
-
if (parseFloat(style.borderTopWidth) < 1) continue;
|
|
956
|
-
const r = sc.getBoundingClientRect();
|
|
957
|
-
const y = r.top - rootRect.top;
|
|
958
|
-
const x = r.left - rootRect.left;
|
|
959
|
-
const w = r.width;
|
|
960
|
-
dividers.push({ side: "html", idx: dividers.length, y, x, width: w, label: "", source: "par" });
|
|
961
|
-
}
|
|
962
|
-
// Sort by Y position for consistent pairing
|
|
963
|
-
dividers.sort((a, b) => a.y - b.y);
|
|
964
|
-
dividers.forEach((d, i) => { d.idx = i; });
|
|
965
|
-
return dividers;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
function collectSvgFragmentDividers(root, rootRect) {
|
|
969
|
-
const dividers = [];
|
|
970
|
-
for (const line of root.querySelectorAll("line.fragment-separator")) {
|
|
971
|
-
const lineRect = line.getBoundingClientRect();
|
|
972
|
-
const strokeWidth = parseFloat(getComputedStyle(line).strokeWidth || "0") || 0;
|
|
973
|
-
const half = strokeWidth / 2;
|
|
974
|
-
// The painted top edge of the stroke = center - half.
|
|
975
|
-
// This matches HTML border-top measurement (top edge of the 1px border).
|
|
976
|
-
const y = (lineRect.top - rootRect.top) - half;
|
|
977
|
-
const x = lineRect.left - rootRect.left;
|
|
978
|
-
const w = lineRect.width;
|
|
979
|
-
dividers.push({ side: "svg", idx: dividers.length, y, x, width: w, label: "" });
|
|
980
|
-
}
|
|
981
|
-
return dividers;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function collectHtmlDividers(root, rootRect) {
|
|
985
|
-
const dividers = [];
|
|
986
|
-
for (const el of root.querySelectorAll(".divider")) {
|
|
987
|
-
const nameEl = el.querySelector(".name");
|
|
988
|
-
if (!nameEl) continue;
|
|
989
|
-
const r = el.getBoundingClientRect();
|
|
990
|
-
const nr = nameEl.getBoundingClientRect();
|
|
991
|
-
dividers.push({
|
|
992
|
-
side: "html",
|
|
993
|
-
idx: dividers.length,
|
|
994
|
-
label: nameEl.textContent.trim(),
|
|
995
|
-
y: Math.round((r.top - rootRect.top + r.bottom - rootRect.top) / 2),
|
|
996
|
-
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) },
|
|
997
|
-
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) },
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
return dividers;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
function collectSvgDividers(root, rootRect) {
|
|
1004
|
-
const dividers = [];
|
|
1005
|
-
for (const g of root.querySelectorAll("g.divider")) {
|
|
1006
|
-
const label = g.querySelector(".divider-label");
|
|
1007
|
-
const bg = g.querySelector(".divider-bg");
|
|
1008
|
-
if (!label) continue;
|
|
1009
|
-
const lr = label.getBoundingClientRect();
|
|
1010
|
-
const bgRect = bg ? strokedElementOuterRect(bg, rootRect) : null;
|
|
1011
|
-
dividers.push({
|
|
1012
|
-
side: "svg",
|
|
1013
|
-
idx: dividers.length,
|
|
1014
|
-
label: label.textContent.trim(),
|
|
1015
|
-
y: Math.round(lr.top - rootRect.top + lr.height / 2),
|
|
1016
|
-
box: bgRect ? { x: Math.round(bgRect.x), y: Math.round(bgRect.y), w: Math.round(bgRect.w), h: Math.round(bgRect.h) } : null,
|
|
1017
|
-
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) },
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
return dividers;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* Critical fragment header color calibration.
|
|
1025
|
-
* HTML renders .fragment-critical .header::before with border-bottom: 2px solid
|
|
1026
|
-
* (the unique thick header separator not present on other fragment types).
|
|
1027
|
-
* SVG renders no extra border for critical kind.
|
|
1028
|
-
* This collects the header bounding box + color style from both sides for comparison.
|
|
1029
|
-
*/
|
|
1030
|
-
function collectHtmlCriticalFragmentHeaders(root, rootRect) {
|
|
1031
|
-
const results = [];
|
|
1032
|
-
for (const frag of root.querySelectorAll(".fragment.fragment-critical")) {
|
|
1033
|
-
const header = frag.querySelector(":scope > .header");
|
|
1034
|
-
if (!header) continue;
|
|
1035
|
-
const r = header.getBoundingClientRect();
|
|
1036
|
-
const box = {
|
|
1037
|
-
x: Math.round(r.left - rootRect.left),
|
|
1038
|
-
y: Math.round(r.top - rootRect.top),
|
|
1039
|
-
w: Math.round(r.width),
|
|
1040
|
-
h: Math.round(r.height),
|
|
1041
|
-
};
|
|
1042
|
-
// ::before pseudo-element carries the border-bottom: 2px solid style
|
|
1043
|
-
const pseudoStyle = window.getComputedStyle(header, "::before");
|
|
1044
|
-
const borderBottomWidth = parseFloat(pseudoStyle.borderBottomWidth || "0") || 0;
|
|
1045
|
-
const borderBottomColor = pseudoStyle.borderBottomColor || "";
|
|
1046
|
-
results.push({
|
|
1047
|
-
side: "html",
|
|
1048
|
-
idx: results.length,
|
|
1049
|
-
box,
|
|
1050
|
-
headerBottomY: box.y + box.h,
|
|
1051
|
-
borderBottomWidth,
|
|
1052
|
-
borderBottomColor,
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
return results;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
function collectSvgCriticalFragmentHeaders(root, rootRect) {
|
|
1059
|
-
const results = [];
|
|
1060
|
-
for (const frag of root.querySelectorAll("g.fragment.fragment-critical")) {
|
|
1061
|
-
const headerRect_el = frag.querySelector("rect.fragment-header");
|
|
1062
|
-
if (!headerRect_el) continue;
|
|
1063
|
-
const r = headerRect_el.getBoundingClientRect();
|
|
1064
|
-
const box = {
|
|
1065
|
-
x: Math.round(r.left - rootRect.left),
|
|
1066
|
-
y: Math.round(r.top - rootRect.top),
|
|
1067
|
-
w: Math.round(r.width),
|
|
1068
|
-
h: Math.round(r.height),
|
|
1069
|
-
};
|
|
1070
|
-
const headerBottomY = box.y + box.h;
|
|
1071
|
-
// Look for a line element at the header bottom edge (within 2px)
|
|
1072
|
-
const lines = Array.from(frag.querySelectorAll("line"));
|
|
1073
|
-
const borderLine = lines.find(l => {
|
|
1074
|
-
const lr = l.getBoundingClientRect();
|
|
1075
|
-
return Math.abs((lr.top - rootRect.top) - headerBottomY) < 3;
|
|
1076
|
-
});
|
|
1077
|
-
results.push({
|
|
1078
|
-
side: "svg",
|
|
1079
|
-
idx: results.length,
|
|
1080
|
-
box,
|
|
1081
|
-
headerBottomY,
|
|
1082
|
-
hasHeaderBottomLine: !!borderLine,
|
|
1083
|
-
headerBottomLineColor: borderLine ? (borderLine.getAttribute("stroke") || getComputedStyle(borderLine).stroke || "") : null,
|
|
1084
|
-
headerBottomLineWidth: borderLine ? (parseFloat(borderLine.getAttribute("stroke-width") || getComputedStyle(borderLine).strokeWidth || "0") || 0) : null,
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
return results;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
const prepared = typeof window.prepareHtmlForCapture === "function"
|
|
1091
|
-
? window.prepareHtmlForCapture()
|
|
1092
|
-
: null;
|
|
1093
|
-
const htmlOutput = document.getElementById("html-output");
|
|
1094
|
-
const htmlRoot = htmlOutput.querySelector(".frame") || htmlOutput.querySelector(".sequence-diagram") || htmlOutput;
|
|
1095
|
-
const svgRoot = document.querySelector("#svg-output > svg") || document.querySelector("#svg-output svg");
|
|
1096
|
-
const htmlRootRect = htmlRoot.getBoundingClientRect();
|
|
1097
|
-
const svgRootRect = svgRoot.getBoundingClientRect();
|
|
1098
|
-
const svgFrameBorderEl = svgRoot.querySelector("rect.frame-border-inner, rect.frame-border, rect.frame-box");
|
|
1099
|
-
|
|
1100
|
-
return {
|
|
1101
|
-
caseName: new URLSearchParams(window.location.search).get("case") || "",
|
|
1102
|
-
htmlRootSelector: htmlOutput.querySelector(".frame") ? "#html-output .frame" : "#html-output .sequence-diagram",
|
|
1103
|
-
svgRootSelector: "#svg-output > svg",
|
|
1104
|
-
prepared: Boolean(prepared),
|
|
1105
|
-
htmlRoot: { width: htmlRootRect.width, height: htmlRootRect.height },
|
|
1106
|
-
svgRoot: { width: svgRootRect.width, height: svgRootRect.height },
|
|
1107
|
-
htmlRootBox: { x: 0, y: 0, w: htmlRootRect.width, h: htmlRootRect.height },
|
|
1108
|
-
svgRootBox: { x: 0, y: 0, w: svgRootRect.width, h: svgRootRect.height },
|
|
1109
|
-
svgFrameBorderBox: boxOrNull(strokedElementOuterRect(svgFrameBorderEl, svgRootRect)),
|
|
1110
|
-
htmlTitle: collectHtmlTitle(htmlRoot, htmlRootRect),
|
|
1111
|
-
svgTitle: collectSvgTitle(svgRoot, svgRootRect),
|
|
1112
|
-
htmlLabels: collectHtmlLabels(htmlRoot, htmlRootRect),
|
|
1113
|
-
svgLabels: collectSvgLabels(svgRoot, svgRootRect),
|
|
1114
|
-
htmlNumbers: collectHtmlNumbers(htmlRoot, htmlRootRect),
|
|
1115
|
-
svgNumbers: collectSvgNumbers(svgRoot, svgRootRect),
|
|
1116
|
-
htmlArrows: collectHtmlArrows(htmlRoot, htmlRootRect),
|
|
1117
|
-
svgArrows: collectSvgArrows(svgRoot, svgRootRect),
|
|
1118
|
-
htmlParticipants: collectHtmlParticipants(htmlRoot, htmlRootRect),
|
|
1119
|
-
svgParticipants: collectSvgParticipants(svgRoot, svgRootRect),
|
|
1120
|
-
htmlComments: collectHtmlComments(htmlRoot, htmlRootRect),
|
|
1121
|
-
svgComments: collectSvgComments(svgRoot, svgRootRect),
|
|
1122
|
-
htmlGroups: collectHtmlGroups(htmlRoot, htmlRootRect),
|
|
1123
|
-
svgGroups: collectSvgGroups(svgRoot, svgRootRect),
|
|
1124
|
-
htmlOccurrences: collectHtmlOccurrences(htmlRoot, htmlRootRect),
|
|
1125
|
-
svgOccurrences: collectSvgOccurrences(svgRoot, svgRootRect),
|
|
1126
|
-
htmlFragmentDividers: collectHtmlFragmentDividers(htmlRoot, htmlRootRect),
|
|
1127
|
-
svgFragmentDividers: collectSvgFragmentDividers(svgRoot, svgRootRect),
|
|
1128
|
-
htmlDividers: collectHtmlDividers(htmlRoot, htmlRootRect),
|
|
1129
|
-
svgDividers: collectSvgDividers(svgRoot, svgRootRect),
|
|
1130
|
-
htmlCriticalFragmentHeaders: collectHtmlCriticalFragmentHeaders(htmlRoot, htmlRootRect),
|
|
1131
|
-
svgCriticalFragmentHeaders: collectSvgCriticalFragmentHeaders(svgRoot, svgRootRect),
|
|
1132
|
-
};
|
|
1133
|
-
});
|
|
1134
|
-
}
|