assign-gingerly 0.0.85 → 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 +34 -39
- package/assignFrom.js +78 -2
- package/assignFrom.ts +80 -2
- package/inferencer/types/NewCustomElementFeature.md +3 -1
- package/inferencer/types/NewHTMLFirstCustomElement.md +2 -0
- package/inferencer/types/assign-gingerly/types.d.ts +43 -5
- package/package.json +9 -4
- 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/utils/resolveLhsPath.js +68 -0
- package/utils/resolveLhsPath.ts +81 -0
- package/handlers/join.ts +0 -79
|
@@ -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",
|
|
@@ -103,9 +104,13 @@
|
|
|
103
104
|
"default": "./handlers/lazyLoadSwitch.js",
|
|
104
105
|
"types": "./handlers/lazyLoadSwitch.ts"
|
|
105
106
|
},
|
|
106
|
-
"./
|
|
107
|
-
"default": "./
|
|
108
|
-
"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"
|
|
109
114
|
},
|
|
110
115
|
"./handlers/microDataJoin.js": {
|
|
111
116
|
"default": "./handlers/microDataJoin.js",
|
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { resolveValues } from './resolve/resolveValues.js';
|
|
7
7
|
import { getValues } from './resolve/getValues.js';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveLhsPath } from './utils/resolveLhsPath.js';
|
|
9
9
|
import { findClassPrototypeInPath } from './utils/findClassPrototypeInPath.js';
|
|
10
10
|
/**
|
|
11
11
|
* Map of built-in handler names to their module paths.
|
|
12
12
|
* These are auto-loaded on demand — no explicit import required.
|
|
13
|
+
*
|
|
14
|
+
* `join` moved to a synchronous ` =&` op (see syncOps/join.ts) — it never had
|
|
15
|
+
* side effects or anything to await, so it didn't belong behind this async pipeline.
|
|
13
16
|
*/
|
|
14
17
|
const BUILT_IN_MAP = {
|
|
15
18
|
'builtIns.lazyLoad': './handlers/lazyLoad.js',
|
|
16
19
|
'builtIns.lazyLoadSwitch': './handlers/lazyLoadSwitch.js',
|
|
17
|
-
'builtIns.join': './handlers/join.js',
|
|
18
20
|
'builtIns.microDataJoin': './handlers/microDataJoin.js',
|
|
19
21
|
'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
|
|
20
22
|
'builtIns.rangeSelector': './handlers/rangeSelector.js',
|
|
@@ -98,59 +100,7 @@ export async function processHandlerCommands(target, handlerKeys, pattern, optio
|
|
|
98
100
|
continue;
|
|
99
101
|
// Resolve the LHS path, preserving parent + key for return-value assignment.
|
|
100
102
|
// lhsParent[lhsKey] === lhsTarget (the current value at the path)
|
|
101
|
-
|
|
102
|
-
let lhsParent = undefined;
|
|
103
|
-
let lhsKey = undefined;
|
|
104
|
-
if (lhsPath.startsWith('?.')) {
|
|
105
|
-
const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
|
|
106
|
-
const withMethodsSet = options.withMethods
|
|
107
|
-
? options.withMethods instanceof Set
|
|
108
|
-
? options.withMethods
|
|
109
|
-
: new Set(options.withMethods)
|
|
110
|
-
: undefined;
|
|
111
|
-
if (withMethodsSet && pathParts.length > 0) {
|
|
112
|
-
const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
|
|
113
|
-
lhsParent = result.target;
|
|
114
|
-
lhsKey = result.lastKey;
|
|
115
|
-
lhsTarget = result.target[result.lastKey];
|
|
116
|
-
// If last key is a method, call it to get the target
|
|
117
|
-
if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
|
|
118
|
-
lhsTarget = result.target[result.lastKey].call(result.target);
|
|
119
|
-
lhsParent = undefined; // Can't assign back to a method call result
|
|
120
|
-
lhsKey = undefined;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
// Simple path navigation — walk to parent, keep last key
|
|
125
|
-
if (pathParts.length === 0) {
|
|
126
|
-
lhsTarget = target;
|
|
127
|
-
}
|
|
128
|
-
else if (pathParts.length === 1) {
|
|
129
|
-
lhsParent = target;
|
|
130
|
-
lhsKey = pathParts[0];
|
|
131
|
-
lhsTarget = target[pathParts[0]];
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
let current = target;
|
|
135
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
136
|
-
if (current == null)
|
|
137
|
-
break;
|
|
138
|
-
current = current[pathParts[i]];
|
|
139
|
-
}
|
|
140
|
-
lhsParent = current;
|
|
141
|
-
lhsKey = pathParts[pathParts.length - 1];
|
|
142
|
-
lhsTarget = current != null ? current[lhsKey] : undefined;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
else if (lhsPath) {
|
|
147
|
-
lhsParent = target;
|
|
148
|
-
lhsKey = lhsPath;
|
|
149
|
-
lhsTarget = target[lhsPath];
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
lhsTarget = target;
|
|
153
|
-
}
|
|
103
|
+
const { lhsTarget, lhsParent, lhsKey } = resolveLhsPath(target, lhsPath, options);
|
|
154
104
|
// Execute handlers sequentially, sharing the same lhsTarget
|
|
155
105
|
for (const config of configs) {
|
|
156
106
|
//return; //1.3ms
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { resolveValues } from './resolve/resolveValues.js';
|
|
8
8
|
import { getValues } from './resolve/getValues.js';
|
|
9
|
-
import {
|
|
9
|
+
import { resolveLhsPath } from './utils/resolveLhsPath.js';
|
|
10
10
|
import { findClassPrototypeInPath } from './utils/findClassPrototypeInPath.js';
|
|
11
11
|
import type { PermissionProcessor } from './types/assign-gingerly/types.js';
|
|
12
12
|
import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFromAsync.js';
|
|
@@ -14,11 +14,13 @@ import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFr
|
|
|
14
14
|
/**
|
|
15
15
|
* Map of built-in handler names to their module paths.
|
|
16
16
|
* These are auto-loaded on demand — no explicit import required.
|
|
17
|
+
*
|
|
18
|
+
* `join` moved to a synchronous ` =&` op (see syncOps/join.ts) — it never had
|
|
19
|
+
* side effects or anything to await, so it didn't belong behind this async pipeline.
|
|
17
20
|
*/
|
|
18
21
|
const BUILT_IN_MAP: Record<string, string> = {
|
|
19
22
|
'builtIns.lazyLoad': './handlers/lazyLoad.js',
|
|
20
23
|
'builtIns.lazyLoadSwitch': './handlers/lazyLoadSwitch.js',
|
|
21
|
-
'builtIns.join': './handlers/join.js',
|
|
22
24
|
'builtIns.microDataJoin': './handlers/microDataJoin.js',
|
|
23
25
|
'builtIns.manageTemplateList': './handlers/manageTemplateList.js',
|
|
24
26
|
'builtIns.rangeSelector': './handlers/rangeSelector.js',
|
|
@@ -121,55 +123,8 @@ export async function processHandlerCommands(
|
|
|
121
123
|
|
|
122
124
|
// Resolve the LHS path, preserving parent + key for return-value assignment.
|
|
123
125
|
// lhsParent[lhsKey] === lhsTarget (the current value at the path)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let lhsKey: string | undefined = undefined;
|
|
127
|
-
|
|
128
|
-
if (lhsPath.startsWith('?.')) {
|
|
129
|
-
const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
|
|
130
|
-
const withMethodsSet = options.withMethods
|
|
131
|
-
? options.withMethods instanceof Set
|
|
132
|
-
? options.withMethods
|
|
133
|
-
: new Set(options.withMethods)
|
|
134
|
-
: undefined;
|
|
135
|
-
|
|
136
|
-
if (withMethodsSet && pathParts.length > 0) {
|
|
137
|
-
const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
|
|
138
|
-
lhsParent = result.target;
|
|
139
|
-
lhsKey = result.lastKey;
|
|
140
|
-
lhsTarget = result.target[result.lastKey];
|
|
141
|
-
// If last key is a method, call it to get the target
|
|
142
|
-
if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
|
|
143
|
-
lhsTarget = result.target[result.lastKey].call(result.target);
|
|
144
|
-
lhsParent = undefined; // Can't assign back to a method call result
|
|
145
|
-
lhsKey = undefined;
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
// Simple path navigation — walk to parent, keep last key
|
|
149
|
-
if (pathParts.length === 0) {
|
|
150
|
-
lhsTarget = target;
|
|
151
|
-
} else if (pathParts.length === 1) {
|
|
152
|
-
lhsParent = target;
|
|
153
|
-
lhsKey = pathParts[0];
|
|
154
|
-
lhsTarget = target[pathParts[0]];
|
|
155
|
-
} else {
|
|
156
|
-
let current = target;
|
|
157
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
158
|
-
if (current == null) break;
|
|
159
|
-
current = current[pathParts[i]];
|
|
160
|
-
}
|
|
161
|
-
lhsParent = current;
|
|
162
|
-
lhsKey = pathParts[pathParts.length - 1];
|
|
163
|
-
lhsTarget = current != null ? current[lhsKey] : undefined;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} else if (lhsPath) {
|
|
167
|
-
lhsParent = target;
|
|
168
|
-
lhsKey = lhsPath;
|
|
169
|
-
lhsTarget = target[lhsPath];
|
|
170
|
-
} else {
|
|
171
|
-
lhsTarget = target;
|
|
172
|
-
}
|
|
126
|
+
const { lhsTarget, lhsParent, lhsKey } = resolveLhsPath(target, lhsPath, options);
|
|
127
|
+
|
|
173
128
|
// Execute handlers sequentially, sharing the same lhsTarget
|
|
174
129
|
for (const config of configs) {
|
|
175
130
|
//return; //1.3ms
|
|
@@ -1,73 +1,61 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Joins a resolved array into a single string. Supports nested sub-arrays
|
|
5
|
-
* with "all-or-nothing" semantics: if any element in a nested sub-array
|
|
6
|
-
* resolves to null/undefined, the entire sub-array is dropped.
|
|
7
|
-
*
|
|
8
|
-
* This
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*/
|
|
63
|
-
export class JoinHandler {
|
|
64
|
-
config;
|
|
65
|
-
constructor(config) {
|
|
66
|
-
this.config = config;
|
|
67
|
-
}
|
|
68
|
-
async assign(lhsTarget, resolvedParams) {
|
|
69
|
-
const { value, separator = '' } = resolvedParams;
|
|
70
|
-
const items = Array.isArray(value) ? processValue(value) : [value];
|
|
71
|
-
return items.join(separator);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* join — synchronous compute op for the ` =&` operator.
|
|
3
|
+
*
|
|
4
|
+
* Joins a resolved array into a single string. Supports nested sub-arrays
|
|
5
|
+
* with "all-or-nothing" semantics: if any element in a nested sub-array
|
|
6
|
+
* resolves to null/undefined, the entire sub-array is dropped.
|
|
7
|
+
*
|
|
8
|
+
* This is the sync-op successor to the old `builtIns.join` handler (which lived
|
|
9
|
+
* behind the async ` =>` pipeline). Unlike a handler, an op is a plain function —
|
|
10
|
+
* no class, no dynamic import, no await anywhere in its path.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* assignFrom(oElement, {
|
|
14
|
+
* '?.textContent =&': {
|
|
15
|
+
* join: ['?.lastName', ', ', '?.firstName']
|
|
16
|
+
* }
|
|
17
|
+
* }, { from: vm });
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // With optional segment (all-or-nothing) and a separator:
|
|
21
|
+
* assignFrom(oElement, {
|
|
22
|
+
* '?.textContent =&': {
|
|
23
|
+
* join: ['?.lastName', ['?.middleName'], '?.firstName'],
|
|
24
|
+
* separator: ', '
|
|
25
|
+
* }
|
|
26
|
+
* }, { from: vm });
|
|
27
|
+
* // If middleName is undefined, the sub-array ['?.middleName'] is dropped entirely.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Process nested arrays with all-or-nothing null semantics.
|
|
31
|
+
* - Top-level null/undefined values are filtered out.
|
|
32
|
+
* - If a nested sub-array contains any null/undefined element, the entire sub-array is dropped.
|
|
33
|
+
* - Nested sub-arrays that pass are flattened into the result.
|
|
34
|
+
*/
|
|
35
|
+
function processValue(value) {
|
|
36
|
+
const result = [];
|
|
37
|
+
for (const item of value) {
|
|
38
|
+
if (Array.isArray(item)) {
|
|
39
|
+
// All-or-nothing: if any element is null/undefined, drop the entire sub-array
|
|
40
|
+
if (item.some(el => el == null)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Sub-array passes — flatten its elements (recursively process nested arrays)
|
|
44
|
+
result.push(...processValue(item));
|
|
45
|
+
}
|
|
46
|
+
else if (item != null) {
|
|
47
|
+
result.push(item);
|
|
48
|
+
}
|
|
49
|
+
// Top-level null/undefined are silently filtered out
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* `join` sync op — args is the resolved `join:` array, extra carries sibling
|
|
55
|
+
* config keys (currently just `separator`, default `''`).
|
|
56
|
+
*/
|
|
57
|
+
export function join(args, extra = {}) {
|
|
58
|
+
const { separator = '' } = extra;
|
|
59
|
+
const items = Array.isArray(args) ? processValue(args) : [args];
|
|
60
|
+
return items.join(separator);
|
|
61
|
+
}
|
package/syncOps/join.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* join — synchronous compute op for the ` =&` operator.
|
|
3
|
+
*
|
|
4
|
+
* Joins a resolved array into a single string. Supports nested sub-arrays
|
|
5
|
+
* with "all-or-nothing" semantics: if any element in a nested sub-array
|
|
6
|
+
* resolves to null/undefined, the entire sub-array is dropped.
|
|
7
|
+
*
|
|
8
|
+
* This is the sync-op successor to the old `builtIns.join` handler (which lived
|
|
9
|
+
* behind the async ` =>` pipeline). Unlike a handler, an op is a plain function —
|
|
10
|
+
* no class, no dynamic import, no await anywhere in its path.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* assignFrom(oElement, {
|
|
14
|
+
* '?.textContent =&': {
|
|
15
|
+
* join: ['?.lastName', ', ', '?.firstName']
|
|
16
|
+
* }
|
|
17
|
+
* }, { from: vm });
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // With optional segment (all-or-nothing) and a separator:
|
|
21
|
+
* assignFrom(oElement, {
|
|
22
|
+
* '?.textContent =&': {
|
|
23
|
+
* join: ['?.lastName', ['?.middleName'], '?.firstName'],
|
|
24
|
+
* separator: ', '
|
|
25
|
+
* }
|
|
26
|
+
* }, { from: vm });
|
|
27
|
+
* // If middleName is undefined, the sub-array ['?.middleName'] is dropped entirely.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Process nested arrays with all-or-nothing null semantics.
|
|
32
|
+
* - Top-level null/undefined values are filtered out.
|
|
33
|
+
* - If a nested sub-array contains any null/undefined element, the entire sub-array is dropped.
|
|
34
|
+
* - Nested sub-arrays that pass are flattened into the result.
|
|
35
|
+
*/
|
|
36
|
+
function processValue(value: any[]): any[] {
|
|
37
|
+
const result: any[] = [];
|
|
38
|
+
for (const item of value) {
|
|
39
|
+
if (Array.isArray(item)) {
|
|
40
|
+
// All-or-nothing: if any element is null/undefined, drop the entire sub-array
|
|
41
|
+
if (item.some(el => el == null)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Sub-array passes — flatten its elements (recursively process nested arrays)
|
|
45
|
+
result.push(...processValue(item));
|
|
46
|
+
} else if (item != null) {
|
|
47
|
+
result.push(item);
|
|
48
|
+
}
|
|
49
|
+
// Top-level null/undefined are silently filtered out
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `join` sync op — args is the resolved `join:` array, extra carries sibling
|
|
56
|
+
* config keys (currently just `separator`, default `''`).
|
|
57
|
+
*/
|
|
58
|
+
export function join(args: any, extra: Record<string, any> = {}): string {
|
|
59
|
+
const { separator = '' } = extra;
|
|
60
|
+
const items = Array.isArray(args) ? processValue(args) : [args];
|
|
61
|
+
return items.join(separator);
|
|
62
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registry.ts — Registry of ` =&` sync ops.
|
|
3
|
+
*
|
|
4
|
+
* Every op is a plain, synchronous, side-effect-free function: (args, extra) => value.
|
|
5
|
+
* `args` is the resolved value of the key matching the op's own name (e.g. `join:`),
|
|
6
|
+
* `extra` carries any sibling config keys (e.g. `separator:`). All statically
|
|
7
|
+
* imported — no dynamic loading, unlike the ` =>` built-in handler map — because
|
|
8
|
+
* the entire point of ` =&` is to never introduce an await.
|
|
9
|
+
*
|
|
10
|
+
* Add new sync ops here as they're written.
|
|
11
|
+
*/
|
|
12
|
+
import { join } from './join.js';
|
|
13
|
+
export const SYNC_OPS = {
|
|
14
|
+
join,
|
|
15
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registry.ts — Registry of ` =&` sync ops.
|
|
3
|
+
*
|
|
4
|
+
* Every op is a plain, synchronous, side-effect-free function: (args, extra) => value.
|
|
5
|
+
* `args` is the resolved value of the key matching the op's own name (e.g. `join:`),
|
|
6
|
+
* `extra` carries any sibling config keys (e.g. `separator:`). All statically
|
|
7
|
+
* imported — no dynamic loading, unlike the ` =>` built-in handler map — because
|
|
8
|
+
* the entire point of ` =&` is to never introduce an await.
|
|
9
|
+
*
|
|
10
|
+
* Add new sync ops here as they're written.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join } from './join.js';
|
|
14
|
+
|
|
15
|
+
export type SyncOp = (args: any, extra: Record<string, any>) => any;
|
|
16
|
+
|
|
17
|
+
export const SYNC_OPS: Record<string, SyncOp> = {
|
|
18
|
+
join,
|
|
19
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveLhsPath.ts — Shared LHS path resolution for operator commands.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the `?.`-prefixed path in front of an operator suffix (` =>`, ` =&`, ...)
|
|
5
|
+
* against a target, preserving the parent object and final key so the caller can
|
|
6
|
+
* assign a computed/returned value back to that path.
|
|
7
|
+
*
|
|
8
|
+
* Shared by processHandlerCommands.ts (` =>`) and the sync-op dispatcher in
|
|
9
|
+
* assignFrom.ts (` =&`) — the two operators that hand a value back to the LHS
|
|
10
|
+
* rather than assigning it directly.
|
|
11
|
+
*/
|
|
12
|
+
import { evaluatePathWithMethods } from '../assignGingerly.js';
|
|
13
|
+
export function resolveLhsPath(target, lhsPath, options) {
|
|
14
|
+
let lhsTarget;
|
|
15
|
+
let lhsParent = undefined;
|
|
16
|
+
let lhsKey = undefined;
|
|
17
|
+
if (lhsPath.startsWith('?.')) {
|
|
18
|
+
const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
|
|
19
|
+
const withMethodsSet = options.withMethods
|
|
20
|
+
? options.withMethods instanceof Set
|
|
21
|
+
? options.withMethods
|
|
22
|
+
: new Set(options.withMethods)
|
|
23
|
+
: undefined;
|
|
24
|
+
if (withMethodsSet && pathParts.length > 0) {
|
|
25
|
+
const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
|
|
26
|
+
lhsParent = result.target;
|
|
27
|
+
lhsKey = result.lastKey;
|
|
28
|
+
lhsTarget = result.target[result.lastKey];
|
|
29
|
+
// If last key is a method, call it to get the target
|
|
30
|
+
if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
|
|
31
|
+
lhsTarget = result.target[result.lastKey].call(result.target);
|
|
32
|
+
lhsParent = undefined; // Can't assign back to a method call result
|
|
33
|
+
lhsKey = undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Simple path navigation — walk to parent, keep last key
|
|
38
|
+
if (pathParts.length === 0) {
|
|
39
|
+
lhsTarget = target;
|
|
40
|
+
}
|
|
41
|
+
else if (pathParts.length === 1) {
|
|
42
|
+
lhsParent = target;
|
|
43
|
+
lhsKey = pathParts[0];
|
|
44
|
+
lhsTarget = target[pathParts[0]];
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
let current = target;
|
|
48
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
49
|
+
if (current == null)
|
|
50
|
+
break;
|
|
51
|
+
current = current[pathParts[i]];
|
|
52
|
+
}
|
|
53
|
+
lhsParent = current;
|
|
54
|
+
lhsKey = pathParts[pathParts.length - 1];
|
|
55
|
+
lhsTarget = current != null ? current[lhsKey] : undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (lhsPath) {
|
|
60
|
+
lhsParent = target;
|
|
61
|
+
lhsKey = lhsPath;
|
|
62
|
+
lhsTarget = target[lhsPath];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
lhsTarget = target;
|
|
66
|
+
}
|
|
67
|
+
return { lhsTarget, lhsParent, lhsKey };
|
|
68
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveLhsPath.ts — Shared LHS path resolution for operator commands.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the `?.`-prefixed path in front of an operator suffix (` =>`, ` =&`, ...)
|
|
5
|
+
* against a target, preserving the parent object and final key so the caller can
|
|
6
|
+
* assign a computed/returned value back to that path.
|
|
7
|
+
*
|
|
8
|
+
* Shared by processHandlerCommands.ts (` =>`) and the sync-op dispatcher in
|
|
9
|
+
* assignFrom.ts (` =&`) — the two operators that hand a value back to the LHS
|
|
10
|
+
* rather than assigning it directly.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { evaluatePathWithMethods } from '../assignGingerly.js';
|
|
14
|
+
|
|
15
|
+
export interface ResolvedLhsPath {
|
|
16
|
+
lhsTarget: any;
|
|
17
|
+
lhsParent: any;
|
|
18
|
+
lhsKey: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ResolveLhsPathOptions {
|
|
22
|
+
withMethods?: Set<string> | string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveLhsPath(
|
|
26
|
+
target: any,
|
|
27
|
+
lhsPath: string,
|
|
28
|
+
options: ResolveLhsPathOptions
|
|
29
|
+
): ResolvedLhsPath {
|
|
30
|
+
let lhsTarget: any;
|
|
31
|
+
let lhsParent: any = undefined;
|
|
32
|
+
let lhsKey: string | undefined = undefined;
|
|
33
|
+
|
|
34
|
+
if (lhsPath.startsWith('?.')) {
|
|
35
|
+
const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
|
|
36
|
+
const withMethodsSet = options.withMethods
|
|
37
|
+
? options.withMethods instanceof Set
|
|
38
|
+
? options.withMethods
|
|
39
|
+
: new Set(options.withMethods)
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
42
|
+
if (withMethodsSet && pathParts.length > 0) {
|
|
43
|
+
const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
|
|
44
|
+
lhsParent = result.target;
|
|
45
|
+
lhsKey = result.lastKey;
|
|
46
|
+
lhsTarget = result.target[result.lastKey];
|
|
47
|
+
// If last key is a method, call it to get the target
|
|
48
|
+
if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
|
|
49
|
+
lhsTarget = result.target[result.lastKey].call(result.target);
|
|
50
|
+
lhsParent = undefined; // Can't assign back to a method call result
|
|
51
|
+
lhsKey = undefined;
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// Simple path navigation — walk to parent, keep last key
|
|
55
|
+
if (pathParts.length === 0) {
|
|
56
|
+
lhsTarget = target;
|
|
57
|
+
} else if (pathParts.length === 1) {
|
|
58
|
+
lhsParent = target;
|
|
59
|
+
lhsKey = pathParts[0];
|
|
60
|
+
lhsTarget = target[pathParts[0]];
|
|
61
|
+
} else {
|
|
62
|
+
let current = target;
|
|
63
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
64
|
+
if (current == null) break;
|
|
65
|
+
current = current[pathParts[i]];
|
|
66
|
+
}
|
|
67
|
+
lhsParent = current;
|
|
68
|
+
lhsKey = pathParts[pathParts.length - 1];
|
|
69
|
+
lhsTarget = current != null ? current[lhsKey] : undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} else if (lhsPath) {
|
|
73
|
+
lhsParent = target;
|
|
74
|
+
lhsKey = lhsPath;
|
|
75
|
+
lhsTarget = target[lhsPath];
|
|
76
|
+
} else {
|
|
77
|
+
lhsTarget = target;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { lhsTarget, lhsParent, lhsKey };
|
|
81
|
+
}
|