@safe-ugc-ui/validator 0.3.1 → 0.5.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",
@@ -282,45 +285,8 @@ var STATIC_ONLY_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
282
285
  "borderLeft",
283
286
  // Shadow
284
287
  "boxShadow",
285
- // Stacking
286
- "zIndex"
288
+ "textShadow"
287
289
  ]);
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
290
  function validateNodeStyle(node, ctx, errors) {
325
291
  const style = node.style;
326
292
  if (!style) {
@@ -330,12 +296,21 @@ function validateNodeStyle(node, ctx, errors) {
330
296
  if (value === void 0) {
331
297
  continue;
332
298
  }
333
- if (STATIC_ONLY_STYLE_PROPERTIES.has(prop)) {
334
- if (isRef(value) || isExpr(value)) {
299
+ if (typeof value === "object" && value !== null && "$ref" in value && typeof value.$ref === "string") {
300
+ if (STATIC_ONLY_STYLE_PROPERTIES.has(prop)) {
335
301
  errors.push(
336
302
  createError(
337
303
  "DYNAMIC_NOT_ALLOWED",
338
- `Style property "${prop}" must be a static literal; $ref and $expr are not allowed.`,
304
+ `Style property "${prop}" must be a static literal; $ref is not allowed.`,
305
+ `${ctx.path}.style.${prop}`
306
+ )
307
+ );
308
+ }
309
+ if (STRUCTURED_OBJECT_STYLE_PROPERTIES.has(prop)) {
310
+ errors.push(
311
+ createError(
312
+ "DYNAMIC_NOT_ALLOWED",
313
+ `Style property "${prop}" must be an object literal; use $ref only inside its nested fields.`,
339
314
  `${ctx.path}.style.${prop}`
340
315
  )
341
316
  );
@@ -346,7 +321,6 @@ function validateNodeStyle(node, ctx, errors) {
346
321
  function validateValueTypes(views) {
347
322
  const errors = [];
348
323
  traverseCard(views, (node, ctx) => {
349
- validateNodeFields(node, ctx, errors);
350
324
  validateNodeStyle(node, ctx, errors);
351
325
  });
352
326
  return errors;
@@ -364,6 +338,8 @@ import {
364
338
  TRANSFORM_TRANSLATE_MAX,
365
339
  FONT_SIZE_MIN,
366
340
  FONT_SIZE_MAX,
341
+ TEXT_SHADOW_MAX_COUNT,
342
+ TEXT_SHADOW_BLUR_MAX,
367
343
  BOX_SHADOW_MAX_COUNT,
368
344
  BOX_SHADOW_BLUR_MAX,
369
345
  BOX_SHADOW_SPREAD_MAX,
@@ -377,8 +353,7 @@ import {
377
353
  TRANSITION_DELAY_MAX,
378
354
  TRANSITION_MAX_COUNT,
379
355
  ALLOWED_TRANSITION_PROPERTIES,
380
- isRef as isRef2,
381
- isExpr as isExpr2
356
+ isRef
382
357
  } from "@safe-ugc-ui/types";
383
358
  var COLOR_PROPERTIES = /* @__PURE__ */ new Set(["backgroundColor", "color"]);
384
359
  var LENGTH_PROPERTIES = /* @__PURE__ */ new Set([
@@ -434,7 +409,7 @@ function isLiteralString(value) {
434
409
  return typeof value === "string";
435
410
  }
436
411
  function isDynamic(value) {
437
- return isRef2(value) || isExpr2(value);
412
+ return isRef(value);
438
413
  }
439
414
  function isValidColor(value) {
440
415
  const lower = value.toLowerCase();
@@ -479,7 +454,7 @@ function collectDangerousCssErrors(value, path, errors) {
479
454
  }
480
455
  return;
481
456
  }
482
- if (isRef2(value) || isExpr2(value)) {
457
+ if (isRef(value)) {
483
458
  return;
484
459
  }
485
460
  if (Array.isArray(value)) {
@@ -523,8 +498,87 @@ function validateShadowObject(shadow, path, errors) {
523
498
  );
524
499
  }
525
500
  }
501
+ function validateTextShadowObject(shadow, path, errors) {
502
+ if (isLiteralNumber(shadow.blur) && shadow.blur > TEXT_SHADOW_BLUR_MAX) {
503
+ errors.push(
504
+ createError(
505
+ "STYLE_VALUE_OUT_OF_RANGE",
506
+ `textShadow blur (${shadow.blur}) exceeds maximum of ${TEXT_SHADOW_BLUR_MAX} at "${path}.blur"`,
507
+ `${path}.blur`
508
+ )
509
+ );
510
+ }
511
+ if (isLiteralString(shadow.color) && !isValidColor(shadow.color)) {
512
+ errors.push(
513
+ createError(
514
+ "INVALID_COLOR",
515
+ `Invalid color "${shadow.color}" at "${path}.color"`,
516
+ `${path}.color`
517
+ )
518
+ );
519
+ }
520
+ }
526
521
  var STYLE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
527
- function validateSingleStyle(style, stylePath, errors) {
522
+ function reportStyleRefError(rawRef, stylePath, cardStyles, errors) {
523
+ const trimmedRef = rawRef.trim();
524
+ if (!STYLE_NAME_PATTERN.test(trimmedRef)) {
525
+ errors.push(
526
+ createError(
527
+ "INVALID_STYLE_REF",
528
+ `$style value "${rawRef}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/ at "${stylePath}.$style"`,
529
+ `${stylePath}.$style`
530
+ )
531
+ );
532
+ return void 0;
533
+ }
534
+ if (!cardStyles || !(trimmedRef in cardStyles)) {
535
+ errors.push(
536
+ createError(
537
+ "STYLE_REF_NOT_FOUND",
538
+ `$style references "${trimmedRef}" which is not defined in card.styles at "${stylePath}.$style"`,
539
+ `${stylePath}.$style`
540
+ )
541
+ );
542
+ return void 0;
543
+ }
544
+ return trimmedRef;
545
+ }
546
+ function resolveStyleRef(style, stylePath, cardStyles, errors) {
547
+ if (typeof style.$style !== "string") {
548
+ return style;
549
+ }
550
+ const trimmedRef = reportStyleRefError(style.$style, stylePath, cardStyles, errors);
551
+ if (!trimmedRef || !cardStyles) {
552
+ return void 0;
553
+ }
554
+ return mergeStyleWithRef(cardStyles[trimmedRef], style);
555
+ }
556
+ function collectNestedStyleRefErrors(value, path, errors) {
557
+ if (Array.isArray(value)) {
558
+ for (let i = 0; i < value.length; i++) {
559
+ collectNestedStyleRefErrors(value[i], `${path}[${i}]`, errors);
560
+ }
561
+ return;
562
+ }
563
+ if (typeof value !== "object" || value === null) {
564
+ return;
565
+ }
566
+ for (const [key, child] of Object.entries(value)) {
567
+ const childPath = `${path}.${key}`;
568
+ if (key === "$style") {
569
+ errors.push(
570
+ createError(
571
+ "STYLE_CIRCULAR_REF",
572
+ `$style cannot be used inside card.styles definitions at "${childPath}"`,
573
+ childPath
574
+ )
575
+ );
576
+ continue;
577
+ }
578
+ collectNestedStyleRefErrors(child, childPath, errors);
579
+ }
580
+ }
581
+ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverStyleRefs = true) {
528
582
  const STRUCTURED_FIELDS = /* @__PURE__ */ new Set(["transition", "hoverStyle"]);
529
583
  for (const key of Object.keys(style)) {
530
584
  if (!STRUCTURED_FIELDS.has(key) && FORBIDDEN_STYLE_PROPERTIES.includes(key)) {
@@ -685,6 +739,37 @@ function validateSingleStyle(style, stylePath, errors) {
685
739
  );
686
740
  }
687
741
  }
742
+ if ("textShadow" in style && style.textShadow != null) {
743
+ const textShadow = style.textShadow;
744
+ const textShadowPath = `${stylePath}.textShadow`;
745
+ if (Array.isArray(textShadow)) {
746
+ if (textShadow.length > TEXT_SHADOW_MAX_COUNT) {
747
+ errors.push(
748
+ createError(
749
+ "STYLE_VALUE_OUT_OF_RANGE",
750
+ `textShadow has ${textShadow.length} entries, maximum is ${TEXT_SHADOW_MAX_COUNT} at "${textShadowPath}"`,
751
+ textShadowPath
752
+ )
753
+ );
754
+ }
755
+ for (let i = 0; i < textShadow.length; i++) {
756
+ const shadow = textShadow[i];
757
+ if (typeof shadow === "object" && shadow !== null) {
758
+ validateTextShadowObject(
759
+ shadow,
760
+ `${textShadowPath}[${i}]`,
761
+ errors
762
+ );
763
+ }
764
+ }
765
+ } else if (typeof textShadow === "object" && textShadow !== null) {
766
+ validateTextShadowObject(
767
+ textShadow,
768
+ textShadowPath,
769
+ errors
770
+ );
771
+ }
772
+ }
688
773
  for (const [key, value] of Object.entries(style)) {
689
774
  collectDangerousCssErrors(value, `${stylePath}.${key}`, errors);
690
775
  }
@@ -809,16 +894,16 @@ function validateSingleStyle(style, stylePath, errors) {
809
894
  )
810
895
  );
811
896
  }
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
- )
897
+ const mergedHoverStyle = allowHoverStyleRefs ? resolveStyleRef(hoverObj, hoverPath, cardStyles, errors) : hoverObj;
898
+ if (mergedHoverStyle) {
899
+ validateSingleStyle(
900
+ mergedHoverStyle,
901
+ hoverPath,
902
+ errors,
903
+ cardStyles,
904
+ allowHoverStyleRefs
819
905
  );
820
906
  }
821
- validateSingleStyle(hoverObj, hoverPath, errors);
822
907
  }
823
908
  }
824
909
  if ("transition" in style && style.transition != null) {
@@ -906,16 +991,8 @@ function validateStyles(views, cardStyles) {
906
991
  )
907
992
  );
908
993
  }
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);
994
+ collectNestedStyleRefErrors(styleEntry, entryPath, errors);
995
+ validateSingleStyle(styleEntry, entryPath, errors, void 0, false);
919
996
  }
920
997
  }
921
998
  traverseCard(views, (node, ctx) => {
@@ -924,33 +1001,9 @@ function validateStyles(views, cardStyles) {
924
1001
  return;
925
1002
  }
926
1003
  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);
1004
+ const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
1005
+ if (mergedStyle) {
1006
+ validateSingleStyle(mergedStyle, stylePath, errors, cardStyles);
954
1007
  }
955
1008
  });
956
1009
  return errors;
@@ -959,7 +1012,7 @@ function validateStyles(views, cardStyles) {
959
1012
  // src/security.ts
960
1013
  import {
961
1014
  PROTOTYPE_POLLUTION_SEGMENTS,
962
- isRef as isRef3
1015
+ isRef as isRef2
963
1016
  } from "@safe-ugc-ui/types";
964
1017
  var FORBIDDEN_URL_PREFIXES = [
965
1018
  "http://",
@@ -972,7 +1025,7 @@ function scanForRefs(obj, path, errors) {
972
1025
  if (obj === null || obj === void 0 || typeof obj !== "object") {
973
1026
  return;
974
1027
  }
975
- if (isRef3(obj)) {
1028
+ if (isRef2(obj)) {
976
1029
  const refStr = obj.$ref;
977
1030
  const segments = refStr.split(/[.\[]/);
978
1031
  for (const segment of segments) {
@@ -1109,12 +1162,11 @@ function validateSecurity(card) {
1109
1162
  delete nodeFields.type;
1110
1163
  delete nodeFields.style;
1111
1164
  delete nodeFields.children;
1112
- delete nodeFields.condition;
1113
1165
  if (type === "Image" || type === "Avatar") {
1114
1166
  const src = node.src;
1115
1167
  if (typeof src === "string") {
1116
1168
  checkSrcValue(src, type, `${path}.src`, errors);
1117
- } else if (isRef3(src)) {
1169
+ } else if (isRef2(src)) {
1118
1170
  if (state) {
1119
1171
  const resolved = resolveRefFromState(
1120
1172
  src.$ref,
@@ -1207,8 +1259,7 @@ import {
1207
1259
  MAX_OVERFLOW_AUTO_COUNT,
1208
1260
  MAX_STACK_NESTING,
1209
1261
  PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2,
1210
- isRef as isRef4,
1211
- isExpr as isExpr3
1262
+ isRef as isRef3
1212
1263
  } from "@safe-ugc-ui/types";
1213
1264
  function utf8ByteLength(str) {
1214
1265
  let bytes = 0;
@@ -1269,7 +1320,7 @@ function countTemplateMetrics(template, cardStyles) {
1269
1320
  result.nodes = 1;
1270
1321
  if (node.type === "Text") {
1271
1322
  const content = node.content;
1272
- if (typeof content === "string" && !isRef4(content) && !isExpr3(content)) {
1323
+ if (typeof content === "string" && !isRef3(content)) {
1273
1324
  result.textBytes = utf8ByteLength(content);
1274
1325
  }
1275
1326
  }
@@ -1325,7 +1376,7 @@ function validateLimits(card) {
1325
1376
  nodeCount++;
1326
1377
  if (node.type === "Text") {
1327
1378
  const content = node.content;
1328
- if (typeof content === "string" && !isRef4(content) && !isExpr3(content)) {
1379
+ if (typeof content === "string" && !isRef3(content)) {
1329
1380
  textContentBytes += utf8ByteLength(content);
1330
1381
  }
1331
1382
  }
@@ -1460,428 +1511,6 @@ function validateLimits(card) {
1460
1511
  return errors;
1461
1512
  }
1462
1513
 
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
1514
  // src/index.ts
1886
1515
  function utf8ByteLength2(str) {
1887
1516
  let bytes = 0;
@@ -1915,7 +1544,6 @@ function runAllChecks(input) {
1915
1544
  cardStyles
1916
1545
  }));
1917
1546
  errors.push(...validateLimits({ state: obj.state, views, cardStyles }));
1918
- errors.push(...validateExprConstraints(views));
1919
1547
  return errors;
1920
1548
  }
1921
1549
  function validate(input) {
@@ -1958,7 +1586,6 @@ export {
1958
1586
  traverseNode,
1959
1587
  validResult,
1960
1588
  validate,
1961
- validateExprConstraints,
1962
1589
  validateLimits,
1963
1590
  validateNodes,
1964
1591
  validateRaw,