tina4-nodejs 3.13.88 → 3.13.89
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 +82 -2
- package/packages/core/dist/index.js +82 -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.89)
|
|
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.89 - 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,916 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;
|
|
@@ -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;
|
|
@@ -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;
|