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

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,995 @@
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
+ const eventAttributes = new Set([
48
+ 'click', 'tap', 'pointertap', 'pointerdown', 'pointerup', 'pointermove',
49
+ 'pointerover', 'pointerout', 'pointerupoutside', 'mousedown', 'mouseup',
50
+ 'mousemove', 'mouseover', 'mouseout', 'touchstart', 'touchend', 'touchmove',
51
+ 'touchcancel', 'rightclick', 'keydown', 'keyup', 'keypress'
52
+ ]);
53
+
54
+ // DisplayObject special attributes that should not be in attrs
55
+ const displayObjectAttributes = new Set([
56
+ 'x', 'y', 'scale', 'anchor', 'skew', 'tint', 'rotation', 'angle',
57
+ 'zIndex', 'roundPixels', 'cursor', 'visible', 'alpha', 'pivot', 'filters', 'maskOf',
58
+ 'blendMode', 'filterArea', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
59
+ 'aspectRatio', 'flexGrow', 'flexShrink', 'flexBasis', 'rowGap', 'columnGap',
60
+ 'positionType', 'top', 'right', 'bottom', 'left', 'objectFit', 'objectPosition',
61
+ 'transformOrigin', 'flexDirection', 'justifyContent', 'alignItems', 'alignContent',
62
+ 'alignSelf', 'margin', 'padding', 'border', 'gap', 'blur', 'shadow'
63
+ ]);
64
+
65
+ function isDOMElement(tagName) {
66
+ // Don't transform framework components to DOM elements
67
+ if (frameworkComponents.has(tagName)) {
68
+ return false;
69
+ }
70
+ return domElements.has(tagName.toLowerCase());
71
+ }
72
+
73
+ function isVoidElement(tagName) {
74
+ return voidElements.has(tagName.toLowerCase());
75
+ }
76
+
77
+ function formatAttributes(attributes) {
78
+ if (attributes.length === 0) {
79
+ return null;
80
+ }
81
+
82
+ // Check if there's exactly one attribute and it's a spread attribute
83
+ if (attributes.length === 1 && attributes[0].startsWith('...')) {
84
+ // Return the identifier directly, removing the '...'
85
+ return attributes[0].substring(3);
86
+ }
87
+
88
+ // Otherwise, format as an object literal
89
+ const formattedAttrs = attributes.map(attr => {
90
+ // If it's a spread attribute, keep it as is
91
+ if (attr.startsWith('...')) {
92
+ return attr;
93
+ }
94
+ // If it's a standalone attribute (doesn't contain ':'), format as shorthand property 'name'
95
+ if (!attr.includes(':')) {
96
+ return attr; // JS object literal shorthand
97
+ }
98
+ // Otherwise (key: value), keep it as is
99
+ return attr;
100
+ });
101
+
102
+ return `{ ${formattedAttrs.join(', ')} }`;
103
+ }
104
+
105
+ function formatDOMElement(tagName, attributes) {
106
+ if (attributes.length === 0) {
107
+ return `h(DOMElement, { element: "${tagName}" })`;
108
+ }
109
+
110
+ const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
111
+
112
+ // Build the result
113
+ const parts = [`element: "${tagName}"`];
114
+
115
+ if (domAttrs.length > 0) {
116
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
117
+ }
118
+
119
+ if (displayObjectAttrs.length > 0) {
120
+ parts.push(...displayObjectAttrs);
121
+ }
122
+
123
+ return `h(DOMElement, { ${parts.join(', ')} })`;
124
+ }
125
+
126
+ function splitAttributes(attributes) {
127
+ const domAttrs = [];
128
+ const displayObjectAttrs = [];
129
+ const classValues = [];
130
+ let classInsertIndex = null;
131
+
132
+ attributes.forEach(attr => {
133
+ // Handle spread attributes
134
+ if (attr.startsWith('...')) {
135
+ displayObjectAttrs.push(attr);
136
+ return;
137
+ }
138
+
139
+ // Extract attribute name and value (if present)
140
+ let attrName;
141
+ let attrValue;
142
+ if (attr.includes(':')) {
143
+ const colonIndex = attr.indexOf(':');
144
+ attrName = attr.slice(0, colonIndex).trim().replace(/['"]/g, '');
145
+ attrValue = attr.slice(colonIndex + 1).trim();
146
+ } else {
147
+ // Standalone attribute
148
+ attrName = attr.replace(/['"]/g, '');
149
+ }
150
+
151
+ // Check if it's a DisplayObject attribute
152
+ if (displayObjectAttributes.has(attrName)) {
153
+ displayObjectAttrs.push(attr);
154
+ return;
155
+ }
156
+
157
+ if (attrName === 'class' && attrValue !== undefined) {
158
+ classValues.push(attrValue);
159
+ if (classInsertIndex === null) {
160
+ classInsertIndex = domAttrs.length;
161
+ }
162
+ return;
163
+ }
164
+
165
+ domAttrs.push(attr);
166
+ });
167
+
168
+ if (classValues.length > 0) {
169
+ const mergedClass = classValues.length === 1
170
+ ? `class: ${classValues[0]}`
171
+ : `class: [${classValues.join(', ')}]`;
172
+ if (classInsertIndex === null) {
173
+ domAttrs.push(mergedClass);
174
+ } else {
175
+ domAttrs.splice(classInsertIndex, 0, mergedClass);
176
+ }
177
+ }
178
+
179
+ return { domAttrs, displayObjectAttrs };
180
+ }
181
+
182
+ function formatDOMContainerAttributes(attributes) {
183
+ if (attributes.length === 0) {
184
+ return null;
185
+ }
186
+
187
+ const propsEntries = [];
188
+ const domAttrs = [];
189
+ const classValues = [];
190
+ let classInsertIndex = null;
191
+ let attrsInsertIndex = null;
192
+ let attrsIndex = null;
193
+ let attrsValue = null;
194
+
195
+ attributes.forEach(attr => {
196
+ if (attr.startsWith('...')) {
197
+ propsEntries.push(attr);
198
+ return;
199
+ }
200
+
201
+ let attrName;
202
+ let attrValue;
203
+ if (attr.includes(':')) {
204
+ const colonIndex = attr.indexOf(':');
205
+ attrName = attr.slice(0, colonIndex).trim().replace(/['"]/g, '');
206
+ attrValue = attr.slice(colonIndex + 1).trim();
207
+ } else {
208
+ attrName = attr.replace(/['"]/g, '');
209
+ }
210
+
211
+ if (attrName === 'class' && attrValue !== undefined) {
212
+ classValues.push(attrValue);
213
+ if (classInsertIndex === null) {
214
+ classInsertIndex = domAttrs.length;
215
+ }
216
+ if (attrsInsertIndex === null) {
217
+ attrsInsertIndex = propsEntries.length;
218
+ }
219
+ return;
220
+ }
221
+
222
+ if (attrName === 'style') {
223
+ domAttrs.push(attr);
224
+ if (attrsInsertIndex === null) {
225
+ attrsInsertIndex = propsEntries.length;
226
+ }
227
+ return;
228
+ }
229
+
230
+ if (attrName === 'attrs' && attrValue !== undefined) {
231
+ attrsValue = attrValue;
232
+ attrsIndex = propsEntries.length;
233
+ propsEntries.push(null);
234
+ return;
235
+ }
236
+
237
+ propsEntries.push(attr);
238
+ });
239
+
240
+ if (classValues.length > 0) {
241
+ const mergedClass = classValues.length === 1
242
+ ? `class: ${classValues[0]}`
243
+ : `class: [${classValues.join(', ')}]`;
244
+ if (classInsertIndex === null) {
245
+ domAttrs.push(mergedClass);
246
+ } else {
247
+ domAttrs.splice(classInsertIndex, 0, mergedClass);
248
+ }
249
+ }
250
+
251
+ let attrsEntry = null;
252
+ if (attrsValue && domAttrs.length > 0) {
253
+ attrsEntry = `attrs: { ...${attrsValue}, ${domAttrs.join(', ')} }`;
254
+ } else if (attrsValue) {
255
+ attrsEntry = `attrs: ${attrsValue}`;
256
+ } else if (domAttrs.length > 0) {
257
+ attrsEntry = `attrs: { ${domAttrs.join(', ')} }`;
258
+ }
259
+
260
+ if (attrsEntry) {
261
+ if (attrsIndex !== null) {
262
+ propsEntries[attrsIndex] = attrsEntry;
263
+ } else if (attrsInsertIndex !== null) {
264
+ propsEntries.splice(attrsInsertIndex, 0, attrsEntry);
265
+ } else {
266
+ propsEntries.unshift(attrsEntry);
267
+ }
268
+ }
269
+
270
+ const filteredEntries = propsEntries.filter(entry => entry !== null);
271
+ if (filteredEntries.length === 0) {
272
+ return null;
273
+ }
274
+
275
+ if (filteredEntries.length === 1 && filteredEntries[0].startsWith('...')) {
276
+ return filteredEntries[0].substring(3);
277
+ }
278
+
279
+ return `{ ${filteredEntries.join(', ')} }`;
280
+ }
281
+
282
+ function hasFunctionCall(value) {
283
+ return /[a-zA-Z_][a-zA-Z0-9_]*\s*\(/.test(value);
284
+ }
285
+
286
+ function hasIdentifier(value) {
287
+ return /[a-zA-Z_]/.test(value);
288
+ }
289
+
290
+ function isSimpleAccessor(value) {
291
+ return /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/.test(value.trim());
292
+ }
293
+
294
+ function formatObjectLiteralSpacing(value) {
295
+ const trimmed = value.trim();
296
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
297
+ return value;
298
+ }
299
+ const inner = trimmed.slice(1, -1).trim();
300
+ return `{ ${inner} }`;
301
+ }
302
+
303
+ function transformBareIdentifiersToSignals(value) {
304
+ return value.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g, (match, name, offset) => {
305
+ if (['true', 'false', 'null'].includes(name)) {
306
+ return match;
307
+ }
308
+
309
+ const beforeMatch = value.substring(0, offset);
310
+ const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
311
+ const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
312
+ if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
313
+ return match;
314
+ }
315
+
316
+ const charBefore = offset > 0 ? value[offset - 1] : '';
317
+ const charAfter = offset + match.length < value.length ? value[offset + match.length] : '';
318
+
319
+ if (charBefore === '.' || charAfter === '.') {
320
+ return match;
321
+ }
322
+
323
+ const afterSlice = value.slice(offset + match.length);
324
+ if (/^\s*\(/.test(afterSlice)) {
325
+ return match;
326
+ }
327
+
328
+ return `${name}()`;
329
+ });
330
+ }
331
+ }
332
+
333
+ start
334
+ = _ elements:(element)* _ {
335
+ if (elements.length === 1) {
336
+ return elements[0];
337
+ }
338
+ return `[${elements.join(',')}]`;
339
+ }
340
+
341
+ element "component or control structure"
342
+ = forLoop
343
+ / ifCondition
344
+ / svgElement
345
+ / domElementWithText
346
+ / domElementWithMixedContent
347
+ / selfClosingElement
348
+ / voidElement
349
+ / openCloseElement
350
+ / openUnclosedTag
351
+ / comment
352
+
353
+ selfClosingElement "self-closing component tag"
354
+ = _ "<" _ tagName:tagName _ attributes:attributes _ "/>" _ {
355
+ // Check if it's a DOM element
356
+ if (isDOMElement(tagName)) {
357
+ return formatDOMElement(tagName, attributes);
358
+ }
359
+ if (tagName === 'DOMContainer') {
360
+ const attrsString = formatDOMContainerAttributes(attributes);
361
+ return attrsString ? `h(DOMContainer, ${attrsString})` : `h(DOMContainer)`;
362
+ }
363
+ // Otherwise, treat as regular component
364
+ const attrsString = formatAttributes(attributes);
365
+ return attrsString ? `h(${tagName}, ${attrsString})` : `h(${tagName})`;
366
+ }
367
+
368
+ voidElement "void DOM element tag"
369
+ = _ "<" _ tagName:tagName &{ return isVoidElement(tagName); } _ attributes:attributes _ ">" _ {
370
+ return formatDOMElement(tagName, attributes);
371
+ }
372
+
373
+ domElementWithText "DOM element with text content"
374
+ = "<" _ tagName:tagName &{ return !isVoidElement(tagName); } _ attributes:attributes _ ">" _ text:simpleTextContent _ "</" _ closingTagName:tagName _ ">" _ {
375
+ if (tagName !== closingTagName) {
376
+ generateError(
377
+ `Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
378
+ location()
379
+ );
380
+ }
381
+
382
+ if (isDOMElement(tagName)) {
383
+ if (attributes.length === 0) {
384
+ return `h(DOMElement, { element: "${tagName}", textContent: ${text} })`;
385
+ }
386
+
387
+ const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
388
+
389
+ // Build the result
390
+ const parts = [`element: "${tagName}"`];
391
+
392
+ if (domAttrs.length > 0) {
393
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
394
+ }
395
+
396
+ parts.push(`textContent: ${text}`);
397
+
398
+ if (displayObjectAttrs.length > 0) {
399
+ parts.push(...displayObjectAttrs);
400
+ }
401
+
402
+ return `h(DOMElement, { ${parts.join(', ')} })`;
403
+ }
404
+
405
+ // If not a DOM element, fall back to regular parsing
406
+ return null;
407
+ }
408
+
409
+ domElementWithMixedContent "DOM element with mixed content"
410
+ = "<" _ tagName:tagName &{ return isDOMElement(tagName) && !isVoidElement(tagName); } _ attributes:attributes _ ">" _ children:domContent _ "</" _ closingTagName:tagName _ ">" _ {
411
+ if (tagName !== closingTagName) {
412
+ generateError(
413
+ `Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
414
+ location()
415
+ );
416
+ }
417
+
418
+ const childrenContent = children ? children : null;
419
+
420
+ if (attributes.length === 0) {
421
+ if (childrenContent) {
422
+ return `h(DOMElement, { element: "${tagName}" }, ${childrenContent})`;
423
+ } else {
424
+ return `h(DOMElement, { element: "${tagName}" })`;
425
+ }
426
+ }
427
+
428
+ const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
429
+
430
+ // Build the result
431
+ const parts = [`element: "${tagName}"`];
432
+
433
+ if (domAttrs.length > 0) {
434
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
435
+ }
436
+
437
+ if (displayObjectAttrs.length > 0) {
438
+ parts.push(...displayObjectAttrs);
439
+ }
440
+
441
+ if (childrenContent) {
442
+ return `h(DOMElement, { ${parts.join(', ')} }, ${childrenContent})`;
443
+ } else {
444
+ return `h(DOMElement, { ${parts.join(', ')} })`;
445
+ }
446
+ }
447
+
448
+ simpleTextContent "simple text content"
449
+ = parts:(simpleDynamicPart / simpleTextPart)+ {
450
+ const validParts = parts.filter(p => p !== null);
451
+ if (validParts.length === 0) return null;
452
+ if (validParts.length === 1) return validParts[0];
453
+
454
+ // Multiple parts - need to concatenate
455
+ const normalizedParts = validParts.map(part => {
456
+ if (typeof part === 'string' && part.startsWith('computed(() => ') && part.endsWith(')')) {
457
+ return part.slice('computed(() => '.length, -1);
458
+ }
459
+ return part;
460
+ });
461
+ const hasSignals = normalizedParts.some(part => part && part.includes && part.includes('()'));
462
+ if (hasSignals) {
463
+ return `computed(() => ${normalizedParts.join(' + ')})`;
464
+ }
465
+ return normalizedParts.join(' + ');
466
+ }
467
+
468
+ simpleTextPart "simple text part"
469
+ = !("@for" / "@if") text:$([^<{@]+) {
470
+ const trimmed = text.trim();
471
+ if (!trimmed) return null;
472
+ const escaped = text
473
+ .replace(/\\/g, '\\\\')
474
+ .replace(/'/g, "\\'")
475
+ .replace(/\r/g, '\\r')
476
+ .replace(/\n/g, '\\n')
477
+ .replace(/\t/g, '\\t');
478
+ return `'${escaped}'`;
479
+ }
480
+
481
+ simpleDynamicPart "simple dynamic part"
482
+ = "{{" _ expr:attributeValue _ "}}" {
483
+ const trimmedExpr = expr.trim();
484
+ if (!trimmedExpr) {
485
+ return trimmedExpr;
486
+ }
487
+ if (hasFunctionCall(trimmedExpr)) {
488
+ return `computed(() => ${trimmedExpr})`;
489
+ }
490
+ return trimmedExpr;
491
+ }
492
+ / "{" _ expr:attributeValue _ "}" {
493
+ const trimmedExpr = expr.trim();
494
+ if (!trimmedExpr) {
495
+ return trimmedExpr;
496
+ }
497
+ if (hasFunctionCall(trimmedExpr)) {
498
+ return `computed(() => ${trimmedExpr})`;
499
+ }
500
+ return trimmedExpr;
501
+ }
502
+
503
+ openCloseElement "component with content"
504
+ = "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ "</" _ closingTagName:tagName _ ">" _ {
505
+ if (tagName !== closingTagName) {
506
+ generateError(
507
+ `Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
508
+ location()
509
+ );
510
+ }
511
+
512
+ const children = content ? content : null;
513
+
514
+ // Check if it's a DOM element
515
+ if (isDOMElement(tagName)) {
516
+ if (attributes.length === 0) {
517
+ if (children) {
518
+ return `h(DOMElement, { element: "${tagName}" }, ${children})`;
519
+ } else {
520
+ return `h(DOMElement, { element: "${tagName}" })`;
521
+ }
522
+ }
523
+
524
+ const { domAttrs, displayObjectAttrs } = splitAttributes(attributes);
525
+
526
+ // Build the result
527
+ const parts = [`element: "${tagName}"`];
528
+
529
+ if (domAttrs.length > 0) {
530
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
531
+ }
532
+
533
+ if (displayObjectAttrs.length > 0) {
534
+ parts.push(...displayObjectAttrs);
535
+ }
536
+
537
+ if (children) {
538
+ return `h(DOMElement, { ${parts.join(', ')} }, ${children})`;
539
+ } else {
540
+ return `h(DOMElement, { ${parts.join(', ')} })`;
541
+ }
542
+ }
543
+
544
+ // Otherwise, treat as regular component
545
+ if (tagName === 'DOMContainer') {
546
+ const attrsString = formatDOMContainerAttributes(attributes);
547
+ if (attrsString && children) {
548
+ return `h(DOMContainer, ${attrsString}, ${children})`;
549
+ } else if (attrsString) {
550
+ return `h(DOMContainer, ${attrsString})`;
551
+ } else if (children) {
552
+ return `h(DOMContainer, null, ${children})`;
553
+ } else {
554
+ return `h(DOMContainer)`;
555
+ }
556
+ }
557
+
558
+ const attrsString = formatAttributes(attributes);
559
+ if (attrsString && children) {
560
+ return `h(${tagName}, ${attrsString}, ${children})`;
561
+ } else if (attrsString) {
562
+ return `h(${tagName}, ${attrsString})`;
563
+ } else if (children) {
564
+ return `h(${tagName}, null, ${children})`;
565
+ } else {
566
+ return `h(${tagName})`;
567
+ }
568
+ }
569
+
570
+ attributes "component attributes"
571
+ = attrs:(attribute (_ attribute)*)? {
572
+ return attrs
573
+ ? [attrs[0]].concat(attrs[1].map(a => a[1]))
574
+ : [];
575
+ }
576
+
577
+ attribute "attribute"
578
+ = staticAttribute
579
+ / dynamicAttribute
580
+ / eventHandler
581
+ / spreadAttribute
582
+ / unclosedQuote
583
+ / unclosedBrace
584
+
585
+ spreadAttribute "spread attribute"
586
+ = "..." expr:(functionCallExpr / dotNotation) {
587
+ return "..." + expr;
588
+ }
589
+
590
+ functionCallExpr "function call"
591
+ = name:dotNotation "(" args:functionArgs? ")" {
592
+ return `${name}(${args || ''})`;
593
+ }
594
+
595
+ dotNotation "property access"
596
+ = first:identifier rest:("." identifier)* {
597
+ return text();
598
+ }
599
+
600
+ eventHandler "event handler"
601
+ = "@" eventName:identifier _ "=" _ "{" _ handlerName:attributeValue _ "}" {
602
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
603
+ const formattedName = needsQuotes ? `'${eventName}'` : eventName;
604
+ return `${formattedName}: ${handlerName}`;
605
+ }
606
+ / "@" eventName:attributeName _ {
607
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
608
+ return needsQuotes ? `'${eventName}'` : eventName;
609
+ }
610
+
611
+ dynamicAttribute "dynamic attribute"
612
+ = attributeName:attributeName _ "=" _ "{" _ attributeValue:attributeValue _ "}" {
613
+ // Check if attributeName needs to be quoted (contains dash or other invalid JS identifier chars)
614
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
615
+ const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
616
+
617
+ if (eventAttributes.has(attributeName)) {
618
+ return `${formattedName}: ${attributeValue}`;
619
+ }
620
+
621
+ // If it's a complex object with strings, preserve it as is
622
+ if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}') &&
623
+ (attributeValue.includes('"') || attributeValue.includes("'"))) {
624
+ return `${formattedName}: ${attributeValue}`;
625
+ }
626
+
627
+ // If it's a template string, keep it as-is
628
+ if (attributeValue.trim().startsWith('`') && attributeValue.trim().endsWith('`')) {
629
+ return formattedName + ': ' + attributeValue;
630
+ }
631
+
632
+ // Handle other types of values
633
+ if (attributeValue.startsWith('h(') || attributeValue.includes('=>')) {
634
+ return `${formattedName}: ${attributeValue}`;
635
+ }
636
+
637
+ const trimmedValue = attributeValue.trim();
638
+ if (trimmedValue.match(/^[a-zA-Z_]\w*$/)) {
639
+ return `${formattedName}: ${attributeValue}`;
640
+ }
641
+
642
+ if (/^\d+(\.\d+)?$/.test(trimmedValue) || ['true', 'false', 'null'].includes(trimmedValue)) {
643
+ return `${formattedName}: ${attributeValue}`;
644
+ }
645
+
646
+ if (isSimpleAccessor(trimmedValue)) {
647
+ return `${formattedName}: ${attributeValue}`;
648
+ }
649
+
650
+ const isObjectLiteral = trimmedValue.startsWith('{') && trimmedValue.endsWith('}');
651
+ const isArrayLiteral = trimmedValue.startsWith('[') && trimmedValue.endsWith(']');
652
+ if (isObjectLiteral) {
653
+ const formattedObject = formatObjectLiteralSpacing(attributeValue);
654
+ if (hasFunctionCall(trimmedValue)) {
655
+ return `${formattedName}: computed(() => (${formattedObject}))`;
656
+ }
657
+ return `${formattedName}: ${formattedObject}`;
658
+ }
659
+ if (isArrayLiteral) {
660
+ if (hasFunctionCall(trimmedValue)) {
661
+ return `${formattedName}: computed(() => ${attributeValue})`;
662
+ }
663
+ return `${formattedName}: ${attributeValue}`;
664
+ }
665
+
666
+ if (hasFunctionCall(trimmedValue)) {
667
+ return `${formattedName}: computed(() => ${attributeValue})`;
668
+ }
669
+
670
+ if (!hasIdentifier(trimmedValue)) {
671
+ return `${formattedName}: ${attributeValue}`;
672
+ }
673
+
674
+ const computedValue = transformBareIdentifiersToSignals(attributeValue);
675
+ return `${formattedName}: computed(() => ${computedValue})`;
676
+ }
677
+ / attributeName:attributeName _ {
678
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
679
+ return needsQuotes ? `'${attributeName}'` : attributeName;
680
+ }
681
+
682
+ attributeValue "attribute value"
683
+ = element
684
+ / functionWithElement
685
+ / objectLiteral
686
+ / $([^{}]* ("{" [^{}]* "}" [^{}]*)*) {
687
+ return text().trim()
688
+ }
689
+
690
+ objectLiteral "object literal"
691
+ = "{" _ objContent:objectContent _ "}" {
692
+ return `{ ${objContent} }`;
693
+ }
694
+
695
+ objectContent
696
+ = prop:objectProperty rest:(_ "," _ objectProperty)* {
697
+ return [prop].concat(rest.map(r => r[3])).join(', ');
698
+ }
699
+ / "" { return ""; }
700
+
701
+ objectProperty
702
+ = key:identifier _ ":" _ value:propertyValue {
703
+ return `${key}: ${value}`;
704
+ }
705
+ / key:identifier {
706
+ return key;
707
+ }
708
+
709
+ propertyValue
710
+ = nestedObject
711
+ / element
712
+ / functionWithElement
713
+ / stringLiteral
714
+ / number
715
+ / identifier
716
+
717
+ nestedObject
718
+ = "{" _ objContent:objectContent _ "}" {
719
+ return `{ ${objContent} }`;
720
+ }
721
+
722
+ stringLiteral
723
+ = '"' chars:[^"]* '"' { return text(); }
724
+ / "'" chars:[^']* "'" { return text(); }
725
+
726
+ functionWithElement "function expression"
727
+ = "(" _ params:functionParams? _ ")" _ "=>" _ elem:element {
728
+ return `${params ? `(${params}) =>` : '() =>'} ${elem}`;
729
+ }
730
+
731
+ functionParams
732
+ = destructuredParams
733
+ / simpleParams
734
+
735
+ destructuredParams
736
+ = "{" _ param:identifier rest:(_ "," _ identifier)* _ "}" {
737
+ return `{${[param].concat(rest.map(r => r[3])).join(', ')}}`;
738
+ }
739
+
740
+ simpleParams
741
+ = param:identifier rest:(_ "," _ identifier)* {
742
+ return [param].concat(rest.map(r => r[3])).join(', ');
743
+ }
744
+
745
+ staticAttribute "static attribute"
746
+ = attributeName:attributeName _ "=" _ "\"" attributeValue:staticValue "\"" {
747
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
748
+ const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
749
+ return `${formattedName}: ${attributeValue}`;
750
+ }
751
+
752
+ eventAttribute
753
+ = "(" _ eventName:eventName _ ")" _ "=" _ "\"" eventAction:eventAction "\"" {
754
+ return `${eventName}: () => { ${eventAction} }`;
755
+ }
756
+
757
+ staticValue
758
+ = [^"]+ {
759
+ var val = text();
760
+ return `'${val}'`
761
+ }
762
+
763
+ content "component content"
764
+ = elements:(element)* {
765
+ const filteredElements = elements.filter(el => el !== null);
766
+ if (filteredElements.length === 0) return null;
767
+ if (filteredElements.length === 1) return filteredElements[0];
768
+ return `[${filteredElements.join(', ')}]`;
769
+ }
770
+
771
+ domContent "DOM content"
772
+ = elements:(domContentPart)* {
773
+ const filteredElements = elements.filter(el => el !== null);
774
+ if (filteredElements.length === 0) return null;
775
+ if (filteredElements.length === 1) return filteredElements[0];
776
+ return `[${filteredElements.join(', ')}]`;
777
+ }
778
+
779
+ domContentPart
780
+ = element
781
+ / simpleTextContent
782
+
783
+
784
+
785
+ textNode
786
+ = text:$([^<]+) {
787
+ const trimmed = text.trim();
788
+ return trimmed ? `'${trimmed}'` : null;
789
+ }
790
+
791
+ textElement
792
+ = text:[^<>]+ {
793
+ const trimmed = text.join('').trim();
794
+ return trimmed ? JSON.stringify(trimmed) : null;
795
+ }
796
+
797
+ forLoop "for loop"
798
+ = _ "@for" _ "(" _ variableName:(tupleDestructuring / identifier) _ "of" _ iterable:iterable _ ")" _ "{" _ content:content _ "}" _ {
799
+ return `loop(${iterable}, ${variableName} => ${content})`;
800
+ }
801
+
802
+ tupleDestructuring "destructuring pattern"
803
+ = "(" _ first:identifier _ "," _ second:identifier _ ")" {
804
+ return `(${first}, ${second})`;
805
+ }
806
+
807
+ ifCondition "if condition"
808
+ = _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ elseIfs:elseIfClause* elseClause:elseClause? _ {
809
+ let result = `cond(${condition}, () => ${content}`;
810
+
811
+ // Add else if clauses
812
+ elseIfs.forEach(elseIf => {
813
+ result += `, [${elseIf.condition}, () => ${elseIf.content}]`;
814
+ });
815
+
816
+ // Add else clause if present
817
+ if (elseClause) {
818
+ result += `, () => ${elseClause}`;
819
+ }
820
+
821
+ result += ')';
822
+ return result;
823
+ }
824
+
825
+ elseIfClause "else if clause"
826
+ = _ "@else" _ "if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
827
+ return { condition, content };
828
+ }
829
+
830
+ elseClause "else clause"
831
+ = _ "@else" _ "{" _ content:content _ "}" _ {
832
+ return content;
833
+ }
834
+
835
+ tagName "tag name"
836
+ = tagExpression
837
+
838
+ tagExpression "tag expression"
839
+ = first:tagPart rest:("." tagPart)* {
840
+ return text();
841
+ }
842
+
843
+ tagPart "tag part"
844
+ = name:[a-zA-Z][a-zA-Z0-9]* args:("(" functionArgs? ")")? {
845
+ return text();
846
+ }
847
+
848
+ attributeName "attribute name"
849
+ = [a-zA-Z][a-zA-Z0-9-]* { return text(); }
850
+
851
+ eventName
852
+ = [a-zA-Z][a-zA-Z0-9-]* { return text(); }
853
+
854
+ variableName
855
+ = [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
856
+
857
+ iterable "iterable expression"
858
+ = id:identifier "(" _ args:functionArgs? _ ")" { // Direct function call
859
+ return `${id}(${args || ''})`;
860
+ }
861
+ / first:identifier "." rest:dotFunctionChain { // Dot notation possibly with function call
862
+ return `${first}.${rest}`;
863
+ }
864
+ / id:identifier { return id; }
865
+
866
+ dotFunctionChain
867
+ = segment:identifier "(" _ args:functionArgs? _ ")" rest:("." dotFunctionChain)? {
868
+ const restStr = rest ? `.${rest[1]}` : '';
869
+ return `${segment}(${args || ''})${restStr}`;
870
+ }
871
+ / segment:identifier rest:("." dotFunctionChain)? {
872
+ const restStr = rest ? `.${rest[1]}` : '';
873
+ return `${segment}${restStr}`;
874
+ }
875
+
876
+ condition "condition expression"
877
+ = text:$(conditionChunk*) {
878
+ const originalText = text.trim();
879
+ if (!originalText) {
880
+ return originalText;
881
+ }
882
+
883
+ const hasOperator = /[!<>=&|]/.test(originalText);
884
+ if (hasOperator) {
885
+ return `computed(() => ${originalText})`;
886
+ }
887
+
888
+ return originalText;
889
+ }
890
+
891
+ conditionChunk
892
+ = "(" conditionChunk* ")"
893
+ / [^()]
894
+
895
+ functionCall "function call"
896
+ = name:identifier "(" args:functionArgs? ")" {
897
+ return `${name}(${args || ''})`;
898
+ }
899
+
900
+ functionCallWithArgs "function call with complex args"
901
+ = name:identifier "(" args:complexFunctionArgs? ")" {
902
+ return `${name}(${args || ''})`;
903
+ }
904
+
905
+ functionArgs
906
+ = arg:functionArg rest:("," _ functionArg)* {
907
+ return [arg].concat(rest.map(r => r[2])).join(', ');
908
+ }
909
+
910
+ complexFunctionArgs
911
+ = arg:complexFunctionArg rest:("," _ complexFunctionArg)* {
912
+ return [arg].concat(rest.map(r => r[2])).join(', ');
913
+ }
914
+
915
+ functionArg
916
+ = _ value:(identifier / number / string) _ {
917
+ return value;
918
+ }
919
+
920
+ complexFunctionArg "complex function argument"
921
+ = _ value:complexArgExpression _ {
922
+ return value.trim();
923
+ }
924
+
925
+ complexArgExpression "complex argument expression"
926
+ = $([^,)]* ("(" [^)]* ")" [^,)]*)*) {
927
+ return text().trim();
928
+ }
929
+
930
+ number
931
+ = [0-9]+ ("." [0-9]+)? { return text(); }
932
+
933
+ string
934
+ = '"' chars:[^"]* '"' { return text(); }
935
+ / "'" chars:[^']* "'" { return text(); }
936
+
937
+ eventAction
938
+ = [^"]* { return text(); }
939
+
940
+ _ 'whitespace'
941
+ = [ \t\n\r]*
942
+
943
+ identifier
944
+ = [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
945
+
946
+ comment
947
+ = singleComment+ {
948
+ return null
949
+ }
950
+
951
+ singleComment
952
+ = "<!--" _ content:((!("-->") .)* "-->") _ {
953
+ return null;
954
+ }
955
+
956
+ // Add a special error detection rule for unclosed tags
957
+ openUnclosedTag "unclosed tag"
958
+ = "<" _ tagName:tagName &{ return !isVoidElement(tagName); } _ attributes:attributes _ ">" _ content:content _ !("</" _ closingTagName:tagName _ ">") {
959
+ generateError(
960
+ `Unclosed tag: <${tagName}> is missing its closing tag`,
961
+ location()
962
+ );
963
+ }
964
+
965
+ // Add error detection for unclosed quotes in static attributes
966
+ unclosedQuote "unclosed string"
967
+ = attributeName:attributeName _ "=" _ "\"" [^"]* !("\"") {
968
+ generateError(
969
+ `Missing closing quote in attribute '${attributeName}'`,
970
+ location()
971
+ );
972
+ }
973
+
974
+ // Add error detection for unclosed braces in dynamic attributes
975
+ unclosedBrace "unclosed brace"
976
+ = attributeName:attributeName _ "=" _ "{" !("}" / _ "}") [^{}]* {
977
+ generateError(
978
+ `Missing closing brace in dynamic attribute '${attributeName}'`,
979
+ location()
980
+ );
981
+ }
982
+
983
+ svgElement "SVG element"
984
+ = "<svg" attrs:([^>]*) ">" content:svgInnerContent "</svg>" _ {
985
+ const attributes = attrs.join('').trim();
986
+ // Clean up the content by removing extra whitespace and newlines
987
+ const cleanContent = content.replace(/\s+/g, ' ').trim();
988
+ const rawContent = `<svg${attributes ? ' ' + attributes : ''}>${cleanContent}</svg>`;
989
+ return `h(Svg, { content: \`${rawContent}\` })`;
990
+ }
991
+
992
+ svgInnerContent "SVG inner content"
993
+ = content:$((!("</svg>") .)*) {
994
+ return content;
995
+ }