assign-gingerly 0.0.34 → 0.0.36

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
@@ -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/assignFrom.js CHANGED
@@ -22,6 +22,9 @@
22
22
  import { resolveValues } from './resolveValues.js';
23
23
  import assignGingerly from './assignGingerly.js';
24
24
  export function assignFrom(target, pattern, options) {
25
- const resolved = resolveValues(pattern, options.from);
25
+ const resolved = resolveValues(pattern, options.from, {
26
+ withMethods: options.withMethods,
27
+ aka: options.aka
28
+ });
26
29
  return assignGingerly(target, resolved, options);
27
30
  }
package/assignFrom.ts CHANGED
@@ -32,6 +32,9 @@ export function assignFrom(
32
32
  pattern: Record<string, any>,
33
33
  options: AssignFromOptions
34
34
  ): any {
35
- const resolved = resolveValues(pattern, options.from);
35
+ const resolved = resolveValues(pattern, options.from, {
36
+ withMethods: options.withMethods,
37
+ aka: options.aka
38
+ });
36
39
  return assignGingerly(target, resolved, options);
37
40
  }
@@ -45,11 +45,13 @@ function parseDeleteCommand(key) {
45
45
  }
46
46
  /**
47
47
  * Helper function to parse a path string with ?. notation
48
+ * Always splits on '?.' delimiter, preserving dots that are part of values
49
+ * (e.g., CSS class selectors like '.username')
50
+ * Paths must use ?. notation — plain dot notation is not supported.
48
51
  */
49
52
  function parsePath(path) {
50
53
  return path
51
- .split('.')
52
- .map(part => part.replace(/\?/g, ''))
54
+ .split('?.')
53
55
  .filter(part => part.length > 0);
54
56
  }
55
57
  /**
@@ -107,25 +109,39 @@ export function assignTentatively(target, source, options) {
107
109
  if (isIncCommand(key)) {
108
110
  const path = parseIncCommand(key);
109
111
  if (path) {
110
- const pathParts = parsePath(path);
111
- const topLevelKey = pathParts[0];
112
- // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
113
- if (!(topLevelKey in target)) {
114
- trackedCreatedPaths.add(topLevelKey);
115
- }
116
- const lastKey = pathParts[pathParts.length - 1];
117
- const parent = ensureNestedPath(target, pathParts);
118
- // If property already exists, store original value for reversal
119
- if (lastKey in parent) {
120
- const fullPath = `?.${pathParts.join('?.')}`;
121
- if (!(fullPath in reversal)) {
122
- reversal[fullPath] = parent[lastKey];
112
+ if (isNestedPath(path)) {
113
+ const pathParts = parsePath(path);
114
+ const topLevelKey = pathParts[0];
115
+ // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
116
+ if (!(topLevelKey in target)) {
117
+ trackedCreatedPaths.add(topLevelKey);
118
+ }
119
+ const lastKey = pathParts[pathParts.length - 1];
120
+ const parent = ensureNestedPath(target, pathParts);
121
+ // If property already exists, store original value for reversal
122
+ if (lastKey in parent) {
123
+ const fullPath = `?.${pathParts.join('?.')}`;
124
+ if (!(fullPath in reversal)) {
125
+ reversal[fullPath] = parent[lastKey];
126
+ }
127
+ parent[lastKey] += value;
128
+ }
129
+ else {
130
+ // Property doesn't exist, create it with the value
131
+ parent[lastKey] = value;
123
132
  }
124
- parent[lastKey] += value;
125
133
  }
126
134
  else {
127
- // Property doesn't exist, create it with the value
128
- parent[lastKey] = value;
135
+ // Plain key - direct operation on target
136
+ if (path in target) {
137
+ if (!(path in reversal)) {
138
+ reversal[path] = target[path];
139
+ }
140
+ target[path] += value;
141
+ }
142
+ else {
143
+ target[path] = value;
144
+ }
129
145
  }
130
146
  }
131
147
  continue;
@@ -135,27 +151,37 @@ export function assignTentatively(target, source, options) {
135
151
  const lhsPath = parseToggleCommand(key);
136
152
  if (lhsPath) {
137
153
  const rhsPath = value;
138
- const pathParts = parsePath(lhsPath);
139
- const topLevelKey = pathParts[0];
140
- // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
141
- if (!(topLevelKey in target)) {
142
- trackedCreatedPaths.add(topLevelKey);
154
+ let lhsParent;
155
+ let lhsLastKey;
156
+ let lhsPathParts;
157
+ if (isNestedPath(lhsPath)) {
158
+ lhsPathParts = parsePath(lhsPath);
159
+ const topLevelKey = lhsPathParts[0];
160
+ // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
161
+ if (!(topLevelKey in target)) {
162
+ trackedCreatedPaths.add(topLevelKey);
163
+ }
164
+ lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
165
+ lhsParent = ensureNestedPath(target, lhsPathParts);
166
+ }
167
+ else {
168
+ lhsPathParts = [lhsPath];
169
+ lhsLastKey = lhsPath;
170
+ lhsParent = target;
143
171
  }
144
- const lastKey = pathParts[pathParts.length - 1];
145
- const parent = ensureNestedPath(target, pathParts);
146
172
  // Determine what to negate
147
173
  let valueToNegate;
148
174
  if (rhsPath === '.') {
149
175
  // Self-reference
150
- if (lastKey in parent) {
151
- valueToNegate = parent[lastKey];
176
+ if (lhsLastKey in lhsParent) {
177
+ valueToNegate = lhsParent[lhsLastKey];
152
178
  }
153
179
  else {
154
180
  valueToNegate = undefined;
155
181
  }
156
182
  }
157
- else {
158
- // RHS path: navigate to get the value (don't create paths)
183
+ else if (isNestedPath(rhsPath)) {
184
+ // RHS nested path: navigate to get the value (don't create paths)
159
185
  const rhsPathParts = parsePath(rhsPath);
160
186
  let current = target;
161
187
  let exists = true;
@@ -170,14 +196,18 @@ export function assignTentatively(target, source, options) {
170
196
  }
171
197
  valueToNegate = exists ? current : true;
172
198
  }
199
+ else {
200
+ // Plain key RHS
201
+ valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
202
+ }
173
203
  // Store original value for reversal if it exists
174
- if (lastKey in parent) {
175
- const fullPath = `?.${pathParts.join('?.')}`;
204
+ if (lhsLastKey in lhsParent) {
205
+ const fullPath = `?.${lhsPathParts.join('?.')}`;
176
206
  if (!(fullPath in reversal)) {
177
- reversal[fullPath] = parent[lastKey];
207
+ reversal[fullPath] = lhsParent[lhsLastKey];
178
208
  }
179
209
  }
180
- parent[lastKey] = !valueToNegate;
210
+ lhsParent[lhsLastKey] = !valueToNegate;
181
211
  }
182
212
  continue;
183
213
  }
@@ -185,26 +215,38 @@ export function assignTentatively(target, source, options) {
185
215
  if (isDeleteCommand(key)) {
186
216
  const path = parseDeleteCommand(key);
187
217
  if (path !== null) {
188
- const pathParts = parsePath(path);
189
218
  // Determine the parent object
190
219
  let parent = target;
191
220
  let canDelete = true;
192
- // If path is empty or just '?', delete from root
193
- if (pathParts.length === 0) {
194
- parent = target;
195
- }
196
- else {
197
- // Navigate to parent without creating intermediate paths
198
- for (const part of pathParts) {
199
- if (parent && typeof parent === 'object' && part in parent) {
200
- parent = parent[part];
201
- }
202
- else {
203
- canDelete = false;
204
- break;
221
+ let pathParts = [];
222
+ if (isNestedPath(path)) {
223
+ pathParts = parsePath(path);
224
+ if (pathParts.length === 0) {
225
+ parent = target;
226
+ }
227
+ else {
228
+ for (const part of pathParts) {
229
+ if (parent && typeof parent === 'object' && part in parent) {
230
+ parent = parent[part];
231
+ }
232
+ else {
233
+ canDelete = false;
234
+ break;
235
+ }
205
236
  }
206
237
  }
207
238
  }
239
+ else if (path.length > 0) {
240
+ // Plain key - navigate one level
241
+ pathParts = [path];
242
+ if (parent && typeof parent === 'object' && path in parent) {
243
+ parent = parent[path];
244
+ }
245
+ else {
246
+ canDelete = false;
247
+ }
248
+ }
249
+ // else: empty path = delete from root
208
250
  if (canDelete && typeof parent === 'object' && parent !== null) {
209
251
  // RHS can be a string (single property) or array (multiple properties)
210
252
  const propertiesToDelete = Array.isArray(value) ? value : [value];
@@ -212,7 +254,7 @@ export function assignTentatively(target, source, options) {
212
254
  if (prop in parent) {
213
255
  // Store original value for reversal
214
256
  const fullPath = pathParts.length > 0
215
- ? `?.${pathParts.join('?.')}.${prop}`
257
+ ? `?.${pathParts.join('?.')}?.${prop}`
216
258
  : `?.${prop}`;
217
259
  if (!(fullPath in reversal)) {
218
260
  reversal[fullPath] = parent[prop];
@@ -58,11 +58,13 @@ function parseDeleteCommand(key: string): string | null {
58
58
 
59
59
  /**
60
60
  * Helper function to parse a path string with ?. notation
61
+ * Always splits on '?.' delimiter, preserving dots that are part of values
62
+ * (e.g., CSS class selectors like '.username')
63
+ * Paths must use ?. notation — plain dot notation is not supported.
61
64
  */
62
65
  function parsePath(path: string): string[] {
63
66
  return path
64
- .split('.')
65
- .map(part => part.replace(/\?/g, ''))
67
+ .split('?.')
66
68
  .filter(part => part.length > 0);
67
69
  }
68
70
 
@@ -132,27 +134,39 @@ export function assignTentatively(
132
134
  if (isIncCommand(key)) {
133
135
  const path = parseIncCommand(key);
134
136
  if (path) {
135
- const pathParts = parsePath(path);
136
- const topLevelKey = pathParts[0];
137
-
138
- // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
139
- if (!(topLevelKey in target)) {
140
- trackedCreatedPaths.add(topLevelKey);
141
- }
142
-
143
- const lastKey = pathParts[pathParts.length - 1];
144
- const parent = ensureNestedPath(target, pathParts);
137
+ if (isNestedPath(path)) {
138
+ const pathParts = parsePath(path);
139
+ const topLevelKey = pathParts[0];
140
+
141
+ // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
142
+ if (!(topLevelKey in target)) {
143
+ trackedCreatedPaths.add(topLevelKey);
144
+ }
145
+
146
+ const lastKey = pathParts[pathParts.length - 1];
147
+ const parent = ensureNestedPath(target, pathParts);
145
148
 
146
- // If property already exists, store original value for reversal
147
- if (lastKey in parent) {
148
- const fullPath = `?.${pathParts.join('?.')}`;
149
- if (!(fullPath in reversal)) {
150
- reversal[fullPath] = parent[lastKey];
149
+ // If property already exists, store original value for reversal
150
+ if (lastKey in parent) {
151
+ const fullPath = `?.${pathParts.join('?.')}`;
152
+ if (!(fullPath in reversal)) {
153
+ reversal[fullPath] = parent[lastKey];
154
+ }
155
+ parent[lastKey] += value;
156
+ } else {
157
+ // Property doesn't exist, create it with the value
158
+ parent[lastKey] = value;
151
159
  }
152
- parent[lastKey] += value;
153
160
  } else {
154
- // Property doesn't exist, create it with the value
155
- parent[lastKey] = value;
161
+ // Plain key - direct operation on target
162
+ if (path in target) {
163
+ if (!(path in reversal)) {
164
+ reversal[path] = target[path];
165
+ }
166
+ target[path] += value;
167
+ } else {
168
+ target[path] = value;
169
+ }
156
170
  }
157
171
  }
158
172
  continue;
@@ -163,28 +177,39 @@ export function assignTentatively(
163
177
  const lhsPath = parseToggleCommand(key);
164
178
  if (lhsPath) {
165
179
  const rhsPath = value;
166
- const pathParts = parsePath(lhsPath);
167
- const topLevelKey = pathParts[0];
168
180
 
169
- // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
170
- if (!(topLevelKey in target)) {
171
- trackedCreatedPaths.add(topLevelKey);
172
- }
181
+ let lhsParent: any;
182
+ let lhsLastKey: string;
183
+ let lhsPathParts: string[];
173
184
 
174
- const lastKey = pathParts[pathParts.length - 1];
175
- const parent = ensureNestedPath(target, pathParts);
185
+ if (isNestedPath(lhsPath)) {
186
+ lhsPathParts = parsePath(lhsPath);
187
+ const topLevelKey = lhsPathParts[0];
188
+
189
+ // Track if we created a new top-level path (BEFORE calling ensureNestedPath)
190
+ if (!(topLevelKey in target)) {
191
+ trackedCreatedPaths.add(topLevelKey);
192
+ }
193
+
194
+ lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
195
+ lhsParent = ensureNestedPath(target, lhsPathParts);
196
+ } else {
197
+ lhsPathParts = [lhsPath];
198
+ lhsLastKey = lhsPath;
199
+ lhsParent = target;
200
+ }
176
201
 
177
202
  // Determine what to negate
178
203
  let valueToNegate;
179
204
  if (rhsPath === '.') {
180
205
  // Self-reference
181
- if (lastKey in parent) {
182
- valueToNegate = parent[lastKey];
206
+ if (lhsLastKey in lhsParent) {
207
+ valueToNegate = lhsParent[lhsLastKey];
183
208
  } else {
184
209
  valueToNegate = undefined;
185
210
  }
186
- } else {
187
- // RHS path: navigate to get the value (don't create paths)
211
+ } else if (isNestedPath(rhsPath)) {
212
+ // RHS nested path: navigate to get the value (don't create paths)
188
213
  const rhsPathParts = parsePath(rhsPath);
189
214
  let current = target;
190
215
  let exists = true;
@@ -199,17 +224,20 @@ export function assignTentatively(
199
224
  }
200
225
 
201
226
  valueToNegate = exists ? current : true;
227
+ } else {
228
+ // Plain key RHS
229
+ valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
202
230
  }
203
231
 
204
232
  // Store original value for reversal if it exists
205
- if (lastKey in parent) {
206
- const fullPath = `?.${pathParts.join('?.')}`;
233
+ if (lhsLastKey in lhsParent) {
234
+ const fullPath = `?.${lhsPathParts.join('?.')}`;
207
235
  if (!(fullPath in reversal)) {
208
- reversal[fullPath] = parent[lastKey];
236
+ reversal[fullPath] = lhsParent[lhsLastKey];
209
237
  }
210
238
  }
211
239
 
212
- parent[lastKey] = !valueToNegate;
240
+ lhsParent[lhsLastKey] = !valueToNegate;
213
241
  }
214
242
  continue;
215
243
  }
@@ -218,26 +246,35 @@ export function assignTentatively(
218
246
  if (isDeleteCommand(key)) {
219
247
  const path = parseDeleteCommand(key);
220
248
  if (path !== null) {
221
- const pathParts = parsePath(path);
222
-
223
249
  // Determine the parent object
224
250
  let parent = target;
225
251
  let canDelete = true;
252
+ let pathParts: string[] = [];
226
253
 
227
- // If path is empty or just '?', delete from root
228
- if (pathParts.length === 0) {
229
- parent = target;
230
- } else {
231
- // Navigate to parent without creating intermediate paths
232
- for (const part of pathParts) {
233
- if (parent && typeof parent === 'object' && part in parent) {
234
- parent = parent[part];
235
- } else {
236
- canDelete = false;
237
- break;
254
+ if (isNestedPath(path)) {
255
+ pathParts = parsePath(path);
256
+ if (pathParts.length === 0) {
257
+ parent = target;
258
+ } else {
259
+ for (const part of pathParts) {
260
+ if (parent && typeof parent === 'object' && part in parent) {
261
+ parent = parent[part];
262
+ } else {
263
+ canDelete = false;
264
+ break;
265
+ }
238
266
  }
239
267
  }
268
+ } else if (path.length > 0) {
269
+ // Plain key - navigate one level
270
+ pathParts = [path];
271
+ if (parent && typeof parent === 'object' && path in parent) {
272
+ parent = parent[path];
273
+ } else {
274
+ canDelete = false;
275
+ }
240
276
  }
277
+ // else: empty path = delete from root
241
278
 
242
279
  if (canDelete && typeof parent === 'object' && parent !== null) {
243
280
  // RHS can be a string (single property) or array (multiple properties)
@@ -247,7 +284,7 @@ export function assignTentatively(
247
284
  if (prop in parent) {
248
285
  // Store original value for reversal
249
286
  const fullPath = pathParts.length > 0
250
- ? `?.${pathParts.join('?.')}.${prop}`
287
+ ? `?.${pathParts.join('?.')}?.${prop}`
251
288
  : `?.${prop}`;
252
289
  if (!(fullPath in reversal)) {
253
290
  reversal[fullPath] = parent[prop];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
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
@@ -1,3 +1,52 @@
1
+ /**
2
+ * Apply alias substitutions to a path string.
3
+ * Replaces complete tokens between `?.` delimiters with their aliased values.
4
+ */
5
+ function applyAliases(path, aliasMap) {
6
+ if (aliasMap.size === 0)
7
+ return path;
8
+ const parts = path.split('?.');
9
+ const substituted = parts.map(part => aliasMap.get(part) ?? part);
10
+ return substituted.join('?.');
11
+ }
12
+ /**
13
+ * Navigate a path against a source object, optionally calling methods.
14
+ * Returns the resolved value at the end of the path.
15
+ */
16
+ function navigatePath(source, parts, withMethods) {
17
+ let current = source;
18
+ let i = 0;
19
+ while (i < parts.length) {
20
+ if (current == null)
21
+ return current;
22
+ const part = parts[i];
23
+ if (withMethods && withMethods.has(part)) {
24
+ const method = current[part];
25
+ if (typeof method === 'function') {
26
+ const nextPart = parts[i + 1];
27
+ if (nextPart !== undefined && !(withMethods.has(nextPart))) {
28
+ // Call method with next segment as argument, consume it
29
+ current = method.call(current, nextPart);
30
+ i += 2;
31
+ }
32
+ else {
33
+ // Consecutive methods or last segment — call with no args
34
+ current = method.call(current);
35
+ i++;
36
+ }
37
+ }
38
+ else {
39
+ current = current[part];
40
+ i++;
41
+ }
42
+ }
43
+ else {
44
+ current = current[part];
45
+ i++;
46
+ }
47
+ }
48
+ return current;
49
+ }
1
50
  /**
2
51
  * Resolve RHS path strings in a pattern object against a source object.
3
52
  *
@@ -5,36 +54,55 @@
5
54
  * and resolved against the source object using optional chaining semantics.
6
55
  * Non-string values and strings not starting with `?.` pass through unchanged.
7
56
  *
57
+ * Supports `withMethods` for calling methods during resolution and `aka` for
58
+ * alias substitution, consistent with assignGingerly's LHS path handling.
59
+ *
60
+ * Special case: `'?.'` (empty path) resolves to the source object itself.
61
+ *
8
62
  * @param pattern - Object whose RHS values may contain `?.` path strings
9
63
  * @param source - Object to resolve paths against
64
+ * @param options - Optional withMethods and aka for method calls and aliases
10
65
  * @returns New object with path strings replaced by resolved values
11
66
  *
12
67
  * @example
13
- * const pattern = {
68
+ * const result = resolveValues({
14
69
  * hello: '?.myPropContainer?.stringProp',
15
70
  * foo: '?.myFooString',
16
71
  * literal: 42
17
- * };
18
- * const source = {
19
- * myPropContainer: { stringProp: 'Venus' },
20
- * myFooString: 'bar'
21
- * };
22
- * const result = resolveValues(pattern, source);
23
- * // { hello: 'Venus', foo: 'bar', literal: 42 }
72
+ * }, source);
73
+ *
74
+ * @example
75
+ * // With methods and aliases
76
+ * const result = resolveValues({
77
+ * text: '?.q?..username?.textContent'
78
+ * }, source, {
79
+ * withMethods: ['querySelector'],
80
+ * aka: { 'q': 'querySelector' }
81
+ * });
24
82
  */
25
- export function resolveValues(pattern, source) {
83
+ export function resolveValues(pattern, source, options) {
84
+ // Build alias map
85
+ const aliasMap = new Map();
86
+ if (options?.aka) {
87
+ for (const [alias, target] of Object.entries(options.aka)) {
88
+ aliasMap.set(alias, target);
89
+ }
90
+ }
91
+ // Build methods set
92
+ const withMethods = options?.withMethods
93
+ ? options.withMethods instanceof Set
94
+ ? options.withMethods
95
+ : new Set(options.withMethods)
96
+ : undefined;
26
97
  const result = {};
27
98
  for (const [key, value] of Object.entries(pattern)) {
28
99
  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);
31
- let current = source;
32
- for (const part of parts) {
33
- if (current == null)
34
- break;
35
- current = current[part];
36
- }
37
- result[key] = current;
100
+ // Apply aliases to the RHS path
101
+ const aliased = applyAliases(value, aliasMap);
102
+ // Parse path: split on '?.' delimiter, filter empties
103
+ const parts = aliased.split('?.').filter(p => p.length > 0);
104
+ // Navigate with method support
105
+ result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
38
106
  }
39
107
  else {
40
108
  result[key] = value;
package/resolveValues.ts CHANGED
@@ -1,3 +1,74 @@
1
+ /**
2
+ * Options for resolveValues
3
+ */
4
+ export interface ResolveValuesOptions {
5
+ /**
6
+ * Method names that should be called instead of accessed as properties.
7
+ * When a path segment matches, it's called as a method with the next segment as argument.
8
+ */
9
+ withMethods?: string[] | Set<string>;
10
+
11
+ /**
12
+ * Alias mappings for path segments.
13
+ * Substituted before path resolution, matching complete tokens between `?.` delimiters.
14
+ */
15
+ aka?: Record<string, string>;
16
+ }
17
+
18
+ /**
19
+ * Apply alias substitutions to a path string.
20
+ * Replaces complete tokens between `?.` delimiters with their aliased values.
21
+ */
22
+ function applyAliases(path: string, aliasMap: Map<string, string>): string {
23
+ if (aliasMap.size === 0) return path;
24
+ const parts = path.split('?.');
25
+ const substituted = parts.map(part => aliasMap.get(part) ?? part);
26
+ return substituted.join('?.');
27
+ }
28
+
29
+ /**
30
+ * Navigate a path against a source object, optionally calling methods.
31
+ * Returns the resolved value at the end of the path.
32
+ */
33
+ function navigatePath(
34
+ source: any,
35
+ parts: string[],
36
+ withMethods: Set<string> | undefined
37
+ ): any {
38
+ let current = source;
39
+ let i = 0;
40
+
41
+ while (i < parts.length) {
42
+ if (current == null) return current;
43
+
44
+ const part = parts[i];
45
+
46
+ if (withMethods && withMethods.has(part)) {
47
+ const method = current[part];
48
+ if (typeof method === 'function') {
49
+ const nextPart = parts[i + 1];
50
+ if (nextPart !== undefined && !(withMethods.has(nextPart))) {
51
+ // Call method with next segment as argument, consume it
52
+ current = method.call(current, nextPart);
53
+ i += 2;
54
+ } else {
55
+ // Consecutive methods or last segment — call with no args
56
+ current = method.call(current);
57
+ i++;
58
+ }
59
+ } else {
60
+ current = current[part];
61
+ i++;
62
+ }
63
+ } else {
64
+ current = current[part];
65
+ i++;
66
+ }
67
+ }
68
+
69
+ return current;
70
+ }
71
+
1
72
  /**
2
73
  * Resolve RHS path strings in a pattern object against a source object.
3
74
  *
@@ -5,38 +76,63 @@
5
76
  * and resolved against the source object using optional chaining semantics.
6
77
  * Non-string values and strings not starting with `?.` pass through unchanged.
7
78
  *
79
+ * Supports `withMethods` for calling methods during resolution and `aka` for
80
+ * alias substitution, consistent with assignGingerly's LHS path handling.
81
+ *
82
+ * Special case: `'?.'` (empty path) resolves to the source object itself.
83
+ *
8
84
  * @param pattern - Object whose RHS values may contain `?.` path strings
9
85
  * @param source - Object to resolve paths against
86
+ * @param options - Optional withMethods and aka for method calls and aliases
10
87
  * @returns New object with path strings replaced by resolved values
11
88
  *
12
89
  * @example
13
- * const pattern = {
90
+ * const result = resolveValues({
14
91
  * hello: '?.myPropContainer?.stringProp',
15
92
  * foo: '?.myFooString',
16
93
  * literal: 42
17
- * };
18
- * const source = {
19
- * myPropContainer: { stringProp: 'Venus' },
20
- * myFooString: 'bar'
21
- * };
22
- * const result = resolveValues(pattern, source);
23
- * // { hello: 'Venus', foo: 'bar', literal: 42 }
94
+ * }, source);
95
+ *
96
+ * @example
97
+ * // With methods and aliases
98
+ * const result = resolveValues({
99
+ * text: '?.q?..username?.textContent'
100
+ * }, source, {
101
+ * withMethods: ['querySelector'],
102
+ * aka: { 'q': 'querySelector' }
103
+ * });
24
104
  */
25
105
  export function resolveValues(
26
106
  pattern: Record<string, any>,
27
- source: any
107
+ source: any,
108
+ options?: ResolveValuesOptions
28
109
  ): Record<string, any> {
110
+ // Build alias map
111
+ const aliasMap = new Map<string, string>();
112
+ if (options?.aka) {
113
+ for (const [alias, target] of Object.entries(options.aka)) {
114
+ aliasMap.set(alias, target);
115
+ }
116
+ }
117
+
118
+ // Build methods set
119
+ const withMethods = options?.withMethods
120
+ ? options.withMethods instanceof Set
121
+ ? options.withMethods
122
+ : new Set(options.withMethods)
123
+ : undefined;
124
+
29
125
  const result: Record<string, any> = {};
30
126
  for (const [key, value] of Object.entries(pattern)) {
31
127
  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);
34
- let current = source;
35
- for (const part of parts) {
36
- if (current == null) break;
37
- current = current[part];
38
- }
39
- result[key] = current;
128
+ // Apply aliases to the RHS path
129
+ const aliased = applyAliases(value, aliasMap);
130
+
131
+ // Parse path: split on '?.' delimiter, filter empties
132
+ const parts = aliased.split('?.').filter(p => p.length > 0);
133
+
134
+ // Navigate with method support
135
+ result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
40
136
  } else {
41
137
  result[key] = value;
42
138
  }