@chilts/weave 0.6.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 +303 -0
- package/ReadMe.md +261 -8
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.js +133 -0
- package/dist/cli/index.d.ts +42 -4
- package/dist/cli/index.js +190 -36
- package/dist/codegen/index.d.ts +17 -3
- package/dist/codegen/index.js +436 -238
- 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 +8 -4
- package/dist/index.js +4 -2
- package/dist/lexer/index.d.ts +5 -1
- package/dist/lexer/index.js +340 -92
- 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 +45 -1
- package/dist/runtime/index.js +67 -2
- package/package.json +23 -5
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;
|
|
@@ -14,43 +14,119 @@ export class CodegenError extends Error {
|
|
|
14
14
|
function capitalize(s) {
|
|
15
15
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
16
16
|
}
|
|
17
|
+
// Appends a compiler-supplied argument (`attrs`, `blocks`, ...) to a
|
|
18
|
+
// captured argument or parameter list. A multi-line list may already end in
|
|
19
|
+
// a trailing comma, and when its ')' sat on a line of its own the extra goes
|
|
20
|
+
// on its own line too, so the output keeps the shape the source was written in.
|
|
21
|
+
// `ownLine` is false for an extra that opens a multi-line literal of its own.
|
|
22
|
+
function appendArg(args, extra, ownLine = true) {
|
|
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
|
+
};
|
|
34
|
+
if (ownLine && tail.includes('\n')) {
|
|
35
|
+
const indent = /^ */.exec(lastLine)[0];
|
|
36
|
+
return concat(head, body.endsWith(',') ? '' : ',', `\n${indent}`, extra, `,${tail}`);
|
|
37
|
+
}
|
|
38
|
+
return concat(head, body.endsWith(',') ? ' ' : ', ', extra);
|
|
39
|
+
}
|
|
40
|
+
// Indents the continuation lines of a multi-line captured expression to sit
|
|
41
|
+
// under the output line it starts on. Captured text never holds a string that
|
|
42
|
+
// spans lines, so every newline is safe to indent after.
|
|
43
|
+
function reindent(text, prefix) {
|
|
44
|
+
return prefix === '' ? text : text.replace(/\n(?!\n|$)/g, `\n${prefix}`);
|
|
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
|
+
}
|
|
17
63
|
function camelToKebab(name) {
|
|
18
64
|
return name.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
19
65
|
}
|
|
20
|
-
|
|
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 = [];
|
|
21
70
|
function walk(n) {
|
|
22
|
-
if (n.type === 'Block')
|
|
23
|
-
|
|
71
|
+
if (n.type === 'Block') {
|
|
72
|
+
found.push(n);
|
|
73
|
+
n.children.forEach(walk);
|
|
74
|
+
}
|
|
24
75
|
if (n.type === 'Element')
|
|
25
|
-
|
|
76
|
+
n.children.forEach(walk);
|
|
77
|
+
if (n.type === 'MixinCall')
|
|
78
|
+
n.block.forEach(walk);
|
|
26
79
|
if (n.type === 'Conditional') {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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);
|
|
35
88
|
}
|
|
36
|
-
if (n.type === 'Iteration')
|
|
37
|
-
return n.body.some(walk);
|
|
38
89
|
if (n.type === 'Extends') {
|
|
39
90
|
// The entries in `blocks` are fills (the slots this template fills in
|
|
40
91
|
// its parent), not slots of its own. A mid-layer template defines its
|
|
41
92
|
// own slots by nesting `block` nodes inside that fill content — those
|
|
42
93
|
// are what require a `blocks` parameter on this function.
|
|
43
|
-
|
|
94
|
+
for (const fill of n.blocks)
|
|
95
|
+
fill.children.forEach(walk);
|
|
44
96
|
}
|
|
45
|
-
return false;
|
|
46
97
|
}
|
|
47
|
-
|
|
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 };
|
|
48
122
|
}
|
|
49
123
|
function mapping(loc, genLine = 0) {
|
|
50
124
|
return { genLine, genCol: 0, srcLine: loc.line - 1, srcCol: loc.col - 1 };
|
|
51
125
|
}
|
|
52
126
|
class CodeGenerator {
|
|
53
127
|
formatted;
|
|
128
|
+
// Set while generating a mixin body, where `block` means call-site content.
|
|
129
|
+
inMixin = false;
|
|
54
130
|
constructor(formatted = false) {
|
|
55
131
|
this.formatted = formatted;
|
|
56
132
|
}
|
|
@@ -63,23 +139,19 @@ class CodeGenerator {
|
|
|
63
139
|
* Non-comment expressions get trailing commas only if more non-comment expressions follow.
|
|
64
140
|
*/
|
|
65
141
|
joinWithCommentChunks(chunks, indent) {
|
|
66
|
-
const
|
|
67
|
-
const lines = [];
|
|
68
|
-
let currentLine = 0;
|
|
142
|
+
const parts = [];
|
|
69
143
|
for (let i = 0; i < chunks.length; i++) {
|
|
70
144
|
const c = chunks[i];
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
else {
|
|
145
|
+
if (i > 0)
|
|
146
|
+
parts.push('\n');
|
|
147
|
+
parts.push(indent, c);
|
|
148
|
+
if (!c.code.startsWith('//')) {
|
|
76
149
|
const hasMore = chunks.slice(i + 1).some(e => !e.code.startsWith('//'));
|
|
77
|
-
|
|
150
|
+
if (hasMore)
|
|
151
|
+
parts.push(',');
|
|
78
152
|
}
|
|
79
|
-
allMappings.push(...offsetMappings(c.mappings, currentLine));
|
|
80
|
-
currentLine += lineCount(c.code) + 1;
|
|
81
153
|
}
|
|
82
|
-
return
|
|
154
|
+
return concat(...parts);
|
|
83
155
|
}
|
|
84
156
|
genNode(node, indent = 0) {
|
|
85
157
|
switch (node.type) {
|
|
@@ -110,9 +182,10 @@ class CodeGenerator {
|
|
|
110
182
|
case 'MixinDefinition':
|
|
111
183
|
return this.genMixinDefinition(node, indent);
|
|
112
184
|
case 'MixinCall':
|
|
113
|
-
return this.genMixinCall(node);
|
|
185
|
+
return this.genMixinCall(node, indent);
|
|
114
186
|
case 'Include':
|
|
115
|
-
|
|
187
|
+
// Hoisted: generate() emits the import once it knows the names.
|
|
188
|
+
return null;
|
|
116
189
|
case 'Block':
|
|
117
190
|
return this.genBlock(node, indent);
|
|
118
191
|
case 'Extends':
|
|
@@ -125,53 +198,53 @@ class CodeGenerator {
|
|
|
125
198
|
genConditional(node, indent) {
|
|
126
199
|
const bi = this.formatted ? this.ind(indent + 1) : ' ';
|
|
127
200
|
const consequentChunk = this.genFragment(node.consequent, indent + 1);
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
201
|
+
const condition = verbatim({ text: node.condition, loc: node.conditionLoc });
|
|
202
|
+
const parts = [
|
|
203
|
+
node.negated ? concat('!(', condition, ')') : condition,
|
|
204
|
+
`\n${bi}? `, consequentChunk,
|
|
132
205
|
];
|
|
133
206
|
for (const alt of node.alternates) {
|
|
134
207
|
const altBodyChunk = this.genFragment(alt.body, indent + 1);
|
|
135
|
-
|
|
136
|
-
code += `\n${bi}: ${alt.condition}\n${bi}? ${altBodyChunk.code}`;
|
|
137
|
-
mappings.push(...offsetMappings(altBodyChunk.mappings, prevLines + 2));
|
|
208
|
+
parts.push(`\n${bi}: `, verbatim({ text: alt.condition, loc: alt.conditionLoc }), `\n${bi}? `, altBodyChunk);
|
|
138
209
|
}
|
|
139
210
|
if (node.fallback !== null) {
|
|
140
|
-
|
|
141
|
-
const prevLines = lineCount(code);
|
|
142
|
-
code += `\n${bi}: ${fallbackChunk.code}`;
|
|
143
|
-
mappings.push(...offsetMappings(fallbackChunk.mappings, prevLines + 1));
|
|
211
|
+
parts.push(`\n${bi}: `, this.genFragment(node.fallback, indent + 1));
|
|
144
212
|
}
|
|
145
213
|
else {
|
|
146
|
-
|
|
214
|
+
parts.push(`\n${bi}: null`);
|
|
147
215
|
}
|
|
148
|
-
return
|
|
216
|
+
return concat(mark(node.loc), ...parts);
|
|
149
217
|
}
|
|
150
218
|
// --- Iteration generation ---
|
|
151
219
|
genIteration(node, indent) {
|
|
152
|
-
const { variable, iterable } = this.parseForParams(node.params, node.loc);
|
|
220
|
+
const { variable, iterable } = this.parseForParams({ text: node.params, loc: node.paramsLoc }, node.loc);
|
|
153
221
|
const bodyChunk = this.genFragment(node.body, indent + 1);
|
|
154
222
|
// Wrap destructured variables in parens for arrow function
|
|
155
|
-
const param = variable.startsWith('{') || variable.startsWith('[')
|
|
156
|
-
?
|
|
157
|
-
: variable;
|
|
223
|
+
const param = variable.text.startsWith('{') || variable.text.startsWith('[')
|
|
224
|
+
? concat('(', verbatim(variable), ')')
|
|
225
|
+
: verbatim(variable);
|
|
158
226
|
const bi = this.formatted ? this.ind(indent + 1) : ' ';
|
|
159
227
|
const pi = this.formatted ? this.ind(indent) : '';
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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);
|
|
166
237
|
}
|
|
167
238
|
parseForParams(params, loc) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
239
|
+
// A multi-line header may put `of` at the start or end of a line.
|
|
240
|
+
const of = /\s+of\s+/.exec(params.text);
|
|
241
|
+
if (of === null) {
|
|
242
|
+
throw new CodegenError(`Invalid for params: only "for...of" is supported, got "${params.text}"`, loc.line, loc.col);
|
|
243
|
+
}
|
|
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;
|
|
175
248
|
return { variable, iterable };
|
|
176
249
|
}
|
|
177
250
|
// --- Fragment helper ---
|
|
@@ -194,14 +267,12 @@ class CodeGenerator {
|
|
|
194
267
|
.filter((e) => e !== null);
|
|
195
268
|
if (!this.formatted) {
|
|
196
269
|
const inner = this.joinWithCommentChunks(chunks, ' ');
|
|
197
|
-
|
|
198
|
-
return { code, mappings: offsetMappings(inner.mappings, 1) };
|
|
270
|
+
return concat('h(Fragment, {},\n', inner, '\n)');
|
|
199
271
|
}
|
|
200
272
|
const ci = this.ind(indent + 1);
|
|
201
273
|
const pi = this.ind(indent);
|
|
202
274
|
const inner = this.joinWithCommentChunks(chunks, ci);
|
|
203
|
-
|
|
204
|
-
return { code, mappings: offsetMappings(inner.mappings, 1) };
|
|
275
|
+
return concat('h(Fragment, {},\n', inner, `\n${pi})`);
|
|
205
276
|
}
|
|
206
277
|
// --- Element generation ---
|
|
207
278
|
genElement(node, indent) {
|
|
@@ -210,31 +281,23 @@ class CodeGenerator {
|
|
|
210
281
|
.map(c => this.genNode(c, indent + 1))
|
|
211
282
|
.filter((e) => e !== null);
|
|
212
283
|
if (children.length === 0) {
|
|
213
|
-
return
|
|
284
|
+
return within(node.loc, `h("${node.tag}", `, attrs, ')');
|
|
214
285
|
}
|
|
215
286
|
const hasComments = children.some(c => c.code.startsWith('//'));
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return
|
|
219
|
-
}
|
|
287
|
+
const inline = () => within(node.loc, `h("${node.tag}", `, attrs, ...children.flatMap(c => [', ', c]), ')');
|
|
288
|
+
if (!this.formatted && !hasComments)
|
|
289
|
+
return inline();
|
|
220
290
|
// Keep inline when all children are leaf nodes (text, expressions) and no comments
|
|
221
291
|
const leafTypes = new Set(['Text', 'BufferedCode', 'Pipe']);
|
|
222
292
|
const allLeaf = !hasComments && node.children
|
|
223
293
|
.filter(c => c.type !== 'Comment' && c.type !== 'PassthroughComment')
|
|
224
294
|
.every(c => leafTypes.has(c.type));
|
|
225
|
-
if (allLeaf)
|
|
226
|
-
|
|
227
|
-
return chunk(`h("${node.tag}", ${attrs}, ${childrenStr})`, node.loc);
|
|
228
|
-
}
|
|
295
|
+
if (allLeaf)
|
|
296
|
+
return inline();
|
|
229
297
|
const ci = this.ind(indent + 1);
|
|
230
298
|
const pi = this.ind(indent);
|
|
231
299
|
const inner = this.joinWithCommentChunks(children, ci);
|
|
232
|
-
|
|
233
|
-
const mappings = [
|
|
234
|
-
mapping(node.loc),
|
|
235
|
-
...offsetMappings(inner.mappings, 1),
|
|
236
|
-
];
|
|
237
|
-
return { code, mappings };
|
|
300
|
+
return within(node.loc, `h("${node.tag}", `, attrs, ',\n', inner, `\n${pi})`);
|
|
238
301
|
}
|
|
239
302
|
genAttrs(node) {
|
|
240
303
|
const parts = [];
|
|
@@ -243,42 +306,59 @@ class CodeGenerator {
|
|
|
243
306
|
// Classes: merge shorthand + explicit into one
|
|
244
307
|
if (node.classes.length > 0 && explicitClass && explicitClass.value !== null) {
|
|
245
308
|
const prefix = node.classes.join(' ');
|
|
246
|
-
parts.push(
|
|
309
|
+
parts.push(concat(this.attrName(explicitClass), ': ', this.mergeClassValue(prefix, explicitClass)));
|
|
247
310
|
}
|
|
248
311
|
else if (node.classes.length > 0) {
|
|
249
|
-
parts.push(`class: "${node.classes.join(' ')}"`);
|
|
312
|
+
parts.push(chunk(`class: "${node.classes.join(' ')}"`));
|
|
250
313
|
}
|
|
251
314
|
// ID from shorthand
|
|
252
315
|
if (node.id !== null) {
|
|
253
|
-
parts.push(`id: "${node.id}"`);
|
|
316
|
+
parts.push(chunk(`id: "${node.id}"`));
|
|
254
317
|
}
|
|
255
318
|
// Explicit attributes (skip class if already merged)
|
|
256
319
|
for (const attr of node.attrs) {
|
|
257
320
|
if (attr === explicitClass && node.classes.length > 0)
|
|
258
321
|
continue;
|
|
259
|
-
|
|
260
|
-
parts.push(`...${attr.name}`);
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
// Every non-spread attribute carries a value (the parser rejects
|
|
264
|
-
// valueless attributes); booleans arrive as the verbatim
|
|
265
|
-
// expression `true` / `false`.
|
|
266
|
-
const key = this.attrKey(attr.name);
|
|
267
|
-
parts.push(`${key}: ${attr.value}`);
|
|
268
|
-
}
|
|
322
|
+
parts.push(this.genAttr(attr));
|
|
269
323
|
}
|
|
270
|
-
|
|
271
|
-
return '{}';
|
|
272
|
-
return `{ ${parts.join(', ')} }`;
|
|
324
|
+
return this.attrObject(parts);
|
|
273
325
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 };
|
|
352
|
+
// String and template literals are always strings, so merge them now.
|
|
353
|
+
// Anything else may be undefined/null/false at render time, which must
|
|
354
|
+
// add nothing rather than stringify into the class list.
|
|
355
|
+
if (value === '""' || value === "''" || value === '``') {
|
|
356
|
+
return chunk(`"${prefix}"`);
|
|
357
|
+
}
|
|
358
|
+
if (value.startsWith('`') || value.startsWith('"') || value.startsWith("'")) {
|
|
359
|
+
return concat(verbatim(sliceCaptured(captured, 0, 1)), `${prefix} `, verbatim(sliceCaptured(captured, 1)));
|
|
360
|
+
}
|
|
361
|
+
return concat(`mergeClass("${prefix}", `, verbatim(captured), ')');
|
|
282
362
|
}
|
|
283
363
|
attrKey(name) {
|
|
284
364
|
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name))
|
|
@@ -287,29 +367,31 @@ class CodeGenerator {
|
|
|
287
367
|
}
|
|
288
368
|
// --- Text generation ---
|
|
289
369
|
genText(node) {
|
|
290
|
-
return
|
|
370
|
+
return concat(mark(node.loc), this.genSegments(node.segments));
|
|
291
371
|
}
|
|
292
372
|
genPipe(node) {
|
|
293
373
|
if (node.segments.length === 0) {
|
|
294
374
|
return chunk('" "', node.loc);
|
|
295
375
|
}
|
|
296
|
-
return
|
|
376
|
+
return concat(mark(node.loc), this.genSegments(node.segments));
|
|
297
377
|
}
|
|
298
378
|
genSegments(segments) {
|
|
299
379
|
if (segments.length === 1 && segments[0].type === 'text') {
|
|
300
|
-
return `"${this.escapeString(segments[0].value)}"
|
|
380
|
+
return chunk(`"${this.escapeString(segments[0].value)}"`);
|
|
301
381
|
}
|
|
302
|
-
|
|
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`'];
|
|
303
385
|
for (const seg of segments) {
|
|
304
386
|
if (seg.type === 'text') {
|
|
305
|
-
|
|
387
|
+
parts.push(this.escapeTemplateLiteral(seg.value));
|
|
306
388
|
}
|
|
307
389
|
else {
|
|
308
|
-
|
|
390
|
+
parts.push('${', verbatim({ text: seg.expression, loc: seg.loc ? [seg.loc] : undefined }), '}');
|
|
309
391
|
}
|
|
310
392
|
}
|
|
311
|
-
|
|
312
|
-
return
|
|
393
|
+
parts.push('`');
|
|
394
|
+
return concat(...parts);
|
|
313
395
|
}
|
|
314
396
|
escapeString(s) {
|
|
315
397
|
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
@@ -321,34 +403,38 @@ class CodeGenerator {
|
|
|
321
403
|
genImport(node) {
|
|
322
404
|
return chunk(node.statement, node.loc);
|
|
323
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.
|
|
324
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) {
|
|
325
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');
|
|
326
427
|
// Check if body contains an ExtendsNode (page template)
|
|
327
428
|
const extendsNode = node.body.find((c) => c.type === 'Extends');
|
|
328
429
|
if (extendsNode) {
|
|
329
430
|
const extendsChunk = this.genExtends(extendsNode, indent + 1);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const params = templateHasBlocks(node)
|
|
333
|
-
? `${node.params}, blocks: ${capitalize(node.name)}Blocks = {}`
|
|
334
|
-
: node.params;
|
|
335
|
-
const code = `export function ${node.name}(${params}) {\n${i1}return ${extendsChunk.code}\n}`;
|
|
336
|
-
const mappings = [
|
|
337
|
-
mapping(node.loc),
|
|
338
|
-
...offsetMappings(extendsChunk.mappings, 1),
|
|
339
|
-
];
|
|
340
|
-
return { code, mappings };
|
|
341
|
-
}
|
|
342
|
-
// Check if body tree contains BlockNodes (layout template)
|
|
343
|
-
const hasBlocks = templateHasBlocks(node);
|
|
344
|
-
const params = hasBlocks
|
|
345
|
-
? `${node.params}, blocks: ${capitalize(node.name)}Blocks = {}`
|
|
346
|
-
: node.params;
|
|
431
|
+
return concat(signature, `${i1}return `, extendsChunk, '\n}');
|
|
432
|
+
}
|
|
347
433
|
const bodyChunks = node.body
|
|
348
|
-
.map(child => this.genNode(child, indent +
|
|
434
|
+
.map(child => this.genNode(child, indent + 1))
|
|
349
435
|
.filter((e) => e !== null);
|
|
350
436
|
if (bodyChunks.length === 0) {
|
|
351
|
-
return
|
|
437
|
+
return concat(signature, `${i1}return ''\n}`);
|
|
352
438
|
}
|
|
353
439
|
// Separate comments from code chunks
|
|
354
440
|
const commentChunks = bodyChunks.filter(c => c.code.startsWith('//'));
|
|
@@ -363,104 +449,87 @@ class CodeGenerator {
|
|
|
363
449
|
else {
|
|
364
450
|
bodyChunk = this.genFragment(node.body, indent + 1);
|
|
365
451
|
}
|
|
366
|
-
//
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
: '';
|
|
370
|
-
const commentLines = commentChunks.length;
|
|
371
|
-
const code = `export function ${node.name}(${params}) {\n${commentPrefix}${i1}return ${bodyChunk.code}\n}`;
|
|
372
|
-
const mappings = [
|
|
373
|
-
mapping(node.loc),
|
|
374
|
-
...offsetMappings(bodyChunk.mappings, 1 + commentLines), // body starts after fn sig + comments
|
|
375
|
-
];
|
|
376
|
-
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}');
|
|
377
455
|
}
|
|
378
456
|
// --- Mixin generation ---
|
|
379
457
|
genMixinDefinition(node, indent) {
|
|
380
458
|
const i1 = this.ind(indent + 1);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return
|
|
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}');
|
|
482
|
+
}
|
|
483
|
+
genMixinCall(node, indent = 0) {
|
|
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;
|
|
490
|
+
const prefix = this.formatted ? this.ind(indent) : '';
|
|
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), ')');
|
|
404
496
|
}
|
|
405
497
|
genCallAttrs(attrs) {
|
|
406
|
-
|
|
407
|
-
for (const attr of attrs) {
|
|
408
|
-
if (attr.spread) {
|
|
409
|
-
parts.push(`...${attr.name}`);
|
|
410
|
-
}
|
|
411
|
-
else {
|
|
412
|
-
// Every non-spread attribute carries a value (the parser rejects
|
|
413
|
-
// valueless attributes); booleans arrive as the verbatim
|
|
414
|
-
// expression `true` / `false`.
|
|
415
|
-
const key = this.attrKey(attr.name);
|
|
416
|
-
parts.push(`${key}: ${attr.value}`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
return `{ ${parts.join(', ')} }`;
|
|
498
|
+
return this.attrObject(attrs.map(attr => this.genAttr(attr)));
|
|
420
499
|
}
|
|
421
500
|
// --- Block and extends generation ---
|
|
422
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}`;
|
|
423
511
|
if (node.children.length === 0) {
|
|
424
|
-
return chunk(
|
|
512
|
+
return chunk(`${ref}?.()`, node.loc);
|
|
425
513
|
}
|
|
426
514
|
const bi = this.formatted ? this.ind(indent + 1) : ' ';
|
|
427
515
|
const defaultContentChunk = this.genFragment(node.children, indent + 1);
|
|
428
|
-
|
|
429
|
-
const mappings = [
|
|
430
|
-
mapping(node.loc),
|
|
431
|
-
...offsetMappings(defaultContentChunk.mappings, 2),
|
|
432
|
-
];
|
|
433
|
-
return { code, mappings };
|
|
516
|
+
return concat(mark(node.loc), `${ref}\n${bi}? ${ref}()\n${bi}: `, defaultContentChunk);
|
|
434
517
|
}
|
|
435
518
|
genExtends(node, indent) {
|
|
436
519
|
const bi = this.formatted ? this.ind(indent + 1) : ' ';
|
|
437
520
|
const pi = this.formatted ? this.ind(indent) : '';
|
|
438
|
-
const blockChunks =
|
|
439
|
-
for (const block of node.blocks) {
|
|
440
|
-
const bodyChunk = this.genFragment(block.children, indent + 1);
|
|
441
|
-
const blockCode = `${bi}${block.name}: () => ${bodyChunk.code}`;
|
|
442
|
-
const blockMappings = [
|
|
443
|
-
mapping(block.loc),
|
|
444
|
-
...offsetMappings(bodyChunk.mappings, 0),
|
|
445
|
-
];
|
|
446
|
-
blockChunks.push({ code: blockCode, mappings: blockMappings });
|
|
447
|
-
}
|
|
521
|
+
const blockChunks = node.blocks.map(block => concat(mark(block.loc), `${bi}${block.name}: () => `, this.genFragment(block.children, indent + 1)));
|
|
448
522
|
const joinedBlocks = joinChunks(blockChunks, ',\n');
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
...offsetMappings(joinedBlocks.mappings, 1),
|
|
453
|
-
];
|
|
454
|
-
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}})`);
|
|
455
526
|
}
|
|
456
|
-
genInclude(node) {
|
|
457
|
-
|
|
458
|
-
const name = segments[segments.length - 1];
|
|
459
|
-
return chunk(`import { ${name} } from '${node.path}.js'`, node.loc);
|
|
527
|
+
genInclude(node, names) {
|
|
528
|
+
return chunk(`import { ${names.join(', ')} } from '${node.path}.js'`, node.loc);
|
|
460
529
|
}
|
|
461
530
|
// --- Simple generators ---
|
|
462
531
|
genBufferedCode(node) {
|
|
463
|
-
return
|
|
532
|
+
return concat(mark(node.loc), verbatim({ text: node.expression, loc: node.expressionLoc }));
|
|
464
533
|
}
|
|
465
534
|
genDoctype(node) {
|
|
466
535
|
return chunk('doctype()', node.loc);
|
|
@@ -470,12 +539,21 @@ class CodeGenerator {
|
|
|
470
539
|
function countEffective(nodes) {
|
|
471
540
|
return nodes.filter(n => n.type !== 'Comment' && n.type !== 'PassthroughComment').length;
|
|
472
541
|
}
|
|
542
|
+
/** True when shorthand classes merge with a `class=` value that is not a string literal. */
|
|
543
|
+
function needsRuntimeClassMerge(node) {
|
|
544
|
+
const explicitClass = node.attrs.find(a => a.name === 'class' && !a.spread);
|
|
545
|
+
if (!explicitClass || explicitClass.value === null)
|
|
546
|
+
return false;
|
|
547
|
+
return !/^["'`]/.test(explicitClass.value);
|
|
548
|
+
}
|
|
473
549
|
function scanUsedHelpers(node) {
|
|
474
550
|
const helpers = new Set();
|
|
475
551
|
function walk(n) {
|
|
476
552
|
switch (n.type) {
|
|
477
553
|
case 'Element':
|
|
478
554
|
helpers.add('h');
|
|
555
|
+
if (n.classes.length > 0 && needsRuntimeClassMerge(n))
|
|
556
|
+
helpers.add('mergeClass');
|
|
479
557
|
if (n.rawBlock)
|
|
480
558
|
helpers.add('raw');
|
|
481
559
|
for (const c of n.children)
|
|
@@ -512,12 +590,21 @@ function scanUsedHelpers(node) {
|
|
|
512
590
|
}
|
|
513
591
|
break;
|
|
514
592
|
case 'Iteration':
|
|
593
|
+
helpers.add('each');
|
|
515
594
|
if (countEffective(n.body) > 1) {
|
|
516
595
|
helpers.add('h');
|
|
517
596
|
helpers.add('Fragment');
|
|
518
597
|
}
|
|
519
598
|
for (const c of n.body)
|
|
520
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
|
+
}
|
|
521
608
|
break;
|
|
522
609
|
case 'Template': {
|
|
523
610
|
// Template body wraps in a Fragment when it has more than one
|
|
@@ -540,6 +627,10 @@ function scanUsedHelpers(node) {
|
|
|
540
627
|
walk(c);
|
|
541
628
|
break;
|
|
542
629
|
case 'MixinCall':
|
|
630
|
+
if (countEffective(n.block) > 1) {
|
|
631
|
+
helpers.add('h');
|
|
632
|
+
helpers.add('Fragment');
|
|
633
|
+
}
|
|
543
634
|
for (const c of n.block)
|
|
544
635
|
walk(c);
|
|
545
636
|
break;
|
|
@@ -561,6 +652,11 @@ function scanUsedHelpers(node) {
|
|
|
561
652
|
walk(c);
|
|
562
653
|
}
|
|
563
654
|
break;
|
|
655
|
+
case 'Text':
|
|
656
|
+
case 'Pipe':
|
|
657
|
+
if (n.segments.some(s => s.type === 'interpolation'))
|
|
658
|
+
helpers.add('interpolate');
|
|
659
|
+
break;
|
|
564
660
|
case 'Document':
|
|
565
661
|
for (const c of n.children)
|
|
566
662
|
walk(c);
|
|
@@ -570,6 +666,125 @@ function scanUsedHelpers(node) {
|
|
|
570
666
|
walk(node);
|
|
571
667
|
return helpers;
|
|
572
668
|
}
|
|
669
|
+
// --- Include resolution ---
|
|
670
|
+
/** Names of the mixins a document defines at top level. */
|
|
671
|
+
export function definedMixins(doc) {
|
|
672
|
+
return doc.children.flatMap(c => (c.type === 'MixinDefinition' ? [c.name] : []));
|
|
673
|
+
}
|
|
674
|
+
/** Every mixin call in the document, first call of each name, in source order. */
|
|
675
|
+
function collectMixinCalls(doc) {
|
|
676
|
+
const calls = new Map();
|
|
677
|
+
function walkAll(nodes) {
|
|
678
|
+
for (const n of nodes)
|
|
679
|
+
walk(n);
|
|
680
|
+
}
|
|
681
|
+
function walk(n) {
|
|
682
|
+
switch (n.type) {
|
|
683
|
+
case 'MixinCall':
|
|
684
|
+
if (!calls.has(n.name))
|
|
685
|
+
calls.set(n.name, n);
|
|
686
|
+
walkAll(n.block);
|
|
687
|
+
break;
|
|
688
|
+
case 'Element':
|
|
689
|
+
walkAll(n.children);
|
|
690
|
+
break;
|
|
691
|
+
case 'Conditional':
|
|
692
|
+
walkAll(n.consequent);
|
|
693
|
+
for (const alt of n.alternates)
|
|
694
|
+
walkAll(alt.body);
|
|
695
|
+
if (n.fallback)
|
|
696
|
+
walkAll(n.fallback);
|
|
697
|
+
break;
|
|
698
|
+
case 'Iteration':
|
|
699
|
+
walkAll(n.body);
|
|
700
|
+
if (n.fallback)
|
|
701
|
+
walkAll(n.fallback);
|
|
702
|
+
break;
|
|
703
|
+
case 'Template':
|
|
704
|
+
case 'MixinDefinition':
|
|
705
|
+
walkAll(n.body);
|
|
706
|
+
break;
|
|
707
|
+
case 'Block':
|
|
708
|
+
walkAll(n.children);
|
|
709
|
+
break;
|
|
710
|
+
case 'Extends':
|
|
711
|
+
for (const block of n.blocks)
|
|
712
|
+
walkAll(block.children);
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
walkAll(doc.children);
|
|
717
|
+
return calls;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Value bindings a pass-through `import` statement introduces. A loose read,
|
|
721
|
+
* not a TS parse: it only needs to spot a mixin imported by hand.
|
|
722
|
+
*/
|
|
723
|
+
function importedBindings(statement) {
|
|
724
|
+
if (/^import\s+type\b/.test(statement))
|
|
725
|
+
return [];
|
|
726
|
+
const clause = /^import\s+([\s\S]*?)\s+from\b/.exec(statement)?.[1];
|
|
727
|
+
if (!clause)
|
|
728
|
+
return [];
|
|
729
|
+
const names = [];
|
|
730
|
+
const defaultName = /^([A-Za-z_$][\w$]*)/.exec(clause)?.[1];
|
|
731
|
+
if (defaultName)
|
|
732
|
+
names.push(defaultName);
|
|
733
|
+
const namespace = /\*\s*as\s+([A-Za-z_$][\w$]*)/.exec(clause)?.[1];
|
|
734
|
+
if (namespace)
|
|
735
|
+
names.push(namespace);
|
|
736
|
+
const named = /\{([^}]*)\}/.exec(clause)?.[1];
|
|
737
|
+
if (named) {
|
|
738
|
+
for (const spec of named.split(',')) {
|
|
739
|
+
const s = spec.trim();
|
|
740
|
+
if (s === '' || /^type\s/.test(s))
|
|
741
|
+
continue;
|
|
742
|
+
names.push(s.split(/\s+as\s+/).pop().trim());
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return names;
|
|
746
|
+
}
|
|
747
|
+
/** Which called mixins each include must import, in first-call order. */
|
|
748
|
+
function resolveIncludes(doc, resolveInclude) {
|
|
749
|
+
const includes = doc.children.filter((c) => c.type === 'Include');
|
|
750
|
+
const picks = new Map(includes.map(inc => [inc, []]));
|
|
751
|
+
if (includes.length === 0)
|
|
752
|
+
return picks;
|
|
753
|
+
const provided = new Set(definedMixins(doc));
|
|
754
|
+
for (const child of doc.children) {
|
|
755
|
+
if (child.type === 'Import') {
|
|
756
|
+
for (const name of importedBindings(child.statement))
|
|
757
|
+
provided.add(name);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const needed = [...collectMixinCalls(doc).values()].filter(call => !provided.has(call.name));
|
|
761
|
+
if (!resolveInclude) {
|
|
762
|
+
if (includes.length > 1 && needed.length > 0) {
|
|
763
|
+
const inc = includes[1];
|
|
764
|
+
throw new CodegenError(`Cannot tell which include defines mixin "${needed[0].name}" without an include resolver`, inc.loc.line, inc.loc.col);
|
|
765
|
+
}
|
|
766
|
+
picks.set(includes[0], needed.map(call => call.name));
|
|
767
|
+
return picks;
|
|
768
|
+
}
|
|
769
|
+
const available = new Map();
|
|
770
|
+
for (const inc of includes) {
|
|
771
|
+
try {
|
|
772
|
+
available.set(inc, new Set(resolveInclude(inc.path)));
|
|
773
|
+
}
|
|
774
|
+
catch (err) {
|
|
775
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
776
|
+
throw new CodegenError(`Cannot include ${inc.path}: ${msg}`, inc.loc.line, inc.loc.col);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
for (const call of needed) {
|
|
780
|
+
const inc = includes.find(i => available.get(i).has(call.name));
|
|
781
|
+
if (!inc) {
|
|
782
|
+
throw new CodegenError(`Mixin "${call.name}" is not defined in this file or any include`, call.loc.line, call.loc.col);
|
|
783
|
+
}
|
|
784
|
+
picks.get(inc).push(call.name);
|
|
785
|
+
}
|
|
786
|
+
return picks;
|
|
787
|
+
}
|
|
573
788
|
// --- Public API ---
|
|
574
789
|
/** Generate a TypeScript expression from a single AST node. */
|
|
575
790
|
export function generateExpr(node) {
|
|
@@ -580,14 +795,14 @@ export function generateExpr(node) {
|
|
|
580
795
|
return result.code;
|
|
581
796
|
}
|
|
582
797
|
/** Generate TypeScript source from a DocumentNode. */
|
|
583
|
-
export function generate(doc) {
|
|
798
|
+
export function generate(doc, options = {}) {
|
|
584
799
|
const gen = new CodeGenerator(true);
|
|
585
800
|
const parts = [];
|
|
586
801
|
// Scan for used runtime helpers
|
|
587
802
|
const helpers = scanUsedHelpers(doc);
|
|
588
803
|
// Emit runtime import (canonical order)
|
|
589
804
|
if (helpers.size > 0) {
|
|
590
|
-
const order = ['h', 'doctype', 'Fragment', 'raw'];
|
|
805
|
+
const order = ['h', 'doctype', 'Fragment', 'raw', 'mergeClass', 'each', 'interpolate'];
|
|
591
806
|
const used = order.filter(h => helpers.has(h));
|
|
592
807
|
parts.push(chunk(`import { ${used.join(', ')} } from "@chilts/weave"`));
|
|
593
808
|
}
|
|
@@ -596,33 +811,16 @@ export function generate(doc) {
|
|
|
596
811
|
if (hasMixinDefs) {
|
|
597
812
|
parts.push(chunk('import type { Attrs } from "@chilts/weave"'));
|
|
598
813
|
}
|
|
599
|
-
//
|
|
600
|
-
let blocksTypeName = null;
|
|
814
|
+
// Emit user imports
|
|
601
815
|
for (const child of doc.children) {
|
|
602
|
-
if (child.type === '
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
// Emit user imports (augmented with blocks type if needed)
|
|
607
|
-
for (const child of doc.children) {
|
|
608
|
-
if (child.type === 'Import') {
|
|
609
|
-
let statement = child.statement;
|
|
610
|
-
if (blocksTypeName && statement.includes('import type')) {
|
|
611
|
-
const idx = statement.indexOf(' }');
|
|
612
|
-
if (idx !== -1) {
|
|
613
|
-
statement = statement.slice(0, idx) + `, ${blocksTypeName}` + statement.slice(idx);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
parts.push(chunk(statement, child.loc));
|
|
617
|
-
}
|
|
816
|
+
if (child.type === 'Import')
|
|
817
|
+
parts.push(gen.genImport(child));
|
|
618
818
|
}
|
|
619
|
-
// Emit include imports
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
parts.push(result);
|
|
625
|
-
}
|
|
819
|
+
// Emit include imports: only the mixins this file calls, so an include
|
|
820
|
+
// that provides nothing used emits nothing.
|
|
821
|
+
for (const [include, names] of resolveIncludes(doc, options.resolveInclude)) {
|
|
822
|
+
if (names.length > 0)
|
|
823
|
+
parts.push(gen.genInclude(include, names));
|
|
626
824
|
}
|
|
627
825
|
// Emit extends imports (auto-import layout function)
|
|
628
826
|
for (const child of doc.children) {
|
|
@@ -647,16 +845,16 @@ export function generate(doc) {
|
|
|
647
845
|
return { code: final.code + '\n', mappings: final.mappings };
|
|
648
846
|
}
|
|
649
847
|
/** Full pipeline: source string → TypeScript source. */
|
|
650
|
-
export function compile(source) {
|
|
848
|
+
export function compile(source, options = {}) {
|
|
651
849
|
const tokens = lex(source);
|
|
652
850
|
const doc = parse(tokens);
|
|
653
|
-
return generate(doc).code;
|
|
851
|
+
return generate(doc, options).code;
|
|
654
852
|
}
|
|
655
853
|
/** Full pipeline with source map generation. */
|
|
656
854
|
export function compileWithSourceMap(source, opts) {
|
|
657
855
|
const tokens = lex(source);
|
|
658
856
|
const doc = parse(tokens);
|
|
659
|
-
const result = generate(doc);
|
|
857
|
+
const result = generate(doc, opts);
|
|
660
858
|
const sourceMap = buildSourceMap(result.mappings, opts.file, opts.sourceFile);
|
|
661
859
|
return { code: result.code, sourceMap };
|
|
662
860
|
}
|