@barefootjs/xslate 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/dist/adapter/boolean-result.d.ts +42 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +1204 -0
- package/dist/adapter/xslate-adapter.d.ts +176 -0
- package/dist/adapter/xslate-adapter.d.ts.map +1 -0
- package/dist/build.d.ts +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +1224 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1204 -0
- package/lib/BarefootJS/Backend/Xslate.pm +152 -0
- package/package.json +82 -0
- package/src/__tests__/xslate-counter.test.ts +142 -0
- package/src/adapter/boolean-result.ts +126 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/xslate-adapter.ts +1721 -0
- package/src/build.ts +37 -0
- package/src/index.ts +8 -0
package/dist/build.js
ADDED
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
// src/adapter/xslate-adapter.ts
|
|
2
|
+
import {
|
|
3
|
+
BaseAdapter,
|
|
4
|
+
isBooleanAttr,
|
|
5
|
+
parseExpression as parseExpression2,
|
|
6
|
+
isSupported,
|
|
7
|
+
identifierPath,
|
|
8
|
+
emitParsedExpr,
|
|
9
|
+
emitIRNode,
|
|
10
|
+
emitAttrValue
|
|
11
|
+
} from "@barefootjs/jsx";
|
|
12
|
+
|
|
13
|
+
// src/adapter/boolean-result.ts
|
|
14
|
+
import { parseExpression } from "@barefootjs/jsx";
|
|
15
|
+
var COMPARISON_OPS = new Set([
|
|
16
|
+
"<",
|
|
17
|
+
">",
|
|
18
|
+
"<=",
|
|
19
|
+
">=",
|
|
20
|
+
"==",
|
|
21
|
+
"===",
|
|
22
|
+
"!=",
|
|
23
|
+
"!=="
|
|
24
|
+
]);
|
|
25
|
+
function isBooleanResultParsed(node) {
|
|
26
|
+
switch (node.kind) {
|
|
27
|
+
case "literal":
|
|
28
|
+
return node.literalType === "boolean";
|
|
29
|
+
case "binary":
|
|
30
|
+
return COMPARISON_OPS.has(node.op);
|
|
31
|
+
case "unary":
|
|
32
|
+
return node.op === "!";
|
|
33
|
+
case "logical":
|
|
34
|
+
return isBooleanResultParsed(node.left) && isBooleanResultParsed(node.right);
|
|
35
|
+
case "conditional":
|
|
36
|
+
return isBooleanResultParsed(node.consequent) && isBooleanResultParsed(node.alternate);
|
|
37
|
+
default:
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isBooleanResultExpr(expr) {
|
|
42
|
+
const parsed = parseExpression(expr.trim());
|
|
43
|
+
if (!parsed)
|
|
44
|
+
return false;
|
|
45
|
+
return isBooleanResultParsed(parsed);
|
|
46
|
+
}
|
|
47
|
+
var ARIA_BOOLEAN_ATTRS = new Set([
|
|
48
|
+
"aria-atomic",
|
|
49
|
+
"aria-busy",
|
|
50
|
+
"aria-disabled",
|
|
51
|
+
"aria-hidden",
|
|
52
|
+
"aria-modal",
|
|
53
|
+
"aria-multiline",
|
|
54
|
+
"aria-multiselectable",
|
|
55
|
+
"aria-readonly",
|
|
56
|
+
"aria-required",
|
|
57
|
+
"aria-checked",
|
|
58
|
+
"aria-pressed"
|
|
59
|
+
]);
|
|
60
|
+
function isAriaBooleanAttr(name) {
|
|
61
|
+
return ARIA_BOOLEAN_ATTRS.has(name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/adapter/xslate-adapter.ts
|
|
65
|
+
import { BF_SLOT, BF_COND } from "@barefootjs/shared";
|
|
66
|
+
var XSLATE_TEMPLATE_PRIMITIVES = {
|
|
67
|
+
"JSON.stringify": { arity: 1, emit: (args) => `$bf.json(${args[0]})` },
|
|
68
|
+
String: { arity: 1, emit: (args) => `$bf.string(${args[0]})` },
|
|
69
|
+
Number: { arity: 1, emit: (args) => `$bf.number(${args[0]})` },
|
|
70
|
+
"Math.floor": { arity: 1, emit: (args) => `$bf.floor(${args[0]})` },
|
|
71
|
+
"Math.ceil": { arity: 1, emit: (args) => `$bf.ceil(${args[0]})` },
|
|
72
|
+
"Math.round": { arity: 1, emit: (args) => `$bf.round(${args[0]})` }
|
|
73
|
+
};
|
|
74
|
+
var XSLATE_PRIMITIVE_EMIT_MAP = Object.fromEntries(Object.entries(XSLATE_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit]));
|
|
75
|
+
function resolveJsxChildrenProp(props) {
|
|
76
|
+
const prop = props.find((p) => p.name === "children");
|
|
77
|
+
if (!prop)
|
|
78
|
+
return [];
|
|
79
|
+
if (prop.value.kind !== "jsx-children")
|
|
80
|
+
return [];
|
|
81
|
+
return prop.value.children;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class XslateAdapter extends BaseAdapter {
|
|
85
|
+
name = "xslate";
|
|
86
|
+
extension = ".tx";
|
|
87
|
+
templatesPerComponent = true;
|
|
88
|
+
importMapInjection = "html-snippet";
|
|
89
|
+
templatePrimitives = XSLATE_PRIMITIVE_EMIT_MAP;
|
|
90
|
+
componentName = "";
|
|
91
|
+
options;
|
|
92
|
+
errors = [];
|
|
93
|
+
inLoop = false;
|
|
94
|
+
propsObjectName = null;
|
|
95
|
+
propsParams = [];
|
|
96
|
+
stringValueNames = new Set;
|
|
97
|
+
constructor(options = {}) {
|
|
98
|
+
super();
|
|
99
|
+
this.options = {
|
|
100
|
+
clientJsBasePath: options.clientJsBasePath ?? "/static/components/",
|
|
101
|
+
barefootJsPath: options.barefootJsPath ?? "/static/components/barefoot.js"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
generate(ir, options) {
|
|
105
|
+
this.componentName = ir.metadata.componentName;
|
|
106
|
+
this.propsObjectName = ir.metadata.propsObjectName ?? null;
|
|
107
|
+
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
108
|
+
this.stringValueNames = new Set;
|
|
109
|
+
for (const s of ir.metadata.signals) {
|
|
110
|
+
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
111
|
+
this.stringValueNames.add(s.getter);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const p of ir.metadata.propsParams) {
|
|
115
|
+
if (isStringTypeInfo(p.type))
|
|
116
|
+
this.stringValueNames.add(p.name);
|
|
117
|
+
}
|
|
118
|
+
this.errors = [];
|
|
119
|
+
this.childrenCaptureCounter = 0;
|
|
120
|
+
if (!options?.siblingTemplatesRegistered) {
|
|
121
|
+
this.checkImportedLoopChildComponents(ir);
|
|
122
|
+
}
|
|
123
|
+
const templateBody = ir.root.type === "if-statement" ? this.renderIfStatement(ir.root) : this.renderNode(ir.root);
|
|
124
|
+
const scriptReg = options?.skipScriptRegistration ? "" : this.generateScriptRegistrations(ir, options?.scriptBaseName);
|
|
125
|
+
const template = `${scriptReg}${templateBody}
|
|
126
|
+
`;
|
|
127
|
+
if (this.errors.length > 0) {
|
|
128
|
+
ir.errors.push(...this.errors);
|
|
129
|
+
}
|
|
130
|
+
const sections = {
|
|
131
|
+
imports: "",
|
|
132
|
+
types: "",
|
|
133
|
+
component: template,
|
|
134
|
+
defaultExport: ""
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
template,
|
|
138
|
+
sections,
|
|
139
|
+
extension: this.extension
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
generateScriptRegistrations(ir, scriptBaseName) {
|
|
143
|
+
const hasInteractivity = this.hasClientInteractivity(ir);
|
|
144
|
+
if (!hasInteractivity)
|
|
145
|
+
return "";
|
|
146
|
+
const name = scriptBaseName ?? ir.metadata.componentName;
|
|
147
|
+
const runtimePath = this.options.barefootJsPath;
|
|
148
|
+
const clientJsPath = `${this.options.clientJsBasePath}${name}.client.js`;
|
|
149
|
+
const lines = [];
|
|
150
|
+
lines.push(`: my $_bf_reg0 = $bf.register_script('${runtimePath}');`);
|
|
151
|
+
lines.push(`: my $_bf_reg1 = $bf.register_script('${clientJsPath}');`);
|
|
152
|
+
lines.push("");
|
|
153
|
+
return lines.join(`
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
hasClientInteractivity(ir) {
|
|
157
|
+
return ir.metadata.signals.length > 0 || ir.metadata.effects.length > 0 || ir.metadata.onMounts.length > 0 || (ir.metadata.clientAnalysis?.needsInit ?? false);
|
|
158
|
+
}
|
|
159
|
+
renderNode(node) {
|
|
160
|
+
return emitIRNode(node, this, {});
|
|
161
|
+
}
|
|
162
|
+
emitElement(node, _ctx, _emit) {
|
|
163
|
+
return this.renderElement(node);
|
|
164
|
+
}
|
|
165
|
+
emitText(node) {
|
|
166
|
+
return node.value;
|
|
167
|
+
}
|
|
168
|
+
emitExpression(node) {
|
|
169
|
+
return this.renderExpression(node);
|
|
170
|
+
}
|
|
171
|
+
emitConditional(node, _ctx, _emit) {
|
|
172
|
+
return this.renderConditional(node);
|
|
173
|
+
}
|
|
174
|
+
emitLoop(node, _ctx, _emit) {
|
|
175
|
+
return this.renderLoop(node);
|
|
176
|
+
}
|
|
177
|
+
emitComponent(node, _ctx, _emit) {
|
|
178
|
+
return this.renderComponent(node);
|
|
179
|
+
}
|
|
180
|
+
emitFragment(node, _ctx, _emit) {
|
|
181
|
+
return this.renderFragment(node);
|
|
182
|
+
}
|
|
183
|
+
emitSlot(node) {
|
|
184
|
+
return this.renderSlot(node);
|
|
185
|
+
}
|
|
186
|
+
emitIfStatement(node, _ctx, _emit) {
|
|
187
|
+
return this.renderIfStatement(node);
|
|
188
|
+
}
|
|
189
|
+
emitProvider(node, _ctx, _emit) {
|
|
190
|
+
return this.renderChildren(node.children);
|
|
191
|
+
}
|
|
192
|
+
emitAsync(node, _ctx, _emit) {
|
|
193
|
+
return this.renderAsync(node);
|
|
194
|
+
}
|
|
195
|
+
renderElement(element) {
|
|
196
|
+
const tag = element.tag;
|
|
197
|
+
const attrs = this.renderAttributes(element);
|
|
198
|
+
const children = this.renderChildren(element.children);
|
|
199
|
+
let hydrationAttrs = "";
|
|
200
|
+
if (element.needsScope) {
|
|
201
|
+
hydrationAttrs += ` ${this.renderScopeMarker("")}`;
|
|
202
|
+
}
|
|
203
|
+
if (element.slotId) {
|
|
204
|
+
hydrationAttrs += ` ${this.renderSlotMarker(element.slotId)}`;
|
|
205
|
+
}
|
|
206
|
+
const voidElements = [
|
|
207
|
+
"area",
|
|
208
|
+
"base",
|
|
209
|
+
"br",
|
|
210
|
+
"col",
|
|
211
|
+
"embed",
|
|
212
|
+
"hr",
|
|
213
|
+
"img",
|
|
214
|
+
"input",
|
|
215
|
+
"link",
|
|
216
|
+
"meta",
|
|
217
|
+
"param",
|
|
218
|
+
"source",
|
|
219
|
+
"track",
|
|
220
|
+
"wbr"
|
|
221
|
+
];
|
|
222
|
+
if (voidElements.includes(tag.toLowerCase())) {
|
|
223
|
+
return `<${tag}${attrs}${hydrationAttrs}>`;
|
|
224
|
+
}
|
|
225
|
+
return `<${tag}${attrs}${hydrationAttrs}>${children}</${tag}>`;
|
|
226
|
+
}
|
|
227
|
+
renderExpression(expr) {
|
|
228
|
+
if (expr.clientOnly) {
|
|
229
|
+
if (expr.slotId) {
|
|
230
|
+
return `<: $bf.comment("client:${expr.slotId}") | mark_raw :>`;
|
|
231
|
+
}
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
const perlExpr = this.convertExpressionToKolon(expr.expr);
|
|
235
|
+
if (expr.slotId) {
|
|
236
|
+
return `<: $bf.text_start("${expr.slotId}") | mark_raw :><: ${perlExpr} :><: $bf.text_end() | mark_raw :>`;
|
|
237
|
+
}
|
|
238
|
+
return `<: ${perlExpr} :>`;
|
|
239
|
+
}
|
|
240
|
+
renderConditional(cond) {
|
|
241
|
+
if (cond.clientOnly && cond.slotId) {
|
|
242
|
+
return `<: $bf.comment("cond-start:${cond.slotId}") | mark_raw :><: $bf.comment("cond-end:${cond.slotId}") | mark_raw :>`;
|
|
243
|
+
}
|
|
244
|
+
const condition = this.convertExpressionToKolon(cond.condition);
|
|
245
|
+
const whenTrue = this.renderNode(cond.whenTrue);
|
|
246
|
+
const whenFalse = this.renderNodeOrNull(cond.whenFalse);
|
|
247
|
+
const isFragmentBranch = cond.whenTrue.type === "fragment" || cond.whenFalse.type === "fragment";
|
|
248
|
+
const useCommentMarkers = cond.slotId && isFragmentBranch;
|
|
249
|
+
let markedTrue = whenTrue;
|
|
250
|
+
let markedFalse = whenFalse;
|
|
251
|
+
if (cond.slotId && !useCommentMarkers) {
|
|
252
|
+
markedTrue = this.addCondMarkerToFirstElement(whenTrue, cond.slotId);
|
|
253
|
+
markedFalse = whenFalse ? this.addCondMarkerToFirstElement(whenFalse, cond.slotId) : whenFalse;
|
|
254
|
+
}
|
|
255
|
+
let result;
|
|
256
|
+
if (useCommentMarkers) {
|
|
257
|
+
const inner = whenFalse ? `
|
|
258
|
+
: if (${condition}) {
|
|
259
|
+
${whenTrue}
|
|
260
|
+
: } else {
|
|
261
|
+
${whenFalse}
|
|
262
|
+
: }
|
|
263
|
+
` : `
|
|
264
|
+
: if (${condition}) {
|
|
265
|
+
${whenTrue}
|
|
266
|
+
: }
|
|
267
|
+
`;
|
|
268
|
+
result = `<: $bf.comment("cond-start:${cond.slotId}") | mark_raw :>${inner}<: $bf.comment("cond-end:${cond.slotId}") | mark_raw :>`;
|
|
269
|
+
} else if (markedFalse) {
|
|
270
|
+
result = `
|
|
271
|
+
: if (${condition}) {
|
|
272
|
+
${markedTrue}
|
|
273
|
+
: } else {
|
|
274
|
+
${markedFalse}
|
|
275
|
+
: }
|
|
276
|
+
`;
|
|
277
|
+
} else if (cond.slotId) {
|
|
278
|
+
result = `<: $bf.comment("cond-start:${cond.slotId}") | mark_raw :>
|
|
279
|
+
: if (${condition}) {
|
|
280
|
+
${whenTrue}
|
|
281
|
+
: }
|
|
282
|
+
<: $bf.comment("cond-end:${cond.slotId}") | mark_raw :>`;
|
|
283
|
+
} else {
|
|
284
|
+
result = `
|
|
285
|
+
: if (${condition}) {
|
|
286
|
+
${whenTrue}
|
|
287
|
+
: }
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
renderNodeOrNull(node) {
|
|
293
|
+
if (node.type === "expression" && (node.expr === "null" || node.expr === "undefined")) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return this.renderNode(node);
|
|
297
|
+
}
|
|
298
|
+
addCondMarkerToFirstElement(content, condId) {
|
|
299
|
+
const match = content.match(/^(<\w+)([\s>])/);
|
|
300
|
+
if (match) {
|
|
301
|
+
return content.replace(/^(<\w+)([\s>])/, `$1 ${BF_COND}="${condId}"$2`);
|
|
302
|
+
}
|
|
303
|
+
return `<: $bf.comment("cond-start:${condId}") | mark_raw :>${content}<: $bf.comment("cond-end:${condId}") | mark_raw :>`;
|
|
304
|
+
}
|
|
305
|
+
checkImportedLoopChildComponents(ir) {
|
|
306
|
+
const relativeImports = new Set;
|
|
307
|
+
for (const imp of ir.metadata.templateImports ?? ir.metadata.imports ?? []) {
|
|
308
|
+
if (!imp.source.startsWith("./") && !imp.source.startsWith("../"))
|
|
309
|
+
continue;
|
|
310
|
+
if (imp.isTypeOnly)
|
|
311
|
+
continue;
|
|
312
|
+
for (const spec of imp.specifiers) {
|
|
313
|
+
relativeImports.add(spec.alias ?? spec.name);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (relativeImports.size === 0)
|
|
317
|
+
return;
|
|
318
|
+
const loc = { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } };
|
|
319
|
+
const visit = (node, inLoop) => {
|
|
320
|
+
switch (node.type) {
|
|
321
|
+
case "component": {
|
|
322
|
+
const comp = node;
|
|
323
|
+
if (inLoop && relativeImports.has(comp.name)) {
|
|
324
|
+
this.errors.push({
|
|
325
|
+
code: "BF103",
|
|
326
|
+
severity: "error",
|
|
327
|
+
message: `Component <${comp.name}> is imported from a sibling module and used inside a loop. The Xslate adapter emits a cross-template call; the child template must be registered alongside the parent at render time.`,
|
|
328
|
+
loc: comp.loc ?? loc,
|
|
329
|
+
suggestion: {
|
|
330
|
+
message: `Options:
|
|
331
|
+
` + ` 1. Compile '${comp.name}' (its source file) with the same adapter and register the resulting Xslate template alongside the parent at render time.
|
|
332
|
+
` + ` 2. Inline <${comp.name}> directly inside the loop body so no cross-file template lookup is needed.
|
|
333
|
+
` + ` 3. Mark the loop position as @client-only so the template is materialised on the client instead of at SSR time.`
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
for (const child of comp.children)
|
|
338
|
+
visit(child, inLoop);
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
case "element":
|
|
342
|
+
for (const child of node.children)
|
|
343
|
+
visit(child, inLoop);
|
|
344
|
+
break;
|
|
345
|
+
case "fragment":
|
|
346
|
+
for (const child of node.children)
|
|
347
|
+
visit(child, inLoop);
|
|
348
|
+
break;
|
|
349
|
+
case "conditional": {
|
|
350
|
+
const cond = node;
|
|
351
|
+
visit(cond.whenTrue, inLoop);
|
|
352
|
+
if (cond.whenFalse)
|
|
353
|
+
visit(cond.whenFalse, inLoop);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case "loop":
|
|
357
|
+
for (const child of node.children)
|
|
358
|
+
visit(child, true);
|
|
359
|
+
break;
|
|
360
|
+
case "if-statement": {
|
|
361
|
+
const stmt = node;
|
|
362
|
+
visit(stmt.consequent, inLoop);
|
|
363
|
+
if (stmt.alternate)
|
|
364
|
+
visit(stmt.alternate, inLoop);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "provider":
|
|
368
|
+
for (const child of node.children)
|
|
369
|
+
visit(child, inLoop);
|
|
370
|
+
break;
|
|
371
|
+
case "async": {
|
|
372
|
+
const a = node;
|
|
373
|
+
visit(a.fallback, inLoop);
|
|
374
|
+
for (const child of a.children)
|
|
375
|
+
visit(child, inLoop);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
visit(ir.root, false);
|
|
381
|
+
}
|
|
382
|
+
renderLoop(loop) {
|
|
383
|
+
if (loop.clientOnly)
|
|
384
|
+
return "";
|
|
385
|
+
if (loop.paramBindings && loop.paramBindings.length > 0) {
|
|
386
|
+
this.errors.push({
|
|
387
|
+
code: "BF104",
|
|
388
|
+
severity: "error",
|
|
389
|
+
message: `Loop callback uses an array/object destructure pattern (\`${loop.param}\`) that the Xslate adapter cannot lower — Kolon \`for LIST -> $item\` binds a single scalar and can't unpack a tuple.`,
|
|
390
|
+
loc: loop.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
391
|
+
suggestion: {
|
|
392
|
+
message: `Options:
|
|
393
|
+
` + ` 1. Rename the parameter to a single name and access tuple elements with index syntax in the body (e.g. \`entry => entry[0]\` instead of \`([k, v]) => ...\`).
|
|
394
|
+
` + ` 2. Mark the loop position as @client-only so the destructure runs in JS on the client.
|
|
395
|
+
` + ` 3. Move the loop into a primitive that the adapter registers explicitly.`
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const rawArray = this.convertExpressionToKolon(loop.array);
|
|
400
|
+
let array = rawArray;
|
|
401
|
+
if (loop.sortComparator) {
|
|
402
|
+
array = renderSortMethod(rawArray, loop.sortComparator);
|
|
403
|
+
}
|
|
404
|
+
const param = loop.param;
|
|
405
|
+
const renderedChildren = this.renderChildren(loop.children);
|
|
406
|
+
const loopVar = loop.iterationShape === "keys" ? "__bf_item" : param;
|
|
407
|
+
const indexLocalLines = [];
|
|
408
|
+
if (loop.iterationShape === "keys") {
|
|
409
|
+
indexLocalLines.push(`: my $${param} = $~${loopVar}.index;`);
|
|
410
|
+
} else if (loop.index) {
|
|
411
|
+
indexLocalLines.push(`: my $${loop.index} = $~${loopVar}.index;`);
|
|
412
|
+
}
|
|
413
|
+
const prevInLoop = this.inLoop;
|
|
414
|
+
this.inLoop = true;
|
|
415
|
+
const childrenUnderLoop = this.renderChildren(loop.children);
|
|
416
|
+
this.inLoop = prevInLoop;
|
|
417
|
+
const bodyChildren = loop.bodyIsItemConditional && loop.key ? `<: $bf.comment("loop-i:" ~ ${this.convertExpressionToKolon(loop.key)}) | mark_raw :>
|
|
418
|
+
${childrenUnderLoop}` : childrenUnderLoop;
|
|
419
|
+
const lines = [];
|
|
420
|
+
lines.push(`<: $bf.comment("loop:${loop.markerId}") | mark_raw :>`);
|
|
421
|
+
lines.push(`: for ${array} -> $${loopVar} {`);
|
|
422
|
+
for (const il of indexLocalLines)
|
|
423
|
+
lines.push(il);
|
|
424
|
+
if (loop.filterPredicate) {
|
|
425
|
+
let filterCond;
|
|
426
|
+
if (loop.filterPredicate.blockBody) {
|
|
427
|
+
filterCond = this.renderBlockBodyCondition(loop.filterPredicate.blockBody, loop.filterPredicate.param);
|
|
428
|
+
} else if (loop.filterPredicate.predicate) {
|
|
429
|
+
filterCond = this.renderKolonFilterExpr(loop.filterPredicate.predicate, loop.filterPredicate.param);
|
|
430
|
+
} else {
|
|
431
|
+
filterCond = "1";
|
|
432
|
+
}
|
|
433
|
+
if (loop.filterPredicate.param !== param) {
|
|
434
|
+
filterCond = filterCond.replace(new RegExp(`\\$${loop.filterPredicate.param}\\b`, "g"), `$${param}`);
|
|
435
|
+
}
|
|
436
|
+
lines.push(`: if (${filterCond}) {`);
|
|
437
|
+
lines.push(bodyChildren);
|
|
438
|
+
lines.push(`: }`);
|
|
439
|
+
} else {
|
|
440
|
+
lines.push(bodyChildren);
|
|
441
|
+
}
|
|
442
|
+
lines.push(`: }`);
|
|
443
|
+
lines.push(`<: $bf.comment("/loop:${loop.markerId}") | mark_raw :>`);
|
|
444
|
+
return lines.join(`
|
|
445
|
+
`);
|
|
446
|
+
}
|
|
447
|
+
componentPropEmitter = {
|
|
448
|
+
emitLiteral: (value, name) => `${name} => '${value.value}'`,
|
|
449
|
+
emitExpression: (value, name) => {
|
|
450
|
+
if (value.parts) {
|
|
451
|
+
return `${name} => ${this.convertTemplateLiteralPartsToKolon(value.parts)}`;
|
|
452
|
+
}
|
|
453
|
+
return `${name} => ${this.convertExpressionToKolon(value.expr)}`;
|
|
454
|
+
},
|
|
455
|
+
emitSpread: (value) => {
|
|
456
|
+
return this.convertExpressionToKolon(value.expr);
|
|
457
|
+
},
|
|
458
|
+
emitTemplate: (value, name) => `${name} => ${this.convertTemplateLiteralPartsToKolon(value.parts)}`,
|
|
459
|
+
emitBooleanAttr: (_value, name) => `${name} => 1`,
|
|
460
|
+
emitBooleanShorthand: (_value, name) => `${name} => 1`,
|
|
461
|
+
emitJsxChildren: () => ""
|
|
462
|
+
};
|
|
463
|
+
renderComponent(comp) {
|
|
464
|
+
const propParts = [];
|
|
465
|
+
for (const p of comp.props) {
|
|
466
|
+
if (p.name.match(/^on[A-Z]/) && p.value.kind === "expression")
|
|
467
|
+
continue;
|
|
468
|
+
if (p.value.kind === "spread") {
|
|
469
|
+
const trimmed = p.value.expr.trim();
|
|
470
|
+
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
471
|
+
for (const pp of this.propsParams) {
|
|
472
|
+
propParts.push(`${pp.name} => $${pp.name}`);
|
|
473
|
+
}
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
this.errors.push({
|
|
477
|
+
code: "BF101",
|
|
478
|
+
severity: "error",
|
|
479
|
+
message: `Spread props (\`{...${trimmed}}\`) on a child component cannot be lowered to Kolon — Kolon hashref method args can't splat a runtime hash into named entries.`,
|
|
480
|
+
loc: comp.loc ?? { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
481
|
+
suggestion: {
|
|
482
|
+
message: "Pass the child component its props explicitly rather than spreading a runtime object."
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const lowered = emitAttrValue(p.value, this.componentPropEmitter, p.name);
|
|
488
|
+
if (lowered)
|
|
489
|
+
propParts.push(lowered);
|
|
490
|
+
}
|
|
491
|
+
if (comp.slotId && !this.inLoop) {
|
|
492
|
+
propParts.push(`_bf_slot => '${comp.slotId}'`);
|
|
493
|
+
}
|
|
494
|
+
const tplName = this.toTemplateName(comp.name);
|
|
495
|
+
const effectiveChildren = comp.children.length > 0 ? comp.children : resolveJsxChildrenProp(comp.props);
|
|
496
|
+
if (effectiveChildren.length > 0) {
|
|
497
|
+
const childrenBody = this.renderChildren(effectiveChildren);
|
|
498
|
+
const macroName = `bf_children_${comp.slotId ?? "c" + this.childrenCaptureCounter++}`;
|
|
499
|
+
const childrenEntry = `children => ${macroName}()`;
|
|
500
|
+
const allParts = [...propParts, childrenEntry];
|
|
501
|
+
return `<: macro ${macroName} -> () { :>${childrenBody}<: } :><: $bf.render_child('${tplName}', { ${allParts.join(", ")} }) | mark_raw :>`;
|
|
502
|
+
}
|
|
503
|
+
const hashEntries = propParts.length > 0 ? `, { ${propParts.join(", ")} }` : "";
|
|
504
|
+
return `<: $bf.render_child('${tplName}'${hashEntries}) | mark_raw :>`;
|
|
505
|
+
}
|
|
506
|
+
childrenCaptureCounter = 0;
|
|
507
|
+
toTemplateName(componentName) {
|
|
508
|
+
return componentName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
509
|
+
}
|
|
510
|
+
renderIfStatement(ifStmt) {
|
|
511
|
+
const condition = this.convertExpressionToKolon(ifStmt.condition);
|
|
512
|
+
const consequent = ifStmt.consequent.type === "if-statement" ? this.renderIfStatement(ifStmt.consequent) : this.renderNode(ifStmt.consequent);
|
|
513
|
+
let result = `: if (${condition}) {
|
|
514
|
+
${consequent}
|
|
515
|
+
`;
|
|
516
|
+
if (ifStmt.alternate) {
|
|
517
|
+
if (ifStmt.alternate.type === "if-statement") {
|
|
518
|
+
const altResult = this.renderIfStatement(ifStmt.alternate);
|
|
519
|
+
result += altResult.replace(/^: if/, ": } elsif");
|
|
520
|
+
} else {
|
|
521
|
+
const alternate = this.renderNode(ifStmt.alternate);
|
|
522
|
+
result += `: } else {
|
|
523
|
+
${alternate}
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
result += `: }`;
|
|
528
|
+
return result;
|
|
529
|
+
}
|
|
530
|
+
renderFragment(fragment) {
|
|
531
|
+
const children = this.renderChildren(fragment.children);
|
|
532
|
+
if (fragment.needsScopeComment) {
|
|
533
|
+
return `<: $bf.scope_comment() | mark_raw :>${children}`;
|
|
534
|
+
}
|
|
535
|
+
return children;
|
|
536
|
+
}
|
|
537
|
+
renderSlot(_slot) {
|
|
538
|
+
return `<: $children | mark_raw :>`;
|
|
539
|
+
}
|
|
540
|
+
renderAsync(node) {
|
|
541
|
+
const fallback = this.renderNode(node.fallback);
|
|
542
|
+
const children = this.renderChildren(node.children);
|
|
543
|
+
const macroName = `bf_async_fallback_${node.id}`;
|
|
544
|
+
return `<: macro ${macroName} -> () { :>${fallback}<: } :><: $bf.async_boundary('${node.id}', ${macroName}()) | mark_raw :>
|
|
545
|
+
${children}`;
|
|
546
|
+
}
|
|
547
|
+
elementAttrEmitter = {
|
|
548
|
+
emitLiteral: (value, name) => `${name}="${value.value}"`,
|
|
549
|
+
emitExpression: (value, name) => {
|
|
550
|
+
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
551
|
+
return "";
|
|
552
|
+
}
|
|
553
|
+
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
554
|
+
return `<: ${this.convertExpressionToKolon(value.expr)} ? '${name}' : '' :>`;
|
|
555
|
+
}
|
|
556
|
+
const perl = this.convertExpressionToKolon(value.expr);
|
|
557
|
+
if (isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name)) {
|
|
558
|
+
return `${name}="<: $bf.bool_str(${perl}) :>"`;
|
|
559
|
+
}
|
|
560
|
+
return `${name}="<: ${perl} :>"`;
|
|
561
|
+
},
|
|
562
|
+
emitBooleanAttr: (_value, name) => name,
|
|
563
|
+
emitTemplate: (value, name) => `${name}="<: ${this.convertTemplateLiteralPartsToKolon(value.parts)} :>"`,
|
|
564
|
+
emitSpread: (value) => {
|
|
565
|
+
if (this.refuseUnsupportedAttrExpression(value.expr, "...")) {
|
|
566
|
+
return "";
|
|
567
|
+
}
|
|
568
|
+
const trimmed = value.expr.trim();
|
|
569
|
+
if (this.propsObjectName && this.propsObjectName === trimmed) {
|
|
570
|
+
const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
|
|
571
|
+
return `<: $bf.spread_attrs({${entries.join(", ")}}) | mark_raw :>`;
|
|
572
|
+
}
|
|
573
|
+
const perlExpr = this.convertExpressionToKolon(value.expr);
|
|
574
|
+
return `<: $bf.spread_attrs(${perlExpr}) | mark_raw :>`;
|
|
575
|
+
},
|
|
576
|
+
emitBooleanShorthand: () => "",
|
|
577
|
+
emitJsxChildren: () => ""
|
|
578
|
+
};
|
|
579
|
+
renderAttributes(element) {
|
|
580
|
+
const parts = [];
|
|
581
|
+
for (const attr of element.attrs) {
|
|
582
|
+
let attrName;
|
|
583
|
+
if (attr.name === "className")
|
|
584
|
+
attrName = "class";
|
|
585
|
+
else if (attr.name === "key")
|
|
586
|
+
attrName = "data-key";
|
|
587
|
+
else
|
|
588
|
+
attrName = attr.name;
|
|
589
|
+
const lowered = emitAttrValue(attr.value, this.elementAttrEmitter, attrName);
|
|
590
|
+
if (lowered)
|
|
591
|
+
parts.push(lowered);
|
|
592
|
+
}
|
|
593
|
+
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
594
|
+
}
|
|
595
|
+
renderScopeMarker(_instanceIdExpr) {
|
|
596
|
+
return `bf-s="<: $bf.scope_attr() :>" <: $bf.hydration_attrs() | mark_raw :> <: $bf.props_attr() | mark_raw :>`;
|
|
597
|
+
}
|
|
598
|
+
renderSlotMarker(slotId) {
|
|
599
|
+
return `${BF_SLOT}="${slotId}"`;
|
|
600
|
+
}
|
|
601
|
+
renderCondMarker(condId) {
|
|
602
|
+
return `${BF_COND}="${condId}"`;
|
|
603
|
+
}
|
|
604
|
+
renderKolonFilterExpr(expr, param, localVarMap = new Map) {
|
|
605
|
+
return emitParsedExpr(expr, new XslateFilterEmitter(param, localVarMap, (n) => this._isStringValueName(n)));
|
|
606
|
+
}
|
|
607
|
+
renderBlockBodyCondition(statements, param) {
|
|
608
|
+
const localVarMap = new Map;
|
|
609
|
+
const paths = this.collectReturnPaths(statements, [], localVarMap, param);
|
|
610
|
+
if (paths.length === 0)
|
|
611
|
+
return "1";
|
|
612
|
+
if (paths.length === 1)
|
|
613
|
+
return this.buildSinglePathCondition(paths[0], param, localVarMap);
|
|
614
|
+
const parts = [];
|
|
615
|
+
for (const path of paths) {
|
|
616
|
+
if (path.result.kind === "literal" && path.result.literalType === "boolean" && path.result.value === false)
|
|
617
|
+
continue;
|
|
618
|
+
const cond = this.buildSinglePathCondition(path, param, localVarMap);
|
|
619
|
+
if (cond !== "0")
|
|
620
|
+
parts.push(cond);
|
|
621
|
+
}
|
|
622
|
+
if (parts.length === 0)
|
|
623
|
+
return "0";
|
|
624
|
+
if (parts.length === 1)
|
|
625
|
+
return parts[0];
|
|
626
|
+
return `(${parts.join(" || ")})`;
|
|
627
|
+
}
|
|
628
|
+
collectReturnPaths(statements, currentConditions, localVarMap, param) {
|
|
629
|
+
const paths = [];
|
|
630
|
+
for (const stmt of statements) {
|
|
631
|
+
if (stmt.kind === "var-decl") {
|
|
632
|
+
if (stmt.init.kind === "call" && stmt.init.callee.kind === "identifier") {
|
|
633
|
+
localVarMap.set(stmt.name, stmt.init.callee.name);
|
|
634
|
+
}
|
|
635
|
+
} else if (stmt.kind === "return") {
|
|
636
|
+
paths.push({ conditions: [...currentConditions], result: stmt.value });
|
|
637
|
+
break;
|
|
638
|
+
} else if (stmt.kind === "if") {
|
|
639
|
+
const thenPaths = this.collectReturnPaths(stmt.consequent, [...currentConditions, stmt.condition], localVarMap, param);
|
|
640
|
+
paths.push(...thenPaths);
|
|
641
|
+
if (stmt.alternate) {
|
|
642
|
+
const negated = { kind: "unary", op: "!", argument: stmt.condition };
|
|
643
|
+
const elsePaths = this.collectReturnPaths(stmt.alternate, [...currentConditions, negated], localVarMap, param);
|
|
644
|
+
paths.push(...elsePaths);
|
|
645
|
+
} else {
|
|
646
|
+
currentConditions.push({ kind: "unary", op: "!", argument: stmt.condition });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return paths;
|
|
651
|
+
}
|
|
652
|
+
buildSinglePathCondition(path, param, localVarMap) {
|
|
653
|
+
if (path.result.kind === "literal" && path.result.literalType === "boolean") {
|
|
654
|
+
if (path.result.value === true) {
|
|
655
|
+
if (path.conditions.length === 0)
|
|
656
|
+
return "1";
|
|
657
|
+
return this.renderConditionsAnd(path.conditions, param, localVarMap);
|
|
658
|
+
}
|
|
659
|
+
return "0";
|
|
660
|
+
}
|
|
661
|
+
if (path.conditions.length === 0) {
|
|
662
|
+
return this.renderKolonFilterExpr(path.result, param, localVarMap);
|
|
663
|
+
}
|
|
664
|
+
const condPart = this.renderConditionsAnd(path.conditions, param, localVarMap);
|
|
665
|
+
const resultPart = this.renderKolonFilterExpr(path.result, param, localVarMap);
|
|
666
|
+
return `(${condPart} && ${resultPart})`;
|
|
667
|
+
}
|
|
668
|
+
renderConditionsAnd(conditions, param, localVarMap) {
|
|
669
|
+
if (conditions.length === 0)
|
|
670
|
+
return "1";
|
|
671
|
+
if (conditions.length === 1)
|
|
672
|
+
return this.renderKolonFilterExpr(conditions[0], param, localVarMap);
|
|
673
|
+
const parts = conditions.map((c) => this.renderKolonFilterExpr(c, param, localVarMap));
|
|
674
|
+
return `(${parts.join(" && ")})`;
|
|
675
|
+
}
|
|
676
|
+
convertTemplateLiteralPartsToKolon(literalParts) {
|
|
677
|
+
const parts = [];
|
|
678
|
+
for (const part of literalParts) {
|
|
679
|
+
if (part.type === "string") {
|
|
680
|
+
parts.push(this.substituteJsInterpolationsToKolon(part.value));
|
|
681
|
+
} else if (part.type === "ternary") {
|
|
682
|
+
const cond = this.convertExpressionToKolon(part.condition);
|
|
683
|
+
parts.push(`(${cond} ? '${part.whenTrue}' : '${part.whenFalse}')`);
|
|
684
|
+
} else if (part.type === "lookup") {
|
|
685
|
+
const keyExpr = this.convertExpressionToKolon(part.key);
|
|
686
|
+
const entries = Object.entries(part.cases).map(([k, v]) => `'${k}' => '${v}'`).join(", ");
|
|
687
|
+
parts.push(`({ ${entries} }[${keyExpr}] // '')`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return parts.length === 1 ? parts[0] : parts.join(" ~ ");
|
|
691
|
+
}
|
|
692
|
+
substituteJsInterpolationsToKolon(s) {
|
|
693
|
+
const segments = [];
|
|
694
|
+
const re = /\$\{([^}]+)\}/g;
|
|
695
|
+
let lastIndex = 0;
|
|
696
|
+
let m;
|
|
697
|
+
while ((m = re.exec(s)) !== null) {
|
|
698
|
+
if (m.index > lastIndex) {
|
|
699
|
+
segments.push(`'${s.slice(lastIndex, m.index)}'`);
|
|
700
|
+
}
|
|
701
|
+
segments.push(this.convertExpressionToKolon(m[1].trim()));
|
|
702
|
+
lastIndex = re.lastIndex;
|
|
703
|
+
}
|
|
704
|
+
if (lastIndex < s.length) {
|
|
705
|
+
segments.push(`'${s.slice(lastIndex)}'`);
|
|
706
|
+
}
|
|
707
|
+
if (segments.length === 0)
|
|
708
|
+
return `''`;
|
|
709
|
+
return segments.length === 1 ? segments[0] : `(${segments.join(" ~ ")})`;
|
|
710
|
+
}
|
|
711
|
+
refuseUnsupportedAttrExpression(expr, attrName) {
|
|
712
|
+
let probe = expr.trim();
|
|
713
|
+
while (probe.startsWith("("))
|
|
714
|
+
probe = probe.slice(1).trimStart();
|
|
715
|
+
const startsAsObjectLiteral = probe.startsWith("{");
|
|
716
|
+
const hasTaggedTemplate = /[A-Za-z_$][\w$]*\s*`/.test(probe);
|
|
717
|
+
if (!startsAsObjectLiteral && !hasTaggedTemplate)
|
|
718
|
+
return false;
|
|
719
|
+
const parsed = parseExpression2(expr.trim());
|
|
720
|
+
const support = isSupported(parsed);
|
|
721
|
+
if (parsed.kind !== "unsupported" && support.supported)
|
|
722
|
+
return false;
|
|
723
|
+
const reason = support.reason ?? (parsed.kind === "unsupported" ? parsed.reason : undefined);
|
|
724
|
+
const reasonLine = reason ? `
|
|
725
|
+
${reason}` : "";
|
|
726
|
+
this.errors.push({
|
|
727
|
+
code: "BF101",
|
|
728
|
+
severity: "error",
|
|
729
|
+
message: `Expression not supported on attribute '${attrName}': ${expr.trim()}${reasonLine}`,
|
|
730
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
731
|
+
suggestion: {
|
|
732
|
+
message: "The Xslate adapter cannot lower JS object literals or tagged-template-literal expressions into Kolon. Move the expression into a `'use client'` component (so hydration computes it), or expand it into discrete attributes whose values are values the adapter can lower."
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
convertExpressionToKolon(expr) {
|
|
738
|
+
const trimmed = expr.trim();
|
|
739
|
+
if (trimmed === "")
|
|
740
|
+
return "''";
|
|
741
|
+
const parsed = parseExpression2(trimmed);
|
|
742
|
+
const support = isSupported(parsed);
|
|
743
|
+
if (!support.supported) {
|
|
744
|
+
this.errors.push({
|
|
745
|
+
code: "BF101",
|
|
746
|
+
severity: "error",
|
|
747
|
+
message: `Expression not supported: ${trimmed}`,
|
|
748
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
749
|
+
suggestion: {
|
|
750
|
+
message: support.reason ? `${support.reason}
|
|
751
|
+
|
|
752
|
+
Options:
|
|
753
|
+
1. Use /* @client */ for client-side evaluation
|
|
754
|
+
2. Pre-compute the value in the backend` : `Options:
|
|
755
|
+
1. Use /* @client */ for client-side evaluation
|
|
756
|
+
2. Pre-compute the value in the backend`
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
return "''";
|
|
760
|
+
}
|
|
761
|
+
return this.renderParsedExprToKolon(parsed);
|
|
762
|
+
}
|
|
763
|
+
renderParsedExprToKolon(expr) {
|
|
764
|
+
return emitParsedExpr(expr, new XslateTopLevelEmitter(this));
|
|
765
|
+
}
|
|
766
|
+
_isStringValueName(name) {
|
|
767
|
+
return this.stringValueNames.has(name);
|
|
768
|
+
}
|
|
769
|
+
_recordExprBF101(message, reason) {
|
|
770
|
+
this.errors.push({
|
|
771
|
+
code: "BF101",
|
|
772
|
+
severity: "error",
|
|
773
|
+
message,
|
|
774
|
+
loc: { file: this.componentName + ".tsx", start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
775
|
+
suggestion: {
|
|
776
|
+
message: reason ? `${reason}
|
|
777
|
+
|
|
778
|
+
Options:
|
|
779
|
+
1. Use /* @client */ for client-side evaluation
|
|
780
|
+
2. Pre-compute the value in the backend` : `Options:
|
|
781
|
+
1. Use /* @client */ for client-side evaluation
|
|
782
|
+
2. Pre-compute the value in the backend`
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
_renderKolonFilterExprPublic(expr, param) {
|
|
787
|
+
return this.renderKolonFilterExpr(expr, param);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function renderArrayMethod(method, object, args, emit) {
|
|
791
|
+
switch (method) {
|
|
792
|
+
case "join": {
|
|
793
|
+
const obj = emit(object);
|
|
794
|
+
const sep = args.length >= 1 ? emit(args[0]) : `','`;
|
|
795
|
+
return `$bf.join(${obj}, ${sep})`;
|
|
796
|
+
}
|
|
797
|
+
case "includes": {
|
|
798
|
+
const obj = emit(object);
|
|
799
|
+
const needle = emit(args[0]);
|
|
800
|
+
return `$bf.includes(${obj}, ${needle})`;
|
|
801
|
+
}
|
|
802
|
+
case "indexOf":
|
|
803
|
+
case "lastIndexOf": {
|
|
804
|
+
const fn = method === "indexOf" ? "index_of" : "last_index_of";
|
|
805
|
+
const obj = emit(object);
|
|
806
|
+
const needle = emit(args[0]);
|
|
807
|
+
return `$bf.${fn}(${obj}, ${needle})`;
|
|
808
|
+
}
|
|
809
|
+
case "at": {
|
|
810
|
+
const obj = emit(object);
|
|
811
|
+
const idx = args.length >= 1 ? emit(args[0]) : "0";
|
|
812
|
+
return `$bf.at(${obj}, ${idx})`;
|
|
813
|
+
}
|
|
814
|
+
case "concat": {
|
|
815
|
+
if (args.length === 0) {
|
|
816
|
+
return emit(object);
|
|
817
|
+
}
|
|
818
|
+
const a = emit(object);
|
|
819
|
+
const b = emit(args[0]);
|
|
820
|
+
return `$bf.concat(${a}, ${b})`;
|
|
821
|
+
}
|
|
822
|
+
case "slice": {
|
|
823
|
+
const recv = emit(object);
|
|
824
|
+
const start = args.length >= 1 ? emit(args[0]) : "0";
|
|
825
|
+
const end = args.length >= 2 ? emit(args[1]) : "undef";
|
|
826
|
+
return `$bf.slice(${recv}, ${start}, ${end})`;
|
|
827
|
+
}
|
|
828
|
+
case "reverse":
|
|
829
|
+
case "toReversed": {
|
|
830
|
+
const recv = emit(object);
|
|
831
|
+
return `$bf.reverse(${recv})`;
|
|
832
|
+
}
|
|
833
|
+
case "toLowerCase": {
|
|
834
|
+
const recv = emit(object);
|
|
835
|
+
return `$bf.lc(${recv})`;
|
|
836
|
+
}
|
|
837
|
+
case "toUpperCase": {
|
|
838
|
+
const recv = emit(object);
|
|
839
|
+
return `$bf.uc(${recv})`;
|
|
840
|
+
}
|
|
841
|
+
case "trim": {
|
|
842
|
+
const recv = emit(object);
|
|
843
|
+
return `$bf.trim(${recv})`;
|
|
844
|
+
}
|
|
845
|
+
case "split": {
|
|
846
|
+
const recv = emit(object);
|
|
847
|
+
if (args.length === 0) {
|
|
848
|
+
return `$bf.split(${recv})`;
|
|
849
|
+
}
|
|
850
|
+
const sep = emit(args[0]);
|
|
851
|
+
if (args.length === 1) {
|
|
852
|
+
return `$bf.split(${recv}, ${sep})`;
|
|
853
|
+
}
|
|
854
|
+
const limit = emit(args[1]);
|
|
855
|
+
return `$bf.split(${recv}, ${sep}, ${limit})`;
|
|
856
|
+
}
|
|
857
|
+
case "startsWith":
|
|
858
|
+
case "endsWith": {
|
|
859
|
+
const fn = method === "startsWith" ? "starts_with" : "ends_with";
|
|
860
|
+
const recv = emit(object);
|
|
861
|
+
const arg = emit(args[0]);
|
|
862
|
+
if (args.length >= 2) {
|
|
863
|
+
return `$bf.${fn}(${recv}, ${arg}, ${emit(args[1])})`;
|
|
864
|
+
}
|
|
865
|
+
return `$bf.${fn}(${recv}, ${arg})`;
|
|
866
|
+
}
|
|
867
|
+
case "replace": {
|
|
868
|
+
const recv = emit(object);
|
|
869
|
+
const oldS = emit(args[0]);
|
|
870
|
+
const newS = emit(args[1]);
|
|
871
|
+
return `$bf.replace(${recv}, ${oldS}, ${newS})`;
|
|
872
|
+
}
|
|
873
|
+
case "repeat": {
|
|
874
|
+
const recv = emit(object);
|
|
875
|
+
const count = args.length === 0 ? "0" : emit(args[0]);
|
|
876
|
+
return `$bf.repeat(${recv}, ${count})`;
|
|
877
|
+
}
|
|
878
|
+
case "padStart":
|
|
879
|
+
case "padEnd": {
|
|
880
|
+
const fn = method === "padStart" ? "pad_start" : "pad_end";
|
|
881
|
+
const recv = emit(object);
|
|
882
|
+
if (args.length === 0) {
|
|
883
|
+
return `$bf.${fn}(${recv}, 0)`;
|
|
884
|
+
}
|
|
885
|
+
const target = emit(args[0]);
|
|
886
|
+
if (args.length === 1) {
|
|
887
|
+
return `$bf.${fn}(${recv}, ${target})`;
|
|
888
|
+
}
|
|
889
|
+
const pad = emit(args[1]);
|
|
890
|
+
return `$bf.${fn}(${recv}, ${target}, ${pad})`;
|
|
891
|
+
}
|
|
892
|
+
default: {
|
|
893
|
+
const _exhaustive = method;
|
|
894
|
+
throw new Error(`renderArrayMethod: unhandled ArrayMethod '${_exhaustive}'`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function renderSortMethod(recv, c) {
|
|
899
|
+
const keyHashes = c.keys.map((k) => {
|
|
900
|
+
const keyEntry = k.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${k.key.field}'`;
|
|
901
|
+
return `{ ${keyEntry}, compare_type => '${k.type}', direction => '${k.direction}' }`;
|
|
902
|
+
});
|
|
903
|
+
return `$bf.sort(${recv}, { keys => [${keyHashes.join(", ")}] })`;
|
|
904
|
+
}
|
|
905
|
+
function renderReduceMethod(recv, op, direction) {
|
|
906
|
+
const keyEntry = op.key.kind === "self" ? `key_kind => 'self'` : `key_kind => 'field', key => '${op.key.field}'`;
|
|
907
|
+
const init = op.type === "string" ? `'${op.init.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'` : op.init;
|
|
908
|
+
return `$bf.reduce(${recv}, { op => '${op.op}', ${keyEntry}, type => '${op.type}', init => ${init}, direction => '${direction}' })`;
|
|
909
|
+
}
|
|
910
|
+
function renderFlatMethod(recv, depth) {
|
|
911
|
+
const d = depth === "infinity" ? -1 : depth;
|
|
912
|
+
return `$bf.flat(${recv}, ${d})`;
|
|
913
|
+
}
|
|
914
|
+
function renderFlatMapMethod(recv, op) {
|
|
915
|
+
const proj = op.projection;
|
|
916
|
+
if (proj.kind === "tuple") {
|
|
917
|
+
const specs = proj.elements.map((l) => l.kind === "self" ? `['self', '']` : `['field', '${l.field}']`).join(", ");
|
|
918
|
+
return `$bf.flat_map_tuple(${recv}, ${specs})`;
|
|
919
|
+
}
|
|
920
|
+
if (proj.kind === "self")
|
|
921
|
+
return `$bf.flat_map(${recv}, 'self', '')`;
|
|
922
|
+
return `$bf.flat_map(${recv}, 'field', '${proj.field}')`;
|
|
923
|
+
}
|
|
924
|
+
function isStringTypeInfo(type) {
|
|
925
|
+
return type?.kind === "primitive" && type.primitive === "string";
|
|
926
|
+
}
|
|
927
|
+
function isBareStringLiteral(initialValue) {
|
|
928
|
+
if (!initialValue)
|
|
929
|
+
return false;
|
|
930
|
+
const v = initialValue.trim();
|
|
931
|
+
return v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"');
|
|
932
|
+
}
|
|
933
|
+
function isStringTypedOperand(expr, isStringName) {
|
|
934
|
+
if (expr.kind === "literal" && expr.literalType === "string")
|
|
935
|
+
return true;
|
|
936
|
+
if (expr.kind === "call" && expr.callee.kind === "identifier" && expr.args.length === 0) {
|
|
937
|
+
return isStringName(expr.callee.name);
|
|
938
|
+
}
|
|
939
|
+
if (expr.kind === "member" && expr.object.kind === "identifier" && expr.object.name === "props") {
|
|
940
|
+
return isStringName(expr.property);
|
|
941
|
+
}
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
class XslateFilterEmitter {
|
|
946
|
+
param;
|
|
947
|
+
localVarMap;
|
|
948
|
+
isStringName;
|
|
949
|
+
constructor(param, localVarMap, isStringName = () => false) {
|
|
950
|
+
this.param = param;
|
|
951
|
+
this.localVarMap = localVarMap;
|
|
952
|
+
this.isStringName = isStringName;
|
|
953
|
+
}
|
|
954
|
+
identifier(name) {
|
|
955
|
+
if (name === this.param)
|
|
956
|
+
return `$${this.param}`;
|
|
957
|
+
const signal = this.localVarMap.get(name);
|
|
958
|
+
if (signal)
|
|
959
|
+
return `$${signal}`;
|
|
960
|
+
return `$${name}`;
|
|
961
|
+
}
|
|
962
|
+
literal(value, literalType) {
|
|
963
|
+
if (literalType === "string")
|
|
964
|
+
return `'${value}'`;
|
|
965
|
+
if (literalType === "boolean")
|
|
966
|
+
return value ? "1" : "0";
|
|
967
|
+
if (literalType === "null")
|
|
968
|
+
return "undef";
|
|
969
|
+
return String(value);
|
|
970
|
+
}
|
|
971
|
+
member(object, property, _computed, emit) {
|
|
972
|
+
if (property === "length") {
|
|
973
|
+
return `${emit(object)}.size()`;
|
|
974
|
+
}
|
|
975
|
+
return `${emit(object)}.${property}`;
|
|
976
|
+
}
|
|
977
|
+
call(callee, args, emit) {
|
|
978
|
+
if (callee.kind === "identifier" && args.length === 0) {
|
|
979
|
+
return `$${callee.name}`;
|
|
980
|
+
}
|
|
981
|
+
return emit(callee);
|
|
982
|
+
}
|
|
983
|
+
unary(op, argument, emit) {
|
|
984
|
+
const arg = emit(argument);
|
|
985
|
+
if (op === "!") {
|
|
986
|
+
const needsParens = argument.kind === "binary" || argument.kind === "logical";
|
|
987
|
+
return needsParens ? `!(${arg})` : `!${arg}`;
|
|
988
|
+
}
|
|
989
|
+
if (op === "-")
|
|
990
|
+
return `-${arg}`;
|
|
991
|
+
return arg;
|
|
992
|
+
}
|
|
993
|
+
binary(op, left, right, emit) {
|
|
994
|
+
const l = emit(left);
|
|
995
|
+
const r = emit(right);
|
|
996
|
+
const isStr = (e) => isStringTypedOperand(e, this.isStringName);
|
|
997
|
+
const stringCmp = isStr(left) || isStr(right);
|
|
998
|
+
if ((op === "===" || op === "==") && stringCmp) {
|
|
999
|
+
return `${l} eq ${r}`;
|
|
1000
|
+
}
|
|
1001
|
+
if ((op === "!==" || op === "!=") && stringCmp) {
|
|
1002
|
+
return `${l} ne ${r}`;
|
|
1003
|
+
}
|
|
1004
|
+
const opMap = {
|
|
1005
|
+
"===": "==",
|
|
1006
|
+
"!==": "!=",
|
|
1007
|
+
">": ">",
|
|
1008
|
+
"<": "<",
|
|
1009
|
+
">=": ">=",
|
|
1010
|
+
"<=": "<=",
|
|
1011
|
+
"+": "+",
|
|
1012
|
+
"-": "-",
|
|
1013
|
+
"*": "*",
|
|
1014
|
+
"/": "/"
|
|
1015
|
+
};
|
|
1016
|
+
return `${l} ${opMap[op] ?? op} ${r}`;
|
|
1017
|
+
}
|
|
1018
|
+
logical(op, left, right, emit) {
|
|
1019
|
+
const l = emit(left);
|
|
1020
|
+
const r = emit(right);
|
|
1021
|
+
if (op === "&&")
|
|
1022
|
+
return `(${l} && ${r})`;
|
|
1023
|
+
if (op === "||")
|
|
1024
|
+
return `(${l} || ${r})`;
|
|
1025
|
+
return `(${l} // ${r})`;
|
|
1026
|
+
}
|
|
1027
|
+
higherOrder(method, object, param, predicate, emit) {
|
|
1028
|
+
return emit(object);
|
|
1029
|
+
}
|
|
1030
|
+
arrayLiteral(elements, emit) {
|
|
1031
|
+
return `[${elements.map(emit).join(", ")}]`;
|
|
1032
|
+
}
|
|
1033
|
+
arrayMethod(method, object, args, emit) {
|
|
1034
|
+
return renderArrayMethod(method, object, args, emit);
|
|
1035
|
+
}
|
|
1036
|
+
sortMethod(_method, object, comparator, emit) {
|
|
1037
|
+
return renderSortMethod(emit(object), comparator);
|
|
1038
|
+
}
|
|
1039
|
+
reduceMethod(method, object, reduceOp, emit) {
|
|
1040
|
+
return renderReduceMethod(emit(object), reduceOp, method === "reduceRight" ? "right" : "left");
|
|
1041
|
+
}
|
|
1042
|
+
flatMethod(object, depth, emit) {
|
|
1043
|
+
return renderFlatMethod(emit(object), depth);
|
|
1044
|
+
}
|
|
1045
|
+
flatMapMethod(object, op, emit) {
|
|
1046
|
+
return renderFlatMapMethod(emit(object), op);
|
|
1047
|
+
}
|
|
1048
|
+
conditional(_test, _consequent, _alternate) {
|
|
1049
|
+
return "1";
|
|
1050
|
+
}
|
|
1051
|
+
templateLiteral(_parts) {
|
|
1052
|
+
return "1";
|
|
1053
|
+
}
|
|
1054
|
+
arrowFn(_param, _body) {
|
|
1055
|
+
return "1";
|
|
1056
|
+
}
|
|
1057
|
+
unsupported(_raw, _reason) {
|
|
1058
|
+
return "1";
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
class XslateTopLevelEmitter {
|
|
1063
|
+
adapter;
|
|
1064
|
+
constructor(adapter) {
|
|
1065
|
+
this.adapter = adapter;
|
|
1066
|
+
}
|
|
1067
|
+
identifier(name) {
|
|
1068
|
+
return `$${name}`;
|
|
1069
|
+
}
|
|
1070
|
+
literal(value, literalType) {
|
|
1071
|
+
if (literalType === "string")
|
|
1072
|
+
return `'${value}'`;
|
|
1073
|
+
if (literalType === "boolean")
|
|
1074
|
+
return value ? "1" : "0";
|
|
1075
|
+
if (literalType === "null")
|
|
1076
|
+
return "undef";
|
|
1077
|
+
return String(value);
|
|
1078
|
+
}
|
|
1079
|
+
member(object, property, _computed, emit) {
|
|
1080
|
+
if (object.kind === "identifier" && object.name === "props") {
|
|
1081
|
+
return `$${property}`;
|
|
1082
|
+
}
|
|
1083
|
+
const obj = emit(object);
|
|
1084
|
+
if (property === "length")
|
|
1085
|
+
return `${obj}.size()`;
|
|
1086
|
+
return `${obj}.${property}`;
|
|
1087
|
+
}
|
|
1088
|
+
call(callee, args, emit) {
|
|
1089
|
+
if (callee.kind === "identifier" && args.length === 0) {
|
|
1090
|
+
return `$${callee.name}`;
|
|
1091
|
+
}
|
|
1092
|
+
const path = identifierPath(callee);
|
|
1093
|
+
const spec = path ? XSLATE_TEMPLATE_PRIMITIVES[path] : undefined;
|
|
1094
|
+
if (path && spec) {
|
|
1095
|
+
if (args.length === spec.arity) {
|
|
1096
|
+
return spec.emit(args.map(emit));
|
|
1097
|
+
}
|
|
1098
|
+
this.adapter._recordExprBF101(`templatePrimitive '${path}' expects ${spec.arity} arg(s), got ${args.length}`, `Call '${path}' with exactly ${spec.arity} argument(s).`);
|
|
1099
|
+
return "''";
|
|
1100
|
+
}
|
|
1101
|
+
return emit(callee);
|
|
1102
|
+
}
|
|
1103
|
+
unary(op, argument, emit) {
|
|
1104
|
+
const arg = emit(argument);
|
|
1105
|
+
if (op === "!")
|
|
1106
|
+
return `!${arg}`;
|
|
1107
|
+
if (op === "-")
|
|
1108
|
+
return `-${arg}`;
|
|
1109
|
+
return arg;
|
|
1110
|
+
}
|
|
1111
|
+
binary(op, left, right, emit) {
|
|
1112
|
+
const l = emit(left);
|
|
1113
|
+
const r = emit(right);
|
|
1114
|
+
const isStr = (e) => isStringTypedOperand(e, (n) => this.adapter._isStringValueName(n));
|
|
1115
|
+
const stringCmp = isStr(left) || isStr(right);
|
|
1116
|
+
if ((op === "===" || op === "==") && stringCmp) {
|
|
1117
|
+
return `${l} eq ${r}`;
|
|
1118
|
+
}
|
|
1119
|
+
if ((op === "!==" || op === "!=") && stringCmp) {
|
|
1120
|
+
return `${l} ne ${r}`;
|
|
1121
|
+
}
|
|
1122
|
+
const opMap = {
|
|
1123
|
+
"===": "==",
|
|
1124
|
+
"!==": "!=",
|
|
1125
|
+
">": ">",
|
|
1126
|
+
"<": "<",
|
|
1127
|
+
">=": ">=",
|
|
1128
|
+
"<=": "<=",
|
|
1129
|
+
"+": "+",
|
|
1130
|
+
"-": "-",
|
|
1131
|
+
"*": "*"
|
|
1132
|
+
};
|
|
1133
|
+
return `${l} ${opMap[op] ?? op} ${r}`;
|
|
1134
|
+
}
|
|
1135
|
+
logical(op, left, right, emit) {
|
|
1136
|
+
const l = emit(left);
|
|
1137
|
+
const r = emit(right);
|
|
1138
|
+
if (op === "&&")
|
|
1139
|
+
return `(${l} && ${r})`;
|
|
1140
|
+
if (op === "||")
|
|
1141
|
+
return `(${l} || ${r})`;
|
|
1142
|
+
return `(${l} // ${r})`;
|
|
1143
|
+
}
|
|
1144
|
+
higherOrder(method, object, param, predicate, emit) {
|
|
1145
|
+
if (method === "find" || method === "findIndex" || method === "findLast" || method === "findLastIndex") {
|
|
1146
|
+
this.adapter._recordExprBF101(`Xslate adapter has not lowered Array.prototype.${method} yet`);
|
|
1147
|
+
return "''";
|
|
1148
|
+
}
|
|
1149
|
+
if (method === "filter" || method === "every" || method === "some") {
|
|
1150
|
+
this.adapter._recordExprBF101(`Xslate adapter does not lower a standalone Array.prototype.${method} yet ` + `(the .filter(...).map(...) loop form is supported). ` + `Use /* @client */ or precompute the value.`);
|
|
1151
|
+
return "''";
|
|
1152
|
+
}
|
|
1153
|
+
return emit(object);
|
|
1154
|
+
}
|
|
1155
|
+
arrayLiteral(elements, emit) {
|
|
1156
|
+
return `[${elements.map(emit).join(", ")}]`;
|
|
1157
|
+
}
|
|
1158
|
+
arrayMethod(method, object, args, emit) {
|
|
1159
|
+
return renderArrayMethod(method, object, args, emit);
|
|
1160
|
+
}
|
|
1161
|
+
sortMethod(_method, object, comparator, emit) {
|
|
1162
|
+
return renderSortMethod(emit(object), comparator);
|
|
1163
|
+
}
|
|
1164
|
+
reduceMethod(method, object, reduceOp, emit) {
|
|
1165
|
+
return renderReduceMethod(emit(object), reduceOp, method === "reduceRight" ? "right" : "left");
|
|
1166
|
+
}
|
|
1167
|
+
flatMethod(object, depth, emit) {
|
|
1168
|
+
return renderFlatMethod(emit(object), depth);
|
|
1169
|
+
}
|
|
1170
|
+
flatMapMethod(object, op, emit) {
|
|
1171
|
+
return renderFlatMapMethod(emit(object), op);
|
|
1172
|
+
}
|
|
1173
|
+
conditional(test, consequent, alternate, emit) {
|
|
1174
|
+
return `(${emit(test)} ? ${emit(consequent)} : ${emit(alternate)})`;
|
|
1175
|
+
}
|
|
1176
|
+
templateLiteral(parts, emit) {
|
|
1177
|
+
const terms = [];
|
|
1178
|
+
for (const part of parts) {
|
|
1179
|
+
if (part.type === "string") {
|
|
1180
|
+
if (part.value !== "") {
|
|
1181
|
+
terms.push(`'${part.value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`);
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
const rendered = emit(part.expr);
|
|
1185
|
+
const needsParens = part.expr.kind === "binary" || part.expr.kind === "logical" || part.expr.kind === "conditional";
|
|
1186
|
+
terms.push(needsParens ? `(${rendered})` : rendered);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (terms.length === 0)
|
|
1190
|
+
return `''`;
|
|
1191
|
+
return terms.join(" ~ ");
|
|
1192
|
+
}
|
|
1193
|
+
arrowFn(_param, _body) {
|
|
1194
|
+
return "''";
|
|
1195
|
+
}
|
|
1196
|
+
unsupported(_raw, _reason) {
|
|
1197
|
+
return "''";
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
var xslateAdapter = new XslateAdapter;
|
|
1201
|
+
// src/build.ts
|
|
1202
|
+
function createConfig(options = {}) {
|
|
1203
|
+
return {
|
|
1204
|
+
adapter: new XslateAdapter(options.adapterOptions),
|
|
1205
|
+
paths: options.paths,
|
|
1206
|
+
components: options.components,
|
|
1207
|
+
outDir: options.outDir,
|
|
1208
|
+
minify: options.minify,
|
|
1209
|
+
contentHash: options.contentHash,
|
|
1210
|
+
externals: options.externals,
|
|
1211
|
+
externalsBasePath: options.externalsBasePath,
|
|
1212
|
+
bundleEntries: options.bundleEntries,
|
|
1213
|
+
localImportPrefixes: options.localImportPrefixes,
|
|
1214
|
+
outputLayout: options.outputLayout ?? {
|
|
1215
|
+
templates: "templates",
|
|
1216
|
+
clientJs: "client",
|
|
1217
|
+
runtime: "client"
|
|
1218
|
+
},
|
|
1219
|
+
postBuild: options.postBuild
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
export {
|
|
1223
|
+
createConfig
|
|
1224
|
+
};
|