assign-gingerly 0.0.59 → 0.0.61
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 +307 -14
- 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 +26 -0
- package/builtInEmoji.ts +34 -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/handlers/rangeSelector.js +52 -0
- package/handlers/rangeSelector.ts +90 -0
- 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 +7 -1
- package/processHandlerCommands.ts +8 -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
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* },
|
|
19
19
|
* fromEachItem: {
|
|
20
20
|
* assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
|
|
21
|
-
* withOptions: { withMethods: ['querySelector'],
|
|
21
|
+
* withOptions: { withMethods: ['querySelector'], infer: true },
|
|
22
22
|
* resolve: { key: '?.rank' }
|
|
23
23
|
* }
|
|
24
24
|
* }
|
|
@@ -75,6 +75,7 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const fromEachItem = this.config.fromEachItem;
|
|
78
|
+
const configs = fromEachItem?.configs; // Array form for multi-element templates
|
|
78
79
|
const assignToFragment = fromEachItem?.assignToFragment ?? {};
|
|
79
80
|
const withOptions = fromEachItem?.withOptions ?? {};
|
|
80
81
|
const perItemResolve = fromEachItem?.resolve ?? {};
|
|
@@ -85,10 +86,10 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
85
86
|
const sourceAssignToFragment = fromSource?.assignToFragment;
|
|
86
87
|
const sourceWithOptions = fromSource?.withOptions ?? {};
|
|
87
88
|
|
|
88
|
-
// Detect fast path: no assignToFragment patterns, just
|
|
89
|
-
const hasAssignPatterns = Object.keys(assignToFragment).length > 0;
|
|
90
|
-
const inferredConfig = withOptions.
|
|
91
|
-
const useFastPath = !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
|
|
89
|
+
// Detect fast path: no assignToFragment patterns, just infer (only for non-configs mode)
|
|
90
|
+
const hasAssignPatterns = !configs && Object.keys(assignToFragment).length > 0;
|
|
91
|
+
const inferredConfig = !configs && withOptions.infer;
|
|
92
|
+
const useFastPath = !configs && !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
|
|
92
93
|
|
|
93
94
|
const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
|
|
94
95
|
|
|
@@ -149,17 +150,28 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
149
150
|
if (state.keyToNodes.has(key) && oldKeys.has(key)) {
|
|
150
151
|
// Existing item — update in place
|
|
151
152
|
const existingNodes = state.keyToNodes.get(key)!;
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
|
|
154
|
+
if (shouldYield) await new Promise(r => setTimeout(r, 0));
|
|
155
|
+
|
|
156
|
+
if (configs) {
|
|
157
|
+
// Multi-element: zip configs with element nodes
|
|
158
|
+
const elements = existingNodes.filter(n => n instanceof Element) as Element[];
|
|
159
|
+
const len = Math.min(elements.length, configs.length);
|
|
160
|
+
for (let j = 0; j < len; j++) {
|
|
161
|
+
const cfg = configs[j];
|
|
162
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
|
|
160
163
|
}
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
} else {
|
|
165
|
+
const rootEl = existingNodes.find(n => n instanceof Element) as Element | undefined;
|
|
166
|
+
if (rootEl) {
|
|
167
|
+
if (processInferred) {
|
|
168
|
+
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
169
|
+
} else {
|
|
170
|
+
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
171
|
+
}
|
|
172
|
+
if (sourceAssignToFragment && options?.from) {
|
|
173
|
+
assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
|
|
174
|
+
}
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
177
|
newKeyToNodes.set(key, existingNodes);
|
|
@@ -180,21 +192,32 @@ export class ManageTemplateListHandler implements AssignFromHandler {
|
|
|
180
192
|
const clonedNodes = Array.from(content.childNodes);
|
|
181
193
|
|
|
182
194
|
// Apply per-item assignments to the cloned fragment
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
195
|
+
const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
|
|
196
|
+
if (shouldYield) await new Promise(r => setTimeout(r, 0));
|
|
197
|
+
|
|
198
|
+
if (configs) {
|
|
199
|
+
// Multi-element: zip configs with element nodes
|
|
200
|
+
const elements = clonedNodes.filter(n => n instanceof Element) as Element[];
|
|
201
|
+
const len = Math.min(elements.length, configs.length);
|
|
202
|
+
for (let j = 0; j < len; j++) {
|
|
203
|
+
const cfg = configs[j];
|
|
204
|
+
assignFrom(elements[j], cfg.assignToFragment ?? {}, { from: item, ...cfg.withOptions });
|
|
194
205
|
}
|
|
206
|
+
} else {
|
|
207
|
+
const rootEl = clonedNodes.find(n => n instanceof Element) as Element | undefined;
|
|
208
|
+
if (rootEl) {
|
|
209
|
+
const tempContainer = document.createDocumentFragment();
|
|
210
|
+
tempContainer.appendChild(content);
|
|
195
211
|
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
if (processInferred) {
|
|
213
|
+
processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
|
|
214
|
+
} else {
|
|
215
|
+
assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (sourceAssignToFragment && options?.from) {
|
|
219
|
+
assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
|
|
220
|
+
}
|
|
198
221
|
}
|
|
199
222
|
}
|
|
200
223
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtIns.rangeSelector handler for assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates a value against a series of range conditions and merges
|
|
5
|
+
* the matched case's properties into the target.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import assignGingerly from '../assignGingerly.js';
|
|
9
|
+
|
|
10
|
+
const OPERATORS = new Set(['<=', '<', '>=', '>', '===', '!==']);
|
|
11
|
+
|
|
12
|
+
function checkCondition(value, op, threshold) {
|
|
13
|
+
switch (op) {
|
|
14
|
+
case '<=': return value <= threshold;
|
|
15
|
+
case '<': return value < threshold;
|
|
16
|
+
case '>=': return value >= threshold;
|
|
17
|
+
case '>': return value > threshold;
|
|
18
|
+
case '===': return value === threshold;
|
|
19
|
+
case '!==': return value !== threshold;
|
|
20
|
+
default: return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function caseMatches(value, caseObj) {
|
|
25
|
+
for (const key of Object.keys(caseObj)) {
|
|
26
|
+
if (OPERATORS.has(key)) {
|
|
27
|
+
if (!checkCondition(value, key, caseObj[key])) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class RangeSelectorHandler {
|
|
36
|
+
config;
|
|
37
|
+
constructor(config) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
}
|
|
40
|
+
async assign(lhsTarget, resolvedParams) {
|
|
41
|
+
const { value, when } = resolvedParams;
|
|
42
|
+
if (!Array.isArray(when)) return;
|
|
43
|
+
for (const caseObj of when) {
|
|
44
|
+
if (caseMatches(value, caseObj)) {
|
|
45
|
+
if (caseObj.merge && typeof caseObj.merge === 'object') {
|
|
46
|
+
assignGingerly(lhsTarget, caseObj.merge);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtIns.rangeSelector handler for assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Evaluates a value against a series of range conditions and merges
|
|
5
|
+
* the matched case's properties into the target. Useful for converting
|
|
6
|
+
* imperative if/else-if chains into declarative JSON configs.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* assignFrom(element, {
|
|
10
|
+
* '?. =>': {
|
|
11
|
+
* do: 'builtIns.rangeSelector',
|
|
12
|
+
* get: {
|
|
13
|
+
* value: '?.count',
|
|
14
|
+
* when: [
|
|
15
|
+
* { '<=': 10, merge: { status: 'low' } },
|
|
16
|
+
* { '<': 20, merge: { status: 'medium' } },
|
|
17
|
+
* { merge: { status: 'high' } }
|
|
18
|
+
* ]
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* }, { from: vm });
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { AssignFromHandler } from '../assignFromAsync.js';
|
|
25
|
+
import assignGingerly from '../assignGingerly.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Operator keys recognized in case objects.
|
|
29
|
+
*/
|
|
30
|
+
const OPERATORS = new Set(['<=', '<', '>=', '>', '===', '!==']);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if a single operator condition is satisfied.
|
|
34
|
+
*/
|
|
35
|
+
function checkCondition(value: any, op: string, threshold: any): boolean {
|
|
36
|
+
switch (op) {
|
|
37
|
+
case '<=': return value <= threshold;
|
|
38
|
+
case '<': return value < threshold;
|
|
39
|
+
case '>=': return value >= threshold;
|
|
40
|
+
case '>': return value > threshold;
|
|
41
|
+
case '===': return value === threshold;
|
|
42
|
+
case '!==': return value !== threshold;
|
|
43
|
+
default: return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if all operator conditions in a case object are satisfied (AND logic).
|
|
49
|
+
* Returns true if no operator keys present (catch-all/default case).
|
|
50
|
+
*/
|
|
51
|
+
function caseMatches(value: any, caseObj: Record<string, any>): boolean {
|
|
52
|
+
let hasCondition = false;
|
|
53
|
+
for (const key of Object.keys(caseObj)) {
|
|
54
|
+
if (OPERATORS.has(key)) {
|
|
55
|
+
hasCondition = true;
|
|
56
|
+
if (!checkCondition(value, key, caseObj[key])) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// No operator keys = default/catch-all
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* RangeSelectorHandler — declarative range-based conditional merge.
|
|
67
|
+
*/
|
|
68
|
+
export class RangeSelectorHandler implements AssignFromHandler {
|
|
69
|
+
config: any;
|
|
70
|
+
|
|
71
|
+
constructor(config: any) {
|
|
72
|
+
this.config = config;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async assign(lhsTarget: any, resolvedParams: any): Promise<void> {
|
|
76
|
+
const { value, when } = resolvedParams;
|
|
77
|
+
|
|
78
|
+
if (!Array.isArray(when)) return;
|
|
79
|
+
|
|
80
|
+
// Find first matching case (short-circuit)
|
|
81
|
+
for (const caseObj of when) {
|
|
82
|
+
if (caseMatches(value, caseObj)) {
|
|
83
|
+
if (caseObj.merge && typeof caseObj.merge === 'object') {
|
|
84
|
+
assignGingerly(lhsTarget, caseObj.merge);
|
|
85
|
+
}
|
|
86
|
+
return; // First match wins
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
package/inferencer/inferencer.js
CHANGED
|
@@ -81,38 +81,26 @@ export class Infer {
|
|
|
81
81
|
return inferValueProperty(this.enhancedElement);
|
|
82
82
|
}
|
|
83
83
|
['|'](itempropAttr, scopeBoundary = '[itemscope]') {
|
|
84
|
-
|
|
85
|
-
return Array.from(candidates)
|
|
86
|
-
.filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
87
|
-
.map(x => new Infer(x, itempropAttr));
|
|
84
|
+
return this.#queryScoped(`[itemprop="${itempropAttr}"]`, itempropAttr, scopeBoundary);
|
|
88
85
|
}
|
|
89
86
|
['@'](nameAttr, scopeBoundary) {
|
|
90
|
-
|
|
91
|
-
const filtered = scopeBoundary
|
|
92
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
93
|
-
: Array.from(candidates);
|
|
94
|
-
return filtered.map(x => new Infer(x, nameAttr));
|
|
87
|
+
return this.#queryScoped(`[name="${nameAttr}"]`, nameAttr, scopeBoundary);
|
|
95
88
|
}
|
|
96
89
|
['%'](partAttr, scopeBoundary) {
|
|
97
|
-
|
|
98
|
-
const filtered = scopeBoundary
|
|
99
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
100
|
-
: Array.from(candidates);
|
|
101
|
-
return filtered.map(x => new Infer(x, partAttr));
|
|
90
|
+
return this.#queryScoped(`[part~="${partAttr}"]`, partAttr, scopeBoundary);
|
|
102
91
|
}
|
|
103
92
|
['#'](id, scopeBoundary) {
|
|
104
|
-
|
|
105
|
-
const filtered = scopeBoundary
|
|
106
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
107
|
-
: Array.from(candidates);
|
|
108
|
-
return filtered.map(x => new Infer(x, id));
|
|
93
|
+
return this.#queryScoped(`#${id}`, id, scopeBoundary);
|
|
109
94
|
}
|
|
110
95
|
['.'](className, scopeBoundary) {
|
|
111
|
-
|
|
96
|
+
return this.#queryScoped(`.${className}`, className, scopeBoundary);
|
|
97
|
+
}
|
|
98
|
+
#queryScoped(selector, propName, scopeBoundary) {
|
|
99
|
+
const candidates = this.enhancedElement.querySelectorAll(selector);
|
|
112
100
|
const filtered = scopeBoundary
|
|
113
101
|
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
114
102
|
: Array.from(candidates);
|
|
115
|
-
return filtered.map(x => new Infer(x,
|
|
103
|
+
return filtered.map(x => new Infer(x, propName));
|
|
116
104
|
}
|
|
117
105
|
setDisplay(vm) {
|
|
118
106
|
const val = this.#propName ? vm[this.#propName] : inferBindingProperty(this.enhancedElement);
|
package/inferencer/inferencer.ts
CHANGED
|
@@ -103,42 +103,31 @@ export class Infer<TValue = any, TDisplay = any> {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
['|'](itempropAttr: string, scopeBoundary: string = '[itemscope]'){
|
|
106
|
-
|
|
107
|
-
return Array.from(candidates)
|
|
108
|
-
.filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
109
|
-
.map(x => new Infer(x, itempropAttr));
|
|
106
|
+
return this.#queryScoped(`[itemprop="${itempropAttr}"]`, itempropAttr, scopeBoundary);
|
|
110
107
|
}
|
|
111
108
|
|
|
112
109
|
['@'](nameAttr: string, scopeBoundary?: string){
|
|
113
|
-
|
|
114
|
-
const filtered = scopeBoundary
|
|
115
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
116
|
-
: Array.from(candidates);
|
|
117
|
-
return filtered.map(x => new Infer(x, nameAttr));
|
|
110
|
+
return this.#queryScoped(`[name="${nameAttr}"]`, nameAttr, scopeBoundary);
|
|
118
111
|
}
|
|
119
112
|
|
|
120
113
|
['%'](partAttr: string, scopeBoundary?: string){
|
|
121
|
-
|
|
122
|
-
const filtered = scopeBoundary
|
|
123
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
124
|
-
: Array.from(candidates);
|
|
125
|
-
return filtered.map(x => new Infer(x, partAttr));
|
|
114
|
+
return this.#queryScoped(`[part~="${partAttr}"]`, partAttr, scopeBoundary);
|
|
126
115
|
}
|
|
127
116
|
|
|
128
117
|
['#'](id: string, scopeBoundary?: string){
|
|
129
|
-
|
|
130
|
-
const filtered = scopeBoundary
|
|
131
|
-
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
132
|
-
: Array.from(candidates);
|
|
133
|
-
return filtered.map(x => new Infer(x, id));
|
|
118
|
+
return this.#queryScoped(`#${id}`, id, scopeBoundary);
|
|
134
119
|
}
|
|
135
120
|
|
|
136
121
|
['.'](className: string, scopeBoundary?: string){
|
|
137
|
-
|
|
122
|
+
return this.#queryScoped(`.${className}`, className, scopeBoundary);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#queryScoped(selector: string, propName: string, scopeBoundary?: string): Infer[] {
|
|
126
|
+
const candidates = this.enhancedElement.querySelectorAll(selector);
|
|
138
127
|
const filtered = scopeBoundary
|
|
139
128
|
? Array.from(candidates).filter(el => withScopePerimeter(this.enhancedElement, el, scopeBoundary))
|
|
140
129
|
: Array.from(candidates);
|
|
141
|
-
return filtered.map(x => new Infer(x,
|
|
130
|
+
return filtered.map(x => new Infer(x, propName));
|
|
142
131
|
}
|
|
143
132
|
|
|
144
133
|
setDisplay(vm: any){
|
package/inferredAssignments.js
CHANGED
|
@@ -17,11 +17,14 @@ export function processInferredAssignments(target, from, config) {
|
|
|
17
17
|
return;
|
|
18
18
|
if (!from || typeof from !== 'object')
|
|
19
19
|
return;
|
|
20
|
-
const { byItemprop } = config;
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const { byItemprop, byName } = config;
|
|
21
|
+
// Resolve aliases: '|' → byItemprop, '@' → byName
|
|
22
|
+
const effectiveByItemprop = byItemprop ?? config['|'];
|
|
23
|
+
const effectiveByName = byName ?? config['@'];
|
|
24
|
+
if (effectiveByItemprop) {
|
|
25
|
+
const keys = effectiveByItemprop === true
|
|
23
26
|
? Object.keys(from)
|
|
24
|
-
:
|
|
27
|
+
: effectiveByItemprop;
|
|
25
28
|
const infer = new Infer(target);
|
|
26
29
|
for (const key of keys) {
|
|
27
30
|
if (!(key in from))
|
|
@@ -35,4 +38,31 @@ export function processInferredAssignments(target, from, config) {
|
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
}
|
|
41
|
+
if (effectiveByName) {
|
|
42
|
+
// Normalize config
|
|
43
|
+
let keys;
|
|
44
|
+
let scopeBoundary;
|
|
45
|
+
if (effectiveByName === true) {
|
|
46
|
+
keys = Object.keys(from);
|
|
47
|
+
}
|
|
48
|
+
else if (Array.isArray(effectiveByName)) {
|
|
49
|
+
keys = effectiveByName;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Object form: { props, outside }
|
|
53
|
+
keys = effectiveByName.props === true ? Object.keys(from) : effectiveByName.props;
|
|
54
|
+
scopeBoundary = effectiveByName.outside;
|
|
55
|
+
}
|
|
56
|
+
const infer = new Infer(target);
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
if (!(key in from))
|
|
59
|
+
continue;
|
|
60
|
+
const value = from[key];
|
|
61
|
+
// Use inferencer's ['@'] method — by name attribute, optional scope boundary
|
|
62
|
+
const matches = infer['@'](key, scopeBoundary);
|
|
63
|
+
for (const match of matches) {
|
|
64
|
+
match.value = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
38
68
|
}
|
package/inferredAssignments.ts
CHANGED
|
@@ -20,8 +20,23 @@ export interface InferredAssignmentsConfig {
|
|
|
20
20
|
*/
|
|
21
21
|
byItemprop?: string[] | true;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/** Concise alias for byItemprop */
|
|
24
|
+
'|'?: string[] | true;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Array of property keys to distribute by name attribute.
|
|
28
|
+
* For each key, finds [name="${key}"] elements and sets the value using
|
|
29
|
+
* the inferred property (value, checked, etc.).
|
|
30
|
+
*
|
|
31
|
+
* Pass `true` to infer all keys from the `from` source object.
|
|
32
|
+
*
|
|
33
|
+
* Object form enables donut-hole scoping:
|
|
34
|
+
* { props: ['firstName', 'lastName'], outside: 'fieldset' }
|
|
35
|
+
*/
|
|
36
|
+
byName?: string[] | true | { props: string[] | true; outside: string };
|
|
37
|
+
|
|
38
|
+
/** Concise alias for byName */
|
|
39
|
+
'@'?: string[] | true | { props: string[] | true; outside: string };
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
/**
|
|
@@ -39,12 +54,16 @@ export function processInferredAssignments(
|
|
|
39
54
|
if (!(target instanceof Element)) return;
|
|
40
55
|
if (!from || typeof from !== 'object') return;
|
|
41
56
|
|
|
42
|
-
const { byItemprop } = config;
|
|
57
|
+
const { byItemprop, byName } = config;
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
// Resolve aliases: '|' → byItemprop, '@' → byName
|
|
60
|
+
const effectiveByItemprop = byItemprop ?? config['|'];
|
|
61
|
+
const effectiveByName = byName ?? config['@'];
|
|
62
|
+
|
|
63
|
+
if (effectiveByItemprop) {
|
|
64
|
+
const keys = effectiveByItemprop === true
|
|
46
65
|
? Object.keys(from)
|
|
47
|
-
:
|
|
66
|
+
: effectiveByItemprop;
|
|
48
67
|
|
|
49
68
|
const infer = new Infer(target);
|
|
50
69
|
|
|
@@ -62,4 +81,35 @@ export function processInferredAssignments(
|
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
}
|
|
84
|
+
|
|
85
|
+
if (effectiveByName) {
|
|
86
|
+
// Normalize config
|
|
87
|
+
let keys: string[];
|
|
88
|
+
let scopeBoundary: string | undefined;
|
|
89
|
+
|
|
90
|
+
if (effectiveByName === true) {
|
|
91
|
+
keys = Object.keys(from);
|
|
92
|
+
} else if (Array.isArray(effectiveByName)) {
|
|
93
|
+
keys = effectiveByName;
|
|
94
|
+
} else {
|
|
95
|
+
// Object form: { props, outside }
|
|
96
|
+
keys = effectiveByName.props === true ? Object.keys(from) : effectiveByName.props;
|
|
97
|
+
scopeBoundary = effectiveByName.outside;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const infer = new Infer(target);
|
|
101
|
+
|
|
102
|
+
for (const key of keys) {
|
|
103
|
+
if (!(key in from)) continue;
|
|
104
|
+
|
|
105
|
+
const value = from[key];
|
|
106
|
+
|
|
107
|
+
// Use inferencer's ['@'] method — by name attribute, optional scope boundary
|
|
108
|
+
const matches: Infer[] = infer['@'](key, scopeBoundary);
|
|
109
|
+
|
|
110
|
+
for (const match of matches) {
|
|
111
|
+
match.value = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
65
115
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.61",
|
|
4
4
|
"description": "This package provides a utility function for carefully merging one object into another.",
|
|
5
5
|
"homepage": "https://github.com/bahrus/assign-gingerly#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -150,6 +150,18 @@
|
|
|
150
150
|
"default": "./assignFrom.js",
|
|
151
151
|
"types": "./assignFrom.ts"
|
|
152
152
|
},
|
|
153
|
+
"./assignFrom-extension.js": {
|
|
154
|
+
"default": "./assignFrom-extension.js",
|
|
155
|
+
"types": "./assignFrom-extension.ts"
|
|
156
|
+
},
|
|
157
|
+
"./assignFromAsync-extension.js": {
|
|
158
|
+
"default": "./assignFromAsync-extension.js",
|
|
159
|
+
"types": "./assignFromAsync-extension.ts"
|
|
160
|
+
},
|
|
161
|
+
"./builtInEmoji.js": {
|
|
162
|
+
"default": "./builtInEmoji.js",
|
|
163
|
+
"types": "./builtInEmoji.ts"
|
|
164
|
+
},
|
|
153
165
|
"./assignFeatures.js": {
|
|
154
166
|
"default": "./assignFeatures.js",
|
|
155
167
|
"types": "./assignFeatures.ts"
|
package/playwright.config.ts
CHANGED
|
@@ -16,9 +16,9 @@ export default defineConfig({
|
|
|
16
16
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
17
17
|
forbidOnly: !!process.env.CI,
|
|
18
18
|
/* Retry on CI only */
|
|
19
|
-
retries: process.env.CI ?
|
|
19
|
+
retries: process.env.CI ? 1 : 0,
|
|
20
20
|
/* Opt out of parallel tests on CI. */
|
|
21
|
-
workers: process.env.CI ?
|
|
21
|
+
workers: process.env.CI ? 2 : undefined,
|
|
22
22
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
23
23
|
reporter: [ ['html', { open: 'never' }] ],
|
|
24
24
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
@@ -50,5 +50,6 @@ export default defineConfig({
|
|
|
50
50
|
command: 'npm run serve',
|
|
51
51
|
port: 8000,
|
|
52
52
|
reuseExistingServer: !process.env.CI,
|
|
53
|
+
timeout: 30000,
|
|
53
54
|
},
|
|
54
55
|
});
|
|
@@ -17,6 +17,7 @@ const BUILT_IN_MAP = {
|
|
|
17
17
|
'builtIns.join': './handlers/join.js',
|
|
18
18
|
'builtIns.microDataJoin': './handlers/microDataJoin.js',
|
|
19
19
|
'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
|
|
20
|
+
'builtIns.rangeSelector': './handlers/rangeSelector.js',
|
|
20
21
|
};
|
|
21
22
|
/**
|
|
22
23
|
* Find a handler class in a dynamically imported module.
|
|
@@ -70,8 +71,13 @@ async function resolveFromHandlers(name, handlers, permissions) {
|
|
|
70
71
|
if (typeof entry === 'function') {
|
|
71
72
|
return entry;
|
|
72
73
|
}
|
|
73
|
-
//
|
|
74
|
+
// String value — could be a built-in alias or an import path
|
|
74
75
|
if (typeof entry === 'string') {
|
|
76
|
+
// Built-in alias: redirect to built-in loader
|
|
77
|
+
if (entry.startsWith('builtIns.')) {
|
|
78
|
+
return loadBuiltIn(entry);
|
|
79
|
+
}
|
|
80
|
+
// Import path string — validate and dynamically import
|
|
75
81
|
if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
|
|
76
82
|
throw new Error(`assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
|
|
77
83
|
`Only relative, absolute, or bare specifier paths are allowed (no cross-domain URLs). ` +
|
|
@@ -21,6 +21,7 @@ const BUILT_IN_MAP: Record<string, string> = {
|
|
|
21
21
|
'builtIns.join': './handlers/join.js',
|
|
22
22
|
'builtIns.microDataJoin': './handlers/microDataJoin.js',
|
|
23
23
|
'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
|
|
24
|
+
'builtIns.rangeSelector': './handlers/rangeSelector.js',
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -81,8 +82,14 @@ async function resolveFromHandlers(
|
|
|
81
82
|
return entry as AssignFromHandlerConstructor;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
//
|
|
85
|
+
// String value — could be a built-in alias or an import path
|
|
85
86
|
if (typeof entry === 'string') {
|
|
87
|
+
// Built-in alias: redirect to built-in loader
|
|
88
|
+
if (entry.startsWith('builtIns.')) {
|
|
89
|
+
return loadBuiltIn(entry);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Import path string — validate and dynamically import
|
|
86
93
|
if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
|
|
87
94
|
throw new Error(
|
|
88
95
|
`assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
|