assign-gingerly 0.0.90 → 0.0.92
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 +42 -0
- package/assignFrom.js +50 -4
- package/assignFrom.ts +49 -4
- package/assignFromAsync.js +24 -2
- package/assignFromAsync.ts +24 -2
- package/inferencer/inferencer.js +80 -2
- package/inferencer/inferencer.ts +80 -3
- package/inferencer/types/NewCustomElement.md +12 -0
- package/inferencer/types/NewCustomElementFeature.md +28 -4
- package/inferencer/types/NewHTMLFirstCustomElement.md +196 -2
- package/inferencer/types/NewJSFirstCustomElement.md +11 -3
- package/inferencer/types/assign-gingerly/types.d.ts +8 -0
- package/inferencer/types/be-intl/types.d.ts +68 -0
- package/inferencer/types/chip-away/types.d.ts +71 -0
- package/inferencer/types/el-maker/types.d.ts +17 -1
- package/inferencer/types/h2o-table/types.d.ts +73 -0
- package/inferencer/types/id-referencer/types.d.ts +35 -0
- package/inferencer/types/nested-regex-groups/types.d.ts +2 -0
- package/inferencer/types/swipe-dismiss/types.d.ts +14 -0
- package/package.json +1 -1
- package/resolve/getValues.js +87 -44
- package/resolve/getValues.ts +106 -38
- package/types/assign-gingerly/types.d.ts +8 -0
package/README.md
CHANGED
|
@@ -102,6 +102,8 @@ assignFrom adds support for:
|
|
|
102
102
|
5. Looped substitution with `where_x_in` / `where_y_in` / `where_z_in` for expanding template patterns into multiple concrete assignments.
|
|
103
103
|
6. Dynamic substitutions via the `substitutions` option for injecting runtime string values into path segments. See [docs/substitutions.md](docs/substitutions.md).
|
|
104
104
|
7. Spread merging via the `"..."` key.
|
|
105
|
+
8. Boolean coercion / negation of a resolved value via a leading `!` marker: `'!!?.isOpen'` resolves `?.isOpen` then coerces to a real boolean, `'!?.isOpen'` negates it (`!!!` negates, and so on). Only honored in front of a `?.` path, `$0` reference, or protocol — a plain literal like `'!important'` is left untouched. Nullish coerces to `false`, which makes `!!` the safe way to feed an optional source value into a boolean sink (`hidden`, `disabled`, `classList.toggle`'s force argument, …).
|
|
106
|
+
9. Multiple invocations of one `withMethods` method via the ` =*` operator — see [docs/multi-invoke.md](docs/multi-invoke.md).
|
|
105
107
|
|
|
106
108
|
Example:
|
|
107
109
|
|
|
@@ -853,6 +855,7 @@ While we are in the business of passing values of object A into object B, we mig
|
|
|
853
855
|
| ` ?=` | Ternary | Conditional assignment (assignFrom only) — [details](docs/ternary-assignment.md) | `'?.text ?=': ['?.cond', 'yes', 'no']` |
|
|
854
856
|
| ` =>` | Handler | Invoke a handler plugin (assignFrom only) | `'?.el =>': { do: 'builtIns.lazyLoad', ... }` |
|
|
855
857
|
| ` =&` | Sync op | Compute a value synchronously and assign it back (assignFrom only) — no dynamic import, no await, ever | `'?.text =&': { join: ['?.first', ' ', '?.last'] }` |
|
|
858
|
+
| ` =*` | Multi-invoke | Call a `withMethods` method once per argument-list (assignFrom only) — [details](docs/multi-invoke.md) | `` '?.classList?.toggle =*': [['a', '!!?.x'], ['b', '!!?.y']] `` |
|
|
856
859
|
|
|
857
860
|
All operators use a space before the suffix to distinguish them from property names. They compose with `?.` nested paths and `withMethods`.
|
|
858
861
|
|
|
@@ -957,6 +960,45 @@ The `=!` command syntax is `<path> =!` where the path uses the `?.` nested notat
|
|
|
957
960
|
|
|
958
961
|
For existing values, the toggle is performed using JavaScript's logical NOT operator (`!value`), regardless of what type it is.
|
|
959
962
|
|
|
963
|
+
## Example 5a - Conditional class lists with the `!!` marker and `=*` command
|
|
964
|
+
|
|
965
|
+
The imperative pattern
|
|
966
|
+
|
|
967
|
+
```JS
|
|
968
|
+
if (vm.isOpen) el.classList.add('isOpenCls');
|
|
969
|
+
else el.classList.remove('isOpenCls');
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
is just `el.classList.toggle('isOpenCls', vm.isOpen)`. In `assignFrom` that is a `toggle`
|
|
973
|
+
call in `withMethods` with a resolved force argument — and the `!!` marker coerces the
|
|
974
|
+
resolved value to a real boolean so a missing source value *removes* the class instead of
|
|
975
|
+
flipping it:
|
|
976
|
+
|
|
977
|
+
```TypeScript
|
|
978
|
+
assignFrom(el, {
|
|
979
|
+
'?.classList?.toggle': ['isOpenCls', '!!?.isOpen']
|
|
980
|
+
}, { from: vm, withMethods: ['toggle'] });
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
For several classes at once, the `=*` operator runs the method once per argument-list:
|
|
984
|
+
|
|
985
|
+
```TypeScript
|
|
986
|
+
const vm = { isOpen: true, isLoading: false, hasError: true };
|
|
987
|
+
|
|
988
|
+
assignFrom(el, {
|
|
989
|
+
'?.classList?.toggle =*': [
|
|
990
|
+
['isOpenCls', '!!?.isOpen'],
|
|
991
|
+
['loadingCls', '!!?.isLoading'],
|
|
992
|
+
['errorCls', '!!?.hasError'],
|
|
993
|
+
]
|
|
994
|
+
}, { from: vm, withMethods: ['toggle'] });
|
|
995
|
+
|
|
996
|
+
// toggle('isOpenCls', true); toggle('loadingCls', false); toggle('errorCls', true)
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
See [docs/multi-invoke.md](docs/multi-invoke.md) for the full `=*` semantics, including its
|
|
1000
|
+
interaction with `where_x_in`.
|
|
1001
|
+
|
|
960
1002
|
## Example 6 - Deleting properties with -= command
|
|
961
1003
|
|
|
962
1004
|
The `-=` command allows us to delete properties from objects:
|
package/assignFrom.js
CHANGED
|
@@ -57,6 +57,21 @@ export function parseSyncOpCommand(key) {
|
|
|
57
57
|
return null;
|
|
58
58
|
return key.substring(0, key.length - 3); // Remove ' =&' suffix
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if a key ends with the multi-invoke operator ' =*'.
|
|
62
|
+
*/
|
|
63
|
+
export function isMultiInvokeCommand(key) {
|
|
64
|
+
return key.endsWith(' =*');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parse a =* multi-invoke command and extract the base LHS path — the path a
|
|
68
|
+
* `withMethods` method sits at, to be called once per argument-list on the RHS.
|
|
69
|
+
*/
|
|
70
|
+
export function parseMultiInvokeCommand(key) {
|
|
71
|
+
if (!isMultiInvokeCommand(key))
|
|
72
|
+
return null;
|
|
73
|
+
return key.substring(0, key.length - 3); // Remove ' =*' suffix
|
|
74
|
+
}
|
|
60
75
|
/**
|
|
61
76
|
* Throws if a resolved sync-op value (or anything nested inside it) is a thenable.
|
|
62
77
|
*
|
|
@@ -338,12 +353,19 @@ export function expandSubstitutions(pattern, options) {
|
|
|
338
353
|
return mergeHandlerDuplicates(entries);
|
|
339
354
|
}
|
|
340
355
|
/**
|
|
341
|
-
* Convert entries to an object
|
|
356
|
+
* Convert entries to an object. Duplicate keys — which `where_x_in` expansion can
|
|
357
|
+
* produce — are collapsed with last-wins, except:
|
|
358
|
+
* - ` =>` handler keys merge into the Multiple Handlers array form.
|
|
359
|
+
* - ` =*` multi-invoke keys concatenate their argument-list arrays, so an
|
|
360
|
+
* expanded pattern still invokes the method once per expansion.
|
|
342
361
|
*/
|
|
343
362
|
export function mergeHandlerDuplicates(entries) {
|
|
344
363
|
const result = {};
|
|
345
364
|
for (const [key, value] of entries) {
|
|
346
|
-
if (key.endsWith('
|
|
365
|
+
if (key in result && key.endsWith(' =*') && Array.isArray(result[key]) && Array.isArray(value)) {
|
|
366
|
+
result[key] = result[key].concat(value);
|
|
367
|
+
}
|
|
368
|
+
else if (key.endsWith(' =>') && key in result) {
|
|
347
369
|
const existing = result[key];
|
|
348
370
|
if (Array.isArray(existing)) {
|
|
349
371
|
existing.push(value);
|
|
@@ -389,6 +411,7 @@ export function categorizeKeys(expandedPattern) {
|
|
|
389
411
|
const idRefHandlerKeys = [];
|
|
390
412
|
const ternaryKeys = [];
|
|
391
413
|
const syncOpKeys = [];
|
|
414
|
+
const multiInvokeKeys = [];
|
|
392
415
|
for (const key of Object.keys(expandedPattern)) {
|
|
393
416
|
if (isHandlerCommand(key)) {
|
|
394
417
|
if (key.startsWith('#[')) {
|
|
@@ -404,6 +427,9 @@ export function categorizeKeys(expandedPattern) {
|
|
|
404
427
|
else if (isSyncOpCommand(key)) {
|
|
405
428
|
syncOpKeys.push(key);
|
|
406
429
|
}
|
|
430
|
+
else if (isMultiInvokeCommand(key)) {
|
|
431
|
+
multiInvokeKeys.push(key);
|
|
432
|
+
}
|
|
407
433
|
else if (key.startsWith('#[')) {
|
|
408
434
|
idRefNormalKeys.push(key);
|
|
409
435
|
}
|
|
@@ -411,7 +437,7 @@ export function categorizeKeys(expandedPattern) {
|
|
|
411
437
|
normalPattern[key] = expandedPattern[key];
|
|
412
438
|
}
|
|
413
439
|
}
|
|
414
|
-
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys };
|
|
440
|
+
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys, multiInvokeKeys };
|
|
415
441
|
}
|
|
416
442
|
/**
|
|
417
443
|
* Merge pin and at into a single lookup map for resolveIdVariable.
|
|
@@ -469,7 +495,7 @@ export function assignFrom(target, pattern, options, permissionProcessor) {
|
|
|
469
495
|
// Expand looped substitution variables
|
|
470
496
|
const expandedPattern = expandSubstitutions(pattern, options);
|
|
471
497
|
// Categorize keys
|
|
472
|
-
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys } = categorizeKeys(expandedPattern);
|
|
498
|
+
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys, multiInvokeKeys } = categorizeKeys(expandedPattern);
|
|
473
499
|
const resolveOptions = { ...options, root: target, permissionProcessor };
|
|
474
500
|
// Process ?= ternary keys (sync)
|
|
475
501
|
if (ternaryKeys.length > 0) {
|
|
@@ -517,6 +543,26 @@ export function assignFrom(target, pattern, options, permissionProcessor) {
|
|
|
517
543
|
}
|
|
518
544
|
}
|
|
519
545
|
}
|
|
546
|
+
// Process =* multi-invoke keys (sync): call a withMethods method sitting at the
|
|
547
|
+
// base path once per argument-list on the RHS. Each argument-list is resolved
|
|
548
|
+
// against `from` (so `?.` paths and `!!` markers work), then delegated to
|
|
549
|
+
// assignGingerly as a single array-valued key — which spreads it as call args.
|
|
550
|
+
if (multiInvokeKeys.length > 0) {
|
|
551
|
+
for (const key of multiInvokeKeys) {
|
|
552
|
+
const basePath = parseMultiInvokeCommand(key);
|
|
553
|
+
if (basePath === null)
|
|
554
|
+
continue;
|
|
555
|
+
const callList = expandedPattern[key];
|
|
556
|
+
if (!Array.isArray(callList)) {
|
|
557
|
+
throw new Error(`assignFrom: multi-invoke command "${key}" requires an array of argument-lists`);
|
|
558
|
+
}
|
|
559
|
+
for (const rawArgs of callList) {
|
|
560
|
+
const argList = Array.isArray(rawArgs) ? rawArgs : [rawArgs];
|
|
561
|
+
const resolved = getValues({ __args: argList }, options.from, resolveOptions).__args;
|
|
562
|
+
assignGingerly(target, { [basePath]: resolved }, options, permissionProcessor);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
520
566
|
// Process normal keys via getValues (sync) + assignGingerly
|
|
521
567
|
if (Object.keys(normalPattern).length > 0) {
|
|
522
568
|
// Resolve #[x] references on RHS values before getValues
|
package/assignFrom.ts
CHANGED
|
@@ -67,6 +67,22 @@ export function parseSyncOpCommand(key: string): string | null {
|
|
|
67
67
|
return key.substring(0, key.length - 3); // Remove ' =&' suffix
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Check if a key ends with the multi-invoke operator ' =*'.
|
|
72
|
+
*/
|
|
73
|
+
export function isMultiInvokeCommand(key: string): boolean {
|
|
74
|
+
return key.endsWith(' =*');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse a =* multi-invoke command and extract the base LHS path — the path a
|
|
79
|
+
* `withMethods` method sits at, to be called once per argument-list on the RHS.
|
|
80
|
+
*/
|
|
81
|
+
export function parseMultiInvokeCommand(key: string): string | null {
|
|
82
|
+
if (!isMultiInvokeCommand(key)) return null;
|
|
83
|
+
return key.substring(0, key.length - 3); // Remove ' =*' suffix
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
/**
|
|
71
87
|
* Throws if a resolved sync-op value (or anything nested inside it) is a thenable.
|
|
72
88
|
*
|
|
@@ -349,12 +365,18 @@ export function expandSubstitutions(
|
|
|
349
365
|
}
|
|
350
366
|
|
|
351
367
|
/**
|
|
352
|
-
* Convert entries to an object
|
|
368
|
+
* Convert entries to an object. Duplicate keys — which `where_x_in` expansion can
|
|
369
|
+
* produce — are collapsed with last-wins, except:
|
|
370
|
+
* - ` =>` handler keys merge into the Multiple Handlers array form.
|
|
371
|
+
* - ` =*` multi-invoke keys concatenate their argument-list arrays, so an
|
|
372
|
+
* expanded pattern still invokes the method once per expansion.
|
|
353
373
|
*/
|
|
354
374
|
export function mergeHandlerDuplicates(entries: [string, any][]): Record<string, any> {
|
|
355
375
|
const result: Record<string, any> = {};
|
|
356
376
|
for (const [key, value] of entries) {
|
|
357
|
-
if (key.endsWith('
|
|
377
|
+
if (key in result && key.endsWith(' =*') && Array.isArray(result[key]) && Array.isArray(value)) {
|
|
378
|
+
result[key] = (result[key] as any[]).concat(value);
|
|
379
|
+
} else if (key.endsWith(' =>') && key in result) {
|
|
358
380
|
const existing = result[key];
|
|
359
381
|
if (Array.isArray(existing)) {
|
|
360
382
|
existing.push(value);
|
|
@@ -400,6 +422,7 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
|
|
|
400
422
|
const idRefHandlerKeys: string[] = [];
|
|
401
423
|
const ternaryKeys: string[] = [];
|
|
402
424
|
const syncOpKeys: string[] = [];
|
|
425
|
+
const multiInvokeKeys: string[] = [];
|
|
403
426
|
|
|
404
427
|
for (const key of Object.keys(expandedPattern)) {
|
|
405
428
|
if (isHandlerCommand(key)) {
|
|
@@ -412,6 +435,8 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
|
|
|
412
435
|
ternaryKeys.push(key);
|
|
413
436
|
} else if (isSyncOpCommand(key)) {
|
|
414
437
|
syncOpKeys.push(key);
|
|
438
|
+
} else if (isMultiInvokeCommand(key)) {
|
|
439
|
+
multiInvokeKeys.push(key);
|
|
415
440
|
} else if (key.startsWith('#[')) {
|
|
416
441
|
idRefNormalKeys.push(key);
|
|
417
442
|
} else {
|
|
@@ -419,7 +444,7 @@ export function categorizeKeys(expandedPattern: Record<string, any>) {
|
|
|
419
444
|
}
|
|
420
445
|
}
|
|
421
446
|
|
|
422
|
-
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys };
|
|
447
|
+
return { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys, multiInvokeKeys };
|
|
423
448
|
}
|
|
424
449
|
|
|
425
450
|
/**
|
|
@@ -495,7 +520,7 @@ export function assignFrom(
|
|
|
495
520
|
const expandedPattern = expandSubstitutions(pattern, options);
|
|
496
521
|
|
|
497
522
|
// Categorize keys
|
|
498
|
-
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys } = categorizeKeys(expandedPattern);
|
|
523
|
+
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, ternaryKeys, syncOpKeys, multiInvokeKeys } = categorizeKeys(expandedPattern);
|
|
499
524
|
|
|
500
525
|
const resolveOptions = { ...options, root: target, permissionProcessor } as any;
|
|
501
526
|
|
|
@@ -547,6 +572,26 @@ export function assignFrom(
|
|
|
547
572
|
}
|
|
548
573
|
}
|
|
549
574
|
|
|
575
|
+
// Process =* multi-invoke keys (sync): call a withMethods method sitting at the
|
|
576
|
+
// base path once per argument-list on the RHS. Each argument-list is resolved
|
|
577
|
+
// against `from` (so `?.` paths and `!!` markers work), then delegated to
|
|
578
|
+
// assignGingerly as a single array-valued key — which spreads it as call args.
|
|
579
|
+
if (multiInvokeKeys.length > 0) {
|
|
580
|
+
for (const key of multiInvokeKeys) {
|
|
581
|
+
const basePath = parseMultiInvokeCommand(key);
|
|
582
|
+
if (basePath === null) continue;
|
|
583
|
+
const callList = expandedPattern[key];
|
|
584
|
+
if (!Array.isArray(callList)) {
|
|
585
|
+
throw new Error(`assignFrom: multi-invoke command "${key}" requires an array of argument-lists`);
|
|
586
|
+
}
|
|
587
|
+
for (const rawArgs of callList) {
|
|
588
|
+
const argList = Array.isArray(rawArgs) ? rawArgs : [rawArgs];
|
|
589
|
+
const resolved = getValues({ __args: argList }, options.from, resolveOptions).__args;
|
|
590
|
+
assignGingerly(target, { [basePath]: resolved }, options, permissionProcessor);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
550
595
|
// Process normal keys via getValues (sync) + assignGingerly
|
|
551
596
|
if (Object.keys(normalPattern).length > 0) {
|
|
552
597
|
// Resolve #[x] references on RHS values before getValues
|
package/assignFromAsync.js
CHANGED
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { resolveValues } from './resolve/resolveValues.js';
|
|
23
23
|
import assignGingerly from './assignGingerly.js';
|
|
24
|
-
import { expandSubstitutions, categorizeKeys, handleSpreads } from './assignFrom.js';
|
|
24
|
+
import { expandSubstitutions, categorizeKeys, handleSpreads, parseMultiInvokeCommand } from './assignFrom.js';
|
|
25
25
|
// Module cache for processHandlerCommands — avoids await on dynamic import after first call
|
|
26
26
|
let _processHandlerCommands;
|
|
27
27
|
export async function assignFromAsync(target, pattern, options, permissionProcessor) {
|
|
28
28
|
// First: expand looped substitution variables (${x}, ${y}, ${z})
|
|
29
29
|
const expandedPattern = expandSubstitutions(pattern, options);
|
|
30
30
|
// Categorize keys
|
|
31
|
-
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys } = categorizeKeys(expandedPattern);
|
|
31
|
+
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, multiInvokeKeys } = categorizeKeys(expandedPattern);
|
|
32
32
|
// Process normal keys via resolveValues + assignGingerly
|
|
33
33
|
if (Object.keys(normalPattern).length > 0) {
|
|
34
34
|
const resolved = await resolveValues(normalPattern, options.from, {
|
|
@@ -44,6 +44,28 @@ export async function assignFromAsync(target, pattern, options, permissionProces
|
|
|
44
44
|
handleSpreads(resolved);
|
|
45
45
|
assignGingerly(target, resolved, options, permissionProcessor);
|
|
46
46
|
}
|
|
47
|
+
// Process =* multi-invoke keys: call a withMethods method sitting at the base
|
|
48
|
+
// path once per argument-list on the RHS (see assignFrom for the sync form).
|
|
49
|
+
if (multiInvokeKeys && multiInvokeKeys.length > 0) {
|
|
50
|
+
const { withMethods, aka, akaMethods, substitutions, protocols, from } = options;
|
|
51
|
+
for (const key of multiInvokeKeys) {
|
|
52
|
+
const basePath = parseMultiInvokeCommand(key);
|
|
53
|
+
if (basePath === null)
|
|
54
|
+
continue;
|
|
55
|
+
const callList = expandedPattern[key];
|
|
56
|
+
if (!Array.isArray(callList)) {
|
|
57
|
+
throw new Error(`assignFrom: multi-invoke command "${key}" requires an array of argument-lists`);
|
|
58
|
+
}
|
|
59
|
+
for (const rawArgs of callList) {
|
|
60
|
+
const argList = Array.isArray(rawArgs) ? rawArgs : [rawArgs];
|
|
61
|
+
const resolved = await resolveValues({ __args: argList }, from, {
|
|
62
|
+
withMethods, aka, akaMethods, substitutions, protocols,
|
|
63
|
+
root: target, permissionProcessor
|
|
64
|
+
});
|
|
65
|
+
assignGingerly(target, { [basePath]: resolved.__args }, options, permissionProcessor);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
47
69
|
// Process #[x] normal keys — resolve element, then apply remaining path + value
|
|
48
70
|
if (idRefNormalKeys.length > 0 && (options.pin || options.at)) {
|
|
49
71
|
const ids = { ...options.pin, ...options.at };
|
package/assignFromAsync.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {IAssignGingerlyOptions} from './types/assign-gingerly/types.js';
|
|
|
24
24
|
import assignGingerly from './assignGingerly.js';
|
|
25
25
|
import type { PermissionProcessor, AssignFromHandler, AssignFromHandlerConstructor } from './types/assign-gingerly/types.js';
|
|
26
26
|
import {
|
|
27
|
-
expandSubstitutions, categorizeKeys, handleSpreads, isHandlerCommand
|
|
27
|
+
expandSubstitutions, categorizeKeys, handleSpreads, isHandlerCommand, parseMultiInvokeCommand
|
|
28
28
|
} from './assignFrom.js';
|
|
29
29
|
|
|
30
30
|
export interface AssignFromOptions extends IAssignGingerlyOptions {
|
|
@@ -61,7 +61,7 @@ export async function assignFromAsync(
|
|
|
61
61
|
const expandedPattern = expandSubstitutions(pattern, options);
|
|
62
62
|
|
|
63
63
|
// Categorize keys
|
|
64
|
-
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys } = categorizeKeys(expandedPattern);
|
|
64
|
+
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys, multiInvokeKeys } = categorizeKeys(expandedPattern);
|
|
65
65
|
|
|
66
66
|
// Process normal keys via resolveValues + assignGingerly
|
|
67
67
|
if (Object.keys(normalPattern).length > 0) {
|
|
@@ -81,6 +81,28 @@ export async function assignFromAsync(
|
|
|
81
81
|
assignGingerly(target, resolved, options, permissionProcessor);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Process =* multi-invoke keys: call a withMethods method sitting at the base
|
|
85
|
+
// path once per argument-list on the RHS (see assignFrom for the sync form).
|
|
86
|
+
if (multiInvokeKeys && multiInvokeKeys.length > 0) {
|
|
87
|
+
const { withMethods, aka, akaMethods, substitutions, protocols, from } = options;
|
|
88
|
+
for (const key of multiInvokeKeys) {
|
|
89
|
+
const basePath = parseMultiInvokeCommand(key);
|
|
90
|
+
if (basePath === null) continue;
|
|
91
|
+
const callList = expandedPattern[key];
|
|
92
|
+
if (!Array.isArray(callList)) {
|
|
93
|
+
throw new Error(`assignFrom: multi-invoke command "${key}" requires an array of argument-lists`);
|
|
94
|
+
}
|
|
95
|
+
for (const rawArgs of callList) {
|
|
96
|
+
const argList = Array.isArray(rawArgs) ? rawArgs : [rawArgs];
|
|
97
|
+
const resolved = await resolveValues({ __args: argList }, from, {
|
|
98
|
+
withMethods, aka, akaMethods, substitutions, protocols,
|
|
99
|
+
root: target, permissionProcessor
|
|
100
|
+
});
|
|
101
|
+
assignGingerly(target, { [basePath]: resolved.__args }, options, permissionProcessor);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
// Process #[x] normal keys — resolve element, then apply remaining path + value
|
|
85
107
|
if (idRefNormalKeys.length > 0 && (options.pin || options.at)) {
|
|
86
108
|
const ids = { ...options.pin, ...options.at };
|
package/inferencer/inferencer.js
CHANGED
|
@@ -50,12 +50,21 @@ export class Infer {
|
|
|
50
50
|
}
|
|
51
51
|
#value;
|
|
52
52
|
get value() {
|
|
53
|
-
|
|
53
|
+
const element = this.#weakRef.deref();
|
|
54
|
+
if (element === undefined)
|
|
55
|
+
return this.#value;
|
|
56
|
+
const propName = typeof this.#propName === 'string'
|
|
57
|
+
? this.#propName
|
|
58
|
+
: inferValueProperty(element);
|
|
59
|
+
return coerceElementValue(element, propName);
|
|
54
60
|
}
|
|
55
61
|
set value(nv) {
|
|
56
62
|
this.#value = nv;
|
|
57
63
|
const { enhancedElement } = this;
|
|
58
|
-
|
|
64
|
+
const propName = typeof this.#propName === 'string'
|
|
65
|
+
? this.#propName
|
|
66
|
+
: inferValueProperty(enhancedElement);
|
|
67
|
+
enhancedElement[propName] = serializeForProperty(propName, nv);
|
|
59
68
|
}
|
|
60
69
|
#display;
|
|
61
70
|
get display() {
|
|
@@ -166,6 +175,75 @@ export function inferValueProperty(element) {
|
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Read the inferred value property off an element and coerce it to a natural
|
|
180
|
+
* JavaScript type, mirroring the legacy be-value-added parsing rules:
|
|
181
|
+
* - `<time>` (dateTime) -> Date (or undefined when empty)
|
|
182
|
+
* - `<input type=number|range>` (valueAsNumber) -> number (undefined when NaN)
|
|
183
|
+
* - `<input type=checkbox|radio>` (checked) -> boolean
|
|
184
|
+
* - schema.org `itemtype` hints (Number/Integer/Float/Boolean/Date/DateTime) are honored
|
|
185
|
+
* - `textContent` is returned verbatim
|
|
186
|
+
* - everything else is JSON-parsed when possible (so `<data value="123">` -> 123,
|
|
187
|
+
* `<data value="true">` -> true), falling back to the raw string
|
|
188
|
+
*/
|
|
189
|
+
export function coerceElementValue(element, propName = inferValueProperty(element)) {
|
|
190
|
+
const raw = element[propName];
|
|
191
|
+
switch (propName) {
|
|
192
|
+
case 'valueAsNumber':
|
|
193
|
+
return Number.isNaN(raw) ? undefined : raw;
|
|
194
|
+
case 'valueAsDate':
|
|
195
|
+
return raw ?? undefined;
|
|
196
|
+
case 'checked':
|
|
197
|
+
case 'selectedIndex':
|
|
198
|
+
return raw;
|
|
199
|
+
case 'dateTime': {
|
|
200
|
+
const s = raw == null ? '' : String(raw);
|
|
201
|
+
return s === '' ? undefined : new Date(s);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (raw == null)
|
|
205
|
+
return undefined;
|
|
206
|
+
if (typeof raw !== 'string')
|
|
207
|
+
return raw;
|
|
208
|
+
switch (element.getAttribute('itemtype')) {
|
|
209
|
+
case 'https://schema.org/Number': return Number(raw);
|
|
210
|
+
case 'https://schema.org/Integer': return parseInt(raw, 10);
|
|
211
|
+
case 'https://schema.org/Float': return parseFloat(raw);
|
|
212
|
+
case 'https://schema.org/Boolean': return raw === 'true' || raw === 'True';
|
|
213
|
+
case 'https://schema.org/Date':
|
|
214
|
+
case 'https://schema.org/DateTime': return new Date(raw);
|
|
215
|
+
}
|
|
216
|
+
if (propName === 'textContent')
|
|
217
|
+
return raw;
|
|
218
|
+
if (raw === '')
|
|
219
|
+
return undefined;
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(raw);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return raw;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Serialize a JS value for assignment to a (usually string-typed) DOM value
|
|
229
|
+
* property, mirroring legacy be-value-added write-back:
|
|
230
|
+
* - DOM-typed properties (`checked`, `valueAsNumber`, `valueAsDate`) take the raw value
|
|
231
|
+
* - a `Date` is written as an ISO string (so `<time>.dateTime` round-trips)
|
|
232
|
+
* - plain objects / arrays are JSON-stringified
|
|
233
|
+
*/
|
|
234
|
+
export function serializeForProperty(propName, nv) {
|
|
235
|
+
switch (propName) {
|
|
236
|
+
case 'checked':
|
|
237
|
+
case 'valueAsNumber':
|
|
238
|
+
case 'valueAsDate':
|
|
239
|
+
return nv;
|
|
240
|
+
}
|
|
241
|
+
if (nv instanceof Date)
|
|
242
|
+
return nv.toISOString();
|
|
243
|
+
if (nv !== null && typeof nv === 'object')
|
|
244
|
+
return JSON.stringify(nv);
|
|
245
|
+
return nv;
|
|
246
|
+
}
|
|
169
247
|
/**
|
|
170
248
|
* Infer the most appropriate display property for an element
|
|
171
249
|
* @param element - The element to infer the property for
|
package/inferencer/inferencer.ts
CHANGED
|
@@ -65,13 +65,21 @@ export class Infer<TValue = any, TDisplay = any> {
|
|
|
65
65
|
#value: TValue | undefined;
|
|
66
66
|
|
|
67
67
|
get value(): TValue | undefined {
|
|
68
|
-
|
|
68
|
+
const element = this.#weakRef.deref();
|
|
69
|
+
if (element === undefined) return this.#value;
|
|
70
|
+
const propName = typeof this.#propName === 'string'
|
|
71
|
+
? this.#propName
|
|
72
|
+
: inferValueProperty(element);
|
|
73
|
+
return coerceElementValue(element, propName) as TValue | undefined;
|
|
69
74
|
}
|
|
70
|
-
|
|
75
|
+
|
|
71
76
|
set value(nv: TValue){
|
|
72
77
|
this.#value = nv;
|
|
73
78
|
const {enhancedElement} = this;
|
|
74
|
-
|
|
79
|
+
const propName = typeof this.#propName === 'string'
|
|
80
|
+
? this.#propName
|
|
81
|
+
: inferValueProperty(enhancedElement);
|
|
82
|
+
(enhancedElement as any)[propName] = serializeForProperty(propName, nv);
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
#display: TDisplay | undefined;
|
|
@@ -200,6 +208,75 @@ export function inferValueProperty(element: Element): string {
|
|
|
200
208
|
}
|
|
201
209
|
}
|
|
202
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Read the inferred value property off an element and coerce it to a natural
|
|
213
|
+
* JavaScript type, mirroring the legacy be-value-added parsing rules:
|
|
214
|
+
* - `<time>` (dateTime) -> Date (or undefined when empty)
|
|
215
|
+
* - `<input type=number|range>` (valueAsNumber) -> number (undefined when NaN)
|
|
216
|
+
* - `<input type=checkbox|radio>` (checked) -> boolean
|
|
217
|
+
* - schema.org `itemtype` hints (Number/Integer/Float/Boolean/Date/DateTime) are honored
|
|
218
|
+
* - `textContent` is returned verbatim
|
|
219
|
+
* - everything else is JSON-parsed when possible (so `<data value="123">` -> 123,
|
|
220
|
+
* `<data value="true">` -> true), falling back to the raw string
|
|
221
|
+
*/
|
|
222
|
+
export function coerceElementValue(element: Element, propName: string = inferValueProperty(element)): any {
|
|
223
|
+
const raw = (element as any)[propName];
|
|
224
|
+
|
|
225
|
+
switch (propName) {
|
|
226
|
+
case 'valueAsNumber':
|
|
227
|
+
return Number.isNaN(raw) ? undefined : raw;
|
|
228
|
+
case 'valueAsDate':
|
|
229
|
+
return raw ?? undefined;
|
|
230
|
+
case 'checked':
|
|
231
|
+
case 'selectedIndex':
|
|
232
|
+
return raw;
|
|
233
|
+
case 'dateTime': {
|
|
234
|
+
const s = raw == null ? '' : String(raw);
|
|
235
|
+
return s === '' ? undefined : new Date(s);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (raw == null) return undefined;
|
|
240
|
+
if (typeof raw !== 'string') return raw;
|
|
241
|
+
|
|
242
|
+
switch (element.getAttribute('itemtype')) {
|
|
243
|
+
case 'https://schema.org/Number': return Number(raw);
|
|
244
|
+
case 'https://schema.org/Integer': return parseInt(raw, 10);
|
|
245
|
+
case 'https://schema.org/Float': return parseFloat(raw);
|
|
246
|
+
case 'https://schema.org/Boolean': return raw === 'true' || raw === 'True';
|
|
247
|
+
case 'https://schema.org/Date':
|
|
248
|
+
case 'https://schema.org/DateTime': return new Date(raw);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (propName === 'textContent') return raw;
|
|
252
|
+
if (raw === '') return undefined;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
return JSON.parse(raw);
|
|
256
|
+
} catch {
|
|
257
|
+
return raw;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Serialize a JS value for assignment to a (usually string-typed) DOM value
|
|
263
|
+
* property, mirroring legacy be-value-added write-back:
|
|
264
|
+
* - DOM-typed properties (`checked`, `valueAsNumber`, `valueAsDate`) take the raw value
|
|
265
|
+
* - a `Date` is written as an ISO string (so `<time>.dateTime` round-trips)
|
|
266
|
+
* - plain objects / arrays are JSON-stringified
|
|
267
|
+
*/
|
|
268
|
+
export function serializeForProperty(propName: string, nv: any): any {
|
|
269
|
+
switch (propName) {
|
|
270
|
+
case 'checked':
|
|
271
|
+
case 'valueAsNumber':
|
|
272
|
+
case 'valueAsDate':
|
|
273
|
+
return nv;
|
|
274
|
+
}
|
|
275
|
+
if (nv instanceof Date) return nv.toISOString();
|
|
276
|
+
if (nv !== null && typeof nv === 'object') return JSON.stringify(nv);
|
|
277
|
+
return nv;
|
|
278
|
+
}
|
|
279
|
+
|
|
203
280
|
/**
|
|
204
281
|
* Infer the most appropriate display property for an element
|
|
205
282
|
* @param element - The element to infer the property for
|
|
@@ -139,6 +139,18 @@ Plus infrastructure:
|
|
|
139
139
|
- `attachInternals()` called in the constructor
|
|
140
140
|
- Async `fallbackSpawn` for lazy-loading all feature implementations
|
|
141
141
|
|
|
142
|
+
### Injecting a custom or package-local feature
|
|
143
|
+
|
|
144
|
+
`assignFeatures` accepts a `spawn` per feature key — a class, an async loader, or
|
|
145
|
+
an **import-path string**. A string `spawn` overrides the catalog `fallbackSpawn`
|
|
146
|
+
and is dynamically `import()`ed through the page's import map, so `el-maker.json`
|
|
147
|
+
stays pure JSON. Use it to add a feature that isn't in the catalog, or to swap in
|
|
148
|
+
a package-local **subclass** of a catalog feature when the generic one needs
|
|
149
|
+
element-specific logic. See
|
|
150
|
+
[NewHTMLFirstCustomElement.md → "give a shared feature element-specific logic"](./NewHTMLFirstCustomElement.md#how-do-i-give-a-shared-feature-element-specific-logic-penciling-in)
|
|
151
|
+
and the [css-charts](https://github.com/bahrus/css-charts) conversion for a
|
|
152
|
+
worked example ("penciling in" `CSSChartsH2OTable extends H2OTable`).
|
|
153
|
+
|
|
142
154
|
|
|
143
155
|
|
|
144
156
|
|
|
@@ -71,10 +71,14 @@ A custom element feature is a class that:
|
|
|
71
71
|
|
|
72
72
|
## Step 3: Create Type Definitions
|
|
73
73
|
|
|
74
|
+
**All types for the feature live in `types/[project-name]/types.d.ts` — nothing else.** Do not scatter `@typedef {Object} ...` blocks through the `.js` file. That includes the `customData` / injection-config shape, the `detail` payload of any event the feature dispatches, and every internal helper type. The `.js` file only ever *imports* these via `@import`; it never defines them. A reader (or the package that later adopts the feature) should be able to learn the whole type surface from the one `.d.ts`.
|
|
75
|
+
|
|
74
76
|
Create `types/[project-name]/types.d.ts` with the feature structure:
|
|
75
77
|
|
|
76
78
|
```typescript
|
|
77
|
-
import { SpawnContext } from "../assign-gingerly/types";
|
|
79
|
+
import { SpawnContext, FeatureSpawnContext } from "../assign-gingerly/types";
|
|
80
|
+
|
|
81
|
+
export { FeatureSpawnContext };
|
|
78
82
|
|
|
79
83
|
/**
|
|
80
84
|
* Configuration/properties that the feature exposes
|
|
@@ -96,9 +100,26 @@ export type AP = AllProps;
|
|
|
96
100
|
export type PAP = Partial<AP>;
|
|
97
101
|
|
|
98
102
|
/**
|
|
99
|
-
*
|
|
103
|
+
* The `customData` passed through the injection config — parsed by the
|
|
104
|
+
* constructor into initial state. Keep every configurable knob here.
|
|
105
|
+
*/
|
|
106
|
+
export interface CustomData {
|
|
107
|
+
myProp?: string;
|
|
108
|
+
eventType?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `detail` payload for any CustomEvent the feature dispatches on the host.
|
|
100
113
|
*/
|
|
101
|
-
export interface
|
|
114
|
+
export interface MyFeatureResolvedDetail {
|
|
115
|
+
// ...
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If a project's `types/assign-gingerly/types.d.ts` does not yet export `FeatureSpawnContext`, define it locally in this file instead (as `truth-sourcer` and `face-up` do):
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
export interface FeatureSpawnContext {
|
|
102
123
|
key: string;
|
|
103
124
|
optIn: any;
|
|
104
125
|
injection: any;
|
|
@@ -110,7 +131,9 @@ export interface FeatureSpawnContext extends SpawnContext {
|
|
|
110
131
|
**Key points:**
|
|
111
132
|
- `FeatureProps` — the public API of the feature
|
|
112
133
|
- `AllProps` — includes internal state like a WeakRef to the host element
|
|
134
|
+
- `CustomData` — the injection-config shape; the constructor reads `ctx.injection.customData` and narrows it to this type
|
|
113
135
|
- The feature class does NOT need to extend any base class
|
|
136
|
+
- The `.js` file imports all of the above with `/** @import {...} from './types/[project-name]/types' */` — it defines no types of its own
|
|
114
137
|
|
|
115
138
|
## Step 4: Create the Feature Class
|
|
116
139
|
|
|
@@ -150,7 +173,7 @@ export { MyFeature };
|
|
|
150
173
|
- Constructor signature: `(hostElement, ctx, initVals)`
|
|
151
174
|
- Store host as a `WeakRef` to avoid preventing garbage collection
|
|
152
175
|
- Apply `initVals` via `Object.assign` in the constructor
|
|
153
|
-
- Use `@ts-check` with JSDoc type imports from the `types/` folder
|
|
176
|
+
- Use `@ts-check` with JSDoc type imports from the `types/` folder — all type definitions live in `types/[project-name]/types.d.ts`, never as inline `@typedef` blocks in the `.js`
|
|
154
177
|
- No compiled TypeScript — ship raw `.js` files
|
|
155
178
|
|
|
156
179
|
## Step 5: Create imports.html
|
|
@@ -679,6 +702,7 @@ customElements.define('my-element', MyElement);
|
|
|
679
702
|
|
|
680
703
|
- **Call `assignFeatures` before `customElements.define()`** — getters must be on the prototype before instances exist
|
|
681
704
|
- **Use `@ts-check`** — catches type errors early in `.js` files
|
|
705
|
+
- **Keep all types in `types/[project-name]/types.d.ts`** — including `customData` and event `detail` shapes; the `.js` only `@import`s them, it never declares `@typedef`s
|
|
682
706
|
- **Store host as WeakRef** — prevents memory leaks
|
|
683
707
|
- **Keep features focused** — one responsibility per feature class
|
|
684
708
|
- **Use `validateShape`** — catches injection errors early in development
|