assign-gingerly 0.0.57 → 0.0.59

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/getValues.ts ADDED
@@ -0,0 +1,255 @@
1
+ /**
2
+ * getValues.ts — Synchronous value resolution for path strings.
3
+ *
4
+ * The synchronous counterpart to resolveValues. Resolves `?.`-prefixed path
5
+ * strings against a source object, with support for withMethods, aka aliases,
6
+ * synchronous protocols, arrays, and nested plain objects.
7
+ *
8
+ * For async protocol handlers, use resolveValues instead.
9
+ *
10
+ * @example
11
+ * import { getValues, getValue } from 'assign-gingerly/getValues.js';
12
+ *
13
+ * const result = getValues({
14
+ * name: '?.user?.name',
15
+ * greeting: '?.messages?.hello',
16
+ * count: 42
17
+ * }, source, { withMethods: ['querySelector'], aka: { q: 'querySelector' } });
18
+ */
19
+
20
+ import type { GetValuesOptions } from './types/assign-gingerly/types.js';
21
+
22
+ /**
23
+ * Apply alias substitutions to a path string.
24
+ * Replaces complete tokens between `?.` delimiters with their aliased values.
25
+ */
26
+ function applyAliases(path: string, aliasMap: Map<string, string>): string {
27
+ if (aliasMap.size === 0) return path;
28
+ const parts = path.split('?.');
29
+ const substituted = parts.map(part => aliasMap.get(part) ?? part);
30
+ return substituted.join('?.');
31
+ }
32
+
33
+ /**
34
+ * Path cache for parsed path strings.
35
+ * Avoids re-splitting the same path on repeated calls.
36
+ */
37
+ const pathCache = new Map<string, string[]>();
38
+
39
+ /**
40
+ * Parse a `?.`-delimited path string into segments, with caching.
41
+ */
42
+ function parseCachedPath(path: string): string[] {
43
+ let parts = pathCache.get(path);
44
+ if (!parts) {
45
+ parts = path.split('?.').filter(p => p.length > 0);
46
+ pathCache.set(path, parts);
47
+ }
48
+ return parts;
49
+ }
50
+
51
+ /**
52
+ * Navigate a path against a source object, optionally calling methods.
53
+ * Returns the resolved value at the end of the path.
54
+ */
55
+ function navigatePath(
56
+ source: any,
57
+ parts: string[],
58
+ withMethods: Set<string> | undefined
59
+ ): any {
60
+ let current = source;
61
+ let i = 0;
62
+
63
+ while (i < parts.length) {
64
+ if (current == null) return current;
65
+
66
+ const part = parts[i];
67
+
68
+ if (withMethods && withMethods.has(part)) {
69
+ const method = current[part];
70
+ if (typeof method === 'function') {
71
+ const nextPart = parts[i + 1];
72
+ if (nextPart !== undefined && !(withMethods.has(nextPart))) {
73
+ current = method.call(current, nextPart);
74
+ i += 2;
75
+ } else {
76
+ current = method.call(current);
77
+ i++;
78
+ }
79
+ } else {
80
+ current = current[part];
81
+ i++;
82
+ }
83
+ } else {
84
+ current = current[part];
85
+ i++;
86
+ }
87
+ }
88
+
89
+ return current;
90
+ }
91
+
92
+ /**
93
+ * Checks if a string value looks like a protocol reference.
94
+ */
95
+ function hasProtocol(value: string): boolean {
96
+ return value.includes('://');
97
+ }
98
+
99
+ /**
100
+ * Resolve a protocol-prefixed value synchronously.
101
+ */
102
+ function getProtocolValue(
103
+ value: string,
104
+ protocols: Record<string, (key: string) => any>,
105
+ options?: GetValuesOptions
106
+ ): any {
107
+ const protoEnd = value.indexOf('://');
108
+ const protocol = value.substring(0, protoEnd);
109
+
110
+ const handler = protocols[protocol];
111
+ if (!handler) return value; // not a recognized protocol
112
+
113
+ const rest = value.substring(protoEnd + 3);
114
+
115
+ const pathStart = rest.indexOf('?.');
116
+ const key = pathStart === -1 ? rest : rest.substring(0, pathStart);
117
+ const path = pathStart === -1 ? null : rest.substring(pathStart);
118
+
119
+ const resolved = handler(key);
120
+
121
+ if (path) {
122
+ return getValue(path, resolved, options);
123
+ }
124
+ return resolved;
125
+ }
126
+
127
+ /**
128
+ * Resolve path strings and protocols within an array (synchronous).
129
+ * Recurses into nested arrays and plain objects.
130
+ */
131
+ function getArray(
132
+ arr: any[],
133
+ source: any,
134
+ aliasMap: Map<string, string>,
135
+ withMethods: Set<string> | undefined,
136
+ protocols: Record<string, (key: string) => any> | undefined,
137
+ options?: GetValuesOptions
138
+ ): any[] {
139
+ const result: any[] = [];
140
+ for (const item of arr) {
141
+ if (typeof item === 'string' && item.startsWith('?.')) {
142
+ const aliased = applyAliases(item, aliasMap);
143
+ const parts = parseCachedPath(aliased);
144
+ result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
145
+ } else if (typeof item === 'string' && protocols && hasProtocol(item)) {
146
+ result.push(getProtocolValue(item, protocols, options));
147
+ } else if (Array.isArray(item)) {
148
+ result.push(getArray(item, source, aliasMap, withMethods, protocols, options));
149
+ } else if (item && typeof item === 'object') {
150
+ const proto = Object.getPrototypeOf(item);
151
+ if (proto === Object.prototype || proto === null) {
152
+ result.push(getValues(item, source, options));
153
+ } else {
154
+ result.push(item);
155
+ }
156
+ } else {
157
+ result.push(item);
158
+ }
159
+ }
160
+ return result;
161
+ }
162
+
163
+ /**
164
+ * Synchronously resolve RHS path strings in a pattern object against a source object.
165
+ *
166
+ * Any value that is a string starting with `?.` is treated as a path
167
+ * and resolved against the source object. Non-string values and strings
168
+ * not starting with `?.` pass through unchanged.
169
+ *
170
+ * @param pattern - Object whose RHS values may contain `?.` path strings
171
+ * @param source - Object to resolve paths against
172
+ * @param options - Optional withMethods, aka, and synchronous protocols
173
+ * @returns New object with path strings replaced by resolved values
174
+ */
175
+ export function getValues(
176
+ pattern: Record<string, any>,
177
+ source: any,
178
+ options?: GetValuesOptions
179
+ ): Record<string, any> {
180
+ // Build alias map
181
+ const aliasMap = new Map<string, string>();
182
+ if (options?.aka) {
183
+ for (const [alias, target] of Object.entries(options.aka)) {
184
+ aliasMap.set(alias, target);
185
+ }
186
+ }
187
+
188
+ // Build methods set
189
+ const withMethods = options?.withMethods
190
+ ? options.withMethods instanceof Set
191
+ ? options.withMethods
192
+ : new Set(options.withMethods)
193
+ : undefined;
194
+
195
+ const protocols = options?.protocols;
196
+
197
+ const result: Record<string, any> = {};
198
+ for (const [key, value] of Object.entries(pattern)) {
199
+ if (typeof value === 'string' && value.startsWith('?.')) {
200
+ const aliased = applyAliases(value, aliasMap);
201
+ const parts = parseCachedPath(aliased);
202
+ result[key] = parts.length === 0 ? source : navigatePath(source, parts, withMethods);
203
+ } else if (typeof value === 'string' && protocols && hasProtocol(value)) {
204
+ result[key] = getProtocolValue(value, protocols, options);
205
+ } else if (Array.isArray(value)) {
206
+ result[key] = getArray(value, source, aliasMap, withMethods, protocols, options);
207
+ } else if (typeof value === 'object' && value !== null) {
208
+ const proto = Object.getPrototypeOf(value);
209
+ if (proto === Object.prototype || proto === null) {
210
+ result[key] = getValues(value, source, options);
211
+ } else {
212
+ result[key] = value;
213
+ }
214
+ } else {
215
+ result[key] = value;
216
+ }
217
+ }
218
+ return result;
219
+ }
220
+
221
+ /**
222
+ * Synchronously resolve a single `?.`-delimited path string against a source object.
223
+ *
224
+ * @param path - A `?.`-delimited path string (e.g., '?.user?.name')
225
+ * @param source - Object to resolve the path against
226
+ * @param options - Optional withMethods and aka
227
+ * @returns The resolved value, or undefined if any segment is nullish
228
+ */
229
+ export function getValue(
230
+ path: string,
231
+ source: any,
232
+ options?: GetValuesOptions
233
+ ): any {
234
+ if (!path.startsWith('?.')) return path;
235
+
236
+ let aliased = path;
237
+ if (options?.aka) {
238
+ const aliasMap = new Map<string, string>();
239
+ for (const [alias, target] of Object.entries(options.aka)) {
240
+ aliasMap.set(alias, target);
241
+ }
242
+ aliased = applyAliases(path, aliasMap);
243
+ }
244
+
245
+ const parts = parseCachedPath(aliased);
246
+ if (parts.length === 0) return source;
247
+
248
+ const withMethods = options?.withMethods
249
+ ? options.withMethods instanceof Set
250
+ ? options.withMethods
251
+ : new Set(options.withMethods)
252
+ : undefined;
253
+
254
+ return navigatePath(source, parts, withMethods);
255
+ }
package/handlers/join.ts CHANGED
@@ -31,7 +31,7 @@
31
31
  * // If middleName is undefined, the sub-array [', ', undefined] is dropped entirely.
32
32
  */
33
33
 
34
- import type { AssignFromHandler } from '../assignFrom.js';
34
+ import type { AssignFromHandler } from '../assignFromAsync.js';
35
35
 
36
36
  /**
37
37
  * Process nested arrays with all-or-nothing null semantics.
@@ -73,7 +73,7 @@ export class LazyLoadHandler {
73
73
  }
74
74
  else {
75
75
  if (transitional) {
76
- ensureHideStyle(lhsTarget.getRootNode());
76
+ ensureHideStyle(lhsTarget.getRootNode(), hideClass, hideCss);
77
77
  withTransition(startMarker, 'show', true, () => {
78
78
  this.cloneAndInsertSync(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
79
79
  });
@@ -89,7 +89,7 @@ export class LazyLoadHandler {
89
89
  [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
90
90
  }
91
91
  if (transitional) {
92
- ensureHideStyle(lhsTarget.getRootNode());
92
+ ensureHideStyle(lhsTarget.getRootNode(), hideClass, hideCss);
93
93
  withTransition(startMarker, 'show', true, () => {
94
94
  this.cloneAndInsertSync(instantiate, startMarker, endMarker, lhsTarget, resolvedParams);
95
95
  });
@@ -19,7 +19,7 @@
19
19
  * }, { withMethods: ['querySelector'], from: myVM });
20
20
  */
21
21
 
22
- import type { AssignFromHandler } from '../assignFrom.js';
22
+ import type { AssignFromHandler } from '../assignFromAsync.js';
23
23
  import type { LazyLoadResolvedParams, LazyLoadInstantiatedContext } from '../types/assign-gingerly/types.js';
24
24
  import { withTransition, ensureHideStyle, DEFAULT_HIDE_CLASS } from '../transitionHelper.js';
25
25
  import { findMarkers, createMarkers, getNodesBetweenMarkers, findMarkersSibling, createMarkersSibling, MARKER_START_PREFIX, MARKER_END } from '../markerUtils.js';
@@ -102,7 +102,7 @@ export class LazyLoadHandler implements AssignFromHandler {
102
102
  } else {
103
103
  // Content was removed (forget mode) — re-clone
104
104
  if (transitional) {
105
- ensureHideStyle(lhsTarget.getRootNode());
105
+ ensureHideStyle(lhsTarget.getRootNode(), hideClass, hideCss);
106
106
  withTransition(startMarker, 'show', true, () => {
107
107
  this.cloneAndInsertSync(instantiate, startMarker!, endMarker!, lhsTarget, resolvedParams);
108
108
  });
@@ -118,7 +118,7 @@ export class LazyLoadHandler implements AssignFromHandler {
118
118
  [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
119
119
  }
120
120
  if (transitional) {
121
- ensureHideStyle(lhsTarget.getRootNode());
121
+ ensureHideStyle(lhsTarget.getRootNode(), hideClass, hideCss);
122
122
  withTransition(startMarker, 'show', true, () => {
123
123
  this.cloneAndInsertSync(instantiate, startMarker!, endMarker!, lhsTarget, resolvedParams);
124
124
  });
@@ -21,7 +21,7 @@
21
21
  */
22
22
 
23
23
  import { LazyLoadHandler } from './lazyLoad.js';
24
- import type { AssignFromHandler } from '../assignFrom.js';
24
+ import type { AssignFromHandler } from '../assignFromAsync.js';
25
25
  import type { LazyLoadSwitchResolvedParams } from '../types/assign-gingerly/types.js';
26
26
 
27
27
  /**
@@ -0,0 +1,203 @@
1
+ /**
2
+ * builtIns.manageTemplateList handler for assignFrom.
3
+ *
4
+ * Clones a template once per item in an iterable, distributing each item's
5
+ * properties into its clone via assignFrom. Manages the list over time —
6
+ * reconciling by key to add, remove, and update-in-place.
7
+ *
8
+ * This handler is auto-loaded by processHandlerCommands when
9
+ * `do: 'builtIns.manageTemplateList'` is encountered.
10
+ *
11
+ * @example
12
+ * assignFrom(document.body, {
13
+ * '?.querySelector?.tbody =>': {
14
+ * do: 'builtIns.manageTemplateList',
15
+ * resolve: {
16
+ * forEach: '?.rankings',
17
+ * instantiate: 'globalThis://country-ranking',
18
+ * },
19
+ * fromEachItem: {
20
+ * assignToFragment: { '?.querySelector?.tr?.ish': '?.' },
21
+ * withOptions: { withMethods: ['querySelector'], inferredAssignments: true },
22
+ * resolve: { key: '?.rank' }
23
+ * }
24
+ * }
25
+ * }, { from: vm, withMethods: ['querySelector'], protocols: { globalThis: k => globalThis[k] } });
26
+ */
27
+ import { findMarkers, createMarkers } from '../markerUtils.js';
28
+ import { resolveValue } from '../resolveValues.js';
29
+ import { assignFrom } from '../assignFrom.js';
30
+ import { processInferredAssignments } from '../inferredAssignments.js';
31
+ const listStateMap = new WeakMap();
32
+ /**
33
+ * ManageTemplateListHandler — clones a template per iterable item with keyed reconciliation.
34
+ */
35
+ export class ManageTemplateListHandler {
36
+ config;
37
+ constructor(config) {
38
+ this.config = config;
39
+ }
40
+ async assign(lhsTarget, resolvedParams, options) {
41
+ //return;
42
+ const { forEach: items, instantiate, method = 'appendChild', forget = false, markerName, yieldEvery, } = resolvedParams;
43
+ if (!(lhsTarget instanceof Element)) {
44
+ throw new Error('builtIns.manageTemplateList: lhsTarget must be a DOM Element');
45
+ }
46
+ if (!items || typeof items[Symbol.iterator] !== 'function') {
47
+ return; // Nothing to iterate
48
+ }
49
+ const fromEachItem = this.config.fromEachItem;
50
+ const assignToFragment = fromEachItem?.assignToFragment ?? {};
51
+ const withOptions = fromEachItem?.withOptions ?? {};
52
+ const perItemResolve = fromEachItem?.resolve ?? {};
53
+ const keyPath = perItemResolve.key; // e.g., '?.rank'
54
+ // fromSource config — assigns from the outer `from` (parent VM) to each clone
55
+ const fromSource = this.config.fromSource;
56
+ const sourceAssignToFragment = fromSource?.assignToFragment;
57
+ const sourceWithOptions = fromSource?.withOptions ?? {};
58
+ // Detect fast path: no assignToFragment patterns, just inferredAssignments
59
+ const hasAssignPatterns = Object.keys(assignToFragment).length > 0;
60
+ const inferredConfig = withOptions.inferredAssignments;
61
+ const useFastPath = !hasAssignPatterns && inferredConfig && !sourceAssignToFragment;
62
+ const name = markerName ?? getMarkerName(instantiate) ?? 'templateList';
63
+ // Find or create markers
64
+ let [startMarker, endMarker] = findMarkers(lhsTarget, name);
65
+ if (!startMarker || !endMarker) {
66
+ [startMarker, endMarker] = createMarkers(lhsTarget, name, method);
67
+ }
68
+ // Get or create list state
69
+ let state = listStateMap.get(startMarker);
70
+ if (!state) {
71
+ state = { keyToNodes: new Map(), keyOrder: [] };
72
+ listStateMap.set(startMarker, state);
73
+ }
74
+ // Convert iterable to array
75
+ const itemsArray = Array.from(items);
76
+ // Fast path: use processInferredAssignments directly when possible
77
+ const processInferred = useFastPath ? processInferredAssignments : null;
78
+ const newKeys = itemsArray.map((item, index) => {
79
+ if (keyPath) {
80
+ return resolveValue(keyPath, item);
81
+ }
82
+ return index; // Positional fallback when no key specified
83
+ });
84
+ // Determine what changed
85
+ const oldKeys = new Set(state.keyOrder);
86
+ const newKeySet = new Set(newKeys);
87
+ // Keys to remove
88
+ for (const oldKey of state.keyOrder) {
89
+ if (!newKeySet.has(oldKey)) {
90
+ const nodes = state.keyToNodes.get(oldKey);
91
+ if (nodes) {
92
+ if (forget) {
93
+ for (const node of nodes)
94
+ node.parentNode?.removeChild(node);
95
+ }
96
+ else {
97
+ for (const node of nodes) {
98
+ if (node instanceof Element)
99
+ node.setAttribute('hidden', '');
100
+ }
101
+ }
102
+ state.keyToNodes.delete(oldKey);
103
+ }
104
+ }
105
+ }
106
+ // Process items — clone new ones, update existing
107
+ const fragment = document.createDocumentFragment();
108
+ const newKeyToNodes = new Map();
109
+ for (let i = 0; i < itemsArray.length; i++) {
110
+ const item = itemsArray[i];
111
+ const key = newKeys[i];
112
+ if (state.keyToNodes.has(key) && oldKeys.has(key)) {
113
+ // Existing item — update in place
114
+ const existingNodes = state.keyToNodes.get(key);
115
+ const rootEl = existingNodes.find(n => n instanceof Element);
116
+ if (rootEl) {
117
+ const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
118
+ if (shouldYield)
119
+ await new Promise(r => setTimeout(r, 0));
120
+ if (processInferred) {
121
+ processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
122
+ }
123
+ else {
124
+ assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
125
+ }
126
+ if (sourceAssignToFragment && options?.from) {
127
+ assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
128
+ }
129
+ }
130
+ newKeyToNodes.set(key, existingNodes);
131
+ for (const node of existingNodes) {
132
+ if (node instanceof Element)
133
+ node.removeAttribute('hidden');
134
+ }
135
+ }
136
+ else {
137
+ // New item — clone template
138
+ let content;
139
+ if (instantiate instanceof HTMLTemplateElement) {
140
+ content = instantiate.content.cloneNode(true);
141
+ }
142
+ else if (instantiate instanceof DocumentFragment) {
143
+ content = instantiate.cloneNode(true);
144
+ }
145
+ else {
146
+ throw new Error('builtIns.manageTemplateList: instantiate must be an HTMLTemplateElement or DocumentFragment');
147
+ }
148
+ const clonedNodes = Array.from(content.childNodes);
149
+ // Apply per-item assignments to the cloned fragment
150
+ const rootEl = clonedNodes.find(n => n instanceof Element);
151
+ if (rootEl) {
152
+ const tempContainer = document.createDocumentFragment();
153
+ tempContainer.appendChild(content);
154
+ const shouldYield = yieldEvery && i > 0 && i % yieldEvery === 0;
155
+ if (shouldYield)
156
+ await new Promise(r => setTimeout(r, 0));
157
+ if (processInferred) {
158
+ processInferred(rootEl, item, inferredConfig === true ? { byItemprop: true } : inferredConfig);
159
+ }
160
+ else {
161
+ assignFrom(rootEl, assignToFragment, { from: item, ...withOptions });
162
+ }
163
+ if (sourceAssignToFragment && options?.from) {
164
+ assignFrom(rootEl, sourceAssignToFragment, { from: options.from, ...sourceWithOptions });
165
+ }
166
+ }
167
+ for (const node of clonedNodes) {
168
+ fragment.appendChild(node);
169
+ }
170
+ newKeyToNodes.set(key, clonedNodes);
171
+ }
172
+ }
173
+ // Wait for async rendering in the fragment to settle before committing
174
+ if (fragment.childNodes.length > 0) {
175
+ const waitOpt = resolvedParams.waitForSettled;
176
+ if (waitOpt) {
177
+ const { waitForSettled } = await import('../waitForSettled.js');
178
+ const idleMs = typeof waitOpt === 'object' ? waitOpt.idleMs : 100;
179
+ const timeout = typeof waitOpt === 'object' ? waitOpt.timeout : undefined;
180
+ try {
181
+ await waitForSettled(fragment, idleMs, timeout);
182
+ }
183
+ catch (e) {
184
+ console.warn('builtIns.manageTemplateList:', e.message, '— inserting fragment anyway');
185
+ }
186
+ }
187
+ // Insert new fragment before end marker
188
+ endMarker.parentNode.insertBefore(fragment, endMarker);
189
+ }
190
+ // Update state
191
+ state.keyToNodes = newKeyToNodes;
192
+ state.keyOrder = newKeys;
193
+ }
194
+ }
195
+ /**
196
+ * Get marker name from a template element.
197
+ */
198
+ function getMarkerName(templateEl) {
199
+ if (templateEl instanceof HTMLTemplateElement) {
200
+ return templateEl.id || undefined;
201
+ }
202
+ return undefined;
203
+ }