@c9up/inker 0.1.6 → 0.1.8
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/README.md +1 -1
- package/dist/InkerProvider.d.ts +5 -1
- package/dist/InkerProvider.d.ts.map +1 -1
- package/dist/InkerProvider.js +33 -17
- package/dist/InkerProvider.js.map +1 -1
- package/dist/InkerRenderError.d.ts +1 -1
- package/dist/InkerRenderError.d.ts.map +1 -1
- package/dist/InkerRenderError.js.map +1 -1
- package/dist/InkerRenderer.d.ts +9 -0
- package/dist/InkerRenderer.d.ts.map +1 -1
- package/dist/InkerRenderer.js +13 -0
- package/dist/InkerRenderer.js.map +1 -1
- package/dist/Templates.d.ts +44 -0
- package/dist/Templates.d.ts.map +1 -1
- package/dist/Templates.js +271 -147
- package/dist/Templates.js.map +1 -1
- package/dist/globals.d.ts +10 -0
- package/dist/globals.d.ts.map +1 -0
- package/dist/globals.js +235 -0
- package/dist/globals.js.map +1 -0
- package/dist/identifierGuards.d.ts +2 -2
- package/dist/identifierGuards.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loadNapi.d.ts +6 -4
- package/dist/loadNapi.d.ts.map +1 -1
- package/dist/loadNapi.js +2 -2
- package/dist/loadNapi.js.map +1 -1
- package/dist/renderNode.d.ts +184 -0
- package/dist/renderNode.d.ts.map +1 -0
- package/dist/renderNode.js +479 -0
- package/dist/renderNode.js.map +1 -0
- 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/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 +3 -4
- package/scripts/copy-napi.mjs +0 -62
- package/src/InkerProvider.ts +0 -588
- package/src/InkerRenderError.ts +0 -49
- package/src/InkerRenderer.ts +0 -55
- package/src/SafeString.ts +0 -27
- package/src/Templates.ts +0 -1332
- package/src/helpers.ts +0 -76
- package/src/identifierGuards.ts +0 -49
- package/src/index.ts +0 -16
- package/src/loadNapi.ts +0 -270
- package/src/services/main.ts +0 -56
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-side renderer (62-2 pivot away from the embedded QuickJS VM). Walks the
|
|
3
|
+
* JSON AST produced by the Rust `parseTemplateJson` and evaluates each
|
|
4
|
+
* expression's verbatim `source` in Node's own V8 — with the registered helpers
|
|
5
|
+
* and the render scope in lexical scope — exactly like Adonis Edge (one runtime,
|
|
6
|
+
* helpers are plain functions in scope, callable anywhere including inside arrow
|
|
7
|
+
* fns / loop-scoped args). No Rust↔Node bridge, no tape, no unsafe.
|
|
8
|
+
*
|
|
9
|
+
* The expression source is author-controlled (`.inker` files), the same trust
|
|
10
|
+
* level as the rest of the app's code — Edge's model.
|
|
11
|
+
*/
|
|
12
|
+
export type EachBindingJson = {
|
|
13
|
+
readonly Single: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly Destructured: readonly [string, string];
|
|
16
|
+
} | {
|
|
17
|
+
readonly Indexed: {
|
|
18
|
+
readonly item: string;
|
|
19
|
+
readonly index: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export type InkerNodeJson = {
|
|
23
|
+
readonly type: "Text";
|
|
24
|
+
readonly value: string;
|
|
25
|
+
} | {
|
|
26
|
+
readonly type: "Interpolation";
|
|
27
|
+
readonly source: string;
|
|
28
|
+
readonly escape: boolean;
|
|
29
|
+
readonly line?: number;
|
|
30
|
+
readonly column?: number;
|
|
31
|
+
} | {
|
|
32
|
+
readonly type: "If";
|
|
33
|
+
readonly condition: {
|
|
34
|
+
readonly source: string;
|
|
35
|
+
};
|
|
36
|
+
readonly line?: number;
|
|
37
|
+
readonly column?: number;
|
|
38
|
+
readonly then_nodes: readonly InkerNodeJson[];
|
|
39
|
+
readonly else_nodes: readonly InkerNodeJson[] | null;
|
|
40
|
+
} | {
|
|
41
|
+
readonly type: "Each";
|
|
42
|
+
readonly iterable_source: string;
|
|
43
|
+
readonly line?: number;
|
|
44
|
+
readonly column?: number;
|
|
45
|
+
readonly binding: EachBindingJson;
|
|
46
|
+
readonly body_nodes: readonly InkerNodeJson[];
|
|
47
|
+
readonly else_nodes: readonly InkerNodeJson[] | null;
|
|
48
|
+
} | {
|
|
49
|
+
readonly type: "Let";
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly source: string;
|
|
52
|
+
/** When true, `name` is a destructuring pattern (`{ a, b }` /
|
|
53
|
+
* `[x, ...rest]`), `source` is the right-hand expression, and `names`
|
|
54
|
+
* carries the bound identifiers extracted + validated by the Rust
|
|
55
|
+
* parser (62-2). */
|
|
56
|
+
readonly destructure?: boolean;
|
|
57
|
+
readonly names?: readonly string[];
|
|
58
|
+
readonly line?: number;
|
|
59
|
+
readonly column?: number;
|
|
60
|
+
} | {
|
|
61
|
+
readonly type: "Layout";
|
|
62
|
+
readonly name: string;
|
|
63
|
+
} | {
|
|
64
|
+
readonly type: "Partial";
|
|
65
|
+
readonly name: string;
|
|
66
|
+
} | {
|
|
67
|
+
readonly type: "Slot";
|
|
68
|
+
readonly name: string;
|
|
69
|
+
} | {
|
|
70
|
+
readonly type: "Component";
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly args: readonly {
|
|
73
|
+
readonly key: string;
|
|
74
|
+
readonly source: string;
|
|
75
|
+
}[];
|
|
76
|
+
readonly body_nodes: readonly InkerNodeJson[];
|
|
77
|
+
readonly named_slots: readonly {
|
|
78
|
+
readonly name: string;
|
|
79
|
+
readonly nodes: readonly InkerNodeJson[];
|
|
80
|
+
}[];
|
|
81
|
+
} | {
|
|
82
|
+
readonly type: "Section";
|
|
83
|
+
readonly name: string;
|
|
84
|
+
readonly body_nodes: readonly InkerNodeJson[];
|
|
85
|
+
} | {
|
|
86
|
+
readonly type: "Super";
|
|
87
|
+
} | {
|
|
88
|
+
readonly type: "Eval";
|
|
89
|
+
readonly source: string;
|
|
90
|
+
readonly line?: number;
|
|
91
|
+
readonly column?: number;
|
|
92
|
+
} | {
|
|
93
|
+
readonly type: "Dump";
|
|
94
|
+
readonly source: string;
|
|
95
|
+
readonly line?: number;
|
|
96
|
+
readonly column?: number;
|
|
97
|
+
} | {
|
|
98
|
+
readonly type: "CustomTag";
|
|
99
|
+
readonly name: string;
|
|
100
|
+
readonly args_source: string;
|
|
101
|
+
readonly line?: number;
|
|
102
|
+
readonly column?: number;
|
|
103
|
+
};
|
|
104
|
+
export type HelperMap = ReadonlyMap<string, (...args: readonly unknown[]) => unknown>;
|
|
105
|
+
/**
|
|
106
|
+
* The token a custom tag's `compile` receives (Edge `token` parity). `jsArg` is
|
|
107
|
+
* the verbatim argument source between the tag's parens (`@svg('x')` → `'x'`).
|
|
108
|
+
*/
|
|
109
|
+
export interface InkerTagToken {
|
|
110
|
+
readonly properties: {
|
|
111
|
+
readonly jsArg: string;
|
|
112
|
+
};
|
|
113
|
+
readonly filename: string;
|
|
114
|
+
readonly loc: {
|
|
115
|
+
readonly start: {
|
|
116
|
+
readonly line: number;
|
|
117
|
+
readonly col: number;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The output buffer a custom tag's `compile` writes to (Edge `buffer` parity).
|
|
123
|
+
* `writeRaw` emits verbatim markup; `outputExpression` evaluates a template
|
|
124
|
+
* expression (a JS source string) in the render scope and emits its value.
|
|
125
|
+
*/
|
|
126
|
+
export interface InkerTagBuffer {
|
|
127
|
+
writeRaw(text: string): void;
|
|
128
|
+
outputExpression(jsExpression: string, filename: string, line: number, shouldEscape: boolean): void;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The parser a custom tag's `compile` receives (Edge `parser` parity). Minimal
|
|
132
|
+
* for now — the canonical Edge tags (`@svg`, `@time`) don't touch it; reserved
|
|
133
|
+
* for later `parseJsArg` / `stringifyExpression` parity.
|
|
134
|
+
*/
|
|
135
|
+
export interface InkerTagParser {
|
|
136
|
+
readonly utils: Readonly<Record<string, never>>;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A custom tag definition (AdonisJS/Edge `registerTag`). `@<tagName>(jsArg)` in a
|
|
140
|
+
* template runs `compile(parser, buffer, token)`, which writes markup to the
|
|
141
|
+
* buffer. INKER DEVIATION (named): Edge runs `compile` ONCE at template
|
|
142
|
+
* compilation (it emits JS); inker parses in Rust and renders by walking the
|
|
143
|
+
* JSON AST, so `compile` runs at RENDER time — same authoring model, no compile
|
|
144
|
+
* phase to imitate. Only inline tags (`block: false`) are supported for now.
|
|
145
|
+
*/
|
|
146
|
+
export interface InkerTag {
|
|
147
|
+
readonly tagName: string;
|
|
148
|
+
readonly block: boolean;
|
|
149
|
+
readonly seekable: boolean;
|
|
150
|
+
compile(parser: InkerTagParser, buffer: InkerTagBuffer, token: InkerTagToken): void;
|
|
151
|
+
}
|
|
152
|
+
export type TagMap = ReadonlyMap<string, InkerTag>;
|
|
153
|
+
export interface NodeRenderContext {
|
|
154
|
+
/** Layout body-injection target for `{{> body }}`. */
|
|
155
|
+
readonly bodyHtml?: string;
|
|
156
|
+
/** Component slot content by name; `{{> name }}` resolves from here. */
|
|
157
|
+
readonly slots?: ReadonlyMap<string, string>;
|
|
158
|
+
/** Pre-loaded `@include` partials by normalized key → their node list. */
|
|
159
|
+
readonly partials?: ReadonlyMap<string, readonly InkerNodeJson[]>;
|
|
160
|
+
/** Pre-loaded `@component` templates by normalized key → their node list. */
|
|
161
|
+
readonly components?: ReadonlyMap<string, readonly InkerNodeJson[]>;
|
|
162
|
+
/** Rendered `@section` content by name (child fills, injected at layout yields). */
|
|
163
|
+
readonly sections?: ReadonlyMap<string, string>;
|
|
164
|
+
/** Layout's default content for the section currently being rendered (`@super`). */
|
|
165
|
+
readonly superHtml?: string;
|
|
166
|
+
/** Runtime-registered custom tags; a `@<name>(...)` resolves its handler here. */
|
|
167
|
+
readonly tags?: TagMap;
|
|
168
|
+
}
|
|
169
|
+
/** Split a node list into its top-level `@section` blocks and the rest (the
|
|
170
|
+
* default body). Used to separate a child's sections from its `{{> body }}`
|
|
171
|
+
* content, and to collect a layout's section yields + their defaults. */
|
|
172
|
+
export declare function collectSections(nodes: readonly InkerNodeJson[]): {
|
|
173
|
+
sections: Map<string, readonly InkerNodeJson[]>;
|
|
174
|
+
body: InkerNodeJson[];
|
|
175
|
+
};
|
|
176
|
+
/** Mirror the Rust `normalize_partial_key`: strip `./`, collapse `//`, drop
|
|
177
|
+
* `/./` and a trailing `/`. */
|
|
178
|
+
export declare function normalizePartialKey(name: string): string;
|
|
179
|
+
/**
|
|
180
|
+
* Render a parsed template's node list against `data`, with `helpers` in scope.
|
|
181
|
+
* Layout / partial / component composition is layered on top by the caller.
|
|
182
|
+
*/
|
|
183
|
+
export declare function renderNodeTree(nodes: readonly InkerNodeJson[], data: Record<string, unknown>, helpers: HelperMap, ctx?: NodeRenderContext): string;
|
|
184
|
+
//# sourceMappingURL=renderNode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderNode.d.ts","sourceRoot":"","sources":["../src/renderNode.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { EDGE_GLOBALS, htmlAttrs } from "./globals.js";
|
|
2
|
+
import { InkerRenderError } from "./InkerRenderError.js";
|
|
3
|
+
import { SafeString } from "./SafeString.js";
|
|
4
|
+
// ---- HTML escaping — the 8 characters inker escapes in `{{ }}` (Rust escape.rs) ----
|
|
5
|
+
const ESCAPE_RE = /[&<>"'`\u2028\u2029]/g;
|
|
6
|
+
const ESCAPE_MAP = {
|
|
7
|
+
"&": "&",
|
|
8
|
+
"<": "<",
|
|
9
|
+
">": ">",
|
|
10
|
+
'"': """,
|
|
11
|
+
"'": "'",
|
|
12
|
+
"`": "`",
|
|
13
|
+
"\u2028": "
",
|
|
14
|
+
"\u2029": "
",
|
|
15
|
+
};
|
|
16
|
+
function escapeHtml(s) {
|
|
17
|
+
return s.replace(ESCAPE_RE, (ch) => ESCAPE_MAP[ch] ?? ch);
|
|
18
|
+
}
|
|
19
|
+
const TAG_PARSER = { utils: {} };
|
|
20
|
+
// INKER DEVIATION (named, hardening): a guard object shadowing the Node globals
|
|
21
|
+
// that turn "template = author-controlled code" into RCE / secret-leak. Placed
|
|
22
|
+
// as the OUTERMOST `with` so a legitimately-registered helper or a data key of
|
|
23
|
+
// the same name still wins, but a bare `process` / `require` / `globalThis` in
|
|
24
|
+
// an expression resolves to `undefined` instead of the real global. This is a
|
|
25
|
+
// bar-raise, NOT a sandbox: property-chain escapes (`({}).constructor…`) are not
|
|
26
|
+
// blocked — templates remain trusted code (see ADR-008). `renderString` compiles
|
|
27
|
+
// arbitrary source through this path, so it too must be fed trusted input only.
|
|
28
|
+
const SANDBOX_SHADOW = Object.freeze({
|
|
29
|
+
process: undefined,
|
|
30
|
+
globalThis: undefined,
|
|
31
|
+
global: undefined,
|
|
32
|
+
require: undefined,
|
|
33
|
+
module: undefined,
|
|
34
|
+
exports: undefined,
|
|
35
|
+
Function: undefined,
|
|
36
|
+
eval: undefined,
|
|
37
|
+
__dirname: undefined,
|
|
38
|
+
__filename: undefined,
|
|
39
|
+
});
|
|
40
|
+
// Compile-once cache: expression source → a JS fn evaluating it with helpers +
|
|
41
|
+
// scope in scope (`with` — a `new Function` body is non-strict).
|
|
42
|
+
const exprCache = new Map();
|
|
43
|
+
function compileExpr(source) {
|
|
44
|
+
const cached = exprCache.get(source);
|
|
45
|
+
if (cached !== undefined)
|
|
46
|
+
return cached;
|
|
47
|
+
let fn;
|
|
48
|
+
try {
|
|
49
|
+
// `new Function` — the author-controlled template expression compiled to a
|
|
50
|
+
// V8 closure (Edge model); the source comes from `.inker` files, not user
|
|
51
|
+
// input, so this is the same trust level as the rest of the app's code.
|
|
52
|
+
// `$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}); } } }`);
|
|
54
|
+
fn = (s, h) => compiled(SANDBOX_SHADOW, h, s);
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", `invalid expression \`${source}\`: ${errMessage(cause)}`, undefined, { cause });
|
|
58
|
+
}
|
|
59
|
+
exprCache.set(source, fn);
|
|
60
|
+
return fn;
|
|
61
|
+
}
|
|
62
|
+
function errMessage(e) {
|
|
63
|
+
return e instanceof Error ? e.message : String(e);
|
|
64
|
+
}
|
|
65
|
+
function evalExpr(source, scope, helpers, pos) {
|
|
66
|
+
try {
|
|
67
|
+
return compileExpr(source)(scope, helpers);
|
|
68
|
+
}
|
|
69
|
+
catch (cause) {
|
|
70
|
+
if (cause instanceof InkerRenderError)
|
|
71
|
+
throw cause;
|
|
72
|
+
const msg = errMessage(cause);
|
|
73
|
+
// A bare unknown identifier / navigating into null/undefined maps to the
|
|
74
|
+
// same typed code the retired Rust path resolver produced.
|
|
75
|
+
const code = /is not defined|cannot read propert/i.test(msg)
|
|
76
|
+
? "E_INKER_UNKNOWN_IDENTIFIER"
|
|
77
|
+
: "E_INKER_INVALID_EXPRESSION";
|
|
78
|
+
throw new InkerRenderError(code, msg, { line: pos?.line, column: pos?.column }, { cause });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ---- @let destructuring (62-2 Edge parity): `@let({ a, b } = obj)` ----
|
|
82
|
+
//
|
|
83
|
+
// Like Edge (which compiles the pattern with a real JS parser), the Rust parser
|
|
84
|
+
// captures the verbatim pattern (as `name`) + the right-hand `source`, AND
|
|
85
|
+
// extracts + validates the bound identifiers (`names`) at parse time — invalid /
|
|
86
|
+
// reserved / prototype-pollution binding names error there, with a JS-aware
|
|
87
|
+
// scanner so a `,`/`=` inside a default value never mis-splits the pattern. Here
|
|
88
|
+
// we only compile `const <pattern> = (<rhs>); return { <names> }` in V8 and
|
|
89
|
+
// thread the bound values into the render scope; `names` is already trusted.
|
|
90
|
+
const destructureCache = new Map();
|
|
91
|
+
/** Evaluate a destructuring `@let` and return the newly-bound names as an
|
|
92
|
+
* object to fold into the render scope. `names` are the Rust-validated bound
|
|
93
|
+
* identifiers. */
|
|
94
|
+
function evalLetDestructure(pattern, rhs, names, scope, helpers, pos) {
|
|
95
|
+
const key = `${pattern} ${rhs}`;
|
|
96
|
+
let fn = destructureCache.get(key);
|
|
97
|
+
if (fn === undefined) {
|
|
98
|
+
try {
|
|
99
|
+
const compiled = new Function("$g", "$h", "$s", `with($g){ with($h){ with($s){ const ${pattern} = (${rhs}); return { ${names.join(", ")} }; } } }`);
|
|
100
|
+
// `compiled` (a `Function`) returns `any`, assignable to the declared
|
|
101
|
+
// `Record<string, unknown>` result without a cast.
|
|
102
|
+
fn = (s, h) => compiled(SANDBOX_SHADOW, h, s);
|
|
103
|
+
}
|
|
104
|
+
catch (cause) {
|
|
105
|
+
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", `invalid @let destructuring \`${pattern} = ${rhs}\`: ${errMessage(cause)}`, { line: pos.line, column: pos.column }, { cause });
|
|
106
|
+
}
|
|
107
|
+
destructureCache.set(key, fn);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return fn(scope, helpers);
|
|
111
|
+
}
|
|
112
|
+
catch (cause) {
|
|
113
|
+
if (cause instanceof InkerRenderError)
|
|
114
|
+
throw cause;
|
|
115
|
+
const msg = errMessage(cause);
|
|
116
|
+
const code = /is not defined|cannot read propert|cannot destructure/i.test(msg)
|
|
117
|
+
? "E_INKER_UNKNOWN_IDENTIFIER"
|
|
118
|
+
: "E_INKER_INVALID_EXPRESSION";
|
|
119
|
+
throw new InkerRenderError(code, msg, { line: pos.line, column: pos.column }, { cause });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Stringify a scalar for interpolation (parity with the Rust `safe_stringify`). */
|
|
123
|
+
function stringifyScalar(v, source) {
|
|
124
|
+
switch (typeof v) {
|
|
125
|
+
case "string":
|
|
126
|
+
return v;
|
|
127
|
+
case "number":
|
|
128
|
+
return String(v); // JS renders integer floats without `.0`, `-0` → "0"
|
|
129
|
+
case "boolean":
|
|
130
|
+
return v ? "true" : "false";
|
|
131
|
+
case "bigint":
|
|
132
|
+
return v.toString();
|
|
133
|
+
default:
|
|
134
|
+
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", `cannot interpolate a ${Array.isArray(v) ? "array" : typeof v} value from \`${source}\``);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Split a node list into its top-level `@section` blocks and the rest (the
|
|
138
|
+
* default body). Used to separate a child's sections from its `{{> body }}`
|
|
139
|
+
* content, and to collect a layout's section yields + their defaults. */
|
|
140
|
+
export function collectSections(nodes) {
|
|
141
|
+
const sections = new Map();
|
|
142
|
+
const body = [];
|
|
143
|
+
for (const node of nodes) {
|
|
144
|
+
if (node.type === "Section") {
|
|
145
|
+
sections.set(node.name, node.body_nodes);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
body.push(node);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { sections, body };
|
|
152
|
+
}
|
|
153
|
+
/** Mirror the Rust `normalize_partial_key`: strip `./`, collapse `//`, drop
|
|
154
|
+
* `/./` and a trailing `/`. */
|
|
155
|
+
export function normalizePartialKey(name) {
|
|
156
|
+
let k = name.replace(/\/\.\//g, "/").replace(/\/{2,}/g, "/");
|
|
157
|
+
if (k.startsWith("./"))
|
|
158
|
+
k = k.slice(2);
|
|
159
|
+
if (k.endsWith("/"))
|
|
160
|
+
k = k.slice(0, -1);
|
|
161
|
+
return k;
|
|
162
|
+
}
|
|
163
|
+
function isRecord(v) {
|
|
164
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
165
|
+
}
|
|
166
|
+
// ---- The walker ----
|
|
167
|
+
/** Render a list of sibling nodes, threading `@let` bindings forward. */
|
|
168
|
+
function renderNodes(nodes, data, helpers, ctx, out) {
|
|
169
|
+
let scope = data;
|
|
170
|
+
for (const node of nodes) {
|
|
171
|
+
if (node.type === "Let") {
|
|
172
|
+
if (node.destructure) {
|
|
173
|
+
scope = {
|
|
174
|
+
...scope,
|
|
175
|
+
...evalLetDestructure(node.name, node.source, node.names ?? [], scope, helpers, node),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const value = evalExpr(node.source, scope, helpers, node);
|
|
180
|
+
scope = { ...scope, [node.name]: value };
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
renderNode(node, scope, helpers, ctx, out);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function renderNode(node, scope, helpers, ctx, out) {
|
|
188
|
+
switch (node.type) {
|
|
189
|
+
case "Text":
|
|
190
|
+
out.push(node.value);
|
|
191
|
+
return;
|
|
192
|
+
case "Interpolation": {
|
|
193
|
+
const v = evalExpr(node.source, scope, helpers, node);
|
|
194
|
+
if (v instanceof SafeString) {
|
|
195
|
+
out.push(v.value);
|
|
196
|
+
}
|
|
197
|
+
else if (v === null || v === undefined) {
|
|
198
|
+
// renders empty
|
|
199
|
+
}
|
|
200
|
+
else if (node.escape) {
|
|
201
|
+
out.push(escapeHtml(stringifyScalar(v, node.source)));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
out.push(stringifyScalar(v, node.source));
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
case "If": {
|
|
209
|
+
const v = evalExpr(node.condition.source, scope, helpers, node);
|
|
210
|
+
if (v) {
|
|
211
|
+
renderNodes(node.then_nodes, scope, helpers, ctx, out);
|
|
212
|
+
}
|
|
213
|
+
else if (node.else_nodes) {
|
|
214
|
+
renderNodes(node.else_nodes, scope, helpers, ctx, out);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
case "Each":
|
|
219
|
+
renderEach(node, scope, helpers, ctx, out);
|
|
220
|
+
return;
|
|
221
|
+
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);
|
|
226
|
+
}
|
|
227
|
+
else if (node.name === "body" && ctx.bodyHtml !== undefined) {
|
|
228
|
+
out.push(ctx.bodyHtml);
|
|
229
|
+
}
|
|
230
|
+
// unknown slot → empty (Edge parity)
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
case "Partial": {
|
|
234
|
+
// `@include('name')` renders the partial's nodes in the SAME scope.
|
|
235
|
+
const partial = ctx.partials?.get(normalizePartialKey(node.name));
|
|
236
|
+
if (partial === undefined) {
|
|
237
|
+
throw new InkerRenderError("E_INKER_DISK_REQUIRED", `@include('${node.name}') — partial not loaded (composer must preload it)`);
|
|
238
|
+
}
|
|
239
|
+
renderNodes(partial, scope, helpers, ctx, out);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
case "Component":
|
|
243
|
+
renderComponent(node, scope, helpers, ctx, out);
|
|
244
|
+
return;
|
|
245
|
+
case "Section": {
|
|
246
|
+
// In a layout, a `@section('name')` is a yield: inject the child's
|
|
247
|
+
// filled content if present, else render the layout's default body.
|
|
248
|
+
// In a standalone template (no layout), it just renders inline.
|
|
249
|
+
const filled = ctx.sections?.get(node.name);
|
|
250
|
+
if (filled !== undefined) {
|
|
251
|
+
out.push(filled);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
renderNodes(node.body_nodes, scope, helpers, ctx, out);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
case "Super":
|
|
259
|
+
// Inside a child section: the layout's default content for it.
|
|
260
|
+
if (ctx.superHtml !== undefined)
|
|
261
|
+
out.push(ctx.superHtml);
|
|
262
|
+
return;
|
|
263
|
+
case "Eval":
|
|
264
|
+
// Evaluate for side effects (e.g. a helper call); emit nothing.
|
|
265
|
+
evalExpr(node.source, scope, helpers, node);
|
|
266
|
+
return;
|
|
267
|
+
case "Dump": {
|
|
268
|
+
// Pretty-print the value for debugging (Edge `@dump`).
|
|
269
|
+
const value = evalExpr(node.source, scope, helpers, node);
|
|
270
|
+
let json;
|
|
271
|
+
try {
|
|
272
|
+
json = JSON.stringify(value, null, 2) ?? String(value);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
json = String(value);
|
|
276
|
+
}
|
|
277
|
+
out.push(`<pre class="inker-dump">${escapeHtml(json)}</pre>`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
case "CustomTag": {
|
|
281
|
+
// `@<tagName>(jsArg)` — a registered custom tag (Edge `registerTag`).
|
|
282
|
+
const tag = ctx.tags?.get(node.name);
|
|
283
|
+
if (tag === undefined) {
|
|
284
|
+
throw new InkerRenderError("E_INKER_UNKNOWN_TAG", `@${node.name} — no tag registered with this name (register it with \`templates.registerTag({ tagName: '${node.name}', … })\`)`, { line: node.line, column: node.column });
|
|
285
|
+
}
|
|
286
|
+
const jsArg = node.args_source;
|
|
287
|
+
if (!tag.seekable && jsArg.trim() !== "") {
|
|
288
|
+
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", `@${node.name} does not accept arguments (registered with seekable: false)`, { line: node.line, column: node.column });
|
|
289
|
+
}
|
|
290
|
+
const token = {
|
|
291
|
+
properties: { jsArg },
|
|
292
|
+
filename: "",
|
|
293
|
+
loc: { start: { line: node.line ?? 0, col: node.column ?? 0 } },
|
|
294
|
+
};
|
|
295
|
+
const buffer = {
|
|
296
|
+
writeRaw: (text) => {
|
|
297
|
+
out.push(text);
|
|
298
|
+
},
|
|
299
|
+
outputExpression: (expr, _filename, line, shouldEscape) => {
|
|
300
|
+
const v = evalExpr(expr, scope, helpers, {
|
|
301
|
+
line,
|
|
302
|
+
column: node.column,
|
|
303
|
+
});
|
|
304
|
+
if (v instanceof SafeString) {
|
|
305
|
+
out.push(v.value);
|
|
306
|
+
}
|
|
307
|
+
else if (v === null || v === undefined) {
|
|
308
|
+
// renders empty
|
|
309
|
+
}
|
|
310
|
+
else if (shouldEscape) {
|
|
311
|
+
out.push(escapeHtml(stringifyScalar(v, expr)));
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
out.push(stringifyScalar(v, expr));
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
tag.compile(TAG_PARSER, buffer, token);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
case "Layout":
|
|
322
|
+
// The composer strips `@layout()` before render; a residual one is a
|
|
323
|
+
// no-op (its body/slot injection is handled outside the walker).
|
|
324
|
+
return;
|
|
325
|
+
default:
|
|
326
|
+
throw new InkerRenderError("E_INKER_INVALID_EXPRESSION", `node type '${node.type}' is not yet handled by the Node renderer`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function renderEach(node, scope, helpers, ctx, out) {
|
|
330
|
+
const iterable = evalExpr(node.iterable_source, scope, helpers, node);
|
|
331
|
+
// A `[k, v]` destructured binding means "each element is a pair" ONLY when
|
|
332
|
+
// iterating an array of pairs; over an object/Map/Set it binds key + value.
|
|
333
|
+
// Decide by the ITERABLE kind, never by the element shape (an object whose
|
|
334
|
+
// value happens to be an array must not be mistaken for a pair).
|
|
335
|
+
const arrayOfPairs = Array.isArray(iterable);
|
|
336
|
+
// [value, key/index] pairs in iteration order.
|
|
337
|
+
const entries = [];
|
|
338
|
+
if (Array.isArray(iterable)) {
|
|
339
|
+
for (let i = 0; i < iterable.length; i++)
|
|
340
|
+
entries.push([iterable[i], i]);
|
|
341
|
+
}
|
|
342
|
+
else if (iterable instanceof Map) {
|
|
343
|
+
for (const [k, val] of iterable)
|
|
344
|
+
entries.push([val, k]);
|
|
345
|
+
}
|
|
346
|
+
else if (iterable instanceof Set) {
|
|
347
|
+
let i = 0;
|
|
348
|
+
for (const val of iterable)
|
|
349
|
+
entries.push([val, i++]);
|
|
350
|
+
}
|
|
351
|
+
else if (isRecord(iterable)) {
|
|
352
|
+
for (const [k, val] of Object.entries(iterable)) {
|
|
353
|
+
if (k === "__proto__" || k === "constructor" || k === "prototype")
|
|
354
|
+
continue;
|
|
355
|
+
entries.push([val, k]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// A null/undefined iterable is the common "maybe-absent value" mistake —
|
|
360
|
+
// point at the `@if()` guard (parity with the Rust hint).
|
|
361
|
+
const hint = iterable === null || iterable === undefined
|
|
362
|
+
? " — wrap the loop in `@if()` to guard a possibly-absent value"
|
|
363
|
+
: "";
|
|
364
|
+
throw new InkerRenderError("E_INKER_INVALID_ITERABLE", `@each iterable \`${node.iterable_source}\` is not an array or object${hint}`);
|
|
365
|
+
}
|
|
366
|
+
if (entries.length === 0) {
|
|
367
|
+
if (node.else_nodes)
|
|
368
|
+
renderNodes(node.else_nodes, scope, helpers, ctx, out);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const binding = node.binding;
|
|
372
|
+
for (const [value, key] of entries) {
|
|
373
|
+
let childScope;
|
|
374
|
+
if ("Single" in binding) {
|
|
375
|
+
childScope = { ...scope, [binding.Single]: value };
|
|
376
|
+
}
|
|
377
|
+
else if ("Destructured" in binding) {
|
|
378
|
+
const [kName, vName] = binding.Destructured;
|
|
379
|
+
// array-of-pairs: `value` is `[k, v]`; object/Map/Set: key + value.
|
|
380
|
+
childScope =
|
|
381
|
+
arrayOfPairs && Array.isArray(value)
|
|
382
|
+
? { ...scope, [kName]: value[0], [vName]: value[1] }
|
|
383
|
+
: { ...scope, [kName]: key, [vName]: value };
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
childScope = {
|
|
387
|
+
...scope,
|
|
388
|
+
[binding.Indexed.item]: value,
|
|
389
|
+
[binding.Indexed.index]: key,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
renderNodes(node.body_nodes, childScope, helpers, ctx, out);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function mergeProps(values, defaults) {
|
|
396
|
+
const out = { ...values };
|
|
397
|
+
for (const [key, val] of Object.entries(defaults)) {
|
|
398
|
+
if (key === "class" &&
|
|
399
|
+
typeof val === "string" &&
|
|
400
|
+
typeof out.class === "string") {
|
|
401
|
+
out.class = `${val} ${out.class}`.trim(); // classes are combined (Edge)
|
|
402
|
+
}
|
|
403
|
+
else if (!Object.hasOwn(out, key)) {
|
|
404
|
+
out[key] = val; // caller props win; defaults fill gaps
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
function makeProps(values) {
|
|
410
|
+
return {
|
|
411
|
+
all: () => ({ ...values }),
|
|
412
|
+
get: (key, fallback) => Object.hasOwn(values, key) ? values[key] : fallback,
|
|
413
|
+
has: (key) => Object.hasOwn(values, key),
|
|
414
|
+
only: (keys) => makeProps(Object.fromEntries(keys
|
|
415
|
+
.filter((k) => Object.hasOwn(values, k))
|
|
416
|
+
.map((k) => [k, values[k]]))),
|
|
417
|
+
except: (keys) => makeProps(Object.fromEntries(Object.entries(values).filter(([k]) => !keys.includes(k)))),
|
|
418
|
+
merge: (defaults) => makeProps(mergeProps(values, defaults)),
|
|
419
|
+
toAttrs: () => htmlAttrs(values),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function renderComponent(node, scope, helpers, ctx, out) {
|
|
423
|
+
const template = ctx.components?.get(normalizePartialKey(node.name));
|
|
424
|
+
if (template === undefined) {
|
|
425
|
+
throw new InkerRenderError("E_INKER_DISK_REQUIRED", `@component('${node.name}') — component not loaded (composer must preload it)`);
|
|
426
|
+
}
|
|
427
|
+
// Args evaluate in the CALLER scope and become the component's OWN scope
|
|
428
|
+
// (props); the component does not inherit the caller's data.
|
|
429
|
+
const props = {};
|
|
430
|
+
for (const arg of node.args)
|
|
431
|
+
props[arg.key] = evalExpr(arg.source, scope, helpers);
|
|
432
|
+
// Slot content renders in the CALLER scope; `{{> name }}` in the component
|
|
433
|
+
// injects it. The default (`body`) slot is the block body outside `@slot()`.
|
|
434
|
+
const slots = new Map();
|
|
435
|
+
const bodyOut = [];
|
|
436
|
+
renderNodes(node.body_nodes, scope, helpers, ctx, bodyOut);
|
|
437
|
+
slots.set("body", bodyOut.join(""));
|
|
438
|
+
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(""));
|
|
442
|
+
}
|
|
443
|
+
// `$slots.main()` renders the default (body) slot; `$slots.<name>()` a named
|
|
444
|
+
// slot; `$slots.<name>` is undefined when absent (so `@if($slots.footer)`
|
|
445
|
+
// works). `$props` is the chainable prop API. Both are in the component scope
|
|
446
|
+
// alongside the raw prop values (Edge parity, 62-4).
|
|
447
|
+
// Null-proto: a `@slot('__proto__')` (which the parser's name regex allows)
|
|
448
|
+
// then assigns an OWN `__proto__` key instead of mutating the object's
|
|
449
|
+
// prototype — no prototype pollution.
|
|
450
|
+
const $slots = Object.create(null);
|
|
451
|
+
$slots.main = () => new SafeString(slots.get("body") ?? "");
|
|
452
|
+
for (const [name, html] of slots) {
|
|
453
|
+
if (name === "body")
|
|
454
|
+
continue;
|
|
455
|
+
$slots[name] = () => new SafeString(html);
|
|
456
|
+
}
|
|
457
|
+
const componentScope = { ...props, $props: makeProps(props), $slots };
|
|
458
|
+
const subCtx = {
|
|
459
|
+
partials: ctx.partials,
|
|
460
|
+
components: ctx.components,
|
|
461
|
+
tags: ctx.tags,
|
|
462
|
+
slots,
|
|
463
|
+
};
|
|
464
|
+
renderNodes(template, componentScope, helpers, subCtx, out);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Render a parsed template's node list against `data`, with `helpers` in scope.
|
|
468
|
+
* Layout / partial / component composition is layered on top by the caller.
|
|
469
|
+
*/
|
|
470
|
+
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
|
+
const out = [];
|
|
476
|
+
renderNodes(nodes, isRecord(data) ? data : {}, helperObj, ctx, out);
|
|
477
|
+
return out.join("");
|
|
478
|
+
}
|
|
479
|
+
//# sourceMappingURL=renderNode.js.map
|