@canvasengine/compiler 2.0.0-beta.2 → 2.0.0-beta.21

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/grammar.pegjs CHANGED
@@ -5,6 +5,134 @@
5
5
  `at line ${start.line}, column ${start.column} to line ${end.line}, column ${end.column}`;
6
6
  throw new Error(errorMessage);
7
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'
40
+ ]);
41
+
42
+ // DisplayObject special attributes that should not be in attrs
43
+ const displayObjectAttributes = new Set([
44
+ 'x', 'y', 'scale', 'anchor', 'skew', 'tint', 'rotation', 'angle',
45
+ 'zIndex', 'roundPixels', 'cursor', 'visible', 'alpha', 'pivot', 'filters', 'maskOf',
46
+ 'blendMode', 'filterArea', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
47
+ 'aspectRatio', 'flexGrow', 'flexShrink', 'flexBasis', 'rowGap', 'columnGap',
48
+ 'positionType', 'top', 'right', 'bottom', 'left', 'objectFit', 'objectPosition',
49
+ 'transformOrigin', 'flexDirection', 'justifyContent', 'alignItems', 'alignContent',
50
+ 'alignSelf', 'margin', 'padding', 'border', 'gap', 'blur', 'shadow'
51
+ ]);
52
+
53
+ function isDOMElement(tagName) {
54
+ // Don't transform framework components to DOM elements
55
+ if (frameworkComponents.has(tagName)) {
56
+ return false;
57
+ }
58
+ return domElements.has(tagName.toLowerCase());
59
+ }
60
+
61
+ function formatAttributes(attributes) {
62
+ if (attributes.length === 0) {
63
+ return null;
64
+ }
65
+
66
+ // Check if there's exactly one attribute and it's a spread attribute
67
+ if (attributes.length === 1 && attributes[0].startsWith('...')) {
68
+ // Return the identifier directly, removing the '...'
69
+ return attributes[0].substring(3);
70
+ }
71
+
72
+ // Otherwise, format as an object literal
73
+ const formattedAttrs = attributes.map(attr => {
74
+ // If it's a spread attribute, keep it as is
75
+ if (attr.startsWith('...')) {
76
+ return attr;
77
+ }
78
+ // If it's a standalone attribute (doesn't contain ':'), format as shorthand property 'name'
79
+ if (!attr.includes(':')) {
80
+ return attr; // JS object literal shorthand
81
+ }
82
+ // Otherwise (key: value), keep it as is
83
+ return attr;
84
+ });
85
+
86
+ return `{ ${formattedAttrs.join(', ')} }`;
87
+ }
88
+
89
+ function formatDOMElement(tagName, attributes) {
90
+ if (attributes.length === 0) {
91
+ return `h(DOMElement, { element: "${tagName}" })`;
92
+ }
93
+
94
+ // Separate DisplayObject attributes from DOM attributes
95
+ const domAttrs = [];
96
+ const displayObjectAttrs = [];
97
+
98
+ attributes.forEach(attr => {
99
+ // Handle spread attributes
100
+ if (attr.startsWith('...')) {
101
+ displayObjectAttrs.push(attr);
102
+ return;
103
+ }
104
+
105
+ // Extract attribute name
106
+ let attrName;
107
+ if (attr.includes(':')) {
108
+ // Format: "name: value" or "'name': value"
109
+ attrName = attr.split(':')[0].trim().replace(/['"]/g, '');
110
+ } else {
111
+ // Standalone attribute
112
+ attrName = attr.replace(/['"]/g, '');
113
+ }
114
+
115
+ // Check if it's a DisplayObject attribute
116
+ if (displayObjectAttributes.has(attrName)) {
117
+ displayObjectAttrs.push(attr);
118
+ } else {
119
+ domAttrs.push(attr);
120
+ }
121
+ });
122
+
123
+ // Build the result
124
+ const parts = [`element: "${tagName}"`];
125
+
126
+ if (domAttrs.length > 0) {
127
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
128
+ }
129
+
130
+ if (displayObjectAttrs.length > 0) {
131
+ parts.push(...displayObjectAttrs);
132
+ }
133
+
134
+ return `h(DOMElement, { ${parts.join(', ')} })`;
135
+ }
8
136
  }
9
137
 
10
138
  start
@@ -15,30 +143,205 @@ start
15
143
  return `[${elements.join(',')}]`;
16
144
  }
17
145
 
18
- element
146
+ element "component or control structure"
19
147
  = forLoop
20
148
  / ifCondition
149
+ / svgElement
150
+ / domElementWithText
21
151
  / selfClosingElement
22
152
  / openCloseElement
23
- / comment
153
+ / openUnclosedTag
154
+ / comment
24
155
 
25
- selfClosingElement
156
+ selfClosingElement "self-closing component tag"
26
157
  = _ "<" _ tagName:tagName _ attributes:attributes _ "/>" _ {
27
- const attrs = attributes.length > 0 ? `{ ${attributes.join(', ')} }` : null;
28
- return attrs ? `h(${tagName}, ${attrs})` : `h(${tagName})`;
158
+ // Check if it's a DOM element
159
+ if (isDOMElement(tagName)) {
160
+ return formatDOMElement(tagName, attributes);
161
+ }
162
+ // Otherwise, treat as regular component
163
+ const attrsString = formatAttributes(attributes);
164
+ return attrsString ? `h(${tagName}, ${attrsString})` : `h(${tagName})`;
29
165
  }
30
166
 
31
- openCloseElement
32
- = "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ "</" _ closingTagName:tagName _ ">" {
167
+ domElementWithText "DOM element with text content"
168
+ = "<" _ tagName:tagName _ attributes:attributes _ ">" _ text:simpleTextContent _ "</" _ closingTagName:tagName _ ">" _ {
33
169
  if (tagName !== closingTagName) {
34
- error("Mismatched opening and closing tags");
170
+ generateError(
171
+ `Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
172
+ location()
173
+ );
35
174
  }
36
- const attrs = attributes.length > 0 ? `{ ${attributes.join(', ')} }` : null;
175
+
176
+ if (isDOMElement(tagName)) {
177
+ if (attributes.length === 0) {
178
+ return `h(DOMElement, { element: "${tagName}", textContent: ${text} })`;
179
+ }
180
+
181
+ // Separate DisplayObject attributes from DOM attributes
182
+ const domAttrs = [];
183
+ const displayObjectAttrs = [];
184
+
185
+ attributes.forEach(attr => {
186
+ // Handle spread attributes
187
+ if (attr.startsWith('...')) {
188
+ displayObjectAttrs.push(attr);
189
+ return;
190
+ }
191
+
192
+ // Extract attribute name
193
+ let attrName;
194
+ if (attr.includes(':')) {
195
+ // Format: "name: value" or "'name': value"
196
+ attrName = attr.split(':')[0].trim().replace(/['"]/g, '');
197
+ } else {
198
+ // Standalone attribute
199
+ attrName = attr.replace(/['"]/g, '');
200
+ }
201
+
202
+ // Check if it's a DisplayObject attribute
203
+ if (displayObjectAttributes.has(attrName)) {
204
+ displayObjectAttrs.push(attr);
205
+ } else {
206
+ domAttrs.push(attr);
207
+ }
208
+ });
209
+
210
+ // Build the result
211
+ const parts = [`element: "${tagName}"`];
212
+
213
+ if (domAttrs.length > 0) {
214
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
215
+ }
216
+
217
+ parts.push(`textContent: ${text}`);
218
+
219
+ if (displayObjectAttrs.length > 0) {
220
+ parts.push(...displayObjectAttrs);
221
+ }
222
+
223
+ return `h(DOMElement, { ${parts.join(', ')} })`;
224
+ }
225
+
226
+ // If not a DOM element, fall back to regular parsing
227
+ return null;
228
+ }
229
+
230
+ simpleTextContent "simple text content"
231
+ = parts:(simpleDynamicPart / simpleTextPart)+ {
232
+ const validParts = parts.filter(p => p !== null);
233
+ if (validParts.length === 0) return null;
234
+ if (validParts.length === 1) return validParts[0];
235
+
236
+ // Multiple parts - need to concatenate
237
+ const hasSignals = validParts.some(part => part && part.includes && part.includes('()'));
238
+ if (hasSignals) {
239
+ return `computed(() => ${validParts.join(' + ')})`;
240
+ }
241
+ return validParts.join(' + ');
242
+ }
243
+
244
+ simpleTextPart "simple text part"
245
+ = !("@for" / "@if") text:$([^<{@]+) {
246
+ const trimmed = text.trim();
247
+ return trimmed ? `'${trimmed}'` : null;
248
+ }
249
+
250
+ simpleDynamicPart "simple dynamic part"
251
+ = "{" _ expr:attributeValue _ "}" {
252
+ // Handle dynamic expressions like {item.name} or {@text}
253
+ if (expr.trim().match(/^@?[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
254
+ let foundSignal = false;
255
+ const computedValue = expr.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => {
256
+ if (match.startsWith('@')) {
257
+ return match.substring(1);
258
+ }
259
+ foundSignal = true;
260
+ return `${match}()`;
261
+ });
262
+ if (foundSignal) {
263
+ return `computed(() => ${computedValue})`;
264
+ }
265
+ return computedValue;
266
+ }
267
+ return expr;
268
+ }
269
+
270
+ openCloseElement "component with content"
271
+ = "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ "</" _ closingTagName:tagName _ ">" _ {
272
+ if (tagName !== closingTagName) {
273
+ generateError(
274
+ `Mismatched tag: opened <${tagName}> but closed </${closingTagName}>`,
275
+ location()
276
+ );
277
+ }
278
+
279
+ // Check if it's a DOM element
280
+ if (isDOMElement(tagName)) {
281
+ const children = content ? content : null;
282
+
283
+ if (attributes.length === 0) {
284
+ if (children) {
285
+ return `h(DOMElement, { element: "${tagName}" }, ${children})`;
286
+ } else {
287
+ return `h(DOMElement, { element: "${tagName}" })`;
288
+ }
289
+ }
290
+
291
+ // Separate DisplayObject attributes from DOM attributes
292
+ const domAttrs = [];
293
+ const displayObjectAttrs = [];
294
+
295
+ attributes.forEach(attr => {
296
+ // Handle spread attributes
297
+ if (attr.startsWith('...')) {
298
+ displayObjectAttrs.push(attr);
299
+ return;
300
+ }
301
+
302
+ // Extract attribute name
303
+ let attrName;
304
+ if (attr.includes(':')) {
305
+ // Format: "name: value" or "'name': value"
306
+ attrName = attr.split(':')[0].trim().replace(/['"]/g, '');
307
+ } else {
308
+ // Standalone attribute
309
+ attrName = attr.replace(/['"]/g, '');
310
+ }
311
+
312
+ // Check if it's a DisplayObject attribute
313
+ if (displayObjectAttributes.has(attrName)) {
314
+ displayObjectAttrs.push(attr);
315
+ } else {
316
+ domAttrs.push(attr);
317
+ }
318
+ });
319
+
320
+ // Build the result
321
+ const parts = [`element: "${tagName}"`];
322
+
323
+ if (domAttrs.length > 0) {
324
+ parts.push(`attrs: { ${domAttrs.join(', ')} }`);
325
+ }
326
+
327
+ if (displayObjectAttrs.length > 0) {
328
+ parts.push(...displayObjectAttrs);
329
+ }
330
+
331
+ if (children) {
332
+ return `h(DOMElement, { ${parts.join(', ')} }, ${children})`;
333
+ } else {
334
+ return `h(DOMElement, { ${parts.join(', ')} })`;
335
+ }
336
+ }
337
+
338
+ // Otherwise, treat as regular component
339
+ const attrsString = formatAttributes(attributes);
37
340
  const children = content ? content : null;
38
- if (attrs && children) {
39
- return `h(${tagName}, ${attrs}, ${children})`;
40
- } else if (attrs) {
41
- return `h(${tagName}, ${attrs})`;
341
+ if (attrsString && children) {
342
+ return `h(${tagName}, ${attrsString}, ${children})`;
343
+ } else if (attrsString) {
344
+ return `h(${tagName}, ${attrsString})`;
42
345
  } else if (children) {
43
346
  return `h(${tagName}, null, ${children})`;
44
347
  } else {
@@ -46,49 +349,88 @@ openCloseElement
46
349
  }
47
350
  }
48
351
 
49
- / "<" _ tagName:tagName _ attributes:attributes _ {
50
- generateError("Syntaxe d'élément invalide", location());
51
- }
52
-
53
- attributes
352
+ attributes "component attributes"
54
353
  = attrs:(attribute (_ attribute)*)? {
55
354
  return attrs
56
355
  ? [attrs[0]].concat(attrs[1].map(a => a[1]))
57
356
  : [];
58
357
  }
59
358
 
60
- attribute
359
+ attribute "attribute"
61
360
  = staticAttribute
62
361
  / dynamicAttribute
63
362
  / eventHandler
363
+ / spreadAttribute
364
+ / unclosedQuote
365
+ / unclosedBrace
64
366
 
65
- eventHandler
367
+ spreadAttribute "spread attribute"
368
+ = "..." expr:(functionCallExpr / dotNotation) {
369
+ return "..." + expr;
370
+ }
371
+
372
+ functionCallExpr "function call"
373
+ = name:dotNotation "(" args:functionArgs? ")" {
374
+ return `${name}(${args || ''})`;
375
+ }
376
+
377
+ dotNotation "property access"
378
+ = first:identifier rest:("." identifier)* {
379
+ return text();
380
+ }
381
+
382
+ eventHandler "event handler"
66
383
  = "@" eventName:identifier _ "=" _ "{" _ handlerName:attributeValue _ "}" {
67
- return `${eventName}: ${handlerName}`;
384
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
385
+ const formattedName = needsQuotes ? `'${eventName}'` : eventName;
386
+ return `${formattedName}: ${handlerName}`;
68
387
  }
69
388
  / "@" eventName:attributeName _ {
70
- return eventName;
389
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(eventName);
390
+ return needsQuotes ? `'${eventName}'` : eventName;
71
391
  }
72
392
 
73
- dynamicAttribute
393
+ dynamicAttribute "dynamic attribute"
74
394
  = attributeName:attributeName _ "=" _ "{" _ attributeValue:attributeValue _ "}" {
75
- if (attributeValue.trim().match(/^[a-zA-Z_]\w*$/)) {
76
- return `${attributeName}: ${attributeValue}`;
395
+ // Check if attributeName needs to be quoted (contains dash or other invalid JS identifier chars)
396
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
397
+ const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
398
+
399
+ // If it's a complex object literal starting with curly braces, preserve it as is
400
+ if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) {
401
+ return `${formattedName}: ${attributeValue}`;
402
+ }
403
+
404
+ // Handle other types of values
405
+ if (attributeValue.startsWith('h(') || attributeValue.includes('=>')) {
406
+ return `${formattedName}: ${attributeValue}`;
407
+ } else if (attributeValue.trim().match(/^[a-zA-Z_]\w*$/)) {
408
+ return `${formattedName}: ${attributeValue}`;
77
409
  } else {
78
- return `${attributeName}: computed(() => ${attributeValue.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => {
410
+ let foundSignal = false;
411
+ const computedValue = attributeValue.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => {
79
412
  if (match.startsWith('@')) {
80
413
  return match.substring(1);
81
414
  }
415
+ foundSignal = true;
82
416
  return `${match}()`;
83
- })})`;
417
+ });
418
+ if (foundSignal) {
419
+ return `${formattedName}: computed(() => ${computedValue})`;
420
+ }
421
+ return `${formattedName}: ${computedValue}`;
84
422
  }
85
423
  }
86
424
  / attributeName:attributeName _ {
87
- return attributeName;
425
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
426
+ return needsQuotes ? `'${attributeName}'` : attributeName;
88
427
  }
89
428
 
90
- attributeValue
91
- = $([^{}]* ("{" [^{}]* "}" [^{}]*)*) {
429
+ attributeValue "attribute value"
430
+ = element
431
+ / functionWithElement
432
+ / objectLiteral
433
+ / $([^{}]* ("{" [^{}]* "}" [^{}]*)*) {
92
434
  const t = text().trim()
93
435
  if (t.startsWith("{") && t.endsWith("}")) {
94
436
  return `(${t})`;
@@ -96,9 +438,65 @@ attributeValue
96
438
  return t
97
439
  }
98
440
 
99
- staticAttribute
441
+ objectLiteral "object literal"
442
+ = "{" _ objContent:objectContent _ "}" {
443
+ return `{ ${objContent} }`;
444
+ }
445
+
446
+ objectContent
447
+ = prop:objectProperty rest:(_ "," _ objectProperty)* {
448
+ return [prop].concat(rest.map(r => r[3])).join(', ');
449
+ }
450
+ / "" { return ""; }
451
+
452
+ objectProperty
453
+ = key:identifier _ ":" _ value:propertyValue {
454
+ return `${key}: ${value}`;
455
+ }
456
+ / key:identifier {
457
+ return key;
458
+ }
459
+
460
+ propertyValue
461
+ = nestedObject
462
+ / element
463
+ / functionWithElement
464
+ / stringLiteral
465
+ / identifier
466
+
467
+ nestedObject
468
+ = "{" _ objContent:objectContent _ "}" {
469
+ return `{ ${objContent} }`;
470
+ }
471
+
472
+ stringLiteral
473
+ = '"' chars:[^"]* '"' { return text(); }
474
+ / "'" chars:[^']* "'" { return text(); }
475
+
476
+ functionWithElement "function expression"
477
+ = "(" _ params:functionParams? _ ")" _ "=>" _ elem:element {
478
+ return `${params ? `(${params}) =>` : '() =>'} ${elem}`;
479
+ }
480
+
481
+ functionParams
482
+ = destructuredParams
483
+ / simpleParams
484
+
485
+ destructuredParams
486
+ = "{" _ param:identifier rest:(_ "," _ identifier)* _ "}" {
487
+ return `{${[param].concat(rest.map(r => r[3])).join(', ')}}`;
488
+ }
489
+
490
+ simpleParams
491
+ = param:identifier rest:(_ "," _ identifier)* {
492
+ return [param].concat(rest.map(r => r[3])).join(', ');
493
+ }
494
+
495
+ staticAttribute "static attribute"
100
496
  = attributeName:attributeName _ "=" _ "\"" attributeValue:staticValue "\"" {
101
- return `${attributeName}: ${attributeValue}`;
497
+ const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName);
498
+ const formattedName = needsQuotes ? `'${attributeName}'` : attributeName;
499
+ return `${formattedName}: ${attributeValue}`;
102
500
  }
103
501
 
104
502
  eventAttribute
@@ -109,10 +507,10 @@ eventAttribute
109
507
  staticValue
110
508
  = [^"]+ {
111
509
  var val = text();
112
- return isNaN(val) ? `'${val}'` : val;
510
+ return `'${val}'`
113
511
  }
114
512
 
115
- content
513
+ content "component content"
116
514
  = elements:(element)* {
117
515
  const filteredElements = elements.filter(el => el !== null);
118
516
  if (filteredElements.length === 0) return null;
@@ -120,6 +518,8 @@ content
120
518
  return `[${filteredElements.join(', ')}]`;
121
519
  }
122
520
 
521
+
522
+
123
523
  textNode
124
524
  = text:$([^<]+) {
125
525
  const trimmed = text.trim();
@@ -132,20 +532,35 @@ textElement
132
532
  return trimmed ? JSON.stringify(trimmed) : null;
133
533
  }
134
534
 
135
- forLoop
136
- = _ "@for" _ "(" _ variableName:identifier _ "of" _ iterable:identifier _ ")" _ "{" _ content:content _ "}" _ {
137
- return `loop(${iterable}, (${variableName}) => ${content})`;
535
+ forLoop "for loop"
536
+ = _ "@for" _ "(" _ variableName:(tupleDestructuring / identifier) _ "of" _ iterable:iterable _ ")" _ "{" _ content:content _ "}" _ {
537
+ return `loop(${iterable}, ${variableName} => ${content})`;
538
+ }
539
+
540
+ tupleDestructuring "destructuring pattern"
541
+ = "(" _ first:identifier _ "," _ second:identifier _ ")" {
542
+ return `(${first}, ${second})`;
138
543
  }
139
544
 
140
- ifCondition
545
+ ifCondition "if condition"
141
546
  = _ "@if" _ "(" _ condition:condition _ ")" _ "{" _ content:content _ "}" _ {
142
547
  return `cond(${condition}, () => ${content})`;
143
548
  }
144
549
 
145
- tagName
146
- = [a-zA-Z][a-zA-Z0-9]* { return text(); }
550
+ tagName "tag name"
551
+ = tagExpression
552
+
553
+ tagExpression "tag expression"
554
+ = first:tagPart rest:("." tagPart)* {
555
+ return text();
556
+ }
147
557
 
148
- attributeName
558
+ tagPart "tag part"
559
+ = name:[a-zA-Z][a-zA-Z0-9]* args:("(" functionArgs? ")")? {
560
+ return text();
561
+ }
562
+
563
+ attributeName "attribute name"
149
564
  = [a-zA-Z][a-zA-Z0-9-]* { return text(); }
150
565
 
151
566
  eventName
@@ -154,11 +569,69 @@ eventName
154
569
  variableName
155
570
  = [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
156
571
 
157
- iterable
158
- = [a-zA-Z_][a-zA-Z0-9_]* { return text(); }
572
+ iterable "iterable expression"
573
+ = id:identifier "(" _ args:functionArgs? _ ")" { // Direct function call
574
+ return `${id}(${args || ''})`;
575
+ }
576
+ / first:identifier "." rest:dotFunctionChain { // Dot notation possibly with function call
577
+ return `${first}.${rest}`;
578
+ }
579
+ / id:identifier { return id; }
580
+
581
+ dotFunctionChain
582
+ = segment:identifier "(" _ args:functionArgs? _ ")" rest:("." dotFunctionChain)? {
583
+ const restStr = rest ? `.${rest[1]}` : '';
584
+ return `${segment}(${args || ''})${restStr}`;
585
+ }
586
+ / segment:identifier rest:("." dotFunctionChain)? {
587
+ const restStr = rest ? `.${rest[1]}` : '';
588
+ return `${segment}${restStr}`;
589
+ }
590
+
591
+ condition "condition expression"
592
+ = functionCall
593
+ / text_condition:$([^)]*) {
594
+ const originalText = text_condition.trim();
595
+
596
+ // Transform simple identifiers to function calls like "foo" to "foo()"
597
+ // This regex matches identifiers not followed by an opening parenthesis.
598
+ // This transformation should only apply if we are wrapping in 'computed'.
599
+ if (originalText.includes('!') || originalText.includes('&&') || originalText.includes('||')) {
600
+ const transformedText = originalText.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*\()/g, (match) => {
601
+ // Do not transform keywords (true, false, null) or numeric literals
602
+ if (['true', 'false', 'null'].includes(match) || /^\d+(\.\d+)?$/.test(match)) {
603
+ return match;
604
+ }
605
+ return `${match}()`;
606
+ });
607
+ return `computed(() => ${transformedText})`;
608
+ }
609
+ // For simple conditions (no !, &&, ||), return the original text as is.
610
+ // Cases like `myFunction()` are handled by the `functionCall` rule.
611
+ return originalText;
612
+ }
159
613
 
160
- condition
161
- = $([^)]*) { return text().trim(); }
614
+ functionCall "function call"
615
+ = name:identifier "(" args:functionArgs? ")" {
616
+ return `${name}(${args || ''})`;
617
+ }
618
+
619
+ functionArgs
620
+ = arg:functionArg rest:("," _ functionArg)* {
621
+ return [arg].concat(rest.map(r => r[2])).join(', ');
622
+ }
623
+
624
+ functionArg
625
+ = _ value:(identifier / number / string) _ {
626
+ return value;
627
+ }
628
+
629
+ number
630
+ = [0-9]+ ("." [0-9]+)? { return text(); }
631
+
632
+ string
633
+ = '"' chars:[^"]* '"' { return text(); }
634
+ / "'" chars:[^']* "'" { return text(); }
162
635
 
163
636
  eventAction
164
637
  = [^"]* { return text(); }
@@ -177,4 +650,45 @@ comment
177
650
  singleComment
178
651
  = "<!--" _ content:((!("-->") .)* "-->") _ {
179
652
  return null;
653
+ }
654
+
655
+ // Add a special error detection rule for unclosed tags
656
+ openUnclosedTag "unclosed tag"
657
+ = "<" _ tagName:tagName _ attributes:attributes _ ">" _ content:content _ !("</" _ closingTagName:tagName _ ">") {
658
+ generateError(
659
+ `Unclosed tag: <${tagName}> is missing its closing tag`,
660
+ location()
661
+ );
662
+ }
663
+
664
+ // Add error detection for unclosed quotes in static attributes
665
+ unclosedQuote "unclosed string"
666
+ = attributeName:attributeName _ "=" _ "\"" [^"]* !("\"") {
667
+ generateError(
668
+ `Missing closing quote in attribute '${attributeName}'`,
669
+ location()
670
+ );
671
+ }
672
+
673
+ // Add error detection for unclosed braces in dynamic attributes
674
+ unclosedBrace "unclosed brace"
675
+ = attributeName:attributeName _ "=" _ "{" !("}" / _ "}") [^{}]* {
676
+ generateError(
677
+ `Missing closing brace in dynamic attribute '${attributeName}'`,
678
+ location()
679
+ );
680
+ }
681
+
682
+ svgElement "SVG element"
683
+ = "<svg" attrs:([^>]*) ">" content:svgInnerContent "</svg>" _ {
684
+ const attributes = attrs.join('').trim();
685
+ // Clean up the content by removing extra whitespace and newlines
686
+ const cleanContent = content.replace(/\s+/g, ' ').trim();
687
+ const rawContent = `<svg${attributes ? ' ' + attributes : ''}>${cleanContent}</svg>`;
688
+ return `h(Svg, { content: \`${rawContent}\` })`;
689
+ }
690
+
691
+ svgInnerContent "SVG inner content"
692
+ = content:$((!("</svg>") .)*) {
693
+ return content;
180
694
  }