|  | // Copyright (c) 2012 Rob Burns | 
|  | // | 
|  | // Permission is hereby granted, free of charge, to any person | 
|  | // obtaining a copy of this software and associated documentation | 
|  | // files (the "Software"), to deal in the Software without | 
|  | // restriction, including without limitation the rights to use, | 
|  | // copy, modify, merge, publish, distribute, sublicense, and/or sell | 
|  | // copies of the Software, and to permit persons to whom the | 
|  | // Software is furnished to do so, subject to the following | 
|  | // conditions: | 
|  | // | 
|  | // The above copyright notice and this permission notice shall be | 
|  | // included in all copies or substantial portions of the Software. | 
|  | // | 
|  | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | 
|  | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | 
|  | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | 
|  | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | 
|  | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | 
|  | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | 
|  | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | 
|  | // OTHER DEALINGS IN THE SOFTWARE. | 
|  |  | 
|  | // Converted from https://github.com/rburns/ansi-to-html | 
|  |  | 
|  | 'use strict'; | 
|  |  | 
|  | var _ = require('underscore'); | 
|  |  | 
|  | var defaults = { | 
|  | fg: '#FFF', | 
|  | bg: '#000', | 
|  | newline: false, | 
|  | escapeXML: false, | 
|  | stream: false, | 
|  | colors: getDefaultColors() | 
|  | }; | 
|  |  | 
|  | function getDefaultColors() { | 
|  | var colors = { | 
|  | 0: '#000', | 
|  | 1: '#A00', | 
|  | 2: '#0A0', | 
|  | 3: '#A50', | 
|  | 4: '#00A', | 
|  | 5: '#A0A', | 
|  | 6: '#0AA', | 
|  | 7: '#AAA', | 
|  | 8: '#555', | 
|  | 9: '#F55', | 
|  | 10: '#5F5', | 
|  | 11: '#FF5', | 
|  | 12: '#55F', | 
|  | 13: '#F5F', | 
|  | 14: '#5FF', | 
|  | 15: '#FFF' | 
|  | }; | 
|  |  | 
|  | range(0, 5).forEach(function (red) { | 
|  | range(0, 5).forEach(function (green) { | 
|  | range(0, 5).forEach(function (blue) { | 
|  | return setStyleColor(red, green, blue, colors); | 
|  | }); | 
|  | }); | 
|  | }); | 
|  |  | 
|  | range(0, 23).forEach(function (gray) { | 
|  | var c = gray + 232; | 
|  | var l = toHexString(gray * 10 + 8); | 
|  |  | 
|  | colors[c] = '#' + l + l + l; | 
|  | }); | 
|  |  | 
|  | return colors; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {number} red | 
|  | * @param {number} green | 
|  | * @param {number} blue | 
|  | * @param {object} colors | 
|  | */ | 
|  | function setStyleColor(red, green, blue, colors) { | 
|  | var c = 16 + red * 36 + green * 6 + blue; | 
|  | var r = red > 0 ? red * 40 + 55 : 0; | 
|  | var g = green > 0 ? green * 40 + 55 : 0; | 
|  | var b = blue > 0 ? blue * 40 + 55 : 0; | 
|  |  | 
|  | colors[c] = toColorHexString([r, g, b]); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Converts from a number like 15 to a hex string like 'F' | 
|  | * @param {number} num | 
|  | * @returns {string} | 
|  | */ | 
|  | function toHexString(num) { | 
|  | var str = num.toString(16); | 
|  |  | 
|  | while (str.length < 2) { | 
|  | str = '0' + str; | 
|  | } | 
|  |  | 
|  | return str; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Converts from an array of numbers like [15, 15, 15] to a hex string like 'FFF' | 
|  | * @param {[red, green, blue]} ref | 
|  | * @returns {string} | 
|  | */ | 
|  | function toColorHexString(ref) { | 
|  | var results = []; | 
|  |  | 
|  | for (var j = 0, len = ref.length; j < len; j++) { | 
|  | results.push(toHexString(ref[j])); | 
|  | } | 
|  |  | 
|  | return '#' + results.join(''); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {Array} stack | 
|  | * @param {string} token | 
|  | * @param {*} data | 
|  | * @param {object} options | 
|  | */ | 
|  | function generateOutput(stack, token, data, options) { | 
|  | var result; | 
|  |  | 
|  | if (token === 'text') { | 
|  | result = pushText(data, options); | 
|  | } else if (token === 'display') { | 
|  | result = handleDisplay(stack, data, options); | 
|  | } else if (token === 'xterm256') { | 
|  | result = pushForegroundColor(stack, options.colors[data]); | 
|  | } | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {Array} stack | 
|  | * @param {number} code | 
|  | * @param {object} options | 
|  | * @returns {*} | 
|  | */ | 
|  | function handleDisplay(stack, code, options) { | 
|  | code = parseInt(code, 10); | 
|  | var result; | 
|  |  | 
|  | var codeMap = { | 
|  | '-1': function _() { | 
|  | return '<br/>'; | 
|  | }, | 
|  | 0: function _() { | 
|  | return stack.length && resetStyles(stack); | 
|  | }, | 
|  | 1: function _() { | 
|  | return pushTag(stack, 'b'); | 
|  | }, | 
|  | 2: function _() { | 
|  | return pushStyle(stack, 'opacity:0.6'); | 
|  | }, | 
|  | 3: function _() { | 
|  | return pushTag(stack, 'i'); | 
|  | }, | 
|  | 4: function _() { | 
|  | return pushTag(stack, 'u'); | 
|  | }, | 
|  | 8: function _() { | 
|  | return pushStyle(stack, 'display:none'); | 
|  | }, | 
|  | 9: function _() { | 
|  | return pushTag(stack, 'strike'); | 
|  | }, | 
|  | 22: function _() { | 
|  | return closeTag(stack, 'b'); | 
|  | }, | 
|  | 23: function _() { | 
|  | return closeTag(stack, 'i'); | 
|  | }, | 
|  | 24: function _() { | 
|  | return closeTag(stack, 'u'); | 
|  | }, | 
|  | 39: function _() { | 
|  | return pushForegroundColor(stack, options.fg); | 
|  | }, | 
|  | 49: function _() { | 
|  | return pushBackgroundColor(stack, options.bg); | 
|  | } | 
|  | }; | 
|  |  | 
|  | if (codeMap[code]) { | 
|  | result = codeMap[code](); | 
|  | } else if (4 < code && code < 7) { | 
|  | result = pushTag(stack, 'blink'); | 
|  | } else if (29 < code && code < 38) { | 
|  | result = pushForegroundColor(stack, options.colors[code - 30]); | 
|  | } else if (39 < code && code < 48) { | 
|  | result = pushBackgroundColor(stack, options.colors[code - 40]); | 
|  | } else if (89 < code && code < 98) { | 
|  | result = pushForegroundColor(stack, options.colors[8 + (code - 90)]); | 
|  | } else if (99 < code && code < 108) { | 
|  | result = pushBackgroundColor(stack, options.colors[8 + (code - 100)]); | 
|  | } | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Clear all the styles | 
|  | * @returns {string} | 
|  | */ | 
|  | function resetStyles(stack) { | 
|  | var stackClone = stack.slice(0); | 
|  |  | 
|  | stack.length = 0; | 
|  |  | 
|  | return stackClone.reverse().map(function (tag) { | 
|  | return '</' + tag + '>'; | 
|  | }).join(''); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Creates an array of numbers ranging from low to high | 
|  | * @param {number} low | 
|  | * @param {number} high | 
|  | * @returns {Array} | 
|  | * @example range(3, 7); // creates [3, 4, 5, 6, 7] | 
|  | */ | 
|  | function range(low, high) { | 
|  | var results = []; | 
|  |  | 
|  | for (var j = low; j <= high; j++) { | 
|  | results.push(j); | 
|  | } | 
|  |  | 
|  | return results; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a new function that is true if value is NOT the same category | 
|  | * @param {string} category | 
|  | * @returns {function} | 
|  | */ | 
|  | function notCategory(category) { | 
|  | return function (e) { | 
|  | return (category === null || e.category !== category) && category !== 'all'; | 
|  | }; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Converts a code into an ansi token type | 
|  | * @param {number} code | 
|  | * @returns {string} | 
|  | */ | 
|  | function categoryForCode(code) { | 
|  | code = parseInt(code, 10); | 
|  | var result = null; | 
|  |  | 
|  | if (code === 0) { | 
|  | result = 'all'; | 
|  | } else if (code === 1) { | 
|  | result = 'bold'; | 
|  | } else if (2 < code && code < 5) { | 
|  | result = 'underline'; | 
|  | } else if (4 < code && code < 7) { | 
|  | result = 'blink'; | 
|  | } else if (code === 8) { | 
|  | result = 'hide'; | 
|  | } else if (code === 9) { | 
|  | result = 'strike'; | 
|  | } else if (29 < code && code < 38 || code === 39 || 89 < code && code < 98) { | 
|  | result = 'foreground-color'; | 
|  | } else if (39 < code && code < 48 || code === 49 || 99 < code && code < 108) { | 
|  | result = 'background-color'; | 
|  | } | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} text | 
|  | * @param {object} options | 
|  | * @returns {string} | 
|  | */ | 
|  | function pushText(text, options) { | 
|  | if (options.escapeXML) { | 
|  | return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); | 
|  | } | 
|  |  | 
|  | return text; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {Array} stack | 
|  | * @param {string} tag | 
|  | * @param {string} [style=''] | 
|  | * @returns {string} | 
|  | */ | 
|  | function pushTag(stack, tag, style) { | 
|  | if (!style) { | 
|  | style = ''; | 
|  | } | 
|  |  | 
|  | stack.push(tag); | 
|  |  | 
|  | return ['<' + tag, style ? ' style="' + style + '"' : void 0, '>'].join(''); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {Array} stack | 
|  | * @param {string} style | 
|  | * @returns {string} | 
|  | */ | 
|  | function pushStyle(stack, style) { | 
|  | return pushTag(stack, 'span', style); | 
|  | } | 
|  |  | 
|  | function pushForegroundColor(stack, color) { | 
|  | return pushTag(stack, 'span', 'color:' + color); | 
|  | } | 
|  |  | 
|  | function pushBackgroundColor(stack, color) { | 
|  | return pushTag(stack, 'span', 'background-color:' + color); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {Array} stack | 
|  | * @param {string} style | 
|  | * @returns {string} | 
|  | */ | 
|  | function closeTag(stack, style) { | 
|  | var last; | 
|  |  | 
|  | if (stack.slice(-1)[0] === style) { | 
|  | last = stack.pop(); | 
|  | } | 
|  |  | 
|  | if (last) { | 
|  | return '</' + style + '>'; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} text | 
|  | * @param {object} options | 
|  | * @param {function} callback | 
|  | * @returns {Array} | 
|  | */ | 
|  | function tokenize(text, options, callback) { | 
|  | var ansiMatch = false; | 
|  | var ansiHandler = 3; | 
|  |  | 
|  | function remove() { | 
|  | return ''; | 
|  | } | 
|  |  | 
|  | function removeXterm256(m, g1) { | 
|  | callback('xterm256', g1); | 
|  | return ''; | 
|  | } | 
|  |  | 
|  | function newline(m) { | 
|  | if (options.newline) { | 
|  | callback('display', -1); | 
|  | } else { | 
|  | callback('text', m); | 
|  | } | 
|  |  | 
|  | return ''; | 
|  | } | 
|  |  | 
|  | function ansiMess(m, g1) { | 
|  | ansiMatch = true; | 
|  | if (g1.trim().length === 0) { | 
|  | g1 = '0'; | 
|  | } | 
|  |  | 
|  | g1 = g1.replace(/;+$/, "").split(';'); | 
|  |  | 
|  | for (var o = 0, len = g1.length; o < len; o++) { | 
|  | callback('display', g1[o]); | 
|  | } | 
|  |  | 
|  | return ''; | 
|  | } | 
|  |  | 
|  | function realText(m) { | 
|  | callback('text', m); | 
|  |  | 
|  | return ''; | 
|  | } | 
|  |  | 
|  | /* eslint no-control-regex:0 */ | 
|  | var tokens = [{ | 
|  | pattern: /^\x08+/, | 
|  | sub: remove | 
|  | }, { | 
|  | pattern: /^\x1b\[[012]?K/, | 
|  | sub: remove | 
|  | }, { | 
|  | pattern: /^\x1b\[38;5;(\d+)m/, | 
|  | sub: removeXterm256 | 
|  | }, { | 
|  | pattern: /^\n/, | 
|  | sub: newline | 
|  | }, { | 
|  | pattern: /^\x1b\[((?:\d{1,3};?)+|)m/, | 
|  | sub: ansiMess | 
|  | }, { | 
|  | pattern: /^\x1b\[?[\d;]{0,3}/, | 
|  | sub: remove | 
|  | }, { | 
|  | pattern: /^([^\x1b\x08\n]+)/, | 
|  | sub: realText | 
|  | }]; | 
|  |  | 
|  | function process(handler, i) { | 
|  | if (i > ansiHandler && ansiMatch) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | ansiMatch = false; | 
|  |  | 
|  | text = text.replace(handler.pattern, handler.sub); | 
|  | } | 
|  |  | 
|  | var handler; | 
|  | var results1 = []; | 
|  | var length = text.length; | 
|  |  | 
|  | outer: while (length > 0) { | 
|  | for (var i = 0, o = 0, len = tokens.length; o < len; i = ++o) { | 
|  | handler = tokens[i]; | 
|  | process(handler, i); | 
|  |  | 
|  | if (text.length !== length) { | 
|  | // We matched a token and removed it from the text. We need to | 
|  | // start matching *all* tokens against the new text. | 
|  | length = text.length; | 
|  | continue outer; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (text.length === length) { | 
|  | break; | 
|  | } else { | 
|  | results1.push(0); | 
|  | } | 
|  |  | 
|  | length = text.length; | 
|  | } | 
|  |  | 
|  | return results1; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * If streaming, then the stack is "sticky" | 
|  | * | 
|  | * @param {Array} stickyStack | 
|  | * @param {string} token | 
|  | * @param {*} data | 
|  | * @returns {Array} | 
|  | */ | 
|  | function updateStickyStack(stickyStack, token, data) { | 
|  | if (token !== 'text') { | 
|  | stickyStack = stickyStack.filter(notCategory(categoryForCode(data))); | 
|  | stickyStack.push({token: token, data: data, category: categoryForCode(data)}); | 
|  | } | 
|  |  | 
|  | return stickyStack; | 
|  | } | 
|  |  | 
|  | function Filter(options) { | 
|  | options = options || {}; | 
|  |  | 
|  | if (options.colors) { | 
|  | options.colors = _.extend(defaults.colors, options.colors); | 
|  | } | 
|  | this.opts = _.extend({}, defaults, options); | 
|  | this.stack = []; | 
|  | this.stickyStack = []; | 
|  | } | 
|  |  | 
|  | Filter.prototype = { | 
|  | toHtml: function toHtml(input) { | 
|  | var _this = this; | 
|  |  | 
|  | input = typeof input === 'string' ? [input] : input; | 
|  | var stack = this.stack; | 
|  | var options = this.opts; | 
|  | var buf = []; | 
|  |  | 
|  | this.stickyStack.forEach(function (element) { | 
|  | var output = generateOutput(stack, element.token, element.data, options); | 
|  |  | 
|  | if (output) { | 
|  | buf.push(output); | 
|  | } | 
|  | }); | 
|  |  | 
|  | tokenize(input.join(''), options, function (token, data) { | 
|  | var output = generateOutput(stack, token, data, options); | 
|  |  | 
|  | if (output) { | 
|  | buf.push(output); | 
|  | } | 
|  |  | 
|  | if (options.stream) { | 
|  | _this.stickyStack = updateStickyStack(_this.stickyStack, token, data); | 
|  | } | 
|  | }); | 
|  |  | 
|  | if (stack.length) { | 
|  | buf.push(resetStyles(stack)); | 
|  | } | 
|  |  | 
|  | return buf.join(''); | 
|  | } | 
|  | }; | 
|  |  | 
|  | module.exports = Filter; |