@jbrowse/text-indexing-core 4.3.0 → 5.0.0-beta.2
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/esm/index.d.ts +7 -2
- package/esm/index.js +7 -4
- package/esm/indexFiles.d.ts +15 -0
- package/esm/indexFiles.js +55 -0
- package/esm/trixPaths.d.ts +34 -0
- package/esm/trixPaths.js +55 -0
- package/esm/types/common.d.ts +5 -5
- package/esm/types/common.js +89 -92
- package/esm/types/gff3Adapter.d.ts +20 -11
- package/esm/types/gff3Adapter.js +42 -8
- package/esm/types/gtfAdapter.d.ts +25 -0
- package/esm/types/gtfAdapter.js +149 -0
- package/esm/types/vcfAdapter.d.ts +2 -10
- package/esm/types/vcfAdapter.js +34 -13
- package/esm/util.d.ts +31 -0
- package/esm/util.js +18 -1
- package/package.json +3 -6
package/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
1
|
+
export { decodeURIComponentNoThrow, defaultAttributesToIndex, defaultFeatureTypesToExclude, indexableAdapters, isSupportedIndexingAdapter, } from './util.ts';
|
|
2
|
+
export type { Gff3IndexerOptions, Gff3Adapter, Gff3TabixAdapter, GtfAdapter, IndexableFormat, IndexerOptions, LocalPathLocation, Track, UriLocation, VcfAdapter, VcfTabixAdapter, } from './util.ts';
|
|
3
|
+
export { createReadlineInterface, generateMeta, getLocalOrRemoteStream, guessAdapterFromFileName, isURL, makeLocation, parseAttributes, } from './types/common.ts';
|
|
4
|
+
export { TRIX_DIR, sanitizeForFilename, trixFileNames, trixFilePaths, trixFileUris, } from './trixPaths.ts';
|
|
3
5
|
export { indexGff3 } from './types/gff3Adapter.ts';
|
|
6
|
+
export { indexGtf } from './types/gtfAdapter.ts';
|
|
4
7
|
export { indexVcf } from './types/vcfAdapter.ts';
|
|
8
|
+
export { indexFiles } from './indexFiles.ts';
|
|
9
|
+
export type { TrackIndexProgress } from './indexFiles.ts';
|
package/esm/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
1
|
+
export { decodeURIComponentNoThrow, defaultAttributesToIndex, defaultFeatureTypesToExclude, indexableAdapters, isSupportedIndexingAdapter, } from './util.js';
|
|
2
|
+
export { createReadlineInterface, generateMeta, getLocalOrRemoteStream, guessAdapterFromFileName, isURL, makeLocation, parseAttributes, } from './types/common.js';
|
|
3
|
+
export { TRIX_DIR, sanitizeForFilename, trixFileNames, trixFilePaths, trixFileUris, } from './trixPaths.js';
|
|
4
|
+
export { indexGff3 } from './types/gff3Adapter.js';
|
|
5
|
+
export { indexGtf } from './types/gtfAdapter.js';
|
|
6
|
+
export { indexVcf } from './types/vcfAdapter.js';
|
|
7
|
+
export { indexFiles } from './indexFiles.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Track } from './util.ts';
|
|
2
|
+
export interface TrackIndexProgress {
|
|
3
|
+
onStart?: (totalBytes: number) => void;
|
|
4
|
+
onUpdate?: (processedBytes: number) => void;
|
|
5
|
+
onDone?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function indexFiles({ tracks, attributesToIndex, outDir, featureTypesToExclude, featureTypesToInclude, makeProgress, checkAbort, }: {
|
|
8
|
+
tracks: Track[];
|
|
9
|
+
attributesToIndex: string[];
|
|
10
|
+
outDir: string;
|
|
11
|
+
featureTypesToExclude: string[];
|
|
12
|
+
featureTypesToInclude?: string[];
|
|
13
|
+
makeProgress?: (trackId: string) => TrackIndexProgress;
|
|
14
|
+
checkAbort?: () => void;
|
|
15
|
+
}): AsyncGenerator<string, void, unknown>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { indexGff3 } from './types/gff3Adapter.js';
|
|
2
|
+
import { indexGtf } from './types/gtfAdapter.js';
|
|
3
|
+
import { indexVcf } from './types/vcfAdapter.js';
|
|
4
|
+
import { indexableAdapters } from './util.js';
|
|
5
|
+
function getIndexingLocation(track, locationKey) {
|
|
6
|
+
const { adapter } = track;
|
|
7
|
+
// adapter[locationKey] is the canonical location object, e.g. gffGzLocation;
|
|
8
|
+
// fall back to the adapter itself to support the shorthand where a bare `uri`
|
|
9
|
+
// sits directly on the adapter
|
|
10
|
+
const loc = (adapter?.[locationKey] ?? adapter);
|
|
11
|
+
return loc?.locationType === 'LocalPathLocation' ? loc.localPath : loc?.uri;
|
|
12
|
+
}
|
|
13
|
+
const noop = () => { };
|
|
14
|
+
// shared generator that streams index records for a set of tracks. Dispatches
|
|
15
|
+
// to the gff3/vcf indexers based on adapter type; per-track attribute and
|
|
16
|
+
// exclude/include overrides from textSearching take precedence over the
|
|
17
|
+
// defaults.
|
|
18
|
+
export async function* indexFiles({ tracks, attributesToIndex, outDir, featureTypesToExclude, featureTypesToInclude, makeProgress, checkAbort, }) {
|
|
19
|
+
for (const track of tracks) {
|
|
20
|
+
checkAbort?.();
|
|
21
|
+
const { adapter, textSearching, trackId } = track;
|
|
22
|
+
const indexable = indexableAdapters[adapter?.type ?? ''];
|
|
23
|
+
const inLocation = indexable
|
|
24
|
+
? getIndexingLocation(track, indexable.locationKey)
|
|
25
|
+
: undefined;
|
|
26
|
+
if (indexable && inLocation) {
|
|
27
|
+
const progress = makeProgress?.(trackId);
|
|
28
|
+
const common = {
|
|
29
|
+
config: track,
|
|
30
|
+
attributesToIndex: textSearching?.indexingAttributes ?? attributesToIndex,
|
|
31
|
+
inLocation,
|
|
32
|
+
outDir,
|
|
33
|
+
onStart: progress?.onStart ?? noop,
|
|
34
|
+
onUpdate: progress?.onUpdate ?? noop,
|
|
35
|
+
checkAbort,
|
|
36
|
+
};
|
|
37
|
+
if (indexable.format === 'gff3') {
|
|
38
|
+
yield* indexGff3({
|
|
39
|
+
...common,
|
|
40
|
+
featureTypesToExclude: textSearching?.indexingFeatureTypesToExclude ??
|
|
41
|
+
featureTypesToExclude,
|
|
42
|
+
featureTypesToInclude: textSearching?.indexingFeatureTypesToInclude ??
|
|
43
|
+
featureTypesToInclude,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else if (indexable.format === 'gtf') {
|
|
47
|
+
yield* indexGtf(common);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
yield* indexVcf(common);
|
|
51
|
+
}
|
|
52
|
+
progress?.onDone?.();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Every trix artifact lands in this directory under the output directory. */
|
|
2
|
+
export declare const TRIX_DIR = "trix";
|
|
3
|
+
export declare function sanitizeForFilename(name: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* The three files an index called `name` writes, sanitized once.
|
|
6
|
+
*
|
|
7
|
+
* Everything that writes a trix artifact or points a config at one derives its
|
|
8
|
+
* paths from here. They used to sanitize independently and one of them didn't:
|
|
9
|
+
* the CLI wrote `.ix`/`.ixx` under the raw name while `generateMeta` and
|
|
10
|
+
* `createTrixAdapter` sanitized, so a trackId holding a `/` aimed the write at
|
|
11
|
+
* a `trix/` subdirectory that does not exist, and the rest of the
|
|
12
|
+
* Windows-invalid set wrote a file no search would look for.
|
|
13
|
+
*/
|
|
14
|
+
export declare function trixFileNames(name: string): {
|
|
15
|
+
ix: string;
|
|
16
|
+
ixx: string;
|
|
17
|
+
meta: string;
|
|
18
|
+
};
|
|
19
|
+
/** {@link trixFileNames} as filesystem paths under `outDir`, for a writer. */
|
|
20
|
+
export declare function trixFilePaths(outDir: string, name: string): {
|
|
21
|
+
ix: string;
|
|
22
|
+
ixx: string;
|
|
23
|
+
meta: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* {@link trixFileNames} as URIs relative to the config, for an adapter conf.
|
|
27
|
+
* Always `/`-joined — a URI is not a filesystem path, so this one does not go
|
|
28
|
+
* through `path.join`, which would emit `\` on Windows.
|
|
29
|
+
*/
|
|
30
|
+
export declare function trixFileUris(name: string): {
|
|
31
|
+
ix: string;
|
|
32
|
+
ixx: string;
|
|
33
|
+
meta: string;
|
|
34
|
+
};
|
package/esm/trixPaths.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/** Every trix artifact lands in this directory under the output directory. */
|
|
3
|
+
export const TRIX_DIR = 'trix';
|
|
4
|
+
// Windows reserves these device names even with an extension, so a track or
|
|
5
|
+
// assembly literally named e.g. NUL would otherwise yield an unusable NUL.ix
|
|
6
|
+
const windowsReservedName = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
7
|
+
// makes `name` safe as a filename on Windows: replaces the invalid characters
|
|
8
|
+
// \ / : * ? " < > |, drops trailing dots/spaces (Windows silently strips them),
|
|
9
|
+
// and escapes reserved device names
|
|
10
|
+
export function sanitizeForFilename(name) {
|
|
11
|
+
const cleaned = name
|
|
12
|
+
.replaceAll(/[\\/:*?"<>|]/g, '_')
|
|
13
|
+
.replace(/(?<![. ])[. ]+$/, '');
|
|
14
|
+
return windowsReservedName.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The three files an index called `name` writes, sanitized once.
|
|
18
|
+
*
|
|
19
|
+
* Everything that writes a trix artifact or points a config at one derives its
|
|
20
|
+
* paths from here. They used to sanitize independently and one of them didn't:
|
|
21
|
+
* the CLI wrote `.ix`/`.ixx` under the raw name while `generateMeta` and
|
|
22
|
+
* `createTrixAdapter` sanitized, so a trackId holding a `/` aimed the write at
|
|
23
|
+
* a `trix/` subdirectory that does not exist, and the rest of the
|
|
24
|
+
* Windows-invalid set wrote a file no search would look for.
|
|
25
|
+
*/
|
|
26
|
+
export function trixFileNames(name) {
|
|
27
|
+
const safeName = sanitizeForFilename(name);
|
|
28
|
+
return {
|
|
29
|
+
ix: `${safeName}.ix`,
|
|
30
|
+
ixx: `${safeName}.ixx`,
|
|
31
|
+
meta: `${safeName}_meta.json`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** {@link trixFileNames} as filesystem paths under `outDir`, for a writer. */
|
|
35
|
+
export function trixFilePaths(outDir, name) {
|
|
36
|
+
const names = trixFileNames(name);
|
|
37
|
+
return {
|
|
38
|
+
ix: path.join(outDir, TRIX_DIR, names.ix),
|
|
39
|
+
ixx: path.join(outDir, TRIX_DIR, names.ixx),
|
|
40
|
+
meta: path.join(outDir, TRIX_DIR, names.meta),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* {@link trixFileNames} as URIs relative to the config, for an adapter conf.
|
|
45
|
+
* Always `/`-joined — a URI is not a filesystem path, so this one does not go
|
|
46
|
+
* through `path.join`, which would emit `\` on Windows.
|
|
47
|
+
*/
|
|
48
|
+
export function trixFileUris(name) {
|
|
49
|
+
const names = trixFileNames(name);
|
|
50
|
+
return {
|
|
51
|
+
ix: `${TRIX_DIR}/${names.ix}`,
|
|
52
|
+
ixx: `${TRIX_DIR}/${names.ixx}`,
|
|
53
|
+
meta: `${TRIX_DIR}/${names.meta}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
package/esm/types/common.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import readline from 'readline';
|
|
2
|
-
import { Readable } from 'stream';
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
3
|
import type { LocalPathLocation, Track, UriLocation } from '../util.ts';
|
|
4
|
-
export declare function isURL(
|
|
4
|
+
export declare function isURL(fileName: string): boolean;
|
|
5
5
|
export declare function getLocalOrRemoteStream({ file, out, onStart, onUpdate, }: {
|
|
6
6
|
file: string;
|
|
7
7
|
out: string;
|
|
@@ -12,12 +12,12 @@ export declare function createReadlineInterface(stream: Readable, inLocation: st
|
|
|
12
12
|
export declare function parseAttributes(infoString: string, decodeFunc: (s: string) => string): Record<string, string | undefined>;
|
|
13
13
|
export declare function makeLocation(location: string, protocol: string): UriLocation | LocalPathLocation;
|
|
14
14
|
export declare function guessAdapterFromFileName(filePath: string): Track;
|
|
15
|
-
export declare function
|
|
16
|
-
export declare function generateMeta({ configs, attributesToIndex, outDir, name, featureTypesToExclude, assemblyNames, }: {
|
|
15
|
+
export declare function generateMeta({ configs, attributesToIndex, outDir, name, featureTypesToExclude, featureTypesToInclude, assemblyNames, }: {
|
|
17
16
|
configs: Track[];
|
|
18
17
|
attributesToIndex: string[];
|
|
19
18
|
outDir: string;
|
|
20
19
|
name: string;
|
|
21
20
|
featureTypesToExclude: string[];
|
|
21
|
+
featureTypesToInclude?: string[];
|
|
22
22
|
assemblyNames: string[];
|
|
23
23
|
}): void;
|
package/esm/types/common.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { createGunzip } from 'node:zlib';
|
|
8
|
+
import { trixFilePaths } from '../trixPaths.js';
|
|
9
|
+
import { indexableAdapters } from '../util.js';
|
|
10
|
+
export function isURL(fileName) {
|
|
9
11
|
try {
|
|
10
|
-
url = new URL(
|
|
12
|
+
const url = new URL(fileName);
|
|
13
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
11
14
|
}
|
|
12
|
-
catch
|
|
15
|
+
catch {
|
|
13
16
|
return false;
|
|
14
17
|
}
|
|
15
|
-
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
16
18
|
}
|
|
17
19
|
function convertFileUrlToPath(fileUrl) {
|
|
18
20
|
try {
|
|
@@ -22,9 +24,49 @@ function convertFileUrlToPath(fileUrl) {
|
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
catch {
|
|
27
|
+
// not a valid URL
|
|
25
28
|
}
|
|
26
29
|
return undefined;
|
|
27
30
|
}
|
|
31
|
+
// Turn a WHATWG ReadableStream into a node Readable by driving its reader.
|
|
32
|
+
//
|
|
33
|
+
// DO NOT replace this with `Readable.fromWeb(body)`. It looks equivalent and
|
|
34
|
+
// passes tests (undici's fetch body IS a node:stream/web stream), but the
|
|
35
|
+
// desktop indexing worker runs on Chromium's global fetch, whose body is a DOM
|
|
36
|
+
// ReadableStream from a different realm. `Readable.fromWeb`'s internal
|
|
37
|
+
// `instanceof` check rejects it with the baffling "must be an instance of
|
|
38
|
+
// ReadableStream. Received an instance of ReadableStream", and remote (URL)
|
|
39
|
+
// track indexing breaks. `getReader()` is standard on every WHATWG stream
|
|
40
|
+
// regardless of realm, so this works everywhere. See common.test.ts
|
|
41
|
+
// "foreign realm" regression test.
|
|
42
|
+
function webStreamToNodeReadable(reader) {
|
|
43
|
+
return new Readable({
|
|
44
|
+
read() {
|
|
45
|
+
reader
|
|
46
|
+
.read()
|
|
47
|
+
.then(({ done, value }) => {
|
|
48
|
+
this.push(done ? null : Buffer.from(value));
|
|
49
|
+
})
|
|
50
|
+
.catch((e) => {
|
|
51
|
+
this.destroy(e);
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
// Cancel the source when the Readable is destroyed early (e.g. an aborted
|
|
55
|
+
// index) so the underlying fetch connection is released rather than left
|
|
56
|
+
// hanging — the cleanup Readable.fromWeb would do for us if it accepted
|
|
57
|
+
// this stream.
|
|
58
|
+
destroy(error, callback) {
|
|
59
|
+
reader
|
|
60
|
+
.cancel(error ?? undefined)
|
|
61
|
+
.then(() => {
|
|
62
|
+
callback(error);
|
|
63
|
+
})
|
|
64
|
+
.catch(() => {
|
|
65
|
+
callback(error);
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
28
70
|
export async function getLocalOrRemoteStream({ file, out, onStart, onUpdate, }) {
|
|
29
71
|
let receivedBytes = 0;
|
|
30
72
|
if (isURL(file)) {
|
|
@@ -38,19 +80,12 @@ export async function getLocalOrRemoteStream({ file, out, onStart, onUpdate, })
|
|
|
38
80
|
if (!body) {
|
|
39
81
|
throw new Error(`Failed to fetch ${file}: no response body`);
|
|
40
82
|
}
|
|
83
|
+
// node-fetch gives a node Readable directly; global fetch gives a web
|
|
84
|
+
// ReadableStream we adapt (see webStreamToNodeReadable — do not swap in
|
|
85
|
+
// Readable.fromWeb here).
|
|
41
86
|
const nodeStream = body instanceof Readable
|
|
42
87
|
? body
|
|
43
|
-
:
|
|
44
|
-
async read() {
|
|
45
|
-
const reader = body.getReader();
|
|
46
|
-
let result = await reader.read();
|
|
47
|
-
while (!result.done) {
|
|
48
|
-
this.push(result.value);
|
|
49
|
-
result = await reader.read();
|
|
50
|
-
}
|
|
51
|
-
this.push(null);
|
|
52
|
-
},
|
|
53
|
-
});
|
|
88
|
+
: webStreamToNodeReadable(body.getReader());
|
|
54
89
|
nodeStream.on('data', chunk => {
|
|
55
90
|
receivedBytes += chunk.length;
|
|
56
91
|
onUpdate(receivedBytes);
|
|
@@ -58,6 +93,7 @@ export async function getLocalOrRemoteStream({ file, out, onStart, onUpdate, })
|
|
|
58
93
|
return nodeStream;
|
|
59
94
|
}
|
|
60
95
|
else {
|
|
96
|
+
// Handle file:// URLs by converting to local path
|
|
61
97
|
const localPath = convertFileUrlToPath(file) ?? file;
|
|
62
98
|
const filename = path.isAbsolute(localPath)
|
|
63
99
|
? localPath
|
|
@@ -73,19 +109,10 @@ export async function getLocalOrRemoteStream({ file, out, onStart, onUpdate, })
|
|
|
73
109
|
}
|
|
74
110
|
}
|
|
75
111
|
export function createReadlineInterface(stream, inLocation) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
inputStream = stream.pipe(gunzip);
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
inputStream = stream;
|
|
84
|
-
}
|
|
85
|
-
const rl = readline.createInterface({
|
|
86
|
-
input: inputStream,
|
|
87
|
-
});
|
|
88
|
-
return rl;
|
|
112
|
+
const inputStream = /\.b?gz$/i.test(inLocation)
|
|
113
|
+
? stream.pipe(createGunzip())
|
|
114
|
+
: stream;
|
|
115
|
+
return readline.createInterface({ input: inputStream });
|
|
89
116
|
}
|
|
90
117
|
export function parseAttributes(infoString, decodeFunc) {
|
|
91
118
|
const result = {};
|
|
@@ -114,61 +141,33 @@ export function makeLocation(location, protocol) {
|
|
|
114
141
|
}
|
|
115
142
|
throw new Error(`invalid protocol ${protocol}`);
|
|
116
143
|
}
|
|
144
|
+
// ordered most-specific-first so e.g. `.vcf.gz` matches before `.vcf`.
|
|
145
|
+
//
|
|
146
|
+
// `.gtf.gz` maps to GtfAdapter rather than GtfTabixAdapter on purpose: this
|
|
147
|
+
// guesser only invents a throwaway track for `text-index --file`, which streams
|
|
148
|
+
// the file start to finish and never opens the .tbi, and GtfAdapter's
|
|
149
|
+
// gtfLocation takes a gzipped path (the reader gunzips on the suffix). A track
|
|
150
|
+
// in a config.json is the other case — `jbrowse add-track` gives a bgzipped GTF
|
|
151
|
+
// a GtfTabixAdapter, which is indexable too, from its own gtfGzLocation.
|
|
152
|
+
const adapterGuesses = [
|
|
153
|
+
{ regex: /\.vcf\.b?gz$/i, type: 'VcfTabixAdapter' },
|
|
154
|
+
{ regex: /\.gff3?\.b?gz$/i, type: 'Gff3TabixAdapter' },
|
|
155
|
+
{ regex: /\.gtf(\.b?gz)?$/i, type: 'GtfAdapter' },
|
|
156
|
+
{ regex: /\.vcf$/i, type: 'VcfAdapter' },
|
|
157
|
+
{ regex: /\.gff3?$/i, type: 'Gff3Adapter' },
|
|
158
|
+
];
|
|
117
159
|
export function guessAdapterFromFileName(filePath) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
trackId: name,
|
|
123
|
-
name: name,
|
|
124
|
-
assemblyNames: [],
|
|
125
|
-
adapter: {
|
|
126
|
-
type: 'VcfTabixAdapter',
|
|
127
|
-
vcfGzLocation: makeLocation(filePath, protocol),
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
else if (/\.gff3?\.b?gz$/i.test(filePath)) {
|
|
132
|
-
return {
|
|
133
|
-
trackId: name,
|
|
134
|
-
name,
|
|
135
|
-
assemblyNames: [],
|
|
136
|
-
adapter: {
|
|
137
|
-
type: 'Gff3TabixAdapter',
|
|
138
|
-
gffGzLocation: makeLocation(filePath, protocol),
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
else if (/\.gtf?$/i.test(filePath)) {
|
|
160
|
+
const guess = adapterGuesses.find(g => g.regex.test(filePath));
|
|
161
|
+
if (guess) {
|
|
162
|
+
const name = path.basename(filePath);
|
|
163
|
+
const protocol = isURL(filePath) ? 'uri' : 'localPath';
|
|
143
164
|
return {
|
|
144
165
|
trackId: name,
|
|
145
166
|
name,
|
|
146
167
|
assemblyNames: [],
|
|
147
168
|
adapter: {
|
|
148
|
-
type:
|
|
149
|
-
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
else if (/\.vcf$/i.test(filePath)) {
|
|
154
|
-
return {
|
|
155
|
-
trackId: name,
|
|
156
|
-
name,
|
|
157
|
-
assemblyNames: [],
|
|
158
|
-
adapter: {
|
|
159
|
-
type: 'VcfAdapter',
|
|
160
|
-
vcfLocation: makeLocation(filePath, protocol),
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
else if (/\.gff3?$/i.test(filePath)) {
|
|
165
|
-
return {
|
|
166
|
-
trackId: name,
|
|
167
|
-
name,
|
|
168
|
-
assemblyNames: [],
|
|
169
|
-
adapter: {
|
|
170
|
-
type: 'Gff3Adapter',
|
|
171
|
-
gffLocation: makeLocation(filePath, protocol),
|
|
169
|
+
type: guess.type,
|
|
170
|
+
[indexableAdapters[guess.type].locationKey]: makeLocation(filePath, protocol),
|
|
172
171
|
},
|
|
173
172
|
};
|
|
174
173
|
}
|
|
@@ -176,20 +175,18 @@ export function guessAdapterFromFileName(filePath) {
|
|
|
176
175
|
throw new Error(`Unsupported file type ${filePath}`);
|
|
177
176
|
}
|
|
178
177
|
}
|
|
179
|
-
export function
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
export function generateMeta({ configs, attributesToIndex, outDir, name, featureTypesToExclude, assemblyNames, }) {
|
|
183
|
-
const safeName = sanitizeForFilename(name);
|
|
184
|
-
fs.writeFileSync(path.join(outDir, 'trix', `${safeName}_meta.json`), JSON.stringify({
|
|
178
|
+
export function generateMeta({ configs, attributesToIndex, outDir, name, featureTypesToExclude, featureTypesToInclude, assemblyNames, }) {
|
|
179
|
+
fs.writeFileSync(trixFilePaths(outDir, name).meta, JSON.stringify({
|
|
185
180
|
dateCreated: new Date().toISOString(),
|
|
186
181
|
tracks: configs.map(config => {
|
|
187
182
|
const { trackId, textSearching, adapter } = config;
|
|
188
183
|
return {
|
|
189
184
|
trackId,
|
|
190
|
-
attributesIndexed: textSearching?.indexingAttributes
|
|
191
|
-
excludedTypes: textSearching?.indexingFeatureTypesToExclude
|
|
185
|
+
attributesIndexed: textSearching?.indexingAttributes ?? attributesToIndex,
|
|
186
|
+
excludedTypes: textSearching?.indexingFeatureTypesToExclude ??
|
|
192
187
|
featureTypesToExclude,
|
|
188
|
+
includedTypes: textSearching?.indexingFeatureTypesToInclude ??
|
|
189
|
+
featureTypesToInclude,
|
|
193
190
|
adapterConf: adapter,
|
|
194
191
|
};
|
|
195
192
|
}),
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import type { Gff3IndexerOptions } from '../util.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Stream one GFF3 into trix records, one line per indexed feature.
|
|
4
|
+
*
|
|
5
|
+
* Two type gates, and they answer different questions. `featureTypesToExclude`
|
|
6
|
+
* names types this file is known to carry and nobody would search for — it is
|
|
7
|
+
* the right shape when you own the vocabulary, which is why the default is
|
|
8
|
+
* `exon,CDS`. `featureTypesToInclude` names the types worth indexing and drops
|
|
9
|
+
* everything else, which is the only shape that stays correct against a
|
|
10
|
+
* vocabulary someone else controls: an NCBI RefSeq GFF3 draws from 115 feature
|
|
11
|
+
* types, 80 of which are leaf regulatory/repeat/alignment records carrying no
|
|
12
|
+
* name (a `match` labels itself with a bare UUID, a `cDNA_match` with an MD5,
|
|
13
|
+
* every `biological_region` with the literal string "biological region"), and a
|
|
14
|
+
* deny list written against the types seen so far leaks the next one NCBI adds.
|
|
15
|
+
* The allow list for that file is ~33 names — gene, pseudogene, and the
|
|
16
|
+
* transcript-level types — and does not grow.
|
|
17
|
+
*
|
|
18
|
+
* Both apply when both are given: include admits, exclude then narrows.
|
|
19
|
+
*/
|
|
20
|
+
export declare function indexGff3({ config, attributesToIndex, inLocation, outDir, featureTypesToExclude, featureTypesToInclude, onStart, onUpdate, checkAbort, }: Gff3IndexerOptions): AsyncGenerator<string, void, unknown>;
|
package/esm/types/gff3Adapter.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
import { decodeURIComponentNoThrow } from
|
|
2
|
-
import { createReadlineInterface, getLocalOrRemoteStream, parseAttributes, } from
|
|
3
|
-
|
|
1
|
+
import { decodeURIComponentNoThrow } from '../util.js';
|
|
2
|
+
import { createReadlineInterface, getLocalOrRemoteStream, parseAttributes, } from './common.js';
|
|
3
|
+
/**
|
|
4
|
+
* Stream one GFF3 into trix records, one line per indexed feature.
|
|
5
|
+
*
|
|
6
|
+
* Two type gates, and they answer different questions. `featureTypesToExclude`
|
|
7
|
+
* names types this file is known to carry and nobody would search for — it is
|
|
8
|
+
* the right shape when you own the vocabulary, which is why the default is
|
|
9
|
+
* `exon,CDS`. `featureTypesToInclude` names the types worth indexing and drops
|
|
10
|
+
* everything else, which is the only shape that stays correct against a
|
|
11
|
+
* vocabulary someone else controls: an NCBI RefSeq GFF3 draws from 115 feature
|
|
12
|
+
* types, 80 of which are leaf regulatory/repeat/alignment records carrying no
|
|
13
|
+
* name (a `match` labels itself with a bare UUID, a `cDNA_match` with an MD5,
|
|
14
|
+
* every `biological_region` with the literal string "biological region"), and a
|
|
15
|
+
* deny list written against the types seen so far leaks the next one NCBI adds.
|
|
16
|
+
* The allow list for that file is ~33 names — gene, pseudogene, and the
|
|
17
|
+
* transcript-level types — and does not grow.
|
|
18
|
+
*
|
|
19
|
+
* Both apply when both are given: include admits, exclude then narrows.
|
|
20
|
+
*/
|
|
21
|
+
export async function* indexGff3({ config, attributesToIndex, inLocation, outDir, featureTypesToExclude, featureTypesToInclude, onStart, onUpdate, checkAbort, }) {
|
|
4
22
|
const { trackId } = config;
|
|
5
23
|
const stream = await getLocalOrRemoteStream({
|
|
6
24
|
file: inLocation,
|
|
@@ -10,19 +28,35 @@ export async function* indexGff3({ config, attributesToIndex, inLocation, outDir
|
|
|
10
28
|
});
|
|
11
29
|
const rl = createReadlineInterface(stream, inLocation);
|
|
12
30
|
const excludeSet = new Set(featureTypesToExclude);
|
|
31
|
+
// Absent rather than empty, so an empty list means "no allow list" and not
|
|
32
|
+
// "index nothing" — a caller that reads the list out of a config gets the
|
|
33
|
+
// former when the slot is unset, and silently indexing zero features would
|
|
34
|
+
// look exactly like a successful run.
|
|
35
|
+
const includeSet = featureTypesToInclude?.length
|
|
36
|
+
? new Set(featureTypesToInclude)
|
|
37
|
+
: undefined;
|
|
13
38
|
const encodedTrackId = encodeURIComponent(trackId);
|
|
14
39
|
for await (const line of rl) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
else if (line.startsWith('#')) {
|
|
40
|
+
checkAbort?.();
|
|
41
|
+
if (!line.trim() || line.startsWith('#')) {
|
|
19
42
|
continue;
|
|
20
43
|
}
|
|
21
44
|
else if (line.startsWith('>')) {
|
|
22
45
|
break;
|
|
23
46
|
}
|
|
24
47
|
const [seq_id, , type, start, end, , , , col9] = line.split('\t');
|
|
25
|
-
|
|
48
|
+
// a valid GFF3 feature row has 9 tab-delimited columns; skip malformed or
|
|
49
|
+
// truncated lines that would otherwise make parseAttributes throw on an
|
|
50
|
+
// undefined column 9 (mirrors the guard in the vcf indexer)
|
|
51
|
+
if (seq_id === undefined ||
|
|
52
|
+
type === undefined ||
|
|
53
|
+
start === undefined ||
|
|
54
|
+
end === undefined ||
|
|
55
|
+
col9 === undefined) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!excludeSet.has(type) &&
|
|
59
|
+
(includeSet === undefined || includeSet.has(type))) {
|
|
26
60
|
const col9attrs = parseAttributes(col9, decodeURIComponentNoThrow);
|
|
27
61
|
const attrs = attributesToIndex
|
|
28
62
|
.map(attr => col9attrs[attr])
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IndexerOptions } from '../util.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Parse the GTF 9th column (`gene_id "X"; transcript_id "Y";`), mirroring
|
|
4
|
+
* plugins/gtf so the indexed text matches what the track displays: entries are
|
|
5
|
+
* `key value` separated by a space (not `key=value`), values may be quoted, a
|
|
6
|
+
* repeated key expresses multiple values, and a quoted value may contain the
|
|
7
|
+
* ';' the entries are split on (`note "a; b"`), so its pieces are rejoined
|
|
8
|
+
* before the value is read.
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseGtfAttributes(attrString: string): Record<string, string[]>;
|
|
11
|
+
/**
|
|
12
|
+
* GTF, unlike GFF3, has no gene or transcript rows: a gene exists only as the
|
|
13
|
+
* exon/CDS/UTR rows repeating its `gene_id`. Indexing per row would point every
|
|
14
|
+
* hit at a single exon, so rows are grouped by `gene_id` and `transcript_id`
|
|
15
|
+
* and each group is emitted once spanning all of its rows.
|
|
16
|
+
*
|
|
17
|
+
* Grouping is what keeps one entry per gene rather than one per exon, so
|
|
18
|
+
* `featureTypesToExclude` does not apply here — dropping rows would only
|
|
19
|
+
* truncate the spans.
|
|
20
|
+
*
|
|
21
|
+
* The group map is held until EOF because GTF rows for one locus are not
|
|
22
|
+
* guaranteed contiguous. It is keyed by gene/transcript id, so it stays far
|
|
23
|
+
* smaller than the whole-file parse plugins/gtf already does to display a GTF.
|
|
24
|
+
*/
|
|
25
|
+
export declare function indexGtf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, checkAbort, }: IndexerOptions): AsyncGenerator<string, void, unknown>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createReadlineInterface, getLocalOrRemoteStream } from './common.js';
|
|
2
|
+
// GFF3-style attribute names have no GTF equivalent, so the shared defaults
|
|
3
|
+
// (`Name`, `ID`, `symbol`) would index nothing. Each requested name also pulls
|
|
4
|
+
// in its GTF spellings, so `jbrowse text-index` works on a GTF out of the box
|
|
5
|
+
// while an explicit `--attributes gene_biotype` still works verbatim.
|
|
6
|
+
const gtfAttributeAliases = {
|
|
7
|
+
Name: ['gene_name', 'transcript_name'],
|
|
8
|
+
ID: ['gene_id', 'transcript_id'],
|
|
9
|
+
symbol: ['gene_name'],
|
|
10
|
+
};
|
|
11
|
+
function expandAttributeNames(attributesToIndex) {
|
|
12
|
+
return [
|
|
13
|
+
...new Set(attributesToIndex.flatMap(a => [a, ...(gtfAttributeAliases[a] ?? [])])),
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* True when an entry opens a double quote it never closes, meaning the ';' it
|
|
18
|
+
* was split on fell inside the value instead of ending the entry.
|
|
19
|
+
*/
|
|
20
|
+
function hasUnclosedQuote(entry) {
|
|
21
|
+
const first = entry.indexOf('"');
|
|
22
|
+
return first !== -1 && !entry.includes('"', first + 1);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse the GTF 9th column (`gene_id "X"; transcript_id "Y";`), mirroring
|
|
26
|
+
* plugins/gtf so the indexed text matches what the track displays: entries are
|
|
27
|
+
* `key value` separated by a space (not `key=value`), values may be quoted, a
|
|
28
|
+
* repeated key expresses multiple values, and a quoted value may contain the
|
|
29
|
+
* ';' the entries are split on (`note "a; b"`), so its pieces are rejoined
|
|
30
|
+
* before the value is read.
|
|
31
|
+
*/
|
|
32
|
+
export function parseGtfAttributes(attrString) {
|
|
33
|
+
const attrs = {};
|
|
34
|
+
const entries = attrString.split(';');
|
|
35
|
+
for (let i = 0; i < entries.length; i++) {
|
|
36
|
+
let entry = entries[i];
|
|
37
|
+
while (hasUnclosedQuote(entry) && i + 1 < entries.length) {
|
|
38
|
+
entry += `;${entries[++i]}`;
|
|
39
|
+
}
|
|
40
|
+
const trimmed = entry.trim();
|
|
41
|
+
const sp = trimmed.indexOf(' ');
|
|
42
|
+
if (sp !== -1) {
|
|
43
|
+
const key = trimmed.slice(0, sp);
|
|
44
|
+
const value = trimmed
|
|
45
|
+
.slice(sp + 1)
|
|
46
|
+
.trim()
|
|
47
|
+
.replaceAll(/^"|"$/g, '')
|
|
48
|
+
// a comma separates record ids in the trix .ix format, so it can never
|
|
49
|
+
// survive into an indexed word
|
|
50
|
+
.replaceAll(',', ' ');
|
|
51
|
+
if (value) {
|
|
52
|
+
;
|
|
53
|
+
(attrs[key] ??= []).push(value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return attrs;
|
|
58
|
+
}
|
|
59
|
+
// composite map keys join refName + id; a separator that cannot occur in
|
|
60
|
+
// either keeps two loci from colliding
|
|
61
|
+
const SEP = '\u0000';
|
|
62
|
+
/**
|
|
63
|
+
* GTF, unlike GFF3, has no gene or transcript rows: a gene exists only as the
|
|
64
|
+
* exon/CDS/UTR rows repeating its `gene_id`. Indexing per row would point every
|
|
65
|
+
* hit at a single exon, so rows are grouped by `gene_id` and `transcript_id`
|
|
66
|
+
* and each group is emitted once spanning all of its rows.
|
|
67
|
+
*
|
|
68
|
+
* Grouping is what keeps one entry per gene rather than one per exon, so
|
|
69
|
+
* `featureTypesToExclude` does not apply here — dropping rows would only
|
|
70
|
+
* truncate the spans.
|
|
71
|
+
*
|
|
72
|
+
* The group map is held until EOF because GTF rows for one locus are not
|
|
73
|
+
* guaranteed contiguous. It is keyed by gene/transcript id, so it stays far
|
|
74
|
+
* smaller than the whole-file parse plugins/gtf already does to display a GTF.
|
|
75
|
+
*/
|
|
76
|
+
export async function* indexGtf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, checkAbort, }) {
|
|
77
|
+
const { trackId } = config;
|
|
78
|
+
const stream = await getLocalOrRemoteStream({
|
|
79
|
+
file: inLocation,
|
|
80
|
+
out: outDir,
|
|
81
|
+
onStart,
|
|
82
|
+
onUpdate,
|
|
83
|
+
});
|
|
84
|
+
const rl = createReadlineInterface(stream, inLocation);
|
|
85
|
+
const encodedTrackId = encodeURIComponent(trackId);
|
|
86
|
+
const attributeNames = expandAttributeNames(attributesToIndex);
|
|
87
|
+
const groups = new Map();
|
|
88
|
+
function accumulate(key, refName, start, end, values) {
|
|
89
|
+
if (values.length > 0) {
|
|
90
|
+
const group = groups.get(key);
|
|
91
|
+
if (group) {
|
|
92
|
+
group.start = Math.min(group.start, start);
|
|
93
|
+
group.end = Math.max(group.end, end);
|
|
94
|
+
for (const value of values) {
|
|
95
|
+
group.attrs.add(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
groups.set(key, { refName, start, end, attrs: new Set(values) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for await (const line of rl) {
|
|
104
|
+
checkAbort?.();
|
|
105
|
+
if (line.trim() && !line.startsWith('#')) {
|
|
106
|
+
const [seq_id, , , start, end, , , , col9] = line.split('\t');
|
|
107
|
+
if (seq_id !== undefined &&
|
|
108
|
+
start !== undefined &&
|
|
109
|
+
end !== undefined &&
|
|
110
|
+
col9 !== undefined) {
|
|
111
|
+
const parsed = parseGtfAttributes(col9);
|
|
112
|
+
const geneId = parsed.gene_id?.[0];
|
|
113
|
+
const transcriptId = parsed.transcript_id?.[0];
|
|
114
|
+
const s = Number(start);
|
|
115
|
+
const e = Number(end);
|
|
116
|
+
// `gene_*` names describe the gene, everything else the transcript, so
|
|
117
|
+
// a gene entry is labeled by its gene_name rather than by whichever
|
|
118
|
+
// transcript happened to come first
|
|
119
|
+
const geneValues = [];
|
|
120
|
+
const otherValues = [];
|
|
121
|
+
for (const name of attributeNames) {
|
|
122
|
+
for (const value of parsed[name] ?? []) {
|
|
123
|
+
;
|
|
124
|
+
(name.startsWith('gene') ? geneValues : otherValues).push(value);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (transcriptId === undefined) {
|
|
128
|
+
const values = [...geneValues, ...otherValues];
|
|
129
|
+
// no id to group on: merge rows that carry identical values, which
|
|
130
|
+
// still collapses the repeated rows of one feature into one entry
|
|
131
|
+
const key = geneId ?? values.join(SEP);
|
|
132
|
+
accumulate(`gene${SEP}${seq_id}${SEP}${key}`, seq_id, s, e, values);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
accumulate(`tx${SEP}${seq_id}${SEP}${transcriptId}`, seq_id, s, e, otherValues);
|
|
136
|
+
if (geneId !== undefined) {
|
|
137
|
+
accumulate(`gene${SEP}${seq_id}${SEP}${geneId}`, seq_id, s, e, geneValues);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const { refName, start, end, attrs } of groups.values()) {
|
|
144
|
+
const locStr = `${refName}:${start}..${end}`;
|
|
145
|
+
const encodedAttrs = [...attrs].map(a => `"${encodeURIComponent(a)}"`);
|
|
146
|
+
const record = `["${encodeURIComponent(locStr)}"|"${encodedTrackId}"|${encodedAttrs.join('|')}]`;
|
|
147
|
+
yield `${record} ${[...attrs].join(' ')}\n`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
trackId: string;
|
|
4
|
-
};
|
|
5
|
-
attributesToIndex: string[];
|
|
6
|
-
inLocation: string;
|
|
7
|
-
outDir: string;
|
|
8
|
-
onStart: (totalBytes: number) => void;
|
|
9
|
-
onUpdate: (progressBytes: number) => void;
|
|
10
|
-
}): AsyncGenerator<string, void, unknown>;
|
|
1
|
+
import type { IndexerOptions } from '../util.ts';
|
|
2
|
+
export declare function indexVcf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, checkAbort, }: IndexerOptions): AsyncGenerator<string, void, unknown>;
|
package/esm/types/vcfAdapter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { decodeURIComponentNoThrow } from
|
|
2
|
-
import { createReadlineInterface, getLocalOrRemoteStream, parseAttributes, } from
|
|
3
|
-
export async function* indexVcf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, }) {
|
|
1
|
+
import { decodeURIComponentNoThrow } from '../util.js';
|
|
2
|
+
import { createReadlineInterface, getLocalOrRemoteStream, parseAttributes, } from './common.js';
|
|
3
|
+
export async function* indexVcf({ config, attributesToIndex, inLocation, outDir, onStart, onUpdate, checkAbort, }) {
|
|
4
4
|
const { trackId } = config;
|
|
5
5
|
const stream = await getLocalOrRemoteStream({
|
|
6
6
|
file: inLocation,
|
|
@@ -11,25 +11,46 @@ export async function* indexVcf({ config, attributesToIndex, inLocation, outDir,
|
|
|
11
11
|
const rl = createReadlineInterface(stream, inLocation);
|
|
12
12
|
const encodedTrackId = encodeURIComponent(trackId);
|
|
13
13
|
for await (const line of rl) {
|
|
14
|
-
|
|
14
|
+
checkAbort?.();
|
|
15
|
+
if (!line.trim() || line.startsWith('#')) {
|
|
15
16
|
continue;
|
|
16
17
|
}
|
|
17
|
-
const [ref, pos, id,
|
|
18
|
-
|
|
18
|
+
const [ref, pos, id, , , , , info] = line.split('\t');
|
|
19
|
+
// a valid VCF data row has at least 8 tab-delimited columns; skip blank or
|
|
20
|
+
// malformed/short lines that would otherwise emit garbage records or throw
|
|
21
|
+
if (ref === undefined ||
|
|
22
|
+
pos === undefined ||
|
|
23
|
+
id === undefined ||
|
|
24
|
+
info === undefined) {
|
|
19
25
|
continue;
|
|
20
26
|
}
|
|
21
27
|
const fields = parseAttributes(info, decodeURIComponentNoThrow);
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
// an empty `END=` parses to 0 via Number(''), which is finite; require a
|
|
29
|
+
// non-empty value before trusting it, else fall back to pos+1
|
|
30
|
+
const endNum = Number(fields.END);
|
|
31
|
+
const end = fields.END && Number.isFinite(endNum) ? endNum : Number(pos) + 1;
|
|
32
|
+
const locStr = `${ref}:${pos}..${end}`;
|
|
24
33
|
const encodedLocStr = encodeURIComponent(locStr);
|
|
25
34
|
const infoAttrs = attributesToIndex
|
|
26
35
|
.map(attr => fields[attr])
|
|
27
36
|
.filter((f) => !!f);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
// VCF separates multiple IDs with ';' (matching @gmod/vcf), not ','. '.' is
|
|
38
|
+
// the no-id sentinel; such a row is still worth indexing by its INFO
|
|
39
|
+
// attributes, it just has no id to search by
|
|
40
|
+
const variantIds = id === '.' ? [] : id.split(';').filter(Boolean);
|
|
41
|
+
// one record per id so each is separately searchable, plus an id-less
|
|
42
|
+
// record when the row carries only attributes
|
|
43
|
+
const attrGroups = variantIds.length
|
|
44
|
+
? variantIds.map(variantId => [variantId, ...infoAttrs])
|
|
45
|
+
: [infoAttrs];
|
|
46
|
+
for (const attrs of attrGroups) {
|
|
47
|
+
if (attrs.length > 0) {
|
|
48
|
+
// attributes go in the record so the adapter can display them, and in
|
|
49
|
+
// the trailing word list so trix indexes them as searchable terms
|
|
50
|
+
const encodedAttrs = attrs.map(a => `"${encodeURIComponent(a)}"`);
|
|
51
|
+
const record = `["${encodedLocStr}"|"${encodedTrackId}"|${encodedAttrs.join('|')}]`;
|
|
52
|
+
yield `${record} ${[...new Set(attrs)].join(' ')}\n`;
|
|
53
|
+
}
|
|
33
54
|
}
|
|
34
55
|
}
|
|
35
56
|
}
|
package/esm/util.d.ts
CHANGED
|
@@ -28,18 +28,49 @@ export interface VcfAdapter {
|
|
|
28
28
|
vcfLocation: Loc;
|
|
29
29
|
}
|
|
30
30
|
export interface Track {
|
|
31
|
+
type?: string;
|
|
31
32
|
adapter?: {
|
|
32
33
|
type: string;
|
|
33
34
|
[key: string]: unknown;
|
|
34
35
|
};
|
|
35
36
|
textSearching?: {
|
|
36
37
|
indexingFeatureTypesToExclude?: string[];
|
|
38
|
+
indexingFeatureTypesToInclude?: string[];
|
|
37
39
|
indexingAttributes?: string[];
|
|
38
40
|
[key: string]: unknown;
|
|
39
41
|
};
|
|
42
|
+
metadata?: {
|
|
43
|
+
skipTextIndex?: boolean;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
};
|
|
40
46
|
name: string;
|
|
41
47
|
assemblyNames: string[];
|
|
42
48
|
trackId: string;
|
|
49
|
+
category?: string[];
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
export declare const defaultAttributesToIndex: string[];
|
|
53
|
+
export declare const defaultFeatureTypesToExclude: string[];
|
|
54
|
+
export type IndexableFormat = 'gff3' | 'gtf' | 'vcf';
|
|
55
|
+
export declare const indexableAdapters: Record<string, {
|
|
56
|
+
locationKey: string;
|
|
57
|
+
format: IndexableFormat;
|
|
58
|
+
}>;
|
|
59
|
+
export declare function isSupportedIndexingAdapter(type?: string): boolean;
|
|
60
|
+
export interface IndexerOptions {
|
|
61
|
+
config: {
|
|
62
|
+
trackId: string;
|
|
63
|
+
};
|
|
64
|
+
attributesToIndex: string[];
|
|
65
|
+
inLocation: string;
|
|
66
|
+
outDir: string;
|
|
67
|
+
onStart: (totalBytes: number) => void;
|
|
68
|
+
onUpdate: (progressBytes: number) => void;
|
|
69
|
+
checkAbort?: () => void;
|
|
70
|
+
}
|
|
71
|
+
export interface Gff3IndexerOptions extends IndexerOptions {
|
|
72
|
+
featureTypesToExclude: string[];
|
|
73
|
+
featureTypesToInclude?: string[];
|
|
43
74
|
}
|
|
44
75
|
export declare function decodeURIComponentNoThrow(uri: string): string;
|
|
45
76
|
export {};
|
package/esm/util.js
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
export const defaultAttributesToIndex = ['Name', 'ID', 'symbol'];
|
|
2
|
+
export const defaultFeatureTypesToExclude = ['exon', 'CDS'];
|
|
3
|
+
// single source of truth for what `jbrowse text-index` can index: which adapter
|
|
4
|
+
// types, where each keeps its file location, and which line parser to use.
|
|
5
|
+
// Everything else (the adapter dispatch, the CLI's supported() gate, the
|
|
6
|
+
// filename guesser) derives from this table.
|
|
7
|
+
export const indexableAdapters = {
|
|
8
|
+
Gff3Adapter: { locationKey: 'gffLocation', format: 'gff3' },
|
|
9
|
+
Gff3TabixAdapter: { locationKey: 'gffGzLocation', format: 'gff3' },
|
|
10
|
+
GtfAdapter: { locationKey: 'gtfLocation', format: 'gtf' },
|
|
11
|
+
GtfTabixAdapter: { locationKey: 'gtfGzLocation', format: 'gtf' },
|
|
12
|
+
VcfAdapter: { locationKey: 'vcfLocation', format: 'vcf' },
|
|
13
|
+
VcfTabixAdapter: { locationKey: 'vcfGzLocation', format: 'vcf' },
|
|
14
|
+
};
|
|
15
|
+
export function isSupportedIndexingAdapter(type = '') {
|
|
16
|
+
return type in indexableAdapters;
|
|
17
|
+
}
|
|
1
18
|
export function decodeURIComponentNoThrow(uri) {
|
|
2
19
|
try {
|
|
3
20
|
return decodeURIComponent(uri);
|
|
4
21
|
}
|
|
5
|
-
catch
|
|
22
|
+
catch {
|
|
6
23
|
return uri;
|
|
7
24
|
}
|
|
8
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/text-indexing-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "JBrowse 2 core text indexing routines for parsing GFF3 and VCF files",
|
|
6
6
|
"keywords": [
|
|
@@ -22,10 +22,7 @@
|
|
|
22
22
|
"main": "esm/index.js",
|
|
23
23
|
"types": "esm/index.d.ts",
|
|
24
24
|
"exports": {
|
|
25
|
-
".":
|
|
26
|
-
"types": "./esm/index.d.ts",
|
|
27
|
-
"import": "./esm/index.js"
|
|
28
|
-
}
|
|
25
|
+
".": "./esm/index.js"
|
|
29
26
|
},
|
|
30
27
|
"files": [
|
|
31
28
|
"esm"
|
|
@@ -36,7 +33,7 @@
|
|
|
36
33
|
},
|
|
37
34
|
"sideEffects": false,
|
|
38
35
|
"scripts": {
|
|
39
|
-
"build:esm": "tsc
|
|
36
|
+
"build:esm": "node ../../node_modules/typescript7/bin/tsc --build tsconfig.build.esm.json",
|
|
40
37
|
"build": "pnpm build:esm",
|
|
41
38
|
"test": "cd ../..; jest --passWithNoTests packages/text-indexing-core",
|
|
42
39
|
"clean": "rimraf esm *.tsbuildinfo",
|