@chilts/weave 0.6.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 +53 -2
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +96 -11
- package/dist/codegen/index.d.ts +17 -3
- package/dist/codegen/index.js +194 -48
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/lexer/index.js +220 -91
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +11 -1
- package/package.json +7 -3
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,70 +227,63 @@ 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() {
|
|
@@ -301,8 +296,9 @@ class Lexer {
|
|
|
301
296
|
this.emit('TAG', 'div');
|
|
302
297
|
}
|
|
303
298
|
else {
|
|
299
|
+
const tagStart = this.pos;
|
|
304
300
|
const tag = this.scanTagName();
|
|
305
|
-
this.emit('TAG', tag);
|
|
301
|
+
this.emit('TAG', tag, tagStart);
|
|
306
302
|
}
|
|
307
303
|
// Classes and IDs
|
|
308
304
|
this.scanClassesAndIds();
|
|
@@ -311,7 +307,7 @@ class Lexer {
|
|
|
311
307
|
this.emit('ATTR_START', '(');
|
|
312
308
|
this.pos++; // skip '('
|
|
313
309
|
this.scanAttributes(elementIndent);
|
|
314
|
-
this.emit('ATTR_END', ')');
|
|
310
|
+
this.emit('ATTR_END', ')', this.pos - 1);
|
|
315
311
|
}
|
|
316
312
|
// Raw block: tag!
|
|
317
313
|
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '!') {
|
|
@@ -337,35 +333,58 @@ class Lexer {
|
|
|
337
333
|
return this.currentLine.slice(start, this.pos);
|
|
338
334
|
}
|
|
339
335
|
scanClassesAndIds() {
|
|
336
|
+
let prevClass = null;
|
|
340
337
|
while (this.pos < this.currentLine.length) {
|
|
341
338
|
const ch = this.currentLine[this.pos];
|
|
342
339
|
if (ch === '.') {
|
|
340
|
+
const dotPos = this.pos;
|
|
341
|
+
const dotCol = this.indentWidth + dotPos + 1;
|
|
343
342
|
this.pos++; // skip dot
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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);
|
|
350
352
|
}
|
|
351
|
-
|
|
353
|
+
prevClass = raw;
|
|
354
|
+
this.emit('CLASS', name, dotPos);
|
|
352
355
|
}
|
|
353
356
|
else if (ch === '#') {
|
|
357
|
+
const hashPos = this.pos;
|
|
354
358
|
this.pos++; // skip hash
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const c = this.currentLine[this.pos];
|
|
358
|
-
if (c === ' ' || c === '(' || c === '#' || c === '.' || c === '=' || c === '!')
|
|
359
|
-
break;
|
|
360
|
-
this.pos++;
|
|
361
|
-
}
|
|
362
|
-
this.emit('ID', this.currentLine.slice(start, this.pos));
|
|
359
|
+
prevClass = null;
|
|
360
|
+
this.emit('ID', this.scanShorthandName().name, hashPos);
|
|
363
361
|
}
|
|
364
362
|
else {
|
|
365
363
|
break;
|
|
366
364
|
}
|
|
367
365
|
}
|
|
368
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
|
+
}
|
|
369
388
|
// Scans an attribute list, with the cursor just past the opening '('.
|
|
370
389
|
// `ownerIndent` is the indent of the element or mixin call that owns the
|
|
371
390
|
// list, which bounds the multi-line form.
|
|
@@ -461,6 +480,7 @@ class Lexer {
|
|
|
461
480
|
// Scans a single `...spread`, `name` or `name=value`, with the cursor on
|
|
462
481
|
// its first character.
|
|
463
482
|
scanAttribute() {
|
|
483
|
+
const start = this.pos;
|
|
464
484
|
// Spread: ...identifier
|
|
465
485
|
if (this.currentLine[this.pos] === '.' &&
|
|
466
486
|
this.pos + 2 < this.currentLine.length &&
|
|
@@ -468,7 +488,7 @@ class Lexer {
|
|
|
468
488
|
this.currentLine[this.pos + 2] === '.') {
|
|
469
489
|
this.pos += 3; // skip ...
|
|
470
490
|
const name = this.scanIdentifier();
|
|
471
|
-
this.emit('SPREAD', name);
|
|
491
|
+
this.emit('SPREAD', name, start);
|
|
472
492
|
return;
|
|
473
493
|
}
|
|
474
494
|
// Attribute name
|
|
@@ -482,12 +502,13 @@ class Lexer {
|
|
|
482
502
|
throw new LexerError(`Unexpected '${this.currentLine[this.pos]}' in attribute list; ` +
|
|
483
503
|
`wrap complex attribute values in (parentheses) or \`backticks\``, this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
484
504
|
}
|
|
485
|
-
this.emit('ATTR_NAME', name);
|
|
505
|
+
this.emit('ATTR_NAME', name, start);
|
|
486
506
|
// Check for = value
|
|
487
507
|
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '=') {
|
|
488
508
|
this.pos++; // skip =
|
|
509
|
+
const valueStart = this.pos;
|
|
489
510
|
const value = this.scanAttrValue();
|
|
490
|
-
this.emit('ATTR_VALUE', value);
|
|
511
|
+
this.emit('ATTR_VALUE', value, valueStart);
|
|
491
512
|
}
|
|
492
513
|
}
|
|
493
514
|
scanAttrName() {
|
|
@@ -571,18 +592,16 @@ class Lexer {
|
|
|
571
592
|
const elementIndent = this.indentWidth;
|
|
572
593
|
this.pos = 1; // skip +
|
|
573
594
|
const name = this.scanIdentifier();
|
|
574
|
-
|
|
595
|
+
// Anchored on the '+', not where the name scan left the cursor.
|
|
596
|
+
this.emit('MIXIN_CALL', name, 0);
|
|
575
597
|
// Args in first set of parens
|
|
576
|
-
|
|
577
|
-
const args = this.scanBalancedParens();
|
|
578
|
-
this.emit('PARAMS', args);
|
|
579
|
-
}
|
|
598
|
+
this.scanParamsIfPresent();
|
|
580
599
|
// Extra attrs in second set of parens
|
|
581
600
|
if (this.pos < this.currentLine.length && this.currentLine[this.pos] === '(') {
|
|
582
601
|
this.emit('ATTR_START', '(');
|
|
583
602
|
this.pos++; // skip '('
|
|
584
603
|
this.scanAttributes(elementIndent);
|
|
585
|
-
this.emit('ATTR_END', ')');
|
|
604
|
+
this.emit('ATTR_END', ')', this.pos - 1);
|
|
586
605
|
}
|
|
587
606
|
}
|
|
588
607
|
// --- Pipe scanner ---
|
|
@@ -599,17 +618,21 @@ class Lexer {
|
|
|
599
618
|
this.pos = this.currentLine.length;
|
|
600
619
|
}
|
|
601
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.
|
|
602
623
|
scanTextWithInterpolation(text) {
|
|
603
624
|
let i = 0;
|
|
604
625
|
let buf = '';
|
|
626
|
+
let bufStart = 0;
|
|
605
627
|
while (i < text.length) {
|
|
606
628
|
// Check for interpolation #{...}
|
|
607
629
|
if (text[i] === '#' && i + 1 < text.length && text[i + 1] === '{') {
|
|
608
630
|
// Emit accumulated text
|
|
609
631
|
if (buf.length > 0) {
|
|
610
|
-
this.emit('TEXT', buf);
|
|
632
|
+
this.emit('TEXT', buf, this.pos + bufStart);
|
|
611
633
|
buf = '';
|
|
612
634
|
}
|
|
635
|
+
const interpStart = i;
|
|
613
636
|
i += 2; // skip #{
|
|
614
637
|
// Scan balanced braces to find closing }
|
|
615
638
|
let depth = 1;
|
|
@@ -641,10 +664,11 @@ class Lexer {
|
|
|
641
664
|
i++;
|
|
642
665
|
}
|
|
643
666
|
if (depth !== 0) {
|
|
644
|
-
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);
|
|
645
668
|
}
|
|
646
|
-
this.emit('INTERPOLATION', text.slice(start, i));
|
|
669
|
+
this.emit('INTERPOLATION', text.slice(start, i), this.pos + interpStart);
|
|
647
670
|
i++; // skip closing }
|
|
671
|
+
bufStart = i;
|
|
648
672
|
}
|
|
649
673
|
else {
|
|
650
674
|
buf += text[i];
|
|
@@ -653,42 +677,119 @@ class Lexer {
|
|
|
653
677
|
}
|
|
654
678
|
// Emit remaining text
|
|
655
679
|
if (buf.length > 0) {
|
|
656
|
-
this.emit('TEXT', buf);
|
|
680
|
+
this.emit('TEXT', buf, this.pos + bufStart);
|
|
657
681
|
}
|
|
658
682
|
}
|
|
659
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.
|
|
660
701
|
scanBalancedParens() {
|
|
661
702
|
if (this.currentLine[this.pos] !== '(') {
|
|
662
703
|
throw new LexerError('Expected opening parenthesis', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
663
704
|
}
|
|
705
|
+
const ownerIndent = this.indentWidth;
|
|
706
|
+
const openLine = this.lineIndex + 1;
|
|
707
|
+
const openCol = this.indentWidth + this.pos + 1;
|
|
664
708
|
this.pos++; // skip opening (
|
|
665
709
|
let depth = 1;
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
+
}
|
|
672
744
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
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();
|
|
678
751
|
}
|
|
679
|
-
|
|
680
|
-
|
|
752
|
+
if (pieces.length === 0) {
|
|
753
|
+
pieces.push(text.trimStart());
|
|
681
754
|
}
|
|
682
|
-
else {
|
|
683
|
-
|
|
755
|
+
else if (text !== '' || !isComment) {
|
|
756
|
+
// Lines holding nothing but a comment are dropped entirely.
|
|
757
|
+
pieces.push(text === '' ? '' : prefix + text);
|
|
684
758
|
}
|
|
759
|
+
if (depth === 0)
|
|
760
|
+
break;
|
|
761
|
+
this.nextParamsLine(ownerIndent, openLine, openCol);
|
|
762
|
+
prefix = ' '.repeat(this.indentWidth - ownerIndent);
|
|
685
763
|
}
|
|
686
|
-
if (depth !== 0) {
|
|
687
|
-
throw new LexerError('Unterminated parentheses', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
688
|
-
}
|
|
689
|
-
const content = this.currentLine.slice(start, this.pos);
|
|
690
764
|
this.pos++; // skip closing )
|
|
691
|
-
|
|
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
|
+
}
|
|
692
793
|
}
|
|
693
794
|
scanString(quote) {
|
|
694
795
|
const start = this.pos;
|
|
@@ -745,7 +846,10 @@ class Lexer {
|
|
|
745
846
|
}
|
|
746
847
|
throw new LexerError('Unterminated template literal', this.lineIndex + 1, this.indentWidth + this.pos + 1);
|
|
747
848
|
}
|
|
748
|
-
/**
|
|
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
|
+
*/
|
|
749
853
|
skipStringInPlace(quote) {
|
|
750
854
|
this.pos++; // skip opening quote
|
|
751
855
|
while (this.pos < this.currentLine.length) {
|
|
@@ -754,7 +858,7 @@ class Lexer {
|
|
|
754
858
|
}
|
|
755
859
|
else if (this.currentLine[this.pos] === quote) {
|
|
756
860
|
this.pos++;
|
|
757
|
-
return;
|
|
861
|
+
return true;
|
|
758
862
|
}
|
|
759
863
|
else if (quote === '`' &&
|
|
760
864
|
this.currentLine[this.pos] === '$' &&
|
|
@@ -764,21 +868,27 @@ class Lexer {
|
|
|
764
868
|
this.pos += 2;
|
|
765
869
|
let depth = 1;
|
|
766
870
|
while (this.pos < this.currentLine.length && depth > 0) {
|
|
767
|
-
if (this.currentLine[this.pos] === '{')
|
|
871
|
+
if (this.currentLine[this.pos] === '{') {
|
|
768
872
|
depth++;
|
|
769
|
-
|
|
873
|
+
}
|
|
874
|
+
else if (this.currentLine[this.pos] === '}') {
|
|
770
875
|
depth--;
|
|
771
|
-
|
|
772
|
-
|
|
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++;
|
|
773
884
|
}
|
|
774
|
-
|
|
775
|
-
this.pos++; // skip closing }
|
|
776
|
-
return;
|
|
885
|
+
// fall through to keep scanning for the closing backtick
|
|
777
886
|
}
|
|
778
887
|
else {
|
|
779
888
|
this.pos++;
|
|
780
889
|
}
|
|
781
890
|
}
|
|
891
|
+
return false;
|
|
782
892
|
}
|
|
783
893
|
// --- Utility methods ---
|
|
784
894
|
scanIdentifier() {
|
|
@@ -794,17 +904,36 @@ class Lexer {
|
|
|
794
904
|
}
|
|
795
905
|
return this.currentLine.slice(start, this.pos);
|
|
796
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
|
+
}
|
|
797
924
|
skipSpaces() {
|
|
798
925
|
while (this.pos < this.currentLine.length && this.currentLine[this.pos] === ' ') {
|
|
799
926
|
this.pos++;
|
|
800
927
|
}
|
|
801
928
|
}
|
|
802
|
-
|
|
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) {
|
|
803
932
|
this.tokens.push({
|
|
804
933
|
type,
|
|
805
934
|
value,
|
|
806
935
|
line: this.lineIndex + 1,
|
|
807
|
-
col: this.indentWidth +
|
|
936
|
+
col: this.indentWidth + start + 1,
|
|
808
937
|
});
|
|
809
938
|
}
|
|
810
939
|
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export declare class SafeHtml {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function escape(val: string): string;
|
|
11
11
|
export declare function raw(html: string): SafeHtml;
|
|
12
|
+
/**
|
|
13
|
+
* Merge shorthand classes (`.btn`) with an explicit `class=` value that is only
|
|
14
|
+
* known at render time. A value that means "absent" — undefined, null, a
|
|
15
|
+
* boolean, or "" — adds nothing, so the shorthand classes always survive.
|
|
16
|
+
*/
|
|
17
|
+
export declare function mergeClass(prefix: string, value: string | number | boolean | null | undefined): string;
|
|
12
18
|
export declare function doctype(): SafeHtml;
|
|
13
19
|
export declare function h(tag: string | symbol, attrs: Attrs, ...children: unknown[]): SafeHtml;
|
|
14
20
|
export {};
|
package/dist/runtime/index.js
CHANGED
|
@@ -28,6 +28,16 @@ export function escape(val) {
|
|
|
28
28
|
export function raw(html) {
|
|
29
29
|
return new SafeHtml(html);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Merge shorthand classes (`.btn`) with an explicit `class=` value that is only
|
|
33
|
+
* known at render time. A value that means "absent" — undefined, null, a
|
|
34
|
+
* boolean, or "" — adds nothing, so the shorthand classes always survive.
|
|
35
|
+
*/
|
|
36
|
+
export function mergeClass(prefix, value) {
|
|
37
|
+
if (value == null || typeof value === 'boolean' || value === '')
|
|
38
|
+
return prefix;
|
|
39
|
+
return `${prefix} ${value}`;
|
|
40
|
+
}
|
|
31
41
|
export function doctype() {
|
|
32
42
|
return new SafeHtml('<!DOCTYPE html>');
|
|
33
43
|
}
|
|
@@ -57,7 +67,7 @@ function renderChildren(children) {
|
|
|
57
67
|
function renderAttrs(attrs) {
|
|
58
68
|
let out = '';
|
|
59
69
|
for (const [key, val] of Object.entries(attrs)) {
|
|
60
|
-
if (val
|
|
70
|
+
if (val == null || val === false)
|
|
61
71
|
continue;
|
|
62
72
|
if (val === true) {
|
|
63
73
|
out += ` ${key}`;
|
package/package.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"name": "@chilts/weave",
|
|
3
3
|
"title": "Weave",
|
|
4
4
|
"description": "A Pug-inspired templating engine that compiles to TypeScript.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.7.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"bin": {
|
|
9
|
-
"weave": "
|
|
9
|
+
"weave": "bin/weave.js"
|
|
10
10
|
},
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"exports": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"dist/",
|
|
20
20
|
"bin/",
|
|
21
|
+
"CHANGELOG.md",
|
|
21
22
|
"!dist/**/*.map"
|
|
22
23
|
],
|
|
23
24
|
"directories": {
|
|
@@ -42,7 +43,10 @@
|
|
|
42
43
|
"homepage": "https://weave.chilts.org",
|
|
43
44
|
"repository": {
|
|
44
45
|
"type": "git",
|
|
45
|
-
"url": "
|
|
46
|
+
"url": "git+https://codeberg.org/chilts/weave.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://codeberg.org/chilts/weave/issues"
|
|
46
50
|
},
|
|
47
51
|
"author": "Andrew Chilton <andychilton@gmail.com>",
|
|
48
52
|
"keywords": [
|