assign-gingerly 0.0.26 → 0.0.27
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/ScopedParserRegistry.js +94 -0
- package/ScopedParserRegistry.ts +111 -0
- package/package.json +1 -1
- package/parseWithAttrs.js +61 -45
- package/parseWithAttrs.ts +82 -65
- package/parserRegistry.js +31 -1
- package/parserRegistry.ts +53 -11
- package/types/assign-gingerly/types.d.ts +55 -5
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for parsers scoped to a synthesizer element (be-hive, htmx-container, etc.)
|
|
3
|
+
* Enables lazy-loading of complex parsers with Promise-based waiting
|
|
4
|
+
*/
|
|
5
|
+
export class ScopedParserRegistry {
|
|
6
|
+
parsers = new Map();
|
|
7
|
+
pendingWaits = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* Register a parser with a given name
|
|
10
|
+
* Resolves any pending waiters for this parser
|
|
11
|
+
* @param name - The name to register the parser under
|
|
12
|
+
* @param parser - The parser function
|
|
13
|
+
*/
|
|
14
|
+
register(name, parser) {
|
|
15
|
+
if (this.parsers.has(name)) {
|
|
16
|
+
console.warn(`Parser "${name}" already registered in scoped registry, overwriting`);
|
|
17
|
+
}
|
|
18
|
+
this.parsers.set(name, parser);
|
|
19
|
+
// Resolve any pending waiters for this parser
|
|
20
|
+
const waiters = this.pendingWaits.get(name);
|
|
21
|
+
if (waiters) {
|
|
22
|
+
waiters.forEach(({ resolve }) => resolve());
|
|
23
|
+
this.pendingWaits.delete(name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get a parser by name
|
|
28
|
+
* @param name - The name of the parser
|
|
29
|
+
* @returns The parser function or undefined if not found
|
|
30
|
+
*/
|
|
31
|
+
get(name) {
|
|
32
|
+
return this.parsers.get(name);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if a parser is registered
|
|
36
|
+
* @param name - The name to check
|
|
37
|
+
* @returns True if the parser exists
|
|
38
|
+
*/
|
|
39
|
+
has(name) {
|
|
40
|
+
return this.parsers.has(name);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Wait for multiple parsers to be registered
|
|
44
|
+
* @param names - Array of parser names to wait for
|
|
45
|
+
* @param timeout - Timeout in milliseconds (default: 60000)
|
|
46
|
+
* @returns Promise that resolves when all parsers are registered
|
|
47
|
+
* @throws Error listing missing parsers if timeout expires
|
|
48
|
+
*/
|
|
49
|
+
waitFor(names, timeout = 60000) {
|
|
50
|
+
// Check if all parsers are already registered
|
|
51
|
+
const missing = names.filter(name => !this.has(name));
|
|
52
|
+
if (missing.length === 0) {
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
}
|
|
55
|
+
// Create promise that resolves when all parsers are registered
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
let timeoutId;
|
|
58
|
+
// Set timeout that rejects with descriptive error
|
|
59
|
+
timeoutId = setTimeout(() => {
|
|
60
|
+
const stillMissing = names.filter(name => !this.has(name));
|
|
61
|
+
reject(new Error(`Timeout waiting for parsers: ${stillMissing.join(', ')}`));
|
|
62
|
+
}, timeout);
|
|
63
|
+
// Track which parsers we're waiting for
|
|
64
|
+
let remainingCount = missing.length;
|
|
65
|
+
// Create waiter for each missing parser
|
|
66
|
+
missing.forEach(name => {
|
|
67
|
+
const waiter = {
|
|
68
|
+
resolve: () => {
|
|
69
|
+
remainingCount--;
|
|
70
|
+
if (remainingCount === 0) {
|
|
71
|
+
if (timeoutId !== undefined) {
|
|
72
|
+
clearTimeout(timeoutId);
|
|
73
|
+
}
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
reject
|
|
78
|
+
};
|
|
79
|
+
// Add waiter to pending list
|
|
80
|
+
if (!this.pendingWaits.has(name)) {
|
|
81
|
+
this.pendingWaits.set(name, []);
|
|
82
|
+
}
|
|
83
|
+
this.pendingWaits.get(name).push(waiter);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get all registered parser names
|
|
89
|
+
* @returns Array of parser names
|
|
90
|
+
*/
|
|
91
|
+
getNames() {
|
|
92
|
+
return Array.from(this.parsers.keys());
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ParserFunction } from './types/assign-gingerly/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Registry for parsers scoped to a synthesizer element (be-hive, htmx-container, etc.)
|
|
5
|
+
* Enables lazy-loading of complex parsers with Promise-based waiting
|
|
6
|
+
*/
|
|
7
|
+
export class ScopedParserRegistry {
|
|
8
|
+
private parsers = new Map<string, ParserFunction>();
|
|
9
|
+
private pendingWaits = new Map<string, Array<{
|
|
10
|
+
resolve: () => void;
|
|
11
|
+
reject: (error: Error) => void;
|
|
12
|
+
}>>();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register a parser with a given name
|
|
16
|
+
* Resolves any pending waiters for this parser
|
|
17
|
+
* @param name - The name to register the parser under
|
|
18
|
+
* @param parser - The parser function
|
|
19
|
+
*/
|
|
20
|
+
register(name: string, parser: ParserFunction): void {
|
|
21
|
+
if (this.parsers.has(name)) {
|
|
22
|
+
console.warn(`Parser "${name}" already registered in scoped registry, overwriting`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.parsers.set(name, parser);
|
|
26
|
+
|
|
27
|
+
// Resolve any pending waiters for this parser
|
|
28
|
+
const waiters = this.pendingWaits.get(name);
|
|
29
|
+
if (waiters) {
|
|
30
|
+
waiters.forEach(({ resolve }) => resolve());
|
|
31
|
+
this.pendingWaits.delete(name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get a parser by name
|
|
37
|
+
* @param name - The name of the parser
|
|
38
|
+
* @returns The parser function or undefined if not found
|
|
39
|
+
*/
|
|
40
|
+
get(name: string): ParserFunction | undefined {
|
|
41
|
+
return this.parsers.get(name);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a parser is registered
|
|
46
|
+
* @param name - The name to check
|
|
47
|
+
* @returns True if the parser exists
|
|
48
|
+
*/
|
|
49
|
+
has(name: string): boolean {
|
|
50
|
+
return this.parsers.has(name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wait for multiple parsers to be registered
|
|
55
|
+
* @param names - Array of parser names to wait for
|
|
56
|
+
* @param timeout - Timeout in milliseconds (default: 60000)
|
|
57
|
+
* @returns Promise that resolves when all parsers are registered
|
|
58
|
+
* @throws Error listing missing parsers if timeout expires
|
|
59
|
+
*/
|
|
60
|
+
waitFor(names: string[], timeout: number = 60000): Promise<void> {
|
|
61
|
+
// Check if all parsers are already registered
|
|
62
|
+
const missing = names.filter(name => !this.has(name));
|
|
63
|
+
if (missing.length === 0) {
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create promise that resolves when all parsers are registered
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
+
|
|
71
|
+
// Set timeout that rejects with descriptive error
|
|
72
|
+
timeoutId = setTimeout(() => {
|
|
73
|
+
const stillMissing = names.filter(name => !this.has(name));
|
|
74
|
+
reject(new Error(`Timeout waiting for parsers: ${stillMissing.join(', ')}`));
|
|
75
|
+
}, timeout);
|
|
76
|
+
|
|
77
|
+
// Track which parsers we're waiting for
|
|
78
|
+
let remainingCount = missing.length;
|
|
79
|
+
|
|
80
|
+
// Create waiter for each missing parser
|
|
81
|
+
missing.forEach(name => {
|
|
82
|
+
const waiter = {
|
|
83
|
+
resolve: () => {
|
|
84
|
+
remainingCount--;
|
|
85
|
+
if (remainingCount === 0) {
|
|
86
|
+
if (timeoutId !== undefined) {
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
}
|
|
89
|
+
resolve();
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
reject
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Add waiter to pending list
|
|
96
|
+
if (!this.pendingWaits.has(name)) {
|
|
97
|
+
this.pendingWaits.set(name, []);
|
|
98
|
+
}
|
|
99
|
+
this.pendingWaits.get(name)!.push(waiter);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get all registered parser names
|
|
106
|
+
* @returns Array of parser names
|
|
107
|
+
*/
|
|
108
|
+
getNames(): string[] {
|
|
109
|
+
return Array.from(this.parsers.keys());
|
|
110
|
+
}
|
|
111
|
+
}
|
package/package.json
CHANGED
package/parseWithAttrs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { globalParserRegistry } from './parserRegistry.js';
|
|
1
|
+
import { globalParserRegistry, getParserRegistry } from './parserRegistry.js';
|
|
2
2
|
import { resolveTemplate } from './resolveTemplate.js';
|
|
3
3
|
// Module-level cache for parsed attribute values
|
|
4
4
|
// Structure: Map<configKey, Map<attrValue, parsedValue>>
|
|
@@ -7,14 +7,15 @@ const parseCache = new Map();
|
|
|
7
7
|
* Resolves a parser specification to an actual parser function
|
|
8
8
|
* Supports:
|
|
9
9
|
* - Inline functions (direct use)
|
|
10
|
-
* - Named parsers from
|
|
11
|
-
* -
|
|
10
|
+
* - Named parsers from scoped registry (if synthesizerElement provided)
|
|
11
|
+
* - Named parsers from global registry (fallback)
|
|
12
12
|
*
|
|
13
13
|
* @param parserSpec - Parser function or string reference
|
|
14
|
+
* @param synthesizerElement - Optional synthesizer element for scoped parser lookup
|
|
14
15
|
* @returns The resolved parser function
|
|
15
16
|
* @throws Error if parser cannot be resolved
|
|
16
17
|
*/
|
|
17
|
-
function resolveParser(parserSpec) {
|
|
18
|
+
function resolveParser(parserSpec, synthesizerElement) {
|
|
18
19
|
// Undefined - no parser specified
|
|
19
20
|
if (parserSpec === undefined) {
|
|
20
21
|
return undefined;
|
|
@@ -23,38 +24,28 @@ function resolveParser(parserSpec) {
|
|
|
23
24
|
if (typeof parserSpec === 'function') {
|
|
24
25
|
return parserSpec;
|
|
25
26
|
}
|
|
26
|
-
//
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (!ctr) {
|
|
35
|
-
throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: custom element "${elementName}" not found`);
|
|
36
|
-
}
|
|
37
|
-
if (typeof ctr[methodName] !== 'function') {
|
|
38
|
-
throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: static method "${methodName}" not found on custom element "${elementName}"`);
|
|
39
|
-
}
|
|
40
|
-
return ctr[methodName];
|
|
41
|
-
}
|
|
42
|
-
catch (e) {
|
|
43
|
-
if (e instanceof Error && e.message.startsWith('Cannot resolve parser')) {
|
|
44
|
-
throw e;
|
|
27
|
+
// String reference - resolve from scoped or global registry
|
|
28
|
+
if (typeof parserSpec === 'string') {
|
|
29
|
+
// Check scoped registry first (if synthesizerElement provided)
|
|
30
|
+
if (synthesizerElement) {
|
|
31
|
+
const scopedRegistry = getParserRegistry(synthesizerElement);
|
|
32
|
+
const scopedParser = scopedRegistry.get(parserSpec);
|
|
33
|
+
if (scopedParser) {
|
|
34
|
+
return scopedParser;
|
|
45
35
|
}
|
|
46
|
-
throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: ${e instanceof Error ? e.message : String(e)}`);
|
|
47
36
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (parser) {
|
|
53
|
-
return parser;
|
|
37
|
+
// Fallback to global registry
|
|
38
|
+
const globalParser = globalParserRegistry.get(parserSpec);
|
|
39
|
+
if (globalParser) {
|
|
40
|
+
return globalParser;
|
|
54
41
|
}
|
|
55
|
-
// Not found in registry
|
|
56
|
-
throw new Error(`Parser "${parserSpec}" not found
|
|
57
|
-
`
|
|
42
|
+
// Not found in either registry
|
|
43
|
+
throw new Error(`Parser "${parserSpec}" not found. ` +
|
|
44
|
+
`Checked ${synthesizerElement ? 'scoped registry and ' : ''}global registry.\n` +
|
|
45
|
+
`Ensure the parser is registered via:\n` +
|
|
46
|
+
`- <script type="emc-parser" src="..." parser-name="${parserSpec}">\n` +
|
|
47
|
+
`- registerParser(synthesizerElement, "${parserSpec}", parserFn)\n` +
|
|
48
|
+
`- globalParserRegistry.register("${parserSpec}", parserFn)`);
|
|
58
49
|
}
|
|
59
50
|
return undefined;
|
|
60
51
|
}
|
|
@@ -74,9 +65,6 @@ function getCacheKey(config) {
|
|
|
74
65
|
else if (typeof config.parser === 'string') {
|
|
75
66
|
parserStr = `named:${config.parser}`;
|
|
76
67
|
}
|
|
77
|
-
else if (Array.isArray(config.parser)) {
|
|
78
|
-
parserStr = `tuple:${config.parser[0]}.${config.parser[1]}`;
|
|
79
|
-
}
|
|
80
68
|
else {
|
|
81
69
|
parserStr = 'custom';
|
|
82
70
|
}
|
|
@@ -87,12 +75,13 @@ function getCacheKey(config) {
|
|
|
87
75
|
* @param attrValue - The attribute value to parse (or null)
|
|
88
76
|
* @param config - The attribute configuration
|
|
89
77
|
* @param parser - The parser function to use
|
|
78
|
+
* @param context - The parser context to pass to the parser
|
|
90
79
|
* @returns The parsed value
|
|
91
80
|
*/
|
|
92
|
-
function parseWithCache(attrValue, config, parser) {
|
|
81
|
+
function parseWithCache(attrValue, config, parser, context) {
|
|
93
82
|
// Skip caching for Boolean (presence check doesn't benefit from caching)
|
|
94
83
|
if (config.instanceOf === 'Boolean') {
|
|
95
|
-
return parser
|
|
84
|
+
return callParser(parser, attrValue, context);
|
|
96
85
|
}
|
|
97
86
|
// Get or create cache for this config
|
|
98
87
|
const cacheKey = getCacheKey(config);
|
|
@@ -111,7 +100,7 @@ function parseWithCache(attrValue, config, parser) {
|
|
|
111
100
|
return cachedValue;
|
|
112
101
|
}
|
|
113
102
|
// Parse the value
|
|
114
|
-
const parsedValue = parser
|
|
103
|
+
const parsedValue = callParser(parser, attrValue, context);
|
|
115
104
|
// Store in cache
|
|
116
105
|
valueCache.set(valueCacheKey, parsedValue);
|
|
117
106
|
// Return clone if requested (even on first parse)
|
|
@@ -120,6 +109,23 @@ function parseWithCache(attrValue, config, parser) {
|
|
|
120
109
|
}
|
|
121
110
|
return parsedValue;
|
|
122
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Calls a parser function with the appropriate signature
|
|
114
|
+
* Handles both simple (value-only) and advanced (value + context) parser signatures
|
|
115
|
+
* @param parser - The parser function
|
|
116
|
+
* @param attrValue - The attribute value
|
|
117
|
+
* @param context - The parser context
|
|
118
|
+
* @returns The parsed value
|
|
119
|
+
*/
|
|
120
|
+
function callParser(parser, attrValue, context) {
|
|
121
|
+
// Check parser arity (number of parameters)
|
|
122
|
+
// If parser accepts 2+ parameters, pass context
|
|
123
|
+
if (parser.length >= 2) {
|
|
124
|
+
return parser(attrValue, context);
|
|
125
|
+
}
|
|
126
|
+
// Otherwise, call with just the value (simple form)
|
|
127
|
+
return parser(attrValue);
|
|
128
|
+
}
|
|
123
129
|
/**
|
|
124
130
|
* Checks if a string contains a dash or non-ASCII character
|
|
125
131
|
*/
|
|
@@ -217,9 +223,12 @@ function getDefaultParser(instanceOf) {
|
|
|
217
223
|
* @param element - The DOM element to read attributes from
|
|
218
224
|
* @param attrPatterns - The attribute patterns configuration
|
|
219
225
|
* @param allowUnprefixed - Pattern (string or RegExp) that element tag name must match to allow unprefixed attributes
|
|
226
|
+
* @param spawnContext - Optional spawn context containing enhancement config and synthesizer element
|
|
220
227
|
* @returns Object with parsed attribute values ready for initVals
|
|
221
228
|
*/
|
|
222
|
-
export function parseWithAttrs(element, attrPatterns, allowUnprefixed) {
|
|
229
|
+
export function parseWithAttrs(element, attrPatterns, allowUnprefixed, spawnContext) {
|
|
230
|
+
// Extract synthesizerElement from spawnContext for backward compatibility
|
|
231
|
+
const synthesizerElement = spawnContext?.synthesizerElement;
|
|
223
232
|
// Validate base attribute if present
|
|
224
233
|
if ('base' in attrPatterns) {
|
|
225
234
|
const baseValue = attrPatterns.base;
|
|
@@ -276,6 +285,13 @@ export function parseWithAttrs(element, attrPatterns, allowUnprefixed) {
|
|
|
276
285
|
// Second pass: read attributes and parse values
|
|
277
286
|
for (const [key, { attrName, config }] of resolvedAttrs) {
|
|
278
287
|
const attrValue = getAttributeValue(element, attrName, allowUnprefixed);
|
|
288
|
+
// Create parser context
|
|
289
|
+
const parserContext = {
|
|
290
|
+
attrConfig: config,
|
|
291
|
+
spawnContext,
|
|
292
|
+
element,
|
|
293
|
+
attrName
|
|
294
|
+
};
|
|
279
295
|
// Handle missing attribute
|
|
280
296
|
if (attrValue === null) {
|
|
281
297
|
// Use valIfNull if defined
|
|
@@ -297,19 +313,19 @@ export function parseWithAttrs(element, attrPatterns, allowUnprefixed) {
|
|
|
297
313
|
}
|
|
298
314
|
// For Boolean without valIfNull, fall through to parser
|
|
299
315
|
else {
|
|
300
|
-
const parser = resolveParser(config.parser) || getDefaultParser(config.instanceOf);
|
|
301
|
-
const parsedValue = parser
|
|
316
|
+
const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
|
|
317
|
+
const parsedValue = callParser(parser, attrValue, parserContext);
|
|
302
318
|
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
|
303
319
|
result[mapsTo] = parsedValue;
|
|
304
320
|
}
|
|
305
321
|
continue;
|
|
306
322
|
}
|
|
307
323
|
// Attribute exists - parse normally
|
|
308
|
-
const parser = resolveParser(config.parser) || getDefaultParser(config.instanceOf);
|
|
324
|
+
const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
|
|
309
325
|
// Use cache if parseCache is specified
|
|
310
326
|
const parsedValue = config.parseCache
|
|
311
|
-
? parseWithCache(attrValue, config, parser)
|
|
312
|
-
: parser
|
|
327
|
+
? parseWithCache(attrValue, config, parser, parserContext)
|
|
328
|
+
: callParser(parser, attrValue, parserContext);
|
|
313
329
|
// Determine target property
|
|
314
330
|
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
|
315
331
|
// Add to result
|
package/parseWithAttrs.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AttrPatterns, AttrConfig } from './types/assign-gingerly/types';
|
|
2
|
-
import { globalParserRegistry } from './parserRegistry.js';
|
|
1
|
+
import { AttrPatterns, AttrConfig, ParserFunction, ParserContext, SpawnContext } from './types/assign-gingerly/types';
|
|
2
|
+
import { globalParserRegistry, getParserRegistry } from './parserRegistry.js';
|
|
3
3
|
import { resolveTemplate } from './resolveTemplate.js';
|
|
4
4
|
|
|
5
5
|
// Module-level cache for parsed attribute values
|
|
@@ -10,16 +10,18 @@ const parseCache = new Map<string, Map<string, any>>();
|
|
|
10
10
|
* Resolves a parser specification to an actual parser function
|
|
11
11
|
* Supports:
|
|
12
12
|
* - Inline functions (direct use)
|
|
13
|
-
* - Named parsers from
|
|
14
|
-
* -
|
|
13
|
+
* - Named parsers from scoped registry (if synthesizerElement provided)
|
|
14
|
+
* - Named parsers from global registry (fallback)
|
|
15
15
|
*
|
|
16
16
|
* @param parserSpec - Parser function or string reference
|
|
17
|
+
* @param synthesizerElement - Optional synthesizer element for scoped parser lookup
|
|
17
18
|
* @returns The resolved parser function
|
|
18
19
|
* @throws Error if parser cannot be resolved
|
|
19
20
|
*/
|
|
20
21
|
function resolveParser(
|
|
21
|
-
parserSpec:
|
|
22
|
-
|
|
22
|
+
parserSpec: ParserFunction | string | undefined,
|
|
23
|
+
synthesizerElement?: Element
|
|
24
|
+
): ParserFunction | undefined {
|
|
23
25
|
// Undefined - no parser specified
|
|
24
26
|
if (parserSpec === undefined) {
|
|
25
27
|
return undefined;
|
|
@@ -30,52 +32,31 @@ function resolveParser(
|
|
|
30
32
|
return parserSpec;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
//
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const ctr = customElements.get(elementName);
|
|
45
|
-
if (!ctr) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Cannot resolve parser [${elementName}, ${methodName}]: custom element "${elementName}" not found`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (typeof (ctr as any)[methodName] !== 'function') {
|
|
52
|
-
throw new Error(
|
|
53
|
-
`Cannot resolve parser [${elementName}, ${methodName}]: static method "${methodName}" not found on custom element "${elementName}"`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return (ctr as any)[methodName];
|
|
58
|
-
} catch (e) {
|
|
59
|
-
if (e instanceof Error && e.message.startsWith('Cannot resolve parser')) {
|
|
60
|
-
throw e;
|
|
35
|
+
// String reference - resolve from scoped or global registry
|
|
36
|
+
if (typeof parserSpec === 'string') {
|
|
37
|
+
// Check scoped registry first (if synthesizerElement provided)
|
|
38
|
+
if (synthesizerElement) {
|
|
39
|
+
const scopedRegistry = getParserRegistry(synthesizerElement);
|
|
40
|
+
const scopedParser = scopedRegistry.get(parserSpec);
|
|
41
|
+
if (scopedParser) {
|
|
42
|
+
return scopedParser;
|
|
61
43
|
}
|
|
62
|
-
throw new Error(
|
|
63
|
-
`Cannot resolve parser [${elementName}, ${methodName}]: ${e instanceof Error ? e.message : String(e)}`
|
|
64
|
-
);
|
|
65
44
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (parser) {
|
|
72
|
-
return parser;
|
|
45
|
+
|
|
46
|
+
// Fallback to global registry
|
|
47
|
+
const globalParser = globalParserRegistry.get(parserSpec);
|
|
48
|
+
if (globalParser) {
|
|
49
|
+
return globalParser;
|
|
73
50
|
}
|
|
74
51
|
|
|
75
|
-
// Not found in registry
|
|
52
|
+
// Not found in either registry
|
|
76
53
|
throw new Error(
|
|
77
|
-
`Parser "${parserSpec}" not found
|
|
78
|
-
`
|
|
54
|
+
`Parser "${parserSpec}" not found. ` +
|
|
55
|
+
`Checked ${synthesizerElement ? 'scoped registry and ' : ''}global registry.\n` +
|
|
56
|
+
`Ensure the parser is registered via:\n` +
|
|
57
|
+
`- <script type="emc-parser" src="..." parser-name="${parserSpec}">\n` +
|
|
58
|
+
`- registerParser(synthesizerElement, "${parserSpec}", parserFn)\n` +
|
|
59
|
+
`- globalParserRegistry.register("${parserSpec}", parserFn)`
|
|
79
60
|
);
|
|
80
61
|
}
|
|
81
62
|
|
|
@@ -97,8 +78,6 @@ function getCacheKey(config: AttrConfig<any>): string {
|
|
|
97
78
|
parserStr = 'builtin';
|
|
98
79
|
} else if (typeof config.parser === 'string') {
|
|
99
80
|
parserStr = `named:${config.parser}`;
|
|
100
|
-
} else if (Array.isArray(config.parser)) {
|
|
101
|
-
parserStr = `tuple:${config.parser[0]}.${config.parser[1]}`;
|
|
102
81
|
} else {
|
|
103
82
|
parserStr = 'custom';
|
|
104
83
|
}
|
|
@@ -111,16 +90,18 @@ function getCacheKey(config: AttrConfig<any>): string {
|
|
|
111
90
|
* @param attrValue - The attribute value to parse (or null)
|
|
112
91
|
* @param config - The attribute configuration
|
|
113
92
|
* @param parser - The parser function to use
|
|
93
|
+
* @param context - The parser context to pass to the parser
|
|
114
94
|
* @returns The parsed value
|
|
115
95
|
*/
|
|
116
96
|
function parseWithCache(
|
|
117
97
|
attrValue: string | null,
|
|
118
98
|
config: AttrConfig<any>,
|
|
119
|
-
parser:
|
|
99
|
+
parser: ParserFunction,
|
|
100
|
+
context: ParserContext
|
|
120
101
|
): any {
|
|
121
102
|
// Skip caching for Boolean (presence check doesn't benefit from caching)
|
|
122
103
|
if (config.instanceOf === 'Boolean') {
|
|
123
|
-
return parser
|
|
104
|
+
return callParser(parser, attrValue, context);
|
|
124
105
|
}
|
|
125
106
|
|
|
126
107
|
// Get or create cache for this config
|
|
@@ -145,7 +126,7 @@ function parseWithCache(
|
|
|
145
126
|
}
|
|
146
127
|
|
|
147
128
|
// Parse the value
|
|
148
|
-
const parsedValue = parser
|
|
129
|
+
const parsedValue = callParser(parser, attrValue, context);
|
|
149
130
|
|
|
150
131
|
// Store in cache
|
|
151
132
|
valueCache.set(valueCacheKey, parsedValue);
|
|
@@ -158,6 +139,29 @@ function parseWithCache(
|
|
|
158
139
|
return parsedValue;
|
|
159
140
|
}
|
|
160
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Calls a parser function with the appropriate signature
|
|
144
|
+
* Handles both simple (value-only) and advanced (value + context) parser signatures
|
|
145
|
+
* @param parser - The parser function
|
|
146
|
+
* @param attrValue - The attribute value
|
|
147
|
+
* @param context - The parser context
|
|
148
|
+
* @returns The parsed value
|
|
149
|
+
*/
|
|
150
|
+
function callParser(
|
|
151
|
+
parser: ParserFunction,
|
|
152
|
+
attrValue: string | null,
|
|
153
|
+
context: ParserContext
|
|
154
|
+
): any {
|
|
155
|
+
// Check parser arity (number of parameters)
|
|
156
|
+
// If parser accepts 2+ parameters, pass context
|
|
157
|
+
if (parser.length >= 2) {
|
|
158
|
+
return parser(attrValue, context);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Otherwise, call with just the value (simple form)
|
|
162
|
+
return parser(attrValue);
|
|
163
|
+
}
|
|
164
|
+
|
|
161
165
|
/**
|
|
162
166
|
* Checks if a string contains a dash or non-ASCII character
|
|
163
167
|
*/
|
|
@@ -210,16 +214,16 @@ function getAttributeValue(
|
|
|
210
214
|
/**
|
|
211
215
|
* Gets the default parser for a given instanceOf type
|
|
212
216
|
*/
|
|
213
|
-
function getDefaultParser(instanceOf?: string | Function):
|
|
217
|
+
function getDefaultParser(instanceOf?: string | Function): ParserFunction {
|
|
214
218
|
if (!instanceOf) {
|
|
215
|
-
return (v) => v; // Default to identity
|
|
219
|
+
return (v: string | null) => v; // Default to identity
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
const typeStr = typeof instanceOf === 'string' ? instanceOf : instanceOf.name;
|
|
219
223
|
|
|
220
224
|
switch (typeStr) {
|
|
221
225
|
case 'Object':
|
|
222
|
-
return (v) => {
|
|
226
|
+
return (v: string | null) => {
|
|
223
227
|
if (v === null || v === '') return null;
|
|
224
228
|
try {
|
|
225
229
|
return JSON.parse(v);
|
|
@@ -228,7 +232,7 @@ function getDefaultParser(instanceOf?: string | Function): (v: string | null) =>
|
|
|
228
232
|
}
|
|
229
233
|
};
|
|
230
234
|
case 'Array':
|
|
231
|
-
return (v) => {
|
|
235
|
+
return (v: string | null) => {
|
|
232
236
|
if (v === null || v === '') return null;
|
|
233
237
|
try {
|
|
234
238
|
return JSON.parse(v);
|
|
@@ -237,7 +241,7 @@ function getDefaultParser(instanceOf?: string | Function): (v: string | null) =>
|
|
|
237
241
|
}
|
|
238
242
|
};
|
|
239
243
|
case 'Number':
|
|
240
|
-
return (v) => {
|
|
244
|
+
return (v: string | null) => {
|
|
241
245
|
if (v === null || v === '') return null;
|
|
242
246
|
const num = Number(v);
|
|
243
247
|
if (isNaN(num)) {
|
|
@@ -246,10 +250,10 @@ function getDefaultParser(instanceOf?: string | Function): (v: string | null) =>
|
|
|
246
250
|
return num;
|
|
247
251
|
};
|
|
248
252
|
case 'Boolean':
|
|
249
|
-
return (v) => v !== null; // Presence check
|
|
253
|
+
return (v: string | null) => v !== null; // Presence check
|
|
250
254
|
case 'String':
|
|
251
255
|
default:
|
|
252
|
-
return (v) => v; // Identity
|
|
256
|
+
return (v: string | null) => v; // Identity
|
|
253
257
|
}
|
|
254
258
|
}
|
|
255
259
|
|
|
@@ -258,13 +262,18 @@ function getDefaultParser(instanceOf?: string | Function): (v: string | null) =>
|
|
|
258
262
|
* @param element - The DOM element to read attributes from
|
|
259
263
|
* @param attrPatterns - The attribute patterns configuration
|
|
260
264
|
* @param allowUnprefixed - Pattern (string or RegExp) that element tag name must match to allow unprefixed attributes
|
|
265
|
+
* @param spawnContext - Optional spawn context containing enhancement config and synthesizer element
|
|
261
266
|
* @returns Object with parsed attribute values ready for initVals
|
|
262
267
|
*/
|
|
263
268
|
export function parseWithAttrs<T = any>(
|
|
264
269
|
element: Element,
|
|
265
270
|
attrPatterns: AttrPatterns<T>,
|
|
266
|
-
allowUnprefixed?: string | RegExp
|
|
271
|
+
allowUnprefixed?: string | RegExp,
|
|
272
|
+
spawnContext?: SpawnContext<T>
|
|
267
273
|
): Partial<T> {
|
|
274
|
+
// Extract synthesizerElement from spawnContext for backward compatibility
|
|
275
|
+
const synthesizerElement = spawnContext?.synthesizerElement;
|
|
276
|
+
|
|
268
277
|
// Validate base attribute if present
|
|
269
278
|
if ('base' in attrPatterns) {
|
|
270
279
|
const baseValue = attrPatterns.base as string;
|
|
@@ -334,6 +343,14 @@ export function parseWithAttrs<T = any>(
|
|
|
334
343
|
for (const [key, { attrName, config }] of resolvedAttrs) {
|
|
335
344
|
const attrValue = getAttributeValue(element, attrName, allowUnprefixed);
|
|
336
345
|
|
|
346
|
+
// Create parser context
|
|
347
|
+
const parserContext: ParserContext<T> = {
|
|
348
|
+
attrConfig: config,
|
|
349
|
+
spawnContext,
|
|
350
|
+
element,
|
|
351
|
+
attrName
|
|
352
|
+
};
|
|
353
|
+
|
|
337
354
|
// Handle missing attribute
|
|
338
355
|
if (attrValue === null) {
|
|
339
356
|
// Use valIfNull if defined
|
|
@@ -355,8 +372,8 @@ export function parseWithAttrs<T = any>(
|
|
|
355
372
|
}
|
|
356
373
|
// For Boolean without valIfNull, fall through to parser
|
|
357
374
|
else {
|
|
358
|
-
const parser = resolveParser(config.parser) || getDefaultParser(config.instanceOf);
|
|
359
|
-
const parsedValue = parser
|
|
375
|
+
const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
|
|
376
|
+
const parsedValue = callParser(parser, attrValue, parserContext);
|
|
360
377
|
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
|
361
378
|
result[mapsTo as string] = parsedValue;
|
|
362
379
|
}
|
|
@@ -364,12 +381,12 @@ export function parseWithAttrs<T = any>(
|
|
|
364
381
|
}
|
|
365
382
|
|
|
366
383
|
// Attribute exists - parse normally
|
|
367
|
-
const parser = resolveParser(config.parser) || getDefaultParser(config.instanceOf);
|
|
384
|
+
const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
|
|
368
385
|
|
|
369
386
|
// Use cache if parseCache is specified
|
|
370
387
|
const parsedValue = config.parseCache
|
|
371
|
-
? parseWithCache(attrValue, config, parser)
|
|
372
|
-
: parser
|
|
388
|
+
? parseWithCache(attrValue, config, parser, parserContext)
|
|
389
|
+
: callParser(parser, attrValue, parserContext);
|
|
373
390
|
|
|
374
391
|
// Determine target property
|
|
375
392
|
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
package/parserRegistry.js
CHANGED
|
@@ -55,7 +55,7 @@ export const globalParserRegistry = new ParserRegistry();
|
|
|
55
55
|
// Register common built-in parsers
|
|
56
56
|
globalParserRegistry.register('timestamp', (v) => v ? new Date(v).getTime() : null);
|
|
57
57
|
globalParserRegistry.register('date', (v) => v ? new Date(v) : null);
|
|
58
|
-
globalParserRegistry.register('csv', (v) => v ? v.split(',').map(s => s.trim()) : []);
|
|
58
|
+
globalParserRegistry.register('csv', (v) => v ? v.split(',').map((s) => s.trim()) : []);
|
|
59
59
|
globalParserRegistry.register('int', (v) => v ? parseInt(v, 10) : null);
|
|
60
60
|
globalParserRegistry.register('float', (v) => v ? parseFloat(v) : null);
|
|
61
61
|
globalParserRegistry.register('boolean', (v) => v !== null);
|
|
@@ -69,3 +69,33 @@ globalParserRegistry.register('json', (v) => {
|
|
|
69
69
|
throw new Error(`Failed to parse JSON: "${v}". Error: ${e}`);
|
|
70
70
|
}
|
|
71
71
|
});
|
|
72
|
+
import { ScopedParserRegistry } from './ScopedParserRegistry.js';
|
|
73
|
+
/**
|
|
74
|
+
* Symbol for storing scoped parser registry on synthesizer elements
|
|
75
|
+
* Using Symbol.for ensures the same symbol is used across different versions of the package
|
|
76
|
+
*/
|
|
77
|
+
const SCOPED_REGISTRY_SYMBOL = Symbol.for('assign-gingerly.scopedParserRegistry');
|
|
78
|
+
/**
|
|
79
|
+
* Get the scoped parser registry for a synthesizer element
|
|
80
|
+
* Creates a new registry if one doesn't exist
|
|
81
|
+
* @param synthesizerElement - The synthesizer element (be-hive, htmx-container, alpine-scope, etc.)
|
|
82
|
+
* @returns The scoped parser registry for this element
|
|
83
|
+
*/
|
|
84
|
+
export function getParserRegistry(synthesizerElement) {
|
|
85
|
+
let registry = synthesizerElement[SCOPED_REGISTRY_SYMBOL];
|
|
86
|
+
if (!registry) {
|
|
87
|
+
registry = new ScopedParserRegistry();
|
|
88
|
+
synthesizerElement[SCOPED_REGISTRY_SYMBOL] = registry;
|
|
89
|
+
}
|
|
90
|
+
return registry;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Register a parser in a synthesizer element's scoped registry
|
|
94
|
+
* @param synthesizerElement - The synthesizer element to register the parser with
|
|
95
|
+
* @param name - Parser name
|
|
96
|
+
* @param parser - Parser function
|
|
97
|
+
*/
|
|
98
|
+
export function registerParser(synthesizerElement, name, parser) {
|
|
99
|
+
const registry = getParserRegistry(synthesizerElement);
|
|
100
|
+
registry.register(name, parser);
|
|
101
|
+
}
|
package/parserRegistry.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import { ParserFunction } from './types/assign-gingerly/types';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Registry for named parsers that can be referenced by string name
|
|
3
5
|
* Enables JSON serialization of configs with custom parsers
|
|
4
6
|
*/
|
|
5
7
|
export class ParserRegistry {
|
|
6
|
-
private parsers = new Map<string,
|
|
8
|
+
private parsers = new Map<string, ParserFunction>();
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Register a parser with a given name
|
|
10
12
|
* @param name - The name to register the parser under
|
|
11
13
|
* @param parser - The parser function
|
|
12
14
|
*/
|
|
13
|
-
register(name: string, parser:
|
|
15
|
+
register(name: string, parser: ParserFunction): void {
|
|
14
16
|
if (this.parsers.has(name)) {
|
|
15
17
|
console.warn(`Parser "${name}" already registered, overwriting`);
|
|
16
18
|
}
|
|
@@ -22,7 +24,7 @@ export class ParserRegistry {
|
|
|
22
24
|
* @param name - The name of the parser
|
|
23
25
|
* @returns The parser function or undefined if not found
|
|
24
26
|
*/
|
|
25
|
-
get(name: string):
|
|
27
|
+
get(name: string): ParserFunction | undefined {
|
|
26
28
|
return this.parsers.get(name);
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -60,31 +62,31 @@ export class ParserRegistry {
|
|
|
60
62
|
export const globalParserRegistry = new ParserRegistry();
|
|
61
63
|
|
|
62
64
|
// Register common built-in parsers
|
|
63
|
-
globalParserRegistry.register('timestamp', (v) =>
|
|
65
|
+
globalParserRegistry.register('timestamp', (v: string | null) =>
|
|
64
66
|
v ? new Date(v).getTime() : null
|
|
65
67
|
);
|
|
66
68
|
|
|
67
|
-
globalParserRegistry.register('date', (v) =>
|
|
69
|
+
globalParserRegistry.register('date', (v: string | null) =>
|
|
68
70
|
v ? new Date(v) : null
|
|
69
71
|
);
|
|
70
72
|
|
|
71
|
-
globalParserRegistry.register('csv', (v) =>
|
|
72
|
-
v ? v.split(',').map(s => s.trim()) : []
|
|
73
|
+
globalParserRegistry.register('csv', (v: string | null) =>
|
|
74
|
+
v ? v.split(',').map((s: string) => s.trim()) : []
|
|
73
75
|
);
|
|
74
76
|
|
|
75
|
-
globalParserRegistry.register('int', (v) =>
|
|
77
|
+
globalParserRegistry.register('int', (v: string | null) =>
|
|
76
78
|
v ? parseInt(v, 10) : null
|
|
77
79
|
);
|
|
78
80
|
|
|
79
|
-
globalParserRegistry.register('float', (v) =>
|
|
81
|
+
globalParserRegistry.register('float', (v: string | null) =>
|
|
80
82
|
v ? parseFloat(v) : null
|
|
81
83
|
);
|
|
82
84
|
|
|
83
|
-
globalParserRegistry.register('boolean', (v) =>
|
|
85
|
+
globalParserRegistry.register('boolean', (v: string | null) =>
|
|
84
86
|
v !== null
|
|
85
87
|
);
|
|
86
88
|
|
|
87
|
-
globalParserRegistry.register('json', (v) => {
|
|
89
|
+
globalParserRegistry.register('json', (v: string | null) => {
|
|
88
90
|
if (v === null || v === '') return null;
|
|
89
91
|
try {
|
|
90
92
|
return JSON.parse(v);
|
|
@@ -92,3 +94,43 @@ globalParserRegistry.register('json', (v) => {
|
|
|
92
94
|
throw new Error(`Failed to parse JSON: "${v}". Error: ${e}`);
|
|
93
95
|
}
|
|
94
96
|
});
|
|
97
|
+
|
|
98
|
+
import { ScopedParserRegistry } from './ScopedParserRegistry.js';
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Symbol for storing scoped parser registry on synthesizer elements
|
|
102
|
+
* Using Symbol.for ensures the same symbol is used across different versions of the package
|
|
103
|
+
*/
|
|
104
|
+
const SCOPED_REGISTRY_SYMBOL = Symbol.for('assign-gingerly.scopedParserRegistry');
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the scoped parser registry for a synthesizer element
|
|
108
|
+
* Creates a new registry if one doesn't exist
|
|
109
|
+
* @param synthesizerElement - The synthesizer element (be-hive, htmx-container, alpine-scope, etc.)
|
|
110
|
+
* @returns The scoped parser registry for this element
|
|
111
|
+
*/
|
|
112
|
+
export function getParserRegistry(synthesizerElement: Element): ScopedParserRegistry {
|
|
113
|
+
let registry = (synthesizerElement as any)[SCOPED_REGISTRY_SYMBOL];
|
|
114
|
+
|
|
115
|
+
if (!registry) {
|
|
116
|
+
registry = new ScopedParserRegistry();
|
|
117
|
+
(synthesizerElement as any)[SCOPED_REGISTRY_SYMBOL] = registry;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return registry;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Register a parser in a synthesizer element's scoped registry
|
|
125
|
+
* @param synthesizerElement - The synthesizer element to register the parser with
|
|
126
|
+
* @param name - Parser name
|
|
127
|
+
* @param parser - Parser function
|
|
128
|
+
*/
|
|
129
|
+
export function registerParser(
|
|
130
|
+
synthesizerElement: Element,
|
|
131
|
+
name: string,
|
|
132
|
+
parser: ParserFunction
|
|
133
|
+
): void {
|
|
134
|
+
const registry = getParserRegistry(synthesizerElement);
|
|
135
|
+
registry.register(name, parser);
|
|
136
|
+
}
|
|
@@ -74,6 +74,44 @@ export type pathString = `?.${string}`;
|
|
|
74
74
|
export type CustomElementName = string;
|
|
75
75
|
export type CustomElementConstructorStaticMethodName = string;
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Context passed to parser functions
|
|
79
|
+
* Provides access to configuration and spawn context for advanced parsing scenarios
|
|
80
|
+
*/
|
|
81
|
+
export interface ParserContext<T = any> {
|
|
82
|
+
/**
|
|
83
|
+
* The attribute configuration that matched this attribute
|
|
84
|
+
* Useful for parsers that need to access additional config properties
|
|
85
|
+
*/
|
|
86
|
+
attrConfig: AttrConfig<T>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The spawn context containing enhancement config and synthesizer element
|
|
90
|
+
* Useful for parsers that need access to the enhancement or synthesizer context
|
|
91
|
+
*/
|
|
92
|
+
spawnContext?: SpawnContext<T>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The element being enhanced
|
|
96
|
+
* Useful for parsers that need to read other attributes or element properties
|
|
97
|
+
*/
|
|
98
|
+
element: Element;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The attribute name that was matched (resolved from template)
|
|
102
|
+
* Useful for parsers that handle multiple attributes
|
|
103
|
+
*/
|
|
104
|
+
attrName: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parser function signature
|
|
109
|
+
* Can accept just the attribute value (simple form) or value + context (advanced form)
|
|
110
|
+
*/
|
|
111
|
+
export type ParserFunction<T = any> =
|
|
112
|
+
| ((attrValue: string | null) => any)
|
|
113
|
+
| ((attrValue: string | null, context?: ParserContext<T>) => any);
|
|
114
|
+
|
|
77
115
|
export interface AttrConfig<T = any> {
|
|
78
116
|
/**
|
|
79
117
|
* Type of the property value (JSON-serializable string format)
|
|
@@ -100,13 +138,19 @@ export interface AttrConfig<T = any> {
|
|
|
100
138
|
/**
|
|
101
139
|
* Parser to transform attribute string value
|
|
102
140
|
* - Function: Inline parser function (not JSON serializable)
|
|
103
|
-
*
|
|
104
|
-
*
|
|
141
|
+
* - Simple form: (attrValue: string | null) => any
|
|
142
|
+
* - Advanced form: (attrValue: string | null, context: ParserContext) => any
|
|
143
|
+
* - String: Named parser reference (JSON serializable) - looks up in scoped registry (if available) then global parser registry (e.g., 'timestamp', 'csv')
|
|
144
|
+
*
|
|
145
|
+
* Parser functions can optionally accept a second parameter (ParserContext) which provides:
|
|
146
|
+
* - attrConfig: The full AttrConfig object for this attribute
|
|
147
|
+
* - spawnContext: The SpawnContext with enhancement config and synthesizer element
|
|
148
|
+
* - element: The element being enhanced
|
|
149
|
+
* - attrName: The resolved attribute name
|
|
105
150
|
*/
|
|
106
151
|
parser?:
|
|
107
|
-
|
|
|
108
|
-
| string
|
|
109
|
-
| [CustomElementName, CustomElementConstructorStaticMethodName]
|
|
152
|
+
| ParserFunction<T>
|
|
153
|
+
| string
|
|
110
154
|
;
|
|
111
155
|
|
|
112
156
|
/**
|
|
@@ -156,6 +200,12 @@ export type AttrPatterns<T = any> = {
|
|
|
156
200
|
export interface SpawnContext<T = any, TMountContext = any> {
|
|
157
201
|
config: EnhancementConfig<T>;
|
|
158
202
|
mountCtx?: TMountContext;
|
|
203
|
+
/**
|
|
204
|
+
* Reference to the synthesizer element (be-hive, htmx-container, alpine-scope, etc.)
|
|
205
|
+
* that contains the EMC script defining this enhancement.
|
|
206
|
+
* Used for scoped parser registry access during attribute parsing.
|
|
207
|
+
*/
|
|
208
|
+
synthesizerElement?: Element;
|
|
159
209
|
}
|
|
160
210
|
|
|
161
211
|
/**
|