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.
@@ -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);
@@ -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
+ }
package/assignFrom.js CHANGED
@@ -7,11 +7,14 @@
7
7
  * For async protocol handlers or awaitable handler execution, use assignFromAsync.
8
8
  *
9
9
  * Handler commands (` =>`) are fire-and-forget (kicked off asynchronously, not awaited).
10
+ * Sync-op commands (` =&`) are always fully synchronous — see SYNC_OPS.
10
11
  */
11
12
  import { getValues, getValue } from './resolve/getValues.js';
12
13
  import assignGingerly from './assignGingerly.js';
13
14
  import { resolveIdVariable, parseIdRef } from './resolve/resolveIdRef.js';
14
15
  import { processInferredAssignments } from './inferredAssignments.js';
16
+ import { resolveLhsPath } from './utils/resolveLhsPath.js';
17
+ import { SYNC_OPS } from './syncOps/registry.js';
15
18
  /**
16
19
  * Supported substitution variables and their option keys.
17
20
  */
@@ -40,6 +43,48 @@ export function parseTernaryCommand(key) {
40
43
  return null;
41
44
  return key.substring(0, key.length - 3); // Remove ' ?=' suffix
42
45
  }
46
+ /**
47
+ * Check if a key ends with the sync-op operator ' =&'.
48
+ */
49
+ export function isSyncOpCommand(key) {
50
+ return key.endsWith(' =&');
51
+ }
52
+ /**
53
+ * Parse a =& sync-op command and extract the LHS path.
54
+ */
55
+ export function parseSyncOpCommand(key) {
56
+ if (!isSyncOpCommand(key))
57
+ return null;
58
+ return key.substring(0, key.length - 3); // Remove ' =&' suffix
59
+ }
60
+ /**
61
+ * Throws if a resolved sync-op value (or anything nested inside it) is a thenable.
62
+ *
63
+ * getValues is synchronous by contract, but `options.protocols` handlers are
64
+ * typed to allow returning a Promise. A sync op has no await to catch that —
65
+ * left unchecked it would silently stringify as "[object Promise]" — so this
66
+ * turns it into a clear error instead.
67
+ */
68
+ function assertNoThenable(value, opName, key) {
69
+ if (value == null)
70
+ return;
71
+ if (typeof value.then === 'function') {
72
+ throw new Error(`assignFrom: sync op '${opName}' (key "${key}") received an async value — ` +
73
+ `a protocol in options.protocols returned a Promise, which ' =&' cannot await. ` +
74
+ `Use ' =>' with 'resolve:' instead for async values.`);
75
+ }
76
+ if (Array.isArray(value)) {
77
+ for (const item of value)
78
+ assertNoThenable(item, opName, key);
79
+ }
80
+ else if (typeof value === 'object') {
81
+ const proto = Object.getPrototypeOf(value);
82
+ if (proto === Object.prototype || proto === null) {
83
+ for (const item of Object.values(value))
84
+ assertNoThenable(item, opName, key);
85
+ }
86
+ }
87
+ }
43
88
  /**
44
89
  * Resolve a single value — if it's a `?.` path string, resolve against source.
45
90
  * If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
@@ -343,6 +388,7 @@ export function categorizeKeys(expandedPattern) {
343
388
  const idRefNormalKeys = [];
344
389
  const idRefHandlerKeys = [];
345
390
  const ternaryKeys = [];
391
+ const syncOpKeys = [];
346
392
  for (const key of Object.keys(expandedPattern)) {
347
393
  if (isHandlerCommand(key)) {
348
394
  if (key.startsWith('#[')) {
@@ -355,6 +401,9 @@ export function categorizeKeys(expandedPattern) {
355
401
  else if (isTernaryCommand(key)) {
356
402
  ternaryKeys.push(key);
357
403
  }
404
+ else if (isSyncOpCommand(key)) {
405
+ syncOpKeys.push(key);
406
+ }
358
407
  else if (key.startsWith('#[')) {
359
408
  idRefNormalKeys.push(key);
360
409
  }
@@ -362,7 +411,7 @@ export function categorizeKeys(expandedPattern) {
362
411
  normalPattern[key] = expandedPattern[key];
363
412
  }
364
413
  }
365
- return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
414
+ return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys };
366
415
  }
367
416
  /**
368
417
  * Merge pin and at into a single lookup map for resolveIdVariable.
@@ -420,7 +469,7 @@ export function assignFrom(target, pattern, options, permissionProcessor) {
420
469
  // Expand looped substitution variables
421
470
  const expandedPattern = expandSubstitutions(pattern, options);
422
471
  // Categorize keys
423
- const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
472
+ const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys } = categorizeKeys(expandedPattern);
424
473
  const resolveOptions = { ...options, root: target, permissionProcessor };
425
474
  // Process ?= ternary keys (sync)
426
475
  if (ternaryKeys.length > 0) {
@@ -441,6 +490,33 @@ export function assignFrom(target, pattern, options, permissionProcessor) {
441
490
  assignGingerly(target, ternaryResolved, options, permissionProcessor);
442
491
  }
443
492
  }
493
+ // Process =& sync-op keys (sync — always, no dynamic import, no await, ever)
494
+ if (syncOpKeys.length > 0) {
495
+ for (const key of syncOpKeys) {
496
+ const lhsPath = parseSyncOpCommand(key);
497
+ if (lhsPath === null)
498
+ continue;
499
+ const config = expandedPattern[key];
500
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
501
+ throw new Error(`assignFrom: sync-op command "${key}" requires a config object naming exactly one op`);
502
+ }
503
+ const opName = Object.keys(config).find(k => k in SYNC_OPS);
504
+ if (!opName) {
505
+ throw new Error(`assignFrom: sync-op command "${key}" does not name a known op (${Object.keys(SYNC_OPS).join(', ')})`);
506
+ }
507
+ const resolvedConfig = getValues(config, options.from, resolveOptions);
508
+ assertNoThenable(resolvedConfig, opName, key);
509
+ const { [opName]: args, ...extra } = resolvedConfig;
510
+ const result = SYNC_OPS[opName](args, extra);
511
+ if (result === undefined)
512
+ continue;
513
+ const { lhsParent, lhsKey } = resolveLhsPath(target, lhsPath, options);
514
+ if (lhsParent != null && lhsKey != null
515
+ && !permissionProcessor?.redirectRestrictedProp(lhsParent, lhsKey, result)) {
516
+ lhsParent[lhsKey] = result;
517
+ }
518
+ }
519
+ }
444
520
  // Process normal keys via getValues (sync) + assignGingerly
445
521
  if (Object.keys(normalPattern).length > 0) {
446
522
  // Resolve #[x] references on RHS values before getValues
package/assignFrom.ts CHANGED
@@ -7,12 +7,15 @@
7
7
  * For async protocol handlers or awaitable handler execution, use assignFromAsync.
8
8
  *
9
9
  * Handler commands (` =>`) are fire-and-forget (kicked off asynchronously, not awaited).
10
+ * Sync-op commands (` =&`) are always fully synchronous — see SYNC_OPS.
10
11
  */
11
12
 
12
13
  import { getValues, getValue } from './resolve/getValues.js';
13
14
  import assignGingerly from './assignGingerly.js';
14
15
  import { resolveIdVariable, parseIdRef } from './resolve/resolveIdRef.js';
15
16
  import { processInferredAssignments } from './inferredAssignments.js';
17
+ import { resolveLhsPath } from './utils/resolveLhsPath.js';
18
+ import { SYNC_OPS } from './syncOps/registry.js';
16
19
  import type { PermissionProcessor, AssignFromOptions, AssignFromHandler, AssignFromHandlerConstructor } from './types/assign-gingerly/types.js';
17
20
 
18
21
  // Re-export types for consumers
@@ -49,6 +52,48 @@ export function parseTernaryCommand(key: string): string | null {
49
52
  return key.substring(0, key.length - 3); // Remove ' ?=' suffix
50
53
  }
51
54
 
55
+ /**
56
+ * Check if a key ends with the sync-op operator ' =&'.
57
+ */
58
+ export function isSyncOpCommand(key: string): boolean {
59
+ return key.endsWith(' =&');
60
+ }
61
+
62
+ /**
63
+ * Parse a =& sync-op command and extract the LHS path.
64
+ */
65
+ export function parseSyncOpCommand(key: string): string | null {
66
+ if (!isSyncOpCommand(key)) return null;
67
+ return key.substring(0, key.length - 3); // Remove ' =&' suffix
68
+ }
69
+
70
+ /**
71
+ * Throws if a resolved sync-op value (or anything nested inside it) is a thenable.
72
+ *
73
+ * getValues is synchronous by contract, but `options.protocols` handlers are
74
+ * typed to allow returning a Promise. A sync op has no await to catch that —
75
+ * left unchecked it would silently stringify as "[object Promise]" — so this
76
+ * turns it into a clear error instead.
77
+ */
78
+ function assertNoThenable(value: any, opName: string, key: string): void {
79
+ if (value == null) return;
80
+ if (typeof value.then === 'function') {
81
+ throw new Error(
82
+ `assignFrom: sync op '${opName}' (key "${key}") received an async value — ` +
83
+ `a protocol in options.protocols returned a Promise, which ' =&' cannot await. ` +
84
+ `Use ' =>' with 'resolve:' instead for async values.`
85
+ );
86
+ }
87
+ if (Array.isArray(value)) {
88
+ for (const item of value) assertNoThenable(item, opName, key);
89
+ } else if (typeof value === 'object') {
90
+ const proto = Object.getPrototypeOf(value);
91
+ if (proto === Object.prototype || proto === null) {
92
+ for (const item of Object.values(value)) assertNoThenable(item, opName, key);
93
+ }
94
+ }
95
+ }
96
+
52
97
  /**
53
98
  * Resolve a single value — if it's a `?.` path string, resolve against source.
54
99
  * If it's a protocol string, resolve via protocol. Otherwise pass through as literal.
@@ -354,6 +399,7 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
354
399
  const idRefNormalKeys: string[] = [];
355
400
  const idRefHandlerKeys: string[] = [];
356
401
  const ternaryKeys: string[] = [];
402
+ const syncOpKeys: string[] = [];
357
403
 
358
404
  for (const key of Object.keys(expandedPattern)) {
359
405
  if (isHandlerCommand(key)) {
@@ -364,6 +410,8 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
364
410
  }
365
411
  } else if (isTernaryCommand(key)) {
366
412
  ternaryKeys.push(key);
413
+ } else if (isSyncOpCommand(key)) {
414
+ syncOpKeys.push(key);
367
415
  } else if (key.startsWith('#[')) {
368
416
  idRefNormalKeys.push(key);
369
417
  } else {
@@ -371,7 +419,7 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
371
419
  }
372
420
  }
373
421
 
374
- return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys };
422
+ return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys };
375
423
  }
376
424
 
377
425
  /**
@@ -447,7 +495,7 @@ export function assignFrom(
447
495
  const expandedPattern = expandSubstitutions(pattern, options);
448
496
 
449
497
  // Categorize keys
450
- const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys } = categorizeKeys(expandedPattern);
498
+ const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys } = categorizeKeys(expandedPattern);
451
499
 
452
500
  const resolveOptions = { ...options, root: target, permissionProcessor } as any;
453
501
 
@@ -469,6 +517,36 @@ export function assignFrom(
469
517
  }
470
518
  }
471
519
 
520
+ // Process =& sync-op keys (sync — always, no dynamic import, no await, ever)
521
+ if (syncOpKeys.length > 0) {
522
+ for (const key of syncOpKeys) {
523
+ const lhsPath = parseSyncOpCommand(key);
524
+ if (lhsPath === null) continue;
525
+ const config = expandedPattern[key];
526
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
527
+ throw new Error(`assignFrom: sync-op command "${key}" requires a config object naming exactly one op`);
528
+ }
529
+
530
+ const opName = Object.keys(config).find(k => k in SYNC_OPS);
531
+ if (!opName) {
532
+ throw new Error(`assignFrom: sync-op command "${key}" does not name a known op (${Object.keys(SYNC_OPS).join(', ')})`);
533
+ }
534
+
535
+ const resolvedConfig = getValues(config, options.from, resolveOptions);
536
+ assertNoThenable(resolvedConfig, opName, key);
537
+
538
+ const { [opName]: args, ...extra } = resolvedConfig;
539
+ const result = SYNC_OPS[opName](args, extra);
540
+ if (result === undefined) continue;
541
+
542
+ const { lhsParent, lhsKey } = resolveLhsPath(target, lhsPath, options);
543
+ if (lhsParent != null && lhsKey != null
544
+ && !permissionProcessor?.redirectRestrictedProp(lhsParent, lhsKey, result)) {
545
+ lhsParent[lhsKey] = result;
546
+ }
547
+ }
548
+ }
549
+
472
550
  // Process normal keys via getValues (sync) + assignGingerly
473
551
  if (Object.keys(normalPattern).length > 0) {
474
552
  // Resolve #[x] references on RHS values before getValues
@@ -224,7 +224,7 @@ Create `.vscode/settings.json`:
224
224
  }
225
225
  ```
226
226
 
227
- ## Step 8: Set Up .kiro Directory
227
+ ## Step 8: Set Up .kiro Directory Only if kiro is implementing.
228
228
 
229
229
  Create `.kiro/steering/project-context.md` to reference the shared types documentation:
230
230
 
@@ -350,6 +350,8 @@ customElements.assignFeatures(MyElement, {
350
350
 
351
351
  The parsed attributes (`{ myProp: 'hello', count: 42 }`) are passed as `initVals` to the constructor.
352
352
 
353
+ See [withAttrs](https://github.com/bahrus/assign-gingerly/blob/baseline/docs/withAttrs.md) for an in-depth discussion of all the various configuration options.
354
+
353
355
  ### Async Spawn (Lazy Loading)
354
356
 
355
357
  Feature implementations can be loaded asynchronously:
@@ -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:10, //milliseconds
384
+ delay:100, //milliseconds
383
385
  ifAllOf: ['expanded'],
384
386
  assign: {
385
387
  set($.querySelector('a').focus()).to({}),
@@ -387,6 +389,71 @@ 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
+ [Please fully digest all the attribute parsing tha assign-gingerly provides before configuring the attributes.](https://github.com/bahrus/assign-gingerly/blob/baseline/docs/withAttrs.md)
455
+
456
+
390
457
  ## Step 8
391
458
 
392
459
  Run `node el-maker.mjs` (or `npm run build-el-maker` if your `package.json` includes a watch script) to regenerate `el-maker.json`.
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assign-gingerly",
3
- "version": "0.0.84",
3
+ "version": "0.0.86",
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": {
@@ -22,6 +22,7 @@
22
22
  "handlers/**",
23
23
  "inferencer/**",
24
24
  "resolve/**",
25
+ "syncOps/**",
25
26
  "utils/**",
26
27
  "README.md",
27
28
  "LICENSE",
@@ -51,6 +52,10 @@
51
52
  "./parserRegistry.js": {
52
53
  "default": "./parserRegistry.js"
53
54
  },
55
+ "./SplitParser.js": {
56
+ "default": "./SplitParser.js",
57
+ "types": "./SplitParser.ts"
58
+ },
54
59
  "./parseWithAttrs.js": {
55
60
  "default": "./parseWithAttrs.js",
56
61
  "types": "./parseWithAttrs.ts"
@@ -99,9 +104,13 @@
99
104
  "default": "./handlers/lazyLoadSwitch.js",
100
105
  "types": "./handlers/lazyLoadSwitch.ts"
101
106
  },
102
- "./handlers/join.js": {
103
- "default": "./handlers/join.js",
104
- "types": "./handlers/join.ts"
107
+ "./syncOps/join.js": {
108
+ "default": "./syncOps/join.js",
109
+ "types": "./syncOps/join.ts"
110
+ },
111
+ "./syncOps/registry.js": {
112
+ "default": "./syncOps/registry.js",
113
+ "types": "./syncOps/registry.ts"
105
114
  },
106
115
  "./handlers/microDataJoin.js": {
107
116
  "default": "./handlers/microDataJoin.js",