@chilts/weave 0.7.0 → 0.8.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 +126 -1
- package/ReadMe.md +208 -6
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.js +133 -0
- package/dist/cli/index.d.ts +26 -4
- package/dist/cli/index.js +95 -26
- package/dist/codegen/index.js +269 -217
- package/dist/codegen/sourcemap.d.ts +49 -1
- package/dist/codegen/sourcemap.js +154 -2
- package/dist/diagnostics/index.d.ts +25 -0
- package/dist/diagnostics/index.js +68 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/lexer/index.d.ts +5 -1
- package/dist/lexer/index.js +129 -10
- package/dist/parser/index.d.ts +1 -1
- package/dist/parser/index.js +74 -9
- package/dist/parser/types.d.ts +13 -0
- package/dist/runtime/index.d.ts +40 -2
- package/dist/runtime/index.js +59 -4
- package/package.json +17 -3
package/dist/parser/index.js
CHANGED
|
@@ -59,6 +59,11 @@ class Parser {
|
|
|
59
59
|
loc(tok) {
|
|
60
60
|
return { line: tok.line, col: tok.col };
|
|
61
61
|
}
|
|
62
|
+
// A captured value starts where its token does, unless the lexer recorded
|
|
63
|
+
// its lines (a params group, whose token is anchored on the '(').
|
|
64
|
+
valueLoc(tok) {
|
|
65
|
+
return tok.starts ?? [this.loc(tok)];
|
|
66
|
+
}
|
|
62
67
|
// --- Core parsing ---
|
|
63
68
|
parseDocument() {
|
|
64
69
|
const startTok = this.peek();
|
|
@@ -94,6 +99,7 @@ class Parser {
|
|
|
94
99
|
case 'INTERPOLATION':
|
|
95
100
|
return this.parseText();
|
|
96
101
|
case 'IF':
|
|
102
|
+
case 'UNLESS':
|
|
97
103
|
return this.parseConditional();
|
|
98
104
|
case 'FOR':
|
|
99
105
|
return this.parseIteration();
|
|
@@ -107,11 +113,13 @@ class Parser {
|
|
|
107
113
|
return this.parseExtends();
|
|
108
114
|
case 'INCLUDE':
|
|
109
115
|
return this.parseInclude();
|
|
110
|
-
// Standalone else if / else are errors — they must follow an if
|
|
116
|
+
// Standalone else if / else are errors — they must follow an if (or a for)
|
|
111
117
|
case 'ELSE_IF':
|
|
112
118
|
case 'ELSE': {
|
|
113
119
|
const tok = this.peek();
|
|
114
|
-
throw new ParserError(
|
|
120
|
+
throw new ParserError(type === 'ELSE'
|
|
121
|
+
? 'Unexpected else without preceding if or for'
|
|
122
|
+
: 'Unexpected else if without preceding if', tok.line, tok.col);
|
|
115
123
|
}
|
|
116
124
|
default: {
|
|
117
125
|
const tok = this.peek();
|
|
@@ -158,10 +166,15 @@ class Parser {
|
|
|
158
166
|
}
|
|
159
167
|
// --- Control flow parsers ---
|
|
160
168
|
parseConditional() {
|
|
161
|
-
const startTok = this.
|
|
169
|
+
const startTok = this.advance(); // IF or UNLESS
|
|
162
170
|
const conditionTok = this.expect('BUFFERED_CODE');
|
|
163
171
|
this.skip('NEWLINE');
|
|
164
172
|
const consequent = this.parseBlock();
|
|
173
|
+
// 'unless' is a guard only: an else after it reads as a double negative.
|
|
174
|
+
if (startTok.type === 'UNLESS' && (this.peekType() === 'ELSE' || this.peekType() === 'ELSE_IF')) {
|
|
175
|
+
const tok = this.peek();
|
|
176
|
+
throw new ParserError(`Unexpected ${tok.value} after unless — 'unless' takes no else; write 'if' with the branches swapped`, tok.line, tok.col);
|
|
177
|
+
}
|
|
165
178
|
const alternates = [];
|
|
166
179
|
while (this.peekType() === 'ELSE_IF') {
|
|
167
180
|
this.advance(); // consume ELSE_IF
|
|
@@ -169,6 +182,7 @@ class Parser {
|
|
|
169
182
|
this.skip('NEWLINE');
|
|
170
183
|
alternates.push({
|
|
171
184
|
condition: altCondTok.value,
|
|
185
|
+
conditionLoc: this.valueLoc(altCondTok),
|
|
172
186
|
body: this.parseBlock(),
|
|
173
187
|
});
|
|
174
188
|
}
|
|
@@ -181,6 +195,8 @@ class Parser {
|
|
|
181
195
|
return {
|
|
182
196
|
type: 'Conditional',
|
|
183
197
|
condition: conditionTok.value,
|
|
198
|
+
conditionLoc: this.valueLoc(conditionTok),
|
|
199
|
+
...(startTok.type === 'UNLESS' && { negated: true }),
|
|
184
200
|
consequent,
|
|
185
201
|
alternates,
|
|
186
202
|
fallback,
|
|
@@ -192,10 +208,22 @@ class Parser {
|
|
|
192
208
|
const paramsTok = this.expect('PARAMS');
|
|
193
209
|
this.skip('NEWLINE');
|
|
194
210
|
const body = this.parseBlock();
|
|
211
|
+
if (this.peekType() === 'ELSE_IF') {
|
|
212
|
+
const tok = this.peek();
|
|
213
|
+
throw new ParserError('Unexpected else if after for — only a plain else (rendered when the collection is empty) may follow a for', tok.line, tok.col);
|
|
214
|
+
}
|
|
215
|
+
let fallback = null;
|
|
216
|
+
if (this.peekType() === 'ELSE') {
|
|
217
|
+
this.advance(); // consume ELSE
|
|
218
|
+
this.skip('NEWLINE');
|
|
219
|
+
fallback = this.parseBlock();
|
|
220
|
+
}
|
|
195
221
|
return {
|
|
196
222
|
type: 'Iteration',
|
|
197
223
|
params: paramsTok.value,
|
|
224
|
+
paramsLoc: this.valueLoc(paramsTok),
|
|
198
225
|
body,
|
|
226
|
+
fallback,
|
|
199
227
|
loc: this.loc(startTok),
|
|
200
228
|
};
|
|
201
229
|
}
|
|
@@ -210,6 +238,7 @@ class Parser {
|
|
|
210
238
|
type: 'Template',
|
|
211
239
|
name: nameTok.value,
|
|
212
240
|
params: paramsTok.value,
|
|
241
|
+
paramsLoc: this.valueLoc(paramsTok),
|
|
213
242
|
body,
|
|
214
243
|
loc: this.loc(startTok),
|
|
215
244
|
};
|
|
@@ -225,6 +254,7 @@ class Parser {
|
|
|
225
254
|
type: 'MixinDefinition',
|
|
226
255
|
name: nameTok.value,
|
|
227
256
|
params: paramsTok.value,
|
|
257
|
+
paramsLoc: this.valueLoc(paramsTok),
|
|
228
258
|
body,
|
|
229
259
|
loc: this.loc(startTok),
|
|
230
260
|
};
|
|
@@ -232,8 +262,11 @@ class Parser {
|
|
|
232
262
|
parseMixinCall() {
|
|
233
263
|
const startTok = this.expect('MIXIN_CALL');
|
|
234
264
|
let args = '';
|
|
265
|
+
let argsLoc;
|
|
235
266
|
if (this.peekType() === 'PARAMS') {
|
|
236
|
-
|
|
267
|
+
const argsTok = this.advance();
|
|
268
|
+
args = argsTok.value;
|
|
269
|
+
argsLoc = this.valueLoc(argsTok);
|
|
237
270
|
}
|
|
238
271
|
const attrs = [];
|
|
239
272
|
if (this.peekType() === 'ATTR_START') {
|
|
@@ -241,12 +274,38 @@ class Parser {
|
|
|
241
274
|
attrs.push(...this.parseAttributes());
|
|
242
275
|
this.expect('ATTR_END');
|
|
243
276
|
}
|
|
277
|
+
// One line of call-site content on the call's own line, as for an
|
|
278
|
+
// element: '+btn("x") Save' or '+btn("x")= label'.
|
|
279
|
+
let inline = null;
|
|
280
|
+
if (this.peekType() === 'BUFFERED_CODE') {
|
|
281
|
+
const codeTok = this.advance();
|
|
282
|
+
inline = {
|
|
283
|
+
type: 'BufferedCode',
|
|
284
|
+
expression: codeTok.value,
|
|
285
|
+
expressionLoc: this.valueLoc(codeTok),
|
|
286
|
+
loc: this.loc(codeTok),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
else if (this.peekType() === 'TEXT' || this.peekType() === 'INTERPOLATION') {
|
|
290
|
+
inline = this.parseTextSegments();
|
|
291
|
+
}
|
|
244
292
|
this.skip('NEWLINE');
|
|
245
|
-
|
|
293
|
+
// The content is a single callback, so it comes from one place or the
|
|
294
|
+
// other. An element can mix the two because its children are a list.
|
|
295
|
+
let block = this.parseBlock();
|
|
296
|
+
if (inline !== null) {
|
|
297
|
+
const content = block.find(n => n.type !== 'Comment' && n.type !== 'PassthroughComment');
|
|
298
|
+
if (content) {
|
|
299
|
+
throw new ParserError(`Mixin call '+${startTok.value}' has inline content, so it can't also have indented content; ` +
|
|
300
|
+
'move the inline content onto its own indented line', content.loc.line, content.loc.col);
|
|
301
|
+
}
|
|
302
|
+
block = [inline, ...block];
|
|
303
|
+
}
|
|
246
304
|
return {
|
|
247
305
|
type: 'MixinCall',
|
|
248
306
|
name: startTok.value,
|
|
249
307
|
args,
|
|
308
|
+
...(argsLoc ? { argsLoc } : {}),
|
|
250
309
|
attrs,
|
|
251
310
|
block,
|
|
252
311
|
loc: this.loc(startTok),
|
|
@@ -286,6 +345,7 @@ class Parser {
|
|
|
286
345
|
type: 'Extends',
|
|
287
346
|
target: targetTok.value,
|
|
288
347
|
args: argsTok.value,
|
|
348
|
+
argsLoc: this.valueLoc(argsTok),
|
|
289
349
|
blocks,
|
|
290
350
|
loc: this.loc(startTok),
|
|
291
351
|
};
|
|
@@ -324,6 +384,7 @@ class Parser {
|
|
|
324
384
|
children.push({
|
|
325
385
|
type: 'BufferedCode',
|
|
326
386
|
expression: codeTok.value,
|
|
387
|
+
expressionLoc: this.valueLoc(codeTok),
|
|
327
388
|
loc: this.loc(codeTok),
|
|
328
389
|
});
|
|
329
390
|
}
|
|
@@ -392,8 +453,8 @@ class Parser {
|
|
|
392
453
|
}
|
|
393
454
|
throw new ParserError(`Attribute '${name}' has no value.` + hint, nameTok.line, nameTok.col);
|
|
394
455
|
}
|
|
395
|
-
const
|
|
396
|
-
attrs.push({ name: nameTok.value, value, spread: false });
|
|
456
|
+
const valueTok = this.advance();
|
|
457
|
+
attrs.push({ name: nameTok.value, value: valueTok.value, valueLoc: this.valueLoc(valueTok), spread: false });
|
|
397
458
|
}
|
|
398
459
|
else {
|
|
399
460
|
const tok = this.peek();
|
|
@@ -412,11 +473,15 @@ class Parser {
|
|
|
412
473
|
segments.push({ type: 'text', value: tok.value });
|
|
413
474
|
}
|
|
414
475
|
else {
|
|
415
|
-
segments.push({ type: 'interpolation', expression: tok.value });
|
|
476
|
+
segments.push({ type: 'interpolation', expression: tok.value, loc: this.interpolationLoc(tok) });
|
|
416
477
|
}
|
|
417
478
|
}
|
|
418
479
|
return { type: 'Text', segments, loc: this.loc(startTok) };
|
|
419
480
|
}
|
|
481
|
+
// An INTERPOLATION token is anchored on its '#{'.
|
|
482
|
+
interpolationLoc(tok) {
|
|
483
|
+
return { line: tok.line, col: tok.col + 2 };
|
|
484
|
+
}
|
|
420
485
|
parsePipe() {
|
|
421
486
|
const startTok = this.expect('PIPE');
|
|
422
487
|
const segments = [];
|
|
@@ -426,7 +491,7 @@ class Parser {
|
|
|
426
491
|
segments.push({ type: 'text', value: tok.value });
|
|
427
492
|
}
|
|
428
493
|
else {
|
|
429
|
-
segments.push({ type: 'interpolation', expression: tok.value });
|
|
494
|
+
segments.push({ type: 'interpolation', expression: tok.value, loc: this.interpolationLoc(tok) });
|
|
430
495
|
}
|
|
431
496
|
}
|
|
432
497
|
this.skip('NEWLINE');
|
package/dist/parser/types.d.ts
CHANGED
|
@@ -2,20 +2,24 @@ export interface SourceLocation {
|
|
|
2
2
|
line: number;
|
|
3
3
|
col: number;
|
|
4
4
|
}
|
|
5
|
+
export type ValueLoc = SourceLocation[];
|
|
5
6
|
export type TextSegment = {
|
|
6
7
|
type: 'text';
|
|
7
8
|
value: string;
|
|
8
9
|
} | {
|
|
9
10
|
type: 'interpolation';
|
|
10
11
|
expression: string;
|
|
12
|
+
loc?: SourceLocation;
|
|
11
13
|
};
|
|
12
14
|
export interface AttributeNode {
|
|
13
15
|
name: string;
|
|
14
16
|
value: string | null;
|
|
17
|
+
valueLoc?: ValueLoc;
|
|
15
18
|
spread: boolean;
|
|
16
19
|
}
|
|
17
20
|
export interface ConditionalBranch {
|
|
18
21
|
condition: string;
|
|
22
|
+
conditionLoc?: ValueLoc;
|
|
19
23
|
body: AstNode[];
|
|
20
24
|
}
|
|
21
25
|
export interface DocumentNode {
|
|
@@ -27,6 +31,7 @@ export interface TemplateNode {
|
|
|
27
31
|
type: 'Template';
|
|
28
32
|
name: string;
|
|
29
33
|
params: string;
|
|
34
|
+
paramsLoc?: ValueLoc;
|
|
30
35
|
body: AstNode[];
|
|
31
36
|
loc: SourceLocation;
|
|
32
37
|
}
|
|
@@ -34,6 +39,7 @@ export interface MixinDefinitionNode {
|
|
|
34
39
|
type: 'MixinDefinition';
|
|
35
40
|
name: string;
|
|
36
41
|
params: string;
|
|
42
|
+
paramsLoc?: ValueLoc;
|
|
37
43
|
body: AstNode[];
|
|
38
44
|
loc: SourceLocation;
|
|
39
45
|
}
|
|
@@ -41,6 +47,7 @@ export interface MixinCallNode {
|
|
|
41
47
|
type: 'MixinCall';
|
|
42
48
|
name: string;
|
|
43
49
|
args: string;
|
|
50
|
+
argsLoc?: ValueLoc;
|
|
44
51
|
attrs: AttributeNode[];
|
|
45
52
|
block: AstNode[];
|
|
46
53
|
loc: SourceLocation;
|
|
@@ -64,11 +71,14 @@ export interface TextNode {
|
|
|
64
71
|
export interface BufferedCodeNode {
|
|
65
72
|
type: 'BufferedCode';
|
|
66
73
|
expression: string;
|
|
74
|
+
expressionLoc?: ValueLoc;
|
|
67
75
|
loc: SourceLocation;
|
|
68
76
|
}
|
|
69
77
|
export interface ConditionalNode {
|
|
70
78
|
type: 'Conditional';
|
|
71
79
|
condition: string;
|
|
80
|
+
conditionLoc?: ValueLoc;
|
|
81
|
+
negated?: boolean;
|
|
72
82
|
consequent: AstNode[];
|
|
73
83
|
alternates: ConditionalBranch[];
|
|
74
84
|
fallback: AstNode[] | null;
|
|
@@ -77,7 +87,9 @@ export interface ConditionalNode {
|
|
|
77
87
|
export interface IterationNode {
|
|
78
88
|
type: 'Iteration';
|
|
79
89
|
params: string;
|
|
90
|
+
paramsLoc?: ValueLoc;
|
|
80
91
|
body: AstNode[];
|
|
92
|
+
fallback: AstNode[] | null;
|
|
81
93
|
loc: SourceLocation;
|
|
82
94
|
}
|
|
83
95
|
export interface BlockNode {
|
|
@@ -90,6 +102,7 @@ export interface ExtendsNode {
|
|
|
90
102
|
type: 'Extends';
|
|
91
103
|
target: string;
|
|
92
104
|
args: string;
|
|
105
|
+
argsLoc?: ValueLoc;
|
|
93
106
|
blocks: BlockNode[];
|
|
94
107
|
loc: SourceLocation;
|
|
95
108
|
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type AttrValue = string | number | boolean | null | undefined;
|
|
2
|
+
/**
|
|
3
|
+
* A `class=` value: a string, an array of class values (nested, falsy entries
|
|
4
|
+
* dropped), or an object whose keys are kept when their value is truthy —
|
|
5
|
+
* `class=['btn', { active: isActive }]`.
|
|
6
|
+
*/
|
|
7
|
+
export type ClassValue = AttrValue | readonly ClassValue[] | {
|
|
8
|
+
readonly [name: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A `style=` value: a string, or an object of properties whose `null`,
|
|
12
|
+
* `undefined`, `false` and `""` values are dropped. camelCase keys become
|
|
13
|
+
* kebab-case (`backgroundColor` → `background-color`); `--custom` properties
|
|
14
|
+
* are kept as written.
|
|
15
|
+
*/
|
|
16
|
+
export type StyleValue = string | null | undefined | false | StyleObject;
|
|
17
|
+
type StyleObject = {
|
|
18
|
+
readonly [property: string]: string | number | null | undefined | false;
|
|
19
|
+
};
|
|
20
|
+
export type Attrs = {
|
|
21
|
+
class?: ClassValue;
|
|
22
|
+
style?: StyleValue;
|
|
23
|
+
[name: string]: AttrValue | ClassValue | StyleValue;
|
|
24
|
+
};
|
|
2
25
|
export declare const Fragment: unique symbol;
|
|
3
26
|
declare const RAW: unique symbol;
|
|
4
27
|
export declare class SafeHtml {
|
|
@@ -13,9 +36,24 @@ export declare function raw(html: string): SafeHtml;
|
|
|
13
36
|
* Merge shorthand classes (`.btn`) with an explicit `class=` value that is only
|
|
14
37
|
* known at render time. A value that means "absent" — undefined, null, a
|
|
15
38
|
* boolean, or "" — adds nothing, so the shorthand classes always survive.
|
|
39
|
+
* Arrays and objects are flattened as for `class=` itself.
|
|
16
40
|
*/
|
|
17
|
-
export declare function mergeClass(prefix: string, value:
|
|
41
|
+
export declare function mergeClass(prefix: string, value: ClassValue): string;
|
|
18
42
|
export declare function doctype(): SafeHtml;
|
|
43
|
+
/**
|
|
44
|
+
* Tag for text with `#{}` interpolations, which compiles to
|
|
45
|
+
* interpolate`Hi ${name}!`. A plain template literal would stringify each
|
|
46
|
+
* value before the runtime saw it, so `raw()` and mixin output were escaped
|
|
47
|
+
* and `undefined` printed as "undefined". Each value renders exactly as an
|
|
48
|
+
* element child does instead, and the literal text is escaped.
|
|
49
|
+
*/
|
|
50
|
+
export declare function interpolate(strings: TemplateStringsArray, ...values: unknown[]): SafeHtml;
|
|
51
|
+
/**
|
|
52
|
+
* Render `fn` for each item of any iterable — an array, Set, Map, generator or
|
|
53
|
+
* iterator — or `otherwise()` when it yields nothing. This is what `for` and
|
|
54
|
+
* `for ... else` compile to. The iterable is consumed exactly once.
|
|
55
|
+
*/
|
|
56
|
+
export declare function each<T>(items: Iterable<T>, fn: (item: T) => unknown, otherwise?: () => unknown): SafeHtml;
|
|
19
57
|
export declare function h(tag: string | symbol, attrs: Attrs, ...children: unknown[]): SafeHtml;
|
|
20
58
|
export {};
|
|
21
59
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/runtime/index.js
CHANGED
|
@@ -28,15 +28,40 @@ export function escape(val) {
|
|
|
28
28
|
export function raw(html) {
|
|
29
29
|
return new SafeHtml(html);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Flatten a class value to a space-separated list. A value that means
|
|
33
|
+
* "absent" — undefined, null, a boolean, or "" — yields "".
|
|
34
|
+
*/
|
|
35
|
+
function classList(value) {
|
|
36
|
+
if (value == null || typeof value === 'boolean')
|
|
37
|
+
return '';
|
|
38
|
+
if (typeof value === 'string')
|
|
39
|
+
return value;
|
|
40
|
+
if (typeof value === 'number')
|
|
41
|
+
return String(value);
|
|
42
|
+
if (Array.isArray(value))
|
|
43
|
+
return value.map(classList).filter(c => c !== '').join(' ');
|
|
44
|
+
return Object.keys(value).filter(name => value[name]).join(' ');
|
|
45
|
+
}
|
|
46
|
+
function styleString(value) {
|
|
47
|
+
const decls = [];
|
|
48
|
+
for (const [prop, val] of Object.entries(value)) {
|
|
49
|
+
if (val == null || val === false || val === '')
|
|
50
|
+
continue;
|
|
51
|
+
const name = prop.startsWith('--') ? prop : prop.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`);
|
|
52
|
+
decls.push(`${name}:${val}`);
|
|
53
|
+
}
|
|
54
|
+
return decls.join(';');
|
|
55
|
+
}
|
|
31
56
|
/**
|
|
32
57
|
* Merge shorthand classes (`.btn`) with an explicit `class=` value that is only
|
|
33
58
|
* known at render time. A value that means "absent" — undefined, null, a
|
|
34
59
|
* boolean, or "" — adds nothing, so the shorthand classes always survive.
|
|
60
|
+
* Arrays and objects are flattened as for `class=` itself.
|
|
35
61
|
*/
|
|
36
62
|
export function mergeClass(prefix, value) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return `${prefix} ${value}`;
|
|
63
|
+
const classes = classList(value);
|
|
64
|
+
return classes === '' ? prefix : `${prefix} ${classes}`;
|
|
40
65
|
}
|
|
41
66
|
export function doctype() {
|
|
42
67
|
return new SafeHtml('<!DOCTYPE html>');
|
|
@@ -64,9 +89,39 @@ function renderChildren(children) {
|
|
|
64
89
|
}
|
|
65
90
|
return out;
|
|
66
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Tag for text with `#{}` interpolations, which compiles to
|
|
94
|
+
* interpolate`Hi ${name}!`. A plain template literal would stringify each
|
|
95
|
+
* value before the runtime saw it, so `raw()` and mixin output were escaped
|
|
96
|
+
* and `undefined` printed as "undefined". Each value renders exactly as an
|
|
97
|
+
* element child does instead, and the literal text is escaped.
|
|
98
|
+
*/
|
|
99
|
+
export function interpolate(strings, ...values) {
|
|
100
|
+
let out = escape(strings[0]);
|
|
101
|
+
for (let i = 0; i < values.length; i++) {
|
|
102
|
+
out += renderChildren([values[i]]) + escape(strings[i + 1]);
|
|
103
|
+
}
|
|
104
|
+
return new SafeHtml(out);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Render `fn` for each item of any iterable — an array, Set, Map, generator or
|
|
108
|
+
* iterator — or `otherwise()` when it yields nothing. This is what `for` and
|
|
109
|
+
* `for ... else` compile to. The iterable is consumed exactly once.
|
|
110
|
+
*/
|
|
111
|
+
export function each(items, fn, otherwise) {
|
|
112
|
+
const rendered = Array.from(items, item => fn(item));
|
|
113
|
+
if (rendered.length === 0 && otherwise)
|
|
114
|
+
return new SafeHtml(renderChildren([otherwise()]));
|
|
115
|
+
return new SafeHtml(renderChildren(rendered));
|
|
116
|
+
}
|
|
67
117
|
function renderAttrs(attrs) {
|
|
68
118
|
let out = '';
|
|
69
|
-
for (
|
|
119
|
+
for (let [key, val] of Object.entries(attrs)) {
|
|
120
|
+
if (val !== null && typeof val === 'object' && (key === 'class' || key === 'style')) {
|
|
121
|
+
val = key === 'class' ? classList(val) : styleString(val);
|
|
122
|
+
if (val === '')
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
70
125
|
if (val == null || val === false)
|
|
71
126
|
continue;
|
|
72
127
|
if (val === true) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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.8.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"bin": {
|
|
@@ -27,17 +27,31 @@
|
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsc",
|
|
30
|
-
"test": "vitest run",
|
|
30
|
+
"test": "npm run lint && npm run typecheck && vitest run",
|
|
31
31
|
"test:watch": "vitest",
|
|
32
|
-
"lint": "
|
|
32
|
+
"lint": "eslint .",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
33
34
|
"prepublishOnly": "npm run build"
|
|
34
35
|
},
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=20"
|
|
37
38
|
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"typescript": ">=5"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"typescript": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
38
47
|
"devDependencies": {
|
|
48
|
+
"@eslint/js": "^10.0.1",
|
|
49
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
39
50
|
"@types/node": "^22.0.0",
|
|
51
|
+
"eslint": "^10.13.0",
|
|
52
|
+
"globals": "^17.13.0",
|
|
40
53
|
"typescript": "^5.9.3",
|
|
54
|
+
"typescript-eslint": "^8.72.0",
|
|
41
55
|
"vitest": "^4.0.0"
|
|
42
56
|
},
|
|
43
57
|
"homepage": "https://weave.chilts.org",
|