assign-gingerly 0.0.52 → 0.0.54

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/paths.js ADDED
@@ -0,0 +1,183 @@
1
+ /**
2
+ * paths.js — Typed path proxy and template tags for assignFrom authoring.
3
+ *
4
+ * Provides compile-time autocomplete and type safety for `?.`-prefixed path strings.
5
+ * Supports method call syntax, alias reversal, and batch proxy extraction.
6
+ */
7
+
8
+ /**
9
+ * Symbol used internally to detect path proxy objects.
10
+ */
11
+ const PATH_SYMBOL = Symbol('assign-gingerly-path');
12
+
13
+ /**
14
+ * Create a recursive proxy that records property access paths.
15
+ * Supports both property access and method call syntax (via apply trap on function target).
16
+ */
17
+ function createPathProxy(prefix, options) {
18
+ const aliasMap = options?.aka;
19
+
20
+ function handler() {}
21
+
22
+ return new Proxy(handler, {
23
+ get(_, prop) {
24
+ if (prop === 'path' || prop === PATH_SYMBOL) {
25
+ return prefix.length > 0 ? `?.${prefix}` : '?.';
26
+ }
27
+ if (typeof prop === 'symbol') return undefined;
28
+
29
+ let segment = String(prop);
30
+ if (aliasMap) {
31
+ for (const [alias, target] of Object.entries(aliasMap)) {
32
+ if (target === segment) { segment = alias; break; }
33
+ }
34
+ }
35
+
36
+ const newPath = prefix ? `${prefix}?.${segment}` : segment;
37
+ return createPathProxy(newPath, options);
38
+ },
39
+ apply(_, __, args) {
40
+ if (args.length > 0) {
41
+ const arg = args[0];
42
+ let argStr;
43
+ if (arg === true) argStr = 'true';
44
+ else if (arg === false) argStr = 'false';
45
+ else if (arg && typeof arg === 'object' && PATH_SYMBOL in arg) {
46
+ const fullPath = arg[PATH_SYMBOL];
47
+ argStr = fullPath.startsWith('?.') ? fullPath.substring(2) : fullPath;
48
+ }
49
+ else argStr = String(arg);
50
+
51
+ const newPath = prefix ? `${prefix}?.${argStr}` : argStr;
52
+ return createPathProxy(newPath, options);
53
+ }
54
+ return createPathProxy(prefix, options);
55
+ }
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Create a typed path proxy.
61
+ */
62
+ export function paths(options) {
63
+ return createPathProxy('', options);
64
+ }
65
+
66
+ /**
67
+ * Create an assignment pair: { [lhs.path]: rhs.path }.
68
+ */
69
+ export function set(lhs) {
70
+ const lhsStr = lhs && typeof lhs === 'object' && PATH_SYMBOL in lhs
71
+ ? lhs[PATH_SYMBOL]
72
+ : String(lhs);
73
+ return {
74
+ to(rhs) {
75
+ const rhsStr = rhs && typeof rhs === 'object' && PATH_SYMBOL in rhs
76
+ ? rhs[PATH_SYMBOL]
77
+ : rhs;
78
+ return { [lhsStr]: rhsStr };
79
+ }
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Recursively walk a value and convert any path proxy objects to path strings.
85
+ */
86
+ export function smoothOver(value) {
87
+ if (value && typeof value === 'object' && PATH_SYMBOL in value) {
88
+ return value[PATH_SYMBOL];
89
+ }
90
+ if (Array.isArray(value)) {
91
+ return value.map(smoothOver);
92
+ }
93
+ if (value && typeof value === 'object') {
94
+ const proto = Object.getPrototypeOf(value);
95
+ if (proto === Object.prototype || proto === null) {
96
+ const result = {};
97
+ for (const [k, v] of Object.entries(value)) {
98
+ result[k] = smoothOver(v);
99
+ }
100
+ return result;
101
+ }
102
+ }
103
+ return value;
104
+ }
105
+
106
+ /**
107
+ * Merge multiple set(...).to(...) pairs into an { assign: {...} } object.
108
+ * Spread the result into a merge config.
109
+ */
110
+ export function doAssign(...pairs) {
111
+ return { assign: Object.assign({}, ...pairs) };
112
+ }
113
+
114
+ /**
115
+ * Tagged template literal that splits a template into an array of parts.
116
+ * Path proxy objects are auto-detected and converted to path strings.
117
+ */
118
+ export function sp(strings, ...values) {
119
+ const result = [];
120
+ for (let i = 0; i < strings.length; i++) {
121
+ if (strings[i]) result.push(strings[i]);
122
+ if (i < values.length) {
123
+ const v = values[i];
124
+ if (v && typeof v === 'object' && PATH_SYMBOL in v) {
125
+ result.push(v[PATH_SYMBOL]);
126
+ }
127
+ else if (Array.isArray(v)) {
128
+ result.push(v.map(el =>
129
+ el && typeof el === 'object' && PATH_SYMBOL in el ? el[PATH_SYMBOL] : el
130
+ ));
131
+ }
132
+ else {
133
+ result.push(v);
134
+ }
135
+ }
136
+ }
137
+ return result;
138
+ }
139
+
140
+ /**
141
+ * Extract the last segment from a `?.`-prefixed path string.
142
+ */
143
+ function extractPropName(pathStr) {
144
+ const parts = pathStr.split('?.');
145
+ return parts[parts.length - 1];
146
+ }
147
+
148
+ /**
149
+ * Tagged template literal that produces {prop, val} objects for microDataJoin.
150
+ */
151
+ export function md(strings, ...values) {
152
+ const result = [];
153
+ for (let i = 0; i < strings.length; i++) {
154
+ if (strings[i]) result.push(strings[i]);
155
+ if (i < values.length) {
156
+ const v = values[i];
157
+ if (v && typeof v === 'object' && PATH_SYMBOL in v) {
158
+ const pathStr = v[PATH_SYMBOL];
159
+ result.push({ prop: extractPropName(pathStr), val: pathStr });
160
+ }
161
+ else if (Array.isArray(v)) {
162
+ result.push(v.map(el => {
163
+ if (el && typeof el === 'object' && PATH_SYMBOL in el) {
164
+ const pathStr = el[PATH_SYMBOL];
165
+ return { prop: extractPropName(pathStr), val: pathStr };
166
+ }
167
+ return el;
168
+ }));
169
+ }
170
+ else if (v && typeof v === 'object' && 'prop' in v) {
171
+ const processed = { ...v };
172
+ if (processed.val && typeof processed.val === 'object' && PATH_SYMBOL in processed.val) {
173
+ processed.val = processed.val[PATH_SYMBOL];
174
+ }
175
+ result.push(processed);
176
+ }
177
+ else {
178
+ result.push(v);
179
+ }
180
+ }
181
+ }
182
+ return result;
183
+ }
package/paths.ts ADDED
@@ -0,0 +1,334 @@
1
+ /**
2
+ * paths.ts — Typed path proxy and template tag for assignFrom authoring.
3
+ *
4
+ * Provides compile-time autocomplete and type safety for `?.`-prefixed path strings.
5
+ *
6
+ * @example
7
+ * import { paths, sp } from 'assign-gingerly/paths.js';
8
+ *
9
+ * interface Person {
10
+ * firstName?: string;
11
+ * middleName?: string;
12
+ * lastName: string;
13
+ * address: { city: string; zip: string };
14
+ * }
15
+ *
16
+ * const $ = paths<Person>();
17
+ *
18
+ * // Use sp (split into parts) to create arrays for builtIns.join:
19
+ * const value = sp`${$.lastName}, ${$.firstName}`;
20
+ * // ['?.lastName', ', ', '?.firstName']
21
+ *
22
+ * // Use .path for raw string contexts (object keys, plain arrays):
23
+ * const key = $.textContent.path; // '?.textContent'
24
+ */
25
+
26
+ /**
27
+ * Symbol used internally to detect path proxy objects.
28
+ * The sp tag function uses this to auto-extract path strings from proxies.
29
+ */
30
+ const PATH_SYMBOL = Symbol('assign-gingerly-path');
31
+
32
+ /**
33
+ * Type that maps an object type to a proxy where every property access
34
+ * returns either a deeper proxy (for object properties) or a terminal
35
+ * with a `.path` string accessor — while providing full autocomplete.
36
+ * Enhanced: also callable (for method call syntax).
37
+ */
38
+ export type PathProxy<T> = {
39
+ [K in keyof T]-?: T[K] extends ((...args: any[]) => infer R)
40
+ ? ((...args: any[]) => PathProxy<NonNullable<R>> & { readonly path: string }) & PathProxy<NonNullable<R>> & { readonly path: string }
41
+ : T[K] extends (object | undefined | null)
42
+ ? PathProxy<NonNullable<T[K]>> & { readonly path: string } & ((...args: any[]) => PathProxy<any> & { readonly path: string })
43
+ : { readonly path: string } & ((...args: any[]) => PathProxy<any> & { readonly path: string });
44
+ } & { readonly path: string } & ((...args: any[]) => PathProxy<any> & { readonly path: string });
45
+
46
+ /**
47
+ * Options for paths proxy creation.
48
+ */
49
+ export interface PathsOptions {
50
+ /** Alias map (alias → full name). Proxy reverses aliases: full name → alias in output. */
51
+ aka?: Record<string, string>;
52
+ /** Method names — used for disambiguation (future use). */
53
+ withMethods?: string[] | Set<string>;
54
+ }
55
+
56
+ /**
57
+ * Create a recursive proxy that records property access paths.
58
+ * Supports both property access and method call syntax (via apply trap on function target).
59
+ *
60
+ * When `aka` is provided, property names that match an alias *value* are output
61
+ * using the alias *key* instead (reverse alias).
62
+ */
63
+ function createPathProxy(prefix: string, options?: PathsOptions): any {
64
+ const aliasMap = options?.aka;
65
+
66
+ // Use a function as the target to enable the apply trap
67
+ function handler() {}
68
+
69
+ return new Proxy(handler, {
70
+ get(_, prop: string | symbol) {
71
+ if (prop === 'path' || prop === PATH_SYMBOL) {
72
+ return prefix.length > 0 ? `?.${prefix}` : '?.';
73
+ }
74
+ // Ignore symbol access (Symbol.iterator, Symbol.toPrimitive, etc.)
75
+ if (typeof prop === 'symbol') return undefined;
76
+
77
+ // Apply reverse alias: if prop matches an alias value, use the alias key
78
+ let segment = String(prop);
79
+ if (aliasMap) {
80
+ for (const [alias, target] of Object.entries(aliasMap)) {
81
+ if (target === segment) { segment = alias; break; }
82
+ }
83
+ }
84
+
85
+ const newPath = prefix ? `${prefix}?.${segment}` : segment;
86
+ return createPathProxy(newPath, options);
87
+ },
88
+ apply(_, __, args) {
89
+ // Method call syntax: $.querySelector('.username') → extends path with the argument
90
+ if (args.length > 0) {
91
+ const arg = args[0];
92
+ let argStr: string;
93
+ if (arg === true) argStr = 'true';
94
+ else if (arg === false) argStr = 'false';
95
+ else if (arg && typeof arg === 'object' && PATH_SYMBOL in arg) {
96
+ // Proxy arg — extract path without '?.' prefix
97
+ const fullPath = arg[PATH_SYMBOL] as string;
98
+ argStr = fullPath.startsWith('?.') ? fullPath.substring(2) : fullPath;
99
+ }
100
+ else argStr = String(arg);
101
+
102
+ const newPath = prefix ? `${prefix}?.${argStr}` : argStr;
103
+ return createPathProxy(newPath, options);
104
+ }
105
+ // No args — method called with no arguments, return self
106
+ return createPathProxy(prefix, options);
107
+ }
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Create a typed path proxy for a given interface/type.
113
+ * Property accesses on the returned proxy produce `?.`-prefixed path strings.
114
+ * Method calls append their argument to the path.
115
+ *
116
+ * @param options - Optional aka aliases and withMethods for path generation
117
+ *
118
+ * @example
119
+ * const $ = paths<Person>();
120
+ * $.lastName.path // '?.lastName'
121
+ * $.address.city.path // '?.address?.city'
122
+ *
123
+ * // With aka (reverse alias applied):
124
+ * const $ = paths<MyEl>({ aka: { q: 'querySelector' } });
125
+ * $.querySelector('.user').textContent.path // '?.q?..user?.textContent'
126
+ *
127
+ * // Inside sp template literals, .path is not needed:
128
+ * sp`${$.lastName}, ${$.firstName}` // ['?.lastName', ', ', '?.firstName']
129
+ */
130
+ export function paths<T>(options?: PathsOptions): PathProxy<T> {
131
+ return createPathProxy('', options) as any;
132
+ }
133
+
134
+ /**
135
+ * Create an assignment pair: { [lhs.path]: rhs.path }.
136
+ * Used to express "set this target to this source value" in a spreadable form.
137
+ *
138
+ * @example
139
+ * set($.clone.querySelector('.username').textContent).to($.username)
140
+ * // { '?.clone?.q?..username?.textContent': '?.username' }
141
+ *
142
+ * // Spread into an assign object:
143
+ * assign: {
144
+ * ...set($.textContent).to($.name),
145
+ * ...set($.className).to($.theme),
146
+ * count: 1
147
+ * }
148
+ */
149
+ export function set(lhs: any): { to: (rhs: any) => Record<string, any> } {
150
+ const lhsStr = lhs && typeof lhs === 'object' && PATH_SYMBOL in lhs
151
+ ? lhs[PATH_SYMBOL]
152
+ : String(lhs);
153
+ return {
154
+ to(rhs: any): Record<string, any> {
155
+ const rhsStr = rhs && typeof rhs === 'object' && PATH_SYMBOL in rhs
156
+ ? rhs[PATH_SYMBOL]
157
+ : rhs;
158
+ return { [lhsStr]: rhsStr };
159
+ }
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Recursively walk a value and convert any path proxy objects to their
165
+ * `?.`-prefixed string representation.
166
+ *
167
+ * Use this to wrap entire config objects or arrays that contain proxy values,
168
+ * extracting all path strings in one pass.
169
+ *
170
+ * @example
171
+ * const $ = paths<MyVM>({ aka: { q: 'querySelector' } });
172
+ *
173
+ * const config = smoothOver({
174
+ * assign: {
175
+ * incrementButton: $.clone.querySelector('.increment'),
176
+ * decrementButton: $.clone.querySelector('.decrement'),
177
+ * }
178
+ * });
179
+ * // { assign: { incrementButton: '?.clone?.q?..increment', ... } }
180
+ */
181
+ export function smoothOver(value: any): any {
182
+ if (value && typeof value === 'object' && PATH_SYMBOL in value) {
183
+ return value[PATH_SYMBOL];
184
+ }
185
+ if (Array.isArray(value)) {
186
+ return value.map(smoothOver);
187
+ }
188
+ if (value && typeof value === 'object') {
189
+ const proto = Object.getPrototypeOf(value);
190
+ if (proto === Object.prototype || proto === null) {
191
+ const result: Record<string, any> = {};
192
+ for (const [k, v] of Object.entries(value)) {
193
+ result[k] = smoothOver(v);
194
+ }
195
+ return result;
196
+ }
197
+ }
198
+ return value;
199
+ }
200
+
201
+ /**
202
+ * Merge multiple set(...).to(...) pairs (and/or plain objects) into an `{ assign: {...} }` object.
203
+ * Spread the result into a merge config to avoid repeated `...` per entry.
204
+ *
205
+ * @example
206
+ * {
207
+ * ifKeyIn: ['statusClassName', 'statusMessageText'],
208
+ * ifAllOf: ['clone'],
209
+ * ...doAssign(
210
+ * set($.clone.querySelector('.status').className).to($.statusClassName),
211
+ * set($.clone.querySelector('.status-text').textContent).to($.statusMessageText),
212
+ * )
213
+ * }
214
+ * // Equivalent to: { ifKeyIn: [...], ifAllOf: [...], assign: { '?.clone?.q?..status?.className': '?.statusClassName', ... } }
215
+ *
216
+ * // Mix with literal values:
217
+ * ...doAssign(
218
+ * set($.clone.querySelector('.count-value').textContent).to($.count),
219
+ * { renderCount: 1 },
220
+ * )
221
+ */
222
+ export function doAssign(...pairs: Record<string, any>[]): { assign: Record<string, any> } {
223
+ return { assign: Object.assign({}, ...pairs) };
224
+ }
225
+ /**
226
+ * Tagged template literal that splits a template into an array of parts.
227
+ * Interleaves static string segments with interpolated values.
228
+ *
229
+ * Path proxy objects are auto-detected and converted to their `?.`-prefixed
230
+ * string representation — no `.path` call needed inside sp template literals.
231
+ *
232
+ * Arrays passed as interpolations are preserved as nested arrays (for
233
+ * all-or-nothing optional segments in builtIns.join).
234
+ *
235
+ * @example
236
+ * const $ = paths<Person>();
237
+ *
238
+ * // Basic usage:
239
+ * sp`${$.lastName}, ${$.firstName}`
240
+ * // ['?.lastName', ', ', '?.firstName']
241
+ *
242
+ * // With optional segment (nested array, all-or-nothing in join):
243
+ * sp`${$.lastName}${[', ', $.middleName]}, ${$.firstName}`
244
+ * // ['?.lastName', [', ', '?.middleName'], ', ', '?.firstName']
245
+ */
246
+ export function sp(strings: TemplateStringsArray, ...values: any[]): any[] {
247
+ const result: any[] = [];
248
+ for (let i = 0; i < strings.length; i++) {
249
+ if (strings[i]) result.push(strings[i]);
250
+ if (i < values.length) {
251
+ const v = values[i];
252
+ if (v && typeof v === 'object' && PATH_SYMBOL in v) {
253
+ // Auto-extract path from proxy object
254
+ result.push(v[PATH_SYMBOL]);
255
+ } else if (Array.isArray(v)) {
256
+ // Nested array — recursively extract paths from proxy elements
257
+ result.push(v.map(el =>
258
+ el && typeof el === 'object' && PATH_SYMBOL in el ? el[PATH_SYMBOL] : el
259
+ ));
260
+ } else {
261
+ result.push(v);
262
+ }
263
+ }
264
+ }
265
+ return result;
266
+ }
267
+
268
+ /**
269
+ * Extract the last segment from a `?.`-prefixed path string.
270
+ * e.g., '?.address?.city' → 'city', '?.firstName' → 'firstName'
271
+ */
272
+ function extractPropName(pathStr: string): string {
273
+ const parts = pathStr.split('?.');
274
+ return parts[parts.length - 1];
275
+ }
276
+
277
+ /**
278
+ * Tagged template literal that produces an array of {prop, val} objects + literal strings.
279
+ * Designed for `builtIns.microDataJoin` — provides both the property name (for itemprop)
280
+ * and the path string (for resolution).
281
+ *
282
+ * Path proxy objects are auto-detected and converted to `{prop, val}` objects.
283
+ * Plain objects passed as interpolations are preserved as-is (allows developer overrides
284
+ * with custom prop, val, format, etc.).
285
+ * Arrays are preserved as nested arrays (for optional segments).
286
+ *
287
+ * @example
288
+ * const $ = paths<Person>();
289
+ *
290
+ * // Basic usage:
291
+ * md`${$.firstName} ${$.lastName}`
292
+ * // [{ prop: 'firstName', val: '?.firstName' }, ' ', { prop: 'lastName', val: '?.lastName' }]
293
+ *
294
+ * // With developer override (custom prop name, format):
295
+ * md`${$.firstName} ${{ prop: 'birthDate', val: $.birthDT, format: 'long' }}`
296
+ * // [{ prop: 'firstName', val: '?.firstName' }, ' ', { prop: 'birthDate', val: '?.birthDT', format: 'long' }]
297
+ *
298
+ * // With optional segment:
299
+ * md`${$.firstName}${[' ', $.middleName]} ${$.lastName}`
300
+ * // [{ prop: 'firstName', val: '?.firstName' }, [' ', { prop: 'middleName', val: '?.middleName' }], ' ', { prop: 'lastName', val: '?.lastName' }]
301
+ */
302
+ export function md(strings: TemplateStringsArray, ...values: any[]): any[] {
303
+ const result: any[] = [];
304
+ for (let i = 0; i < strings.length; i++) {
305
+ if (strings[i]) result.push(strings[i]);
306
+ if (i < values.length) {
307
+ const v = values[i];
308
+ if (v && typeof v === 'object' && PATH_SYMBOL in v) {
309
+ // Proxy object → {prop, val}
310
+ const pathStr = v[PATH_SYMBOL] as string;
311
+ result.push({ prop: extractPropName(pathStr), val: pathStr });
312
+ } else if (Array.isArray(v)) {
313
+ // Nested array — recursively convert proxy elements to {prop, val}
314
+ result.push(v.map(el => {
315
+ if (el && typeof el === 'object' && PATH_SYMBOL in el) {
316
+ const pathStr = el[PATH_SYMBOL] as string;
317
+ return { prop: extractPropName(pathStr), val: pathStr };
318
+ }
319
+ return el;
320
+ }));
321
+ } else if (v && typeof v === 'object' && 'prop' in v) {
322
+ // Developer override object — extract val from proxy if present
323
+ const processed = { ...v };
324
+ if (processed.val && typeof processed.val === 'object' && PATH_SYMBOL in processed.val) {
325
+ processed.val = processed.val[PATH_SYMBOL];
326
+ }
327
+ result.push(processed);
328
+ } else {
329
+ result.push(v);
330
+ }
331
+ }
332
+ }
333
+ return result;
334
+ }