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
package/assignFrom.ts
CHANGED
|
@@ -1,140 +1,157 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* resolved values into a target using assignGingerly.
|
|
2
|
+
* assignFrom.ts — Synchronous assign-from-source function.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Resolves RHS path strings synchronously via getValues, then assigns into the target.
|
|
5
|
+
* Supports looped substitution, #[x] refs, inferredAssignments, and handleSpreads — all sync.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
10
|
-
* @param options - Options including `from` (source object) and any assignGingerly options
|
|
11
|
-
* @returns The target object after merging
|
|
7
|
+
* For async protocol handlers or awaitable handler execution, use assignFromAsync.
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
* const source = { theme: { color: 'red' }, label: 'Hello' };
|
|
15
|
-
* const target = { color: 'blue', text: '' };
|
|
16
|
-
* assignFrom(target, {
|
|
17
|
-
* color: '?.theme?.color',
|
|
18
|
-
* text: '?.label'
|
|
19
|
-
* }, { from: source });
|
|
20
|
-
* // target is now { color: 'red', text: 'Hello' }
|
|
9
|
+
* Handler commands (` =>`) are fire-and-forget (kicked off asynchronously, not awaited).
|
|
21
10
|
*/
|
|
22
|
-
|
|
11
|
+
|
|
12
|
+
import { getValues, getValue } from './getValues.js';
|
|
23
13
|
import assignGingerly, { IAssignGingerlyOptions } from './assignGingerly.js';
|
|
14
|
+
import { resolveIdVariable, parseIdRef } from './resolveIdRef.js';
|
|
15
|
+
import { processInferredAssignments } from './inferredAssignments.js';
|
|
24
16
|
import type { AssignPermissions } from './isAllowedImportPath.js';
|
|
25
17
|
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* Elements are cached via WeakRef with getElementById fallback on cache miss.
|
|
45
|
-
*/
|
|
46
|
-
withIds?: Record<string, string | { qry: string }>;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Handler implementations scoped to this call.
|
|
50
|
-
* Key: the `do` name referenced in handler configs.
|
|
51
|
-
* Value: a class constructor, or an import path to dynamically load one.
|
|
52
|
-
*
|
|
53
|
-
* Import paths must be local (relative, absolute, or bare specifier — no cross-domain URLs).
|
|
54
|
-
* The module's default export is checked first; otherwise the first exported class
|
|
55
|
-
* with an `assign` method on its prototype is used.
|
|
56
|
-
*
|
|
57
|
-
* Built-in handlers (builtIns.*) auto-load without needing to be listed here.
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* handlers: {
|
|
61
|
-
* 'my-list': MyListHandler, // class constructor
|
|
62
|
-
* 'my-chart': './handlers/chart.js', // dynamic import path
|
|
63
|
-
* 'vendor-widget': 'some-package/handler.js', // bare specifier (import map)
|
|
64
|
-
* }
|
|
65
|
-
*/
|
|
66
|
-
handlers?: Record<string, AssignFromHandlerConstructor | string>;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Inferred assignments — automatically distribute source values to matching
|
|
70
|
-
* DOM elements based on structural conventions (itemprop, name, etc.).
|
|
71
|
-
*
|
|
72
|
-
* Uses the inferencer submodule to determine the correct property for each
|
|
73
|
-
* matched element (textContent, value, checked, dateTime, ish, etc.).
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* inferredAssignments: {
|
|
77
|
-
* byItemprop: ['user', 'name', 'email'], // or true for all source keys
|
|
78
|
-
* beVigilant: true, // watch for new matching elements (requires signal)
|
|
79
|
-
* }
|
|
80
|
-
*/
|
|
81
|
-
inferredAssignments?: {
|
|
82
|
-
byItemprop?: string[] | true;
|
|
83
|
-
/** Watch for new matching elements via MutationObserver. Requires options.signal for cleanup. */
|
|
84
|
-
beVigilant?: boolean;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Bulk enhancement application via EMC JSON configs.
|
|
89
|
-
* Finds matching elements and spawns enhancements on them.
|
|
90
|
-
*
|
|
91
|
-
* Each entry specifies an EMC JSON path and optionally overrides the matching selector.
|
|
92
|
-
* Enhancements are auto-registered if not already present in the enhancement registry.
|
|
93
|
-
*
|
|
94
|
-
* No scope perimeter is applied — use mount-observer for reactive/scoped enhancement.
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* enhance: [
|
|
98
|
-
* { emc: 'be-bound/emc.json', matching: '[name]' },
|
|
99
|
-
* { emc: 'be-observant/emc.json', matching: '[itemprop]' },
|
|
100
|
-
* ]
|
|
101
|
-
*/
|
|
102
|
-
enhance?: Array<{ emc: string; matching?: string; parse?: boolean }>;
|
|
18
|
+
// Re-export types and interfaces for consumers
|
|
19
|
+
export type { AssignFromOptions, AssignFromHandler, AssignFromHandlerConstructor } from './assignFromAsync.js';
|
|
20
|
+
import type { AssignFromOptions } from './assignFromAsync.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Supported substitution variables and their option keys.
|
|
24
|
+
*/
|
|
25
|
+
export const SUBSTITUTION_VARS = [
|
|
26
|
+
{ placeholder: '${x}', optionKey: 'where_x_in' },
|
|
27
|
+
{ placeholder: '${y}', optionKey: 'where_y_in' },
|
|
28
|
+
{ placeholder: '${z}', optionKey: 'where_z_in' },
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a key ends with the handler operator ' =>'.
|
|
33
|
+
*/
|
|
34
|
+
export function isHandlerCommand(key: string): boolean {
|
|
35
|
+
return key.endsWith(' =>');
|
|
103
36
|
}
|
|
104
37
|
|
|
105
38
|
/**
|
|
106
|
-
*
|
|
107
|
-
* Handlers are invoked when a LHS key ends with ' =>'.
|
|
39
|
+
* Check if a key ends with the ternary operator ' ?='.
|
|
108
40
|
*/
|
|
109
|
-
export
|
|
110
|
-
|
|
41
|
+
export function isTernaryCommand(key: string): boolean {
|
|
42
|
+
return key.endsWith(' ?=');
|
|
111
43
|
}
|
|
112
44
|
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
51
|
}
|
|
116
52
|
|
|
117
53
|
/**
|
|
118
|
-
*
|
|
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.
|
|
119
56
|
*/
|
|
120
|
-
function
|
|
121
|
-
|
|
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;
|
|
122
78
|
}
|
|
123
79
|
|
|
124
80
|
/**
|
|
125
|
-
*
|
|
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).
|
|
126
91
|
*/
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
}
|
|
132
150
|
|
|
133
151
|
/**
|
|
134
152
|
* Recursively substitute a placeholder in all string values of an object.
|
|
135
|
-
* Returns a new object (shallow clone at each level) with substitutions applied.
|
|
136
153
|
*/
|
|
137
|
-
function substituteInValue(value: any, placeholder: string, replacement: string): any {
|
|
154
|
+
export function substituteInValue(value: any, placeholder: string, replacement: string): any {
|
|
138
155
|
if (typeof value === 'string') {
|
|
139
156
|
return value.includes(placeholder) ? value.replaceAll(placeholder, replacement) : value;
|
|
140
157
|
}
|
|
@@ -157,7 +174,7 @@ function substituteInValue(value: any, placeholder: string, replacement: string)
|
|
|
157
174
|
/**
|
|
158
175
|
* Check if a pattern entry (key + value) contains a given placeholder.
|
|
159
176
|
*/
|
|
160
|
-
function entryContainsPlaceholder(key: string, value: any, placeholder: string): boolean {
|
|
177
|
+
export function entryContainsPlaceholder(key: string, value: any, placeholder: string): boolean {
|
|
161
178
|
if (key.includes(placeholder)) return true;
|
|
162
179
|
return valueContainsPlaceholder(value, placeholder);
|
|
163
180
|
}
|
|
@@ -165,7 +182,7 @@ function entryContainsPlaceholder(key: string, value: any, placeholder: string):
|
|
|
165
182
|
/**
|
|
166
183
|
* Check if a value (string, object, or array) contains a placeholder.
|
|
167
184
|
*/
|
|
168
|
-
function valueContainsPlaceholder(value: any, placeholder: string): boolean {
|
|
185
|
+
export function valueContainsPlaceholder(value: any, placeholder: string): boolean {
|
|
169
186
|
if (typeof value === 'string') return value.includes(placeholder);
|
|
170
187
|
if (Array.isArray(value)) return value.some(item => valueContainsPlaceholder(item, placeholder));
|
|
171
188
|
if (value && typeof value === 'object') {
|
|
@@ -179,12 +196,8 @@ function valueContainsPlaceholder(value: any, placeholder: string): boolean {
|
|
|
179
196
|
|
|
180
197
|
/**
|
|
181
198
|
* Expand looped substitution variables in a pattern.
|
|
182
|
-
* Applies cartesian expansion: x values are expanded first, then y, then z.
|
|
183
|
-
* Each variable multiplies the entries — result count = x.length × y.length × z.length.
|
|
184
|
-
*
|
|
185
|
-
* Returns the expanded pattern (or the original if no substitutions apply).
|
|
186
199
|
*/
|
|
187
|
-
function expandSubstitutions(
|
|
200
|
+
export function expandSubstitutions(
|
|
188
201
|
pattern: Record<string, any>,
|
|
189
202
|
options: AssignFromOptions
|
|
190
203
|
): Record<string, any> {
|
|
@@ -197,7 +210,6 @@ function expandSubstitutions(
|
|
|
197
210
|
const expanded: [string, any][] = [];
|
|
198
211
|
for (const [key, value] of entries) {
|
|
199
212
|
if (entryContainsPlaceholder(key, value, placeholder)) {
|
|
200
|
-
// Expand this entry for each value in the variable array
|
|
201
213
|
for (const replacement of values) {
|
|
202
214
|
const newKey = key.includes(placeholder)
|
|
203
215
|
? key.replaceAll(placeholder, replacement)
|
|
@@ -206,7 +218,6 @@ function expandSubstitutions(
|
|
|
206
218
|
expanded.push([newKey, newValue]);
|
|
207
219
|
}
|
|
208
220
|
} else {
|
|
209
|
-
// No placeholder in this entry — pass through
|
|
210
221
|
expanded.push([key, value]);
|
|
211
222
|
}
|
|
212
223
|
}
|
|
@@ -218,14 +229,11 @@ function expandSubstitutions(
|
|
|
218
229
|
|
|
219
230
|
/**
|
|
220
231
|
* Convert entries to an object, merging duplicate handler (` =>`) keys into arrays.
|
|
221
|
-
* For normal (non-handler) keys, later entries overwrite earlier ones (standard object behavior).
|
|
222
|
-
* For handler keys, duplicate entries are combined into an array (Multiple Handlers pattern).
|
|
223
232
|
*/
|
|
224
|
-
function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
|
|
233
|
+
export function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
|
|
225
234
|
const result: Record<string, any> = {};
|
|
226
235
|
for (const [key, value] of entries) {
|
|
227
236
|
if (key.endsWith(' =>') && key in result) {
|
|
228
|
-
// Duplicate handler key — merge into array
|
|
229
237
|
const existing = result[key];
|
|
230
238
|
if (Array.isArray(existing)) {
|
|
231
239
|
existing.push(value);
|
|
@@ -239,20 +247,37 @@ function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
|
|
|
239
247
|
return result;
|
|
240
248
|
}
|
|
241
249
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Recursively walk an object and handle "..." spread keys.
|
|
252
|
+
*/
|
|
253
|
+
export function handleSpreads(obj: Record<string, any>): Record<string, any> {
|
|
254
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
255
|
+
if (key !== '...' && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
256
|
+
const proto = Object.getPrototypeOf(value);
|
|
257
|
+
if (proto === Object.prototype || proto === null) {
|
|
258
|
+
obj[key] = handleSpreads(value);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if ('...' in obj) {
|
|
263
|
+
const spreadValue = obj['...'];
|
|
264
|
+
delete obj['...'];
|
|
265
|
+
if (spreadValue && typeof spreadValue === 'object') {
|
|
266
|
+
Object.assign(obj, spreadValue);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return obj;
|
|
270
|
+
}
|
|
250
271
|
|
|
251
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Categorize pattern keys into handler keys, #[x] keys, and normal keys.
|
|
274
|
+
*/
|
|
275
|
+
export function categorizeKeys(expandedPattern: Record<string, any>) {
|
|
252
276
|
const handlerKeys: string[] = [];
|
|
253
277
|
const normalPattern: Record<string, any> = {};
|
|
254
278
|
const idRefNormalKeys: string[] = [];
|
|
255
279
|
const idRefHandlerKeys: string[] = [];
|
|
280
|
+
const ternaryKeys: string[] = [];
|
|
256
281
|
|
|
257
282
|
for (const key of Object.keys(expandedPattern)) {
|
|
258
283
|
if (isHandlerCommand(key)) {
|
|
@@ -261,6 +286,8 @@ export async function assignFrom(
|
|
|
261
286
|
} else {
|
|
262
287
|
handlerKeys.push(key);
|
|
263
288
|
}
|
|
289
|
+
} else if (isTernaryCommand(key)) {
|
|
290
|
+
ternaryKeys.push(key);
|
|
264
291
|
} else if (key.startsWith('#[')) {
|
|
265
292
|
idRefNormalKeys.push(key);
|
|
266
293
|
} else {
|
|
@@ -268,131 +295,190 @@ export async function assignFrom(
|
|
|
268
295
|
}
|
|
269
296
|
}
|
|
270
297
|
|
|
271
|
-
|
|
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 };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Process #[x] normal keys synchronously.
|
|
313
|
+
*/
|
|
314
|
+
function processIdRefNormalKeys(
|
|
315
|
+
idRefNormalKeys: string[],
|
|
316
|
+
expandedPattern: Record<string, any>,
|
|
317
|
+
target: any,
|
|
318
|
+
options: AssignFromOptions
|
|
319
|
+
): void {
|
|
320
|
+
const ids = getEffectiveIds(options);
|
|
321
|
+
if (!ids) return;
|
|
322
|
+
|
|
323
|
+
for (const key of idRefNormalKeys) {
|
|
324
|
+
const parsed = parseIdRef(key);
|
|
325
|
+
if (!parsed) continue;
|
|
326
|
+
|
|
327
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
328
|
+
if (!el) continue;
|
|
329
|
+
|
|
330
|
+
const value = expandedPattern[key];
|
|
331
|
+
if (parsed.remainingPath) {
|
|
332
|
+
const resolvedValue = getValues(
|
|
333
|
+
{ __v: value }, options.from,
|
|
334
|
+
{ withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
|
|
335
|
+
);
|
|
336
|
+
assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
|
|
337
|
+
} else {
|
|
338
|
+
const resolvedValue = getValues(
|
|
339
|
+
typeof value === 'object' && value !== null ? value : { __v: value },
|
|
340
|
+
options.from,
|
|
341
|
+
{ withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
|
|
342
|
+
);
|
|
343
|
+
if (!('__v' in resolvedValue)) {
|
|
344
|
+
assignGingerly(el, resolvedValue, options);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Synchronous assignFrom — resolves values, assigns to target, all without awaiting.
|
|
352
|
+
*
|
|
353
|
+
* Handler commands (` =>`), beVigilant, and enhance are fire-and-forget (async, non-blocking).
|
|
354
|
+
* For awaitable handler execution, use assignFromAsync.
|
|
355
|
+
*
|
|
356
|
+
* @param target - Object to merge resolved values into
|
|
357
|
+
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
358
|
+
* @param options - Options including `from` (source object)
|
|
359
|
+
* @param permissions - Optional security permissions
|
|
360
|
+
* @returns The target object after merging
|
|
361
|
+
*/
|
|
362
|
+
export function assignFrom(
|
|
363
|
+
target: any,
|
|
364
|
+
pattern: Record<string, any>,
|
|
365
|
+
options: AssignFromOptions,
|
|
366
|
+
permissions?: AssignPermissions
|
|
367
|
+
): any {
|
|
368
|
+
// Expand looped substitution variables
|
|
369
|
+
const expandedPattern = expandSubstitutions(pattern, options);
|
|
370
|
+
|
|
371
|
+
// Categorize keys
|
|
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
|
+
}
|
|
391
|
+
|
|
392
|
+
// Process normal keys via getValues (sync) + assignGingerly
|
|
272
393
|
if (Object.keys(normalPattern).length > 0) {
|
|
273
|
-
|
|
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
|
+
|
|
421
|
+
const resolved = getValues(normalPattern, options.from, {
|
|
274
422
|
withMethods: options.withMethods,
|
|
275
423
|
aka: options.aka,
|
|
276
424
|
protocols: options.protocols
|
|
277
425
|
});
|
|
278
426
|
|
|
279
|
-
// Recursively handle "..." spread keys at all nesting levels
|
|
280
427
|
handleSpreads(resolved);
|
|
281
|
-
|
|
282
428
|
assignGingerly(target, resolved, options);
|
|
283
429
|
}
|
|
284
430
|
|
|
285
|
-
// Process #[x] normal keys
|
|
286
|
-
if (idRefNormalKeys.length > 0
|
|
287
|
-
|
|
288
|
-
for (const key of idRefNormalKeys) {
|
|
289
|
-
const parsed = parseIdRef(key);
|
|
290
|
-
if (!parsed) continue;
|
|
291
|
-
|
|
292
|
-
const el = resolveIdVariable(parsed.varName, target, options.withIds);
|
|
293
|
-
if (!el) continue;
|
|
294
|
-
|
|
295
|
-
const value = expandedPattern[key];
|
|
296
|
-
if (parsed.remainingPath) {
|
|
297
|
-
// Resolve the RHS value
|
|
298
|
-
const resolvedValue = await resolveValues(
|
|
299
|
-
{ __v: value }, options.from,
|
|
300
|
-
{ withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
|
|
301
|
-
);
|
|
302
|
-
// Apply remaining path on the resolved element
|
|
303
|
-
assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
|
|
304
|
-
} else {
|
|
305
|
-
// No remaining path — resolve and assign directly to the element
|
|
306
|
-
const resolvedValue = await resolveValues(
|
|
307
|
-
typeof value === 'object' && value !== null ? value : { __v: value },
|
|
308
|
-
options.from,
|
|
309
|
-
{ withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
|
|
310
|
-
);
|
|
311
|
-
if ('__v' in resolvedValue) {
|
|
312
|
-
// Single value — can't assign to element root without a path
|
|
313
|
-
} else {
|
|
314
|
-
assignGingerly(el, resolvedValue, options);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
431
|
+
// Process #[x] normal keys (sync)
|
|
432
|
+
if (idRefNormalKeys.length > 0) {
|
|
433
|
+
processIdRefNormalKeys(idRefNormalKeys, expandedPattern, target, options);
|
|
318
434
|
}
|
|
319
435
|
|
|
320
|
-
// Process handler commands
|
|
436
|
+
// Process handler commands — fire-and-forget (async)
|
|
321
437
|
if (handlerKeys.length > 0) {
|
|
322
|
-
|
|
323
|
-
|
|
438
|
+
import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
|
|
439
|
+
processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
|
|
440
|
+
});
|
|
324
441
|
}
|
|
325
442
|
|
|
326
|
-
// Process #[x] handler keys —
|
|
327
|
-
if (idRefHandlerKeys.length > 0 && options.withIds) {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const syntheticKey = parsed.remainingPath
|
|
341
|
-
? `${parsed.remainingPath} =>`
|
|
342
|
-
: ' =>';
|
|
343
|
-
|
|
344
|
-
const syntheticPattern: Record<string, any> = {
|
|
345
|
-
[syntheticKey]: expandedPattern[key]
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
await processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
|
|
349
|
-
}
|
|
443
|
+
// Process #[x] handler keys — fire-and-forget (async)
|
|
444
|
+
if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
|
|
445
|
+
const ids = getEffectiveIds(options)!;
|
|
446
|
+
import('./processHandlerCommands.js').then(({ processHandlerCommands }) => {
|
|
447
|
+
for (const key of idRefHandlerKeys) {
|
|
448
|
+
const parsed = parseIdRef(key);
|
|
449
|
+
if (!parsed) continue;
|
|
450
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
451
|
+
if (!el) continue;
|
|
452
|
+
const syntheticKey = parsed.remainingPath ? `${parsed.remainingPath} =>` : ' =>';
|
|
453
|
+
const syntheticPattern = { [syntheticKey]: expandedPattern[key] };
|
|
454
|
+
processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
350
457
|
}
|
|
351
458
|
|
|
352
|
-
// Process inferred assignments
|
|
353
|
-
if (options.
|
|
354
|
-
|
|
355
|
-
await processInferredAssignments(target, options.from, options.inferredAssignments);
|
|
459
|
+
// Process inferred assignments (sync)
|
|
460
|
+
if (options.infer) {
|
|
461
|
+
processInferredAssignments(target, options.from, options.infer);
|
|
356
462
|
|
|
357
|
-
//
|
|
358
|
-
if (options.
|
|
463
|
+
// beVigilant — fire-and-forget (async)
|
|
464
|
+
if (options.infer.beVigilant) {
|
|
359
465
|
if (!options.signal) {
|
|
360
|
-
throw new Error('assignFrom:
|
|
466
|
+
throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
|
|
361
467
|
}
|
|
362
|
-
|
|
363
|
-
|
|
468
|
+
import('./beVigilant.js').then(({ setupVigilantObserver }) => {
|
|
469
|
+
setupVigilantObserver(target, options.from, options.infer!, options.signal!);
|
|
470
|
+
});
|
|
364
471
|
}
|
|
365
472
|
}
|
|
366
473
|
|
|
367
|
-
// Process bulk enhancements —
|
|
474
|
+
// Process bulk enhancements — fire-and-forget (async)
|
|
368
475
|
if (options.enhance && options.enhance.length > 0) {
|
|
369
|
-
|
|
370
|
-
|
|
476
|
+
import('./enhanceAll.js').then(({ enhanceAll }) => {
|
|
477
|
+
enhanceAll(target, options.enhance!, permissions);
|
|
478
|
+
});
|
|
371
479
|
}
|
|
372
480
|
|
|
373
481
|
return target;
|
|
374
482
|
}
|
|
375
483
|
|
|
376
|
-
|
|
377
|
-
* Recursively walk an object and handle "..." spread keys.
|
|
378
|
-
* When a "..." key is found, its value (which should be an object after protocol resolution)
|
|
379
|
-
* is spread into the parent, replacing the "..." entry.
|
|
380
|
-
*/
|
|
381
|
-
function handleSpreads(obj: Record<string, any>): Record<string, any> {
|
|
382
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
383
|
-
if (key !== '...' && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
384
|
-
const proto = Object.getPrototypeOf(value);
|
|
385
|
-
if (proto === Object.prototype || proto === null) {
|
|
386
|
-
obj[key] = handleSpreads(value);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if ('...' in obj) {
|
|
391
|
-
const spreadValue = obj['...'];
|
|
392
|
-
delete obj['...'];
|
|
393
|
-
if (spreadValue && typeof spreadValue === 'object') {
|
|
394
|
-
Object.assign(obj, spreadValue);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return obj;
|
|
398
|
-
}
|
|
484
|
+
export default assignFrom;
|