@roottale/cms-core 0.53.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 +9 -0
- package/README.md +1 -0
- package/dist/index.cjs +578 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +573 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -43,9 +43,11 @@ __export(src_exports, {
|
|
|
43
43
|
boundFieldValue: () => boundFieldValue,
|
|
44
44
|
chainResolvers: () => chainResolvers,
|
|
45
45
|
createFieldBindingResolver: () => createFieldBindingResolver,
|
|
46
|
+
ensureImportedHtmlDocumentRoots: () => ensureImportedHtmlDocumentRoots,
|
|
46
47
|
ensureStableIds: () => ensureStableIds,
|
|
47
48
|
extractFaqEntries: () => extractFaqEntries,
|
|
48
49
|
faqPageJsonLd: () => faqPageJsonLd,
|
|
50
|
+
htmlToPlainText: () => htmlToPlainText,
|
|
49
51
|
injectStableIdsTree: () => injectStableIdsTree,
|
|
50
52
|
isAllowedEmbedSrc: () => isAllowedEmbedSrc,
|
|
51
53
|
isAllowedMapEmbedSrc: () => isAllowedMapEmbedSrc,
|
|
@@ -62,6 +64,9 @@ __export(src_exports, {
|
|
|
62
64
|
resolvePostPath: () => resolvePostPath,
|
|
63
65
|
resolvePostUrl: () => resolvePostUrl,
|
|
64
66
|
sanitizeHtml: () => sanitizeHtml,
|
|
67
|
+
sanitizeImportedCss: () => sanitizeImportedCss,
|
|
68
|
+
sanitizeImportedHtml: () => sanitizeImportedHtml,
|
|
69
|
+
scopeImportedCss: () => scopeImportedCss,
|
|
65
70
|
serializeBlock: () => serializeBlock,
|
|
66
71
|
validateCollections: () => validateCollections,
|
|
67
72
|
verifyPreviewToken: () => verifyPreviewToken
|
|
@@ -450,6 +455,467 @@ function normalizeImageAlt(value) {
|
|
|
450
455
|
return `${visible}\u2026`;
|
|
451
456
|
}
|
|
452
457
|
|
|
458
|
+
// src/imported-html-sanitize.ts
|
|
459
|
+
var import_postcss = __toESM(require("postcss"), 1);
|
|
460
|
+
var import_sanitize_html = __toESM(require("sanitize-html"), 1);
|
|
461
|
+
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;
|
|
462
|
+
var ALLOWED_AT_RULES = /* @__PURE__ */ new Set([
|
|
463
|
+
"container",
|
|
464
|
+
"font-face",
|
|
465
|
+
"keyframes",
|
|
466
|
+
"-webkit-keyframes",
|
|
467
|
+
"layer",
|
|
468
|
+
"media",
|
|
469
|
+
"supports"
|
|
470
|
+
]);
|
|
471
|
+
var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src"]);
|
|
472
|
+
var ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
473
|
+
var UNSAFE_CSS_VALUE_RE = /(?:expression\s*\(|javascript\s*:|vbscript\s*:|@import|<\/?(?:style|script)|\\0|\\0000)/i;
|
|
474
|
+
var UNSAFE_CSS_PROPERTY_RE = /^(?:behavior|-moz-binding)$/i;
|
|
475
|
+
var FIXED_POSITION_RE = /^(?:fixed|sticky)$/i;
|
|
476
|
+
var ORIGINAL_H1_ATTRIBUTE = "data-rt-original-heading";
|
|
477
|
+
var SCOPE_ROOT_BOUNDARY_PROPERTIES = /* @__PURE__ */ new Set([
|
|
478
|
+
"bottom",
|
|
479
|
+
"inset",
|
|
480
|
+
"inset-block",
|
|
481
|
+
"inset-inline",
|
|
482
|
+
"left",
|
|
483
|
+
"position",
|
|
484
|
+
"right",
|
|
485
|
+
"rotate",
|
|
486
|
+
"scale",
|
|
487
|
+
"top",
|
|
488
|
+
"transform",
|
|
489
|
+
"translate",
|
|
490
|
+
"z-index"
|
|
491
|
+
]);
|
|
492
|
+
function fromCodePointSafe(value, radix) {
|
|
493
|
+
const codePoint = Number.parseInt(value, radix);
|
|
494
|
+
if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 1114111) {
|
|
495
|
+
return "";
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
return String.fromCodePoint(codePoint);
|
|
499
|
+
} catch {
|
|
500
|
+
return "";
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function isSafeUrl(tagName, attributeName, value) {
|
|
504
|
+
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();
|
|
505
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(normalized)?.[1];
|
|
506
|
+
if (!scheme) return true;
|
|
507
|
+
if (ALLOWED_URL_SCHEMES.has(scheme)) return true;
|
|
508
|
+
return tagName === "img" && attributeName === "src" && scheme === "data" && normalized.startsWith("data:image/") && !normalized.startsWith("data:image/svg");
|
|
509
|
+
}
|
|
510
|
+
function cssUrlsAreSafe(value, allowedAssetUrls) {
|
|
511
|
+
const urlRe = /url\(\s*(["']?)([^)'"\s]+)\1\s*\)/gi;
|
|
512
|
+
let matched = false;
|
|
513
|
+
for (const match of value.matchAll(urlRe)) {
|
|
514
|
+
matched = true;
|
|
515
|
+
const url = match[2]?.trim() ?? "";
|
|
516
|
+
if (!url.startsWith("#") && !allowedAssetUrls.has(url)) return false;
|
|
517
|
+
}
|
|
518
|
+
return !/url\s*\(/i.test(value) || matched;
|
|
519
|
+
}
|
|
520
|
+
function declarationIsSafe(property, value, allowedAssetUrls) {
|
|
521
|
+
if (UNSAFE_CSS_PROPERTY_RE.test(property) || UNSAFE_CSS_VALUE_RE.test(value)) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
if (!cssUrlsAreSafe(value, allowedAssetUrls)) return false;
|
|
525
|
+
if (property === "position" && /\b(?:fixed|sticky)\b/i.test(value)) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
if (property.startsWith("--") && FIXED_POSITION_RE.test(value.trim())) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
if (property === "z-index") {
|
|
532
|
+
const normalized = value.trim().toLowerCase();
|
|
533
|
+
if (normalized !== "auto") {
|
|
534
|
+
const zIndex = Number(normalized);
|
|
535
|
+
if (!Number.isInteger(zIndex) || Math.abs(zIndex) > 100) return false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
for (const match of value.matchAll(/(-?\d+(?:\.\d+)?)(px|vh|vw)/gi)) {
|
|
539
|
+
const magnitude = Math.abs(Number(match[1]));
|
|
540
|
+
const limit = match[2].toLowerCase() === "px" ? 4e3 : 200;
|
|
541
|
+
if (magnitude > limit) return false;
|
|
542
|
+
}
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
function cleanCssContainer(container, allowedAssetUrls) {
|
|
546
|
+
container.walkComments((comment) => {
|
|
547
|
+
comment.remove();
|
|
548
|
+
});
|
|
549
|
+
container.walkAtRules((atRule) => {
|
|
550
|
+
const name = atRule.name.toLowerCase();
|
|
551
|
+
const isKeyframes = name === "keyframes" || name === "-webkit-keyframes";
|
|
552
|
+
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)) {
|
|
553
|
+
atRule.remove();
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
container.walkDecls((declaration) => {
|
|
557
|
+
const property = declaration.prop.trim().toLowerCase();
|
|
558
|
+
if (!property || !declarationIsSafe(property, declaration.value, allowedAssetUrls)) {
|
|
559
|
+
declaration.remove();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
container.walkRules((rule) => {
|
|
564
|
+
if (!rule.selector.trim() || !rule.nodes?.length || /<\/?(?:style|script)/i.test(rule.selector)) {
|
|
565
|
+
rule.remove();
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
container.walkAtRules("font-face", (fontFace) => {
|
|
569
|
+
const declarations = fontFace.nodes ?? [];
|
|
570
|
+
const hasFamily = declarations.some(
|
|
571
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "font-family"
|
|
572
|
+
);
|
|
573
|
+
const hasSource = declarations.some(
|
|
574
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "src"
|
|
575
|
+
);
|
|
576
|
+
if (!hasFamily || !hasSource) fontFace.remove();
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
function sanitizeImportedCss(css, allowedAssetUrls = []) {
|
|
580
|
+
if (!css.trim()) return "";
|
|
581
|
+
try {
|
|
582
|
+
const root = import_postcss.default.parse(css);
|
|
583
|
+
cleanCssContainer(root, new Set(allowedAssetUrls));
|
|
584
|
+
return root.toString().trim();
|
|
585
|
+
} catch {
|
|
586
|
+
return "";
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function sanitizeInlineStyle(style, allowedAssetUrls) {
|
|
590
|
+
try {
|
|
591
|
+
const root = import_postcss.default.parse(`x{${style}}`);
|
|
592
|
+
cleanCssContainer(root, allowedAssetUrls);
|
|
593
|
+
const rule = root.first;
|
|
594
|
+
if (!rule || rule.type !== "rule") return "";
|
|
595
|
+
return rule.nodes.map((node) => node.toString()).join(";");
|
|
596
|
+
} catch {
|
|
597
|
+
return "";
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function sanitizeImportedAttributes(tagName, attributes, allowedAssetUrls) {
|
|
601
|
+
const next = {};
|
|
602
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
603
|
+
const attributeName = name.toLowerCase();
|
|
604
|
+
if (attributeName === "style") {
|
|
605
|
+
const style = sanitizeInlineStyle(value, allowedAssetUrls);
|
|
606
|
+
if (style) next[name] = style;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (attributeName === "srcset") {
|
|
610
|
+
const srcset = sanitizeSrcset(value);
|
|
611
|
+
if (srcset) next[name] = srcset;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if ((attributeName === "fill" || attributeName === "stroke") && /url\s*\(/i.test(value) && !/^url\(#[a-z_][\w:.-]*\)$/i.test(value.trim())) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if ((tagName === "use" || tagName === "image") && (attributeName === "href" || attributeName === "xlink:href") && !value.trim().startsWith("#")) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
next[name] = URL_ATTRIBUTES.has(attributeName) && !isSafeUrl(tagName, attributeName, value) ? "#" : value;
|
|
621
|
+
}
|
|
622
|
+
if (tagName === "a" && next.target === "_blank") {
|
|
623
|
+
next.rel = "noopener noreferrer";
|
|
624
|
+
}
|
|
625
|
+
if (tagName === "h1") {
|
|
626
|
+
next[ORIGINAL_H1_ATTRIBUTE] = "h1";
|
|
627
|
+
return { tagName: "h2", attribs: next };
|
|
628
|
+
}
|
|
629
|
+
return { tagName, attribs: next };
|
|
630
|
+
}
|
|
631
|
+
function sanitizeSrcset(value) {
|
|
632
|
+
const candidates = [];
|
|
633
|
+
for (const rawCandidate of value.split(",")) {
|
|
634
|
+
const candidate = rawCandidate.trim();
|
|
635
|
+
if (!candidate) continue;
|
|
636
|
+
const [url, ...descriptorParts] = candidate.split(/\s+/);
|
|
637
|
+
if (!url || !isSafeUrl("img", "src", url)) continue;
|
|
638
|
+
const descriptor = descriptorParts.join(" ");
|
|
639
|
+
if (descriptor && !/^(?:\d+(?:\.\d+)?x|\d+w)$/.test(descriptor)) continue;
|
|
640
|
+
candidates.push(descriptor ? `${url} ${descriptor}` : url);
|
|
641
|
+
}
|
|
642
|
+
return candidates.join(", ");
|
|
643
|
+
}
|
|
644
|
+
var IMPORTED_HTML_OPTIONS = {
|
|
645
|
+
allowedTags: [
|
|
646
|
+
"a",
|
|
647
|
+
"abbr",
|
|
648
|
+
"address",
|
|
649
|
+
"article",
|
|
650
|
+
"aside",
|
|
651
|
+
"b",
|
|
652
|
+
"bdi",
|
|
653
|
+
"bdo",
|
|
654
|
+
"blockquote",
|
|
655
|
+
"br",
|
|
656
|
+
"caption",
|
|
657
|
+
"cite",
|
|
658
|
+
"code",
|
|
659
|
+
"col",
|
|
660
|
+
"colgroup",
|
|
661
|
+
"dd",
|
|
662
|
+
"del",
|
|
663
|
+
"details",
|
|
664
|
+
"dfn",
|
|
665
|
+
"div",
|
|
666
|
+
"dl",
|
|
667
|
+
"dt",
|
|
668
|
+
"em",
|
|
669
|
+
"figcaption",
|
|
670
|
+
"figure",
|
|
671
|
+
"footer",
|
|
672
|
+
"h1",
|
|
673
|
+
"h2",
|
|
674
|
+
"h3",
|
|
675
|
+
"h4",
|
|
676
|
+
"h5",
|
|
677
|
+
"h6",
|
|
678
|
+
"header",
|
|
679
|
+
"hr",
|
|
680
|
+
"i",
|
|
681
|
+
"img",
|
|
682
|
+
"kbd",
|
|
683
|
+
"li",
|
|
684
|
+
"main",
|
|
685
|
+
"mark",
|
|
686
|
+
"nav",
|
|
687
|
+
"ol",
|
|
688
|
+
"p",
|
|
689
|
+
"picture",
|
|
690
|
+
"pre",
|
|
691
|
+
"q",
|
|
692
|
+
"s",
|
|
693
|
+
"samp",
|
|
694
|
+
"section",
|
|
695
|
+
"small",
|
|
696
|
+
"source",
|
|
697
|
+
"svg",
|
|
698
|
+
"g",
|
|
699
|
+
"path",
|
|
700
|
+
"circle",
|
|
701
|
+
"ellipse",
|
|
702
|
+
"line",
|
|
703
|
+
"polyline",
|
|
704
|
+
"polygon",
|
|
705
|
+
"rect",
|
|
706
|
+
"defs",
|
|
707
|
+
"lineargradient",
|
|
708
|
+
"radialgradient",
|
|
709
|
+
"stop",
|
|
710
|
+
"use",
|
|
711
|
+
"clippath",
|
|
712
|
+
"mask",
|
|
713
|
+
"pattern",
|
|
714
|
+
"title",
|
|
715
|
+
"desc",
|
|
716
|
+
"span",
|
|
717
|
+
"strong",
|
|
718
|
+
"sub",
|
|
719
|
+
"summary",
|
|
720
|
+
"sup",
|
|
721
|
+
"table",
|
|
722
|
+
"tbody",
|
|
723
|
+
"td",
|
|
724
|
+
"tfoot",
|
|
725
|
+
"th",
|
|
726
|
+
"thead",
|
|
727
|
+
"time",
|
|
728
|
+
"tr",
|
|
729
|
+
"u",
|
|
730
|
+
"ul",
|
|
731
|
+
"var",
|
|
732
|
+
"wbr"
|
|
733
|
+
],
|
|
734
|
+
allowedAttributes: {
|
|
735
|
+
"*": ["aria-*", "class", "data-*", "dir", "id", "lang", "role", "style", "title"],
|
|
736
|
+
a: [
|
|
737
|
+
"href",
|
|
738
|
+
{ name: "rel", multiple: true, values: ["noopener", "noreferrer"] },
|
|
739
|
+
{ name: "target", values: ["_blank"] }
|
|
740
|
+
],
|
|
741
|
+
col: ["span", "style"],
|
|
742
|
+
img: ["alt", "height", "loading", "sizes", "src", "srcset", "style", "width"],
|
|
743
|
+
source: ["height", "media", "sizes", "src", "srcset", "type", "width"],
|
|
744
|
+
svg: ["aria-*", "class", "fill", "height", "preserveAspectRatio", "role", "stroke", "style", "viewBox", "viewbox", "width", "xmlns"],
|
|
745
|
+
g: ["class", "fill", "stroke", "stroke-width", "style", "transform"],
|
|
746
|
+
path: ["class", "d", "fill", "fill-rule", "stroke", "stroke-linecap", "stroke-linejoin", "stroke-width", "style", "transform"],
|
|
747
|
+
circle: ["class", "cx", "cy", "fill", "r", "stroke", "stroke-width", "style", "transform"],
|
|
748
|
+
ellipse: ["class", "cx", "cy", "fill", "rx", "ry", "stroke", "stroke-width", "style", "transform"],
|
|
749
|
+
line: ["class", "stroke", "stroke-linecap", "stroke-width", "style", "transform", "x1", "x2", "y1", "y2"],
|
|
750
|
+
polyline: ["class", "fill", "points", "stroke", "stroke-linecap", "stroke-linejoin", "stroke-width", "style", "transform"],
|
|
751
|
+
polygon: ["class", "fill", "points", "stroke", "stroke-width", "style", "transform"],
|
|
752
|
+
rect: ["class", "fill", "height", "rx", "ry", "stroke", "stroke-width", "style", "transform", "width", "x", "y"],
|
|
753
|
+
lineargradient: ["gradientTransform", "gradientUnits", "id", "spreadMethod", "x1", "x2", "y1", "y2"],
|
|
754
|
+
radialgradient: ["cx", "cy", "fx", "fy", "gradientTransform", "gradientUnits", "id", "r", "spreadMethod"],
|
|
755
|
+
stop: ["offset", "stop-color", "stop-opacity", "style"],
|
|
756
|
+
use: ["fill", "height", "href", "stroke", "style", "width", "x", "xlink:href", "y"],
|
|
757
|
+
clippath: ["clipPathUnits", "id", "transform"],
|
|
758
|
+
mask: ["height", "id", "maskContentUnits", "maskUnits", "width", "x", "y"],
|
|
759
|
+
pattern: ["height", "id", "patternContentUnits", "patternTransform", "patternUnits", "preserveAspectRatio", "viewBox", "width", "x", "y"],
|
|
760
|
+
td: ["colspan", "rowspan", "style"],
|
|
761
|
+
th: [
|
|
762
|
+
"colspan",
|
|
763
|
+
"rowspan",
|
|
764
|
+
"style",
|
|
765
|
+
{ name: "scope", values: ["col", "row", "colgroup", "rowgroup"] }
|
|
766
|
+
],
|
|
767
|
+
time: ["datetime"]
|
|
768
|
+
},
|
|
769
|
+
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
770
|
+
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
|
771
|
+
allowedSchemesAppliedToAttributes: ["href", "src"],
|
|
772
|
+
allowProtocolRelative: false,
|
|
773
|
+
nonTextTags: ["script", "style", "textarea", "option", "iframe", "object", "embed", "foreignobject", "math", "template"]
|
|
774
|
+
};
|
|
775
|
+
function sanitizeImportedHtml(html, allowedAssetUrls = []) {
|
|
776
|
+
if (!html) return html;
|
|
777
|
+
const allowedSet = new Set(allowedAssetUrls);
|
|
778
|
+
return (0, import_sanitize_html.default)(html, {
|
|
779
|
+
...IMPORTED_HTML_OPTIONS,
|
|
780
|
+
transformTags: {
|
|
781
|
+
"*": (tagName, attributes) => sanitizeImportedAttributes(tagName, attributes, allowedSet)
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
function ensureImportedHtmlDocumentRoots(html, allowedAssetUrls = []) {
|
|
786
|
+
const safeHtml = sanitizeImportedHtml(html, allowedAssetUrls).trim();
|
|
787
|
+
if (!safeHtml) return safeHtml;
|
|
788
|
+
if (/^<div\b[^>]*\bdata-rt-import-document(?:=(?:""|'')|(?=[\s>]))[^>]*>\s*<div\b[^>]*\bdata-rt-import-body(?:=(?:""|'')|(?=[\s>]))/i.test(
|
|
789
|
+
safeHtml
|
|
790
|
+
)) {
|
|
791
|
+
return safeHtml;
|
|
792
|
+
}
|
|
793
|
+
return `<div data-rt-import-document><div data-rt-import-body>${safeHtml}</div></div>`;
|
|
794
|
+
}
|
|
795
|
+
function normalizeDocumentSelector(selector) {
|
|
796
|
+
const typeBoundary = "(?=$|[\\s>+~,.#:\\[\\)])";
|
|
797
|
+
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(
|
|
798
|
+
new RegExp(`(^|[\\s>+~,\\(])h1${typeBoundary}`, "gi"),
|
|
799
|
+
`$1[${ORIGINAL_H1_ATTRIBUTE}="h1"]`
|
|
800
|
+
).replace(/:root/g, "[data-rt-import-document]");
|
|
801
|
+
}
|
|
802
|
+
function selectorTargetsDocumentBoundary(selector) {
|
|
803
|
+
return selector.split(",").some((part) => {
|
|
804
|
+
const normalized = part.trim();
|
|
805
|
+
const targetsDocumentRoot = normalized.startsWith("[data-rt-import-document]") && !/[\s>+~]/.test(normalized.slice("[data-rt-import-document]".length));
|
|
806
|
+
const targetsBodyRoot = /\[data-rt-import-body\](?:[.#:][\w-]+|\[[^\]]+\])*\s*$/.test(
|
|
807
|
+
normalized
|
|
808
|
+
);
|
|
809
|
+
return targetsDocumentRoot || targetsBodyRoot;
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
function replaceAnimationName(value, names) {
|
|
813
|
+
let next = value;
|
|
814
|
+
for (const [name, scopedName] of names) {
|
|
815
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
816
|
+
next = next.replace(new RegExp(`(^|[^\\w-])${escaped}(?=$|[^\\w-])`, "g"), `$1${scopedName}`);
|
|
817
|
+
}
|
|
818
|
+
return next;
|
|
819
|
+
}
|
|
820
|
+
function unquoteCssIdentifier(value) {
|
|
821
|
+
const normalized = value.trim();
|
|
822
|
+
const match = /^(?:"([^"]+)"|'([^']+)'|([a-z_][\w -]*))$/i.exec(normalized);
|
|
823
|
+
return (match?.[1] ?? match?.[2] ?? match?.[3] ?? "").trim() || null;
|
|
824
|
+
}
|
|
825
|
+
function replaceFontFamilyName(value, names) {
|
|
826
|
+
let next = value;
|
|
827
|
+
for (const [name, scopedName] of names) {
|
|
828
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
829
|
+
next = next.replace(
|
|
830
|
+
new RegExp(`(["'])${escaped}\\1|(^|[,\\s])${escaped}(?=$|[,\\s/])`, "gi"),
|
|
831
|
+
(matched, quote, prefix) => quote ? `${quote}${scopedName}${quote}` : `${prefix ?? ""}${scopedName}`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return next;
|
|
835
|
+
}
|
|
836
|
+
function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
837
|
+
if (!IMPORT_SCOPE_RE.test(scope)) return "";
|
|
838
|
+
const safeCss = sanitizeImportedCss(css, allowedAssetUrls);
|
|
839
|
+
if (!safeCss) return "";
|
|
840
|
+
try {
|
|
841
|
+
const root = import_postcss.default.parse(safeCss);
|
|
842
|
+
const keyframeNames = /* @__PURE__ */ new Map();
|
|
843
|
+
const fontFamilyNames = /* @__PURE__ */ new Map();
|
|
844
|
+
const fontFaces = root.nodes.filter(
|
|
845
|
+
(node) => node.type === "atrule" && node.name.toLowerCase() === "font-face"
|
|
846
|
+
);
|
|
847
|
+
for (const fontFace of fontFaces) {
|
|
848
|
+
if (fontFace.type !== "atrule") continue;
|
|
849
|
+
const family = fontFace.nodes?.find(
|
|
850
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "font-family"
|
|
851
|
+
);
|
|
852
|
+
if (!family || family.type !== "decl") {
|
|
853
|
+
fontFace.remove();
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const originalName = unquoteCssIdentifier(family.value);
|
|
857
|
+
const hasSupportedSource = fontFace.nodes?.some(
|
|
858
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "src" && /url\(\s*["']?https:\/\/[^)'"\s]+\.woff2?(?:[?#][^)'"\s]*)?["']?\s*\)/i.test(
|
|
859
|
+
node.value
|
|
860
|
+
)
|
|
861
|
+
);
|
|
862
|
+
if (!originalName || !hasSupportedSource) {
|
|
863
|
+
fontFace.remove();
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const scopedName = `${scope}-${originalName}`;
|
|
867
|
+
fontFamilyNames.set(originalName, scopedName);
|
|
868
|
+
family.value = family.value.replace(originalName, scopedName);
|
|
869
|
+
}
|
|
870
|
+
root.walkAtRules((atRule) => {
|
|
871
|
+
const name = atRule.name.toLowerCase();
|
|
872
|
+
if (name !== "keyframes" && name !== "-webkit-keyframes") return;
|
|
873
|
+
const originalName = atRule.params.trim();
|
|
874
|
+
const scopedName = `${scope}-${originalName}`;
|
|
875
|
+
keyframeNames.set(originalName, scopedName);
|
|
876
|
+
atRule.params = scopedName;
|
|
877
|
+
});
|
|
878
|
+
root.walkDecls((declaration) => {
|
|
879
|
+
const property = declaration.prop.trim().toLowerCase();
|
|
880
|
+
if (property === "animation" || property === "animation-name") {
|
|
881
|
+
declaration.value = replaceAnimationName(declaration.value, keyframeNames);
|
|
882
|
+
}
|
|
883
|
+
if (property === "font-family" || property === "font") {
|
|
884
|
+
declaration.value = replaceFontFamilyName(
|
|
885
|
+
declaration.value,
|
|
886
|
+
fontFamilyNames
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
root.walkRules((rule) => {
|
|
891
|
+
const parent = rule.parent;
|
|
892
|
+
if (parent?.type === "atrule" && (parent.name.toLowerCase() === "keyframes" || parent.name.toLowerCase() === "-webkit-keyframes")) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
rule.selector = normalizeDocumentSelector(rule.selector);
|
|
896
|
+
if (selectorTargetsDocumentBoundary(rule.selector)) {
|
|
897
|
+
rule.walkDecls((declaration) => {
|
|
898
|
+
if (SCOPE_ROOT_BOUNDARY_PROPERTIES.has(
|
|
899
|
+
declaration.prop.trim().toLowerCase()
|
|
900
|
+
)) {
|
|
901
|
+
declaration.remove();
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
const scoped = import_postcss.default.atRule({
|
|
907
|
+
name: "scope",
|
|
908
|
+
params: `([data-rt-import-scope="${scope}"])`
|
|
909
|
+
});
|
|
910
|
+
const preservedFontFaces = fontFaces.filter((fontFace) => fontFace.parent);
|
|
911
|
+
for (const fontFace of preservedFontFaces) fontFace.remove();
|
|
912
|
+
scoped.append(root.nodes);
|
|
913
|
+
return [...preservedFontFaces.map((fontFace) => fontFace.toString()), scoped.toString()].filter(Boolean).join("\n");
|
|
914
|
+
} catch {
|
|
915
|
+
return "";
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
453
919
|
// src/preview-token.ts
|
|
454
920
|
var import_node_crypto = require("crypto");
|
|
455
921
|
var DELIMITER = ".";
|
|
@@ -485,15 +951,58 @@ function verifyPreviewToken(secret, token) {
|
|
|
485
951
|
}
|
|
486
952
|
|
|
487
953
|
// src/sanitize.ts
|
|
488
|
-
var
|
|
954
|
+
var import_sanitize_html2 = __toESM(require("sanitize-html"), 1);
|
|
489
955
|
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href"]);
|
|
490
|
-
var
|
|
956
|
+
var ALLOWED_URL_SCHEMES2 = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
491
957
|
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;
|
|
492
958
|
var SLASH_EVENT_ATTR_IN_URL_RE = /\/on[a-z]+=/i;
|
|
493
959
|
var ROOTTALE_CLASS_RE = /^rt-[a-z0-9_-]+$/;
|
|
494
960
|
var HEX_COLOR_RE = /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i;
|
|
495
961
|
var TEXT_ALIGN_RE = /^(left|right|center|justify)$/;
|
|
496
962
|
var ASPECT_RATIO_RE = /^\d+(?:\.\d+)?\s*\/\s*\d+(?:\.\d+)?$/;
|
|
963
|
+
var CSS_LENGTH_RE = /^(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))$/i;
|
|
964
|
+
var CSS_LENGTH_LIST_RE = /^(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))(?:\s+(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))){0,3}$/i;
|
|
965
|
+
var CSS_LINE_HEIGHT_RE = /^(?:normal|\d+(?:\.\d+)?(?:px|em|rem|%)?)$/i;
|
|
966
|
+
var CSS_FONT_WEIGHT_RE = /^(?:normal|bold|[1-9]00)$/i;
|
|
967
|
+
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;
|
|
968
|
+
var CSS_TEXT_DECORATION_RE = /^(?:none|underline|line-through)$/i;
|
|
969
|
+
var CSS_MAGNITUDE_LIMITS = {
|
|
970
|
+
"font-size": { "": 0, px: 96, em: 5, rem: 5, "%": 200 },
|
|
971
|
+
"line-height": { "": 5, px: 96, em: 5, rem: 5, "%": 500 },
|
|
972
|
+
margin: { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
973
|
+
"margin-bottom": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
974
|
+
"margin-left": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
975
|
+
"margin-right": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
976
|
+
"margin-top": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
977
|
+
padding: { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
978
|
+
"padding-bottom": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
979
|
+
"padding-left": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
980
|
+
"padding-right": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
981
|
+
"padding-top": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
982
|
+
width: { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 },
|
|
983
|
+
"max-width": { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 },
|
|
984
|
+
height: { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 }
|
|
985
|
+
};
|
|
986
|
+
function hasSafeCssMagnitude(property, value) {
|
|
987
|
+
const limits = CSS_MAGNITUDE_LIMITS[property];
|
|
988
|
+
if (!limits) return true;
|
|
989
|
+
const numericTokens = value.matchAll(/(\d+(?:\.\d+)?)(px|em|rem|%)?/gi);
|
|
990
|
+
for (const match of numericTokens) {
|
|
991
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
992
|
+
const limit = limits[unit];
|
|
993
|
+
if (limit === void 0 || Number(match[1]) > limit) return false;
|
|
994
|
+
}
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
function filterUnsafeCssMagnitudes(style) {
|
|
998
|
+
return style.split(";").map((declaration) => declaration.trim()).filter(Boolean).filter((declaration) => {
|
|
999
|
+
const colon = declaration.indexOf(":");
|
|
1000
|
+
if (colon < 1) return false;
|
|
1001
|
+
const property = declaration.slice(0, colon).trim().toLowerCase();
|
|
1002
|
+
const value = declaration.slice(colon + 1).trim();
|
|
1003
|
+
return hasSafeCssMagnitude(property, value);
|
|
1004
|
+
}).join(";");
|
|
1005
|
+
}
|
|
497
1006
|
var NAMED_ENTITY = {
|
|
498
1007
|
colon: ":",
|
|
499
1008
|
tab: " ",
|
|
@@ -506,7 +1015,7 @@ var NAMED_ENTITY = {
|
|
|
506
1015
|
lpar: "(",
|
|
507
1016
|
rpar: ")"
|
|
508
1017
|
};
|
|
509
|
-
function
|
|
1018
|
+
function fromCodePointSafe2(n) {
|
|
510
1019
|
if (!Number.isFinite(n) || n < 0 || n > 1114111) return "";
|
|
511
1020
|
try {
|
|
512
1021
|
return String.fromCodePoint(n);
|
|
@@ -515,7 +1024,7 @@ function fromCodePointSafe(n) {
|
|
|
515
1024
|
}
|
|
516
1025
|
}
|
|
517
1026
|
function decodeUrlProbe(value) {
|
|
518
|
-
const decoded = value.replace(/&#x([0-9a-f]+);?/gi, (_, h) =>
|
|
1027
|
+
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);
|
|
519
1028
|
return [...decoded].filter((c) => {
|
|
520
1029
|
const n = c.codePointAt(0) ?? 0;
|
|
521
1030
|
return n > 32 && n !== 127;
|
|
@@ -535,7 +1044,7 @@ function isSafeUrlValue(tag, attr, value) {
|
|
|
535
1044
|
if (SLASH_EVENT_ATTR_IN_URL_RE.test(probe)) return false;
|
|
536
1045
|
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe)?.[1];
|
|
537
1046
|
if (!scheme) return true;
|
|
538
|
-
if (
|
|
1047
|
+
if (ALLOWED_URL_SCHEMES2.has(scheme)) return true;
|
|
539
1048
|
if (scheme === "data" && tag === "img" && attr === "src" && probe.startsWith("data:image/")) {
|
|
540
1049
|
return !probe.startsWith("data:image/svg");
|
|
541
1050
|
}
|
|
@@ -545,8 +1054,16 @@ function sanitizeUrlAttributes(tagName, attribs) {
|
|
|
545
1054
|
const next = {};
|
|
546
1055
|
for (const [name, value] of Object.entries(attribs)) {
|
|
547
1056
|
const attr = name.toLowerCase();
|
|
1057
|
+
if (attr === "style") {
|
|
1058
|
+
const style = filterUnsafeCssMagnitudes(value);
|
|
1059
|
+
if (style) next[name] = style;
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
548
1062
|
next[name] = URL_ATTRS.has(attr) && !isSafeUrlValue(tagName, attr, value) ? "#" : value;
|
|
549
1063
|
}
|
|
1064
|
+
if (tagName === "a" && next.target === "_blank") {
|
|
1065
|
+
next.rel = "noopener noreferrer";
|
|
1066
|
+
}
|
|
550
1067
|
return { tagName, attribs: next };
|
|
551
1068
|
}
|
|
552
1069
|
var SANITIZE_OPTIONS = {
|
|
@@ -572,19 +1089,25 @@ var SANITIZE_OPTIONS = {
|
|
|
572
1089
|
"p",
|
|
573
1090
|
"pre",
|
|
574
1091
|
"s",
|
|
1092
|
+
"section",
|
|
1093
|
+
"small",
|
|
575
1094
|
"span",
|
|
576
1095
|
"strong",
|
|
1096
|
+
"sub",
|
|
577
1097
|
"summary",
|
|
1098
|
+
"sup",
|
|
578
1099
|
"table",
|
|
579
1100
|
"tbody",
|
|
580
1101
|
"td",
|
|
1102
|
+
"tfoot",
|
|
581
1103
|
"th",
|
|
1104
|
+
"thead",
|
|
582
1105
|
"tr",
|
|
583
1106
|
"u",
|
|
584
1107
|
"ul"
|
|
585
1108
|
],
|
|
586
1109
|
allowedAttributes: {
|
|
587
|
-
"*": ["class", "data-*"],
|
|
1110
|
+
"*": ["class", "data-*", "style"],
|
|
588
1111
|
a: [
|
|
589
1112
|
"href",
|
|
590
1113
|
"title",
|
|
@@ -601,7 +1124,14 @@ var SANITIZE_OPTIONS = {
|
|
|
601
1124
|
p: ["class", "data-*", "style"],
|
|
602
1125
|
span: ["class", "data-*", "style"],
|
|
603
1126
|
td: ["colspan", "rowspan", "class", "data-*"],
|
|
604
|
-
|
|
1127
|
+
// scope 는 표 머리행의 접근성 의미(스크린리더 열/행 연결) — 값 집합을 고정해 허용.
|
|
1128
|
+
th: [
|
|
1129
|
+
"colspan",
|
|
1130
|
+
"rowspan",
|
|
1131
|
+
"class",
|
|
1132
|
+
"data-*",
|
|
1133
|
+
{ name: "scope", values: ["col", "row", "colgroup", "rowgroup"] }
|
|
1134
|
+
]
|
|
605
1135
|
},
|
|
606
1136
|
allowedClasses: {
|
|
607
1137
|
"*": [ROOTTALE_CLASS_RE]
|
|
@@ -611,8 +1141,31 @@ var SANITIZE_OPTIONS = {
|
|
|
611
1141
|
"aspect-ratio": [ASPECT_RATIO_RE],
|
|
612
1142
|
background: [HEX_COLOR_RE],
|
|
613
1143
|
"background-color": [HEX_COLOR_RE],
|
|
1144
|
+
border: [CSS_BORDER_RE],
|
|
1145
|
+
"border-bottom": [CSS_BORDER_RE],
|
|
1146
|
+
"border-left": [CSS_BORDER_RE],
|
|
1147
|
+
"border-right": [CSS_BORDER_RE],
|
|
1148
|
+
"border-top": [CSS_BORDER_RE],
|
|
1149
|
+
"border-collapse": [/^(?:collapse|separate)$/i],
|
|
614
1150
|
color: [HEX_COLOR_RE],
|
|
615
|
-
"
|
|
1151
|
+
"font-size": [CSS_LENGTH_RE],
|
|
1152
|
+
"font-weight": [CSS_FONT_WEIGHT_RE],
|
|
1153
|
+
height: [CSS_LENGTH_RE, /^auto$/i],
|
|
1154
|
+
"line-height": [CSS_LINE_HEIGHT_RE],
|
|
1155
|
+
margin: [CSS_LENGTH_LIST_RE],
|
|
1156
|
+
"margin-bottom": [CSS_LENGTH_RE],
|
|
1157
|
+
"margin-left": [CSS_LENGTH_RE],
|
|
1158
|
+
"margin-right": [CSS_LENGTH_RE],
|
|
1159
|
+
"margin-top": [CSS_LENGTH_RE],
|
|
1160
|
+
"max-width": [CSS_LENGTH_RE, /^none$/i],
|
|
1161
|
+
padding: [CSS_LENGTH_LIST_RE],
|
|
1162
|
+
"padding-bottom": [CSS_LENGTH_RE],
|
|
1163
|
+
"padding-left": [CSS_LENGTH_RE],
|
|
1164
|
+
"padding-right": [CSS_LENGTH_RE],
|
|
1165
|
+
"padding-top": [CSS_LENGTH_RE],
|
|
1166
|
+
"text-align": [TEXT_ALIGN_RE],
|
|
1167
|
+
"text-decoration": [CSS_TEXT_DECORATION_RE],
|
|
1168
|
+
width: [CSS_LENGTH_RE, /^auto$/i]
|
|
616
1169
|
}
|
|
617
1170
|
},
|
|
618
1171
|
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
@@ -641,7 +1194,18 @@ var SANITIZE_OPTIONS = {
|
|
|
641
1194
|
};
|
|
642
1195
|
function sanitizeHtml(html) {
|
|
643
1196
|
if (!html) return html;
|
|
644
|
-
return (0,
|
|
1197
|
+
return (0, import_sanitize_html2.default)(stripMalformedDangerousTokens(html), SANITIZE_OPTIONS);
|
|
1198
|
+
}
|
|
1199
|
+
function htmlToPlainText(html) {
|
|
1200
|
+
const safeHtml = sanitizeHtml(html);
|
|
1201
|
+
const withBlockBoundaries = safeHtml.replace(
|
|
1202
|
+
/<(?:br|hr)\b[^>]*>|<\/(?:blockquote|details|div|figcaption|figure|h[2-4]|li|p|pre|section|summary|table|tbody|td|th|tr)>/gi,
|
|
1203
|
+
" "
|
|
1204
|
+
);
|
|
1205
|
+
return (0, import_sanitize_html2.default)(withBlockBoundaries, {
|
|
1206
|
+
allowedTags: [],
|
|
1207
|
+
allowedAttributes: {}
|
|
1208
|
+
}).replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
|
|
645
1209
|
}
|
|
646
1210
|
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'";
|
|
647
1211
|
|
|
@@ -674,9 +1238,11 @@ function injectStableIdsTree(blocks) {
|
|
|
674
1238
|
boundFieldValue,
|
|
675
1239
|
chainResolvers,
|
|
676
1240
|
createFieldBindingResolver,
|
|
1241
|
+
ensureImportedHtmlDocumentRoots,
|
|
677
1242
|
ensureStableIds,
|
|
678
1243
|
extractFaqEntries,
|
|
679
1244
|
faqPageJsonLd,
|
|
1245
|
+
htmlToPlainText,
|
|
680
1246
|
injectStableIdsTree,
|
|
681
1247
|
isAllowedEmbedSrc,
|
|
682
1248
|
isAllowedMapEmbedSrc,
|
|
@@ -693,6 +1259,9 @@ function injectStableIdsTree(blocks) {
|
|
|
693
1259
|
resolvePostPath,
|
|
694
1260
|
resolvePostUrl,
|
|
695
1261
|
sanitizeHtml,
|
|
1262
|
+
sanitizeImportedCss,
|
|
1263
|
+
sanitizeImportedHtml,
|
|
1264
|
+
scopeImportedCss,
|
|
696
1265
|
serializeBlock,
|
|
697
1266
|
validateCollections,
|
|
698
1267
|
verifyPreviewToken
|