pulse-js-framework 1.0.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.
@@ -0,0 +1,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
+ 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
+ };