@zenuml/core 3.46.4 → 3.46.5

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 (70) hide show
  1. package/.claude/skills/babysit-pr/SKILL.md +203 -0
  2. package/.claude/skills/babysit-pr/agents/openai.yaml +7 -0
  3. package/.claude/skills/dia-scoring/SKILL.md +1 -1
  4. package/.claude/skills/propagate-core-release/SKILL.md +200 -0
  5. package/.claude/skills/propagate-core-release/agents/openai.yaml +7 -0
  6. package/.claude/skills/propagate-core-release/references/downstreams.md +41 -0
  7. package/dist/stats.html +1 -1
  8. package/dist/zenuml.esm.mjs +3 -3
  9. package/dist/zenuml.js +3 -3
  10. package/docs/superpowers/plans/2026-03-27-e2e-test-reorg.md +698 -0
  11. package/{cy → e2e/data}/compare-cases.js +70 -37
  12. package/{cy/smoke-editable-label.html → e2e/fixtures/editable-label.html} +1 -1
  13. package/{cy/editable-span-test.html → e2e/fixtures/editable-span.html} +1 -1
  14. package/e2e/fixtures/fixture.html +31 -0
  15. package/{cy → e2e/tools}/canonical-history.html +1 -1
  16. package/{cy → e2e/tools}/compare-case.html +3 -3
  17. package/{cy → e2e/tools}/compare.html +2 -2
  18. package/{cy → e2e/tools}/native-diff-ext/content.js +2 -2
  19. package/firebase-debug.log +108 -0
  20. package/index.html +2 -2
  21. package/mermaid-zenuml-async-spa-auth.png +0 -0
  22. package/mermaid-zenuml-async-spa-auth.snapshot.md +96 -0
  23. package/package.json +1 -1
  24. package/scripts/analyze-compare-case/collect-data.mjs +1 -1
  25. package/scripts/analyze-compare-case.mjs +1 -1
  26. package/skills/dia-scoring/SKILL.md +1 -1
  27. package/vite.config.ts +5 -5
  28. package/cy/async-message-1.html +0 -32
  29. package/cy/async-message-2.html +0 -46
  30. package/cy/async-message-3.html +0 -41
  31. package/cy/creation-rtl.html +0 -28
  32. package/cy/defect-406-alt-under-creation.html +0 -40
  33. package/cy/demo1.html +0 -28
  34. package/cy/demo3.html +0 -28
  35. package/cy/demo4.html +0 -28
  36. package/cy/element-report.html +0 -705
  37. package/cy/fragments-with-return.html +0 -35
  38. package/cy/icons-test.html +0 -29
  39. package/cy/if-fragment.html +0 -28
  40. package/cy/legacy-vs-html.html +0 -291
  41. package/cy/named-parameters.html +0 -30
  42. package/cy/nested-interaction-with-fragment.html +0 -34
  43. package/cy/nested-interaction-with-outbound.html +0 -34
  44. package/cy/parity-test.html +0 -122
  45. package/cy/return-in-nested-if.html +0 -29
  46. package/cy/return.html +0 -38
  47. package/cy/self-sync-message-at-root.html +0 -28
  48. package/cy/smoke-creation.html +0 -26
  49. package/cy/smoke-fragment-issue.html +0 -36
  50. package/cy/smoke-fragment.html +0 -42
  51. package/cy/smoke-interaction.html +0 -34
  52. package/cy/smoke.html +0 -40
  53. package/cy/theme-default-test.html +0 -28
  54. package/cy/vertical-1.html +0 -25
  55. package/cy/vertical-10.html +0 -33
  56. package/cy/vertical-11.html +0 -29
  57. package/cy/vertical-2.html +0 -23
  58. package/cy/vertical-3.html +0 -37
  59. package/cy/vertical-4.html +0 -42
  60. package/cy/vertical-5.html +0 -40
  61. package/cy/vertical-6.html +0 -29
  62. package/cy/vertical-7.html +0 -27
  63. package/cy/vertical-8.html +0 -32
  64. package/cy/vertical-9.html +0 -25
  65. package/cy/xss.html +0 -21
  66. /package/{cy → e2e/data}/diff-algorithm.js +0 -0
  67. /package/{cy → e2e/fixtures}/svg-test.html +0 -0
  68. /package/{cy → e2e/tools}/native-diff-ext/background.js +0 -0
  69. /package/{cy → e2e/tools}/native-diff-ext/bridge.js +0 -0
  70. /package/{cy → e2e/tools}/svg-preview.html +0 -0
@@ -1,705 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Element Measurement Report</title>
6
- <style>
7
- * { box-sizing: border-box; }
8
- body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #f1f5f9; }
9
- .header {
10
- padding: 12px 20px; background: #1e293b; color: white;
11
- display: flex; align-items: center; gap: 16px; position: sticky; top: 0; z-index: 100;
12
- }
13
- .header h1 { margin: 0; font-size: 16px; font-weight: 600; }
14
- .header button {
15
- padding: 6px 16px; background: #7c3aed; border: none; border-radius: 4px;
16
- color: white; font-size: 13px; cursor: pointer; font-weight: 500;
17
- }
18
- .header button:hover { background: #6d28d9; }
19
- .header button:disabled { background: #475569; cursor: wait; }
20
- .header .progress { font-size: 12px; color: #94a3b8; }
21
- .summary {
22
- margin: 12px 20px; padding: 12px 16px; background: white; border-radius: 6px;
23
- font-size: 13px; border: 1px solid #e2e8f0;
24
- }
25
- .summary table { width: 100%; border-collapse: collapse; font-size: 12px; }
26
- .summary th { text-align: left; padding: 4px 8px; border-bottom: 2px solid #e2e8f0; color: #64748b; font-weight: 600; }
27
- .summary td { padding: 4px 8px; border-bottom: 1px solid #f1f5f9; }
28
- .summary .case-link { color: #7c3aed; text-decoration: none; cursor: pointer; }
29
- .summary .case-link:hover { text-decoration: underline; }
30
- .case-card {
31
- margin: 12px 20px; background: white; border-radius: 6px;
32
- border: 1px solid #e2e8f0; overflow: hidden;
33
- }
34
- .case-header {
35
- padding: 8px 16px; background: #f8fafc; border-bottom: 1px solid #e2e8f0;
36
- display: flex; align-items: center; gap: 12px; cursor: pointer;
37
- }
38
- .case-header:hover { background: #f1f5f9; }
39
- .case-header h3 { margin: 0; font-size: 14px; font-weight: 600; }
40
- .case-header .badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }
41
- .case-header .badge.good { background: #dcfce7; color: #166534; }
42
- .case-header .badge.warn { background: #fef9c3; color: #854d0e; }
43
- .case-header .badge.bad { background: #fee2e2; color: #991b1b; }
44
- .case-body { display: none; }
45
- .case-body.open { display: block; }
46
- .diagrams {
47
- display: flex; gap: 0; border-bottom: 1px solid #e2e8f0;
48
- }
49
- .diagram-panel { flex: 1; overflow: auto; max-height: 400px; }
50
- .diagram-panel h4 {
51
- margin: 0; padding: 6px 12px; background: #f8fafc; font-size: 11px;
52
- color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
53
- border-bottom: 1px solid #e2e8f0; position: sticky; top: 0; z-index: 1;
54
- }
55
- .diagram-panel .content { padding: 8px; background: white; }
56
- .diagram-panel .content img { max-width: 100%; }
57
- .diagram-panel .content svg { max-width: 100%; height: auto; }
58
- .diagram-panel + .diagram-panel { border-left: 1px solid #e2e8f0; }
59
- .measure-table { width: 100%; border-collapse: collapse; font-size: 12px; }
60
- .measure-table th {
61
- text-align: left; padding: 4px 8px; border-bottom: 2px solid #e2e8f0;
62
- color: #64748b; font-weight: 600; font-size: 11px; position: sticky; top: 0;
63
- background: white;
64
- }
65
- .measure-table td { padding: 3px 8px; border-bottom: 1px solid #f8fafc; font-family: 'SF Mono', monospace; font-size: 11px; }
66
- .measure-table tr:hover { background: #f8fafc; }
67
- .measure-table .el-name { font-weight: 500; color: #334155; }
68
- .measure-table .delta { font-weight: 600; }
69
- .measure-table .delta.ok { color: #16a34a; }
70
- .measure-table .delta.warn { color: #ca8a04; }
71
- .measure-table .delta.bad { color: #dc2626; }
72
- .measure-table .section-row td { background: #f8fafc; font-weight: 600; color: #475569; padding-top: 8px; }
73
- #workspace { position: absolute; left: 0; top: 0; width: 900px; opacity: 0; pointer-events: none; z-index: -1; }
74
- </style>
75
- </head>
76
- <body>
77
- <div class="header">
78
- <h1>Element Measurement Report</h1>
79
- <button id="run-btn">Generate Report</button>
80
- <span class="progress" id="progress"></span>
81
- </div>
82
- <div id="summary"></div>
83
- <div id="report"></div>
84
- <div id="workspace"></div>
85
-
86
- <script type="module">
87
- import ZenUml from "/src/core.tsx";
88
- import { renderToSvg } from "/src/svg/renderToSvg.ts";
89
- import { toPng } from "html-to-image";
90
-
91
- const CASES = {
92
- "empty": ``,
93
- "smoke": `title ABCD Title\n// Generating Sequence Diagrams from Java code is experimental.\n// Please report errors to https://github.com/ZenUml/jetbrains-zenuml/discussions\nMarkdownJavaFxHtmlPanel\nMarkdownJavaFxHtmlPanel.readFromInputStream(inputStream) {\n StringBuilder resultStringBuilder = new StringBuilder();\n try {\n // String line;\n while((line = br.readLine()) != null) {\n resultStringBuilder.append(line);\n }\n }\n catch(IOException) {\n return "";\n }\n return "resultStringBuilder.toString()";\n}`,
94
- "simple-messages": `A -> B: hello\nB -> C: process\nC -> B: result\nB -> A: done`,
95
- "self-call": `A.method() {\n B.method\n}`,
96
- "if-fragment": `title Issue 232\nClient -> Server:SendRequest\nif(true){\n Server -> Server: processRequest\n}`,
97
- "fragment-loop": `A -> B: request\nloop(condition) {\n B -> C: process\n}`,
98
- "fragment-tcf": `A.method {\n try {\n B.process\n } catch(error) {\n C.handle\n } finally {\n D.cleanup\n }\n}`,
99
- "creation": `title Title 1\nA.m {\n new B(1,2,3,4)\n}`,
100
- "creation-rtl": `"b:B"\na1 = A.method() {\n // abcde\n b = new B()\n}`,
101
- "defect-406": `title Title 1\nA.m1 {\n new B(1,2,3,4) {\n if(x) {\n C.m2\n }\n while(y) {\n D.m3\n }\n par {\n E.m4\n F.m5\n }\n opt {\n G.m6\n }\n }\n}`,
102
- "fragment": `A\nB\nC\nif(x) {\n loop(y) {\n try {\n par {\n A.m();\n B.m();\n }\n } catch(e) {\n opt {\n new C\n }\n } finally {\n C.m\n }\n }\n}`,
103
- "fragment-issue": `// This sample is carefully crafted. It shows a known issues: fragment stretched to\n// svc (should not), because parser thinks the return statement returns to svc.\ngroup Backend {@VPC svc @RDS rep}\ngroup { Client }\nClient->SGW."Get order by id" {\n svc.Get(id) {\n rep."load order" {\n if(order == null) {\n @return\n SGW->Client:401\n }\n }\n }\n}`,
104
- "fragments-return": `A.method {\n if(x) {\n return x\n } else {\n return y\n }\n try {\n return 1\n } catch {\n return 2\n } finally {\n return 3\n }\n}`,
105
- "interaction": `if(x) {\n A.method() {\n B.method() {\n BSelfMethod00000000000\n A.method()\n }\n ASelf {\n B->A.method\n }\n }\n}`,
106
- "async-1": `A->A: async\nA->B: async\nA->C: async\nB->B: async\nB->C: async\nB->A: async\nC->C: async\nC->B: async\nC->A: async`,
107
- "async-2": `A.method {\n A->A: async\n A->B: async\n A->C: async\n B->B: async\n B->C: async\n B->A: async\n C->C: async\n C->B: async\n C->A: async\n B.method {\n A->A: async\n A->B: async\n A->C: async\n B->B: async\n B->C: async\n B->A: async\n C->C: async\n C->B: async\n C->A: async\n }\n}`,
108
- "async-3": `A B C\nC.method {\n A->C: async\n C->A: async\n C->B: async\n B->C: async\n B.method {\n A->A: async\n A->B: async\n A->C: async\n B->B: async\n B->C: async\n B->A: async\n C->C: async\n C->B: async\n C->A: async\n }\n}`,
109
- "return": `A B C D\nA->B.method() {\n ret0_assign_rtl =C.method_long_to_give_space {\n @return C->D: ret1_annotation_ltr\n ret5_assign_ltr = B.method\n B.method2 {\n return ret2_return_ltr\n }\n }\n return ret2_return_rtl\n @return B->A: ret4_annotation_rtl\n}`,
110
- "self-sync": `selfSync() {\n A.method {\n B.method\n }\n}`,
111
- "nested-fragment": `title Nested Interaction\nA.Read() {\n B.Submit() {\n Process() {\n if (true) {\n ProcessCallback() {\n A.method\n }\n }\n }\n }\n}`,
112
- "nested-outbound": `title Nested Interaction with Outbound\nA.Read() {\n B.Submit() {\n C->B.method {\n if (true) {\n ProcessCallback() {\n A.method\n }\n }\n }\n }\n}`,
113
- "named-params": `title Named Parameters Test\n// Testing named parameter syntax (param=value)\nA.method(userId=123, name="John")\nB.create(type="User", active=true)\nC.mixedCall(1, name="Mixed", enabled=false)\nD.oldStyle(1, 2, 3)\nE.complex(first="value1", second=42, third=true, fourth="final")`,
114
- "vertical-1": `// red\n// green\na = A.m111\nnew E`,
115
- "vertical-2": `// [red]\nnew B`,
116
- "vertical-3": `if(x) {\n // comment\n new A\n} else {\n new B\n}\nnew C\ntry {\n new D\n} catch {\n par {\n new E\n new F\n }\n}`,
117
- "vertical-4": `if(x) {\n // comment\n new A\n} else {\n new B\n}\nnew C\ntry {\n new D\n} catch {\n par {\n new E\n new F\n if(x) {\n new X\n } else {\n new Y\n }\n }\n}`,
118
- "vertical-5": `par {\n new F\n if(x) {\n new X\n } else {\n try {\n new Y\n } catch {\n par {\n new G\n if(x) {\n new H\n } else {\n new I\n }\n }\n }\n }\n}`,
119
- "vertical-6": `new a\nif(x) {\n\tnew b\n} else {\n\tnew c\n\tnew e\n}\nnew D`,
120
- "vertical-7": `A.method\nsection(){\n new B\n}`,
121
- "vertical-8": `new Creation() {\n return from_creation\n}\nreturn "from if to original source"\ntry {\n new AHasAVeryLongNameLongNameLongNameLongName() {\n new CreatWithinCreat()\n }\n}`,
122
- "vertical-9": `A0->A0: self\nnew A`,
123
- "vertical-10": `new E\nE.messageA()\nnew A {\n if (x) {\n new D\n }\n new B {\n new C\n }\n}`,
124
- "vertical-11": `A.call {\n // pre creation\n A->B: prep\n a = new A()\n a->B: post\n}`,
125
- "demo1-smoke": `// comments at the beginning should be ignored\ntitle This is a title\n@Lambda <<stereotype>> ParticipantName\ngroup "B C" {@EC2 B @ECS C}\n"bg color" #FF0000\n@Starter("OptionalStarter")\nnew B\nReturnType ret = ParticipantName.methodA(a, b) {\n critical("This is a critical message") {\n ReturnType ret2 = selfCall() {\n B.syncCallWithinSelfCall() {\n ParticipantName.rightToLeftCall()\n return B\n }\n "space in name"->"bg color".syncMethod(from, to)\n }\n }\n // A comment for alt\n if (condition) {\n // A comment for creation\n ret = new CreatAndAssign()\n "ret:CreatAndAssign".method(create, and, assign)\n // A comment for async self\n B->B: Self Async\n // A comment for async message\n B->C: Async Message within fragment\n new Creation() {\n return from_creation\n }\n return "from if to original source"\n try {\n new AHasAVeryLongNameLongNameLongNameLongName() {\n new CreatWithinCreat()\n C.rightToLeftFromCreation() {\n B.FurtherRightToLeftFromCreation()\n }\n }\n } catch (Exception) {\n self {\n return C\n }\n } finally {\n C: async call from implied source\n }\n =====divider can be anywhere=====\n } else if ("another condition") {\n par {\n B.method\n C.method\n }\n } else {\n // A comment for loop\n forEach(Z) {\n Z.method() {\n return Z\n }\n }\n }\n}`,
126
- "demo3-nested-fragments": `ret = A.methodA() {\n if (x) {\n B.methodB()\n if (y) {\n C.methodC()\n }\n }\n while (x) {\n B.methodB()\n while (y) {\n C.methodC()\n }\n }\n if (x) {\n method()\n if (y) {\n method2()\n }\n }\n while (x) {\n method()\n while (y) {\n method2()\n }\n }\n while (x) {\n method()\n if (y) {\n method2()\n }\n }\n if (x) {\n method()\n while (y) {\n method2()\n }\n }\n}`,
127
- "demo4-fragment-span": `ret = A.methodA() {\n B.method() {\n if (X) {\n C.methodC() {\n a = A.methodA() {\n D.method()\n }\n }\n }\n while (Y) {\n C.methodC() {\n A.methodA()\n }\n }\n }\n }`,
128
- "demo5-self-named": `A.methodA() { A.methodA1() }`,
129
- "demo6-async-styled": `A->A:: Hello\nA->B:: Hello B\nB->A: So what`,
130
- };
131
-
132
- // ——— Measurement functions ———
133
-
134
- /** Measure elements in the live HTML DOM, relative to the seq-diagram container */
135
- function measureHtmlElements(container) {
136
- const ref = container.getBoundingClientRect();
137
- const rel = (r) => ({
138
- x: Math.round((r.left - ref.left) * 10) / 10,
139
- y: Math.round((r.top - ref.top) * 10) / 10,
140
- w: Math.round(r.width * 10) / 10,
141
- h: Math.round(r.height * 10) / 10,
142
- });
143
-
144
- const m = { participants: [], occurrences: [], messages: [], selfCalls: [], fragments: [], returns: [], diagram: {} };
145
- m.diagram = { width: Math.round(ref.width * 10) / 10, height: Math.round(ref.height * 10) / 10 };
146
-
147
- // Participants
148
- container.querySelectorAll("[data-participant-id]").forEach((el) => {
149
- const r = rel(el.getBoundingClientRect());
150
- m.participants.push({
151
- name: el.getAttribute("data-participant-id"),
152
- centerX: Math.round((r.x + r.w / 2) * 10) / 10,
153
- topY: r.y, width: r.w, height: r.h,
154
- });
155
- });
156
-
157
- // Occurrences
158
- container.querySelectorAll('[data-el-type="occurrence"]').forEach((el, i) => {
159
- const r = rel(el.getBoundingClientRect());
160
- m.occurrences.push({
161
- participant: el.getAttribute("data-belongs-to") || `occ-${i}`,
162
- idx: i, x: r.x, y: r.y, width: r.w, height: r.h,
163
- });
164
- });
165
-
166
- // Messages (non-self sync/async interactions)
167
- container.querySelectorAll('.interaction:not(.return):not(.creation)').forEach((el, i) => {
168
- const isSelf = el.classList.contains("self-invocation") ||
169
- el.querySelector(".self-invocation") !== null ||
170
- (el.classList.contains("self"));
171
- if (isSelf) return; // handled separately
172
-
173
- const msgDiv = el.querySelector(".message");
174
- if (!msgDiv) return;
175
- const r = rel(msgDiv.getBoundingClientRect());
176
- const to = el.getAttribute("data-to") || "";
177
- const sig = el.getAttribute("data-signature") || "";
178
- m.messages.push({
179
- idx: i, label: sig || to,
180
- fromX: r.x, toX: r.x + r.w,
181
- y: r.y + r.h, // bottom of div = where the border-bottom line is
182
- width: r.w, height: r.h,
183
- });
184
- });
185
-
186
- // Self-calls
187
- container.querySelectorAll('.self-invocation').forEach((el, i) => {
188
- const arrowSvg = el.querySelector("svg.arrow") || el.querySelector("svg");
189
- if (!arrowSvg) return;
190
- const r = rel(arrowSvg.getBoundingClientRect());
191
- const interaction = el.closest(".interaction");
192
- const sig = interaction?.getAttribute("data-signature") || "";
193
- m.selfCalls.push({
194
- idx: i, label: sig,
195
- x: r.x, y: r.y, width: r.w, height: r.h,
196
- });
197
- });
198
-
199
- // Fragments
200
- container.querySelectorAll(".fragment").forEach((el, i) => {
201
- const r = rel(el.getBoundingClientRect());
202
- const kind = [...el.classList].find(c => c.startsWith("fragment-"))?.replace("fragment-", "") ||
203
- [...el.classList].find(c => ["alt", "loop", "opt", "par", "tcf", "critical", "section", "ref"].includes(c)) || "?";
204
- m.fragments.push({
205
- idx: i, kind, x: r.x, y: r.y, width: r.w, height: r.h,
206
- });
207
- });
208
-
209
- // Returns
210
- container.querySelectorAll('.interaction.return[data-type="return"]').forEach((el, i) => {
211
- const msgDiv = el.querySelector(".message");
212
- if (!msgDiv) return;
213
- const r = rel(msgDiv.getBoundingClientRect());
214
- const sig = el.getAttribute("data-signature") || "";
215
- m.returns.push({
216
- idx: i, label: sig,
217
- fromX: r.x, toX: r.x + r.w, y: r.y + r.h,
218
- });
219
- });
220
-
221
- return m;
222
- }
223
-
224
- /** Measure elements from the SVG string by parsing attributes */
225
- function measureSvgElements(svgString) {
226
- const parser = new DOMParser();
227
- const doc = parser.parseFromString(svgString, "image/svg+xml");
228
- const pad = 10; // <g transform="translate(10, 10)">
229
-
230
- const m = { participants: [], occurrences: [], messages: [], selfCalls: [], fragments: [], returns: [], diagram: {} };
231
-
232
- // Diagram size
233
- const svgEl = doc.querySelector("svg");
234
- if (svgEl) {
235
- m.diagram = {
236
- width: +svgEl.getAttribute("width") || 0,
237
- height: +svgEl.getAttribute("height") || 0,
238
- };
239
- }
240
-
241
- // Participants
242
- doc.querySelectorAll("g.participant, g.participant-starter").forEach((g) => {
243
- const rect = g.querySelector("rect.participant-box");
244
- const name = g.getAttribute("data-participant");
245
- if (!rect || !name) return;
246
- const x = +rect.getAttribute("x") + pad;
247
- const y = +rect.getAttribute("y") + pad;
248
- const w = +rect.getAttribute("width");
249
- const h = +rect.getAttribute("height");
250
- m.participants.push({
251
- name,
252
- centerX: Math.round((x + w / 2) * 10) / 10,
253
- topY: y, width: w, height: h,
254
- });
255
- });
256
-
257
- // Occurrences
258
- doc.querySelectorAll("rect.occurrence").forEach((rect, i) => {
259
- m.occurrences.push({
260
- participant: rect.getAttribute("data-participant") || `occ-${i}`,
261
- idx: i,
262
- x: +rect.getAttribute("x") + pad,
263
- y: +rect.getAttribute("y") + pad,
264
- width: +rect.getAttribute("width"),
265
- height: +rect.getAttribute("height"),
266
- });
267
- });
268
-
269
- // Messages (non-self)
270
- let msgIdx = 0;
271
- doc.querySelectorAll("g.message").forEach((g) => {
272
- if (g.classList.contains("self-call")) return;
273
- const line = g.querySelector("line.message-line");
274
- const text = g.querySelector("text.message-label");
275
- if (!line) return;
276
- const x1 = +line.getAttribute("x1") + pad;
277
- const x2 = +line.getAttribute("x2") + pad;
278
- const y = +line.getAttribute("y1") + pad;
279
- m.messages.push({
280
- idx: msgIdx++,
281
- label: text ? text.textContent.trim() : "",
282
- fromX: Math.min(x1, x2),
283
- toX: Math.max(x1, x2),
284
- y,
285
- width: Math.abs(x2 - x1),
286
- height: 0,
287
- });
288
- });
289
-
290
- // Self-calls
291
- let scIdx = 0;
292
- doc.querySelectorAll("g.message.self-call").forEach((g) => {
293
- const path = g.querySelector("path.message-line, path");
294
- const text = g.querySelector("text.message-label");
295
- if (!path) return;
296
- const d = path.getAttribute("d") || "";
297
- // Parse "M x1 y1 L ..." to get origin
298
- const mMatch = d.match(/M\s*([\d.]+)\s+([\d.]+)/);
299
- if (!mMatch) return;
300
- const x = +mMatch[1] + pad;
301
- const y = +mMatch[2] + pad;
302
- // Parse width from the L commands
303
- const lMatches = [...d.matchAll(/[LQ]\s*([\d.]+)\s+([\d.]+)/g)];
304
- let maxX = x, maxY = y;
305
- for (const lm of lMatches) {
306
- maxX = Math.max(maxX, +lm[1] + pad);
307
- maxY = Math.max(maxY, +lm[2] + pad);
308
- }
309
- m.selfCalls.push({
310
- idx: scIdx++,
311
- label: text ? text.textContent.trim() : "",
312
- x, y,
313
- width: Math.round((maxX - x) * 10) / 10,
314
- height: Math.round((maxY - y) * 10) / 10,
315
- });
316
- });
317
-
318
- // Fragments
319
- doc.querySelectorAll("g.fragment, g[class*='fragment-']").forEach((g, i) => {
320
- const rect = g.querySelector("rect.fragment-border");
321
- if (!rect) return;
322
- const kind = ([...g.classList].find(c => c.startsWith("fragment-")) || "").replace("fragment-", "");
323
- m.fragments.push({
324
- idx: i, kind,
325
- x: +rect.getAttribute("x") + pad,
326
- y: +rect.getAttribute("y") + pad,
327
- width: +rect.getAttribute("width"),
328
- height: +rect.getAttribute("height"),
329
- });
330
- });
331
-
332
- // Returns
333
- doc.querySelectorAll("g.return").forEach((g, i) => {
334
- const line = g.querySelector("line.return-line");
335
- const text = g.querySelector("text.return-label");
336
- if (!line) return;
337
- const x1 = +line.getAttribute("x1") + pad;
338
- const x2 = +line.getAttribute("x2") + pad;
339
- const y = +line.getAttribute("y1") + pad;
340
- m.returns.push({
341
- idx: i,
342
- label: text ? text.textContent.trim() : "",
343
- fromX: Math.min(x1, x2),
344
- toX: Math.max(x1, x2),
345
- y,
346
- });
347
- });
348
-
349
- return m;
350
- }
351
-
352
- /** Align measurements: normalize both to first non-starter participant as origin */
353
- function alignMeasurements(htmlM, svgM) {
354
- const htmlAnchor = htmlM.participants.find(p => p.name !== "_STARTER_");
355
- const svgAnchor = svgM.participants.find(p => p.name !== "_STARTER_");
356
- if (!htmlAnchor || !svgAnchor) return { dx: 0, dy: 0 };
357
- return {
358
- dx: htmlAnchor.centerX - svgAnchor.centerX,
359
- dy: htmlAnchor.topY - svgAnchor.topY,
360
- };
361
- }
362
-
363
- /** Compare matched elements and return rows for the table */
364
- function buildComparisonRows(htmlM, svgM) {
365
- const { dx, dy } = alignMeasurements(htmlM, svgM);
366
- const rows = [];
367
-
368
- function addSection(title) {
369
- rows.push({ section: title });
370
- }
371
-
372
- function addRow(name, prop, htmlVal, svgVal) {
373
- const svgAligned = svgVal + (prop.includes("X") || prop === "width" ? dx : prop.includes("Y") || prop === "y" || prop === "height" ? dy : 0);
374
- // For width/height, don't apply offset
375
- const delta = prop === "width" || prop === "height"
376
- ? Math.round((htmlVal - svgVal) * 10) / 10
377
- : Math.round((htmlVal - svgAligned) * 10) / 10;
378
- rows.push({
379
- name, prop,
380
- html: Math.round(htmlVal * 10) / 10,
381
- svg: Math.round(svgVal * 10) / 10,
382
- delta,
383
- absDelta: Math.abs(delta),
384
- });
385
- }
386
-
387
- // Diagram size
388
- addSection("Diagram");
389
- addRow("Diagram", "width", htmlM.diagram.width, svgM.diagram.width);
390
- addRow("Diagram", "height", htmlM.diagram.height, svgM.diagram.height);
391
-
392
- // Participants
393
- addSection("Participants");
394
- for (const hp of htmlM.participants) {
395
- const sp = svgM.participants.find(p => p.name === hp.name);
396
- if (!sp) {
397
- rows.push({ name: hp.name, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
398
- continue;
399
- }
400
- addRow(hp.name, "centerX", hp.centerX, sp.centerX);
401
- addRow(hp.name, "topY", hp.topY, sp.topY);
402
- addRow(hp.name, "width", hp.width, sp.width);
403
- addRow(hp.name, "height", hp.height, sp.height);
404
- }
405
- // SVG-only participants
406
- for (const sp of svgM.participants) {
407
- if (!htmlM.participants.find(p => p.name === sp.name)) {
408
- rows.push({ name: sp.name, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
409
- }
410
- }
411
-
412
- // Occurrences
413
- if (htmlM.occurrences.length > 0 || svgM.occurrences.length > 0) {
414
- addSection("Occurrences");
415
- const maxLen = Math.max(htmlM.occurrences.length, svgM.occurrences.length);
416
- for (let i = 0; i < maxLen; i++) {
417
- const ho = htmlM.occurrences[i];
418
- const so = svgM.occurrences[i];
419
- const label = ho?.participant || so?.participant || `#${i}`;
420
- if (ho && so) {
421
- addRow(`${label} #${i}`, "x", ho.x, so.x);
422
- addRow(`${label} #${i}`, "y", ho.y, so.y);
423
- addRow(`${label} #${i}`, "width", ho.width, so.width);
424
- addRow(`${label} #${i}`, "height", ho.height, so.height);
425
- } else if (ho) {
426
- rows.push({ name: `${label} #${i}`, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
427
- } else {
428
- rows.push({ name: `${label} #${i}`, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
429
- }
430
- }
431
- }
432
-
433
- // Messages
434
- if (htmlM.messages.length > 0 || svgM.messages.length > 0) {
435
- addSection("Messages");
436
- const maxLen = Math.max(htmlM.messages.length, svgM.messages.length);
437
- for (let i = 0; i < maxLen; i++) {
438
- const hm = htmlM.messages[i];
439
- const sm = svgM.messages[i];
440
- const label = hm?.label || sm?.label || `msg#${i}`;
441
- if (hm && sm) {
442
- addRow(label, "fromX", hm.fromX, sm.fromX);
443
- addRow(label, "toX", hm.toX, sm.toX);
444
- addRow(label, "y", hm.y, sm.y);
445
- } else if (hm) {
446
- rows.push({ name: label, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
447
- } else {
448
- rows.push({ name: label, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
449
- }
450
- }
451
- }
452
-
453
- // Self-calls
454
- if (htmlM.selfCalls.length > 0 || svgM.selfCalls.length > 0) {
455
- addSection("Self-Calls");
456
- const maxLen = Math.max(htmlM.selfCalls.length, svgM.selfCalls.length);
457
- for (let i = 0; i < maxLen; i++) {
458
- const hs = htmlM.selfCalls[i];
459
- const ss = svgM.selfCalls[i];
460
- const label = hs?.label || ss?.label || `self#${i}`;
461
- if (hs && ss) {
462
- addRow(label, "x", hs.x, ss.x);
463
- addRow(label, "y", hs.y, ss.y);
464
- addRow(label, "width", hs.width, ss.width);
465
- addRow(label, "height", hs.height, ss.height);
466
- } else if (hs) {
467
- rows.push({ name: label, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
468
- } else {
469
- rows.push({ name: label, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
470
- }
471
- }
472
- }
473
-
474
- // Fragments
475
- if (htmlM.fragments.length > 0 || svgM.fragments.length > 0) {
476
- addSection("Fragments");
477
- const maxLen = Math.max(htmlM.fragments.length, svgM.fragments.length);
478
- for (let i = 0; i < maxLen; i++) {
479
- const hf = htmlM.fragments[i];
480
- const sf = svgM.fragments[i];
481
- const label = `${hf?.kind || sf?.kind || "?"} #${i}`;
482
- if (hf && sf) {
483
- addRow(label, "x", hf.x, sf.x);
484
- addRow(label, "y", hf.y, sf.y);
485
- addRow(label, "width", hf.width, sf.width);
486
- addRow(label, "height", hf.height, sf.height);
487
- } else if (hf) {
488
- rows.push({ name: label, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
489
- } else {
490
- rows.push({ name: label, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
491
- }
492
- }
493
- }
494
-
495
- // Returns
496
- if (htmlM.returns.length > 0 || svgM.returns.length > 0) {
497
- addSection("Returns");
498
- const maxLen = Math.max(htmlM.returns.length, svgM.returns.length);
499
- for (let i = 0; i < maxLen; i++) {
500
- const hr = htmlM.returns[i];
501
- const sr = svgM.returns[i];
502
- const label = hr?.label || sr?.label || `ret#${i}`;
503
- if (hr && sr) {
504
- addRow(label, "fromX", hr.fromX, sr.fromX);
505
- addRow(label, "toX", hr.toX, sr.toX);
506
- addRow(label, "y", hr.y, sr.y);
507
- } else if (hr) {
508
- rows.push({ name: label, prop: "*", html: "present", svg: "MISSING", delta: "N/A", absDelta: 999 });
509
- } else {
510
- rows.push({ name: label, prop: "*", html: "MISSING", svg: "present", delta: "N/A", absDelta: 999 });
511
- }
512
- }
513
- }
514
-
515
- return rows;
516
- }
517
-
518
- // ——— Report rendering ———
519
-
520
- function renderTable(rows) {
521
- let html = `<table class="measure-table">
522
- <thead><tr>
523
- <th>Element</th><th>Property</th><th>HTML</th><th>SVG</th><th style="text-align:right">Delta</th>
524
- </tr></thead><tbody>`;
525
-
526
- for (const r of rows) {
527
- if (r.section) {
528
- html += `<tr class="section-row"><td colspan="5">${r.section}</td></tr>`;
529
- continue;
530
- }
531
- const cls = typeof r.absDelta === "number"
532
- ? r.absDelta <= 3 ? "ok" : r.absDelta <= 10 ? "warn" : "bad"
533
- : "bad";
534
- const deltaStr = typeof r.delta === "number" ? (r.delta > 0 ? `+${r.delta}` : `${r.delta}`) : r.delta;
535
- html += `<tr>
536
- <td class="el-name">${r.name}</td>
537
- <td>${r.prop}</td>
538
- <td>${r.html}</td>
539
- <td>${r.svg}</td>
540
- <td class="delta ${cls}" style="text-align:right">${deltaStr}</td>
541
- </tr>`;
542
- }
543
-
544
- html += `</tbody></table>`;
545
- return html;
546
- }
547
-
548
- // ——— Main processing ———
549
-
550
- async function processCase(caseName, code) {
551
- // 1. SVG
552
- const svgResult = renderToSvg(code);
553
- const svgM = measureSvgElements(svgResult.svg);
554
-
555
- // 2. HTML — render in workspace
556
- const workspace = document.getElementById("workspace");
557
- workspace.style.opacity = "1"; // must be visible for getBoundingClientRect
558
- workspace.innerHTML = '<div id="ws-diagram"><pre class="zenuml" style="margin:0"></pre></div>';
559
- const pre = workspace.querySelector("pre.zenuml");
560
- pre.textContent = code;
561
- const zu = new ZenUml(pre);
562
- await zu.render(code, { theme: "theme-default" });
563
-
564
- // Wait for layout
565
- await new Promise(r => setTimeout(r, 100));
566
- await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
567
-
568
- // Hide footer (SVG has no footer equivalent)
569
- const footer = workspace.querySelector(".footer");
570
- if (footer) footer.style.display = "none";
571
-
572
- // Find the sequence diagram container
573
- const seqDiagram = workspace.querySelector(".sequence-diagram") || workspace.querySelector(".seq-diagram") || workspace.querySelector("#ws-diagram");
574
-
575
- // 3. Measure HTML
576
- const htmlM = measureHtmlElements(seqDiagram);
577
-
578
- // 4. Capture HTML screenshot
579
- let htmlImageUrl = null;
580
- try {
581
- htmlImageUrl = await toPng(seqDiagram, { pixelRatio: 1 });
582
- } catch (e) {
583
- console.warn(`toPng failed for ${caseName}:`, e);
584
- }
585
-
586
- // 5. Cleanup
587
- workspace.innerHTML = "";
588
- workspace.style.opacity = "0";
589
-
590
- return { svgString: svgResult.svg, svgM, htmlM, htmlImageUrl };
591
- }
592
-
593
- async function generateReport() {
594
- const btn = document.getElementById("run-btn");
595
- const progress = document.getElementById("progress");
596
- const report = document.getElementById("report");
597
- const summaryDiv = document.getElementById("summary");
598
- btn.disabled = true;
599
- report.innerHTML = "";
600
- summaryDiv.innerHTML = "";
601
-
602
- const caseNames = Object.keys(CASES);
603
- const summaryRows = [];
604
-
605
- for (let i = 0; i < caseNames.length; i++) {
606
- const name = caseNames[i];
607
- progress.textContent = `Processing ${i + 1}/${caseNames.length}: ${name}...`;
608
-
609
- try {
610
- const result = await processCase(name, CASES[name]);
611
- const rows = buildComparisonRows(result.htmlM, result.svgM);
612
-
613
- // Stats for this case
614
- const dataRows = rows.filter(r => !r.section && typeof r.absDelta === "number");
615
- const avgDelta = dataRows.length > 0
616
- ? Math.round((dataRows.reduce((s, r) => s + r.absDelta, 0) / dataRows.length) * 10) / 10
617
- : 0;
618
- const maxDelta = dataRows.length > 0
619
- ? Math.max(...dataRows.map(r => r.absDelta))
620
- : 0;
621
- const badCount = dataRows.filter(r => r.absDelta > 10).length;
622
- const warnCount = dataRows.filter(r => r.absDelta > 3 && r.absDelta <= 10).length;
623
-
624
- summaryRows.push({ name, avgDelta, maxDelta, badCount, warnCount, totalProps: dataRows.length });
625
-
626
- // Build case card
627
- const card = document.createElement("div");
628
- card.className = "case-card";
629
- card.id = `case-${name}`;
630
-
631
- const badgeClass = maxDelta <= 3 ? "good" : maxDelta <= 10 ? "warn" : "bad";
632
- const badgeText = `avg ${avgDelta}px, max ${maxDelta}px`;
633
-
634
- card.innerHTML = `
635
- <div class="case-header" onclick="this.nextElementSibling.classList.toggle('open')">
636
- <h3>${name}</h3>
637
- <span class="badge ${badgeClass}">${badgeText}</span>
638
- <span style="margin-left:auto;color:#94a3b8;font-size:11px">${dataRows.length} properties measured</span>
639
- </div>
640
- <div class="case-body">
641
- <div class="diagrams">
642
- <div class="diagram-panel">
643
- <h4>HTML Renderer</h4>
644
- <div class="content">
645
- ${result.htmlImageUrl ? `<img src="${result.htmlImageUrl}" />` : "<em>Capture failed</em>"}
646
- </div>
647
- </div>
648
- <div class="diagram-panel">
649
- <h4>SVG Renderer</h4>
650
- <div class="content">${result.svgString}</div>
651
- </div>
652
- </div>
653
- <div style="padding:8px;max-height:500px;overflow:auto">
654
- ${renderTable(rows)}
655
- </div>
656
- </div>
657
- `;
658
- report.appendChild(card);
659
- } catch (e) {
660
- console.error(`Error processing ${name}:`, e);
661
- const card = document.createElement("div");
662
- card.className = "case-card";
663
- card.innerHTML = `<div class="case-header"><h3>${name}</h3><span class="badge bad">ERROR: ${e.message}</span></div>`;
664
- report.appendChild(card);
665
- summaryRows.push({ name, avgDelta: -1, maxDelta: -1, badCount: -1, warnCount: -1, totalProps: 0 });
666
- }
667
- }
668
-
669
- // Summary table
670
- const overallAvg = summaryRows.filter(r => r.avgDelta >= 0).length > 0
671
- ? Math.round(summaryRows.filter(r => r.avgDelta >= 0).reduce((s, r) => s + r.avgDelta, 0) / summaryRows.filter(r => r.avgDelta >= 0).length * 10) / 10
672
- : 0;
673
- const overallMax = Math.max(...summaryRows.filter(r => r.maxDelta >= 0).map(r => r.maxDelta), 0);
674
-
675
- summaryDiv.innerHTML = `<div class="summary">
676
- <div style="margin-bottom:8px"><strong>Overall:</strong> avg delta ${overallAvg}px, max delta ${overallMax}px across ${caseNames.length} cases</div>
677
- <table>
678
- <thead><tr>
679
- <th>Case</th><th>Properties</th><th style="text-align:right">Avg Δ</th><th style="text-align:right">Max Δ</th><th style="text-align:right">Bad (&gt;10px)</th><th style="text-align:right">Warn (&gt;3px)</th>
680
- </tr></thead>
681
- <tbody>
682
- ${summaryRows.sort((a, b) => b.maxDelta - a.maxDelta).map(r => {
683
- const cls = r.maxDelta <= 3 ? "ok" : r.maxDelta <= 10 ? "warn" : "bad";
684
- return `<tr>
685
- <td><a class="case-link" onclick="document.getElementById('case-${r.name}').querySelector('.case-body').classList.add('open'); document.getElementById('case-${r.name}').scrollIntoView({behavior:'smooth'})">${r.name}</a></td>
686
- <td>${r.totalProps}</td>
687
- <td class="delta ${cls}" style="text-align:right">${r.avgDelta >= 0 ? r.avgDelta : "ERR"}</td>
688
- <td class="delta ${cls}" style="text-align:right">${r.maxDelta >= 0 ? r.maxDelta : "ERR"}</td>
689
- <td style="text-align:right;color:${r.badCount > 0 ? '#dc2626' : '#16a34a'}">${r.badCount >= 0 ? r.badCount : "-"}</td>
690
- <td style="text-align:right;color:${r.warnCount > 0 ? '#ca8a04' : '#16a34a'}">${r.warnCount >= 0 ? r.warnCount : "-"}</td>
691
- </tr>`;
692
- }).join("")}
693
- </tbody>
694
- </table>
695
- </div>`;
696
-
697
- progress.textContent = `Done! ${caseNames.length} cases processed.`;
698
- btn.disabled = false;
699
- btn.textContent = "Regenerate";
700
- }
701
-
702
- document.getElementById("run-btn").addEventListener("click", generateReport);
703
- </script>
704
- </body>
705
- </html>