| // 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'); |
| }, |
| 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; |