@c9up/inker 0.1.8 → 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.
Files changed (42) hide show
  1. package/dist/InkerProvider.d.ts +23 -1
  2. package/dist/InkerProvider.d.ts.map +1 -1
  3. package/dist/InkerProvider.js +133 -2
  4. package/dist/InkerProvider.js.map +1 -1
  5. package/dist/InkerRenderError.d.ts +8 -1
  6. package/dist/InkerRenderError.d.ts.map +1 -1
  7. package/dist/InkerRenderError.js.map +1 -1
  8. package/dist/Templates.d.ts +235 -4
  9. package/dist/Templates.d.ts.map +1 -1
  10. package/dist/Templates.js +832 -91
  11. package/dist/Templates.js.map +1 -1
  12. package/dist/globals.d.ts +3 -3
  13. package/dist/globals.d.ts.map +1 -1
  14. package/dist/globals.js +190 -6
  15. package/dist/globals.js.map +1 -1
  16. package/dist/helpers.d.ts +11 -1
  17. package/dist/helpers.d.ts.map +1 -1
  18. package/dist/index.d.ts +2 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +5 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/loadNapi.d.ts +6 -2
  23. package/dist/loadNapi.d.ts.map +1 -1
  24. package/dist/loadNapi.js.map +1 -1
  25. package/dist/renderNode.d.ts +99 -3
  26. package/dist/renderNode.d.ts.map +1 -1
  27. package/dist/renderNode.js +375 -57
  28. package/dist/renderNode.js.map +1 -1
  29. package/dist/stacks.d.ts +25 -0
  30. package/dist/stacks.d.ts.map +1 -0
  31. package/dist/stacks.js +97 -0
  32. package/dist/stacks.js.map +1 -0
  33. package/dist/testing/index.d.ts +42 -0
  34. package/dist/testing/index.d.ts.map +1 -0
  35. package/dist/testing/index.js +50 -0
  36. package/dist/testing/index.js.map +1 -0
  37. package/index.darwin-arm64.node +0 -0
  38. package/index.darwin-x64.node +0 -0
  39. package/index.linux-arm64-gnu.node +0 -0
  40. package/index.linux-x64-gnu.node +0 -0
  41. package/index.win32-x64-msvc.node +0 -0
  42. package/package.json +7 -3
@@ -1,4 +1,4 @@
1
- import { EDGE_GLOBALS, htmlAttrs } from "./globals.js";
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
- const compiled = new Function("$g", "$h", "$s", `with($g){ with($h){ with($s){ return (${source}); } } }`);
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 = evalExpr(node.source, scope, helpers, node);
180
- scope = { ...scope, [node.name]: value };
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 = evalExpr(node.source, scope, helpers, node);
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 = evalExpr(node.condition.source, scope, helpers, node);
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
- if (ctx.slots?.has(node.name)) {
223
- const html = ctx.slots.get(node.name);
224
- if (html !== undefined)
225
- out.push(html);
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
- evalExpr(node.source, scope, helpers, node);
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 = evalExpr(node.source, scope, helpers, node);
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 = evalExpr(node.iterable_source, scope, helpers, node);
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 childScope;
621
+ let bindings;
374
622
  if ("Single" in binding) {
375
- childScope = { ...scope, [binding.Single]: value };
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
- childScope =
628
+ bindings =
381
629
  arrayOfPairs && Array.isArray(value)
382
- ? { ...scope, [kName]: value[0], [vName]: value[1] }
383
- : { ...scope, [kName]: key, [vName]: value };
630
+ ? { [kName]: value[0], [vName]: value[1] }
631
+ : { [kName]: key, [vName]: value };
384
632
  }
385
633
  else {
386
- childScope = {
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] = evalExpr(arg.source, scope, helpers);
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
- const bodyOut = [];
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
- const slotOut = [];
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
- $slots.main = () => new SafeString(slots.get("body") ?? "");
452
- for (const [name, html] of slots) {
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] = () => new SafeString(html);
729
+ $slots[name] = () => asSafe(render());
456
730
  }
457
- const componentScope = { ...props, $props: makeProps(props), $slots };
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
- renderNodes(nodes, isRecord(data) ? data : {}, helperObj, ctx, out);
477
- return out.join("");
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