tina4-nodejs 3.0.0-rc.2 → 3.1.0
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/BENCHMARK_REPORT.md +248 -86
- package/CARBONAH.md +4 -4
- package/CLAUDE.md +16 -1
- package/COMPARISON.md +58 -46
- package/README.md +60 -6
- package/package.json +2 -1
- package/packages/cli/src/bin.ts +8 -0
- package/packages/cli/src/commands/generate.ts +237 -0
- package/packages/core/gallery/queue/meta.json +1 -1
- package/packages/core/gallery/queue/src/lib/queueDb.ts +32 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +28 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +28 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +20 -10
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +25 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +36 -6
- package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +160 -0
- package/packages/core/src/cache.ts +402 -10
- package/packages/core/src/index.ts +5 -2
- package/packages/core/src/messenger.ts +118 -36
- package/packages/core/src/queue.ts +172 -92
- package/packages/core/src/response.ts +46 -0
- package/packages/core/src/router.ts +94 -1
- package/packages/core/src/server.ts +66 -7
- package/packages/core/src/types.ts +20 -1
- package/packages/core/src/websocketConnection.ts +16 -0
- package/packages/frond/src/engine.ts +184 -6
- package/packages/orm/src/baseModel.ts +274 -20
- package/packages/orm/src/cachedDatabase.ts +180 -0
- package/packages/orm/src/index.ts +4 -0
- package/packages/orm/src/model.ts +1 -0
- package/packages/orm/src/types.ts +75 -0
|
@@ -3,6 +3,8 @@ import { resolve, dirname, join, relative } from "node:path";
|
|
|
3
3
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
4
|
import { isatty } from "node:tty";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import cluster from "node:cluster";
|
|
7
|
+
import os from "node:os";
|
|
6
8
|
import type { Tina4Config, Tina4Request, Tina4Response } from "./types.js";
|
|
7
9
|
import { Router, defaultRouter, runRouteMiddlewares } from "./router.js";
|
|
8
10
|
import { discoverRoutes } from "./routeDiscovery.js";
|
|
@@ -280,14 +282,68 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
280
282
|
router: Router;
|
|
281
283
|
port: number;
|
|
282
284
|
}> {
|
|
285
|
+
// Load .env early so TINA4_DEBUG is available for cluster decision
|
|
286
|
+
loadEnv();
|
|
287
|
+
|
|
283
288
|
const { port, host } = resolvePortAndHost(config);
|
|
284
|
-
const routesDir = resolve(config?.routesDir ?? "src/routes");
|
|
285
|
-
const modelsDir = resolve(config?.modelsDir ?? "src/models");
|
|
286
|
-
const staticDir = resolve(config?.staticDir ?? "public");
|
|
287
|
-
const templatesDir = resolve(config?.templatesDir ?? "src/templates");
|
|
288
289
|
|
|
289
|
-
//
|
|
290
|
-
|
|
290
|
+
// Cluster mode for production: fork workers based on CPU count
|
|
291
|
+
// Only when not in dev mode and running as primary process
|
|
292
|
+
if (cluster.isPrimary && !isDevMode()) {
|
|
293
|
+
const numCPUs = os.cpus().length;
|
|
294
|
+
if (numCPUs > 1) {
|
|
295
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
296
|
+
const isTty = isatty(1);
|
|
297
|
+
const color = isTty ? "\x1b[32m" : "";
|
|
298
|
+
const reset = isTty ? "\x1b[0m" : "";
|
|
299
|
+
const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
|
|
300
|
+
|
|
301
|
+
console.log(`${color}
|
|
302
|
+
______ _ __ __
|
|
303
|
+
/_ __/(_)___ ____ _/ // /
|
|
304
|
+
/ / / / __ \\/ __ \`/ // /_
|
|
305
|
+
/ / / / / / / /_/ /__ __/
|
|
306
|
+
/_/ /_/_/ /_/\\__,_/ /_/
|
|
307
|
+
${reset}
|
|
308
|
+
Tina4 Node.js v${TINA4_VERSION} — This is not a framework
|
|
309
|
+
|
|
310
|
+
Server: http://${displayHost}:${port} (cluster, ${numCPUs} workers)
|
|
311
|
+
Swagger: http://localhost:${port}/swagger
|
|
312
|
+
Dashboard: http://localhost:${port}/__dev
|
|
313
|
+
Debug: OFF (Log level: ${logLevel})
|
|
314
|
+
`);
|
|
315
|
+
|
|
316
|
+
for (let i = 0; i < numCPUs; i++) {
|
|
317
|
+
cluster.fork();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
cluster.on("exit", (worker, code, _signal) => {
|
|
321
|
+
if (code !== 0) {
|
|
322
|
+
console.log(` Worker ${worker.process.pid} exited (code ${code}), restarting...`);
|
|
323
|
+
cluster.fork();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Return a handle that kills all workers
|
|
328
|
+
return {
|
|
329
|
+
close: () => {
|
|
330
|
+
for (const id in cluster.workers) {
|
|
331
|
+
cluster.workers[id]?.kill();
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
router: new Router(),
|
|
335
|
+
port,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const base = config?.basePath ? resolve(config.basePath) : process.cwd();
|
|
341
|
+
const routesDir = resolve(base, config?.routesDir ?? "src/routes");
|
|
342
|
+
const modelsDir = resolve(base, config?.modelsDir ?? "src/models");
|
|
343
|
+
const staticDir = resolve(base, config?.staticDir ?? "public");
|
|
344
|
+
const templatesDir = resolve(base, config?.templatesDir ?? "src/templates");
|
|
345
|
+
|
|
346
|
+
// .env already loaded above for cluster decision
|
|
291
347
|
|
|
292
348
|
const router = new Router();
|
|
293
349
|
const middleware = new MiddlewareChain();
|
|
@@ -581,6 +637,9 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
581
637
|
const color = isTty ? "\x1b[32m" : "";
|
|
582
638
|
const reset = isTty ? "\x1b[0m" : "";
|
|
583
639
|
|
|
640
|
+
// Determine server mode label
|
|
641
|
+
const serverMode = isDebug ? "single" : (cluster.isWorker ? "cluster-worker" : "single");
|
|
642
|
+
|
|
584
643
|
// Banner goes to stdout via console.log — NOT through the framework logger
|
|
585
644
|
console.log(`${color}
|
|
586
645
|
______ _ __ __
|
|
@@ -591,7 +650,7 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
591
650
|
${reset}
|
|
592
651
|
Tina4 Node.js v${TINA4_VERSION} — This is not a framework
|
|
593
652
|
|
|
594
|
-
Server: http://${displayHost}:${port}
|
|
653
|
+
Server: http://${displayHost}:${port} (${serverMode})
|
|
595
654
|
Swagger: http://localhost:${port}/swagger
|
|
596
655
|
Dashboard: http://localhost:${port}/__dev
|
|
597
656
|
Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})
|
|
@@ -36,7 +36,8 @@ export interface Tina4ResponseMethods {
|
|
|
36
36
|
redirect(url: string, code?: number): Tina4Response;
|
|
37
37
|
cookie(name: string, value: string, options?: CookieOptions): Tina4Response;
|
|
38
38
|
clearCookie(name: string, options?: CookieOptions): Tina4Response;
|
|
39
|
-
|
|
39
|
+
file(path: string, options?: { download?: boolean; contentType?: string }): Tina4Response;
|
|
40
|
+
render(template: string, data?: Record<string, unknown>): Promise<Tina4Response>;
|
|
40
41
|
template(name: string, data?: Record<string, unknown>): Promise<Tina4Response>;
|
|
41
42
|
/** The underlying ServerResponse for advanced use */
|
|
42
43
|
raw: ServerResponse;
|
|
@@ -80,6 +81,9 @@ export interface RouteMeta {
|
|
|
80
81
|
export interface Tina4Config {
|
|
81
82
|
port?: number;
|
|
82
83
|
host?: string;
|
|
84
|
+
/** Base directory for the project. When set, routesDir, modelsDir, templatesDir,
|
|
85
|
+
* and staticDir are resolved relative to this path instead of process.cwd(). */
|
|
86
|
+
basePath?: string;
|
|
83
87
|
routesDir?: string;
|
|
84
88
|
modelsDir?: string;
|
|
85
89
|
templatesDir?: string;
|
|
@@ -96,3 +100,18 @@ export type Middleware = (
|
|
|
96
100
|
res: Tina4Response,
|
|
97
101
|
next: () => void
|
|
98
102
|
) => void | Promise<void>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Handler for WebSocket routes.
|
|
106
|
+
* conn — a connection-like object with send/close methods.
|
|
107
|
+
* message — the incoming text or binary message.
|
|
108
|
+
*/
|
|
109
|
+
export type WebSocketRouteHandler = (
|
|
110
|
+
conn: { id: string; send: (data: string) => void; close: () => void },
|
|
111
|
+
message: string | Buffer,
|
|
112
|
+
) => void | Promise<void>;
|
|
113
|
+
|
|
114
|
+
export interface WebSocketRouteDefinition {
|
|
115
|
+
pattern: string;
|
|
116
|
+
handler: WebSocketRouteHandler;
|
|
117
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a WebSocket connection with send/broadcast/close capabilities.
|
|
3
|
+
* Used by Router.websocket() route handlers.
|
|
4
|
+
*/
|
|
5
|
+
export interface WebSocketConnection {
|
|
6
|
+
/** Unique connection identifier */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Send a message to this connection */
|
|
9
|
+
send(message: string): void;
|
|
10
|
+
/** Broadcast a message to all other connections on the same path */
|
|
11
|
+
broadcast(message: string): void;
|
|
12
|
+
/** Close this connection */
|
|
13
|
+
close(): void;
|
|
14
|
+
/** The WebSocket route path this connection is on */
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* extends/block, include, macro, set, comments, whitespace control, tests.
|
|
6
6
|
*/
|
|
7
7
|
import { createHash, createHmac } from "node:crypto";
|
|
8
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
9
9
|
import { join, resolve } from "node:path";
|
|
10
10
|
|
|
11
11
|
// ── Types ──────────────────────────────────────────────────────
|
|
@@ -183,6 +183,13 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// Jinja2-style inline if: value if condition else other_value
|
|
187
|
+
const inlineIfMatch = expr.match(/^(.+?)\s+if\s+(.+?)\s+else\s+(.+)$/);
|
|
188
|
+
if (inlineIfMatch) {
|
|
189
|
+
const cond = evalExpr(inlineIfMatch[2], context);
|
|
190
|
+
return cond ? evalExpr(inlineIfMatch[1], context) : evalExpr(inlineIfMatch[3], context);
|
|
191
|
+
}
|
|
192
|
+
|
|
186
193
|
// Null coalescing: value ?? "default"
|
|
187
194
|
const qqIdx = expr.indexOf("??");
|
|
188
195
|
if (qqIdx !== -1) {
|
|
@@ -825,6 +832,11 @@ export class Frond {
|
|
|
825
832
|
private _allowedTags: Set<string> | null;
|
|
826
833
|
private _allowedVars: Set<string> | null;
|
|
827
834
|
private fragmentCache: Map<string, [string, number]>;
|
|
835
|
+
private _autoEscape: boolean;
|
|
836
|
+
/** Token pre-compilation cache for file templates */
|
|
837
|
+
private compiled = new Map<string, { tokens: Token[]; mtime: number }>();
|
|
838
|
+
/** Token pre-compilation cache for string templates */
|
|
839
|
+
private compiledStrings = new Map<string, Token[]>();
|
|
828
840
|
|
|
829
841
|
constructor(templateDir: string = "src/templates") {
|
|
830
842
|
this.templateDir = resolve(templateDir);
|
|
@@ -836,6 +848,7 @@ export class Frond {
|
|
|
836
848
|
this._allowedTags = null;
|
|
837
849
|
this._allowedVars = null;
|
|
838
850
|
this.fragmentCache = new Map();
|
|
851
|
+
this._autoEscape = true;
|
|
839
852
|
|
|
840
853
|
// Built-in global functions
|
|
841
854
|
this.globals.formToken = (descriptor?: string) => _generateFormToken(descriptor || "");
|
|
@@ -872,13 +885,54 @@ export class Frond {
|
|
|
872
885
|
|
|
873
886
|
render(template: string, data?: Record<string, unknown>): string {
|
|
874
887
|
const context = { ...this.globals, ...(data || {}) };
|
|
875
|
-
const
|
|
876
|
-
|
|
888
|
+
const filePath = join(this.templateDir, template);
|
|
889
|
+
|
|
890
|
+
if (!existsSync(filePath)) {
|
|
891
|
+
throw new Error(`Template not found: ${filePath}`);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const debugMode = (process.env.TINA4_DEBUG || "").toLowerCase() === "true";
|
|
895
|
+
const cached = this.compiled.get(template);
|
|
896
|
+
|
|
897
|
+
if (cached) {
|
|
898
|
+
if (debugMode) {
|
|
899
|
+
// Dev mode: check if file changed
|
|
900
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
901
|
+
if (cached.mtime === mtime) {
|
|
902
|
+
return this.executeCached(cached.tokens, context);
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
// Production: skip mtime check, cache is permanent
|
|
906
|
+
return this.executeCached(cached.tokens, context);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Cache miss — load, tokenize, cache
|
|
911
|
+
const source = readFileSync(filePath, "utf-8");
|
|
912
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
913
|
+
const tokens = tokenize(source);
|
|
914
|
+
this.compiled.set(template, { tokens, mtime });
|
|
915
|
+
return this.executeWithSource(source, tokens, context);
|
|
877
916
|
}
|
|
878
917
|
|
|
879
918
|
renderString(source: string, data?: Record<string, unknown>): string {
|
|
880
919
|
const context = { ...this.globals, ...(data || {}) };
|
|
881
|
-
|
|
920
|
+
|
|
921
|
+
const key = createHash("md5").update(source).digest("hex");
|
|
922
|
+
const cachedTokens = this.compiledStrings.get(key);
|
|
923
|
+
if (cachedTokens) {
|
|
924
|
+
return this.executeCached(cachedTokens, context);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const tokens = tokenize(source);
|
|
928
|
+
this.compiledStrings.set(key, tokens);
|
|
929
|
+
return this.executeCached(tokens, context);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** Clear all compiled template caches. */
|
|
933
|
+
clearCache(): void {
|
|
934
|
+
this.compiled.clear();
|
|
935
|
+
this.compiledStrings.clear();
|
|
882
936
|
}
|
|
883
937
|
|
|
884
938
|
private load(name: string): string {
|
|
@@ -889,6 +943,48 @@ export class Frond {
|
|
|
889
943
|
return readFileSync(filePath, "utf-8");
|
|
890
944
|
}
|
|
891
945
|
|
|
946
|
+
/** Execute pre-tokenized template against context. */
|
|
947
|
+
private executeCached(tokens: Token[], context: Record<string, unknown>): string {
|
|
948
|
+
if (Object.keys(this.tests).length > 0) {
|
|
949
|
+
context.__frond_tests__ = this.tests;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Check if first non-text token is an extends block
|
|
953
|
+
for (const [ttype, raw] of tokens) {
|
|
954
|
+
if (ttype === "TEXT") {
|
|
955
|
+
if (raw.trim()) break;
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (ttype === "BLOCK") {
|
|
959
|
+
const [content] = stripTag(raw);
|
|
960
|
+
if (content.startsWith("extends ")) {
|
|
961
|
+
// Extends requires source-based execution for block extraction
|
|
962
|
+
const source = tokens.map(([, v]) => v).join("");
|
|
963
|
+
return this.execute(source, context);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
return this.renderTokens(tokens, context);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Execute with both source and pre-tokenized tokens available. */
|
|
972
|
+
private executeWithSource(source: string, tokens: Token[], context: Record<string, unknown>): string {
|
|
973
|
+
if (Object.keys(this.tests).length > 0) {
|
|
974
|
+
context.__frond_tests__ = this.tests;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const extendsMatch = source.match(/\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/);
|
|
978
|
+
if (extendsMatch) {
|
|
979
|
+
const parentName = extendsMatch[1];
|
|
980
|
+
const parentSource = this.load(parentName);
|
|
981
|
+
const childBlocks = this.extractBlocks(source);
|
|
982
|
+
return this.renderWithBlocks(parentSource, context, childBlocks);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return this.renderTokens(tokens, context);
|
|
986
|
+
}
|
|
987
|
+
|
|
892
988
|
private execute(source: string, context: Record<string, unknown>): string {
|
|
893
989
|
// Inject custom tests into context for evalTest to find
|
|
894
990
|
if (Object.keys(this.tests).length > 0) {
|
|
@@ -1015,6 +1111,14 @@ export class Frond {
|
|
|
1015
1111
|
const [result, skip] = this.handleCache(tokens, i, context);
|
|
1016
1112
|
output.push(result);
|
|
1017
1113
|
i = skip;
|
|
1114
|
+
} else if (tag === "spaceless") {
|
|
1115
|
+
const [result, skip] = this.handleSpaceless(tokens, i, context);
|
|
1116
|
+
output.push(result);
|
|
1117
|
+
i = skip;
|
|
1118
|
+
} else if (tag === "autoescape") {
|
|
1119
|
+
const [result, skip] = this.handleAutoescape(tokens, i, context);
|
|
1120
|
+
output.push(result);
|
|
1121
|
+
i = skip;
|
|
1018
1122
|
} else if (tag === "block" || tag === "endblock" || tag === "extends") {
|
|
1019
1123
|
i++; // Already handled
|
|
1020
1124
|
} else {
|
|
@@ -1092,8 +1196,8 @@ export class Frond {
|
|
|
1092
1196
|
return value.value;
|
|
1093
1197
|
}
|
|
1094
1198
|
|
|
1095
|
-
// Auto-escape HTML unless marked safe
|
|
1096
|
-
if (!isSafe && typeof value === "string") {
|
|
1199
|
+
// Auto-escape HTML unless marked safe or auto-escape is disabled
|
|
1200
|
+
if (!isSafe && this._autoEscape && typeof value === "string") {
|
|
1097
1201
|
value = htmlEscape(value);
|
|
1098
1202
|
}
|
|
1099
1203
|
|
|
@@ -1472,4 +1576,78 @@ export class Frond {
|
|
|
1472
1576
|
this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1000]);
|
|
1473
1577
|
return [rendered, i];
|
|
1474
1578
|
}
|
|
1579
|
+
|
|
1580
|
+
private handleSpaceless(tokens: Token[], start: number, context: Record<string, unknown>): [string, number] {
|
|
1581
|
+
const bodyTokens: Token[] = [];
|
|
1582
|
+
let i = start + 1;
|
|
1583
|
+
let depth = 0;
|
|
1584
|
+
while (i < tokens.length) {
|
|
1585
|
+
if (tokens[i][0] === "BLOCK") {
|
|
1586
|
+
const [tagContent] = stripTag(tokens[i][1]);
|
|
1587
|
+
const tag = tagContent.split(/\s+/)[0] || "";
|
|
1588
|
+
if (tag === "spaceless") {
|
|
1589
|
+
depth++;
|
|
1590
|
+
bodyTokens.push(tokens[i]);
|
|
1591
|
+
} else if (tag === "endspaceless") {
|
|
1592
|
+
if (depth === 0) {
|
|
1593
|
+
i++;
|
|
1594
|
+
break;
|
|
1595
|
+
}
|
|
1596
|
+
depth--;
|
|
1597
|
+
bodyTokens.push(tokens[i]);
|
|
1598
|
+
} else {
|
|
1599
|
+
bodyTokens.push(tokens[i]);
|
|
1600
|
+
}
|
|
1601
|
+
} else {
|
|
1602
|
+
bodyTokens.push(tokens[i]);
|
|
1603
|
+
}
|
|
1604
|
+
i++;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
let rendered = this.renderTokens([...bodyTokens], context);
|
|
1608
|
+
rendered = rendered.replace(/>\s+</g, "><");
|
|
1609
|
+
return [rendered, i];
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
private handleAutoescape(tokens: Token[], start: number, context: Record<string, unknown>): [string, number] {
|
|
1613
|
+
const [content] = stripTag(tokens[start][1]);
|
|
1614
|
+
const modeMatch = content.match(/^autoescape\s+(false|true)/);
|
|
1615
|
+
const autoEscapeOn = !(modeMatch && modeMatch[1] === "false");
|
|
1616
|
+
|
|
1617
|
+
const bodyTokens: Token[] = [];
|
|
1618
|
+
let i = start + 1;
|
|
1619
|
+
let depth = 0;
|
|
1620
|
+
while (i < tokens.length) {
|
|
1621
|
+
if (tokens[i][0] === "BLOCK") {
|
|
1622
|
+
const [tagContent] = stripTag(tokens[i][1]);
|
|
1623
|
+
const tag = tagContent.split(/\s+/)[0] || "";
|
|
1624
|
+
if (tag === "autoescape") {
|
|
1625
|
+
depth++;
|
|
1626
|
+
bodyTokens.push(tokens[i]);
|
|
1627
|
+
} else if (tag === "endautoescape") {
|
|
1628
|
+
if (depth === 0) {
|
|
1629
|
+
i++;
|
|
1630
|
+
break;
|
|
1631
|
+
}
|
|
1632
|
+
depth--;
|
|
1633
|
+
bodyTokens.push(tokens[i]);
|
|
1634
|
+
} else {
|
|
1635
|
+
bodyTokens.push(tokens[i]);
|
|
1636
|
+
}
|
|
1637
|
+
} else {
|
|
1638
|
+
bodyTokens.push(tokens[i]);
|
|
1639
|
+
}
|
|
1640
|
+
i++;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (!autoEscapeOn) {
|
|
1644
|
+
const oldAutoEscape = this._autoEscape;
|
|
1645
|
+
this._autoEscape = false;
|
|
1646
|
+
const rendered = this.renderTokens([...bodyTokens], context);
|
|
1647
|
+
this._autoEscape = oldAutoEscape;
|
|
1648
|
+
return [rendered, i];
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
return [this.renderTokens([...bodyTokens], context), i];
|
|
1652
|
+
}
|
|
1475
1653
|
}
|