@chilts/weave 0.5.0 → 0.7.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/CHANGELOG.md +178 -0
- package/ReadMe.md +145 -1
- package/dist/cli/index.d.ts +19 -1
- package/dist/cli/index.js +323 -14
- package/dist/codegen/index.d.ts +17 -3
- package/dist/codegen/index.js +208 -56
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/lexer/index.js +335 -108
- package/dist/parser/index.js +27 -3
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +11 -1
- package/package.json +9 -4
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/codegen/index.d.ts.map +0 -1
- package/dist/codegen/index.js.map +0 -1
- package/dist/codegen/sourcemap.d.ts.map +0 -1
- package/dist/codegen/sourcemap.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lexer/index.d.ts.map +0 -1
- package/dist/lexer/index.js.map +0 -1
- package/dist/parser/index.d.ts.map +0 -1
- package/dist/parser/index.js.map +0 -1
- package/dist/parser/types.d.ts.map +0 -1
- package/dist/parser/types.js.map +0 -1
- package/dist/runtime/index.d.ts.map +0 -1
- package/dist/runtime/index.js.map +0 -1
package/dist/lexer/index.js
CHANGED
|
@@ -54,7 +54,9 @@ class Lexer {
|
|
|
54
54
|
}
|
|
55
55
|
// Preserve indentation relative to the base
|
|
56
56
|
const text = rawLine.slice(this.rawBlockBaseIndent);
|
|
57
|
-
|
|
57
|
+
// The cursor still belongs to the previous line, so anchor
|
|
58
|
+
// explicitly on this line's first non-space character.
|
|
59
|
+
this.emit('RAW_TEXT', text, 0);
|
|
58
60
|
this.emit('NEWLINE', '');
|
|
59
61
|
continue;
|
|
60
62
|
}
|
|
@@ -225,81 +227,78 @@ class Lexer {
|
|
|
225
227
|
}
|
|
226
228
|
scanBlock() {
|
|
227
229
|
this.emit('BLOCK', 'block');
|
|
228
|
-
const
|
|
229
|
-
this.
|
|
230
|
+
const nameStart = this.skipSpacesFrom('block '.length);
|
|
231
|
+
const name = this.currentLine.slice(nameStart).trim();
|
|
232
|
+
this.emit('IDENTIFIER', name, nameStart);
|
|
230
233
|
this.pos = this.currentLine.length;
|
|
231
234
|
}
|
|
232
235
|
scanInclude() {
|
|
233
236
|
this.emit('INCLUDE', 'include');
|
|
234
|
-
const
|
|
235
|
-
this.
|
|
237
|
+
const pathStart = this.skipSpacesFrom('include '.length);
|
|
238
|
+
const path = this.currentLine.slice(pathStart).trim();
|
|
239
|
+
this.emit('PATH', path, pathStart);
|
|
236
240
|
this.pos = this.currentLine.length;
|
|
237
241
|
}
|
|
238
242
|
// --- Declaration scanners ---
|
|
239
243
|
scanTemplate() {
|
|
244
|
+
this.emit('TEMPLATE', 'template', 0);
|
|
240
245
|
this.pos = 'template '.length;
|
|
241
|
-
this.
|
|
246
|
+
const nameStart = this.pos;
|
|
242
247
|
const name = this.scanIdentifier();
|
|
243
|
-
this.emit('IDENTIFIER', name);
|
|
244
|
-
|
|
245
|
-
const params = this.scanBalancedParens();
|
|
246
|
-
this.emit('PARAMS', params);
|
|
247
|
-
}
|
|
248
|
+
this.emit('IDENTIFIER', name, nameStart);
|
|
249
|
+
this.scanParamsIfPresent();
|
|
248
250
|
}
|
|
249
251
|
scanMixin() {
|
|
252
|
+
this.emit('MIXIN', 'mixin', 0);
|
|
250
253
|
this.pos = 'mixin '.length;
|
|
251
|
-
this.
|
|
254
|
+
const nameStart = this.pos;
|
|
252
255
|
const name = this.scanIdentifier();
|
|
253
|
-
this.emit('IDENTIFIER', name);
|
|
254
|
-
|
|
255
|
-
const params = this.scanBalancedParens();
|
|
256
|
-
this.emit('PARAMS', params);
|
|
257
|
-
}
|
|
256
|
+
this.emit('IDENTIFIER', name, nameStart);
|
|
257
|
+
this.scanParamsIfPresent();
|
|
258
258
|
}
|
|
259
259
|
scanExtends() {
|
|
260
|
+
this.emit('EXTENDS', 'extends', 0);
|
|
260
261
|
this.pos = 'extends '.length;
|
|
261
|
-
this.
|
|
262
|
+
const nameStart = this.pos;
|
|
262
263
|
const name = this.scanIdentifier();
|
|
263
|
-
this.emit('IDENTIFIER', name);
|
|
264
|
-
|
|
265
|
-
const params = this.scanBalancedParens();
|
|
266
|
-
this.emit('PARAMS', params);
|
|
267
|
-
}
|
|
264
|
+
this.emit('IDENTIFIER', name, nameStart);
|
|
265
|
+
this.scanParamsIfPresent();
|
|
268
266
|
}
|
|
269
267
|
// --- Control flow scanners ---
|
|
270
268
|
scanIf() {
|
|
269
|
+
this.emit('IF', 'if', 0);
|
|
271
270
|
this.pos = 'if '.length;
|
|
272
|
-
this.emit('IF', 'if');
|
|
273
271
|
const expr = this.currentLine.slice(this.pos);
|
|
274
272
|
this.emit('BUFFERED_CODE', expr);
|
|
275
273
|
this.pos = this.currentLine.length;
|
|
276
274
|
}
|
|
277
275
|
scanElseIf() {
|
|
276
|
+
this.emit('ELSE_IF', 'else if', 0);
|
|
278
277
|
this.pos = 'else if '.length;
|
|
279
|
-
this.emit('ELSE_IF', 'else if');
|
|
280
278
|
const expr = this.currentLine.slice(this.pos);
|
|
281
279
|
this.emit('BUFFERED_CODE', expr);
|
|
282
280
|
this.pos = this.currentLine.length;
|
|
283
281
|
}
|
|
284
282
|
scanFor() {
|
|
283
|
+
this.emit('FOR', 'for', 0);
|
|
285
284
|
this.pos = 'for '.length;
|
|
286
|
-
this.emit('FOR', 'for');
|
|
287
285
|
this.skipSpaces();
|
|
288
|
-
|
|
289
|
-
const params = this.scanBalancedParens();
|
|
290
|
-
this.emit('PARAMS', params);
|
|
291
|
-
}
|
|
286
|
+
this.scanParamsIfPresent();
|
|
292
287
|
}
|
|
293
288
|
// --- Element scanner ---
|
|
294
289
|
scanElement() {
|
|
295
290
|
const ch = this.currentLine[this.pos];
|
|
291
|
+
// Captured up front: a multi-line attribute list moves the cursor onto
|
|
292
|
+
// later lines and updates this.indentWidth as it goes.
|
|
293
|
+
const elementIndent = this.indentWidth;
|
|
296
294
|
// Tag name or implicit div
|
|
297
295
|
if (ch === '.' || ch === '#') {
|
|
298
296
|
this.emit('TAG', 'div');
|
|
299
297
|
}
|
|
300
298
|
else {
|
|
299
|
+
const tagStart = this.pos;
|
|
301
300
|
const tag = this.scanTagName();
|
|
302
|
-
this.emit('TAG', tag);
|
|
301
|
+
this.emit('TAG', tag, tagStart);
|
|
303
302
|
}
|
|
304
303
|
// Classes and IDs
|
|
305
304
|
this.scanClassesAndIds();
|
|
@@ -307,13 +306,13 @@ class Lexer {
|
|
|
307
306
|
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
308
307
|
this.emit('ATTR_START', '(');
|
|
309
308
|
this.pos++; // skip '('
|
|
310
|
-
this.scanAttributes();
|
|
311
|
-
this.emit('ATTR_END', ')');
|
|
309
|
+
this.scanAttributes(elementIndent);
|
|
310
|
+
this.emit('ATTR_END', ')', this.pos - 1);
|
|
312
311
|
}
|
|
313
312
|
// Raw block: tag!
|
|
314
313
|
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '!') {
|
|
315
314
|
this.pos++; // skip !
|
|
316
|
-
this.rawBlockIndent =
|
|
315
|
+
this.rawBlockIndent = elementIndent;
|
|
317
316
|
this.rawBlockBaseIndent = null;
|
|
318
317
|
return;
|
|
319
318
|
}
|
|
@@ -334,36 +333,75 @@ class Lexer {
|
|
|
334
333
|
return this.currentLine.slice(start, this.pos);
|
|
335
334
|
}
|
|
336
335
|
scanClassesAndIds() {
|
|
336
|
+
let prevClass = null;
|
|
337
337
|
while (this.pos < this.currentLine.length) {
|
|
338
338
|
const ch = this.currentLine[this.pos];
|
|
339
339
|
if (ch === '.') {
|
|
340
|
+
const dotPos = this.pos;
|
|
341
|
+
const dotCol = this.indentWidth + dotPos + 1;
|
|
340
342
|
this.pos++; // skip dot
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
343
|
+
const { name, raw } = this.scanShorthandName();
|
|
344
|
+
// A dot is a class separator, so '.gap-1.5' silently became
|
|
345
|
+
// 'gap-1' plus a stray '5'. A class that is a bare number is
|
|
346
|
+
// almost certainly that mistake, so reject it and name the fix.
|
|
347
|
+
if (/^[0-9]+$/.test(name)) {
|
|
348
|
+
const example = prevClass === null ? `.gap-1\\.${name}` : `.${prevClass}\\.${name}`;
|
|
349
|
+
const split = prevClass === null ? '' : ` ('.${prevClass}.${name}' was split into '${prevClass}' and '${name}')`;
|
|
350
|
+
throw new LexerError(`Class '.${name}' is a bare number; a dot inside a class name is read as a separator${split}. ` +
|
|
351
|
+
`Escape the dot with a backslash, e.g. '${example}'`, this.lineIndex + 1, dotCol);
|
|
347
352
|
}
|
|
348
|
-
|
|
353
|
+
prevClass = raw;
|
|
354
|
+
this.emit('CLASS', name, dotPos);
|
|
349
355
|
}
|
|
350
356
|
else if (ch === '#') {
|
|
357
|
+
const hashPos = this.pos;
|
|
351
358
|
this.pos++; // skip hash
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const c = this.currentLine[this.pos];
|
|
355
|
-
if (c === ' ' || c === '(' || c === '#' || c === '.' || c === '=' || c === '!')
|
|
356
|
-
break;
|
|
357
|
-
this.pos++;
|
|
358
|
-
}
|
|
359
|
-
this.emit('ID', this.currentLine.slice(start, this.pos));
|
|
359
|
+
prevClass = null;
|
|
360
|
+
this.emit('ID', this.scanShorthandName().name, hashPos);
|
|
360
361
|
}
|
|
361
362
|
else {
|
|
362
363
|
break;
|
|
363
364
|
}
|
|
364
365
|
}
|
|
365
366
|
}
|
|
366
|
-
|
|
367
|
+
// Scans a class or id name after its '.' or '#'. A backslash-escaped dot
|
|
368
|
+
// ('\.') is a literal dot rather than the start of the next class, so
|
|
369
|
+
// Tailwind's 'gap-1\.5' is a single class 'gap-1.5'. Returns the unescaped
|
|
370
|
+
// name and the source text it came from.
|
|
371
|
+
scanShorthandName() {
|
|
372
|
+
const start = this.pos;
|
|
373
|
+
let name = '';
|
|
374
|
+
while (this.pos < this.currentLine.length) {
|
|
375
|
+
const c = this.currentLine[this.pos];
|
|
376
|
+
if (c === '\\' && this.currentLine[this.pos + 1] === '.') {
|
|
377
|
+
name += '.';
|
|
378
|
+
this.pos += 2;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (c === ' ' || c === '(' || c === '#' || c === '.' || c === '=' || c === '!')
|
|
382
|
+
break;
|
|
383
|
+
name += c;
|
|
384
|
+
this.pos++;
|
|
385
|
+
}
|
|
386
|
+
return { name, raw: this.currentLine.slice(start, this.pos) };
|
|
387
|
+
}
|
|
388
|
+
// Scans an attribute list, with the cursor just past the opening '('.
|
|
389
|
+
// `ownerIndent` is the indent of the element or mixin call that owns the
|
|
390
|
+
// list, which bounds the multi-line form.
|
|
391
|
+
//
|
|
392
|
+
// Two shapes are accepted:
|
|
393
|
+
// - inline: the whole list lives on one line and ends with ')'
|
|
394
|
+
// - multi-line: the '(' is the last non-space character on its line,
|
|
395
|
+
// one attribute per line follows, and a ')' starting its
|
|
396
|
+
// own line closes the list
|
|
397
|
+
scanAttributes(ownerIndent) {
|
|
398
|
+
const openLine = this.lineIndex + 1;
|
|
399
|
+
const openCol = this.indentWidth + this.pos; // the '(' we just stepped over
|
|
400
|
+
// A '(' with nothing after it opens a multi-line attribute list.
|
|
401
|
+
if (this.currentLine.slice(this.pos).trim() === '') {
|
|
402
|
+
this.scanAttributeBlock(ownerIndent, openLine, openCol);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
367
405
|
while (this.pos < this.currentLine.length) {
|
|
368
406
|
this.skipSpaces();
|
|
369
407
|
if (this.pos >= this.currentLine.length)
|
|
@@ -372,26 +410,106 @@ class Lexer {
|
|
|
372
410
|
this.pos++; // skip closing paren
|
|
373
411
|
return;
|
|
374
412
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
413
|
+
this.scanAttribute();
|
|
414
|
+
}
|
|
415
|
+
// Running off the end of the line without a ')' used to be accepted
|
|
416
|
+
// silently: the following lines were then lexed as child elements, so
|
|
417
|
+
// attribute names became tags and their values became visible text.
|
|
418
|
+
throw new LexerError("Unterminated attribute list; expected ')' before the end of the line. " +
|
|
419
|
+
'To split attributes over several lines, end the line with the ' +
|
|
420
|
+
"opening '(' and close with a ')' on its own line", openLine, openCol);
|
|
421
|
+
}
|
|
422
|
+
// Consumes the lines of a multi-line attribute list, up to and including
|
|
423
|
+
// the line whose first non-space character is the closing ')'. Those lines
|
|
424
|
+
// never reach the main loop, so they produce no INDENT/DEDENT/NEWLINE
|
|
425
|
+
// tokens and the resulting token stream is identical to the inline form.
|
|
426
|
+
scanAttributeBlock(ownerIndent, openLine, openCol) {
|
|
427
|
+
while (this.lineIndex + 1 < this.lines.length) {
|
|
428
|
+
this.lineIndex++;
|
|
429
|
+
const rawLine = this.lines[this.lineIndex];
|
|
430
|
+
// Blank lines may separate groups of attributes
|
|
431
|
+
if (rawLine.trim().length === 0)
|
|
383
432
|
continue;
|
|
433
|
+
this.indentWidth = this.measureIndent(rawLine);
|
|
434
|
+
this.currentLine = rawLine.slice(this.indentWidth);
|
|
435
|
+
this.pos = 0;
|
|
436
|
+
// A ')' starting the line closes the list; the rest of that line is
|
|
437
|
+
// trailing content for the owning element, exactly as if the whole
|
|
438
|
+
// list had been written inline.
|
|
439
|
+
if (this.currentLine[0] === ')') {
|
|
440
|
+
this.pos = 1;
|
|
441
|
+
return;
|
|
384
442
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
443
|
+
if (this.indentWidth <= ownerIndent) {
|
|
444
|
+
throw new LexerError(`Expected ')' to close the attribute list opened at line ${openLine}; ` +
|
|
445
|
+
'attributes in a multi-line list must be indented under their element', this.lineIndex + 1, this.indentWidth + 1);
|
|
446
|
+
}
|
|
447
|
+
this.skipSpaces();
|
|
448
|
+
this.scanAttribute();
|
|
449
|
+
// One attribute per line. This is stricter than the inline form on
|
|
450
|
+
// purpose: it keeps the door open to letting multi-token bare values
|
|
451
|
+
// (ternaries, concatenation) go unwrapped here later, which only
|
|
452
|
+
// works while the newline is the separator.
|
|
453
|
+
this.skipSpaces();
|
|
454
|
+
if (this.pos < this.currentLine.length) {
|
|
455
|
+
const rest = this.currentLine.slice(this.pos);
|
|
456
|
+
const ch = rest[0];
|
|
457
|
+
let message;
|
|
458
|
+
if (ch === ')') {
|
|
459
|
+
message =
|
|
460
|
+
"')' must start its own line to close the attribute list " +
|
|
461
|
+
`opened at line ${openLine}`;
|
|
462
|
+
}
|
|
463
|
+
else if (/^[a-zA-Z_]/.test(rest) || rest.startsWith('...')) {
|
|
464
|
+
message =
|
|
465
|
+
'Only one attribute is allowed per line in a multi-line ' +
|
|
466
|
+
`attribute list (opened at line ${openLine})`;
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
// Leftovers that can't start another attribute are almost
|
|
470
|
+
// always the tail of a multi-token bare value.
|
|
471
|
+
message =
|
|
472
|
+
`Unexpected '${ch}' in attribute list; ` +
|
|
473
|
+
'wrap complex attribute values in (parentheses) or `backticks`';
|
|
474
|
+
}
|
|
475
|
+
throw new LexerError(message, this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
393
476
|
}
|
|
394
477
|
}
|
|
478
|
+
throw new LexerError("Unterminated attribute list; expected a ')' on its own line", openLine, openCol);
|
|
479
|
+
}
|
|
480
|
+
// Scans a single `...spread`, `name` or `name=value`, with the cursor on
|
|
481
|
+
// its first character.
|
|
482
|
+
scanAttribute() {
|
|
483
|
+
const start = this.pos;
|
|
484
|
+
// Spread: ...identifier
|
|
485
|
+
if (this.currentLine[this.pos] === '.' &&
|
|
486
|
+
this.pos + 2 < this.currentLine.length &&
|
|
487
|
+
this.currentLine[this.pos + 1] === '.' &&
|
|
488
|
+
this.currentLine[this.pos + 2] === '.') {
|
|
489
|
+
this.pos += 3; // skip ...
|
|
490
|
+
const name = this.scanIdentifier();
|
|
491
|
+
this.emit('SPREAD', name, start);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// Attribute name
|
|
495
|
+
const name = this.scanAttrName();
|
|
496
|
+
if (name === '') {
|
|
497
|
+
// scanAttrName made no progress: the cursor is parked on a
|
|
498
|
+
// character that can't start an attribute name (e.g. the `?`,
|
|
499
|
+
// `:`, `+` or quote of a multi-token bare attribute value).
|
|
500
|
+
// Without this guard the caller's loop spins forever emitting
|
|
501
|
+
// empty ATTR_NAME tokens until the process aborts.
|
|
502
|
+
throw new LexerError(`Unexpected '${this.currentLine[this.pos]}' in attribute list; ` +
|
|
503
|
+
`wrap complex attribute values in (parentheses) or \`backticks\``, this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
504
|
+
}
|
|
505
|
+
this.emit('ATTR_NAME', name, start);
|
|
506
|
+
// Check for = value
|
|
507
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '=') {
|
|
508
|
+
this.pos++; // skip =
|
|
509
|
+
const valueStart = this.pos;
|
|
510
|
+
const value = this.scanAttrValue();
|
|
511
|
+
this.emit('ATTR_VALUE', value, valueStart);
|
|
512
|
+
}
|
|
395
513
|
}
|
|
396
514
|
scanAttrName() {
|
|
397
515
|
const start = this.pos;
|
|
@@ -471,20 +589,19 @@ class Lexer {
|
|
|
471
589
|
}
|
|
472
590
|
// --- Mixin call scanner ---
|
|
473
591
|
scanMixinCall() {
|
|
592
|
+
const elementIndent = this.indentWidth;
|
|
474
593
|
this.pos = 1; // skip +
|
|
475
594
|
const name = this.scanIdentifier();
|
|
476
|
-
|
|
595
|
+
// Anchored on the '+', not where the name scan left the cursor.
|
|
596
|
+
this.emit('MIXIN_CALL', name, 0);
|
|
477
597
|
// Args in first set of parens
|
|
478
|
-
|
|
479
|
-
const args = this.scanBalancedParens();
|
|
480
|
-
this.emit('PARAMS', args);
|
|
481
|
-
}
|
|
598
|
+
this.scanParamsIfPresent();
|
|
482
599
|
// Extra attrs in second set of parens
|
|
483
600
|
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
484
601
|
this.emit('ATTR_START', '(');
|
|
485
602
|
this.pos++; // skip '('
|
|
486
|
-
this.scanAttributes();
|
|
487
|
-
this.emit('ATTR_END', ')');
|
|
603
|
+
this.scanAttributes(elementIndent);
|
|
604
|
+
this.emit('ATTR_END', ')', this.pos - 1);
|
|
488
605
|
}
|
|
489
606
|
}
|
|
490
607
|
// --- Pipe scanner ---
|
|
@@ -501,17 +618,21 @@ class Lexer {
|
|
|
501
618
|
this.pos = this.currentLine.length;
|
|
502
619
|
}
|
|
503
620
|
// --- Text with interpolation ---
|
|
621
|
+
// `text` is the rest of the current line from the cursor, so an offset
|
|
622
|
+
// into it plus the cursor is that character's position on the line.
|
|
504
623
|
scanTextWithInterpolation(text) {
|
|
505
624
|
let i = 0;
|
|
506
625
|
let buf = '';
|
|
626
|
+
let bufStart = 0;
|
|
507
627
|
while (i < text.length) {
|
|
508
628
|
// Check for interpolation #{...}
|
|
509
629
|
if (text[i] === '#' && i + 1 < text.length && text[i + 1] === '{') {
|
|
510
630
|
// Emit accumulated text
|
|
511
631
|
if (buf.length > 0) {
|
|
512
|
-
this.emit('TEXT', buf);
|
|
632
|
+
this.emit('TEXT', buf, this.pos + bufStart);
|
|
513
633
|
buf = '';
|
|
514
634
|
}
|
|
635
|
+
const interpStart = i;
|
|
515
636
|
i += 2; // skip #{
|
|
516
637
|
// Scan balanced braces to find closing }
|
|
517
638
|
let depth = 1;
|
|
@@ -543,10 +664,11 @@ class Lexer {
|
|
|
543
664
|
i++;
|
|
544
665
|
}
|
|
545
666
|
if (depth !== 0) {
|
|
546
|
-
throw new LexerError('Unterminated interpolation', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
667
|
+
throw new LexerError('Unterminated interpolation', this.lineIndex + 1, this.indentWidth + this.pos + interpStart + 1);
|
|
547
668
|
}
|
|
548
|
-
this.emit('INTERPOLATION', text.slice(start, i));
|
|
669
|
+
this.emit('INTERPOLATION', text.slice(start, i), this.pos + interpStart);
|
|
549
670
|
i++; // skip closing }
|
|
671
|
+
bufStart = i;
|
|
550
672
|
}
|
|
551
673
|
else {
|
|
552
674
|
buf += text[i];
|
|
@@ -555,42 +677,119 @@ class Lexer {
|
|
|
555
677
|
}
|
|
556
678
|
// Emit remaining text
|
|
557
679
|
if (buf.length > 0) {
|
|
558
|
-
this.emit('TEXT', buf);
|
|
680
|
+
this.emit('TEXT', buf, this.pos + bufStart);
|
|
559
681
|
}
|
|
560
682
|
}
|
|
561
683
|
// --- Balanced scanning helpers ---
|
|
684
|
+
// Scans a '(...)' group whose contents are captured verbatim (params, for
|
|
685
|
+
// headers, mixin call args), with the cursor on the '('. Returns the text
|
|
686
|
+
// between the parens.
|
|
687
|
+
//
|
|
688
|
+
// The group may span lines: it simply stays open until its parens balance.
|
|
689
|
+
// Unlike a multi-line attribute list there is no required shape, so the
|
|
690
|
+
// way TypeScript is usually wrapped works as written:
|
|
691
|
+
//
|
|
692
|
+
// template dashboard({
|
|
693
|
+
// title,
|
|
694
|
+
// }: DashboardProps)
|
|
695
|
+
//
|
|
696
|
+
// Continuation lines are consumed here, so they never reach the main loop
|
|
697
|
+
// and produce no INDENT/DEDENT/NEWLINE tokens. They keep their indentation
|
|
698
|
+
// relative to the line that opened the group. `//` comments are dropped:
|
|
699
|
+
// the captured text is re-emitted inside a single expression, where a
|
|
700
|
+
// comment would swallow whatever the compiler appends after it.
|
|
562
701
|
scanBalancedParens() {
|
|
563
702
|
if (this.currentLine[this.pos] !== '(') {
|
|
564
703
|
throw new LexerError('Expected opening parenthesis', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
565
704
|
}
|
|
705
|
+
const ownerIndent = this.indentWidth;
|
|
706
|
+
const openLine = this.lineIndex + 1;
|
|
707
|
+
const openCol = this.indentWidth + this.pos + 1;
|
|
566
708
|
this.pos++; // skip opening (
|
|
567
709
|
let depth = 1;
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
710
|
+
const pieces = [];
|
|
711
|
+
let prefix = ''; // this line's indentation relative to the owner
|
|
712
|
+
for (;;) {
|
|
713
|
+
const start = this.pos;
|
|
714
|
+
let end = -1; // where this line's content stops: the closing ')' or a comment
|
|
715
|
+
while (this.pos < this.currentLine.length) {
|
|
716
|
+
const ch = this.currentLine[this.pos];
|
|
717
|
+
if (ch === '(') {
|
|
718
|
+
depth++;
|
|
719
|
+
this.pos++;
|
|
720
|
+
}
|
|
721
|
+
else if (ch === ')') {
|
|
722
|
+
depth--;
|
|
723
|
+
if (depth === 0) {
|
|
724
|
+
end = this.pos;
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
this.pos++;
|
|
728
|
+
}
|
|
729
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
730
|
+
const quotePos = this.pos;
|
|
731
|
+
if (!this.skipStringInPlace(ch)) {
|
|
732
|
+
// A string has to close on its own line; letting it run
|
|
733
|
+
// on would throw the paren count off for every line after.
|
|
734
|
+
throw new LexerError('Unterminated string', this.lineIndex + 1, this.indentWidth + quotePos + 1);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
else if (ch === '/' && this.currentLine[this.pos + 1] === '/') {
|
|
738
|
+
end = this.pos;
|
|
739
|
+
this.pos = this.currentLine.length;
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
this.pos++;
|
|
743
|
+
}
|
|
574
744
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
this.pos++;
|
|
745
|
+
const isComment = end !== -1 && depth > 0;
|
|
746
|
+
const text = this.currentLine.slice(start, end === -1 ? this.currentLine.length : end).trimEnd();
|
|
747
|
+
if (depth === 0 && pieces.length === 0) {
|
|
748
|
+
// Closed on the line it opened on: the common, single-line case.
|
|
749
|
+
this.pos++; // skip closing )
|
|
750
|
+
return text.trim();
|
|
580
751
|
}
|
|
581
|
-
|
|
582
|
-
|
|
752
|
+
if (pieces.length === 0) {
|
|
753
|
+
pieces.push(text.trimStart());
|
|
583
754
|
}
|
|
584
|
-
else {
|
|
585
|
-
|
|
755
|
+
else if (text !== '' || !isComment) {
|
|
756
|
+
// Lines holding nothing but a comment are dropped entirely.
|
|
757
|
+
pieces.push(text === '' ? '' : prefix + text);
|
|
586
758
|
}
|
|
759
|
+
if (depth === 0)
|
|
760
|
+
break;
|
|
761
|
+
this.nextParamsLine(ownerIndent, openLine, openCol);
|
|
762
|
+
prefix = ' '.repeat(this.indentWidth - ownerIndent);
|
|
587
763
|
}
|
|
588
|
-
if (depth !== 0) {
|
|
589
|
-
throw new LexerError('Unterminated parentheses', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
590
|
-
}
|
|
591
|
-
const content = this.currentLine.slice(start, this.pos);
|
|
592
764
|
this.pos++; // skip closing )
|
|
593
|
-
|
|
765
|
+
const content = pieces.join('\n');
|
|
766
|
+
return content.trim() === '' ? '' : content;
|
|
767
|
+
}
|
|
768
|
+
// Moves onto the next non-blank line of a multi-line paren group. Lines in
|
|
769
|
+
// the group must be indented under its owner; the one exception is the
|
|
770
|
+
// line that closes it (e.g. '}: Props)'), which may sit level with the
|
|
771
|
+
// owner as long as it starts with a closing bracket. Anything else at or
|
|
772
|
+
// left of the owner means the ')' was forgotten, so stop there rather
|
|
773
|
+
// than swallowing the rest of the file.
|
|
774
|
+
nextParamsLine(ownerIndent, openLine, openCol) {
|
|
775
|
+
for (;;) {
|
|
776
|
+
if (this.lineIndex + 1 >= this.lines.length) {
|
|
777
|
+
throw new LexerError('Unterminated parentheses', openLine, openCol);
|
|
778
|
+
}
|
|
779
|
+
this.lineIndex++;
|
|
780
|
+
const rawLine = this.lines[this.lineIndex];
|
|
781
|
+
if (rawLine.trim().length === 0)
|
|
782
|
+
continue;
|
|
783
|
+
this.indentWidth = this.measureIndent(rawLine);
|
|
784
|
+
this.currentLine = rawLine.slice(this.indentWidth);
|
|
785
|
+
this.pos = 0;
|
|
786
|
+
if (this.indentWidth < ownerIndent ||
|
|
787
|
+
(this.indentWidth === ownerIndent && !/^[)}\]]/.test(this.currentLine))) {
|
|
788
|
+
throw new LexerError(`Expected ')' to close the parentheses opened at line ${openLine}; ` +
|
|
789
|
+
'continuation lines must be indented under the line that opens them', this.lineIndex + 1, this.indentWidth + 1);
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
594
793
|
}
|
|
595
794
|
scanString(quote) {
|
|
596
795
|
const start = this.pos;
|
|
@@ -647,7 +846,10 @@ class Lexer {
|
|
|
647
846
|
}
|
|
648
847
|
throw new LexerError('Unterminated template literal', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
649
848
|
}
|
|
650
|
-
/**
|
|
849
|
+
/**
|
|
850
|
+
* Skip over a string in-place (used within balanced paren scanning).
|
|
851
|
+
* Returns false when the line ends before the string closes.
|
|
852
|
+
*/
|
|
651
853
|
skipStringInPlace(quote) {
|
|
652
854
|
this.pos++; // skip opening quote
|
|
653
855
|
while (this.pos < this.currentLine.length) {
|
|
@@ -656,7 +858,7 @@ class Lexer {
|
|
|
656
858
|
}
|
|
657
859
|
else if (this.currentLine[this.pos] === quote) {
|
|
658
860
|
this.pos++;
|
|
659
|
-
return;
|
|
861
|
+
return true;
|
|
660
862
|
}
|
|
661
863
|
else if (quote === '`' &&
|
|
662
864
|
this.currentLine[this.pos] === '$' &&
|
|
@@ -666,21 +868,27 @@ class Lexer {
|
|
|
666
868
|
this.pos += 2;
|
|
667
869
|
let depth = 1;
|
|
668
870
|
while (this.pos < this.currentLine.length && depth > 0) {
|
|
669
|
-
if (this.currentLine[this.pos] === '{')
|
|
871
|
+
if (this.currentLine[this.pos] === '{') {
|
|
670
872
|
depth++;
|
|
671
|
-
|
|
873
|
+
}
|
|
874
|
+
else if (this.currentLine[this.pos] === '}') {
|
|
672
875
|
depth--;
|
|
673
|
-
|
|
674
|
-
|
|
876
|
+
}
|
|
877
|
+
else if (this.currentLine[this.pos] === '"' ||
|
|
878
|
+
this.currentLine[this.pos] === "'" ||
|
|
879
|
+
this.currentLine[this.pos] === '`') {
|
|
880
|
+
this.skipStringInPlace(this.currentLine[this.pos]);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
this.pos++;
|
|
675
884
|
}
|
|
676
|
-
|
|
677
|
-
this.pos++; // skip closing }
|
|
678
|
-
return;
|
|
885
|
+
// fall through to keep scanning for the closing backtick
|
|
679
886
|
}
|
|
680
887
|
else {
|
|
681
888
|
this.pos++;
|
|
682
889
|
}
|
|
683
890
|
}
|
|
891
|
+
return false;
|
|
684
892
|
}
|
|
685
893
|
// --- Utility methods ---
|
|
686
894
|
scanIdentifier() {
|
|
@@ -696,17 +904,36 @@ class Lexer {
|
|
|
696
904
|
}
|
|
697
905
|
return this.currentLine.slice(start, this.pos);
|
|
698
906
|
}
|
|
907
|
+
// Scans a '(...)' group at the cursor, if there is one, as a PARAMS token
|
|
908
|
+
// anchored on the '('. The group may run onto later lines, so the anchor
|
|
909
|
+
// is taken before scanning moves the cursor.
|
|
910
|
+
scanParamsIfPresent() {
|
|
911
|
+
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
912
|
+
const line = this.lineIndex + 1;
|
|
913
|
+
const col = this.indentWidth + this.pos + 1;
|
|
914
|
+
const params = this.scanBalancedParens();
|
|
915
|
+
this.tokens.push({ type: 'PARAMS', value: params, line, col });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// Returns the first non-space position at or after `pos` on the current line.
|
|
919
|
+
skipSpacesFrom(pos) {
|
|
920
|
+
while (pos < this.currentLine.length && this.currentLine[pos] === ' ')
|
|
921
|
+
pos++;
|
|
922
|
+
return pos;
|
|
923
|
+
}
|
|
699
924
|
skipSpaces() {
|
|
700
925
|
while (this.pos < this.currentLine.length && this.currentLine[this.pos] === ' ') {
|
|
701
926
|
this.pos++;
|
|
702
927
|
}
|
|
703
928
|
}
|
|
704
|
-
|
|
929
|
+
// `start` is the token's position on the current line; it defaults to the
|
|
930
|
+
// cursor, which for scanned tokens is already past the token itself.
|
|
931
|
+
emit(type, value, start = this.pos) {
|
|
705
932
|
this.tokens.push({
|
|
706
933
|
type,
|
|
707
934
|
value,
|
|
708
935
|
line: this.lineIndex + 1,
|
|
709
|
-
col: this.indentWidth +
|
|
936
|
+
col: this.indentWidth + start + 1,
|
|
710
937
|
});
|
|
711
938
|
}
|
|
712
939
|
}
|