@tissuumaps/core 0.1.0-beta.1 → 0.1.0-beta.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @tissuumaps/core
2
2
 
3
+ ## 0.1.0-beta.3
4
+
5
+ ### Minor Changes
6
+
7
+ - [#302](https://github.com/TissUUmaps/TissUUmaps/pull/302) [`a422564`](https://github.com/TissUUmaps/TissUUmaps/commit/a4225640dd4be545727210c11e5c12d3d81d650a) Thanks [@jwindhager](https://github.com/jwindhager)! - Data providers gain the optional `supports`, `readName` and `prepareDataSource` methods, and `SourceUtils` gains `getPathSegments`, `getStem`, `getExtension` and `makeProjectPath`.
8
+
9
+ Breaking: data provider UI schemas no longer contain the `source` control, which the app now renders itself.
10
+
11
+ - [#300](https://github.com/TissUUmaps/TissUUmaps/pull/300) [`9fc85a9`](https://github.com/TissUUmaps/TissUUmaps/commit/9fc85a9148be339aa0e419620253f03aace7041a) Thanks [@jwindhager](https://github.com/jwindhager)! - The app store tracks the active panel (`activePanelId`) and the expanded objects of each panel (`expandedImageIds` etc.).
12
+
13
+ Breaking: `showImageSettings` etc. and the `imageSettingsRequest` etc. state are removed. Plugins call `setExpandedImageIds` etc. and `setActivePanelId` instead.
14
+
15
+ ### Patch Changes
16
+
17
+ - [#307](https://github.com/TissUUmaps/TissUUmaps/pull/307) [`16e181e`](https://github.com/TissUUmaps/TissUUmaps/commit/16e181efd919f11a54b259c7e96231b724e628dd) Thanks [@jwindhager](https://github.com/jwindhager)! - Documentation links point to the new TissUUmaps website at https://tissuumaps.github.io/: the app's Docs link, and the packages' READMEs and homepages.
18
+
19
+ ## 0.1.0-beta.2
20
+
21
+ ### Minor Changes
22
+
23
+ - [#296](https://github.com/TissUUmaps/TissUUmaps/pull/296) [`15a214a`](https://github.com/TissUUmaps/TissUUmaps/commit/15a214a7950a1e9aa605ca5445cc4273b826ca2d) Thanks [@jwindhager](https://github.com/jwindhager)! - `immer` is declared as an optional peer dependency ([#277](https://github.com/TissUUmaps/TissUUmaps/issues/277)), since the store types depend on it through `zustand/immer`.
24
+
3
25
  ## 0.1.0-beta.1
4
26
 
5
27
  ### Minor Changes
package/README.md CHANGED
@@ -19,6 +19,6 @@ stores and the data provider interfaces) silently degrade to `any` when
19
19
 
20
20
  ## Documentation
21
21
 
22
- - [API reference](https://tissuumaps.github.io/TissUUmaps/docs/docs/api/@tissuumaps/core/)
23
- - [Data model](https://tissuumaps.github.io/TissUUmaps/docs/docs/concepts/data-model/)
24
- - [Code architecture](https://tissuumaps.github.io/TissUUmaps/docs/docs/development/code-architecture/)
22
+ - [API reference](https://tissuumaps.github.io/docs/api/@tissuumaps/core/)
23
+ - [Data model](https://tissuumaps.github.io/docs/concepts/data-model/)
24
+ - [Code architecture](https://tissuumaps.github.io/docs/development/code-architecture/)
package/dist/index.d.ts CHANGED
@@ -98,33 +98,46 @@ export declare type AppStoreActions = {
98
98
  */
99
99
  setHighlightedItemGroup: (highlightedItemGroup: HighlightedItemGroup | null) => void;
100
100
  /**
101
- * Shows the settings of an image: the Images panel is brought to the front
102
- * and the settings are expanded
101
+ * Makes a panel the active panel, bringing its tab to the front
103
102
  *
104
- * @param imageId - ID of the image whose settings to show
103
+ * An ID that does not identify a panel of the layout, e.g. that of the viewer
104
+ * or of a plugin that is not mounted, is reverted, leaving the layout
105
+ * unchanged.
106
+ *
107
+ * @param activePanelId - The ID of the panel, see
108
+ * {@link AppStoreState.activePanelId}
109
+ */
110
+ setActivePanelId: (activePanelId: string) => void;
111
+ /**
112
+ * Sets the images whose entries are expanded in the Images panel
113
+ *
114
+ * @param expandedImageIds - The IDs of the images, in any order
105
115
  */
106
- showImageSettings: (imageId: string) => void;
116
+ setExpandedImageIds: (expandedImageIds: string[]) => void;
107
117
  /**
108
- * Shows the settings of labels: the Labels panel is brought to the front
109
- * and the settings are expanded
118
+ * Sets the labels whose entries are expanded in the Labels panel
110
119
  *
111
- * @param labelsId - ID of the labels whose settings to show
120
+ * @param expandedLabelsIds - The IDs of the labels, in any order
112
121
  */
113
- showLabelsSettings: (labelsId: string) => void;
122
+ setExpandedLabelsIds: (expandedLabelsIds: string[]) => void;
114
123
  /**
115
- * Shows the settings of points: the Points panel is brought to the front
116
- * and the settings are expanded
124
+ * Sets the points whose entries are expanded in the Points panel
117
125
  *
118
- * @param pointsId - ID of the points whose settings to show
126
+ * @param expandedPointsIds - The IDs of the points, in any order
119
127
  */
120
- showPointsSettings: (pointsId: string) => void;
128
+ setExpandedPointsIds: (expandedPointsIds: string[]) => void;
121
129
  /**
122
- * Shows the settings of shapes: the Shapes panel is brought to the front
123
- * and the settings are expanded
130
+ * Sets the shapes whose entries are expanded in the Shapes panel
124
131
  *
125
- * @param shapesId - ID of the shapes whose settings to show
132
+ * @param expandedShapesIds - The IDs of the shapes, in any order
126
133
  */
127
- showShapesSettings: (shapesId: string) => void;
134
+ setExpandedShapesIds: (expandedShapesIds: string[]) => void;
135
+ /**
136
+ * Sets the tables whose entries are expanded in the Tables panel
137
+ *
138
+ * @param expandedTableIds - The IDs of the tables, in any order
139
+ */
140
+ setExpandedTableIds: (expandedTableIds: string[]) => void;
128
141
  /**
129
142
  * Registers an image data provider
130
143
  *
@@ -214,45 +227,45 @@ export declare type AppStoreState = {
214
227
  container?: HTMLElement;
215
228
  }>;
216
229
  /**
217
- * The latest request to show the settings of an image, or `null` if there has
218
- * been none
230
+ * The ID of the active panel, i.e. the panel whose tab was selected last
219
231
  *
220
- * Set by {@link AppStoreActions.showImageSettings} to a new object on every call, so
221
- * that a repeated request for the same image is a change too.
232
+ * Either the ID of a built-in panel or that of a mounted plugin's panel, as
233
+ * listed in the plugin documentation. Kept in sync with the panel layout in
234
+ * both directions: selecting a tab sets it, and setting it brings that
235
+ * panel's tab to the front and activates its group. The viewer has no tab and
236
+ * is never the active panel, so focusing it leaves this unchanged.
222
237
  */
223
- imageSettingsRequest: {
224
- imageId: string;
225
- } | null;
238
+ activePanelId: string;
226
239
  /**
227
- * The latest request to show the settings of labels, or `null` if there has
228
- * been none
240
+ * The IDs of the images whose entries are expanded in the Images panel
229
241
  *
230
- * Set by {@link AppStoreActions.showLabelsSettings} to a new object on every call, so
231
- * that a repeated request for the same labels is a change too.
242
+ * Cleared whenever another project is loaded or the project is closed.
232
243
  */
233
- labelsSettingsRequest: {
234
- labelsId: string;
235
- } | null;
244
+ expandedImageIds: string[];
236
245
  /**
237
- * The latest request to show the settings of points, or `null` if there has
238
- * been none
246
+ * The IDs of the labels whose entries are expanded in the Labels panel
239
247
  *
240
- * Set by {@link AppStoreActions.showPointsSettings} to a new object on every call, so
241
- * that a repeated request for the same points is a change too.
248
+ * Cleared whenever another project is loaded or the project is closed.
242
249
  */
243
- pointsSettingsRequest: {
244
- pointsId: string;
245
- } | null;
250
+ expandedLabelsIds: string[];
246
251
  /**
247
- * The latest request to show the settings of shapes, or `null` if there has
248
- * been none
252
+ * The IDs of the points whose entries are expanded in the Points panel
249
253
  *
250
- * Set by {@link AppStoreActions.showShapesSettings} to a new object on every call, so
251
- * that a repeated request for the same shapes is a change too.
254
+ * Cleared whenever another project is loaded or the project is closed.
252
255
  */
253
- shapesSettingsRequest: {
254
- shapesId: string;
255
- } | null;
256
+ expandedPointsIds: string[];
257
+ /**
258
+ * The IDs of the shapes whose entries are expanded in the Shapes panel
259
+ *
260
+ * Cleared whenever another project is loaded or the project is closed.
261
+ */
262
+ expandedShapesIds: string[];
263
+ /**
264
+ * The IDs of the tables whose entries are expanded in the Tables panel
265
+ *
266
+ * Cleared whenever another project is loaded or the project is closed.
267
+ */
268
+ expandedTableIds: string[];
256
269
  };
257
270
 
258
271
  /**
@@ -907,7 +920,12 @@ export declare interface DataProvider<TDataSource extends DataSource, TData exte
907
920
  readonly name: string;
908
921
  /** The JSON schema for the data source */
909
922
  readonly schema: JsonSchema;
910
- /** The JSON Forms UI schema for the data source */
923
+ /**
924
+ * The JSON Forms UI schema for the data source
925
+ *
926
+ * It must not contain a control for the data source's `source`, which the
927
+ * application renders itself if the schema declares it, as a string.
928
+ */
911
929
  readonly uischema: UISchemaElement;
912
930
  /**
913
931
  * Returns the data source with all of this data provider's defaults applied
@@ -935,6 +953,60 @@ export declare interface DataProvider<TDataSource extends DataSource, TData exte
935
953
  * @returns The normalized data source
936
954
  */
937
955
  normalize(dataSource: TDataSource, workspace: FileSystemDirectoryHandle | null, projectSource: string | null): TNormalizedDataSource;
956
+ /**
957
+ * Returns whether this data provider can likely open a source
958
+ *
959
+ * Used to pick a data provider for a source the user entered or dropped,
960
+ * e.g. by its file extension or by cheaply reading its metadata. A data
961
+ * provider without this method is never picked automatically, but can
962
+ * still be chosen by the user. Callers treat a rejection as `false`.
963
+ *
964
+ * @param normalizedSource - The source, normalized with
965
+ * `SourceUtils.normalizeSource` like {@link DataProvider.normalize} does
966
+ * @param workspace - The directory handle of the open workspace, if any
967
+ * @param options - Optional abort signal
968
+ * @returns A promise that resolves to whether the source is supported
969
+ */
970
+ supports?(normalizedSource: string, workspace: FileSystemDirectoryHandle | null, options?: {
971
+ signal?: AbortSignal;
972
+ }): Promise<boolean>;
973
+ /**
974
+ * Reads the name a source stores in its metadata
975
+ *
976
+ * Used to name data objects added from a source the user entered or
977
+ * dropped. Only names stored in the source are returned, never names
978
+ * derived from the source itself, such as from its path. Without this
979
+ * method, or if it resolves to `undefined`, callers derive the name from the
980
+ * file name. Callers treat a rejection like `undefined`.
981
+ *
982
+ * @param normalizedSource - The source, normalized with
983
+ * `SourceUtils.normalizeSource` like {@link DataProvider.normalize} does
984
+ * @param workspace - The directory handle of the open workspace, if any
985
+ * @param options - Optional abort signal
986
+ * @returns A promise that resolves to the name, if any
987
+ */
988
+ readName?(normalizedSource: string, workspace: FileSystemDirectoryHandle | null, options?: {
989
+ signal?: AbortSignal;
990
+ }): Promise<string | undefined>;
991
+ /**
992
+ * Prepares a data source before a data object backed by it is added, or
993
+ * before it is saved with a changed source or type
994
+ *
995
+ * Called once per added data object, with the data source the user entered,
996
+ * and once per saved edit that changes the data source's `source` or `type`.
997
+ * May create the data objects the data source depends on, and returns the
998
+ * data source to add or save.
999
+ *
1000
+ * @param dataSource - The data source to prepare, not normalized
1001
+ * @param workspace - The directory handle of the open workspace, if any
1002
+ * @param projectSource - Where the project was loaded from, if anywhere (see
1003
+ * {@link DataProvider.normalize})
1004
+ * @param options - Optional abort signal
1005
+ * @returns A promise that resolves to the data source to add or save
1006
+ */
1007
+ prepareDataSource?(dataSource: TDataSource, workspace: FileSystemDirectoryHandle | null, projectSource: string | null, options?: {
1008
+ signal?: AbortSignal;
1009
+ }): Promise<TDataSource>;
938
1010
  /**
939
1011
  * Opens a data source and returns the loaded data accessor
940
1012
  *
@@ -3985,6 +4057,14 @@ export declare type SizeConfig = ConstantConfig<number, {
3985
4057
  * URL's path are dropped silently. Resolving within the workspace collapses the
3986
4058
  * segments and fails if the path leaves the workspace.
3987
4059
  *
4060
+ * Helpers work on normalized sources: {@link SourceUtils.isWorkspacePath}
4061
+ * classifies them, {@link SourceUtils.makeWorkspacePath} builds them,
4062
+ * {@link SourceUtils.getPathSegments}, {@link SourceUtils.getParentSource},
4063
+ * {@link SourceUtils.getStem} and {@link SourceUtils.getExtension} take them
4064
+ * apart, and
4065
+ * {@link SourceUtils.makeProjectPath} turns them back into project-relative
4066
+ * paths.
4067
+ *
3988
4068
  * A relative path whose first segment contains a colon (e.g.
3989
4069
  * `sample1:ch2.tif`) is taken for a URL; write it as `./sample1:ch2.tif`
3990
4070
  * instead.
@@ -3994,38 +4074,6 @@ export declare class SourceUtils {
3994
4074
  private static readonly _appPathPrefix;
3995
4075
  private static readonly _workspacePathPrefix;
3996
4076
  private static readonly _urlSchemePattern;
3997
- /**
3998
- * Returns whether a normalized source refers to a file or directory within
3999
- * the workspace
4000
- *
4001
- * @param normalizedSource - The normalized source (see
4002
- * {@link SourceUtils.normalizeSource})
4003
- * @returns `true` for workspace-relative paths, `false` for URLs and
4004
- * app-relative paths
4005
- */
4006
- static isWorkspacePath(normalizedSource: string): boolean;
4007
- /**
4008
- * Builds a workspace-relative path from the segments of a path within the
4009
- * workspace, as returned by `FileSystemDirectoryHandle.resolve`
4010
- *
4011
- * @param segments - The path segments, from the workspace root down to the
4012
- * file or directory
4013
- * @returns The workspace-relative path (with `/` prefix)
4014
- */
4015
- static makeWorkspacePath(segments: string[]): string;
4016
- /**
4017
- * Returns the directory that contains a normalized source
4018
- *
4019
- * @param normalizedSource - The normalized source (see
4020
- * {@link SourceUtils.normalizeSource})
4021
- * @returns The normalized parent source and the name of the source within
4022
- * it, decoded for URLs; `null` if the source is the root of its URL's path
4023
- * or lies directly in the workspace, as the workspace root is no source
4024
- */
4025
- static getParentSource(normalizedSource: string): {
4026
- parentSource: string;
4027
- name: string;
4028
- } | null;
4029
4077
  /**
4030
4078
  * Normalizes a source, applying the fallbacks for missing project sources
4031
4079
  * and workspaces
@@ -4098,6 +4146,24 @@ export declare class SourceUtils {
4098
4146
  static resolveSourceFile(normalizedSource: string, workspace: FileSystemDirectoryHandle | null, options?: {
4099
4147
  signal?: AbortSignal;
4100
4148
  }): Promise<string | FileSystemFileHandle>;
4149
+ /**
4150
+ * Resolves a normalized source to an absolute URL or a directory handle
4151
+ *
4152
+ * Like {@link SourceUtils.resolveSource}, for sources that have to refer to
4153
+ * a directory.
4154
+ *
4155
+ * @param normalizedSource - See {@link SourceUtils.resolveSource}
4156
+ * @param workspace - See {@link SourceUtils.resolveSource}
4157
+ * @param options - See {@link SourceUtils.resolveSource}
4158
+ * @returns A promise that resolves to the absolute URL, or to the directory
4159
+ * handle for sources within the workspace
4160
+ * @throws See {@link SourceUtils.resolveSource}
4161
+ * @throws DOMException if the source refers to a file within the workspace
4162
+ * (`TypeMismatchError`)
4163
+ */
4164
+ static resolveSourceDirectory(normalizedSource: string, workspace: FileSystemDirectoryHandle | null, options?: {
4165
+ signal?: AbortSignal;
4166
+ }): Promise<string | FileSystemDirectoryHandle>;
4101
4167
  /**
4102
4168
  * Resolves a normalized source to an absolute URL or an opened file
4103
4169
  *
@@ -4128,23 +4194,102 @@ export declare class SourceUtils {
4128
4194
  file: File;
4129
4195
  }>;
4130
4196
  /**
4131
- * Resolves a normalized source to an absolute URL or a directory handle
4197
+ * Returns whether a normalized source refers to a file or directory within
4198
+ * the workspace
4132
4199
  *
4133
- * Like {@link SourceUtils.resolveSource}, for sources that have to refer to
4134
- * a directory.
4200
+ * @param normalizedSource - The normalized source (see
4201
+ * {@link SourceUtils.normalizeSource})
4202
+ * @returns `true` for workspace-relative paths, `false` for URLs and
4203
+ * app-relative paths
4204
+ */
4205
+ static isWorkspacePath(normalizedSource: string): boolean;
4206
+ /**
4207
+ * Builds a workspace-relative path from the segments of a path within the
4208
+ * workspace, as returned by `FileSystemDirectoryHandle.resolve`
4135
4209
  *
4136
- * @param normalizedSource - See {@link SourceUtils.resolveSource}
4137
- * @param workspace - See {@link SourceUtils.resolveSource}
4138
- * @param options - See {@link SourceUtils.resolveSource}
4139
- * @returns A promise that resolves to the absolute URL, or to the directory
4140
- * handle for sources within the workspace
4141
- * @throws See {@link SourceUtils.resolveSource}
4142
- * @throws DOMException if the source refers to a file within the workspace
4143
- * (`TypeMismatchError`)
4210
+ * @param segments - The path segments, from the workspace root down to the
4211
+ * file or directory
4212
+ * @returns The workspace-relative path (with `/` prefix)
4144
4213
  */
4145
- static resolveSourceDirectory(normalizedSource: string, workspace: FileSystemDirectoryHandle | null, options?: {
4146
- signal?: AbortSignal;
4147
- }): Promise<string | FileSystemDirectoryHandle>;
4214
+ static makeWorkspacePath(segments: string[]): string;
4215
+ /**
4216
+ * Turns a workspace-relative path into a path relative to the project file,
4217
+ * if the project was loaded from the workspace
4218
+ *
4219
+ * The result normalizes back to the same workspace-relative path (see
4220
+ * {@link SourceUtils.normalizeSource}), so data sources stay valid when the
4221
+ * project and its data are moved together. A first segment that contains a
4222
+ * colon gets a `./` prefix where it would otherwise be taken for a URL.
4223
+ *
4224
+ * @param normalizedSource - The normalized source (see
4225
+ * {@link SourceUtils.normalizeSource}); a workspace-relative path has to
4226
+ * name a file or directory within the workspace, not its root
4227
+ * @param projectSource - Where the project was loaded from (see
4228
+ * {@link SourceUtils.normalizeSource})
4229
+ * @returns The project-relative path (e.g. `points.csv` or
4230
+ * `../shared/x.csv`, or `.` for the project file's own directory) if both
4231
+ * the source and the project source are workspace-relative; the source
4232
+ * unchanged otherwise
4233
+ */
4234
+ static makeProjectPath(normalizedSource: string, projectSource: string | null): string;
4235
+ /**
4236
+ * Returns the path segments of a normalized source
4237
+ *
4238
+ * @param normalizedSource - The normalized source (see
4239
+ * {@link SourceUtils.normalizeSource})
4240
+ * @returns The segments from the workspace root down to the file or
4241
+ * directory, or the decoded segments of a URL's path, without its query and
4242
+ * hash; empty for URLs without a path, such as `data:` URLs, and for the
4243
+ * root of an origin
4244
+ * @throws TypeError if the source is neither a workspace-relative path nor
4245
+ * an absolute URL
4246
+ * @throws Error if a workspace-relative path leads above the workspace root
4247
+ */
4248
+ static getPathSegments(normalizedSource: string): string[];
4249
+ /**
4250
+ * Returns the directory that contains a normalized source
4251
+ *
4252
+ * @param normalizedSource - The normalized source (see
4253
+ * {@link SourceUtils.normalizeSource})
4254
+ * @returns The normalized parent source and the name of the source within
4255
+ * it, decoded for URLs; `null` if the source is the root of its URL's path,
4256
+ * is a URL without a path (such as a `data:` URL), or lies directly in the
4257
+ * workspace, as the workspace root is no source
4258
+ * @throws See {@link SourceUtils.getPathSegments}
4259
+ */
4260
+ static getParentSource(normalizedSource: string): {
4261
+ parentSource: string;
4262
+ name: string;
4263
+ } | null;
4264
+ /**
4265
+ * Returns the name of the file or directory a normalized source refers to,
4266
+ * without its last extension
4267
+ *
4268
+ * Only the last extension is dropped, so `cells.ome.zarr` gives `cells.ome`.
4269
+ * A leading dot does not start an extension, so `.hidden` stays as is.
4270
+ *
4271
+ * @param normalizedSource - The normalized source (see
4272
+ * {@link SourceUtils.normalizeSource})
4273
+ * @returns The stem of the last path segment, decoded for URLs; an empty
4274
+ * string for URLs without a path, such as `data:` URLs or the root of an
4275
+ * origin
4276
+ * @throws See {@link SourceUtils.getPathSegments}
4277
+ */
4278
+ static getStem(normalizedSource: string): string;
4279
+ /**
4280
+ * Returns the last extension of the file or directory a normalized source
4281
+ * refers to, lower-cased for comparison
4282
+ *
4283
+ * Only the last extension is returned, so `cells.ome.zarr` gives `.zarr`.
4284
+ * A leading dot does not start an extension, so `.hidden` has none.
4285
+ *
4286
+ * @param normalizedSource - The normalized source (see
4287
+ * {@link SourceUtils.normalizeSource})
4288
+ * @returns The extension including its dot (e.g. `.csv`), or an empty
4289
+ * string if the last path segment has none
4290
+ * @throws See {@link SourceUtils.getPathSegments}
4291
+ */
4292
+ static getExtension(normalizedSource: string): string;
4148
4293
  /**
4149
4294
  * Normalizes a project-relative path
4150
4295
  *
@@ -4201,6 +4346,25 @@ export declare class SourceUtils {
4201
4346
  * @throws TypeError if the path does not form a valid URL with the base URL
4202
4347
  */
4203
4348
  private static _normalizeAppPath;
4349
+ /**
4350
+ * Splits a workspace-relative path into its collapsed segments, the inverse
4351
+ * of {@link SourceUtils.makeWorkspacePath}
4352
+ *
4353
+ * @param workspacePath - The workspace-relative path (with `/` prefix)
4354
+ * @returns The segments from the workspace root down to the file or
4355
+ * directory, empty for the root itself
4356
+ * @throws Error if the path leads above the workspace root
4357
+ */
4358
+ private static _getWorkspaceSegments;
4359
+ /**
4360
+ * Splits the path of a URL into its segments
4361
+ *
4362
+ * @param url - The URL
4363
+ * @returns The non-empty, still encoded segments of the URL's path; empty
4364
+ * for URLs without a hierarchical path, such as `data:` and `blob:` URLs,
4365
+ * whose path does not start with `/`
4366
+ */
4367
+ private static _getURLSegments;
4204
4368
  /**
4205
4369
  * Splits a path into segments, dropping empty and `.` segments and
4206
4370
  * collapsing `..` segments against the segments preceding them
@@ -4212,6 +4376,22 @@ export declare class SourceUtils {
4212
4376
  * @throws Error if the path leads above the root
4213
4377
  */
4214
4378
  private static _collapseSegments;
4379
+ /**
4380
+ * Returns where the last extension of a file or directory name starts
4381
+ *
4382
+ * @param name - The name
4383
+ * @returns The index of the extension's dot, or `-1` if the name has no
4384
+ * extension; a leading dot does not start one
4385
+ */
4386
+ private static _getExtensionIndex;
4387
+ /**
4388
+ * Decodes a percent-encoded URL path segment
4389
+ *
4390
+ * @param segment - The encoded segment
4391
+ * @returns The decoded segment, or the segment as is if it holds a malformed
4392
+ * escape (e.g. a stray `%`), which the URL parser lets through
4393
+ */
4394
+ private static _decodeSegment;
4215
4395
  }
4216
4396
 
4217
4397
  /**
package/dist/index.js CHANGED
@@ -2346,6 +2346,14 @@ var ProjectUtils = class {
2346
2346
  * URL's path are dropped silently. Resolving within the workspace collapses the
2347
2347
  * segments and fails if the path leaves the workspace.
2348
2348
  *
2349
+ * Helpers work on normalized sources: {@link SourceUtils.isWorkspacePath}
2350
+ * classifies them, {@link SourceUtils.makeWorkspacePath} builds them,
2351
+ * {@link SourceUtils.getPathSegments}, {@link SourceUtils.getParentSource},
2352
+ * {@link SourceUtils.getStem} and {@link SourceUtils.getExtension} take them
2353
+ * apart, and
2354
+ * {@link SourceUtils.makeProjectPath} turns them back into project-relative
2355
+ * paths.
2356
+ *
2349
2357
  * A relative path whose first segment contains a colon (e.g.
2350
2358
  * `sample1:ch2.tif`) is taken for a URL; write it as `./sample1:ch2.tif`
2351
2359
  * instead.
@@ -2356,58 +2364,6 @@ var SourceUtils = class SourceUtils {
2356
2364
  static _workspacePathPrefix = SourceUtils._pathSep;
2357
2365
  static _urlSchemePattern = /^[a-z][a-z0-9+.-]*:/i;
2358
2366
  /**
2359
- * Returns whether a normalized source refers to a file or directory within
2360
- * the workspace
2361
- *
2362
- * @param normalizedSource - The normalized source (see
2363
- * {@link SourceUtils.normalizeSource})
2364
- * @returns `true` for workspace-relative paths, `false` for URLs and
2365
- * app-relative paths
2366
- */
2367
- static isWorkspacePath(normalizedSource) {
2368
- return normalizedSource.startsWith(SourceUtils._workspacePathPrefix) && !normalizedSource.startsWith(SourceUtils._appPathPrefix);
2369
- }
2370
- /**
2371
- * Builds a workspace-relative path from the segments of a path within the
2372
- * workspace, as returned by `FileSystemDirectoryHandle.resolve`
2373
- *
2374
- * @param segments - The path segments, from the workspace root down to the
2375
- * file or directory
2376
- * @returns The workspace-relative path (with `/` prefix)
2377
- */
2378
- static makeWorkspacePath(segments) {
2379
- return SourceUtils._workspacePathPrefix + segments.join(SourceUtils._pathSep);
2380
- }
2381
- /**
2382
- * Returns the directory that contains a normalized source
2383
- *
2384
- * @param normalizedSource - The normalized source (see
2385
- * {@link SourceUtils.normalizeSource})
2386
- * @returns The normalized parent source and the name of the source within
2387
- * it, decoded for URLs; `null` if the source is the root of its URL's path
2388
- * or lies directly in the workspace, as the workspace root is no source
2389
- */
2390
- static getParentSource(normalizedSource) {
2391
- if (SourceUtils.isWorkspacePath(normalizedSource)) {
2392
- const segments = normalizedSource.substring(SourceUtils._workspacePathPrefix.length).split(SourceUtils._pathSep);
2393
- const name = segments.pop();
2394
- if (name === void 0 || segments.length === 0) return null;
2395
- return {
2396
- parentSource: SourceUtils.makeWorkspacePath(segments),
2397
- name
2398
- };
2399
- }
2400
- const url = new URL(normalizedSource);
2401
- const segments = url.pathname.split(SourceUtils._pathSep).filter((segment) => segment !== "");
2402
- const name = segments.pop();
2403
- if (name === void 0) return null;
2404
- url.pathname = segments.join(SourceUtils._pathSep);
2405
- return {
2406
- parentSource: url.toString(),
2407
- name: decodeURIComponent(name)
2408
- };
2409
- }
2410
- /**
2411
2367
  * Normalizes a source, applying the fallbacks for missing project sources
2412
2368
  * and workspaces
2413
2369
  *
@@ -2471,7 +2427,7 @@ var SourceUtils = class SourceUtils {
2471
2427
  signal?.throwIfAborted();
2472
2428
  if (!SourceUtils.isWorkspacePath(normalizedSource)) return normalizedSource;
2473
2429
  if (workspace === null) throw new Error(`Cannot resolve workspace-relative path without workspace: ${normalizedSource}`);
2474
- const segments = SourceUtils._collapseSegments(normalizedSource.substring(SourceUtils._workspacePathPrefix.length));
2430
+ const segments = SourceUtils._getWorkspaceSegments(normalizedSource);
2475
2431
  const entryName = segments.pop();
2476
2432
  if (entryName === void 0) throw new Error(`Not a workspace file or directory: ${normalizedSource}`);
2477
2433
  let dir = workspace;
@@ -2511,6 +2467,26 @@ var SourceUtils = class SourceUtils {
2511
2467
  return resolvedSource;
2512
2468
  }
2513
2469
  /**
2470
+ * Resolves a normalized source to an absolute URL or a directory handle
2471
+ *
2472
+ * Like {@link SourceUtils.resolveSource}, for sources that have to refer to
2473
+ * a directory.
2474
+ *
2475
+ * @param normalizedSource - See {@link SourceUtils.resolveSource}
2476
+ * @param workspace - See {@link SourceUtils.resolveSource}
2477
+ * @param options - See {@link SourceUtils.resolveSource}
2478
+ * @returns A promise that resolves to the absolute URL, or to the directory
2479
+ * handle for sources within the workspace
2480
+ * @throws See {@link SourceUtils.resolveSource}
2481
+ * @throws DOMException if the source refers to a file within the workspace
2482
+ * (`TypeMismatchError`)
2483
+ */
2484
+ static async resolveSourceDirectory(normalizedSource, workspace, options) {
2485
+ const resolvedSource = await SourceUtils.resolveSource(normalizedSource, workspace, options);
2486
+ if (typeof resolvedSource !== "string" && resolvedSource.kind !== "directory") throw new DOMException(`Not a workspace directory: ${normalizedSource}`, "TypeMismatchError");
2487
+ return resolvedSource;
2488
+ }
2489
+ /**
2514
2490
  * Resolves a normalized source to an absolute URL or an opened file
2515
2491
  *
2516
2492
  * Like {@link SourceUtils.resolveSourceFile}, for readers that take either
@@ -2539,24 +2515,141 @@ var SourceUtils = class SourceUtils {
2539
2515
  return { file };
2540
2516
  }
2541
2517
  /**
2542
- * Resolves a normalized source to an absolute URL or a directory handle
2518
+ * Returns whether a normalized source refers to a file or directory within
2519
+ * the workspace
2543
2520
  *
2544
- * Like {@link SourceUtils.resolveSource}, for sources that have to refer to
2545
- * a directory.
2521
+ * @param normalizedSource - The normalized source (see
2522
+ * {@link SourceUtils.normalizeSource})
2523
+ * @returns `true` for workspace-relative paths, `false` for URLs and
2524
+ * app-relative paths
2525
+ */
2526
+ static isWorkspacePath(normalizedSource) {
2527
+ return normalizedSource.startsWith(SourceUtils._workspacePathPrefix) && !normalizedSource.startsWith(SourceUtils._appPathPrefix);
2528
+ }
2529
+ /**
2530
+ * Builds a workspace-relative path from the segments of a path within the
2531
+ * workspace, as returned by `FileSystemDirectoryHandle.resolve`
2546
2532
  *
2547
- * @param normalizedSource - See {@link SourceUtils.resolveSource}
2548
- * @param workspace - See {@link SourceUtils.resolveSource}
2549
- * @param options - See {@link SourceUtils.resolveSource}
2550
- * @returns A promise that resolves to the absolute URL, or to the directory
2551
- * handle for sources within the workspace
2552
- * @throws See {@link SourceUtils.resolveSource}
2553
- * @throws DOMException if the source refers to a file within the workspace
2554
- * (`TypeMismatchError`)
2533
+ * @param segments - The path segments, from the workspace root down to the
2534
+ * file or directory
2535
+ * @returns The workspace-relative path (with `/` prefix)
2555
2536
  */
2556
- static async resolveSourceDirectory(normalizedSource, workspace, options) {
2557
- const resolvedSource = await SourceUtils.resolveSource(normalizedSource, workspace, options);
2558
- if (typeof resolvedSource !== "string" && resolvedSource.kind !== "directory") throw new DOMException(`Not a workspace directory: ${normalizedSource}`, "TypeMismatchError");
2559
- return resolvedSource;
2537
+ static makeWorkspacePath(segments) {
2538
+ return SourceUtils._workspacePathPrefix + segments.join(SourceUtils._pathSep);
2539
+ }
2540
+ /**
2541
+ * Turns a workspace-relative path into a path relative to the project file,
2542
+ * if the project was loaded from the workspace
2543
+ *
2544
+ * The result normalizes back to the same workspace-relative path (see
2545
+ * {@link SourceUtils.normalizeSource}), so data sources stay valid when the
2546
+ * project and its data are moved together. A first segment that contains a
2547
+ * colon gets a `./` prefix where it would otherwise be taken for a URL.
2548
+ *
2549
+ * @param normalizedSource - The normalized source (see
2550
+ * {@link SourceUtils.normalizeSource}); a workspace-relative path has to
2551
+ * name a file or directory within the workspace, not its root
2552
+ * @param projectSource - Where the project was loaded from (see
2553
+ * {@link SourceUtils.normalizeSource})
2554
+ * @returns The project-relative path (e.g. `points.csv` or
2555
+ * `../shared/x.csv`, or `.` for the project file's own directory) if both
2556
+ * the source and the project source are workspace-relative; the source
2557
+ * unchanged otherwise
2558
+ */
2559
+ static makeProjectPath(normalizedSource, projectSource) {
2560
+ if (projectSource === null || !SourceUtils.isWorkspacePath(projectSource) || !SourceUtils.isWorkspacePath(normalizedSource)) return normalizedSource;
2561
+ const projectDirSegments = SourceUtils._getWorkspaceSegments(projectSource).slice(0, -1);
2562
+ const segments = SourceUtils._getWorkspaceSegments(normalizedSource);
2563
+ let commonLength = 0;
2564
+ while (commonLength < projectDirSegments.length && commonLength < segments.length && projectDirSegments[commonLength] === segments[commonLength]) commonLength++;
2565
+ const projectPath = [...Array(projectDirSegments.length - commonLength).fill(".."), ...segments.slice(commonLength)].join(SourceUtils._pathSep);
2566
+ if (projectPath === "") return ".";
2567
+ return SourceUtils._urlSchemePattern.test(projectPath) ? `.${SourceUtils._pathSep}${projectPath}` : projectPath;
2568
+ }
2569
+ /**
2570
+ * Returns the path segments of a normalized source
2571
+ *
2572
+ * @param normalizedSource - The normalized source (see
2573
+ * {@link SourceUtils.normalizeSource})
2574
+ * @returns The segments from the workspace root down to the file or
2575
+ * directory, or the decoded segments of a URL's path, without its query and
2576
+ * hash; empty for URLs without a path, such as `data:` URLs, and for the
2577
+ * root of an origin
2578
+ * @throws TypeError if the source is neither a workspace-relative path nor
2579
+ * an absolute URL
2580
+ * @throws Error if a workspace-relative path leads above the workspace root
2581
+ */
2582
+ static getPathSegments(normalizedSource) {
2583
+ if (SourceUtils.isWorkspacePath(normalizedSource)) return SourceUtils._getWorkspaceSegments(normalizedSource);
2584
+ return SourceUtils._getURLSegments(new URL(normalizedSource)).map((segment) => SourceUtils._decodeSegment(segment));
2585
+ }
2586
+ /**
2587
+ * Returns the directory that contains a normalized source
2588
+ *
2589
+ * @param normalizedSource - The normalized source (see
2590
+ * {@link SourceUtils.normalizeSource})
2591
+ * @returns The normalized parent source and the name of the source within
2592
+ * it, decoded for URLs; `null` if the source is the root of its URL's path,
2593
+ * is a URL without a path (such as a `data:` URL), or lies directly in the
2594
+ * workspace, as the workspace root is no source
2595
+ * @throws See {@link SourceUtils.getPathSegments}
2596
+ */
2597
+ static getParentSource(normalizedSource) {
2598
+ if (SourceUtils.isWorkspacePath(normalizedSource)) {
2599
+ const segments = SourceUtils._getWorkspaceSegments(normalizedSource);
2600
+ const name = segments.pop();
2601
+ if (name === void 0 || segments.length === 0) return null;
2602
+ return {
2603
+ parentSource: SourceUtils.makeWorkspacePath(segments),
2604
+ name
2605
+ };
2606
+ }
2607
+ const url = new URL(normalizedSource);
2608
+ const segments = SourceUtils._getURLSegments(url);
2609
+ const name = segments.pop();
2610
+ if (name === void 0) return null;
2611
+ url.pathname = segments.join(SourceUtils._pathSep);
2612
+ return {
2613
+ parentSource: url.toString(),
2614
+ name: SourceUtils._decodeSegment(name)
2615
+ };
2616
+ }
2617
+ /**
2618
+ * Returns the name of the file or directory a normalized source refers to,
2619
+ * without its last extension
2620
+ *
2621
+ * Only the last extension is dropped, so `cells.ome.zarr` gives `cells.ome`.
2622
+ * A leading dot does not start an extension, so `.hidden` stays as is.
2623
+ *
2624
+ * @param normalizedSource - The normalized source (see
2625
+ * {@link SourceUtils.normalizeSource})
2626
+ * @returns The stem of the last path segment, decoded for URLs; an empty
2627
+ * string for URLs without a path, such as `data:` URLs or the root of an
2628
+ * origin
2629
+ * @throws See {@link SourceUtils.getPathSegments}
2630
+ */
2631
+ static getStem(normalizedSource) {
2632
+ const name = SourceUtils.getPathSegments(normalizedSource).at(-1) ?? "";
2633
+ const extensionIndex = SourceUtils._getExtensionIndex(name);
2634
+ return extensionIndex !== -1 ? name.substring(0, extensionIndex) : name;
2635
+ }
2636
+ /**
2637
+ * Returns the last extension of the file or directory a normalized source
2638
+ * refers to, lower-cased for comparison
2639
+ *
2640
+ * Only the last extension is returned, so `cells.ome.zarr` gives `.zarr`.
2641
+ * A leading dot does not start an extension, so `.hidden` has none.
2642
+ *
2643
+ * @param normalizedSource - The normalized source (see
2644
+ * {@link SourceUtils.normalizeSource})
2645
+ * @returns The extension including its dot (e.g. `.csv`), or an empty
2646
+ * string if the last path segment has none
2647
+ * @throws See {@link SourceUtils.getPathSegments}
2648
+ */
2649
+ static getExtension(normalizedSource) {
2650
+ const name = SourceUtils.getPathSegments(normalizedSource).at(-1) ?? "";
2651
+ const extensionIndex = SourceUtils._getExtensionIndex(name);
2652
+ return extensionIndex !== -1 ? name.substring(extensionIndex).toLowerCase() : "";
2560
2653
  }
2561
2654
  /**
2562
2655
  * Normalizes a project-relative path
@@ -2585,7 +2678,7 @@ var SourceUtils = class SourceUtils {
2585
2678
  } catch (error) {
2586
2679
  throw new Error(`Invalid project-relative path: ${projectPath}`, { cause: error });
2587
2680
  }
2588
- const projectDirSegments = SourceUtils._collapseSegments(projectSource.substring(SourceUtils._workspacePathPrefix.length)).slice(0, -1);
2681
+ const projectDirSegments = SourceUtils._getWorkspaceSegments(projectSource).slice(0, -1);
2589
2682
  const segments = SourceUtils._collapseSegments(projectPath, projectDirSegments);
2590
2683
  return SourceUtils._normalizeWorkspacePath(SourceUtils.makeWorkspacePath(segments), workspace, options);
2591
2684
  }
@@ -2612,13 +2705,12 @@ var SourceUtils = class SourceUtils {
2612
2705
  */
2613
2706
  static _normalizeWorkspacePath(workspacePath, workspace, options) {
2614
2707
  if (!workspacePath.startsWith(SourceUtils._workspacePathPrefix)) throw new Error(`Invalid workspace-relative path: ${workspacePath}`);
2615
- const path = workspacePath.substring(SourceUtils._workspacePathPrefix.length);
2616
2708
  if (workspace !== null) {
2617
- const segments = SourceUtils._collapseSegments(path);
2709
+ const segments = SourceUtils._getWorkspaceSegments(workspacePath);
2618
2710
  if (segments.length === 0) throw new Error(`Not a workspace file or directory: ${workspacePath}`);
2619
2711
  return SourceUtils.makeWorkspacePath(segments);
2620
2712
  }
2621
- return SourceUtils._normalizeAppPath(SourceUtils._appPathPrefix + path, options);
2713
+ return SourceUtils._normalizeAppPath(SourceUtils._appPathPrefix + workspacePath.substring(SourceUtils._workspacePathPrefix.length), options);
2622
2714
  }
2623
2715
  /**
2624
2716
  * Normalizes an app-relative path to an absolute URL
@@ -2641,6 +2733,30 @@ var SourceUtils = class SourceUtils {
2641
2733
  return new URL(path, baseUrl).toString();
2642
2734
  }
2643
2735
  /**
2736
+ * Splits a workspace-relative path into its collapsed segments, the inverse
2737
+ * of {@link SourceUtils.makeWorkspacePath}
2738
+ *
2739
+ * @param workspacePath - The workspace-relative path (with `/` prefix)
2740
+ * @returns The segments from the workspace root down to the file or
2741
+ * directory, empty for the root itself
2742
+ * @throws Error if the path leads above the workspace root
2743
+ */
2744
+ static _getWorkspaceSegments(workspacePath) {
2745
+ return SourceUtils._collapseSegments(workspacePath.substring(SourceUtils._workspacePathPrefix.length));
2746
+ }
2747
+ /**
2748
+ * Splits the path of a URL into its segments
2749
+ *
2750
+ * @param url - The URL
2751
+ * @returns The non-empty, still encoded segments of the URL's path; empty
2752
+ * for URLs without a hierarchical path, such as `data:` and `blob:` URLs,
2753
+ * whose path does not start with `/`
2754
+ */
2755
+ static _getURLSegments(url) {
2756
+ if (!url.pathname.startsWith(SourceUtils._pathSep)) return [];
2757
+ return url.pathname.split(SourceUtils._pathSep).filter((segment) => segment !== "");
2758
+ }
2759
+ /**
2644
2760
  * Splits a path into segments, dropping empty and `.` segments and
2645
2761
  * collapsing `..` segments against the segments preceding them
2646
2762
  *
@@ -2661,6 +2777,31 @@ var SourceUtils = class SourceUtils {
2661
2777
  }
2662
2778
  return segments;
2663
2779
  }
2780
+ /**
2781
+ * Returns where the last extension of a file or directory name starts
2782
+ *
2783
+ * @param name - The name
2784
+ * @returns The index of the extension's dot, or `-1` if the name has no
2785
+ * extension; a leading dot does not start one
2786
+ */
2787
+ static _getExtensionIndex(name) {
2788
+ const extensionIndex = name.lastIndexOf(".");
2789
+ return extensionIndex > 0 ? extensionIndex : -1;
2790
+ }
2791
+ /**
2792
+ * Decodes a percent-encoded URL path segment
2793
+ *
2794
+ * @param segment - The encoded segment
2795
+ * @returns The decoded segment, or the segment as is if it holds a malformed
2796
+ * escape (e.g. a stray `%`), which the URL parser lets through
2797
+ */
2798
+ static _decodeSegment(segment) {
2799
+ try {
2800
+ return decodeURIComponent(segment);
2801
+ } catch {
2802
+ return segment;
2803
+ }
2804
+ }
2664
2805
  };
2665
2806
  //#endregion
2666
2807
  //#region src/utils/TableUtils.ts
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@tissuumaps/core",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Core model, types and utilities shared by the TissUUmaps packages",
5
- "homepage": "https://tissuumaps.github.io/TissUUmaps/",
5
+ "homepage": "https://tissuumaps.github.io/",
6
6
  "bugs": "https://github.com/TissUUmaps/TissUUmaps/issues",
7
7
  "repository": {
8
8
  "type": "git",
@@ -32,11 +32,13 @@
32
32
  "@jsonforms/core": "^3.8.0",
33
33
  "@types/d3-color": "^3.1.3",
34
34
  "@types/d3-scale-chromatic": "^3.1.0",
35
+ "immer": "^11.1.8",
35
36
  "openseadragon": "^6.1.1",
36
37
  "zustand": "^5.0.15"
37
38
  },
38
39
  "peerDependencies": {
39
40
  "@jsonforms/core": "^3.8.0",
41
+ "immer": "^11.1.8",
40
42
  "openseadragon": "^6.1.1",
41
43
  "zustand": "^5.0.15"
42
44
  },
@@ -44,6 +46,9 @@
44
46
  "@jsonforms/core": {
45
47
  "optional": true
46
48
  },
49
+ "immer": {
50
+ "optional": true
51
+ },
47
52
  "openseadragon": {
48
53
  "optional": true
49
54
  },