tina4-nodejs 3.13.133 → 3.13.134
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/README.md +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +3181 -3051
- package/packages/cli/src/commands/generate.ts +33 -22
- package/packages/cli/src/commands/lint.ts +77 -111
- package/packages/core/dist/index.js +3090 -2952
- package/packages/core/src/.tina4-metrics.json +15004 -0
- package/packages/core/src/aiClient.ts +199 -161
- package/packages/core/src/dispatchPipeline.ts +65 -67
- package/packages/core/src/docs.ts +52 -544
- package/packages/core/src/docsParser.ts +270 -0
- package/packages/core/src/docsScanner.ts +121 -0
- package/packages/core/src/docsSignatures.ts +165 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/logger.ts +68 -82
- package/packages/core/src/mcp.ts +32 -60
- package/packages/core/src/messenger.ts +136 -157
- package/packages/core/src/middleware.ts +56 -60
- package/packages/core/src/plan.ts +78 -70
- package/packages/core/src/projectIndex.ts +15 -288
- package/packages/core/src/projectIndexExtractors.ts +126 -0
- package/packages/core/src/projectIndexStorage.ts +122 -0
- package/packages/core/src/push.ts +281 -0
- package/packages/core/src/server.ts +182 -183
- package/packages/frond/dist/index.js +607 -770
- package/packages/frond/src/engine.ts +670 -818
- package/packages/orm/dist/index.js +3100 -2965
- package/packages/orm/src/adapters/mongodb.ts +99 -144
- package/packages/orm/src/baseModel.ts +429 -515
- package/packages/orm/src/fakeData.ts +73 -61
- package/packages/orm/src/migration.ts +96 -126
- package/packages/orm/src/seeder.ts +6 -238
- package/packages/orm/src/seederTable.ts +101 -0
- package/packages/orm/src/seederTypes.ts +14 -0
- package/packages/orm/src/validation.ts +97 -80
- package/types/core/src/aiClient.d.ts +5 -0
- package/types/core/src/docsParser.d.ts +28 -0
- package/types/core/src/docsScanner.d.ts +1 -0
- package/types/core/src/docsSignatures.d.ts +11 -0
- package/types/core/src/index.d.ts +2 -0
- package/types/core/src/messenger.d.ts +8 -0
- package/types/core/src/projectIndexExtractors.d.ts +3 -0
- package/types/core/src/projectIndexStorage.d.ts +13 -0
- package/types/core/src/push.d.ts +45 -0
- package/types/frond/src/engine.d.ts +25 -0
- package/types/orm/src/fakeData.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +3 -89
- package/types/orm/src/seederTable.d.ts +9 -0
- package/types/orm/src/seederTypes.d.ts +16 -0
|
@@ -93,60 +93,53 @@ var JSON_UNSAFE_MAP = {
|
|
|
93
93
|
function jsonSafe(value) {
|
|
94
94
|
return new SafeString(jsonText(value).replace(JSON_UNSAFE_RE, (c) => JSON_UNSAFE_MAP[c]));
|
|
95
95
|
}
|
|
96
|
-
function
|
|
97
|
-
if (value === null) return "null";
|
|
98
|
-
if (value === void 0) return "undefined";
|
|
99
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
100
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
101
|
-
if (typeof value === "bigint") return `${value.toString()}n
|
|
102
|
-
if (typeof value === "symbol") return value.toString();
|
|
103
|
-
if (typeof value === "function") {
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
function inspectPrimitive(value) {
|
|
97
|
+
if (value === null) return { handled: true, output: "null" };
|
|
98
|
+
if (value === void 0) return { handled: true, output: "undefined" };
|
|
99
|
+
if (typeof value === "string") return { handled: true, output: JSON.stringify(value) };
|
|
100
|
+
if (typeof value === "number" || typeof value === "boolean") return { handled: true, output: String(value) };
|
|
101
|
+
if (typeof value === "bigint") return { handled: true, output: `${value.toString()}n` };
|
|
102
|
+
if (typeof value === "symbol") return { handled: true, output: value.toString() };
|
|
103
|
+
if (typeof value === "function") return { handled: true, output: `[Function: ${value.name || "(anonymous)"}]` };
|
|
104
|
+
return { handled: false, output: "" };
|
|
105
|
+
}
|
|
106
|
+
function inspectCollection(obj, seen, depth) {
|
|
107
|
+
if (obj instanceof Map) {
|
|
108
|
+
if (obj.size === 0) return "Map(0) {}";
|
|
109
|
+
const entries = [...obj].map(([key, value]) => `${inspectValue(key, seen, depth + 1)} => ${inspectValue(value, seen, depth + 1)}`);
|
|
110
|
+
return `Map(${obj.size}) { ${entries.join(", ")} }`;
|
|
111
|
+
}
|
|
112
|
+
if (obj instanceof Set) {
|
|
113
|
+
if (obj.size === 0) return "Set(0) {}";
|
|
114
|
+
return `Set(${obj.size}) { ${[...obj].map((value) => inspectValue(value, seen, depth + 1)).join(", ")} }`;
|
|
106
115
|
}
|
|
116
|
+
if (Array.isArray(obj)) {
|
|
117
|
+
if (obj.length === 0) return "[]";
|
|
118
|
+
return `[${obj.map((value) => inspectValue(value, seen, depth + 1)).join(", ")}]`;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function inspectObject(obj, seen, depth) {
|
|
123
|
+
if (obj instanceof Date) return `Date(${obj.toISOString()})`;
|
|
124
|
+
if (obj instanceof RegExp) return obj.toString();
|
|
125
|
+
if (obj instanceof Error) return `${obj.constructor.name}(${JSON.stringify(obj.message)})`;
|
|
126
|
+
const collection = inspectCollection(obj, seen, depth);
|
|
127
|
+
if (collection !== null) return collection;
|
|
128
|
+
const keys = Object.keys(obj);
|
|
129
|
+
const className = obj.constructor && obj.constructor.name !== "Object" ? `${obj.constructor.name} ` : "";
|
|
130
|
+
if (keys.length === 0) return `${className}{}`;
|
|
131
|
+
const props = keys.map((key) => `${key}: ${inspectValue(obj[key], seen, depth + 1)}`);
|
|
132
|
+
return `${className}{ ${props.join(", ")} }`;
|
|
133
|
+
}
|
|
134
|
+
function inspectValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
135
|
+
const primitive = inspectPrimitive(value);
|
|
136
|
+
if (primitive.handled) return primitive.output;
|
|
107
137
|
const obj = value;
|
|
108
138
|
if (seen.has(obj)) return "[Circular]";
|
|
109
139
|
seen.add(obj);
|
|
110
140
|
if (depth > 8) return "[...]";
|
|
111
141
|
try {
|
|
112
|
-
|
|
113
|
-
return `Date(${obj.toISOString()})`;
|
|
114
|
-
}
|
|
115
|
-
if (obj instanceof RegExp) {
|
|
116
|
-
return obj.toString();
|
|
117
|
-
}
|
|
118
|
-
if (obj instanceof Error) {
|
|
119
|
-
return `${obj.constructor.name}(${JSON.stringify(obj.message)})`;
|
|
120
|
-
}
|
|
121
|
-
if (obj instanceof Map) {
|
|
122
|
-
if (obj.size === 0) return "Map(0) {}";
|
|
123
|
-
const entries = [];
|
|
124
|
-
for (const [k, v] of obj) {
|
|
125
|
-
entries.push(`${inspectValue(k, seen, depth + 1)} => ${inspectValue(v, seen, depth + 1)}`);
|
|
126
|
-
}
|
|
127
|
-
return `Map(${obj.size}) { ${entries.join(", ")} }`;
|
|
128
|
-
}
|
|
129
|
-
if (obj instanceof Set) {
|
|
130
|
-
if (obj.size === 0) return "Set(0) {}";
|
|
131
|
-
const items = [];
|
|
132
|
-
for (const v of obj) {
|
|
133
|
-
items.push(inspectValue(v, seen, depth + 1));
|
|
134
|
-
}
|
|
135
|
-
return `Set(${obj.size}) { ${items.join(", ")} }`;
|
|
136
|
-
}
|
|
137
|
-
if (Array.isArray(obj)) {
|
|
138
|
-
if (obj.length === 0) return "[]";
|
|
139
|
-
const items = obj.map((v) => inspectValue(v, seen, depth + 1));
|
|
140
|
-
return `[${items.join(", ")}]`;
|
|
141
|
-
}
|
|
142
|
-
const keys = Object.keys(obj);
|
|
143
|
-
const className = obj.constructor && obj.constructor.name !== "Object" ? `${obj.constructor.name} ` : "";
|
|
144
|
-
if (keys.length === 0) return `${className}{}`;
|
|
145
|
-
const props = keys.map((k) => {
|
|
146
|
-
const v = obj[k];
|
|
147
|
-
return `${k}: ${inspectValue(v, seen, depth + 1)}`;
|
|
148
|
-
});
|
|
149
|
-
return `${className}{ ${props.join(", ")} }`;
|
|
142
|
+
return inspectObject(obj, seen, depth);
|
|
150
143
|
} finally {
|
|
151
144
|
seen.delete(obj);
|
|
152
145
|
}
|
|
@@ -172,6 +165,33 @@ var FILTER_WITH_ARGS_RE = /^(\w+)\s*\(([\s\S]*)\)$/;
|
|
|
172
165
|
var FILTER_COMPARISON_RE = /^(\w+)\s*(!=|==|>=|<=|>|<)\s*(.+)$/;
|
|
173
166
|
var TITLE_WORD_RE = /\b\w/g;
|
|
174
167
|
var STRIP_TAGS_RE = /<[^>]+>/g;
|
|
168
|
+
var FAST_FILTERS = {
|
|
169
|
+
upper: (value) => String(value).toUpperCase(),
|
|
170
|
+
lower: (value) => String(value).toLowerCase(),
|
|
171
|
+
trim: (value) => String(value).trim(),
|
|
172
|
+
length: (value) => Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "object" && value !== null ? Object.keys(value).length : 0,
|
|
173
|
+
capitalize: (value) => {
|
|
174
|
+
const s = String(value);
|
|
175
|
+
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
|
176
|
+
},
|
|
177
|
+
title: (value) => String(value).replace(TITLE_WORD_RE, (c) => c.toUpperCase()),
|
|
178
|
+
string: (value) => String(value),
|
|
179
|
+
int: (value) => value ? parseInt(String(value), 10) || 0 : 0,
|
|
180
|
+
float: (value) => value ? parseFloat(String(value)) || 0 : 0,
|
|
181
|
+
abs: (value) => typeof value === "number" ? Math.abs(value) : value,
|
|
182
|
+
striptags: (value) => String(value).replace(STRIP_TAGS_RE, ""),
|
|
183
|
+
first: (value) => Array.isArray(value) ? value[0] ?? null : null,
|
|
184
|
+
last: (value) => Array.isArray(value) ? value[value.length - 1] ?? null : null,
|
|
185
|
+
keys: (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value) : [],
|
|
186
|
+
values: (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [],
|
|
187
|
+
json_encode: (value) => jsonSafe(value),
|
|
188
|
+
dump: (value) => renderDump(value),
|
|
189
|
+
nl2br: (value) => new SafeString(htmlEscape(String(value)).replace(/\n/g, "<br />\n")),
|
|
190
|
+
unique: (value) => Array.isArray(value) ? [...new Set(value)] : value,
|
|
191
|
+
sort: (value) => Array.isArray(value) ? [...value].sort() : value,
|
|
192
|
+
reverse: (value) => Array.isArray(value) ? [...value].reverse() : String(value).split("").reverse().join(""),
|
|
193
|
+
filter: (value) => Array.isArray(value) ? value.filter(Boolean) : value
|
|
194
|
+
};
|
|
175
195
|
var FORMAT_RE = /%%|%([-+ 0]*)(\d+)?(?:\.(\d+))?([sdifFeEgGxXob])/g;
|
|
176
196
|
var LEADING_WS_RE = /^\s+/;
|
|
177
197
|
var TRAILING_WS_RE = /\s+$/;
|
|
@@ -277,153 +297,136 @@ function extendsTarget(source) {
|
|
|
277
297
|
const match = source.match(EXTENDS_RE);
|
|
278
298
|
return match ? match[1] : "";
|
|
279
299
|
}
|
|
280
|
-
function
|
|
281
|
-
expr = expr.trim();
|
|
282
|
-
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
|
|
283
|
-
return expr.slice(1, -1);
|
|
284
|
-
}
|
|
285
|
-
if (NUMERIC_RE.test(expr)) {
|
|
286
|
-
return expr.includes(".") ? parseFloat(expr) : parseInt(expr, 10);
|
|
287
|
-
}
|
|
288
|
-
if (expr === "true") return true;
|
|
289
|
-
if (expr === "false") return false;
|
|
290
|
-
if (expr === "null" || expr === "none" || expr === "None") return null;
|
|
291
|
-
if (expr.startsWith("[") && expr.endsWith("]")) {
|
|
292
|
-
const inner = expr.slice(1, -1).trim();
|
|
293
|
-
if (inner === "") return [];
|
|
294
|
-
const items = splitArgs(inner);
|
|
295
|
-
return items.map((item) => evalExpr(item.trim(), context));
|
|
296
|
-
}
|
|
297
|
-
let parts;
|
|
298
|
-
let fromBracket;
|
|
300
|
+
function parsePath(expr) {
|
|
299
301
|
const cachedPath = pathParseCache.get(expr);
|
|
300
|
-
if (cachedPath)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
if (ch === "." && depth === 0) {
|
|
332
|
-
if (current) {
|
|
333
|
-
parts.push(current);
|
|
334
|
-
fromBracket.push(false);
|
|
335
|
-
}
|
|
336
|
-
current = "";
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
if (ch === "[" && depth === 0) {
|
|
340
|
-
if (current) {
|
|
341
|
-
parts.push(current);
|
|
342
|
-
fromBracket.push(false);
|
|
343
|
-
}
|
|
344
|
-
current = "";
|
|
345
|
-
const end = expr.indexOf("]", i + 1);
|
|
346
|
-
if (end !== -1) {
|
|
347
|
-
parts.push(expr.slice(i + 1, end));
|
|
348
|
-
fromBracket.push(true);
|
|
349
|
-
i = end;
|
|
350
|
-
}
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
current += ch;
|
|
354
|
-
}
|
|
302
|
+
if (cachedPath) return cachedPath;
|
|
303
|
+
const parts = [];
|
|
304
|
+
const fromBracket = [];
|
|
305
|
+
let current = "";
|
|
306
|
+
let depth = 0;
|
|
307
|
+
let inQuote = null;
|
|
308
|
+
for (let i = 0; i < expr.length; i++) {
|
|
309
|
+
const ch = expr[i];
|
|
310
|
+
if (inQuote) {
|
|
311
|
+
current += ch;
|
|
312
|
+
if (ch === inQuote) inQuote = null;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (ch === '"' || ch === "'") {
|
|
316
|
+
inQuote = ch;
|
|
317
|
+
current += ch;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (ch === "(") {
|
|
321
|
+
depth++;
|
|
322
|
+
current += ch;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (ch === ")") {
|
|
326
|
+
depth--;
|
|
327
|
+
current += ch;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (ch === "." && depth === 0) {
|
|
355
331
|
if (current) {
|
|
356
332
|
parts.push(current);
|
|
357
333
|
fromBracket.push(false);
|
|
358
334
|
}
|
|
359
|
-
|
|
360
|
-
capCache(pathParseCache, MEMO_CACHE_MAX);
|
|
361
|
-
pathParseCache.set(expr, [parts, fromBracket]);
|
|
362
|
-
}
|
|
363
|
-
let value = context;
|
|
364
|
-
for (let pi = 0; pi < parts.length; pi++) {
|
|
365
|
-
const part = parts[pi];
|
|
366
|
-
const isBracket = fromBracket[pi];
|
|
367
|
-
if (value === null || value === void 0) return null;
|
|
368
|
-
const methodMatch = part.match(METHOD_CALL_RE);
|
|
369
|
-
if (methodMatch) {
|
|
370
|
-
const methodName = methodMatch[1];
|
|
371
|
-
const rawArgs = methodMatch[2] || "";
|
|
372
|
-
if (typeof value === "object" && value !== null && methodName in value) {
|
|
373
|
-
const fn = value[methodName];
|
|
374
|
-
if (typeof fn === "function") {
|
|
375
|
-
if (rawArgs.trim()) {
|
|
376
|
-
const argParts = splitArgs(rawArgs);
|
|
377
|
-
const evalArgs = argParts.map((a) => evalExpr(a.trim(), context));
|
|
378
|
-
value = fn.apply(value, evalArgs);
|
|
379
|
-
} else {
|
|
380
|
-
value = fn.call(value);
|
|
381
|
-
}
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
const isQuotedPart = part.startsWith('"') && part.endsWith('"') || part.startsWith("'") && part.endsWith("'");
|
|
388
|
-
if (isBracket && part.includes(":") && !isQuotedPart) {
|
|
389
|
-
const sliceParts = part.split(":", 2);
|
|
390
|
-
const sStart = sliceParts[0].trim() ? parseInt(String(evalExpr(sliceParts[0].trim(), context)), 10) : void 0;
|
|
391
|
-
const sEnd = sliceParts[1].trim() ? parseInt(String(evalExpr(sliceParts[1].trim(), context)), 10) : void 0;
|
|
392
|
-
if (Array.isArray(value)) {
|
|
393
|
-
value = value.slice(sStart ?? 0, sEnd);
|
|
394
|
-
} else if (typeof value === "string") {
|
|
395
|
-
value = value.slice(sStart ?? 0, sEnd);
|
|
396
|
-
} else {
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
335
|
+
current = "";
|
|
399
336
|
continue;
|
|
400
337
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const asNum = parseInt(part, 10);
|
|
406
|
-
if (!isNaN(asNum) && String(asNum) === part) {
|
|
407
|
-
key = asNum;
|
|
408
|
-
} else if (isBracket) {
|
|
409
|
-
const resolved = evalExpr(part, context);
|
|
410
|
-
key = resolved !== void 0 ? String(resolved) : part;
|
|
411
|
-
} else {
|
|
412
|
-
key = part;
|
|
338
|
+
if (ch === "[" && depth === 0) {
|
|
339
|
+
if (current) {
|
|
340
|
+
parts.push(current);
|
|
341
|
+
fromBracket.push(false);
|
|
413
342
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
value = typeof v === "function" ? v.call(value) : v;
|
|
421
|
-
} else {
|
|
422
|
-
return null;
|
|
343
|
+
current = "";
|
|
344
|
+
const end = expr.indexOf("]", i + 1);
|
|
345
|
+
if (end !== -1) {
|
|
346
|
+
parts.push(expr.slice(i + 1, end));
|
|
347
|
+
fromBracket.push(true);
|
|
348
|
+
i = end;
|
|
423
349
|
}
|
|
424
|
-
|
|
425
|
-
return null;
|
|
350
|
+
continue;
|
|
426
351
|
}
|
|
352
|
+
current += ch;
|
|
353
|
+
}
|
|
354
|
+
if (current) {
|
|
355
|
+
parts.push(current);
|
|
356
|
+
fromBracket.push(false);
|
|
357
|
+
}
|
|
358
|
+
const parsed = [parts, fromBracket];
|
|
359
|
+
capCache(pathParseCache, MEMO_CACHE_MAX);
|
|
360
|
+
pathParseCache.set(expr, parsed);
|
|
361
|
+
return parsed;
|
|
362
|
+
}
|
|
363
|
+
function resolveMethodPart(value, part, context) {
|
|
364
|
+
const methodMatch = part.match(METHOD_CALL_RE);
|
|
365
|
+
if (!methodMatch || typeof value !== "object" || value === null) return { matched: false, value };
|
|
366
|
+
const methodName = methodMatch[1];
|
|
367
|
+
const rawArgs = methodMatch[2] || "";
|
|
368
|
+
const fn = value[methodName];
|
|
369
|
+
if (typeof fn !== "function") return { matched: false, value };
|
|
370
|
+
const args = rawArgs.trim() ? splitArgs(rawArgs).map((a) => evalExpr(a.trim(), context)) : [];
|
|
371
|
+
return { matched: true, value: fn.apply(value, args) };
|
|
372
|
+
}
|
|
373
|
+
function resolvePathKey(part, isBracket, context) {
|
|
374
|
+
const isQuotedPart = part.startsWith('"') && part.endsWith('"') || part.startsWith("'") && part.endsWith("'");
|
|
375
|
+
if (isQuotedPart) return part.slice(1, -1);
|
|
376
|
+
const asNum = parseInt(part, 10);
|
|
377
|
+
if (!isNaN(asNum) && String(asNum) === part) return asNum;
|
|
378
|
+
if (isBracket) {
|
|
379
|
+
const resolved = evalExpr(part, context);
|
|
380
|
+
return resolved !== void 0 ? String(resolved) : part;
|
|
381
|
+
}
|
|
382
|
+
return part;
|
|
383
|
+
}
|
|
384
|
+
function resolvePathPart(value, key) {
|
|
385
|
+
if (typeof value !== "object" || value === null) return { found: false, value: null };
|
|
386
|
+
if (Array.isArray(value) && typeof key === "number") return { found: true, value: value[key] };
|
|
387
|
+
if (!(key in value)) return { found: false, value: null };
|
|
388
|
+
const member = value[key];
|
|
389
|
+
return { found: true, value: typeof member === "function" ? member.call(value) : member };
|
|
390
|
+
}
|
|
391
|
+
function resolveLiteral(expr, context) {
|
|
392
|
+
if (expr.startsWith('"') && expr.endsWith('"') || expr.startsWith("'") && expr.endsWith("'")) {
|
|
393
|
+
return { handled: true, value: expr.slice(1, -1) };
|
|
394
|
+
}
|
|
395
|
+
if (NUMERIC_RE.test(expr)) return { handled: true, value: expr.includes(".") ? parseFloat(expr) : parseInt(expr, 10) };
|
|
396
|
+
if (expr === "true") return { handled: true, value: true };
|
|
397
|
+
if (expr === "false") return { handled: true, value: false };
|
|
398
|
+
if (expr === "null" || expr === "none" || expr === "None") return { handled: true, value: null };
|
|
399
|
+
if (expr.startsWith("[") && expr.endsWith("]")) {
|
|
400
|
+
const inner = expr.slice(1, -1).trim();
|
|
401
|
+
return { handled: true, value: inner === "" ? [] : splitArgs(inner).map((item) => evalExpr(item.trim(), context)) };
|
|
402
|
+
}
|
|
403
|
+
return { handled: false, value: void 0 };
|
|
404
|
+
}
|
|
405
|
+
function resolvePathSegment(value, part, isBracket, context) {
|
|
406
|
+
if (value === null || value === void 0) return { found: false, value: null };
|
|
407
|
+
const method = resolveMethodPart(value, part, context);
|
|
408
|
+
if (part.match(METHOD_CALL_RE)) return method.matched ? { found: true, value: method.value } : { found: false, value: null };
|
|
409
|
+
const isQuotedPart = part.startsWith('"') && part.endsWith('"') || part.startsWith("'") && part.endsWith("'");
|
|
410
|
+
if (isBracket && part.includes(":") && !isQuotedPart) {
|
|
411
|
+
const [rawStart, rawEnd] = part.split(":", 2);
|
|
412
|
+
const start = rawStart.trim() ? parseInt(String(evalExpr(rawStart.trim(), context)), 10) : void 0;
|
|
413
|
+
const end = rawEnd.trim() ? parseInt(String(evalExpr(rawEnd.trim(), context)), 10) : void 0;
|
|
414
|
+
if (Array.isArray(value)) return { found: true, value: value.slice(start ?? 0, end) };
|
|
415
|
+
if (typeof value === "string") return { found: true, value: value.slice(start ?? 0, end) };
|
|
416
|
+
return { found: false, value: null };
|
|
417
|
+
}
|
|
418
|
+
return resolvePathPart(value, resolvePathKey(part, isBracket, context));
|
|
419
|
+
}
|
|
420
|
+
function resolveVar(expr, context) {
|
|
421
|
+
expr = expr.trim();
|
|
422
|
+
const literal = resolveLiteral(expr, context);
|
|
423
|
+
if (literal.handled) return literal.value;
|
|
424
|
+
const [parts, fromBracket] = parsePath(expr);
|
|
425
|
+
let value = context;
|
|
426
|
+
for (let pi = 0; pi < parts.length; pi++) {
|
|
427
|
+
const result = resolvePathSegment(value, parts[pi], fromBracket[pi], context);
|
|
428
|
+
if (!result.found) return null;
|
|
429
|
+
value = result.value;
|
|
427
430
|
}
|
|
428
431
|
return value;
|
|
429
432
|
}
|
|
@@ -1096,6 +1099,41 @@ function numberFormat(value, decimals, decimalPoint = ".", thousandsSep = ",") {
|
|
|
1096
1099
|
const formatted = intPart.replace(THOUSANDS_RE, thousandsSep);
|
|
1097
1100
|
return decPart ? `${formatted}${decimalPoint}${decPart}` : formatted;
|
|
1098
1101
|
}
|
|
1102
|
+
function formatInteger(type, arg) {
|
|
1103
|
+
const integer = Math.trunc(Number(arg) || 0);
|
|
1104
|
+
if (type === "d" || type === "i") return String(integer);
|
|
1105
|
+
if (type === "x") return integer.toString(16);
|
|
1106
|
+
if (type === "X") return integer.toString(16).toUpperCase();
|
|
1107
|
+
if (type === "o") return integer.toString(8);
|
|
1108
|
+
return integer.toString(2);
|
|
1109
|
+
}
|
|
1110
|
+
function formatDecimal(type, precision, arg) {
|
|
1111
|
+
const places = precision !== void 0 ? precision : 6;
|
|
1112
|
+
if (type === "f" || type === "F") return Number(arg).toFixed(places);
|
|
1113
|
+
if (type === "e" || type === "E") {
|
|
1114
|
+
const out = Number(arg).toExponential(places);
|
|
1115
|
+
return type === "E" ? out.toUpperCase() : out;
|
|
1116
|
+
}
|
|
1117
|
+
return String(Number(arg));
|
|
1118
|
+
}
|
|
1119
|
+
function formatValue(type, precision, arg) {
|
|
1120
|
+
if (type === "s") return String(arg ?? "");
|
|
1121
|
+
if ("dixXob".includes(type)) return formatInteger(type, arg);
|
|
1122
|
+
if ("fFeEgG".includes(type)) return formatDecimal(type, precision, arg);
|
|
1123
|
+
return String(arg ?? "");
|
|
1124
|
+
}
|
|
1125
|
+
function padFormattedValue(value, flags, width) {
|
|
1126
|
+
if (!width) return value;
|
|
1127
|
+
const targetWidth = parseInt(width, 10);
|
|
1128
|
+
if (value.length >= targetWidth) return value;
|
|
1129
|
+
const padFlags = flags || "";
|
|
1130
|
+
return padFlags.includes("-") ? value.padEnd(targetWidth, " ") : value.padStart(targetWidth, padFlags.includes("0") ? "0" : " ");
|
|
1131
|
+
}
|
|
1132
|
+
function formatMatch(match, flags, width, prec, type, arg) {
|
|
1133
|
+
if (match === "%%") return "%";
|
|
1134
|
+
const precision = prec !== void 0 ? parseInt(prec, 10) : void 0;
|
|
1135
|
+
return padFormattedValue(formatValue(type, precision, arg), flags, width);
|
|
1136
|
+
}
|
|
1099
1137
|
var BUILTIN_FILTERS = {
|
|
1100
1138
|
upper: (v) => String(v).toUpperCase(),
|
|
1101
1139
|
lower: (v) => String(v).toLowerCase(),
|
|
@@ -1244,58 +1282,7 @@ var BUILTIN_FILTERS = {
|
|
|
1244
1282
|
url_encode: (v) => encodeURIComponent(String(v)),
|
|
1245
1283
|
format: (v, ...args) => {
|
|
1246
1284
|
let idx = 0;
|
|
1247
|
-
return String(v).replace(FORMAT_RE, (m, flags, width, prec, type) =>
|
|
1248
|
-
if (m === "%%") return "%";
|
|
1249
|
-
const arg = args[idx++];
|
|
1250
|
-
const p = prec !== void 0 ? parseInt(String(prec), 10) : void 0;
|
|
1251
|
-
let out;
|
|
1252
|
-
switch (type) {
|
|
1253
|
-
case "s":
|
|
1254
|
-
out = String(arg ?? "");
|
|
1255
|
-
break;
|
|
1256
|
-
case "d":
|
|
1257
|
-
case "i":
|
|
1258
|
-
out = String(Math.trunc(Number(arg) || 0));
|
|
1259
|
-
break;
|
|
1260
|
-
case "f":
|
|
1261
|
-
case "F":
|
|
1262
|
-
out = Number(arg).toFixed(p !== void 0 ? p : 6);
|
|
1263
|
-
break;
|
|
1264
|
-
case "e":
|
|
1265
|
-
case "E": {
|
|
1266
|
-
out = Number(arg).toExponential(p !== void 0 ? p : 6);
|
|
1267
|
-
if (type === "E") out = out.toUpperCase();
|
|
1268
|
-
break;
|
|
1269
|
-
}
|
|
1270
|
-
case "g":
|
|
1271
|
-
case "G":
|
|
1272
|
-
out = String(Number(arg));
|
|
1273
|
-
break;
|
|
1274
|
-
case "x":
|
|
1275
|
-
out = Math.trunc(Number(arg) || 0).toString(16);
|
|
1276
|
-
break;
|
|
1277
|
-
case "X":
|
|
1278
|
-
out = Math.trunc(Number(arg) || 0).toString(16).toUpperCase();
|
|
1279
|
-
break;
|
|
1280
|
-
case "o":
|
|
1281
|
-
out = Math.trunc(Number(arg) || 0).toString(8);
|
|
1282
|
-
break;
|
|
1283
|
-
case "b":
|
|
1284
|
-
out = Math.trunc(Number(arg) || 0).toString(2);
|
|
1285
|
-
break;
|
|
1286
|
-
default:
|
|
1287
|
-
out = String(arg ?? "");
|
|
1288
|
-
}
|
|
1289
|
-
if (width) {
|
|
1290
|
-
const w = parseInt(String(width), 10);
|
|
1291
|
-
if (out.length < w) {
|
|
1292
|
-
const f = String(flags || "");
|
|
1293
|
-
if (f.includes("-")) out = out.padEnd(w, " ");
|
|
1294
|
-
else out = out.padStart(w, f.includes("0") ? "0" : " ");
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
return out;
|
|
1298
|
-
});
|
|
1285
|
+
return String(v).replace(FORMAT_RE, (m, flags, width, prec, type) => formatMatch(m, flags, width, prec, type, args[idx++]));
|
|
1299
1286
|
},
|
|
1300
1287
|
dump: (v) => JSON.stringify(v),
|
|
1301
1288
|
formToken: (v) => _generateFormToken(v != null ? String(v) : ""),
|
|
@@ -1422,6 +1409,7 @@ var Frond = class _Frond {
|
|
|
1422
1409
|
_allowedVars;
|
|
1423
1410
|
fragmentCache;
|
|
1424
1411
|
_autoEscape;
|
|
1412
|
+
blockHandlers;
|
|
1425
1413
|
/**
|
|
1426
1414
|
* Token pre-compilation cache for file templates.
|
|
1427
1415
|
*
|
|
@@ -1452,6 +1440,47 @@ var Frond = class _Frond {
|
|
|
1452
1440
|
this._allowedVars = null;
|
|
1453
1441
|
this.fragmentCache = /* @__PURE__ */ new Map();
|
|
1454
1442
|
this._autoEscape = true;
|
|
1443
|
+
this.blockHandlers = {
|
|
1444
|
+
if: (tokens, start, _content, context) => {
|
|
1445
|
+
const [output, next] = this.handleIf(tokens, start, context);
|
|
1446
|
+
return { output, next };
|
|
1447
|
+
},
|
|
1448
|
+
for: (tokens, start, _content, context) => {
|
|
1449
|
+
const [output, next] = this.handleFor(tokens, start, context);
|
|
1450
|
+
return { output, next };
|
|
1451
|
+
},
|
|
1452
|
+
set: (tokens, start, content, context) => {
|
|
1453
|
+
if (!content.includes("=")) return { next: this.handleSetBlock(tokens, start, context) };
|
|
1454
|
+
this.handleSet(content, context);
|
|
1455
|
+
return { next: start + 1 };
|
|
1456
|
+
},
|
|
1457
|
+
include: (_tokens, start, content, context) => ({ output: this.handleInclude(content, context), next: start + 1 }),
|
|
1458
|
+
macro: (tokens, start, _content, context) => ({ next: this.handleMacro(tokens, start, context) }),
|
|
1459
|
+
import: (_tokens, start, content, context) => {
|
|
1460
|
+
this.handleImportAs(content, context);
|
|
1461
|
+
return { next: start + 1 };
|
|
1462
|
+
},
|
|
1463
|
+
from: (_tokens, start, content, context) => {
|
|
1464
|
+
this.handleFromImport(content, context);
|
|
1465
|
+
return { next: start + 1 };
|
|
1466
|
+
},
|
|
1467
|
+
cache: (tokens, start, _content, context) => {
|
|
1468
|
+
const [output, next] = this.handleCache(tokens, start, context);
|
|
1469
|
+
return { output, next };
|
|
1470
|
+
},
|
|
1471
|
+
live: (tokens, start, _content, context) => {
|
|
1472
|
+
const [output, next] = this.handleLive(tokens, start, context);
|
|
1473
|
+
return { output, next };
|
|
1474
|
+
},
|
|
1475
|
+
spaceless: (tokens, start, _content, context) => {
|
|
1476
|
+
const [output, next] = this.handleSpaceless(tokens, start, context);
|
|
1477
|
+
return { output, next };
|
|
1478
|
+
},
|
|
1479
|
+
autoescape: (tokens, start, _content, context) => {
|
|
1480
|
+
const [output, next] = this.handleAutoescape(tokens, start, context);
|
|
1481
|
+
return { output, next };
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1455
1484
|
this.globals.formToken = (descriptor) => _generateFormToken(descriptor || "");
|
|
1456
1485
|
this.globals.form_token = (descriptor) => _generateFormToken(descriptor || "");
|
|
1457
1486
|
this.globals.formTokenValue = (descriptor) => _generateFormTokenValue(descriptor || "");
|
|
@@ -1773,97 +1802,72 @@ var Frond = class _Frond {
|
|
|
1773
1802
|
const result = this.substituteBlocks(parentSource, childBlocks, context);
|
|
1774
1803
|
return this.renderTokens(tokenize(result), context);
|
|
1775
1804
|
}
|
|
1805
|
+
dispatchBlock(tokens, start, content, tag, context) {
|
|
1806
|
+
if (!this.tagPermitted(tag)) return { next: this.skipDeniedTag(tokens, start, tag, content) };
|
|
1807
|
+
const handler = this.blockHandlers[tag];
|
|
1808
|
+
if (handler) return handler(tokens, start, content, context);
|
|
1809
|
+
if (tag === "block" || tag === "endblock" || tag === "extends") return { next: start + 1 };
|
|
1810
|
+
if (tag !== "" && !TERMINATOR_TAGS.has(tag)) {
|
|
1811
|
+
throw new Error(
|
|
1812
|
+
`Frond: unknown tag "${tag}" -- known tags are: ${[...KNOWN_TAGS].sort().join(", ")}`
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
return { next: start + 1 };
|
|
1816
|
+
}
|
|
1817
|
+
renderTextToken(tokens, index, output) {
|
|
1818
|
+
output.push(tokens[index][1]);
|
|
1819
|
+
return index + 1;
|
|
1820
|
+
}
|
|
1821
|
+
renderVarToken(tokens, index, context, output) {
|
|
1822
|
+
const [content, stripB, stripA] = stripTag(tokens[index][1]);
|
|
1823
|
+
if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].replace(TRAILING_WS_RE, "");
|
|
1824
|
+
const result = this.evalVar(content, context);
|
|
1825
|
+
output.push(result !== null && result !== void 0 ? String(result) : "");
|
|
1826
|
+
if (stripA && index + 1 < tokens.length && tokens[index + 1][0] === "TEXT") {
|
|
1827
|
+
tokens[index + 1] = ["TEXT", tokens[index + 1][1].replace(LEADING_WS_RE, "")];
|
|
1828
|
+
}
|
|
1829
|
+
return index + 1;
|
|
1830
|
+
}
|
|
1831
|
+
renderBlockToken(tokens, index, context, output) {
|
|
1832
|
+
const [content, stripB, stripA] = stripTag(tokens[index][1]);
|
|
1833
|
+
if (stripB && output.length > 0) output[output.length - 1] = output[output.length - 1].replace(TRAILING_WS_RE, "");
|
|
1834
|
+
const tag = content.split(/\s+/)[0] || "";
|
|
1835
|
+
if (stripA && index + 1 < tokens.length && tokens[index + 1][0] === "TEXT") {
|
|
1836
|
+
tokens[index + 1] = ["TEXT", tokens[index + 1][1].replace(LEADING_WS_RE, "")];
|
|
1837
|
+
}
|
|
1838
|
+
let next;
|
|
1839
|
+
if (tag === "if" && this.tagPermitted(tag)) {
|
|
1840
|
+
const [result, after] = this.handleIf(tokens, index, context);
|
|
1841
|
+
output.push(result);
|
|
1842
|
+
next = after;
|
|
1843
|
+
} else if (tag === "for" && this.tagPermitted(tag)) {
|
|
1844
|
+
const [result, after] = this.handleFor(tokens, index, context);
|
|
1845
|
+
output.push(result);
|
|
1846
|
+
next = after;
|
|
1847
|
+
} else {
|
|
1848
|
+
const block = this.dispatchBlock(tokens, index, content, tag, context);
|
|
1849
|
+
if (block.output !== void 0) output.push(block.output);
|
|
1850
|
+
next = block.next;
|
|
1851
|
+
}
|
|
1852
|
+
if (stripA && next < tokens.length && tokens[next][0] === "TEXT") {
|
|
1853
|
+
tokens[next] = ["TEXT", tokens[next][1].replace(LEADING_WS_RE, "")];
|
|
1854
|
+
}
|
|
1855
|
+
return next;
|
|
1856
|
+
}
|
|
1776
1857
|
renderTokens(tokens, context) {
|
|
1777
1858
|
context.__frond_apply_filters__ = this._applyFiltersBound;
|
|
1778
1859
|
const output = [];
|
|
1779
1860
|
let i = 0;
|
|
1780
1861
|
while (i < tokens.length) {
|
|
1781
|
-
const [ttype
|
|
1862
|
+
const [ttype] = tokens[i];
|
|
1782
1863
|
if (ttype === "TEXT") {
|
|
1783
|
-
|
|
1784
|
-
i++;
|
|
1864
|
+
i = this.renderTextToken(tokens, i, output);
|
|
1785
1865
|
} else if (ttype === "COMMENT") {
|
|
1786
1866
|
i++;
|
|
1787
1867
|
} else if (ttype === "VAR") {
|
|
1788
|
-
|
|
1789
|
-
if (stripB && output.length > 0) {
|
|
1790
|
-
output[output.length - 1] = output[output.length - 1].replace(TRAILING_WS_RE, "");
|
|
1791
|
-
}
|
|
1792
|
-
const result = this.evalVar(content, context);
|
|
1793
|
-
output.push(result !== null && result !== void 0 ? String(result) : "");
|
|
1794
|
-
if (stripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
|
|
1795
|
-
tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
|
|
1796
|
-
}
|
|
1797
|
-
i++;
|
|
1868
|
+
i = this.renderVarToken(tokens, i, context, output);
|
|
1798
1869
|
} else if (ttype === "BLOCK") {
|
|
1799
|
-
|
|
1800
|
-
if (stripB && output.length > 0) {
|
|
1801
|
-
output[output.length - 1] = output[output.length - 1].replace(TRAILING_WS_RE, "");
|
|
1802
|
-
}
|
|
1803
|
-
const parts = content.split(/\s+/);
|
|
1804
|
-
const tag = parts[0] || "";
|
|
1805
|
-
if (stripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
|
|
1806
|
-
tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
|
|
1807
|
-
}
|
|
1808
|
-
if (!this.tagPermitted(tag)) {
|
|
1809
|
-
i = this.skipDeniedTag(tokens, i, tag, content);
|
|
1810
|
-
} else if (tag === "if") {
|
|
1811
|
-
const [result, skip] = this.handleIf(tokens, i, context);
|
|
1812
|
-
output.push(result);
|
|
1813
|
-
i = skip;
|
|
1814
|
-
} else if (tag === "for") {
|
|
1815
|
-
const [result, skip] = this.handleFor(tokens, i, context);
|
|
1816
|
-
output.push(result);
|
|
1817
|
-
i = skip;
|
|
1818
|
-
} else if (tag === "set") {
|
|
1819
|
-
if (!content.includes("=")) {
|
|
1820
|
-
i = this.handleSetBlock(tokens, i, context);
|
|
1821
|
-
} else {
|
|
1822
|
-
this.handleSet(content, context);
|
|
1823
|
-
i++;
|
|
1824
|
-
}
|
|
1825
|
-
} else if (tag === "include") {
|
|
1826
|
-
const result = this.handleInclude(content, context);
|
|
1827
|
-
output.push(result);
|
|
1828
|
-
i++;
|
|
1829
|
-
} else if (tag === "macro") {
|
|
1830
|
-
const skip = this.handleMacro(tokens, i, context);
|
|
1831
|
-
i = skip;
|
|
1832
|
-
} else if (tag === "import") {
|
|
1833
|
-
this.handleImportAs(content, context);
|
|
1834
|
-
i++;
|
|
1835
|
-
} else if (tag === "from") {
|
|
1836
|
-
this.handleFromImport(content, context);
|
|
1837
|
-
i++;
|
|
1838
|
-
} else if (tag === "cache") {
|
|
1839
|
-
const [result, skip] = this.handleCache(tokens, i, context);
|
|
1840
|
-
output.push(result);
|
|
1841
|
-
i = skip;
|
|
1842
|
-
} else if (tag === "live") {
|
|
1843
|
-
const [result, skip] = this.handleLive(tokens, i, context);
|
|
1844
|
-
output.push(result);
|
|
1845
|
-
i = skip;
|
|
1846
|
-
} else if (tag === "spaceless") {
|
|
1847
|
-
const [result, skip] = this.handleSpaceless(tokens, i, context);
|
|
1848
|
-
output.push(result);
|
|
1849
|
-
i = skip;
|
|
1850
|
-
} else if (tag === "autoescape") {
|
|
1851
|
-
const [result, skip] = this.handleAutoescape(tokens, i, context);
|
|
1852
|
-
output.push(result);
|
|
1853
|
-
i = skip;
|
|
1854
|
-
} else if (tag === "block" || tag === "endblock" || tag === "extends") {
|
|
1855
|
-
i++;
|
|
1856
|
-
} else {
|
|
1857
|
-
i++;
|
|
1858
|
-
if (tag !== "" && !TERMINATOR_TAGS.has(tag)) {
|
|
1859
|
-
throw new Error(
|
|
1860
|
-
`Frond: unknown tag "${tag}" -- known tags are: ${[...KNOWN_TAGS].sort().join(", ")}`
|
|
1861
|
-
);
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
if (stripA && i < tokens.length && tokens[i][0] === "TEXT") {
|
|
1865
|
-
tokens[i] = ["TEXT", tokens[i][1].replace(LEADING_WS_RE, "")];
|
|
1866
|
-
}
|
|
1870
|
+
i = this.renderBlockToken(tokens, i, context, output);
|
|
1867
1871
|
} else {
|
|
1868
1872
|
i++;
|
|
1869
1873
|
}
|
|
@@ -1894,6 +1898,46 @@ var Frond = class _Frond {
|
|
|
1894
1898
|
if (!GATEABLE_TAGS.has(tag)) return true;
|
|
1895
1899
|
return this._allowedTags.has(tag);
|
|
1896
1900
|
}
|
|
1901
|
+
applyFilterValue(value, fname, args, context) {
|
|
1902
|
+
const [realFname, tailPath] = splitFilterNameAndPath(fname);
|
|
1903
|
+
if (tailPath) {
|
|
1904
|
+
let applied = false;
|
|
1905
|
+
if (realFname === "first") {
|
|
1906
|
+
value = Array.isArray(value) ? value[0] ?? null : null;
|
|
1907
|
+
applied = true;
|
|
1908
|
+
} else if (realFname === "last") {
|
|
1909
|
+
value = Array.isArray(value) ? value[value.length - 1] ?? null : null;
|
|
1910
|
+
applied = true;
|
|
1911
|
+
} else if (this.filters[realFname]) {
|
|
1912
|
+
value = this.filters[realFname](value, ...args);
|
|
1913
|
+
applied = true;
|
|
1914
|
+
}
|
|
1915
|
+
if (applied) return evalExpr("__frondFilterTmp." + tailPath, { __frondFilterTmp: value });
|
|
1916
|
+
}
|
|
1917
|
+
const fn = this.filters[fname];
|
|
1918
|
+
if (fn) return fn(value, ...args);
|
|
1919
|
+
const comparison = fname.match(FILTER_COMPARISON_RE);
|
|
1920
|
+
if (!comparison) return evalExpr(fname, context);
|
|
1921
|
+
const comparisonFn = this.filters[comparison[1]];
|
|
1922
|
+
if (comparisonFn) value = comparisonFn(value, ...args);
|
|
1923
|
+
const right = evalExpr(comparison[3].trim(), context);
|
|
1924
|
+
switch (comparison[2]) {
|
|
1925
|
+
case "!=":
|
|
1926
|
+
return value !== right;
|
|
1927
|
+
case "==":
|
|
1928
|
+
return value === right;
|
|
1929
|
+
case ">=":
|
|
1930
|
+
return value >= right;
|
|
1931
|
+
case "<=":
|
|
1932
|
+
return value <= right;
|
|
1933
|
+
case ">":
|
|
1934
|
+
return value > right;
|
|
1935
|
+
case "<":
|
|
1936
|
+
return value < right;
|
|
1937
|
+
default:
|
|
1938
|
+
return value;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1897
1941
|
/**
|
|
1898
1942
|
* Consume a denied tag WITHOUT running it, returning the index past its body.
|
|
1899
1943
|
*
|
|
@@ -1937,58 +1981,8 @@ var Frond = class _Frond {
|
|
|
1937
1981
|
for (const [fname, rawArgs] of filters) {
|
|
1938
1982
|
const args = rawArgs.map((a) => a instanceof VarRef ? evalExpr(a.name, context) : a);
|
|
1939
1983
|
if (fname === "raw" || fname === "safe") continue;
|
|
1940
|
-
if (
|
|
1941
|
-
|
|
1942
|
-
if (tailPath) {
|
|
1943
|
-
let applied = false;
|
|
1944
|
-
if (realFname === "first") {
|
|
1945
|
-
value = Array.isArray(value) ? value[0] ?? null : null;
|
|
1946
|
-
applied = true;
|
|
1947
|
-
} else if (realFname === "last") {
|
|
1948
|
-
value = Array.isArray(value) ? value[value.length - 1] ?? null : null;
|
|
1949
|
-
applied = true;
|
|
1950
|
-
} else if (this.filters[realFname]) {
|
|
1951
|
-
value = this.filters[realFname](value, ...args);
|
|
1952
|
-
applied = true;
|
|
1953
|
-
}
|
|
1954
|
-
if (applied) {
|
|
1955
|
-
value = evalExpr("__frondFilterTmp." + tailPath, { __frondFilterTmp: value });
|
|
1956
|
-
continue;
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
const fn = this.filters[fname];
|
|
1960
|
-
if (fn) {
|
|
1961
|
-
value = fn(value, ...args);
|
|
1962
|
-
} else {
|
|
1963
|
-
const m = fname.match(FILTER_COMPARISON_RE);
|
|
1964
|
-
if (m) {
|
|
1965
|
-
const fn2 = this.filters[m[1]];
|
|
1966
|
-
if (fn2) value = fn2(value, ...args);
|
|
1967
|
-
const right = evalExpr(m[3].trim(), context);
|
|
1968
|
-
switch (m[2]) {
|
|
1969
|
-
case "!=":
|
|
1970
|
-
value = value !== right;
|
|
1971
|
-
break;
|
|
1972
|
-
case "==":
|
|
1973
|
-
value = value === right;
|
|
1974
|
-
break;
|
|
1975
|
-
case ">=":
|
|
1976
|
-
value = value >= right;
|
|
1977
|
-
break;
|
|
1978
|
-
case "<=":
|
|
1979
|
-
value = value <= right;
|
|
1980
|
-
break;
|
|
1981
|
-
case ">":
|
|
1982
|
-
value = value > right;
|
|
1983
|
-
break;
|
|
1984
|
-
case "<":
|
|
1985
|
-
value = value < right;
|
|
1986
|
-
break;
|
|
1987
|
-
}
|
|
1988
|
-
} else {
|
|
1989
|
-
value = evalExpr(fname, context);
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1984
|
+
if (!this.filterPermitted(fname)) continue;
|
|
1985
|
+
value = this.applyFilterValue(value, fname, args, context);
|
|
1992
1986
|
}
|
|
1993
1987
|
return value;
|
|
1994
1988
|
}
|
|
@@ -2013,218 +2007,92 @@ var Frond = class _Frond {
|
|
|
2013
2007
|
for (const [fname, rawArgs] of filters) {
|
|
2014
2008
|
const args = rawArgs.map((a) => a instanceof VarRef ? evalExpr(a.name, context) : a);
|
|
2015
2009
|
if (fname === "raw" || fname === "safe") continue;
|
|
2016
|
-
if (
|
|
2017
|
-
|
|
2018
|
-
if (tailPath) {
|
|
2019
|
-
let applied = false;
|
|
2020
|
-
if (realFname === "first") {
|
|
2021
|
-
value = Array.isArray(value) ? value[0] ?? null : null;
|
|
2022
|
-
applied = true;
|
|
2023
|
-
} else if (realFname === "last") {
|
|
2024
|
-
value = Array.isArray(value) ? value[value.length - 1] ?? null : null;
|
|
2025
|
-
applied = true;
|
|
2026
|
-
} else if (this.filters[realFname]) {
|
|
2027
|
-
value = this.filters[realFname](value, ...args);
|
|
2028
|
-
applied = true;
|
|
2029
|
-
}
|
|
2030
|
-
if (applied) {
|
|
2031
|
-
value = evalExpr(
|
|
2032
|
-
"__frondFilterTmp." + tailPath,
|
|
2033
|
-
{ __frondFilterTmp: value }
|
|
2034
|
-
);
|
|
2035
|
-
continue;
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
const fn = this.filters[fname];
|
|
2039
|
-
if (fn) {
|
|
2040
|
-
value = fn(value, ...args);
|
|
2041
|
-
} else {
|
|
2042
|
-
const m = fname.match(FILTER_COMPARISON_RE);
|
|
2043
|
-
if (m) {
|
|
2044
|
-
const realFilter = m[1];
|
|
2045
|
-
const op = m[2];
|
|
2046
|
-
const rightExpr = m[3].trim();
|
|
2047
|
-
const fn2 = this.filters[realFilter];
|
|
2048
|
-
if (fn2) {
|
|
2049
|
-
value = fn2(value, ...args);
|
|
2050
|
-
}
|
|
2051
|
-
const right = evalExpr(rightExpr, context);
|
|
2052
|
-
switch (op) {
|
|
2053
|
-
case "!=":
|
|
2054
|
-
value = value !== right;
|
|
2055
|
-
break;
|
|
2056
|
-
case "==":
|
|
2057
|
-
value = value === right;
|
|
2058
|
-
break;
|
|
2059
|
-
case ">=":
|
|
2060
|
-
value = value >= right;
|
|
2061
|
-
break;
|
|
2062
|
-
case "<=":
|
|
2063
|
-
value = value <= right;
|
|
2064
|
-
break;
|
|
2065
|
-
case ">":
|
|
2066
|
-
value = value > right;
|
|
2067
|
-
break;
|
|
2068
|
-
case "<":
|
|
2069
|
-
value = value < right;
|
|
2070
|
-
break;
|
|
2071
|
-
}
|
|
2072
|
-
} else {
|
|
2073
|
-
value = evalExpr(fname, context);
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2010
|
+
if (!this.filterPermitted(fname)) continue;
|
|
2011
|
+
value = this.applyFilterValue(value, fname, args, context);
|
|
2076
2012
|
}
|
|
2077
2013
|
return value;
|
|
2078
2014
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2015
|
+
/**
|
|
2016
|
+
* Apply the no-argument filters that are common enough to avoid generic
|
|
2017
|
+
* dispatch. Keeping this table separate from evalVarInner makes the
|
|
2018
|
+
* expression pipeline easier to audit without changing filter order.
|
|
2019
|
+
*/
|
|
2020
|
+
applyFastFilter(name, value) {
|
|
2021
|
+
const handler = FAST_FILTERS[name];
|
|
2022
|
+
return handler ? { handled: true, value: handler(value) } : { handled: false, value };
|
|
2023
|
+
}
|
|
2024
|
+
applyRenderedFilter(value, fname, args) {
|
|
2025
|
+
const [realFname, tailPath] = splitFilterNameAndPath(fname);
|
|
2026
|
+
if (tailPath) {
|
|
2027
|
+
let applied = false;
|
|
2028
|
+
if (realFname === "first") {
|
|
2029
|
+
value = Array.isArray(value) ? value[0] ?? null : null;
|
|
2030
|
+
applied = true;
|
|
2031
|
+
} else if (realFname === "last") {
|
|
2032
|
+
value = Array.isArray(value) ? value[value.length - 1] ?? null : null;
|
|
2033
|
+
applied = true;
|
|
2034
|
+
} else if (this.filters[realFname]) {
|
|
2035
|
+
value = this.filters[realFname](value, ...args);
|
|
2036
|
+
applied = true;
|
|
2037
|
+
}
|
|
2038
|
+
if (applied) return evalExpr("__frondFilterTmp." + tailPath, { __frondFilterTmp: value });
|
|
2039
|
+
}
|
|
2040
|
+
if (args.length === 0) {
|
|
2041
|
+
const fast = this.applyFastFilter(fname, value);
|
|
2042
|
+
if (fast.handled) return fast.value;
|
|
2043
|
+
}
|
|
2044
|
+
const fn = this.filters[fname];
|
|
2045
|
+
return fn ? fn(value, ...args) : value;
|
|
2046
|
+
}
|
|
2047
|
+
variablePermitted(varName) {
|
|
2048
|
+
if (!this._sandbox || this._allowedVars === null) return true;
|
|
2049
|
+
const rootVar = varName.split(".")[0].split("[")[0].trim();
|
|
2050
|
+
return !rootVar || rootVar === "loop" || this._allowedVars.has(rootVar);
|
|
2051
|
+
}
|
|
2052
|
+
resolveConcatenation(expr, context) {
|
|
2053
|
+
if (findOutsideQuotes(expr, "~") < 0) return { handled: false, value: void 0 };
|
|
2054
|
+
let value = evalExpr(expr, context);
|
|
2055
|
+
if (value instanceof SafeString) return { handled: true, value: value.value };
|
|
2056
|
+
if (this._autoEscape && typeof value === "string") value = htmlEscape(value);
|
|
2057
|
+
return { handled: true, value };
|
|
2058
|
+
}
|
|
2059
|
+
applyRenderedFilters(value, filters, context) {
|
|
2060
|
+
let safe = false;
|
|
2097
2061
|
for (const [fname, rawArgs] of filters) {
|
|
2098
2062
|
const args = rawArgs.map((a) => a instanceof VarRef ? evalExpr(a.name, context) : a);
|
|
2099
2063
|
if (fname === "raw" || fname === "safe") {
|
|
2100
|
-
if (this.filterPermitted(fname))
|
|
2064
|
+
if (this.filterPermitted(fname)) safe = true;
|
|
2101
2065
|
continue;
|
|
2102
2066
|
}
|
|
2103
|
-
if (fname === "escape" || fname === "e")
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
if (this._sandbox && this._allowedFilters !== null) {
|
|
2107
|
-
if (!this._allowedFilters.has(fname)) {
|
|
2108
|
-
continue;
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
const [realFname, tailPath] = splitFilterNameAndPath(fname);
|
|
2112
|
-
if (tailPath) {
|
|
2113
|
-
let applied = false;
|
|
2114
|
-
if (realFname === "first") {
|
|
2115
|
-
value = Array.isArray(value) ? value[0] ?? null : null;
|
|
2116
|
-
applied = true;
|
|
2117
|
-
} else if (realFname === "last") {
|
|
2118
|
-
value = Array.isArray(value) ? value[value.length - 1] ?? null : null;
|
|
2119
|
-
applied = true;
|
|
2120
|
-
} else if (this.filters[realFname]) {
|
|
2121
|
-
value = this.filters[realFname](value, ...args);
|
|
2122
|
-
applied = true;
|
|
2123
|
-
}
|
|
2124
|
-
if (applied) {
|
|
2125
|
-
value = evalExpr(
|
|
2126
|
-
"__frondFilterTmp." + tailPath,
|
|
2127
|
-
{ __frondFilterTmp: value }
|
|
2128
|
-
);
|
|
2129
|
-
continue;
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
if (args.length === 0) {
|
|
2133
|
-
switch (fname) {
|
|
2134
|
-
case "upper":
|
|
2135
|
-
value = String(value).toUpperCase();
|
|
2136
|
-
continue;
|
|
2137
|
-
case "lower":
|
|
2138
|
-
value = String(value).toLowerCase();
|
|
2139
|
-
continue;
|
|
2140
|
-
case "trim":
|
|
2141
|
-
value = String(value).trim();
|
|
2142
|
-
continue;
|
|
2143
|
-
case "length":
|
|
2144
|
-
if (Array.isArray(value)) {
|
|
2145
|
-
value = value.length;
|
|
2146
|
-
} else if (typeof value === "string") {
|
|
2147
|
-
value = value.length;
|
|
2148
|
-
} else if (typeof value === "object" && value !== null) {
|
|
2149
|
-
value = Object.keys(value).length;
|
|
2150
|
-
} else {
|
|
2151
|
-
value = 0;
|
|
2152
|
-
}
|
|
2153
|
-
continue;
|
|
2154
|
-
case "capitalize": {
|
|
2155
|
-
const s = String(value);
|
|
2156
|
-
value = s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
|
2157
|
-
continue;
|
|
2158
|
-
}
|
|
2159
|
-
case "title":
|
|
2160
|
-
value = String(value).replace(TITLE_WORD_RE, (c) => c.toUpperCase());
|
|
2161
|
-
continue;
|
|
2162
|
-
case "string":
|
|
2163
|
-
value = String(value);
|
|
2164
|
-
continue;
|
|
2165
|
-
case "int":
|
|
2166
|
-
value = value ? parseInt(String(value), 10) || 0 : 0;
|
|
2167
|
-
continue;
|
|
2168
|
-
case "float":
|
|
2169
|
-
value = value ? parseFloat(String(value)) || 0 : 0;
|
|
2170
|
-
continue;
|
|
2171
|
-
case "abs":
|
|
2172
|
-
value = typeof value === "number" ? Math.abs(value) : value;
|
|
2173
|
-
continue;
|
|
2174
|
-
case "striptags":
|
|
2175
|
-
value = String(value).replace(STRIP_TAGS_RE, "");
|
|
2176
|
-
continue;
|
|
2177
|
-
case "first":
|
|
2178
|
-
value = Array.isArray(value) ? value[0] ?? null : null;
|
|
2179
|
-
continue;
|
|
2180
|
-
case "last":
|
|
2181
|
-
value = Array.isArray(value) ? value[value.length - 1] ?? null : null;
|
|
2182
|
-
continue;
|
|
2183
|
-
case "keys":
|
|
2184
|
-
value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.keys(value) : [];
|
|
2185
|
-
continue;
|
|
2186
|
-
case "values":
|
|
2187
|
-
value = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.values(value) : [];
|
|
2188
|
-
continue;
|
|
2189
|
-
case "json_encode":
|
|
2190
|
-
value = jsonSafe(value);
|
|
2191
|
-
continue;
|
|
2192
|
-
case "dump":
|
|
2193
|
-
value = renderDump(value);
|
|
2194
|
-
continue;
|
|
2195
|
-
case "nl2br":
|
|
2196
|
-
value = new SafeString(htmlEscape(String(value)).replace(/\n/g, "<br />\n"));
|
|
2197
|
-
continue;
|
|
2198
|
-
case "unique":
|
|
2199
|
-
value = Array.isArray(value) ? [...new Set(value)] : value;
|
|
2200
|
-
continue;
|
|
2201
|
-
case "sort":
|
|
2202
|
-
value = Array.isArray(value) ? [...value].sort() : value;
|
|
2203
|
-
continue;
|
|
2204
|
-
case "reverse":
|
|
2205
|
-
value = Array.isArray(value) ? [...value].reverse() : String(value).split("").reverse().join("");
|
|
2206
|
-
continue;
|
|
2207
|
-
case "filter":
|
|
2208
|
-
value = Array.isArray(value) ? value.filter(Boolean) : value;
|
|
2209
|
-
continue;
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
const fn = this.filters[fname];
|
|
2213
|
-
if (fn) {
|
|
2214
|
-
value = fn(value, ...args);
|
|
2215
|
-
}
|
|
2067
|
+
if ((fname === "escape" || fname === "e") && this.filterPermitted(fname)) safe = true;
|
|
2068
|
+
if (!this.filterPermitted(fname)) continue;
|
|
2069
|
+
value = this.applyRenderedFilter(value, fname, args);
|
|
2216
2070
|
}
|
|
2071
|
+
return { value, safe };
|
|
2072
|
+
}
|
|
2073
|
+
evalVarInner(expr, context) {
|
|
2074
|
+
const [varName, filters] = parseFilterChain(expr);
|
|
2075
|
+
if (!this.variablePermitted(varName)) return "";
|
|
2076
|
+
const concatenated = this.resolveConcatenation(expr, context);
|
|
2077
|
+
if (concatenated.handled) return concatenated.value;
|
|
2078
|
+
const applied = this.applyRenderedFilters(evalExpr(varName, context), filters, context);
|
|
2079
|
+
let value = applied.value;
|
|
2217
2080
|
if (value instanceof SafeString) {
|
|
2218
2081
|
return value.value;
|
|
2219
2082
|
}
|
|
2220
|
-
if (!
|
|
2083
|
+
if (!applied.safe && this._autoEscape && typeof value === "string") {
|
|
2221
2084
|
value = htmlEscape(value);
|
|
2222
2085
|
}
|
|
2223
2086
|
return value;
|
|
2224
2087
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2088
|
+
pushIfBranch(branches, condition, branchTokens, stripBefore) {
|
|
2089
|
+
if (stripBefore && branchTokens.length > 0 && branchTokens[branchTokens.length - 1][0] === "TEXT") {
|
|
2090
|
+
const last = branchTokens[branchTokens.length - 1];
|
|
2091
|
+
branchTokens[branchTokens.length - 1] = ["TEXT", last[1].replace(TRAILING_WS_RE, "")];
|
|
2092
|
+
}
|
|
2093
|
+
branches.push([condition, branchTokens]);
|
|
2094
|
+
}
|
|
2095
|
+
collectIfBranches(tokens, start, conditionExpr) {
|
|
2228
2096
|
const branches = [];
|
|
2229
2097
|
let currentTokens = [];
|
|
2230
2098
|
let currentCond = conditionExpr;
|
|
@@ -2232,61 +2100,52 @@ var Frond = class _Frond {
|
|
|
2232
2100
|
let i = start + 1;
|
|
2233
2101
|
while (i < tokens.length) {
|
|
2234
2102
|
const [ttype, raw] = tokens[i];
|
|
2235
|
-
if (ttype
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
i++;
|
|
2253
|
-
break;
|
|
2254
|
-
} else if ((tag === "elseif" || tag === "elif") && depth === 0) {
|
|
2255
|
-
if (tagStripB && currentTokens.length > 0 && currentTokens[currentTokens.length - 1][0] === "TEXT") {
|
|
2256
|
-
currentTokens[currentTokens.length - 1] = ["TEXT", currentTokens[currentTokens.length - 1][1].replace(TRAILING_WS_RE, "")];
|
|
2257
|
-
}
|
|
2258
|
-
branches.push([currentCond, currentTokens]);
|
|
2259
|
-
currentCond = tagContent.slice(tag.length).trim();
|
|
2260
|
-
currentTokens = [];
|
|
2261
|
-
} else if (tag === "else" && depth === 0) {
|
|
2262
|
-
if (tagStripB && currentTokens.length > 0 && currentTokens[currentTokens.length - 1][0] === "TEXT") {
|
|
2263
|
-
currentTokens[currentTokens.length - 1] = ["TEXT", currentTokens[currentTokens.length - 1][1].replace(TRAILING_WS_RE, "")];
|
|
2264
|
-
}
|
|
2265
|
-
branches.push([currentCond, currentTokens]);
|
|
2266
|
-
currentCond = null;
|
|
2267
|
-
currentTokens = [];
|
|
2268
|
-
} else {
|
|
2269
|
-
currentTokens.push(tokens[i]);
|
|
2103
|
+
if (ttype !== "BLOCK") {
|
|
2104
|
+
currentTokens.push(tokens[i]);
|
|
2105
|
+
i++;
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
const [tagContent, tagStripB, tagStripA] = stripTag(raw);
|
|
2109
|
+
const tag = tagContent.split(/\s+/)[0] || "";
|
|
2110
|
+
if (tag === "if") {
|
|
2111
|
+
depth++;
|
|
2112
|
+
currentTokens.push(tokens[i]);
|
|
2113
|
+
} else if (tag === "endif" && depth > 0) {
|
|
2114
|
+
depth--;
|
|
2115
|
+
currentTokens.push(tokens[i]);
|
|
2116
|
+
} else if (tag === "endif") {
|
|
2117
|
+
this.pushIfBranch(branches, currentCond, currentTokens, tagStripB);
|
|
2118
|
+
if (tagStripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
|
|
2119
|
+
tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
|
|
2270
2120
|
}
|
|
2121
|
+
return { branches, next: i + 1 };
|
|
2122
|
+
} else if ((tag === "elseif" || tag === "elif") && depth === 0) {
|
|
2123
|
+
this.pushIfBranch(branches, currentCond, currentTokens, tagStripB);
|
|
2124
|
+
currentCond = tagContent.slice(tag.length).trim();
|
|
2125
|
+
currentTokens = [];
|
|
2126
|
+
} else if (tag === "else" && depth === 0) {
|
|
2127
|
+
this.pushIfBranch(branches, currentCond, currentTokens, tagStripB);
|
|
2128
|
+
currentCond = null;
|
|
2129
|
+
currentTokens = [];
|
|
2271
2130
|
} else {
|
|
2272
2131
|
currentTokens.push(tokens[i]);
|
|
2273
2132
|
}
|
|
2274
2133
|
i++;
|
|
2275
2134
|
}
|
|
2135
|
+
return { branches, next: i };
|
|
2136
|
+
}
|
|
2137
|
+
handleIf(tokens, start, context) {
|
|
2138
|
+
const [content] = stripTag(tokens[start][1]);
|
|
2139
|
+
const conditionExpr = content.slice(3).trim();
|
|
2140
|
+
const { branches, next } = this.collectIfBranches(tokens, start, conditionExpr);
|
|
2276
2141
|
for (const [cond, branchTokens] of branches) {
|
|
2277
2142
|
if (cond === null || evalComparison(cond, context, this.evalVarRaw.bind(this))) {
|
|
2278
|
-
return [this.renderTokens([...branchTokens], context),
|
|
2143
|
+
return [this.renderTokens([...branchTokens], context), next];
|
|
2279
2144
|
}
|
|
2280
2145
|
}
|
|
2281
|
-
return ["",
|
|
2146
|
+
return ["", next];
|
|
2282
2147
|
}
|
|
2283
|
-
|
|
2284
|
-
const [content] = stripTag(tokens[start][1]);
|
|
2285
|
-
const forMatch = content.match(/^for\s+(\w+)(?:\s*,\s*(\w+))?\s+in\s+(.+)/);
|
|
2286
|
-
if (!forMatch) return ["", start + 1];
|
|
2287
|
-
const var1 = forMatch[1];
|
|
2288
|
-
const var2 = forMatch[2] || null;
|
|
2289
|
-
const iterableExpr = forMatch[3].trim();
|
|
2148
|
+
collectForTokens(tokens, start) {
|
|
2290
2149
|
const bodyTokens = [];
|
|
2291
2150
|
const elseTokens = [];
|
|
2292
2151
|
let inElse = false;
|
|
@@ -2294,65 +2153,64 @@ var Frond = class _Frond {
|
|
|
2294
2153
|
let ifDepth = 0;
|
|
2295
2154
|
let i = start + 1;
|
|
2296
2155
|
while (i < tokens.length) {
|
|
2297
|
-
const
|
|
2298
|
-
if (
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
forDepth++;
|
|
2303
|
-
(inElse ? elseTokens : bodyTokens).push(tokens[i]);
|
|
2304
|
-
} else if (tag === "endfor" && forDepth > 0) {
|
|
2305
|
-
forDepth--;
|
|
2306
|
-
(inElse ? elseTokens : bodyTokens).push(tokens[i]);
|
|
2307
|
-
} else if (tag === "endfor" && forDepth === 0) {
|
|
2308
|
-
i++;
|
|
2309
|
-
break;
|
|
2310
|
-
} else if (tag === "if") {
|
|
2311
|
-
ifDepth++;
|
|
2312
|
-
(inElse ? elseTokens : bodyTokens).push(tokens[i]);
|
|
2313
|
-
} else if (tag === "endif") {
|
|
2314
|
-
ifDepth--;
|
|
2315
|
-
(inElse ? elseTokens : bodyTokens).push(tokens[i]);
|
|
2316
|
-
} else if (tag === "else" && forDepth === 0 && ifDepth === 0) {
|
|
2317
|
-
inElse = true;
|
|
2318
|
-
} else {
|
|
2319
|
-
(inElse ? elseTokens : bodyTokens).push(tokens[i]);
|
|
2320
|
-
}
|
|
2321
|
-
} else {
|
|
2322
|
-
(inElse ? elseTokens : bodyTokens).push(tokens[i]);
|
|
2156
|
+
const token = tokens[i];
|
|
2157
|
+
if (token[0] !== "BLOCK") {
|
|
2158
|
+
(inElse ? elseTokens : bodyTokens).push(token);
|
|
2159
|
+
i++;
|
|
2160
|
+
continue;
|
|
2323
2161
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
if (
|
|
2329
|
-
|
|
2162
|
+
const [tagContent] = stripTag(token[1]);
|
|
2163
|
+
const tag = tagContent.split(/\s+/)[0] || "";
|
|
2164
|
+
if (tag === "for") forDepth++;
|
|
2165
|
+
else if (tag === "endfor" && forDepth > 0) forDepth--;
|
|
2166
|
+
else if (tag === "endfor") return { bodyTokens, elseTokens, next: i + 1 };
|
|
2167
|
+
else if (tag === "if") ifDepth++;
|
|
2168
|
+
else if (tag === "endif") ifDepth--;
|
|
2169
|
+
else if (tag === "else" && forDepth === 0 && ifDepth === 0) {
|
|
2170
|
+
inElse = true;
|
|
2171
|
+
i++;
|
|
2172
|
+
continue;
|
|
2330
2173
|
}
|
|
2331
|
-
|
|
2174
|
+
(inElse ? elseTokens : bodyTokens).push(token);
|
|
2175
|
+
i++;
|
|
2332
2176
|
}
|
|
2177
|
+
return { bodyTokens, elseTokens, next: i };
|
|
2178
|
+
}
|
|
2179
|
+
forItems(iterable) {
|
|
2180
|
+
const isDict = typeof iterable === "object" && iterable !== null && !Array.isArray(iterable);
|
|
2181
|
+
if (isDict) return { items: Object.entries(iterable), isDict: true };
|
|
2182
|
+
return { items: Array.isArray(iterable) ? iterable : [], isDict: false };
|
|
2183
|
+
}
|
|
2184
|
+
handleFor(tokens, start, context) {
|
|
2185
|
+
const [content] = stripTag(tokens[start][1]);
|
|
2186
|
+
const forMatch = content.match(/^for\s+(\w+)(?:\s*,\s*(\w+))?\s+in\s+(.+)/);
|
|
2187
|
+
if (!forMatch) return ["", start + 1];
|
|
2188
|
+
const var1 = forMatch[1];
|
|
2189
|
+
const var2 = forMatch[2] || null;
|
|
2190
|
+
const { bodyTokens, elseTokens, next: i } = this.collectForTokens(tokens, start);
|
|
2191
|
+
const iterable = evalExpr(forMatch[3].trim(), context);
|
|
2192
|
+
const { items, isDict } = this.forItems(iterable);
|
|
2193
|
+
if (items.length === 0) return [elseTokens.length ? this.renderTokens([...elseTokens], context) : "", i];
|
|
2333
2194
|
const output = [];
|
|
2334
|
-
const isDict = typeof iterable === "object" && !Array.isArray(iterable);
|
|
2335
|
-
const items = isDict ? Object.entries(iterable) : Array.isArray(iterable) ? iterable : [];
|
|
2336
|
-
const total = items.length;
|
|
2337
2195
|
const loopObj = {
|
|
2338
2196
|
index: 0,
|
|
2339
2197
|
index0: 0,
|
|
2340
2198
|
first: false,
|
|
2341
2199
|
last: false,
|
|
2342
|
-
length:
|
|
2200
|
+
length: items.length,
|
|
2343
2201
|
revindex: 0,
|
|
2344
2202
|
revindex0: 0,
|
|
2345
2203
|
even: false,
|
|
2346
2204
|
odd: false
|
|
2347
2205
|
};
|
|
2348
|
-
for (let idx = 0; idx <
|
|
2206
|
+
for (let idx = 0; idx < items.length; idx++) {
|
|
2349
2207
|
const item = items[idx];
|
|
2350
2208
|
loopObj.index = idx + 1;
|
|
2351
2209
|
loopObj.index0 = idx;
|
|
2352
2210
|
loopObj.first = idx === 0;
|
|
2353
|
-
loopObj.last = idx ===
|
|
2354
|
-
loopObj.revindex =
|
|
2355
|
-
loopObj.revindex0 =
|
|
2211
|
+
loopObj.last = idx === items.length - 1;
|
|
2212
|
+
loopObj.revindex = items.length - idx;
|
|
2213
|
+
loopObj.revindex0 = items.length - idx - 1;
|
|
2356
2214
|
loopObj.even = (idx + 1) % 2 === 0;
|
|
2357
2215
|
loopObj.odd = (idx + 1) % 2 !== 0;
|
|
2358
2216
|
const locals = { loop: loopObj };
|
|
@@ -2360,18 +2218,15 @@ var Frond = class _Frond {
|
|
|
2360
2218
|
const [key, value] = item;
|
|
2361
2219
|
locals[var1] = key;
|
|
2362
2220
|
if (var2) locals[var2] = value;
|
|
2221
|
+
} else if (var2) {
|
|
2222
|
+
locals[var1] = idx;
|
|
2223
|
+
locals[var2] = item;
|
|
2363
2224
|
} else {
|
|
2364
|
-
|
|
2365
|
-
locals[var1] = idx;
|
|
2366
|
-
locals[var2] = item;
|
|
2367
|
-
} else {
|
|
2368
|
-
locals[var1] = item;
|
|
2369
|
-
}
|
|
2225
|
+
locals[var1] = item;
|
|
2370
2226
|
}
|
|
2371
2227
|
const loopCtx = new Proxy(locals, {
|
|
2372
2228
|
get(target, prop) {
|
|
2373
|
-
|
|
2374
|
-
return context[prop];
|
|
2229
|
+
return prop in target ? target[prop] : context[prop];
|
|
2375
2230
|
},
|
|
2376
2231
|
set(target, prop, value) {
|
|
2377
2232
|
target[prop] = value;
|
|
@@ -2500,44 +2355,8 @@ var Frond = class _Frond {
|
|
|
2500
2355
|
const alias = m[2];
|
|
2501
2356
|
const namespace = {};
|
|
2502
2357
|
const source = this.load(filename);
|
|
2503
|
-
const
|
|
2504
|
-
|
|
2505
|
-
while (i < tokens.length) {
|
|
2506
|
-
const [ttype, raw] = tokens[i];
|
|
2507
|
-
if (ttype === "BLOCK") {
|
|
2508
|
-
const [tagContent] = stripTag(raw);
|
|
2509
|
-
if ((tagContent.split(/\s+/)[0] || "") === "macro") {
|
|
2510
|
-
const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
2511
|
-
if (macroM) {
|
|
2512
|
-
const macroName = macroM[1];
|
|
2513
|
-
const params = _Frond.parseMacroParams(macroM[2]);
|
|
2514
|
-
const bodyTokens = [];
|
|
2515
|
-
i++;
|
|
2516
|
-
while (i < tokens.length) {
|
|
2517
|
-
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
2518
|
-
i++;
|
|
2519
|
-
break;
|
|
2520
|
-
}
|
|
2521
|
-
bodyTokens.push(tokens[i]);
|
|
2522
|
-
i++;
|
|
2523
|
-
}
|
|
2524
|
-
const capturedBody = [...bodyTokens];
|
|
2525
|
-
const capturedParams = [...params];
|
|
2526
|
-
const capturedCtx = { ...context };
|
|
2527
|
-
const engine = this;
|
|
2528
|
-
namespace[macroName] = (...args) => {
|
|
2529
|
-
const macroCtx = { ...capturedCtx };
|
|
2530
|
-
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
2531
|
-
const [pname, pdefault] = capturedParams[pi];
|
|
2532
|
-
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2533
|
-
}
|
|
2534
|
-
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
2535
|
-
};
|
|
2536
|
-
continue;
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
i++;
|
|
2358
|
+
for (const definition of this.collectMacroDefinitions(tokenize(source))) {
|
|
2359
|
+
namespace[definition.name] = this.createMacro(definition, context);
|
|
2541
2360
|
}
|
|
2542
2361
|
context[alias] = namespace;
|
|
2543
2362
|
}
|
|
@@ -2547,46 +2366,61 @@ var Frond = class _Frond {
|
|
|
2547
2366
|
const filename = m[1];
|
|
2548
2367
|
const names = m[2].split(",").map((n) => n.trim()).filter(Boolean);
|
|
2549
2368
|
const source = this.load(filename);
|
|
2550
|
-
const
|
|
2369
|
+
for (const definition of this.collectMacroDefinitions(tokenize(source))) {
|
|
2370
|
+
if (names.includes(definition.name)) {
|
|
2371
|
+
context[definition.name] = this.createMacro(definition, context);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
collectMacroDefinitions(tokens) {
|
|
2376
|
+
const definitions = [];
|
|
2551
2377
|
let i = 0;
|
|
2552
2378
|
while (i < tokens.length) {
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
const paramNames = _Frond.parseMacroParams(macroM[2]);
|
|
2562
|
-
const bodyTokens = [];
|
|
2563
|
-
i++;
|
|
2564
|
-
while (i < tokens.length) {
|
|
2565
|
-
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
2566
|
-
i++;
|
|
2567
|
-
break;
|
|
2568
|
-
}
|
|
2569
|
-
bodyTokens.push(tokens[i]);
|
|
2570
|
-
i++;
|
|
2571
|
-
}
|
|
2572
|
-
const capturedBody = [...bodyTokens];
|
|
2573
|
-
const capturedParams = [...paramNames];
|
|
2574
|
-
const capturedCtx = { ...context };
|
|
2575
|
-
const engine = this;
|
|
2576
|
-
context[macroName] = (...args) => {
|
|
2577
|
-
const macroCtx = { ...capturedCtx };
|
|
2578
|
-
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
2579
|
-
const [pname, pdefault] = capturedParams[pi];
|
|
2580
|
-
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2581
|
-
}
|
|
2582
|
-
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
2583
|
-
};
|
|
2584
|
-
continue;
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2379
|
+
if (tokens[i][0] !== "BLOCK") {
|
|
2380
|
+
i++;
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
const [tagContent] = stripTag(tokens[i][1]);
|
|
2384
|
+
if ((tagContent.split(/\s+/)[0] || "") !== "macro") {
|
|
2385
|
+
i++;
|
|
2386
|
+
continue;
|
|
2587
2387
|
}
|
|
2388
|
+
const macroMatch = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
|
|
2389
|
+
if (!macroMatch) {
|
|
2390
|
+
i++;
|
|
2391
|
+
continue;
|
|
2392
|
+
}
|
|
2393
|
+
const bodyTokens = [];
|
|
2588
2394
|
i++;
|
|
2395
|
+
while (i < tokens.length) {
|
|
2396
|
+
if (tokens[i][0] === "BLOCK" && tokens[i][1].includes("endmacro")) {
|
|
2397
|
+
i++;
|
|
2398
|
+
break;
|
|
2399
|
+
}
|
|
2400
|
+
bodyTokens.push(tokens[i]);
|
|
2401
|
+
i++;
|
|
2402
|
+
}
|
|
2403
|
+
definitions.push({
|
|
2404
|
+
name: macroMatch[1],
|
|
2405
|
+
params: _Frond.parseMacroParams(macroMatch[2]),
|
|
2406
|
+
bodyTokens
|
|
2407
|
+
});
|
|
2589
2408
|
}
|
|
2409
|
+
return definitions;
|
|
2410
|
+
}
|
|
2411
|
+
createMacro(definition, context) {
|
|
2412
|
+
const capturedBody = [...definition.bodyTokens];
|
|
2413
|
+
const capturedParams = [...definition.params];
|
|
2414
|
+
const capturedCtx = { ...context };
|
|
2415
|
+
const engine = this;
|
|
2416
|
+
return (...args) => {
|
|
2417
|
+
const macroCtx = { ...capturedCtx };
|
|
2418
|
+
for (let pi = 0; pi < capturedParams.length; pi++) {
|
|
2419
|
+
const [pname, pdefault] = capturedParams[pi];
|
|
2420
|
+
macroCtx[pname] = pi < args.length ? args[pi] : pdefault;
|
|
2421
|
+
}
|
|
2422
|
+
return new SafeString(engine.renderTokens([...capturedBody], macroCtx));
|
|
2423
|
+
};
|
|
2590
2424
|
}
|
|
2591
2425
|
/**
|
|
2592
2426
|
* Collect the body tokens of a {% <openTag> %}...{% end<openTag> %} block,
|
|
@@ -2676,60 +2510,63 @@ var Frond = class _Frond {
|
|
|
2676
2510
|
}
|
|
2677
2511
|
const name = m[1];
|
|
2678
2512
|
const rest = (m[2] || "").trim();
|
|
2513
|
+
const options = this.parseLiveOptions(rest);
|
|
2514
|
+
const [bodyTokens, i] = this.collectLiveBody(tokens, start);
|
|
2515
|
+
_Frond.liveFragments.set(name, bodyTokens.map((t) => t[1]).join(""));
|
|
2516
|
+
const attrs = this.liveAttributes(name, options);
|
|
2517
|
+
const firstPaint = this.renderTokens([...bodyTokens], context);
|
|
2518
|
+
return [`<div ${attrs.join(" ")}>${firstPaint}</div>`, i];
|
|
2519
|
+
}
|
|
2520
|
+
parseLiveOptions(rest) {
|
|
2679
2521
|
const parts = rest.split(/\s+/).filter(Boolean);
|
|
2680
2522
|
const mode = parts[0] || "";
|
|
2681
|
-
const
|
|
2682
|
-
const src =
|
|
2683
|
-
if (src && (
|
|
2523
|
+
const sourceMatch = rest.match(LIVE_SRC_RE);
|
|
2524
|
+
const src = sourceMatch ? sourceMatch[1] : null;
|
|
2525
|
+
if (src && /^(?:https?:)?\/\//.test(src)) {
|
|
2684
2526
|
throw new Error("live: src must be a same-origin path, not an absolute URL");
|
|
2685
2527
|
}
|
|
2686
|
-
let interval = null;
|
|
2687
|
-
let wsPath = null;
|
|
2688
2528
|
if (mode === "poll") {
|
|
2689
2529
|
if (!parts[1] || !/^\d+$/.test(parts[1])) {
|
|
2690
2530
|
throw new Error('live: poll requires seconds, e.g. {% live "x" poll 5 %}');
|
|
2691
2531
|
}
|
|
2692
|
-
interval
|
|
2693
|
-
} else if (mode === "sse") {
|
|
2694
|
-
} else if (mode === "ws") {
|
|
2695
|
-
const wm = rest.match(LIVE_WS_RE);
|
|
2696
|
-
if (!wm) {
|
|
2697
|
-
throw new Error('live: ws requires a path, e.g. {% live "x" ws "/ws/x" %}');
|
|
2698
|
-
}
|
|
2699
|
-
wsPath = wm[1];
|
|
2700
|
-
} else {
|
|
2701
|
-
throw new Error(`live: unknown transport "${mode}" (use poll N, sse, or ws "path")`);
|
|
2532
|
+
return { mode, src, interval: parseInt(parts[1], 10), wsPath: null };
|
|
2702
2533
|
}
|
|
2703
|
-
|
|
2534
|
+
if (mode === "sse") return { mode, src, interval: null, wsPath: null };
|
|
2535
|
+
if (mode === "ws") {
|
|
2536
|
+
const wsMatch = rest.match(LIVE_WS_RE);
|
|
2537
|
+
if (!wsMatch) throw new Error('live: ws requires a path, e.g. {% live "x" ws "/ws/x" %}');
|
|
2538
|
+
return { mode, src, interval: null, wsPath: wsMatch[1] };
|
|
2539
|
+
}
|
|
2540
|
+
throw new Error(`live: unknown transport "${mode}" (use poll N, sse, or ws "path")`);
|
|
2541
|
+
}
|
|
2542
|
+
collectLiveBody(tokens, start) {
|
|
2543
|
+
const body = [];
|
|
2704
2544
|
let i = start + 1;
|
|
2705
2545
|
while (i < tokens.length) {
|
|
2706
|
-
if (tokens[i][0]
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
if (tag === "live") throw new Error("live: nested live blocks are not supported");
|
|
2710
|
-
if (tag === "endlive") {
|
|
2711
|
-
i++;
|
|
2712
|
-
break;
|
|
2713
|
-
}
|
|
2714
|
-
bodyTokens.push(tokens[i]);
|
|
2715
|
-
} else {
|
|
2716
|
-
bodyTokens.push(tokens[i]);
|
|
2546
|
+
if (tokens[i][0] !== "BLOCK") {
|
|
2547
|
+
body.push(tokens[i++]);
|
|
2548
|
+
continue;
|
|
2717
2549
|
}
|
|
2718
|
-
i
|
|
2550
|
+
const [tagContent] = stripTag(tokens[i][1]);
|
|
2551
|
+
const tag = tagContent.split(/\s+/)[0] || "";
|
|
2552
|
+
if (tag === "live") throw new Error("live: nested live blocks are not supported");
|
|
2553
|
+
if (tag === "endlive") return [body, i + 1];
|
|
2554
|
+
body.push(tokens[i++]);
|
|
2719
2555
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2556
|
+
return [body, i];
|
|
2557
|
+
}
|
|
2558
|
+
liveAttributes(name, options) {
|
|
2559
|
+
const endpoint = options.src || `/__frond/live/${name}`;
|
|
2722
2560
|
const attrs = [`data-frond-live="${liveAttr(name)}"`, `id="live-${liveAttr(name)}"`];
|
|
2723
|
-
if (mode === "poll") {
|
|
2724
|
-
attrs.push('data-mode="poll"', `data-interval="${interval}"`, `data-src="${liveAttr(endpoint)}"`);
|
|
2725
|
-
} else if (mode === "sse") {
|
|
2561
|
+
if (options.mode === "poll") {
|
|
2562
|
+
attrs.push('data-mode="poll"', `data-interval="${options.interval}"`, `data-src="${liveAttr(endpoint)}"`);
|
|
2563
|
+
} else if (options.mode === "sse") {
|
|
2726
2564
|
attrs.push('data-mode="sse"', `data-src="${liveAttr(endpoint)}"`);
|
|
2727
|
-
} else
|
|
2728
|
-
_Frond.liveWsPaths.set(name, wsPath);
|
|
2729
|
-
attrs.push('data-mode="ws"', `data-ws="${liveAttr(wsPath)}"`);
|
|
2565
|
+
} else {
|
|
2566
|
+
_Frond.liveWsPaths.set(name, options.wsPath);
|
|
2567
|
+
attrs.push('data-mode="ws"', `data-ws="${liveAttr(options.wsPath)}"`);
|
|
2730
2568
|
}
|
|
2731
|
-
|
|
2732
|
-
return [`<div ${attrs.join(" ")}>${firstPaint}</div>`, i];
|
|
2569
|
+
return attrs;
|
|
2733
2570
|
}
|
|
2734
2571
|
// ── Live-block class API (mirrors Python master + PHP/Ruby facades) ──
|
|
2735
2572
|
/**
|