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.
Files changed (50) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +2 -2
  3. package/package.json +1 -1
  4. package/packages/cli/dist/bin.js +3181 -3051
  5. package/packages/cli/src/commands/generate.ts +33 -22
  6. package/packages/cli/src/commands/lint.ts +77 -111
  7. package/packages/core/dist/index.js +3090 -2952
  8. package/packages/core/src/.tina4-metrics.json +15004 -0
  9. package/packages/core/src/aiClient.ts +199 -161
  10. package/packages/core/src/dispatchPipeline.ts +65 -67
  11. package/packages/core/src/docs.ts +52 -544
  12. package/packages/core/src/docsParser.ts +270 -0
  13. package/packages/core/src/docsScanner.ts +121 -0
  14. package/packages/core/src/docsSignatures.ts +165 -0
  15. package/packages/core/src/index.ts +2 -0
  16. package/packages/core/src/logger.ts +68 -82
  17. package/packages/core/src/mcp.ts +32 -60
  18. package/packages/core/src/messenger.ts +136 -157
  19. package/packages/core/src/middleware.ts +56 -60
  20. package/packages/core/src/plan.ts +78 -70
  21. package/packages/core/src/projectIndex.ts +15 -288
  22. package/packages/core/src/projectIndexExtractors.ts +126 -0
  23. package/packages/core/src/projectIndexStorage.ts +122 -0
  24. package/packages/core/src/push.ts +281 -0
  25. package/packages/core/src/server.ts +182 -183
  26. package/packages/frond/dist/index.js +607 -770
  27. package/packages/frond/src/engine.ts +670 -818
  28. package/packages/orm/dist/index.js +3100 -2965
  29. package/packages/orm/src/adapters/mongodb.ts +99 -144
  30. package/packages/orm/src/baseModel.ts +429 -515
  31. package/packages/orm/src/fakeData.ts +73 -61
  32. package/packages/orm/src/migration.ts +96 -126
  33. package/packages/orm/src/seeder.ts +6 -238
  34. package/packages/orm/src/seederTable.ts +101 -0
  35. package/packages/orm/src/seederTypes.ts +14 -0
  36. package/packages/orm/src/validation.ts +97 -80
  37. package/types/core/src/aiClient.d.ts +5 -0
  38. package/types/core/src/docsParser.d.ts +28 -0
  39. package/types/core/src/docsScanner.d.ts +1 -0
  40. package/types/core/src/docsSignatures.d.ts +11 -0
  41. package/types/core/src/index.d.ts +2 -0
  42. package/types/core/src/messenger.d.ts +8 -0
  43. package/types/core/src/projectIndexExtractors.d.ts +3 -0
  44. package/types/core/src/projectIndexStorage.d.ts +13 -0
  45. package/types/core/src/push.d.ts +45 -0
  46. package/types/frond/src/engine.d.ts +25 -0
  47. package/types/orm/src/fakeData.d.ts +3 -0
  48. package/types/orm/src/seeder.d.ts +3 -89
  49. package/types/orm/src/seederTable.d.ts +9 -0
  50. 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 inspectValue(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
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
- const name = value.name || "(anonymous)";
105
- return `[Function: ${name}]`;
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
- if (obj instanceof Date) {
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 resolveVar(expr, context) {
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
- [parts, fromBracket] = cachedPath;
302
- } else {
303
- parts = [];
304
- fromBracket = [];
305
- {
306
- let current = "";
307
- let depth = 0;
308
- let inQuote = null;
309
- for (let i = 0; i < expr.length; i++) {
310
- const ch = expr[i];
311
- if (inQuote) {
312
- current += ch;
313
- if (ch === inQuote) inQuote = null;
314
- continue;
315
- }
316
- if (ch === '"' || ch === "'") {
317
- inQuote = ch;
318
- current += ch;
319
- continue;
320
- }
321
- if (ch === "(") {
322
- depth++;
323
- current += ch;
324
- continue;
325
- }
326
- if (ch === ")") {
327
- depth--;
328
- current += ch;
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
- let key;
402
- if (isQuotedPart) {
403
- key = part.slice(1, -1);
404
- } else {
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
- if (typeof value === "object" && value !== null) {
416
- if (Array.isArray(value) && typeof key === "number") {
417
- value = value[key];
418
- } else if (key in value) {
419
- const v = value[key];
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
- } else {
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, raw] = tokens[i];
1862
+ const [ttype] = tokens[i];
1782
1863
  if (ttype === "TEXT") {
1783
- output.push(raw);
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
- const [content, stripB, stripA] = stripTag(raw);
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
- const [content, stripB, stripA] = stripTag(raw);
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 (this._sandbox && this._allowedFilters !== null && !this._allowedFilters.has(fname)) continue;
1941
- const [realFname, tailPath] = splitFilterNameAndPath(fname);
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 (this._sandbox && this._allowedFilters !== null && !this._allowedFilters.has(fname)) continue;
2017
- const [realFname, tailPath] = splitFilterNameAndPath(fname);
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
- evalVarInner(expr, context) {
2080
- const [varName, filters] = parseFilterChain(expr);
2081
- if (this._sandbox && this._allowedVars !== null) {
2082
- const rootVar = varName.split(".")[0].split("[")[0].trim();
2083
- if (rootVar && !this._allowedVars.has(rootVar) && rootVar !== "loop") {
2084
- return "";
2085
- }
2086
- }
2087
- if (findOutsideQuotes(expr, "~") >= 0) {
2088
- let concatValue = evalExpr(expr, context);
2089
- if (concatValue instanceof SafeString) return concatValue.value;
2090
- if (this._autoEscape && typeof concatValue === "string") {
2091
- concatValue = htmlEscape(concatValue);
2092
- }
2093
- return concatValue;
2094
- }
2095
- let value = evalExpr(varName, context);
2096
- let isSafe = false;
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)) isSafe = true;
2064
+ if (this.filterPermitted(fname)) safe = true;
2101
2065
  continue;
2102
2066
  }
2103
- if (fname === "escape" || fname === "e") {
2104
- if (this.filterPermitted(fname)) isSafe = true;
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 (!isSafe && this._autoEscape && typeof value === "string") {
2083
+ if (!applied.safe && this._autoEscape && typeof value === "string") {
2221
2084
  value = htmlEscape(value);
2222
2085
  }
2223
2086
  return value;
2224
2087
  }
2225
- handleIf(tokens, start, context) {
2226
- const [content] = stripTag(tokens[start][1]);
2227
- const conditionExpr = content.slice(3).trim();
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 === "BLOCK") {
2236
- const [tagContent, tagStripB, tagStripA] = stripTag(raw);
2237
- const tag = tagContent.split(/\s+/)[0] || "";
2238
- if (tag === "if") {
2239
- depth++;
2240
- currentTokens.push(tokens[i]);
2241
- } else if (tag === "endif" && depth > 0) {
2242
- depth--;
2243
- currentTokens.push(tokens[i]);
2244
- } else if (tag === "endif" && depth === 0) {
2245
- if (tagStripB && currentTokens.length > 0 && currentTokens[currentTokens.length - 1][0] === "TEXT") {
2246
- currentTokens[currentTokens.length - 1] = ["TEXT", currentTokens[currentTokens.length - 1][1].replace(TRAILING_WS_RE, "")];
2247
- }
2248
- branches.push([currentCond, currentTokens]);
2249
- if (tagStripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
2250
- tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
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), i];
2143
+ return [this.renderTokens([...branchTokens], context), next];
2279
2144
  }
2280
2145
  }
2281
- return ["", i];
2146
+ return ["", next];
2282
2147
  }
2283
- handleFor(tokens, start, context) {
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 [ttype, raw] = tokens[i];
2298
- if (ttype === "BLOCK") {
2299
- const [tagContent] = stripTag(raw);
2300
- const tag = tagContent.split(/\s+/)[0] || "";
2301
- if (tag === "for") {
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
- i++;
2325
- }
2326
- const iterable = evalExpr(iterableExpr, context);
2327
- if (!iterable || Array.isArray(iterable) && iterable.length === 0 || typeof iterable === "object" && !Array.isArray(iterable) && Object.keys(iterable).length === 0) {
2328
- if (elseTokens.length > 0) {
2329
- return [this.renderTokens([...elseTokens], context), i];
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
- return ["", i];
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: total,
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 < total; 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 === total - 1;
2354
- loopObj.revindex = total - idx;
2355
- loopObj.revindex0 = total - idx - 1;
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
- if (var2) {
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
- if (prop in target) return target[prop];
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 tokens = tokenize(source);
2504
- let i = 0;
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 tokens = tokenize(source);
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
- const [ttype, raw] = tokens[i];
2554
- if (ttype === "BLOCK") {
2555
- const [tagContent] = stripTag(raw);
2556
- const tag = tagContent.split(/\s+/)[0] || "";
2557
- if (tag === "macro") {
2558
- const macroM = tagContent.match(/^macro\s+(\w+)\s*\(([^)]*)\)/);
2559
- if (macroM && names.includes(macroM[1])) {
2560
- const macroName = macroM[1];
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 sm = rest.match(LIVE_SRC_RE);
2682
- const src = sm ? sm[1] : null;
2683
- if (src && (src.startsWith("http://") || src.startsWith("https://") || src.startsWith("//"))) {
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 = parseInt(parts[1], 10);
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
- const bodyTokens = [];
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] === "BLOCK") {
2707
- const [tagContent] = stripTag(tokens[i][1]);
2708
- const tag = tagContent.split(/\s+/)[0] || "";
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
- _Frond.liveFragments.set(name, bodyTokens.map((t) => t[1]).join(""));
2721
- const endpoint = src || `/__frond/live/${name}`;
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 if (mode === "ws") {
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
- const firstPaint = this.renderTokens([...bodyTokens], context);
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
  /**