@roottale/cms-core 0.46.0 → 0.53.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/CHANGELOG.md +24 -0
- package/README.md +2 -0
- package/dist/index.cjs +596 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +589 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -369,6 +369,478 @@ function faqPageJsonLd(entries) {
|
|
|
369
369
|
};
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
// src/image-alt.ts
|
|
373
|
+
var IMAGE_ALT_MAX_LENGTH = 160;
|
|
374
|
+
function normalizeImageAlt(value) {
|
|
375
|
+
if (typeof value !== "string") return "";
|
|
376
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
377
|
+
const characters = Array.from(normalized);
|
|
378
|
+
if (characters.length <= IMAGE_ALT_MAX_LENGTH) return normalized;
|
|
379
|
+
const visible = characters.slice(0, IMAGE_ALT_MAX_LENGTH - 1).join("").trimEnd();
|
|
380
|
+
return `${visible}\u2026`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/imported-html-sanitize.ts
|
|
384
|
+
import postcss from "postcss";
|
|
385
|
+
import sanitizeHtmlLibrary from "sanitize-html";
|
|
386
|
+
var IMPORT_SCOPE_RE = /^rt-import-[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
387
|
+
var ALLOWED_AT_RULES = /* @__PURE__ */ new Set([
|
|
388
|
+
"container",
|
|
389
|
+
"font-face",
|
|
390
|
+
"keyframes",
|
|
391
|
+
"-webkit-keyframes",
|
|
392
|
+
"layer",
|
|
393
|
+
"media",
|
|
394
|
+
"supports"
|
|
395
|
+
]);
|
|
396
|
+
var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src"]);
|
|
397
|
+
var ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
398
|
+
var UNSAFE_CSS_VALUE_RE = /(?:expression\s*\(|javascript\s*:|vbscript\s*:|@import|<\/?(?:style|script)|\\0|\\0000)/i;
|
|
399
|
+
var UNSAFE_CSS_PROPERTY_RE = /^(?:behavior|-moz-binding)$/i;
|
|
400
|
+
var FIXED_POSITION_RE = /^(?:fixed|sticky)$/i;
|
|
401
|
+
var ORIGINAL_H1_ATTRIBUTE = "data-rt-original-heading";
|
|
402
|
+
var SCOPE_ROOT_BOUNDARY_PROPERTIES = /* @__PURE__ */ new Set([
|
|
403
|
+
"bottom",
|
|
404
|
+
"inset",
|
|
405
|
+
"inset-block",
|
|
406
|
+
"inset-inline",
|
|
407
|
+
"left",
|
|
408
|
+
"position",
|
|
409
|
+
"right",
|
|
410
|
+
"rotate",
|
|
411
|
+
"scale",
|
|
412
|
+
"top",
|
|
413
|
+
"transform",
|
|
414
|
+
"translate",
|
|
415
|
+
"z-index"
|
|
416
|
+
]);
|
|
417
|
+
function fromCodePointSafe(value, radix) {
|
|
418
|
+
const codePoint = Number.parseInt(value, radix);
|
|
419
|
+
if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 1114111) {
|
|
420
|
+
return "";
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
return String.fromCodePoint(codePoint);
|
|
424
|
+
} catch {
|
|
425
|
+
return "";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function isSafeUrl(tagName, attributeName, value) {
|
|
429
|
+
const normalized = value.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => fromCodePointSafe(hex, 16)).replace(/&#(\d+);?/g, (_, decimal) => fromCodePointSafe(decimal, 10)).replace(/[\u0000-\u0020\u007f]/g, "").toLowerCase();
|
|
430
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(normalized)?.[1];
|
|
431
|
+
if (!scheme) return true;
|
|
432
|
+
if (ALLOWED_URL_SCHEMES.has(scheme)) return true;
|
|
433
|
+
return tagName === "img" && attributeName === "src" && scheme === "data" && normalized.startsWith("data:image/") && !normalized.startsWith("data:image/svg");
|
|
434
|
+
}
|
|
435
|
+
function cssUrlsAreSafe(value, allowedAssetUrls) {
|
|
436
|
+
const urlRe = /url\(\s*(["']?)([^)'"\s]+)\1\s*\)/gi;
|
|
437
|
+
let matched = false;
|
|
438
|
+
for (const match of value.matchAll(urlRe)) {
|
|
439
|
+
matched = true;
|
|
440
|
+
const url = match[2]?.trim() ?? "";
|
|
441
|
+
if (!url.startsWith("#") && !allowedAssetUrls.has(url)) return false;
|
|
442
|
+
}
|
|
443
|
+
return !/url\s*\(/i.test(value) || matched;
|
|
444
|
+
}
|
|
445
|
+
function declarationIsSafe(property, value, allowedAssetUrls) {
|
|
446
|
+
if (UNSAFE_CSS_PROPERTY_RE.test(property) || UNSAFE_CSS_VALUE_RE.test(value)) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
if (!cssUrlsAreSafe(value, allowedAssetUrls)) return false;
|
|
450
|
+
if (property === "position" && /\b(?:fixed|sticky)\b/i.test(value)) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
if (property.startsWith("--") && FIXED_POSITION_RE.test(value.trim())) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
if (property === "z-index") {
|
|
457
|
+
const normalized = value.trim().toLowerCase();
|
|
458
|
+
if (normalized !== "auto") {
|
|
459
|
+
const zIndex = Number(normalized);
|
|
460
|
+
if (!Number.isInteger(zIndex) || Math.abs(zIndex) > 100) return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const match of value.matchAll(/(-?\d+(?:\.\d+)?)(px|vh|vw)/gi)) {
|
|
464
|
+
const magnitude = Math.abs(Number(match[1]));
|
|
465
|
+
const limit = match[2].toLowerCase() === "px" ? 4e3 : 200;
|
|
466
|
+
if (magnitude > limit) return false;
|
|
467
|
+
}
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
function cleanCssContainer(container, allowedAssetUrls) {
|
|
471
|
+
container.walkComments((comment) => {
|
|
472
|
+
comment.remove();
|
|
473
|
+
});
|
|
474
|
+
container.walkAtRules((atRule) => {
|
|
475
|
+
const name = atRule.name.toLowerCase();
|
|
476
|
+
const isKeyframes = name === "keyframes" || name === "-webkit-keyframes";
|
|
477
|
+
if (!ALLOWED_AT_RULES.has(name) || !atRule.nodes?.length || name === "font-face" && atRule.parent?.type !== "root" || isKeyframes && !/^[a-z_][\w-]*$/i.test(atRule.params.trim()) || /<\/?(?:style|script)/i.test(atRule.params)) {
|
|
478
|
+
atRule.remove();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
container.walkDecls((declaration) => {
|
|
482
|
+
const property = declaration.prop.trim().toLowerCase();
|
|
483
|
+
if (!property || !declarationIsSafe(property, declaration.value, allowedAssetUrls)) {
|
|
484
|
+
declaration.remove();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
container.walkRules((rule) => {
|
|
489
|
+
if (!rule.selector.trim() || !rule.nodes?.length || /<\/?(?:style|script)/i.test(rule.selector)) {
|
|
490
|
+
rule.remove();
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
container.walkAtRules("font-face", (fontFace) => {
|
|
494
|
+
const declarations = fontFace.nodes ?? [];
|
|
495
|
+
const hasFamily = declarations.some(
|
|
496
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "font-family"
|
|
497
|
+
);
|
|
498
|
+
const hasSource = declarations.some(
|
|
499
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "src"
|
|
500
|
+
);
|
|
501
|
+
if (!hasFamily || !hasSource) fontFace.remove();
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
function sanitizeImportedCss(css, allowedAssetUrls = []) {
|
|
505
|
+
if (!css.trim()) return "";
|
|
506
|
+
try {
|
|
507
|
+
const root = postcss.parse(css);
|
|
508
|
+
cleanCssContainer(root, new Set(allowedAssetUrls));
|
|
509
|
+
return root.toString().trim();
|
|
510
|
+
} catch {
|
|
511
|
+
return "";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function sanitizeInlineStyle(style, allowedAssetUrls) {
|
|
515
|
+
try {
|
|
516
|
+
const root = postcss.parse(`x{${style}}`);
|
|
517
|
+
cleanCssContainer(root, allowedAssetUrls);
|
|
518
|
+
const rule = root.first;
|
|
519
|
+
if (!rule || rule.type !== "rule") return "";
|
|
520
|
+
return rule.nodes.map((node) => node.toString()).join(";");
|
|
521
|
+
} catch {
|
|
522
|
+
return "";
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function sanitizeImportedAttributes(tagName, attributes, allowedAssetUrls) {
|
|
526
|
+
const next = {};
|
|
527
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
528
|
+
const attributeName = name.toLowerCase();
|
|
529
|
+
if (attributeName === "style") {
|
|
530
|
+
const style = sanitizeInlineStyle(value, allowedAssetUrls);
|
|
531
|
+
if (style) next[name] = style;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (attributeName === "srcset") {
|
|
535
|
+
const srcset = sanitizeSrcset(value);
|
|
536
|
+
if (srcset) next[name] = srcset;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if ((attributeName === "fill" || attributeName === "stroke") && /url\s*\(/i.test(value) && !/^url\(#[a-z_][\w:.-]*\)$/i.test(value.trim())) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if ((tagName === "use" || tagName === "image") && (attributeName === "href" || attributeName === "xlink:href") && !value.trim().startsWith("#")) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
next[name] = URL_ATTRIBUTES.has(attributeName) && !isSafeUrl(tagName, attributeName, value) ? "#" : value;
|
|
546
|
+
}
|
|
547
|
+
if (tagName === "a" && next.target === "_blank") {
|
|
548
|
+
next.rel = "noopener noreferrer";
|
|
549
|
+
}
|
|
550
|
+
if (tagName === "h1") {
|
|
551
|
+
next[ORIGINAL_H1_ATTRIBUTE] = "h1";
|
|
552
|
+
return { tagName: "h2", attribs: next };
|
|
553
|
+
}
|
|
554
|
+
return { tagName, attribs: next };
|
|
555
|
+
}
|
|
556
|
+
function sanitizeSrcset(value) {
|
|
557
|
+
const candidates = [];
|
|
558
|
+
for (const rawCandidate of value.split(",")) {
|
|
559
|
+
const candidate = rawCandidate.trim();
|
|
560
|
+
if (!candidate) continue;
|
|
561
|
+
const [url, ...descriptorParts] = candidate.split(/\s+/);
|
|
562
|
+
if (!url || !isSafeUrl("img", "src", url)) continue;
|
|
563
|
+
const descriptor = descriptorParts.join(" ");
|
|
564
|
+
if (descriptor && !/^(?:\d+(?:\.\d+)?x|\d+w)$/.test(descriptor)) continue;
|
|
565
|
+
candidates.push(descriptor ? `${url} ${descriptor}` : url);
|
|
566
|
+
}
|
|
567
|
+
return candidates.join(", ");
|
|
568
|
+
}
|
|
569
|
+
var IMPORTED_HTML_OPTIONS = {
|
|
570
|
+
allowedTags: [
|
|
571
|
+
"a",
|
|
572
|
+
"abbr",
|
|
573
|
+
"address",
|
|
574
|
+
"article",
|
|
575
|
+
"aside",
|
|
576
|
+
"b",
|
|
577
|
+
"bdi",
|
|
578
|
+
"bdo",
|
|
579
|
+
"blockquote",
|
|
580
|
+
"br",
|
|
581
|
+
"caption",
|
|
582
|
+
"cite",
|
|
583
|
+
"code",
|
|
584
|
+
"col",
|
|
585
|
+
"colgroup",
|
|
586
|
+
"dd",
|
|
587
|
+
"del",
|
|
588
|
+
"details",
|
|
589
|
+
"dfn",
|
|
590
|
+
"div",
|
|
591
|
+
"dl",
|
|
592
|
+
"dt",
|
|
593
|
+
"em",
|
|
594
|
+
"figcaption",
|
|
595
|
+
"figure",
|
|
596
|
+
"footer",
|
|
597
|
+
"h1",
|
|
598
|
+
"h2",
|
|
599
|
+
"h3",
|
|
600
|
+
"h4",
|
|
601
|
+
"h5",
|
|
602
|
+
"h6",
|
|
603
|
+
"header",
|
|
604
|
+
"hr",
|
|
605
|
+
"i",
|
|
606
|
+
"img",
|
|
607
|
+
"kbd",
|
|
608
|
+
"li",
|
|
609
|
+
"main",
|
|
610
|
+
"mark",
|
|
611
|
+
"nav",
|
|
612
|
+
"ol",
|
|
613
|
+
"p",
|
|
614
|
+
"picture",
|
|
615
|
+
"pre",
|
|
616
|
+
"q",
|
|
617
|
+
"s",
|
|
618
|
+
"samp",
|
|
619
|
+
"section",
|
|
620
|
+
"small",
|
|
621
|
+
"source",
|
|
622
|
+
"svg",
|
|
623
|
+
"g",
|
|
624
|
+
"path",
|
|
625
|
+
"circle",
|
|
626
|
+
"ellipse",
|
|
627
|
+
"line",
|
|
628
|
+
"polyline",
|
|
629
|
+
"polygon",
|
|
630
|
+
"rect",
|
|
631
|
+
"defs",
|
|
632
|
+
"lineargradient",
|
|
633
|
+
"radialgradient",
|
|
634
|
+
"stop",
|
|
635
|
+
"use",
|
|
636
|
+
"clippath",
|
|
637
|
+
"mask",
|
|
638
|
+
"pattern",
|
|
639
|
+
"title",
|
|
640
|
+
"desc",
|
|
641
|
+
"span",
|
|
642
|
+
"strong",
|
|
643
|
+
"sub",
|
|
644
|
+
"summary",
|
|
645
|
+
"sup",
|
|
646
|
+
"table",
|
|
647
|
+
"tbody",
|
|
648
|
+
"td",
|
|
649
|
+
"tfoot",
|
|
650
|
+
"th",
|
|
651
|
+
"thead",
|
|
652
|
+
"time",
|
|
653
|
+
"tr",
|
|
654
|
+
"u",
|
|
655
|
+
"ul",
|
|
656
|
+
"var",
|
|
657
|
+
"wbr"
|
|
658
|
+
],
|
|
659
|
+
allowedAttributes: {
|
|
660
|
+
"*": ["aria-*", "class", "data-*", "dir", "id", "lang", "role", "style", "title"],
|
|
661
|
+
a: [
|
|
662
|
+
"href",
|
|
663
|
+
{ name: "rel", multiple: true, values: ["noopener", "noreferrer"] },
|
|
664
|
+
{ name: "target", values: ["_blank"] }
|
|
665
|
+
],
|
|
666
|
+
col: ["span", "style"],
|
|
667
|
+
img: ["alt", "height", "loading", "sizes", "src", "srcset", "style", "width"],
|
|
668
|
+
source: ["height", "media", "sizes", "src", "srcset", "type", "width"],
|
|
669
|
+
svg: ["aria-*", "class", "fill", "height", "preserveAspectRatio", "role", "stroke", "style", "viewBox", "viewbox", "width", "xmlns"],
|
|
670
|
+
g: ["class", "fill", "stroke", "stroke-width", "style", "transform"],
|
|
671
|
+
path: ["class", "d", "fill", "fill-rule", "stroke", "stroke-linecap", "stroke-linejoin", "stroke-width", "style", "transform"],
|
|
672
|
+
circle: ["class", "cx", "cy", "fill", "r", "stroke", "stroke-width", "style", "transform"],
|
|
673
|
+
ellipse: ["class", "cx", "cy", "fill", "rx", "ry", "stroke", "stroke-width", "style", "transform"],
|
|
674
|
+
line: ["class", "stroke", "stroke-linecap", "stroke-width", "style", "transform", "x1", "x2", "y1", "y2"],
|
|
675
|
+
polyline: ["class", "fill", "points", "stroke", "stroke-linecap", "stroke-linejoin", "stroke-width", "style", "transform"],
|
|
676
|
+
polygon: ["class", "fill", "points", "stroke", "stroke-width", "style", "transform"],
|
|
677
|
+
rect: ["class", "fill", "height", "rx", "ry", "stroke", "stroke-width", "style", "transform", "width", "x", "y"],
|
|
678
|
+
lineargradient: ["gradientTransform", "gradientUnits", "id", "spreadMethod", "x1", "x2", "y1", "y2"],
|
|
679
|
+
radialgradient: ["cx", "cy", "fx", "fy", "gradientTransform", "gradientUnits", "id", "r", "spreadMethod"],
|
|
680
|
+
stop: ["offset", "stop-color", "stop-opacity", "style"],
|
|
681
|
+
use: ["fill", "height", "href", "stroke", "style", "width", "x", "xlink:href", "y"],
|
|
682
|
+
clippath: ["clipPathUnits", "id", "transform"],
|
|
683
|
+
mask: ["height", "id", "maskContentUnits", "maskUnits", "width", "x", "y"],
|
|
684
|
+
pattern: ["height", "id", "patternContentUnits", "patternTransform", "patternUnits", "preserveAspectRatio", "viewBox", "width", "x", "y"],
|
|
685
|
+
td: ["colspan", "rowspan", "style"],
|
|
686
|
+
th: [
|
|
687
|
+
"colspan",
|
|
688
|
+
"rowspan",
|
|
689
|
+
"style",
|
|
690
|
+
{ name: "scope", values: ["col", "row", "colgroup", "rowgroup"] }
|
|
691
|
+
],
|
|
692
|
+
time: ["datetime"]
|
|
693
|
+
},
|
|
694
|
+
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
695
|
+
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
|
696
|
+
allowedSchemesAppliedToAttributes: ["href", "src"],
|
|
697
|
+
allowProtocolRelative: false,
|
|
698
|
+
nonTextTags: ["script", "style", "textarea", "option", "iframe", "object", "embed", "foreignobject", "math", "template"]
|
|
699
|
+
};
|
|
700
|
+
function sanitizeImportedHtml(html, allowedAssetUrls = []) {
|
|
701
|
+
if (!html) return html;
|
|
702
|
+
const allowedSet = new Set(allowedAssetUrls);
|
|
703
|
+
return sanitizeHtmlLibrary(html, {
|
|
704
|
+
...IMPORTED_HTML_OPTIONS,
|
|
705
|
+
transformTags: {
|
|
706
|
+
"*": (tagName, attributes) => sanitizeImportedAttributes(tagName, attributes, allowedSet)
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function ensureImportedHtmlDocumentRoots(html, allowedAssetUrls = []) {
|
|
711
|
+
const safeHtml = sanitizeImportedHtml(html, allowedAssetUrls).trim();
|
|
712
|
+
if (!safeHtml) return safeHtml;
|
|
713
|
+
if (/^<div\b[^>]*\bdata-rt-import-document(?:=(?:""|'')|(?=[\s>]))[^>]*>\s*<div\b[^>]*\bdata-rt-import-body(?:=(?:""|'')|(?=[\s>]))/i.test(
|
|
714
|
+
safeHtml
|
|
715
|
+
)) {
|
|
716
|
+
return safeHtml;
|
|
717
|
+
}
|
|
718
|
+
return `<div data-rt-import-document><div data-rt-import-body>${safeHtml}</div></div>`;
|
|
719
|
+
}
|
|
720
|
+
function normalizeDocumentSelector(selector) {
|
|
721
|
+
const typeBoundary = "(?=$|[\\s>+~,.#:\\[\\)])";
|
|
722
|
+
return selector.replace(/html\s*>\s*body/gi, "[data-rt-import-document] > [data-rt-import-body]").replace(/html\s+body/gi, "[data-rt-import-document] [data-rt-import-body]").replace(new RegExp(`(^|[\\s>+~,\\(])html${typeBoundary}`, "gi"), "$1[data-rt-import-document]").replace(new RegExp(`(^|[\\s>+~,\\(])body${typeBoundary}`, "gi"), "$1[data-rt-import-body]").replace(
|
|
723
|
+
new RegExp(`(^|[\\s>+~,\\(])h1${typeBoundary}`, "gi"),
|
|
724
|
+
`$1[${ORIGINAL_H1_ATTRIBUTE}="h1"]`
|
|
725
|
+
).replace(/:root/g, "[data-rt-import-document]");
|
|
726
|
+
}
|
|
727
|
+
function selectorTargetsDocumentBoundary(selector) {
|
|
728
|
+
return selector.split(",").some((part) => {
|
|
729
|
+
const normalized = part.trim();
|
|
730
|
+
const targetsDocumentRoot = normalized.startsWith("[data-rt-import-document]") && !/[\s>+~]/.test(normalized.slice("[data-rt-import-document]".length));
|
|
731
|
+
const targetsBodyRoot = /\[data-rt-import-body\](?:[.#:][\w-]+|\[[^\]]+\])*\s*$/.test(
|
|
732
|
+
normalized
|
|
733
|
+
);
|
|
734
|
+
return targetsDocumentRoot || targetsBodyRoot;
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function replaceAnimationName(value, names) {
|
|
738
|
+
let next = value;
|
|
739
|
+
for (const [name, scopedName] of names) {
|
|
740
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
741
|
+
next = next.replace(new RegExp(`(^|[^\\w-])${escaped}(?=$|[^\\w-])`, "g"), `$1${scopedName}`);
|
|
742
|
+
}
|
|
743
|
+
return next;
|
|
744
|
+
}
|
|
745
|
+
function unquoteCssIdentifier(value) {
|
|
746
|
+
const normalized = value.trim();
|
|
747
|
+
const match = /^(?:"([^"]+)"|'([^']+)'|([a-z_][\w -]*))$/i.exec(normalized);
|
|
748
|
+
return (match?.[1] ?? match?.[2] ?? match?.[3] ?? "").trim() || null;
|
|
749
|
+
}
|
|
750
|
+
function replaceFontFamilyName(value, names) {
|
|
751
|
+
let next = value;
|
|
752
|
+
for (const [name, scopedName] of names) {
|
|
753
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
754
|
+
next = next.replace(
|
|
755
|
+
new RegExp(`(["'])${escaped}\\1|(^|[,\\s])${escaped}(?=$|[,\\s/])`, "gi"),
|
|
756
|
+
(matched, quote, prefix) => quote ? `${quote}${scopedName}${quote}` : `${prefix ?? ""}${scopedName}`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return next;
|
|
760
|
+
}
|
|
761
|
+
function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
762
|
+
if (!IMPORT_SCOPE_RE.test(scope)) return "";
|
|
763
|
+
const safeCss = sanitizeImportedCss(css, allowedAssetUrls);
|
|
764
|
+
if (!safeCss) return "";
|
|
765
|
+
try {
|
|
766
|
+
const root = postcss.parse(safeCss);
|
|
767
|
+
const keyframeNames = /* @__PURE__ */ new Map();
|
|
768
|
+
const fontFamilyNames = /* @__PURE__ */ new Map();
|
|
769
|
+
const fontFaces = root.nodes.filter(
|
|
770
|
+
(node) => node.type === "atrule" && node.name.toLowerCase() === "font-face"
|
|
771
|
+
);
|
|
772
|
+
for (const fontFace of fontFaces) {
|
|
773
|
+
if (fontFace.type !== "atrule") continue;
|
|
774
|
+
const family = fontFace.nodes?.find(
|
|
775
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "font-family"
|
|
776
|
+
);
|
|
777
|
+
if (!family || family.type !== "decl") {
|
|
778
|
+
fontFace.remove();
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const originalName = unquoteCssIdentifier(family.value);
|
|
782
|
+
const hasSupportedSource = fontFace.nodes?.some(
|
|
783
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "src" && /url\(\s*["']?https:\/\/[^)'"\s]+\.woff2?(?:[?#][^)'"\s]*)?["']?\s*\)/i.test(
|
|
784
|
+
node.value
|
|
785
|
+
)
|
|
786
|
+
);
|
|
787
|
+
if (!originalName || !hasSupportedSource) {
|
|
788
|
+
fontFace.remove();
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const scopedName = `${scope}-${originalName}`;
|
|
792
|
+
fontFamilyNames.set(originalName, scopedName);
|
|
793
|
+
family.value = family.value.replace(originalName, scopedName);
|
|
794
|
+
}
|
|
795
|
+
root.walkAtRules((atRule) => {
|
|
796
|
+
const name = atRule.name.toLowerCase();
|
|
797
|
+
if (name !== "keyframes" && name !== "-webkit-keyframes") return;
|
|
798
|
+
const originalName = atRule.params.trim();
|
|
799
|
+
const scopedName = `${scope}-${originalName}`;
|
|
800
|
+
keyframeNames.set(originalName, scopedName);
|
|
801
|
+
atRule.params = scopedName;
|
|
802
|
+
});
|
|
803
|
+
root.walkDecls((declaration) => {
|
|
804
|
+
const property = declaration.prop.trim().toLowerCase();
|
|
805
|
+
if (property === "animation" || property === "animation-name") {
|
|
806
|
+
declaration.value = replaceAnimationName(declaration.value, keyframeNames);
|
|
807
|
+
}
|
|
808
|
+
if (property === "font-family" || property === "font") {
|
|
809
|
+
declaration.value = replaceFontFamilyName(
|
|
810
|
+
declaration.value,
|
|
811
|
+
fontFamilyNames
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
root.walkRules((rule) => {
|
|
816
|
+
const parent = rule.parent;
|
|
817
|
+
if (parent?.type === "atrule" && (parent.name.toLowerCase() === "keyframes" || parent.name.toLowerCase() === "-webkit-keyframes")) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
rule.selector = normalizeDocumentSelector(rule.selector);
|
|
821
|
+
if (selectorTargetsDocumentBoundary(rule.selector)) {
|
|
822
|
+
rule.walkDecls((declaration) => {
|
|
823
|
+
if (SCOPE_ROOT_BOUNDARY_PROPERTIES.has(
|
|
824
|
+
declaration.prop.trim().toLowerCase()
|
|
825
|
+
)) {
|
|
826
|
+
declaration.remove();
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
const scoped = postcss.atRule({
|
|
832
|
+
name: "scope",
|
|
833
|
+
params: `([data-rt-import-scope="${scope}"])`
|
|
834
|
+
});
|
|
835
|
+
const preservedFontFaces = fontFaces.filter((fontFace) => fontFace.parent);
|
|
836
|
+
for (const fontFace of preservedFontFaces) fontFace.remove();
|
|
837
|
+
scoped.append(root.nodes);
|
|
838
|
+
return [...preservedFontFaces.map((fontFace) => fontFace.toString()), scoped.toString()].filter(Boolean).join("\n");
|
|
839
|
+
} catch {
|
|
840
|
+
return "";
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
372
844
|
// src/preview-token.ts
|
|
373
845
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
374
846
|
var DELIMITER = ".";
|
|
@@ -404,15 +876,58 @@ function verifyPreviewToken(secret, token) {
|
|
|
404
876
|
}
|
|
405
877
|
|
|
406
878
|
// src/sanitize.ts
|
|
407
|
-
import
|
|
879
|
+
import sanitizeHtmlLibrary2 from "sanitize-html";
|
|
408
880
|
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href"]);
|
|
409
|
-
var
|
|
881
|
+
var ALLOWED_URL_SCHEMES2 = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
410
882
|
var MALFORMED_DANGEROUS_TOKEN_RE = /<\/?[^>\s/]*(?:script|style|iframe|object|embed|svg|math|template)[^>\s/]*(?:\s[^>]*)?>[\s\S]*?<\/?[^>\s/]*(?:script|style|iframe|object|embed|svg|math|template)[^>\s/]*(?:\s[^>]*)?>/gi;
|
|
411
883
|
var SLASH_EVENT_ATTR_IN_URL_RE = /\/on[a-z]+=/i;
|
|
412
884
|
var ROOTTALE_CLASS_RE = /^rt-[a-z0-9_-]+$/;
|
|
413
885
|
var HEX_COLOR_RE = /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i;
|
|
414
886
|
var TEXT_ALIGN_RE = /^(left|right|center|justify)$/;
|
|
415
887
|
var ASPECT_RATIO_RE = /^\d+(?:\.\d+)?\s*\/\s*\d+(?:\.\d+)?$/;
|
|
888
|
+
var CSS_LENGTH_RE = /^(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))$/i;
|
|
889
|
+
var CSS_LENGTH_LIST_RE = /^(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))(?:\s+(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))){0,3}$/i;
|
|
890
|
+
var CSS_LINE_HEIGHT_RE = /^(?:normal|\d+(?:\.\d+)?(?:px|em|rem|%)?)$/i;
|
|
891
|
+
var CSS_FONT_WEIGHT_RE = /^(?:normal|bold|[1-9]00)$/i;
|
|
892
|
+
var CSS_BORDER_RE = /^(?:none|0|(?:\d(?:\.\d+)?|[12]\d(?:\.\d+)?|3[0-2](?:\.0+)?)px\s+(?:solid|dashed|dotted)\s+#[0-9a-f]{3}(?:[0-9a-f]{3})?)$/i;
|
|
893
|
+
var CSS_TEXT_DECORATION_RE = /^(?:none|underline|line-through)$/i;
|
|
894
|
+
var CSS_MAGNITUDE_LIMITS = {
|
|
895
|
+
"font-size": { "": 0, px: 96, em: 5, rem: 5, "%": 200 },
|
|
896
|
+
"line-height": { "": 5, px: 96, em: 5, rem: 5, "%": 500 },
|
|
897
|
+
margin: { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
898
|
+
"margin-bottom": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
899
|
+
"margin-left": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
900
|
+
"margin-right": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
901
|
+
"margin-top": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
902
|
+
padding: { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
903
|
+
"padding-bottom": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
904
|
+
"padding-left": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
905
|
+
"padding-right": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
906
|
+
"padding-top": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
907
|
+
width: { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 },
|
|
908
|
+
"max-width": { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 },
|
|
909
|
+
height: { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 }
|
|
910
|
+
};
|
|
911
|
+
function hasSafeCssMagnitude(property, value) {
|
|
912
|
+
const limits = CSS_MAGNITUDE_LIMITS[property];
|
|
913
|
+
if (!limits) return true;
|
|
914
|
+
const numericTokens = value.matchAll(/(\d+(?:\.\d+)?)(px|em|rem|%)?/gi);
|
|
915
|
+
for (const match of numericTokens) {
|
|
916
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
917
|
+
const limit = limits[unit];
|
|
918
|
+
if (limit === void 0 || Number(match[1]) > limit) return false;
|
|
919
|
+
}
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
function filterUnsafeCssMagnitudes(style) {
|
|
923
|
+
return style.split(";").map((declaration) => declaration.trim()).filter(Boolean).filter((declaration) => {
|
|
924
|
+
const colon = declaration.indexOf(":");
|
|
925
|
+
if (colon < 1) return false;
|
|
926
|
+
const property = declaration.slice(0, colon).trim().toLowerCase();
|
|
927
|
+
const value = declaration.slice(colon + 1).trim();
|
|
928
|
+
return hasSafeCssMagnitude(property, value);
|
|
929
|
+
}).join(";");
|
|
930
|
+
}
|
|
416
931
|
var NAMED_ENTITY = {
|
|
417
932
|
colon: ":",
|
|
418
933
|
tab: " ",
|
|
@@ -425,7 +940,7 @@ var NAMED_ENTITY = {
|
|
|
425
940
|
lpar: "(",
|
|
426
941
|
rpar: ")"
|
|
427
942
|
};
|
|
428
|
-
function
|
|
943
|
+
function fromCodePointSafe2(n) {
|
|
429
944
|
if (!Number.isFinite(n) || n < 0 || n > 1114111) return "";
|
|
430
945
|
try {
|
|
431
946
|
return String.fromCodePoint(n);
|
|
@@ -434,7 +949,7 @@ function fromCodePointSafe(n) {
|
|
|
434
949
|
}
|
|
435
950
|
}
|
|
436
951
|
function decodeUrlProbe(value) {
|
|
437
|
-
const decoded = value.replace(/&#x([0-9a-f]+);?/gi, (_, h) =>
|
|
952
|
+
const decoded = value.replace(/&#x([0-9a-f]+);?/gi, (_, h) => fromCodePointSafe2(parseInt(h, 16))).replace(/&#(\d+);?/g, (_, d) => fromCodePointSafe2(parseInt(d, 10))).replace(/&([a-z]+);/gi, (m, name) => NAMED_ENTITY[name.toLowerCase()] ?? m);
|
|
438
953
|
return [...decoded].filter((c) => {
|
|
439
954
|
const n = c.codePointAt(0) ?? 0;
|
|
440
955
|
return n > 32 && n !== 127;
|
|
@@ -454,7 +969,7 @@ function isSafeUrlValue(tag, attr, value) {
|
|
|
454
969
|
if (SLASH_EVENT_ATTR_IN_URL_RE.test(probe)) return false;
|
|
455
970
|
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe)?.[1];
|
|
456
971
|
if (!scheme) return true;
|
|
457
|
-
if (
|
|
972
|
+
if (ALLOWED_URL_SCHEMES2.has(scheme)) return true;
|
|
458
973
|
if (scheme === "data" && tag === "img" && attr === "src" && probe.startsWith("data:image/")) {
|
|
459
974
|
return !probe.startsWith("data:image/svg");
|
|
460
975
|
}
|
|
@@ -464,8 +979,16 @@ function sanitizeUrlAttributes(tagName, attribs) {
|
|
|
464
979
|
const next = {};
|
|
465
980
|
for (const [name, value] of Object.entries(attribs)) {
|
|
466
981
|
const attr = name.toLowerCase();
|
|
982
|
+
if (attr === "style") {
|
|
983
|
+
const style = filterUnsafeCssMagnitudes(value);
|
|
984
|
+
if (style) next[name] = style;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
467
987
|
next[name] = URL_ATTRS.has(attr) && !isSafeUrlValue(tagName, attr, value) ? "#" : value;
|
|
468
988
|
}
|
|
989
|
+
if (tagName === "a" && next.target === "_blank") {
|
|
990
|
+
next.rel = "noopener noreferrer";
|
|
991
|
+
}
|
|
469
992
|
return { tagName, attribs: next };
|
|
470
993
|
}
|
|
471
994
|
var SANITIZE_OPTIONS = {
|
|
@@ -491,19 +1014,25 @@ var SANITIZE_OPTIONS = {
|
|
|
491
1014
|
"p",
|
|
492
1015
|
"pre",
|
|
493
1016
|
"s",
|
|
1017
|
+
"section",
|
|
1018
|
+
"small",
|
|
494
1019
|
"span",
|
|
495
1020
|
"strong",
|
|
1021
|
+
"sub",
|
|
496
1022
|
"summary",
|
|
1023
|
+
"sup",
|
|
497
1024
|
"table",
|
|
498
1025
|
"tbody",
|
|
499
1026
|
"td",
|
|
1027
|
+
"tfoot",
|
|
500
1028
|
"th",
|
|
1029
|
+
"thead",
|
|
501
1030
|
"tr",
|
|
502
1031
|
"u",
|
|
503
1032
|
"ul"
|
|
504
1033
|
],
|
|
505
1034
|
allowedAttributes: {
|
|
506
|
-
"*": ["class", "data-*"],
|
|
1035
|
+
"*": ["class", "data-*", "style"],
|
|
507
1036
|
a: [
|
|
508
1037
|
"href",
|
|
509
1038
|
"title",
|
|
@@ -520,7 +1049,14 @@ var SANITIZE_OPTIONS = {
|
|
|
520
1049
|
p: ["class", "data-*", "style"],
|
|
521
1050
|
span: ["class", "data-*", "style"],
|
|
522
1051
|
td: ["colspan", "rowspan", "class", "data-*"],
|
|
523
|
-
|
|
1052
|
+
// scope 는 표 머리행의 접근성 의미(스크린리더 열/행 연결) — 값 집합을 고정해 허용.
|
|
1053
|
+
th: [
|
|
1054
|
+
"colspan",
|
|
1055
|
+
"rowspan",
|
|
1056
|
+
"class",
|
|
1057
|
+
"data-*",
|
|
1058
|
+
{ name: "scope", values: ["col", "row", "colgroup", "rowgroup"] }
|
|
1059
|
+
]
|
|
524
1060
|
},
|
|
525
1061
|
allowedClasses: {
|
|
526
1062
|
"*": [ROOTTALE_CLASS_RE]
|
|
@@ -530,13 +1066,38 @@ var SANITIZE_OPTIONS = {
|
|
|
530
1066
|
"aspect-ratio": [ASPECT_RATIO_RE],
|
|
531
1067
|
background: [HEX_COLOR_RE],
|
|
532
1068
|
"background-color": [HEX_COLOR_RE],
|
|
1069
|
+
border: [CSS_BORDER_RE],
|
|
1070
|
+
"border-bottom": [CSS_BORDER_RE],
|
|
1071
|
+
"border-left": [CSS_BORDER_RE],
|
|
1072
|
+
"border-right": [CSS_BORDER_RE],
|
|
1073
|
+
"border-top": [CSS_BORDER_RE],
|
|
1074
|
+
"border-collapse": [/^(?:collapse|separate)$/i],
|
|
533
1075
|
color: [HEX_COLOR_RE],
|
|
534
|
-
"
|
|
1076
|
+
"font-size": [CSS_LENGTH_RE],
|
|
1077
|
+
"font-weight": [CSS_FONT_WEIGHT_RE],
|
|
1078
|
+
height: [CSS_LENGTH_RE, /^auto$/i],
|
|
1079
|
+
"line-height": [CSS_LINE_HEIGHT_RE],
|
|
1080
|
+
margin: [CSS_LENGTH_LIST_RE],
|
|
1081
|
+
"margin-bottom": [CSS_LENGTH_RE],
|
|
1082
|
+
"margin-left": [CSS_LENGTH_RE],
|
|
1083
|
+
"margin-right": [CSS_LENGTH_RE],
|
|
1084
|
+
"margin-top": [CSS_LENGTH_RE],
|
|
1085
|
+
"max-width": [CSS_LENGTH_RE, /^none$/i],
|
|
1086
|
+
padding: [CSS_LENGTH_LIST_RE],
|
|
1087
|
+
"padding-bottom": [CSS_LENGTH_RE],
|
|
1088
|
+
"padding-left": [CSS_LENGTH_RE],
|
|
1089
|
+
"padding-right": [CSS_LENGTH_RE],
|
|
1090
|
+
"padding-top": [CSS_LENGTH_RE],
|
|
1091
|
+
"text-align": [TEXT_ALIGN_RE],
|
|
1092
|
+
"text-decoration": [CSS_TEXT_DECORATION_RE],
|
|
1093
|
+
width: [CSS_LENGTH_RE, /^auto$/i]
|
|
535
1094
|
}
|
|
536
1095
|
},
|
|
537
1096
|
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
538
1097
|
allowedSchemesByTag: {
|
|
539
|
-
|
|
1098
|
+
// tag별 목록은 전역 allowedSchemes를 덮어쓴다. data만 쓰면 정상적인
|
|
1099
|
+
// http/https 이미지 src까지 제거되므로 이미지가 사용할 전체 scheme을 명시한다.
|
|
1100
|
+
img: ["http", "https", "data"]
|
|
540
1101
|
},
|
|
541
1102
|
allowedSchemesAppliedToAttributes: ["href", "src", "xlink:href"],
|
|
542
1103
|
allowProtocolRelative: false,
|
|
@@ -558,7 +1119,18 @@ var SANITIZE_OPTIONS = {
|
|
|
558
1119
|
};
|
|
559
1120
|
function sanitizeHtml(html) {
|
|
560
1121
|
if (!html) return html;
|
|
561
|
-
return
|
|
1122
|
+
return sanitizeHtmlLibrary2(stripMalformedDangerousTokens(html), SANITIZE_OPTIONS);
|
|
1123
|
+
}
|
|
1124
|
+
function htmlToPlainText(html) {
|
|
1125
|
+
const safeHtml = sanitizeHtml(html);
|
|
1126
|
+
const withBlockBoundaries = safeHtml.replace(
|
|
1127
|
+
/<(?:br|hr)\b[^>]*>|<\/(?:blockquote|details|div|figcaption|figure|h[2-4]|li|p|pre|section|summary|table|tbody|td|th|tr)>/gi,
|
|
1128
|
+
" "
|
|
1129
|
+
);
|
|
1130
|
+
return sanitizeHtmlLibrary2(withBlockBoundaries, {
|
|
1131
|
+
allowedTags: [],
|
|
1132
|
+
allowedAttributes: {}
|
|
1133
|
+
}).replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
|
|
562
1134
|
}
|
|
563
1135
|
var PHASE_1_CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://imagedelivery.net https://*.r2.cloudflarestorage.com; font-src 'self' data:; connect-src 'self'; frame-src https://www.youtube-nocookie.com https://www.youtube.com https://player.vimeo.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
|
|
564
1136
|
|
|
@@ -580,6 +1152,7 @@ export {
|
|
|
580
1152
|
BLOCK_NAME_PATTERN,
|
|
581
1153
|
FIELD_BINDING_SOURCE,
|
|
582
1154
|
FIELD_BINDING_SOURCE_ALIAS,
|
|
1155
|
+
IMAGE_ALT_MAX_LENGTH,
|
|
583
1156
|
PHASE_1_CORE_BLOCKS,
|
|
584
1157
|
PHASE_1_CSP,
|
|
585
1158
|
applyBlockBindings,
|
|
@@ -589,9 +1162,11 @@ export {
|
|
|
589
1162
|
boundFieldValue,
|
|
590
1163
|
chainResolvers,
|
|
591
1164
|
createFieldBindingResolver,
|
|
1165
|
+
ensureImportedHtmlDocumentRoots,
|
|
592
1166
|
ensureStableIds,
|
|
593
1167
|
extractFaqEntries,
|
|
594
1168
|
faqPageJsonLd,
|
|
1169
|
+
htmlToPlainText,
|
|
595
1170
|
injectStableIdsTree,
|
|
596
1171
|
isAllowedEmbedSrc,
|
|
597
1172
|
isAllowedMapEmbedSrc,
|
|
@@ -599,6 +1174,7 @@ export {
|
|
|
599
1174
|
isValidBlock,
|
|
600
1175
|
issuePreviewToken,
|
|
601
1176
|
normalizeEmbed,
|
|
1177
|
+
normalizeImageAlt,
|
|
602
1178
|
parseBlock,
|
|
603
1179
|
postsForCollection,
|
|
604
1180
|
postsInAnyCollection,
|
|
@@ -607,6 +1183,9 @@ export {
|
|
|
607
1183
|
resolvePostPath,
|
|
608
1184
|
resolvePostUrl,
|
|
609
1185
|
sanitizeHtml,
|
|
1186
|
+
sanitizeImportedCss,
|
|
1187
|
+
sanitizeImportedHtml,
|
|
1188
|
+
scopeImportedCss,
|
|
610
1189
|
serializeBlock,
|
|
611
1190
|
validateCollections,
|
|
612
1191
|
verifyPreviewToken
|