page-analyzer 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -9
- package/index.js +206 -22
- package/llm/analyzers/event-analyzer/event-analyzer-blocks.js +23 -2
- package/llm/analyzers/event-analyzer/event-analyzer-constants.js +1 -1
- package/llm/analyzers/event-analyzer/event-analyzer.js +1 -1
- package/package.json +6 -3
- package/page-extractor.js +562 -36
- package/result-viewer.html +1064 -0
- package/scripts/analyze.js +51 -0
- package/scripts/build-result-viewer.js +1076 -0
- package/scripts/serve-result-viewer.js +68 -0
- package/test/smoke.test.js +454 -0
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
7
|
+
const outputPath = path.join(projectRoot, 'result-viewer.html');
|
|
8
|
+
|
|
9
|
+
const html = `<!doctype html>
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="utf-8">
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
14
|
+
<link rel="icon" href="data:,">
|
|
15
|
+
<title>Page Analyzer Result Viewer</title>
|
|
16
|
+
<style>
|
|
17
|
+
:root {
|
|
18
|
+
--bg: #f4f0e8;
|
|
19
|
+
--paper: #fffaf0;
|
|
20
|
+
--ink: #171511;
|
|
21
|
+
--muted: #696255;
|
|
22
|
+
--line: #d5cbb9;
|
|
23
|
+
--accent: #0f766e;
|
|
24
|
+
--accent-2: #b45309;
|
|
25
|
+
--accent-3: #7c2d12;
|
|
26
|
+
--panel: #ede2d0;
|
|
27
|
+
--shadow: rgba(45, 35, 20, 0.14);
|
|
28
|
+
--missing: #8f1d1d;
|
|
29
|
+
--ok: #126b48;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
* {
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
body {
|
|
37
|
+
margin: 0;
|
|
38
|
+
min-width: 320px;
|
|
39
|
+
color: var(--ink);
|
|
40
|
+
background:
|
|
41
|
+
linear-gradient(90deg, rgba(15, 118, 110, 0.06) 0 1px, transparent 1px 100%),
|
|
42
|
+
linear-gradient(180deg, rgba(180, 83, 9, 0.05) 0 1px, transparent 1px 100%),
|
|
43
|
+
var(--bg);
|
|
44
|
+
background-size: 44px 44px;
|
|
45
|
+
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
|
46
|
+
letter-spacing: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
button,
|
|
50
|
+
input {
|
|
51
|
+
font: inherit;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.shell {
|
|
55
|
+
min-height: 100vh;
|
|
56
|
+
display: grid;
|
|
57
|
+
grid-template-columns: minmax(270px, 340px) minmax(0, 1fr);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sidebar {
|
|
61
|
+
position: sticky;
|
|
62
|
+
top: 0;
|
|
63
|
+
height: 100vh;
|
|
64
|
+
overflow: auto;
|
|
65
|
+
border-right: 1px solid var(--line);
|
|
66
|
+
background: rgba(255, 250, 240, 0.88);
|
|
67
|
+
backdrop-filter: blur(14px);
|
|
68
|
+
padding: 22px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.brand {
|
|
72
|
+
margin-bottom: 18px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.eyebrow {
|
|
76
|
+
color: var(--accent-2);
|
|
77
|
+
font-size: 12px;
|
|
78
|
+
font-weight: 800;
|
|
79
|
+
text-transform: uppercase;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
h1,
|
|
83
|
+
h2,
|
|
84
|
+
h3 {
|
|
85
|
+
margin: 0;
|
|
86
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
87
|
+
font-weight: 700;
|
|
88
|
+
letter-spacing: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
h1 {
|
|
92
|
+
margin-top: 6px;
|
|
93
|
+
font-size: 28px;
|
|
94
|
+
line-height: 1.06;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.title {
|
|
98
|
+
margin-top: 10px;
|
|
99
|
+
color: var(--muted);
|
|
100
|
+
font-size: 13px;
|
|
101
|
+
line-height: 1.45;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.load-state {
|
|
105
|
+
margin-top: 12px;
|
|
106
|
+
border: 1px solid var(--line);
|
|
107
|
+
background: #fffdf7;
|
|
108
|
+
color: var(--muted);
|
|
109
|
+
padding: 10px;
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
line-height: 1.45;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.load-state.error {
|
|
115
|
+
border-color: rgba(143, 29, 29, 0.28);
|
|
116
|
+
background: rgba(143, 29, 29, 0.08);
|
|
117
|
+
color: var(--missing);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.url-loader,
|
|
121
|
+
.file-loader {
|
|
122
|
+
display: grid;
|
|
123
|
+
gap: 8px;
|
|
124
|
+
margin-top: 8px;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.url-loader label,
|
|
128
|
+
.file-loader span {
|
|
129
|
+
color: var(--muted);
|
|
130
|
+
font-size: 11px;
|
|
131
|
+
font-weight: 800;
|
|
132
|
+
text-transform: uppercase;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.url-row {
|
|
136
|
+
display: grid;
|
|
137
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
138
|
+
gap: 8px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.url-loader input,
|
|
142
|
+
.file-loader input {
|
|
143
|
+
width: 100%;
|
|
144
|
+
border: 1px solid var(--line);
|
|
145
|
+
background: #fffdf7;
|
|
146
|
+
padding: 8px;
|
|
147
|
+
color: var(--ink);
|
|
148
|
+
font-size: 12px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.url-loader button {
|
|
152
|
+
min-width: 68px;
|
|
153
|
+
border: 1px solid var(--ink);
|
|
154
|
+
background: var(--ink);
|
|
155
|
+
color: white;
|
|
156
|
+
padding: 0 12px;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
font-size: 12px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.metrics {
|
|
162
|
+
display: grid;
|
|
163
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
164
|
+
gap: 8px;
|
|
165
|
+
margin: 18px 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.metric {
|
|
169
|
+
min-height: 64px;
|
|
170
|
+
border: 1px solid var(--line);
|
|
171
|
+
background: var(--paper);
|
|
172
|
+
padding: 10px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.metric strong {
|
|
176
|
+
display: block;
|
|
177
|
+
font-size: 22px;
|
|
178
|
+
line-height: 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.metric span {
|
|
182
|
+
display: block;
|
|
183
|
+
margin-top: 6px;
|
|
184
|
+
color: var(--muted);
|
|
185
|
+
font-size: 12px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.toolbar {
|
|
189
|
+
display: grid;
|
|
190
|
+
gap: 10px;
|
|
191
|
+
margin: 12px 0 14px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.search {
|
|
195
|
+
width: 100%;
|
|
196
|
+
height: 38px;
|
|
197
|
+
border: 1px solid var(--line);
|
|
198
|
+
border-radius: 0;
|
|
199
|
+
background: #fffdf7;
|
|
200
|
+
color: var(--ink);
|
|
201
|
+
padding: 0 10px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.filters {
|
|
205
|
+
display: grid;
|
|
206
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
207
|
+
border: 1px solid var(--line);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.filter {
|
|
211
|
+
height: 34px;
|
|
212
|
+
border: 0;
|
|
213
|
+
border-right: 1px solid var(--line);
|
|
214
|
+
background: #fffdf7;
|
|
215
|
+
color: var(--muted);
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.filter:last-child {
|
|
220
|
+
border-right: 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.filter[aria-pressed="true"] {
|
|
224
|
+
background: var(--accent);
|
|
225
|
+
color: white;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.block-list {
|
|
229
|
+
display: grid;
|
|
230
|
+
gap: 8px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.block-tab {
|
|
234
|
+
width: 100%;
|
|
235
|
+
min-height: 72px;
|
|
236
|
+
border: 1px solid var(--line);
|
|
237
|
+
background: rgba(255, 253, 247, 0.9);
|
|
238
|
+
color: var(--ink);
|
|
239
|
+
text-align: left;
|
|
240
|
+
padding: 10px;
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.block-tab:hover,
|
|
245
|
+
.block-tab.active {
|
|
246
|
+
border-color: var(--accent);
|
|
247
|
+
box-shadow: inset 4px 0 0 var(--accent);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.tab-top {
|
|
251
|
+
display: flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
justify-content: space-between;
|
|
254
|
+
gap: 8px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.tab-name {
|
|
258
|
+
overflow: hidden;
|
|
259
|
+
text-overflow: ellipsis;
|
|
260
|
+
white-space: nowrap;
|
|
261
|
+
font-weight: 800;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.tab-index {
|
|
265
|
+
flex: 0 0 auto;
|
|
266
|
+
color: var(--muted);
|
|
267
|
+
font-family: "SFMono-Regular", Consolas, monospace;
|
|
268
|
+
font-size: 12px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.tab-desc {
|
|
272
|
+
display: -webkit-box;
|
|
273
|
+
margin-top: 6px;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
color: var(--muted);
|
|
276
|
+
font-size: 12px;
|
|
277
|
+
line-height: 1.35;
|
|
278
|
+
-webkit-line-clamp: 2;
|
|
279
|
+
-webkit-box-orient: vertical;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.content {
|
|
283
|
+
min-width: 0;
|
|
284
|
+
padding: 28px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.summary-row {
|
|
288
|
+
display: flex;
|
|
289
|
+
align-items: flex-start;
|
|
290
|
+
justify-content: space-between;
|
|
291
|
+
gap: 18px;
|
|
292
|
+
margin-bottom: 18px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.summary-copy {
|
|
296
|
+
max-width: 940px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.summary-copy h2 {
|
|
300
|
+
font-size: 34px;
|
|
301
|
+
line-height: 1.08;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.summary-copy p {
|
|
305
|
+
margin: 10px 0 0;
|
|
306
|
+
color: var(--muted);
|
|
307
|
+
line-height: 1.55;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.actions {
|
|
311
|
+
display: flex;
|
|
312
|
+
gap: 8px;
|
|
313
|
+
flex-wrap: wrap;
|
|
314
|
+
justify-content: flex-end;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.action {
|
|
318
|
+
min-height: 36px;
|
|
319
|
+
border: 1px solid var(--ink);
|
|
320
|
+
background: var(--ink);
|
|
321
|
+
color: white;
|
|
322
|
+
padding: 0 12px;
|
|
323
|
+
cursor: pointer;
|
|
324
|
+
text-decoration: none;
|
|
325
|
+
display: inline-flex;
|
|
326
|
+
align-items: center;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.action.secondary {
|
|
330
|
+
background: transparent;
|
|
331
|
+
color: var(--ink);
|
|
332
|
+
border-color: var(--line);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.detail {
|
|
336
|
+
display: grid;
|
|
337
|
+
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
|
|
338
|
+
gap: 18px;
|
|
339
|
+
align-items: start;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.panel {
|
|
343
|
+
border: 1px solid var(--line);
|
|
344
|
+
background: rgba(255, 250, 240, 0.9);
|
|
345
|
+
box-shadow: 0 18px 40px var(--shadow);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.panel-head {
|
|
349
|
+
border-bottom: 1px solid var(--line);
|
|
350
|
+
padding: 16px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.panel-head h3 {
|
|
354
|
+
font-size: 22px;
|
|
355
|
+
line-height: 1.1;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.panel-body {
|
|
359
|
+
padding: 16px;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.shot-box {
|
|
363
|
+
display: grid;
|
|
364
|
+
gap: 10px;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.screenshot-frame {
|
|
368
|
+
min-height: 260px;
|
|
369
|
+
max-height: 620px;
|
|
370
|
+
overflow: auto;
|
|
371
|
+
border: 1px solid var(--line);
|
|
372
|
+
background:
|
|
373
|
+
linear-gradient(45deg, rgba(23, 21, 17, 0.05) 25%, transparent 25% 75%, rgba(23, 21, 17, 0.05) 75%),
|
|
374
|
+
linear-gradient(45deg, rgba(23, 21, 17, 0.05) 25%, transparent 25% 75%, rgba(23, 21, 17, 0.05) 75%),
|
|
375
|
+
#fffdf7;
|
|
376
|
+
background-position: 0 0, 10px 10px;
|
|
377
|
+
background-size: 20px 20px;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.screenshot-frame img {
|
|
381
|
+
display: block;
|
|
382
|
+
width: 100%;
|
|
383
|
+
height: auto;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.missing-shot {
|
|
387
|
+
min-height: 260px;
|
|
388
|
+
display: grid;
|
|
389
|
+
place-items: center;
|
|
390
|
+
color: var(--missing);
|
|
391
|
+
text-align: center;
|
|
392
|
+
padding: 22px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.info-grid {
|
|
396
|
+
display: grid;
|
|
397
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
398
|
+
gap: 10px;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.info {
|
|
402
|
+
border: 1px solid var(--line);
|
|
403
|
+
background: #fffdf7;
|
|
404
|
+
padding: 10px;
|
|
405
|
+
min-height: 58px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.info label {
|
|
409
|
+
display: block;
|
|
410
|
+
color: var(--muted);
|
|
411
|
+
font-size: 11px;
|
|
412
|
+
font-weight: 800;
|
|
413
|
+
text-transform: uppercase;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.info span {
|
|
417
|
+
display: block;
|
|
418
|
+
margin-top: 5px;
|
|
419
|
+
overflow-wrap: anywhere;
|
|
420
|
+
font-size: 13px;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.wide {
|
|
424
|
+
grid-column: 1 / -1;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.chips {
|
|
428
|
+
display: flex;
|
|
429
|
+
gap: 6px;
|
|
430
|
+
flex-wrap: wrap;
|
|
431
|
+
margin-top: 10px;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.chip {
|
|
435
|
+
border: 1px solid var(--line);
|
|
436
|
+
background: var(--panel);
|
|
437
|
+
color: var(--ink);
|
|
438
|
+
padding: 4px 7px;
|
|
439
|
+
font-size: 12px;
|
|
440
|
+
line-height: 1.2;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.chip.ok {
|
|
444
|
+
border-color: rgba(18, 107, 72, 0.35);
|
|
445
|
+
background: rgba(18, 107, 72, 0.1);
|
|
446
|
+
color: var(--ok);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.chip.warn {
|
|
450
|
+
border-color: rgba(143, 29, 29, 0.28);
|
|
451
|
+
background: rgba(143, 29, 29, 0.08);
|
|
452
|
+
color: var(--missing);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
pre {
|
|
456
|
+
max-height: 420px;
|
|
457
|
+
margin: 0;
|
|
458
|
+
overflow: auto;
|
|
459
|
+
border: 1px solid var(--line);
|
|
460
|
+
background: #171511;
|
|
461
|
+
color: #f7eddc;
|
|
462
|
+
padding: 12px;
|
|
463
|
+
font-family: "SFMono-Regular", Consolas, monospace;
|
|
464
|
+
font-size: 12px;
|
|
465
|
+
line-height: 1.55;
|
|
466
|
+
white-space: pre-wrap;
|
|
467
|
+
overflow-wrap: anywhere;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.all-blocks {
|
|
471
|
+
display: grid;
|
|
472
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
473
|
+
gap: 12px;
|
|
474
|
+
margin-top: 18px;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.mini {
|
|
478
|
+
border: 1px solid var(--line);
|
|
479
|
+
background: rgba(255, 250, 240, 0.82);
|
|
480
|
+
padding: 10px;
|
|
481
|
+
cursor: pointer;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.mini img {
|
|
485
|
+
width: 100%;
|
|
486
|
+
aspect-ratio: 16 / 9;
|
|
487
|
+
object-fit: cover;
|
|
488
|
+
border: 1px solid var(--line);
|
|
489
|
+
background: #fffdf7;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.mini h3 {
|
|
493
|
+
margin-top: 9px;
|
|
494
|
+
font-size: 17px;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.mini p {
|
|
498
|
+
margin: 6px 0 0;
|
|
499
|
+
color: var(--muted);
|
|
500
|
+
font-size: 12px;
|
|
501
|
+
line-height: 1.35;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.empty-thumb {
|
|
505
|
+
width: 100%;
|
|
506
|
+
aspect-ratio: 16 / 9;
|
|
507
|
+
display: grid;
|
|
508
|
+
place-items: center;
|
|
509
|
+
border: 1px solid var(--line);
|
|
510
|
+
background: #fffdf7;
|
|
511
|
+
color: var(--missing);
|
|
512
|
+
font-size: 12px;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
@media (max-width: 980px) {
|
|
516
|
+
.shell {
|
|
517
|
+
grid-template-columns: 1fr;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.sidebar {
|
|
521
|
+
position: relative;
|
|
522
|
+
height: auto;
|
|
523
|
+
border-right: 0;
|
|
524
|
+
border-bottom: 1px solid var(--line);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.detail {
|
|
528
|
+
grid-template-columns: 1fr;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
@media (max-width: 640px) {
|
|
533
|
+
.content,
|
|
534
|
+
.sidebar {
|
|
535
|
+
padding: 16px;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.summary-row {
|
|
539
|
+
display: grid;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.summary-copy h2 {
|
|
543
|
+
font-size: 28px;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.info-grid {
|
|
547
|
+
grid-template-columns: 1fr;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
</style>
|
|
551
|
+
</head>
|
|
552
|
+
<body>
|
|
553
|
+
<div class="shell">
|
|
554
|
+
<aside class="sidebar">
|
|
555
|
+
<div class="brand">
|
|
556
|
+
<div class="eyebrow">Page Analyzer</div>
|
|
557
|
+
<h1>Block Review</h1>
|
|
558
|
+
<div class="title" id="page-title"></div>
|
|
559
|
+
<div class="load-state" id="load-state">Loading result.json...</div>
|
|
560
|
+
<form class="url-loader" id="url-loader">
|
|
561
|
+
<label for="result-url">Result JSON URL</label>
|
|
562
|
+
<div class="url-row">
|
|
563
|
+
<input id="result-url" type="text" inputmode="url" autocomplete="url" placeholder="https://example.com/result.json">
|
|
564
|
+
<button type="submit">Load</button>
|
|
565
|
+
</div>
|
|
566
|
+
</form>
|
|
567
|
+
<label class="file-loader" id="file-loader">
|
|
568
|
+
<span>Choose result.json manually</span>
|
|
569
|
+
<input id="result-file" type="file" accept="application/json,.json">
|
|
570
|
+
</label>
|
|
571
|
+
</div>
|
|
572
|
+
<div class="metrics" id="metrics"></div>
|
|
573
|
+
<div class="toolbar">
|
|
574
|
+
<input class="search" id="search" type="search" placeholder="Search blocks, selectors, semantics">
|
|
575
|
+
<div class="filters" role="group" aria-label="Block filter">
|
|
576
|
+
<button class="filter" data-filter="all" aria-pressed="true">All</button>
|
|
577
|
+
<button class="filter" data-filter="shots" aria-pressed="false">Shots</button>
|
|
578
|
+
<button class="filter" data-filter="missing" aria-pressed="false">Missing</button>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
<div class="block-list" id="block-list"></div>
|
|
582
|
+
</aside>
|
|
583
|
+
<main class="content">
|
|
584
|
+
<section class="summary-row">
|
|
585
|
+
<div class="summary-copy">
|
|
586
|
+
<h2 id="selected-title"></h2>
|
|
587
|
+
<p id="selected-description"></p>
|
|
588
|
+
</div>
|
|
589
|
+
<div class="actions">
|
|
590
|
+
<a class="action secondary" id="full-page-link" target="_blank" rel="noreferrer">Open full page shot</a>
|
|
591
|
+
<button class="action" id="copy-selector" type="button">Copy selector</button>
|
|
592
|
+
</div>
|
|
593
|
+
</section>
|
|
594
|
+
|
|
595
|
+
<section class="detail">
|
|
596
|
+
<article class="panel">
|
|
597
|
+
<div class="panel-head">
|
|
598
|
+
<h3>Screenshot</h3>
|
|
599
|
+
</div>
|
|
600
|
+
<div class="panel-body">
|
|
601
|
+
<div class="shot-box" id="screenshot"></div>
|
|
602
|
+
</div>
|
|
603
|
+
</article>
|
|
604
|
+
|
|
605
|
+
<article class="panel">
|
|
606
|
+
<div class="panel-head">
|
|
607
|
+
<h3>Block Data</h3>
|
|
608
|
+
</div>
|
|
609
|
+
<div class="panel-body">
|
|
610
|
+
<div class="info-grid" id="block-info"></div>
|
|
611
|
+
</div>
|
|
612
|
+
</article>
|
|
613
|
+
</section>
|
|
614
|
+
|
|
615
|
+
<section class="panel" style="margin-top: 18px;">
|
|
616
|
+
<div class="panel-head">
|
|
617
|
+
<h3>All Blocks</h3>
|
|
618
|
+
</div>
|
|
619
|
+
<div class="panel-body">
|
|
620
|
+
<div class="all-blocks" id="all-blocks"></div>
|
|
621
|
+
</div>
|
|
622
|
+
</section>
|
|
623
|
+
|
|
624
|
+
<section class="panel" style="margin-top: 18px;">
|
|
625
|
+
<div class="panel-head">
|
|
626
|
+
<h3>Raw Block JSON</h3>
|
|
627
|
+
</div>
|
|
628
|
+
<div class="panel-body">
|
|
629
|
+
<pre id="raw-json"></pre>
|
|
630
|
+
</div>
|
|
631
|
+
</section>
|
|
632
|
+
</main>
|
|
633
|
+
</div>
|
|
634
|
+
|
|
635
|
+
<script>
|
|
636
|
+
let data = null;
|
|
637
|
+
let blocks = [];
|
|
638
|
+
let screenshotRows = [];
|
|
639
|
+
let screenshotByBlockIdx = new Map();
|
|
640
|
+
let selectedIndex = 0;
|
|
641
|
+
let activeFilter = 'all';
|
|
642
|
+
let query = '';
|
|
643
|
+
let resultSourceUrl = '';
|
|
644
|
+
|
|
645
|
+
const DEFAULT_RESULT_URL = './result.json';
|
|
646
|
+
|
|
647
|
+
const els = {
|
|
648
|
+
pageTitle: document.getElementById('page-title'),
|
|
649
|
+
metrics: document.getElementById('metrics'),
|
|
650
|
+
list: document.getElementById('block-list'),
|
|
651
|
+
search: document.getElementById('search'),
|
|
652
|
+
selectedTitle: document.getElementById('selected-title'),
|
|
653
|
+
selectedDescription: document.getElementById('selected-description'),
|
|
654
|
+
screenshot: document.getElementById('screenshot'),
|
|
655
|
+
info: document.getElementById('block-info'),
|
|
656
|
+
raw: document.getElementById('raw-json'),
|
|
657
|
+
allBlocks: document.getElementById('all-blocks'),
|
|
658
|
+
copySelector: document.getElementById('copy-selector'),
|
|
659
|
+
fullPageLink: document.getElementById('full-page-link'),
|
|
660
|
+
loadState: document.getElementById('load-state'),
|
|
661
|
+
fileLoader: document.getElementById('file-loader'),
|
|
662
|
+
resultFile: document.getElementById('result-file'),
|
|
663
|
+
urlLoader: document.getElementById('url-loader'),
|
|
664
|
+
resultUrl: document.getElementById('result-url')
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
function asArray(value) {
|
|
668
|
+
if (Array.isArray(value)) return value;
|
|
669
|
+
if (value === undefined || value === null || value === '') return [];
|
|
670
|
+
return [value];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function isLikelyLocalAbsolutePath(value) {
|
|
674
|
+
return /^\\/(Users|Volumes|Applications|System|Library|private|tmp|var|home)\\//.test(value);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function pathToUrl(value) {
|
|
678
|
+
const text = String(value || '').trim();
|
|
679
|
+
if (!text) return '';
|
|
680
|
+
if (/^(https?:|file:|data:|blob:)/i.test(text)) return text;
|
|
681
|
+
if (resultSourceUrl && /^(https?:|file:)/i.test(resultSourceUrl)) {
|
|
682
|
+
const shouldResolveFromSource = !text.startsWith('/') ||
|
|
683
|
+
(/^https?:/i.test(resultSourceUrl) && !isLikelyLocalAbsolutePath(text));
|
|
684
|
+
if (shouldResolveFromSource) {
|
|
685
|
+
try {
|
|
686
|
+
return new URL(text, resultSourceUrl).href;
|
|
687
|
+
} catch {
|
|
688
|
+
// Fall through to local snapshot handling.
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const snapshotIndex = text.lastIndexOf('/snapshots/');
|
|
693
|
+
if (snapshotIndex >= 0) {
|
|
694
|
+
return encodeURI('./snapshots/' + text.slice(snapshotIndex + '/snapshots/'.length));
|
|
695
|
+
}
|
|
696
|
+
if (text.startsWith('/')) return 'file://' + encodeURI(text);
|
|
697
|
+
return text;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function getShot(block, index) {
|
|
701
|
+
const direct = asArray(block.blockScreenshotPaths)[0] || block.blockScreenshotPath || block.screenshotPath || '';
|
|
702
|
+
if (direct) return { path: direct };
|
|
703
|
+
return screenshotByBlockIdx.get(index) || null;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function formatPosition(position) {
|
|
707
|
+
if (!position || typeof position !== 'object') return 'n/a';
|
|
708
|
+
const left = Number(position.left || 0).toFixed(0);
|
|
709
|
+
const top = Number(position.top || 0).toFixed(0);
|
|
710
|
+
const width = Number(position.width || 0).toFixed(0);
|
|
711
|
+
const height = Number(position.height || 0).toFixed(0);
|
|
712
|
+
return 'x=' + left + ', y=' + top + ', ' + width + 'x' + height;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function textMatches(block, index) {
|
|
716
|
+
const haystack = [
|
|
717
|
+
index,
|
|
718
|
+
block.blockName,
|
|
719
|
+
block.blockDescription,
|
|
720
|
+
block.blockCssPath,
|
|
721
|
+
block.blockIdxs,
|
|
722
|
+
...asArray(block.blockSemantics),
|
|
723
|
+
...asArray(block.blockPossibleEvents),
|
|
724
|
+
...asArray(block.blockSemanticGroups).map((item) => JSON.stringify(item))
|
|
725
|
+
].join(' ').toLowerCase();
|
|
726
|
+
return haystack.includes(query.toLowerCase());
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function isVisibleByFilter(block, index) {
|
|
730
|
+
const hasShot = Boolean(getShot(block, index));
|
|
731
|
+
if (activeFilter === 'shots') return hasShot;
|
|
732
|
+
if (activeFilter === 'missing') return !hasShot;
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function visibleBlocks() {
|
|
737
|
+
return blocks
|
|
738
|
+
.map((block, index) => ({ block, index }))
|
|
739
|
+
.filter(({ block, index }) => textMatches(block, index) && isVisibleByFilter(block, index));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function screenshotCount() {
|
|
743
|
+
const paths = new Set();
|
|
744
|
+
for (const item of screenshotRows) {
|
|
745
|
+
if (item?.path) paths.add(item.path);
|
|
746
|
+
}
|
|
747
|
+
for (const block of blocks) {
|
|
748
|
+
for (const path of asArray(block.blockScreenshotPaths)) {
|
|
749
|
+
if (path) paths.add(path);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return paths.size;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function renderMetrics() {
|
|
756
|
+
const stats = data.analysis?.block_analysis?.stats || {};
|
|
757
|
+
const metrics = [
|
|
758
|
+
['Blocks', blocks.length],
|
|
759
|
+
['Screenshots', screenshotCount()],
|
|
760
|
+
['Elements', data.parseMetrics?.elementsCount || 0],
|
|
761
|
+
['Parse ms', data.parseMetrics?.parseMs || 0]
|
|
762
|
+
];
|
|
763
|
+
els.metrics.innerHTML = metrics.map(([label, value]) => (
|
|
764
|
+
'<div class="metric"><strong>' + value + '</strong><span>' + label + '</span></div>'
|
|
765
|
+
)).join('');
|
|
766
|
+
if (stats.total_blocks && stats.total_blocks !== blocks.length) {
|
|
767
|
+
els.metrics.insertAdjacentHTML('beforeend',
|
|
768
|
+
'<div class="metric"><strong>' + stats.total_blocks + '</strong><span>Reported blocks</span></div>'
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function renderList() {
|
|
774
|
+
const rows = visibleBlocks();
|
|
775
|
+
if (!rows.length) {
|
|
776
|
+
els.list.innerHTML = '<div class="load-state">No blocks match the current search or filter.</div>';
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
els.list.innerHTML = rows.map(({ block, index }) => {
|
|
780
|
+
const hasShot = Boolean(getShot(block, index));
|
|
781
|
+
const status = hasShot ? 'shot' : 'no shot';
|
|
782
|
+
const name = block.blockName || 'Unnamed block';
|
|
783
|
+
const desc = block.blockDescription || block.blockCssPath || 'No description';
|
|
784
|
+
return '<button class="block-tab ' + (index === selectedIndex ? 'active' : '') + '" data-index="' + index + '">' +
|
|
785
|
+
'<div class="tab-top"><span class="tab-name">' + escapeHtml(name) + '</span><span class="tab-index">#' + index + ' ' + status + '</span></div>' +
|
|
786
|
+
'<div class="tab-desc">' + escapeHtml(desc) + '</div>' +
|
|
787
|
+
'</button>';
|
|
788
|
+
}).join('');
|
|
789
|
+
|
|
790
|
+
for (const button of els.list.querySelectorAll('.block-tab')) {
|
|
791
|
+
button.addEventListener('click', () => selectBlock(Number(button.dataset.index)));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function renderAllBlocks() {
|
|
796
|
+
const rows = visibleBlocks();
|
|
797
|
+
if (!rows.length) {
|
|
798
|
+
els.allBlocks.innerHTML = '<div class="missing-shot">No blocks to show.</div>';
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
els.allBlocks.innerHTML = rows.map(({ block, index }) => {
|
|
802
|
+
const shot = getShot(block, index);
|
|
803
|
+
const image = shot
|
|
804
|
+
? '<img src="' + pathToUrl(shot.path) + '" alt="Screenshot for block ' + index + '">'
|
|
805
|
+
: '<div class="empty-thumb">No selector screenshot</div>';
|
|
806
|
+
return '<article class="mini" data-index="' + index + '">' +
|
|
807
|
+
image +
|
|
808
|
+
'<h3>#' + index + ' ' + escapeHtml(block.blockName || 'Unnamed block') + '</h3>' +
|
|
809
|
+
'<p>' + escapeHtml(block.blockDescription || block.blockCssPath || 'No description') + '</p>' +
|
|
810
|
+
'</article>';
|
|
811
|
+
}).join('');
|
|
812
|
+
|
|
813
|
+
for (const item of els.allBlocks.querySelectorAll('.mini')) {
|
|
814
|
+
item.addEventListener('click', () => {
|
|
815
|
+
selectBlock(Number(item.dataset.index));
|
|
816
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function renderSelected() {
|
|
822
|
+
if (!blocks.length) {
|
|
823
|
+
els.selectedTitle.textContent = 'No blocks found';
|
|
824
|
+
els.selectedDescription.textContent = 'Loaded JSON does not contain block analysis rows.';
|
|
825
|
+
els.copySelector.disabled = true;
|
|
826
|
+
els.fullPageLink.href = pathToUrl(data.screenshots?.fullPage || '');
|
|
827
|
+
els.fullPageLink.style.display = data.screenshots?.fullPage ? 'inline-flex' : 'none';
|
|
828
|
+
els.screenshot.innerHTML = '<div class="missing-shot">Load a Page Analyzer result with analysis.block_analysis.blocks.</div>';
|
|
829
|
+
els.info.innerHTML = '';
|
|
830
|
+
els.raw.textContent = JSON.stringify(data, null, 2);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (!blocks[selectedIndex]) selectedIndex = 0;
|
|
834
|
+
const block = blocks[selectedIndex] || {};
|
|
835
|
+
const shot = getShot(block, selectedIndex);
|
|
836
|
+
els.selectedTitle.textContent = '#' + selectedIndex + ' ' + (block.blockName || 'Unnamed block');
|
|
837
|
+
els.selectedDescription.textContent = block.blockDescription || 'No description available.';
|
|
838
|
+
els.copySelector.disabled = !block.blockCssPath;
|
|
839
|
+
els.fullPageLink.href = pathToUrl(data.screenshots?.fullPage || '');
|
|
840
|
+
els.fullPageLink.style.display = data.screenshots?.fullPage ? 'inline-flex' : 'none';
|
|
841
|
+
|
|
842
|
+
if (shot?.path) {
|
|
843
|
+
els.screenshot.innerHTML =
|
|
844
|
+
'<div class="screenshot-frame"><img src="' + pathToUrl(shot.path) + '" alt="Screenshot for selected block"></div>' +
|
|
845
|
+
'<div class="info wide"><label>Screenshot path</label><span>' + escapeHtml(shot.path) + '</span></div>';
|
|
846
|
+
} else {
|
|
847
|
+
els.screenshot.innerHTML =
|
|
848
|
+
'<div class="missing-shot">No screenshot was generated for this block.<br>Most likely the selector was empty, hidden, or not screenshotable.</div>';
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const semantics = asArray(block.blockSemantics);
|
|
852
|
+
const events = asArray(block.blockPossibleEvents);
|
|
853
|
+
const groups = asArray(block.blockSemanticGroups);
|
|
854
|
+
els.info.innerHTML = [
|
|
855
|
+
info('Block name', block.blockName || 'n/a'),
|
|
856
|
+
info('Block idxs', block.blockIdxs || String(selectedIndex)),
|
|
857
|
+
info('Mode', block.mode || 'n/a'),
|
|
858
|
+
info('Rows', block.rowCount ?? 'n/a'),
|
|
859
|
+
info('Position', formatPosition(block.blockPosition), true),
|
|
860
|
+
info('Selector', block.blockCssPath || 'n/a', true),
|
|
861
|
+
chipInfo('Semantics', semantics, 'No semantics'),
|
|
862
|
+
chipInfo('Possible events', events, 'No possible events'),
|
|
863
|
+
chipInfo('Semantic groups', groups.map((item) => item.blockSemantic + ': ' + item.blockIdxs), 'No semantic groups')
|
|
864
|
+
].join('');
|
|
865
|
+
els.raw.textContent = JSON.stringify(block, null, 2);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function info(label, value, wide = false) {
|
|
869
|
+
return '<div class="info ' + (wide ? 'wide' : '') + '"><label>' + escapeHtml(label) + '</label><span>' + escapeHtml(value) + '</span></div>';
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function chipInfo(label, values, emptyText) {
|
|
873
|
+
const list = Array.isArray(values) ? values.filter(Boolean) : [];
|
|
874
|
+
const chips = list.length
|
|
875
|
+
? list.map((value) => '<span class="chip ok">' + escapeHtml(value) + '</span>').join('')
|
|
876
|
+
: '<span class="chip warn">' + escapeHtml(emptyText) + '</span>';
|
|
877
|
+
return '<div class="info wide"><label>' + escapeHtml(label) + '</label><div class="chips">' + chips + '</div></div>';
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function selectBlock(index) {
|
|
881
|
+
selectedIndex = Number.isInteger(index) ? index : 0;
|
|
882
|
+
renderList();
|
|
883
|
+
renderSelected();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function escapeHtml(value) {
|
|
887
|
+
return String(value ?? '')
|
|
888
|
+
.replace(/&/g, '&')
|
|
889
|
+
.replace(/</g, '<')
|
|
890
|
+
.replace(/>/g, '>')
|
|
891
|
+
.replace(/"/g, '"')
|
|
892
|
+
.replace(/'/g, ''');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
els.search.addEventListener('input', () => {
|
|
896
|
+
query = els.search.value;
|
|
897
|
+
renderList();
|
|
898
|
+
renderAllBlocks();
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
for (const button of document.querySelectorAll('.filter')) {
|
|
902
|
+
button.addEventListener('click', () => {
|
|
903
|
+
activeFilter = button.dataset.filter;
|
|
904
|
+
for (const item of document.querySelectorAll('.filter')) {
|
|
905
|
+
item.setAttribute('aria-pressed', String(item === button));
|
|
906
|
+
}
|
|
907
|
+
renderList();
|
|
908
|
+
renderAllBlocks();
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
els.copySelector.addEventListener('click', async () => {
|
|
913
|
+
const selector = blocks[selectedIndex]?.blockCssPath || '';
|
|
914
|
+
if (!selector) return;
|
|
915
|
+
try {
|
|
916
|
+
await navigator.clipboard.writeText(selector);
|
|
917
|
+
els.copySelector.textContent = 'Copied';
|
|
918
|
+
setTimeout(() => { els.copySelector.textContent = 'Copy selector'; }, 900);
|
|
919
|
+
} catch {
|
|
920
|
+
window.prompt('Copy selector', selector);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
function setLoadState(message, isError = false) {
|
|
925
|
+
els.loadState.textContent = message;
|
|
926
|
+
els.loadState.classList.toggle('error', isError);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function getBlocks(nextData) {
|
|
930
|
+
const candidates = [
|
|
931
|
+
nextData?.analysis?.block_analysis?.blocks,
|
|
932
|
+
nextData?.block_analysis?.blocks,
|
|
933
|
+
nextData?.analysis?.blocks,
|
|
934
|
+
nextData?.blocks
|
|
935
|
+
];
|
|
936
|
+
return candidates.find(Array.isArray) || [];
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function getScreenshotRows(nextData) {
|
|
940
|
+
const rows = nextData?.screenshots?.blocks || nextData?.blockScreenshots || [];
|
|
941
|
+
if (Array.isArray(rows)) {
|
|
942
|
+
return rows
|
|
943
|
+
.map((item, index) => typeof item === 'string' ? { blockIdx: index, path: item } : item)
|
|
944
|
+
.filter((item) => item && item.path);
|
|
945
|
+
}
|
|
946
|
+
if (rows && typeof rows === 'object') {
|
|
947
|
+
return Object.entries(rows).map(([blockIdx, path]) => ({ blockIdx, path }));
|
|
948
|
+
}
|
|
949
|
+
return [];
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function buildScreenshotMap(rows) {
|
|
953
|
+
return new Map(rows.map((item, index) => [
|
|
954
|
+
Number(item.blockIdx ?? item.blockIndex ?? item.index ?? index),
|
|
955
|
+
item
|
|
956
|
+
]));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function sourceLabelForUrl(value) {
|
|
960
|
+
try {
|
|
961
|
+
const url = new URL(value, window.location.href);
|
|
962
|
+
return url.pathname.split('/').filter(Boolean).pop() || url.host || value;
|
|
963
|
+
} catch {
|
|
964
|
+
return value;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function normalizeResultUrl(value) {
|
|
969
|
+
const text = String(value || '').trim();
|
|
970
|
+
if (!text) {
|
|
971
|
+
throw new Error('Result JSON URL is empty');
|
|
972
|
+
}
|
|
973
|
+
return new URL(text, window.location.href).href;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function getInitialResultUrl() {
|
|
977
|
+
const params = new URLSearchParams(window.location.search);
|
|
978
|
+
return params.get('result') || params.get('url') || '';
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function rememberResultUrl(value) {
|
|
982
|
+
const url = new URL(window.location.href);
|
|
983
|
+
url.searchParams.set('result', value);
|
|
984
|
+
url.searchParams.delete('url');
|
|
985
|
+
window.history.replaceState(null, '', url);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function forgetResultUrl() {
|
|
989
|
+
const url = new URL(window.location.href);
|
|
990
|
+
url.searchParams.delete('result');
|
|
991
|
+
url.searchParams.delete('url');
|
|
992
|
+
window.history.replaceState(null, '', url);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function renderLoadError(error, title = 'result.json not loaded') {
|
|
996
|
+
if (data) return;
|
|
997
|
+
els.selectedTitle.textContent = title;
|
|
998
|
+
els.selectedDescription.textContent = error.message || 'Unknown load error';
|
|
999
|
+
els.screenshot.innerHTML = '<div class="missing-shot">Load result.json to inspect blocks.</div>';
|
|
1000
|
+
els.info.innerHTML = '';
|
|
1001
|
+
els.raw.textContent = '';
|
|
1002
|
+
els.allBlocks.innerHTML = '';
|
|
1003
|
+
els.list.innerHTML = '';
|
|
1004
|
+
els.metrics.innerHTML = '';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function initialize(nextData, sourceLabel, sourceUrl = '') {
|
|
1008
|
+
data = nextData || {};
|
|
1009
|
+
resultSourceUrl = sourceUrl ? normalizeResultUrl(sourceUrl) : '';
|
|
1010
|
+
blocks = getBlocks(data);
|
|
1011
|
+
screenshotRows = getScreenshotRows(data);
|
|
1012
|
+
screenshotByBlockIdx = buildScreenshotMap(screenshotRows);
|
|
1013
|
+
selectedIndex = 0;
|
|
1014
|
+
query = '';
|
|
1015
|
+
els.search.value = '';
|
|
1016
|
+
els.pageTitle.textContent = data.title || 'Untitled page';
|
|
1017
|
+
setLoadState(sourceLabel + ' loaded. ' + blocks.length + ' blocks, ' + screenshotCount() + ' screenshots.');
|
|
1018
|
+
renderMetrics();
|
|
1019
|
+
renderList();
|
|
1020
|
+
renderAllBlocks();
|
|
1021
|
+
renderSelected();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function loadResultFromUrl(rawUrl, updateAddress = false) {
|
|
1025
|
+
const resolvedUrl = normalizeResultUrl(rawUrl);
|
|
1026
|
+
els.resultUrl.value = rawUrl;
|
|
1027
|
+
setLoadState('Loading ' + rawUrl + '...');
|
|
1028
|
+
try {
|
|
1029
|
+
const response = await fetch(resolvedUrl, { cache: 'no-store' });
|
|
1030
|
+
if (!response.ok) {
|
|
1031
|
+
throw new Error('HTTP ' + response.status + ' while loading result JSON');
|
|
1032
|
+
}
|
|
1033
|
+
initialize(await response.json(), sourceLabelForUrl(rawUrl), resolvedUrl);
|
|
1034
|
+
if (updateAddress) rememberResultUrl(rawUrl);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
setLoadState(
|
|
1037
|
+
'Could not load ' + rawUrl + '. ' + (error.message || 'Unknown load error') + '. Remote URLs must allow browser CORS access.',
|
|
1038
|
+
true
|
|
1039
|
+
);
|
|
1040
|
+
renderLoadError(error);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
async function loadResultJson() {
|
|
1045
|
+
const initialUrl = getInitialResultUrl() || DEFAULT_RESULT_URL;
|
|
1046
|
+
els.resultUrl.value = initialUrl;
|
|
1047
|
+
await loadResultFromUrl(initialUrl);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
els.urlLoader.addEventListener('submit', async (event) => {
|
|
1051
|
+
event.preventDefault();
|
|
1052
|
+
await loadResultFromUrl(els.resultUrl.value, true);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
els.resultFile.addEventListener('change', async () => {
|
|
1056
|
+
const file = els.resultFile.files?.[0];
|
|
1057
|
+
if (!file) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
try {
|
|
1061
|
+
initialize(JSON.parse(await file.text()), file.name);
|
|
1062
|
+
els.resultUrl.value = '';
|
|
1063
|
+
forgetResultUrl();
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
setLoadState('Could not parse selected JSON: ' + error.message, true);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
loadResultJson();
|
|
1070
|
+
</script>
|
|
1071
|
+
</body>
|
|
1072
|
+
</html>
|
|
1073
|
+
`;
|
|
1074
|
+
|
|
1075
|
+
fs.writeFileSync(outputPath, html);
|
|
1076
|
+
console.log(`Wrote ${outputPath}`);
|