@c9up/inker 0.1.7 → 0.1.9
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/InkerProvider.d.ts +23 -1
- package/dist/InkerProvider.d.ts.map +1 -1
- package/dist/InkerProvider.js +133 -2
- package/dist/InkerProvider.js.map +1 -1
- package/dist/InkerRenderError.d.ts +8 -1
- package/dist/InkerRenderError.d.ts.map +1 -1
- package/dist/InkerRenderError.js.map +1 -1
- package/dist/Templates.d.ts +235 -4
- package/dist/Templates.d.ts.map +1 -1
- package/dist/Templates.js +832 -91
- package/dist/Templates.js.map +1 -1
- package/dist/globals.d.ts +3 -3
- package/dist/globals.d.ts.map +1 -1
- package/dist/globals.js +190 -6
- package/dist/globals.js.map +1 -1
- package/dist/helpers.d.ts +11 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/loadNapi.d.ts +6 -2
- package/dist/loadNapi.d.ts.map +1 -1
- package/dist/loadNapi.js.map +1 -1
- package/dist/renderNode.d.ts +99 -3
- package/dist/renderNode.d.ts.map +1 -1
- package/dist/renderNode.js +375 -57
- package/dist/renderNode.js.map +1 -1
- package/dist/services/main.d.ts.map +1 -1
- package/dist/services/main.js +9 -0
- package/dist/services/main.js.map +1 -1
- package/dist/stacks.d.ts +25 -0
- package/dist/stacks.d.ts.map +1 -0
- package/dist/stacks.js +97 -0
- package/dist/stacks.js.map +1 -0
- package/dist/testing/index.d.ts +42 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +50 -0
- package/dist/testing/index.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +7 -3
package/dist/renderNode.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { htmlAttrs, INKER_GLOBALS } from "./globals.js";
|
|
2
2
|
import { InkerRenderError } from "./InkerRenderError.js";
|
|
3
3
|
import { SafeString } from "./SafeString.js";
|
|
4
4
|
// ---- HTML escaping — the 8 characters inker escapes in `{{ }}` (Rust escape.rs) ----
|
|
@@ -40,6 +40,14 @@ const SANDBOX_SHADOW = Object.freeze({
|
|
|
40
40
|
// Compile-once cache: expression source → a JS fn evaluating it with helpers +
|
|
41
41
|
// scope in scope (`with` — a `new Function` body is non-strict).
|
|
42
42
|
const exprCache = new Map();
|
|
43
|
+
/** `await` as a keyword, not as part of an identifier (`awaited`, `myAwait`). */
|
|
44
|
+
const AWAIT_RE = /(^|[^\w$.])await[\s(]/;
|
|
45
|
+
const AsyncFunction = Object.getPrototypeOf(async () => { }).constructor;
|
|
46
|
+
function isThenable(value) {
|
|
47
|
+
return ((typeof value === "object" || typeof value === "function") &&
|
|
48
|
+
value !== null &&
|
|
49
|
+
typeof Reflect.get(value, "then") === "function");
|
|
50
|
+
}
|
|
43
51
|
function compileExpr(source) {
|
|
44
52
|
const cached = exprCache.get(source);
|
|
45
53
|
if (cached !== undefined)
|
|
@@ -50,7 +58,12 @@ function compileExpr(source) {
|
|
|
50
58
|
// V8 closure (Edge model); the source comes from `.inker` files, not user
|
|
51
59
|
// input, so this is the same trust level as the rest of the app's code.
|
|
52
60
|
// `$g` (the global shadow) is outermost, then helpers `$h`, then scope `$s`.
|
|
53
|
-
|
|
61
|
+
// An expression using `await` has to be compiled as an ASYNC function —
|
|
62
|
+
// `await` is a syntax error anywhere else — and then returns a promise
|
|
63
|
+
// the walker suspends on. `with` is legal in both (a `new Function` body
|
|
64
|
+
// is non-strict), so the scope chain is identical either way.
|
|
65
|
+
const Ctor = AWAIT_RE.test(source) ? AsyncFunction : Function;
|
|
66
|
+
const compiled = new Ctor("$g", "$h", "$s", `with($g){ with($h){ with($s){ return (${source}); } } }`);
|
|
54
67
|
fn = (s, h) => compiled(SANDBOX_SHADOW, h, s);
|
|
55
68
|
}
|
|
56
69
|
catch (cause) {
|
|
@@ -78,6 +91,51 @@ function evalExpr(source, scope, helpers, pos) {
|
|
|
78
91
|
throw new InkerRenderError(code, msg, { line: pos?.line, column: pos?.column }, { cause });
|
|
79
92
|
}
|
|
80
93
|
}
|
|
94
|
+
/** Evaluate an expression, suspending the walk if it produced a promise. */
|
|
95
|
+
function* evalStep(source, scope, helpers, pos) {
|
|
96
|
+
const value = evalExpr(source, scope, helpers, pos);
|
|
97
|
+
return isThenable(value) ? yield value : value;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Run a walk to completion.
|
|
101
|
+
*
|
|
102
|
+
* Returns the rendered string when the template never suspended, and a promise
|
|
103
|
+
* when it did — which is what lets one walker serve both `renderString` (sync)
|
|
104
|
+
* and `render` (async) without a second copy to keep in step. Adonis reaches
|
|
105
|
+
* the same place with two compilers; parsing in Rust and walking the AST, this
|
|
106
|
+
* is the shape that fits.
|
|
107
|
+
*/
|
|
108
|
+
/**
|
|
109
|
+
* Run a walk that must finish synchronously — a custom tag's `compile` and the
|
|
110
|
+
* sync render entry points are plain callbacks with nowhere to await. Suspending
|
|
111
|
+
* there is an authoring error, reported as one rather than leaking a promise
|
|
112
|
+
* into the output.
|
|
113
|
+
*/
|
|
114
|
+
function driveSync(step, out, what) {
|
|
115
|
+
const next = step.next();
|
|
116
|
+
while (!next.done) {
|
|
117
|
+
step.return(undefined);
|
|
118
|
+
throw new InkerRenderError("E_INKER_ASYNC_NOT_SUPPORTED", `${what} cannot use \`await\` — it renders synchronously`);
|
|
119
|
+
}
|
|
120
|
+
return out.join("");
|
|
121
|
+
}
|
|
122
|
+
function drive(step, out) {
|
|
123
|
+
let next = step.next();
|
|
124
|
+
while (!next.done) {
|
|
125
|
+
const pending = next.value;
|
|
126
|
+
if (!isThenable(pending)) {
|
|
127
|
+
next = step.next(pending);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
return (async () => {
|
|
131
|
+
let cur = step.next(await pending);
|
|
132
|
+
while (!cur.done)
|
|
133
|
+
cur = step.next(await cur.value);
|
|
134
|
+
return out.join("");
|
|
135
|
+
})();
|
|
136
|
+
}
|
|
137
|
+
return out.join("");
|
|
138
|
+
}
|
|
81
139
|
// ---- @let destructuring (62-2 Edge parity): `@let({ a, b } = obj)` ----
|
|
82
140
|
//
|
|
83
141
|
// Like Edge (which compiles the pattern with a real JS parser), the Rust parser
|
|
@@ -163,34 +221,97 @@ export function normalizePartialKey(name) {
|
|
|
163
221
|
function isRecord(v) {
|
|
164
222
|
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
165
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Derive a nested scope frame from `parent`. Frames CHAIN through the prototype
|
|
226
|
+
* rather than flattening with a spread, which is what lets `@assign` reach the
|
|
227
|
+
* frame that actually owns a binding: `@let(total = 0)` outside a loop and
|
|
228
|
+
* `@assign(total = total + 1)` inside it must hit the same slot, and a spread
|
|
229
|
+
* copy would strand the write on a per-iteration duplicate. `with()` reads walk
|
|
230
|
+
* the chain, so lookup is unchanged.
|
|
231
|
+
*
|
|
232
|
+
* `defineProperty` rather than assignment: a `__proto__` key would otherwise
|
|
233
|
+
* invoke the setter and re-point the chain instead of adding a binding. Binding
|
|
234
|
+
* names are validated in Rust, so this is depth, not the only guard.
|
|
235
|
+
*/
|
|
236
|
+
function childScope(parent, bindings) {
|
|
237
|
+
const frame = Object.create(parent);
|
|
238
|
+
for (const key of Object.keys(bindings)) {
|
|
239
|
+
Object.defineProperty(frame, key, {
|
|
240
|
+
value: bindings[key],
|
|
241
|
+
writable: true,
|
|
242
|
+
enumerable: true,
|
|
243
|
+
configurable: true,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return frame;
|
|
247
|
+
}
|
|
248
|
+
/** A plain identifier target for `@assign`, as opposed to a member path. */
|
|
249
|
+
const BARE_IDENT_RE = /^[A-Za-z_$][\w$]*$/;
|
|
250
|
+
/** Keys `@inject` refuses to copy into `$context`. */
|
|
251
|
+
const PROTO_KEYS = new Set([
|
|
252
|
+
"__proto__",
|
|
253
|
+
"constructor",
|
|
254
|
+
"prototype",
|
|
255
|
+
]);
|
|
256
|
+
/**
|
|
257
|
+
* The stack store for this render. It is created by the composer (one per
|
|
258
|
+
* `render()`, shared by body, sections and layout) — a missing one means a
|
|
259
|
+
* stack tag was reached through a code path that never set one up, which is a
|
|
260
|
+
* wiring bug, not an authoring mistake.
|
|
261
|
+
*/
|
|
262
|
+
function requireStacks(ctx, tag, pos) {
|
|
263
|
+
if (ctx.stacks === undefined) {
|
|
264
|
+
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", `${tag} used in a render with no stack store`, { line: pos.line, column: pos.column });
|
|
265
|
+
}
|
|
266
|
+
return ctx.stacks;
|
|
267
|
+
}
|
|
268
|
+
/** Find the frame in the scope chain that OWNS `name`, or null if none does. */
|
|
269
|
+
function ownerFrame(scope, name) {
|
|
270
|
+
let frame = scope;
|
|
271
|
+
while (frame !== null) {
|
|
272
|
+
if (Object.hasOwn(frame, name))
|
|
273
|
+
return frame;
|
|
274
|
+
const parent = Object.getPrototypeOf(frame);
|
|
275
|
+
frame = parent;
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
166
279
|
// ---- The walker ----
|
|
167
280
|
/** Render a list of sibling nodes, threading `@let` bindings forward. */
|
|
168
|
-
function renderNodes(nodes, data, helpers, ctx, out) {
|
|
281
|
+
function* renderNodes(nodes, data, helpers, ctx, out) {
|
|
169
282
|
let scope = data;
|
|
170
283
|
for (const node of nodes) {
|
|
171
284
|
if (node.type === "Let") {
|
|
285
|
+
// `@let` short-circuits `renderNode`, so its own line has to be
|
|
286
|
+
// stamped here or the binding would evaluate under the previous
|
|
287
|
+
// node's `$lineNumber`.
|
|
288
|
+
if (node.line !== undefined)
|
|
289
|
+
helpers.$lineNumber = node.line;
|
|
172
290
|
if (node.destructure) {
|
|
173
|
-
scope =
|
|
174
|
-
...scope,
|
|
175
|
-
...evalLetDestructure(node.name, node.source, node.names ?? [], scope, helpers, node),
|
|
176
|
-
};
|
|
291
|
+
scope = childScope(scope, evalLetDestructure(node.name, node.source, node.names ?? [], scope, helpers, node));
|
|
177
292
|
}
|
|
178
293
|
else {
|
|
179
|
-
const value =
|
|
180
|
-
scope =
|
|
294
|
+
const value = yield* evalStep(node.source, scope, helpers, node);
|
|
295
|
+
scope = childScope(scope, { [node.name]: value });
|
|
181
296
|
}
|
|
182
297
|
continue;
|
|
183
298
|
}
|
|
184
|
-
renderNode(node, scope, helpers, ctx, out);
|
|
299
|
+
yield* renderNode(node, scope, helpers, ctx, out);
|
|
185
300
|
}
|
|
186
301
|
}
|
|
187
|
-
function renderNode(node, scope, helpers, ctx, out) {
|
|
302
|
+
function* renderNode(node, scope, helpers, ctx, out) {
|
|
303
|
+
// Edge emits line tracking into the compiled template, which is what makes
|
|
304
|
+
// `$lineNumber` readable from a template. Walking instead, the walker is the
|
|
305
|
+
// only place that knows — one write per node, in the layer under the render
|
|
306
|
+
// data so a caller's own `$lineNumber` key still wins.
|
|
307
|
+
if ("line" in node)
|
|
308
|
+
helpers.$lineNumber = node.line;
|
|
188
309
|
switch (node.type) {
|
|
189
310
|
case "Text":
|
|
190
311
|
out.push(node.value);
|
|
191
312
|
return;
|
|
192
313
|
case "Interpolation": {
|
|
193
|
-
const v =
|
|
314
|
+
const v = yield* evalStep(node.source, scope, helpers, node);
|
|
194
315
|
if (v instanceof SafeString) {
|
|
195
316
|
out.push(v.value);
|
|
196
317
|
}
|
|
@@ -206,23 +327,23 @@ function renderNode(node, scope, helpers, ctx, out) {
|
|
|
206
327
|
return;
|
|
207
328
|
}
|
|
208
329
|
case "If": {
|
|
209
|
-
const v =
|
|
330
|
+
const v = yield* evalStep(node.condition.source, scope, helpers, node);
|
|
210
331
|
if (v) {
|
|
211
|
-
renderNodes(node.then_nodes, scope, helpers, ctx, out);
|
|
332
|
+
yield* renderNodes(node.then_nodes, scope, helpers, ctx, out);
|
|
212
333
|
}
|
|
213
334
|
else if (node.else_nodes) {
|
|
214
|
-
renderNodes(node.else_nodes, scope, helpers, ctx, out);
|
|
335
|
+
yield* renderNodes(node.else_nodes, scope, helpers, ctx, out);
|
|
215
336
|
}
|
|
216
337
|
return;
|
|
217
338
|
}
|
|
218
339
|
case "Each":
|
|
219
|
-
renderEach(node, scope, helpers, ctx, out);
|
|
340
|
+
yield* renderEach(node, scope, helpers, ctx, out);
|
|
220
341
|
return;
|
|
221
342
|
case "Slot": {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
343
|
+
const slot = ctx.slots?.get(node.name);
|
|
344
|
+
if (slot !== undefined) {
|
|
345
|
+
const html = slot();
|
|
346
|
+
out.push(isThenable(html) ? String(yield html) : html);
|
|
226
347
|
}
|
|
227
348
|
else if (node.name === "body" && ctx.bodyHtml !== undefined) {
|
|
228
349
|
out.push(ctx.bodyHtml);
|
|
@@ -236,11 +357,11 @@ function renderNode(node, scope, helpers, ctx, out) {
|
|
|
236
357
|
if (partial === undefined) {
|
|
237
358
|
throw new InkerRenderError("E_INKER_DISK_REQUIRED", `@include('${node.name}') — partial not loaded (composer must preload it)`);
|
|
238
359
|
}
|
|
239
|
-
renderNodes(partial, scope, helpers, ctx, out);
|
|
360
|
+
yield* renderNodes(partial, scope, helpers, ctx, out);
|
|
240
361
|
return;
|
|
241
362
|
}
|
|
242
363
|
case "Component":
|
|
243
|
-
renderComponent(node, scope, helpers, ctx, out);
|
|
364
|
+
yield* renderComponent(node, scope, helpers, ctx, out);
|
|
244
365
|
return;
|
|
245
366
|
case "Section": {
|
|
246
367
|
// In a layout, a `@section('name')` is a yield: inject the child's
|
|
@@ -251,7 +372,7 @@ function renderNode(node, scope, helpers, ctx, out) {
|
|
|
251
372
|
out.push(filled);
|
|
252
373
|
}
|
|
253
374
|
else {
|
|
254
|
-
renderNodes(node.body_nodes, scope, helpers, ctx, out);
|
|
375
|
+
yield* renderNodes(node.body_nodes, scope, helpers, ctx, out);
|
|
255
376
|
}
|
|
256
377
|
return;
|
|
257
378
|
}
|
|
@@ -262,11 +383,11 @@ function renderNode(node, scope, helpers, ctx, out) {
|
|
|
262
383
|
return;
|
|
263
384
|
case "Eval":
|
|
264
385
|
// Evaluate for side effects (e.g. a helper call); emit nothing.
|
|
265
|
-
|
|
386
|
+
yield* evalStep(node.source, scope, helpers, node);
|
|
266
387
|
return;
|
|
267
388
|
case "Dump": {
|
|
268
389
|
// Pretty-print the value for debugging (Edge `@dump`).
|
|
269
|
-
const value =
|
|
390
|
+
const value = yield* evalStep(node.source, scope, helpers, node);
|
|
270
391
|
let json;
|
|
271
392
|
try {
|
|
272
393
|
json = JSON.stringify(value, null, 2) ?? String(value);
|
|
@@ -275,6 +396,111 @@ function renderNode(node, scope, helpers, ctx, out) {
|
|
|
275
396
|
json = String(value);
|
|
276
397
|
}
|
|
277
398
|
out.push(`<pre class="inker-dump">${escapeHtml(json)}</pre>`);
|
|
399
|
+
// `@dd` — dump and die. The dump is already in `out`, but the render
|
|
400
|
+
// is abandoned, so it is carried on the error for the caller to show.
|
|
401
|
+
if (node.die) {
|
|
402
|
+
throw new InkerRenderError("E_INKER_DUMP_DIE", `@dd(${node.source}) stopped the render:\n${json}`, { line: node.line, column: node.column, expression: node.source });
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
case "Assign": {
|
|
407
|
+
// `target` is `<lhs> <operator>`; splitting them in Rust is what lets a
|
|
408
|
+
// bare identifier be written back to the frame that owns it.
|
|
409
|
+
const opStart = node.target.lastIndexOf(" ");
|
|
410
|
+
const lhs = node.target.slice(0, opStart);
|
|
411
|
+
const operator = node.target.slice(opStart + 1);
|
|
412
|
+
if (BARE_IDENT_RE.test(lhs)) {
|
|
413
|
+
// Compute in the reading scope, then store in the OWNING frame —
|
|
414
|
+
// `with(){ x = … }` would create a shadowing copy on the innermost
|
|
415
|
+
// frame instead, so a loop's writes would vanish at each iteration.
|
|
416
|
+
const owner = ownerFrame(scope, lhs);
|
|
417
|
+
if (owner === null) {
|
|
418
|
+
throw new InkerRenderError("E_INKER_UNKNOWN_IDENTIFIER", `@assign cannot assign to '${lhs}' — no such binding in scope (use @let to declare it)`, { line: node.line, column: node.column });
|
|
419
|
+
}
|
|
420
|
+
const rhs = operator === "="
|
|
421
|
+
? node.source
|
|
422
|
+
: `(${lhs}) ${operator.slice(0, -1)} (${node.source})`;
|
|
423
|
+
const value = yield* evalStep(rhs, scope, helpers, node);
|
|
424
|
+
Object.defineProperty(owner, lhs, {
|
|
425
|
+
value,
|
|
426
|
+
writable: true,
|
|
427
|
+
enumerable: true,
|
|
428
|
+
configurable: true,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
// A member path (`user.name`, `rows[0].qty`) mutates the object it
|
|
433
|
+
// points at, so evaluating the assignment is the assignment.
|
|
434
|
+
yield* evalStep(`${lhs} ${operator} (${node.source})`, scope, helpers, node);
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
case "Inject": {
|
|
439
|
+
if (ctx.context === undefined) {
|
|
440
|
+
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", "@inject can only be used inside a component — there is no $context to write to at the top level", { line: node.line, column: node.column });
|
|
441
|
+
}
|
|
442
|
+
const value = yield* evalStep(node.source, scope, helpers, node);
|
|
443
|
+
if (!isRecord(value)) {
|
|
444
|
+
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", `@inject(${node.source}) expects an object, got ${value === null ? "null" : typeof value}`, { line: node.line, column: node.column });
|
|
445
|
+
}
|
|
446
|
+
for (const key of Object.keys(value)) {
|
|
447
|
+
if (PROTO_KEYS.has(key))
|
|
448
|
+
continue;
|
|
449
|
+
ctx.context[key] = value[key];
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
case "Debugger":
|
|
454
|
+
// Edge compiles `@debugger` to a `debugger` statement; with the AST
|
|
455
|
+
// walked rather than compiled, the breakpoint lands here — still under
|
|
456
|
+
// `node --inspect`, and `node` / `scope` / `ctx` are the template's own
|
|
457
|
+
// state at that point, which is what the author wants to inspect.
|
|
458
|
+
// biome-ignore lint/suspicious/noDebugger: the statement IS the feature — `@debugger` has no other implementation.
|
|
459
|
+
debugger;
|
|
460
|
+
return;
|
|
461
|
+
case "NewError": {
|
|
462
|
+
// `@newError(message, filename?, line?, col?)` — the position args let a
|
|
463
|
+
// component blame its CALLER (`$caller.line`) rather than itself.
|
|
464
|
+
const parts = yield* evalStep(`[${node.source}]`, scope, helpers, node);
|
|
465
|
+
const [message, filename, line, column] = Array.isArray(parts)
|
|
466
|
+
? parts
|
|
467
|
+
: [parts];
|
|
468
|
+
throw new InkerRenderError("E_INKER_TEMPLATE_ERROR", typeof message === "string" ? message : String(message), {
|
|
469
|
+
line: typeof line === "number" ? line : node.line,
|
|
470
|
+
column: typeof column === "number" ? column : node.column,
|
|
471
|
+
templateName: typeof filename === "string" ? filename : ctx.templateName,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
case "Stack": {
|
|
475
|
+
const name = yield* evalStep(node.source, scope, helpers, node);
|
|
476
|
+
if (typeof name !== "string") {
|
|
477
|
+
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", `@stack(${node.source}) expects a string name, got ${typeof name}`, { line: node.line, column: node.column });
|
|
478
|
+
}
|
|
479
|
+
out.push(requireStacks(ctx, "@stack", node).create(name));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
case "PushTo": {
|
|
483
|
+
const name = yield* evalStep(node.source, scope, helpers, node);
|
|
484
|
+
if (typeof name !== "string") {
|
|
485
|
+
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", `@${node.once ? "pushOnceTo" : "pushTo"}(${node.source}) expects a string name, got ${typeof name}`, { line: node.line, column: node.column });
|
|
486
|
+
}
|
|
487
|
+
const stacks = requireStacks(ctx, `@${node.once ? "pushOnceTo" : "pushTo"}`, node);
|
|
488
|
+
if (node.once) {
|
|
489
|
+
// The source id is the CALL SITE, so a component used ten times
|
|
490
|
+
// contributes its script tag once — which is the tag's whole point.
|
|
491
|
+
// Rendering the body is skipped entirely on a repeat.
|
|
492
|
+
const sourceId = `${ctx.templateName ?? "?"}:${node.line}:${node.column}`;
|
|
493
|
+
if (stacks.hasSource(name, sourceId))
|
|
494
|
+
return;
|
|
495
|
+
const body = [];
|
|
496
|
+
yield* renderNodes(node.body_nodes, scope, helpers, ctx, body);
|
|
497
|
+
stacks.pushOnceTo(name, sourceId, body.join(""));
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
const body = [];
|
|
501
|
+
yield* renderNodes(node.body_nodes, scope, helpers, ctx, body);
|
|
502
|
+
stacks.pushTo(name, body.join(""));
|
|
503
|
+
}
|
|
278
504
|
return;
|
|
279
505
|
}
|
|
280
506
|
case "CustomTag": {
|
|
@@ -289,8 +515,23 @@ function renderNode(node, scope, helpers, ctx, out) {
|
|
|
289
515
|
}
|
|
290
516
|
const token = {
|
|
291
517
|
properties: { jsArg },
|
|
292
|
-
filename: "",
|
|
518
|
+
filename: ctx.templateName ?? "",
|
|
293
519
|
loc: { start: { line: node.line ?? 0, col: node.column ?? 0 } },
|
|
520
|
+
// Rendered on demand, in the tag's own scope. A block tag that
|
|
521
|
+
// discards its body (a `@cache` miss, a permission check) never
|
|
522
|
+
// pays for it, and one that wraps its body calls this once.
|
|
523
|
+
// Like a slot: a string when the body finished synchronously, a
|
|
524
|
+
// promise when it suspended — so a tag that awaits its own check
|
|
525
|
+
// can `await token.renderBody()`.
|
|
526
|
+
renderBody: (locals) => {
|
|
527
|
+
const bodyOut = [];
|
|
528
|
+
const bodyScope = locals === undefined ? scope : childScope(scope, { ...locals });
|
|
529
|
+
return drive(renderNodes(node.body_nodes, bodyScope, helpers, ctx, bodyOut), bodyOut);
|
|
530
|
+
},
|
|
531
|
+
evaluate: (expression) => evalExpr(expression, scope, helpers, {
|
|
532
|
+
line: node.line,
|
|
533
|
+
column: node.column,
|
|
534
|
+
}),
|
|
294
535
|
};
|
|
295
536
|
const buffer = {
|
|
296
537
|
writeRaw: (text) => {
|
|
@@ -301,6 +542,9 @@ function renderNode(node, scope, helpers, ctx, out) {
|
|
|
301
542
|
line,
|
|
302
543
|
column: node.column,
|
|
303
544
|
});
|
|
545
|
+
if (isThenable(v)) {
|
|
546
|
+
throw new InkerRenderError("E_INKER_ASYNC_NOT_SUPPORTED", `@${node.name} cannot output an awaited expression — a tag renders synchronously`, { line, column: node.column });
|
|
547
|
+
}
|
|
304
548
|
if (v instanceof SafeString) {
|
|
305
549
|
out.push(v.value);
|
|
306
550
|
}
|
|
@@ -315,7 +559,11 @@ function renderNode(node, scope, helpers, ctx, out) {
|
|
|
315
559
|
}
|
|
316
560
|
},
|
|
317
561
|
};
|
|
318
|
-
tag.compile(TAG_PARSER, buffer, token);
|
|
562
|
+
const compiled = tag.compile(TAG_PARSER, buffer, token);
|
|
563
|
+
// An async tag suspends the walk here, so everything it wrote lands
|
|
564
|
+
// in order and nothing else renders in between.
|
|
565
|
+
if (isThenable(compiled))
|
|
566
|
+
yield compiled;
|
|
319
567
|
return;
|
|
320
568
|
}
|
|
321
569
|
case "Layout":
|
|
@@ -326,8 +574,8 @@ function renderNode(node, scope, helpers, ctx, out) {
|
|
|
326
574
|
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", `node type '${node.type}' is not yet handled by the Node renderer`);
|
|
327
575
|
}
|
|
328
576
|
}
|
|
329
|
-
function renderEach(node, scope, helpers, ctx, out) {
|
|
330
|
-
const iterable =
|
|
577
|
+
function* renderEach(node, scope, helpers, ctx, out) {
|
|
578
|
+
const iterable = yield* evalStep(node.iterable_source, scope, helpers, node);
|
|
331
579
|
// A `[k, v]` destructured binding means "each element is a pair" ONLY when
|
|
332
580
|
// iterating an array of pairs; over an object/Map/Set it binds key + value.
|
|
333
581
|
// Decide by the ITERABLE kind, never by the element shape (an object whose
|
|
@@ -365,31 +613,30 @@ function renderEach(node, scope, helpers, ctx, out) {
|
|
|
365
613
|
}
|
|
366
614
|
if (entries.length === 0) {
|
|
367
615
|
if (node.else_nodes)
|
|
368
|
-
renderNodes(node.else_nodes, scope, helpers, ctx, out);
|
|
616
|
+
yield* renderNodes(node.else_nodes, scope, helpers, ctx, out);
|
|
369
617
|
return;
|
|
370
618
|
}
|
|
371
619
|
const binding = node.binding;
|
|
372
620
|
for (const [value, key] of entries) {
|
|
373
|
-
let
|
|
621
|
+
let bindings;
|
|
374
622
|
if ("Single" in binding) {
|
|
375
|
-
|
|
623
|
+
bindings = { [binding.Single]: value };
|
|
376
624
|
}
|
|
377
625
|
else if ("Destructured" in binding) {
|
|
378
626
|
const [kName, vName] = binding.Destructured;
|
|
379
627
|
// array-of-pairs: `value` is `[k, v]`; object/Map/Set: key + value.
|
|
380
|
-
|
|
628
|
+
bindings =
|
|
381
629
|
arrayOfPairs && Array.isArray(value)
|
|
382
|
-
? {
|
|
383
|
-
: {
|
|
630
|
+
? { [kName]: value[0], [vName]: value[1] }
|
|
631
|
+
: { [kName]: key, [vName]: value };
|
|
384
632
|
}
|
|
385
633
|
else {
|
|
386
|
-
|
|
387
|
-
...scope,
|
|
634
|
+
bindings = {
|
|
388
635
|
[binding.Indexed.item]: value,
|
|
389
636
|
[binding.Indexed.index]: key,
|
|
390
637
|
};
|
|
391
638
|
}
|
|
392
|
-
renderNodes(node.body_nodes, childScope, helpers, ctx, out);
|
|
639
|
+
yield* renderNodes(node.body_nodes, childScope(scope, bindings), helpers, ctx, out);
|
|
393
640
|
}
|
|
394
641
|
}
|
|
395
642
|
function mergeProps(values, defaults) {
|
|
@@ -416,10 +663,16 @@ function makeProps(values) {
|
|
|
416
663
|
.map((k) => [k, values[k]]))),
|
|
417
664
|
except: (keys) => makeProps(Object.fromEntries(Object.entries(values).filter(([k]) => !keys.includes(k)))),
|
|
418
665
|
merge: (defaults) => makeProps(mergeProps(values, defaults)),
|
|
666
|
+
// Conditional merges (Edge `mergeIf` / `mergeUnless`). The condition is
|
|
667
|
+
// whatever the template hands over, so it is read for truthiness rather
|
|
668
|
+
// than required to be a boolean — `$props.mergeIf($props.get('x'), …)`
|
|
669
|
+
// is the documented usage and `get` returns `unknown`.
|
|
670
|
+
mergeIf: (condition, defaults) => condition ? makeProps(mergeProps(values, defaults)) : makeProps(values),
|
|
671
|
+
mergeUnless: (condition, defaults) => condition ? makeProps(values) : makeProps(mergeProps(values, defaults)),
|
|
419
672
|
toAttrs: () => htmlAttrs(values),
|
|
420
673
|
};
|
|
421
674
|
}
|
|
422
|
-
function renderComponent(node, scope, helpers, ctx, out) {
|
|
675
|
+
function* renderComponent(node, scope, helpers, ctx, out) {
|
|
423
676
|
const template = ctx.components?.get(normalizePartialKey(node.name));
|
|
424
677
|
if (template === undefined) {
|
|
425
678
|
throw new InkerRenderError("E_INKER_DISK_REQUIRED", `@component('${node.name}') — component not loaded (composer must preload it)`);
|
|
@@ -428,17 +681,35 @@ function renderComponent(node, scope, helpers, ctx, out) {
|
|
|
428
681
|
// (props); the component does not inherit the caller's data.
|
|
429
682
|
const props = {};
|
|
430
683
|
for (const arg of node.args)
|
|
431
|
-
props[arg.key] =
|
|
684
|
+
props[arg.key] = yield* evalStep(arg.source, scope, helpers);
|
|
685
|
+
// `$context` — state an enclosing component provided via `@inject`, which
|
|
686
|
+
// this component may extend for its own descendants. Each level gets its OWN
|
|
687
|
+
// copy (Edge does the same), so a nested `@inject` never leaks back up to a
|
|
688
|
+
// sibling subtree. Null-proto: context keys come from template authors.
|
|
689
|
+
const $context = Object.assign(Object.create(null), ctx.context);
|
|
432
690
|
// Slot content renders in the CALLER scope; `{{> name }}` in the component
|
|
433
691
|
// injects it. The default (`body`) slot is the block body outside `@slot()`.
|
|
692
|
+
//
|
|
693
|
+
// Rendering is DEFERRED to first use: the component's own body runs first,
|
|
694
|
+
// and only then does a slot render — which is the whole point of `@inject`,
|
|
695
|
+
// since the values it publishes must exist before the slot's nested
|
|
696
|
+
// components look them up. The slot sees this component's `$context` while
|
|
697
|
+
// keeping the caller's scope, exactly Edge's `state.$slots.$context`.
|
|
698
|
+
const slotCtx = { ...ctx, context: $context };
|
|
699
|
+
// A slot renders on use, and its body may itself await — so the thunk hands
|
|
700
|
+
// back a string when it finished synchronously and a promise when it did
|
|
701
|
+
// not. `{{{ $slots.main() }}}` keeps working for the ordinary case, and an
|
|
702
|
+
// awaiting body is reached with `{{{ await $slots.main() }}}`, as in Adonis.
|
|
703
|
+
const renderSlot = (nodes) => {
|
|
704
|
+
return () => {
|
|
705
|
+
const slotOut = [];
|
|
706
|
+
return drive(renderNodes(nodes, scope, helpers, slotCtx, slotOut), slotOut);
|
|
707
|
+
};
|
|
708
|
+
};
|
|
434
709
|
const slots = new Map();
|
|
435
|
-
|
|
436
|
-
renderNodes(node.body_nodes, scope, helpers, ctx, bodyOut);
|
|
437
|
-
slots.set("body", bodyOut.join(""));
|
|
710
|
+
slots.set("body", renderSlot(node.body_nodes));
|
|
438
711
|
for (const named of node.named_slots) {
|
|
439
|
-
|
|
440
|
-
renderNodes(named.nodes, scope, helpers, ctx, slotOut);
|
|
441
|
-
slots.set(named.name, slotOut.join(""));
|
|
712
|
+
slots.set(named.name, renderSlot(named.nodes));
|
|
442
713
|
}
|
|
443
714
|
// `$slots.main()` renders the default (body) slot; `$slots.<name>()` a named
|
|
444
715
|
// slot; `$slots.<name>` is undefined when absent (so `@if($slots.footer)`
|
|
@@ -448,32 +719,79 @@ function renderComponent(node, scope, helpers, ctx, out) {
|
|
|
448
719
|
// then assigns an OWN `__proto__` key instead of mutating the object's
|
|
449
720
|
// prototype — no prototype pollution.
|
|
450
721
|
const $slots = Object.create(null);
|
|
451
|
-
|
|
452
|
-
|
|
722
|
+
const asSafe = (html) => isThenable(html)
|
|
723
|
+
? Promise.resolve(html).then((v) => new SafeString(v))
|
|
724
|
+
: new SafeString(html);
|
|
725
|
+
$slots.main = () => asSafe(slots.get("body")?.() ?? "");
|
|
726
|
+
for (const [name, render] of slots) {
|
|
453
727
|
if (name === "body")
|
|
454
728
|
continue;
|
|
455
|
-
$slots[name] = () =>
|
|
729
|
+
$slots[name] = () => asSafe(render());
|
|
456
730
|
}
|
|
457
|
-
|
|
731
|
+
// `$caller` describes where the component was invoked from (Edge parity).
|
|
732
|
+
// Inker carries line/column on every node; `template` is the caller's name
|
|
733
|
+
// when the composer knew it. Frozen — it is diagnostic data, not a channel
|
|
734
|
+
// back into the caller.
|
|
735
|
+
const $caller = Object.freeze({
|
|
736
|
+
line: node.line,
|
|
737
|
+
col: node.column,
|
|
738
|
+
// Edge names this `filename` and puts the resolved absolute path in it.
|
|
739
|
+
// INKER DEVIATION (named): the LOGICAL template name goes here instead —
|
|
740
|
+
// it is what `render()` was called with and what an error should quote.
|
|
741
|
+
// The key keeps Edge's name so `@newError(msg, $caller.filename, …)`,
|
|
742
|
+
// straight out of the Edge docs, works unchanged.
|
|
743
|
+
filename: ctx.templateName,
|
|
744
|
+
});
|
|
745
|
+
const componentScope = {
|
|
746
|
+
...props,
|
|
747
|
+
$props: makeProps(props),
|
|
748
|
+
$slots,
|
|
749
|
+
$caller,
|
|
750
|
+
$context,
|
|
751
|
+
// The component's OWN name (Edge `$filename`), as `$caller.filename` is
|
|
752
|
+
// the invoking template's.
|
|
753
|
+
$filename: node.name,
|
|
754
|
+
};
|
|
458
755
|
const subCtx = {
|
|
459
756
|
partials: ctx.partials,
|
|
460
757
|
components: ctx.components,
|
|
461
758
|
tags: ctx.tags,
|
|
759
|
+
stacks: ctx.stacks,
|
|
462
760
|
slots,
|
|
761
|
+
context: $context,
|
|
762
|
+
// A component nested in THIS one must see this component as its caller;
|
|
763
|
+
// without it `$caller.filename` was undefined one level down.
|
|
764
|
+
templateName: node.name,
|
|
463
765
|
};
|
|
464
|
-
renderNodes(template, componentScope, helpers, subCtx, out);
|
|
766
|
+
yield* renderNodes(template, componentScope, helpers, subCtx, out);
|
|
767
|
+
}
|
|
768
|
+
/** Build the helper layer and start a walk over `nodes`. */
|
|
769
|
+
function startWalk(nodes, data, helpers, ctx, out) {
|
|
770
|
+
// Inker's built-in globals are always in scope; registered helpers overlay them.
|
|
771
|
+
const helperObj = { ...INKER_GLOBALS };
|
|
772
|
+
for (const [name, fn] of helpers)
|
|
773
|
+
helperObj[name] = fn;
|
|
774
|
+
// `$filename` sits in the helper layer, UNDER the render data, so a template
|
|
775
|
+
// can read the name it is being rendered as without shadowing a caller's
|
|
776
|
+
// own `$filename` key (Adonis scopes it the same way, as a function local).
|
|
777
|
+
helperObj.$filename = ctx.templateName;
|
|
778
|
+
return renderNodes(nodes, isRecord(data) ? data : {}, helperObj, ctx, out);
|
|
465
779
|
}
|
|
466
780
|
/**
|
|
467
781
|
* Render a parsed template's node list against `data`, with `helpers` in scope.
|
|
468
782
|
* Layout / partial / component composition is layered on top by the caller.
|
|
783
|
+
*
|
|
784
|
+
* SYNCHRONOUS: an expression using `await` raises rather than leaking a promise
|
|
785
|
+
* into the output. `renderNodeTreeAsync` is the awaiting counterpart, which is
|
|
786
|
+
* the split Adonis draws between `renderSync` and `render`.
|
|
469
787
|
*/
|
|
470
788
|
export function renderNodeTree(nodes, data, helpers, ctx = {}) {
|
|
471
|
-
// Edge-core globals are always in scope; registered helpers overlay them.
|
|
472
|
-
const helperObj = { ...EDGE_GLOBALS };
|
|
473
|
-
for (const [name, fn] of helpers)
|
|
474
|
-
helperObj[name] = fn;
|
|
475
789
|
const out = [];
|
|
476
|
-
|
|
477
|
-
|
|
790
|
+
return driveSync(startWalk(nodes, data, helpers, ctx, out), out, "a synchronous render");
|
|
791
|
+
}
|
|
792
|
+
/** Render a node list, awaiting any expression that needs it. */
|
|
793
|
+
export async function renderNodeTreeAsync(nodes, data, helpers, ctx = {}) {
|
|
794
|
+
const out = [];
|
|
795
|
+
return drive(startWalk(nodes, data, helpers, ctx, out), out);
|
|
478
796
|
}
|
|
479
797
|
//# sourceMappingURL=renderNode.js.map
|