@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.js
CHANGED
|
@@ -380,6 +380,467 @@ function normalizeImageAlt(value) {
|
|
|
380
380
|
return `${visible}\u2026`;
|
|
381
381
|
}
|
|
382
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
|
+
|
|
383
844
|
// src/preview-token.ts
|
|
384
845
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
385
846
|
var DELIMITER = ".";
|
|
@@ -415,15 +876,58 @@ function verifyPreviewToken(secret, token) {
|
|
|
415
876
|
}
|
|
416
877
|
|
|
417
878
|
// src/sanitize.ts
|
|
418
|
-
import
|
|
879
|
+
import sanitizeHtmlLibrary2 from "sanitize-html";
|
|
419
880
|
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href"]);
|
|
420
|
-
var
|
|
881
|
+
var ALLOWED_URL_SCHEMES2 = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
421
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;
|
|
422
883
|
var SLASH_EVENT_ATTR_IN_URL_RE = /\/on[a-z]+=/i;
|
|
423
884
|
var ROOTTALE_CLASS_RE = /^rt-[a-z0-9_-]+$/;
|
|
424
885
|
var HEX_COLOR_RE = /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i;
|
|
425
886
|
var TEXT_ALIGN_RE = /^(left|right|center|justify)$/;
|
|
426
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
|
+
}
|
|
427
931
|
var NAMED_ENTITY = {
|
|
428
932
|
colon: ":",
|
|
429
933
|
tab: " ",
|
|
@@ -436,7 +940,7 @@ var NAMED_ENTITY = {
|
|
|
436
940
|
lpar: "(",
|
|
437
941
|
rpar: ")"
|
|
438
942
|
};
|
|
439
|
-
function
|
|
943
|
+
function fromCodePointSafe2(n) {
|
|
440
944
|
if (!Number.isFinite(n) || n < 0 || n > 1114111) return "";
|
|
441
945
|
try {
|
|
442
946
|
return String.fromCodePoint(n);
|
|
@@ -445,7 +949,7 @@ function fromCodePointSafe(n) {
|
|
|
445
949
|
}
|
|
446
950
|
}
|
|
447
951
|
function decodeUrlProbe(value) {
|
|
448
|
-
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);
|
|
449
953
|
return [...decoded].filter((c) => {
|
|
450
954
|
const n = c.codePointAt(0) ?? 0;
|
|
451
955
|
return n > 32 && n !== 127;
|
|
@@ -465,7 +969,7 @@ function isSafeUrlValue(tag, attr, value) {
|
|
|
465
969
|
if (SLASH_EVENT_ATTR_IN_URL_RE.test(probe)) return false;
|
|
466
970
|
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe)?.[1];
|
|
467
971
|
if (!scheme) return true;
|
|
468
|
-
if (
|
|
972
|
+
if (ALLOWED_URL_SCHEMES2.has(scheme)) return true;
|
|
469
973
|
if (scheme === "data" && tag === "img" && attr === "src" && probe.startsWith("data:image/")) {
|
|
470
974
|
return !probe.startsWith("data:image/svg");
|
|
471
975
|
}
|
|
@@ -475,8 +979,16 @@ function sanitizeUrlAttributes(tagName, attribs) {
|
|
|
475
979
|
const next = {};
|
|
476
980
|
for (const [name, value] of Object.entries(attribs)) {
|
|
477
981
|
const attr = name.toLowerCase();
|
|
982
|
+
if (attr === "style") {
|
|
983
|
+
const style = filterUnsafeCssMagnitudes(value);
|
|
984
|
+
if (style) next[name] = style;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
478
987
|
next[name] = URL_ATTRS.has(attr) && !isSafeUrlValue(tagName, attr, value) ? "#" : value;
|
|
479
988
|
}
|
|
989
|
+
if (tagName === "a" && next.target === "_blank") {
|
|
990
|
+
next.rel = "noopener noreferrer";
|
|
991
|
+
}
|
|
480
992
|
return { tagName, attribs: next };
|
|
481
993
|
}
|
|
482
994
|
var SANITIZE_OPTIONS = {
|
|
@@ -502,19 +1014,25 @@ var SANITIZE_OPTIONS = {
|
|
|
502
1014
|
"p",
|
|
503
1015
|
"pre",
|
|
504
1016
|
"s",
|
|
1017
|
+
"section",
|
|
1018
|
+
"small",
|
|
505
1019
|
"span",
|
|
506
1020
|
"strong",
|
|
1021
|
+
"sub",
|
|
507
1022
|
"summary",
|
|
1023
|
+
"sup",
|
|
508
1024
|
"table",
|
|
509
1025
|
"tbody",
|
|
510
1026
|
"td",
|
|
1027
|
+
"tfoot",
|
|
511
1028
|
"th",
|
|
1029
|
+
"thead",
|
|
512
1030
|
"tr",
|
|
513
1031
|
"u",
|
|
514
1032
|
"ul"
|
|
515
1033
|
],
|
|
516
1034
|
allowedAttributes: {
|
|
517
|
-
"*": ["class", "data-*"],
|
|
1035
|
+
"*": ["class", "data-*", "style"],
|
|
518
1036
|
a: [
|
|
519
1037
|
"href",
|
|
520
1038
|
"title",
|
|
@@ -531,7 +1049,14 @@ var SANITIZE_OPTIONS = {
|
|
|
531
1049
|
p: ["class", "data-*", "style"],
|
|
532
1050
|
span: ["class", "data-*", "style"],
|
|
533
1051
|
td: ["colspan", "rowspan", "class", "data-*"],
|
|
534
|
-
|
|
1052
|
+
// scope 는 표 머리행의 접근성 의미(스크린리더 열/행 연결) — 값 집합을 고정해 허용.
|
|
1053
|
+
th: [
|
|
1054
|
+
"colspan",
|
|
1055
|
+
"rowspan",
|
|
1056
|
+
"class",
|
|
1057
|
+
"data-*",
|
|
1058
|
+
{ name: "scope", values: ["col", "row", "colgroup", "rowgroup"] }
|
|
1059
|
+
]
|
|
535
1060
|
},
|
|
536
1061
|
allowedClasses: {
|
|
537
1062
|
"*": [ROOTTALE_CLASS_RE]
|
|
@@ -541,8 +1066,31 @@ var SANITIZE_OPTIONS = {
|
|
|
541
1066
|
"aspect-ratio": [ASPECT_RATIO_RE],
|
|
542
1067
|
background: [HEX_COLOR_RE],
|
|
543
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],
|
|
544
1075
|
color: [HEX_COLOR_RE],
|
|
545
|
-
"
|
|
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]
|
|
546
1094
|
}
|
|
547
1095
|
},
|
|
548
1096
|
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
@@ -571,7 +1119,18 @@ var SANITIZE_OPTIONS = {
|
|
|
571
1119
|
};
|
|
572
1120
|
function sanitizeHtml(html) {
|
|
573
1121
|
if (!html) return html;
|
|
574
|
-
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();
|
|
575
1134
|
}
|
|
576
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'";
|
|
577
1136
|
|
|
@@ -603,9 +1162,11 @@ export {
|
|
|
603
1162
|
boundFieldValue,
|
|
604
1163
|
chainResolvers,
|
|
605
1164
|
createFieldBindingResolver,
|
|
1165
|
+
ensureImportedHtmlDocumentRoots,
|
|
606
1166
|
ensureStableIds,
|
|
607
1167
|
extractFaqEntries,
|
|
608
1168
|
faqPageJsonLd,
|
|
1169
|
+
htmlToPlainText,
|
|
609
1170
|
injectStableIdsTree,
|
|
610
1171
|
isAllowedEmbedSrc,
|
|
611
1172
|
isAllowedMapEmbedSrc,
|
|
@@ -622,6 +1183,9 @@ export {
|
|
|
622
1183
|
resolvePostPath,
|
|
623
1184
|
resolvePostUrl,
|
|
624
1185
|
sanitizeHtml,
|
|
1186
|
+
sanitizeImportedCss,
|
|
1187
|
+
sanitizeImportedHtml,
|
|
1188
|
+
scopeImportedCss,
|
|
625
1189
|
serializeBlock,
|
|
626
1190
|
validateCollections,
|
|
627
1191
|
verifyPreviewToken
|