assign-gingerly 0.0.81 → 0.0.83
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/DX/emojis.js +2 -1
- package/DX/emojis.ts +2 -1
- package/DX/paths.js +14 -14
- package/DX/paths.ts +155 -156
- package/README.md +2 -1
- package/assignFrom.js +6 -3
- package/assignFrom.ts +6 -3
- package/assignFromAsync.js +4 -3
- package/assignFromAsync.ts +4 -3
- package/inferencer/types/NewCustomElement.md +8 -5
- package/inferencer/types/NewHTMLFirstCustomElement.md +75 -14
- package/inferencer/types/assign-gingerly/types.d.ts +21 -0
- package/inferencer/types/el-maker/types.d.ts +2 -0
- package/inferencer/types/roundabout/types.d.ts +9 -1
- package/inferencer/types/swipe-dismiss/types.d.ts +43 -0
- package/package.json +1 -1
- package/processHandlerCommands.js +2 -0
- package/processHandlerCommands.ts +2 -0
- package/resolve/getValues.js +55 -9
- package/resolve/getValues.ts +70 -9
- package/types/assign-gingerly/types.d.ts +21 -0
package/assignFromAsync.js
CHANGED
|
@@ -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
|
}
|
package/assignFromAsync.ts
CHANGED
|
@@ -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
|
|
@@ -60,7 +60,7 @@ Create `types/[project-name]/types.d.ts` with the element's property interface:
|
|
|
60
60
|
/**
|
|
61
61
|
* Properties specific to this custom element
|
|
62
62
|
*/
|
|
63
|
-
export interface
|
|
63
|
+
export interface EndUserProps {
|
|
64
64
|
// Properties unique to this element
|
|
65
65
|
myProp: string;
|
|
66
66
|
disabled: boolean;
|
|
@@ -69,18 +69,21 @@ export interface ElementProps {
|
|
|
69
69
|
/**
|
|
70
70
|
* Full property set including internal state
|
|
71
71
|
*/
|
|
72
|
-
export interface AllProps extends
|
|
72
|
+
export interface AllProps extends EndUserProps {
|
|
73
73
|
idx: number;
|
|
74
74
|
item: any;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export type
|
|
77
|
+
export type AP = AllProps;
|
|
78
|
+
|
|
79
|
+
export interface RunTimeProps extends AllProps, HTMLElement
|
|
80
|
+
|
|
78
81
|
```
|
|
79
82
|
|
|
80
83
|
**Key points:**
|
|
81
|
-
- `
|
|
84
|
+
- `EndUserProps` — the public API specific to this element
|
|
82
85
|
- `AllProps` — includes internal/computed state managed by roundabout
|
|
83
|
-
- Export `
|
|
86
|
+
- Export `AP` as a convenience alias
|
|
84
87
|
|
|
85
88
|
|
|
86
89
|
## Step 4: Create imports.html
|
|
@@ -238,17 +238,17 @@ import { fileURLToPath } from 'url';
|
|
|
238
238
|
import {akaMethods as m, aka, builtInEmoji} from 'assign-gingerly/DX/emojis.js';
|
|
239
239
|
import {paths, doAssign, set, smoothOver} from 'assign-gingerly/DX/paths.js';
|
|
240
240
|
|
|
241
|
-
/** @import {EndUserProps, AP} from './types'; */
|
|
241
|
+
/** @import {EndUserProps, AP, RuntimeProps} from './types'; */
|
|
242
242
|
/** @import {RoundaboutOptions} from './types/roundabout/types' */
|
|
243
243
|
/** @import {ElMakerConfig} from './types/el-maker/types' */
|
|
244
244
|
|
|
245
245
|
const withMethods = [m['🔍']];
|
|
246
246
|
|
|
247
|
-
const $ = (/** @type {typeof paths<
|
|
247
|
+
const $ = (/** @type {typeof paths<RuntimeProps>} */ (/** @type {any} */(paths)))({withMethods});
|
|
248
248
|
|
|
249
249
|
|
|
250
250
|
/**
|
|
251
|
-
* @type {RoundaboutOptions<
|
|
251
|
+
* @type {RoundaboutOptions<AP>}
|
|
252
252
|
*/
|
|
253
253
|
const raConfig = {
|
|
254
254
|
weakRef: {
|
|
@@ -262,30 +262,30 @@ const raConfig = {
|
|
|
262
262
|
},
|
|
263
263
|
compacts: {
|
|
264
264
|
on_click_of_expandButton_assign: {
|
|
265
|
-
[$.expandButton.hidden.
|
|
266
|
-
[$.collapseButton.hidden.
|
|
265
|
+
[$.expandButton.hidden.Path]: true,
|
|
266
|
+
[$.collapseButton.hidden.Path]: false,
|
|
267
267
|
expanded: true,
|
|
268
268
|
|
|
269
269
|
},
|
|
270
270
|
on_click_of_collapseButton_assign: {
|
|
271
271
|
expanded: false,
|
|
272
|
-
[$.expandButton.hidden.
|
|
273
|
-
[$.collapseButton.hidden.
|
|
272
|
+
[$.expandButton.hidden.Path]: false,
|
|
273
|
+
[$.collapseButton.hidden.Path]: true,
|
|
274
274
|
}
|
|
275
275
|
},
|
|
276
276
|
merges: smoothOver([
|
|
277
277
|
{
|
|
278
278
|
ifKeyIn: ['clone'],
|
|
279
|
-
|
|
280
|
-
expandButton
|
|
281
|
-
collapseButton
|
|
282
|
-
|
|
279
|
+
...doAssign(
|
|
280
|
+
set($.expandButton).to($.clone.querySelector('[name=expand]')),
|
|
281
|
+
set($.collapseButton).to($.clone.querySelector('[name=collapse]')),
|
|
282
|
+
)
|
|
283
283
|
},
|
|
284
284
|
{
|
|
285
285
|
ifKeyIn: ['expanded'],
|
|
286
286
|
...doAssign(
|
|
287
287
|
set($.ariaExpanded).to($.expanded),
|
|
288
|
-
set(
|
|
288
|
+
set($.ariaControlsElements.Each.hidden.QMEq).to([$.expanded, false, 'until-found'])
|
|
289
289
|
)
|
|
290
290
|
},
|
|
291
291
|
{
|
|
@@ -303,13 +303,25 @@ const raConfig = {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
|
|
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
|
+
|
|
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
|
}
|
|
@@ -326,6 +338,55 @@ writeFileSync(outputFile, render(), 'utf8');
|
|
|
326
338
|
|
|
327
339
|
</details>
|
|
328
340
|
|
|
341
|
+
Some non obvious scenarios:
|
|
342
|
+
|
|
343
|
+
### How can I perform an action when the escape key is pressed somewhere on the document?
|
|
344
|
+
|
|
345
|
+
Answer:
|
|
346
|
+
|
|
347
|
+
From the side-burger example.
|
|
348
|
+
|
|
349
|
+
```JS
|
|
350
|
+
const raConfig = {
|
|
351
|
+
...
|
|
352
|
+
compacts: {
|
|
353
|
+
on_keydown_of_ownerDocument_assignFromEvent: {
|
|
354
|
+
[$.escapeKeyPressed.QMEq.Path]: [['?.key', 'Escape'], true, false]
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
merges: smoothOver([
|
|
358
|
+
{
|
|
359
|
+
ifKeyIn: ['expanded'],
|
|
360
|
+
...doAssign(
|
|
361
|
+
...
|
|
362
|
+
set($.escapeKeyPressed).to(false)
|
|
363
|
+
)
|
|
364
|
+
},
|
|
365
|
+
...
|
|
366
|
+
{
|
|
367
|
+
ifAllOf: ['escapeKeyPressed'],
|
|
368
|
+
assign: {
|
|
369
|
+
expanded: false
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
]),
|
|
373
|
+
};
|
|
374
|
+
```
|
|
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
|
+
|
|
329
390
|
## Step 8
|
|
330
391
|
|
|
331
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,
|
|
@@ -54,6 +54,7 @@ export type Compacts<TProps = any, TActions = TProps, TEvents extends string = s
|
|
|
54
54
|
| Partial<{[key in `on_${TEvents}_of_${keyof TProps & string}_inc_${keyof TProps & string}_by`]: number}>
|
|
55
55
|
| Partial<{[key in `on_${TEvents}_of_${keyof TProps & string}_set_${keyof TProps & string}_to`]: any}>
|
|
56
56
|
| Partial<{[key in `on_${TEvents}_of_${keyof TProps & string}_assign`]: Record<string, any>}>
|
|
57
|
+
| Partial<{[key in `on_${TEvents}_of_${keyof TProps & string}_assignFromEvent`]: Record<string, any>}>
|
|
57
58
|
;
|
|
58
59
|
|
|
59
60
|
export type Hitches<TProps = any, TActions = TProps> =
|
|
@@ -160,7 +161,14 @@ export interface RAConfig<
|
|
|
160
161
|
initialPropVals?: Partial<{[key in keyof TProps & string]: unknown}>,
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
export interface
|
|
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
|
+
|
|
171
|
+
export interface RoundaboutOptions<TProps = unknown, TActions = TProps, ETProps = TProps, EventTypes extends string = string> extends RAConfig<TProps, TActions, ETProps, unknown, EventTypes> {
|
|
164
172
|
vm?: TProps & TActions & RoundaboutReady,
|
|
165
173
|
//for enhanced elements, pass in the container, referenced via $0.
|
|
166
174
|
container?: EventTarget,
|
|
@@ -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
|
@@ -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
|
});
|
package/resolve/getValues.js
CHANGED
|
@@ -67,6 +67,45 @@ 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 (typeof resolved !== 'string') {
|
|
100
|
+
throw new Error(`Substitution '${name}' must resolve to a string, got ${typeof resolved}`);
|
|
101
|
+
}
|
|
102
|
+
if (resolved.includes('?.')) {
|
|
103
|
+
throw new Error(`Substitution '${name}' resolved to a string containing '?.', which would alter the path structure: '${resolved}'`);
|
|
104
|
+
}
|
|
105
|
+
map.set(name, resolved);
|
|
106
|
+
}
|
|
107
|
+
return map;
|
|
108
|
+
}
|
|
70
109
|
/**
|
|
71
110
|
* Resolve a special root-reference token at the start of a string.
|
|
72
111
|
* '$0' refers to the first argument passed to assignFrom / resolveValues.
|
|
@@ -159,12 +198,13 @@ function getProtocolValue(value, protocols, options) {
|
|
|
159
198
|
* Resolve path strings and protocols within an array (synchronous).
|
|
160
199
|
* Recurses into nested arrays and plain objects.
|
|
161
200
|
*/
|
|
162
|
-
function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
201
|
+
function getArray(arr, source, aliasMap, withMethods, protocols, options, substitutionMap) {
|
|
163
202
|
const permissionProcessor = options?.permissionProcessor;
|
|
164
203
|
const result = [];
|
|
165
204
|
for (const item of arr) {
|
|
166
205
|
if (typeof item === 'string' && item.startsWith('?.')) {
|
|
167
|
-
const
|
|
206
|
+
const substituted = applySubstitutions(item, substitutionMap);
|
|
207
|
+
const aliased = applyAliases(substituted, aliasMap);
|
|
168
208
|
const parts = parseCachedPath(aliased);
|
|
169
209
|
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods, permissionProcessor));
|
|
170
210
|
}
|
|
@@ -174,7 +214,8 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
|
174
214
|
result.push(source);
|
|
175
215
|
}
|
|
176
216
|
else {
|
|
177
|
-
const
|
|
217
|
+
const substitutedPath = applySubstitutions(rootRef.path, substitutionMap);
|
|
218
|
+
const aliased = applyAliases(substitutedPath, aliasMap);
|
|
178
219
|
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
179
220
|
const parts = parseCachedPath(normalizedPath);
|
|
180
221
|
result.push(parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods, permissionProcessor));
|
|
@@ -184,7 +225,7 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
|
184
225
|
result.push(getProtocolValue(item, protocols, options));
|
|
185
226
|
}
|
|
186
227
|
else if (Array.isArray(item)) {
|
|
187
|
-
result.push(getArray(item, source, aliasMap, withMethods, protocols, options));
|
|
228
|
+
result.push(getArray(item, source, aliasMap, withMethods, protocols, options, substitutionMap));
|
|
188
229
|
}
|
|
189
230
|
else if (item && typeof item === 'object') {
|
|
190
231
|
const proto = Object.getPrototypeOf(item);
|
|
@@ -215,12 +256,14 @@ function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
|
215
256
|
*/
|
|
216
257
|
export function getValues(pattern, source, options) {
|
|
217
258
|
const { aliasMap, withMethods } = normalizeAliasOptions(options);
|
|
259
|
+
const substitutionMap = resolveSubstitutions(options?.substitutions, source, options);
|
|
218
260
|
const protocols = options?.protocols;
|
|
219
261
|
const permissionProcessor = options?.permissionProcessor;
|
|
220
262
|
const result = {};
|
|
221
263
|
for (const [key, value] of Object.entries(pattern)) {
|
|
222
264
|
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
223
|
-
const
|
|
265
|
+
const substituted = applySubstitutions(value, substitutionMap);
|
|
266
|
+
const aliased = applyAliases(substituted, aliasMap);
|
|
224
267
|
const parts = parseCachedPath(aliased);
|
|
225
268
|
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods, permissionProcessor);
|
|
226
269
|
}
|
|
@@ -230,7 +273,8 @@ export function getValues(pattern, source, options) {
|
|
|
230
273
|
result[key] = source;
|
|
231
274
|
}
|
|
232
275
|
else {
|
|
233
|
-
const
|
|
276
|
+
const substitutedPath = applySubstitutions(rootRef.path, substitutionMap);
|
|
277
|
+
const aliased = applyAliases(substitutedPath, aliasMap);
|
|
234
278
|
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
235
279
|
const parts = parseCachedPath(normalizedPath);
|
|
236
280
|
result[key] = parts.length === 0 ? rootRef.source : navigatePath(rootRef.source, parts, withMethods, permissionProcessor);
|
|
@@ -240,7 +284,7 @@ export function getValues(pattern, source, options) {
|
|
|
240
284
|
result[key] = getProtocolValue(value, protocols, options);
|
|
241
285
|
}
|
|
242
286
|
else if (Array.isArray(value)) {
|
|
243
|
-
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options);
|
|
287
|
+
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options, substitutionMap);
|
|
244
288
|
}
|
|
245
289
|
else if (typeof value === 'object' && value !== null) {
|
|
246
290
|
const proto = Object.getPrototypeOf(value);
|
|
@@ -274,10 +318,12 @@ export function getValue(path, source, options) {
|
|
|
274
318
|
else if (!path.startsWith('?.')) {
|
|
275
319
|
return path;
|
|
276
320
|
}
|
|
277
|
-
|
|
321
|
+
const substitutionMap = resolveSubstitutions(options?.substitutions, source, options);
|
|
322
|
+
const substituted = applySubstitutions(path, substitutionMap);
|
|
323
|
+
let aliased = substituted;
|
|
278
324
|
const { aliasMap } = normalizeAliasOptions(options);
|
|
279
325
|
if (aliasMap.size > 0) {
|
|
280
|
-
aliased = applyAliases(
|
|
326
|
+
aliased = applyAliases(substituted, aliasMap);
|
|
281
327
|
}
|
|
282
328
|
const normalizedPath = aliased.startsWith('?.') ? aliased : (aliased ? `?.${aliased}` : '?.');
|
|
283
329
|
const parts = parseCachedPath(normalizedPath);
|