UNPKG

@gmod/gtf

Version:

read and write GTF data as streams

274 lines (273 loc) 10.2 kB
//@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