pulse-js-framework 1.4.3 → 1.4.4
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/README.md +22 -0
- package/cli/mobile.js +48 -71
- package/compiler/parser.js +57 -136
- package/compiler/transformer.js +75 -197
- package/package.json +22 -4
- package/types/dom.d.ts +288 -0
- package/types/index.d.ts +120 -0
- package/types/pulse.d.ts +149 -0
- package/types/router.d.ts +197 -0
- package/types/store.d.ts +170 -0
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ A declarative DOM framework with CSS selector-based structure and reactive pulsa
|
|
|
14
14
|
- **Lightweight** - Minimal footprint, maximum performance
|
|
15
15
|
- **Router & Store** - Built-in SPA routing and state management
|
|
16
16
|
- **Mobile Apps** - Build native Android & iOS apps (zero dependencies)
|
|
17
|
+
- **TypeScript Support** - Full type definitions for IDE autocomplete
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
@@ -398,6 +399,27 @@ onNativeReady(({ platform }) => {
|
|
|
398
399
|
|
|
399
400
|
**Available APIs:** Storage, Device Info, Network Status, Toast, Vibration, Clipboard, App Lifecycle
|
|
400
401
|
|
|
402
|
+
## TypeScript Support
|
|
403
|
+
|
|
404
|
+
Pulse includes full TypeScript definitions for IDE autocomplete and type checking:
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { pulse, effect, computed, Pulse } from 'pulse-js-framework/runtime';
|
|
408
|
+
import { el, list, when } from 'pulse-js-framework/runtime';
|
|
409
|
+
import { createRouter, Router } from 'pulse-js-framework/runtime/router';
|
|
410
|
+
import { createStore, Store } from 'pulse-js-framework/runtime/store';
|
|
411
|
+
|
|
412
|
+
// Full autocomplete for all APIs
|
|
413
|
+
const count: Pulse<number> = pulse(0);
|
|
414
|
+
const doubled = computed(() => count.get() * 2);
|
|
415
|
+
|
|
416
|
+
effect(() => {
|
|
417
|
+
console.log(count.get()); // Type-safe access
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Types are automatically detected by IDEs (VS Code, WebStorm) without additional configuration.
|
|
422
|
+
|
|
401
423
|
## Examples
|
|
402
424
|
|
|
403
425
|
- [Blog](examples/blog) - Full blog app with CRUD, categories, search, dark mode
|
package/cli/mobile.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, readdirSync } from 'fs';
|
|
7
|
-
import { join,
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { execSync } from 'child_process';
|
|
10
10
|
|
|
@@ -14,6 +14,38 @@ const __dirname = dirname(__filename);
|
|
|
14
14
|
const MOBILE_DIR = 'mobile';
|
|
15
15
|
const CONFIG_FILE = 'pulse.mobile.json';
|
|
16
16
|
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Helper functions
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/** Create directory if it doesn't exist */
|
|
22
|
+
const mkdirp = (path) => !existsSync(path) && mkdirSync(path, { recursive: true });
|
|
23
|
+
|
|
24
|
+
/** Create multiple directories at once */
|
|
25
|
+
const mkdirs = (base, dirs) => dirs.forEach(d => mkdirp(join(base, d)));
|
|
26
|
+
|
|
27
|
+
/** Write file (auto-creates parent directories) */
|
|
28
|
+
const writeFile = (path, content) => {
|
|
29
|
+
mkdirp(dirname(path));
|
|
30
|
+
writeFileSync(path, content.trim());
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Apply template variables to content */
|
|
34
|
+
const applyTemplate = (content, config) => content
|
|
35
|
+
.replace(/\{\{APP_NAME\}\}/g, config.name)
|
|
36
|
+
.replace(/\{\{DISPLAY_NAME\}\}/g, config.displayName)
|
|
37
|
+
.replace(/\{\{PACKAGE_ID\}\}/g, config.packageId)
|
|
38
|
+
.replace(/\{\{VERSION\}\}/g, config.version)
|
|
39
|
+
.replace(/\{\{MIN_SDK\}\}/g, String(config.android?.minSdkVersion || 24))
|
|
40
|
+
.replace(/\{\{TARGET_SDK\}\}/g, String(config.android?.targetSdkVersion || 34))
|
|
41
|
+
.replace(/\{\{COMPILE_SDK\}\}/g, String(config.android?.compileSdkVersion || 34))
|
|
42
|
+
.replace(/\{\{IOS_TARGET\}\}/g, config.ios?.deploymentTarget || '13.0');
|
|
43
|
+
|
|
44
|
+
/** Convert string to PascalCase */
|
|
45
|
+
const toPascalCase = (str) => str
|
|
46
|
+
.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
47
|
+
.replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
48
|
+
|
|
17
49
|
/**
|
|
18
50
|
* Handle mobile subcommands
|
|
19
51
|
*/
|
|
@@ -419,67 +451,35 @@ function loadConfig(root) {
|
|
|
419
451
|
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
420
452
|
}
|
|
421
453
|
|
|
422
|
-
/**
|
|
423
|
-
* Copy and process template files
|
|
424
|
-
*/
|
|
454
|
+
/** Copy and process template files */
|
|
425
455
|
function copyAndProcessTemplate(src, dest, config) {
|
|
426
|
-
if (!existsSync(src))
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
mkdirSync(dest, { recursive: true });
|
|
456
|
+
if (!existsSync(src)) return;
|
|
457
|
+
mkdirp(dest);
|
|
431
458
|
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
for (const file of files) {
|
|
459
|
+
for (const file of readdirSync(src, { withFileTypes: true })) {
|
|
435
460
|
const srcPath = join(src, file.name);
|
|
436
461
|
const destPath = join(dest, file.name);
|
|
437
462
|
|
|
438
463
|
if (file.isDirectory()) {
|
|
439
464
|
copyAndProcessTemplate(srcPath, destPath, config);
|
|
440
465
|
} else {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// Replace template variables
|
|
444
|
-
content = content
|
|
445
|
-
.replace(/\{\{APP_NAME\}\}/g, config.name)
|
|
446
|
-
.replace(/\{\{DISPLAY_NAME\}\}/g, config.displayName)
|
|
447
|
-
.replace(/\{\{PACKAGE_ID\}\}/g, config.packageId)
|
|
448
|
-
.replace(/\{\{VERSION\}\}/g, config.version)
|
|
449
|
-
.replace(/\{\{MIN_SDK\}\}/g, String(config.android?.minSdkVersion || 24))
|
|
450
|
-
.replace(/\{\{TARGET_SDK\}\}/g, String(config.android?.targetSdkVersion || 34))
|
|
451
|
-
.replace(/\{\{COMPILE_SDK\}\}/g, String(config.android?.compileSdkVersion || 34))
|
|
452
|
-
.replace(/\{\{IOS_TARGET\}\}/g, config.ios?.deploymentTarget || '13.0');
|
|
453
|
-
|
|
454
|
-
writeFileSync(destPath, content);
|
|
466
|
+
writeFileSync(destPath, applyTemplate(readFileSync(srcPath, 'utf-8'), config));
|
|
455
467
|
}
|
|
456
468
|
}
|
|
457
469
|
}
|
|
458
470
|
|
|
459
|
-
/**
|
|
460
|
-
* Create Android project from scratch
|
|
461
|
-
*/
|
|
471
|
+
/** Create Android project from scratch */
|
|
462
472
|
function createAndroidProject(androidDir, config) {
|
|
463
473
|
const packagePath = config.packageId.replace(/\./g, '/');
|
|
464
474
|
|
|
465
475
|
// Create directory structure
|
|
466
|
-
|
|
476
|
+
mkdirs(androidDir, [
|
|
467
477
|
'app/src/main/java/' + packagePath,
|
|
468
|
-
'app/src/main/res/layout',
|
|
469
|
-
'app/src/main/res/
|
|
470
|
-
'app/src/main/res/
|
|
471
|
-
'app/src/main/
|
|
472
|
-
|
|
473
|
-
'app/src/main/res/mipmap-xhdpi',
|
|
474
|
-
'app/src/main/res/mipmap-xxhdpi',
|
|
475
|
-
'app/src/main/res/mipmap-xxxhdpi',
|
|
476
|
-
'app/src/main/assets/www',
|
|
477
|
-
'gradle/wrapper'
|
|
478
|
-
];
|
|
479
|
-
|
|
480
|
-
for (const dir of dirs) {
|
|
481
|
-
mkdirSync(join(androidDir, dir), { recursive: true });
|
|
482
|
-
}
|
|
478
|
+
'app/src/main/res/layout', 'app/src/main/res/values', 'app/src/main/res/drawable',
|
|
479
|
+
'app/src/main/res/mipmap-hdpi', 'app/src/main/res/mipmap-mdpi',
|
|
480
|
+
'app/src/main/res/mipmap-xhdpi', 'app/src/main/res/mipmap-xxhdpi', 'app/src/main/res/mipmap-xxxhdpi',
|
|
481
|
+
'app/src/main/assets/www', 'gradle/wrapper'
|
|
482
|
+
]);
|
|
483
483
|
|
|
484
484
|
// MainActivity.java
|
|
485
485
|
writeFileSync(join(androidDir, 'app/src/main/java', packagePath, 'MainActivity.java'), `
|
|
@@ -906,21 +906,9 @@ gradle %*
|
|
|
906
906
|
`.trim());
|
|
907
907
|
}
|
|
908
908
|
|
|
909
|
-
/**
|
|
910
|
-
* Create iOS project from scratch
|
|
911
|
-
*/
|
|
909
|
+
/** Create iOS project from scratch */
|
|
912
910
|
function createIOSProject(iosDir, config) {
|
|
913
|
-
|
|
914
|
-
const dirs = [
|
|
915
|
-
'PulseApp',
|
|
916
|
-
'PulseApp/www',
|
|
917
|
-
'PulseApp/Assets.xcassets',
|
|
918
|
-
'PulseApp.xcodeproj'
|
|
919
|
-
];
|
|
920
|
-
|
|
921
|
-
for (const dir of dirs) {
|
|
922
|
-
mkdirSync(join(iosDir, dir), { recursive: true });
|
|
923
|
-
}
|
|
911
|
+
mkdirs(iosDir, ['PulseApp', 'PulseApp/www', 'PulseApp/Assets.xcassets', 'PulseApp.xcodeproj']);
|
|
924
912
|
|
|
925
913
|
// AppDelegate.swift
|
|
926
914
|
writeFileSync(join(iosDir, 'PulseApp/AppDelegate.swift'), `
|
|
@@ -1430,18 +1418,7 @@ function generateXcodeProject(config) {
|
|
|
1430
1418
|
`;
|
|
1431
1419
|
}
|
|
1432
1420
|
|
|
1433
|
-
/**
|
|
1434
|
-
* Convert string to PascalCase
|
|
1435
|
-
*/
|
|
1436
|
-
function toPascalCase(str) {
|
|
1437
|
-
return str
|
|
1438
|
-
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
|
|
1439
|
-
.replace(/^(.)/, (_, char) => char.toUpperCase());
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
/**
|
|
1443
|
-
* Show mobile help
|
|
1444
|
-
*/
|
|
1421
|
+
/** Show mobile help */
|
|
1445
1422
|
function showMobileHelp() {
|
|
1446
1423
|
console.log(`
|
|
1447
1424
|
Pulse Mobile - Zero-Dependency Mobile Platform
|
package/compiler/parser.js
CHANGED
|
@@ -384,39 +384,36 @@ export class Parser {
|
|
|
384
384
|
return new ASTNode(NodeType.Property, { name: name.value, value });
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Try to parse a literal token (STRING, NUMBER, TRUE, FALSE, NULL)
|
|
389
|
+
* Returns the AST node or null if not a literal
|
|
390
|
+
*/
|
|
391
|
+
tryParseLiteral() {
|
|
392
|
+
const token = this.current();
|
|
393
|
+
if (!token) return null;
|
|
394
|
+
|
|
395
|
+
const literalMap = {
|
|
396
|
+
[TokenType.STRING]: () => new ASTNode(NodeType.Literal, { value: this.advance().value, raw: token.raw }),
|
|
397
|
+
[TokenType.NUMBER]: () => new ASTNode(NodeType.Literal, { value: this.advance().value }),
|
|
398
|
+
[TokenType.TRUE]: () => (this.advance(), new ASTNode(NodeType.Literal, { value: true })),
|
|
399
|
+
[TokenType.FALSE]: () => (this.advance(), new ASTNode(NodeType.Literal, { value: false })),
|
|
400
|
+
[TokenType.NULL]: () => (this.advance(), new ASTNode(NodeType.Literal, { value: null }))
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return literalMap[token.type]?.() || null;
|
|
404
|
+
}
|
|
405
|
+
|
|
387
406
|
/**
|
|
388
407
|
* Parse a value (literal, object, array, etc.)
|
|
389
408
|
*/
|
|
390
409
|
parseValue() {
|
|
391
|
-
if (this.is(TokenType.LBRACE))
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (this.is(TokenType.
|
|
398
|
-
const token = this.advance();
|
|
399
|
-
return new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
|
|
400
|
-
}
|
|
401
|
-
if (this.is(TokenType.NUMBER)) {
|
|
402
|
-
const token = this.advance();
|
|
403
|
-
return new ASTNode(NodeType.Literal, { value: token.value });
|
|
404
|
-
}
|
|
405
|
-
if (this.is(TokenType.TRUE)) {
|
|
406
|
-
this.advance();
|
|
407
|
-
return new ASTNode(NodeType.Literal, { value: true });
|
|
408
|
-
}
|
|
409
|
-
if (this.is(TokenType.FALSE)) {
|
|
410
|
-
this.advance();
|
|
411
|
-
return new ASTNode(NodeType.Literal, { value: false });
|
|
412
|
-
}
|
|
413
|
-
if (this.is(TokenType.NULL)) {
|
|
414
|
-
this.advance();
|
|
415
|
-
return new ASTNode(NodeType.Literal, { value: null });
|
|
416
|
-
}
|
|
417
|
-
if (this.is(TokenType.IDENT)) {
|
|
418
|
-
return this.parseIdentifierOrExpression();
|
|
419
|
-
}
|
|
410
|
+
if (this.is(TokenType.LBRACE)) return this.parseObjectLiteral();
|
|
411
|
+
if (this.is(TokenType.LBRACKET)) return this.parseArrayLiteral();
|
|
412
|
+
|
|
413
|
+
const literal = this.tryParseLiteral();
|
|
414
|
+
if (literal) return literal;
|
|
415
|
+
|
|
416
|
+
if (this.is(TokenType.IDENT)) return this.parseIdentifierOrExpression();
|
|
420
417
|
|
|
421
418
|
throw new Error(
|
|
422
419
|
`Unexpected token ${this.current()?.type} in value at line ${this.current()?.line}`
|
|
@@ -612,32 +609,18 @@ export class Parser {
|
|
|
612
609
|
|
|
613
610
|
let value;
|
|
614
611
|
if (this.is(TokenType.LBRACE)) {
|
|
615
|
-
|
|
616
|
-
this.advance(); // consume {
|
|
612
|
+
this.advance();
|
|
617
613
|
value = this.parseExpression();
|
|
618
614
|
this.expect(TokenType.RBRACE);
|
|
619
|
-
} else if (this.is(TokenType.STRING)) {
|
|
620
|
-
// String prop: name="value"
|
|
621
|
-
const token = this.advance();
|
|
622
|
-
value = new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
|
|
623
|
-
} else if (this.is(TokenType.NUMBER)) {
|
|
624
|
-
// Number prop: name=123
|
|
625
|
-
const token = this.advance();
|
|
626
|
-
value = new ASTNode(NodeType.Literal, { value: token.value });
|
|
627
|
-
} else if (this.is(TokenType.TRUE)) {
|
|
628
|
-
this.advance();
|
|
629
|
-
value = new ASTNode(NodeType.Literal, { value: true });
|
|
630
|
-
} else if (this.is(TokenType.FALSE)) {
|
|
631
|
-
this.advance();
|
|
632
|
-
value = new ASTNode(NodeType.Literal, { value: false });
|
|
633
|
-
} else if (this.is(TokenType.NULL)) {
|
|
634
|
-
this.advance();
|
|
635
|
-
value = new ASTNode(NodeType.Literal, { value: null });
|
|
636
|
-
} else if (this.is(TokenType.IDENT)) {
|
|
637
|
-
// Identifier prop: name=someVar
|
|
638
|
-
value = this.parseIdentifierOrExpression();
|
|
639
615
|
} else {
|
|
640
|
-
|
|
616
|
+
value = this.tryParseLiteral();
|
|
617
|
+
if (!value) {
|
|
618
|
+
if (this.is(TokenType.IDENT)) {
|
|
619
|
+
value = this.parseIdentifierOrExpression();
|
|
620
|
+
} else {
|
|
621
|
+
throw this.createError(`Unexpected token in prop value: ${this.current()?.type}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
641
624
|
}
|
|
642
625
|
|
|
643
626
|
return new ASTNode(NodeType.Property, { name: name.value, value });
|
|
@@ -885,80 +868,39 @@ export class Parser {
|
|
|
885
868
|
}
|
|
886
869
|
|
|
887
870
|
/**
|
|
888
|
-
*
|
|
889
|
-
*/
|
|
890
|
-
parseOrExpression() {
|
|
891
|
-
let left = this.parseAndExpression();
|
|
892
|
-
|
|
893
|
-
while (this.is(TokenType.OR)) {
|
|
894
|
-
this.advance();
|
|
895
|
-
const right = this.parseAndExpression();
|
|
896
|
-
left = new ASTNode(NodeType.BinaryExpression, { operator: '||', left, right });
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
return left;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
/**
|
|
903
|
-
* Parse AND expression
|
|
871
|
+
* Binary operator precedence table (higher = binds tighter)
|
|
904
872
|
*/
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
return left;
|
|
915
|
-
}
|
|
873
|
+
static BINARY_OPS = [
|
|
874
|
+
{ ops: [TokenType.OR], name: 'or' },
|
|
875
|
+
{ ops: [TokenType.AND], name: 'and' },
|
|
876
|
+
{ ops: [TokenType.EQEQ, TokenType.EQEQEQ, TokenType.NEQ, TokenType.NEQEQ,
|
|
877
|
+
TokenType.LT, TokenType.GT, TokenType.LTE, TokenType.GTE], name: 'comparison' },
|
|
878
|
+
{ ops: [TokenType.PLUS, TokenType.MINUS], name: 'additive' },
|
|
879
|
+
{ ops: [TokenType.STAR, TokenType.SLASH, TokenType.PERCENT], name: 'multiplicative' }
|
|
880
|
+
];
|
|
916
881
|
|
|
917
882
|
/**
|
|
918
|
-
*
|
|
883
|
+
* Generic binary expression parser using precedence climbing
|
|
919
884
|
*/
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
while (this.isAny(TokenType.EQEQ, TokenType.EQEQEQ, TokenType.NEQ, TokenType.NEQEQ,
|
|
924
|
-
TokenType.LT, TokenType.GT, TokenType.LTE, TokenType.GTE)) {
|
|
925
|
-
const operator = this.advance().value;
|
|
926
|
-
const right = this.parseAdditiveExpression();
|
|
927
|
-
left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
|
|
885
|
+
parseBinaryExpr(level = 0) {
|
|
886
|
+
if (level >= Parser.BINARY_OPS.length) {
|
|
887
|
+
return this.parseUnaryExpression();
|
|
928
888
|
}
|
|
929
889
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Parse additive expression
|
|
935
|
-
*/
|
|
936
|
-
parseAdditiveExpression() {
|
|
937
|
-
let left = this.parseMultiplicativeExpression();
|
|
890
|
+
let left = this.parseBinaryExpr(level + 1);
|
|
891
|
+
const { ops } = Parser.BINARY_OPS[level];
|
|
938
892
|
|
|
939
|
-
while (this.isAny(
|
|
893
|
+
while (this.isAny(...ops)) {
|
|
940
894
|
const operator = this.advance().value;
|
|
941
|
-
const right = this.
|
|
895
|
+
const right = this.parseBinaryExpr(level + 1);
|
|
942
896
|
left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
|
|
943
897
|
}
|
|
944
898
|
|
|
945
899
|
return left;
|
|
946
900
|
}
|
|
947
901
|
|
|
948
|
-
/**
|
|
949
|
-
|
|
950
|
-
*/
|
|
951
|
-
parseMultiplicativeExpression() {
|
|
952
|
-
let left = this.parseUnaryExpression();
|
|
953
|
-
|
|
954
|
-
while (this.isAny(TokenType.STAR, TokenType.SLASH, TokenType.PERCENT)) {
|
|
955
|
-
const operator = this.advance().value;
|
|
956
|
-
const right = this.parseUnaryExpression();
|
|
957
|
-
left = new ASTNode(NodeType.BinaryExpression, { operator, left, right });
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
return left;
|
|
961
|
-
}
|
|
902
|
+
/** Parse OR expression (entry point for binary expressions) */
|
|
903
|
+
parseOrExpression() { return this.parseBinaryExpr(0); }
|
|
962
904
|
|
|
963
905
|
/**
|
|
964
906
|
* Parse unary expression
|
|
@@ -1048,30 +990,9 @@ export class Parser {
|
|
|
1048
990
|
return new ASTNode(NodeType.SpreadElement, { argument });
|
|
1049
991
|
}
|
|
1050
992
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
if (this.is(TokenType.STRING)) {
|
|
1057
|
-
const token = this.advance();
|
|
1058
|
-
return new ASTNode(NodeType.Literal, { value: token.value, raw: token.raw });
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
if (this.is(TokenType.TRUE)) {
|
|
1062
|
-
this.advance();
|
|
1063
|
-
return new ASTNode(NodeType.Literal, { value: true });
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
if (this.is(TokenType.FALSE)) {
|
|
1067
|
-
this.advance();
|
|
1068
|
-
return new ASTNode(NodeType.Literal, { value: false });
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
if (this.is(TokenType.NULL)) {
|
|
1072
|
-
this.advance();
|
|
1073
|
-
return new ASTNode(NodeType.Literal, { value: null });
|
|
1074
|
-
}
|
|
993
|
+
// Try parsing a literal (NUMBER, STRING, TRUE, FALSE, NULL)
|
|
994
|
+
const literal = this.tryParseLiteral();
|
|
995
|
+
if (literal) return literal;
|
|
1075
996
|
|
|
1076
997
|
// In expressions, SELECTOR tokens should be treated as IDENT
|
|
1077
998
|
// This happens when identifiers like 'selectedCategory' are followed by space in view context
|