page-analyzer 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/index.js +42 -38
- package/package.json +2 -1
- package/page-extractor.js +228 -49
- package/result-viewer.html +237 -35
- package/scripts/build-result-viewer.js +237 -35
- package/test/smoke.test.js +240 -1
package/result-viewer.html
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<link rel="icon" href="data:,">
|
|
6
7
|
<title>Page Analyzer Result Viewer</title>
|
|
7
8
|
<style>
|
|
8
9
|
:root {
|
|
@@ -108,12 +109,28 @@
|
|
|
108
109
|
color: var(--missing);
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
.url-loader,
|
|
111
113
|
.file-loader {
|
|
112
114
|
display: grid;
|
|
113
115
|
gap: 8px;
|
|
114
116
|
margin-top: 8px;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
.url-loader label,
|
|
120
|
+
.file-loader span {
|
|
121
|
+
color: var(--muted);
|
|
122
|
+
font-size: 11px;
|
|
123
|
+
font-weight: 800;
|
|
124
|
+
text-transform: uppercase;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.url-row {
|
|
128
|
+
display: grid;
|
|
129
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
130
|
+
gap: 8px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.url-loader input,
|
|
117
134
|
.file-loader input {
|
|
118
135
|
width: 100%;
|
|
119
136
|
border: 1px solid var(--line);
|
|
@@ -123,6 +140,16 @@
|
|
|
123
140
|
font-size: 12px;
|
|
124
141
|
}
|
|
125
142
|
|
|
143
|
+
.url-loader button {
|
|
144
|
+
min-width: 68px;
|
|
145
|
+
border: 1px solid var(--ink);
|
|
146
|
+
background: var(--ink);
|
|
147
|
+
color: white;
|
|
148
|
+
padding: 0 12px;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
.metrics {
|
|
127
154
|
display: grid;
|
|
128
155
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -522,7 +549,14 @@
|
|
|
522
549
|
<h1>Block Review</h1>
|
|
523
550
|
<div class="title" id="page-title"></div>
|
|
524
551
|
<div class="load-state" id="load-state">Loading result.json...</div>
|
|
525
|
-
<
|
|
552
|
+
<form class="url-loader" id="url-loader">
|
|
553
|
+
<label for="result-url">Result JSON URL</label>
|
|
554
|
+
<div class="url-row">
|
|
555
|
+
<input id="result-url" type="text" inputmode="url" autocomplete="url" placeholder="https://example.com/result.json">
|
|
556
|
+
<button type="submit">Load</button>
|
|
557
|
+
</div>
|
|
558
|
+
</form>
|
|
559
|
+
<label class="file-loader" id="file-loader">
|
|
526
560
|
<span>Choose result.json manually</span>
|
|
527
561
|
<input id="result-file" type="file" accept="application/json,.json">
|
|
528
562
|
</label>
|
|
@@ -598,6 +632,9 @@
|
|
|
598
632
|
let selectedIndex = 0;
|
|
599
633
|
let activeFilter = 'all';
|
|
600
634
|
let query = '';
|
|
635
|
+
let resultSourceUrl = '';
|
|
636
|
+
|
|
637
|
+
const DEFAULT_RESULT_URL = './result.json';
|
|
601
638
|
|
|
602
639
|
const els = {
|
|
603
640
|
pageTitle: document.getElementById('page-title'),
|
|
@@ -614,13 +651,36 @@
|
|
|
614
651
|
fullPageLink: document.getElementById('full-page-link'),
|
|
615
652
|
loadState: document.getElementById('load-state'),
|
|
616
653
|
fileLoader: document.getElementById('file-loader'),
|
|
617
|
-
resultFile: document.getElementById('result-file')
|
|
654
|
+
resultFile: document.getElementById('result-file'),
|
|
655
|
+
urlLoader: document.getElementById('url-loader'),
|
|
656
|
+
resultUrl: document.getElementById('result-url')
|
|
618
657
|
};
|
|
619
658
|
|
|
659
|
+
function asArray(value) {
|
|
660
|
+
if (Array.isArray(value)) return value;
|
|
661
|
+
if (value === undefined || value === null || value === '') return [];
|
|
662
|
+
return [value];
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function isLikelyLocalAbsolutePath(value) {
|
|
666
|
+
return /^\/(Users|Volumes|Applications|System|Library|private|tmp|var|home)\//.test(value);
|
|
667
|
+
}
|
|
668
|
+
|
|
620
669
|
function pathToUrl(value) {
|
|
621
|
-
const text = String(value || '');
|
|
670
|
+
const text = String(value || '').trim();
|
|
622
671
|
if (!text) return '';
|
|
623
|
-
if (/^(https?:|file:|data:)/i.test(text)) return text;
|
|
672
|
+
if (/^(https?:|file:|data:|blob:)/i.test(text)) return text;
|
|
673
|
+
if (resultSourceUrl && /^(https?:|file:)/i.test(resultSourceUrl)) {
|
|
674
|
+
const shouldResolveFromSource = !text.startsWith('/') ||
|
|
675
|
+
(/^https?:/i.test(resultSourceUrl) && !isLikelyLocalAbsolutePath(text));
|
|
676
|
+
if (shouldResolveFromSource) {
|
|
677
|
+
try {
|
|
678
|
+
return new URL(text, resultSourceUrl).href;
|
|
679
|
+
} catch {
|
|
680
|
+
// Fall through to local snapshot handling.
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
624
684
|
const snapshotIndex = text.lastIndexOf('/snapshots/');
|
|
625
685
|
if (snapshotIndex >= 0) {
|
|
626
686
|
return encodeURI('./snapshots/' + text.slice(snapshotIndex + '/snapshots/'.length));
|
|
@@ -629,8 +689,21 @@
|
|
|
629
689
|
return text;
|
|
630
690
|
}
|
|
631
691
|
|
|
692
|
+
function safeResourceUrl(value) {
|
|
693
|
+
const url = pathToUrl(value);
|
|
694
|
+
if (!url) return '';
|
|
695
|
+
if (/^(https?:|file:|blob:)/i.test(url)) return url;
|
|
696
|
+
if (/^data:image\/(png|jpe?g|gif|webp);/i.test(url)) return url;
|
|
697
|
+
if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) return url;
|
|
698
|
+
return '';
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function imageSrcAttr(value) {
|
|
702
|
+
return escapeHtml(safeResourceUrl(value));
|
|
703
|
+
}
|
|
704
|
+
|
|
632
705
|
function getShot(block, index) {
|
|
633
|
-
const direct =
|
|
706
|
+
const direct = asArray(block.blockScreenshotPaths)[0] || block.blockScreenshotPath || block.screenshotPath || '';
|
|
634
707
|
if (direct) return { path: direct };
|
|
635
708
|
return screenshotByBlockIdx.get(index) || null;
|
|
636
709
|
}
|
|
@@ -651,8 +724,9 @@
|
|
|
651
724
|
block.blockDescription,
|
|
652
725
|
block.blockCssPath,
|
|
653
726
|
block.blockIdxs,
|
|
654
|
-
...(block.blockSemantics
|
|
655
|
-
...(block.blockPossibleEvents
|
|
727
|
+
...asArray(block.blockSemantics),
|
|
728
|
+
...asArray(block.blockPossibleEvents),
|
|
729
|
+
...asArray(block.blockSemanticGroups).map((item) => JSON.stringify(item))
|
|
656
730
|
].join(' ').toLowerCase();
|
|
657
731
|
return haystack.includes(query.toLowerCase());
|
|
658
732
|
}
|
|
@@ -670,11 +744,24 @@
|
|
|
670
744
|
.filter(({ block, index }) => textMatches(block, index) && isVisibleByFilter(block, index));
|
|
671
745
|
}
|
|
672
746
|
|
|
747
|
+
function screenshotCount() {
|
|
748
|
+
const paths = new Set();
|
|
749
|
+
for (const item of screenshotRows) {
|
|
750
|
+
if (item?.path) paths.add(item.path);
|
|
751
|
+
}
|
|
752
|
+
for (const block of blocks) {
|
|
753
|
+
for (const path of asArray(block.blockScreenshotPaths)) {
|
|
754
|
+
if (path) paths.add(path);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return paths.size;
|
|
758
|
+
}
|
|
759
|
+
|
|
673
760
|
function renderMetrics() {
|
|
674
761
|
const stats = data.analysis?.block_analysis?.stats || {};
|
|
675
762
|
const metrics = [
|
|
676
763
|
['Blocks', blocks.length],
|
|
677
|
-
['Screenshots',
|
|
764
|
+
['Screenshots', screenshotCount()],
|
|
678
765
|
['Elements', data.parseMetrics?.elementsCount || 0],
|
|
679
766
|
['Parse ms', data.parseMetrics?.parseMs || 0]
|
|
680
767
|
];
|
|
@@ -690,6 +777,10 @@
|
|
|
690
777
|
|
|
691
778
|
function renderList() {
|
|
692
779
|
const rows = visibleBlocks();
|
|
780
|
+
if (!rows.length) {
|
|
781
|
+
els.list.innerHTML = '<div class="load-state">No blocks match the current search or filter.</div>';
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
693
784
|
els.list.innerHTML = rows.map(({ block, index }) => {
|
|
694
785
|
const hasShot = Boolean(getShot(block, index));
|
|
695
786
|
const status = hasShot ? 'shot' : 'no shot';
|
|
@@ -707,10 +798,16 @@
|
|
|
707
798
|
}
|
|
708
799
|
|
|
709
800
|
function renderAllBlocks() {
|
|
710
|
-
|
|
801
|
+
const rows = visibleBlocks();
|
|
802
|
+
if (!rows.length) {
|
|
803
|
+
els.allBlocks.innerHTML = '<div class="missing-shot">No blocks to show.</div>';
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
els.allBlocks.innerHTML = rows.map(({ block, index }) => {
|
|
711
807
|
const shot = getShot(block, index);
|
|
712
|
-
const
|
|
713
|
-
|
|
808
|
+
const shotUrl = shot?.path ? imageSrcAttr(shot.path) : '';
|
|
809
|
+
const image = shotUrl
|
|
810
|
+
? '<img src="' + shotUrl + '" alt="Screenshot for block ' + index + '">'
|
|
714
811
|
: '<div class="empty-thumb">No selector screenshot</div>';
|
|
715
812
|
return '<article class="mini" data-index="' + index + '">' +
|
|
716
813
|
image +
|
|
@@ -728,26 +825,41 @@
|
|
|
728
825
|
}
|
|
729
826
|
|
|
730
827
|
function renderSelected() {
|
|
828
|
+
if (!blocks.length) {
|
|
829
|
+
els.selectedTitle.textContent = 'No blocks found';
|
|
830
|
+
els.selectedDescription.textContent = 'Loaded JSON does not contain block analysis rows.';
|
|
831
|
+
els.copySelector.disabled = true;
|
|
832
|
+
const fullPageUrl = safeResourceUrl(data.screenshots?.fullPage || '');
|
|
833
|
+
els.fullPageLink.href = fullPageUrl;
|
|
834
|
+
els.fullPageLink.style.display = fullPageUrl ? 'inline-flex' : 'none';
|
|
835
|
+
els.screenshot.innerHTML = '<div class="missing-shot">Load a Page Analyzer result with analysis.block_analysis.blocks.</div>';
|
|
836
|
+
els.info.innerHTML = '';
|
|
837
|
+
els.raw.textContent = JSON.stringify(data, null, 2);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (!blocks[selectedIndex]) selectedIndex = 0;
|
|
731
841
|
const block = blocks[selectedIndex] || {};
|
|
732
842
|
const shot = getShot(block, selectedIndex);
|
|
733
843
|
els.selectedTitle.textContent = '#' + selectedIndex + ' ' + (block.blockName || 'Unnamed block');
|
|
734
844
|
els.selectedDescription.textContent = block.blockDescription || 'No description available.';
|
|
735
845
|
els.copySelector.disabled = !block.blockCssPath;
|
|
736
|
-
|
|
737
|
-
els.fullPageLink.
|
|
846
|
+
const fullPageUrl = safeResourceUrl(data.screenshots?.fullPage || '');
|
|
847
|
+
els.fullPageLink.href = fullPageUrl;
|
|
848
|
+
els.fullPageLink.style.display = fullPageUrl ? 'inline-flex' : 'none';
|
|
738
849
|
|
|
739
|
-
|
|
850
|
+
const shotUrl = shot?.path ? imageSrcAttr(shot.path) : '';
|
|
851
|
+
if (shotUrl) {
|
|
740
852
|
els.screenshot.innerHTML =
|
|
741
|
-
'<div class="screenshot-frame"><img src="' +
|
|
853
|
+
'<div class="screenshot-frame"><img src="' + shotUrl + '" alt="Screenshot for selected block"></div>' +
|
|
742
854
|
'<div class="info wide"><label>Screenshot path</label><span>' + escapeHtml(shot.path) + '</span></div>';
|
|
743
855
|
} else {
|
|
744
856
|
els.screenshot.innerHTML =
|
|
745
857
|
'<div class="missing-shot">No screenshot was generated for this block.<br>Most likely the selector was empty, hidden, or not screenshotable.</div>';
|
|
746
858
|
}
|
|
747
859
|
|
|
748
|
-
const semantics = block.blockSemantics
|
|
749
|
-
const events = block.blockPossibleEvents
|
|
750
|
-
const groups = block.blockSemanticGroups
|
|
860
|
+
const semantics = asArray(block.blockSemantics);
|
|
861
|
+
const events = asArray(block.blockPossibleEvents);
|
|
862
|
+
const groups = asArray(block.blockSemanticGroups);
|
|
751
863
|
els.info.innerHTML = [
|
|
752
864
|
info('Block name', block.blockName || 'n/a'),
|
|
753
865
|
info('Block idxs', block.blockIdxs || String(selectedIndex)),
|
|
@@ -823,44 +935,132 @@
|
|
|
823
935
|
els.loadState.classList.toggle('error', isError);
|
|
824
936
|
}
|
|
825
937
|
|
|
826
|
-
function
|
|
938
|
+
function getBlocks(nextData) {
|
|
939
|
+
const candidates = [
|
|
940
|
+
nextData?.analysis?.block_analysis?.blocks,
|
|
941
|
+
nextData?.block_analysis?.blocks,
|
|
942
|
+
nextData?.analysis?.blocks,
|
|
943
|
+
nextData?.blocks
|
|
944
|
+
];
|
|
945
|
+
return candidates.find(Array.isArray) || [];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function getScreenshotRows(nextData) {
|
|
949
|
+
const rows = nextData?.screenshots?.blocks || nextData?.blockScreenshots || [];
|
|
950
|
+
if (Array.isArray(rows)) {
|
|
951
|
+
return rows
|
|
952
|
+
.map((item, index) => typeof item === 'string' ? { blockIdx: index, path: item } : item)
|
|
953
|
+
.filter((item) => item && item.path);
|
|
954
|
+
}
|
|
955
|
+
if (rows && typeof rows === 'object') {
|
|
956
|
+
return Object.entries(rows).map(([blockIdx, path]) => ({ blockIdx, path }));
|
|
957
|
+
}
|
|
958
|
+
return [];
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function buildScreenshotMap(rows) {
|
|
962
|
+
return new Map(rows.map((item, index) => [
|
|
963
|
+
Number(item.blockIdx ?? item.blockIndex ?? item.index ?? index),
|
|
964
|
+
item
|
|
965
|
+
]));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function sourceLabelForUrl(value) {
|
|
969
|
+
try {
|
|
970
|
+
const url = new URL(value, window.location.href);
|
|
971
|
+
return url.pathname.split('/').filter(Boolean).pop() || url.host || value;
|
|
972
|
+
} catch {
|
|
973
|
+
return value;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function normalizeResultUrl(value) {
|
|
978
|
+
const text = String(value || '').trim();
|
|
979
|
+
if (!text) {
|
|
980
|
+
throw new Error('Result JSON URL is empty');
|
|
981
|
+
}
|
|
982
|
+
return new URL(text, window.location.href).href;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function getInitialResultUrl() {
|
|
986
|
+
const params = new URLSearchParams(window.location.search);
|
|
987
|
+
return params.get('result') || params.get('url') || '';
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function rememberResultUrl(value) {
|
|
991
|
+
const url = new URL(window.location.href);
|
|
992
|
+
url.searchParams.set('result', value);
|
|
993
|
+
url.searchParams.delete('url');
|
|
994
|
+
window.history.replaceState(null, '', url);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function forgetResultUrl() {
|
|
998
|
+
const url = new URL(window.location.href);
|
|
999
|
+
url.searchParams.delete('result');
|
|
1000
|
+
url.searchParams.delete('url');
|
|
1001
|
+
window.history.replaceState(null, '', url);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function renderLoadError(error, title = 'result.json not loaded') {
|
|
1005
|
+
if (data) return;
|
|
1006
|
+
els.selectedTitle.textContent = title;
|
|
1007
|
+
els.selectedDescription.textContent = error.message || 'Unknown load error';
|
|
1008
|
+
els.screenshot.innerHTML = '<div class="missing-shot">Load result.json to inspect blocks.</div>';
|
|
1009
|
+
els.info.innerHTML = '';
|
|
1010
|
+
els.raw.textContent = '';
|
|
1011
|
+
els.allBlocks.innerHTML = '';
|
|
1012
|
+
els.list.innerHTML = '';
|
|
1013
|
+
els.metrics.innerHTML = '';
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function initialize(nextData, sourceLabel, sourceUrl = '') {
|
|
827
1017
|
data = nextData || {};
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1018
|
+
resultSourceUrl = sourceUrl ? normalizeResultUrl(sourceUrl) : '';
|
|
1019
|
+
blocks = getBlocks(data);
|
|
1020
|
+
screenshotRows = getScreenshotRows(data);
|
|
1021
|
+
screenshotByBlockIdx = buildScreenshotMap(screenshotRows);
|
|
831
1022
|
selectedIndex = 0;
|
|
832
1023
|
query = '';
|
|
833
1024
|
els.search.value = '';
|
|
834
1025
|
els.pageTitle.textContent = data.title || 'Untitled page';
|
|
835
|
-
setLoadState(sourceLabel + ' loaded. ' + blocks.length + ' blocks, ' +
|
|
836
|
-
els.fileLoader.hidden = true;
|
|
1026
|
+
setLoadState(sourceLabel + ' loaded. ' + blocks.length + ' blocks, ' + screenshotCount() + ' screenshots.');
|
|
837
1027
|
renderMetrics();
|
|
838
1028
|
renderList();
|
|
839
1029
|
renderAllBlocks();
|
|
840
1030
|
renderSelected();
|
|
841
1031
|
}
|
|
842
1032
|
|
|
843
|
-
async function
|
|
1033
|
+
async function loadResultFromUrl(rawUrl, updateAddress = false) {
|
|
1034
|
+
const resolvedUrl = normalizeResultUrl(rawUrl);
|
|
1035
|
+
els.resultUrl.value = rawUrl;
|
|
1036
|
+
setLoadState('Loading ' + rawUrl + '...');
|
|
844
1037
|
try {
|
|
845
|
-
const response = await fetch(
|
|
1038
|
+
const response = await fetch(resolvedUrl, { cache: 'no-store' });
|
|
846
1039
|
if (!response.ok) {
|
|
847
|
-
throw new Error('HTTP ' + response.status + ' while loading result
|
|
1040
|
+
throw new Error('HTTP ' + response.status + ' while loading result JSON');
|
|
848
1041
|
}
|
|
849
|
-
initialize(await response.json(),
|
|
1042
|
+
initialize(await response.json(), sourceLabelForUrl(rawUrl), resolvedUrl);
|
|
1043
|
+
if (updateAddress) rememberResultUrl(rawUrl);
|
|
850
1044
|
} catch (error) {
|
|
851
1045
|
setLoadState(
|
|
852
|
-
'Could not load
|
|
1046
|
+
'Could not load ' + rawUrl + '. ' + (error.message || 'Unknown load error') + '. Remote URLs must allow browser CORS access.',
|
|
853
1047
|
true
|
|
854
1048
|
);
|
|
855
|
-
|
|
856
|
-
els.selectedTitle.textContent = 'result.json not loaded';
|
|
857
|
-
els.selectedDescription.textContent = error.message || 'Unknown load error';
|
|
858
|
-
els.screenshot.innerHTML = '<div class="missing-shot">Load result.json to inspect blocks.</div>';
|
|
859
|
-
els.info.innerHTML = '';
|
|
860
|
-
els.raw.textContent = '';
|
|
1049
|
+
renderLoadError(error);
|
|
861
1050
|
}
|
|
862
1051
|
}
|
|
863
1052
|
|
|
1053
|
+
async function loadResultJson() {
|
|
1054
|
+
const initialUrl = getInitialResultUrl() || DEFAULT_RESULT_URL;
|
|
1055
|
+
els.resultUrl.value = initialUrl;
|
|
1056
|
+
await loadResultFromUrl(initialUrl);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
els.urlLoader.addEventListener('submit', async (event) => {
|
|
1060
|
+
event.preventDefault();
|
|
1061
|
+
await loadResultFromUrl(els.resultUrl.value, true);
|
|
1062
|
+
});
|
|
1063
|
+
|
|
864
1064
|
els.resultFile.addEventListener('change', async () => {
|
|
865
1065
|
const file = els.resultFile.files?.[0];
|
|
866
1066
|
if (!file) {
|
|
@@ -868,6 +1068,8 @@
|
|
|
868
1068
|
}
|
|
869
1069
|
try {
|
|
870
1070
|
initialize(JSON.parse(await file.text()), file.name);
|
|
1071
|
+
els.resultUrl.value = '';
|
|
1072
|
+
forgetResultUrl();
|
|
871
1073
|
} catch (error) {
|
|
872
1074
|
setLoadState('Could not parse selected JSON: ' + error.message, true);
|
|
873
1075
|
}
|