assign-gingerly 0.0.82 → 0.0.84

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
@@ -100,7 +100,8 @@ assignFrom adds support for:
100
100
  3. Protocol resolution (`globalThis://`, `localStorage://`, custom sync protocols).
101
101
  4. Handler plugins via the ` =>` operator for custom logic (fire-and-forget in sync mode, awaitable in async mode).
102
102
  5. Looped substitution with `where_x_in` / `where_y_in` / `where_z_in` for expanding template patterns into multiple concrete assignments.
103
- 6. Spread merging via the `"..."` key.
103
+ 6. Dynamic substitutions via the `substitutions` option for injecting runtime string values into path segments. See [docs/substitutions.md](docs/substitutions.md).
104
+ 7. Spread merging via the `"..."` key.
104
105
 
105
106
  Example:
106
107
 
package/assignFrom.js CHANGED
@@ -49,6 +49,7 @@ function resolveTernaryValue(value, source, options) {
49
49
  return getValue(value, source, {
50
50
  withMethods: options.withMethods,
51
51
  aka: options.aka,
52
+ substitutions: options.substitutions,
52
53
  protocols: options.protocols,
53
54
  root: options.root
54
55
  });
@@ -61,6 +62,7 @@ function resolveTernaryValue(value, source, options) {
61
62
  return getValue(value, source, {
62
63
  withMethods: options.withMethods,
63
64
  aka: options.aka,
65
+ substitutions: options.substitutions,
64
66
  protocols: options.protocols,
65
67
  root: options.root
66
68
  });
@@ -381,7 +383,7 @@ function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, option
381
383
  const ids = getEffectiveIds(options);
382
384
  if (!ids)
383
385
  return;
384
- const { withMethods, aka, akaMethods, protocols, from } = options;
386
+ const { withMethods, aka, akaMethods, protocols, from, substitutions } = options;
385
387
  for (const key of idRefNormalKeys) {
386
388
  const parsed = parseIdRef(key);
387
389
  if (!parsed)
@@ -391,11 +393,11 @@ function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, option
391
393
  continue;
392
394
  const value = expandedPattern[key];
393
395
  if (parsed.remainingPath) {
394
- const resolvedValue = getValues({ __v: value }, from, { withMethods, aka, akaMethods, protocols, root: target, permissionProcessor });
396
+ const resolvedValue = getValues({ __v: value }, from, { withMethods, aka, akaMethods, substitutions, protocols, root: target, permissionProcessor });
395
397
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options, permissionProcessor);
396
398
  }
397
399
  else {
398
- const resolvedValue = getValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, protocols, root: target, permissionProcessor });
400
+ const resolvedValue = getValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, substitutions, protocols, root: target, permissionProcessor });
399
401
  if (!('__v' in resolvedValue)) {
400
402
  assignGingerly(el, resolvedValue, options, permissionProcessor);
401
403
  }
@@ -458,6 +460,7 @@ export function assignFrom(target, pattern, options, permissionProcessor) {
458
460
  withMethods: options.withMethods,
459
461
  aka: options.aka,
460
462
  akaMethods: options.akaMethods,
463
+ substitutions: options.substitutions,
461
464
  protocols: options.protocols,
462
465
  root: target,
463
466
  permissionProcessor
package/assignFrom.ts CHANGED
@@ -58,6 +58,7 @@ function resolveTernaryValue(value: any, source: any, options: AssignFromOptions
58
58
  return getValue(value, source, {
59
59
  withMethods: options.withMethods,
60
60
  aka: options.aka,
61
+ substitutions: options.substitutions,
61
62
  protocols: options.protocols,
62
63
  root: options.root
63
64
  });
@@ -70,6 +71,7 @@ function resolveTernaryValue(value: any, source: any, options: AssignFromOptions
70
71
  return getValue(value, source, {
71
72
  withMethods: options.withMethods,
72
73
  aka: options.aka,
74
+ substitutions: options.substitutions,
73
75
  protocols: options.protocols,
74
76
  root: options.root
75
77
  });
@@ -395,7 +397,7 @@ function processIdRefNormalKeys(
395
397
  const ids = getEffectiveIds(options);
396
398
  if (!ids) return;
397
399
 
398
- const { withMethods, aka, akaMethods, protocols, from } = options;
400
+ const { withMethods, aka, akaMethods, protocols, from, substitutions } = options;
399
401
  for (const key of idRefNormalKeys) {
400
402
  const parsed = parseIdRef(key);
401
403
  if (!parsed) continue;
@@ -407,14 +409,14 @@ function processIdRefNormalKeys(
407
409
  if (parsed.remainingPath) {
408
410
  const resolvedValue = getValues(
409
411
  { __v: value }, from,
410
- { withMethods, aka, akaMethods, protocols, root: target, permissionProcessor }
412
+ { withMethods, aka, akaMethods, substitutions, protocols, root: target, permissionProcessor }
411
413
  );
412
414
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options, permissionProcessor);
413
415
  } else {
414
416
  const resolvedValue = getValues(
415
417
  typeof value === 'object' && value !== null ? value : { __v: value },
416
418
  from,
417
- { withMethods, aka, akaMethods, protocols, root: target, permissionProcessor }
419
+ { withMethods, aka, akaMethods, substitutions, protocols, root: target, permissionProcessor }
418
420
  );
419
421
  if (!('__v' in resolvedValue)) {
420
422
  assignGingerly(el, resolvedValue, options, permissionProcessor);
@@ -486,6 +488,7 @@ export function assignFrom(
486
488
  withMethods: options.withMethods,
487
489
  aka: options.aka,
488
490
  akaMethods: options.akaMethods,
491
+ substitutions: options.substitutions,
489
492
  protocols: options.protocols,
490
493
  root: target,
491
494
  permissionProcessor
@@ -35,6 +35,7 @@ export async function assignFromAsync(target, pattern, options, permissionProces
35
35
  withMethods: options.withMethods,
36
36
  aka: options.aka,
37
37
  akaMethods: options.akaMethods,
38
+ substitutions: options.substitutions,
38
39
  protocols: options.protocols,
39
40
  root: target,
40
41
  permissionProcessor
@@ -47,7 +48,7 @@ export async function assignFromAsync(target, pattern, options, permissionProces
47
48
  if (idRefNormalKeys.length > 0 && (options.pin || options.at)) {
48
49
  const ids = { ...options.pin, ...options.at };
49
50
  const { resolveIdVariable, parseIdRef } = await import('./resolve/resolveIdRef.js');
50
- const { withMethods, aka, akaMethods, protocols, from } = options;
51
+ const { withMethods, aka, akaMethods, protocols, from, substitutions } = options;
51
52
  for (const key of idRefNormalKeys) {
52
53
  const parsed = parseIdRef(key);
53
54
  if (!parsed)
@@ -58,13 +59,13 @@ export async function assignFromAsync(target, pattern, options, permissionProces
58
59
  const value = expandedPattern[key];
59
60
  if (parsed.remainingPath) {
60
61
  // Resolve the RHS value
61
- const resolvedValue = await resolveValues({ __v: value }, from, { withMethods, aka, akaMethods, protocols, root: el, permissionProcessor });
62
+ const resolvedValue = await resolveValues({ __v: value }, from, { withMethods, aka, akaMethods, substitutions, protocols, root: el, permissionProcessor });
62
63
  // Apply remaining path on the resolved element
63
64
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options, permissionProcessor);
64
65
  }
65
66
  else {
66
67
  // No remaining path — resolve and assign directly to the element
67
- const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, protocols, root: el, permissionProcessor });
68
+ const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, from, { withMethods, aka, akaMethods, substitutions, protocols, root: el, permissionProcessor });
68
69
  if ('__v' in resolvedValue) {
69
70
  // Single value — can't assign to element root without a path
70
71
  }
@@ -69,6 +69,7 @@ export async function assignFromAsync(
69
69
  withMethods: options.withMethods,
70
70
  aka: options.aka,
71
71
  akaMethods: options.akaMethods,
72
+ substitutions: options.substitutions,
72
73
  protocols: options.protocols,
73
74
  root: target,
74
75
  permissionProcessor
@@ -84,7 +85,7 @@ export async function assignFromAsync(
84
85
  if (idRefNormalKeys.length > 0 && (options.pin || options.at)) {
85
86
  const ids = { ...options.pin, ...options.at };
86
87
  const { resolveIdVariable, parseIdRef } = await import('./resolve/resolveIdRef.js');
87
- const { withMethods, aka, akaMethods, protocols, from } = options;
88
+ const { withMethods, aka, akaMethods, protocols, from, substitutions } = options;
88
89
  for (const key of idRefNormalKeys) {
89
90
  const parsed = parseIdRef(key);
90
91
  if (!parsed) continue;
@@ -97,7 +98,7 @@ export async function assignFromAsync(
97
98
  // Resolve the RHS value
98
99
  const resolvedValue = await resolveValues(
99
100
  { __v: value }, from,
100
- { withMethods, aka, akaMethods, protocols, root: el, permissionProcessor }
101
+ { withMethods, aka, akaMethods, substitutions, protocols, root: el, permissionProcessor }
101
102
  );
102
103
  // Apply remaining path on the resolved element
103
104
  assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options, permissionProcessor);
@@ -106,7 +107,7 @@ export async function assignFromAsync(
106
107
  const resolvedValue = await resolveValues(
107
108
  typeof value === 'object' && value !== null ? value : { __v: value },
108
109
  from,
109
- { withMethods, aka, akaMethods, protocols, root: el, permissionProcessor }
110
+ { withMethods, aka, akaMethods, substitutions, protocols, root: el, permissionProcessor }
110
111
  );
111
112
  if ('__v' in resolvedValue) {
112
113
  // Single value — can't assign to element root without a path
@@ -303,13 +303,25 @@ const raConfig = {
303
303
  }
304
304
  }
305
305
 
306
+ // withAttrs configuration for parsing element attributes
307
+ const withAttrs = {
308
+ base: 'user-counter',
309
+ count: '${base}-count',
310
+ _count: {
311
+ instanceOf: 'Number',
312
+ valIfNull: 0,
313
+ },
314
+ username: '${base}-username',
315
+ };
316
+
306
317
  /** @type {ElMakerConfig<AP>} */
307
318
  const features = {
308
319
  assignFeatures: {
309
320
  roundabout: {
310
321
  customData: {
311
322
  raConfig,
312
- }
323
+ },
324
+ withAttrs
313
325
  },
314
326
  templateMaker: {}
315
327
  }
@@ -361,6 +373,20 @@ const raConfig = {
361
373
  };
362
374
  ```
363
375
 
376
+ ## How can I set focus after a delay?
377
+
378
+ Work is underway to improve the DX a bit, but for now:
379
+
380
+ ```JS
381
+ {
382
+ delay:10, //milliseconds
383
+ ifAllOf: ['expanded'],
384
+ assign: {
385
+ set($.querySelector('a').focus()).to({}),
386
+ }
387
+ },
388
+ ```
389
+
364
390
  ## Step 8
365
391
 
366
392
  Run `node el-maker.mjs` (or `npm run build-el-maker` if your `package.json` includes a watch script) to regenerate `el-maker.json`.
@@ -267,6 +267,17 @@ export interface IAssignGingerlyOptions {
267
267
  */
268
268
  aka?: Record<string, string>;
269
269
 
270
+ /**
271
+ * Value substitutions for path segments.
272
+ * Each key names a placeholder; the value is a `?.`-delimited path resolved
273
+ * against the source (`from`) object. The resolved string value replaces any
274
+ * matching whole path segment in RHS path strings before path evaluation.
275
+ *
276
+ * Substitution values must be strings and must not contain the `?.` sequence,
277
+ * otherwise an error is thrown.
278
+ */
279
+ substitutions?: Record<string, string>;
280
+
270
281
  /**
271
282
  * Shorthand for binding method aliases from the source object.
272
283
  * Each entry maps an alias to a method name and is normalized into
@@ -357,6 +368,16 @@ export interface AssignFromOptions {
357
368
  /** Alias mappings for path segments */
358
369
  aka?: Record<string, string>;
359
370
 
371
+ /**
372
+ * Value substitutions for path segments.
373
+ * Each key names a placeholder; the value is a `?.`-delimited path resolved
374
+ * against `from`. The resolved string value replaces any matching whole path
375
+ * segment in RHS path strings before path evaluation.
376
+ *
377
+ * Substitution values must be strings and must not contain `?.`.
378
+ */
379
+ substitutions?: Record<string, string>;
380
+
360
381
  /** AbortSignal for cleanup */
361
382
  signal?: AbortSignal;
362
383
 
@@ -2,6 +2,7 @@ import {RAConfig} from '../roundabout/types.js';
2
2
  import {FontFaceFeatureConfig} from '../font-face-feature/types.js';
3
3
  import {CustomData as TSCD} from '../truth-sourcer/types.js';
4
4
  import {CustomData as FUCD} from '../face-up/types.js';
5
+ import {AttrPatterns} from '../assign-gingerly/types.js';
5
6
 
6
7
  export interface ElMakerConfig<AllProps = any, TActions = AllProps> {
7
8
  assignFeatures: {
@@ -10,6 +11,7 @@ export interface ElMakerConfig<AllProps = any, TActions = AllProps> {
10
11
  customData: {
11
12
  raConfig: RAConfig<AllProps, TActions, TActions>
12
13
  }
14
+ withAttrs: AttrPatterns<AllProps>
13
15
  },
14
16
  fontMgr?: {
15
17
  spawn?: string,
@@ -161,6 +161,13 @@ export interface RAConfig<
161
161
  initialPropVals?: Partial<{[key in keyof TProps & string]: unknown}>,
162
162
  }
163
163
 
164
+ // export interface RoundaboutFeatureConfig<
165
+ // TProps = unknown, TActions = TProps, ETProps = TProps,
166
+ // TCustomData = unknown, TEvents extends string = string>{
167
+ // RAConfig: RAConfig<TProps, TActions, ETProps, TCustomData, TEvents>,
168
+
169
+ // }
170
+
164
171
  export interface RoundaboutOptions<TProps = unknown, TActions = TProps, ETProps = TProps, EventTypes extends string = string> extends RAConfig<TProps, TActions, ETProps, unknown, EventTypes> {
165
172
  vm?: TProps & TActions & RoundaboutReady,
166
173
  //for enhanced elements, pass in the container, referenced via $0.
@@ -217,7 +224,14 @@ export interface WeakRefConfig<TProps = any> {
217
224
  /**
218
225
  * Properties to automatically wrap in WeakRef
219
226
  */
220
- properties: Array<keyof TProps & string>;
227
+ properties?: Array<keyof TProps & string>;
228
+
229
+ /**
230
+ * Array-valued properties whose elements should be stored as WeakRefs.
231
+ * When accessed, the getter returns a new array with each WeakRef dereferenced.
232
+ * Collected elements appear as `undefined` in their original positions.
233
+ */
234
+ listProperties?: Array<keyof TProps & string>;
221
235
 
222
236
  /**
223
237
  * Logging behavior when deref returns null/undefined
@@ -0,0 +1,43 @@
1
+ import { FeatureSpawnContext } from "../assign-gingerly/types";
2
+
3
+ /**
4
+ * Public properties of the SwipeDismissFeature.
5
+ * These can be set directly on the feature instance or initialized via attributes.
6
+ */
7
+ export interface SwipeDismissProps {
8
+ /** Axis along which the dismiss gesture is measured. */
9
+ axis: 'x' | 'y';
10
+ /**
11
+ * Direction that counts toward dismissal.
12
+ * 1 = right/down, -1 = left/up.
13
+ * Set to 'both' to allow swiping in either direction (e.g. toasts/snackbars).
14
+ */
15
+ direction: 1 | -1 | 'both';
16
+ /** Fraction of the panel size that triggers commit. */
17
+ distanceThreshold: number;
18
+ /** Velocity threshold in px/ms; a fast flick commits even under distanceThreshold. */
19
+ velocityThreshold: number;
20
+ /** CSS selector for the drag handle. Defaults to the host element. */
21
+ handleSelector: string | null;
22
+ /** CSS selector for the panel that visually follows the drag. Defaults to the handle. */
23
+ panelSelector: string | null;
24
+ /** Called on every pointermove with the current delta and fraction of the threshold. */
25
+ onProgress: ((deltaPx: number, fraction: number) => void) | null;
26
+ /** Called when the gesture crosses the commit threshold. */
27
+ onCommit: (() => void) | null;
28
+ /** Called when the gesture is released before the commit threshold. */
29
+ onCancel: (() => void) | null;
30
+ }
31
+
32
+ /**
33
+ * Internal state of the feature.
34
+ */
35
+ export interface AllProps extends SwipeDismissProps {
36
+ /** WeakRef to the host custom element. */
37
+ hostRef: WeakRef<Element>;
38
+ }
39
+
40
+ export type AP = AllProps;
41
+ export type PAP = Partial<AllProps>;
42
+
43
+ export { FeatureSpawnContext };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.82",
3
+ "version": "0.0.84",
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": {
@@ -171,6 +171,7 @@ export async function processHandlerCommands(target, handlerKeys, pattern, optio
171
171
  resolvedParams = getValues(config.get, options.from, {
172
172
  withMethods: options.withMethods,
173
173
  aka: options.aka,
174
+ substitutions: options.substitutions,
174
175
  protocols: options.protocols,
175
176
  root: target
176
177
  });
@@ -180,6 +181,7 @@ export async function processHandlerCommands(target, handlerKeys, pattern, optio
180
181
  const asyncResolved = await resolveValues(config.resolve, options.from, {
181
182
  withMethods: options.withMethods,
182
183
  aka: options.aka,
184
+ substitutions: options.substitutions,
183
185
  protocols: options.protocols,
184
186
  root: target
185
187
  });
@@ -192,6 +192,7 @@ export async function processHandlerCommands(
192
192
  resolvedParams = getValues(config.get, options.from, {
193
193
  withMethods: options.withMethods,
194
194
  aka: options.aka,
195
+ substitutions: options.substitutions,
195
196
  protocols: options.protocols,
196
197
  root: target
197
198
  });
@@ -201,6 +202,7 @@ export async function processHandlerCommands(
201
202
  const asyncResolved = await resolveValues(config.resolve, options.from, {
202
203
  withMethods: options.withMethods,
203
204
  aka: options.aka,
205
+ substitutions: options.substitutions,
204
206
  protocols: options.protocols,
205
207
  root: target
206
208
  });
@@ -67,6 +67,47 @@ function applyAliases(path, aliasMap) {
67
67
  const substituted = parts.map(part => aliasMap.get(part) ?? part);
68
68
  return substituted.join('?.');
69
69
  }
70
+ /**
71
+ * Apply value substitutions to a path string.
72
+ * Replaces complete tokens between `?.` delimiters with their resolved values.
73
+ * Substitutions are applied before aliases.
74
+ */
75
+ function applySubstitutions(path, substitutionMap) {
76
+ if (!substitutionMap || substitutionMap.size === 0)
77
+ return path;
78
+ const parts = path.split('?.');
79
+ const substituted = parts.map(part => substitutionMap.get(part) ?? part);
80
+ return substituted.join('?.');
81
+ }
82
+ /**
83
+ * Resolve substitution values declared in options.substitutions.
84
+ * Each substitution path is resolved against the source object without
85
+ * applying substitutions itself, to avoid infinite recursion.
86
+ * Resolved values must be strings and must not contain `?.`.
87
+ */
88
+ function resolveSubstitutions(substitutions, source, options) {
89
+ const map = new Map();
90
+ if (!substitutions)
91
+ return map;
92
+ for (const [name, path] of Object.entries(substitutions)) {
93
+ // Resolve the substitution path against the source, but do not apply
94
+ // substitutions to that path. Root references ($0) are also disabled
95
+ // for substitution paths so values are sourced from `from` only.
96
+ const resolved = getValue(path, source, options
97
+ ? { ...options, substitutions: undefined, root: undefined }
98
+ : undefined);
99
+ if (resolved === null || typeof resolved === 'undefined')
100
+ continue;
101
+ if (typeof resolved !== 'string') {
102
+ throw new Error(`Substitution '${name}' must resolve to a string, got ${typeof resolved}`);
103
+ }
104
+ if (resolved.includes('?.')) {
105
+ throw new Error(`Substitution '${name}' resolved to a string containing '?.', which would alter the path structure: '${resolved}'`);
106
+ }
107
+ map.set(name, resolved);
108
+ }
109
+ return map;
110
+ }
70
111
  /**
71
112
  * Resolve a special root-reference token at the start of a string.
72
113
  * '$0' refers to the first argument passed to assignFrom / resolveValues.
@@ -159,12 +200,13 @@ function getProtocolValue(value, protocols, options) {
159
200
  * Resolve path strings and protocols within an array (synchronous).
160
201
  * Recurses into nested arrays and plain objects.
161
202
  */
162
- function getArray(arr, source, aliasMap, withMethods, protocols, options) {
203
+ function getArray(arr, source, aliasMap, withMethods, protocols, options, substitutionMap) {
163
204
  const permissionProcessor = options?.permissionProcessor;
164
205
  const result = [];
165
206
  for (const item of arr) {
166
207
  if (typeof item === 'string' && item.startsWith('?.')) {
167
- const aliased = applyAliases(item, aliasMap);
208
+ const substituted = applySubstitutions(item, substitutionMap);
209
+ const aliased = applyAliases(substituted, aliasMap);
168
210
  const parts = parseCachedPath(aliased);
169
211
  result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods, permissionProcessor));
170
212
  }
@@ -174,7 +216,8 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
174
216
  result.push(source);
175
217
  }
176
218
  else {
177
- const aliased = applyAliases(rootRef.path, aliasMap);
219
+ const substitutedPath = applySubstitutions(rootRef.path, substitutionMap);
220
+ const aliased = applyAliases(substitutedPath, aliasMap);
178
221
  const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
179
222
  const parts = parseCachedPath(normalizedPath);
180
223
  result.push(parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods, permissionProcessor));
@@ -184,7 +227,7 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
184
227
  result.push(getProtocolValue(item, protocols, options));
185
228
  }
186
229
  else if (Array.isArray(item)) {
187
- result.push(getArray(item, source, aliasMap, withMethods, protocols, options));
230
+ result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
188
231
  }
189
232
  else if (item && typeof item === 'object') {
190
233
  const proto = Object.getPrototypeOf(item);
@@ -215,12 +258,14 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
215
258
  */
216
259
  export function getValues(pattern, source, options) {
217
260
  const { aliasMap, withMethods } = normalizeAliasOptions(options);
261
+ const substitutionMap = resolveSubstitutions(options?.substitutions, source, options);
218
262
  const protocols = options?.protocols;
219
263
  const permissionProcessor = options?.permissionProcessor;
220
264
  const result = {};
221
265
  for (const [key, value] of Object.entries(pattern)) {
222
266
  if (typeof value === 'string' && value.startsWith('?.')) {
223
- const aliased = applyAliases(value, aliasMap);
267
+ const substituted = applySubstitutions(value, substitutionMap);
268
+ const aliased = applyAliases(substituted, aliasMap);
224
269
  const parts = parseCachedPath(aliased);
225
270
  result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods, permissionProcessor);
226
271
  }
@@ -230,7 +275,8 @@ export function getValues(pattern, source, options) {
230
275
  result[key] = source;
231
276
  }
232
277
  else {
233
- const aliased = applyAliases(rootRef.path, aliasMap);
278
+ const substitutedPath = applySubstitutions(rootRef.path, substitutionMap);
279
+ const aliased = applyAliases(substitutedPath, aliasMap);
234
280
  const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
235
281
  const parts = parseCachedPath(normalizedPath);
236
282
  result[key] = parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods, permissionProcessor);
@@ -240,7 +286,7 @@ export function getValues(pattern, source, options) {
240
286
  result[key] = getProtocolValue(value, protocols, options);
241
287
  }
242
288
  else if (Array.isArray(value)) {
243
- result[key] = getArray(value, source, aliasMap, withMethods, protocols, options);
289
+ result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
244
290
  }
245
291
  else if (typeof value === 'object' && value !== null) {
246
292
  const proto = Object.getPrototypeOf(value);
@@ -274,10 +320,12 @@ export function getValue(path, source, options) {
274
320
  else if (!path.startsWith('?.')) {
275
321
  return path;
276
322
  }
277
- let aliased = path;
323
+ const substitutionMap = resolveSubstitutions(options?.substitutions, source, options);
324
+ const substituted = applySubstitutions(path, substitutionMap);
325
+ let aliased = substituted;
278
326
  const { aliasMap } = normalizeAliasOptions(options);
279
327
  if (aliasMap.size > 0) {
280
- aliased = applyAliases(path, aliasMap);
328
+ aliased = applyAliases(substituted, aliasMap);
281
329
  }
282
330
  const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
283
331
  const parts = parseCachedPath(normalizedPath);
@@ -85,6 +85,59 @@ function applyAliases(path: string, aliasMap: Map<string, string>): string {
85
85
  return substituted.join('?.');
86
86
  }
87
87
 
88
+ /**
89
+ * Apply value substitutions to a path string.
90
+ * Replaces complete tokens between `?.` delimiters with their resolved values.
91
+ * Substitutions are applied before aliases.
92
+ */
93
+ function applySubstitutions(path: string, substitutionMap?: Map<string, string>): string {
94
+ if (!substitutionMap || substitutionMap.size === 0) return path;
95
+ const parts = path.split('?.');
96
+ const substituted = parts.map(part => substitutionMap.get(part) ?? part);
97
+ return substituted.join('?.');
98
+ }
99
+
100
+ /**
101
+ * Resolve substitution values declared in options.substitutions.
102
+ * Each substitution path is resolved against the source object without
103
+ * applying substitutions itself, to avoid infinite recursion.
104
+ * Resolved values must be strings and must not contain `?.`.
105
+ */
106
+ function resolveSubstitutions(
107
+ substitutions: Record<string, string> | undefined,
108
+ source: any,
109
+ options: GetValuesOptions | undefined
110
+ ): Map<string, string> {
111
+ const map = new Map<string, string>();
112
+ if (!substitutions) return map;
113
+
114
+ for (const [name, path] of Object.entries(substitutions)) {
115
+ // Resolve the substitution path against the source, but do not apply
116
+ // substitutions to that path. Root references ($0) are also disabled
117
+ // for substitution paths so values are sourced from `from` only.
118
+ const resolved = getValue(
119
+ path,
120
+ source,
121
+ options
122
+ ? { ...options, substitutions: undefined, root: undefined }
123
+ : undefined
124
+ );
125
+ if(resolved === null || typeof resolved === 'undefined') continue;
126
+ if (typeof resolved !== 'string') {
127
+ throw new Error(
128
+ `Substitution '${name}' must resolve to a string, got ${typeof resolved}`
129
+ );
130
+ }
131
+ if (resolved.includes('?.')) {
132
+ throw new Error(
133
+ `Substitution '${name}' resolved to a string containing '?.', which would alter the path structure: '${resolved}'`
134
+ );
135
+ }
136
+ map.set(name, resolved);
137
+ }
138
+ return map;
139
+ }
140
+
88
141
  /**
89
142
  * Resolve a special root-reference token at the start of a string.
90
143
  * '$0' refers to the first argument passed to assignFrom / resolveValues.
@@ -200,13 +253,15 @@ function getArray(
200
253
  aliasMap: Map<string, string>,
201
254
  withMethods: Set<string> | undefined,
202
255
  protocols: Record<string, (key: string) => any> | undefined,
203
- options?: GetValuesOptions
256
+ options?: GetValuesOptions,
257
+ substitutionMap?: Map<string, string>
204
258
  ): any[] {
205
259
  const permissionProcessor = options?.permissionProcessor;
206
260
  const result: any[] = [];
207
261
  for (const item of arr) {
208
262
  if (typeof item === 'string' && item.startsWith('?.')) {
209
- const aliased = applyAliases(item, aliasMap);
263
+ const substituted = applySubstitutions(item, substitutionMap);
264
+ const aliased = applyAliases(substituted, aliasMap);
210
265
  const parts = parseCachedPath(aliased);
211
266
  result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods, permissionProcessor));
212
267
  } else if (typeof item === 'string' && item.startsWith('$0')) {
@@ -214,7 +269,8 @@ function getArray(
214
269
  if (rootRef === null) {
215
270
  result.push(source);
216
271
  } else {
217
- const aliased = applyAliases(rootRef.path, aliasMap);
272
+ const substitutedPath = applySubstitutions(rootRef.path, substitutionMap);
273
+ const aliased = applyAliases(substitutedPath, aliasMap);
218
274
  const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
219
275
  const parts = parseCachedPath(normalizedPath);
220
276
  result.push(parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods, permissionProcessor));
@@ -222,7 +278,7 @@ function getArray(
222
278
  } else if (typeof item === 'string' && protocols && hasProtocol(item)) {
223
279
  result.push(getProtocolValue(item, protocols, options));
224
280
  } else if (Array.isArray(item)) {
225
- result.push(getArray(item, source, aliasMap, withMethods, protocols, options));
281
+ result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
226
282
  } else if (item && typeof item === 'object') {
227
283
  const proto = Object.getPrototypeOf(item);
228
284
  if (proto === Object.prototype || proto === null) {
@@ -255,6 +311,7 @@ export function getValues(
255
311
  options?: GetValuesOptions
256
312
  ): Record<string, any> {
257
313
  const { aliasMap, withMethods } = normalizeAliasOptions(options);
314
+ const substitutionMap = resolveSubstitutions(options?.substitutions, source, options);
258
315
 
259
316
  const protocols = options?.protocols;
260
317
  const permissionProcessor = options?.permissionProcessor;
@@ -262,7 +319,8 @@ export function getValues(
262
319
  const result: Record<string, any> = {};
263
320
  for (const [key, value] of Object.entries(pattern)) {
264
321
  if (typeof value === 'string' && value.startsWith('?.')) {
265
- const aliased = applyAliases(value, aliasMap);
322
+ const substituted = applySubstitutions(value, substitutionMap);
323
+ const aliased = applyAliases(substituted, aliasMap);
266
324
  const parts = parseCachedPath(aliased);
267
325
  result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods, permissionProcessor);
268
326
  } else if (typeof value === 'string' && value.startsWith('$0')) {
@@ -270,7 +328,8 @@ export function getValues(
270
328
  if (rootRef === null) {
271
329
  result[key] = source;
272
330
  } else {
273
- const aliased = applyAliases(rootRef.path, aliasMap);
331
+ const substitutedPath = applySubstitutions(rootRef.path, substitutionMap);
332
+ const aliased = applyAliases(substitutedPath, aliasMap);
274
333
  const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
275
334
  const parts = parseCachedPath(normalizedPath);
276
335
  result[key] = parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods, permissionProcessor);
@@ -278,7 +337,7 @@ export function getValues(
278
337
  } else if (typeof value === 'string' && protocols && hasProtocol(value)) {
279
338
  result[key] = getProtocolValue(value, protocols, options);
280
339
  } else if (Array.isArray(value)) {
281
- result[key] = getArray(value, source, aliasMap, withMethods, protocols, options);
340
+ result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
282
341
  } else if (typeof value === 'object' && value !== null) {
283
342
  const proto = Object.getPrototypeOf(value);
284
343
  if (proto === Object.prototype || proto === null) {
@@ -314,10 +373,13 @@ export function getValue(
314
373
  return path;
315
374
  }
316
375
 
317
- let aliased = path;
376
+ const substitutionMap = resolveSubstitutions(options?.substitutions, source, options);
377
+
378
+ const substituted = applySubstitutions(path, substitutionMap);
379
+ let aliased = substituted;
318
380
  const { aliasMap } = normalizeAliasOptions(options);
319
381
  if (aliasMap.size > 0) {
320
- aliased = applyAliases(path, aliasMap);
382
+ aliased = applyAliases(substituted, aliasMap);
321
383
  }
322
384
 
323
385
  const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
@@ -267,6 +267,17 @@ export interface IAssignGingerlyOptions {
267
267
  */
268
268
  aka?: Record<string, string>;
269
269
 
270
+ /**
271
+ * Value substitutions for path segments.
272
+ * Each key names a placeholder; the value is a `?.`-delimited path resolved
273
+ * against the source (`from`) object. The resolved string value replaces any
274
+ * matching whole path segment in RHS path strings before path evaluation.
275
+ *
276
+ * Substitution values must be strings and must not contain the `?.` sequence,
277
+ * otherwise an error is thrown.
278
+ */
279
+ substitutions?: Record<string, string>;
280
+
270
281
  /**
271
282
  * Shorthand for binding method aliases from the source object.
272
283
  * Each entry maps an alias to a method name and is normalized into
@@ -357,6 +368,16 @@ export interface AssignFromOptions {
357
368
  /** Alias mappings for path segments */
358
369
  aka?: Record<string, string>;
359
370
 
371
+ /**
372
+ * Value substitutions for path segments.
373
+ * Each key names a placeholder; the value is a `?.`-delimited path resolved
374
+ * against `from`. The resolved string value replaces any matching whole path
375
+ * segment in RHS path strings before path evaluation.
376
+ *
377
+ * Substitution values must be strings and must not contain `?.`.
378
+ */
379
+ substitutions?: Record<string, string>;
380
+
360
381
  /** AbortSignal for cleanup */
361
382
  signal?: AbortSignal;
362
383