Skip to content

Commit

Permalink
Re-factor ANSI color handling
Browse files Browse the repository at this point in the history
The implementation is based on Python code from
nbconvert.filters.ansi2html().

Among other things, this fixes #988.
  • Loading branch information
mgeier committed Mar 21, 2016
1 parent f3e2b02 commit 7bee6c5
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 144 deletions.
340 changes: 199 additions & 141 deletions notebook/static/base/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
define([
'codemirror/lib/codemirror',
'moment',
'underscore',
// silently upgrades CodeMirror
'codemirror/mode/meta',
], function(CodeMirror, moment){
], function(CodeMirror, moment, _){
"use strict";

// keep track of which extensions have been loaded already
Expand Down Expand Up @@ -192,165 +193,222 @@ define([
return uuid;
};


//Fix raw text to parse correctly in crazy XML
function xmlencode(string) {
return string.replace(/\&/g,'&'+'amp;')
.replace(/</g,'&'+'lt;')
.replace(/>/g,'&'+'gt;')
.replace(/\'/g,'&'+'apos;')
.replace(/\"/g,'&'+'quot;')
.replace(/`/g,'&'+'#96;');
var _ANSI_COLORS = [
"ansi-black",
"ansi-red",
"ansi-green",
"ansi-yellow",
"ansi-blue",
"ansi-magenta",
"ansi-cyan",
"ansi-white",
"ansi-black-intense",
"ansi-red-intense",
"ansi-green-intense",
"ansi-yellow-intense",
"ansi-blue-intense",
"ansi-magenta-intense",
"ansi-cyan-intense",
"ansi-white-intense",
];

function _parseNumbers(text) {
var numbers = text.split(";");
numbers = numbers.map(text => text ? Number.parseInt(text) : 0);
if (numbers.some(Number.isNaN)) {
numbers = []; // Ignored: Invalid color specification
}
return numbers;
}


//Map from terminal commands to CSS classes
var ansi_colormap = {
"01":"ansibold",

"30":"ansiblack",
"31":"ansired",
"32":"ansigreen",
"33":"ansiyellow",
"34":"ansiblue",
"35":"ansipurple",
"36":"ansicyan",
"37":"ansigray",

"40":"ansibgblack",
"41":"ansibgred",
"42":"ansibggreen",
"43":"ansibgyellow",
"44":"ansibgblue",
"45":"ansibgpurple",
"46":"ansibgcyan",
"47":"ansibggray"
};

function _process_numbers(attrs, numbers) {
// process ansi escapes
function _getExtendedColors(numbers) {
var r, g, b;
var n = numbers.shift();
if (ansi_colormap[n]) {
if ( ! attrs["class"] ) {
attrs["class"] = ansi_colormap[n];
} else {
attrs["class"] += " " + ansi_colormap[n];
}
} else if (n == "38" || n == "48") {
// VT100 256 color or 24 bit RGB
if (numbers.length < 2) {
console.log("Not enough fields for VT100 color", numbers);
return;
if (n === 2 && numbers.length >= 3) {
// 24-bit RGB
r = numbers.shift();
g = numbers.shift();
b = numbers.shift();
if ([r, g, b].some(c => c < 0 || 255 < c)) {
throw new RangeError();
}

var index_or_rgb = numbers.shift();
var r,g,b;
if (index_or_rgb == "5") {
// 256 color
var idx = parseInt(numbers.shift(), 10);
if (idx < 16) {
// indexed ANSI
// ignore bright / non-bright distinction
idx = idx % 8;
var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
if ( ! attrs["class"] ) {
attrs["class"] = ansiclass;
} else {
attrs["class"] += " " + ansiclass;
}
return;
} else if (idx < 232) {
// 216 color 6x6x6 RGB
idx = idx - 16;
b = idx % 6;
g = Math.floor(idx / 6) % 6;
r = Math.floor(idx / 36) % 6;
// convert to rgb
r = (r * 51);
g = (g * 51);
b = (b * 51);
} else {
// grayscale
idx = idx - 231;
// it's 1-24 and should *not* include black or white,
// so a 26 point scale
r = g = b = Math.floor(idx * 256 / 26);
}
} else if (index_or_rgb == "2") {
// Simple 24 bit RGB
if (numbers.length > 3) {
console.log("Not enough fields for RGB", numbers);
return;
}
r = numbers.shift();
g = numbers.shift();
b = numbers.shift();
} else if (n === 5 && numbers.length >= 1) {
// 256 colors
var idx = numbers.shift();
if (idx < 0) {
throw new RangeError();
} else if (idx < 16) {
// 16 default terminal colors
return idx;
} else if (idx < 232) {
// 6x6x6 color cube, see http://stackoverflow.com/a/27165165/500098
r = Math.floor((idx - 16) / 36);
r = r > 0 ? 55 + r * 40 : 0;
g = Math.floor(((idx - 16) % 36) / 6);
g = g > 0 ? 55 + g * 40 : 0;
b = (idx - 16) % 6;
b = b > 0 ? 55 + b * 40 : 0;
} else if (idx < 256) {
// grayscale, see http://stackoverflow.com/a/27165165/500098
r = g = b = (idx - 232) * 10 + 8;
} else {
console.log("unrecognized control", numbers);
return;
}
if (r !== undefined) {
// apply the rgb color
var line;
if (n == "38") {
line = "color: ";
} else {
line = "background-color: ";
}
line = line + "rgb(" + r + "," + g + "," + b + ");";
if ( !attrs.style ) {
attrs.style = line;
} else {
attrs.style += " " + line;
}
throw new RangeError();
}
} else {
throw new RangeError();
}
return [r, g, b];
}

function _ansispan(str) {
// ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
// regular ansi escapes (using the table above)
var is_open = false;
return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
if (!pattern || prefix === '39') {
// [(01|22|39|)m close spans
if (is_open) {
is_open = false;
return "</span>";
} else {
return "";
}
var ansi_re = /\x1b\[(.*?)([@-~])/g;
var fg = [];
var bg = [];
var bold = false;
var match;
var out = [];
var numbers = [];
var start = 0;

str += "\x1b[m"; // Ensure markup for trailing text
while ((match = ansi_re.exec(str))) {
if (match[2] === "m") {
numbers = _parseNumbers(match[1]);
} else {
is_open = true;
// Ignored: Not a color code
}
var chunk = str.substring(start, match.index);
if (chunk) {
if (bold && 0 <= fg && fg < 8) {
fg += 8; // Bold text uses "intense" colors
}
var classes = [];
var styles = [];

// consume sequence of color escapes
var numbers = pattern.match(/\d+/g);
var attrs = {};
while (numbers.length > 0) {
_process_numbers(attrs, numbers);
if (typeof fg === "number") {
classes.push(_ANSI_COLORS[fg] + "-fg");
} else if (fg.length) {
styles.push(`color: rgb(${fg})`);
}

var span = "<span ";
Object.keys(attrs).map(function (attr) {
span = span + " " + attr + '="' + attrs[attr] + '"';
});
return span + ">";
if (typeof bg === "number") {
classes.push(_ANSI_COLORS[bg] + "-bg");
} else if (bg.length) {
styles.push(`background-color: rgb(${bg})`);
}

if (bold) {
classes.push("ansi-bold");
}

if (classes.length || styles.length) {
out.push("<span");
if (classes.length) {
out.push(` class="${classes.join(" ")}"`);
}
if (styles.length) {
out.push(` style="${styles.join("; ")}"`);
}
out.push(">");
out.push(chunk);
out.push("</span>");
} else {
out.push(chunk);
}
}
});
start = ansi_re.lastIndex;

while (numbers.length) {
var n = numbers.shift();
switch (n) {
case 0:
fg = bg = [];
bold = false;
break;
case 1:
case 5:
bold = true;
break;
case 21:
case 22:
bold = false;
break;
case 30:
case 31:
case 32:
case 33:
case 34:
case 35:
case 36:
case 37:
fg = n - 30;
break;
case 38:
try {
fg = _getExtendedColors(numbers);
} catch(e) {
numbers.length = 0;
}
break;
case 39:
fg = [];
break;
case 40:
case 41:
case 42:
case 43:
case 44:
case 45:
case 46:
case 47:
bg = n - 40;
break;
case 48:
try {
bg = _getExtendedColors(numbers);
} catch(e) {
numbers.length = 0;
}
break;
case 49:
bg = [];
break;
case 90:
case 91:
case 92:
case 93:
case 94:
case 95:
case 96:
case 97:
fg = n - 90 + 8;
break;
case 100:
case 101:
case 102:
case 103:
case 104:
case 105:
case 106:
case 107:
bg = n - 100 + 8;
break;
default:
// Unknown codes are ignored
}
}
}
return out.join("");
}

// Transform ANSI color escape codes into HTML <span> tags with css
// classes listed in the above ansi_colormap object. The actual color used
// are set in the css file.
// Transform ANSI color escape codes into HTML <span> tags with CSS
// classes such as "ansi-green-intense-fg".
// The actual colors used are set in the CSS file.
// This is supposed to have the same behavior as nbconvert.filters.ansi2html()
function fixConsole(txt) {
txt = xmlencode(txt);
txt = _.escape(txt);

// Strip all ANSI codes that are not color related. Matches
// all ANSI codes that do not end with "m".
var ignored_re = /(?=(\033\[[?\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
txt = txt.replace(ignored_re, "");

// color ansi codes
// color ansi codes (and remove non-color escape sequences)
txt = _ansispan(txt);
return txt;
}
Expand Down
2 changes: 1 addition & 1 deletion notebook/static/notebook/js/outputarea.js
Original file line number Diff line number Diff line change
Expand Up @@ -659,9 +659,9 @@ define([
var append_text = function (data, md, element) {
var type = 'text/plain';
var toinsert = this.create_output_subarea(md, "output_text", type);
data = utils.fixCarriageReturn(data);
// escape ANSI & HTML specials in plaintext:
data = utils.fixConsole(data);
data = utils.fixCarriageReturn(data);
data = utils.autoLinkUrls(data);
// The only user content injected with this HTML call is
// escaped by the fixConsole() method.
Expand Down
2 changes: 1 addition & 1 deletion notebook/static/notebook/js/pager.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ define([
* The only user content injected with this HTML call is escaped by
* the fixConsole() method.
*/
this.pager_element.find(".container").append($('<pre/>').html(utils.fixCarriageReturn(utils.fixConsole(text))));
this.pager_element.find(".container").append($('<pre/>').html(utils.fixConsole(utils.fixCarriageReturn(text))));
};

Pager.prototype.append = function (htm) {
Expand Down
Loading

0 comments on commit 7bee6c5

Please sign in to comment.