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 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
- 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
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 IBaseRegistryItem<T = any> {
289
- spawn: {new(): T} | Promise<{new(): T}>
290
- symlinks: {[key: symbol]: keyof T}
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 BaseRegistry{
314
- push(IBaseRegistryItem | IBaseRegistryItem[]){
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 baseRegistry = new BaseRegistry;
321
- baseRegistry.push([
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: BaseRegistry
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 BaseRegistry();
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 BaseRegistry();
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: BaseRegistry
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 `BaseRegistry` instance is created on first access and cached for subsequent uses:
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 BaseRegistry();
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 { BaseRegistry } from 'assign-gingerly';
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
- Enhancement classes should follow this constructor signature:
661
+ Element enhancement classes should follow this constructor signature:
653
662
 
654
663
  ```TypeScript
655
664
  interface SpawnContext<T, TMountContext = any> {
656
- config: IBaseRegistryItem<T>;
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 IBaseRegistryItem<T> {
719
+ interface IEnhancementRegistryItem<T, TObj = Element> {
709
720
  spawn: {
710
- new (oElement?: Element, ctx?: SpawnContext<T>, initVals?: Partial<T>): T;
711
- canSpawn?: (obj: any, ctx?: SpawnContext<T>) => boolean; // Optional spawn guard
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 `BaseRegistry` class includes a `findByEnhKey` method:
816
+ The `EnhancementRegistry` class includes a `findByEnhKey` method:
806
817
 
807
818
  ```TypeScript
808
- const registry = new BaseRegistry();
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 BaseRegistry();
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: IBaseRegistryItem<T> }`
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, { BaseRegistry } from 'assign-gingerly';
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 BaseRegistry();
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 into structured data objects. It's particularly useful for custom elements and web components that need to extract configuration from 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
- **Empty selectors return attribute-only selectors:**
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
- // '[my-attr], [enh-my-attr], [my-attr-theme], [enh-my-attr-theme]'
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: string
2386
+ selectors?: string
2368
2387
  ): string
2369
2388
  ```
2370
2389
 
2371
2390
  **Parameters:**
2372
2391
  - `config`: Enhancement configuration with `withAttrs` property
2373
- - `selectors`: Comma-separated CSS selectors (e.g., `'div, span'`)
2374
- - If empty string or whitespace only, returns attribute selectors without element prefix
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 BaseRegistry {
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 BaseRegistry
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
- if (!instanceMap.has(target)) {
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
- if (!instanceMap.has(target)) {
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 BaseRegistry | BaseRegistry;
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 BaseRegistry {
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 BaseRegistry
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
- if (!instanceMap.has(target)) {
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
- if (!instanceMap.has(target)) {
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 - Comma-separated CSS selectors to match (e.g., 'template, script')
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 - Comma-separated CSS selectors to match (e.g., 'template, script')
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: string
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 { BaseRegistry } from './assignGingerly.js';
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 {BaseRegistry} from './assignGingerly.js';
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';
@@ -1,4 +1,22 @@
1
- import assignGingerly, { BaseRegistry, getInstanceMap } from './assignGingerly.js';
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 BaseRegistry();
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
- if (!instanceMap.has(element)) {
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
- if (!instanceMap.has(element)) {
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
- if (!enhContainerWeakMap.has(this)) {
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,
@@ -1,4 +1,4 @@
1
- import assignGingerly, { BaseRegistry, IAssignGingerlyOptions, getInstanceMap, INSTANCE_MAP_GUID } from './assignGingerly.js';
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 BaseRegistry | BaseRegistry;
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 BaseRegistry();
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
- if (!instanceMap.has(element)) {
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
- if (!instanceMap.has(element)) {
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
- if (!enhContainerWeakMap.has(this)) {
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.17",
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
- if (!parseCache.has(cacheKey)) {
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
- if (!parseCache.has(cacheKey)) {
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
+ }