From 8ae11f070aafea81bc91bdaf6918f396777f88df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ale=C5=A1=20Smodi=C5=A1?= <aless@guru.si>
Date: Thu, 17 Sep 2015 11:45:03 +0200
Subject: Console fix: a line can now have multiple CSS classes, so text can
 e.g. now be in multiple colors.

---
 js/codeq/console.js | 237 +++++++++++++++++++++++++++++++++++++++++++++-------
 js/codeq/startup.js |   1 +
 2 files changed, 207 insertions(+), 31 deletions(-)

(limited to 'js/codeq')

diff --git a/js/codeq/console.js b/js/codeq/console.js
index 99c1550..9331bc1 100644
--- a/js/codeq/console.js
+++ b/js/codeq/console.js
@@ -160,7 +160,7 @@
     };
 
     codeq.makeConsole = function (jqElt, options) {
-        var lines = [], // console content
+        var lines = [],// console content, line descriptors of the form {'content': 'string', 'classNames': [], 'jqLine': jQuery object, 'row': number}
             maxLines = (options && options.maxLines) || 10000, // how many lines to display at most
             currentRow = 0, // cursor position
             currentCol = 0,
@@ -169,14 +169,65 @@
             inputDisabled = false, // whether to ignore input
             lineBuffered = true, // whether we are line-buffered or not buffered
 
+            renderSpan = function (lineDescriptor, startCol, length) {
+                var jqLine = lineDescriptor.jqLine,
+                    content = lineDescriptor.content,
+                    classes = lineDescriptor.classNames,
+                    endCol = startCol + length,
+                    N = classes ? classes.length : 0,
+                    i = 0,
+                    spans = [], // what to render, items are of the form {'content': string, 'cssClass': string}
+                    entry, classSpan, jq, span, s;
+                // seek out the first css class
+                while (i < N) {
+                    entry = classes[i];
+                    if (((entry.start + entry.length) > startCol) && (entry.start < endCol)) break;
+                    i++;
+                }
+                if (i < N) {
+                    // render the first span
+                    classSpan = entry.length - startCol + entry.start;
+                    span = {'content': content.substr(startCol, classSpan), 'cssClass': entry.name};
+                    spans.push(span);
+                    startCol += classSpan;
+                    i++;
+                    // render the rest
+                    while ((startCol < endCol) && (i < N)) {
+                        entry = classes[i];
+                        s = content.substr(startCol, entry.length);
+                        if (entry.name === span.cssClass) {
+                            span.content += s; // join content where the class is the same
+                        }
+                        else {
+                            span = {'content': s, 'cssClass': entry.name};
+                            spans.push(span);
+                        }
+                        startCol += entry.length;
+                        i++;
+                    }
+                }
+                // render any leftover
+                if (startCol < endCol) {
+                    entry = makeClassDescriptor('', startCol, endCol - startCol);
+                    spans.push({'content': content.substr(startCol), 'cssClass': ''});
+                }
+                // render spans
+                for (i = 0; i < spans.length; i++) {
+                    span = spans[i];
+                    jq = $('<span class="cq-con-text ' + span.cssClass + '"></span>');
+                    jqLine.append(jq);
+                    jq.text(span.content);
+                }
+                return entry; // return the last entry used
+            },
+
             renderLine = function (lineDescriptor) {
                 var jqLine = lineDescriptor.jqLine,
-                    className = 'cq-con-text ' + lineDescriptor.className,
-                    content = lineDescriptor.content || '',
-                    jq1, jq2, jqCursor;
+                    content = lineDescriptor.content,
+                    jqCursor, classDescriptor;
 
                 if (!jqLine) {
-                    jqLine = $('<pre class="cq-con-line ' + lineDescriptor.className + '"></pre>');
+                    jqLine = $('<pre class="cq-con-line"></pre>');
                     if (lineDescriptor.row == 0) {
                         jqContent.prepend(jqLine);
                     }
@@ -196,36 +247,30 @@
                 if (currentRow == lineDescriptor.row) {
                     // mark the cursor in the current line
                     if (currentCol >= content.length) {
-                        jq1 = $('<span class="' + className + '"></span>');
+                        // cursor after the last character
+                        renderSpan(lineDescriptor, 0, content.length);
                         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>');
+                        // cursor at the first character
+                        classDescriptor = lineDescriptor.classNames[0] || makeClassDescriptor('', 0, content.length);
+                        jqCursor = $('<span class="cq-con-cursor ' + classDescriptor.name + '"></span>');
                         jqLine.append(jqCursor);
-                        jqLine.append(jq2);
                         jqCursor.text(content.charAt(0));
-                        jq2.text(content.substr(1));
+                        renderSpan(lineDescriptor, 1, content.length - 1);
                     }
                     else {
-                        jq1 = $('<span class="' + className + '"></span>');
-                        jq2 = $('<span class="' + className + '"></span>');
-                        jqCursor = $('<span class="cq-con-cursor"></span>');
-                        jqLine.append(jq1);
+                        // cursor somewhere in between
+                        classDescriptor = renderSpan(lineDescriptor, 0, currentCol);
+                        jqCursor = $('<span class="cq-con-cursor ' + classDescriptor.name + '"></span>');
                         jqLine.append(jqCursor);
-                        jqLine.append(jq2);
-                        jq1.text(content.substr(0, currentCol));
                         jqCursor.text(content.charAt(currentCol));
-                        jq2.text(content.substr(currentCol + 1));
+                        renderSpan(lineDescriptor, currentCol + 1, content.length - currentCol - 1);
                     }
                 }
                 else {
-                    jq1 = $('<span class="' + className + '"></span>');
-                    jqLine.append(jq1);
-                    jq1.text(lineDescriptor.content);
+                    renderSpan(lineDescriptor, 0, content.length);
                 }
             },
 
@@ -259,6 +304,120 @@
                 return newLines;
             },
 
+            makeClassDescriptor = function (name, start, length) {
+                return {'name': name, 'start': start, 'length': length};
+            },
+
+            indexOfClassAtOrAfterColumn = function (classes, col) {
+                var N = classes.length,
+                    i;
+                for (i = 0; i < N; i++) {
+                    if (classes[i].start >= col) return i;
+                }
+                return N;
+            },
+
+            insertClass = function (classes, newClassName, startingCol, length) {
+                var newEntry = makeClassDescriptor(newClassName, startingCol, length),
+                    endCol = startingCol + length,
+                    i, N, prevClass, pos, auxEntry, delta;
+                // seek out the position where to insert the new class
+                i = indexOfClassAtOrAfterColumn(classes, startingCol);
+                prevClass = i > 0 ? classes[i-1] : null;
+                // insert the new class
+                classes.splice(i, 0, newEntry);
+                // if needed, adjust the length of the previous class
+                if (prevClass) {
+                    pos = prevClass.start + prevClass.length;
+                    if (pos > startingCol) {
+                        // split the previous class in two, the new class gets inserted in between
+                        delta = pos - startingCol;
+                        auxEntry = makeClassDescriptor(prevClass.name, endCol, delta);
+                        classes.splice(i+1, 0, auxEntry);
+                        prevClass.length = prevClass.length - delta;
+                    }
+                }
+                // readjust positions of the classes after the new one
+                N = classes.length;
+                for (i++; i < N; i++) {
+                    auxEntry = classes[i];
+                    auxEntry.start = endCol;
+                    endCol += auxEntry.length;
+                }
+            },
+
+            removeClasses = function (classes, startCol, length) {
+                var i, entry, endCol, e, prefixDelta, suffixDelta, N;
+                if (!length) return;
+                endCol = startCol + length;
+                for (i = classes.length - 1; i >= 0; i--) {
+                    entry = classes[i];
+                    e = entry.start + entry.length;
+                    if ((e > startCol) && (entry.start < endCol)) {
+                        prefixDelta = entry.start < startCol ? startCol - entry.start : 0;
+                        suffixDelta = e > endCol ? e - endCol : 0;
+                        if ((prefixDelta == 0) && (suffixDelta == 0)) classes.splice(i, 1);
+                        else entry.length = prefixDelta + suffixDelta;
+                    }
+                }
+                // readjust positions
+                N = classes.length;
+                endCol = 0;
+                for (i = 0; i < N; i++) {
+                    entry = classes[i];
+                    entry.start = endCol;
+                    endCol += entry.length;
+                }
+            },
+
+            removeClassesBefore = function (classes, col) {
+                var i = indexOfClassAtOrAfterColumn(classes, col);
+                if (i == 0) return; // nothing to remove
+                classes.splice(0, i);
+            },
+
+            replaceClassesBeforeColumn = function (classes, col, newClassName, newLength) {
+                var i = indexOfClassAtOrAfterColumn(classes, col),
+                    newEntry = makeClassDescriptor(newClassName, 0, newLength);
+                classes.splice(0, i, newEntry);
+            },
+
+            classesBetween = function (classes, startCol, endCol) {
+                var N = classes.length,
+                    result = [],
+                    i, e, entry, prefixDelta, suffixDelta;
+                for (i = 0; i < N; i++) {
+                    entry = classes[i];
+                    e = entry.start + entry.length; // end column of this css class
+                    if ((e > startCol) && (entry.start < endCol)) {
+                        prefixDelta = entry.start < startCol ? startCol - entry.start : 0;
+                        suffixDelta = e > endCol ? e - endCol : 0;
+                        result.push(makeClassDescriptor(entry.name, entry.start + prefixDelta, entry.length - prefixDelta - suffixDelta));
+                    }
+                }
+                return result;
+            },
+
+            mergeClasses = function () {
+                var result = [],
+                    i, arg, j, entry, endCol;
+                for (i = 0; i < arguments.length; i++) {
+                    arg = arguments[i];
+                    if (arg instanceof Array) {
+                        for (j = 0; j < arg.length; j++) result.push(arg[j]);
+                    }
+                    else result.push(arg);
+                }
+                // adjust positions of css classes
+                endCol = 0;
+                for (i = 0; i < result.length; i++) {
+                    entry = result[i];
+                    entry.start = endCol;
+                    endCol += entry.length;
+                }
+                return result;
+            },
+
             // the handler object that is returned
             handler = {
                 'setLineBuffered': function () { lineBuffered = true; },
@@ -309,14 +468,15 @@
 
                 'insertRow': function (row, content, className, deferReflow) {
                     // inserts a new row at the position specified
-                    var lineDescriptor, i, jqLine;
+                    var lineDescriptor, i;
+                    if (!className) className = 'output';
                     for (i = lines.length; i < row; i++) {
                         // insert any empty rows
-                        lineDescriptor = {content: '', className: className, row: i};
+                        lineDescriptor = {content: '', classNames: [], row: i};
                         lines.push(lineDescriptor);
                         if (!deferReflow) renderLine(lineDescriptor);
                     }
-                    lineDescriptor = {content: content, className: className, row: row};
+                    lineDescriptor = {content: content, classNames: content.length > 0 ? [makeClassDescriptor(className, 0, content.length)] : [], row: row};
                     if (row >= lines.length) lines.push(lineDescriptor);
                     else lines.splice(row, 0, lineDescriptor);
                     if (!deferReflow) this.reflow(row);
@@ -324,7 +484,7 @@
                 },
 
                 'splice': function (startRow, startCol, endRow, endCol, newText, className) {
-                    var i, j, n, startLine, endLine, newLines, newTextLines, part1, part2;
+                    var i, j, n, startLine, endLine, newLines, part1, part2, s, auxLine;
 
                     if (!className) className = 'output';
                     // pre-process newText, convert it to newLines: an array of strings without newlines
@@ -370,19 +530,25 @@
                         if (newLines.length > 1) {
                             // the first and the last new lines are separate existing lines, modify them
                             startLine.content = part1 + newLines[0];
+                            startLine.classNames = mergeClasses(classesBetween(startLine.classNames, 0, part1.length), makeClassDescriptor(className, part1.length, newLines[0].length));
                             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);
+                                auxLine = this.insertRow(j, newLines[newLines.length - 1], className, true);
+                                insertClass(auxLine.classNames, auxLine.content.length, part2.length);
+                                auxLine.content = auxLine.content + part2;
                                 j++;
                             }
                             else {
-                                endLine.content = newLines[newLines.length - 1] + part2;
+                                s = newLines[newLines.length - 1];
+                                endLine.content = s + part2;
+                                replaceClassesBeforeColumn(endLine.classNames, endCol, className, s.length);
                                 n--; // retain the last line
                             }
                         }
                         else {
                             // the first and the last new lines are the same existing line
                             startLine.content = part1 + newLines[0] + part2;
+                            startLine.classNames = mergeClasses(classesBetween(startLine.classNames, 0, part1.length), makeClassDescriptor(className, part1.length, newLines[0].length), classesBetween(endLine.classNames, endCol, endCol + part2.length));
                         }
                         // remove the rest of the old lines
                         while (n > 0) {
@@ -422,7 +588,9 @@
                         startRow = lines.length - 1,
                         i, lineDescriptor;
                     if (newLines.length == 0) return; // nothing to do
+                    if (typeof className !== 'string') className = 'output';
                     lineDescriptor = lines[startRow];
+                    insertClass(lineDescriptor.classNames, className, lineDescriptor.content.length, newLines[0].length);
                     lineDescriptor.content = lineDescriptor.content + newLines[0];
                     for (i = 1; i < newLines.length; i++) {
                         lineDescriptor = this.insertRow(startRow + i, newLines[i], className, true);
@@ -441,20 +609,25 @@
                         part1 = currentContent.substring(0, currentCol),
                         part2 = currentContent.substring(currentCol),
                         newLines = formNewLines(text),
-                        i, n;
+                        i, n, auxLine;
+                    if (typeof className !== 'string') className = 'output';
                     if (newLines.length == 0) return; // nothing to do
                     else if (newLines.length == 1) {
+                        insertClass(currentLine.classNames, className, part1.length, newLines[0].length);
                         currentLine.content = part1 + newLines[0] + part2;
                         if (!dontMoveCursor) currentCol += newLines[0].length;
                         renderLine(currentLine);
                     }
                     else {
+                        currentLine.classNames = mergeClasses(classesBetween(currentLine.classNames, 0, part1.length), makeClassDescriptor(className, part1.length, newLines[0].length));
                         currentLine.content = part1 + newLines[0];
                         n = newLines.length - 1;
                         for (i = 1; i < n; i++) {
                             this.insertRow(startRow + i, newLines[i], className, true);
                         }
-                        this.insertRow(startRow + n, newLines[n] + part2, className, true);
+                        auxLine = this.insertRow(startRow + n, newLines[n], className, true);
+                        insertClass(auxLine.classNames, auxLine.content.length, part2.length);
+                        auxLine.content = auxLine.content + part2;
                         if (!dontMoveCursor) {
                             currentRow = startRow + n;
                             currentCol = newLines[n].length;
@@ -485,6 +658,7 @@
                         leftmost = typeof this.leftmostCol === 'number' && this.leftmostCol || 0;
                     if ((currentCol > leftmost) && (currentCol <= content.length)) {
                         currentCol--;
+                        removeClasses(lineDescriptor.classNames, currentCol, 1);
                         lineDescriptor.content = content.substring(0, currentCol) + content.substring(currentCol + 1);
                         renderLine(lineDescriptor);
                     }
@@ -494,6 +668,7 @@
                     var lineDescriptor = lines[currentRow],
                         content = lineDescriptor.content;
                     if ((currentCol >= 0) && (currentCol < content.length)) {
+                        removeClasses(lineDescriptor.classNames, currentCol, 1);
                         lineDescriptor.content = content.substring(0, currentCol) + content.substring(currentCol + 1);
                         renderLine(lineDescriptor);
                     }
@@ -739,7 +914,7 @@
             lines.push({content: options.greeting, className: 'greeting'});
             currentRow++;
         }
-        lines.push({content: '', className: 'input'}); // initial input line
+        lines.push({content: '', classNames: []}); // initial input line
         render();
 
         return handler;
diff --git a/js/codeq/startup.js b/js/codeq/startup.js
index 980d3d7..730a9f8 100644
--- a/js/codeq/startup.js
+++ b/js/codeq/startup.js
@@ -77,6 +77,7 @@ $(document).ready(function () {
                             })
                             .fail(function (reason) {
                                 $('#disabled').css('display', 'none');
+                                codeq.log.error('Login request failed: ' + reason, reason);
                                 alert('Login request failed: ' + reason);
                             })
                             .done();
-- 
cgit v1.2.1