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.
Files changed (110) hide show
  1. package/README.md +143 -2
  2. package/assignFrom.js +37 -18
  3. package/assignFrom.ts +62 -3
  4. package/assignGingerly.js +34 -1
  5. package/assignGingerly.ts +54 -1
  6. package/beVigilant.js +73 -0
  7. package/beVigilant.ts +85 -0
  8. package/enhanceAll.js +106 -0
  9. package/enhanceAll.ts +138 -0
  10. package/handlers/join.js +74 -0
  11. package/handlers/join.ts +80 -0
  12. package/handlers/lazyLoad.js +212 -0
  13. package/handlers/lazyLoad.ts +307 -0
  14. package/handlers/lazyLoadSwitch.js +58 -0
  15. package/handlers/lazyLoadSwitch.ts +63 -0
  16. package/handlers/microDataJoin.js +184 -0
  17. package/handlers/microDataJoin.ts +270 -0
  18. package/inferencer/.gitmodules +3 -0
  19. package/inferencer/.vscode/settings.json +2 -0
  20. package/inferencer/InferencedPropagator.js +230 -0
  21. package/inferencer/InferencedPropagator.ts +269 -0
  22. package/inferencer/LICENSE +21 -0
  23. package/inferencer/README.md +524 -0
  24. package/inferencer/Requirements/SupportForPropagator.md +368 -0
  25. package/inferencer/imports.html +7 -0
  26. package/inferencer/inferencer.js +254 -0
  27. package/inferencer/inferencer.ts +292 -0
  28. package/inferencer/package-lock.json +129 -0
  29. package/inferencer/package.json +60 -0
  30. package/inferencer/playwright-report/data/507ad515125e13390ea07de92f22331c913fa068.md +55 -0
  31. package/inferencer/playwright-report/index.html +90 -0
  32. package/inferencer/playwright.config.ts +54 -0
  33. package/inferencer/test-results/.last-run.json +6 -0
  34. package/inferencer/test-results/inferencer-Inferencer-Enha-535bc-inferencer-tests-in-browser-chromium/error-context.md +55 -0
  35. package/inferencer/tests/inferencedPropagator.html +428 -0
  36. package/inferencer/tests/inferencedPropagator.spec.ts +18 -0
  37. package/inferencer/tests/inferencer.html +355 -0
  38. package/inferencer/tests/inferencer.spec.ts +19 -0
  39. package/inferencer/tsconfig.json +19 -0
  40. package/inferencer/types/.kiro/specs/conversion-template/README.md +128 -0
  41. package/inferencer/types/.kiro/specs/conversion-template/design.md +360 -0
  42. package/inferencer/types/.kiro/specs/conversion-template/requirements.md +191 -0
  43. package/inferencer/types/.kiro/specs/conversion-template/tasks.md +174 -0
  44. package/inferencer/types/.kiro/steering/coding-standards.md +53 -0
  45. package/inferencer/types/.kiro/steering/conversion-guide.md +108 -0
  46. package/inferencer/types/.kiro/steering/declarative-configuration.md +108 -0
  47. package/inferencer/types/.kiro/steering/emc-json-serializability.md +306 -0
  48. package/inferencer/types/EnhancementConversionInstructions.md +1854 -0
  49. package/inferencer/types/LICENSE +21 -0
  50. package/inferencer/types/NewCustomElement.md +388 -0
  51. package/inferencer/types/NewCustomElementFeature.md +683 -0
  52. package/inferencer/types/NewEnhancementInstructions.md +705 -0
  53. package/inferencer/types/README.md +2 -0
  54. package/inferencer/types/agrace/types.d.ts +11 -0
  55. package/inferencer/types/assign-gingerly/types.d.ts +572 -0
  56. package/inferencer/types/be-a-beacon/types.d.ts +17 -0
  57. package/inferencer/types/be-bound/types.d.ts +66 -0
  58. package/inferencer/types/be-buttoned-up/types.d.ts +19 -0
  59. package/inferencer/types/be-calculating/types.d.ts +54 -0
  60. package/inferencer/types/be-clonable/types.d.ts +38 -0
  61. package/inferencer/types/be-committed/types.d.ts +22 -0
  62. package/inferencer/types/be-consoling/types.d.ts +24 -0
  63. package/inferencer/types/be-decked-with/types.d.ts +26 -0
  64. package/inferencer/types/be-delible/types.d.ts +27 -0
  65. package/inferencer/types/be-dispatching/types.d.ts +34 -0
  66. package/inferencer/types/be-evanescent/types.d.ts +20 -0
  67. package/inferencer/types/be-flashy/types.d.ts +21 -0
  68. package/inferencer/types/be-gone/types.d.ts +25 -0
  69. package/inferencer/types/be-observing/types.d.ts +55 -0
  70. package/inferencer/types/be-reflective/types.d.ts +78 -0
  71. package/inferencer/types/be-reformable/types.d.ts +49 -0
  72. package/inferencer/types/be-render-neutral/types.d.ts +32 -0
  73. package/inferencer/types/be-switched/types.d.ts +146 -0
  74. package/inferencer/types/be-typed/types.d.ts +32 -0
  75. package/inferencer/types/be-valued/types.d.ts +22 -0
  76. package/inferencer/types/data-props/types.d.ts +34 -0
  77. package/inferencer/types/do-inc/types.d.ts +56 -0
  78. package/inferencer/types/do-invoke/types.d.ts +38 -0
  79. package/inferencer/types/do-merge/types.d.ts +28 -0
  80. package/inferencer/types/do-toggle/types.d.ts +31 -0
  81. package/inferencer/types/face-up/types.d.ts +100 -0
  82. package/inferencer/types/fetch-for/types.d.ts +36 -0
  83. package/inferencer/types/folder-picker/types.d.ts +21 -0
  84. package/inferencer/types/global.d.ts +29 -0
  85. package/inferencer/types/id-generation/types.d.ts +26 -0
  86. package/inferencer/types/inferencer/types.d.ts +46 -0
  87. package/inferencer/types/mount-observer/types.d.ts +363 -0
  88. package/inferencer/types/nested-regex-groups/types.d.ts +107 -0
  89. package/inferencer/types/pipe-in/types.d.ts +52 -0
  90. package/inferencer/types/roundabout/types.d.ts +268 -0
  91. package/inferencer/types/soak-up/types.d.ts +40 -0
  92. package/inferencer/types/templ-maker/types.d.ts +43 -0
  93. package/inferencer/types/time-ticker/types.d.ts +62 -0
  94. package/inferencer/types/truth-sourcer/types.d.ts +44 -0
  95. package/inferencer/upSearch.js +27 -0
  96. package/inferencer/upSearch.ts +26 -0
  97. package/inferencer/withScopePerimeter.js +27 -0
  98. package/inferencer/withScopePerimeter.ts +33 -0
  99. package/inferredAssignments.js +38 -0
  100. package/inferredAssignments.ts +65 -0
  101. package/isAllowedImportPath.js +42 -0
  102. package/isAllowedImportPath.ts +53 -0
  103. package/package.json +27 -1
  104. package/paths.js +49 -1
  105. package/paths.ts +80 -1
  106. package/processHandlerCommands.js +19 -19
  107. package/processHandlerCommands.ts +10 -13
  108. package/resolveIdRef.js +144 -125
  109. package/resolveIdRef.ts +30 -0
  110. 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
+ }
@@ -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
+ }
@@ -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
+ }