tina4-nodejs 3.13.86 → 3.13.87
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/CLAUDE.md +3 -3
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +148 -12
- package/packages/core/dist/index.js +147 -11
- package/packages/core/src/metrics.ts +11 -2
- package/packages/frond/dist/index.js +104 -5
- package/packages/frond/src/engine.ts +159 -6
- package/packages/orm/dist/index.js +147 -11
- package/packages/orm/src/adapters/firebird.ts +28 -2
- package/packages/orm/src/adapters/postgres.ts +11 -0
- package/packages/orm/src/database.ts +15 -3
|
@@ -101,6 +101,15 @@ function liveAttr(value) {
|
|
|
101
101
|
}
|
|
102
102
|
var filterChainCache = /* @__PURE__ */ new Map();
|
|
103
103
|
var pathParseCache = /* @__PURE__ */ new Map();
|
|
104
|
+
var TEMPLATE_CACHE_MAX = 256;
|
|
105
|
+
function capCache(cache, maxEntries) {
|
|
106
|
+
if (cache.size < maxEntries) return;
|
|
107
|
+
let drop = Math.floor(maxEntries / 2);
|
|
108
|
+
for (const key of cache.keys()) {
|
|
109
|
+
cache.delete(key);
|
|
110
|
+
if (--drop <= 0) break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
104
113
|
var TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
105
114
|
var RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
106
115
|
function tokenize(source) {
|
|
@@ -454,6 +463,9 @@ function evalExpr(expr, context) {
|
|
|
454
463
|
}).join("");
|
|
455
464
|
}
|
|
456
465
|
}
|
|
466
|
+
if (expr.startsWith("not ")) {
|
|
467
|
+
return evalComparison(expr, context);
|
|
468
|
+
}
|
|
457
469
|
for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
|
|
458
470
|
if (findOutsideQuotes(expr, op) >= 0) {
|
|
459
471
|
return evalComparison(expr, context);
|
|
@@ -1409,6 +1421,7 @@ var Frond = class _Frond {
|
|
|
1409
1421
|
const source = readFileSync(filePath, "utf-8");
|
|
1410
1422
|
const mtime = statSync(filePath).mtimeMs;
|
|
1411
1423
|
const tokens = tokenize(source);
|
|
1424
|
+
capCache(this.compiled, TEMPLATE_CACHE_MAX);
|
|
1412
1425
|
this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
|
|
1413
1426
|
return this.executeWithSource(source, tokens, context);
|
|
1414
1427
|
}
|
|
@@ -1423,6 +1436,7 @@ var Frond = class _Frond {
|
|
|
1423
1436
|
}
|
|
1424
1437
|
}
|
|
1425
1438
|
const tokens = tokenize(source);
|
|
1439
|
+
capCache(this.compiledStrings, TEMPLATE_CACHE_MAX);
|
|
1426
1440
|
this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
|
|
1427
1441
|
return this.executeCached(tokens, context);
|
|
1428
1442
|
}
|
|
@@ -1638,6 +1652,9 @@ var Frond = class _Frond {
|
|
|
1638
1652
|
} else if (tag === "macro") {
|
|
1639
1653
|
const skip = this.handleMacro(tokens, i, context);
|
|
1640
1654
|
i = skip;
|
|
1655
|
+
} else if (tag === "import") {
|
|
1656
|
+
this.handleImportAs(content, context);
|
|
1657
|
+
i++;
|
|
1641
1658
|
} else if (tag === "from") {
|
|
1642
1659
|
this.handleFromImport(content, context);
|
|
1643
1660
|
i++;
|
|
@@ -2204,7 +2221,7 @@ var Frond = class _Frond {
|
|
|
2204
2221
|
return i2;
|
|
2205
2222
|
}
|
|
2206
2223
|
const macroName = m[1];
|
|
2207
|
-
const
|
|
2224
|
+
const params = _Frond.parseMacroParams(m[2]);
|
|
2208
2225
|
const bodyTokens = [];
|
|
2209
2226
|
let i = start + 1;
|
|
2210
2227
|
while (i < tokens.length) {
|
|
@@ -2219,13 +2236,94 @@ var Frond = class _Frond {
|
|
|
2219
2236
|
const capturedContext = { ...context };
|
|
2220
2237
|
context[macroName] = (...args) => {
|
|
2221
2238
|
const macroCtx = { ...capturedContext };
|
|
2222
|
-
for (let pi = 0; pi <
|
|
2223
|
-
|
|
2239
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
2240
|
+
const [pname, pdefault] = params[pi];
|
|
2241
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2224
2242
|
}
|
|
2225
2243
|
return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
|
|
2226
2244
|
};
|
|
2227
2245
|
return i;
|
|
2228
2246
|
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Parse a macro parameter list into [name, default] pairs.
|
|
2249
|
+
*
|
|
2250
|
+
* Handles: name, name="default", name='default'. Splitting on "," alone left a
|
|
2251
|
+
* defaulted parameter literally NAMED `greeting='Hello'`, so the body's
|
|
2252
|
+
* {{ greeting }} matched nothing (rendered empty) AND the caller's positional
|
|
2253
|
+
* argument was stored under that junk key and lost. Mirrors the Python master's
|
|
2254
|
+
* _parse_macro_params. The default is null when none is declared.
|
|
2255
|
+
*/
|
|
2256
|
+
static parseMacroParams(rawParams) {
|
|
2257
|
+
return rawParams.split(",").map((p) => p.trim()).filter(Boolean).map((p) => {
|
|
2258
|
+
const eq = p.indexOf("=");
|
|
2259
|
+
if (eq === -1) return [p, null];
|
|
2260
|
+
const name = p.slice(0, eq).trim();
|
|
2261
|
+
let dflt = p.slice(eq + 1).trim();
|
|
2262
|
+
if (dflt.length >= 2 && (dflt.startsWith('"') && dflt.endsWith('"') || dflt.startsWith("'") && dflt.endsWith("'"))) {
|
|
2263
|
+
dflt = dflt.slice(1, -1);
|
|
2264
|
+
}
|
|
2265
|
+
return [name, dflt];
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
|
|
2270
|
+
*
|
|
2271
|
+
* The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
|
|
2272
|
+
* resolves through the engine's existing dotted-call path and each macro keeps the
|
|
2273
|
+
* same argument binding, default handling and SafeString output as any other macro.
|
|
2274
|
+
* A namespace object (not a class) is deliberate: a function stored as a class
|
|
2275
|
+
* attribute binds as a method and would inject the namespace as the first argument,
|
|
2276
|
+
* which is exactly the argument-shift bug the Python master carried (fixed there
|
|
2277
|
+
* with types.SimpleNamespace). Both import forms must render identically.
|
|
2278
|
+
*/
|
|
2279
|
+
handleImportAs(content, context) {
|
|
2280
|
+
const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
|
|
2281
|
+
if (!m) return;
|
|
2282
|
+
const filename = m[1];
|
|
2283
|
+
const alias = m[2];
|
|
2284
|
+
const namespace = {};
|
|
2285
|
+
const source = this.load(filename);
|
|
2286
|
+
const tokens = tokenize(source);
|
|
2287
|
+
let i = 0;
|
|
2288
|
+
while (i < tokens.length) {
|
|
2289
|
+
const [ttype, raw] = tokens[i];
|
|
2290
|
+
if (ttype === "BLOCK") {
|
|
2291
|
+
const [tagContent] = stripTag(raw);
|
|
2292
|
+
if ((tagContent.split(/\s+/)[0] || "") === "macro") {
|
|
2293
|
+
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
2294
|
+
if (macroM) {
|
|
2295
|
+
const macroName = macroM[1];
|
|
2296
|
+
const params = _Frond.parseMacroParams(macroM[2]);
|
|
2297
|
+
const bodyTokens = [];
|
|
2298
|
+
i++;
|
|
2299
|
+
while (i < tokens.length) {
|
|
2300
|
+
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
2301
|
+
i++;
|
|
2302
|
+
break;
|
|
2303
|
+
}
|
|
2304
|
+
bodyTokens.push(tokens[i]);
|
|
2305
|
+
i++;
|
|
2306
|
+
}
|
|
2307
|
+
const capturedBody = [...bodyTokens];
|
|
2308
|
+
const capturedParams = [...params];
|
|
2309
|
+
const capturedCtx = { ...context };
|
|
2310
|
+
const engine = this;
|
|
2311
|
+
namespace[macroName] = (...args) => {
|
|
2312
|
+
const macroCtx = { ...capturedCtx };
|
|
2313
|
+
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
2314
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
2315
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2316
|
+
}
|
|
2317
|
+
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
2318
|
+
};
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
i++;
|
|
2324
|
+
}
|
|
2325
|
+
context[alias] = namespace;
|
|
2326
|
+
}
|
|
2229
2327
|
handleFromImport(content, context) {
|
|
2230
2328
|
const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
|
|
2231
2329
|
if (!m) return;
|
|
@@ -2243,7 +2341,7 @@ var Frond = class _Frond {
|
|
|
2243
2341
|
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
2244
2342
|
if (macroM && names.includes(macroM[1])) {
|
|
2245
2343
|
const macroName = macroM[1];
|
|
2246
|
-
const paramNames = macroM[2]
|
|
2344
|
+
const paramNames = _Frond.parseMacroParams(macroM[2]);
|
|
2247
2345
|
const bodyTokens = [];
|
|
2248
2346
|
i++;
|
|
2249
2347
|
while (i < tokens.length) {
|
|
@@ -2261,7 +2359,8 @@ var Frond = class _Frond {
|
|
|
2261
2359
|
context[macroName] = (...args) => {
|
|
2262
2360
|
const macroCtx = { ...capturedCtx };
|
|
2263
2361
|
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
2264
|
-
|
|
2362
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
2363
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2265
2364
|
}
|
|
2266
2365
|
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
2267
2366
|
};
|
|
@@ -202,6 +202,40 @@ const filterChainCache = new Map<string, [string, [string, unknown[]][]]>();
|
|
|
202
202
|
/** Cache for parsed dotted/bracket paths: expr string -> [parts, fromBracket] */
|
|
203
203
|
const pathParseCache = new Map<string, [string[], boolean[]]>();
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Hard cap on the template caches — `compiled` and `compiledStrings`
|
|
207
|
+
* (ADR-0004, parity with PHP/Python/Ruby TEMPLATE_CACHE_MAX).
|
|
208
|
+
*
|
|
209
|
+
* An entry here is a whole token list, so the cap sits well below what a
|
|
210
|
+
* per-expression memo would justify. 256 is far above any real application's
|
|
211
|
+
* template count, so a normal app never evicts. The cap exists for the
|
|
212
|
+
* workload that genuinely grows without limit for the life of a worker:
|
|
213
|
+
* `renderString` keys on md5(source), so an app that builds template strings
|
|
214
|
+
* dynamically adds an entry per distinct string.
|
|
215
|
+
*/
|
|
216
|
+
export const TEMPLATE_CACHE_MAX = 256;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Keep a memo cache bounded. Call immediately before inserting a new entry.
|
|
220
|
+
*
|
|
221
|
+
* Eviction is insertion-ordered (oldest first), not true LRU: a `Map`
|
|
222
|
+
* preserves insertion order, so dropping from the front is cheap, whereas
|
|
223
|
+
* refreshing recency on every cache HIT would add writes to the hottest path
|
|
224
|
+
* in a render and cost more than it saves. Half the cache is dropped at once
|
|
225
|
+
* so the sweep amortises to O(1) per insert.
|
|
226
|
+
*
|
|
227
|
+
* Evicting can never change what a render produces: every read site treats a
|
|
228
|
+
* miss as "recompute", so a swept entry is rebuilt on next use.
|
|
229
|
+
*/
|
|
230
|
+
function capCache(cache: Map<string, unknown>, maxEntries: number): void {
|
|
231
|
+
if (cache.size < maxEntries) return;
|
|
232
|
+
let drop = Math.floor(maxEntries / 2);
|
|
233
|
+
for (const key of cache.keys()) {
|
|
234
|
+
cache.delete(key);
|
|
235
|
+
if (--drop <= 0) break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
205
239
|
// ── Lexer ──────────────────────────────────────────────────────
|
|
206
240
|
|
|
207
241
|
const TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
@@ -609,7 +643,21 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
|
609
643
|
}
|
|
610
644
|
}
|
|
611
645
|
|
|
612
|
-
//
|
|
646
|
+
// Comparison/logical operators -> evalComparison, the SAME evaluator {% if %}
|
|
647
|
+
// uses, so a condition means the same thing in a condition and in an output
|
|
648
|
+
// expression.
|
|
649
|
+
//
|
|
650
|
+
// The LEADING unary `not` needs its own check: every operator below is matched
|
|
651
|
+
// WITH surrounding spaces, so `not x` (nothing to its left) matched none of
|
|
652
|
+
// them, fell through to the variable-resolution tail, and was looked up as a
|
|
653
|
+
// variable literally named "not x" -- found nothing, rendered EMPTY.
|
|
654
|
+
// `{% if not x %}` and `x and not y` always worked; only the standalone
|
|
655
|
+
// `{{ not x }}` was dropped, and before booleans rendered lowercase a dropped
|
|
656
|
+
// expression and `false -> ''` looked identical, which is why it survived.
|
|
657
|
+
// Fixed in 3.13.87 alongside the boolean contract.
|
|
658
|
+
if (expr.startsWith("not ")) {
|
|
659
|
+
return evalComparison(expr, context);
|
|
660
|
+
}
|
|
613
661
|
for (const op of [" not in ", " in ", " is not ", " is ", "!=", "==", ">=", "<=", ">", "<", " and ", " or ", " not "]) {
|
|
614
662
|
if (findOutsideQuotes(expr, op) >= 0) {
|
|
615
663
|
return evalComparison(expr, context);
|
|
@@ -1685,6 +1733,7 @@ export class Frond {
|
|
|
1685
1733
|
const source = readFileSync(filePath, "utf-8");
|
|
1686
1734
|
const mtime = statSync(filePath).mtimeMs;
|
|
1687
1735
|
const tokens = tokenize(source);
|
|
1736
|
+
capCache(this.compiled as Map<string, unknown>, TEMPLATE_CACHE_MAX);
|
|
1688
1737
|
this.compiled.set(template, { tokens, mtime, cachedAt: Date.now() });
|
|
1689
1738
|
return this.executeWithSource(source, tokens, context);
|
|
1690
1739
|
}
|
|
@@ -1702,6 +1751,7 @@ export class Frond {
|
|
|
1702
1751
|
}
|
|
1703
1752
|
|
|
1704
1753
|
const tokens = tokenize(source);
|
|
1754
|
+
capCache(this.compiledStrings as Map<string, unknown>, TEMPLATE_CACHE_MAX);
|
|
1705
1755
|
this.compiledStrings.set(key, { tokens, cachedAt: Date.now() });
|
|
1706
1756
|
return this.executeCached(tokens, context);
|
|
1707
1757
|
}
|
|
@@ -1977,6 +2027,9 @@ export class Frond {
|
|
|
1977
2027
|
} else if (tag === "macro") {
|
|
1978
2028
|
const skip = this.handleMacro(tokens, i, context);
|
|
1979
2029
|
i = skip;
|
|
2030
|
+
} else if (tag === "import") {
|
|
2031
|
+
this.handleImportAs(content, context);
|
|
2032
|
+
i++;
|
|
1980
2033
|
} else if (tag === "from") {
|
|
1981
2034
|
this.handleFromImport(content, context);
|
|
1982
2035
|
i++;
|
|
@@ -2587,7 +2640,7 @@ export class Frond {
|
|
|
2587
2640
|
}
|
|
2588
2641
|
|
|
2589
2642
|
const macroName = m[1];
|
|
2590
|
-
const
|
|
2643
|
+
const params = Frond.parseMacroParams(m[2]);
|
|
2591
2644
|
|
|
2592
2645
|
// Collect body tokens
|
|
2593
2646
|
const bodyTokens: Token[] = [];
|
|
@@ -2606,8 +2659,9 @@ export class Frond {
|
|
|
2606
2659
|
const capturedContext = { ...context };
|
|
2607
2660
|
context[macroName] = (...args: unknown[]) => {
|
|
2608
2661
|
const macroCtx: Record<string, unknown> = { ...capturedContext };
|
|
2609
|
-
for (let pi = 0; pi <
|
|
2610
|
-
|
|
2662
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
2663
|
+
const [pname, pdefault] = params[pi];
|
|
2664
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2611
2665
|
}
|
|
2612
2666
|
return new SafeString(engine.renderTokens([...bodyTokens], macroCtx));
|
|
2613
2667
|
};
|
|
@@ -2615,6 +2669,104 @@ export class Frond {
|
|
|
2615
2669
|
return i;
|
|
2616
2670
|
}
|
|
2617
2671
|
|
|
2672
|
+
/**
|
|
2673
|
+
* Parse a macro parameter list into [name, default] pairs.
|
|
2674
|
+
*
|
|
2675
|
+
* Handles: name, name="default", name='default'. Splitting on "," alone left a
|
|
2676
|
+
* defaulted parameter literally NAMED `greeting='Hello'`, so the body's
|
|
2677
|
+
* {{ greeting }} matched nothing (rendered empty) AND the caller's positional
|
|
2678
|
+
* argument was stored under that junk key and lost. Mirrors the Python master's
|
|
2679
|
+
* _parse_macro_params. The default is null when none is declared.
|
|
2680
|
+
*/
|
|
2681
|
+
static parseMacroParams(rawParams: string): Array<[string, string | null]> {
|
|
2682
|
+
return rawParams
|
|
2683
|
+
.split(",")
|
|
2684
|
+
.map(p => p.trim())
|
|
2685
|
+
.filter(Boolean)
|
|
2686
|
+
.map(p => {
|
|
2687
|
+
const eq = p.indexOf("=");
|
|
2688
|
+
if (eq === -1) return [p, null] as [string, string | null];
|
|
2689
|
+
const name = p.slice(0, eq).trim();
|
|
2690
|
+
let dflt = p.slice(eq + 1).trim();
|
|
2691
|
+
if (
|
|
2692
|
+
dflt.length >= 2 &&
|
|
2693
|
+
((dflt.startsWith('"') && dflt.endsWith('"')) ||
|
|
2694
|
+
(dflt.startsWith("'") && dflt.endsWith("'")))
|
|
2695
|
+
) {
|
|
2696
|
+
dflt = dflt.slice(1, -1);
|
|
2697
|
+
}
|
|
2698
|
+
return [name, dflt] as [string, string | null];
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
/**
|
|
2703
|
+
* {% import "file" as alias %} -- load EVERY macro in a file under one namespace.
|
|
2704
|
+
*
|
|
2705
|
+
* The alias is bound as a plain object of macro functions, so {{ alias.greet(x) }}
|
|
2706
|
+
* resolves through the engine's existing dotted-call path and each macro keeps the
|
|
2707
|
+
* same argument binding, default handling and SafeString output as any other macro.
|
|
2708
|
+
* A namespace object (not a class) is deliberate: a function stored as a class
|
|
2709
|
+
* attribute binds as a method and would inject the namespace as the first argument,
|
|
2710
|
+
* which is exactly the argument-shift bug the Python master carried (fixed there
|
|
2711
|
+
* with types.SimpleNamespace). Both import forms must render identically.
|
|
2712
|
+
*/
|
|
2713
|
+
private handleImportAs(content: string, context: Record<string, unknown>): void {
|
|
2714
|
+
const m = content.match(/^import\s+["'](.+?)["']\s+as\s+(\w+)/);
|
|
2715
|
+
if (!m) return;
|
|
2716
|
+
|
|
2717
|
+
const filename = m[1];
|
|
2718
|
+
const alias = m[2];
|
|
2719
|
+
const namespace: Record<string, unknown> = {};
|
|
2720
|
+
|
|
2721
|
+
const source = this.load(filename);
|
|
2722
|
+
const tokens = tokenize(source);
|
|
2723
|
+
|
|
2724
|
+
let i = 0;
|
|
2725
|
+
while (i < tokens.length) {
|
|
2726
|
+
const [ttype, raw] = tokens[i];
|
|
2727
|
+
if (ttype === "BLOCK") {
|
|
2728
|
+
const [tagContent] = stripTag(raw);
|
|
2729
|
+
if ((tagContent.split(/\s+/)[0] || "") === "macro") {
|
|
2730
|
+
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
2731
|
+
if (macroM) {
|
|
2732
|
+
const macroName = macroM[1];
|
|
2733
|
+
const params = Frond.parseMacroParams(macroM[2]);
|
|
2734
|
+
|
|
2735
|
+
const bodyTokens: Token[] = [];
|
|
2736
|
+
i++;
|
|
2737
|
+
while (i < tokens.length) {
|
|
2738
|
+
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
2739
|
+
i++;
|
|
2740
|
+
break;
|
|
2741
|
+
}
|
|
2742
|
+
bodyTokens.push(tokens[i]);
|
|
2743
|
+
i++;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
// Own copies per macro — avoids closure-over-loop-variable sharing.
|
|
2747
|
+
const capturedBody = [...bodyTokens];
|
|
2748
|
+
const capturedParams = [...params];
|
|
2749
|
+
const capturedCtx = { ...context };
|
|
2750
|
+
const engine = this;
|
|
2751
|
+
|
|
2752
|
+
namespace[macroName] = (...args: unknown[]) => {
|
|
2753
|
+
const macroCtx: Record<string, unknown> = { ...capturedCtx };
|
|
2754
|
+
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
2755
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
2756
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2757
|
+
}
|
|
2758
|
+
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
2759
|
+
};
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
i++;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
context[alias] = namespace;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2618
2770
|
private handleFromImport(content: string, context: Record<string, unknown>): void {
|
|
2619
2771
|
const m = content.match(/^from\s+["'](.+?)["']\s+import\s+(.+)/);
|
|
2620
2772
|
if (!m) return;
|
|
@@ -2635,7 +2787,7 @@ export class Frond {
|
|
|
2635
2787
|
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
2636
2788
|
if (macroM && names.includes(macroM[1])) {
|
|
2637
2789
|
const macroName = macroM[1];
|
|
2638
|
-
const paramNames = macroM[2]
|
|
2790
|
+
const paramNames = Frond.parseMacroParams(macroM[2]);
|
|
2639
2791
|
|
|
2640
2792
|
const bodyTokens: Token[] = [];
|
|
2641
2793
|
i++;
|
|
@@ -2657,7 +2809,8 @@ export class Frond {
|
|
|
2657
2809
|
context[macroName] = (...args: unknown[]) => {
|
|
2658
2810
|
const macroCtx: Record<string, unknown> = { ...capturedCtx };
|
|
2659
2811
|
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
2660
|
-
|
|
2812
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
2813
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2661
2814
|
}
|
|
2662
2815
|
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
2663
2816
|
};
|