assign-gingerly 0.0.58 → 0.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +511 -142
- package/assignFrom-extension.js +25 -0
- package/assignFrom-extension.ts +53 -0
- package/assignFrom.js +306 -133
- package/assignFrom.ts +318 -232
- package/assignFromAsync-extension.js +28 -0
- package/assignFromAsync-extension.ts +58 -0
- package/assignFromAsync.js +120 -0
- package/assignFromAsync.ts +256 -0
- package/assignGingerly.js +50 -0
- package/assignGingerly.ts +51 -0
- package/builtInEmoji.js +25 -0
- package/builtInEmoji.ts +33 -0
- package/getValues.js +223 -0
- package/getValues.ts +255 -0
- package/handlers/join.ts +1 -1
- package/handlers/lazyLoad.js +33 -2
- package/handlers/lazyLoad.ts +47 -2
- package/handlers/lazyLoadSwitch.ts +1 -1
- package/handlers/manageTemplateList.js +226 -0
- package/handlers/manageTemplateList.ts +263 -0
- package/handlers/microDataJoin.ts +1 -1
- package/index.js +1 -0
- package/index.ts +2 -0
- package/inferencer/inferencer.js +9 -21
- package/inferencer/inferencer.ts +10 -21
- package/inferredAssignments.js +35 -5
- package/inferredAssignments.ts +58 -8
- package/markerUtils.js +136 -127
- package/package.json +30 -1
- package/playwright.config.ts +3 -2
- package/processHandlerCommands.js +36 -4
- package/processHandlerCommands.ts +38 -8
- package/resolveIdRef.js +70 -19
- package/resolveIdRef.ts +73 -21
- package/resolveValues.js +41 -125
- package/resolveValues.ts +131 -255
- package/types/assign-gingerly/types.d.ts +61 -0
- package/waitForSettled.js +57 -0
- package/waitForSettled.ts +65 -0
- package/withIdsCorrector.js +47 -0
- package/withIdsCorrector.ts +59 -0
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { resolveValues } from './resolveValues.js';
|
|
8
|
+
import { getValues } from './getValues.js';
|
|
8
9
|
import { evaluatePathWithMethods } from './assignGingerly.js';
|
|
9
10
|
import { isAllowedImportPath } from './isAllowedImportPath.js';
|
|
10
11
|
import type { AssignPermissions } from './isAllowedImportPath.js';
|
|
11
|
-
import type { AssignFromOptions, AssignFromHandlerConstructor } from './
|
|
12
|
+
import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFromAsync.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Map of built-in handler names to their module paths.
|
|
@@ -19,6 +20,7 @@ const BUILT_IN_MAP: Record<string, string> = {
|
|
|
19
20
|
'builtIns.lazyLoadSwitch': './handlers/lazyLoadSwitch.js',
|
|
20
21
|
'builtIns.join': './handlers/join.js',
|
|
21
22
|
'builtIns.microDataJoin': './handlers/microDataJoin.js',
|
|
23
|
+
'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -41,15 +43,25 @@ function findHandlerInModule(module: any): AssignFromHandlerConstructor | undefi
|
|
|
41
43
|
return undefined;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Cache for loaded built-in handler classes — avoids await on subsequent calls.
|
|
48
|
+
*/
|
|
49
|
+
const handlerCache = new Map<string, AssignFromHandlerConstructor>();
|
|
50
|
+
|
|
44
51
|
/**
|
|
45
52
|
* Dynamically load a built-in handler by name.
|
|
46
53
|
* Returns the handler constructor, or undefined if the name isn't a recognized built-in.
|
|
54
|
+
* Cached after first load — subsequent calls are synchronous.
|
|
47
55
|
*/
|
|
48
56
|
async function loadBuiltIn(name: string): Promise<AssignFromHandlerConstructor | undefined> {
|
|
57
|
+
const cached = handlerCache.get(name);
|
|
58
|
+
if (cached) return cached;
|
|
49
59
|
const path = BUILT_IN_MAP[name];
|
|
50
60
|
if (!path) return undefined;
|
|
51
61
|
const module = await import(path);
|
|
52
|
-
|
|
62
|
+
const cls = findHandlerInModule(module);
|
|
63
|
+
if (cls) handlerCache.set(name, cls);
|
|
64
|
+
return cls;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
/**
|
|
@@ -69,8 +81,14 @@ async function resolveFromHandlers(
|
|
|
69
81
|
return entry as AssignFromHandlerConstructor;
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
//
|
|
84
|
+
// String value — could be a built-in alias or an import path
|
|
73
85
|
if (typeof entry === 'string') {
|
|
86
|
+
// Built-in alias: redirect to built-in loader
|
|
87
|
+
if (entry.startsWith('builtIns.')) {
|
|
88
|
+
return loadBuiltIn(entry);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Import path string — validate and dynamically import
|
|
74
92
|
if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
|
|
75
93
|
throw new Error(
|
|
76
94
|
`assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
|
|
@@ -107,6 +125,7 @@ export async function processHandlerCommands(
|
|
|
107
125
|
options: AssignFromOptions,
|
|
108
126
|
permissions?: AssignPermissions
|
|
109
127
|
): Promise<void> {
|
|
128
|
+
|
|
110
129
|
for (const key of handlerKeys) {
|
|
111
130
|
const lhsPath = key.substring(0, key.length - 3); // Remove ' =>'
|
|
112
131
|
const rhs = pattern[key];
|
|
@@ -178,35 +197,46 @@ export async function processHandlerCommands(
|
|
|
178
197
|
} else {
|
|
179
198
|
lhsTarget = target;
|
|
180
199
|
}
|
|
181
|
-
|
|
182
200
|
// Execute handlers sequentially, sharing the same lhsTarget
|
|
183
201
|
for (const config of configs) {
|
|
202
|
+
//return; //1.3ms
|
|
184
203
|
// 1. Check options.handlers (local, per-call)
|
|
185
204
|
let HandlerClass = await resolveFromHandlers(config.do, options.handlers, permissions);
|
|
186
|
-
|
|
205
|
+
//return; // 1.4
|
|
187
206
|
// 2. Fallback to built-in auto-load
|
|
188
207
|
if (!HandlerClass && config.do.startsWith('builtIns.')) {
|
|
189
208
|
HandlerClass = await loadBuiltIn(config.do);
|
|
190
209
|
}
|
|
210
|
+
//return; //1.4
|
|
191
211
|
|
|
192
212
|
if (!HandlerClass) {
|
|
193
213
|
throw new Error(`assignFrom: unknown handler "${config.do}". Provide it in options.handlers.`);
|
|
194
214
|
}
|
|
195
215
|
|
|
196
|
-
// Resolve '
|
|
216
|
+
// Resolve 'get' map synchronously (no thread yield)
|
|
197
217
|
let resolvedParams: Record<string, any> = {};
|
|
218
|
+
if (config.get) {
|
|
219
|
+
resolvedParams = getValues(config.get, options.from, {
|
|
220
|
+
withMethods: options.withMethods,
|
|
221
|
+
aka: options.aka,
|
|
222
|
+
protocols: options.protocols
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// Resolve 'resolve' map asynchronously (yields to microtask queue)
|
|
198
226
|
if (config.resolve) {
|
|
199
|
-
|
|
227
|
+
const asyncResolved = await resolveValues(config.resolve, options.from, {
|
|
200
228
|
withMethods: options.withMethods,
|
|
201
229
|
aka: options.aka,
|
|
202
230
|
protocols: options.protocols
|
|
203
231
|
});
|
|
232
|
+
Object.assign(resolvedParams, asyncResolved);
|
|
204
233
|
}
|
|
205
234
|
|
|
206
235
|
// Instantiate and invoke the handler
|
|
207
236
|
const handler = new HandlerClass(config);
|
|
237
|
+
//return; //1.5ms
|
|
208
238
|
const result = await handler.assign(lhsTarget, resolvedParams, options);
|
|
209
|
-
|
|
239
|
+
//return; //1.5ms
|
|
210
240
|
// Return-value protocol: if handler returns a non-undefined value,
|
|
211
241
|
// assign it back to the LHS path
|
|
212
242
|
if (result !== undefined && lhsParent != null && lhsKey != null) {
|
package/resolveIdRef.js
CHANGED
|
@@ -18,14 +18,15 @@ const idCacheMap = new WeakMap();
|
|
|
18
18
|
const idCounterMap = new WeakMap();
|
|
19
19
|
/**
|
|
20
20
|
* Generate a unique ID within a rootNode.
|
|
21
|
-
* Format:
|
|
21
|
+
* Format: -ag:0, -ag:1, -ag:2, ...
|
|
22
|
+
* Starts with '-' and contains ':' to avoid collision with JS identifiers/globalThis properties.
|
|
22
23
|
*/
|
|
23
24
|
function generateUniqueId(rootNode) {
|
|
24
25
|
let counter = idCounterMap.get(rootNode) ?? 0;
|
|
25
26
|
let id;
|
|
26
27
|
// Ensure uniqueness (skip if ID already exists in the document)
|
|
27
28
|
do {
|
|
28
|
-
id =
|
|
29
|
+
id = `-ag:${counter}`;
|
|
29
30
|
counter++;
|
|
30
31
|
} while (rootNode.getElementById?.(id));
|
|
31
32
|
idCounterMap.set(rootNode, counter);
|
|
@@ -43,6 +44,55 @@ export function resolveIdVariable(varName, target, withIds) {
|
|
|
43
44
|
const config = withIds[varName];
|
|
44
45
|
if (config === undefined)
|
|
45
46
|
return undefined;
|
|
47
|
+
// For 'at' option path-based configs (array or { path }), resolve directly from target — no ID, no caching
|
|
48
|
+
// These are target-relative and fast (~2-4ns), for when structure is guaranteed stable
|
|
49
|
+
if (Array.isArray(config)) {
|
|
50
|
+
let current = target;
|
|
51
|
+
for (const idx of config) {
|
|
52
|
+
if (!current || !current.children)
|
|
53
|
+
break;
|
|
54
|
+
current = current.children[idx];
|
|
55
|
+
}
|
|
56
|
+
return current instanceof Element ? current : undefined;
|
|
57
|
+
}
|
|
58
|
+
if (typeof config === 'object' && 'path' in config && !('qry' in config)) {
|
|
59
|
+
// Determine if this is from 'at' (no ID assignment) or 'withIds' with path (assigns ID + caches)
|
|
60
|
+
// When called from 'at', we skip ID/caching. When from 'withIds', we assign ID and cache.
|
|
61
|
+
// Distinguish by presence in the options — caller passes the merged map.
|
|
62
|
+
// For now: { path } without 'noId' → assign ID + cache (withIds behavior)
|
|
63
|
+
const rootNode = target.getRootNode?.() ?? target;
|
|
64
|
+
let current = target;
|
|
65
|
+
for (const idx of config.path) {
|
|
66
|
+
if (!current || !current.children)
|
|
67
|
+
break;
|
|
68
|
+
current = current.children[idx];
|
|
69
|
+
}
|
|
70
|
+
let el = current instanceof Element ? current : null;
|
|
71
|
+
// Validation: check if resolved element matches expected selector
|
|
72
|
+
if (config.expect) {
|
|
73
|
+
const didNotMatch = !el || !el.matches(config.expect);
|
|
74
|
+
if (didNotMatch) {
|
|
75
|
+
if (config.fallback) {
|
|
76
|
+
el = target.querySelector?.(config.expect) ?? el;
|
|
77
|
+
}
|
|
78
|
+
// Fire-and-forget: log correction suggestion
|
|
79
|
+
const capturedConfig = config;
|
|
80
|
+
const capturedVarName = varName;
|
|
81
|
+
import('./withIdsCorrector.js').then(module => {
|
|
82
|
+
module.logConfigCorrection(target, capturedVarName, capturedConfig);
|
|
83
|
+
}).catch(() => { });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!el)
|
|
87
|
+
return undefined;
|
|
88
|
+
// Assign ID for stability against future DOM mutations
|
|
89
|
+
let id = el.id;
|
|
90
|
+
if (!id) {
|
|
91
|
+
id = generateUniqueId(rootNode);
|
|
92
|
+
el.id = id;
|
|
93
|
+
}
|
|
94
|
+
return el;
|
|
95
|
+
}
|
|
46
96
|
const rootNode = target.getRootNode?.() ?? target;
|
|
47
97
|
// Get or create cache for this rootNode
|
|
48
98
|
let cache = idCacheMap.get(rootNode);
|
|
@@ -50,28 +100,27 @@ export function resolveIdVariable(varName, target, withIds) {
|
|
|
50
100
|
cache = new Map();
|
|
51
101
|
idCacheMap.set(rootNode, cache);
|
|
52
102
|
}
|
|
53
|
-
// Check cache first
|
|
54
|
-
const cached = cache.get(varName);
|
|
55
|
-
if (cached) {
|
|
56
|
-
const el = cached.ref.deref();
|
|
57
|
-
if (el)
|
|
58
|
-
return el;
|
|
59
|
-
// WeakRef was collected — try getElementById fallback
|
|
60
|
-
const el2 = rootNode.getElementById?.(cached.id);
|
|
61
|
-
if (el2) {
|
|
62
|
-
cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
|
|
63
|
-
return el2;
|
|
64
|
-
}
|
|
65
|
-
// Element no longer exists — fall through to re-query
|
|
66
|
-
}
|
|
67
103
|
// First time or cache miss — resolve the element
|
|
68
104
|
let el = null;
|
|
69
105
|
if (typeof config === 'string') {
|
|
70
106
|
// String form: existing ID — use getElementById directly
|
|
107
|
+
// Check cache first (getElementById lookups are cacheable — global to rootNode)
|
|
108
|
+
const cached = cache.get(varName);
|
|
109
|
+
if (cached) {
|
|
110
|
+
const cachedEl = cached.ref.deref();
|
|
111
|
+
if (cachedEl)
|
|
112
|
+
return cachedEl;
|
|
113
|
+
// WeakRef was collected — try getElementById fallback
|
|
114
|
+
const el2 = rootNode.getElementById?.(cached.id);
|
|
115
|
+
if (el2) {
|
|
116
|
+
cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
|
|
117
|
+
return el2;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
71
120
|
el = rootNode.getElementById?.(config) ?? null;
|
|
72
121
|
}
|
|
73
122
|
else {
|
|
74
|
-
// Object form: { qry } — run querySelector against target
|
|
123
|
+
// Object form: { qry } — run querySelector against target (target-relative, no cache)
|
|
75
124
|
el = target.querySelector?.(config.qry) ?? null;
|
|
76
125
|
}
|
|
77
126
|
if (!el)
|
|
@@ -82,8 +131,10 @@ export function resolveIdVariable(varName, target, withIds) {
|
|
|
82
131
|
id = generateUniqueId(rootNode);
|
|
83
132
|
el.id = id;
|
|
84
133
|
}
|
|
85
|
-
// Cache
|
|
86
|
-
|
|
134
|
+
// Cache only for string-form (getElementById) — not for qry form (target-relative)
|
|
135
|
+
if (typeof config === 'string') {
|
|
136
|
+
cache.set(varName, { id, ref: new WeakRef(el) });
|
|
137
|
+
}
|
|
87
138
|
return el;
|
|
88
139
|
}
|
|
89
140
|
/**
|
package/resolveIdRef.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
/**
|
|
12
12
|
* Configuration for a withIds entry.
|
|
13
13
|
*/
|
|
14
|
-
export type WithIdConfig = string | { qry: string };
|
|
14
|
+
export type WithIdConfig = string | { qry: string } | number[] | { path: number[]; expect?: string; fallback?: boolean };
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Module-level cache: rootNode → Map<varName, { id, WeakRef }>
|
|
@@ -26,14 +26,15 @@ const idCounterMap = new WeakMap<object, number>();
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Generate a unique ID within a rootNode.
|
|
29
|
-
* Format:
|
|
29
|
+
* Format: -ag:0, -ag:1, -ag:2, ...
|
|
30
|
+
* Starts with '-' and contains ':' to avoid collision with JS identifiers/globalThis properties.
|
|
30
31
|
*/
|
|
31
32
|
function generateUniqueId(rootNode: any): string {
|
|
32
33
|
let counter = idCounterMap.get(rootNode) ?? 0;
|
|
33
34
|
let id: string;
|
|
34
35
|
// Ensure uniqueness (skip if ID already exists in the document)
|
|
35
36
|
do {
|
|
36
|
-
id =
|
|
37
|
+
id = `-ag:${counter}`;
|
|
37
38
|
counter++;
|
|
38
39
|
} while (rootNode.getElementById?.(id));
|
|
39
40
|
idCounterMap.set(rootNode, counter);
|
|
@@ -56,6 +57,57 @@ export function resolveIdVariable(
|
|
|
56
57
|
const config = withIds[varName];
|
|
57
58
|
if (config === undefined) return undefined;
|
|
58
59
|
|
|
60
|
+
// For 'at' option path-based configs (array or { path }), resolve directly from target — no ID, no caching
|
|
61
|
+
// These are target-relative and fast (~2-4ns), for when structure is guaranteed stable
|
|
62
|
+
if (Array.isArray(config)) {
|
|
63
|
+
let current: any = target;
|
|
64
|
+
for (const idx of config) {
|
|
65
|
+
if (!current || !current.children) break;
|
|
66
|
+
current = current.children[idx];
|
|
67
|
+
}
|
|
68
|
+
return current instanceof Element ? current : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof config === 'object' && 'path' in config && !('qry' in config)) {
|
|
72
|
+
// Determine if this is from 'at' (no ID assignment) or 'withIds' with path (assigns ID + caches)
|
|
73
|
+
// When called from 'at', we skip ID/caching. When from 'withIds', we assign ID and cache.
|
|
74
|
+
// Distinguish by presence in the options — caller passes the merged map.
|
|
75
|
+
// For now: { path } without 'noId' → assign ID + cache (withIds behavior)
|
|
76
|
+
const rootNode = target.getRootNode?.() ?? target;
|
|
77
|
+
let current: any = target;
|
|
78
|
+
for (const idx of config.path) {
|
|
79
|
+
if (!current || !current.children) break;
|
|
80
|
+
current = current.children[idx];
|
|
81
|
+
}
|
|
82
|
+
let el: Element | null = current instanceof Element ? current : null;
|
|
83
|
+
|
|
84
|
+
// Validation: check if resolved element matches expected selector
|
|
85
|
+
if (config.expect) {
|
|
86
|
+
const didNotMatch = !el || !el.matches(config.expect);
|
|
87
|
+
if (didNotMatch) {
|
|
88
|
+
if (config.fallback) {
|
|
89
|
+
el = target.querySelector?.(config.expect) ?? el;
|
|
90
|
+
}
|
|
91
|
+
// Fire-and-forget: log correction suggestion
|
|
92
|
+
const capturedConfig = config;
|
|
93
|
+
const capturedVarName = varName;
|
|
94
|
+
import('./withIdsCorrector.js').then(module => {
|
|
95
|
+
module.logConfigCorrection(target, capturedVarName, capturedConfig);
|
|
96
|
+
}).catch(() => {});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!el) return undefined;
|
|
101
|
+
|
|
102
|
+
// Assign ID for stability against future DOM mutations
|
|
103
|
+
let id = el.id;
|
|
104
|
+
if (!id) {
|
|
105
|
+
id = generateUniqueId(rootNode);
|
|
106
|
+
el.id = id;
|
|
107
|
+
}
|
|
108
|
+
return el;
|
|
109
|
+
}
|
|
110
|
+
|
|
59
111
|
const rootNode = target.getRootNode?.() ?? target;
|
|
60
112
|
|
|
61
113
|
// Get or create cache for this rootNode
|
|
@@ -65,29 +117,27 @@ export function resolveIdVariable(
|
|
|
65
117
|
idCacheMap.set(rootNode, cache);
|
|
66
118
|
}
|
|
67
119
|
|
|
68
|
-
// Check cache first
|
|
69
|
-
const cached = cache.get(varName);
|
|
70
|
-
if (cached) {
|
|
71
|
-
const el = cached.ref.deref();
|
|
72
|
-
if (el) return el;
|
|
73
|
-
|
|
74
|
-
// WeakRef was collected — try getElementById fallback
|
|
75
|
-
const el2 = rootNode.getElementById?.(cached.id);
|
|
76
|
-
if (el2) {
|
|
77
|
-
cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
|
|
78
|
-
return el2;
|
|
79
|
-
}
|
|
80
|
-
// Element no longer exists — fall through to re-query
|
|
81
|
-
}
|
|
82
|
-
|
|
83
120
|
// First time or cache miss — resolve the element
|
|
84
121
|
let el: Element | null = null;
|
|
85
122
|
|
|
86
123
|
if (typeof config === 'string') {
|
|
87
124
|
// String form: existing ID — use getElementById directly
|
|
125
|
+
// Check cache first (getElementById lookups are cacheable — global to rootNode)
|
|
126
|
+
const cached = cache.get(varName);
|
|
127
|
+
if (cached) {
|
|
128
|
+
const cachedEl = cached.ref.deref();
|
|
129
|
+
if (cachedEl) return cachedEl;
|
|
130
|
+
|
|
131
|
+
// WeakRef was collected — try getElementById fallback
|
|
132
|
+
const el2 = rootNode.getElementById?.(cached.id);
|
|
133
|
+
if (el2) {
|
|
134
|
+
cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
|
|
135
|
+
return el2;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
88
138
|
el = rootNode.getElementById?.(config) ?? null;
|
|
89
139
|
} else {
|
|
90
|
-
// Object form: { qry } — run querySelector against target
|
|
140
|
+
// Object form: { qry } — run querySelector against target (target-relative, no cache)
|
|
91
141
|
el = target.querySelector?.(config.qry) ?? null;
|
|
92
142
|
}
|
|
93
143
|
|
|
@@ -100,8 +150,10 @@ export function resolveIdVariable(
|
|
|
100
150
|
el.id = id;
|
|
101
151
|
}
|
|
102
152
|
|
|
103
|
-
// Cache
|
|
104
|
-
|
|
153
|
+
// Cache only for string-form (getElementById) — not for qry form (target-relative)
|
|
154
|
+
if (typeof config === 'string') {
|
|
155
|
+
cache.set(varName, { id, ref: new WeakRef(el) });
|
|
156
|
+
}
|
|
105
157
|
return el;
|
|
106
158
|
}
|
|
107
159
|
|
package/resolveValues.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveValues.ts — Async value resolution for path strings.
|
|
3
|
+
*
|
|
4
|
+
* Thin async wrapper around getValues that adds support for async protocol handlers.
|
|
5
|
+
* For synchronous-only use cases, import getValues/getValue directly for better performance.
|
|
6
|
+
*
|
|
7
|
+
* Re-exports ResolveValuesOptions for backward compatibility.
|
|
8
|
+
*/
|
|
9
|
+
import { getValue } from './getValues.js';
|
|
10
|
+
// Re-export getValue as resolveValue for backward compatibility
|
|
11
|
+
export { getValue as resolveValue };
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a string value looks like a protocol reference.
|
|
14
|
+
*/
|
|
15
|
+
function hasProtocol(value) {
|
|
16
|
+
return value.includes('://');
|
|
17
|
+
}
|
|
1
18
|
/**
|
|
2
19
|
* Apply alias substitutions to a path string.
|
|
3
|
-
* Replaces complete tokens between `?.` delimiters with their aliased values.
|
|
4
20
|
*/
|
|
5
21
|
function applyAliases(path, aliasMap) {
|
|
6
22
|
if (aliasMap.size === 0)
|
|
@@ -11,12 +27,8 @@ function applyAliases(path, aliasMap) {
|
|
|
11
27
|
}
|
|
12
28
|
/**
|
|
13
29
|
* Path cache for parsed path strings.
|
|
14
|
-
* Avoids re-splitting the same path on repeated calls.
|
|
15
30
|
*/
|
|
16
31
|
const pathCache = new Map();
|
|
17
|
-
/**
|
|
18
|
-
* Parse a `?.`-delimited path string into segments, with caching.
|
|
19
|
-
*/
|
|
20
32
|
function parseCachedPath(path) {
|
|
21
33
|
let parts = pathCache.get(path);
|
|
22
34
|
if (!parts) {
|
|
@@ -25,44 +37,8 @@ function parseCachedPath(path) {
|
|
|
25
37
|
}
|
|
26
38
|
return parts;
|
|
27
39
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Resolves a protocol-prefixed value (e.g., 'globalThis://key?.path').
|
|
30
|
-
*
|
|
31
|
-
* 1. Extracts the protocol name (before '://')
|
|
32
|
-
* 2. If the protocol isn't in the protocols map, returns the value unchanged (false positive)
|
|
33
|
-
* 3. Extracts the key (between '://' and first '?.' or end of string)
|
|
34
|
-
* 4. Calls the protocol handler with the key
|
|
35
|
-
* 5. If there's a remaining '?.' path, resolves it against the handler's result
|
|
36
|
-
*/
|
|
37
|
-
async function resolveProtocolValue(value, protocols, options) {
|
|
38
|
-
// Extract protocol name (before ://)
|
|
39
|
-
const protoEnd = value.indexOf('://');
|
|
40
|
-
const protocol = value.substring(0, protoEnd);
|
|
41
|
-
// Resolve via protocol handler
|
|
42
|
-
const handler = protocols[protocol];
|
|
43
|
-
if (!handler)
|
|
44
|
-
return value; // false flag — coincidentally looks like a protocol
|
|
45
|
-
const rest = value.substring(protoEnd + 3);
|
|
46
|
-
// Split at first ?. to separate key from path
|
|
47
|
-
const pathStart = rest.indexOf('?.');
|
|
48
|
-
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
49
|
-
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
50
|
-
const resolved = await handler(key);
|
|
51
|
-
// If there's a remaining path, resolve it against the result
|
|
52
|
-
if (path) {
|
|
53
|
-
return resolveValue(path, resolved, options);
|
|
54
|
-
}
|
|
55
|
-
return resolved;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Checks if a string value looks like a protocol reference.
|
|
59
|
-
*/
|
|
60
|
-
function hasProtocol(value) {
|
|
61
|
-
return value.includes('://');
|
|
62
|
-
}
|
|
63
40
|
/**
|
|
64
41
|
* Navigate a path against a source object, optionally calling methods.
|
|
65
|
-
* Returns the resolved value at the end of the path.
|
|
66
42
|
*/
|
|
67
43
|
function navigatePath(source, parts, withMethods) {
|
|
68
44
|
let current = source;
|
|
@@ -76,12 +52,10 @@ function navigatePath(source, parts, withMethods) {
|
|
|
76
52
|
if (typeof method === 'function') {
|
|
77
53
|
const nextPart = parts[i + 1];
|
|
78
54
|
if (nextPart !== undefined && !(withMethods.has(nextPart))) {
|
|
79
|
-
// Call method with next segment as argument, consume it
|
|
80
55
|
current = method.call(current, nextPart);
|
|
81
56
|
i += 2;
|
|
82
57
|
}
|
|
83
58
|
else {
|
|
84
|
-
// Consecutive methods or last segment — call with no args
|
|
85
59
|
current = method.call(current);
|
|
86
60
|
i++;
|
|
87
61
|
}
|
|
@@ -99,9 +73,26 @@ function navigatePath(source, parts, withMethods) {
|
|
|
99
73
|
return current;
|
|
100
74
|
}
|
|
101
75
|
/**
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
76
|
+
* Resolves a protocol-prefixed value asynchronously.
|
|
77
|
+
*/
|
|
78
|
+
async function resolveProtocolValue(value, protocols, options) {
|
|
79
|
+
const protoEnd = value.indexOf('://');
|
|
80
|
+
const protocol = value.substring(0, protoEnd);
|
|
81
|
+
const handler = protocols[protocol];
|
|
82
|
+
if (!handler)
|
|
83
|
+
return value;
|
|
84
|
+
const rest = value.substring(protoEnd + 3);
|
|
85
|
+
const pathStart = rest.indexOf('?.');
|
|
86
|
+
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
87
|
+
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
88
|
+
const resolved = await handler(key);
|
|
89
|
+
if (path) {
|
|
90
|
+
return getValue(path, resolved, options);
|
|
91
|
+
}
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve path strings and protocol references within an array (async).
|
|
105
96
|
*/
|
|
106
97
|
async function resolveArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
107
98
|
const result = [];
|
|
@@ -133,47 +124,23 @@ async function resolveArray(arr, source, aliasMap, withMethods, protocols, optio
|
|
|
133
124
|
return result;
|
|
134
125
|
}
|
|
135
126
|
/**
|
|
136
|
-
*
|
|
127
|
+
* Async resolve RHS path strings in a pattern object against a source object.
|
|
137
128
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* Non-string values and strings not starting with `?.` pass through unchanged.
|
|
141
|
-
*
|
|
142
|
-
* Supports `withMethods` for calling methods during resolution and `aka` for
|
|
143
|
-
* alias substitution, consistent with assignGingerly's LHS path handling.
|
|
144
|
-
*
|
|
145
|
-
* Special case: `'?.'` (empty path) resolves to the source object itself.
|
|
129
|
+
* Supports async protocol handlers (e.g., fetch, IndexedDB).
|
|
130
|
+
* For synchronous-only patterns, use `getValues` from 'assign-gingerly/getValues.js' instead.
|
|
146
131
|
*
|
|
147
132
|
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
148
133
|
* @param source - Object to resolve paths against
|
|
149
|
-
* @param options - Optional withMethods
|
|
134
|
+
* @param options - Optional withMethods, aka, and protocol handlers
|
|
150
135
|
* @returns New object with path strings replaced by resolved values
|
|
151
|
-
*
|
|
152
|
-
* @example
|
|
153
|
-
* const result = resolveValues({
|
|
154
|
-
* hello: '?.myPropContainer?.stringProp',
|
|
155
|
-
* foo: '?.myFooString',
|
|
156
|
-
* literal: 42
|
|
157
|
-
* }, source);
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* // With methods and aliases
|
|
161
|
-
* const result = resolveValues({
|
|
162
|
-
* text: '?.q?..username?.textContent'
|
|
163
|
-
* }, source, {
|
|
164
|
-
* withMethods: ['querySelector'],
|
|
165
|
-
* aka: { 'q': 'querySelector' }
|
|
166
|
-
* });
|
|
167
136
|
*/
|
|
168
137
|
export async function resolveValues(pattern, source, options) {
|
|
169
|
-
// Build alias map
|
|
170
138
|
const aliasMap = new Map();
|
|
171
139
|
if (options?.aka) {
|
|
172
140
|
for (const [alias, target] of Object.entries(options.aka)) {
|
|
173
141
|
aliasMap.set(alias, target);
|
|
174
142
|
}
|
|
175
143
|
}
|
|
176
|
-
// Build methods set
|
|
177
144
|
const withMethods = options?.withMethods
|
|
178
145
|
? options.withMethods instanceof Set
|
|
179
146
|
? options.withMethods
|
|
@@ -183,24 +150,17 @@ export async function resolveValues(pattern, source, options) {
|
|
|
183
150
|
const result = {};
|
|
184
151
|
for (const [key, value] of Object.entries(pattern)) {
|
|
185
152
|
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
186
|
-
// Apply aliases to the RHS path
|
|
187
153
|
const aliased = applyAliases(value, aliasMap);
|
|
188
|
-
// Parse path with caching
|
|
189
154
|
const parts = parseCachedPath(aliased);
|
|
190
|
-
// Navigate with method support
|
|
191
155
|
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
192
156
|
}
|
|
193
157
|
else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
194
|
-
// Protocol-prefixed value — resolve asynchronously
|
|
195
158
|
result[key] = await resolveProtocolValue(value, protocols, options);
|
|
196
159
|
}
|
|
197
160
|
else if (Array.isArray(value)) {
|
|
198
|
-
// Resolve path strings and protocols within arrays (recursing into nested arrays)
|
|
199
161
|
result[key] = await resolveArray(value, source, aliasMap, withMethods, protocols, options);
|
|
200
162
|
}
|
|
201
163
|
else if (typeof value === 'object' && value !== null) {
|
|
202
|
-
// Recursively resolve nested plain objects (e.g., headers: { "...": "globalThis://key" })
|
|
203
|
-
// Only recurse into plain objects — skip DOM elements, class instances, etc.
|
|
204
164
|
const proto = Object.getPrototypeOf(value);
|
|
205
165
|
if (proto === Object.prototype || proto === null) {
|
|
206
166
|
result[key] = await resolveValues(value, source, options);
|
|
@@ -215,47 +175,3 @@ export async function resolveValues(pattern, source, options) {
|
|
|
215
175
|
}
|
|
216
176
|
return result;
|
|
217
177
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Resolve a single `?.`-delimited path string against a source object.
|
|
220
|
-
*
|
|
221
|
-
* This is a lighter-weight alternative to `resolveValues` when you only need
|
|
222
|
-
* to resolve one path and don't want the overhead of creating wrapper objects.
|
|
223
|
-
*
|
|
224
|
-
* @param path - A `?.`-delimited path string (e.g., '?.behaviors?.command')
|
|
225
|
-
* @param source - Object to resolve the path against
|
|
226
|
-
* @param options - Optional withMethods and aka for method calls and aliases
|
|
227
|
-
* @returns The resolved value, or undefined if any segment is nullish
|
|
228
|
-
*
|
|
229
|
-
* @example
|
|
230
|
-
* const value = resolveValue('?.behaviors?.commandBehavior?.command', el);
|
|
231
|
-
*
|
|
232
|
-
* @example
|
|
233
|
-
* const value = resolveValue('?.q?.myEl?.textContent', el, {
|
|
234
|
-
* withMethods: ['querySelector'],
|
|
235
|
-
* aka: { 'q': 'querySelector' }
|
|
236
|
-
* });
|
|
237
|
-
*/
|
|
238
|
-
export function resolveValue(path, source, options) {
|
|
239
|
-
if (!path.startsWith('?.'))
|
|
240
|
-
return path;
|
|
241
|
-
// Build alias map
|
|
242
|
-
let aliased = path;
|
|
243
|
-
if (options?.aka) {
|
|
244
|
-
const aliasMap = new Map();
|
|
245
|
-
for (const [alias, target] of Object.entries(options.aka)) {
|
|
246
|
-
aliasMap.set(alias, target);
|
|
247
|
-
}
|
|
248
|
-
aliased = applyAliases(path, aliasMap);
|
|
249
|
-
}
|
|
250
|
-
// Parse path with caching
|
|
251
|
-
const parts = parseCachedPath(aliased);
|
|
252
|
-
if (parts.length === 0)
|
|
253
|
-
return source;
|
|
254
|
-
// Build methods set
|
|
255
|
-
const withMethods = options?.withMethods
|
|
256
|
-
? options.withMethods instanceof Set
|
|
257
|
-
? options.withMethods
|
|
258
|
-
: new Set(options.withMethods)
|
|
259
|
-
: undefined;
|
|
260
|
-
return navigatePath(source, parts, withMethods);
|
|
261
|
-
}
|