assign-gingerly 0.0.59 → 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 +269 -12
- package/assignFrom-extension.js +25 -0
- package/assignFrom-extension.ts +53 -0
- package/assignFrom.js +190 -12
- package/assignFrom.ts +184 -11
- package/assignFromAsync-extension.js +28 -0
- package/assignFromAsync-extension.ts +58 -0
- package/assignFromAsync.js +11 -9
- package/assignFromAsync.ts +27 -11
- package/assignGingerly.js +50 -0
- package/assignGingerly.ts +51 -0
- package/builtInEmoji.js +25 -0
- package/builtInEmoji.ts +33 -0
- package/handlers/lazyLoad.js +33 -2
- package/handlers/lazyLoad.ts +46 -1
- package/handlers/manageTemplateList.js +54 -31
- package/handlers/manageTemplateList.ts +51 -28
- package/inferencer/inferencer.js +9 -21
- package/inferencer/inferencer.ts +10 -21
- package/inferredAssignments.js +34 -4
- package/inferredAssignments.ts +56 -6
- package/package.json +13 -1
- package/playwright.config.ts +3 -2
- package/processHandlerCommands.js +6 -1
- package/processHandlerCommands.ts +7 -1
- package/resolveIdRef.js +70 -19
- package/resolveIdRef.ts +73 -21
- package/types/assign-gingerly/types.d.ts +10 -0
- package/withIdsCorrector.js +47 -0
- package/withIdsCorrector.ts +59 -0
package/assignFrom.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Handler commands (` =>`) are fire-and-forget (kicked off asynchronously, not awaited).
|
|
10
10
|
*/
|
|
11
|
-
import { getValues } from './getValues.js';
|
|
11
|
+
import { getValues, getValue } from './getValues.js';
|
|
12
12
|
import assignGingerly from './assignGingerly.js';
|
|
13
13
|
import { resolveIdVariable, parseIdRef } from './resolveIdRef.js';
|
|
14
14
|
import { processInferredAssignments } from './inferredAssignments.js';
|
|
@@ -26,6 +26,120 @@ export const SUBSTITUTION_VARS = [
|
|
|
26
26
|
export function isHandlerCommand(key) {
|
|
27
27
|
return key.endsWith(' =>');
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if a key ends with the ternary operator ' ?='.
|
|
31
|
+
*/
|
|
32
|
+
export function isTernaryCommand(key) {
|
|
33
|
+
return key.endsWith(' ?=');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse a ?= ternary command and extract the LHS path.
|
|
37
|
+
*/
|
|
38
|
+
export function parseTernaryCommand(key) {
|
|
39
|
+
if (!isTernaryCommand(key))
|
|
40
|
+
return null;
|
|
41
|
+
return key.substring(0, key.length - 3); // Remove ' ?=' suffix
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a single value — if it's a `?.` path string, resolve against source.
|
|
45
|
+
* If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
|
|
46
|
+
*/
|
|
47
|
+
function resolveTernaryValue(value, source, options) {
|
|
48
|
+
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
49
|
+
return getValue(value, source, {
|
|
50
|
+
withMethods: options.withMethods,
|
|
51
|
+
aka: options.aka,
|
|
52
|
+
protocols: options.protocols
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'string' && value.includes('://') && options.protocols) {
|
|
56
|
+
// Check if it matches a known protocol
|
|
57
|
+
const protoEnd = value.indexOf('://');
|
|
58
|
+
const protocol = value.substring(0, protoEnd);
|
|
59
|
+
if (options.protocols[protocol]) {
|
|
60
|
+
return getValue(value, source, {
|
|
61
|
+
withMethods: options.withMethods,
|
|
62
|
+
aka: options.aka,
|
|
63
|
+
protocols: options.protocols
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Evaluate a ?= ternary expression.
|
|
71
|
+
*
|
|
72
|
+
* Supported forms:
|
|
73
|
+
* - [ifTruthy, thenResult] — guard (skip if falsy)
|
|
74
|
+
* - [ifTruthy, thenResult, elseResult] — ternary
|
|
75
|
+
* - [ifTrue, trueResult, falseResult, neither] — three-state (true/false/nullish)
|
|
76
|
+
* - [[lhs, rhs], ifEqual, ifNotEqual?] — equality comparison
|
|
77
|
+
* - [[lhs, rhs], ifEqual] — equality guard
|
|
78
|
+
*
|
|
79
|
+
* Returns undefined to signal "skip assignment" (guard forms when condition not met).
|
|
80
|
+
*/
|
|
81
|
+
const TERNARY_SKIP = Symbol('ternary-skip');
|
|
82
|
+
function evaluateTernary(arr, source, options) {
|
|
83
|
+
const condition = arr[0];
|
|
84
|
+
if (Array.isArray(condition)) {
|
|
85
|
+
// Comparison mode: [[lhs, rhs], ...] or [[lhs, op, rhs], ...]
|
|
86
|
+
const lhs = resolveTernaryValue(condition[0], source, options);
|
|
87
|
+
if (condition.length === 2) {
|
|
88
|
+
// Equality: [[lhs, rhs], result, elseResult?]
|
|
89
|
+
const rhs = resolveTernaryValue(condition[1], source, options);
|
|
90
|
+
if (lhs === rhs) {
|
|
91
|
+
return resolveTernaryValue(arr[1], source, options);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Operator: [[lhs, op, rhs], result, elseResult?]
|
|
99
|
+
const op = condition[1];
|
|
100
|
+
const rhs = resolveTernaryValue(condition[2], source, options);
|
|
101
|
+
const satisfied = compareWithOp(lhs, op, rhs);
|
|
102
|
+
if (satisfied) {
|
|
103
|
+
return resolveTernaryValue(arr[1], source, options);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Truthiness mode
|
|
112
|
+
const resolved = resolveTernaryValue(condition, source, options);
|
|
113
|
+
if (arr.length === 4) {
|
|
114
|
+
// [ifTrue, trueResult, falseResult, neitherResult]
|
|
115
|
+
if (resolved == null)
|
|
116
|
+
return resolveTernaryValue(arr[3], source, options);
|
|
117
|
+
return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
|
|
118
|
+
}
|
|
119
|
+
else if (arr.length === 3) {
|
|
120
|
+
// [ifTruthy, thenResult, elseResult]
|
|
121
|
+
return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// [ifTruthy, thenResult] — guard, skip if falsy
|
|
125
|
+
return resolved ? resolveTernaryValue(arr[1], source, options) : TERNARY_SKIP;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Compare two values with a given operator.
|
|
131
|
+
*/
|
|
132
|
+
function compareWithOp(lhs, op, rhs) {
|
|
133
|
+
switch (op) {
|
|
134
|
+
case '===': return lhs === rhs;
|
|
135
|
+
case '!==': return lhs !== rhs;
|
|
136
|
+
case '>': return lhs > rhs;
|
|
137
|
+
case '>=': return lhs >= rhs;
|
|
138
|
+
case '<': return lhs < rhs;
|
|
139
|
+
case '<=': return lhs <= rhs;
|
|
140
|
+
default: return lhs === rhs; // fallback to equality
|
|
141
|
+
}
|
|
142
|
+
}
|
|
29
143
|
/**
|
|
30
144
|
* Recursively substitute a placeholder in all string values of an object.
|
|
31
145
|
*/
|
|
@@ -150,6 +264,7 @@ export function categorizeKeys(expandedPattern) {
|
|
|
150
264
|
const normalPattern = {};
|
|
151
265
|
const idRefNormalKeys = [];
|
|
152
266
|
const idRefHandlerKeys = [];
|
|
267
|
+
const ternaryKeys = [];
|
|
153
268
|
for (const key of Object.keys(expandedPattern)) {
|
|
154
269
|
if (isHandlerCommand(key)) {
|
|
155
270
|
if (key.startsWith('#[')) {
|
|
@@ -159,6 +274,9 @@ export function categorizeKeys(expandedPattern) {
|
|
|
159
274
|
handlerKeys.push(key);
|
|
160
275
|
}
|
|
161
276
|
}
|
|
277
|
+
else if (isTernaryCommand(key)) {
|
|
278
|
+
ternaryKeys.push(key);
|
|
279
|
+
}
|
|
162
280
|
else if (key.startsWith('#[')) {
|
|
163
281
|
idRefNormalKeys.push(key);
|
|
164
282
|
}
|
|
@@ -166,19 +284,32 @@ export function categorizeKeys(expandedPattern) {
|
|
|
166
284
|
normalPattern[key] = expandedPattern[key];
|
|
167
285
|
}
|
|
168
286
|
}
|
|
169
|
-
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys };
|
|
287
|
+
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Merge withIds and at into a single lookup map for resolveIdVariable.
|
|
291
|
+
*/
|
|
292
|
+
function getEffectiveIds(options) {
|
|
293
|
+
if (!options.withIds && !options.at)
|
|
294
|
+
return undefined;
|
|
295
|
+
if (options.withIds && !options.at)
|
|
296
|
+
return options.withIds;
|
|
297
|
+
if (!options.withIds && options.at)
|
|
298
|
+
return options.at;
|
|
299
|
+
return { ...options.withIds, ...options.at };
|
|
170
300
|
}
|
|
171
301
|
/**
|
|
172
302
|
* Process #[x] normal keys synchronously.
|
|
173
303
|
*/
|
|
174
304
|
function processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, options) {
|
|
175
|
-
|
|
305
|
+
const ids = getEffectiveIds(options);
|
|
306
|
+
if (!ids)
|
|
176
307
|
return;
|
|
177
308
|
for (const key of idRefNormalKeys) {
|
|
178
309
|
const parsed = parseIdRef(key);
|
|
179
310
|
if (!parsed)
|
|
180
311
|
continue;
|
|
181
|
-
const el = resolveIdVariable(parsed.varName, target,
|
|
312
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
182
313
|
if (!el)
|
|
183
314
|
continue;
|
|
184
315
|
const value = expandedPattern[key];
|
|
@@ -210,9 +341,55 @@ export function assignFrom(target, pattern, options, permissions) {
|
|
|
210
341
|
// Expand looped substitution variables
|
|
211
342
|
const expandedPattern = expandSubstitutions(pattern, options);
|
|
212
343
|
// Categorize keys
|
|
213
|
-
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys } = categorizeKeys(expandedPattern);
|
|
344
|
+
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
|
|
345
|
+
// Process ?= ternary keys (sync)
|
|
346
|
+
if (ternaryKeys.length > 0) {
|
|
347
|
+
const ternaryResolved = {};
|
|
348
|
+
for (const key of ternaryKeys) {
|
|
349
|
+
const lhsPath = parseTernaryCommand(key);
|
|
350
|
+
if (!lhsPath)
|
|
351
|
+
continue;
|
|
352
|
+
const arr = expandedPattern[key];
|
|
353
|
+
if (!Array.isArray(arr) || arr.length < 2)
|
|
354
|
+
continue;
|
|
355
|
+
const result = evaluateTernary(arr, options.from, options);
|
|
356
|
+
if (result !== TERNARY_SKIP) {
|
|
357
|
+
ternaryResolved[lhsPath] = result;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (Object.keys(ternaryResolved).length > 0) {
|
|
361
|
+
assignGingerly(target, ternaryResolved, options);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
214
364
|
// Process normal keys via getValues (sync) + assignGingerly
|
|
215
365
|
if (Object.keys(normalPattern).length > 0) {
|
|
366
|
+
// Resolve #[x] references on RHS values before getValues
|
|
367
|
+
if (options.withIds || options.at) {
|
|
368
|
+
const ids = getEffectiveIds(options);
|
|
369
|
+
for (const key of Object.keys(normalPattern)) {
|
|
370
|
+
const value = normalPattern[key];
|
|
371
|
+
if (typeof value === 'string' && value.startsWith('#[')) {
|
|
372
|
+
const closeIdx = value.indexOf(']');
|
|
373
|
+
if (closeIdx !== -1) {
|
|
374
|
+
const varName = value.substring(2, closeIdx);
|
|
375
|
+
const el = resolveIdVariable(varName, target, ids);
|
|
376
|
+
if (el) {
|
|
377
|
+
const remainingPath = value.substring(closeIdx + 1);
|
|
378
|
+
if (remainingPath) {
|
|
379
|
+
normalPattern[key] = getValue(remainingPath, el, {
|
|
380
|
+
withMethods: options.withMethods,
|
|
381
|
+
aka: options.aka,
|
|
382
|
+
protocols: options.protocols
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
normalPattern[key] = el.id; // bare #[x] → ID string
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
216
393
|
const resolved = getValues(normalPattern, options.from, {
|
|
217
394
|
withMethods: options.withMethods,
|
|
218
395
|
aka: options.aka,
|
|
@@ -232,13 +409,14 @@ export function assignFrom(target, pattern, options, permissions) {
|
|
|
232
409
|
});
|
|
233
410
|
}
|
|
234
411
|
// Process #[x] handler keys — fire-and-forget (async)
|
|
235
|
-
if (idRefHandlerKeys.length > 0 && options.withIds) {
|
|
412
|
+
if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
|
|
413
|
+
const ids = getEffectiveIds(options);
|
|
236
414
|
import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
|
|
237
415
|
for (const key of idRefHandlerKeys) {
|
|
238
416
|
const parsed = parseIdRef(key);
|
|
239
417
|
if (!parsed)
|
|
240
418
|
continue;
|
|
241
|
-
const el = resolveIdVariable(parsed.varName, target,
|
|
419
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
242
420
|
if (!el)
|
|
243
421
|
continue;
|
|
244
422
|
const syntheticKey = parsed.remainingPath ? `${parsed.remainingPath} =>` : ' =>';
|
|
@@ -248,15 +426,15 @@ export function assignFrom(target, pattern, options, permissions) {
|
|
|
248
426
|
});
|
|
249
427
|
}
|
|
250
428
|
// Process inferred assignments (sync)
|
|
251
|
-
if (options.
|
|
252
|
-
processInferredAssignments(target, options.from, options.
|
|
429
|
+
if (options.infer) {
|
|
430
|
+
processInferredAssignments(target, options.from, options.infer);
|
|
253
431
|
// beVigilant — fire-and-forget (async)
|
|
254
|
-
if (options.
|
|
432
|
+
if (options.infer.beVigilant) {
|
|
255
433
|
if (!options.signal) {
|
|
256
|
-
throw new Error('assignFrom:
|
|
434
|
+
throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
|
|
257
435
|
}
|
|
258
436
|
import('./beVigilant.js').then(({ setupVigilantObserver }) => {
|
|
259
|
-
setupVigilantObserver(target, options.from, options.
|
|
437
|
+
setupVigilantObserver(target, options.from, options.infer, options.signal);
|
|
260
438
|
});
|
|
261
439
|
}
|
|
262
440
|
}
|
package/assignFrom.ts
CHANGED
|
@@ -35,6 +35,119 @@ export function isHandlerCommand(key: string): boolean {
|
|
|
35
35
|
return key.endsWith(' =>');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Check if a key ends with the ternary operator ' ?='.
|
|
40
|
+
*/
|
|
41
|
+
export function isTernaryCommand(key: string): boolean {
|
|
42
|
+
return key.endsWith(' ?=');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse a ?= ternary command and extract the LHS path.
|
|
47
|
+
*/
|
|
48
|
+
export function parseTernaryCommand(key: string): string | null {
|
|
49
|
+
if (!isTernaryCommand(key)) return null;
|
|
50
|
+
return key.substring(0, key.length - 3); // Remove ' ?=' suffix
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a single value — if it's a `?.` path string, resolve against source.
|
|
55
|
+
* If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
|
|
56
|
+
*/
|
|
57
|
+
function resolveTernaryValue(value: any, source: any, options: AssignFromOptions): any {
|
|
58
|
+
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
59
|
+
return getValue(value, source, {
|
|
60
|
+
withMethods: options.withMethods,
|
|
61
|
+
aka: options.aka,
|
|
62
|
+
protocols: options.protocols
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'string' && value.includes('://') && options.protocols) {
|
|
66
|
+
// Check if it matches a known protocol
|
|
67
|
+
const protoEnd = value.indexOf('://');
|
|
68
|
+
const protocol = value.substring(0, protoEnd);
|
|
69
|
+
if (options.protocols[protocol]) {
|
|
70
|
+
return getValue(value, source, {
|
|
71
|
+
withMethods: options.withMethods,
|
|
72
|
+
aka: options.aka,
|
|
73
|
+
protocols: options.protocols
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Evaluate a ?= ternary expression.
|
|
82
|
+
*
|
|
83
|
+
* Supported forms:
|
|
84
|
+
* - [ifTruthy, thenResult] — guard (skip if falsy)
|
|
85
|
+
* - [ifTruthy, thenResult, elseResult] — ternary
|
|
86
|
+
* - [ifTrue, trueResult, falseResult, neither] — three-state (true/false/nullish)
|
|
87
|
+
* - [[lhs, rhs], ifEqual, ifNotEqual?] — equality comparison
|
|
88
|
+
* - [[lhs, rhs], ifEqual] — equality guard
|
|
89
|
+
*
|
|
90
|
+
* Returns undefined to signal "skip assignment" (guard forms when condition not met).
|
|
91
|
+
*/
|
|
92
|
+
const TERNARY_SKIP = Symbol('ternary-skip');
|
|
93
|
+
|
|
94
|
+
function evaluateTernary(arr: any[], source: any, options: AssignFromOptions): any {
|
|
95
|
+
const condition = arr[0];
|
|
96
|
+
|
|
97
|
+
if (Array.isArray(condition)) {
|
|
98
|
+
// Comparison mode: [[lhs, rhs], ...] or [[lhs, op, rhs], ...]
|
|
99
|
+
const lhs = resolveTernaryValue(condition[0], source, options);
|
|
100
|
+
if (condition.length === 2) {
|
|
101
|
+
// Equality: [[lhs, rhs], result, elseResult?]
|
|
102
|
+
const rhs = resolveTernaryValue(condition[1], source, options);
|
|
103
|
+
if (lhs === rhs) {
|
|
104
|
+
return resolveTernaryValue(arr[1], source, options);
|
|
105
|
+
} else {
|
|
106
|
+
return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
// Operator: [[lhs, op, rhs], result, elseResult?]
|
|
110
|
+
const op = condition[1] as string;
|
|
111
|
+
const rhs = resolveTernaryValue(condition[2], source, options);
|
|
112
|
+
const satisfied = compareWithOp(lhs, op, rhs);
|
|
113
|
+
if (satisfied) {
|
|
114
|
+
return resolveTernaryValue(arr[1], source, options);
|
|
115
|
+
} else {
|
|
116
|
+
return arr.length > 2 ? resolveTernaryValue(arr[2], source, options) : TERNARY_SKIP;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// Truthiness mode
|
|
121
|
+
const resolved = resolveTernaryValue(condition, source, options);
|
|
122
|
+
if (arr.length === 4) {
|
|
123
|
+
// [ifTrue, trueResult, falseResult, neitherResult]
|
|
124
|
+
if (resolved == null) return resolveTernaryValue(arr[3], source, options);
|
|
125
|
+
return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
|
|
126
|
+
} else if (arr.length === 3) {
|
|
127
|
+
// [ifTruthy, thenResult, elseResult]
|
|
128
|
+
return resolved ? resolveTernaryValue(arr[1], source, options) : resolveTernaryValue(arr[2], source, options);
|
|
129
|
+
} else {
|
|
130
|
+
// [ifTruthy, thenResult] — guard, skip if falsy
|
|
131
|
+
return resolved ? resolveTernaryValue(arr[1], source, options) : TERNARY_SKIP;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Compare two values with a given operator.
|
|
138
|
+
*/
|
|
139
|
+
function compareWithOp(lhs: any, op: string, rhs: any): boolean {
|
|
140
|
+
switch (op) {
|
|
141
|
+
case '===': return lhs === rhs;
|
|
142
|
+
case '!==': return lhs !== rhs;
|
|
143
|
+
case '>': return lhs > rhs;
|
|
144
|
+
case '>=': return lhs >= rhs;
|
|
145
|
+
case '<': return lhs < rhs;
|
|
146
|
+
case '<=': return lhs <= rhs;
|
|
147
|
+
default: return lhs === rhs; // fallback to equality
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
38
151
|
/**
|
|
39
152
|
* Recursively substitute a placeholder in all string values of an object.
|
|
40
153
|
*/
|
|
@@ -164,6 +277,7 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
|
|
|
164
277
|
const normalPattern: Record<string, any> = {};
|
|
165
278
|
const idRefNormalKeys: string[] = [];
|
|
166
279
|
const idRefHandlerKeys: string[] = [];
|
|
280
|
+
const ternaryKeys: string[] = [];
|
|
167
281
|
|
|
168
282
|
for (const key of Object.keys(expandedPattern)) {
|
|
169
283
|
if (isHandlerCommand(key)) {
|
|
@@ -172,6 +286,8 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
|
|
|
172
286
|
} else {
|
|
173
287
|
handlerKeys.push(key);
|
|
174
288
|
}
|
|
289
|
+
} else if (isTernaryCommand(key)) {
|
|
290
|
+
ternaryKeys.push(key);
|
|
175
291
|
} else if (key.startsWith('#[')) {
|
|
176
292
|
idRefNormalKeys.push(key);
|
|
177
293
|
} else {
|
|
@@ -179,7 +295,17 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
|
|
|
179
295
|
}
|
|
180
296
|
}
|
|
181
297
|
|
|
182
|
-
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys };
|
|
298
|
+
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Merge withIds and at into a single lookup map for resolveIdVariable.
|
|
303
|
+
*/
|
|
304
|
+
function getEffectiveIds(options: AssignFromOptions): Record<string, any> | undefined {
|
|
305
|
+
if (!options.withIds && !options.at) return undefined;
|
|
306
|
+
if (options.withIds && !options.at) return options.withIds;
|
|
307
|
+
if (!options.withIds && options.at) return options.at;
|
|
308
|
+
return { ...options.withIds, ...options.at };
|
|
183
309
|
}
|
|
184
310
|
|
|
185
311
|
/**
|
|
@@ -191,13 +317,14 @@ function processIdRefNormalKeys(
|
|
|
191
317
|
target: any,
|
|
192
318
|
options: AssignFromOptions
|
|
193
319
|
): void {
|
|
194
|
-
|
|
320
|
+
const ids = getEffectiveIds(options);
|
|
321
|
+
if (!ids) return;
|
|
195
322
|
|
|
196
323
|
for (const key of idRefNormalKeys) {
|
|
197
324
|
const parsed = parseIdRef(key);
|
|
198
325
|
if (!parsed) continue;
|
|
199
326
|
|
|
200
|
-
const el = resolveIdVariable(parsed.varName, target,
|
|
327
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
201
328
|
if (!el) continue;
|
|
202
329
|
|
|
203
330
|
const value = expandedPattern[key];
|
|
@@ -242,10 +369,55 @@ export function assignFrom(
|
|
|
242
369
|
const expandedPattern = expandSubstitutions(pattern, options);
|
|
243
370
|
|
|
244
371
|
// Categorize keys
|
|
245
|
-
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys } = categorizeKeys(expandedPattern);
|
|
372
|
+
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
|
|
373
|
+
|
|
374
|
+
// Process ?= ternary keys (sync)
|
|
375
|
+
if (ternaryKeys.length > 0) {
|
|
376
|
+
const ternaryResolved: Record<string, any> = {};
|
|
377
|
+
for (const key of ternaryKeys) {
|
|
378
|
+
const lhsPath = parseTernaryCommand(key);
|
|
379
|
+
if (!lhsPath) continue;
|
|
380
|
+
const arr = expandedPattern[key];
|
|
381
|
+
if (!Array.isArray(arr) || arr.length < 2) continue;
|
|
382
|
+
const result = evaluateTernary(arr, options.from, options);
|
|
383
|
+
if (result !== TERNARY_SKIP) {
|
|
384
|
+
ternaryResolved[lhsPath] = result;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (Object.keys(ternaryResolved).length > 0) {
|
|
388
|
+
assignGingerly(target, ternaryResolved, options);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
246
391
|
|
|
247
392
|
// Process normal keys via getValues (sync) + assignGingerly
|
|
248
393
|
if (Object.keys(normalPattern).length > 0) {
|
|
394
|
+
// Resolve #[x] references on RHS values before getValues
|
|
395
|
+
if (options.withIds || options.at) {
|
|
396
|
+
const ids = getEffectiveIds(options)!;
|
|
397
|
+
for (const key of Object.keys(normalPattern)) {
|
|
398
|
+
const value = normalPattern[key];
|
|
399
|
+
if (typeof value === 'string' && value.startsWith('#[')) {
|
|
400
|
+
const closeIdx = value.indexOf(']');
|
|
401
|
+
if (closeIdx !== -1) {
|
|
402
|
+
const varName = value.substring(2, closeIdx);
|
|
403
|
+
const el = resolveIdVariable(varName, target, ids);
|
|
404
|
+
if (el) {
|
|
405
|
+
const remainingPath = value.substring(closeIdx + 1);
|
|
406
|
+
if (remainingPath) {
|
|
407
|
+
normalPattern[key] = getValue(remainingPath, el, {
|
|
408
|
+
withMethods: options.withMethods,
|
|
409
|
+
aka: options.aka,
|
|
410
|
+
protocols: options.protocols
|
|
411
|
+
});
|
|
412
|
+
} else {
|
|
413
|
+
normalPattern[key] = el.id; // bare #[x] → ID string
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
249
421
|
const resolved = getValues(normalPattern, options.from, {
|
|
250
422
|
withMethods: options.withMethods,
|
|
251
423
|
aka: options.aka,
|
|
@@ -269,12 +441,13 @@ export function assignFrom(
|
|
|
269
441
|
}
|
|
270
442
|
|
|
271
443
|
// Process #[x] handler keys — fire-and-forget (async)
|
|
272
|
-
if (idRefHandlerKeys.length > 0 && options.withIds) {
|
|
444
|
+
if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
|
|
445
|
+
const ids = getEffectiveIds(options)!;
|
|
273
446
|
import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
|
|
274
447
|
for (const key of idRefHandlerKeys) {
|
|
275
448
|
const parsed = parseIdRef(key);
|
|
276
449
|
if (!parsed) continue;
|
|
277
|
-
const el = resolveIdVariable(parsed.varName, target,
|
|
450
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
278
451
|
if (!el) continue;
|
|
279
452
|
const syntheticKey = parsed.remainingPath ? `${parsed.remainingPath} =>` : ' =>';
|
|
280
453
|
const syntheticPattern = { [syntheticKey]: expandedPattern[key] };
|
|
@@ -284,16 +457,16 @@ export function assignFrom(
|
|
|
284
457
|
}
|
|
285
458
|
|
|
286
459
|
// Process inferred assignments (sync)
|
|
287
|
-
if (options.
|
|
288
|
-
processInferredAssignments(target, options.from, options.
|
|
460
|
+
if (options.infer) {
|
|
461
|
+
processInferredAssignments(target, options.from, options.infer);
|
|
289
462
|
|
|
290
463
|
// beVigilant — fire-and-forget (async)
|
|
291
|
-
if (options.
|
|
464
|
+
if (options.infer.beVigilant) {
|
|
292
465
|
if (!options.signal) {
|
|
293
|
-
throw new Error('assignFrom:
|
|
466
|
+
throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
|
|
294
467
|
}
|
|
295
468
|
import('./beVigilant.js').then(({ setupVigilantObserver }) => {
|
|
296
|
-
setupVigilantObserver(target, options.from, options.
|
|
469
|
+
setupVigilantObserver(target, options.from, options.infer!, options.signal!);
|
|
297
470
|
});
|
|
298
471
|
}
|
|
299
472
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assignFromAsync-extension.js — Adds assignFromAsync to Object.prototype.
|
|
3
|
+
*
|
|
4
|
+
* Import this module for the side effect of extending all objects with
|
|
5
|
+
* the assignFromAsync method, enabling awaitable handler execution:
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import 'assign-gingerly/assignFromAsync-extension.js';
|
|
9
|
+
*
|
|
10
|
+
* await oElement.assignFromAsync({
|
|
11
|
+
* '?.querySelector?..mainView =>': {
|
|
12
|
+
* do: 'builtIns.lazyLoad',
|
|
13
|
+
* get: { if: '?.isVisible', instantiate: 'globalThis://myTemplate' }
|
|
14
|
+
* }
|
|
15
|
+
* }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { assignFromAsync } from './assignFromAsync.js';
|
|
19
|
+
|
|
20
|
+
Object.defineProperty(Object.prototype, 'assignFromAsync', {
|
|
21
|
+
value: async function (pattern, options) {
|
|
22
|
+
await assignFromAsync(this, pattern, options);
|
|
23
|
+
return this;
|
|
24
|
+
},
|
|
25
|
+
writable: true,
|
|
26
|
+
enumerable: false,
|
|
27
|
+
configurable: true,
|
|
28
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assignFromAsync-extension.ts — Adds assignFromAsync to Object.prototype.
|
|
3
|
+
*
|
|
4
|
+
* Import this module for the side effect of extending all objects with
|
|
5
|
+
* the assignFromAsync method, enabling awaitable handler execution:
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import 'assign-gingerly/assignFromAsync-extension.js';
|
|
9
|
+
*
|
|
10
|
+
* await oElement.assignFromAsync({
|
|
11
|
+
* '?.querySelector?..mainView =>': {
|
|
12
|
+
* do: 'builtIns.lazyLoad',
|
|
13
|
+
* get: { if: '?.isVisible', instantiate: 'globalThis://myTemplate' }
|
|
14
|
+
* }
|
|
15
|
+
* }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { assignFromAsync } from './assignFromAsync.js';
|
|
19
|
+
import type { AssignFromOptions } from './assignFromAsync.js';
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
interface Object {
|
|
23
|
+
/**
|
|
24
|
+
* Resolve RHS path strings from a source object and assign into this object.
|
|
25
|
+
* Async — awaits handler execution and supports async protocol handlers.
|
|
26
|
+
*
|
|
27
|
+
* @param pattern - Object with LHS paths as keys and RHS path strings (or literals) as values
|
|
28
|
+
* @param options - Configuration including `from` (source object), protocols, withMethods, etc.
|
|
29
|
+
* @returns Promise resolving to this object after assignment
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* await oElement.assignFromAsync({
|
|
33
|
+
* '?.querySelector?..outlet =>': {
|
|
34
|
+
* do: 'builtIns.lazyLoad',
|
|
35
|
+
* get: { if: '?.showContent', instantiate: 'globalThis://myTemplate' }
|
|
36
|
+
* }
|
|
37
|
+
* }, { from: viewModel, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
38
|
+
*/
|
|
39
|
+
assignFromAsync(
|
|
40
|
+
pattern: Record<string, any>,
|
|
41
|
+
options: AssignFromOptions
|
|
42
|
+
): Promise<this>;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Object.defineProperty(Object.prototype, 'assignFromAsync', {
|
|
47
|
+
value: async function <T extends object>(
|
|
48
|
+
this: T,
|
|
49
|
+
pattern: Record<string, any>,
|
|
50
|
+
options: AssignFromOptions
|
|
51
|
+
): Promise<T> {
|
|
52
|
+
await assignFromAsync(this, pattern, options);
|
|
53
|
+
return this;
|
|
54
|
+
},
|
|
55
|
+
writable: true,
|
|
56
|
+
enumerable: false,
|
|
57
|
+
configurable: true,
|
|
58
|
+
});
|
package/assignFromAsync.js
CHANGED
|
@@ -41,13 +41,14 @@ export async function assignFromAsync(target, pattern, options, permissions) {
|
|
|
41
41
|
assignGingerly(target, resolved, options);
|
|
42
42
|
}
|
|
43
43
|
// Process #[x] normal keys — resolve element, then apply remaining path + value
|
|
44
|
-
if (idRefNormalKeys.length > 0 && options.withIds) {
|
|
44
|
+
if (idRefNormalKeys.length > 0 && (options.withIds || options.at)) {
|
|
45
|
+
const ids = { ...options.withIds, ...options.at };
|
|
45
46
|
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
46
47
|
for (const key of idRefNormalKeys) {
|
|
47
48
|
const parsed = parseIdRef(key);
|
|
48
49
|
if (!parsed)
|
|
49
50
|
continue;
|
|
50
|
-
const el = resolveIdVariable(parsed.varName, target,
|
|
51
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
51
52
|
if (!el)
|
|
52
53
|
continue;
|
|
53
54
|
const value = expandedPattern[key];
|
|
@@ -75,14 +76,15 @@ export async function assignFromAsync(target, pattern, options, permissions) {
|
|
|
75
76
|
await _processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
|
|
76
77
|
}
|
|
77
78
|
// Process #[x] handler keys — resolve element, then pass to handler processing
|
|
78
|
-
if (idRefHandlerKeys.length > 0 && options.withIds) {
|
|
79
|
+
if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
|
|
80
|
+
const ids = { ...options.withIds, ...options.at };
|
|
79
81
|
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
80
82
|
_processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
|
|
81
83
|
for (const key of idRefHandlerKeys) {
|
|
82
84
|
const parsed = parseIdRef(key);
|
|
83
85
|
if (!parsed)
|
|
84
86
|
continue;
|
|
85
|
-
const el = resolveIdVariable(parsed.varName, target,
|
|
87
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
86
88
|
if (!el)
|
|
87
89
|
continue;
|
|
88
90
|
// Build a synthetic key for processHandlerCommands:
|
|
@@ -97,16 +99,16 @@ export async function assignFromAsync(target, pattern, options, permissions) {
|
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
101
|
// Process inferred assignments — dynamically imported only when option is present
|
|
100
|
-
if (options.
|
|
102
|
+
if (options.infer) {
|
|
101
103
|
const { processInferredAssignments } = await import('./inferredAssignments.js');
|
|
102
|
-
await processInferredAssignments(target, options.from, options.
|
|
104
|
+
await processInferredAssignments(target, options.from, options.infer);
|
|
103
105
|
// Set up MutationObserver for new matching elements if beVigilant
|
|
104
|
-
if (options.
|
|
106
|
+
if (options.infer.beVigilant) {
|
|
105
107
|
if (!options.signal) {
|
|
106
|
-
throw new Error('assignFrom:
|
|
108
|
+
throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
|
|
107
109
|
}
|
|
108
110
|
const { setupVigilantObserver } = await import('./beVigilant.js');
|
|
109
|
-
setupVigilantObserver(target, options.from, options.
|
|
111
|
+
setupVigilantObserver(target, options.from, options.infer, options.signal);
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
// Process bulk enhancements — dynamically imported only when option is present
|