assign-gingerly 0.0.58 → 0.0.60

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.
Files changed (42) hide show
  1. package/README.md +511 -142
  2. package/assignFrom-extension.js +25 -0
  3. package/assignFrom-extension.ts +53 -0
  4. package/assignFrom.js +306 -133
  5. package/assignFrom.ts +318 -232
  6. package/assignFromAsync-extension.js +28 -0
  7. package/assignFromAsync-extension.ts +58 -0
  8. package/assignFromAsync.js +120 -0
  9. package/assignFromAsync.ts +256 -0
  10. package/assignGingerly.js +50 -0
  11. package/assignGingerly.ts +51 -0
  12. package/builtInEmoji.js +25 -0
  13. package/builtInEmoji.ts +33 -0
  14. package/getValues.js +223 -0
  15. package/getValues.ts +255 -0
  16. package/handlers/join.ts +1 -1
  17. package/handlers/lazyLoad.js +33 -2
  18. package/handlers/lazyLoad.ts +47 -2
  19. package/handlers/lazyLoadSwitch.ts +1 -1
  20. package/handlers/manageTemplateList.js +226 -0
  21. package/handlers/manageTemplateList.ts +263 -0
  22. package/handlers/microDataJoin.ts +1 -1
  23. package/index.js +1 -0
  24. package/index.ts +2 -0
  25. package/inferencer/inferencer.js +9 -21
  26. package/inferencer/inferencer.ts +10 -21
  27. package/inferredAssignments.js +35 -5
  28. package/inferredAssignments.ts +58 -8
  29. package/markerUtils.js +136 -127
  30. package/package.json +30 -1
  31. package/playwright.config.ts +3 -2
  32. package/processHandlerCommands.js +36 -4
  33. package/processHandlerCommands.ts +38 -8
  34. package/resolveIdRef.js +70 -19
  35. package/resolveIdRef.ts +73 -21
  36. package/resolveValues.js +41 -125
  37. package/resolveValues.ts +131 -255
  38. package/types/assign-gingerly/types.d.ts +61 -0
  39. package/waitForSettled.js +57 -0
  40. package/waitForSettled.ts +65 -0
  41. package/withIdsCorrector.js +47 -0
  42. package/withIdsCorrector.ts +59 -0
@@ -0,0 +1,28 @@
1
+ /**
2
+ * assignFromAsync-extension.js — Adds assignFromAsync to Object.prototype.
3
+ *
4
+ * Import this module for the side effect of extending all objects with
5
+ * the assignFromAsync method, enabling awaitable handler execution:
6
+ *
7
+ * @example
8
+ * import 'assign-gingerly/assignFromAsync-extension.js';
9
+ *
10
+ * await oElement.assignFromAsync({
11
+ * '?.querySelector?..mainView =>': {
12
+ * do: 'builtIns.lazyLoad',
13
+ * get: { if: '?.isVisible', instantiate: 'globalThis://myTemplate' }
14
+ * }
15
+ * }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
16
+ */
17
+
18
+ import { assignFromAsync } from './assignFromAsync.js';
19
+
20
+ Object.defineProperty(Object.prototype, 'assignFromAsync', {
21
+ value: async function (pattern, options) {
22
+ await assignFromAsync(this, pattern, options);
23
+ return this;
24
+ },
25
+ writable: true,
26
+ enumerable: false,
27
+ configurable: true,
28
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * assignFromAsync-extension.ts — Adds assignFromAsync to Object.prototype.
3
+ *
4
+ * Import this module for the side effect of extending all objects with
5
+ * the assignFromAsync method, enabling awaitable handler execution:
6
+ *
7
+ * @example
8
+ * import 'assign-gingerly/assignFromAsync-extension.js';
9
+ *
10
+ * await oElement.assignFromAsync({
11
+ * '?.querySelector?..mainView =>': {
12
+ * do: 'builtIns.lazyLoad',
13
+ * get: { if: '?.isVisible', instantiate: 'globalThis://myTemplate' }
14
+ * }
15
+ * }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
16
+ */
17
+
18
+ import { assignFromAsync } from './assignFromAsync.js';
19
+ import type { AssignFromOptions } from './assignFromAsync.js';
20
+
21
+ declare global {
22
+ interface Object {
23
+ /**
24
+ * Resolve RHS path strings from a source object and assign into this object.
25
+ * Async — awaits handler execution and supports async protocol handlers.
26
+ *
27
+ * @param pattern - Object with LHS paths as keys and RHS path strings (or literals) as values
28
+ * @param options - Configuration including `from` (source object), protocols, withMethods, etc.
29
+ * @returns Promise resolving to this object after assignment
30
+ *
31
+ * @example
32
+ * await oElement.assignFromAsync({
33
+ * '?.querySelector?..outlet =>': {
34
+ * do: 'builtIns.lazyLoad',
35
+ * get: { if: '?.showContent', instantiate: 'globalThis://myTemplate' }
36
+ * }
37
+ * }, { from: viewModel, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
38
+ */
39
+ assignFromAsync(
40
+ pattern: Record<string, any>,
41
+ options: AssignFromOptions
42
+ ): Promise<this>;
43
+ }
44
+ }
45
+
46
+ Object.defineProperty(Object.prototype, 'assignFromAsync', {
47
+ value: async function <T extends object>(
48
+ this: T,
49
+ pattern: Record<string, any>,
50
+ options: AssignFromOptions
51
+ ): Promise<T> {
52
+ await assignFromAsync(this, pattern, options);
53
+ return this;
54
+ },
55
+ writable: true,
56
+ enumerable: false,
57
+ configurable: true,
58
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Resolve RHS path strings against a source object, then assign the
3
+ * resolved values into a target using assignGingerly.
4
+ *
5
+ * Combines resolveValues + assignGingerly into a single call.
6
+ * Inherits all assignGingerly options (withMethods, aka, signal, etc.).
7
+ *
8
+ * @param target - Object to merge resolved values into
9
+ * @param pattern - Object whose RHS values may contain `?.` path strings
10
+ * @param options - Options including `from` (source object) and any assignGingerly options
11
+ * @returns The target object after merging
12
+ *
13
+ * @example
14
+ * const source = { theme: { color: 'red' }, label: 'Hello' };
15
+ * const target = { color: 'blue', text: '' };
16
+ * assignFrom(target, {
17
+ * color: '?.theme?.color',
18
+ * text: '?.label'
19
+ * }, { from: source });
20
+ * // target is now { color: 'red', text: 'Hello' }
21
+ */
22
+ import { resolveValues } from './resolveValues.js';
23
+ import assignGingerly from './assignGingerly.js';
24
+ import { expandSubstitutions, categorizeKeys, handleSpreads } from './assignFrom.js';
25
+ // Module cache for processHandlerCommands — avoids await on dynamic import after first call
26
+ let _processHandlerCommands;
27
+ export async function assignFromAsync(target, pattern, options, permissions) {
28
+ // First: expand looped substitution variables (${x}, ${y}, ${z})
29
+ const expandedPattern = expandSubstitutions(pattern, options);
30
+ // Categorize keys
31
+ const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys } = categorizeKeys(expandedPattern);
32
+ // Process normal keys via resolveValues + assignGingerly
33
+ if (Object.keys(normalPattern).length > 0) {
34
+ const resolved = await resolveValues(normalPattern, options.from, {
35
+ withMethods: options.withMethods,
36
+ aka: options.aka,
37
+ protocols: options.protocols
38
+ });
39
+ // Recursively handle "..." spread keys at all nesting levels
40
+ handleSpreads(resolved);
41
+ assignGingerly(target, resolved, options);
42
+ }
43
+ // Process #[x] normal keys — resolve element, then apply remaining path + value
44
+ if (idRefNormalKeys.length > 0 && (options.withIds || options.at)) {
45
+ const ids = { ...options.withIds, ...options.at };
46
+ const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
47
+ for (const key of idRefNormalKeys) {
48
+ const parsed = parseIdRef(key);
49
+ if (!parsed)
50
+ continue;
51
+ const el = resolveIdVariable(parsed.varName, target, ids);
52
+ if (!el)
53
+ continue;
54
+ const value = expandedPattern[key];
55
+ if (parsed.remainingPath) {
56
+ // Resolve the RHS value
57
+ const resolvedValue = await resolveValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
58
+ // Apply remaining path on the resolved element
59
+ assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
60
+ }
61
+ else {
62
+ // No remaining path — resolve and assign directly to the element
63
+ const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
64
+ if ('__v' in resolvedValue) {
65
+ // Single value — can't assign to element root without a path
66
+ }
67
+ else {
68
+ assignGingerly(el, resolvedValue, options);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ // Process handler commands ( =>) — cached after first load to avoid await overhead
74
+ if (handlerKeys.length > 0) {
75
+ _processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
76
+ await _processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
77
+ }
78
+ // Process #[x] handler keys — resolve element, then pass to handler processing
79
+ if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
80
+ const ids = { ...options.withIds, ...options.at };
81
+ const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
82
+ _processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
83
+ for (const key of idRefHandlerKeys) {
84
+ const parsed = parseIdRef(key);
85
+ if (!parsed)
86
+ continue;
87
+ const el = resolveIdVariable(parsed.varName, target, ids);
88
+ if (!el)
89
+ continue;
90
+ // Build a synthetic key for processHandlerCommands:
91
+ // The resolved element becomes the target, remaining path is the LHS
92
+ const syntheticKey = parsed.remainingPath
93
+ ? `${parsed.remainingPath} =>`
94
+ : ' =>';
95
+ const syntheticPattern = {
96
+ [syntheticKey]: expandedPattern[key]
97
+ };
98
+ await _processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
99
+ }
100
+ }
101
+ // Process inferred assignments — dynamically imported only when option is present
102
+ if (options.infer) {
103
+ const { processInferredAssignments } = await import('./inferredAssignments.js');
104
+ await processInferredAssignments(target, options.from, options.infer);
105
+ // Set up MutationObserver for new matching elements if beVigilant
106
+ if (options.infer.beVigilant) {
107
+ if (!options.signal) {
108
+ throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
109
+ }
110
+ const { setupVigilantObserver } = await import('./beVigilant.js');
111
+ setupVigilantObserver(target, options.from, options.infer, options.signal);
112
+ }
113
+ }
114
+ // Process bulk enhancements — dynamically imported only when option is present
115
+ if (options.enhance && options.enhance.length > 0) {
116
+ const { enhanceAll } = await import('./enhanceAll.js');
117
+ await enhanceAll(target, options.enhance, permissions);
118
+ }
119
+ return target;
120
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Resolve RHS path strings against a source object, then assign the
3
+ * resolved values into a target using assignGingerly.
4
+ *
5
+ * Combines resolveValues + assignGingerly into a single call.
6
+ * Inherits all assignGingerly options (withMethods, aka, signal, etc.).
7
+ *
8
+ * @param target - Object to merge resolved values into
9
+ * @param pattern - Object whose RHS values may contain `?.` path strings
10
+ * @param options - Options including `from` (source object) and any assignGingerly options
11
+ * @returns The target object after merging
12
+ *
13
+ * @example
14
+ * const source = { theme: { color: 'red' }, label: 'Hello' };
15
+ * const target = { color: 'blue', text: '' };
16
+ * assignFrom(target, {
17
+ * color: '?.theme?.color',
18
+ * text: '?.label'
19
+ * }, { from: source });
20
+ * // target is now { color: 'red', text: 'Hello' }
21
+ */
22
+ import { resolveValues } from './resolveValues.js';
23
+ import assignGingerly, { IAssignGingerlyOptions } from './assignGingerly.js';
24
+ import type { AssignPermissions } from './isAllowedImportPath.js';
25
+ import {
26
+ expandSubstitutions, categorizeKeys, handleSpreads, isHandlerCommand
27
+ } from './assignFrom.js';
28
+
29
+ export interface AssignFromOptions extends IAssignGingerlyOptions {
30
+ /** Source object to resolve RHS path strings against */
31
+ from: any;
32
+
33
+ /** Protocol handlers (sync or async) */
34
+ protocols?: Record<string, (key: string) => any | Promise<any>>;
35
+
36
+ /** Loop variable bindings — expand pattern entries containing ${x} */
37
+ where_x_in?: string[];
38
+ /** Loop variable bindings — expand pattern entries containing ${y} */
39
+ where_y_in?: string[];
40
+ /** Loop variable bindings — expand pattern entries containing ${z} */
41
+ where_z_in?: string[];
42
+
43
+ /**
44
+ * Cached element references by variable name.
45
+ * Used with `#[varName]` syntax in LHS keys for fast repeated element access.
46
+ *
47
+ * - String value: existing element ID (uses getElementById)
48
+ * - Object value: { qry: 'selector' } — finds element via querySelector on target, auto-assigns an ID
49
+ *
50
+ * Elements are cached via WeakRef with getElementById fallback on cache miss.
51
+ */
52
+ withIds?: Record<string, string | { qry: string }>;
53
+
54
+ /**
55
+ * Positional element references for use with `#[varName]` syntax.
56
+ * Resolves elements by child index path — no IDs assigned, no caching.
57
+ *
58
+ * - Array value: child index path (e.g., [0, 1] = target.children[0].children[1])
59
+ * - Object value: { path: [...], expect?: 'selector', fallback?: true }
60
+ * expect: validates via element.matches(), logs correction if wrong
61
+ * fallback: on mismatch, recovers via querySelector(expect)
62
+ */
63
+ at?: Record<string, number[] | { path: number[]; expect?: string; fallback?: boolean }>;
64
+
65
+ /**
66
+ * Handler implementations scoped to this call.
67
+ * Key: the `do` name referenced in handler configs.
68
+ * Value: a class constructor, or an import path to dynamically load one.
69
+ *
70
+ * Import paths must be local (relative, absolute, or bare specifier — no cross-domain URLs).
71
+ * The module's default export is checked first; otherwise the first exported class
72
+ * with an `assign` method on its prototype is used.
73
+ *
74
+ * Built-in handlers (builtIns.*) auto-load without needing to be listed here.
75
+ *
76
+ * @example
77
+ * handlers: {
78
+ * 'my-list': MyListHandler, // class constructor
79
+ * 'my-chart': './handlers/chart.js', // dynamic import path
80
+ * 'vendor-widget': 'some-package/handler.js', // bare specifier (import map)
81
+ * }
82
+ */
83
+ handlers?: Record<string, AssignFromHandlerConstructor | string>;
84
+
85
+ /**
86
+ * Inferred assignments — automatically distribute source values to matching
87
+ * DOM elements based on structural conventions (itemprop, name, etc.).
88
+ *
89
+ * Uses the inferencer submodule to determine the correct property for each
90
+ * matched element (textContent, value, checked, dateTime, ish, etc.).
91
+ *
92
+ * @example
93
+ * infer: {
94
+ * byItemprop: ['user', 'name', 'email'], // or true for all source keys
95
+ * beVigilant: true, // watch for new matching elements (requires signal)
96
+ * }
97
+ */
98
+ infer?: {
99
+ byItemprop?: string[] | true;
100
+ '|'?: string[] | true;
101
+ byName?: string[] | true | { props: string[] | true; outside: string };
102
+ '@'?: string[] | true | { props: string[] | true; outside: string };
103
+ /** Watch for new matching elements via MutationObserver. Requires options.signal for cleanup. */
104
+ beVigilant?: boolean;
105
+ };
106
+
107
+ /**
108
+ * Bulk enhancement application via EMC JSON configs.
109
+ * Finds matching elements and spawns enhancements on them.
110
+ *
111
+ * Each entry specifies an EMC JSON path and optionally overrides the matching selector.
112
+ * Enhancements are auto-registered if not already present in the enhancement registry.
113
+ *
114
+ * No scope perimeter is applied — use mount-observer for reactive/scoped enhancement.
115
+ *
116
+ * @example
117
+ * enhance: [
118
+ * { emc: 'be-bound/emc.json', matching: '[name]' },
119
+ * { emc: 'be-observant/emc.json', matching: '[itemprop]' },
120
+ * ]
121
+ */
122
+ enhance?: Array<{ emc: string; matching?: string; parse?: boolean }>;
123
+ }
124
+
125
+ /**
126
+ * Interface for assignFrom handler classes.
127
+ * Handlers are invoked when a LHS key ends with ' =>'.
128
+ */
129
+ export interface AssignFromHandler {
130
+ assign(lhsTarget: any, resolvedParams: Record<string, any>, options: AssignFromOptions): Promise<void> | void;
131
+ }
132
+
133
+ export interface AssignFromHandlerConstructor {
134
+ new (config: any): AssignFromHandler;
135
+ }
136
+
137
+ // Module cache for processHandlerCommands — avoids await on dynamic import after first call
138
+ let _processHandlerCommands: any;
139
+
140
+ export async function assignFromAsync(
141
+ target: any,
142
+ pattern: Record<string, any>,
143
+ options: AssignFromOptions,
144
+ permissions?: AssignPermissions
145
+ ): Promise<any> {
146
+ // First: expand looped substitution variables (${x}, ${y}, ${z})
147
+ const expandedPattern = expandSubstitutions(pattern, options);
148
+
149
+ // Categorize keys
150
+ const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys } = categorizeKeys(expandedPattern);
151
+
152
+ // Process normal keys via resolveValues + assignGingerly
153
+ if (Object.keys(normalPattern).length > 0) {
154
+ const resolved = await resolveValues(normalPattern, options.from, {
155
+ withMethods: options.withMethods,
156
+ aka: options.aka,
157
+ protocols: options.protocols
158
+ });
159
+
160
+ // Recursively handle "..." spread keys at all nesting levels
161
+ handleSpreads(resolved);
162
+
163
+ assignGingerly(target, resolved, options);
164
+ }
165
+
166
+ // Process #[x] normal keys — resolve element, then apply remaining path + value
167
+ if (idRefNormalKeys.length > 0 && (options.withIds || options.at)) {
168
+ const ids = { ...options.withIds, ...options.at };
169
+ const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
170
+ for (const key of idRefNormalKeys) {
171
+ const parsed = parseIdRef(key);
172
+ if (!parsed) continue;
173
+
174
+ const el = resolveIdVariable(parsed.varName, target, ids);
175
+ if (!el) continue;
176
+
177
+ const value = expandedPattern[key];
178
+ if (parsed.remainingPath) {
179
+ // Resolve the RHS value
180
+ const resolvedValue = await resolveValues(
181
+ { __v: value }, options.from,
182
+ { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
183
+ );
184
+ // Apply remaining path on the resolved element
185
+ assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
186
+ } else {
187
+ // No remaining path — resolve and assign directly to the element
188
+ const resolvedValue = await resolveValues(
189
+ typeof value === 'object' && value !== null ? value : { __v: value },
190
+ options.from,
191
+ { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
192
+ );
193
+ if ('__v' in resolvedValue) {
194
+ // Single value — can't assign to element root without a path
195
+ } else {
196
+ assignGingerly(el, resolvedValue, options);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ // Process handler commands ( =>) — cached after first load to avoid await overhead
202
+ if (handlerKeys.length > 0) {
203
+
204
+ _processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
205
+ await _processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
206
+ }
207
+ // Process #[x] handler keys — resolve element, then pass to handler processing
208
+ if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
209
+ const ids = { ...options.withIds, ...options.at };
210
+ const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
211
+ _processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
212
+
213
+ for (const key of idRefHandlerKeys) {
214
+ const parsed = parseIdRef(key);
215
+ if (!parsed) continue;
216
+
217
+ const el = resolveIdVariable(parsed.varName, target, ids);
218
+ if (!el) continue;
219
+
220
+ // Build a synthetic key for processHandlerCommands:
221
+ // The resolved element becomes the target, remaining path is the LHS
222
+ const syntheticKey = parsed.remainingPath
223
+ ? `${parsed.remainingPath} =>`
224
+ : ' =>';
225
+
226
+ const syntheticPattern: Record<string, any> = {
227
+ [syntheticKey]: expandedPattern[key]
228
+ };
229
+
230
+ await _processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
231
+ }
232
+ }
233
+
234
+ // Process inferred assignments — dynamically imported only when option is present
235
+ if (options.infer) {
236
+ const { processInferredAssignments } = await import('./inferredAssignments.js');
237
+ await processInferredAssignments(target, options.from, options.infer);
238
+
239
+ // Set up MutationObserver for new matching elements if beVigilant
240
+ if (options.infer.beVigilant) {
241
+ if (!options.signal) {
242
+ throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
243
+ }
244
+ const { setupVigilantObserver } = await import('./beVigilant.js');
245
+ setupVigilantObserver(target, options.from, options.infer, options.signal);
246
+ }
247
+ }
248
+
249
+ // Process bulk enhancements — dynamically imported only when option is present
250
+ if (options.enhance && options.enhance.length > 0) {
251
+ const { enhanceAll } = await import('./enhanceAll.js');
252
+ await enhanceAll(target, options.enhance, permissions);
253
+ }
254
+
255
+ return target;
256
+ }
package/assignGingerly.js CHANGED
@@ -226,6 +226,21 @@ function parseDeleteCommand(key) {
226
226
  }
227
227
  return key.substring(0, key.length - 3); // Remove ' -=' suffix
228
228
  }
229
+ /**
230
+ * Helper function to check if a key represents a Y= merge command
231
+ */
232
+ function isMergeCommand(key) {
233
+ return key.endsWith(' Y=');
234
+ }
235
+ /**
236
+ * Helper function to parse a Y= merge command and extract the path
237
+ */
238
+ function parseMergeCommand(key) {
239
+ if (!isMergeCommand(key)) {
240
+ return null;
241
+ }
242
+ return key.substring(0, key.length - 3); // Remove ' Y=' suffix
243
+ }
229
244
  /**
230
245
  * Helper function to parse a path string with ?. notation
231
246
  * Always splits on '?.' delimiter, preserving dots that are part of values
@@ -769,6 +784,41 @@ export function assignGingerly(target, source, options, permissions) {
769
784
  }
770
785
  continue;
771
786
  }
787
+ // Handle Y= merge commands (recursive assignGingerly into sub-object)
788
+ if (isMergeCommand(key)) {
789
+ const path = parseMergeCommand(key);
790
+ if (path) {
791
+ // Navigate to the target sub-object
792
+ let mergeTarget;
793
+ if (isNestedPath(path)) {
794
+ if (withMethodsSet) {
795
+ const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet);
796
+ mergeTarget = result.target[result.lastKey];
797
+ }
798
+ else {
799
+ const pathParts = parsePath(path);
800
+ mergeTarget = target;
801
+ for (const part of pathParts) {
802
+ if (mergeTarget && typeof mergeTarget === 'object' && part in mergeTarget) {
803
+ mergeTarget = mergeTarget[part];
804
+ }
805
+ else {
806
+ mergeTarget = undefined;
807
+ break;
808
+ }
809
+ }
810
+ }
811
+ }
812
+ else {
813
+ mergeTarget = target[path];
814
+ }
815
+ // Recursively merge if target is a valid object
816
+ if (mergeTarget && typeof mergeTarget === 'object') {
817
+ assignGingerly(mergeTarget, value, options, permissions);
818
+ }
819
+ }
820
+ continue;
821
+ }
772
822
  if (isNestedPath(key)) {
773
823
  const pathParts = parsePath(key);
774
824
  // Check if path contains @each or @eachTime (forEach)
package/assignGingerly.ts CHANGED
@@ -388,6 +388,23 @@ function parseDeleteCommand(key: string): string | null {
388
388
  return key.substring(0, key.length - 3); // Remove ' -=' suffix
389
389
  }
390
390
 
391
+ /**
392
+ * Helper function to check if a key represents a Y= merge command
393
+ */
394
+ function isMergeCommand(key: string): boolean {
395
+ return key.endsWith(' Y=');
396
+ }
397
+
398
+ /**
399
+ * Helper function to parse a Y= merge command and extract the path
400
+ */
401
+ function parseMergeCommand(key: string): string | null {
402
+ if (!isMergeCommand(key)) {
403
+ return null;
404
+ }
405
+ return key.substring(0, key.length - 3); // Remove ' Y=' suffix
406
+ }
407
+
391
408
  /**
392
409
  * Helper function to parse a path string with ?. notation
393
410
  * Always splits on '?.' delimiter, preserving dots that are part of values
@@ -968,6 +985,40 @@ export function assignGingerly(
968
985
  continue;
969
986
  }
970
987
 
988
+ // Handle Y= merge commands (recursive assignGingerly into sub-object)
989
+ if (isMergeCommand(key)) {
990
+ const path = parseMergeCommand(key);
991
+ if (path) {
992
+ // Navigate to the target sub-object
993
+ let mergeTarget: any;
994
+ if (isNestedPath(path)) {
995
+ if (withMethodsSet) {
996
+ const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet);
997
+ mergeTarget = result.target[result.lastKey];
998
+ } else {
999
+ const pathParts = parsePath(path);
1000
+ mergeTarget = target;
1001
+ for (const part of pathParts) {
1002
+ if (mergeTarget && typeof mergeTarget === 'object' && part in mergeTarget) {
1003
+ mergeTarget = mergeTarget[part];
1004
+ } else {
1005
+ mergeTarget = undefined;
1006
+ break;
1007
+ }
1008
+ }
1009
+ }
1010
+ } else {
1011
+ mergeTarget = target[path];
1012
+ }
1013
+
1014
+ // Recursively merge if target is a valid object
1015
+ if (mergeTarget && typeof mergeTarget === 'object') {
1016
+ assignGingerly(mergeTarget, value, options, permissions);
1017
+ }
1018
+ }
1019
+ continue;
1020
+ }
1021
+
971
1022
  if (isNestedPath(key)) {
972
1023
  const pathParts = parsePath(key);
973
1024
 
@@ -0,0 +1,25 @@
1
+ /**
2
+ * builtInEmoji.js — Predefined emoji aliases for built-in handlers.
3
+ *
4
+ * Import and spread into the `handlers` option for concise handler configs:
5
+ *
6
+ * @example
7
+ * import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
8
+ *
9
+ * assignFrom(target, {
10
+ * '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
11
+ * }, { from: vm, handlers: builtInEmoji });
12
+ */
13
+
14
+ /**
15
+ * Emoji → built-in handler name mapping.
16
+ */
17
+ export const builtInEmoji = {
18
+ '📦': 'builtIns.lazyLoad',
19
+ '🎚️': 'builtIns.lazyLoadSwitch',
20
+ '🔗': 'builtIns.join',
21
+ '🏷️': 'builtIns.microDataJoin',
22
+ '📋': 'builtIns.manageTemplateList',
23
+ };
24
+
25
+ export default builtInEmoji;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * builtInEmoji.ts — Predefined emoji aliases for built-in handlers.
3
+ *
4
+ * Import and spread into the `handlers` option for concise handler configs:
5
+ *
6
+ * @example
7
+ * import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
8
+ *
9
+ * assignFrom(target, {
10
+ * '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
11
+ * }, { from: vm, handlers: builtInEmoji });
12
+ */
13
+
14
+ /**
15
+ * Emoji → built-in handler name mapping.
16
+ *
17
+ * | Emoji | Handler |
18
+ * |-------|---------|
19
+ * | 📦 | builtIns.lazyLoad |
20
+ * | 🎚️ | builtIns.lazyLoadSwitch |
21
+ * | 🔗 | builtIns.join |
22
+ * | 🏷️ | builtIns.microDataJoin |
23
+ * | 📋 | builtIns.manageTemplateList |
24
+ */
25
+ export const builtInEmoji: Record<string, string> = {
26
+ '📦': 'builtIns.lazyLoad',
27
+ '🎚️': 'builtIns.lazyLoadSwitch',
28
+ '🔗': 'builtIns.join',
29
+ '🏷️': 'builtIns.microDataJoin',
30
+ '📋': 'builtIns.manageTemplateList',
31
+ };
32
+
33
+ export default builtInEmoji;