public/webconsole.js in rack-webconsole-pry-0.1.7 vs public/webconsole.js in rack-webconsole-pry-0.1.8

- old
+ new

@@ -1,84 +1,259 @@ (function($) { +/* AnsiParse */ +ansiparse = function (str) { + // + // I'm terrible at writing parsers. + // + var matchingControl = null, + matchingData = null, + matchingText = '', + ansiState = [], + result = [], + state = {}, + eraseChar; + + // + // General workflow for this thing is: + // \033\[33mText + // | | | + // | | matchingText + // | matchingData + // matchingControl + // + // In further steps we hope it's all going to be fine. It usually is. + // + + // + // Erases a char from the output + // + eraseChar = function () { + var index, text; + if (matchingText.length) { + matchingText = matchingText.substr(0, matchingText.length - 1); + } + else if (result.length) { + index = result.length - 1; + text = result[index].text; + if (text.length === 1) { + // + // A result bit was fully deleted, pop it out to simplify the final output + // + result.pop(); + } + else { + result[index].text = text.substr(0, text.length - 1); + } + } + }; + + for (var i = 0; i < str.length; i++) { + if (matchingControl != null) { + if (matchingControl == '\u001B' && str[i] == '\[') { + // + // We've matched full control code. Lets start matching formating data. + // + + // + // "emit" matched text with correct state + // + if (matchingText) { + state.text = matchingText; + result.push(state); + state = {}; + matchingText = ""; + } + + matchingControl = null; + matchingData = ''; + } + else { + // + // We failed to match anything - most likely a bad control code. We + // go back to matching regular strings. + // + matchingText += matchingControl + str[i]; + matchingControl = null; + } + continue; + } + else if (matchingData != null) { + if (str[i] == ';') { + // + // `;` separates many formatting codes, for example: `\033[33;43m` + // means that both `33` and `43` should be applied. + // + // TODO: this can be simplified by modifying state here. + // + ansiState.push(matchingData); + matchingData = ''; + } + else if (str[i] == 'm') { + // + // `m` finished whole formatting code. We can proceed to matching + // formatted text. + // + ansiState.push(matchingData); + matchingData = null; + matchingText = ''; + + // + // Convert matched formatting data into user-friendly state object. + // + // TODO: DRY. + // + ansiState.forEach(function (ansiCode) { + if (ansiparse.foregroundColors[ansiCode]) { + state.foreground = ansiparse.foregroundColors[ansiCode]; + } + else if (ansiparse.backgroundColors[ansiCode]) { + state.background = ansiparse.backgroundColors[ansiCode]; + } + else if (ansiCode == 39) { + delete state.foreground; + } + else if (ansiCode == 49) { + delete state.background; + } + else if (ansiparse.styles[ansiCode]) { + state[ansiparse.styles[ansiCode]] = true; + } + else if (ansiCode == 22) { + state.bold = false; + } + else if (ansiCode == 23) { + state.italic = false; + } + else if (ansiCode == 24) { + state.underline = false; + } + }); + ansiState = []; + } + else { + matchingData += str[i]; + } + continue; + } + + if (str[i] == '\u001B') { + matchingControl = str[i]; + } + else if (str[i] == '\u0008') { + eraseChar(); + } + else { + matchingText += str[i]; + } + } + + if (matchingText) { + state.text = matchingText + (matchingControl ? matchingControl : ''); + result.push(state); + } + return result; +} + +ansiparse.foregroundColors = { + '30': 'black', + '31': 'red', + '32': 'green', + '33': 'yellow', + '34': 'blue', + '35': 'magenta', + '36': 'cyan', + '37': 'white', + '90': 'grey' +}; + +ansiparse.backgroundColors = { + '40': 'black', + '41': 'red', + '42': 'green', + '43': 'yellow', + '44': 'blue', + '45': 'magenta', + '46': 'cyan', + '47': 'white' +}; + +ansiparse.styles = { + '1': 'bold', + '3': 'italic', + '4': 'underline' +}; + +if (typeof module == "object" && typeof window == "undefined") { + module.exports = ansiparse; +} + +/* app */ var webconsole = { history:[], pointer:0, query:$('#webconsole_query') } $('#rack-webconsole form').submit(function(e){ e.preventDefault(); }); - var prevStyle = { - color: "#ffffff", - bold: false, - underline: false - } - // colors var colors = { - 30: "#eeeeee", - 31: "#ff6c60", - 32: "#a8ff60", - 33: "#ffffb6", - 34: "#96cbfe", - 35: "#ff73fd", - 36: "#c6c5fe", - 37: "#eeeeee" + 'black': "#eeeeee", + 'red': "#ff6c60", + 'green': "#a8ff60", + 'yellow': "#ffffb6", + 'blue': "#96cbfe", + 'magenta': "#ff73fd", + 'cyan': "#c6c5fe", + 'white': "#eeeeee" } var boldColors = { - 30: "#7c7c7c", - 31: "#ffb6b0", - 32: "#ceffac", - 33: "#ffffcb", - 34: "#b5dcfe", - 35: "#ff9cfe", - 36: "#dfdffe", - 37: "#ffffff" + 'black': "#7c7c7c", + 'red': "#ffb6b0", + 'green': "#ceffac", + 'yellow': "#ffffcb", + 'blue': "#b5dcfe", + 'magenta': "#ff9cfe", + 'cyan': "#dfdffe", + 'white': "#ffffff" } - function resetBashStyle() + function subColor(color, elem) { - prevStyle = { - color: colors[37], - bold: 'normal', - underline: 'none' - }; + if (color == undefined) color = 'white'; + if (elem.bold && boldColors[color] != undefined) { + color = boldColors[color]; + } else if (!elem.bold && colors[color] != undefined) { + color = colors[color]; + } + return color; } - function bashColorToHtml(bcolor) + function parseBashString(str, into) { - // set values - var all = bcolor.split(/;/g) - if (all.indexOf("0") > 0) // ignore anything before 0, since 0 resets - all.splice(0, all.indexOf("0")); - if (all.indexOf("0") >= 0) - resetBashStyle(); - if (all.indexOf("1") >= 0) - prevStyle['bold'] = 'bold'; - if (all.indexOf("4") >= 0) - prevStyle['underline'] = 'underline'; - if (prevStyle['bold'] == 'bold') - colorMap = boldColors; - else - colorMap = colors; - $.each(all, function(idx, val) { - var i = parseInt(val); - if (i > 10 && colorMap[i] != undefined) - prevStyle['color'] = colorMap[i]; + $.each(ansiparse(str), function(idx, elem) { + if (elem.text.length == 1 && elem.text[0] == '\n') { + into.append("<br>"); + return true; + } + var domElem = $('<span></span>'); + domElem.text(elem.text); + domElem.html(domElem.text().replace(/\n/g, "<br>")); + domElem.css('color', subColor(elem.foreground, elem)); + if (elem.bold) + domElem.css('font-weight', 'bold'); + if (elem.italic) + domElem.css('font-style', 'italic'); + if (elem.underline) + domElem.css('text-decoration', 'underline'); + if (elem.background) + domElem.css('background-color', subColor(elem.background, elem)); + into.append(domElem); }); - return 'color:'+prevStyle['color']+';font-weight:'+prevStyle['bold']+ - ';text-decoration:'+prevStyle['underline']; } - function parseBashString(str) - { - str = str.replace(/\u001B\[([0-9;]+)m/g, function(fm, sm) { - return '</span><span style="'+bashColorToHtml(sm)+'">'; - }).replace(/\n/g, "<br>"); - return '<span>'+str+'</span>'; - } $("#rack-webconsole form input").keyup(function(event) { function escapeHTML(string) { return(string.replace(/&/g,'&amp;'). replace(/>/g,'&gt;'). replace(/</g,'&lt;'). @@ -95,13 +270,15 @@ type: 'POST', dataType: 'json', data: ({query: webconsole.query.val(), token: "$TOKEN"}), success: function (data) { var query_class = data.previous_multi_line ? 'query_multiline' : 'query'; - var result = "<div class='" + query_class + "'>" + - parseBashString(escapeHTML(data.prompt)) + "</div>"; + var result = $("<div class='" + query_class + "'></div>"); + parseBashString(escapeHTML(data.prompt), result); if (!data.multi_line) { - result += "<div class='result'>" + parseBashString(escapeHTML(data.result)) + "</div>"; + var mresult = $("<div class='result'></div>"); + parseBashString(escapeHTML(data.result), mresult); + result.append(mresult); } $("#rack-webconsole .results").append(result); $("#rack-webconsole .results_wrapper").scrollTop( $("#rack-webconsole .results").height() );