@gmod/bam 8.5.1 → 8.7.0

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.
Files changed (51) hide show
  1. package/README.md +30 -0
  2. package/dist/bamFile.d.ts +94 -1
  3. package/dist/bamFile.js +94 -3
  4. package/dist/bamFile.js.map +1 -1
  5. package/dist/htsget.d.ts +8 -1
  6. package/dist/htsget.js +4 -1
  7. package/dist/htsget.js.map +1 -1
  8. package/dist/index.d.ts +5 -1
  9. package/dist/index.js +15 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/mismatches.d.ts +130 -0
  12. package/dist/mismatches.js +404 -0
  13. package/dist/mismatches.js.map +1 -0
  14. package/dist/record.d.ts +46 -0
  15. package/dist/record.js +77 -10
  16. package/dist/record.js.map +1 -1
  17. package/dist/reference.d.ts +45 -0
  18. package/dist/reference.js +61 -0
  19. package/dist/reference.js.map +1 -0
  20. package/dist/seqAlphabet.d.ts +3 -0
  21. package/dist/seqAlphabet.js +14 -0
  22. package/dist/seqAlphabet.js.map +1 -0
  23. package/esm/bamFile.d.ts +94 -1
  24. package/esm/bamFile.js +94 -3
  25. package/esm/bamFile.js.map +1 -1
  26. package/esm/htsget.d.ts +8 -1
  27. package/esm/htsget.js +4 -1
  28. package/esm/htsget.js.map +1 -1
  29. package/esm/index.d.ts +5 -1
  30. package/esm/index.js +6 -0
  31. package/esm/index.js.map +1 -1
  32. package/esm/mismatches.d.ts +130 -0
  33. package/esm/mismatches.js +400 -0
  34. package/esm/mismatches.js.map +1 -0
  35. package/esm/record.d.ts +46 -0
  36. package/esm/record.js +70 -3
  37. package/esm/record.js.map +1 -1
  38. package/esm/reference.d.ts +45 -0
  39. package/esm/reference.js +55 -0
  40. package/esm/reference.js.map +1 -0
  41. package/esm/seqAlphabet.d.ts +3 -0
  42. package/esm/seqAlphabet.js +11 -0
  43. package/esm/seqAlphabet.js.map +1 -0
  44. package/package.json +1 -1
  45. package/src/bamFile.ts +178 -1
  46. package/src/htsget.ts +16 -2
  47. package/src/index.ts +26 -1
  48. package/src/mismatches.ts +629 -0
  49. package/src/record.ts +97 -3
  50. package/src/reference.ts +87 -0
  51. package/src/seqAlphabet.ts +10 -0
@@ -0,0 +1,629 @@
1
+ import {
2
+ CIGAR_DEL,
3
+ CIGAR_DIFF,
4
+ CIGAR_EQUAL,
5
+ CIGAR_HARD_CLIP,
6
+ CIGAR_INS,
7
+ CIGAR_MATCH,
8
+ CIGAR_REF_SKIP,
9
+ CIGAR_SOFT_CLIP,
10
+ } from './cigar.ts'
11
+ import { CHAR_CODE_FROM_NIBBLE, referenceNibble } from './reference.ts'
12
+ import { SEQRET_DECODER } from './seqAlphabet.ts'
13
+
14
+ import type { PackedReference } from './reference.ts'
15
+
16
+ /**
17
+ * The kind of difference a {@link Mismatch} reports, as the CIGAR-style char
18
+ * code it corresponds to. `@gmod/cram`'s `forEachMismatch` emits the same
19
+ * values (its `RF_*` constants), so one `switch` handles records from either
20
+ * format.
21
+ *
22
+ * Note these are char codes and NOT the packed-CIGAR op numbers in `cigar.ts`
23
+ * — `MISMATCH_DELETION` is 68 (`'D'`), where `CIGAR_DEL` is 2.
24
+ */
25
+ export const MISMATCH_SUBST = 88 // 'X'
26
+ export const MISMATCH_INSERTION = 73 // 'I'
27
+ export const MISMATCH_DELETION = 68 // 'D'
28
+ export const MISMATCH_REF_SKIP = 78 // 'N'
29
+ export const MISMATCH_SOFT_CLIP = 83 // 'S'
30
+ export const MISMATCH_HARD_CLIP = 72 // 'H'
31
+
32
+ /**
33
+ * One difference between a read and the reference, as
34
+ * {@link BamRecord.getMismatches} reports it.
35
+ *
36
+ * This is the level most consumers want. Deriving the same thing from `CIGAR`
37
+ * and `MD` yourself means knowing that MD counts reference bases and the CIGAR
38
+ * counts both, that MD is absent from a majority of aligners' output and the
39
+ * reference has to stand in for it, that an `=`/`X` CIGAR carries the
40
+ * substitutions itself, and that a deletion's MD payload has to be stepped over
41
+ * rather than read as matches. Every one of those has been a bug in a
42
+ * downstream consumer.
43
+ */
44
+ export interface Mismatch {
45
+ /**
46
+ * Which kind of difference: `MISMATCH_SUBST` (X), `MISMATCH_INSERTION` (I),
47
+ * `MISMATCH_DELETION` (D), `MISMATCH_REF_SKIP` (N), `MISMATCH_SOFT_CLIP` (S)
48
+ * or `MISMATCH_HARD_CLIP` (H). A substitution arrives as `MISMATCH_SUBST`
49
+ * whether the file spelled it with MD, with an `X` CIGAR op, or not at all
50
+ * (leaving it to be found against the reference).
51
+ */
52
+ code: number
53
+ /** 0-based reference position the difference starts at */
54
+ refPos: number
55
+ /**
56
+ * How many reference bases it covers: 1 for a substitution, the deleted or
57
+ * skipped length for D/N, and 0 for insertions and clips, which consume read
58
+ * bases without consuming reference.
59
+ */
60
+ length: number
61
+ /**
62
+ * The substituted base for X, or the inserted bases for I; empty for
63
+ * D/N/S/H, which have no bases of their own to report. Also empty for an
64
+ * insertion in a read with no stored sequence (`SEQ` of `*`), where the
65
+ * length is all the file kept.
66
+ */
67
+ bases: string
68
+ /** quality score of a substituted base, or -1 when the read stores none */
69
+ qual: number
70
+ /**
71
+ * Char code of the reference base a substitution replaces, 0 when unknown —
72
+ * which is what an `X` CIGAR op in a read carrying neither an MD tag nor a
73
+ * reference gives. Upper-cased, since a soft-masked reference would otherwise
74
+ * report lowercase.
75
+ */
76
+ refBaseCode: number
77
+ /** read bases consumed: the inserted or clipped length; 0 otherwise */
78
+ clipLength: number
79
+ }
80
+
81
+ export type MismatchCallback = (
82
+ code: number,
83
+ refPos: number,
84
+ length: number,
85
+ bases: string,
86
+ qual: number,
87
+ refBaseCode: number,
88
+ clipLength: number,
89
+ ) => void
90
+
91
+ export interface MismatchOptions {
92
+ /**
93
+ * Only report differences touching this reference range, 0-based and
94
+ * half-open like every other range this library takes. A deletion or skip is
95
+ * reported when any of the bases it covers falls in the range; everything
96
+ * else when its own position does.
97
+ */
98
+ start?: number
99
+ end?: number
100
+ /**
101
+ * Report positions relative to this reference coordinate, rather than as
102
+ * absolute reference positions. `record.start` gives read-relative
103
+ * positions; the default of 0 gives reference ones.
104
+ *
105
+ * This exists so a consumer with its own coordinate convention can hand its
106
+ * own callback straight to this walk. Converting afterwards means a second
107
+ * callback between this one and the consumer's, and `@gmod/cram` measured
108
+ * that indirect call at ~17% of its walk (its ADR 0008).
109
+ *
110
+ * The window is *not* relative to it: `start`/`end` stay absolute, because
111
+ * they describe a region of the reference rather than a position in the
112
+ * output.
113
+ *
114
+ * SYNC: `@gmod/cram`'s `MismatchOptions.origin`, same meaning.
115
+ */
116
+ origin?: number
117
+ /**
118
+ * Reference bases to resolve substitutions against, for reads with no MD tag.
119
+ * Overrides whatever {@link BamRecord.setReference} bound, and unlike that
120
+ * method it takes a region of any extent: bases outside it are simply left
121
+ * uncompared. Pack it once per region with `packReference`, not per read.
122
+ */
123
+ ref?: PackedReference
124
+ }
125
+
126
+ // M and = both mean "aligned to the reference", differing only in whether the
127
+ // file promises the bases match. Which is exactly the distinction MD (or the
128
+ // reference) exists to settle, so the walk treats them identically.
129
+ const CIGAR_M_EQ_MASK = (1 << CIGAR_MATCH) | (1 << CIGAR_EQUAL)
130
+
131
+ // A clip has no bases to report — its length travels in `clipLength`, which is
132
+ // what every consumer reads.
133
+ const NO_BASES = ''
134
+
135
+ function reportRefMismatch(
136
+ callback: MismatchCallback,
137
+ refPos: number,
138
+ seqNibble: number,
139
+ qual: number,
140
+ refNibble: number,
141
+ ) {
142
+ callback(
143
+ MISMATCH_SUBST,
144
+ refPos,
145
+ 1,
146
+ SEQRET_DECODER[seqNibble]!,
147
+ qual,
148
+ CHAR_CODE_FROM_NIBBLE[refNibble]!,
149
+ 0,
150
+ )
151
+ }
152
+
153
+ // One base compared on its own: the ends of a run whose length or alignment
154
+ // leaves it without a partner in the same byte.
155
+ function compareRefBase(
156
+ callback: MismatchCallback,
157
+ numericSeq: ArrayLike<number>,
158
+ qual: ArrayLike<number> | null | undefined,
159
+ ref: PackedReference,
160
+ seqIdx: number,
161
+ refIdx: number,
162
+ refPos: number,
163
+ ) {
164
+ const seqNibble =
165
+ (numericSeq[seqIdx >> 1]! >> ((1 - (seqIdx & 1)) << 2)) & 0xf
166
+ const refNibble = referenceNibble(ref, refIdx)
167
+ if (seqNibble !== refNibble) {
168
+ reportRefMismatch(
169
+ callback,
170
+ refPos,
171
+ seqNibble,
172
+ qual ? qual[seqIdx]! : -1,
173
+ refNibble,
174
+ )
175
+ }
176
+ }
177
+
178
+ /**
179
+ * The mismatch walk over a record's already-packed fields, for callers holding
180
+ * BAM's arrays without a {@link BamRecord} around them — a SAM parser, or a
181
+ * worker that was handed the typed arrays rather than the record.
182
+ * `record.forEachMismatch()` is this function with the record's own fields
183
+ * filled in, and is what most code should call.
184
+ *
185
+ * Nothing here allocates per difference except the `bases` string, and that
186
+ * only for substitutions and insertions.
187
+ *
188
+ * @param cigar packed CIGAR ops, i.e. `record.NUMERIC_CIGAR`
189
+ * @param numericSeq 4-bit packed SEQ, i.e. `record.NUMERIC_SEQ`
190
+ * @param seqLength bases in `numericSeq`; 0 for a read stored with `SEQ` of `*`
191
+ * @param md the MD tag's bytes (`record.NUMERIC_MD`), or undefined. Preferred
192
+ * over `ref` when present, being both cheaper and what the aligner asserted
193
+ * @param qual per-base quality scores, or null/undefined when the read has none
194
+ * @param ref reference bases to compare against when there is no MD tag. Bases
195
+ * the region does not cover are left uncompared
196
+ * @param refStart the record's own 0-based reference start
197
+ * @param windowStart 0-based half-open reference window to report within, in
198
+ * ABSOLUTE reference coordinates whatever `origin` is. Pass
199
+ * `-Infinity`/`Infinity` for the whole read; a viewport that clips the walk
200
+ * is what keeps a chromosome-spanning contig alignment from being walked in
201
+ * full for every screenful
202
+ * @param windowEnd
203
+ * @param origin what reported positions are relative to: 0 for reference
204
+ * positions, `refStart` for read-relative ones. See
205
+ * {@link MismatchOptions.origin}
206
+ * @param callback `(code, refPos, length, bases, qual, refBaseCode, clipLength)`
207
+ */
208
+ export function forEachMismatchNumeric(
209
+ cigar: ArrayLike<number>,
210
+ numericSeq: ArrayLike<number>,
211
+ seqLength: number,
212
+ md: ArrayLike<number> | undefined,
213
+ qual: ArrayLike<number> | null | undefined,
214
+ ref: PackedReference | undefined,
215
+ refStart: number,
216
+ windowStart: number,
217
+ windowEnd: number,
218
+ origin: number,
219
+ callback: MismatchCallback,
220
+ ) {
221
+ // The walk runs in read-relative offsets — `roffset` reference bases and
222
+ // `soffset` read bases consumed — and converts back at the callback, so the
223
+ // window is converted once here rather than per op.
224
+ //
225
+ // Clamped to int32 rather than left at the ±Infinity an unwindowed walk
226
+ // passes, because every op of the walk compares an offset against these and
227
+ // Infinity makes each of those a Float64 comparison. BAM positions are int32,
228
+ // so this cannot narrow a window anyone can express.
229
+ const winLo =
230
+ windowStart <= -0x80000000 ? -0x80000000 : windowStart - refStart
231
+ const winHi = windowEnd >= 0x7fffffff ? 0x7fffffff : windowEnd - refStart
232
+
233
+ // What an offset is emitted as. Folding `origin` in here rather than at the
234
+ // callbacks keeps the cost of having the option at zero: every emit already
235
+ // added `refStart`, and now adds this instead.
236
+ const outBase = refStart - origin
237
+
238
+ // Fast path for reads with no sequence (e.g. secondary alignments with SEQ='*')
239
+ if (seqLength === 0) {
240
+ let roffset = 0
241
+ for (let i = 0, l = cigar.length; i < l; i++) {
242
+ // `roffset` only grows, and nothing at or past the window's right edge is
243
+ // reported, so the rest of the CIGAR cannot produce anything (see the
244
+ // same check in the main loop)
245
+ if (roffset >= winHi) {
246
+ break
247
+ }
248
+ const packed = cigar[i]!
249
+ const len = packed >>> 4
250
+ const op = packed & 0xf
251
+ // X consumes the reference exactly like M/=; without a sequence there are
252
+ // no bases to report for it, but it still has to advance — leaving it out
253
+ // shifted every later op left by the total X length, which is every indel
254
+ // in an --eqx CIGAR walked without a sequence.
255
+ if ((1 << op) & CIGAR_M_EQ_MASK || op === CIGAR_DIFF) {
256
+ roffset += len
257
+ } else if (op === CIGAR_INS) {
258
+ if (roffset >= winLo && roffset < winHi) {
259
+ callback(MISMATCH_INSERTION, outBase + roffset, 0, '', -1, 0, len)
260
+ }
261
+ } else if (op === CIGAR_DEL) {
262
+ if (roffset < winHi && roffset + len > winLo) {
263
+ callback(
264
+ MISMATCH_DELETION,
265
+ outBase + roffset,
266
+ len,
267
+ NO_BASES,
268
+ -1,
269
+ 0,
270
+ 0,
271
+ )
272
+ }
273
+ roffset += len
274
+ } else if (op === CIGAR_REF_SKIP) {
275
+ if (roffset < winHi && roffset + len > winLo) {
276
+ callback(
277
+ MISMATCH_REF_SKIP,
278
+ outBase + roffset,
279
+ len,
280
+ NO_BASES,
281
+ -1,
282
+ 0,
283
+ 0,
284
+ )
285
+ }
286
+ roffset += len
287
+ } else if (op === CIGAR_SOFT_CLIP) {
288
+ if (roffset >= winLo && roffset < winHi) {
289
+ callback(
290
+ MISMATCH_SOFT_CLIP,
291
+ outBase + roffset,
292
+ 0,
293
+ NO_BASES,
294
+ -1,
295
+ 0,
296
+ len,
297
+ )
298
+ }
299
+ } else if (op === CIGAR_HARD_CLIP) {
300
+ if (roffset >= winLo && roffset < winHi) {
301
+ callback(
302
+ MISMATCH_HARD_CLIP,
303
+ outBase + roffset,
304
+ 0,
305
+ NO_BASES,
306
+ -1,
307
+ 0,
308
+ len,
309
+ )
310
+ }
311
+ }
312
+ }
313
+ return
314
+ }
315
+
316
+ const mdLength = md?.length ?? 0
317
+ const hasQual = !!qual
318
+ const hasMD = md && mdLength > 0
319
+
320
+ // Where the reference region sits in read-relative offsets, and the window
321
+ // narrowed to it. Only base COMPARISON is bounded by the region — an indel or
322
+ // a clip needs no reference base, so a region covering part of a read still
323
+ // reports every one of them.
324
+ //
325
+ // Ternaries rather than Math.max/Math.min, which matters more than it looks:
326
+ // an unwindowed walk has `winLo`/`winHi` at ±Infinity, so Math.min would hand
327
+ // back a Float64 and `cmpHi` would carry that all the way into `jHi`, the
328
+ // bound of the innermost (two-bases-per-byte) loop. This way an unwindowed
329
+ // walk keeps that bound a Smi, which measured ~8% of the reference path on
330
+ // dense short reads.
331
+ let refOffset = 0
332
+ let cmpLo = 0
333
+ let cmpHi = 0
334
+ if (ref !== undefined) {
335
+ refOffset = refStart - ref.start
336
+ const refLo = -refOffset
337
+ const refHi = ref.length - refOffset
338
+ cmpLo = winLo > refLo ? winLo : refLo
339
+ cmpHi = winHi < refHi ? winHi : refHi
340
+ }
341
+
342
+ let roffset = 0
343
+ let soffset = 0
344
+ let mdIdx = 0
345
+ let mdMatchRemaining = 0
346
+
347
+ if (hasMD) {
348
+ while (mdIdx < mdLength) {
349
+ const c = md[mdIdx]!
350
+ if (c >= 48 && c <= 57) {
351
+ mdMatchRemaining = mdMatchRemaining * 10 + (c - 48)
352
+ mdIdx++
353
+ } else {
354
+ break
355
+ }
356
+ }
357
+ }
358
+
359
+ for (let i = 0, l = cigar.length; i < l; i++) {
360
+ // Stop at the window's right edge rather than walking the rest of the
361
+ // CIGAR: `roffset` only grows, and every report below needs
362
+ // `roffset < winHi`, so nothing past here can produce anything. Matters for
363
+ // a whole chromosome stored as one BAM read, whose CIGAR runs to millions
364
+ // of ops for a screenful of them — and costs one compare per op otherwise,
365
+ // since an unwindowed walk has `winHi` at +Infinity.
366
+ if (roffset >= winHi) {
367
+ break
368
+ }
369
+ const packed = cigar[i]!
370
+ const len = packed >>> 4
371
+ const op = packed & 0xf
372
+
373
+ if ((1 << op) & CIGAR_M_EQ_MASK) {
374
+ if (hasMD) {
375
+ let remaining = len
376
+ let localOffset = 0
377
+
378
+ while (remaining > 0) {
379
+ if (mdMatchRemaining >= remaining) {
380
+ mdMatchRemaining -= remaining
381
+ localOffset += remaining
382
+ remaining = 0
383
+ } else {
384
+ localOffset += mdMatchRemaining
385
+ remaining -= mdMatchRemaining
386
+ mdMatchRemaining = 0
387
+
388
+ if (mdIdx < mdLength && md[mdIdx]! >= 65 && md[mdIdx]! <= 90) {
389
+ const pos = roffset + localOffset
390
+ if (pos >= winLo && pos < winHi) {
391
+ const seqIdx = soffset + localOffset
392
+ const sb = numericSeq[seqIdx >> 1]!
393
+ const nibble = (sb >> ((1 - (seqIdx & 1)) << 2)) & 0xf
394
+
395
+ callback(
396
+ MISMATCH_SUBST,
397
+ outBase + pos,
398
+ 1,
399
+ SEQRET_DECODER[nibble]!,
400
+ hasQual ? qual[seqIdx]! : -1,
401
+ md[mdIdx]!,
402
+ 0,
403
+ )
404
+ }
405
+
406
+ mdIdx++
407
+ localOffset++
408
+ remaining--
409
+ mdMatchRemaining = 0
410
+ while (mdIdx < mdLength) {
411
+ const c = md[mdIdx]!
412
+ if (c >= 48 && c <= 57) {
413
+ mdMatchRemaining = mdMatchRemaining * 10 + (c - 48)
414
+ mdIdx++
415
+ } else {
416
+ break
417
+ }
418
+ }
419
+ } else {
420
+ break
421
+ }
422
+ }
423
+ }
424
+ } else if (ref) {
425
+ // Only compare bases whose reference position falls in the window AND
426
+ // in the region. A whole-chromosome contig has M ops totalling ~250M
427
+ // bases; without this clip every one is compared (and `ref` would have
428
+ // to cover the whole chromosome).
429
+ const jLo = cmpLo > roffset ? cmpLo - roffset : 0
430
+ const jHi = cmpHi < roffset + len ? cmpHi - roffset : len
431
+ let j = jLo
432
+ // A leading odd read index has no partner in its byte, so compare it
433
+ // alone and let the pair loop start on a byte boundary.
434
+ if (j < jHi && (soffset + j) & 1) {
435
+ compareRefBase(
436
+ callback,
437
+ numericSeq,
438
+ qual,
439
+ ref,
440
+ soffset + j,
441
+ refOffset + roffset + j,
442
+ outBase + roffset + j,
443
+ )
444
+ j++
445
+ }
446
+ // Two bases per byte load and compare. `even`/`odd` differ only in
447
+ // which reference parity they put on a byte boundary, so one of them
448
+ // always lines up with the read's packed sequence; the nibbles are
449
+ // unpacked only for the byte that actually differs.
450
+ const refIdx = refOffset + roffset + j
451
+ const refPairs = refIdx & 1 ? ref.odd : ref.even
452
+ let refByte = (refIdx + (refIdx & 1)) >> 1
453
+ let seqByte = (soffset + j) >> 1
454
+ for (; j + 1 < jHi; j += 2, refByte++, seqByte++) {
455
+ const seqPair = numericSeq[seqByte]!
456
+ const refPair = refPairs[refByte]!
457
+ if (seqPair !== refPair) {
458
+ const seqHi = (seqPair >> 4) & 0xf
459
+ const refHi = (refPair >> 4) & 0xf
460
+ if (seqHi !== refHi) {
461
+ reportRefMismatch(
462
+ callback,
463
+ outBase + roffset + j,
464
+ seqHi,
465
+ hasQual ? qual[soffset + j]! : -1,
466
+ refHi,
467
+ )
468
+ }
469
+ const seqLo = seqPair & 0xf
470
+ const refLo = refPair & 0xf
471
+ if (seqLo !== refLo) {
472
+ reportRefMismatch(
473
+ callback,
474
+ outBase + roffset + j + 1,
475
+ seqLo,
476
+ hasQual ? qual[soffset + j + 1]! : -1,
477
+ refLo,
478
+ )
479
+ }
480
+ }
481
+ }
482
+ if (j < jHi) {
483
+ compareRefBase(
484
+ callback,
485
+ numericSeq,
486
+ qual,
487
+ ref,
488
+ soffset + j,
489
+ refOffset + roffset + j,
490
+ outBase + roffset + j,
491
+ )
492
+ }
493
+ }
494
+ soffset += len
495
+ roffset += len
496
+ } else if (op === CIGAR_INS) {
497
+ if (roffset >= winLo && roffset < winHi) {
498
+ // Optimized insertion base extraction - avoid string concat for common
499
+ // cases
500
+ let insertedBases: string
501
+ if (len === 1) {
502
+ // Single base insertion - most common case
503
+ const sb = numericSeq[soffset >> 1]!
504
+ const nibble = (sb >> ((1 - (soffset & 1)) << 2)) & 0xf
505
+ insertedBases = SEQRET_DECODER[nibble]!
506
+ } else if (len === 2) {
507
+ // Two base insertion - second most common
508
+ const seqIdx0 = soffset
509
+ const sb0 = numericSeq[seqIdx0 >> 1]!
510
+ const nibble0 = (sb0 >> ((1 - (seqIdx0 & 1)) << 2)) & 0xf
511
+ const seqIdx1 = soffset + 1
512
+ const sb1 = numericSeq[seqIdx1 >> 1]!
513
+ const nibble1 = (sb1 >> ((1 - (seqIdx1 & 1)) << 2)) & 0xf
514
+ insertedBases = SEQRET_DECODER[nibble0]! + SEQRET_DECODER[nibble1]!
515
+ } else {
516
+ const bases = new Array<string>(len)
517
+ for (let j = 0; j < len; j++) {
518
+ const seqIdx = soffset + j
519
+ const sb = numericSeq[seqIdx >> 1]!
520
+ const nibble = (sb >> ((1 - (seqIdx & 1)) << 2)) & 0xf
521
+ bases[j] = SEQRET_DECODER[nibble]!
522
+ }
523
+ insertedBases = bases.join('')
524
+ }
525
+ callback(
526
+ MISMATCH_INSERTION,
527
+ outBase + roffset,
528
+ 0,
529
+ insertedBases,
530
+ -1,
531
+ 0,
532
+ len,
533
+ )
534
+ }
535
+ soffset += len
536
+ } else if (op === CIGAR_DEL) {
537
+ if (roffset < winHi && roffset + len > winLo) {
538
+ callback(MISMATCH_DELETION, outBase + roffset, len, NO_BASES, -1, 0, 0)
539
+ }
540
+
541
+ // MD spells a deletion as ^ then the deleted reference bases, which are
542
+ // not a difference this reports (the D above already is) but do have to
543
+ // be stepped over, or every later position in the read reads off the
544
+ // wrong part of the tag.
545
+ // eslint-disable-next-line @typescript-eslint/no-confusing-non-null-assertion
546
+ if (hasMD && mdIdx < mdLength && md[mdIdx]! === 94) {
547
+ mdIdx++
548
+ while (mdIdx < mdLength && md[mdIdx]! >= 65) {
549
+ mdIdx++
550
+ }
551
+ mdMatchRemaining = 0
552
+ while (mdIdx < mdLength) {
553
+ const c = md[mdIdx]!
554
+ if (c >= 48 && c <= 57) {
555
+ mdMatchRemaining = mdMatchRemaining * 10 + (c - 48)
556
+ mdIdx++
557
+ } else {
558
+ break
559
+ }
560
+ }
561
+ }
562
+ roffset += len
563
+ } else if (op === CIGAR_REF_SKIP) {
564
+ if (roffset < winHi && roffset + len > winLo) {
565
+ callback(MISMATCH_REF_SKIP, outBase + roffset, len, NO_BASES, -1, 0, 0)
566
+ }
567
+ roffset += len
568
+ } else if (op === CIGAR_DIFF) {
569
+ for (let j = 0; j < len; j++) {
570
+ const seqIdx = soffset + j
571
+ const sb = numericSeq[seqIdx >> 1]!
572
+ const nibble = (sb >> ((1 - (seqIdx & 1)) << 2)) & 0xf
573
+
574
+ // An X op says a substitution is here without saying what it replaces,
575
+ // so the reference base comes from MD or the region — and is 0, i.e.
576
+ // unknown, when the read carries neither.
577
+ let refBaseCode = 0
578
+ if (hasMD) {
579
+ if (mdMatchRemaining === 0 && mdIdx < mdLength && md[mdIdx]! >= 65) {
580
+ refBaseCode = md[mdIdx]!
581
+ mdIdx++
582
+ mdMatchRemaining = 0
583
+ while (mdIdx < mdLength) {
584
+ const c = md[mdIdx]!
585
+ if (c >= 48 && c <= 57) {
586
+ mdMatchRemaining = mdMatchRemaining * 10 + (c - 48)
587
+ mdIdx++
588
+ } else {
589
+ break
590
+ }
591
+ }
592
+ } else if (mdMatchRemaining > 0) {
593
+ mdMatchRemaining--
594
+ }
595
+ } else if (ref) {
596
+ const refIdx = refOffset + roffset + j
597
+ if (refIdx >= 0 && refIdx < ref.length) {
598
+ refBaseCode = CHAR_CODE_FROM_NIBBLE[referenceNibble(ref, refIdx)]!
599
+ }
600
+ }
601
+
602
+ const pos = roffset + j
603
+ if (pos >= winLo && pos < winHi) {
604
+ callback(
605
+ MISMATCH_SUBST,
606
+ outBase + pos,
607
+ 1,
608
+ SEQRET_DECODER[nibble]!,
609
+ hasQual ? qual[seqIdx]! : -1,
610
+ refBaseCode,
611
+ 0,
612
+ )
613
+ }
614
+ }
615
+ soffset += len
616
+ roffset += len
617
+ } else if (op === CIGAR_SOFT_CLIP) {
618
+ if (roffset >= winLo && roffset < winHi) {
619
+ callback(MISMATCH_SOFT_CLIP, outBase + roffset, 0, NO_BASES, -1, 0, len)
620
+ }
621
+ soffset += len
622
+ } else if (op === CIGAR_HARD_CLIP) {
623
+ if (roffset >= winLo && roffset < winHi) {
624
+ callback(MISMATCH_HARD_CLIP, outBase + roffset, 0, NO_BASES, -1, 0, len)
625
+ }
626
+ }
627
+ // P (padding) consumes neither read nor reference and is not a difference
628
+ }
629
+ }