assign-gingerly 0.0.11 → 0.0.13
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 +581 -118
- package/assignGingerly.js +10 -6
- package/assignGingerly.ts +14 -25
- package/index.js +2 -0
- package/index.ts +2 -0
- package/object-extension.js +9 -7
- package/object-extension.ts +9 -7
- package/package.json +8 -1
- package/parseWithAttrs.js +150 -6
- package/parseWithAttrs.ts +176 -6
- package/parserRegistry.js +71 -0
- package/parserRegistry.ts +94 -0
- package/types.d.ts +24 -3
package/assignGingerly.js
CHANGED
|
@@ -344,9 +344,11 @@ export function assignGingerly(target, source, options) {
|
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
// Find the mapped property name
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
347
|
+
if (registryItem.symlinks) {
|
|
348
|
+
const mappedKey = registryItem.symlinks[sym];
|
|
349
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
350
|
+
instance[mappedKey] = value;
|
|
351
|
+
}
|
|
350
352
|
}
|
|
351
353
|
}
|
|
352
354
|
}
|
|
@@ -397,9 +399,11 @@ export function assignGingerly(target, source, options) {
|
|
|
397
399
|
target.enh[registryItem.enhKey] = instance;
|
|
398
400
|
}
|
|
399
401
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
402
|
+
if (registryItem.symlinks) {
|
|
403
|
+
const mappedKey = registryItem.symlinks[prop];
|
|
404
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
405
|
+
instance[mappedKey] = value;
|
|
406
|
+
}
|
|
403
407
|
}
|
|
404
408
|
}
|
|
405
409
|
}
|
package/assignGingerly.ts
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration for enhancing elements with class instances
|
|
3
|
-
*/
|
|
4
|
-
export interface EnhancementConfig<T = any> {
|
|
5
|
-
spawn: {
|
|
6
|
-
new (oElement?: Element, ctx?: SpawnContext<T>, initVals?: Partial<T>): T;
|
|
7
|
-
canSpawn?: (obj: any, ctx?: SpawnContext<T>) => boolean;
|
|
8
|
-
};
|
|
9
|
-
symlinks: { [key: string | symbol]: keyof T };
|
|
10
|
-
enhKey?: string;
|
|
11
|
-
lifecycleKeys?: {
|
|
12
|
-
dispose?: string;
|
|
13
|
-
resolved?: string;
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
1
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
2
|
+
|
|
3
|
+
import { EnhancementConfig } from "./types";
|
|
20
4
|
|
|
21
5
|
/**
|
|
22
6
|
* @deprecated Use EnhancementConfig instead
|
|
@@ -411,10 +395,13 @@ export function assignGingerly(
|
|
|
411
395
|
}
|
|
412
396
|
|
|
413
397
|
// Find the mapped property name
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
(instance
|
|
398
|
+
if(registryItem.symlinks){
|
|
399
|
+
const mappedKey = registryItem.symlinks[sym];
|
|
400
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
401
|
+
(instance as any)[mappedKey] = value;
|
|
402
|
+
}
|
|
417
403
|
}
|
|
404
|
+
|
|
418
405
|
}
|
|
419
406
|
}
|
|
420
407
|
}
|
|
@@ -471,11 +458,13 @@ export function assignGingerly(
|
|
|
471
458
|
(target as any).enh[registryItem.enhKey] = instance;
|
|
472
459
|
}
|
|
473
460
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
461
|
+
if(registryItem.symlinks){
|
|
462
|
+
const mappedKey = registryItem.symlinks[prop];
|
|
463
|
+
if (mappedKey && instance && typeof instance === 'object') {
|
|
464
|
+
(instance as any)[mappedKey] = value;
|
|
465
|
+
}
|
|
478
466
|
}
|
|
467
|
+
|
|
479
468
|
}
|
|
480
469
|
}
|
|
481
470
|
return true;
|
package/index.js
CHANGED
|
@@ -2,4 +2,6 @@ export { assignGingerly } from './assignGingerly.js';
|
|
|
2
2
|
export { assignTentatively } from './assignTentatively.js';
|
|
3
3
|
export { BaseRegistry } from './assignGingerly.js';
|
|
4
4
|
export { waitForEvent } from './waitForEvent.js';
|
|
5
|
+
export { ParserRegistry, globalParserRegistry } from './parserRegistry.js';
|
|
6
|
+
export { parseWithAttrs } from './parseWithAttrs.js';
|
|
5
7
|
import './object-extension.js';
|
package/index.ts
CHANGED
|
@@ -2,4 +2,6 @@ export {assignGingerly} from './assignGingerly.js';
|
|
|
2
2
|
export {assignTentatively} from './assignTentatively.js';
|
|
3
3
|
export {BaseRegistry} from './assignGingerly.js';
|
|
4
4
|
export {waitForEvent} from './waitForEvent.js';
|
|
5
|
+
export {ParserRegistry, globalParserRegistry} from './parserRegistry.js';
|
|
6
|
+
export {parseWithAttrs} from './parseWithAttrs.js';
|
|
5
7
|
import './object-extension.js';
|
package/object-extension.js
CHANGED
|
@@ -48,9 +48,10 @@ class ElementEnhancementContainer {
|
|
|
48
48
|
/**
|
|
49
49
|
* Get or spawn an instance for a registry item
|
|
50
50
|
* @param registryItem - The registry item to get/spawn instance for
|
|
51
|
+
* @param mountCtx - Optional context to pass to the spawned instance
|
|
51
52
|
* @returns The spawned instance
|
|
52
53
|
*/
|
|
53
|
-
get(registryItem) {
|
|
54
|
+
get(registryItem, mountCtx) {
|
|
54
55
|
const element = this.element;
|
|
55
56
|
// Get the registry from customElementRegistry
|
|
56
57
|
const registry = element.customElementRegistry?.enhancementRegistry;
|
|
@@ -75,7 +76,7 @@ class ElementEnhancementContainer {
|
|
|
75
76
|
const SpawnClass = registryItem.spawn;
|
|
76
77
|
// Check canSpawn if it exists
|
|
77
78
|
if (typeof SpawnClass.canSpawn === 'function') {
|
|
78
|
-
const ctx = { config: registryItem };
|
|
79
|
+
const ctx = { config: registryItem, mountCtx };
|
|
79
80
|
if (!SpawnClass.canSpawn(element, ctx)) {
|
|
80
81
|
// canSpawn returned false, return undefined
|
|
81
82
|
return undefined;
|
|
@@ -83,7 +84,7 @@ class ElementEnhancementContainer {
|
|
|
83
84
|
}
|
|
84
85
|
// Check if there's an enhKey
|
|
85
86
|
if (registryItem.enhKey) {
|
|
86
|
-
const ctx = { config: registryItem };
|
|
87
|
+
const ctx = { config: registryItem, mountCtx };
|
|
87
88
|
const self = this;
|
|
88
89
|
// Parse attributes if withAttrs is defined
|
|
89
90
|
let attrInitVals = undefined;
|
|
@@ -111,7 +112,7 @@ class ElementEnhancementContainer {
|
|
|
111
112
|
}
|
|
112
113
|
else {
|
|
113
114
|
// No enhKey, just spawn with element
|
|
114
|
-
const ctx = { config: registryItem };
|
|
115
|
+
const ctx = { config: registryItem, mountCtx };
|
|
115
116
|
instance = new SpawnClass(element, ctx);
|
|
116
117
|
}
|
|
117
118
|
// Store in global instance map
|
|
@@ -152,16 +153,17 @@ class ElementEnhancementContainer {
|
|
|
152
153
|
/**
|
|
153
154
|
* Wait for an enhancement instance to be resolved
|
|
154
155
|
* @param registryItem - The registry item to wait for
|
|
156
|
+
* @param mountCtx - Optional context to pass to the spawned instance
|
|
155
157
|
* @returns Promise that resolves with the spawned instance
|
|
156
158
|
*/
|
|
157
|
-
async whenResolved(registryItem) {
|
|
159
|
+
async whenResolved(registryItem, mountCtx) {
|
|
158
160
|
const lifecycleKeys = normalizeLifecycleKeys(registryItem?.lifecycleKeys);
|
|
159
161
|
const resolvedKey = lifecycleKeys?.resolved;
|
|
160
162
|
if (resolvedKey === undefined) {
|
|
161
163
|
throw new Error('Must specify resolved key in lifecycleKeys');
|
|
162
164
|
}
|
|
163
|
-
// Get or spawn the instance
|
|
164
|
-
const spawnedInstance = this.get(registryItem);
|
|
165
|
+
// Get or spawn the instance (pass mountCtx through)
|
|
166
|
+
const spawnedInstance = this.get(registryItem, mountCtx);
|
|
165
167
|
// Check if already resolved
|
|
166
168
|
if (spawnedInstance[resolvedKey]) {
|
|
167
169
|
return spawnedInstance;
|
package/object-extension.ts
CHANGED
|
@@ -114,9 +114,10 @@ class ElementEnhancementContainer {
|
|
|
114
114
|
/**
|
|
115
115
|
* Get or spawn an instance for a registry item
|
|
116
116
|
* @param registryItem - The registry item to get/spawn instance for
|
|
117
|
+
* @param mountCtx - Optional context to pass to the spawned instance
|
|
117
118
|
* @returns The spawned instance
|
|
118
119
|
*/
|
|
119
|
-
get(registryItem: any): any {
|
|
120
|
+
get(registryItem: any, mountCtx?: any): any {
|
|
120
121
|
const element = this.element;
|
|
121
122
|
|
|
122
123
|
// Get the registry from customElementRegistry
|
|
@@ -148,7 +149,7 @@ class ElementEnhancementContainer {
|
|
|
148
149
|
|
|
149
150
|
// Check canSpawn if it exists
|
|
150
151
|
if (typeof SpawnClass.canSpawn === 'function') {
|
|
151
|
-
const ctx = { config: registryItem };
|
|
152
|
+
const ctx = { config: registryItem, mountCtx };
|
|
152
153
|
if (!SpawnClass.canSpawn(element, ctx)) {
|
|
153
154
|
// canSpawn returned false, return undefined
|
|
154
155
|
return undefined;
|
|
@@ -157,7 +158,7 @@ class ElementEnhancementContainer {
|
|
|
157
158
|
|
|
158
159
|
// Check if there's an enhKey
|
|
159
160
|
if (registryItem.enhKey) {
|
|
160
|
-
const ctx = { config: registryItem };
|
|
161
|
+
const ctx = { config: registryItem, mountCtx };
|
|
161
162
|
const self = this as any;
|
|
162
163
|
|
|
163
164
|
// Parse attributes if withAttrs is defined
|
|
@@ -192,7 +193,7 @@ class ElementEnhancementContainer {
|
|
|
192
193
|
self[registryItem.enhKey] = instance;
|
|
193
194
|
} else {
|
|
194
195
|
// No enhKey, just spawn with element
|
|
195
|
-
const ctx = { config: registryItem };
|
|
196
|
+
const ctx = { config: registryItem, mountCtx };
|
|
196
197
|
instance = new SpawnClass(element, ctx);
|
|
197
198
|
}
|
|
198
199
|
|
|
@@ -243,9 +244,10 @@ class ElementEnhancementContainer {
|
|
|
243
244
|
/**
|
|
244
245
|
* Wait for an enhancement instance to be resolved
|
|
245
246
|
* @param registryItem - The registry item to wait for
|
|
247
|
+
* @param mountCtx - Optional context to pass to the spawned instance
|
|
246
248
|
* @returns Promise that resolves with the spawned instance
|
|
247
249
|
*/
|
|
248
|
-
async whenResolved(registryItem: any): Promise<any> {
|
|
250
|
+
async whenResolved(registryItem: any, mountCtx?: any): Promise<any> {
|
|
249
251
|
const lifecycleKeys = normalizeLifecycleKeys(registryItem?.lifecycleKeys);
|
|
250
252
|
const resolvedKey = lifecycleKeys?.resolved;
|
|
251
253
|
|
|
@@ -253,8 +255,8 @@ class ElementEnhancementContainer {
|
|
|
253
255
|
throw new Error('Must specify resolved key in lifecycleKeys');
|
|
254
256
|
}
|
|
255
257
|
|
|
256
|
-
// Get or spawn the instance
|
|
257
|
-
const spawnedInstance = this.get(registryItem);
|
|
258
|
+
// Get or spawn the instance (pass mountCtx through)
|
|
259
|
+
const spawnedInstance = this.get(registryItem, mountCtx);
|
|
258
260
|
|
|
259
261
|
// Check if already resolved
|
|
260
262
|
if ((spawnedInstance as any)[resolvedKey]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
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": {
|
|
@@ -35,6 +35,13 @@
|
|
|
35
35
|
},
|
|
36
36
|
"./waitForEvent.js": {
|
|
37
37
|
"default": "./waitForEvent.js"
|
|
38
|
+
},
|
|
39
|
+
"./parserRegistry.js": {
|
|
40
|
+
"default": "./parserRegistry.js"
|
|
41
|
+
},
|
|
42
|
+
"./parseWithAttrs.js": {
|
|
43
|
+
"default": "./parseWithAttrs.js",
|
|
44
|
+
"types": "./types.d.ts"
|
|
38
45
|
}
|
|
39
46
|
},
|
|
40
47
|
"main": "index.js",
|
package/parseWithAttrs.js
CHANGED
|
@@ -1,3 +1,121 @@
|
|
|
1
|
+
import { globalParserRegistry } from './parserRegistry.js';
|
|
2
|
+
// Module-level cache for parsed attribute values
|
|
3
|
+
// Structure: Map<configKey, Map<attrValue, parsedValue>>
|
|
4
|
+
const parseCache = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a parser specification to an actual parser function
|
|
7
|
+
* Supports:
|
|
8
|
+
* - Inline functions (direct use)
|
|
9
|
+
* - Named parsers from global registry
|
|
10
|
+
* - Custom element static methods (element-name.methodName)
|
|
11
|
+
*
|
|
12
|
+
* @param parserSpec - Parser function or string reference
|
|
13
|
+
* @returns The resolved parser function
|
|
14
|
+
* @throws Error if parser cannot be resolved
|
|
15
|
+
*/
|
|
16
|
+
function resolveParser(parserSpec) {
|
|
17
|
+
// Undefined - no parser specified
|
|
18
|
+
if (parserSpec === undefined) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
// Inline function - use directly
|
|
22
|
+
if (typeof parserSpec === 'function') {
|
|
23
|
+
return parserSpec;
|
|
24
|
+
}
|
|
25
|
+
// String reference - resolve it
|
|
26
|
+
if (typeof parserSpec === 'string') {
|
|
27
|
+
// Check if it's a custom element reference (contains dot)
|
|
28
|
+
if (parserSpec.includes('.')) {
|
|
29
|
+
const dotIndex = parserSpec.indexOf('.');
|
|
30
|
+
const elementName = parserSpec.substring(0, dotIndex);
|
|
31
|
+
const methodName = parserSpec.substring(dotIndex + 1);
|
|
32
|
+
// Try custom element lookup
|
|
33
|
+
if (typeof customElements !== 'undefined') {
|
|
34
|
+
try {
|
|
35
|
+
const ctr = customElements.get(elementName);
|
|
36
|
+
if (ctr && typeof ctr[methodName] === 'function') {
|
|
37
|
+
return ctr[methodName];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// customElements.get might throw, fall through to registry
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Fall through to global registry (allows dot notation in registry too)
|
|
45
|
+
}
|
|
46
|
+
// Try global registry
|
|
47
|
+
const parser = globalParserRegistry.get(parserSpec);
|
|
48
|
+
if (parser) {
|
|
49
|
+
return parser;
|
|
50
|
+
}
|
|
51
|
+
// Not found anywhere
|
|
52
|
+
throw new Error(`Parser "${parserSpec}" not found. ` +
|
|
53
|
+
`Check that it's registered in globalParserRegistry or exists as a static method on the custom element.`);
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Creates a cache key from an AttrConfig
|
|
59
|
+
* Includes instanceOf and parser identifier to ensure correct cache hits
|
|
60
|
+
*/
|
|
61
|
+
function getCacheKey(config) {
|
|
62
|
+
const instanceOfStr = typeof config.instanceOf === 'function'
|
|
63
|
+
? config.instanceOf.name
|
|
64
|
+
: (config.instanceOf || 'default');
|
|
65
|
+
// Include parser in cache key
|
|
66
|
+
let parserStr;
|
|
67
|
+
if (config.parser === undefined) {
|
|
68
|
+
parserStr = 'builtin';
|
|
69
|
+
}
|
|
70
|
+
else if (typeof config.parser === 'string') {
|
|
71
|
+
parserStr = `named:${config.parser}`;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
parserStr = 'custom';
|
|
75
|
+
}
|
|
76
|
+
return `${instanceOfStr}|${parserStr}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Gets a cached parsed value or parses and caches it
|
|
80
|
+
* @param attrValue - The attribute value to parse (or null)
|
|
81
|
+
* @param config - The attribute configuration
|
|
82
|
+
* @param parser - The parser function to use
|
|
83
|
+
* @returns The parsed value
|
|
84
|
+
*/
|
|
85
|
+
function parseWithCache(attrValue, config, parser) {
|
|
86
|
+
// Skip caching for Boolean (presence check doesn't benefit from caching)
|
|
87
|
+
if (config.instanceOf === 'Boolean') {
|
|
88
|
+
return parser(attrValue);
|
|
89
|
+
}
|
|
90
|
+
// Get or create cache for this config
|
|
91
|
+
const cacheKey = getCacheKey(config);
|
|
92
|
+
if (!parseCache.has(cacheKey)) {
|
|
93
|
+
parseCache.set(cacheKey, new Map());
|
|
94
|
+
}
|
|
95
|
+
const valueCache = parseCache.get(cacheKey);
|
|
96
|
+
// Use special key for null values
|
|
97
|
+
const valueCacheKey = attrValue === null ? '__NULL__' : attrValue;
|
|
98
|
+
// Check if we have a cached value
|
|
99
|
+
if (valueCache.has(valueCacheKey)) {
|
|
100
|
+
const cachedValue = valueCache.get(valueCacheKey);
|
|
101
|
+
// Return clone if requested
|
|
102
|
+
if (config.parseCache === 'cloned') {
|
|
103
|
+
// Use structuredClone for deep cloning
|
|
104
|
+
return structuredClone(cachedValue);
|
|
105
|
+
}
|
|
106
|
+
// Return shared reference
|
|
107
|
+
return cachedValue;
|
|
108
|
+
}
|
|
109
|
+
// Parse the value
|
|
110
|
+
const parsedValue = parser(attrValue);
|
|
111
|
+
// Store in cache
|
|
112
|
+
valueCache.set(valueCacheKey, parsedValue);
|
|
113
|
+
// Return clone if requested (even on first parse)
|
|
114
|
+
if (config.parseCache === 'cloned') {
|
|
115
|
+
return structuredClone(parsedValue);
|
|
116
|
+
}
|
|
117
|
+
return parsedValue;
|
|
118
|
+
}
|
|
1
119
|
/**
|
|
2
120
|
* Checks if a string contains a dash or non-ASCII character
|
|
3
121
|
*/
|
|
@@ -188,14 +306,40 @@ export function parseWithAttrs(element, attrPatterns, allowUnprefixed) {
|
|
|
188
306
|
// Second pass: read attributes and parse values
|
|
189
307
|
for (const [key, { attrName, config }] of resolvedAttrs) {
|
|
190
308
|
const attrValue = getAttributeValue(element, attrName, allowUnprefixed);
|
|
191
|
-
//
|
|
192
|
-
if (attrValue === null
|
|
309
|
+
// Handle missing attribute
|
|
310
|
+
if (attrValue === null) {
|
|
311
|
+
// Use valIfNull if defined
|
|
312
|
+
if (config.valIfNull !== undefined) {
|
|
313
|
+
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
|
314
|
+
if (mapsTo === '.') {
|
|
315
|
+
// Spread into root
|
|
316
|
+
if (typeof config.valIfNull === 'object' && config.valIfNull !== null) {
|
|
317
|
+
Object.assign(result, config.valIfNull);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
result[mapsTo] = config.valIfNull;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Skip if no valIfNull and not Boolean (Boolean uses presence check)
|
|
325
|
+
else if (config.instanceOf !== 'Boolean') {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
// For Boolean without valIfNull, fall through to parser
|
|
329
|
+
else {
|
|
330
|
+
const parser = resolveParser(config.parser) || getDefaultParser(config.instanceOf);
|
|
331
|
+
const parsedValue = parser(attrValue);
|
|
332
|
+
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
|
333
|
+
result[mapsTo] = parsedValue;
|
|
334
|
+
}
|
|
193
335
|
continue;
|
|
194
336
|
}
|
|
195
|
-
//
|
|
196
|
-
const parser = config.parser || getDefaultParser(config.instanceOf);
|
|
197
|
-
//
|
|
198
|
-
const parsedValue =
|
|
337
|
+
// Attribute exists - parse normally
|
|
338
|
+
const parser = resolveParser(config.parser) || getDefaultParser(config.instanceOf);
|
|
339
|
+
// Use cache if parseCache is specified
|
|
340
|
+
const parsedValue = config.parseCache
|
|
341
|
+
? parseWithCache(attrValue, config, parser)
|
|
342
|
+
: parser(attrValue);
|
|
199
343
|
// Determine target property
|
|
200
344
|
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
|
201
345
|
// Add to result
|
package/parseWithAttrs.ts
CHANGED
|
@@ -1,4 +1,148 @@
|
|
|
1
1
|
import { AttrPatterns, AttrConfig } from './types';
|
|
2
|
+
import { globalParserRegistry } from './parserRegistry.js';
|
|
3
|
+
|
|
4
|
+
// Module-level cache for parsed attribute values
|
|
5
|
+
// Structure: Map<configKey, Map<attrValue, parsedValue>>
|
|
6
|
+
const parseCache = new Map<string, Map<string, any>>();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves a parser specification to an actual parser function
|
|
10
|
+
* Supports:
|
|
11
|
+
* - Inline functions (direct use)
|
|
12
|
+
* - Named parsers from global registry
|
|
13
|
+
* - Custom element static methods (element-name.methodName)
|
|
14
|
+
*
|
|
15
|
+
* @param parserSpec - Parser function or string reference
|
|
16
|
+
* @returns The resolved parser function
|
|
17
|
+
* @throws Error if parser cannot be resolved
|
|
18
|
+
*/
|
|
19
|
+
function resolveParser(parserSpec: ((v: string | null) => any) | string | undefined): ((v: string | null) => any) | undefined {
|
|
20
|
+
// Undefined - no parser specified
|
|
21
|
+
if (parserSpec === undefined) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Inline function - use directly
|
|
26
|
+
if (typeof parserSpec === 'function') {
|
|
27
|
+
return parserSpec;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// String reference - resolve it
|
|
31
|
+
if (typeof parserSpec === 'string') {
|
|
32
|
+
// Check if it's a custom element reference (contains dot)
|
|
33
|
+
if (parserSpec.includes('.')) {
|
|
34
|
+
const dotIndex = parserSpec.indexOf('.');
|
|
35
|
+
const elementName = parserSpec.substring(0, dotIndex);
|
|
36
|
+
const methodName = parserSpec.substring(dotIndex + 1);
|
|
37
|
+
|
|
38
|
+
// Try custom element lookup
|
|
39
|
+
if (typeof customElements !== 'undefined') {
|
|
40
|
+
try {
|
|
41
|
+
const ctr = customElements.get(elementName);
|
|
42
|
+
if (ctr && typeof (ctr as any)[methodName] === 'function') {
|
|
43
|
+
return (ctr as any)[methodName];
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// customElements.get might throw, fall through to registry
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fall through to global registry (allows dot notation in registry too)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Try global registry
|
|
54
|
+
const parser = globalParserRegistry.get(parserSpec);
|
|
55
|
+
if (parser) {
|
|
56
|
+
return parser;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Not found anywhere
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Parser "${parserSpec}" not found. ` +
|
|
62
|
+
`Check that it's registered in globalParserRegistry or exists as a static method on the custom element.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a cache key from an AttrConfig
|
|
71
|
+
* Includes instanceOf and parser identifier to ensure correct cache hits
|
|
72
|
+
*/
|
|
73
|
+
function getCacheKey(config: AttrConfig<any>): string {
|
|
74
|
+
const instanceOfStr = typeof config.instanceOf === 'function'
|
|
75
|
+
? config.instanceOf.name
|
|
76
|
+
: (config.instanceOf || 'default');
|
|
77
|
+
|
|
78
|
+
// Include parser in cache key
|
|
79
|
+
let parserStr: string;
|
|
80
|
+
if (config.parser === undefined) {
|
|
81
|
+
parserStr = 'builtin';
|
|
82
|
+
} else if (typeof config.parser === 'string') {
|
|
83
|
+
parserStr = `named:${config.parser}`;
|
|
84
|
+
} else {
|
|
85
|
+
parserStr = 'custom';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return `${instanceOfStr}|${parserStr}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Gets a cached parsed value or parses and caches it
|
|
93
|
+
* @param attrValue - The attribute value to parse (or null)
|
|
94
|
+
* @param config - The attribute configuration
|
|
95
|
+
* @param parser - The parser function to use
|
|
96
|
+
* @returns The parsed value
|
|
97
|
+
*/
|
|
98
|
+
function parseWithCache(
|
|
99
|
+
attrValue: string | null,
|
|
100
|
+
config: AttrConfig<any>,
|
|
101
|
+
parser: (v: string | null) => any
|
|
102
|
+
): any {
|
|
103
|
+
// Skip caching for Boolean (presence check doesn't benefit from caching)
|
|
104
|
+
if (config.instanceOf === 'Boolean') {
|
|
105
|
+
return parser(attrValue);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Get or create cache for this config
|
|
109
|
+
const cacheKey = getCacheKey(config);
|
|
110
|
+
if (!parseCache.has(cacheKey)) {
|
|
111
|
+
parseCache.set(cacheKey, new Map());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const valueCache = parseCache.get(cacheKey)!;
|
|
115
|
+
|
|
116
|
+
// Use special key for null values
|
|
117
|
+
const valueCacheKey = attrValue === null ? '__NULL__' : attrValue;
|
|
118
|
+
|
|
119
|
+
// Check if we have a cached value
|
|
120
|
+
if (valueCache.has(valueCacheKey)) {
|
|
121
|
+
const cachedValue = valueCache.get(valueCacheKey);
|
|
122
|
+
|
|
123
|
+
// Return clone if requested
|
|
124
|
+
if (config.parseCache === 'cloned') {
|
|
125
|
+
// Use structuredClone for deep cloning
|
|
126
|
+
return structuredClone(cachedValue);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Return shared reference
|
|
130
|
+
return cachedValue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Parse the value
|
|
134
|
+
const parsedValue = parser(attrValue);
|
|
135
|
+
|
|
136
|
+
// Store in cache
|
|
137
|
+
valueCache.set(valueCacheKey, parsedValue);
|
|
138
|
+
|
|
139
|
+
// Return clone if requested (even on first parse)
|
|
140
|
+
if (config.parseCache === 'cloned') {
|
|
141
|
+
return structuredClone(parsedValue);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return parsedValue;
|
|
145
|
+
}
|
|
2
146
|
|
|
3
147
|
/**
|
|
4
148
|
* Checks if a string contains a dash or non-ASCII character
|
|
@@ -221,16 +365,42 @@ export function parseWithAttrs<T = any>(
|
|
|
221
365
|
for (const [key, { attrName, config }] of resolvedAttrs) {
|
|
222
366
|
const attrValue = getAttributeValue(element, attrName, allowUnprefixed);
|
|
223
367
|
|
|
224
|
-
//
|
|
225
|
-
if (attrValue === null
|
|
368
|
+
// Handle missing attribute
|
|
369
|
+
if (attrValue === null) {
|
|
370
|
+
// Use valIfNull if defined
|
|
371
|
+
if (config.valIfNull !== undefined) {
|
|
372
|
+
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
|
373
|
+
|
|
374
|
+
if (mapsTo === '.') {
|
|
375
|
+
// Spread into root
|
|
376
|
+
if (typeof config.valIfNull === 'object' && config.valIfNull !== null) {
|
|
377
|
+
Object.assign(result, config.valIfNull);
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
result[mapsTo as string] = config.valIfNull;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Skip if no valIfNull and not Boolean (Boolean uses presence check)
|
|
384
|
+
else if (config.instanceOf !== 'Boolean') {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
// For Boolean without valIfNull, fall through to parser
|
|
388
|
+
else {
|
|
389
|
+
const parser = resolveParser(config.parser) || getDefaultParser(config.instanceOf);
|
|
390
|
+
const parsedValue = parser(attrValue);
|
|
391
|
+
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
|
392
|
+
result[mapsTo as string] = parsedValue;
|
|
393
|
+
}
|
|
226
394
|
continue;
|
|
227
395
|
}
|
|
228
396
|
|
|
229
|
-
//
|
|
230
|
-
const parser = config.parser || getDefaultParser(config.instanceOf);
|
|
397
|
+
// Attribute exists - parse normally
|
|
398
|
+
const parser = resolveParser(config.parser) || getDefaultParser(config.instanceOf);
|
|
231
399
|
|
|
232
|
-
//
|
|
233
|
-
const parsedValue =
|
|
400
|
+
// Use cache if parseCache is specified
|
|
401
|
+
const parsedValue = config.parseCache
|
|
402
|
+
? parseWithCache(attrValue, config, parser)
|
|
403
|
+
: parser(attrValue);
|
|
234
404
|
|
|
235
405
|
// Determine target property
|
|
236
406
|
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|