roqa 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,871 @@
1
+ import {
2
+ getJSXElementName,
3
+ getJSXChildren,
4
+ isJSXElement,
5
+ isJSXText,
6
+ isJSXExpressionContainer,
7
+ extractJSXAttributes,
8
+ isForComponent,
9
+ isShowComponent,
10
+ isJSXFragment,
11
+ } from "../parser.js";
12
+ import { CONSTANTS, escapeTemplateString, escapeHtml, escapeAttr } from "../utils.js";
13
+
14
+ /**
15
+ * Template extraction and DOM traversal generation
16
+ *
17
+ * This module converts JSX trees into:
18
+ * 1. Static HTML template strings
19
+ * 2. DOM traversal code (firstChild/nextSibling chains)
20
+ * 3. Binding point metadata for dynamic content
21
+ */
22
+
23
+ /**
24
+ * Set of SVG element tag names
25
+ */
26
+ const SVG_ELEMENTS = new Set([
27
+ "svg",
28
+ "circle",
29
+ "ellipse",
30
+ "line",
31
+ "path",
32
+ "polygon",
33
+ "polyline",
34
+ "rect",
35
+ "g",
36
+ "defs",
37
+ "symbol",
38
+ "use",
39
+ "text",
40
+ "tspan",
41
+ "textPath",
42
+ "image",
43
+ "clipPath",
44
+ "mask",
45
+ "pattern",
46
+ "marker",
47
+ "linearGradient",
48
+ "radialGradient",
49
+ "stop",
50
+ "filter",
51
+ "feBlend",
52
+ "feColorMatrix",
53
+ "feComponentTransfer",
54
+ "feComposite",
55
+ "feConvolveMatrix",
56
+ "feDiffuseLighting",
57
+ "feDisplacementMap",
58
+ "feDistantLight",
59
+ "feDropShadow",
60
+ "feFlood",
61
+ "feFuncA",
62
+ "feFuncB",
63
+ "feFuncG",
64
+ "feFuncR",
65
+ "feGaussianBlur",
66
+ "feImage",
67
+ "feMerge",
68
+ "feMergeNode",
69
+ "feMorphology",
70
+ "feOffset",
71
+ "fePointLight",
72
+ "feSpecularLighting",
73
+ "feSpotLight",
74
+ "feTile",
75
+ "feTurbulence",
76
+ "foreignObject",
77
+ "animate",
78
+ "animateMotion",
79
+ "animateTransform",
80
+ "set",
81
+ "desc",
82
+ "metadata",
83
+ "title",
84
+ "a",
85
+ "switch",
86
+ "view",
87
+ ]);
88
+
89
+ /**
90
+ * Check if a tag name is an SVG element
91
+ * @param {string} tagName
92
+ * @returns {boolean}
93
+ */
94
+ export function isSvgElement(tagName) {
95
+ return SVG_ELEMENTS.has(tagName);
96
+ }
97
+
98
+ /**
99
+ * Per-file template registry for deduplication
100
+ */
101
+ export class TemplateRegistry {
102
+ constructor() {
103
+ this.templates = new Map(); // html -> { id, varName, isSvg }
104
+ this.counter = 0;
105
+ }
106
+
107
+ /**
108
+ * Register a template and return its info
109
+ * @param {string} html - The HTML string
110
+ * @param {boolean} isSvg - Whether this is an SVG template
111
+ * @returns {{ id: number, varName: string, isNew: boolean, isSvg: boolean }}
112
+ */
113
+ register(html, isSvg = false) {
114
+ const key = isSvg ? `svg:${html}` : html;
115
+ if (this.templates.has(key)) {
116
+ return { ...this.templates.get(key), isNew: false };
117
+ }
118
+
119
+ this.counter++;
120
+ const info = {
121
+ id: this.counter,
122
+ varName: `${CONSTANTS.TEMPLATE_PREFIX}${this.counter}`,
123
+ isSvg,
124
+ };
125
+ this.templates.set(key, info);
126
+ return { ...info, isNew: true };
127
+ }
128
+
129
+ /**
130
+ * Get all template declarations
131
+ * @returns {string[]}
132
+ */
133
+ getDeclarations() {
134
+ const declarations = [];
135
+ for (const [key, info] of this.templates) {
136
+ const html = info.isSvg ? key.slice(4) : key; // Remove 'svg:' prefix if present
137
+ const templateFn = info.isSvg ? "svgTemplate" : "template";
138
+ declarations.push(`const ${info.varName} = ${templateFn}('${escapeTemplateString(html)}');`);
139
+ }
140
+ return declarations;
141
+ }
142
+
143
+ /**
144
+ * Check if any templates use SVG
145
+ * @returns {boolean}
146
+ */
147
+ hasSvgTemplates() {
148
+ for (const info of this.templates.values()) {
149
+ if (info.isSvg) return true;
150
+ }
151
+ return false;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Type-specific variable name counters
157
+ */
158
+ export class VariableNameGenerator {
159
+ constructor() {
160
+ this.counters = new Map();
161
+ this.textNodeCounters = new Map(); // Track text node counts per parent
162
+ }
163
+
164
+ /**
165
+ * Sanitize a tag name to be a valid JavaScript identifier
166
+ * @param {string} tagName - The HTML tag name (may contain hyphens for custom elements)
167
+ * @returns {string}
168
+ */
169
+ sanitizeTagName(tagName) {
170
+ // Replace hyphens with underscores to make valid JS identifiers
171
+ return tagName.replace(/-/g, "_");
172
+ }
173
+
174
+ /**
175
+ * Generate a unique variable name for an element type
176
+ * @param {string} tagName - The HTML tag name
177
+ * @returns {string}
178
+ */
179
+ generate(tagName) {
180
+ const sanitized = this.sanitizeTagName(tagName);
181
+ const current = this.counters.get(sanitized) || 0;
182
+ this.counters.set(sanitized, current + 1);
183
+ return `${sanitized}_${current + 1}`;
184
+ }
185
+
186
+ /**
187
+ * Generate a variable name for a text node
188
+ * @param {string} parentVarName - The parent element's variable name
189
+ * @returns {string}
190
+ */
191
+ generateTextNode(parentVarName) {
192
+ const key = `text_${parentVarName}`;
193
+ const current = this.textNodeCounters.get(key) || 0;
194
+ this.textNodeCounters.set(key, current + 1);
195
+ // First text node: p_1_text, second: p_1_text_2, etc.
196
+ return current === 0 ? `${parentVarName}_text` : `${parentVarName}_text_${current + 1}`;
197
+ }
198
+
199
+ /**
200
+ * Generate a root variable name
201
+ * @returns {string}
202
+ */
203
+ generateRoot() {
204
+ const current = this.counters.get(CONSTANTS.ROOT_COUNTER_KEY) || 0;
205
+ this.counters.set(CONSTANTS.ROOT_COUNTER_KEY, current + 1);
206
+ return `${CONSTANTS.ROOT_PREFIX}${current + 1}`;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Extract template from JSX element
212
+ * @param {import("@babel/types").JSXElement} node - The JSX element
213
+ * @param {TemplateRegistry} registry - Template registry for deduplication
214
+ * @param {VariableNameGenerator} nameGen - Variable name generator
215
+ * @param {boolean} isComponentRoot - If true, traversal uses 'this.firstChild' for root
216
+ * @param {boolean} isFragment - If true, the node is a JSX fragment
217
+ * @param {Set<string>} roqaComponentTags - Set of tag names defined via defineComponent (Roqa components)
218
+ * @returns {TemplateExtractionResult}
219
+ *
220
+ * @typedef {Object} TemplateExtractionResult
221
+ * @property {string} templateVar - The template variable name
222
+ * @property {string} rootVar - The root element variable name
223
+ * @property {TraversalStep[]} traversal - DOM traversal steps
224
+ * @property {BindingPoint[]} bindings - Dynamic binding points
225
+ * @property {EventBinding[]} events - Event handler bindings
226
+ * @property {ForBlock[]} forBlocks - For loop blocks
227
+ * @property {ShowBlock[]} showBlocks - Show conditional blocks
228
+ */
229
+ export function extractTemplate(
230
+ node,
231
+ registry,
232
+ nameGen,
233
+ isComponentRoot = false,
234
+ isFragment = false,
235
+ roqaComponentTags = new Set(),
236
+ ) {
237
+ if (isFragment || isJSXFragment(node)) {
238
+ return extractFragmentTemplate(node, registry, nameGen, isComponentRoot, roqaComponentTags);
239
+ }
240
+
241
+ const { html, bindings, events, forBlocks, showBlocks, structure, isSvg } = jsxToHtml(
242
+ node,
243
+ nameGen,
244
+ roqaComponentTags,
245
+ );
246
+
247
+ const templateInfo = registry.register(html, isSvg);
248
+ const rootVar = nameGen.generateRoot();
249
+
250
+ // Generate traversal code
251
+ const traversal = generateTraversal(structure, rootVar, isComponentRoot);
252
+
253
+ return {
254
+ templateVar: templateInfo.varName,
255
+ rootVar,
256
+ traversal,
257
+ bindings,
258
+ events,
259
+ forBlocks,
260
+ showBlocks,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Extract template from a JSX fragment (multiple root elements)
266
+ * @param {import("@babel/types").JSXFragment} node - The JSX fragment
267
+ * @param {TemplateRegistry} registry - Template registry for deduplication
268
+ * @param {VariableNameGenerator} nameGen - Variable name generator
269
+ * @param {boolean} isComponentRoot - If true, traversal uses 'this.firstChild' for root
270
+ * @param {Set<string>} roqaComponentTags - Set of tag names defined via defineComponent (Roqa components)
271
+ * @returns {TemplateExtractionResult}
272
+ */
273
+ function extractFragmentTemplate(
274
+ node,
275
+ registry,
276
+ nameGen,
277
+ isComponentRoot,
278
+ roqaComponentTags = new Set(),
279
+ ) {
280
+ const bindings = [];
281
+ const events = [];
282
+ const forBlocks = [];
283
+ const showBlocks = [];
284
+ const structures = [];
285
+ let html = "";
286
+
287
+ // Process each child of the fragment as a root element
288
+ const children = node.children || [];
289
+ let childIndex = 0;
290
+
291
+ for (const child of children) {
292
+ if (isJSXText(child)) {
293
+ // Normalize whitespace, preserve spaces between sibling elements
294
+ const text = child.value.replace(/\s+/g, " ");
295
+ if (text && text !== " ") {
296
+ // Non-whitespace text between fragment children - add to HTML
297
+ // (Single space-only text nodes between block elements can be dropped)
298
+ html += escapeHtml(text);
299
+ // Track static text nodes for proper sibling traversal
300
+ structures.push({
301
+ varName: null,
302
+ tagName: "__static_text__",
303
+ children: [],
304
+ textNodes: [],
305
+ isStaticText: true,
306
+ });
307
+ }
308
+ } else if (isJSXElement(child)) {
309
+ const childPath = [childIndex];
310
+ // Check if this child is an SVG element (for fragment children)
311
+ const childTagName = getJSXElementName(child);
312
+ const childInSvg = isSvgElement(childTagName);
313
+ const childResult = processElement(
314
+ child,
315
+ nameGen,
316
+ bindings,
317
+ events,
318
+ forBlocks,
319
+ showBlocks,
320
+ childPath,
321
+ roqaComponentTags,
322
+ childInSvg,
323
+ );
324
+ html += childResult.html;
325
+ structures.push(childResult.structure);
326
+ childIndex++;
327
+ } else if (isJSXExpressionContainer(child)) {
328
+ // Dynamic expression at fragment level
329
+ html += "<!---->";
330
+ const textVarName = nameGen.generateTextNode("fragment");
331
+ bindings.push({
332
+ type: "text",
333
+ varName: null, // No parent element for fragment-level expressions
334
+ textVarName,
335
+ expression: child.expression,
336
+ path: [childIndex],
337
+ childIndex: structures.length,
338
+ staticPrefix: "",
339
+ usesMarker: true,
340
+ isFragmentChild: true,
341
+ });
342
+ // Add a pseudo-structure entry for traversal
343
+ structures.push({
344
+ varName: textVarName,
345
+ tagName: "__text__",
346
+ children: [],
347
+ textNodes: [],
348
+ isTextMarker: true,
349
+ });
350
+ childIndex++;
351
+ }
352
+ }
353
+
354
+ const templateInfo = registry.register(html);
355
+ const rootVar = nameGen.generateRoot();
356
+
357
+ // Generate traversal for fragments (multiple roots as siblings)
358
+ const traversal = generateFragmentTraversal(structures, rootVar, isComponentRoot);
359
+
360
+ return {
361
+ templateVar: templateInfo.varName,
362
+ rootVar,
363
+ traversal,
364
+ bindings,
365
+ events,
366
+ forBlocks,
367
+ showBlocks,
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Generate DOM traversal for fragment (multiple root elements as siblings)
373
+ * @param {ElementStructure[]} structures - Array of root element structures
374
+ * @param {string} rootVar - The root variable name
375
+ * @param {boolean} isComponentRoot - If true, use 'this.firstChild' for first root
376
+ * @returns {TraversalStep[]}
377
+ */
378
+ function generateFragmentTraversal(structures, rootVar, isComponentRoot) {
379
+ const steps = [];
380
+
381
+ let prevVar = null;
382
+ let pendingNextSiblingCount = 0; // Track static text nodes that need to be skipped
383
+
384
+ for (let i = 0; i < structures.length; i++) {
385
+ const structure = structures[i];
386
+
387
+ if (structure.isStaticText) {
388
+ // Static text node - just count it for sibling traversal
389
+ pendingNextSiblingCount++;
390
+ continue;
391
+ }
392
+
393
+ if (structure.isTextMarker) {
394
+ // This is a dynamic text marker - handle it specially
395
+ const markerVarName = `${structure.varName}_marker`;
396
+ if (prevVar === null) {
397
+ steps.push({
398
+ varName: markerVarName,
399
+ code: isComponentRoot ? "this.firstChild" : `${rootVar}.firstChild`,
400
+ isMarker: true,
401
+ textVarName: structure.varName,
402
+ });
403
+ } else {
404
+ // Chain nextSibling calls for any static text nodes we skipped
405
+ let code = `${prevVar}.nextSibling`;
406
+ for (let j = 0; j < pendingNextSiblingCount; j++) {
407
+ code += ".nextSibling";
408
+ }
409
+ steps.push({
410
+ varName: markerVarName,
411
+ code,
412
+ isMarker: true,
413
+ textVarName: structure.varName,
414
+ });
415
+ }
416
+ prevVar = structure.varName; // Use the text node for next traversal
417
+ pendingNextSiblingCount = 0;
418
+ } else {
419
+ // Regular element
420
+ if (prevVar === null) {
421
+ steps.push({
422
+ varName: structure.varName,
423
+ code: isComponentRoot ? "this.firstChild" : `${rootVar}.firstChild`,
424
+ });
425
+ } else {
426
+ // Chain nextSibling calls for any static text nodes we skipped
427
+ let code = `${prevVar}.nextSibling`;
428
+ for (let j = 0; j < pendingNextSiblingCount; j++) {
429
+ code += ".nextSibling";
430
+ }
431
+ steps.push({
432
+ varName: structure.varName,
433
+ code,
434
+ });
435
+ }
436
+ prevVar = structure.varName;
437
+ pendingNextSiblingCount = 0;
438
+
439
+ // Recurse into this element's children
440
+ generateChildTraversal(structure, steps);
441
+ }
442
+ }
443
+
444
+ return steps;
445
+ }
446
+
447
+ /**
448
+ * Convert JSX to HTML string and collect dynamic parts
449
+ * @param {import("@babel/types").JSXElement} node
450
+ * @param {VariableNameGenerator} nameGen
451
+ * @param {Set<string>} roqaComponentTags - Set of tag names defined via defineComponent (Roqa components)
452
+ * @returns {{ html: string, bindings: BindingPoint[], events: EventBinding[], forBlocks: ForBlock[], showBlocks: ShowBlock[], structure: ElementStructure, isSvg: boolean }}
453
+ */
454
+ function jsxToHtml(node, nameGen, roqaComponentTags = new Set()) {
455
+ const bindings = [];
456
+ const events = [];
457
+ const forBlocks = [];
458
+ const showBlocks = [];
459
+
460
+ // Check if root element is an SVG element
461
+ const rootTagName = getJSXElementName(node);
462
+ const isSvg = isSvgElement(rootTagName);
463
+
464
+ const { html, structure } = processElement(
465
+ node,
466
+ nameGen,
467
+ bindings,
468
+ events,
469
+ forBlocks,
470
+ showBlocks,
471
+ [],
472
+ roqaComponentTags,
473
+ isSvg,
474
+ );
475
+
476
+ return { html, bindings, events, forBlocks, showBlocks, structure, isSvg };
477
+ }
478
+
479
+ /**
480
+ * @typedef {Object} ElementStructure
481
+ * @property {string} varName - Variable name for this element
482
+ * @property {string} tagName - HTML tag name
483
+ * @property {ElementStructure[]} children - Child element structures
484
+ * @property {boolean} hasTextChild - Whether this element has a text child needing a variable
485
+ * @property {string|null} textVarName - Variable name for the text node if present
486
+ * @property {TextNodeInfo[]} textNodes - Info about text nodes for traversal
487
+ */
488
+
489
+ /**
490
+ * @typedef {Object} TextNodeInfo
491
+ * @property {string} varName - Variable name for the text node
492
+ * @property {number} childIndex - The index of this text node among all child nodes
493
+ * @property {boolean} isDynamic - Whether this is a dynamic expression placeholder
494
+ */
495
+
496
+ /**
497
+ * Process a JSX element into HTML and structure
498
+ * @param {import("@babel/types").JSXElement} node
499
+ * @param {VariableNameGenerator} nameGen
500
+ * @param {BindingPoint[]} bindings
501
+ * @param {EventBinding[]} events
502
+ * @param {ForBlock[]} forBlocks
503
+ * @param {ShowBlock[]} showBlocks
504
+ * @param {number[]} path - Current path in the tree (for binding locations)
505
+ * @param {Set<string>} roqaComponentTags - Set of tag names defined via defineComponent (Roqa components)
506
+ * @param {boolean} inSvg - Whether we're inside an SVG context
507
+ * @returns {{ html: string, structure: ElementStructure }}
508
+ */
509
+ function processElement(
510
+ node,
511
+ nameGen,
512
+ bindings,
513
+ events,
514
+ forBlocks,
515
+ showBlocks,
516
+ path,
517
+ roqaComponentTags = new Set(),
518
+ inSvg = false,
519
+ ) {
520
+ const tagName = getJSXElementName(node);
521
+ const varName = nameGen.generate(tagName);
522
+ const attrs = extractJSXAttributes(node.openingElement);
523
+
524
+ // Check if this is a custom element (has hyphen in name - web component standard)
525
+ const isCustomElement = tagName.includes("-");
526
+
527
+ // Check if this is a Roqa-defined custom element (registered via defineComponent)
528
+ // Only Roqa components get special prop handling; external custom elements are left alone
529
+ const isRoqaComponent = roqaComponentTags.has(tagName);
530
+
531
+ // Check if we're entering or already in SVG context
532
+ const isInSvgContext = inSvg || isSvgElement(tagName);
533
+
534
+ let html = `<${tagName}`;
535
+ const structure = {
536
+ varName,
537
+ tagName,
538
+ children: [],
539
+ hasTextChild: false,
540
+ textVarName: null,
541
+ textNodes: [], // Track all text nodes that need variables
542
+ };
543
+
544
+ // Process attributes
545
+ for (const [name, value] of attrs) {
546
+ if (name === "...") {
547
+ // Spread attributes - skip for now (could add runtime handling)
548
+ continue;
549
+ }
550
+
551
+ // Check for event handlers (onclick, oninput, etc.)
552
+ if (name.startsWith("on")) {
553
+ const eventName = name.slice(2).toLowerCase();
554
+ events.push({
555
+ varName,
556
+ eventName,
557
+ handler: value, // JSXExpressionContainer or null
558
+ path: [...path],
559
+ });
560
+ continue;
561
+ }
562
+
563
+ // Handle attribute values
564
+ if (value === null) {
565
+ // Boolean attribute: <button disabled>
566
+ html += ` ${name}`;
567
+ } else if (value.type === "StringLiteral") {
568
+ // Static string: class="foo"
569
+ // Always include static attributes in the HTML template
570
+ // This ensures getAttribute() works for all elements including custom elements
571
+ html += ` ${name}="${escapeAttr(value.value)}"`;
572
+ } else if (value.type === "JSXExpressionContainer") {
573
+ // Dynamic expression: class={expr}
574
+ // Add placeholder for static attributes, bind for dynamic
575
+ if (name === "class" || name === "className") {
576
+ // For class bindings, we'll handle at runtime
577
+ bindings.push({
578
+ type: "attribute",
579
+ varName,
580
+ attrName: "className",
581
+ expression: value.expression,
582
+ path: [...path],
583
+ isSvg: isInSvgContext,
584
+ });
585
+ } else if (isRoqaComponent) {
586
+ // For Roqa-defined custom elements, use setProp() to pass data
587
+ bindings.push({
588
+ type: "prop",
589
+ varName,
590
+ propName: name,
591
+ expression: value.expression,
592
+ path: [...path],
593
+ });
594
+ } else if (isCustomElement) {
595
+ // For other custom elements (third-party web components),
596
+ // set properties directly on the element before it connects
597
+ bindings.push({
598
+ type: "prop",
599
+ varName,
600
+ propName: name,
601
+ expression: value.expression,
602
+ path: [...path],
603
+ isThirdParty: true, // Mark as third-party (no WeakMap, direct property)
604
+ });
605
+ } else {
606
+ bindings.push({
607
+ type: "attribute",
608
+ varName,
609
+ attrName: name,
610
+ expression: value.expression,
611
+ path: [...path],
612
+ isSvg: isInSvgContext,
613
+ });
614
+ }
615
+ }
616
+ }
617
+
618
+ html += ">";
619
+
620
+ // Process children - first pass to collect all content parts
621
+ const children = getJSXChildren(node);
622
+ const contentParts = []; // Array of { type: 'static'|'dynamic', value: string|expression }
623
+ let hasDynamicContent = false;
624
+
625
+ for (const child of children) {
626
+ if (isJSXText(child)) {
627
+ // Static text - normalize whitespace but preserve spaces between elements
628
+ const text = child.value.replace(/\s+/g, " ");
629
+ if (text) {
630
+ contentParts.push({ type: "static", value: text });
631
+ }
632
+ } else if (isJSXExpressionContainer(child)) {
633
+ // Check if expression is a static string literal (e.g., {" "} from formatters)
634
+ if (child.expression.type === "StringLiteral") {
635
+ // Treat string literals as static text
636
+ contentParts.push({ type: "static", value: child.expression.value });
637
+ } else {
638
+ hasDynamicContent = true;
639
+ contentParts.push({ type: "dynamic", expression: child.expression });
640
+ }
641
+ } else if (isJSXElement(child)) {
642
+ // Element child - flush content parts and process element
643
+ if (contentParts.length > 0) {
644
+ // If we have mixed content before an element, handle it
645
+ if (hasDynamicContent) {
646
+ // Add space placeholder for dynamic content
647
+ html += " ";
648
+ const textVarName = nameGen.generateTextNode(varName);
649
+ structure.hasTextChild = true;
650
+ structure.textVarName = textVarName;
651
+ structure.textNodes.push({
652
+ varName: textVarName,
653
+ childIndex: 0,
654
+ isDynamic: true,
655
+ });
656
+ bindings.push({
657
+ type: "text",
658
+ varName,
659
+ textVarName,
660
+ contentParts: [...contentParts],
661
+ path: [...path],
662
+ });
663
+ } else {
664
+ // All static - just add to HTML (preserve whitespace between elements)
665
+ for (const part of contentParts) {
666
+ html += escapeHtml(part.value);
667
+ }
668
+ }
669
+ contentParts.length = 0;
670
+ hasDynamicContent = false;
671
+ }
672
+
673
+ // Check if it's a <For> component
674
+ if (isForComponent(child)) {
675
+ forBlocks.push({
676
+ containerVarName: varName,
677
+ node: child,
678
+ path: [...path, structure.children.length],
679
+ });
680
+ } else if (isShowComponent(child)) {
681
+ // Check if it's a <Show> component
682
+ showBlocks.push({
683
+ containerVarName: varName,
684
+ node: child,
685
+ path: [...path, structure.children.length],
686
+ });
687
+ } else {
688
+ // Regular child element
689
+ const childResult = processElement(
690
+ child,
691
+ nameGen,
692
+ bindings,
693
+ events,
694
+ forBlocks,
695
+ showBlocks,
696
+ [...path, structure.children.length],
697
+ roqaComponentTags,
698
+ isInSvgContext,
699
+ );
700
+ html += childResult.html;
701
+ structure.children.push(childResult.structure);
702
+ }
703
+ }
704
+ }
705
+
706
+ // Handle remaining content parts after processing all children
707
+ if (contentParts.length > 0) {
708
+ if (hasDynamicContent) {
709
+ // Element has dynamic content - use space placeholder
710
+ html += " ";
711
+ const textVarName = nameGen.generateTextNode(varName);
712
+ structure.hasTextChild = true;
713
+ structure.textVarName = textVarName;
714
+ structure.textNodes.push({
715
+ varName: textVarName,
716
+ childIndex: structure.children.length,
717
+ isDynamic: true,
718
+ });
719
+ bindings.push({
720
+ type: "text",
721
+ varName,
722
+ textVarName,
723
+ contentParts: [...contentParts],
724
+ path: [...path],
725
+ });
726
+ } else {
727
+ // All static content - join and trim only leading/trailing whitespace
728
+ const staticContent = contentParts.map((p) => p.value).join("");
729
+ // Only trim if this is trailing content (after last element)
730
+ // We need to preserve internal spacing but can trim the end
731
+ const trimmedContent = structure.children.length > 0 ? staticContent : staticContent.trim();
732
+ if (trimmedContent) {
733
+ html += escapeHtml(trimmedContent);
734
+ }
735
+ }
736
+ }
737
+
738
+ // Self-closing tags
739
+ const voidElements = new Set([
740
+ "area",
741
+ "base",
742
+ "br",
743
+ "col",
744
+ "embed",
745
+ "hr",
746
+ "img",
747
+ "input",
748
+ "link",
749
+ "meta",
750
+ "param",
751
+ "source",
752
+ "track",
753
+ "wbr",
754
+ ]);
755
+
756
+ if (!voidElements.has(tagName)) {
757
+ html += `</${tagName}>`;
758
+ }
759
+
760
+ return { html, structure };
761
+ }
762
+
763
+ /**
764
+ * Generate DOM traversal code from element structure
765
+ * @param {ElementStructure} structure
766
+ * @param {string} rootVar
767
+ * @param {boolean} isComponentRoot - If true, use 'this.firstChild' for root element
768
+ * @returns {TraversalStep[]}
769
+ *
770
+ * @typedef {Object} TraversalStep
771
+ * @property {string} varName - Variable being declared
772
+ * @property {string} code - The traversal code (e.g., "rootVar.firstChild")
773
+ */
774
+ function generateTraversal(structure, rootVar, isComponentRoot = false) {
775
+ const steps = [];
776
+
777
+ // First step: get root element
778
+ // For component roots, we use 'this.firstChild' because the DocumentFragment
779
+ // becomes empty after appendChild
780
+ // For nested templates (like inside forBlock), we use the rootVar
781
+ steps.push({
782
+ varName: structure.varName,
783
+ code: isComponentRoot ? "this.firstChild" : `${rootVar}.firstChild`,
784
+ });
785
+
786
+ // Generate traversal for text nodes and children
787
+ generateChildTraversal(structure, steps);
788
+
789
+ return steps;
790
+ }
791
+
792
+ /**
793
+ * Recursively generate traversal for children
794
+ */
795
+ function generateChildTraversal(structure, steps) {
796
+ const { varName, children, textNodes } = structure;
797
+
798
+ // Process text nodes that come BEFORE any child elements first
799
+ // These can use firstChild traversal and don't depend on element variables
800
+ for (let i = 0; i < textNodes.length; i++) {
801
+ const textNode = textNodes[i];
802
+ if (textNode.isDynamic && textNode.childIndex === 0) {
803
+ // Text is the first child - access it directly
804
+ steps.push({
805
+ varName: textNode.varName,
806
+ code: `${varName}.firstChild`,
807
+ });
808
+ }
809
+ }
810
+
811
+ // Process child elements
812
+ let prevVar = null;
813
+ let elementIndex = 0;
814
+
815
+ for (const child of children) {
816
+ if (elementIndex === 0) {
817
+ // First element child
818
+ if (textNodes.length > 0 && textNodes[0].childIndex === 0) {
819
+ // There's a text node before this element, use nextSibling from it
820
+ steps.push({
821
+ varName: child.varName,
822
+ code: `${textNodes[0].varName}.nextSibling`,
823
+ });
824
+ } else {
825
+ // No text nodes before, use firstChild
826
+ steps.push({
827
+ varName: child.varName,
828
+ code: `${varName}.firstChild`,
829
+ });
830
+ }
831
+ } else {
832
+ // Subsequent children: use nextSibling from previous element
833
+ steps.push({
834
+ varName: child.varName,
835
+ code: `${prevVar}.nextSibling`,
836
+ });
837
+ }
838
+
839
+ prevVar = child.varName;
840
+ elementIndex++;
841
+
842
+ // Recurse into this child
843
+ generateChildTraversal(child, steps);
844
+ }
845
+
846
+ // Process text nodes that come AFTER child elements
847
+ // These need to reference the previous element variable, which is now declared
848
+ for (let i = 0; i < textNodes.length; i++) {
849
+ const textNode = textNodes[i];
850
+ if (textNode.isDynamic && textNode.childIndex > 0) {
851
+ // Text comes after elements - traverse from last element before it
852
+ const prevChild = children[textNode.childIndex - 1];
853
+ if (prevChild) {
854
+ steps.push({
855
+ varName: textNode.varName,
856
+ code: `${prevChild.varName}.nextSibling`,
857
+ });
858
+ } else {
859
+ // Fallback - traverse from parent
860
+ let traversal = `${varName}.firstChild`;
861
+ for (let j = 0; j < textNode.childIndex; j++) {
862
+ traversal += ".nextSibling";
863
+ }
864
+ steps.push({
865
+ varName: textNode.varName,
866
+ code: traversal,
867
+ });
868
+ }
869
+ }
870
+ }
871
+ }