tina4-nodejs 3.13.88 → 3.13.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -3
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +109 -19
- package/packages/cli/src/commands/generate.ts +43 -17
- package/packages/core/dist/index.js +82 -2
- package/packages/core/public/js/tina4-dev-admin.min.js +2 -2
- package/packages/frond/dist/index.js +81 -1
- package/packages/frond/src/engine.ts +88 -1
- package/packages/orm/dist/index.js +82 -2
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.90)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.90 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
8
8
|
|
|
9
9
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
10
10
|
|
|
@@ -1244,7 +1244,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
|
|
|
1244
1244
|
## v3 Features Summary
|
|
1245
1245
|
|
|
1246
1246
|
- **98 built-in features**, zero third-party dependencies
|
|
1247
|
-
- **5,
|
|
1247
|
+
- **5,923 tests** passing across 189 files (build + typecheck green)
|
|
1248
1248
|
- **Race-safe `getNextId()`** with atomic sequence table (`tina4_sequences`) for SQLite/MySQL/MSSQL; PostgreSQL auto-creates sequences
|
|
1249
1249
|
- **Frond template engine optimizations**: pre-compiled regexes, lazy loop context (copy-on-write), filter chain caching, path split caching, inline common filters (11-15% speedup)
|
|
1250
1250
|
- **Production server auto-detect**: `npx tina4nodejs serve --production` auto-uses cluster mode
|
package/package.json
CHANGED
package/packages/cli/dist/bin.js
CHANGED
|
@@ -13793,7 +13793,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13793
13793
|
function _generateFormTokenValue(descriptor = "") {
|
|
13794
13794
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13795
13795
|
}
|
|
13796
|
-
var SafeString, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13796
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13797
13797
|
var init_engine = __esm({
|
|
13798
13798
|
"../frond/src/engine.ts"() {
|
|
13799
13799
|
"use strict";
|
|
@@ -13805,6 +13805,37 @@ var init_engine = __esm({
|
|
|
13805
13805
|
return this.value;
|
|
13806
13806
|
}
|
|
13807
13807
|
};
|
|
13808
|
+
KNOWN_TAGS = /* @__PURE__ */ new Set([
|
|
13809
|
+
"autoescape",
|
|
13810
|
+
"block",
|
|
13811
|
+
"cache",
|
|
13812
|
+
"extends",
|
|
13813
|
+
"for",
|
|
13814
|
+
"from",
|
|
13815
|
+
"if",
|
|
13816
|
+
"import",
|
|
13817
|
+
"include",
|
|
13818
|
+
"live",
|
|
13819
|
+
"macro",
|
|
13820
|
+
"raw",
|
|
13821
|
+
"set",
|
|
13822
|
+
"spaceless"
|
|
13823
|
+
]);
|
|
13824
|
+
TERMINATOR_TAGS = /* @__PURE__ */ new Set([
|
|
13825
|
+
"elif",
|
|
13826
|
+
"else",
|
|
13827
|
+
"elseif",
|
|
13828
|
+
"endautoescape",
|
|
13829
|
+
"endblock",
|
|
13830
|
+
"endcache",
|
|
13831
|
+
"endfor",
|
|
13832
|
+
"endif",
|
|
13833
|
+
"endlive",
|
|
13834
|
+
"endmacro",
|
|
13835
|
+
"endraw",
|
|
13836
|
+
"endset",
|
|
13837
|
+
"endspaceless"
|
|
13838
|
+
]);
|
|
13808
13839
|
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
13809
13840
|
JSON_UNSAFE_MAP = {
|
|
13810
13841
|
"<": "\\u003c",
|
|
@@ -14459,8 +14490,11 @@ var init_engine = __esm({
|
|
|
14459
14490
|
i = skip;
|
|
14460
14491
|
}
|
|
14461
14492
|
} else if (tag === "set") {
|
|
14493
|
+
const isBlockSet = !content.includes("=");
|
|
14462
14494
|
if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
|
|
14463
|
-
i
|
|
14495
|
+
i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
|
|
14496
|
+
} else if (isBlockSet) {
|
|
14497
|
+
i = this.handleSetBlock(tokens, i, context);
|
|
14464
14498
|
} else {
|
|
14465
14499
|
this.handleSet(content, context);
|
|
14466
14500
|
i++;
|
|
@@ -14502,6 +14536,11 @@ var init_engine = __esm({
|
|
|
14502
14536
|
i++;
|
|
14503
14537
|
} else {
|
|
14504
14538
|
i++;
|
|
14539
|
+
if (tag !== "" && !TERMINATOR_TAGS.has(tag)) {
|
|
14540
|
+
throw new Error(
|
|
14541
|
+
`Frond: unknown tag "${tag}" -- known tags are: ${[...KNOWN_TAGS].sort().join(", ")}`
|
|
14542
|
+
);
|
|
14543
|
+
}
|
|
14505
14544
|
}
|
|
14506
14545
|
if (stripA && i < tokens.length && tokens[i][0] === "TEXT") {
|
|
14507
14546
|
tokens[i] = ["TEXT", tokens[i][1].replace(LEADING_WS_RE, "")];
|
|
@@ -15398,6 +15437,47 @@ var init_engine = __esm({
|
|
|
15398
15437
|
}
|
|
15399
15438
|
return html;
|
|
15400
15439
|
}
|
|
15440
|
+
/**
|
|
15441
|
+
* {% set name %}...{% endset %} -- render the body and bind it.
|
|
15442
|
+
*
|
|
15443
|
+
* Emits nothing itself. The captured value is a SafeString because it is
|
|
15444
|
+
* template output that has already been escaped on the way in; re-escaping it
|
|
15445
|
+
* at {{ name }} would double-encode every entity. Twig and Jinja2 both mark the
|
|
15446
|
+
* capture safe. Returns the index just past {% endset %}.
|
|
15447
|
+
*/
|
|
15448
|
+
handleSetBlock(tokens, start2, context) {
|
|
15449
|
+
const [content] = stripTag(tokens[start2][1]);
|
|
15450
|
+
const name = (content.split(/\s+/)[1] || "").trim();
|
|
15451
|
+
const bodyTokens = [];
|
|
15452
|
+
let i = start2 + 1;
|
|
15453
|
+
let depth = 0;
|
|
15454
|
+
while (i < tokens.length) {
|
|
15455
|
+
if (tokens[i][0] === "BLOCK") {
|
|
15456
|
+
const [tagContent] = stripTag(tokens[i][1]);
|
|
15457
|
+
const tag = tagContent.split(/\s+/)[0] || "";
|
|
15458
|
+
if (tag === "set" && !tagContent.includes("=")) {
|
|
15459
|
+
depth++;
|
|
15460
|
+
bodyTokens.push(tokens[i]);
|
|
15461
|
+
} else if (tag === "endset") {
|
|
15462
|
+
if (depth === 0) {
|
|
15463
|
+
i++;
|
|
15464
|
+
break;
|
|
15465
|
+
}
|
|
15466
|
+
depth--;
|
|
15467
|
+
bodyTokens.push(tokens[i]);
|
|
15468
|
+
} else {
|
|
15469
|
+
bodyTokens.push(tokens[i]);
|
|
15470
|
+
}
|
|
15471
|
+
} else {
|
|
15472
|
+
bodyTokens.push(tokens[i]);
|
|
15473
|
+
}
|
|
15474
|
+
i++;
|
|
15475
|
+
}
|
|
15476
|
+
if (name) {
|
|
15477
|
+
context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
|
|
15478
|
+
}
|
|
15479
|
+
return i;
|
|
15480
|
+
}
|
|
15401
15481
|
handleSpaceless(tokens, start2, context) {
|
|
15402
15482
|
const bodyTokens = [];
|
|
15403
15483
|
let i = start2 + 1;
|
|
@@ -38681,6 +38761,11 @@ function parseFields(fieldsStr) {
|
|
|
38681
38761
|
}
|
|
38682
38762
|
return result;
|
|
38683
38763
|
}
|
|
38764
|
+
var DEFAULT_FIELDS = [["name", "string"]];
|
|
38765
|
+
function fieldsOrDefault(fieldsStr) {
|
|
38766
|
+
const parsed = parseFields(fieldsStr);
|
|
38767
|
+
return parsed.length > 0 ? parsed : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
|
|
38768
|
+
}
|
|
38684
38769
|
function parseCliArgs(args) {
|
|
38685
38770
|
const booleanFlags = /* @__PURE__ */ new Set([
|
|
38686
38771
|
"no-browser",
|
|
@@ -38796,7 +38881,7 @@ async function generate2(what, name, extraArgs = []) {
|
|
|
38796
38881
|
}
|
|
38797
38882
|
}
|
|
38798
38883
|
function generateModel(name, flags, emitTest = true) {
|
|
38799
|
-
const fields =
|
|
38884
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
38800
38885
|
const table2 = toTableName(name);
|
|
38801
38886
|
const dir = resolve29("src/models");
|
|
38802
38887
|
ensureDir(dir);
|
|
@@ -38804,13 +38889,9 @@ function generateModel(name, flags, emitTest = true) {
|
|
|
38804
38889
|
const fieldLines = [
|
|
38805
38890
|
` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`
|
|
38806
38891
|
];
|
|
38807
|
-
|
|
38808
|
-
|
|
38809
|
-
|
|
38810
|
-
fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
|
|
38811
|
-
}
|
|
38812
|
-
} else {
|
|
38813
|
-
fieldLines.push(` name: { type: "string" as const },`);
|
|
38892
|
+
for (const [fname, ftype] of fields) {
|
|
38893
|
+
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
38894
|
+
fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
|
|
38814
38895
|
}
|
|
38815
38896
|
fieldLines.push(` created_at: { type: "datetime" as const },`);
|
|
38816
38897
|
const content = `import { BaseModel } from "tina4-nodejs/orm";
|
|
@@ -38824,7 +38905,7 @@ ${fieldLines.join("\n")}
|
|
|
38824
38905
|
`;
|
|
38825
38906
|
writeFileSafe(path8, content);
|
|
38826
38907
|
if (!flags["no-migration"]) {
|
|
38827
|
-
generateMigration(`create_${table2}`, flags, fields
|
|
38908
|
+
generateMigration(`create_${table2}`, flags, fields, table2, false);
|
|
38828
38909
|
}
|
|
38829
38910
|
if (emitTest) emitModelTest(name, table2, fields);
|
|
38830
38911
|
}
|
|
@@ -38895,7 +38976,12 @@ ${extend(
|
|
|
38895
38976
|
"validate / business rules before persist",
|
|
38896
38977
|
`e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`
|
|
38897
38978
|
)} const item = new ${model}(req.body as Record<string, unknown>);
|
|
38898
|
-
|
|
38979
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
38980
|
+
// write is reported to the client as a 201 carrying unsaved data.
|
|
38981
|
+
if ((await item.save()) === false) {
|
|
38982
|
+
res.json({ error: "Could not create ${singular}" }, 400);
|
|
38983
|
+
return;
|
|
38984
|
+
}
|
|
38899
38985
|
res.json({ data: item.toObject() }, 201);
|
|
38900
38986
|
}
|
|
38901
38987
|
`
|
|
@@ -38975,7 +39061,12 @@ ${extend(
|
|
|
38975
39061
|
"guard which fields / who may update",
|
|
38976
39062
|
`e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`
|
|
38977
39063
|
)} Object.assign(item, req.body as Record<string, unknown>);
|
|
38978
|
-
|
|
39064
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
39065
|
+
// write is reported to the client as a 200 carrying unsaved data.
|
|
39066
|
+
if ((await item.save()) === false) {
|
|
39067
|
+
res.json({ error: "Could not update ${singular}" }, 400);
|
|
39068
|
+
return;
|
|
39069
|
+
}
|
|
38979
39070
|
res.json({ data: item.toObject() });
|
|
38980
39071
|
}
|
|
38981
39072
|
`
|
|
@@ -39282,7 +39373,7 @@ void test${titleName};
|
|
|
39282
39373
|
writeFileSafe(path8, content);
|
|
39283
39374
|
}
|
|
39284
39375
|
function generateForm(name, flags) {
|
|
39285
|
-
const fields =
|
|
39376
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
39286
39377
|
const table2 = toTableName(name);
|
|
39287
39378
|
const routeName = toPlural(table2);
|
|
39288
39379
|
const inputTypes = {
|
|
@@ -39302,9 +39393,8 @@ function generateForm(name, flags) {
|
|
|
39302
39393
|
const dir = resolve29("src/templates/forms");
|
|
39303
39394
|
ensureDir(dir);
|
|
39304
39395
|
const path8 = join33(dir, `${table2}.twig`);
|
|
39305
|
-
const fieldEntries = fields.length > 0 ? fields : [["name", "string"]];
|
|
39306
39396
|
let fieldHtml = "";
|
|
39307
|
-
for (const [fname, ftype] of
|
|
39397
|
+
for (const [fname, ftype] of fields) {
|
|
39308
39398
|
const itype = inputTypes[ftype] || "text";
|
|
39309
39399
|
const label = fname.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
39310
39400
|
const step = ["float", "numeric", "decimal"].includes(ftype) ? ' step="0.01"' : "";
|
|
@@ -39348,10 +39438,10 @@ function generateForm(name, flags) {
|
|
|
39348
39438
|
writeFileSafe(path8, content);
|
|
39349
39439
|
}
|
|
39350
39440
|
function generateView(name, flags) {
|
|
39351
|
-
const fields =
|
|
39441
|
+
const fields = fieldsOrDefault(flags.fields || "");
|
|
39352
39442
|
const table2 = toTableName(name);
|
|
39353
39443
|
const routeName = toPlural(table2);
|
|
39354
|
-
const cols = fields.
|
|
39444
|
+
const cols = fields.map(([f]) => f);
|
|
39355
39445
|
const dir = resolve29("src/templates/pages");
|
|
39356
39446
|
ensureDir(dir);
|
|
39357
39447
|
const listPath = join33(dir, `${routeName}.twig`);
|
|
@@ -39866,7 +39956,7 @@ function sampleLiteral(fieldType) {
|
|
|
39866
39956
|
}
|
|
39867
39957
|
}
|
|
39868
39958
|
function emitModelTest(model, table2, fields) {
|
|
39869
|
-
const flds = fields.length > 0 ? fields : [[
|
|
39959
|
+
const flds = fields.length > 0 ? fields : DEFAULT_FIELDS.map(([f, t]) => [f, t]);
|
|
39870
39960
|
const payload = flds.map(([f, t]) => `${f}: ${sampleLiteral(t)}`).join(", ");
|
|
39871
39961
|
const stringField = flds.find(([, t]) => ["string", "str", "text"].includes((t || "string").toLowerCase()))?.[0];
|
|
39872
39962
|
const valueAssert = stringField ? `
|
|
@@ -115,6 +115,21 @@ export function parseFields(fieldsStr: string): Array<[string, string]> {
|
|
|
115
115
|
return result;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// Called without --fields, the generators fall back to a single `name` string
|
|
119
|
+
// column. That default MUST be materialised here, in one place, and then flow
|
|
120
|
+
// into the model, the migration, the form, the view and the test alike. It used
|
|
121
|
+
// to live only inside the model template, so `generate model X` / `generate
|
|
122
|
+
// crud X` wrote a model declaring `name` while the migration - built from the
|
|
123
|
+
// parsed field list, which was empty - created only id + created_at. The first
|
|
124
|
+
// write then failed with "table x has no column named name".
|
|
125
|
+
export const DEFAULT_FIELDS: ReadonlyArray<[string, string]> = [["name", "string"]];
|
|
126
|
+
|
|
127
|
+
/** Parsed --fields, or the default single `name` column when none given. */
|
|
128
|
+
export function fieldsOrDefault(fieldsStr: string): Array<[string, string]> {
|
|
129
|
+
const parsed = parseFields(fieldsStr);
|
|
130
|
+
return parsed.length > 0 ? parsed : DEFAULT_FIELDS.map(([f, t]) => [f, t] as [string, string]);
|
|
131
|
+
}
|
|
132
|
+
|
|
118
133
|
export function parseCliArgs(args: string[]): { flags: Record<string, string | boolean>; positional: string[] } {
|
|
119
134
|
// Boolean-only flags that never take a value argument.
|
|
120
135
|
const booleanFlags = new Set([
|
|
@@ -290,7 +305,7 @@ export async function generate(what: string, name: string, extraArgs: string[] =
|
|
|
290
305
|
// ── Model ───────────────────────────────────────────────────────────
|
|
291
306
|
|
|
292
307
|
function generateModel(name: string, flags: Record<string, string | boolean>, emitTest = true): void {
|
|
293
|
-
const fields =
|
|
308
|
+
const fields = fieldsOrDefault((flags.fields as string) || "");
|
|
294
309
|
const table = toTableName(name);
|
|
295
310
|
const dir = resolve("src/models");
|
|
296
311
|
ensureDir(dir);
|
|
@@ -300,13 +315,9 @@ function generateModel(name: string, flags: Record<string, string | boolean>, em
|
|
|
300
315
|
const fieldLines: string[] = [
|
|
301
316
|
` id: { type: "integer" as const, primaryKey: true, autoIncrement: true },`,
|
|
302
317
|
];
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
fieldLines.push(` name: { type: "string" as const },`);
|
|
318
|
+
for (const [fname, ftype] of fields) {
|
|
319
|
+
const info = FIELD_TYPE_MAP[ftype] || FIELD_TYPE_MAP.string;
|
|
320
|
+
fieldLines.push(` ${fname}: { type: ${info.orm} as const },`);
|
|
310
321
|
}
|
|
311
322
|
fieldLines.push(` created_at: { type: "datetime" as const },`);
|
|
312
323
|
|
|
@@ -328,7 +339,11 @@ ${fieldLines.join("\n")}
|
|
|
328
339
|
// (below) proves the schema through the real ORM, so the migration sub-call
|
|
329
340
|
// does NOT also co-emit a migration test (emitTest=false).
|
|
330
341
|
if (!flags["no-migration"]) {
|
|
331
|
-
|
|
342
|
+
// Always hand over the RESOLVED field list. Passing `undefined` when the
|
|
343
|
+
// parsed list was empty made the migration fall back to its own
|
|
344
|
+
// parseFields() - also empty - so the table got only id + created_at while
|
|
345
|
+
// the model above declared `name`, and the first write 500'd.
|
|
346
|
+
generateMigration(`create_${table}`, flags, fields, table, false);
|
|
332
347
|
}
|
|
333
348
|
|
|
334
349
|
// Co-emit a real SQLite roundtrip test next to the model. Composite
|
|
@@ -417,7 +432,12 @@ ${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Creat
|
|
|
417
432
|
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
418
433
|
${extend("validate / business rules before persist",
|
|
419
434
|
`e.g. reject invalid input; ground: tina4_context("validate before create", "nodejs")`)} const item = new ${model}(req.body as Record<string, unknown>);
|
|
420
|
-
|
|
435
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
436
|
+
// write is reported to the client as a 201 carrying unsaved data.
|
|
437
|
+
if ((await item.save()) === false) {
|
|
438
|
+
res.json({ error: "Could not create ${singular}" }, 400);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
421
441
|
res.json({ data: item.toObject() }, 201);
|
|
422
442
|
}
|
|
423
443
|
`,
|
|
@@ -499,7 +519,12 @@ export default async function (req: Tina4Request, res: Tina4Response) {
|
|
|
499
519
|
}
|
|
500
520
|
${extend("guard which fields / who may update",
|
|
501
521
|
`e.g. enforce ownership; ground: tina4_context("authorize update", "nodejs")`)} Object.assign(item, req.body as Record<string, unknown>);
|
|
502
|
-
|
|
522
|
+
// save() returns false on failure rather than throwing - check it, or a failed
|
|
523
|
+
// write is reported to the client as a 200 carrying unsaved data.
|
|
524
|
+
if ((await item.save()) === false) {
|
|
525
|
+
res.json({ error: "Could not update ${singular}" }, 400);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
503
528
|
res.json({ data: item.toObject() });
|
|
504
529
|
}
|
|
505
530
|
`,
|
|
@@ -873,7 +898,7 @@ void test${titleName};
|
|
|
873
898
|
// ── Form ────────────────────────────────────────────────────────────
|
|
874
899
|
|
|
875
900
|
function generateForm(name: string, flags: Record<string, string | boolean>): void {
|
|
876
|
-
const fields =
|
|
901
|
+
const fields = fieldsOrDefault((flags.fields as string) || "");
|
|
877
902
|
const table = toTableName(name);
|
|
878
903
|
const routeName = toPlural(table);
|
|
879
904
|
|
|
@@ -890,9 +915,8 @@ function generateForm(name: string, flags: Record<string, string | boolean>): vo
|
|
|
890
915
|
const path = join(dir, `${table}.twig`);
|
|
891
916
|
|
|
892
917
|
// Build form fields
|
|
893
|
-
const fieldEntries = fields.length > 0 ? fields : [["name", "string"] as [string, string]];
|
|
894
918
|
let fieldHtml = "";
|
|
895
|
-
for (const [fname, ftype] of
|
|
919
|
+
for (const [fname, ftype] of fields) {
|
|
896
920
|
const itype = inputTypes[ftype] || "text";
|
|
897
921
|
const label = fname.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
898
922
|
const step = ["float", "numeric", "decimal"].includes(ftype) ? ' step="0.01"' : "";
|
|
@@ -946,11 +970,11 @@ function generateForm(name: string, flags: Record<string, string | boolean>): vo
|
|
|
946
970
|
// ── View ────────────────────────────────────────────────────────────
|
|
947
971
|
|
|
948
972
|
function generateView(name: string, flags: Record<string, string | boolean>): void {
|
|
949
|
-
const fields =
|
|
973
|
+
const fields = fieldsOrDefault((flags.fields as string) || "");
|
|
950
974
|
const table = toTableName(name);
|
|
951
975
|
const routeName = toPlural(table);
|
|
952
976
|
|
|
953
|
-
const cols = fields.
|
|
977
|
+
const cols = fields.map(([f]) => f);
|
|
954
978
|
|
|
955
979
|
const dir = resolve("src/templates/pages");
|
|
956
980
|
ensureDir(dir);
|
|
@@ -1591,7 +1615,9 @@ function sampleLiteral(fieldType: string): string {
|
|
|
1591
1615
|
|
|
1592
1616
|
/** model → real SQLite roundtrip (create / read back / missing → null). */
|
|
1593
1617
|
function emitModelTest(model: string, table: string, fields: Array<[string, string]>): void {
|
|
1594
|
-
|
|
1618
|
+
// Reuse the single DEFAULT_FIELDS constant rather than re-stating the literal,
|
|
1619
|
+
// so the co-emitted test can never describe a shape the model does not have.
|
|
1620
|
+
const flds = fields.length > 0 ? fields : DEFAULT_FIELDS.map(([f, t]) => [f, t] as [string, string]);
|
|
1595
1621
|
const payload = flds.map(([f, t]) => `${f}: ${sampleLiteral(t)}`).join(", ");
|
|
1596
1622
|
const stringField = flds.find(([, t]) => ["string", "str", "text"].includes((t || "string").toLowerCase()))?.[0];
|
|
1597
1623
|
const valueAssert = stringField
|
|
@@ -13792,7 +13792,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
13792
13792
|
function _generateFormTokenValue(descriptor = "") {
|
|
13793
13793
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
13794
13794
|
}
|
|
13795
|
-
var SafeString, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13795
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
13796
13796
|
var init_engine = __esm({
|
|
13797
13797
|
"../frond/src/engine.ts"() {
|
|
13798
13798
|
"use strict";
|
|
@@ -13804,6 +13804,37 @@ var init_engine = __esm({
|
|
|
13804
13804
|
return this.value;
|
|
13805
13805
|
}
|
|
13806
13806
|
};
|
|
13807
|
+
KNOWN_TAGS = /* @__PURE__ */ new Set([
|
|
13808
|
+
"autoescape",
|
|
13809
|
+
"block",
|
|
13810
|
+
"cache",
|
|
13811
|
+
"extends",
|
|
13812
|
+
"for",
|
|
13813
|
+
"from",
|
|
13814
|
+
"if",
|
|
13815
|
+
"import",
|
|
13816
|
+
"include",
|
|
13817
|
+
"live",
|
|
13818
|
+
"macro",
|
|
13819
|
+
"raw",
|
|
13820
|
+
"set",
|
|
13821
|
+
"spaceless"
|
|
13822
|
+
]);
|
|
13823
|
+
TERMINATOR_TAGS = /* @__PURE__ */ new Set([
|
|
13824
|
+
"elif",
|
|
13825
|
+
"else",
|
|
13826
|
+
"elseif",
|
|
13827
|
+
"endautoescape",
|
|
13828
|
+
"endblock",
|
|
13829
|
+
"endcache",
|
|
13830
|
+
"endfor",
|
|
13831
|
+
"endif",
|
|
13832
|
+
"endlive",
|
|
13833
|
+
"endmacro",
|
|
13834
|
+
"endraw",
|
|
13835
|
+
"endset",
|
|
13836
|
+
"endspaceless"
|
|
13837
|
+
]);
|
|
13807
13838
|
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
13808
13839
|
JSON_UNSAFE_MAP = {
|
|
13809
13840
|
"<": "\\u003c",
|
|
@@ -14458,8 +14489,11 @@ var init_engine = __esm({
|
|
|
14458
14489
|
i = skip;
|
|
14459
14490
|
}
|
|
14460
14491
|
} else if (tag === "set") {
|
|
14492
|
+
const isBlockSet = !content.includes("=");
|
|
14461
14493
|
if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
|
|
14462
|
-
i
|
|
14494
|
+
i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
|
|
14495
|
+
} else if (isBlockSet) {
|
|
14496
|
+
i = this.handleSetBlock(tokens, i, context);
|
|
14463
14497
|
} else {
|
|
14464
14498
|
this.handleSet(content, context);
|
|
14465
14499
|
i++;
|
|
@@ -14501,6 +14535,11 @@ var init_engine = __esm({
|
|
|
14501
14535
|
i++;
|
|
14502
14536
|
} else {
|
|
14503
14537
|
i++;
|
|
14538
|
+
if (tag !== "" && !TERMINATOR_TAGS.has(tag)) {
|
|
14539
|
+
throw new Error(
|
|
14540
|
+
`Frond: unknown tag "${tag}" -- known tags are: ${[...KNOWN_TAGS].sort().join(", ")}`
|
|
14541
|
+
);
|
|
14542
|
+
}
|
|
14504
14543
|
}
|
|
14505
14544
|
if (stripA && i < tokens.length && tokens[i][0] === "TEXT") {
|
|
14506
14545
|
tokens[i] = ["TEXT", tokens[i][1].replace(LEADING_WS_RE, "")];
|
|
@@ -15397,6 +15436,47 @@ var init_engine = __esm({
|
|
|
15397
15436
|
}
|
|
15398
15437
|
return html;
|
|
15399
15438
|
}
|
|
15439
|
+
/**
|
|
15440
|
+
* {% set name %}...{% endset %} -- render the body and bind it.
|
|
15441
|
+
*
|
|
15442
|
+
* Emits nothing itself. The captured value is a SafeString because it is
|
|
15443
|
+
* template output that has already been escaped on the way in; re-escaping it
|
|
15444
|
+
* at {{ name }} would double-encode every entity. Twig and Jinja2 both mark the
|
|
15445
|
+
* capture safe. Returns the index just past {% endset %}.
|
|
15446
|
+
*/
|
|
15447
|
+
handleSetBlock(tokens, start2, context) {
|
|
15448
|
+
const [content] = stripTag(tokens[start2][1]);
|
|
15449
|
+
const name = (content.split(/\s+/)[1] || "").trim();
|
|
15450
|
+
const bodyTokens = [];
|
|
15451
|
+
let i = start2 + 1;
|
|
15452
|
+
let depth = 0;
|
|
15453
|
+
while (i < tokens.length) {
|
|
15454
|
+
if (tokens[i][0] === "BLOCK") {
|
|
15455
|
+
const [tagContent] = stripTag(tokens[i][1]);
|
|
15456
|
+
const tag = tagContent.split(/\s+/)[0] || "";
|
|
15457
|
+
if (tag === "set" && !tagContent.includes("=")) {
|
|
15458
|
+
depth++;
|
|
15459
|
+
bodyTokens.push(tokens[i]);
|
|
15460
|
+
} else if (tag === "endset") {
|
|
15461
|
+
if (depth === 0) {
|
|
15462
|
+
i++;
|
|
15463
|
+
break;
|
|
15464
|
+
}
|
|
15465
|
+
depth--;
|
|
15466
|
+
bodyTokens.push(tokens[i]);
|
|
15467
|
+
} else {
|
|
15468
|
+
bodyTokens.push(tokens[i]);
|
|
15469
|
+
}
|
|
15470
|
+
} else {
|
|
15471
|
+
bodyTokens.push(tokens[i]);
|
|
15472
|
+
}
|
|
15473
|
+
i++;
|
|
15474
|
+
}
|
|
15475
|
+
if (name) {
|
|
15476
|
+
context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
|
|
15477
|
+
}
|
|
15478
|
+
return i;
|
|
15479
|
+
}
|
|
15400
15480
|
handleSpaceless(tokens, start2, context) {
|
|
15401
15481
|
const bodyTokens = [];
|
|
15402
15482
|
let i = start2 + 1;
|
|
@@ -985,7 +985,7 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
|
|
|
985
985
|
<span class="thread-pip"></span>
|
|
986
986
|
<span class="thread-title">${g(t.title)}</span>
|
|
987
987
|
${o}
|
|
988
|
-
</div>`}).join("")}let cd=!1;async function Qa(i=0){for(let e=0;;e++)try{Se=await NZ(),cd=!1,EQ();return}catch(t){if(e>=i){cd=!0,console.error("refreshThreadList failed",t);return}await new Promise(n=>setTimeout(n,250*2**e))}}function HZ(i){const e=document.getElementById("editor-ai-messages");if(!e)return;const t=rt.get(i)||[];if(!t.length){e.innerHTML='<div class="ai-msg ai-bot" style="opacity:0.6">Start the conversation…</div>';return}e.innerHTML="";for(const n of t){const r=document.createElement("div");r.className=`ai-msg ai-${n.role==="user"?"user":"bot"}`,n.role==="user"?r.textContent=n.content:r.innerHTML=fs(n.content),e.appendChild(r)}e.scrollTop=e.scrollHeight}async function AQ(i,e=!1){if(!(!e&&rt.has(i)))try{const t=await FZ(i);rt.set(i,t)}catch(t){console.error(`loadThreadMessages(${i}) failed`,t),rt.set(i,[])}}async function dd(i){if(i!==ve){ve=i;try{localStorage.setItem(WQ,i)}catch{}EQ(),await AQ(i),HZ(i)}}async function KZ(){try{const i=await cs();Se.push(i),rt.set(i.id,[]),await dd(i.id);const e=document.getElementById("editor-ai-input");e==null||e.focus()}catch(i){AZ(`<span style="color:var(--danger)">Couldn't create thread: ${g(String((i==null?void 0:i.message)||i))}</span>`,"bot")}}async function JZ(i){const e=document.querySelector(`.thread-row[data-thread-id="${CSS.escape(i)}"]`),t=e==null?void 0:e.querySelector(".thread-title");if(!e||!t)return;const n=t.textContent||"",r=document.createElement("input");r.className="thread-title-edit",r.value=n,t.replaceWith(r),r.focus(),r.select();const s=a=>{const l=document.createElement("span");l.className="thread-title",l.textContent=a,r.replaceWith(l)},o=async()=>{const a=r.value.trim();if(!a||a===n){s(n);return}try{const l=await ga(i,{title:a}),O=Se.find(c=>c.id===i);O&&(O.title=l.title),s(l.title)}catch(l){console.error("rename failed",l),s(n)}};r.addEventListener("blur",()=>{o()}),r.addEventListener("keydown",a=>{a.key==="Enter"?(a.preventDefault(),r.blur()):a.key==="Escape"&&(a.preventDefault(),s(n))})}async function ez(){if(await Qa(4),!Se.length){ve=null,er();return}try{const i=localStorage.getItem(WQ);i&&Se.some(e=>e.id===i)&&(ve=i)}catch{}ya()}window.__editorThreadNew=()=>{KZ()},window.__editorThreadSwitch=i=>{dd(i)},window.__editorThreadRename=i=>{JZ(i)};const tz={done:"DONE",awaiting_customer:"AWAITING YOU",wont_do:"WONT DO",blocked:"BLOCKED",feedback:"NEW FEEDBACK",idle:"IDLE",running:"RUNNING"};function hd(i){const e=tz[i]||i.toUpperCase();return`<span class="status-pill" data-status="${g(i)}">${g(e)}</span>`}function fd(i){if(!i)return"";let e;if(/^\d+Z$/.test(i)?e=new Date(parseInt(i,10)*1e3):e=new Date(i),isNaN(e.getTime()))return i;const t=n=>String(n).padStart(2,"0");return`${t(e.getDate())}/${t(e.getMonth()+1)}/${e.getFullYear()} ${t(e.getHours())}:${t(e.getMinutes())}`}let Jn=null;function ya(){const i=
|
|
988
|
+
</div>`}).join("")}let cd=!1;async function Qa(i=0){for(let e=0;;e++)try{Se=await NZ(),cd=!1,EQ();return}catch(t){if(e>=i){cd=!0,console.error("refreshThreadList failed",t);return}await new Promise(n=>setTimeout(n,250*2**e))}}function HZ(i){const e=document.getElementById("editor-ai-messages");if(!e)return;const t=rt.get(i)||[];if(!t.length){e.innerHTML='<div class="ai-msg ai-bot" style="opacity:0.6">Start the conversation…</div>';return}e.innerHTML="";for(const n of t){const r=document.createElement("div");r.className=`ai-msg ai-${n.role==="user"?"user":"bot"}`,n.role==="user"?r.textContent=n.content:r.innerHTML=fs(n.content),e.appendChild(r)}e.scrollTop=e.scrollHeight}async function AQ(i,e=!1){if(!(!e&&rt.has(i)))try{const t=await FZ(i);rt.set(i,t)}catch(t){console.error(`loadThreadMessages(${i}) failed`,t),rt.set(i,[])}}async function dd(i){if(i!==ve){ve=i;try{localStorage.setItem(WQ,i)}catch{}EQ(),await AQ(i),HZ(i)}}async function KZ(){try{const i=await cs();Se.push(i),rt.set(i.id,[]),await dd(i.id);const e=document.getElementById("editor-ai-input");e==null||e.focus()}catch(i){AZ(`<span style="color:var(--danger)">Couldn't create thread: ${g(String((i==null?void 0:i.message)||i))}</span>`,"bot")}}async function JZ(i){const e=document.querySelector(`.thread-row[data-thread-id="${CSS.escape(i)}"]`),t=e==null?void 0:e.querySelector(".thread-title");if(!e||!t)return;const n=t.textContent||"",r=document.createElement("input");r.className="thread-title-edit",r.value=n,t.replaceWith(r),r.focus(),r.select();const s=a=>{const l=document.createElement("span");l.className="thread-title",l.textContent=a,r.replaceWith(l)},o=async()=>{const a=r.value.trim();if(!a||a===n){s(n);return}try{const l=await ga(i,{title:a}),O=Se.find(c=>c.id===i);O&&(O.title=l.title),s(l.title)}catch(l){console.error("rename failed",l),s(n)}};r.addEventListener("blur",()=>{o()}),r.addEventListener("keydown",a=>{a.key==="Enter"?(a.preventDefault(),r.blur()):a.key==="Escape"&&(a.preventDefault(),s(n))})}async function ez(){if(await Qa(4),!Se.length){ve=null,er();return}try{const i=localStorage.getItem(WQ);i&&Se.some(e=>e.id===i)&&(ve=i)}catch{}ya()}window.__editorThreadNew=()=>{KZ()},window.__editorThreadSwitch=i=>{dd(i)},window.__editorThreadRename=i=>{JZ(i)};const tz={done:"DONE",awaiting_customer:"AWAITING YOU",wont_do:"WONT DO",blocked:"BLOCKED",feedback:"NEW FEEDBACK",idle:"IDLE",running:"RUNNING"};function hd(i){const e=tz[i]||i.toUpperCase();return`<span class="status-pill" data-status="${g(i)}">${g(e)}</span>`}function fd(i){if(!i)return"";let e;if(/^\d+Z$/.test(i)?e=new Date(parseInt(i,10)*1e3):e=new Date(i),isNaN(e.getTime()))return i;const t=n=>String(n).padStart(2,"0");return`${t(e.getDate())}/${t(e.getMonth()+1)}/${e.getFullYear()} ${t(e.getHours())}:${t(e.getMinutes())}`}let Jn=null;function ya(){const i=s=>document.getElementById(s),e=i("threads-pane-head-list");e&&(e.hidden=!1);const t=i("threads-pane-head-detail");t&&(t.hidden=!0);const n=i("threads-list-view");n&&(n.hidden=!1);const r=i("threads-detail-view");r&&(r.hidden=!0),Qa().then(er)}async function ba(i){await dd(i);const e=Se.find(c=>c.id===i);if(!e)return;const t=c=>document.getElementById(c),n=t("threads-pane-head-list");n&&(n.hidden=!0);const r=t("threads-pane-head-detail");r&&(r.hidden=!1);const s=t("threads-list-view");s&&(s.hidden=!0);const o=t("threads-detail-view");o&&(o.hidden=!1);const a=t("threads-detail-title");a&&(a.textContent=e.title||"Thread");const l=t("threads-detail-meta"),O=e.sender?`<span>📨 from ${g(e.sender)}</span>`:"";l&&(l.innerHTML=`${hd(e.status_hint||"idle")} <span>${g(fd(e.last_message_at))}</span> ${O}`),pd(i),setTimeout(()=>{var c;return(c=t("threads-reply-input"))==null?void 0:c.focus()},30)}async function iz(){if(!ve)return;const i=ve;try{await ga(i,{archived:!0,closure_reason:"done"});const e=Se.find(t=>t.id===i);e&&(e.archived=!0,e.closure_reason="done"),ve=null,ya()}catch(e){console.error("archive failed",e)}}async function nz(i){try{await ga(i,{archived:!0});const e=Se.find(t=>t.id===i);e&&(e.archived=!0),ve===i?(ve=null,ya()):er()}catch(e){console.error("archive-from-list failed",e)}}function er(){const i=document.getElementById("threads-rows");if(!i)return;if(!Se.length){i.innerHTML=cd?`<div class="threads-empty">
|
|
989
989
|
<div style="margin-bottom:0.6rem">Can't reach the agent — threads unavailable.</div>
|
|
990
990
|
<button type="button" class="action-pill"
|
|
991
991
|
onclick="window.__threadsRetry()">Retry</button>
|
|
@@ -1012,7 +1012,7 @@ ${t.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
|
|
|
1012
1012
|
<span>severity: ${g(e.severity||"-")}</span>
|
|
1013
1013
|
</div>
|
|
1014
1014
|
<div style="margin-top:0.3rem">${g(e.summary||"")}</div>
|
|
1015
|
-
</div>`}catch{return fs(i)}}async function az(){try{const i=await cs();Se.push(i),rt.set(i.id,[]),await ba(i.id)}catch(i){console.error("threadsNew failed",i)}}async function lz(){if(!ve)return;const i=Se.find(t=>t.id===ve);if(!i)return;const e=window.prompt("Rename thread:",i.title);if(!(e==null||e.trim()===""||e===i.title))try{const t=await ga(ve,{title:e.trim()});i.title=t.title;const n=document.getElementById("threads-detail-title");n&&(n.textContent=t.title),er()}catch(t){console.error("rename failed",t)}}function Oz(i){return i?i.status_hint==="done"||i.status_hint==="wont_do"||i.closure_reason==="done"||i.closure_reason==="wont_do":!1}async function md(){const i=document.getElementById("threads-reply-input");if(!i)return;let e=i.value.trim();if(!e)return;i.value="";const t=e.match(/^new topic:\s*(.*)$/is);if(t){const o=t[1].trim();try{const a=await cs(o.slice(0,80)||void 0);if(Se.push(a),rt.set(a.id,[]),await ba(a.id),!o)return;e=o}catch(a){console.error("New Topic spawn failed",a);return}}if(!t&&ve&&Oz(Se.find(o=>o.id===ve)))try{const o=await cs(e.slice(0,80)||void 0);Se.push(o),rt.set(o.id,[]),await ba(o.id)}catch(o){console.error("auto-new-thread on done reply failed",o);return}if(!ve)try{const o=await cs(e.slice(0,80));Se.push(o),rt.set(o.id,[]),ve=o.id}catch(o){console.error("auto-create failed",o);return}const n=ve,r=rt.get(n)||[];r.push({id:`local-${Date.now()}`,role:"user",content:e,timestamp:new Date().toISOString(),thread_id:n}),rt.set(n,r),pd(n),$a.add(n),er();const s=document.getElementById("threads-chat");Jn==null||Jn.abort(),Jn=new AbortController;try{await pz(e,n,Jn.signal,s)}catch(o){if((o==null?void 0:o.name)!=="AbortError"){const a=document.createElement("div");a.className="ai-msg ai-bot",a.style.color="var(--danger,#f38ba8)",a.textContent=`Connection failed: ${(o==null?void 0:o.message)||o}`,s==null||s.appendChild(a)}}finally{$a.delete(n),Jn=null;try{await AQ(n,!0)}catch{}await Qa(),pd(n);const o=Se.find(
|
|
1015
|
+
</div>`}catch{return fs(i)}}async function az(){try{const i=await cs();Se.push(i),rt.set(i.id,[]),await ba(i.id)}catch(i){console.error("threadsNew failed",i)}}async function lz(){if(!ve)return;const i=Se.find(t=>t.id===ve);if(!i)return;const e=window.prompt("Rename thread:",i.title);if(!(e==null||e.trim()===""||e===i.title))try{const t=await ga(ve,{title:e.trim()});i.title=t.title;const n=document.getElementById("threads-detail-title");n&&(n.textContent=t.title),er()}catch(t){console.error("rename failed",t)}}function Oz(i){return i?i.status_hint==="done"||i.status_hint==="wont_do"||i.closure_reason==="done"||i.closure_reason==="wont_do":!1}async function md(){const i=document.getElementById("threads-reply-input");if(!i)return;let e=i.value.trim();if(!e)return;i.value="";const t=e.match(/^new topic:\s*(.*)$/is);if(t){const o=t[1].trim();try{const a=await cs(o.slice(0,80)||void 0);if(Se.push(a),rt.set(a.id,[]),await ba(a.id),!o)return;e=o}catch(a){console.error("New Topic spawn failed",a);return}}if(!t&&ve&&Oz(Se.find(o=>o.id===ve)))try{const o=await cs(e.slice(0,80)||void 0);Se.push(o),rt.set(o.id,[]),await ba(o.id)}catch(o){console.error("auto-new-thread on done reply failed",o);return}if(!ve)try{const o=await cs(e.slice(0,80));Se.push(o),rt.set(o.id,[]),ve=o.id}catch(o){console.error("auto-create failed",o);return}const n=ve,r=rt.get(n)||[];r.push({id:`local-${Date.now()}`,role:"user",content:e,timestamp:new Date().toISOString(),thread_id:n}),rt.set(n,r),pd(n),$a.add(n),er();const s=document.getElementById("threads-chat");Jn==null||Jn.abort(),Jn=new AbortController;try{await pz(e,n,Jn.signal,s)}catch(o){if((o==null?void 0:o.name)!=="AbortError"){const a=document.createElement("div");a.className="ai-msg ai-bot",a.style.color="var(--danger,#f38ba8)",a.textContent=`Connection failed: ${(o==null?void 0:o.message)||o}`,s==null||s.appendChild(a)}}finally{$a.delete(n),Jn=null;try{await AQ(n,!0)}catch{}await Qa(),pd(n);const o=Se.find(l=>l.id===n),a=document.getElementById("threads-detail-meta");if(o&&a){const l=o.sender?`<span>📨 from ${g(o.sender)}</span>`:"";a.innerHTML=`${hd(o.status_hint||"idle")} <span>${g(fd(o.last_message_at))}</span> ${l}`}}}queueMicrotask(()=>{const i=document.getElementById("threads-reply-form");i&&i.addEventListener("submit",t=>{t.preventDefault(),md()});const e=document.getElementById("threads-reply-input");e&&e.addEventListener("keydown",t=>{t.key==="Enter"&&!t.shiftKey&&(t.preventDefault(),t.stopPropagation(),md())})}),window.__threadsShowList=()=>ya(),window.__threadsShowDetail=i=>{ba(i)},window.__threadsNew=()=>{az()},window.__threadsRetry=()=>{Qa(2).then(er)},window.__threadsRenameActive=()=>{lz()};let un=!1;async function jQ(){const i=document.getElementById("plans-panel"),e=document.getElementById("plans-toggle-btn");i&&(un=!un,i.hidden=!un,e==null||e.classList.toggle("active",un),un&&await cz())}async function cz(){const i=document.getElementById("plans-rows");if(i){i.innerHTML='<div class="threads-empty">Loading plans…</div>';try{const e=await Mi("plan_list",{}),t=e.ok&&Array.isArray(e.result)?e.result:[];if(!t.length){i.innerHTML='<div class="threads-empty">No plans yet — they appear here when the planner agent creates one.</div>';return}const n=[...t].sort((r,s)=>String(s.name||s.file||"").localeCompare(String(r.name||r.file||"")));i.innerHTML=n.map(r=>{var h,f;const s=String(r.name||r.file||""),o=String(r.path||`plan/${s}`),a=String(r.title||s).slice(0,60),l=r.steps_done??((h=r.progress)==null?void 0:h.done)??0,O=r.steps_total??((f=r.progress)==null?void 0:f.total)??0,c=O>0?`${l}/${O} steps`:"",d=r.is_current||r.current?" · ★ current":"";return`<div class="plan-row" onclick="window.__plansOpen('${g(o)}')" title="Open ${g(o)} in editor">
|
|
1016
1016
|
<div class="plan-name">${g(a)}</div>
|
|
1017
1017
|
<div class="plan-meta">${g(s)}${c?" · "+g(c):""}${d}</div>
|
|
1018
1018
|
</div>`}).join("")}catch{i.innerHTML='<div class="threads-empty" style="color:var(--danger,#f38ba8)">Failed to load plans</div>'}}}async function dz(i){var t;try{await ui(i)}catch(n){console.error("plansOpen failed",n)}un=!1;const e=document.getElementById("plans-panel");e&&(e.hidden=!0),(t=document.getElementById("plans-toggle-btn"))==null||t.classList.remove("active")}window.__plansToggle=()=>{jQ()},window.__plansOpen=i=>{dz(i)};let ds=!1;async function hz(){const i=document.getElementById("grounding-panel"),e=document.getElementById("grounding-toggle-btn");i&&(un&&jQ(),ds=!ds,i.hidden=!ds,e==null||e.classList.toggle("active",ds),ds&&await MQ())}async function MQ(){const i=document.getElementById("grounding-body");if(!i)return;i.innerHTML='<div class="threads-empty">Loading…</div>';let e={};try{const r=await fetch("/__dev/api/grounding/status");r.ok&&(e=await r.json())}catch{}const t=g(e.url||"https://mcp.tina4.com"),n=e.configured?`<span style="color:var(--success,#a6e3a1)">● Configured</span> <span style="opacity:0.6">(…${g(e.last4||"")})</span>`:'<span style="color:var(--warn,#f9e2af)">○ Not set</span> — using local corpus fallback';i.innerHTML=`
|
|
@@ -10,6 +10,37 @@ var SafeString = class {
|
|
|
10
10
|
return this.value;
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
|
+
var KNOWN_TAGS = /* @__PURE__ */ new Set([
|
|
14
|
+
"autoescape",
|
|
15
|
+
"block",
|
|
16
|
+
"cache",
|
|
17
|
+
"extends",
|
|
18
|
+
"for",
|
|
19
|
+
"from",
|
|
20
|
+
"if",
|
|
21
|
+
"import",
|
|
22
|
+
"include",
|
|
23
|
+
"live",
|
|
24
|
+
"macro",
|
|
25
|
+
"raw",
|
|
26
|
+
"set",
|
|
27
|
+
"spaceless"
|
|
28
|
+
]);
|
|
29
|
+
var TERMINATOR_TAGS = /* @__PURE__ */ new Set([
|
|
30
|
+
"elif",
|
|
31
|
+
"else",
|
|
32
|
+
"elseif",
|
|
33
|
+
"endautoescape",
|
|
34
|
+
"endblock",
|
|
35
|
+
"endcache",
|
|
36
|
+
"endfor",
|
|
37
|
+
"endif",
|
|
38
|
+
"endlive",
|
|
39
|
+
"endmacro",
|
|
40
|
+
"endraw",
|
|
41
|
+
"endset",
|
|
42
|
+
"endspaceless"
|
|
43
|
+
]);
|
|
13
44
|
function jsonText(value) {
|
|
14
45
|
try {
|
|
15
46
|
const text = JSON.stringify(value);
|
|
@@ -1668,8 +1699,11 @@ var Frond = class _Frond {
|
|
|
1668
1699
|
i = skip;
|
|
1669
1700
|
}
|
|
1670
1701
|
} else if (tag === "set") {
|
|
1702
|
+
const isBlockSet = !content.includes("=");
|
|
1671
1703
|
if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
|
|
1672
|
-
i
|
|
1704
|
+
i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
|
|
1705
|
+
} else if (isBlockSet) {
|
|
1706
|
+
i = this.handleSetBlock(tokens, i, context);
|
|
1673
1707
|
} else {
|
|
1674
1708
|
this.handleSet(content, context);
|
|
1675
1709
|
i++;
|
|
@@ -1711,6 +1745,11 @@ var Frond = class _Frond {
|
|
|
1711
1745
|
i++;
|
|
1712
1746
|
} else {
|
|
1713
1747
|
i++;
|
|
1748
|
+
if (tag !== "" && !TERMINATOR_TAGS.has(tag)) {
|
|
1749
|
+
throw new Error(
|
|
1750
|
+
`Frond: unknown tag "${tag}" -- known tags are: ${[...KNOWN_TAGS].sort().join(", ")}`
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1714
1753
|
}
|
|
1715
1754
|
if (stripA && i < tokens.length && tokens[i][0] === "TEXT") {
|
|
1716
1755
|
tokens[i] = ["TEXT", tokens[i][1].replace(LEADING_WS_RE, "")];
|
|
@@ -2607,6 +2646,47 @@ var Frond = class _Frond {
|
|
|
2607
2646
|
}
|
|
2608
2647
|
return html;
|
|
2609
2648
|
}
|
|
2649
|
+
/**
|
|
2650
|
+
* {% set name %}...{% endset %} -- render the body and bind it.
|
|
2651
|
+
*
|
|
2652
|
+
* Emits nothing itself. The captured value is a SafeString because it is
|
|
2653
|
+
* template output that has already been escaped on the way in; re-escaping it
|
|
2654
|
+
* at {{ name }} would double-encode every entity. Twig and Jinja2 both mark the
|
|
2655
|
+
* capture safe. Returns the index just past {% endset %}.
|
|
2656
|
+
*/
|
|
2657
|
+
handleSetBlock(tokens, start, context) {
|
|
2658
|
+
const [content] = stripTag(tokens[start][1]);
|
|
2659
|
+
const name = (content.split(/\s+/)[1] || "").trim();
|
|
2660
|
+
const bodyTokens = [];
|
|
2661
|
+
let i = start + 1;
|
|
2662
|
+
let depth = 0;
|
|
2663
|
+
while (i < tokens.length) {
|
|
2664
|
+
if (tokens[i][0] === "BLOCK") {
|
|
2665
|
+
const [tagContent] = stripTag(tokens[i][1]);
|
|
2666
|
+
const tag = tagContent.split(/\s+/)[0] || "";
|
|
2667
|
+
if (tag === "set" && !tagContent.includes("=")) {
|
|
2668
|
+
depth++;
|
|
2669
|
+
bodyTokens.push(tokens[i]);
|
|
2670
|
+
} else if (tag === "endset") {
|
|
2671
|
+
if (depth === 0) {
|
|
2672
|
+
i++;
|
|
2673
|
+
break;
|
|
2674
|
+
}
|
|
2675
|
+
depth--;
|
|
2676
|
+
bodyTokens.push(tokens[i]);
|
|
2677
|
+
} else {
|
|
2678
|
+
bodyTokens.push(tokens[i]);
|
|
2679
|
+
}
|
|
2680
|
+
} else {
|
|
2681
|
+
bodyTokens.push(tokens[i]);
|
|
2682
|
+
}
|
|
2683
|
+
i++;
|
|
2684
|
+
}
|
|
2685
|
+
if (name) {
|
|
2686
|
+
context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
|
|
2687
|
+
}
|
|
2688
|
+
return i;
|
|
2689
|
+
}
|
|
2610
2690
|
handleSpaceless(tokens, start, context) {
|
|
2611
2691
|
const bodyTokens = [];
|
|
2612
2692
|
let i = start + 1;
|
|
@@ -34,6 +34,31 @@ class SafeString {
|
|
|
34
34
|
toString() { return this.value; }
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Every tag that OPENS a construct.
|
|
39
|
+
*
|
|
40
|
+
* An unknown tag is a typo, and 3.13.89 makes it throw rather than render its
|
|
41
|
+
* body: a mistyped guard -- {% iff is_admin %} instead of {% if is_admin %} --
|
|
42
|
+
* used to render the gated content UNCONDITIONALLY, so a reviewer saw a guard
|
|
43
|
+
* that was not there. Twig and Jinja2 both raise on an unknown tag; Frond now
|
|
44
|
+
* does too. There is no user-extension point for tags in any of the four
|
|
45
|
+
* frameworks, so an unknown name is always a mistake, never a plugin.
|
|
46
|
+
*/
|
|
47
|
+
const KNOWN_TAGS = new Set([
|
|
48
|
+
"autoescape", "block", "cache", "extends", "for", "from", "if", "import",
|
|
49
|
+
"include", "live", "macro", "raw", "set", "spaceless",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Terminators and branch keywords. These reach the tag dispatch only when stray
|
|
54
|
+
* (their own collector consumes them in the normal case), and a stray one keeps
|
|
55
|
+
* the old render-nothing behaviour -- see the comment at the throw.
|
|
56
|
+
*/
|
|
57
|
+
const TERMINATOR_TAGS = new Set([
|
|
58
|
+
"elif", "else", "elseif", "endautoescape", "endblock", "endcache", "endfor",
|
|
59
|
+
"endif", "endlive", "endmacro", "endraw", "endset", "endspaceless",
|
|
60
|
+
]);
|
|
61
|
+
|
|
37
62
|
/**
|
|
38
63
|
* Serialize a value to compact JSON text that is always valid JSON.
|
|
39
64
|
*
|
|
@@ -2077,8 +2102,16 @@ export class Frond {
|
|
|
2077
2102
|
i = skip;
|
|
2078
2103
|
}
|
|
2079
2104
|
} else if (tag === "set") {
|
|
2105
|
+
// An assignment has an "="; without one this is the BLOCK form,
|
|
2106
|
+
// {% set name %}...{% endset %}, which captures its rendered body. A
|
|
2107
|
+
// bare includes() is exact here, not a shortcut: the block form's tag
|
|
2108
|
+
// content is only ever "set <name>", so an "=" anywhere -- even inside
|
|
2109
|
+
// a quoted value like {% set m = "a = b" %} -- means assignment.
|
|
2110
|
+
const isBlockSet = !content.includes("=");
|
|
2080
2111
|
if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
|
|
2081
|
-
i
|
|
2112
|
+
i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
|
|
2113
|
+
} else if (isBlockSet) {
|
|
2114
|
+
i = this.handleSetBlock(tokens, i, context);
|
|
2082
2115
|
} else {
|
|
2083
2116
|
this.handleSet(content, context);
|
|
2084
2117
|
i++;
|
|
@@ -2120,6 +2153,16 @@ export class Frond {
|
|
|
2120
2153
|
i++; // Already handled
|
|
2121
2154
|
} else {
|
|
2122
2155
|
i++;
|
|
2156
|
+
if (tag !== "" && !TERMINATOR_TAGS.has(tag)) {
|
|
2157
|
+
throw new Error(
|
|
2158
|
+
`Frond: unknown tag "${tag}" -- known tags are: ${[...KNOWN_TAGS].sort().join(", ")}`,
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
// An empty tag ({% %}) or a stray terminator (an {% endif %} with
|
|
2162
|
+
// no {% if %}): no output.
|
|
2163
|
+
// Malformed, but it has always rendered nothing, and nothing is the
|
|
2164
|
+
// safe answer -- unlike an unknown tag it cannot expose content that
|
|
2165
|
+
// was meant to be gated.
|
|
2123
2166
|
}
|
|
2124
2167
|
|
|
2125
2168
|
if (stripA && i < tokens.length && tokens[i][0] === "TEXT") {
|
|
@@ -3121,6 +3164,50 @@ export class Frond {
|
|
|
3121
3164
|
return html;
|
|
3122
3165
|
}
|
|
3123
3166
|
|
|
3167
|
+
/**
|
|
3168
|
+
* {% set name %}...{% endset %} -- render the body and bind it.
|
|
3169
|
+
*
|
|
3170
|
+
* Emits nothing itself. The captured value is a SafeString because it is
|
|
3171
|
+
* template output that has already been escaped on the way in; re-escaping it
|
|
3172
|
+
* at {{ name }} would double-encode every entity. Twig and Jinja2 both mark the
|
|
3173
|
+
* capture safe. Returns the index just past {% endset %}.
|
|
3174
|
+
*/
|
|
3175
|
+
private handleSetBlock(tokens: Token[], start: number, context: Record<string, unknown>): number {
|
|
3176
|
+
const [content] = stripTag(tokens[start][1]);
|
|
3177
|
+
const name = (content.split(/\s+/)[1] || "").trim();
|
|
3178
|
+
|
|
3179
|
+
const bodyTokens: Token[] = [];
|
|
3180
|
+
let i = start + 1;
|
|
3181
|
+
let depth = 0;
|
|
3182
|
+
while (i < tokens.length) {
|
|
3183
|
+
if (tokens[i][0] === "BLOCK") {
|
|
3184
|
+
const [tagContent] = stripTag(tokens[i][1]);
|
|
3185
|
+
const tag = tagContent.split(/\s+/)[0] || "";
|
|
3186
|
+
if (tag === "set" && !tagContent.includes("=")) {
|
|
3187
|
+
depth++;
|
|
3188
|
+
bodyTokens.push(tokens[i]);
|
|
3189
|
+
} else if (tag === "endset") {
|
|
3190
|
+
if (depth === 0) {
|
|
3191
|
+
i++;
|
|
3192
|
+
break;
|
|
3193
|
+
}
|
|
3194
|
+
depth--;
|
|
3195
|
+
bodyTokens.push(tokens[i]);
|
|
3196
|
+
} else {
|
|
3197
|
+
bodyTokens.push(tokens[i]);
|
|
3198
|
+
}
|
|
3199
|
+
} else {
|
|
3200
|
+
bodyTokens.push(tokens[i]);
|
|
3201
|
+
}
|
|
3202
|
+
i++;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
if (name) {
|
|
3206
|
+
context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
|
|
3207
|
+
}
|
|
3208
|
+
return i;
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3124
3211
|
private handleSpaceless(tokens: Token[], start: number, context: Record<string, unknown>): [string, number] {
|
|
3125
3212
|
const bodyTokens: Token[] = [];
|
|
3126
3213
|
let i = start + 1;
|
|
@@ -4428,7 +4428,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
4428
4428
|
function _generateFormTokenValue(descriptor = "") {
|
|
4429
4429
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
4430
4430
|
}
|
|
4431
|
-
var SafeString, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
4431
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
4432
4432
|
var init_engine = __esm({
|
|
4433
4433
|
"../frond/src/engine.ts"() {
|
|
4434
4434
|
"use strict";
|
|
@@ -4440,6 +4440,37 @@ var init_engine = __esm({
|
|
|
4440
4440
|
return this.value;
|
|
4441
4441
|
}
|
|
4442
4442
|
};
|
|
4443
|
+
KNOWN_TAGS = /* @__PURE__ */ new Set([
|
|
4444
|
+
"autoescape",
|
|
4445
|
+
"block",
|
|
4446
|
+
"cache",
|
|
4447
|
+
"extends",
|
|
4448
|
+
"for",
|
|
4449
|
+
"from",
|
|
4450
|
+
"if",
|
|
4451
|
+
"import",
|
|
4452
|
+
"include",
|
|
4453
|
+
"live",
|
|
4454
|
+
"macro",
|
|
4455
|
+
"raw",
|
|
4456
|
+
"set",
|
|
4457
|
+
"spaceless"
|
|
4458
|
+
]);
|
|
4459
|
+
TERMINATOR_TAGS = /* @__PURE__ */ new Set([
|
|
4460
|
+
"elif",
|
|
4461
|
+
"else",
|
|
4462
|
+
"elseif",
|
|
4463
|
+
"endautoescape",
|
|
4464
|
+
"endblock",
|
|
4465
|
+
"endcache",
|
|
4466
|
+
"endfor",
|
|
4467
|
+
"endif",
|
|
4468
|
+
"endlive",
|
|
4469
|
+
"endmacro",
|
|
4470
|
+
"endraw",
|
|
4471
|
+
"endset",
|
|
4472
|
+
"endspaceless"
|
|
4473
|
+
]);
|
|
4443
4474
|
JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
|
|
4444
4475
|
JSON_UNSAFE_MAP = {
|
|
4445
4476
|
"<": "\\u003c",
|
|
@@ -5094,8 +5125,11 @@ var init_engine = __esm({
|
|
|
5094
5125
|
i = skip;
|
|
5095
5126
|
}
|
|
5096
5127
|
} else if (tag === "set") {
|
|
5128
|
+
const isBlockSet = !content.includes("=");
|
|
5097
5129
|
if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
|
|
5098
|
-
i
|
|
5130
|
+
i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
|
|
5131
|
+
} else if (isBlockSet) {
|
|
5132
|
+
i = this.handleSetBlock(tokens, i, context);
|
|
5099
5133
|
} else {
|
|
5100
5134
|
this.handleSet(content, context);
|
|
5101
5135
|
i++;
|
|
@@ -5137,6 +5171,11 @@ var init_engine = __esm({
|
|
|
5137
5171
|
i++;
|
|
5138
5172
|
} else {
|
|
5139
5173
|
i++;
|
|
5174
|
+
if (tag !== "" && !TERMINATOR_TAGS.has(tag)) {
|
|
5175
|
+
throw new Error(
|
|
5176
|
+
`Frond: unknown tag "${tag}" -- known tags are: ${[...KNOWN_TAGS].sort().join(", ")}`
|
|
5177
|
+
);
|
|
5178
|
+
}
|
|
5140
5179
|
}
|
|
5141
5180
|
if (stripA && i < tokens.length && tokens[i][0] === "TEXT") {
|
|
5142
5181
|
tokens[i] = ["TEXT", tokens[i][1].replace(LEADING_WS_RE, "")];
|
|
@@ -6033,6 +6072,47 @@ var init_engine = __esm({
|
|
|
6033
6072
|
}
|
|
6034
6073
|
return html;
|
|
6035
6074
|
}
|
|
6075
|
+
/**
|
|
6076
|
+
* {% set name %}...{% endset %} -- render the body and bind it.
|
|
6077
|
+
*
|
|
6078
|
+
* Emits nothing itself. The captured value is a SafeString because it is
|
|
6079
|
+
* template output that has already been escaped on the way in; re-escaping it
|
|
6080
|
+
* at {{ name }} would double-encode every entity. Twig and Jinja2 both mark the
|
|
6081
|
+
* capture safe. Returns the index just past {% endset %}.
|
|
6082
|
+
*/
|
|
6083
|
+
handleSetBlock(tokens, start2, context) {
|
|
6084
|
+
const [content] = stripTag(tokens[start2][1]);
|
|
6085
|
+
const name = (content.split(/\s+/)[1] || "").trim();
|
|
6086
|
+
const bodyTokens = [];
|
|
6087
|
+
let i = start2 + 1;
|
|
6088
|
+
let depth = 0;
|
|
6089
|
+
while (i < tokens.length) {
|
|
6090
|
+
if (tokens[i][0] === "BLOCK") {
|
|
6091
|
+
const [tagContent] = stripTag(tokens[i][1]);
|
|
6092
|
+
const tag = tagContent.split(/\s+/)[0] || "";
|
|
6093
|
+
if (tag === "set" && !tagContent.includes("=")) {
|
|
6094
|
+
depth++;
|
|
6095
|
+
bodyTokens.push(tokens[i]);
|
|
6096
|
+
} else if (tag === "endset") {
|
|
6097
|
+
if (depth === 0) {
|
|
6098
|
+
i++;
|
|
6099
|
+
break;
|
|
6100
|
+
}
|
|
6101
|
+
depth--;
|
|
6102
|
+
bodyTokens.push(tokens[i]);
|
|
6103
|
+
} else {
|
|
6104
|
+
bodyTokens.push(tokens[i]);
|
|
6105
|
+
}
|
|
6106
|
+
} else {
|
|
6107
|
+
bodyTokens.push(tokens[i]);
|
|
6108
|
+
}
|
|
6109
|
+
i++;
|
|
6110
|
+
}
|
|
6111
|
+
if (name) {
|
|
6112
|
+
context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
|
|
6113
|
+
}
|
|
6114
|
+
return i;
|
|
6115
|
+
}
|
|
6036
6116
|
handleSpaceless(tokens, start2, context) {
|
|
6037
6117
|
const bodyTokens = [];
|
|
6038
6118
|
let i = start2 + 1;
|