assign-gingerly 0.0.12 → 0.0.14
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 +547 -117
- package/assignGingerly.ts +1 -1
- package/index.js +2 -0
- package/index.ts +2 -0
- package/package.json +14 -6
- package/parseWithAttrs.js +150 -6
- package/parseWithAttrs.ts +177 -7
- package/parserRegistry.js +71 -0
- package/parserRegistry.ts +94 -0
- package/types/LICENSE +21 -0
- package/types/README.md +2 -0
- package/{types.d.ts → types/assign-gingerly/types.d.ts} +22 -2
package/assignGingerly.ts
CHANGED
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
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": {
|
|
@@ -13,28 +13,36 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "Bruce B. Anderson <andeson.bruce.b@gmail.com>",
|
|
15
15
|
"type": "module",
|
|
16
|
-
"types": "types.d.ts",
|
|
16
|
+
"types": "types/assign-gingerly/types.d.ts",
|
|
17
17
|
"files": [
|
|
18
18
|
"*.js",
|
|
19
19
|
"*.ts",
|
|
20
20
|
"README.md",
|
|
21
|
-
"LICENSE"
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"types/assign-gingerly/types.d.ts"
|
|
22
23
|
],
|
|
23
24
|
"exports": {
|
|
24
25
|
".": {
|
|
25
26
|
"default": "./index.js",
|
|
26
|
-
"types": "./
|
|
27
|
+
"types": "./index.ts"
|
|
27
28
|
},
|
|
28
29
|
"./assignGingerly.js": {
|
|
29
30
|
"default": "./assignGingerly.js",
|
|
30
|
-
"types": "./
|
|
31
|
+
"types": "./assignGingerly.ts"
|
|
31
32
|
},
|
|
32
33
|
"./assignTentatively.js": {
|
|
33
34
|
"default": "./assignTentatively.js",
|
|
34
|
-
"types": "./
|
|
35
|
+
"types": "./assignTentatively.ts"
|
|
35
36
|
},
|
|
36
37
|
"./waitForEvent.js": {
|
|
37
38
|
"default": "./waitForEvent.js"
|
|
39
|
+
},
|
|
40
|
+
"./parserRegistry.js": {
|
|
41
|
+
"default": "./parserRegistry.js"
|
|
42
|
+
},
|
|
43
|
+
"./parseWithAttrs.js": {
|
|
44
|
+
"default": "./parseWithAttrs.js",
|
|
45
|
+
"types": "./parseWithAttrs.ts"
|
|
38
46
|
}
|
|
39
47
|
},
|
|
40
48
|
"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
|
-
import { AttrPatterns, AttrConfig } from './types';
|
|
1
|
+
import { AttrPatterns, AttrConfig } from './types/assign-gingerly/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);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for named parsers that can be referenced by string name
|
|
3
|
+
* Enables JSON serialization of configs with custom parsers
|
|
4
|
+
*/
|
|
5
|
+
export class ParserRegistry {
|
|
6
|
+
parsers = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Register a parser with a given name
|
|
9
|
+
* @param name - The name to register the parser under
|
|
10
|
+
* @param parser - The parser function
|
|
11
|
+
*/
|
|
12
|
+
register(name, parser) {
|
|
13
|
+
if (this.parsers.has(name)) {
|
|
14
|
+
console.warn(`Parser "${name}" already registered, overwriting`);
|
|
15
|
+
}
|
|
16
|
+
this.parsers.set(name, parser);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get a parser by name
|
|
20
|
+
* @param name - The name of the parser
|
|
21
|
+
* @returns The parser function or undefined if not found
|
|
22
|
+
*/
|
|
23
|
+
get(name) {
|
|
24
|
+
return this.parsers.get(name);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if a parser is registered
|
|
28
|
+
* @param name - The name to check
|
|
29
|
+
* @returns True if the parser exists
|
|
30
|
+
*/
|
|
31
|
+
has(name) {
|
|
32
|
+
return this.parsers.has(name);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Unregister a parser
|
|
36
|
+
* @param name - The name of the parser to remove
|
|
37
|
+
* @returns True if the parser was removed, false if it didn't exist
|
|
38
|
+
*/
|
|
39
|
+
unregister(name) {
|
|
40
|
+
return this.parsers.delete(name);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get all registered parser names
|
|
44
|
+
* @returns Array of parser names
|
|
45
|
+
*/
|
|
46
|
+
getNames() {
|
|
47
|
+
return Array.from(this.parsers.keys());
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Global parser registry instance
|
|
52
|
+
* Use this to register parsers that can be referenced by name in configs
|
|
53
|
+
*/
|
|
54
|
+
export const globalParserRegistry = new ParserRegistry();
|
|
55
|
+
// Register common built-in parsers
|
|
56
|
+
globalParserRegistry.register('timestamp', (v) => v ? new Date(v).getTime() : null);
|
|
57
|
+
globalParserRegistry.register('date', (v) => v ? new Date(v) : null);
|
|
58
|
+
globalParserRegistry.register('csv', (v) => v ? v.split(',').map(s => s.trim()) : []);
|
|
59
|
+
globalParserRegistry.register('int', (v) => v ? parseInt(v, 10) : null);
|
|
60
|
+
globalParserRegistry.register('float', (v) => v ? parseFloat(v) : null);
|
|
61
|
+
globalParserRegistry.register('boolean', (v) => v !== null);
|
|
62
|
+
globalParserRegistry.register('json', (v) => {
|
|
63
|
+
if (v === null || v === '')
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(v);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
throw new Error(`Failed to parse JSON: "${v}". Error: ${e}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for named parsers that can be referenced by string name
|
|
3
|
+
* Enables JSON serialization of configs with custom parsers
|
|
4
|
+
*/
|
|
5
|
+
export class ParserRegistry {
|
|
6
|
+
private parsers = new Map<string, (v: string | null) => any>();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register a parser with a given name
|
|
10
|
+
* @param name - The name to register the parser under
|
|
11
|
+
* @param parser - The parser function
|
|
12
|
+
*/
|
|
13
|
+
register(name: string, parser: (v: string | null) => any): void {
|
|
14
|
+
if (this.parsers.has(name)) {
|
|
15
|
+
console.warn(`Parser "${name}" already registered, overwriting`);
|
|
16
|
+
}
|
|
17
|
+
this.parsers.set(name, parser);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get a parser by name
|
|
22
|
+
* @param name - The name of the parser
|
|
23
|
+
* @returns The parser function or undefined if not found
|
|
24
|
+
*/
|
|
25
|
+
get(name: string): ((v: string | null) => any) | undefined {
|
|
26
|
+
return this.parsers.get(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a parser is registered
|
|
31
|
+
* @param name - The name to check
|
|
32
|
+
* @returns True if the parser exists
|
|
33
|
+
*/
|
|
34
|
+
has(name: string): boolean {
|
|
35
|
+
return this.parsers.has(name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Unregister a parser
|
|
40
|
+
* @param name - The name of the parser to remove
|
|
41
|
+
* @returns True if the parser was removed, false if it didn't exist
|
|
42
|
+
*/
|
|
43
|
+
unregister(name: string): boolean {
|
|
44
|
+
return this.parsers.delete(name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get all registered parser names
|
|
49
|
+
* @returns Array of parser names
|
|
50
|
+
*/
|
|
51
|
+
getNames(): string[] {
|
|
52
|
+
return Array.from(this.parsers.keys());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Global parser registry instance
|
|
58
|
+
* Use this to register parsers that can be referenced by name in configs
|
|
59
|
+
*/
|
|
60
|
+
export const globalParserRegistry = new ParserRegistry();
|
|
61
|
+
|
|
62
|
+
// Register common built-in parsers
|
|
63
|
+
globalParserRegistry.register('timestamp', (v) =>
|
|
64
|
+
v ? new Date(v).getTime() : null
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
globalParserRegistry.register('date', (v) =>
|
|
68
|
+
v ? new Date(v) : null
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
globalParserRegistry.register('csv', (v) =>
|
|
72
|
+
v ? v.split(',').map(s => s.trim()) : []
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
globalParserRegistry.register('int', (v) =>
|
|
76
|
+
v ? parseInt(v, 10) : null
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
globalParserRegistry.register('float', (v) =>
|
|
80
|
+
v ? parseFloat(v) : null
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
globalParserRegistry.register('boolean', (v) =>
|
|
84
|
+
v !== null
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
globalParserRegistry.register('json', (v) => {
|
|
88
|
+
if (v === null || v === '') return null;
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(v);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
throw new Error(`Failed to parse JSON: "${v}". Error: ${e}`);
|
|
93
|
+
}
|
|
94
|
+
});
|
package/types/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bruce B. Anderson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/types/README.md
ADDED
|
@@ -88,9 +88,29 @@ export interface AttrConfig<T = any> {
|
|
|
88
88
|
| `${pathString} -=`
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
91
|
+
* Parser to transform attribute string value
|
|
92
|
+
* - Function: Inline parser function (not JSON serializable)
|
|
93
|
+
* - String: Named parser reference (JSON serializable)
|
|
94
|
+
* - Simple name: Looks up in global parser registry (e.g., 'timestamp', 'csv')
|
|
95
|
+
* - Dot notation: Looks up static method on custom element (e.g., 'my-widget.parseSpecial')
|
|
96
|
+
* Falls back to global registry if custom element not found
|
|
92
97
|
*/
|
|
93
|
-
parser?: (attrValue: string | null) => any;
|
|
98
|
+
parser?: ((attrValue: string | null) => any) | string;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Default value to use when attribute is missing
|
|
102
|
+
* If defined, bypasses parser when attribute is not present
|
|
103
|
+
* If undefined, property is not added to initVals when attribute is missing
|
|
104
|
+
*/
|
|
105
|
+
valIfNull?: any;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Enable caching of parsed attribute values
|
|
109
|
+
* - 'shared': Cache and reuse the same parsed object (fast, but enhancements must not mutate)
|
|
110
|
+
* - 'cloned': Cache and return a structural clone (safer, but slower)
|
|
111
|
+
* Note: Parsers should be pure functions when using caching
|
|
112
|
+
*/
|
|
113
|
+
parseCache?: 'shared' | 'cloned';
|
|
94
114
|
|
|
95
115
|
// /**
|
|
96
116
|
// * Whether to only read the initial value (true) or continue observing changes (false)
|