@terminalfour/terminalfour-js 1.0.0-rc.1

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.
Files changed (253) hide show
  1. package/LICENSE.md +106 -0
  2. package/README.md +169 -0
  3. package/dist/cjs/element-resolver.d.ts +115 -0
  4. package/dist/cjs/element-resolver.d.ts.map +1 -0
  5. package/dist/cjs/element-resolver.js +391 -0
  6. package/dist/cjs/element-resolver.js.map +1 -0
  7. package/dist/cjs/errors.d.ts +21 -0
  8. package/dist/cjs/errors.d.ts.map +1 -0
  9. package/dist/cjs/errors.js +21 -0
  10. package/dist/cjs/errors.js.map +1 -0
  11. package/dist/cjs/handlebars.d.ts +123 -0
  12. package/dist/cjs/handlebars.d.ts.map +1 -0
  13. package/dist/cjs/handlebars.js +306 -0
  14. package/dist/cjs/handlebars.js.map +1 -0
  15. package/dist/cjs/http-client.d.ts +21 -0
  16. package/dist/cjs/http-client.d.ts.map +1 -0
  17. package/dist/cjs/http-client.js +126 -0
  18. package/dist/cjs/http-client.js.map +1 -0
  19. package/dist/cjs/index.d.ts +37 -0
  20. package/dist/cjs/index.d.ts.map +1 -0
  21. package/dist/cjs/index.js +55 -0
  22. package/dist/cjs/index.js.map +1 -0
  23. package/dist/cjs/media-category-ref.d.ts +78 -0
  24. package/dist/cjs/media-category-ref.d.ts.map +1 -0
  25. package/dist/cjs/media-category-ref.js +184 -0
  26. package/dist/cjs/media-category-ref.js.map +1 -0
  27. package/dist/cjs/media-library.d.ts +30 -0
  28. package/dist/cjs/media-library.d.ts.map +1 -0
  29. package/dist/cjs/media-library.js +77 -0
  30. package/dist/cjs/media-library.js.map +1 -0
  31. package/dist/cjs/models/content-item.d.ts +80 -0
  32. package/dist/cjs/models/content-item.d.ts.map +1 -0
  33. package/dist/cjs/models/content-item.js +682 -0
  34. package/dist/cjs/models/content-item.js.map +1 -0
  35. package/dist/cjs/models/media-category-item.d.ts +29 -0
  36. package/dist/cjs/models/media-category-item.d.ts.map +1 -0
  37. package/dist/cjs/models/media-category-item.js +33 -0
  38. package/dist/cjs/models/media-category-item.js.map +1 -0
  39. package/dist/cjs/models/media-item.d.ts +74 -0
  40. package/dist/cjs/models/media-item.d.ts.map +1 -0
  41. package/dist/cjs/models/media-item.js +188 -0
  42. package/dist/cjs/models/media-item.js.map +1 -0
  43. package/dist/cjs/models/section-item.d.ts +58 -0
  44. package/dist/cjs/models/section-item.d.ts.map +1 -0
  45. package/dist/cjs/models/section-item.js +166 -0
  46. package/dist/cjs/models/section-item.js.map +1 -0
  47. package/dist/cjs/package.json +1 -0
  48. package/dist/cjs/resources/channel-resource.d.ts +112 -0
  49. package/dist/cjs/resources/channel-resource.d.ts.map +1 -0
  50. package/dist/cjs/resources/channel-resource.js +107 -0
  51. package/dist/cjs/resources/channel-resource.js.map +1 -0
  52. package/dist/cjs/resources/content-resource.d.ts +50 -0
  53. package/dist/cjs/resources/content-resource.d.ts.map +1 -0
  54. package/dist/cjs/resources/content-resource.js +286 -0
  55. package/dist/cjs/resources/content-resource.js.map +1 -0
  56. package/dist/cjs/resources/content-type-resource.d.ts +283 -0
  57. package/dist/cjs/resources/content-type-resource.d.ts.map +1 -0
  58. package/dist/cjs/resources/content-type-resource.js +970 -0
  59. package/dist/cjs/resources/content-type-resource.js.map +1 -0
  60. package/dist/cjs/resources/group-resource.d.ts +96 -0
  61. package/dist/cjs/resources/group-resource.d.ts.map +1 -0
  62. package/dist/cjs/resources/group-resource.js +213 -0
  63. package/dist/cjs/resources/group-resource.js.map +1 -0
  64. package/dist/cjs/resources/list-resource.d.ts +111 -0
  65. package/dist/cjs/resources/list-resource.d.ts.map +1 -0
  66. package/dist/cjs/resources/list-resource.js +179 -0
  67. package/dist/cjs/resources/list-resource.js.map +1 -0
  68. package/dist/cjs/resources/media-resource.d.ts +69 -0
  69. package/dist/cjs/resources/media-resource.d.ts.map +1 -0
  70. package/dist/cjs/resources/media-resource.js +210 -0
  71. package/dist/cjs/resources/media-resource.js.map +1 -0
  72. package/dist/cjs/resources/media-type-resource.d.ts +70 -0
  73. package/dist/cjs/resources/media-type-resource.d.ts.map +1 -0
  74. package/dist/cjs/resources/media-type-resource.js +195 -0
  75. package/dist/cjs/resources/media-type-resource.js.map +1 -0
  76. package/dist/cjs/resources/navigation-resource.d.ts +664 -0
  77. package/dist/cjs/resources/navigation-resource.d.ts.map +1 -0
  78. package/dist/cjs/resources/navigation-resource.js +2349 -0
  79. package/dist/cjs/resources/navigation-resource.js.map +1 -0
  80. package/dist/cjs/resources/page-layout-resource.d.ts +83 -0
  81. package/dist/cjs/resources/page-layout-resource.d.ts.map +1 -0
  82. package/dist/cjs/resources/page-layout-resource.js +214 -0
  83. package/dist/cjs/resources/page-layout-resource.js.map +1 -0
  84. package/dist/cjs/resources/user-resource.d.ts +126 -0
  85. package/dist/cjs/resources/user-resource.d.ts.map +1 -0
  86. package/dist/cjs/resources/user-resource.js +317 -0
  87. package/dist/cjs/resources/user-resource.js.map +1 -0
  88. package/dist/cjs/section-ref.d.ts +185 -0
  89. package/dist/cjs/section-ref.d.ts.map +1 -0
  90. package/dist/cjs/section-ref.js +813 -0
  91. package/dist/cjs/section-ref.js.map +1 -0
  92. package/dist/cjs/site-structure.d.ts +18 -0
  93. package/dist/cjs/site-structure.d.ts.map +1 -0
  94. package/dist/cjs/site-structure.js +57 -0
  95. package/dist/cjs/site-structure.js.map +1 -0
  96. package/dist/cjs/t4-client.d.ts +121 -0
  97. package/dist/cjs/t4-client.d.ts.map +1 -0
  98. package/dist/cjs/t4-client.js +190 -0
  99. package/dist/cjs/t4-client.js.map +1 -0
  100. package/dist/cjs/type-registry.d.ts +31 -0
  101. package/dist/cjs/type-registry.d.ts.map +1 -0
  102. package/dist/cjs/type-registry.js +76 -0
  103. package/dist/cjs/type-registry.js.map +1 -0
  104. package/dist/cjs/types.d.ts +211 -0
  105. package/dist/cjs/types.d.ts.map +1 -0
  106. package/dist/cjs/types.js +3 -0
  107. package/dist/cjs/types.js.map +1 -0
  108. package/dist/cjs/utils.d.ts +147 -0
  109. package/dist/cjs/utils.d.ts.map +1 -0
  110. package/dist/cjs/utils.js +408 -0
  111. package/dist/cjs/utils.js.map +1 -0
  112. package/dist/esm/element-resolver.d.ts +115 -0
  113. package/dist/esm/element-resolver.d.ts.map +1 -0
  114. package/dist/esm/element-resolver.js +387 -0
  115. package/dist/esm/element-resolver.js.map +1 -0
  116. package/dist/esm/errors.d.ts +21 -0
  117. package/dist/esm/errors.d.ts.map +1 -0
  118. package/dist/esm/errors.js +17 -0
  119. package/dist/esm/errors.js.map +1 -0
  120. package/dist/esm/handlebars.d.ts +123 -0
  121. package/dist/esm/handlebars.d.ts.map +1 -0
  122. package/dist/esm/handlebars.js +300 -0
  123. package/dist/esm/handlebars.js.map +1 -0
  124. package/dist/esm/http-client.d.ts +21 -0
  125. package/dist/esm/http-client.d.ts.map +1 -0
  126. package/dist/esm/http-client.js +122 -0
  127. package/dist/esm/http-client.js.map +1 -0
  128. package/dist/esm/index.d.ts +37 -0
  129. package/dist/esm/index.d.ts.map +1 -0
  130. package/dist/esm/index.js +26 -0
  131. package/dist/esm/index.js.map +1 -0
  132. package/dist/esm/media-category-ref.d.ts +78 -0
  133. package/dist/esm/media-category-ref.d.ts.map +1 -0
  134. package/dist/esm/media-category-ref.js +180 -0
  135. package/dist/esm/media-category-ref.js.map +1 -0
  136. package/dist/esm/media-library.d.ts +30 -0
  137. package/dist/esm/media-library.d.ts.map +1 -0
  138. package/dist/esm/media-library.js +73 -0
  139. package/dist/esm/media-library.js.map +1 -0
  140. package/dist/esm/models/content-item.d.ts +80 -0
  141. package/dist/esm/models/content-item.d.ts.map +1 -0
  142. package/dist/esm/models/content-item.js +676 -0
  143. package/dist/esm/models/content-item.js.map +1 -0
  144. package/dist/esm/models/media-category-item.d.ts +29 -0
  145. package/dist/esm/models/media-category-item.d.ts.map +1 -0
  146. package/dist/esm/models/media-category-item.js +29 -0
  147. package/dist/esm/models/media-category-item.js.map +1 -0
  148. package/dist/esm/models/media-item.d.ts +74 -0
  149. package/dist/esm/models/media-item.d.ts.map +1 -0
  150. package/dist/esm/models/media-item.js +184 -0
  151. package/dist/esm/models/media-item.js.map +1 -0
  152. package/dist/esm/models/section-item.d.ts +58 -0
  153. package/dist/esm/models/section-item.d.ts.map +1 -0
  154. package/dist/esm/models/section-item.js +162 -0
  155. package/dist/esm/models/section-item.js.map +1 -0
  156. package/dist/esm/resources/channel-resource.d.ts +112 -0
  157. package/dist/esm/resources/channel-resource.d.ts.map +1 -0
  158. package/dist/esm/resources/channel-resource.js +102 -0
  159. package/dist/esm/resources/channel-resource.js.map +1 -0
  160. package/dist/esm/resources/content-resource.d.ts +50 -0
  161. package/dist/esm/resources/content-resource.d.ts.map +1 -0
  162. package/dist/esm/resources/content-resource.js +282 -0
  163. package/dist/esm/resources/content-resource.js.map +1 -0
  164. package/dist/esm/resources/content-type-resource.d.ts +283 -0
  165. package/dist/esm/resources/content-type-resource.d.ts.map +1 -0
  166. package/dist/esm/resources/content-type-resource.js +964 -0
  167. package/dist/esm/resources/content-type-resource.js.map +1 -0
  168. package/dist/esm/resources/group-resource.d.ts +96 -0
  169. package/dist/esm/resources/group-resource.d.ts.map +1 -0
  170. package/dist/esm/resources/group-resource.js +208 -0
  171. package/dist/esm/resources/group-resource.js.map +1 -0
  172. package/dist/esm/resources/list-resource.d.ts +111 -0
  173. package/dist/esm/resources/list-resource.d.ts.map +1 -0
  174. package/dist/esm/resources/list-resource.js +174 -0
  175. package/dist/esm/resources/list-resource.js.map +1 -0
  176. package/dist/esm/resources/media-resource.d.ts +69 -0
  177. package/dist/esm/resources/media-resource.d.ts.map +1 -0
  178. package/dist/esm/resources/media-resource.js +206 -0
  179. package/dist/esm/resources/media-resource.js.map +1 -0
  180. package/dist/esm/resources/media-type-resource.d.ts +70 -0
  181. package/dist/esm/resources/media-type-resource.d.ts.map +1 -0
  182. package/dist/esm/resources/media-type-resource.js +190 -0
  183. package/dist/esm/resources/media-type-resource.js.map +1 -0
  184. package/dist/esm/resources/navigation-resource.d.ts +664 -0
  185. package/dist/esm/resources/navigation-resource.d.ts.map +1 -0
  186. package/dist/esm/resources/navigation-resource.js +2344 -0
  187. package/dist/esm/resources/navigation-resource.js.map +1 -0
  188. package/dist/esm/resources/page-layout-resource.d.ts +83 -0
  189. package/dist/esm/resources/page-layout-resource.d.ts.map +1 -0
  190. package/dist/esm/resources/page-layout-resource.js +209 -0
  191. package/dist/esm/resources/page-layout-resource.js.map +1 -0
  192. package/dist/esm/resources/user-resource.d.ts +126 -0
  193. package/dist/esm/resources/user-resource.d.ts.map +1 -0
  194. package/dist/esm/resources/user-resource.js +312 -0
  195. package/dist/esm/resources/user-resource.js.map +1 -0
  196. package/dist/esm/section-ref.d.ts +185 -0
  197. package/dist/esm/section-ref.d.ts.map +1 -0
  198. package/dist/esm/section-ref.js +808 -0
  199. package/dist/esm/section-ref.js.map +1 -0
  200. package/dist/esm/site-structure.d.ts +18 -0
  201. package/dist/esm/site-structure.d.ts.map +1 -0
  202. package/dist/esm/site-structure.js +53 -0
  203. package/dist/esm/site-structure.js.map +1 -0
  204. package/dist/esm/t4-client.d.ts +121 -0
  205. package/dist/esm/t4-client.d.ts.map +1 -0
  206. package/dist/esm/t4-client.js +186 -0
  207. package/dist/esm/t4-client.js.map +1 -0
  208. package/dist/esm/type-registry.d.ts +31 -0
  209. package/dist/esm/type-registry.d.ts.map +1 -0
  210. package/dist/esm/type-registry.js +72 -0
  211. package/dist/esm/type-registry.js.map +1 -0
  212. package/dist/esm/types.d.ts +211 -0
  213. package/dist/esm/types.d.ts.map +1 -0
  214. package/dist/esm/types.js +2 -0
  215. package/dist/esm/types.js.map +1 -0
  216. package/dist/esm/utils.d.ts +147 -0
  217. package/dist/esm/utils.d.ts.map +1 -0
  218. package/dist/esm/utils.js +355 -0
  219. package/dist/esm/utils.js.map +1 -0
  220. package/docs/channels.md +62 -0
  221. package/docs/content-types.md +313 -0
  222. package/docs/content.md +199 -0
  223. package/docs/error-handling.md +86 -0
  224. package/docs/getting-started.md +146 -0
  225. package/docs/groups-and-users.md +167 -0
  226. package/docs/handlebars.md +145 -0
  227. package/docs/lists.md +98 -0
  228. package/docs/media-types.md +111 -0
  229. package/docs/media.md +169 -0
  230. package/docs/navigation/a-to-z.md +62 -0
  231. package/docs/navigation/breadcrumbs.md +67 -0
  232. package/docs/navigation/css-selector.md +53 -0
  233. package/docs/navigation/generate-file.md +48 -0
  234. package/docs/navigation/keyword-search.md +134 -0
  235. package/docs/navigation/language-switcher.md +37 -0
  236. package/docs/navigation/link-menu.md +105 -0
  237. package/docs/navigation/pagination.md +80 -0
  238. package/docs/navigation/previous-next-fulltext.md +43 -0
  239. package/docs/navigation/publish-to-one-file.md +93 -0
  240. package/docs/navigation/related-content.md +79 -0
  241. package/docs/navigation/related-section-branch.md +31 -0
  242. package/docs/navigation/return-to-index.md +35 -0
  243. package/docs/navigation/section-details.md +51 -0
  244. package/docs/navigation/section-iterator.md +33 -0
  245. package/docs/navigation/section-meta-info.md +39 -0
  246. package/docs/navigation/site-map.md +58 -0
  247. package/docs/navigation/top-content.md +80 -0
  248. package/docs/navigation/top-stories.md +47 -0
  249. package/docs/navigation.md +134 -0
  250. package/docs/page-layouts.md +71 -0
  251. package/docs/sections.md +224 -0
  252. package/docs/typescript.md +124 -0
  253. package/package.json +64 -0
@@ -0,0 +1,2344 @@
1
+ /** Maps SDK type codes to API type codes */
2
+ const SDK_TO_API = {
3
+ 'a-to-z': 'a2z',
4
+ 'breadcrumbs': 'breadcrumbs',
5
+ 'css-selector': 'css-selector',
6
+ 'generate-file': 'Generate File',
7
+ 'keyword-search': 'keyword',
8
+ 'language-switcher': 'languageswitcher',
9
+ 'link-menu': 'linkmenu',
10
+ 'pagination': 'pagination',
11
+ 'previous-next-fulltext': 'previousNextFulltext',
12
+ 'publish-to-one-file': 'publishonefile',
13
+ 'related-content': 'relatedcontent',
14
+ 'related-section-branch': 'relatedSectionBranch',
15
+ 'return-to-index': 'return-to-index',
16
+ 'section-details': 'sectiondetails',
17
+ 'section-iterator': 'sectioniterator',
18
+ 'section-meta-info': 'meta',
19
+ 'site-map': 'sitemap',
20
+ 'top-content': 'topcontent',
21
+ 'top-stories': 'topstories',
22
+ };
23
+ /** Maps API type codes to SDK type codes */
24
+ const API_TO_SDK = Object.fromEntries(Object.entries(SDK_TO_API).map(([sdk, api]) => [api, sdk]));
25
+ /** Human-readable type name mapping */
26
+ export const NAVIGATION_TYPE_NAMES = {
27
+ 'a-to-z': 'A to Z Navigation',
28
+ 'breadcrumbs': 'Breadcrumbs',
29
+ 'css-selector': 'CSS Selector',
30
+ 'generate-file': 'Generate File',
31
+ 'keyword-search': 'Keyword Search Content',
32
+ 'language-switcher': 'Language Switcher',
33
+ 'link-menu': 'Link Menu',
34
+ 'pagination': 'Pagination',
35
+ 'previous-next-fulltext': 'Previous/Next Fulltext Content',
36
+ 'publish-to-one-file': 'Publish to One File',
37
+ 'related-content': 'Related Content',
38
+ 'related-section-branch': 'Related Section Branch',
39
+ 'return-to-index': 'Return to Index',
40
+ 'section-details': 'Section Details',
41
+ 'section-iterator': 'Section Iterator',
42
+ 'section-meta-info': 'Section Meta Info',
43
+ 'site-map': 'Site Map',
44
+ 'top-content': 'Top Content',
45
+ 'top-stories': 'Top Stories',
46
+ };
47
+ /**
48
+ * Converts a kebab-case, snake_case, or space-separated string to camelCase.
49
+ * e.g. "before-menu" → "beforeMenu", "start_level" → "startLevel", "File Name" → "fileName"
50
+ */
51
+ function toCamelCase(str) {
52
+ return str
53
+ .replace(/[-_ ](.)/g, (_, c) => c.toUpperCase())
54
+ .replace(/^(.)/, (c) => c.toLowerCase());
55
+ }
56
+ /**
57
+ * Converts a camelCase string back to the API's original key format.
58
+ * We store the original keys on the raw data so we can map back precisely.
59
+ */
60
+ function fromCamelCase(camel, originalKeys) {
61
+ const lower = camel.toLowerCase();
62
+ return originalKeys.find((k) => toCamelCase(k) === camel) ?? originalKeys.find((k) => k.toLowerCase().replace(/[-_]/g, '') === lower) ?? camel;
63
+ }
64
+ /**
65
+ * Transforms raw camelCased string properties into friendly typed properties for A to Z.
66
+ * Hides no internal fields (A to Z has no hidden fields).
67
+ */
68
+ function transformA2ZRead(raw) {
69
+ return {
70
+ startLevel: parseInt(raw.startLevel ?? '0', 10),
71
+ endLevel: parseInt(raw.endLevel ?? '0', 10),
72
+ useSectionMetaData: raw.useSectionMetaDataElement === 'yes',
73
+ sectionMetaContentTypeElement: raw.sectionMetaDataTemplate ?? '',
74
+ microSite: raw.selMicroSite ? parseInt(raw.selMicroSite, 10) || null : null,
75
+ beforeMenu: raw.beforeMenu ?? '',
76
+ afterMenu: raw.afterMenu ?? '',
77
+ beforeItem: raw.beforeItem ?? '',
78
+ afterItem: raw.afterItem ?? '',
79
+ };
80
+ }
81
+ /**
82
+ * Transforms friendly typed properties back to camelCased string properties for A to Z save.
83
+ */
84
+ function transformA2ZWrite(props) {
85
+ return {
86
+ startLevel: String(props.startLevel ?? 0),
87
+ endLevel: String(props.endLevel ?? 0),
88
+ useSectionMetaDataElement: props.useSectionMetaData ? 'yes' : 'no',
89
+ sectionMetaDataTemplate: String(props.sectionMetaContentTypeElement ?? ''),
90
+ selMicroSite: props.microSite != null ? String(props.microSite) : '',
91
+ beforeMenu: String(props.beforeMenu ?? ''),
92
+ afterMenu: String(props.afterMenu ?? ''),
93
+ beforeItem: String(props.beforeItem ?? ''),
94
+ afterItem: String(props.afterItem ?? ''),
95
+ };
96
+ }
97
+ /**
98
+ * Transforms raw camelCased string properties into friendly typed properties for Breadcrumbs.
99
+ * Hides internal fields: overSpillFlag, breadcrumbType, appendContentElement.
100
+ * Renames: overSpillLength → maxLength.
101
+ */
102
+ function transformBreadcrumbsRead(raw) {
103
+ return {
104
+ startLevel: parseInt(raw.startLevel ?? '0', 10),
105
+ endLevel: parseInt(raw.endLevel ?? '0', 10),
106
+ useLinks: raw.useLinks === 'yes',
107
+ linkCurrent: raw.linkCurrent === 'yes',
108
+ hideHome: raw.hideHome === 'yes',
109
+ noSpace: raw.noSpace === 'yes',
110
+ maxLength: parseInt(raw.overSpillLength ?? '0', 10),
111
+ separator: raw.separatorHtml ?? '',
112
+ elementToAppend: raw.elementToAppend ?? '',
113
+ beforeHtml: raw.beforeHtml ?? '',
114
+ afterHtml: raw.afterHtml ?? '',
115
+ };
116
+ }
117
+ /**
118
+ * Transforms friendly typed properties back to camelCased string properties for Breadcrumbs save.
119
+ * Derives hidden fields: overSpillFlag, breadcrumbType, appendContentElement.
120
+ */
121
+ function transformBreadcrumbsWrite(props) {
122
+ const maxLength = Number(props.maxLength ?? 0);
123
+ const elementToAppend = String(props.elementToAppend ?? '');
124
+ return {
125
+ startLevel: String(props.startLevel ?? 0),
126
+ endLevel: String(props.endLevel ?? 0),
127
+ useLinks: props.useLinks ? 'yes' : 'no',
128
+ linkCurrent: props.linkCurrent ? 'yes' : 'no',
129
+ hideHome: props.hideHome ? 'yes' : 'no',
130
+ noSpace: props.noSpace ? 'yes' : 'no',
131
+ overSpillLength: String(maxLength),
132
+ overSpillFlag: maxLength > 0 ? 'yes' : 'no',
133
+ breadcrumbType: maxLength > 0 ? '20' : '10',
134
+ appendContentElement: elementToAppend ? 'yes' : 'no',
135
+ elementToAppend,
136
+ separatorHtml: String(props.separator ?? ''),
137
+ beforeHtml: String(props.beforeHtml ?? ''),
138
+ afterHtml: String(props.afterHtml ?? ''),
139
+ };
140
+ }
141
+ /**
142
+ * Transforms raw camelCased string properties into friendly typed properties for CSS Selector.
143
+ * Reconstructs the branches array from numbered properties.
144
+ */
145
+ function transformCssSelectorRead(raw) {
146
+ const result = {
147
+ defaultStylesheet: parseInt(raw.defaultStyleSheet ?? '0', 10) || null,
148
+ language: raw.language ?? '',
149
+ };
150
+ // Reconstruct branches from numbered properties (branch-1-name, branch-1-root, style-sheet-1, etc.)
151
+ const branches = [];
152
+ let i = 1;
153
+ while (true) {
154
+ const stylesheetKey = `styleSheet${i}`;
155
+ if (!(stylesheetKey in raw) || !raw[stylesheetKey])
156
+ break;
157
+ const branch = {
158
+ stylesheet: parseInt(raw[stylesheetKey], 10),
159
+ };
160
+ const nameKey = `branch${i}Name`;
161
+ const rootKey = `branch${i}Root`;
162
+ if (raw[nameKey])
163
+ branch.name = raw[nameKey];
164
+ if (raw[rootKey])
165
+ branch.rootSection = parseInt(raw[rootKey], 10);
166
+ branches.push(branch);
167
+ i++;
168
+ }
169
+ if (branches.length > 0)
170
+ result.branches = branches;
171
+ return result;
172
+ }
173
+ /**
174
+ * Transforms friendly typed properties back to camelCased string properties for CSS Selector save.
175
+ */
176
+ function transformCssSelectorWrite(props) {
177
+ const result = {
178
+ defaultStyleSheet: String(props.defaultStylesheet ?? ''),
179
+ language: String(props.language ?? ''),
180
+ };
181
+ const branches = props.branches;
182
+ if (branches && branches.length > 0) {
183
+ for (let i = 0; i < branches.length; i++) {
184
+ const n = i + 1;
185
+ result[`styleSheet${n}`] = String(branches[i].stylesheet);
186
+ result[`branch${n}Name`] = branches[i].name ?? '';
187
+ result[`branch${n}Root`] = branches[i].rootSection != null ? String(branches[i].rootSection) : '';
188
+ }
189
+ }
190
+ return result;
191
+ }
192
+ /**
193
+ * Transforms raw camelCased string properties into friendly typed properties for Generate File.
194
+ * Note: API keys like "File Name", "Append Content ID" become "fileName", "appendContentID" after toCamelCase.
195
+ */
196
+ function transformGenerateFileRead(raw) {
197
+ return {
198
+ fileName: raw.fileName ?? '',
199
+ appendContentId: raw.appendContentID === 'yes',
200
+ fileExtension: raw.fileExtension ?? '',
201
+ baseDirectory: raw.baseDirectory ?? '',
202
+ layout: raw.formatter ?? '',
203
+ appendDirectory: raw.appendDirectory === 'yes',
204
+ mediaFile: raw.mediaFile ? parseInt(raw.mediaFile, 10) || null : null,
205
+ };
206
+ }
207
+ /**
208
+ * Transforms friendly typed properties back to camelCased string properties for Generate File save.
209
+ * Keys match what toCamelCase produces from the API's space-separated keys.
210
+ */
211
+ function transformGenerateFileWrite(props) {
212
+ const result = {
213
+ fileName: String(props.fileName ?? ''),
214
+ appendContentID: props.appendContentId ? 'yes' : 'no',
215
+ fileExtension: String(props.fileExtension ?? ''),
216
+ baseDirectory: String(props.baseDirectory ?? ''),
217
+ formatter: String(props.layout ?? ''),
218
+ appendDirectory: props.appendDirectory ? 'yes' : 'no',
219
+ };
220
+ // mediaFile needs special handling — empty object when not set
221
+ if (props.mediaFile != null) {
222
+ result.mediaFile = String(props.mediaFile);
223
+ }
224
+ return result;
225
+ }
226
+ /**
227
+ * Transforms raw camelCased string properties into friendly typed properties for Language Switcher.
228
+ * Hides: imageLink (derived field).
229
+ */
230
+ function transformLanguageSwitcherRead(raw) {
231
+ return {
232
+ langCode: raw.langCode ?? '',
233
+ alwaysOutput: raw.alwaysOutput === 'yes',
234
+ imageUrl: raw.url ?? '',
235
+ imageExtension: raw.imageExt ?? '',
236
+ imageProperties: raw.imageProperties ?? '',
237
+ beforeHtml: raw.before ?? '',
238
+ afterHtml: raw.after ?? '',
239
+ };
240
+ }
241
+ /**
242
+ * Transforms friendly typed properties back to camelCased string properties for Language Switcher save.
243
+ * Derives: imageLink from whether any image fields are set.
244
+ */
245
+ function transformLanguageSwitcherWrite(props) {
246
+ const imageUrl = String(props.imageUrl ?? '');
247
+ const imageExtension = String(props.imageExtension ?? '');
248
+ const imageProperties = String(props.imageProperties ?? '');
249
+ const imageLink = imageUrl || imageExtension || imageProperties ? 'yes' : 'no';
250
+ return {
251
+ langCode: String(props.langCode ?? ''),
252
+ alwaysOutput: props.alwaysOutput ? 'yes' : 'no',
253
+ imageLink,
254
+ url: imageUrl,
255
+ imageExt: imageExtension,
256
+ imageProperties,
257
+ before: String(props.beforeHtml ?? ''),
258
+ after: String(props.afterHtml ?? ''),
259
+ };
260
+ }
261
+ /**
262
+ * Transforms raw camelCased string properties into friendly typed properties for Pagination.
263
+ * Hides: useAltFormatter (derived). Renames fields to SDK-friendly names.
264
+ */
265
+ function transformPaginationRead(raw) {
266
+ const fetchMethodApi = raw.fetchMethod ?? 'fetch-method-current';
267
+ return {
268
+ contentTypeId: parseInt(raw.templateList ?? '0', 10),
269
+ fetchMethod: FETCH_METHOD_FROM_API[fetchMethodApi] ?? 'current',
270
+ section: parseInt(raw.section ?? '0', 10) || 0,
271
+ level: parseInt(raw.level ?? '0', 10),
272
+ numToRecurse: parseInt(raw.numToRecurse ?? '0', 10),
273
+ contentItemsPerPage: parseInt(raw.numberOfPieces ?? '0', 10),
274
+ maxContentItems: parseInt(raw.maxNumberOfPieces ?? '0', 10),
275
+ maxLinksPerPage: parseInt(raw.numLinksToShow ?? '0', 10),
276
+ altLayoutName: raw.altFormatterType ?? '',
277
+ searchHiddenSections: raw.showHiddenSections === 'yes',
278
+ beforeHtml: raw.beforeHtml ?? '',
279
+ afterHtml: raw.afterHtml ?? '',
280
+ beforePaginationHtml: raw.beforePaginationHtml ?? '',
281
+ afterPaginationHtml: raw.afterPaginationHtml ?? '',
282
+ betweenPaginationHtml: raw.betweenPaginationHtml ?? '',
283
+ };
284
+ }
285
+ /**
286
+ * Transforms friendly typed properties back to camelCased string properties for Pagination save.
287
+ * Derives: useAltFormatter from altLayoutName.
288
+ */
289
+ function transformPaginationWrite(props) {
290
+ const altLayoutName = String(props.altLayoutName ?? '');
291
+ const fetchMethod = props.fetchMethod ?? 'current';
292
+ return {
293
+ templateList: String(props.contentTypeId ?? '0'),
294
+ fetchMethod: FETCH_METHOD_TO_API[fetchMethod] ?? 'fetch-method-current',
295
+ section: String(props.section ?? 0),
296
+ level: String(props.level ?? 0),
297
+ numToRecurse: String(props.numToRecurse ?? 0),
298
+ numberOfPieces: String(props.contentItemsPerPage ?? 0),
299
+ maxNumberOfPieces: String(props.maxContentItems ?? 0),
300
+ numLinksToShow: String(props.maxLinksPerPage ?? 0),
301
+ useAltFormatter: altLayoutName ? 'yes' : 'no',
302
+ altFormatterType: altLayoutName,
303
+ showHiddenSections: props.searchHiddenSections ? 'yes' : 'no',
304
+ beforeHtml: String(props.beforeHtml ?? ''),
305
+ afterHtml: String(props.afterHtml ?? ''),
306
+ beforePaginationHtml: String(props.beforePaginationHtml ?? ''),
307
+ afterPaginationHtml: String(props.afterPaginationHtml ?? ''),
308
+ betweenPaginationHtml: String(props.betweenPaginationHtml ?? ''),
309
+ };
310
+ }
311
+ /**
312
+ * Transforms raw camelCased string properties into friendly typed properties for Previous/Next Fulltext.
313
+ * Hides: customFormatter (derived from altLayoutName).
314
+ * Collapses previous/next/previousAndNext into a single `type` field.
315
+ * Note: API keys start with "id_" so camelCased keys start with "id" (e.g. idPrevious, idNext).
316
+ */
317
+ function transformPreviousNextRead(raw) {
318
+ let type = 'previous';
319
+ if (raw.idPrevious === 'true')
320
+ type = 'previous';
321
+ else if (raw.idNext === 'true')
322
+ type = 'next';
323
+ else if (raw.idPreviousAndNext === 'true')
324
+ type = 'both';
325
+ return {
326
+ type,
327
+ altLayoutName: raw.idCustomFormatterTextarea ?? '',
328
+ skipNonFulltextContent: raw.idSkipNonFulltextContent === 'yes',
329
+ onlyLinkToContentWithNav: raw.idNextNavigationWithPreviousNextNavigation === 'yes',
330
+ sameContentTypeRestriction: raw.idSameTemplateRestriction === 'yes',
331
+ displayOnBoundary: raw.idDisplayOnBoundary === 'yes',
332
+ displayContentNameAsTitle: raw.idDisplayContentNameAsTitle === 'yes',
333
+ previousHtml: raw.idPreviousHtml ?? '',
334
+ betweenHtml: raw.idBetweenHtml ?? '',
335
+ nextHtml: raw.idNextHtml ?? '',
336
+ };
337
+ }
338
+ /**
339
+ * Transforms friendly typed properties back to camelCased string properties for Previous/Next Fulltext save.
340
+ * Derives: customFormatter from altLayoutName. Expands `type` to three boolean fields.
341
+ * Keys use the camelCased form of the API keys (e.g. idPrevious, idNext).
342
+ */
343
+ function transformPreviousNextWrite(props) {
344
+ const type = props.type ?? 'previous';
345
+ const altLayoutName = String(props.altLayoutName ?? '');
346
+ return {
347
+ idPrevious: type === 'previous' ? 'true' : 'false',
348
+ idNext: type === 'next' ? 'true' : 'false',
349
+ idPreviousAndNext: type === 'both' ? 'true' : 'false',
350
+ idCustomFormatter: altLayoutName ? 'yes' : 'no',
351
+ idCustomFormatterTextarea: altLayoutName,
352
+ idSkipNonFulltextContent: props.skipNonFulltextContent ? 'yes' : 'no',
353
+ idNextNavigationWithPreviousNextNavigation: props.onlyLinkToContentWithNav ? 'yes' : 'no',
354
+ idSameTemplateRestriction: props.sameContentTypeRestriction ? 'yes' : 'no',
355
+ idDisplayOnBoundary: props.displayOnBoundary ? 'yes' : 'no',
356
+ idDisplayContentNameAsTitle: props.displayContentNameAsTitle ? 'yes' : 'no',
357
+ idPreviousHtml: String(props.previousHtml ?? ''),
358
+ idBetweenHtml: String(props.betweenHtml ?? ''),
359
+ idNextHtml: String(props.nextHtml ?? ''),
360
+ };
361
+ }
362
+ /**
363
+ * Transforms raw camelCased string properties into friendly typed properties for Section Iterator.
364
+ */
365
+ function transformSectionIteratorRead(raw) {
366
+ return {
367
+ beforeHtml: raw.beforeHtml ?? '',
368
+ betweenHtml: raw.betweenHtml ?? '',
369
+ afterHtml: raw.afterHtml ?? '',
370
+ };
371
+ }
372
+ /**
373
+ * Transforms friendly typed properties back to camelCased string properties for Section Iterator save.
374
+ */
375
+ function transformSectionIteratorWrite(props) {
376
+ return {
377
+ beforeHtml: String(props.beforeHtml ?? ''),
378
+ betweenHtml: String(props.betweenHtml ?? ''),
379
+ afterHtml: String(props.afterHtml ?? ''),
380
+ };
381
+ }
382
+ /**
383
+ * Transforms raw camelCased string properties into friendly typed properties for Related Section Branch.
384
+ */
385
+ function transformRelatedSectionBranchRead(raw) {
386
+ return {
387
+ childSectionName: raw.nameOfChildSection ?? '',
388
+ linkText: raw.linkText ?? '',
389
+ };
390
+ }
391
+ /**
392
+ * Transforms friendly typed properties back to camelCased string properties for Related Section Branch save.
393
+ */
394
+ function transformRelatedSectionBranchWrite(props) {
395
+ return {
396
+ nameOfChildSection: String(props.childSectionName ?? ''),
397
+ linkText: String(props.linkText ?? ''),
398
+ };
399
+ }
400
+ /**
401
+ * Transforms raw camelCased string properties into friendly typed properties for Return to Index.
402
+ */
403
+ function transformReturnToIndexRead(raw) {
404
+ return {
405
+ linkText: raw.linkText ?? '',
406
+ appendSectionName: raw.appendSectionName === 'yes',
407
+ scrollToContent: raw.scrollToContent === 'yes',
408
+ linkTarget: raw.linkTarget ?? '',
409
+ };
410
+ }
411
+ /**
412
+ * Transforms friendly typed properties back to camelCased string properties for Return to Index save.
413
+ */
414
+ function transformReturnToIndexWrite(props) {
415
+ return {
416
+ linkText: String(props.linkText ?? ''),
417
+ appendSectionName: props.appendSectionName ? 'yes' : 'no',
418
+ scrollToContent: props.scrollToContent ? 'yes' : 'no',
419
+ linkTarget: String(props.linkTarget ?? ''),
420
+ };
421
+ }
422
+ /**
423
+ * Transforms raw camelCased string properties into friendly typed properties for Section Meta Info.
424
+ * Note: metaType will be the raw ID string here — resolved to name in get().
425
+ */
426
+ function transformSectionMetaInfoRead(raw) {
427
+ return {
428
+ metaType: raw.metaType ?? '',
429
+ dateFormat: raw.dateFormat ?? '',
430
+ beforeHtml: raw.beforeHtml ?? '',
431
+ afterHtml: raw.afterHtml ?? '',
432
+ };
433
+ }
434
+ /**
435
+ * Transforms friendly typed properties back to camelCased string properties for Section Meta Info save.
436
+ * Note: metaType here could be a name (needs resolution) or already an ID string.
437
+ */
438
+ function transformSectionMetaInfoWrite(props) {
439
+ return {
440
+ metaType: String(props.metaType ?? ''),
441
+ dateFormat: String(props.dateFormat ?? ''),
442
+ beforeHtml: String(props.beforeHtml ?? ''),
443
+ afterHtml: String(props.afterHtml ?? ''),
444
+ };
445
+ }
446
+ /**
447
+ * Transforms raw camelCased string properties into friendly typed properties for Top Stories.
448
+ */
449
+ function transformTopStoriesRead(raw) {
450
+ return {
451
+ section: parseInt(raw.section ?? '0', 10),
452
+ numToShow: parseInt(raw.numtoshow ?? '0', 10),
453
+ linkToFulltext: raw.linkToFulltext === 'yes',
454
+ title: raw.title ?? '',
455
+ beforeMenuHtml: raw.beforeMenuHtml ?? '',
456
+ afterMenuHtml: raw.afterMenuHtml ?? '',
457
+ beforeHtml: raw.beforeHtml ?? '',
458
+ afterHtml: raw.afterHtml ?? '',
459
+ };
460
+ }
461
+ /**
462
+ * Transforms friendly typed properties back to camelCased string properties for Top Stories save.
463
+ */
464
+ function transformTopStoriesWrite(props) {
465
+ return {
466
+ section: String(props.section ?? 0),
467
+ numtoshow: String(props.numToShow ?? 0),
468
+ linkToFulltext: props.linkToFulltext ? 'yes' : 'no',
469
+ title: String(props.title ?? ''),
470
+ beforeMenuHtml: String(props.beforeMenuHtml ?? ''),
471
+ afterMenuHtml: String(props.afterMenuHtml ?? ''),
472
+ beforeHtml: String(props.beforeHtml ?? ''),
473
+ afterHtml: String(props.afterHtml ?? ''),
474
+ };
475
+ }
476
+ /**
477
+ * Transforms raw camelCased string properties into friendly typed properties for Site Map.
478
+ */
479
+ function transformSiteMapRead(raw) {
480
+ const enableContentCount = raw.enableContentCount === 'yes';
481
+ const result = {
482
+ startSection: parseInt(raw.section ?? '0', 10),
483
+ levels: parseInt(raw.levels ?? '0', 10),
484
+ childSectionLinks: raw.showRelativeChildSections === 'yes',
485
+ enableContentCount,
486
+ };
487
+ if (enableContentCount) {
488
+ const templateType = raw.templateType ?? '0';
489
+ result.contentTypeIds = templateType && templateType !== '0'
490
+ ? templateType.split(',').map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n) && n > 0)
491
+ : [];
492
+ result.maxLevelsToCount = raw.maxLevelsToCount ? parseInt(raw.maxLevelsToCount, 10) : '';
493
+ result.countRecursively = raw.countRecursively === 'yes';
494
+ result.htmlBeforeContentCount = raw.htmlBeforeContentCount ?? '';
495
+ result.htmlAfterContentCount = raw.htmlAfterContentCount ?? '';
496
+ }
497
+ return result;
498
+ }
499
+ /**
500
+ * Transforms friendly typed properties back to camelCased string properties for Site Map save.
501
+ */
502
+ function transformSiteMapWrite(props) {
503
+ const enableContentCount = !!props.enableContentCount;
504
+ return {
505
+ section: String(props.startSection ?? 0),
506
+ levels: String(props.levels ?? 0),
507
+ showRelativeChildSections: props.childSectionLinks ? 'yes' : 'no',
508
+ enableContentCount: enableContentCount ? 'yes' : 'no',
509
+ templateType: enableContentCount ? (() => {
510
+ const ids = props.contentTypeIds;
511
+ return ids && ids.length > 0 ? ids.join(',') : '0';
512
+ })() : '0',
513
+ maxLevelsToCount: enableContentCount ? String(props.maxLevelsToCount ?? '') : '',
514
+ countRecursively: enableContentCount ? (props.countRecursively ? 'yes' : 'no') : 'no',
515
+ htmlBeforeContentCount: enableContentCount ? String(props.htmlBeforeContentCount ?? '') : '',
516
+ htmlAfterContentCount: enableContentCount ? String(props.htmlAfterContentCount ?? '') : '',
517
+ };
518
+ }
519
+ /**
520
+ * Transforms raw camelCased string properties into friendly typed properties for Section Details.
521
+ * Hides irrelevant fields based on detailMethod.
522
+ */
523
+ function transformSectionDetailsRead(raw) {
524
+ const methodApi = raw.detailsMethod ?? 'details-method-current';
525
+ const method = DETAIL_METHOD_FROM_API[methodApi] ?? 'current';
526
+ const result = {
527
+ detailMethod: method,
528
+ displayType: DISPLAY_TYPE_FROM_API[raw.displayType ?? ''] ?? 'id',
529
+ };
530
+ if (method === 'level') {
531
+ result.level = parseInt(raw.level ?? '0', 10);
532
+ }
533
+ if (method === 'section') {
534
+ result.section = parseInt(raw.section ?? '0', 10);
535
+ }
536
+ return result;
537
+ }
538
+ /**
539
+ * Transforms friendly typed properties back to camelCased string properties for Section Details save.
540
+ */
541
+ function transformSectionDetailsWrite(props) {
542
+ const method = props.detailMethod ?? 'current';
543
+ return {
544
+ detailsMethod: DETAIL_METHOD_TO_API[method] ?? 'details-method-current',
545
+ level: String(props.level ?? 0),
546
+ section: String(props.section ?? 0),
547
+ displayType: DISPLAY_TYPE_TO_API[props.displayType ?? 'id'] ?? 'display-type-id',
548
+ };
549
+ }
550
+ /**
551
+ * Transforms raw camelCased string properties into friendly typed properties for Related Content.
552
+ * Hides deprecated/internal fields.
553
+ */
554
+ function transformRelatedContentRead(raw) {
555
+ const fetchMethodApi = raw.fetchMethod ?? 'fetch-method-current';
556
+ const fetchMethod = RC_FETCH_METHOD_FROM_API[fetchMethodApi] ?? 'current';
557
+ const altLayoutName = raw.altFormatterType ?? '';
558
+ const result = {
559
+ fetchMethod,
560
+ title: raw.title ?? '',
561
+ beforeHtml: raw.beforeHtml ?? '',
562
+ afterHtml: raw.afterHtml ?? '',
563
+ };
564
+ if (altLayoutName)
565
+ result.altLayoutName = altLayoutName;
566
+ if (fetchMethod === 'section') {
567
+ result.section = parseInt(raw.section ?? '0', 10);
568
+ }
569
+ else if (fetchMethod === 'child') {
570
+ const childName = raw.fetchChild ?? '';
571
+ if (childName)
572
+ result.childSectionName = childName;
573
+ const templateIds = raw.templateIds ?? '';
574
+ result.contentTypeIds = templateIds ? templateIds.split(',').map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n)) : [];
575
+ result.display = parseInt(raw.numberOfPieces ?? '0', 10);
576
+ result.recurseChildSection = raw.recurseChildSection === 'yes';
577
+ }
578
+ return result;
579
+ }
580
+ /**
581
+ * Transforms friendly typed properties back to camelCased string properties for Related Content save.
582
+ */
583
+ function transformRelatedContentWrite(props) {
584
+ const fetchMethod = props.fetchMethod ?? 'current';
585
+ const altLayoutName = String(props.altLayoutName ?? '');
586
+ const contentTypeIds = props.contentTypeIds;
587
+ return {
588
+ fetchMethod: RC_FETCH_METHOD_TO_API[fetchMethod] ?? 'fetch-method-current',
589
+ relatedcontentType: RC_TYPE_MAP[fetchMethod] ?? 'rc',
590
+ section: fetchMethod === 'section' ? String(props.section ?? 0) : '0',
591
+ fetchChild: fetchMethod === 'child' ? String(props.childSectionName ?? '') : '',
592
+ templateIds: fetchMethod === 'child' && contentTypeIds ? contentTypeIds.join(',') : '',
593
+ numberOfPieces: fetchMethod === 'child' ? String(props.display ?? 0) : '0',
594
+ recurseChildSection: fetchMethod === 'child' ? (props.recurseChildSection ? 'yes' : 'no') : 'no',
595
+ useAltFormatter: altLayoutName ? 'yes' : 'no',
596
+ altFormatterType: altLayoutName,
597
+ title: String(props.title ?? ''),
598
+ beforeHtml: String(props.beforeHtml ?? ''),
599
+ afterHtml: String(props.afterHtml ?? ''),
600
+ searchUpwards: 'no',
601
+ moreLink: 'no',
602
+ moreLinkText: '',
603
+ levelsToRecurse: '',
604
+ showNameWhenHidden: 'no',
605
+ };
606
+ }
607
+ /**
608
+ * Transforms raw camelCased string properties into friendly typed properties for Link Menu.
609
+ * Conditionally shows fields based on menuType.
610
+ */
611
+ function transformLinkMenuRead(raw) {
612
+ const menuTypeApi = raw.menutype ?? 'branch';
613
+ const menuType = LINK_MENU_TYPE_FROM_API[menuTypeApi] ?? 'branch-at-level';
614
+ const numToRecurse = parseInt(raw.numtorecurse ?? '1', 10);
615
+ const result = {
616
+ menuType,
617
+ menuDisplayType: MENU_DISPLAY_FROM_API[raw.menuDisplayType ?? ''] ?? 'normal',
618
+ showNonCurrentChildren: raw.showNonCurrentChildren === 'yes',
619
+ useCurrentBranchClass: raw.classCurrentBranch === 'yes',
620
+ currentSectionLink: raw.makeSectionLink === 'yes',
621
+ addSectionName: raw.titlePrependSect === 'yes',
622
+ title: raw.title ?? '',
623
+ beforeMenuHtml: raw.beforeMenuHtml ?? '',
624
+ afterMenuHtml: raw.afterMenuHtml ?? '',
625
+ beforeLinkHtml: raw.beforeHtml ?? '',
626
+ afterLinkHtml: raw.afterHtml ?? '',
627
+ betweenLink: raw.betweenLink ?? '',
628
+ };
629
+ if (menuType === 'branch-at-level') {
630
+ result.level = parseInt(raw.level ?? '0', 10);
631
+ result.numToRecurse = numToRecurse;
632
+ if (numToRecurse > 1) {
633
+ result.subNavigationType = LINK_DISPLAY_FROM_API[raw.subNavigationType ?? ''] ?? 'ul';
634
+ }
635
+ }
636
+ if (menuType === 'siblings-and-children') {
637
+ result.subNavigationType = LINK_DISPLAY_FROM_API[raw.subNavigationType ?? ''] ?? 'ul';
638
+ }
639
+ if (menuType === 'children') {
640
+ result.displaySpecificBranch = raw.displaySpecificBranch === 'yes';
641
+ if (raw.displaySpecificBranch === 'yes') {
642
+ result.specificBranchId = parseInt(raw.specificBranchId ?? '0', 10);
643
+ }
644
+ result.showSiblingsIfNoChildren = raw.sibIfNoChildren === 'yes';
645
+ result.showAncestorsIfNoChildren = raw.ancIfNoChildren === 'yes';
646
+ }
647
+ return result;
648
+ }
649
+ /**
650
+ * Transforms friendly typed properties back to camelCased string properties for Link Menu save.
651
+ */
652
+ function transformLinkMenuWrite(props) {
653
+ const menuType = props.menuType ?? 'branch-at-level';
654
+ const numToRecurse = menuType === 'branch-at-level' ? Number(props.numToRecurse ?? 1) : 1;
655
+ const showLinkDisplay = (menuType === 'branch-at-level' && numToRecurse > 1) || menuType === 'siblings-and-children';
656
+ return {
657
+ menutype: LINK_MENU_TYPE_TO_API[menuType] ?? 'branch',
658
+ menuDisplayType: MENU_DISPLAY_TO_API[props.menuDisplayType ?? 'normal'] ?? 'menu-display-normal',
659
+ level: menuType === 'branch-at-level' ? String(props.level ?? 0) : '0',
660
+ numtorecurse: menuType === 'branch-at-level' ? String(numToRecurse) : '1',
661
+ subNavigationType: showLinkDisplay ? (LINK_DISPLAY_TO_API[props.subNavigationType ?? 'ul'] ?? 'link-display-ul') : 'link-display-ul',
662
+ showNonCurrentChildren: props.showNonCurrentChildren ? 'yes' : 'no',
663
+ classCurrentBranch: props.useCurrentBranchClass ? 'yes' : 'no',
664
+ makeSectionLink: props.currentSectionLink ? 'yes' : 'no',
665
+ titlePrependSect: props.addSectionName ? 'yes' : 'no',
666
+ displaySpecificBranch: menuType === 'children' ? (props.displaySpecificBranch ? 'yes' : 'no') : 'no',
667
+ specificBranchId: menuType === 'children' && props.displaySpecificBranch ? String(props.specificBranchId ?? 0) : '0',
668
+ sibIfNoChildren: menuType === 'children' ? (props.showSiblingsIfNoChildren ? 'yes' : 'no') : 'no',
669
+ ancIfNoChildren: menuType === 'children' ? (props.showAncestorsIfNoChildren ? 'yes' : 'no') : 'no',
670
+ title: String(props.title ?? ''),
671
+ beforeMenuHtml: String(props.beforeMenuHtml ?? ''),
672
+ afterMenuHtml: String(props.afterMenuHtml ?? ''),
673
+ beforeHtml: String(props.beforeLinkHtml ?? ''),
674
+ afterHtml: String(props.afterLinkHtml ?? ''),
675
+ betweenLink: String(props.betweenLink ?? ''),
676
+ };
677
+ }
678
+ /**
679
+ * Transforms raw camelCased string properties into friendly typed properties for Publish to One File.
680
+ * Conditionally shows fields based on startSection mode, showSectionName, and pagination.
681
+ */
682
+ function transformPublishOneFileRead(raw) {
683
+ let startSection = 'current';
684
+ if (raw.contentTypeSection === 'yes') {
685
+ startSection = 'element';
686
+ }
687
+ else if (raw.startSection && raw.startSection !== '0') {
688
+ startSection = 'specific';
689
+ }
690
+ const showSectionName = raw.showSectionName === 'yes';
691
+ const pagination = raw.paginationAcrossPages === 'yes';
692
+ const altLayoutName = raw.altFormatterType ?? '';
693
+ const result = {
694
+ contentTypeId: (() => { const v = parseInt(raw.templateList ?? '0', 10); return v > 0 ? v : null; })(),
695
+ startSection,
696
+ showHiddenSections: raw.showHiddenSections === 'yes',
697
+ levelsToRecurse: parseInt(raw.levelsToRecurse ?? '1', 10),
698
+ beforeHtml: raw.beforeHtml ?? '',
699
+ afterHtml: raw.afterHtml ?? '',
700
+ showSectionName,
701
+ enableCaching: raw.enableCaching === 'yes',
702
+ pagination,
703
+ };
704
+ if (altLayoutName)
705
+ result.altLayoutName = altLayoutName;
706
+ if (startSection === 'specific') {
707
+ result.section = parseInt(raw.startSection ?? '0', 10);
708
+ }
709
+ else if (startSection === 'element') {
710
+ result.startSectionElement = raw.startSectionElement ?? '';
711
+ }
712
+ if (showSectionName) {
713
+ result.showNameForHidden = raw.showNameForHidden === 'yes';
714
+ result.beforeSectionName = raw.beforeSectionName ?? '';
715
+ result.afterSectionName = raw.afterSectionName ?? '';
716
+ }
717
+ const surroundingStyle = parseInt(raw.surroundingStyle ?? '0', 10);
718
+ if (surroundingStyle > 0)
719
+ result.surroundingPageLayout = surroundingStyle;
720
+ if (pagination) {
721
+ result.contentPerPage = parseInt(raw.contentPerPage ?? '0', 10);
722
+ result.beforePaginationHtml = raw.beforePaginationHtml ?? '';
723
+ result.betweenPaginationHtml = raw.betweenPaginationHtml ?? '';
724
+ result.afterPaginationHtml = raw.afterPaginationHtml ?? '';
725
+ }
726
+ return result;
727
+ }
728
+ /**
729
+ * Transforms friendly typed properties back to camelCased string properties for Publish to One File save.
730
+ */
731
+ function transformPublishOneFileWrite(props) {
732
+ const startSection = props.startSection ?? 'current';
733
+ const showSectionName = !!props.showSectionName;
734
+ const pagination = !!props.pagination;
735
+ const altLayoutName = String(props.altLayoutName ?? '');
736
+ return {
737
+ templateList: String(props.contentTypeId && props.contentTypeId > 0 ? props.contentTypeId : 0),
738
+ contentTypeSection: startSection === 'element' ? 'yes' : 'no',
739
+ startSection: startSection === 'specific' ? String(props.section ?? 0) : '0',
740
+ startSectionElement: startSection === 'element' ? String(props.startSectionElement ?? '') : '',
741
+ showHiddenSections: props.showHiddenSections ? 'yes' : 'no',
742
+ levelsToRecurse: String(props.levelsToRecurse ?? 1),
743
+ beforeHtml: String(props.beforeHtml ?? ''),
744
+ afterHtml: String(props.afterHtml ?? ''),
745
+ showSectionName: showSectionName ? 'yes' : 'no',
746
+ showNameForHidden: showSectionName ? (props.showNameForHidden ? 'yes' : 'no') : 'no',
747
+ beforeSectionName: showSectionName ? String(props.beforeSectionName ?? '') : '',
748
+ afterSectionName: showSectionName ? String(props.afterSectionName ?? '') : '',
749
+ surroundingStyle: props.surroundingPageLayout ? String(props.surroundingPageLayout) : '',
750
+ useAltFormatter: altLayoutName ? 'yes' : 'no',
751
+ altFormatterType: altLayoutName,
752
+ enableCaching: props.enableCaching !== false ? 'yes' : 'no',
753
+ paginationAcrossPages: pagination ? 'yes' : 'no',
754
+ contentPerPage: pagination ? String(props.contentPerPage ?? 0) : '0',
755
+ beforePaginationHtml: pagination ? String(props.beforePaginationHtml ?? '') : '',
756
+ betweenPaginationHtml: pagination ? String(props.betweenPaginationHtml ?? '') : '',
757
+ afterPaginationHtml: pagination ? String(props.afterPaginationHtml ?? '') : '',
758
+ };
759
+ }
760
+ /**
761
+ * Transforms raw camelCased string properties into friendly typed properties for Top Content.
762
+ */
763
+ function transformTopContentRead(raw) {
764
+ const fetchMethodApi = raw.fetchMethod ?? 'fetch-method-current';
765
+ const fetchMethod = TC_FETCH_METHOD_FROM_API[fetchMethodApi] ?? 'current';
766
+ const altLayoutName = raw.altFormatterType ?? '';
767
+ const templateIds = raw.templateIds ?? '';
768
+ const result = {
769
+ fetchMethod,
770
+ contentTypeIds: templateIds ? templateIds.split(',').map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n) && n > 0) : [],
771
+ channelId: parseInt(raw.channelId ?? '0', 10),
772
+ upcomingContent: raw.upcomingContent === 'yes',
773
+ dateElement: raw.pubElement ?? '',
774
+ ignoreDateOrdering: raw.dateOrderedContent === 'yes',
775
+ numToDisplay: parseInt(raw.numberOfPieces ?? '0', 10),
776
+ startAt: parseInt(raw.startingContent ?? '0', 10),
777
+ title: raw.title ?? '',
778
+ beforeHtml: raw.beforeHtml ?? '',
779
+ afterHtml: raw.afterHtml ?? '',
780
+ };
781
+ if (fetchMethod === 'branch' || fetchMethod === 'section') {
782
+ result.section = parseInt(raw.section ?? '0', 10);
783
+ }
784
+ if (altLayoutName)
785
+ result.altLayoutName = altLayoutName;
786
+ return result;
787
+ }
788
+ /**
789
+ * Transforms friendly typed properties back to camelCased string properties for Top Content save.
790
+ */
791
+ function transformTopContentWrite(props) {
792
+ const fetchMethod = props.fetchMethod ?? 'current';
793
+ const altLayoutName = String(props.altLayoutName ?? '');
794
+ const contentTypeIds = props.contentTypeIds;
795
+ return {
796
+ fetchMethod: TC_FETCH_METHOD_TO_API[fetchMethod] ?? 'fetch-method-current',
797
+ section: (fetchMethod === 'branch' || fetchMethod === 'section') ? String(props.section ?? 0) : '0',
798
+ templateIds: contentTypeIds && contentTypeIds.length > 0 ? contentTypeIds.join(',') : '',
799
+ channelId: String(props.channelId ?? 0),
800
+ upcomingContent: props.upcomingContent ? 'yes' : 'no',
801
+ pubElement: String(props.dateElement ?? ''),
802
+ dateOrderedContent: props.ignoreDateOrdering ? 'yes' : 'no',
803
+ numberOfPieces: String(props.numToDisplay ?? 0),
804
+ startingContent: String(props.startAt ?? 0),
805
+ useAltFormatter: altLayoutName ? 'yes' : 'no',
806
+ altFormatterType: altLayoutName,
807
+ title: String(props.title ?? ''),
808
+ beforeHtml: String(props.beforeHtml ?? ''),
809
+ afterHtml: String(props.afterHtml ?? ''),
810
+ };
811
+ }
812
+ /**
813
+ * Transforms raw camelCased string properties into friendly typed properties for Keyword Search Content.
814
+ */
815
+ function transformKeywordSearchRead(raw) {
816
+ const keywordFetchMethod = KW_FETCH_FROM_API[raw.fetchMethod ?? ''] ?? 'current';
817
+ const contentFetchMethod = KW_CONTENT_FETCH_FROM_API[raw.searchFetchMethod ?? ''] ?? 'section';
818
+ const altLayoutName = raw.altFormatterType ?? '';
819
+ const pagination = raw.paginationEnabled === 'yes';
820
+ const sortByDateElement = raw.orderByDateElement === 'yes';
821
+ const result = {
822
+ keywordFetchMethod,
823
+ narrowToSingleContentItem: raw.narrowOnFulltext === 'yes',
824
+ keywordContentTypeId: (() => { const v = parseInt(raw.templateListGet ?? '-1', 10); return v > 0 ? v : null; })(),
825
+ keywordElements: raw.templateElementGet ? raw.templateElementGet.split(',').map((s) => s.trim()).filter(Boolean) : [],
826
+ contentFetchMethod,
827
+ searchContentTypeId: (() => { const v = parseInt(raw.templateListSearch ?? '-1', 10); return v > 0 ? v : null; })(),
828
+ searchElements: raw.templateElementSearch ? raw.templateElementSearch.split(',').map((s) => s.trim()).filter(Boolean) : [],
829
+ numToDisplay: parseInt(raw.numberOfPieces ?? '0', 10) || 0,
830
+ sortType: KW_SORT_FROM_API[raw.orderBy ?? ''] ?? 'name',
831
+ sortByDateElement,
832
+ showUpcomingContent: raw.showUpcomingContent === 'yes',
833
+ showHiddenSections: raw.showHiddenSections === 'yes',
834
+ matchCompositeKeywords: raw.matchCompositeKeywords === 'yes',
835
+ matchSubItems: raw.matchSubItems === 'yes',
836
+ crossLanguageSearch: raw.crossLanguageSearchingEnabled === 'yes',
837
+ beforeHtml: raw.beforeHtml ?? '',
838
+ afterHtml: raw.afterHtml ?? '',
839
+ pagination,
840
+ };
841
+ // Keyword fetch method specific
842
+ if (keywordFetchMethod === 'section') {
843
+ result.keywordSection = parseInt(raw.section ?? '0', 10);
844
+ }
845
+ // Content fetch method specific
846
+ if (contentFetchMethod === 'section' || contentFetchMethod === 'branch') {
847
+ const searchSection = parseInt(raw.searchSection ?? '0', 10);
848
+ const searchSectionElement = raw.templateElementForSearchSection ?? '';
849
+ if (searchSection > 0) {
850
+ result.searchSection = searchSection;
851
+ }
852
+ else if (searchSectionElement) {
853
+ result.searchSectionElement = searchSectionElement;
854
+ }
855
+ }
856
+ if (contentFetchMethod === 'branch-at-level') {
857
+ result.startLevel = parseInt(raw.level ?? '0', 10);
858
+ result.endLevel = parseInt(raw.numToRecurse ?? '0', 10);
859
+ }
860
+ // Conditional fields
861
+ if (sortByDateElement) {
862
+ result.dateElementName = raw.orderByDateElementName ?? '';
863
+ }
864
+ if (raw.crossLanguageSearchingEnabled === 'yes' && raw.crossLanguageSearchingLanguages) {
865
+ result.crossLanguageLanguages = raw.crossLanguageSearchingLanguages.split(',').map((s) => s.trim()).filter(Boolean);
866
+ }
867
+ if (altLayoutName)
868
+ result.altLayoutName = altLayoutName;
869
+ if (pagination) {
870
+ result.contentPerPage = parseInt(raw.contentPerPage ?? '0', 10);
871
+ result.beforePaginationHtml = raw.beforePaginationHtml ?? '';
872
+ result.betweenPaginationHtml = raw.betweenPaginationHtml ?? '';
873
+ result.afterPaginationHtml = raw.afterPaginationHtml ?? '';
874
+ }
875
+ return result;
876
+ }
877
+ /**
878
+ * Transforms friendly typed properties back to camelCased string properties for Keyword Search Content save.
879
+ */
880
+ function transformKeywordSearchWrite(props) {
881
+ const keywordFetchMethod = props.keywordFetchMethod ?? 'current';
882
+ const contentFetchMethod = props.contentFetchMethod ?? 'section';
883
+ const altLayoutName = String(props.altLayoutName ?? '');
884
+ const pagination = !!props.pagination;
885
+ const crossLangs = props.crossLanguageLanguages;
886
+ return {
887
+ fetchMethod: KW_FETCH_TO_API[keywordFetchMethod] ?? 'fetch-method-current',
888
+ narrowOnFulltext: props.narrowToSingleContentItem ? 'yes' : 'no',
889
+ templateListGet: String(props.keywordContentTypeId && props.keywordContentTypeId > 0 ? props.keywordContentTypeId : -1),
890
+ templateElementGet: Array.isArray(props.keywordElements) ? props.keywordElements.join(',') : '',
891
+ searchFetchMethod: KW_CONTENT_FETCH_TO_API[contentFetchMethod] ?? 'fetch-method-section',
892
+ section: keywordFetchMethod === 'section' ? String(props.keywordSection ?? 0) : '0',
893
+ searchSection: (contentFetchMethod === 'section' || contentFetchMethod === 'branch') && !props.searchSectionElement ? String(props.searchSection ?? 0) : '0',
894
+ templateElementForSearchSection: String(props.searchSectionElement ?? ''),
895
+ level: contentFetchMethod === 'branch-at-level' ? String(props.startLevel ?? 0) : '0',
896
+ numToRecurse: contentFetchMethod === 'branch-at-level' ? String(props.endLevel ?? 0) : '0',
897
+ templateListSearch: String(props.searchContentTypeId && props.searchContentTypeId > 0 ? props.searchContentTypeId : -1),
898
+ templateElementSearch: Array.isArray(props.searchElements) ? props.searchElements.join(',') : '',
899
+ numberOfPieces: String(props.numToDisplay ?? ''),
900
+ orderBy: KW_SORT_TO_API[props.sortType ?? 'name'] ?? 'order-name',
901
+ orderByDateElement: props.sortByDateElement ? 'yes' : 'no',
902
+ orderByDateElementName: props.sortByDateElement ? String(props.dateElementName ?? '') : '',
903
+ showUpcomingContent: props.showUpcomingContent ? 'yes' : 'no',
904
+ showHiddenSections: props.showHiddenSections ? 'yes' : 'no',
905
+ matchCompositeKeywords: props.matchCompositeKeywords ? 'yes' : 'no',
906
+ matchSubItems: props.matchSubItems ? 'yes' : 'no',
907
+ crossLanguageSearchingEnabled: props.crossLanguageSearch ? 'yes' : 'no',
908
+ crossLanguageSearchingLanguages: crossLangs && crossLangs.length > 0 ? crossLangs.join(',') : '',
909
+ useAltFormatter: altLayoutName ? 'yes' : 'no',
910
+ altFormatterType: altLayoutName,
911
+ beforeHtml: String(props.beforeHtml ?? ''),
912
+ afterHtml: String(props.afterHtml ?? ''),
913
+ paginationEnabled: pagination ? 'yes' : 'no',
914
+ contentPerPage: pagination ? String(props.contentPerPage ?? 0) : '0',
915
+ beforePaginationHtml: pagination ? String(props.beforePaginationHtml ?? '') : '',
916
+ betweenPaginationHtml: pagination ? String(props.betweenPaginationHtml ?? '') : '',
917
+ afterPaginationHtml: pagination ? String(props.afterPaginationHtml ?? '') : '',
918
+ };
919
+ }
920
+ /** Applies type-aware read transformation if available, otherwise returns raw camelCased strings. */
921
+ function transformPropertiesRead(type, raw) {
922
+ switch (type) {
923
+ case 'a-to-z': return transformA2ZRead(raw);
924
+ case 'breadcrumbs': return transformBreadcrumbsRead(raw);
925
+ case 'css-selector': return transformCssSelectorRead(raw);
926
+ case 'generate-file': return transformGenerateFileRead(raw);
927
+ case 'language-switcher': return transformLanguageSwitcherRead(raw);
928
+ case 'pagination': return transformPaginationRead(raw);
929
+ case 'previous-next-fulltext': return transformPreviousNextRead(raw);
930
+ case 'section-iterator': return transformSectionIteratorRead(raw);
931
+ case 'related-section-branch': return transformRelatedSectionBranchRead(raw);
932
+ case 'return-to-index': return transformReturnToIndexRead(raw);
933
+ case 'section-meta-info': return transformSectionMetaInfoRead(raw);
934
+ case 'top-stories': return transformTopStoriesRead(raw);
935
+ case 'site-map': return transformSiteMapRead(raw);
936
+ case 'section-details': return transformSectionDetailsRead(raw);
937
+ case 'related-content': return transformRelatedContentRead(raw);
938
+ case 'link-menu': return transformLinkMenuRead(raw);
939
+ case 'publish-to-one-file': return transformPublishOneFileRead(raw);
940
+ case 'top-content': return transformTopContentRead(raw);
941
+ case 'keyword-search': return transformKeywordSearchRead(raw);
942
+ default: return { ...raw };
943
+ }
944
+ }
945
+ /** Applies type-aware write transformation if available, otherwise returns the values as strings. */
946
+ function transformPropertiesWrite(type, props) {
947
+ switch (type) {
948
+ case 'a-to-z': return transformA2ZWrite(props);
949
+ case 'breadcrumbs': return transformBreadcrumbsWrite(props);
950
+ case 'css-selector': return transformCssSelectorWrite(props);
951
+ case 'generate-file': return transformGenerateFileWrite(props);
952
+ case 'language-switcher': return transformLanguageSwitcherWrite(props);
953
+ case 'pagination': return transformPaginationWrite(props);
954
+ case 'previous-next-fulltext': return transformPreviousNextWrite(props);
955
+ case 'section-iterator': return transformSectionIteratorWrite(props);
956
+ case 'related-section-branch': return transformRelatedSectionBranchWrite(props);
957
+ case 'return-to-index': return transformReturnToIndexWrite(props);
958
+ case 'section-meta-info': return transformSectionMetaInfoWrite(props);
959
+ case 'top-stories': return transformTopStoriesWrite(props);
960
+ case 'site-map': return transformSiteMapWrite(props);
961
+ case 'section-details': return transformSectionDetailsWrite(props);
962
+ case 'related-content': return transformRelatedContentWrite(props);
963
+ case 'link-menu': return transformLinkMenuWrite(props);
964
+ case 'publish-to-one-file': return transformPublishOneFileWrite(props);
965
+ case 'top-content': return transformTopContentWrite(props);
966
+ case 'keyword-search': return transformKeywordSearchWrite(props);
967
+ default: {
968
+ const result = {};
969
+ for (const [k, v] of Object.entries(props))
970
+ result[k] = String(v ?? '');
971
+ return result;
972
+ }
973
+ }
974
+ }
975
+ /**
976
+ * A mutable navigation object. Modify properties and call save() to persist.
977
+ */
978
+ export class NavigationObject {
979
+ constructor(raw, httpClient) {
980
+ this.id = raw.id;
981
+ this.name = raw.name;
982
+ this.description = raw.description ?? '';
983
+ this.type = API_TO_SDK[raw.navigationType] ?? raw.navigationType;
984
+ this.typeName = NAVIGATION_TYPE_NAMES[this.type] || raw.navigationTypeName || raw.navigationType;
985
+ this.enabled = raw.isEnabled;
986
+ this.cachingEnabled = raw.isCachingEnabled;
987
+ this.previewEnabled = raw.isPreviewModeEnabled;
988
+ // Convert properties to camelCase keys with string values
989
+ const originalKeys = [];
990
+ const rawCamelProps = {};
991
+ for (const [key, prop] of Object.entries(raw.properties ?? {})) {
992
+ originalKeys.push(key);
993
+ rawCamelProps[toCamelCase(key)] = prop.value ?? '';
994
+ }
995
+ // Apply type-aware read transformation
996
+ this.properties = transformPropertiesRead(this.type, rawCamelProps);
997
+ Object.defineProperty(this, '_httpClient', { value: httpClient, enumerable: false });
998
+ Object.defineProperty(this, '_rawData', { value: raw, enumerable: false, writable: true });
999
+ Object.defineProperty(this, '_originalPropertyKeys', { value: originalKeys, enumerable: false });
1000
+ }
1001
+ /** Persists current property values to the server via PUT. */
1002
+ async save() {
1003
+ // Apply type-aware write transformation (coerce back to strings, derive hidden fields)
1004
+ const camelStringProps = transformPropertiesWrite(this.type, this.properties);
1005
+ // Rebuild properties in API format
1006
+ const apiProperties = {};
1007
+ for (const [camelKey, value] of Object.entries(camelStringProps)) {
1008
+ const originalKey = fromCamelCase(camelKey, this._originalPropertyKeys);
1009
+ apiProperties[originalKey] = {
1010
+ value,
1011
+ attribute: originalKey,
1012
+ navigationPropertyID: this.id,
1013
+ };
1014
+ }
1015
+ const body = {
1016
+ ...this._rawData,
1017
+ name: this.name,
1018
+ description: this.description,
1019
+ isEnabled: this.enabled,
1020
+ isCachingEnabled: this.cachingEnabled,
1021
+ isPreviewModeEnabled: this.previewEnabled,
1022
+ properties: apiProperties,
1023
+ };
1024
+ await this._httpClient.request({
1025
+ method: 'PUT',
1026
+ path: `/navigation/${this.id}`,
1027
+ body,
1028
+ });
1029
+ }
1030
+ }
1031
+ /** Maps SDK fetch method names to API values */
1032
+ const FETCH_METHOD_TO_API = {
1033
+ 'current': 'fetch-method-current',
1034
+ 'current-branch': 'fetch-method-current-branch',
1035
+ 'branch': 'fetch-method-branch',
1036
+ 'branch-at-level': 'fetch-method-branch-at-level',
1037
+ 'section': 'fetch-method-section',
1038
+ };
1039
+ /** Maps API fetch method values to SDK names */
1040
+ const FETCH_METHOD_FROM_API = Object.fromEntries(Object.entries(FETCH_METHOD_TO_API).map(([sdk, api]) => [api, sdk]));
1041
+ /** Maps SDK detail method to API value */
1042
+ const DETAIL_METHOD_TO_API = {
1043
+ 'current': 'details-method-current',
1044
+ 'level': 'details-method-level',
1045
+ 'section': 'details-method-section',
1046
+ };
1047
+ /** Maps API detail method to SDK value */
1048
+ const DETAIL_METHOD_FROM_API = Object.fromEntries(Object.entries(DETAIL_METHOD_TO_API).map(([sdk, api]) => [api, sdk]));
1049
+ /** Maps SDK display type to API value */
1050
+ const DISPLAY_TYPE_TO_API = {
1051
+ 'id': 'display-type-id',
1052
+ 'name': 'display-type-name',
1053
+ 'path': 'display-type-path',
1054
+ 'link': 'display-type-link',
1055
+ };
1056
+ /** Maps API display type to SDK value */
1057
+ const DISPLAY_TYPE_FROM_API = Object.fromEntries(Object.entries(DISPLAY_TYPE_TO_API).map(([sdk, api]) => [api, sdk]));
1058
+ /** Maps SDK fetch method to API value for Related Content */
1059
+ const RC_FETCH_METHOD_TO_API = {
1060
+ 'current': 'fetch-method-current',
1061
+ 'section': 'fetch-method-section',
1062
+ 'child': 'fetch-method-child',
1063
+ };
1064
+ /** Maps API fetch method to SDK value for Related Content */
1065
+ const RC_FETCH_METHOD_FROM_API = Object.fromEntries(Object.entries(RC_FETCH_METHOD_TO_API).map(([sdk, api]) => [api, sdk]));
1066
+ /** Maps fetch method to the relatedcontent-type value */
1067
+ const RC_TYPE_MAP = {
1068
+ 'current': 'rc',
1069
+ 'section': 'rcb',
1070
+ 'child': 'rcbl',
1071
+ };
1072
+ /** Maps SDK menu type to API value */
1073
+ const LINK_MENU_TYPE_TO_API = {
1074
+ 'branch-at-level': 'branch',
1075
+ 'children': 'children',
1076
+ 'siblings': 'siblings',
1077
+ 'siblings-and-children': 'siblings-and-children',
1078
+ };
1079
+ /** Maps API menu type to SDK value */
1080
+ const LINK_MENU_TYPE_FROM_API = Object.fromEntries(Object.entries(LINK_MENU_TYPE_TO_API).map(([sdk, api]) => [api, sdk]));
1081
+ const MENU_DISPLAY_TO_API = {
1082
+ 'normal': 'menu-display-normal',
1083
+ 'dropdown': 'menu-display-dropdown',
1084
+ };
1085
+ const MENU_DISPLAY_FROM_API = Object.fromEntries(Object.entries(MENU_DISPLAY_TO_API).map(([sdk, api]) => [api, sdk]));
1086
+ const LINK_DISPLAY_TO_API = {
1087
+ 'ul': 'link-display-ul',
1088
+ 'table': 'link-display-table',
1089
+ 'div': 'link-display-div',
1090
+ };
1091
+ const LINK_DISPLAY_FROM_API = Object.fromEntries(Object.entries(LINK_DISPLAY_TO_API).map(([sdk, api]) => [api, sdk]));
1092
+ /** Maps SDK fetch method to API value for Top Content */
1093
+ const TC_FETCH_METHOD_TO_API = {
1094
+ 'current': 'fetch-method-current',
1095
+ 'current-branch': 'fetch-method-current-branch',
1096
+ 'branch': 'fetch-method-branch',
1097
+ 'section': 'fetch-method-section',
1098
+ };
1099
+ /** Maps API fetch method to SDK value for Top Content */
1100
+ const TC_FETCH_METHOD_FROM_API = Object.fromEntries(Object.entries(TC_FETCH_METHOD_TO_API).map(([sdk, api]) => [api, sdk]));
1101
+ /** Maps SDK keyword fetch method to API */
1102
+ const KW_FETCH_TO_API = {
1103
+ 'current': 'fetch-method-current',
1104
+ 'parent': 'fetch-method-parent',
1105
+ 'section': 'fetch-method-section',
1106
+ };
1107
+ const KW_FETCH_FROM_API = Object.fromEntries(Object.entries(KW_FETCH_TO_API).map(([sdk, api]) => [api, sdk]));
1108
+ /** Maps SDK content fetch method to API */
1109
+ const KW_CONTENT_FETCH_TO_API = {
1110
+ 'section': 'fetch-method-section',
1111
+ 'branch': 'fetch-method-branch',
1112
+ 'branch-at-level': 'fetch-method-branch-at-level',
1113
+ };
1114
+ const KW_CONTENT_FETCH_FROM_API = Object.fromEntries(Object.entries(KW_CONTENT_FETCH_TO_API).map(([sdk, api]) => [api, sdk]));
1115
+ /** Maps SDK sort type to API */
1116
+ const KW_SORT_TO_API = {
1117
+ 'name': 'order-name',
1118
+ 'name-desc': 'order-name-desc',
1119
+ 'last-modified': 'order-last-modified',
1120
+ };
1121
+ const KW_SORT_FROM_API = Object.fromEntries(Object.entries(KW_SORT_TO_API).map(([sdk, api]) => [api, sdk]));
1122
+ /** Collects all microsite IDs from a channels response (recursively) */
1123
+ function collectMicrositeIds(channels) {
1124
+ const ids = new Set();
1125
+ function walk(items) {
1126
+ for (const item of items) {
1127
+ if (item.microSites) {
1128
+ for (const ms of item.microSites) {
1129
+ ids.add(ms.id);
1130
+ if (ms.microSites)
1131
+ walk(ms.microSites);
1132
+ }
1133
+ }
1134
+ }
1135
+ }
1136
+ walk(channels);
1137
+ return ids;
1138
+ }
1139
+ /**
1140
+ * Resource for navigation object operations.
1141
+ * Accessible via `t4.navigation`.
1142
+ */
1143
+ export class NavigationResource {
1144
+ constructor(httpClient) {
1145
+ this.httpClient = httpClient;
1146
+ }
1147
+ /**
1148
+ * Lists all navigation objects.
1149
+ * Optionally filter by type.
1150
+ */
1151
+ async list(options) {
1152
+ const raw = await this.httpClient.request({
1153
+ method: 'GET',
1154
+ path: '/navigation',
1155
+ });
1156
+ let items = raw.map((item) => {
1157
+ const sdkType = API_TO_SDK[item.navigationType] ?? item.navigationType;
1158
+ return {
1159
+ id: item.id,
1160
+ name: item.name,
1161
+ description: item.description ?? '',
1162
+ type: sdkType,
1163
+ typeName: item.navigationTypeName || NAVIGATION_TYPE_NAMES[sdkType] || item.navigationType,
1164
+ enabled: item.navigationEnabled,
1165
+ };
1166
+ });
1167
+ if (options?.type) {
1168
+ items = items.filter((item) => item.type === options.type);
1169
+ }
1170
+ return items;
1171
+ }
1172
+ /** Gets a single navigation object by ID. Returns a mutable NavigationObject. */
1173
+ async get(id) {
1174
+ const raw = await this.httpClient.request({
1175
+ method: 'GET',
1176
+ path: `/navigation/${id}`,
1177
+ });
1178
+ const nav = new NavigationObject(raw, this.httpClient);
1179
+ return await this.resolveNavigationProperties(nav);
1180
+ }
1181
+ /** Post-construction async resolution — resolves IDs to names and raw element names to aliases. */
1182
+ async resolveNavigationProperties(nav) {
1183
+ // Section Meta Info: resolve metaType ID to name
1184
+ if (nav.type === 'section-meta-info' && nav.properties.metaType) {
1185
+ const metaId = parseInt(String(nav.properties.metaType), 10);
1186
+ if (metaId) {
1187
+ try {
1188
+ const metaLevels = await this.getMetaLevels();
1189
+ const match = metaLevels.find((m) => m.id === metaId);
1190
+ if (match)
1191
+ nav.properties.metaType = match.name;
1192
+ }
1193
+ catch { /* leave as ID */ }
1194
+ }
1195
+ }
1196
+ // A to Z: resolve raw element name back to alias for sectionMetaContentTypeElement
1197
+ if (nav.type === 'a-to-z' && nav.properties.sectionMetaContentTypeElement) {
1198
+ const rawName = String(nav.properties.sectionMetaContentTypeElement);
1199
+ if (rawName) {
1200
+ nav.properties.sectionMetaContentTypeElement = await this.resolveMetaDataElementName(rawName);
1201
+ }
1202
+ }
1203
+ // Publish to One File: resolve raw element name back to alias for startSectionElement
1204
+ if (nav.type === 'publish-to-one-file' && nav.properties.startSectionElement) {
1205
+ const contentTypeId = nav.properties.contentTypeId;
1206
+ if (contentTypeId && contentTypeId > 0) {
1207
+ const rawName = String(nav.properties.startSectionElement);
1208
+ nav.properties.startSectionElement = await this.resolveElementNameToAlias(contentTypeId, rawName);
1209
+ }
1210
+ }
1211
+ // Keyword Search: resolve raw element names back to aliases
1212
+ if (nav.type === 'keyword-search') {
1213
+ const kwCtId = nav.properties.keywordContentTypeId;
1214
+ const searchCtId = nav.properties.searchContentTypeId;
1215
+ // Resolve keywordElements (names → aliases)
1216
+ if (kwCtId && kwCtId > 0 && Array.isArray(nav.properties.keywordElements) && nav.properties.keywordElements.length > 0) {
1217
+ nav.properties.keywordElements = await this.resolveElementNamesToAliases(kwCtId, nav.properties.keywordElements);
1218
+ }
1219
+ // Resolve searchElements (names → aliases)
1220
+ if (searchCtId && searchCtId > 0 && Array.isArray(nav.properties.searchElements) && nav.properties.searchElements.length > 0) {
1221
+ nav.properties.searchElements = await this.resolveElementNamesToAliases(searchCtId, nav.properties.searchElements);
1222
+ }
1223
+ // Resolve searchSectionElement (name → alias)
1224
+ if (kwCtId && kwCtId > 0 && nav.properties.searchSectionElement) {
1225
+ nav.properties.searchSectionElement = await this.resolveElementNameToAlias(kwCtId, nav.properties.searchSectionElement);
1226
+ }
1227
+ }
1228
+ return nav;
1229
+ }
1230
+ /** Creates a new navigation object. */
1231
+ async create(data) {
1232
+ if (!data.name?.trim())
1233
+ throw new Error('Navigation object name is required');
1234
+ if (!data.type)
1235
+ throw new Error('Navigation object type is required');
1236
+ if (!NAVIGATION_TYPE_NAMES[data.type])
1237
+ throw new Error(`Unknown navigation type "${data.type}"`);
1238
+ const apiType = SDK_TO_API[data.type];
1239
+ const properties = await this.buildProperties(data.type, (data.properties ?? {}));
1240
+ const body = {
1241
+ isEnabled: data.enabled ?? true,
1242
+ isPreviewModeEnabled: data.previewEnabled ?? true,
1243
+ isCachingEnabled: this.getDefaultCaching(data.type),
1244
+ name: data.name,
1245
+ description: data.description ?? '',
1246
+ navigationType: apiType,
1247
+ sharedGroups: [],
1248
+ primaryGroup: { id: 0 },
1249
+ properties,
1250
+ };
1251
+ // CSS Selector quirk: requires "section-name": "on" at the top level
1252
+ if (data.type === 'css-selector') {
1253
+ body['section-name'] = 'on';
1254
+ }
1255
+ // Keyword Search quirk: "search-content-element-name" mirrors searchSectionElement at top level
1256
+ if (data.type === 'keyword-search') {
1257
+ const kwProps = data.properties;
1258
+ if (kwProps?.searchSectionElement) {
1259
+ body['search-content-element-name'] = kwProps.searchSectionElement;
1260
+ }
1261
+ }
1262
+ const response = await this.httpClient.request({
1263
+ method: 'POST',
1264
+ path: '/navigation',
1265
+ body,
1266
+ });
1267
+ return await this.resolveNavigationProperties(new NavigationObject(response, this.httpClient));
1268
+ }
1269
+ /**
1270
+ * Updates a navigation object's properties (immutable pattern).
1271
+ * Fetches the existing object, applies your changes, saves, and returns it.
1272
+ *
1273
+ * `properties` is merged into the existing properties — pass only the keys you
1274
+ * want to change. Everything else is preserved. The navigation `type` cannot be
1275
+ * changed after creation.
1276
+ */
1277
+ async update(id, data) {
1278
+ const nav = await this.get(id);
1279
+ if (data.name !== undefined) {
1280
+ if (!data.name.trim())
1281
+ throw new Error('Navigation object name cannot be empty');
1282
+ nav.name = data.name;
1283
+ }
1284
+ if (data.description !== undefined)
1285
+ nav.description = data.description;
1286
+ if (data.enabled !== undefined)
1287
+ nav.enabled = data.enabled;
1288
+ if (data.previewEnabled !== undefined)
1289
+ nav.previewEnabled = data.previewEnabled;
1290
+ if (data.cachingEnabled !== undefined)
1291
+ nav.cachingEnabled = data.cachingEnabled;
1292
+ // Merge properties rather than replace — callers pass only what changes
1293
+ if (data.properties !== undefined) {
1294
+ nav.properties = {
1295
+ ...nav.properties,
1296
+ ...data.properties,
1297
+ };
1298
+ }
1299
+ await nav.save();
1300
+ return nav;
1301
+ }
1302
+ /** Deletes a navigation object by ID. */
1303
+ async delete(id) {
1304
+ await this.httpClient.request({
1305
+ method: 'DELETE',
1306
+ path: `/navigation/${id}`,
1307
+ });
1308
+ }
1309
+ /** Returns the default caching value per type (some types always use false). */
1310
+ getDefaultCaching(type) {
1311
+ if (type === 'top-stories' || type === 'related-content' || type === 'top-content' || type === 'keyword-search')
1312
+ return true;
1313
+ return false;
1314
+ }
1315
+ /** Builds and validates properties for the given navigation type. */
1316
+ async buildProperties(type, input) {
1317
+ switch (type) {
1318
+ case 'a-to-z':
1319
+ return this.buildA2ZProperties(input);
1320
+ case 'breadcrumbs':
1321
+ return this.buildBreadcrumbsProperties(input);
1322
+ case 'css-selector':
1323
+ return this.buildCssSelectorProperties(input);
1324
+ case 'generate-file':
1325
+ return this.buildGenerateFileProperties(input);
1326
+ case 'language-switcher':
1327
+ return this.buildLanguageSwitcherProperties(input);
1328
+ case 'pagination':
1329
+ return this.buildPaginationProperties(input);
1330
+ case 'previous-next-fulltext':
1331
+ return this.buildPreviousNextProperties(input);
1332
+ case 'section-iterator':
1333
+ return this.buildSectionIteratorProperties(input);
1334
+ case 'related-section-branch':
1335
+ return this.buildRelatedSectionBranchProperties(input);
1336
+ case 'return-to-index':
1337
+ return this.buildReturnToIndexProperties(input);
1338
+ case 'section-meta-info':
1339
+ return this.buildSectionMetaInfoProperties(input);
1340
+ case 'top-stories':
1341
+ return this.buildTopStoriesProperties(input);
1342
+ case 'site-map':
1343
+ return this.buildSiteMapProperties(input);
1344
+ case 'section-details':
1345
+ return this.buildSectionDetailsProperties(input);
1346
+ case 'related-content':
1347
+ return this.buildRelatedContentProperties(input);
1348
+ case 'link-menu':
1349
+ return this.buildLinkMenuProperties(input);
1350
+ case 'publish-to-one-file':
1351
+ return this.buildPublishToOneFileProperties(input);
1352
+ case 'top-content':
1353
+ return this.buildTopContentProperties(input);
1354
+ case 'keyword-search':
1355
+ return this.buildKeywordSearchProperties(input);
1356
+ default:
1357
+ // For types not yet implemented, pass properties through as-is
1358
+ return this.buildGenericProperties(input);
1359
+ }
1360
+ }
1361
+ /** Builds properties for A to Z Navigation with validation. */
1362
+ async buildA2ZProperties(input) {
1363
+ const startLevel = input.startLevel ?? 0;
1364
+ const endLevel = input.endLevel ?? 0;
1365
+ const useSectionMetaData = input.useSectionMetaData ?? false;
1366
+ const sectionMetaContentTypeElement = input.sectionMetaContentTypeElement ?? '';
1367
+ const microSite = input.microSite ?? null;
1368
+ const beforeMenu = input.beforeMenu ?? '';
1369
+ const afterMenu = input.afterMenu ?? '';
1370
+ const beforeItem = input.beforeItem ?? '';
1371
+ const afterItem = input.afterItem ?? '';
1372
+ // Validate: sectionMetaContentTypeElement only valid when useSectionMetaData is true
1373
+ if (sectionMetaContentTypeElement && !useSectionMetaData) {
1374
+ throw new Error('sectionMetaContentTypeElement can only be set when useSectionMetaData is true');
1375
+ }
1376
+ // Validate sectionMetaContentTypeElement against the Section Meta Data content type
1377
+ // Resolve alias → raw name for the API
1378
+ let resolvedMetaElement = sectionMetaContentTypeElement;
1379
+ if (useSectionMetaData && sectionMetaContentTypeElement) {
1380
+ resolvedMetaElement = await this.resolveMetaDataElementAlias(sectionMetaContentTypeElement);
1381
+ }
1382
+ // Validate microsite ID
1383
+ if (microSite !== null) {
1384
+ await this.validateMicrositeId(microSite);
1385
+ }
1386
+ return {
1387
+ 'start_level': { value: String(startLevel) },
1388
+ 'end_level': { value: String(endLevel) },
1389
+ 'use_section_meta_data_element': { value: useSectionMetaData ? 'yes' : 'no' },
1390
+ 'section_meta_data_template': { value: resolvedMetaElement },
1391
+ 'sel_micro_site': { value: microSite !== null ? String(microSite) : '' },
1392
+ 'before_menu': { value: beforeMenu },
1393
+ 'after_menu': { value: afterMenu },
1394
+ 'before_item': { value: beforeItem },
1395
+ 'after_item': { value: afterItem },
1396
+ };
1397
+ }
1398
+ /** Builds properties for Breadcrumbs with validation. */
1399
+ buildBreadcrumbsProperties(input) {
1400
+ const startLevel = input.startLevel ?? 0;
1401
+ const endLevel = input.endLevel ?? 0;
1402
+ const useLinks = input.useLinks ?? false;
1403
+ const linkCurrent = input.linkCurrent ?? false;
1404
+ const hideHome = input.hideHome ?? false;
1405
+ const noSpace = input.noSpace ?? false;
1406
+ const maxLength = input.maxLength ?? 0;
1407
+ const separator = input.separator ?? '';
1408
+ const elementToAppend = input.elementToAppend ?? '';
1409
+ const beforeHtml = input.beforeHtml ?? '';
1410
+ const afterHtml = input.afterHtml ?? '';
1411
+ // Validation
1412
+ if (startLevel < 0)
1413
+ throw new Error('startLevel must be 0 or greater');
1414
+ if (endLevel < 0)
1415
+ throw new Error('endLevel must be 0 or greater');
1416
+ if (maxLength < 0)
1417
+ throw new Error('maxLength must be 0 or greater');
1418
+ if (noSpace && !useLinks)
1419
+ throw new Error('noSpace can only be true when useLinks is true');
1420
+ if (maxLength > 0 && (startLevel > 0 || endLevel > 0)) {
1421
+ throw new Error('maxLength cannot be set when startLevel or endLevel are above 0. Use either startLevel/endLevel or maxLength, not both.');
1422
+ }
1423
+ // Derived values (hidden from developer)
1424
+ const overspillFlag = maxLength > 0 ? 'yes' : 'no';
1425
+ const breadcrumbType = maxLength > 0 ? '20' : '10';
1426
+ const appendContentElement = elementToAppend ? 'yes' : 'no';
1427
+ return {
1428
+ 'start-level': { value: String(startLevel) },
1429
+ 'end-level': { value: String(endLevel) },
1430
+ 'use-links': { value: useLinks ? 'yes' : 'no' },
1431
+ 'link-current': { value: linkCurrent ? 'yes' : 'no' },
1432
+ 'hide-home': { value: hideHome ? 'yes' : 'no' },
1433
+ 'no-space': { value: noSpace ? 'yes' : 'no' },
1434
+ 'over_spill_length': { value: String(maxLength) },
1435
+ 'over_spill_flag': { value: overspillFlag },
1436
+ 'breadcrumb-type': { value: breadcrumbType },
1437
+ 'append-content-element': { value: appendContentElement },
1438
+ 'element-to-append': { value: elementToAppend },
1439
+ 'separator-html': { value: separator },
1440
+ 'before-html': { value: beforeHtml },
1441
+ 'after-html': { value: afterHtml },
1442
+ };
1443
+ }
1444
+ /** Builds properties for Section Iterator. */
1445
+ buildSectionIteratorProperties(input) {
1446
+ return {
1447
+ 'before-html': { value: input.beforeHtml ?? '' },
1448
+ 'between-html': { value: input.betweenHtml ?? '' },
1449
+ 'after-html': { value: input.afterHtml ?? '' },
1450
+ };
1451
+ }
1452
+ /** Builds properties for Related Section Branch. */
1453
+ buildRelatedSectionBranchProperties(input) {
1454
+ return {
1455
+ 'Name of child section': { value: input.childSectionName ?? '' },
1456
+ 'Link Text': { value: input.linkText ?? '' },
1457
+ };
1458
+ }
1459
+ /** Builds properties for Return to Index. */
1460
+ buildReturnToIndexProperties(input) {
1461
+ return {
1462
+ 'link-text': { value: input.linkText ?? '' },
1463
+ 'append-section-name': { value: input.appendSectionName ? 'yes' : 'no' },
1464
+ 'scroll-to-content': { value: input.scrollToContent ? 'yes' : 'no' },
1465
+ 'link-target': { value: input.linkTarget ?? '' },
1466
+ };
1467
+ }
1468
+ /** Builds properties for Section Meta Info with meta type validation. */
1469
+ async buildSectionMetaInfoProperties(input) {
1470
+ if (!input.metaType?.trim())
1471
+ throw new Error('metaType is required for Section Meta Info navigation');
1472
+ // Resolve meta type name to ID
1473
+ const metaLevels = await this.getMetaLevels();
1474
+ const match = metaLevels.find((m) => m.name.toLowerCase() === input.metaType.toLowerCase());
1475
+ if (!match) {
1476
+ const validNames = metaLevels.map((m) => `"${m.name}"`).join(', ');
1477
+ throw new Error(`Invalid metaType "${input.metaType}". Valid options: ${validNames}`);
1478
+ }
1479
+ return {
1480
+ 'meta-type': { value: String(match.id) },
1481
+ 'date-format': { value: input.dateFormat ?? '' },
1482
+ 'before-html': { value: input.beforeHtml ?? '' },
1483
+ 'after-html': { value: input.afterHtml ?? '' },
1484
+ };
1485
+ }
1486
+ /** Fetches meta tag level definitions from GET /meta/level. */
1487
+ async getMetaLevels() {
1488
+ return this.httpClient.request({
1489
+ method: 'GET',
1490
+ path: '/meta/level',
1491
+ });
1492
+ }
1493
+ /** Builds properties for Section Details with validation. */
1494
+ async buildSectionDetailsProperties(input) {
1495
+ const method = input.detailMethod ?? 'current';
1496
+ const validMethods = ['current', 'level', 'section'];
1497
+ if (!validMethods.includes(method)) {
1498
+ throw new Error(`Invalid detailMethod "${method}". Valid options: ${validMethods.join(', ')}`);
1499
+ }
1500
+ const validDisplayTypes = ['id', 'name', 'path', 'link'];
1501
+ const displayType = input.displayType ?? 'id';
1502
+ if (!validDisplayTypes.includes(displayType)) {
1503
+ throw new Error(`Invalid displayType "${displayType}". Valid options: ${validDisplayTypes.join(', ')}`);
1504
+ }
1505
+ let level = 0;
1506
+ let section = 0;
1507
+ if (method === 'level') {
1508
+ level = input.level ?? 0;
1509
+ }
1510
+ else if (input.level && input.level > 0) {
1511
+ throw new Error('level can only be set when detailMethod is "level"');
1512
+ }
1513
+ if (method === 'section') {
1514
+ if (!input.section)
1515
+ throw new Error('section is required when detailMethod is "section"');
1516
+ await this.validateSectionExists(input.section);
1517
+ section = input.section;
1518
+ }
1519
+ else if (input.section && input.section > 0) {
1520
+ throw new Error('section can only be set when detailMethod is "section"');
1521
+ }
1522
+ return {
1523
+ 'details-method': { value: DETAIL_METHOD_TO_API[method] },
1524
+ 'level': { value: String(level) },
1525
+ 'section': { value: String(section) },
1526
+ 'display-type': { value: DISPLAY_TYPE_TO_API[displayType] },
1527
+ };
1528
+ }
1529
+ /** Builds properties for Link Menu with validation. */
1530
+ async buildLinkMenuProperties(input) {
1531
+ if (!input.menuType)
1532
+ throw new Error('menuType is required for Link Menu navigation');
1533
+ const validTypes = ['branch-at-level', 'children', 'siblings', 'siblings-and-children'];
1534
+ if (!validTypes.includes(input.menuType)) {
1535
+ throw new Error(`Invalid menuType "${input.menuType}". Valid options: ${validTypes.join(', ')}`);
1536
+ }
1537
+ const menuType = input.menuType;
1538
+ const numToRecurse = menuType === 'branch-at-level' ? (input.numToRecurse ?? 1) : 1;
1539
+ const showLinkDisplay = (menuType === 'branch-at-level' && numToRecurse > 1) || menuType === 'siblings-and-children';
1540
+ // Validate conditional fields
1541
+ if (menuType !== 'branch-at-level' && input.level && input.level > 0) {
1542
+ throw new Error('level can only be set when menuType is "branch-at-level"');
1543
+ }
1544
+ if (menuType !== 'branch-at-level' && input.numToRecurse && input.numToRecurse > 1) {
1545
+ throw new Error('numToRecurse can only be set when menuType is "branch-at-level"');
1546
+ }
1547
+ if (!showLinkDisplay && input.subNavigationType) {
1548
+ throw new Error('subNavigationType can only be set when menuType is "branch-at-level" with numToRecurse > 1, or "siblings-and-children"');
1549
+ }
1550
+ if (menuType !== 'children' && input.displaySpecificBranch) {
1551
+ throw new Error('displaySpecificBranch can only be set when menuType is "children"');
1552
+ }
1553
+ if (menuType !== 'children' && input.showSiblingsIfNoChildren) {
1554
+ throw new Error('showSiblingsIfNoChildren can only be set when menuType is "children"');
1555
+ }
1556
+ if (menuType !== 'children' && input.showAncestorsIfNoChildren) {
1557
+ throw new Error('showAncestorsIfNoChildren can only be set when menuType is "children"');
1558
+ }
1559
+ // Validate specificBranchId
1560
+ if (input.displaySpecificBranch && input.specificBranchId) {
1561
+ await this.validateSectionExists(input.specificBranchId);
1562
+ }
1563
+ if (input.specificBranchId && !input.displaySpecificBranch) {
1564
+ throw new Error('specificBranchId can only be set when displaySpecificBranch is true');
1565
+ }
1566
+ return {
1567
+ 'menutype': { value: LINK_MENU_TYPE_TO_API[menuType] },
1568
+ 'menu-display-type': { value: MENU_DISPLAY_TO_API[input.menuDisplayType ?? 'normal'] ?? 'menu-display-normal' },
1569
+ 'level': { value: menuType === 'branch-at-level' ? String(input.level ?? 0) : '0' },
1570
+ 'numtorecurse': { value: menuType === 'branch-at-level' ? String(numToRecurse) : '1' },
1571
+ 'link-display-type': { value: showLinkDisplay ? (LINK_DISPLAY_TO_API[input.subNavigationType ?? 'ul'] ?? 'link-display-ul') : 'link-display-ul' },
1572
+ 'show-non-current-children': { value: input.showNonCurrentChildren ? 'yes' : 'no' },
1573
+ 'class_current_branch': { value: input.useCurrentBranchClass ? 'yes' : 'no' },
1574
+ 'make-section-link': { value: input.currentSectionLink ? 'yes' : 'no' },
1575
+ 'title-prepend-sect': { value: input.addSectionName ? 'yes' : 'no' },
1576
+ 'display-specific-branch': { value: menuType === 'children' && input.displaySpecificBranch ? 'yes' : 'no' },
1577
+ 'specific-branch-id': { value: menuType === 'children' && input.displaySpecificBranch ? String(input.specificBranchId ?? 0) : '0' },
1578
+ 'sib-if-no-children': { value: menuType === 'children' ? (input.showSiblingsIfNoChildren ? 'yes' : 'no') : 'no' },
1579
+ 'anc-if-no-children': { value: menuType === 'children' ? (input.showAncestorsIfNoChildren ? 'yes' : 'no') : 'no' },
1580
+ 'title': { value: input.title ?? '' },
1581
+ 'before-menu-html': { value: input.beforeMenuHtml ?? '' },
1582
+ 'after-menu-html': { value: input.afterMenuHtml ?? '' },
1583
+ 'before-html': { value: input.beforeLinkHtml ?? '' },
1584
+ 'after-html': { value: input.afterLinkHtml ?? '' },
1585
+ 'between-link': { value: input.betweenLink ?? '' },
1586
+ };
1587
+ }
1588
+ /** Builds properties for Publish to One File with validation. */
1589
+ async buildPublishToOneFileProperties(input) {
1590
+ const startSection = input.startSection ?? 'current';
1591
+ const validModes = ['current', 'specific', 'element'];
1592
+ if (!validModes.includes(startSection)) {
1593
+ throw new Error(`Invalid startSection "${startSection}". Valid options: ${validModes.join(', ')}`);
1594
+ }
1595
+ // Validate contentTypeId if non-zero
1596
+ if (input.contentTypeId && input.contentTypeId > 0) {
1597
+ await this.validateContentTypeExists(input.contentTypeId);
1598
+ }
1599
+ // Validate section for 'specific' mode
1600
+ if (startSection === 'specific') {
1601
+ if (!input.section)
1602
+ throw new Error('section is required when startSection is "specific"');
1603
+ await this.validateSectionExists(input.section);
1604
+ }
1605
+ else if (input.section) {
1606
+ throw new Error('section can only be set when startSection is "specific"');
1607
+ }
1608
+ // Validate startSectionElement for 'element' mode
1609
+ if (startSection !== 'element' && input.startSectionElement) {
1610
+ throw new Error('startSectionElement can only be set when startSection is "element"');
1611
+ }
1612
+ // Validate and resolve startSectionElement against content type if contentTypeId > 0
1613
+ let resolvedStartSectionElement = input.startSectionElement ?? '';
1614
+ if (startSection === 'element' && resolvedStartSectionElement && input.contentTypeId && input.contentTypeId > 0) {
1615
+ const elements = await this.getContentTypeElements(input.contentTypeId);
1616
+ const sectionLinkTypeId = await this.getElementTypeId('Section/Content Link');
1617
+ const sectionLinkElements = elements.filter((el) => el.type === sectionLinkTypeId);
1618
+ const match = sectionLinkElements.find((el) => (el.alias || el.name).toLowerCase() === resolvedStartSectionElement.toLowerCase());
1619
+ if (!match) {
1620
+ const validNames = sectionLinkElements.map((el) => `"${el.alias || el.name}"`).join(', ');
1621
+ throw new Error(`Invalid startSectionElement "${resolvedStartSectionElement}". Valid Section/Content Link elements on content type ${input.contentTypeId}: ${validNames || 'none'}`);
1622
+ }
1623
+ resolvedStartSectionElement = match.name; // alias → raw name for API
1624
+ }
1625
+ // Validate showSectionName-gated fields
1626
+ const showSectionName = input.showSectionName ?? false;
1627
+ if (!showSectionName) {
1628
+ if (input.showNameForHidden)
1629
+ throw new Error('showNameForHidden can only be set when showSectionName is true');
1630
+ if (input.beforeSectionName)
1631
+ throw new Error('beforeSectionName can only be set when showSectionName is true');
1632
+ if (input.afterSectionName)
1633
+ throw new Error('afterSectionName can only be set when showSectionName is true');
1634
+ }
1635
+ // Validate surroundingPageLayout
1636
+ if (input.surroundingPageLayout) {
1637
+ await this.validatePageLayoutExists(input.surroundingPageLayout);
1638
+ }
1639
+ // Validate pagination-gated fields
1640
+ const pagination = input.pagination ?? false;
1641
+ if (!pagination) {
1642
+ if (input.contentPerPage && input.contentPerPage > 0)
1643
+ throw new Error('contentPerPage can only be set when pagination is true');
1644
+ if (input.beforePaginationHtml)
1645
+ throw new Error('beforePaginationHtml can only be set when pagination is true');
1646
+ if (input.betweenPaginationHtml)
1647
+ throw new Error('betweenPaginationHtml can only be set when pagination is true');
1648
+ if (input.afterPaginationHtml)
1649
+ throw new Error('afterPaginationHtml can only be set when pagination is true');
1650
+ }
1651
+ const altLayoutName = input.altLayoutName ?? '';
1652
+ return {
1653
+ 'template-list': { value: String(input.contentTypeId ?? 0) },
1654
+ 'content-type-section': { value: startSection === 'element' ? 'yes' : 'no' },
1655
+ 'start-section': { value: startSection === 'specific' ? String(input.section) : '0' },
1656
+ 'start-section-element': { value: startSection === 'element' ? resolvedStartSectionElement : '' },
1657
+ 'show-hidden-sections': { value: input.showHiddenSections ? 'yes' : 'no' },
1658
+ 'levels-to-recurse': { value: String(input.levelsToRecurse ?? 1) },
1659
+ 'before-html': { value: input.beforeHtml ?? '' },
1660
+ 'after-html': { value: input.afterHtml ?? '' },
1661
+ 'show-section-name': { value: showSectionName ? 'yes' : 'no' },
1662
+ 'show-name-for-hidden': { value: showSectionName && input.showNameForHidden ? 'yes' : 'no' },
1663
+ 'before-section-name': { value: showSectionName ? (input.beforeSectionName ?? '') : '' },
1664
+ 'after-section-name': { value: showSectionName ? (input.afterSectionName ?? '') : '' },
1665
+ 'surrounding-style': { value: input.surroundingPageLayout ? String(input.surroundingPageLayout) : '' },
1666
+ 'use-alt-formatter': { value: altLayoutName ? 'yes' : 'no' },
1667
+ 'alt-formatter-type': { value: altLayoutName },
1668
+ 'enable-caching': { value: input.enableCaching !== false ? 'yes' : 'no' },
1669
+ 'pagination-across-pages': { value: pagination ? 'yes' : 'no' },
1670
+ 'content-per-page': { value: pagination ? String(input.contentPerPage ?? 0) : '0' },
1671
+ 'before-pagination-html': { value: pagination ? (input.beforePaginationHtml ?? '') : '' },
1672
+ 'between-pagination-html': { value: pagination ? (input.betweenPaginationHtml ?? '') : '' },
1673
+ 'after-pagination-html': { value: pagination ? (input.afterPaginationHtml ?? '') : '' },
1674
+ };
1675
+ }
1676
+ /** Builds properties for Top Content with validation. */
1677
+ async buildTopContentProperties(input) {
1678
+ const fetchMethod = input.fetchMethod ?? 'current';
1679
+ const validMethods = ['current', 'current-branch', 'branch', 'section'];
1680
+ if (!validMethods.includes(fetchMethod)) {
1681
+ throw new Error(`Invalid fetchMethod "${fetchMethod}". Valid options: ${validMethods.join(', ')}`);
1682
+ }
1683
+ // Validate section for branch/section methods
1684
+ if (fetchMethod === 'branch' || fetchMethod === 'section') {
1685
+ if (!input.section)
1686
+ throw new Error(`section is required when fetchMethod is "${fetchMethod}"`);
1687
+ await this.validateSectionExists(input.section);
1688
+ }
1689
+ else if (input.section && input.section > 0) {
1690
+ throw new Error('section can only be set when fetchMethod is "branch" or "section"');
1691
+ }
1692
+ // Validate contentTypeIds
1693
+ if (input.contentTypeIds && input.contentTypeIds.length > 0) {
1694
+ for (const ctId of input.contentTypeIds) {
1695
+ await this.validateContentTypeExists(ctId);
1696
+ }
1697
+ }
1698
+ // Validate channelId
1699
+ if (input.channelId && input.channelId > 0) {
1700
+ await this.validateChannelExists(input.channelId);
1701
+ }
1702
+ const altLayoutName = input.altLayoutName ?? '';
1703
+ return {
1704
+ 'fetch-method': { value: TC_FETCH_METHOD_TO_API[fetchMethod] },
1705
+ 'section': { value: (fetchMethod === 'branch' || fetchMethod === 'section') ? String(input.section) : '0' },
1706
+ 'template-ids': { value: input.contentTypeIds && input.contentTypeIds.length > 0 ? input.contentTypeIds.join(',') : '' },
1707
+ 'channel-id': { value: String(input.channelId ?? 0) },
1708
+ 'upcoming-content': { value: input.upcomingContent ? 'yes' : 'no' },
1709
+ 'pub-element': { value: input.dateElement ?? '' },
1710
+ 'date_ordered_content': { value: input.ignoreDateOrdering ? 'yes' : 'no' },
1711
+ 'number-of-pieces': { value: String(input.numToDisplay ?? 0) },
1712
+ 'starting-content': { value: String(input.startAt ?? 0) },
1713
+ 'use-alt-formatter': { value: altLayoutName ? 'yes' : 'no' },
1714
+ 'alt-formatter-type': { value: altLayoutName },
1715
+ 'title': { value: input.title ?? '' },
1716
+ 'before-html': { value: input.beforeHtml ?? '' },
1717
+ 'after-html': { value: input.afterHtml ?? '' },
1718
+ };
1719
+ }
1720
+ /** Builds properties for Keyword Search Content with validation. */
1721
+ async buildKeywordSearchProperties(input) {
1722
+ const keywordFetchMethod = input.keywordFetchMethod ?? 'current';
1723
+ const contentFetchMethod = input.contentFetchMethod ?? 'section';
1724
+ const pagination = input.pagination ?? false;
1725
+ // Validate keyword fetch method
1726
+ if (keywordFetchMethod === 'section') {
1727
+ if (!input.keywordSection)
1728
+ throw new Error('keywordSection is required when keywordFetchMethod is "section"');
1729
+ await this.validateSectionExists(input.keywordSection);
1730
+ }
1731
+ else if (input.keywordSection && input.keywordSection > 0) {
1732
+ throw new Error('keywordSection can only be set when keywordFetchMethod is "section"');
1733
+ }
1734
+ // Validate keywordContentTypeId
1735
+ if (input.keywordContentTypeId && input.keywordContentTypeId > 0) {
1736
+ await this.validateContentTypeExists(input.keywordContentTypeId);
1737
+ }
1738
+ // Validate content fetch method
1739
+ if (contentFetchMethod === 'section' || contentFetchMethod === 'branch') {
1740
+ if (!input.searchSection && !input.searchSectionElement) {
1741
+ throw new Error(`searchSection or searchSectionElement is required when contentFetchMethod is "${contentFetchMethod}"`);
1742
+ }
1743
+ if (input.searchSection && input.searchSectionElement) {
1744
+ throw new Error('Cannot set both searchSection and searchSectionElement. Use one or the other.');
1745
+ }
1746
+ if (input.searchSection) {
1747
+ await this.validateSectionExists(input.searchSection);
1748
+ }
1749
+ // Validate searchSectionElement against content type if keywordContentTypeId > 0 — must be Section/Content Link (type 14)
1750
+ if (input.searchSectionElement && input.keywordContentTypeId && input.keywordContentTypeId > 0) {
1751
+ const elements = await this.getContentTypeElements(input.keywordContentTypeId);
1752
+ const sectionLinkTypeId = await this.getElementTypeId('Section/Content Link');
1753
+ const sectionLinkElements = elements.filter((el) => el.type === sectionLinkTypeId);
1754
+ const match = sectionLinkElements.find((el) => (el.alias || el.name).toLowerCase() === input.searchSectionElement.toLowerCase());
1755
+ if (!match) {
1756
+ const validNames = sectionLinkElements.map((el) => `"${el.alias || el.name}"`).join(', ');
1757
+ throw new Error(`Invalid searchSectionElement "${input.searchSectionElement}". Valid Section/Content Link elements on content type ${input.keywordContentTypeId}: ${validNames || 'none'}`);
1758
+ }
1759
+ }
1760
+ }
1761
+ // Validate branch-at-level fields
1762
+ if (contentFetchMethod !== 'branch-at-level') {
1763
+ if (input.startLevel && input.startLevel > 0)
1764
+ throw new Error('startLevel can only be set when contentFetchMethod is "branch-at-level"');
1765
+ if (input.endLevel && input.endLevel > 0)
1766
+ throw new Error('endLevel can only be set when contentFetchMethod is "branch-at-level"');
1767
+ }
1768
+ // Validate searchContentTypeId
1769
+ if (input.searchContentTypeId && input.searchContentTypeId > 0) {
1770
+ await this.validateContentTypeExists(input.searchContentTypeId);
1771
+ }
1772
+ // Validate date element fields
1773
+ if (!input.sortByDateElement && input.dateElementName) {
1774
+ throw new Error('dateElementName can only be set when sortByDateElement is true');
1775
+ }
1776
+ // Validate pagination fields
1777
+ if (!pagination) {
1778
+ if (input.contentPerPage && input.contentPerPage > 0)
1779
+ throw new Error('contentPerPage can only be set when pagination is true');
1780
+ if (input.beforePaginationHtml)
1781
+ throw new Error('beforePaginationHtml can only be set when pagination is true');
1782
+ if (input.betweenPaginationHtml)
1783
+ throw new Error('betweenPaginationHtml can only be set when pagination is true');
1784
+ if (input.afterPaginationHtml)
1785
+ throw new Error('afterPaginationHtml can only be set when pagination is true');
1786
+ }
1787
+ // Resolve element aliases to raw names for the API
1788
+ let resolvedKeywordElements = input.keywordElements ?? [];
1789
+ if (resolvedKeywordElements.length > 0 && input.keywordContentTypeId && input.keywordContentTypeId > 0) {
1790
+ resolvedKeywordElements = await this.resolveElementAliasesToNames(input.keywordContentTypeId, resolvedKeywordElements, 'keywordElements');
1791
+ }
1792
+ let resolvedSearchElements = input.searchElements ?? [];
1793
+ if (resolvedSearchElements.length > 0 && input.searchContentTypeId && input.searchContentTypeId > 0) {
1794
+ resolvedSearchElements = await this.resolveElementAliasesToNames(input.searchContentTypeId, resolvedSearchElements, 'searchElements');
1795
+ }
1796
+ let resolvedSearchSectionElement = input.searchSectionElement ?? '';
1797
+ if (resolvedSearchSectionElement && input.keywordContentTypeId && input.keywordContentTypeId > 0) {
1798
+ const elements = await this.getContentTypeElements(input.keywordContentTypeId);
1799
+ const sectionLinkTypeId = await this.getElementTypeId('Section/Content Link');
1800
+ const sectionLinkElements = elements.filter((el) => el.type === sectionLinkTypeId);
1801
+ const match = sectionLinkElements.find((el) => (el.alias || el.name).toLowerCase() === resolvedSearchSectionElement.toLowerCase());
1802
+ if (match) {
1803
+ resolvedSearchSectionElement = match.name; // alias → raw name
1804
+ }
1805
+ }
1806
+ const altLayoutName = input.altLayoutName ?? '';
1807
+ const crossLangs = input.crossLanguageLanguages ?? [];
1808
+ return {
1809
+ 'fetch-method': { value: KW_FETCH_TO_API[keywordFetchMethod] },
1810
+ 'narrow-on-fulltext': { value: input.narrowToSingleContentItem ? 'yes' : 'no' },
1811
+ 'template-list-get': { value: String(input.keywordContentTypeId && input.keywordContentTypeId > 0 ? input.keywordContentTypeId : -1) },
1812
+ 'template-element-get': { value: resolvedKeywordElements.join(',') },
1813
+ 'search-fetch-method': { value: KW_CONTENT_FETCH_TO_API[contentFetchMethod] },
1814
+ 'section': { value: keywordFetchMethod === 'section' ? String(input.keywordSection) : '0' },
1815
+ 'search-section': { value: (contentFetchMethod === 'section' || contentFetchMethod === 'branch') && input.searchSection ? String(input.searchSection) : '0' },
1816
+ 'template-element-for-search-section': { value: resolvedSearchSectionElement },
1817
+ 'level': { value: contentFetchMethod === 'branch-at-level' ? String(input.startLevel ?? 0) : '0' },
1818
+ 'num-to-recurse': { value: contentFetchMethod === 'branch-at-level' ? String(input.endLevel ?? 0) : '0' },
1819
+ 'template-list-search': { value: String(input.searchContentTypeId && input.searchContentTypeId > 0 ? input.searchContentTypeId : -1) },
1820
+ 'template-element-search': { value: resolvedSearchElements.join(',') },
1821
+ 'number-of-pieces': { value: String(input.numToDisplay ?? '') },
1822
+ 'order-by': { value: KW_SORT_TO_API[input.sortType ?? 'name'] ?? 'order-name' },
1823
+ 'order-by-date-element': { value: input.sortByDateElement ? 'yes' : 'no' },
1824
+ 'order-by-date-element-name': { value: input.sortByDateElement ? (input.dateElementName ?? '') : '' },
1825
+ 'show-upcoming-content': { value: input.showUpcomingContent ? 'yes' : 'no' },
1826
+ 'show-hidden-sections': { value: input.showHiddenSections ? 'yes' : 'no' },
1827
+ 'match-composite-keywords': { value: input.matchCompositeKeywords ? 'yes' : 'no' },
1828
+ 'match-sub-items': { value: input.matchSubItems ? 'yes' : 'no' },
1829
+ 'cross-language-searching-enabled': { value: input.crossLanguageSearch ? 'yes' : 'no' },
1830
+ 'cross-language-searching-languages': { value: crossLangs.join(',') },
1831
+ 'use-alt-formatter': { value: altLayoutName ? 'yes' : 'no' },
1832
+ 'alt-formatter-type': { value: altLayoutName },
1833
+ 'before-html': { value: input.beforeHtml ?? '' },
1834
+ 'after-html': { value: input.afterHtml ?? '' },
1835
+ 'pagination-enabled': { value: pagination ? 'yes' : 'no' },
1836
+ 'content-per-page': { value: pagination ? String(input.contentPerPage ?? 0) : '0' },
1837
+ 'before-pagination-html': { value: pagination ? (input.beforePaginationHtml ?? '') : '' },
1838
+ 'between-pagination-html': { value: pagination ? (input.betweenPaginationHtml ?? '') : '' },
1839
+ 'after-pagination-html': { value: pagination ? (input.afterPaginationHtml ?? '') : '' },
1840
+ };
1841
+ }
1842
+ /** Fetches content type elements. */
1843
+ async getContentTypeElements(contentTypeId) {
1844
+ const ct = await this.httpClient.request({
1845
+ method: 'GET',
1846
+ path: `/contenttype/${contentTypeId}`,
1847
+ });
1848
+ return ct.contentTypeElements ?? [];
1849
+ }
1850
+ /** Resolves an element type name (e.g. 'Plain Text', 'Section/Content Link') to its numeric ID via GET /type/. */
1851
+ async getElementTypeId(typeName) {
1852
+ const types = await this.httpClient.request({
1853
+ method: 'GET',
1854
+ path: '/type/',
1855
+ });
1856
+ const match = types.find((t) => t.name.toLowerCase() === typeName.toLowerCase());
1857
+ if (!match)
1858
+ throw new Error(`Unknown element type "${typeName}"`);
1859
+ return match.id;
1860
+ }
1861
+ /**
1862
+ * Validates an element alias exists on a content type and returns the raw name.
1863
+ * Developer passes alias → SDK sends raw name to API.
1864
+ */
1865
+ async resolveElementAliasToName(contentTypeId, alias, fieldLabel) {
1866
+ const elements = await this.getContentTypeElements(contentTypeId);
1867
+ const match = elements.find((el) => (el.alias || el.name).toLowerCase() === alias.toLowerCase());
1868
+ if (!match) {
1869
+ const validNames = elements.map((el) => `"${el.alias || el.name}"`).join(', ');
1870
+ throw new Error(`Invalid ${fieldLabel} "${alias}". Valid elements on content type ${contentTypeId}: ${validNames}`);
1871
+ }
1872
+ return match.name;
1873
+ }
1874
+ /**
1875
+ * Resolves multiple element aliases to raw names.
1876
+ */
1877
+ async resolveElementAliasesToNames(contentTypeId, aliases, fieldLabel) {
1878
+ const elements = await this.getContentTypeElements(contentTypeId);
1879
+ const names = [];
1880
+ for (const alias of aliases) {
1881
+ const match = elements.find((el) => (el.alias || el.name).toLowerCase() === alias.toLowerCase());
1882
+ if (!match) {
1883
+ const validNames = elements.map((el) => `"${el.alias || el.name}"`).join(', ');
1884
+ throw new Error(`Invalid ${fieldLabel} "${alias}". Valid elements on content type ${contentTypeId}: ${validNames}`);
1885
+ }
1886
+ names.push(match.name);
1887
+ }
1888
+ return names;
1889
+ }
1890
+ /**
1891
+ * Resolves raw element names back to aliases for display.
1892
+ * API stores raw name → SDK returns alias to developer.
1893
+ */
1894
+ async resolveElementNamesToAliases(contentTypeId, names) {
1895
+ try {
1896
+ const elements = await this.getContentTypeElements(contentTypeId);
1897
+ return names.map((name) => {
1898
+ const match = elements.find((el) => el.name === name);
1899
+ return match ? (match.alias || match.name) : name;
1900
+ });
1901
+ }
1902
+ catch {
1903
+ return names; // If fetch fails, return raw names
1904
+ }
1905
+ }
1906
+ /**
1907
+ * Resolves a single raw element name back to its alias.
1908
+ */
1909
+ async resolveElementNameToAlias(contentTypeId, name) {
1910
+ try {
1911
+ const elements = await this.getContentTypeElements(contentTypeId);
1912
+ const match = elements.find((el) => el.name === name);
1913
+ return match ? (match.alias || match.name) : name;
1914
+ }
1915
+ catch {
1916
+ return name;
1917
+ }
1918
+ }
1919
+ /** Validates that an element name exists on a content type (legacy — used by searchSectionElement validation in builder). */
1920
+ async validateElementOnContentType(contentTypeId, elementName) {
1921
+ const elements = await this.getContentTypeElements(contentTypeId);
1922
+ const match = elements.find((el) => (el.alias || el.name).toLowerCase() === elementName.toLowerCase() || el.name.toLowerCase() === elementName.toLowerCase());
1923
+ if (!match) {
1924
+ const validNames = elements.map((el) => `"${el.alias || el.name}"`).join(', ');
1925
+ throw new Error(`Invalid searchSectionElement "${elementName}". Valid elements on content type ${contentTypeId}: ${validNames}`);
1926
+ }
1927
+ }
1928
+ /** Validates that a channel exists by ID. */
1929
+ async validateChannelExists(id) {
1930
+ try {
1931
+ await this.httpClient.request({
1932
+ method: 'GET',
1933
+ path: `/channel/${id}`,
1934
+ });
1935
+ }
1936
+ catch {
1937
+ throw new Error(`Invalid channelId: channel ${id} not found`);
1938
+ }
1939
+ }
1940
+ /** Validates that a page layout exists by ID. */
1941
+ async validatePageLayoutExists(id) {
1942
+ try {
1943
+ await this.httpClient.request({
1944
+ method: 'GET',
1945
+ path: `/pageLayout/${id}`,
1946
+ });
1947
+ }
1948
+ catch {
1949
+ throw new Error(`Invalid surroundingPageLayout: page layout ${id} not found`);
1950
+ }
1951
+ }
1952
+ /** Builds properties for Related Content with validation. */
1953
+ async buildRelatedContentProperties(input) {
1954
+ const fetchMethod = input.fetchMethod ?? 'current';
1955
+ const validMethods = ['current', 'section', 'child'];
1956
+ if (!validMethods.includes(fetchMethod)) {
1957
+ throw new Error(`Invalid fetchMethod "${fetchMethod}". Valid options: ${validMethods.join(', ')}`);
1958
+ }
1959
+ const altLayoutName = input.altLayoutName ?? '';
1960
+ // Validate section for 'section' method
1961
+ if (fetchMethod === 'section') {
1962
+ if (!input.section)
1963
+ throw new Error('section is required when fetchMethod is "section"');
1964
+ await this.validateSectionExists(input.section);
1965
+ }
1966
+ // Validate contentTypeIds for 'child' method
1967
+ if (fetchMethod === 'child') {
1968
+ if (!input.contentTypeIds || input.contentTypeIds.length === 0) {
1969
+ throw new Error('contentTypeIds is required when fetchMethod is "child"');
1970
+ }
1971
+ for (const ctId of input.contentTypeIds) {
1972
+ await this.validateContentTypeExists(ctId);
1973
+ }
1974
+ }
1975
+ return {
1976
+ 'fetch-method': { value: RC_FETCH_METHOD_TO_API[fetchMethod] },
1977
+ 'relatedcontent-type': { value: RC_TYPE_MAP[fetchMethod] },
1978
+ 'section': { value: fetchMethod === 'section' ? String(input.section) : '0' },
1979
+ 'fetch-child': fetchMethod === 'child' && input.childSectionName ? { value: input.childSectionName } : {},
1980
+ 'template-ids': { value: fetchMethod === 'child' ? (input.contentTypeIds ?? []).join(',') : '' },
1981
+ 'number-of-pieces': { value: fetchMethod === 'child' ? String(input.display ?? 0) : '0' },
1982
+ 'recurse-child-section': { value: fetchMethod === 'child' ? (input.recurseChildSection ? 'yes' : 'no') : 'no' },
1983
+ 'use-alt-formatter': { value: altLayoutName ? 'yes' : 'no' },
1984
+ 'alt-formatter-type': { value: altLayoutName },
1985
+ 'title': { value: input.title ?? '' },
1986
+ 'before-html': { value: input.beforeHtml ?? '' },
1987
+ 'after-html': { value: input.afterHtml ?? '' },
1988
+ 'search-upwards': { value: 'no' },
1989
+ 'more-link': { value: 'no' },
1990
+ 'more-link-text': {},
1991
+ 'levels-to-recurse': {},
1992
+ 'show-name-when-hidden': { value: 'no' },
1993
+ };
1994
+ }
1995
+ /** Builds properties for Top Stories with section validation. */
1996
+ async buildTopStoriesProperties(input) {
1997
+ if (!input.section)
1998
+ throw new Error('section is required for Top Stories navigation');
1999
+ await this.validateSectionExists(input.section);
2000
+ return {
2001
+ 'section': { value: String(input.section) },
2002
+ 'numtoshow': { value: String(input.numToShow ?? 0) },
2003
+ 'link-to-fulltext': { value: input.linkToFulltext ? 'yes' : 'no' },
2004
+ 'title': { value: input.title ?? '' },
2005
+ 'before-menu-html': { value: input.beforeMenuHtml ?? '' },
2006
+ 'after-menu-html': { value: input.afterMenuHtml ?? '' },
2007
+ 'before-html': { value: input.beforeHtml ?? '' },
2008
+ 'after-html': { value: input.afterHtml ?? '' },
2009
+ };
2010
+ }
2011
+ /** Builds properties for Previous/Next Fulltext Content. */
2012
+ buildPreviousNextProperties(input) {
2013
+ const type = input.type ?? 'previous';
2014
+ const validTypes = ['previous', 'next', 'both'];
2015
+ if (!validTypes.includes(type)) {
2016
+ throw new Error(`Invalid type "${type}". Valid options: ${validTypes.join(', ')}`);
2017
+ }
2018
+ const altLayoutName = input.altLayoutName ?? '';
2019
+ return {
2020
+ 'id_previous': { value: type === 'previous' ? 'true' : 'false' },
2021
+ 'id_next': { value: type === 'next' ? 'true' : 'false' },
2022
+ 'id_previous_and_next': { value: type === 'both' ? 'true' : 'false' },
2023
+ 'id_custom_formatter': { value: altLayoutName ? 'yes' : 'no' },
2024
+ 'id_custom_formatter_textarea': { value: altLayoutName },
2025
+ 'id_skip_non_fulltext_content': { value: input.skipNonFulltextContent ? 'yes' : 'no' },
2026
+ 'id_next_navigation_with_previous_next_navigation': { value: input.onlyLinkToContentWithNav ? 'yes' : 'no' },
2027
+ 'id_same_template_restriction': { value: input.sameContentTypeRestriction ? 'yes' : 'no' },
2028
+ 'id_display_on_boundary': { value: input.displayOnBoundary ? 'yes' : 'no' },
2029
+ 'id_display_content_name_as_title': { value: input.displayContentNameAsTitle ? 'yes' : 'no' },
2030
+ 'id_previous_html': { value: input.previousHtml ?? '' },
2031
+ 'id_between_html': { value: input.betweenHtml ?? '' },
2032
+ 'id_next_html': { value: input.nextHtml ?? '' },
2033
+ };
2034
+ }
2035
+ /** Builds properties for Pagination with validation. */
2036
+ async buildPaginationProperties(input) {
2037
+ if (!input.contentTypeId)
2038
+ throw new Error('contentTypeId is required for Pagination navigation');
2039
+ const fetchMethod = input.fetchMethod ?? 'current';
2040
+ const validMethods = ['current', 'current-branch', 'branch', 'branch-at-level', 'section'];
2041
+ if (!validMethods.includes(fetchMethod)) {
2042
+ throw new Error(`Invalid fetchMethod "${fetchMethod}". Valid options: ${validMethods.join(', ')}`);
2043
+ }
2044
+ // Validate content type exists
2045
+ await this.validateContentTypeExists(input.contentTypeId);
2046
+ // Validate altLayoutName against content type layouts if provided
2047
+ const altLayoutName = input.altLayoutName ?? '';
2048
+ if (altLayoutName) {
2049
+ await this.validateLayoutOnContentType(input.contentTypeId, altLayoutName);
2050
+ }
2051
+ // Determine section/level/numToRecurse based on fetchMethod
2052
+ let section = 0;
2053
+ let level = 0;
2054
+ let numToRecurse = 0;
2055
+ switch (fetchMethod) {
2056
+ case 'current':
2057
+ // All forced to 0
2058
+ break;
2059
+ case 'current-branch':
2060
+ numToRecurse = input.numToRecurse ?? 0;
2061
+ break;
2062
+ case 'branch':
2063
+ if (!input.section)
2064
+ throw new Error('section is required when fetchMethod is "branch"');
2065
+ await this.validateSectionExists(input.section);
2066
+ section = input.section;
2067
+ numToRecurse = input.numToRecurse ?? 0;
2068
+ break;
2069
+ case 'branch-at-level':
2070
+ if (!input.section)
2071
+ throw new Error('section is required when fetchMethod is "branch-at-level"');
2072
+ await this.validateSectionExists(input.section);
2073
+ section = input.section;
2074
+ level = input.level ?? 0;
2075
+ numToRecurse = input.numToRecurse ?? 0;
2076
+ break;
2077
+ case 'section':
2078
+ if (!input.section)
2079
+ throw new Error('section is required when fetchMethod is "section"');
2080
+ await this.validateSectionExists(input.section);
2081
+ section = input.section;
2082
+ break;
2083
+ }
2084
+ return {
2085
+ 'template-list': { value: String(input.contentTypeId) },
2086
+ 'fetch-method': { value: FETCH_METHOD_TO_API[fetchMethod] },
2087
+ 'section': { value: String(section) },
2088
+ 'level': { value: String(level) },
2089
+ 'num-to-recurse': { value: String(numToRecurse) },
2090
+ 'number-of-pieces': { value: String(input.contentItemsPerPage ?? 0) },
2091
+ 'max-number-of-pieces': { value: String(input.maxContentItems ?? 0) },
2092
+ 'num-links-to-show': { value: String(input.maxLinksPerPage ?? 0) },
2093
+ 'use-alt-formatter': { value: altLayoutName ? 'yes' : 'no' },
2094
+ 'alt-formatter-type': { value: altLayoutName },
2095
+ 'show-hidden-sections': { value: input.searchHiddenSections ? 'yes' : 'no' },
2096
+ 'before-html': { value: input.beforeHtml ?? '' },
2097
+ 'after-html': { value: input.afterHtml ?? '' },
2098
+ 'before-pagination-html': { value: input.beforePaginationHtml ?? '' },
2099
+ 'after-pagination-html': { value: input.afterPaginationHtml ?? '' },
2100
+ 'between-pagination-html': { value: input.betweenPaginationHtml ?? '' },
2101
+ };
2102
+ }
2103
+ /** Builds properties for Site Map with validation. */
2104
+ async buildSiteMapProperties(input) {
2105
+ const startSection = input.startSection ?? 0;
2106
+ const enableContentCount = input.enableContentCount ?? false;
2107
+ // Validate section if non-zero
2108
+ if (startSection > 0) {
2109
+ await this.validateSectionExists(startSection);
2110
+ }
2111
+ // Validate content-count-only fields
2112
+ if (!enableContentCount) {
2113
+ if (input.contentTypeIds && input.contentTypeIds.length > 0) {
2114
+ throw new Error('contentTypeIds can only be set when enableContentCount is true');
2115
+ }
2116
+ if (input.countRecursively) {
2117
+ throw new Error('countRecursively can only be set when enableContentCount is true');
2118
+ }
2119
+ if (input.maxLevelsToCount) {
2120
+ throw new Error('maxLevelsToCount can only be set when enableContentCount is true');
2121
+ }
2122
+ if (input.htmlBeforeContentCount) {
2123
+ throw new Error('htmlBeforeContentCount can only be set when enableContentCount is true');
2124
+ }
2125
+ if (input.htmlAfterContentCount) {
2126
+ throw new Error('htmlAfterContentCount can only be set when enableContentCount is true');
2127
+ }
2128
+ }
2129
+ // Validate contentTypeIds
2130
+ if (enableContentCount && input.contentTypeIds && input.contentTypeIds.length > 0) {
2131
+ for (const ctId of input.contentTypeIds) {
2132
+ await this.validateContentTypeExists(ctId);
2133
+ }
2134
+ }
2135
+ const contentTypeIdsStr = enableContentCount && input.contentTypeIds && input.contentTypeIds.length > 0
2136
+ ? input.contentTypeIds.join(',')
2137
+ : '0';
2138
+ return {
2139
+ 'section': { value: String(startSection) },
2140
+ 'levels': { value: String(input.levels ?? 0) },
2141
+ 'show_relative_child_sections': { value: input.childSectionLinks ? 'yes' : 'no' },
2142
+ 'enable_content_count': { value: enableContentCount ? 'yes' : 'no' },
2143
+ 'template_type': { value: contentTypeIdsStr },
2144
+ 'max_levels_to_count': { value: enableContentCount ? String(input.maxLevelsToCount ?? '') : '' },
2145
+ 'count_recursively': { value: enableContentCount ? (input.countRecursively ? 'yes' : 'no') : 'no' },
2146
+ 'html_before_content_count': { value: enableContentCount ? (input.htmlBeforeContentCount ?? '') : '' },
2147
+ 'html_after_content_count': { value: enableContentCount ? (input.htmlAfterContentCount ?? '') : '' },
2148
+ };
2149
+ }
2150
+ /** Validates that a content type exists by ID. */
2151
+ async validateContentTypeExists(id) {
2152
+ try {
2153
+ await this.httpClient.request({
2154
+ method: 'GET',
2155
+ path: `/contenttype/${id}`,
2156
+ });
2157
+ }
2158
+ catch {
2159
+ throw new Error(`Invalid contentTypeId: content type ${id} not found`);
2160
+ }
2161
+ }
2162
+ /** Validates that a layout name exists on a content type. */
2163
+ async validateLayoutOnContentType(contentTypeId, layoutName) {
2164
+ try {
2165
+ const layouts = await this.httpClient.request({
2166
+ method: 'GET',
2167
+ path: `/layout/contenttype/${contentTypeId}/en`,
2168
+ });
2169
+ const match = layouts.find((l) => l.name === layoutName);
2170
+ if (!match) {
2171
+ const validNames = layouts.map((l) => `"${l.name}"`).join(', ');
2172
+ throw new Error(`Invalid altLayoutName "${layoutName}". Valid layouts for content type ${contentTypeId}: ${validNames || 'none'}`);
2173
+ }
2174
+ }
2175
+ catch (e) {
2176
+ if (e instanceof Error && e.message.startsWith('Invalid altLayoutName'))
2177
+ throw e;
2178
+ throw new Error(`Invalid altLayoutName "${layoutName}": could not verify layouts for content type ${contentTypeId}`);
2179
+ }
2180
+ }
2181
+ /** Builds properties for Language Switcher. */
2182
+ buildLanguageSwitcherProperties(input) {
2183
+ const langCode = input.langCode ?? '';
2184
+ const alwaysOutput = input.alwaysOutput ?? false;
2185
+ const imageUrl = input.imageUrl ?? '';
2186
+ const imageExtension = input.imageExtension ?? '';
2187
+ const imageProperties = input.imageProperties ?? '';
2188
+ const beforeHtml = input.beforeHtml ?? '';
2189
+ const afterHtml = input.afterHtml ?? '';
2190
+ // Derive imageLink from whether any image fields are set
2191
+ const imageLink = imageUrl || imageExtension || imageProperties ? 'yes' : 'no';
2192
+ return {
2193
+ 'lang-code': { value: langCode },
2194
+ 'always-output': { value: alwaysOutput ? 'yes' : 'no' },
2195
+ 'image-link': { value: imageLink },
2196
+ 'url': { value: imageUrl },
2197
+ 'image-ext': { value: imageExtension },
2198
+ 'image-properties': { value: imageProperties },
2199
+ 'before': { value: beforeHtml },
2200
+ 'after': { value: afterHtml },
2201
+ };
2202
+ }
2203
+ /** Builds properties for Generate File with validation. */
2204
+ async buildGenerateFileProperties(input) {
2205
+ const fileName = input.fileName ?? '';
2206
+ const appendContentId = input.appendContentId ?? false;
2207
+ const fileExtension = input.fileExtension ?? '';
2208
+ const baseDirectory = input.baseDirectory ?? '';
2209
+ const layout = input.layout ?? '';
2210
+ const appendDirectory = input.appendDirectory ?? false;
2211
+ const mediaFile = input.mediaFile ?? null;
2212
+ // Validate media file if provided
2213
+ if (mediaFile !== null) {
2214
+ await this.validateMediaExists(mediaFile, 'mediaFile');
2215
+ }
2216
+ const result = {
2217
+ 'File Name': { value: fileName },
2218
+ 'Append Content ID': { value: appendContentId ? 'yes' : 'no' },
2219
+ 'File Extension': { value: fileExtension },
2220
+ 'Base Directory': { value: baseDirectory },
2221
+ 'Formatter': { value: layout },
2222
+ 'Append Directory': { value: appendDirectory ? 'yes' : 'no' },
2223
+ 'Media File': mediaFile !== null ? { value: mediaFile } : {},
2224
+ };
2225
+ return result;
2226
+ }
2227
+ /** Builds properties for CSS Selector with validation. */
2228
+ async buildCssSelectorProperties(input) {
2229
+ if (!input.defaultStylesheet)
2230
+ throw new Error('defaultStylesheet is required for CSS Selector navigation');
2231
+ // Validate default stylesheet exists as a media item
2232
+ await this.validateMediaExists(input.defaultStylesheet, 'defaultStylesheet');
2233
+ const result = {
2234
+ 'default-style-sheet': { value: String(input.defaultStylesheet) },
2235
+ 'language': { value: input.language ?? '' },
2236
+ };
2237
+ if (input.branches && input.branches.length > 0) {
2238
+ for (let i = 0; i < input.branches.length; i++) {
2239
+ const branch = input.branches[i];
2240
+ const n = i + 1;
2241
+ if (!branch.stylesheet)
2242
+ throw new Error(`branches[${i}].stylesheet is required`);
2243
+ // Validate: name OR rootSection, not both
2244
+ if (branch.name && branch.rootSection != null) {
2245
+ throw new Error(`branches[${i}] cannot have both name and rootSection. Use one or the other.`);
2246
+ }
2247
+ // Validate stylesheet media exists
2248
+ await this.validateMediaExists(branch.stylesheet, `branches[${i}].stylesheet`);
2249
+ // Validate section exists if rootSection provided
2250
+ if (branch.rootSection != null) {
2251
+ await this.validateSectionExists(branch.rootSection);
2252
+ }
2253
+ result[`style-sheet-${n}`] = { value: String(branch.stylesheet) };
2254
+ result[`branch-${n}-name`] = { value: branch.name ?? '' };
2255
+ result[`branch-${n}-root`] = { value: branch.rootSection != null ? String(branch.rootSection) : '' };
2256
+ }
2257
+ }
2258
+ return result;
2259
+ }
2260
+ /** Validates that a media item exists by ID. */
2261
+ async validateMediaExists(id, fieldName) {
2262
+ try {
2263
+ await this.httpClient.request({
2264
+ method: 'GET',
2265
+ path: `/media/${id}/smxx`,
2266
+ });
2267
+ }
2268
+ catch {
2269
+ throw new Error(`Invalid ${fieldName}: media item ${id} not found`);
2270
+ }
2271
+ }
2272
+ /** Validates that a section exists by ID. */
2273
+ async validateSectionExists(id) {
2274
+ try {
2275
+ await this.httpClient.request({
2276
+ method: 'GET',
2277
+ path: `/hierarchy/${id}/en`,
2278
+ });
2279
+ }
2280
+ catch {
2281
+ throw new Error(`Invalid rootSection: section ${id} not found`);
2282
+ }
2283
+ }
2284
+ /** Passes properties through without type-specific validation. */
2285
+ buildGenericProperties(input) {
2286
+ const result = {};
2287
+ for (const [key, value] of Object.entries(input)) {
2288
+ // Convert camelCase to the likely API key format (snake_case or kebab-case)
2289
+ const apiKey = key.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase());
2290
+ result[apiKey] = { value: String(value ?? '') };
2291
+ }
2292
+ return result;
2293
+ }
2294
+ /** Resolves a meta data element alias to its raw name. Returns the raw name for the API. Only Plain Text elements are valid. */
2295
+ async resolveMetaDataElementAlias(elementAlias) {
2296
+ const config = await this.httpClient.request({
2297
+ method: 'GET',
2298
+ path: '/config/hierarchy.metaDataContentType',
2299
+ });
2300
+ const contentTypeId = parseInt(config.value, 10);
2301
+ if (!contentTypeId)
2302
+ return elementAlias; // No metadata content type configured — pass through
2303
+ const elements = await this.getContentTypeElements(contentTypeId);
2304
+ const plainTextTypeId = await this.getElementTypeId('Plain Text');
2305
+ const plainTextElements = elements.filter((el) => el.type === plainTextTypeId);
2306
+ const match = plainTextElements.find((el) => (el.alias || el.name).toLowerCase() === elementAlias.toLowerCase());
2307
+ if (!match) {
2308
+ const validNames = plainTextElements.map((el) => `"${el.alias || el.name}"`).join(', ');
2309
+ throw new Error(`Invalid sectionMetaContentTypeElement "${elementAlias}". Valid Plain Text elements are: ${validNames}`);
2310
+ }
2311
+ return match.name;
2312
+ }
2313
+ /** Resolves a raw meta data element name back to its alias for display. */
2314
+ async resolveMetaDataElementName(rawName) {
2315
+ try {
2316
+ const config = await this.httpClient.request({
2317
+ method: 'GET',
2318
+ path: '/config/hierarchy.metaDataContentType',
2319
+ });
2320
+ const contentTypeId = parseInt(config.value, 10);
2321
+ if (!contentTypeId)
2322
+ return rawName;
2323
+ const elements = await this.getContentTypeElements(contentTypeId);
2324
+ const match = elements.find((el) => el.name === rawName);
2325
+ return match ? (match.alias || match.name) : rawName;
2326
+ }
2327
+ catch {
2328
+ return rawName;
2329
+ }
2330
+ }
2331
+ /** Validates that a microsite ID exists. */
2332
+ async validateMicrositeId(id) {
2333
+ const channels = await this.httpClient.request({
2334
+ method: 'GET',
2335
+ path: '/channel',
2336
+ });
2337
+ const validIds = collectMicrositeIds(channels);
2338
+ if (!validIds.has(id)) {
2339
+ const validList = Array.from(validIds).join(', ');
2340
+ throw new Error(`Invalid microSite ID ${id}. Valid microsite IDs are: ${validList || 'none (no microsites configured)'}`);
2341
+ }
2342
+ }
2343
+ }
2344
+ //# sourceMappingURL=navigation-resource.js.map