@canvasengine/compiler 2.0.0-beta.5 → 2.0.0-beta.51

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,1119 @@
1
+ {
2
+ function generateError(message, location) {
3
+ const { start, end } = location;
4
+ const errorMessage = `${message}\n` +
5
+ `at line ${start.line}, column ${start.column} to line ${end.line}, column ${end.column}`;
6
+ throw new Error(errorMessage);
7
+ }
8
+
9
+ /*——— Custom error handler for syntax errors ———*/
10
+ function parseError(error) {
11
+ // error.expected : array of { type, description }
12
+ // error.found : string | null
13
+ // error.location : { start, end }
14
+ const { expected, found, location } = error;
15
+
16
+ // Group expected items by description to avoid duplicates
17
+ const uniqueExpected = [...new Set(expected.map(e => e.description))];
18
+
19
+ // Format the expected values in a more readable way
20
+ const expectedDesc = uniqueExpected
21
+ .map(desc => `'${desc}'`)
22
+ .join(' or ');
23
+
24
+ const foundDesc = found === null ? 'end of input' : `'${found}'`;
25
+
26
+ generateError(
27
+ `Syntax error: expected ${expectedDesc} but found ${foundDesc}`,
28
+ location
29
+ );
30
+ }
31
+
32
+ // List of standard HTML DOM elements
33
+ const domElements = new Set([
34
+ 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 'samp', 's', 'script', 'section', 'select', 'slot', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'
35
+ ]);
36
+
37
+ // Framework components that should NOT be transformed to DOM elements
38
+ const frameworkComponents = new Set([
39
+ 'Canvas', 'Container', 'Sprite', 'Text', 'DOMElement', 'Svg', 'Button'
40
+ ]);
41
+
42
+ const voidElements = new Set([
43
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta',
44
+ 'param', 'source', 'track', 'wbr'
45
+ ]);
46
+
47
+ // DisplayObject special attributes that should not be in attrs
48
+ const displayObjectAttributes = new Set([
49
+ 'x', 'y', 'scale', 'anchor', 'skew', 'tint', 'rotation', 'angle',
50
+ 'zIndex', 'roundPixels', 'cursor', 'visible', 'alpha', 'pivot', 'filters', 'maskOf',
51
+ 'blendMode', 'filterArea', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
52
+ 'aspectRatio', 'flexGrow', 'flexShrink', 'flexBasis', 'rowGap', 'columnGap',
53
+ 'positionType', 'top', 'right', 'bottom', 'left', 'objectFit', 'objectPosition',
54
+ 'transformOrigin', 'flexDirection', 'justifyContent', 'alignItems', 'alignContent',
55
+ 'alignSelf', 'margin', 'padding', 'border', 'gap', 'blur', 'shadow'
56
+ ]);
57
+
58
+ function isDOMElement(tagName) {
59
+ // Don't transform framework components to DOM elements
60
+ if (frameworkComponents.has(tagName)) {
61
+ return false;
62
+ }
63
+ return domElements.has(tagName.toLowerCase());
64
+ }
65
+
66
+ function isVoidElement(tagName) {
67
+ return voidElements.has(tagName.toLowerCase());
68
+ }
69
+
70
+ function formatAttributes(attributes) {
71
+ if (attributes.length === 0) {
72
+ return null;
73
+ }
74
+
75
+ // Check if there's exactly one attribute and it's a spread attribute
76
+ if (attributes.length === 1 && attributes[0].startsWith('...')) {
77
+ // Return the identifier directly, removing the '...'
78
+ return attributes[0].substring(3);
79
+ }
80
+
81
+ // Otherwise, format as an object literal
82
+ const formattedAttrs = attributes.map(attr => {
83
+ // If it's a spread attribute, keep it as is
84
+ if (attr.startsWith('...')) {
85
+ return attr;
86
+ }
87
+ // If it's a standalone attribute (doesn't contain ':'), format as shorthand property 'name'
88
+ if (!attr.includes(':')) {
89
+ return attr; // JS object literal shorthand
90
+ }
91
+ // Otherwise (key: value), keep it as is
92
+ return attr;
93
+ });
94
+
95
+ return `{ ${formattedAttrs.join(', ')} }`;
96
+ }
97
+
98
+ function formatDOMElement(tagName, attributes) {
99
+ if (attributes.length === 0) {
100
+ return `h(DOMElement, { element: "${tagName}" })`;
101
+ }
102
+
103
+ const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
104
+
105
+ // Build the result
106
+ const parts = [`element: "${tagName}"`];
107
+
108
+ if (domAttrs.length > 0) {
109
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
110
+ }
111
+
112
+ if (displayObjectAttrs.length > 0) {
113
+ parts.push(...displayObjectAttrs);
114
+ }
115
+
116
+ return `h(DOMElement, { ${parts.join(', ')} })`;
117
+ }
118
+
119
+ function splitAttributes(attributes) {
120
+ const domAttrs = [];
121
+ const displayObjectAttrs = [];
122
+ const classValues = [];
123
+ let classInsertIndex = null;
124
+
125
+ attributes.forEach(attr => {
126
+ // Handle spread attributes
127
+ if (attr.startsWith('...')) {
128
+ displayObjectAttrs.push(attr);
129
+ return;
130
+ }
131
+
132
+ // Extract attribute name and value (if present)
133
+ let attrName;
134
+ let attrValue;
135
+ if (attr.includes(':')) {
136
+ const colonIndex = attr.indexOf(':');
137
+ attrName = attr.slice(0, colonIndex).trim().replace(/['"]/g, '');
138
+ attrValue = attr.slice(colonIndex + 1).trim();
139
+ } else {
140
+ // Standalone attribute
141
+ attrName = attr.replace(/['"]/g, '');
142
+ }
143
+
144
+ // Check if it's a DisplayObject attribute
145
+ if (displayObjectAttributes.has(attrName)) {
146
+ displayObjectAttrs.push(attr);
147
+ return;
148
+ }
149
+
150
+ if (attrName === 'class' && attrValue !== undefined) {
151
+ classValues.push(attrValue);
152
+ if (classInsertIndex === null) {
153
+ classInsertIndex = domAttrs.length;
154
+ }
155
+ return;
156
+ }
157
+
158
+ domAttrs.push(attr);
159
+ });
160
+
161
+ if (classValues.length > 0) {
162
+ const mergedClass = classValues.length === 1
163
+ ? `class: ${classValues[0]}`
164
+ : `class: [${classValues.join(', ')}]`;
165
+ if (classInsertIndex === null) {
166
+ domAttrs.push(mergedClass);
167
+ } else {
168
+ domAttrs.splice(classInsertIndex, 0, mergedClass);
169
+ }
170
+ }
171
+
172
+ return { domAttrs, displayObjectAttrs };
173
+ }
174
+ }
175
+
176
+ start
177
+ = _ elements:(element)* _ {
178
+ if (elements.length === 1) {
179
+ return elements[0];
180
+ }
181
+ return `[${elements.join(',')}]`;
182
+ }
183
+
184
+ element "component or control structure"
185
+ = forLoop
186
+ / ifCondition
187
+ / svgElement
188
+ / domElementWithText
189
+ / domElementWithMixedContent
190
+ / selfClosingElement
191
+ / voidElement
192
+ / openCloseElement
193
+ / openUnclosedTag
194
+ / comment
195
+
196
+ selfClosingElement "self-closing component tag"
197
+ = _ "<" _ tagName:tagName _ attributes:attributes _ "/>" _ {
198
+ // Check if it's a DOM element
199
+ if (isDOMElement(tagName)) {
200
+ return formatDOMElement(tagName, attributes);
201
+ }
202
+ // Otherwise, treat as regular component
203
+ const attrsString = formatAttributes(attributes);
204
+ return attrsString ? `h(${tagName}, ${attrsString})` : `h(${tagName})`;
205
+ }
206
+
207
+ voidElement "void DOM element tag"
208
+ = _ "<" _ tagName:tagName &{ return isVoidElement(tagName); } _ attributes:attributes _ ">" _ {
209
+ return formatDOMElement(tagName, attributes);
210
+ }
211
+
212
+ domElementWithText "DOM element with text content"
213
+ = "<" _ tagName:tagName &{ return !isVoidElement(tagName); } _ attributes:attributes _ ">" _ text:simpleTextContent _ "</" _ closingTagName:tagName _ ">" _ {
214
+ if (tagName !== closingTagName) {
215
+ generateError(
216
+ `Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
217
+ location()
218
+ );
219
+ }
220
+
221
+ if (isDOMElement(tagName)) {
222
+ if (attributes.length === 0) {
223
+ return `h(DOMElement, { element: "${tagName}", textContent: ${text} })`;
224
+ }
225
+
226
+ const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
227
+
228
+ // Build the result
229
+ const parts = [`element: "${tagName}"`];
230
+
231
+ if (domAttrs.length > 0) {
232
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
233
+ }
234
+
235
+ parts.push(`textContent: ${text}`);
236
+
237
+ if (displayObjectAttrs.length > 0) {
238
+ parts.push(...displayObjectAttrs);
239
+ }
240
+
241
+ return `h(DOMElement, { ${parts.join(', ')} })`;
242
+ }
243
+
244
+ // If not a DOM element, fall back to regular parsing
245
+ return null;
246
+ }
247
+
248
+ domElementWithMixedContent "DOM element with mixed content"
249
+ = "<" _ tagName:tagName &{ return isDOMElement(tagName) && !isVoidElement(tagName); } _ attributes:attributes _ ">" _ children:domContent _ "</" _ closingTagName:tagName _ ">" _ {
250
+ if (tagName !== closingTagName) {
251
+ generateError(
252
+ `Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
253
+ location()
254
+ );
255
+ }
256
+
257
+ const childrenContent = children ? children : null;
258
+
259
+ if (attributes.length === 0) {
260
+ if (childrenContent) {
261
+ return `h(DOMElement, { element: "${tagName}" }, ${childrenContent})`;
262
+ } else {
263
+ return `h(DOMElement, { element: "${tagName}" })`;
264
+ }
265
+ }
266
+
267
+ const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
268
+
269
+ // Build the result
270
+ const parts = [`element: "${tagName}"`];
271
+
272
+ if (domAttrs.length > 0) {
273
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
274
+ }
275
+
276
+ if (displayObjectAttrs.length > 0) {
277
+ parts.push(...displayObjectAttrs);
278
+ }
279
+
280
+ if (childrenContent) {
281
+ return `h(DOMElement, { ${parts.join(', ')} }, ${childrenContent})`;
282
+ } else {
283
+ return `h(DOMElement, { ${parts.join(', ')} })`;
284
+ }
285
+ }
286
+
287
+ simpleTextContent "simple text content"
288
+ = parts:(simpleDynamicPart / simpleTextPart)+ {
289
+ const validParts = parts.filter(p => p !== null);
290
+ if (validParts.length === 0) return null;
291
+ if (validParts.length === 1) return validParts[0];
292
+
293
+ // Multiple parts - need to concatenate
294
+ const normalizedParts = validParts.map(part => {
295
+ if (typeof part === 'string' && part.startsWith('computed(() => ') && part.endsWith(')')) {
296
+ return part.slice('computed(() => '.length, -1);
297
+ }
298
+ return part;
299
+ });
300
+ const hasSignals = normalizedParts.some(part => part && part.includes && part.includes('()'));
301
+ if (hasSignals) {
302
+ return `computed(() => ${normalizedParts.join(' + ')})`;
303
+ }
304
+ return normalizedParts.join(' + ');
305
+ }
306
+
307
+ simpleTextPart "simple text part"
308
+ = !("@for" / "@if") text:$([^<{@]+) {
309
+ const trimmed = text.trim();
310
+ if (!trimmed) return null;
311
+ const escaped = text
312
+ .replace(/\\/g, '\\\\')
313
+ .replace(/'/g, "\\'")
314
+ .replace(/\r/g, '\\r')
315
+ .replace(/\n/g, '\\n')
316
+ .replace(/\t/g, '\\t');
317
+ return `'${escaped}'`;
318
+ }
319
+
320
+ simpleDynamicPart "simple dynamic part"
321
+ = "{{" _ expr:attributeValue _ "}}" {
322
+ // Handle double brace expressions like {{ object.x }} or {{ @object.x }} or {{ @object.@x }}
323
+ if (expr.trim().match(/^(@?[a-zA-Z_][a-zA-Z0-9_]*)(\.@?[a-zA-Z_][a-zA-Z0-9_]*)*$/)) {
324
+ let foundSignal = false;
325
+ let hasLiterals = false;
326
+
327
+ // Split by dots to handle each part separately
328
+ const parts = expr.split('.');
329
+ const allLiterals = parts.every(part => part.trim().startsWith('@'));
330
+
331
+ let computedValue;
332
+
333
+ if (allLiterals) {
334
+ // All parts are literals, just remove @ prefixes
335
+ computedValue = parts.map(part => part.replace('@', '')).join('.');
336
+ hasLiterals = true;
337
+ } else {
338
+ // Transform each part individually
339
+ computedValue = parts.map(part => {
340
+ const trimmedPart = part.trim();
341
+ if (trimmedPart.startsWith('@')) {
342
+ hasLiterals = true;
343
+ return trimmedPart.substring(1); // Remove @ prefix for literals
344
+ } else {
345
+ // Don't transform keywords
346
+ if (['true', 'false', 'null'].includes(trimmedPart)) {
347
+ return trimmedPart;
348
+ }
349
+ foundSignal = true;
350
+ return `${trimmedPart}()`;
351
+ }
352
+ }).join('.');
353
+ }
354
+
355
+ if (foundSignal && !allLiterals) {
356
+ return `computed(() => ${computedValue})`;
357
+ }
358
+ return computedValue;
359
+ }
360
+ return expr;
361
+ }
362
+ / "{" _ expr:attributeValue _ "}" {
363
+ // Handle single brace expressions like {item.name} or {@text}
364
+ if (expr.trim().match(/^(@?[a-zA-Z_][a-zA-Z0-9_]*)(\.@?[a-zA-Z_][a-zA-Z0-9_]*)*$/)) {
365
+ let foundSignal = false;
366
+ const computedValue = expr.replace(/@?([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*:)/g, (match, p1) => {
367
+ if (match.startsWith('@')) {
368
+ return p1;
369
+ }
370
+ foundSignal = true;
371
+ return `${p1}()`;
372
+ });
373
+ if (foundSignal) {
374
+ return `computed(() => ${computedValue})`;
375
+ }
376
+ return computedValue;
377
+ }
378
+ return expr;
379
+ }
380
+
381
+ openCloseElement "component with content"
382
+ = "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ "</" _ closingTagName:tagName _ ">" _ {
383
+ if (tagName !== closingTagName) {
384
+ generateError(
385
+ `Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
386
+ location()
387
+ );
388
+ }
389
+
390
+ // Check if it's a DOM element
391
+ if (isDOMElement(tagName)) {
392
+ const children = content ? content : null;
393
+
394
+ if (attributes.length === 0) {
395
+ if (children) {
396
+ return `h(DOMElement, { element: "${tagName}" }, ${children})`;
397
+ } else {
398
+ return `h(DOMElement, { element: "${tagName}" })`;
399
+ }
400
+ }
401
+
402
+ const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
403
+
404
+ // Build the result
405
+ const parts = [`element: "${tagName}"`];
406
+
407
+ if (domAttrs.length > 0) {
408
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
409
+ }
410
+
411
+ if (displayObjectAttrs.length > 0) {
412
+ parts.push(...displayObjectAttrs);
413
+ }
414
+
415
+ if (children) {
416
+ return `h(DOMElement, { ${parts.join(', ')} }, ${children})`;
417
+ } else {
418
+ return `h(DOMElement, { ${parts.join(', ')} })`;
419
+ }
420
+ }
421
+
422
+ // Otherwise, treat as regular component
423
+ const attrsString = formatAttributes(attributes);
424
+ const children = content ? content : null;
425
+ if (attrsString && children) {
426
+ return `h(${tagName}, ${attrsString}, ${children})`;
427
+ } else if (attrsString) {
428
+ return `h(${tagName}, ${attrsString})`;
429
+ } else if (children) {
430
+ return `h(${tagName}, null, ${children})`;
431
+ } else {
432
+ return `h(${tagName})`;
433
+ }
434
+ }
435
+
436
+ attributes "component attributes"
437
+ = attrs:(attribute (_ attribute)*)? {
438
+ return attrs
439
+ ? [attrs[0]].concat(attrs[1].map(a => a[1]))
440
+ : [];
441
+ }
442
+
443
+ attribute "attribute"
444
+ = staticAttribute
445
+ / dynamicAttribute
446
+ / eventHandler
447
+ / spreadAttribute
448
+ / unclosedQuote
449
+ / unclosedBrace
450
+
451
+ spreadAttribute "spread attribute"
452
+ = "..." expr:(functionCallExpr / dotNotation) {
453
+ return "..." + expr;
454
+ }
455
+
456
+ functionCallExpr "function call"
457
+ = name:dotNotation "(" args:functionArgs? ")" {
458
+ return `${name}(${args || ''})`;
459
+ }
460
+
461
+ dotNotation "property access"
462
+ = first:identifier rest:("." identifier)* {
463
+ return text();
464
+ }
465
+
466
+ eventHandler "event handler"
467
+ = "@" eventName:identifier _ "=" _ "{" _ handlerName:attributeValue _ "}" {
468
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
469
+ const formattedName = needsQuotes ? `'${eventName}'` : eventName;
470
+ return `${formattedName}: ${handlerName}`;
471
+ }
472
+ / "@" eventName:attributeName _ {
473
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
474
+ return needsQuotes ? `'${eventName}'` : eventName;
475
+ }
476
+
477
+ dynamicAttribute "dynamic attribute"
478
+ = attributeName:attributeName _ "=" _ "{" _ attributeValue:attributeValue _ "}" {
479
+ // Check if attributeName needs to be quoted (contains dash or other invalid JS identifier chars)
480
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
481
+ const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
482
+
483
+
484
+ // If it's a complex object with strings, preserve it as is
485
+ if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}') &&
486
+ (attributeValue.includes('"') || attributeValue.includes("'"))) {
487
+ return `${formattedName}: ${attributeValue}`;
488
+ }
489
+
490
+ // If it's a template string, transform expressions inside ${}
491
+ if (attributeValue.trim().startsWith('`') && attributeValue.trim().endsWith('`')) {
492
+ // Transform expressions inside ${} in template strings
493
+ let transformedTemplate = attributeValue;
494
+
495
+ // Find and replace ${expression} patterns
496
+ let startIndex = 0;
497
+ while (true) {
498
+ const dollarIndex = transformedTemplate.indexOf('${', startIndex);
499
+ if (dollarIndex === -1) break;
500
+
501
+ const braceIndex = transformedTemplate.indexOf('}', dollarIndex);
502
+ if (braceIndex === -1) break;
503
+
504
+ const expr = transformedTemplate.substring(dollarIndex + 2, braceIndex);
505
+ const trimmedExpr = expr.trim();
506
+
507
+ let replacement;
508
+ if (trimmedExpr.startsWith('@')) {
509
+ // Remove @ prefix for literals
510
+ replacement = '${' + trimmedExpr.substring(1) + '}';
511
+ } else if (trimmedExpr.match(/^[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
512
+ // Transform identifiers to signals
513
+ replacement = '${' + trimmedExpr + '()}';
514
+ } else {
515
+ // Keep as is for complex expressions
516
+ replacement = '${' + expr + '}';
517
+ }
518
+
519
+ transformedTemplate = transformedTemplate.substring(0, dollarIndex) +
520
+ replacement +
521
+ transformedTemplate.substring(braceIndex + 1);
522
+
523
+ startIndex = dollarIndex + replacement.length;
524
+ }
525
+
526
+ return formattedName + ': ' + transformedTemplate;
527
+ }
528
+
529
+ // Handle other types of values
530
+ if (attributeValue.startsWith('h(') || attributeValue.includes('=>')) {
531
+ return `${formattedName}: ${attributeValue}`;
532
+ } else if (attributeValue.trim().match(/^[a-zA-Z_]\w*$/)) {
533
+ return `${formattedName}: ${attributeValue}`;
534
+ } else {
535
+ // Check if this is an object or array literal
536
+ const isObjectLiteral = attributeValue.trim().startsWith('{ ') && attributeValue.trim().endsWith(' }');
537
+ const isArrayLiteral = attributeValue.trim().startsWith('[') && attributeValue.trim().endsWith(']');
538
+
539
+ let foundSignal = false;
540
+ let hasLiterals = false;
541
+ let computedValue = attributeValue;
542
+
543
+ // For simple object and array literals (like {x: x, y: 20} or [x, 20]),
544
+ // don't use computed() at all and don't transform identifiers
545
+ if ((isObjectLiteral || isArrayLiteral) && !attributeValue.includes('()')) {
546
+ // Don't transform anything, return as is
547
+ foundSignal = false;
548
+ computedValue = attributeValue;
549
+ } else {
550
+ // Apply signal transformation for other values
551
+ computedValue = attributeValue.replace(/@?([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*:)/g, (match, p1, offset) => {
552
+ // Don't transform keywords, numbers, or if we're inside quotes
553
+ if (['true', 'false', 'null'].includes(p1) || /^\d+(\.\d+)?$/.test(p1)) {
554
+ return match;
555
+ }
556
+
557
+ // Check if we're inside a string literal
558
+ const beforeMatch = attributeValue.substring(0, offset);
559
+ const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
560
+ const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
561
+
562
+ // If we're inside quotes, don't transform
563
+ if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
564
+ return match;
565
+ }
566
+
567
+ if (match.startsWith('@')) {
568
+ hasLiterals = true;
569
+ return p1; // Remove @ prefix
570
+ }
571
+ foundSignal = true;
572
+ return `${p1}()`;
573
+ });
574
+
575
+ // Check if any values already contain signals (ending with ())
576
+ if (attributeValue.includes('()')) {
577
+ foundSignal = true;
578
+ }
579
+ }
580
+
581
+ if (foundSignal) {
582
+ // For objects, wrap in parentheses
583
+ if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) {
584
+ // Remove spaces for objects in parentheses
585
+ const cleanedObject = computedValue.replace(/{ /g, '{').replace(/ }/g, '}');
586
+ return `${formattedName}: computed(() => (${cleanedObject}))`;
587
+ }
588
+ return `${formattedName}: computed(() => ${computedValue})`;
589
+ }
590
+
591
+ // If only literals (all @), don't use computed
592
+ if (hasLiterals && !foundSignal) {
593
+ return `${formattedName}: ${computedValue}`;
594
+ }
595
+
596
+ // For static objects and arrays, return as is without parentheses
597
+ return `${formattedName}: ${computedValue}`;
598
+ }
599
+ }
600
+ / attributeName:attributeName _ {
601
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
602
+ return needsQuotes ? `'${attributeName}'` : attributeName;
603
+ }
604
+
605
+ attributeValue "attribute value"
606
+ = element
607
+ / functionWithElement
608
+ / objectLiteral
609
+ / $([^{}]* ("{" [^{}]* "}" [^{}]*)*) {
610
+ return text().trim()
611
+ }
612
+
613
+ objectLiteral "object literal"
614
+ = "{" _ objContent:objectContent _ "}" {
615
+ return `{ ${objContent} }`;
616
+ }
617
+
618
+ objectContent
619
+ = prop:objectProperty rest:(_ "," _ objectProperty)* {
620
+ return [prop].concat(rest.map(r => r[3])).join(', ');
621
+ }
622
+ / "" { return ""; }
623
+
624
+ objectProperty
625
+ = key:identifier _ ":" _ value:propertyValue {
626
+ return `${key}: ${value}`;
627
+ }
628
+ / key:identifier {
629
+ return key;
630
+ }
631
+
632
+ propertyValue
633
+ = nestedObject
634
+ / element
635
+ / functionWithElement
636
+ / stringLiteral
637
+ / number
638
+ / identifier
639
+
640
+ nestedObject
641
+ = "{" _ objContent:objectContent _ "}" {
642
+ return `{ ${objContent} }`;
643
+ }
644
+
645
+ stringLiteral
646
+ = '"' chars:[^"]* '"' { return text(); }
647
+ / "'" chars:[^']* "'" { return text(); }
648
+
649
+ functionWithElement "function expression"
650
+ = "(" _ params:functionParams? _ ")" _ "=>" _ elem:element {
651
+ return `${params ? `(${params}) =>` : '() =>'} ${elem}`;
652
+ }
653
+
654
+ functionParams
655
+ = destructuredParams
656
+ / simpleParams
657
+
658
+ destructuredParams
659
+ = "{" _ param:identifier rest:(_ "," _ identifier)* _ "}" {
660
+ return `{${[param].concat(rest.map(r => r[3])).join(', ')}}`;
661
+ }
662
+
663
+ simpleParams
664
+ = param:identifier rest:(_ "," _ identifier)* {
665
+ return [param].concat(rest.map(r => r[3])).join(', ');
666
+ }
667
+
668
+ staticAttribute "static attribute"
669
+ = attributeName:attributeName _ "=" _ "\"" attributeValue:staticValue "\"" {
670
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
671
+ const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
672
+ return `${formattedName}: ${attributeValue}`;
673
+ }
674
+
675
+ eventAttribute
676
+ = "(" _ eventName:eventName _ ")" _ "=" _ "\"" eventAction:eventAction "\"" {
677
+ return `${eventName}: () => { ${eventAction} }`;
678
+ }
679
+
680
+ staticValue
681
+ = [^"]+ {
682
+ var val = text();
683
+ return `'${val}'`
684
+ }
685
+
686
+ content "component content"
687
+ = elements:(element)* {
688
+ const filteredElements = elements.filter(el => el !== null);
689
+ if (filteredElements.length === 0) return null;
690
+ if (filteredElements.length === 1) return filteredElements[0];
691
+ return `[${filteredElements.join(', ')}]`;
692
+ }
693
+
694
+ domContent "DOM content"
695
+ = elements:(domContentPart)* {
696
+ const filteredElements = elements.filter(el => el !== null);
697
+ if (filteredElements.length === 0) return null;
698
+ if (filteredElements.length === 1) return filteredElements[0];
699
+ return `[${filteredElements.join(', ')}]`;
700
+ }
701
+
702
+ domContentPart
703
+ = element
704
+ / simpleTextContent
705
+
706
+
707
+
708
+ textNode
709
+ = text:$([^<]+) {
710
+ const trimmed = text.trim();
711
+ return trimmed ? `'${trimmed}'` : null;
712
+ }
713
+
714
+ textElement
715
+ = text:[^<>]+ {
716
+ const trimmed = text.join('').trim();
717
+ return trimmed ? JSON.stringify(trimmed) : null;
718
+ }
719
+
720
+ forLoop "for loop"
721
+ = _ "@for" _ "(" _ variableName:(tupleDestructuring / identifier) _ "of" _ iterable:iterable _ ")" _ "{" _ content:content _ "}" _ {
722
+ return `loop(${iterable}, ${variableName} => ${content})`;
723
+ }
724
+
725
+ tupleDestructuring "destructuring pattern"
726
+ = "(" _ first:identifier _ "," _ second:identifier _ ")" {
727
+ return `(${first}, ${second})`;
728
+ }
729
+
730
+ ifCondition "if condition"
731
+ = _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ elseIfs:elseIfClause* elseClause:elseClause? _ {
732
+ let result = `cond(${condition}, () => ${content}`;
733
+
734
+ // Add else if clauses
735
+ elseIfs.forEach(elseIf => {
736
+ result += `, [${elseIf.condition}, () => ${elseIf.content}]`;
737
+ });
738
+
739
+ // Add else clause if present
740
+ if (elseClause) {
741
+ result += `, () => ${elseClause}`;
742
+ }
743
+
744
+ result += ')';
745
+ return result;
746
+ }
747
+
748
+ elseIfClause "else if clause"
749
+ = _ "@else" _ "if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
750
+ return { condition, content };
751
+ }
752
+
753
+ elseClause "else clause"
754
+ = _ "@else" _ "{" _ content:content _ "}" _ {
755
+ return content;
756
+ }
757
+
758
+ tagName "tag name"
759
+ = tagExpression
760
+
761
+ tagExpression "tag expression"
762
+ = first:tagPart rest:("." tagPart)* {
763
+ return text();
764
+ }
765
+
766
+ tagPart "tag part"
767
+ = name:[a-zA-Z][a-zA-Z0-9]* args:("(" functionArgs? ")")? {
768
+ return text();
769
+ }
770
+
771
+ attributeName "attribute name"
772
+ = [a-zA-Z][a-zA-Z0-9-]* { return text(); }
773
+
774
+ eventName
775
+ = [a-zA-Z][a-zA-Z0-9-]* { return text(); }
776
+
777
+ variableName
778
+ = [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
779
+
780
+ iterable "iterable expression"
781
+ = id:identifier "(" _ args:functionArgs? _ ")" { // Direct function call
782
+ return `${id}(${args || ''})`;
783
+ }
784
+ / first:identifier "." rest:dotFunctionChain { // Dot notation possibly with function call
785
+ return `${first}.${rest}`;
786
+ }
787
+ / id:identifier { return id; }
788
+
789
+ dotFunctionChain
790
+ = segment:identifier "(" _ args:functionArgs? _ ")" rest:("." dotFunctionChain)? {
791
+ const restStr = rest ? `.${rest[1]}` : '';
792
+ return `${segment}(${args || ''})${restStr}`;
793
+ }
794
+ / segment:identifier rest:("." dotFunctionChain)? {
795
+ const restStr = rest ? `.${rest[1]}` : '';
796
+ return `${segment}${restStr}`;
797
+ }
798
+
799
+ condition "condition expression"
800
+ = functionCall
801
+ / functionCallWithArgs
802
+ / text_condition:$([^)]*) {
803
+ const originalText = text_condition.trim();
804
+
805
+ // Handle expressions with @ literals (like @item.@id)
806
+ // First, process dot notation expressions with @ literals
807
+ let processedText = originalText;
808
+ const dotNotationReplacements = new Map();
809
+ let replacementCounter = 0;
810
+
811
+ // Process dot notation expressions like @item.@id, @item.id, item.@id
812
+ // Only process expressions that contain at least one @
813
+ processedText = processedText.replace(/(@[a-zA-Z_][a-zA-Z0-9_]*)(\.@?[a-zA-Z_][a-zA-Z0-9_]*)+|([a-zA-Z_][a-zA-Z0-9_]*)(\.@[a-zA-Z_][a-zA-Z0-9_]*)+/g, (match) => {
814
+ // Split by dots to handle each part separately
815
+ const parts = match.split('.');
816
+ const allLiterals = parts.every(part => part.trim().startsWith('@'));
817
+
818
+ let replacement;
819
+ if (allLiterals) {
820
+ // All parts are literals, just remove @ prefixes (no signal transformation)
821
+ replacement = parts.map(part => part.trim().replace('@', '')).join('.');
822
+ } else {
823
+ // Transform each part individually
824
+ // Note: In conditions with operators, even @ literals in the first part
825
+ // should be transformed to signals for comparison
826
+ replacement = parts.map((part, index) => {
827
+ const trimmedPart = part.trim();
828
+ if (trimmedPart.startsWith('@')) {
829
+ // For the first part in conditions with operators, we still want to transform to signal
830
+ // For later parts, keep as literal
831
+ if (index === 0) {
832
+ // First part: remove @ but will be transformed to signal later
833
+ return trimmedPart.substring(1);
834
+ } else {
835
+ // Later parts: remove @ and keep as literal (no signal)
836
+ return trimmedPart.substring(1);
837
+ }
838
+ } else {
839
+ // Don't transform keywords
840
+ if (['true', 'false', 'null'].includes(trimmedPart)) {
841
+ return trimmedPart;
842
+ }
843
+ // Check if already a function call
844
+ if (trimmedPart.includes('(')) {
845
+ return trimmedPart;
846
+ }
847
+ // Transform to signal
848
+ return `${trimmedPart}()`;
849
+ }
850
+ }).join('.');
851
+ }
852
+
853
+ // Store replacement and use a temporary marker
854
+ const marker = `__DOT_NOTATION_${replacementCounter++}__`;
855
+ dotNotationReplacements.set(marker, replacement);
856
+ return marker;
857
+ });
858
+
859
+ // Now handle standalone @ identifiers (not in dot notation)
860
+ processedText = processedText.replace(/@([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\.)/g, (match, p1) => {
861
+ return p1; // Remove @ prefix for standalone literals
862
+ });
863
+
864
+ // Transform simple identifiers to function calls like "foo" to "foo()"
865
+ // This regex matches identifiers not followed by an opening parenthesis.
866
+ // This transformation should only apply if we are wrapping in 'computed'.
867
+ if (processedText.includes('!') || processedText.includes('&&') || processedText.includes('||') ||
868
+ processedText.includes('>=') || processedText.includes('<=') || processedText.includes('===') ||
869
+ processedText.includes('!==') || processedText.includes('==') || processedText.includes('!=') ||
870
+ processedText.includes('>') || processedText.includes('<')) {
871
+ // Replace dot notation markers with their processed values BEFORE transforming identifiers
872
+ // This way, expressions like @item.id become item.id() and then item() is transformed
873
+ let textWithReplacements = processedText;
874
+ dotNotationReplacements.forEach((value, marker) => {
875
+ textWithReplacements = textWithReplacements.replace(new RegExp(marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value);
876
+ });
877
+
878
+ const transformedText = textWithReplacements.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match, p1, offset) => {
879
+ // Do not transform keywords (true, false, null) or numeric literals
880
+ if (['true', 'false', 'null'].includes(match) || /^\d+(\.\d+)?$/.test(match)) {
881
+ return match;
882
+ }
883
+
884
+ // Check if this is a marker (starts with __DOT_NOTATION_)
885
+ if (match.startsWith('__DOT_NOTATION_')) {
886
+ return match; // Don't transform markers
887
+ }
888
+
889
+ // Check if the match is inside quotes
890
+ const beforeMatch = processedText.substring(0, offset);
891
+ const afterMatch = processedText.substring(offset + match.length);
892
+ const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
893
+ const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
894
+
895
+ // If we're inside quotes, don't transform
896
+ if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
897
+ return match;
898
+ }
899
+
900
+ // Check if this identifier is part of a dot notation expression
901
+ const charBefore = offset > 0 ? textWithReplacements[offset - 1] : '';
902
+ const charAfter = offset + match.length < textWithReplacements.length ? textWithReplacements[offset + match.length] : '';
903
+
904
+ // If there's a dot before or after, this is part of dot notation
905
+ if (charBefore === '.' || charAfter === '.') {
906
+ // Check if this dot notation expression was a marker (had @ in original)
907
+ const beforeContext = originalText.substring(Math.max(0, offset - 20), offset);
908
+ const afterContext = originalText.substring(offset, Math.min(originalText.length, offset + match.length + 20));
909
+ const fullContext = beforeContext + afterContext;
910
+
911
+ // Check if this identifier had @ in the original
912
+ const hadAt = fullContext.includes('@' + match) || fullContext.match(new RegExp('@' + match.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b'));
913
+
914
+ if (hadAt) {
915
+ // Check if ALL parts of the dot notation had @ (all literals)
916
+ // Find the full dot notation expression in the original
917
+ const dotExprMatch = originalText.match(/(@?[a-zA-Z_][a-zA-Z0-9_]*)(\.@?[a-zA-Z_][a-zA-Z0-9_]*)+/);
918
+ if (dotExprMatch) {
919
+ const dotExpr = dotExprMatch[0];
920
+ const allPartsHadAt = dotExpr.split('.').every(part => part.trim().startsWith('@'));
921
+ if (allPartsHadAt) {
922
+ // All parts were literals (@item.@id), don't transform
923
+ return match;
924
+ }
925
+ }
926
+
927
+ // Only some parts had @ (@item.id or item.@id)
928
+ if (charAfter === '.') {
929
+ // First part: transform to signal even if it had @
930
+ return `${match}()`;
931
+ } else if (charBefore === '.') {
932
+ // Later part: already processed in marker, don't retransform
933
+ return match;
934
+ }
935
+ }
936
+
937
+ // In conditions with operators, transform dot notation expressions to signals
938
+ // (e.g., user.role becomes user().role())
939
+ // This applies to regular dot notation, not markers (which are already processed)
940
+ return `${match}()`;
941
+ }
942
+
943
+ return `${match}()`;
944
+ });
945
+
946
+ return `computed(() => ${transformedText})`;
947
+ }
948
+ // For simple conditions (no !, &&, ||), return the processed text as is.
949
+ // Cases like `myFunction()` are handled by the `functionCallWithArgs` rule.
950
+
951
+ // Replace dot notation markers with their processed values
952
+ let finalText = processedText;
953
+ dotNotationReplacements.forEach((value, marker) => {
954
+ finalText = finalText.replace(new RegExp(marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value);
955
+ });
956
+
957
+ return finalText;
958
+ }
959
+
960
+ functionCall "function call"
961
+ = name:identifier "(" args:functionArgs? ")" {
962
+ return `${name}(${args || ''})`;
963
+ }
964
+
965
+ functionCallWithArgs "function call with complex args"
966
+ = name:identifier "(" args:complexFunctionArgs? ")" {
967
+ return `${name}(${args || ''})`;
968
+ }
969
+
970
+ functionArgs
971
+ = arg:functionArg rest:("," _ functionArg)* {
972
+ return [arg].concat(rest.map(r => r[2])).join(', ');
973
+ }
974
+
975
+ complexFunctionArgs
976
+ = arg:complexFunctionArg rest:("," _ complexFunctionArg)* {
977
+ return [arg].concat(rest.map(r => r[2])).join(', ');
978
+ }
979
+
980
+ functionArg
981
+ = _ value:(identifier / number / string) _ {
982
+ return value;
983
+ }
984
+
985
+ complexFunctionArg "complex function argument"
986
+ = _ value:complexArgExpression _ {
987
+ // Process @ literals and transform identifiers to signals
988
+ // Handle dot notation with @ literals like @item.@id, @item.id, item.@id
989
+ // Similar logic to simpleDynamicPart but for function arguments
990
+
991
+ let processed = value.trim();
992
+ const original = processed;
993
+
994
+ // Check if it's a dot notation expression
995
+ if (processed.match(/^(@?[a-zA-Z_][a-zA-Z0-9_]*)(\.@?[a-zA-Z_][a-zA-Z0-9_]*)*$/)) {
996
+ // Split by dots to handle each part separately
997
+ const parts = processed.split('.');
998
+ const allLiterals = parts.every(part => part.trim().startsWith('@'));
999
+
1000
+ let computedValue;
1001
+
1002
+ if (allLiterals) {
1003
+ // All parts are literals, just remove @ prefixes
1004
+ computedValue = parts.map(part => part.trim().replace('@', '')).join('.');
1005
+ } else {
1006
+ // Transform each part individually
1007
+ computedValue = parts.map(part => {
1008
+ const trimmedPart = part.trim();
1009
+ if (trimmedPart.startsWith('@')) {
1010
+ return trimmedPart.substring(1); // Remove @ prefix for literals
1011
+ } else {
1012
+ // Don't transform keywords
1013
+ if (['true', 'false', 'null'].includes(trimmedPart)) {
1014
+ return trimmedPart;
1015
+ }
1016
+ // Check if it's already a function call
1017
+ if (trimmedPart.includes('(')) {
1018
+ return trimmedPart;
1019
+ }
1020
+ // Transform to signal
1021
+ return `${trimmedPart}()`;
1022
+ }
1023
+ }).join('.');
1024
+ }
1025
+
1026
+ return computedValue;
1027
+ }
1028
+
1029
+ // Handle standalone identifiers (not dot notation)
1030
+ // If it starts with @, remove @ prefix (literal)
1031
+ if (processed.startsWith('@')) {
1032
+ return processed.substring(1);
1033
+ }
1034
+
1035
+ // Don't transform keywords or numbers
1036
+ if (['true', 'false', 'null'].includes(processed) || /^\d+(\.\d+)?$/.test(processed)) {
1037
+ return processed;
1038
+ }
1039
+
1040
+ // Check if it's already a function call
1041
+ if (processed.includes('(')) {
1042
+ return processed;
1043
+ }
1044
+
1045
+ // Transform identifier to signal
1046
+ return `${processed}()`;
1047
+ }
1048
+
1049
+ complexArgExpression "complex argument expression"
1050
+ = $([^,)]* ("(" [^)]* ")" [^,)]*)*) {
1051
+ return text().trim();
1052
+ }
1053
+
1054
+ number
1055
+ = [0-9]+ ("." [0-9]+)? { return text(); }
1056
+
1057
+ string
1058
+ = '"' chars:[^"]* '"' { return text(); }
1059
+ / "'" chars:[^']* "'" { return text(); }
1060
+
1061
+ eventAction
1062
+ = [^"]* { return text(); }
1063
+
1064
+ _ 'whitespace'
1065
+ = [ \t\n\r]*
1066
+
1067
+ identifier
1068
+ = [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
1069
+
1070
+ comment
1071
+ = singleComment+ {
1072
+ return null
1073
+ }
1074
+
1075
+ singleComment
1076
+ = "<!--" _ content:((!("-->") .)* "-->") _ {
1077
+ return null;
1078
+ }
1079
+
1080
+ // Add a special error detection rule for unclosed tags
1081
+ openUnclosedTag "unclosed tag"
1082
+ = "<" _ tagName:tagName &{ return !isVoidElement(tagName); } _ attributes:attributes _ ">" _ content:content _ !("</" _ closingTagName:tagName _ ">") {
1083
+ generateError(
1084
+ `Unclosed tag: <${tagName}> is missing its closing tag`,
1085
+ location()
1086
+ );
1087
+ }
1088
+
1089
+ // Add error detection for unclosed quotes in static attributes
1090
+ unclosedQuote "unclosed string"
1091
+ = attributeName:attributeName _ "=" _ "\"" [^"]* !("\"") {
1092
+ generateError(
1093
+ `Missing closing quote in attribute '${attributeName}'`,
1094
+ location()
1095
+ );
1096
+ }
1097
+
1098
+ // Add error detection for unclosed braces in dynamic attributes
1099
+ unclosedBrace "unclosed brace"
1100
+ = attributeName:attributeName _ "=" _ "{" !("}" / _ "}") [^{}]* {
1101
+ generateError(
1102
+ `Missing closing brace in dynamic attribute '${attributeName}'`,
1103
+ location()
1104
+ );
1105
+ }
1106
+
1107
+ svgElement "SVG element"
1108
+ = "<svg" attrs:([^>]*) ">" content:svgInnerContent "</svg>" _ {
1109
+ const attributes = attrs.join('').trim();
1110
+ // Clean up the content by removing extra whitespace and newlines
1111
+ const cleanContent = content.replace(/\s+/g, ' ').trim();
1112
+ const rawContent = `<svg${attributes ? ' ' + attributes : ''}>${cleanContent}</svg>`;
1113
+ return `h(Svg, { content: \`${rawContent}\` })`;
1114
+ }
1115
+
1116
+ svgInnerContent "SVG inner content"
1117
+ = content:$((!("</svg>") .)*) {
1118
+ return content;
1119
+ }