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/assignGingerly.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
import { EnhancementConfig } from "./types/assign-gingerly/types";
|
|
4
|
+
import type { FeatureConfigsMap } from "./types/assign-gingerly/types";
|
|
5
|
+
import type { AssignPermissions } from "./isAllowedImportPath.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Constructor signature for ItemScope Manager classes
|
|
@@ -116,6 +118,18 @@ export interface IAssignGingerlyOptions {
|
|
|
116
118
|
* // Calls: (await el.whenFeatureReady('photoTaker')).someProp = 'hello'
|
|
117
119
|
*/
|
|
118
120
|
withAsyncMethods?: string[] | Set<string>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Bulk enhancement application via EMC JSON configs.
|
|
124
|
+
* Finds matching elements and spawns enhancements on them.
|
|
125
|
+
* Fire-and-forget (async) — assignGingerly remains synchronous.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* enhance: [
|
|
129
|
+
* { emc: 'be-bound/emc.json', matching: '[name]' },
|
|
130
|
+
* ]
|
|
131
|
+
*/
|
|
132
|
+
enhance?: Array<{ emc: string; matching?: string; parse?: boolean }>;
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
/**
|
|
@@ -167,6 +181,23 @@ export class EnhancementRegistry extends EventTarget {
|
|
|
167
181
|
|
|
168
182
|
// Dispatch event after adding items
|
|
169
183
|
this.dispatchEvent(new EnhancementRegisteredEvent(items));
|
|
184
|
+
|
|
185
|
+
// Process features if present (fire-and-forget, uses shared featuresRegistry)
|
|
186
|
+
const itemsArr = Array.isArray(items) ? items : [items];
|
|
187
|
+
for (const item of itemsArr) {
|
|
188
|
+
if (item.features) {
|
|
189
|
+
this.#assignFeatures(item.spawn, item.features);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async #assignFeatures(spawn: any, features: FeatureConfigsMap): Promise<void> {
|
|
195
|
+
const { assignFeatures } = await import('./assignFeatures.js');
|
|
196
|
+
const featuresRegistry = (this as any)._featuresRegistry
|
|
197
|
+
?? (typeof customElements !== 'undefined' ? (customElements as any).featuresRegistry : undefined);
|
|
198
|
+
if (featuresRegistry) {
|
|
199
|
+
assignFeatures(spawn, features, featuresRegistry);
|
|
200
|
+
}
|
|
170
201
|
}
|
|
171
202
|
|
|
172
203
|
getItems(): EnhancementConfig[] {
|
|
@@ -218,6 +249,20 @@ export class ItemscopeRegistry extends EventTarget {
|
|
|
218
249
|
}
|
|
219
250
|
this.#configs.set(name, config);
|
|
220
251
|
this.dispatchEvent(new Event(name));
|
|
252
|
+
|
|
253
|
+
// Process features if present (fire-and-forget, uses shared featuresRegistry)
|
|
254
|
+
if ((config as any).features) {
|
|
255
|
+
this.#assignFeatures(config.manager, (config as any).features);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async #assignFeatures(manager: any, features: FeatureConfigsMap): Promise<void> {
|
|
260
|
+
const { assignFeatures } = await import('./assignFeatures.js');
|
|
261
|
+
const featuresRegistry = (this as any)._featuresRegistry
|
|
262
|
+
?? (typeof customElements !== 'undefined' ? (customElements as any).featuresRegistry : undefined);
|
|
263
|
+
if (featuresRegistry) {
|
|
264
|
+
assignFeatures(manager, features, featuresRegistry);
|
|
265
|
+
}
|
|
221
266
|
}
|
|
222
267
|
|
|
223
268
|
/**
|
|
@@ -691,7 +736,8 @@ function applyAliases(key: string, aliasMap: Map<string, string>): string {
|
|
|
691
736
|
export function assignGingerly(
|
|
692
737
|
target: any,
|
|
693
738
|
source: Record<string | symbol, any>,
|
|
694
|
-
options?: IAssignGingerlyOptions
|
|
739
|
+
options?: IAssignGingerlyOptions,
|
|
740
|
+
permissions?: AssignPermissions
|
|
695
741
|
): any {
|
|
696
742
|
if (!target || typeof target !== 'object') {
|
|
697
743
|
return target;
|
|
@@ -1274,6 +1320,13 @@ export function assignGingerly(
|
|
|
1274
1320
|
});
|
|
1275
1321
|
}
|
|
1276
1322
|
|
|
1323
|
+
// Fire-and-forget bulk enhancements (async, non-blocking)
|
|
1324
|
+
if (options?.enhance && options.enhance.length > 0 && typeof target === 'object' && target instanceof Element) {
|
|
1325
|
+
import('./enhanceAll.js').then(({ enhanceAll }) => {
|
|
1326
|
+
enhanceAll(target, options!.enhance!, permissions);
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1277
1330
|
return target;
|
|
1278
1331
|
}
|
|
1279
1332
|
|
package/beVigilant.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* beVigilant.ts — MutationObserver-based reactive inference for newly added elements.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imported when `inferredAssignments.beVigilant: true` is set.
|
|
5
|
+
* Watches for new [itemprop] elements and attribute changes, applying inferred
|
|
6
|
+
* assignments from the live `from` object.
|
|
7
|
+
*
|
|
8
|
+
* Requires an AbortSignal for cleanup (disconnects the observer on abort).
|
|
9
|
+
*/
|
|
10
|
+
import { Infer } from './inferencer/inferencer.js';
|
|
11
|
+
import { withScopePerimeter } from './inferencer/withScopePerimeter.js';
|
|
12
|
+
import { registerInCache } from './resolveIdRef.js';
|
|
13
|
+
/**
|
|
14
|
+
* Set up a MutationObserver that watches for new [itemprop] elements
|
|
15
|
+
* and applies inferred assignments from the live `from` object.
|
|
16
|
+
*
|
|
17
|
+
* @param target - The DOM element to observe
|
|
18
|
+
* @param from - The live source object (read at observation time for current values)
|
|
19
|
+
* @param config - The inferredAssignments config (byItemprop keys)
|
|
20
|
+
* @param signal - AbortSignal for cleanup (required)
|
|
21
|
+
*/
|
|
22
|
+
export function setupVigilantObserver(target, from, config, signal) {
|
|
23
|
+
const keys = config.byItemprop === true ? null : new Set(config.byItemprop);
|
|
24
|
+
const observer = new MutationObserver((mutations) => {
|
|
25
|
+
for (const mutation of mutations) {
|
|
26
|
+
if (mutation.type === 'childList') {
|
|
27
|
+
// Process added nodes and their descendants
|
|
28
|
+
for (const node of mutation.addedNodes) {
|
|
29
|
+
if (!(node instanceof Element))
|
|
30
|
+
continue;
|
|
31
|
+
// Check the added element itself
|
|
32
|
+
processElement(node, from, keys, target);
|
|
33
|
+
// Check descendants (respecting scope perimeter)
|
|
34
|
+
const nested = node.querySelectorAll('[itemprop]');
|
|
35
|
+
for (const el of nested) {
|
|
36
|
+
processElement(el, from, keys, target);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (mutation.type === 'attributes' && mutation.target instanceof Element) {
|
|
41
|
+
// itemprop attribute was added or changed on an existing element
|
|
42
|
+
processElement(mutation.target, from, keys, target);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
observer.observe(target, {
|
|
47
|
+
childList: true,
|
|
48
|
+
subtree: true,
|
|
49
|
+
attributes: true,
|
|
50
|
+
attributeFilter: ['itemprop']
|
|
51
|
+
});
|
|
52
|
+
// Cleanup on abort
|
|
53
|
+
signal.addEventListener('abort', () => observer.disconnect(), { once: true });
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Process a single element: check if it matches, apply inferred value, cache it.
|
|
57
|
+
*/
|
|
58
|
+
function processElement(element, from, keys, scopeRoot) {
|
|
59
|
+
const itemprop = element.getAttribute('itemprop');
|
|
60
|
+
if (!itemprop)
|
|
61
|
+
return;
|
|
62
|
+
if (keys && !keys.has(itemprop))
|
|
63
|
+
return;
|
|
64
|
+
if (!(itemprop in from))
|
|
65
|
+
return;
|
|
66
|
+
if (!withScopePerimeter(scopeRoot, element, '[itemscope]'))
|
|
67
|
+
return;
|
|
68
|
+
// Set value via inferencer
|
|
69
|
+
const infer = new Infer(element, itemprop);
|
|
70
|
+
infer.value = from[itemprop];
|
|
71
|
+
// Cache for future fast access
|
|
72
|
+
registerInCache(scopeRoot, itemprop, element);
|
|
73
|
+
}
|
package/beVigilant.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* beVigilant.ts — MutationObserver-based reactive inference for newly added elements.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imported when `inferredAssignments.beVigilant: true` is set.
|
|
5
|
+
* Watches for new [itemprop] elements and attribute changes, applying inferred
|
|
6
|
+
* assignments from the live `from` object.
|
|
7
|
+
*
|
|
8
|
+
* Requires an AbortSignal for cleanup (disconnects the observer on abort).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Infer } from './inferencer/inferencer.js';
|
|
12
|
+
import { withScopePerimeter } from './inferencer/withScopePerimeter.js';
|
|
13
|
+
import { registerInCache } from './resolveIdRef.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Set up a MutationObserver that watches for new [itemprop] elements
|
|
17
|
+
* and applies inferred assignments from the live `from` object.
|
|
18
|
+
*
|
|
19
|
+
* @param target - The DOM element to observe
|
|
20
|
+
* @param from - The live source object (read at observation time for current values)
|
|
21
|
+
* @param config - The inferredAssignments config (byItemprop keys)
|
|
22
|
+
* @param signal - AbortSignal for cleanup (required)
|
|
23
|
+
*/
|
|
24
|
+
export function setupVigilantObserver(
|
|
25
|
+
target: Element,
|
|
26
|
+
from: any,
|
|
27
|
+
config: { byItemprop?: string[] | true },
|
|
28
|
+
signal: AbortSignal
|
|
29
|
+
): void {
|
|
30
|
+
const keys = config.byItemprop === true ? null : new Set(config.byItemprop);
|
|
31
|
+
|
|
32
|
+
const observer = new MutationObserver((mutations) => {
|
|
33
|
+
for (const mutation of mutations) {
|
|
34
|
+
if (mutation.type === 'childList') {
|
|
35
|
+
// Process added nodes and their descendants
|
|
36
|
+
for (const node of mutation.addedNodes) {
|
|
37
|
+
if (!(node instanceof Element)) continue;
|
|
38
|
+
// Check the added element itself
|
|
39
|
+
processElement(node, from, keys, target);
|
|
40
|
+
// Check descendants (respecting scope perimeter)
|
|
41
|
+
const nested = node.querySelectorAll('[itemprop]');
|
|
42
|
+
for (const el of nested) {
|
|
43
|
+
processElement(el as Element, from, keys, target);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else if (mutation.type === 'attributes' && mutation.target instanceof Element) {
|
|
47
|
+
// itemprop attribute was added or changed on an existing element
|
|
48
|
+
processElement(mutation.target, from, keys, target);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
observer.observe(target, {
|
|
54
|
+
childList: true,
|
|
55
|
+
subtree: true,
|
|
56
|
+
attributes: true,
|
|
57
|
+
attributeFilter: ['itemprop']
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Cleanup on abort
|
|
61
|
+
signal.addEventListener('abort', () => observer.disconnect(), { once: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Process a single element: check if it matches, apply inferred value, cache it.
|
|
66
|
+
*/
|
|
67
|
+
function processElement(
|
|
68
|
+
element: Element,
|
|
69
|
+
from: any,
|
|
70
|
+
keys: Set<string> | null,
|
|
71
|
+
scopeRoot: Element
|
|
72
|
+
): void {
|
|
73
|
+
const itemprop = element.getAttribute('itemprop');
|
|
74
|
+
if (!itemprop) return;
|
|
75
|
+
if (keys && !keys.has(itemprop)) return;
|
|
76
|
+
if (!(itemprop in from)) return;
|
|
77
|
+
if (!withScopePerimeter(scopeRoot, element, '[itemscope]')) return;
|
|
78
|
+
|
|
79
|
+
// Set value via inferencer
|
|
80
|
+
const infer = new Infer(element, itemprop);
|
|
81
|
+
infer.value = from[itemprop];
|
|
82
|
+
|
|
83
|
+
// Cache for future fast access
|
|
84
|
+
registerInCache(scopeRoot, itemprop, element);
|
|
85
|
+
}
|
package/enhanceAll.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enhanceAll.ts — Bulk enhancement application via EMC (Element Mount Configuration) JSON.
|
|
3
|
+
*
|
|
4
|
+
* Finds matching elements within a target and spawns enhancements on them
|
|
5
|
+
* using the configuration from EMC JSON files that enhancement packages publish.
|
|
6
|
+
*
|
|
7
|
+
* Dynamically imported by assignFrom when `enhance` option is present,
|
|
8
|
+
* or used standalone.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { enhanceAll } from 'assign-gingerly/enhanceAll.js';
|
|
12
|
+
*
|
|
13
|
+
* await enhanceAll(shadowRoot, [
|
|
14
|
+
* { emc: 'be-bound/emc.json', matching: '[name]' },
|
|
15
|
+
* { emc: 'be-observant/emc.json', matching: '[itemprop]' },
|
|
16
|
+
* ]);
|
|
17
|
+
*/
|
|
18
|
+
import { isAllowedImportPath } from './isAllowedImportPath.js';
|
|
19
|
+
/**
|
|
20
|
+
* Apply enhancements in bulk to matching elements within a target.
|
|
21
|
+
*
|
|
22
|
+
* For each entry:
|
|
23
|
+
* 1. Dynamically imports the EMC JSON
|
|
24
|
+
* 2. Extracts enhConfig (spawn path, enhKey, withAttrs)
|
|
25
|
+
* 3. Finds matching elements via querySelectorAll
|
|
26
|
+
* 4. Registers the enhancement if not already registered
|
|
27
|
+
* 5. Spawns the enhancement on each matched element via enh.get()
|
|
28
|
+
*
|
|
29
|
+
* Note: No scope perimeter is applied. For reactive observation of new elements,
|
|
30
|
+
* use mount-observer instead.
|
|
31
|
+
*
|
|
32
|
+
* @param target - The DOM element to search within
|
|
33
|
+
* @param configs - Array of enhancement configurations
|
|
34
|
+
*/
|
|
35
|
+
export async function enhanceAll(target, configs, permissions) {
|
|
36
|
+
for (const config of configs) {
|
|
37
|
+
// Validate EMC path unless cross-domain imports are explicitly permitted
|
|
38
|
+
if (!permissions?.crossDomainImports && !isAllowedImportPath(config.emc)) {
|
|
39
|
+
throw new Error(`enhanceAll: EMC path "${config.emc}" is a cross-domain URL. ` +
|
|
40
|
+
`Only relative, absolute, or bare specifier paths are allowed by default. ` +
|
|
41
|
+
`Pass { crossDomainImports: true } in permissions to override.`);
|
|
42
|
+
}
|
|
43
|
+
// 1. Import the EMC JSON
|
|
44
|
+
const emcModule = await import(config.emc, { with: { type: 'json' } });
|
|
45
|
+
const emc = emcModule.default ?? emcModule;
|
|
46
|
+
const { enhConfig } = emc;
|
|
47
|
+
if (!enhConfig) {
|
|
48
|
+
throw new Error(`enhanceAll: EMC at "${config.emc}" does not contain an enhConfig field`);
|
|
49
|
+
}
|
|
50
|
+
// 2. Determine the selector
|
|
51
|
+
const selector = config.matching ?? enhConfig.matching ?? '*';
|
|
52
|
+
// 3. Find matching elements
|
|
53
|
+
const elements = target.querySelectorAll(selector);
|
|
54
|
+
if (elements.length === 0)
|
|
55
|
+
continue;
|
|
56
|
+
// 4. Resolve and register the enhancement
|
|
57
|
+
const registryItem = await resolveAndRegister(enhConfig, target);
|
|
58
|
+
if (!registryItem)
|
|
59
|
+
continue;
|
|
60
|
+
// 5. Spawn enhancement on each matched element
|
|
61
|
+
for (const el of elements) {
|
|
62
|
+
try {
|
|
63
|
+
el.enh.get(registryItem);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// If enh gateway isn't available, skip silently
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the enhancement's registry item from EMC enhConfig.
|
|
73
|
+
* If not already registered, dynamically imports the spawn module and registers it.
|
|
74
|
+
*/
|
|
75
|
+
async function resolveAndRegister(enhConfig, target) {
|
|
76
|
+
const { enhKey, spawn: spawnPath } = enhConfig;
|
|
77
|
+
// Get the enhancement registry for this element's scope
|
|
78
|
+
const registry = target.customElementRegistry?.enhancementRegistry
|
|
79
|
+
?? (typeof customElements !== 'undefined' ? customElements.enhancementRegistry : undefined);
|
|
80
|
+
if (!registry)
|
|
81
|
+
return null;
|
|
82
|
+
// Check if already registered
|
|
83
|
+
if (enhKey) {
|
|
84
|
+
const existing = registry.findByEnhKey(enhKey);
|
|
85
|
+
if (existing)
|
|
86
|
+
return existing;
|
|
87
|
+
}
|
|
88
|
+
// Not registered — dynamically import the spawn module
|
|
89
|
+
if (!spawnPath)
|
|
90
|
+
return null;
|
|
91
|
+
const spawnModule = await import(spawnPath);
|
|
92
|
+
const SpawnClass = spawnModule.default ?? Object.values(spawnModule).find((v) => typeof v === 'function' && v.prototype);
|
|
93
|
+
if (!SpawnClass)
|
|
94
|
+
return null;
|
|
95
|
+
// Build and register the registry item
|
|
96
|
+
const registryItem = {
|
|
97
|
+
spawn: SpawnClass,
|
|
98
|
+
enhKey,
|
|
99
|
+
};
|
|
100
|
+
// Include withAttrs if present in enhConfig
|
|
101
|
+
if (enhConfig.withAttrs) {
|
|
102
|
+
registryItem.withAttrs = enhConfig.withAttrs;
|
|
103
|
+
}
|
|
104
|
+
registry.push(registryItem);
|
|
105
|
+
return registryItem;
|
|
106
|
+
}
|
package/enhanceAll.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enhanceAll.ts — Bulk enhancement application via EMC (Element Mount Configuration) JSON.
|
|
3
|
+
*
|
|
4
|
+
* Finds matching elements within a target and spawns enhancements on them
|
|
5
|
+
* using the configuration from EMC JSON files that enhancement packages publish.
|
|
6
|
+
*
|
|
7
|
+
* Dynamically imported by assignFrom when `enhance` option is present,
|
|
8
|
+
* or used standalone.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { enhanceAll } from 'assign-gingerly/enhanceAll.js';
|
|
12
|
+
*
|
|
13
|
+
* await enhanceAll(shadowRoot, [
|
|
14
|
+
* { emc: 'be-bound/emc.json', matching: '[name]' },
|
|
15
|
+
* { emc: 'be-observant/emc.json', matching: '[itemprop]' },
|
|
16
|
+
* ]);
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { isAllowedImportPath } from './isAllowedImportPath.js';
|
|
20
|
+
import type { AssignPermissions } from './isAllowedImportPath.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for a single enhancement to apply in bulk.
|
|
24
|
+
*/
|
|
25
|
+
export interface EnhanceConfig {
|
|
26
|
+
/** Path to the EMC JSON file (dynamically imported) */
|
|
27
|
+
emc: string;
|
|
28
|
+
/** Override the CSS selector from the EMC (optional) */
|
|
29
|
+
matching?: string;
|
|
30
|
+
/** Whether to parse attributes via withAttrs (default: false). Phase II feature. */
|
|
31
|
+
parse?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Apply enhancements in bulk to matching elements within a target.
|
|
36
|
+
*
|
|
37
|
+
* For each entry:
|
|
38
|
+
* 1. Dynamically imports the EMC JSON
|
|
39
|
+
* 2. Extracts enhConfig (spawn path, enhKey, withAttrs)
|
|
40
|
+
* 3. Finds matching elements via querySelectorAll
|
|
41
|
+
* 4. Registers the enhancement if not already registered
|
|
42
|
+
* 5. Spawns the enhancement on each matched element via enh.get()
|
|
43
|
+
*
|
|
44
|
+
* Note: No scope perimeter is applied. For reactive observation of new elements,
|
|
45
|
+
* use mount-observer instead.
|
|
46
|
+
*
|
|
47
|
+
* @param target - The DOM element to search within
|
|
48
|
+
* @param configs - Array of enhancement configurations
|
|
49
|
+
*/
|
|
50
|
+
export async function enhanceAll(
|
|
51
|
+
target: Element,
|
|
52
|
+
configs: EnhanceConfig[],
|
|
53
|
+
permissions?: AssignPermissions
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
for (const config of configs) {
|
|
56
|
+
// Validate EMC path unless cross-domain imports are explicitly permitted
|
|
57
|
+
if (!permissions?.crossDomainImports && !isAllowedImportPath(config.emc)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`enhanceAll: EMC path "${config.emc}" is a cross-domain URL. ` +
|
|
60
|
+
`Only relative, absolute, or bare specifier paths are allowed by default. ` +
|
|
61
|
+
`Pass { crossDomainImports: true } in permissions to override.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 1. Import the EMC JSON
|
|
66
|
+
const emcModule = await import(config.emc, { with: { type: 'json' } });
|
|
67
|
+
const emc = emcModule.default ?? emcModule;
|
|
68
|
+
const { enhConfig } = emc;
|
|
69
|
+
|
|
70
|
+
if (!enhConfig) {
|
|
71
|
+
throw new Error(`enhanceAll: EMC at "${config.emc}" does not contain an enhConfig field`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Determine the selector
|
|
75
|
+
const selector = config.matching ?? enhConfig.matching ?? '*';
|
|
76
|
+
|
|
77
|
+
// 3. Find matching elements
|
|
78
|
+
const elements = target.querySelectorAll(selector);
|
|
79
|
+
if (elements.length === 0) continue;
|
|
80
|
+
|
|
81
|
+
// 4. Resolve and register the enhancement
|
|
82
|
+
const registryItem = await resolveAndRegister(enhConfig, target);
|
|
83
|
+
if (!registryItem) continue;
|
|
84
|
+
|
|
85
|
+
// 5. Spawn enhancement on each matched element
|
|
86
|
+
for (const el of elements) {
|
|
87
|
+
try {
|
|
88
|
+
(el as any).enh.get(registryItem);
|
|
89
|
+
} catch {
|
|
90
|
+
// If enh gateway isn't available, skip silently
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the enhancement's registry item from EMC enhConfig.
|
|
98
|
+
* If not already registered, dynamically imports the spawn module and registers it.
|
|
99
|
+
*/
|
|
100
|
+
async function resolveAndRegister(enhConfig: any, target: Element): Promise<any> {
|
|
101
|
+
const { enhKey, spawn: spawnPath } = enhConfig;
|
|
102
|
+
|
|
103
|
+
// Get the enhancement registry for this element's scope
|
|
104
|
+
const registry = (target as any).customElementRegistry?.enhancementRegistry
|
|
105
|
+
?? (typeof customElements !== 'undefined' ? (customElements as any).enhancementRegistry : undefined);
|
|
106
|
+
|
|
107
|
+
if (!registry) return null;
|
|
108
|
+
|
|
109
|
+
// Check if already registered
|
|
110
|
+
if (enhKey) {
|
|
111
|
+
const existing = registry.findByEnhKey(enhKey);
|
|
112
|
+
if (existing) return existing;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Not registered — dynamically import the spawn module
|
|
116
|
+
if (!spawnPath) return null;
|
|
117
|
+
|
|
118
|
+
const spawnModule = await import(spawnPath);
|
|
119
|
+
const SpawnClass = spawnModule.default ?? Object.values(spawnModule).find(
|
|
120
|
+
(v: any) => typeof v === 'function' && v.prototype
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (!SpawnClass) return null;
|
|
124
|
+
|
|
125
|
+
// Build and register the registry item
|
|
126
|
+
const registryItem: any = {
|
|
127
|
+
spawn: SpawnClass,
|
|
128
|
+
enhKey,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Include withAttrs if present in enhConfig
|
|
132
|
+
if (enhConfig.withAttrs) {
|
|
133
|
+
registryItem.withAttrs = enhConfig.withAttrs;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
registry.push(registryItem);
|
|
137
|
+
return registryItem;
|
|
138
|
+
}
|
package/handlers/join.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtIns.join handler for assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Joins a resolved array into a single string. Supports nested sub-arrays
|
|
5
|
+
* with "all-or-nothing" semantics: if any element in a nested sub-array
|
|
6
|
+
* resolves to null/undefined, the entire sub-array is dropped.
|
|
7
|
+
*
|
|
8
|
+
* This handler is auto-loaded by processHandlerCommands when `do: 'builtIns.join'`
|
|
9
|
+
* is encountered — no explicit import is needed.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* assignFrom(oElement, {
|
|
13
|
+
* '?.textContent =>': {
|
|
14
|
+
* do: 'builtIns.join',
|
|
15
|
+
* resolve: {
|
|
16
|
+
* value: ['?.lastName', ', ', '?.firstName']
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }, { from: vm });
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // With optional segment (all-or-nothing):
|
|
23
|
+
* assignFrom(oElement, {
|
|
24
|
+
* '?.textContent =>': {
|
|
25
|
+
* do: 'builtIns.join',
|
|
26
|
+
* resolve: {
|
|
27
|
+
* value: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* }, { from: vm });
|
|
31
|
+
* // If middleName is undefined, the sub-array [', ', undefined] is dropped entirely.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Process nested arrays with all-or-nothing null semantics.
|
|
35
|
+
* - Top-level null/undefined values are filtered out.
|
|
36
|
+
* - If a nested sub-array contains any null/undefined element, the entire sub-array is dropped.
|
|
37
|
+
* - Nested sub-arrays that pass are flattened into the result.
|
|
38
|
+
*/
|
|
39
|
+
function processValue(value) {
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const item of value) {
|
|
42
|
+
if (Array.isArray(item)) {
|
|
43
|
+
// All-or-nothing: if any element is null/undefined, drop the entire sub-array
|
|
44
|
+
if (item.some(el => el == null)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// Sub-array passes — flatten its elements (recursively process nested arrays)
|
|
48
|
+
result.push(...processValue(item));
|
|
49
|
+
}
|
|
50
|
+
else if (item != null) {
|
|
51
|
+
result.push(item);
|
|
52
|
+
}
|
|
53
|
+
// Top-level null/undefined are silently filtered out
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* JoinHandler — built-in handler for composing strings from resolved arrays.
|
|
59
|
+
*
|
|
60
|
+
* Returns the joined string via the return-value protocol, which causes
|
|
61
|
+
* processHandlerCommands to assign it back to the LHS path.
|
|
62
|
+
*/
|
|
63
|
+
export class JoinHandler {
|
|
64
|
+
config;
|
|
65
|
+
constructor(config) {
|
|
66
|
+
this.config = config;
|
|
67
|
+
}
|
|
68
|
+
async assign(lhsTarget, resolvedParams) {
|
|
69
|
+
const { value } = resolvedParams;
|
|
70
|
+
const separator = this.config.separator ?? '';
|
|
71
|
+
const items = Array.isArray(value) ? processValue(value) : [value];
|
|
72
|
+
return items.join(separator);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/handlers/join.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtIns.join handler for assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Joins a resolved array into a single string. Supports nested sub-arrays
|
|
5
|
+
* with "all-or-nothing" semantics: if any element in a nested sub-array
|
|
6
|
+
* resolves to null/undefined, the entire sub-array is dropped.
|
|
7
|
+
*
|
|
8
|
+
* This handler is auto-loaded by processHandlerCommands when `do: 'builtIns.join'`
|
|
9
|
+
* is encountered — no explicit import is needed.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* assignFrom(oElement, {
|
|
13
|
+
* '?.textContent =>': {
|
|
14
|
+
* do: 'builtIns.join',
|
|
15
|
+
* resolve: {
|
|
16
|
+
* value: ['?.lastName', ', ', '?.firstName']
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }, { from: vm });
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // With optional segment (all-or-nothing):
|
|
23
|
+
* assignFrom(oElement, {
|
|
24
|
+
* '?.textContent =>': {
|
|
25
|
+
* do: 'builtIns.join',
|
|
26
|
+
* resolve: {
|
|
27
|
+
* value: ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* }, { from: vm });
|
|
31
|
+
* // If middleName is undefined, the sub-array [', ', undefined] is dropped entirely.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { AssignFromHandler } from '../assignFrom.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Process nested arrays with all-or-nothing null semantics.
|
|
38
|
+
* - Top-level null/undefined values are filtered out.
|
|
39
|
+
* - If a nested sub-array contains any null/undefined element, the entire sub-array is dropped.
|
|
40
|
+
* - Nested sub-arrays that pass are flattened into the result.
|
|
41
|
+
*/
|
|
42
|
+
function processValue(value: any[]): any[] {
|
|
43
|
+
const result: any[] = [];
|
|
44
|
+
for (const item of value) {
|
|
45
|
+
if (Array.isArray(item)) {
|
|
46
|
+
// All-or-nothing: if any element is null/undefined, drop the entire sub-array
|
|
47
|
+
if (item.some(el => el == null)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Sub-array passes — flatten its elements (recursively process nested arrays)
|
|
51
|
+
result.push(...processValue(item));
|
|
52
|
+
} else if (item != null) {
|
|
53
|
+
result.push(item);
|
|
54
|
+
}
|
|
55
|
+
// Top-level null/undefined are silently filtered out
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* JoinHandler — built-in handler for composing strings from resolved arrays.
|
|
62
|
+
*
|
|
63
|
+
* Returns the joined string via the return-value protocol, which causes
|
|
64
|
+
* processHandlerCommands to assign it back to the LHS path.
|
|
65
|
+
*/
|
|
66
|
+
export class JoinHandler implements AssignFromHandler {
|
|
67
|
+
config: any;
|
|
68
|
+
|
|
69
|
+
constructor(config: any) {
|
|
70
|
+
this.config = config;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async assign(lhsTarget: any, resolvedParams: Record<string, any>): Promise<string> {
|
|
74
|
+
const { value } = resolvedParams;
|
|
75
|
+
const separator = this.config.separator ?? '';
|
|
76
|
+
|
|
77
|
+
const items = Array.isArray(value) ? processValue(value) : [value];
|
|
78
|
+
return items.join(separator);
|
|
79
|
+
}
|
|
80
|
+
}
|