@zeropress/build-core 0.6.10 → 0.6.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeropress/build-core",
3
- "version": "0.6.10",
3
+ "version": "0.6.11",
4
4
  "description": "Shared ZeroPress build core",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -336,14 +336,14 @@ function sanitizeHtml(html) {
336
336
  ]);
337
337
 
338
338
  const allowedAttributes = {
339
- a: new Set(['href', 'title', 'class', 'id']),
339
+ a: new Set(['href', 'title', 'class', 'id', 'target', 'rel']),
340
340
  aside: new Set(['role', 'class', 'id']),
341
341
  img: new Set(['src', 'srcset', 'sizes', 'alt', 'title', 'class', 'id', 'width', 'height', 'loading', 'decoding']),
342
342
  iframe: new Set(['src', 'width', 'height', 'frameborder', 'allowfullscreen', 'class', 'title']),
343
343
  input: new Set(['type', 'checked', 'disabled', 'class', 'id', 'aria-label']),
344
344
  source: new Set(['src', 'srcset', 'sizes', 'type', 'media', 'width', 'height', 'class', 'id']),
345
- video: new Set(['src', 'controls', 'autoplay', 'loop', 'muted', 'playsinline', 'poster', 'preload', 'width', 'height', 'class', 'id', 'title']),
346
- audio: new Set(['src', 'controls', 'autoplay', 'loop', 'muted', 'preload', 'class', 'id', 'title']),
345
+ video: new Set(['src', 'controls', 'controlslist', 'autoplay', 'loop', 'muted', 'playsinline', 'poster', 'preload', 'width', 'height', 'class', 'id', 'title']),
346
+ audio: new Set(['src', 'controls', 'controlslist', 'autoplay', 'loop', 'muted', 'preload', 'class', 'id', 'title']),
347
347
  track: new Set(['src', 'kind', 'srclang', 'label', 'default', 'class', 'id']),
348
348
  '*': new Set(['class', 'id']),
349
349
  };
@@ -368,6 +368,8 @@ function sanitizeHtml(html) {
368
368
  const tagAllowedAttributes = allowedAttributes[normalizedTag] || new Set();
369
369
  const globalAllowedAttributes = allowedAttributes['*'];
370
370
  const filteredAttributes = [];
371
+ let anchorTarget = '';
372
+ let anchorRel = '';
371
373
 
372
374
  if (attributeString) {
373
375
  const attributePattern = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
@@ -385,10 +387,37 @@ function sanitizeHtml(html) {
385
387
  continue;
386
388
  }
387
389
 
390
+ if (normalizedTag === 'a' && attributeName === 'target') {
391
+ const target = sanitizeAnchorTarget(attributeValue);
392
+ if (!target) {
393
+ continue;
394
+ }
395
+ anchorTarget = target;
396
+ continue;
397
+ }
398
+
399
+ if (normalizedTag === 'a' && attributeName === 'rel') {
400
+ const rel = sanitizeRelList(attributeValue);
401
+ if (!rel) {
402
+ continue;
403
+ }
404
+ anchorRel = rel;
405
+ continue;
406
+ }
407
+
388
408
  if (attributeName === 'srcset' && !isSafeSrcset(attributeValue, safeUriPattern)) {
389
409
  continue;
390
410
  }
391
411
 
412
+ if (attributeName === 'controlslist') {
413
+ const controlsList = sanitizeControlsList(attributeValue);
414
+ if (!controlsList) {
415
+ continue;
416
+ }
417
+ filteredAttributes.push(`${attributeName}="${controlsList}"`);
418
+ continue;
419
+ }
420
+
392
421
  if (normalizedTag === 'input' && attributeName === 'type' && attributeValue !== 'checkbox') {
393
422
  continue;
394
423
  }
@@ -397,6 +426,18 @@ function sanitizeHtml(html) {
397
426
  }
398
427
  }
399
428
 
429
+ if (normalizedTag === 'a') {
430
+ if (anchorTarget) {
431
+ filteredAttributes.push(`target="${anchorTarget}"`);
432
+ }
433
+ if (anchorTarget === '_blank') {
434
+ anchorRel = ensureBlankTargetRel(anchorRel);
435
+ }
436
+ if (anchorRel) {
437
+ filteredAttributes.push(`rel="${anchorRel}"`);
438
+ }
439
+ }
440
+
400
441
  const isSelfClosing = match.endsWith('/>') || normalizedTag === 'br' || normalizedTag === 'hr' || normalizedTag === 'img' || normalizedTag === 'input' || normalizedTag === 'source' || normalizedTag === 'track';
401
442
  const attributeSuffix = filteredAttributes.length > 0 ? ` ${filteredAttributes.join(' ')}` : '';
402
443
  return isSelfClosing ? `<${normalizedTag}${attributeSuffix} />` : `<${normalizedTag}${attributeSuffix}>`;
@@ -414,6 +455,52 @@ function sanitizeHtml(html) {
414
455
  .join('');
415
456
  }
416
457
 
458
+ function sanitizeAnchorTarget(value) {
459
+ const normalized = String(value).trim().toLowerCase();
460
+ return normalized === '_blank' ? '_blank' : '';
461
+ }
462
+
463
+ function sanitizeRelList(value) {
464
+ const allowedTokens = new Set(['noopener', 'noreferrer', 'nofollow', 'ugc', 'sponsored', 'external']);
465
+ const tokens = String(value)
466
+ .toLowerCase()
467
+ .split(/\s+/)
468
+ .filter((token, index, allTokens) => (
469
+ allowedTokens.has(token)
470
+ && allTokens.indexOf(token) === index
471
+ ));
472
+
473
+ return tokens.join(' ');
474
+ }
475
+
476
+ function ensureBlankTargetRel(value) {
477
+ const tokens = String(value)
478
+ .toLowerCase()
479
+ .split(/\s+/)
480
+ .filter(Boolean);
481
+
482
+ for (const requiredToken of ['noopener', 'noreferrer']) {
483
+ if (!tokens.includes(requiredToken)) {
484
+ tokens.push(requiredToken);
485
+ }
486
+ }
487
+
488
+ return tokens.join(' ');
489
+ }
490
+
491
+ function sanitizeControlsList(value) {
492
+ const allowedTokens = new Set(['nodownload', 'nofullscreen', 'noremoteplayback']);
493
+ const tokens = String(value)
494
+ .toLowerCase()
495
+ .split(/\s+/)
496
+ .filter((token, index, allTokens) => (
497
+ allowedTokens.has(token)
498
+ && allTokens.indexOf(token) === index
499
+ ));
500
+
501
+ return tokens.join(' ');
502
+ }
503
+
417
504
  function isSafeSrcset(value, safeUriPattern) {
418
505
  const candidates = String(value)
419
506
  .split(',')