assign-gingerly 0.0.58 → 0.0.60
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 +511 -142
- package/assignFrom-extension.js +25 -0
- package/assignFrom-extension.ts +53 -0
- package/assignFrom.js +306 -133
- package/assignFrom.ts +318 -232
- package/assignFromAsync-extension.js +28 -0
- package/assignFromAsync-extension.ts +58 -0
- package/assignFromAsync.js +120 -0
- package/assignFromAsync.ts +256 -0
- package/assignGingerly.js +50 -0
- package/assignGingerly.ts +51 -0
- package/builtInEmoji.js +25 -0
- package/builtInEmoji.ts +33 -0
- package/getValues.js +223 -0
- package/getValues.ts +255 -0
- package/handlers/join.ts +1 -1
- package/handlers/lazyLoad.js +33 -2
- package/handlers/lazyLoad.ts +47 -2
- package/handlers/lazyLoadSwitch.ts +1 -1
- package/handlers/manageTemplateList.js +226 -0
- package/handlers/manageTemplateList.ts +263 -0
- package/handlers/microDataJoin.ts +1 -1
- package/index.js +1 -0
- package/index.ts +2 -0
- package/inferencer/inferencer.js +9 -21
- package/inferencer/inferencer.ts +10 -21
- package/inferredAssignments.js +35 -5
- package/inferredAssignments.ts +58 -8
- package/markerUtils.js +136 -127
- package/package.json +30 -1
- package/playwright.config.ts +3 -2
- package/processHandlerCommands.js +36 -4
- package/processHandlerCommands.ts +38 -8
- package/resolveIdRef.js +70 -19
- package/resolveIdRef.ts +73 -21
- package/resolveValues.js +41 -125
- package/resolveValues.ts +131 -255
- package/types/assign-gingerly/types.d.ts +61 -0
- package/waitForSettled.js +57 -0
- package/waitForSettled.ts +65 -0
- package/withIdsCorrector.js +47 -0
- package/withIdsCorrector.ts +59 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assignFromAsync-extension.js — Adds assignFromAsync to Object.prototype.
|
|
3
|
+
*
|
|
4
|
+
* Import this module for the side effect of extending all objects with
|
|
5
|
+
* the assignFromAsync method, enabling awaitable handler execution:
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import 'assign-gingerly/assignFromAsync-extension.js';
|
|
9
|
+
*
|
|
10
|
+
* await oElement.assignFromAsync({
|
|
11
|
+
* '?.querySelector?..mainView =>': {
|
|
12
|
+
* do: 'builtIns.lazyLoad',
|
|
13
|
+
* get: { if: '?.isVisible', instantiate: 'globalThis://myTemplate' }
|
|
14
|
+
* }
|
|
15
|
+
* }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { assignFromAsync } from './assignFromAsync.js';
|
|
19
|
+
|
|
20
|
+
Object.defineProperty(Object.prototype, 'assignFromAsync', {
|
|
21
|
+
value: async function (pattern, options) {
|
|
22
|
+
await assignFromAsync(this, pattern, options);
|
|
23
|
+
return this;
|
|
24
|
+
},
|
|
25
|
+
writable: true,
|
|
26
|
+
enumerable: false,
|
|
27
|
+
configurable: true,
|
|
28
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assignFromAsync-extension.ts — Adds assignFromAsync to Object.prototype.
|
|
3
|
+
*
|
|
4
|
+
* Import this module for the side effect of extending all objects with
|
|
5
|
+
* the assignFromAsync method, enabling awaitable handler execution:
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import 'assign-gingerly/assignFromAsync-extension.js';
|
|
9
|
+
*
|
|
10
|
+
* await oElement.assignFromAsync({
|
|
11
|
+
* '?.querySelector?..mainView =>': {
|
|
12
|
+
* do: 'builtIns.lazyLoad',
|
|
13
|
+
* get: { if: '?.isVisible', instantiate: 'globalThis://myTemplate' }
|
|
14
|
+
* }
|
|
15
|
+
* }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { assignFromAsync } from './assignFromAsync.js';
|
|
19
|
+
import type { AssignFromOptions } from './assignFromAsync.js';
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
interface Object {
|
|
23
|
+
/**
|
|
24
|
+
* Resolve RHS path strings from a source object and assign into this object.
|
|
25
|
+
* Async — awaits handler execution and supports async protocol handlers.
|
|
26
|
+
*
|
|
27
|
+
* @param pattern - Object with LHS paths as keys and RHS path strings (or literals) as values
|
|
28
|
+
* @param options - Configuration including `from` (source object), protocols, withMethods, etc.
|
|
29
|
+
* @returns Promise resolving to this object after assignment
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* await oElement.assignFromAsync({
|
|
33
|
+
* '?.querySelector?..outlet =>': {
|
|
34
|
+
* do: 'builtIns.lazyLoad',
|
|
35
|
+
* get: { if: '?.showContent', instantiate: 'globalThis://myTemplate' }
|
|
36
|
+
* }
|
|
37
|
+
* }, { from: viewModel, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
|
|
38
|
+
*/
|
|
39
|
+
assignFromAsync(
|
|
40
|
+
pattern: Record<string, any>,
|
|
41
|
+
options: AssignFromOptions
|
|
42
|
+
): Promise<this>;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Object.defineProperty(Object.prototype, 'assignFromAsync', {
|
|
47
|
+
value: async function <T extends object>(
|
|
48
|
+
this: T,
|
|
49
|
+
pattern: Record<string, any>,
|
|
50
|
+
options: AssignFromOptions
|
|
51
|
+
): Promise<T> {
|
|
52
|
+
await assignFromAsync(this, pattern, options);
|
|
53
|
+
return this;
|
|
54
|
+
},
|
|
55
|
+
writable: true,
|
|
56
|
+
enumerable: false,
|
|
57
|
+
configurable: true,
|
|
58
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve RHS path strings against a source object, then assign the
|
|
3
|
+
* resolved values into a target using assignGingerly.
|
|
4
|
+
*
|
|
5
|
+
* Combines resolveValues + assignGingerly into a single call.
|
|
6
|
+
* Inherits all assignGingerly options (withMethods, aka, signal, etc.).
|
|
7
|
+
*
|
|
8
|
+
* @param target - Object to merge resolved values into
|
|
9
|
+
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
10
|
+
* @param options - Options including `from` (source object) and any assignGingerly options
|
|
11
|
+
* @returns The target object after merging
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const source = { theme: { color: 'red' }, label: 'Hello' };
|
|
15
|
+
* const target = { color: 'blue', text: '' };
|
|
16
|
+
* assignFrom(target, {
|
|
17
|
+
* color: '?.theme?.color',
|
|
18
|
+
* text: '?.label'
|
|
19
|
+
* }, { from: source });
|
|
20
|
+
* // target is now { color: 'red', text: 'Hello' }
|
|
21
|
+
*/
|
|
22
|
+
import { resolveValues } from './resolveValues.js';
|
|
23
|
+
import assignGingerly from './assignGingerly.js';
|
|
24
|
+
import { expandSubstitutions, categorizeKeys, handleSpreads } from './assignFrom.js';
|
|
25
|
+
// Module cache for processHandlerCommands — avoids await on dynamic import after first call
|
|
26
|
+
let _processHandlerCommands;
|
|
27
|
+
export async function assignFromAsync(target, pattern, options, permissions) {
|
|
28
|
+
// First: expand looped substitution variables (${x}, ${y}, ${z})
|
|
29
|
+
const expandedPattern = expandSubstitutions(pattern, options);
|
|
30
|
+
// Categorize keys
|
|
31
|
+
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys } = categorizeKeys(expandedPattern);
|
|
32
|
+
// Process normal keys via resolveValues + assignGingerly
|
|
33
|
+
if (Object.keys(normalPattern).length > 0) {
|
|
34
|
+
const resolved = await resolveValues(normalPattern, options.from, {
|
|
35
|
+
withMethods: options.withMethods,
|
|
36
|
+
aka: options.aka,
|
|
37
|
+
protocols: options.protocols
|
|
38
|
+
});
|
|
39
|
+
// Recursively handle "..." spread keys at all nesting levels
|
|
40
|
+
handleSpreads(resolved);
|
|
41
|
+
assignGingerly(target, resolved, options);
|
|
42
|
+
}
|
|
43
|
+
// Process #[x] normal keys — resolve element, then apply remaining path + value
|
|
44
|
+
if (idRefNormalKeys.length > 0 && (options.withIds || options.at)) {
|
|
45
|
+
const ids = { ...options.withIds, ...options.at };
|
|
46
|
+
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
47
|
+
for (const key of idRefNormalKeys) {
|
|
48
|
+
const parsed = parseIdRef(key);
|
|
49
|
+
if (!parsed)
|
|
50
|
+
continue;
|
|
51
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
52
|
+
if (!el)
|
|
53
|
+
continue;
|
|
54
|
+
const value = expandedPattern[key];
|
|
55
|
+
if (parsed.remainingPath) {
|
|
56
|
+
// Resolve the RHS value
|
|
57
|
+
const resolvedValue = await resolveValues({ __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
|
|
58
|
+
// Apply remaining path on the resolved element
|
|
59
|
+
assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// No remaining path — resolve and assign directly to the element
|
|
63
|
+
const resolvedValue = await resolveValues(typeof value === 'object' && value !== null ? value : { __v: value }, options.from, { withMethods: options.withMethods, aka: options.aka, protocols: options.protocols });
|
|
64
|
+
if ('__v' in resolvedValue) {
|
|
65
|
+
// Single value — can't assign to element root without a path
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
assignGingerly(el, resolvedValue, options);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Process handler commands ( =>) — cached after first load to avoid await overhead
|
|
74
|
+
if (handlerKeys.length > 0) {
|
|
75
|
+
_processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
|
|
76
|
+
await _processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
|
|
77
|
+
}
|
|
78
|
+
// Process #[x] handler keys — resolve element, then pass to handler processing
|
|
79
|
+
if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
|
|
80
|
+
const ids = { ...options.withIds, ...options.at };
|
|
81
|
+
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
82
|
+
_processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
|
|
83
|
+
for (const key of idRefHandlerKeys) {
|
|
84
|
+
const parsed = parseIdRef(key);
|
|
85
|
+
if (!parsed)
|
|
86
|
+
continue;
|
|
87
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
88
|
+
if (!el)
|
|
89
|
+
continue;
|
|
90
|
+
// Build a synthetic key for processHandlerCommands:
|
|
91
|
+
// The resolved element becomes the target, remaining path is the LHS
|
|
92
|
+
const syntheticKey = parsed.remainingPath
|
|
93
|
+
? `${parsed.remainingPath} =>`
|
|
94
|
+
: ' =>';
|
|
95
|
+
const syntheticPattern = {
|
|
96
|
+
[syntheticKey]: expandedPattern[key]
|
|
97
|
+
};
|
|
98
|
+
await _processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Process inferred assignments — dynamically imported only when option is present
|
|
102
|
+
if (options.infer) {
|
|
103
|
+
const { processInferredAssignments } = await import('./inferredAssignments.js');
|
|
104
|
+
await processInferredAssignments(target, options.from, options.infer);
|
|
105
|
+
// Set up MutationObserver for new matching elements if beVigilant
|
|
106
|
+
if (options.infer.beVigilant) {
|
|
107
|
+
if (!options.signal) {
|
|
108
|
+
throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
|
|
109
|
+
}
|
|
110
|
+
const { setupVigilantObserver } = await import('./beVigilant.js');
|
|
111
|
+
setupVigilantObserver(target, options.from, options.infer, options.signal);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Process bulk enhancements — dynamically imported only when option is present
|
|
115
|
+
if (options.enhance && options.enhance.length > 0) {
|
|
116
|
+
const { enhanceAll } = await import('./enhanceAll.js');
|
|
117
|
+
await enhanceAll(target, options.enhance, permissions);
|
|
118
|
+
}
|
|
119
|
+
return target;
|
|
120
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve RHS path strings against a source object, then assign the
|
|
3
|
+
* resolved values into a target using assignGingerly.
|
|
4
|
+
*
|
|
5
|
+
* Combines resolveValues + assignGingerly into a single call.
|
|
6
|
+
* Inherits all assignGingerly options (withMethods, aka, signal, etc.).
|
|
7
|
+
*
|
|
8
|
+
* @param target - Object to merge resolved values into
|
|
9
|
+
* @param pattern - Object whose RHS values may contain `?.` path strings
|
|
10
|
+
* @param options - Options including `from` (source object) and any assignGingerly options
|
|
11
|
+
* @returns The target object after merging
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const source = { theme: { color: 'red' }, label: 'Hello' };
|
|
15
|
+
* const target = { color: 'blue', text: '' };
|
|
16
|
+
* assignFrom(target, {
|
|
17
|
+
* color: '?.theme?.color',
|
|
18
|
+
* text: '?.label'
|
|
19
|
+
* }, { from: source });
|
|
20
|
+
* // target is now { color: 'red', text: 'Hello' }
|
|
21
|
+
*/
|
|
22
|
+
import { resolveValues } from './resolveValues.js';
|
|
23
|
+
import assignGingerly, { IAssignGingerlyOptions } from './assignGingerly.js';
|
|
24
|
+
import type { AssignPermissions } from './isAllowedImportPath.js';
|
|
25
|
+
import {
|
|
26
|
+
expandSubstitutions, categorizeKeys, handleSpreads, isHandlerCommand
|
|
27
|
+
} from './assignFrom.js';
|
|
28
|
+
|
|
29
|
+
export interface AssignFromOptions extends IAssignGingerlyOptions {
|
|
30
|
+
/** Source object to resolve RHS path strings against */
|
|
31
|
+
from: any;
|
|
32
|
+
|
|
33
|
+
/** Protocol handlers (sync or async) */
|
|
34
|
+
protocols?: Record<string, (key: string) => any | Promise<any>>;
|
|
35
|
+
|
|
36
|
+
/** Loop variable bindings — expand pattern entries containing ${x} */
|
|
37
|
+
where_x_in?: string[];
|
|
38
|
+
/** Loop variable bindings — expand pattern entries containing ${y} */
|
|
39
|
+
where_y_in?: string[];
|
|
40
|
+
/** Loop variable bindings — expand pattern entries containing ${z} */
|
|
41
|
+
where_z_in?: string[];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Cached element references by variable name.
|
|
45
|
+
* Used with `#[varName]` syntax in LHS keys for fast repeated element access.
|
|
46
|
+
*
|
|
47
|
+
* - String value: existing element ID (uses getElementById)
|
|
48
|
+
* - Object value: { qry: 'selector' } — finds element via querySelector on target, auto-assigns an ID
|
|
49
|
+
*
|
|
50
|
+
* Elements are cached via WeakRef with getElementById fallback on cache miss.
|
|
51
|
+
*/
|
|
52
|
+
withIds?: Record<string, string | { qry: string }>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Positional element references for use with `#[varName]` syntax.
|
|
56
|
+
* Resolves elements by child index path — no IDs assigned, no caching.
|
|
57
|
+
*
|
|
58
|
+
* - Array value: child index path (e.g., [0, 1] = target.children[0].children[1])
|
|
59
|
+
* - Object value: { path: [...], expect?: 'selector', fallback?: true }
|
|
60
|
+
* expect: validates via element.matches(), logs correction if wrong
|
|
61
|
+
* fallback: on mismatch, recovers via querySelector(expect)
|
|
62
|
+
*/
|
|
63
|
+
at?: Record<string, number[] | { path: number[]; expect?: string; fallback?: boolean }>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Handler implementations scoped to this call.
|
|
67
|
+
* Key: the `do` name referenced in handler configs.
|
|
68
|
+
* Value: a class constructor, or an import path to dynamically load one.
|
|
69
|
+
*
|
|
70
|
+
* Import paths must be local (relative, absolute, or bare specifier — no cross-domain URLs).
|
|
71
|
+
* The module's default export is checked first; otherwise the first exported class
|
|
72
|
+
* with an `assign` method on its prototype is used.
|
|
73
|
+
*
|
|
74
|
+
* Built-in handlers (builtIns.*) auto-load without needing to be listed here.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* handlers: {
|
|
78
|
+
* 'my-list': MyListHandler, // class constructor
|
|
79
|
+
* 'my-chart': './handlers/chart.js', // dynamic import path
|
|
80
|
+
* 'vendor-widget': 'some-package/handler.js', // bare specifier (import map)
|
|
81
|
+
* }
|
|
82
|
+
*/
|
|
83
|
+
handlers?: Record<string, AssignFromHandlerConstructor | string>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Inferred assignments — automatically distribute source values to matching
|
|
87
|
+
* DOM elements based on structural conventions (itemprop, name, etc.).
|
|
88
|
+
*
|
|
89
|
+
* Uses the inferencer submodule to determine the correct property for each
|
|
90
|
+
* matched element (textContent, value, checked, dateTime, ish, etc.).
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* infer: {
|
|
94
|
+
* byItemprop: ['user', 'name', 'email'], // or true for all source keys
|
|
95
|
+
* beVigilant: true, // watch for new matching elements (requires signal)
|
|
96
|
+
* }
|
|
97
|
+
*/
|
|
98
|
+
infer?: {
|
|
99
|
+
byItemprop?: string[] | true;
|
|
100
|
+
'|'?: string[] | true;
|
|
101
|
+
byName?: string[] | true | { props: string[] | true; outside: string };
|
|
102
|
+
'@'?: string[] | true | { props: string[] | true; outside: string };
|
|
103
|
+
/** Watch for new matching elements via MutationObserver. Requires options.signal for cleanup. */
|
|
104
|
+
beVigilant?: boolean;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Bulk enhancement application via EMC JSON configs.
|
|
109
|
+
* Finds matching elements and spawns enhancements on them.
|
|
110
|
+
*
|
|
111
|
+
* Each entry specifies an EMC JSON path and optionally overrides the matching selector.
|
|
112
|
+
* Enhancements are auto-registered if not already present in the enhancement registry.
|
|
113
|
+
*
|
|
114
|
+
* No scope perimeter is applied — use mount-observer for reactive/scoped enhancement.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* enhance: [
|
|
118
|
+
* { emc: 'be-bound/emc.json', matching: '[name]' },
|
|
119
|
+
* { emc: 'be-observant/emc.json', matching: '[itemprop]' },
|
|
120
|
+
* ]
|
|
121
|
+
*/
|
|
122
|
+
enhance?: Array<{ emc: string; matching?: string; parse?: boolean }>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Interface for assignFrom handler classes.
|
|
127
|
+
* Handlers are invoked when a LHS key ends with ' =>'.
|
|
128
|
+
*/
|
|
129
|
+
export interface AssignFromHandler {
|
|
130
|
+
assign(lhsTarget: any, resolvedParams: Record<string, any>, options: AssignFromOptions): Promise<void> | void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface AssignFromHandlerConstructor {
|
|
134
|
+
new (config: any): AssignFromHandler;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Module cache for processHandlerCommands — avoids await on dynamic import after first call
|
|
138
|
+
let _processHandlerCommands: any;
|
|
139
|
+
|
|
140
|
+
export async function assignFromAsync(
|
|
141
|
+
target: any,
|
|
142
|
+
pattern: Record<string, any>,
|
|
143
|
+
options: AssignFromOptions,
|
|
144
|
+
permissions?: AssignPermissions
|
|
145
|
+
): Promise<any> {
|
|
146
|
+
// First: expand looped substitution variables (${x}, ${y}, ${z})
|
|
147
|
+
const expandedPattern = expandSubstitutions(pattern, options);
|
|
148
|
+
|
|
149
|
+
// Categorize keys
|
|
150
|
+
const { handlerKeys, normalPattern, idRefNormalKeys, idRefHandlerKeys } = categorizeKeys(expandedPattern);
|
|
151
|
+
|
|
152
|
+
// Process normal keys via resolveValues + assignGingerly
|
|
153
|
+
if (Object.keys(normalPattern).length > 0) {
|
|
154
|
+
const resolved = await resolveValues(normalPattern, options.from, {
|
|
155
|
+
withMethods: options.withMethods,
|
|
156
|
+
aka: options.aka,
|
|
157
|
+
protocols: options.protocols
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Recursively handle "..." spread keys at all nesting levels
|
|
161
|
+
handleSpreads(resolved);
|
|
162
|
+
|
|
163
|
+
assignGingerly(target, resolved, options);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Process #[x] normal keys — resolve element, then apply remaining path + value
|
|
167
|
+
if (idRefNormalKeys.length > 0 && (options.withIds || options.at)) {
|
|
168
|
+
const ids = { ...options.withIds, ...options.at };
|
|
169
|
+
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
170
|
+
for (const key of idRefNormalKeys) {
|
|
171
|
+
const parsed = parseIdRef(key);
|
|
172
|
+
if (!parsed) continue;
|
|
173
|
+
|
|
174
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
175
|
+
if (!el) continue;
|
|
176
|
+
|
|
177
|
+
const value = expandedPattern[key];
|
|
178
|
+
if (parsed.remainingPath) {
|
|
179
|
+
// Resolve the RHS value
|
|
180
|
+
const resolvedValue = await resolveValues(
|
|
181
|
+
{ __v: value }, options.from,
|
|
182
|
+
{ withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
|
|
183
|
+
);
|
|
184
|
+
// Apply remaining path on the resolved element
|
|
185
|
+
assignGingerly(el, { [parsed.remainingPath]: resolvedValue.__v }, options);
|
|
186
|
+
} else {
|
|
187
|
+
// No remaining path — resolve and assign directly to the element
|
|
188
|
+
const resolvedValue = await resolveValues(
|
|
189
|
+
typeof value === 'object' && value !== null ? value : { __v: value },
|
|
190
|
+
options.from,
|
|
191
|
+
{ withMethods: options.withMethods, aka: options.aka, protocols: options.protocols }
|
|
192
|
+
);
|
|
193
|
+
if ('__v' in resolvedValue) {
|
|
194
|
+
// Single value — can't assign to element root without a path
|
|
195
|
+
} else {
|
|
196
|
+
assignGingerly(el, resolvedValue, options);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Process handler commands ( =>) — cached after first load to avoid await overhead
|
|
202
|
+
if (handlerKeys.length > 0) {
|
|
203
|
+
|
|
204
|
+
_processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
|
|
205
|
+
await _processHandlerCommands(target, handlerKeys, expandedPattern, options, permissions);
|
|
206
|
+
}
|
|
207
|
+
// Process #[x] handler keys — resolve element, then pass to handler processing
|
|
208
|
+
if (idRefHandlerKeys.length > 0 && (options.withIds || options.at)) {
|
|
209
|
+
const ids = { ...options.withIds, ...options.at };
|
|
210
|
+
const { resolveIdVariable, parseIdRef } = await import('./resolveIdRef.js');
|
|
211
|
+
_processHandlerCommands ??= (await import('./processHandlerCommands.js')).processHandlerCommands;
|
|
212
|
+
|
|
213
|
+
for (const key of idRefHandlerKeys) {
|
|
214
|
+
const parsed = parseIdRef(key);
|
|
215
|
+
if (!parsed) continue;
|
|
216
|
+
|
|
217
|
+
const el = resolveIdVariable(parsed.varName, target, ids);
|
|
218
|
+
if (!el) continue;
|
|
219
|
+
|
|
220
|
+
// Build a synthetic key for processHandlerCommands:
|
|
221
|
+
// The resolved element becomes the target, remaining path is the LHS
|
|
222
|
+
const syntheticKey = parsed.remainingPath
|
|
223
|
+
? `${parsed.remainingPath} =>`
|
|
224
|
+
: ' =>';
|
|
225
|
+
|
|
226
|
+
const syntheticPattern: Record<string, any> = {
|
|
227
|
+
[syntheticKey]: expandedPattern[key]
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
await _processHandlerCommands(el, [syntheticKey], syntheticPattern, options, permissions);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Process inferred assignments — dynamically imported only when option is present
|
|
235
|
+
if (options.infer) {
|
|
236
|
+
const { processInferredAssignments } = await import('./inferredAssignments.js');
|
|
237
|
+
await processInferredAssignments(target, options.from, options.infer);
|
|
238
|
+
|
|
239
|
+
// Set up MutationObserver for new matching elements if beVigilant
|
|
240
|
+
if (options.infer.beVigilant) {
|
|
241
|
+
if (!options.signal) {
|
|
242
|
+
throw new Error('assignFrom: infer.beVigilant requires options.signal (AbortSignal) for cleanup');
|
|
243
|
+
}
|
|
244
|
+
const { setupVigilantObserver } = await import('./beVigilant.js');
|
|
245
|
+
setupVigilantObserver(target, options.from, options.infer, options.signal);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Process bulk enhancements — dynamically imported only when option is present
|
|
250
|
+
if (options.enhance && options.enhance.length > 0) {
|
|
251
|
+
const { enhanceAll } = await import('./enhanceAll.js');
|
|
252
|
+
await enhanceAll(target, options.enhance, permissions);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return target;
|
|
256
|
+
}
|
package/assignGingerly.js
CHANGED
|
@@ -226,6 +226,21 @@ function parseDeleteCommand(key) {
|
|
|
226
226
|
}
|
|
227
227
|
return key.substring(0, key.length - 3); // Remove ' -=' suffix
|
|
228
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Helper function to check if a key represents a Y= merge command
|
|
231
|
+
*/
|
|
232
|
+
function isMergeCommand(key) {
|
|
233
|
+
return key.endsWith(' Y=');
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Helper function to parse a Y= merge command and extract the path
|
|
237
|
+
*/
|
|
238
|
+
function parseMergeCommand(key) {
|
|
239
|
+
if (!isMergeCommand(key)) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return key.substring(0, key.length - 3); // Remove ' Y=' suffix
|
|
243
|
+
}
|
|
229
244
|
/**
|
|
230
245
|
* Helper function to parse a path string with ?. notation
|
|
231
246
|
* Always splits on '?.' delimiter, preserving dots that are part of values
|
|
@@ -769,6 +784,41 @@ export function assignGingerly(target, source, options, permissions) {
|
|
|
769
784
|
}
|
|
770
785
|
continue;
|
|
771
786
|
}
|
|
787
|
+
// Handle Y= merge commands (recursive assignGingerly into sub-object)
|
|
788
|
+
if (isMergeCommand(key)) {
|
|
789
|
+
const path = parseMergeCommand(key);
|
|
790
|
+
if (path) {
|
|
791
|
+
// Navigate to the target sub-object
|
|
792
|
+
let mergeTarget;
|
|
793
|
+
if (isNestedPath(path)) {
|
|
794
|
+
if (withMethodsSet) {
|
|
795
|
+
const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet);
|
|
796
|
+
mergeTarget = result.target[result.lastKey];
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
const pathParts = parsePath(path);
|
|
800
|
+
mergeTarget = target;
|
|
801
|
+
for (const part of pathParts) {
|
|
802
|
+
if (mergeTarget && typeof mergeTarget === 'object' && part in mergeTarget) {
|
|
803
|
+
mergeTarget = mergeTarget[part];
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
mergeTarget = undefined;
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
mergeTarget = target[path];
|
|
814
|
+
}
|
|
815
|
+
// Recursively merge if target is a valid object
|
|
816
|
+
if (mergeTarget && typeof mergeTarget === 'object') {
|
|
817
|
+
assignGingerly(mergeTarget, value, options, permissions);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
772
822
|
if (isNestedPath(key)) {
|
|
773
823
|
const pathParts = parsePath(key);
|
|
774
824
|
// Check if path contains @each or @eachTime (forEach)
|
package/assignGingerly.ts
CHANGED
|
@@ -388,6 +388,23 @@ function parseDeleteCommand(key: string): string | null {
|
|
|
388
388
|
return key.substring(0, key.length - 3); // Remove ' -=' suffix
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Helper function to check if a key represents a Y= merge command
|
|
393
|
+
*/
|
|
394
|
+
function isMergeCommand(key: string): boolean {
|
|
395
|
+
return key.endsWith(' Y=');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Helper function to parse a Y= merge command and extract the path
|
|
400
|
+
*/
|
|
401
|
+
function parseMergeCommand(key: string): string | null {
|
|
402
|
+
if (!isMergeCommand(key)) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return key.substring(0, key.length - 3); // Remove ' Y=' suffix
|
|
406
|
+
}
|
|
407
|
+
|
|
391
408
|
/**
|
|
392
409
|
* Helper function to parse a path string with ?. notation
|
|
393
410
|
* Always splits on '?.' delimiter, preserving dots that are part of values
|
|
@@ -968,6 +985,40 @@ export function assignGingerly(
|
|
|
968
985
|
continue;
|
|
969
986
|
}
|
|
970
987
|
|
|
988
|
+
// Handle Y= merge commands (recursive assignGingerly into sub-object)
|
|
989
|
+
if (isMergeCommand(key)) {
|
|
990
|
+
const path = parseMergeCommand(key);
|
|
991
|
+
if (path) {
|
|
992
|
+
// Navigate to the target sub-object
|
|
993
|
+
let mergeTarget: any;
|
|
994
|
+
if (isNestedPath(path)) {
|
|
995
|
+
if (withMethodsSet) {
|
|
996
|
+
const result = evaluatePathWithMethods(target, parsePath(path), value, withMethodsSet);
|
|
997
|
+
mergeTarget = result.target[result.lastKey];
|
|
998
|
+
} else {
|
|
999
|
+
const pathParts = parsePath(path);
|
|
1000
|
+
mergeTarget = target;
|
|
1001
|
+
for (const part of pathParts) {
|
|
1002
|
+
if (mergeTarget && typeof mergeTarget === 'object' && part in mergeTarget) {
|
|
1003
|
+
mergeTarget = mergeTarget[part];
|
|
1004
|
+
} else {
|
|
1005
|
+
mergeTarget = undefined;
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} else {
|
|
1011
|
+
mergeTarget = target[path];
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Recursively merge if target is a valid object
|
|
1015
|
+
if (mergeTarget && typeof mergeTarget === 'object') {
|
|
1016
|
+
assignGingerly(mergeTarget, value, options, permissions);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
971
1022
|
if (isNestedPath(key)) {
|
|
972
1023
|
const pathParts = parsePath(key);
|
|
973
1024
|
|
package/builtInEmoji.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtInEmoji.js — Predefined emoji aliases for built-in handlers.
|
|
3
|
+
*
|
|
4
|
+
* Import and spread into the `handlers` option for concise handler configs:
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
|
|
8
|
+
*
|
|
9
|
+
* assignFrom(target, {
|
|
10
|
+
* '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
|
|
11
|
+
* }, { from: vm, handlers: builtInEmoji });
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Emoji → built-in handler name mapping.
|
|
16
|
+
*/
|
|
17
|
+
export const builtInEmoji = {
|
|
18
|
+
'📦': 'builtIns.lazyLoad',
|
|
19
|
+
'🎚️': 'builtIns.lazyLoadSwitch',
|
|
20
|
+
'🔗': 'builtIns.join',
|
|
21
|
+
'🏷️': 'builtIns.microDataJoin',
|
|
22
|
+
'📋': 'builtIns.manageTemplateList',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default builtInEmoji;
|
package/builtInEmoji.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* builtInEmoji.ts — Predefined emoji aliases for built-in handlers.
|
|
3
|
+
*
|
|
4
|
+
* Import and spread into the `handlers` option for concise handler configs:
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { builtInEmoji } from 'assign-gingerly/builtInEmoji.js';
|
|
8
|
+
*
|
|
9
|
+
* assignFrom(target, {
|
|
10
|
+
* '?.el =>': { do: '🔗', get: { value: ['?.first', ' ', '?.last'] } }
|
|
11
|
+
* }, { from: vm, handlers: builtInEmoji });
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Emoji → built-in handler name mapping.
|
|
16
|
+
*
|
|
17
|
+
* | Emoji | Handler |
|
|
18
|
+
* |-------|---------|
|
|
19
|
+
* | 📦 | builtIns.lazyLoad |
|
|
20
|
+
* | 🎚️ | builtIns.lazyLoadSwitch |
|
|
21
|
+
* | 🔗 | builtIns.join |
|
|
22
|
+
* | 🏷️ | builtIns.microDataJoin |
|
|
23
|
+
* | 📋 | builtIns.manageTemplateList |
|
|
24
|
+
*/
|
|
25
|
+
export const builtInEmoji: Record<string, string> = {
|
|
26
|
+
'📦': 'builtIns.lazyLoad',
|
|
27
|
+
'🎚️': 'builtIns.lazyLoadSwitch',
|
|
28
|
+
'🔗': 'builtIns.join',
|
|
29
|
+
'🏷️': 'builtIns.microDataJoin',
|
|
30
|
+
'📋': 'builtIns.manageTemplateList',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default builtInEmoji;
|