@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.js
CHANGED
|
@@ -127,18 +127,38 @@ function resolvePostCollection(post, collections) {
|
|
|
127
127
|
return collections.find((c) => c.key === post.collectionKey) ?? null;
|
|
128
128
|
}
|
|
129
129
|
function resolvePostUrl(siteUrl, post, collections) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
const path = resolvePostPath(post, collections);
|
|
131
|
+
return path ? `${trimTrailingSlash(siteUrl)}${path}` : null;
|
|
132
|
+
}
|
|
133
|
+
function resolvePostCategorySlug(post, collection) {
|
|
134
|
+
const allowed = new Set(collection.categories);
|
|
135
|
+
const slugs = [
|
|
136
|
+
...new Set(
|
|
137
|
+
post.terms.filter(
|
|
138
|
+
(term) => term.taxonomy === "category" && allowed.has(term.slug)
|
|
139
|
+
).map((term) => term.slug)
|
|
140
|
+
)
|
|
141
|
+
];
|
|
142
|
+
return slugs.length === 1 ? slugs[0] : null;
|
|
143
|
+
}
|
|
144
|
+
function resolveCategoryPath(collection, categorySlug) {
|
|
145
|
+
if (!collection.archives || !collection.basePath || !categorySlug) return null;
|
|
146
|
+
const basePath = trimTrailingSlash(collection.basePath);
|
|
147
|
+
const encodedSlug = encodeURIComponent(categorySlug);
|
|
148
|
+
return collection.categoryPath === "direct" ? `${basePath}/${encodedSlug}` : `${basePath}/categories/${encodedSlug}`;
|
|
135
149
|
}
|
|
136
150
|
function resolvePostPath(post, collections) {
|
|
137
151
|
const collection = resolvePostCollection(post, collections);
|
|
138
152
|
if (!collection) return null;
|
|
139
153
|
if (collection.detail === false) return null;
|
|
140
154
|
if (!collection.basePath) return null;
|
|
141
|
-
|
|
155
|
+
const basePath = trimTrailingSlash(collection.basePath);
|
|
156
|
+
const encodedPostSlug = encodeURIComponent(post.slug);
|
|
157
|
+
if (collection.detailPath !== "category") {
|
|
158
|
+
return `${basePath}/${encodedPostSlug}`;
|
|
159
|
+
}
|
|
160
|
+
const categorySlug = resolvePostCategorySlug(post, collection);
|
|
161
|
+
return categorySlug ? `${basePath}/${encodeURIComponent(categorySlug)}/${encodedPostSlug}` : null;
|
|
142
162
|
}
|
|
143
163
|
function postsForCollection(posts, collections, key) {
|
|
144
164
|
return posts.filter((p) => resolvePostCollection(p, collections)?.key === key);
|
|
@@ -178,10 +198,87 @@ function validateCollections(collections) {
|
|
|
178
198
|
}
|
|
179
199
|
categoryOwner.set(slug, c.key);
|
|
180
200
|
}
|
|
201
|
+
if (c.detailPath === "category") {
|
|
202
|
+
if (!c.basePath) {
|
|
203
|
+
problems.push(
|
|
204
|
+
`category \uC0C1\uC138 \uACBD\uB85C\uB294 basePath \uAC00 \uD544\uC694\uD568: "${c.key}"`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (c.detail === false) {
|
|
208
|
+
problems.push(
|
|
209
|
+
`category \uC0C1\uC138 \uACBD\uB85C\uB294 \uAE00\uBCC4 \uC0C1\uC138 \uD398\uC774\uC9C0\uAC00 \uD544\uC694\uD568: "${c.key}"`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (!c.archives) {
|
|
213
|
+
problems.push(
|
|
214
|
+
`category \uC0C1\uC138 \uACBD\uB85C\uB294 \uCE74\uD14C\uACE0\uB9AC \uC544\uCE74\uC774\uBE0C\uAC00 \uD544\uC694\uD568: "${c.key}"`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (c.categoryCardinality !== "exactly-one") {
|
|
218
|
+
problems.push(
|
|
219
|
+
`category \uC0C1\uC138 \uACBD\uB85C\uB294 categoryCardinality "exactly-one" \uC774 \uD544\uC694\uD568: "${c.key}"`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
181
223
|
}
|
|
182
224
|
return problems;
|
|
183
225
|
}
|
|
184
226
|
|
|
227
|
+
// src/category-tree.ts
|
|
228
|
+
function normalizeBasePath(basePath) {
|
|
229
|
+
return basePath.replace(/\/+$/, "");
|
|
230
|
+
}
|
|
231
|
+
function resolveCategoryTreeChain(presentation, leafId, terms) {
|
|
232
|
+
if (!Number.isInteger(presentation.categoryDepth) || presentation.categoryDepth < 1) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const termsById = new Map(terms.map((term) => [term.id, term]));
|
|
236
|
+
if (termsById.size !== terms.length) return null;
|
|
237
|
+
if (terms.some((term) => term.parentId === leafId)) return null;
|
|
238
|
+
const reversed = [];
|
|
239
|
+
const visited = /* @__PURE__ */ new Set();
|
|
240
|
+
let currentId = leafId;
|
|
241
|
+
while (currentId) {
|
|
242
|
+
if (visited.has(currentId)) return null;
|
|
243
|
+
visited.add(currentId);
|
|
244
|
+
const current = termsById.get(currentId);
|
|
245
|
+
if (!current || !current.slug) return null;
|
|
246
|
+
reversed.push(current);
|
|
247
|
+
currentId = current.parentId ?? null;
|
|
248
|
+
}
|
|
249
|
+
if (reversed.length !== presentation.categoryDepth) return null;
|
|
250
|
+
return reversed.reverse();
|
|
251
|
+
}
|
|
252
|
+
function resolveCategoryTreeHubPaths(presentation, termId, terms) {
|
|
253
|
+
const chain = resolveCategoryTreeChain(presentation, termId, terms);
|
|
254
|
+
const basePath = normalizeBasePath(presentation.basePath);
|
|
255
|
+
if (!chain || !basePath) return null;
|
|
256
|
+
const paths = [basePath];
|
|
257
|
+
const segments = [];
|
|
258
|
+
for (const term of chain) {
|
|
259
|
+
segments.push(encodeURIComponent(term.slug));
|
|
260
|
+
paths.push(`${basePath}/${segments.join("/")}`);
|
|
261
|
+
}
|
|
262
|
+
return paths;
|
|
263
|
+
}
|
|
264
|
+
function resolveCategoryTreeEntryPath(presentation, entry, terms) {
|
|
265
|
+
if (!entry.slug) return null;
|
|
266
|
+
const knownIds = new Set(terms.map((term) => term.id));
|
|
267
|
+
const categoryIds = [
|
|
268
|
+
...new Set(
|
|
269
|
+
entry.terms.filter((term) => term.taxonomy === "category" && knownIds.has(term.id)).map((term) => term.id)
|
|
270
|
+
)
|
|
271
|
+
];
|
|
272
|
+
if (categoryIds.length !== 1) return null;
|
|
273
|
+
const hubs = resolveCategoryTreeHubPaths(
|
|
274
|
+
presentation,
|
|
275
|
+
categoryIds[0],
|
|
276
|
+
terms
|
|
277
|
+
);
|
|
278
|
+
const leafPath = hubs?.at(-1);
|
|
279
|
+
return leafPath ? `${leafPath}/${encodeURIComponent(entry.slug)}` : null;
|
|
280
|
+
}
|
|
281
|
+
|
|
185
282
|
// src/delimiter.ts
|
|
186
283
|
function serializeBlock(block) {
|
|
187
284
|
const exportable = sanitizeAttributesForExport(block);
|
|
@@ -380,6 +477,467 @@ function normalizeImageAlt(value) {
|
|
|
380
477
|
return `${visible}\u2026`;
|
|
381
478
|
}
|
|
382
479
|
|
|
480
|
+
// src/imported-html-sanitize.ts
|
|
481
|
+
import postcss from "postcss";
|
|
482
|
+
import sanitizeHtmlLibrary from "sanitize-html";
|
|
483
|
+
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;
|
|
484
|
+
var ALLOWED_AT_RULES = /* @__PURE__ */ new Set([
|
|
485
|
+
"container",
|
|
486
|
+
"font-face",
|
|
487
|
+
"keyframes",
|
|
488
|
+
"-webkit-keyframes",
|
|
489
|
+
"layer",
|
|
490
|
+
"media",
|
|
491
|
+
"supports"
|
|
492
|
+
]);
|
|
493
|
+
var URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src"]);
|
|
494
|
+
var ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
495
|
+
var UNSAFE_CSS_VALUE_RE = /(?:expression\s*\(|javascript\s*:|vbscript\s*:|@import|<\/?(?:style|script)|\\0|\\0000)/i;
|
|
496
|
+
var UNSAFE_CSS_PROPERTY_RE = /^(?:behavior|-moz-binding)$/i;
|
|
497
|
+
var FIXED_POSITION_RE = /^(?:fixed|sticky)$/i;
|
|
498
|
+
var ORIGINAL_H1_ATTRIBUTE = "data-rt-original-heading";
|
|
499
|
+
var SCOPE_ROOT_BOUNDARY_PROPERTIES = /* @__PURE__ */ new Set([
|
|
500
|
+
"bottom",
|
|
501
|
+
"inset",
|
|
502
|
+
"inset-block",
|
|
503
|
+
"inset-inline",
|
|
504
|
+
"left",
|
|
505
|
+
"position",
|
|
506
|
+
"right",
|
|
507
|
+
"rotate",
|
|
508
|
+
"scale",
|
|
509
|
+
"top",
|
|
510
|
+
"transform",
|
|
511
|
+
"translate",
|
|
512
|
+
"z-index"
|
|
513
|
+
]);
|
|
514
|
+
function fromCodePointSafe(value, radix) {
|
|
515
|
+
const codePoint = Number.parseInt(value, radix);
|
|
516
|
+
if (!Number.isFinite(codePoint) || codePoint < 0 || codePoint > 1114111) {
|
|
517
|
+
return "";
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
return String.fromCodePoint(codePoint);
|
|
521
|
+
} catch {
|
|
522
|
+
return "";
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function isSafeUrl(tagName, attributeName, value) {
|
|
526
|
+
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();
|
|
527
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(normalized)?.[1];
|
|
528
|
+
if (!scheme) return true;
|
|
529
|
+
if (ALLOWED_URL_SCHEMES.has(scheme)) return true;
|
|
530
|
+
return tagName === "img" && attributeName === "src" && scheme === "data" && normalized.startsWith("data:image/") && !normalized.startsWith("data:image/svg");
|
|
531
|
+
}
|
|
532
|
+
function cssUrlsAreSafe(value, allowedAssetUrls) {
|
|
533
|
+
const urlRe = /url\(\s*(["']?)([^)'"\s]+)\1\s*\)/gi;
|
|
534
|
+
let matched = false;
|
|
535
|
+
for (const match of value.matchAll(urlRe)) {
|
|
536
|
+
matched = true;
|
|
537
|
+
const url = match[2]?.trim() ?? "";
|
|
538
|
+
if (!url.startsWith("#") && !allowedAssetUrls.has(url)) return false;
|
|
539
|
+
}
|
|
540
|
+
return !/url\s*\(/i.test(value) || matched;
|
|
541
|
+
}
|
|
542
|
+
function declarationIsSafe(property, value, allowedAssetUrls) {
|
|
543
|
+
if (UNSAFE_CSS_PROPERTY_RE.test(property) || UNSAFE_CSS_VALUE_RE.test(value)) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
if (!cssUrlsAreSafe(value, allowedAssetUrls)) return false;
|
|
547
|
+
if (property === "position" && /\b(?:fixed|sticky)\b/i.test(value)) {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
if (property.startsWith("--") && FIXED_POSITION_RE.test(value.trim())) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
if (property === "z-index") {
|
|
554
|
+
const normalized = value.trim().toLowerCase();
|
|
555
|
+
if (normalized !== "auto") {
|
|
556
|
+
const zIndex = Number(normalized);
|
|
557
|
+
if (!Number.isInteger(zIndex) || Math.abs(zIndex) > 100) return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
for (const match of value.matchAll(/(-?\d+(?:\.\d+)?)(px|vh|vw)/gi)) {
|
|
561
|
+
const magnitude = Math.abs(Number(match[1]));
|
|
562
|
+
const limit = match[2].toLowerCase() === "px" ? 4e3 : 200;
|
|
563
|
+
if (magnitude > limit) return false;
|
|
564
|
+
}
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
function cleanCssContainer(container, allowedAssetUrls) {
|
|
568
|
+
container.walkComments((comment) => {
|
|
569
|
+
comment.remove();
|
|
570
|
+
});
|
|
571
|
+
container.walkAtRules((atRule) => {
|
|
572
|
+
const name = atRule.name.toLowerCase();
|
|
573
|
+
const isKeyframes = name === "keyframes" || name === "-webkit-keyframes";
|
|
574
|
+
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)) {
|
|
575
|
+
atRule.remove();
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
container.walkDecls((declaration) => {
|
|
579
|
+
const property = declaration.prop.trim().toLowerCase();
|
|
580
|
+
if (!property || !declarationIsSafe(property, declaration.value, allowedAssetUrls)) {
|
|
581
|
+
declaration.remove();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
container.walkRules((rule) => {
|
|
586
|
+
if (!rule.selector.trim() || !rule.nodes?.length || /<\/?(?:style|script)/i.test(rule.selector)) {
|
|
587
|
+
rule.remove();
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
container.walkAtRules("font-face", (fontFace) => {
|
|
591
|
+
const declarations = fontFace.nodes ?? [];
|
|
592
|
+
const hasFamily = declarations.some(
|
|
593
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "font-family"
|
|
594
|
+
);
|
|
595
|
+
const hasSource = declarations.some(
|
|
596
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "src"
|
|
597
|
+
);
|
|
598
|
+
if (!hasFamily || !hasSource) fontFace.remove();
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
function sanitizeImportedCss(css, allowedAssetUrls = []) {
|
|
602
|
+
if (!css.trim()) return "";
|
|
603
|
+
try {
|
|
604
|
+
const root = postcss.parse(css);
|
|
605
|
+
cleanCssContainer(root, new Set(allowedAssetUrls));
|
|
606
|
+
return root.toString().trim();
|
|
607
|
+
} catch {
|
|
608
|
+
return "";
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function sanitizeInlineStyle(style, allowedAssetUrls) {
|
|
612
|
+
try {
|
|
613
|
+
const root = postcss.parse(`x{${style}}`);
|
|
614
|
+
cleanCssContainer(root, allowedAssetUrls);
|
|
615
|
+
const rule = root.first;
|
|
616
|
+
if (!rule || rule.type !== "rule") return "";
|
|
617
|
+
return rule.nodes.map((node) => node.toString()).join(";");
|
|
618
|
+
} catch {
|
|
619
|
+
return "";
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function sanitizeImportedAttributes(tagName, attributes, allowedAssetUrls) {
|
|
623
|
+
const next = {};
|
|
624
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
625
|
+
const attributeName = name.toLowerCase();
|
|
626
|
+
if (attributeName === "style") {
|
|
627
|
+
const style = sanitizeInlineStyle(value, allowedAssetUrls);
|
|
628
|
+
if (style) next[name] = style;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (attributeName === "srcset") {
|
|
632
|
+
const srcset = sanitizeSrcset(value);
|
|
633
|
+
if (srcset) next[name] = srcset;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if ((attributeName === "fill" || attributeName === "stroke") && /url\s*\(/i.test(value) && !/^url\(#[a-z_][\w:.-]*\)$/i.test(value.trim())) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if ((tagName === "use" || tagName === "image") && (attributeName === "href" || attributeName === "xlink:href") && !value.trim().startsWith("#")) {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
next[name] = URL_ATTRIBUTES.has(attributeName) && !isSafeUrl(tagName, attributeName, value) ? "#" : value;
|
|
643
|
+
}
|
|
644
|
+
if (tagName === "a" && next.target === "_blank") {
|
|
645
|
+
next.rel = "noopener noreferrer";
|
|
646
|
+
}
|
|
647
|
+
if (tagName === "h1") {
|
|
648
|
+
next[ORIGINAL_H1_ATTRIBUTE] = "h1";
|
|
649
|
+
return { tagName: "h2", attribs: next };
|
|
650
|
+
}
|
|
651
|
+
return { tagName, attribs: next };
|
|
652
|
+
}
|
|
653
|
+
function sanitizeSrcset(value) {
|
|
654
|
+
const candidates = [];
|
|
655
|
+
for (const rawCandidate of value.split(",")) {
|
|
656
|
+
const candidate = rawCandidate.trim();
|
|
657
|
+
if (!candidate) continue;
|
|
658
|
+
const [url, ...descriptorParts] = candidate.split(/\s+/);
|
|
659
|
+
if (!url || !isSafeUrl("img", "src", url)) continue;
|
|
660
|
+
const descriptor = descriptorParts.join(" ");
|
|
661
|
+
if (descriptor && !/^(?:\d+(?:\.\d+)?x|\d+w)$/.test(descriptor)) continue;
|
|
662
|
+
candidates.push(descriptor ? `${url} ${descriptor}` : url);
|
|
663
|
+
}
|
|
664
|
+
return candidates.join(", ");
|
|
665
|
+
}
|
|
666
|
+
var IMPORTED_HTML_OPTIONS = {
|
|
667
|
+
allowedTags: [
|
|
668
|
+
"a",
|
|
669
|
+
"abbr",
|
|
670
|
+
"address",
|
|
671
|
+
"article",
|
|
672
|
+
"aside",
|
|
673
|
+
"b",
|
|
674
|
+
"bdi",
|
|
675
|
+
"bdo",
|
|
676
|
+
"blockquote",
|
|
677
|
+
"br",
|
|
678
|
+
"caption",
|
|
679
|
+
"cite",
|
|
680
|
+
"code",
|
|
681
|
+
"col",
|
|
682
|
+
"colgroup",
|
|
683
|
+
"dd",
|
|
684
|
+
"del",
|
|
685
|
+
"details",
|
|
686
|
+
"dfn",
|
|
687
|
+
"div",
|
|
688
|
+
"dl",
|
|
689
|
+
"dt",
|
|
690
|
+
"em",
|
|
691
|
+
"figcaption",
|
|
692
|
+
"figure",
|
|
693
|
+
"footer",
|
|
694
|
+
"h1",
|
|
695
|
+
"h2",
|
|
696
|
+
"h3",
|
|
697
|
+
"h4",
|
|
698
|
+
"h5",
|
|
699
|
+
"h6",
|
|
700
|
+
"header",
|
|
701
|
+
"hr",
|
|
702
|
+
"i",
|
|
703
|
+
"img",
|
|
704
|
+
"kbd",
|
|
705
|
+
"li",
|
|
706
|
+
"main",
|
|
707
|
+
"mark",
|
|
708
|
+
"nav",
|
|
709
|
+
"ol",
|
|
710
|
+
"p",
|
|
711
|
+
"picture",
|
|
712
|
+
"pre",
|
|
713
|
+
"q",
|
|
714
|
+
"s",
|
|
715
|
+
"samp",
|
|
716
|
+
"section",
|
|
717
|
+
"small",
|
|
718
|
+
"source",
|
|
719
|
+
"svg",
|
|
720
|
+
"g",
|
|
721
|
+
"path",
|
|
722
|
+
"circle",
|
|
723
|
+
"ellipse",
|
|
724
|
+
"line",
|
|
725
|
+
"polyline",
|
|
726
|
+
"polygon",
|
|
727
|
+
"rect",
|
|
728
|
+
"defs",
|
|
729
|
+
"lineargradient",
|
|
730
|
+
"radialgradient",
|
|
731
|
+
"stop",
|
|
732
|
+
"use",
|
|
733
|
+
"clippath",
|
|
734
|
+
"mask",
|
|
735
|
+
"pattern",
|
|
736
|
+
"title",
|
|
737
|
+
"desc",
|
|
738
|
+
"span",
|
|
739
|
+
"strong",
|
|
740
|
+
"sub",
|
|
741
|
+
"summary",
|
|
742
|
+
"sup",
|
|
743
|
+
"table",
|
|
744
|
+
"tbody",
|
|
745
|
+
"td",
|
|
746
|
+
"tfoot",
|
|
747
|
+
"th",
|
|
748
|
+
"thead",
|
|
749
|
+
"time",
|
|
750
|
+
"tr",
|
|
751
|
+
"u",
|
|
752
|
+
"ul",
|
|
753
|
+
"var",
|
|
754
|
+
"wbr"
|
|
755
|
+
],
|
|
756
|
+
allowedAttributes: {
|
|
757
|
+
"*": ["aria-*", "class", "data-*", "dir", "id", "lang", "role", "style", "title"],
|
|
758
|
+
a: [
|
|
759
|
+
"href",
|
|
760
|
+
{ name: "rel", multiple: true, values: ["noopener", "noreferrer"] },
|
|
761
|
+
{ name: "target", values: ["_blank"] }
|
|
762
|
+
],
|
|
763
|
+
col: ["span", "style"],
|
|
764
|
+
img: ["alt", "height", "loading", "sizes", "src", "srcset", "style", "width"],
|
|
765
|
+
source: ["height", "media", "sizes", "src", "srcset", "type", "width"],
|
|
766
|
+
svg: ["aria-*", "class", "fill", "height", "preserveAspectRatio", "role", "stroke", "style", "viewBox", "viewbox", "width", "xmlns"],
|
|
767
|
+
g: ["class", "fill", "stroke", "stroke-width", "style", "transform"],
|
|
768
|
+
path: ["class", "d", "fill", "fill-rule", "stroke", "stroke-linecap", "stroke-linejoin", "stroke-width", "style", "transform"],
|
|
769
|
+
circle: ["class", "cx", "cy", "fill", "r", "stroke", "stroke-width", "style", "transform"],
|
|
770
|
+
ellipse: ["class", "cx", "cy", "fill", "rx", "ry", "stroke", "stroke-width", "style", "transform"],
|
|
771
|
+
line: ["class", "stroke", "stroke-linecap", "stroke-width", "style", "transform", "x1", "x2", "y1", "y2"],
|
|
772
|
+
polyline: ["class", "fill", "points", "stroke", "stroke-linecap", "stroke-linejoin", "stroke-width", "style", "transform"],
|
|
773
|
+
polygon: ["class", "fill", "points", "stroke", "stroke-width", "style", "transform"],
|
|
774
|
+
rect: ["class", "fill", "height", "rx", "ry", "stroke", "stroke-width", "style", "transform", "width", "x", "y"],
|
|
775
|
+
lineargradient: ["gradientTransform", "gradientUnits", "id", "spreadMethod", "x1", "x2", "y1", "y2"],
|
|
776
|
+
radialgradient: ["cx", "cy", "fx", "fy", "gradientTransform", "gradientUnits", "id", "r", "spreadMethod"],
|
|
777
|
+
stop: ["offset", "stop-color", "stop-opacity", "style"],
|
|
778
|
+
use: ["fill", "height", "href", "stroke", "style", "width", "x", "xlink:href", "y"],
|
|
779
|
+
clippath: ["clipPathUnits", "id", "transform"],
|
|
780
|
+
mask: ["height", "id", "maskContentUnits", "maskUnits", "width", "x", "y"],
|
|
781
|
+
pattern: ["height", "id", "patternContentUnits", "patternTransform", "patternUnits", "preserveAspectRatio", "viewBox", "width", "x", "y"],
|
|
782
|
+
td: ["colspan", "rowspan", "style"],
|
|
783
|
+
th: [
|
|
784
|
+
"colspan",
|
|
785
|
+
"rowspan",
|
|
786
|
+
"style",
|
|
787
|
+
{ name: "scope", values: ["col", "row", "colgroup", "rowgroup"] }
|
|
788
|
+
],
|
|
789
|
+
time: ["datetime"]
|
|
790
|
+
},
|
|
791
|
+
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
792
|
+
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
|
793
|
+
allowedSchemesAppliedToAttributes: ["href", "src"],
|
|
794
|
+
allowProtocolRelative: false,
|
|
795
|
+
nonTextTags: ["script", "style", "textarea", "option", "iframe", "object", "embed", "foreignobject", "math", "template"]
|
|
796
|
+
};
|
|
797
|
+
function sanitizeImportedHtml(html, allowedAssetUrls = []) {
|
|
798
|
+
if (!html) return html;
|
|
799
|
+
const allowedSet = new Set(allowedAssetUrls);
|
|
800
|
+
return sanitizeHtmlLibrary(html, {
|
|
801
|
+
...IMPORTED_HTML_OPTIONS,
|
|
802
|
+
transformTags: {
|
|
803
|
+
"*": (tagName, attributes) => sanitizeImportedAttributes(tagName, attributes, allowedSet)
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
function ensureImportedHtmlDocumentRoots(html, allowedAssetUrls = []) {
|
|
808
|
+
const safeHtml = sanitizeImportedHtml(html, allowedAssetUrls).trim();
|
|
809
|
+
if (!safeHtml) return safeHtml;
|
|
810
|
+
if (/^<div\b[^>]*\bdata-rt-import-document(?:=(?:""|'')|(?=[\s>]))[^>]*>\s*<div\b[^>]*\bdata-rt-import-body(?:=(?:""|'')|(?=[\s>]))/i.test(
|
|
811
|
+
safeHtml
|
|
812
|
+
)) {
|
|
813
|
+
return safeHtml;
|
|
814
|
+
}
|
|
815
|
+
return `<div data-rt-import-document><div data-rt-import-body>${safeHtml}</div></div>`;
|
|
816
|
+
}
|
|
817
|
+
function normalizeDocumentSelector(selector) {
|
|
818
|
+
const typeBoundary = "(?=$|[\\s>+~,.#:\\[\\)])";
|
|
819
|
+
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(
|
|
820
|
+
new RegExp(`(^|[\\s>+~,\\(])h1${typeBoundary}`, "gi"),
|
|
821
|
+
`$1[${ORIGINAL_H1_ATTRIBUTE}="h1"]`
|
|
822
|
+
).replace(/:root/g, "[data-rt-import-document]");
|
|
823
|
+
}
|
|
824
|
+
function selectorTargetsDocumentBoundary(selector) {
|
|
825
|
+
return selector.split(",").some((part) => {
|
|
826
|
+
const normalized = part.trim();
|
|
827
|
+
const targetsDocumentRoot = normalized.startsWith("[data-rt-import-document]") && !/[\s>+~]/.test(normalized.slice("[data-rt-import-document]".length));
|
|
828
|
+
const targetsBodyRoot = /\[data-rt-import-body\](?:[.#:][\w-]+|\[[^\]]+\])*\s*$/.test(
|
|
829
|
+
normalized
|
|
830
|
+
);
|
|
831
|
+
return targetsDocumentRoot || targetsBodyRoot;
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
function replaceAnimationName(value, names) {
|
|
835
|
+
let next = value;
|
|
836
|
+
for (const [name, scopedName] of names) {
|
|
837
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
838
|
+
next = next.replace(new RegExp(`(^|[^\\w-])${escaped}(?=$|[^\\w-])`, "g"), `$1${scopedName}`);
|
|
839
|
+
}
|
|
840
|
+
return next;
|
|
841
|
+
}
|
|
842
|
+
function unquoteCssIdentifier(value) {
|
|
843
|
+
const normalized = value.trim();
|
|
844
|
+
const match = /^(?:"([^"]+)"|'([^']+)'|([a-z_][\w -]*))$/i.exec(normalized);
|
|
845
|
+
return (match?.[1] ?? match?.[2] ?? match?.[3] ?? "").trim() || null;
|
|
846
|
+
}
|
|
847
|
+
function replaceFontFamilyName(value, names) {
|
|
848
|
+
let next = value;
|
|
849
|
+
for (const [name, scopedName] of names) {
|
|
850
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
851
|
+
next = next.replace(
|
|
852
|
+
new RegExp(`(["'])${escaped}\\1|(^|[,\\s])${escaped}(?=$|[,\\s/])`, "gi"),
|
|
853
|
+
(matched, quote, prefix) => quote ? `${quote}${scopedName}${quote}` : `${prefix ?? ""}${scopedName}`
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
return next;
|
|
857
|
+
}
|
|
858
|
+
function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
859
|
+
if (!IMPORT_SCOPE_RE.test(scope)) return "";
|
|
860
|
+
const safeCss = sanitizeImportedCss(css, allowedAssetUrls);
|
|
861
|
+
if (!safeCss) return "";
|
|
862
|
+
try {
|
|
863
|
+
const root = postcss.parse(safeCss);
|
|
864
|
+
const keyframeNames = /* @__PURE__ */ new Map();
|
|
865
|
+
const fontFamilyNames = /* @__PURE__ */ new Map();
|
|
866
|
+
const fontFaces = root.nodes.filter(
|
|
867
|
+
(node) => node.type === "atrule" && node.name.toLowerCase() === "font-face"
|
|
868
|
+
);
|
|
869
|
+
for (const fontFace of fontFaces) {
|
|
870
|
+
if (fontFace.type !== "atrule") continue;
|
|
871
|
+
const family = fontFace.nodes?.find(
|
|
872
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "font-family"
|
|
873
|
+
);
|
|
874
|
+
if (!family || family.type !== "decl") {
|
|
875
|
+
fontFace.remove();
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
const originalName = unquoteCssIdentifier(family.value);
|
|
879
|
+
const hasSupportedSource = fontFace.nodes?.some(
|
|
880
|
+
(node) => node.type === "decl" && node.prop.toLowerCase() === "src" && /url\(\s*["']?https:\/\/[^)'"\s]+\.woff2?(?:[?#][^)'"\s]*)?["']?\s*\)/i.test(
|
|
881
|
+
node.value
|
|
882
|
+
)
|
|
883
|
+
);
|
|
884
|
+
if (!originalName || !hasSupportedSource) {
|
|
885
|
+
fontFace.remove();
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
const scopedName = `${scope}-${originalName}`;
|
|
889
|
+
fontFamilyNames.set(originalName, scopedName);
|
|
890
|
+
family.value = family.value.replace(originalName, scopedName);
|
|
891
|
+
}
|
|
892
|
+
root.walkAtRules((atRule) => {
|
|
893
|
+
const name = atRule.name.toLowerCase();
|
|
894
|
+
if (name !== "keyframes" && name !== "-webkit-keyframes") return;
|
|
895
|
+
const originalName = atRule.params.trim();
|
|
896
|
+
const scopedName = `${scope}-${originalName}`;
|
|
897
|
+
keyframeNames.set(originalName, scopedName);
|
|
898
|
+
atRule.params = scopedName;
|
|
899
|
+
});
|
|
900
|
+
root.walkDecls((declaration) => {
|
|
901
|
+
const property = declaration.prop.trim().toLowerCase();
|
|
902
|
+
if (property === "animation" || property === "animation-name") {
|
|
903
|
+
declaration.value = replaceAnimationName(declaration.value, keyframeNames);
|
|
904
|
+
}
|
|
905
|
+
if (property === "font-family" || property === "font") {
|
|
906
|
+
declaration.value = replaceFontFamilyName(
|
|
907
|
+
declaration.value,
|
|
908
|
+
fontFamilyNames
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
root.walkRules((rule) => {
|
|
913
|
+
const parent = rule.parent;
|
|
914
|
+
if (parent?.type === "atrule" && (parent.name.toLowerCase() === "keyframes" || parent.name.toLowerCase() === "-webkit-keyframes")) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
rule.selector = normalizeDocumentSelector(rule.selector);
|
|
918
|
+
if (selectorTargetsDocumentBoundary(rule.selector)) {
|
|
919
|
+
rule.walkDecls((declaration) => {
|
|
920
|
+
if (SCOPE_ROOT_BOUNDARY_PROPERTIES.has(
|
|
921
|
+
declaration.prop.trim().toLowerCase()
|
|
922
|
+
)) {
|
|
923
|
+
declaration.remove();
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
const scoped = postcss.atRule({
|
|
929
|
+
name: "scope",
|
|
930
|
+
params: `([data-rt-import-scope="${scope}"])`
|
|
931
|
+
});
|
|
932
|
+
const preservedFontFaces = fontFaces.filter((fontFace) => fontFace.parent);
|
|
933
|
+
for (const fontFace of preservedFontFaces) fontFace.remove();
|
|
934
|
+
scoped.append(root.nodes);
|
|
935
|
+
return [...preservedFontFaces.map((fontFace) => fontFace.toString()), scoped.toString()].filter(Boolean).join("\n");
|
|
936
|
+
} catch {
|
|
937
|
+
return "";
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
383
941
|
// src/preview-token.ts
|
|
384
942
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
385
943
|
var DELIMITER = ".";
|
|
@@ -415,15 +973,58 @@ function verifyPreviewToken(secret, token) {
|
|
|
415
973
|
}
|
|
416
974
|
|
|
417
975
|
// src/sanitize.ts
|
|
418
|
-
import
|
|
976
|
+
import sanitizeHtmlLibrary2 from "sanitize-html";
|
|
419
977
|
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href"]);
|
|
420
|
-
var
|
|
978
|
+
var ALLOWED_URL_SCHEMES2 = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
421
979
|
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
980
|
var SLASH_EVENT_ATTR_IN_URL_RE = /\/on[a-z]+=/i;
|
|
423
981
|
var ROOTTALE_CLASS_RE = /^rt-[a-z0-9_-]+$/;
|
|
424
982
|
var HEX_COLOR_RE = /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i;
|
|
425
983
|
var TEXT_ALIGN_RE = /^(left|right|center|justify)$/;
|
|
426
984
|
var ASPECT_RATIO_RE = /^\d+(?:\.\d+)?\s*\/\s*\d+(?:\.\d+)?$/;
|
|
985
|
+
var CSS_LENGTH_RE = /^(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))$/i;
|
|
986
|
+
var CSS_LENGTH_LIST_RE = /^(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))(?:\s+(?:0|\d+(?:\.\d+)?(?:px|em|rem|%))){0,3}$/i;
|
|
987
|
+
var CSS_LINE_HEIGHT_RE = /^(?:normal|\d+(?:\.\d+)?(?:px|em|rem|%)?)$/i;
|
|
988
|
+
var CSS_FONT_WEIGHT_RE = /^(?:normal|bold|[1-9]00)$/i;
|
|
989
|
+
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;
|
|
990
|
+
var CSS_TEXT_DECORATION_RE = /^(?:none|underline|line-through)$/i;
|
|
991
|
+
var CSS_MAGNITUDE_LIMITS = {
|
|
992
|
+
"font-size": { "": 0, px: 96, em: 5, rem: 5, "%": 200 },
|
|
993
|
+
"line-height": { "": 5, px: 96, em: 5, rem: 5, "%": 500 },
|
|
994
|
+
margin: { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
995
|
+
"margin-bottom": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
996
|
+
"margin-left": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
997
|
+
"margin-right": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
998
|
+
"margin-top": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
999
|
+
padding: { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1000
|
+
"padding-bottom": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1001
|
+
"padding-left": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1002
|
+
"padding-right": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1003
|
+
"padding-top": { "": 0, px: 2e3, em: 20, rem: 20, "%": 200 },
|
|
1004
|
+
width: { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 },
|
|
1005
|
+
"max-width": { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 },
|
|
1006
|
+
height: { "": 0, px: 4e3, em: 100, rem: 100, "%": 100 }
|
|
1007
|
+
};
|
|
1008
|
+
function hasSafeCssMagnitude(property, value) {
|
|
1009
|
+
const limits = CSS_MAGNITUDE_LIMITS[property];
|
|
1010
|
+
if (!limits) return true;
|
|
1011
|
+
const numericTokens = value.matchAll(/(\d+(?:\.\d+)?)(px|em|rem|%)?/gi);
|
|
1012
|
+
for (const match of numericTokens) {
|
|
1013
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
1014
|
+
const limit = limits[unit];
|
|
1015
|
+
if (limit === void 0 || Number(match[1]) > limit) return false;
|
|
1016
|
+
}
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
function filterUnsafeCssMagnitudes(style) {
|
|
1020
|
+
return style.split(";").map((declaration) => declaration.trim()).filter(Boolean).filter((declaration) => {
|
|
1021
|
+
const colon = declaration.indexOf(":");
|
|
1022
|
+
if (colon < 1) return false;
|
|
1023
|
+
const property = declaration.slice(0, colon).trim().toLowerCase();
|
|
1024
|
+
const value = declaration.slice(colon + 1).trim();
|
|
1025
|
+
return hasSafeCssMagnitude(property, value);
|
|
1026
|
+
}).join(";");
|
|
1027
|
+
}
|
|
427
1028
|
var NAMED_ENTITY = {
|
|
428
1029
|
colon: ":",
|
|
429
1030
|
tab: " ",
|
|
@@ -436,7 +1037,7 @@ var NAMED_ENTITY = {
|
|
|
436
1037
|
lpar: "(",
|
|
437
1038
|
rpar: ")"
|
|
438
1039
|
};
|
|
439
|
-
function
|
|
1040
|
+
function fromCodePointSafe2(n) {
|
|
440
1041
|
if (!Number.isFinite(n) || n < 0 || n > 1114111) return "";
|
|
441
1042
|
try {
|
|
442
1043
|
return String.fromCodePoint(n);
|
|
@@ -445,7 +1046,7 @@ function fromCodePointSafe(n) {
|
|
|
445
1046
|
}
|
|
446
1047
|
}
|
|
447
1048
|
function decodeUrlProbe(value) {
|
|
448
|
-
const decoded = value.replace(/&#x([0-9a-f]+);?/gi, (_, h) =>
|
|
1049
|
+
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
1050
|
return [...decoded].filter((c) => {
|
|
450
1051
|
const n = c.codePointAt(0) ?? 0;
|
|
451
1052
|
return n > 32 && n !== 127;
|
|
@@ -465,7 +1066,7 @@ function isSafeUrlValue(tag, attr, value) {
|
|
|
465
1066
|
if (SLASH_EVENT_ATTR_IN_URL_RE.test(probe)) return false;
|
|
466
1067
|
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe)?.[1];
|
|
467
1068
|
if (!scheme) return true;
|
|
468
|
-
if (
|
|
1069
|
+
if (ALLOWED_URL_SCHEMES2.has(scheme)) return true;
|
|
469
1070
|
if (scheme === "data" && tag === "img" && attr === "src" && probe.startsWith("data:image/")) {
|
|
470
1071
|
return !probe.startsWith("data:image/svg");
|
|
471
1072
|
}
|
|
@@ -475,8 +1076,16 @@ function sanitizeUrlAttributes(tagName, attribs) {
|
|
|
475
1076
|
const next = {};
|
|
476
1077
|
for (const [name, value] of Object.entries(attribs)) {
|
|
477
1078
|
const attr = name.toLowerCase();
|
|
1079
|
+
if (attr === "style") {
|
|
1080
|
+
const style = filterUnsafeCssMagnitudes(value);
|
|
1081
|
+
if (style) next[name] = style;
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
478
1084
|
next[name] = URL_ATTRS.has(attr) && !isSafeUrlValue(tagName, attr, value) ? "#" : value;
|
|
479
1085
|
}
|
|
1086
|
+
if (tagName === "a" && next.target === "_blank") {
|
|
1087
|
+
next.rel = "noopener noreferrer";
|
|
1088
|
+
}
|
|
480
1089
|
return { tagName, attribs: next };
|
|
481
1090
|
}
|
|
482
1091
|
var SANITIZE_OPTIONS = {
|
|
@@ -502,19 +1111,25 @@ var SANITIZE_OPTIONS = {
|
|
|
502
1111
|
"p",
|
|
503
1112
|
"pre",
|
|
504
1113
|
"s",
|
|
1114
|
+
"section",
|
|
1115
|
+
"small",
|
|
505
1116
|
"span",
|
|
506
1117
|
"strong",
|
|
1118
|
+
"sub",
|
|
507
1119
|
"summary",
|
|
1120
|
+
"sup",
|
|
508
1121
|
"table",
|
|
509
1122
|
"tbody",
|
|
510
1123
|
"td",
|
|
1124
|
+
"tfoot",
|
|
511
1125
|
"th",
|
|
1126
|
+
"thead",
|
|
512
1127
|
"tr",
|
|
513
1128
|
"u",
|
|
514
1129
|
"ul"
|
|
515
1130
|
],
|
|
516
1131
|
allowedAttributes: {
|
|
517
|
-
"*": ["class", "data-*"],
|
|
1132
|
+
"*": ["class", "data-*", "style"],
|
|
518
1133
|
a: [
|
|
519
1134
|
"href",
|
|
520
1135
|
"title",
|
|
@@ -531,7 +1146,14 @@ var SANITIZE_OPTIONS = {
|
|
|
531
1146
|
p: ["class", "data-*", "style"],
|
|
532
1147
|
span: ["class", "data-*", "style"],
|
|
533
1148
|
td: ["colspan", "rowspan", "class", "data-*"],
|
|
534
|
-
|
|
1149
|
+
// scope 는 표 머리행의 접근성 의미(스크린리더 열/행 연결) — 값 집합을 고정해 허용.
|
|
1150
|
+
th: [
|
|
1151
|
+
"colspan",
|
|
1152
|
+
"rowspan",
|
|
1153
|
+
"class",
|
|
1154
|
+
"data-*",
|
|
1155
|
+
{ name: "scope", values: ["col", "row", "colgroup", "rowgroup"] }
|
|
1156
|
+
]
|
|
535
1157
|
},
|
|
536
1158
|
allowedClasses: {
|
|
537
1159
|
"*": [ROOTTALE_CLASS_RE]
|
|
@@ -541,8 +1163,31 @@ var SANITIZE_OPTIONS = {
|
|
|
541
1163
|
"aspect-ratio": [ASPECT_RATIO_RE],
|
|
542
1164
|
background: [HEX_COLOR_RE],
|
|
543
1165
|
"background-color": [HEX_COLOR_RE],
|
|
1166
|
+
border: [CSS_BORDER_RE],
|
|
1167
|
+
"border-bottom": [CSS_BORDER_RE],
|
|
1168
|
+
"border-left": [CSS_BORDER_RE],
|
|
1169
|
+
"border-right": [CSS_BORDER_RE],
|
|
1170
|
+
"border-top": [CSS_BORDER_RE],
|
|
1171
|
+
"border-collapse": [/^(?:collapse|separate)$/i],
|
|
544
1172
|
color: [HEX_COLOR_RE],
|
|
545
|
-
"
|
|
1173
|
+
"font-size": [CSS_LENGTH_RE],
|
|
1174
|
+
"font-weight": [CSS_FONT_WEIGHT_RE],
|
|
1175
|
+
height: [CSS_LENGTH_RE, /^auto$/i],
|
|
1176
|
+
"line-height": [CSS_LINE_HEIGHT_RE],
|
|
1177
|
+
margin: [CSS_LENGTH_LIST_RE],
|
|
1178
|
+
"margin-bottom": [CSS_LENGTH_RE],
|
|
1179
|
+
"margin-left": [CSS_LENGTH_RE],
|
|
1180
|
+
"margin-right": [CSS_LENGTH_RE],
|
|
1181
|
+
"margin-top": [CSS_LENGTH_RE],
|
|
1182
|
+
"max-width": [CSS_LENGTH_RE, /^none$/i],
|
|
1183
|
+
padding: [CSS_LENGTH_LIST_RE],
|
|
1184
|
+
"padding-bottom": [CSS_LENGTH_RE],
|
|
1185
|
+
"padding-left": [CSS_LENGTH_RE],
|
|
1186
|
+
"padding-right": [CSS_LENGTH_RE],
|
|
1187
|
+
"padding-top": [CSS_LENGTH_RE],
|
|
1188
|
+
"text-align": [TEXT_ALIGN_RE],
|
|
1189
|
+
"text-decoration": [CSS_TEXT_DECORATION_RE],
|
|
1190
|
+
width: [CSS_LENGTH_RE, /^auto$/i]
|
|
546
1191
|
}
|
|
547
1192
|
},
|
|
548
1193
|
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
@@ -571,7 +1216,18 @@ var SANITIZE_OPTIONS = {
|
|
|
571
1216
|
};
|
|
572
1217
|
function sanitizeHtml(html) {
|
|
573
1218
|
if (!html) return html;
|
|
574
|
-
return
|
|
1219
|
+
return sanitizeHtmlLibrary2(stripMalformedDangerousTokens(html), SANITIZE_OPTIONS);
|
|
1220
|
+
}
|
|
1221
|
+
function htmlToPlainText(html) {
|
|
1222
|
+
const safeHtml = sanitizeHtml(html);
|
|
1223
|
+
const withBlockBoundaries = safeHtml.replace(
|
|
1224
|
+
/<(?:br|hr)\b[^>]*>|<\/(?:blockquote|details|div|figcaption|figure|h[2-4]|li|p|pre|section|summary|table|tbody|td|th|tr)>/gi,
|
|
1225
|
+
" "
|
|
1226
|
+
);
|
|
1227
|
+
return sanitizeHtmlLibrary2(withBlockBoundaries, {
|
|
1228
|
+
allowedTags: [],
|
|
1229
|
+
allowedAttributes: {}
|
|
1230
|
+
}).replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
|
|
575
1231
|
}
|
|
576
1232
|
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
1233
|
|
|
@@ -603,9 +1259,11 @@ export {
|
|
|
603
1259
|
boundFieldValue,
|
|
604
1260
|
chainResolvers,
|
|
605
1261
|
createFieldBindingResolver,
|
|
1262
|
+
ensureImportedHtmlDocumentRoots,
|
|
606
1263
|
ensureStableIds,
|
|
607
1264
|
extractFaqEntries,
|
|
608
1265
|
faqPageJsonLd,
|
|
1266
|
+
htmlToPlainText,
|
|
609
1267
|
injectStableIdsTree,
|
|
610
1268
|
isAllowedEmbedSrc,
|
|
611
1269
|
isAllowedMapEmbedSrc,
|
|
@@ -618,10 +1276,18 @@ export {
|
|
|
618
1276
|
postsForCollection,
|
|
619
1277
|
postsInAnyCollection,
|
|
620
1278
|
postsNotInAnyCollection,
|
|
1279
|
+
resolveCategoryPath,
|
|
1280
|
+
resolveCategoryTreeChain,
|
|
1281
|
+
resolveCategoryTreeEntryPath,
|
|
1282
|
+
resolveCategoryTreeHubPaths,
|
|
1283
|
+
resolvePostCategorySlug,
|
|
621
1284
|
resolvePostCollection,
|
|
622
1285
|
resolvePostPath,
|
|
623
1286
|
resolvePostUrl,
|
|
624
1287
|
sanitizeHtml,
|
|
1288
|
+
sanitizeImportedCss,
|
|
1289
|
+
sanitizeImportedHtml,
|
|
1290
|
+
scopeImportedCss,
|
|
625
1291
|
serializeBlock,
|
|
626
1292
|
validateCollections,
|
|
627
1293
|
verifyPreviewToken
|