@teleporthq/teleport-plugin-html-base-component 0.43.0-alpha.0 → 0.43.3

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.
@@ -21,44 +21,136 @@ import {
21
21
  ComponentStructure,
22
22
  UIDLComponentOutputOptions,
23
23
  UIDLElement,
24
+ ElementsLookup,
25
+ UIDLConditionalNode,
26
+ PropDefaultValueTypes,
27
+ UIDLCMSListRepeaterNode,
28
+ UIDLStaticValue,
29
+ UIDLRawValue,
24
30
  } from '@teleporthq/teleport-types'
25
31
  import { join, relative } from 'path'
26
- import { HASTBuilders, HASTUtils } from '@teleporthq/teleport-plugin-common'
27
- import { StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
32
+ import { HASTBuilders, HASTUtils, ASTUtils } from '@teleporthq/teleport-plugin-common'
33
+ import { GenericUtils, StringUtils, UIDLUtils } from '@teleporthq/teleport-shared'
28
34
  import { staticNode } from '@teleporthq/teleport-uidl-builders'
29
35
  import { createCSSPlugin } from '@teleporthq/teleport-plugin-css'
36
+ import { generateUniqueKeys, createNodesLookup } from '@teleporthq/teleport-uidl-resolver'
30
37
  import { DEFAULT_COMPONENT_CHUNK_NAME } from './constants'
31
38
 
39
+ const getTranslation = (
40
+ id: string,
41
+ options: GeneratorOptions
42
+ ): UIDLElementNode | UIDLStaticValue | null => {
43
+ const i18n = options.internationalization
44
+ if (!i18n?.translations) {
45
+ return null
46
+ }
47
+ const locale = i18n.targetLocale || i18n.main?.locale
48
+ if (!locale) {
49
+ return null
50
+ }
51
+ return i18n.translations[locale]?.[id] || null
52
+ }
53
+
54
+ const resolveTranslationText = (translation: UIDLElementNode | UIDLStaticValue): string => {
55
+ if (translation.type === 'static') {
56
+ return String(translation.content)
57
+ }
58
+ if (translation.type === 'element' && translation.content.children) {
59
+ return translation.content.children
60
+ .map((child) => {
61
+ if (child.type === 'static') {
62
+ return String(child.content)
63
+ }
64
+ if (child.type === 'element') {
65
+ return resolveTranslationText(child)
66
+ }
67
+ return ''
68
+ })
69
+ .join('')
70
+ }
71
+ return ''
72
+ }
73
+
74
+ const isValidURL = (url: string) => {
75
+ try {
76
+ /* tslint:disable:no-unused-expression */
77
+ new URL(url)
78
+ return true
79
+ } catch (error) {
80
+ return false
81
+ }
82
+ }
83
+
84
+ const addNodeToLookup = (
85
+ key: string,
86
+ tag: HastNode | HastText | Array<HastNode | HastText>,
87
+ nodesLoookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>
88
+ ) => {
89
+ // In html code-generation we combine the nodes of the component that is being consumed with the current component.
90
+ // As html can't load the component at runtime like react or any other frameworks. So, we merge the component as a standalone
91
+ // component in the current component.
92
+ const currentLookup = nodesLoookup[key]
93
+ if (currentLookup) {
94
+ if (Array.isArray(currentLookup)) {
95
+ Array.isArray(tag) ? currentLookup.push(...tag) : currentLookup.push(tag)
96
+ } else {
97
+ nodesLoookup[key] = Array.isArray(tag) ? [currentLookup, ...tag] : [currentLookup, tag]
98
+ }
99
+
100
+ return
101
+ }
102
+
103
+ nodesLoookup[key] = tag
104
+ }
105
+
32
106
  type NodeToHTML<NodeType, ReturnType> = (
33
107
  node: NodeType,
34
- templatesLookUp: Record<string, unknown>,
108
+ componentName: string,
109
+ nodesLookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>,
35
110
  propDefinitions: Record<string, UIDLPropDefinition>,
36
111
  stateDefinitions: Record<string, UIDLStateDefinition>,
37
112
  subComponentOptions: {
38
113
  externals: Record<string, ComponentUIDL>
39
114
  plugins: ComponentPlugin[]
115
+ standaloneHtmlComponents?: boolean
40
116
  },
41
117
  structure: {
42
118
  chunks: ChunkDefinition[]
43
119
  dependencies: Record<string, UIDLDependency>
44
120
  options: GeneratorOptions
45
121
  outputOptions: UIDLComponentOutputOptions
122
+ },
123
+ /**
124
+ * This param is just to be able to handle CMS array mappers/Repeater nodes. A bit hacky, better support should be implemented
125
+ */
126
+ resolvedExpressions?: {
127
+ expressions: Record<string, UIDLPropDefinition>
128
+ currentIndex: number
46
129
  }
47
130
  ) => ReturnType
48
131
 
49
- export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastText>> = async (
132
+ export const generateHtmlSyntax: NodeToHTML<
133
+ UIDLNode,
134
+ Promise<HastNode | HastText | Array<HastNode | HastText>>
135
+ > = async (
50
136
  node,
51
- templatesLookUp,
137
+ compName,
138
+ nodesLookup,
52
139
  propDefinitions,
53
140
  stateDefinitions,
54
141
  subComponentOptions,
55
- structure
142
+ structure,
143
+ resolvedExpressions
56
144
  ) => {
57
145
  switch (node.type) {
58
146
  case 'inject':
59
- case 'raw':
60
147
  return HASTBuilders.createTextNode(node.content.toString())
61
148
 
149
+ case 'raw': {
150
+ const rawNode = node as UIDLRawValue
151
+ return HASTBuilders.createTextNode((rawNode.fallback || rawNode.content).toString())
152
+ }
153
+
62
154
  case 'static':
63
155
  return HASTBuilders.createTextNode(StringUtils.encode(node.content.toString()))
64
156
 
@@ -66,24 +158,247 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
66
158
  return HASTBuilders.createHTMLNode(node.type)
67
159
 
68
160
  case 'element':
69
- return generatElementNode(
161
+ const elementNode = await generateElementNode(
70
162
  node,
71
- templatesLookUp,
163
+ compName,
164
+ nodesLookup,
72
165
  propDefinitions,
73
166
  stateDefinitions,
74
167
  subComponentOptions,
75
- structure
168
+ structure,
169
+ resolvedExpressions
76
170
  )
171
+ return elementNode
77
172
 
78
173
  case 'dynamic':
79
- return generateDynamicNode(
174
+ const dynamicNode = await generateDynamicNode(
80
175
  node,
81
- templatesLookUp,
176
+ compName,
177
+ nodesLookup,
178
+ propDefinitions,
179
+ stateDefinitions,
180
+ subComponentOptions,
181
+ structure,
182
+ resolvedExpressions
183
+ )
184
+ return dynamicNode
185
+
186
+ case 'repeat':
187
+ const repeatNode = await generateHtmlSyntax(
188
+ node.content.node,
189
+ compName,
190
+ nodesLookup,
82
191
  propDefinitions,
83
192
  stateDefinitions,
84
193
  subComponentOptions,
85
- structure
194
+ structure,
195
+ resolvedExpressions
86
196
  )
197
+ return repeatNode
198
+
199
+ case 'conditional':
200
+ const conditionalNodeComment = HASTBuilders.createTextNode('')
201
+ const {
202
+ value: staticValue,
203
+ reference,
204
+ condition: { conditions, matchingCriteria },
205
+ } = node.content
206
+
207
+ if (reference.type !== 'dynamic') {
208
+ return conditionalNodeComment
209
+ }
210
+
211
+ const {
212
+ content: { referenceType, id, refPath = [] },
213
+ } = reference
214
+
215
+ switch (referenceType) {
216
+ case 'prop': {
217
+ const usedProp = propDefinitions[id]
218
+ if (usedProp === undefined || usedProp.defaultValue === undefined) {
219
+ return conditionalNodeComment
220
+ }
221
+ let defaultValue = usedProp.defaultValue
222
+ for (const path of refPath) {
223
+ defaultValue = (defaultValue as Record<string, unknown[]>)?.[path]
224
+ }
225
+
226
+ // If defaultValue is undefined or null after path traversal, use original default
227
+ defaultValue = defaultValue ?? usedProp.defaultValue
228
+
229
+ // Since we know the operand and the default value from the prop.
230
+ // We can try building the condition and check if the condition is true or false.
231
+ // @todo: You can only use a 'value' in UIDL or 'conditions' but not both.
232
+ // UIDL validations need to be improved on this aspect.
233
+ const dynamicConditions = createConditionalStatement(
234
+ staticValue !== undefined ? [{ operand: staticValue, operation: '===' }] : conditions,
235
+ defaultValue
236
+ )
237
+ const matchCondition = matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
238
+ const conditionString = dynamicConditions.join(` ${matchCondition} `)
239
+
240
+ try {
241
+ // tslint:disable-next-line function-constructor
242
+ const isConditionPassing = new Function(`return ${conditionString}`)()
243
+ if (isConditionPassing) {
244
+ return generateHtmlSyntax(
245
+ node.content.node,
246
+ compName,
247
+ nodesLookup,
248
+ propDefinitions,
249
+ stateDefinitions,
250
+ subComponentOptions,
251
+ structure,
252
+ resolvedExpressions
253
+ )
254
+ }
255
+ } catch (error) {
256
+ return conditionalNodeComment
257
+ }
258
+
259
+ return conditionalNodeComment
260
+ }
261
+
262
+ case 'local': {
263
+ if (!resolvedExpressions || resolvedExpressions.currentIndex === undefined) {
264
+ return conditionalNodeComment
265
+ }
266
+
267
+ // Find the matching expression context for this local reference
268
+ const expressionEntries = Object.values(resolvedExpressions.expressions || {})
269
+ let localValue: unknown
270
+ for (const expr of expressionEntries) {
271
+ if (expr && Array.isArray(expr.defaultValue)) {
272
+ const currentItem = expr.defaultValue[resolvedExpressions.currentIndex]
273
+ if (currentItem !== undefined) {
274
+ localValue = currentItem
275
+ for (const path of refPath) {
276
+ localValue = (localValue as Record<string, unknown>)?.[path]
277
+ }
278
+ break
279
+ }
280
+ }
281
+ }
282
+
283
+ if (localValue === undefined) {
284
+ return conditionalNodeComment
285
+ }
286
+
287
+ const localConditions = createConditionalStatement(
288
+ staticValue !== undefined ? [{ operand: staticValue, operation: '===' }] : conditions,
289
+ localValue as UIDLPropDefinition['defaultValue']
290
+ )
291
+ const localMatchCondition = matchingCriteria && matchingCriteria === 'all' ? '&&' : '||'
292
+ const localConditionString = localConditions.join(` ${localMatchCondition} `)
293
+
294
+ try {
295
+ // tslint:disable-next-line function-constructor
296
+ const isLocalConditionPassing = new Function(`return ${localConditionString}`)()
297
+ if (isLocalConditionPassing) {
298
+ return generateHtmlSyntax(
299
+ node.content.node,
300
+ compName,
301
+ nodesLookup,
302
+ propDefinitions,
303
+ stateDefinitions,
304
+ subComponentOptions,
305
+ structure,
306
+ resolvedExpressions
307
+ )
308
+ }
309
+ } catch (error) {
310
+ return conditionalNodeComment
311
+ }
312
+
313
+ return conditionalNodeComment
314
+ }
315
+
316
+ case 'state':
317
+ default:
318
+ return conditionalNodeComment
319
+ }
320
+
321
+ case 'expr':
322
+ const content = node.content.split('?.')
323
+
324
+ if (resolvedExpressions && resolvedExpressions.expressions?.[content[0] || '']) {
325
+ const uidlDynamicRef: UIDLDynamicReference = {
326
+ type: 'dynamic',
327
+ content: {
328
+ referenceType: 'prop',
329
+ refPath: [resolvedExpressions.currentIndex.toString(), ...content.slice(1)],
330
+ id: content[0],
331
+ },
332
+ }
333
+ const generatedNode = await generateDynamicNode(
334
+ uidlDynamicRef,
335
+ compName,
336
+ nodesLookup,
337
+ resolvedExpressions.expressions,
338
+ stateDefinitions,
339
+ subComponentOptions,
340
+ structure
341
+ )
342
+ return generatedNode
343
+ }
344
+
345
+ // Fallback: support simple prop/state expressions outside of repeater context
346
+ if (content[0] && (propDefinitions?.[content[0]] || stateDefinitions?.[content[0]])) {
347
+ const isProp = Boolean(propDefinitions?.[content[0]])
348
+ const uidlDynamicRef: UIDLDynamicReference = {
349
+ type: 'dynamic',
350
+ content: {
351
+ referenceType: isProp ? 'prop' : 'state',
352
+ refPath: content.slice(1),
353
+ id: content[0],
354
+ },
355
+ }
356
+ const generatedNode = await generateDynamicNode(
357
+ uidlDynamicRef,
358
+ compName,
359
+ nodesLookup,
360
+ isProp ? propDefinitions : stateDefinitions,
361
+ stateDefinitions,
362
+ subComponentOptions,
363
+ structure
364
+ )
365
+ return generatedNode
366
+ }
367
+
368
+ return HASTBuilders.createComment('Expressions are not supported in HTML')
369
+ case 'cms-list-repeater':
370
+ return generateRepeaterNode(
371
+ node,
372
+ compName,
373
+ nodesLookup,
374
+ propDefinitions,
375
+ stateDefinitions,
376
+ subComponentOptions,
377
+ structure,
378
+ resolvedExpressions
379
+ )
380
+
381
+ case 'cms-item':
382
+ case 'cms-list':
383
+ case 'data-source-item':
384
+ case 'data-source-list':
385
+ // For HTML generation, render the success node content
386
+ // Since HTML doesn't support dynamic data fetching, we just render the static structure
387
+ const successNode = (node.content as any).nodes?.success
388
+ if (successNode) {
389
+ return generateHtmlSyntax(
390
+ successNode,
391
+ compName,
392
+ nodesLookup,
393
+ propDefinitions,
394
+ stateDefinitions,
395
+ subComponentOptions,
396
+ structure,
397
+ resolvedExpressions
398
+ )
399
+ }
400
+ // If no success node, return empty div
401
+ return HASTBuilders.createHTMLNode('div')
87
402
 
88
403
  default:
89
404
  throw new HTMLComponentGeneratorError(
@@ -96,14 +411,269 @@ export const generateHtmlSynatx: NodeToHTML<UIDLNode, Promise<HastNode | HastTex
96
411
  }
97
412
  }
98
413
 
99
- const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastText>> = async (
414
+ const createConditionalStatement = (
415
+ conditions: UIDLConditionalNode['content']['condition']['conditions'],
416
+ leftOperand: UIDLPropDefinition['defaultValue']
417
+ ) => {
418
+ return conditions.map((condition) => {
419
+ const { operation, operand } = condition
420
+
421
+ if (operand === undefined) {
422
+ return `${ASTUtils.convertToUnaryOperator(operation)}${getValueType(operand)}`
423
+ }
424
+
425
+ return `${getValueType(leftOperand)} ${ASTUtils.convertToBinaryOperator(
426
+ operation
427
+ )} ${getValueType(operand)}`
428
+ })
429
+ }
430
+
431
+ const getValueType = (value: UIDLPropDefinition['defaultValue']) => {
432
+ const valueType = typeof value
433
+ switch (valueType) {
434
+ case 'string':
435
+ return `"${value}"`
436
+ case 'number':
437
+ return value
438
+ case 'boolean':
439
+ return value
440
+ case 'object':
441
+ // `typeof null === 'object'` — render as the null literal so comparisons
442
+ // against a missing/null default evaluate sensibly.
443
+ if (value === null) {
444
+ return 'null'
445
+ }
446
+ // Handle dynamic references (local, prop, state)
447
+ if (value && typeof value === 'object' && 'type' in value) {
448
+ const dynamicValue = value as unknown as UIDLDynamicReference
449
+ if (dynamicValue.type === 'dynamic' && dynamicValue.content) {
450
+ const { referenceType, id, refPath } = dynamicValue.content
451
+ if (referenceType === 'local' && id) {
452
+ // Local reference from repeater context
453
+ if (refPath && refPath.length > 0) {
454
+ return `${id}.${refPath.join('.')}`
455
+ }
456
+ return id
457
+ } else if (referenceType === 'prop' || referenceType === 'state') {
458
+ // Prop or state reference
459
+ const key = refPath && refPath.length > 0 ? refPath.join('.') : id
460
+ return key
461
+ }
462
+ }
463
+ }
464
+ // Handle link-type prop default values ({ url, newTab }). Collapse to the
465
+ // url string so comparisons like `mapUrl !== '--'` evaluate sensibly.
466
+ if (
467
+ value &&
468
+ typeof value === 'object' &&
469
+ 'url' in (value as Record<string, unknown>) &&
470
+ typeof (value as Record<string, unknown>).url === 'string'
471
+ ) {
472
+ return `"${(value as Record<string, unknown>).url}"`
473
+ }
474
+ throw new HTMLComponentGeneratorError(
475
+ `Conditional node received an operand of type ${valueType} \n
476
+ Received ${JSON.stringify(value)}`
477
+ )
478
+ default:
479
+ throw new HTMLComponentGeneratorError(
480
+ `Conditional node received an operand of type ${valueType} \n
481
+ Received ${JSON.stringify(value)}`
482
+ )
483
+ }
484
+ }
485
+
486
+ const generateRepeaterNode: NodeToHTML<
487
+ UIDLCMSListRepeaterNode,
488
+ Promise<HastNode | HastText>
489
+ > = async (
490
+ node,
491
+ compName,
492
+ nodesLookup,
493
+ propDefinitions,
494
+ stateDefinitions,
495
+ subComponentOptions,
496
+ structure,
497
+ resolvedExpressions
498
+ ) => {
499
+ const { nodes } = node.content
500
+
501
+ const contextId = node.content.renderPropIdentifier
502
+ const sourceValue = node.content.source
503
+ let propDef =
504
+ sourceValue && typeof sourceValue === 'string'
505
+ ? propDefinitions[
506
+ Object.keys(propDefinitions).find((propKey) => sourceValue.includes(propKey)) || ''
507
+ ]
508
+ : undefined
509
+
510
+ /*
511
+ * When we have a nested repeater (e.g. source = "context_yu137?.list || []"),
512
+ * `propDef` points to the parent context's full array. We need to resolve the
513
+ * nested path (e.g. ".list") from each item in the parent iteration instead.
514
+ */
515
+ let nestedPath: string[] | null = null
516
+ if (propDef && resolvedExpressions && sourceValue && typeof sourceValue === 'string') {
517
+ const parentContextKey = Object.keys(resolvedExpressions.expressions || {}).find((key) =>
518
+ sourceValue.includes(key)
519
+ )
520
+ if (parentContextKey) {
521
+ const parentPropDef = resolvedExpressions.expressions[parentContextKey]
522
+ // Extract the nested path from the source expression (e.g. "context_yu137?.list || []" → ["list"])
523
+ const pathMatch = sourceValue.match(
524
+ new RegExp(`${parentContextKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}((?:\\?\\.\\w+)+)`)
525
+ )
526
+ if (pathMatch && pathMatch[1]) {
527
+ nestedPath = pathMatch[1].split('?.').filter(Boolean)
528
+ }
529
+
530
+ if (nestedPath && parentPropDef && Array.isArray(parentPropDef.defaultValue)) {
531
+ const parentItem = parentPropDef.defaultValue[resolvedExpressions.currentIndex]
532
+ if (parentItem && typeof parentItem === 'object' && !Array.isArray(parentItem)) {
533
+ const nestedValue = nestedPath.reduce(
534
+ (acc: unknown, key: string) =>
535
+ acc && typeof acc === 'object' && !Array.isArray(acc)
536
+ ? (acc as Record<string, unknown>)[key]
537
+ : acc,
538
+ parentItem
539
+ )
540
+ if (Array.isArray(nestedValue)) {
541
+ propDef = {
542
+ defaultValue: nestedValue,
543
+ id: contextId,
544
+ type: 'array',
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }
550
+ }
551
+
552
+ if (!propDef || !Array.isArray(propDef.defaultValue)) {
553
+ // If no prop is found we might have a static source value
554
+ try {
555
+ const parsedSource = JSON.parse(sourceValue)
556
+ propDef = {
557
+ defaultValue: parsedSource,
558
+ id: contextId,
559
+ type: 'array',
560
+ }
561
+ } catch {
562
+ // Silent fail
563
+ }
564
+ }
565
+
566
+ // We do the check again to keep typescript happy, otherwise this could be in catch
567
+ if (!propDef || !Array.isArray(propDef.defaultValue)) {
568
+ return HASTBuilders.createComment(
569
+ 'CMS Array Mapper/Repeater not supported in HTML without a prop source'
570
+ )
571
+ }
572
+ propDefinitions[contextId] = propDef
573
+
574
+ const elementNode = HASTBuilders.createHTMLNode('div')
575
+ node.content.nodes.list.content.style = { display: { type: 'static', content: 'contents' } }
576
+ if (node.content.nodes.empty) {
577
+ node.content.nodes.empty.content.style = { display: { type: 'static', content: 'contents' } }
578
+ }
579
+ if (node.content.nodes.loading) {
580
+ node.content.nodes.loading.content.style = { display: { type: 'static', content: 'contents' } }
581
+ }
582
+ // Empty case
583
+ if (propDef.defaultValue.length === 0) {
584
+ const emptyChildren = nodes.empty?.content.children
585
+ if (emptyChildren) {
586
+ for (const child of emptyChildren) {
587
+ const childTag = await generateHtmlSyntax(
588
+ child,
589
+ compName,
590
+ nodesLookup,
591
+ propDefinitions,
592
+ stateDefinitions,
593
+ subComponentOptions,
594
+ structure
595
+ )
596
+
597
+ if (typeof childTag === 'string') {
598
+ HASTUtils.addTextNode(elementNode, childTag)
599
+ } else {
600
+ HASTUtils.addChildNode(elementNode, childTag as HastNode)
601
+ }
602
+ }
603
+ }
604
+
605
+ if (nodes.empty) {
606
+ addNodeToLookup(`${node.content.nodes.empty.content.key}`, elementNode, nodesLookup)
607
+ }
608
+ return elementNode
609
+ }
610
+
611
+ const listChildren = nodes.list.content.children
612
+ if (listChildren) {
613
+ for (let index = 0; index < propDef.defaultValue.length; index++) {
614
+ for (const child of listChildren) {
615
+ const childTag = await generateHtmlSyntax(
616
+ child,
617
+ compName,
618
+ nodesLookup,
619
+ propDefinitions,
620
+ stateDefinitions,
621
+ subComponentOptions,
622
+ structure,
623
+ { currentIndex: index, expressions: { [contextId]: propDef } }
624
+ )
625
+
626
+ if (typeof childTag === 'string') {
627
+ HASTUtils.addTextNode(elementNode, childTag)
628
+ } else {
629
+ HASTUtils.addChildNode(elementNode, childTag as HastNode)
630
+ }
631
+ }
632
+ }
633
+ }
634
+
635
+ addNodeToLookup(`${node.content.nodes.list.content.key}`, elementNode, nodesLookup)
636
+ return elementNode
637
+ }
638
+
639
+ const generateElementNode: NodeToHTML<
640
+ UIDLElementNode,
641
+ Promise<HastNode | HastText | Array<HastNode | HastText>>
642
+ > = async (
100
643
  node,
101
- templatesLookUp,
644
+ compName,
645
+ nodesLookup,
102
646
  propDefinitions,
103
647
  stateDefinitions,
104
648
  subComponentOptions,
105
- structure
649
+ structure,
650
+ resolvedExpressions
106
651
  ) => {
652
+ // Check if this is a wrapped data-source node
653
+ if (
654
+ node.content &&
655
+ typeof node.content === 'object' &&
656
+ 'type' in node.content &&
657
+ (node.content.type === 'data-source-item' || node.content.type === 'data-source-list')
658
+ ) {
659
+ // Handle as data-source node - just render the success content
660
+ const successNode = (node.content as any).nodes?.success
661
+ if (successNode) {
662
+ return generateHtmlSyntax(
663
+ successNode,
664
+ compName,
665
+ nodesLookup,
666
+ propDefinitions,
667
+ stateDefinitions,
668
+ subComponentOptions,
669
+ structure,
670
+ resolvedExpressions
671
+ )
672
+ }
673
+ // If no success node, return empty div
674
+ return HASTBuilders.createHTMLNode('div')
675
+ }
676
+
107
677
  const {
108
678
  elementType,
109
679
  children,
@@ -111,43 +681,45 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
111
681
  style = {},
112
682
  referencedStyles = {},
113
683
  dependency,
114
- key,
115
684
  } = node.content
116
-
117
- const elementNode = HASTBuilders.createHTMLNode(elementType)
118
- templatesLookUp[key] = elementNode
119
-
120
685
  const { dependencies } = structure
121
- if (dependency && (dependency as UIDLDependency)?.type !== 'local') {
122
- dependencies[dependency.path] = dependency
123
- }
124
-
125
686
  if (dependency && (dependency as UIDLDependency)?.type === 'local') {
126
687
  const compTag = await generateComponentContent(
127
688
  node,
689
+ nodesLookup,
128
690
  propDefinitions,
129
691
  stateDefinitions,
130
692
  subComponentOptions,
131
- structure
693
+ structure,
694
+ resolvedExpressions
132
695
  )
696
+
697
+ if ('tagName' in compTag) {
698
+ compTag.children.unshift(HASTBuilders.createComment(`${node.content.semanticType} component`))
699
+ }
700
+
133
701
  return compTag
134
702
  }
135
703
 
704
+ if (dependency && (dependency as UIDLDependency)?.type !== 'local') {
705
+ dependencies[dependency.path] = dependency
706
+ }
707
+
708
+ const elementNode = HASTBuilders.createHTMLNode(elementType)
709
+
136
710
  if (children) {
137
711
  for (const child of children) {
138
- const childTag = await generateHtmlSynatx(
712
+ const childTag = await generateHtmlSyntax(
139
713
  child,
140
- templatesLookUp,
714
+ compName,
715
+ nodesLookup,
141
716
  propDefinitions,
142
717
  stateDefinitions,
143
718
  subComponentOptions,
144
- structure
719
+ structure,
720
+ resolvedExpressions
145
721
  )
146
722
 
147
- if (!childTag) {
148
- return
149
- }
150
-
151
723
  if (typeof childTag === 'string') {
152
724
  HASTUtils.addTextNode(elementNode, childTag)
153
725
  } else {
@@ -160,61 +732,87 @@ const generatElementNode: NodeToHTML<UIDLElementNode, Promise<HastNode | HastTex
160
732
  Object.keys(referencedStyles).forEach((styleRef) => {
161
733
  const refStyle = referencedStyles[styleRef]
162
734
  if (refStyle.content.mapType === 'inlined') {
163
- handleStyles(node, refStyle.content.styles, propDefinitions, stateDefinitions)
164
- return
735
+ handleStyles(
736
+ node,
737
+ refStyle.content.styles,
738
+ propDefinitions,
739
+ stateDefinitions,
740
+ structure.options
741
+ )
165
742
  }
166
743
  })
167
744
  }
168
745
 
169
746
  if (Object.keys(style).length > 0) {
170
- handleStyles(node, style, propDefinitions, stateDefinitions)
747
+ handleStyles(node, style, propDefinitions, stateDefinitions, structure.options)
171
748
  }
172
749
 
173
- if (Object.keys(attrs).length > 0) {
174
- handleAttributes(
175
- elementType,
176
- elementNode,
177
- attrs,
178
- propDefinitions,
179
- stateDefinitions,
180
- structure.options.projectRouteDefinition,
181
- structure.outputOptions
182
- )
183
- }
750
+ handleAttributes(
751
+ elementType,
752
+ elementNode,
753
+ attrs,
754
+ propDefinitions,
755
+ stateDefinitions,
756
+ structure.options.projectRouteDefinition,
757
+ structure.outputOptions,
758
+ structure.options,
759
+ resolvedExpressions?.currentIndex
760
+ )
184
761
 
762
+ addNodeToLookup(node.content.key, elementNode, nodesLookup)
185
763
  return elementNode
186
764
  }
187
765
 
766
+ const createLookupTable = (
767
+ component: ComponentUIDL,
768
+ nodesLookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>
769
+ ): ElementsLookup => {
770
+ const lookup: ElementsLookup = {}
771
+ for (const node of Object.keys(nodesLookup)) {
772
+ lookup[node] = {
773
+ count: 1,
774
+ nextKey: '1',
775
+ }
776
+ }
777
+ createNodesLookup(component, lookup)
778
+ return lookup
779
+ }
780
+
188
781
  const generateComponentContent = async (
189
782
  node: UIDLElementNode,
783
+ nodesLookup: Record<string, HastNode | HastText | Array<HastNode | HastText>>,
190
784
  propDefinitions: Record<string, UIDLPropDefinition>,
191
785
  stateDefinitions: Record<string, UIDLStateDefinition>,
192
786
  subComponentOptions: {
193
787
  externals: Record<string, ComponentUIDL>
194
788
  plugins: ComponentPlugin[]
789
+ standaloneHtmlComponents?: boolean
195
790
  },
196
791
  structure: {
197
792
  chunks: ChunkDefinition[]
198
793
  dependencies: Record<string, UIDLDependency>
199
794
  options: GeneratorOptions
200
795
  outputOptions: UIDLComponentOutputOptions
796
+ },
797
+ resolvedExpressions?: {
798
+ expressions: Record<string, UIDLPropDefinition>
799
+ currentIndex: number
201
800
  }
202
801
  ) => {
203
- const { externals, plugins } = subComponentOptions
204
- const { elementType, attrs = {}, key, children = [] } = node.content
802
+ const { externals, plugins, standaloneHtmlComponents = false } = subComponentOptions
803
+ const { elementType, attrs = {}, children = [] } = node.content
205
804
  const { dependencies, chunks = [], options } = structure
206
- const comp = UIDLUtils.cloneObject(externals[elementType] || {}) as ComponentUIDL
207
- const lookUpTemplates: Record<string, unknown> = {}
208
- let compHasSlots: boolean = false
209
-
210
- if (!comp || !comp?.node) {
211
- throw new HTMLComponentGeneratorError(`${elementType} is not found from the externals. \n
212
- Received ${JSON.stringify(Object.keys(externals), null, 2)}`)
805
+ // "Component" will not exist when generating a component because the resolver checks for illegal class names
806
+ const componentName = elementType === 'Component' ? 'AppComponent' : elementType
807
+ const component = externals[componentName]
808
+ if (component === undefined) {
809
+ throw new HTMLComponentGeneratorError(`${componentName} is missing from externals object`)
213
810
  }
214
811
 
812
+ const componentClone = UIDLUtils.cloneObject<ComponentUIDL>(component)
813
+
215
814
  if (children.length) {
216
- compHasSlots = true
217
- UIDLUtils.traverseNodes(comp.node, (childNode, parentNode) => {
815
+ UIDLUtils.traverseNodes(componentClone.node, (childNode, parentNode) => {
218
816
  if (childNode.type === 'slot' && parentNode.type === 'element') {
219
817
  const nonSlotNodes = parentNode.content?.children?.filter((n) => n.type !== 'slot')
220
818
  parentNode.content.children = [
@@ -224,6 +822,7 @@ const generateComponentContent = async (
224
822
  content: {
225
823
  key: 'custom-slot',
226
824
  elementType: 'slot',
825
+ name: componentClone.name + 'slot',
227
826
  style: {
228
827
  display: {
229
828
  type: 'static',
@@ -243,31 +842,47 @@ const generateComponentContent = async (
243
842
  node.content.children = []
244
843
  }
245
844
 
246
- const combinedProps = { ...propDefinitions, ...(comp?.propDefinitions || {}) }
247
-
248
- const propsForInstance = Object.keys(combinedProps).reduce(
249
- (acc: Record<string, UIDLPropDefinition>, propKey) => {
250
- if (attrs[propKey]) {
251
- acc[propKey] = {
252
- ...combinedProps[propKey],
253
- defaultValue: attrs[propKey]?.content || combinedProps[propKey]?.defaultValue,
254
- }
255
- } else {
256
- acc[propKey] = combinedProps[propKey]
257
- }
845
+ // In UIDL, we define only the link between a component and a page.
846
+ // We define this link using the UIDLLocalDependency approach.
847
+ // So, during the page resolution step, where we ideally generate the unique keys for the components.
848
+ // We can't generate the unique keys for the components because we don't have the full UIDL of the component.
849
+ // When we are using components in a page, the `addExternalComponents` step of the
850
+ // html-component-generator will add the full UIDL of the component to the externals object after resolving them.
851
+ // But when a component is used multiple number of times, we are basically using the same nodes again and again.
852
+ // Which indivates duplication. So, we create a lookup table of all the nodes present with us in the page
853
+ // And then pass it to the component to avoid any coilissions.
854
+ const lookupTableForCurrentPage = createLookupTable(componentClone, nodesLookup)
855
+ generateUniqueKeys(componentClone, lookupTableForCurrentPage)
258
856
 
259
- return acc
260
- },
261
- {}
262
- )
263
-
264
- const combinedStates = { ...stateDefinitions, ...(comp?.stateDefinitions || {}) }
857
+ // We are combining props of the current component
858
+ // with props of the component that we need to generate.
859
+ // Refer to line 309, for element props. We either pick from the attr of the current instance of component
860
+ // or from the propDefinitions of the component that we are generating.
861
+ // We NEED to keep passing the props of the current component to the child component and so on,
862
+ // so that all props are forwarded.
863
+ const combinedProps: Record<string, UIDLPropDefinition> = {
864
+ ...Object.keys(propDefinitions).reduce<Record<string, UIDLPropDefinition>>(
865
+ (acc: Record<string, UIDLPropDefinition>, propKey) => {
866
+ acc[propKey] = propDefinitions[propKey]
867
+ return acc
868
+ },
869
+ {}
870
+ ),
871
+ ...(componentClone?.propDefinitions || {}),
872
+ }
873
+ const combinedStates = { ...stateDefinitions, ...(componentClone?.stateDefinitions || {}) }
265
874
  const statesForInstance = Object.keys(combinedStates).reduce(
266
875
  (acc: Record<string, UIDLStateDefinition>, propKey) => {
267
- if (attrs[propKey]) {
876
+ const attr = attrs[propKey]
877
+
878
+ if (attr?.type === 'object') {
879
+ throw new Error(`Object attributes are not supported in html exports`)
880
+ }
881
+
882
+ if (attr) {
268
883
  acc[propKey] = {
269
884
  ...combinedStates[propKey],
270
- defaultValue: attrs[propKey]?.content || combinedStates[propKey]?.defaultValue,
885
+ defaultValue: attr?.content ?? combinedStates[propKey]?.defaultValue,
271
886
  }
272
887
  } else {
273
888
  acc[propKey] = combinedStates[propKey]
@@ -278,42 +893,150 @@ const generateComponentContent = async (
278
893
  {}
279
894
  )
280
895
 
281
- const elementNode = HASTBuilders.createHTMLNode(StringUtils.camelCaseToDashCase(elementType))
282
- lookUpTemplates[key] = elementNode
283
-
284
- const compTag = (await generateHtmlSynatx(
285
- {
286
- ...comp.node,
287
- content: {
288
- ...comp.node.content,
289
- style: {
290
- ...(comp.node.content?.style || {}),
291
- display: {
292
- type: 'static',
293
- content: 'contents',
294
- },
896
+ const propsForInstance: Record<string, UIDLPropDefinition> = {}
897
+ // this is where we check if the component we are conusming is actually passing any props to the instance.
898
+ // 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
899
+ // the component instance that we are using here.
900
+ for (const propKey of Object.keys(combinedProps)) {
901
+ const attribute = attrs[propKey]
902
+
903
+ if (attribute?.type === 'element') {
904
+ propsForInstance[propKey] = {
905
+ ...combinedProps[propKey],
906
+ defaultValue: attrs[propKey],
907
+ }
908
+ }
909
+
910
+ if (attribute?.type === 'dynamic') {
911
+ // When we are using a component instance in a component and the attribute
912
+ // that is passed to the component is of dynamic reference.
913
+ // If means, the component is redirecting the prop that is received to the prop of the component that it is consuming.
914
+ // 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.
915
+ // And similary we do the same for the states.
916
+ switch (attribute.content.referenceType) {
917
+ case 'prop':
918
+ propsForInstance[propKey] = combinedProps[attribute.content.id]
919
+ break
920
+ case 'state':
921
+ propsForInstance[propKey] = combinedStates[attribute.content.id]
922
+ break
923
+ case 'expr':
924
+ // Ignore expr type attributes in html comp instances for the time being.
925
+ break
926
+ default:
927
+ throw new Error(
928
+ `ReferenceType ${attribute.content.referenceType} is not supported in HTML Export.`
929
+ )
930
+ }
931
+ }
932
+
933
+ if (attribute?.type === 'object') {
934
+ propsForInstance[propKey] = {
935
+ ...combinedProps[propKey],
936
+ defaultValue: (attribute?.content as object) || combinedProps[propKey]?.defaultValue,
937
+ }
938
+ }
939
+
940
+ if (attribute?.type === 'expr') {
941
+ const [ctxId, ...refPath] = attribute.content.split('?.')
942
+ const propKeyFromAttr = Object.keys(combinedProps).find((key) => key === ctxId)
943
+
944
+ const resolvedValue = combinedProps[propKeyFromAttr]
945
+ const hasRefPath = refPath.length > 0
946
+ // Build the path using repeater index when available
947
+ const fullRefPath =
948
+ typeof resolvedExpressions?.currentIndex === 'number' && hasRefPath
949
+ ? [resolvedExpressions.currentIndex.toString(), ...refPath]
950
+ : refPath
951
+
952
+ if (Array.isArray(resolvedValue)) {
953
+ // If the resolved value itself is an array definition, pass it through
954
+ propsForInstance[propKey] = resolvedValue
955
+ } else {
956
+ const defaultVal = resolvedValue?.defaultValue
957
+ const extracted =
958
+ hasRefPath && defaultVal !== undefined
959
+ ? extractDefaultValueFromRefPath(defaultVal, fullRefPath)
960
+ : defaultVal ?? null
961
+ propsForInstance[propKey] = {
962
+ ...resolvedValue,
963
+ defaultValue: extracted,
964
+ }
965
+ }
966
+ }
967
+
968
+ if (
969
+ attribute?.type !== 'dynamic' &&
970
+ attribute?.type !== 'element' &&
971
+ attribute?.type !== 'object' &&
972
+ attribute?.type !== 'expr'
973
+ ) {
974
+ propsForInstance[propKey] = {
975
+ ...combinedProps[propKey],
976
+ defaultValue: attribute?.content ?? combinedProps[propKey]?.defaultValue,
977
+ }
978
+ }
979
+
980
+ if (attribute === undefined) {
981
+ const propFromCurrentComponent = combinedProps[propKey]
982
+ propsForInstance[propKey] = propFromCurrentComponent
983
+ }
984
+ }
985
+
986
+ let componentWrapper = StringUtils.camelCaseToDashCase(`${componentName}-wrapper`)
987
+ const isExistingNode = nodesLookup[componentWrapper]
988
+ if (isExistingNode !== undefined) {
989
+ componentWrapper = `${componentWrapper}-${StringUtils.generateRandomString()}`
990
+ }
991
+
992
+ // Transfer data-node attributes from component instance to wrapper
993
+ const wrapperAttrs: Record<string, UIDLAttributeValue> = {}
994
+ Object.keys(attrs).forEach((attrKey) => {
995
+ if (attrKey.startsWith('dataNode') || attrKey.startsWith('data-node')) {
996
+ wrapperAttrs[attrKey] = attrs[attrKey]
997
+ }
998
+ })
999
+
1000
+ const componentInstanceToGenerate: UIDLElementNode = {
1001
+ type: 'element',
1002
+ content: {
1003
+ elementType: componentWrapper,
1004
+ key: componentWrapper,
1005
+ children: [componentClone.node],
1006
+ attrs: wrapperAttrs,
1007
+ style: {
1008
+ display: {
1009
+ type: 'static',
1010
+ content: 'contents',
295
1011
  },
296
1012
  },
297
1013
  },
298
- lookUpTemplates,
1014
+ }
1015
+
1016
+ const compTag = await generateHtmlSyntax(
1017
+ componentInstanceToGenerate,
1018
+ component.name,
1019
+ nodesLookup,
299
1020
  propsForInstance,
300
1021
  statesForInstance,
301
1022
  subComponentOptions,
302
- structure
303
- )) as unknown as HastNode
1023
+ structure,
1024
+ resolvedExpressions
1025
+ )
304
1026
 
305
1027
  const cssPlugin = createCSSPlugin({
306
1028
  templateStyle: 'html',
307
1029
  templateChunkName: DEFAULT_COMPONENT_CHUNK_NAME,
308
- declareDependency: 'import',
309
- forceScoping: true,
310
- chunkName: comp.name,
1030
+ declareDependency: standaloneHtmlComponents ? 'none' : 'import',
1031
+ chunkName: componentClone.name,
311
1032
  staticPropReferences: true,
1033
+ standaloneHtmlComponents,
312
1034
  })
313
1035
 
314
1036
  const initialStructure: ComponentStructure = {
315
1037
  uidl: {
316
- ...comp,
1038
+ ...componentClone,
1039
+ node: componentInstanceToGenerate,
317
1040
  propDefinitions: propsForInstance,
318
1041
  stateDefinitions: statesForInstance,
319
1042
  },
@@ -325,7 +1048,7 @@ const generateComponentContent = async (
325
1048
  linkAfter: [],
326
1049
  content: compTag,
327
1050
  meta: {
328
- nodesLookup: lookUpTemplates,
1051
+ nodesLookup,
329
1052
  },
330
1053
  },
331
1054
  ],
@@ -341,41 +1064,132 @@ const generateComponentContent = async (
341
1064
  Promise.resolve(initialStructure)
342
1065
  )
343
1066
 
344
- if (compHasSlots) {
345
- result.chunks.forEach((chunk) => {
346
- if (chunk.fileType === FileType.CSS) {
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)
1067
+ result.chunks.forEach((chunk) => {
1068
+ if (chunk.fileType === FileType.CSS) {
1069
+ chunks.push(chunk)
360
1070
  }
361
- }
1071
+ })
362
1072
 
1073
+ addNodeToLookup(node.content.key, compTag, nodesLookup)
363
1074
  return compTag
364
1075
  }
365
1076
 
366
- const generateDynamicNode: NodeToHTML<UIDLDynamicReference, HastNode> = (
1077
+ const generateDynamicNode: NodeToHTML<
1078
+ UIDLDynamicReference,
1079
+ Promise<HastNode | HastText | Array<HastNode | HastText>>
1080
+ > = async (
367
1081
  node,
368
- _,
1082
+ compName,
1083
+ nodesLookup,
369
1084
  propDefinitions,
370
- stateDefinitions
371
- ) => {
372
- const spanTag = HASTBuilders.createHTMLNode('span')
373
- const usedReferenceValue =
374
- node.content.referenceType === 'prop'
375
- ? getValueFromReference(node.content.id, propDefinitions)
376
- : getValueFromReference(node.content.id, stateDefinitions)
1085
+ stateDefinitions,
1086
+ subComponentOptions,
1087
+ structure,
1088
+ resolvedExpressions?
1089
+ ): Promise<HastNode | HastText | Array<HastNode | HastText>> => {
1090
+ if (node.content.referenceType === 'locale') {
1091
+ const translation = getTranslation(node.content.id, structure.options)
1092
+ if (translation) {
1093
+ if (translation.type === 'static') {
1094
+ return HASTBuilders.createTextNode(String(translation.content))
1095
+ }
1096
+ if (translation.type === 'element') {
1097
+ return generateHtmlSyntax(
1098
+ translation,
1099
+ compName,
1100
+ nodesLookup,
1101
+ propDefinitions,
1102
+ stateDefinitions,
1103
+ subComponentOptions,
1104
+ structure
1105
+ )
1106
+ }
1107
+ }
1108
+ const localeTag = HASTBuilders.createHTMLNode('span')
1109
+ const commentNode = HASTBuilders.createComment(`Content for locale ${node.content.id}`)
1110
+ HASTUtils.addChildNode(localeTag, commentNode)
1111
+ return localeTag
1112
+ }
1113
+
1114
+ if (
1115
+ node.content.referenceType === 'global' ||
1116
+ (node.content.referenceType as string) === 'globalState'
1117
+ ) {
1118
+ const globalTag = HASTBuilders.createHTMLNode('span')
1119
+ const commentNode = HASTBuilders.createComment(`Global reference: ${node.content.id}`)
1120
+ HASTUtils.addChildNode(globalTag, commentNode)
1121
+ return globalTag
1122
+ }
1123
+
1124
+ const usedReferenceValue = getValueFromReference(
1125
+ node.content.id,
1126
+ node.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
1127
+ )
1128
+
1129
+ if (
1130
+ (usedReferenceValue.type === 'object' || usedReferenceValue.type === 'array') &&
1131
+ usedReferenceValue.defaultValue
1132
+ ) {
1133
+ // Let's say users are biding the prop to a node using something like this "fields.Title"
1134
+ // But the fields in the object is the value where the object is defined either in propDefinitions
1135
+ // or on the attrs. So, we just need to parsed the rest of the object path and get the value from the object.
1136
+ const extracted = extractDefaultValueFromRefPath(
1137
+ usedReferenceValue.defaultValue as Record<string, UIDLPropDefinition>,
1138
+ node.content.refPath
1139
+ )
1140
+ if (extracted === undefined || extracted === null) {
1141
+ return HASTBuilders.createTextNode('')
1142
+ }
1143
+ return HASTBuilders.createTextNode(String(extracted))
1144
+ }
1145
+
1146
+ if (usedReferenceValue.type === 'element') {
1147
+ const elementNode = usedReferenceValue.defaultValue as UIDLElementNode
1148
+ if (elementNode) {
1149
+ // In repeater context, avoid reusing cached nodes; uniquify key per iteration
1150
+ if (resolvedExpressions && typeof resolvedExpressions.currentIndex === 'number') {
1151
+ const elementClone = UIDLUtils.cloneObject<UIDLElementNode>(elementNode)
1152
+ if (elementClone?.content?.key) {
1153
+ elementClone.content.key = `${elementClone.content.key}-${resolvedExpressions.currentIndex}`
1154
+ }
1155
+ const iterElementTag = await generateHtmlSyntax(
1156
+ elementClone,
1157
+ compName,
1158
+ nodesLookup,
1159
+ propDefinitions,
1160
+ stateDefinitions,
1161
+ subComponentOptions,
1162
+ structure,
1163
+ resolvedExpressions
1164
+ )
1165
+ return iterElementTag
1166
+ }
1167
+
1168
+ if (elementNode.content.key in nodesLookup) {
1169
+ return nodesLookup[elementNode.content.key]
1170
+ }
1171
+
1172
+ const generatedElementTag = await generateHtmlSyntax(
1173
+ elementNode,
1174
+ compName,
1175
+ nodesLookup,
1176
+ propDefinitions,
1177
+ stateDefinitions,
1178
+ subComponentOptions,
1179
+ structure,
1180
+ resolvedExpressions
1181
+ )
1182
+ return generatedElementTag
1183
+ }
377
1184
 
378
- HASTUtils.addTextNode(spanTag, String(usedReferenceValue))
1185
+ const spanTagWrapper = HASTBuilders.createHTMLNode('span')
1186
+ const commentNode = HASTBuilders.createComment(`Content for slot ${node.content.id}`)
1187
+ HASTUtils.addChildNode(spanTagWrapper, commentNode)
1188
+ return spanTagWrapper
1189
+ }
1190
+
1191
+ const spanTag = HASTBuilders.createHTMLNode('span')
1192
+ HASTUtils.addTextNode(spanTag, String(usedReferenceValue.defaultValue))
379
1193
  return spanTag
380
1194
  }
381
1195
 
@@ -383,15 +1197,35 @@ const handleStyles = (
383
1197
  node: UIDLElementNode,
384
1198
  styles: UIDLStyleDefinitions,
385
1199
  propDefinitions: Record<string, UIDLPropDefinition>,
386
- stateDefinitions: Record<string, UIDLStateDefinition>
1200
+ stateDefinitions: Record<string, UIDLStateDefinition>,
1201
+ options: GeneratorOptions
387
1202
  ) => {
388
1203
  Object.keys(styles).forEach((styleKey) => {
389
1204
  let style: string | UIDLStyleValue = styles[styleKey]
390
1205
  if (style.type === 'dynamic' && style.content?.referenceType !== 'token') {
391
- if (style.content.referenceType === 'prop') {
392
- style = getValueFromReference(style.content.id, propDefinitions)
393
- } else if (style.content.referenceType === 'state') {
394
- style = getValueFromReference(style.content.id, stateDefinitions)
1206
+ if (style.content?.referenceType === 'locale') {
1207
+ const translation = getTranslation(style.content.id, options)
1208
+ const resolvedText = translation
1209
+ ? resolveTranslationText(translation)
1210
+ : `[locale: ${style.content.id}]`
1211
+ node.content.style[styleKey] = staticNode(resolvedText)
1212
+ return
1213
+ }
1214
+ if (
1215
+ style.content?.referenceType === 'global' ||
1216
+ (style.content?.referenceType as string) === 'globalState'
1217
+ ) {
1218
+ node.content.style[styleKey] = staticNode(`[global: ${style.content.id}]`)
1219
+ return
1220
+ }
1221
+ const referencedValue = getValueFromReference(
1222
+ style.content.id,
1223
+ style.content.referenceType === 'prop' ? propDefinitions : stateDefinitions
1224
+ )
1225
+ if (referencedValue.type === 'string' || referencedValue.type === 'number') {
1226
+ style = String(
1227
+ extractDefaultValueFromRefPath(referencedValue.defaultValue, style?.content?.refPath)
1228
+ )
395
1229
  }
396
1230
  node.content.style[styleKey] = typeof style === 'string' ? staticNode(style) : style
397
1231
  }
@@ -405,102 +1239,177 @@ const handleAttributes = (
405
1239
  propDefinitions: Record<string, UIDLPropDefinition>,
406
1240
  stateDefinitions: Record<string, UIDLStateDefinition>,
407
1241
  routeDefinitions: UIDLRouteDefinitions,
408
- outputOptions: UIDLComponentOutputOptions
1242
+ outputOptions: UIDLComponentOutputOptions,
1243
+ options: GeneratorOptions,
1244
+ currentIndex?: number
409
1245
  ) => {
410
- Object.keys(attrs).forEach((attrKey) => {
1246
+ for (const attrKey of Object.keys(attrs)) {
411
1247
  const attrValue = attrs[attrKey]
1248
+ const { type, content } = attrValue
412
1249
 
413
- if (
414
- attrKey === 'href' &&
415
- attrValue.type === 'static' &&
416
- typeof attrValue.content === 'string' &&
417
- attrValue.content.startsWith('/')
418
- ) {
419
- let targetLink
1250
+ switch (type) {
1251
+ case 'static': {
1252
+ if (attrKey === 'href' && typeof content === 'string' && content.startsWith('/')) {
1253
+ let targetLink
420
1254
 
421
- const targetRoute = (routeDefinitions?.values || []).find(
422
- (route) => route.pageOptions.navLink === attrValue.content
423
- )
1255
+ const targetRoute = (routeDefinitions?.values || []).find(
1256
+ (route) => route.pageOptions.navLink === content
1257
+ )
424
1258
 
425
- if (targetRoute) {
426
- targetLink = targetRoute.pageOptions.navLink
427
- }
1259
+ if (targetRoute) {
1260
+ targetLink = targetRoute.pageOptions.navLink
1261
+ }
428
1262
 
429
- if (!targetRoute && attrValue.content === '/home') {
430
- targetLink = '/'
431
- }
1263
+ if (!targetRoute && content === '/home') {
1264
+ targetLink = '/'
1265
+ }
1266
+
1267
+ if (!targetLink && !targetRoute) {
1268
+ targetLink = content
1269
+ }
1270
+
1271
+ const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
1272
+ const localPrefix = relative(
1273
+ `/${currentPageRoute}`,
1274
+ `/${targetLink === '/' ? 'index' : targetLink}`
1275
+ )
1276
+
1277
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
1278
+ break
1279
+ }
432
1280
 
433
- if (!targetLink && !targetRoute) {
434
- targetLink = attrValue.content
1281
+ if (typeof content === 'boolean') {
1282
+ htmlNode.properties[attrKey] = content === true ? 'true' : 'false'
1283
+ } else if (typeof content === 'string' || typeof attrValue.content === 'number') {
1284
+ let value = StringUtils.encode(String(attrValue.content))
1285
+
1286
+ /*
1287
+ elementType of image is always mapped to img.
1288
+ For reference, check `html-mapping` file.
1289
+ */
1290
+ if (
1291
+ (elementType === 'img' || elementType === 'video') &&
1292
+ attrKey === 'src' &&
1293
+ !isValidURL(value)
1294
+ ) {
1295
+ /*
1296
+ By default we just prefix all the asset paths with just the
1297
+ assetPrefix that is configured in the project. But for `html` generators
1298
+ we need to prefix that with the current file location.
1299
+
1300
+ Because, all the other frameworks have a build setup. which serves all the
1301
+ assets from the `public` folder. But in the case of `html` here is how it works
1302
+
1303
+ We load a file from `index.html` the request for the image goes from
1304
+ '...url.../public/...image...'
1305
+ If it's a nested url, then the request goes from
1306
+ '...url/nested/public/...image..'
1307
+
1308
+ But the nested folder is available only on the root. With this
1309
+ The url changes prefixes to
1310
+
1311
+ ../public/playground_assets/..image.. etc depending on the dept the file is in.
1312
+ */
1313
+ value = join(relative(join(...outputOptions.folderPath), './'), value)
1314
+ }
1315
+
1316
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
1317
+ }
1318
+
1319
+ break
435
1320
  }
436
1321
 
437
- const currentPageRoute = join(...(outputOptions?.folderPath || []), './')
438
- const localPrefix = relative(
439
- `/${currentPageRoute}`,
440
- `/${targetLink === '/' ? 'index' : targetLink}`
441
- )
1322
+ case 'dynamic': {
1323
+ if (content.referenceType === 'locale') {
1324
+ const translation = getTranslation(content.id, options)
1325
+ const resolvedText = translation
1326
+ ? resolveTranslationText(translation)
1327
+ : `[locale: ${content.id}]`
1328
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, resolvedText)
1329
+ break
1330
+ }
442
1331
 
443
- HASTUtils.addAttributeToNode(htmlNode, attrKey, `${localPrefix}.html`)
444
- return
445
- }
1332
+ if (
1333
+ content.referenceType === 'global' ||
1334
+ (content.referenceType as string) === 'globalState'
1335
+ ) {
1336
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, `[global: ${content.id}]`)
1337
+ break
1338
+ }
446
1339
 
447
- if (attrValue.type === 'dynamic') {
448
- const value =
449
- attrValue.content.referenceType === 'prop'
450
- ? getValueFromReference(attrValue.content.id, propDefinitions)
451
- : getValueFromReference(attrValue.content.id, stateDefinitions)
452
- HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value))
453
- return
454
- }
1340
+ const value = getValueFromReference(
1341
+ content.id,
1342
+ content.referenceType === 'prop' ? propDefinitions : stateDefinitions
1343
+ )
455
1344
 
456
- if (attrValue.type === 'raw') {
457
- HASTUtils.addAttributeToNode(htmlNode, attrKey, attrValue.content)
458
- return
459
- }
1345
+ // A `func` prop has no meaningful static HTML representation; skip
1346
+ // rather than emitting the stringified function body as an attribute.
1347
+ if (value.type === 'func') {
1348
+ break
1349
+ }
460
1350
 
461
- if (typeof attrValue.content === 'boolean') {
462
- HASTUtils.addBooleanAttributeToNode(htmlNode, attrKey)
463
- return
464
- } else if (typeof attrValue.content === 'string' || typeof attrValue.content === 'number') {
465
- let value = StringUtils.encode(String(attrValue.content))
1351
+ const extracted = extractDefaultValueFromRefPath(value.defaultValue, content.refPath)
1352
+ const extractedValue = String(extracted)
466
1353
 
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.
1354
+ if (
1355
+ (elementType === 'img' || elementType === 'video') &&
1356
+ attrKey === 'src' &&
1357
+ !extractedValue.startsWith('http')
1358
+ ) {
1359
+ const path = join(relative(join(...outputOptions.folderPath), './'), extractedValue)
1360
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, path)
1361
+ break
1362
+ }
476
1363
 
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
1364
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, extractedValue)
1365
+ break
1366
+ }
479
1367
 
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..'
1368
+ case 'raw': {
1369
+ const rawAttr = attrValue as UIDLRawValue
1370
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, rawAttr.fallback || rawAttr.content)
1371
+ break
1372
+ }
484
1373
 
485
- But the nested folder is available only on the root. With this
486
- The url changes prefixes to
1374
+ case 'expr': {
1375
+ const fullPath = content.split('?.')
1376
+ const prop = propDefinitions[fullPath?.[0] || '']
1377
+
1378
+ if (!prop) {
1379
+ break
1380
+ }
487
1381
 
488
- ../public/playground_assets/..image.. etc depending on the dept the file is in.
489
- */
490
- value = join(relative(join(...outputOptions.folderPath), './'), value)
1382
+ const path =
1383
+ typeof currentIndex === 'number'
1384
+ ? [currentIndex.toString(), ...fullPath.slice(1)]
1385
+ : fullPath.slice(1)
1386
+ const value = extractDefaultValueFromRefPath(prop.defaultValue, path)
1387
+ if (!value) {
1388
+ break
1389
+ }
1390
+ HASTUtils.addAttributeToNode(htmlNode, attrKey, String(value))
1391
+ break
491
1392
  }
492
1393
 
493
- HASTUtils.addAttributeToNode(htmlNode, attrKey, value)
494
- return
1394
+ case 'element':
1395
+ case 'import':
1396
+ case 'object':
1397
+ break
1398
+
1399
+ default: {
1400
+ throw new HTMLComponentGeneratorError(
1401
+ `Received ${JSON.stringify(attrValue, null, 2)} \n in handleAttributes for html`
1402
+ )
1403
+ }
495
1404
  }
496
- })
1405
+ }
497
1406
  }
498
1407
 
499
1408
  const getValueFromReference = (
500
1409
  key: string,
501
1410
  definitions: Record<string, UIDLPropDefinition>
502
- ): string => {
503
- const usedReferenceValue = definitions[key.includes('.') ? key.split('.')[0] : key]
1411
+ ): UIDLPropDefinition | undefined => {
1412
+ const usedReferenceValue = definitions[key.includes('?.') ? key.split('?.')[0] : key]
504
1413
 
505
1414
  if (!usedReferenceValue) {
506
1415
  throw new HTMLComponentGeneratorError(
@@ -508,9 +1417,14 @@ const getValueFromReference = (
508
1417
  )
509
1418
  }
510
1419
 
511
- if (!usedReferenceValue.hasOwnProperty('defaultValue')) {
1420
+ if (
1421
+ usedReferenceValue?.type &&
1422
+ ['string', 'number', 'object', 'element', 'array', 'boolean', 'link', 'func'].includes(
1423
+ usedReferenceValue.type
1424
+ ) === false
1425
+ ) {
512
1426
  throw new HTMLComponentGeneratorError(
513
- `Default value is missing from dynamic reference - ${JSON.stringify(
1427
+ `Attribute is using dynamic value, but received of type ${JSON.stringify(
514
1428
  usedReferenceValue,
515
1429
  null,
516
1430
  2
@@ -518,9 +1432,12 @@ const getValueFromReference = (
518
1432
  )
519
1433
  }
520
1434
 
521
- if (!['string', 'number', 'object'].includes(usedReferenceValue?.type)) {
1435
+ if (
1436
+ usedReferenceValue.type !== 'element' &&
1437
+ usedReferenceValue.hasOwnProperty('defaultValue') === false
1438
+ ) {
522
1439
  throw new HTMLComponentGeneratorError(
523
- `Attribute is using dynamic value, but received of type ${JSON.stringify(
1440
+ `Default value is missing from dynamic reference - ${JSON.stringify(
524
1441
  usedReferenceValue,
525
1442
  null,
526
1443
  2
@@ -528,5 +1445,33 @@ const getValueFromReference = (
528
1445
  )
529
1446
  }
530
1447
 
531
- return String(usedReferenceValue.defaultValue)
1448
+ return usedReferenceValue
1449
+ }
1450
+
1451
+ const extractDefaultValueFromRefPath = (
1452
+ propDefaultValue: PropDefaultValueTypes,
1453
+ refPath?: string[]
1454
+ ): PropDefaultValueTypes => {
1455
+ if (!refPath || refPath.length === 0) {
1456
+ return propDefaultValue
1457
+ }
1458
+
1459
+ // Directly handle array indexing for the first segment when applicable
1460
+ if (Array.isArray(propDefaultValue)) {
1461
+ const [first, ...rest] = refPath
1462
+ const idx = Number(first)
1463
+ if (!Number.isNaN(idx) && idx >= 0 && idx < propDefaultValue.length) {
1464
+ const nextVal = propDefaultValue[idx] as PropDefaultValueTypes
1465
+ if (rest.length === 0) {
1466
+ return nextVal
1467
+ }
1468
+ return extractDefaultValueFromRefPath(nextVal, rest)
1469
+ }
1470
+ }
1471
+
1472
+ if (typeof propDefaultValue !== 'object') {
1473
+ return propDefaultValue
1474
+ }
1475
+
1476
+ return GenericUtils.getValueFromPath(refPath.join('.'), propDefaultValue) as PropDefaultValueTypes
532
1477
  }