assign-gingerly 0.0.53 → 0.0.55
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 +847 -5
- package/assignFrom.js +229 -9
- package/assignFrom.ts +338 -9
- package/assignGingerly.js +34 -1
- package/assignGingerly.ts +54 -1
- package/beVigilant.js +73 -0
- package/beVigilant.ts +85 -0
- package/enhanceAll.js +106 -0
- package/enhanceAll.ts +138 -0
- package/handlers/join.js +74 -0
- package/handlers/join.ts +80 -0
- package/handlers/lazyLoad.js +212 -0
- package/handlers/lazyLoad.ts +307 -0
- package/handlers/lazyLoadSwitch.js +58 -0
- package/handlers/lazyLoadSwitch.ts +63 -0
- package/handlers/microDataJoin.js +184 -0
- package/handlers/microDataJoin.ts +270 -0
- package/inferencer/.gitmodules +3 -0
- package/inferencer/.vscode/settings.json +2 -0
- package/inferencer/InferencedPropagator.js +230 -0
- package/inferencer/InferencedPropagator.ts +269 -0
- package/inferencer/LICENSE +21 -0
- package/inferencer/README.md +524 -0
- package/inferencer/Requirements/SupportForPropagator.md +368 -0
- package/inferencer/imports.html +7 -0
- package/inferencer/inferencer.js +254 -0
- package/inferencer/inferencer.ts +292 -0
- package/inferencer/package-lock.json +129 -0
- package/inferencer/package.json +60 -0
- package/inferencer/playwright-report/data/507ad515125e13390ea07de92f22331c913fa068.md +55 -0
- package/inferencer/playwright-report/index.html +90 -0
- package/inferencer/playwright.config.ts +54 -0
- package/inferencer/test-results/.last-run.json +6 -0
- package/inferencer/test-results/inferencer-Inferencer-Enha-535bc-inferencer-tests-in-browser-chromium/error-context.md +55 -0
- package/inferencer/tests/inferencedPropagator.html +428 -0
- package/inferencer/tests/inferencedPropagator.spec.ts +18 -0
- package/inferencer/tests/inferencer.html +355 -0
- package/inferencer/tests/inferencer.spec.ts +19 -0
- package/inferencer/tsconfig.json +19 -0
- package/inferencer/types/.kiro/specs/conversion-template/README.md +128 -0
- package/inferencer/types/.kiro/specs/conversion-template/design.md +360 -0
- package/inferencer/types/.kiro/specs/conversion-template/requirements.md +191 -0
- package/inferencer/types/.kiro/specs/conversion-template/tasks.md +174 -0
- package/inferencer/types/.kiro/steering/coding-standards.md +53 -0
- package/inferencer/types/.kiro/steering/conversion-guide.md +108 -0
- package/inferencer/types/.kiro/steering/declarative-configuration.md +108 -0
- package/inferencer/types/.kiro/steering/emc-json-serializability.md +306 -0
- package/inferencer/types/EnhancementConversionInstructions.md +1854 -0
- package/inferencer/types/LICENSE +21 -0
- package/inferencer/types/NewCustomElement.md +388 -0
- package/inferencer/types/NewCustomElementFeature.md +683 -0
- package/inferencer/types/NewEnhancementInstructions.md +705 -0
- package/inferencer/types/README.md +2 -0
- package/inferencer/types/agrace/types.d.ts +11 -0
- package/inferencer/types/assign-gingerly/types.d.ts +572 -0
- package/inferencer/types/be-a-beacon/types.d.ts +17 -0
- package/inferencer/types/be-bound/types.d.ts +66 -0
- package/inferencer/types/be-buttoned-up/types.d.ts +19 -0
- package/inferencer/types/be-calculating/types.d.ts +54 -0
- package/inferencer/types/be-clonable/types.d.ts +38 -0
- package/inferencer/types/be-committed/types.d.ts +22 -0
- package/inferencer/types/be-consoling/types.d.ts +24 -0
- package/inferencer/types/be-decked-with/types.d.ts +26 -0
- package/inferencer/types/be-delible/types.d.ts +27 -0
- package/inferencer/types/be-dispatching/types.d.ts +34 -0
- package/inferencer/types/be-evanescent/types.d.ts +20 -0
- package/inferencer/types/be-flashy/types.d.ts +21 -0
- package/inferencer/types/be-gone/types.d.ts +25 -0
- package/inferencer/types/be-observing/types.d.ts +55 -0
- package/inferencer/types/be-reflective/types.d.ts +78 -0
- package/inferencer/types/be-reformable/types.d.ts +49 -0
- package/inferencer/types/be-render-neutral/types.d.ts +32 -0
- package/inferencer/types/be-switched/types.d.ts +146 -0
- package/inferencer/types/be-typed/types.d.ts +32 -0
- package/inferencer/types/be-valued/types.d.ts +22 -0
- package/inferencer/types/data-props/types.d.ts +34 -0
- package/inferencer/types/do-inc/types.d.ts +56 -0
- package/inferencer/types/do-invoke/types.d.ts +38 -0
- package/inferencer/types/do-merge/types.d.ts +28 -0
- package/inferencer/types/do-toggle/types.d.ts +31 -0
- package/inferencer/types/face-up/types.d.ts +100 -0
- package/inferencer/types/fetch-for/types.d.ts +36 -0
- package/inferencer/types/folder-picker/types.d.ts +21 -0
- package/inferencer/types/global.d.ts +29 -0
- package/inferencer/types/id-generation/types.d.ts +26 -0
- package/inferencer/types/inferencer/types.d.ts +46 -0
- package/inferencer/types/mount-observer/types.d.ts +363 -0
- package/inferencer/types/nested-regex-groups/types.d.ts +107 -0
- package/inferencer/types/pipe-in/types.d.ts +52 -0
- package/inferencer/types/roundabout/types.d.ts +268 -0
- package/inferencer/types/soak-up/types.d.ts +40 -0
- package/inferencer/types/templ-maker/types.d.ts +43 -0
- package/inferencer/types/time-ticker/types.d.ts +62 -0
- package/inferencer/types/truth-sourcer/types.d.ts +44 -0
- package/inferencer/upSearch.js +27 -0
- package/inferencer/upSearch.ts +26 -0
- package/inferencer/withScopePerimeter.js +27 -0
- package/inferencer/withScopePerimeter.ts +33 -0
- package/inferredAssignments.js +38 -0
- package/inferredAssignments.ts +65 -0
- package/isAllowedImportPath.js +42 -0
- package/isAllowedImportPath.ts +53 -0
- package/package.json +57 -3
- package/paths.js +231 -0
- package/paths.ts +413 -0
- package/processHandlerCommands.js +188 -0
- package/processHandlerCommands.ts +217 -0
- package/resolveIdRef.js +144 -0
- package/resolveIdRef.ts +170 -0
- package/resolveValues.js +41 -2
- package/resolveValues.ts +41 -1
- package/transitionHelper.js +109 -0
- package/transitionHelper.ts +132 -0
- package/types/assign-gingerly/types.d.ts +89 -0
package/assignFrom.js
CHANGED
|
@@ -21,15 +21,235 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { resolveValues } from './resolveValues.js';
|
|
23
23
|
import assignGingerly from './assignGingerly.js';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Check if a key ends with the handler operator ' =>'.
|
|
26
|
+
*/
|
|
27
|
+
function isHandlerCommand(key) {
|
|
28
|
+
return key.endsWith(' =>');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Supported substitution variables and their option keys.
|
|
32
|
+
*/
|
|
33
|
+
const SUBSTITUTION_VARS = [
|
|
34
|
+
{ placeholder: '${x}', optionKey: 'where_x_in' },
|
|
35
|
+
{ placeholder: '${y}', optionKey: 'where_y_in' },
|
|
36
|
+
{ placeholder: '${z}', optionKey: 'where_z_in' },
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Recursively substitute a placeholder in all string values of an object.
|
|
40
|
+
* Returns a new object (shallow clone at each level) with substitutions applied.
|
|
41
|
+
*/
|
|
42
|
+
function substituteInValue(value, placeholder, replacement) {
|
|
43
|
+
if (typeof value === 'string') {
|
|
44
|
+
return value.includes(placeholder) ? value.replaceAll(placeholder, replacement) : value;
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return value.map(item => substituteInValue(item, placeholder, replacement));
|
|
48
|
+
}
|
|
49
|
+
if (value && typeof value === 'object') {
|
|
50
|
+
const proto = Object.getPrototypeOf(value);
|
|
51
|
+
if (proto === Object.prototype || proto === null) {
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const [k, v] of Object.entries(value)) {
|
|
54
|
+
result[k] = substituteInValue(v, placeholder, replacement);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check if a pattern entry (key + value) contains a given placeholder.
|
|
63
|
+
*/
|
|
64
|
+
function entryContainsPlaceholder(key, value, placeholder) {
|
|
65
|
+
if (key.includes(placeholder))
|
|
66
|
+
return true;
|
|
67
|
+
return valueContainsPlaceholder(value, placeholder);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if a value (string, object, or array) contains a placeholder.
|
|
71
|
+
*/
|
|
72
|
+
function valueContainsPlaceholder(value, placeholder) {
|
|
73
|
+
if (typeof value === 'string')
|
|
74
|
+
return value.includes(placeholder);
|
|
75
|
+
if (Array.isArray(value))
|
|
76
|
+
return value.some(item => valueContainsPlaceholder(item, placeholder));
|
|
77
|
+
if (value && typeof value === 'object') {
|
|
78
|
+
const proto = Object.getPrototypeOf(value);
|
|
79
|
+
if (proto === Object.prototype || proto === null) {
|
|
80
|
+
return Object.values(value).some(v => valueContainsPlaceholder(v, placeholder));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Expand looped substitution variables in a pattern.
|
|
87
|
+
* Applies cartesian expansion: x values are expanded first, then y, then z.
|
|
88
|
+
* Each variable multiplies the entries — result count = x.length × y.length × z.length.
|
|
89
|
+
*
|
|
90
|
+
* Returns the expanded pattern (or the original if no substitutions apply).
|
|
91
|
+
*/
|
|
92
|
+
function expandSubstitutions(pattern, options) {
|
|
93
|
+
let entries = Object.entries(pattern);
|
|
94
|
+
for (const { placeholder, optionKey } of SUBSTITUTION_VARS) {
|
|
95
|
+
const values = options[optionKey];
|
|
96
|
+
if (!values || values.length === 0)
|
|
97
|
+
continue;
|
|
98
|
+
const expanded = [];
|
|
99
|
+
for (const [key, value] of entries) {
|
|
100
|
+
if (entryContainsPlaceholder(key, value, placeholder)) {
|
|
101
|
+
// Expand this entry for each value in the variable array
|
|
102
|
+
for (const replacement of values) {
|
|
103
|
+
const newKey = key.includes(placeholder)
|
|
104
|
+
? key.replaceAll(placeholder, replacement)
|
|
105
|
+
: key;
|
|
106
|
+
const newValue = substituteInValue(value, placeholder, replacement);
|
|
107
|
+
expanded.push([newKey, newValue]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// No placeholder in this entry — pass through
|
|
112
|
+
expanded.push([key, value]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
entries = expanded;
|
|
116
|
+
}
|
|
117
|
+
return mergeHandlerDuplicates(entries);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Convert entries to an object, merging duplicate handler (` =>`) keys into arrays.
|
|
121
|
+
* For normal (non-handler) keys, later entries overwrite earlier ones (standard object behavior).
|
|
122
|
+
* For handler keys, duplicate entries are combined into an array (Multiple Handlers pattern).
|
|
123
|
+
*/
|
|
124
|
+
function mergeHandlerDuplicates(entries) {
|
|
125
|
+
const result = {};
|
|
126
|
+
for (const [key, value] of entries) {
|
|
127
|
+
if (key.endsWith(' =>') && key in result) {
|
|
128
|
+
// Duplicate handler key — merge into array
|
|
129
|
+
const existing = result[key];
|
|
130
|
+
if (Array.isArray(existing)) {
|
|
131
|
+
existing.push(value);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
result[key] = [existing, value];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
result[key] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
export async function assignFrom(target, pattern, options, permissions) {
|
|
144
|
+
// First: expand looped substitution variables (${x}, ${y}, ${z})
|
|
145
|
+
const expandedPattern = expandSubstitutions(pattern, options);
|
|
146
|
+
// Separate handler commands ( =>), #[x] keys, and normal keys
|
|
147
|
+
const handlerKeys = [];
|
|
148
|
+
const normalPattern = {};
|
|
149
|
+
const idRefNormalKeys = [];
|
|
150
|
+
const idRefHandlerKeys = [];
|
|
151
|
+
for (const key of Object.keys(expandedPattern)) {
|
|
152
|
+
if (isHandlerCommand(key)) {
|
|
153
|
+
if (key.startsWith('#[')) {
|
|
154
|
+
idRefHandlerKeys.push(key);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
handlerKeys.push(key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else if (key.startsWith('#[')) {
|
|
161
|
+
idRefNormalKeys.push(key);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
normalPattern[key] = expandedPattern[key];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Process normal keys via resolveValues + assignGingerly
|
|
168
|
+
if (Object.keys(normalPattern).length > 0) {
|
|
169
|
+
const resolved = await resolveValues(normalPattern, options.from, {
|
|
170
|
+
withMethods: options.withMethods,
|
|
171
|
+
aka: options.aka,
|
|
172
|
+
protocols: options.protocols
|
|
173
|
+
});
|
|
174
|
+
// Recursively handle "..." spread keys at all nesting levels
|
|
175
|
+
handleSpreads(resolved);
|
|
176
|
+
assignGingerly(target, resolved, options);
|
|
177
|
+
}
|
|
178
|
+
// Process #[x] normal keys — resolve element, then apply remaining path + value
|
|
179
|
+
if (idRefNormalKeys.length > 0 && options.withIds) {
|
|
180
|
+
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
181
|
+
for (const key of idRefNormalKeys) {
|
|
182
|
+
const parsed = parseIdRef(key);
|
|
183
|
+
if (!parsed)
|
|
184
|
+
continue;
|
|
185
|
+
const el = resolveIdVariable(parsed.varName, target, options.withIds);
|
|
186
|
+
if (!el)
|
|
187
|
+
continue;
|
|
188
|
+
const value = expandedPattern[key];
|
|
189
|
+
if (parsed.remainingPath) {
|
|
190
|
+
// Resolve the RHS value
|
|
191
|
+
const resolvedValue = await resolveValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
|
|
192
|
+
// Apply remaining path on the resolved element
|
|
193
|
+
assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// No remaining path — resolve and assign directly to the element
|
|
197
|
+
const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
|
|
198
|
+
if ('__v' in resolvedValue) {
|
|
199
|
+
// Single value — can't assign to element root without a path
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
assignGingerly(el, resolvedValue, options);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Process handler commands ( =>) — dynamically imported only when needed
|
|
208
|
+
if (handlerKeys.length > 0) {
|
|
209
|
+
const { processHandlerCommands } = await import('./processHandlerCommands.js');
|
|
210
|
+
await processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
|
|
211
|
+
}
|
|
212
|
+
// Process #[x] handler keys — resolve element, then pass to handler processing
|
|
213
|
+
if (idRefHandlerKeys.length > 0 && options.withIds) {
|
|
214
|
+
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
215
|
+
const { processHandlerCommands } = await import('./processHandlerCommands.js');
|
|
216
|
+
for (const key of idRefHandlerKeys) {
|
|
217
|
+
const parsed = parseIdRef(key);
|
|
218
|
+
if (!parsed)
|
|
219
|
+
continue;
|
|
220
|
+
const el = resolveIdVariable(parsed.varName, target, options.withIds);
|
|
221
|
+
if (!el)
|
|
222
|
+
continue;
|
|
223
|
+
// Build a synthetic key for processHandlerCommands:
|
|
224
|
+
// The resolved element becomes the target, remaining path is the LHS
|
|
225
|
+
const syntheticKey = parsed.remainingPath
|
|
226
|
+
? `${parsed.remainingPath} =>`
|
|
227
|
+
: ' =>';
|
|
228
|
+
const syntheticPattern = {
|
|
229
|
+
[syntheticKey]: expandedPattern[key]
|
|
230
|
+
};
|
|
231
|
+
await processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Process inferred assignments — dynamically imported only when option is present
|
|
235
|
+
if (options.inferredAssignments) {
|
|
236
|
+
const { processInferredAssignments } = await import('./inferredAssignments.js');
|
|
237
|
+
await processInferredAssignments(target, options.from, options.inferredAssignments);
|
|
238
|
+
// Set up MutationObserver for new matching elements if beVigilant
|
|
239
|
+
if (options.inferredAssignments.beVigilant) {
|
|
240
|
+
if (!options.signal) {
|
|
241
|
+
throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
|
|
242
|
+
}
|
|
243
|
+
const { setupVigilantObserver } = await import('./beVigilant.js');
|
|
244
|
+
setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Process bulk enhancements — dynamically imported only when option is present
|
|
248
|
+
if (options.enhance && options.enhance.length > 0) {
|
|
249
|
+
const { enhanceAll } = await import('./enhanceAll.js');
|
|
250
|
+
await enhanceAll(target, options.enhance, permissions);
|
|
251
|
+
}
|
|
252
|
+
return target;
|
|
33
253
|
}
|
|
34
254
|
/**
|
|
35
255
|
* Recursively walk an object and handle "..." spread keys.
|
package/assignFrom.ts
CHANGED
|
@@ -21,27 +21,356 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { resolveValues, ResolveValuesOptions } from './resolveValues.js';
|
|
23
23
|
import assignGingerly, { IAssignGingerlyOptions } from './assignGingerly.js';
|
|
24
|
+
import type { AssignPermissions } from './isAllowedImportPath.js';
|
|
24
25
|
|
|
25
26
|
export interface AssignFromOptions extends IAssignGingerlyOptions, ResolveValuesOptions {
|
|
26
27
|
/** Source object to resolve RHS path strings against */
|
|
27
28
|
from: any;
|
|
29
|
+
|
|
30
|
+
/** Loop variable bindings — expand pattern entries containing ${x} */
|
|
31
|
+
where_x_in?: string[];
|
|
32
|
+
/** Loop variable bindings — expand pattern entries containing ${y} */
|
|
33
|
+
where_y_in?: string[];
|
|
34
|
+
/** Loop variable bindings — expand pattern entries containing ${z} */
|
|
35
|
+
where_z_in?: string[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Cached element references by variable name.
|
|
39
|
+
* Used with `#[varName]` syntax in LHS keys for fast repeated element access.
|
|
40
|
+
*
|
|
41
|
+
* - String value: existing element ID (uses getElementById)
|
|
42
|
+
* - Object value: { qry: 'selector' } — finds element via querySelector on target, auto-assigns an ID
|
|
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 }>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Interface for assignFrom handler classes.
|
|
107
|
+
* Handlers are invoked when a LHS key ends with ' =>'.
|
|
108
|
+
*/
|
|
109
|
+
export interface AssignFromHandler {
|
|
110
|
+
assign(lhsTarget: any, resolvedParams: Record<string, any>, options: AssignFromOptions): Promise<void> | void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface AssignFromHandlerConstructor {
|
|
114
|
+
new (config: any): AssignFromHandler;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a key ends with the handler operator ' =>'.
|
|
119
|
+
*/
|
|
120
|
+
function isHandlerCommand(key: string): boolean {
|
|
121
|
+
return key.endsWith(' =>');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Supported substitution variables and their option keys.
|
|
126
|
+
*/
|
|
127
|
+
const SUBSTITUTION_VARS = [
|
|
128
|
+
{ placeholder: '${x}', optionKey: 'where_x_in' },
|
|
129
|
+
{ placeholder: '${y}', optionKey: 'where_y_in' },
|
|
130
|
+
{ placeholder: '${z}', optionKey: 'where_z_in' },
|
|
131
|
+
] as const;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 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
|
+
*/
|
|
137
|
+
function substituteInValue(value: any, placeholder: string, replacement: string): any {
|
|
138
|
+
if (typeof value === 'string') {
|
|
139
|
+
return value.includes(placeholder) ? value.replaceAll(placeholder, replacement) : value;
|
|
140
|
+
}
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
return value.map(item => substituteInValue(item, placeholder, replacement));
|
|
143
|
+
}
|
|
144
|
+
if (value && typeof value === 'object') {
|
|
145
|
+
const proto = Object.getPrototypeOf(value);
|
|
146
|
+
if (proto === Object.prototype || proto === null) {
|
|
147
|
+
const result: Record<string, any> = {};
|
|
148
|
+
for (const [k, v] of Object.entries(value)) {
|
|
149
|
+
result[k] = substituteInValue(v, placeholder, replacement);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if a pattern entry (key + value) contains a given placeholder.
|
|
159
|
+
*/
|
|
160
|
+
function entryContainsPlaceholder(key: string, value: any, placeholder: string): boolean {
|
|
161
|
+
if (key.includes(placeholder)) return true;
|
|
162
|
+
return valueContainsPlaceholder(value, placeholder);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if a value (string, object, or array) contains a placeholder.
|
|
167
|
+
*/
|
|
168
|
+
function valueContainsPlaceholder(value: any, placeholder: string): boolean {
|
|
169
|
+
if (typeof value === 'string') return value.includes(placeholder);
|
|
170
|
+
if (Array.isArray(value)) return value.some(item => valueContainsPlaceholder(item, placeholder));
|
|
171
|
+
if (value && typeof value === 'object') {
|
|
172
|
+
const proto = Object.getPrototypeOf(value);
|
|
173
|
+
if (proto === Object.prototype || proto === null) {
|
|
174
|
+
return Object.values(value).some(v => valueContainsPlaceholder(v, placeholder));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 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
|
+
*/
|
|
187
|
+
function expandSubstitutions(
|
|
188
|
+
pattern: Record<string, any>,
|
|
189
|
+
options: AssignFromOptions
|
|
190
|
+
): Record<string, any> {
|
|
191
|
+
let entries = Object.entries(pattern);
|
|
192
|
+
|
|
193
|
+
for (const { placeholder, optionKey } of SUBSTITUTION_VARS) {
|
|
194
|
+
const values = options[optionKey as keyof AssignFromOptions] as string[] | undefined;
|
|
195
|
+
if (!values || values.length === 0) continue;
|
|
196
|
+
|
|
197
|
+
const expanded: [string, any][] = [];
|
|
198
|
+
for (const [key, value] of entries) {
|
|
199
|
+
if (entryContainsPlaceholder(key, value, placeholder)) {
|
|
200
|
+
// Expand this entry for each value in the variable array
|
|
201
|
+
for (const replacement of values) {
|
|
202
|
+
const newKey = key.includes(placeholder)
|
|
203
|
+
? key.replaceAll(placeholder, replacement)
|
|
204
|
+
: key;
|
|
205
|
+
const newValue = substituteInValue(value, placeholder, replacement);
|
|
206
|
+
expanded.push([newKey, newValue]);
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// No placeholder in this entry — pass through
|
|
210
|
+
expanded.push([key, value]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
entries = expanded;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return mergeHandlerDuplicates(entries);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 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
|
+
*/
|
|
224
|
+
function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
|
|
225
|
+
const result: Record<string, any> = {};
|
|
226
|
+
for (const [key, value] of entries) {
|
|
227
|
+
if (key.endsWith(' =>') && key in result) {
|
|
228
|
+
// Duplicate handler key — merge into array
|
|
229
|
+
const existing = result[key];
|
|
230
|
+
if (Array.isArray(existing)) {
|
|
231
|
+
existing.push(value);
|
|
232
|
+
} else {
|
|
233
|
+
result[key] = [existing, value];
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
result[key] = value;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
28
240
|
}
|
|
29
241
|
|
|
30
242
|
export async function assignFrom(
|
|
31
243
|
target: any,
|
|
32
244
|
pattern: Record<string, any>,
|
|
33
|
-
options: AssignFromOptions
|
|
245
|
+
options: AssignFromOptions,
|
|
246
|
+
permissions?: AssignPermissions
|
|
34
247
|
): Promise<any> {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
248
|
+
// First: expand looped substitution variables (${x}, ${y}, ${z})
|
|
249
|
+
const expandedPattern = expandSubstitutions(pattern, options);
|
|
250
|
+
|
|
251
|
+
// Separate handler commands ( =>), #[x] keys, and normal keys
|
|
252
|
+
const handlerKeys: string[] = [];
|
|
253
|
+
const normalPattern: Record<string, any> = {};
|
|
254
|
+
const idRefNormalKeys: string[] = [];
|
|
255
|
+
const idRefHandlerKeys: string[] = [];
|
|
256
|
+
|
|
257
|
+
for (const key of Object.keys(expandedPattern)) {
|
|
258
|
+
if (isHandlerCommand(key)) {
|
|
259
|
+
if (key.startsWith('#[')) {
|
|
260
|
+
idRefHandlerKeys.push(key);
|
|
261
|
+
} else {
|
|
262
|
+
handlerKeys.push(key);
|
|
263
|
+
}
|
|
264
|
+
} else if (key.startsWith('#[')) {
|
|
265
|
+
idRefNormalKeys.push(key);
|
|
266
|
+
} else {
|
|
267
|
+
normalPattern[key] = expandedPattern[key];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Process normal keys via resolveValues + assignGingerly
|
|
272
|
+
if (Object.keys(normalPattern).length > 0) {
|
|
273
|
+
const resolved = await resolveValues(normalPattern, options.from, {
|
|
274
|
+
withMethods: options.withMethods,
|
|
275
|
+
aka: options.aka,
|
|
276
|
+
protocols: options.protocols
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Recursively handle "..." spread keys at all nesting levels
|
|
280
|
+
handleSpreads(resolved);
|
|
281
|
+
|
|
282
|
+
assignGingerly(target, resolved, options);
|
|
283
|
+
}
|
|
40
284
|
|
|
41
|
-
//
|
|
42
|
-
|
|
285
|
+
// Process #[x] normal keys — resolve element, then apply remaining path + value
|
|
286
|
+
if (idRefNormalKeys.length > 0 && options.withIds) {
|
|
287
|
+
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
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
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Process handler commands ( =>) — dynamically imported only when needed
|
|
321
|
+
if (handlerKeys.length > 0) {
|
|
322
|
+
const { processHandlerCommands } = await import('./processHandlerCommands.js');
|
|
323
|
+
await processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Process #[x] handler keys — resolve element, then pass to handler processing
|
|
327
|
+
if (idRefHandlerKeys.length > 0 && options.withIds) {
|
|
328
|
+
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
329
|
+
const { processHandlerCommands } = await import('./processHandlerCommands.js');
|
|
330
|
+
|
|
331
|
+
for (const key of idRefHandlerKeys) {
|
|
332
|
+
const parsed = parseIdRef(key);
|
|
333
|
+
if (!parsed) continue;
|
|
334
|
+
|
|
335
|
+
const el = resolveIdVariable(parsed.varName, target, options.withIds);
|
|
336
|
+
if (!el) continue;
|
|
337
|
+
|
|
338
|
+
// Build a synthetic key for processHandlerCommands:
|
|
339
|
+
// The resolved element becomes the target, remaining path is the LHS
|
|
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
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Process inferred assignments — dynamically imported only when option is present
|
|
353
|
+
if (options.inferredAssignments) {
|
|
354
|
+
const { processInferredAssignments } = await import('./inferredAssignments.js');
|
|
355
|
+
await processInferredAssignments(target, options.from, options.inferredAssignments);
|
|
356
|
+
|
|
357
|
+
// Set up MutationObserver for new matching elements if beVigilant
|
|
358
|
+
if (options.inferredAssignments.beVigilant) {
|
|
359
|
+
if (!options.signal) {
|
|
360
|
+
throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
|
|
361
|
+
}
|
|
362
|
+
const { setupVigilantObserver } = await import('./beVigilant.js');
|
|
363
|
+
setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Process bulk enhancements — dynamically imported only when option is present
|
|
368
|
+
if (options.enhance && options.enhance.length > 0) {
|
|
369
|
+
const { enhanceAll } = await import('./enhanceAll.js');
|
|
370
|
+
await enhanceAll(target, options.enhance, permissions);
|
|
371
|
+
}
|
|
43
372
|
|
|
44
|
-
return
|
|
373
|
+
return target;
|
|
45
374
|
}
|
|
46
375
|
|
|
47
376
|
/**
|
package/assignGingerly.js
CHANGED
|
@@ -42,6 +42,21 @@ export class EnhancementRegistry extends EventTarget {
|
|
|
42
42
|
}
|
|
43
43
|
// Dispatch event after adding items
|
|
44
44
|
this.dispatchEvent(new EnhancementRegisteredEvent(items));
|
|
45
|
+
// Process features if present (fire-and-forget, uses shared featuresRegistry)
|
|
46
|
+
const itemsArr = Array.isArray(items) ? items : [items];
|
|
47
|
+
for (const item of itemsArr) {
|
|
48
|
+
if (item.features) {
|
|
49
|
+
this.#assignFeatures(item.spawn, item.features);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async #assignFeatures(spawn, features) {
|
|
54
|
+
const { assignFeatures } = await import('./assignFeatures.js');
|
|
55
|
+
const featuresRegistry = this._featuresRegistry
|
|
56
|
+
?? (typeof customElements !== 'undefined' ? customElements.featuresRegistry : undefined);
|
|
57
|
+
if (featuresRegistry) {
|
|
58
|
+
assignFeatures(spawn, features, featuresRegistry);
|
|
59
|
+
}
|
|
45
60
|
}
|
|
46
61
|
getItems() {
|
|
47
62
|
return Array.from(this.#items);
|
|
@@ -89,6 +104,18 @@ export class ItemscopeRegistry extends EventTarget {
|
|
|
89
104
|
}
|
|
90
105
|
this.#configs.set(name, config);
|
|
91
106
|
this.dispatchEvent(new Event(name));
|
|
107
|
+
// Process features if present (fire-and-forget, uses shared featuresRegistry)
|
|
108
|
+
if (config.features) {
|
|
109
|
+
this.#assignFeatures(config.manager, config.features);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async #assignFeatures(manager, features) {
|
|
113
|
+
const { assignFeatures } = await import('./assignFeatures.js');
|
|
114
|
+
const featuresRegistry = this._featuresRegistry
|
|
115
|
+
?? (typeof customElements !== 'undefined' ? customElements.featuresRegistry : undefined);
|
|
116
|
+
if (featuresRegistry) {
|
|
117
|
+
assignFeatures(manager, features, featuresRegistry);
|
|
118
|
+
}
|
|
92
119
|
}
|
|
93
120
|
/**
|
|
94
121
|
* Get a manager configuration by name
|
|
@@ -515,7 +542,7 @@ function applyAliases(key, aliasMap) {
|
|
|
515
542
|
/**
|
|
516
543
|
* Main assignGingerly function
|
|
517
544
|
*/
|
|
518
|
-
export function assignGingerly(target, source, options) {
|
|
545
|
+
export function assignGingerly(target, source, options, permissions) {
|
|
519
546
|
if (!target || typeof target !== 'object') {
|
|
520
547
|
return target;
|
|
521
548
|
}
|
|
@@ -1059,6 +1086,12 @@ export function assignGingerly(target, source, options) {
|
|
|
1059
1086
|
configurable: true,
|
|
1060
1087
|
});
|
|
1061
1088
|
}
|
|
1089
|
+
// Fire-and-forget bulk enhancements (async, non-blocking)
|
|
1090
|
+
if (options?.enhance && options.enhance.length > 0 && typeof target === 'object' && target instanceof Element) {
|
|
1091
|
+
import('./enhanceAll.js').then(({ enhanceAll }) => {
|
|
1092
|
+
enhanceAll(target, options.enhance, permissions);
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1062
1095
|
return target;
|
|
1063
1096
|
}
|
|
1064
1097
|
export default assignGingerly;
|