assign-gingerly 0.0.35 → 0.0.37

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/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.35",
3
+ "version": "0.0.37",
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/parseWithAttrs.js CHANGED
@@ -143,10 +143,15 @@ function hasDashOrNonASCII(str) {
143
143
  * Gets attribute value with smart enh- prefix handling
144
144
  * @param element - The element to read from
145
145
  * @param attrName - The attribute name (without enh- prefix)
146
- * @param allowUnprefixed - Pattern (string or RegExp) that element tag name must match to allow unprefixed attributes
146
+ * @param allowUnprefixed - Pattern (string or RegExp) that element tag name must match to allow unprefixed attributes,
147
+ * or `true` for custom element mode (read attributes directly, no enh- prefix)
147
148
  * @returns The attribute value or null
148
149
  */
149
150
  function getAttributeValue(element, attrName, allowUnprefixed) {
151
+ // Custom element mode - read attribute directly, no enh- prefix
152
+ if (allowUnprefixed === true) {
153
+ return element.getAttribute(attrName);
154
+ }
150
155
  const { localName } = element;
151
156
  const isCustomElement = localName.includes('-');
152
157
  const isSVGElement = typeof SVGElement !== 'undefined' && element instanceof SVGElement;
@@ -222,15 +227,18 @@ function getDefaultParser(instanceOf) {
222
227
  * Parses attributes from an element based on AttrPatterns configuration
223
228
  * @param element - The DOM element to read attributes from
224
229
  * @param attrPatterns - The attribute patterns configuration
225
- * @param allowUnprefixed - Pattern (string or RegExp) that element tag name must match to allow unprefixed attributes
230
+ * @param allowUnprefixed - Pattern (string or RegExp) that element tag name must match to allow unprefixed attributes,
231
+ * or `true` for custom element mode: reads attributes directly (no enh- prefix),
232
+ * skips base attribute dash validation, and skips properties already set on the element
226
233
  * @param spawnContext - Optional spawn context containing enhancement config and synthesizer element
227
234
  * @returns Object with parsed attribute values ready for initVals
228
235
  */
229
236
  export function parseWithAttrs(element, attrPatterns, allowUnprefixed, spawnContext) {
230
237
  // Extract synthesizerElement from spawnContext for backward compatibility
231
238
  const synthesizerElement = spawnContext?.synthesizerElement;
232
- // Validate base attribute if present
233
- if ('base' in attrPatterns) {
239
+ const isCustomElementMode = allowUnprefixed === true;
240
+ // Validate base attribute if present (skip in custom element mode)
241
+ if ('base' in attrPatterns && !isCustomElementMode) {
234
242
  const baseValue = attrPatterns.base;
235
243
  if (!hasDashOrNonASCII(baseValue)) {
236
244
  throw new Error(`Invalid base attribute "${baseValue}": must contain a dash (-) or non-ASCII character. ` +
@@ -284,6 +292,13 @@ export function parseWithAttrs(element, attrPatterns, allowUnprefixed, spawnCont
284
292
  }
285
293
  // Second pass: read attributes and parse values
286
294
  for (const [key, { attrName, config }] of resolvedAttrs) {
295
+ // In custom element mode, skip properties already set on the element
296
+ if (isCustomElementMode) {
297
+ const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
298
+ if (mapsTo !== '.' && element[mapsTo] !== undefined) {
299
+ continue;
300
+ }
301
+ }
287
302
  const attrValue = getAttributeValue(element, attrName, allowUnprefixed);
288
303
  // Create parser context
289
304
  const parserContext = {
package/parseWithAttrs.ts CHANGED
@@ -178,14 +178,20 @@ function hasDashOrNonASCII(str: string): boolean {
178
178
  * Gets attribute value with smart enh- prefix handling
179
179
  * @param element - The element to read from
180
180
  * @param attrName - The attribute name (without enh- prefix)
181
- * @param allowUnprefixed - Pattern (string or RegExp) that element tag name must match to allow unprefixed attributes
181
+ * @param allowUnprefixed - Pattern (string or RegExp) that element tag name must match to allow unprefixed attributes,
182
+ * or `true` for custom element mode (read attributes directly, no enh- prefix)
182
183
  * @returns The attribute value or null
183
184
  */
184
185
  function getAttributeValue(
185
186
  element: Element,
186
187
  attrName: string,
187
- allowUnprefixed?: string | RegExp
188
+ allowUnprefixed?: string | RegExp | true
188
189
  ): string | null {
190
+ // Custom element mode - read attribute directly, no enh- prefix
191
+ if (allowUnprefixed === true) {
192
+ return element.getAttribute(attrName);
193
+ }
194
+
189
195
  const { localName } = element;
190
196
  const isCustomElement = localName.includes('-');
191
197
  const isSVGElement = typeof SVGElement !== 'undefined' && element instanceof SVGElement;
@@ -261,21 +267,25 @@ function getDefaultParser(instanceOf?: string | Function): ParserFunction {
261
267
  * Parses attributes from an element based on AttrPatterns configuration
262
268
  * @param element - The DOM element to read attributes from
263
269
  * @param attrPatterns - The attribute patterns configuration
264
- * @param allowUnprefixed - Pattern (string or RegExp) that element tag name must match to allow unprefixed attributes
270
+ * @param allowUnprefixed - Pattern (string or RegExp) that element tag name must match to allow unprefixed attributes,
271
+ * or `true` for custom element mode: reads attributes directly (no enh- prefix),
272
+ * skips base attribute dash validation, and skips properties already set on the element
265
273
  * @param spawnContext - Optional spawn context containing enhancement config and synthesizer element
266
274
  * @returns Object with parsed attribute values ready for initVals
267
275
  */
268
276
  export function parseWithAttrs<T = any>(
269
277
  element: Element,
270
278
  attrPatterns: AttrPatterns<T>,
271
- allowUnprefixed?: string | RegExp,
279
+ allowUnprefixed?: string | RegExp | true,
272
280
  spawnContext?: SpawnContext<T>
273
281
  ): Partial<T> {
274
282
  // Extract synthesizerElement from spawnContext for backward compatibility
275
283
  const synthesizerElement = spawnContext?.synthesizerElement;
276
284
 
277
- // Validate base attribute if present
278
- if ('base' in attrPatterns) {
285
+ const isCustomElementMode = allowUnprefixed === true;
286
+
287
+ // Validate base attribute if present (skip in custom element mode)
288
+ if ('base' in attrPatterns && !isCustomElementMode) {
279
289
  const baseValue = attrPatterns.base as string;
280
290
  if (!hasDashOrNonASCII(baseValue)) {
281
291
  throw new Error(
@@ -341,6 +351,14 @@ export function parseWithAttrs<T = any>(
341
351
 
342
352
  // Second pass: read attributes and parse values
343
353
  for (const [key, { attrName, config }] of resolvedAttrs) {
354
+ // In custom element mode, skip properties already set on the element
355
+ if (isCustomElementMode) {
356
+ const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
357
+ if (mapsTo !== '.' && (element as any)[mapsTo as string] !== undefined) {
358
+ continue;
359
+ }
360
+ }
361
+
344
362
  const attrValue = getAttributeValue(element, attrName, allowUnprefixed);
345
363
 
346
364
  // Create parser context
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,38 +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('?.')) {
100
+ // Apply aliases to the RHS path
101
+ const aliased = applyAliases(value, aliasMap);
29
102
  // 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);
33
- let current = source;
34
- for (const part of parts) {
35
- if (current == null)
36
- break;
37
- current = current[part];
38
- }
39
- result[key] = current;
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);
40
106
  }
41
107
  else {
42
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,40 +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('?.')) {
128
+ // Apply aliases to the RHS path
129
+ const aliased = applyAliases(value, aliasMap);
130
+
32
131
  // 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);
36
- let current = source;
37
- for (const part of parts) {
38
- if (current == null) break;
39
- current = current[part];
40
- }
41
- result[key] = current;
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);
42
136
  } else {
43
137
  result[key] = value;
44
138
  }