assign-gingerly 0.0.54 → 0.0.56
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 +186 -0
- package/handlers/lazyLoad.ts +281 -0
- package/handlers/lazyLoadSwitch.js +58 -0
- package/handlers/lazyLoadSwitch.ts +64 -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/markerUtils.js +127 -0
- package/markerUtils.ts +162 -0
- package/package.json +31 -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 +87 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
## Introduction
|
|
11
11
|
|
|
12
|
-
This package starts out innocently enough -- it provides
|
|
12
|
+
This package starts out innocently enough -- it provides the ability to carefully merge one object into another. This is a primitive sorely lacking in the web, and this package is a polyfill for what we (me with a lot of help from AI) would like to see built into the platform. We make no apologies about adding these features directly to the underlying API's, as it is part of a proposal which is sitting there gathering dust, with no apparent alternatives under consideration. In particular the reference:
|
|
13
13
|
|
|
14
14
|
```JavaScript
|
|
15
15
|
import 'assign-gingerly/object-extension.js';
|
|
@@ -1131,7 +1131,7 @@ const result = assignGingerly({}, {
|
|
|
1131
1131
|
"[Symbol.for('BqnnTPWRHkWdVGWcGQoAiw')]": true,
|
|
1132
1132
|
style: {
|
|
1133
1133
|
height: '40px'
|
|
1134
|
-
}
|
|
1134
|
+
},
|
|
1135
1135
|
enh: {
|
|
1136
1136
|
mellowYellow?.madAboutFourteen': true
|
|
1137
1137
|
}
|
|
@@ -4126,6 +4126,104 @@ The `#[x]` + `withIds` pattern gives you the Map+WeakRef speed tier automaticall
|
|
|
4126
4126
|
- **Scoped to rootNode** — works correctly inside Shadow DOM (IDs are scoped per shadow root).
|
|
4127
4127
|
- **`assignFrom` only** — this feature is not available in `assignGingerly` or `assignTentatively`.
|
|
4128
4128
|
|
|
4129
|
+
## Inferred Assignments
|
|
4130
|
+
|
|
4131
|
+
`assignFrom` can automatically distribute source values to matching DOM elements based on structural conventions — no manual path strings needed. The [inferencer](./inferencer/) submodule determines the correct property for each element type.
|
|
4132
|
+
|
|
4133
|
+
```TypeScript
|
|
4134
|
+
const vm = {
|
|
4135
|
+
name: 'Alice',
|
|
4136
|
+
email: 'alice@example.com',
|
|
4137
|
+
user: { role: 'admin', avatar: '/img/alice.png' }
|
|
4138
|
+
};
|
|
4139
|
+
|
|
4140
|
+
await assignFrom(outerDiv, {}, {
|
|
4141
|
+
from: vm,
|
|
4142
|
+
inferredAssignments: {
|
|
4143
|
+
byItemprop: ['name', 'email', 'user']
|
|
4144
|
+
}
|
|
4145
|
+
});
|
|
4146
|
+
```
|
|
4147
|
+
|
|
4148
|
+
For each key in `byItemprop`, this finds `[itemprop="${key}"]` elements within the target (respecting itemscope boundaries) and sets the value using the inferred property:
|
|
4149
|
+
|
|
4150
|
+
| Element | Inferred Property |
|
|
4151
|
+
|---------|------------------|
|
|
4152
|
+
| `<span itemprop="name">` | `textContent` |
|
|
4153
|
+
| `<input itemprop="email">` | `value` |
|
|
4154
|
+
| `<time itemprop="date">` | `dateTime` |
|
|
4155
|
+
| `<data itemprop="count">` | `value` |
|
|
4156
|
+
| `<div itemprop="user" itemscope="user-card">` | `ish` (manager routing) |
|
|
4157
|
+
|
|
4158
|
+
**Scope perimeter:** Queries respect itemscope boundaries — `[itemprop="name"]` inside a nested `[itemscope]` won't be matched from the outer scope (donut-hole scoping).
|
|
4159
|
+
|
|
4160
|
+
**Pass `true` to infer all source keys:**
|
|
4161
|
+
|
|
4162
|
+
```TypeScript
|
|
4163
|
+
inferredAssignments: { byItemprop: true }
|
|
4164
|
+
```
|
|
4165
|
+
|
|
4166
|
+
For full details, see [docs/inferred-assignments.md](docs/inferred-assignments.md).
|
|
4167
|
+
|
|
4168
|
+
## Bulk Enhancement with `enhance` and `enhanceAll`
|
|
4169
|
+
|
|
4170
|
+
Apply enhancements in bulk to matching elements using EMC (Element Mount Configuration) JSON files that enhancement packages already publish. No manual registration step needed — enhancements are auto-loaded and registered on demand.
|
|
4171
|
+
|
|
4172
|
+
```TypeScript
|
|
4173
|
+
import { assignFrom } from 'assign-gingerly/assignFrom.js';
|
|
4174
|
+
|
|
4175
|
+
await assignFrom(shadowRoot, { /* normal assignments */ }, {
|
|
4176
|
+
from: vm,
|
|
4177
|
+
enhance: [
|
|
4178
|
+
{ emc: 'be-bound/emc.json', matching: '[name]' },
|
|
4179
|
+
{ emc: 'be-observant/emc.json', matching: '[itemprop]' },
|
|
4180
|
+
]
|
|
4181
|
+
});
|
|
4182
|
+
```
|
|
4183
|
+
|
|
4184
|
+
**How it works:**
|
|
4185
|
+
|
|
4186
|
+
1. Dynamically imports each EMC JSON file
|
|
4187
|
+
2. Extracts the `enhConfig` (spawn path, enhKey)
|
|
4188
|
+
3. Finds matching elements via `querySelectorAll` (using `matching` override, or the EMC's own selector)
|
|
4189
|
+
4. Registers the enhancement in the element's `enhancementRegistry` if not already present
|
|
4190
|
+
5. Spawns the enhancement on each matched element via `element.enh.get(registryItem)`
|
|
4191
|
+
|
|
4192
|
+
**Standalone usage (without `assignFrom`):**
|
|
4193
|
+
|
|
4194
|
+
```TypeScript
|
|
4195
|
+
import { enhanceAll } from 'assign-gingerly/enhanceAll.js';
|
|
4196
|
+
|
|
4197
|
+
await enhanceAll(shadowRoot, [
|
|
4198
|
+
{ emc: 'be-bound/emc.json', matching: '[name]' },
|
|
4199
|
+
]);
|
|
4200
|
+
```
|
|
4201
|
+
|
|
4202
|
+
**With `assignGingerly` (fire-and-forget):**
|
|
4203
|
+
|
|
4204
|
+
```TypeScript
|
|
4205
|
+
assignGingerly(shadowRoot, {
|
|
4206
|
+
'?.someProperty': 'value'
|
|
4207
|
+
}, {
|
|
4208
|
+
enhance: [{ emc: 'be-bound/emc.json', matching: '[name]' }]
|
|
4209
|
+
});
|
|
4210
|
+
// Enhancements apply asynchronously in the background
|
|
4211
|
+
```
|
|
4212
|
+
|
|
4213
|
+
**Configuration:**
|
|
4214
|
+
|
|
4215
|
+
| Field | Type | Description |
|
|
4216
|
+
|-------|------|-------------|
|
|
4217
|
+
| `emc` | `string` | Path to EMC JSON file (dynamically imported) |
|
|
4218
|
+
| `matching` | `string?` | CSS selector override (defaults to EMC's own selector) |
|
|
4219
|
+
| `parse` | `boolean?` | Parse attributes via withAttrs (default: `false`, Phase II) |
|
|
4220
|
+
|
|
4221
|
+
**Notes:**
|
|
4222
|
+
|
|
4223
|
+
- No scope perimeter is applied — the selector does all filtering. For reactive observation of new elements, use [mount-observer](https://github.com/bahrus/mount-observer) instead.
|
|
4224
|
+
- Requires JSON module import support (`import ... with { type: 'json' }`).
|
|
4225
|
+
- Enhancement modules are loaded lazily — zero cost until `enhance` is used.
|
|
4226
|
+
|
|
4129
4227
|
## Custom Assignment with `static assignTo` Protocol
|
|
4130
4228
|
|
|
4131
4229
|
Classes can opt into custom assignment behavior by defining a `static assignTo` method. When `assignGingerly` encounters a property whose current value is an instance of such a class, it delegates the assignment to `assignTo` instead of performing the default merge/replace logic.
|
|
@@ -4788,6 +4886,33 @@ This is useful for:
|
|
|
4788
4886
|
4. On first access, the getter spawns the feature instance, validates it (optionally), caches it, and returns it.
|
|
4789
4887
|
5. Because the property is getter-only (no setter), `assignGingerly` automatically merges into the spawned instance when assigning object values to that property.
|
|
4790
4888
|
|
|
4889
|
+
### Features at Registration Time
|
|
4890
|
+
|
|
4891
|
+
Features can also be bundled directly into enhancement and itemscope manager registration configs — no separate `assignFeatures` call needed:
|
|
4892
|
+
|
|
4893
|
+
```TypeScript
|
|
4894
|
+
// Enhancement with features
|
|
4895
|
+
enhancementRegistry.push({
|
|
4896
|
+
spawn: MyEnhancement,
|
|
4897
|
+
enhKey: 'myEnh',
|
|
4898
|
+
features: {
|
|
4899
|
+
photoTaker: { spawn: PhotoTakerImpl },
|
|
4900
|
+
badgeMaker: { spawn: BadgeMakerImpl },
|
|
4901
|
+
}
|
|
4902
|
+
});
|
|
4903
|
+
|
|
4904
|
+
// Itemscope manager with features
|
|
4905
|
+
customElements.itemscopeRegistry.define('my-manager', {
|
|
4906
|
+
manager: MyManagerClass,
|
|
4907
|
+
lifecycleKeys: { dispose: 'cleanup', resolved: 'isReady' },
|
|
4908
|
+
features: {
|
|
4909
|
+
roundabout: { customData: { /* ... */ } },
|
|
4910
|
+
}
|
|
4911
|
+
});
|
|
4912
|
+
```
|
|
4913
|
+
|
|
4914
|
+
This calls `assignFeatures` automatically under the hood using the shared `customElements.featuresRegistry`. The standalone `assignFeatures` call remains available for cases where you need to associate features with a class you don't own or after initial registration.
|
|
4915
|
+
|
|
4791
4916
|
### Basic example
|
|
4792
4917
|
|
|
4793
4918
|
```JavaScript
|
|
@@ -5660,3 +5785,19 @@ See [time-ticker/wireFeatures.js](https://github.com/bahrus/time-ticker/blob/bas
|
|
|
5660
5785
|
|
|
5661
5786
|
- **Nested features**: Support `?.path?.notation` keys directly in `assignFeatures` (without requiring `PropertyBag`).
|
|
5662
5787
|
- **`@each` + async interaction**: Combine async methods with iteration.
|
|
5788
|
+
|
|
5789
|
+
|
|
5790
|
+
## Viewing Locally
|
|
5791
|
+
|
|
5792
|
+
Any web server that serves static files with server-side includes will do but...
|
|
5793
|
+
|
|
5794
|
+
1. Install git
|
|
5795
|
+
2. Fork/clone this repo
|
|
5796
|
+
3. Install node.js
|
|
5797
|
+
4. Open command window to folder where you cloned this repo
|
|
5798
|
+
5. > git submodule add https://github.com/bahrus/types.git types
|
|
5799
|
+
6. > git submodule add https://github.com/bahrus/inferencer.git inferencer
|
|
5800
|
+
7. > git submodule update --init --recursive
|
|
5801
|
+
8. > npm install
|
|
5802
|
+
9. > npm run serve
|
|
5803
|
+
10. Open http://localhost:8000/demo/ in a modern browser
|
package/assignFrom.js
CHANGED
|
@@ -140,7 +140,7 @@ function mergeHandlerDuplicates(entries) {
|
|
|
140
140
|
}
|
|
141
141
|
return result;
|
|
142
142
|
}
|
|
143
|
-
export async function assignFrom(target, pattern, options) {
|
|
143
|
+
export async function assignFrom(target, pattern, options, permissions) {
|
|
144
144
|
// First: expand looped substitution variables (${x}, ${y}, ${z})
|
|
145
145
|
const expandedPattern = expandSubstitutions(pattern, options);
|
|
146
146
|
// Separate handler commands ( =>), #[x] keys, and normal keys
|
|
@@ -180,27 +180,25 @@ export async function assignFrom(target, pattern, options) {
|
|
|
180
180
|
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
181
181
|
for (const key of idRefNormalKeys) {
|
|
182
182
|
const parsed = parseIdRef(key);
|
|
183
|
-
if (!parsed)
|
|
183
|
+
if (!parsed)
|
|
184
|
+
continue;
|
|
184
185
|
const el = resolveIdVariable(parsed.varName, target, options.withIds);
|
|
185
|
-
if (!el)
|
|
186
|
+
if (!el)
|
|
187
|
+
continue;
|
|
186
188
|
const value = expandedPattern[key];
|
|
187
189
|
if (parsed.remainingPath) {
|
|
188
190
|
// Resolve the RHS value
|
|
189
|
-
const resolvedValue = await resolveValues(
|
|
190
|
-
{ __v: value }, options.from,
|
|
191
|
-
{ withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
|
|
192
|
-
);
|
|
191
|
+
const resolvedValue = await resolveValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
|
|
193
192
|
// Apply remaining path on the resolved element
|
|
194
193
|
assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
|
|
195
194
|
}
|
|
196
195
|
else {
|
|
197
196
|
// No remaining path — resolve and assign directly to the element
|
|
198
|
-
const resolvedValue = await resolveValues(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (!('__v' in resolvedValue)) {
|
|
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 {
|
|
204
202
|
assignGingerly(el, resolvedValue, options);
|
|
205
203
|
}
|
|
206
204
|
}
|
|
@@ -209,7 +207,7 @@ export async function assignFrom(target, pattern, options) {
|
|
|
209
207
|
// Process handler commands ( =>) — dynamically imported only when needed
|
|
210
208
|
if (handlerKeys.length > 0) {
|
|
211
209
|
const { processHandlerCommands } = await import('./processHandlerCommands.js');
|
|
212
|
-
await processHandlerCommands(target, handlerKeys, expandedPattern, options);
|
|
210
|
+
await processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
|
|
213
211
|
}
|
|
214
212
|
// Process #[x] handler keys — resolve element, then pass to handler processing
|
|
215
213
|
if (idRefHandlerKeys.length > 0 && options.withIds) {
|
|
@@ -217,19 +215,40 @@ export async function assignFrom(target, pattern, options) {
|
|
|
217
215
|
const { processHandlerCommands } = await import('./processHandlerCommands.js');
|
|
218
216
|
for (const key of idRefHandlerKeys) {
|
|
219
217
|
const parsed = parseIdRef(key);
|
|
220
|
-
if (!parsed)
|
|
218
|
+
if (!parsed)
|
|
219
|
+
continue;
|
|
221
220
|
const el = resolveIdVariable(parsed.varName, target, options.withIds);
|
|
222
|
-
if (!el)
|
|
223
|
-
|
|
221
|
+
if (!el)
|
|
222
|
+
continue;
|
|
223
|
+
// Build a synthetic key for processHandlerCommands:
|
|
224
|
+
// The resolved element becomes the target, remaining path is the LHS
|
|
224
225
|
const syntheticKey = parsed.remainingPath
|
|
225
226
|
? `${parsed.remainingPath} =>`
|
|
226
227
|
: ' =>';
|
|
227
228
|
const syntheticPattern = {
|
|
228
229
|
[syntheticKey]: expandedPattern[key]
|
|
229
230
|
};
|
|
230
|
-
await processHandlerCommands(el, [syntheticKey], syntheticPattern, options);
|
|
231
|
+
await processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
|
|
231
232
|
}
|
|
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
|
+
}
|
|
233
252
|
return target;
|
|
234
253
|
}
|
|
235
254
|
/**
|
package/assignFrom.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
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 */
|
|
@@ -63,6 +64,42 @@ export interface AssignFromOptions extends IAssignGingerlyOptions, ResolveValues
|
|
|
63
64
|
* }
|
|
64
65
|
*/
|
|
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 }>;
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
/**
|
|
@@ -205,7 +242,8 @@ function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
|
|
|
205
242
|
export async function assignFrom(
|
|
206
243
|
target: any,
|
|
207
244
|
pattern: Record<string, any>,
|
|
208
|
-
options: AssignFromOptions
|
|
245
|
+
options: AssignFromOptions,
|
|
246
|
+
permissions?: AssignPermissions
|
|
209
247
|
): Promise<any> {
|
|
210
248
|
// First: expand looped substitution variables (${x}, ${y}, ${z})
|
|
211
249
|
const expandedPattern = expandSubstitutions(pattern, options);
|
|
@@ -282,7 +320,7 @@ export async function assignFrom(
|
|
|
282
320
|
// Process handler commands ( =>) — dynamically imported only when needed
|
|
283
321
|
if (handlerKeys.length > 0) {
|
|
284
322
|
const { processHandlerCommands } = await import('./processHandlerCommands.js');
|
|
285
|
-
await processHandlerCommands(target, handlerKeys, expandedPattern, options);
|
|
323
|
+
await processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
|
|
286
324
|
}
|
|
287
325
|
|
|
288
326
|
// Process #[x] handler keys — resolve element, then pass to handler processing
|
|
@@ -307,10 +345,31 @@ export async function assignFrom(
|
|
|
307
345
|
[syntheticKey]: expandedPattern[key]
|
|
308
346
|
};
|
|
309
347
|
|
|
310
|
-
await processHandlerCommands(el, [syntheticKey], syntheticPattern, options);
|
|
348
|
+
await processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
|
|
311
349
|
}
|
|
312
350
|
}
|
|
313
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
|
+
}
|
|
372
|
+
|
|
314
373
|
return target;
|
|
315
374
|
}
|
|
316
375
|
|
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;
|
package/assignGingerly.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
import { EnhancementConfig } from "./types/assign-gingerly/types";
|
|
4
|
+
import type { FeatureConfigsMap } from "./types/assign-gingerly/types";
|
|
5
|
+
import type { AssignPermissions } from "./isAllowedImportPath.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Constructor signature for ItemScope Manager classes
|
|
@@ -116,6 +118,18 @@ export interface IAssignGingerlyOptions {
|
|
|
116
118
|
* // Calls: (await el.whenFeatureReady('photoTaker')).someProp = 'hello'
|
|
117
119
|
*/
|
|
118
120
|
withAsyncMethods?: string[] | Set<string>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Bulk enhancement application via EMC JSON configs.
|
|
124
|
+
* Finds matching elements and spawns enhancements on them.
|
|
125
|
+
* Fire-and-forget (async) — assignGingerly remains synchronous.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* enhance: [
|
|
129
|
+
* { emc: 'be-bound/emc.json', matching: '[name]' },
|
|
130
|
+
* ]
|
|
131
|
+
*/
|
|
132
|
+
enhance?: Array<{ emc: string; matching?: string; parse?: boolean }>;
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
/**
|
|
@@ -167,6 +181,23 @@ export class EnhancementRegistry extends EventTarget {
|
|
|
167
181
|
|
|
168
182
|
// Dispatch event after adding items
|
|
169
183
|
this.dispatchEvent(new EnhancementRegisteredEvent(items));
|
|
184
|
+
|
|
185
|
+
// Process features if present (fire-and-forget, uses shared featuresRegistry)
|
|
186
|
+
const itemsArr = Array.isArray(items) ? items : [items];
|
|
187
|
+
for (const item of itemsArr) {
|
|
188
|
+
if (item.features) {
|
|
189
|
+
this.#assignFeatures(item.spawn, item.features);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async #assignFeatures(spawn: any, features: FeatureConfigsMap): Promise<void> {
|
|
195
|
+
const { assignFeatures } = await import('./assignFeatures.js');
|
|
196
|
+
const featuresRegistry = (this as any)._featuresRegistry
|
|
197
|
+
?? (typeof customElements !== 'undefined' ? (customElements as any).featuresRegistry : undefined);
|
|
198
|
+
if (featuresRegistry) {
|
|
199
|
+
assignFeatures(spawn, features, featuresRegistry);
|
|
200
|
+
}
|
|
170
201
|
}
|
|
171
202
|
|
|
172
203
|
getItems(): EnhancementConfig[] {
|
|
@@ -218,6 +249,20 @@ export class ItemscopeRegistry extends EventTarget {
|
|
|
218
249
|
}
|
|
219
250
|
this.#configs.set(name, config);
|
|
220
251
|
this.dispatchEvent(new Event(name));
|
|
252
|
+
|
|
253
|
+
// Process features if present (fire-and-forget, uses shared featuresRegistry)
|
|
254
|
+
if ((config as any).features) {
|
|
255
|
+
this.#assignFeatures(config.manager, (config as any).features);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async #assignFeatures(manager: any, features: FeatureConfigsMap): Promise<void> {
|
|
260
|
+
const { assignFeatures } = await import('./assignFeatures.js');
|
|
261
|
+
const featuresRegistry = (this as any)._featuresRegistry
|
|
262
|
+
?? (typeof customElements !== 'undefined' ? (customElements as any).featuresRegistry : undefined);
|
|
263
|
+
if (featuresRegistry) {
|
|
264
|
+
assignFeatures(manager, features, featuresRegistry);
|
|
265
|
+
}
|
|
221
266
|
}
|
|
222
267
|
|
|
223
268
|
/**
|
|
@@ -691,7 +736,8 @@ function applyAliases(key: string, aliasMap: Map<string, string>): string {
|
|
|
691
736
|
export function assignGingerly(
|
|
692
737
|
target: any,
|
|
693
738
|
source: Record<string | symbol, any>,
|
|
694
|
-
options?: IAssignGingerlyOptions
|
|
739
|
+
options?: IAssignGingerlyOptions,
|
|
740
|
+
permissions?: AssignPermissions
|
|
695
741
|
): any {
|
|
696
742
|
if (!target || typeof target !== 'object') {
|
|
697
743
|
return target;
|
|
@@ -1274,6 +1320,13 @@ export function assignGingerly(
|
|
|
1274
1320
|
});
|
|
1275
1321
|
}
|
|
1276
1322
|
|
|
1323
|
+
// Fire-and-forget bulk enhancements (async, non-blocking)
|
|
1324
|
+
if (options?.enhance && options.enhance.length > 0 && typeof target === 'object' && target instanceof Element) {
|
|
1325
|
+
import('./enhanceAll.js').then(({ enhanceAll }) => {
|
|
1326
|
+
enhanceAll(target, options!.enhance!, permissions);
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1277
1330
|
return target;
|
|
1278
1331
|
}
|
|
1279
1332
|
|
package/beVigilant.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* beVigilant.ts — MutationObserver-based reactive inference for newly added elements.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imported when `inferredAssignments.beVigilant: true` is set.
|
|
5
|
+
* Watches for new [itemprop] elements and attribute changes, applying inferred
|
|
6
|
+
* assignments from the live `from` object.
|
|
7
|
+
*
|
|
8
|
+
* Requires an AbortSignal for cleanup (disconnects the observer on abort).
|
|
9
|
+
*/
|
|
10
|
+
import { Infer } from './inferencer/inferencer.js';
|
|
11
|
+
import { withScopePerimeter } from './inferencer/withScopePerimeter.js';
|
|
12
|
+
import { registerInCache } from './resolveIdRef.js';
|
|
13
|
+
/**
|
|
14
|
+
* Set up a MutationObserver that watches for new [itemprop] elements
|
|
15
|
+
* and applies inferred assignments from the live `from` object.
|
|
16
|
+
*
|
|
17
|
+
* @param target - The DOM element to observe
|
|
18
|
+
* @param from - The live source object (read at observation time for current values)
|
|
19
|
+
* @param config - The inferredAssignments config (byItemprop keys)
|
|
20
|
+
* @param signal - AbortSignal for cleanup (required)
|
|
21
|
+
*/
|
|
22
|
+
export function setupVigilantObserver(target, from, config, signal) {
|
|
23
|
+
const keys = config.byItemprop === true ? null : new Set(config.byItemprop);
|
|
24
|
+
const observer = new MutationObserver((mutations) => {
|
|
25
|
+
for (const mutation of mutations) {
|
|
26
|
+
if (mutation.type === 'childList') {
|
|
27
|
+
// Process added nodes and their descendants
|
|
28
|
+
for (const node of mutation.addedNodes) {
|
|
29
|
+
if (!(node instanceof Element))
|
|
30
|
+
continue;
|
|
31
|
+
// Check the added element itself
|
|
32
|
+
processElement(node, from, keys, target);
|
|
33
|
+
// Check descendants (respecting scope perimeter)
|
|
34
|
+
const nested = node.querySelectorAll('[itemprop]');
|
|
35
|
+
for (const el of nested) {
|
|
36
|
+
processElement(el, from, keys, target);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (mutation.type === 'attributes' && mutation.target instanceof Element) {
|
|
41
|
+
// itemprop attribute was added or changed on an existing element
|
|
42
|
+
processElement(mutation.target, from, keys, target);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
observer.observe(target, {
|
|
47
|
+
childList: true,
|
|
48
|
+
subtree: true,
|
|
49
|
+
attributes: true,
|
|
50
|
+
attributeFilter: ['itemprop']
|
|
51
|
+
});
|
|
52
|
+
// Cleanup on abort
|
|
53
|
+
signal.addEventListener('abort', () => observer.disconnect(), { once: true });
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Process a single element: check if it matches, apply inferred value, cache it.
|
|
57
|
+
*/
|
|
58
|
+
function processElement(element, from, keys, scopeRoot) {
|
|
59
|
+
const itemprop = element.getAttribute('itemprop');
|
|
60
|
+
if (!itemprop)
|
|
61
|
+
return;
|
|
62
|
+
if (keys && !keys.has(itemprop))
|
|
63
|
+
return;
|
|
64
|
+
if (!(itemprop in from))
|
|
65
|
+
return;
|
|
66
|
+
if (!withScopePerimeter(scopeRoot, element, '[itemscope]'))
|
|
67
|
+
return;
|
|
68
|
+
// Set value via inferencer
|
|
69
|
+
const infer = new Infer(element, itemprop);
|
|
70
|
+
infer.value = from[itemprop];
|
|
71
|
+
// Cache for future fast access
|
|
72
|
+
registerInCache(scopeRoot, itemprop, element);
|
|
73
|
+
}
|