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.
Files changed (114) hide show
  1. package/README.md +847 -5
  2. package/assignFrom.js +229 -9
  3. package/assignFrom.ts +338 -9
  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 +57 -3
  104. package/paths.js +231 -0
  105. package/paths.ts +413 -0
  106. package/processHandlerCommands.js +188 -0
  107. package/processHandlerCommands.ts +217 -0
  108. package/resolveIdRef.js +144 -0
  109. package/resolveIdRef.ts +170 -0
  110. package/resolveValues.js +41 -2
  111. package/resolveValues.ts +41 -1
  112. package/transitionHelper.js +109 -0
  113. package/transitionHelper.ts +132 -0
  114. package/types/assign-gingerly/types.d.ts +89 -0
package/assignFrom.js CHANGED
@@ -21,15 +21,235 @@
21
21
  */
22
22
  import { resolveValues } from './resolveValues.js';
23
23
  import assignGingerly from './assignGingerly.js';
24
- export async function assignFrom(target, pattern, options) {
25
- const resolved = await resolveValues(pattern, options.from, {
26
- withMethods: options.withMethods,
27
- aka: options.aka,
28
- protocols: options.protocols
29
- });
30
- // Recursively handle "..." spread keys at all nesting levels
31
- handleSpreads(resolved);
32
- return assignGingerly(target, resolved, options);
24
+ /**
25
+ * Check if a key ends with the handler operator ' =>'.
26
+ */
27
+ function isHandlerCommand(key) {
28
+ return key.endsWith(' =>');
29
+ }
30
+ /**
31
+ * Supported substitution variables and their option keys.
32
+ */
33
+ const SUBSTITUTION_VARS = [
34
+ { placeholder: '${x}', optionKey: 'where_x_in' },
35
+ { placeholder: '${y}', optionKey: 'where_y_in' },
36
+ { placeholder: '${z}', optionKey: 'where_z_in' },
37
+ ];
38
+ /**
39
+ * Recursively substitute a placeholder in all string values of an object.
40
+ * Returns a new object (shallow clone at each level) with substitutions applied.
41
+ */
42
+ function substituteInValue(value, placeholder, replacement) {
43
+ if (typeof value === 'string') {
44
+ return value.includes(placeholder) ? value.replaceAll(placeholder, replacement) : value;
45
+ }
46
+ if (Array.isArray(value)) {
47
+ return value.map(item => substituteInValue(item, placeholder, replacement));
48
+ }
49
+ if (value && typeof value === 'object') {
50
+ const proto = Object.getPrototypeOf(value);
51
+ if (proto === Object.prototype || proto === null) {
52
+ const result = {};
53
+ for (const [k, v] of Object.entries(value)) {
54
+ result[k] = substituteInValue(v, placeholder, replacement);
55
+ }
56
+ return result;
57
+ }
58
+ }
59
+ return value;
60
+ }
61
+ /**
62
+ * Check if a pattern entry (key + value) contains a given placeholder.
63
+ */
64
+ function entryContainsPlaceholder(key, value, placeholder) {
65
+ if (key.includes(placeholder))
66
+ return true;
67
+ return valueContainsPlaceholder(value, placeholder);
68
+ }
69
+ /**
70
+ * Check if a value (string, object, or array) contains a placeholder.
71
+ */
72
+ function valueContainsPlaceholder(value, placeholder) {
73
+ if (typeof value === 'string')
74
+ return value.includes(placeholder);
75
+ if (Array.isArray(value))
76
+ return value.some(item => valueContainsPlaceholder(item, placeholder));
77
+ if (value && typeof value === 'object') {
78
+ const proto = Object.getPrototypeOf(value);
79
+ if (proto === Object.prototype || proto === null) {
80
+ return Object.values(value).some(v => valueContainsPlaceholder(v, placeholder));
81
+ }
82
+ }
83
+ return false;
84
+ }
85
+ /**
86
+ * Expand looped substitution variables in a pattern.
87
+ * Applies cartesian expansion: x values are expanded first, then y, then z.
88
+ * Each variable multiplies the entries — result count = x.length × y.length × z.length.
89
+ *
90
+ * Returns the expanded pattern (or the original if no substitutions apply).
91
+ */
92
+ function expandSubstitutions(pattern, options) {
93
+ let entries = Object.entries(pattern);
94
+ for (const { placeholder, optionKey } of SUBSTITUTION_VARS) {
95
+ const values = options[optionKey];
96
+ if (!values || values.length === 0)
97
+ continue;
98
+ const expanded = [];
99
+ for (const [key, value] of entries) {
100
+ if (entryContainsPlaceholder(key, value, placeholder)) {
101
+ // Expand this entry for each value in the variable array
102
+ for (const replacement of values) {
103
+ const newKey = key.includes(placeholder)
104
+ ? key.replaceAll(placeholder, replacement)
105
+ : key;
106
+ const newValue = substituteInValue(value, placeholder, replacement);
107
+ expanded.push([newKey, newValue]);
108
+ }
109
+ }
110
+ else {
111
+ // No placeholder in this entry — pass through
112
+ expanded.push([key, value]);
113
+ }
114
+ }
115
+ entries = expanded;
116
+ }
117
+ return mergeHandlerDuplicates(entries);
118
+ }
119
+ /**
120
+ * Convert entries to an object, merging duplicate handler (` =>`) keys into arrays.
121
+ * For normal (non-handler) keys, later entries overwrite earlier ones (standard object behavior).
122
+ * For handler keys, duplicate entries are combined into an array (Multiple Handlers pattern).
123
+ */
124
+ function mergeHandlerDuplicates(entries) {
125
+ const result = {};
126
+ for (const [key, value] of entries) {
127
+ if (key.endsWith(' =>') && key in result) {
128
+ // Duplicate handler key — merge into array
129
+ const existing = result[key];
130
+ if (Array.isArray(existing)) {
131
+ existing.push(value);
132
+ }
133
+ else {
134
+ result[key] = [existing, value];
135
+ }
136
+ }
137
+ else {
138
+ result[key] = value;
139
+ }
140
+ }
141
+ return result;
142
+ }
143
+ export async function assignFrom(target, pattern, options, permissions) {
144
+ // First: expand looped substitution variables (${x}, ${y}, ${z})
145
+ const expandedPattern = expandSubstitutions(pattern, options);
146
+ // Separate handler commands ( =>), #[x] keys, and normal keys
147
+ const handlerKeys = [];
148
+ const normalPattern = {};
149
+ const idRefNormalKeys = [];
150
+ const idRefHandlerKeys = [];
151
+ for (const key of Object.keys(expandedPattern)) {
152
+ if (isHandlerCommand(key)) {
153
+ if (key.startsWith('#[')) {
154
+ idRefHandlerKeys.push(key);
155
+ }
156
+ else {
157
+ handlerKeys.push(key);
158
+ }
159
+ }
160
+ else if (key.startsWith('#[')) {
161
+ idRefNormalKeys.push(key);
162
+ }
163
+ else {
164
+ normalPattern[key] = expandedPattern[key];
165
+ }
166
+ }
167
+ // Process normal keys via resolveValues + assignGingerly
168
+ if (Object.keys(normalPattern).length > 0) {
169
+ const resolved = await resolveValues(normalPattern, options.from, {
170
+ withMethods: options.withMethods,
171
+ aka: options.aka,
172
+ protocols: options.protocols
173
+ });
174
+ // Recursively handle "..." spread keys at all nesting levels
175
+ handleSpreads(resolved);
176
+ assignGingerly(target, resolved, options);
177
+ }
178
+ // Process #[x] normal keys — resolve element, then apply remaining path + value
179
+ if (idRefNormalKeys.length > 0 && options.withIds) {
180
+ const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
181
+ for (const key of idRefNormalKeys) {
182
+ const parsed = parseIdRef(key);
183
+ if (!parsed)
184
+ continue;
185
+ const el = resolveIdVariable(parsed.varName, target, options.withIds);
186
+ if (!el)
187
+ continue;
188
+ const value = expandedPattern[key];
189
+ if (parsed.remainingPath) {
190
+ // Resolve the RHS value
191
+ const resolvedValue = await resolveValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
192
+ // Apply remaining path on the resolved element
193
+ assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
194
+ }
195
+ else {
196
+ // No remaining path — resolve and assign directly to the element
197
+ const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
198
+ if ('__v' in resolvedValue) {
199
+ // Single value — can't assign to element root without a path
200
+ }
201
+ else {
202
+ assignGingerly(el, resolvedValue, options);
203
+ }
204
+ }
205
+ }
206
+ }
207
+ // Process handler commands ( =>) — dynamically imported only when needed
208
+ if (handlerKeys.length > 0) {
209
+ const { processHandlerCommands } = await import('./processHandlerCommands.js');
210
+ await processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
211
+ }
212
+ // Process #[x] handler keys — resolve element, then pass to handler processing
213
+ if (idRefHandlerKeys.length > 0 && options.withIds) {
214
+ const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
215
+ const { processHandlerCommands } = await import('./processHandlerCommands.js');
216
+ for (const key of idRefHandlerKeys) {
217
+ const parsed = parseIdRef(key);
218
+ if (!parsed)
219
+ continue;
220
+ const el = resolveIdVariable(parsed.varName, target, options.withIds);
221
+ if (!el)
222
+ continue;
223
+ // Build a synthetic key for processHandlerCommands:
224
+ // The resolved element becomes the target, remaining path is the LHS
225
+ const syntheticKey = parsed.remainingPath
226
+ ? `${parsed.remainingPath} =>`
227
+ : ' =>';
228
+ const syntheticPattern = {
229
+ [syntheticKey]: expandedPattern[key]
230
+ };
231
+ await processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
232
+ }
233
+ }
234
+ // Process inferred assignments — dynamically imported only when option is present
235
+ if (options.inferredAssignments) {
236
+ const { processInferredAssignments } = await import('./inferredAssignments.js');
237
+ await processInferredAssignments(target, options.from, options.inferredAssignments);
238
+ // Set up MutationObserver for new matching elements if beVigilant
239
+ if (options.inferredAssignments.beVigilant) {
240
+ if (!options.signal) {
241
+ throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
242
+ }
243
+ const { setupVigilantObserver } = await import('./beVigilant.js');
244
+ setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
245
+ }
246
+ }
247
+ // Process bulk enhancements — dynamically imported only when option is present
248
+ if (options.enhance && options.enhance.length > 0) {
249
+ const { enhanceAll } = await import('./enhanceAll.js');
250
+ await enhanceAll(target, options.enhance, permissions);
251
+ }
252
+ return target;
33
253
  }
34
254
  /**
35
255
  * Recursively walk an object and handle "..." spread keys.
package/assignFrom.ts CHANGED
@@ -21,27 +21,356 @@
21
21
  */
22
22
  import { resolveValues, ResolveValuesOptions } from './resolveValues.js';
23
23
  import assignGingerly, { IAssignGingerlyOptions } from './assignGingerly.js';
24
+ import type { AssignPermissions } from './isAllowedImportPath.js';
24
25
 
25
26
  export interface AssignFromOptions extends IAssignGingerlyOptions, ResolveValuesOptions {
26
27
  /** Source object to resolve RHS path strings against */
27
28
  from: any;
29
+
30
+ /** Loop variable bindings — expand pattern entries containing ${x} */
31
+ where_x_in?: string[];
32
+ /** Loop variable bindings — expand pattern entries containing ${y} */
33
+ where_y_in?: string[];
34
+ /** Loop variable bindings — expand pattern entries containing ${z} */
35
+ where_z_in?: string[];
36
+
37
+ /**
38
+ * Cached element references by variable name.
39
+ * Used with `#[varName]` syntax in LHS keys for fast repeated element access.
40
+ *
41
+ * - String value: existing element ID (uses getElementById)
42
+ * - Object value: { qry: 'selector' } — finds element via querySelector on target, auto-assigns an ID
43
+ *
44
+ * Elements are cached via WeakRef with getElementById fallback on cache miss.
45
+ */
46
+ withIds?: Record<string, string | { qry: string }>;
47
+
48
+ /**
49
+ * Handler implementations scoped to this call.
50
+ * Key: the `do` name referenced in handler configs.
51
+ * Value: a class constructor, or an import path to dynamically load one.
52
+ *
53
+ * Import paths must be local (relative, absolute, or bare specifier — no cross-domain URLs).
54
+ * The module's default export is checked first; otherwise the first exported class
55
+ * with an `assign` method on its prototype is used.
56
+ *
57
+ * Built-in handlers (builtIns.*) auto-load without needing to be listed here.
58
+ *
59
+ * @example
60
+ * handlers: {
61
+ * 'my-list': MyListHandler, // class constructor
62
+ * 'my-chart': './handlers/chart.js', // dynamic import path
63
+ * 'vendor-widget': 'some-package/handler.js', // bare specifier (import map)
64
+ * }
65
+ */
66
+ handlers?: Record<string, AssignFromHandlerConstructor | string>;
67
+
68
+ /**
69
+ * Inferred assignments — automatically distribute source values to matching
70
+ * DOM elements based on structural conventions (itemprop, name, etc.).
71
+ *
72
+ * Uses the inferencer submodule to determine the correct property for each
73
+ * matched element (textContent, value, checked, dateTime, ish, etc.).
74
+ *
75
+ * @example
76
+ * inferredAssignments: {
77
+ * byItemprop: ['user', 'name', 'email'], // or true for all source keys
78
+ * beVigilant: true, // watch for new matching elements (requires signal)
79
+ * }
80
+ */
81
+ inferredAssignments?: {
82
+ byItemprop?: string[] | true;
83
+ /** Watch for new matching elements via MutationObserver. Requires options.signal for cleanup. */
84
+ beVigilant?: boolean;
85
+ };
86
+
87
+ /**
88
+ * Bulk enhancement application via EMC JSON configs.
89
+ * Finds matching elements and spawns enhancements on them.
90
+ *
91
+ * Each entry specifies an EMC JSON path and optionally overrides the matching selector.
92
+ * Enhancements are auto-registered if not already present in the enhancement registry.
93
+ *
94
+ * No scope perimeter is applied — use mount-observer for reactive/scoped enhancement.
95
+ *
96
+ * @example
97
+ * enhance: [
98
+ * { emc: 'be-bound/emc.json', matching: '[name]' },
99
+ * { emc: 'be-observant/emc.json', matching: '[itemprop]' },
100
+ * ]
101
+ */
102
+ enhance?: Array<{ emc: string; matching?: string; parse?: boolean }>;
103
+ }
104
+
105
+ /**
106
+ * Interface for assignFrom handler classes.
107
+ * Handlers are invoked when a LHS key ends with ' =>'.
108
+ */
109
+ export interface AssignFromHandler {
110
+ assign(lhsTarget: any, resolvedParams: Record<string, any>, options: AssignFromOptions): Promise<void> | void;
111
+ }
112
+
113
+ export interface AssignFromHandlerConstructor {
114
+ new (config: any): AssignFromHandler;
115
+ }
116
+
117
+ /**
118
+ * Check if a key ends with the handler operator ' =>'.
119
+ */
120
+ function isHandlerCommand(key: string): boolean {
121
+ return key.endsWith(' =>');
122
+ }
123
+
124
+ /**
125
+ * Supported substitution variables and their option keys.
126
+ */
127
+ const SUBSTITUTION_VARS = [
128
+ { placeholder: '${x}', optionKey: 'where_x_in' },
129
+ { placeholder: '${y}', optionKey: 'where_y_in' },
130
+ { placeholder: '${z}', optionKey: 'where_z_in' },
131
+ ] as const;
132
+
133
+ /**
134
+ * Recursively substitute a placeholder in all string values of an object.
135
+ * Returns a new object (shallow clone at each level) with substitutions applied.
136
+ */
137
+ function substituteInValue(value: any, placeholder: string, replacement: string): any {
138
+ if (typeof value === 'string') {
139
+ return value.includes(placeholder) ? value.replaceAll(placeholder, replacement) : value;
140
+ }
141
+ if (Array.isArray(value)) {
142
+ return value.map(item => substituteInValue(item, placeholder, replacement));
143
+ }
144
+ if (value && typeof value === 'object') {
145
+ const proto = Object.getPrototypeOf(value);
146
+ if (proto === Object.prototype || proto === null) {
147
+ const result: Record<string, any> = {};
148
+ for (const [k, v] of Object.entries(value)) {
149
+ result[k] = substituteInValue(v, placeholder, replacement);
150
+ }
151
+ return result;
152
+ }
153
+ }
154
+ return value;
155
+ }
156
+
157
+ /**
158
+ * Check if a pattern entry (key + value) contains a given placeholder.
159
+ */
160
+ function entryContainsPlaceholder(key: string, value: any, placeholder: string): boolean {
161
+ if (key.includes(placeholder)) return true;
162
+ return valueContainsPlaceholder(value, placeholder);
163
+ }
164
+
165
+ /**
166
+ * Check if a value (string, object, or array) contains a placeholder.
167
+ */
168
+ function valueContainsPlaceholder(value: any, placeholder: string): boolean {
169
+ if (typeof value === 'string') return value.includes(placeholder);
170
+ if (Array.isArray(value)) return value.some(item => valueContainsPlaceholder(item, placeholder));
171
+ if (value && typeof value === 'object') {
172
+ const proto = Object.getPrototypeOf(value);
173
+ if (proto === Object.prototype || proto === null) {
174
+ return Object.values(value).some(v => valueContainsPlaceholder(v, placeholder));
175
+ }
176
+ }
177
+ return false;
178
+ }
179
+
180
+ /**
181
+ * Expand looped substitution variables in a pattern.
182
+ * Applies cartesian expansion: x values are expanded first, then y, then z.
183
+ * Each variable multiplies the entries — result count = x.length × y.length × z.length.
184
+ *
185
+ * Returns the expanded pattern (or the original if no substitutions apply).
186
+ */
187
+ function expandSubstitutions(
188
+ pattern: Record<string, any>,
189
+ options: AssignFromOptions
190
+ ): Record<string, any> {
191
+ let entries = Object.entries(pattern);
192
+
193
+ for (const { placeholder, optionKey } of SUBSTITUTION_VARS) {
194
+ const values = options[optionKey as keyof AssignFromOptions] as string[] | undefined;
195
+ if (!values || values.length === 0) continue;
196
+
197
+ const expanded: [string, any][] = [];
198
+ for (const [key, value] of entries) {
199
+ if (entryContainsPlaceholder(key, value, placeholder)) {
200
+ // Expand this entry for each value in the variable array
201
+ for (const replacement of values) {
202
+ const newKey = key.includes(placeholder)
203
+ ? key.replaceAll(placeholder, replacement)
204
+ : key;
205
+ const newValue = substituteInValue(value, placeholder, replacement);
206
+ expanded.push([newKey, newValue]);
207
+ }
208
+ } else {
209
+ // No placeholder in this entry — pass through
210
+ expanded.push([key, value]);
211
+ }
212
+ }
213
+ entries = expanded;
214
+ }
215
+
216
+ return mergeHandlerDuplicates(entries);
217
+ }
218
+
219
+ /**
220
+ * Convert entries to an object, merging duplicate handler (` =>`) keys into arrays.
221
+ * For normal (non-handler) keys, later entries overwrite earlier ones (standard object behavior).
222
+ * For handler keys, duplicate entries are combined into an array (Multiple Handlers pattern).
223
+ */
224
+ function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
225
+ const result: Record<string, any> = {};
226
+ for (const [key, value] of entries) {
227
+ if (key.endsWith(' =>') && key in result) {
228
+ // Duplicate handler key — merge into array
229
+ const existing = result[key];
230
+ if (Array.isArray(existing)) {
231
+ existing.push(value);
232
+ } else {
233
+ result[key] = [existing, value];
234
+ }
235
+ } else {
236
+ result[key] = value;
237
+ }
238
+ }
239
+ return result;
28
240
  }
29
241
 
30
242
  export async function assignFrom(
31
243
  target: any,
32
244
  pattern: Record<string, any>,
33
- options: AssignFromOptions
245
+ options: AssignFromOptions,
246
+ permissions?: AssignPermissions
34
247
  ): Promise<any> {
35
- const resolved = await resolveValues(pattern, options.from, {
36
- withMethods: options.withMethods,
37
- aka: options.aka,
38
- protocols: options.protocols
39
- });
248
+ // First: expand looped substitution variables (${x}, ${y}, ${z})
249
+ const expandedPattern = expandSubstitutions(pattern, options);
250
+
251
+ // Separate handler commands ( =>), #[x] keys, and normal keys
252
+ const handlerKeys: string[] = [];
253
+ const normalPattern: Record<string, any> = {};
254
+ const idRefNormalKeys: string[] = [];
255
+ const idRefHandlerKeys: string[] = [];
256
+
257
+ for (const key of Object.keys(expandedPattern)) {
258
+ if (isHandlerCommand(key)) {
259
+ if (key.startsWith('#[')) {
260
+ idRefHandlerKeys.push(key);
261
+ } else {
262
+ handlerKeys.push(key);
263
+ }
264
+ } else if (key.startsWith('#[')) {
265
+ idRefNormalKeys.push(key);
266
+ } else {
267
+ normalPattern[key] = expandedPattern[key];
268
+ }
269
+ }
270
+
271
+ // Process normal keys via resolveValues + assignGingerly
272
+ if (Object.keys(normalPattern).length > 0) {
273
+ const resolved = await resolveValues(normalPattern, options.from, {
274
+ withMethods: options.withMethods,
275
+ aka: options.aka,
276
+ protocols: options.protocols
277
+ });
278
+
279
+ // Recursively handle "..." spread keys at all nesting levels
280
+ handleSpreads(resolved);
281
+
282
+ assignGingerly(target, resolved, options);
283
+ }
40
284
 
41
- // Recursively handle "..." spread keys at all nesting levels
42
- handleSpreads(resolved);
285
+ // Process #[x] normal keys resolve element, then apply remaining path + value
286
+ if (idRefNormalKeys.length > 0 && options.withIds) {
287
+ const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
288
+ for (const key of idRefNormalKeys) {
289
+ const parsed = parseIdRef(key);
290
+ if (!parsed) continue;
291
+
292
+ const el = resolveIdVariable(parsed.varName, target, options.withIds);
293
+ if (!el) continue;
294
+
295
+ const value = expandedPattern[key];
296
+ if (parsed.remainingPath) {
297
+ // Resolve the RHS value
298
+ const resolvedValue = await resolveValues(
299
+ { __v: value }, options.from,
300
+ { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
301
+ );
302
+ // Apply remaining path on the resolved element
303
+ assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
304
+ } else {
305
+ // No remaining path — resolve and assign directly to the element
306
+ const resolvedValue = await resolveValues(
307
+ typeof value === 'object' && value !== null ? value : { __v: value },
308
+ options.from,
309
+ { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
310
+ );
311
+ if ('__v' in resolvedValue) {
312
+ // Single value — can't assign to element root without a path
313
+ } else {
314
+ assignGingerly(el, resolvedValue, options);
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ // Process handler commands ( =>) — dynamically imported only when needed
321
+ if (handlerKeys.length > 0) {
322
+ const { processHandlerCommands } = await import('./processHandlerCommands.js');
323
+ await processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
324
+ }
325
+
326
+ // Process #[x] handler keys — resolve element, then pass to handler processing
327
+ if (idRefHandlerKeys.length > 0 && options.withIds) {
328
+ const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
329
+ const { processHandlerCommands } = await import('./processHandlerCommands.js');
330
+
331
+ for (const key of idRefHandlerKeys) {
332
+ const parsed = parseIdRef(key);
333
+ if (!parsed) continue;
334
+
335
+ const el = resolveIdVariable(parsed.varName, target, options.withIds);
336
+ if (!el) continue;
337
+
338
+ // Build a synthetic key for processHandlerCommands:
339
+ // The resolved element becomes the target, remaining path is the LHS
340
+ const syntheticKey = parsed.remainingPath
341
+ ? `${parsed.remainingPath} =>`
342
+ : ' =>';
343
+
344
+ const syntheticPattern: Record<string, any> = {
345
+ [syntheticKey]: expandedPattern[key]
346
+ };
347
+
348
+ await processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
349
+ }
350
+ }
351
+
352
+ // Process inferred assignments — dynamically imported only when option is present
353
+ if (options.inferredAssignments) {
354
+ const { processInferredAssignments } = await import('./inferredAssignments.js');
355
+ await processInferredAssignments(target, options.from, options.inferredAssignments);
356
+
357
+ // Set up MutationObserver for new matching elements if beVigilant
358
+ if (options.inferredAssignments.beVigilant) {
359
+ if (!options.signal) {
360
+ throw new Error('assignFrom: inferredAssignments.beVigilant requires options.signal (AbortSignal) for cleanup');
361
+ }
362
+ const { setupVigilantObserver } = await import('./beVigilant.js');
363
+ setupVigilantObserver(target, options.from, options.inferredAssignments, options.signal);
364
+ }
365
+ }
366
+
367
+ // Process bulk enhancements — dynamically imported only when option is present
368
+ if (options.enhance && options.enhance.length > 0) {
369
+ const { enhanceAll } = await import('./enhanceAll.js');
370
+ await enhanceAll(target, options.enhance, permissions);
371
+ }
43
372
 
44
- return assignGingerly(target, resolved, options);
373
+ return target;
45
374
  }
46
375
 
47
376
  /**
package/assignGingerly.js CHANGED
@@ -42,6 +42,21 @@ export class EnhancementRegistry extends EventTarget {
42
42
  }
43
43
  // Dispatch event after adding items
44
44
  this.dispatchEvent(new EnhancementRegisteredEvent(items));
45
+ // Process features if present (fire-and-forget, uses shared featuresRegistry)
46
+ const itemsArr = Array.isArray(items) ? items : [items];
47
+ for (const item of itemsArr) {
48
+ if (item.features) {
49
+ this.#assignFeatures(item.spawn, item.features);
50
+ }
51
+ }
52
+ }
53
+ async #assignFeatures(spawn, features) {
54
+ const { assignFeatures } = await import('./assignFeatures.js');
55
+ const featuresRegistry = this._featuresRegistry
56
+ ?? (typeof customElements !== 'undefined' ? customElements.featuresRegistry : undefined);
57
+ if (featuresRegistry) {
58
+ assignFeatures(spawn, features, featuresRegistry);
59
+ }
45
60
  }
46
61
  getItems() {
47
62
  return Array.from(this.#items);
@@ -89,6 +104,18 @@ export class ItemscopeRegistry extends EventTarget {
89
104
  }
90
105
  this.#configs.set(name, config);
91
106
  this.dispatchEvent(new Event(name));
107
+ // Process features if present (fire-and-forget, uses shared featuresRegistry)
108
+ if (config.features) {
109
+ this.#assignFeatures(config.manager, config.features);
110
+ }
111
+ }
112
+ async #assignFeatures(manager, features) {
113
+ const { assignFeatures } = await import('./assignFeatures.js');
114
+ const featuresRegistry = this._featuresRegistry
115
+ ?? (typeof customElements !== 'undefined' ? customElements.featuresRegistry : undefined);
116
+ if (featuresRegistry) {
117
+ assignFeatures(manager, features, featuresRegistry);
118
+ }
92
119
  }
93
120
  /**
94
121
  * Get a manager configuration by name
@@ -515,7 +542,7 @@ function applyAliases(key, aliasMap) {
515
542
  /**
516
543
  * Main assignGingerly function
517
544
  */
518
- export function assignGingerly(target, source, options) {
545
+ export function assignGingerly(target, source, options, permissions) {
519
546
  if (!target || typeof target !== 'object') {
520
547
  return target;
521
548
  }
@@ -1059,6 +1086,12 @@ export function assignGingerly(target, source, options) {
1059
1086
  configurable: true,
1060
1087
  });
1061
1088
  }
1089
+ // Fire-and-forget bulk enhancements (async, non-blocking)
1090
+ if (options?.enhance && options.enhance.length > 0 && typeof target === 'object' && target instanceof Element) {
1091
+ import('./enhanceAll.js').then(({ enhanceAll }) => {
1092
+ enhanceAll(target, options.enhance, permissions);
1093
+ });
1094
+ }
1062
1095
  return target;
1063
1096
  }
1064
1097
  export default assignGingerly;