gff-nostream
Version:
utilities to read GFF3 data
163 lines • 5.19 kB
JavaScript
// Fast, low-level functions for parsing GFF3.
// JavaScript port of Robert Buels's Bio::GFF3::LowLevel Perl module.
const HEX_LOOKUP = {};
for (let i = 0; i < 256; i++) {
const hex = i.toString(16).toUpperCase().padStart(2, '0');
HEX_LOOKUP[hex] = String.fromCharCode(i);
HEX_LOOKUP[hex.toLowerCase()] = String.fromCharCode(i);
}
/**
* Unescape a string value used in a GFF3 attribute.
*
* @param stringVal - Escaped GFF3 string value
* @returns An unescaped string value
*/
export function unescape(stringVal) {
const idx = stringVal.indexOf('%');
if (idx === -1) {
return stringVal;
}
let result = '';
let lastIdx = 0;
let i = idx;
while (i < stringVal.length) {
const char = stringVal[i] === '%' && i + 2 < stringVal.length
? HEX_LOOKUP[stringVal.slice(i + 1, i + 3)]
: undefined;
if (char !== undefined) {
result += stringVal.slice(lastIdx, i) + char;
i += 3;
lastIdx = i;
}
else {
// Not a valid escape: advance one char so a '%' that begins a real
// escape immediately after isn't swallowed (e.g. the %20 in "a%b%20c").
i++;
}
}
return result + stringVal.slice(lastIdx);
}
function isEmpty(s) {
return s.length === 0 || s === '.';
}
function strField(s, shouldUnescape, empty) {
return isEmpty(s) ? empty : shouldUnescape ? unescape(s) : s;
}
const JBROWSE_DEFAULT_FIELDS = new Set([
'start',
'end',
'seq_id',
'score',
'type',
'source',
'phase',
'strand',
]);
// Pre-computed lowercase for common GFF3 spec attribute names to avoid
// toLowerCase() calls in the hot path
const COMMON_ATTRS = {
ID: 'id',
Name: 'name',
Parent: 'parent',
Note: 'note',
Dbxref: 'dbxref',
Ontology_term: 'ontology_term',
Is_circular: 'is_circular',
Alias: 'alias',
Target: 'target',
Gap: 'gap',
Derives_from: 'derives_from',
id: 'id',
name: 'name',
parent: 'parent',
note: 'note',
dbxref: 'dbxref',
alias: 'alias',
target: 'target',
gap: 'gap',
};
const STRAND_MAP = {
'+': 1,
'-': -1,
'.': 0,
};
/**
* Parse the 9th column (attributes) of a GFF3 feature line into `result`,
* lowercasing keys and suffixing any that collide with a default field name.
* Pass shouldUnescape=false as a fast path for data with no escaped characters.
*/
export function parseAttributes(attrString, result, shouldUnescape) {
if (attrString.length === 0 || attrString === '.') {
return;
}
let len = attrString.length;
if (attrString[len - 1] === '\n') {
len = attrString[len - 2] === '\r' ? len - 2 : len - 1;
attrString = attrString.slice(0, len);
}
let start = 0;
while (start < len) {
let semiIdx = attrString.indexOf(';', start);
if (semiIdx === -1) {
semiIdx = len;
}
if (semiIdx > start) {
const eqIdx = attrString.indexOf('=', start);
if (eqIdx !== -1 && eqIdx < semiIdx && eqIdx + 1 < semiIdx) {
const tag = attrString.slice(start, eqIdx);
let key = COMMON_ATTRS[tag];
if (key === undefined) {
key = tag.toLowerCase();
if (JBROWSE_DEFAULT_FIELDS.has(key)) {
key += '2';
}
}
const values = [];
let valStart = eqIdx + 1;
while (valStart < semiIdx) {
let commaIdx = attrString.indexOf(',', valStart);
if (commaIdx === -1 || commaIdx > semiIdx) {
commaIdx = semiIdx;
}
if (commaIdx > valStart) {
const val = attrString.slice(valStart, commaIdx);
values.push(shouldUnescape ? unescape(val) : val);
}
valStart = commaIdx + 1;
}
result[key] = values.length === 1 ? values[0] : values;
}
}
start = semiIdx + 1;
}
}
/**
* Parse a GFF3 feature line. Unescaping is skipped entirely for lines with no
* '%' character, which is the common case.
*
* @param line - GFF3 feature line
* @returns The parsed feature
*/
export function parseFeature(line) {
const f = line.split('\t');
const shouldUnescape = line.includes('%');
const startStr = f[3];
const endStr = f[4];
const scoreStr = f[5];
const phase = f[7];
const attrString = f[8];
const result = {
refName: strField(f[0], shouldUnescape, ''),
source: strField(f[1], shouldUnescape, null),
type: strField(f[2], shouldUnescape, null),
start: isEmpty(startStr) ? 0 : +startStr - 1,
end: isEmpty(endStr) ? 0 : +endStr,
score: isEmpty(scoreStr) ? undefined : +scoreStr,
strand: STRAND_MAP[f[6]],
phase: isEmpty(phase) ? undefined : +phase,
subfeatures: [],
};
parseAttributes(attrString, result, shouldUnescape);
return result;
}
//# sourceMappingURL=util.js.map