pulse-js-framework 1.0.0 → 1.4.0

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/compiler/lexer.js CHANGED
@@ -1,581 +1,766 @@
1
- /**
2
- * Pulse Lexer - Tokenizer for .pulse files
3
- *
4
- * Converts source code into a stream of tokens
5
- */
6
-
7
- // Token types
8
- export const TokenType = {
9
- // Keywords
10
- STATE: 'STATE',
11
- VIEW: 'VIEW',
12
- ACTIONS: 'ACTIONS',
13
- STYLE: 'STYLE',
14
-
15
- // Directives
16
- AT: 'AT', // @
17
- PAGE: 'PAGE',
18
- ROUTE: 'ROUTE',
19
- IF: 'IF',
20
- ELSE: 'ELSE',
21
- EACH: 'EACH',
22
- IN: 'IN',
23
-
24
- // Punctuation
25
- LBRACE: 'LBRACE', // {
26
- RBRACE: 'RBRACE', // }
27
- LPAREN: 'LPAREN', // (
28
- RPAREN: 'RPAREN', // )
29
- LBRACKET: 'LBRACKET', // [
30
- RBRACKET: 'RBRACKET', // ]
31
- COLON: 'COLON', // :
32
- COMMA: 'COMMA', // ,
33
- DOT: 'DOT', // .
34
- HASH: 'HASH', // #
35
- SEMICOLON: 'SEMICOLON', // ;
36
-
37
- // Operators
38
- PLUS: 'PLUS',
39
- MINUS: 'MINUS',
40
- STAR: 'STAR',
41
- SLASH: 'SLASH',
42
- EQ: 'EQ', // =
43
- EQEQ: 'EQEQ', // ==
44
- NEQ: 'NEQ', // !=
45
- LT: 'LT', // <
46
- GT: 'GT', // >
47
- LTE: 'LTE', // <=
48
- GTE: 'GTE', // >=
49
- AND: 'AND', // &&
50
- OR: 'OR', // ||
51
- NOT: 'NOT', // !
52
- PLUSPLUS: 'PLUSPLUS', // ++
53
- MINUSMINUS: 'MINUSMINUS', // --
54
-
55
- // Literals
56
- STRING: 'STRING',
57
- NUMBER: 'NUMBER',
58
- TRUE: 'TRUE',
59
- FALSE: 'FALSE',
60
- NULL: 'NULL',
61
-
62
- // Identifiers and selectors
63
- IDENT: 'IDENT',
64
- SELECTOR: 'SELECTOR', // CSS selector like .class, #id, tag.class#id
65
-
66
- // Special
67
- INTERPOLATION_START: 'INTERPOLATION_START', // {
68
- INTERPOLATION_END: 'INTERPOLATION_END', // }
69
- TEXT: 'TEXT', // Text content inside strings
70
-
71
- // Misc
72
- NEWLINE: 'NEWLINE',
73
- EOF: 'EOF',
74
- ERROR: 'ERROR'
75
- };
76
-
77
- // Keywords map
78
- const KEYWORDS = {
79
- 'state': TokenType.STATE,
80
- 'view': TokenType.VIEW,
81
- 'actions': TokenType.ACTIONS,
82
- 'style': TokenType.STYLE,
83
- 'if': TokenType.IF,
84
- 'else': TokenType.ELSE,
85
- 'each': TokenType.EACH,
86
- 'in': TokenType.IN,
87
- 'page': TokenType.PAGE,
88
- 'route': TokenType.ROUTE,
89
- 'true': TokenType.TRUE,
90
- 'false': TokenType.FALSE,
91
- 'null': TokenType.NULL,
92
- 'async': TokenType.IDENT,
93
- 'await': TokenType.IDENT
94
- };
95
-
96
- /**
97
- * Token class
98
- */
99
- export class Token {
100
- constructor(type, value, line, column, raw = null) {
101
- this.type = type;
102
- this.value = value;
103
- this.line = line;
104
- this.column = column;
105
- this.raw = raw || value;
106
- }
107
-
108
- toString() {
109
- return `Token(${this.type}, ${JSON.stringify(this.value)}, ${this.line}:${this.column})`;
110
- }
111
- }
112
-
113
- /**
114
- * Lexer class
115
- */
116
- export class Lexer {
117
- constructor(source) {
118
- this.source = source;
119
- this.pos = 0;
120
- this.line = 1;
121
- this.column = 1;
122
- this.tokens = [];
123
- }
124
-
125
- /**
126
- * Get current character
127
- */
128
- current() {
129
- return this.source[this.pos];
130
- }
131
-
132
- /**
133
- * Peek at character at offset
134
- */
135
- peek(offset = 1) {
136
- return this.source[this.pos + offset];
137
- }
138
-
139
- /**
140
- * Check if at end of input
141
- */
142
- isEOF() {
143
- return this.pos >= this.source.length;
144
- }
145
-
146
- /**
147
- * Advance position and return current char
148
- */
149
- advance() {
150
- const char = this.current();
151
- this.pos++;
152
- if (char === '\n') {
153
- this.line++;
154
- this.column = 1;
155
- } else {
156
- this.column++;
157
- }
158
- return char;
159
- }
160
-
161
- /**
162
- * Create a token
163
- */
164
- token(type, value, raw = null) {
165
- return new Token(type, value, this.line, this.column, raw);
166
- }
167
-
168
- /**
169
- * Skip whitespace (but not newlines in some contexts)
170
- */
171
- skipWhitespace(includeNewlines = true) {
172
- while (!this.isEOF()) {
173
- const char = this.current();
174
- if (char === ' ' || char === '\t' || char === '\r') {
175
- this.advance();
176
- } else if (char === '\n' && includeNewlines) {
177
- this.advance();
178
- } else if (char === '/' && this.peek() === '/') {
179
- // Single-line comment
180
- while (!this.isEOF() && this.current() !== '\n') {
181
- this.advance();
182
- }
183
- } else if (char === '/' && this.peek() === '*') {
184
- // Multi-line comment
185
- this.advance(); // /
186
- this.advance(); // *
187
- while (!this.isEOF() && !(this.current() === '*' && this.peek() === '/')) {
188
- this.advance();
189
- }
190
- if (!this.isEOF()) {
191
- this.advance(); // *
192
- this.advance(); // /
193
- }
194
- } else {
195
- break;
196
- }
197
- }
198
- }
199
-
200
- /**
201
- * Read a string literal
202
- */
203
- readString() {
204
- const quote = this.advance(); // opening quote
205
- const startLine = this.line;
206
- const startColumn = this.column;
207
- let value = '';
208
- let raw = quote;
209
-
210
- while (!this.isEOF() && this.current() !== quote) {
211
- if (this.current() === '\\') {
212
- raw += this.advance();
213
- if (!this.isEOF()) {
214
- const escaped = this.advance();
215
- raw += escaped;
216
- switch (escaped) {
217
- case 'n': value += '\n'; break;
218
- case 't': value += '\t'; break;
219
- case 'r': value += '\r'; break;
220
- case '\\': value += '\\'; break;
221
- case '"': value += '"'; break;
222
- case "'": value += "'"; break;
223
- default: value += escaped;
224
- }
225
- }
226
- } else {
227
- value += this.current();
228
- raw += this.advance();
229
- }
230
- }
231
-
232
- if (!this.isEOF()) {
233
- raw += this.advance(); // closing quote
234
- }
235
-
236
- return new Token(TokenType.STRING, value, startLine, startColumn, raw);
237
- }
238
-
239
- /**
240
- * Read a number literal
241
- */
242
- readNumber() {
243
- const startLine = this.line;
244
- const startColumn = this.column;
245
- let value = '';
246
-
247
- // Integer part
248
- while (!this.isEOF() && /[0-9]/.test(this.current())) {
249
- value += this.advance();
250
- }
251
-
252
- // Decimal part
253
- if (this.current() === '.' && /[0-9]/.test(this.peek())) {
254
- value += this.advance(); // .
255
- while (!this.isEOF() && /[0-9]/.test(this.current())) {
256
- value += this.advance();
257
- }
258
- }
259
-
260
- // Exponent part
261
- if (this.current() === 'e' || this.current() === 'E') {
262
- value += this.advance();
263
- if (this.current() === '+' || this.current() === '-') {
264
- value += this.advance();
265
- }
266
- while (!this.isEOF() && /[0-9]/.test(this.current())) {
267
- value += this.advance();
268
- }
269
- }
270
-
271
- return new Token(TokenType.NUMBER, parseFloat(value), startLine, startColumn, value);
272
- }
273
-
274
- /**
275
- * Read an identifier or keyword
276
- */
277
- readIdentifier() {
278
- const startLine = this.line;
279
- const startColumn = this.column;
280
- let value = '';
281
-
282
- while (!this.isEOF() && /[a-zA-Z0-9_$]/.test(this.current())) {
283
- value += this.advance();
284
- }
285
-
286
- const type = KEYWORDS[value] || TokenType.IDENT;
287
- return new Token(type, value, startLine, startColumn);
288
- }
289
-
290
- /**
291
- * Read a CSS selector
292
- * Examples: div, .class, #id, button.primary, input[type=text]
293
- */
294
- readSelector() {
295
- const startLine = this.line;
296
- const startColumn = this.column;
297
- let value = '';
298
-
299
- // Start with tag name if present
300
- if (/[a-zA-Z]/.test(this.current())) {
301
- while (!this.isEOF() && /[a-zA-Z0-9-]/.test(this.current())) {
302
- value += this.advance();
303
- }
304
- }
305
-
306
- // Continue with classes, ids, and attributes
307
- while (!this.isEOF()) {
308
- if (this.current() === '.') {
309
- value += this.advance();
310
- while (!this.isEOF() && /[a-zA-Z0-9_-]/.test(this.current())) {
311
- value += this.advance();
312
- }
313
- } else if (this.current() === '#') {
314
- value += this.advance();
315
- while (!this.isEOF() && /[a-zA-Z0-9_-]/.test(this.current())) {
316
- value += this.advance();
317
- }
318
- } else if (this.current() === '[') {
319
- value += this.advance();
320
- while (!this.isEOF() && this.current() !== ']') {
321
- value += this.advance();
322
- }
323
- if (this.current() === ']') {
324
- value += this.advance();
325
- }
326
- } else {
327
- break;
328
- }
329
- }
330
-
331
- return new Token(TokenType.SELECTOR, value, startLine, startColumn);
332
- }
333
-
334
- /**
335
- * Tokenize the entire source
336
- */
337
- tokenize() {
338
- this.tokens = [];
339
- let inViewBlock = false;
340
- let braceDepth = 0;
341
-
342
- while (!this.isEOF()) {
343
- this.skipWhitespace();
344
-
345
- if (this.isEOF()) break;
346
-
347
- const startLine = this.line;
348
- const startColumn = this.column;
349
- const char = this.current();
350
-
351
- // String literals
352
- if (char === '"' || char === "'") {
353
- this.tokens.push(this.readString());
354
- continue;
355
- }
356
-
357
- // Numbers
358
- if (/[0-9]/.test(char)) {
359
- this.tokens.push(this.readNumber());
360
- continue;
361
- }
362
-
363
- // At-sign for directives
364
- if (char === '@') {
365
- this.advance();
366
- this.tokens.push(new Token(TokenType.AT, '@', startLine, startColumn));
367
- continue;
368
- }
369
-
370
- // Punctuation and operators
371
- switch (char) {
372
- case '{':
373
- this.advance();
374
- braceDepth++;
375
- this.tokens.push(new Token(TokenType.LBRACE, '{', startLine, startColumn));
376
- continue;
377
- case '}':
378
- this.advance();
379
- braceDepth--;
380
- this.tokens.push(new Token(TokenType.RBRACE, '}', startLine, startColumn));
381
- continue;
382
- case '(':
383
- this.advance();
384
- this.tokens.push(new Token(TokenType.LPAREN, '(', startLine, startColumn));
385
- continue;
386
- case ')':
387
- this.advance();
388
- this.tokens.push(new Token(TokenType.RPAREN, ')', startLine, startColumn));
389
- continue;
390
- case '[':
391
- this.advance();
392
- this.tokens.push(new Token(TokenType.LBRACKET, '[', startLine, startColumn));
393
- continue;
394
- case ']':
395
- this.advance();
396
- this.tokens.push(new Token(TokenType.RBRACKET, ']', startLine, startColumn));
397
- continue;
398
- case ':':
399
- this.advance();
400
- this.tokens.push(new Token(TokenType.COLON, ':', startLine, startColumn));
401
- continue;
402
- case ',':
403
- this.advance();
404
- this.tokens.push(new Token(TokenType.COMMA, ',', startLine, startColumn));
405
- continue;
406
- case ';':
407
- this.advance();
408
- this.tokens.push(new Token(TokenType.SEMICOLON, ';', startLine, startColumn));
409
- continue;
410
- case '+':
411
- this.advance();
412
- if (this.current() === '+') {
413
- this.advance();
414
- this.tokens.push(new Token(TokenType.PLUSPLUS, '++', startLine, startColumn));
415
- } else {
416
- this.tokens.push(new Token(TokenType.PLUS, '+', startLine, startColumn));
417
- }
418
- continue;
419
- case '-':
420
- this.advance();
421
- if (this.current() === '-') {
422
- this.advance();
423
- this.tokens.push(new Token(TokenType.MINUSMINUS, '--', startLine, startColumn));
424
- } else {
425
- this.tokens.push(new Token(TokenType.MINUS, '-', startLine, startColumn));
426
- }
427
- continue;
428
- case '*':
429
- this.advance();
430
- this.tokens.push(new Token(TokenType.STAR, '*', startLine, startColumn));
431
- continue;
432
- case '/':
433
- this.advance();
434
- this.tokens.push(new Token(TokenType.SLASH, '/', startLine, startColumn));
435
- continue;
436
- case '=':
437
- this.advance();
438
- if (this.current() === '=') {
439
- this.advance();
440
- this.tokens.push(new Token(TokenType.EQEQ, '==', startLine, startColumn));
441
- } else {
442
- this.tokens.push(new Token(TokenType.EQ, '=', startLine, startColumn));
443
- }
444
- continue;
445
- case '!':
446
- this.advance();
447
- if (this.current() === '=') {
448
- this.advance();
449
- this.tokens.push(new Token(TokenType.NEQ, '!=', startLine, startColumn));
450
- } else {
451
- this.tokens.push(new Token(TokenType.NOT, '!', startLine, startColumn));
452
- }
453
- continue;
454
- case '<':
455
- this.advance();
456
- if (this.current() === '=') {
457
- this.advance();
458
- this.tokens.push(new Token(TokenType.LTE, '<=', startLine, startColumn));
459
- } else {
460
- this.tokens.push(new Token(TokenType.LT, '<', startLine, startColumn));
461
- }
462
- continue;
463
- case '>':
464
- this.advance();
465
- if (this.current() === '=') {
466
- this.advance();
467
- this.tokens.push(new Token(TokenType.GTE, '>=', startLine, startColumn));
468
- } else {
469
- this.tokens.push(new Token(TokenType.GT, '>', startLine, startColumn));
470
- }
471
- continue;
472
- case '&':
473
- this.advance();
474
- if (this.current() === '&') {
475
- this.advance();
476
- this.tokens.push(new Token(TokenType.AND, '&&', startLine, startColumn));
477
- }
478
- continue;
479
- case '|':
480
- this.advance();
481
- if (this.current() === '|') {
482
- this.advance();
483
- this.tokens.push(new Token(TokenType.OR, '||', startLine, startColumn));
484
- }
485
- continue;
486
- }
487
-
488
- // Check for selector start (. or #) in view context
489
- if ((char === '.' || char === '#') && this.isViewContext()) {
490
- this.tokens.push(this.readSelector());
491
- continue;
492
- }
493
-
494
- // Dot outside selector
495
- if (char === '.') {
496
- this.advance();
497
- this.tokens.push(new Token(TokenType.DOT, '.', startLine, startColumn));
498
- continue;
499
- }
500
-
501
- // Hash outside selector
502
- if (char === '#') {
503
- this.advance();
504
- this.tokens.push(new Token(TokenType.HASH, '#', startLine, startColumn));
505
- continue;
506
- }
507
-
508
- // Identifiers, keywords, and selectors
509
- if (/[a-zA-Z_$]/.test(char)) {
510
- // Check if we're in view context and this could be a selector
511
- if (this.isViewContext() && this.couldBeSelector()) {
512
- this.tokens.push(this.readSelector());
513
- } else {
514
- this.tokens.push(this.readIdentifier());
515
- }
516
- continue;
517
- }
518
-
519
- // Unknown character - error
520
- this.tokens.push(new Token(TokenType.ERROR, char, startLine, startColumn));
521
- this.advance();
522
- }
523
-
524
- this.tokens.push(new Token(TokenType.EOF, null, this.line, this.column));
525
- return this.tokens;
526
- }
527
-
528
- /**
529
- * Check if we're in a view context where selectors are expected
530
- */
531
- isViewContext() {
532
- // Look back through tokens for 'view' keyword followed by '{'
533
- for (let i = this.tokens.length - 1; i >= 0; i--) {
534
- const token = this.tokens[i];
535
- if (token.type === TokenType.VIEW) {
536
- return true;
537
- }
538
- if (token.type === TokenType.STATE ||
539
- token.type === TokenType.ACTIONS ||
540
- token.type === TokenType.STYLE) {
541
- return false;
542
- }
543
- }
544
- return false;
545
- }
546
-
547
- /**
548
- * Check if current position could start a selector
549
- */
550
- couldBeSelector() {
551
- // Check if followed by . or # or [ or { which indicate selector continuation
552
- let lookahead = 0;
553
- while (this.pos + lookahead < this.source.length) {
554
- const char = this.source[this.pos + lookahead];
555
- if (/[a-zA-Z0-9_-]/.test(char)) {
556
- lookahead++;
557
- continue;
558
- }
559
- if (char === '.' || char === '#' || char === '[' || char === '{' || char === ' ') {
560
- return true;
561
- }
562
- break;
563
- }
564
- return false;
565
- }
566
- }
567
-
568
- /**
569
- * Convenience function to tokenize source
570
- */
571
- export function tokenize(source) {
572
- const lexer = new Lexer(source);
573
- return lexer.tokenize();
574
- }
575
-
576
- export default {
577
- TokenType,
578
- Token,
579
- Lexer,
580
- tokenize
581
- };
1
+ /**
2
+ * Pulse Lexer - Tokenizer for .pulse files
3
+ *
4
+ * Converts source code into a stream of tokens
5
+ */
6
+
7
+ // Token types
8
+ export const TokenType = {
9
+ // Keywords
10
+ STATE: 'STATE',
11
+ PROPS: 'PROPS',
12
+ VIEW: 'VIEW',
13
+ ACTIONS: 'ACTIONS',
14
+ STYLE: 'STYLE',
15
+ IMPORT: 'IMPORT',
16
+ FROM: 'FROM',
17
+ AS: 'AS',
18
+ EXPORT: 'EXPORT',
19
+ SLOT: 'SLOT',
20
+
21
+ // Router/Store keywords
22
+ ROUTER: 'ROUTER',
23
+ STORE: 'STORE',
24
+ ROUTES: 'ROUTES',
25
+ GETTERS: 'GETTERS',
26
+ BEFORE_EACH: 'BEFORE_EACH',
27
+ AFTER_EACH: 'AFTER_EACH',
28
+ PERSIST: 'PERSIST',
29
+ STORAGE_KEY: 'STORAGE_KEY',
30
+ PLUGINS: 'PLUGINS',
31
+ MODE: 'MODE',
32
+ BASE: 'BASE',
33
+
34
+ // Directives
35
+ AT: 'AT', // @
36
+ PAGE: 'PAGE',
37
+ ROUTE: 'ROUTE',
38
+ IF: 'IF',
39
+ ELSE: 'ELSE',
40
+ EACH: 'EACH',
41
+ FOR: 'FOR',
42
+ IN: 'IN',
43
+ OF: 'OF',
44
+
45
+ // Router view directives
46
+ LINK: 'LINK',
47
+ OUTLET: 'OUTLET',
48
+ NAVIGATE: 'NAVIGATE',
49
+ BACK: 'BACK',
50
+ FORWARD: 'FORWARD',
51
+
52
+ // Punctuation
53
+ LBRACE: 'LBRACE', // {
54
+ RBRACE: 'RBRACE', // }
55
+ LPAREN: 'LPAREN', // (
56
+ RPAREN: 'RPAREN', // )
57
+ LBRACKET: 'LBRACKET', // [
58
+ RBRACKET: 'RBRACKET', // ]
59
+ COLON: 'COLON', // :
60
+ COMMA: 'COMMA', // ,
61
+ DOT: 'DOT', // .
62
+ HASH: 'HASH', // #
63
+ SEMICOLON: 'SEMICOLON', // ;
64
+
65
+ // Operators
66
+ PLUS: 'PLUS',
67
+ MINUS: 'MINUS',
68
+ STAR: 'STAR',
69
+ SLASH: 'SLASH',
70
+ PERCENT: 'PERCENT', // %
71
+ EQ: 'EQ', // =
72
+ EQEQ: 'EQEQ', // ==
73
+ EQEQEQ: 'EQEQEQ', // ===
74
+ NEQ: 'NEQ', // !=
75
+ NEQEQ: 'NEQEQ', // !==
76
+ LT: 'LT', // <
77
+ GT: 'GT', // >
78
+ LTE: 'LTE', // <=
79
+ GTE: 'GTE', // >=
80
+ AND: 'AND', // &&
81
+ OR: 'OR', // ||
82
+ NOT: 'NOT', // !
83
+ PLUSPLUS: 'PLUSPLUS', // ++
84
+ MINUSMINUS: 'MINUSMINUS', // --
85
+ QUESTION: 'QUESTION', // ?
86
+ ARROW: 'ARROW', // =>
87
+ SPREAD: 'SPREAD', // ...
88
+
89
+ // Literals
90
+ STRING: 'STRING',
91
+ TEMPLATE: 'TEMPLATE', // Template literal `...`
92
+ NUMBER: 'NUMBER',
93
+ TRUE: 'TRUE',
94
+ FALSE: 'FALSE',
95
+ NULL: 'NULL',
96
+
97
+ // Identifiers and selectors
98
+ IDENT: 'IDENT',
99
+ SELECTOR: 'SELECTOR', // CSS selector like .class, #id, tag.class#id
100
+
101
+ // Special
102
+ INTERPOLATION_START: 'INTERPOLATION_START', // {
103
+ INTERPOLATION_END: 'INTERPOLATION_END', // }
104
+ TEXT: 'TEXT', // Text content inside strings
105
+
106
+ // Misc
107
+ NEWLINE: 'NEWLINE',
108
+ EOF: 'EOF',
109
+ ERROR: 'ERROR'
110
+ };
111
+
112
+ // Keywords map
113
+ const KEYWORDS = {
114
+ 'state': TokenType.STATE,
115
+ 'props': TokenType.PROPS,
116
+ 'view': TokenType.VIEW,
117
+ 'actions': TokenType.ACTIONS,
118
+ 'style': TokenType.STYLE,
119
+ 'import': TokenType.IMPORT,
120
+ 'from': TokenType.FROM,
121
+ 'as': TokenType.AS,
122
+ 'export': TokenType.EXPORT,
123
+ 'slot': TokenType.SLOT,
124
+ 'if': TokenType.IF,
125
+ 'else': TokenType.ELSE,
126
+ 'each': TokenType.EACH,
127
+ 'for': TokenType.FOR,
128
+ 'in': TokenType.IN,
129
+ 'of': TokenType.OF,
130
+ 'page': TokenType.PAGE,
131
+ 'route': TokenType.ROUTE,
132
+ 'router': TokenType.ROUTER,
133
+ 'store': TokenType.STORE,
134
+ 'routes': TokenType.ROUTES,
135
+ 'getters': TokenType.GETTERS,
136
+ 'beforeEach': TokenType.BEFORE_EACH,
137
+ 'afterEach': TokenType.AFTER_EACH,
138
+ 'persist': TokenType.PERSIST,
139
+ 'storageKey': TokenType.STORAGE_KEY,
140
+ 'plugins': TokenType.PLUGINS,
141
+ 'mode': TokenType.MODE,
142
+ 'base': TokenType.BASE,
143
+ 'link': TokenType.LINK,
144
+ 'outlet': TokenType.OUTLET,
145
+ 'navigate': TokenType.NAVIGATE,
146
+ 'back': TokenType.BACK,
147
+ 'forward': TokenType.FORWARD,
148
+ 'true': TokenType.TRUE,
149
+ 'false': TokenType.FALSE,
150
+ 'null': TokenType.NULL,
151
+ 'async': TokenType.IDENT,
152
+ 'await': TokenType.IDENT,
153
+ 'let': TokenType.IDENT,
154
+ 'const': TokenType.IDENT,
155
+ 'return': TokenType.IDENT,
156
+ 'new': TokenType.IDENT,
157
+ 'function': TokenType.IDENT,
158
+ 'this': TokenType.IDENT
159
+ };
160
+
161
+ /**
162
+ * Token class
163
+ */
164
+ export class Token {
165
+ constructor(type, value, line, column, raw = null, startPos = null, endPos = null) {
166
+ this.type = type;
167
+ this.value = value;
168
+ this.line = line;
169
+ this.column = column;
170
+ this.raw = raw || value;
171
+ this.startPos = startPos;
172
+ this.endPos = endPos;
173
+ }
174
+
175
+ toString() {
176
+ return `Token(${this.type}, ${JSON.stringify(this.value)}, ${this.line}:${this.column})`;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Lexer class
182
+ */
183
+ export class Lexer {
184
+ constructor(source) {
185
+ this.source = source;
186
+ this.pos = 0;
187
+ this.line = 1;
188
+ this.column = 1;
189
+ this.tokens = [];
190
+ }
191
+
192
+ /**
193
+ * Get current character
194
+ */
195
+ current() {
196
+ return this.source[this.pos];
197
+ }
198
+
199
+ /**
200
+ * Peek at character at offset
201
+ */
202
+ peek(offset = 1) {
203
+ return this.source[this.pos + offset];
204
+ }
205
+
206
+ /**
207
+ * Check if at end of input
208
+ */
209
+ isEOF() {
210
+ return this.pos >= this.source.length;
211
+ }
212
+
213
+ /**
214
+ * Advance position and return current char
215
+ */
216
+ advance() {
217
+ const char = this.current();
218
+ this.pos++;
219
+ if (char === '\n') {
220
+ this.line++;
221
+ this.column = 1;
222
+ } else {
223
+ this.column++;
224
+ }
225
+ return char;
226
+ }
227
+
228
+ /**
229
+ * Create a token
230
+ */
231
+ token(type, value, raw = null) {
232
+ return new Token(type, value, this.line, this.column, raw);
233
+ }
234
+
235
+ /**
236
+ * Skip whitespace (but not newlines in some contexts)
237
+ */
238
+ skipWhitespace(includeNewlines = true) {
239
+ while (!this.isEOF()) {
240
+ const char = this.current();
241
+ if (char === ' ' || char === '\t' || char === '\r') {
242
+ this.advance();
243
+ } else if (char === '\n' && includeNewlines) {
244
+ this.advance();
245
+ } else if (char === '/' && this.peek() === '/') {
246
+ // Single-line comment
247
+ while (!this.isEOF() && this.current() !== '\n') {
248
+ this.advance();
249
+ }
250
+ } else if (char === '/' && this.peek() === '*') {
251
+ // Multi-line comment
252
+ this.advance(); // /
253
+ this.advance(); // *
254
+ while (!this.isEOF() && !(this.current() === '*' && this.peek() === '/')) {
255
+ this.advance();
256
+ }
257
+ if (!this.isEOF()) {
258
+ this.advance(); // *
259
+ this.advance(); // /
260
+ }
261
+ } else {
262
+ break;
263
+ }
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Peek at the current word without advancing position
269
+ */
270
+ peekWord() {
271
+ let word = '';
272
+ let i = this.pos;
273
+ while (i < this.source.length && /[a-zA-Z0-9_$]/.test(this.source[i])) {
274
+ word += this.source[i];
275
+ i++;
276
+ }
277
+ return word;
278
+ }
279
+
280
+ /**
281
+ * Read a string literal
282
+ */
283
+ readString() {
284
+ const quote = this.advance(); // opening quote
285
+ const startLine = this.line;
286
+ const startColumn = this.column;
287
+ let value = '';
288
+ let raw = quote;
289
+
290
+ while (!this.isEOF() && this.current() !== quote) {
291
+ if (this.current() === '\\') {
292
+ raw += this.advance();
293
+ if (!this.isEOF()) {
294
+ const escaped = this.advance();
295
+ raw += escaped;
296
+ switch (escaped) {
297
+ case 'n': value += '\n'; break;
298
+ case 't': value += '\t'; break;
299
+ case 'r': value += '\r'; break;
300
+ case '\\': value += '\\'; break;
301
+ case '"': value += '"'; break;
302
+ case "'": value += "'"; break;
303
+ default: value += escaped;
304
+ }
305
+ }
306
+ } else {
307
+ value += this.current();
308
+ raw += this.advance();
309
+ }
310
+ }
311
+
312
+ if (!this.isEOF()) {
313
+ raw += this.advance(); // closing quote
314
+ }
315
+
316
+ return new Token(TokenType.STRING, value, startLine, startColumn, raw);
317
+ }
318
+
319
+ /**
320
+ * Read a template literal (backtick string)
321
+ */
322
+ readTemplateLiteral() {
323
+ const startLine = this.line;
324
+ const startColumn = this.column;
325
+ this.advance(); // opening backtick
326
+ let value = '';
327
+ let raw = '`';
328
+
329
+ while (!this.isEOF() && this.current() !== '`') {
330
+ if (this.current() === '\\') {
331
+ raw += this.advance();
332
+ if (!this.isEOF()) {
333
+ const escaped = this.advance();
334
+ raw += escaped;
335
+ value += escaped === 'n' ? '\n' : escaped === 't' ? '\t' : escaped;
336
+ }
337
+ } else if (this.current() === '$' && this.peek() === '{') {
338
+ // Template expression ${...}
339
+ value += this.current();
340
+ raw += this.advance();
341
+ value += this.current();
342
+ raw += this.advance();
343
+ let braceCount = 1;
344
+ while (!this.isEOF() && braceCount > 0) {
345
+ if (this.current() === '{') braceCount++;
346
+ else if (this.current() === '}') braceCount--;
347
+ value += this.current();
348
+ raw += this.advance();
349
+ }
350
+ } else {
351
+ value += this.current();
352
+ raw += this.advance();
353
+ }
354
+ }
355
+
356
+ if (!this.isEOF()) {
357
+ raw += this.advance(); // closing backtick
358
+ }
359
+
360
+ return new Token(TokenType.TEMPLATE, value, startLine, startColumn, raw);
361
+ }
362
+
363
+ /**
364
+ * Read a number literal
365
+ */
366
+ readNumber() {
367
+ const startLine = this.line;
368
+ const startColumn = this.column;
369
+ let value = '';
370
+
371
+ // Integer part
372
+ while (!this.isEOF() && /[0-9]/.test(this.current())) {
373
+ value += this.advance();
374
+ }
375
+
376
+ // Decimal part
377
+ if (this.current() === '.' && /[0-9]/.test(this.peek())) {
378
+ value += this.advance(); // .
379
+ while (!this.isEOF() && /[0-9]/.test(this.current())) {
380
+ value += this.advance();
381
+ }
382
+ }
383
+
384
+ // Exponent part
385
+ if (this.current() === 'e' || this.current() === 'E') {
386
+ value += this.advance();
387
+ if (this.current() === '+' || this.current() === '-') {
388
+ value += this.advance();
389
+ }
390
+ while (!this.isEOF() && /[0-9]/.test(this.current())) {
391
+ value += this.advance();
392
+ }
393
+ }
394
+
395
+ return new Token(TokenType.NUMBER, parseFloat(value), startLine, startColumn, value);
396
+ }
397
+
398
+ /**
399
+ * Read an identifier or keyword
400
+ */
401
+ readIdentifier() {
402
+ const startLine = this.line;
403
+ const startColumn = this.column;
404
+ let value = '';
405
+
406
+ while (!this.isEOF() && /[a-zA-Z0-9_$]/.test(this.current())) {
407
+ value += this.advance();
408
+ }
409
+
410
+ const type = KEYWORDS[value] || TokenType.IDENT;
411
+ return new Token(type, value, startLine, startColumn);
412
+ }
413
+
414
+ /**
415
+ * Read a CSS selector
416
+ * Examples: div, .class, #id, button.primary, input[type=text]
417
+ */
418
+ readSelector() {
419
+ const startLine = this.line;
420
+ const startColumn = this.column;
421
+ let value = '';
422
+
423
+ // Start with tag name if present
424
+ if (/[a-zA-Z]/.test(this.current())) {
425
+ while (!this.isEOF() && /[a-zA-Z0-9-]/.test(this.current())) {
426
+ value += this.advance();
427
+ }
428
+ }
429
+
430
+ // Continue with classes, ids, and attributes
431
+ while (!this.isEOF()) {
432
+ if (this.current() === '.') {
433
+ value += this.advance();
434
+ while (!this.isEOF() && /[a-zA-Z0-9_-]/.test(this.current())) {
435
+ value += this.advance();
436
+ }
437
+ } else if (this.current() === '#') {
438
+ value += this.advance();
439
+ while (!this.isEOF() && /[a-zA-Z0-9_-]/.test(this.current())) {
440
+ value += this.advance();
441
+ }
442
+ } else if (this.current() === '[') {
443
+ value += this.advance();
444
+ while (!this.isEOF() && this.current() !== ']') {
445
+ value += this.advance();
446
+ }
447
+ if (this.current() === ']') {
448
+ value += this.advance();
449
+ }
450
+ } else {
451
+ break;
452
+ }
453
+ }
454
+
455
+ return new Token(TokenType.SELECTOR, value, startLine, startColumn);
456
+ }
457
+
458
+ /**
459
+ * Tokenize the entire source
460
+ */
461
+ tokenize() {
462
+ this.tokens = [];
463
+ let inViewBlock = false;
464
+ let braceDepth = 0;
465
+
466
+ while (!this.isEOF()) {
467
+ this.skipWhitespace();
468
+
469
+ if (this.isEOF()) break;
470
+
471
+ const startLine = this.line;
472
+ const startColumn = this.column;
473
+ const char = this.current();
474
+
475
+ // Template literals
476
+ if (char === '`') {
477
+ this.tokens.push(this.readTemplateLiteral());
478
+ continue;
479
+ }
480
+
481
+ // String literals
482
+ if (char === '"' || char === "'") {
483
+ this.tokens.push(this.readString());
484
+ continue;
485
+ }
486
+
487
+ // Spread operator
488
+ if (char === '.' && this.peek() === '.' && this.peek(2) === '.') {
489
+ this.advance();
490
+ this.advance();
491
+ this.advance();
492
+ this.tokens.push(new Token(TokenType.SPREAD, '...', startLine, startColumn));
493
+ continue;
494
+ }
495
+
496
+ // Numbers
497
+ if (/[0-9]/.test(char)) {
498
+ this.tokens.push(this.readNumber());
499
+ continue;
500
+ }
501
+
502
+ // At-sign for directives
503
+ if (char === '@') {
504
+ this.advance();
505
+ this.tokens.push(new Token(TokenType.AT, '@', startLine, startColumn));
506
+ continue;
507
+ }
508
+
509
+ // Punctuation and operators
510
+ switch (char) {
511
+ case '{':
512
+ this.advance();
513
+ braceDepth++;
514
+ this.tokens.push(new Token(TokenType.LBRACE, '{', startLine, startColumn));
515
+ continue;
516
+ case '}':
517
+ this.advance();
518
+ braceDepth--;
519
+ this.tokens.push(new Token(TokenType.RBRACE, '}', startLine, startColumn));
520
+ continue;
521
+ case '(':
522
+ this.advance();
523
+ this.tokens.push(new Token(TokenType.LPAREN, '(', startLine, startColumn));
524
+ continue;
525
+ case ')':
526
+ this.advance();
527
+ this.tokens.push(new Token(TokenType.RPAREN, ')', startLine, startColumn));
528
+ continue;
529
+ case '[':
530
+ this.advance();
531
+ this.tokens.push(new Token(TokenType.LBRACKET, '[', startLine, startColumn));
532
+ continue;
533
+ case ']':
534
+ this.advance();
535
+ this.tokens.push(new Token(TokenType.RBRACKET, ']', startLine, startColumn));
536
+ continue;
537
+ case ':':
538
+ this.advance();
539
+ this.tokens.push(new Token(TokenType.COLON, ':', startLine, startColumn));
540
+ continue;
541
+ case ',':
542
+ this.advance();
543
+ this.tokens.push(new Token(TokenType.COMMA, ',', startLine, startColumn));
544
+ continue;
545
+ case ';':
546
+ this.advance();
547
+ this.tokens.push(new Token(TokenType.SEMICOLON, ';', startLine, startColumn));
548
+ continue;
549
+ case '+':
550
+ this.advance();
551
+ if (this.current() === '+') {
552
+ this.advance();
553
+ this.tokens.push(new Token(TokenType.PLUSPLUS, '++', startLine, startColumn));
554
+ } else {
555
+ this.tokens.push(new Token(TokenType.PLUS, '+', startLine, startColumn));
556
+ }
557
+ continue;
558
+ case '-':
559
+ this.advance();
560
+ if (this.current() === '-') {
561
+ this.advance();
562
+ this.tokens.push(new Token(TokenType.MINUSMINUS, '--', startLine, startColumn));
563
+ } else {
564
+ this.tokens.push(new Token(TokenType.MINUS, '-', startLine, startColumn));
565
+ }
566
+ continue;
567
+ case '*':
568
+ this.advance();
569
+ this.tokens.push(new Token(TokenType.STAR, '*', startLine, startColumn));
570
+ continue;
571
+ case '/':
572
+ this.advance();
573
+ this.tokens.push(new Token(TokenType.SLASH, '/', startLine, startColumn));
574
+ continue;
575
+ case '=':
576
+ this.advance();
577
+ if (this.current() === '=') {
578
+ this.advance();
579
+ if (this.current() === '=') {
580
+ this.advance();
581
+ this.tokens.push(new Token(TokenType.EQEQEQ, '===', startLine, startColumn));
582
+ } else {
583
+ this.tokens.push(new Token(TokenType.EQEQ, '==', startLine, startColumn));
584
+ }
585
+ } else if (this.current() === '>') {
586
+ this.advance();
587
+ this.tokens.push(new Token(TokenType.ARROW, '=>', startLine, startColumn));
588
+ } else {
589
+ this.tokens.push(new Token(TokenType.EQ, '=', startLine, startColumn));
590
+ }
591
+ continue;
592
+ case '?':
593
+ this.advance();
594
+ this.tokens.push(new Token(TokenType.QUESTION, '?', startLine, startColumn));
595
+ continue;
596
+ case '%':
597
+ this.advance();
598
+ this.tokens.push(new Token(TokenType.PERCENT, '%', startLine, startColumn));
599
+ continue;
600
+ case '!':
601
+ this.advance();
602
+ if (this.current() === '=') {
603
+ this.advance();
604
+ if (this.current() === '=') {
605
+ this.advance();
606
+ this.tokens.push(new Token(TokenType.NEQEQ, '!==', startLine, startColumn));
607
+ } else {
608
+ this.tokens.push(new Token(TokenType.NEQ, '!=', startLine, startColumn));
609
+ }
610
+ } else {
611
+ this.tokens.push(new Token(TokenType.NOT, '!', startLine, startColumn));
612
+ }
613
+ continue;
614
+ case '<':
615
+ this.advance();
616
+ if (this.current() === '=') {
617
+ this.advance();
618
+ this.tokens.push(new Token(TokenType.LTE, '<=', startLine, startColumn));
619
+ } else {
620
+ this.tokens.push(new Token(TokenType.LT, '<', startLine, startColumn));
621
+ }
622
+ continue;
623
+ case '>':
624
+ this.advance();
625
+ if (this.current() === '=') {
626
+ this.advance();
627
+ this.tokens.push(new Token(TokenType.GTE, '>=', startLine, startColumn));
628
+ } else {
629
+ this.tokens.push(new Token(TokenType.GT, '>', startLine, startColumn));
630
+ }
631
+ continue;
632
+ case '&':
633
+ this.advance();
634
+ if (this.current() === '&') {
635
+ this.advance();
636
+ this.tokens.push(new Token(TokenType.AND, '&&', startLine, startColumn));
637
+ }
638
+ continue;
639
+ case '|':
640
+ this.advance();
641
+ if (this.current() === '|') {
642
+ this.advance();
643
+ this.tokens.push(new Token(TokenType.OR, '||', startLine, startColumn));
644
+ }
645
+ continue;
646
+ }
647
+
648
+ // Check for selector start (. or #) in view context
649
+ if ((char === '.' || char === '#') && this.isViewContext()) {
650
+ this.tokens.push(this.readSelector());
651
+ continue;
652
+ }
653
+
654
+ // Dot outside selector
655
+ if (char === '.') {
656
+ this.advance();
657
+ this.tokens.push(new Token(TokenType.DOT, '.', startLine, startColumn));
658
+ continue;
659
+ }
660
+
661
+ // Hash outside selector
662
+ if (char === '#') {
663
+ this.advance();
664
+ this.tokens.push(new Token(TokenType.HASH, '#', startLine, startColumn));
665
+ continue;
666
+ }
667
+
668
+ // Identifiers, keywords, and selectors
669
+ if (/[a-zA-Z_$]/.test(char)) {
670
+ // First check if this is a keyword - keywords take precedence
671
+ const word = this.peekWord();
672
+ if (KEYWORDS[word]) {
673
+ this.tokens.push(this.readIdentifier());
674
+ } else if (this.isViewContext() && this.couldBeSelector()) {
675
+ // Only treat as selector if not a keyword
676
+ this.tokens.push(this.readSelector());
677
+ } else {
678
+ this.tokens.push(this.readIdentifier());
679
+ }
680
+ continue;
681
+ }
682
+
683
+ // Unknown character - error
684
+ this.tokens.push(new Token(TokenType.ERROR, char, startLine, startColumn));
685
+ this.advance();
686
+ }
687
+
688
+ this.tokens.push(new Token(TokenType.EOF, null, this.line, this.column));
689
+ return this.tokens;
690
+ }
691
+
692
+ /**
693
+ * Check if we're in a view context where selectors are expected
694
+ */
695
+ isViewContext() {
696
+ // Look back through tokens for 'view' keyword
697
+ let inView = false;
698
+ let parenDepth = 0;
699
+
700
+ for (let i = this.tokens.length - 1; i >= 0; i--) {
701
+ const token = this.tokens[i];
702
+
703
+ // Track parentheses depth (backwards)
704
+ if (token.type === TokenType.RPAREN) {
705
+ parenDepth++;
706
+ } else if (token.type === TokenType.LPAREN) {
707
+ parenDepth--;
708
+ // If we go negative, we're inside parentheses (expression context)
709
+ if (parenDepth < 0) {
710
+ return false; // Inside expression, not selector context
711
+ }
712
+ }
713
+
714
+ if (token.type === TokenType.VIEW) {
715
+ inView = true;
716
+ break;
717
+ }
718
+ if (token.type === TokenType.STATE ||
719
+ token.type === TokenType.ACTIONS ||
720
+ token.type === TokenType.STYLE ||
721
+ token.type === TokenType.ROUTER ||
722
+ token.type === TokenType.STORE ||
723
+ token.type === TokenType.ROUTES ||
724
+ token.type === TokenType.GETTERS) {
725
+ return false;
726
+ }
727
+ }
728
+
729
+ return inView;
730
+ }
731
+
732
+ /**
733
+ * Check if current position could start a selector
734
+ */
735
+ couldBeSelector() {
736
+ // Check if followed by . or # or [ or { which indicate selector continuation
737
+ let lookahead = 0;
738
+ while (this.pos + lookahead < this.source.length) {
739
+ const char = this.source[this.pos + lookahead];
740
+ if (/[a-zA-Z0-9_-]/.test(char)) {
741
+ lookahead++;
742
+ continue;
743
+ }
744
+ if (char === '.' || char === '#' || char === '[' || char === '{' || char === ' ') {
745
+ return true;
746
+ }
747
+ break;
748
+ }
749
+ return false;
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Convenience function to tokenize source
755
+ */
756
+ export function tokenize(source) {
757
+ const lexer = new Lexer(source);
758
+ return lexer.tokenize();
759
+ }
760
+
761
+ export default {
762
+ TokenType,
763
+ Token,
764
+ Lexer,
765
+ tokenize
766
+ };