@teleporthq/teleport-plugin-html-base-component 0.42.0-alpha.0 → 0.42.2
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 +639 -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 +639 -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 +898 -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(
|
|
133
|
+
node,
|
|
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(
|
|
80
262
|
node,
|
|
81
|
-
|
|
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,149 @@ 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
|
+
// Transfer data-node attributes from component instance to wrapper
|
|
817
|
+
const wrapperAttrs: Record<string, UIDLAttributeValue> = {}
|
|
818
|
+
Object.keys(attrs).forEach((attrKey) => {
|
|
819
|
+
if (attrKey.startsWith('dataNode') || attrKey.startsWith('data-node')) {
|
|
820
|
+
wrapperAttrs[attrKey] = attrs[attrKey]
|
|
821
|
+
}
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
const componentInstanceToGenerate: UIDLElementNode = {
|
|
825
|
+
type: 'element',
|
|
826
|
+
content: {
|
|
827
|
+
elementType: componentWrapper,
|
|
828
|
+
key: componentWrapper,
|
|
829
|
+
children: [componentClone.node],
|
|
830
|
+
attrs: wrapperAttrs,
|
|
831
|
+
style: {
|
|
832
|
+
display: {
|
|
833
|
+
type: 'static',
|
|
834
|
+
content: 'contents',
|
|
295
835
|
},
|
|
296
836
|
},
|
|
297
837
|
},
|
|
298
|
-
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const compTag = await generateHtmlSyntax(
|
|
841
|
+
componentInstanceToGenerate,
|
|
842
|
+
component.name,
|
|
843
|
+
nodesLookup,
|
|
299
844
|
propsForInstance,
|
|
300
845
|
statesForInstance,
|
|
301
846
|
subComponentOptions,
|
|
302
|
-
structure
|
|
303
|
-
|
|
847
|
+
structure,
|
|
848
|
+
resolvedExpressions
|
|
849
|
+
)
|
|
304
850
|
|
|
305
851
|
const cssPlugin = createCSSPlugin({
|
|
306
852
|
templateStyle: 'html',
|
|
307
853
|
templateChunkName: DEFAULT_COMPONENT_CHUNK_NAME,
|
|
308
854
|
declareDependency: 'import',
|
|
309
|
-
|
|
310
|
-
chunkName: comp.name,
|
|
855
|
+
chunkName: componentClone.name,
|
|
311
856
|
staticPropReferences: true,
|
|
312
857
|
})
|
|
313
858
|
|
|
314
859
|
const initialStructure: ComponentStructure = {
|
|
315
860
|
uidl: {
|
|
316
|
-
...
|
|
861
|
+
...componentClone,
|
|
862
|
+
node: componentInstanceToGenerate,
|
|
317
863
|
propDefinitions: propsForInstance,
|
|
318
864
|
stateDefinitions: statesForInstance,
|
|
319
865
|
},
|
|
@@ -325,7 +871,7 @@ const generateComponentContent = async (
|
|
|
325
871
|
linkAfter: [],
|
|
326
872
|
content: compTag,
|
|
327
873
|
meta: {
|
|
328
|
-
nodesLookup
|
|
874
|
+
nodesLookup,
|
|
329
875
|
},
|
|
330
876
|
},
|
|
331
877
|
],
|
|
@@ -341,41 +887,105 @@ const generateComponentContent = async (
|
|
|
341
887
|
Promise.resolve(initialStructure)
|
|
342
888
|
)
|
|
343
889
|
|
|
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)
|
|
890
|
+
result.chunks.forEach((chunk) => {
|
|
891
|
+
if (chunk.fileType === FileType.CSS) {
|
|
892
|
+
chunks.push(chunk)
|
|
360
893
|
}
|
|
361
|
-
}
|
|
894
|
+
})
|
|
362
895
|
|
|
896
|
+
addNodeToLookup(node.content.key, compTag, nodesLookup)
|
|
363
897
|
return compTag
|
|
364
898
|
}
|
|
365
899
|
|
|
366
|
-
const generateDynamicNode: NodeToHTML<
|
|
900
|
+
const generateDynamicNode: NodeToHTML<
|
|
901
|
+
UIDLDynamicReference,
|
|
902
|
+
Promise<HastNode | HastText | Array<HastNode | HastText>>
|
|
903
|
+
> = async (
|
|
367
904
|
node,
|
|
368
|
-
|
|
905
|
+
compName,
|
|
906
|
+
nodesLookup,
|
|
369
907
|
propDefinitions,
|
|
370
|
-
stateDefinitions
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
908
|
+
stateDefinitions,
|
|
909
|
+
subComponentOptions,
|
|
910
|
+
structure,
|
|
911
|
+
resolvedExpressions?
|
|
912
|
+
): Promise<HastNode | HastText | Array<HastNode | HastText>> => {
|
|
913
|
+
if (node.content.referenceType === 'locale') {
|
|
914
|
+
const localeTag = HASTBuilders.createHTMLNode('span')
|
|
915
|
+
const commentNode = HASTBuilders.createComment(`Content for locale ${node.content.id}`)
|
|
916
|
+
HASTUtils.addChildNode(localeTag, commentNode)
|
|
917
|
+
return localeTag
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const usedReferenceValue = getValueFromReference(
|
|
921
|
+
node.content.id,
|
|
922
|
+
node.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
if (
|
|
926
|
+
(usedReferenceValue.type === 'object' || usedReferenceValue.type === 'array') &&
|
|
927
|
+
usedReferenceValue.defaultValue
|
|
928
|
+
) {
|
|
929
|
+
// Let's say users are biding the prop to a node using something like this "fields.Title"
|
|
930
|
+
// But the fields in the object is the value where the object is defined either in propDefinitions
|
|
931
|
+
// or on the attrs. So, we just need to parsed the rest of the object path and get the value from the object.
|
|
932
|
+
const extracted = extractDefaultValueFromRefPath(
|
|
933
|
+
usedReferenceValue.defaultValue as Record<string, UIDLPropDefinition>,
|
|
934
|
+
node.content.refPath
|
|
935
|
+
)
|
|
936
|
+
if (extracted === undefined || extracted === null) {
|
|
937
|
+
return HASTBuilders.createTextNode('')
|
|
938
|
+
}
|
|
939
|
+
return HASTBuilders.createTextNode(String(extracted))
|
|
940
|
+
}
|
|
377
941
|
|
|
378
|
-
|
|
942
|
+
if (usedReferenceValue.type === 'element') {
|
|
943
|
+
const elementNode = usedReferenceValue.defaultValue as UIDLElementNode
|
|
944
|
+
if (elementNode) {
|
|
945
|
+
// In repeater context, avoid reusing cached nodes; uniquify key per iteration
|
|
946
|
+
if (resolvedExpressions && typeof resolvedExpressions.currentIndex === 'number') {
|
|
947
|
+
const elementClone = UIDLUtils.cloneObject<UIDLElementNode>(elementNode)
|
|
948
|
+
if (elementClone?.content?.key) {
|
|
949
|
+
elementClone.content.key = `${elementClone.content.key}-${resolvedExpressions.currentIndex}`
|
|
950
|
+
}
|
|
951
|
+
const iterElementTag = await generateHtmlSyntax(
|
|
952
|
+
elementClone,
|
|
953
|
+
compName,
|
|
954
|
+
nodesLookup,
|
|
955
|
+
propDefinitions,
|
|
956
|
+
stateDefinitions,
|
|
957
|
+
subComponentOptions,
|
|
958
|
+
structure,
|
|
959
|
+
resolvedExpressions
|
|
960
|
+
)
|
|
961
|
+
return iterElementTag
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (elementNode.content.key in nodesLookup) {
|
|
965
|
+
return nodesLookup[elementNode.content.key]
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const generatedElementTag = await generateHtmlSyntax(
|
|
969
|
+
elementNode,
|
|
970
|
+
compName,
|
|
971
|
+
nodesLookup,
|
|
972
|
+
propDefinitions,
|
|
973
|
+
stateDefinitions,
|
|
974
|
+
subComponentOptions,
|
|
975
|
+
structure,
|
|
976
|
+
resolvedExpressions
|
|
977
|
+
)
|
|
978
|
+
return generatedElementTag
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const spanTagWrapper = HASTBuilders.createHTMLNode('span')
|
|
982
|
+
const commentNode = HASTBuilders.createComment(`Content for slot ${node.content.id}`)
|
|
983
|
+
HASTUtils.addChildNode(spanTagWrapper, commentNode)
|
|
984
|
+
return spanTagWrapper
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const spanTag = HASTBuilders.createHTMLNode('span')
|
|
988
|
+
HASTUtils.addTextNode(spanTag, String(usedReferenceValue.defaultValue))
|
|
379
989
|
return spanTag
|
|
380
990
|
}
|
|
381
991
|
|
|
@@ -388,10 +998,14 @@ const handleStyles = (
|
|
|
388
998
|
Object.keys(styles).forEach((styleKey) => {
|
|
389
999
|
let style: string | UIDLStyleValue = styles[styleKey]
|
|
390
1000
|
if (style.type === 'dynamic' && style.content?.referenceType !== 'token') {
|
|
391
|
-
|
|
392
|
-
style
|
|
393
|
-
|
|
394
|
-
|
|
1001
|
+
const referencedValue = getValueFromReference(
|
|
1002
|
+
style.content.id,
|
|
1003
|
+
style.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
|
|
1004
|
+
)
|
|
1005
|
+
if (referencedValue.type === 'string' || referencedValue.type === 'number') {
|
|
1006
|
+
style = String(
|
|
1007
|
+
extractDefaultValueFromRefPath(referencedValue.defaultValue, style?.content?.refPath)
|
|
1008
|
+
)
|
|
395
1009
|
}
|
|
396
1010
|
node.content.style[styleKey] = typeof style === 'string' ? staticNode(style) : style
|
|
397
1011
|
}
|
|
@@ -405,102 +1019,148 @@ const handleAttributes = (
|
|
|
405
1019
|
propDefinitions: Record<string, UIDLPropDefinition>,
|
|
406
1020
|
stateDefinitions: Record<string, UIDLStateDefinition>,
|
|
407
1021
|
routeDefinitions: UIDLRouteDefinitions,
|
|
408
|
-
outputOptions: UIDLComponentOutputOptions
|
|
1022
|
+
outputOptions: UIDLComponentOutputOptions,
|
|
1023
|
+
currentIndex?: number
|
|
409
1024
|
) => {
|
|
410
|
-
Object.keys(attrs)
|
|
1025
|
+
for (const attrKey of Object.keys(attrs)) {
|
|
411
1026
|
const attrValue = attrs[attrKey]
|
|
1027
|
+
const { type, content } = attrValue
|
|
412
1028
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
attrValue.content.startsWith('/')
|
|
418
|
-
) {
|
|
419
|
-
let targetLink
|
|
1029
|
+
switch (type) {
|
|
1030
|
+
case 'static': {
|
|
1031
|
+
if (attrKey === 'href' && typeof content === 'string' && content.startsWith('/')) {
|
|
1032
|
+
let targetLink
|
|
420
1033
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
1034
|
+
const targetRoute = (routeDefinitions?.values || []).find(
|
|
1035
|
+
(route) => route.pageOptions.navLink === content
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
if (targetRoute) {
|
|
1039
|
+
targetLink = targetRoute.pageOptions.navLink
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (!targetRoute && content === '/home') {
|
|
1043
|
+
targetLink = '/'
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (!targetLink && !targetRoute) {
|
|
1047
|
+
targetLink = content
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
|
|
1051
|
+
const localPrefix = relative(
|
|
1052
|
+
`/${currentPageRoute}`,
|
|
1053
|
+
`/${targetLink === '/' ? 'index' : targetLink}`
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
|
|
1057
|
+
break
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (typeof content === 'boolean') {
|
|
1061
|
+
htmlNode.properties[attrKey] = content === true ? 'true' : 'false'
|
|
1062
|
+
} else if (typeof content === 'string' || typeof attrValue.content === 'number') {
|
|
1063
|
+
let value = StringUtils.encode(String(attrValue.content))
|
|
1064
|
+
|
|
1065
|
+
/*
|
|
1066
|
+
elementType of image is always mapped to img.
|
|
1067
|
+
For reference, check `html-mapping` file.
|
|
1068
|
+
*/
|
|
1069
|
+
if (elementType === 'img' && attrKey === 'src' && !isValidURL(value)) {
|
|
1070
|
+
/*
|
|
1071
|
+
By default we just prefix all the asset paths with just the
|
|
1072
|
+
assetPrefix that is configured in the project. But for `html` generators
|
|
1073
|
+
we need to prefix that with the current file location.
|
|
1074
|
+
|
|
1075
|
+
Because, all the other frameworks have a build setup. which serves all the
|
|
1076
|
+
assets from the `public` folder. But in the case of `html` here is how it works
|
|
424
1077
|
|
|
425
|
-
|
|
426
|
-
|
|
1078
|
+
We load a file from `index.html` the request for the image goes from
|
|
1079
|
+
'...url.../public/...image...'
|
|
1080
|
+
If it's a nested url, then the request goes from
|
|
1081
|
+
'...url/nested/public/...image..'
|
|
1082
|
+
|
|
1083
|
+
But the nested folder is available only on the root. With this
|
|
1084
|
+
The url changes prefixes to
|
|
1085
|
+
|
|
1086
|
+
../public/playground_assets/..image.. etc depending on the dept the file is in.
|
|
1087
|
+
*/
|
|
1088
|
+
value = join(relative(join(...outputOptions.folderPath), './'), value)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
break
|
|
427
1095
|
}
|
|
428
1096
|
|
|
429
|
-
|
|
430
|
-
|
|
1097
|
+
case 'dynamic': {
|
|
1098
|
+
const value = getValueFromReference(
|
|
1099
|
+
content.id,
|
|
1100
|
+
content.referenceType === 'prop' ? propDefinitions : stateDefinitions
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
const extracted = extractDefaultValueFromRefPath(value.defaultValue, content.refPath)
|
|
1104
|
+
const extractedValue = String(extracted)
|
|
1105
|
+
|
|
1106
|
+
if (
|
|
1107
|
+
(elementType === 'img' || elementType === 'video') &&
|
|
1108
|
+
attrKey === 'src' &&
|
|
1109
|
+
!extractedValue.startsWith('http')
|
|
1110
|
+
) {
|
|
1111
|
+
const path = join(relative(join(...outputOptions.folderPath), './'), extractedValue)
|
|
1112
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, path)
|
|
1113
|
+
break
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, extractedValue)
|
|
1117
|
+
break
|
|
431
1118
|
}
|
|
432
1119
|
|
|
433
|
-
|
|
434
|
-
|
|
1120
|
+
case 'raw': {
|
|
1121
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, content)
|
|
1122
|
+
break
|
|
435
1123
|
}
|
|
436
1124
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
`/${targetLink === '/' ? 'index' : targetLink}`
|
|
441
|
-
)
|
|
1125
|
+
case 'expr': {
|
|
1126
|
+
const fullPath = content.split('?.')
|
|
1127
|
+
const prop = propDefinitions[fullPath?.[0] || '']
|
|
442
1128
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1129
|
+
if (!prop) {
|
|
1130
|
+
break
|
|
1131
|
+
}
|
|
446
1132
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1133
|
+
const path =
|
|
1134
|
+
typeof currentIndex === 'number'
|
|
1135
|
+
? [currentIndex.toString(), ...fullPath.slice(1)]
|
|
1136
|
+
: fullPath.slice(1)
|
|
1137
|
+
const value = extractDefaultValueFromRefPath(prop.defaultValue, path)
|
|
1138
|
+
if (!value) {
|
|
1139
|
+
break
|
|
1140
|
+
}
|
|
1141
|
+
HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value))
|
|
1142
|
+
break
|
|
1143
|
+
}
|
|
455
1144
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
1145
|
+
case 'element':
|
|
1146
|
+
case 'import':
|
|
1147
|
+
case 'object':
|
|
1148
|
+
break
|
|
460
1149
|
|
|
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)
|
|
1150
|
+
default: {
|
|
1151
|
+
throw new HTMLComponentGeneratorError(
|
|
1152
|
+
`Received ${JSON.stringify(attrValue, null, 2)} \n in handleAttributes for html`
|
|
1153
|
+
)
|
|
491
1154
|
}
|
|
492
|
-
|
|
493
|
-
HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
|
|
494
|
-
return
|
|
495
1155
|
}
|
|
496
|
-
}
|
|
1156
|
+
}
|
|
497
1157
|
}
|
|
498
1158
|
|
|
499
1159
|
const getValueFromReference = (
|
|
500
1160
|
key: string,
|
|
501
1161
|
definitions: Record<string, UIDLPropDefinition>
|
|
502
|
-
):
|
|
503
|
-
const usedReferenceValue = definitions[key.includes('
|
|
1162
|
+
): UIDLPropDefinition | undefined => {
|
|
1163
|
+
const usedReferenceValue = definitions[key.includes('?.') ? key.split('?.')[0] : key]
|
|
504
1164
|
|
|
505
1165
|
if (!usedReferenceValue) {
|
|
506
1166
|
throw new HTMLComponentGeneratorError(
|
|
@@ -508,9 +1168,13 @@ const getValueFromReference = (
|
|
|
508
1168
|
)
|
|
509
1169
|
}
|
|
510
1170
|
|
|
511
|
-
if (
|
|
1171
|
+
if (
|
|
1172
|
+
['string', 'number', 'object', 'element', 'array', 'boolean'].includes(
|
|
1173
|
+
usedReferenceValue?.type
|
|
1174
|
+
) === false
|
|
1175
|
+
) {
|
|
512
1176
|
throw new HTMLComponentGeneratorError(
|
|
513
|
-
`
|
|
1177
|
+
`Attribute is using dynamic value, but received of type ${JSON.stringify(
|
|
514
1178
|
usedReferenceValue,
|
|
515
1179
|
null,
|
|
516
1180
|
2
|
|
@@ -518,9 +1182,12 @@ const getValueFromReference = (
|
|
|
518
1182
|
)
|
|
519
1183
|
}
|
|
520
1184
|
|
|
521
|
-
if (
|
|
1185
|
+
if (
|
|
1186
|
+
usedReferenceValue.type !== 'element' &&
|
|
1187
|
+
usedReferenceValue.hasOwnProperty('defaultValue') === false
|
|
1188
|
+
) {
|
|
522
1189
|
throw new HTMLComponentGeneratorError(
|
|
523
|
-
`
|
|
1190
|
+
`Default value is missing from dynamic reference - ${JSON.stringify(
|
|
524
1191
|
usedReferenceValue,
|
|
525
1192
|
null,
|
|
526
1193
|
2
|
|
@@ -528,5 +1195,33 @@ const getValueFromReference = (
|
|
|
528
1195
|
)
|
|
529
1196
|
}
|
|
530
1197
|
|
|
531
|
-
return
|
|
1198
|
+
return usedReferenceValue
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const extractDefaultValueFromRefPath = (
|
|
1202
|
+
propDefaultValue: PropDefaultValueTypes,
|
|
1203
|
+
refPath?: string[]
|
|
1204
|
+
): PropDefaultValueTypes => {
|
|
1205
|
+
if (!refPath || refPath.length === 0) {
|
|
1206
|
+
return propDefaultValue
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Directly handle array indexing for the first segment when applicable
|
|
1210
|
+
if (Array.isArray(propDefaultValue)) {
|
|
1211
|
+
const [first, ...rest] = refPath
|
|
1212
|
+
const idx = Number(first)
|
|
1213
|
+
if (!Number.isNaN(idx) && idx >= 0 && idx < propDefaultValue.length) {
|
|
1214
|
+
const nextVal = propDefaultValue[idx] as PropDefaultValueTypes
|
|
1215
|
+
if (rest.length === 0) {
|
|
1216
|
+
return nextVal
|
|
1217
|
+
}
|
|
1218
|
+
return extractDefaultValueFromRefPath(nextVal, rest)
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (typeof propDefaultValue !== 'object') {
|
|
1223
|
+
return propDefaultValue
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
return GenericUtils.getValueFromPath(refPath.join('.'), propDefaultValue) as PropDefaultValueTypes
|
|
532
1227
|
}
|