assign-gingerly 0.0.34 → 0.0.36
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 +22 -0
- package/assignFrom.js +4 -1
- package/assignFrom.ts +4 -1
- package/assignTentatively.js +90 -48
- package/assignTentatively.ts +87 -50
- package/package.json +1 -1
- package/resolveValues.js +86 -18
- package/resolveValues.ts +113 -17
package/README.md
CHANGED
|
@@ -3303,6 +3303,28 @@ console.log(result);
|
|
|
3303
3303
|
|
|
3304
3304
|
-->
|
|
3305
3305
|
|
|
3306
|
+
## Resolving and Assigning with `assignFrom`
|
|
3307
|
+
|
|
3308
|
+
The `assignFrom` function combines RHS path resolution with `assignGingerly` in a single call. It resolves `?.`-prefixed RHS values against a source object, then assigns the results into a target. Use `?.` alone as a RHS value to reference the entire source object itself.
|
|
3309
|
+
|
|
3310
|
+
```TypeScript
|
|
3311
|
+
import { assignFrom } from 'assign-gingerly/assignFrom.js';
|
|
3312
|
+
|
|
3313
|
+
const viewModel = { username: 'Alice', clone: someDocumentFragment };
|
|
3314
|
+
|
|
3315
|
+
assignFrom(target, {
|
|
3316
|
+
'?.appendChild': '?..clone', // source.clone
|
|
3317
|
+
'?.clone?.q?..username?.textContent': '?.username', // source.username
|
|
3318
|
+
ref: '?.' // the source object itself
|
|
3319
|
+
}, {
|
|
3320
|
+
from: viewModel,
|
|
3321
|
+
withMethods: ['appendChild'],
|
|
3322
|
+
aka: { 'q': 'querySelector' }
|
|
3323
|
+
});
|
|
3324
|
+
```
|
|
3325
|
+
|
|
3326
|
+
For full documentation, see [docs/assignFrom.md](docs/assignFrom.md).
|
|
3327
|
+
|
|
3306
3328
|
## Itemscope Managers (Chrome 146+)
|
|
3307
3329
|
|
|
3308
3330
|
Itemscope Managers provide a way to manage DOM fragments and their associated data/view models for elements with the `itemscope` attribute. This feature enables frameworks and libraries to manage light children of web components, DOM fragments from looping constructs, and scenarios where custom element wrapping is not feasible.
|
package/assignFrom.js
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
import { resolveValues } from './resolveValues.js';
|
|
23
23
|
import assignGingerly from './assignGingerly.js';
|
|
24
24
|
export function assignFrom(target, pattern, options) {
|
|
25
|
-
const resolved = resolveValues(pattern, options.from
|
|
25
|
+
const resolved = resolveValues(pattern, options.from, {
|
|
26
|
+
withMethods: options.withMethods,
|
|
27
|
+
aka: options.aka
|
|
28
|
+
});
|
|
26
29
|
return assignGingerly(target, resolved, options);
|
|
27
30
|
}
|
package/assignFrom.ts
CHANGED
|
@@ -32,6 +32,9 @@ export function assignFrom(
|
|
|
32
32
|
pattern: Record<string, any>,
|
|
33
33
|
options: AssignFromOptions
|
|
34
34
|
): any {
|
|
35
|
-
const resolved = resolveValues(pattern, options.from
|
|
35
|
+
const resolved = resolveValues(pattern, options.from, {
|
|
36
|
+
withMethods: options.withMethods,
|
|
37
|
+
aka: options.aka
|
|
38
|
+
});
|
|
36
39
|
return assignGingerly(target, resolved, options);
|
|
37
40
|
}
|
package/assignTentatively.js
CHANGED
|
@@ -45,11 +45,13 @@ function parseDeleteCommand(key) {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Helper function to parse a path string with ?. notation
|
|
48
|
+
* Always splits on '?.' delimiter, preserving dots that are part of values
|
|
49
|
+
* (e.g., CSS class selectors like '.username')
|
|
50
|
+
* Paths must use ?. notation — plain dot notation is not supported.
|
|
48
51
|
*/
|
|
49
52
|
function parsePath(path) {
|
|
50
53
|
return path
|
|
51
|
-
.split('
|
|
52
|
-
.map(part => part.replace(/\?/g, ''))
|
|
54
|
+
.split('?.')
|
|
53
55
|
.filter(part => part.length > 0);
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
@@ -107,25 +109,39 @@ export function assignTentatively(target, source, options) {
|
|
|
107
109
|
if (isIncCommand(key)) {
|
|
108
110
|
const path = parseIncCommand(key);
|
|
109
111
|
if (path) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
112
|
+
if (isNestedPath(path)) {
|
|
113
|
+
const pathParts = parsePath(path);
|
|
114
|
+
const topLevelKey = pathParts[0];
|
|
115
|
+
// Track if we created a new top-level path (BEFORE calling ensureNestedPath)
|
|
116
|
+
if (!(topLevelKey in target)) {
|
|
117
|
+
trackedCreatedPaths.add(topLevelKey);
|
|
118
|
+
}
|
|
119
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
120
|
+
const parent = ensureNestedPath(target, pathParts);
|
|
121
|
+
// If property already exists, store original value for reversal
|
|
122
|
+
if (lastKey in parent) {
|
|
123
|
+
const fullPath = `?.${pathParts.join('?.')}`;
|
|
124
|
+
if (!(fullPath in reversal)) {
|
|
125
|
+
reversal[fullPath] = parent[lastKey];
|
|
126
|
+
}
|
|
127
|
+
parent[lastKey] += value;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Property doesn't exist, create it with the value
|
|
131
|
+
parent[lastKey] = value;
|
|
123
132
|
}
|
|
124
|
-
parent[lastKey] += value;
|
|
125
133
|
}
|
|
126
134
|
else {
|
|
127
|
-
//
|
|
128
|
-
|
|
135
|
+
// Plain key - direct operation on target
|
|
136
|
+
if (path in target) {
|
|
137
|
+
if (!(path in reversal)) {
|
|
138
|
+
reversal[path] = target[path];
|
|
139
|
+
}
|
|
140
|
+
target[path] += value;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
target[path] = value;
|
|
144
|
+
}
|
|
129
145
|
}
|
|
130
146
|
}
|
|
131
147
|
continue;
|
|
@@ -135,27 +151,37 @@ export function assignTentatively(target, source, options) {
|
|
|
135
151
|
const lhsPath = parseToggleCommand(key);
|
|
136
152
|
if (lhsPath) {
|
|
137
153
|
const rhsPath = value;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
154
|
+
let lhsParent;
|
|
155
|
+
let lhsLastKey;
|
|
156
|
+
let lhsPathParts;
|
|
157
|
+
if (isNestedPath(lhsPath)) {
|
|
158
|
+
lhsPathParts = parsePath(lhsPath);
|
|
159
|
+
const topLevelKey = lhsPathParts[0];
|
|
160
|
+
// Track if we created a new top-level path (BEFORE calling ensureNestedPath)
|
|
161
|
+
if (!(topLevelKey in target)) {
|
|
162
|
+
trackedCreatedPaths.add(topLevelKey);
|
|
163
|
+
}
|
|
164
|
+
lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
|
|
165
|
+
lhsParent = ensureNestedPath(target, lhsPathParts);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
lhsPathParts = [lhsPath];
|
|
169
|
+
lhsLastKey = lhsPath;
|
|
170
|
+
lhsParent = target;
|
|
143
171
|
}
|
|
144
|
-
const lastKey = pathParts[pathParts.length - 1];
|
|
145
|
-
const parent = ensureNestedPath(target, pathParts);
|
|
146
172
|
// Determine what to negate
|
|
147
173
|
let valueToNegate;
|
|
148
174
|
if (rhsPath === '.') {
|
|
149
175
|
// Self-reference
|
|
150
|
-
if (
|
|
151
|
-
valueToNegate =
|
|
176
|
+
if (lhsLastKey in lhsParent) {
|
|
177
|
+
valueToNegate = lhsParent[lhsLastKey];
|
|
152
178
|
}
|
|
153
179
|
else {
|
|
154
180
|
valueToNegate = undefined;
|
|
155
181
|
}
|
|
156
182
|
}
|
|
157
|
-
else {
|
|
158
|
-
// RHS path: navigate to get the value (don't create paths)
|
|
183
|
+
else if (isNestedPath(rhsPath)) {
|
|
184
|
+
// RHS nested path: navigate to get the value (don't create paths)
|
|
159
185
|
const rhsPathParts = parsePath(rhsPath);
|
|
160
186
|
let current = target;
|
|
161
187
|
let exists = true;
|
|
@@ -170,14 +196,18 @@ export function assignTentatively(target, source, options) {
|
|
|
170
196
|
}
|
|
171
197
|
valueToNegate = exists ? current : true;
|
|
172
198
|
}
|
|
199
|
+
else {
|
|
200
|
+
// Plain key RHS
|
|
201
|
+
valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
|
|
202
|
+
}
|
|
173
203
|
// Store original value for reversal if it exists
|
|
174
|
-
if (
|
|
175
|
-
const fullPath = `?.${
|
|
204
|
+
if (lhsLastKey in lhsParent) {
|
|
205
|
+
const fullPath = `?.${lhsPathParts.join('?.')}`;
|
|
176
206
|
if (!(fullPath in reversal)) {
|
|
177
|
-
reversal[fullPath] =
|
|
207
|
+
reversal[fullPath] = lhsParent[lhsLastKey];
|
|
178
208
|
}
|
|
179
209
|
}
|
|
180
|
-
|
|
210
|
+
lhsParent[lhsLastKey] = !valueToNegate;
|
|
181
211
|
}
|
|
182
212
|
continue;
|
|
183
213
|
}
|
|
@@ -185,26 +215,38 @@ export function assignTentatively(target, source, options) {
|
|
|
185
215
|
if (isDeleteCommand(key)) {
|
|
186
216
|
const path = parseDeleteCommand(key);
|
|
187
217
|
if (path !== null) {
|
|
188
|
-
const pathParts = parsePath(path);
|
|
189
218
|
// Determine the parent object
|
|
190
219
|
let parent = target;
|
|
191
220
|
let canDelete = true;
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
parent
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
221
|
+
let pathParts = [];
|
|
222
|
+
if (isNestedPath(path)) {
|
|
223
|
+
pathParts = parsePath(path);
|
|
224
|
+
if (pathParts.length === 0) {
|
|
225
|
+
parent = target;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
for (const part of pathParts) {
|
|
229
|
+
if (parent && typeof parent === 'object' && part in parent) {
|
|
230
|
+
parent = parent[part];
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
canDelete = false;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
205
236
|
}
|
|
206
237
|
}
|
|
207
238
|
}
|
|
239
|
+
else if (path.length > 0) {
|
|
240
|
+
// Plain key - navigate one level
|
|
241
|
+
pathParts = [path];
|
|
242
|
+
if (parent && typeof parent === 'object' && path in parent) {
|
|
243
|
+
parent = parent[path];
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
canDelete = false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// else: empty path = delete from root
|
|
208
250
|
if (canDelete && typeof parent === 'object' && parent !== null) {
|
|
209
251
|
// RHS can be a string (single property) or array (multiple properties)
|
|
210
252
|
const propertiesToDelete = Array.isArray(value) ? value : [value];
|
|
@@ -212,7 +254,7 @@ export function assignTentatively(target, source, options) {
|
|
|
212
254
|
if (prop in parent) {
|
|
213
255
|
// Store original value for reversal
|
|
214
256
|
const fullPath = pathParts.length > 0
|
|
215
|
-
? `?.${pathParts.join('?.')}
|
|
257
|
+
? `?.${pathParts.join('?.')}?.${prop}`
|
|
216
258
|
: `?.${prop}`;
|
|
217
259
|
if (!(fullPath in reversal)) {
|
|
218
260
|
reversal[fullPath] = parent[prop];
|
package/assignTentatively.ts
CHANGED
|
@@ -58,11 +58,13 @@ function parseDeleteCommand(key: string): string | null {
|
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Helper function to parse a path string with ?. notation
|
|
61
|
+
* Always splits on '?.' delimiter, preserving dots that are part of values
|
|
62
|
+
* (e.g., CSS class selectors like '.username')
|
|
63
|
+
* Paths must use ?. notation — plain dot notation is not supported.
|
|
61
64
|
*/
|
|
62
65
|
function parsePath(path: string): string[] {
|
|
63
66
|
return path
|
|
64
|
-
.split('
|
|
65
|
-
.map(part => part.replace(/\?/g, ''))
|
|
67
|
+
.split('?.')
|
|
66
68
|
.filter(part => part.length > 0);
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -132,27 +134,39 @@ export function assignTentatively(
|
|
|
132
134
|
if (isIncCommand(key)) {
|
|
133
135
|
const path = parseIncCommand(key);
|
|
134
136
|
if (path) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
137
|
+
if (isNestedPath(path)) {
|
|
138
|
+
const pathParts = parsePath(path);
|
|
139
|
+
const topLevelKey = pathParts[0];
|
|
140
|
+
|
|
141
|
+
// Track if we created a new top-level path (BEFORE calling ensureNestedPath)
|
|
142
|
+
if (!(topLevelKey in target)) {
|
|
143
|
+
trackedCreatedPaths.add(topLevelKey);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const lastKey = pathParts[pathParts.length - 1];
|
|
147
|
+
const parent = ensureNestedPath(target, pathParts);
|
|
145
148
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
// If property already exists, store original value for reversal
|
|
150
|
+
if (lastKey in parent) {
|
|
151
|
+
const fullPath = `?.${pathParts.join('?.')}`;
|
|
152
|
+
if (!(fullPath in reversal)) {
|
|
153
|
+
reversal[fullPath] = parent[lastKey];
|
|
154
|
+
}
|
|
155
|
+
parent[lastKey] += value;
|
|
156
|
+
} else {
|
|
157
|
+
// Property doesn't exist, create it with the value
|
|
158
|
+
parent[lastKey] = value;
|
|
151
159
|
}
|
|
152
|
-
parent[lastKey] += value;
|
|
153
160
|
} else {
|
|
154
|
-
//
|
|
155
|
-
|
|
161
|
+
// Plain key - direct operation on target
|
|
162
|
+
if (path in target) {
|
|
163
|
+
if (!(path in reversal)) {
|
|
164
|
+
reversal[path] = target[path];
|
|
165
|
+
}
|
|
166
|
+
target[path] += value;
|
|
167
|
+
} else {
|
|
168
|
+
target[path] = value;
|
|
169
|
+
}
|
|
156
170
|
}
|
|
157
171
|
}
|
|
158
172
|
continue;
|
|
@@ -163,28 +177,39 @@ export function assignTentatively(
|
|
|
163
177
|
const lhsPath = parseToggleCommand(key);
|
|
164
178
|
if (lhsPath) {
|
|
165
179
|
const rhsPath = value;
|
|
166
|
-
const pathParts = parsePath(lhsPath);
|
|
167
|
-
const topLevelKey = pathParts[0];
|
|
168
180
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
181
|
+
let lhsParent: any;
|
|
182
|
+
let lhsLastKey: string;
|
|
183
|
+
let lhsPathParts: string[];
|
|
173
184
|
|
|
174
|
-
|
|
175
|
-
|
|
185
|
+
if (isNestedPath(lhsPath)) {
|
|
186
|
+
lhsPathParts = parsePath(lhsPath);
|
|
187
|
+
const topLevelKey = lhsPathParts[0];
|
|
188
|
+
|
|
189
|
+
// Track if we created a new top-level path (BEFORE calling ensureNestedPath)
|
|
190
|
+
if (!(topLevelKey in target)) {
|
|
191
|
+
trackedCreatedPaths.add(topLevelKey);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
lhsLastKey = lhsPathParts[lhsPathParts.length - 1];
|
|
195
|
+
lhsParent = ensureNestedPath(target, lhsPathParts);
|
|
196
|
+
} else {
|
|
197
|
+
lhsPathParts = [lhsPath];
|
|
198
|
+
lhsLastKey = lhsPath;
|
|
199
|
+
lhsParent = target;
|
|
200
|
+
}
|
|
176
201
|
|
|
177
202
|
// Determine what to negate
|
|
178
203
|
let valueToNegate;
|
|
179
204
|
if (rhsPath === '.') {
|
|
180
205
|
// Self-reference
|
|
181
|
-
if (
|
|
182
|
-
valueToNegate =
|
|
206
|
+
if (lhsLastKey in lhsParent) {
|
|
207
|
+
valueToNegate = lhsParent[lhsLastKey];
|
|
183
208
|
} else {
|
|
184
209
|
valueToNegate = undefined;
|
|
185
210
|
}
|
|
186
|
-
} else {
|
|
187
|
-
// RHS path: navigate to get the value (don't create paths)
|
|
211
|
+
} else if (isNestedPath(rhsPath)) {
|
|
212
|
+
// RHS nested path: navigate to get the value (don't create paths)
|
|
188
213
|
const rhsPathParts = parsePath(rhsPath);
|
|
189
214
|
let current = target;
|
|
190
215
|
let exists = true;
|
|
@@ -199,17 +224,20 @@ export function assignTentatively(
|
|
|
199
224
|
}
|
|
200
225
|
|
|
201
226
|
valueToNegate = exists ? current : true;
|
|
227
|
+
} else {
|
|
228
|
+
// Plain key RHS
|
|
229
|
+
valueToNegate = (rhsPath in target) ? target[rhsPath] : true;
|
|
202
230
|
}
|
|
203
231
|
|
|
204
232
|
// Store original value for reversal if it exists
|
|
205
|
-
if (
|
|
206
|
-
const fullPath = `?.${
|
|
233
|
+
if (lhsLastKey in lhsParent) {
|
|
234
|
+
const fullPath = `?.${lhsPathParts.join('?.')}`;
|
|
207
235
|
if (!(fullPath in reversal)) {
|
|
208
|
-
reversal[fullPath] =
|
|
236
|
+
reversal[fullPath] = lhsParent[lhsLastKey];
|
|
209
237
|
}
|
|
210
238
|
}
|
|
211
239
|
|
|
212
|
-
|
|
240
|
+
lhsParent[lhsLastKey] = !valueToNegate;
|
|
213
241
|
}
|
|
214
242
|
continue;
|
|
215
243
|
}
|
|
@@ -218,26 +246,35 @@ export function assignTentatively(
|
|
|
218
246
|
if (isDeleteCommand(key)) {
|
|
219
247
|
const path = parseDeleteCommand(key);
|
|
220
248
|
if (path !== null) {
|
|
221
|
-
const pathParts = parsePath(path);
|
|
222
|
-
|
|
223
249
|
// Determine the parent object
|
|
224
250
|
let parent = target;
|
|
225
251
|
let canDelete = true;
|
|
252
|
+
let pathParts: string[] = [];
|
|
226
253
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
254
|
+
if (isNestedPath(path)) {
|
|
255
|
+
pathParts = parsePath(path);
|
|
256
|
+
if (pathParts.length === 0) {
|
|
257
|
+
parent = target;
|
|
258
|
+
} else {
|
|
259
|
+
for (const part of pathParts) {
|
|
260
|
+
if (parent && typeof parent === 'object' && part in parent) {
|
|
261
|
+
parent = parent[part];
|
|
262
|
+
} else {
|
|
263
|
+
canDelete = false;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
238
266
|
}
|
|
239
267
|
}
|
|
268
|
+
} else if (path.length > 0) {
|
|
269
|
+
// Plain key - navigate one level
|
|
270
|
+
pathParts = [path];
|
|
271
|
+
if (parent && typeof parent === 'object' && path in parent) {
|
|
272
|
+
parent = parent[path];
|
|
273
|
+
} else {
|
|
274
|
+
canDelete = false;
|
|
275
|
+
}
|
|
240
276
|
}
|
|
277
|
+
// else: empty path = delete from root
|
|
241
278
|
|
|
242
279
|
if (canDelete && typeof parent === 'object' && parent !== null) {
|
|
243
280
|
// RHS can be a string (single property) or array (multiple properties)
|
|
@@ -247,7 +284,7 @@ export function assignTentatively(
|
|
|
247
284
|
if (prop in parent) {
|
|
248
285
|
// Store original value for reversal
|
|
249
286
|
const fullPath = pathParts.length > 0
|
|
250
|
-
? `?.${pathParts.join('?.')}
|
|
287
|
+
? `?.${pathParts.join('?.')}?.${prop}`
|
|
251
288
|
: `?.${prop}`;
|
|
252
289
|
if (!(fullPath in reversal)) {
|
|
253
290
|
reversal[fullPath] = parent[prop];
|
package/package.json
CHANGED
package/resolveValues.js
CHANGED
|
@@ -1,3 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply alias substitutions to a path string.
|
|
3
|
+
* Replaces complete tokens between `?.` delimiters with their aliased values.
|
|
4
|
+
*/
|
|
5
|
+
function applyAliases(path, aliasMap) {
|
|
6
|
+
if (aliasMap.size === 0)
|
|
7
|
+
return path;
|
|
8
|
+
const parts = path.split('?.');
|
|
9
|
+
const substituted = parts.map(part => aliasMap.get(part) ?? part);
|
|
10
|
+
return substituted.join('?.');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Navigate a path against a source object, optionally calling methods.
|
|
14
|
+
* Returns the resolved value at the end of the path.
|
|
15
|
+
*/
|
|
16
|
+
function navigatePath(source, parts, withMethods) {
|
|
17
|
+
let current = source;
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < parts.length) {
|
|
20
|
+
if (current == null)
|
|
21
|
+
return current;
|
|
22
|
+
const part = parts[i];
|
|
23
|
+
if (withMethods && withMethods.has(part)) {
|
|
24
|
+
const method = current[part];
|
|
25
|
+
if (typeof method === 'function') {
|
|
26
|
+
const nextPart = parts[i + 1];
|
|
27
|
+
if (nextPart !== undefined && !(withMethods.has(nextPart))) {
|
|
28
|
+
// Call method with next segment as argument, consume it
|
|
29
|
+
current = method.call(current, nextPart);
|
|
30
|
+
i += 2;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// Consecutive methods or last segment — call with no args
|
|
34
|
+
current = method.call(current);
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
current = current[part];
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
current = current[part];
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
1
50
|
/**
|
|
2
51
|
* Resolve RHS path strings in a pattern object against a source object.
|
|
3
52
|
*
|
|
@@ -5,36 +54,55 @@
|
|
|
5
54
|
* and resolved against the source object using optional chaining semantics.
|
|
6
55
|
* Non-string values and strings not starting with `?.` pass through unchanged.
|
|
7
56
|
*
|
|
57
|
+
* Supports `withMethods` for calling methods during resolution and `aka` for
|
|
58
|
+
* alias substitution, consistent with assignGingerly's LHS path handling.
|
|
59
|
+
*
|
|
60
|
+
* Special case: `'?.'` (empty path) resolves to the source object itself.
|
|
61
|
+
*
|
|
8
62
|
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
9
63
|
* @param source - Object to resolve paths against
|
|
64
|
+
* @param options - Optional withMethods and aka for method calls and aliases
|
|
10
65
|
* @returns New object with path strings replaced by resolved values
|
|
11
66
|
*
|
|
12
67
|
* @example
|
|
13
|
-
* const
|
|
68
|
+
* const result = resolveValues({
|
|
14
69
|
* hello: '?.myPropContainer?.stringProp',
|
|
15
70
|
* foo: '?.myFooString',
|
|
16
71
|
* literal: 42
|
|
17
|
-
* };
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
72
|
+
* }, source);
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // With methods and aliases
|
|
76
|
+
* const result = resolveValues({
|
|
77
|
+
* text: '?.q?..username?.textContent'
|
|
78
|
+
* }, source, {
|
|
79
|
+
* withMethods: ['querySelector'],
|
|
80
|
+
* aka: { 'q': 'querySelector' }
|
|
81
|
+
* });
|
|
24
82
|
*/
|
|
25
|
-
export function resolveValues(pattern, source) {
|
|
83
|
+
export function resolveValues(pattern, source, options) {
|
|
84
|
+
// Build alias map
|
|
85
|
+
const aliasMap = new Map();
|
|
86
|
+
if (options?.aka) {
|
|
87
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
88
|
+
aliasMap.set(alias, target);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Build methods set
|
|
92
|
+
const withMethods = options?.withMethods
|
|
93
|
+
? options.withMethods instanceof Set
|
|
94
|
+
? options.withMethods
|
|
95
|
+
: new Set(options.withMethods)
|
|
96
|
+
: undefined;
|
|
26
97
|
const result = {};
|
|
27
98
|
for (const [key, value] of Object.entries(pattern)) {
|
|
28
99
|
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
29
|
-
//
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
current = current[part];
|
|
36
|
-
}
|
|
37
|
-
result[key] = current;
|
|
100
|
+
// Apply aliases to the RHS path
|
|
101
|
+
const aliased = applyAliases(value, aliasMap);
|
|
102
|
+
// Parse path: split on '?.' delimiter, filter empties
|
|
103
|
+
const parts = aliased.split('?.').filter(p => p.length > 0);
|
|
104
|
+
// Navigate with method support
|
|
105
|
+
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
38
106
|
}
|
|
39
107
|
else {
|
|
40
108
|
result[key] = value;
|
package/resolveValues.ts
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for resolveValues
|
|
3
|
+
*/
|
|
4
|
+
export interface ResolveValuesOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Method names that should be called instead of accessed as properties.
|
|
7
|
+
* When a path segment matches, it's called as a method with the next segment as argument.
|
|
8
|
+
*/
|
|
9
|
+
withMethods?: string[] | Set<string>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Alias mappings for path segments.
|
|
13
|
+
* Substituted before path resolution, matching complete tokens between `?.` delimiters.
|
|
14
|
+
*/
|
|
15
|
+
aka?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Apply alias substitutions to a path string.
|
|
20
|
+
* Replaces complete tokens between `?.` delimiters with their aliased values.
|
|
21
|
+
*/
|
|
22
|
+
function applyAliases(path: string, aliasMap: Map<string, string>): string {
|
|
23
|
+
if (aliasMap.size === 0) return path;
|
|
24
|
+
const parts = path.split('?.');
|
|
25
|
+
const substituted = parts.map(part => aliasMap.get(part) ?? part);
|
|
26
|
+
return substituted.join('?.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Navigate a path against a source object, optionally calling methods.
|
|
31
|
+
* Returns the resolved value at the end of the path.
|
|
32
|
+
*/
|
|
33
|
+
function navigatePath(
|
|
34
|
+
source: any,
|
|
35
|
+
parts: string[],
|
|
36
|
+
withMethods: Set<string> | undefined
|
|
37
|
+
): any {
|
|
38
|
+
let current = source;
|
|
39
|
+
let i = 0;
|
|
40
|
+
|
|
41
|
+
while (i < parts.length) {
|
|
42
|
+
if (current == null) return current;
|
|
43
|
+
|
|
44
|
+
const part = parts[i];
|
|
45
|
+
|
|
46
|
+
if (withMethods && withMethods.has(part)) {
|
|
47
|
+
const method = current[part];
|
|
48
|
+
if (typeof method === 'function') {
|
|
49
|
+
const nextPart = parts[i + 1];
|
|
50
|
+
if (nextPart !== undefined && !(withMethods.has(nextPart))) {
|
|
51
|
+
// Call method with next segment as argument, consume it
|
|
52
|
+
current = method.call(current, nextPart);
|
|
53
|
+
i += 2;
|
|
54
|
+
} else {
|
|
55
|
+
// Consecutive methods or last segment — call with no args
|
|
56
|
+
current = method.call(current);
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
current = current[part];
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
current = current[part];
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return current;
|
|
70
|
+
}
|
|
71
|
+
|
|
1
72
|
/**
|
|
2
73
|
* Resolve RHS path strings in a pattern object against a source object.
|
|
3
74
|
*
|
|
@@ -5,38 +76,63 @@
|
|
|
5
76
|
* and resolved against the source object using optional chaining semantics.
|
|
6
77
|
* Non-string values and strings not starting with `?.` pass through unchanged.
|
|
7
78
|
*
|
|
79
|
+
* Supports `withMethods` for calling methods during resolution and `aka` for
|
|
80
|
+
* alias substitution, consistent with assignGingerly's LHS path handling.
|
|
81
|
+
*
|
|
82
|
+
* Special case: `'?.'` (empty path) resolves to the source object itself.
|
|
83
|
+
*
|
|
8
84
|
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
9
85
|
* @param source - Object to resolve paths against
|
|
86
|
+
* @param options - Optional withMethods and aka for method calls and aliases
|
|
10
87
|
* @returns New object with path strings replaced by resolved values
|
|
11
88
|
*
|
|
12
89
|
* @example
|
|
13
|
-
* const
|
|
90
|
+
* const result = resolveValues({
|
|
14
91
|
* hello: '?.myPropContainer?.stringProp',
|
|
15
92
|
* foo: '?.myFooString',
|
|
16
93
|
* literal: 42
|
|
17
|
-
* };
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
94
|
+
* }, source);
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* // With methods and aliases
|
|
98
|
+
* const result = resolveValues({
|
|
99
|
+
* text: '?.q?..username?.textContent'
|
|
100
|
+
* }, source, {
|
|
101
|
+
* withMethods: ['querySelector'],
|
|
102
|
+
* aka: { 'q': 'querySelector' }
|
|
103
|
+
* });
|
|
24
104
|
*/
|
|
25
105
|
export function resolveValues(
|
|
26
106
|
pattern: Record<string, any>,
|
|
27
|
-
source: any
|
|
107
|
+
source: any,
|
|
108
|
+
options?: ResolveValuesOptions
|
|
28
109
|
): Record<string, any> {
|
|
110
|
+
// Build alias map
|
|
111
|
+
const aliasMap = new Map<string, string>();
|
|
112
|
+
if (options?.aka) {
|
|
113
|
+
for (const [alias, target] of Object.entries(options.aka)) {
|
|
114
|
+
aliasMap.set(alias, target);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Build methods set
|
|
119
|
+
const withMethods = options?.withMethods
|
|
120
|
+
? options.withMethods instanceof Set
|
|
121
|
+
? options.withMethods
|
|
122
|
+
: new Set(options.withMethods)
|
|
123
|
+
: undefined;
|
|
124
|
+
|
|
29
125
|
const result: Record<string, any> = {};
|
|
30
126
|
for (const [key, value] of Object.entries(pattern)) {
|
|
31
127
|
if (typeof value === 'string' && value.startsWith('?.')) {
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
result[key] =
|
|
128
|
+
// Apply aliases to the RHS path
|
|
129
|
+
const aliased = applyAliases(value, aliasMap);
|
|
130
|
+
|
|
131
|
+
// Parse path: split on '?.' delimiter, filter empties
|
|
132
|
+
const parts = aliased.split('?.').filter(p => p.length > 0);
|
|
133
|
+
|
|
134
|
+
// Navigate with method support
|
|
135
|
+
result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
|
|
40
136
|
} else {
|
|
41
137
|
result[key] = value;
|
|
42
138
|
}
|