summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--js/codeq/console.js662
1 files changed, 662 insertions, 0 deletions
diff --git a/js/codeq/console.js b/js/codeq/console.js
new file mode 100644
index 0000000..7dba8e1
--- /dev/null
+++ b/js/codeq/console.js
@@ -0,0 +1,662 @@
+/**
+ * Takes a div element and runs a console simulator within.
+ */
+
+(function () {
+
+ var noop = function () { return false; }, // ignore keypress
+
+ defaultKeyMappings = { // keyed by modifiers, each either 1 or 0: shift || ctrl || alt
+ '000': {
+ 'ArrowLeft': function () { this.moveLeft(); return false; },
+ 'ArrowRight': function () { this.moveRight(); return false; },
+ 'ArrowUp': noop,
+ 'ArrowDown': noop,
+ 'Backspace': function () { this.deleteCharacterLeft(); return false; },
+ 'Delete': function () { this.deleteCharacterRight(); return false; },
+ 'End': function () { this.moveToEndOfLine(); return false; },
+ 'Home': function () { this.moveToStartOfLine(); return false; },
+ 'Enter': function () { return true; }, // if it returns false in line-buffered mode, then a line will never be formed -> no input ever
+ 'Tab': noop
+ }
+ };
+
+ // Conversion table from legacy key codes for assorted keys
+ // For details see:
+ // * https://w3c.github.io/uievents/
+ // * http://www.w3.org/TR/2015/WD-DOM-Level-3-Events-code-20150428/
+ // * http://www.w3.org/TR/2015/WD-DOM-Level-3-Events-key-20150427/
+ // and their references.
+ var legacyKeyConversion = {
+ 0x03: {'code': 'Cancel'},
+ 0x06: {'code': 'Help'},
+ 0x08: {'code': 'Backspace'},
+ 0x09: {'code': 'Tab'},
+ 0x0c: {'code': 'Clear'},
+ 0x0d: {'code': 'Enter'},
+ 0x13: {'code': 'Pause'},
+ 0x1b: {'code': 'Escape'},
+ 0x20: {'code': 'Space', 'key': ' '},
+ 0x21: {'code': 'PageUp'},
+ 0x22: {'code': 'PageDown'},
+ 0x23: {'code': 'End'},
+ 0x24: {'code': 'Home'},
+ 0x25: {'code': 'ArrowLeft'},
+ 0x26: {'code': 'ArrowUp'},
+ 0x27: {'code': 'ArrowRight'},
+ 0x28: {'code': 'ArrowDown'},
+ 0x2c: {'code': 'PrintScreen'},
+ 0x2d: {'code': 'Insert'},
+ 0x2e: {'code': 'Delete'},
+ 0x6a: {'code': 'NumpadMultiply', 'key': '*'},
+ 0x6b: {'code': 'NumpadAdd', 'key': '+'},
+ 0x6c: {'code': 'NumpadComma', 'key': ','},
+ 0x6d: {'code': 'NumpadSubtract', 'key': '-'},
+ 0x6e: {'code': 'NumpadDecimal', 'key': '.'},
+ 0x6f: {'code': 'NumpadDivide', 'key': '/'},
+ 0x90: {'code': 'NumLock'},
+ 0x91: {'code': 'ScrollLock'},
+ 0xba: {'code': 'Semicolon', 'key': ';'},
+ 0xbb: {'code': 'Equal', 'key': '='},
+ 0xbc: {'code': 'Comma', 'key': ','},
+ 0xbd: {'code': 'Minus', 'key': '-'},
+ 0xbe: {'code': 'Period', 'key': '.'},
+ 0xbf: {'code': 'Slash', 'key': '/'},
+ 0xc0: {'code': 'Backquote', 'key': '`'},
+ 0xdb: {'code': 'BracketLeft', 'key': '['},
+ 0xdc: {'code': 'Backslash', 'key': '\\'},
+ 0xdd: {'code': 'BracketRight', 'key': ']'},
+ 0xde: {'code': 'Quote', 'key': "'"}
+ };
+ // generate the rest of the mappings
+ (function (table) {
+ var userAgent = navigator.userAgent,
+ i;
+ // digits
+ for (i = 0; i < 10; i++) table[0x30 + i] = {'code': 'Digit' + i, 'key': '' + i};
+ // characters: 'key' is the lower-cased character
+ for (i = 0; i < 26; i++) table[0x41 + i] = {'code': 'Key' + String.fromCharCode(0x41 + i), 'key': String.fromCharCode(0x61 + i)};
+ // numpad
+ for (i = 0; i < 10; i++) table[0x60 + i] = {'code': 'Numpad' + i, 'key': '' + i};
+ // function keys F1 to F24
+ for (i = 1; i < 25; i++) table[0x6f + i] = {'code': 'F' + i};
+
+ // override tables
+ if (userAgent.indexOf('Gecko/') >= 0) {
+ // Firefox
+ table[0x3b] = {'code': 'Semicolon', 'key': ';'};
+ table[0x3d] = {'code': 'Equal', 'key': '='};
+ table[0x6b] = {'code': 'Equal', 'key': '='};
+ table[0x6d] = {'code': 'Minus', 'key': '-'};
+ table[0xbb] = {'code': 'NumpadAdd', 'key': '+'};
+ table[0xbd] = {'code': 'NumpadSubtract', 'key': '-'};
+ if (navigator.platform.indexOf('Mac') >= 0) {
+ table[0x0c] = {'code': 'NumLock'};
+ table[0xad] = {'code': 'Minus', 'key': '-'};
+ }
+ else if (navigator.platform.indexOf('Win') >= 0) {
+ table[0xad] = {'code': 'Minus', 'key': '-'};
+ }
+ }
+ if (navigator.vendor && (navigator.vendor.indexOf('Apple') >= 0)) {
+ // Safari
+ table[0x03] = {'code': 'Enter'};
+ table[0x19] = {'code': 'Tab'};
+ }
+ if ((userAgent.indexOf('iPad') >= 0) || (userAgent.indexOf('iPod') >= 0) || (userAgent.indexOf('iPhone') >= 0)) {
+ // ios
+ table[0x0a] = {'code': 'Enter'};
+ }
+ })(legacyKeyConversion);
+
+ // some (older) browsers specify incorrect key codes, this is a fix-up mapping
+ var codeFixTable = {
+ 'Esc': 'Escape',
+ 'Left': 'ArrowLeft',
+ 'Up': 'ArrowUp',
+ 'Right': 'ArrowRight',
+ 'Down': 'ArrowDown',
+ 'Del': 'Delete'
+ };
+
+ /**
+ * Takes the given DOM Keyboard Event and returns a normalized representation.
+ * @param event
+ */
+ var processKeyboardEvent = function (event) {
+ var keyCode, converted, result, c;
+ if (('code' in event) && (('key' in event) || ('keyIdentifier') in event)) {
+ // we have DOM3 KeyboardEvent, no conversion needed
+ result = {'code': codeFixTable[event.code] || event.code, 'key': event.key || event.keyIdentifier};
+ }
+ else {
+ keyCode = event.keyCode || event.which;
+ converted = legacyKeyConversion[keyCode];
+ if (converted) {
+ result = {'code': converted.code};
+ if (converted.key) result.key = converted.key;
+ else result.key = converted.code; // as per DOM3 spec: unprintables have the key the same as the code TODO: not true for all of them, e.g. AltLeft -> Alt
+ }
+ else {
+ // unknown key, try to set at least some info
+ result = {'code': ''};
+ c = String.fromCharCode(keyCode);
+ if (c !== '') result.key = c;
+ else result.key = 'Unidentified';
+ }
+ }
+ // char is set only in keypress events, never in keydown and keyup
+ // (keypress is not a part of any standard)
+ if ('char' in event) {
+ result.char = event.char;
+ }
+ else if ('charCode' in event) {
+ result.char = String.fromCharCode(event.charCode);
+ }
+ else if (event.type == 'keypress') {
+ // TODO: generate result.char somehow? Probably not needed, we should use the input event instead of the keypress event anyway.
+ }
+ return result;
+ };
+
+ codeq.makeConsole = function (jqElt, options) {
+ var lines = [], // console content
+ maxLines = options.maxLines || 10000, // how many lines to display at most
+ currentRow = 0, // cursor position
+ currentCol = 0,
+ jqContent = $('<div></div>'), // the rendering of the console
+ jqInput = $('<textarea style="left: 0; bottom: 0; width: 2px; position: absolute;"></textarea>'), // for receiving keypresses, clipboard content
+ inputDisabled = false, // whether to ignore input
+ lineBuffered = true, // whether we are line-buffered or not buffered
+
+ renderLine = function (lineDescriptor) {
+ var jqLine = lineDescriptor.jqLine,
+ className = 'cq-con-text ' + lineDescriptor.className,
+ content = lineDescriptor.content || '',
+ jq1, jq2, jqCursor;
+
+ if (!jqLine) {
+ jqLine = $('<div class="' + lineDescriptor.className + '"></div>');
+ if (lineDescriptor.row == 0) {
+ jqContent.prepend(jqLine);
+ }
+ else if ((lineDescriptor.row < lines.length) && lines[lineDescriptor.row - 1].jqLine) {
+ lines[lineDescriptor.row - 1].jqLine.after(jqLine);
+ }
+ else {
+ // bail-out line, we should never end up here
+ jqContent.append(jqLine);
+ }
+ lineDescriptor.jqLine = jqLine;
+ }
+ else {
+ jqLine.empty();
+ }
+
+ if (currentRow == lineDescriptor.row) {
+ // mark the cursor in the current line
+ if (currentCol >= content.length) {
+ jq1 = $('<span class="' + className + '"></span>');
+ jqCursor = $('<span class="cq-con-cursor">&nbsp;</span>');
+ jqLine.append(jq1);
+ jqLine.append(jqCursor);
+ jq1.text(content);
+ }
+ else if (currentCol <= 0) {
+ jq2 = $('<span class="' + className + '"></span>');
+ jqCursor = $('<span class="cq-con-cursor"></span>');
+ jqLine.append(jqCursor);
+ jqLine.append(jq2);
+ jqCursor.text(content.charAt(0));
+ jq2.text(content.substr(1));
+ }
+ else {
+ jq1 = $('<span class="' + className + '"></span>');
+ jq2 = $('<span class="' + className + '"></span>');
+ jqCursor = $('<span class="cq-con-cursor"></span>');
+ jqLine.append(jq1);
+ jqLine.append(jqCursor);
+ jqLine.append(jq2);
+ jq1.text(content.substr(0, currentCol - 1));
+ jqCursor.text(content.charAt(currentCol));
+ jq2.text(content.substr(currentCol + 1));
+ }
+ }
+ else {
+ jq1 = $('<span class="' + className + '"></span>');
+ jqLine.append(jq1);
+ jq1.text(lineDescriptor.content);
+ }
+ },
+
+ render = function () {
+ var i, lineDescriptor, n = lines.length;
+ jqContent.empty();
+ for (i = 0; i < n; i++) {
+ lineDescriptor = lines[i];
+ lineDescriptor.row = i;
+ if (lineDescriptor.jqLine) {
+ lineDescriptor.jqLine.remove();
+ lineDescriptor.jqLine = null;
+ }
+ renderLine(lineDescriptor);
+ }
+ },
+
+ formNewLines = function (newText) {
+ var newLines, newTextLines;
+ if (!newText) newLines = [];
+ else if (newText instanceof Array) {
+ newLines = [];
+ for (i = 0; i < newText.length; i++) {
+ newTextLines = ('' + newText[i]).split('\n');
+ for (j = 0; j < newTextLines.length; j++) newLines.push(newTextLines[j]);
+ }
+ }
+ else {
+ newLines = ('' + newText).split('\n');
+ }
+ return newLines;
+ },
+
+ // the handler object that is returned
+ handler = {
+ 'setLineBuffered': function () { lineBuffered = true; },
+ 'setNotBuffered': function () { lineBuffered = false; },
+ 'onInput': false, // the caller should assign a function here, that takes input as parameter
+ 'onKeypress': false, // same, but it takes a keycode, and returns a keycode
+ 'inputEnable': function () { inputDisabled = false; },
+ 'inputDisable': function () { inputDisabled = true; },
+ 'keyMaps': {}, // will be filled in later
+
+ 'reflow': function () {
+ var startRow, endRow, i, lineDescriptor;
+ if (arguments.length == 0) {
+ // no arguments, reflow all lines
+ startRow = 0;
+ endRow = lines.length - 1;
+ }
+ else if (arguments.length == 1) {
+ // only the starting row given, reflow all lines from there on
+ startRow = arguments[0];
+ endRow = lines.length - 1;
+ }
+ else {
+ // both arguments are given
+ startRow = arguments[0];
+ endRow = arguments[1];
+ }
+ for (i = startRow; i <= endRow; i++) {
+ lineDescriptor = lines[i];
+ lineDescriptor.row = i;
+ renderLine(lineDescriptor);
+ }
+ },
+
+ 'removeRow': function (row, deferReflow) {
+ var lineDescriptor;
+ if (row >= lines.length) return; // already absent
+ lineDescriptor = lines[row];
+ lines.splice(row, 1);
+ if (lineDescriptor.jqLine) {
+ lineDescriptor.jqLine.remove();
+ lineDescriptor.jqLine = null;
+ }
+ if (!deferReflow) this.reflow(row);
+ return lineDescriptor;
+ },
+
+ 'insertRow': function (row, content, className, deferReflow) {
+ // inserts a new row at the position specified
+ var lineDescriptor, i, jqLine;
+ for (i = lines.length; i < row; i++) {
+ // insert any empty rows
+ lineDescriptor = {content: '', className: className, row: i};
+ lines.push(lineDescriptor);
+ if (!deferReflow) renderLine(lineDescriptor);
+ }
+ lineDescriptor = {content: content, className: className, row: row};
+ if (row >= lines.length) lines.push(lineDescriptor);
+ else lines.splice(row, 0, lineDescriptor);
+ if (!deferReflow) this.reflow(row);
+ return lineDescriptor;
+ },
+
+ 'splice': function (startRow, startCol, endRow, endCol, newText, className) {
+ var i, j, n, startLine, endLine, newLines, newTextLines, part1, part2;
+
+ if (!className) className = 'output';
+ // pre-process newText, convert it to newLines: an array of strings without newlines
+ newLines = formNewLines(newText);
+ if (newLines.length == 0) newLines.push(''); // there must at least be an empty "glue"
+
+ if (lines.length > 0) {
+ // there is something to remove
+ // verify sanity
+ if (startRow < 0) startRow = 0;
+ else if (startRow >= lines.length) startRow = lines.length - 1;
+ if (endRow < 0) endRow = 0;
+ else if (endRow >= lines.length) endRow = lines.length - 1;
+ if (endRow < startRow) {
+ // swap the meaning of the start and end coordinates
+ i = startRow;
+ startRow = endRow;
+ endRow = i;
+ i = startCol;
+ startCol = endCol;
+ endCol = i;
+ }
+ if (startRow == endRow) {
+ if (endCol < startCol) {
+ i = startCol;
+ startCol = endCol;
+ endCol = i;
+ }
+ }
+ startLine = lines[startRow];
+ endLine = lines[endRow];
+ if (startCol < 0) startCol = 0;
+ else if (startCol > startLine.length) startCol = startLine.length;
+ if (endCol < 0) endCol = 0;
+ else if (endCol > endLine.length) endCol = endLine.length;
+
+ // execute splice
+ part1 = startLine.content.substring(0, startCol); // the remaining content on the first and last lines
+ part2 = endLine.content.substring(endCol);
+ // modify the first and last lines, and make the list of the old lines to remove
+ j = startRow + 1; // where to start removing lines
+ n = endRow - startRow; // how many lines to remove
+ if (newLines.length > 1) {
+ // the first and the last new lines are separate existing lines, modify them
+ startLine.content = part1 + newLines[0];
+ if (startRow == endRow) {
+ // we need to create one additional line to hold the compositum of the last line and part2
+ this.insertRow(j, newLines[newLines.length - 1] + part2, className, true);
+ j++;
+ }
+ else {
+ endLine.content = newLines[newLines.length - 1] + part2;
+ n--; // retain the last line
+ }
+ }
+ else {
+ // the first and the last new lines are the same existing line
+ startLine.content = part1 + newLines[0] + part2;
+ }
+ // remove the rest of the old lines
+ while (n > 0) {
+ this.removeRow(j, true);
+ n--;
+ }
+ // insert new lines in between
+ j = newLines.length - 1; // the first and the last lines have already been set up
+ for (i = 1; i < j; i++) {
+ this.insertRow(startRow + i, newLines[i], className, true);
+ }
+ // optimization step: if only one line changed, then only reflow the line
+ if ((n == 0) && (newLines.length == 1)) {
+ renderLine(startLine);
+ }
+ else {
+ this.reflow(startRow);
+ }
+ }
+ else {
+ // no content exists, add all lines
+ for (i = 0; i < newLines.length; i++) {
+ this.insertRow(i, newLines[i], className, true);
+ }
+ this.reflow();
+ }
+ },
+
+ 'insert': function (text, row, col, className) {
+ this.splice(row, col, row, col, text, className || 'output');
+ },
+
+ 'append': function (text, className) {
+ // we add to the end, so optimize: do not call splice(), it is too heavy for this purpose
+ var newLines = formNewLines(text),
+ startRow = currentRow,
+ i, lineDescriptor;
+ if (newLines.length == 0) return; // nothing to do
+ lineDescriptor = lines[currentRow];
+ lineDescriptor.content = lineDescriptor.content + newLines[0];
+ renderLine(lineDescriptor);
+ for (i = 1; i < newLines.length; i++) {
+ renderLine(this.insertRow(startRow + i, newLines[i], className, true));
+ }
+ },
+
+ 'moveLeft': function () {
+ if (currentCol > 0) {
+ currentCol--;
+ renderLine(lines[currentRow]);
+ }
+ },
+
+ 'moveRight': function () {
+ var lineDescriptor = lines[currentRow];
+ if (currentCol < lineDescriptor.content.length) {
+ currentCol++;
+ renderLine(lineDescriptor);
+ }
+ },
+
+ 'deleteCharacterLeft': function () {
+ var lineDescriptor = lines[currentRow],
+ content = lineDescriptor.content;
+ if ((currentCol > 0) && (currentCol <= content.length)) {
+ currentCol--;
+ lineDescriptor.content = content.substring(0, currentCol) + content.substring(currentCol + 1);
+ renderLine(lineDescriptor);
+ }
+ },
+
+ 'deleteCharacterRight': function () {
+ var lineDescriptor = lines[currentRow],
+ content = lineDescriptor.content;
+ if ((currentCol >= 0) && (currentCol < content.length)) {
+ lineDescriptor.content = content.substring(0, currentCol) + content.substring(currentCol + 1);
+ renderLine(lineDescriptor);
+ }
+ },
+
+ 'moveToEndOfLine': function () {
+ var lineDescriptor = lines[currentRow],
+ n = lineDescriptor.content.length;
+ if (currentCol != n) {
+ currentCol = n;
+ renderLine(lineDescriptor);
+ }
+ },
+
+ 'moveToStartOfLine': function () {
+ var lineDescriptor = lines[currentRow];
+ if (currentCol != 0) {
+ currentCol = 0;
+ renderLine(lineDescriptor);
+ }
+ },
+
+ 'destroy': function () {
+ var n = lines.length,
+ i, lineDescriptor;
+ // detach all jQuery objects from internal structures
+ for (i = 0; i < n; i++) {
+ lineDescriptor = lines[i];
+ if (lineDescriptor.jqLine) {
+ lineDescriptor.jqLine.remove();
+ lineDescriptor.jqLine = null;
+ }
+ }
+ lines.length = 0;
+ // clear and detach the rest
+ render = null;
+ renderLine = null;
+ jqContent.remove();
+ jqContent = null;
+ jqInput.remove();
+ jqInput = null;
+ for (i in handler) {
+ if (handler.hasOwnProperty(i)) {
+ delete handler[i];
+ }
+ }
+ jqElt.empty();
+ jqElt = null;
+ options = null;
+ }
+ },
+
+ handleInput = function (chars) {
+ var cookedChars = chars, // default
+ startingRow = currentRow,
+ endingRow, i;
+
+ if (inputDisabled) {
+ // flatly ignore
+ clipboardPasteInProgress = false;
+ return;
+ }
+
+ // first cook the input
+ if (typeof handler.onKeypress === 'function') {
+ try {
+ cookedChars = handler.onKeypress(chars, clipboardPasteInProgress || !aKeyIsDown);
+ }
+ catch (e) {
+ // TODO: log error
+ }
+ }
+
+ if (!cookedChars) {
+ // nothing to append
+ clipboardPasteInProgress = false;
+ return;
+ }
+
+ // now serve the cooking
+ handler.append(cookedChars, 'input'); // append what we have to the display
+ if (typeof handler.onInput === 'function') { // now tell the owner what we've done
+ if (lineBuffered) {
+ if (currentRow !== startingRow) {
+ // in line-buffered mode emit each line separately, except for the last (current) line
+ for (i = startingRow; i < endingRow; i++) {
+ try {
+ handler.onInput(lines[i].content);
+ }
+ catch (e) {
+ // TODO: log error
+ }
+ }
+ }
+ }
+ else {
+ try {
+ handler.onInput(cookedChars);
+ }
+ catch (e) {
+ // TODO: log error
+ }
+ }
+ }
+
+ clipboardPasteInProgress = false;
+ },
+
+ clipboardPasteInProgress = false, // whether the previous keydown was a CTRL+V or shift+insert
+ aKeyIsDown = false, // whether a key is currently pressed: if it's not and there is input, then we got a paste via mouse
+ i, j, a, b, c;
+
+ // copy default key handlers
+ a = handler.keyMaps;
+ for (i in defaultKeyMappings) {
+ if (!defaultKeyMappings.hasOwnProperty(i)) continue;
+ b = defaultKeyMappings[i];
+ c = {};
+ a[i] = c;
+ for (j in b) {
+ if (!b.hasOwnProperty(j)) continue;
+ c[j] = b[j];
+ }
+ }
+ a = null;
+ b = null;
+ c = null;
+
+ // initialize the DOM container
+ jqElt.empty();
+ jqElt.append(jqContent);
+ jqElt.append(jqInput);
+ jqElt.on('click', function () {
+ // set focus to the textarea so we pick up keypresses
+ jqInput.css('position', 'fixed').focus();
+ });
+ jqElt.on('blur', function () {
+ jqInput.css('position', 'absolute');
+ });
+
+ // register key handlers
+ jqInput.on('keydown', function (evt) {
+ var modifiers = ['' + (0 + evt.shiftKey), '' + (0 + (evt.ctrlKey || evt.metaKey)), '' + (0 + evt.altKey)].join(''),
+ handlersForModifiers = handler.keyMaps[modifiers],
+ acceptKeydown = !inputDisabled, // the default value
+ eventInfo = processKeyboardEvent(evt),
+ handlerForKeydown = handlersForModifiers && handlersForModifiers[eventInfo.code];
+
+ if (acceptKeydown && (typeof handlerForKeydown === 'function')) {
+ try {
+ acceptKeydown = handlerForKeydown.apply(handler, [eventInfo]);
+ }
+ catch (e) {
+ // ignore errors, TODO: log errors
+ }
+ }
+
+ if (!acceptKeydown) {
+ evt.preventDefault();
+ evt.stopPropagation();
+ return false;
+ }
+
+ // paste: CTRL+V or shift+insert
+ clipboardPasteInProgress = ((modifiers === '010') && (eventInfo.code === 'KeyV')) || ((modifiers === '100') && (eventInfo.code === 'Insert'));
+ aKeyIsDown = true;
+
+ return true;
+ });
+ jqInput.on('keyup', function (evt) {
+ aKeyIsDown = false;
+ });
+
+ if ('oninput' in jqInput[0]) {
+ // the element supports input events, use them in preference to keypress events
+ jqInput.on('input', function (evt) {
+ // this event cannot be prevented/cancelled in the DOM scheme of things, but we can simulate prevention, because we have a "detached" display
+ handleInput(evt.input);
+ });
+ }
+ else {
+ // the element does not support the "modern" input events, so it ought to support the old keypress events
+ jqInput.on('keypress', function (evt) {
+ // this event cannot be prevented/cancelled in the DOM scheme of things, but we can simulate prevention, because we have a "detached" display
+ var keyChar = false;
+ if ('char' in evt) keyChar = evt.char;
+ else if ('charCode' in evt) keyChar = String.fromCharCode(evt.charCode);
+ else return; // ignore input
+ handleInput(keyChar);
+ });
+ }
+
+ // emit greeting if provided
+ if (options.greeting) {
+ lines.push({content: options.greeting, className: 'greeting'});
+ currentRow++;
+ }
+ lines.push({content: '', className: 'input'}); // initial input line
+ render();
+
+ return handler;
+ };
+})(); \ No newline at end of file