@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/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 sanitizeHtmlLibrary from "sanitize-html";
879
+ import sanitizeHtmlLibrary2 from "sanitize-html";
419
880
  var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href"]);
420
- var ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
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 fromCodePointSafe(n) {
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) => fromCodePointSafe(parseInt(h, 16))).replace(/&#(\d+);?/g, (_, d) => fromCodePointSafe(parseInt(d, 10))).replace(/&([a-z]+);/gi, (m, name) => NAMED_ENTITY[name.toLowerCase()] ?? m);
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 (ALLOWED_URL_SCHEMES.has(scheme)) return true;
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
- th: ["colspan", "rowspan", "class", "data-*"]
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
- "text-align": [TEXT_ALIGN_RE]
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 sanitizeHtmlLibrary(stripMalformedDangerousTokens(html), SANITIZE_OPTIONS);
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