@chilts/weave 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/ReadMe.md +177 -0
- package/bin/weave.js +4 -0
- package/dist/cli/index.d.ts +31 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +250 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/codegen/index.d.ts +27 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +604 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/sourcemap.d.ts +43 -0
- package/dist/codegen/sourcemap.d.ts.map +1 -0
- package/dist/codegen/sourcemap.js +113 -0
- package/dist/codegen/sourcemap.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer/index.d.ts +14 -0
- package/dist/lexer/index.d.ts.map +1 -0
- package/dist/lexer/index.js +679 -0
- package/dist/lexer/index.js.map +1 -0
- package/dist/parser/index.d.ts +10 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +392 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/types.d.ts +125 -0
- package/dist/parser/types.d.ts.map +1 -0
- package/dist/parser/types.js +3 -0
- package/dist/parser/types.js.map +1 -0
- package/dist/runtime/index.d.ts +15 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +82 -0
- package/dist/runtime/index.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
export class LexerError extends Error {
|
|
2
|
+
line;
|
|
3
|
+
col;
|
|
4
|
+
constructor(message, line, col) {
|
|
5
|
+
super(`${message} at line ${line}, col ${col}`);
|
|
6
|
+
this.name = 'LexerError';
|
|
7
|
+
this.line = line;
|
|
8
|
+
this.col = col;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// Keywords that are recognized at the start of a line.
|
|
12
|
+
// Each must be followed by a space, '(', or end-of-line to be treated as a keyword.
|
|
13
|
+
const KEYWORDS = new Set([
|
|
14
|
+
'import',
|
|
15
|
+
'template',
|
|
16
|
+
'mixin',
|
|
17
|
+
'extends',
|
|
18
|
+
'block',
|
|
19
|
+
'include',
|
|
20
|
+
'doctype',
|
|
21
|
+
'if',
|
|
22
|
+
'for',
|
|
23
|
+
]);
|
|
24
|
+
class Lexer {
|
|
25
|
+
lines;
|
|
26
|
+
lineIndex = 0;
|
|
27
|
+
pos = 0;
|
|
28
|
+
currentLine = '';
|
|
29
|
+
indentWidth = 0; // leading spaces of current line
|
|
30
|
+
indentStack = [0];
|
|
31
|
+
tokens = [];
|
|
32
|
+
constructor(source) {
|
|
33
|
+
this.lines = source.split('\n');
|
|
34
|
+
}
|
|
35
|
+
lex() {
|
|
36
|
+
for (this.lineIndex = 0; this.lineIndex < this.lines.length; this.lineIndex++) {
|
|
37
|
+
const rawLine = this.lines[this.lineIndex];
|
|
38
|
+
// Skip blank lines (empty or whitespace-only)
|
|
39
|
+
if (rawLine.trim().length === 0) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Measure indentation
|
|
43
|
+
this.indentWidth = this.measureIndent(rawLine);
|
|
44
|
+
this.currentLine = rawLine.slice(this.indentWidth);
|
|
45
|
+
this.pos = 0;
|
|
46
|
+
// Emit INDENT/DEDENT tokens
|
|
47
|
+
this.emitIndentation(this.indentWidth);
|
|
48
|
+
// Dispatch based on line content
|
|
49
|
+
this.processLine();
|
|
50
|
+
// Emit NEWLINE
|
|
51
|
+
this.emit('NEWLINE', '');
|
|
52
|
+
}
|
|
53
|
+
// Close any remaining indentation
|
|
54
|
+
while (this.indentStack.length > 1) {
|
|
55
|
+
this.indentStack.pop();
|
|
56
|
+
this.emit('DEDENT', '');
|
|
57
|
+
}
|
|
58
|
+
this.emit('EOF', '');
|
|
59
|
+
return this.tokens;
|
|
60
|
+
}
|
|
61
|
+
measureIndent(line) {
|
|
62
|
+
let indent = 0;
|
|
63
|
+
for (let i = 0; i < line.length; i++) {
|
|
64
|
+
if (line[i] === ' ') {
|
|
65
|
+
indent++;
|
|
66
|
+
}
|
|
67
|
+
else if (line[i] === '\t') {
|
|
68
|
+
throw new LexerError('Tab characters are not allowed for indentation, use spaces', this.lineIndex + 1, i + 1);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return indent;
|
|
75
|
+
}
|
|
76
|
+
emitIndentation(indent) {
|
|
77
|
+
const current = this.indentStack[this.indentStack.length - 1];
|
|
78
|
+
if (indent > current) {
|
|
79
|
+
this.indentStack.push(indent);
|
|
80
|
+
this.emit('INDENT', '');
|
|
81
|
+
}
|
|
82
|
+
else if (indent < current) {
|
|
83
|
+
while (this.indentStack[this.indentStack.length - 1] > indent) {
|
|
84
|
+
this.indentStack.pop();
|
|
85
|
+
this.emit('DEDENT', '');
|
|
86
|
+
}
|
|
87
|
+
if (this.indentStack[this.indentStack.length - 1] !== indent) {
|
|
88
|
+
throw new LexerError('Inconsistent indentation', this.lineIndex + 1, indent + 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
processLine() {
|
|
93
|
+
const line = this.currentLine;
|
|
94
|
+
// Comment: //-
|
|
95
|
+
if (line.startsWith('//-')) {
|
|
96
|
+
this.scanComment();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Pass-through comment: //
|
|
100
|
+
if (line.startsWith('//')) {
|
|
101
|
+
this.scanPassthroughComment();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// else if (must check before 'else' alone)
|
|
105
|
+
if (line === 'else if' || line.startsWith('else if ') || line.startsWith('else if(')) {
|
|
106
|
+
this.scanElseIf();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// else
|
|
110
|
+
if (line === 'else') {
|
|
111
|
+
this.emit('ELSE', 'else');
|
|
112
|
+
this.pos = 4;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Check single-word keywords with word boundary
|
|
116
|
+
for (const keyword of KEYWORDS) {
|
|
117
|
+
if (this.matchesKeyword(line, keyword)) {
|
|
118
|
+
this.dispatchKeyword(keyword);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Mixin call: +name(...)
|
|
123
|
+
if (line.startsWith('+')) {
|
|
124
|
+
this.scanMixinCall();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Pipe text: | content
|
|
128
|
+
if (line.startsWith('|')) {
|
|
129
|
+
this.scanPipe();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Element: starts with tag, .class, or #id
|
|
133
|
+
this.scanElement();
|
|
134
|
+
}
|
|
135
|
+
matchesKeyword(line, keyword) {
|
|
136
|
+
if (!line.startsWith(keyword))
|
|
137
|
+
return false;
|
|
138
|
+
if (line.length === keyword.length)
|
|
139
|
+
return true;
|
|
140
|
+
const next = line[keyword.length];
|
|
141
|
+
return next === ' ' || next === '(';
|
|
142
|
+
}
|
|
143
|
+
dispatchKeyword(keyword) {
|
|
144
|
+
switch (keyword) {
|
|
145
|
+
case 'import':
|
|
146
|
+
this.scanImport();
|
|
147
|
+
break;
|
|
148
|
+
case 'template':
|
|
149
|
+
this.scanTemplate();
|
|
150
|
+
break;
|
|
151
|
+
case 'mixin':
|
|
152
|
+
this.scanMixin();
|
|
153
|
+
break;
|
|
154
|
+
case 'extends':
|
|
155
|
+
this.scanExtends();
|
|
156
|
+
break;
|
|
157
|
+
case 'block':
|
|
158
|
+
this.scanBlock();
|
|
159
|
+
break;
|
|
160
|
+
case 'include':
|
|
161
|
+
this.scanInclude();
|
|
162
|
+
break;
|
|
163
|
+
case 'doctype':
|
|
164
|
+
this.scanDoctype();
|
|
165
|
+
break;
|
|
166
|
+
case 'if':
|
|
167
|
+
this.scanIf();
|
|
168
|
+
break;
|
|
169
|
+
case 'for':
|
|
170
|
+
this.scanFor();
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// --- Simple scanners ---
|
|
175
|
+
scanComment() {
|
|
176
|
+
// //- or //- text
|
|
177
|
+
const text = this.currentLine.length > 3 ? this.currentLine.slice(3).trimStart() : '';
|
|
178
|
+
this.emit('COMMENT', text);
|
|
179
|
+
this.pos = this.currentLine.length;
|
|
180
|
+
}
|
|
181
|
+
scanPassthroughComment() {
|
|
182
|
+
// // or // text — capture everything after //
|
|
183
|
+
const text = this.currentLine.slice(2);
|
|
184
|
+
this.emit('PASSTHROUGH_COMMENT', text);
|
|
185
|
+
this.pos = this.currentLine.length;
|
|
186
|
+
}
|
|
187
|
+
scanImport() {
|
|
188
|
+
this.emit('IMPORT', this.currentLine);
|
|
189
|
+
this.pos = this.currentLine.length;
|
|
190
|
+
}
|
|
191
|
+
scanDoctype() {
|
|
192
|
+
const variant = this.currentLine.slice('doctype '.length);
|
|
193
|
+
this.emit('DOCTYPE', variant);
|
|
194
|
+
this.pos = this.currentLine.length;
|
|
195
|
+
}
|
|
196
|
+
scanBlock() {
|
|
197
|
+
this.emit('BLOCK', 'block');
|
|
198
|
+
const name = this.currentLine.slice('block '.length).trim();
|
|
199
|
+
this.emit('IDENTIFIER', name);
|
|
200
|
+
this.pos = this.currentLine.length;
|
|
201
|
+
}
|
|
202
|
+
scanInclude() {
|
|
203
|
+
this.emit('INCLUDE', 'include');
|
|
204
|
+
const path = this.currentLine.slice('include '.length).trim();
|
|
205
|
+
this.emit('PATH', path);
|
|
206
|
+
this.pos = this.currentLine.length;
|
|
207
|
+
}
|
|
208
|
+
// --- Declaration scanners ---
|
|
209
|
+
scanTemplate() {
|
|
210
|
+
this.pos = 'template '.length;
|
|
211
|
+
this.emit('TEMPLATE', 'template');
|
|
212
|
+
const name = this.scanIdentifier();
|
|
213
|
+
this.emit('IDENTIFIER', name);
|
|
214
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
215
|
+
const params = this.scanBalancedParens();
|
|
216
|
+
this.emit('PARAMS', params);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
scanMixin() {
|
|
220
|
+
this.pos = 'mixin '.length;
|
|
221
|
+
this.emit('MIXIN', 'mixin');
|
|
222
|
+
const name = this.scanIdentifier();
|
|
223
|
+
this.emit('IDENTIFIER', name);
|
|
224
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
225
|
+
const params = this.scanBalancedParens();
|
|
226
|
+
this.emit('PARAMS', params);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
scanExtends() {
|
|
230
|
+
this.pos = 'extends '.length;
|
|
231
|
+
this.emit('EXTENDS', 'extends');
|
|
232
|
+
const name = this.scanIdentifier();
|
|
233
|
+
this.emit('IDENTIFIER', name);
|
|
234
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
235
|
+
const params = this.scanBalancedParens();
|
|
236
|
+
this.emit('PARAMS', params);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// --- Control flow scanners ---
|
|
240
|
+
scanIf() {
|
|
241
|
+
this.pos = 'if '.length;
|
|
242
|
+
this.emit('IF', 'if');
|
|
243
|
+
const expr = this.currentLine.slice(this.pos);
|
|
244
|
+
this.emit('BUFFERED_CODE', expr);
|
|
245
|
+
this.pos = this.currentLine.length;
|
|
246
|
+
}
|
|
247
|
+
scanElseIf() {
|
|
248
|
+
this.pos = 'else if '.length;
|
|
249
|
+
this.emit('ELSE_IF', 'else if');
|
|
250
|
+
const expr = this.currentLine.slice(this.pos);
|
|
251
|
+
this.emit('BUFFERED_CODE', expr);
|
|
252
|
+
this.pos = this.currentLine.length;
|
|
253
|
+
}
|
|
254
|
+
scanFor() {
|
|
255
|
+
this.pos = 'for '.length;
|
|
256
|
+
this.emit('FOR', 'for');
|
|
257
|
+
this.skipSpaces();
|
|
258
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
259
|
+
const params = this.scanBalancedParens();
|
|
260
|
+
this.emit('PARAMS', params);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// --- Element scanner ---
|
|
264
|
+
scanElement() {
|
|
265
|
+
const ch = this.currentLine[this.pos];
|
|
266
|
+
// Tag name or implicit div
|
|
267
|
+
if (ch === '.' || ch === '#') {
|
|
268
|
+
this.emit('TAG', 'div');
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const tag = this.scanTagName();
|
|
272
|
+
this.emit('TAG', tag);
|
|
273
|
+
}
|
|
274
|
+
// Classes and IDs
|
|
275
|
+
this.scanClassesAndIds();
|
|
276
|
+
// Attributes
|
|
277
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
278
|
+
this.emit('ATTR_START', '(');
|
|
279
|
+
this.pos++; // skip '('
|
|
280
|
+
this.scanAttributes();
|
|
281
|
+
this.emit('ATTR_END', ')');
|
|
282
|
+
}
|
|
283
|
+
// Trailing content
|
|
284
|
+
this.scanTrailingContent();
|
|
285
|
+
}
|
|
286
|
+
scanTagName() {
|
|
287
|
+
const start = this.pos;
|
|
288
|
+
while (this.pos < this.currentLine.length) {
|
|
289
|
+
const ch = this.currentLine[this.pos];
|
|
290
|
+
if (/[a-zA-Z0-9-]/.test(ch)) {
|
|
291
|
+
this.pos++;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return this.currentLine.slice(start, this.pos);
|
|
298
|
+
}
|
|
299
|
+
scanClassesAndIds() {
|
|
300
|
+
while (this.pos < this.currentLine.length) {
|
|
301
|
+
const ch = this.currentLine[this.pos];
|
|
302
|
+
if (ch === '.') {
|
|
303
|
+
this.pos++; // skip dot
|
|
304
|
+
const start = this.pos;
|
|
305
|
+
while (this.pos < this.currentLine.length) {
|
|
306
|
+
const c = this.currentLine[this.pos];
|
|
307
|
+
if (c === ' ' || c === '(' || c === '#' || c === '.' || c === '=')
|
|
308
|
+
break;
|
|
309
|
+
this.pos++;
|
|
310
|
+
}
|
|
311
|
+
this.emit('CLASS', this.currentLine.slice(start, this.pos));
|
|
312
|
+
}
|
|
313
|
+
else if (ch === '#') {
|
|
314
|
+
this.pos++; // skip hash
|
|
315
|
+
const start = this.pos;
|
|
316
|
+
while (this.pos < this.currentLine.length) {
|
|
317
|
+
const c = this.currentLine[this.pos];
|
|
318
|
+
if (c === ' ' || c === '(' || c === '#' || c === '.' || c === '=')
|
|
319
|
+
break;
|
|
320
|
+
this.pos++;
|
|
321
|
+
}
|
|
322
|
+
this.emit('ID', this.currentLine.slice(start, this.pos));
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
scanAttributes() {
|
|
330
|
+
while (this.pos < this.currentLine.length) {
|
|
331
|
+
this.skipSpaces();
|
|
332
|
+
if (this.pos >= this.currentLine.length)
|
|
333
|
+
break;
|
|
334
|
+
if (this.currentLine[this.pos] === ')') {
|
|
335
|
+
this.pos++; // skip closing paren
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// Spread: ...identifier
|
|
339
|
+
if (this.currentLine[this.pos] === '.' &&
|
|
340
|
+
this.pos + 2 < this.currentLine.length &&
|
|
341
|
+
this.currentLine[this.pos + 1] === '.' &&
|
|
342
|
+
this.currentLine[this.pos + 2] === '.') {
|
|
343
|
+
this.pos += 3; // skip ...
|
|
344
|
+
const name = this.scanIdentifier();
|
|
345
|
+
this.emit('SPREAD', name);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
// Attribute name
|
|
349
|
+
const name = this.scanAttrName();
|
|
350
|
+
this.emit('ATTR_NAME', name);
|
|
351
|
+
// Check for = value
|
|
352
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '=') {
|
|
353
|
+
this.pos++; // skip =
|
|
354
|
+
const value = this.scanAttrValue();
|
|
355
|
+
this.emit('ATTR_VALUE', value);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
scanAttrName() {
|
|
360
|
+
const start = this.pos;
|
|
361
|
+
while (this.pos < this.currentLine.length) {
|
|
362
|
+
const ch = this.currentLine[this.pos];
|
|
363
|
+
if (/[a-zA-Z0-9_-]/.test(ch)) {
|
|
364
|
+
this.pos++;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return this.currentLine.slice(start, this.pos);
|
|
371
|
+
}
|
|
372
|
+
scanAttrValue() {
|
|
373
|
+
const ch = this.currentLine[this.pos];
|
|
374
|
+
// Quoted string
|
|
375
|
+
if (ch === '"' || ch === "'") {
|
|
376
|
+
return this.scanString(ch);
|
|
377
|
+
}
|
|
378
|
+
// Template literal
|
|
379
|
+
if (ch === '`') {
|
|
380
|
+
return this.scanTemplateLiteral();
|
|
381
|
+
}
|
|
382
|
+
// Bare expression — scan until whitespace, ), or end
|
|
383
|
+
const start = this.pos;
|
|
384
|
+
let depth = 0;
|
|
385
|
+
while (this.pos < this.currentLine.length) {
|
|
386
|
+
const c = this.currentLine[this.pos];
|
|
387
|
+
if (c === '(') {
|
|
388
|
+
depth++;
|
|
389
|
+
this.pos++;
|
|
390
|
+
}
|
|
391
|
+
else if (c === ')') {
|
|
392
|
+
if (depth === 0)
|
|
393
|
+
break;
|
|
394
|
+
depth--;
|
|
395
|
+
this.pos++;
|
|
396
|
+
}
|
|
397
|
+
else if (c === ' ' && depth === 0) {
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
this.pos++;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return this.currentLine.slice(start, this.pos);
|
|
405
|
+
}
|
|
406
|
+
scanTrailingContent() {
|
|
407
|
+
if (this.pos >= this.currentLine.length)
|
|
408
|
+
return;
|
|
409
|
+
const ch = this.currentLine[this.pos];
|
|
410
|
+
// Buffered code: = expression
|
|
411
|
+
if (ch === '=') {
|
|
412
|
+
this.pos++; // skip =
|
|
413
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === ' ') {
|
|
414
|
+
this.pos++; // skip space after =
|
|
415
|
+
}
|
|
416
|
+
const rest = this.currentLine.slice(this.pos);
|
|
417
|
+
if (rest.includes('#{')) {
|
|
418
|
+
this.scanTextWithInterpolation(rest);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
this.emit('BUFFERED_CODE', rest);
|
|
422
|
+
}
|
|
423
|
+
this.pos = this.currentLine.length;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
// Inline text: space then text
|
|
427
|
+
if (ch === ' ') {
|
|
428
|
+
this.pos++; // skip leading space
|
|
429
|
+
const rest = this.currentLine.slice(this.pos);
|
|
430
|
+
this.scanTextWithInterpolation(rest);
|
|
431
|
+
this.pos = this.currentLine.length;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// --- Mixin call scanner ---
|
|
436
|
+
scanMixinCall() {
|
|
437
|
+
this.pos = 1; // skip +
|
|
438
|
+
const name = this.scanIdentifier();
|
|
439
|
+
this.emit('MIXIN_CALL', name);
|
|
440
|
+
// Args in first set of parens
|
|
441
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
442
|
+
const args = this.scanBalancedParens();
|
|
443
|
+
this.emit('PARAMS', args);
|
|
444
|
+
}
|
|
445
|
+
// Extra attrs in second set of parens
|
|
446
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
447
|
+
this.emit('ATTR_START', '(');
|
|
448
|
+
this.pos++; // skip '('
|
|
449
|
+
this.scanAttributes();
|
|
450
|
+
this.emit('ATTR_END', ')');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// --- Pipe scanner ---
|
|
454
|
+
scanPipe() {
|
|
455
|
+
this.emit('PIPE', '|');
|
|
456
|
+
this.pos = 1; // skip |
|
|
457
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === ' ') {
|
|
458
|
+
this.pos++; // skip space after |
|
|
459
|
+
}
|
|
460
|
+
const rest = this.currentLine.slice(this.pos);
|
|
461
|
+
if (rest.length > 0) {
|
|
462
|
+
this.scanTextWithInterpolation(rest);
|
|
463
|
+
}
|
|
464
|
+
this.pos = this.currentLine.length;
|
|
465
|
+
}
|
|
466
|
+
// --- Text with interpolation ---
|
|
467
|
+
scanTextWithInterpolation(text) {
|
|
468
|
+
let i = 0;
|
|
469
|
+
let buf = '';
|
|
470
|
+
while (i < text.length) {
|
|
471
|
+
// Check for interpolation #{...}
|
|
472
|
+
if (text[i] === '#' && i + 1 < text.length && text[i + 1] === '{') {
|
|
473
|
+
// Emit accumulated text
|
|
474
|
+
if (buf.length > 0) {
|
|
475
|
+
this.emit('TEXT', buf);
|
|
476
|
+
buf = '';
|
|
477
|
+
}
|
|
478
|
+
i += 2; // skip #{
|
|
479
|
+
// Scan balanced braces to find closing }
|
|
480
|
+
let depth = 1;
|
|
481
|
+
const start = i;
|
|
482
|
+
while (i < text.length && depth > 0) {
|
|
483
|
+
const ch = text[i];
|
|
484
|
+
if (ch === '{') {
|
|
485
|
+
depth++;
|
|
486
|
+
}
|
|
487
|
+
else if (ch === '}') {
|
|
488
|
+
depth--;
|
|
489
|
+
if (depth === 0)
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
493
|
+
// Skip string contents
|
|
494
|
+
const quote = ch;
|
|
495
|
+
i++;
|
|
496
|
+
while (i < text.length) {
|
|
497
|
+
if (text[i] === '\\') {
|
|
498
|
+
i++; // skip escaped char
|
|
499
|
+
}
|
|
500
|
+
else if (text[i] === quote) {
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
i++;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
i++;
|
|
507
|
+
}
|
|
508
|
+
if (depth !== 0) {
|
|
509
|
+
throw new LexerError('Unterminated interpolation', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
510
|
+
}
|
|
511
|
+
this.emit('INTERPOLATION', text.slice(start, i));
|
|
512
|
+
i++; // skip closing }
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
buf += text[i];
|
|
516
|
+
i++;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Emit remaining text
|
|
520
|
+
if (buf.length > 0) {
|
|
521
|
+
this.emit('TEXT', buf);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// --- Balanced scanning helpers ---
|
|
525
|
+
scanBalancedParens() {
|
|
526
|
+
if (this.currentLine[this.pos] !== '(') {
|
|
527
|
+
throw new LexerError('Expected opening parenthesis', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
528
|
+
}
|
|
529
|
+
this.pos++; // skip opening (
|
|
530
|
+
let depth = 1;
|
|
531
|
+
const start = this.pos;
|
|
532
|
+
while (this.pos < this.currentLine.length && depth > 0) {
|
|
533
|
+
const ch = this.currentLine[this.pos];
|
|
534
|
+
if (ch === '(') {
|
|
535
|
+
depth++;
|
|
536
|
+
this.pos++;
|
|
537
|
+
}
|
|
538
|
+
else if (ch === ')') {
|
|
539
|
+
depth--;
|
|
540
|
+
if (depth === 0)
|
|
541
|
+
break;
|
|
542
|
+
this.pos++;
|
|
543
|
+
}
|
|
544
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
545
|
+
this.skipStringInPlace(ch);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
this.pos++;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (depth !== 0) {
|
|
552
|
+
throw new LexerError('Unterminated parentheses', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
553
|
+
}
|
|
554
|
+
const content = this.currentLine.slice(start, this.pos);
|
|
555
|
+
this.pos++; // skip closing )
|
|
556
|
+
return content.trim();
|
|
557
|
+
}
|
|
558
|
+
scanString(quote) {
|
|
559
|
+
const start = this.pos;
|
|
560
|
+
this.pos++; // skip opening quote
|
|
561
|
+
while (this.pos < this.currentLine.length) {
|
|
562
|
+
if (this.currentLine[this.pos] === '\\') {
|
|
563
|
+
this.pos += 2; // skip escaped char
|
|
564
|
+
}
|
|
565
|
+
else if (this.currentLine[this.pos] === quote) {
|
|
566
|
+
this.pos++; // skip closing quote
|
|
567
|
+
return this.currentLine.slice(start, this.pos);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
this.pos++;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
throw new LexerError('Unterminated string', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
574
|
+
}
|
|
575
|
+
scanTemplateLiteral() {
|
|
576
|
+
const start = this.pos;
|
|
577
|
+
this.pos++; // skip opening backtick
|
|
578
|
+
while (this.pos < this.currentLine.length) {
|
|
579
|
+
if (this.currentLine[this.pos] === '\\') {
|
|
580
|
+
this.pos += 2;
|
|
581
|
+
}
|
|
582
|
+
else if (this.currentLine[this.pos] === '`') {
|
|
583
|
+
this.pos++;
|
|
584
|
+
return this.currentLine.slice(start, this.pos);
|
|
585
|
+
}
|
|
586
|
+
else if (this.currentLine[this.pos] === '$' &&
|
|
587
|
+
this.pos + 1 < this.currentLine.length &&
|
|
588
|
+
this.currentLine[this.pos + 1] === '{') {
|
|
589
|
+
this.pos += 2; // skip ${
|
|
590
|
+
let depth = 1;
|
|
591
|
+
while (this.pos < this.currentLine.length && depth > 0) {
|
|
592
|
+
if (this.currentLine[this.pos] === '{') {
|
|
593
|
+
depth++;
|
|
594
|
+
}
|
|
595
|
+
else if (this.currentLine[this.pos] === '}') {
|
|
596
|
+
depth--;
|
|
597
|
+
}
|
|
598
|
+
else if (this.currentLine[this.pos] === '"' ||
|
|
599
|
+
this.currentLine[this.pos] === "'" ||
|
|
600
|
+
this.currentLine[this.pos] === '`') {
|
|
601
|
+
this.skipStringInPlace(this.currentLine[this.pos]);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
this.pos++;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
this.pos++;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
throw new LexerError('Unterminated template literal', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
612
|
+
}
|
|
613
|
+
/** Skip over a string in-place (used within balanced paren scanning) */
|
|
614
|
+
skipStringInPlace(quote) {
|
|
615
|
+
this.pos++; // skip opening quote
|
|
616
|
+
while (this.pos < this.currentLine.length) {
|
|
617
|
+
if (this.currentLine[this.pos] === '\\') {
|
|
618
|
+
this.pos += 2;
|
|
619
|
+
}
|
|
620
|
+
else if (this.currentLine[this.pos] === quote) {
|
|
621
|
+
this.pos++;
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
else if (quote === '`' &&
|
|
625
|
+
this.currentLine[this.pos] === '$' &&
|
|
626
|
+
this.pos + 1 < this.currentLine.length &&
|
|
627
|
+
this.currentLine[this.pos + 1] === '{') {
|
|
628
|
+
// Template literal ${...} - skip the nested expression
|
|
629
|
+
this.pos += 2;
|
|
630
|
+
let depth = 1;
|
|
631
|
+
while (this.pos < this.currentLine.length && depth > 0) {
|
|
632
|
+
if (this.currentLine[this.pos] === '{')
|
|
633
|
+
depth++;
|
|
634
|
+
else if (this.currentLine[this.pos] === '}')
|
|
635
|
+
depth--;
|
|
636
|
+
if (depth > 0)
|
|
637
|
+
this.pos++;
|
|
638
|
+
}
|
|
639
|
+
if (this.pos < this.currentLine.length)
|
|
640
|
+
this.pos++; // skip closing }
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
this.pos++;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// --- Utility methods ---
|
|
649
|
+
scanIdentifier() {
|
|
650
|
+
const start = this.pos;
|
|
651
|
+
while (this.pos < this.currentLine.length) {
|
|
652
|
+
const ch = this.currentLine[this.pos];
|
|
653
|
+
if (/[a-zA-Z0-9_]/.test(ch)) {
|
|
654
|
+
this.pos++;
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return this.currentLine.slice(start, this.pos);
|
|
661
|
+
}
|
|
662
|
+
skipSpaces() {
|
|
663
|
+
while (this.pos < this.currentLine.length && this.currentLine[this.pos] === ' ') {
|
|
664
|
+
this.pos++;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
emit(type, value) {
|
|
668
|
+
this.tokens.push({
|
|
669
|
+
type,
|
|
670
|
+
value,
|
|
671
|
+
line: this.lineIndex + 1,
|
|
672
|
+
col: this.indentWidth + this.pos + 1,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
export function lex(source) {
|
|
677
|
+
return new Lexer(source).lex();
|
|
678
|
+
}
|
|
679
|
+
//# sourceMappingURL=index.js.map
|