@genome-spy/core 0.66.1 → 0.68.0
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/dist/bundle/index.es.js +7669 -6115
- package/dist/bundle/index.js +114 -133
- package/dist/schema.json +534 -132
- package/dist/src/data/collector.d.ts +20 -0
- package/dist/src/data/collector.d.ts.map +1 -1
- package/dist/src/data/collector.js +148 -0
- package/dist/src/data/dataFlow.d.ts +6 -0
- package/dist/src/data/dataFlow.d.ts.map +1 -1
- package/dist/src/data/dataFlow.js +10 -0
- package/dist/src/data/flowHandle.d.ts +2 -0
- package/dist/src/data/flowHandle.d.ts.map +1 -1
- package/dist/src/data/flowHandle.js +1 -0
- package/dist/src/data/flowInit.d.ts +12 -4
- package/dist/src/data/flowInit.d.ts.map +1 -1
- package/dist/src/data/flowInit.js +115 -17
- package/dist/src/data/flowNode.d.ts +8 -0
- package/dist/src/data/flowNode.d.ts.map +1 -1
- package/dist/src/data/flowNode.js +18 -0
- package/dist/src/data/keyIndex.d.ts +18 -0
- package/dist/src/data/keyIndex.d.ts.map +1 -0
- package/dist/src/data/keyIndex.js +241 -0
- package/dist/src/data/keyIndex.test.d.ts +2 -0
- package/dist/src/data/keyIndex.test.d.ts.map +1 -0
- package/dist/src/data/sources/dataSource.d.ts.map +1 -1
- package/dist/src/data/sources/dataSource.js +5 -1
- package/dist/src/data/sources/dataSourceFactory.d.ts +14 -12
- package/dist/src/data/sources/dataSourceFactory.d.ts.map +1 -1
- package/dist/src/data/sources/dataSourceFactory.js +52 -16
- package/dist/src/data/sources/lazy/mockLazySource.d.ts +29 -0
- package/dist/src/data/sources/lazy/mockLazySource.d.ts.map +1 -0
- package/dist/src/data/sources/lazy/mockLazySource.js +44 -0
- package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts +22 -1
- package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/singleAxisLazySource.js +34 -2
- package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +15 -0
- package/dist/src/data/sources/lazy/tabixSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/tabixSource.js +15 -5
- package/dist/src/data/transforms/stack.d.ts.map +1 -1
- package/dist/src/data/transforms/stack.js +1 -0
- package/dist/src/encoder/accessor.d.ts +43 -0
- package/dist/src/encoder/accessor.d.ts.map +1 -1
- package/dist/src/encoder/accessor.js +164 -0
- package/dist/src/encoder/encoder.d.ts +11 -2
- package/dist/src/encoder/encoder.d.ts.map +1 -1
- package/dist/src/encoder/encoder.js +24 -4
- package/dist/src/encoder/metadataChannels.d.ts +15 -0
- package/dist/src/encoder/metadataChannels.d.ts.map +1 -0
- package/dist/src/encoder/metadataChannels.js +65 -0
- package/dist/src/encoder/metadataChannels.test.d.ts +2 -0
- package/dist/src/encoder/metadataChannels.test.d.ts.map +1 -0
- package/dist/src/genome/scaleLocus.d.ts.map +1 -1
- package/dist/src/genome/scaleLocus.js +14 -1
- package/dist/src/genomeSpy/containerUi.d.ts +0 -1
- package/dist/src/genomeSpy/containerUi.d.ts.map +1 -1
- package/dist/src/genomeSpy/containerUi.js +0 -14
- package/dist/src/genomeSpy/loadingIndicatorManager.d.ts +3 -7
- package/dist/src/genomeSpy/loadingIndicatorManager.d.ts.map +1 -1
- package/dist/src/genomeSpy/loadingIndicatorManager.js +68 -20
- package/dist/src/genomeSpy/loadingStatusRegistry.d.ts +52 -0
- package/dist/src/genomeSpy/loadingStatusRegistry.d.ts.map +1 -0
- package/dist/src/genomeSpy/loadingStatusRegistry.js +86 -0
- package/dist/src/genomeSpy/viewContextFactory.d.ts.map +1 -1
- package/dist/src/genomeSpy/viewContextFactory.js +0 -1
- package/dist/src/genomeSpy/viewDataInit.d.ts +10 -0
- package/dist/src/genomeSpy/viewDataInit.d.ts.map +1 -1
- package/dist/src/genomeSpy/viewDataInit.js +166 -2
- package/dist/src/genomeSpy/viewDataInit.test.d.ts +2 -0
- package/dist/src/genomeSpy/viewDataInit.test.d.ts.map +1 -0
- package/dist/src/genomeSpy.d.ts +1 -2
- package/dist/src/genomeSpy.d.ts.map +1 -1
- package/dist/src/genomeSpy.js +69 -27
- package/dist/src/gl/dataToVertices.d.ts.map +1 -1
- package/dist/src/gl/dataToVertices.js +16 -4
- package/dist/src/marks/mark.d.ts.map +1 -1
- package/dist/src/marks/mark.js +18 -11
- package/dist/src/marks/markUtils.js +1 -1
- package/dist/src/scale/scale.d.ts +6 -1
- package/dist/src/scale/scale.d.ts.map +1 -1
- package/dist/src/scale/scale.js +83 -23
- package/dist/src/scales/axisResolution.d.ts.map +1 -1
- package/dist/src/scales/axisResolution.js +10 -0
- package/dist/src/scales/{scaleDomainAggregator.d.ts → domainPlanner.d.ts} +8 -5
- package/dist/src/scales/domainPlanner.d.ts.map +1 -0
- package/dist/src/scales/domainPlanner.js +285 -0
- package/dist/src/scales/domainPlanner.test.d.ts +2 -0
- package/dist/src/scales/domainPlanner.test.d.ts.map +1 -0
- package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -1
- package/dist/src/scales/scaleInstanceManager.js +8 -4
- package/dist/src/scales/scaleInteractionController.d.ts +6 -0
- package/dist/src/scales/scaleInteractionController.d.ts.map +1 -1
- package/dist/src/scales/scaleInteractionController.js +41 -3
- package/dist/src/scales/scaleResolution.d.ts +19 -16
- package/dist/src/scales/scaleResolution.d.ts.map +1 -1
- package/dist/src/scales/scaleResolution.js +255 -70
- package/dist/src/scales/scaleResolution.test.d.ts.map +1 -1
- package/dist/src/selection/selection.d.ts +21 -0
- package/dist/src/selection/selection.d.ts.map +1 -1
- package/dist/src/selection/selection.js +82 -0
- package/dist/src/spec/channel.d.ts +52 -15
- package/dist/src/spec/data.d.ts +4 -0
- package/dist/src/spec/parameter.d.ts +16 -11
- package/dist/src/spec/testing.d.ts +12 -0
- package/dist/src/spec/testing.d.ts.map +1 -0
- package/dist/src/spec/testing.js +20 -0
- package/dist/src/spec/view.d.ts +45 -10
- package/dist/src/styles/genome-spy.css +3 -31
- package/dist/src/styles/genome-spy.css.d.ts +1 -1
- package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
- package/dist/src/styles/genome-spy.css.js +0 -29
- package/dist/src/types/encoder.d.ts +37 -2
- package/dist/src/types/rendering.d.ts +4 -3
- package/dist/src/types/viewContext.d.ts +0 -14
- package/dist/src/utils/domainArray.d.ts.map +1 -1
- package/dist/src/utils/domainArray.js +3 -0
- package/dist/src/utils/indexer.d.ts +3 -0
- package/dist/src/utils/indexer.d.ts.map +1 -1
- package/dist/src/utils/indexer.js +3 -0
- package/dist/src/utils/throttle.d.ts +4 -1
- package/dist/src/utils/throttle.d.ts.map +1 -1
- package/dist/src/utils/throttle.js +54 -23
- package/dist/src/utils/throttle.test.d.ts +2 -0
- package/dist/src/utils/throttle.test.d.ts.map +1 -0
- package/dist/src/utils/transition.d.ts +21 -0
- package/dist/src/utils/transition.d.ts.map +1 -1
- package/dist/src/utils/transition.js +28 -0
- package/dist/src/utils/ui/tooltip.d.ts.map +1 -1
- package/dist/src/utils/ui/tooltip.js +7 -1
- package/dist/src/utils/ui/tooltip.test.d.ts +2 -0
- package/dist/src/utils/ui/tooltip.test.d.ts.map +1 -0
- package/dist/src/view/axisGridView.d.ts.map +1 -1
- package/dist/src/view/axisGridView.js +22 -5
- package/dist/src/view/axisView.d.ts.map +1 -1
- package/dist/src/view/axisView.js +20 -5
- package/dist/src/view/concatView.js +3 -3
- package/dist/src/view/containerMutationHelper.d.ts.map +1 -1
- package/dist/src/view/containerMutationHelper.js +6 -2
- package/dist/src/view/containerView.d.ts +9 -5
- package/dist/src/view/containerView.d.ts.map +1 -1
- package/dist/src/view/containerView.js +34 -9
- package/dist/src/view/dataReadiness.d.ts +46 -0
- package/dist/src/view/dataReadiness.d.ts.map +1 -0
- package/dist/src/view/dataReadiness.js +267 -0
- package/dist/src/view/dataReadiness.test.d.ts +2 -0
- package/dist/src/view/dataReadiness.test.d.ts.map +1 -0
- package/dist/src/view/facetView.d.ts.map +1 -1
- package/dist/src/view/facetView.js +7 -5
- package/dist/src/view/flowBuilder.d.ts +5 -3
- package/dist/src/view/flowBuilder.d.ts.map +1 -1
- package/dist/src/view/flowBuilder.js +74 -7
- package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
- package/dist/src/view/gridView/gridChild.js +8 -0
- package/dist/src/view/gridView/gridView.d.ts.map +1 -1
- package/dist/src/view/gridView/gridView.js +119 -2
- package/dist/src/view/gridView/scrollbar.d.ts.map +1 -1
- package/dist/src/view/gridView/scrollbar.js +3 -0
- package/dist/src/view/gridView/selectionRect.d.ts.map +1 -1
- package/dist/src/view/gridView/selectionRect.js +20 -5
- package/dist/src/view/gridView/separatorView.d.ts +51 -0
- package/dist/src/view/gridView/separatorView.d.ts.map +1 -0
- package/dist/src/view/gridView/separatorView.js +275 -0
- package/dist/src/view/layerView.js +3 -3
- package/dist/src/view/layout/flexLayout.d.ts +0 -30
- package/dist/src/view/layout/flexLayout.d.ts.map +1 -1
- package/dist/src/view/layout/flexLayout.js +0 -86
- package/dist/src/view/paramMediator.d.ts +19 -0
- package/dist/src/view/paramMediator.d.ts.map +1 -1
- package/dist/src/view/paramMediator.js +86 -19
- package/dist/src/view/testUtils.d.ts.map +1 -1
- package/dist/src/view/testUtils.js +11 -1
- package/dist/src/view/unitView.d.ts +8 -13
- package/dist/src/view/unitView.d.ts.map +1 -1
- package/dist/src/view/unitView.js +127 -43
- package/dist/src/view/view.d.ts +34 -14
- package/dist/src/view/view.d.ts.map +1 -1
- package/dist/src/view/view.js +119 -9
- package/dist/src/view/viewFactory.d.ts.map +1 -1
- package/dist/src/view/viewFactory.js +20 -1
- package/dist/src/view/viewSelectors.d.ts +148 -0
- package/dist/src/view/viewSelectors.d.ts.map +1 -0
- package/dist/src/view/viewSelectors.js +773 -0
- package/dist/src/view/viewSelectors.test.d.ts +2 -0
- package/dist/src/view/viewSelectors.test.d.ts.map +1 -0
- package/dist/src/view/viewUtils.d.ts +0 -8
- package/dist/src/view/viewUtils.d.ts.map +1 -1
- package/dist/src/view/viewUtils.js +1 -21
- package/package.json +3 -3
- package/dist/src/scales/scaleDomainAggregator.d.ts.map +0 -1
- package/dist/src/scales/scaleDomainAggregator.js +0 -162
- package/dist/src/scales/scaleDomainAggregator.test.d.ts +0 -2
- package/dist/src/scales/scaleDomainAggregator.test.d.ts.map +0 -1
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { VISIT_SKIP, VISIT_STOP } from "./view.js";
|
|
2
|
+
import { isSelectionParameter, isVariableParameter } from "./paramMediator.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Selectors identify views and parameters in a way that stays stable when the
|
|
6
|
+
* same template/import is instantiated multiple times. They combine a chain of
|
|
7
|
+
* named import instances (scope) with an explicit view or parameter name so
|
|
8
|
+
* bookmarkable state and visibility toggles do not rely on globally-unique
|
|
9
|
+
* names or runtime-only nodes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{ scope: string[], view: string }} ViewSelector
|
|
14
|
+
* @typedef {{ scope: string[], param: string }} ParamSelector
|
|
15
|
+
* @typedef {{ view: import("./view.js").default, param: import("../spec/parameter.js").Parameter, selector: ParamSelector }} BookmarkableParamEntry
|
|
16
|
+
* @typedef {{ message: string, scope: string[] }} SelectorValidationIssue
|
|
17
|
+
* @typedef {{ name: string | null }} ImportScopeInfo
|
|
18
|
+
* @typedef {"exclude" | "excludeSubtree"} AddressableOverride
|
|
19
|
+
* @typedef {{ skipSubtree?: boolean }} AddressableOptions
|
|
20
|
+
* @typedef {{ view: import("./view.js").default, param: import("../spec/parameter.js").Parameter }} ResolvedParam
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const PARAM_SELECTOR_KEY_PREFIX = "p:";
|
|
24
|
+
|
|
25
|
+
/** @type {WeakMap<import("./view.js").default, ImportScopeInfo>} */
|
|
26
|
+
const importScopes = new WeakMap();
|
|
27
|
+
|
|
28
|
+
/** @type {WeakMap<import("./view.js").default, AddressableOverride>} */
|
|
29
|
+
const addressableOverrides = new WeakMap();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Marks a view as the root of an import scope.
|
|
33
|
+
*
|
|
34
|
+
* @param {import("./view.js").default} view
|
|
35
|
+
* @param {string | null} scopeName
|
|
36
|
+
*/
|
|
37
|
+
export function registerImportInstance(view, scopeName) {
|
|
38
|
+
if (scopeName !== null && typeof scopeName !== "string") {
|
|
39
|
+
throw new Error("Import scope name must be a string or null.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
importScopes.set(view, { name: scopeName });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns import scope info for a view, if it is an import root.
|
|
47
|
+
*
|
|
48
|
+
* @param {import("./view.js").default} view
|
|
49
|
+
* @returns {ImportScopeInfo | undefined}
|
|
50
|
+
*/
|
|
51
|
+
export function getImportScopeInfo(view) {
|
|
52
|
+
return importScopes.get(view);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Marks a view as non-addressable for selector resolution.
|
|
57
|
+
*
|
|
58
|
+
* @param {import("./view.js").default} view
|
|
59
|
+
* @param {AddressableOptions} [options]
|
|
60
|
+
*/
|
|
61
|
+
export function markViewAsNonAddressable(view, options = {}) {
|
|
62
|
+
const skipSubtree = options.skipSubtree ?? false;
|
|
63
|
+
const behavior = skipSubtree ? "excludeSubtree" : "exclude";
|
|
64
|
+
addressableOverrides.set(view, behavior);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the import scope chain for a view, using named import instances.
|
|
69
|
+
*
|
|
70
|
+
* @param {import("./view.js").default} view
|
|
71
|
+
* @returns {string[]}
|
|
72
|
+
*/
|
|
73
|
+
export function getViewScopeChain(view) {
|
|
74
|
+
const ancestors = view.getDataAncestors();
|
|
75
|
+
|
|
76
|
+
/** @type {string[]} */
|
|
77
|
+
const chain = [];
|
|
78
|
+
|
|
79
|
+
for (let i = ancestors.length - 1; i >= 0; i -= 1) {
|
|
80
|
+
const info = importScopes.get(ancestors[i]);
|
|
81
|
+
if (info && typeof info.name === "string") {
|
|
82
|
+
chain.push(info.name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return chain;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns a view selector for a view with an explicit name.
|
|
91
|
+
*
|
|
92
|
+
* @param {import("./view.js").default} view
|
|
93
|
+
* @returns {ViewSelector}
|
|
94
|
+
*/
|
|
95
|
+
export function getViewSelector(view) {
|
|
96
|
+
const explicitName = view.explicitName;
|
|
97
|
+
if (!explicitName) {
|
|
98
|
+
throw new Error("Cannot build a selector for a view without a name.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
scope: getViewScopeChain(view),
|
|
103
|
+
view: explicitName,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns a parameter selector for a parameter registered in a view.
|
|
109
|
+
*
|
|
110
|
+
* @param {import("./view.js").default} view
|
|
111
|
+
* @param {string} paramName
|
|
112
|
+
* @returns {ParamSelector}
|
|
113
|
+
*/
|
|
114
|
+
export function getParamSelector(view, paramName) {
|
|
115
|
+
if (!paramName) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"Cannot build a selector for a parameter without a name."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
scope: getViewScopeChain(view),
|
|
123
|
+
param: paramName,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Returns a stable key for a parameter selector.
|
|
129
|
+
*
|
|
130
|
+
* @param {ParamSelector} selector
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
export function makeParamSelectorKey(selector) {
|
|
134
|
+
validateParamSelector(selector);
|
|
135
|
+
return (
|
|
136
|
+
PARAM_SELECTOR_KEY_PREFIX +
|
|
137
|
+
JSON.stringify({ scope: selector.scope, param: selector.param })
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Enumerates views that can be addressed by selectors.
|
|
143
|
+
*
|
|
144
|
+
* @param {import("./view.js").default} root
|
|
145
|
+
* @returns {import("./view.js").default[]}
|
|
146
|
+
*/
|
|
147
|
+
export function getAddressableViews(root) {
|
|
148
|
+
/** @type {import("./view.js").default[]} */
|
|
149
|
+
const views = [];
|
|
150
|
+
|
|
151
|
+
visitAddressableViews(root, (view) => {
|
|
152
|
+
views.push(view);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return views;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Visits all addressable views in the hierarchy.
|
|
160
|
+
*
|
|
161
|
+
* @param {import("./view.js").default} root
|
|
162
|
+
* @param {import("./view.js").Visitor} visitor
|
|
163
|
+
*/
|
|
164
|
+
export function visitAddressableViews(root, visitor) {
|
|
165
|
+
root.visit((view) => {
|
|
166
|
+
const behavior = addressableOverrides.get(view);
|
|
167
|
+
if (behavior === "excludeSubtree") {
|
|
168
|
+
return VISIT_SKIP;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (behavior === "exclude") {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return visitor(view);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolves a view selector to a unique view within the matching scope.
|
|
181
|
+
*
|
|
182
|
+
* @param {import("./view.js").default} root
|
|
183
|
+
* @param {ViewSelector} selector
|
|
184
|
+
* @returns {import("./view.js").default | undefined}
|
|
185
|
+
*/
|
|
186
|
+
export function resolveViewSelector(root, selector) {
|
|
187
|
+
validateViewSelector(selector);
|
|
188
|
+
|
|
189
|
+
const scopeRoot = resolveScopeRoot(root, selector.scope);
|
|
190
|
+
if (!scopeRoot) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** @type {import("./view.js").default[]} */
|
|
195
|
+
const matches = [];
|
|
196
|
+
|
|
197
|
+
visitViewsInScope(
|
|
198
|
+
scopeRoot,
|
|
199
|
+
(view) => {
|
|
200
|
+
if (view.explicitName === selector.view) {
|
|
201
|
+
matches.push(view);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{ includeNamedImportRoots: true }
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (matches.length === 1) {
|
|
208
|
+
return matches[0];
|
|
209
|
+
} else if (matches.length === 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
throw new Error(
|
|
214
|
+
'View selector is ambiguous for view "' +
|
|
215
|
+
selector.view +
|
|
216
|
+
'" in scope ' +
|
|
217
|
+
JSON.stringify(selector.scope)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Resolves a parameter selector to a unique bookmarkable parameter.
|
|
223
|
+
*
|
|
224
|
+
* @param {import("./view.js").default} root
|
|
225
|
+
* @param {ParamSelector} selector
|
|
226
|
+
* @returns {ResolvedParam | undefined}
|
|
227
|
+
*/
|
|
228
|
+
export function resolveParamSelector(root, selector) {
|
|
229
|
+
validateParamSelector(selector);
|
|
230
|
+
|
|
231
|
+
const scopeRoot = resolveScopeRoot(root, selector.scope);
|
|
232
|
+
if (!scopeRoot) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** @type {ResolvedParam[]} */
|
|
237
|
+
const matches = [];
|
|
238
|
+
|
|
239
|
+
visitViewsInScope(scopeRoot, (view) => {
|
|
240
|
+
for (const [name, param] of view.paramMediator.paramConfigs) {
|
|
241
|
+
if (name !== selector.param) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!isBookmarkableParam(param)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
matches.push({ view, param });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (matches.length === 1) {
|
|
254
|
+
return matches[0];
|
|
255
|
+
} else if (matches.length === 0) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
throw new Error(
|
|
260
|
+
'Param selector is ambiguous for param "' +
|
|
261
|
+
selector.param +
|
|
262
|
+
'" in scope ' +
|
|
263
|
+
JSON.stringify(selector.scope)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Visits bookmarkable parameters in the view hierarchy.
|
|
269
|
+
*
|
|
270
|
+
* @param {import("./view.js").default} root
|
|
271
|
+
* @param {(entry: BookmarkableParamEntry) => void} visitor
|
|
272
|
+
*/
|
|
273
|
+
export function visitBookmarkableParams(root, visitor) {
|
|
274
|
+
root.visit((view) => {
|
|
275
|
+
const behavior = addressableOverrides.get(view);
|
|
276
|
+
if (behavior === "excludeSubtree") {
|
|
277
|
+
return VISIT_SKIP;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (behavior === "exclude") {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const [name, param] of view.paramMediator.paramConfigs) {
|
|
285
|
+
if (!isBookmarkableParam(param)) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
visitor({
|
|
290
|
+
view,
|
|
291
|
+
param,
|
|
292
|
+
selector: getParamSelector(view, name),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Returns bookmarkable parameters in the view hierarchy.
|
|
300
|
+
*
|
|
301
|
+
* @param {import("./view.js").default} root
|
|
302
|
+
* @returns {BookmarkableParamEntry[]}
|
|
303
|
+
*/
|
|
304
|
+
export function getBookmarkableParams(root) {
|
|
305
|
+
/** @type {BookmarkableParamEntry[]} */
|
|
306
|
+
const entries = [];
|
|
307
|
+
visitBookmarkableParams(root, (entry) => entries.push(entry));
|
|
308
|
+
return entries;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Validates naming and scoping constraints for addressable views and parameters.
|
|
313
|
+
*
|
|
314
|
+
* @param {import("./view.js").default} root
|
|
315
|
+
* @returns {SelectorValidationIssue[]}
|
|
316
|
+
*/
|
|
317
|
+
export function validateSelectorConstraints(root) {
|
|
318
|
+
/** @type {SelectorValidationIssue[]} */
|
|
319
|
+
const issues = [];
|
|
320
|
+
|
|
321
|
+
for (const scopeRoot of collectScopeRoots(root)) {
|
|
322
|
+
const scope = getScopeChainForRoot(scopeRoot);
|
|
323
|
+
validateViewNamesInScope(scopeRoot, scope, issues);
|
|
324
|
+
validateParamNamesInScope(scopeRoot, scope, issues);
|
|
325
|
+
validateImportInstanceNames(scopeRoot, scope, issues);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return issues;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Validates the structural shape of a parameter selector.
|
|
333
|
+
*
|
|
334
|
+
* @param {ParamSelector} selector
|
|
335
|
+
*/
|
|
336
|
+
function validateParamSelector(selector) {
|
|
337
|
+
if (!selector || !Array.isArray(selector.scope)) {
|
|
338
|
+
throw new Error("Param selector scope must be an array.");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (typeof selector.param !== "string" || !selector.param.length) {
|
|
342
|
+
throw new Error("Param selector param must be a non-empty string.");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Validates the structural shape of a view selector.
|
|
348
|
+
*
|
|
349
|
+
* @param {ViewSelector} selector
|
|
350
|
+
*/
|
|
351
|
+
function validateViewSelector(selector) {
|
|
352
|
+
if (!selector || !Array.isArray(selector.scope)) {
|
|
353
|
+
throw new Error("View selector scope must be an array.");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (typeof selector.view !== "string" || !selector.view.length) {
|
|
357
|
+
throw new Error("View selector view must be a non-empty string.");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Returns the effective configurableVisibility value for a view.
|
|
363
|
+
*
|
|
364
|
+
* @param {import("./view.js").default} view
|
|
365
|
+
* @returns {boolean}
|
|
366
|
+
*/
|
|
367
|
+
function isConfigurableVisibility(view) {
|
|
368
|
+
const explicit = view.spec.configurableVisibility;
|
|
369
|
+
if (explicit !== undefined) {
|
|
370
|
+
return explicit;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return !(
|
|
374
|
+
view.layoutParent &&
|
|
375
|
+
view.layoutParent.spec &&
|
|
376
|
+
"layer" in view.layoutParent.spec
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Returns true for parameters that are persisted in bookmarks.
|
|
382
|
+
*
|
|
383
|
+
* @param {import("../spec/parameter.js").Parameter} param
|
|
384
|
+
* @returns {boolean}
|
|
385
|
+
*/
|
|
386
|
+
function isBookmarkableParam(param) {
|
|
387
|
+
if (param.persist === false) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (isSelectionParameter(param)) {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (isVariableParameter(param)) {
|
|
396
|
+
return Boolean(param.bind);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Collects scope roots for all named import instances and the top-level root.
|
|
404
|
+
*
|
|
405
|
+
* @param {import("./view.js").default} root
|
|
406
|
+
* @returns {import("./view.js").default[]}
|
|
407
|
+
*/
|
|
408
|
+
function collectScopeRoots(root) {
|
|
409
|
+
/** @type {Set<import("./view.js").default>} */
|
|
410
|
+
const roots = new Set([root]);
|
|
411
|
+
|
|
412
|
+
root.visit((view) => {
|
|
413
|
+
const info = importScopes.get(view);
|
|
414
|
+
if (info && typeof info.name === "string") {
|
|
415
|
+
roots.add(view);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
return Array.from(roots);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Builds the full scope chain for a scope root, including its own name.
|
|
424
|
+
*
|
|
425
|
+
* @param {import("./view.js").default} scopeRoot
|
|
426
|
+
* @returns {string[]}
|
|
427
|
+
*/
|
|
428
|
+
function getScopeChainForRoot(scopeRoot) {
|
|
429
|
+
const chain = getViewScopeChain(scopeRoot);
|
|
430
|
+
const info = importScopes.get(scopeRoot);
|
|
431
|
+
if (info && typeof info.name === "string") {
|
|
432
|
+
return [...chain, info.name];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return chain;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Formats a scope chain for diagnostics.
|
|
440
|
+
*
|
|
441
|
+
* @param {string[]} scope
|
|
442
|
+
* @returns {string}
|
|
443
|
+
*/
|
|
444
|
+
function formatScope(scope) {
|
|
445
|
+
if (!scope.length) {
|
|
446
|
+
return "import scope (root)";
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return "import scope [" + scope.join(" / ") + "]";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Checks configurable view names for required explicit and unique naming.
|
|
454
|
+
*
|
|
455
|
+
* @param {import("./view.js").default} scopeRoot
|
|
456
|
+
* @param {string[]} scope
|
|
457
|
+
* @param {SelectorValidationIssue[]} issues
|
|
458
|
+
*/
|
|
459
|
+
function validateViewNamesInScope(scopeRoot, scope, issues) {
|
|
460
|
+
/** @type {Map<string, import("./view.js").default[]>} */
|
|
461
|
+
const names = new Map();
|
|
462
|
+
|
|
463
|
+
visitViewsInScope(
|
|
464
|
+
scopeRoot,
|
|
465
|
+
(view) => {
|
|
466
|
+
const explicitName = view.explicitName;
|
|
467
|
+
const isConfigurable = isConfigurableVisibility(view);
|
|
468
|
+
const isExplicitlyConfigurable =
|
|
469
|
+
view.spec.configurableVisibility === true;
|
|
470
|
+
|
|
471
|
+
if (!isConfigurable) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!explicitName) {
|
|
476
|
+
if (!isExplicitlyConfigurable) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
issues.push({
|
|
481
|
+
message:
|
|
482
|
+
"Configurable view must have an explicit name in " +
|
|
483
|
+
formatScope(scope) +
|
|
484
|
+
".",
|
|
485
|
+
scope,
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const matches = names.get(explicitName);
|
|
491
|
+
if (matches) {
|
|
492
|
+
matches.push(view);
|
|
493
|
+
} else {
|
|
494
|
+
names.set(explicitName, [view]);
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
{ includeNamedImportRoots: true }
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
for (const [name, matches] of names) {
|
|
501
|
+
if (matches.length <= 1) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const paths = matches.map((view) => view.getPathString()).join(", ");
|
|
506
|
+
|
|
507
|
+
issues.push({
|
|
508
|
+
message:
|
|
509
|
+
'Configurable view name "' +
|
|
510
|
+
name +
|
|
511
|
+
'" is not unique within ' +
|
|
512
|
+
formatScope(scope) +
|
|
513
|
+
". Found in: " +
|
|
514
|
+
paths +
|
|
515
|
+
".",
|
|
516
|
+
scope,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Checks bookmarkable parameter names for uniqueness within a scope.
|
|
523
|
+
*
|
|
524
|
+
* @param {import("./view.js").default} scopeRoot
|
|
525
|
+
* @param {string[]} scope
|
|
526
|
+
* @param {SelectorValidationIssue[]} issues
|
|
527
|
+
*/
|
|
528
|
+
function validateParamNamesInScope(scopeRoot, scope, issues) {
|
|
529
|
+
/** @type {Map<string, import("./view.js").default[]>} */
|
|
530
|
+
const names = new Map();
|
|
531
|
+
|
|
532
|
+
visitViewsInScope(scopeRoot, (view) => {
|
|
533
|
+
for (const [name, param] of view.paramMediator.paramConfigs) {
|
|
534
|
+
if (!isBookmarkableParam(param)) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const matches = names.get(name);
|
|
539
|
+
if (matches) {
|
|
540
|
+
matches.push(view);
|
|
541
|
+
} else {
|
|
542
|
+
names.set(name, [view]);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
for (const [name, matches] of names) {
|
|
548
|
+
if (matches.length <= 1) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const paths = matches.map((view) => view.getPathString()).join(", ");
|
|
553
|
+
|
|
554
|
+
issues.push({
|
|
555
|
+
message:
|
|
556
|
+
'Bookmarkable parameter "' +
|
|
557
|
+
name +
|
|
558
|
+
'" is not unique within ' +
|
|
559
|
+
formatScope(scope) +
|
|
560
|
+
". Found in: " +
|
|
561
|
+
paths +
|
|
562
|
+
".",
|
|
563
|
+
scope,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Ensures addressable import instances are uniquely named in a scope.
|
|
570
|
+
*
|
|
571
|
+
* @param {import("./view.js").default} scopeRoot
|
|
572
|
+
* @param {string[]} scope
|
|
573
|
+
* @param {SelectorValidationIssue[]} issues
|
|
574
|
+
*/
|
|
575
|
+
function validateImportInstanceNames(scopeRoot, scope, issues) {
|
|
576
|
+
const importRoots = collectImmediateImportRoots(scopeRoot);
|
|
577
|
+
if (!importRoots.length) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const addressableRoots = importRoots.filter((view) =>
|
|
582
|
+
hasAddressableFeatures(view)
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
if (addressableRoots.length <= 1) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** @type {Map<string, number>} */
|
|
590
|
+
const counts = new Map();
|
|
591
|
+
|
|
592
|
+
for (const view of addressableRoots) {
|
|
593
|
+
const info = importScopes.get(view);
|
|
594
|
+
const name = info ? info.name : undefined;
|
|
595
|
+
if (typeof name !== "string" || !name.length) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
for (const [name, count] of counts) {
|
|
603
|
+
if (count > 1) {
|
|
604
|
+
issues.push({
|
|
605
|
+
message:
|
|
606
|
+
'Import instance name "' +
|
|
607
|
+
name +
|
|
608
|
+
'" is used multiple times for addressable instances in ' +
|
|
609
|
+
formatScope(scope) +
|
|
610
|
+
".",
|
|
611
|
+
scope,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Collects direct import roots under a scope root.
|
|
619
|
+
*
|
|
620
|
+
* @param {import("./view.js").default} scopeRoot
|
|
621
|
+
* @returns {import("./view.js").default[]}
|
|
622
|
+
*/
|
|
623
|
+
function collectImmediateImportRoots(scopeRoot) {
|
|
624
|
+
/** @type {import("./view.js").default[]} */
|
|
625
|
+
const roots = [];
|
|
626
|
+
|
|
627
|
+
scopeRoot.visit((view) => {
|
|
628
|
+
if (view === scopeRoot) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const behavior = addressableOverrides.get(view);
|
|
633
|
+
if (behavior === "excludeSubtree") {
|
|
634
|
+
return VISIT_SKIP;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const info = importScopes.get(view);
|
|
638
|
+
if (info) {
|
|
639
|
+
roots.push(view);
|
|
640
|
+
return VISIT_SKIP;
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
return roots;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Detects whether a subtree exposes configurable views or bookmarkable params.
|
|
649
|
+
*
|
|
650
|
+
* @param {import("./view.js").default} root
|
|
651
|
+
* @returns {boolean}
|
|
652
|
+
*/
|
|
653
|
+
function hasAddressableFeatures(root) {
|
|
654
|
+
let found = false;
|
|
655
|
+
|
|
656
|
+
root.visit((view) => {
|
|
657
|
+
const behavior = addressableOverrides.get(view);
|
|
658
|
+
if (behavior === "excludeSubtree") {
|
|
659
|
+
return VISIT_SKIP;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (behavior !== "exclude") {
|
|
663
|
+
const isConfigurable = isConfigurableVisibility(view);
|
|
664
|
+
const isExplicitlyConfigurable =
|
|
665
|
+
view.spec.configurableVisibility === true;
|
|
666
|
+
|
|
667
|
+
if (
|
|
668
|
+
isConfigurable &&
|
|
669
|
+
(view.explicitName || isExplicitlyConfigurable)
|
|
670
|
+
) {
|
|
671
|
+
found = true;
|
|
672
|
+
return VISIT_STOP;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
for (const param of view.paramMediator.paramConfigs.values()) {
|
|
676
|
+
if (isBookmarkableParam(param)) {
|
|
677
|
+
found = true;
|
|
678
|
+
return VISIT_STOP;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
return found;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Resolves a scope chain to its scope root.
|
|
689
|
+
*
|
|
690
|
+
* @param {import("./view.js").default} root
|
|
691
|
+
* @param {string[]} scope
|
|
692
|
+
* @returns {import("./view.js").default | undefined}
|
|
693
|
+
*/
|
|
694
|
+
function resolveScopeRoot(root, scope) {
|
|
695
|
+
/** @type {import("./view.js").default} */
|
|
696
|
+
let current = root;
|
|
697
|
+
|
|
698
|
+
for (const name of scope) {
|
|
699
|
+
if (typeof name !== "string" || !name.length) {
|
|
700
|
+
throw new Error("Scope names must be non-empty strings.");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** @type {import("./view.js").default | undefined} */
|
|
704
|
+
let match;
|
|
705
|
+
let hasDuplicate = false;
|
|
706
|
+
|
|
707
|
+
visitViewsInScope(
|
|
708
|
+
current,
|
|
709
|
+
(view) => {
|
|
710
|
+
const info = importScopes.get(view);
|
|
711
|
+
if (info && info.name === name) {
|
|
712
|
+
if (match) {
|
|
713
|
+
hasDuplicate = true;
|
|
714
|
+
return VISIT_STOP;
|
|
715
|
+
}
|
|
716
|
+
match = view;
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
{ includeNamedImportRoots: true }
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
if (hasDuplicate) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
'Multiple import instances named "' + name + '" in scope.'
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (match) {
|
|
729
|
+
current = match;
|
|
730
|
+
} else {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return current;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Visits addressable views within a scope, skipping nested named import roots.
|
|
740
|
+
*
|
|
741
|
+
* @param {import("./view.js").default} scopeRoot
|
|
742
|
+
* @param {import("./view.js").Visitor} visitor
|
|
743
|
+
* @param {{ includeNamedImportRoots?: boolean }} [options]
|
|
744
|
+
*/
|
|
745
|
+
function visitViewsInScope(scopeRoot, visitor, options = {}) {
|
|
746
|
+
const includeNamedImportRoots = options.includeNamedImportRoots ?? false;
|
|
747
|
+
|
|
748
|
+
scopeRoot.visit((view) => {
|
|
749
|
+
const behavior = addressableOverrides.get(view);
|
|
750
|
+
if (behavior === "excludeSubtree") {
|
|
751
|
+
return VISIT_SKIP;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const info = importScopes.get(view);
|
|
755
|
+
const isNamedImportRoot =
|
|
756
|
+
view !== scopeRoot && info && typeof info.name === "string";
|
|
757
|
+
|
|
758
|
+
if (isNamedImportRoot) {
|
|
759
|
+
if (behavior !== "exclude" && includeNamedImportRoots) {
|
|
760
|
+
const result = visitor(view);
|
|
761
|
+
if (result === VISIT_STOP) {
|
|
762
|
+
return result;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return VISIT_SKIP;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (behavior !== "exclude") {
|
|
770
|
+
return visitor(view);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}
|