page-analyzer 1.2.0 → 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 CHANGED
@@ -305,7 +305,7 @@ const result = await analyzeUrl('https://example.com', {
305
305
  bucket: 'my-bucket',
306
306
  region: 'ap-northeast-1',
307
307
  prefix: 'page-analyzer/snapshots',
308
- publicBaseUrl: 'https://cdn.example.com/page-analyzer/snapshots',
308
+ publicBaseUrl: 'https://cdn.example.com',
309
309
  credentials: {
310
310
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
311
311
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
@@ -316,7 +316,7 @@ const result = await analyzeUrl('https://example.com', {
316
316
  });
317
317
  ```
318
318
 
319
- `extractorConfig.s3.bucket` 和 `extractorConfig.s3.region` 必填。`credentials` 可省略,省略时使用 AWS SDK 默认凭证链。`publicBaseUrl` 可省略,省略时返回 `https://<bucket>.s3.<region>.amazonaws.com/<key>`。
319
+ `extractorConfig.s3.bucket` 和 `extractorConfig.s3.region` 必填。`credentials` 可省略,省略时使用 AWS SDK 默认凭证链。`publicBaseUrl` 可省略,省略时返回 `https://<bucket>.s3.<region>.amazonaws.com/<key>`;配置后返回 `${publicBaseUrl}/<key>`。
320
320
 
321
321
  ### parserConfig
322
322
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "page-analyzer",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "description": "Standalone page analysis module.",
6
6
  "license": "MIT",
package/page-extractor.js CHANGED
@@ -107,9 +107,9 @@ function encodeS3Key(key) {
107
107
  .join('/');
108
108
  }
109
109
 
110
- function buildS3Url(s3Config, key, filename) {
110
+ function buildS3Url(s3Config, key) {
111
111
  if (s3Config.publicBaseUrl) {
112
- return `${s3Config.publicBaseUrl}/${encodeURIComponent(filename)}`;
112
+ return `${s3Config.publicBaseUrl}/${encodeS3Key(key)}`;
113
113
  }
114
114
 
115
115
  return `https://${s3Config.bucket}.s3.${s3Config.region}.amazonaws.com/${encodeS3Key(key)}`;
@@ -196,7 +196,7 @@ export class PageExtractor {
196
196
  try {
197
197
  const command = new PutObjectCommand(commandInput);
198
198
  await client.send(command);
199
- return buildS3Url(s3Config, key, filename);
199
+ return buildS3Url(s3Config, key);
200
200
  } catch (error) {
201
201
  lastError = error;
202
202
  if (attempt < s3Config.maxUploadAttempts) {
@@ -689,6 +689,19 @@
689
689
  return text;
690
690
  }
691
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
+
692
705
  function getShot(block, index) {
693
706
  const direct = asArray(block.blockScreenshotPaths)[0] || block.blockScreenshotPath || block.screenshotPath || '';
694
707
  if (direct) return { path: direct };
@@ -792,8 +805,9 @@
792
805
  }
793
806
  els.allBlocks.innerHTML = rows.map(({ block, index }) => {
794
807
  const shot = getShot(block, index);
795
- const image = shot
796
- ? '<img src="' + pathToUrl(shot.path) + '" alt="Screenshot for block ' + index + '">'
808
+ const shotUrl = shot?.path ? imageSrcAttr(shot.path) : '';
809
+ const image = shotUrl
810
+ ? '<img src="' + shotUrl + '" alt="Screenshot for block ' + index + '">'
797
811
  : '<div class="empty-thumb">No selector screenshot</div>';
798
812
  return '<article class="mini" data-index="' + index + '">' +
799
813
  image +
@@ -815,8 +829,9 @@
815
829
  els.selectedTitle.textContent = 'No blocks found';
816
830
  els.selectedDescription.textContent = 'Loaded JSON does not contain block analysis rows.';
817
831
  els.copySelector.disabled = true;
818
- els.fullPageLink.href = pathToUrl(data.screenshots?.fullPage || '');
819
- els.fullPageLink.style.display = data.screenshots?.fullPage ? 'inline-flex' : 'none';
832
+ const fullPageUrl = safeResourceUrl(data.screenshots?.fullPage || '');
833
+ els.fullPageLink.href = fullPageUrl;
834
+ els.fullPageLink.style.display = fullPageUrl ? 'inline-flex' : 'none';
820
835
  els.screenshot.innerHTML = '<div class="missing-shot">Load a Page Analyzer result with analysis.block_analysis.blocks.</div>';
821
836
  els.info.innerHTML = '';
822
837
  els.raw.textContent = JSON.stringify(data, null, 2);
@@ -828,12 +843,14 @@
828
843
  els.selectedTitle.textContent = '#' + selectedIndex + ' ' + (block.blockName || 'Unnamed block');
829
844
  els.selectedDescription.textContent = block.blockDescription || 'No description available.';
830
845
  els.copySelector.disabled = !block.blockCssPath;
831
- els.fullPageLink.href = pathToUrl(data.screenshots?.fullPage || '');
832
- els.fullPageLink.style.display = data.screenshots?.fullPage ? 'inline-flex' : 'none';
846
+ const fullPageUrl = safeResourceUrl(data.screenshots?.fullPage || '');
847
+ els.fullPageLink.href = fullPageUrl;
848
+ els.fullPageLink.style.display = fullPageUrl ? 'inline-flex' : 'none';
833
849
 
834
- if (shot?.path) {
850
+ const shotUrl = shot?.path ? imageSrcAttr(shot.path) : '';
851
+ if (shotUrl) {
835
852
  els.screenshot.innerHTML =
836
- '<div class="screenshot-frame"><img src="' + pathToUrl(shot.path) + '" alt="Screenshot for selected block"></div>' +
853
+ '<div class="screenshot-frame"><img src="' + shotUrl + '" alt="Screenshot for selected block"></div>' +
837
854
  '<div class="info wide"><label>Screenshot path</label><span>' + escapeHtml(shot.path) + '</span></div>';
838
855
  } else {
839
856
  els.screenshot.innerHTML =
@@ -697,6 +697,19 @@ const html = `<!doctype html>
697
697
  return text;
698
698
  }
699
699
 
700
+ function safeResourceUrl(value) {
701
+ const url = pathToUrl(value);
702
+ if (!url) return '';
703
+ if (/^(https?:|file:|blob:)/i.test(url)) return url;
704
+ if (/^data:image\\/(png|jpe?g|gif|webp);/i.test(url)) return url;
705
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) return url;
706
+ return '';
707
+ }
708
+
709
+ function imageSrcAttr(value) {
710
+ return escapeHtml(safeResourceUrl(value));
711
+ }
712
+
700
713
  function getShot(block, index) {
701
714
  const direct = asArray(block.blockScreenshotPaths)[0] || block.blockScreenshotPath || block.screenshotPath || '';
702
715
  if (direct) return { path: direct };
@@ -800,8 +813,9 @@ const html = `<!doctype html>
800
813
  }
801
814
  els.allBlocks.innerHTML = rows.map(({ block, index }) => {
802
815
  const shot = getShot(block, index);
803
- const image = shot
804
- ? '<img src="' + pathToUrl(shot.path) + '" alt="Screenshot for block ' + index + '">'
816
+ const shotUrl = shot?.path ? imageSrcAttr(shot.path) : '';
817
+ const image = shotUrl
818
+ ? '<img src="' + shotUrl + '" alt="Screenshot for block ' + index + '">'
805
819
  : '<div class="empty-thumb">No selector screenshot</div>';
806
820
  return '<article class="mini" data-index="' + index + '">' +
807
821
  image +
@@ -823,8 +837,9 @@ const html = `<!doctype html>
823
837
  els.selectedTitle.textContent = 'No blocks found';
824
838
  els.selectedDescription.textContent = 'Loaded JSON does not contain block analysis rows.';
825
839
  els.copySelector.disabled = true;
826
- els.fullPageLink.href = pathToUrl(data.screenshots?.fullPage || '');
827
- els.fullPageLink.style.display = data.screenshots?.fullPage ? 'inline-flex' : 'none';
840
+ const fullPageUrl = safeResourceUrl(data.screenshots?.fullPage || '');
841
+ els.fullPageLink.href = fullPageUrl;
842
+ els.fullPageLink.style.display = fullPageUrl ? 'inline-flex' : 'none';
828
843
  els.screenshot.innerHTML = '<div class="missing-shot">Load a Page Analyzer result with analysis.block_analysis.blocks.</div>';
829
844
  els.info.innerHTML = '';
830
845
  els.raw.textContent = JSON.stringify(data, null, 2);
@@ -836,12 +851,14 @@ const html = `<!doctype html>
836
851
  els.selectedTitle.textContent = '#' + selectedIndex + ' ' + (block.blockName || 'Unnamed block');
837
852
  els.selectedDescription.textContent = block.blockDescription || 'No description available.';
838
853
  els.copySelector.disabled = !block.blockCssPath;
839
- els.fullPageLink.href = pathToUrl(data.screenshots?.fullPage || '');
840
- els.fullPageLink.style.display = data.screenshots?.fullPage ? 'inline-flex' : 'none';
854
+ const fullPageUrl = safeResourceUrl(data.screenshots?.fullPage || '');
855
+ els.fullPageLink.href = fullPageUrl;
856
+ els.fullPageLink.style.display = fullPageUrl ? 'inline-flex' : 'none';
841
857
 
842
- if (shot?.path) {
858
+ const shotUrl = shot?.path ? imageSrcAttr(shot.path) : '';
859
+ if (shotUrl) {
843
860
  els.screenshot.innerHTML =
844
- '<div class="screenshot-frame"><img src="' + pathToUrl(shot.path) + '" alt="Screenshot for selected block"></div>' +
861
+ '<div class="screenshot-frame"><img src="' + shotUrl + '" alt="Screenshot for selected block"></div>' +
845
862
  '<div class="info wide"><label>Screenshot path</label><span>' + escapeHtml(shot.path) + '</span></div>';
846
863
  } else {
847
864
  els.screenshot.innerHTML =
@@ -331,7 +331,7 @@ async function analyzeWith(options = {}) {
331
331
  bucket: 'page-analyzer-test',
332
332
  region: 'ap-northeast-1',
333
333
  prefix: '/page-analyzer/snapshots/',
334
- publicBaseUrl: 'https://cdn.example.com/page-analyzer/snapshots/',
334
+ publicBaseUrl: 'https://cdn.example.com/',
335
335
  client: s3Client
336
336
  }
337
337
  });
@@ -357,13 +357,11 @@ async function analyzeWith(options = {}) {
357
357
  assert.match(fullPageUpload.Key, /^page-analyzer\/snapshots\/example-com-demo-.*-full-page\.png$/);
358
358
  assert.match(blockUpload.Key, /^page-analyzer\/snapshots\/example-com-demo-.*-block-000\.png$/);
359
359
 
360
- const fullPageFilename = fullPageUpload.Key.split('/').pop();
361
- const blockFilename = blockUpload.Key.split('/').pop();
362
360
  assert.equal(
363
361
  screenshots.fullPage,
364
- `https://cdn.example.com/page-analyzer/snapshots/${fullPageFilename}`
362
+ `https://cdn.example.com/${fullPageUpload.Key}`
365
363
  );
366
- assert.equal(screenshots.blocks[0].path, `https://cdn.example.com/page-analyzer/snapshots/${blockFilename}`);
364
+ assert.equal(screenshots.blocks[0].path, `https://cdn.example.com/${blockUpload.Key}`);
367
365
  }
368
366
 
369
367
  {
@@ -407,7 +405,7 @@ async function analyzeWith(options = {}) {
407
405
  bucket: 'page-analyzer-test',
408
406
  region: 'ap-northeast-1',
409
407
  prefix: 'page-analyzer/snapshots',
410
- publicBaseUrl: 'https://cdn.example.com/page-analyzer/snapshots',
408
+ publicBaseUrl: 'https://cdn.example.com',
411
409
  client: s3Client
412
410
  }
413
411
  });