tina4-nodejs 3.13.101 → 3.13.103
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 +2 -2
- package/package.json +2 -2
- package/packages/cli/dist/bin.js +117 -112
- package/packages/core/dist/index.js +117 -112
- package/packages/core/public/js/tina4-dev-admin.min.js +2 -2
- package/packages/core/src/metrics.ts +1 -1
- package/packages/frond/dist/index.js +113 -109
- package/packages/frond/src/engine.ts +128 -130
- package/packages/orm/dist/index.js +117 -112
- package/types/frond/src/engine.d.ts +4 -2
|
@@ -342,8 +342,8 @@ export const pathParseCache = new Map<string, [string[], boolean[]]>();
|
|
|
342
342
|
export const TEMPLATE_CACHE_MAX = 256;
|
|
343
343
|
|
|
344
344
|
/**
|
|
345
|
-
* Hard cap on every per-expression memo cache — `filterChainCache
|
|
346
|
-
* `pathParseCache` (ADR-0004, parity with PHP's MEMO_CACHE_MAX and the
|
|
345
|
+
* Hard cap on every per-expression memo cache — `filterChainCache`,
|
|
346
|
+
* `pathParseCache`, and `expressionFormCache` (ADR-0004, parity with PHP's MEMO_CACHE_MAX and the
|
|
347
347
|
* Python master's `@lru_cache(maxsize=1024)` on the equivalent module-level
|
|
348
348
|
* parsers). Deliberately higher than TEMPLATE_CACHE_MAX: one entry here is a
|
|
349
349
|
* small parsed-path array, orders of magnitude smaller than a token list.
|
|
@@ -750,76 +750,71 @@ function splitOutsideQuotes(expr: string, sep: string): string[] {
|
|
|
750
750
|
return parts;
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
-
|
|
754
|
-
expr = expr.trim();
|
|
753
|
+
const EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
|
|
755
754
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
755
|
+
type ExprResult = unknown | typeof EXPR_NOT_MATCHED;
|
|
756
|
+
|
|
757
|
+
function parenthesizedInner(expr: string): string | null {
|
|
758
|
+
if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
|
|
759
|
+
let depth = 0;
|
|
760
|
+
for (let index = 0; index < expr.length; index++) {
|
|
761
|
+
if (expr[index] === "(") depth++;
|
|
762
|
+
else if (expr[index] === ")") depth--;
|
|
763
|
+
if (depth === 0 && index < expr.length - 1) return null;
|
|
763
764
|
}
|
|
765
|
+
return expr.slice(1, -1);
|
|
766
|
+
}
|
|
764
767
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if (expr[pi] === "(") depth++;
|
|
771
|
-
else if (expr[pi] === ")") depth--;
|
|
772
|
-
if (depth === 0 && pi < expr.length - 1) {
|
|
773
|
-
matched = false;
|
|
774
|
-
break;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
if (matched) {
|
|
778
|
-
return evalExpr(expr.slice(1, -1), context);
|
|
779
|
-
}
|
|
768
|
+
function evalPrimary(expr: string, context: Record<string, unknown>): ExprResult {
|
|
769
|
+
const quote = expr[0];
|
|
770
|
+
if (expr.length >= 2 && (quote === '"' || quote === "'") &&
|
|
771
|
+
expr.endsWith(quote) && !expr.slice(1, -1).includes(quote)) {
|
|
772
|
+
return expr.slice(1, -1);
|
|
780
773
|
}
|
|
774
|
+
const inner = parenthesizedInner(expr);
|
|
775
|
+
if (inner !== null) return evalExpr(inner, context);
|
|
776
|
+
return EXPR_NOT_MATCHED;
|
|
777
|
+
}
|
|
781
778
|
|
|
782
|
-
|
|
783
|
-
// Match carefully to handle nested ternaries
|
|
779
|
+
function evalTernaryExpression(expr: string, context: Record<string, unknown>): ExprResult {
|
|
784
780
|
const ternaryIdx = findTernary(expr);
|
|
785
|
-
if (ternaryIdx
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
return cond ? evalExpr(truePart, context) : evalExpr(falsePart, context);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
781
|
+
if (ternaryIdx === -1) return EXPR_NOT_MATCHED;
|
|
782
|
+
const rest = expr.slice(ternaryIdx + 1);
|
|
783
|
+
const colonIdx = findColon(rest);
|
|
784
|
+
if (colonIdx === -1) return EXPR_NOT_MATCHED;
|
|
785
|
+
const condition = evalExpr(expr.slice(0, ternaryIdx).trim(), context);
|
|
786
|
+
const branch = condition ? rest.slice(0, colonIdx) : rest.slice(colonIdx + 1);
|
|
787
|
+
return evalExpr(branch.trim(), context);
|
|
788
|
+
}
|
|
796
789
|
|
|
797
|
-
|
|
790
|
+
function evalInlineIfExpression(expr: string, context: Record<string, unknown>): ExprResult {
|
|
798
791
|
const ifIdx = findOutsideQuotes(expr, " if ");
|
|
799
|
-
if (ifIdx
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
return cond ? evalExpr(valuePart, context) : evalExpr(elsePart, context);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
792
|
+
if (ifIdx < 0) return EXPR_NOT_MATCHED;
|
|
793
|
+
const elseIdx = findOutsideQuotes(expr, " else ");
|
|
794
|
+
if (elseIdx < 0 || elseIdx <= ifIdx) return EXPR_NOT_MATCHED;
|
|
795
|
+
const condition = evalExpr(expr.slice(ifIdx + 4, elseIdx).trim(), context);
|
|
796
|
+
const branch = condition ? expr.slice(0, ifIdx) : expr.slice(elseIdx + 6);
|
|
797
|
+
return evalExpr(branch.trim(), context);
|
|
798
|
+
}
|
|
809
799
|
|
|
810
|
-
|
|
800
|
+
function evalCoalesceExpression(expr: string, context: Record<string, unknown>): ExprResult {
|
|
811
801
|
const qqIdx = findOutsideQuotes(expr, "??");
|
|
812
|
-
if (qqIdx
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
802
|
+
if (qqIdx === -1) return EXPR_NOT_MATCHED;
|
|
803
|
+
const value = evalExpr(expr.slice(0, qqIdx).trim(), context);
|
|
804
|
+
return value === null || value === undefined
|
|
805
|
+
? evalExpr(expr.slice(qqIdx + 2).trim(), context)
|
|
806
|
+
: value;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function evalConditional(expr: string, context: Record<string, unknown>): ExprResult {
|
|
810
|
+
for (const evaluator of [evalTernaryExpression, evalInlineIfExpression, evalCoalesceExpression]) {
|
|
811
|
+
const result = evaluator(expr, context);
|
|
812
|
+
if (result !== EXPR_NOT_MATCHED) return result;
|
|
820
813
|
}
|
|
814
|
+
return EXPR_NOT_MATCHED;
|
|
815
|
+
}
|
|
821
816
|
|
|
822
|
-
|
|
817
|
+
function evalConcatOrComparison(expr: string, context: Record<string, unknown>): ExprResult {
|
|
823
818
|
if (findOutsideQuotes(expr, "~") >= 0) {
|
|
824
819
|
const parts = splitOutsideQuotes(expr, "~");
|
|
825
820
|
if (parts.length > 1) {
|
|
@@ -829,19 +824,6 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
|
829
824
|
}).join("");
|
|
830
825
|
}
|
|
831
826
|
}
|
|
832
|
-
|
|
833
|
-
// Comparison/logical operators -> evalComparison, the SAME evaluator {% if %}
|
|
834
|
-
// uses, so a condition means the same thing in a condition and in an output
|
|
835
|
-
// expression.
|
|
836
|
-
//
|
|
837
|
-
// The LEADING unary `not` needs its own check: every operator below is matched
|
|
838
|
-
// WITH surrounding spaces, so `not x` (nothing to its left) matched none of
|
|
839
|
-
// them, fell through to the variable-resolution tail, and was looked up as a
|
|
840
|
-
// variable literally named "not x" -- found nothing, rendered EMPTY.
|
|
841
|
-
// `{% if not x %}` and `x and not y` always worked; only the standalone
|
|
842
|
-
// `{{ not x }}` was dropped, and before booleans rendered lowercase a dropped
|
|
843
|
-
// expression and `false -> ''` looked identical, which is why it survived.
|
|
844
|
-
// Fixed in 3.13.87 alongside the boolean contract.
|
|
845
827
|
if (expr.startsWith("not ")) {
|
|
846
828
|
return evalComparison(expr, context);
|
|
847
829
|
}
|
|
@@ -850,8 +832,20 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
|
850
832
|
return evalComparison(expr, context);
|
|
851
833
|
}
|
|
852
834
|
}
|
|
835
|
+
return EXPR_NOT_MATCHED;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const ARITHMETIC_OPERATIONS: Record<string, (left: number, right: number) => number> = {
|
|
839
|
+
"+": (left, right) => left + right,
|
|
840
|
+
"-": (left, right) => left - right,
|
|
841
|
+
"*": (left, right) => left * right,
|
|
842
|
+
"//": (left, right) => right !== 0 ? Math.floor(left / right) : 0,
|
|
843
|
+
"/": (left, right) => right !== 0 ? left / right : 0,
|
|
844
|
+
"%": (left, right) => right !== 0 ? left % right : 0,
|
|
845
|
+
"**": (left, right) => left ** right,
|
|
846
|
+
};
|
|
853
847
|
|
|
854
|
-
|
|
848
|
+
function evalArithmeticExpression(expr: string, context: Record<string, unknown>): ExprResult {
|
|
855
849
|
for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
|
|
856
850
|
const pos = findOutsideQuotes(expr, op);
|
|
857
851
|
if (pos >= 0) {
|
|
@@ -864,35 +858,16 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
|
864
858
|
let rNum = rVal != null ? Number(rVal) : 0;
|
|
865
859
|
if (isNaN(lNum)) lNum = 0;
|
|
866
860
|
if (isNaN(rNum)) rNum = 0;
|
|
867
|
-
|
|
868
|
-
// Preserve int type when both operands are int-like (except for / which returns float)
|
|
869
|
-
const bothInt = Number.isInteger(lNum) && Number.isInteger(rNum) && opS !== "/";
|
|
870
|
-
let result: number;
|
|
871
|
-
switch (opS) {
|
|
872
|
-
case "+": result = lNum + rNum; break;
|
|
873
|
-
case "-": result = lNum - rNum; break;
|
|
874
|
-
case "*": result = lNum * rNum; break;
|
|
875
|
-
case "//": result = rNum !== 0 ? Math.floor(lNum / rNum) : 0; break;
|
|
876
|
-
case "/": result = rNum !== 0 ? lNum / rNum : 0; break;
|
|
877
|
-
case "%": result = rNum !== 0 ? lNum % rNum : 0; break;
|
|
878
|
-
case "**": result = lNum ** rNum; break;
|
|
879
|
-
default: result = 0;
|
|
880
|
-
}
|
|
881
|
-
return bothInt && Number.isInteger(result) ? result : result;
|
|
861
|
+
return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
|
|
882
862
|
} catch {
|
|
883
863
|
return null;
|
|
884
864
|
}
|
|
885
865
|
}
|
|
886
866
|
}
|
|
867
|
+
return EXPR_NOT_MATCHED;
|
|
868
|
+
}
|
|
887
869
|
|
|
888
|
-
|
|
889
|
-
// concat (`~`), comparisons and arithmetic, but looser than member/function
|
|
890
|
-
// access — so it is resolved here, AFTER those operators have had a chance to
|
|
891
|
-
// split the expression. Handling it inside the expression evaluator (not only
|
|
892
|
-
// at the {{ }} output layer) makes filters work at any nesting depth: concat
|
|
893
|
-
// operands, ternary branches, and parenthesised sub-expressions. This fixes
|
|
894
|
-
// both the `|`-vs-`~` precedence bug and the "pipe inside parens returns empty"
|
|
895
|
-
// defect. (#171)
|
|
870
|
+
function evalFilterExpression(expr: string, context: Record<string, unknown>): ExprResult {
|
|
896
871
|
if (findOutsideQuotes(expr, "|") >= 0) {
|
|
897
872
|
const [baseExpr, filters] = parseFilterChain(expr);
|
|
898
873
|
if (filters.length > 0) {
|
|
@@ -919,43 +894,66 @@ function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
|
919
894
|
return value;
|
|
920
895
|
}
|
|
921
896
|
}
|
|
897
|
+
return EXPR_NOT_MATCHED;
|
|
898
|
+
}
|
|
922
899
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const fnName = fnMatch[1];
|
|
927
|
-
const rawArgs = fnMatch[2] || "";
|
|
900
|
+
function evaluateCallArgs(rawArgs: string, context: Record<string, unknown>): unknown[] {
|
|
901
|
+
return rawArgs.trim() ? splitArgs(rawArgs).map(arg => evalExpr(arg.trim(), context)) : [];
|
|
902
|
+
}
|
|
928
903
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
904
|
+
function evalDottedFunction(name: string, rawArgs: string, context: Record<string, unknown>): ExprResult {
|
|
905
|
+
const lastDot = name.lastIndexOf(".");
|
|
906
|
+
const owner = resolveVar(name.slice(0, lastDot), context);
|
|
907
|
+
const member = name.slice(lastDot + 1);
|
|
908
|
+
if (!owner || typeof owner !== "object" || !(member in (owner as Record<string, unknown>))) {
|
|
909
|
+
return EXPR_NOT_MATCHED;
|
|
910
|
+
}
|
|
911
|
+
const method = (owner as Record<string, unknown>)[member];
|
|
912
|
+
return typeof method === "function"
|
|
913
|
+
? method.apply(owner, evaluateCallArgs(rawArgs, context))
|
|
914
|
+
: EXPR_NOT_MATCHED;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function evalFunctionExpression(expr: string, context: Record<string, unknown>): ExprResult {
|
|
918
|
+
const match = expr.match(FN_CALL_RE);
|
|
919
|
+
if (!match) return EXPR_NOT_MATCHED;
|
|
920
|
+
const name = match[1];
|
|
921
|
+
const rawArgs = match[2] || "";
|
|
922
|
+
if (name.includes(".")) return evalDottedFunction(name, rawArgs, context);
|
|
923
|
+
const fn = context[name] ?? resolveVar(name, context);
|
|
924
|
+
if (typeof fn === "function") return fn(...evaluateCallArgs(rawArgs, context));
|
|
925
|
+
return EXPR_NOT_MATCHED;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const EXPR_EVALUATORS = [
|
|
929
|
+
evalPrimary,
|
|
930
|
+
evalConditional,
|
|
931
|
+
evalConcatOrComparison,
|
|
932
|
+
evalArithmeticExpression,
|
|
933
|
+
evalFilterExpression,
|
|
934
|
+
evalFunctionExpression,
|
|
935
|
+
] as const;
|
|
936
|
+
/** Cached expression dispatcher branch; exported only for cache-bound verification. */
|
|
937
|
+
export const expressionFormCache = new Map<string, number>();
|
|
938
|
+
|
|
939
|
+
function evalExpr(expr: string, context: Record<string, unknown>): unknown {
|
|
940
|
+
expr = expr.trim();
|
|
941
|
+
const cachedForm = expressionFormCache.get(expr);
|
|
942
|
+
if (cachedForm !== undefined) {
|
|
943
|
+
if (cachedForm === -1) return resolveVar(expr, context);
|
|
944
|
+
const result = EXPR_EVALUATORS[cachedForm](expr, context);
|
|
945
|
+
return result === EXPR_NOT_MATCHED ? resolveVar(expr, context) : result;
|
|
946
|
+
}
|
|
947
|
+
for (let index = 0; index < EXPR_EVALUATORS.length; index++) {
|
|
948
|
+
const result = EXPR_EVALUATORS[index](expr, context);
|
|
949
|
+
if (result !== EXPR_NOT_MATCHED) {
|
|
950
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
951
|
+
expressionFormCache.set(expr, index);
|
|
952
|
+
return result;
|
|
956
953
|
}
|
|
957
954
|
}
|
|
958
|
-
|
|
955
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
956
|
+
expressionFormCache.set(expr, FN_CALL_RE.test(expr) ? EXPR_EVALUATORS.length - 1 : -1);
|
|
959
957
|
return resolveVar(expr, context);
|
|
960
958
|
}
|
|
961
959
|
|
|
@@ -2622,6 +2622,7 @@ __export(engine_exports, {
|
|
|
2622
2622
|
Frond: () => Frond,
|
|
2623
2623
|
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
2624
2624
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
2625
|
+
expressionFormCache: () => expressionFormCache,
|
|
2625
2626
|
filterChainCache: () => filterChainCache,
|
|
2626
2627
|
pathParseCache: () => pathParseCache,
|
|
2627
2628
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
@@ -3030,62 +3031,58 @@ function splitOutsideQuotes(expr, sep6) {
|
|
|
3030
3031
|
parts.push(expr.slice(currentStart));
|
|
3031
3032
|
return parts;
|
|
3032
3033
|
}
|
|
3033
|
-
function
|
|
3034
|
-
expr
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
if (
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
}
|
|
3041
|
-
if (expr.length >= 2 && expr[0] === "(" && expr.endsWith(")")) {
|
|
3042
|
-
let depth = 0;
|
|
3043
|
-
let matched = true;
|
|
3044
|
-
for (let pi = 0; pi < expr.length; pi++) {
|
|
3045
|
-
if (expr[pi] === "(") depth++;
|
|
3046
|
-
else if (expr[pi] === ")") depth--;
|
|
3047
|
-
if (depth === 0 && pi < expr.length - 1) {
|
|
3048
|
-
matched = false;
|
|
3049
|
-
break;
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
if (matched) {
|
|
3053
|
-
return evalExpr(expr.slice(1, -1), context);
|
|
3054
|
-
}
|
|
3034
|
+
function parenthesizedInner(expr) {
|
|
3035
|
+
if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
|
|
3036
|
+
let depth = 0;
|
|
3037
|
+
for (let index = 0; index < expr.length; index++) {
|
|
3038
|
+
if (expr[index] === "(") depth++;
|
|
3039
|
+
else if (expr[index] === ")") depth--;
|
|
3040
|
+
if (depth === 0 && index < expr.length - 1) return null;
|
|
3055
3041
|
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
const truePart = rest.slice(0, colonIdx).trim();
|
|
3063
|
-
const falsePart = rest.slice(colonIdx + 1).trim();
|
|
3064
|
-
const cond = evalExpr(condPart, context);
|
|
3065
|
-
return cond ? evalExpr(truePart, context) : evalExpr(falsePart, context);
|
|
3066
|
-
}
|
|
3042
|
+
return expr.slice(1, -1);
|
|
3043
|
+
}
|
|
3044
|
+
function evalPrimary(expr, context) {
|
|
3045
|
+
const quote = expr[0];
|
|
3046
|
+
if (expr.length >= 2 && (quote === '"' || quote === "'") && expr.endsWith(quote) && !expr.slice(1, -1).includes(quote)) {
|
|
3047
|
+
return expr.slice(1, -1);
|
|
3067
3048
|
}
|
|
3049
|
+
const inner = parenthesizedInner(expr);
|
|
3050
|
+
if (inner !== null) return evalExpr(inner, context);
|
|
3051
|
+
return EXPR_NOT_MATCHED;
|
|
3052
|
+
}
|
|
3053
|
+
function evalTernaryExpression(expr, context) {
|
|
3054
|
+
const ternaryIdx = findTernary(expr);
|
|
3055
|
+
if (ternaryIdx === -1) return EXPR_NOT_MATCHED;
|
|
3056
|
+
const rest = expr.slice(ternaryIdx + 1);
|
|
3057
|
+
const colonIdx = findColon(rest);
|
|
3058
|
+
if (colonIdx === -1) return EXPR_NOT_MATCHED;
|
|
3059
|
+
const condition = evalExpr(expr.slice(0, ternaryIdx).trim(), context);
|
|
3060
|
+
const branch = condition ? rest.slice(0, colonIdx) : rest.slice(colonIdx + 1);
|
|
3061
|
+
return evalExpr(branch.trim(), context);
|
|
3062
|
+
}
|
|
3063
|
+
function evalInlineIfExpression(expr, context) {
|
|
3068
3064
|
const ifIdx = findOutsideQuotes(expr, " if ");
|
|
3069
|
-
if (ifIdx
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3065
|
+
if (ifIdx < 0) return EXPR_NOT_MATCHED;
|
|
3066
|
+
const elseIdx = findOutsideQuotes(expr, " else ");
|
|
3067
|
+
if (elseIdx < 0 || elseIdx <= ifIdx) return EXPR_NOT_MATCHED;
|
|
3068
|
+
const condition = evalExpr(expr.slice(ifIdx + 4, elseIdx).trim(), context);
|
|
3069
|
+
const branch = condition ? expr.slice(0, ifIdx) : expr.slice(elseIdx + 6);
|
|
3070
|
+
return evalExpr(branch.trim(), context);
|
|
3071
|
+
}
|
|
3072
|
+
function evalCoalesceExpression(expr, context) {
|
|
3079
3073
|
const qqIdx = findOutsideQuotes(expr, "??");
|
|
3080
|
-
if (qqIdx
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
return
|
|
3074
|
+
if (qqIdx === -1) return EXPR_NOT_MATCHED;
|
|
3075
|
+
const value = evalExpr(expr.slice(0, qqIdx).trim(), context);
|
|
3076
|
+
return value === null || value === void 0 ? evalExpr(expr.slice(qqIdx + 2).trim(), context) : value;
|
|
3077
|
+
}
|
|
3078
|
+
function evalConditional(expr, context) {
|
|
3079
|
+
for (const evaluator of [evalTernaryExpression, evalInlineIfExpression, evalCoalesceExpression]) {
|
|
3080
|
+
const result = evaluator(expr, context);
|
|
3081
|
+
if (result !== EXPR_NOT_MATCHED) return result;
|
|
3088
3082
|
}
|
|
3083
|
+
return EXPR_NOT_MATCHED;
|
|
3084
|
+
}
|
|
3085
|
+
function evalConcatOrComparison(expr, context) {
|
|
3089
3086
|
if (findOutsideQuotes(expr, "~") >= 0) {
|
|
3090
3087
|
const parts = splitOutsideQuotes(expr, "~");
|
|
3091
3088
|
if (parts.length > 1) {
|
|
@@ -3103,6 +3100,9 @@ function evalExpr(expr, context) {
|
|
|
3103
3100
|
return evalComparison(expr, context);
|
|
3104
3101
|
}
|
|
3105
3102
|
}
|
|
3103
|
+
return EXPR_NOT_MATCHED;
|
|
3104
|
+
}
|
|
3105
|
+
function evalArithmeticExpression(expr, context) {
|
|
3106
3106
|
for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
|
|
3107
3107
|
const pos = findOutsideQuotes(expr, op);
|
|
3108
3108
|
if (pos >= 0) {
|
|
@@ -3115,40 +3115,15 @@ function evalExpr(expr, context) {
|
|
|
3115
3115
|
let rNum = rVal != null ? Number(rVal) : 0;
|
|
3116
3116
|
if (isNaN(lNum)) lNum = 0;
|
|
3117
3117
|
if (isNaN(rNum)) rNum = 0;
|
|
3118
|
-
|
|
3119
|
-
const bothInt = Number.isInteger(lNum) && Number.isInteger(rNum) && opS !== "/";
|
|
3120
|
-
let result;
|
|
3121
|
-
switch (opS) {
|
|
3122
|
-
case "+":
|
|
3123
|
-
result = lNum + rNum;
|
|
3124
|
-
break;
|
|
3125
|
-
case "-":
|
|
3126
|
-
result = lNum - rNum;
|
|
3127
|
-
break;
|
|
3128
|
-
case "*":
|
|
3129
|
-
result = lNum * rNum;
|
|
3130
|
-
break;
|
|
3131
|
-
case "//":
|
|
3132
|
-
result = rNum !== 0 ? Math.floor(lNum / rNum) : 0;
|
|
3133
|
-
break;
|
|
3134
|
-
case "/":
|
|
3135
|
-
result = rNum !== 0 ? lNum / rNum : 0;
|
|
3136
|
-
break;
|
|
3137
|
-
case "%":
|
|
3138
|
-
result = rNum !== 0 ? lNum % rNum : 0;
|
|
3139
|
-
break;
|
|
3140
|
-
case "**":
|
|
3141
|
-
result = lNum ** rNum;
|
|
3142
|
-
break;
|
|
3143
|
-
default:
|
|
3144
|
-
result = 0;
|
|
3145
|
-
}
|
|
3146
|
-
return bothInt && Number.isInteger(result) ? result : result;
|
|
3118
|
+
return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
|
|
3147
3119
|
} catch {
|
|
3148
3120
|
return null;
|
|
3149
3121
|
}
|
|
3150
3122
|
}
|
|
3151
3123
|
}
|
|
3124
|
+
return EXPR_NOT_MATCHED;
|
|
3125
|
+
}
|
|
3126
|
+
function evalFilterExpression(expr, context) {
|
|
3152
3127
|
if (findOutsideQuotes(expr, "|") >= 0) {
|
|
3153
3128
|
const [baseExpr, filters] = parseFilterChain(expr);
|
|
3154
3129
|
if (filters.length > 0) {
|
|
@@ -3167,38 +3142,49 @@ function evalExpr(expr, context) {
|
|
|
3167
3142
|
return value;
|
|
3168
3143
|
}
|
|
3169
3144
|
}
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3145
|
+
return EXPR_NOT_MATCHED;
|
|
3146
|
+
}
|
|
3147
|
+
function evaluateCallArgs(rawArgs, context) {
|
|
3148
|
+
return rawArgs.trim() ? splitArgs(rawArgs).map((arg) => evalExpr(arg.trim(), context)) : [];
|
|
3149
|
+
}
|
|
3150
|
+
function evalDottedFunction(name, rawArgs, context) {
|
|
3151
|
+
const lastDot = name.lastIndexOf(".");
|
|
3152
|
+
const owner = resolveVar(name.slice(0, lastDot), context);
|
|
3153
|
+
const member = name.slice(lastDot + 1);
|
|
3154
|
+
if (!owner || typeof owner !== "object" || !(member in owner)) {
|
|
3155
|
+
return EXPR_NOT_MATCHED;
|
|
3156
|
+
}
|
|
3157
|
+
const method = owner[member];
|
|
3158
|
+
return typeof method === "function" ? method.apply(owner, evaluateCallArgs(rawArgs, context)) : EXPR_NOT_MATCHED;
|
|
3159
|
+
}
|
|
3160
|
+
function evalFunctionExpression(expr, context) {
|
|
3161
|
+
const match = expr.match(FN_CALL_RE);
|
|
3162
|
+
if (!match) return EXPR_NOT_MATCHED;
|
|
3163
|
+
const name = match[1];
|
|
3164
|
+
const rawArgs = match[2] || "";
|
|
3165
|
+
if (name.includes(".")) return evalDottedFunction(name, rawArgs, context);
|
|
3166
|
+
const fn = context[name] ?? resolveVar(name, context);
|
|
3167
|
+
if (typeof fn === "function") return fn(...evaluateCallArgs(rawArgs, context));
|
|
3168
|
+
return EXPR_NOT_MATCHED;
|
|
3169
|
+
}
|
|
3170
|
+
function evalExpr(expr, context) {
|
|
3171
|
+
expr = expr.trim();
|
|
3172
|
+
const cachedForm = expressionFormCache.get(expr);
|
|
3173
|
+
if (cachedForm !== void 0) {
|
|
3174
|
+
if (cachedForm === -1) return resolveVar(expr, context);
|
|
3175
|
+
const result = EXPR_EVALUATORS[cachedForm](expr, context);
|
|
3176
|
+
return result === EXPR_NOT_MATCHED ? resolveVar(expr, context) : result;
|
|
3177
|
+
}
|
|
3178
|
+
for (let index = 0; index < EXPR_EVALUATORS.length; index++) {
|
|
3179
|
+
const result = EXPR_EVALUATORS[index](expr, context);
|
|
3180
|
+
if (result !== EXPR_NOT_MATCHED) {
|
|
3181
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
3182
|
+
expressionFormCache.set(expr, index);
|
|
3183
|
+
return result;
|
|
3200
3184
|
}
|
|
3201
3185
|
}
|
|
3186
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
3187
|
+
expressionFormCache.set(expr, FN_CALL_RE.test(expr) ? EXPR_EVALUATORS.length - 1 : -1);
|
|
3202
3188
|
return resolveVar(expr, context);
|
|
3203
3189
|
}
|
|
3204
3190
|
function findTernary(expr) {
|
|
@@ -3654,7 +3640,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
3654
3640
|
function _generateFormTokenValue(descriptor = "") {
|
|
3655
3641
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
3656
3642
|
}
|
|
3657
|
-
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, 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, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
3643
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, 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, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, EXPR_NOT_MATCHED, ARITHMETIC_OPERATIONS, EXPR_EVALUATORS, expressionFormCache, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
3658
3644
|
var init_engine = __esm({
|
|
3659
3645
|
"../frond/src/engine.ts"() {
|
|
3660
3646
|
"use strict";
|
|
@@ -3756,6 +3742,25 @@ var init_engine = __esm({
|
|
|
3756
3742
|
MEMO_CACHE_MAX = 1024;
|
|
3757
3743
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
3758
3744
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
3745
|
+
EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
|
|
3746
|
+
ARITHMETIC_OPERATIONS = {
|
|
3747
|
+
"+": (left, right) => left + right,
|
|
3748
|
+
"-": (left, right) => left - right,
|
|
3749
|
+
"*": (left, right) => left * right,
|
|
3750
|
+
"//": (left, right) => right !== 0 ? Math.floor(left / right) : 0,
|
|
3751
|
+
"/": (left, right) => right !== 0 ? left / right : 0,
|
|
3752
|
+
"%": (left, right) => right !== 0 ? left % right : 0,
|
|
3753
|
+
"**": (left, right) => left ** right
|
|
3754
|
+
};
|
|
3755
|
+
EXPR_EVALUATORS = [
|
|
3756
|
+
evalPrimary,
|
|
3757
|
+
evalConditional,
|
|
3758
|
+
evalConcatOrComparison,
|
|
3759
|
+
evalArithmeticExpression,
|
|
3760
|
+
evalFilterExpression,
|
|
3761
|
+
evalFunctionExpression
|
|
3762
|
+
];
|
|
3763
|
+
expressionFormCache = /* @__PURE__ */ new Map();
|
|
3759
3764
|
VarRef = class {
|
|
3760
3765
|
constructor(name) {
|
|
3761
3766
|
this.name = name;
|
|
@@ -12232,7 +12237,7 @@ var init_metrics = __esm({
|
|
|
12232
12237
|
};
|
|
12233
12238
|
INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
|
|
12234
12239
|
SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
|
|
12235
|
-
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "
|
|
12240
|
+
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_referencing_test"];
|
|
12236
12241
|
FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
|
|
12237
12242
|
}
|
|
12238
12243
|
});
|
|
@@ -36,8 +36,8 @@ export declare const pathParseCache: Map<string, [string[], boolean[]]>;
|
|
|
36
36
|
*/
|
|
37
37
|
export declare const TEMPLATE_CACHE_MAX = 256;
|
|
38
38
|
/**
|
|
39
|
-
* Hard cap on every per-expression memo cache — `filterChainCache
|
|
40
|
-
* `pathParseCache` (ADR-0004, parity with PHP's MEMO_CACHE_MAX and the
|
|
39
|
+
* Hard cap on every per-expression memo cache — `filterChainCache`,
|
|
40
|
+
* `pathParseCache`, and `expressionFormCache` (ADR-0004, parity with PHP's MEMO_CACHE_MAX and the
|
|
41
41
|
* Python master's `@lru_cache(maxsize=1024)` on the equivalent module-level
|
|
42
42
|
* parsers). Deliberately higher than TEMPLATE_CACHE_MAX: one entry here is a
|
|
43
43
|
* small parsed-path array, orders of magnitude smaller than a token list.
|
|
@@ -47,6 +47,8 @@ export declare const TEMPLATE_CACHE_MAX = 256;
|
|
|
47
47
|
* string, the same order of magnitude as a compiled template.
|
|
48
48
|
*/
|
|
49
49
|
export declare const MEMO_CACHE_MAX = 1024;
|
|
50
|
+
/** Cached expression dispatcher branch; exported only for cache-bound verification. */
|
|
51
|
+
export declare const expressionFormCache: Map<string, number>;
|
|
50
52
|
/**
|
|
51
53
|
* Set the session ID used by formToken() / form_token() for CSRF session binding.
|
|
52
54
|
*/
|