@canvasengine/compiler 2.0.0-beta.27 → 2.0.0-beta.29
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 +122 -36
- package/package.json +1 -1
- package/tests/compiler.spec.ts +51 -14
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
|
|
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,
|
|
447
|
+
// If it's a template string, transform expressions inside ${}
|
|
407
448
|
if (attributeValue.trim().startsWith('`') && attributeValue.trim().endsWith('`')) {
|
|
408
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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 (
|
|
436
|
-
|
|
437
|
-
|
|
532
|
+
// Check if any values already contain signals (ending with ())
|
|
533
|
+
if (attributeValue.includes('()')) {
|
|
534
|
+
foundSignal = true;
|
|
438
535
|
}
|
|
439
|
-
|
|
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,
|
|
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
|
-
|
|
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
package/tests/compiler.spec.ts
CHANGED
|
@@ -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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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', () => {
|