@roottale/cms-core 0.55.0 → 0.57.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 +36 -0
- package/dist/index.cjs +308 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -1
- package/dist/index.d.ts +174 -1
- package/dist/index.js +283 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -279,6 +279,27 @@ function resolveCategoryTreeEntryPath(presentation, entry, terms) {
|
|
|
279
279
|
return leafPath ? `${leafPath}/${encodeURIComponent(entry.slug)}` : null;
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
// src/public-path.ts
|
|
283
|
+
var LEGACY_BLOG_DETAIL_PATH = "/blog";
|
|
284
|
+
function resolvePostPublicPath(post, context) {
|
|
285
|
+
const presentation = context.presentation;
|
|
286
|
+
if (presentation && presentation.kind !== "data_only") {
|
|
287
|
+
if (presentation.kind === "fixed_page") return presentation.path;
|
|
288
|
+
if (presentation.kind === "detail") {
|
|
289
|
+
if (!presentation.detailPath.includes(":slug")) return null;
|
|
290
|
+
return presentation.detailPath.replace(":slug", encodeURIComponent(post.slug));
|
|
291
|
+
}
|
|
292
|
+
return resolveCategoryTreeEntryPath(presentation, post, context.categories);
|
|
293
|
+
}
|
|
294
|
+
const collectionKey = post.collectionKey ?? (presentation ? context.compatibilityCollectionKey ?? post.modelKey ?? null : null);
|
|
295
|
+
const routable = { slug: post.slug, terms: post.terms, collectionKey };
|
|
296
|
+
if (!resolvePostCollection(routable, context.collections)) {
|
|
297
|
+
if (presentation) return null;
|
|
298
|
+
return `${LEGACY_BLOG_DETAIL_PATH}/${post.slug}`;
|
|
299
|
+
}
|
|
300
|
+
return resolvePostPath(routable, context.collections);
|
|
301
|
+
}
|
|
302
|
+
|
|
282
303
|
// src/delimiter.ts
|
|
283
304
|
function serializeBlock(block) {
|
|
284
305
|
const exportable = sanitizeAttributesForExport(block);
|
|
@@ -938,6 +959,152 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
|
938
959
|
}
|
|
939
960
|
}
|
|
940
961
|
|
|
962
|
+
// src/internal-content-links.ts
|
|
963
|
+
var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
|
|
964
|
+
function findInternalContentLinkTokens(text) {
|
|
965
|
+
const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
|
|
966
|
+
const tokens = [];
|
|
967
|
+
for (const match of text.matchAll(pattern)) {
|
|
968
|
+
const [raw, key, label] = match;
|
|
969
|
+
if (!key || !label || match.index === void 0) continue;
|
|
970
|
+
tokens.push({
|
|
971
|
+
index: match.index,
|
|
972
|
+
raw,
|
|
973
|
+
key: key.toLowerCase(),
|
|
974
|
+
label: label.trim()
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
return tokens;
|
|
978
|
+
}
|
|
979
|
+
function internalContentKeyFromPath(path) {
|
|
980
|
+
if (!path) return null;
|
|
981
|
+
const segments = path.split("/").filter(Boolean).map((segment) => {
|
|
982
|
+
try {
|
|
983
|
+
return decodeURIComponent(segment).trim().toLowerCase();
|
|
984
|
+
} catch {
|
|
985
|
+
return "";
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
if (segments.length === 0 || segments.some((segment) => segment === "" || segment.includes("."))) {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
return segments.join(".");
|
|
992
|
+
}
|
|
993
|
+
function internalContentPathIndex(entries) {
|
|
994
|
+
const index = /* @__PURE__ */ new Map();
|
|
995
|
+
const previous = /* @__PURE__ */ new Map();
|
|
996
|
+
for (const entry of entries) {
|
|
997
|
+
const path = entry.path;
|
|
998
|
+
const key = internalContentKeyFromPath(path);
|
|
999
|
+
if (!path || !key) continue;
|
|
1000
|
+
index.set(key, path);
|
|
1001
|
+
const segments = key.split(".");
|
|
1002
|
+
for (const oldSlug of entry.previousSlugs ?? []) {
|
|
1003
|
+
const oldKey = [...segments.slice(0, -1), oldSlug.trim().toLowerCase()].join(".");
|
|
1004
|
+
if (oldKey && !oldKey.includes("..")) previous.set(oldKey, path);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
for (const [key, path] of previous) {
|
|
1008
|
+
if (!index.has(key)) index.set(key, path);
|
|
1009
|
+
}
|
|
1010
|
+
return index;
|
|
1011
|
+
}
|
|
1012
|
+
function internalContentLinkResolverFrom(source) {
|
|
1013
|
+
if (!source) return () => null;
|
|
1014
|
+
if (typeof source === "function") return source;
|
|
1015
|
+
return (key) => source.get(key.toLowerCase()) ?? null;
|
|
1016
|
+
}
|
|
1017
|
+
function isSafeInternalPath(path) {
|
|
1018
|
+
return typeof path === "string" && path.startsWith("/") && !path.startsWith("//");
|
|
1019
|
+
}
|
|
1020
|
+
function escapeHtmlText(value) {
|
|
1021
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1022
|
+
}
|
|
1023
|
+
var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
1024
|
+
var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
1025
|
+
var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
|
|
1026
|
+
function renderInternalContentLinksInText(text, resolve) {
|
|
1027
|
+
const tokens = findInternalContentLinkTokens(text);
|
|
1028
|
+
if (tokens.length === 0) return { html: text, hasTokens: false };
|
|
1029
|
+
let cursor = 0;
|
|
1030
|
+
let html = "";
|
|
1031
|
+
for (const token of tokens) {
|
|
1032
|
+
html += text.slice(cursor, token.index);
|
|
1033
|
+
const path = resolve(token.key);
|
|
1034
|
+
const label = escapeHtmlText(token.label);
|
|
1035
|
+
html += isSafeInternalPath(path) ? `<a href="${escapeHtmlText(path)}" class="${INTERNAL_CONTENT_LINK_CLASS}">${label}</a>` : `<span class="${INTERNAL_CONTENT_LINK_CLASS} ${INTERNAL_CONTENT_LINK_PENDING_CLASS}" ${INTERNAL_CONTENT_LINK_PENDING_ATTR}="${escapeHtmlText(token.key)}">${label}</span>`;
|
|
1036
|
+
cursor = token.index + token.raw.length;
|
|
1037
|
+
}
|
|
1038
|
+
html += text.slice(cursor);
|
|
1039
|
+
return { html, hasTokens: true };
|
|
1040
|
+
}
|
|
1041
|
+
var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
1042
|
+
"a",
|
|
1043
|
+
"code",
|
|
1044
|
+
"pre",
|
|
1045
|
+
"script",
|
|
1046
|
+
"style",
|
|
1047
|
+
"textarea"
|
|
1048
|
+
]);
|
|
1049
|
+
var VOID_HTML_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img", "input", "source", "wbr"]);
|
|
1050
|
+
function htmlTagEnd(html, start) {
|
|
1051
|
+
let quote = null;
|
|
1052
|
+
for (let index = start + 1; index < html.length; index += 1) {
|
|
1053
|
+
const character = html[index];
|
|
1054
|
+
if (quote) {
|
|
1055
|
+
if (character === quote) quote = null;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (character === '"' || character === "'") {
|
|
1059
|
+
quote = character;
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (character === ">") return index;
|
|
1063
|
+
}
|
|
1064
|
+
return -1;
|
|
1065
|
+
}
|
|
1066
|
+
function renderInternalContentLinksInHtml(html, resolve) {
|
|
1067
|
+
if (!html.includes("[[internal:")) return { html, hasTokens: false };
|
|
1068
|
+
let blockedDepth = 0;
|
|
1069
|
+
let cursor = 0;
|
|
1070
|
+
let out = "";
|
|
1071
|
+
let hasTokens = false;
|
|
1072
|
+
const emitText = (text) => {
|
|
1073
|
+
if (blockedDepth > 0 || !text) {
|
|
1074
|
+
out += text;
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const rendered = renderInternalContentLinksInText(text, resolve);
|
|
1078
|
+
hasTokens ||= rendered.hasTokens;
|
|
1079
|
+
out += rendered.html;
|
|
1080
|
+
};
|
|
1081
|
+
while (cursor < html.length) {
|
|
1082
|
+
const tagStart = html.indexOf("<", cursor);
|
|
1083
|
+
if (tagStart < 0) {
|
|
1084
|
+
emitText(html.slice(cursor));
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
emitText(html.slice(cursor, tagStart));
|
|
1088
|
+
const tagEnd = htmlTagEnd(html, tagStart);
|
|
1089
|
+
if (tagEnd < 0) {
|
|
1090
|
+
out += html.slice(tagStart);
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
const tag = html.slice(tagStart, tagEnd + 1);
|
|
1094
|
+
out += tag;
|
|
1095
|
+
const parsed = /^<\s*(\/?)\s*([a-z0-9-]+)/i.exec(tag);
|
|
1096
|
+
const tagName = parsed?.[2]?.toLowerCase();
|
|
1097
|
+
if (tagName && INTERNAL_LINK_BLOCKED_HTML_TAGS.has(tagName)) {
|
|
1098
|
+
const isClosing = parsed?.[1] === "/";
|
|
1099
|
+
const isVoid = VOID_HTML_TAGS.has(tagName) || /\/\s*>$/.test(tag);
|
|
1100
|
+
if (isClosing) blockedDepth = Math.max(0, blockedDepth - 1);
|
|
1101
|
+
else if (!isVoid) blockedDepth += 1;
|
|
1102
|
+
}
|
|
1103
|
+
cursor = tagEnd + 1;
|
|
1104
|
+
}
|
|
1105
|
+
return { html: out, hasTokens };
|
|
1106
|
+
}
|
|
1107
|
+
|
|
941
1108
|
// src/preview-token.ts
|
|
942
1109
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
943
1110
|
var DELIMITER = ".";
|
|
@@ -1245,13 +1412,114 @@ function ensureStableIds(block) {
|
|
|
1245
1412
|
function injectStableIdsTree(blocks) {
|
|
1246
1413
|
return blocks.map((b) => ensureStableIds(b));
|
|
1247
1414
|
}
|
|
1415
|
+
|
|
1416
|
+
// src/site-patterns.ts
|
|
1417
|
+
var SITE_PATTERN_SLOTS = [
|
|
1418
|
+
{ key: "post_footer", label: "\uAE00 \uD558\uB2E8", description: "\uAE00 \uBCF8\uBB38 \uBC14\uB85C \uC544\uB798" }
|
|
1419
|
+
];
|
|
1420
|
+
var POST_FOOTER_SLOT = "post_footer";
|
|
1421
|
+
var SITE_PATTERN_PLACEMENTS_KEY = "site_pattern_placements";
|
|
1422
|
+
var SITE_PATTERN_KEY_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
1423
|
+
function isValidSitePatternKey(key) {
|
|
1424
|
+
return SITE_PATTERN_KEY_PATTERN.test(key);
|
|
1425
|
+
}
|
|
1426
|
+
function isSitePatternSlotKey(value) {
|
|
1427
|
+
return SITE_PATTERN_SLOTS.some((slot) => slot.key === value);
|
|
1428
|
+
}
|
|
1429
|
+
var EMPTY_SITE_PATTERN_PLACEMENTS = { rules: [] };
|
|
1430
|
+
function isRecord(value) {
|
|
1431
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1432
|
+
}
|
|
1433
|
+
function readNullableKey(value) {
|
|
1434
|
+
if (value === null) return null;
|
|
1435
|
+
if (typeof value === "string") {
|
|
1436
|
+
const trimmed = value.trim();
|
|
1437
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1438
|
+
}
|
|
1439
|
+
return void 0;
|
|
1440
|
+
}
|
|
1441
|
+
function readSitePatternPlacements(raw) {
|
|
1442
|
+
let value = raw;
|
|
1443
|
+
if (typeof value === "string") {
|
|
1444
|
+
try {
|
|
1445
|
+
value = JSON.parse(value);
|
|
1446
|
+
} catch {
|
|
1447
|
+
return EMPTY_SITE_PATTERN_PLACEMENTS;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (!isRecord(value) || !Array.isArray(value.rules)) {
|
|
1451
|
+
return EMPTY_SITE_PATTERN_PLACEMENTS;
|
|
1452
|
+
}
|
|
1453
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
1454
|
+
for (const item of value.rules) {
|
|
1455
|
+
if (!isRecord(item)) continue;
|
|
1456
|
+
const slot = typeof item.slot === "string" ? item.slot : "";
|
|
1457
|
+
if (!isSitePatternSlotKey(slot)) continue;
|
|
1458
|
+
const collectionKey = readNullableKey(item.collectionKey);
|
|
1459
|
+
if (collectionKey === void 0) continue;
|
|
1460
|
+
const patternKey = readNullableKey(item.patternKey);
|
|
1461
|
+
if (patternKey === void 0) continue;
|
|
1462
|
+
if (patternKey !== null && !isValidSitePatternKey(patternKey)) continue;
|
|
1463
|
+
byTarget.set(`${slot}\0${collectionKey ?? ""}`, {
|
|
1464
|
+
slot,
|
|
1465
|
+
collectionKey,
|
|
1466
|
+
patternKey
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return { rules: [...byTarget.values()] };
|
|
1470
|
+
}
|
|
1471
|
+
function findSitePatternPlacementRule(placements, slot, collectionKey) {
|
|
1472
|
+
return placements.rules.find(
|
|
1473
|
+
(rule) => rule.slot === slot && rule.collectionKey === collectionKey
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
function setSitePatternPlacementRule(placements, target, patternKey) {
|
|
1477
|
+
const rest = placements.rules.filter(
|
|
1478
|
+
(rule) => !(rule.slot === target.slot && rule.collectionKey === target.collectionKey)
|
|
1479
|
+
);
|
|
1480
|
+
if (patternKey === void 0) return { rules: rest };
|
|
1481
|
+
return {
|
|
1482
|
+
rules: [
|
|
1483
|
+
...rest,
|
|
1484
|
+
{ slot: target.slot, collectionKey: target.collectionKey, patternKey }
|
|
1485
|
+
]
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function emptyPatternSlots() {
|
|
1489
|
+
return Object.fromEntries(SITE_PATTERN_SLOTS.map((slot) => [slot.key, null]));
|
|
1490
|
+
}
|
|
1491
|
+
function resolvePatternSlots(post, placements, publishedKeys) {
|
|
1492
|
+
const slots = emptyPatternSlots();
|
|
1493
|
+
for (const slot of SITE_PATTERN_SLOTS) {
|
|
1494
|
+
const specific = post.collectionKey !== null ? findSitePatternPlacementRule(placements, slot.key, post.collectionKey) : void 0;
|
|
1495
|
+
const rule = specific ?? findSitePatternPlacementRule(placements, slot.key, null);
|
|
1496
|
+
const key = rule?.patternKey ?? null;
|
|
1497
|
+
slots[slot.key] = key !== null && publishedKeys.has(key) ? key : null;
|
|
1498
|
+
}
|
|
1499
|
+
return slots;
|
|
1500
|
+
}
|
|
1501
|
+
function referencedSitePatternKeys(placements) {
|
|
1502
|
+
return new Set(
|
|
1503
|
+
placements.rules.map((rule) => rule.patternKey).filter((key) => key !== null)
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1248
1506
|
export {
|
|
1249
1507
|
BLOCK_NAME_PATTERN,
|
|
1508
|
+
EMPTY_SITE_PATTERN_PLACEMENTS,
|
|
1250
1509
|
FIELD_BINDING_SOURCE,
|
|
1251
1510
|
FIELD_BINDING_SOURCE_ALIAS,
|
|
1252
1511
|
IMAGE_ALT_MAX_LENGTH,
|
|
1512
|
+
INTERNAL_CONTENT_LINK_CLASS,
|
|
1513
|
+
INTERNAL_CONTENT_LINK_PENDING_ATTR,
|
|
1514
|
+
INTERNAL_CONTENT_LINK_PENDING_CLASS,
|
|
1515
|
+
INTERNAL_CONTENT_LINK_TOKEN_SOURCE,
|
|
1516
|
+
LEGACY_BLOG_DETAIL_PATH,
|
|
1253
1517
|
PHASE_1_CORE_BLOCKS,
|
|
1254
1518
|
PHASE_1_CSP,
|
|
1519
|
+
POST_FOOTER_SLOT,
|
|
1520
|
+
SITE_PATTERN_KEY_PATTERN,
|
|
1521
|
+
SITE_PATTERN_PLACEMENTS_KEY,
|
|
1522
|
+
SITE_PATTERN_SLOTS,
|
|
1255
1523
|
applyBlockBindings,
|
|
1256
1524
|
bindingSrcValue,
|
|
1257
1525
|
bindingTextValue,
|
|
@@ -1259,16 +1527,24 @@ export {
|
|
|
1259
1527
|
boundFieldValue,
|
|
1260
1528
|
chainResolvers,
|
|
1261
1529
|
createFieldBindingResolver,
|
|
1530
|
+
emptyPatternSlots,
|
|
1262
1531
|
ensureImportedHtmlDocumentRoots,
|
|
1263
1532
|
ensureStableIds,
|
|
1264
1533
|
extractFaqEntries,
|
|
1265
1534
|
faqPageJsonLd,
|
|
1535
|
+
findInternalContentLinkTokens,
|
|
1536
|
+
findSitePatternPlacementRule,
|
|
1266
1537
|
htmlToPlainText,
|
|
1267
1538
|
injectStableIdsTree,
|
|
1539
|
+
internalContentKeyFromPath,
|
|
1540
|
+
internalContentLinkResolverFrom,
|
|
1541
|
+
internalContentPathIndex,
|
|
1268
1542
|
isAllowedEmbedSrc,
|
|
1269
1543
|
isAllowedMapEmbedSrc,
|
|
1270
1544
|
isInCollectionMode,
|
|
1545
|
+
isSitePatternSlotKey,
|
|
1271
1546
|
isValidBlock,
|
|
1547
|
+
isValidSitePatternKey,
|
|
1272
1548
|
issuePreviewToken,
|
|
1273
1549
|
normalizeEmbed,
|
|
1274
1550
|
normalizeImageAlt,
|
|
@@ -1276,19 +1552,26 @@ export {
|
|
|
1276
1552
|
postsForCollection,
|
|
1277
1553
|
postsInAnyCollection,
|
|
1278
1554
|
postsNotInAnyCollection,
|
|
1555
|
+
readSitePatternPlacements,
|
|
1556
|
+
referencedSitePatternKeys,
|
|
1557
|
+
renderInternalContentLinksInHtml,
|
|
1558
|
+
renderInternalContentLinksInText,
|
|
1279
1559
|
resolveCategoryPath,
|
|
1280
1560
|
resolveCategoryTreeChain,
|
|
1281
1561
|
resolveCategoryTreeEntryPath,
|
|
1282
1562
|
resolveCategoryTreeHubPaths,
|
|
1563
|
+
resolvePatternSlots,
|
|
1283
1564
|
resolvePostCategorySlug,
|
|
1284
1565
|
resolvePostCollection,
|
|
1285
1566
|
resolvePostPath,
|
|
1567
|
+
resolvePostPublicPath,
|
|
1286
1568
|
resolvePostUrl,
|
|
1287
1569
|
sanitizeHtml,
|
|
1288
1570
|
sanitizeImportedCss,
|
|
1289
1571
|
sanitizeImportedHtml,
|
|
1290
1572
|
scopeImportedCss,
|
|
1291
1573
|
serializeBlock,
|
|
1574
|
+
setSitePatternPlacementRule,
|
|
1292
1575
|
validateCollections,
|
|
1293
1576
|
verifyPreviewToken
|
|
1294
1577
|
};
|