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/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
|
+
}
|
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:
|
|
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', '
|
|
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.
|
|
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
|
-
"./
|
|
103
|
-
"default": "./
|
|
104
|
-
"types": "./
|
|
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",
|