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/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 parsers from scoped registry (if synthesizerElement provided)
14
- * - Named parsers from global registry (fallback)
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 string reference
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: ParserFunction | string | undefined,
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
- // 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;
43
- }
110
+ const registered = lookupNamedParser(parserSpec, synthesizerElement);
111
+ if (isAttrParserConstructor(registered)) {
112
+ return instantiateClassParser(registered, parserOptions);
44
113
  }
45
-
46
- // Fallback to global registry
47
- const globalParser = globalParserRegistry.get(parserSpec);
48
- if (globalParser) {
49
- return globalParser;
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
- // Not found in either registry
53
- throw new Error(
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)`
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
- parserStr = `named:${config.parser}`;
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
- import { ScopedParserRegistry } from './ScopedParserRegistry.js';
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
- import { ScopedParserRegistry } from './ScopedParserRegistry.js';
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', 'csv')
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