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 CHANGED
@@ -1,10 +1,10 @@
1
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.101)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.103)
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.101 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.13.103 - 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
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.13.101",
3
+ "version": "3.13.103",
4
4
  "type": "module",
5
- "description": "Tina4 for Node.js/TypeScript - 54 built-in features, zero dependencies",
5
+ "description": "Tina4 for Node.js/TypeScript - native TypeScript conventions and shared Tina4 contracts",
6
6
  "keywords": [
7
7
  "tina4",
8
8
  "framework",
@@ -1463,6 +1463,7 @@ __export(engine_exports, {
1463
1463
  Frond: () => Frond,
1464
1464
  MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
1465
1465
  TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
1466
+ expressionFormCache: () => expressionFormCache,
1466
1467
  filterChainCache: () => filterChainCache,
1467
1468
  pathParseCache: () => pathParseCache,
1468
1469
  setFormTokenSessionId: () => setFormTokenSessionId
@@ -1871,62 +1872,58 @@ function splitOutsideQuotes(expr, sep6) {
1871
1872
  parts.push(expr.slice(currentStart));
1872
1873
  return parts;
1873
1874
  }
1874
- function evalExpr(expr, context) {
1875
- expr = expr.trim();
1876
- if (expr.length >= 2) {
1877
- const q = expr[0];
1878
- if ((q === '"' || q === "'") && expr.endsWith(q) && !expr.slice(1, -1).includes(q)) {
1879
- return expr.slice(1, -1);
1880
- }
1881
- }
1882
- if (expr.length >= 2 && expr[0] === "(" && expr.endsWith(")")) {
1883
- let depth = 0;
1884
- let matched = true;
1885
- for (let pi = 0; pi < expr.length; pi++) {
1886
- if (expr[pi] === "(") depth++;
1887
- else if (expr[pi] === ")") depth--;
1888
- if (depth === 0 && pi < expr.length - 1) {
1889
- matched = false;
1890
- break;
1891
- }
1892
- }
1893
- if (matched) {
1894
- return evalExpr(expr.slice(1, -1), context);
1895
- }
1875
+ function parenthesizedInner(expr) {
1876
+ if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
1877
+ let depth = 0;
1878
+ for (let index = 0; index < expr.length; index++) {
1879
+ if (expr[index] === "(") depth++;
1880
+ else if (expr[index] === ")") depth--;
1881
+ if (depth === 0 && index < expr.length - 1) return null;
1896
1882
  }
1897
- const ternaryIdx = findTernary(expr);
1898
- if (ternaryIdx !== -1) {
1899
- const condPart = expr.slice(0, ternaryIdx).trim();
1900
- const rest = expr.slice(ternaryIdx + 1);
1901
- const colonIdx = findColon(rest);
1902
- if (colonIdx !== -1) {
1903
- const truePart = rest.slice(0, colonIdx).trim();
1904
- const falsePart = rest.slice(colonIdx + 1).trim();
1905
- const cond = evalExpr(condPart, context);
1906
- return cond ? evalExpr(truePart, context) : evalExpr(falsePart, context);
1907
- }
1883
+ return expr.slice(1, -1);
1884
+ }
1885
+ function evalPrimary(expr, context) {
1886
+ const quote = expr[0];
1887
+ if (expr.length >= 2 && (quote === '"' || quote === "'") && expr.endsWith(quote) && !expr.slice(1, -1).includes(quote)) {
1888
+ return expr.slice(1, -1);
1908
1889
  }
1890
+ const inner = parenthesizedInner(expr);
1891
+ if (inner !== null) return evalExpr(inner, context);
1892
+ return EXPR_NOT_MATCHED;
1893
+ }
1894
+ function evalTernaryExpression(expr, context) {
1895
+ const ternaryIdx = findTernary(expr);
1896
+ if (ternaryIdx === -1) return EXPR_NOT_MATCHED;
1897
+ const rest = expr.slice(ternaryIdx + 1);
1898
+ const colonIdx = findColon(rest);
1899
+ if (colonIdx === -1) return EXPR_NOT_MATCHED;
1900
+ const condition = evalExpr(expr.slice(0, ternaryIdx).trim(), context);
1901
+ const branch = condition ? rest.slice(0, colonIdx) : rest.slice(colonIdx + 1);
1902
+ return evalExpr(branch.trim(), context);
1903
+ }
1904
+ function evalInlineIfExpression(expr, context) {
1909
1905
  const ifIdx = findOutsideQuotes(expr, " if ");
1910
- if (ifIdx >= 0) {
1911
- const elseIdx = findOutsideQuotes(expr, " else ");
1912
- if (elseIdx >= 0 && elseIdx > ifIdx) {
1913
- const valuePart = expr.slice(0, ifIdx).trim();
1914
- const condPart = expr.slice(ifIdx + 4, elseIdx).trim();
1915
- const elsePart = expr.slice(elseIdx + 6).trim();
1916
- const cond = evalExpr(condPart, context);
1917
- return cond ? evalExpr(valuePart, context) : evalExpr(elsePart, context);
1918
- }
1919
- }
1906
+ if (ifIdx < 0) return EXPR_NOT_MATCHED;
1907
+ const elseIdx = findOutsideQuotes(expr, " else ");
1908
+ if (elseIdx < 0 || elseIdx <= ifIdx) return EXPR_NOT_MATCHED;
1909
+ const condition = evalExpr(expr.slice(ifIdx + 4, elseIdx).trim(), context);
1910
+ const branch = condition ? expr.slice(0, ifIdx) : expr.slice(elseIdx + 6);
1911
+ return evalExpr(branch.trim(), context);
1912
+ }
1913
+ function evalCoalesceExpression(expr, context) {
1920
1914
  const qqIdx = findOutsideQuotes(expr, "??");
1921
- if (qqIdx !== -1) {
1922
- const left = expr.slice(0, qqIdx).trim();
1923
- const right = expr.slice(qqIdx + 2).trim();
1924
- const val = evalExpr(left, context);
1925
- if (val === null || val === void 0) {
1926
- return evalExpr(right, context);
1927
- }
1928
- return val;
1915
+ if (qqIdx === -1) return EXPR_NOT_MATCHED;
1916
+ const value = evalExpr(expr.slice(0, qqIdx).trim(), context);
1917
+ return value === null || value === void 0 ? evalExpr(expr.slice(qqIdx + 2).trim(), context) : value;
1918
+ }
1919
+ function evalConditional(expr, context) {
1920
+ for (const evaluator of [evalTernaryExpression, evalInlineIfExpression, evalCoalesceExpression]) {
1921
+ const result = evaluator(expr, context);
1922
+ if (result !== EXPR_NOT_MATCHED) return result;
1929
1923
  }
1924
+ return EXPR_NOT_MATCHED;
1925
+ }
1926
+ function evalConcatOrComparison(expr, context) {
1930
1927
  if (findOutsideQuotes(expr, "~") >= 0) {
1931
1928
  const parts = splitOutsideQuotes(expr, "~");
1932
1929
  if (parts.length > 1) {
@@ -1944,6 +1941,9 @@ function evalExpr(expr, context) {
1944
1941
  return evalComparison(expr, context);
1945
1942
  }
1946
1943
  }
1944
+ return EXPR_NOT_MATCHED;
1945
+ }
1946
+ function evalArithmeticExpression(expr, context) {
1947
1947
  for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
1948
1948
  const pos = findOutsideQuotes(expr, op);
1949
1949
  if (pos >= 0) {
@@ -1956,40 +1956,15 @@ function evalExpr(expr, context) {
1956
1956
  let rNum = rVal != null ? Number(rVal) : 0;
1957
1957
  if (isNaN(lNum)) lNum = 0;
1958
1958
  if (isNaN(rNum)) rNum = 0;
1959
- const opS = op.trim();
1960
- const bothInt = Number.isInteger(lNum) && Number.isInteger(rNum) && opS !== "/";
1961
- let result;
1962
- switch (opS) {
1963
- case "+":
1964
- result = lNum + rNum;
1965
- break;
1966
- case "-":
1967
- result = lNum - rNum;
1968
- break;
1969
- case "*":
1970
- result = lNum * rNum;
1971
- break;
1972
- case "//":
1973
- result = rNum !== 0 ? Math.floor(lNum / rNum) : 0;
1974
- break;
1975
- case "/":
1976
- result = rNum !== 0 ? lNum / rNum : 0;
1977
- break;
1978
- case "%":
1979
- result = rNum !== 0 ? lNum % rNum : 0;
1980
- break;
1981
- case "**":
1982
- result = lNum ** rNum;
1983
- break;
1984
- default:
1985
- result = 0;
1986
- }
1987
- return bothInt && Number.isInteger(result) ? result : result;
1959
+ return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
1988
1960
  } catch {
1989
1961
  return null;
1990
1962
  }
1991
1963
  }
1992
1964
  }
1965
+ return EXPR_NOT_MATCHED;
1966
+ }
1967
+ function evalFilterExpression(expr, context) {
1993
1968
  if (findOutsideQuotes(expr, "|") >= 0) {
1994
1969
  const [baseExpr, filters] = parseFilterChain(expr);
1995
1970
  if (filters.length > 0) {
@@ -2008,38 +1983,49 @@ function evalExpr(expr, context) {
2008
1983
  return value;
2009
1984
  }
2010
1985
  }
2011
- const fnMatch = expr.match(FN_CALL_RE);
2012
- if (fnMatch) {
2013
- const fnName = fnMatch[1];
2014
- const rawArgs = fnMatch[2] || "";
2015
- if (fnName.includes(".")) {
2016
- const lastDot = fnName.lastIndexOf(".");
2017
- const objPath = fnName.slice(0, lastDot);
2018
- const methodName = fnName.slice(lastDot + 1);
2019
- const obj = resolveVar(objPath, context);
2020
- if (obj && typeof obj === "object" && methodName in obj) {
2021
- const method = obj[methodName];
2022
- if (typeof method === "function") {
2023
- if (rawArgs.trim()) {
2024
- const parts = splitArgs(rawArgs);
2025
- const evalArgs = parts.map((a) => evalExpr(a.trim(), context));
2026
- return method.apply(obj, evalArgs);
2027
- }
2028
- return method.call(obj);
2029
- }
2030
- }
2031
- } else {
2032
- const fn = context[fnName] ?? resolveVar(fnName, context);
2033
- if (typeof fn === "function") {
2034
- if (rawArgs.trim()) {
2035
- const parts = splitArgs(rawArgs);
2036
- const evalArgs = parts.map((a) => evalExpr(a.trim(), context));
2037
- return fn(...evalArgs);
2038
- }
2039
- return fn();
2040
- }
1986
+ return EXPR_NOT_MATCHED;
1987
+ }
1988
+ function evaluateCallArgs(rawArgs, context) {
1989
+ return rawArgs.trim() ? splitArgs(rawArgs).map((arg) => evalExpr(arg.trim(), context)) : [];
1990
+ }
1991
+ function evalDottedFunction(name, rawArgs, context) {
1992
+ const lastDot = name.lastIndexOf(".");
1993
+ const owner = resolveVar(name.slice(0, lastDot), context);
1994
+ const member = name.slice(lastDot + 1);
1995
+ if (!owner || typeof owner !== "object" || !(member in owner)) {
1996
+ return EXPR_NOT_MATCHED;
1997
+ }
1998
+ const method = owner[member];
1999
+ return typeof method === "function" ? method.apply(owner, evaluateCallArgs(rawArgs, context)) : EXPR_NOT_MATCHED;
2000
+ }
2001
+ function evalFunctionExpression(expr, context) {
2002
+ const match = expr.match(FN_CALL_RE);
2003
+ if (!match) return EXPR_NOT_MATCHED;
2004
+ const name = match[1];
2005
+ const rawArgs = match[2] || "";
2006
+ if (name.includes(".")) return evalDottedFunction(name, rawArgs, context);
2007
+ const fn = context[name] ?? resolveVar(name, context);
2008
+ if (typeof fn === "function") return fn(...evaluateCallArgs(rawArgs, context));
2009
+ return EXPR_NOT_MATCHED;
2010
+ }
2011
+ function evalExpr(expr, context) {
2012
+ expr = expr.trim();
2013
+ const cachedForm = expressionFormCache.get(expr);
2014
+ if (cachedForm !== void 0) {
2015
+ if (cachedForm === -1) return resolveVar(expr, context);
2016
+ const result = EXPR_EVALUATORS[cachedForm](expr, context);
2017
+ return result === EXPR_NOT_MATCHED ? resolveVar(expr, context) : result;
2018
+ }
2019
+ for (let index = 0; index < EXPR_EVALUATORS.length; index++) {
2020
+ const result = EXPR_EVALUATORS[index](expr, context);
2021
+ if (result !== EXPR_NOT_MATCHED) {
2022
+ capCache(expressionFormCache, MEMO_CACHE_MAX);
2023
+ expressionFormCache.set(expr, index);
2024
+ return result;
2041
2025
  }
2042
2026
  }
2027
+ capCache(expressionFormCache, MEMO_CACHE_MAX);
2028
+ expressionFormCache.set(expr, FN_CALL_RE.test(expr) ? EXPR_EVALUATORS.length - 1 : -1);
2043
2029
  return resolveVar(expr, context);
2044
2030
  }
2045
2031
  function findTernary(expr) {
@@ -2495,7 +2481,7 @@ function _generateFormToken(descriptor = "") {
2495
2481
  function _generateFormTokenValue(descriptor = "") {
2496
2482
  return new SafeString(_buildFormTokenJwt(descriptor));
2497
2483
  }
2498
- 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;
2484
+ 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;
2499
2485
  var init_engine = __esm({
2500
2486
  "../frond/src/engine.ts"() {
2501
2487
  "use strict";
@@ -2597,6 +2583,25 @@ var init_engine = __esm({
2597
2583
  MEMO_CACHE_MAX = 1024;
2598
2584
  TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
2599
2585
  RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
2586
+ EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
2587
+ ARITHMETIC_OPERATIONS = {
2588
+ "+": (left, right) => left + right,
2589
+ "-": (left, right) => left - right,
2590
+ "*": (left, right) => left * right,
2591
+ "//": (left, right) => right !== 0 ? Math.floor(left / right) : 0,
2592
+ "/": (left, right) => right !== 0 ? left / right : 0,
2593
+ "%": (left, right) => right !== 0 ? left % right : 0,
2594
+ "**": (left, right) => left ** right
2595
+ };
2596
+ EXPR_EVALUATORS = [
2597
+ evalPrimary,
2598
+ evalConditional,
2599
+ evalConcatOrComparison,
2600
+ evalArithmeticExpression,
2601
+ evalFilterExpression,
2602
+ evalFunctionExpression
2603
+ ];
2604
+ expressionFormCache = /* @__PURE__ */ new Map();
2600
2605
  VarRef = class {
2601
2606
  constructor(name) {
2602
2607
  this.name = name;
@@ -23000,7 +23005,7 @@ var init_metrics = __esm({
23000
23005
  };
23001
23006
  INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
23002
23007
  SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
23003
- FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_tests"];
23008
+ FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_referencing_test"];
23004
23009
  FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
23005
23010
  }
23006
23011
  });
@@ -1462,6 +1462,7 @@ __export(engine_exports, {
1462
1462
  Frond: () => Frond,
1463
1463
  MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
1464
1464
  TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
1465
+ expressionFormCache: () => expressionFormCache,
1465
1466
  filterChainCache: () => filterChainCache,
1466
1467
  pathParseCache: () => pathParseCache,
1467
1468
  setFormTokenSessionId: () => setFormTokenSessionId
@@ -1870,62 +1871,58 @@ function splitOutsideQuotes(expr, sep6) {
1870
1871
  parts.push(expr.slice(currentStart));
1871
1872
  return parts;
1872
1873
  }
1873
- function evalExpr(expr, context) {
1874
- expr = expr.trim();
1875
- if (expr.length >= 2) {
1876
- const q = expr[0];
1877
- if ((q === '"' || q === "'") && expr.endsWith(q) && !expr.slice(1, -1).includes(q)) {
1878
- return expr.slice(1, -1);
1879
- }
1880
- }
1881
- if (expr.length >= 2 && expr[0] === "(" && expr.endsWith(")")) {
1882
- let depth = 0;
1883
- let matched = true;
1884
- for (let pi = 0; pi < expr.length; pi++) {
1885
- if (expr[pi] === "(") depth++;
1886
- else if (expr[pi] === ")") depth--;
1887
- if (depth === 0 && pi < expr.length - 1) {
1888
- matched = false;
1889
- break;
1890
- }
1891
- }
1892
- if (matched) {
1893
- return evalExpr(expr.slice(1, -1), context);
1894
- }
1874
+ function parenthesizedInner(expr) {
1875
+ if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
1876
+ let depth = 0;
1877
+ for (let index = 0; index < expr.length; index++) {
1878
+ if (expr[index] === "(") depth++;
1879
+ else if (expr[index] === ")") depth--;
1880
+ if (depth === 0 && index < expr.length - 1) return null;
1895
1881
  }
1896
- const ternaryIdx = findTernary(expr);
1897
- if (ternaryIdx !== -1) {
1898
- const condPart = expr.slice(0, ternaryIdx).trim();
1899
- const rest = expr.slice(ternaryIdx + 1);
1900
- const colonIdx = findColon(rest);
1901
- if (colonIdx !== -1) {
1902
- const truePart = rest.slice(0, colonIdx).trim();
1903
- const falsePart = rest.slice(colonIdx + 1).trim();
1904
- const cond = evalExpr(condPart, context);
1905
- return cond ? evalExpr(truePart, context) : evalExpr(falsePart, context);
1906
- }
1882
+ return expr.slice(1, -1);
1883
+ }
1884
+ function evalPrimary(expr, context) {
1885
+ const quote = expr[0];
1886
+ if (expr.length >= 2 && (quote === '"' || quote === "'") && expr.endsWith(quote) && !expr.slice(1, -1).includes(quote)) {
1887
+ return expr.slice(1, -1);
1907
1888
  }
1889
+ const inner = parenthesizedInner(expr);
1890
+ if (inner !== null) return evalExpr(inner, context);
1891
+ return EXPR_NOT_MATCHED;
1892
+ }
1893
+ function evalTernaryExpression(expr, context) {
1894
+ const ternaryIdx = findTernary(expr);
1895
+ if (ternaryIdx === -1) return EXPR_NOT_MATCHED;
1896
+ const rest = expr.slice(ternaryIdx + 1);
1897
+ const colonIdx = findColon(rest);
1898
+ if (colonIdx === -1) return EXPR_NOT_MATCHED;
1899
+ const condition = evalExpr(expr.slice(0, ternaryIdx).trim(), context);
1900
+ const branch = condition ? rest.slice(0, colonIdx) : rest.slice(colonIdx + 1);
1901
+ return evalExpr(branch.trim(), context);
1902
+ }
1903
+ function evalInlineIfExpression(expr, context) {
1908
1904
  const ifIdx = findOutsideQuotes(expr, " if ");
1909
- if (ifIdx >= 0) {
1910
- const elseIdx = findOutsideQuotes(expr, " else ");
1911
- if (elseIdx >= 0 && elseIdx > ifIdx) {
1912
- const valuePart = expr.slice(0, ifIdx).trim();
1913
- const condPart = expr.slice(ifIdx + 4, elseIdx).trim();
1914
- const elsePart = expr.slice(elseIdx + 6).trim();
1915
- const cond = evalExpr(condPart, context);
1916
- return cond ? evalExpr(valuePart, context) : evalExpr(elsePart, context);
1917
- }
1918
- }
1905
+ if (ifIdx < 0) return EXPR_NOT_MATCHED;
1906
+ const elseIdx = findOutsideQuotes(expr, " else ");
1907
+ if (elseIdx < 0 || elseIdx <= ifIdx) return EXPR_NOT_MATCHED;
1908
+ const condition = evalExpr(expr.slice(ifIdx + 4, elseIdx).trim(), context);
1909
+ const branch = condition ? expr.slice(0, ifIdx) : expr.slice(elseIdx + 6);
1910
+ return evalExpr(branch.trim(), context);
1911
+ }
1912
+ function evalCoalesceExpression(expr, context) {
1919
1913
  const qqIdx = findOutsideQuotes(expr, "??");
1920
- if (qqIdx !== -1) {
1921
- const left = expr.slice(0, qqIdx).trim();
1922
- const right = expr.slice(qqIdx + 2).trim();
1923
- const val = evalExpr(left, context);
1924
- if (val === null || val === void 0) {
1925
- return evalExpr(right, context);
1926
- }
1927
- return val;
1914
+ if (qqIdx === -1) return EXPR_NOT_MATCHED;
1915
+ const value = evalExpr(expr.slice(0, qqIdx).trim(), context);
1916
+ return value === null || value === void 0 ? evalExpr(expr.slice(qqIdx + 2).trim(), context) : value;
1917
+ }
1918
+ function evalConditional(expr, context) {
1919
+ for (const evaluator of [evalTernaryExpression, evalInlineIfExpression, evalCoalesceExpression]) {
1920
+ const result = evaluator(expr, context);
1921
+ if (result !== EXPR_NOT_MATCHED) return result;
1928
1922
  }
1923
+ return EXPR_NOT_MATCHED;
1924
+ }
1925
+ function evalConcatOrComparison(expr, context) {
1929
1926
  if (findOutsideQuotes(expr, "~") >= 0) {
1930
1927
  const parts = splitOutsideQuotes(expr, "~");
1931
1928
  if (parts.length > 1) {
@@ -1943,6 +1940,9 @@ function evalExpr(expr, context) {
1943
1940
  return evalComparison(expr, context);
1944
1941
  }
1945
1942
  }
1943
+ return EXPR_NOT_MATCHED;
1944
+ }
1945
+ function evalArithmeticExpression(expr, context) {
1946
1946
  for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
1947
1947
  const pos = findOutsideQuotes(expr, op);
1948
1948
  if (pos >= 0) {
@@ -1955,40 +1955,15 @@ function evalExpr(expr, context) {
1955
1955
  let rNum = rVal != null ? Number(rVal) : 0;
1956
1956
  if (isNaN(lNum)) lNum = 0;
1957
1957
  if (isNaN(rNum)) rNum = 0;
1958
- const opS = op.trim();
1959
- const bothInt = Number.isInteger(lNum) && Number.isInteger(rNum) && opS !== "/";
1960
- let result;
1961
- switch (opS) {
1962
- case "+":
1963
- result = lNum + rNum;
1964
- break;
1965
- case "-":
1966
- result = lNum - rNum;
1967
- break;
1968
- case "*":
1969
- result = lNum * rNum;
1970
- break;
1971
- case "//":
1972
- result = rNum !== 0 ? Math.floor(lNum / rNum) : 0;
1973
- break;
1974
- case "/":
1975
- result = rNum !== 0 ? lNum / rNum : 0;
1976
- break;
1977
- case "%":
1978
- result = rNum !== 0 ? lNum % rNum : 0;
1979
- break;
1980
- case "**":
1981
- result = lNum ** rNum;
1982
- break;
1983
- default:
1984
- result = 0;
1985
- }
1986
- return bothInt && Number.isInteger(result) ? result : result;
1958
+ return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
1987
1959
  } catch {
1988
1960
  return null;
1989
1961
  }
1990
1962
  }
1991
1963
  }
1964
+ return EXPR_NOT_MATCHED;
1965
+ }
1966
+ function evalFilterExpression(expr, context) {
1992
1967
  if (findOutsideQuotes(expr, "|") >= 0) {
1993
1968
  const [baseExpr, filters] = parseFilterChain(expr);
1994
1969
  if (filters.length > 0) {
@@ -2007,38 +1982,49 @@ function evalExpr(expr, context) {
2007
1982
  return value;
2008
1983
  }
2009
1984
  }
2010
- const fnMatch = expr.match(FN_CALL_RE);
2011
- if (fnMatch) {
2012
- const fnName = fnMatch[1];
2013
- const rawArgs = fnMatch[2] || "";
2014
- if (fnName.includes(".")) {
2015
- const lastDot = fnName.lastIndexOf(".");
2016
- const objPath = fnName.slice(0, lastDot);
2017
- const methodName = fnName.slice(lastDot + 1);
2018
- const obj = resolveVar(objPath, context);
2019
- if (obj && typeof obj === "object" && methodName in obj) {
2020
- const method = obj[methodName];
2021
- if (typeof method === "function") {
2022
- if (rawArgs.trim()) {
2023
- const parts = splitArgs(rawArgs);
2024
- const evalArgs = parts.map((a) => evalExpr(a.trim(), context));
2025
- return method.apply(obj, evalArgs);
2026
- }
2027
- return method.call(obj);
2028
- }
2029
- }
2030
- } else {
2031
- const fn = context[fnName] ?? resolveVar(fnName, context);
2032
- if (typeof fn === "function") {
2033
- if (rawArgs.trim()) {
2034
- const parts = splitArgs(rawArgs);
2035
- const evalArgs = parts.map((a) => evalExpr(a.trim(), context));
2036
- return fn(...evalArgs);
2037
- }
2038
- return fn();
2039
- }
1985
+ return EXPR_NOT_MATCHED;
1986
+ }
1987
+ function evaluateCallArgs(rawArgs, context) {
1988
+ return rawArgs.trim() ? splitArgs(rawArgs).map((arg) => evalExpr(arg.trim(), context)) : [];
1989
+ }
1990
+ function evalDottedFunction(name, rawArgs, context) {
1991
+ const lastDot = name.lastIndexOf(".");
1992
+ const owner = resolveVar(name.slice(0, lastDot), context);
1993
+ const member = name.slice(lastDot + 1);
1994
+ if (!owner || typeof owner !== "object" || !(member in owner)) {
1995
+ return EXPR_NOT_MATCHED;
1996
+ }
1997
+ const method = owner[member];
1998
+ return typeof method === "function" ? method.apply(owner, evaluateCallArgs(rawArgs, context)) : EXPR_NOT_MATCHED;
1999
+ }
2000
+ function evalFunctionExpression(expr, context) {
2001
+ const match = expr.match(FN_CALL_RE);
2002
+ if (!match) return EXPR_NOT_MATCHED;
2003
+ const name = match[1];
2004
+ const rawArgs = match[2] || "";
2005
+ if (name.includes(".")) return evalDottedFunction(name, rawArgs, context);
2006
+ const fn = context[name] ?? resolveVar(name, context);
2007
+ if (typeof fn === "function") return fn(...evaluateCallArgs(rawArgs, context));
2008
+ return EXPR_NOT_MATCHED;
2009
+ }
2010
+ function evalExpr(expr, context) {
2011
+ expr = expr.trim();
2012
+ const cachedForm = expressionFormCache.get(expr);
2013
+ if (cachedForm !== void 0) {
2014
+ if (cachedForm === -1) return resolveVar(expr, context);
2015
+ const result = EXPR_EVALUATORS[cachedForm](expr, context);
2016
+ return result === EXPR_NOT_MATCHED ? resolveVar(expr, context) : result;
2017
+ }
2018
+ for (let index = 0; index < EXPR_EVALUATORS.length; index++) {
2019
+ const result = EXPR_EVALUATORS[index](expr, context);
2020
+ if (result !== EXPR_NOT_MATCHED) {
2021
+ capCache(expressionFormCache, MEMO_CACHE_MAX);
2022
+ expressionFormCache.set(expr, index);
2023
+ return result;
2040
2024
  }
2041
2025
  }
2026
+ capCache(expressionFormCache, MEMO_CACHE_MAX);
2027
+ expressionFormCache.set(expr, FN_CALL_RE.test(expr) ? EXPR_EVALUATORS.length - 1 : -1);
2042
2028
  return resolveVar(expr, context);
2043
2029
  }
2044
2030
  function findTernary(expr) {
@@ -2494,7 +2480,7 @@ function _generateFormToken(descriptor = "") {
2494
2480
  function _generateFormTokenValue(descriptor = "") {
2495
2481
  return new SafeString(_buildFormTokenJwt(descriptor));
2496
2482
  }
2497
- 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;
2483
+ 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;
2498
2484
  var init_engine = __esm({
2499
2485
  "../frond/src/engine.ts"() {
2500
2486
  "use strict";
@@ -2596,6 +2582,25 @@ var init_engine = __esm({
2596
2582
  MEMO_CACHE_MAX = 1024;
2597
2583
  TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
2598
2584
  RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
2585
+ EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
2586
+ ARITHMETIC_OPERATIONS = {
2587
+ "+": (left, right) => left + right,
2588
+ "-": (left, right) => left - right,
2589
+ "*": (left, right) => left * right,
2590
+ "//": (left, right) => right !== 0 ? Math.floor(left / right) : 0,
2591
+ "/": (left, right) => right !== 0 ? left / right : 0,
2592
+ "%": (left, right) => right !== 0 ? left % right : 0,
2593
+ "**": (left, right) => left ** right
2594
+ };
2595
+ EXPR_EVALUATORS = [
2596
+ evalPrimary,
2597
+ evalConditional,
2598
+ evalConcatOrComparison,
2599
+ evalArithmeticExpression,
2600
+ evalFilterExpression,
2601
+ evalFunctionExpression
2602
+ ];
2603
+ expressionFormCache = /* @__PURE__ */ new Map();
2599
2604
  VarRef = class {
2600
2605
  constructor(name) {
2601
2606
  this.name = name;
@@ -22979,7 +22984,7 @@ var init_metrics = __esm({
22979
22984
  };
22980
22985
  INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
22981
22986
  SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
22982
- FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_tests"];
22987
+ FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_referencing_test"];
22983
22988
  FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
22984
22989
  }
22985
22990
  });