UNPKG

@gmod/bed

Version:

A BED file format parser with autoSql support

111 lines 4.34 kB
import { parse } from './autoSql.js'; import types from "./defaultTypes.js"; import { detectTypes } from "./util.js"; const strandMap = { '.': 0, '-': -1, '+': 1 }; // Lightweight percent-decode for chrom names: rewrites only well-formed %XX // escapes and leaves a bare '%' untouched. decodeURIComponent throws on a '%' // not followed by valid hex (e.g. a contig name like "chr%_test"), taking down // the whole parse; the BED spec defines chrom as [A-Za-z0-9_] with no // percent-encoding, so the full UTF-8 machinery isn't warranted here. function decodeChrom(chrom) { return chrom.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))); } // heuristic that a BED file is BED12 like...the number in col 10 is // blockCount-like function isBed12Like(fields) { const blockCount = Number.parseInt(fields[9], 10); return (fields.length >= 12 && !Number.isNaN(blockCount) && fields[10]?.split(',').filter(f => f).length === blockCount); } export default class BED { autoSql; attemptDefaultBed; constructor(opts = {}) { if (opts.autoSql) { this.autoSql = detectTypes(parse(opts.autoSql)); } else if (opts.type) { if (!types[opts.type]) { throw new Error('Type not found'); } this.autoSql = detectTypes(types[opts.type]); } else { this.autoSql = detectTypes(types.defaultBedSchema); this.attemptDefaultBed = true; } } /* * parses a line of text as a BED line with the loaded autoSql schema * * @param line - a BED line as tab delimited text or array * @param opts - supply opts.uniqueId * @return a object representing a feature */ parseLine(line, options = {}) { const { autoSql } = this; const { uniqueId } = options; const fields = Array.isArray(line) ? line : line.split('\t'); const feature = {}; if (!this.attemptDefaultBed || isBed12Like(fields)) { for (let index = 0; index < autoSql.fields.length; index++) { const autoField = autoSql.fields[index]; const rawColumn = fields[index]; const { isNumeric, isArray, arrayIsNumeric, name } = autoField; if (rawColumn === undefined) { break; } if (rawColumn !== '.') { if (isNumeric) { const num = Number(rawColumn); feature[name] = Number.isNaN(num) ? rawColumn : num; } else if (isArray) { const parts = rawColumn.split(','); if (parts.at(-1) === '') { parts.pop(); } feature[name] = arrayIsNumeric ? parts.map(Number) : parts; } else { feature[name] = rawColumn; } } } } else { feature.chrom = fields[0]; feature.chromStart = Number(fields[1]); feature.chromEnd = Number(fields[2]); if (fields[3] !== undefined) { feature.name = fields[3]; } const field4 = fields[4]; if (field4 !== undefined) { const asNum = Number.parseFloat(field4); if (Number.isNaN(asNum)) { feature.field4 = field4; } else { feature.score = asNum; } } const field5 = fields[5]; if (field5 !== undefined) { feature[field5 === '+' || field5 === '-' ? 'strand' : 'field5'] = field5; } for (let i = 6; i < fields.length; i++) { feature['field' + i] = fields[i]; } } if (uniqueId) { feature.uniqueId = uniqueId; } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition feature.strand = strandMap[feature.strand] ?? 0; feature.chrom = decodeChrom(String(feature.chrom)); return feature; } } //# sourceMappingURL=parser.js.map