@lithent/lithent-template-parser 0.1.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 +159 -0
- package/dist/codegen/createElement.d.ts +10 -0
- package/dist/codegen/index.d.ts +1 -0
- package/dist/compiler.d.ts +26 -0
- package/dist/docPipe/index.d.ts +40 -0
- package/dist/docPipe/scan.d.ts +6 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.mjs +2302 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.cjs +13 -0
- package/dist/index.umd.cjs.map +1 -0
- package/dist/parser/ast.d.ts +207 -0
- package/dist/parser/lexer.d.ts +109 -0
- package/dist/parser/parser.d.ts +79 -0
- package/dist/parser/tokens.d.ts +51 -0
- package/dist/transform/directives.d.ts +30 -0
- package/dist/transform/index.d.ts +14 -0
- package/dist/transform/optimize.d.ts +21 -0
- package/package.json +53 -0
- package/src/codegen/createElement.ts +661 -0
- package/src/codegen/index.ts +1 -0
- package/src/compiler.ts +70 -0
- package/src/docPipe/index.ts +104 -0
- package/src/docPipe/scan.ts +318 -0
- package/src/index.ts +16 -0
- package/src/parser/ast.ts +478 -0
- package/src/parser/lexer.ts +561 -0
- package/src/parser/parser.ts +514 -0
- package/src/parser/tokens.ts +84 -0
- package/src/playground/main.ts +206 -0
- package/src/playground/style.css +135 -0
- package/src/shims/babel-env.ts +17 -0
- package/src/tests/compiler.test.ts +434 -0
- package/src/tests/docPipe.test.ts +41 -0
- package/src/tests/helpers.ts +96 -0
- package/src/tests/lexer.test.ts +282 -0
- package/src/tests/parser.test.ts +691 -0
- package/src/transform/directives.ts +160 -0
- package/src/transform/index.ts +29 -0
- package/src/transform/optimize.ts +203 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Token,
|
|
3
|
+
TokenType,
|
|
4
|
+
Position,
|
|
5
|
+
createPosition,
|
|
6
|
+
createToken,
|
|
7
|
+
} from './tokens';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Lexer for Lithent templates
|
|
11
|
+
* Converts template string into a stream of tokens
|
|
12
|
+
*/
|
|
13
|
+
export class Lexer {
|
|
14
|
+
private input: string;
|
|
15
|
+
private position: number = 0;
|
|
16
|
+
private line: number = 1;
|
|
17
|
+
private column: number = 1;
|
|
18
|
+
private tokens: Token[] = [];
|
|
19
|
+
|
|
20
|
+
constructor(input: string) {
|
|
21
|
+
this.input = input;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Tokenize the entire input
|
|
26
|
+
*/
|
|
27
|
+
public tokenize(): Token[] {
|
|
28
|
+
while (this.position < this.input.length) {
|
|
29
|
+
this.readToken();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Add EOF token
|
|
33
|
+
const pos = this.getCurrentPosition();
|
|
34
|
+
this.tokens.push(createToken(TokenType.EOF, '', pos, pos));
|
|
35
|
+
|
|
36
|
+
return this.tokens;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get current position
|
|
41
|
+
*/
|
|
42
|
+
private getCurrentPosition(): Position {
|
|
43
|
+
return createPosition(this.line, this.column, this.position);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Peek at character without consuming
|
|
48
|
+
*/
|
|
49
|
+
private peek(offset: number = 0): string {
|
|
50
|
+
return this.input[this.position + offset] || '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Consume and return current character
|
|
55
|
+
*/
|
|
56
|
+
private advance(): string {
|
|
57
|
+
const char = this.input[this.position];
|
|
58
|
+
this.position++;
|
|
59
|
+
|
|
60
|
+
if (char === '\n') {
|
|
61
|
+
this.line++;
|
|
62
|
+
this.column = 1;
|
|
63
|
+
} else {
|
|
64
|
+
this.column++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return char;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if we've reached the end
|
|
72
|
+
*/
|
|
73
|
+
private isEOF(): boolean {
|
|
74
|
+
return this.position >= this.input.length;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read next token
|
|
79
|
+
*/
|
|
80
|
+
private readToken(): void {
|
|
81
|
+
const char = this.peek();
|
|
82
|
+
|
|
83
|
+
// Fragment open <>
|
|
84
|
+
if (char === '<' && this.peek(1) === '>') {
|
|
85
|
+
this.readFragmentOpen();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fragment close </>
|
|
90
|
+
if (char === '<' && this.peek(1) === '/' && this.peek(2) === '>') {
|
|
91
|
+
this.readFragmentClose();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for comments first
|
|
96
|
+
if (
|
|
97
|
+
char === '<' &&
|
|
98
|
+
this.peek(1) === '!' &&
|
|
99
|
+
this.peek(2) === '-' &&
|
|
100
|
+
this.peek(3) === '-'
|
|
101
|
+
) {
|
|
102
|
+
this.readComment();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check for tags
|
|
107
|
+
if (char === '<') {
|
|
108
|
+
if (this.peek(1) === '/') {
|
|
109
|
+
this.readCloseTag();
|
|
110
|
+
} else {
|
|
111
|
+
this.readOpenTag();
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for expressions
|
|
117
|
+
if (char === '{') {
|
|
118
|
+
this.readExpression();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check for whitespace
|
|
123
|
+
if (this.isWhitespace(char)) {
|
|
124
|
+
this.readWhitespace();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Otherwise it's text content
|
|
129
|
+
this.readText();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Read HTML comment
|
|
134
|
+
*/
|
|
135
|
+
private readComment(): void {
|
|
136
|
+
const start = this.getCurrentPosition();
|
|
137
|
+
let value = '';
|
|
138
|
+
|
|
139
|
+
// Consume <!--
|
|
140
|
+
value += this.advance() + this.advance() + this.advance() + this.advance();
|
|
141
|
+
|
|
142
|
+
// Read until -->
|
|
143
|
+
while (!this.isEOF()) {
|
|
144
|
+
if (this.peek() === '-' && this.peek(1) === '-' && this.peek(2) === '>') {
|
|
145
|
+
value += this.advance() + this.advance() + this.advance();
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
value += this.advance();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const end = this.getCurrentPosition();
|
|
152
|
+
this.tokens.push(createToken(TokenType.COMMENT, value, start, end));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Read opening tag <
|
|
157
|
+
*/
|
|
158
|
+
private readOpenTag(): void {
|
|
159
|
+
const start = this.getCurrentPosition();
|
|
160
|
+
this.advance(); // consume <
|
|
161
|
+
const end = this.getCurrentPosition();
|
|
162
|
+
this.tokens.push(createToken(TokenType.TAG_OPEN_START, '<', start, end));
|
|
163
|
+
|
|
164
|
+
// Now read attributes until we hit > or />
|
|
165
|
+
this.readTagContent();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Read closing tag </
|
|
170
|
+
*/
|
|
171
|
+
private readCloseTag(): void {
|
|
172
|
+
const start = this.getCurrentPosition();
|
|
173
|
+
this.advance(); // consume <
|
|
174
|
+
this.advance(); // consume /
|
|
175
|
+
const end = this.getCurrentPosition();
|
|
176
|
+
this.tokens.push(createToken(TokenType.TAG_CLOSE_START, '</', start, end));
|
|
177
|
+
|
|
178
|
+
// Read tag name
|
|
179
|
+
this.skipWhitespace();
|
|
180
|
+
this.readIdentifier();
|
|
181
|
+
|
|
182
|
+
// Read closing >
|
|
183
|
+
this.skipWhitespace();
|
|
184
|
+
if (this.peek() === '>') {
|
|
185
|
+
const start = this.getCurrentPosition();
|
|
186
|
+
this.advance();
|
|
187
|
+
const end = this.getCurrentPosition();
|
|
188
|
+
this.tokens.push(createToken(TokenType.TAG_OPEN_END, '>', start, end));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Read tag content (tag name and attributes)
|
|
194
|
+
*/
|
|
195
|
+
private readTagContent(): void {
|
|
196
|
+
this.skipWhitespace();
|
|
197
|
+
|
|
198
|
+
// Read tag name
|
|
199
|
+
this.readIdentifier();
|
|
200
|
+
|
|
201
|
+
// Read attributes
|
|
202
|
+
while (!this.isEOF()) {
|
|
203
|
+
this.skipWhitespace();
|
|
204
|
+
|
|
205
|
+
const char = this.peek();
|
|
206
|
+
|
|
207
|
+
// Self-closing tag
|
|
208
|
+
if (char === '/' && this.peek(1) === '>') {
|
|
209
|
+
const start = this.getCurrentPosition();
|
|
210
|
+
this.advance();
|
|
211
|
+
this.advance();
|
|
212
|
+
const end = this.getCurrentPosition();
|
|
213
|
+
this.tokens.push(
|
|
214
|
+
createToken(TokenType.TAG_SELF_CLOSE, '/>', start, end)
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// End of opening tag
|
|
220
|
+
if (char === '>') {
|
|
221
|
+
const start = this.getCurrentPosition();
|
|
222
|
+
this.advance();
|
|
223
|
+
const end = this.getCurrentPosition();
|
|
224
|
+
this.tokens.push(createToken(TokenType.TAG_OPEN_END, '>', start, end));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Read attribute
|
|
229
|
+
this.readAttribute();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Read an attribute (name and optional value)
|
|
235
|
+
*/
|
|
236
|
+
private readAttribute(): void {
|
|
237
|
+
const identifier = this.readIdentifier();
|
|
238
|
+
if (!identifier) return;
|
|
239
|
+
|
|
240
|
+
this.skipWhitespace();
|
|
241
|
+
|
|
242
|
+
// Check for = sign
|
|
243
|
+
if (this.peek() === '=') {
|
|
244
|
+
const start = this.getCurrentPosition();
|
|
245
|
+
this.advance();
|
|
246
|
+
const end = this.getCurrentPosition();
|
|
247
|
+
this.tokens.push(
|
|
248
|
+
createToken(TokenType.ATTRIBUTE_EQUALS, '=', start, end)
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
this.skipWhitespace();
|
|
252
|
+
|
|
253
|
+
// Read attribute value
|
|
254
|
+
const char = this.peek();
|
|
255
|
+
if (char === '"' || char === "'") {
|
|
256
|
+
this.readStringLiteral();
|
|
257
|
+
} else if (char === '{') {
|
|
258
|
+
this.readExpression();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Read identifier (tag name or attribute name)
|
|
265
|
+
*/
|
|
266
|
+
private readIdentifier(): string {
|
|
267
|
+
const start = this.getCurrentPosition();
|
|
268
|
+
let value = '';
|
|
269
|
+
|
|
270
|
+
// First character must be letter, underscore, or $ (or : for namespaced attributes)
|
|
271
|
+
const first = this.peek();
|
|
272
|
+
if (this.isIdentifierStart(first)) {
|
|
273
|
+
value += this.advance();
|
|
274
|
+
} else {
|
|
275
|
+
// Consume unexpected character to avoid infinite loops
|
|
276
|
+
const unexpectedStart = this.getCurrentPosition();
|
|
277
|
+
const char = this.advance();
|
|
278
|
+
const unexpectedEnd = this.getCurrentPosition();
|
|
279
|
+
this.tokens.push(
|
|
280
|
+
createToken(TokenType.TEXT, char, unexpectedStart, unexpectedEnd)
|
|
281
|
+
);
|
|
282
|
+
return '';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Read rest of identifier
|
|
286
|
+
while (!this.isEOF() && this.isIdentifierPart(this.peek())) {
|
|
287
|
+
value += this.advance();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const end = this.getCurrentPosition();
|
|
291
|
+
|
|
292
|
+
// Check if this is a directive or special keyword
|
|
293
|
+
const tokenType = this.getIdentifierTokenType(value);
|
|
294
|
+
this.tokens.push(createToken(tokenType, value, start, end));
|
|
295
|
+
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Determine token type for identifier
|
|
301
|
+
*/
|
|
302
|
+
private getIdentifierTokenType(value: string): TokenType {
|
|
303
|
+
switch (value) {
|
|
304
|
+
case 'l-if':
|
|
305
|
+
return TokenType.DIRECTIVE_IF;
|
|
306
|
+
case 'l-else-if':
|
|
307
|
+
return TokenType.DIRECTIVE_ELSE_IF;
|
|
308
|
+
case 'l-else':
|
|
309
|
+
return TokenType.DIRECTIVE_ELSE;
|
|
310
|
+
case 'l-for':
|
|
311
|
+
return TokenType.DIRECTIVE_FOR;
|
|
312
|
+
case 'ref':
|
|
313
|
+
return TokenType.KEYWORD_REF;
|
|
314
|
+
default:
|
|
315
|
+
return TokenType.IDENTIFIER;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Read string literal
|
|
321
|
+
*/
|
|
322
|
+
private readStringLiteral(): void {
|
|
323
|
+
const start = this.getCurrentPosition();
|
|
324
|
+
const quote = this.advance(); // " or '
|
|
325
|
+
let value = quote;
|
|
326
|
+
let escaped = false;
|
|
327
|
+
|
|
328
|
+
while (!this.isEOF()) {
|
|
329
|
+
const char = this.peek();
|
|
330
|
+
|
|
331
|
+
if (escaped) {
|
|
332
|
+
value += this.advance();
|
|
333
|
+
escaped = false;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (char === '\\') {
|
|
338
|
+
escaped = true;
|
|
339
|
+
value += this.advance();
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (char === quote) {
|
|
344
|
+
value += this.advance();
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
value += this.advance();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const end = this.getCurrentPosition();
|
|
352
|
+
this.tokens.push(createToken(TokenType.STRING_LITERAL, value, start, end));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Read fragment open <>
|
|
357
|
+
*/
|
|
358
|
+
private readFragmentOpen(): void {
|
|
359
|
+
const start = this.getCurrentPosition();
|
|
360
|
+
this.advance(); // <
|
|
361
|
+
this.advance(); // >
|
|
362
|
+
const end = this.getCurrentPosition();
|
|
363
|
+
this.tokens.push(createToken(TokenType.FRAGMENT_OPEN, '<>', start, end));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Read fragment close </>
|
|
368
|
+
*/
|
|
369
|
+
private readFragmentClose(): void {
|
|
370
|
+
const start = this.getCurrentPosition();
|
|
371
|
+
this.advance(); // <
|
|
372
|
+
this.advance(); // /
|
|
373
|
+
this.advance(); // >
|
|
374
|
+
const end = this.getCurrentPosition();
|
|
375
|
+
this.tokens.push(createToken(TokenType.FRAGMENT_CLOSE, '</>', start, end));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Read expression content inside {}
|
|
380
|
+
*/
|
|
381
|
+
private readExpression(): void {
|
|
382
|
+
const startPos = this.getCurrentPosition();
|
|
383
|
+
this.advance(); // consume {
|
|
384
|
+
const openEnd = this.getCurrentPosition();
|
|
385
|
+
this.tokens.push(
|
|
386
|
+
createToken(TokenType.EXPRESSION_START, '{', startPos, openEnd)
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
let value = '';
|
|
390
|
+
let depth = 1;
|
|
391
|
+
let inString: string | null = null;
|
|
392
|
+
let escaped = false;
|
|
393
|
+
let contentStart: Position | null = null;
|
|
394
|
+
let contentEnd: Position = openEnd;
|
|
395
|
+
|
|
396
|
+
const consumeChar = (): string => {
|
|
397
|
+
const charStart = this.getCurrentPosition();
|
|
398
|
+
const char = this.advance();
|
|
399
|
+
const charEnd = this.getCurrentPosition();
|
|
400
|
+
|
|
401
|
+
value += char;
|
|
402
|
+
if (!contentStart) {
|
|
403
|
+
contentStart = charStart;
|
|
404
|
+
}
|
|
405
|
+
contentEnd = charEnd;
|
|
406
|
+
|
|
407
|
+
return char;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
while (!this.isEOF() && depth > 0) {
|
|
411
|
+
const char = this.peek();
|
|
412
|
+
|
|
413
|
+
// Handle string literals inside expressions
|
|
414
|
+
if (inString) {
|
|
415
|
+
if (escaped) {
|
|
416
|
+
consumeChar();
|
|
417
|
+
escaped = false;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (char === '\\') {
|
|
421
|
+
escaped = true;
|
|
422
|
+
consumeChar();
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (char === inString) {
|
|
426
|
+
consumeChar();
|
|
427
|
+
inString = null;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
consumeChar();
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Not in string
|
|
435
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
436
|
+
inString = char;
|
|
437
|
+
consumeChar();
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (char === '{') {
|
|
442
|
+
depth++;
|
|
443
|
+
consumeChar();
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (char === '}') {
|
|
448
|
+
depth--;
|
|
449
|
+
if (depth === 0) {
|
|
450
|
+
// End of expression
|
|
451
|
+
if (value.trim()) {
|
|
452
|
+
const start = contentStart ?? openEnd;
|
|
453
|
+
const end = contentEnd;
|
|
454
|
+
this.tokens.push(
|
|
455
|
+
createToken(TokenType.EXPRESSION_CONTENT, value, start, end)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const closeStart = this.getCurrentPosition();
|
|
460
|
+
this.advance(); // consume }
|
|
461
|
+
const closeEnd = this.getCurrentPosition();
|
|
462
|
+
this.tokens.push(
|
|
463
|
+
createToken(TokenType.EXPRESSION_END, '}', closeStart, closeEnd)
|
|
464
|
+
);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
consumeChar();
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
consumeChar();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// If we get here, expression wasn't closed properly
|
|
475
|
+
// Still add the content we have
|
|
476
|
+
if (value.trim()) {
|
|
477
|
+
const start = contentStart ?? openEnd;
|
|
478
|
+
const end = contentStart ? contentEnd : this.getCurrentPosition();
|
|
479
|
+
this.tokens.push(
|
|
480
|
+
createToken(TokenType.EXPRESSION_CONTENT, value, start, end)
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Read text content
|
|
487
|
+
*/
|
|
488
|
+
private readText(): void {
|
|
489
|
+
const start = this.getCurrentPosition();
|
|
490
|
+
let value = '';
|
|
491
|
+
|
|
492
|
+
while (!this.isEOF()) {
|
|
493
|
+
const char = this.peek();
|
|
494
|
+
|
|
495
|
+
// Stop at tag or expression start
|
|
496
|
+
if (char === '<' || char === '{') {
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
value += this.advance();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (value) {
|
|
504
|
+
const end = this.getCurrentPosition();
|
|
505
|
+
this.tokens.push(createToken(TokenType.TEXT, value, start, end));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Read whitespace
|
|
511
|
+
*/
|
|
512
|
+
private readWhitespace(): void {
|
|
513
|
+
const start = this.getCurrentPosition();
|
|
514
|
+
let value = '';
|
|
515
|
+
|
|
516
|
+
while (!this.isEOF() && this.isWhitespace(this.peek())) {
|
|
517
|
+
value += this.advance();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const end = this.getCurrentPosition();
|
|
521
|
+
this.tokens.push(createToken(TokenType.WHITESPACE, value, start, end));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Skip whitespace without creating tokens
|
|
526
|
+
*/
|
|
527
|
+
private skipWhitespace(): void {
|
|
528
|
+
while (!this.isEOF() && this.isWhitespace(this.peek())) {
|
|
529
|
+
this.advance();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Check if character is whitespace
|
|
535
|
+
*/
|
|
536
|
+
private isWhitespace(char: string): boolean {
|
|
537
|
+
return /\s/.test(char);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Check if character can start an identifier
|
|
542
|
+
*/
|
|
543
|
+
private isIdentifierStart(char: string): boolean {
|
|
544
|
+
return /[a-zA-Z_$:]/.test(char);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Check if character can be part of an identifier
|
|
549
|
+
*/
|
|
550
|
+
private isIdentifierPart(char: string): boolean {
|
|
551
|
+
return /[a-zA-Z0-9_$:.-]/.test(char);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Tokenize a template string
|
|
557
|
+
*/
|
|
558
|
+
export function tokenize(input: string): Token[] {
|
|
559
|
+
const lexer = new Lexer(input);
|
|
560
|
+
return lexer.tokenize();
|
|
561
|
+
}
|