pulse-js-framework 1.4.0 → 1.4.2

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,766 +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
- 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
- };
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
+ };