ansidec
Version:
Limited Unix ANSI escape sequences transformer for use in Browsers
782 lines (765 loc) • 34.3 kB
JavaScript
/**@license
* ▄████████ ███▄▄▄▄ ▄████████ ▄█ ████████▄ ▄████████ ▄████████
* ███ ███ ███▀▀▀██▄ ███ ███ ███ ███ ▀███ ███ ███ ███ ███
* ███ ███ ███ ███ ███ █▀ ███▌ ███ ███ ███ █▀ ███ █▀
* ███ ███ ███ ███ ███ ███▌ ███ ███ ▄███▄▄▄ ███
* ▀███████████ ███ ███ ▀███████████ ███▌ ███ ███ ▀▀███▀▀▀ ███
* ███ ███ ███ ███ ███ ███ ███ ███ ███ █▄ ███ █▄
* ███ ███ ███ ███ ▄█ ███ ███ ███ ▄███ ███ ███ ███ ███
* ███ █▀ ▀█ █▀ ▄████████▀ █▀ ████████▀ ██████████ ████████▀
* v. 0.3.4
*
* Copyright (c) 2018-2019 Jakub T. Jankiewicz <https://jcubic.pl/me>
* Released under the MIT license
*
* Contains: SAUCE parser from ansilove.js MIT license
*
* Based on jQuery Terminal's unix formatting
*
*/
/* global define, global, module, require, Uint8Array */
(function(factory) {
var root = typeof window !== 'undefined' ? window : global;
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
// istanbul ignore next
define([], function() {
factory(window.TextEncoder);
});
} else if (typeof module === 'object' && module.exports) {
// Node/CommonJS
module.exports = factory(require('util').TextEncoder);
} else {
// Browser
// istanbul ignore next
root.ansi = factory(window.TextEncoder);
}
})(function(TextEncoder, undefined) {
// we match characters and html entities because command line escape brackets
// echo don't, when writing formatter always process html entitites so it work
// for cmd plugin as well for echo
var chr = '[^\\x08]|[\\r\\n]{2}|&[^;]+;';
var backspace_re = new RegExp('^(' + chr + ')?\\x08');
var overtyping_re = new RegExp('^(?:(' + chr + ')?\\x08(_|\\1)|' +
'(_)\\x08(' + chr + '))');
var new_line_re = /^(\r\n|\n\r|\r|\n)/;
var clear_line_re = /[^\r\n]+\r\x1B\[K/g;
// ---------------------------------------------------------------------
// :: Replace overtyping (from man) formatting with terminal formatting
// :: it also handle any backspaces
// ---------------------------------------------------------------------
function overtyping(callback, string) {
var removed_chars = [];
var new_position;
var char_count = 0;
var backspaces = [];
function replace(string, position) {
var result = '';
var push = 0;
var start;
char_count = 0;
function correct_position(start, match, rep_string) {
// logic taken from $.terminal.tracking_replace
if (start < position) {
var last_index = start + match.length;
if (last_index < position) {
// It's after the replacement, move it
new_position = Math.max(
0,
new_position +
rep_string.length -
match.length
);
} else {
// It's *in* the replacement, put it just after
new_position += rep_string.length - (position - start);
}
}
}
for (var i = 0; i < string.length; ++i) {
var partial = string.substring(i);
var match = partial.match(backspace_re);
var removed_char = removed_chars[0];
if (match) {
// we remove backspace and character or html entity before it
// but we keep it in removed array so we can put it back
// when we have caritage return or line feed
if (match[1]) {
start = i - match[1].length + push;
removed_chars.push({
index: start,
string: match[1],
overtyping: partial.match(overtyping_re)
});
correct_position(start, match[0], '', 1);
}
if (char_count < 0) {
char_count = 0;
}
backspaces = backspaces.map(function(b) {
return b - 1;
});
backspaces.push(start);
return result + partial.replace(backspace_re, '');
} else if (partial.match(new_line_re)) {
// if newline we need to add at the end all characters
// removed by backspace but only if there are no more
// other characters than backspaces added between
// backspaces and newline
if (removed_chars.length) {
var chars = removed_chars;
removed_chars = [];
chars.reverse().forEach(function(char) {
if (i > char.index) {
if (--char_count <= 0) {
correct_position(char.index, '', char.string, 2);
result += char.string;
}
} else {
removed_chars.unshift(char);
}
});
}
var m = partial.match(new_line_re);
result += m[1];
i += m[1].length - 1;
} else {
if (backspaces.length) {
var backspace = backspaces[0];
if (i === backspace) {
backspaces.shift();
}
if (i >= backspace) {
char_count++;
}
}
if (removed_chars.length) {
// if we are in index of removed character we check if the
// character is the same it will be bold or if removed char
// or char at index is underscore then it will
// be terminal formatting with underscore
if (i > removed_char.index && removed_char.overtyping) {
removed_chars.shift();
correct_position(removed_char.index, '', removed_char.string);
// if we add special character we need to correct
// next push to removed_char array
push++;
// we use special characters instead of terminal
// formatting so it's easier to proccess when removing
// backspaces
if (removed_char.string === string[i]) {
result += string[i] + '\uFFF1';
continue;
} else if (removed_char.string === '_' ||
string[i] === '_') {
var chr;
if (removed_char.string === '_') {
chr = string[i];
} else {
chr = removed_char.string;
}
result += chr + '\uFFF2';
continue;
}
}
}
result += string[i];
}
}
return result;
}
var break_next = false;
// we need to clear line \x1b[K in overtyping because it need to be before
// overtyping and from_ansi need to be called after so it escape stuff
// between Escape Code and cmd will have escaped formatting typed by user
string = string.replace(clear_line_re, '');
// loop until not more backspaces
while (string.match(/\x08/) || removed_chars.length) {
string = replace(string, new_position);
if (break_next) {
break;
}
if (!string.match(/\x08/)) {
// we break the loop so if removed_chars still chave items
// we don't have infite loop
break_next = true;
}
}
function format(string, chr, style) {
var re = new RegExp('((:?.' + chr + ')+)', 'g');
return string.replace(re, function(_, string) {
var re = new RegExp(chr, 'g');
return callback(style, null, null, string.replace(re, ''));
});
}
// replace special characters with terminal formatting
if (typeof callback === 'function') {
string = format(string, '\uFFF1', {bold: true});
string = format(string, '\uFFF2', {underline: true});
}
return string;
}
// ---------------------------------------------------------------------
// :: Html colors taken from ANSI formatting in Linux Terminal
// ---------------------------------------------------------------------
var ansi_colors = {
normal: {
black: '#000',
red: '#A00',
green: '#008400',
yellow: '#A50',
blue: '#00A',
magenta: '#A0A',
cyan: '#0AA',
white: '#AAA'
},
faited: {
black: '#000',
red: '#640000',
green: '#006100',
yellow: '#737300',
blue: '#000087',
magenta: '#650065',
cyan: '#008787',
white: '#818181'
},
bold: {
black: '#444',
red: '#F55',
green: '#44D544',
yellow: '#FF5',
blue: '#55F',
magenta: '#F5F',
cyan: '#5FF',
white: '#FFF'
},
// XTerm 8-bit pallete
palette: [
'#000000', '#AA0000', '#00AA00', '#AA5500', '#0000AA', '#AA00AA',
'#00AAAA', '#AAAAAA', '#555555', '#FF5555', '#55FF55', '#FFFF55',
'#5555FF', '#FF55FF', '#55FFFF', '#FFFFFF', '#000000', '#00005F',
'#000087', '#0000AF', '#0000D7', '#0000FF', '#005F00', '#005F5F',
'#005F87', '#005FAF', '#005FD7', '#005FFF', '#008700', '#00875F',
'#008787', '#0087AF', '#0087D7', '#0087FF', '#00AF00', '#00AF5F',
'#00AF87', '#00AFAF', '#00AFD7', '#00AFFF', '#00D700', '#00D75F',
'#00D787', '#00D7AF', '#00D7D7', '#00D7FF', '#00FF00', '#00FF5F',
'#00FF87', '#00FFAF', '#00FFD7', '#00FFFF', '#5F0000', '#5F005F',
'#5F0087', '#5F00AF', '#5F00D7', '#5F00FF', '#5F5F00', '#5F5F5F',
'#5F5F87', '#5F5FAF', '#5F5FD7', '#5F5FFF', '#5F8700', '#5F875F',
'#5F8787', '#5F87AF', '#5F87D7', '#5F87FF', '#5FAF00', '#5FAF5F',
'#5FAF87', '#5FAFAF', '#5FAFD7', '#5FAFFF', '#5FD700', '#5FD75F',
'#5FD787', '#5FD7AF', '#5FD7D7', '#5FD7FF', '#5FFF00', '#5FFF5F',
'#5FFF87', '#5FFFAF', '#5FFFD7', '#5FFFFF', '#870000', '#87005F',
'#870087', '#8700AF', '#8700D7', '#8700FF', '#875F00', '#875F5F',
'#875F87', '#875FAF', '#875FD7', '#875FFF', '#878700', '#87875F',
'#878787', '#8787AF', '#8787D7', '#8787FF', '#87AF00', '#87AF5F',
'#87AF87', '#87AFAF', '#87AFD7', '#87AFFF', '#87D700', '#87D75F',
'#87D787', '#87D7AF', '#87D7D7', '#87D7FF', '#87FF00', '#87FF5F',
'#87FF87', '#87FFAF', '#87FFD7', '#87FFFF', '#AF0000', '#AF005F',
'#AF0087', '#AF00AF', '#AF00D7', '#AF00FF', '#AF5F00', '#AF5F5F',
'#AF5F87', '#AF5FAF', '#AF5FD7', '#AF5FFF', '#AF8700', '#AF875F',
'#AF8787', '#AF87AF', '#AF87D7', '#AF87FF', '#AFAF00', '#AFAF5F',
'#AFAF87', '#AFAFAF', '#AFAFD7', '#AFAFFF', '#AFD700', '#AFD75F',
'#AFD787', '#AFD7AF', '#AFD7D7', '#AFD7FF', '#AFFF00', '#AFFF5F',
'#AFFF87', '#AFFFAF', '#AFFFD7', '#AFFFFF', '#D70000', '#D7005F',
'#D70087', '#D700AF', '#D700D7', '#D700FF', '#D75F00', '#D75F5F',
'#D75F87', '#D75FAF', '#D75FD7', '#D75FFF', '#D78700', '#D7875F',
'#D78787', '#D787AF', '#D787D7', '#D787FF', '#D7AF00', '#D7AF5F',
'#D7AF87', '#D7AFAF', '#D7AFD7', '#D7AFFF', '#D7D700', '#D7D75F',
'#D7D787', '#D7D7AF', '#D7D7D7', '#D7D7FF', '#D7FF00', '#D7FF5F',
'#D7FF87', '#D7FFAF', '#D7FFD7', '#D7FFFF', '#FF0000', '#FF005F',
'#FF0087', '#FF00AF', '#FF00D7', '#FF00FF', '#FF5F00', '#FF5F5F',
'#FF5F87', '#FF5FAF', '#FF5FD7', '#FF5FFF', '#FF8700', '#FF875F',
'#FF8787', '#FF87AF', '#FF87D7', '#FF87FF', '#FFAF00', '#FFAF5F',
'#FFAF87', '#FFAFAF', '#FFAFD7', '#FFAFFF', '#FFD700', '#FFD75F',
'#FFD787', '#FFD7AF', '#FFD7D7', '#FFD7FF', '#FFFF00', '#FFFF5F',
'#FFFF87', '#FFFFAF', '#FFFFD7', '#FFFFFF', '#080808', '#121212',
'#1C1C1C', '#262626', '#303030', '#3A3A3A', '#444444', '#4E4E4E',
'#585858', '#626262', '#6C6C6C', '#767676', '#808080', '#8A8A8A',
'#949494', '#9E9E9E', '#A8A8A8', '#B2B2B2', '#BCBCBC', '#C6C6C6',
'#D0D0D0', '#DADADA', '#E4E4E4', '#EEEEEE'
]
};
var from_ansi = (function() {
var color_list = {
30: 'black',
31: 'red',
32: 'green',
33: 'yellow',
34: 'blue',
35: 'magenta',
36: 'cyan',
37: 'white',
39: 'inherit' // default color
};
var background_list = {
40: 'black',
41: 'red',
42: 'green',
43: 'yellow',
44: 'blue',
45: 'magenta',
46: 'cyan',
47: 'white',
49: 'transparent' // default background
};
function format_ansi(code, state) {
var controls = code.split(';');
var num;
var styles = [];
var output_color = '';
var output_background = '';
var _process_true_color = -1;
var _ex_color = false;
var _ex_background = false;
var _process_8bit = false;
var palette = ansi_colors.palette;
function set_styles(num) {
switch (num) {
case 0:
Object.keys(state).forEach(function(key) {
delete state[key];
});
state.bold = false;
state.faited = false;
break;
case 1:
styles.bold = state.bold = true;
state.faited = false;
break;
case 4:
styles.underline = state.underline = true;
break;
case 3:
styles.italic = state.italic = true;
break;
case 5:
if (_ex_color || _ex_background) {
_process_8bit = true;
}
break;
case 38:
_ex_color = true;
break;
case 48:
_ex_background = true;
break;
case 2:
if (_ex_color || _ex_background) {
_process_true_color = 0;
} else {
state.faited = true;
state.bold = false;
}
break;
case 7:
state.reverse = true;
break;
default:
if (controls[1] !== '5') {
if (color_list[num]) {
output_color = color_list[num];
}
if (background_list[num]) {
output_background = background_list[num];
}
}
}
}
// -----------------------------------------------------------------
function process_true_color() {
if (_ex_color) {
if (!output_color) {
output_color = '#';
}
if (output_color.length < 7) {
output_color += ('0' + num.toString(16)).slice(-2);
}
}
if (_ex_background) {
if (!output_background) {
output_background = '#';
}
if (output_background.length < 7) {
output_background += ('0' + num.toString(16)).slice(-2);
}
}
if (_process_true_color === 2) {
_process_true_color = -1;
} else {
_process_true_color++;
}
}
// -----------------------------------------------------------------
function should__process_8bit() {
return _process_8bit && ((_ex_background && !output_background) ||
(_ex_color && !output_color));
}
// -----------------------------------------------------------------
function process_8bit() {
if (_ex_color && palette[num] && !output_color) {
output_color = palette[num];
}
if (_ex_background && palette[num] && !output_background) {
output_background = palette[num];
}
_process_8bit = false;
}
// -----------------------------------------------------------------
for (var i in controls) {
if (controls.hasOwnProperty(i)) {
num = parseInt(controls[i], 10);
if (_process_true_color > -1) {
process_true_color();
} else if (should__process_8bit()) {
process_8bit();
} else {
set_styles(num);
}
}
}
if (state.reverse) {
if (output_color || output_background) {
var tmp = output_background;
output_background = output_color;
output_color = tmp;
} else {
output_color = 'black';
output_background = 'white';
}
}
output_color = output_color || state.color;
output_background = output_background || state.background;
state.background = output_background;
state.color = output_color;
var colors, color, background;
if (state.bold) {
colors = ansi_colors.bold;
} else if (state.faited) {
colors = ansi_colors.faited;
} else {
colors = ansi_colors.normal;
}
if (typeof output_color !== 'undefined') {
if (output_color.match(/^#/)) {
color = output_color;
} else if (output_color === 'inherit') {
color = output_color;
} else {
color = colors[output_color];
}
}
if (typeof output_background !== 'undefined') {
if (output_background.match(/^#/)) {
background = output_background;
} else if (output_background === 'transparent') {
background = output_background;
} else {
background = ansi_colors.normal[output_background];
}
}
var ret = [styles, color, background];
return ret;
}
return function from_ansi(callback, input) {
var state = {}; // used to inherit vales from previous formatting
var ansi_re = /(\x1B\[[0-9;]*[A-Za-z])/g;
var cursor_re = /(.*)\r?\n\x1b\[1A\x1b\[([0-9]+)C/g;
// move up and right we need to delete what's after in previous line
input = input.replace(cursor_re, function(_, line, n) {
n = parseInt(n, 10);
var parts = line.split(ansi_re).filter(Boolean);
var result = [];
for (var i = 0; i < parts.length; ++i) {
if (parts[i].match(ansi_re)) {
result.push(parts[i]);
} else {
var len = parts[i].length;
if (len > n) {
result.push(parts[i].substring(0, n));
break;
} else {
result.push(parts[i]);
}
n -= len;
}
}
return result.join('');
});
// move right is just repate space
input = input.replace(/\x1b\[([0-9]+)C/g, function(_, num) {
return new Array(+num + 1).join(' ');
});
var splitted = input.split(ansi_re);
if (splitted.length === 1) {
return input;
}
var output = [];
//skip closing at the begining
if (splitted.length > 3) {
var str = splitted.slice(0, 3).join('');
if (str.match(/^\[0*m$/)) {
splitted = splitted.slice(3);
}
}
var code, match, inside = false;
for (var i = 0; i < splitted.length; ++i) {
match = splitted[i].match(/^\x1B\[([0-9;]*)([A-Za-z])$/);
if (match) {
switch (match[2]) {
case 'm':
code = format_ansi(match[1], state);
if (inside) {
if (+match[1] === 0) {
inside = false;
output.push(false);
} else {
output.push(code);
}
} else if (+match[1] !== 0) {
output.push(code);
inside = true;
}
break;
}
} else {
output.push(splitted[i].replace(/\x1b\[[0-9;]*/g, ''));
}
}
var formatting;
return output.reduce(function(acc, obj) {
if (typeof obj === 'string') {
if (formatting && obj) {
var args = formatting.concat([obj]);
if (typeof callback === 'function') {
return acc + callback.apply(null, args);
}
}
formatting = null;
return acc + obj;
} else {
formatting = obj;
return acc;
}
}, '');
};
})();
// -------------------------------------------------------------------------
function format(callback, text) {
if (text === undefined) {
return function(text) {
return format(callback, text);
};
}
return from_ansi(callback, overtyping(callback, text));
}
// -------------------------------------------------------------------------
function html(text) {
return format(function(styles, color, background, text) {
var style = [];
if (color) {
style.push('color:' + color);
}
if (background) {
style.push('background:' + background);
}
if (styles.bold) {
style.push('font-weight:bold');
}
if (styles.italic) {
style.push('font-style:italic');
}
if (styles.underline) {
styles.push('text-decoration:underline');
}
return '<span style="' + style.join(';') + '">' + text + '</span>';
}, text);
}
// -------------------------------------------------------------------------
// :: SAUSE parser
// :: http://www.acid.org/info/sauce/sauce.htm
// :: source: https://github.com/nvdnkpr/ansilove.js
// -------------------------------------------------------------------------
// This function returns an object that emulates basic file-handling when
// fed an array of bytes.
// -------------------------------------------------------------------------
function File(bytes) {
// pos points to the current position in the 'file'. SAUCE_ID, COMNT_ID,
// and commentCount are used later when parsing the SAUCE record.
var pos, SAUCE_ID, COMNT_ID, commentCount;
// Raw Bytes for "SAUCE" and "COMNT", used when parsing SAUCE records.
SAUCE_ID = new Uint8Array([0x53, 0x41, 0x55, 0x43, 0x45]);
COMNT_ID = new Uint8Array([0x43, 0x4F, 0x4D, 0x4E, 0x54]);
// Returns an 8-bit byte at the current byte position, <pos>. Also
// advances <pos> by a single byte. Throws an error if we advance beyond
// the length of the array.
this.get = function() {
if (pos >= bytes.length) {
throw new Error("Unexpected end of file reached.");
}
return bytes[pos++];
};
// Same as get(), but returns a 16-bit byte. Also advances <pos>
// by two (8-bit) bytes.
this.get16 = function() {
var v;
v = this.get();
return v + (this.get() << 8);
};
// Same as get(), but returns a 32-bit byte. Also advances <pos>
// by four (8-bit) bytes.
this.get32 = function() {
var v;
v = this.get();
v += this.get() << 8;
v += this.get() << 16;
return v + (this.get() << 24);
};
// Exactly the same as get(), but returns a character symbol,
// instead of the value. e.g. 65 = "A".
this.getC = function() {
return String.fromCharCode(this.get());
};
// Returns a string of <num> characters at the current file position,
// and strips the trailing whitespace characters.
// Advances <pos> by <num> by calling getC().
this.getS = function(num) {
var string;
string = "";
while (num-- > 0) {
string += this.getC();
}
return string.replace(/[\x00\s]+$/, '');
};
// Returns "true" if, at the current <pos>, a string of characters
// matches <match>. Does not increment <pos>.
this.lookahead = function(match) {
if (SAUCE_ID === match) {
//debugger;
}
var i;
for (i = 0; i < match.length; ++i) {
if ((pos + i === bytes.length) || (bytes[pos + i] !== match[i])) {
break;
}
}
return i === match.length;
};
// Returns an array of <num> bytes found at the current <pos>. Also
// increments <pos>.
this.read = function(num) {
var t;
t = pos;
// If num is undefined, return all the bytes until the end of file.
num = num || this.size - pos;
while (++pos < this.size) {
if (--num === 0) {
break;
}
}
return bytes.subarray(t, pos);
};
// Sets a new value for <pos>. Equivalent to seeking a file to a new position.
this.seek = function(newPos) {
pos = newPos;
};
// Returns the value found at <pos>, without incrementing <pos>.
this.peek = function(num) {
num = num || 0;
return bytes[pos + num];
};
// Returns the the current position being read in the file, in amount
// of bytes. i.e. <pos>.
this.getPos = function() {
return pos;
};
// Returns true if the end of file has been reached. <this.size> is set
// later by the SAUCE parsing section, as it is not always the same
// value as the length of <bytes>. (In case there is a SAUCE record,
// and optional comments).
this.eof = function() {
return pos === this.size;
};
// Seek to the position we would expect to find a SAUCE record.
pos = bytes.length - 128;
// If we find "SAUCE".
if (this.lookahead(SAUCE_ID)) {
this.sauce = {};
// Read "SAUCE".
this.getS(5);
// Read and store the various SAUCE values.
this.sauce.version = this.getS(2); // String, maximum of 2 characters
this.sauce.title = this.getS(35); // String, maximum of 35 characters
this.sauce.author = this.getS(20); // String, maximum of 20 characters
this.sauce.group = this.getS(20); // String, maximum of 20 characters
this.sauce.date = this.getS(8); // String, maximum of 8 characters
this.sauce.fileSize = this.get32(); // unsigned 32-bit
this.sauce.dataType = this.get(); // unsigned 8-bit
this.sauce.fileType = this.get(); // unsigned 8-bit
this.sauce.tInfo = [];
this.sauce.tInfo.push(this.get16()); // unsigned 16-bit
this.sauce.tInfo.push(this.get16()); // unsigned 16-bit
this.sauce.tInfo.push(this.get16()); // unsigned 16-bit
this.sauce.tInfo.push(this.get16()); // unsigned 16-bit
// Initialize the comments array.
this.sauce.comments = [];
commentCount = this.get(); // unsigned 8-bit
this.sauce.flags = this.get(); // unsigned 8-bit
this.sauce.zstring = this.getS(22);
if (commentCount > 0) {
// If we have a value for the comments amount, seek to the position
// we'd expect to find them...
pos = bytes.length - 128 - (commentCount * 64) - 5;
// ... and check that we find a COMNT header.
if (this.lookahead(COMNT_ID)) {
// Read COMNT ...
this.getS(5);
// ... and push everything we find after that into
// our <this.sauce.comments> array, in 64-byte chunks,
// stripping the trailing whitespace in the getS() function.
while (commentCount-- > 0) {
this.sauce.comments.push(this.getS(64));
}
}
}
}
// Seek back to the start of the file, ready for reading.
pos = 0;
if (this.sauce) {
// If we have found a SAUCE record, and the fileSize field passes
// some basic sanity checks...
if (this.sauce.fileSize > 0 && this.sauce.fileSize < bytes.length) {
// Set <this.size> to the value set in SAUCE.
this.size = this.sauce.fileSize;
} else {
// If it fails the sanity checks, just assume that SAUCE record
// can't be trusted, and set <this.size> to the position
// where the SAUCE record begins.
this.size = bytes.length - 128;
}
} else {
// If there is no SAUCE record, assume that everything
// in <bytes> relates to an image.
this.size = bytes.length;
}
}
// -------------------------------------------------------------------------
function sause(arg) {
var uint_a;
if (typeof arg === 'string') {
uint_a = new Uint8Array(new TextEncoder('utf8').encode(arg));
} else {
uint_a = arg;
}
var f = new File(uint_a);
return f.sauce;
}
// -------------------------------------------------------------------------
return {
version: '0.3.4',
meta: sause,
format: format,
html: html,
colors: ansi_colors
};
});