pulse-js-framework 1.4.1 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +414 -414
- package/cli/analyze.js +499 -499
- package/cli/build.js +341 -341
- package/cli/dev.js +3 -3
- package/cli/format.js +704 -704
- package/cli/index.js +398 -398
- package/cli/lint.js +642 -642
- package/cli/utils/file-utils.js +298 -298
- package/compiler/lexer.js +766 -766
- package/compiler/parser.js +1797 -1797
- package/compiler/transformer.js +1332 -1332
- package/index.js +1 -1
- package/loader/vite-plugin.js +1 -1
- package/package.json +68 -68
- package/runtime/router.js +596 -596
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
|
+
};
|