assign-gingerly 0.0.53 → 0.0.55
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 +847 -5
- package/assignFrom.js +229 -9
- package/assignFrom.ts +338 -9
- package/assignGingerly.js +34 -1
- package/assignGingerly.ts +54 -1
- package/beVigilant.js +73 -0
- package/beVigilant.ts +85 -0
- package/enhanceAll.js +106 -0
- package/enhanceAll.ts +138 -0
- package/handlers/join.js +74 -0
- package/handlers/join.ts +80 -0
- package/handlers/lazyLoad.js +212 -0
- package/handlers/lazyLoad.ts +307 -0
- package/handlers/lazyLoadSwitch.js +58 -0
- package/handlers/lazyLoadSwitch.ts +63 -0
- package/handlers/microDataJoin.js +184 -0
- package/handlers/microDataJoin.ts +270 -0
- package/inferencer/.gitmodules +3 -0
- package/inferencer/.vscode/settings.json +2 -0
- package/inferencer/InferencedPropagator.js +230 -0
- package/inferencer/InferencedPropagator.ts +269 -0
- package/inferencer/LICENSE +21 -0
- package/inferencer/README.md +524 -0
- package/inferencer/Requirements/SupportForPropagator.md +368 -0
- package/inferencer/imports.html +7 -0
- package/inferencer/inferencer.js +254 -0
- package/inferencer/inferencer.ts +292 -0
- package/inferencer/package-lock.json +129 -0
- package/inferencer/package.json +60 -0
- package/inferencer/playwright-report/data/507ad515125e13390ea07de92f22331c913fa068.md +55 -0
- package/inferencer/playwright-report/index.html +90 -0
- package/inferencer/playwright.config.ts +54 -0
- package/inferencer/test-results/.last-run.json +6 -0
- package/inferencer/test-results/inferencer-Inferencer-Enha-535bc-inferencer-tests-in-browser-chromium/error-context.md +55 -0
- package/inferencer/tests/inferencedPropagator.html +428 -0
- package/inferencer/tests/inferencedPropagator.spec.ts +18 -0
- package/inferencer/tests/inferencer.html +355 -0
- package/inferencer/tests/inferencer.spec.ts +19 -0
- package/inferencer/tsconfig.json +19 -0
- package/inferencer/types/.kiro/specs/conversion-template/README.md +128 -0
- package/inferencer/types/.kiro/specs/conversion-template/design.md +360 -0
- package/inferencer/types/.kiro/specs/conversion-template/requirements.md +191 -0
- package/inferencer/types/.kiro/specs/conversion-template/tasks.md +174 -0
- package/inferencer/types/.kiro/steering/coding-standards.md +53 -0
- package/inferencer/types/.kiro/steering/conversion-guide.md +108 -0
- package/inferencer/types/.kiro/steering/declarative-configuration.md +108 -0
- package/inferencer/types/.kiro/steering/emc-json-serializability.md +306 -0
- package/inferencer/types/EnhancementConversionInstructions.md +1854 -0
- package/inferencer/types/LICENSE +21 -0
- package/inferencer/types/NewCustomElement.md +388 -0
- package/inferencer/types/NewCustomElementFeature.md +683 -0
- package/inferencer/types/NewEnhancementInstructions.md +705 -0
- package/inferencer/types/README.md +2 -0
- package/inferencer/types/agrace/types.d.ts +11 -0
- package/inferencer/types/assign-gingerly/types.d.ts +572 -0
- package/inferencer/types/be-a-beacon/types.d.ts +17 -0
- package/inferencer/types/be-bound/types.d.ts +66 -0
- package/inferencer/types/be-buttoned-up/types.d.ts +19 -0
- package/inferencer/types/be-calculating/types.d.ts +54 -0
- package/inferencer/types/be-clonable/types.d.ts +38 -0
- package/inferencer/types/be-committed/types.d.ts +22 -0
- package/inferencer/types/be-consoling/types.d.ts +24 -0
- package/inferencer/types/be-decked-with/types.d.ts +26 -0
- package/inferencer/types/be-delible/types.d.ts +27 -0
- package/inferencer/types/be-dispatching/types.d.ts +34 -0
- package/inferencer/types/be-evanescent/types.d.ts +20 -0
- package/inferencer/types/be-flashy/types.d.ts +21 -0
- package/inferencer/types/be-gone/types.d.ts +25 -0
- package/inferencer/types/be-observing/types.d.ts +55 -0
- package/inferencer/types/be-reflective/types.d.ts +78 -0
- package/inferencer/types/be-reformable/types.d.ts +49 -0
- package/inferencer/types/be-render-neutral/types.d.ts +32 -0
- package/inferencer/types/be-switched/types.d.ts +146 -0
- package/inferencer/types/be-typed/types.d.ts +32 -0
- package/inferencer/types/be-valued/types.d.ts +22 -0
- package/inferencer/types/data-props/types.d.ts +34 -0
- package/inferencer/types/do-inc/types.d.ts +56 -0
- package/inferencer/types/do-invoke/types.d.ts +38 -0
- package/inferencer/types/do-merge/types.d.ts +28 -0
- package/inferencer/types/do-toggle/types.d.ts +31 -0
- package/inferencer/types/face-up/types.d.ts +100 -0
- package/inferencer/types/fetch-for/types.d.ts +36 -0
- package/inferencer/types/folder-picker/types.d.ts +21 -0
- package/inferencer/types/global.d.ts +29 -0
- package/inferencer/types/id-generation/types.d.ts +26 -0
- package/inferencer/types/inferencer/types.d.ts +46 -0
- package/inferencer/types/mount-observer/types.d.ts +363 -0
- package/inferencer/types/nested-regex-groups/types.d.ts +107 -0
- package/inferencer/types/pipe-in/types.d.ts +52 -0
- package/inferencer/types/roundabout/types.d.ts +268 -0
- package/inferencer/types/soak-up/types.d.ts +40 -0
- package/inferencer/types/templ-maker/types.d.ts +43 -0
- package/inferencer/types/time-ticker/types.d.ts +62 -0
- package/inferencer/types/truth-sourcer/types.d.ts +44 -0
- package/inferencer/upSearch.js +27 -0
- package/inferencer/upSearch.ts +26 -0
- package/inferencer/withScopePerimeter.js +27 -0
- package/inferencer/withScopePerimeter.ts +33 -0
- package/inferredAssignments.js +38 -0
- package/inferredAssignments.ts +65 -0
- package/isAllowedImportPath.js +42 -0
- package/isAllowedImportPath.ts +53 -0
- package/package.json +57 -3
- package/paths.js +231 -0
- package/paths.ts +413 -0
- package/processHandlerCommands.js +188 -0
- package/processHandlerCommands.ts +217 -0
- package/resolveIdRef.js +144 -0
- package/resolveIdRef.ts +170 -0
- package/resolveValues.js +41 -2
- package/resolveValues.ts +41 -1
- package/transitionHelper.js +109 -0
- package/transitionHelper.ts +132 -0
- package/types/assign-gingerly/types.d.ts +89 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* processHandlerCommands - Handles ` =>` operator keys in assignFrom.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imported only when ` =>` keys are detected in the pattern.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolveValues } from './resolveValues.js';
|
|
8
|
+
import { evaluatePathWithMethods } from './assignGingerly.js';
|
|
9
|
+
import { isAllowedImportPath } from './isAllowedImportPath.js';
|
|
10
|
+
import type { AssignPermissions } from './isAllowedImportPath.js';
|
|
11
|
+
import type { AssignFromOptions, AssignFromHandlerConstructor } from './assignFrom.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Map of built-in handler names to their module paths.
|
|
15
|
+
* These are auto-loaded on demand — no explicit import required.
|
|
16
|
+
*/
|
|
17
|
+
const BUILT_IN_MAP: Record<string, string> = {
|
|
18
|
+
'builtIns.lazyLoad': './handlers/lazyLoad.js',
|
|
19
|
+
'builtIns.lazyLoadSwitch': './handlers/lazyLoadSwitch.js',
|
|
20
|
+
'builtIns.join': './handlers/join.js',
|
|
21
|
+
'builtIns.microDataJoin': './handlers/microDataJoin.js',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find a handler class in a dynamically imported module.
|
|
26
|
+
* Checks default export first, then searches for the first class with `assign` on prototype.
|
|
27
|
+
*/
|
|
28
|
+
function findHandlerInModule(module: any): AssignFromHandlerConstructor | undefined {
|
|
29
|
+
// Check default export first
|
|
30
|
+
if (module.default && typeof module.default === 'function'
|
|
31
|
+
&& module.default.prototype && 'assign' in module.default.prototype) {
|
|
32
|
+
return module.default;
|
|
33
|
+
}
|
|
34
|
+
// Search other exports
|
|
35
|
+
for (const key of Object.keys(module)) {
|
|
36
|
+
const exported = module[key];
|
|
37
|
+
if (typeof exported === 'function' && exported.prototype && 'assign' in exported.prototype) {
|
|
38
|
+
return exported as AssignFromHandlerConstructor;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Dynamically load a built-in handler by name.
|
|
46
|
+
* Returns the handler constructor, or undefined if the name isn't a recognized built-in.
|
|
47
|
+
*/
|
|
48
|
+
async function loadBuiltIn(name: string): Promise<AssignFromHandlerConstructor | undefined> {
|
|
49
|
+
const path = BUILT_IN_MAP[name];
|
|
50
|
+
if (!path) return undefined;
|
|
51
|
+
const module = await import(path);
|
|
52
|
+
return findHandlerInModule(module);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a handler from options.handlers (class constructor or import path).
|
|
57
|
+
*/
|
|
58
|
+
async function resolveFromHandlers(
|
|
59
|
+
name: string,
|
|
60
|
+
handlers: Record<string, AssignFromHandlerConstructor | string> | undefined,
|
|
61
|
+
permissions?: AssignPermissions
|
|
62
|
+
): Promise<AssignFromHandlerConstructor | undefined> {
|
|
63
|
+
if (!handlers || !(name in handlers)) return undefined;
|
|
64
|
+
|
|
65
|
+
const entry = handlers[name];
|
|
66
|
+
|
|
67
|
+
// Class constructor — use directly
|
|
68
|
+
if (typeof entry === 'function') {
|
|
69
|
+
return entry as AssignFromHandlerConstructor;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Import path string — validate and dynamically import
|
|
73
|
+
if (typeof entry === 'string') {
|
|
74
|
+
if (!permissions?.crossDomainImports && !isAllowedImportPath(entry)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`assignFrom: handler "${name}" has an invalid import path "${entry}". ` +
|
|
77
|
+
`Only relative, absolute, or bare specifier paths are allowed (no cross-domain URLs). ` +
|
|
78
|
+
`Pass { crossDomainImports: true } in permissions to override.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const module = await import(entry);
|
|
82
|
+
const HandlerClass = findHandlerInModule(module);
|
|
83
|
+
if (!HandlerClass) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`assignFrom: handler "${name}" — module "${entry}" does not export a valid handler class.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return HandlerClass;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Process all handler command keys (ending with ' =>') in a pattern.
|
|
96
|
+
*
|
|
97
|
+
* @param target - The target object being assigned to
|
|
98
|
+
* @param handlerKeys - Array of keys ending with ' =>'
|
|
99
|
+
* @param pattern - The original pattern object
|
|
100
|
+
* @param options - The assignFrom options
|
|
101
|
+
* @param handlerRegistry - The registry of handler classes
|
|
102
|
+
*/
|
|
103
|
+
export async function processHandlerCommands(
|
|
104
|
+
target: any,
|
|
105
|
+
handlerKeys: string[],
|
|
106
|
+
pattern: Record<string, any>,
|
|
107
|
+
options: AssignFromOptions,
|
|
108
|
+
permissions?: AssignPermissions
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
for (const key of handlerKeys) {
|
|
111
|
+
const lhsPath = key.substring(0, key.length - 3); // Remove ' =>'
|
|
112
|
+
const rhs = pattern[key];
|
|
113
|
+
|
|
114
|
+
// Normalize RHS to an array of handler configs
|
|
115
|
+
const configs = Array.isArray(rhs) ? rhs : [rhs];
|
|
116
|
+
|
|
117
|
+
// Validate — no nested arrays
|
|
118
|
+
for (const config of configs) {
|
|
119
|
+
if (Array.isArray(config)) {
|
|
120
|
+
throw new Error(`assignFrom: handler command "${key}" does not support nested arrays`);
|
|
121
|
+
}
|
|
122
|
+
if (!config || typeof config !== 'object' || !config.do) {
|
|
123
|
+
throw new Error(`assignFrom: handler command "${key}" requires a config object with a "do" field`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Empty array — skip silently
|
|
128
|
+
if (configs.length === 0) continue;
|
|
129
|
+
|
|
130
|
+
// Resolve the LHS path, preserving parent + key for return-value assignment.
|
|
131
|
+
// lhsParent[lhsKey] === lhsTarget (the current value at the path)
|
|
132
|
+
let lhsTarget: any;
|
|
133
|
+
let lhsParent: any = undefined;
|
|
134
|
+
let lhsKey: string | undefined = undefined;
|
|
135
|
+
|
|
136
|
+
if (lhsPath.startsWith('?.')) {
|
|
137
|
+
const pathParts = lhsPath.split('?.').filter(p => p.length > 0);
|
|
138
|
+
const withMethodsSet = options.withMethods
|
|
139
|
+
? options.withMethods instanceof Set
|
|
140
|
+
? options.withMethods
|
|
141
|
+
: new Set(options.withMethods)
|
|
142
|
+
: undefined;
|
|
143
|
+
|
|
144
|
+
if (withMethodsSet && pathParts.length > 0) {
|
|
145
|
+
const result = evaluatePathWithMethods(target, pathParts, undefined, withMethodsSet);
|
|
146
|
+
lhsParent = result.target;
|
|
147
|
+
lhsKey = result.lastKey;
|
|
148
|
+
lhsTarget = result.target[result.lastKey];
|
|
149
|
+
// If last key is a method, call it to get the target
|
|
150
|
+
if (result.isMethod && typeof result.target[result.lastKey] === 'function') {
|
|
151
|
+
lhsTarget = result.target[result.lastKey].call(result.target);
|
|
152
|
+
lhsParent = undefined; // Can't assign back to a method call result
|
|
153
|
+
lhsKey = undefined;
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// Simple path navigation — walk to parent, keep last key
|
|
157
|
+
if (pathParts.length === 0) {
|
|
158
|
+
lhsTarget = target;
|
|
159
|
+
} else if (pathParts.length === 1) {
|
|
160
|
+
lhsParent = target;
|
|
161
|
+
lhsKey = pathParts[0];
|
|
162
|
+
lhsTarget = target[pathParts[0]];
|
|
163
|
+
} else {
|
|
164
|
+
let current = target;
|
|
165
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
166
|
+
if (current == null) break;
|
|
167
|
+
current = current[pathParts[i]];
|
|
168
|
+
}
|
|
169
|
+
lhsParent = current;
|
|
170
|
+
lhsKey = pathParts[pathParts.length - 1];
|
|
171
|
+
lhsTarget = current != null ? current[lhsKey] : undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else if (lhsPath) {
|
|
175
|
+
lhsParent = target;
|
|
176
|
+
lhsKey = lhsPath;
|
|
177
|
+
lhsTarget = target[lhsPath];
|
|
178
|
+
} else {
|
|
179
|
+
lhsTarget = target;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Execute handlers sequentially, sharing the same lhsTarget
|
|
183
|
+
for (const config of configs) {
|
|
184
|
+
// 1. Check options.handlers (local, per-call)
|
|
185
|
+
let HandlerClass = await resolveFromHandlers(config.do, options.handlers, permissions);
|
|
186
|
+
|
|
187
|
+
// 2. Fallback to built-in auto-load
|
|
188
|
+
if (!HandlerClass && config.do.startsWith('builtIns.')) {
|
|
189
|
+
HandlerClass = await loadBuiltIn(config.do);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!HandlerClass) {
|
|
193
|
+
throw new Error(`assignFrom: unknown handler "${config.do}". Provide it in options.handlers.`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Resolve 'resolve' map if present — uses full resolveValues (paths, protocols, literals)
|
|
197
|
+
let resolvedParams: Record<string, any> = {};
|
|
198
|
+
if (config.resolve) {
|
|
199
|
+
resolvedParams = await resolveValues(config.resolve, options.from, {
|
|
200
|
+
withMethods: options.withMethods,
|
|
201
|
+
aka: options.aka,
|
|
202
|
+
protocols: options.protocols
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Instantiate and invoke the handler
|
|
207
|
+
const handler = new HandlerClass(config);
|
|
208
|
+
const result = await handler.assign(lhsTarget, resolvedParams, options);
|
|
209
|
+
|
|
210
|
+
// Return-value protocol: if handler returns a non-undefined value,
|
|
211
|
+
// assign it back to the LHS path
|
|
212
|
+
if (result !== undefined && lhsParent != null && lhsKey != null) {
|
|
213
|
+
lhsParent[lhsKey] = result;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
package/resolveIdRef.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveIdRef.ts — Cached element resolution via #[x] syntax.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imported by assignFrom when `withIds` is provided or `#[x]` patterns are detected.
|
|
5
|
+
* Provides lazy, WeakRef-cached element lookups keyed by variable name.
|
|
6
|
+
*
|
|
7
|
+
* First access: runs the query against the target, auto-assigns an ID if needed, caches via WeakRef.
|
|
8
|
+
* Subsequent access: WeakRef.deref() (~10ns) or getElementById fallback (~20-100ns).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Module-level cache: rootNode → Map<varName, { id, WeakRef }>
|
|
12
|
+
* WeakMap ensures cleanup when rootNode is GC'd.
|
|
13
|
+
*/
|
|
14
|
+
const idCacheMap = new WeakMap();
|
|
15
|
+
/**
|
|
16
|
+
* Counter per rootNode for generating unique IDs.
|
|
17
|
+
*/
|
|
18
|
+
const idCounterMap = new WeakMap();
|
|
19
|
+
/**
|
|
20
|
+
* Generate a unique ID within a rootNode.
|
|
21
|
+
* Format: _ag0, _ag1, _ag2, ...
|
|
22
|
+
*/
|
|
23
|
+
function generateUniqueId(rootNode) {
|
|
24
|
+
let counter = idCounterMap.get(rootNode) ?? 0;
|
|
25
|
+
let id;
|
|
26
|
+
// Ensure uniqueness (skip if ID already exists in the document)
|
|
27
|
+
do {
|
|
28
|
+
id = `_ag${counter}`;
|
|
29
|
+
counter++;
|
|
30
|
+
} while (rootNode.getElementById?.(id));
|
|
31
|
+
idCounterMap.set(rootNode, counter);
|
|
32
|
+
return id;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a single #[varName] reference lazily.
|
|
36
|
+
*
|
|
37
|
+
* @param varName - The variable name (e.g., 'x' from '#[x]')
|
|
38
|
+
* @param target - The target element to query against
|
|
39
|
+
* @param withIds - The withIds configuration map
|
|
40
|
+
* @returns The resolved element, or undefined if not found
|
|
41
|
+
*/
|
|
42
|
+
export function resolveIdVariable(varName, target, withIds) {
|
|
43
|
+
const config = withIds[varName];
|
|
44
|
+
if (config === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
const rootNode = target.getRootNode?.() ?? target;
|
|
47
|
+
// Get or create cache for this rootNode
|
|
48
|
+
let cache = idCacheMap.get(rootNode);
|
|
49
|
+
if (!cache) {
|
|
50
|
+
cache = new Map();
|
|
51
|
+
idCacheMap.set(rootNode, cache);
|
|
52
|
+
}
|
|
53
|
+
// Check cache first
|
|
54
|
+
const cached = cache.get(varName);
|
|
55
|
+
if (cached) {
|
|
56
|
+
const el = cached.ref.deref();
|
|
57
|
+
if (el)
|
|
58
|
+
return el;
|
|
59
|
+
// WeakRef was collected — try getElementById fallback
|
|
60
|
+
const el2 = rootNode.getElementById?.(cached.id);
|
|
61
|
+
if (el2) {
|
|
62
|
+
cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
|
|
63
|
+
return el2;
|
|
64
|
+
}
|
|
65
|
+
// Element no longer exists — fall through to re-query
|
|
66
|
+
}
|
|
67
|
+
// First time or cache miss — resolve the element
|
|
68
|
+
let el = null;
|
|
69
|
+
if (typeof config === 'string') {
|
|
70
|
+
// String form: existing ID — use getElementById directly
|
|
71
|
+
el = rootNode.getElementById?.(config) ?? null;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Object form: { qry } — run querySelector against target
|
|
75
|
+
el = target.querySelector?.(config.qry) ?? null;
|
|
76
|
+
}
|
|
77
|
+
if (!el)
|
|
78
|
+
return undefined;
|
|
79
|
+
// Ensure the element has an ID
|
|
80
|
+
let id = el.id;
|
|
81
|
+
if (!id) {
|
|
82
|
+
id = generateUniqueId(rootNode);
|
|
83
|
+
el.id = id;
|
|
84
|
+
}
|
|
85
|
+
// Cache it
|
|
86
|
+
cache.set(varName, { id, ref: new WeakRef(el) });
|
|
87
|
+
return el;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if a key starts with #[...] syntax.
|
|
91
|
+
*/
|
|
92
|
+
export function hasIdRef(key) {
|
|
93
|
+
return key.startsWith('#[');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Extract the variable name and remaining path from a #[x]... key.
|
|
97
|
+
* Returns [varName, remainingPath] or null if not a valid #[x] reference.
|
|
98
|
+
*
|
|
99
|
+
* Examples:
|
|
100
|
+
* '#[x]' → ['x', '']
|
|
101
|
+
* '#[x] =>' → ['x', ''] (handler suffix handled separately)
|
|
102
|
+
* '#[x]?.querySelector?..child' → ['x', '?.querySelector?..child']
|
|
103
|
+
* '#[x]?.textContent =>' → ['x', '?.textContent'] (handler suffix handled separately)
|
|
104
|
+
*/
|
|
105
|
+
export function parseIdRef(key) {
|
|
106
|
+
const closeIdx = key.indexOf(']');
|
|
107
|
+
if (closeIdx === -1)
|
|
108
|
+
return null;
|
|
109
|
+
const varName = key.substring(2, closeIdx); // skip '#['
|
|
110
|
+
if (!varName)
|
|
111
|
+
return null;
|
|
112
|
+
let remainingPath = key.substring(closeIdx + 1);
|
|
113
|
+
// Remove handler suffix if present (caller handles it separately)
|
|
114
|
+
if (remainingPath.endsWith(' =>')) {
|
|
115
|
+
remainingPath = remainingPath.substring(0, remainingPath.length - 3);
|
|
116
|
+
}
|
|
117
|
+
return { varName, remainingPath };
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Register an element in the WeakRef cache for fast subsequent access.
|
|
121
|
+
* Auto-assigns an ID if the element doesn't have one.
|
|
122
|
+
*
|
|
123
|
+
* Used by beVigilant to cache newly discovered elements so that future
|
|
124
|
+
* assignFrom calls can resolve them via getElementById/WeakRef.
|
|
125
|
+
*
|
|
126
|
+
* @param target - The target element (used to determine rootNode)
|
|
127
|
+
* @param varName - The cache key (typically the itemprop name)
|
|
128
|
+
* @param element - The element to cache
|
|
129
|
+
*/
|
|
130
|
+
export function registerInCache(target, varName, element) {
|
|
131
|
+
const rootNode = target.getRootNode?.() ?? target;
|
|
132
|
+
let cache = idCacheMap.get(rootNode);
|
|
133
|
+
if (!cache) {
|
|
134
|
+
cache = new Map();
|
|
135
|
+
idCacheMap.set(rootNode, cache);
|
|
136
|
+
}
|
|
137
|
+
// Ensure the element has an ID
|
|
138
|
+
let id = element.id;
|
|
139
|
+
if (!id) {
|
|
140
|
+
id = generateUniqueId(rootNode);
|
|
141
|
+
element.id = id;
|
|
142
|
+
}
|
|
143
|
+
cache.set(varName, { id, ref: new WeakRef(element) });
|
|
144
|
+
}
|
package/resolveIdRef.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveIdRef.ts — Cached element resolution via #[x] syntax.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imported by assignFrom when `withIds` is provided or `#[x]` patterns are detected.
|
|
5
|
+
* Provides lazy, WeakRef-cached element lookups keyed by variable name.
|
|
6
|
+
*
|
|
7
|
+
* First access: runs the query against the target, auto-assigns an ID if needed, caches via WeakRef.
|
|
8
|
+
* Subsequent access: WeakRef.deref() (~10ns) or getElementById fallback (~20-100ns).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configuration for a withIds entry.
|
|
13
|
+
*/
|
|
14
|
+
export type WithIdConfig = string | { qry: string };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Module-level cache: rootNode → Map<varName, { id, WeakRef }>
|
|
18
|
+
* WeakMap ensures cleanup when rootNode is GC'd.
|
|
19
|
+
*/
|
|
20
|
+
const idCacheMap = new WeakMap<object, Map<string, { id: string; ref: WeakRef<Element> }>>();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Counter per rootNode for generating unique IDs.
|
|
24
|
+
*/
|
|
25
|
+
const idCounterMap = new WeakMap<object, number>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a unique ID within a rootNode.
|
|
29
|
+
* Format: _ag0, _ag1, _ag2, ...
|
|
30
|
+
*/
|
|
31
|
+
function generateUniqueId(rootNode: any): string {
|
|
32
|
+
let counter = idCounterMap.get(rootNode) ?? 0;
|
|
33
|
+
let id: string;
|
|
34
|
+
// Ensure uniqueness (skip if ID already exists in the document)
|
|
35
|
+
do {
|
|
36
|
+
id = `_ag${counter}`;
|
|
37
|
+
counter++;
|
|
38
|
+
} while (rootNode.getElementById?.(id));
|
|
39
|
+
idCounterMap.set(rootNode, counter);
|
|
40
|
+
return id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a single #[varName] reference lazily.
|
|
45
|
+
*
|
|
46
|
+
* @param varName - The variable name (e.g., 'x' from '#[x]')
|
|
47
|
+
* @param target - The target element to query against
|
|
48
|
+
* @param withIds - The withIds configuration map
|
|
49
|
+
* @returns The resolved element, or undefined if not found
|
|
50
|
+
*/
|
|
51
|
+
export function resolveIdVariable(
|
|
52
|
+
varName: string,
|
|
53
|
+
target: any,
|
|
54
|
+
withIds: Record<string, WithIdConfig>
|
|
55
|
+
): Element | undefined {
|
|
56
|
+
const config = withIds[varName];
|
|
57
|
+
if (config === undefined) return undefined;
|
|
58
|
+
|
|
59
|
+
const rootNode = target.getRootNode?.() ?? target;
|
|
60
|
+
|
|
61
|
+
// Get or create cache for this rootNode
|
|
62
|
+
let cache = idCacheMap.get(rootNode);
|
|
63
|
+
if (!cache) {
|
|
64
|
+
cache = new Map();
|
|
65
|
+
idCacheMap.set(rootNode, cache);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check cache first
|
|
69
|
+
const cached = cache.get(varName);
|
|
70
|
+
if (cached) {
|
|
71
|
+
const el = cached.ref.deref();
|
|
72
|
+
if (el) return el;
|
|
73
|
+
|
|
74
|
+
// WeakRef was collected — try getElementById fallback
|
|
75
|
+
const el2 = rootNode.getElementById?.(cached.id);
|
|
76
|
+
if (el2) {
|
|
77
|
+
cache.set(varName, { id: cached.id, ref: new WeakRef(el2) });
|
|
78
|
+
return el2;
|
|
79
|
+
}
|
|
80
|
+
// Element no longer exists — fall through to re-query
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// First time or cache miss — resolve the element
|
|
84
|
+
let el: Element | null = null;
|
|
85
|
+
|
|
86
|
+
if (typeof config === 'string') {
|
|
87
|
+
// String form: existing ID — use getElementById directly
|
|
88
|
+
el = rootNode.getElementById?.(config) ?? null;
|
|
89
|
+
} else {
|
|
90
|
+
// Object form: { qry } — run querySelector against target
|
|
91
|
+
el = target.querySelector?.(config.qry) ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!el) return undefined;
|
|
95
|
+
|
|
96
|
+
// Ensure the element has an ID
|
|
97
|
+
let id = el.id;
|
|
98
|
+
if (!id) {
|
|
99
|
+
id = generateUniqueId(rootNode);
|
|
100
|
+
el.id = id;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Cache it
|
|
104
|
+
cache.set(varName, { id, ref: new WeakRef(el) });
|
|
105
|
+
return el;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a key starts with #[...] syntax.
|
|
110
|
+
*/
|
|
111
|
+
export function hasIdRef(key: string): boolean {
|
|
112
|
+
return key.startsWith('#[');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract the variable name and remaining path from a #[x]... key.
|
|
117
|
+
* Returns [varName, remainingPath] or null if not a valid #[x] reference.
|
|
118
|
+
*
|
|
119
|
+
* Examples:
|
|
120
|
+
* '#[x]' → ['x', '']
|
|
121
|
+
* '#[x] =>' → ['x', ''] (handler suffix handled separately)
|
|
122
|
+
* '#[x]?.querySelector?..child' → ['x', '?.querySelector?..child']
|
|
123
|
+
* '#[x]?.textContent =>' → ['x', '?.textContent'] (handler suffix handled separately)
|
|
124
|
+
*/
|
|
125
|
+
export function parseIdRef(key: string): { varName: string; remainingPath: string } | null {
|
|
126
|
+
const closeIdx = key.indexOf(']');
|
|
127
|
+
if (closeIdx === -1) return null;
|
|
128
|
+
|
|
129
|
+
const varName = key.substring(2, closeIdx); // skip '#['
|
|
130
|
+
if (!varName) return null;
|
|
131
|
+
|
|
132
|
+
let remainingPath = key.substring(closeIdx + 1);
|
|
133
|
+
|
|
134
|
+
// Remove handler suffix if present (caller handles it separately)
|
|
135
|
+
if (remainingPath.endsWith(' =>')) {
|
|
136
|
+
remainingPath = remainingPath.substring(0, remainingPath.length - 3);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { varName, remainingPath };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Register an element in the WeakRef cache for fast subsequent access.
|
|
144
|
+
* Auto-assigns an ID if the element doesn't have one.
|
|
145
|
+
*
|
|
146
|
+
* Used by beVigilant to cache newly discovered elements so that future
|
|
147
|
+
* assignFrom calls can resolve them via getElementById/WeakRef.
|
|
148
|
+
*
|
|
149
|
+
* @param target - The target element (used to determine rootNode)
|
|
150
|
+
* @param varName - The cache key (typically the itemprop name)
|
|
151
|
+
* @param element - The element to cache
|
|
152
|
+
*/
|
|
153
|
+
export function registerInCache(target: any, varName: string, element: Element): void {
|
|
154
|
+
const rootNode = target.getRootNode?.() ?? target;
|
|
155
|
+
|
|
156
|
+
let cache = idCacheMap.get(rootNode);
|
|
157
|
+
if (!cache) {
|
|
158
|
+
cache = new Map();
|
|
159
|
+
idCacheMap.set(rootNode, cache);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Ensure the element has an ID
|
|
163
|
+
let id = element.id;
|
|
164
|
+
if (!id) {
|
|
165
|
+
id = generateUniqueId(rootNode);
|
|
166
|
+
element.id = id;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
cache.set(varName, { id, ref: new WeakRef(element) });
|
|
170
|
+
}
|
package/resolveValues.js
CHANGED
|
@@ -98,6 +98,40 @@ function navigatePath(source, parts, withMethods) {
|
|
|
98
98
|
}
|
|
99
99
|
return current;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolve path strings and protocol references within an array.
|
|
103
|
+
* Recurses into nested arrays and plain objects. Non-string elements,
|
|
104
|
+
* class instances, and other non-plain objects pass through unchanged.
|
|
105
|
+
*/
|
|
106
|
+
async function resolveArray(arr, source, aliasMap, withMethods, protocols, options) {
|
|
107
|
+
const result = [];
|
|
108
|
+
for (const item of arr) {
|
|
109
|
+
if (typeof item === 'string' && item.startsWith('?.')) {
|
|
110
|
+
const aliased = applyAliases(item, aliasMap);
|
|
111
|
+
const parts = parseCachedPath(aliased);
|
|
112
|
+
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
|
|
113
|
+
}
|
|
114
|
+
else if (typeof item === 'string' && protocols && hasProtocol(item)) {
|
|
115
|
+
result.push(await resolveProtocolValue(item, protocols, options));
|
|
116
|
+
}
|
|
117
|
+
else if (Array.isArray(item)) {
|
|
118
|
+
result.push(await resolveArray(item, source, aliasMap, withMethods, protocols, options));
|
|
119
|
+
}
|
|
120
|
+
else if (item && typeof item === 'object') {
|
|
121
|
+
const proto = Object.getPrototypeOf(item);
|
|
122
|
+
if (proto === Object.prototype || proto === null) {
|
|
123
|
+
result.push(await resolveValues(item, source, options));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
result.push(item);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
result.push(item);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
101
135
|
/**
|
|
102
136
|
* Resolve RHS path strings in a pattern object against a source object.
|
|
103
137
|
*
|
|
@@ -160,13 +194,18 @@ export async function resolveValues(pattern, source, options) {
|
|
|
160
194
|
// Protocol-prefixed value — resolve asynchronously
|
|
161
195
|
result[key] = await resolveProtocolValue(value, protocols, options);
|
|
162
196
|
}
|
|
163
|
-
else if (
|
|
197
|
+
else if (Array.isArray(value)) {
|
|
198
|
+
// Resolve path strings and protocols within arrays (recursing into nested arrays)
|
|
199
|
+
result[key] = await resolveArray(value, source, aliasMap, withMethods, protocols, options);
|
|
200
|
+
}
|
|
201
|
+
else if (typeof value === 'object' && value !== null) {
|
|
164
202
|
// Recursively resolve nested plain objects (e.g., headers: { "...": "globalThis://key" })
|
|
165
203
|
// Only recurse into plain objects — skip DOM elements, class instances, etc.
|
|
166
204
|
const proto = Object.getPrototypeOf(value);
|
|
167
205
|
if (proto === Object.prototype || proto === null) {
|
|
168
206
|
result[key] = await resolveValues(value, source, options);
|
|
169
|
-
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
170
209
|
result[key] = value;
|
|
171
210
|
}
|
|
172
211
|
}
|
package/resolveValues.ts
CHANGED
|
@@ -147,6 +147,43 @@ function navigatePath(
|
|
|
147
147
|
return current;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Resolve path strings and protocol references within an array.
|
|
152
|
+
* Recurses into nested arrays and plain objects. Non-string elements,
|
|
153
|
+
* class instances, and other non-plain objects pass through unchanged.
|
|
154
|
+
*/
|
|
155
|
+
async function resolveArray(
|
|
156
|
+
arr: any[],
|
|
157
|
+
source: any,
|
|
158
|
+
aliasMap: Map<string, string>,
|
|
159
|
+
withMethods: Set<string> | undefined,
|
|
160
|
+
protocols: Record<string, (key: string) => any | Promise<any>> | undefined,
|
|
161
|
+
options?: ResolveValuesOptions
|
|
162
|
+
): Promise<any[]> {
|
|
163
|
+
const result: any[] = [];
|
|
164
|
+
for (const item of arr) {
|
|
165
|
+
if (typeof item === 'string' && item.startsWith('?.')) {
|
|
166
|
+
const aliased = applyAliases(item, aliasMap);
|
|
167
|
+
const parts = parseCachedPath(aliased);
|
|
168
|
+
result.push(parts.length === 0 ? source : navigatePath(source, parts, withMethods));
|
|
169
|
+
} else if (typeof item === 'string' && protocols && hasProtocol(item)) {
|
|
170
|
+
result.push(await resolveProtocolValue(item, protocols, options));
|
|
171
|
+
} else if (Array.isArray(item)) {
|
|
172
|
+
result.push(await resolveArray(item, source, aliasMap, withMethods, protocols, options));
|
|
173
|
+
} else if (item && typeof item === 'object') {
|
|
174
|
+
const proto = Object.getPrototypeOf(item);
|
|
175
|
+
if (proto === Object.prototype || proto === null) {
|
|
176
|
+
result.push(await resolveValues(item, source, options));
|
|
177
|
+
} else {
|
|
178
|
+
result.push(item);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
result.push(item);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
150
187
|
/**
|
|
151
188
|
* Resolve RHS path strings in a pattern object against a source object.
|
|
152
189
|
*
|
|
@@ -216,7 +253,10 @@ export async function resolveValues(
|
|
|
216
253
|
} else if (typeof value === 'string' && protocols && hasProtocol(value)) {
|
|
217
254
|
// Protocol-prefixed value — resolve asynchronously
|
|
218
255
|
result[key] = await resolveProtocolValue(value, protocols, options);
|
|
219
|
-
} else if (
|
|
256
|
+
} else if (Array.isArray(value)) {
|
|
257
|
+
// Resolve path strings and protocols within arrays (recursing into nested arrays)
|
|
258
|
+
result[key] = await resolveArray(value, source, aliasMap, withMethods, protocols, options);
|
|
259
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
220
260
|
// Recursively resolve nested plain objects (e.g., headers: { "...": "globalThis://key" })
|
|
221
261
|
// Only recurse into plain objects — skip DOM elements, class instances, etc.
|
|
222
262
|
const proto = Object.getPrototypeOf(value);
|