@gmod/tabix 3.1.0 → 3.1.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/src/csi.ts CHANGED
@@ -58,7 +58,7 @@ export default class CSI extends IndexFile {
58
58
  const formatFlags = dataView.getInt32(offset, true)
59
59
  const coordinateType =
60
60
  formatFlags & 0x10000 ? 'zero-based-half-open' : '1-based-closed'
61
- const format = formats[(formatFlags & 0xF) as 0 | 1 | 2]
61
+ const format = formats[(formatFlags & 0xf) as 0 | 1 | 2]
62
62
  if (!format) {
63
63
  throw new Error(`invalid Tabix preset format flags ${formatFlags}`)
64
64
  }
@@ -87,31 +87,6 @@ export default class CSI extends IndexFile {
87
87
  }
88
88
  }
89
89
 
90
- _parseNameBytes(namesBytes: Uint8Array) {
91
- let currRefId = 0
92
- let currNameStart = 0
93
- const refIdToName = []
94
- const refNameToId: Record<string, number> = {}
95
- const decoder = new TextDecoder('utf8')
96
- for (let i = 0; i < namesBytes.length; i += 1) {
97
- if (!namesBytes[i]) {
98
- if (currNameStart < i) {
99
- const refName = this.renameRefSeq(
100
- decoder.decode(namesBytes.subarray(currNameStart, i)),
101
- )
102
- refIdToName[currRefId] = refName
103
- refNameToId[refName] = currRefId
104
- }
105
- currNameStart = i + 1
106
- currRefId += 1
107
- }
108
- }
109
- return {
110
- refNameToId,
111
- refIdToName,
112
- }
113
- }
114
-
115
90
  async _parse(opts: Options = {}) {
116
91
  const bytes = await unzip(await this.filehandle.readFile(opts))
117
92
  const dataView = new DataView(bytes.buffer)
package/src/indexFile.ts CHANGED
@@ -76,4 +76,29 @@ export default abstract class IndexFile {
76
76
  const idx = await this.parse(opts)
77
77
  return !!idx.indices[seqId]?.binIndex
78
78
  }
79
+
80
+ _parseNameBytes(namesBytes: Uint8Array) {
81
+ let currRefId = 0
82
+ let currNameStart = 0
83
+ const refIdToName: string[] = []
84
+ const refNameToId: Record<string, number> = {}
85
+ const decoder = new TextDecoder('utf8')
86
+ for (let i = 0; i < namesBytes.length; i += 1) {
87
+ if (!namesBytes[i]) {
88
+ if (currNameStart < i) {
89
+ const refName = this.renameRefSeq(
90
+ decoder.decode(namesBytes.subarray(currNameStart, i)),
91
+ )
92
+ refIdToName[currRefId] = refName
93
+ refNameToId[refName] = currRefId
94
+ }
95
+ currNameStart = i + 1
96
+ currRefId += 1
97
+ }
98
+ }
99
+ return {
100
+ refNameToId,
101
+ refIdToName,
102
+ }
103
+ }
79
104
  }
@@ -11,11 +11,6 @@ import { checkAbortSignal } from './util.ts'
11
11
 
12
12
  import type { GenericFilehandle } from 'generic-filehandle2'
13
13
 
14
- function isASCII(str: string) {
15
- // eslint-disable-next-line no-control-regex
16
- return /^[\u0000-\u007F]*$/.test(str)
17
- }
18
-
19
14
  type GetLinesCallback = (line: string, fileOffset: number) => void
20
15
 
21
16
  interface GetLinesOpts {
@@ -164,6 +159,21 @@ export default class TabixIndexedFile {
164
159
  * @returns promise that is resolved when the whole read is finished,
165
160
  * rejected on error
166
161
  */
162
+ private calculateFileOffset(
163
+ cpositions: number[],
164
+ dpositions: number[],
165
+ pos: number,
166
+ blockStart: number,
167
+ minvDataPosition: number,
168
+ ) {
169
+ return (
170
+ cpositions[pos]! * (1 << 8) +
171
+ (blockStart - dpositions[pos]!) +
172
+ minvDataPosition +
173
+ 1
174
+ )
175
+ }
176
+
167
177
  async getLines(
168
178
  refName: string,
169
179
  s: number | undefined,
@@ -212,69 +222,104 @@ export default class TabixIndexedFile {
212
222
  let pos = 0
213
223
 
214
224
  // fast path, Buffer is just ASCII chars and not gigantor, can be
215
- // converted to string and processed directly. if it is not ASCII or
216
- // gigantic (chrome max str len is 512Mb), we have to decode line by line
225
+ // converted to string and processed directly.
226
+ //
227
+ // if it is not ASCII or, we have to decode line by line, as it is
228
+ // otherwise hard to get the right 'fileOffset' based feature IDs
229
+ //
230
+ // we use a basic check for isASCII: string length equals buffer length
231
+ // if it is ASCII...no multi-byte decodings
217
232
  const str = decoder.decode(buffer)
218
- const strIsASCII = isASCII(str)
219
- while (blockStart < str.length) {
220
- let line: string
221
- let n: number
222
- if (strIsASCII) {
223
- n = str.indexOf('\n', blockStart)
233
+ const strIsASCII = buffer.length == str.length
234
+ if (strIsASCII) {
235
+ while (blockStart < str.length) {
236
+ const n = str.indexOf('\n', blockStart)
224
237
  if (n === -1) {
225
238
  break
226
239
  }
227
- line = str.slice(blockStart, n)
228
- } else {
229
- n = buffer.indexOf('\n'.charCodeAt(0), blockStart)
240
+ const line = str.slice(blockStart, n)
241
+
242
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
243
+ if (dpositions) {
244
+ const target = blockStart + c.minv.dataPosition
245
+ while (pos < dpositions.length && target >= dpositions[pos]!) {
246
+ pos++
247
+ }
248
+ }
249
+
250
+ // filter the line for whether it is within the requested range
251
+ const { startCoordinate, overlaps } = this.checkLine(
252
+ metadata,
253
+ refName,
254
+ start,
255
+ end,
256
+ line,
257
+ )
258
+
259
+ if (overlaps) {
260
+ callback(
261
+ line,
262
+ this.calculateFileOffset(
263
+ cpositions,
264
+ dpositions,
265
+ pos,
266
+ blockStart,
267
+ c.minv.dataPosition,
268
+ ),
269
+ )
270
+ } else if (startCoordinate !== undefined && startCoordinate >= end) {
271
+ // the lines were overlapping the region, but now have stopped, so we
272
+ // must be at the end of the relevant data and we can stop processing
273
+ // data now
274
+ return
275
+ }
276
+ blockStart = n + 1
277
+ }
278
+ } else {
279
+ while (blockStart < buffer.length) {
280
+ const n = buffer.indexOf('\n'.charCodeAt(0), blockStart)
230
281
  if (n === -1) {
231
282
  break
232
283
  }
233
284
  const b = buffer.slice(blockStart, n)
234
- line = decoder.decode(b)
235
- }
285
+ const line = decoder.decode(b)
236
286
 
237
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
238
- if (dpositions) {
239
- while (blockStart + c.minv.dataPosition >= dpositions[pos++]!) {}
240
- pos--
241
- }
287
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
288
+ if (dpositions) {
289
+ const target = blockStart + c.minv.dataPosition
290
+ while (pos < dpositions.length && target >= dpositions[pos]!) {
291
+ pos++
292
+ }
293
+ }
242
294
 
243
- // filter the line for whether it is within the requested range
244
- const { startCoordinate, overlaps } = this.checkLine(
245
- metadata,
246
- refName,
247
- start,
248
- end,
249
- line,
250
- )
251
-
252
- if (overlaps) {
253
- callback(
295
+ // filter the line for whether it is within the requested range
296
+ const { startCoordinate, overlaps } = this.checkLine(
297
+ metadata,
298
+ refName,
299
+ start,
300
+ end,
254
301
  line,
255
- // cpositions[pos] refers to actual file offset of a bgzip block
256
- // boundaries
257
- //
258
- // we multiply by (1 <<8) in order to make sure each block has a
259
- // "unique" address space so that data in that block could never
260
- // overlap
261
- //
262
- // then the blockStart-dpositions is an uncompressed file offset
263
- // from that bgzip block boundary, and since the cpositions are
264
- // multiplied by (1 << 8) these uncompressed offsets get a unique
265
- // space
266
- cpositions[pos]! * (1 << 8) +
267
- (blockStart - dpositions[pos]!) +
268
- c.minv.dataPosition +
269
- 1,
270
302
  )
271
- } else if (startCoordinate !== undefined && startCoordinate >= end) {
272
- // the lines were overlapping the region, but now have stopped, so we
273
- // must be at the end of the relevant data and we can stop processing
274
- // data now
275
- return
303
+
304
+ if (overlaps) {
305
+ callback(
306
+ line,
307
+ this.calculateFileOffset(
308
+ cpositions,
309
+ dpositions,
310
+ pos,
311
+ blockStart,
312
+ c.minv.dataPosition,
313
+ ),
314
+ )
315
+ } else if (startCoordinate !== undefined && startCoordinate >= end) {
316
+ // the lines were overlapping the region, but now have stopped, so we
317
+ // must be at the end of the relevant data and we can stop processing
318
+ // data now
319
+ return
320
+ }
321
+ blockStart = n + 1
276
322
  }
277
- blockStart = n + 1
278
323
  }
279
324
  }
280
325
  }
@@ -448,11 +493,11 @@ export default class TabixIndexedFile {
448
493
  }
449
494
  }
450
495
  }
451
- currentColumnStart = i + 1
452
- currentColumnNumber += 1
453
- if (currentColumnNumber > maxColumn) {
496
+ if (currentColumnNumber === maxColumn) {
454
497
  break
455
498
  }
499
+ currentColumnStart = i + 1
500
+ currentColumnNumber += 1
456
501
  }
457
502
  }
458
503
  return {
@@ -470,17 +515,10 @@ export default class TabixIndexedFile {
470
515
  // be another pairwise feature at the end of this one
471
516
  const isTRA = info.includes('SVTYPE=TRA')
472
517
  if (info[0] !== '.' && !isTRA) {
473
- let prevChar = ';'
474
- for (let j = 0; j < info.length; j += 1) {
475
- if (prevChar === ';' && info.slice(j, j + 4) === 'END=') {
476
- let valueEnd = info.indexOf(';', j)
477
- if (valueEnd === -1) {
478
- valueEnd = info.length
479
- }
480
- endCoordinate = Number.parseInt(info.slice(j + 4, valueEnd), 10)
481
- break
482
- }
483
- prevChar = info[j]
518
+ const endRegex = /(?:^|;)END=([^;]+)/
519
+ const match = endRegex.exec(info)
520
+ if (match) {
521
+ endCoordinate = Number.parseInt(match[1]!, 10)
484
522
  }
485
523
  } else if (isTRA) {
486
524
  return startCoordinate + 1
package/src/tbi.ts CHANGED
@@ -62,7 +62,7 @@ export default class TabixIndex extends IndexFile {
62
62
  1: 'SAM',
63
63
  2: 'VCF',
64
64
  }
65
- const format = formatOpts[formatFlags & 0xF]
65
+ const format = formatOpts[formatFlags & 0xf]
66
66
  if (!format) {
67
67
  throw new Error(`invalid Tabix preset format flags ${formatFlags}`)
68
68
  }
@@ -160,31 +160,6 @@ export default class TabixIndex extends IndexFile {
160
160
  }
161
161
  }
162
162
 
163
- _parseNameBytes(namesBytes: Uint8Array) {
164
- let currRefId = 0
165
- let currNameStart = 0
166
- const refIdToName: string[] = []
167
- const refNameToId: Record<string, number> = {}
168
- const decoder = new TextDecoder('utf8')
169
- for (let i = 0; i < namesBytes.length; i += 1) {
170
- if (!namesBytes[i]) {
171
- if (currNameStart < i) {
172
- const refName = this.renameRefSeq(
173
- decoder.decode(namesBytes.subarray(currNameStart, i)),
174
- )
175
- refIdToName[currRefId] = refName
176
- refNameToId[refName] = currRefId
177
- }
178
- currNameStart = i + 1
179
- currRefId += 1
180
- }
181
- }
182
- return {
183
- refNameToId,
184
- refIdToName,
185
- }
186
- }
187
-
188
163
  async blocksForRange(
189
164
  refName: string,
190
165
  min: number,