Skip to content

Instantly share code, notes, and snippets.

@Twisol
Created May 2, 2011 00:29
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Twisol/951035 to your computer and use it in GitHub Desktop.
Save Twisol/951035 to your computer and use it in GitHub Desktop.
ANSI sequence parser (Node.js) and client-side renderer
var sys = require("sys");
var ANSI = (function() {
sys.inherits(ANSI, require("events").EventEmitter);
function ANSI() {
this.state = "plain";
this.reset();
}
ANSI.prototype.reset = function() {
this.params = [];
this.param = null; // null coerces to 0 when used in arithmetic
this.intermediate = [];
};
ANSI.prototype.parse = function(str) {
var end = str.length;
var i = 0;
var left = this.state === "plain" ? i : end;
var right = end;
var ch, nextESC;
while (i < end) {
ch = str[i];
switch (this.state) {
// Plain, normal text
case "plain":
nextESC = str.indexOf("\u001b", i);
if (nextESC === -1) {
i = end;
} else {
right = i = nextESC;
this.state = "esc";
}
break;
// An ESC has been detected - begin processing ANSI sequence
case "esc":
if (ch === "[") {
this.state = "params";
} else {
this.state = "plain";
}
break;
// Capture a parameter
case "params":
if ("0" <= ch && ch <= "9") {
this.param = (this.param * 10) + (ch - 0);
break;
} else {
this.params.push(this.param);
this.param = null;
this.state = "param-end";
// NOTE: FALL-THROUGH
}
// Check if there are any more parameters
// This label isn't strictly neccesary, but it's self-documenting.
case "param-end":
if (ch === ";") {
this.state = "params";
break;
} else {
this.state = "intermediate";
// NOTE: FALL-THROUGH
}
// Capture intermediate databytes
case "intermediate":
if (" " <= ch && ch <= "/") {
this.intermediate.push(ch);
break;
} else {
this.state = "final";
// NOTE: FALL-THROUGH
}
// Capture the command type
// This label isn't strictly necessary, but it's self-documenting.
case "final":
if ("@" <= ch && ch <= "~") {
if (left < right) {
this.emit("data", str.slice(left, right));
}
this.emit("command", ch, this.params, this.intermediate.join(""));
left = i + 1;
}
this.state = "plain";
this.reset();
right = end;
break;
}
i += 1;
}
if (left < right) {
this.emit("data", str.slice(left, right));
}
};
return ANSI;
})();
module.exports = ANSI;
(function(Aspect) {
Aspect.Renderer = new Class({
defaults: {
"fg": 7,
"bg": 0,
},
initialize: function(world, element) {
this.world = world;
this.e = element;
this.resetAttributes();
},
display: function(data) {
var buf = [this.newStyleRun()];
var invalidated = false;
for (var l = data.length, i = 0; i < l; ++i) {
var entry = data[i];
if (entry instanceof Array) {
if (entry[0] == "m") {
this.setANSI(entry[1]);
invalidated = true;
}
} else {
if (invalidated) {
buf.push("</span>", this.newStyleRun());
invalidated = false;
}
buf.push(entry.replace(/&/g, "&amp;").replace(/</g, "&lt;"));
}
}
buf.push("</span>");
var span = document.createElement("span");
span.innerHTML = buf.join("");
this.e.appendChild(span);
},
setANSI: function(commands) {
// default case: \e[m
if (commands.length == 0) {
this.resetAttributes();
return;
}
// xterm-256: \e[38;5;Xm for FG or \e[48;5;Xm for BG,
// where X is a number in the interval [0, 255].
if (commands.length == 3 &&
(commands[0] === 38 || commands[0] === 48) &&
commands[1] === 5) {
var num = commands[2];
if (num !== null && (0 <= num && num <= 255)) {
if (commands[0] === 38) {
this.fg = num;
this.xterm = true;
} else {
this.bg = num;
}
return;
}
}
// Standard commands (well, mostly)
for (var l = commands.length, i = 0; i < l; ++i) {
var command = commands[i];
switch (command) {
// Color sequences are used the most, so they come first
case 30: case 31: case 32: case 33:
case 34: case 35: case 36: case 37:
this.fg = command - 30;
this.xterm = false;
break;
case 40: case 41: case 42: case 43:
case 44: case 45: case 46: case 47:
this.bg = command - 40;
break;
case 0: this.resetAttributes(); break;
case 1: this.bright = true; break;
case 3: this.italic = true; break;
case 4: this.underline = true; break;
case 7: this.negative = true; break;
case 8: this.concealed = true; break;
case 9: this.strike = true; break;
case 22: this.bright = false; break;
case 23: this.italic = false; break;
case 24: this.underline = false; break;
case 27: this.negative = false; break;
case 28: this.concealed = false; break;
case 29: this.strike = false; break;
case 39:
this.fg = this.defaults.fg;
this.xterm = false;
break;
case 49:
this.bg = this.defaults.bg;
break;
case 90: case 91: case 92: case 93:
case 94: case 95: case 96: case 97:
this.fg = command - 90 + 8;
this.xterm = true;
break;
case 100: case 101: case 102: case 103:
case 104: case 105: case 106: case 107:
this.bg = command - 100 + 8;
this.xterm = true;
break;
}
}
},
newStyleRun: function() {
var fg = this.fg;
if (this.bright && !this.xterm) {
fg += 8;
}
var bg = this.bg;
if (this.negative) {
bg = fg;
fg = this.bg;
}
if (this.concealed) {
fg = bg;
}
var span = ["<span class=\"fg" + fg + " bg" + bg];
if (this.italic) { span.push(" italic"); }
if (this.strike) { span.push(" strike"); }
if (this.underline) { span.push(" underline"); }
span.push("\">");
return span.join("");
},
resetAttributes: function() {
this.fg = this.defaults.fg;
this.bg = this.defaults.bg;
this.negative = this.italic = this.bright = this.strike = this.underline = this.concealed = this.xterm = false;
}
});
})(Aspect);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment