assign-gingerly 0.0.83 → 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/inferencer/types/roundabout/types.d.ts +8 -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/resolve/getValues.js +2 -0
- package/resolve/getValues.ts +1 -0
- package/types/assign-gingerly/types.d.ts +43 -5
package/ScopedParserRegistry.js
CHANGED
|
@@ -9,7 +9,7 @@ export class ScopedParserRegistry {
|
|
|
9
9
|
* Register a parser with a given name
|
|
10
10
|
* Resolves any pending waiters for this parser
|
|
11
11
|
* @param name - The name to register the parser under
|
|
12
|
-
* @param parser - The parser function
|
|
12
|
+
* @param parser - The parser function or class constructor
|
|
13
13
|
*/
|
|
14
14
|
register(name, parser) {
|
|
15
15
|
if (this.parsers.has(name)) {
|
|
@@ -26,7 +26,7 @@ export class ScopedParserRegistry {
|
|
|
26
26
|
/**
|
|
27
27
|
* Get a parser by name
|
|
28
28
|
* @param name - The name of the parser
|
|
29
|
-
* @returns The parser function or undefined if not found
|
|
29
|
+
* @returns The parser function, class constructor, or undefined if not found
|
|
30
30
|
*/
|
|
31
31
|
get(name) {
|
|
32
32
|
return this.parsers.get(name);
|
package/ScopedParserRegistry.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { ParserFunction } from './types/assign-gingerly/types';
|
|
1
|
+
import { ParserFunction, AttrParserConstructor } from './types/assign-gingerly/types';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Registry for parsers scoped to a synthesizer element (be-hive, htmx-container, etc.)
|
|
5
5
|
* Enables lazy-loading of complex parsers with Promise-based waiting
|
|
6
6
|
*/
|
|
7
7
|
export class ScopedParserRegistry {
|
|
8
|
-
private parsers = new Map<string, ParserFunction>();
|
|
8
|
+
private parsers = new Map<string, ParserFunction | AttrParserConstructor>();
|
|
9
9
|
private pendingWaits = new Map<string, Array<{
|
|
10
10
|
resolve: () => void;
|
|
11
11
|
reject: (error: Error) => void;
|
|
@@ -15,9 +15,9 @@ export class ScopedParserRegistry {
|
|
|
15
15
|
* Register a parser with a given name
|
|
16
16
|
* Resolves any pending waiters for this parser
|
|
17
17
|
* @param name - The name to register the parser under
|
|
18
|
-
* @param parser - The parser function
|
|
18
|
+
* @param parser - The parser function or class constructor
|
|
19
19
|
*/
|
|
20
|
-
register(name: string, parser: ParserFunction): void {
|
|
20
|
+
register(name: string, parser: ParserFunction | AttrParserConstructor): void {
|
|
21
21
|
if (this.parsers.has(name)) {
|
|
22
22
|
console.warn(`Parser "${name}" already registered in scoped registry, overwriting`);
|
|
23
23
|
}
|
|
@@ -35,9 +35,9 @@ export class ScopedParserRegistry {
|
|
|
35
35
|
/**
|
|
36
36
|
* Get a parser by name
|
|
37
37
|
* @param name - The name of the parser
|
|
38
|
-
* @returns The parser function or undefined if not found
|
|
38
|
+
* @returns The parser function, class constructor, or undefined if not found
|
|
39
39
|
*/
|
|
40
|
-
get(name: string): ParserFunction | undefined {
|
|
40
|
+
get(name: string): ParserFunction | AttrParserConstructor | undefined {
|
|
41
41
|
return this.parsers.get(name);
|
|
42
42
|
}
|
|
43
43
|
|
package/SplitParser.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes regex metacharacters so a string delimiter is treated literally.
|
|
3
|
+
*/
|
|
4
|
+
function escapeRegex(str) {
|
|
5
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Built-in class parser that splits an attribute value into an array.
|
|
9
|
+
* Registered under the name 'splitter' in globalParserRegistry.
|
|
10
|
+
*/
|
|
11
|
+
export class SplitParser {
|
|
12
|
+
delimiter;
|
|
13
|
+
trim;
|
|
14
|
+
skipEmpty;
|
|
15
|
+
dedupe;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
const opts = options ?? {};
|
|
18
|
+
const rawDelimiter = opts.delimiter;
|
|
19
|
+
if (rawDelimiter === undefined) {
|
|
20
|
+
this.delimiter = /\s+/;
|
|
21
|
+
}
|
|
22
|
+
else if (typeof rawDelimiter === 'string') {
|
|
23
|
+
this.delimiter = rawDelimiter === '' ? /(?:)/ : new RegExp(escapeRegex(rawDelimiter));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
this.delimiter = new RegExp(rawDelimiter.pattern, rawDelimiter.flags ?? '');
|
|
27
|
+
}
|
|
28
|
+
this.trim = opts.trim ?? true;
|
|
29
|
+
this.skipEmpty = opts.skipEmpty ?? true;
|
|
30
|
+
this.dedupe = opts.dedupe ?? false;
|
|
31
|
+
}
|
|
32
|
+
parse(v, _context) {
|
|
33
|
+
if (v === null || v === '') {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
let parts = v.split(this.delimiter);
|
|
37
|
+
if (this.trim) {
|
|
38
|
+
parts = parts.map((s) => s.trim());
|
|
39
|
+
}
|
|
40
|
+
if (this.skipEmpty) {
|
|
41
|
+
parts = parts.filter((s) => s !== '');
|
|
42
|
+
}
|
|
43
|
+
if (this.dedupe) {
|
|
44
|
+
parts = [...new Set(parts)];
|
|
45
|
+
}
|
|
46
|
+
return parts;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/SplitParser.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AttrParser, ParserContext } from './types/assign-gingerly/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for SplitParser
|
|
5
|
+
*/
|
|
6
|
+
export interface SplitParserOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Delimiter used to split the attribute value.
|
|
9
|
+
* - String: treated as a literal separator (regex specials are escaped)
|
|
10
|
+
* - Object: { pattern: string; flags?: string } builds a RegExp directly
|
|
11
|
+
* - Default: /\s+/
|
|
12
|
+
*/
|
|
13
|
+
delimiter?: string | { pattern: string; flags?: string };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Whether to trim each split part.
|
|
17
|
+
* Default: true
|
|
18
|
+
*/
|
|
19
|
+
trim?: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether to skip empty strings after splitting/trimming.
|
|
23
|
+
* Default: true
|
|
24
|
+
*/
|
|
25
|
+
skipEmpty?: boolean;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether to remove duplicate values.
|
|
29
|
+
* Default: false
|
|
30
|
+
*/
|
|
31
|
+
dedupe?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Escapes regex metacharacters so a string delimiter is treated literally.
|
|
36
|
+
*/
|
|
37
|
+
function escapeRegex(str: string): string {
|
|
38
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Built-in class parser that splits an attribute value into an array.
|
|
43
|
+
* Registered under the name 'splitter' in globalParserRegistry.
|
|
44
|
+
*/
|
|
45
|
+
export class SplitParser implements AttrParser {
|
|
46
|
+
private delimiter: RegExp;
|
|
47
|
+
private trim: boolean;
|
|
48
|
+
private skipEmpty: boolean;
|
|
49
|
+
private dedupe: boolean;
|
|
50
|
+
|
|
51
|
+
constructor(options?: SplitParserOptions) {
|
|
52
|
+
const opts = options ?? {};
|
|
53
|
+
const rawDelimiter = opts.delimiter;
|
|
54
|
+
|
|
55
|
+
if (rawDelimiter === undefined) {
|
|
56
|
+
this.delimiter = /\s+/;
|
|
57
|
+
} else if (typeof rawDelimiter === 'string') {
|
|
58
|
+
this.delimiter = rawDelimiter === '' ? /(?:)/ : new RegExp(escapeRegex(rawDelimiter));
|
|
59
|
+
} else {
|
|
60
|
+
this.delimiter = new RegExp(rawDelimiter.pattern, rawDelimiter.flags ?? '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.trim = opts.trim ?? true;
|
|
64
|
+
this.skipEmpty = opts.skipEmpty ?? true;
|
|
65
|
+
this.dedupe = opts.dedupe ?? false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
parse(v: string | null, _context?: ParserContext): any {
|
|
69
|
+
if (v === null || v === '') {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let parts = v.split(this.delimiter);
|
|
74
|
+
|
|
75
|
+
if (this.trim) {
|
|
76
|
+
parts = parts.map((s) => s.trim());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.skipEmpty) {
|
|
80
|
+
parts = parts.filter((s) => s !== '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this.dedupe) {
|
|
84
|
+
parts = [...new Set(parts)];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return parts;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
- **[plus-minus](https://github.com/bahrus/plus-minus)** -- Expand / Collapse component - More robust examples of dynamic DOM manipulation with the help of roundabout configuration. Also demonstrates use of the DX libraries to get typing intellisense help.
|
|
9
9
|
|
|
10
|
+
- **[side-burger](https://github.com/bahrus/side-burger)** -- Side Drawer component with menu.
|
|
11
|
+
|
|
10
12
|
## Step 4
|
|
11
13
|
|
|
12
14
|
Add the following additional dependencies in package.json:
|
|
@@ -379,7 +381,7 @@ Work is underway to improve the DX a bit, but for now:
|
|
|
379
381
|
|
|
380
382
|
```JS
|
|
381
383
|
{
|
|
382
|
-
delay:
|
|
384
|
+
delay:100, //milliseconds
|
|
383
385
|
ifAllOf: ['expanded'],
|
|
384
386
|
assign: {
|
|
385
387
|
set($.querySelector('a').focus()).to({}),
|
|
@@ -387,6 +389,69 @@ Work is underway to improve the DX a bit, but for now:
|
|
|
387
389
|
},
|
|
388
390
|
```
|
|
389
391
|
|
|
392
|
+
## How can I set externally specified elements to inert?
|
|
393
|
+
|
|
394
|
+
This is implemented with the [side-burger](https://github.com/bahrus/side-burger) custom element, to see the full context.
|
|
395
|
+
|
|
396
|
+
Suppose we define a property on the custom element, "inertTarget" which allows the developer to specify css matches from the root document to set to inert when the sidebar is open.
|
|
397
|
+
|
|
398
|
+
```JS
|
|
399
|
+
|
|
400
|
+
// kept separate because "smoothOver" destroys typechecking
|
|
401
|
+
/** @type Merges<AP> */
|
|
402
|
+
const merges = [
|
|
403
|
+
...
|
|
404
|
+
{
|
|
405
|
+
ifAllOf: [props.clone, props.inertTarget, props.open],
|
|
406
|
+
...doAssign(
|
|
407
|
+
set(props.inertTargetElements).to($.ownerDocument.querySelectorAll($.inertTarget)),
|
|
408
|
+
set($.inertTargetElements.Each.inert).to(true)
|
|
409
|
+
)
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
ifAllOf: [props.clone, props.inertTarget],
|
|
413
|
+
ifNoneOf: [props.open],
|
|
414
|
+
...doAssign(
|
|
415
|
+
set($.inertTargetElements.Each.inert).to(false)
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
/** @type {AttrPatterns<AP>} */
|
|
421
|
+
const withAttrs = {
|
|
422
|
+
...
|
|
423
|
+
[props.inertTarget]: 'inert-target',
|
|
424
|
+
[`_${props.inertTarget}`]: {
|
|
425
|
+
mapsTo: props.inertTarget,
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* @type {RoundaboutOptions<AP, Actions, AP, 'click' | 'keydown'>}
|
|
431
|
+
*/
|
|
432
|
+
const raConfig = {
|
|
433
|
+
weakRef: {
|
|
434
|
+
...
|
|
435
|
+
listProperties: [props.inertTargetElements],
|
|
436
|
+
...
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
assignOptions: {
|
|
440
|
+
akaMethods: {
|
|
441
|
+
...
|
|
442
|
+
'🧺': m['🧺'], //querySelectorAll
|
|
443
|
+
},
|
|
444
|
+
substitutions: {
|
|
445
|
+
inertTarget: '?.inertTarget'
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
...
|
|
449
|
+
merges: smoothOver(merges),
|
|
450
|
+
...
|
|
451
|
+
};
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
|
|
390
455
|
## Step 8
|
|
391
456
|
|
|
392
457
|
Run `node el-maker.mjs` (or `npm run build-el-maker` if your `package.json` includes a watch script) to regenerate `el-maker.json`.
|
|
@@ -224,7 +224,14 @@ export interface WeakRefConfig<TProps = any> {
|
|
|
224
224
|
/**
|
|
225
225
|
* Properties to automatically wrap in WeakRef
|
|
226
226
|
*/
|
|
227
|
-
properties
|
|
227
|
+
properties?: Array<keyof TProps & string>;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Array-valued properties whose elements should be stored as WeakRefs.
|
|
231
|
+
* When accessed, the getter returns a new array with each WeakRef dereferenced.
|
|
232
|
+
* Collected elements appear as `undefined` in their original positions.
|
|
233
|
+
*/
|
|
234
|
+
listProperties?: Array<keyof TProps & string>;
|
|
228
235
|
|
|
229
236
|
/**
|
|
230
237
|
* Logging behavior when deref returns null/undefined
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.85",
|
|
4
4
|
"description": "This package provides a utility function for carefully merging one object into another.",
|
|
5
5
|
"homepage": "https://github.com/bahrus/assign-gingerly#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
"./parserRegistry.js": {
|
|
52
52
|
"default": "./parserRegistry.js"
|
|
53
53
|
},
|
|
54
|
+
"./SplitParser.js": {
|
|
55
|
+
"default": "./SplitParser.js",
|
|
56
|
+
"types": "./SplitParser.ts"
|
|
57
|
+
},
|
|
54
58
|
"./parseWithAttrs.js": {
|
|
55
59
|
"default": "./parseWithAttrs.js",
|
|
56
60
|
"types": "./parseWithAttrs.ts"
|
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)
|