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/getValues.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* getValues.ts — Synchronous value resolution for path strings.
|
|
3
|
+
*
|
|
4
|
+
* The synchronous counterpart to resolveValues. Resolves `?.`-prefixed path
|
|
5
|
+
* strings against a source object, with support for withMethods, aka aliases,
|
|
6
|
+
* synchronous protocols, arrays, and nested plain objects.
|
|
7
|
+
*
|
|
8
|
+
* For async protocol handlers, use resolveValues instead.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { getValues, getValue } from 'assign-gingerly/getValues.js';
|
|
12
|
+
*
|
|
13
|
+
* const result = getValues({
|
|
14
|
+
* name: '?.user?.name',
|
|
15
|
+
* greeting: '?.messages?.hello',
|
|
16
|
+
* count: 42
|
|
17
|
+
* }, source, { withMethods: ['querySelector'], aka: { q: 'querySelector' } });
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Apply alias substitutions to a path string.
|
|
21
|
+
* Replaces complete tokens between `?.` delimiters with their aliased values.
|
|
22
|
+
*/
|
|
23
|
+
function applyAliases(path, aliasMap) {
|
|
24
|
+
if (aliasMap.size === 0)
|
|
25
|
+
return path;
|
|
26
|
+
const parts = path.split('?.');
|
|
27
|
+
const substituted = parts.map(part => aliasMap.get(part) ?? part);
|
|
28
|
+
return substituted.join('?.');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Path cache for parsed path strings.
|
|
32
|
+
* Avoids re-splitting the same path on repeated calls.
|
|
33
|
+
*/
|
|
34
|
+
const pathCache = new Map();
|
|
35
|
+
/**
|
|
36
|
+
* Parse a `?.`-delimited path string into segments, with caching.
|
|
37
|
+
*/
|
|
38
|
+
function parseCachedPath(path) {
|
|
39
|
+
let parts = pathCache.get(path);
|
|
40
|
+
if (!parts) {
|
|
41
|
+
parts = path.split('?.').filter(p => p.length > 0);
|
|
42
|
+
pathCache.set(path, parts);
|
|
43
|
+
}
|
|
44
|
+
return parts;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Navigate a path against a source object, optionally calling methods.
|
|
48
|
+
* Returns the resolved value at the end of the path.
|
|
49
|
+
*/
|
|
50
|
+
function navigatePath(source, parts, withMethods) {
|
|
51
|
+
let current = source;
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < parts.length) {
|
|
54
|
+
if (current == null)
|
|
55
|
+
return current;
|
|
56
|
+
const part = parts[i];
|
|
57
|
+
if (withMethods && withMethods.has(part)) {
|
|
58
|
+
const method = current[part];
|
|
59
|
+
if (typeof method === 'function') {
|
|
60
|
+
const nextPart = parts[i + 1];
|
|
61
|
+
if (nextPart !== undefined && !(withMethods.has(nextPart))) {
|
|
62
|
+
current = method.call(current, nextPart);
|
|
63
|
+
i += 2;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
current = method.call(current);
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
current = current[part];
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
current = current[part];
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Checks if a string value looks like a protocol reference.
|
|
84
|
+
*/
|
|
85
|
+
function hasProtocol(value) {
|
|
86
|
+
return value.includes('://');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a protocol-prefixed value synchronously.
|
|
90
|
+
*/
|
|
91
|
+
function getProtocolValue(value, protocols, options) {
|
|
92
|
+
const protoEnd = value.indexOf('://');
|
|
93
|
+
const protocol = value.substring(0, protoEnd);
|
|
94
|
+
const handler = protocols[protocol];
|
|
95
|
+
if (!handler)
|
|
96
|
+
return value; // not a recognized protocol
|
|
97
|
+
const rest = value.substring(protoEnd + 3);
|
|
98
|
+
const pathStart = rest.indexOf('?.');
|
|
99
|
+
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
100
|
+
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
101
|
+
const resolved = handler(key);
|
|
102
|
+
if (path) {
|
|
103
|
+
return getValue(path, resolved, options);
|
|
104
|
+
}
|
|
105
|
+
return resolved;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolve path strings and protocols within an array (synchronous).
|
|
109
|
+
* Recurses into nested arrays and plain objects.
|
|
110
|
+
*/
|
|
111
|
+
function getArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
112
|
+
const result = [];
|
|
113
|
+
for (const item of arr) {
|
|
114
|
+
if (typeof item === 'string' && item.startsWith('?.')) {
|
|
115
|
+
const aliased = applyAliases(item, aliasMap);
|
|
116
|
+
const parts = parseCachedPath(aliased);
|
|
117
|
+
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
|
|
118
|
+
}
|
|
119
|
+
else if (typeof item === 'string' && protocols && hasProtocol(item)) {
|
|
120
|
+
result.push(getProtocolValue(item, protocols, options));
|
|
121
|
+
}
|
|
122
|
+
else if (Array.isArray(item)) {
|
|
123
|
+
result.push(getArray(item, source, aliasMap, withMethods, protocols, options));
|
|
124
|
+
}
|
|
125
|
+
else if (item && typeof item === 'object') {
|
|
126
|
+
const proto = Object.getPrototypeOf(item);
|
|
127
|
+
if (proto === Object.prototype || proto === null) {
|
|
128
|
+
result.push(getValues(item, source, options));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
result.push(item);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
result.push(item);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Synchronously resolve RHS path strings in a pattern object against a source object.
|
|
142
|
+
*
|
|
143
|
+
* Any value that is a string starting with `?.` is treated as a path
|
|
144
|
+
* and resolved against the source object. Non-string values and strings
|
|
145
|
+
* not starting with `?.` pass through unchanged.
|
|
146
|
+
*
|
|
147
|
+
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
148
|
+
* @param source - Object to resolve paths against
|
|
149
|
+
* @param options - Optional withMethods, aka, and synchronous protocols
|
|
150
|
+
* @returns New object with path strings replaced by resolved values
|
|
151
|
+
*/
|
|
152
|
+
export function getValues(pattern, source, options) {
|
|
153
|
+
// Build alias map
|
|
154
|
+
const aliasMap = new Map();
|
|
155
|
+
if (options?.aka) {
|
|
156
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
157
|
+
aliasMap.set(alias, target);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Build methods set
|
|
161
|
+
const withMethods = options?.withMethods
|
|
162
|
+
? options.withMethods instanceof Set
|
|
163
|
+
? options.withMethods
|
|
164
|
+
: new Set(options.withMethods)
|
|
165
|
+
: undefined;
|
|
166
|
+
const protocols = options?.protocols;
|
|
167
|
+
const result = {};
|
|
168
|
+
for (const [key, value] of Object.entries(pattern)) {
|
|
169
|
+
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
170
|
+
const aliased = applyAliases(value, aliasMap);
|
|
171
|
+
const parts = parseCachedPath(aliased);
|
|
172
|
+
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
173
|
+
}
|
|
174
|
+
else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
175
|
+
result[key] = getProtocolValue(value, protocols, options);
|
|
176
|
+
}
|
|
177
|
+
else if (Array.isArray(value)) {
|
|
178
|
+
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options);
|
|
179
|
+
}
|
|
180
|
+
else if (typeof value === 'object' && value !== null) {
|
|
181
|
+
const proto = Object.getPrototypeOf(value);
|
|
182
|
+
if (proto === Object.prototype || proto === null) {
|
|
183
|
+
result[key] = getValues(value, source, options);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
result[key] = value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
result[key] = value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Synchronously resolve a single `?.`-delimited path string against a source object.
|
|
197
|
+
*
|
|
198
|
+
* @param path - A `?.`-delimited path string (e.g., '?.user?.name')
|
|
199
|
+
* @param source - Object to resolve the path against
|
|
200
|
+
* @param options - Optional withMethods and aka
|
|
201
|
+
* @returns The resolved value, or undefined if any segment is nullish
|
|
202
|
+
*/
|
|
203
|
+
export function getValue(path, source, options) {
|
|
204
|
+
if (!path.startsWith('?.'))
|
|
205
|
+
return path;
|
|
206
|
+
let aliased = path;
|
|
207
|
+
if (options?.aka) {
|
|
208
|
+
const aliasMap = new Map();
|
|
209
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
210
|
+
aliasMap.set(alias, target);
|
|
211
|
+
}
|
|
212
|
+
aliased = applyAliases(path, aliasMap);
|
|
213
|
+
}
|
|
214
|
+
const parts = parseCachedPath(aliased);
|
|
215
|
+
if (parts.length === 0)
|
|
216
|
+
return source;
|
|
217
|
+
const withMethods = options?.withMethods
|
|
218
|
+
? options.withMethods instanceof Set
|
|
219
|
+
? options.withMethods
|
|
220
|
+
: new Set(options.withMethods)
|
|
221
|
+
: undefined;
|
|
222
|
+
return navigatePath(source, parts, withMethods);
|
|
223
|
+
}
|
package/getValues.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* getValues.ts — Synchronous value resolution for path strings.
|
|
3
|
+
*
|
|
4
|
+
* The synchronous counterpart to resolveValues. Resolves `?.`-prefixed path
|
|
5
|
+
* strings against a source object, with support for withMethods, aka aliases,
|
|
6
|
+
* synchronous protocols, arrays, and nested plain objects.
|
|
7
|
+
*
|
|
8
|
+
* For async protocol handlers, use resolveValues instead.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { getValues, getValue } from 'assign-gingerly/getValues.js';
|
|
12
|
+
*
|
|
13
|
+
* const result = getValues({
|
|
14
|
+
* name: '?.user?.name',
|
|
15
|
+
* greeting: '?.messages?.hello',
|
|
16
|
+
* count: 42
|
|
17
|
+
* }, source, { withMethods: ['querySelector'], aka: { q: 'querySelector' } });
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { GetValuesOptions } from './types/assign-gingerly/types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Apply alias substitutions to a path string.
|
|
24
|
+
* Replaces complete tokens between `?.` delimiters with their aliased values.
|
|
25
|
+
*/
|
|
26
|
+
function applyAliases(path: string, aliasMap: Map<string, string>): string {
|
|
27
|
+
if (aliasMap.size === 0) return path;
|
|
28
|
+
const parts = path.split('?.');
|
|
29
|
+
const substituted = parts.map(part => aliasMap.get(part) ?? part);
|
|
30
|
+
return substituted.join('?.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Path cache for parsed path strings.
|
|
35
|
+
* Avoids re-splitting the same path on repeated calls.
|
|
36
|
+
*/
|
|
37
|
+
const pathCache = new Map<string, string[]>();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a `?.`-delimited path string into segments, with caching.
|
|
41
|
+
*/
|
|
42
|
+
function parseCachedPath(path: string): string[] {
|
|
43
|
+
let parts = pathCache.get(path);
|
|
44
|
+
if (!parts) {
|
|
45
|
+
parts = path.split('?.').filter(p => p.length > 0);
|
|
46
|
+
pathCache.set(path, parts);
|
|
47
|
+
}
|
|
48
|
+
return parts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Navigate a path against a source object, optionally calling methods.
|
|
53
|
+
* Returns the resolved value at the end of the path.
|
|
54
|
+
*/
|
|
55
|
+
function navigatePath(
|
|
56
|
+
source: any,
|
|
57
|
+
parts: string[],
|
|
58
|
+
withMethods: Set<string> | undefined
|
|
59
|
+
): any {
|
|
60
|
+
let current = source;
|
|
61
|
+
let i = 0;
|
|
62
|
+
|
|
63
|
+
while (i < parts.length) {
|
|
64
|
+
if (current == null) return current;
|
|
65
|
+
|
|
66
|
+
const part = parts[i];
|
|
67
|
+
|
|
68
|
+
if (withMethods && withMethods.has(part)) {
|
|
69
|
+
const method = current[part];
|
|
70
|
+
if (typeof method === 'function') {
|
|
71
|
+
const nextPart = parts[i + 1];
|
|
72
|
+
if (nextPart !== undefined && !(withMethods.has(nextPart))) {
|
|
73
|
+
current = method.call(current, nextPart);
|
|
74
|
+
i += 2;
|
|
75
|
+
} else {
|
|
76
|
+
current = method.call(current);
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
current = current[part];
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
current = current[part];
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return current;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Checks if a string value looks like a protocol reference.
|
|
94
|
+
*/
|
|
95
|
+
function hasProtocol(value: string): boolean {
|
|
96
|
+
return value.includes('://');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a protocol-prefixed value synchronously.
|
|
101
|
+
*/
|
|
102
|
+
function getProtocolValue(
|
|
103
|
+
value: string,
|
|
104
|
+
protocols: Record<string, (key: string) => any>,
|
|
105
|
+
options?: GetValuesOptions
|
|
106
|
+
): any {
|
|
107
|
+
const protoEnd = value.indexOf('://');
|
|
108
|
+
const protocol = value.substring(0, protoEnd);
|
|
109
|
+
|
|
110
|
+
const handler = protocols[protocol];
|
|
111
|
+
if (!handler) return value; // not a recognized protocol
|
|
112
|
+
|
|
113
|
+
const rest = value.substring(protoEnd + 3);
|
|
114
|
+
|
|
115
|
+
const pathStart = rest.indexOf('?.');
|
|
116
|
+
const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
|
|
117
|
+
const path = pathStart === -1 ? null : rest.substring(pathStart);
|
|
118
|
+
|
|
119
|
+
const resolved = handler(key);
|
|
120
|
+
|
|
121
|
+
if (path) {
|
|
122
|
+
return getValue(path, resolved, options);
|
|
123
|
+
}
|
|
124
|
+
return resolved;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve path strings and protocols within an array (synchronous).
|
|
129
|
+
* Recurses into nested arrays and plain objects.
|
|
130
|
+
*/
|
|
131
|
+
function getArray(
|
|
132
|
+
arr: any[],
|
|
133
|
+
source: any,
|
|
134
|
+
aliasMap: Map<string, string>,
|
|
135
|
+
withMethods: Set<string> | undefined,
|
|
136
|
+
protocols: Record<string, (key: string) => any> | undefined,
|
|
137
|
+
options?: GetValuesOptions
|
|
138
|
+
): any[] {
|
|
139
|
+
const result: any[] = [];
|
|
140
|
+
for (const item of arr) {
|
|
141
|
+
if (typeof item === 'string' && item.startsWith('?.')) {
|
|
142
|
+
const aliased = applyAliases(item, aliasMap);
|
|
143
|
+
const parts = parseCachedPath(aliased);
|
|
144
|
+
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
|
|
145
|
+
} else if (typeof item === 'string' && protocols && hasProtocol(item)) {
|
|
146
|
+
result.push(getProtocolValue(item, protocols, options));
|
|
147
|
+
} else if (Array.isArray(item)) {
|
|
148
|
+
result.push(getArray(item, source, aliasMap, withMethods, protocols, options));
|
|
149
|
+
} else if (item && typeof item === 'object') {
|
|
150
|
+
const proto = Object.getPrototypeOf(item);
|
|
151
|
+
if (proto === Object.prototype || proto === null) {
|
|
152
|
+
result.push(getValues(item, source, options));
|
|
153
|
+
} else {
|
|
154
|
+
result.push(item);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
result.push(item);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Synchronously resolve RHS path strings in a pattern object against a source object.
|
|
165
|
+
*
|
|
166
|
+
* Any value that is a string starting with `?.` is treated as a path
|
|
167
|
+
* and resolved against the source object. Non-string values and strings
|
|
168
|
+
* not starting with `?.` pass through unchanged.
|
|
169
|
+
*
|
|
170
|
+
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
171
|
+
* @param source - Object to resolve paths against
|
|
172
|
+
* @param options - Optional withMethods, aka, and synchronous protocols
|
|
173
|
+
* @returns New object with path strings replaced by resolved values
|
|
174
|
+
*/
|
|
175
|
+
export function getValues(
|
|
176
|
+
pattern: Record<string, any>,
|
|
177
|
+
source: any,
|
|
178
|
+
options?: GetValuesOptions
|
|
179
|
+
): Record<string, any> {
|
|
180
|
+
// Build alias map
|
|
181
|
+
const aliasMap = new Map<string, string>();
|
|
182
|
+
if (options?.aka) {
|
|
183
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
184
|
+
aliasMap.set(alias, target);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Build methods set
|
|
189
|
+
const withMethods = options?.withMethods
|
|
190
|
+
? options.withMethods instanceof Set
|
|
191
|
+
? options.withMethods
|
|
192
|
+
: new Set(options.withMethods)
|
|
193
|
+
: undefined;
|
|
194
|
+
|
|
195
|
+
const protocols = options?.protocols;
|
|
196
|
+
|
|
197
|
+
const result: Record<string, any> = {};
|
|
198
|
+
for (const [key, value] of Object.entries(pattern)) {
|
|
199
|
+
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
200
|
+
const aliased = applyAliases(value, aliasMap);
|
|
201
|
+
const parts = parseCachedPath(aliased);
|
|
202
|
+
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
203
|
+
} else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
204
|
+
result[key] = getProtocolValue(value, protocols, options);
|
|
205
|
+
} else if (Array.isArray(value)) {
|
|
206
|
+
result[key] = getArray(value, source, aliasMap, withMethods, protocols, options);
|
|
207
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
208
|
+
const proto = Object.getPrototypeOf(value);
|
|
209
|
+
if (proto === Object.prototype || proto === null) {
|
|
210
|
+
result[key] = getValues(value, source, options);
|
|
211
|
+
} else {
|
|
212
|
+
result[key] = value;
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
result[key] = value;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Synchronously resolve a single `?.`-delimited path string against a source object.
|
|
223
|
+
*
|
|
224
|
+
* @param path - A `?.`-delimited path string (e.g., '?.user?.name')
|
|
225
|
+
* @param source - Object to resolve the path against
|
|
226
|
+
* @param options - Optional withMethods and aka
|
|
227
|
+
* @returns The resolved value, or undefined if any segment is nullish
|
|
228
|
+
*/
|
|
229
|
+
export function getValue(
|
|
230
|
+
path: string,
|
|
231
|
+
source: any,
|
|
232
|
+
options?: GetValuesOptions
|
|
233
|
+
): any {
|
|
234
|
+
if (!path.startsWith('?.')) return path;
|
|
235
|
+
|
|
236
|
+
let aliased = path;
|
|
237
|
+
if (options?.aka) {
|
|
238
|
+
const aliasMap = new Map<string, string>();
|
|
239
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
240
|
+
aliasMap.set(alias, target);
|
|
241
|
+
}
|
|
242
|
+
aliased = applyAliases(path, aliasMap);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const parts = parseCachedPath(aliased);
|
|
246
|
+
if (parts.length === 0) return source;
|
|
247
|
+
|
|
248
|
+
const withMethods = options?.withMethods
|
|
249
|
+
? options.withMethods instanceof Set
|
|
250
|
+
? options.withMethods
|
|
251
|
+
: new Set(options.withMethods)
|
|
252
|
+
: undefined;
|
|
253
|
+
|
|
254
|
+
return navigatePath(source, parts, withMethods);
|
|
255
|
+
}
|
package/handlers/join.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* // If middleName is undefined, the sub-array [', ', undefined] is dropped entirely.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import type { AssignFromHandler } from '../
|
|
34
|
+
import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Process nested arrays with all-or-nothing null semantics.
|
package/handlers/lazyLoad.js
CHANGED
|
@@ -37,14 +37,17 @@ function getMarkerName(templateEl) {
|
|
|
37
37
|
*
|
|
38
38
|
* Exported so it can be subclassed for custom behavior.
|
|
39
39
|
*/
|
|
40
|
+
import { assignFrom } from '../assignFrom.js';
|
|
40
41
|
export class LazyLoadHandler {
|
|
41
42
|
config;
|
|
43
|
+
_options;
|
|
42
44
|
static #markerCounter = 0;
|
|
43
45
|
constructor(config) {
|
|
44
46
|
this.config = config;
|
|
45
47
|
}
|
|
46
|
-
async assign(lhsTarget, resolvedParams) {
|
|
47
|
-
|
|
48
|
+
async assign(lhsTarget, resolvedParams, options) {
|
|
49
|
+
this._options = options;
|
|
50
|
+
const { if: condition, instantiate, method = 'appendChild', forget = false, transitional = false, hideClass = DEFAULT_HIDE_CLASS, hideCss, markerName, toggleInert = false, toggleDisabled = false, placeholder } = resolvedParams;
|
|
48
51
|
if (!(lhsTarget instanceof Element)) {
|
|
49
52
|
throw new Error('builtIns.lazyLoad: lhsTarget must be a DOM Element');
|
|
50
53
|
}
|
|
@@ -83,6 +86,16 @@ export class LazyLoadHandler {
|
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
else {
|
|
89
|
+
// Remove placeholder content if specified
|
|
90
|
+
if (placeholder) {
|
|
91
|
+
const [phStart, phEnd] = findMarkers(lhsTarget, placeholder);
|
|
92
|
+
if (phStart && phEnd) {
|
|
93
|
+
const phNodes = getNodesBetweenMarkers(phStart, phEnd);
|
|
94
|
+
for (const node of phNodes) {
|
|
95
|
+
node.parentNode?.removeChild(node);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
86
99
|
if (method === 'after') {
|
|
87
100
|
[startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
|
|
88
101
|
} else {
|
|
@@ -164,6 +177,7 @@ export class LazyLoadHandler {
|
|
|
164
177
|
throw new Error(`builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`);
|
|
165
178
|
}
|
|
166
179
|
const nodes = Array.from(content.childNodes);
|
|
180
|
+
this.applyAssign(nodes, resolvedParams);
|
|
167
181
|
endMarker.parentNode.insertBefore(content, endMarker);
|
|
168
182
|
return nodes;
|
|
169
183
|
}
|
|
@@ -179,6 +193,7 @@ export class LazyLoadHandler {
|
|
|
179
193
|
throw new Error(`builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`);
|
|
180
194
|
}
|
|
181
195
|
const nodes = Array.from(content.childNodes);
|
|
196
|
+
this.applyAssign(nodes, resolvedParams);
|
|
182
197
|
endMarker.parentNode.insertBefore(content, endMarker);
|
|
183
198
|
await this.onCloneInserted(nodes, lhsTarget, resolvedParams);
|
|
184
199
|
if (resolvedParams.onInstantiated && typeof resolvedParams.onInstantiated === 'function') {
|
|
@@ -192,6 +207,22 @@ export class LazyLoadHandler {
|
|
|
192
207
|
}
|
|
193
208
|
return nodes;
|
|
194
209
|
}
|
|
210
|
+
applyAssign(nodes, resolvedParams) {
|
|
211
|
+
const assign = resolvedParams.assign;
|
|
212
|
+
if (!assign) return;
|
|
213
|
+
const elements = nodes.filter(n => n instanceof Element);
|
|
214
|
+
if (elements.length === 0) return;
|
|
215
|
+
const from = this._options?.from ?? {};
|
|
216
|
+
if (assign.configs && Array.isArray(assign.configs)) {
|
|
217
|
+
const len = Math.min(elements.length, assign.configs.length);
|
|
218
|
+
for (let j = 0; j < len; j++) {
|
|
219
|
+
const cfg = assign.configs[j];
|
|
220
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from, ...cfg.withOptions });
|
|
221
|
+
}
|
|
222
|
+
} else if (assign.assignToFragment) {
|
|
223
|
+
assignFrom(elements[0], assign.assignToFragment, { from, ...assign.withOptions });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
195
226
|
async onCloneInserted(nodes, lhsTarget, resolvedParams) {
|
|
196
227
|
// No-op by default. Subclasses override.
|
|
197
228
|
}
|
package/handlers/lazyLoad.ts
CHANGED
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
* }, { withMethods: ['querySelector'], from: myVM });
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import type { AssignFromHandler } from '../
|
|
22
|
+
import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
23
23
|
import type { LazyLoadResolvedParams, LazyLoadInstantiatedContext } from '../types/assign-gingerly/types.js';
|
|
24
24
|
import { withTransition, ensureHideStyle, DEFAULT_HIDE_CLASS } from '../transitionHelper.js';
|
|
25
25
|
import { findMarkers, createMarkers, getNodesBetweenMarkers, findMarkersSibling, createMarkersSibling, MARKER_START_PREFIX, MARKER_END } from '../markerUtils.js';
|
|
26
|
+
import { assignFrom } from '../assignFrom.js';
|
|
26
27
|
|
|
27
28
|
export type { LazyLoadResolvedParams, LazyLoadInstantiatedContext };
|
|
28
29
|
|
|
@@ -46,13 +47,15 @@ function getMarkerName(templateEl: any): string {
|
|
|
46
47
|
*/
|
|
47
48
|
export class LazyLoadHandler implements AssignFromHandler {
|
|
48
49
|
config: any;
|
|
50
|
+
_options: any;
|
|
49
51
|
static #markerCounter = 0;
|
|
50
52
|
|
|
51
53
|
constructor(config: any) {
|
|
52
54
|
this.config = config;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
async assign(lhsTarget: any, resolvedParams: LazyLoadResolvedParams): Promise<void> {
|
|
57
|
+
async assign(lhsTarget: any, resolvedParams: LazyLoadResolvedParams, options?: any): Promise<void> {
|
|
58
|
+
this._options = options; // Store for applyAssign access
|
|
56
59
|
const {
|
|
57
60
|
if: condition,
|
|
58
61
|
instantiate,
|
|
@@ -64,6 +67,7 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
64
67
|
markerName,
|
|
65
68
|
toggleInert = false,
|
|
66
69
|
toggleDisabled = false,
|
|
70
|
+
placeholder,
|
|
67
71
|
} = resolvedParams;
|
|
68
72
|
|
|
69
73
|
if (!(lhsTarget instanceof Element)) {
|
|
@@ -112,6 +116,16 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
112
116
|
}
|
|
113
117
|
} else {
|
|
114
118
|
// No markers — first time. Create markers and clone template.
|
|
119
|
+
// Remove placeholder content if specified
|
|
120
|
+
if (placeholder) {
|
|
121
|
+
const [phStart, phEnd] = findMarkers(lhsTarget, placeholder);
|
|
122
|
+
if (phStart && phEnd) {
|
|
123
|
+
const phNodes = getNodesBetweenMarkers(phStart, phEnd);
|
|
124
|
+
for (const node of phNodes) {
|
|
125
|
+
node.parentNode?.removeChild(node);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
115
129
|
if (method === 'after') {
|
|
116
130
|
[startMarker, endMarker] = createMarkersSibling(lhsTarget, name);
|
|
117
131
|
} else {
|
|
@@ -229,6 +243,10 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
const nodes = Array.from(content.childNodes);
|
|
246
|
+
|
|
247
|
+
// Apply assignments to cloned content before insertion
|
|
248
|
+
this.applyAssign(nodes, resolvedParams);
|
|
249
|
+
|
|
232
250
|
endMarker.parentNode!.insertBefore(content, endMarker);
|
|
233
251
|
return nodes;
|
|
234
252
|
}
|
|
@@ -259,6 +277,9 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
259
277
|
// Capture nodes before insertion (childNodes empties after insertBefore)
|
|
260
278
|
const nodes = Array.from(content.childNodes);
|
|
261
279
|
|
|
280
|
+
// Apply assignments to cloned content before insertion
|
|
281
|
+
this.applyAssign(nodes, resolvedParams);
|
|
282
|
+
|
|
262
283
|
// Insert before endMarker
|
|
263
284
|
endMarker.parentNode!.insertBefore(content, endMarker);
|
|
264
285
|
|
|
@@ -279,6 +300,30 @@ export class LazyLoadHandler implements AssignFromHandler {
|
|
|
279
300
|
return nodes;
|
|
280
301
|
}
|
|
281
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Apply assignFrom to cloned nodes based on the `assign` config.
|
|
305
|
+
* Supports single-element and multi-element (configs array) templates.
|
|
306
|
+
*/
|
|
307
|
+
protected applyAssign(nodes: Node[], resolvedParams: LazyLoadResolvedParams): void {
|
|
308
|
+
const assign = (resolvedParams as any).assign;
|
|
309
|
+
if (!assign) return;
|
|
310
|
+
|
|
311
|
+
const elements = nodes.filter(n => n instanceof Element) as Element[];
|
|
312
|
+
if (elements.length === 0) return;
|
|
313
|
+
|
|
314
|
+
const from = this._options?.from ?? {};
|
|
315
|
+
|
|
316
|
+
if (assign.configs && Array.isArray(assign.configs)) {
|
|
317
|
+
const len = Math.min(elements.length, assign.configs.length);
|
|
318
|
+
for (let j = 0; j < len; j++) {
|
|
319
|
+
const cfg = assign.configs[j];
|
|
320
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from, ...cfg.withOptions });
|
|
321
|
+
}
|
|
322
|
+
} else if (assign.assignToFragment) {
|
|
323
|
+
assignFrom(elements[0], assign.assignToFragment, { from, ...assign.withOptions });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
282
327
|
/**
|
|
283
328
|
* Hook called after template content is cloned and inserted.
|
|
284
329
|
* Override in subclasses for custom post-clone logic.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { LazyLoadHandler } from './lazyLoad.js';
|
|
24
|
-
import type { AssignFromHandler } from '../
|
|
24
|
+
import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
25
25
|
import type { LazyLoadSwitchResolvedParams } from '../types/assign-gingerly/types.js';
|
|
26
26
|
|
|
27
27
|
/**
|