jamestalmage/ansi-stream

View on GitHub
index.js

Summary

Maintainability
A
25 mins
Test Coverage
'use strict';
var regex = require('ansi-regex');
var through2 = require('through2');
var StringDecoder = require('string_decoder').StringDecoder;
var EscapeCode = require('escape-code');

module.exports = createStream;
module.exports.EscapeCode = EscapeCode;

function createStream(maxBuffer) {
  maxBuffer = typeof maxBuffer === 'number' ? maxBuffer : 25;
  var ansiStream = new AnsiStream();
  ansiStream._maxBuffer = maxBuffer;
  ansiStream._prev = '';
  return ansiStream;
}

var AnsiStream = through2.ctor(
  {objectMode:true},

  function handleChunk(chunk, enc, cb) {
    var str;
    if (typeof chunk === 'string' || chunk instanceof String) {
      str = chunk;
    } else if (chunk instanceof Buffer) {
      var decoder = this._stringDecoder ||
        (this._stringDecoder = new StringDecoder());
      str = decoder.write(chunk);
    } else {
      this._emptyBuffer();
      this.push(chunk);
      cb();
      return;
    }
    this._prev = this._consumeString(this._prev + str, true);
    cb();
  },

  function handleEndOfStream(cb) {
    this._emptyBuffer();
    cb();
  }
);

var asp = AnsiStream.prototype;

asp._emptyBuffer = function emptyBuffer() {
  var prev = this._prev;
  if (prev) {
    prev = this._consumeString(prev, false);
    if (prev) {
      this.push(prev);
    }
  }
  this._prev = '';
};

/**
 * Consume as much of the current chunk as possible
 *
 * @param str the current chunk (concatenated with the remainder of the previous chunk if it exists)
 *
 * @param moreAvailable `true` if there are (potentially) more chunks coming,
 *          will be called once with `false at the end of the stream.
 *
 * @returns {string} the remaining unconsumed chunk.
 *            It will always be an empty string if `maxBuffer` is `0`.
 */
asp._consumeString = function consumeString(str, moreAvailable) {
  var r = regex();
  var lastIndex = 0;
  var match;

  while ((match = r.exec(str))) {
    var nextIndex = match.index;
    if (nextIndex !== lastIndex) {
      this.push(str.substring(lastIndex, nextIndex));
    }
    var m0 = match[0];
    nextIndex += m0.length;

    if (moreAvailable && nextIndex === str.length) {
      return m0;
    }

    this.push(new EscapeCode(m0));

    lastIndex = nextIndex;
  }

  return this._finalizeRemainder(str.substr(lastIndex));
};

/**
 * A hack to deal with the situation where an escape code sequence spans two chunks (very unlikely).
 * If it sees a potential start of the escape code sequence near the end of the chunk, it will
 * wait to see more characters before pushing.
 *
 * @param string - the remaining end of the current chunk of the current chunk
 *
 * @returns {String} - The remaining text to leave in the buffer.
 */
asp._finalizeRemainder = function finalizeRemainder(string) {
  if (!this._maxBuffer) {
    this.push(string);
    return '';
  }
  if (string) {
    var start = Math.max(0, string.length - this._maxBuffer);
    var p1 = string.indexOf('\u001b', start);
    var p2 = string.indexOf('\u009b', start);
    var p = Math.max(p1, p2);
    if (p < 0) {
      this.push(string);
      string = '';
    } else if (p > 0) {
      this.push(string.substring(0, p));
      string = string.substring(p);
    }
  }
  return string;
};