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