diff options
| -rw-r--r-- | js/ansi/ansi.js | 405 | ||||
| -rw-r--r-- | js/ansi/newlines.js | 71 | ||||
| -rwxr-xr-x | js/test.js | 2 | 
3 files changed, 477 insertions, 1 deletions
diff --git a/js/ansi/ansi.js b/js/ansi/ansi.js new file mode 100644 index 0000000..52fc8ec --- /dev/null +++ b/js/ansi/ansi.js @@ -0,0 +1,405 @@ + +/** + * References: + * + *   - http://en.wikipedia.org/wiki/ANSI_escape_code + *   - http://www.termsys.demon.co.uk/vtansi.htm + * + */ + +/** + * Module dependencies. + */ + +var emitNewlineEvents = require('./newlines') +  , prefix = '\x1b[' // For all escape codes +  , suffix = 'm'     // Only for color codes + +/** + * The ANSI escape sequences. + */ + +var codes = { +    up: 'A' +  , down: 'B' +  , forward: 'C' +  , back: 'D' +  , nextLine: 'E' +  , previousLine: 'F' +  , horizontalAbsolute: 'G' +  , eraseData: 'J' +  , eraseLine: 'K' +  , scrollUp: 'S' +  , scrollDown: 'T' +  , savePosition: 's' +  , restorePosition: 'u' +  , queryPosition: '6n' +  , hide: '?25l' +  , show: '?25h' +} + +/** + * Rendering ANSI codes. + */ + +var styles = { +    bold: 1 +  , italic: 3 +  , underline: 4 +  , inverse: 7 +} + +/** + * The negating ANSI code for the rendering modes. + */ + +var reset = { +    bold: 22 +  , italic: 23 +  , underline: 24 +  , inverse: 27 +} + +/** + * The standard, styleable ANSI colors. + */ + +var colors = { +    white: 37 +  , black: 30 +  , blue: 34 +  , cyan: 36 +  , green: 32 +  , magenta: 35 +  , red: 31 +  , yellow: 33 +  , grey: 90 +  , brightBlack: 90 +  , brightRed: 91 +  , brightGreen: 92 +  , brightYellow: 93 +  , brightBlue: 94 +  , brightMagenta: 95 +  , brightCyan: 96 +  , brightWhite: 97 +} + + +/** + * Creates a Cursor instance based off the given `writable stream` instance. + */ + +function ansi (stream, options) { +  if (stream._ansicursor) { +    return stream._ansicursor +  } else { +    return stream._ansicursor = new Cursor(stream, options) +  } +} +module.exports = exports = ansi + +/** + * The `Cursor` class. + */ + +function Cursor (stream, options) { +  if (!(this instanceof Cursor)) { +    return new Cursor(stream, options) +  } +  if (typeof stream != 'object' || typeof stream.write != 'function') { +    throw new Error('a valid Stream instance must be passed in') +  } + +  // the stream to use +  this.stream = stream + +  // when 'enabled' is false then all the functions are no-ops except for write() +  this.enabled = options && options.enabled +  if (typeof this.enabled === 'undefined') { +    this.enabled = stream.isTTY +  } +  this.enabled = !!this.enabled + +  // then `buffering` is true, then `write()` calls are buffered in +  // memory until `flush()` is invoked +  this.buffering = !!(options && options.buffering) +  this._buffer = [] + +  // controls the foreground and background colors +  this.fg = this.foreground = new Colorer(this, 0) +  this.bg = this.background = new Colorer(this, 10) + +  // defaults +  this.Bold = false +  this.Italic = false +  this.Underline = false +  this.Inverse = false + +  // keep track of the number of "newlines" that get encountered +  this.newlines = 0 +  emitNewlineEvents(stream) +  stream.on('newline', function () { +    this.newlines++ +  }.bind(this)) +} +exports.Cursor = Cursor + +/** + * Helper function that calls `write()` on the underlying Stream. + * Returns `this` instead of the write() return value to keep + * the chaining going. + */ + +Cursor.prototype.write = function (data) { +  if (this.buffering) { +    this._buffer.push(arguments) +  } else { +    this.stream.write.apply(this.stream, arguments) +  } +  return this +} + +/** + * Buffer `write()` calls into memory. + * + * @api public + */ + +Cursor.prototype.buffer = function () { +  this.buffering = true +  return this +} + +/** + * Write out the in-memory buffer. + * + * @api public + */ + +Cursor.prototype.flush = function () { +  this.buffering = false +  var str = this._buffer.map(function (args) { +    if (args.length != 1) throw new Error('unexpected args length! ' + args.length); +    return args[0]; +  }).join(''); +  this._buffer.splice(0); // empty +  this.write(str); +  return this +} + + +/** + * The `Colorer` class manages both the background and foreground colors. + */ + +function Colorer (cursor, base) { +  this.current = null +  this.cursor = cursor +  this.base = base +} +exports.Colorer = Colorer + +/** + * Write an ANSI color code, ensuring that the same code doesn't get rewritten. + */ + +Colorer.prototype._setColorCode = function setColorCode (code) { +  var c = String(code) +  if (this.current === c) return +  this.cursor.enabled && this.cursor.write(prefix + c + suffix) +  this.current = c +  return this +} + + +/** + * Set up the positional ANSI codes. + */ + +Object.keys(codes).forEach(function (name) { +  var code = String(codes[name]) +  Cursor.prototype[name] = function () { +    var c = code +    if (arguments.length > 0) { +      c = toArray(arguments).map(Math.round).join(';') + code +    } +    this.enabled && this.write(prefix + c) +    return this +  } +}) + +/** + * Set up the functions for the rendering ANSI codes. + */ + +Object.keys(styles).forEach(function (style) { +  var name = style[0].toUpperCase() + style.substring(1) +    , c = styles[style] +    , r = reset[style] + +  Cursor.prototype[style] = function () { +    if (this[name]) return +    this.enabled && this.write(prefix + c + suffix) +    this[name] = true +    return this +  } + +  Cursor.prototype['reset' + name] = function () { +    if (!this[name]) return +    this.enabled && this.write(prefix + r + suffix) +    this[name] = false +    return this +  } +}) + +/** + * Setup the functions for the standard colors. + */ + +Object.keys(colors).forEach(function (color) { +  var code = colors[color] + +  Colorer.prototype[color] = function () { +    this._setColorCode(this.base + code) +    return this.cursor +  } + +  Cursor.prototype[color] = function () { +    return this.foreground[color]() +  } +}) + +/** + * Makes a beep sound! + */ + +Cursor.prototype.beep = function () { +  this.enabled && this.write('\x07') +  return this +} + +/** + * Moves cursor to specific position + */ + +Cursor.prototype.goto = function (x, y) { +  x = x | 0 +  y = y | 0 +  this.enabled && this.write(prefix + y + ';' + x + 'H') +  return this +} + +/** + * Resets the color. + */ + +Colorer.prototype.reset = function () { +  this._setColorCode(this.base + 39) +  return this.cursor +} + +/** + * Resets all ANSI formatting on the stream. + */ + +Cursor.prototype.reset = function () { +  this.enabled && this.write(prefix + '0' + suffix) +  this.Bold = false +  this.Italic = false +  this.Underline = false +  this.Inverse = false +  this.foreground.current = null +  this.background.current = null +  return this +} + +/** + * Sets the foreground color with the given RGB values. + * The closest match out of the 216 colors is picked. + */ + +Colorer.prototype.rgb = function (r, g, b) { +  var base = this.base + 38 +    , code = rgb(r, g, b) +  this._setColorCode(base + ';5;' + code) +  return this.cursor +} + +/** + * Same as `cursor.fg.rgb(r, g, b)`. + */ + +Cursor.prototype.rgb = function (r, g, b) { +  return this.foreground.rgb(r, g, b) +} + +/** + * Accepts CSS color codes for use with ANSI escape codes. + * For example: `#FF000` would be bright red. + */ + +Colorer.prototype.hex = function (color) { +  return this.rgb.apply(this, hex(color)) +} + +/** + * Same as `cursor.fg.hex(color)`. + */ + +Cursor.prototype.hex = function (color) { +  return this.foreground.hex(color) +} + + +// UTIL FUNCTIONS // + +/** + * Translates a 255 RGB value to a 0-5 ANSI RGV value, + * then returns the single ANSI color code to use. + */ + +function rgb (r, g, b) { +  var red = r / 255 * 5 +    , green = g / 255 * 5 +    , blue = b / 255 * 5 +  return rgb5(red, green, blue) +} + +/** + * Turns rgb 0-5 values into a single ANSI color code to use. + */ + +function rgb5 (r, g, b) { +  var red = Math.round(r) +    , green = Math.round(g) +    , blue = Math.round(b) +  return 16 + (red*36) + (green*6) + blue +} + +/** + * Accepts a hex CSS color code string (# is optional) and + * translates it into an Array of 3 RGB 0-255 values, which + * can then be used with rgb(). + */ + +function hex (color) { +  var c = color[0] === '#' ? color.substring(1) : color +    , r = c.substring(0, 2) +    , g = c.substring(2, 4) +    , b = c.substring(4, 6) +  return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)] +} + +/** + * Turns an array-like object into a real array. + */ + +function toArray (a) { +  var i = 0 +    , l = a.length +    , rtn = [] +  for (; i<l; i++) { +    rtn.push(a[i]) +  } +  return rtn +} diff --git a/js/ansi/newlines.js b/js/ansi/newlines.js new file mode 100644 index 0000000..4e37a0a --- /dev/null +++ b/js/ansi/newlines.js @@ -0,0 +1,71 @@ + +/** + * Accepts any node Stream instance and hijacks its "write()" function, + * so that it can count any newlines that get written to the output. + * + * When a '\n' byte is encountered, then a "newline" event will be emitted + * on the stream, with no arguments. It is up to the listeners to determine + * any necessary deltas required for their use-case. + * + * Ex: + * + *   var cursor = ansi(process.stdout) + *     , ln = 0 + *   process.stdout.on('newline', function () { + *    ln++ + *   }) + */ + +/** + * Module dependencies. + */ + +var assert = require('assert') +var NEWLINE = '\n'.charCodeAt(0) + +function emitNewlineEvents (stream) { +  if (stream._emittingNewlines) { +    // already emitting newline events +    return +  } + +  var write = stream.write + +  stream.write = function (data) { +    // first write the data +    var rtn = write.apply(stream, arguments) + +    if (stream.listeners('newline').length > 0) { +      var len = data.length +        , i = 0 +      // now try to calculate any deltas +      if (typeof data == 'string') { +        for (; i<len; i++) { +          processByte(stream, data.charCodeAt(i)) +        } +      } else { +        // buffer +        for (; i<len; i++) { +          processByte(stream, data[i]) +        } +      } +    } + +    return rtn +  } + +  stream._emittingNewlines = true +} +module.exports = emitNewlineEvents + + +/** + * Processes an individual byte being written to a stream + */ + +function processByte (stream, b) { +  assert.equal(typeof b, 'number') +  if (b === NEWLINE) { +    stream.emit('newline') +  } +} @@ -3,7 +3,7 @@  var fs = require('fs');  var util = require('util');  var stmd = require('./stmd'); -var ansi = require('ansi') +var ansi = require('./ansi/ansi')  var cursor = ansi(process.stdout);  var writer = new stmd.HtmlRenderer();  | 
