assign-gingerly 0.0.78 → 0.0.80

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/assignGingerly.js CHANGED
@@ -420,6 +420,7 @@ export function isAllowedMethod(methodName, withMethods, permissionProcessor) {
420
420
  export function evaluatePathWithMethods(target, pathParts, value, withMethods, permissionProcessor) {
421
421
  let current = target;
422
422
  let i = 0;
423
+ let lastSegmentConsumed = false;
423
424
  // Process all segments except the last one
424
425
  while (i < pathParts.length - 1) {
425
426
  const part = pathParts[i];
@@ -433,14 +434,18 @@ export function evaluatePathWithMethods(target, pathParts, value, withMethods, p
433
434
  const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
434
435
  const method = current[methodName];
435
436
  if (typeof method === 'function') {
437
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
436
438
  if (isZeroArgMethod || nextIsMethod) {
437
439
  // Zero-arg call - next part is either a method or explicitly not an argument
438
- current = method.call(current);
440
+ current = method.call(current, ...appendArgs);
439
441
  }
440
442
  else {
441
443
  // Only current is method - call with next part as string arg
442
- current = method.call(current, nextPart);
444
+ current = method.call(current, nextPart, ...appendArgs);
443
445
  i++; // Skip next part since we consumed it as argument
446
+ if (i === pathParts.length - 1) {
447
+ lastSegmentConsumed = true;
448
+ }
444
449
  }
445
450
  }
446
451
  else {
@@ -469,7 +474,8 @@ export function evaluatePathWithMethods(target, pathParts, value, withMethods, p
469
474
  target: current,
470
475
  lastKey,
471
476
  isMethod: isAllowedMethod(lastKey, withMethods, permissionProcessor),
472
- isZeroArg
477
+ isZeroArg,
478
+ lastSegmentConsumed
473
479
  };
474
480
  }
475
481
  /**
@@ -541,8 +547,9 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
541
547
  const methodName = isZeroArgMethod ? part.slice(0, -1) : part;
542
548
  const method = current[methodName];
543
549
  if (typeof method === 'function') {
550
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
544
551
  if (isZeroArgMethod) {
545
- current = method.call(current);
552
+ current = method.call(current, ...appendArgs);
546
553
  continue;
547
554
  }
548
555
  // For methods in the middle, we need to check the next part
@@ -551,15 +558,15 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
551
558
  const nextIsMethod = nextPart && (withMethods.has(nextPart)
552
559
  || (nextPart.endsWith('|') && withMethods.has(nextPart.slice(0, -1))));
553
560
  if (nextIsMethod) {
554
- current = method.call(current);
561
+ current = method.call(current, ...appendArgs);
555
562
  }
556
563
  else if (nextPart) {
557
- current = method.call(current, nextPart);
564
+ current = method.call(current, nextPart, ...appendArgs);
558
565
  // Skip next part
559
566
  pathToForEach.splice(nextIndex, 1);
560
567
  }
561
568
  else {
562
- current = method.call(current);
569
+ current = method.call(current, ...appendArgs);
563
570
  }
564
571
  }
565
572
  else {
@@ -582,15 +589,16 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
582
589
  // Last segment is a method - call it
583
590
  const method = result.target[result.lastKey];
584
591
  if (typeof method === 'function') {
592
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
585
593
  if (result.isZeroArg) {
586
- // Trailing | marker - call with no arguments, ignoring the value
587
- method.call(result.target);
594
+ // Trailing | marker - call with appended args only, ignoring the value
595
+ method.call(result.target, ...appendArgs);
588
596
  }
589
597
  else if (Array.isArray(value)) {
590
- method.apply(result.target, value);
598
+ method.apply(result.target, [...value, ...appendArgs]);
591
599
  }
592
600
  else {
593
- method.call(result.target, value);
601
+ method.call(result.target, value, ...appendArgs);
594
602
  }
595
603
  }
596
604
  }
@@ -620,6 +628,95 @@ function applyToEach(iterable, remainingPath, value, withMethods, aliasMap, opti
620
628
  }
621
629
  }
622
630
  }
631
+ /**
632
+ * Resolve the RHS of a toggle (=!) command to the value that should be negated.
633
+ * Supports:
634
+ * - '?.path' nested path strings resolved against target (returns the resolved value, or true if missing)
635
+ * - plain key strings looked up on target (returns the value, or true if missing)
636
+ * - resolved literal values (e.g., booleans from assignFrom) returned as-is
637
+ */
638
+ function resolveValueToNegate(rhsPath, target) {
639
+ if (typeof rhsPath === 'string' && isNestedPath(rhsPath)) {
640
+ const rhsPathParts = parsePath(rhsPath);
641
+ let current = target;
642
+ let exists = true;
643
+ for (const part of rhsPathParts) {
644
+ if (current && typeof current === 'object' && part in current) {
645
+ current = current[part];
646
+ }
647
+ else {
648
+ exists = false;
649
+ break;
650
+ }
651
+ }
652
+ return exists ? current : true;
653
+ }
654
+ if (typeof rhsPath === 'string') {
655
+ return (rhsPath in target) ? target[rhsPath] : true;
656
+ }
657
+ // Non-string resolved value (e.g., boolean, number, null from assignFrom)
658
+ return rhsPath;
659
+ }
660
+ /**
661
+ * Navigate a path that leads to an iterable, returning the iterable value or undefined.
662
+ * Handles withMethods and the evaluatePathWithMethods semantics.
663
+ */
664
+ function getIterableAtPath(target, pathParts, value, withMethods, permissionProcessor) {
665
+ let current = target;
666
+ if (pathParts.length > 0) {
667
+ if (withMethods && withMethods.size > 0) {
668
+ const result = evaluatePathWithMethods(target, pathParts, value, withMethods, permissionProcessor);
669
+ // evaluatePathWithMethods returns the container object + last key by default.
670
+ // If the last segment was consumed as a method argument, or the last segment
671
+ // is a zero-arg method marked with |, result.target is already the value.
672
+ if (result.lastSegmentConsumed) {
673
+ current = result.target;
674
+ }
675
+ else if (result.isMethod) {
676
+ const method = result.target[result.lastKey];
677
+ if (typeof method === 'function') {
678
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
679
+ current = method.call(result.target, ...appendArgs);
680
+ }
681
+ else {
682
+ current = method;
683
+ }
684
+ }
685
+ else {
686
+ current = result.target[result.lastKey];
687
+ }
688
+ }
689
+ else {
690
+ for (const part of pathParts) {
691
+ current = current[part];
692
+ }
693
+ }
694
+ }
695
+ return isIterable(current) ? current : undefined;
696
+ }
697
+ /**
698
+ * Apply an operator command (+=, =!, -=, Y=) to each item in an iterable.
699
+ * Detects @each in the path, navigates to the iterable, then builds a synthetic
700
+ * command key for the remaining path and delegates to assignGingerly per item.
701
+ * Nested @each is handled recursively through assignGingerly.
702
+ */
703
+ function applyCommandToEach(target, pathParts, commandSuffix, value, withMethods, aliasMap, options, permissionProcessor) {
704
+ const forEachIndex = pathParts.findIndex(part => isForEachSymbol(part, aliasMap));
705
+ if (forEachIndex === -1)
706
+ return;
707
+ const pathToForEach = pathParts.slice(0, forEachIndex);
708
+ const pathAfterForEach = pathParts.slice(forEachIndex + 1);
709
+ const iterable = getIterableAtPath(target, pathToForEach, value, withMethods, permissionProcessor);
710
+ if (!iterable)
711
+ return;
712
+ const items = Array.isArray(iterable) ? iterable : Array.from(iterable);
713
+ const syntheticKey = pathAfterForEach.length > 0
714
+ ? `?.${pathAfterForEach.join('?.')}${commandSuffix}`
715
+ : commandSuffix;
716
+ for (const item of items) {
717
+ assignGingerly(item, { [syntheticKey]: value }, options, permissionProcessor);
718
+ }
719
+ }
623
720
  /**
624
721
  * Apply alias substitutions to a key string.
625
722
  * Replaces complete tokens between `?.` delimiters with their aliased values.
@@ -724,6 +821,11 @@ export function assignGingerly(target, source, options, permissionProcessor) {
724
821
  if (path) {
725
822
  if (isNestedPath(path)) {
726
823
  const pathParts = parsePath(path);
824
+ // Check for @each in path
825
+ if (pathParts.some(part => isForEachSymbol(part, aliasMap))) {
826
+ applyCommandToEach(target, pathParts, ' +=', value, withMethodsSet, aliasMap, options, permissionProcessor);
827
+ continue;
828
+ }
727
829
  // Check for withMethods path evaluation
728
830
  let lhsValue;
729
831
  let lhsParent;
@@ -792,54 +894,47 @@ export function assignGingerly(target, source, options, permissionProcessor) {
792
894
  const lhsPath = parseToggleCommand(key);
793
895
  if (lhsPath) {
794
896
  const rhsPath = value;
795
- // Resolve LHS
796
- let lhsParent;
797
- let lhsLastKey;
798
897
  if (isNestedPath(lhsPath)) {
799
898
  const lhsPathParts = parsePath(lhsPath);
800
- lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
801
- lhsParent = ensureNestedPath(target, lhsPathParts);
802
- }
803
- else {
804
- lhsLastKey = lhsPath;
805
- lhsParent = target;
806
- }
807
- // Determine what to negate
808
- let valueToNegate;
809
- if (rhsPath === '.') {
810
- // Self-reference: negate the LHS value itself (if it exists)
811
- if (lhsLastKey in lhsParent) {
812
- valueToNegate = lhsParent[lhsLastKey];
899
+ // Check for @each in the LHS path
900
+ if (lhsPathParts.some(part => isForEachSymbol(part, aliasMap))) {
901
+ // Resolve non-self-referencing RHS paths against the original target
902
+ // before iterating, so each item negates the same root value.
903
+ const resolvedValue = (rhsPath === '.' || typeof rhsPath !== 'string')
904
+ ? rhsPath
905
+ : resolveValueToNegate(rhsPath, target);
906
+ applyCommandToEach(target, lhsPathParts, ' =!', resolvedValue, withMethodsSet, aliasMap, options, permissionProcessor);
907
+ continue;
908
+ }
909
+ // No @each in path - standard toggle
910
+ const lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
911
+ const lhsParent = ensureNestedPath(target, lhsPathParts);
912
+ // Determine what to negate
913
+ let valueToNegate;
914
+ if (rhsPath === '.') {
915
+ valueToNegate = (lhsLastKey in lhsParent) ? lhsParent[lhsLastKey] : undefined;
813
916
  }
814
917
  else {
815
- valueToNegate = undefined;
918
+ valueToNegate = resolveValueToNegate(rhsPath, target);
919
+ }
920
+ if (!permissionProcessor?.checkRestrictedProp(lhsLastKey)) {
921
+ lhsParent[lhsLastKey] = !valueToNegate;
816
922
  }
817
923
  }
818
924
  else {
819
- // RHS path: navigate to get the value (don't create paths)
820
- if (isNestedPath(rhsPath)) {
821
- const rhsPathParts = parsePath(rhsPath);
822
- let current = target;
823
- let exists = true;
824
- for (const part of rhsPathParts) {
825
- if (current && typeof current === 'object' && part in current) {
826
- current = current[part];
827
- }
828
- else {
829
- exists = false;
830
- break;
831
- }
832
- }
833
- valueToNegate = exists ? current : true;
925
+ // Plain key LHS
926
+ const lhsLastKey = lhsPath;
927
+ const lhsParent = target;
928
+ let valueToNegate;
929
+ if (rhsPath === '.') {
930
+ valueToNegate = (lhsLastKey in lhsParent) ? lhsParent[lhsLastKey] : undefined;
834
931
  }
835
932
  else {
836
- // Plain key RHS
837
- valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
933
+ valueToNegate = resolveValueToNegate(rhsPath, target);
934
+ }
935
+ if (!permissionProcessor?.checkRestrictedProp(lhsLastKey)) {
936
+ lhsParent[lhsLastKey] = !valueToNegate;
838
937
  }
839
- }
840
- // Apply negation to LHS — check restriction first
841
- if (!permissionProcessor?.checkRestrictedProp(lhsLastKey)) {
842
- lhsParent[lhsLastKey] = !valueToNegate;
843
938
  }
844
939
  }
845
940
  continue;
@@ -853,6 +948,11 @@ export function assignGingerly(target, source, options, permissionProcessor) {
853
948
  let canDelete = true;
854
949
  if (isNestedPath(path)) {
855
950
  const pathParts = parsePath(path);
951
+ // Check for @each in path
952
+ if (pathParts.some(part => isForEachSymbol(part, aliasMap))) {
953
+ applyCommandToEach(target, pathParts, ' -=', value, withMethodsSet, aliasMap, options, permissionProcessor);
954
+ continue;
955
+ }
856
956
  if (pathParts.length === 0) {
857
957
  parent = target;
858
958
  }
@@ -898,6 +998,11 @@ export function assignGingerly(target, source, options, permissionProcessor) {
898
998
  if (permissionProcessor?.checkRestrictedProp(lastKey)) {
899
999
  continue;
900
1000
  }
1001
+ // Check for @each in path
1002
+ if (isNestedPath(path) && pathParts.some(part => isForEachSymbol(part, aliasMap))) {
1003
+ applyCommandToEach(target, pathParts, ' Y=', value, withMethodsSet, aliasMap, options, permissionProcessor);
1004
+ continue;
1005
+ }
901
1006
  // Navigate to the target sub-object
902
1007
  let mergeTarget;
903
1008
  if (isNestedPath(path)) {
@@ -953,22 +1058,9 @@ export function assignGingerly(target, source, options, permissionProcessor) {
953
1058
  const pathToForEach = pathParts.slice(0, forEachIndex);
954
1059
  const pathAfterForEach = pathParts.slice(forEachIndex + 1);
955
1060
  // Navigate to the iterable
956
- let current = target;
957
- if (pathToForEach.length > 0) {
958
- if (withMethodsSet) {
959
- const result = evaluatePathWithMethods(target, pathToForEach, value, withMethodsSet, permissionProcessor);
960
- // The result.target is the current position after evaluating the path
961
- // This is already the iterable we want
962
- current = result.target;
963
- }
964
- else {
965
- for (const part of pathToForEach) {
966
- current = current[part];
967
- }
968
- }
969
- }
1061
+ const current = getIterableAtPath(target, pathToForEach, value, withMethodsSet, permissionProcessor);
970
1062
  // Apply to each item in the iterable
971
- if (isIterable(current)) {
1063
+ if (current) {
972
1064
  applyToEach(current, pathAfterForEach, value, withMethodsSet || new Set(), aliasMap, options, permissionProcessor);
973
1065
  }
974
1066
  // If not iterable, let JavaScript throw error naturally when trying to iterate
@@ -991,12 +1083,13 @@ export function assignGingerly(target, source, options, permissionProcessor) {
991
1083
  // Last segment is a method — call it
992
1084
  const method = result.target[result.lastKey];
993
1085
  if (typeof method === 'function') {
994
- // Trailing | marker - call with no arguments, ignoring the value
1086
+ const appendArgs = capturedPermissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
1087
+ // Trailing | marker - call with appended args only, ignoring the value
995
1088
  const returnVal = result.isZeroArg
996
- ? method.call(result.target)
1089
+ ? method.call(result.target, ...appendArgs)
997
1090
  : Array.isArray(capturedValue)
998
- ? method.apply(result.target, capturedValue)
999
- : method.call(result.target, capturedValue);
1091
+ ? method.apply(result.target, [...capturedValue, ...appendArgs])
1092
+ : method.call(result.target, capturedValue, ...appendArgs);
1000
1093
  // If it's an async method, await it (for side effects)
1001
1094
  if (result.isAsyncMethod)
1002
1095
  await returnVal;
@@ -1035,15 +1128,16 @@ export function assignGingerly(target, source, options, permissionProcessor) {
1035
1128
  // Last segment is a method - call it
1036
1129
  const method = result.target[result.lastKey];
1037
1130
  if (typeof method === 'function') {
1131
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(result.lastKey) ?? [];
1038
1132
  if (result.isZeroArg) {
1039
- // Trailing | marker - call with no arguments, ignoring the value
1040
- method.call(result.target);
1133
+ // Trailing | marker - call with appended args only, ignoring the value
1134
+ method.call(result.target, ...appendArgs);
1041
1135
  }
1042
1136
  else if (Array.isArray(value)) {
1043
- method.apply(result.target, value);
1137
+ method.apply(result.target, [...value, ...appendArgs]);
1044
1138
  }
1045
1139
  else {
1046
- method.call(result.target, value);
1140
+ method.call(result.target, value, ...appendArgs);
1047
1141
  }
1048
1142
  }
1049
1143
  // Silently skip if not a function
@@ -1117,15 +1211,16 @@ export function assignGingerly(target, source, options, permissionProcessor) {
1117
1211
  const methodName = isZeroArgKey ? key.slice(0, -1) : key;
1118
1212
  const method = target[methodName];
1119
1213
  if (typeof method === 'function') {
1214
+ const appendArgs = permissionProcessor?.getMethodAppendArgs(methodName) ?? [];
1120
1215
  if (isZeroArgKey) {
1121
- // Trailing | marker - call with no arguments, ignoring the value
1122
- method.call(target);
1216
+ // Trailing | marker - call with appended args only, ignoring the value
1217
+ method.call(target, ...appendArgs);
1123
1218
  }
1124
1219
  else if (Array.isArray(value)) {
1125
- method.apply(target, value);
1220
+ method.apply(target, [...value, ...appendArgs]);
1126
1221
  }
1127
1222
  else {
1128
- method.call(target, value);
1223
+ method.call(target, value, ...appendArgs);
1129
1224
  }
1130
1225
  }
1131
1226
  // Silently skip if not a function