@edgedev/template-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +332 -0
- package/dist/hydrateValues.d.ts +11 -0
- package/dist/hydrateValues.d.ts.map +1 -0
- package/dist/hydrateValues.js +568 -0
- package/dist/hydrateValues.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +351 -0
- package/dist/index.js.map +1 -0
- package/dist/kvIndexClient.d.ts +49 -0
- package/dist/kvIndexClient.d.ts.map +1 -0
- package/dist/kvIndexClient.js +256 -0
- package/dist/kvIndexClient.js.map +1 -0
- package/dist/templateConfig.d.ts +3 -0
- package/dist/templateConfig.d.ts.map +1 -0
- package/dist/templateConfig.js +23 -0
- package/dist/templateConfig.js.map +1 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/unoSsr.d.ts +21 -0
- package/dist/unoSsr.d.ts.map +1 -0
- package/dist/unoSsr.js +198 -0
- package/dist/unoSsr.js.map +1 -0
- package/package.json +34 -0
- package/src/hydrateValues.ts +753 -0
- package/src/index.ts +487 -0
- package/src/kvIndexClient.ts +374 -0
- package/src/templateConfig.ts +25 -0
- package/src/types.ts +54 -0
- package/src/unoSsr.ts +232 -0
- package/tsconfig.json +17 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
// TODO: ADD AS SEPERATE FILE/ FUNCTION THE QUERYING OF DATA FROM FROM EITHER THE API OR THE KV STORE... BUT IT WOUDLN'T BE CALLED SEPERATELY
|
|
2
|
+
// IT WOULD BE CALLED AS PART OF THE RENDERING PROCESS... IF API OR KV IS SPECIFIED IN THE META OR TEMPLATE CONFIG,
|
|
3
|
+
// THIS, WOULD GET THE VALUES THEN PASS THEM TO THE RENDER FUNCTION BELOW
|
|
4
|
+
// ALSO HAVE HELPER FUNCTIONS THIS USES AND WE CAN USE ELSEWHERE TO DO GET KV DATA AND QUERY KV DATA BASED ON OUR INDEX STRUCTURE
|
|
5
|
+
// ALSO TAKE A LOOK AT OUR BLOCKS WE ARE GETTING... ALSO MAKE SURE THAT THE META WE PASS IN AS A VAR IS AN OVERRIDE OF THE TEMPLATE META...
|
|
6
|
+
// MAKE SURE THE META IN THE TEMPLATE IS BEING USED. MAKE SURE SAME
|
|
7
|
+
|
|
8
|
+
import { ARRAY_BLOCK_RE, parseConfig } from './templateConfig'
|
|
9
|
+
import { hydrateValues, type HydrateValuesOptions } from './hydrateValues'
|
|
10
|
+
import { unoCssFromHtml } from './unoSsr'
|
|
11
|
+
import type { MetaEntry, SchemaMap, TemplateMeta, TemplateValues } from './types'
|
|
12
|
+
|
|
13
|
+
export { hydrateValues }
|
|
14
|
+
export type { HydrateValuesOptions }
|
|
15
|
+
export type {
|
|
16
|
+
SchemaValue,
|
|
17
|
+
SchemaArrayEntry,
|
|
18
|
+
SchemaMap,
|
|
19
|
+
MetaEntry,
|
|
20
|
+
TemplateMeta,
|
|
21
|
+
TemplateValues,
|
|
22
|
+
CollectionDefinition,
|
|
23
|
+
QueryFilter,
|
|
24
|
+
QueryOrder,
|
|
25
|
+
QueryItemsInput,
|
|
26
|
+
} from './types'
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
createKvIndexClient,
|
|
30
|
+
type KvIndexClient,
|
|
31
|
+
type KvIndexClientOptions,
|
|
32
|
+
type KvIndexQueryParams,
|
|
33
|
+
type KVNamespaceLike,
|
|
34
|
+
type KvIndexRecord,
|
|
35
|
+
} from './kvIndexClient'
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
normalizeTheme,
|
|
39
|
+
buildCssVarsBlock,
|
|
40
|
+
} from './unoSsr'
|
|
41
|
+
export { unoCssFromHtml }
|
|
42
|
+
|
|
43
|
+
type Primitive = string | number | boolean | null | undefined
|
|
44
|
+
|
|
45
|
+
const PLACEHOLDER_RE = /(?<!\{)\{\{\s*({[\s\S]*?})\s*\}\}(?!\})/g
|
|
46
|
+
const SUBARRAY_BLOCK_RE = /\{\{\{\s*#subarray(?::([A-Za-z_][A-Za-z0-9_-]*))?\s*(?:({[\s\S]*?}))?\s*\}\}\}([\s\S]*?)\{\{\{\s*\/subarray\s*\}\}\}/g
|
|
47
|
+
const SIMPLE_BLOCK_RE = /\{\{\{\s*#(text|image|textarea|richtext)\s*({[\s\S]*?})\s*\}\}\}/g
|
|
48
|
+
const IF_BLOCK_RE = /\{\{\{\s*#if\s*({[\s\S]*?})\s*\}\}\}([\s\S]*?)(?:\{\{\{\s*#else\s*\}\}\}([\s\S]*?))?\{\{\{\s*\/if\s*\}\}\}/g
|
|
49
|
+
|
|
50
|
+
const getByPath = (obj: unknown, path: string): unknown => {
|
|
51
|
+
if (!path || typeof path !== 'string')
|
|
52
|
+
return obj
|
|
53
|
+
return path.split('.').reduce<unknown>((acc, key) => {
|
|
54
|
+
if (acc && typeof acc === 'object' && key in (acc as Record<string, unknown>))
|
|
55
|
+
return (acc as Record<string, unknown>)[key]
|
|
56
|
+
return undefined
|
|
57
|
+
}, obj)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const escapeHtml = (value: Primitive): string => {
|
|
61
|
+
return String(value ?? '')
|
|
62
|
+
.replace(/&/g, '&')
|
|
63
|
+
.replace(/</g, '<')
|
|
64
|
+
.replace(/>/g, '>')
|
|
65
|
+
.replace(/"/g, '"')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const normalizeNumber = (value: unknown): number | null => {
|
|
69
|
+
if (value == null || value === '')
|
|
70
|
+
return null
|
|
71
|
+
const n = Number(value)
|
|
72
|
+
return Number.isFinite(n) ? n : null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const formatters: Record<string, (value: unknown) => string> = {
|
|
76
|
+
text: v => (v == null ? '' : String(v)),
|
|
77
|
+
textarea: v => (v == null ? '' : String(v)),
|
|
78
|
+
number: (v) => {
|
|
79
|
+
const n = normalizeNumber(v)
|
|
80
|
+
return typeof n === 'number' ? n.toLocaleString('en-US') : (v == null ? '' : String(v))
|
|
81
|
+
},
|
|
82
|
+
integer: (v) => {
|
|
83
|
+
const n = normalizeNumber(v)
|
|
84
|
+
return typeof n === 'number' ? String(Math.trunc(n)) : (v == null ? '' : String(v))
|
|
85
|
+
},
|
|
86
|
+
money: (v) => {
|
|
87
|
+
const n = normalizeNumber(v)
|
|
88
|
+
return typeof n === 'number'
|
|
89
|
+
? n.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 })
|
|
90
|
+
: (v == null ? '' : String(v))
|
|
91
|
+
},
|
|
92
|
+
richtext: v => (v == null ? '' : String(v)),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const getFieldType = (schemaMap: SchemaMap | undefined, fieldKey: string | undefined): string | undefined => {
|
|
96
|
+
if (!schemaMap || !fieldKey)
|
|
97
|
+
return undefined
|
|
98
|
+
|
|
99
|
+
const baseKey = String(fieldKey).split('.')[0]
|
|
100
|
+
|
|
101
|
+
if (Array.isArray(schemaMap)) {
|
|
102
|
+
const hit = schemaMap.find(entry => entry?.field === baseKey)
|
|
103
|
+
return hit?.type ?? hit?.value
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return schemaMap[baseKey]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const applySchemaFormat = (fieldKey: string | undefined, value: unknown, schemaMap: SchemaMap | undefined): string => {
|
|
110
|
+
if (!schemaMap || !fieldKey)
|
|
111
|
+
return value == null ? '' : String(value)
|
|
112
|
+
|
|
113
|
+
const type = getFieldType(schemaMap, fieldKey)
|
|
114
|
+
const formatter = type ? formatters[type] : undefined
|
|
115
|
+
return formatter ? formatter(value) : (value == null ? '' : String(value))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const coerceList = (input: unknown): unknown[] => {
|
|
119
|
+
return Array.isArray(input) ? input : []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sliceWithLimit = (list: unknown[], limit: unknown): unknown[] => {
|
|
123
|
+
const n = Number(limit)
|
|
124
|
+
return Number.isFinite(n) && n > 0 ? list.slice(0, n) : list
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const getCommonList = (source: Record<string, unknown>): unknown => {
|
|
128
|
+
for (const key of ['agents', 'items', 'children']) {
|
|
129
|
+
const candidate = source[key]
|
|
130
|
+
if (Array.isArray(candidate))
|
|
131
|
+
return candidate
|
|
132
|
+
}
|
|
133
|
+
return undefined
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const hasOwn = (obj: Record<string, unknown>, key: string): boolean => {
|
|
137
|
+
return Object.prototype.hasOwnProperty.call(obj, key)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const aliasRegex = (alias: string): RegExp => {
|
|
141
|
+
const esc = alias.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
|
|
142
|
+
return new RegExp(`\\{\\{\\s*${esc}\\.([a-zA-Z0-9_.$]+)\\s*\\}\\}`, 'g')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const renderTemplate = (
|
|
146
|
+
content: string,
|
|
147
|
+
values: TemplateValues = {},
|
|
148
|
+
meta: TemplateMeta = {},
|
|
149
|
+
): string => {
|
|
150
|
+
if (!content || typeof content !== 'string')
|
|
151
|
+
return ''
|
|
152
|
+
const renderSection = (
|
|
153
|
+
template: string,
|
|
154
|
+
data: unknown,
|
|
155
|
+
alias?: string,
|
|
156
|
+
schemaCtx?: SchemaMap,
|
|
157
|
+
): string => {
|
|
158
|
+
if (!template)
|
|
159
|
+
return ''
|
|
160
|
+
|
|
161
|
+
const scopedValues = typeof data === 'object' && data !== null
|
|
162
|
+
? (data as Record<string, unknown>)
|
|
163
|
+
: {}
|
|
164
|
+
|
|
165
|
+
let expanded = template.replace(SUBARRAY_BLOCK_RE, (_match, tagAlias, json, innerTpl) => {
|
|
166
|
+
const cfg = parseConfig(json || '{}') ?? {}
|
|
167
|
+
const fieldRaw = typeof cfg.field === 'string' ? cfg.field.trim() : ''
|
|
168
|
+
let list: unknown
|
|
169
|
+
|
|
170
|
+
if (fieldRaw) {
|
|
171
|
+
if (fieldRaw.startsWith('item.')) {
|
|
172
|
+
list = getByPath(scopedValues, fieldRaw.slice(5))
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
list = scopedValues[fieldRaw] ?? getByPath(scopedValues, fieldRaw)
|
|
176
|
+
if (!Array.isArray(list))
|
|
177
|
+
list = getByPath(values, fieldRaw)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (Array.isArray(cfg.value)) {
|
|
181
|
+
list = cfg.value
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!Array.isArray(list)) {
|
|
185
|
+
const fallback = getCommonList(scopedValues)
|
|
186
|
+
if (Array.isArray(fallback))
|
|
187
|
+
list = fallback
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let normalized = coerceList(list)
|
|
191
|
+
normalized = sliceWithLimit(normalized, cfg.limit)
|
|
192
|
+
|
|
193
|
+
const childAlias = typeof cfg.as === 'string' && cfg.as.trim()
|
|
194
|
+
? cfg.as.trim()
|
|
195
|
+
: (tagAlias?.trim() || undefined)
|
|
196
|
+
|
|
197
|
+
const childSchema = typeof cfg.schema === 'object' ? (cfg.schema as SchemaMap) : schemaCtx
|
|
198
|
+
return normalized.map(entry => renderSection(innerTpl, entry, childAlias, childSchema)).join('')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
expanded = expanded.replace(ARRAY_BLOCK_RE, (_m, json, innerTpl) => {
|
|
202
|
+
const cfg = parseConfig(json || '{}') ?? {}
|
|
203
|
+
const fieldRaw = typeof cfg.field === 'string' ? cfg.field.trim() : ''
|
|
204
|
+
let list: unknown
|
|
205
|
+
|
|
206
|
+
if (fieldRaw) {
|
|
207
|
+
if (fieldRaw.startsWith('item.')) {
|
|
208
|
+
list = getByPath(scopedValues, fieldRaw.slice(5))
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
list = scopedValues[fieldRaw] ?? getByPath(scopedValues, fieldRaw)
|
|
212
|
+
if (!Array.isArray(list))
|
|
213
|
+
list = getByPath(values, fieldRaw)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (Array.isArray(cfg.value)) {
|
|
217
|
+
list = cfg.value
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!Array.isArray(list)) {
|
|
221
|
+
const fallback = getCommonList(scopedValues)
|
|
222
|
+
if (Array.isArray(fallback))
|
|
223
|
+
list = fallback
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let normalized = coerceList(list)
|
|
227
|
+
normalized = sliceWithLimit(normalized, cfg.limit)
|
|
228
|
+
|
|
229
|
+
const childAlias = typeof cfg.as === 'string' && cfg.as.trim() ? cfg.as.trim() : undefined
|
|
230
|
+
const childSchema = typeof cfg.schema === 'object' ? (cfg.schema as SchemaMap) : schemaCtx
|
|
231
|
+
return normalized.map(entry => renderSection(innerTpl, entry, childAlias, childSchema)).join('')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
if (alias && typeof alias === 'string') {
|
|
235
|
+
expanded = expanded.replace(aliasRegex(alias), (_match, path) => {
|
|
236
|
+
const value = getByPath(scopedValues, path)
|
|
237
|
+
const type = getFieldType(schemaCtx, path)
|
|
238
|
+
const formatted = applySchemaFormat(path, value, schemaCtx)
|
|
239
|
+
return type === 'richtext'
|
|
240
|
+
? (formatted ?? '')
|
|
241
|
+
: escapeHtml(formatted)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
expanded = expanded.replace(new RegExp(`\\{\\{\\s*${alias}\\s*\\}\\}`, 'g'), () => {
|
|
245
|
+
return data == null ? '' : escapeHtml(String(data))
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
expanded = expanded.replace(/\{\{\s*item\.([a-zA-Z0-9_.$]+)\s*\}\}/g, (_match, path) => {
|
|
250
|
+
const value = getByPath(scopedValues, path)
|
|
251
|
+
const type = getFieldType(schemaCtx, path)
|
|
252
|
+
const formatted = applySchemaFormat(path, value, schemaCtx)
|
|
253
|
+
if (type === 'richtext')
|
|
254
|
+
return formatted ?? ''
|
|
255
|
+
return formatted === '' ? '' : escapeHtml(formatted)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
expanded = expanded.replace(/\{\{\s*item\s*\}\}/g, () => {
|
|
259
|
+
return data == null ? '' : escapeHtml(String(data))
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
expanded = expanded.replace(IF_BLOCK_RE, (_match, json, trueTpl, falseTpl) => {
|
|
263
|
+
const cfg = parseConfig(json || '{}') ?? {}
|
|
264
|
+
const cond = typeof cfg.cond === 'string' ? cfg.cond.trim() : ''
|
|
265
|
+
const comparison = cond.match(/^item\.([a-zA-Z0-9_.$]+)\s*(==|!=|>|<|>=|<=)\s*['"]?([^'"]+)['"]?$/)
|
|
266
|
+
let result = false
|
|
267
|
+
|
|
268
|
+
if (comparison) {
|
|
269
|
+
const [, path, op, valueRaw] = comparison
|
|
270
|
+
const leftValue = getByPath(scopedValues, path)
|
|
271
|
+
const normalize = (input: unknown): string | number => {
|
|
272
|
+
const num = Number(input)
|
|
273
|
+
if (Number.isFinite(num) && `${input}`.trim() !== '')
|
|
274
|
+
return num
|
|
275
|
+
return String(input ?? '')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const left = normalize(leftValue)
|
|
279
|
+
const right = normalize(valueRaw)
|
|
280
|
+
|
|
281
|
+
switch (op) {
|
|
282
|
+
case '==':
|
|
283
|
+
result = left === right
|
|
284
|
+
break
|
|
285
|
+
case '!=':
|
|
286
|
+
result = left !== right
|
|
287
|
+
break
|
|
288
|
+
case '>':
|
|
289
|
+
result = left > right
|
|
290
|
+
break
|
|
291
|
+
case '<':
|
|
292
|
+
result = left < right
|
|
293
|
+
break
|
|
294
|
+
case '>=':
|
|
295
|
+
result = left >= right
|
|
296
|
+
break
|
|
297
|
+
case '<=':
|
|
298
|
+
result = left <= right
|
|
299
|
+
break
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return result
|
|
304
|
+
? renderSection(trueTpl, data, alias, schemaCtx)
|
|
305
|
+
: renderSection(falseTpl ?? '', data, alias, schemaCtx)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
return expanded
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let output = content
|
|
312
|
+
|
|
313
|
+
output = output.replace(SIMPLE_BLOCK_RE, (_match, kind, json) => {
|
|
314
|
+
const cfg = parseConfig(json) ?? {}
|
|
315
|
+
const field = typeof cfg.field === 'string' ? cfg.field : undefined
|
|
316
|
+
const fallback = cfg.value
|
|
317
|
+
const raw = field && hasOwn(values, field)
|
|
318
|
+
? values[field]
|
|
319
|
+
: fallback
|
|
320
|
+
const value = raw == null ? '' : raw
|
|
321
|
+
if (kind === 'text' || kind === 'textarea')
|
|
322
|
+
return escapeHtml(String(value))
|
|
323
|
+
if (kind === 'richtext')
|
|
324
|
+
return String(value)
|
|
325
|
+
return String(value)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
output = output.replace(ARRAY_BLOCK_RE, (full, json, innerTpl) => {
|
|
329
|
+
const cfg = parseConfig(json) ?? {}
|
|
330
|
+
const field = typeof cfg.field === 'string' ? cfg.field : undefined
|
|
331
|
+
|
|
332
|
+
if (field?.startsWith('item.'))
|
|
333
|
+
return full
|
|
334
|
+
|
|
335
|
+
let list: unknown = field ? values[field] : undefined
|
|
336
|
+
if (!Array.isArray(list) && Array.isArray(cfg.value))
|
|
337
|
+
list = cfg.value
|
|
338
|
+
|
|
339
|
+
let normalized = coerceList(list)
|
|
340
|
+
normalized = sliceWithLimit(normalized, cfg.limit)
|
|
341
|
+
|
|
342
|
+
const alias = typeof cfg.as === 'string' && cfg.as.trim() ? cfg.as.trim() : undefined
|
|
343
|
+
const schemaContext = field && meta[field]?.schema
|
|
344
|
+
? meta[field]?.schema
|
|
345
|
+
: (typeof cfg.schema === 'object' ? (cfg.schema as SchemaMap) : undefined)
|
|
346
|
+
|
|
347
|
+
return normalized.map(entry => renderSection(innerTpl, entry, alias, schemaContext)).join('')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
output = output.replace(PLACEHOLDER_RE, (_match, json) => {
|
|
351
|
+
const cfg = parseConfig(json) ?? {}
|
|
352
|
+
const field = typeof cfg.field === 'string' ? cfg.field : undefined
|
|
353
|
+
const type = typeof cfg.type === 'string' ? cfg.type : undefined
|
|
354
|
+
const raw = field && hasOwn(values, field)
|
|
355
|
+
? values[field]
|
|
356
|
+
: cfg.value
|
|
357
|
+
const value = raw == null ? '' : raw
|
|
358
|
+
|
|
359
|
+
if (type === 'text')
|
|
360
|
+
return escapeHtml(String(value))
|
|
361
|
+
if (type === 'richtext')
|
|
362
|
+
return String(value)
|
|
363
|
+
return String(value)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
output = output.replace(/\{\{\s*loading\s*\}\}/g, 'loading')
|
|
367
|
+
output = output.replace(/\{\{\s*loaded\s*\}\}/g, 'loaded')
|
|
368
|
+
|
|
369
|
+
return output
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface PageRenderBlock {
|
|
373
|
+
name?: string | null
|
|
374
|
+
content: string
|
|
375
|
+
values?: TemplateValues | null
|
|
376
|
+
meta?: TemplateMeta | null
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface PageRenderResultBlock {
|
|
380
|
+
name?: string
|
|
381
|
+
html: string
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export interface PageRenderResult {
|
|
385
|
+
blocks: PageRenderResultBlock[]
|
|
386
|
+
css: string
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export type PageRenderHydrateOptions = Pick<HydrateValuesOptions, 'uniqueKey' | 'clientOptions'>
|
|
390
|
+
|
|
391
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
392
|
+
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const coerceTemplateValues = (input: TemplateValues | null | undefined): TemplateValues | undefined => {
|
|
396
|
+
return isPlainObject(input) ? (input as TemplateValues) : undefined
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const coerceTemplateMeta = (input: TemplateMeta | null | undefined): TemplateMeta | undefined => {
|
|
400
|
+
return isPlainObject(input) ? (input as TemplateMeta) : undefined
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const buildHydrateOptions = (
|
|
404
|
+
input?: PageRenderHydrateOptions | null,
|
|
405
|
+
): PageRenderHydrateOptions => {
|
|
406
|
+
const fallbackFetch = typeof globalThis.fetch === 'function'
|
|
407
|
+
? (globalThis.fetch.bind(globalThis) as HydrateValuesOptions['clientOptions']['fetch'])
|
|
408
|
+
: undefined
|
|
409
|
+
|
|
410
|
+
const safeUniqueKey = typeof input?.uniqueKey === 'string' ? input.uniqueKey : ''
|
|
411
|
+
const providedClientOptions = input?.clientOptions
|
|
412
|
+
|
|
413
|
+
const safeClientOptions = providedClientOptions && typeof providedClientOptions === 'object'
|
|
414
|
+
? {
|
|
415
|
+
...providedClientOptions,
|
|
416
|
+
fetch: providedClientOptions.fetch ?? fallbackFetch,
|
|
417
|
+
}
|
|
418
|
+
: {
|
|
419
|
+
binding: null,
|
|
420
|
+
accountId: '',
|
|
421
|
+
namespaceId: '',
|
|
422
|
+
apiToken: '',
|
|
423
|
+
fetch: fallbackFetch,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!safeClientOptions.fetch)
|
|
427
|
+
throw new Error('pageRender requires a fetch implementation (globalThis.fetch or clientOptions.fetch)')
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
uniqueKey: safeUniqueKey,
|
|
431
|
+
clientOptions: safeClientOptions,
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export const pageRender = async (
|
|
436
|
+
blocks: PageRenderBlock[],
|
|
437
|
+
theme: Record<string, unknown> | null | undefined,
|
|
438
|
+
extraHtml: string | null | undefined = '',
|
|
439
|
+
hydrateOptions?: PageRenderHydrateOptions | null,
|
|
440
|
+
): Promise<PageRenderResult> => {
|
|
441
|
+
const renderedBlocks: PageRenderResultBlock[] = []
|
|
442
|
+
const htmlFragments: string[] = []
|
|
443
|
+
const hydrationConfig = buildHydrateOptions(hydrateOptions)
|
|
444
|
+
|
|
445
|
+
for (const block of blocks) {
|
|
446
|
+
if (!block?.content) {
|
|
447
|
+
const blockName = typeof block?.name === 'string' ? block.name : undefined
|
|
448
|
+
renderedBlocks.push({ name: blockName, html: '' })
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const meta = coerceTemplateMeta(block.meta)
|
|
453
|
+
const initialValues = coerceTemplateValues(block.values)
|
|
454
|
+
const resolvedValues = await hydrateValues({
|
|
455
|
+
content: block.content,
|
|
456
|
+
values: initialValues,
|
|
457
|
+
meta,
|
|
458
|
+
uniqueKey: hydrationConfig.uniqueKey,
|
|
459
|
+
clientOptions: hydrationConfig.clientOptions,
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const html = renderTemplate(
|
|
463
|
+
block.content,
|
|
464
|
+
resolvedValues ?? undefined,
|
|
465
|
+
meta,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
const blockName = typeof block.name === 'string' ? block.name : undefined
|
|
469
|
+
renderedBlocks.push({ name: blockName, html })
|
|
470
|
+
if (html)
|
|
471
|
+
htmlFragments.push(html)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (extraHtml && typeof extraHtml === 'string')
|
|
475
|
+
htmlFragments.push(extraHtml)
|
|
476
|
+
|
|
477
|
+
const combinedHtml = htmlFragments.join('\n')
|
|
478
|
+
const safeTheme = theme && typeof theme === 'object' ? theme : {}
|
|
479
|
+
const { css } = await unoCssFromHtml(combinedHtml, safeTheme)
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
blocks: renderedBlocks,
|
|
483
|
+
css,
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export default renderTemplate
|