@roottale/cms-core 0.62.0 → 0.64.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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # @roottale/cms-core
2
2
 
3
+ ## 0.64.0
4
+
5
+ ## 0.63.0
6
+
7
+ ### Minor Changes
8
+
9
+ - a858d8e: 목차 배치를 사이트 코드의 `RootTaleBlogPost.tocPosition`으로 관리할 수 있습니다.
10
+ 명시한 값은 CMS 설정보다 우선하며, 생략한 기존 연동은 CMS 배치를 유지합니다.
11
+ 현재 배치를 prop으로 명시하면 실제 화면을 바꾸지 않고 관리 주체를 코드로 옮깁니다.
12
+
13
+ 공통 블록 디자인은 `RootTaleBlogPost.footerPatternPresentation`과
14
+ `RootTalePostPattern.presentation`으로 전체 대체할 수 있습니다. `null`은 원격
15
+ 디자인 주입을 생략하고 사이트 CSS에 맡기며, 생략한 기존 연동은 원격값을 유지합니다.
16
+ 고객 콘텐츠와 제작 디자인을 분리하는 문서·발행/미리보기 예제를 함께 갱신합니다.
17
+
18
+ 공개 `RootTaleCssVars` 타입의 비공개 UI 패키지 참조도 제거해 독립 소비자의
19
+ strict 타입 검사를 지원합니다.
20
+
21
+ 섹션 구성·디자인 변경 검사 함수를 공개 CMS core에서 공유해 편집기와 저장 단계가
22
+ 같은 고객 편집 경계를 적용합니다.
23
+
3
24
  ## 0.62.0
4
25
 
5
26
  ## 0.61.1
package/dist/index.cjs CHANGED
@@ -71,7 +71,9 @@ __export(src_exports, {
71
71
  extractFaqEntries: () => extractFaqEntries,
72
72
  faqPageJsonLd: () => faqPageJsonLd,
73
73
  findInternalContentLinkTokens: () => findInternalContentLinkTokens,
74
+ findSectionLockViolations: () => findSectionLockViolations,
74
75
  findSitePatternPlacementRule: () => findSitePatternPlacementRule,
76
+ formatSectionLockViolations: () => formatSectionLockViolations,
75
77
  htmlToPlainText: () => htmlToPlainText,
76
78
  injectStableIdsTree: () => injectStableIdsTree,
77
79
  internalContentKeyFromPath: () => internalContentKeyFromPath,
@@ -80,6 +82,7 @@ __export(src_exports, {
80
82
  isAllowedEmbedSrc: () => isAllowedEmbedSrc,
81
83
  isAllowedMapEmbedSrc: () => isAllowedMapEmbedSrc,
82
84
  isInCollectionMode: () => isInCollectionMode,
85
+ isSectionContentPropPath: () => isSectionContentPropPath,
83
86
  isSitePatternSlotKey: () => isSitePatternSlotKey,
84
87
  isValidBlock: () => isValidBlock,
85
88
  isValidSitePatternKey: () => isValidSitePatternKey,
@@ -1721,6 +1724,244 @@ function cmsImageStyle(attrs) {
1721
1724
  } : {}
1722
1725
  };
1723
1726
  }
1727
+
1728
+ // src/section-locks.ts
1729
+ var CONTENT_PROP_PATHS = {
1730
+ hero: [
1731
+ ["eyebrow"],
1732
+ ["title"],
1733
+ ["description"],
1734
+ ["imageSrc"],
1735
+ ["imageAlt"],
1736
+ ["cta", "label"],
1737
+ ["cta", "href"]
1738
+ ],
1739
+ "rich-text": [["heading"], ["body"]],
1740
+ services: [
1741
+ ["heading"],
1742
+ ["description"],
1743
+ ["items", Number.NaN, "title"],
1744
+ ["items", Number.NaN, "description"],
1745
+ ["items", Number.NaN, "href"]
1746
+ ],
1747
+ faq: [
1748
+ ["heading"],
1749
+ ["items", Number.NaN, "question"],
1750
+ ["items", Number.NaN, "answer"]
1751
+ ],
1752
+ "cta-banner": [["title"], ["description"], ["cta", "label"], ["cta", "href"]],
1753
+ "blog-list": [["heading"], ["emptyMessage"]],
1754
+ "lead-form": [["heading"], ["description"], ["submitLabel"]],
1755
+ trust: [
1756
+ ["heading"],
1757
+ ["metrics", Number.NaN, "value"],
1758
+ ["metrics", Number.NaN, "unit"],
1759
+ ["metrics", Number.NaN, "label"],
1760
+ ["metrics", Number.NaN, "source"],
1761
+ ["credentials", Number.NaN, "name"],
1762
+ ["credentials", Number.NaN, "issuer"],
1763
+ ["credentials", Number.NaN, "kind"],
1764
+ ["credentials", Number.NaN, "image"],
1765
+ ["logos", Number.NaN, "src"],
1766
+ ["logos", Number.NaN, "alt"],
1767
+ ["logos", Number.NaN, "href"],
1768
+ ["disclaimer"]
1769
+ ],
1770
+ contact: [
1771
+ ["heading"],
1772
+ ["form", "enabled"],
1773
+ ["phone"],
1774
+ ["address"],
1775
+ ["hours", Number.NaN, "day"],
1776
+ ["hours", Number.NaN, "open"],
1777
+ ["hours", Number.NaN, "close"],
1778
+ ["hours", Number.NaN, "note"],
1779
+ ["mapEmbedUrl"]
1780
+ ],
1781
+ expertise: [
1782
+ ["heading"],
1783
+ ["description"],
1784
+ ["items", Number.NaN, "label"],
1785
+ ["items", Number.NaN, "kicker"],
1786
+ ["items", Number.NaN, "title"],
1787
+ ["items", Number.NaN, "body"],
1788
+ ["items", Number.NaN, "imageSrc"],
1789
+ ["items", Number.NaN, "imageAlt"],
1790
+ ["items", Number.NaN, "tags", Number.NaN],
1791
+ ["items", Number.NaN, "href"],
1792
+ ["items", Number.NaN, "ctaLabel"]
1793
+ ],
1794
+ spaces: [
1795
+ ["heading"],
1796
+ ["description"],
1797
+ ["spaces", Number.NaN, "label"],
1798
+ ["spaces", Number.NaN, "kicker"],
1799
+ ["spaces", Number.NaN, "title"],
1800
+ ["spaces", Number.NaN, "description"],
1801
+ ["spaces", Number.NaN, "imageSrc"],
1802
+ ["spaces", Number.NaN, "imageAlt"],
1803
+ ["spaces", Number.NaN, "tags", Number.NaN],
1804
+ ["disclaimer"]
1805
+ ],
1806
+ concerns: [
1807
+ ["heading"],
1808
+ ["description"],
1809
+ ["figureImage"],
1810
+ ["figureAlt"],
1811
+ ["regions", Number.NaN, "label"],
1812
+ ["regions", Number.NaN, "title"],
1813
+ ["regions", Number.NaN, "description"],
1814
+ ["regions", Number.NaN, "items", Number.NaN],
1815
+ ["regions", Number.NaN, "href"],
1816
+ ["regions", Number.NaN, "ctaLabel"],
1817
+ ["disclaimer"]
1818
+ ]
1819
+ };
1820
+ function valueEquals(left, right) {
1821
+ return JSON.stringify(left) === JSON.stringify(right);
1822
+ }
1823
+ function isRecord2(value) {
1824
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1825
+ }
1826
+ function collectChangedPaths(before, after, prefix = []) {
1827
+ if (valueEquals(before, after)) return [];
1828
+ if (Array.isArray(before) && Array.isArray(after)) {
1829
+ if (before.length !== after.length) return [prefix];
1830
+ return before.flatMap(
1831
+ (item, index) => collectChangedPaths(item, after[index], [...prefix, index])
1832
+ );
1833
+ }
1834
+ if ((isRecord2(before) || before == null) && (isRecord2(after) || after == null)) {
1835
+ const beforeRecord = isRecord2(before) ? before : {};
1836
+ const afterRecord = isRecord2(after) ? after : {};
1837
+ const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)]);
1838
+ if (keys.size === 0) return [prefix];
1839
+ return [...keys].flatMap(
1840
+ (key) => collectChangedPaths(beforeRecord[key], afterRecord[key], [...prefix, key])
1841
+ );
1842
+ }
1843
+ return [prefix];
1844
+ }
1845
+ function contentPathMatches(pattern, path) {
1846
+ if (pattern.length !== path.length) return false;
1847
+ return pattern.every((segment, index) => {
1848
+ const actual = path[index];
1849
+ return Number.isNaN(segment) ? typeof actual === "number" : segment === actual;
1850
+ });
1851
+ }
1852
+ function isSectionContentPropPath(sectionType, path) {
1853
+ const patterns = CONTENT_PROP_PATHS[sectionType] ?? [];
1854
+ return patterns.some((pattern) => contentPathMatches(pattern, path));
1855
+ }
1856
+ function collectSectionSnapshots(doc) {
1857
+ const sections = [];
1858
+ function visit(node, path) {
1859
+ if (!isRecord2(node)) return;
1860
+ if (node.type === "section") {
1861
+ const attrs = isRecord2(node.attrs) ? node.attrs : {};
1862
+ sections.push({
1863
+ id: typeof attrs._id === "string" ? attrs._id : "",
1864
+ path,
1865
+ sectionType: typeof attrs.sectionType === "string" ? attrs.sectionType : "",
1866
+ attrs,
1867
+ props: attrs.props ?? {}
1868
+ });
1869
+ }
1870
+ if (Array.isArray(node.content)) {
1871
+ node.content.forEach((child, index) => visit(child, `${path}.${index}`));
1872
+ }
1873
+ }
1874
+ visit(doc, "doc");
1875
+ return sections;
1876
+ }
1877
+ function findSectionLockViolations(beforeDoc, afterDoc) {
1878
+ const before = collectSectionSnapshots(beforeDoc);
1879
+ const after = collectSectionSnapshots(afterDoc);
1880
+ const afterById = new Map(after.map((section) => [section.id, section]));
1881
+ const beforeById = new Map(before.map((section) => [section.id, section]));
1882
+ const violations = [];
1883
+ for (const sections of [before, after]) {
1884
+ const ids = /* @__PURE__ */ new Set();
1885
+ for (const section of sections) {
1886
+ if (!section.id || ids.has(section.id)) {
1887
+ violations.push({
1888
+ code: "locked_section_structure_changed",
1889
+ sectionId: section.id,
1890
+ sectionType: section.sectionType,
1891
+ field: "_id"
1892
+ });
1893
+ }
1894
+ ids.add(section.id);
1895
+ }
1896
+ }
1897
+ for (const section of before) {
1898
+ const next = afterById.get(section.id);
1899
+ if (!next) {
1900
+ violations.push({
1901
+ code: "locked_section_removed",
1902
+ sectionId: section.id,
1903
+ sectionType: section.sectionType,
1904
+ field: "(node)"
1905
+ });
1906
+ continue;
1907
+ }
1908
+ if (section.path !== next.path) {
1909
+ violations.push({
1910
+ code: "locked_section_moved",
1911
+ sectionId: section.id,
1912
+ sectionType: section.sectionType,
1913
+ field: "(index)"
1914
+ });
1915
+ }
1916
+ const fields = /* @__PURE__ */ new Set([
1917
+ ...Object.keys(section.attrs),
1918
+ ...Object.keys(next.attrs)
1919
+ ]);
1920
+ fields.delete("props");
1921
+ for (const field of fields) {
1922
+ if (!valueEquals(section.attrs[field], next.attrs[field])) {
1923
+ violations.push({
1924
+ code: field === "lock" ? "locked_section_lock_changed" : "locked_section_structure_changed",
1925
+ sectionId: section.id,
1926
+ sectionType: section.sectionType,
1927
+ field
1928
+ });
1929
+ }
1930
+ }
1931
+ for (const path of collectChangedPaths(section.props, next.props)) {
1932
+ if (!isSectionContentPropPath(section.sectionType, path)) {
1933
+ violations.push({
1934
+ code: "locked_section_structure_changed",
1935
+ sectionId: section.id,
1936
+ sectionType: section.sectionType,
1937
+ field: `props.${path.join(".") || "(root)"}`
1938
+ });
1939
+ }
1940
+ }
1941
+ }
1942
+ for (const section of after) {
1943
+ if (beforeById.has(section.id)) continue;
1944
+ violations.push({
1945
+ code: "locked_section_inserted",
1946
+ sectionId: section.id,
1947
+ sectionType: section.sectionType,
1948
+ field: "(node)"
1949
+ });
1950
+ }
1951
+ return violations;
1952
+ }
1953
+ function formatSectionLockViolations(violations) {
1954
+ return violations.map((violation) => {
1955
+ const label = `${violation.sectionType || "section"} \uC139\uC158(${violation.sectionId})`;
1956
+ if (violation.code === "locked_section_inserted") return `${label} \uCD94\uAC00`;
1957
+ if (violation.code === "locked_section_removed") return `${label} \uC0AD\uC81C`;
1958
+ if (violation.code === "locked_section_moved") return `${label} \uC774\uB3D9`;
1959
+ if (violation.code === "locked_section_lock_changed") {
1960
+ return `${label} \uC7A0\uAE08 \uBCC0\uACBD`;
1961
+ }
1962
+ return `${label} \uAD6C\uC870 \uBCC0\uACBD(${violation.field})`;
1963
+ }).join(" / ");
1964
+ }
1724
1965
  // Annotate the CommonJS export names for ESM import in node:
1725
1966
  0 && (module.exports = {
1726
1967
  BLOCK_NAME_PATTERN,
@@ -1764,7 +2005,9 @@ function cmsImageStyle(attrs) {
1764
2005
  extractFaqEntries,
1765
2006
  faqPageJsonLd,
1766
2007
  findInternalContentLinkTokens,
2008
+ findSectionLockViolations,
1767
2009
  findSitePatternPlacementRule,
2010
+ formatSectionLockViolations,
1768
2011
  htmlToPlainText,
1769
2012
  injectStableIdsTree,
1770
2013
  internalContentKeyFromPath,
@@ -1773,6 +2016,7 @@ function cmsImageStyle(attrs) {
1773
2016
  isAllowedEmbedSrc,
1774
2017
  isAllowedMapEmbedSrc,
1775
2018
  isInCollectionMode,
2019
+ isSectionContentPropPath,
1776
2020
  isSitePatternSlotKey,
1777
2021
  isValidBlock,
1778
2022
  isValidSitePatternKey,