@roottale/cms-core 0.53.0 → 0.55.0
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 +18 -0
- package/README.md +1 -0
- package/dist/index.cjs +691 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -2
- package/dist/index.d.ts +68 -2
- package/dist/index.js +681 -15
- 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,
|
|
@@ -58,10 +60,18 @@ __export(src_exports, {
|
|
|
58
60
|
postsForCollection: () => postsForCollection,
|
|
59
61
|
postsInAnyCollection: () => postsInAnyCollection,
|
|
60
62
|
postsNotInAnyCollection: () => postsNotInAnyCollection,
|
|
63
|
+
resolveCategoryPath: () => resolveCategoryPath,
|
|
64
|
+
resolveCategoryTreeChain: () => resolveCategoryTreeChain,
|
|
65
|
+
resolveCategoryTreeEntryPath: () => resolveCategoryTreeEntryPath,
|
|
66
|
+
resolveCategoryTreeHubPaths: () => resolveCategoryTreeHubPaths,
|
|
67
|
+
resolvePostCategorySlug: () => resolvePostCategorySlug,
|
|
61
68
|
resolvePostCollection: () => resolvePostCollection,
|
|
62
69
|
resolvePostPath: () => resolvePostPath,
|
|
63
70
|
resolvePostUrl: () => resolvePostUrl,
|
|
64
71
|
sanitizeHtml: () => sanitizeHtml,
|
|
72
|
+
sanitizeImportedCss: () => sanitizeImportedCss,
|
|
73
|
+
sanitizeImportedHtml: () => sanitizeImportedHtml,
|
|
74
|
+
scopeImportedCss: () => scopeImportedCss,
|
|
65
75
|
serializeBlock: () => serializeBlock,
|
|
66
76
|
validateCollections: () => validateCollections,
|
|
67
77
|
verifyPreviewToken: () => verifyPreviewToken
|
|
@@ -197,18 +207,38 @@ function resolvePostCollection(post, collections) {
|
|
|
197
207
|
return collections.find((c) => c.key === post.collectionKey) ?? null;
|
|
198
208
|
}
|
|
199
209
|
function resolvePostUrl(siteUrl, post, collections) {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
const path = resolvePostPath(post, collections);
|
|
211
|
+
return path ? `${trimTrailingSlash(siteUrl)}${path}` : null;
|
|
212
|
+
}
|
|
213
|
+
function resolvePostCategorySlug(post, collection) {
|
|
214
|
+
const allowed = new Set(collection.categories);
|
|
215
|
+
const slugs = [
|
|
216
|
+
...new Set(
|
|
217
|
+
post.terms.filter(
|
|
218
|
+
(term) => term.taxonomy === "category" && allowed.has(term.slug)
|
|
219
|
+
).map((term) => term.slug)
|
|
220
|
+
)
|
|
221
|
+
];
|
|
222
|
+
return slugs.length === 1 ? slugs[0] : null;
|
|
223
|
+
}
|
|
224
|
+
function resolveCategoryPath(collection, categorySlug) {
|
|
225
|
+
if (!collection.archives || !collection.basePath || !categorySlug) return null;
|
|
226
|
+
const basePath = trimTrailingSlash(collection.basePath);
|
|
227
|
+
const encodedSlug = encodeURIComponent(categorySlug);
|
|
228
|
+
return collection.categoryPath === "direct" ? `${basePath}/${encodedSlug}` : `${basePath}/categories/${encodedSlug}`;
|
|
205
229
|
}
|
|
206
230
|
function resolvePostPath(post, collections) {
|
|
207
231
|
const collection = resolvePostCollection(post, collections);
|
|
208
232
|
if (!collection) return null;
|
|
209
233
|
if (collection.detail === false) return null;
|
|
210
234
|
if (!collection.basePath) return null;
|
|
211
|
-
|
|
235
|
+
const basePath = trimTrailingSlash(collection.basePath);
|
|
236
|
+
const encodedPostSlug = encodeURIComponent(post.slug);
|
|
237
|
+
if (collection.detailPath !== "category") {
|
|
238
|
+
return `${basePath}/${encodedPostSlug}`;
|
|
239
|
+
}
|
|
240
|
+
const categorySlug = resolvePostCategorySlug(post, collection);
|
|
241
|
+
return categorySlug ? `${basePath}/${encodeURIComponent(categorySlug)}/${encodedPostSlug}` : null;
|
|
212
242
|
}
|
|
213
243
|
function postsForCollection(posts, collections, key) {
|
|
214
244
|
return posts.filter((p) => resolvePostCollection(p, collections)?.key === key);
|
|
@@ -248,10 +278,87 @@ function validateCollections(collections) {
|
|
|
248
278
|
}
|
|
249
279
|
categoryOwner.set(slug, c.key);
|
|
250
280
|
}
|
|
281
|
+
if (c.detailPath === "category") {
|
|
282
|
+
if (!c.basePath) {
|
|
283
|
+
problems.push(
|
|
284
|
+
`category \uC0C1\uC138 \uACBD\uB85C\uB294 basePath \uAC00 \uD544\uC694\uD568: "${c.key}"`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (c.detail === false) {
|
|
288
|
+
problems.push(
|
|
289
|
+
`category \uC0C1\uC138 \uACBD\uB85C\uB294 \uAE00\uBCC4 \uC0C1\uC138 \uD398\uC774\uC9C0\uAC00 \uD544\uC694\uD568: "${c.key}"`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (!c.archives) {
|
|
293
|
+
problems.push(
|
|
294
|
+
`category \uC0C1\uC138 \uACBD\uB85C\uB294 \uCE74\uD14C\uACE0\uB9AC \uC544\uCE74\uC774\uBE0C\uAC00 \uD544\uC694\uD568: "${c.key}"`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (c.categoryCardinality !== "exactly-one") {
|
|
298
|
+
problems.push(
|
|
299
|
+
`category \uC0C1\uC138 \uACBD\uB85C\uB294 categoryCardinality "exactly-one" \uC774 \uD544\uC694\uD568: "${c.key}"`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
251
303
|
}
|
|
252
304
|
return problems;
|
|
253
305
|
}
|
|
254
306
|
|
|
307
|
+
// src/category-tree.ts
|
|
308
|
+
function normalizeBasePath(basePath) {
|
|
309
|
+
return basePath.replace(/\/+$/, "");
|
|
310
|
+
}
|
|
311
|
+
function resolveCategoryTreeChain(presentation, leafId, terms) {
|
|
312
|
+
if (!Number.isInteger(presentation.categoryDepth) || presentation.categoryDepth < 1) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
const termsById = new Map(terms.map((term) => [term.id, term]));
|
|
316
|
+
if (termsById.size !== terms.length) return null;
|
|
317
|
+
if (terms.some((term) => term.parentId === leafId)) return null;
|
|
318
|
+
const reversed = [];
|
|
319
|
+
const visited = /* @__PURE__ */ new Set();
|
|
320
|
+
let currentId = leafId;
|
|
321
|
+
while (currentId) {
|
|
322
|
+
if (visited.has(currentId)) return null;
|
|
323
|
+
visited.add(currentId);
|
|
324
|
+
const current = termsById.get(currentId);
|
|
325
|
+
if (!current || !current.slug) return null;
|
|
326
|
+
reversed.push(current);
|
|
327
|
+
currentId = current.parentId ?? null;
|
|
328
|
+
}
|
|
329
|
+
if (reversed.length !== presentation.categoryDepth) return null;
|
|
330
|
+
return reversed.reverse();
|
|
331
|
+
}
|
|
332
|
+
function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
333
|
+
const chain = resolveCategoryTreeChain(presentation, termId, terms);
|
|
334
|
+
const basePath = normalizeBasePath(presentation.basePath);
|
|
335
|
+
if (!chain || !basePath) return null;
|
|
336
|
+
const paths = [basePath];
|
|
337
|
+
const segments = [];
|
|
338
|
+
for (const term of chain) {
|
|
339
|
+
segments.push(encodeURIComponent(term.slug));
|
|
340
|
+
paths.push(`${basePath}/${segments.join("/")}`);
|
|
341
|
+
}
|
|
342
|
+
return paths;
|
|
343
|
+
}
|
|
344
|
+
function resolveCategoryTreeEntryPath(presentation, entry, terms) {
|
|
345
|
+
if (!entry.slug) return null;
|
|
346
|
+
const knownIds = new Set(terms.map((term) => term.id));
|
|
347
|
+
const categoryIds = [
|
|
348
|
+
...new Set(
|
|
349
|
+
entry.terms.filter((term) => term.taxonomy === "category" && knownIds.has(term.id)).map((term) => term.id)
|
|
350
|
+
)
|
|
351
|
+
];
|
|
352
|
+
if (categoryIds.length !== 1) return null;
|
|
353
|
+
const hubs = resolveCategoryTreeHubPaths(
|
|
354
|
+
presentation,
|
|
355
|
+
categoryIds[0],
|
|
356
|
+
terms
|
|
357
|
+
);
|
|
358
|
+
const leafPath = hubs?.at(-1);
|
|
359
|
+
return leafPath ? `${leafPath}/${encodeURIComponent(entry.slug)}` : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
255
362
|
// src/delimiter.ts
|
|
256
363
|
function serializeBlock(block) {
|
|
257
364
|
const exportable = sanitizeAttributesForExport(block);
|
|
@@ -450,6 +557,467 @@ function normalizeImageAlt(value) {
|
|
|
450
557
|
return `${visible}\u2026`;
|
|
451
558
|
}
|
|
452
559
|
|
|
560
|
+
// src/imported-html-sanitize.ts
|
|
561
|
+
var import_postcss = __toESM(require("postcss"), 1);
|
|
562
|
+
var import_sanitize_html = __toESM(require("sanitize-html"), 1);
|
|
563
|
+
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;
|
|
564
|
+
var ALLOWED_AT_RULES = /* @__PURE__ */ new Set([
|
|
565
|
+
"container",
|
|
566
|
+
"font-face",
|
|
567
|
+
"keyframes",
|
|
568
|
+
"-webkit-keyframes",
|
|
569
|
+
"layer",
|
|
570
|
+
"media",
|
|
571
|
+
"supports"
|
|
572
|
+
]);
|
|
573
|
+
var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src"]);
|
|
574
|
+
var ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
575
|
+
var UNSAFE_CSS_VALUE_RE = /(?:expression\s*\(|javascript\s*:|vbscript\s*:|@import|<\/?(?:style|script)|\\0|\\0000)/i;
|
|
576
|
+
var UNSAFE_CSS_PROPERTY_RE = /^(?:behavior|-moz-binding)$/i;
|
|
577
|
+
var FIXED_POSITION_RE = /^(?:fixed|sticky)$/i;
|
|
578
|
+
var ORIGINAL_H1_ATTRIBUTE = "data-rt-original-heading";
|
|
579
|
+
var SCOPE_ROOT_BOUNDARY_PROPERTIES = /* @__PURE__ */ new Set([
|
|
580
|
+
"bottom",
|
|
581
|
+
"inset",
|
|
582
|
+
"inset-block",
|
|
583
|
+
"inset-inline",
|
|
584
|
+
"left",
|
|
585
|
+
"position",
|
|
586
|
+
"right",
|
|
587
|
+
"rotate",
|
|
588
|
+
"scale",
|
|
589
|
+
"top",
|
|
590
|
+
"transform",
|
|
591
|
+
"translate",
|
|
592
|
+
"z-index"
|
|
593
|
+
]);
|
|
594
|
+
function fromCodePointSafe(value, radix) {
|
|
595
|
+
const codePoint = Number.parseInt(value, radix);
|
|
596
|
+
if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 1114111) {
|
|
597
|
+
return "";
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
return String.fromCodePoint(codePoint);
|
|
601
|
+
} catch {
|
|
602
|
+
return "";
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function isSafeUrl(tagName, attributeName, value) {
|
|
606
|
+
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();
|
|
607
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(normalized)?.[1];
|
|
608
|
+
if (!scheme) return true;
|
|
609
|
+
if (ALLOWED_URL_SCHEMES.has(scheme)) return true;
|
|
610
|
+
return tagName === "img" && attributeName === "src" && scheme === "data" && normalized.startsWith("data:image/") && !normalized.startsWith("data:image/svg");
|
|
611
|
+
}
|
|
612
|
+
function cssUrlsAreSafe(value, allowedAssetUrls) {
|
|
613
|
+
const urlRe = /url\(\s*(["']?)([^)'"\s]+)\1\s*\)/gi;
|
|
614
|
+
let matched = false;
|
|
615
|
+
for (const match of value.matchAll(urlRe)) {
|
|
616
|
+
matched = true;
|
|
617
|
+
const url = match[2]?.trim() ?? "";
|
|
618
|
+
if (!url.startsWith("#") && !allowedAssetUrls.has(url)) return false;
|
|
619
|
+
}
|
|
620
|
+
return !/url\s*\(/i.test(value) || matched;
|
|
621
|
+
}
|
|
622
|
+
function declarationIsSafe(property, value, allowedAssetUrls) {
|
|
623
|
+
if (UNSAFE_CSS_PROPERTY_RE.test(property) || UNSAFE_CSS_VALUE_RE.test(value)) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
if (!cssUrlsAreSafe(value, allowedAssetUrls)) return false;
|
|
627
|
+
if (property === "position" && /\b(?:fixed|sticky)\b/i.test(value)) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
if (property.startsWith("--") && FIXED_POSITION_RE.test(value.trim())) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
if (property === "z-index") {
|
|
634
|
+
const normalized = value.trim().toLowerCase();
|
|
635
|
+
if (normalized !== "auto") {
|
|
636
|
+
const zIndex = Number(normalized);
|
|
637
|
+
if (!Number.isInteger(zIndex) || Math.abs(zIndex) > 100) return false;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
for (const match of value.matchAll(/(-?\d+(?:\.\d+)?)(px|vh|vw)/gi)) {
|
|
641
|
+
const magnitude = Math.abs(Number(match[1]));
|
|
642
|
+
const limit = match[2].toLowerCase() === "px" ? 4e3 : 200;
|
|
643
|
+
if (magnitude > limit) return false;
|
|
644
|
+
}
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
function cleanCssContainer(container, allowedAssetUrls) {
|
|
648
|
+
container.walkComments((comment) => {
|
|
649
|
+
comment.remove();
|
|
650
|
+
});
|
|
651
|
+
container.walkAtRules((atRule) => {
|
|
652
|
+
const name = atRule.name.toLowerCase();
|
|
653
|
+
const isKeyframes = name === "keyframes" || name === "-webkit-keyframes";
|
|
654
|
+
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)) {
|
|
655
|
+
atRule.remove();
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
container.walkDecls((declaration) => {
|
|
659
|
+
const property = declaration.prop.trim().toLowerCase();
|
|
660
|
+
if (!property || !declarationIsSafe(property, declaration.value, allowedAssetUrls)) {
|
|
661
|
+
declaration.remove();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
container.walkRules((rule) => {
|
|
666
|
+
if (!rule.selector.trim() || !rule.nodes?.length || /<\/?(?:style|script)/i.test(rule.selector)) {
|
|
667
|
+
rule.remove();
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
container.walkAtRules("font-face", (fontFace) => {
|
|
671
|
+
const declarations = fontFace.nodes ?? [];
|
|
672
|
+
const hasFamily = declarations.some(
|
|
673
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "font-family"
|
|
674
|
+
);
|
|
675
|
+
const hasSource = declarations.some(
|
|
676
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "src"
|
|
677
|
+
);
|
|
678
|
+
if (!hasFamily || !hasSource) fontFace.remove();
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function sanitizeImportedCss(css, allowedAssetUrls = []) {
|
|
682
|
+
if (!css.trim()) return "";
|
|
683
|
+
try {
|
|
684
|
+
const root = import_postcss.default.parse(css);
|
|
685
|
+
cleanCssContainer(root, new Set(allowedAssetUrls));
|
|
686
|
+
return root.toString().trim();
|
|
687
|
+
} catch {
|
|
688
|
+
return "";
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function sanitizeInlineStyle(style, allowedAssetUrls) {
|
|
692
|
+
try {
|
|
693
|
+
const root = import_postcss.default.parse(`x{${style}}`);
|
|
694
|
+
cleanCssContainer(root, allowedAssetUrls);
|
|
695
|
+
const rule = root.first;
|
|
696
|
+
if (!rule || rule.type !== "rule") return "";
|
|
697
|
+
return rule.nodes.map((node) => node.toString()).join(";");
|
|
698
|
+
} catch {
|
|
699
|
+
return "";
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function sanitizeImportedAttributes(tagName, attributes, allowedAssetUrls) {
|
|
703
|
+
const next = {};
|
|
704
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
705
|
+
const attributeName = name.toLowerCase();
|
|
706
|
+
if (attributeName === "style") {
|
|
707
|
+
const style = sanitizeInlineStyle(value, allowedAssetUrls);
|
|
708
|
+
if (style) next[name] = style;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (attributeName === "srcset") {
|
|
712
|
+
const srcset = sanitizeSrcset(value);
|
|
713
|
+
if (srcset) next[name] = srcset;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
if ((attributeName === "fill" || attributeName === "stroke") && /url\s*\(/i.test(value) && !/^url\(#[a-z_][\w:.-]*\)$/i.test(value.trim())) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if ((tagName === "use" || tagName === "image") && (attributeName === "href" || attributeName === "xlink:href") && !value.trim().startsWith("#")) {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
next[name] = URL_ATTRIBUTES.has(attributeName) && !isSafeUrl(tagName, attributeName, value) ? "#" : value;
|
|
723
|
+
}
|
|
724
|
+
if (tagName === "a" && next.target === "_blank") {
|
|
725
|
+
next.rel = "noopener noreferrer";
|
|
726
|
+
}
|
|
727
|
+
if (tagName === "h1") {
|
|
728
|
+
next[ORIGINAL_H1_ATTRIBUTE] = "h1";
|
|
729
|
+
return { tagName: "h2", attribs: next };
|
|
730
|
+
}
|
|
731
|
+
return { tagName, attribs: next };
|
|
732
|
+
}
|
|
733
|
+
function sanitizeSrcset(value) {
|
|
734
|
+
const candidates = [];
|
|
735
|
+
for (const rawCandidate of value.split(",")) {
|
|
736
|
+
const candidate = rawCandidate.trim();
|
|
737
|
+
if (!candidate) continue;
|
|
738
|
+
const [url, ...descriptorParts] = candidate.split(/\s+/);
|
|
739
|
+
if (!url || !isSafeUrl("img", "src", url)) continue;
|
|
740
|
+
const descriptor = descriptorParts.join(" ");
|
|
741
|
+
if (descriptor && !/^(?:\d+(?:\.\d+)?x|\d+w)$/.test(descriptor)) continue;
|
|
742
|
+
candidates.push(descriptor ? `${url} ${descriptor}` : url);
|
|
743
|
+
}
|
|
744
|
+
return candidates.join(", ");
|
|
745
|
+
}
|
|
746
|
+
var IMPORTED_HTML_OPTIONS = {
|
|
747
|
+
allowedTags: [
|
|
748
|
+
"a",
|
|
749
|
+
"abbr",
|
|
750
|
+
"address",
|
|
751
|
+
"article",
|
|
752
|
+
"aside",
|
|
753
|
+
"b",
|
|
754
|
+
"bdi",
|
|
755
|
+
"bdo",
|
|
756
|
+
"blockquote",
|
|
757
|
+
"br",
|
|
758
|
+
"caption",
|
|
759
|
+
"cite",
|
|
760
|
+
"code",
|
|
761
|
+
"col",
|
|
762
|
+
"colgroup",
|
|
763
|
+
"dd",
|
|
764
|
+
"del",
|
|
765
|
+
"details",
|
|
766
|
+
"dfn",
|
|
767
|
+
"div",
|
|
768
|
+
"dl",
|
|
769
|
+
"dt",
|
|
770
|
+
"em",
|
|
771
|
+
"figcaption",
|
|
772
|
+
"figure",
|
|
773
|
+
"footer",
|
|
774
|
+
"h1",
|
|
775
|
+
"h2",
|
|
776
|
+
"h3",
|
|
777
|
+
"h4",
|
|
778
|
+
"h5",
|
|
779
|
+
"h6",
|
|
780
|
+
"header",
|
|
781
|
+
"hr",
|
|
782
|
+
"i",
|
|
783
|
+
"img",
|
|
784
|
+
"kbd",
|
|
785
|
+
"li",
|
|
786
|
+
"main",
|
|
787
|
+
"mark",
|
|
788
|
+
"nav",
|
|
789
|
+
"ol",
|
|
790
|
+
"p",
|
|
791
|
+
"picture",
|
|
792
|
+
"pre",
|
|
793
|
+
"q",
|
|
794
|
+
"s",
|
|
795
|
+
"samp",
|
|
796
|
+
"section",
|
|
797
|
+
"small",
|
|
798
|
+
"source",
|
|
799
|
+
"svg",
|
|
800
|
+
"g",
|
|
801
|
+
"path",
|
|
802
|
+
"circle",
|
|
803
|
+
"ellipse",
|
|
804
|
+
"line",
|
|
805
|
+
"polyline",
|
|
806
|
+
"polygon",
|
|
807
|
+
"rect",
|
|
808
|
+
"defs",
|
|
809
|
+
"lineargradient",
|
|
810
|
+
"radialgradient",
|
|
811
|
+
"stop",
|
|
812
|
+
"use",
|
|
813
|
+
"clippath",
|
|
814
|
+
"mask",
|
|
815
|
+
"pattern",
|
|
816
|
+
"title",
|
|
817
|
+
"desc",
|
|
818
|
+
"span",
|
|
819
|
+
"strong",
|
|
820
|
+
"sub",
|
|
821
|
+
"summary",
|
|
822
|
+
"sup",
|
|
823
|
+
"table",
|
|
824
|
+
"tbody",
|
|
825
|
+
"td",
|
|
826
|
+
"tfoot",
|
|
827
|
+
"th",
|
|
828
|
+
"thead",
|
|
829
|
+
"time",
|
|
830
|
+
"tr",
|
|
831
|
+
"u",
|
|
832
|
+
"ul",
|
|
833
|
+
"var",
|
|
834
|
+
"wbr"
|
|
835
|
+
],
|
|
836
|
+
allowedAttributes: {
|
|
837
|
+
"*": ["aria-*", "class", "data-*", "dir", "id", "lang", "role", "style", "title"],
|
|
838
|
+
a: [
|
|
839
|
+
"href",
|
|
840
|
+
{ name: "rel", multiple: true, values: ["noopener", "noreferrer"] },
|
|
841
|
+
{ name: "target", values: ["_blank"] }
|
|
842
|
+
],
|
|
843
|
+
col: ["span", "style"],
|
|
844
|
+
img: ["alt", "height", "loading", "sizes", "src", "srcset", "style", "width"],
|
|
845
|
+
source: ["height", "media", "sizes", "src", "srcset", "type", "width"],
|
|
846
|
+
svg: ["aria-*", "class", "fill", "height", "preserveAspectRatio", "role", "stroke", "style", "viewBox", "viewbox", "width", "xmlns"],
|
|
847
|
+
g: ["class", "fill", "stroke", "stroke-width", "style", "transform"],
|
|
848
|
+
path: ["class", "d", "fill", "fill-rule", "stroke", "stroke-linecap", "stroke-linejoin", "stroke-width", "style", "transform"],
|
|
849
|
+
circle: ["class", "cx", "cy", "fill", "r", "stroke", "stroke-width", "style", "transform"],
|
|
850
|
+
ellipse: ["class", "cx", "cy", "fill", "rx", "ry", "stroke", "stroke-width", "style", "transform"],
|
|
851
|
+
line: ["class", "stroke", "stroke-linecap", "stroke-width", "style", "transform", "x1", "x2", "y1", "y2"],
|
|
852
|
+
polyline: ["class", "fill", "points", "stroke", "stroke-linecap", "stroke-linejoin", "stroke-width", "style", "transform"],
|
|
853
|
+
polygon: ["class", "fill", "points", "stroke", "stroke-width", "style", "transform"],
|
|
854
|
+
rect: ["class", "fill", "height", "rx", "ry", "stroke", "stroke-width", "style", "transform", "width", "x", "y"],
|
|
855
|
+
lineargradient: ["gradientTransform", "gradientUnits", "id", "spreadMethod", "x1", "x2", "y1", "y2"],
|
|
856
|
+
radialgradient: ["cx", "cy", "fx", "fy", "gradientTransform", "gradientUnits", "id", "r", "spreadMethod"],
|
|
857
|
+
stop: ["offset", "stop-color", "stop-opacity", "style"],
|
|
858
|
+
use: ["fill", "height", "href", "stroke", "style", "width", "x", "xlink:href", "y"],
|
|
859
|
+
clippath: ["clipPathUnits", "id", "transform"],
|
|
860
|
+
mask: ["height", "id", "maskContentUnits", "maskUnits", "width", "x", "y"],
|
|
861
|
+
pattern: ["height", "id", "patternContentUnits", "patternTransform", "patternUnits", "preserveAspectRatio", "viewBox", "width", "x", "y"],
|
|
862
|
+
td: ["colspan", "rowspan", "style"],
|
|
863
|
+
th: [
|
|
864
|
+
"colspan",
|
|
865
|
+
"rowspan",
|
|
866
|
+
"style",
|
|
867
|
+
{ name: "scope", values: ["col", "row", "colgroup", "rowgroup"] }
|
|
868
|
+
],
|
|
869
|
+
time: ["datetime"]
|
|
870
|
+
},
|
|
871
|
+
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
872
|
+
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
|
873
|
+
allowedSchemesAppliedToAttributes: ["href", "src"],
|
|
874
|
+
allowProtocolRelative: false,
|
|
875
|
+
nonTextTags: ["script", "style", "textarea", "option", "iframe", "object", "embed", "foreignobject", "math", "template"]
|
|
876
|
+
};
|
|
877
|
+
function sanitizeImportedHtml(html, allowedAssetUrls = []) {
|
|
878
|
+
if (!html) return html;
|
|
879
|
+
const allowedSet = new Set(allowedAssetUrls);
|
|
880
|
+
return (0, import_sanitize_html.default)(html, {
|
|
881
|
+
...IMPORTED_HTML_OPTIONS,
|
|
882
|
+
transformTags: {
|
|
883
|
+
"*": (tagName, attributes) => sanitizeImportedAttributes(tagName, attributes, allowedSet)
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
function ensureImportedHtmlDocumentRoots(html, allowedAssetUrls = []) {
|
|
888
|
+
const safeHtml = sanitizeImportedHtml(html, allowedAssetUrls).trim();
|
|
889
|
+
if (!safeHtml) return safeHtml;
|
|
890
|
+
if (/^<div\b[^>]*\bdata-rt-import-document(?:=(?:""|'')|(?=[\s>]))[^>]*>\s*<div\b[^>]*\bdata-rt-import-body(?:=(?:""|'')|(?=[\s>]))/i.test(
|
|
891
|
+
safeHtml
|
|
892
|
+
)) {
|
|
893
|
+
return safeHtml;
|
|
894
|
+
}
|
|
895
|
+
return `<div data-rt-import-document><div data-rt-import-body>${safeHtml}</div></div>`;
|
|
896
|
+
}
|
|
897
|
+
function normalizeDocumentSelector(selector) {
|
|
898
|
+
const typeBoundary = "(?=$|[\\s>+~,.#:\\[\\)])";
|
|
899
|
+
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(
|
|
900
|
+
new RegExp(`(^|[\\s>+~,\\(])h1${typeBoundary}`, "gi"),
|
|
901
|
+
`$1[${ORIGINAL_H1_ATTRIBUTE}="h1"]`
|
|
902
|
+
).replace(/:root/g, "[data-rt-import-document]");
|
|
903
|
+
}
|
|
904
|
+
function selectorTargetsDocumentBoundary(selector) {
|
|
905
|
+
return selector.split(",").some((part) => {
|
|
906
|
+
const normalized = part.trim();
|
|
907
|
+
const targetsDocumentRoot = normalized.startsWith("[data-rt-import-document]") && !/[\s>+~]/.test(normalized.slice("[data-rt-import-document]".length));
|
|
908
|
+
const targetsBodyRoot = /\[data-rt-import-body\](?:[.#:][\w-]+|\[[^\]]+\])*\s*$/.test(
|
|
909
|
+
normalized
|
|
910
|
+
);
|
|
911
|
+
return targetsDocumentRoot || targetsBodyRoot;
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
function replaceAnimationName(value, names) {
|
|
915
|
+
let next = value;
|
|
916
|
+
for (const [name, scopedName] of names) {
|
|
917
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
918
|
+
next = next.replace(new RegExp(`(^|[^\\w-])${escaped}(?=$|[^\\w-])`, "g"), `$1${scopedName}`);
|
|
919
|
+
}
|
|
920
|
+
return next;
|
|
921
|
+
}
|
|
922
|
+
function unquoteCssIdentifier(value) {
|
|
923
|
+
const normalized = value.trim();
|
|
924
|
+
const match = /^(?:"([^"]+)"|'([^']+)'|([a-z_][\w -]*))$/i.exec(normalized);
|
|
925
|
+
return (match?.[1] ?? match?.[2] ?? match?.[3] ?? "").trim() || null;
|
|
926
|
+
}
|
|
927
|
+
function replaceFontFamilyName(value, names) {
|
|
928
|
+
let next = value;
|
|
929
|
+
for (const [name, scopedName] of names) {
|
|
930
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
931
|
+
next = next.replace(
|
|
932
|
+
new RegExp(`(["'])${escaped}\\1|(^|[,\\s])${escaped}(?=$|[,\\s/])`, "gi"),
|
|
933
|
+
(matched, quote, prefix) => quote ? `${quote}${scopedName}${quote}` : `${prefix ?? ""}${scopedName}`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
return next;
|
|
937
|
+
}
|
|
938
|
+
function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
939
|
+
if (!IMPORT_SCOPE_RE.test(scope)) return "";
|
|
940
|
+
const safeCss = sanitizeImportedCss(css, allowedAssetUrls);
|
|
941
|
+
if (!safeCss) return "";
|
|
942
|
+
try {
|
|
943
|
+
const root = import_postcss.default.parse(safeCss);
|
|
944
|
+
const keyframeNames = /* @__PURE__ */ new Map();
|
|
945
|
+
const fontFamilyNames = /* @__PURE__ */ new Map();
|
|
946
|
+
const fontFaces = root.nodes.filter(
|
|
947
|
+
(node) => node.type === "atrule" && node.name.toLowerCase() === "font-face"
|
|
948
|
+
);
|
|
949
|
+
for (const fontFace of fontFaces) {
|
|
950
|
+
if (fontFace.type !== "atrule") continue;
|
|
951
|
+
const family = fontFace.nodes?.find(
|
|
952
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "font-family"
|
|
953
|
+
);
|
|
954
|
+
if (!family || family.type !== "decl") {
|
|
955
|
+
fontFace.remove();
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const originalName = unquoteCssIdentifier(family.value);
|
|
959
|
+
const hasSupportedSource = fontFace.nodes?.some(
|
|
960
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "src" && /url\(\s*["']?https:\/\/[^)'"\s]+\.woff2?(?:[?#][^)'"\s]*)?["']?\s*\)/i.test(
|
|
961
|
+
node.value
|
|
962
|
+
)
|
|
963
|
+
);
|
|
964
|
+
if (!originalName || !hasSupportedSource) {
|
|
965
|
+
fontFace.remove();
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const scopedName = `${scope}-${originalName}`;
|
|
969
|
+
fontFamilyNames.set(originalName, scopedName);
|
|
970
|
+
family.value = family.value.replace(originalName, scopedName);
|
|
971
|
+
}
|
|
972
|
+
root.walkAtRules((atRule) => {
|
|
973
|
+
const name = atRule.name.toLowerCase();
|
|
974
|
+
if (name !== "keyframes" && name !== "-webkit-keyframes") return;
|
|
975
|
+
const originalName = atRule.params.trim();
|
|
976
|
+
const scopedName = `${scope}-${originalName}`;
|
|
977
|
+
keyframeNames.set(originalName, scopedName);
|
|
978
|
+
atRule.params = scopedName;
|
|
979
|
+
});
|
|
980
|
+
root.walkDecls((declaration) => {
|
|
981
|
+
const property = declaration.prop.trim().toLowerCase();
|
|
982
|
+
if (property === "animation" || property === "animation-name") {
|
|
983
|
+
declaration.value = replaceAnimationName(declaration.value, keyframeNames);
|
|
984
|
+
}
|
|
985
|
+
if (property === "font-family" || property === "font") {
|
|
986
|
+
declaration.value = replaceFontFamilyName(
|
|
987
|
+
declaration.value,
|
|
988
|
+
fontFamilyNames
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
root.walkRules((rule) => {
|
|
993
|
+
const parent = rule.parent;
|
|
994
|
+
if (parent?.type === "atrule" && (parent.name.toLowerCase() === "keyframes" || parent.name.toLowerCase() === "-webkit-keyframes")) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
rule.selector = normalizeDocumentSelector(rule.selector);
|
|
998
|
+
if (selectorTargetsDocumentBoundary(rule.selector)) {
|
|
999
|
+
rule.walkDecls((declaration) => {
|
|
1000
|
+
if (SCOPE_ROOT_BOUNDARY_PROPERTIES.has(
|
|
1001
|
+
declaration.prop.trim().toLowerCase()
|
|
1002
|
+
)) {
|
|
1003
|
+
declaration.remove();
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
const scoped = import_postcss.default.atRule({
|
|
1009
|
+
name: "scope",
|
|
1010
|
+
params: `([data-rt-import-scope="${scope}"])`
|
|
1011
|
+
});
|
|
1012
|
+
const preservedFontFaces = fontFaces.filter((fontFace) => fontFace.parent);
|
|
1013
|
+
for (const fontFace of preservedFontFaces) fontFace.remove();
|
|
1014
|
+
scoped.append(root.nodes);
|
|
1015
|
+
return [...preservedFontFaces.map((fontFace) => fontFace.toString()), scoped.toString()].filter(Boolean).join("\n");
|
|
1016
|
+
} catch {
|
|
1017
|
+
return "";
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
453
1021
|
// src/preview-token.ts
|
|
454
1022
|
var import_node_crypto = require("crypto");
|
|
455
1023
|
var DELIMITER = ".";
|
|
@@ -485,15 +1053,58 @@ function verifyPreviewToken(secret, token) {
|
|
|
485
1053
|
}
|
|
486
1054
|
|
|
487
1055
|
// src/sanitize.ts
|
|
488
|
-
var
|
|
1056
|
+
var import_sanitize_html2 = __toESM(require("sanitize-html"), 1);
|
|
489
1057
|
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href"]);
|
|
490
|
-
var
|
|
1058
|
+
var ALLOWED_URL_SCHEMES2 = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
491
1059
|
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
1060
|
var SLASH_EVENT_ATTR_IN_URL_RE = /\/on[a-z]+=/i;
|
|
493
1061
|
var ROOTTALE_CLASS_RE = /^rt-[a-z0-9_-]+$/;
|
|
494
1062
|
var HEX_COLOR_RE = /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i;
|
|
495
1063
|
var TEXT_ALIGN_RE = /^(left|right|center|justify)$/;
|
|
496
1064
|
var ASPECT_RATIO_RE = /^\d+(?:\.\d+)?\s*\/\s*\d+(?:\.\d+)?$/;
|
|
1065
|
+
var CSS_LENGTH_RE = /^(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))$/i;
|
|
1066
|
+
var CSS_LENGTH_LIST_RE = /^(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))(?:\s+(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))){0,3}$/i;
|
|
1067
|
+
var CSS_LINE_HEIGHT_RE = /^(?:normal|\d+(?:\.\d+)?(?:px|em|rem|%)?)$/i;
|
|
1068
|
+
var CSS_FONT_WEIGHT_RE = /^(?:normal|bold|[1-9]00)$/i;
|
|
1069
|
+
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;
|
|
1070
|
+
var CSS_TEXT_DECORATION_RE = /^(?:none|underline|line-through)$/i;
|
|
1071
|
+
var CSS_MAGNITUDE_LIMITS = {
|
|
1072
|
+
"font-size": { "": 0, px: 96, em: 5, rem: 5, "%": 200 },
|
|
1073
|
+
"line-height": { "": 5, px: 96, em: 5, rem: 5, "%": 500 },
|
|
1074
|
+
margin: { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1075
|
+
"margin-bottom": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1076
|
+
"margin-left": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1077
|
+
"margin-right": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1078
|
+
"margin-top": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1079
|
+
padding: { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1080
|
+
"padding-bottom": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1081
|
+
"padding-left": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1082
|
+
"padding-right": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1083
|
+
"padding-top": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1084
|
+
width: { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 },
|
|
1085
|
+
"max-width": { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 },
|
|
1086
|
+
height: { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 }
|
|
1087
|
+
};
|
|
1088
|
+
function hasSafeCssMagnitude(property, value) {
|
|
1089
|
+
const limits = CSS_MAGNITUDE_LIMITS[property];
|
|
1090
|
+
if (!limits) return true;
|
|
1091
|
+
const numericTokens = value.matchAll(/(\d+(?:\.\d+)?)(px|em|rem|%)?/gi);
|
|
1092
|
+
for (const match of numericTokens) {
|
|
1093
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
1094
|
+
const limit = limits[unit];
|
|
1095
|
+
if (limit === void 0 || Number(match[1]) > limit) return false;
|
|
1096
|
+
}
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
function filterUnsafeCssMagnitudes(style) {
|
|
1100
|
+
return style.split(";").map((declaration) => declaration.trim()).filter(Boolean).filter((declaration) => {
|
|
1101
|
+
const colon = declaration.indexOf(":");
|
|
1102
|
+
if (colon < 1) return false;
|
|
1103
|
+
const property = declaration.slice(0, colon).trim().toLowerCase();
|
|
1104
|
+
const value = declaration.slice(colon + 1).trim();
|
|
1105
|
+
return hasSafeCssMagnitude(property, value);
|
|
1106
|
+
}).join(";");
|
|
1107
|
+
}
|
|
497
1108
|
var NAMED_ENTITY = {
|
|
498
1109
|
colon: ":",
|
|
499
1110
|
tab: " ",
|
|
@@ -506,7 +1117,7 @@ var NAMED_ENTITY = {
|
|
|
506
1117
|
lpar: "(",
|
|
507
1118
|
rpar: ")"
|
|
508
1119
|
};
|
|
509
|
-
function
|
|
1120
|
+
function fromCodePointSafe2(n) {
|
|
510
1121
|
if (!Number.isFinite(n) || n < 0 || n > 1114111) return "";
|
|
511
1122
|
try {
|
|
512
1123
|
return String.fromCodePoint(n);
|
|
@@ -515,7 +1126,7 @@ function fromCodePointSafe(n) {
|
|
|
515
1126
|
}
|
|
516
1127
|
}
|
|
517
1128
|
function decodeUrlProbe(value) {
|
|
518
|
-
const decoded = value.replace(/&#x([0-9a-f]+);?/gi, (_, h) =>
|
|
1129
|
+
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
1130
|
return [...decoded].filter((c) => {
|
|
520
1131
|
const n = c.codePointAt(0) ?? 0;
|
|
521
1132
|
return n > 32 && n !== 127;
|
|
@@ -535,7 +1146,7 @@ function isSafeUrlValue(tag, attr, value) {
|
|
|
535
1146
|
if (SLASH_EVENT_ATTR_IN_URL_RE.test(probe)) return false;
|
|
536
1147
|
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe)?.[1];
|
|
537
1148
|
if (!scheme) return true;
|
|
538
|
-
if (
|
|
1149
|
+
if (ALLOWED_URL_SCHEMES2.has(scheme)) return true;
|
|
539
1150
|
if (scheme === "data" && tag === "img" && attr === "src" && probe.startsWith("data:image/")) {
|
|
540
1151
|
return !probe.startsWith("data:image/svg");
|
|
541
1152
|
}
|
|
@@ -545,8 +1156,16 @@ function sanitizeUrlAttributes(tagName, attribs) {
|
|
|
545
1156
|
const next = {};
|
|
546
1157
|
for (const [name, value] of Object.entries(attribs)) {
|
|
547
1158
|
const attr = name.toLowerCase();
|
|
1159
|
+
if (attr === "style") {
|
|
1160
|
+
const style = filterUnsafeCssMagnitudes(value);
|
|
1161
|
+
if (style) next[name] = style;
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
548
1164
|
next[name] = URL_ATTRS.has(attr) && !isSafeUrlValue(tagName, attr, value) ? "#" : value;
|
|
549
1165
|
}
|
|
1166
|
+
if (tagName === "a" && next.target === "_blank") {
|
|
1167
|
+
next.rel = "noopener noreferrer";
|
|
1168
|
+
}
|
|
550
1169
|
return { tagName, attribs: next };
|
|
551
1170
|
}
|
|
552
1171
|
var SANITIZE_OPTIONS = {
|
|
@@ -572,19 +1191,25 @@ var SANITIZE_OPTIONS = {
|
|
|
572
1191
|
"p",
|
|
573
1192
|
"pre",
|
|
574
1193
|
"s",
|
|
1194
|
+
"section",
|
|
1195
|
+
"small",
|
|
575
1196
|
"span",
|
|
576
1197
|
"strong",
|
|
1198
|
+
"sub",
|
|
577
1199
|
"summary",
|
|
1200
|
+
"sup",
|
|
578
1201
|
"table",
|
|
579
1202
|
"tbody",
|
|
580
1203
|
"td",
|
|
1204
|
+
"tfoot",
|
|
581
1205
|
"th",
|
|
1206
|
+
"thead",
|
|
582
1207
|
"tr",
|
|
583
1208
|
"u",
|
|
584
1209
|
"ul"
|
|
585
1210
|
],
|
|
586
1211
|
allowedAttributes: {
|
|
587
|
-
"*": ["class", "data-*"],
|
|
1212
|
+
"*": ["class", "data-*", "style"],
|
|
588
1213
|
a: [
|
|
589
1214
|
"href",
|
|
590
1215
|
"title",
|
|
@@ -601,7 +1226,14 @@ var SANITIZE_OPTIONS = {
|
|
|
601
1226
|
p: ["class", "data-*", "style"],
|
|
602
1227
|
span: ["class", "data-*", "style"],
|
|
603
1228
|
td: ["colspan", "rowspan", "class", "data-*"],
|
|
604
|
-
|
|
1229
|
+
// scope 는 표 머리행의 접근성 의미(스크린리더 열/행 연결) — 값 집합을 고정해 허용.
|
|
1230
|
+
th: [
|
|
1231
|
+
"colspan",
|
|
1232
|
+
"rowspan",
|
|
1233
|
+
"class",
|
|
1234
|
+
"data-*",
|
|
1235
|
+
{ name: "scope", values: ["col", "row", "colgroup", "rowgroup"] }
|
|
1236
|
+
]
|
|
605
1237
|
},
|
|
606
1238
|
allowedClasses: {
|
|
607
1239
|
"*": [ROOTTALE_CLASS_RE]
|
|
@@ -611,8 +1243,31 @@ var SANITIZE_OPTIONS = {
|
|
|
611
1243
|
"aspect-ratio": [ASPECT_RATIO_RE],
|
|
612
1244
|
background: [HEX_COLOR_RE],
|
|
613
1245
|
"background-color": [HEX_COLOR_RE],
|
|
1246
|
+
border: [CSS_BORDER_RE],
|
|
1247
|
+
"border-bottom": [CSS_BORDER_RE],
|
|
1248
|
+
"border-left": [CSS_BORDER_RE],
|
|
1249
|
+
"border-right": [CSS_BORDER_RE],
|
|
1250
|
+
"border-top": [CSS_BORDER_RE],
|
|
1251
|
+
"border-collapse": [/^(?:collapse|separate)$/i],
|
|
614
1252
|
color: [HEX_COLOR_RE],
|
|
615
|
-
"
|
|
1253
|
+
"font-size": [CSS_LENGTH_RE],
|
|
1254
|
+
"font-weight": [CSS_FONT_WEIGHT_RE],
|
|
1255
|
+
height: [CSS_LENGTH_RE, /^auto$/i],
|
|
1256
|
+
"line-height": [CSS_LINE_HEIGHT_RE],
|
|
1257
|
+
margin: [CSS_LENGTH_LIST_RE],
|
|
1258
|
+
"margin-bottom": [CSS_LENGTH_RE],
|
|
1259
|
+
"margin-left": [CSS_LENGTH_RE],
|
|
1260
|
+
"margin-right": [CSS_LENGTH_RE],
|
|
1261
|
+
"margin-top": [CSS_LENGTH_RE],
|
|
1262
|
+
"max-width": [CSS_LENGTH_RE, /^none$/i],
|
|
1263
|
+
padding: [CSS_LENGTH_LIST_RE],
|
|
1264
|
+
"padding-bottom": [CSS_LENGTH_RE],
|
|
1265
|
+
"padding-left": [CSS_LENGTH_RE],
|
|
1266
|
+
"padding-right": [CSS_LENGTH_RE],
|
|
1267
|
+
"padding-top": [CSS_LENGTH_RE],
|
|
1268
|
+
"text-align": [TEXT_ALIGN_RE],
|
|
1269
|
+
"text-decoration": [CSS_TEXT_DECORATION_RE],
|
|
1270
|
+
width: [CSS_LENGTH_RE, /^auto$/i]
|
|
616
1271
|
}
|
|
617
1272
|
},
|
|
618
1273
|
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
@@ -641,7 +1296,18 @@ var SANITIZE_OPTIONS = {
|
|
|
641
1296
|
};
|
|
642
1297
|
function sanitizeHtml(html) {
|
|
643
1298
|
if (!html) return html;
|
|
644
|
-
return (0,
|
|
1299
|
+
return (0, import_sanitize_html2.default)(stripMalformedDangerousTokens(html), SANITIZE_OPTIONS);
|
|
1300
|
+
}
|
|
1301
|
+
function htmlToPlainText(html) {
|
|
1302
|
+
const safeHtml = sanitizeHtml(html);
|
|
1303
|
+
const withBlockBoundaries = safeHtml.replace(
|
|
1304
|
+
/<(?:br|hr)\b[^>]*>|<\/(?:blockquote|details|div|figcaption|figure|h[2-4]|li|p|pre|section|summary|table|tbody|td|th|tr)>/gi,
|
|
1305
|
+
" "
|
|
1306
|
+
);
|
|
1307
|
+
return (0, import_sanitize_html2.default)(withBlockBoundaries, {
|
|
1308
|
+
allowedTags: [],
|
|
1309
|
+
allowedAttributes: {}
|
|
1310
|
+
}).replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
|
|
645
1311
|
}
|
|
646
1312
|
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
1313
|
|
|
@@ -674,9 +1340,11 @@ function injectStableIdsTree(blocks) {
|
|
|
674
1340
|
boundFieldValue,
|
|
675
1341
|
chainResolvers,
|
|
676
1342
|
createFieldBindingResolver,
|
|
1343
|
+
ensureImportedHtmlDocumentRoots,
|
|
677
1344
|
ensureStableIds,
|
|
678
1345
|
extractFaqEntries,
|
|
679
1346
|
faqPageJsonLd,
|
|
1347
|
+
htmlToPlainText,
|
|
680
1348
|
injectStableIdsTree,
|
|
681
1349
|
isAllowedEmbedSrc,
|
|
682
1350
|
isAllowedMapEmbedSrc,
|
|
@@ -689,10 +1357,18 @@ function injectStableIdsTree(blocks) {
|
|
|
689
1357
|
postsForCollection,
|
|
690
1358
|
postsInAnyCollection,
|
|
691
1359
|
postsNotInAnyCollection,
|
|
1360
|
+
resolveCategoryPath,
|
|
1361
|
+
resolveCategoryTreeChain,
|
|
1362
|
+
resolveCategoryTreeEntryPath,
|
|
1363
|
+
resolveCategoryTreeHubPaths,
|
|
1364
|
+
resolvePostCategorySlug,
|
|
692
1365
|
resolvePostCollection,
|
|
693
1366
|
resolvePostPath,
|
|
694
1367
|
resolvePostUrl,
|
|
695
1368
|
sanitizeHtml,
|
|
1369
|
+
sanitizeImportedCss,
|
|
1370
|
+
sanitizeImportedHtml,
|
|
1371
|
+
scopeImportedCss,
|
|
696
1372
|
serializeBlock,
|
|
697
1373
|
validateCollections,
|
|
698
1374
|
verifyPreviewToken
|