@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/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("&", "&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
+
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
  };