function char_size() { var span = $(' ' + '').appendTo($('.terminal-output')); var result = { width: span.width(), height: span.outerHeight() }; span.remove(); return result; } function nSpaces(n){ //+ 1 because join inserts between return Array(n + 1).join(" "); } function encodeSpaces(string){ return string.replace(/ /g, " "); } function htmlEncode(value){ return encodeSpaces($('
').text(value).html()); } /* In emacs: Frame = entire emacs window window = a portion of the window rendering a certain buffer Naive way to render the visible parts of a buffer Doesn't do most of the rendering done by emacs. E.g. doesn't handle: * invisible text * overlay * display table * color * ... */ function FrameRenderer(width, height){ this.width = width; this.height = height; //index i = line i of frame //each value is an array of fragments of buffers which together represent text at index i //fragment = one line of one window this.lines = []; } FrameRenderer.prototype.getLine = function(index){ if(index > this.height - 1){ var error = new RangeError(index + " is outside valid lines (min 0, max " + (this.height - 1) + ")"); error.lineRequested = index; throw error; } if(this.lines[index]) return this.lines[index]; else{ this.lines[index] = []; return this.lines[index]; } } /* @throws RangeError no segment corresponding to x, y @returns {object} segment including where x, y is located */ FrameRenderer.prototype.getFragmentAt = function(x, y){ if(!this.lines[y]) throw new RangeError("no line " + y); var result; if(this.lines[y].some(function(fragment){ if(fragment.columnLeft <= x && x <= fragment.columnLeft + fragment.text.length - 1){ result = fragment; return true; } })){ return result; } else throw new RangeError( "no fragment at column " + x + " for the line " + y); } /* Add data to the segment corresponding to x, y @param {object} data key/values to be added to the segment @throws RangeError no segment corresponding to x, y */ FrameRenderer.prototype.addDataToFragment = function(x, y, data){ $.extend(this.getFragmentAt(x, y), data); } FrameRenderer.prototype.processData = function(displayData){ this.addWindows(displayData.windows); } FrameRenderer.prototype.addWindows = function(windows){ windows.forEach(function(window){ this.addWindow(window); }, this); } //TODO missing hscroll handling FrameRenderer.prototype.addWindow = function(window){ //split. this excludes "\\n" (backslash n typed in a buffer) var lines = window.text.replace(/\t/g, nSpaces(window.tabWidth)).split(/\n/g); var windowWidth = window.right - window.left + 1; //split lines too big. Emulates wrap //the place at which line are split can vary //saw case where it is width (w/ gui) anohther width -1 (terminal) lines = $.map(lines, function(line){//map flattens if(line == "") return line; return line.split( //parenthesis to include match in results RegExp("(.{" + (windowWidth - 2) + "})") ).filter(Boolean);//remove "" from the array. "" is falsey. }); lines[window.bottom - window.top] = window.modeLine.slice(0, windowWidth - 1); //make sure lines don't expand past :bottom //Our rendering algorithm (line split, horizontal scroll,...) might //not fit the one used by emacs. Make sure we don't have too many lines lines = lines.splice(0, window.bottom - window.top + 1) lines.forEach(function(line, index){ var fragment = {columnLeft: window.left, columnRight: window.right, text: line}; if(window.point && window.point.y == index + window.top){ fragment.point = window.point; } this.getLine(window.top + index).push(fragment); //TODO ordered insertion this.getLine(window.top + index).sort(function(a, b){ return a.columnLeft - b.columnLeft }); }, this); } FrameRenderer.prototype.renderLine = function(line, target){ var that = this; function reduceLine(acc, windowSegment){ target.append( htmlEncode(nSpaces((windowSegment.columnLeft - acc.charCount)))); target.append(that.renderSegment(windowSegment)); return {charCount: acc.charCount + //previously added windowSegment.text.length + //length of actual text in this segment (windowSegment.columnLeft - acc.charCount)}; //spaces added } //accumulator is the rendered string + the number of chars in the rendered string //must keep track of number of chars handled because //one char in the source may be rendered with multiple chars. e.g.: ' ' => var result = line.reduce(reduceLine, {charCount:0}); return target.append('