@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,682 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContentItem = void 0;
4
+ exports.clearGroupCache = clearGroupCache;
5
+ exports.createContentItem = createContentItem;
6
+ const utils_js_1 = require("../utils.js");
7
+ /** Symbol used to restrict _init() access to the factory function in this module */
8
+ const INIT = Symbol('ContentItem.init');
9
+ /**
10
+ * Reverse-resolves a "listId:itemId" string to the friendly item name.
11
+ */
12
+ async function reverseListValue(value, listId, resolver, language) {
13
+ const match = value.match(/^(\d+):(\d+)$/);
14
+ if (!match || !listId)
15
+ return value;
16
+ const list = await resolver.getList(parseInt(match[1], 10), language);
17
+ const item = list.items.find((i) => i.id === parseInt(match[2], 10));
18
+ return item ? item.name : value;
19
+ }
20
+ /**
21
+ * Reverse-resolves checkbox/multiple-select "listId:id1,id2" to array of names.
22
+ */
23
+ async function reverseCheckboxValue(value, listId, resolver, language) {
24
+ const match = value.match(/^(\d+):(.+)$/);
25
+ if (!match || !listId)
26
+ return [value];
27
+ const list = await resolver.getList(parseInt(match[1], 10), language);
28
+ const ids = match[2].split(',').map((s) => parseInt(s.trim(), 10));
29
+ return ids.map((id) => {
30
+ const item = list.items.find((i) => i.id === id);
31
+ return item ? item.name : String(id);
32
+ });
33
+ }
34
+ /**
35
+ * Reverse-resolves multi-select "listId:id1;listId:id2" to array of names.
36
+ */
37
+ async function reverseMultiSelectValue(value, listId, resolver, language) {
38
+ const parts = value.split(';');
39
+ const names = [];
40
+ for (const part of parts) {
41
+ names.push(await reverseListValue(part.trim(), listId, resolver, language));
42
+ }
43
+ return names;
44
+ }
45
+ /**
46
+ * Reverse-resolves cascading list "listId1:id1, listId2:id2" to array of names.
47
+ */
48
+ async function reverseCascadingValue(value, resolver, language) {
49
+ // Split on comma (with optional space) to get each level
50
+ const parts = value.split(/,\s*/).map((s) => s.trim()).filter(Boolean);
51
+ const names = [];
52
+ for (const part of parts) {
53
+ const match = part.match(/^(\d+):(\d+)$/);
54
+ if (match) {
55
+ try {
56
+ const list = await resolver.getList(parseInt(match[1], 10), language);
57
+ const item = list.items.find((i) => i.id === parseInt(match[2], 10));
58
+ names.push(item ? item.name : part);
59
+ }
60
+ catch (error) {
61
+ (0, utils_js_1.debugWarn)(`Failed to reverse-resolve cascading list value "${part}"`, error);
62
+ names.push(part);
63
+ }
64
+ }
65
+ else {
66
+ names.push(part);
67
+ }
68
+ }
69
+ return names;
70
+ }
71
+ /**
72
+ * Reverse-resolves a keyword selector value to the friendly format.
73
+ * "1:1,1:2,Freetext,1:1&&1:2&&Other" → { or: ["Large", "Small", "Freetext", { and: ["Large", "Small", "Other"] }] }
74
+ */
75
+ async function reverseKeywordValue(value, listId, resolver, language) {
76
+ // Split on commas, but not within && groups
77
+ const orParts = [];
78
+ let current = '';
79
+ for (let i = 0; i < value.length; i++) {
80
+ if (value[i] === ',' && !current.includes('&&')) {
81
+ orParts.push(current);
82
+ current = '';
83
+ }
84
+ else if (value[i] === ',' && current.includes('&&')) {
85
+ // Check if remaining has && — if so this comma is between OR groups
86
+ orParts.push(current);
87
+ current = '';
88
+ }
89
+ else {
90
+ current += value[i];
91
+ }
92
+ }
93
+ if (current)
94
+ orParts.push(current);
95
+ const result = [];
96
+ for (const part of orParts) {
97
+ if (part.includes('&&')) {
98
+ const andItems = part.split('&&');
99
+ const resolved = [];
100
+ for (const item of andItems) {
101
+ const trimmed = item.trim();
102
+ const match = trimmed.match(/^(\d+):(\d+)$/);
103
+ if (match && listId) {
104
+ const list = await resolver.getList(parseInt(match[1], 10), language);
105
+ const found = list.items.find((i) => i.id === parseInt(match[2], 10));
106
+ resolved.push(found ? found.name : trimmed);
107
+ }
108
+ else {
109
+ resolved.push(trimmed);
110
+ }
111
+ }
112
+ result.push({ and: resolved });
113
+ }
114
+ else {
115
+ const trimmed = part.trim();
116
+ const match = trimmed.match(/^(\d+):(\d+)$/);
117
+ if (match && listId) {
118
+ const list = await resolver.getList(parseInt(match[1], 10), language);
119
+ const found = list.items.find((i) => i.id === parseInt(match[2], 10));
120
+ result.push(found ? found.name : trimmed);
121
+ }
122
+ else {
123
+ result.push(trimmed);
124
+ }
125
+ }
126
+ }
127
+ return { or: result };
128
+ }
129
+ /** Per-client cache for group lookup, keyed by HttpClient instance */
130
+ const groupCacheMap = new WeakMap();
131
+ /** Clears the group cache for a specific HttpClient instance */
132
+ function clearGroupCache(httpClient) {
133
+ groupCacheMap.delete(httpClient);
134
+ }
135
+ /**
136
+ * Reverse-resolves a Group Select value to an array of group objects.
137
+ */
138
+ async function reverseGroupSelect(value, httpClient) {
139
+ if (!value)
140
+ return [];
141
+ const ids = value.split(',').map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
142
+ if (ids.length === 0)
143
+ return [];
144
+ // Fetch and cache group tree per client
145
+ const cached = groupCacheMap.get(httpClient);
146
+ let groupData = cached && Date.now() < cached.expiresAt && cached.epoch >= (0, utils_js_1.getCacheEpoch)() ? cached.data : null;
147
+ if (!groupData) {
148
+ try {
149
+ const groups = await httpClient.request({
150
+ method: 'GET',
151
+ path: '/group/topLevelGroups',
152
+ });
153
+ groupData = (0, utils_js_1.flattenGroups)(groups);
154
+ groupCacheMap.set(httpClient, { data: groupData, expiresAt: Date.now() + utils_js_1.DEFAULT_CACHE_TTL, epoch: (0, utils_js_1.getCacheEpoch)() });
155
+ }
156
+ catch (error) {
157
+ (0, utils_js_1.debugWarn)('Failed to resolve group names for Group Select field', error);
158
+ return value;
159
+ }
160
+ }
161
+ return ids.map((id) => ({
162
+ id,
163
+ name: groupData.get(id) ?? `Group ${id}`,
164
+ selected: true,
165
+ }));
166
+ }
167
+ /**
168
+ * Reverse-resolves a Content Owner user ID to a user object.
169
+ */
170
+ async function reverseContentOwner(value, httpClient) {
171
+ const userId = parseInt(value, 10);
172
+ if (isNaN(userId) || userId === 0)
173
+ return value;
174
+ try {
175
+ const user = await httpClient.request({
176
+ method: 'GET',
177
+ path: `/user/${userId}`,
178
+ });
179
+ return {
180
+ id: user.id,
181
+ type: utils_js_1.AUTH_LEVEL_MAP[user.authLevel ?? 2] ?? `unknown (${user.authLevel})`,
182
+ username: user.username,
183
+ firstName: user.firstName,
184
+ lastName: user.lastName,
185
+ emailAddress: user.emailAddress,
186
+ };
187
+ }
188
+ catch (error) {
189
+ (0, utils_js_1.debugWarn)(`Failed to resolve Content Owner user ${value}`, error);
190
+ return value;
191
+ }
192
+ }
193
+ /**
194
+ * Reverse-resolves a T4 SS tag to a friendly object.
195
+ */
196
+ async function reverseSslLink(value, httpClient, language, sectionId, contentId) {
197
+ const match = value.match(/sslink_id="(\d+)"/);
198
+ if (!match)
199
+ return value;
200
+ const sslId = parseInt(match[1], 10);
201
+ try {
202
+ const ssl = await httpClient.request({
203
+ method: 'GET',
204
+ path: `/ssl/${sslId}/${language}/${sectionId}/${contentId}`,
205
+ });
206
+ const path = (ssl.path || '').replace(/&raquo;/g, '»');
207
+ const result = {
208
+ sectionId: ssl.toSection,
209
+ linkText: ssl.linkText,
210
+ path,
211
+ };
212
+ if (ssl.toContent) {
213
+ result.contentId = ssl.toContent;
214
+ }
215
+ return result;
216
+ }
217
+ catch (error) {
218
+ (0, utils_js_1.debugWarn)(`Failed to reverse-resolve SS link (id: ${sslId})`, error);
219
+ return value;
220
+ }
221
+ }
222
+ /** Regex to match T4 SS link tags in HTML content */
223
+ const T4_SSLINK_REGEX = /<t4\s+[^>]*sslink_id="(\d+)"[^>]*\/?\s*>/gi;
224
+ /**
225
+ * Resolves T4 SS link tags within an HTML string to friendly anchor elements.
226
+ * `<t4 type="sslink" sslink_id="39"/>` → `<a href="#" data-t4-sslink="39" data-section-id="233" ...>linkText</a>`
227
+ */
228
+ async function resolveHtmlSsLinks(html, httpClient, language, sectionId, contentId) {
229
+ // Find all sslink tags
230
+ const matches = [];
231
+ let match;
232
+ const regex = new RegExp(T4_SSLINK_REGEX.source, T4_SSLINK_REGEX.flags);
233
+ while ((match = regex.exec(html)) !== null) {
234
+ matches.push({ full: match[0], sslId: parseInt(match[1], 10) });
235
+ }
236
+ if (matches.length === 0)
237
+ return html;
238
+ // Resolve all SS links in parallel
239
+ const resolved = await Promise.all(matches.map(async ({ full, sslId }) => {
240
+ try {
241
+ const ssl = await httpClient.request({
242
+ method: 'GET',
243
+ path: `/ssl/${sslId}/${language}/${sectionId}/${contentId}`,
244
+ });
245
+ let attrs = `href="#" data-t4-sslink="${sslId}" data-section-id="${ssl.toSection}"`;
246
+ if (ssl.toContent) {
247
+ attrs += ` data-content-id="${ssl.toContent}"`;
248
+ }
249
+ attrs += ` data-linktext="${ssl.linkText || ''}"`;
250
+ const displayText = ssl.linkText || 'link';
251
+ return { original: full, replacement: `<a ${attrs}>${displayText}</a>` };
252
+ }
253
+ catch (error) {
254
+ (0, utils_js_1.debugWarn)(`Failed to resolve SS link in HTML (id: ${sslId})`, error);
255
+ return { original: full, replacement: full };
256
+ }
257
+ }));
258
+ // Apply replacements
259
+ let result = html;
260
+ for (const { original, replacement } of resolved) {
261
+ result = result.replace(original, replacement);
262
+ }
263
+ return result;
264
+ }
265
+ /**
266
+ * Resolves a raw element value to its friendly form based on element type.
267
+ */
268
+ async function resolveFriendlyValue(value, typeName, listId, resolver, language, httpClient, sectionId, contentId) {
269
+ if (!resolver || value == null || typeof value !== 'string')
270
+ return value;
271
+ switch (typeName) {
272
+ case 'Select Box':
273
+ case 'Radio Button':
274
+ return reverseListValue(value, listId, resolver, language);
275
+ case 'Check Box':
276
+ case 'Multiple Select':
277
+ return reverseCheckboxValue(value, listId, resolver, language);
278
+ case 'Multi-select List':
279
+ return reverseMultiSelectValue(value, listId, resolver, language);
280
+ case 'Cascading List':
281
+ return reverseCascadingValue(value, resolver, language);
282
+ case 'Keyword Selector':
283
+ return reverseKeywordValue(value, listId, resolver, language);
284
+ case 'Section/Content Link':
285
+ if (httpClient && sectionId !== undefined && contentId !== undefined && value.includes('sslink_id')) {
286
+ return reverseSslLink(value, httpClient, language, sectionId, contentId);
287
+ }
288
+ return value;
289
+ default:
290
+ return value;
291
+ }
292
+ }
293
+ /**
294
+ * Parses raw elements into friendly fields with reverse-resolved list values.
295
+ */
296
+ async function parseElements(raw, templateElements, resolver, language, typeRegistry, httpClient, sectionId, contentId, version) {
297
+ const entries = [];
298
+ const keyMap = new Map();
299
+ for (const [rawKey, value] of Object.entries(raw)) {
300
+ const parsed = (0, utils_js_1.parseElementKey)(rawKey);
301
+ if (parsed) {
302
+ const templateEl = templateElements?.find((el) => el.name === parsed.name);
303
+ const friendlyName = templateEl?.alias || parsed.name;
304
+ const sortOrder = templateEl?.sequence ?? parsed.elementId;
305
+ let friendlyValue = value;
306
+ if (templateEl && typeRegistry) {
307
+ const typeName = await typeRegistry.getNameById(parsed.type);
308
+ const isListType = await typeRegistry.isListType(parsed.type);
309
+ if (isListType && typeof value === 'string') {
310
+ friendlyValue = await resolveFriendlyValue(value, typeName, templateEl.listId, resolver, language, httpClient, sectionId, contentId);
311
+ }
312
+ else if (typeName === 'Date' && (typeof value === 'number' || typeof value === 'string')) {
313
+ const ts = typeof value === 'number' ? value : parseInt(value, 10);
314
+ friendlyValue = ts ? new Date(ts) : null;
315
+ }
316
+ else if (typeName === 'HTML' && typeof value === 'string' && value.includes('sslink_id') && httpClient && sectionId !== undefined && contentId !== undefined) {
317
+ friendlyValue = await resolveHtmlSsLinks(value, httpClient, language, sectionId, contentId);
318
+ }
319
+ else if (typeName === 'Section/Content Link' && typeof value === 'string' && value.includes('sslink_id') && httpClient && sectionId !== undefined && contentId !== undefined) {
320
+ friendlyValue = await reverseSslLink(value, httpClient, language, sectionId, contentId);
321
+ }
322
+ else if (typeName === 'Content Owner' && typeof value === 'string' && httpClient) {
323
+ friendlyValue = await reverseContentOwner(value, httpClient);
324
+ }
325
+ else if (typeName === 'Group Select' && typeof value === 'string' && httpClient) {
326
+ friendlyValue = await reverseGroupSelect(value, httpClient);
327
+ }
328
+ else if (typeName === 'Repeater' && Array.isArray(value)) {
329
+ // Recursively resolve repeater items
330
+ const repeaterElements = templateEl.contentTypeElementConfiguration?.contentTypeDTO?.contentTypeElements ?? null;
331
+ const resolvedItems = [];
332
+ for (const item of value) {
333
+ const repeaterItem = item;
334
+ if (repeaterItem.repeaterContent?.elements) {
335
+ const repeaterParsed = await parseElements(repeaterItem.repeaterContent.elements, repeaterElements, resolver, language, typeRegistry, httpClient, sectionId, repeaterItem.repeaterId, version);
336
+ resolvedItems.push({
337
+ name: repeaterItem.repeaterContent.name,
338
+ fields: repeaterParsed.fields,
339
+ });
340
+ }
341
+ else {
342
+ resolvedItems.push(item);
343
+ }
344
+ }
345
+ friendlyValue = resolvedItems;
346
+ }
347
+ else if (typeName === 'Media' && typeof value === 'string' && value.trim() && httpClient) {
348
+ const mediaId = parseInt(value, 10);
349
+ if (!isNaN(mediaId)) {
350
+ try {
351
+ const media = await httpClient.request({
352
+ method: 'GET',
353
+ path: `/media/${mediaId}/${language}`,
354
+ });
355
+ friendlyValue = {
356
+ id: media.id,
357
+ name: media.name ?? '',
358
+ filename: media.fileName ?? null,
359
+ description: media.description ?? '',
360
+ mediaType: media.typeName ?? null,
361
+ lastModified: media.elements?.['Media#4:4']?.lastModified
362
+ ? new Date(media.elements['Media#4:4'].lastModified)
363
+ : null,
364
+ downloadLink: media.mediaURL ?? null,
365
+ path: media.mediaPath
366
+ ? media.mediaPath.replace(/&raquo;/g, '»').trim()
367
+ : null,
368
+ fileSize: (0, utils_js_1.formatFileSize)(media.mediaSize ?? 0),
369
+ };
370
+ }
371
+ catch (error) {
372
+ (0, utils_js_1.debugWarn)(`Failed to resolve Media element (id: ${mediaId})`, error);
373
+ friendlyValue = mediaId;
374
+ }
375
+ }
376
+ }
377
+ else if ((typeName === 'File' || typeName === 'Image') && value && typeof value === 'object' && 'fileSize' in value) {
378
+ const fileObj = value;
379
+ const enriched = {
380
+ filename: fileObj.preferredFilename ?? null,
381
+ fileSize: (0, utils_js_1.formatFileSize)(Number(fileObj.fileSize) || 0),
382
+ };
383
+ // Fetch download link if we have the context and the file exists
384
+ if (httpClient && contentId && version && (fileObj.existingFile || fileObj.code)) {
385
+ try {
386
+ const download = await httpClient.request({
387
+ method: 'GET',
388
+ path: `/download/file/${contentId}/${language}/${version}/${parsed.elementId}`,
389
+ });
390
+ enriched.downloadLink = download.fileLocation;
391
+ }
392
+ catch (error) {
393
+ (0, utils_js_1.debugWarn)(`Failed to fetch download link for file element ${parsed.elementId}`, error);
394
+ }
395
+ }
396
+ friendlyValue = enriched;
397
+ }
398
+ }
399
+ entries.push({ friendlyName, rawKey, sequence: sortOrder, value: friendlyValue });
400
+ keyMap.set(friendlyName.toLowerCase(), rawKey);
401
+ }
402
+ else {
403
+ entries.push({ friendlyName: rawKey, rawKey, sequence: Infinity, value });
404
+ keyMap.set(rawKey.toLowerCase(), rawKey);
405
+ }
406
+ }
407
+ entries.sort((a, b) => a.sequence - b.sequence);
408
+ const fields = {};
409
+ for (const entry of entries) {
410
+ fields[entry.friendlyName] = entry.value;
411
+ }
412
+ return { fields, keyMap };
413
+ }
414
+ /**
415
+ * A mutable content item that exposes read-only metadata and mutable fields.
416
+ * Fields use friendly names and human-readable list values.
417
+ * Call save() to persist field changes back to the server.
418
+ *
419
+ * When returned from `content.list()`, this is a summary object — only metadata
420
+ * properties are populated. The `fields` property is not present. Use
421
+ * `content.get(id)` to retrieve a full item with resolved fields.
422
+ */
423
+ class ContentItem {
424
+ get status() { return this._status; }
425
+ set status(value) {
426
+ this._status = value;
427
+ this._statusDirty = true;
428
+ }
429
+ constructor(data, httpClient, sectionId, resolver, templateElements, typeRegistry) {
430
+ this._statusDirty = false;
431
+ this._dirtyFields = new Set();
432
+ this.id = data.id;
433
+ this.name = data.name;
434
+ this.contentTypeID = data.contentTypeID;
435
+ this.language = data.language;
436
+ this._status = (0, utils_js_1.mapStatus)(data.status);
437
+ this.version = data.version;
438
+ this.lastModified = data.lastModified ? new Date(data.lastModified) : null;
439
+ this.publishDate = data.publishDate ? new Date(data.publishDate) : null;
440
+ this.expiryDate = data.expiryDate ? new Date(data.expiryDate) : null;
441
+ this.reviewDate = data.reviewDate ? new Date(data.reviewDate) : null;
442
+ this.archiveSection = data.archiveSection ?? null;
443
+ this._httpClient = httpClient;
444
+ this._sectionId = sectionId;
445
+ this._resolver = resolver ?? null;
446
+ this._templateElements = templateElements ?? null;
447
+ this._typeRegistry = typeRegistry ?? null;
448
+ this._rawDTO = data;
449
+ // Fields will be populated async via _init()
450
+ this.fields = {};
451
+ this._keyMap = new Map();
452
+ // Make internal properties non-enumerable
453
+ Object.defineProperty(this, '_httpClient', { value: httpClient, enumerable: false, writable: false });
454
+ Object.defineProperty(this, '_sectionId', { value: sectionId, enumerable: false, writable: false });
455
+ Object.defineProperty(this, '_resolver', { value: resolver ?? null, enumerable: false, writable: false });
456
+ Object.defineProperty(this, '_templateElements', { value: templateElements ?? null, enumerable: false, writable: false });
457
+ Object.defineProperty(this, '_typeRegistry', { value: typeRegistry ?? null, enumerable: false, writable: false });
458
+ Object.defineProperty(this, '_rawDTO', { value: data, enumerable: false, writable: true });
459
+ Object.defineProperty(this, '_status', { value: (0, utils_js_1.mapStatus)(data.status), enumerable: false, writable: true });
460
+ Object.defineProperty(this, '_statusDirty', { value: false, enumerable: false, writable: true });
461
+ Object.defineProperty(this, '_keyMap', { value: new Map(), enumerable: false, writable: true });
462
+ Object.defineProperty(this, '_dirtyFields', { value: new Set(), enumerable: false, writable: true });
463
+ }
464
+ /** Includes getter-based properties (status) in JSON serialisation. */
465
+ toJSON() {
466
+ const obj = {};
467
+ for (const key of Object.keys(this)) {
468
+ obj[key] = this[key];
469
+ }
470
+ obj.status = this.status;
471
+ return obj;
472
+ }
473
+ /** @internal Async initialization — resolves friendly field values. Only callable via the factory. */
474
+ async [INIT](data) {
475
+ // List items have no template elements — they're summary objects without fields.
476
+ // Remove the fields property entirely so it's clear this is metadata only.
477
+ if (!this._templateElements) {
478
+ delete this.fields;
479
+ return;
480
+ }
481
+ const parsed = await parseElements(data.elements ?? {}, this._templateElements, this._resolver, this.language, this._typeRegistry, this._httpClient, this._sectionId, this.id, String(data.version));
482
+ this.fields = parsed.fields;
483
+ this._keyMap = parsed.keyMap;
484
+ this._dirtyFields = new Set();
485
+ // Wrap fields in a Proxy to track which fields the developer modifies
486
+ this.fields = new Proxy(this.fields, {
487
+ set: (target, prop, value) => {
488
+ if (typeof prop === 'string') {
489
+ this._dirtyFields.add(prop);
490
+ }
491
+ target[prop] = value;
492
+ return true;
493
+ },
494
+ });
495
+ }
496
+ /**
497
+ * Persists field and property changes back to the server.
498
+ *
499
+ * Status defaults to `'pending'` on save, matching T4's approval workflow
500
+ * where edits go back through review. To preserve the current status, set
501
+ * `item.status = 'approved'` before saving, or use `item.approve()` to
502
+ * save and approve in one step.
503
+ */
504
+ async save() {
505
+ // Start from the original raw elements (correct API format)
506
+ const rawElements = { ...this._rawDTO.elements };
507
+ // Re-resolve dirty fields through the ElementResolver so friendly values
508
+ // (list names, Date objects, SS link objects, etc.) are converted back
509
+ // to the format the API expects.
510
+ const context = {
511
+ fromSectionId: this._sectionId,
512
+ fromContentId: this.id,
513
+ };
514
+ for (const friendlyName of this._dirtyFields) {
515
+ const rawKey = this._keyMap.get(friendlyName.toLowerCase());
516
+ if (!(friendlyName in this.fields))
517
+ continue;
518
+ if (!rawKey) {
519
+ const validNames = Array.from(this._keyMap.keys())
520
+ .filter((k) => k !== 'name')
521
+ .map((k) => `"${k}"`)
522
+ .join(', ');
523
+ throw new Error(`Unknown field "${friendlyName}" on this content type. Valid fields are: ${validNames}`);
524
+ }
525
+ const value = this.fields[friendlyName];
526
+ // Find the matching template element for this field
527
+ const templateEl = this._templateElements?.find((el) => (el.alias || el.name).toLowerCase() === friendlyName.toLowerCase());
528
+ // Validate maxSize for string values
529
+ if (templateEl?.maxSize && typeof value === 'string' && value.length > templateEl.maxSize) {
530
+ throw new Error(`Field "${friendlyName}" exceeds max size: ${value.length} characters (max ${templateEl.maxSize})`);
531
+ }
532
+ let resolved;
533
+ if (templateEl && this._resolver) {
534
+ resolved = await this._resolver.resolveValue(value, templateEl, this.language, this._templateElements ?? undefined, context);
535
+ }
536
+ else {
537
+ resolved = value;
538
+ }
539
+ // Validate maxSize on the resolved value (skip repeaters — they're arrays)
540
+ if (templateEl?.maxSize && !Array.isArray(resolved)) {
541
+ const resolvedStr = String(resolved ?? '');
542
+ if (resolvedStr.length > templateEl.maxSize) {
543
+ throw new Error(`Field "${friendlyName}" exceeds max size: ${resolvedStr.length} characters (max ${templateEl.maxSize})`);
544
+ }
545
+ }
546
+ rawElements[rawKey] = resolved;
547
+ }
548
+ // Update the Name element to match the current name
549
+ if (this._templateElements) {
550
+ const nameEl = this._templateElements.find((el) => el.name.toLowerCase() === 'name');
551
+ if (nameEl) {
552
+ rawElements[`${nameEl.name}#${nameEl.id}:${nameEl.type}`] = this.name;
553
+ }
554
+ }
555
+ const statusCode = this._statusDirty ? (utils_js_1.STATUS_CODES[this._status] ?? 1) : 1;
556
+ const response = await this._httpClient.request({
557
+ method: 'POST',
558
+ path: `/content/${this._sectionId}/${this.id}/${this.language}`,
559
+ body: {
560
+ id: this.id,
561
+ contentTypeID: this.contentTypeID,
562
+ name: this.name,
563
+ language: this.language,
564
+ status: statusCode,
565
+ elements: rawElements,
566
+ channels: this._rawDTO.channels,
567
+ canPublishNow: true,
568
+ canSaveAndApprove: true,
569
+ publishDate: this.publishDate ? this.publishDate.getTime() : null,
570
+ expiryDate: this.expiryDate ? this.expiryDate.getTime() : null,
571
+ reviewDate: this.reviewDate ? this.reviewDate.getTime() : null,
572
+ archiveSection: this.archiveSection ?? null,
573
+ owner: this._rawDTO.owner ?? { id: 0, type: 'USER' },
574
+ excludedMirrorSectionIds: [],
575
+ sectionIDs: [this._sectionId],
576
+ version: this._rawDTO.version,
577
+ lastModified: this._rawDTO.lastModified,
578
+ },
579
+ });
580
+ this.id = response.id;
581
+ this.name = response.name;
582
+ this.contentTypeID = response.contentTypeID;
583
+ this.language = response.language;
584
+ this._status = (0, utils_js_1.mapStatus)(response.status);
585
+ this._statusDirty = false;
586
+ this.version = response.version;
587
+ this.lastModified = response.lastModified ? new Date(response.lastModified) : null;
588
+ this.publishDate = response.publishDate ? new Date(response.publishDate) : null;
589
+ this.expiryDate = response.expiryDate ? new Date(response.expiryDate) : null;
590
+ this.reviewDate = response.reviewDate ? new Date(response.reviewDate) : null;
591
+ this.archiveSection = response.archiveSection ?? null;
592
+ this._rawDTO = response;
593
+ const parsed = await parseElements(response.elements ?? {}, this._templateElements, this._resolver, this.language, this._typeRegistry, this._httpClient, this._sectionId, response.id, String(response.version));
594
+ this.fields = parsed.fields;
595
+ this._keyMap = parsed.keyMap;
596
+ this._dirtyFields = new Set();
597
+ // Re-wrap fields in Proxy for dirty tracking
598
+ this.fields = new Proxy(this.fields, {
599
+ set: (target, prop, value) => {
600
+ if (typeof prop === 'string') {
601
+ this._dirtyFields.add(prop);
602
+ }
603
+ target[prop] = value;
604
+ return true;
605
+ },
606
+ });
607
+ }
608
+ /**
609
+ * Saves the content item with an approved status.
610
+ * Shorthand for setting `status = 'approved'` and calling `save()`.
611
+ * Throws if the content is already approved.
612
+ */
613
+ async approve() {
614
+ if (this._status === 'approved' && !this._statusDirty) {
615
+ throw new Error(`Content item ${this.id} is already approved.`);
616
+ }
617
+ this.status = 'approved';
618
+ await this.save();
619
+ }
620
+ /**
621
+ * Duplicates this content item, optionally into a different section.
622
+ *
623
+ * - If `sectionId` is omitted, duplicates within the same section and automatically
624
+ * appends `(n)` to the name to avoid collisions.
625
+ * - If `sectionId` is provided, duplicates into that section with the original name.
626
+ */
627
+ async duplicate(sectionId) {
628
+ const destination = sectionId ?? this._sectionId;
629
+ const sameSection = destination === this._sectionId;
630
+ let name = this.name;
631
+ if (sameSection) {
632
+ // Fetch all content in this section to determine the next (n) suffix
633
+ const response = await this._httpClient.request({
634
+ method: 'GET',
635
+ path: `/hierarchy/${this._sectionId}/${this.language}/contents?showAll=false&removeNonTranslated=false`,
636
+ });
637
+ const existingNames = new Set((response.children ?? []).map((child) => child.content.name));
638
+ // Strip any existing trailing " (n)" from the base name
639
+ const baseName = name.replace(/\s*\(\d+\)$/, '');
640
+ let counter = 1;
641
+ let candidate = `${baseName} (${counter})`;
642
+ while (existingNames.has(candidate)) {
643
+ counter++;
644
+ candidate = `${baseName} (${counter})`;
645
+ }
646
+ name = candidate;
647
+ }
648
+ await this._httpClient.request({
649
+ method: 'COPY',
650
+ path: `/content/${this.language}`,
651
+ body: {
652
+ source: this._sectionId,
653
+ destination,
654
+ contents: {
655
+ [this.id]: [{ language: this.language, name }],
656
+ },
657
+ },
658
+ });
659
+ }
660
+ /**
661
+ * Moves this content item to a different section.
662
+ */
663
+ async move(sectionId) {
664
+ await this._httpClient.request({
665
+ method: 'MOVE',
666
+ path: `/content/${this.language}`,
667
+ body: {
668
+ source: this._sectionId,
669
+ destination: sectionId,
670
+ contents: { [this.id]: [] },
671
+ },
672
+ });
673
+ }
674
+ }
675
+ exports.ContentItem = ContentItem;
676
+ /** Factory function to create a ContentItem with async field resolution */
677
+ async function createContentItem(data, httpClient, sectionId, resolver, templateElements, typeRegistry) {
678
+ const item = new ContentItem(data, httpClient, sectionId, resolver, templateElements, typeRegistry);
679
+ await item[INIT](data);
680
+ return item;
681
+ }
682
+ //# sourceMappingURL=content-item.js.map