assign-gingerly 0.0.84 → 0.0.86
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/DX/emojis.js +5 -3
- package/DX/emojis.ts +5 -3
- package/DX/paths.js +2 -2
- package/DX/paths.ts +23 -23
- package/README.md +46 -1218
- package/ScopedParserRegistry.js +2 -2
- package/ScopedParserRegistry.ts +6 -6
- package/SplitParser.js +48 -0
- package/SplitParser.ts +89 -0
- package/assignFrom.js +78 -2
- package/assignFrom.ts +80 -2
- package/inferencer/types/NewCustomElementFeature.md +3 -1
- package/inferencer/types/NewHTMLFirstCustomElement.md +68 -1
- package/inferencer/types/assign-gingerly/types.d.ts +43 -5
- package/package.json +13 -4
- package/parseWithAttrs.js +109 -25
- package/parseWithAttrs.ts +135 -31
- package/parserRegistry.js +5 -4
- package/parserRegistry.ts +10 -12
- package/processHandlerCommands.js +5 -55
- package/processHandlerCommands.ts +6 -51
- package/{handlers → syncOps}/join.js +61 -73
- package/syncOps/join.ts +62 -0
- package/syncOps/registry.js +15 -0
- package/syncOps/registry.ts +19 -0
- package/types/assign-gingerly/types.d.ts +43 -5
- package/utils/resolveLhsPath.js +68 -0
- package/utils/resolveLhsPath.ts +81 -0
- package/handlers/join.ts +0 -79
package/parseWithAttrs.js
CHANGED
|
@@ -3,19 +3,61 @@ import { resolveTemplate } from './resolve/resolveTemplate.js';
|
|
|
3
3
|
// Module-level cache for parsed attribute values
|
|
4
4
|
// Structure: Map<configKey, Map<attrValue, parsedValue>>
|
|
5
5
|
const parseCache = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Detects whether a value is a class-based parser constructor.
|
|
8
|
+
* Class parsers must have a `parse` method on their prototype.
|
|
9
|
+
*/
|
|
10
|
+
function isAttrParserConstructor(value) {
|
|
11
|
+
return typeof value === 'function' && !!value.prototype && 'parse' in value.prototype;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Instantiates a class parser and returns a wrapper function that calls its parse method.
|
|
15
|
+
* A new instance is created for each attribute parse (no cross-call caching).
|
|
16
|
+
*/
|
|
17
|
+
function instantiateClassParser(ParserCtor, options) {
|
|
18
|
+
const instance = new ParserCtor(options);
|
|
19
|
+
return (attrValue, context) => instance.parse(attrValue, context);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Looks up a named parser in the scoped (if available) then global registry.
|
|
23
|
+
*/
|
|
24
|
+
function lookupNamedParser(name, synthesizerElement) {
|
|
25
|
+
// Check scoped registry first (if synthesizerElement provided)
|
|
26
|
+
if (synthesizerElement) {
|
|
27
|
+
const scopedRegistry = getParserRegistry(synthesizerElement);
|
|
28
|
+
const scopedParser = scopedRegistry.get(name);
|
|
29
|
+
if (scopedParser) {
|
|
30
|
+
return scopedParser;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Fallback to global registry
|
|
34
|
+
const globalParser = globalParserRegistry.get(name);
|
|
35
|
+
if (globalParser) {
|
|
36
|
+
return globalParser;
|
|
37
|
+
}
|
|
38
|
+
// Not found in either registry
|
|
39
|
+
throw new Error(`Parser "${name}" not found. ` +
|
|
40
|
+
`Checked ${synthesizerElement ? 'scoped registry and ' : ''}global registry.\n` +
|
|
41
|
+
`Ensure the parser is registered via:\n` +
|
|
42
|
+
`- <script type="emc-parser" src="..." parser-name="${name}">\n` +
|
|
43
|
+
`- registerParser(synthesizerElement, "${name}", parserFn)\n` +
|
|
44
|
+
`- globalParserRegistry.register("${name}", parserFn)`);
|
|
45
|
+
}
|
|
6
46
|
/**
|
|
7
47
|
* Resolves a parser specification to an actual parser function
|
|
8
48
|
* Supports:
|
|
9
49
|
* - Inline functions (direct use)
|
|
10
|
-
* - Named
|
|
11
|
-
* - Named parsers from
|
|
50
|
+
* - Named parser functions or class constructors from scoped/global registry (string form)
|
|
51
|
+
* - Named class parsers with options from the object form { name, options }
|
|
52
|
+
* - Custom element static methods via tuple form [elementName, methodName]
|
|
12
53
|
*
|
|
13
|
-
* @param parserSpec - Parser function or
|
|
54
|
+
* @param parserSpec - Parser function, string reference, tuple, or object reference
|
|
14
55
|
* @param synthesizerElement - Optional synthesizer element for scoped parser lookup
|
|
56
|
+
* @param parserOptions - Optional constructor options when a string resolves to a class parser
|
|
15
57
|
* @returns The resolved parser function
|
|
16
58
|
* @throws Error if parser cannot be resolved
|
|
17
59
|
*/
|
|
18
|
-
function resolveParser(parserSpec, synthesizerElement) {
|
|
60
|
+
function resolveParser(parserSpec, synthesizerElement, parserOptions) {
|
|
19
61
|
// Undefined - no parser specified
|
|
20
62
|
if (parserSpec === undefined) {
|
|
21
63
|
return undefined;
|
|
@@ -24,31 +66,61 @@ function resolveParser(parserSpec, synthesizerElement) {
|
|
|
24
66
|
if (typeof parserSpec === 'function') {
|
|
25
67
|
return parserSpec;
|
|
26
68
|
}
|
|
69
|
+
// Object form: { name, options }
|
|
70
|
+
if (parserSpec !== null &&
|
|
71
|
+
typeof parserSpec === 'object' &&
|
|
72
|
+
!Array.isArray(parserSpec) &&
|
|
73
|
+
'name' in parserSpec) {
|
|
74
|
+
const { name, options } = parserSpec;
|
|
75
|
+
const registered = lookupNamedParser(name, synthesizerElement);
|
|
76
|
+
if (isAttrParserConstructor(registered)) {
|
|
77
|
+
return instantiateClassParser(registered, options);
|
|
78
|
+
}
|
|
79
|
+
return registered;
|
|
80
|
+
}
|
|
27
81
|
// String reference - resolve from scoped or global registry
|
|
28
82
|
if (typeof parserSpec === 'string') {
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
83
|
+
const registered = lookupNamedParser(parserSpec, synthesizerElement);
|
|
84
|
+
if (isAttrParserConstructor(registered)) {
|
|
85
|
+
return instantiateClassParser(registered, parserOptions);
|
|
86
|
+
}
|
|
87
|
+
return registered;
|
|
88
|
+
}
|
|
89
|
+
// Tuple reference: [elementName, methodName]
|
|
90
|
+
if (Array.isArray(parserSpec) && parserSpec.length === 2) {
|
|
91
|
+
const [elementName, methodName] = parserSpec;
|
|
92
|
+
const Ctr = customElements.get(elementName);
|
|
93
|
+
if (!Ctr) {
|
|
94
|
+
throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: custom element "${elementName}" not found`);
|
|
36
95
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return globalParser;
|
|
96
|
+
const method = Ctr[methodName];
|
|
97
|
+
if (typeof method !== 'function') {
|
|
98
|
+
throw new Error(`Cannot resolve parser [${elementName}, ${methodName}]: static method "${methodName}" not found on custom element "${elementName}"`);
|
|
41
99
|
}
|
|
42
|
-
|
|
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)`);
|
|
100
|
+
return method.bind(Ctr);
|
|
49
101
|
}
|
|
50
102
|
return undefined;
|
|
51
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Serializes parser options for use in cache keys.
|
|
106
|
+
* RegExp values are normalized to their string representation.
|
|
107
|
+
*/
|
|
108
|
+
function serializeParserOptions(options) {
|
|
109
|
+
if (options === undefined || options === null) {
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
return JSON.stringify(options, (_key, value) => {
|
|
114
|
+
if (value instanceof RegExp) {
|
|
115
|
+
return value.toString();
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (_e) {
|
|
121
|
+
return String(options);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
52
124
|
/**
|
|
53
125
|
* Creates a cache key from an AttrConfig
|
|
54
126
|
* Includes instanceOf and parser identifier to ensure correct cache hits
|
|
@@ -63,7 +135,19 @@ function getCacheKey(config) {
|
|
|
63
135
|
parserStr = 'builtin';
|
|
64
136
|
}
|
|
65
137
|
else if (typeof config.parser === 'string') {
|
|
66
|
-
|
|
138
|
+
const optionsStr = serializeParserOptions(config.parserOptions);
|
|
139
|
+
parserStr = `named:${config.parser}|options:${optionsStr}`;
|
|
140
|
+
}
|
|
141
|
+
else if (config.parser !== null &&
|
|
142
|
+
typeof config.parser === 'object' &&
|
|
143
|
+
!Array.isArray(config.parser) &&
|
|
144
|
+
'name' in config.parser) {
|
|
145
|
+
const ref = config.parser;
|
|
146
|
+
const optionsStr = serializeParserOptions(ref.options);
|
|
147
|
+
parserStr = `named:${ref.name}|options:${optionsStr}`;
|
|
148
|
+
}
|
|
149
|
+
else if (Array.isArray(config.parser)) {
|
|
150
|
+
parserStr = `tuple:${config.parser[0]},${config.parser[1]}`;
|
|
67
151
|
}
|
|
68
152
|
else {
|
|
69
153
|
parserStr = 'custom';
|
|
@@ -328,7 +412,7 @@ export function parseWithAttrs(element, attrPatterns, allowUnprefixed, spawnCont
|
|
|
328
412
|
}
|
|
329
413
|
// For Boolean without valIfNull, fall through to parser
|
|
330
414
|
else {
|
|
331
|
-
const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
|
|
415
|
+
const parser = resolveParser(config.parser, synthesizerElement, config.parserOptions) || getDefaultParser(config.instanceOf);
|
|
332
416
|
const parsedValue = callParser(parser, attrValue, parserContext);
|
|
333
417
|
const mapsTo = config.mapsTo ?? (key === 'base' ? '.' : key);
|
|
334
418
|
result[mapsTo] = parsedValue;
|
|
@@ -336,7 +420,7 @@ export function parseWithAttrs(element, attrPatterns, allowUnprefixed, spawnCont
|
|
|
336
420
|
continue;
|
|
337
421
|
}
|
|
338
422
|
// Attribute exists - parse normally
|
|
339
|
-
const parser = resolveParser(config.parser, synthesizerElement) || getDefaultParser(config.instanceOf);
|
|
423
|
+
const parser = resolveParser(config.parser, synthesizerElement, config.parserOptions) || getDefaultParser(config.instanceOf);
|
|
340
424
|
// Use cache if parseCache is specified
|
|
341
425
|
const parsedValue = config.parseCache
|
|
342
426
|
? parseWithCache(attrValue, config, parser, parserContext)
|
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);
|
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { resolveValues } from './resolve/resolveValues.js';
|
|
7
7
|
import { getValues } from './resolve/getValues.js';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveLhsPath } from './utils/resolveLhsPath.js';
|
|
9
9
|
import { findClassPrototypeInPath } from './utils/findClassPrototypeInPath.js';
|
|
10
10
|
/**
|
|
11
11
|
* Map of built-in handler names to their module paths.
|
|
12
12
|
* These are auto-loaded on demand — no explicit import required.
|
|
13
|
+
*
|
|
14
|
+
* `join` moved to a synchronous ` =&` op (see syncOps/join.ts) — it never had
|
|
15
|
+
* side effects or anything to await, so it didn't belong behind this async pipeline.
|
|
13
16
|
*/
|
|
14
17
|
const BUILT_IN_MAP = {
|
|
15
18
|
'builtIns.lazyLoad': './handlers/lazyLoad.js',
|
|
16
19
|
'builtIns.lazyLoadSwitch': './handlers/lazyLoadSwitch.js',
|
|
17
|
-
'builtIns.join': './handlers/join.js',
|
|
18
20
|
'builtIns.microDataJoin': './handlers/microDataJoin.js',
|
|
19
21
|
'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
|
|
20
22
|
'builtIns.rangeSelector': './handlers/rangeSelector.js',
|
|
@@ -98,59 +100,7 @@ export async function processHandlerCommands(target, handlerKeys, pattern, optio
|
|
|
98
100
|
continue;
|
|
99
101
|
// Resolve the LHS path, preserving parent + key for return-value assignment.
|
|
100
102
|
// lhsParent[lhsKey] === lhsTarget (the current value at the path)
|
|
101
|
-
|
|
102
|
-
let lhsParent = undefined;
|
|
103
|
-
let lhsKey = undefined;
|
|
104
|
-
if (lhsPath.startsWith('?.')) {
|
|
105
|
-
const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
|
|
106
|
-
const withMethodsSet = options.withMethods
|
|
107
|
-
? options.withMethods instanceof Set
|
|
108
|
-
? options.withMethods
|
|
109
|
-
: new Set(options.withMethods)
|
|
110
|
-
: undefined;
|
|
111
|
-
if (withMethodsSet && pathParts.length > 0) {
|
|
112
|
-
const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
|
|
113
|
-
lhsParent = result.target;
|
|
114
|
-
lhsKey = result.lastKey;
|
|
115
|
-
lhsTarget = result.target[result.lastKey];
|
|
116
|
-
// If last key is a method, call it to get the target
|
|
117
|
-
if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
|
|
118
|
-
lhsTarget = result.target[result.lastKey].call(result.target);
|
|
119
|
-
lhsParent = undefined; // Can't assign back to a method call result
|
|
120
|
-
lhsKey = undefined;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
// Simple path navigation — walk to parent, keep last key
|
|
125
|
-
if (pathParts.length === 0) {
|
|
126
|
-
lhsTarget = target;
|
|
127
|
-
}
|
|
128
|
-
else if (pathParts.length === 1) {
|
|
129
|
-
lhsParent = target;
|
|
130
|
-
lhsKey = pathParts[0];
|
|
131
|
-
lhsTarget = target[pathParts[0]];
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
let current = target;
|
|
135
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
136
|
-
if (current == null)
|
|
137
|
-
break;
|
|
138
|
-
current = current[pathParts[i]];
|
|
139
|
-
}
|
|
140
|
-
lhsParent = current;
|
|
141
|
-
lhsKey = pathParts[pathParts.length - 1];
|
|
142
|
-
lhsTarget = current != null ? current[lhsKey] : undefined;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
else if (lhsPath) {
|
|
147
|
-
lhsParent = target;
|
|
148
|
-
lhsKey = lhsPath;
|
|
149
|
-
lhsTarget = target[lhsPath];
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
lhsTarget = target;
|
|
153
|
-
}
|
|
103
|
+
const { lhsTarget, lhsParent, lhsKey } = resolveLhsPath(target, lhsPath, options);
|
|
154
104
|
// Execute handlers sequentially, sharing the same lhsTarget
|
|
155
105
|
for (const config of configs) {
|
|
156
106
|
//return; //1.3ms
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { resolveValues } from './resolve/resolveValues.js';
|
|
8
8
|
import { getValues } from './resolve/getValues.js';
|
|
9
|
-
import {
|
|
9
|
+
import { resolveLhsPath } from './utils/resolveLhsPath.js';
|
|
10
10
|
import { findClassPrototypeInPath } from './utils/findClassPrototypeInPath.js';
|
|
11
11
|
import type { PermissionProcessor } from './types/assign-gingerly/types.js';
|
|
12
12
|
import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFromAsync.js';
|
|
@@ -14,11 +14,13 @@ import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFr
|
|
|
14
14
|
/**
|
|
15
15
|
* Map of built-in handler names to their module paths.
|
|
16
16
|
* These are auto-loaded on demand — no explicit import required.
|
|
17
|
+
*
|
|
18
|
+
* `join` moved to a synchronous ` =&` op (see syncOps/join.ts) — it never had
|
|
19
|
+
* side effects or anything to await, so it didn't belong behind this async pipeline.
|
|
17
20
|
*/
|
|
18
21
|
const BUILT_IN_MAP: Record<string, string> = {
|
|
19
22
|
'builtIns.lazyLoad': './handlers/lazyLoad.js',
|
|
20
23
|
'builtIns.lazyLoadSwitch': './handlers/lazyLoadSwitch.js',
|
|
21
|
-
'builtIns.join': './handlers/join.js',
|
|
22
24
|
'builtIns.microDataJoin': './handlers/microDataJoin.js',
|
|
23
25
|
'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
|
|
24
26
|
'builtIns.rangeSelector': './handlers/rangeSelector.js',
|
|
@@ -121,55 +123,8 @@ export async function processHandlerCommands(
|
|
|
121
123
|
|
|
122
124
|
// Resolve the LHS path, preserving parent + key for return-value assignment.
|
|
123
125
|
// lhsParent[lhsKey] === lhsTarget (the current value at the path)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let lhsKey: string | undefined = undefined;
|
|
127
|
-
|
|
128
|
-
if (lhsPath.startsWith('?.')) {
|
|
129
|
-
const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
|
|
130
|
-
const withMethodsSet = options.withMethods
|
|
131
|
-
? options.withMethods instanceof Set
|
|
132
|
-
? options.withMethods
|
|
133
|
-
: new Set(options.withMethods)
|
|
134
|
-
: undefined;
|
|
135
|
-
|
|
136
|
-
if (withMethodsSet && pathParts.length > 0) {
|
|
137
|
-
const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
|
|
138
|
-
lhsParent = result.target;
|
|
139
|
-
lhsKey = result.lastKey;
|
|
140
|
-
lhsTarget = result.target[result.lastKey];
|
|
141
|
-
// If last key is a method, call it to get the target
|
|
142
|
-
if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
|
|
143
|
-
lhsTarget = result.target[result.lastKey].call(result.target);
|
|
144
|
-
lhsParent = undefined; // Can't assign back to a method call result
|
|
145
|
-
lhsKey = undefined;
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
// Simple path navigation — walk to parent, keep last key
|
|
149
|
-
if (pathParts.length === 0) {
|
|
150
|
-
lhsTarget = target;
|
|
151
|
-
} else if (pathParts.length === 1) {
|
|
152
|
-
lhsParent = target;
|
|
153
|
-
lhsKey = pathParts[0];
|
|
154
|
-
lhsTarget = target[pathParts[0]];
|
|
155
|
-
} else {
|
|
156
|
-
let current = target;
|
|
157
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
158
|
-
if (current == null) break;
|
|
159
|
-
current = current[pathParts[i]];
|
|
160
|
-
}
|
|
161
|
-
lhsParent = current;
|
|
162
|
-
lhsKey = pathParts[pathParts.length - 1];
|
|
163
|
-
lhsTarget = current != null ? current[lhsKey] : undefined;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} else if (lhsPath) {
|
|
167
|
-
lhsParent = target;
|
|
168
|
-
lhsKey = lhsPath;
|
|
169
|
-
lhsTarget = target[lhsPath];
|
|
170
|
-
} else {
|
|
171
|
-
lhsTarget = target;
|
|
172
|
-
}
|
|
126
|
+
const { lhsTarget, lhsParent, lhsKey } = resolveLhsPath(target, lhsPath, options);
|
|
127
|
+
|
|
173
128
|
// Execute handlers sequentially, sharing the same lhsTarget
|
|
174
129
|
for (const config of configs) {
|
|
175
130
|
//return; //1.3ms
|