@roottale/cms-core 0.56.0 → 0.58.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 +40 -0
- package/dist/index.cjs +348 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +153 -1
- package/dist/index.d.ts +153 -1
- package/dist/index.js +319 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -959,6 +959,152 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
|
959
959
|
}
|
|
960
960
|
}
|
|
961
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
|
+
|
|
962
1108
|
// src/preview-token.ts
|
|
963
1109
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
964
1110
|
var DELIMITER = ".";
|
|
@@ -1266,14 +1412,169 @@ function ensureStableIds(block) {
|
|
|
1266
1412
|
function injectStableIdsTree(blocks) {
|
|
1267
1413
|
return blocks.map((b) => ensureStableIds(b));
|
|
1268
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
|
+
}
|
|
1506
|
+
var DEFAULT_SITE_PATTERN_PRESENTATION = {
|
|
1507
|
+
layout: "plain",
|
|
1508
|
+
background: "#f8f9fa",
|
|
1509
|
+
linkStyle: "text",
|
|
1510
|
+
buttonColors: ["#03c75a", "#1a1a1a", "#666666"]
|
|
1511
|
+
};
|
|
1512
|
+
var SITE_PATTERN_BUTTON_COLORS_MAX = 4;
|
|
1513
|
+
var HEX_COLOR_RE2 = /^#[0-9a-f]{6}$/;
|
|
1514
|
+
function normalizeHexColor(value) {
|
|
1515
|
+
if (typeof value !== "string") return null;
|
|
1516
|
+
let hex = value.trim().toLowerCase();
|
|
1517
|
+
if (/^#[0-9a-f]{3}$/.test(hex)) hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
|
|
1518
|
+
return HEX_COLOR_RE2.test(hex) ? hex : null;
|
|
1519
|
+
}
|
|
1520
|
+
function readSitePatternPresentation(raw) {
|
|
1521
|
+
let value = raw;
|
|
1522
|
+
if (typeof value === "string") {
|
|
1523
|
+
try {
|
|
1524
|
+
value = JSON.parse(value);
|
|
1525
|
+
} catch {
|
|
1526
|
+
value = null;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
const base = DEFAULT_SITE_PATTERN_PRESENTATION;
|
|
1530
|
+
if (!isRecord(value)) return base;
|
|
1531
|
+
const layout = value.layout === "card" ? "card" : "plain";
|
|
1532
|
+
const linkStyle = value.linkStyle === "buttons" ? "buttons" : "text";
|
|
1533
|
+
const background = normalizeHexColor(value.background) ?? base.background;
|
|
1534
|
+
const colors = Array.isArray(value.buttonColors) ? value.buttonColors.map(normalizeHexColor).filter((c) => c !== null).slice(0, SITE_PATTERN_BUTTON_COLORS_MAX) : [];
|
|
1535
|
+
return {
|
|
1536
|
+
layout,
|
|
1537
|
+
background,
|
|
1538
|
+
linkStyle,
|
|
1539
|
+
buttonColors: colors.length > 0 ? colors : base.buttonColors
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function sitePatternButtonColor(presentation, index) {
|
|
1543
|
+
const colors = presentation.buttonColors;
|
|
1544
|
+
return colors[Math.min(index, colors.length - 1)] ?? DEFAULT_SITE_PATTERN_PRESENTATION.buttonColors[0];
|
|
1545
|
+
}
|
|
1546
|
+
function sitePatternPresentationAttributes(presentation) {
|
|
1547
|
+
const cssVariables = { "--rt-pattern-bg": presentation.background };
|
|
1548
|
+
presentation.buttonColors.forEach((color, index) => {
|
|
1549
|
+
cssVariables[`--rt-pattern-btn-${index + 1}`] = color;
|
|
1550
|
+
});
|
|
1551
|
+
return {
|
|
1552
|
+
dataset: {
|
|
1553
|
+
"data-pattern-layout": presentation.layout,
|
|
1554
|
+
"data-pattern-link-style": presentation.linkStyle
|
|
1555
|
+
},
|
|
1556
|
+
cssVariables
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1269
1559
|
export {
|
|
1270
1560
|
BLOCK_NAME_PATTERN,
|
|
1561
|
+
DEFAULT_SITE_PATTERN_PRESENTATION,
|
|
1562
|
+
EMPTY_SITE_PATTERN_PLACEMENTS,
|
|
1271
1563
|
FIELD_BINDING_SOURCE,
|
|
1272
1564
|
FIELD_BINDING_SOURCE_ALIAS,
|
|
1273
1565
|
IMAGE_ALT_MAX_LENGTH,
|
|
1566
|
+
INTERNAL_CONTENT_LINK_CLASS,
|
|
1567
|
+
INTERNAL_CONTENT_LINK_PENDING_ATTR,
|
|
1568
|
+
INTERNAL_CONTENT_LINK_PENDING_CLASS,
|
|
1569
|
+
INTERNAL_CONTENT_LINK_TOKEN_SOURCE,
|
|
1274
1570
|
LEGACY_BLOG_DETAIL_PATH,
|
|
1275
1571
|
PHASE_1_CORE_BLOCKS,
|
|
1276
1572
|
PHASE_1_CSP,
|
|
1573
|
+
POST_FOOTER_SLOT,
|
|
1574
|
+
SITE_PATTERN_BUTTON_COLORS_MAX,
|
|
1575
|
+
SITE_PATTERN_KEY_PATTERN,
|
|
1576
|
+
SITE_PATTERN_PLACEMENTS_KEY,
|
|
1577
|
+
SITE_PATTERN_SLOTS,
|
|
1277
1578
|
applyBlockBindings,
|
|
1278
1579
|
bindingSrcValue,
|
|
1279
1580
|
bindingTextValue,
|
|
@@ -1281,27 +1582,42 @@ export {
|
|
|
1281
1582
|
boundFieldValue,
|
|
1282
1583
|
chainResolvers,
|
|
1283
1584
|
createFieldBindingResolver,
|
|
1585
|
+
emptyPatternSlots,
|
|
1284
1586
|
ensureImportedHtmlDocumentRoots,
|
|
1285
1587
|
ensureStableIds,
|
|
1286
1588
|
extractFaqEntries,
|
|
1287
1589
|
faqPageJsonLd,
|
|
1590
|
+
findInternalContentLinkTokens,
|
|
1591
|
+
findSitePatternPlacementRule,
|
|
1288
1592
|
htmlToPlainText,
|
|
1289
1593
|
injectStableIdsTree,
|
|
1594
|
+
internalContentKeyFromPath,
|
|
1595
|
+
internalContentLinkResolverFrom,
|
|
1596
|
+
internalContentPathIndex,
|
|
1290
1597
|
isAllowedEmbedSrc,
|
|
1291
1598
|
isAllowedMapEmbedSrc,
|
|
1292
1599
|
isInCollectionMode,
|
|
1600
|
+
isSitePatternSlotKey,
|
|
1293
1601
|
isValidBlock,
|
|
1602
|
+
isValidSitePatternKey,
|
|
1294
1603
|
issuePreviewToken,
|
|
1295
1604
|
normalizeEmbed,
|
|
1605
|
+
normalizeHexColor,
|
|
1296
1606
|
normalizeImageAlt,
|
|
1297
1607
|
parseBlock,
|
|
1298
1608
|
postsForCollection,
|
|
1299
1609
|
postsInAnyCollection,
|
|
1300
1610
|
postsNotInAnyCollection,
|
|
1611
|
+
readSitePatternPlacements,
|
|
1612
|
+
readSitePatternPresentation,
|
|
1613
|
+
referencedSitePatternKeys,
|
|
1614
|
+
renderInternalContentLinksInHtml,
|
|
1615
|
+
renderInternalContentLinksInText,
|
|
1301
1616
|
resolveCategoryPath,
|
|
1302
1617
|
resolveCategoryTreeChain,
|
|
1303
1618
|
resolveCategoryTreeEntryPath,
|
|
1304
1619
|
resolveCategoryTreeHubPaths,
|
|
1620
|
+
resolvePatternSlots,
|
|
1305
1621
|
resolvePostCategorySlug,
|
|
1306
1622
|
resolvePostCollection,
|
|
1307
1623
|
resolvePostPath,
|
|
@@ -1312,6 +1628,9 @@ export {
|
|
|
1312
1628
|
sanitizeImportedHtml,
|
|
1313
1629
|
scopeImportedCss,
|
|
1314
1630
|
serializeBlock,
|
|
1631
|
+
setSitePatternPlacementRule,
|
|
1632
|
+
sitePatternButtonColor,
|
|
1633
|
+
sitePatternPresentationAttributes,
|
|
1315
1634
|
validateCollections,
|
|
1316
1635
|
verifyPreviewToken
|
|
1317
1636
|
};
|