@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/codegen/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { lex } from '../lexer/index.js';
|
|
2
2
|
import { parse } from '../parser/index.js';
|
|
3
|
-
import { chunk, joinChunks, offsetMappings, lineCount, buildSourceMap } from './sourcemap.js';
|
|
3
|
+
import { chunk, concat, joinChunks, mark, offsetMappings, lineCount, buildSourceMap, sliceCaptured, verbatim, } from './sourcemap.js';
|
|
4
4
|
export class CodegenError extends Error {
|
|
5
5
|
line;
|
|
6
6
|
col;
|
|
@@ -20,16 +20,22 @@ function capitalize(s) {
|
|
|
20
20
|
// on its own line too, so the output keeps the shape the source was written in.
|
|
21
21
|
// `ownLine` is false for an extra that opens a multi-line literal of its own.
|
|
22
22
|
function appendArg(args, extra, ownLine = true) {
|
|
23
|
-
if (args === '')
|
|
24
|
-
return extra;
|
|
25
|
-
const body = args.trimEnd();
|
|
26
|
-
const tail = args.slice(body.length);
|
|
23
|
+
if (args.code === '')
|
|
24
|
+
return concat(extra);
|
|
25
|
+
const body = args.code.trimEnd();
|
|
26
|
+
const tail = args.code.slice(body.length);
|
|
27
|
+
// Mappings past the body would sit in the trailing space, which moves.
|
|
28
|
+
const bodyLines = lineCount(body);
|
|
29
|
+
const lastLine = body.slice(body.lastIndexOf('\n') + 1);
|
|
30
|
+
const head = {
|
|
31
|
+
code: body,
|
|
32
|
+
mappings: args.mappings.filter(m => m.genLine < bodyLines || (m.genLine === bodyLines && m.genCol < lastLine.length)),
|
|
33
|
+
};
|
|
27
34
|
if (ownLine && tail.includes('\n')) {
|
|
28
|
-
const lastLine = body.slice(body.lastIndexOf('\n') + 1);
|
|
29
35
|
const indent = /^ */.exec(lastLine)[0];
|
|
30
|
-
return
|
|
36
|
+
return concat(head, body.endsWith(',') ? '' : ',', `\n${indent}`, extra, `,${tail}`);
|
|
31
37
|
}
|
|
32
|
-
return
|
|
38
|
+
return concat(head, body.endsWith(',') ? ' ' : ', ', extra);
|
|
33
39
|
}
|
|
34
40
|
// Indents the continuation lines of a multi-line captured expression to sit
|
|
35
41
|
// under the output line it starts on. Captured text never holds a string that
|
|
@@ -37,43 +43,90 @@ function appendArg(args, extra, ownLine = true) {
|
|
|
37
43
|
function reindent(text, prefix) {
|
|
38
44
|
return prefix === '' ? text : text.replace(/\n(?!\n|$)/g, `\n${prefix}`);
|
|
39
45
|
}
|
|
46
|
+
// `reindent` for a chunk: mappings on an indented line move right with it.
|
|
47
|
+
function reindentChunk(c, prefix) {
|
|
48
|
+
if (prefix === '')
|
|
49
|
+
return c;
|
|
50
|
+
const lines = c.code.split('\n');
|
|
51
|
+
const shifted = (line) => line > 0 && lines[line] !== '';
|
|
52
|
+
return {
|
|
53
|
+
code: reindent(c.code, prefix),
|
|
54
|
+
mappings: c.mappings.map(m => shifted(m.genLine) ? { ...m, genCol: m.genCol + prefix.length } : m),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Wraps generated code around child chunks, mapping the wrapper — at the start
|
|
58
|
+
// and after each child — to the node, so only the children's own text maps
|
|
59
|
+
// elsewhere.
|
|
60
|
+
function within(loc, ...parts) {
|
|
61
|
+
return concat(mark(loc), ...parts.flatMap((p) => (typeof p === 'string' ? [p] : [p, mark(loc)])));
|
|
62
|
+
}
|
|
40
63
|
function camelToKebab(name) {
|
|
41
64
|
return name.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
42
65
|
}
|
|
43
|
-
|
|
66
|
+
// Every `block` in `nodes`, in source order. Mixin definitions are not
|
|
67
|
+
// descended into: their blocks belong to the mixin, not the caller.
|
|
68
|
+
function collectBlocks(nodes) {
|
|
69
|
+
const found = [];
|
|
44
70
|
function walk(n) {
|
|
45
|
-
if (n.type === 'Block')
|
|
46
|
-
|
|
71
|
+
if (n.type === 'Block') {
|
|
72
|
+
found.push(n);
|
|
73
|
+
n.children.forEach(walk);
|
|
74
|
+
}
|
|
47
75
|
if (n.type === 'Element')
|
|
48
|
-
|
|
76
|
+
n.children.forEach(walk);
|
|
77
|
+
if (n.type === 'MixinCall')
|
|
78
|
+
n.block.forEach(walk);
|
|
49
79
|
if (n.type === 'Conditional') {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
n.consequent.forEach(walk);
|
|
81
|
+
for (const alt of n.alternates)
|
|
82
|
+
alt.body.forEach(walk);
|
|
83
|
+
n.fallback?.forEach(walk);
|
|
84
|
+
}
|
|
85
|
+
if (n.type === 'Iteration') {
|
|
86
|
+
n.body.forEach(walk);
|
|
87
|
+
n.fallback?.forEach(walk);
|
|
58
88
|
}
|
|
59
|
-
if (n.type === 'Iteration')
|
|
60
|
-
return n.body.some(walk);
|
|
61
89
|
if (n.type === 'Extends') {
|
|
62
90
|
// The entries in `blocks` are fills (the slots this template fills in
|
|
63
91
|
// its parent), not slots of its own. A mid-layer template defines its
|
|
64
92
|
// own slots by nesting `block` nodes inside that fill content — those
|
|
65
93
|
// are what require a `blocks` parameter on this function.
|
|
66
|
-
|
|
94
|
+
for (const fill of n.blocks)
|
|
95
|
+
fill.children.forEach(walk);
|
|
67
96
|
}
|
|
68
|
-
return false;
|
|
69
97
|
}
|
|
70
|
-
|
|
98
|
+
nodes.forEach(walk);
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
/** The named slots a template defines, first of each name, in source order. */
|
|
102
|
+
function templateSlots(node) {
|
|
103
|
+
const slots = new Map();
|
|
104
|
+
for (const b of collectBlocks(node.body)) {
|
|
105
|
+
if (b.name !== '' && !slots.has(b.name))
|
|
106
|
+
slots.set(b.name, b);
|
|
107
|
+
}
|
|
108
|
+
return [...slots.values()];
|
|
109
|
+
}
|
|
110
|
+
/** True when a mixin renders its call-site content with a bare `block`. */
|
|
111
|
+
function mixinHasBlock(node) {
|
|
112
|
+
return collectBlocks(node.body).some(b => b.name === '');
|
|
113
|
+
}
|
|
114
|
+
// The `<Name>Blocks` interface for a template's slots. Each slot is optional
|
|
115
|
+
// (an unfilled slot renders nothing, or its default) and typed like a mixin's
|
|
116
|
+
// call-site content. Each member maps back to the `block` that defines it.
|
|
117
|
+
function blocksInterface(node, slots) {
|
|
118
|
+
const members = slots.map(b => ` ${b.name}?: () => unknown`);
|
|
119
|
+
const code = `export interface ${capitalize(node.name)}Blocks {\n${members.join('\n')}\n}`;
|
|
120
|
+
const mappings = [mapping(node.loc), ...slots.map((b, i) => mapping(b.loc, i + 1))];
|
|
121
|
+
return { code, mappings };
|
|
71
122
|
}
|
|
72
123
|
function mapping(loc, genLine = 0) {
|
|
73
124
|
return { genLine, genCol: 0, srcLine: loc.line - 1, srcCol: loc.col - 1 };
|
|
74
125
|
}
|
|
75
126
|
class CodeGenerator {
|
|
76
127
|
formatted;
|
|
128
|
+
// Set while generating a mixin body, where `block` means call-site content.
|
|
129
|
+
inMixin = false;
|
|
77
130
|
constructor(formatted = false) {
|
|
78
131
|
this.formatted = formatted;
|
|
79
132
|
}
|
|
@@ -86,23 +139,19 @@ class CodeGenerator {
|
|
|
86
139
|
* Non-comment expressions get trailing commas only if more non-comment expressions follow.
|
|
87
140
|
*/
|
|
88
141
|
joinWithCommentChunks(chunks, indent) {
|
|
89
|
-
const
|
|
90
|
-
const lines = [];
|
|
91
|
-
let currentLine = 0;
|
|
142
|
+
const parts = [];
|
|
92
143
|
for (let i = 0; i < chunks.length; i++) {
|
|
93
144
|
const c = chunks[i];
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
else {
|
|
145
|
+
if (i > 0)
|
|
146
|
+
parts.push('\n');
|
|
147
|
+
parts.push(indent, c);
|
|
148
|
+
if (!c.code.startsWith('//')) {
|
|
99
149
|
const hasMore = chunks.slice(i + 1).some(e => !e.code.startsWith('//'));
|
|
100
|
-
|
|
150
|
+
if (hasMore)
|
|
151
|
+
parts.push(',');
|
|
101
152
|
}
|
|
102
|
-
allMappings.push(...offsetMappings(c.mappings, currentLine));
|
|
103
|
-
currentLine += lineCount(c.code) + 1;
|
|
104
153
|
}
|
|
105
|
-
return
|
|
154
|
+
return concat(...parts);
|
|
106
155
|
}
|
|
107
156
|
genNode(node, indent = 0) {
|
|
108
157
|
switch (node.type) {
|
|
@@ -149,55 +198,53 @@ class CodeGenerator {
|
|
|
149
198
|
genConditional(node, indent) {
|
|
150
199
|
const bi = this.formatted ? this.ind(indent + 1) : ' ';
|
|
151
200
|
const consequentChunk = this.genFragment(node.consequent, indent + 1);
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
201
|
+
const condition = verbatim({ text: node.condition, loc: node.conditionLoc });
|
|
202
|
+
const parts = [
|
|
203
|
+
node.negated ? concat('!(', condition, ')') : condition,
|
|
204
|
+
`\n${bi}? `, consequentChunk,
|
|
156
205
|
];
|
|
157
206
|
for (const alt of node.alternates) {
|
|
158
207
|
const altBodyChunk = this.genFragment(alt.body, indent + 1);
|
|
159
|
-
|
|
160
|
-
code += `\n${bi}: ${alt.condition}\n${bi}? ${altBodyChunk.code}`;
|
|
161
|
-
mappings.push(...offsetMappings(altBodyChunk.mappings, prevLines + 2));
|
|
208
|
+
parts.push(`\n${bi}: `, verbatim({ text: alt.condition, loc: alt.conditionLoc }), `\n${bi}? `, altBodyChunk);
|
|
162
209
|
}
|
|
163
210
|
if (node.fallback !== null) {
|
|
164
|
-
|
|
165
|
-
const prevLines = lineCount(code);
|
|
166
|
-
code += `\n${bi}: ${fallbackChunk.code}`;
|
|
167
|
-
mappings.push(...offsetMappings(fallbackChunk.mappings, prevLines + 1));
|
|
211
|
+
parts.push(`\n${bi}: `, this.genFragment(node.fallback, indent + 1));
|
|
168
212
|
}
|
|
169
213
|
else {
|
|
170
|
-
|
|
214
|
+
parts.push(`\n${bi}: null`);
|
|
171
215
|
}
|
|
172
|
-
return
|
|
216
|
+
return concat(mark(node.loc), ...parts);
|
|
173
217
|
}
|
|
174
218
|
// --- Iteration generation ---
|
|
175
219
|
genIteration(node, indent) {
|
|
176
|
-
const { variable, iterable } = this.parseForParams(node.params, node.loc);
|
|
220
|
+
const { variable, iterable } = this.parseForParams({ text: node.params, loc: node.paramsLoc }, node.loc);
|
|
177
221
|
const bodyChunk = this.genFragment(node.body, indent + 1);
|
|
178
222
|
// Wrap destructured variables in parens for arrow function
|
|
179
|
-
const param = variable.startsWith('{') || variable.startsWith('[')
|
|
180
|
-
?
|
|
181
|
-
: variable;
|
|
223
|
+
const param = variable.text.startsWith('{') || variable.text.startsWith('[')
|
|
224
|
+
? concat('(', verbatim(variable), ')')
|
|
225
|
+
: verbatim(variable);
|
|
182
226
|
const bi = this.formatted ? this.ind(indent + 1) : ' ';
|
|
183
227
|
const pi = this.formatted ? this.ind(indent) : '';
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
228
|
+
// `each()` rather than `.map()`, so any iterable works (Set, Map,
|
|
229
|
+
// generators, `xs.entries()`) and an `else` can render when it's empty.
|
|
230
|
+
const head = reindentChunk(concat('each(', verbatim(iterable), ', ', param, ' =>'), this.formatted ? this.ind(indent) : '');
|
|
231
|
+
const parts = [mark(node.loc), head, `\n${bi}`, bodyChunk];
|
|
232
|
+
if (node.fallback) {
|
|
233
|
+
parts.push(`\n${pi}, () =>\n${bi}`, this.genFragment(node.fallback, indent + 1));
|
|
234
|
+
}
|
|
235
|
+
parts.push(`\n${pi})`);
|
|
236
|
+
return concat(...parts);
|
|
191
237
|
}
|
|
192
238
|
parseForParams(params, loc) {
|
|
193
239
|
// A multi-line header may put `of` at the start or end of a line.
|
|
194
|
-
const of = /\s+of\s+/.exec(params);
|
|
240
|
+
const of = /\s+of\s+/.exec(params.text);
|
|
195
241
|
if (of === null) {
|
|
196
|
-
throw new CodegenError(`Invalid for params: only "for...of" is supported, got "${params}"`, loc.line, loc.col);
|
|
242
|
+
throw new CodegenError(`Invalid for params: only "for...of" is supported, got "${params.text}"`, loc.line, loc.col);
|
|
197
243
|
}
|
|
198
|
-
const binding = params
|
|
199
|
-
const iterable = params
|
|
200
|
-
const
|
|
244
|
+
const binding = sliceCaptured(params, 0, of.index);
|
|
245
|
+
const iterable = sliceCaptured(params, of.index + of[0].length);
|
|
246
|
+
const keyword = /^(const|let)\s+/.exec(binding.text);
|
|
247
|
+
const variable = keyword ? sliceCaptured(binding, keyword[0].length) : binding;
|
|
201
248
|
return { variable, iterable };
|
|
202
249
|
}
|
|
203
250
|
// --- Fragment helper ---
|
|
@@ -220,14 +267,12 @@ class CodeGenerator {
|
|
|
220
267
|
.filter((e) => e !== null);
|
|
221
268
|
if (!this.formatted) {
|
|
222
269
|
const inner = this.joinWithCommentChunks(chunks, ' ');
|
|
223
|
-
|
|
224
|
-
return { code, mappings: offsetMappings(inner.mappings, 1) };
|
|
270
|
+
return concat('h(Fragment, {},\n', inner, '\n)');
|
|
225
271
|
}
|
|
226
272
|
const ci = this.ind(indent + 1);
|
|
227
273
|
const pi = this.ind(indent);
|
|
228
274
|
const inner = this.joinWithCommentChunks(chunks, ci);
|
|
229
|
-
|
|
230
|
-
return { code, mappings: offsetMappings(inner.mappings, 1) };
|
|
275
|
+
return concat('h(Fragment, {},\n', inner, `\n${pi})`);
|
|
231
276
|
}
|
|
232
277
|
// --- Element generation ---
|
|
233
278
|
genElement(node, indent) {
|
|
@@ -236,31 +281,23 @@ class CodeGenerator {
|
|
|
236
281
|
.map(c => this.genNode(c, indent + 1))
|
|
237
282
|
.filter((e) => e !== null);
|
|
238
283
|
if (children.length === 0) {
|
|
239
|
-
return
|
|
284
|
+
return within(node.loc, `h("${node.tag}", `, attrs, ')');
|
|
240
285
|
}
|
|
241
286
|
const hasComments = children.some(c => c.code.startsWith('//'));
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return
|
|
245
|
-
}
|
|
287
|
+
const inline = () => within(node.loc, `h("${node.tag}", `, attrs, ...children.flatMap(c => [', ', c]), ')');
|
|
288
|
+
if (!this.formatted && !hasComments)
|
|
289
|
+
return inline();
|
|
246
290
|
// Keep inline when all children are leaf nodes (text, expressions) and no comments
|
|
247
291
|
const leafTypes = new Set(['Text', 'BufferedCode', 'Pipe']);
|
|
248
292
|
const allLeaf = !hasComments && node.children
|
|
249
293
|
.filter(c => c.type !== 'Comment' && c.type !== 'PassthroughComment')
|
|
250
294
|
.every(c => leafTypes.has(c.type));
|
|
251
|
-
if (allLeaf)
|
|
252
|
-
|
|
253
|
-
return chunk(`h("${node.tag}", ${attrs}, ${childrenStr})`, node.loc);
|
|
254
|
-
}
|
|
295
|
+
if (allLeaf)
|
|
296
|
+
return inline();
|
|
255
297
|
const ci = this.ind(indent + 1);
|
|
256
298
|
const pi = this.ind(indent);
|
|
257
299
|
const inner = this.joinWithCommentChunks(children, ci);
|
|
258
|
-
|
|
259
|
-
const mappings = [
|
|
260
|
-
mapping(node.loc),
|
|
261
|
-
...offsetMappings(inner.mappings, 1),
|
|
262
|
-
];
|
|
263
|
-
return { code, mappings };
|
|
300
|
+
return within(node.loc, `h("${node.tag}", `, attrs, ',\n', inner, `\n${pi})`);
|
|
264
301
|
}
|
|
265
302
|
genAttrs(node) {
|
|
266
303
|
const parts = [];
|
|
@@ -269,45 +306,59 @@ class CodeGenerator {
|
|
|
269
306
|
// Classes: merge shorthand + explicit into one
|
|
270
307
|
if (node.classes.length > 0 && explicitClass && explicitClass.value !== null) {
|
|
271
308
|
const prefix = node.classes.join(' ');
|
|
272
|
-
parts.push(
|
|
309
|
+
parts.push(concat(this.attrName(explicitClass), ': ', this.mergeClassValue(prefix, explicitClass)));
|
|
273
310
|
}
|
|
274
311
|
else if (node.classes.length > 0) {
|
|
275
|
-
parts.push(`class: "${node.classes.join(' ')}"`);
|
|
312
|
+
parts.push(chunk(`class: "${node.classes.join(' ')}"`));
|
|
276
313
|
}
|
|
277
314
|
// ID from shorthand
|
|
278
315
|
if (node.id !== null) {
|
|
279
|
-
parts.push(`id: "${node.id}"`);
|
|
316
|
+
parts.push(chunk(`id: "${node.id}"`));
|
|
280
317
|
}
|
|
281
318
|
// Explicit attributes (skip class if already merged)
|
|
282
319
|
for (const attr of node.attrs) {
|
|
283
320
|
if (attr === explicitClass && node.classes.length > 0)
|
|
284
321
|
continue;
|
|
285
|
-
|
|
286
|
-
parts.push(`...${attr.name}`);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
// Every non-spread attribute carries a value (the parser rejects
|
|
290
|
-
// valueless attributes); booleans arrive as the verbatim
|
|
291
|
-
// expression `true` / `false`.
|
|
292
|
-
const key = this.attrKey(attr.name);
|
|
293
|
-
parts.push(`${key}: ${attr.value}`);
|
|
294
|
-
}
|
|
322
|
+
parts.push(this.genAttr(attr));
|
|
295
323
|
}
|
|
296
|
-
|
|
297
|
-
return '{}';
|
|
298
|
-
return `{ ${parts.join(', ')} }`;
|
|
324
|
+
return this.attrObject(parts);
|
|
299
325
|
}
|
|
300
|
-
|
|
326
|
+
// `{ a, b }` from attribute entries, or `{}` for none.
|
|
327
|
+
attrObject(parts) {
|
|
328
|
+
if (parts.length === 0)
|
|
329
|
+
return chunk('{}');
|
|
330
|
+
return concat('{ ', ...parts.flatMap((p, i) => (i === 0 ? [p] : [', ', p])), ' }');
|
|
331
|
+
}
|
|
332
|
+
genAttr(attr) {
|
|
333
|
+
if (attr.spread)
|
|
334
|
+
return chunk(`...${attr.name}`);
|
|
335
|
+
// Every non-spread attribute carries a value (the parser rejects
|
|
336
|
+
// valueless attributes); booleans arrive as the verbatim
|
|
337
|
+
// expression `true` / `false`.
|
|
338
|
+
return concat(this.attrName(attr), ': ', verbatim({ text: attr.value, loc: attr.valueLoc }));
|
|
339
|
+
}
|
|
340
|
+
// The object key for an attribute, mapped to its name: written `name=value`
|
|
341
|
+
// with nothing between, the name ends just before the value.
|
|
342
|
+
attrName(attr) {
|
|
343
|
+
const key = this.attrKey(attr.name);
|
|
344
|
+
const valueStart = attr.valueLoc?.[0];
|
|
345
|
+
if (!valueStart)
|
|
346
|
+
return chunk(key);
|
|
347
|
+
return chunk(key, { line: valueStart.line, col: valueStart.col - attr.name.length - 1 });
|
|
348
|
+
}
|
|
349
|
+
mergeClassValue(prefix, attr) {
|
|
350
|
+
const value = attr.value;
|
|
351
|
+
const captured = { text: value, loc: attr.valueLoc };
|
|
301
352
|
// String and template literals are always strings, so merge them now.
|
|
302
353
|
// Anything else may be undefined/null/false at render time, which must
|
|
303
354
|
// add nothing rather than stringify into the class list.
|
|
304
355
|
if (value === '""' || value === "''" || value === '``') {
|
|
305
|
-
return `"${prefix}"
|
|
356
|
+
return chunk(`"${prefix}"`);
|
|
306
357
|
}
|
|
307
358
|
if (value.startsWith('`') || value.startsWith('"') || value.startsWith("'")) {
|
|
308
|
-
return `${
|
|
359
|
+
return concat(verbatim(sliceCaptured(captured, 0, 1)), `${prefix} `, verbatim(sliceCaptured(captured, 1)));
|
|
309
360
|
}
|
|
310
|
-
return `mergeClass("${prefix}",
|
|
361
|
+
return concat(`mergeClass("${prefix}", `, verbatim(captured), ')');
|
|
311
362
|
}
|
|
312
363
|
attrKey(name) {
|
|
313
364
|
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name))
|
|
@@ -316,29 +367,31 @@ class CodeGenerator {
|
|
|
316
367
|
}
|
|
317
368
|
// --- Text generation ---
|
|
318
369
|
genText(node) {
|
|
319
|
-
return
|
|
370
|
+
return concat(mark(node.loc), this.genSegments(node.segments));
|
|
320
371
|
}
|
|
321
372
|
genPipe(node) {
|
|
322
373
|
if (node.segments.length === 0) {
|
|
323
374
|
return chunk('" "', node.loc);
|
|
324
375
|
}
|
|
325
|
-
return
|
|
376
|
+
return concat(mark(node.loc), this.genSegments(node.segments));
|
|
326
377
|
}
|
|
327
378
|
genSegments(segments) {
|
|
328
379
|
if (segments.length === 1 && segments[0].type === 'text') {
|
|
329
|
-
return `"${this.escapeString(segments[0].value)}"
|
|
380
|
+
return chunk(`"${this.escapeString(segments[0].value)}"`);
|
|
330
381
|
}
|
|
331
|
-
|
|
382
|
+
// Tagged, so each value renders as an element child would: `raw()`
|
|
383
|
+
// and mixin output stay unescaped and null/undefined render nothing.
|
|
384
|
+
const parts = ['interpolate`'];
|
|
332
385
|
for (const seg of segments) {
|
|
333
386
|
if (seg.type === 'text') {
|
|
334
|
-
|
|
387
|
+
parts.push(this.escapeTemplateLiteral(seg.value));
|
|
335
388
|
}
|
|
336
389
|
else {
|
|
337
|
-
|
|
390
|
+
parts.push('${', verbatim({ text: seg.expression, loc: seg.loc ? [seg.loc] : undefined }), '}');
|
|
338
391
|
}
|
|
339
392
|
}
|
|
340
|
-
|
|
341
|
-
return
|
|
393
|
+
parts.push('`');
|
|
394
|
+
return concat(...parts);
|
|
342
395
|
}
|
|
343
396
|
escapeString(s) {
|
|
344
397
|
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
@@ -350,34 +403,38 @@ class CodeGenerator {
|
|
|
350
403
|
genImport(node) {
|
|
351
404
|
return chunk(node.statement, node.loc);
|
|
352
405
|
}
|
|
406
|
+
// A template with slots (a layout, or a mid-layer template that both fills
|
|
407
|
+
// its parent's slots and exposes its own) is preceded by the interface its
|
|
408
|
+
// `blocks` parameter is typed with.
|
|
353
409
|
genTemplate(node, indent) {
|
|
410
|
+
const slots = templateSlots(node);
|
|
411
|
+
const fn = this.genTemplateFunction(node, slots.length > 0, indent);
|
|
412
|
+
if (slots.length === 0)
|
|
413
|
+
return fn;
|
|
414
|
+
const iface = blocksInterface(node, slots);
|
|
415
|
+
return {
|
|
416
|
+
code: `${iface.code}\n\n${fn.code}`,
|
|
417
|
+
mappings: [...iface.mappings, ...offsetMappings(fn.mappings, lineCount(iface.code) + 2)],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
genTemplateFunction(node, hasBlocks, indent) {
|
|
354
421
|
const i1 = this.ind(indent + 1);
|
|
422
|
+
const declared = verbatim({ text: node.params, loc: node.paramsLoc });
|
|
423
|
+
const params = hasBlocks
|
|
424
|
+
? appendArg(declared, `blocks: ${capitalize(node.name)}Blocks = {}`)
|
|
425
|
+
: declared;
|
|
426
|
+
const signature = concat(mark(node.loc), `export function ${node.name}(`, params, ') {\n');
|
|
355
427
|
// Check if body contains an ExtendsNode (page template)
|
|
356
428
|
const extendsNode = node.body.find((c) => c.type === 'Extends');
|
|
357
429
|
if (extendsNode) {
|
|
358
430
|
const extendsChunk = this.genExtends(extendsNode, indent + 1);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const params = templateHasBlocks(node)
|
|
362
|
-
? appendArg(node.params, `blocks: ${capitalize(node.name)}Blocks = {}`)
|
|
363
|
-
: node.params;
|
|
364
|
-
const code = `export function ${node.name}(${params}) {\n${i1}return ${extendsChunk.code}\n}`;
|
|
365
|
-
const mappings = [
|
|
366
|
-
mapping(node.loc),
|
|
367
|
-
...offsetMappings(extendsChunk.mappings, 1 + lineCount(params)),
|
|
368
|
-
];
|
|
369
|
-
return { code, mappings };
|
|
370
|
-
}
|
|
371
|
-
// Check if body tree contains BlockNodes (layout template)
|
|
372
|
-
const hasBlocks = templateHasBlocks(node);
|
|
373
|
-
const params = hasBlocks
|
|
374
|
-
? appendArg(node.params, `blocks: ${capitalize(node.name)}Blocks = {}`)
|
|
375
|
-
: node.params;
|
|
431
|
+
return concat(signature, `${i1}return `, extendsChunk, '\n}');
|
|
432
|
+
}
|
|
376
433
|
const bodyChunks = node.body
|
|
377
|
-
.map(child => this.genNode(child, indent +
|
|
434
|
+
.map(child => this.genNode(child, indent + 1))
|
|
378
435
|
.filter((e) => e !== null);
|
|
379
436
|
if (bodyChunks.length === 0) {
|
|
380
|
-
return
|
|
437
|
+
return concat(signature, `${i1}return ''\n}`);
|
|
381
438
|
}
|
|
382
439
|
// Separate comments from code chunks
|
|
383
440
|
const commentChunks = bodyChunks.filter(c => c.code.startsWith('//'));
|
|
@@ -392,99 +449,87 @@ class CodeGenerator {
|
|
|
392
449
|
else {
|
|
393
450
|
bodyChunk = this.genFragment(node.body, indent + 1);
|
|
394
451
|
}
|
|
395
|
-
//
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
: '';
|
|
399
|
-
const commentLines = commentChunks.length;
|
|
400
|
-
const code = `export function ${node.name}(${params}) {\n${commentPrefix}${i1}return ${bodyChunk.code}\n}`;
|
|
401
|
-
const mappings = [
|
|
402
|
-
mapping(node.loc),
|
|
403
|
-
...offsetMappings(bodyChunk.mappings, 1 + lineCount(params) + commentLines), // body starts after fn sig + comments
|
|
404
|
-
];
|
|
405
|
-
return { code, mappings };
|
|
452
|
+
// Comments go before the return statement
|
|
453
|
+
const comments = commentChunks.flatMap(c => [i1, c, '\n']);
|
|
454
|
+
return concat(signature, ...comments, `${i1}return `, bodyChunk, '\n}');
|
|
406
455
|
}
|
|
407
456
|
// --- Mixin generation ---
|
|
408
457
|
genMixinDefinition(node, indent) {
|
|
409
458
|
const i1 = this.ind(indent + 1);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
459
|
+
this.inMixin = true;
|
|
460
|
+
let bodyChunk;
|
|
461
|
+
try {
|
|
462
|
+
const bodyChunks = node.body
|
|
463
|
+
.map(child => this.genNode(child, indent + 1))
|
|
464
|
+
.filter((e) => e !== null);
|
|
465
|
+
bodyChunk = this.genFragment(bodyChunks.length > 0 ? node.body : [], indent + 1);
|
|
466
|
+
}
|
|
467
|
+
finally {
|
|
468
|
+
this.inMixin = false;
|
|
469
|
+
}
|
|
470
|
+
// `attrs` is always accepted so every call site can pass extra
|
|
471
|
+
// attributes, but a body that never mentions it would trip
|
|
472
|
+
// `noUnusedParameters`; the `_` prefix silences that. We can't parse the
|
|
473
|
+
// TS in the body, so this is a textual check: a stray match (e.g. the
|
|
474
|
+
// word in a string) just keeps the plain name, which is harmless.
|
|
475
|
+
const attrsParam = /\battrs\b/.test(bodyChunk.code) ? 'attrs' : '_attrs';
|
|
476
|
+
let params = appendArg(verbatim({ text: node.params, loc: node.paramsLoc }), `${attrsParam}: Attrs = {}`);
|
|
477
|
+
// A bare `block` renders whatever is indented under the call site,
|
|
478
|
+
// passed as a callback after `attrs` (which callers then always pass).
|
|
479
|
+
if (mixinHasBlock(node))
|
|
480
|
+
params = appendArg(params, 'block?: () => unknown');
|
|
481
|
+
return concat(mark(node.loc), `export function ${node.name}(`, params, `) {\n${i1}return `, bodyChunk, '\n}');
|
|
421
482
|
}
|
|
422
483
|
genMixinCall(node, indent = 0) {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
484
|
+
const hasBlock = countEffective(node.block) > 0;
|
|
485
|
+
const given = verbatim({ text: node.args, loc: node.argsLoc });
|
|
486
|
+
// Call-site content follows `attrs`, so `{}` holds its place when none are given.
|
|
487
|
+
const args = node.attrs.length > 0 || hasBlock
|
|
488
|
+
? appendArg(given, this.genCallAttrs(node.attrs))
|
|
489
|
+
: given;
|
|
426
490
|
const prefix = this.formatted ? this.ind(indent) : '';
|
|
427
|
-
|
|
491
|
+
if (!hasBlock)
|
|
492
|
+
return reindentChunk(within(node.loc, `${node.name}(`, args, ')'), prefix);
|
|
493
|
+
const head = concat(`${node.name}(`, reindentChunk(args, prefix));
|
|
494
|
+
const blockChunk = this.genFragment(node.block, indent);
|
|
495
|
+
return within(node.loc, appendArg(head, concat('() => ', blockChunk), false), ')');
|
|
428
496
|
}
|
|
429
497
|
genCallAttrs(attrs) {
|
|
430
|
-
|
|
431
|
-
for (const attr of attrs) {
|
|
432
|
-
if (attr.spread) {
|
|
433
|
-
parts.push(`...${attr.name}`);
|
|
434
|
-
}
|
|
435
|
-
else {
|
|
436
|
-
// Every non-spread attribute carries a value (the parser rejects
|
|
437
|
-
// valueless attributes); booleans arrive as the verbatim
|
|
438
|
-
// expression `true` / `false`.
|
|
439
|
-
const key = this.attrKey(attr.name);
|
|
440
|
-
parts.push(`${key}: ${attr.value}`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
return `{ ${parts.join(', ')} }`;
|
|
498
|
+
return this.attrObject(attrs.map(attr => this.genAttr(attr)));
|
|
444
499
|
}
|
|
445
500
|
// --- Block and extends generation ---
|
|
446
501
|
genBlock(node, indent) {
|
|
502
|
+
if (node.name === '' && !this.inMixin) {
|
|
503
|
+
throw new CodegenError('A bare "block" is only allowed inside a mixin; name the block', node.loc.line, node.loc.col);
|
|
504
|
+
}
|
|
505
|
+
if (node.name !== '' && this.inMixin) {
|
|
506
|
+
throw new CodegenError(`Named block "${node.name}" is not allowed inside a mixin; use a bare "block" for call-site content`, node.loc.line, node.loc.col);
|
|
507
|
+
}
|
|
508
|
+
// A layout slot lives on the `blocks` parameter; a mixin's call-site
|
|
509
|
+
// content is its `block` parameter.
|
|
510
|
+
const ref = node.name === '' ? 'block' : `blocks.${node.name}`;
|
|
447
511
|
if (node.children.length === 0) {
|
|
448
|
-
return chunk(
|
|
512
|
+
return chunk(`${ref}?.()`, node.loc);
|
|
449
513
|
}
|
|
450
514
|
const bi = this.formatted ? this.ind(indent + 1) : ' ';
|
|
451
515
|
const defaultContentChunk = this.genFragment(node.children, indent + 1);
|
|
452
|
-
|
|
453
|
-
const mappings = [
|
|
454
|
-
mapping(node.loc),
|
|
455
|
-
...offsetMappings(defaultContentChunk.mappings, 2),
|
|
456
|
-
];
|
|
457
|
-
return { code, mappings };
|
|
516
|
+
return concat(mark(node.loc), `${ref}\n${bi}? ${ref}()\n${bi}: `, defaultContentChunk);
|
|
458
517
|
}
|
|
459
518
|
genExtends(node, indent) {
|
|
460
519
|
const bi = this.formatted ? this.ind(indent + 1) : ' ';
|
|
461
520
|
const pi = this.formatted ? this.ind(indent) : '';
|
|
462
|
-
const blockChunks =
|
|
463
|
-
for (const block of node.blocks) {
|
|
464
|
-
const bodyChunk = this.genFragment(block.children, indent + 1);
|
|
465
|
-
const blockCode = `${bi}${block.name}: () => ${bodyChunk.code}`;
|
|
466
|
-
const blockMappings = [
|
|
467
|
-
mapping(block.loc),
|
|
468
|
-
...offsetMappings(bodyChunk.mappings, 0),
|
|
469
|
-
];
|
|
470
|
-
blockChunks.push({ code: blockCode, mappings: blockMappings });
|
|
471
|
-
}
|
|
521
|
+
const blockChunks = node.blocks.map(block => concat(mark(block.loc), `${bi}${block.name}: () => `, this.genFragment(block.children, indent + 1)));
|
|
472
522
|
const joinedBlocks = joinChunks(blockChunks, ',\n');
|
|
473
|
-
const args =
|
|
474
|
-
const head = `${node.target}(
|
|
475
|
-
|
|
476
|
-
const mappings = [
|
|
477
|
-
mapping(node.loc),
|
|
478
|
-
...offsetMappings(joinedBlocks.mappings, 1 + lineCount(head)),
|
|
479
|
-
];
|
|
480
|
-
return { code, mappings };
|
|
523
|
+
const args = reindentChunk(verbatim({ text: node.args, loc: node.argsLoc }), pi);
|
|
524
|
+
const head = concat(`${node.target}(`, appendArg(args, '{', false));
|
|
525
|
+
return concat(mark(node.loc), head, '\n', joinedBlocks, `\n${pi}})`);
|
|
481
526
|
}
|
|
482
527
|
genInclude(node, names) {
|
|
483
528
|
return chunk(`import { ${names.join(', ')} } from '${node.path}.js'`, node.loc);
|
|
484
529
|
}
|
|
485
530
|
// --- Simple generators ---
|
|
486
531
|
genBufferedCode(node) {
|
|
487
|
-
return
|
|
532
|
+
return concat(mark(node.loc), verbatim({ text: node.expression, loc: node.expressionLoc }));
|
|
488
533
|
}
|
|
489
534
|
genDoctype(node) {
|
|
490
535
|
return chunk('doctype()', node.loc);
|
|
@@ -545,12 +590,21 @@ function scanUsedHelpers(node) {
|
|
|
545
590
|
}
|
|
546
591
|
break;
|
|
547
592
|
case 'Iteration':
|
|
593
|
+
helpers.add('each');
|
|
548
594
|
if (countEffective(n.body) > 1) {
|
|
549
595
|
helpers.add('h');
|
|
550
596
|
helpers.add('Fragment');
|
|
551
597
|
}
|
|
552
598
|
for (const c of n.body)
|
|
553
599
|
walk(c);
|
|
600
|
+
if (n.fallback) {
|
|
601
|
+
if (countEffective(n.fallback) > 1) {
|
|
602
|
+
helpers.add('h');
|
|
603
|
+
helpers.add('Fragment');
|
|
604
|
+
}
|
|
605
|
+
for (const c of n.fallback)
|
|
606
|
+
walk(c);
|
|
607
|
+
}
|
|
554
608
|
break;
|
|
555
609
|
case 'Template': {
|
|
556
610
|
// Template body wraps in a Fragment when it has more than one
|
|
@@ -573,6 +627,10 @@ function scanUsedHelpers(node) {
|
|
|
573
627
|
walk(c);
|
|
574
628
|
break;
|
|
575
629
|
case 'MixinCall':
|
|
630
|
+
if (countEffective(n.block) > 1) {
|
|
631
|
+
helpers.add('h');
|
|
632
|
+
helpers.add('Fragment');
|
|
633
|
+
}
|
|
576
634
|
for (const c of n.block)
|
|
577
635
|
walk(c);
|
|
578
636
|
break;
|
|
@@ -594,6 +652,11 @@ function scanUsedHelpers(node) {
|
|
|
594
652
|
walk(c);
|
|
595
653
|
}
|
|
596
654
|
break;
|
|
655
|
+
case 'Text':
|
|
656
|
+
case 'Pipe':
|
|
657
|
+
if (n.segments.some(s => s.type === 'interpolation'))
|
|
658
|
+
helpers.add('interpolate');
|
|
659
|
+
break;
|
|
597
660
|
case 'Document':
|
|
598
661
|
for (const c of n.children)
|
|
599
662
|
walk(c);
|
|
@@ -633,6 +696,10 @@ function collectMixinCalls(doc) {
|
|
|
633
696
|
walkAll(n.fallback);
|
|
634
697
|
break;
|
|
635
698
|
case 'Iteration':
|
|
699
|
+
walkAll(n.body);
|
|
700
|
+
if (n.fallback)
|
|
701
|
+
walkAll(n.fallback);
|
|
702
|
+
break;
|
|
636
703
|
case 'Template':
|
|
637
704
|
case 'MixinDefinition':
|
|
638
705
|
walkAll(n.body);
|
|
@@ -735,7 +802,7 @@ export function generate(doc, options = {}) {
|
|
|
735
802
|
const helpers = scanUsedHelpers(doc);
|
|
736
803
|
// Emit runtime import (canonical order)
|
|
737
804
|
if (helpers.size > 0) {
|
|
738
|
-
const order = ['h', 'doctype', 'Fragment', 'raw', 'mergeClass'];
|
|
805
|
+
const order = ['h', 'doctype', 'Fragment', 'raw', 'mergeClass', 'each', 'interpolate'];
|
|
739
806
|
const used = order.filter(h => helpers.has(h));
|
|
740
807
|
parts.push(chunk(`import { ${used.join(', ')} } from "@chilts/weave"`));
|
|
741
808
|
}
|
|
@@ -744,25 +811,10 @@ export function generate(doc, options = {}) {
|
|
|
744
811
|
if (hasMixinDefs) {
|
|
745
812
|
parts.push(chunk('import type { Attrs } from "@chilts/weave"'));
|
|
746
813
|
}
|
|
747
|
-
//
|
|
748
|
-
let blocksTypeName = null;
|
|
749
|
-
for (const child of doc.children) {
|
|
750
|
-
if (child.type === 'Template' && templateHasBlocks(child)) {
|
|
751
|
-
blocksTypeName = capitalize(child.name) + 'Blocks';
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
// Emit user imports (augmented with blocks type if needed)
|
|
814
|
+
// Emit user imports
|
|
755
815
|
for (const child of doc.children) {
|
|
756
|
-
if (child.type === 'Import')
|
|
757
|
-
|
|
758
|
-
if (blocksTypeName && statement.includes('import type')) {
|
|
759
|
-
const idx = statement.indexOf(' }');
|
|
760
|
-
if (idx !== -1) {
|
|
761
|
-
statement = statement.slice(0, idx) + `, ${blocksTypeName}` + statement.slice(idx);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
parts.push(chunk(statement, child.loc));
|
|
765
|
-
}
|
|
816
|
+
if (child.type === 'Import')
|
|
817
|
+
parts.push(gen.genImport(child));
|
|
766
818
|
}
|
|
767
819
|
// Emit include imports: only the mixins this file calls, so an include
|
|
768
820
|
// that provides nothing used emits nothing.
|