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.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/package.json +77 -0
- package/src/compiler/codegen.js +1217 -0
- package/src/compiler/index.js +47 -0
- package/src/compiler/parser.js +197 -0
- package/src/compiler/transforms/bind-detector.js +264 -0
- package/src/compiler/transforms/events.js +246 -0
- package/src/compiler/transforms/for-transform.js +164 -0
- package/src/compiler/transforms/inline-get.js +1049 -0
- package/src/compiler/transforms/jsx-to-template.js +871 -0
- package/src/compiler/transforms/show-transform.js +78 -0
- package/src/compiler/transforms/validate.js +80 -0
- package/src/compiler/utils.js +69 -0
- package/src/jsx-runtime.d.ts +640 -0
- package/src/jsx-runtime.js +73 -0
- package/src/runtime/cell.js +37 -0
- package/src/runtime/component.js +241 -0
- package/src/runtime/events.js +156 -0
- package/src/runtime/for-block.js +374 -0
- package/src/runtime/index.js +17 -0
- package/src/runtime/show-block.js +115 -0
- package/src/runtime/template.js +32 -0
- package/types/compiler.d.ts +9 -0
- package/types/index.d.ts +433 -0
|
@@ -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
|
+
}
|