@genome-spy/core 0.67.0 → 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.
Files changed (172) hide show
  1. package/dist/bundle/index.es.js +7641 -6313
  2. package/dist/bundle/index.js +115 -134
  3. package/dist/schema.json +534 -132
  4. package/dist/src/data/collector.d.ts +20 -0
  5. package/dist/src/data/collector.d.ts.map +1 -1
  6. package/dist/src/data/collector.js +148 -0
  7. package/dist/src/data/dataFlow.d.ts +6 -0
  8. package/dist/src/data/dataFlow.d.ts.map +1 -1
  9. package/dist/src/data/dataFlow.js +10 -0
  10. package/dist/src/data/flowInit.d.ts.map +1 -1
  11. package/dist/src/data/flowInit.js +2 -3
  12. package/dist/src/data/flowNode.d.ts +8 -0
  13. package/dist/src/data/flowNode.d.ts.map +1 -1
  14. package/dist/src/data/flowNode.js +18 -0
  15. package/dist/src/data/keyIndex.d.ts +18 -0
  16. package/dist/src/data/keyIndex.d.ts.map +1 -0
  17. package/dist/src/data/keyIndex.js +241 -0
  18. package/dist/src/data/keyIndex.test.d.ts +2 -0
  19. package/dist/src/data/keyIndex.test.d.ts.map +1 -0
  20. package/dist/src/data/sources/dataSource.d.ts.map +1 -1
  21. package/dist/src/data/sources/dataSource.js +5 -1
  22. package/dist/src/data/sources/dataSourceFactory.d.ts +14 -12
  23. package/dist/src/data/sources/dataSourceFactory.d.ts.map +1 -1
  24. package/dist/src/data/sources/dataSourceFactory.js +52 -16
  25. package/dist/src/data/sources/lazy/mockLazySource.d.ts +29 -0
  26. package/dist/src/data/sources/lazy/mockLazySource.d.ts.map +1 -0
  27. package/dist/src/data/sources/lazy/mockLazySource.js +44 -0
  28. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts +22 -1
  29. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts.map +1 -1
  30. package/dist/src/data/sources/lazy/singleAxisLazySource.js +34 -2
  31. package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
  32. package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +15 -0
  33. package/dist/src/data/sources/lazy/tabixSource.d.ts.map +1 -1
  34. package/dist/src/data/sources/lazy/tabixSource.js +15 -5
  35. package/dist/src/data/transforms/stack.d.ts.map +1 -1
  36. package/dist/src/data/transforms/stack.js +1 -0
  37. package/dist/src/encoder/accessor.d.ts +43 -0
  38. package/dist/src/encoder/accessor.d.ts.map +1 -1
  39. package/dist/src/encoder/accessor.js +164 -0
  40. package/dist/src/encoder/encoder.d.ts +11 -2
  41. package/dist/src/encoder/encoder.d.ts.map +1 -1
  42. package/dist/src/encoder/encoder.js +24 -4
  43. package/dist/src/encoder/metadataChannels.d.ts +15 -0
  44. package/dist/src/encoder/metadataChannels.d.ts.map +1 -0
  45. package/dist/src/encoder/metadataChannels.js +65 -0
  46. package/dist/src/encoder/metadataChannels.test.d.ts +2 -0
  47. package/dist/src/encoder/metadataChannels.test.d.ts.map +1 -0
  48. package/dist/src/genome/scaleLocus.d.ts.map +1 -1
  49. package/dist/src/genome/scaleLocus.js +14 -1
  50. package/dist/src/genomeSpy/containerUi.d.ts +0 -1
  51. package/dist/src/genomeSpy/containerUi.d.ts.map +1 -1
  52. package/dist/src/genomeSpy/containerUi.js +0 -14
  53. package/dist/src/genomeSpy/loadingIndicatorManager.d.ts +3 -7
  54. package/dist/src/genomeSpy/loadingIndicatorManager.d.ts.map +1 -1
  55. package/dist/src/genomeSpy/loadingIndicatorManager.js +68 -20
  56. package/dist/src/genomeSpy/loadingStatusRegistry.d.ts +52 -0
  57. package/dist/src/genomeSpy/loadingStatusRegistry.d.ts.map +1 -0
  58. package/dist/src/genomeSpy/loadingStatusRegistry.js +86 -0
  59. package/dist/src/genomeSpy/viewContextFactory.d.ts.map +1 -1
  60. package/dist/src/genomeSpy/viewContextFactory.js +0 -1
  61. package/dist/src/genomeSpy/viewDataInit.d.ts.map +1 -1
  62. package/dist/src/genomeSpy/viewDataInit.js +56 -11
  63. package/dist/src/genomeSpy.d.ts +0 -2
  64. package/dist/src/genomeSpy.d.ts.map +1 -1
  65. package/dist/src/genomeSpy.js +46 -26
  66. package/dist/src/marks/mark.d.ts.map +1 -1
  67. package/dist/src/marks/mark.js +18 -11
  68. package/dist/src/marks/markUtils.js +1 -1
  69. package/dist/src/scale/scale.d.ts +6 -1
  70. package/dist/src/scale/scale.d.ts.map +1 -1
  71. package/dist/src/scale/scale.js +83 -23
  72. package/dist/src/scales/axisResolution.d.ts.map +1 -1
  73. package/dist/src/scales/axisResolution.js +10 -0
  74. package/dist/src/scales/{scaleDomainAggregator.d.ts → domainPlanner.d.ts} +6 -3
  75. package/dist/src/scales/domainPlanner.d.ts.map +1 -0
  76. package/dist/src/scales/{scaleDomainAggregator.js → domainPlanner.js} +128 -10
  77. package/dist/src/scales/domainPlanner.test.d.ts +2 -0
  78. package/dist/src/scales/domainPlanner.test.d.ts.map +1 -0
  79. package/dist/src/scales/scaleInteractionController.d.ts +6 -0
  80. package/dist/src/scales/scaleInteractionController.d.ts.map +1 -1
  81. package/dist/src/scales/scaleInteractionController.js +41 -3
  82. package/dist/src/scales/scaleResolution.d.ts +19 -17
  83. package/dist/src/scales/scaleResolution.d.ts.map +1 -1
  84. package/dist/src/scales/scaleResolution.js +181 -70
  85. package/dist/src/scales/scaleResolution.test.d.ts.map +1 -1
  86. package/dist/src/selection/selection.d.ts +21 -0
  87. package/dist/src/selection/selection.d.ts.map +1 -1
  88. package/dist/src/selection/selection.js +82 -0
  89. package/dist/src/spec/channel.d.ts +52 -15
  90. package/dist/src/spec/data.d.ts +4 -0
  91. package/dist/src/spec/parameter.d.ts +16 -11
  92. package/dist/src/spec/testing.d.ts +12 -0
  93. package/dist/src/spec/testing.d.ts.map +1 -0
  94. package/dist/src/spec/testing.js +20 -0
  95. package/dist/src/spec/view.d.ts +45 -10
  96. package/dist/src/styles/genome-spy.css +3 -31
  97. package/dist/src/styles/genome-spy.css.d.ts +1 -1
  98. package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
  99. package/dist/src/styles/genome-spy.css.js +0 -29
  100. package/dist/src/types/encoder.d.ts +37 -2
  101. package/dist/src/types/rendering.d.ts +4 -3
  102. package/dist/src/types/viewContext.d.ts +0 -14
  103. package/dist/src/utils/throttle.d.ts +4 -1
  104. package/dist/src/utils/throttle.d.ts.map +1 -1
  105. package/dist/src/utils/throttle.js +54 -23
  106. package/dist/src/utils/throttle.test.d.ts +2 -0
  107. package/dist/src/utils/throttle.test.d.ts.map +1 -0
  108. package/dist/src/utils/transition.d.ts +21 -0
  109. package/dist/src/utils/transition.d.ts.map +1 -1
  110. package/dist/src/utils/transition.js +28 -0
  111. package/dist/src/utils/ui/tooltip.d.ts.map +1 -1
  112. package/dist/src/utils/ui/tooltip.js +7 -1
  113. package/dist/src/utils/ui/tooltip.test.d.ts +2 -0
  114. package/dist/src/utils/ui/tooltip.test.d.ts.map +1 -0
  115. package/dist/src/view/axisGridView.d.ts.map +1 -1
  116. package/dist/src/view/axisGridView.js +22 -5
  117. package/dist/src/view/axisView.d.ts.map +1 -1
  118. package/dist/src/view/axisView.js +20 -5
  119. package/dist/src/view/concatView.js +3 -3
  120. package/dist/src/view/containerMutationHelper.js +1 -1
  121. package/dist/src/view/containerView.d.ts +9 -5
  122. package/dist/src/view/containerView.d.ts.map +1 -1
  123. package/dist/src/view/containerView.js +34 -9
  124. package/dist/src/view/dataReadiness.d.ts +46 -0
  125. package/dist/src/view/dataReadiness.d.ts.map +1 -0
  126. package/dist/src/view/dataReadiness.js +267 -0
  127. package/dist/src/view/dataReadiness.test.d.ts +2 -0
  128. package/dist/src/view/dataReadiness.test.d.ts.map +1 -0
  129. package/dist/src/view/facetView.d.ts.map +1 -1
  130. package/dist/src/view/facetView.js +7 -5
  131. package/dist/src/view/flowBuilder.d.ts.map +1 -1
  132. package/dist/src/view/flowBuilder.js +5 -1
  133. package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
  134. package/dist/src/view/gridView/gridChild.js +8 -0
  135. package/dist/src/view/gridView/gridView.d.ts.map +1 -1
  136. package/dist/src/view/gridView/gridView.js +119 -2
  137. package/dist/src/view/gridView/scrollbar.d.ts.map +1 -1
  138. package/dist/src/view/gridView/scrollbar.js +3 -0
  139. package/dist/src/view/gridView/selectionRect.d.ts.map +1 -1
  140. package/dist/src/view/gridView/selectionRect.js +20 -5
  141. package/dist/src/view/gridView/separatorView.d.ts +51 -0
  142. package/dist/src/view/gridView/separatorView.d.ts.map +1 -0
  143. package/dist/src/view/gridView/separatorView.js +275 -0
  144. package/dist/src/view/layerView.js +3 -3
  145. package/dist/src/view/layout/flexLayout.d.ts +0 -30
  146. package/dist/src/view/layout/flexLayout.d.ts.map +1 -1
  147. package/dist/src/view/layout/flexLayout.js +0 -86
  148. package/dist/src/view/paramMediator.d.ts +19 -0
  149. package/dist/src/view/paramMediator.d.ts.map +1 -1
  150. package/dist/src/view/paramMediator.js +86 -19
  151. package/dist/src/view/testUtils.d.ts.map +1 -1
  152. package/dist/src/view/testUtils.js +6 -1
  153. package/dist/src/view/unitView.d.ts +8 -13
  154. package/dist/src/view/unitView.d.ts.map +1 -1
  155. package/dist/src/view/unitView.js +110 -41
  156. package/dist/src/view/view.d.ts +22 -14
  157. package/dist/src/view/view.d.ts.map +1 -1
  158. package/dist/src/view/view.js +93 -9
  159. package/dist/src/view/viewFactory.d.ts.map +1 -1
  160. package/dist/src/view/viewFactory.js +20 -1
  161. package/dist/src/view/viewSelectors.d.ts +148 -0
  162. package/dist/src/view/viewSelectors.d.ts.map +1 -0
  163. package/dist/src/view/viewSelectors.js +773 -0
  164. package/dist/src/view/viewSelectors.test.d.ts +2 -0
  165. package/dist/src/view/viewSelectors.test.d.ts.map +1 -0
  166. package/dist/src/view/viewUtils.d.ts +0 -8
  167. package/dist/src/view/viewUtils.d.ts.map +1 -1
  168. package/dist/src/view/viewUtils.js +1 -21
  169. package/package.json +3 -3
  170. package/dist/src/scales/scaleDomainAggregator.d.ts.map +0 -1
  171. package/dist/src/scales/scaleDomainAggregator.test.d.ts +0 -2
  172. 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
+ }