assign-gingerly 0.0.54 → 0.0.56
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 +143 -2
- package/assignFrom.js +37 -18
- package/assignFrom.ts +62 -3
- 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 +186 -0
- package/handlers/lazyLoad.ts +281 -0
- package/handlers/lazyLoadSwitch.js +58 -0
- package/handlers/lazyLoadSwitch.ts +64 -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/markerUtils.js +127 -0
- package/markerUtils.ts +162 -0
- package/package.json +31 -1
- package/paths.js +49 -1
- package/paths.ts +80 -1
- package/processHandlerCommands.js +19 -19
- package/processHandlerCommands.ts +10 -13
- package/resolveIdRef.js +144 -125
- package/resolveIdRef.ts +30 -0
- package/types/assign-gingerly/types.d.ts +87 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* isAllowedImportPath.ts — Security utility for validating import paths.
|
|
3
|
+
*
|
|
4
|
+
* Checks that a path is local (relative, absolute, or bare specifier) and
|
|
5
|
+
* not a cross-domain URL. Used to prevent untrusted HTML attributes from
|
|
6
|
+
* triggering imports to arbitrary external domains.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { isAllowedImportPath } from 'assign-gingerly/isAllowedImportPath.js';
|
|
10
|
+
*
|
|
11
|
+
* isAllowedImportPath('./local.js'); // true
|
|
12
|
+
* isAllowedImportPath('../parent/file.js'); // true
|
|
13
|
+
* isAllowedImportPath('/absolute/path.js'); // true
|
|
14
|
+
* isAllowedImportPath('bare-specifier/mod.js'); // true
|
|
15
|
+
* isAllowedImportPath('https://evil.com/x.js'); // false
|
|
16
|
+
* isAllowedImportPath('//cdn.example.com/x.js'); // false
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Check if an import path is allowed (non-cross-domain).
|
|
20
|
+
*
|
|
21
|
+
* Allowed:
|
|
22
|
+
* - Relative paths: ./foo.js, ../bar.js
|
|
23
|
+
* - Absolute paths: /path/to/file.js
|
|
24
|
+
* - Bare specifiers: package-name/file.js, @scope/package/file.js
|
|
25
|
+
*
|
|
26
|
+
* Blocked:
|
|
27
|
+
* - Protocol URLs: https://..., http://..., data:..., etc.
|
|
28
|
+
* - Protocol-relative: //cdn.example.com/...
|
|
29
|
+
*
|
|
30
|
+
* @param path - The import path to validate
|
|
31
|
+
* @returns true if the path is local/safe, false if cross-domain
|
|
32
|
+
*/
|
|
33
|
+
export function isAllowedImportPath(path) {
|
|
34
|
+
if (path.startsWith('./') || path.startsWith('../') || path.startsWith('/')) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (path.includes('://') || path.startsWith('//')) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// Bare specifier (no protocol, no //) — allowed
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* isAllowedImportPath.ts — Security utility for validating import paths.
|
|
3
|
+
*
|
|
4
|
+
* Checks that a path is local (relative, absolute, or bare specifier) and
|
|
5
|
+
* not a cross-domain URL. Used to prevent untrusted HTML attributes from
|
|
6
|
+
* triggering imports to arbitrary external domains.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { isAllowedImportPath } from 'assign-gingerly/isAllowedImportPath.js';
|
|
10
|
+
*
|
|
11
|
+
* isAllowedImportPath('./local.js'); // true
|
|
12
|
+
* isAllowedImportPath('../parent/file.js'); // true
|
|
13
|
+
* isAllowedImportPath('/absolute/path.js'); // true
|
|
14
|
+
* isAllowedImportPath('bare-specifier/mod.js'); // true
|
|
15
|
+
* isAllowedImportPath('https://evil.com/x.js'); // false
|
|
16
|
+
* isAllowedImportPath('//cdn.example.com/x.js'); // false
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Permissions interface for controlling security-sensitive operations.
|
|
21
|
+
* Passed as the last parameter to assignGingerly, assignFrom, and enhanceAll.
|
|
22
|
+
* Only trusted script can set these — they are never parsed from HTML attributes.
|
|
23
|
+
*/
|
|
24
|
+
export interface AssignPermissions {
|
|
25
|
+
/** Allow imports from cross-domain URLs (default: false) */
|
|
26
|
+
crossDomainImports?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if an import path is allowed (non-cross-domain).
|
|
31
|
+
*
|
|
32
|
+
* Allowed:
|
|
33
|
+
* - Relative paths: ./foo.js, ../bar.js
|
|
34
|
+
* - Absolute paths: /path/to/file.js
|
|
35
|
+
* - Bare specifiers: package-name/file.js, @scope/package/file.js
|
|
36
|
+
*
|
|
37
|
+
* Blocked:
|
|
38
|
+
* - Protocol URLs: https://..., http://..., data:..., etc.
|
|
39
|
+
* - Protocol-relative: //cdn.example.com/...
|
|
40
|
+
*
|
|
41
|
+
* @param path - The import path to validate
|
|
42
|
+
* @returns true if the path is local/safe, false if cross-domain
|
|
43
|
+
*/
|
|
44
|
+
export function isAllowedImportPath(path: string): boolean {
|
|
45
|
+
if (path.startsWith('./') || path.startsWith('../') || path.startsWith('/')) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (path.includes('://') || path.startsWith('//')) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// Bare specifier (no protocol, no //) — allowed
|
|
52
|
+
return true;
|
|
53
|
+
}
|
package/markerUtils.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markerUtils.js — Shared utilities for comment marker management.
|
|
3
|
+
*
|
|
4
|
+
* Used by lazyLoad, microDataJoin, and future template loop handlers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const MARKER_START_PREFIX = '?start name="';
|
|
8
|
+
export const MARKER_END = '?end';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Find existing start/end comment markers (TreeWalker approach).
|
|
12
|
+
*/
|
|
13
|
+
export function findMarkers(target, name) {
|
|
14
|
+
const startText = `${MARKER_START_PREFIX}${name}"`;
|
|
15
|
+
let startMarker = null;
|
|
16
|
+
let endMarker = null;
|
|
17
|
+
|
|
18
|
+
const walker = document.createTreeWalker(target, NodeFilter.SHOW_COMMENT);
|
|
19
|
+
let node;
|
|
20
|
+
while ((node = walker.nextNode())) {
|
|
21
|
+
if (!startMarker && node.data === startText) {
|
|
22
|
+
startMarker = node;
|
|
23
|
+
} else if (startMarker && !endMarker && node.data === MARKER_END) {
|
|
24
|
+
endMarker = node;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [startMarker, endMarker];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Find existing start/end comment markers (XPath approach).
|
|
34
|
+
*/
|
|
35
|
+
export function findMarkersXPath(target, name) {
|
|
36
|
+
const startText = `${MARKER_START_PREFIX}${name}"`;
|
|
37
|
+
|
|
38
|
+
const startResult = document.evaluate(
|
|
39
|
+
`.//comment()[. = "${startText}"]`,
|
|
40
|
+
target,
|
|
41
|
+
null,
|
|
42
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
43
|
+
null
|
|
44
|
+
);
|
|
45
|
+
const startMarker = startResult.singleNodeValue;
|
|
46
|
+
if (!startMarker) return [null, null];
|
|
47
|
+
|
|
48
|
+
const endResult = document.evaluate(
|
|
49
|
+
`following-sibling::comment()[. = "${MARKER_END}"][1]`,
|
|
50
|
+
startMarker,
|
|
51
|
+
null,
|
|
52
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
53
|
+
null
|
|
54
|
+
);
|
|
55
|
+
const endMarker = endResult.singleNodeValue;
|
|
56
|
+
|
|
57
|
+
return [startMarker, endMarker];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create start/end markers and insert them into the target.
|
|
62
|
+
*/
|
|
63
|
+
export function createMarkers(target, name, method = 'appendChild') {
|
|
64
|
+
const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
|
|
65
|
+
const endMarker = document.createComment(MARKER_END);
|
|
66
|
+
|
|
67
|
+
if (method === 'prepend') {
|
|
68
|
+
target.prepend(endMarker);
|
|
69
|
+
target.prepend(startMarker);
|
|
70
|
+
} else {
|
|
71
|
+
target.appendChild(startMarker);
|
|
72
|
+
target.appendChild(endMarker);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [startMarker, endMarker];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get all nodes between start and end markers.
|
|
80
|
+
*/
|
|
81
|
+
export function getNodesBetweenMarkers(start, end) {
|
|
82
|
+
const nodes = [];
|
|
83
|
+
let current = start.nextSibling;
|
|
84
|
+
while (current && current !== end) {
|
|
85
|
+
nodes.push(current);
|
|
86
|
+
current = current.nextSibling;
|
|
87
|
+
}
|
|
88
|
+
return nodes;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Find existing start/end comment markers among siblings of an anchor element.
|
|
93
|
+
* Used for 'after' insertion mode.
|
|
94
|
+
*/
|
|
95
|
+
export function findMarkersSibling(anchor, name) {
|
|
96
|
+
const startText = `${MARKER_START_PREFIX}${name}"`;
|
|
97
|
+
let startMarker = null;
|
|
98
|
+
let endMarker = null;
|
|
99
|
+
|
|
100
|
+
let current = anchor.nextSibling;
|
|
101
|
+
while (current) {
|
|
102
|
+
if (current.nodeType === Node.COMMENT_NODE) {
|
|
103
|
+
if (!startMarker && current.data === startText) {
|
|
104
|
+
startMarker = current;
|
|
105
|
+
} else if (startMarker && !endMarker && current.data === MARKER_END) {
|
|
106
|
+
endMarker = current;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
current = current.nextSibling;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return [startMarker, endMarker];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create start/end markers as siblings after an anchor element.
|
|
118
|
+
* Used for 'after' insertion mode.
|
|
119
|
+
*/
|
|
120
|
+
export function createMarkersSibling(anchor, name) {
|
|
121
|
+
const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
|
|
122
|
+
const endMarker = document.createComment(MARKER_END);
|
|
123
|
+
|
|
124
|
+
anchor.after(startMarker, endMarker);
|
|
125
|
+
|
|
126
|
+
return [startMarker, endMarker];
|
|
127
|
+
}
|
package/markerUtils.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markerUtils.ts — Shared utilities for comment marker management.
|
|
3
|
+
*
|
|
4
|
+
* Used by lazyLoad, microDataJoin, and future template loop handlers.
|
|
5
|
+
* Provides finding, creating, and traversing comment marker pairs.
|
|
6
|
+
*
|
|
7
|
+
* Markers are HTML comment nodes with specific content:
|
|
8
|
+
* - Start: <!--?start name="markerName"-->
|
|
9
|
+
* - End: <!--?end-->
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const MARKER_START_PREFIX = '?start name="';
|
|
13
|
+
export const MARKER_END = '?end';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find existing start/end comment markers in a target element (TreeWalker approach).
|
|
17
|
+
* Searches the subtree of `target` for matching comment nodes.
|
|
18
|
+
*
|
|
19
|
+
* @param target - The element to search within
|
|
20
|
+
* @param name - The marker name to find
|
|
21
|
+
* @returns [startMarker, endMarker] or [null, null] if not found
|
|
22
|
+
*/
|
|
23
|
+
export function findMarkers(target: Element | Node, name: string): [Comment | null, Comment | null] {
|
|
24
|
+
const startText = `${MARKER_START_PREFIX}${name}"`;
|
|
25
|
+
let startMarker: Comment | null = null;
|
|
26
|
+
let endMarker: Comment | null = null;
|
|
27
|
+
|
|
28
|
+
const walker = document.createTreeWalker(target as Node, NodeFilter.SHOW_COMMENT);
|
|
29
|
+
let node: Comment | null;
|
|
30
|
+
while ((node = walker.nextNode() as Comment | null)) {
|
|
31
|
+
if (!startMarker && node.data === startText) {
|
|
32
|
+
startMarker = node;
|
|
33
|
+
} else if (startMarker && !endMarker && node.data === MARKER_END) {
|
|
34
|
+
endMarker = node;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [startMarker, endMarker];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Find existing start/end comment markers using XPath (alternative approach).
|
|
44
|
+
* May be faster in large DOMs due to engine-level indexing.
|
|
45
|
+
*
|
|
46
|
+
* @param target - The element to search within
|
|
47
|
+
* @param name - The marker name to find
|
|
48
|
+
* @returns [startMarker, endMarker] or [null, null] if not found
|
|
49
|
+
*/
|
|
50
|
+
export function findMarkersXPath(target: Element | Node, name: string): [Comment | null, Comment | null] {
|
|
51
|
+
const startText = `${MARKER_START_PREFIX}${name}"`;
|
|
52
|
+
|
|
53
|
+
const startResult = document.evaluate(
|
|
54
|
+
`.//comment()[. = "${startText}"]`,
|
|
55
|
+
target,
|
|
56
|
+
null,
|
|
57
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
58
|
+
null
|
|
59
|
+
);
|
|
60
|
+
const startMarker = startResult.singleNodeValue as Comment | null;
|
|
61
|
+
if (!startMarker) return [null, null];
|
|
62
|
+
|
|
63
|
+
// Find the next sibling comment that is the end marker
|
|
64
|
+
const endResult = document.evaluate(
|
|
65
|
+
`following-sibling::comment()[. = "${MARKER_END}"][1]`,
|
|
66
|
+
startMarker,
|
|
67
|
+
null,
|
|
68
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
69
|
+
null
|
|
70
|
+
);
|
|
71
|
+
const endMarker = endResult.singleNodeValue as Comment | null;
|
|
72
|
+
|
|
73
|
+
return [startMarker, endMarker];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create start/end markers and insert them into the target.
|
|
78
|
+
*
|
|
79
|
+
* @param target - The element to insert markers into
|
|
80
|
+
* @param name - The marker name
|
|
81
|
+
* @param method - 'appendChild' (default) or 'prepend'
|
|
82
|
+
* @returns [startMarker, endMarker]
|
|
83
|
+
*/
|
|
84
|
+
export function createMarkers(target: Element, name: string, method: string = 'appendChild'): [Comment, Comment] {
|
|
85
|
+
const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
|
|
86
|
+
const endMarker = document.createComment(MARKER_END);
|
|
87
|
+
|
|
88
|
+
if (method === 'prepend') {
|
|
89
|
+
target.prepend(endMarker);
|
|
90
|
+
target.prepend(startMarker);
|
|
91
|
+
} else {
|
|
92
|
+
target.appendChild(startMarker);
|
|
93
|
+
target.appendChild(endMarker);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return [startMarker, endMarker];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get all nodes between start and end markers.
|
|
101
|
+
*
|
|
102
|
+
* @param start - The start comment marker
|
|
103
|
+
* @param end - The end comment marker
|
|
104
|
+
* @returns Array of nodes between the markers (exclusive of markers themselves)
|
|
105
|
+
*/
|
|
106
|
+
export function getNodesBetweenMarkers(start: Comment, end: Comment): Node[] {
|
|
107
|
+
const nodes: Node[] = [];
|
|
108
|
+
let current: Node | null = start.nextSibling;
|
|
109
|
+
while (current && current !== end) {
|
|
110
|
+
nodes.push(current);
|
|
111
|
+
current = current.nextSibling;
|
|
112
|
+
}
|
|
113
|
+
return nodes;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Find existing start/end comment markers among siblings of an anchor element.
|
|
118
|
+
* Used for 'after' insertion mode where markers are siblings, not children.
|
|
119
|
+
*
|
|
120
|
+
* @param anchor - The element after which markers were inserted
|
|
121
|
+
* @param name - The marker name to find
|
|
122
|
+
* @returns [startMarker, endMarker] or [null, null] if not found
|
|
123
|
+
*/
|
|
124
|
+
export function findMarkersSibling(anchor: Element | Node, name: string): [Comment | null, Comment | null] {
|
|
125
|
+
const startText = `${MARKER_START_PREFIX}${name}"`;
|
|
126
|
+
let startMarker: Comment | null = null;
|
|
127
|
+
let endMarker: Comment | null = null;
|
|
128
|
+
|
|
129
|
+
let current: Node | null = anchor.nextSibling;
|
|
130
|
+
while (current) {
|
|
131
|
+
if (current.nodeType === Node.COMMENT_NODE) {
|
|
132
|
+
const comment = current as Comment;
|
|
133
|
+
if (!startMarker && comment.data === startText) {
|
|
134
|
+
startMarker = comment;
|
|
135
|
+
} else if (startMarker && !endMarker && comment.data === MARKER_END) {
|
|
136
|
+
endMarker = comment;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
current = current.nextSibling;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return [startMarker, endMarker];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create start/end markers as siblings after an anchor element.
|
|
148
|
+
* Used for 'after' insertion mode.
|
|
149
|
+
*
|
|
150
|
+
* @param anchor - The element to insert markers after
|
|
151
|
+
* @param name - The marker name
|
|
152
|
+
* @returns [startMarker, endMarker]
|
|
153
|
+
*/
|
|
154
|
+
export function createMarkersSibling(anchor: Element | Node, name: string): [Comment, Comment] {
|
|
155
|
+
const startMarker = document.createComment(`${MARKER_START_PREFIX}${name}"`);
|
|
156
|
+
const endMarker = document.createComment(MARKER_END);
|
|
157
|
+
|
|
158
|
+
// Insert after the anchor: anchor → startMarker → endMarker
|
|
159
|
+
(anchor as ChildNode).after(startMarker, endMarker);
|
|
160
|
+
|
|
161
|
+
return [startMarker, endMarker];
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assign-gingerly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.56",
|
|
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": {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"*.js",
|
|
19
19
|
"*.ts",
|
|
20
|
+
"handlers/**",
|
|
21
|
+
"inferencer/**",
|
|
20
22
|
"README.md",
|
|
21
23
|
"LICENSE",
|
|
22
24
|
"types/assign-gingerly/types.d.ts"
|
|
@@ -100,6 +102,34 @@
|
|
|
100
102
|
"default": "./paths.js",
|
|
101
103
|
"types": "./paths.ts"
|
|
102
104
|
},
|
|
105
|
+
"./enhanceAll.js": {
|
|
106
|
+
"default": "./enhanceAll.js",
|
|
107
|
+
"types": "./enhanceAll.ts"
|
|
108
|
+
},
|
|
109
|
+
"./isAllowedImportPath.js": {
|
|
110
|
+
"default": "./isAllowedImportPath.js",
|
|
111
|
+
"types": "./isAllowedImportPath.ts"
|
|
112
|
+
},
|
|
113
|
+
"./markerUtils.js": {
|
|
114
|
+
"default": "./markerUtils.js",
|
|
115
|
+
"types": "./markerUtils.ts"
|
|
116
|
+
},
|
|
117
|
+
"./inferredAssignments.js": {
|
|
118
|
+
"default": "./inferredAssignments.js",
|
|
119
|
+
"types": "./inferredAssignments.ts"
|
|
120
|
+
},
|
|
121
|
+
"./beVigilant.js": {
|
|
122
|
+
"default": "./beVigilant.js",
|
|
123
|
+
"types": "./beVigilant.ts"
|
|
124
|
+
},
|
|
125
|
+
"./inferencer/inferencer.js": {
|
|
126
|
+
"default": "./inferencer/inferencer.js",
|
|
127
|
+
"types": "./inferencer/inferencer.ts"
|
|
128
|
+
},
|
|
129
|
+
"./inferencer/withScopePerimeter.js": {
|
|
130
|
+
"default": "./inferencer/withScopePerimeter.js",
|
|
131
|
+
"types": "./inferencer/withScopePerimeter.ts"
|
|
132
|
+
},
|
|
103
133
|
"./assignFrom.js": {
|
|
104
134
|
"default": "./assignFrom.js",
|
|
105
135
|
"types": "./assignFrom.ts"
|
package/paths.js
CHANGED
|
@@ -10,9 +10,41 @@
|
|
|
10
10
|
*/
|
|
11
11
|
const PATH_SYMBOL = Symbol('assign-gingerly-path');
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Create a proxy for id-ref paths (#[varName]).
|
|
15
|
+
*/
|
|
16
|
+
function createIdRefProxy(idRef, options) {
|
|
17
|
+
function handler() {}
|
|
18
|
+
return new Proxy(handler, {
|
|
19
|
+
get(_, prop) {
|
|
20
|
+
if (prop === 'path' || prop === PATH_SYMBOL) {
|
|
21
|
+
return idRef;
|
|
22
|
+
}
|
|
23
|
+
if (typeof prop === 'symbol') return undefined;
|
|
24
|
+
const chained = `${idRef}?.${String(prop)}`;
|
|
25
|
+
return createIdRefProxy(chained, options);
|
|
26
|
+
},
|
|
27
|
+
apply(_, __, args) {
|
|
28
|
+
if (args.length > 0) {
|
|
29
|
+
const arg = args[0];
|
|
30
|
+
let argStr;
|
|
31
|
+
if (arg === true) argStr = 'true';
|
|
32
|
+
else if (arg === false) argStr = 'false';
|
|
33
|
+
else if (arg && typeof arg === 'object' && PATH_SYMBOL in arg) {
|
|
34
|
+
const fullPath = arg[PATH_SYMBOL];
|
|
35
|
+
argStr = fullPath.startsWith('?.') ? fullPath.substring(2) : fullPath;
|
|
36
|
+
}
|
|
37
|
+
else argStr = String(arg);
|
|
38
|
+
const chained = `${idRef}?.${argStr}`;
|
|
39
|
+
return createIdRefProxy(chained, options);
|
|
40
|
+
}
|
|
41
|
+
return createIdRefProxy(idRef, options);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
13
46
|
/**
|
|
14
47
|
* 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
48
|
*/
|
|
17
49
|
function createPathProxy(prefix, options) {
|
|
18
50
|
const aliasMap = options?.aka;
|
|
@@ -27,6 +59,14 @@ function createPathProxy(prefix, options) {
|
|
|
27
59
|
if (typeof prop === 'symbol') return undefined;
|
|
28
60
|
|
|
29
61
|
let segment = String(prop);
|
|
62
|
+
|
|
63
|
+
// #-prefix: $['#firstName'] → '#[firstName]' (cached element ref)
|
|
64
|
+
if (segment.startsWith('#')) {
|
|
65
|
+
const varName = segment.substring(1);
|
|
66
|
+
const idRef = `#[${varName}]`;
|
|
67
|
+
return createIdRefProxy(idRef, options);
|
|
68
|
+
}
|
|
69
|
+
|
|
30
70
|
if (aliasMap) {
|
|
31
71
|
for (const [alias, target] of Object.entries(aliasMap)) {
|
|
32
72
|
if (target === segment) { segment = alias; break; }
|
|
@@ -111,6 +151,14 @@ export function doAssign(...pairs) {
|
|
|
111
151
|
return { assign: Object.assign({}, ...pairs) };
|
|
112
152
|
}
|
|
113
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Compile-time loop expansion: generates one entry per key from a factory function.
|
|
156
|
+
*/
|
|
157
|
+
export function forEachKeyIn(keys, factory, options) {
|
|
158
|
+
const $ = paths(options);
|
|
159
|
+
return keys.map(key => factory(key, $));
|
|
160
|
+
}
|
|
161
|
+
|
|
114
162
|
/**
|
|
115
163
|
* Tagged template literal that splits a template into an array of parts.
|
|
116
164
|
* Path proxy objects are auto-detected and converted to path strings.
|
package/paths.ts
CHANGED
|
@@ -53,6 +53,44 @@ export interface PathsOptions {
|
|
|
53
53
|
withMethods?: string[] | Set<string>;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Create a proxy for id-ref paths (#[varName]).
|
|
58
|
+
* After the initial #[varName], further property access chains with ?. from the resolved element.
|
|
59
|
+
* .path returns the #[varName] prefix (optionally with further ?. path).
|
|
60
|
+
*/
|
|
61
|
+
function createIdRefProxy(idRef: string, options?: PathsOptions): any {
|
|
62
|
+
function handler() {}
|
|
63
|
+
return new Proxy(handler, {
|
|
64
|
+
get(_, prop: string | symbol) {
|
|
65
|
+
if (prop === 'path' || prop === PATH_SYMBOL) {
|
|
66
|
+
return idRef;
|
|
67
|
+
}
|
|
68
|
+
if (typeof prop === 'symbol') return undefined;
|
|
69
|
+
|
|
70
|
+
// Chain further path segments after the id ref
|
|
71
|
+
const chained = `${idRef}?.${String(prop)}`;
|
|
72
|
+
return createIdRefProxy(chained, options);
|
|
73
|
+
},
|
|
74
|
+
apply(_, __, args) {
|
|
75
|
+
if (args.length > 0) {
|
|
76
|
+
const arg = args[0];
|
|
77
|
+
let argStr: string;
|
|
78
|
+
if (arg === true) argStr = 'true';
|
|
79
|
+
else if (arg === false) argStr = 'false';
|
|
80
|
+
else if (arg && typeof arg === 'object' && PATH_SYMBOL in arg) {
|
|
81
|
+
const fullPath = arg[PATH_SYMBOL] as string;
|
|
82
|
+
argStr = fullPath.startsWith('?.') ? fullPath.substring(2) : fullPath;
|
|
83
|
+
}
|
|
84
|
+
else argStr = String(arg);
|
|
85
|
+
|
|
86
|
+
const chained = `${idRef}?.${argStr}`;
|
|
87
|
+
return createIdRefProxy(chained, options);
|
|
88
|
+
}
|
|
89
|
+
return createIdRefProxy(idRef, options);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
56
94
|
/**
|
|
57
95
|
* Create a recursive proxy that records property access paths.
|
|
58
96
|
* Supports both property access and method call syntax (via apply trap on function target).
|
|
@@ -74,8 +112,17 @@ function createPathProxy(prefix: string, options?: PathsOptions): any {
|
|
|
74
112
|
// Ignore symbol access (Symbol.iterator, Symbol.toPrimitive, etc.)
|
|
75
113
|
if (typeof prop === 'symbol') return undefined;
|
|
76
114
|
|
|
77
|
-
// Apply reverse alias: if prop matches an alias value, use the alias key
|
|
78
115
|
let segment = String(prop);
|
|
116
|
+
|
|
117
|
+
// #-prefix: $['#firstName'] → '#[firstName]' (cached element ref)
|
|
118
|
+
if (segment.startsWith('#')) {
|
|
119
|
+
const varName = segment.substring(1);
|
|
120
|
+
const idRef = `#[${varName}]`;
|
|
121
|
+
// Return a proxy that starts from this id ref (can chain further with ?.)
|
|
122
|
+
return createIdRefProxy(idRef, options);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Apply reverse alias: if prop matches an alias value, use the alias key
|
|
79
126
|
if (aliasMap) {
|
|
80
127
|
for (const [alias, target] of Object.entries(aliasMap)) {
|
|
81
128
|
if (target === segment) { segment = alias; break; }
|
|
@@ -222,6 +269,38 @@ export function smoothOver(value: any): any {
|
|
|
222
269
|
export function doAssign(...pairs: Record<string, any>[]): { assign: Record<string, any> } {
|
|
223
270
|
return { assign: Object.assign({}, ...pairs) };
|
|
224
271
|
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Compile-time loop expansion: generates one entry per key from a factory function.
|
|
275
|
+
* Creates a typed proxy internally — the factory receives both the key and the proxy.
|
|
276
|
+
*
|
|
277
|
+
* @param keys - Array of property names to iterate (type-checked against T)
|
|
278
|
+
* @param factory - Function that produces a config entry for each key
|
|
279
|
+
* @param options - Optional PathsOptions (aka, withMethods) for the internal proxy
|
|
280
|
+
* @returns Array of factory results (one per key) — spread into merges array
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* import { forEachKeyIn, set, doAssign } from 'assign-gingerly/paths.js';
|
|
284
|
+
*
|
|
285
|
+
* interface Person extends HTMLElement { firstName: string; lastName: string; }
|
|
286
|
+
*
|
|
287
|
+
* const merges = [
|
|
288
|
+
* ...forEachKeyIn<Person>(['firstName', 'lastName'], (key, $) => ({
|
|
289
|
+
* ifKeyIn: [key],
|
|
290
|
+
* assignOptions: { withIds: { [key]: { qry: `[name="${key}"]` } } },
|
|
291
|
+
* ...doAssign(set($['#' + key]).to($[key]))
|
|
292
|
+
* })),
|
|
293
|
+
* ];
|
|
294
|
+
*/
|
|
295
|
+
export function forEachKeyIn<T>(
|
|
296
|
+
keys: (keyof T & string)[],
|
|
297
|
+
factory: (key: keyof T & string, proxy: PathProxy<T>) => any,
|
|
298
|
+
options?: PathsOptions
|
|
299
|
+
): any[] {
|
|
300
|
+
const $ = paths<T>(options);
|
|
301
|
+
return keys.map(key => factory(key, $));
|
|
302
|
+
}
|
|
303
|
+
|
|
225
304
|
/**
|
|
226
305
|
* Tagged template literal that splits a template into an array of parts.
|
|
227
306
|
* Interleaves static string segments with interpolated values.
|