@zenuml/core 3.47.9 → 3.48.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.
Files changed (205) hide show
  1. package/dist/cli/zenuml.mjs +13529 -0
  2. package/dist/cli/zenuml.mjs.map +1 -0
  3. package/dist/cloud-icons-eHuugVSv.js.map +1 -0
  4. package/dist/zenuml.esm.mjs +2153 -2156
  5. package/dist/zenuml.esm.mjs.map +1 -0
  6. package/dist/zenuml.js +82 -82
  7. package/dist/zenuml.js.map +1 -0
  8. package/package.json +18 -5
  9. package/.agents/skills/babysit-pr/SKILL.md +0 -223
  10. package/.agents/skills/babysit-pr/agents/openai.yaml +0 -7
  11. package/.agents/skills/dia-scoring/SKILL.md +0 -139
  12. package/.agents/skills/dia-scoring/agents/openai.yaml +0 -7
  13. package/.agents/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  14. package/.agents/skills/land-pr/SKILL.md +0 -120
  15. package/.agents/skills/propagate-core-release/SKILL.md +0 -205
  16. package/.agents/skills/propagate-core-release/agents/openai.yaml +0 -7
  17. package/.agents/skills/propagate-core-release/references/downstreams.md +0 -42
  18. package/.agents/skills/ship-branch/SKILL.md +0 -105
  19. package/.agents/skills/submit-branch/SKILL.md +0 -76
  20. package/.agents/skills/validate-branch/SKILL.md +0 -72
  21. package/.claude/commands/README.md +0 -162
  22. package/.claude/commands/analyze.md +0 -101
  23. package/.claude/commands/clarify.md +0 -158
  24. package/.claude/commands/code-review.md +0 -322
  25. package/.claude/commands/constitution.md +0 -73
  26. package/.claude/commands/create-docs.md +0 -309
  27. package/.claude/commands/full-context.md +0 -121
  28. package/.claude/commands/gemini-consult.md +0 -164
  29. package/.claude/commands/handoff.md +0 -146
  30. package/.claude/commands/implement.md +0 -56
  31. package/.claude/commands/plan.md +0 -43
  32. package/.claude/commands/refactor.md +0 -188
  33. package/.claude/commands/specify.md +0 -21
  34. package/.claude/commands/tasks.md +0 -62
  35. package/.claude/commands/update-docs.md +0 -314
  36. package/.claude/hooks/README.md +0 -270
  37. package/.claude/hooks/config/sensitive-patterns.json +0 -86
  38. package/.claude/hooks/gemini-context-injector.sh +0 -129
  39. package/.claude/hooks/mcp-security-scan.sh +0 -147
  40. package/.claude/hooks/notify.sh +0 -103
  41. package/.claude/hooks/setup/hook-setup.md +0 -96
  42. package/.claude/hooks/setup/settings.json.template +0 -63
  43. package/.claude/hooks/sounds/complete.wav +0 -0
  44. package/.claude/hooks/sounds/input-needed.wav +0 -0
  45. package/.claude/hooks/subagent-context-injector.sh +0 -65
  46. package/.claude/skills/babysit-pr/SKILL.md +0 -223
  47. package/.claude/skills/babysit-pr/agents/openai.yaml +0 -7
  48. package/.claude/skills/dia-scoring/SKILL.md +0 -139
  49. package/.claude/skills/dia-scoring/agents/openai.yaml +0 -7
  50. package/.claude/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  51. package/.claude/skills/emoji-eval/SKILL.md +0 -187
  52. package/.claude/skills/land-pr/SKILL.md +0 -120
  53. package/.claude/skills/propagate-core-release/SKILL.md +0 -205
  54. package/.claude/skills/propagate-core-release/agents/openai.yaml +0 -7
  55. package/.claude/skills/propagate-core-release/references/downstreams.md +0 -42
  56. package/.claude/skills/ship-branch/SKILL.md +0 -105
  57. package/.claude/skills/submit-branch/SKILL.md +0 -76
  58. package/.claude/skills/validate-branch/SKILL.md +0 -72
  59. package/.claude/skills/zenuml-ux-research/SKILL.md +0 -183
  60. package/.claude/skills/zenuml-ux-research/references/assertion-catalog.md +0 -261
  61. package/.claude/skills/zenuml-ux-research/references/best-practices-overview.md +0 -56
  62. package/.claude/skills/zenuml-ux-research/references/report-template.md +0 -89
  63. package/.claude/skills/zenuml-ux-research/references/scenarios/edit-message-label.md +0 -37
  64. package/.claude/skills/zenuml-ux-research/references/scenarios/insert-message.md +0 -36
  65. package/.claude/skills/zenuml-ux-research/references/scenarios/insert-participant.md +0 -31
  66. package/.claude/skills/zenuml-ux-research/references/scenarios/rename-participant.md +0 -33
  67. package/.claude/skills/zenuml-ux-research/references/scenarios/undo-insert.md +0 -35
  68. package/.devcontainer/devcontainer.json +0 -21
  69. package/.dockerignore +0 -19
  70. package/.eslintrc.js +0 -39
  71. package/.git-blame-ignore-revs +0 -6
  72. package/.kiro/hooks/README.md +0 -38
  73. package/.kiro/hooks/session-sound-notification.js +0 -44
  74. package/.kiro/hooks/session-sound-notification.json +0 -23
  75. package/.mcp.json.example +0 -17
  76. package/.nvmrc +0 -1
  77. package/.prettierignore +0 -4
  78. package/.prettierrc +0 -1
  79. package/.specify/memory/constitution.md +0 -33
  80. package/.specify/scripts/bash/check-prerequisites.sh +0 -166
  81. package/.specify/scripts/bash/common.sh +0 -113
  82. package/.specify/scripts/bash/create-new-feature.sh +0 -97
  83. package/.specify/scripts/bash/setup-plan.sh +0 -60
  84. package/.specify/scripts/bash/update-agent-context.sh +0 -728
  85. package/.specify/templates/agent-file-template.md +0 -23
  86. package/.specify/templates/plan-template.md +0 -219
  87. package/.specify/templates/spec-template.md +0 -116
  88. package/.specify/templates/tasks-template.md +0 -127
  89. package/.storybook/main.ts +0 -25
  90. package/.storybook/preview.ts +0 -29
  91. package/.watchmanconfig +0 -3
  92. package/AGENTS.md +0 -26
  93. package/CLAUDE.md +0 -124
  94. package/DEPLOYMENT.md +0 -62
  95. package/Dockerfile +0 -36
  96. package/IMPLEMENTATION_PLAN.md +0 -163
  97. package/Integration/vanilla-js/index.html +0 -42
  98. package/MCP-ASSISTANT-RULES.md +0 -85
  99. package/README_CN.md +0 -15
  100. package/TUTORIAL.md +0 -116
  101. package/antlr/antlr-4.11.1-complete.jar +0 -0
  102. package/bun.lock +0 -1544
  103. package/bunfig.toml +0 -52
  104. package/docs/UNICODE_SUPPORT.md +0 -179
  105. package/docs/ai-context/deployment-infrastructure.md +0 -21
  106. package/docs/ai-context/docs-overview.md +0 -89
  107. package/docs/ai-context/handoff.md +0 -174
  108. package/docs/ai-context/project-structure.md +0 -160
  109. package/docs/ai-context/system-integration.md +0 -21
  110. package/docs/asciidoc/contributor.adoc +0 -54
  111. package/docs/asciidoc/create-my-own-theme.adoc +0 -149
  112. package/docs/asciidoc/images/creation-component.png +0 -0
  113. package/docs/asciidoc/images/creation-rtl.png +0 -0
  114. package/docs/asciidoc/images/message-arrow-rtl.png +0 -0
  115. package/docs/asciidoc/images/occurrence.png +0 -0
  116. package/docs/asciidoc/images/return-message-conflict.png +0 -0
  117. package/docs/asciidoc/images/shift-up-half-the-height.png +0 -0
  118. package/docs/asciidoc/images/three-layer-info-arch.png +0 -0
  119. package/docs/asciidoc/images/vertical-alignment.svg +0 -1
  120. package/docs/asciidoc/images/vertically-aligning.png +0 -0
  121. package/docs/asciidoc/index.adoc +0 -277
  122. package/docs/asciidoc/theme-debug-web-app.png +0 -0
  123. package/docs/asciidoc/tutorial.adoc +0 -22
  124. package/docs/asciidoc/user-css.png +0 -0
  125. package/docs/async-vs-sync-parser-rules.md +0 -81
  126. package/docs/divider-parser-allow-spaces.md +0 -38
  127. package/docs/highlighting-messages.md +0 -52
  128. package/docs/images/editor-sample.png +0 -0
  129. package/docs/inherited-vs-provided-from.md +0 -64
  130. package/docs/parser/Assignment.md +0 -8
  131. package/docs/parser/PARSER_IMPROVEMENTS_CC.md +0 -425
  132. package/docs/parser/grammar_review_gemini.md +0 -116
  133. package/docs/participants-function.md +0 -25
  134. package/docs/responsive-participant-margin.md +0 -52
  135. package/docs/starter.md +0 -9
  136. package/docs/superpowers/plans/2026-03-27-e2e-test-reorg.md +0 -698
  137. package/docs/superpowers/plans/2026-03-30-emoji-support.md +0 -1220
  138. package/docs/superpowers/plans/2026-03-30-self-correcting-scoring.md +0 -206
  139. package/docs/superpowers/plans/2026-04-15-keyboard-editing-on-diagram.md +0 -1992
  140. package/docs/superpowers/plans/2026-04-15-zenuml-ux-research-skill.md +0 -1452
  141. package/docs/ux-research/.gitkeep +0 -0
  142. package/docs/ux-research/2026-04-15-rename-participant.md +0 -156
  143. package/docs/ux-research/2026-04-18-insert-participant.md +0 -151
  144. package/docs/width-translate-and-offsets.md +0 -62
  145. package/docs/xss.md +0 -59
  146. package/e2e/data/compare-cases.js +0 -1090
  147. package/e2e/data/diff-algorithm.js +0 -199
  148. package/e2e/fixtures/create-message.html +0 -26
  149. package/e2e/fixtures/editable-label.html +0 -35
  150. package/e2e/fixtures/editable-span.html +0 -122
  151. package/e2e/fixtures/empty-diagram.html +0 -23
  152. package/e2e/fixtures/fixture.html +0 -31
  153. package/e2e/fixtures/insert-participant.html +0 -23
  154. package/e2e/fixtures/reorder-cross-fragment.html +0 -31
  155. package/e2e/fixtures/reorder-fragment.html +0 -29
  156. package/e2e/fixtures/reorder-message.html +0 -27
  157. package/e2e/fixtures/svg-test.html +0 -21
  158. package/e2e/fixtures/type-switch.html +0 -29
  159. package/e2e/tools/canonical-history.html +0 -908
  160. package/e2e/tools/compare-case.html +0 -371
  161. package/e2e/tools/compare.html +0 -35
  162. package/e2e/tools/native-diff-ext/background.js +0 -60
  163. package/e2e/tools/native-diff-ext/bridge.js +0 -26
  164. package/e2e/tools/native-diff-ext/content.js +0 -194
  165. package/e2e/tools/svg-preview.html +0 -56
  166. package/embed.html +0 -193
  167. package/eslint.config.mjs +0 -35
  168. package/firebase-debug.log +0 -108
  169. package/iframe-container-demo/diagram.html +0 -124
  170. package/iframe-container-demo/host.html +0 -817
  171. package/index.html +0 -771
  172. package/mermaid-zenuml-async-spa-auth.png +0 -0
  173. package/mermaid-zenuml-async-spa-auth.snapshot.md +0 -96
  174. package/newsletter/unicode-support-announcement.md +0 -134
  175. package/playground/creation.html +0 -53
  176. package/playground/message.html +0 -63
  177. package/playwright.config.ts +0 -40
  178. package/renderer.html +0 -366
  179. package/scripts/analyze-compare-case/collect-data.mjs +0 -1134
  180. package/scripts/analyze-compare-case/config.mjs +0 -102
  181. package/scripts/analyze-compare-case/geometry.mjs +0 -101
  182. package/scripts/analyze-compare-case/native-diff.mjs +0 -224
  183. package/scripts/analyze-compare-case/output.mjs +0 -74
  184. package/scripts/analyze-compare-case/panel-diff.mjs +0 -114
  185. package/scripts/analyze-compare-case/report.mjs +0 -162
  186. package/scripts/analyze-compare-case/residual-scopes.mjs +0 -347
  187. package/scripts/analyze-compare-case/scoring.mjs +0 -829
  188. package/scripts/analyze-compare-case.mjs +0 -149
  189. package/scripts/bump-version.js +0 -117
  190. package/scripts/snapshot-dual.js +0 -173
  191. package/scripts/update-snapshots.js +0 -70
  192. package/skills/dia-scoring/SKILL.md +0 -129
  193. package/skills/dia-scoring/agents/openai.yaml +0 -7
  194. package/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  195. package/tailwind.config.js +0 -126
  196. package/test-compression.html +0 -274
  197. package/test-mermaid-zenuml.html +0 -57
  198. package/test-setup.ts +0 -124
  199. package/test-url-params.html +0 -192
  200. package/tsconfig.app.json +0 -31
  201. package/tsconfig.node.json +0 -24
  202. package/tsconfig.test.json +0 -9
  203. package/vite.config.lib.ts +0 -93
  204. package/vite.config.ts +0 -84
  205. 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
- }