tina4-nodejs 3.13.99 → 3.13.100
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 +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +130 -40
- package/packages/core/dist/index.js +129 -40
- package/packages/core/src/ai.ts +15 -6
- package/packages/frond/dist/index.js +118 -36
- package/packages/frond/src/engine.ts +195 -45
- package/packages/orm/dist/index.js +129 -40
- package/types/core/src/ai.d.ts +29 -0
- package/types/frond/src/engine.d.ts +50 -8
|
@@ -2620,7 +2620,10 @@ var init_trustedProxy = __esm({
|
|
|
2620
2620
|
var engine_exports = {};
|
|
2621
2621
|
__export(engine_exports, {
|
|
2622
2622
|
Frond: () => Frond,
|
|
2623
|
+
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
2623
2624
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
2625
|
+
filterChainCache: () => filterChainCache,
|
|
2626
|
+
pathParseCache: () => pathParseCache,
|
|
2624
2627
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
2625
2628
|
});
|
|
2626
2629
|
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
|
|
@@ -2724,6 +2727,12 @@ function capCache(cache, maxEntries) {
|
|
|
2724
2727
|
if (--drop <= 0) break;
|
|
2725
2728
|
}
|
|
2726
2729
|
}
|
|
2730
|
+
function sweepExpiredCache(cache) {
|
|
2731
|
+
const now = Date.now();
|
|
2732
|
+
for (const [key, [, expiresAt]] of cache) {
|
|
2733
|
+
if (expiresAt <= now) cache.delete(key);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2727
2736
|
function tokenize(source) {
|
|
2728
2737
|
const rawBlocks = [];
|
|
2729
2738
|
source = source.replace(RAW_BLOCK_RE, (_match, content) => {
|
|
@@ -2787,6 +2796,16 @@ function stripTag(raw) {
|
|
|
2787
2796
|
}
|
|
2788
2797
|
return [inner.trim(), stripBefore, stripAfter];
|
|
2789
2798
|
}
|
|
2799
|
+
function extendsTarget(source) {
|
|
2800
|
+
const matches = source.match(EXTENDS_RE_GLOBAL);
|
|
2801
|
+
if (matches && matches.length > 1) {
|
|
2802
|
+
throw new Error(
|
|
2803
|
+
`Frond: template has ${matches.length} "{% extends %}" tags -- a template can extend only one parent`
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
const match = source.match(EXTENDS_RE);
|
|
2807
|
+
return match ? match[1] : "";
|
|
2808
|
+
}
|
|
2790
2809
|
function resolveVar(expr, context) {
|
|
2791
2810
|
expr = expr.trim();
|
|
2792
2811
|
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
|
|
@@ -2867,6 +2886,7 @@ function resolveVar(expr, context) {
|
|
|
2867
2886
|
fromBracket.push(false);
|
|
2868
2887
|
}
|
|
2869
2888
|
}
|
|
2889
|
+
capCache(pathParseCache, MEMO_CACHE_MAX);
|
|
2870
2890
|
pathParseCache.set(expr, [parts, fromBracket]);
|
|
2871
2891
|
}
|
|
2872
2892
|
let value = context;
|
|
@@ -3454,6 +3474,7 @@ function parseFilterChain(expr) {
|
|
|
3454
3474
|
}
|
|
3455
3475
|
}
|
|
3456
3476
|
const result = [variable, filters];
|
|
3477
|
+
capCache(filterChainCache, MEMO_CACHE_MAX);
|
|
3457
3478
|
filterChainCache.set(expr, result);
|
|
3458
3479
|
return result;
|
|
3459
3480
|
}
|
|
@@ -3633,7 +3654,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
3633
3654
|
function _generateFormTokenValue(descriptor = "") {
|
|
3634
3655
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
3635
3656
|
}
|
|
3636
|
-
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
3657
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
3637
3658
|
var init_engine = __esm({
|
|
3638
3659
|
"../frond/src/engine.ts"() {
|
|
3639
3660
|
"use strict";
|
|
@@ -3727,9 +3748,12 @@ var init_engine = __esm({
|
|
|
3727
3748
|
LIVE_RE = /^live\s+["']([^"']+)["']([\s\S]*)$/;
|
|
3728
3749
|
LIVE_WS_RE = /ws\s+["']([^"']+)["']/;
|
|
3729
3750
|
LIVE_SRC_RE = /src\s+["']([^"']+)["']/;
|
|
3751
|
+
EXTENDS_RE = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/;
|
|
3752
|
+
EXTENDS_RE_GLOBAL = /\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/g;
|
|
3730
3753
|
filterChainCache = /* @__PURE__ */ new Map();
|
|
3731
3754
|
pathParseCache = /* @__PURE__ */ new Map();
|
|
3732
3755
|
TEMPLATE_CACHE_MAX = 256;
|
|
3756
|
+
MEMO_CACHE_MAX = 1024;
|
|
3733
3757
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
3734
3758
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
3735
3759
|
VarRef = class {
|
|
@@ -4082,29 +4106,22 @@ var init_engine = __esm({
|
|
|
4082
4106
|
return this;
|
|
4083
4107
|
}
|
|
4084
4108
|
/**
|
|
4085
|
-
* Register a custom filter
|
|
4086
|
-
*
|
|
4087
|
-
* the live instance's local filter map also receives the addition
|
|
4088
|
-
* immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
|
|
4109
|
+
* Register a custom filter on this instance only. Use the static method
|
|
4110
|
+
* for process-global registration. tina4: ADR-0052.
|
|
4089
4111
|
*/
|
|
4090
4112
|
addFilter(name, fn) {
|
|
4091
|
-
_Frond.classFilters.set(name, fn);
|
|
4092
4113
|
this.filters[name] = fn;
|
|
4093
4114
|
}
|
|
4094
4115
|
/**
|
|
4095
|
-
* Register a global variable
|
|
4096
|
-
* at class level — see ``addFilter`` for the dual-call semantics.
|
|
4116
|
+
* Register a global variable on this instance only.
|
|
4097
4117
|
*/
|
|
4098
4118
|
addGlobal(name, value) {
|
|
4099
|
-
_Frond.classGlobals.set(name, value);
|
|
4100
4119
|
this.globals[name] = value;
|
|
4101
4120
|
}
|
|
4102
4121
|
/**
|
|
4103
|
-
* Register a custom test
|
|
4104
|
-
* ``addFilter`` for the dual-call semantics.
|
|
4122
|
+
* Register a custom test on this instance only.
|
|
4105
4123
|
*/
|
|
4106
4124
|
addTest(name, fn) {
|
|
4107
|
-
_Frond.classTests.set(name, fn);
|
|
4108
4125
|
this.tests[name] = fn;
|
|
4109
4126
|
}
|
|
4110
4127
|
/**
|
|
@@ -4219,9 +4236,8 @@ var init_engine = __esm({
|
|
|
4219
4236
|
if (Object.keys(this.tests).length > 0) {
|
|
4220
4237
|
context.__frond_tests__ = this.tests;
|
|
4221
4238
|
}
|
|
4222
|
-
const
|
|
4223
|
-
if (
|
|
4224
|
-
const parentName = extendsMatch[1];
|
|
4239
|
+
const parentName = extendsTarget(source);
|
|
4240
|
+
if (parentName) {
|
|
4225
4241
|
const parentSource = this.load(parentName);
|
|
4226
4242
|
const childBlocks = this.extractBlocks(source);
|
|
4227
4243
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -4232,9 +4248,8 @@ var init_engine = __esm({
|
|
|
4232
4248
|
if (Object.keys(this.tests).length > 0) {
|
|
4233
4249
|
context.__frond_tests__ = this.tests;
|
|
4234
4250
|
}
|
|
4235
|
-
const
|
|
4236
|
-
if (
|
|
4237
|
-
const parentName = extendsMatch[1];
|
|
4251
|
+
const parentName = extendsTarget(source);
|
|
4252
|
+
if (parentName) {
|
|
4238
4253
|
const parentSource = this.load(parentName);
|
|
4239
4254
|
const childBlocks = this.extractBlocks(source);
|
|
4240
4255
|
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
@@ -4279,10 +4294,93 @@ var init_engine = __esm({
|
|
|
4279
4294
|
}
|
|
4280
4295
|
return blocks;
|
|
4281
4296
|
}
|
|
4297
|
+
/**
|
|
4298
|
+
* Depth-aware block substitution against `source` (typically the
|
|
4299
|
+
* fully-resolved root template).
|
|
4300
|
+
*
|
|
4301
|
+
* A single regex `.replace()` pass (the flat `pattern` this replaces in
|
|
4302
|
+
* renderWithBlocks) pairs an OUTER block's open tag with the FIRST
|
|
4303
|
+
* `{% endblock %}` found -- which, when the outer block wraps a NESTED
|
|
4304
|
+
* `{% block %}`, is the nested block's own close tag, not the outer's.
|
|
4305
|
+
* That silently truncates the outer block's captured content and drops
|
|
4306
|
+
* everything after the inner endblock (the root-nested-block
|
|
4307
|
+
* content-loss bug). This scans with an open/close depth counter
|
|
4308
|
+
* instead (mirroring extractBlocks), so an outer block always captures
|
|
4309
|
+
* its FULL body, nested child blocks included.
|
|
4310
|
+
*
|
|
4311
|
+
* The content chosen for each block -- the child override in `blocks`
|
|
4312
|
+
* if present, else the block's own default body -- is then recursively
|
|
4313
|
+
* substituted against the SAME `blocks` map before being tokenized and
|
|
4314
|
+
* rendered, so a block nested inside another block resolves correctly
|
|
4315
|
+
* regardless of which template in the inheritance chain declared the
|
|
4316
|
+
* nesting (the root, an intermediate, however many levels deep).
|
|
4317
|
+
*
|
|
4318
|
+
* `{{ parent() }}` / `{{ super() }}` inside a block still render that
|
|
4319
|
+
* block's OWN default content at this level (lazy, on first call).
|
|
4320
|
+
*/
|
|
4321
|
+
substituteBlocks(source, blocks, context) {
|
|
4322
|
+
const blockOpen = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}/g;
|
|
4323
|
+
const blockClose = /\{%[-\s]*endblock\s*[-]?%\}/g;
|
|
4324
|
+
const engine = this;
|
|
4325
|
+
const pieces = [];
|
|
4326
|
+
let pos = 0;
|
|
4327
|
+
while (pos < source.length) {
|
|
4328
|
+
blockOpen.lastIndex = pos;
|
|
4329
|
+
const mOpen = blockOpen.exec(source);
|
|
4330
|
+
if (!mOpen) {
|
|
4331
|
+
pieces.push(source.slice(pos));
|
|
4332
|
+
break;
|
|
4333
|
+
}
|
|
4334
|
+
pieces.push(source.slice(pos, mOpen.index));
|
|
4335
|
+
const name = mOpen[1];
|
|
4336
|
+
const contentStart = mOpen.index + mOpen[0].length;
|
|
4337
|
+
let depth = 1;
|
|
4338
|
+
let scan = contentStart;
|
|
4339
|
+
let closeMatch = null;
|
|
4340
|
+
while (depth > 0 && scan < source.length) {
|
|
4341
|
+
blockOpen.lastIndex = scan;
|
|
4342
|
+
blockClose.lastIndex = scan;
|
|
4343
|
+
const nextOpen = blockOpen.exec(source);
|
|
4344
|
+
const nextClose = blockClose.exec(source);
|
|
4345
|
+
if (!nextClose) break;
|
|
4346
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
4347
|
+
depth++;
|
|
4348
|
+
scan = nextOpen.index + nextOpen[0].length;
|
|
4349
|
+
} else {
|
|
4350
|
+
depth--;
|
|
4351
|
+
if (depth === 0) {
|
|
4352
|
+
closeMatch = nextClose;
|
|
4353
|
+
} else {
|
|
4354
|
+
scan = nextClose.index + nextClose[0].length;
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
if (!closeMatch) {
|
|
4359
|
+
pieces.push(source.slice(mOpen.index));
|
|
4360
|
+
pos = source.length;
|
|
4361
|
+
break;
|
|
4362
|
+
}
|
|
4363
|
+
const parentContent = source.slice(contentStart, closeMatch.index);
|
|
4364
|
+
const blockSource = blocks[name] ?? parentContent;
|
|
4365
|
+
const resolvedSource = engine.substituteBlocks(blockSource, blocks, context);
|
|
4366
|
+
let renderedParent = null;
|
|
4367
|
+
const getParent = () => {
|
|
4368
|
+
if (renderedParent === null) {
|
|
4369
|
+
renderedParent = new SafeString(
|
|
4370
|
+
engine.renderTokens(tokenize(parentContent), context)
|
|
4371
|
+
);
|
|
4372
|
+
}
|
|
4373
|
+
return renderedParent;
|
|
4374
|
+
};
|
|
4375
|
+
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
4376
|
+
pieces.push(engine.renderTokens(tokenize(resolvedSource), blockCtx));
|
|
4377
|
+
pos = closeMatch.index + closeMatch[0].length;
|
|
4378
|
+
}
|
|
4379
|
+
return pieces.join("");
|
|
4380
|
+
}
|
|
4282
4381
|
renderWithBlocks(parentSource, context, childBlocks) {
|
|
4283
|
-
const
|
|
4284
|
-
if (
|
|
4285
|
-
const grandparentName = extendsMatch[1];
|
|
4382
|
+
const grandparentName = extendsTarget(parentSource);
|
|
4383
|
+
if (grandparentName) {
|
|
4286
4384
|
const grandparentSource = this.load(grandparentName);
|
|
4287
4385
|
const parentBlocks = this.extractBlocks(parentSource);
|
|
4288
4386
|
const mergedBlocks = { ...parentBlocks, ...childBlocks };
|
|
@@ -4302,22 +4400,7 @@ var init_engine = __esm({
|
|
|
4302
4400
|
}
|
|
4303
4401
|
return this.renderWithBlocks(grandparentSource, context, mergedBlocks);
|
|
4304
4402
|
}
|
|
4305
|
-
const
|
|
4306
|
-
const engine = this;
|
|
4307
|
-
const result = parentSource.replace(pattern, (_match, name, parentContent) => {
|
|
4308
|
-
const blockSource = childBlocks[name] ?? parentContent;
|
|
4309
|
-
let renderedParent = null;
|
|
4310
|
-
const getParent = () => {
|
|
4311
|
-
if (renderedParent === null) {
|
|
4312
|
-
renderedParent = new SafeString(
|
|
4313
|
-
engine.renderTokens(tokenize(parentContent), context)
|
|
4314
|
-
);
|
|
4315
|
-
}
|
|
4316
|
-
return renderedParent;
|
|
4317
|
-
};
|
|
4318
|
-
const blockCtx = { ...context, parent: getParent, super: getParent };
|
|
4319
|
-
return this.renderTokens(tokenize(blockSource), blockCtx);
|
|
4320
|
-
});
|
|
4403
|
+
const result = this.substituteBlocks(parentSource, childBlocks, context);
|
|
4321
4404
|
return this.renderTokens(tokenize(result), context);
|
|
4322
4405
|
}
|
|
4323
4406
|
renderTokens(tokens, context) {
|
|
@@ -5140,6 +5223,7 @@ var init_engine = __esm({
|
|
|
5140
5223
|
const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
|
|
5141
5224
|
const cacheKey = m ? m[1] : "default";
|
|
5142
5225
|
const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
|
|
5226
|
+
sweepExpiredCache(this.fragmentCache);
|
|
5143
5227
|
const cached = this.fragmentCache.get(cacheKey);
|
|
5144
5228
|
if (cached) {
|
|
5145
5229
|
const [htmlContent, expiresAt] = cached;
|
|
@@ -5187,6 +5271,7 @@ var init_engine = __esm({
|
|
|
5187
5271
|
i++;
|
|
5188
5272
|
}
|
|
5189
5273
|
const rendered = this.renderTokens([...bodyTokens], context);
|
|
5274
|
+
capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
|
|
5190
5275
|
this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
|
|
5191
5276
|
return [rendered, i];
|
|
5192
5277
|
}
|
|
@@ -29385,14 +29470,16 @@ function downloadSkillsSync(jobs) {
|
|
|
29385
29470
|
const jobs = JSON.parse(process.argv[1]);
|
|
29386
29471
|
const fs = require("node:fs");
|
|
29387
29472
|
const path = require("node:path");
|
|
29473
|
+
const transientStatuses = new Set([429, 500, 502, 503, 504]);
|
|
29388
29474
|
async function fetchOne(job) {
|
|
29389
29475
|
const resp = await fetch(job.url, { signal: AbortSignal.timeout(15000) });
|
|
29390
|
-
if (!resp.ok)
|
|
29476
|
+
if (!resp.ok) return { ok: false, retry: transientStatuses.has(resp.status) };
|
|
29391
29477
|
const buf = Buffer.from(await resp.arrayBuffer());
|
|
29392
29478
|
for (const dest of job.dests) {
|
|
29393
29479
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
29394
29480
|
fs.writeFileSync(dest, buf);
|
|
29395
29481
|
}
|
|
29482
|
+
return { ok: true, retry: false };
|
|
29396
29483
|
}
|
|
29397
29484
|
(async () => {
|
|
29398
29485
|
const ok = [];
|
|
@@ -29401,9 +29488,11 @@ function downloadSkillsSync(jobs) {
|
|
|
29401
29488
|
const failed = [];
|
|
29402
29489
|
await Promise.all(pending.map(async (job) => {
|
|
29403
29490
|
try {
|
|
29404
|
-
await fetchOne(job);
|
|
29405
|
-
ok.push(job.url);
|
|
29491
|
+
const result = await fetchOne(job);
|
|
29492
|
+
if (result.ok) ok.push(job.url);
|
|
29493
|
+
else if (result.retry) failed.push(job);
|
|
29406
29494
|
} catch {
|
|
29495
|
+
// DNS, TLS, timeout and connection failures are transient.
|
|
29407
29496
|
failed.push(job);
|
|
29408
29497
|
}
|
|
29409
29498
|
}));
|
package/types/core/src/ai.d.ts
CHANGED
|
@@ -6,6 +6,35 @@ export interface AiTool {
|
|
|
6
6
|
}
|
|
7
7
|
export declare const AI_TOOLS: AiTool[];
|
|
8
8
|
export declare const DEV_SKILL = "tina4-developer-nodejs";
|
|
9
|
+
/**
|
|
10
|
+
* Fetch a set of skill files over the network SYNCHRONOUSLY.
|
|
11
|
+
*
|
|
12
|
+
* Node has no built-in synchronous HTTP, and the whole installer chain
|
|
13
|
+
* (installSelected → installForTool → installClaudeSkills) is synchronous and
|
|
14
|
+
* can't be made async without breaking its existing callers/tests. So we run
|
|
15
|
+
* ONE blocking child `node` process that fetches every URL in parallel with the
|
|
16
|
+
* global `fetch` (Node 18+) and writes each body to all of its destinations.
|
|
17
|
+
* The child prints a JSON array of the URLs it fetched successfully; any fetch
|
|
18
|
+
* failure is skipped, never fatal.
|
|
19
|
+
*
|
|
20
|
+
* MEASURED (2026-08-13): a real GitHub raw-content fetch occasionally drops a
|
|
21
|
+
* request under load (transient DNS/TLS hiccup, not a missing file — every
|
|
22
|
+
* URL here resolves fine on its own) while its siblings in the same batch
|
|
23
|
+
* succeed. One retry pass over only transport failures and transient HTTP
|
|
24
|
+
* statuses, still inside the same child process, fixes that for real installer
|
|
25
|
+
* users too. Permanent 4xx responses are final answers and are not retried.
|
|
26
|
+
*
|
|
27
|
+
* Exported (like `writeOrMerge`/`markersFor`/`skillBlock` above) so
|
|
28
|
+
* aiFetchRetry.test.ts can drive it directly against a real local server —
|
|
29
|
+
* a pure visibility change, no behaviour change.
|
|
30
|
+
*
|
|
31
|
+
* @param jobs one entry per unique URL, with every file path it should land in
|
|
32
|
+
* @returns the set of URLs that were fetched and written to disk
|
|
33
|
+
*/
|
|
34
|
+
export declare function downloadSkillsSync(jobs: {
|
|
35
|
+
url: string;
|
|
36
|
+
dests: string[];
|
|
37
|
+
}[]): Set<string>;
|
|
9
38
|
/**
|
|
10
39
|
* Install the Tina4 SKILL.md skills into the project AND the global
|
|
11
40
|
* ~/.claude/skills, fetched from the release ref matching this framework
|
|
@@ -14,6 +14,15 @@ export interface LiveResponse {
|
|
|
14
14
|
}
|
|
15
15
|
/** WebSocket broadcaster hook wired by @tina4/core so pushLive can broadcast. */
|
|
16
16
|
export type LiveBroadcaster = (wsPath: string | null, name: string, envelope: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Cache for parsed filter chains: expr string -> [variable, filters].
|
|
19
|
+
* Exported (like TEMPLATE_CACHE_MAX) so the ADR-0004 bound has something for
|
|
20
|
+
* a test to inspect directly — module-level state has no instance to read
|
|
21
|
+
* off, unlike `compiled`/`compiledStrings`/`fragmentCache`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const filterChainCache: Map<string, [string, [string, unknown[]][]]>;
|
|
24
|
+
/** Cache for parsed dotted/bracket paths: expr string -> [parts, fromBracket]. Exported for the same reason as filterChainCache. */
|
|
25
|
+
export declare const pathParseCache: Map<string, [string[], boolean[]]>;
|
|
17
26
|
/**
|
|
18
27
|
* Hard cap on the template caches — `compiled` and `compiledStrings`
|
|
19
28
|
* (ADR-0004, parity with PHP/Python/Ruby TEMPLATE_CACHE_MAX).
|
|
@@ -26,6 +35,18 @@ export type LiveBroadcaster = (wsPath: string | null, name: string, envelope: st
|
|
|
26
35
|
* dynamically adds an entry per distinct string.
|
|
27
36
|
*/
|
|
28
37
|
export declare const TEMPLATE_CACHE_MAX = 256;
|
|
38
|
+
/**
|
|
39
|
+
* Hard cap on every per-expression memo cache — `filterChainCache` and
|
|
40
|
+
* `pathParseCache` (ADR-0004, parity with PHP's MEMO_CACHE_MAX and the
|
|
41
|
+
* Python master's `@lru_cache(maxsize=1024)` on the equivalent module-level
|
|
42
|
+
* parsers). Deliberately higher than TEMPLATE_CACHE_MAX: one entry here is a
|
|
43
|
+
* small parsed-path array, orders of magnitude smaller than a token list.
|
|
44
|
+
*
|
|
45
|
+
* Also reused for `fragmentCache` (the `{% cache %}` tag's runtime store):
|
|
46
|
+
* TEMPLATE_CACHE_MAX, not this one — a rendered fragment is a whole HTML
|
|
47
|
+
* string, the same order of magnitude as a compiled template.
|
|
48
|
+
*/
|
|
49
|
+
export declare const MEMO_CACHE_MAX = 1024;
|
|
29
50
|
/**
|
|
30
51
|
* Set the session ID used by formToken() / form_token() for CSRF session binding.
|
|
31
52
|
*/
|
|
@@ -95,20 +116,16 @@ export declare class Frond {
|
|
|
95
116
|
sandbox(filters?: string[], tags?: string[], vars?: string[]): Frond;
|
|
96
117
|
unsandbox(): Frond;
|
|
97
118
|
/**
|
|
98
|
-
* Register a custom filter
|
|
99
|
-
*
|
|
100
|
-
* the live instance's local filter map also receives the addition
|
|
101
|
-
* immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
|
|
119
|
+
* Register a custom filter on this instance only. Use the static method
|
|
120
|
+
* for process-global registration. tina4: ADR-0052.
|
|
102
121
|
*/
|
|
103
122
|
addFilter(name: string, fn: FilterFn): void;
|
|
104
123
|
/**
|
|
105
|
-
* Register a global variable
|
|
106
|
-
* at class level — see ``addFilter`` for the dual-call semantics.
|
|
124
|
+
* Register a global variable on this instance only.
|
|
107
125
|
*/
|
|
108
126
|
addGlobal(name: string, value: unknown): void;
|
|
109
127
|
/**
|
|
110
|
-
* Register a custom test
|
|
111
|
-
* ``addFilter`` for the dual-call semantics.
|
|
128
|
+
* Register a custom test on this instance only.
|
|
112
129
|
*/
|
|
113
130
|
addTest(name: string, fn: TestFn): void;
|
|
114
131
|
/**
|
|
@@ -143,6 +160,31 @@ export declare class Frond {
|
|
|
143
160
|
private executeWithSource;
|
|
144
161
|
private execute;
|
|
145
162
|
private extractBlocks;
|
|
163
|
+
/**
|
|
164
|
+
* Depth-aware block substitution against `source` (typically the
|
|
165
|
+
* fully-resolved root template).
|
|
166
|
+
*
|
|
167
|
+
* A single regex `.replace()` pass (the flat `pattern` this replaces in
|
|
168
|
+
* renderWithBlocks) pairs an OUTER block's open tag with the FIRST
|
|
169
|
+
* `{% endblock %}` found -- which, when the outer block wraps a NESTED
|
|
170
|
+
* `{% block %}`, is the nested block's own close tag, not the outer's.
|
|
171
|
+
* That silently truncates the outer block's captured content and drops
|
|
172
|
+
* everything after the inner endblock (the root-nested-block
|
|
173
|
+
* content-loss bug). This scans with an open/close depth counter
|
|
174
|
+
* instead (mirroring extractBlocks), so an outer block always captures
|
|
175
|
+
* its FULL body, nested child blocks included.
|
|
176
|
+
*
|
|
177
|
+
* The content chosen for each block -- the child override in `blocks`
|
|
178
|
+
* if present, else the block's own default body -- is then recursively
|
|
179
|
+
* substituted against the SAME `blocks` map before being tokenized and
|
|
180
|
+
* rendered, so a block nested inside another block resolves correctly
|
|
181
|
+
* regardless of which template in the inheritance chain declared the
|
|
182
|
+
* nesting (the root, an intermediate, however many levels deep).
|
|
183
|
+
*
|
|
184
|
+
* `{{ parent() }}` / `{{ super() }}` inside a block still render that
|
|
185
|
+
* block's OWN default content at this level (lazy, on first call).
|
|
186
|
+
*/
|
|
187
|
+
private substituteBlocks;
|
|
146
188
|
private renderWithBlocks;
|
|
147
189
|
private renderTokens;
|
|
148
190
|
/**
|