@teleporthq/teleport-plugin-html-base-component 0.42.0-alpha.0 → 0.42.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/__tests__/index.ts +5 -3
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +39 -16
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/node-handlers.d.ts +9 -2
- package/dist/cjs/node-handlers.d.ts.map +1 -1
- package/dist/cjs/node-handlers.js +632 -177
- package/dist/cjs/node-handlers.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +40 -17
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/node-handlers.d.ts +9 -2
- package/dist/esm/node-handlers.d.ts.map +1 -1
- package/dist/esm/node-handlers.js +632 -177
- package/dist/esm/node-handlers.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/index.ts +54 -19
- package/src/node-handlers.ts +889 -203
package/src/node-handlers.ts
CHANGED
|
@@ -21,17 +21,55 @@ import {
|
|
|
21
21
|
ComponentStructure,
|
|
22
22
|
UIDLComponentOutputOptions,
|
|
23
23
|
UIDLElement,
|
|
24
|
+
ElementsLookup,
|
|
25
|
+
UIDLConditionalNode,
|
|
26
|
+
PropDefaultValueTypes,
|
|
27
|
+
UIDLCMSListRepeaterNode,
|
|
24
28
|
} from '@teleporthq/teleport-types'
|
|
25
29
|
import { join, relative } from 'path'
|
|
26
|
-
import { HASTBuilders, HASTUtils } from '@teleporthq/teleport-plugin-common'
|
|
27
|
-
import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
|
|
30
|
+
import { HASTBuilders, HASTUtils, ASTUtils } from '@teleporthq/teleport-plugin-common'
|
|
31
|
+
import { GenericUtils, StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
|
|
28
32
|
import { staticNode } from '@teleporthq/teleport-uidl-builders'
|
|
29
33
|
import { createCSSPlugin } from '@teleporthq/teleport-plugin-css'
|
|
34
|
+
import { generateUniqueKeys, createNodesLookup } from '@teleporthq/teleport-uidl-resolver'
|
|
30
35
|
import { DEFAULT_COMPONENT_CHUNK_NAME } from './constants'
|
|
31
36
|
|
|
37
|
+
const isValidURL = (url: string) => {
|
|
38
|
+
try {
|
|
39
|
+
/* tslint:disable:no-unused-expression */
|
|
40
|
+
new URL(url)
|
|
41
|
+
return true
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const addNodeToLookup = (
|
|
48
|
+
key: string,
|
|
49
|
+
tag: HastNode | HastText | Array<HastNode | HastText>,
|
|
50
|
+
nodesLoookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>
|
|
51
|
+
) => {
|
|
52
|
+
// In html code-generation we combine the nodes of the component that is being consumed with the current component.
|
|
53
|
+
// As html can't load the component at runtime like react or any other frameworks. So, we merge the component as a standalone
|
|
54
|
+
// component in the current component.
|
|
55
|
+
const currentLookup = nodesLoookup[key]
|
|
56
|
+
if (currentLookup) {
|
|
57
|
+
if (Array.isArray(currentLookup)) {
|
|
58
|
+
Array.isArray(tag) ? currentLookup.push(...tag) : currentLookup.push(tag)
|
|
59
|
+
} else {
|
|
60
|
+
nodesLoookup[key] = Array.isArray(tag) ? [currentLookup, ...tag] : [currentLookup, tag]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
nodesLoookup[key] = tag
|
|
67
|
+
}
|
|
68
|
+
|
|
32
69
|
type NodeToHTML<NodeType, ReturnType> = (
|
|
33
70
|
node: NodeType,
|
|
34
|
-
|
|
71
|
+
componentName: string,
|
|
72
|
+
nodesLookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>,
|
|
35
73
|
propDefinitions: Record<string, UIDLPropDefinition>,
|
|
36
74
|
stateDefinitions: Record<string, UIDLStateDefinition>,
|
|
37
75
|
subComponentOptions: {
|
|
@@ -43,16 +81,28 @@ type NodeToHTML<NodeType, ReturnType> = (
|
|
|
43
81
|
dependencies: Record<string, UIDLDependency>
|
|
44
82
|
options: GeneratorOptions
|
|
45
83
|
outputOptions: UIDLComponentOutputOptions
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* This param is just to be able to handle CMS array mappers/Repeater nodes. A bit hacky, better support should be implemented
|
|
87
|
+
*/
|
|
88
|
+
resolvedExpressions?: {
|
|
89
|
+
expressions: Record<string, UIDLPropDefinition>
|
|
90
|
+
currentIndex: number
|
|
46
91
|
}
|
|
47
92
|
) => ReturnType
|
|
48
93
|
|
|
49
|
-
export const
|
|
94
|
+
export const generateHtmlSyntax: NodeToHTML<
|
|
95
|
+
UIDLNode,
|
|
96
|
+
Promise<HastNode | HastText | Array<HastNode | HastText>>
|
|
97
|
+
> = async (
|
|
50
98
|
node,
|
|
51
|
-
|
|
99
|
+
compName,
|
|
100
|
+
nodesLookup,
|
|
52
101
|
propDefinitions,
|
|
53
102
|
stateDefinitions,
|
|
54
103
|
subComponentOptions,
|
|
55
|
-
structure
|
|
104
|
+
structure,
|
|
105
|
+
resolvedExpressions
|
|
56
106
|
) => {
|
|
57
107
|
switch (node.type) {
|
|
58
108
|
case 'inject':
|
|
@@ -66,25 +116,180 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
|
|
|
66
116
|
return HASTBuilders.createHTMLNode(node.type)
|
|
67
117
|
|
|
68
118
|
case 'element':
|
|
69
|
-
|
|
119
|
+
const elementNode = await generateElementNode(
|
|
70
120
|
node,
|
|
71
|
-
|
|
121
|
+
compName,
|
|
122
|
+
nodesLookup,
|
|
72
123
|
propDefinitions,
|
|
73
124
|
stateDefinitions,
|
|
74
125
|
subComponentOptions,
|
|
75
|
-
structure
|
|
126
|
+
structure,
|
|
127
|
+
resolvedExpressions
|
|
76
128
|
)
|
|
129
|
+
return elementNode
|
|
77
130
|
|
|
78
131
|
case 'dynamic':
|
|
79
|
-
|
|
132
|
+
const dynamicNode = await generateDynamicNode(
|
|
80
133
|
node,
|
|
81
|
-
|
|
134
|
+
compName,
|
|
135
|
+
nodesLookup,
|
|
136
|
+
propDefinitions,
|
|
137
|
+
stateDefinitions,
|
|
138
|
+
subComponentOptions,
|
|
139
|
+
structure,
|
|
140
|
+
resolvedExpressions
|
|
141
|
+
)
|
|
142
|
+
return dynamicNode
|
|
143
|
+
|
|
144
|
+
case 'conditional':
|
|
145
|
+
const conditionalNodeComment = HASTBuilders.createTextNode('')
|
|
146
|
+
const {
|
|
147
|
+
value: staticValue,
|
|
148
|
+
reference,
|
|
149
|
+
condition: { conditions, matchingCriteria },
|
|
150
|
+
} = node.content
|
|
151
|
+
|
|
152
|
+
if (reference.type !== 'dynamic') {
|
|
153
|
+
return conditionalNodeComment
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const {
|
|
157
|
+
content: { referenceType, id, refPath = [] },
|
|
158
|
+
} = reference
|
|
159
|
+
|
|
160
|
+
switch (referenceType) {
|
|
161
|
+
case 'prop': {
|
|
162
|
+
const usedProp = propDefinitions[id]
|
|
163
|
+
if (usedProp === undefined || usedProp.defaultValue === undefined) {
|
|
164
|
+
return conditionalNodeComment
|
|
165
|
+
}
|
|
166
|
+
let defaultValue = usedProp.defaultValue
|
|
167
|
+
for (const path of refPath) {
|
|
168
|
+
defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// If defaultValue is undefined or null after path traversal, use original default
|
|
172
|
+
defaultValue = defaultValue ?? usedProp.defaultValue
|
|
173
|
+
|
|
174
|
+
// Since we know the operand and the default value from the prop.
|
|
175
|
+
// We can try building the condition and check if the condition is true or false.
|
|
176
|
+
// @todo: You can only use a 'value' in UIDL or 'conditions' but not both.
|
|
177
|
+
// UIDL validations need to be improved on this aspect.
|
|
178
|
+
const dynamicConditions = createConditionalStatement(
|
|
179
|
+
staticValue !== undefined ? [{ operand: staticValue, operation: '===' }] : conditions,
|
|
180
|
+
defaultValue
|
|
181
|
+
)
|
|
182
|
+
const matchCondition = matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
|
|
183
|
+
const conditionString = dynamicConditions.join(` ${matchCondition} `)
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// tslint:disable-next-line function-constructor
|
|
187
|
+
const isConditionPassing = new Function(`return ${conditionString}`)()
|
|
188
|
+
if (isConditionPassing) {
|
|
189
|
+
return generateHtmlSyntax(
|
|
190
|
+
node.content.node,
|
|
191
|
+
compName,
|
|
192
|
+
nodesLookup,
|
|
193
|
+
propDefinitions,
|
|
194
|
+
stateDefinitions,
|
|
195
|
+
subComponentOptions,
|
|
196
|
+
structure,
|
|
197
|
+
resolvedExpressions
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return conditionalNodeComment
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return conditionalNodeComment
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case 'state':
|
|
208
|
+
default:
|
|
209
|
+
return conditionalNodeComment
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case 'expr':
|
|
213
|
+
const content = node.content.split('?.')
|
|
214
|
+
|
|
215
|
+
if (resolvedExpressions && resolvedExpressions.expressions?.[content[0] || '']) {
|
|
216
|
+
const uidlDynamicRef: UIDLDynamicReference = {
|
|
217
|
+
type: 'dynamic',
|
|
218
|
+
content: {
|
|
219
|
+
referenceType: 'prop',
|
|
220
|
+
refPath: [resolvedExpressions.currentIndex.toString(), ...content.slice(1)],
|
|
221
|
+
id: content[0],
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
const generatedNode = await generateDynamicNode(
|
|
225
|
+
uidlDynamicRef,
|
|
226
|
+
compName,
|
|
227
|
+
nodesLookup,
|
|
228
|
+
resolvedExpressions.expressions,
|
|
229
|
+
stateDefinitions,
|
|
230
|
+
subComponentOptions,
|
|
231
|
+
structure
|
|
232
|
+
)
|
|
233
|
+
return generatedNode
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Fallback: support simple prop/state expressions outside of repeater context
|
|
237
|
+
if (content[0] && (propDefinitions?.[content[0]] || stateDefinitions?.[content[0]])) {
|
|
238
|
+
const isProp = Boolean(propDefinitions?.[content[0]])
|
|
239
|
+
const uidlDynamicRef: UIDLDynamicReference = {
|
|
240
|
+
type: 'dynamic',
|
|
241
|
+
content: {
|
|
242
|
+
referenceType: isProp ? 'prop' : 'state',
|
|
243
|
+
refPath: content.slice(1),
|
|
244
|
+
id: content[0],
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
const generatedNode = await generateDynamicNode(
|
|
248
|
+
uidlDynamicRef,
|
|
249
|
+
compName,
|
|
250
|
+
nodesLookup,
|
|
251
|
+
isProp ? propDefinitions : stateDefinitions,
|
|
252
|
+
stateDefinitions,
|
|
253
|
+
subComponentOptions,
|
|
254
|
+
structure
|
|
255
|
+
)
|
|
256
|
+
return generatedNode
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return HASTBuilders.createComment('Expressions are not supported in HTML')
|
|
260
|
+
case 'cms-list-repeater':
|
|
261
|
+
return generateRepeaterNode(
|
|
262
|
+
node,
|
|
263
|
+
compName,
|
|
264
|
+
nodesLookup,
|
|
82
265
|
propDefinitions,
|
|
83
266
|
stateDefinitions,
|
|
84
267
|
subComponentOptions,
|
|
85
268
|
structure
|
|
86
269
|
)
|
|
87
270
|
|
|
271
|
+
case 'cms-item':
|
|
272
|
+
case 'cms-list':
|
|
273
|
+
case 'data-source-item':
|
|
274
|
+
case 'data-source-list':
|
|
275
|
+
// For HTML generation, render the success node content
|
|
276
|
+
// Since HTML doesn't support dynamic data fetching, we just render the static structure
|
|
277
|
+
const successNode = (node.content as any).nodes?.success
|
|
278
|
+
if (successNode) {
|
|
279
|
+
return generateHtmlSyntax(
|
|
280
|
+
successNode,
|
|
281
|
+
compName,
|
|
282
|
+
nodesLookup,
|
|
283
|
+
propDefinitions,
|
|
284
|
+
stateDefinitions,
|
|
285
|
+
subComponentOptions,
|
|
286
|
+
structure,
|
|
287
|
+
resolvedExpressions
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
// If no success node, return empty div
|
|
291
|
+
return HASTBuilders.createHTMLNode('div')
|
|
292
|
+
|
|
88
293
|
default:
|
|
89
294
|
throw new HTMLComponentGeneratorError(
|
|
90
295
|
`generateHtmlSyntax encountered a node of unsupported type: ${JSON.stringify(
|
|
@@ -96,14 +301,211 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
|
|
|
96
301
|
}
|
|
97
302
|
}
|
|
98
303
|
|
|
99
|
-
const
|
|
304
|
+
const createConditionalStatement = (
|
|
305
|
+
conditions: UIDLConditionalNode['content']['condition']['conditions'],
|
|
306
|
+
leftOperand: UIDLPropDefinition['defaultValue']
|
|
307
|
+
) => {
|
|
308
|
+
return conditions.map((condition) => {
|
|
309
|
+
const { operation, operand } = condition
|
|
310
|
+
|
|
311
|
+
if (operand === undefined) {
|
|
312
|
+
return `${ASTUtils.convertToUnaryOperator(operation)}${getValueType(operand)}`
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return `${getValueType(leftOperand)} ${ASTUtils.convertToBinaryOperator(
|
|
316
|
+
operation
|
|
317
|
+
)} ${getValueType(operand)}`
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const getValueType = (value: UIDLPropDefinition['defaultValue']) => {
|
|
322
|
+
const valueType = typeof value
|
|
323
|
+
switch (valueType) {
|
|
324
|
+
case 'string':
|
|
325
|
+
return `"${value}"`
|
|
326
|
+
case 'number':
|
|
327
|
+
return value
|
|
328
|
+
case 'boolean':
|
|
329
|
+
return value
|
|
330
|
+
case 'object':
|
|
331
|
+
// Handle dynamic references (local, prop, state)
|
|
332
|
+
if (value && typeof value === 'object' && 'type' in value) {
|
|
333
|
+
const dynamicValue = value as unknown as UIDLDynamicReference
|
|
334
|
+
if (dynamicValue.type === 'dynamic' && dynamicValue.content) {
|
|
335
|
+
const { referenceType, id, refPath } = dynamicValue.content
|
|
336
|
+
if (referenceType === 'local' && id) {
|
|
337
|
+
// Local reference from repeater context
|
|
338
|
+
if (refPath && refPath.length > 0) {
|
|
339
|
+
return `${id}.${refPath.join('.')}`
|
|
340
|
+
}
|
|
341
|
+
return id
|
|
342
|
+
} else if (referenceType === 'prop' || referenceType === 'state') {
|
|
343
|
+
// Prop or state reference
|
|
344
|
+
const key = refPath && refPath.length > 0 ? refPath.join('.') : id
|
|
345
|
+
return key
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
throw new HTMLComponentGeneratorError(
|
|
350
|
+
`Conditional node received an operand of type ${valueType} \n
|
|
351
|
+
Received ${JSON.stringify(value)}`
|
|
352
|
+
)
|
|
353
|
+
default:
|
|
354
|
+
throw new HTMLComponentGeneratorError(
|
|
355
|
+
`Conditional node received an operand of type ${valueType} \n
|
|
356
|
+
Received ${JSON.stringify(value)}`
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const generateRepeaterNode: NodeToHTML<
|
|
362
|
+
UIDLCMSListRepeaterNode,
|
|
363
|
+
Promise<HastNode | HastText>
|
|
364
|
+
> = async (
|
|
100
365
|
node,
|
|
101
|
-
|
|
366
|
+
compName,
|
|
367
|
+
nodesLookup,
|
|
102
368
|
propDefinitions,
|
|
103
369
|
stateDefinitions,
|
|
104
370
|
subComponentOptions,
|
|
105
371
|
structure
|
|
106
372
|
) => {
|
|
373
|
+
const { nodes } = node.content
|
|
374
|
+
|
|
375
|
+
const contextId = node.content.renderPropIdentifier
|
|
376
|
+
const sourceValue = node.content.source
|
|
377
|
+
let propDef =
|
|
378
|
+
sourceValue && typeof sourceValue === 'string'
|
|
379
|
+
? propDefinitions[
|
|
380
|
+
Object.keys(propDefinitions).find((propKey) => sourceValue.includes(propKey)) || ''
|
|
381
|
+
]
|
|
382
|
+
: undefined
|
|
383
|
+
|
|
384
|
+
if (!propDef || !Array.isArray(propDef.defaultValue)) {
|
|
385
|
+
// If no prop is found we might have a static source value
|
|
386
|
+
try {
|
|
387
|
+
const parsedSource = JSON.parse(sourceValue)
|
|
388
|
+
propDef = {
|
|
389
|
+
defaultValue: parsedSource,
|
|
390
|
+
id: contextId,
|
|
391
|
+
type: 'array',
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
// Silent fail
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// We do the check again to keep typescript happy, otherwise this could be in catch
|
|
399
|
+
if (!propDef || !Array.isArray(propDef.defaultValue)) {
|
|
400
|
+
return HASTBuilders.createComment(
|
|
401
|
+
'CMS Array Mapper/Repeater not supported in HTML without a prop source'
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
propDefinitions[contextId] = propDef
|
|
405
|
+
|
|
406
|
+
const elementNode = HASTBuilders.createHTMLNode('div')
|
|
407
|
+
node.content.nodes.list.content.style = { display: { type: 'static', content: 'contents' } }
|
|
408
|
+
if (node.content.nodes.empty) {
|
|
409
|
+
node.content.nodes.empty.content.style = { display: { type: 'static', content: 'contents' } }
|
|
410
|
+
}
|
|
411
|
+
if (node.content.nodes.loading) {
|
|
412
|
+
node.content.nodes.loading.content.style = { display: { type: 'static', content: 'contents' } }
|
|
413
|
+
}
|
|
414
|
+
// Empty case
|
|
415
|
+
if (propDef.defaultValue.length === 0) {
|
|
416
|
+
const emptyChildren = nodes.empty?.content.children
|
|
417
|
+
if (emptyChildren) {
|
|
418
|
+
for (const child of emptyChildren) {
|
|
419
|
+
const childTag = await generateHtmlSyntax(
|
|
420
|
+
child,
|
|
421
|
+
compName,
|
|
422
|
+
nodesLookup,
|
|
423
|
+
propDefinitions,
|
|
424
|
+
stateDefinitions,
|
|
425
|
+
subComponentOptions,
|
|
426
|
+
structure
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if (typeof childTag === 'string') {
|
|
430
|
+
HASTUtils.addTextNode(elementNode, childTag)
|
|
431
|
+
} else {
|
|
432
|
+
HASTUtils.addChildNode(elementNode, childTag as HastNode)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (nodes.empty) {
|
|
438
|
+
addNodeToLookup(`${node.content.nodes.empty.content.key}`, elementNode, nodesLookup)
|
|
439
|
+
}
|
|
440
|
+
return elementNode
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const listChildren = nodes.list.content.children
|
|
444
|
+
if (listChildren) {
|
|
445
|
+
for (let index = 0; index < propDef.defaultValue.length; index++) {
|
|
446
|
+
for (const child of listChildren) {
|
|
447
|
+
const childTag = await generateHtmlSyntax(
|
|
448
|
+
child,
|
|
449
|
+
compName,
|
|
450
|
+
nodesLookup,
|
|
451
|
+
propDefinitions,
|
|
452
|
+
stateDefinitions,
|
|
453
|
+
subComponentOptions,
|
|
454
|
+
structure,
|
|
455
|
+
{ currentIndex: index, expressions: { [contextId]: propDef } }
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
if (typeof childTag === 'string') {
|
|
459
|
+
HASTUtils.addTextNode(elementNode, childTag)
|
|
460
|
+
} else {
|
|
461
|
+
HASTUtils.addChildNode(elementNode, childTag as HastNode)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
addNodeToLookup(`${node.content.nodes.list.content.key}`, elementNode, nodesLookup)
|
|
468
|
+
return elementNode
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const generateElementNode: NodeToHTML<
|
|
472
|
+
UIDLElementNode,
|
|
473
|
+
Promise<HastNode | HastText | Array<HastNode | HastText>>
|
|
474
|
+
> = async (
|
|
475
|
+
node,
|
|
476
|
+
compName,
|
|
477
|
+
nodesLookup,
|
|
478
|
+
propDefinitions,
|
|
479
|
+
stateDefinitions,
|
|
480
|
+
subComponentOptions,
|
|
481
|
+
structure,
|
|
482
|
+
resolvedExpressions
|
|
483
|
+
) => {
|
|
484
|
+
// Check if this is a wrapped data-source node
|
|
485
|
+
if (
|
|
486
|
+
node.content &&
|
|
487
|
+
typeof node.content === 'object' &&
|
|
488
|
+
'type' in node.content &&
|
|
489
|
+
(node.content.type === 'data-source-item' || node.content.type === 'data-source-list')
|
|
490
|
+
) {
|
|
491
|
+
// Handle as data-source node - just render the success content
|
|
492
|
+
const successNode = (node.content as any).nodes?.success
|
|
493
|
+
if (successNode) {
|
|
494
|
+
return generateHtmlSyntax(
|
|
495
|
+
successNode,
|
|
496
|
+
compName,
|
|
497
|
+
nodesLookup,
|
|
498
|
+
propDefinitions,
|
|
499
|
+
stateDefinitions,
|
|
500
|
+
subComponentOptions,
|
|
501
|
+
structure,
|
|
502
|
+
resolvedExpressions
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
// If no success node, return empty div
|
|
506
|
+
return HASTBuilders.createHTMLNode('div')
|
|
507
|
+
}
|
|
508
|
+
|
|
107
509
|
const {
|
|
108
510
|
elementType,
|
|
109
511
|
children,
|
|
@@ -111,43 +513,45 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
|
|
|
111
513
|
style = {},
|
|
112
514
|
referencedStyles = {},
|
|
113
515
|
dependency,
|
|
114
|
-
key,
|
|
115
516
|
} = node.content
|
|
116
|
-
|
|
117
|
-
const elementNode = HASTBuilders.createHTMLNode(elementType)
|
|
118
|
-
templatesLookUp[key] = elementNode
|
|
119
|
-
|
|
120
517
|
const { dependencies } = structure
|
|
121
|
-
if (dependency && (dependency as UIDLDependency)?.type !== 'local') {
|
|
122
|
-
dependencies[dependency.path] = dependency
|
|
123
|
-
}
|
|
124
|
-
|
|
125
518
|
if (dependency && (dependency as UIDLDependency)?.type === 'local') {
|
|
126
519
|
const compTag = await generateComponentContent(
|
|
127
520
|
node,
|
|
521
|
+
nodesLookup,
|
|
128
522
|
propDefinitions,
|
|
129
523
|
stateDefinitions,
|
|
130
524
|
subComponentOptions,
|
|
131
|
-
structure
|
|
525
|
+
structure,
|
|
526
|
+
resolvedExpressions
|
|
132
527
|
)
|
|
528
|
+
|
|
529
|
+
if ('tagName' in compTag) {
|
|
530
|
+
compTag.children.unshift(HASTBuilders.createComment(`${node.content.semanticType} component`))
|
|
531
|
+
}
|
|
532
|
+
|
|
133
533
|
return compTag
|
|
134
534
|
}
|
|
135
535
|
|
|
536
|
+
if (dependency && (dependency as UIDLDependency)?.type !== 'local') {
|
|
537
|
+
dependencies[dependency.path] = dependency
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const elementNode = HASTBuilders.createHTMLNode(elementType)
|
|
541
|
+
|
|
136
542
|
if (children) {
|
|
137
543
|
for (const child of children) {
|
|
138
|
-
const childTag = await
|
|
544
|
+
const childTag = await generateHtmlSyntax(
|
|
139
545
|
child,
|
|
140
|
-
|
|
546
|
+
compName,
|
|
547
|
+
nodesLookup,
|
|
141
548
|
propDefinitions,
|
|
142
549
|
stateDefinitions,
|
|
143
550
|
subComponentOptions,
|
|
144
|
-
structure
|
|
551
|
+
structure,
|
|
552
|
+
resolvedExpressions
|
|
145
553
|
)
|
|
146
554
|
|
|
147
|
-
if (!childTag) {
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
555
|
if (typeof childTag === 'string') {
|
|
152
556
|
HASTUtils.addTextNode(elementNode, childTag)
|
|
153
557
|
} else {
|
|
@@ -161,7 +565,6 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
|
|
|
161
565
|
const refStyle = referencedStyles[styleRef]
|
|
162
566
|
if (refStyle.content.mapType === 'inlined') {
|
|
163
567
|
handleStyles(node, refStyle.content.styles, propDefinitions, stateDefinitions)
|
|
164
|
-
return
|
|
165
568
|
}
|
|
166
569
|
})
|
|
167
570
|
}
|
|
@@ -170,23 +573,39 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
|
|
|
170
573
|
handleStyles(node, style, propDefinitions, stateDefinitions)
|
|
171
574
|
}
|
|
172
575
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
576
|
+
handleAttributes(
|
|
577
|
+
elementType,
|
|
578
|
+
elementNode,
|
|
579
|
+
attrs,
|
|
580
|
+
propDefinitions,
|
|
581
|
+
stateDefinitions,
|
|
582
|
+
structure.options.projectRouteDefinition,
|
|
583
|
+
structure.outputOptions,
|
|
584
|
+
resolvedExpressions?.currentIndex
|
|
585
|
+
)
|
|
184
586
|
|
|
587
|
+
addNodeToLookup(node.content.key, elementNode, nodesLookup)
|
|
185
588
|
return elementNode
|
|
186
589
|
}
|
|
187
590
|
|
|
591
|
+
const createLookupTable = (
|
|
592
|
+
component: ComponentUIDL,
|
|
593
|
+
nodesLookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>
|
|
594
|
+
): ElementsLookup => {
|
|
595
|
+
const lookup: ElementsLookup = {}
|
|
596
|
+
for (const node of Object.keys(nodesLookup)) {
|
|
597
|
+
lookup[node] = {
|
|
598
|
+
count: 1,
|
|
599
|
+
nextKey: '1',
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
createNodesLookup(component, lookup)
|
|
603
|
+
return lookup
|
|
604
|
+
}
|
|
605
|
+
|
|
188
606
|
const generateComponentContent = async (
|
|
189
607
|
node: UIDLElementNode,
|
|
608
|
+
nodesLookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>,
|
|
190
609
|
propDefinitions: Record<string, UIDLPropDefinition>,
|
|
191
610
|
stateDefinitions: Record<string, UIDLStateDefinition>,
|
|
192
611
|
subComponentOptions: {
|
|
@@ -198,23 +617,26 @@ const generateComponentContent = async (
|
|
|
198
617
|
dependencies: Record<string, UIDLDependency>
|
|
199
618
|
options: GeneratorOptions
|
|
200
619
|
outputOptions: UIDLComponentOutputOptions
|
|
620
|
+
},
|
|
621
|
+
resolvedExpressions?: {
|
|
622
|
+
expressions: Record<string, UIDLPropDefinition>
|
|
623
|
+
currentIndex: number
|
|
201
624
|
}
|
|
202
625
|
) => {
|
|
203
626
|
const { externals, plugins } = subComponentOptions
|
|
204
|
-
const { elementType, attrs = {},
|
|
627
|
+
const { elementType, attrs = {}, children = [] } = node.content
|
|
205
628
|
const { dependencies, chunks = [], options } = structure
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
throw new HTMLComponentGeneratorError(`${elementType} is not found from the externals. \n
|
|
212
|
-
Received ${JSON.stringify(Object.keys(externals), null, 2)}`)
|
|
629
|
+
// "Component" will not exist when generating a component because the resolver checks for illegal class names
|
|
630
|
+
const componentName = elementType === 'Component' ? 'AppComponent' : elementType
|
|
631
|
+
const component = externals[componentName]
|
|
632
|
+
if (component === undefined) {
|
|
633
|
+
throw new HTMLComponentGeneratorError(`${componentName} is missing from externals object`)
|
|
213
634
|
}
|
|
214
635
|
|
|
636
|
+
const componentClone = UIDLUtils.cloneObject<ComponentUIDL>(component)
|
|
637
|
+
|
|
215
638
|
if (children.length) {
|
|
216
|
-
|
|
217
|
-
UIDLUtils.traverseNodes(comp.node, (childNode, parentNode) => {
|
|
639
|
+
UIDLUtils.traverseNodes(componentClone.node, (childNode, parentNode) => {
|
|
218
640
|
if (childNode.type === 'slot' && parentNode.type === 'element') {
|
|
219
641
|
const nonSlotNodes = parentNode.content?.children?.filter((n) => n.type !== 'slot')
|
|
220
642
|
parentNode.content.children = [
|
|
@@ -224,6 +646,7 @@ const generateComponentContent = async (
|
|
|
224
646
|
content: {
|
|
225
647
|
key: 'custom-slot',
|
|
226
648
|
elementType: 'slot',
|
|
649
|
+
name: componentClone.name + 'slot',
|
|
227
650
|
style: {
|
|
228
651
|
display: {
|
|
229
652
|
type: 'static',
|
|
@@ -243,31 +666,47 @@ const generateComponentContent = async (
|
|
|
243
666
|
node.content.children = []
|
|
244
667
|
}
|
|
245
668
|
|
|
246
|
-
|
|
669
|
+
// In UIDL, we define only the link between a component and a page.
|
|
670
|
+
// We define this link using the UIDLLocalDependency approach.
|
|
671
|
+
// So, during the page resolution step, where we ideally generate the unique keys for the components.
|
|
672
|
+
// We can't generate the unique keys for the components because we don't have the full UIDL of the component.
|
|
673
|
+
// When we are using components in a page, the `addExternalComponents` step of the
|
|
674
|
+
// html-component-generator will add the full UIDL of the component to the externals object after resolving them.
|
|
675
|
+
// But when a component is used multiple number of times, we are basically using the same nodes again and again.
|
|
676
|
+
// Which indivates duplication. So, we create a lookup table of all the nodes present with us in the page
|
|
677
|
+
// And then pass it to the component to avoid any coilissions.
|
|
678
|
+
const lookupTableForCurrentPage = createLookupTable(componentClone, nodesLookup)
|
|
679
|
+
generateUniqueKeys(componentClone, lookupTableForCurrentPage)
|
|
247
680
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const combinedStates = { ...stateDefinitions, ...(
|
|
681
|
+
// We are combining props of the current component
|
|
682
|
+
// with props of the component that we need to generate.
|
|
683
|
+
// Refer to line 309, for element props. We either pick from the attr of the current instance of component
|
|
684
|
+
// or from the propDefinitions of the component that we are generating.
|
|
685
|
+
// We NEED to keep passing the props of the current component to the child component and so on,
|
|
686
|
+
// so that all props are forwarded.
|
|
687
|
+
const combinedProps: Record<string, UIDLPropDefinition> = {
|
|
688
|
+
...Object.keys(propDefinitions).reduce<Record<string, UIDLPropDefinition>>(
|
|
689
|
+
(acc: Record<string, UIDLPropDefinition>, propKey) => {
|
|
690
|
+
acc[propKey] = propDefinitions[propKey]
|
|
691
|
+
return acc
|
|
692
|
+
},
|
|
693
|
+
{}
|
|
694
|
+
),
|
|
695
|
+
...(componentClone?.propDefinitions || {}),
|
|
696
|
+
}
|
|
697
|
+
const combinedStates = { ...stateDefinitions, ...(componentClone?.stateDefinitions || {}) }
|
|
265
698
|
const statesForInstance = Object.keys(combinedStates).reduce(
|
|
266
699
|
(acc: Record<string, UIDLStateDefinition>, propKey) => {
|
|
267
|
-
|
|
700
|
+
const attr = attrs[propKey]
|
|
701
|
+
|
|
702
|
+
if (attr?.type === 'object') {
|
|
703
|
+
throw new Error(`Object attributes are not supported in html exports`)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (attr) {
|
|
268
707
|
acc[propKey] = {
|
|
269
708
|
...combinedStates[propKey],
|
|
270
|
-
defaultValue:
|
|
709
|
+
defaultValue: attr?.content ?? combinedStates[propKey]?.defaultValue,
|
|
271
710
|
}
|
|
272
711
|
} else {
|
|
273
712
|
acc[propKey] = combinedStates[propKey]
|
|
@@ -278,42 +717,140 @@ const generateComponentContent = async (
|
|
|
278
717
|
{}
|
|
279
718
|
)
|
|
280
719
|
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
720
|
+
const propsForInstance: Record<string, UIDLPropDefinition> = {}
|
|
721
|
+
// this is where we check if the component we are conusming is actually passing any props to the instance.
|
|
722
|
+
// We check if we are passing any props and pick the value from the atrrs, if not we pick the value from the propDefinitions of
|
|
723
|
+
// the component instance that we are using here.
|
|
724
|
+
for (const propKey of Object.keys(combinedProps)) {
|
|
725
|
+
const attribute = attrs[propKey]
|
|
726
|
+
|
|
727
|
+
if (attribute?.type === 'element') {
|
|
728
|
+
propsForInstance[propKey] = {
|
|
729
|
+
...combinedProps[propKey],
|
|
730
|
+
defaultValue: attrs[propKey],
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (attribute?.type === 'dynamic') {
|
|
735
|
+
// When we are using a component instance in a component and the attribute
|
|
736
|
+
// that is passed to the component is of dynamic reference.
|
|
737
|
+
// If means, the component is redirecting the prop that is received to the prop of the component that it is consuming.
|
|
738
|
+
// In this case, we need to pass the value of the prop that is received to the prop of the component that it is consuming.
|
|
739
|
+
// And similary we do the same for the states.
|
|
740
|
+
switch (attribute.content.referenceType) {
|
|
741
|
+
case 'prop':
|
|
742
|
+
propsForInstance[propKey] = combinedProps[attribute.content.id]
|
|
743
|
+
break
|
|
744
|
+
case 'state':
|
|
745
|
+
propsForInstance[propKey] = combinedStates[attribute.content.id]
|
|
746
|
+
break
|
|
747
|
+
case 'expr':
|
|
748
|
+
// Ignore expr type attributes in html comp instances for the time being.
|
|
749
|
+
break
|
|
750
|
+
default:
|
|
751
|
+
throw new Error(
|
|
752
|
+
`ReferenceType ${attribute.content.referenceType} is not supported in HTML Export.`
|
|
753
|
+
)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (attribute?.type === 'object') {
|
|
758
|
+
propsForInstance[propKey] = {
|
|
759
|
+
...combinedProps[propKey],
|
|
760
|
+
defaultValue: (attribute?.content as object) || combinedProps[propKey]?.defaultValue,
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (attribute?.type === 'expr') {
|
|
765
|
+
const [ctxId, ...refPath] = attribute.content.split('?.')
|
|
766
|
+
const propKeyFromAttr = Object.keys(combinedProps).find((key) => key === ctxId)
|
|
767
|
+
|
|
768
|
+
const resolvedValue = combinedProps[propKeyFromAttr]
|
|
769
|
+
const hasRefPath = refPath.length > 0
|
|
770
|
+
// Build the path using repeater index when available
|
|
771
|
+
const fullRefPath =
|
|
772
|
+
typeof resolvedExpressions?.currentIndex === 'number' && hasRefPath
|
|
773
|
+
? [resolvedExpressions.currentIndex.toString(), ...refPath]
|
|
774
|
+
: refPath
|
|
775
|
+
|
|
776
|
+
if (Array.isArray(resolvedValue)) {
|
|
777
|
+
// If the resolved value itself is an array definition, pass it through
|
|
778
|
+
propsForInstance[propKey] = resolvedValue
|
|
779
|
+
} else {
|
|
780
|
+
const defaultVal = resolvedValue?.defaultValue
|
|
781
|
+
const extracted =
|
|
782
|
+
hasRefPath && defaultVal !== undefined
|
|
783
|
+
? extractDefaultValueFromRefPath(defaultVal, fullRefPath)
|
|
784
|
+
: defaultVal ?? null
|
|
785
|
+
propsForInstance[propKey] = {
|
|
786
|
+
...resolvedValue,
|
|
787
|
+
defaultValue: extracted,
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (
|
|
793
|
+
attribute?.type !== 'dynamic' &&
|
|
794
|
+
attribute?.type !== 'element' &&
|
|
795
|
+
attribute?.type !== 'object' &&
|
|
796
|
+
attribute?.type !== 'expr'
|
|
797
|
+
) {
|
|
798
|
+
propsForInstance[propKey] = {
|
|
799
|
+
...combinedProps[propKey],
|
|
800
|
+
defaultValue: attribute?.content ?? combinedProps[propKey]?.defaultValue,
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (attribute === undefined) {
|
|
805
|
+
const propFromCurrentComponent = combinedProps[propKey]
|
|
806
|
+
propsForInstance[propKey] = propFromCurrentComponent
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
let componentWrapper = StringUtils.camelCaseToDashCase(`${componentName}-wrapper`)
|
|
811
|
+
const isExistingNode = nodesLookup[componentWrapper]
|
|
812
|
+
if (isExistingNode !== undefined) {
|
|
813
|
+
componentWrapper = `${componentWrapper}-${StringUtils.generateRandomString()}`
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const componentInstanceToGenerate: UIDLElementNode = {
|
|
817
|
+
type: 'element',
|
|
818
|
+
content: {
|
|
819
|
+
elementType: componentWrapper,
|
|
820
|
+
key: componentWrapper,
|
|
821
|
+
children: [componentClone.node],
|
|
822
|
+
style: {
|
|
823
|
+
display: {
|
|
824
|
+
type: 'static',
|
|
825
|
+
content: 'contents',
|
|
295
826
|
},
|
|
296
827
|
},
|
|
297
828
|
},
|
|
298
|
-
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const compTag = await generateHtmlSyntax(
|
|
832
|
+
componentInstanceToGenerate,
|
|
833
|
+
component.name,
|
|
834
|
+
nodesLookup,
|
|
299
835
|
propsForInstance,
|
|
300
836
|
statesForInstance,
|
|
301
837
|
subComponentOptions,
|
|
302
|
-
structure
|
|
303
|
-
|
|
838
|
+
structure,
|
|
839
|
+
resolvedExpressions
|
|
840
|
+
)
|
|
304
841
|
|
|
305
842
|
const cssPlugin = createCSSPlugin({
|
|
306
843
|
templateStyle: 'html',
|
|
307
844
|
templateChunkName: DEFAULT_COMPONENT_CHUNK_NAME,
|
|
308
845
|
declareDependency: 'import',
|
|
309
|
-
|
|
310
|
-
chunkName: comp.name,
|
|
846
|
+
chunkName: componentClone.name,
|
|
311
847
|
staticPropReferences: true,
|
|
312
848
|
})
|
|
313
849
|
|
|
314
850
|
const initialStructure: ComponentStructure = {
|
|
315
851
|
uidl: {
|
|
316
|
-
...
|
|
852
|
+
...componentClone,
|
|
853
|
+
node: componentInstanceToGenerate,
|
|
317
854
|
propDefinitions: propsForInstance,
|
|
318
855
|
stateDefinitions: statesForInstance,
|
|
319
856
|
},
|
|
@@ -325,7 +862,7 @@ const generateComponentContent = async (
|
|
|
325
862
|
linkAfter: [],
|
|
326
863
|
content: compTag,
|
|
327
864
|
meta: {
|
|
328
|
-
nodesLookup
|
|
865
|
+
nodesLookup,
|
|
329
866
|
},
|
|
330
867
|
},
|
|
331
868
|
],
|
|
@@ -341,41 +878,105 @@ const generateComponentContent = async (
|
|
|
341
878
|
Promise.resolve(initialStructure)
|
|
342
879
|
)
|
|
343
880
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
chunks.push(chunk)
|
|
348
|
-
}
|
|
349
|
-
})
|
|
350
|
-
} else {
|
|
351
|
-
const chunk = chunks.find((item) => item.name === comp.name)
|
|
352
|
-
if (!chunk) {
|
|
353
|
-
const styleChunk = result.chunks.find(
|
|
354
|
-
(item: ChunkDefinition) => item.fileType === FileType.CSS
|
|
355
|
-
)
|
|
356
|
-
if (!styleChunk) {
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
chunks.push(styleChunk)
|
|
881
|
+
result.chunks.forEach((chunk) => {
|
|
882
|
+
if (chunk.fileType === FileType.CSS) {
|
|
883
|
+
chunks.push(chunk)
|
|
360
884
|
}
|
|
361
|
-
}
|
|
885
|
+
})
|
|
362
886
|
|
|
887
|
+
addNodeToLookup(node.content.key, compTag, nodesLookup)
|
|
363
888
|
return compTag
|
|
364
889
|
}
|
|
365
890
|
|
|
366
|
-
const generateDynamicNode: NodeToHTML<
|
|
891
|
+
const generateDynamicNode: NodeToHTML<
|
|
892
|
+
UIDLDynamicReference,
|
|
893
|
+
Promise<HastNode | HastText | Array<HastNode | HastText>>
|
|
894
|
+
> = async (
|
|
367
895
|
node,
|
|
368
|
-
|
|
896
|
+
compName,
|
|
897
|
+
nodesLookup,
|
|
369
898
|
propDefinitions,
|
|
370
|
-
stateDefinitions
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
899
|
+
stateDefinitions,
|
|
900
|
+
subComponentOptions,
|
|
901
|
+
structure,
|
|
902
|
+
resolvedExpressions?
|
|
903
|
+
): Promise<HastNode | HastText | Array<HastNode | HastText>> => {
|
|
904
|
+
if (node.content.referenceType === 'locale') {
|
|
905
|
+
const localeTag = HASTBuilders.createHTMLNode('span')
|
|
906
|
+
const commentNode = HASTBuilders.createComment(`Content for locale ${node.content.id}`)
|
|
907
|
+
HASTUtils.addChildNode(localeTag, commentNode)
|
|
908
|
+
return localeTag
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const usedReferenceValue = getValueFromReference(
|
|
912
|
+
node.content.id,
|
|
913
|
+
node.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
|
|
914
|
+
)
|
|
377
915
|
|
|
378
|
-
|
|
916
|
+
if (
|
|
917
|
+
(usedReferenceValue.type === 'object' || usedReferenceValue.type === 'array') &&
|
|
918
|
+
usedReferenceValue.defaultValue
|
|
919
|
+
) {
|
|
920
|
+
// Let's say users are biding the prop to a node using something like this "fields.Title"
|
|
921
|
+
// But the fields in the object is the value where the object is defined either in propDefinitions
|
|
922
|
+
// or on the attrs. So, we just need to parsed the rest of the object path and get the value from the object.
|
|
923
|
+
const extracted = extractDefaultValueFromRefPath(
|
|
924
|
+
usedReferenceValue.defaultValue as Record<string, UIDLPropDefinition>,
|
|
925
|
+
node.content.refPath
|
|
926
|
+
)
|
|
927
|
+
if (extracted === undefined || extracted === null) {
|
|
928
|
+
return HASTBuilders.createTextNode('')
|
|
929
|
+
}
|
|
930
|
+
return HASTBuilders.createTextNode(String(extracted))
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (usedReferenceValue.type === 'element') {
|
|
934
|
+
const elementNode = usedReferenceValue.defaultValue as UIDLElementNode
|
|
935
|
+
if (elementNode) {
|
|
936
|
+
// In repeater context, avoid reusing cached nodes; uniquify key per iteration
|
|
937
|
+
if (resolvedExpressions && typeof resolvedExpressions.currentIndex === 'number') {
|
|
938
|
+
const elementClone = UIDLUtils.cloneObject<UIDLElementNode>(elementNode)
|
|
939
|
+
if (elementClone?.content?.key) {
|
|
940
|
+
elementClone.content.key = `${elementClone.content.key}-${resolvedExpressions.currentIndex}`
|
|
941
|
+
}
|
|
942
|
+
const iterElementTag = await generateHtmlSyntax(
|
|
943
|
+
elementClone,
|
|
944
|
+
compName,
|
|
945
|
+
nodesLookup,
|
|
946
|
+
propDefinitions,
|
|
947
|
+
stateDefinitions,
|
|
948
|
+
subComponentOptions,
|
|
949
|
+
structure,
|
|
950
|
+
resolvedExpressions
|
|
951
|
+
)
|
|
952
|
+
return iterElementTag
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (elementNode.content.key in nodesLookup) {
|
|
956
|
+
return nodesLookup[elementNode.content.key]
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const generatedElementTag = await generateHtmlSyntax(
|
|
960
|
+
elementNode,
|
|
961
|
+
compName,
|
|
962
|
+
nodesLookup,
|
|
963
|
+
propDefinitions,
|
|
964
|
+
stateDefinitions,
|
|
965
|
+
subComponentOptions,
|
|
966
|
+
structure,
|
|
967
|
+
resolvedExpressions
|
|
968
|
+
)
|
|
969
|
+
return generatedElementTag
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const spanTagWrapper = HASTBuilders.createHTMLNode('span')
|
|
973
|
+
const commentNode = HASTBuilders.createComment(`Content for slot ${node.content.id}`)
|
|
974
|
+
HASTUtils.addChildNode(spanTagWrapper, commentNode)
|
|
975
|
+
return spanTagWrapper
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const spanTag = HASTBuilders.createHTMLNode('span')
|
|
979
|
+
HASTUtils.addTextNode(spanTag, String(usedReferenceValue.defaultValue))
|
|
379
980
|
return spanTag
|
|
380
981
|
}
|
|
381
982
|
|
|
@@ -388,10 +989,14 @@ const handleStyles = (
|
|
|
388
989
|
Object.keys(styles).forEach((styleKey) => {
|
|
389
990
|
let style: string | UIDLStyleValue = styles[styleKey]
|
|
390
991
|
if (style.type === 'dynamic' && style.content?.referenceType !== 'token') {
|
|
391
|
-
|
|
392
|
-
style
|
|
393
|
-
|
|
394
|
-
|
|
992
|
+
const referencedValue = getValueFromReference(
|
|
993
|
+
style.content.id,
|
|
994
|
+
style.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
|
|
995
|
+
)
|
|
996
|
+
if (referencedValue.type === 'string' || referencedValue.type === 'number') {
|
|
997
|
+
style = String(
|
|
998
|
+
extractDefaultValueFromRefPath(referencedValue.defaultValue, style?.content?.refPath)
|
|
999
|
+
)
|
|
395
1000
|
}
|
|
396
1001
|
node.content.style[styleKey] = typeof style === 'string' ? staticNode(style) : style
|
|
397
1002
|
}
|
|
@@ -405,102 +1010,148 @@ const handleAttributes = (
|
|
|
405
1010
|
propDefinitions: Record<string, UIDLPropDefinition>,
|
|
406
1011
|
stateDefinitions: Record<string, UIDLStateDefinition>,
|
|
407
1012
|
routeDefinitions: UIDLRouteDefinitions,
|
|
408
|
-
outputOptions: UIDLComponentOutputOptions
|
|
1013
|
+
outputOptions: UIDLComponentOutputOptions,
|
|
1014
|
+
currentIndex?: number
|
|
409
1015
|
) => {
|
|
410
|
-
Object.keys(attrs)
|
|
1016
|
+
for (const attrKey of Object.keys(attrs)) {
|
|
411
1017
|
const attrValue = attrs[attrKey]
|
|
1018
|
+
const { type, content } = attrValue
|
|
412
1019
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
attrValue.content.startsWith('/')
|
|
418
|
-
) {
|
|
419
|
-
let targetLink
|
|
1020
|
+
switch (type) {
|
|
1021
|
+
case 'static': {
|
|
1022
|
+
if (attrKey === 'href' && typeof content === 'string' && content.startsWith('/')) {
|
|
1023
|
+
let targetLink
|
|
420
1024
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
1025
|
+
const targetRoute = (routeDefinitions?.values || []).find(
|
|
1026
|
+
(route) => route.pageOptions.navLink === content
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
if (targetRoute) {
|
|
1030
|
+
targetLink = targetRoute.pageOptions.navLink
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (!targetRoute && content === '/home') {
|
|
1034
|
+
targetLink = '/'
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (!targetLink && !targetRoute) {
|
|
1038
|
+
targetLink = content
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
|
|
1042
|
+
const localPrefix = relative(
|
|
1043
|
+
`/${currentPageRoute}`,
|
|
1044
|
+
`/${targetLink === '/' ? 'index' : targetLink}`
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
|
|
1048
|
+
break
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (typeof content === 'boolean') {
|
|
1052
|
+
htmlNode.properties[attrKey] = content === true ? 'true' : 'false'
|
|
1053
|
+
} else if (typeof content === 'string' || typeof attrValue.content === 'number') {
|
|
1054
|
+
let value = StringUtils.encode(String(attrValue.content))
|
|
1055
|
+
|
|
1056
|
+
/*
|
|
1057
|
+
elementType of image is always mapped to img.
|
|
1058
|
+
For reference, check `html-mapping` file.
|
|
1059
|
+
*/
|
|
1060
|
+
if (elementType === 'img' && attrKey === 'src' && !isValidURL(value)) {
|
|
1061
|
+
/*
|
|
1062
|
+
By default we just prefix all the asset paths with just the
|
|
1063
|
+
assetPrefix that is configured in the project. But for `html` generators
|
|
1064
|
+
we need to prefix that with the current file location.
|
|
1065
|
+
|
|
1066
|
+
Because, all the other frameworks have a build setup. which serves all the
|
|
1067
|
+
assets from the `public` folder. But in the case of `html` here is how it works
|
|
1068
|
+
|
|
1069
|
+
We load a file from `index.html` the request for the image goes from
|
|
1070
|
+
'...url.../public/...image...'
|
|
1071
|
+
If it's a nested url, then the request goes from
|
|
1072
|
+
'...url/nested/public/...image..'
|
|
424
1073
|
|
|
425
|
-
|
|
426
|
-
|
|
1074
|
+
But the nested folder is available only on the root. With this
|
|
1075
|
+
The url changes prefixes to
|
|
1076
|
+
|
|
1077
|
+
../public/playground_assets/..image.. etc depending on the dept the file is in.
|
|
1078
|
+
*/
|
|
1079
|
+
value = join(relative(join(...outputOptions.folderPath), './'), value)
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
break
|
|
427
1086
|
}
|
|
428
1087
|
|
|
429
|
-
|
|
430
|
-
|
|
1088
|
+
case 'dynamic': {
|
|
1089
|
+
const value = getValueFromReference(
|
|
1090
|
+
content.id,
|
|
1091
|
+
content.referenceType === 'prop' ? propDefinitions : stateDefinitions
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
const extracted = extractDefaultValueFromRefPath(value.defaultValue, content.refPath)
|
|
1095
|
+
const extractedValue = String(extracted)
|
|
1096
|
+
|
|
1097
|
+
if (
|
|
1098
|
+
(elementType === 'img' || elementType === 'video') &&
|
|
1099
|
+
attrKey === 'src' &&
|
|
1100
|
+
!extractedValue.startsWith('http')
|
|
1101
|
+
) {
|
|
1102
|
+
const path = join(relative(join(...outputOptions.folderPath), './'), extractedValue)
|
|
1103
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, path)
|
|
1104
|
+
break
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, extractedValue)
|
|
1108
|
+
break
|
|
431
1109
|
}
|
|
432
1110
|
|
|
433
|
-
|
|
434
|
-
|
|
1111
|
+
case 'raw': {
|
|
1112
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, content)
|
|
1113
|
+
break
|
|
435
1114
|
}
|
|
436
1115
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
`/${targetLink === '/' ? 'index' : targetLink}`
|
|
441
|
-
)
|
|
1116
|
+
case 'expr': {
|
|
1117
|
+
const fullPath = content.split('?.')
|
|
1118
|
+
const prop = propDefinitions[fullPath?.[0] || '']
|
|
442
1119
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1120
|
+
if (!prop) {
|
|
1121
|
+
break
|
|
1122
|
+
}
|
|
446
1123
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1124
|
+
const path =
|
|
1125
|
+
typeof currentIndex === 'number'
|
|
1126
|
+
? [currentIndex.toString(), ...fullPath.slice(1)]
|
|
1127
|
+
: fullPath.slice(1)
|
|
1128
|
+
const value = extractDefaultValueFromRefPath(prop.defaultValue, path)
|
|
1129
|
+
if (!value) {
|
|
1130
|
+
break
|
|
1131
|
+
}
|
|
1132
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value))
|
|
1133
|
+
break
|
|
1134
|
+
}
|
|
455
1135
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
1136
|
+
case 'element':
|
|
1137
|
+
case 'import':
|
|
1138
|
+
case 'object':
|
|
1139
|
+
break
|
|
460
1140
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
let value = StringUtils.encode(String(attrValue.content))
|
|
466
|
-
|
|
467
|
-
/*
|
|
468
|
-
elementType of image is always mapped to img.
|
|
469
|
-
For reference, check `html-mapping` file.
|
|
470
|
-
*/
|
|
471
|
-
if (elementType === 'img' && attrKey === 'src') {
|
|
472
|
-
/*
|
|
473
|
-
By default we just prefix all the asset paths with just the
|
|
474
|
-
assetPrefix that is configured in the project. But for `html` generators
|
|
475
|
-
we need to prefix that with the current file location.
|
|
476
|
-
|
|
477
|
-
Because, all the other frameworks have a build setup. which serves all the
|
|
478
|
-
assets from the `public` folder. But in the case of `html` here is how it works
|
|
479
|
-
|
|
480
|
-
We load a file from `index.html` the request for the image goes from
|
|
481
|
-
'...url.../public/...image...'
|
|
482
|
-
If it's a nested url, then the request goes from
|
|
483
|
-
'...url/nested/public/...image..'
|
|
484
|
-
|
|
485
|
-
But the nested folder is available only on the root. With this
|
|
486
|
-
The url changes prefixes to
|
|
487
|
-
|
|
488
|
-
../public/playground_assets/..image.. etc depending on the dept the file is in.
|
|
489
|
-
*/
|
|
490
|
-
value = join(relative(join(...outputOptions.folderPath), './'), value)
|
|
1141
|
+
default: {
|
|
1142
|
+
throw new HTMLComponentGeneratorError(
|
|
1143
|
+
`Received ${JSON.stringify(attrValue, null, 2)} \n in handleAttributes for html`
|
|
1144
|
+
)
|
|
491
1145
|
}
|
|
492
|
-
|
|
493
|
-
HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
|
|
494
|
-
return
|
|
495
1146
|
}
|
|
496
|
-
}
|
|
1147
|
+
}
|
|
497
1148
|
}
|
|
498
1149
|
|
|
499
1150
|
const getValueFromReference = (
|
|
500
1151
|
key: string,
|
|
501
1152
|
definitions: Record<string, UIDLPropDefinition>
|
|
502
|
-
):
|
|
503
|
-
const usedReferenceValue = definitions[key.includes('
|
|
1153
|
+
): UIDLPropDefinition | undefined => {
|
|
1154
|
+
const usedReferenceValue = definitions[key.includes('?.') ? key.split('?.')[0] : key]
|
|
504
1155
|
|
|
505
1156
|
if (!usedReferenceValue) {
|
|
506
1157
|
throw new HTMLComponentGeneratorError(
|
|
@@ -508,9 +1159,13 @@ const getValueFromReference = (
|
|
|
508
1159
|
)
|
|
509
1160
|
}
|
|
510
1161
|
|
|
511
|
-
if (
|
|
1162
|
+
if (
|
|
1163
|
+
['string', 'number', 'object', 'element', 'array', 'boolean'].includes(
|
|
1164
|
+
usedReferenceValue?.type
|
|
1165
|
+
) === false
|
|
1166
|
+
) {
|
|
512
1167
|
throw new HTMLComponentGeneratorError(
|
|
513
|
-
`
|
|
1168
|
+
`Attribute is using dynamic value, but received of type ${JSON.stringify(
|
|
514
1169
|
usedReferenceValue,
|
|
515
1170
|
null,
|
|
516
1171
|
2
|
|
@@ -518,9 +1173,12 @@ const getValueFromReference = (
|
|
|
518
1173
|
)
|
|
519
1174
|
}
|
|
520
1175
|
|
|
521
|
-
if (
|
|
1176
|
+
if (
|
|
1177
|
+
usedReferenceValue.type !== 'element' &&
|
|
1178
|
+
usedReferenceValue.hasOwnProperty('defaultValue') === false
|
|
1179
|
+
) {
|
|
522
1180
|
throw new HTMLComponentGeneratorError(
|
|
523
|
-
`
|
|
1181
|
+
`Default value is missing from dynamic reference - ${JSON.stringify(
|
|
524
1182
|
usedReferenceValue,
|
|
525
1183
|
null,
|
|
526
1184
|
2
|
|
@@ -528,5 +1186,33 @@ const getValueFromReference = (
|
|
|
528
1186
|
)
|
|
529
1187
|
}
|
|
530
1188
|
|
|
531
|
-
return
|
|
1189
|
+
return usedReferenceValue
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const extractDefaultValueFromRefPath = (
|
|
1193
|
+
propDefaultValue: PropDefaultValueTypes,
|
|
1194
|
+
refPath?: string[]
|
|
1195
|
+
): PropDefaultValueTypes => {
|
|
1196
|
+
if (!refPath || refPath.length === 0) {
|
|
1197
|
+
return propDefaultValue
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Directly handle array indexing for the first segment when applicable
|
|
1201
|
+
if (Array.isArray(propDefaultValue)) {
|
|
1202
|
+
const [first, ...rest] = refPath
|
|
1203
|
+
const idx = Number(first)
|
|
1204
|
+
if (!Number.isNaN(idx) && idx >= 0 && idx < propDefaultValue.length) {
|
|
1205
|
+
const nextVal = propDefaultValue[idx] as PropDefaultValueTypes
|
|
1206
|
+
if (rest.length === 0) {
|
|
1207
|
+
return nextVal
|
|
1208
|
+
}
|
|
1209
|
+
return extractDefaultValueFromRefPath(nextVal, rest)
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (typeof propDefaultValue !== 'object') {
|
|
1214
|
+
return propDefaultValue
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return GenericUtils.getValueFromPath(refPath.join('.'), propDefaultValue) as PropDefaultValueTypes
|
|
532
1218
|
}
|