@gmod/gtf
Version:
read and write GTF data as streams
274 lines (273 loc) • 10.2 kB
JavaScript
//@ts-nocheck
import Parser from './parse';
import { formatItem, formatSequence } from './util';
import { Transform } from 'stream';
import { StringDecoder as Decoder } from 'string_decoder';
// don't load fs native module if running in webpacked code
// eslint-disable-next-line camelcase
const fs = typeof __webpack_require__ !== 'function' ? require('fs') : null;
// call a callback on the next process tick if running in
// an environment that supports it
function _callback(callback) {
if (process && process.nextTick) {
process.nextTick(callback);
}
else {
callback();
}
}
// shared arg processing for the parse routines
function _processParseOptions(options, additionalDefaults = {}) {
const out = Object.assign({
parseFeatures: true,
parseDirectives: false,
parseSequences: true,
parseComments: false,
}, additionalDefaults, options);
if (options.parseAll) {
out.parseFeatures = true;
out.parseDirectives = true;
out.parseComments = true;
out.parseSequences = true;
}
return out;
}
class GTFTransform extends Transform {
constructor(inputOptions = {}) {
const options = _processParseOptions(inputOptions);
super({ objectMode: true });
this.encoding = inputOptions.encoding || 'utf8';
this.decoder = new Decoder();
this.textBuffer = '';
const push = this.push.bind(this);
this.parser = new Parser({
featureCallback: options.parseFeatures ? push : null,
directiveCallback: options.parseDirectives ? push : null,
commentCallback: options.parseComments ? push : null,
sequenceCallback: options.parseSequences ? push : null,
errorCallback: err => this.emit('error', err),
bufferSize: options.bufferSize,
});
}
_addLine(data) {
const line = data.toString('utf8');
if (line) {
this.parser.addLine(line);
}
}
_nextText(buffer) {
const pieces = (this.textBuffer + buffer).split(/\r?\n/);
this.textBuffer = pieces.pop();
if (this.maxLineLength && this.textBuffer.length > this.maxLineLength) {
this.emit('error', new Error('maximum line size exceeded'));
return;
}
pieces.forEach(piece => this._addLine(piece));
}
_transform(chunk, encoding, callback) {
this._nextText(this.decoder.write(chunk));
_callback(callback);
}
_flush(callback) {
if (this.decoder.end) {
this._nextText(this.decoder.end());
}
if (this.textBuffer != null) {
this._addLine(this.textBuffer);
}
this.parser.finish();
_callback(callback);
}
}
/**
* Parse a stream of text data into a stream of feature,
* directive, and comment objects.
*
* @param {Object} options optional options object
* @param {string} options.encoding text encoding of the input GTF. default 'utf8'
* @param {boolean} options.parseAll default false. if true, will parse all items. overrides other flags
* @param {boolean} options.parseFeatures default true
* @param {boolean} options.parseDirectives default false
* @param {boolean} options.parseComments default false
* @param {boolean} options.parseSequences default true
* @param {Number} options.bufferSize maximum number of GTF lines to buffer. defaults to 1000
* @returns {ReadableStream} stream (in objectMode) of parsed items
*/
export function parseStream(options = {}) {
const newOptions = Object.assign({ bufferSize: 1000 }, options);
return new GTFTransform(newOptions);
}
/**
* Read and parse a GTF file from the filesystem.
*
* @param {string} filename the filename of the file to parse
* @param {Object} options optional options object
* @param {string} options.encoding the file's string encoding, defaults to 'utf8'
* @param {boolean} options.parseAll default false. if true, will parse all items. overrides other flags
* @param {boolean} options.parseFeatures default true
* @param {boolean} options.parseDirectives default false
* @param {boolean} options.parseComments default false
* @param {boolean} options.parseSequences default true
* @param {Number} options.bufferSize maximum number of GTF lines to buffer. defaults to 1000
* @returns {ReadableStream} stream (in objectMode) of parsed items
*/
export function parseFile(filename, options) {
return fs.createReadStream(filename).pipe(parseStream(options));
}
/**
* Synchronously parse a string containing GTF and return
* an arrayref of the parsed items.
*
* @param {string} str
* @param {Object} inputOptions optional options object
* @param {boolean} inputOptions.parseAll default false. if true, will parse all items. overrides other flags
* @param {boolean} inputOptions.parseFeatures default true
* @param {boolean} inputOptions.parseDirectives default false
* @param {boolean} inputOptions.parseComments default false
* @param {boolean} inputOptions.parseSequences default true
* @returns {Array} array of parsed features, directives, and/or comments
*/
export function parseStringSync(str, inputOptions = {}) {
if (!str) {
return [];
}
const options = _processParseOptions(inputOptions);
const items = [];
const push = items.push.bind(items);
const parser = new Parser({
featureCallback: options.parseFeatures ? push : null,
directiveCallback: options.parseDirectives ? push : null,
commentCallback: options.parseComments ? push : null,
sequenceCallback: options.parseSequences ? push : null,
bufferSize: Infinity,
errorCallback: err => {
throw err;
},
});
str.split(/\r?\n/).forEach(parser.addLine.bind(parser));
parser.finish();
return items;
}
/**
* Format an array of GTF items (features,directives,comments) into string of GTF.
* Does not insert synchronization (###) marks.
* Does not insert directive if it's not already there.
*
* @param {Array[Object]} items
* @returns {String} the formatted GTF
*/
export function formatSync(items) {
// sort items into seq and other
const other = [];
const sequences = [];
items.forEach(i => {
if (i.sequence) {
sequences.push(i);
}
else {
other.push(i);
}
});
let str = other.map(formatItem).join('');
if (sequences.length) {
str += '##FASTA\n';
str += sequences.map(formatSequence).join('');
}
return str;
}
class FormattingTransform extends Transform {
constructor(options = {}) {
super(Object.assign(options, { objectMode: true }));
this.linesSinceLastSyncMark = 0;
this.minLinesBetweenSyncMarks = options.minSyncLines || 100;
this.insertVersionDirective = options.insertVersionDirective || false;
this.haveWeEmittedData = false;
this.fastaMode = false;
}
_transform(chunk, encoding, callback) {
// if we have not emitted anything yet, and this first
// chunk is not a gtf directive, emit one
let str;
if (!this.haveWeEmittedData &&
this.insertVersionDirective &&
(chunk[0] || chunk).directive !== 'gtf') {
this.push('##gtf\n');
}
// if it's a sequence chunk coming down, emit a FASTA directive and
// change to FASTA mode
if (chunk.sequence && !this.fastaMode) {
this.push('##FASTA\n');
this.fastaMode = true;
}
if (Array.isArray(chunk)) {
str = chunk.map(formatItem).join('');
}
else {
str = formatItem(chunk);
}
this.push(str);
if (this.linesSinceLastSyncMark >= this.minLinesBetweenSyncMarks) {
this.push('###\n');
this.linesSinceLastSyncMark = 0;
}
else {
// count the number of newlines in this chunk
let count = 0;
for (let i = 0; i < str.length; i += 1) {
if (str[i] === '\n') {
count += 1;
}
}
this.linesSinceLastSyncMark += count;
}
this.haveWeEmittedData = true;
_callback(callback);
}
}
/**
* Format a stream of items (of the type produced
* by this script) into a stream of GTF text.
*
* Inserts synchronization (###) marks automatically.
*
* @param {Object} options
* @param {Object} options.minSyncLines minimum number of lines between ### marks. default 100
* @param {Boolean} options.insertVersionDirective
* if the first item in the stream is not a ##gff-version directive, insert one to show it's gtf
* default false
*/
export function formatStream(options) {
return new FormattingTransform(options);
}
/**
* Format a stream of items (of the type produced
* by this script) into a GTF file and write it to the filesystem.
* Inserts synchronization (###) marks and a ##gtf
* directive automatically (if one is not already present).
*
* @param {ReadableStream} stream the stream to write to the file
* @param {String} filename the file path to write to
* @param {Object} options
* @param {String} options.encoding default 'utf8'. encoding for the written file
* @param {Number} options.minSyncLines
* minimum number of lines between sync (###) marks. default 100
* @param {Boolean} options.insertVersionDirective
* if the first item in the stream is not a ##gtf directive, insert one.
* default false
* @returns {Promise} promise for the written filename
*/
export function formatFile(stream, filename, options = {}) {
const newOptions = Object.assign({
insertVersionDirective: false,
}, options);
return new Promise((resolve, reject) => {
stream
.pipe(new FormattingTransform(newOptions))
.on('end', () => resolve(filename))
.on('error', reject)
.pipe(fs.createWriteStream(filename, {
encoding: newOptions.encoding || 'utf8',
}));
});
}
//# sourceMappingURL=api.js.map