@zenuml/core 3.46.0 → 3.46.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/dia-scoring/SKILL.md +139 -0
- package/.claude/skills/dia-scoring/agents/openai.yaml +7 -0
- package/.claude/skills/dia-scoring/references/selectors-and-keys.md +253 -0
- package/.claude/skills/land-pr/SKILL.md +98 -0
- package/.claude/skills/ship-branch/SKILL.md +81 -0
- package/.claude/skills/submit-branch/SKILL.md +76 -0
- package/.claude/skills/validate-branch/SKILL.md +54 -0
- package/CLAUDE.md +1 -1
- package/bun.lock +25 -11
- package/cy/canonical-history.html +908 -0
- package/cy/compare-case.html +357 -0
- package/cy/compare-cases.js +824 -0
- package/cy/compare.html +35 -0
- package/cy/diff-algorithm.js +199 -0
- package/cy/element-report.html +705 -0
- package/cy/icons-test.html +29 -0
- package/cy/legacy-vs-html.html +291 -0
- package/cy/native-diff-ext/background.js +60 -0
- package/cy/native-diff-ext/bridge.js +26 -0
- package/cy/native-diff-ext/content.js +194 -0
- package/cy/parity-test.html +122 -0
- package/cy/return-in-nested-if.html +29 -0
- package/cy/svg-preview.html +56 -0
- package/cy/svg-test.html +21 -0
- package/cy/theme-default-test.html +28 -0
- package/dist/stats.html +1 -1
- package/dist/zenuml.esm.mjs +16352 -15223
- package/dist/zenuml.js +701 -575
- package/docs/ship-branch-skill-plan.md +134 -0
- package/docs/superpowers/plans/2026-03-23-svg-parity-features.md +283 -0
- package/index.html +568 -73
- package/package.json +15 -4
- package/scripts/analyze-compare-case/collect-data.mjs +991 -0
- package/scripts/analyze-compare-case/config.mjs +102 -0
- package/scripts/analyze-compare-case/geometry.mjs +101 -0
- package/scripts/analyze-compare-case/native-diff.mjs +224 -0
- package/scripts/analyze-compare-case/output.mjs +74 -0
- package/scripts/analyze-compare-case/panel-diff.mjs +114 -0
- package/scripts/analyze-compare-case/report.mjs +157 -0
- package/scripts/analyze-compare-case/residual-scopes.mjs +325 -0
- package/scripts/analyze-compare-case/scoring.mjs +816 -0
- package/scripts/analyze-compare-case.mjs +149 -0
- package/scripts/snapshot-dual.js +34 -34
- package/skills/dia-scoring/SKILL.md +129 -0
- package/skills/dia-scoring/agents/openai.yaml +7 -0
- package/skills/dia-scoring/references/selectors-and-keys.md +253 -0
- package/test-setup.ts +8 -0
- package/types/index.d.ts +56 -0
- package/vite.config.ts +4 -0
- package/dist/10029-icon-service-Function-Apps-ObflOLuF.js +0 -5
- package/dist/Res_AWS-Identity-Access-Management_IAM-Access-Analyzer_48-BPq60XMY.js +0 -11
- package/dist/Res_AWS-Lambda_Lambda-Function_48-Co38UB_2.js +0 -12
- package/dist/Res_Amazon-EC2_Instance_48-CRaqbNUl.js +0 -12
- package/dist/Res_Amazon-Simple-Notification-Service_Topic_48-q13mxUeM.js +0 -11
- package/dist/Res_Amazon-Simple-Queue-Service_Queue_48-D2-8gbFw.js +0 -11
- package/dist/Robustness_Diagram_Boundary-nYnmTPs8.js +0 -10
- package/dist/Robustness_Diagram_Control-DLNLoMxd.js +0 -11
- package/dist/Robustness_Diagram_Entity-Be3kcbIE.js +0 -11
- package/dist/actor-BMj_HFpo.js +0 -11
- package/dist/database-BKHQQWQK.js +0 -8
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
7
|
+
<title>Icons Test</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
}
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="diagram" class="diagram">
|
|
16
|
+
<pre class="zenuml" style="margin: 0">
|
|
17
|
+
@Actor User
|
|
18
|
+
@Database DB <<database>>
|
|
19
|
+
@Queue MQ <<sqs>>
|
|
20
|
+
|
|
21
|
+
User.login() {
|
|
22
|
+
DB.verify()
|
|
23
|
+
MQ.enqueue()
|
|
24
|
+
}
|
|
25
|
+
</pre>
|
|
26
|
+
</div>
|
|
27
|
+
<script type="module" src="/src/main.ts"></script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Legacy vs HTML Vertical Mode</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { margin: 0; font-family: system-ui, sans-serif; }
|
|
8
|
+
.page-header { padding: 8px 16px; background: #1e293b; color: white; display: flex; gap: 12px; align-items: center; }
|
|
9
|
+
.page-header h2 { margin: 0; font-size: 14px; font-weight: 600; }
|
|
10
|
+
.nav { display: flex; gap: 8px; margin-left: auto; align-items: center; }
|
|
11
|
+
.nav a { color: #93c5fd; text-decoration: none; font-size: 12px; padding: 4px 10px; background: #334155; border-radius: 4px; }
|
|
12
|
+
.nav a:hover { text-decoration: underline; }
|
|
13
|
+
.nav a.hidden { visibility: hidden; }
|
|
14
|
+
.container { display: flex; gap: 0; }
|
|
15
|
+
.panel { flex: 1; overflow: auto; border-right: 1px solid #e2e8f0; }
|
|
16
|
+
.panel:last-child { border-right: none; }
|
|
17
|
+
.panel-header { padding: 6px 12px; background: #334155; color: white; font-size: 13px; font-weight: 600; }
|
|
18
|
+
.panel-content { padding: 0; background: white; }
|
|
19
|
+
.panel-info { padding: 8px 12px; background: #f1f5f9; font-size: 12px; font-family: monospace; color: #475569; border-top: 1px solid #e2e8f0; }
|
|
20
|
+
.delta { padding: 8px 16px; font-size: 13px; font-family: monospace; display: none; }
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<div class="page-header">
|
|
25
|
+
<h2 id="case-name">Vertical Mode Comparison</h2>
|
|
26
|
+
<div class="nav">
|
|
27
|
+
<a id="prev-link" href="#">← Prev</a>
|
|
28
|
+
<a id="next-link" href="#">Next →</a>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div id="delta-bar" class="delta"></div>
|
|
32
|
+
<div class="container">
|
|
33
|
+
<div class="panel">
|
|
34
|
+
<div class="panel-header">HTML Mode (VerticalCoordinates)</div>
|
|
35
|
+
<div class="panel-content" id="html-output"></div>
|
|
36
|
+
<div class="panel-info" id="html-info">Loading...</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="panel">
|
|
39
|
+
<div class="panel-header">Legacy Mode (DOM Measurement)</div>
|
|
40
|
+
<div class="panel-content" id="legacy-output"></div>
|
|
41
|
+
<div class="panel-info" id="legacy-info">Loading...</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script type="module">
|
|
46
|
+
import ZenUml from "/src/core.tsx";
|
|
47
|
+
|
|
48
|
+
const CASES = {
|
|
49
|
+
"return-in-nested-if": `A.m {
|
|
50
|
+
if (condition) {
|
|
51
|
+
return ret
|
|
52
|
+
if(x) {
|
|
53
|
+
new B
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}`,
|
|
57
|
+
"vertical-1": `// red
|
|
58
|
+
// green
|
|
59
|
+
a = A.m111
|
|
60
|
+
new E`,
|
|
61
|
+
"vertical-2": `// [red]
|
|
62
|
+
new B`,
|
|
63
|
+
"vertical-3": `if(x) {
|
|
64
|
+
// comment
|
|
65
|
+
new A
|
|
66
|
+
} else {
|
|
67
|
+
new B
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
new C
|
|
71
|
+
try {
|
|
72
|
+
new D
|
|
73
|
+
} catch {
|
|
74
|
+
par {
|
|
75
|
+
new E
|
|
76
|
+
new F
|
|
77
|
+
}
|
|
78
|
+
}`,
|
|
79
|
+
"vertical-4": `if(x) {
|
|
80
|
+
// comment
|
|
81
|
+
new A
|
|
82
|
+
} else {
|
|
83
|
+
new B
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
new C
|
|
87
|
+
try {
|
|
88
|
+
new D
|
|
89
|
+
} catch {
|
|
90
|
+
par {
|
|
91
|
+
new E
|
|
92
|
+
new F
|
|
93
|
+
if(x) {
|
|
94
|
+
new X
|
|
95
|
+
} else {
|
|
96
|
+
new Y
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}`,
|
|
100
|
+
"vertical-5": `par {
|
|
101
|
+
new F
|
|
102
|
+
if(x) {
|
|
103
|
+
new X
|
|
104
|
+
} else {
|
|
105
|
+
try {
|
|
106
|
+
new Y
|
|
107
|
+
} catch {
|
|
108
|
+
par {
|
|
109
|
+
new G
|
|
110
|
+
if(x) {
|
|
111
|
+
new H
|
|
112
|
+
} else {
|
|
113
|
+
new I
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}`,
|
|
119
|
+
"vertical-6": `new a
|
|
120
|
+
if(x) {
|
|
121
|
+
new b
|
|
122
|
+
} else {
|
|
123
|
+
new c
|
|
124
|
+
new e
|
|
125
|
+
}
|
|
126
|
+
new D`,
|
|
127
|
+
"vertical-7": `A.method
|
|
128
|
+
section(){
|
|
129
|
+
new B
|
|
130
|
+
}`,
|
|
131
|
+
"vertical-8": `new Creation() {
|
|
132
|
+
return from_creation
|
|
133
|
+
}
|
|
134
|
+
return "from if to original source"
|
|
135
|
+
try {
|
|
136
|
+
new AHasAVeryLongNameLongNameLongNameLongName() {
|
|
137
|
+
new CreatWithinCreat()
|
|
138
|
+
}
|
|
139
|
+
}`,
|
|
140
|
+
"vertical-9": `A0->A0: self
|
|
141
|
+
new A`,
|
|
142
|
+
"vertical-10": `new E
|
|
143
|
+
E.messageA()
|
|
144
|
+
new A {
|
|
145
|
+
if (x) {
|
|
146
|
+
new D
|
|
147
|
+
}
|
|
148
|
+
new B {
|
|
149
|
+
new C
|
|
150
|
+
}
|
|
151
|
+
}`,
|
|
152
|
+
"vertical-11": `A.call {
|
|
153
|
+
// pre creation
|
|
154
|
+
A->B: prep
|
|
155
|
+
a = new A()
|
|
156
|
+
a->B: post
|
|
157
|
+
}`,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const caseNames = Object.keys(CASES);
|
|
161
|
+
const params = new URLSearchParams(window.location.search);
|
|
162
|
+
const caseName = params.get("case") || caseNames[0];
|
|
163
|
+
const code = CASES[caseName];
|
|
164
|
+
|
|
165
|
+
// Update title
|
|
166
|
+
document.title = `Legacy vs HTML: ${caseName}`;
|
|
167
|
+
document.getElementById("case-name").textContent = caseName;
|
|
168
|
+
|
|
169
|
+
// Prev/Next navigation
|
|
170
|
+
const idx = caseNames.indexOf(caseName);
|
|
171
|
+
const prevLink = document.getElementById("prev-link");
|
|
172
|
+
const nextLink = document.getElementById("next-link");
|
|
173
|
+
if (idx > 0) {
|
|
174
|
+
prevLink.href = `?case=${encodeURIComponent(caseNames[idx - 1])}`;
|
|
175
|
+
} else {
|
|
176
|
+
prevLink.classList.add("hidden");
|
|
177
|
+
}
|
|
178
|
+
if (idx < caseNames.length - 1) {
|
|
179
|
+
nextLink.href = `?case=${encodeURIComponent(caseNames[idx + 1])}`;
|
|
180
|
+
} else {
|
|
181
|
+
nextLink.classList.add("hidden");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (code === undefined) {
|
|
185
|
+
const errP = document.createElement("p");
|
|
186
|
+
errP.style.color = "red";
|
|
187
|
+
errP.textContent = "Unknown case: " + caseName;
|
|
188
|
+
document.getElementById("html-output").appendChild(errP);
|
|
189
|
+
} else {
|
|
190
|
+
function createPanel(containerId, mode) {
|
|
191
|
+
const container = document.getElementById(containerId);
|
|
192
|
+
const pre = document.createElement("pre");
|
|
193
|
+
pre.className = "zenuml";
|
|
194
|
+
pre.style.margin = "0";
|
|
195
|
+
pre.textContent = code;
|
|
196
|
+
container.appendChild(pre);
|
|
197
|
+
|
|
198
|
+
const zenUml = new ZenUml(pre);
|
|
199
|
+
zenUml.setVerticalMode(mode);
|
|
200
|
+
zenUml.render(code, { theme: "theme-default" });
|
|
201
|
+
return zenUml;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function measureCreationParticipants(containerId) {
|
|
205
|
+
const container = document.getElementById(containerId);
|
|
206
|
+
const diagram = container.querySelector(".sequence-diagram");
|
|
207
|
+
if (!diagram) return {};
|
|
208
|
+
|
|
209
|
+
// Hide footer
|
|
210
|
+
const footer = container.querySelector(".footer");
|
|
211
|
+
if (footer) footer.style.display = "none";
|
|
212
|
+
|
|
213
|
+
const dRect = diagram.getBoundingClientRect();
|
|
214
|
+
const results = {};
|
|
215
|
+
container.querySelectorAll("[data-participant-id]").forEach(el => {
|
|
216
|
+
const name = el.getAttribute("data-participant-id");
|
|
217
|
+
// Check if this is a creation participant (lifeline paddingTop > 20)
|
|
218
|
+
const lifeline = container.querySelector(`[id="${name}"]`);
|
|
219
|
+
const pt = lifeline ? parseFloat(lifeline.style.paddingTop) : 0;
|
|
220
|
+
if (pt > 20) {
|
|
221
|
+
const rect = el.getBoundingClientRect();
|
|
222
|
+
results[name] = Math.round(rect.top - dRect.top);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return results;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Render both panels
|
|
229
|
+
createPanel("html-output", "html");
|
|
230
|
+
createPanel("legacy-output", "legacy");
|
|
231
|
+
|
|
232
|
+
// Poll until both panels have .sequence-diagram, then measure
|
|
233
|
+
const poll = setInterval(() => {
|
|
234
|
+
const htmlDiag = document.querySelector('#html-output .sequence-diagram');
|
|
235
|
+
const legacyDiag = document.querySelector('#legacy-output .sequence-diagram');
|
|
236
|
+
if (!htmlDiag || !legacyDiag) return;
|
|
237
|
+
|
|
238
|
+
clearInterval(poll);
|
|
239
|
+
|
|
240
|
+
setTimeout(() => {
|
|
241
|
+
const htmlPositions = measureCreationParticipants("html-output");
|
|
242
|
+
const legacyPositions = measureCreationParticipants("legacy-output");
|
|
243
|
+
|
|
244
|
+
const allNames = new Set([...Object.keys(htmlPositions), ...Object.keys(legacyPositions)]);
|
|
245
|
+
|
|
246
|
+
if (allNames.size === 0) {
|
|
247
|
+
document.getElementById("html-info").textContent = "No creation participants";
|
|
248
|
+
document.getElementById("legacy-info").textContent = "No creation participants";
|
|
249
|
+
const bar = document.getElementById("delta-bar");
|
|
250
|
+
bar.style.display = "block";
|
|
251
|
+
bar.textContent = "No creation participants in this case";
|
|
252
|
+
bar.style.background = "#e2e8f0";
|
|
253
|
+
bar.style.color = "#475569";
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const htmlLines = [];
|
|
258
|
+
const legacyLines = [];
|
|
259
|
+
const deltaLines = [];
|
|
260
|
+
let allMatch = true;
|
|
261
|
+
|
|
262
|
+
for (const name of allNames) {
|
|
263
|
+
const h = htmlPositions[name];
|
|
264
|
+
const l = legacyPositions[name];
|
|
265
|
+
htmlLines.push(`${name}: ${h != null ? h + 'px' : 'n/a'}`);
|
|
266
|
+
legacyLines.push(`${name}: ${l != null ? l + 'px' : 'n/a'}`);
|
|
267
|
+
if (h != null && l != null) {
|
|
268
|
+
const delta = l - h;
|
|
269
|
+
if (delta !== 0) allMatch = false;
|
|
270
|
+
deltaLines.push(`${name}: ${delta === 0 ? 'identical' : delta + 'px'} (html=${h}, legacy=${l})`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
document.getElementById("html-info").textContent = htmlLines.join(' | ');
|
|
275
|
+
document.getElementById("legacy-info").textContent = legacyLines.join(' | ');
|
|
276
|
+
|
|
277
|
+
const bar = document.getElementById("delta-bar");
|
|
278
|
+
bar.style.display = "block";
|
|
279
|
+
bar.textContent = allMatch
|
|
280
|
+
? `All creation participants identical`
|
|
281
|
+
: deltaLines.join(' | ');
|
|
282
|
+
bar.style.background = allMatch ? "#d1fae5" : "#fef3c7";
|
|
283
|
+
bar.style.color = allMatch ? "#065f46" : "#92400e";
|
|
284
|
+
|
|
285
|
+
console.info("[Parity] %s: %s", caseName, allMatch ? "all identical" : deltaLines.join(", "));
|
|
286
|
+
}, 500);
|
|
287
|
+
}, 100);
|
|
288
|
+
}
|
|
289
|
+
</script>
|
|
290
|
+
</body>
|
|
291
|
+
</html>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Background service worker: uses chrome.debugger + CDP for element screenshots
|
|
2
|
+
|
|
3
|
+
chrome.action.onClicked.addListener(async (tab) => {
|
|
4
|
+
if (!tab.url || !tab.url.includes("compare-case.html")) return;
|
|
5
|
+
try {
|
|
6
|
+
const response = await chrome.tabs.sendMessage(tab.id, { action: "native-diff" });
|
|
7
|
+
console.log("[native-diff-ext bg]", response?.error || `Done: ${response?.pixelPct}%`);
|
|
8
|
+
} catch (err) {
|
|
9
|
+
console.error("[native-diff-ext bg] Failed:", err);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
14
|
+
if (msg.action === "cdp-screenshot") {
|
|
15
|
+
cdpScreenshot(sender.tab.id, msg.selector)
|
|
16
|
+
.then((dataUrl) => sendResponse({ dataUrl }))
|
|
17
|
+
.catch((err) => sendResponse({ error: err.message }));
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Take a single element screenshot
|
|
23
|
+
async function cdpScreenshot(tabId, selector) {
|
|
24
|
+
const target = { tabId };
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await chrome.debugger.attach(target, "1.3");
|
|
28
|
+
const { root } = await cdp(target, "DOM.getDocument", {});
|
|
29
|
+
return await screenshotNode(target, root.nodeId, selector);
|
|
30
|
+
} finally {
|
|
31
|
+
try { await chrome.debugger.detach(target); } catch (_) {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function screenshotNode(target, rootNodeId, selector) {
|
|
36
|
+
const { nodeId } = await cdp(target, "DOM.querySelector", {
|
|
37
|
+
nodeId: rootNodeId,
|
|
38
|
+
selector,
|
|
39
|
+
});
|
|
40
|
+
if (!nodeId) throw new Error(`Element not found: ${selector}`);
|
|
41
|
+
|
|
42
|
+
const { model } = await cdp(target, "DOM.getBoxModel", { nodeId });
|
|
43
|
+
const border = model.border;
|
|
44
|
+
const x = border[0];
|
|
45
|
+
const y = border[1];
|
|
46
|
+
const width = Math.ceil(border[2] - border[0]);
|
|
47
|
+
const height = Math.ceil(border[5] - border[1]);
|
|
48
|
+
|
|
49
|
+
const { data } = await cdp(target, "Page.captureScreenshot", {
|
|
50
|
+
format: "png",
|
|
51
|
+
clip: { x, y, width, height, scale: 1 },
|
|
52
|
+
captureBeyondViewport: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return "data:image/png;base64," + data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cdp(target, method, params) {
|
|
59
|
+
return chrome.debugger.sendCommand(target, method, params);
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Bridge script: runs in ISOLATED world
|
|
2
|
+
// Relays messages between content.js (MAIN world) and background.js (service worker)
|
|
3
|
+
|
|
4
|
+
// Content.js → bridge → background.js: screenshot requests
|
|
5
|
+
window.addEventListener("message", (event) => {
|
|
6
|
+
if (event.data && event.data.type === "native-diff-screenshot") {
|
|
7
|
+
console.log("[native-diff-ext bridge] Screenshot request:", event.data.selector);
|
|
8
|
+
chrome.runtime.sendMessage(
|
|
9
|
+
{ action: "cdp-screenshot", selector: event.data.selector },
|
|
10
|
+
(response) => {
|
|
11
|
+
window.postMessage({
|
|
12
|
+
type: "native-diff-screenshot-response",
|
|
13
|
+
dataUrl: response?.dataUrl,
|
|
14
|
+
error: response?.error,
|
|
15
|
+
}, "*");
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Icon click → content.js: trigger diff
|
|
22
|
+
chrome.runtime.onMessage.addListener((msg) => {
|
|
23
|
+
if (msg.action === "native-diff") {
|
|
24
|
+
window.postMessage({ type: "native-diff-trigger" }, "*");
|
|
25
|
+
}
|
|
26
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Content script: runs in MAIN world (has access to page's window functions)
|
|
2
|
+
// Uses CDP element screenshots via bridge.js → background.js
|
|
3
|
+
// Hides neighboring panels before each screenshot to prevent overlap.
|
|
4
|
+
//
|
|
5
|
+
// Diff algorithm lives in cy/diff-algorithm.js, loaded by compare-case.html
|
|
6
|
+
// and exposed as window.diffFromImages. This script only handles screenshots
|
|
7
|
+
// and orchestration.
|
|
8
|
+
|
|
9
|
+
// Auto-run on page load
|
|
10
|
+
window.addEventListener("load", () => {
|
|
11
|
+
console.log("[native-diff-ext] Page loaded, waiting 1s for renderers...");
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
console.log("[native-diff-ext] Starting native diff...");
|
|
14
|
+
runNativeDiff();
|
|
15
|
+
}, 1000);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Icon click trigger from bridge.js
|
|
19
|
+
window.addEventListener("message", (event) => {
|
|
20
|
+
if (event.data && event.data.type === "native-diff-trigger") {
|
|
21
|
+
console.log("[native-diff-ext] Triggered by icon click");
|
|
22
|
+
runNativeDiff();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Request a single CDP element screenshot via bridge
|
|
27
|
+
function screenshotOne(selector) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
function handler(event) {
|
|
30
|
+
if (event.data && event.data.type === "native-diff-screenshot-response") {
|
|
31
|
+
window.removeEventListener("message", handler);
|
|
32
|
+
resolve(event.data);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
window.addEventListener("message", handler);
|
|
36
|
+
window.postMessage({ type: "native-diff-screenshot", selector }, "*");
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runNativeDiff() {
|
|
41
|
+
if (typeof window.prepareHtmlForCapture !== "function") {
|
|
42
|
+
console.error("[native-diff-ext] prepareHtmlForCapture not found");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (typeof window.diffFromImages !== "function") {
|
|
46
|
+
console.error("[native-diff-ext] diffFromImages not found — compare-case.html must load diff-algorithm.js");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 1. Prepare: hide HTML chrome (same as skill's page.evaluate step)
|
|
51
|
+
console.log("[native-diff-ext] Preparing HTML for capture...");
|
|
52
|
+
window.prepareHtmlForCapture();
|
|
53
|
+
|
|
54
|
+
// 2. Determine selectors
|
|
55
|
+
const htmlSelector = document.querySelector("#html-output .frame")
|
|
56
|
+
? "#html-output .frame"
|
|
57
|
+
: "#html-output .sequence-diagram";
|
|
58
|
+
const svgSelector = "#svg-output > svg";
|
|
59
|
+
|
|
60
|
+
// 3. Screenshot HTML (CDP clip isolates the element — no need to hide siblings)
|
|
61
|
+
console.log("[native-diff-ext] Taking HTML screenshot...");
|
|
62
|
+
const htmlCapture = await screenshotOne(htmlSelector);
|
|
63
|
+
|
|
64
|
+
if (htmlCapture.error) {
|
|
65
|
+
console.error("[native-diff-ext] HTML screenshot failed:", htmlCapture.error);
|
|
66
|
+
window.restoreHtmlAfterCapture();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4. Screenshot SVG
|
|
71
|
+
console.log("[native-diff-ext] Taking SVG screenshot...");
|
|
72
|
+
const svgCapture = await screenshotOne(svgSelector);
|
|
73
|
+
|
|
74
|
+
// 5. Restore HTML chrome
|
|
75
|
+
window.restoreHtmlAfterCapture();
|
|
76
|
+
|
|
77
|
+
if (svgCapture.error) {
|
|
78
|
+
console.error("[native-diff-ext] SVG screenshot failed:", svgCapture.error);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log("[native-diff-ext] Screenshots captured. Running diff...");
|
|
83
|
+
|
|
84
|
+
// 6. Run diff via shared algorithm (exposed by compare-case.html)
|
|
85
|
+
const result = await window.diffFromImages(htmlCapture.dataUrl, svgCapture.dataUrl);
|
|
86
|
+
console.log("[native-diff-ext] Done!", result.pixelPct + "% pixel match", result.posPct + "% position-only match");
|
|
87
|
+
|
|
88
|
+
// Post result back for icon-click flow
|
|
89
|
+
window.postMessage({ type: "native-diff-result", result }, "*");
|
|
90
|
+
|
|
91
|
+
// Batch mode: if __cr_cases is set in localStorage, save result and auto-advance
|
|
92
|
+
try {
|
|
93
|
+
const batchCases = localStorage.getItem("__cr_cases");
|
|
94
|
+
if (batchCases) {
|
|
95
|
+
const cases = JSON.parse(batchCases);
|
|
96
|
+
const results = JSON.parse(localStorage.getItem("__cr_results") || "{}");
|
|
97
|
+
const currentCase = new URLSearchParams(window.location.search).get("case");
|
|
98
|
+
if (currentCase && !results[currentCase]) {
|
|
99
|
+
// Get the DSL for this case from the page (exposed by compare-case.html)
|
|
100
|
+
const dsl = window.__currentDSL || "";
|
|
101
|
+
|
|
102
|
+
results[currentCase] = { score: result.pixelPct, posScore: result.posPct, dsl };
|
|
103
|
+
localStorage.setItem("__cr_results", JSON.stringify(results));
|
|
104
|
+
const doneCount = Object.keys(results).length;
|
|
105
|
+
console.log(`[native-diff-ext] Batch: ${currentCase}=${result.pixelPct}% px / ${result.posPct}% pos (${doneCount}/${cases.length})`);
|
|
106
|
+
if (doneCount < cases.length) {
|
|
107
|
+
const idx = cases.indexOf(currentCase);
|
|
108
|
+
if (idx >= 0 && idx + 1 < cases.length) {
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
window.location.href = `/cy/compare-case.html?case=${cases[idx + 1]}`;
|
|
111
|
+
}, 200);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
const elapsed = ((Date.now() - parseInt(localStorage.getItem("__cr_start") || "0")) / 1000).toFixed(1);
|
|
115
|
+
console.log(`[native-diff-ext] Batch DONE in ${elapsed}s`);
|
|
116
|
+
// Save to IndexedDB for history tracking
|
|
117
|
+
saveBatchToHistory(results, elapsed);
|
|
118
|
+
// Clear batch vars so extension doesn't re-trigger on subsequent page loads
|
|
119
|
+
localStorage.removeItem("__cr_cases");
|
|
120
|
+
localStorage.removeItem("__cr_results");
|
|
121
|
+
localStorage.removeItem("__cr_start");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (e) { /* batch mode is optional, don't break normal flow */ }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- IndexedDB history storage ----
|
|
129
|
+
// Database: "canonical-history", store: "runs"
|
|
130
|
+
// Each run: { timestamp, elapsed, cases: { name: { score, dsl } }, average }
|
|
131
|
+
|
|
132
|
+
function openHistoryDB() {
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const req = indexedDB.open("canonical-history", 1);
|
|
135
|
+
req.onupgradeneeded = () => {
|
|
136
|
+
const db = req.result;
|
|
137
|
+
if (!db.objectStoreNames.contains("runs")) {
|
|
138
|
+
db.createObjectStore("runs", { keyPath: "timestamp" });
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
req.onsuccess = () => resolve(req.result);
|
|
142
|
+
req.onerror = () => reject(req.error);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function saveBatchToHistory(results, elapsed) {
|
|
147
|
+
try {
|
|
148
|
+
const db = await openHistoryDB();
|
|
149
|
+
const scores = Object.values(results).map(r => typeof r === "object" ? r.score : r);
|
|
150
|
+
const posScores = Object.values(results).map(r => {
|
|
151
|
+
if (typeof r === "object" && typeof r.posScore === "number") return r.posScore;
|
|
152
|
+
return typeof r === "object" ? r.score : r;
|
|
153
|
+
});
|
|
154
|
+
const average = scores.length > 0
|
|
155
|
+
? parseFloat((scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1))
|
|
156
|
+
: 0;
|
|
157
|
+
const averagePos = posScores.length > 0
|
|
158
|
+
? parseFloat((posScores.reduce((a, b) => a + b, 0) / posScores.length).toFixed(1))
|
|
159
|
+
: average;
|
|
160
|
+
|
|
161
|
+
const record = {
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
elapsed,
|
|
164
|
+
cases: results,
|
|
165
|
+
average,
|
|
166
|
+
averagePos,
|
|
167
|
+
caseCount: Object.keys(results).length,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const tx = db.transaction("runs", "readwrite");
|
|
171
|
+
tx.objectStore("runs").add(record);
|
|
172
|
+
await new Promise((resolve, reject) => {
|
|
173
|
+
tx.oncomplete = resolve;
|
|
174
|
+
tx.onerror = () => reject(tx.error);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
console.log(`[native-diff-ext] History saved: avg=${average}% px / ${averagePos}% pos, ${Object.keys(results).length} cases`);
|
|
178
|
+
db.close();
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.error("[native-diff-ext] Failed to save history:", e);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Expose history reader for dashboard pages
|
|
185
|
+
window.__getCanonicalHistory = async function() {
|
|
186
|
+
const db = await openHistoryDB();
|
|
187
|
+
const tx = db.transaction("runs", "readonly");
|
|
188
|
+
const store = tx.objectStore("runs");
|
|
189
|
+
return new Promise((resolve, reject) => {
|
|
190
|
+
const req = store.getAll();
|
|
191
|
+
req.onsuccess = () => { db.close(); resolve(req.result); };
|
|
192
|
+
req.onerror = () => { db.close(); reject(req.error); };
|
|
193
|
+
});
|
|
194
|
+
};
|