@zeropress/build-core 0.6.8 → 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.8",
3
+ "version": "0.6.11",
4
4
  "description": "Shared ZeroPress build core",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -329,18 +329,22 @@ function sanitizeHtml(html) {
329
329
  'ul', 'ol', 'li',
330
330
  'blockquote', 'aside',
331
331
  'figure', 'figcaption', 'picture', 'source',
332
+ 'video', 'audio', 'track',
332
333
  'table', 'thead', 'tbody', 'tr', 'th', 'td',
333
334
  'div', 'span', 'nav',
334
335
  'iframe', 'input',
335
336
  ]);
336
337
 
337
338
  const allowedAttributes = {
338
- a: new Set(['href', 'title', 'class', 'id']),
339
+ a: new Set(['href', 'title', 'class', 'id', 'target', 'rel']),
339
340
  aside: new Set(['role', 'class', 'id']),
340
341
  img: new Set(['src', 'srcset', 'sizes', 'alt', 'title', 'class', 'id', 'width', 'height', 'loading', 'decoding']),
341
- iframe: new Set(['src', 'width', 'height', 'frameborder', 'allowfullscreen', 'class']),
342
+ iframe: new Set(['src', 'width', 'height', 'frameborder', 'allowfullscreen', 'class', 'title']),
342
343
  input: new Set(['type', 'checked', 'disabled', 'class', 'id', 'aria-label']),
343
344
  source: new Set(['src', 'srcset', 'sizes', 'type', 'media', 'width', 'height', 'class', 'id']),
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
+ track: new Set(['src', 'kind', 'srclang', 'label', 'default', 'class', 'id']),
344
348
  '*': new Set(['class', 'id']),
345
349
  };
346
350
 
@@ -364,6 +368,8 @@ function sanitizeHtml(html) {
364
368
  const tagAllowedAttributes = allowedAttributes[normalizedTag] || new Set();
365
369
  const globalAllowedAttributes = allowedAttributes['*'];
366
370
  const filteredAttributes = [];
371
+ let anchorTarget = '';
372
+ let anchorRel = '';
367
373
 
368
374
  if (attributeString) {
369
375
  const attributePattern = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
@@ -377,7 +383,25 @@ function sanitizeHtml(html) {
377
383
  continue;
378
384
  }
379
385
 
380
- if ((attributeName === 'href' || attributeName === 'src') && !safeUriPattern.test(attributeValue)) {
386
+ if ((attributeName === 'href' || attributeName === 'src' || attributeName === 'poster') && !safeUriPattern.test(attributeValue)) {
387
+ continue;
388
+ }
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;
381
405
  continue;
382
406
  }
383
407
 
@@ -385,6 +409,15 @@ function sanitizeHtml(html) {
385
409
  continue;
386
410
  }
387
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
+
388
421
  if (normalizedTag === 'input' && attributeName === 'type' && attributeValue !== 'checkbox') {
389
422
  continue;
390
423
  }
@@ -393,7 +426,19 @@ function sanitizeHtml(html) {
393
426
  }
394
427
  }
395
428
 
396
- const isSelfClosing = match.endsWith('/>') || normalizedTag === 'br' || normalizedTag === 'hr' || normalizedTag === 'img' || normalizedTag === 'input' || normalizedTag === 'source';
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
+
441
+ const isSelfClosing = match.endsWith('/>') || normalizedTag === 'br' || normalizedTag === 'hr' || normalizedTag === 'img' || normalizedTag === 'input' || normalizedTag === 'source' || normalizedTag === 'track';
397
442
  const attributeSuffix = filteredAttributes.length > 0 ? ` ${filteredAttributes.join(' ')}` : '';
398
443
  return isSelfClosing ? `<${normalizedTag}${attributeSuffix} />` : `<${normalizedTag}${attributeSuffix}>`;
399
444
  });
@@ -410,6 +455,52 @@ function sanitizeHtml(html) {
410
455
  .join('');
411
456
  }
412
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
+
413
504
  function isSafeSrcset(value, safeUriPattern) {
414
505
  const candidates = String(value)
415
506
  .split(',')