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