@safe-ugc-ui/validator 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -260,7 +260,6 @@ function validateNodes(views) {
260
260
  }
261
261
 
262
262
  // src/value-types.ts
263
- import { isRef, isExpr } from "@safe-ugc-ui/types";
264
263
  var STATIC_ONLY_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
265
264
  // Position / layout
266
265
  "position",
@@ -268,12 +267,16 @@ var STATIC_ONLY_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
268
267
  "right",
269
268
  "bottom",
270
269
  "left",
270
+ // Overflow
271
+ "overflow",
272
+ // Stacking
273
+ "zIndex"
274
+ ]);
275
+ var STRUCTURED_OBJECT_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
271
276
  // Transform
272
277
  "transform",
273
278
  // Gradient
274
279
  "backgroundGradient",
275
- // Overflow
276
- "overflow",
277
280
  // Borders
278
281
  "border",
279
282
  "borderTop",
@@ -281,46 +284,8 @@ var STATIC_ONLY_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
281
284
  "borderBottom",
282
285
  "borderLeft",
283
286
  // Shadow
284
- "boxShadow",
285
- // Stacking
286
- "zIndex"
287
+ "boxShadow"
287
288
  ]);
288
- function validateNodeFields(node, ctx, errors) {
289
- const nodeType = node.type;
290
- if (nodeType === "Image" || nodeType === "Avatar") {
291
- const src = node.src;
292
- if (src !== void 0 && isExpr(src)) {
293
- errors.push(
294
- createError(
295
- "EXPR_NOT_ALLOWED",
296
- `${nodeType}.src does not allow $expr bindings.`,
297
- `${ctx.path}.src`
298
- )
299
- );
300
- }
301
- }
302
- if (nodeType === "Icon") {
303
- const name = node.name;
304
- if (name !== void 0 && isRef(name)) {
305
- errors.push(
306
- createError(
307
- "REF_NOT_ALLOWED",
308
- "Icon.name does not allow $ref bindings.",
309
- `${ctx.path}.name`
310
- )
311
- );
312
- }
313
- if (name !== void 0 && isExpr(name)) {
314
- errors.push(
315
- createError(
316
- "EXPR_NOT_ALLOWED",
317
- "Icon.name does not allow $expr bindings.",
318
- `${ctx.path}.name`
319
- )
320
- );
321
- }
322
- }
323
- }
324
289
  function validateNodeStyle(node, ctx, errors) {
325
290
  const style = node.style;
326
291
  if (!style) {
@@ -330,12 +295,21 @@ function validateNodeStyle(node, ctx, errors) {
330
295
  if (value === void 0) {
331
296
  continue;
332
297
  }
333
- if (STATIC_ONLY_STYLE_PROPERTIES.has(prop)) {
334
- if (isRef(value) || isExpr(value)) {
298
+ if (typeof value === "object" && value !== null && "$ref" in value && typeof value.$ref === "string") {
299
+ if (STATIC_ONLY_STYLE_PROPERTIES.has(prop)) {
335
300
  errors.push(
336
301
  createError(
337
302
  "DYNAMIC_NOT_ALLOWED",
338
- `Style property "${prop}" must be a static literal; $ref and $expr are not allowed.`,
303
+ `Style property "${prop}" must be a static literal; $ref is not allowed.`,
304
+ `${ctx.path}.style.${prop}`
305
+ )
306
+ );
307
+ }
308
+ if (STRUCTURED_OBJECT_STYLE_PROPERTIES.has(prop)) {
309
+ errors.push(
310
+ createError(
311
+ "DYNAMIC_NOT_ALLOWED",
312
+ `Style property "${prop}" must be an object literal; use $ref only inside its nested fields.`,
339
313
  `${ctx.path}.style.${prop}`
340
314
  )
341
315
  );
@@ -346,7 +320,6 @@ function validateNodeStyle(node, ctx, errors) {
346
320
  function validateValueTypes(views) {
347
321
  const errors = [];
348
322
  traverseCard(views, (node, ctx) => {
349
- validateNodeFields(node, ctx, errors);
350
323
  validateNodeStyle(node, ctx, errors);
351
324
  });
352
325
  return errors;
@@ -377,8 +350,7 @@ import {
377
350
  TRANSITION_DELAY_MAX,
378
351
  TRANSITION_MAX_COUNT,
379
352
  ALLOWED_TRANSITION_PROPERTIES,
380
- isRef as isRef2,
381
- isExpr as isExpr2
353
+ isRef
382
354
  } from "@safe-ugc-ui/types";
383
355
  var COLOR_PROPERTIES = /* @__PURE__ */ new Set(["backgroundColor", "color"]);
384
356
  var LENGTH_PROPERTIES = /* @__PURE__ */ new Set([
@@ -434,7 +406,7 @@ function isLiteralString(value) {
434
406
  return typeof value === "string";
435
407
  }
436
408
  function isDynamic(value) {
437
- return isRef2(value) || isExpr2(value);
409
+ return isRef(value);
438
410
  }
439
411
  function isValidColor(value) {
440
412
  const lower = value.toLowerCase();
@@ -479,7 +451,7 @@ function collectDangerousCssErrors(value, path, errors) {
479
451
  }
480
452
  return;
481
453
  }
482
- if (isRef2(value) || isExpr2(value)) {
454
+ if (isRef(value)) {
483
455
  return;
484
456
  }
485
457
  if (Array.isArray(value)) {
@@ -524,7 +496,66 @@ function validateShadowObject(shadow, path, errors) {
524
496
  }
525
497
  }
526
498
  var STYLE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
527
- function validateSingleStyle(style, stylePath, errors) {
499
+ function reportStyleRefError(rawRef, stylePath, cardStyles, errors) {
500
+ const trimmedRef = rawRef.trim();
501
+ if (!STYLE_NAME_PATTERN.test(trimmedRef)) {
502
+ errors.push(
503
+ createError(
504
+ "INVALID_STYLE_REF",
505
+ `$style value "${rawRef}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/ at "${stylePath}.$style"`,
506
+ `${stylePath}.$style`
507
+ )
508
+ );
509
+ return void 0;
510
+ }
511
+ if (!cardStyles || !(trimmedRef in cardStyles)) {
512
+ errors.push(
513
+ createError(
514
+ "STYLE_REF_NOT_FOUND",
515
+ `$style references "${trimmedRef}" which is not defined in card.styles at "${stylePath}.$style"`,
516
+ `${stylePath}.$style`
517
+ )
518
+ );
519
+ return void 0;
520
+ }
521
+ return trimmedRef;
522
+ }
523
+ function resolveStyleRef(style, stylePath, cardStyles, errors) {
524
+ if (typeof style.$style !== "string") {
525
+ return style;
526
+ }
527
+ const trimmedRef = reportStyleRefError(style.$style, stylePath, cardStyles, errors);
528
+ if (!trimmedRef || !cardStyles) {
529
+ return void 0;
530
+ }
531
+ return mergeStyleWithRef(cardStyles[trimmedRef], style);
532
+ }
533
+ function collectNestedStyleRefErrors(value, path, errors) {
534
+ if (Array.isArray(value)) {
535
+ for (let i = 0; i < value.length; i++) {
536
+ collectNestedStyleRefErrors(value[i], `${path}[${i}]`, errors);
537
+ }
538
+ return;
539
+ }
540
+ if (typeof value !== "object" || value === null) {
541
+ return;
542
+ }
543
+ for (const [key, child] of Object.entries(value)) {
544
+ const childPath = `${path}.${key}`;
545
+ if (key === "$style") {
546
+ errors.push(
547
+ createError(
548
+ "STYLE_CIRCULAR_REF",
549
+ `$style cannot be used inside card.styles definitions at "${childPath}"`,
550
+ childPath
551
+ )
552
+ );
553
+ continue;
554
+ }
555
+ collectNestedStyleRefErrors(child, childPath, errors);
556
+ }
557
+ }
558
+ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverStyleRefs = true) {
528
559
  const STRUCTURED_FIELDS = /* @__PURE__ */ new Set(["transition", "hoverStyle"]);
529
560
  for (const key of Object.keys(style)) {
530
561
  if (!STRUCTURED_FIELDS.has(key) && FORBIDDEN_STYLE_PROPERTIES.includes(key)) {
@@ -809,16 +840,16 @@ function validateSingleStyle(style, stylePath, errors) {
809
840
  )
810
841
  );
811
842
  }
812
- if ("$style" in hoverObj) {
813
- errors.push(
814
- createError(
815
- "INVALID_STYLE_REF",
816
- `$style is not allowed inside hoverStyle at "${hoverPath}.$style"`,
817
- `${hoverPath}.$style`
818
- )
843
+ const mergedHoverStyle = allowHoverStyleRefs ? resolveStyleRef(hoverObj, hoverPath, cardStyles, errors) : hoverObj;
844
+ if (mergedHoverStyle) {
845
+ validateSingleStyle(
846
+ mergedHoverStyle,
847
+ hoverPath,
848
+ errors,
849
+ cardStyles,
850
+ allowHoverStyleRefs
819
851
  );
820
852
  }
821
- validateSingleStyle(hoverObj, hoverPath, errors);
822
853
  }
823
854
  }
824
855
  if ("transition" in style && style.transition != null) {
@@ -906,16 +937,8 @@ function validateStyles(views, cardStyles) {
906
937
  )
907
938
  );
908
939
  }
909
- if ("$style" in styleEntry) {
910
- errors.push(
911
- createError(
912
- "STYLE_CIRCULAR_REF",
913
- `$style cannot be used inside card.styles definitions at "${entryPath}.$style"`,
914
- `${entryPath}.$style`
915
- )
916
- );
917
- }
918
- validateSingleStyle(styleEntry, entryPath, errors);
940
+ collectNestedStyleRefErrors(styleEntry, entryPath, errors);
941
+ validateSingleStyle(styleEntry, entryPath, errors, void 0, false);
919
942
  }
920
943
  }
921
944
  traverseCard(views, (node, ctx) => {
@@ -924,33 +947,9 @@ function validateStyles(views, cardStyles) {
924
947
  return;
925
948
  }
926
949
  const stylePath = `${ctx.path}.style`;
927
- if ("$style" in style && typeof style.$style === "string") {
928
- const rawRef = style.$style;
929
- const trimmedRef = rawRef.trim();
930
- if (!STYLE_NAME_PATTERN.test(trimmedRef)) {
931
- errors.push(
932
- createError(
933
- "INVALID_STYLE_REF",
934
- `$style value "${rawRef}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/ at "${stylePath}.$style"`,
935
- `${stylePath}.$style`
936
- )
937
- );
938
- return;
939
- }
940
- if (!cardStyles || !(trimmedRef in cardStyles)) {
941
- errors.push(
942
- createError(
943
- "STYLE_REF_NOT_FOUND",
944
- `$style references "${trimmedRef}" which is not defined in card.styles at "${stylePath}.$style"`,
945
- `${stylePath}.$style`
946
- )
947
- );
948
- return;
949
- }
950
- const mergedStyle = mergeStyleWithRef(cardStyles[trimmedRef], style);
951
- validateSingleStyle(mergedStyle, stylePath, errors);
952
- } else {
953
- validateSingleStyle(style, stylePath, errors);
950
+ const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
951
+ if (mergedStyle) {
952
+ validateSingleStyle(mergedStyle, stylePath, errors, cardStyles);
954
953
  }
955
954
  });
956
955
  return errors;
@@ -959,7 +958,7 @@ function validateStyles(views, cardStyles) {
959
958
  // src/security.ts
960
959
  import {
961
960
  PROTOTYPE_POLLUTION_SEGMENTS,
962
- isRef as isRef3
961
+ isRef as isRef2
963
962
  } from "@safe-ugc-ui/types";
964
963
  var FORBIDDEN_URL_PREFIXES = [
965
964
  "http://",
@@ -972,7 +971,7 @@ function scanForRefs(obj, path, errors) {
972
971
  if (obj === null || obj === void 0 || typeof obj !== "object") {
973
972
  return;
974
973
  }
975
- if (isRef3(obj)) {
974
+ if (isRef2(obj)) {
976
975
  const refStr = obj.$ref;
977
976
  const segments = refStr.split(/[.\[]/);
978
977
  for (const segment of segments) {
@@ -1109,12 +1108,11 @@ function validateSecurity(card) {
1109
1108
  delete nodeFields.type;
1110
1109
  delete nodeFields.style;
1111
1110
  delete nodeFields.children;
1112
- delete nodeFields.condition;
1113
1111
  if (type === "Image" || type === "Avatar") {
1114
1112
  const src = node.src;
1115
1113
  if (typeof src === "string") {
1116
1114
  checkSrcValue(src, type, `${path}.src`, errors);
1117
- } else if (isRef3(src)) {
1115
+ } else if (isRef2(src)) {
1118
1116
  if (state) {
1119
1117
  const resolved = resolveRefFromState(
1120
1118
  src.$ref,
@@ -1207,8 +1205,7 @@ import {
1207
1205
  MAX_OVERFLOW_AUTO_COUNT,
1208
1206
  MAX_STACK_NESTING,
1209
1207
  PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2,
1210
- isRef as isRef4,
1211
- isExpr as isExpr3
1208
+ isRef as isRef3
1212
1209
  } from "@safe-ugc-ui/types";
1213
1210
  function utf8ByteLength(str) {
1214
1211
  let bytes = 0;
@@ -1269,7 +1266,7 @@ function countTemplateMetrics(template, cardStyles) {
1269
1266
  result.nodes = 1;
1270
1267
  if (node.type === "Text") {
1271
1268
  const content = node.content;
1272
- if (typeof content === "string" && !isRef4(content) && !isExpr3(content)) {
1269
+ if (typeof content === "string" && !isRef3(content)) {
1273
1270
  result.textBytes = utf8ByteLength(content);
1274
1271
  }
1275
1272
  }
@@ -1325,7 +1322,7 @@ function validateLimits(card) {
1325
1322
  nodeCount++;
1326
1323
  if (node.type === "Text") {
1327
1324
  const content = node.content;
1328
- if (typeof content === "string" && !isRef4(content) && !isExpr3(content)) {
1325
+ if (typeof content === "string" && !isRef3(content)) {
1329
1326
  textContentBytes += utf8ByteLength(content);
1330
1327
  }
1331
1328
  }
@@ -1460,428 +1457,6 @@ function validateLimits(card) {
1460
1457
  return errors;
1461
1458
  }
1462
1459
 
1463
- // src/expr-constraints.ts
1464
- import {
1465
- EXPR_MAX_LENGTH,
1466
- EXPR_MAX_TOKENS,
1467
- EXPR_MAX_NESTING,
1468
- EXPR_MAX_CONDITION_NESTING,
1469
- EXPR_MAX_REF_DEPTH,
1470
- EXPR_MAX_ARRAY_INDEX,
1471
- EXPR_MAX_STRING_LITERAL,
1472
- EXPR_MAX_FRACTIONAL_DIGITS,
1473
- isRef as isRef5,
1474
- isExpr as isExpr4
1475
- } from "@safe-ugc-ui/types";
1476
- var FORBIDDEN_KEYWORDS = [
1477
- "typeof",
1478
- "instanceof",
1479
- "new",
1480
- "delete",
1481
- "function",
1482
- "return",
1483
- "var",
1484
- "let",
1485
- "const"
1486
- ];
1487
- var FORBIDDEN_KEYWORD_SET = new Set(FORBIDDEN_KEYWORDS);
1488
- var PROTOTYPE_POLLUTION_SEGMENTS3 = /* @__PURE__ */ new Set([
1489
- "__proto__",
1490
- "constructor",
1491
- "prototype"
1492
- ]);
1493
- function tokenize(expr, path) {
1494
- const tokens = [];
1495
- const errors = [];
1496
- let i = 0;
1497
- while (i < expr.length) {
1498
- if (/\s/.test(expr[i])) {
1499
- i++;
1500
- continue;
1501
- }
1502
- if (i + 2 < expr.length) {
1503
- const three = expr.slice(i, i + 3);
1504
- if (three === "===" || three === "!==") {
1505
- tokens.push({ type: "comparison", value: three, position: i });
1506
- i += 3;
1507
- continue;
1508
- }
1509
- }
1510
- if (i + 1 < expr.length) {
1511
- const two = expr.slice(i, i + 2);
1512
- if (two === "==" || two === "!=" || two === "<=" || two === ">=") {
1513
- tokens.push({ type: "comparison", value: two, position: i });
1514
- i += 2;
1515
- continue;
1516
- }
1517
- if (two === "&&" || two === "||") {
1518
- tokens.push({ type: "logic_keyword", value: two, position: i });
1519
- i += 2;
1520
- continue;
1521
- }
1522
- }
1523
- if (expr[i] === "'" || expr[i] === '"') {
1524
- const quote = expr[i];
1525
- let j = i + 1;
1526
- while (j < expr.length && expr[j] !== quote) {
1527
- if (expr[j] === "\\" && j + 1 < expr.length) {
1528
- j += 2;
1529
- } else {
1530
- j++;
1531
- }
1532
- }
1533
- const innerValue = expr.slice(i + 1, j);
1534
- tokens.push({ type: "string", value: innerValue, position: i });
1535
- i = j + 1;
1536
- continue;
1537
- }
1538
- if (/[0-9]/.test(expr[i]) || expr[i] === "-" && i + 1 < expr.length && /[0-9]/.test(expr[i + 1]) && isNegativeSign(tokens)) {
1539
- let j = i;
1540
- if (expr[j] === "-") j++;
1541
- while (j < expr.length && /[0-9]/.test(expr[j])) j++;
1542
- if (j < expr.length && expr[j] === ".") {
1543
- j++;
1544
- while (j < expr.length && /[0-9]/.test(expr[j])) j++;
1545
- }
1546
- const numStr = expr.slice(i, j);
1547
- const dotIdx = numStr.indexOf(".");
1548
- if (dotIdx !== -1) {
1549
- const fractionalPart = numStr.slice(dotIdx + 1);
1550
- if (fractionalPart.length > EXPR_MAX_FRACTIONAL_DIGITS) {
1551
- errors.push(
1552
- createError(
1553
- "EXPR_INVALID_TOKEN",
1554
- `Number literal "${numStr}" at position ${i} has ${fractionalPart.length} fractional digits, maximum is ${EXPR_MAX_FRACTIONAL_DIGITS}.`,
1555
- path
1556
- )
1557
- );
1558
- }
1559
- }
1560
- tokens.push({ type: "number", value: numStr, position: i });
1561
- i = j;
1562
- continue;
1563
- }
1564
- if (expr[i] === "$" || /[a-zA-Z_]/.test(expr[i])) {
1565
- let j = i;
1566
- if (expr[j] === "$") j++;
1567
- while (j < expr.length && /[\w]/.test(expr[j])) j++;
1568
- const word = expr.slice(i, j);
1569
- if (word === "true" || word === "false") {
1570
- tokens.push({ type: "boolean", value: word, position: i });
1571
- } else if (word === "and" || word === "or" || word === "not") {
1572
- tokens.push({ type: "logic_keyword", value: word, position: i });
1573
- } else if (word === "if" || word === "then" || word === "else") {
1574
- tokens.push({ type: "condition_keyword", value: word, position: i });
1575
- } else {
1576
- tokens.push({ type: "identifier", value: word, position: i });
1577
- }
1578
- i = j;
1579
- continue;
1580
- }
1581
- const ch = expr[i];
1582
- if ("+-*/%".includes(ch)) {
1583
- tokens.push({ type: "arithmetic", value: ch, position: i });
1584
- i++;
1585
- continue;
1586
- }
1587
- if (ch === "<" || ch === ">") {
1588
- tokens.push({ type: "comparison", value: ch, position: i });
1589
- i++;
1590
- continue;
1591
- }
1592
- if ("().[]".includes(ch)) {
1593
- tokens.push({ type: "separator", value: ch, position: i });
1594
- i++;
1595
- continue;
1596
- }
1597
- if (ch === "!") {
1598
- tokens.push({ type: "comparison", value: ch, position: i });
1599
- i++;
1600
- continue;
1601
- }
1602
- errors.push(
1603
- createError(
1604
- "EXPR_INVALID_TOKEN",
1605
- `Unrecognized character "${ch}" at position ${i} in expression.`,
1606
- path
1607
- )
1608
- );
1609
- i++;
1610
- }
1611
- for (let t = 0; t < tokens.length; t++) {
1612
- const tok = tokens[t];
1613
- if (tok.value === "===" || tok.value === "!==" || (tok.value === "&&" || tok.value === "||")) {
1614
- errors.push(
1615
- createError(
1616
- "EXPR_FORBIDDEN_TOKEN",
1617
- `Forbidden operator "${tok.value}" at position ${tok.position}. Use "==" / "!=" or "and" / "or" instead.`,
1618
- path
1619
- )
1620
- );
1621
- }
1622
- if (tok.value === "!") {
1623
- errors.push(
1624
- createError(
1625
- "EXPR_FORBIDDEN_TOKEN",
1626
- `Forbidden operator "!" at position ${tok.position}. Use "not" instead.`,
1627
- path
1628
- )
1629
- );
1630
- }
1631
- if (tok.type === "identifier" && FORBIDDEN_KEYWORD_SET.has(tok.value)) {
1632
- errors.push(
1633
- createError(
1634
- "EXPR_FORBIDDEN_TOKEN",
1635
- `Forbidden keyword "${tok.value}" at position ${tok.position}.`,
1636
- path
1637
- )
1638
- );
1639
- }
1640
- if (tok.type === "identifier" && !tok.value.startsWith("$") && !FORBIDDEN_KEYWORD_SET.has(tok.value)) {
1641
- errors.push(
1642
- createError(
1643
- "EXPR_FORBIDDEN_TOKEN",
1644
- `Identifier "${tok.value}" at position ${tok.position} must start with "$". Use "$${tok.value}" for variable references.`,
1645
- path
1646
- )
1647
- );
1648
- }
1649
- if (tok.type === "identifier") {
1650
- const next = tokens[t + 1];
1651
- if (next && next.type === "separator" && next.value === "(") {
1652
- errors.push(
1653
- createError(
1654
- "EXPR_FUNCTION_CALL",
1655
- `Function call pattern detected: "${tok.value}(" at position ${tok.position}. Function calls are not allowed.`,
1656
- path
1657
- )
1658
- );
1659
- }
1660
- }
1661
- }
1662
- return { tokens, errors };
1663
- }
1664
- function isNegativeSign(tokens) {
1665
- if (tokens.length === 0) return true;
1666
- const prev = tokens[tokens.length - 1];
1667
- if (prev.type === "arithmetic" || prev.type === "comparison" || prev.type === "logic_keyword" || prev.type === "condition_keyword") {
1668
- return true;
1669
- }
1670
- if (prev.type === "separator" && (prev.value === "(" || prev.value === "[")) {
1671
- return true;
1672
- }
1673
- return false;
1674
- }
1675
- function scanForDynamicValues(obj, basePath, callback) {
1676
- if (obj === null || obj === void 0 || typeof obj !== "object") {
1677
- return;
1678
- }
1679
- if (isRef5(obj) || isExpr4(obj)) {
1680
- callback(obj, basePath);
1681
- return;
1682
- }
1683
- if (Array.isArray(obj)) {
1684
- for (let i = 0; i < obj.length; i++) {
1685
- scanForDynamicValues(obj[i], `${basePath}[${i}]`, callback);
1686
- }
1687
- } else {
1688
- for (const [key, value] of Object.entries(obj)) {
1689
- scanForDynamicValues(value, `${basePath}.${key}`, callback);
1690
- }
1691
- }
1692
- }
1693
- function validateRef(refValue, path) {
1694
- const errors = [];
1695
- if (refValue.length > 500) {
1696
- errors.push(
1697
- createError(
1698
- "REF_TOO_LONG",
1699
- `$ref value exceeds maximum length of 500 characters (got ${refValue.length}).`,
1700
- path
1701
- )
1702
- );
1703
- }
1704
- const segments = refValue.split(".");
1705
- if (segments.length > EXPR_MAX_REF_DEPTH) {
1706
- errors.push(
1707
- createError(
1708
- "EXPR_REF_DEPTH_EXCEEDED",
1709
- `$ref path depth ${segments.length} exceeds maximum of ${EXPR_MAX_REF_DEPTH}.`,
1710
- path
1711
- )
1712
- );
1713
- }
1714
- const arrayIndexPattern = /\[(\d+)\]/g;
1715
- let match;
1716
- while ((match = arrayIndexPattern.exec(refValue)) !== null) {
1717
- const index = parseInt(match[1], 10);
1718
- if (index > EXPR_MAX_ARRAY_INDEX) {
1719
- errors.push(
1720
- createError(
1721
- "EXPR_ARRAY_INDEX_EXCEEDED",
1722
- `Array index ${index} in $ref exceeds maximum of ${EXPR_MAX_ARRAY_INDEX}.`,
1723
- path
1724
- )
1725
- );
1726
- }
1727
- }
1728
- for (const segment of segments) {
1729
- const cleanSegment = segment.replace(/\[\d+\]/g, "");
1730
- if (PROTOTYPE_POLLUTION_SEGMENTS3.has(cleanSegment)) {
1731
- errors.push(
1732
- createError(
1733
- "PROTOTYPE_POLLUTION",
1734
- `$ref path contains forbidden segment "${cleanSegment}".`,
1735
- path
1736
- )
1737
- );
1738
- }
1739
- }
1740
- return errors;
1741
- }
1742
- function validateExpr(exprValue, path) {
1743
- const errors = [];
1744
- if (exprValue.length > EXPR_MAX_LENGTH) {
1745
- errors.push(
1746
- createError(
1747
- "EXPR_TOO_LONG",
1748
- `Expression exceeds maximum length of ${EXPR_MAX_LENGTH} characters (got ${exprValue.length}).`,
1749
- path
1750
- )
1751
- );
1752
- }
1753
- const { tokens, errors: tokenErrors } = tokenize(exprValue, path);
1754
- errors.push(...tokenErrors);
1755
- if (tokens.length > EXPR_MAX_TOKENS) {
1756
- errors.push(
1757
- createError(
1758
- "EXPR_TOO_MANY_TOKENS",
1759
- `Expression has ${tokens.length} tokens, exceeding maximum of ${EXPR_MAX_TOKENS}.`,
1760
- path
1761
- )
1762
- );
1763
- }
1764
- for (const tok of tokens) {
1765
- if (tok.type === "string" && tok.value.length > EXPR_MAX_STRING_LITERAL) {
1766
- errors.push(
1767
- createError(
1768
- "EXPR_STRING_LITERAL_TOO_LONG",
1769
- `String literal at position ${tok.position} has ${tok.value.length} characters, exceeding maximum of ${EXPR_MAX_STRING_LITERAL}.`,
1770
- path
1771
- )
1772
- );
1773
- }
1774
- }
1775
- let parenDepth = 0;
1776
- let maxParenDepth = 0;
1777
- let ifCount = 0;
1778
- for (const tok of tokens) {
1779
- if (tok.type === "separator" && tok.value === "(") {
1780
- parenDepth++;
1781
- if (parenDepth > maxParenDepth) {
1782
- maxParenDepth = parenDepth;
1783
- }
1784
- } else if (tok.type === "separator" && tok.value === ")") {
1785
- parenDepth--;
1786
- } else if (tok.type === "condition_keyword" && tok.value === "if") {
1787
- ifCount++;
1788
- }
1789
- }
1790
- if (maxParenDepth > EXPR_MAX_NESTING) {
1791
- errors.push(
1792
- createError(
1793
- "EXPR_NESTING_TOO_DEEP",
1794
- `Expression parenthesis nesting depth ${maxParenDepth} exceeds maximum of ${EXPR_MAX_NESTING}.`,
1795
- path
1796
- )
1797
- );
1798
- }
1799
- if (ifCount > EXPR_MAX_CONDITION_NESTING) {
1800
- errors.push(
1801
- createError(
1802
- "EXPR_CONDITION_NESTING_TOO_DEEP",
1803
- `Expression has ${ifCount} nested if-conditions, exceeding maximum of ${EXPR_MAX_CONDITION_NESTING}.`,
1804
- path
1805
- )
1806
- );
1807
- }
1808
- for (let t = 0; t < tokens.length; t++) {
1809
- const tok = tokens[t];
1810
- if (tok.type === "identifier" && tok.value.startsWith("$")) {
1811
- let depth = 1;
1812
- let j = t + 1;
1813
- while (j + 1 < tokens.length) {
1814
- if (tokens[j].type === "separator" && tokens[j].value === "." && tokens[j + 1].type === "identifier") {
1815
- depth++;
1816
- j += 2;
1817
- } else {
1818
- break;
1819
- }
1820
- }
1821
- if (depth > EXPR_MAX_REF_DEPTH) {
1822
- errors.push(
1823
- createError(
1824
- "EXPR_REF_DEPTH_EXCEEDED",
1825
- `Variable reference "${tok.value}" has path depth ${depth}, exceeding maximum of ${EXPR_MAX_REF_DEPTH}.`,
1826
- path
1827
- )
1828
- );
1829
- }
1830
- }
1831
- }
1832
- for (let t = 0; t + 2 < tokens.length; t++) {
1833
- if (tokens[t].type === "separator" && tokens[t].value === "[" && tokens[t + 1].type === "number" && tokens[t + 2].type === "separator" && tokens[t + 2].value === "]") {
1834
- const indexValue = parseFloat(tokens[t + 1].value);
1835
- if (indexValue > EXPR_MAX_ARRAY_INDEX) {
1836
- errors.push(
1837
- createError(
1838
- "EXPR_ARRAY_INDEX_EXCEEDED",
1839
- `Array index ${indexValue} in expression exceeds maximum of ${EXPR_MAX_ARRAY_INDEX}.`,
1840
- path
1841
- )
1842
- );
1843
- }
1844
- }
1845
- }
1846
- return errors;
1847
- }
1848
- function validateExprConstraints(views) {
1849
- const errors = [];
1850
- traverseCard(views, (node, context) => {
1851
- const nodeFields = { ...node };
1852
- delete nodeFields.type;
1853
- delete nodeFields.style;
1854
- delete nodeFields.children;
1855
- delete nodeFields.condition;
1856
- scanForDynamicValues(nodeFields, context.path, (value, valuePath) => {
1857
- if (isRef5(value)) {
1858
- errors.push(...validateRef(value.$ref, valuePath));
1859
- } else if (isExpr4(value)) {
1860
- errors.push(...validateExpr(value.$expr, valuePath));
1861
- }
1862
- });
1863
- if (node.style) {
1864
- scanForDynamicValues(node.style, `${context.path}.style`, (value, valuePath) => {
1865
- if (isRef5(value)) {
1866
- errors.push(...validateRef(value.$ref, valuePath));
1867
- } else if (isExpr4(value)) {
1868
- errors.push(...validateExpr(value.$expr, valuePath));
1869
- }
1870
- });
1871
- }
1872
- if (node.condition !== void 0) {
1873
- scanForDynamicValues(node.condition, `${context.path}.condition`, (value, valuePath) => {
1874
- if (isRef5(value)) {
1875
- errors.push(...validateRef(value.$ref, valuePath));
1876
- } else if (isExpr4(value)) {
1877
- errors.push(...validateExpr(value.$expr, valuePath));
1878
- }
1879
- });
1880
- }
1881
- });
1882
- return errors;
1883
- }
1884
-
1885
1460
  // src/index.ts
1886
1461
  function utf8ByteLength2(str) {
1887
1462
  let bytes = 0;
@@ -1915,7 +1490,6 @@ function runAllChecks(input) {
1915
1490
  cardStyles
1916
1491
  }));
1917
1492
  errors.push(...validateLimits({ state: obj.state, views, cardStyles }));
1918
- errors.push(...validateExprConstraints(views));
1919
1493
  return errors;
1920
1494
  }
1921
1495
  function validate(input) {
@@ -1958,7 +1532,6 @@ export {
1958
1532
  traverseNode,
1959
1533
  validResult,
1960
1534
  validate,
1961
- validateExprConstraints,
1962
1535
  validateLimits,
1963
1536
  validateNodes,
1964
1537
  validateRaw,