assign-gingerly 0.0.84 → 0.0.85
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 +12 -1179
- package/ScopedParserRegistry.js +2 -2
- package/ScopedParserRegistry.ts +6 -6
- package/SplitParser.js +48 -0
- package/SplitParser.ts +89 -0
- package/inferencer/types/NewHTMLFirstCustomElement.md +66 -1
- package/package.json +5 -1
- package/parseWithAttrs.js +109 -25
- package/parseWithAttrs.ts +135 -31
- package/parserRegistry.js +5 -4
- package/parserRegistry.ts +10 -12
- package/types/assign-gingerly/types.d.ts +43 -5
package/parseWithAttrs.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AttrPatterns, AttrConfig, ParserFunction, ParserContext, SpawnContext } from './types/assign-gingerly/types';
|
|
1
|
+
import { AttrPatterns, AttrConfig, ParserFunction, ParserContext, SpawnContext, ParserSpec, AttrParserConstructor, NamedParserRef } from './types/assign-gingerly/types';
|
|
2
2
|
import { globalParserRegistry, getParserRegistry } from './parserRegistry.js';
|
|
3
3
|
import { resolveTemplate } from './resolve/resolveTemplate.js';
|
|
4
4
|
|
|
@@ -6,21 +6,79 @@ import { resolveTemplate } from './resolve/resolveTemplate.js';
|
|
|
6
6
|
// Structure: Map<configKey, Map<attrValue, parsedValue>>
|
|
7
7
|
const parseCache = new Map<string, Map<string, any>>();
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Detects whether a value is a class-based parser constructor.
|
|
11
|
+
* Class parsers must have a `parse` method on their prototype.
|
|
12
|
+
*/
|
|
13
|
+
function isAttrParserConstructor(
|
|
14
|
+
value: ParserFunction | AttrParserConstructor | undefined
|
|
15
|
+
): value is AttrParserConstructor {
|
|
16
|
+
return typeof value === 'function' && !!value.prototype && 'parse' in value.prototype;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Instantiates a class parser and returns a wrapper function that calls its parse method.
|
|
21
|
+
* A new instance is created for each attribute parse (no cross-call caching).
|
|
22
|
+
*/
|
|
23
|
+
function instantiateClassParser(
|
|
24
|
+
ParserCtor: AttrParserConstructor,
|
|
25
|
+
options?: any
|
|
26
|
+
): ParserFunction {
|
|
27
|
+
const instance = new ParserCtor(options);
|
|
28
|
+
return (attrValue, context) => instance.parse(attrValue, context);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Looks up a named parser in the scoped (if available) then global registry.
|
|
33
|
+
*/
|
|
34
|
+
function lookupNamedParser(
|
|
35
|
+
name: string,
|
|
36
|
+
synthesizerElement?: Element
|
|
37
|
+
): ParserFunction | AttrParserConstructor {
|
|
38
|
+
// Check scoped registry first (if synthesizerElement provided)
|
|
39
|
+
if (synthesizerElement) {
|
|
40
|
+
const scopedRegistry = getParserRegistry(synthesizerElement);
|
|
41
|
+
const scopedParser = scopedRegistry.get(name);
|
|
42
|
+
if (scopedParser) {
|
|
43
|
+
return scopedParser;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fallback to global registry
|
|
48
|
+
const globalParser = globalParserRegistry.get(name);
|
|
49
|
+
if (globalParser) {
|
|
50
|
+
return globalParser;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Not found in either registry
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Parser "${name}" not found. ` +
|
|
56
|
+
`Checked ${synthesizerElement ? 'scoped registry and ' : ''}global registry.\n` +
|
|
57
|
+
`Ensure the parser is registered via:\n` +
|
|
58
|
+
`- <script type="emc-parser" src="..." parser-name="${name}">\n` +
|
|
59
|
+
`- registerParser(synthesizerElement, "${name}", parserFn)\n` +
|
|
60
|
+
`- globalParserRegistry.register("${name}", parserFn)`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
9
64
|
/**
|
|
10
65
|
* Resolves a parser specification to an actual parser function
|
|
11
66
|
* Supports:
|
|
12
67
|
* - Inline functions (direct use)
|
|
13
|
-
* - Named
|
|
14
|
-
* - Named parsers from
|
|
68
|
+
* - Named parser functions or class constructors from scoped/global registry (string form)
|
|
69
|
+
* - Named class parsers with options from the object form { name, options }
|
|
70
|
+
* - Custom element static methods via tuple form [elementName, methodName]
|
|
15
71
|
*
|
|
16
|
-
* @param parserSpec - Parser function or
|
|
72
|
+
* @param parserSpec - Parser function, string reference, tuple, or object reference
|
|
17
73
|
* @param synthesizerElement - Optional synthesizer element for scoped parser lookup
|
|
74
|
+
* @param parserOptions - Optional constructor options when a string resolves to a class parser
|
|
18
75
|
* @returns The resolved parser function
|
|
19
76
|
* @throws Error if parser cannot be resolved
|
|
20
77
|
*/
|
|
21
78
|
function resolveParser(
|
|
22
|
-
parserSpec:
|
|
23
|
-
synthesizerElement?: Element
|
|
79
|
+
parserSpec: ParserSpec | undefined,
|
|
80
|
+
synthesizerElement?: Element,
|
|
81
|
+
parserOptions?: any
|
|
24
82
|
): ParserFunction | undefined {
|
|
25
83
|
// Undefined - no parser specified
|
|
26
84
|
if (parserSpec === undefined) {
|
|
@@ -32,37 +90,71 @@ function resolveParser(
|
|
|
32
90
|
return parserSpec;
|
|
33
91
|
}
|
|
34
92
|
|
|
93
|
+
// Object form: { name, options }
|
|
94
|
+
if (
|
|
95
|
+
parserSpec !== null &&
|
|
96
|
+
typeof parserSpec === 'object' &&
|
|
97
|
+
!Array.isArray(parserSpec) &&
|
|
98
|
+
'name' in parserSpec
|
|
99
|
+
) {
|
|
100
|
+
const { name, options } = parserSpec as NamedParserRef;
|
|
101
|
+
const registered = lookupNamedParser(name, synthesizerElement);
|
|
102
|
+
if (isAttrParserConstructor(registered)) {
|
|
103
|
+
return instantiateClassParser(registered, options);
|
|
104
|
+
}
|
|
105
|
+
return registered;
|
|
106
|
+
}
|
|
107
|
+
|
|
35
108
|
// String reference - resolve from scoped or global registry
|
|
36
109
|
if (typeof parserSpec === 'string') {
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
const scopedParser = scopedRegistry.get(parserSpec);
|
|
41
|
-
if (scopedParser) {
|
|
42
|
-
return scopedParser;
|
|
43
|
-
}
|
|
110
|
+
const registered = lookupNamedParser(parserSpec, synthesizerElement);
|
|
111
|
+
if (isAttrParserConstructor(registered)) {
|
|
112
|
+
return instantiateClassParser(registered, parserOptions);
|
|
44
113
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
114
|
+
return registered;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Tuple reference: [elementName, methodName]
|
|
118
|
+
if (Array.isArray(parserSpec) && parserSpec.length === 2) {
|
|
119
|
+
const [elementName, methodName] = parserSpec;
|
|
120
|
+
const Ctr = customElements.get(elementName);
|
|
121
|
+
if (!Ctr) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Cannot resolve parser [${elementName}, ${methodName}]: custom element "${elementName}" not found`
|
|
124
|
+
);
|
|
50
125
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
`- registerParser(synthesizerElement, "${parserSpec}", parserFn)\n` +
|
|
59
|
-
`- globalParserRegistry.register("${parserSpec}", parserFn)`
|
|
60
|
-
);
|
|
126
|
+
const method = (Ctr as any)[methodName];
|
|
127
|
+
if (typeof method !== 'function') {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Cannot resolve parser [${elementName}, ${methodName}]: static method "${methodName}" not found on custom element "${elementName}"`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return method.bind(Ctr);
|
|
61
133
|
}
|
|
62
134
|
|
|
63
135
|
return undefined;
|
|
64
136
|
}
|
|
65
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Serializes parser options for use in cache keys.
|
|
140
|
+
* RegExp values are normalized to their string representation.
|
|
141
|
+
*/
|
|
142
|
+
function serializeParserOptions(options: any): string {
|
|
143
|
+
if (options === undefined || options === null) {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
return JSON.stringify(options, (_key, value) => {
|
|
148
|
+
if (value instanceof RegExp) {
|
|
149
|
+
return value.toString();
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
});
|
|
153
|
+
} catch (_e) {
|
|
154
|
+
return String(options);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
66
158
|
/**
|
|
67
159
|
* Creates a cache key from an AttrConfig
|
|
68
160
|
* Includes instanceOf and parser identifier to ensure correct cache hits
|
|
@@ -77,7 +169,19 @@ function getCacheKey(config: AttrConfig<any>): string {
|
|
|
77
169
|
if (config.parser === undefined) {
|
|
78
170
|
parserStr = 'builtin';
|
|
79
171
|
} else if (typeof config.parser === 'string') {
|
|
80
|
-
|
|
172
|
+
const optionsStr = serializeParserOptions(config.parserOptions);
|
|
173
|
+
parserStr = `named:${config.parser}|options:${optionsStr}`;
|
|
174
|
+
} else if (
|
|
175
|
+
config.parser !== null &&
|
|
176
|
+
typeof config.parser === 'object' &&
|
|
177
|
+
!Array.isArray(config.parser) &&
|
|
178
|
+
'name' in config.parser
|
|
179
|
+
) {
|
|
180
|
+
const ref = config.parser as NamedParserRef;
|
|
181
|
+
const optionsStr = serializeParserOptions(ref.options);
|
|
182
|
+
parserStr = `named:${ref.name}|options:${optionsStr}`;
|
|
183
|
+
} else if (Array.isArray(config.parser)) {
|
|
184
|
+
parserStr = `tuple:${config.parser[0]},${config.parser[1]}`;
|
|
81
185
|
} else {
|
|
82
186
|
parserStr = 'custom';
|
|
83
187
|
}
|
|
@@ -390,7 +494,7 @@ export function parseWithAttrs<T = any>(
|
|
|
390
494
|
}
|
|
391
495
|
// For Boolean without valIfNull, fall through to parser
|
|
392
496
|
else {
|
|
393
|
-
const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
|
|
497
|
+
const parser = resolveParser(config.parser, synthesizerElement, config.parserOptions) || getDefaultParser(config.instanceOf);
|
|
394
498
|
const parsedValue = callParser(parser, attrValue, parserContext);
|
|
395
499
|
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
|
396
500
|
result[mapsTo as string] = parsedValue;
|
|
@@ -399,7 +503,7 @@ export function parseWithAttrs<T = any>(
|
|
|
399
503
|
}
|
|
400
504
|
|
|
401
505
|
// Attribute exists - parse normally
|
|
402
|
-
const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
|
|
506
|
+
const parser = resolveParser(config.parser, synthesizerElement, config.parserOptions) || getDefaultParser(config.instanceOf);
|
|
403
507
|
|
|
404
508
|
// Use cache if parseCache is specified
|
|
405
509
|
const parsedValue = config.parseCache
|
package/parserRegistry.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SplitParser } from './SplitParser.js';
|
|
2
|
+
import { ScopedParserRegistry } from './ScopedParserRegistry.js';
|
|
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
|
|
@@ -7,7 +9,7 @@ export class ParserRegistry {
|
|
|
7
9
|
/**
|
|
8
10
|
* Register a parser with a given name
|
|
9
11
|
* @param name - The name to register the parser under
|
|
10
|
-
* @param parser - The parser function
|
|
12
|
+
* @param parser - The parser function or class constructor
|
|
11
13
|
*/
|
|
12
14
|
register(name, parser) {
|
|
13
15
|
if (this.parsers.has(name)) {
|
|
@@ -18,7 +20,7 @@ export class ParserRegistry {
|
|
|
18
20
|
/**
|
|
19
21
|
* Get a parser by name
|
|
20
22
|
* @param name - The name of the parser
|
|
21
|
-
* @returns The parser function or undefined if not found
|
|
23
|
+
* @returns The parser function, class constructor, or undefined if not found
|
|
22
24
|
*/
|
|
23
25
|
get(name) {
|
|
24
26
|
return this.parsers.get(name);
|
|
@@ -55,7 +57,6 @@ export const globalParserRegistry = new ParserRegistry();
|
|
|
55
57
|
// Register common built-in parsers
|
|
56
58
|
globalParserRegistry.register('timestamp', (v) => v ? new Date(v).getTime() : null);
|
|
57
59
|
globalParserRegistry.register('date', (v) => v ? new Date(v) : null);
|
|
58
|
-
globalParserRegistry.register('csv', (v) => v ? v.split(',').map((s) => s.trim()) : []);
|
|
59
60
|
globalParserRegistry.register('int', (v) => v ? parseInt(v, 10) : null);
|
|
60
61
|
globalParserRegistry.register('float', (v) => v ? parseFloat(v) : null);
|
|
61
62
|
globalParserRegistry.register('boolean', (v) => v !== null);
|
|
@@ -69,7 +70,7 @@ globalParserRegistry.register('json', (v) => {
|
|
|
69
70
|
throw new Error(`Failed to parse JSON: "${v}". Error: ${e}`);
|
|
70
71
|
}
|
|
71
72
|
});
|
|
72
|
-
|
|
73
|
+
globalParserRegistry.register('splitter', SplitParser);
|
|
73
74
|
/**
|
|
74
75
|
* Symbol for storing scoped parser registry on synthesizer elements
|
|
75
76
|
* Using Symbol.for ensures the same symbol is used across different versions of the package
|
package/parserRegistry.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import { ParserFunction } from './types/assign-gingerly/types';
|
|
1
|
+
import { ParserFunction, AttrParserConstructor } from './types/assign-gingerly/types';
|
|
2
|
+
import { SplitParser } from './SplitParser.js';
|
|
3
|
+
import { ScopedParserRegistry } from './ScopedParserRegistry.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Registry for named parsers that can be referenced by string name
|
|
5
7
|
* Enables JSON serialization of configs with custom parsers
|
|
6
8
|
*/
|
|
7
9
|
export class ParserRegistry {
|
|
8
|
-
private parsers = new Map<string, ParserFunction>();
|
|
10
|
+
private parsers = new Map<string, ParserFunction | AttrParserConstructor>();
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Register a parser with a given name
|
|
12
14
|
* @param name - The name to register the parser under
|
|
13
|
-
* @param parser - The parser function
|
|
15
|
+
* @param parser - The parser function or class constructor
|
|
14
16
|
*/
|
|
15
|
-
register(name: string, parser: ParserFunction): void {
|
|
17
|
+
register(name: string, parser: ParserFunction | AttrParserConstructor): void {
|
|
16
18
|
if (this.parsers.has(name)) {
|
|
17
19
|
console.warn(`Parser "${name}" already registered, overwriting`);
|
|
18
20
|
}
|
|
@@ -22,9 +24,9 @@ export class ParserRegistry {
|
|
|
22
24
|
/**
|
|
23
25
|
* Get a parser by name
|
|
24
26
|
* @param name - The name of the parser
|
|
25
|
-
* @returns The parser function or undefined if not found
|
|
27
|
+
* @returns The parser function, class constructor, or undefined if not found
|
|
26
28
|
*/
|
|
27
|
-
get(name: string): ParserFunction | undefined {
|
|
29
|
+
get(name: string): ParserFunction | AttrParserConstructor | undefined {
|
|
28
30
|
return this.parsers.get(name);
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -70,10 +72,6 @@ globalParserRegistry.register('date', (v: string | null) =>
|
|
|
70
72
|
v ? new Date(v) : null
|
|
71
73
|
);
|
|
72
74
|
|
|
73
|
-
globalParserRegistry.register('csv', (v: string | null) =>
|
|
74
|
-
v ? v.split(',').map((s: string) => s.trim()) : []
|
|
75
|
-
);
|
|
76
|
-
|
|
77
75
|
globalParserRegistry.register('int', (v: string | null) =>
|
|
78
76
|
v ? parseInt(v, 10) : null
|
|
79
77
|
);
|
|
@@ -95,7 +93,7 @@ globalParserRegistry.register('json', (v: string | null) => {
|
|
|
95
93
|
}
|
|
96
94
|
});
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
globalParserRegistry.register('splitter', SplitParser);
|
|
99
97
|
|
|
100
98
|
/**
|
|
101
99
|
* Symbol for storing scoped parser registry on synthesizer elements
|
|
@@ -129,7 +127,7 @@ export function getParserRegistry(synthesizerElement: Element): ScopedParserRegi
|
|
|
129
127
|
export function registerParser(
|
|
130
128
|
synthesizerElement: Element,
|
|
131
129
|
name: string,
|
|
132
|
-
parser: ParserFunction
|
|
130
|
+
parser: ParserFunction | AttrParserConstructor
|
|
133
131
|
): void {
|
|
134
132
|
const registry = getParserRegistry(synthesizerElement);
|
|
135
133
|
registry.register(name, parser);
|
|
@@ -112,6 +112,36 @@ export interface ParserContext<T = any> {
|
|
|
112
112
|
attrName: string;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Tuple reference for custom element static method parsers
|
|
117
|
+
* [elementName, methodName]
|
|
118
|
+
*/
|
|
119
|
+
export type ParserTuple = [CustomElementName, CustomElementConstructorStaticMethodName];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Class-based parser interface
|
|
123
|
+
* Classes registered as named parsers are instantiated per attribute parse
|
|
124
|
+
* and their parse method is called with the attribute value and context
|
|
125
|
+
*/
|
|
126
|
+
export interface AttrParser<T = any> {
|
|
127
|
+
parse(attrValue: string | null, context?: ParserContext<T>): any;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Constructor signature for class-based parsers
|
|
132
|
+
*/
|
|
133
|
+
export type AttrParserConstructor<T = any> = {
|
|
134
|
+
new (options?: any): AttrParser<T>;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Object form for referencing a registered named parser with constructor options
|
|
139
|
+
*/
|
|
140
|
+
export interface NamedParserRef {
|
|
141
|
+
name: string;
|
|
142
|
+
options?: any;
|
|
143
|
+
}
|
|
144
|
+
|
|
115
145
|
/**
|
|
116
146
|
* Parser function signature
|
|
117
147
|
* Can accept just the attribute value (simple form) or value + context (advanced form)
|
|
@@ -120,6 +150,15 @@ export type ParserFunction<T = any> =
|
|
|
120
150
|
| ((attrValue: string | null) => any)
|
|
121
151
|
| ((attrValue: string | null, context?: ParserContext<T>) => any);
|
|
122
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Any valid parser specification for AttrConfig.parser
|
|
155
|
+
*/
|
|
156
|
+
export type ParserSpec<T = any> =
|
|
157
|
+
| ParserFunction<T>
|
|
158
|
+
| string
|
|
159
|
+
| ParserTuple
|
|
160
|
+
| NamedParserRef;
|
|
161
|
+
|
|
123
162
|
export interface AttrConfig<T = unknown, TParserConfig = unknown> {
|
|
124
163
|
/**
|
|
125
164
|
* Type of the property value (JSON-serializable string format)
|
|
@@ -148,7 +187,9 @@ export interface AttrConfig<T = unknown, TParserConfig = unknown> {
|
|
|
148
187
|
* - Function: Inline parser function (not JSON serializable)
|
|
149
188
|
* - Simple form: (attrValue: string | null) => any
|
|
150
189
|
* - Advanced form: (attrValue: string | null, context: ParserContext) => any
|
|
151
|
-
* - String: Named parser reference (JSON serializable) - looks up in scoped registry (if available) then global parser registry (e.g., 'timestamp', '
|
|
190
|
+
* - String: Named parser reference (JSON serializable) - looks up in scoped registry (if available) then global parser registry (e.g., 'timestamp', 'splitter')
|
|
191
|
+
* - Tuple: [CustomElementName, StaticMethodName] - looks up a static method on a custom element constructor
|
|
192
|
+
* - Object: { name: string; options?: any } - looks up a registered class parser and instantiates it with the given options
|
|
152
193
|
*
|
|
153
194
|
* Parser functions can optionally accept a second parameter (ParserContext) which provides:
|
|
154
195
|
* - attrConfig: The full AttrConfig object for this attribute
|
|
@@ -156,10 +197,7 @@ export interface AttrConfig<T = unknown, TParserConfig = unknown> {
|
|
|
156
197
|
* - element: The element being enhanced
|
|
157
198
|
* - attrName: The resolved attribute name
|
|
158
199
|
*/
|
|
159
|
-
parser?:
|
|
160
|
-
| ParserFunction<T>
|
|
161
|
-
| string
|
|
162
|
-
;
|
|
200
|
+
parser?: ParserSpec<T>;
|
|
163
201
|
|
|
164
202
|
/**
|
|
165
203
|
* configuration information needed by a custom parser to properly
|