assign-gingerly 0.0.17 → 0.0.19
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 +66 -47
- package/assignGingerly.js +22 -10
- package/assignGingerly.ts +5 -11
- package/buildCSSQuery.js +6 -37
- package/buildCSSQuery.ts +7 -49
- package/global.d.ts +25 -0
- package/index.js +2 -1
- package/index.ts +2 -1
- package/object-extension.js +23 -14
- package/object-extension.ts +6 -16
- package/package.json +5 -1
- package/parseWithAttrs.js +20 -38
- package/parseWithAttrs.ts +2 -50
- package/resolveTemplate.js +49 -0
- package/resolveTemplate.ts +59 -0
- package/types/assign-gingerly/types.d.ts +15 -5
package/README.md
CHANGED
|
@@ -23,8 +23,14 @@ One can achieve the same functionality with a little more work, and "playing nic
|
|
|
23
23
|
|
|
24
24
|
Not only does this polyfill package allow merging data properties onto objects that are expecting them, this polyfill also provides the ability to merge *augmented behavior* onto run-time objects without sub classing all such objects of the same type. This includes the ability to spawn an instance of a class and "merge" it into the API of the original object in an elegant way that is easy to wrap one's brain around, without ever blocking access to the original object or breaking it.
|
|
25
25
|
|
|
26
|
+
|
|
27
|
+
|
|
26
28
|
So we are providing a form of the ["Decorator Pattern"](https://en.wikipedia.org/wiki/Decorator_pattern) or perhaps more accurately the [Extension Object Pattern](https://swiftorial.com/swiftlessons/design-patterns/structural-patterns/extension-object-pattern) as tailored for the quirks of the web.
|
|
27
29
|
|
|
30
|
+
## Custom Enhancement Registry
|
|
31
|
+
|
|
32
|
+
On top of that, this polyfill package builds on the newly minted Custom Element Registry, adding an additional EnhancementRegistry object on top of the customElementRegistry object associated with all elements, to be able to manage namespace conflicts, and, importantly, as a basis for defining custom attributes associated with the enhancements.
|
|
33
|
+
|
|
28
34
|
So in our view this package helps fill the void left by not supporting the "is" attribute for built-in elements (but is not a complete solution, just a critical building block). Mount-observer, mount-observer-script-element, and custom enhancements builds on top of the critical role that assign-gingerly plays.
|
|
29
35
|
|
|
30
36
|
Anyway, let's start out detailing the more innocent features of this package / polyfill.
|
|
@@ -33,7 +39,7 @@ The two utility functions are:
|
|
|
33
39
|
|
|
34
40
|
## assignGingerly
|
|
35
41
|
|
|
36
|
-
assignGingerly builds on Object.assign. Like Object.assign, the object getting assigned can be a JSON stringified object. Some of the unusual syntax we see with assignGingerly is there to continue to support JSON deserialized objects as a viable argument to be passed.
|
|
42
|
+
assignGingerly builds on Object.assign. Like Object.assign, the object getting assigned can often be a JSON stringified object. Some of the unusual syntax we see with assignGingerly is there to continue to support JSON deserialized objects as a viable argument to be passed.
|
|
37
43
|
|
|
38
44
|
assign-gingerly adds support for:
|
|
39
45
|
|
|
@@ -96,7 +102,9 @@ console.log(obj);
|
|
|
96
102
|
|
|
97
103
|
When the right hand side of an expression is an object, assignGingerly is recursively applied (passing the third argument in if applicable, which will be discussed below).
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
Of course, just as Object.assign led to object spread notation, assignGingerly could lead to some sort of deep structural JavaScript syntax, but that is outside the scope of this polyfill package.
|
|
106
|
+
|
|
107
|
+
While we are in the business of passing values of object A into object B, we might as well add some extremely common behavior that allows updating properties of object B based on the current values of object B -- things like incrementing, toggling, and deleting. Deleting is critical for assignTentatively, but is included with both functions.
|
|
100
108
|
|
|
101
109
|
## Example 4 - Incrementing values with += command
|
|
102
110
|
|
|
@@ -213,8 +221,7 @@ console.log(obj);
|
|
|
213
221
|
- Non-existent properties are silently skipped
|
|
214
222
|
- If the parent path doesn't exist, the command is silently skipped
|
|
215
223
|
- For root-level deletion, use ` -=` (space before -=)
|
|
216
|
-
|
|
217
|
-
// }
|
|
224
|
+
|
|
218
225
|
|
|
219
226
|
|
|
220
227
|
|
|
@@ -285,9 +292,9 @@ This guarantees that applying the reversal object restores the object to its exa
|
|
|
285
292
|
## Dependency injection based on a registry object and a Symbolic reference mapping
|
|
286
293
|
|
|
287
294
|
```Typescript
|
|
288
|
-
interface
|
|
289
|
-
spawn: {new(
|
|
290
|
-
symlinks
|
|
295
|
+
interface IEnhancementRegistryItem<T = any, TObjToExtend = any> {
|
|
296
|
+
spawn: {new(objToExtend: TObjToExtend, ctx: SpawnContext, initVals: Partial<T>): T}
|
|
297
|
+
symlinks?: {[key: symbol]: keyof T}
|
|
291
298
|
// Optional: for element enhancement access
|
|
292
299
|
enhKey?: string
|
|
293
300
|
// Optional: automatic attribute parsing
|
|
@@ -310,15 +317,15 @@ class YourEnhancement{
|
|
|
310
317
|
set madAboutFourteen(nv){}
|
|
311
318
|
}
|
|
312
319
|
|
|
313
|
-
class
|
|
314
|
-
push(
|
|
320
|
+
class EnhancementRegistry{
|
|
321
|
+
push(IEnhancementRegistryItem | IEnhancementRegistryItem[]){
|
|
315
322
|
...
|
|
316
323
|
}
|
|
317
324
|
}
|
|
318
325
|
|
|
319
326
|
//Here's where the dependency injection mapping takes place
|
|
320
|
-
const
|
|
321
|
-
|
|
327
|
+
const EnhancementRegistry = new EnhancementRegistry;
|
|
328
|
+
EnhancementRegistry.push([
|
|
322
329
|
{
|
|
323
330
|
symlinks: {
|
|
324
331
|
[isHappy]: 'isHappy'
|
|
@@ -340,7 +347,7 @@ const result = assignGingerly({}, {
|
|
|
340
347
|
'?.style?.height': '40px',
|
|
341
348
|
'?.enh?.mellowYellow?.madAboutFourteen': true
|
|
342
349
|
}, {
|
|
343
|
-
registry:
|
|
350
|
+
registry: EnhancementRegistry
|
|
344
351
|
});
|
|
345
352
|
//result.set[isMellow] = false;
|
|
346
353
|
```
|
|
@@ -394,7 +401,7 @@ const registryItem = {
|
|
|
394
401
|
enhKey: 'myEnh'
|
|
395
402
|
};
|
|
396
403
|
|
|
397
|
-
const registry = new
|
|
404
|
+
const registry = new EnhancementRegistry();
|
|
398
405
|
registry.push(registryItem);
|
|
399
406
|
|
|
400
407
|
const element = document.createElement('div');
|
|
@@ -431,7 +438,7 @@ const registryItem = {
|
|
|
431
438
|
}
|
|
432
439
|
};
|
|
433
440
|
|
|
434
|
-
const registry = new
|
|
441
|
+
const registry = new EnhancementRegistry();
|
|
435
442
|
registry.push(registryItem);
|
|
436
443
|
|
|
437
444
|
const target = {};
|
|
@@ -458,7 +465,7 @@ const result = assignGingerly({}, {
|
|
|
458
465
|
'?.style.height': '40px',
|
|
459
466
|
'?.enh?.mellowYellow?.madAboutFourteen': true
|
|
460
467
|
}, {
|
|
461
|
-
registry:
|
|
468
|
+
registry: EnhancementRegistry
|
|
462
469
|
});
|
|
463
470
|
```
|
|
464
471
|
</details>
|
|
@@ -508,6 +515,9 @@ The prototype extensions are non-enumerable and won't appear in `Object.keys()`
|
|
|
508
515
|
|
|
509
516
|
This package includes support for Chrome's scoped custom element registries, which automatically integrates dependency injection in harmony with scoped custom elements DOM sections or ShadowRoots.
|
|
510
517
|
|
|
518
|
+
> [!NOTE]
|
|
519
|
+
> Safari/WebKit played a critical role in pushing scoped custom element registries forward, and announced with little fanfare or documentation that [Safari 26 supports it](https://developer.apple.com/documentation/safari-release-notes/safari-26-release-notes). However, the Playwright test machinery's cross platform Safari test browser doesn't yet support it.
|
|
520
|
+
|
|
511
521
|
<details>
|
|
512
522
|
<summary>Automatic Registry Population</summary>
|
|
513
523
|
|
|
@@ -515,7 +525,6 @@ When `assignGingerly` or `assignTentatively` is called on an Element instance wi
|
|
|
515
525
|
|
|
516
526
|
```TypeScript
|
|
517
527
|
import 'assign-gingerly/object-extension.js';
|
|
518
|
-
import { BaseRegistry } from 'assign-gingerly';
|
|
519
528
|
|
|
520
529
|
// Set up a registry on the custom element registry
|
|
521
530
|
const myElement = document.createElement('div');
|
|
@@ -542,7 +551,7 @@ myElement.assignGingerly({
|
|
|
542
551
|
<details>
|
|
543
552
|
<summary>Lazy Registry Creation</summary>
|
|
544
553
|
|
|
545
|
-
Each `CustomElementRegistry` instance gets its own `enhancementRegistry` property via a lazy getter. The `
|
|
554
|
+
Each `CustomElementRegistry` instance gets its own `enhancementRegistry` property via a lazy getter. The `EnhancementRegistry` instance is created on first access and cached for subsequent uses:
|
|
546
555
|
|
|
547
556
|
```TypeScript
|
|
548
557
|
const element1 = document.createElement('div');
|
|
@@ -563,7 +572,7 @@ console.log(registry1 === element1.customElementRegistry.enhancementRegistry); /
|
|
|
563
572
|
You can still provide an explicit `registry` option to override the automatic behavior:
|
|
564
573
|
|
|
565
574
|
```TypeScript
|
|
566
|
-
const customRegistry = new
|
|
575
|
+
const customRegistry = new EnhancementRegistry();
|
|
567
576
|
// ... configure customRegistry ...
|
|
568
577
|
|
|
569
578
|
myElement.assignGingerly({
|
|
@@ -584,7 +593,7 @@ The `enh.set` proxy allows you to assign properties to enhancements using a clea
|
|
|
584
593
|
|
|
585
594
|
```TypeScript
|
|
586
595
|
import 'assign-gingerly/object-extension.js';
|
|
587
|
-
//import {
|
|
596
|
+
//import { EnhancementRegistry } from 'assign-gingerly';
|
|
588
597
|
|
|
589
598
|
// Define an enhancement class
|
|
590
599
|
class MyEnhancement {
|
|
@@ -649,15 +658,15 @@ This approach is part of a proposal to WHATWG for standardizing element enhancem
|
|
|
649
658
|
|
|
650
659
|
### Constructor Signature
|
|
651
660
|
|
|
652
|
-
|
|
661
|
+
Element enhancement classes should follow this constructor signature:
|
|
653
662
|
|
|
654
663
|
```TypeScript
|
|
655
664
|
interface SpawnContext<T, TMountContext = any> {
|
|
656
|
-
config:
|
|
665
|
+
config: IEnhancementRegistryItem<T>;
|
|
657
666
|
mountCtx?: TMountContext; // Optional custom context passed by caller
|
|
658
667
|
}
|
|
659
668
|
|
|
660
|
-
class Enhancement {
|
|
669
|
+
class Enhancement<T> {
|
|
661
670
|
constructor(
|
|
662
671
|
oElement?: Element, // The element being enhanced
|
|
663
672
|
ctx?: SpawnContext, // Context with registry item info and optional mountCtx
|
|
@@ -671,6 +680,8 @@ class Enhancement {
|
|
|
671
680
|
|
|
672
681
|
All parameters are optional for backward compatibility with existing code.
|
|
673
682
|
|
|
683
|
+
Note that the class need not extend any base class or leverage any mixins. In fact, ES5 prototype functions can be used, and in both cases are instanted using new .... Arrow functions cannot be used.
|
|
684
|
+
|
|
674
685
|
<details>
|
|
675
686
|
<summary>Passing Custom Context</summary>
|
|
676
687
|
|
|
@@ -705,10 +716,10 @@ This is useful for:
|
|
|
705
716
|
In addition to spawn and symlinks, registry items support optional properties `enhKey`, `withAttrs`, `canSpawn`, and `lifecycleKeys`:
|
|
706
717
|
|
|
707
718
|
```TypeScript
|
|
708
|
-
interface
|
|
719
|
+
interface IEnhancementRegistryItem<T, TObj = Element> {
|
|
709
720
|
spawn: {
|
|
710
|
-
new (
|
|
711
|
-
canSpawn?: (obj:
|
|
721
|
+
new (obj?: TObj, ctx?: SpawnContext<T>, initVals?: Partial<T>): T;
|
|
722
|
+
canSpawn?: (obj: TObj, ctx?: SpawnContext<T>) => boolean; // Optional spawn guard
|
|
712
723
|
};
|
|
713
724
|
symlinks?: { [key: string | symbol]: keyof T };
|
|
714
725
|
enhKey?: string; // String identifier for set proxy access
|
|
@@ -802,10 +813,10 @@ console.log(element.enh.plainData); // { prop1: 'value1', prop2: 'value2' }
|
|
|
802
813
|
<details>
|
|
803
814
|
<summary>Finding Registry Items by enhKey</summary>
|
|
804
815
|
|
|
805
|
-
The `
|
|
816
|
+
The `EnhancementRegistry` class includes a `findByEnhKey` method:
|
|
806
817
|
|
|
807
818
|
```TypeScript
|
|
808
|
-
const registry = new
|
|
819
|
+
const registry = new EnhancementRegistry();
|
|
809
820
|
registry.push({
|
|
810
821
|
spawn: MyEnhancement,
|
|
811
822
|
enhKey: 'myEnh'
|
|
@@ -818,7 +829,7 @@ console.log(item.enhKey); // 'myEnh'
|
|
|
818
829
|
|
|
819
830
|
### Programmatic Instance Spawning with `enh.get()`
|
|
820
831
|
|
|
821
|
-
The `enh.get(registryItem)` method provides a programmatic way to spawn or retrieve enhancement instances:
|
|
832
|
+
The `enh.get(registryItem)` method provides a programmatic way to spawn or retrieve previously instantiated enhancement instances:
|
|
822
833
|
|
|
823
834
|
```TypeScript
|
|
824
835
|
const registryItem = {
|
|
@@ -948,9 +959,9 @@ Note: Symbol event names are not yet supported by the platform but have been req
|
|
|
948
959
|
|
|
949
960
|
</details>
|
|
950
961
|
|
|
951
|
-
### Disposing Enhancement Instances with `enh.dispose()`
|
|
962
|
+
### Disposing Enhancement Instances with `enh.dispose(regItem)`
|
|
952
963
|
|
|
953
|
-
The `enh.dispose()` method provides a way to clean up and remove enhancement instances:
|
|
964
|
+
The `enh.dispose(regItem)` method provides a way to clean up and remove enhancement instances:
|
|
954
965
|
|
|
955
966
|
```TypeScript
|
|
956
967
|
class MyEnhancement {
|
|
@@ -991,7 +1002,7 @@ const instance = element.enh.get(registryItem);
|
|
|
991
1002
|
element.enh.dispose(registryItem);
|
|
992
1003
|
```
|
|
993
1004
|
|
|
994
|
-
**How `enh.dispose()` works:**
|
|
1005
|
+
**How `enh.dispose(regItem)` works:**
|
|
995
1006
|
|
|
996
1007
|
1. **Retrieves instance**: Gets the spawned instance from the global instance map
|
|
997
1008
|
2. **Calls lifecycle method**: If `lifecycleKeys.dispose` is specified, calls that method on the instance (passing the registry item)
|
|
@@ -1043,9 +1054,9 @@ element.enh.dispose(registryItem); // Stops timer and cleans up
|
|
|
1043
1054
|
- Calling `enh.get()` again will create a new instance
|
|
1044
1055
|
- The enhancement property is removed from the enh container
|
|
1045
1056
|
|
|
1046
|
-
### Waiting for Async Initialization with `enh.whenResolved()`
|
|
1057
|
+
### Waiting for Async Initialization with `enh.whenResolved(regItem)`
|
|
1047
1058
|
|
|
1048
|
-
The `enh.whenResolved()` method provides a way to wait for asynchronous enhancement initialization:
|
|
1059
|
+
The `enh.whenResolved(regItem)` method provides a way to wait for asynchronous enhancement initialization:
|
|
1049
1060
|
|
|
1050
1061
|
```TypeScript
|
|
1051
1062
|
class AsyncEnhancement extends EventTarget {
|
|
@@ -1217,7 +1228,7 @@ class DivOnlyEnhancement {
|
|
|
1217
1228
|
}
|
|
1218
1229
|
}
|
|
1219
1230
|
|
|
1220
|
-
const registry = new
|
|
1231
|
+
const registry = new EnhancementRegistry();
|
|
1221
1232
|
registry.push({
|
|
1222
1233
|
spawn: DivOnlyEnhancement,
|
|
1223
1234
|
enhKey: 'divOnly'
|
|
@@ -1251,7 +1262,7 @@ static canSpawn(obj: any, ctx?: SpawnContext<T>): boolean
|
|
|
1251
1262
|
```
|
|
1252
1263
|
|
|
1253
1264
|
- `obj`: The target object being enhanced (element, plain object, etc.)
|
|
1254
|
-
- `ctx`: Optional spawn context containing `{ config:
|
|
1265
|
+
- `ctx`: Optional spawn context containing `{ config: IEnhancementRegistryItem<T> }`
|
|
1255
1266
|
- Returns: `true` to allow spawning, `false` to block
|
|
1256
1267
|
|
|
1257
1268
|
### Use Cases
|
|
@@ -1318,7 +1329,7 @@ class ValidatedEnhancement {
|
|
|
1318
1329
|
### Example with Dependency Injection
|
|
1319
1330
|
|
|
1320
1331
|
```TypeScript
|
|
1321
|
-
import assignGingerly, {
|
|
1332
|
+
import assignGingerly, { EnhancementRegistry } from 'assign-gingerly';
|
|
1322
1333
|
|
|
1323
1334
|
class ElementOnlyEnhancement {
|
|
1324
1335
|
value = null;
|
|
@@ -1328,7 +1339,7 @@ class ElementOnlyEnhancement {
|
|
|
1328
1339
|
}
|
|
1329
1340
|
}
|
|
1330
1341
|
|
|
1331
|
-
const registry = new
|
|
1342
|
+
const registry = new EnhancementRegistry();
|
|
1332
1343
|
const enhSymbol = Symbol.for('myEnhancement');
|
|
1333
1344
|
|
|
1334
1345
|
registry.push({
|
|
@@ -1351,11 +1362,11 @@ assignGingerly(element, { [enhSymbol]: 'test' }, { registry });
|
|
|
1351
1362
|
|
|
1352
1363
|
## Parsing Attributes with `parseWithAttrs`
|
|
1353
1364
|
|
|
1354
|
-
The `parseWithAttrs` function provides a declarative way to read and parse HTML attributes
|
|
1365
|
+
The `parseWithAttrs` function provides a declarative way to read and parse HTML attributes and pass the parsed values into the spawned enhancement constructor.
|
|
1355
1366
|
|
|
1356
1367
|
### Automatic Integration with Enhancement Spawning
|
|
1357
1368
|
|
|
1358
|
-
**Important**: When using the `enh.get()`, `enh.set`, or `assignGingerly()` methods with registry items, you typically **do not need to call `parseWithAttrs()` manually**. The attribute parsing happens automatically during enhancement spawning when you include a `withAttrs` property in your registry item.
|
|
1369
|
+
**Important**: When using the `enh.get()`, `enh.set`, or `assignGingerly()` methods with registry items, you typically **do not need to call `parseWithAttrs()` manually**. The attribute parsing happens automatically during enhancement spawning when you include a `withAttrs` property in your registry item configuration.
|
|
1359
1370
|
|
|
1360
1371
|
```html
|
|
1361
1372
|
<my-element my-enhancement-count="42" my-enhancement-theme="dark"></my-element>
|
|
@@ -1747,6 +1758,8 @@ const result = parseWithAttrs(element, config);
|
|
|
1747
1758
|
|
|
1748
1759
|
**Built-in Named Parsers:**
|
|
1749
1760
|
|
|
1761
|
+
[TODO]: Check if this is all needed
|
|
1762
|
+
|
|
1750
1763
|
The following parsers are pre-registered in `globalParserRegistry`:
|
|
1751
1764
|
|
|
1752
1765
|
- `'timestamp'` - Parses ISO date string to Unix timestamp (milliseconds)
|
|
@@ -2209,7 +2222,11 @@ const elements = document.querySelectorAll(query);
|
|
|
2209
2222
|
**Without selectors (matches any element):**
|
|
2210
2223
|
|
|
2211
2224
|
```TypeScript
|
|
2225
|
+
// Omit the selectors parameter
|
|
2226
|
+
const query = buildCSSQuery(config);
|
|
2227
|
+
// or explicitly pass empty string
|
|
2212
2228
|
const query = buildCSSQuery(config, '');
|
|
2229
|
+
|
|
2213
2230
|
console.log(query);
|
|
2214
2231
|
// '[my-component], [enh-my-component], [my-component-theme], [enh-my-component-theme]'
|
|
2215
2232
|
|
|
@@ -2300,7 +2317,7 @@ buildCSSQuery(config, 'div');
|
|
|
2300
2317
|
|
|
2301
2318
|
### Edge Cases
|
|
2302
2319
|
|
|
2303
|
-
**
|
|
2320
|
+
**Omitting or empty selectors return attribute-only selectors:**
|
|
2304
2321
|
```TypeScript
|
|
2305
2322
|
const config = {
|
|
2306
2323
|
spawn: MyClass,
|
|
@@ -2310,8 +2327,10 @@ const config = {
|
|
|
2310
2327
|
}
|
|
2311
2328
|
};
|
|
2312
2329
|
|
|
2313
|
-
buildCSSQuery(config
|
|
2314
|
-
//
|
|
2330
|
+
buildCSSQuery(config); // Omit selectors parameter
|
|
2331
|
+
// or
|
|
2332
|
+
buildCSSQuery(config, ''); // Empty string
|
|
2333
|
+
// Both return: '[my-attr], [enh-my-attr], [my-attr-theme], [enh-my-attr-theme]'
|
|
2315
2334
|
// Matches any element with these attributes
|
|
2316
2335
|
```
|
|
2317
2336
|
|
|
@@ -2338,7 +2357,7 @@ buildCSSQuery(config, ' div , span , p ');
|
|
|
2338
2357
|
1. **Mount Observer Integration**: Find elements that need enhancement
|
|
2339
2358
|
```TypeScript
|
|
2340
2359
|
// Match any element with the attributes
|
|
2341
|
-
const query = buildCSSQuery(enhancementConfig
|
|
2360
|
+
const query = buildCSSQuery(enhancementConfig);
|
|
2342
2361
|
const observer = new MutationObserver(() => {
|
|
2343
2362
|
const elements = document.querySelectorAll(query);
|
|
2344
2363
|
elements.forEach(el => enhance(el));
|
|
@@ -2364,19 +2383,19 @@ buildCSSQuery(config, ' div , span , p ');
|
|
|
2364
2383
|
```TypeScript
|
|
2365
2384
|
function buildCSSQuery(
|
|
2366
2385
|
config: EnhancementConfig,
|
|
2367
|
-
selectors
|
|
2386
|
+
selectors?: string
|
|
2368
2387
|
): string
|
|
2369
2388
|
```
|
|
2370
2389
|
|
|
2371
2390
|
**Parameters:**
|
|
2372
2391
|
- `config`: Enhancement configuration with `withAttrs` property
|
|
2373
|
-
- `selectors
|
|
2374
|
-
- If
|
|
2392
|
+
- `selectors` (optional): Comma-separated CSS selectors (e.g., `'div, span'`)
|
|
2393
|
+
- If omitted or empty string, returns attribute selectors without element prefix
|
|
2375
2394
|
- This matches any element with the specified attributes
|
|
2376
2395
|
|
|
2377
2396
|
**Returns:**
|
|
2378
2397
|
- CSS query string with cross-product of selectors and attributes
|
|
2379
|
-
- If selectors is empty: returns attribute-only selectors (e.g., `'[attr], [enh-attr]'`)
|
|
2398
|
+
- If selectors is omitted or empty: returns attribute-only selectors (e.g., `'[attr], [enh-attr]'`)
|
|
2380
2399
|
- If withAttrs is missing or empty: returns empty string
|
|
2381
2400
|
|
|
2382
2401
|
**Throws:**
|
package/assignGingerly.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
// Polyfill for Map.prototype.getOrInsert and WeakMap.prototype.getOrInsert
|
|
2
|
+
if (typeof Map.prototype.getOrInsert !== 'function') {
|
|
3
|
+
Map.prototype.getOrInsert = function(key, insert) {
|
|
4
|
+
if (this.has(key)) return this.get(key);
|
|
5
|
+
const value = insert();
|
|
6
|
+
this.set(key, value);
|
|
7
|
+
return value;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
if (typeof WeakMap.prototype.getOrInsert !== 'function') {
|
|
11
|
+
WeakMap.prototype.getOrInsert = function(key, insert) {
|
|
12
|
+
if (this.has(key)) return this.get(key);
|
|
13
|
+
const value = insert();
|
|
14
|
+
this.set(key, value);
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
/**
|
|
2
20
|
* GUID for global instance map storage to ensure uniqueness across package versions
|
|
3
21
|
*/
|
|
@@ -16,7 +34,7 @@ export function getInstanceMap() {
|
|
|
16
34
|
/**
|
|
17
35
|
* Base registry class for managing enhancement configurations
|
|
18
36
|
*/
|
|
19
|
-
export class
|
|
37
|
+
export class EnhancementRegistry {
|
|
20
38
|
items = [];
|
|
21
39
|
push(items) {
|
|
22
40
|
if (Array.isArray(items)) {
|
|
@@ -142,7 +160,7 @@ export function assignGingerly(target, source, options) {
|
|
|
142
160
|
if (!target || typeof target !== 'object') {
|
|
143
161
|
return target;
|
|
144
162
|
}
|
|
145
|
-
const registry = options?.registry instanceof
|
|
163
|
+
const registry = options?.registry instanceof EnhancementRegistry
|
|
146
164
|
? options.registry
|
|
147
165
|
: options?.registry
|
|
148
166
|
? new options.registry()
|
|
@@ -305,10 +323,7 @@ export function assignGingerly(target, source, options) {
|
|
|
305
323
|
if (registryItem) {
|
|
306
324
|
const instanceMap = getInstanceMap();
|
|
307
325
|
// Get or initialize the instances map for this target
|
|
308
|
-
|
|
309
|
-
instanceMap.set(target, new Map());
|
|
310
|
-
}
|
|
311
|
-
const instances = instanceMap.get(target);
|
|
326
|
+
const instances = instanceMap.getOrInsert(target, () => new Map());
|
|
312
327
|
// Check if instance already exists (keyed by registryItem)
|
|
313
328
|
let instance = instances.get(registryItem);
|
|
314
329
|
if (!instance) {
|
|
@@ -363,10 +378,7 @@ export function assignGingerly(target, source, options) {
|
|
|
363
378
|
const registryItem = registry.findBySymbol(prop);
|
|
364
379
|
if (registryItem) {
|
|
365
380
|
const instanceMap = getInstanceMap();
|
|
366
|
-
|
|
367
|
-
instanceMap.set(target, new Map());
|
|
368
|
-
}
|
|
369
|
-
const instances = instanceMap.get(target);
|
|
381
|
+
const instances = instanceMap.getOrInsert(target, () => new Map());
|
|
370
382
|
let instance = instances.get(registryItem);
|
|
371
383
|
if (!instance) {
|
|
372
384
|
const SpawnClass = registryItem.spawn;
|
package/assignGingerly.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type IBaseRegistryItem<T = any> = EnhancementConfig<T>;
|
|
|
11
11
|
* Interface for the options passed to assignGingerly
|
|
12
12
|
*/
|
|
13
13
|
export interface IAssignGingerlyOptions {
|
|
14
|
-
registry?: typeof
|
|
14
|
+
registry?: typeof EnhancementRegistry | EnhancementRegistry;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -34,7 +34,7 @@ export function getInstanceMap(): WeakMap<object, Map<EnhancementConfig, any>> {
|
|
|
34
34
|
/**
|
|
35
35
|
* Base registry class for managing enhancement configurations
|
|
36
36
|
*/
|
|
37
|
-
export class
|
|
37
|
+
export class EnhancementRegistry {
|
|
38
38
|
private items: EnhancementConfig[] = [];
|
|
39
39
|
|
|
40
40
|
push(items: EnhancementConfig | EnhancementConfig[]): void {
|
|
@@ -179,7 +179,7 @@ export function assignGingerly(
|
|
|
179
179
|
return target;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
const registry = options?.registry instanceof
|
|
182
|
+
const registry = options?.registry instanceof EnhancementRegistry
|
|
183
183
|
? options.registry
|
|
184
184
|
: options?.registry
|
|
185
185
|
? new options.registry()
|
|
@@ -350,10 +350,7 @@ export function assignGingerly(
|
|
|
350
350
|
if (registryItem) {
|
|
351
351
|
const instanceMap = getInstanceMap();
|
|
352
352
|
// Get or initialize the instances map for this target
|
|
353
|
-
|
|
354
|
-
instanceMap.set(target, new Map());
|
|
355
|
-
}
|
|
356
|
-
const instances = instanceMap.get(target)!;
|
|
353
|
+
const instances = instanceMap.getOrInsert(target, () => new Map());
|
|
357
354
|
|
|
358
355
|
// Check if instance already exists (keyed by registryItem)
|
|
359
356
|
let instance = instances.get(registryItem);
|
|
@@ -418,10 +415,7 @@ export function assignGingerly(
|
|
|
418
415
|
const registryItem = registry.findBySymbol(prop);
|
|
419
416
|
if (registryItem) {
|
|
420
417
|
const instanceMap = getInstanceMap();
|
|
421
|
-
|
|
422
|
-
instanceMap.set(target, new Map());
|
|
423
|
-
}
|
|
424
|
-
const instances = instanceMap.get(target)!;
|
|
418
|
+
const instances = instanceMap.getOrInsert(target, () => new Map());
|
|
425
419
|
let instance = instances.get(registryItem);
|
|
426
420
|
|
|
427
421
|
if (!instance) {
|
package/buildCSSQuery.js
CHANGED
|
@@ -1,44 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
* Resolves template variables in a string recursively
|
|
3
|
-
* @param template - Template string with ${var} placeholders
|
|
4
|
-
* @param patterns - The patterns object containing variable values
|
|
5
|
-
* @param resolvedCache - Cache of already resolved values
|
|
6
|
-
* @param visitedKeys - Set of keys being resolved (for cycle detection)
|
|
7
|
-
* @returns Resolved string
|
|
8
|
-
*/
|
|
9
|
-
function resolveTemplate(template, patterns, resolvedCache, visitedKeys = new Set()) {
|
|
10
|
-
return template.replace(/\$\{(\w+)\}/g, (match, varName) => {
|
|
11
|
-
// Check if already resolved
|
|
12
|
-
if (resolvedCache.has(varName)) {
|
|
13
|
-
return resolvedCache.get(varName);
|
|
14
|
-
}
|
|
15
|
-
// Check for circular reference
|
|
16
|
-
if (visitedKeys.has(varName)) {
|
|
17
|
-
throw new Error(`Circular reference detected in template variable: ${varName}`);
|
|
18
|
-
}
|
|
19
|
-
const value = patterns[varName];
|
|
20
|
-
if (value === undefined) {
|
|
21
|
-
throw new Error(`Undefined template variable: ${varName}`);
|
|
22
|
-
}
|
|
23
|
-
if (typeof value === 'string') {
|
|
24
|
-
// Recursively resolve
|
|
25
|
-
visitedKeys.add(varName);
|
|
26
|
-
const resolved = resolveTemplate(value, patterns, resolvedCache, visitedKeys);
|
|
27
|
-
visitedKeys.delete(varName);
|
|
28
|
-
resolvedCache.set(varName, resolved);
|
|
29
|
-
return resolved;
|
|
30
|
-
}
|
|
31
|
-
// Non-string value, return as-is
|
|
32
|
-
return String(value);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
1
|
+
import { resolveTemplate } from './resolveTemplate.js';
|
|
35
2
|
/**
|
|
36
3
|
* Extracts attribute names from withAttrs configuration
|
|
37
4
|
* Resolves template variables and excludes underscore-prefixed config keys
|
|
38
5
|
* @param withAttrs - The attribute patterns configuration
|
|
39
6
|
* @returns Array of resolved attribute names
|
|
40
7
|
*/
|
|
41
|
-
function extractAttributeNames(withAttrs) {
|
|
8
|
+
export function extractAttributeNames(withAttrs) {
|
|
42
9
|
const names = [];
|
|
43
10
|
const resolvedCache = new Map();
|
|
44
11
|
// Add base if present
|
|
@@ -64,8 +31,8 @@ function extractAttributeNames(withAttrs) {
|
|
|
64
31
|
* Creates a cross-product of selectors and attribute names (both prefixed and unprefixed)
|
|
65
32
|
*
|
|
66
33
|
* @param config - Enhancement configuration with withAttrs
|
|
67
|
-
* @param selectors -
|
|
68
|
-
* If empty, returns just the attribute selectors without element prefix
|
|
34
|
+
* @param selectors - Optional comma-separated CSS selectors to match (e.g., 'template, script')
|
|
35
|
+
* If omitted or empty, returns just the attribute selectors without element prefix
|
|
69
36
|
* @returns CSS query string with cross-product of selectors and attributes
|
|
70
37
|
*
|
|
71
38
|
* @example
|
|
@@ -83,6 +50,8 @@ function extractAttributeNames(withAttrs) {
|
|
|
83
50
|
* // div[my-attr-theme], span[my-attr-theme], div[enh-my-attr-theme], span[enh-my-attr-theme]'
|
|
84
51
|
*
|
|
85
52
|
* // Without selectors (matches any element)
|
|
53
|
+
* buildCSSQuery(config);
|
|
54
|
+
* // or
|
|
86
55
|
* buildCSSQuery(config, '');
|
|
87
56
|
* // Returns: '[my-attr], [enh-my-attr], [my-attr-theme], [enh-my-attr-theme]'
|
|
88
57
|
*/
|
package/buildCSSQuery.ts
CHANGED
|
@@ -1,49 +1,5 @@
|
|
|
1
1
|
import { EnhancementConfig, AttrPatterns } from './types/assign-gingerly/types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Resolves template variables in a string recursively
|
|
5
|
-
* @param template - Template string with ${var} placeholders
|
|
6
|
-
* @param patterns - The patterns object containing variable values
|
|
7
|
-
* @param resolvedCache - Cache of already resolved values
|
|
8
|
-
* @param visitedKeys - Set of keys being resolved (for cycle detection)
|
|
9
|
-
* @returns Resolved string
|
|
10
|
-
*/
|
|
11
|
-
function resolveTemplate(
|
|
12
|
-
template: string,
|
|
13
|
-
patterns: Record<string, any>,
|
|
14
|
-
resolvedCache: Map<string, string>,
|
|
15
|
-
visitedKeys: Set<string> = new Set()
|
|
16
|
-
): string {
|
|
17
|
-
return template.replace(/\$\{(\w+)\}/g, (match, varName) => {
|
|
18
|
-
// Check if already resolved
|
|
19
|
-
if (resolvedCache.has(varName)) {
|
|
20
|
-
return resolvedCache.get(varName)!;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Check for circular reference
|
|
24
|
-
if (visitedKeys.has(varName)) {
|
|
25
|
-
throw new Error(`Circular reference detected in template variable: ${varName}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const value = patterns[varName];
|
|
29
|
-
|
|
30
|
-
if (value === undefined) {
|
|
31
|
-
throw new Error(`Undefined template variable: ${varName}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (typeof value === 'string') {
|
|
35
|
-
// Recursively resolve
|
|
36
|
-
visitedKeys.add(varName);
|
|
37
|
-
const resolved = resolveTemplate(value, patterns, resolvedCache, visitedKeys);
|
|
38
|
-
visitedKeys.delete(varName);
|
|
39
|
-
resolvedCache.set(varName, resolved);
|
|
40
|
-
return resolved;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Non-string value, return as-is
|
|
44
|
-
return String(value);
|
|
45
|
-
});
|
|
46
|
-
}
|
|
2
|
+
import { resolveTemplate } from './resolveTemplate.js';
|
|
47
3
|
|
|
48
4
|
/**
|
|
49
5
|
* Extracts attribute names from withAttrs configuration
|
|
@@ -51,7 +7,7 @@ function resolveTemplate(
|
|
|
51
7
|
* @param withAttrs - The attribute patterns configuration
|
|
52
8
|
* @returns Array of resolved attribute names
|
|
53
9
|
*/
|
|
54
|
-
function extractAttributeNames(withAttrs: AttrPatterns<any>): string[] {
|
|
10
|
+
export function extractAttributeNames(withAttrs: AttrPatterns<any>): string[] {
|
|
55
11
|
const names: string[] = [];
|
|
56
12
|
const resolvedCache = new Map<string, string>();
|
|
57
13
|
|
|
@@ -82,8 +38,8 @@ function extractAttributeNames(withAttrs: AttrPatterns<any>): string[] {
|
|
|
82
38
|
* Creates a cross-product of selectors and attribute names (both prefixed and unprefixed)
|
|
83
39
|
*
|
|
84
40
|
* @param config - Enhancement configuration with withAttrs
|
|
85
|
-
* @param selectors -
|
|
86
|
-
* If empty, returns just the attribute selectors without element prefix
|
|
41
|
+
* @param selectors - Optional comma-separated CSS selectors to match (e.g., 'template, script')
|
|
42
|
+
* If omitted or empty, returns just the attribute selectors without element prefix
|
|
87
43
|
* @returns CSS query string with cross-product of selectors and attributes
|
|
88
44
|
*
|
|
89
45
|
* @example
|
|
@@ -101,12 +57,14 @@ function extractAttributeNames(withAttrs: AttrPatterns<any>): string[] {
|
|
|
101
57
|
* // div[my-attr-theme], span[my-attr-theme], div[enh-my-attr-theme], span[enh-my-attr-theme]'
|
|
102
58
|
*
|
|
103
59
|
* // Without selectors (matches any element)
|
|
60
|
+
* buildCSSQuery(config);
|
|
61
|
+
* // or
|
|
104
62
|
* buildCSSQuery(config, '');
|
|
105
63
|
* // Returns: '[my-attr], [enh-my-attr], [my-attr-theme], [enh-my-attr-theme]'
|
|
106
64
|
*/
|
|
107
65
|
export function buildCSSQuery(
|
|
108
66
|
config: EnhancementConfig,
|
|
109
|
-
selectors
|
|
67
|
+
selectors?: string
|
|
110
68
|
): string {
|
|
111
69
|
// Validate inputs
|
|
112
70
|
if (!config.withAttrs) {
|
package/global.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Type declarations for Map.prototype.getOrInsert and WeakMap.prototype.getOrInsert
|
|
2
|
+
// Feature is now supported in all modern browsers (Chrome 146+, Firefox 134+, Safari 18.2+)
|
|
3
|
+
// See: https://web-platform-dx.github.io/web-features-explorer/features/getorinsert/
|
|
4
|
+
|
|
5
|
+
interface Map<K, V> {
|
|
6
|
+
/**
|
|
7
|
+
* Returns the value associated with the key if it exists, otherwise inserts
|
|
8
|
+
* the value returned by the insert callback and returns it.
|
|
9
|
+
* @param key The key to look up
|
|
10
|
+
* @param insert A callback that returns the value to insert if the key doesn't exist
|
|
11
|
+
* @returns The existing or newly inserted value
|
|
12
|
+
*/
|
|
13
|
+
getOrInsert(key: K, insert: () => V): V;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface WeakMap<K extends object, V> {
|
|
17
|
+
/**
|
|
18
|
+
* Returns the value associated with the key if it exists, otherwise inserts
|
|
19
|
+
* the value returned by the insert callback and returns it.
|
|
20
|
+
* @param key The key to look up
|
|
21
|
+
* @param insert A callback that returns the value to insert if the key doesn't exist
|
|
22
|
+
* @returns The existing or newly inserted value
|
|
23
|
+
*/
|
|
24
|
+
getOrInsert(key: K, insert: () => V): V;
|
|
25
|
+
}
|
package/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { assignGingerly } from './assignGingerly.js';
|
|
2
2
|
export { assignTentatively } from './assignTentatively.js';
|
|
3
|
-
export {
|
|
3
|
+
export { EnhancementRegistry } from './assignGingerly.js';
|
|
4
4
|
export { waitForEvent } from './waitForEvent.js';
|
|
5
5
|
export { ParserRegistry, globalParserRegistry } from './parserRegistry.js';
|
|
6
6
|
export { parseWithAttrs } from './parseWithAttrs.js';
|
|
7
7
|
export { buildCSSQuery } from './buildCSSQuery.js';
|
|
8
|
+
export { resolveTemplate } from './resolveTemplate.js';
|
|
8
9
|
import './object-extension.js';
|
package/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export {assignGingerly} from './assignGingerly.js';
|
|
2
2
|
export {assignTentatively} from './assignTentatively.js';
|
|
3
|
-
export {
|
|
3
|
+
export {EnhancementRegistry} from './assignGingerly.js';
|
|
4
4
|
export {waitForEvent} from './waitForEvent.js';
|
|
5
5
|
export {ParserRegistry, globalParserRegistry} from './parserRegistry.js';
|
|
6
6
|
export {parseWithAttrs} from './parseWithAttrs.js';
|
|
7
7
|
export {buildCSSQuery} from './buildCSSQuery.js';
|
|
8
|
+
export {resolveTemplate} from './resolveTemplate.js';
|
|
8
9
|
import './object-extension.js';
|
package/object-extension.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
// Polyfill for Map.prototype.getOrInsert and WeakMap.prototype.getOrInsert
|
|
2
|
+
if (typeof Map.prototype.getOrInsert !== 'function') {
|
|
3
|
+
Map.prototype.getOrInsert = function(key, insert) {
|
|
4
|
+
if (this.has(key)) return this.get(key);
|
|
5
|
+
const value = insert();
|
|
6
|
+
this.set(key, value);
|
|
7
|
+
return value;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
if (typeof WeakMap.prototype.getOrInsert !== 'function') {
|
|
11
|
+
WeakMap.prototype.getOrInsert = function(key, insert) {
|
|
12
|
+
if (this.has(key)) return this.get(key);
|
|
13
|
+
const value = insert();
|
|
14
|
+
this.set(key, value);
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
import assignGingerly, { EnhancementRegistry, getInstanceMap } from './assignGingerly.js';
|
|
2
20
|
import { parseWithAttrs } from './parseWithAttrs.js';
|
|
3
21
|
/**
|
|
4
22
|
* Normalizes lifecycleKeys to always return an object with dispose and resolved keys
|
|
@@ -21,7 +39,7 @@ if (typeof CustomElementRegistry !== 'undefined') {
|
|
|
21
39
|
Object.defineProperty(CustomElementRegistry.prototype, 'enhancementRegistry', {
|
|
22
40
|
get: function () {
|
|
23
41
|
// Create a new BaseRegistry instance on first access and cache it
|
|
24
|
-
const registry = new
|
|
42
|
+
const registry = new EnhancementRegistry();
|
|
25
43
|
// Replace the getter with the actual value
|
|
26
44
|
Object.defineProperty(this, 'enhancementRegistry', {
|
|
27
45
|
value: registry,
|
|
@@ -66,10 +84,7 @@ class ElementEnhancementContainer {
|
|
|
66
84
|
}
|
|
67
85
|
// Get or create instance using the global instance map
|
|
68
86
|
const instanceMap = getInstanceMap();
|
|
69
|
-
|
|
70
|
-
instanceMap.set(element, new Map());
|
|
71
|
-
}
|
|
72
|
-
const instances = instanceMap.get(element);
|
|
87
|
+
const instances = instanceMap.getOrInsert(element, () => new Map());
|
|
73
88
|
let instance = instances.get(registryItem);
|
|
74
89
|
if (!instance) {
|
|
75
90
|
// Need to spawn
|
|
@@ -201,10 +216,7 @@ class ElementEnhancementContainer {
|
|
|
201
216
|
const SpawnClass = registryItem.spawn;
|
|
202
217
|
// Check the global instance map first
|
|
203
218
|
const instanceMap = getInstanceMap();
|
|
204
|
-
|
|
205
|
-
instanceMap.set(element, new Map());
|
|
206
|
-
}
|
|
207
|
-
const instances = instanceMap.get(element);
|
|
219
|
+
const instances = instanceMap.getOrInsert(element, () => new Map());
|
|
208
220
|
let instance = instances.get(registryItem);
|
|
209
221
|
if (!instance) {
|
|
210
222
|
// Need to spawn
|
|
@@ -259,10 +271,7 @@ if (typeof Element !== 'undefined') {
|
|
|
259
271
|
const enhContainerWeakMap = new WeakMap();
|
|
260
272
|
Object.defineProperty(Element.prototype, 'enh', {
|
|
261
273
|
get: function () {
|
|
262
|
-
|
|
263
|
-
enhContainerWeakMap.set(this, new ElementEnhancementContainer(this));
|
|
264
|
-
}
|
|
265
|
-
return enhContainerWeakMap.get(this);
|
|
274
|
+
return enhContainerWeakMap.getOrInsert(this, () => new ElementEnhancementContainer(this));
|
|
266
275
|
},
|
|
267
276
|
enumerable: true,
|
|
268
277
|
configurable: true,
|
package/object-extension.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import assignGingerly, {
|
|
1
|
+
import assignGingerly, { EnhancementRegistry, IAssignGingerlyOptions, getInstanceMap, INSTANCE_MAP_GUID } from './assignGingerly.js';
|
|
2
2
|
import { parseWithAttrs } from './parseWithAttrs.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -21,7 +21,7 @@ function normalizeLifecycleKeys(lifecycleKeys: true | { dispose?: string | symbo
|
|
|
21
21
|
*/
|
|
22
22
|
declare global {
|
|
23
23
|
interface CustomElementRegistry {
|
|
24
|
-
enhancementRegistry: typeof
|
|
24
|
+
enhancementRegistry: typeof EnhancementRegistry | EnhancementRegistry;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
interface Element {
|
|
@@ -84,7 +84,7 @@ if (typeof CustomElementRegistry !== 'undefined') {
|
|
|
84
84
|
Object.defineProperty(CustomElementRegistry.prototype, 'enhancementRegistry', {
|
|
85
85
|
get: function () {
|
|
86
86
|
// Create a new BaseRegistry instance on first access and cache it
|
|
87
|
-
const registry = new
|
|
87
|
+
const registry = new EnhancementRegistry();
|
|
88
88
|
// Replace the getter with the actual value
|
|
89
89
|
Object.defineProperty(this, 'enhancementRegistry', {
|
|
90
90
|
value: registry,
|
|
@@ -136,10 +136,7 @@ class ElementEnhancementContainer {
|
|
|
136
136
|
|
|
137
137
|
// Get or create instance using the global instance map
|
|
138
138
|
const instanceMap = getInstanceMap();
|
|
139
|
-
|
|
140
|
-
instanceMap.set(element, new Map());
|
|
141
|
-
}
|
|
142
|
-
const instances = instanceMap.get(element)!;
|
|
139
|
+
const instances = instanceMap.getOrInsert(element, () => new Map());
|
|
143
140
|
|
|
144
141
|
let instance = instances.get(registryItem);
|
|
145
142
|
|
|
@@ -304,10 +301,7 @@ class ElementEnhancementContainer {
|
|
|
304
301
|
|
|
305
302
|
// Check the global instance map first
|
|
306
303
|
const instanceMap = getInstanceMap();
|
|
307
|
-
|
|
308
|
-
instanceMap.set(element, new Map());
|
|
309
|
-
}
|
|
310
|
-
const instances = instanceMap.get(element)!;
|
|
304
|
+
const instances = instanceMap.getOrInsert(element, () => new Map());
|
|
311
305
|
|
|
312
306
|
let instance = instances.get(registryItem);
|
|
313
307
|
|
|
@@ -376,11 +370,7 @@ if (typeof Element !== 'undefined') {
|
|
|
376
370
|
|
|
377
371
|
Object.defineProperty(Element.prototype, 'enh', {
|
|
378
372
|
get: function (this: Element) {
|
|
379
|
-
|
|
380
|
-
enhContainerWeakMap.set(this, new ElementEnhancementContainer(this));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return enhContainerWeakMap.get(this);
|
|
373
|
+
return enhContainerWeakMap.getOrInsert(this, () => new ElementEnhancementContainer(this));
|
|
384
374
|
},
|
|
385
375
|
enumerable: true,
|
|
386
376
|
configurable: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "This package provides a utility function for carefully merging one object into another.",
|
|
5
5
|
"homepage": "https://github.com/bahrus/assign-gingerly#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -47,6 +47,10 @@
|
|
|
47
47
|
"./buildCSSQuery.js": {
|
|
48
48
|
"default": "./buildCSSQuery.js",
|
|
49
49
|
"types": "./buildCSSQuery.ts"
|
|
50
|
+
},
|
|
51
|
+
"./resolveTemplate.js": {
|
|
52
|
+
"default": "./resolveTemplate.js",
|
|
53
|
+
"types": "./resolveTemplate.ts"
|
|
50
54
|
}
|
|
51
55
|
},
|
|
52
56
|
"main": "index.js",
|
package/parseWithAttrs.js
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
// Polyfill for Map.prototype.getOrInsert and WeakMap.prototype.getOrInsert
|
|
2
|
+
if (typeof Map.prototype.getOrInsert !== 'function') {
|
|
3
|
+
Map.prototype.getOrInsert = function(key, insert) {
|
|
4
|
+
if (this.has(key)) return this.get(key);
|
|
5
|
+
const value = insert();
|
|
6
|
+
this.set(key, value);
|
|
7
|
+
return value;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
if (typeof WeakMap.prototype.getOrInsert !== 'function') {
|
|
11
|
+
WeakMap.prototype.getOrInsert = function(key, insert) {
|
|
12
|
+
if (this.has(key)) return this.get(key);
|
|
13
|
+
const value = insert();
|
|
14
|
+
this.set(key, value);
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
import { globalParserRegistry } from './parserRegistry.js';
|
|
20
|
+
import { resolveTemplate } from './resolveTemplate.js';
|
|
2
21
|
// Module-level cache for parsed attribute values
|
|
3
22
|
// Structure: Map<configKey, Map<attrValue, parsedValue>>
|
|
4
23
|
const parseCache = new Map();
|
|
@@ -89,10 +108,7 @@ function parseWithCache(attrValue, config, parser) {
|
|
|
89
108
|
}
|
|
90
109
|
// Get or create cache for this config
|
|
91
110
|
const cacheKey = getCacheKey(config);
|
|
92
|
-
|
|
93
|
-
parseCache.set(cacheKey, new Map());
|
|
94
|
-
}
|
|
95
|
-
const valueCache = parseCache.get(cacheKey);
|
|
111
|
+
const valueCache = parseCache.getOrInsert(cacheKey, () => new Map());
|
|
96
112
|
// Use special key for null values
|
|
97
113
|
const valueCacheKey = attrValue === null ? '__NULL__' : attrValue;
|
|
98
114
|
// Check if we have a cached value
|
|
@@ -160,40 +176,6 @@ function getAttributeValue(element, attrName, allowUnprefixed) {
|
|
|
160
176
|
return enhValue;
|
|
161
177
|
return element.getAttribute(attrName);
|
|
162
178
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Resolves template variables in a string recursively
|
|
165
|
-
* @param template - Template string with ${var} placeholders
|
|
166
|
-
* @param patterns - The patterns object containing variable values
|
|
167
|
-
* @param resolvedCache - Cache of already resolved values
|
|
168
|
-
* @param visitedKeys - Set of keys being resolved (for cycle detection)
|
|
169
|
-
* @returns Resolved string
|
|
170
|
-
*/
|
|
171
|
-
function resolveTemplate(template, patterns, resolvedCache, visitedKeys = new Set()) {
|
|
172
|
-
return template.replace(/\$\{(\w+)\}/g, (match, varName) => {
|
|
173
|
-
// Check if already resolved
|
|
174
|
-
if (resolvedCache.has(varName)) {
|
|
175
|
-
return resolvedCache.get(varName);
|
|
176
|
-
}
|
|
177
|
-
// Check for circular reference
|
|
178
|
-
if (visitedKeys.has(varName)) {
|
|
179
|
-
throw new Error(`Circular reference detected in template variable: ${varName}`);
|
|
180
|
-
}
|
|
181
|
-
const value = patterns[varName];
|
|
182
|
-
if (value === undefined) {
|
|
183
|
-
throw new Error(`Undefined template variable: ${varName}`);
|
|
184
|
-
}
|
|
185
|
-
if (typeof value === 'string') {
|
|
186
|
-
// Recursively resolve
|
|
187
|
-
visitedKeys.add(varName);
|
|
188
|
-
const resolved = resolveTemplate(value, patterns, resolvedCache, visitedKeys);
|
|
189
|
-
visitedKeys.delete(varName);
|
|
190
|
-
resolvedCache.set(varName, resolved);
|
|
191
|
-
return resolved;
|
|
192
|
-
}
|
|
193
|
-
// Non-string value, return as-is
|
|
194
|
-
return String(value);
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
179
|
/**
|
|
198
180
|
* Gets the default parser for a given instanceOf type
|
|
199
181
|
*/
|
package/parseWithAttrs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AttrPatterns, AttrConfig } from './types/assign-gingerly/types';
|
|
2
2
|
import { globalParserRegistry } from './parserRegistry.js';
|
|
3
|
+
import { resolveTemplate } from './resolveTemplate.js';
|
|
3
4
|
|
|
4
5
|
// Module-level cache for parsed attribute values
|
|
5
6
|
// Structure: Map<configKey, Map<attrValue, parsedValue>>
|
|
@@ -107,11 +108,7 @@ function parseWithCache(
|
|
|
107
108
|
|
|
108
109
|
// Get or create cache for this config
|
|
109
110
|
const cacheKey = getCacheKey(config);
|
|
110
|
-
|
|
111
|
-
parseCache.set(cacheKey, new Map());
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const valueCache = parseCache.get(cacheKey)!;
|
|
111
|
+
const valueCache = parseCache.getOrInsert(cacheKey, () => new Map());
|
|
115
112
|
|
|
116
113
|
// Use special key for null values
|
|
117
114
|
const valueCacheKey = attrValue === null ? '__NULL__' : attrValue;
|
|
@@ -193,51 +190,6 @@ function getAttributeValue(
|
|
|
193
190
|
return element.getAttribute(attrName);
|
|
194
191
|
}
|
|
195
192
|
|
|
196
|
-
/**
|
|
197
|
-
* Resolves template variables in a string recursively
|
|
198
|
-
* @param template - Template string with ${var} placeholders
|
|
199
|
-
* @param patterns - The patterns object containing variable values
|
|
200
|
-
* @param resolvedCache - Cache of already resolved values
|
|
201
|
-
* @param visitedKeys - Set of keys being resolved (for cycle detection)
|
|
202
|
-
* @returns Resolved string
|
|
203
|
-
*/
|
|
204
|
-
function resolveTemplate(
|
|
205
|
-
template: string,
|
|
206
|
-
patterns: Record<string, any>,
|
|
207
|
-
resolvedCache: Map<string, string>,
|
|
208
|
-
visitedKeys: Set<string> = new Set()
|
|
209
|
-
): string {
|
|
210
|
-
return template.replace(/\$\{(\w+)\}/g, (match, varName) => {
|
|
211
|
-
// Check if already resolved
|
|
212
|
-
if (resolvedCache.has(varName)) {
|
|
213
|
-
return resolvedCache.get(varName)!;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Check for circular reference
|
|
217
|
-
if (visitedKeys.has(varName)) {
|
|
218
|
-
throw new Error(`Circular reference detected in template variable: ${varName}`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const value = patterns[varName];
|
|
222
|
-
|
|
223
|
-
if (value === undefined) {
|
|
224
|
-
throw new Error(`Undefined template variable: ${varName}`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (typeof value === 'string') {
|
|
228
|
-
// Recursively resolve
|
|
229
|
-
visitedKeys.add(varName);
|
|
230
|
-
const resolved = resolveTemplate(value, patterns, resolvedCache, visitedKeys);
|
|
231
|
-
visitedKeys.delete(varName);
|
|
232
|
-
resolvedCache.set(varName, resolved);
|
|
233
|
-
return resolved;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Non-string value, return as-is
|
|
237
|
-
return String(value);
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
193
|
/**
|
|
242
194
|
* Gets the default parser for a given instanceOf type
|
|
243
195
|
*/
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves template variables in a string recursively
|
|
3
|
+
* Supports ${varName} syntax for variable substitution
|
|
4
|
+
*
|
|
5
|
+
* @param template - Template string with ${var} placeholders
|
|
6
|
+
* @param patterns - The patterns object containing variable values
|
|
7
|
+
* @param resolvedCache - Cache of already resolved values
|
|
8
|
+
* @param visitedKeys - Set of keys being resolved (for cycle detection)
|
|
9
|
+
* @returns Resolved string
|
|
10
|
+
*
|
|
11
|
+
* @throws Error if circular reference is detected
|
|
12
|
+
* @throws Error if template variable is undefined
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const patterns = {
|
|
16
|
+
* base: 'my-component',
|
|
17
|
+
* theme: '${base}-theme'
|
|
18
|
+
* };
|
|
19
|
+
* const cache = new Map();
|
|
20
|
+
*
|
|
21
|
+
* resolveTemplate('${theme}', patterns, cache);
|
|
22
|
+
* // Returns: 'my-component-theme'
|
|
23
|
+
*/
|
|
24
|
+
export function resolveTemplate(template, patterns, resolvedCache, visitedKeys = new Set()) {
|
|
25
|
+
return template.replace(/\$\{(\w+)\}/g, (_match, varName) => {
|
|
26
|
+
// Check if already resolved
|
|
27
|
+
if (resolvedCache.has(varName)) {
|
|
28
|
+
return resolvedCache.get(varName);
|
|
29
|
+
}
|
|
30
|
+
// Check for circular reference
|
|
31
|
+
if (visitedKeys.has(varName)) {
|
|
32
|
+
throw new Error(`Circular reference detected in template variable: ${varName}`);
|
|
33
|
+
}
|
|
34
|
+
const value = patterns[varName];
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
throw new Error(`Undefined template variable: ${varName}`);
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === 'string') {
|
|
39
|
+
// Recursively resolve
|
|
40
|
+
visitedKeys.add(varName);
|
|
41
|
+
const resolved = resolveTemplate(value, patterns, resolvedCache, visitedKeys);
|
|
42
|
+
visitedKeys.delete(varName);
|
|
43
|
+
resolvedCache.set(varName, resolved);
|
|
44
|
+
return resolved;
|
|
45
|
+
}
|
|
46
|
+
// Non-string value, return as-is
|
|
47
|
+
return String(value);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves template variables in a string recursively
|
|
3
|
+
* Supports ${varName} syntax for variable substitution
|
|
4
|
+
*
|
|
5
|
+
* @param template - Template string with ${var} placeholders
|
|
6
|
+
* @param patterns - The patterns object containing variable values
|
|
7
|
+
* @param resolvedCache - Cache of already resolved values
|
|
8
|
+
* @param visitedKeys - Set of keys being resolved (for cycle detection)
|
|
9
|
+
* @returns Resolved string
|
|
10
|
+
*
|
|
11
|
+
* @throws Error if circular reference is detected
|
|
12
|
+
* @throws Error if template variable is undefined
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const patterns = {
|
|
16
|
+
* base: 'my-component',
|
|
17
|
+
* theme: '${base}-theme'
|
|
18
|
+
* };
|
|
19
|
+
* const cache = new Map();
|
|
20
|
+
*
|
|
21
|
+
* resolveTemplate('${theme}', patterns, cache);
|
|
22
|
+
* // Returns: 'my-component-theme'
|
|
23
|
+
*/
|
|
24
|
+
export function resolveTemplate(
|
|
25
|
+
template: string,
|
|
26
|
+
patterns: Record<string, any>,
|
|
27
|
+
resolvedCache: Map<string, string>,
|
|
28
|
+
visitedKeys: Set<string> = new Set()
|
|
29
|
+
): string {
|
|
30
|
+
return template.replace(/\$\{(\w+)\}/g, (_match, varName) => {
|
|
31
|
+
// Check if already resolved
|
|
32
|
+
if (resolvedCache.has(varName)) {
|
|
33
|
+
return resolvedCache.get(varName)!;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check for circular reference
|
|
37
|
+
if (visitedKeys.has(varName)) {
|
|
38
|
+
throw new Error(`Circular reference detected in template variable: ${varName}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const value = patterns[varName];
|
|
42
|
+
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
throw new Error(`Undefined template variable: ${varName}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
// Recursively resolve
|
|
49
|
+
visitedKeys.add(varName);
|
|
50
|
+
const resolved = resolveTemplate(value, patterns, resolvedCache, visitedKeys);
|
|
51
|
+
visitedKeys.delete(varName);
|
|
52
|
+
resolvedCache.set(varName, resolved);
|
|
53
|
+
return resolved;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Non-string value, return as-is
|
|
57
|
+
return String(value);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -25,16 +25,18 @@ type DisposeEvent =
|
|
|
25
25
|
//reference count outside any enhancements goes to zero
|
|
26
26
|
| 'dispose'
|
|
27
27
|
|
|
28
|
+
export type Spawner<T = any, Obj = Element> = {
|
|
29
|
+
new (obj?: Obj, ctx?: SpawnContext<T>, initVals?: Partial<T>): T;
|
|
30
|
+
canSpawn?: (obj: any, ctx?: SpawnContext<T>) => boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
/**
|
|
29
34
|
* Configuration for enhancing elements with class instances
|
|
30
35
|
* Defines how to spawn and initialize enhancement classes
|
|
31
36
|
*/
|
|
32
|
-
export interface EnhancementConfig<T = any> {
|
|
37
|
+
export interface EnhancementConfig<T = any, Obj = Element> {
|
|
33
38
|
|
|
34
|
-
spawn:
|
|
35
|
-
new (obj?: any, ctx?: SpawnContext<T>, initVals?: Partial<T>): T;
|
|
36
|
-
canSpawn?: (obj: any, ctx?: SpawnContext<T>) => boolean;
|
|
37
|
-
};
|
|
39
|
+
spawn: Spawner<T, Obj>;
|
|
38
40
|
|
|
39
41
|
//Applicable to passing in the initVals during the spawn lifecycle event
|
|
40
42
|
withAttrs?: AttrPatterns<T>;
|
|
@@ -180,3 +182,11 @@ export declare function assignGingerly(
|
|
|
180
182
|
): any;
|
|
181
183
|
|
|
182
184
|
export default assignGingerly;
|
|
185
|
+
|
|
186
|
+
export declare class ElementEnhancementGateway{
|
|
187
|
+
enh: ElementEnhancement;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface ElementEnhancement{
|
|
191
|
+
dispose(regItem: EnhancementConfig): void;
|
|
192
|
+
}
|