@jbrowse/plugin-trix 4.3.0 → 5.0.0-beta.10

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.
@@ -5,10 +5,14 @@ import type PluginManager from '@jbrowse/core/PluginManager';
5
5
  import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
6
6
  import type { BaseTextSearchAdapter, BaseTextSearchArgs } from '@jbrowse/core/data_adapters/BaseAdapter';
7
7
  import type { getSubAdapterType } from '@jbrowse/core/data_adapters/dataAdapterCache';
8
+ export declare function snippetAround(str: string, term: string, w?: number): string;
8
9
  export default class TrixTextSearchAdapter extends BaseAdapter implements BaseTextSearchAdapter {
9
- indexingAttributes?: string[];
10
10
  trixJs: Trix;
11
- tracksNames?: string[];
12
11
  constructor(config: AnyConfigurationModel, getSubAdapter?: getSubAdapterType, pluginManager?: PluginManager);
12
+ /**
13
+ * Returns list of results
14
+ * @param args - search options/arguments include: search query,
15
+ * searchType (prefix | full | exact), etc.
16
+ */
13
17
  searchIndex(args: BaseTextSearchArgs): Promise<BaseResult[]>;
14
18
  }
@@ -2,69 +2,101 @@ import Trix from '@gmod/trix';
2
2
  import BaseResult from '@jbrowse/core/TextSearch/BaseResults';
3
3
  import { readConfObject } from '@jbrowse/core/configuration';
4
4
  import { BaseAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
5
+ import { checkAbortSignal } from '@jbrowse/core/util/aborting';
5
6
  import { openLocation } from '@jbrowse/core/util/io';
6
7
  function decodeURIComponentNoThrow(uri) {
7
8
  try {
8
9
  return decodeURIComponent(uri);
9
10
  }
10
- catch (e) {
11
+ catch {
11
12
  return uri;
12
13
  }
13
14
  }
14
- function shorten(str, term, w = 15) {
15
- const tidx = str.toLowerCase().indexOf(term);
16
- return str.length < 40
17
- ? str
18
- : (Math.max(0, tidx - w) > 0 ? '...' : '') +
19
- str.slice(Math.max(0, tidx - w), tidx + term.length + w).trim() +
20
- (tidx + term.length < str.length ? '...' : '');
15
+ // Build a search-result snippet: a window of `w` characters either side of
16
+ // where `term` matched, ellipsized on whichever side was cut. Nothing like the
17
+ // core `shorten`, which just elides a long name.
18
+ export function snippetAround(str, term, w = 15) {
19
+ if (str.length < 40) {
20
+ return str;
21
+ }
22
+ const tidx = str.toLowerCase().indexOf(term.toLowerCase());
23
+ if (tidx === -1) {
24
+ return `${str.slice(0, 40).trim()}...`;
25
+ }
26
+ const start = Math.max(0, tidx - w);
27
+ const end = tidx + term.length + w;
28
+ return ((start > 0 ? '...' : '') +
29
+ str.slice(start, end).trim() +
30
+ (end < str.length ? '...' : ''));
21
31
  }
22
32
  export default class TrixTextSearchAdapter extends BaseAdapter {
23
- indexingAttributes;
24
33
  trixJs;
25
- tracksNames;
26
34
  constructor(config, getSubAdapter, pluginManager) {
27
35
  super(config, getSubAdapter, pluginManager);
28
36
  const ixFilePath = readConfObject(config, 'ixFilePath');
29
37
  const ixxFilePath = readConfObject(config, 'ixxFilePath');
30
38
  if (!ixFilePath) {
31
- throw new Error('must provide out.ix');
39
+ throw new Error('TrixTextSearchAdapter requires an ixFilePath');
32
40
  }
33
41
  if (!ixxFilePath) {
34
- throw new Error('must provide out.ixx');
42
+ throw new Error('TrixTextSearchAdapter requires an ixxFilePath');
35
43
  }
36
44
  this.trixJs = new Trix(openLocation(ixxFilePath, pluginManager), openLocation(ixFilePath, pluginManager), 1500);
37
45
  }
46
+ /**
47
+ * Returns list of results
48
+ * @param args - search options/arguments include: search query,
49
+ * searchType (prefix | full | exact), etc.
50
+ */
38
51
  async searchIndex(args) {
39
- const query = args.queryString.toLowerCase();
40
- const strs = query.split(' ');
41
- const results = await this.trixJs.search(query);
52
+ const query = args.queryString.toLowerCase().trim();
53
+ // any run of whitespace separates words; splitting on a single ' ' let a
54
+ // double space or a tab produce an empty word that matches everything
55
+ const words = query.split(/\s+/);
56
+ const results = await this.trixJs.search(query, { signal: args.signal });
57
+ // the index read has landed but nothing below it has run yet; a broad
58
+ // prefix returns thousands of records to JSON-parse, decode and snippet,
59
+ // and the keystroke that asked for them is 50ms stale by now
60
+ checkAbortSignal(args.signal);
42
61
  const formatted = results
43
- .filter(([, data]) => strs.every(r => decodeURIComponentNoThrow(data).toLowerCase().includes(r)))
44
62
  .map(([term, data]) => {
45
- const result = JSON.parse(data.replaceAll('|', ','));
46
- const [loc, trackId, ...rest] = result.map(record => decodeURIComponentNoThrow(record));
47
- const labelFieldIdx = rest.findIndex(elt => !!elt);
48
- const contextIdx = rest
49
- .map(elt => elt.toLowerCase())
50
- .findIndex(f => f.includes(term.toLowerCase()));
51
- const labelField = rest[labelFieldIdx];
52
- const contextField = rest[contextIdx];
53
- const context = contextIdx !== -1 ? shorten(contextField, term) : undefined;
54
- const label = shorten(labelField, term);
63
+ // record is ["loc"|"trackId"|"attr1"|...] with commas pre-encoded as
64
+ // pipes; swap them back to parse it as a JSON array, then URI-decode
65
+ const [loc, trackId, ...attrs] = JSON.parse(data.replaceAll('|', ',')).map(decodeURIComponentNoThrow);
66
+ return { term, loc, trackId, attrs };
67
+ })
68
+ // multi-word search: keep only entries whose attributes contain every word
69
+ .filter(({ attrs }) => {
70
+ const lower = attrs.join(' ').toLowerCase();
71
+ return words.every(w => lower.includes(w));
72
+ })
73
+ .map(({ term, loc, trackId, attrs }) => {
74
+ const termLower = term.toLowerCase();
75
+ const labelField = attrs.find(Boolean) ?? '';
76
+ const contextField = attrs.find(f => f.toLowerCase().includes(termLower));
77
+ const context = contextField
78
+ ? snippetAround(contextField, term)
79
+ : undefined;
80
+ const label = snippetAround(labelField, term);
55
81
  const displayString = !context || label.toLowerCase() === context.toLowerCase()
56
82
  ? label
57
83
  : `${label} (${context})`;
84
+ // exact match succeeds when any indexed attribute (name, id,
85
+ // description, ...) equals the query, so e.g. searching an ID works.
86
+ // Carried on the result as well as filtered on: only this adapter can
87
+ // judge it, and a caller that has the flag can ask one unrestricted
88
+ // question instead of an exact one and then a broad one
89
+ const exact = attrs.some(a => a.toLowerCase() === query);
58
90
  return new BaseResult({
59
91
  locString: loc,
60
- label: labelField,
92
+ label,
61
93
  displayString,
62
- matchedObject: result.map(record => decodeURIComponent(record)),
63
94
  trackId,
95
+ exact,
64
96
  });
65
97
  });
66
98
  return args.searchType === 'exact'
67
- ? formatted.filter(r => r.getLabel().toLowerCase() === args.queryString.toLowerCase())
99
+ ? formatted.filter(r => r.isExact())
68
100
  : formatted;
69
101
  }
70
102
  }
@@ -1,34 +1,45 @@
1
+ export declare function normalizeSnapshot(snap: Record<string, unknown>): Record<string, unknown>;
2
+ /**
3
+ * #config TrixTextSearchAdapter
4
+ * #trackType TextSearchAdapter
5
+ * #fileFormat textsearch | Trix index (.ix/.ixx) | Built by `jbrowse text-index`
6
+ *
7
+ * #example
8
+ * `jbrowse text-index` writes this entry into `aggregateTextSearchAdapters` for
9
+ * you. The `uri` shorthand points at the `.ix` and the sibling `.ixx` is derived
10
+ * from it, so the pair only needs spelling out when they are named against
11
+ * convention. Written by hand, the string `'trix/hg38.ix'` is the whole entry,
12
+ * the same as `{ uri: 'trix/hg38.ix' }`: a `.ix` implies this type, and
13
+ * `assemblyNames` defaults to the track's own for a per-track index or to the
14
+ * config's one assembly.
15
+ * ```js
16
+ * {
17
+ * type: 'TrixTextSearchAdapter',
18
+ * uri: 'trix/hg38.ix',
19
+ * assemblyNames: ['hg38'],
20
+ * }
21
+ * ```
22
+ */
1
23
  declare const TrixTextSearchAdapter: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
2
- ixFilePath: {
3
- type: string;
4
- defaultValue: {
5
- uri: string;
6
- locationType: string;
7
- };
24
+ type: {
25
+ type: "string";
26
+ } & {
27
+ defaultValue: string;
8
28
  };
9
- ixxFilePath: {
10
- type: string;
11
- defaultValue: {
12
- uri: string;
13
- locationType: string;
14
- };
29
+ readonly ixFilePath: {
30
+ type: "fileLocation";
31
+ } & {
32
+ defaultValue: unknown;
15
33
  };
16
- metaFilePath: {
17
- type: string;
18
- defaultValue: {
19
- uri: string;
20
- locationType: string;
21
- };
34
+ readonly ixxFilePath: {
35
+ type: "fileLocation";
36
+ } & {
37
+ defaultValue: unknown;
22
38
  };
23
- tracks: {
24
- type: string;
25
- defaultValue: never[];
26
- description: string;
39
+ readonly assemblyNames: {
40
+ type: "stringArray";
41
+ } & {
42
+ defaultValue: unknown;
27
43
  };
28
- assemblyNames: {
29
- type: string;
30
- defaultValue: never[];
31
- description: string;
32
- };
33
- }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, "textSearchAdapterId">>;
44
+ }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, true, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
34
45
  export default TrixTextSearchAdapter;
@@ -1,6 +1,47 @@
1
- import { ConfigurationSchema } from '@jbrowse/core/configuration';
2
- function x() { }
1
+ import { ConfigurationSchema, fillLocations } from '@jbrowse/core/configuration';
2
+ export function normalizeSnapshot(snap) {
3
+ return typeof snap.uri === 'string'
4
+ ? fillLocations(snap, {
5
+ // `uri` points at the `.ix` file and the `.ixx` sits beside it (the
6
+ // `jbrowse text-index` naming convention), so derive the pair
7
+ ixFilePath: {
8
+ uri: snap.uri,
9
+ baseUri: snap.baseUri,
10
+ },
11
+ ixxFilePath: {
12
+ uri: `${snap.uri}x`,
13
+ baseUri: snap.baseUri,
14
+ },
15
+ })
16
+ : snap;
17
+ }
18
+ /**
19
+ * #config TrixTextSearchAdapter
20
+ * #trackType TextSearchAdapter
21
+ * #fileFormat textsearch | Trix index (.ix/.ixx) | Built by `jbrowse text-index`
22
+ *
23
+ * #example
24
+ * `jbrowse text-index` writes this entry into `aggregateTextSearchAdapters` for
25
+ * you. The `uri` shorthand points at the `.ix` and the sibling `.ixx` is derived
26
+ * from it, so the pair only needs spelling out when they are named against
27
+ * convention. Written by hand, the string `'trix/hg38.ix'` is the whole entry,
28
+ * the same as `{ uri: 'trix/hg38.ix' }`: a `.ix` implies this type, and
29
+ * `assemblyNames` defaults to the track's own for a per-track index or to the
30
+ * config's one assembly.
31
+ * ```js
32
+ * {
33
+ * type: 'TrixTextSearchAdapter',
34
+ * uri: 'trix/hg38.ix',
35
+ * assemblyNames: ['hg38'],
36
+ * }
37
+ * ```
38
+ */
3
39
  const TrixTextSearchAdapter = ConfigurationSchema('TrixTextSearchAdapter', {
40
+ /**
41
+ * #slot
42
+ * location of the Trix `.ix` index written by `jbrowse text-index`: the
43
+ * sorted term-to-feature table the search box reads.
44
+ */
4
45
  ixFilePath: {
5
46
  type: 'fileLocation',
6
47
  defaultValue: {
@@ -8,6 +49,12 @@ const TrixTextSearchAdapter = ConfigurationSchema('TrixTextSearchAdapter', {
8
49
  locationType: 'UriLocation',
9
50
  },
10
51
  },
52
+ /**
53
+ * #slot
54
+ * location of the `.ixx` prefix index, which records where in the `.ix`
55
+ * each prefix begins, so a lookup takes a couple of range requests instead
56
+ * of a download of the whole index.
57
+ */
11
58
  ixxFilePath: {
12
59
  type: 'fileLocation',
13
60
  defaultValue: {
@@ -15,18 +62,9 @@ const TrixTextSearchAdapter = ConfigurationSchema('TrixTextSearchAdapter', {
15
62
  locationType: 'UriLocation',
16
63
  },
17
64
  },
18
- metaFilePath: {
19
- type: 'fileLocation',
20
- defaultValue: {
21
- uri: 'meta.json',
22
- locationType: 'UriLocation',
23
- },
24
- },
25
- tracks: {
26
- type: 'stringArray',
27
- defaultValue: [],
28
- description: 'List of tracks covered by text search adapter',
29
- },
65
+ /**
66
+ * #slot
67
+ */
30
68
  assemblyNames: {
31
69
  type: 'stringArray',
32
70
  defaultValue: [],
@@ -34,21 +72,21 @@ const TrixTextSearchAdapter = ConfigurationSchema('TrixTextSearchAdapter', {
34
72
  },
35
73
  }, {
36
74
  explicitlyTyped: true,
37
- explicitIdentifier: 'textSearchAdapterId',
38
- preProcessSnapshot: snap => {
39
- return snap.uri
40
- ? {
41
- ...snap,
42
- ixFilePath: {
43
- uri: snap.uri,
44
- baseUri: snap.baseUri,
45
- },
46
- ixxFilePath: {
47
- uri: `${snap.uri}x`,
48
- baseUri: snap.baseUri,
49
- },
50
- }
51
- : snap;
52
- },
75
+ /**
76
+ * #preProcessSnapshot
77
+ *
78
+ *
79
+ * preprocessor to allow minimal config: `uri` points at the `.ix` file and
80
+ * the sibling `.ixx` is derived from it (the `jbrowse text-index` naming
81
+ * convention):
82
+ * ```json
83
+ * {
84
+ * "type": "TrixTextSearchAdapter",
85
+ * "uri": "file.ix",
86
+ * "assemblyNames": ["hg19"]
87
+ * }
88
+ * ```
89
+ */
90
+ preProcessSnapshot: normalizeSnapshot,
53
91
  });
54
92
  export default TrixTextSearchAdapter;
package/esm/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import Plugin from '@jbrowse/core/Plugin';
2
2
  import TextSearchAdapterType from '@jbrowse/core/pluggableElementTypes/TextSearchAdapterType';
3
- import configSchema from "./TrixTextSearchAdapter/configSchema.js";
3
+ import configSchema from './TrixTextSearchAdapter/configSchema.js';
4
4
  export default class TrixPlugin extends Plugin {
5
5
  name = 'TrixPlugin';
6
6
  install(pluginManager) {
@@ -10,7 +10,7 @@ export default class TrixPlugin extends Plugin {
10
10
  displayName: 'Trix text search adapter',
11
11
  configSchema,
12
12
  description: 'Trix text search adapter',
13
- getAdapterClass: () => import("./TrixTextSearchAdapter/TrixTextSearchAdapter.js").then(d => d.default),
13
+ getAdapterClass: () => import('./TrixTextSearchAdapter/TrixTextSearchAdapter.js').then(d => d.default),
14
14
  });
15
15
  });
16
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-trix",
3
- "version": "4.3.0",
3
+ "version": "5.0.0-beta.10",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 trix text search adapter",
6
6
  "keywords": [
@@ -21,11 +21,11 @@
21
21
  "esm"
22
22
  ],
23
23
  "peerDependencies": {
24
- "react": ">=18.0.0"
24
+ "react": ">=19.0.0"
25
25
  },
26
26
  "dependencies": {
27
- "@gmod/trix": "^3.0.12",
28
- "@jbrowse/core": "^4.3.0"
27
+ "@gmod/trix": "^3.0.22",
28
+ "@jbrowse/core": "^5.0.0-beta.10"
29
29
  },
30
30
  "publishConfig": {
31
31
  "access": "public"
@@ -33,16 +33,13 @@
33
33
  "sideEffects": false,
34
34
  "scripts": {
35
35
  "build": "pnpm run /^build:/",
36
- "test": "cd ../..; jest --passWithNoTests plugins/trix",
36
+ "test": "jest --config ../../jest.config.js --passWithNoTests --testPathPatterns plugins/trix",
37
37
  "prebuild": "pnpm clean",
38
- "build:esm": "tsc -p tsconfig.build.esm.json",
38
+ "build:esm": "node ../../node_modules/typescript7/bin/tsc --build tsconfig.build.esm.json",
39
39
  "clean": "rimraf esm *.tsbuildinfo"
40
40
  },
41
41
  "types": "esm/index.d.ts",
42
42
  "exports": {
43
- ".": {
44
- "types": "./esm/index.d.ts",
45
- "import": "./esm/index.js"
46
- }
43
+ ".": "./esm/index.js"
47
44
  }
48
45
  }