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.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/cli/build.js +199 -0
- package/cli/dev.js +225 -0
- package/cli/index.js +324 -0
- package/compiler/index.js +65 -0
- package/compiler/lexer.js +581 -0
- package/compiler/parser.js +900 -0
- package/compiler/transformer.js +552 -0
- package/index.js +19 -0
- package/loader/vite-plugin.js +160 -0
- package/package.json +58 -0
- package/runtime/dom.js +484 -0
- package/runtime/index.js +13 -0
- package/runtime/pulse.js +339 -0
- package/runtime/router.js +392 -0
- package/runtime/store.js +301 -0
|
@@ -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
|
+
};
|