assign-gingerly 0.0.33 → 0.0.35

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/README.md CHANGED
@@ -734,7 +734,7 @@ console.log(obj);
734
734
  // }
735
735
  ```
736
736
 
737
- The `+=` command syntax is `<path> +=` where the path can use the `?.` nested notation. The right-hand side value is added to the existing value using `+=`. If the path doesn't exist, it's created and set directly to the value. If the expression is a string, string concatenation is used. If the expression can't be "added to", it allows JavaScript to throw its natural error.
737
+ The `+=` command syntax is `<path> +=` where the path uses the `?.` nested notation for nested properties, or a plain key for direct properties. The right-hand side value is added to the existing value using `+=`. If the path doesn't exist, it's created and set directly to the value. If the expression is a string, string concatenation is used. If the expression can't be "added to", it allows JavaScript to throw its natural error.
738
738
 
739
739
  ## Example 5 - Toggling boolean values and negating
740
740
 
@@ -764,7 +764,7 @@ console.log(obj);
764
764
  // }
765
765
  ```
766
766
 
767
- The `=!` command syntax is `<path> =!` where the path can use the `?.` nested notation.
767
+ The `=!` command syntax is `<path> =!` where the path uses the `?.` nested notation for nested properties, or a plain key for direct properties.
768
768
 
769
769
  For existing values, the toggle is performed using JavaScript's logical NOT operator (`!value`), regardless of what type it is.
770
770
 
@@ -3303,6 +3303,28 @@ console.log(result);
3303
3303
 
3304
3304
  -->
3305
3305
 
3306
+ ## Resolving and Assigning with `assignFrom`
3307
+
3308
+ The `assignFrom` function combines RHS path resolution with `assignGingerly` in a single call. It resolves `?.`-prefixed RHS values against a source object, then assigns the results into a target. Use `?.` alone as a RHS value to reference the entire source object itself.
3309
+
3310
+ ```TypeScript
3311
+ import { assignFrom } from 'assign-gingerly/assignFrom.js';
3312
+
3313
+ const viewModel = { username: 'Alice', clone: someDocumentFragment };
3314
+
3315
+ assignFrom(target, {
3316
+ '?.appendChild': '?..clone', // source.clone
3317
+ '?.clone?.q?..username?.textContent': '?.username', // source.username
3318
+ ref: '?.' // the source object itself
3319
+ }, {
3320
+ from: viewModel,
3321
+ withMethods: ['appendChild'],
3322
+ aka: { 'q': 'querySelector' }
3323
+ });
3324
+ ```
3325
+
3326
+ For full documentation, see [docs/assignFrom.md](docs/assignFrom.md).
3327
+
3306
3328
  ## Itemscope Managers (Chrome 146+)
3307
3329
 
3308
3330
  Itemscope Managers provide a way to manage DOM fragments and their associated data/view models for elements with the `itemscope` attribute. This feature enables frameworks and libraries to manage light children of web components, DOM fragments from looping constructs, and scenarios where custom element wrapping is not feasible.
package/assignGingerly.js CHANGED
@@ -201,11 +201,13 @@ function parseDeleteCommand(key) {
201
201
  }
202
202
  /**
203
203
  * Helper function to parse a path string with ?. notation
204
+ * Always splits on '?.' delimiter, preserving dots that are part of values
205
+ * (e.g., CSS class selectors like '.username')
206
+ * Paths must use ?. notation — plain dot notation is not supported.
204
207
  */
205
208
  function parsePath(path) {
206
209
  return path
207
- .split('.')
208
- .map(part => part.replace(/\?/g, ''))
210
+ .split('?.')
209
211
  .filter(part => part.length > 0);
210
212
  }
211
213
  /**
@@ -572,16 +574,25 @@ export function assignGingerly(target, source, options) {
572
574
  if (isIncCommand(key)) {
573
575
  const path = parseIncCommand(key);
574
576
  if (path) {
575
- const pathParts = parsePath(path);
576
- const lastKey = pathParts[pathParts.length - 1];
577
- const parent = ensureNestedPath(target, pathParts);
578
- // If the path doesn't exist, set it directly to the value
579
- if (!(lastKey in parent)) {
580
- parent[lastKey] = value;
577
+ if (isNestedPath(path)) {
578
+ const pathParts = parsePath(path);
579
+ const lastKey = pathParts[pathParts.length - 1];
580
+ const parent = ensureNestedPath(target, pathParts);
581
+ if (!(lastKey in parent)) {
582
+ parent[lastKey] = value;
583
+ }
584
+ else {
585
+ parent[lastKey] += value;
586
+ }
581
587
  }
582
588
  else {
583
- // Path exists, apply increment: oldValue += newValue
584
- parent[lastKey] += value;
589
+ // Plain key - direct operation on target
590
+ if (!(path in target)) {
591
+ target[path] = value;
592
+ }
593
+ else {
594
+ target[path] += value;
595
+ }
585
596
  }
586
597
  }
587
598
  continue;
@@ -591,10 +602,18 @@ export function assignGingerly(target, source, options) {
591
602
  const lhsPath = parseToggleCommand(key);
592
603
  if (lhsPath) {
593
604
  const rhsPath = value;
594
- // Parse LHS path
595
- const lhsPathParts = parsePath(lhsPath);
596
- const lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
597
- const lhsParent = ensureNestedPath(target, lhsPathParts);
605
+ // Resolve LHS
606
+ let lhsParent;
607
+ let lhsLastKey;
608
+ if (isNestedPath(lhsPath)) {
609
+ const lhsPathParts = parsePath(lhsPath);
610
+ lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
611
+ lhsParent = ensureNestedPath(target, lhsPathParts);
612
+ }
613
+ else {
614
+ lhsLastKey = lhsPath;
615
+ lhsParent = target;
616
+ }
598
617
  // Determine what to negate
599
618
  let valueToNegate;
600
619
  if (rhsPath === '.') {
@@ -603,26 +622,30 @@ export function assignGingerly(target, source, options) {
603
622
  valueToNegate = lhsParent[lhsLastKey];
604
623
  }
605
624
  else {
606
- // LHS doesn't exist, treat as undefined -> !undefined = true
607
625
  valueToNegate = undefined;
608
626
  }
609
627
  }
610
628
  else {
611
629
  // RHS path: navigate to get the value (don't create paths)
612
- const rhsPathParts = parsePath(rhsPath);
613
- let current = target;
614
- let exists = true;
615
- for (const part of rhsPathParts) {
616
- if (current && typeof current === 'object' && part in current) {
617
- current = current[part];
618
- }
619
- else {
620
- exists = false;
621
- break;
630
+ if (isNestedPath(rhsPath)) {
631
+ const rhsPathParts = parsePath(rhsPath);
632
+ let current = target;
633
+ let exists = true;
634
+ for (const part of rhsPathParts) {
635
+ if (current && typeof current === 'object' && part in current) {
636
+ current = current[part];
637
+ }
638
+ else {
639
+ exists = false;
640
+ break;
641
+ }
622
642
  }
643
+ valueToNegate = exists ? current : true;
644
+ }
645
+ else {
646
+ // Plain key RHS
647
+ valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
623
648
  }
624
- // If RHS doesn't exist, treat as truthy (will become false)
625
- valueToNegate = exists ? current : true;
626
649
  }
627
650
  // Apply negation to LHS
628
651
  lhsParent[lhsLastKey] = !valueToNegate;
@@ -633,28 +656,37 @@ export function assignGingerly(target, source, options) {
633
656
  if (isDeleteCommand(key)) {
634
657
  const path = parseDeleteCommand(key);
635
658
  if (path !== null) {
636
- const pathParts = parsePath(path);
637
659
  // Determine the parent object
638
660
  let parent = target;
639
661
  let canDelete = true;
640
- // If path is empty or just '?', delete from root
641
- if (pathParts.length === 0) {
642
- parent = target;
643
- }
644
- else {
645
- // Navigate to parent object
646
- for (const part of pathParts) {
647
- if (parent && typeof parent === 'object' && part in parent) {
648
- parent = parent[part];
649
- }
650
- else {
651
- canDelete = false;
652
- break;
662
+ if (isNestedPath(path)) {
663
+ const pathParts = parsePath(path);
664
+ if (pathParts.length === 0) {
665
+ parent = target;
666
+ }
667
+ else {
668
+ for (const part of pathParts) {
669
+ if (parent && typeof parent === 'object' && part in parent) {
670
+ parent = parent[part];
671
+ }
672
+ else {
673
+ canDelete = false;
674
+ break;
675
+ }
653
676
  }
654
677
  }
655
678
  }
679
+ else if (path.length > 0) {
680
+ // Plain key - navigate one level
681
+ if (parent && typeof parent === 'object' && path in parent) {
682
+ parent = parent[path];
683
+ }
684
+ else {
685
+ canDelete = false;
686
+ }
687
+ }
688
+ // else: empty path = delete from root
656
689
  if (canDelete && typeof parent === 'object' && parent !== null) {
657
- // RHS can be a string (single property) or array (multiple properties)
658
690
  const propertiesToDelete = Array.isArray(value) ? value : [value];
659
691
  for (const prop of propertiesToDelete) {
660
692
  if (prop in parent) {
package/assignGingerly.ts CHANGED
@@ -326,11 +326,13 @@ function parseDeleteCommand(key: string): string | null {
326
326
 
327
327
  /**
328
328
  * Helper function to parse a path string with ?. notation
329
+ * Always splits on '?.' delimiter, preserving dots that are part of values
330
+ * (e.g., CSS class selectors like '.username')
331
+ * Paths must use ?. notation — plain dot notation is not supported.
329
332
  */
330
333
  function parsePath(path: string): string[] {
331
334
  return path
332
- .split('.')
333
- .map(part => part.replace(/\?/g, ''))
335
+ .split('?.')
334
336
  .filter(part => part.length > 0);
335
337
  }
336
338
 
@@ -736,16 +738,22 @@ export function assignGingerly(
736
738
  if (isIncCommand(key)) {
737
739
  const path = parseIncCommand(key);
738
740
  if (path) {
739
- const pathParts = parsePath(path);
740
- const lastKey = pathParts[pathParts.length - 1];
741
- const parent = ensureNestedPath(target, pathParts);
742
-
743
- // If the path doesn't exist, set it directly to the value
744
- if (!(lastKey in parent)) {
745
- parent[lastKey] = value;
741
+ if (isNestedPath(path)) {
742
+ const pathParts = parsePath(path);
743
+ const lastKey = pathParts[pathParts.length - 1];
744
+ const parent = ensureNestedPath(target, pathParts);
745
+ if (!(lastKey in parent)) {
746
+ parent[lastKey] = value;
747
+ } else {
748
+ parent[lastKey] += value;
749
+ }
746
750
  } else {
747
- // Path exists, apply increment: oldValue += newValue
748
- parent[lastKey] += value;
751
+ // Plain key - direct operation on target
752
+ if (!(path in target)) {
753
+ target[path] = value;
754
+ } else {
755
+ target[path] += value;
756
+ }
749
757
  }
750
758
  }
751
759
  continue;
@@ -757,10 +765,17 @@ export function assignGingerly(
757
765
  if (lhsPath) {
758
766
  const rhsPath = value;
759
767
 
760
- // Parse LHS path
761
- const lhsPathParts = parsePath(lhsPath);
762
- const lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
763
- const lhsParent = ensureNestedPath(target, lhsPathParts);
768
+ // Resolve LHS
769
+ let lhsParent: any;
770
+ let lhsLastKey: string;
771
+ if (isNestedPath(lhsPath)) {
772
+ const lhsPathParts = parsePath(lhsPath);
773
+ lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
774
+ lhsParent = ensureNestedPath(target, lhsPathParts);
775
+ } else {
776
+ lhsLastKey = lhsPath;
777
+ lhsParent = target;
778
+ }
764
779
 
765
780
  // Determine what to negate
766
781
  let valueToNegate;
@@ -769,26 +784,27 @@ export function assignGingerly(
769
784
  if (lhsLastKey in lhsParent) {
770
785
  valueToNegate = lhsParent[lhsLastKey];
771
786
  } else {
772
- // LHS doesn't exist, treat as undefined -> !undefined = true
773
787
  valueToNegate = undefined;
774
788
  }
775
789
  } else {
776
790
  // RHS path: navigate to get the value (don't create paths)
777
- const rhsPathParts = parsePath(rhsPath);
778
- let current = target;
779
- let exists = true;
780
-
781
- for (const part of rhsPathParts) {
782
- if (current && typeof current === 'object' && part in current) {
783
- current = current[part];
784
- } else {
785
- exists = false;
786
- break;
791
+ if (isNestedPath(rhsPath)) {
792
+ const rhsPathParts = parsePath(rhsPath);
793
+ let current = target;
794
+ let exists = true;
795
+ for (const part of rhsPathParts) {
796
+ if (current && typeof current === 'object' && part in current) {
797
+ current = current[part];
798
+ } else {
799
+ exists = false;
800
+ break;
801
+ }
787
802
  }
803
+ valueToNegate = exists ? current : true;
804
+ } else {
805
+ // Plain key RHS
806
+ valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
788
807
  }
789
-
790
- // If RHS doesn't exist, treat as truthy (will become false)
791
- valueToNegate = exists ? current : true;
792
808
  }
793
809
 
794
810
  // Apply negation to LHS
@@ -801,31 +817,36 @@ export function assignGingerly(
801
817
  if (isDeleteCommand(key)) {
802
818
  const path = parseDeleteCommand(key);
803
819
  if (path !== null) {
804
- const pathParts = parsePath(path);
805
-
806
820
  // Determine the parent object
807
821
  let parent = target;
808
822
  let canDelete = true;
809
823
 
810
- // If path is empty or just '?', delete from root
811
- if (pathParts.length === 0) {
812
- parent = target;
813
- } else {
814
- // Navigate to parent object
815
- for (const part of pathParts) {
816
- if (parent && typeof parent === 'object' && part in parent) {
817
- parent = parent[part];
818
- } else {
819
- canDelete = false;
820
- break;
824
+ if (isNestedPath(path)) {
825
+ const pathParts = parsePath(path);
826
+ if (pathParts.length === 0) {
827
+ parent = target;
828
+ } else {
829
+ for (const part of pathParts) {
830
+ if (parent && typeof parent === 'object' && part in parent) {
831
+ parent = parent[part];
832
+ } else {
833
+ canDelete = false;
834
+ break;
835
+ }
821
836
  }
822
837
  }
838
+ } else if (path.length > 0) {
839
+ // Plain key - navigate one level
840
+ if (parent && typeof parent === 'object' && path in parent) {
841
+ parent = parent[path];
842
+ } else {
843
+ canDelete = false;
844
+ }
823
845
  }
846
+ // else: empty path = delete from root
824
847
 
825
848
  if (canDelete && typeof parent === 'object' && parent !== null) {
826
- // RHS can be a string (single property) or array (multiple properties)
827
849
  const propertiesToDelete = Array.isArray(value) ? value : [value];
828
-
829
850
  for (const prop of propertiesToDelete) {
830
851
  if (prop in parent) {
831
852
  delete parent[prop];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "description": "This package provides a utility function for carefully merging one object into another.",
5
5
  "homepage": "https://github.com/bahrus/assign-gingerly#readme",
6
6
  "bugs": {
package/resolveValues.js CHANGED
@@ -26,8 +26,10 @@ export function resolveValues(pattern, source) {
26
26
  const result = {};
27
27
  for (const [key, value] of Object.entries(pattern)) {
28
28
  if (typeof value === 'string' && value.startsWith('?.')) {
29
- // Parse path: split by '.', strip '?', filter empties
30
- const parts = value.split('.').map(p => p.replace(/\?/g, '')).filter(p => p.length > 0);
29
+ // Parse path: split on '?.' delimiter, filter empties
30
+ // Use '?.' as the sole delimiter to preserve dots in values (e.g., CSS selectors)
31
+ // Special case: '?.' alone (empty path) resolves to the source object itself
32
+ const parts = value.split('?.').filter(p => p.length > 0);
31
33
  let current = source;
32
34
  for (const part of parts) {
33
35
  if (current == null)
package/resolveValues.ts CHANGED
@@ -29,8 +29,10 @@ export function resolveValues(
29
29
  const result: Record<string, any> = {};
30
30
  for (const [key, value] of Object.entries(pattern)) {
31
31
  if (typeof value === 'string' && value.startsWith('?.')) {
32
- // Parse path: split by '.', strip '?', filter empties
33
- const parts = value.split('.').map(p => p.replace(/\?/g, '')).filter(p => p.length > 0);
32
+ // Parse path: split on '?.' delimiter, filter empties
33
+ // Use '?.' as the sole delimiter to preserve dots in values (e.g., CSS selectors)
34
+ // Special case: '?.' alone (empty path) resolves to the source object itself
35
+ const parts = value.split('?.').filter(p => p.length > 0);
34
36
  let current = source;
35
37
  for (const part of parts) {
36
38
  if (current == null) break;