@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/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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
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
  };