@canvasengine/compiler 2.0.0-beta.27 → 2.0.0-beta.28

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
@@ -248,8 +248,49 @@ simpleTextPart "simple text part"
248
248
  }
249
249
 
250
250
  simpleDynamicPart "simple dynamic part"
251
- = "{" _ expr:attributeValue _ "}" {
252
- // Handle dynamic expressions like {item.name} or {@text}
251
+ = "{{" _ expr:attributeValue _ "}}" {
252
+ // Handle double brace expressions like {{ object.x }} or {{ @object.x }} or {{ @object.@x }}
253
+ if (expr.trim().match(/^(@?[a-zA-Z_][a-zA-Z0-9_]*)(\.@?[a-zA-Z_][a-zA-Z0-9_]*)*$/)) {
254
+ let foundSignal = false;
255
+ let hasLiterals = false;
256
+
257
+ // Split by dots to handle each part separately
258
+ const parts = expr.split('.');
259
+ const allLiterals = parts.every(part => part.trim().startsWith('@'));
260
+
261
+ let computedValue;
262
+
263
+ if (allLiterals) {
264
+ // All parts are literals, just remove @ prefixes
265
+ computedValue = parts.map(part => part.replace('@', '')).join('.');
266
+ hasLiterals = true;
267
+ } else {
268
+ // Transform each part individually
269
+ computedValue = parts.map(part => {
270
+ const trimmedPart = part.trim();
271
+ if (trimmedPart.startsWith('@')) {
272
+ hasLiterals = true;
273
+ return trimmedPart.substring(1); // Remove @ prefix for literals
274
+ } else {
275
+ // Don't transform keywords
276
+ if (['true', 'false', 'null'].includes(trimmedPart)) {
277
+ return trimmedPart;
278
+ }
279
+ foundSignal = true;
280
+ return `${trimmedPart}()`;
281
+ }
282
+ }).join('.');
283
+ }
284
+
285
+ if (foundSignal && !allLiterals) {
286
+ return `computed(() => ${computedValue})`;
287
+ }
288
+ return computedValue;
289
+ }
290
+ return expr;
291
+ }
292
+ / "{" _ expr:attributeValue _ "}" {
293
+ // Handle single brace expressions like {item.name} or {@text}
253
294
  if (expr.trim().match(/^@?[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
254
295
  let foundSignal = false;
255
296
  const computedValue = expr.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => {
@@ -403,9 +444,43 @@ dynamicAttribute "dynamic attribute"
403
444
  return `${formattedName}: ${attributeValue}`;
404
445
  }
405
446
 
406
- // If it's a template string, preserve it as is
447
+ // If it's a template string, transform expressions inside ${}
407
448
  if (attributeValue.trim().startsWith('`') && attributeValue.trim().endsWith('`')) {
408
- return `${formattedName}: ${attributeValue}`;
449
+ // Transform expressions inside ${} in template strings
450
+ let transformedTemplate = attributeValue;
451
+
452
+ // Find and replace ${expression} patterns
453
+ let startIndex = 0;
454
+ while (true) {
455
+ const dollarIndex = transformedTemplate.indexOf('${', startIndex);
456
+ if (dollarIndex === -1) break;
457
+
458
+ const braceIndex = transformedTemplate.indexOf('}', dollarIndex);
459
+ if (braceIndex === -1) break;
460
+
461
+ const expr = transformedTemplate.substring(dollarIndex + 2, braceIndex);
462
+ const trimmedExpr = expr.trim();
463
+
464
+ let replacement;
465
+ if (trimmedExpr.startsWith('@')) {
466
+ // Remove @ prefix for literals
467
+ replacement = '${' + trimmedExpr.substring(1) + '}';
468
+ } else if (trimmedExpr.match(/^[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
469
+ // Transform identifiers to signals
470
+ replacement = '${' + trimmedExpr + '()}';
471
+ } else {
472
+ // Keep as is for complex expressions
473
+ replacement = '${' + expr + '}';
474
+ }
475
+
476
+ transformedTemplate = transformedTemplate.substring(0, dollarIndex) +
477
+ replacement +
478
+ transformedTemplate.substring(braceIndex + 1);
479
+
480
+ startIndex = dollarIndex + replacement.length;
481
+ }
482
+
483
+ return formattedName + ': ' + transformedTemplate;
409
484
  }
410
485
 
411
486
  // Handle other types of values
@@ -414,31 +489,51 @@ dynamicAttribute "dynamic attribute"
414
489
  } else if (attributeValue.trim().match(/^[a-zA-Z_]\w*$/)) {
415
490
  return `${formattedName}: ${attributeValue}`;
416
491
  } else {
492
+ // Check if this is an object or array literal
493
+ const isObjectLiteral = attributeValue.trim().startsWith('{ ') && attributeValue.trim().endsWith(' }');
494
+ const isArrayLiteral = attributeValue.trim().startsWith('[') && attributeValue.trim().endsWith(']');
495
+
417
496
  let foundSignal = false;
418
497
  let hasLiterals = false;
419
- const computedValue = attributeValue.replace(/@?([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*:)/g, (match, p1, offset) => {
420
- // Don't transform keywords, numbers, or if we're inside quotes
421
- if (['true', 'false', 'null'].includes(p1) || /^\d+(\.\d+)?$/.test(p1)) {
422
- return match;
423
- }
424
-
425
- // Check if we're inside a string literal
426
- const beforeMatch = attributeValue.substring(0, offset);
427
- const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
428
- const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
429
-
430
- // If we're inside quotes, don't transform
431
- if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
432
- return match;
433
- }
498
+ let computedValue = attributeValue;
499
+
500
+ // For simple object and array literals (like {x: x, y: 20} or [x, 20]),
501
+ // don't use computed() at all and don't transform identifiers
502
+ if ((isObjectLiteral || isArrayLiteral) && !attributeValue.includes('()')) {
503
+ // Don't transform anything, return as is
504
+ foundSignal = false;
505
+ computedValue = attributeValue;
506
+ } else {
507
+ // Apply signal transformation for other values
508
+ computedValue = attributeValue.replace(/@?([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*:)/g, (match, p1, offset) => {
509
+ // Don't transform keywords, numbers, or if we're inside quotes
510
+ if (['true', 'false', 'null'].includes(p1) || /^\d+(\.\d+)?$/.test(p1)) {
511
+ return match;
512
+ }
513
+
514
+ // Check if we're inside a string literal
515
+ const beforeMatch = attributeValue.substring(0, offset);
516
+ const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length;
517
+ const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length;
518
+
519
+ // If we're inside quotes, don't transform
520
+ if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) {
521
+ return match;
522
+ }
523
+
524
+ if (match.startsWith('@')) {
525
+ hasLiterals = true;
526
+ return p1; // Remove @ prefix
527
+ }
528
+ foundSignal = true;
529
+ return `${p1}()`;
530
+ });
434
531
 
435
- if (match.startsWith('@')) {
436
- hasLiterals = true;
437
- return p1; // Remove @ prefix
532
+ // Check if any values already contain signals (ending with ())
533
+ if (attributeValue.includes('()')) {
534
+ foundSignal = true;
438
535
  }
439
- foundSignal = true;
440
- return `${p1}()`;
441
- });
536
+ }
442
537
 
443
538
  if (foundSignal) {
444
539
  // For objects, wrap in parentheses
@@ -455,12 +550,7 @@ dynamicAttribute "dynamic attribute"
455
550
  return `${formattedName}: ${computedValue}`;
456
551
  }
457
552
 
458
- // For static objects, add parentheses if it's an object
459
- if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) {
460
- // Remove spaces for objects in parentheses
461
- const cleanedObject = computedValue.replace(/{ /g, '{').replace(/ }/g, '}');
462
- return `${formattedName}: (${cleanedObject})`;
463
- }
553
+ // For static objects and arrays, return as is without parentheses
464
554
  return `${formattedName}: ${computedValue}`;
465
555
  }
466
556
  }
@@ -474,11 +564,7 @@ attributeValue "attribute value"
474
564
  / functionWithElement
475
565
  / objectLiteral
476
566
  / $([^{}]* ("{" [^{}]* "}" [^{}]*)*) {
477
- const t = text().trim()
478
- if (t.startsWith("{") && t.endsWith("}")) {
479
- return `(${t})`;
480
- }
481
- return t
567
+ return text().trim()
482
568
  }
483
569
 
484
570
  objectLiteral "object literal"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canvasengine/compiler",
3
- "version": "2.0.0-beta.27",
3
+ "version": "2.0.0-beta.28",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -286,22 +286,23 @@ describe("Compiler", () => {
286
286
  const output = parser.parse(input);
287
287
  expect(output).toBe(`cond(sprite.visible, () => h(Sprite), [sprite.loading, () => h(Text, { text: 'Loading...' })], () => h(Text, { text: 'Not available' }))`);
288
288
  });
289
- // test("should compile component with templating string", () => {
290
- // const input = `<Canvas width={\`direction: \${direction}\`} />`;
291
- // const output = parser.parse(input);
292
- // expect(output).toBe(`h(Canvas, { width: \`direction: \${direction()}\` })`);
293
- // });
294
-
295
- // test("should compile component with templating string with @ (literal)", () => {
296
- // const input = `<Canvas width={\`direction: \${@direction}\`} />`;
297
- // const output = parser.parse(input);
298
- // expect(output).toBe(`h(Canvas, { width: \`direction: \${direction}\` })`);
299
- // });
289
+
290
+ test("should compile component with templating string", () => {
291
+ const input = `<Canvas width={\`direction: \${direction}\`} />`;
292
+ const output = parser.parse(input);
293
+ expect(output).toBe(`h(Canvas, { width: \`direction: \${direction()}\` })`);
294
+ });
295
+
296
+ test("should compile component with templating string with @ (literal)", () => {
297
+ const input = `<Canvas width={\`direction: \${@direction}\`} />`;
298
+ const output = parser.parse(input);
299
+ expect(output).toBe(`h(Canvas, { width: \`direction: \${direction}\` })`);
300
+ });
300
301
 
301
302
  test("should compile component with object attribute", () => {
302
303
  const input = `<Canvas width={ {x: 10, y: 20} } />`;
303
304
  const output = parser.parse(input);
304
- expect(output).toBe(`h(Canvas, { width: ({x: 10, y: 20}) })`);
305
+ expect(output).toBe(`h(Canvas, { width: { x: 10, y: 20 } })`);
305
306
  });
306
307
 
307
308
  test("should compile component with complex object attribute", () => {
@@ -343,6 +344,24 @@ describe("Compiler", () => {
343
344
  expect(output).toBe(`h(Button, { style: { backgroundColor: { normal: "#6c757d", hover: "#5a6268", pressed: "#545b62" }, text: { fontSize: 16, color: "#ffffff" } } })`);
344
345
  });
345
346
 
347
+ test('should compile component with deep object attribute and shorthand', () => {
348
+ const input = `<Canvas>
349
+ <Container>
350
+ <Sprite x y sheet={{
351
+ definition,
352
+ playing: animationPlaying,
353
+ params: {
354
+ direction
355
+ }
356
+ }}
357
+ controls />
358
+ </Container>
359
+ </Canvas>
360
+ `;
361
+ const output = parser.parse(input);
362
+ expect(output).toBe(`h(Canvas, null, h(Container, null, h(Sprite, { x, y, sheet: { definition, playing: animationPlaying, params: { direction } }, controls })))`);
363
+ });
364
+
346
365
  test("should compile component with deep object attribute but not transform to signal", () => {
347
366
  const input = `<Canvas width={@deep.value} />`;
348
367
  const output = parser.parse(input);
@@ -358,7 +377,7 @@ describe("Compiler", () => {
358
377
  test("should compile component with dynamic object attribute", () => {
359
378
  const input = `<Canvas width={ {x: x, y: 20} } />`;
360
379
  const output = parser.parse(input);
361
- expect(output).toBe(`h(Canvas, { width: computed(() => ({x: x(), y: 20})) })`);
380
+ expect(output).toBe(`h(Canvas, { width: { x: x, y: 20 } })`);
362
381
  });
363
382
 
364
383
  test("should compile component with array attribute", () => {
@@ -370,7 +389,7 @@ describe("Compiler", () => {
370
389
  test("should compile component with dynamic array attribute", () => {
371
390
  const input = `<Canvas width={ [x, 20] } />`;
372
391
  const output = parser.parse(input);
373
- expect(output).toBe(`h(Canvas, { width: computed(() => [x(), 20]) })`);
392
+ expect(output).toBe(`h(Canvas, { width: [x, 20] })`);
374
393
  });
375
394
 
376
395
  test("should compile component with standalone dynamic attribute", () => {
@@ -957,6 +976,24 @@ describe('DOM with special attributes', () => {
957
976
  const output = parser.parse(input);
958
977
  expect(output).toBe('h(DOMElement, { element: "input", attrs: { type: \'password\' }, x: 100, y: 100 })');
959
978
  });
979
+
980
+ test('should compile DOM with text object', () => {
981
+ const input = `<p>{{ object.x }}</p>`;
982
+ const output = parser.parse(input);
983
+ expect(output).toBe('h(DOMElement, { element: "p", textContent: computed(() => object().x()) })');
984
+ });
985
+
986
+ test('should compile DOM with text object with @', () => {
987
+ const input = `<p>{{ @object.x }}</p>`;
988
+ const output = parser.parse(input);
989
+ expect(output).toBe('h(DOMElement, { element: "p", textContent: computed(() => object.x()) })');
990
+ });
991
+
992
+ test('should compile DOM with text object with literal ', () => {
993
+ const input = `<p>{{ @object.@x }}</p>`;
994
+ const output = parser.parse(input);
995
+ expect(output).toBe('h(DOMElement, { element: "p", textContent: object.x })');
996
+ });
960
997
  });
961
998
 
962
999
  describe('DOM with Control Structures', () => {