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