assign-gingerly 0.0.54 → 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 +143 -2
- package/assignFrom.js +37 -18
- package/assignFrom.ts +62 -3
- 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 +27 -1
- package/paths.js +49 -1
- package/paths.ts +80 -1
- package/processHandlerCommands.js +19 -19
- package/processHandlerCommands.ts +10 -13
- package/resolveIdRef.js +144 -125
- package/resolveIdRef.ts +30 -0
- package/types/assign-gingerly/types.d.ts +12 -0
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
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtIns.lazyLoad handler for assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Conditionally clones a template into a target element, using comment markers
|
|
5
|
+
* to track the inserted content. Supports show/hide and show/remove modes.
|
|
6
|
+
*
|
|
7
|
+
* This handler is auto-loaded by processHandlerCommands when `do: 'builtIns.lazyLoad'`
|
|
8
|
+
* is encountered — no explicit import is needed.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* assignFrom(document.body, {
|
|
12
|
+
* '?.querySelector?..mainView =>': {
|
|
13
|
+
* do: 'builtIns.lazyLoad',
|
|
14
|
+
* resolve: {
|
|
15
|
+
* if: '?.isHappy',
|
|
16
|
+
* instantiate: 'globalThis://happyMood',
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* }, { withMethods: ['querySelector'], from: myVM });
|
|
20
|
+
*/
|
|
21
|
+
import { withTransition, ensureHideStyle, DEFAULT_HIDE_CLASS } from '../transitionHelper.js';
|
|
22
|
+
const MARKER_START_PREFIX = '?start name="';
|
|
23
|
+
const MARKER_END = '?end';
|
|
24
|
+
/**
|
|
25
|
+
* Get the marker name from a template element (uses its id).
|
|
26
|
+
*/
|
|
27
|
+
function getMarkerName(templateEl) {
|
|
28
|
+
if (templateEl instanceof HTMLTemplateElement) {
|
|
29
|
+
return templateEl.id || 'anonymous';
|
|
30
|
+
}
|
|
31
|
+
if (typeof templateEl === 'string') {
|
|
32
|
+
return templateEl;
|
|
33
|
+
}
|
|
34
|
+
return 'anonymous';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Find existing start/end comment markers in a target element.
|
|
38
|
+
* Returns [startMarker, endMarker] or [null, null] if not found.
|
|
39
|
+
*/
|
|
40
|
+
function findMarkers(target, name) {
|
|
41
|
+
const startText = `${MARKER_START_PREFIX}${name}"`;
|
|
42
|
+
let startMarker = null;
|
|
43
|
+
let endMarker = null;
|
|
44
|
+
const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
|
|
45
|
+
let node;
|
|
46
|
+
while ((node = walker.nextNode())) {
|
|
47
|
+
if (!startMarker && node.data === startText) {
|
|
48
|
+
startMarker = node;
|
|
49
|
+
}
|
|
50
|
+
else if (startMarker && !endMarker && node.data === MARKER_END) {
|
|
51
|
+
endMarker = node;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return [startMarker, endMarker];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Create start/end markers and insert them into the target.
|
|
59
|
+
*/
|
|
60
|
+
function createMarkers(target, name, method) {
|
|
61
|
+
const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
|
|
62
|
+
const endMarker = document.createComment(MARKER_END);
|
|
63
|
+
if (method === 'prepend') {
|
|
64
|
+
target.prepend(endMarker);
|
|
65
|
+
target.prepend(startMarker);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
target.appendChild(startMarker);
|
|
69
|
+
target.appendChild(endMarker);
|
|
70
|
+
}
|
|
71
|
+
return [startMarker, endMarker];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get all nodes between start and end markers.
|
|
75
|
+
*/
|
|
76
|
+
function getNodesBetweenMarkers(start, end) {
|
|
77
|
+
const nodes = [];
|
|
78
|
+
let current = start.nextSibling;
|
|
79
|
+
while (current && current !== end) {
|
|
80
|
+
nodes.push(current);
|
|
81
|
+
current = current.nextSibling;
|
|
82
|
+
}
|
|
83
|
+
return nodes;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* LazyLoadHandler — built-in handler for conditional template instantiation.
|
|
87
|
+
*
|
|
88
|
+
* Exported so it can be subclassed for custom behavior.
|
|
89
|
+
*/
|
|
90
|
+
export class LazyLoadHandler {
|
|
91
|
+
config;
|
|
92
|
+
constructor(config) {
|
|
93
|
+
this.config = config;
|
|
94
|
+
}
|
|
95
|
+
async assign(lhsTarget, resolvedParams) {
|
|
96
|
+
const { if: condition, instantiate, method = 'appendChild', forget = false, transitional = false, hideClass = DEFAULT_HIDE_CLASS } = resolvedParams;
|
|
97
|
+
if (!(lhsTarget instanceof Element)) {
|
|
98
|
+
throw new Error('builtIns.lazyLoad: lhsTarget must be a DOM Element');
|
|
99
|
+
}
|
|
100
|
+
const name = getMarkerName(instantiate);
|
|
101
|
+
let [startMarker, endMarker] = findMarkers(lhsTarget, name);
|
|
102
|
+
if (condition) {
|
|
103
|
+
if (startMarker && endMarker) {
|
|
104
|
+
const nodes = getNodesBetweenMarkers(startMarker, endMarker);
|
|
105
|
+
if (nodes.length > 0) {
|
|
106
|
+
withTransition(startMarker, 'show', transitional, () => {
|
|
107
|
+
for (const node of nodes) {
|
|
108
|
+
if (node instanceof Element) {
|
|
109
|
+
if (transitional) {
|
|
110
|
+
node.classList.remove(hideClass);
|
|
111
|
+
} else {
|
|
112
|
+
node.removeAttribute('hidden');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
if (transitional) {
|
|
120
|
+
ensureHideStyle(lhsTarget.getRootNode());
|
|
121
|
+
withTransition(startMarker, 'show', true, () => {
|
|
122
|
+
this.cloneAndInsertSync(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
await this.cloneAndInsert(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
[startMarker, endMarker] = createMarkers(lhsTarget, name, method);
|
|
131
|
+
if (transitional) {
|
|
132
|
+
ensureHideStyle(lhsTarget.getRootNode());
|
|
133
|
+
withTransition(startMarker, 'show', true, () => {
|
|
134
|
+
this.cloneAndInsertSync(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
await this.cloneAndInsert(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
if (startMarker && endMarker) {
|
|
143
|
+
const nodes = getNodesBetweenMarkers(startMarker, endMarker);
|
|
144
|
+
if (nodes.length === 0) return;
|
|
145
|
+
if (forget) {
|
|
146
|
+
withTransition(startMarker, 'hide', transitional, () => {
|
|
147
|
+
for (const node of nodes) {
|
|
148
|
+
node.parentNode?.removeChild(node);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
withTransition(startMarker, 'hide', transitional, () => {
|
|
154
|
+
for (const node of nodes) {
|
|
155
|
+
if (node instanceof Element) {
|
|
156
|
+
if (transitional) {
|
|
157
|
+
ensureHideStyle(lhsTarget.getRootNode(), hideClass);
|
|
158
|
+
node.classList.add(hideClass);
|
|
159
|
+
} else {
|
|
160
|
+
node.setAttribute('hidden', '');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
cloneAndInsertSync(templateEl, startMarker, endMarker, lhsTarget, resolvedParams) {
|
|
170
|
+
let content;
|
|
171
|
+
if (templateEl instanceof HTMLTemplateElement) {
|
|
172
|
+
content = templateEl.content.cloneNode(true);
|
|
173
|
+
}
|
|
174
|
+
else if (templateEl instanceof DocumentFragment) {
|
|
175
|
+
content = templateEl.cloneNode(true);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
throw new Error(`builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`);
|
|
179
|
+
}
|
|
180
|
+
const nodes = Array.from(content.childNodes);
|
|
181
|
+
endMarker.parentNode.insertBefore(content, endMarker);
|
|
182
|
+
return nodes;
|
|
183
|
+
}
|
|
184
|
+
async cloneAndInsert(templateEl, startMarker, endMarker, lhsTarget, resolvedParams) {
|
|
185
|
+
let content;
|
|
186
|
+
if (templateEl instanceof HTMLTemplateElement) {
|
|
187
|
+
content = templateEl.content.cloneNode(true);
|
|
188
|
+
}
|
|
189
|
+
else if (templateEl instanceof DocumentFragment) {
|
|
190
|
+
content = templateEl.cloneNode(true);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
throw new Error(`builtIns.lazyLoad: instantiate must resolve to an HTMLTemplateElement or DocumentFragment`);
|
|
194
|
+
}
|
|
195
|
+
const nodes = Array.from(content.childNodes);
|
|
196
|
+
endMarker.parentNode.insertBefore(content, endMarker);
|
|
197
|
+
await this.onCloneInserted(nodes, lhsTarget, resolvedParams);
|
|
198
|
+
if (resolvedParams.onInstantiated && typeof resolvedParams.onInstantiated === 'function') {
|
|
199
|
+
const ctx = {
|
|
200
|
+
nodes,
|
|
201
|
+
target: lhsTarget,
|
|
202
|
+
config: this.config,
|
|
203
|
+
resolvedParams
|
|
204
|
+
};
|
|
205
|
+
await resolvedParams.onInstantiated(ctx);
|
|
206
|
+
}
|
|
207
|
+
return nodes;
|
|
208
|
+
}
|
|
209
|
+
async onCloneInserted(nodes, lhsTarget, resolvedParams) {
|
|
210
|
+
// No-op by default. Subclasses override.
|
|
211
|
+
}
|
|
212
|
+
}
|