ghclive-0.1.0.1: static/document.js
function evtJson(evt, f) {
var str;
if(evt.data instanceof Blob) {
var reader = new FileReader();
reader.onload = function() {
f($.parseJSON(reader.result));
}
reader.readAsText(evt.data);
} else {
f($.parseJSON(evt.data));
}
}
// a map that first converts the keys to string
function SerMap() {
this.map = new Map();
}
SerMap.prototype.set = function(k,v) {
this.map.set(""+k,v);
}
SerMap.prototype.get = function(k) {
return this.map.get(""+k);
}
// specialized atom map, can cause problems with high (> 2000) client ids
function AtomMap() {
this.map = {};
}
AtomMap.prototype.set = function(k,v) {
this.map[k[0]*k[1]+1] = v;
}
AtomMap.prototype.get = function(k,v) {
return this.map[k[0]*k[1]+1];
}
spliceArr = function(arr,i,n,a) {
a.unshift(i,n);
arr.splice.apply(arr,a);
a.shift();
a.shift();
return arr;
}
concatArr = function(arr1,arr2) {
var a = arr1.slice(0);
a.push.apply(a,arr2);
return a;
}
var clientId = -1;
// we work with server-synchronized strictly increasing timestamps
var tsOffset = 0;
var prevTs = 0;
function setClientId(cid) {
// console.log("setting clientid");
// console.log(cid);
clientId = cid;
}
function updateTimestamp(t) {
tsOffset = t - new Date().getTime();
}
function getTimestamp() {
var t = new Date().getTime() + tsOffset;
if(t <= prevTs) t = prevTs + 1;
prevTs = t;
return t;
}
function mkAtomId() {
return [getTimestamp(),clientId];
}
function Document() {
this.lines = [[0,0]]; // lines[line][ch] -> [time,clientid], first ch of every line is line separator atomid (or [0,0] for first)
this.atoms = new AtomMap(); // [time,clientid] -> { id, prev, next, ch, removed }
var firstId = [0,0];
this.atoms.set(firstId, { id: firstId, t: 0, c: 0, prev: null, next: null, ch: ' ', removed: true });
}
// first atom
Document.prototype.first = function() {
return this.atoms.get([0,0]);
}
// set document to list of [clientid,time,char,removed]
// expects first atom id to be [0,0]
Document.prototype.setDocument = function(d) {
// console.log('setdocument');
// console.log(d);
this.lines = [];
this.atoms = new AtomMap();
var currentLine = [];
var atomId = function(n) { return [d[n][0],d[n][1]] };
var newAtoms = new Array(d.length);
for(var i=0;i<d.length;i++) {
var x = d[i];
var aid = atomId(i);
var atom = { id: aid, ch: x[2], removed: x[3] };
newAtoms[i] = atom;
this.atoms.set(aid,atom);
if(!atom.removed || (aid[0] == 0 && aid[1] == 0)) {
if(atom.ch == '\n') {
this.lines.push(currentLine);
currentLine = [];
}
currentLine.push(aid);
}
}
this.lines.push(currentLine);
for(var i=0;i<newAtoms.length;i++) {
newAtoms[i].prev = (i > 0) ? newAtoms[i-1] : null;
newAtoms[i].next = (i < newAtoms.length - 1) ? newAtoms[i+1] : null;
}
}
// returns current version as string
Document.prototype.currentVersion = function() {
var str = '';
var a = this.first();
while(true) {
// console.log('iteration');
// console.log(a);
if(!a.removed) {
str += a.ch;
}
if(!a.next) break;
a = a.next;
}
return str;
}
function aidEq(aid1,aid2) {
if(aid1 == null && aid2 == null) return true;
if(aid1 == null || aid2 == null) return false;
return (aid1[0] == aid2[0] && aid1[1] == aid2[1]);
}
function posEq(pos1,pos2) {
return pos1.ch == pos2.ch && pos1.line == pos2.line;
}
// replace a range { line, ch } - { line, ch } by str
// returns modification messages
Document.prototype.replace = function(from,to,strs) {
var mods = [];
var start = this.idbefore(from); // insert new string after this atom
if(start == null) start = [0,0];
var end = this.pos2atomid(to); // insert new string before this
var aid = this.nextid(start);
var beforeEnd = this.idbefore(to); // last char to be removed
// console.log("start: " + this.atomStr(start) + " end: " + this.atomStr(end));
// console.log("aid: " + aid + " beforeEnd: " + beforeEnd);
// first mark atoms as removed
var atom = (aid != null) ? this.atoms.get(aid) : null;
while(atom != null && !aidEq(atom.id,end) && !posEq(from,to)) {
if(!atom.removed) {
atom.removed = true;
mods.push({ action: 'remove', atom: atom.id });
}
if(aidEq(atom.id, beforeEnd)) {
break;
}
atom = atom.next;
}
// now remove original string
var lastPost = this.lines[to.line].slice(to.ch+1); // part that we have to stick back to the last line
var firstPre = this.lines[from.line].slice(0,from.ch+1);
this.lines.splice(from.line, to.line - from.line + 1); // fixme don't do this if single line
// insert new chars
var newAtoms = [];
var newLines = [];
var currentLine = [];
for(var l=0;l<strs.length;l++) {
var str = strs[l];
for(var i=0;i<str.length;i++) {
var aid = mkAtomId();
var atom = { id: aid, ch: str[i], removed: false }
newAtoms.push(atom);
currentLine.push(aid);
}
newLines.push(currentLine);
currentLine = [];
if(l<strs.length-1) {
var linebr = { id: mkAtomId(), ch: '\n', removed: false};
newAtoms.push(linebr);
currentLine.push(linebr.id);
}
}
for(var i=0;i<newAtoms.length;i++) {
if(i>=1) { newAtoms[i].prev = newAtoms[i-1]; }
if(i<newAtoms.length-1) { newAtoms[i].next = newAtoms[i+1]; }
}
if(newAtoms.length > 0) {
var startAtom = this.atoms.get(start);
var afterStart = this.atoms.get(start).next;
newAtoms[0].prev = startAtom;
startAtom.next = newAtoms[0];
newAtoms[newAtoms.length-1].next = afterStart;
if(afterStart != null) { afterStart.prev = newAtoms[newAtoms.length-1]; }
}
for(var i=0;i<newAtoms.length;i++) {
var a = newAtoms[i];
mods.push({ action: 'insert', after: a.prev.id, id: a.id, ch: a.ch });
this.atoms.set(a.id,a);
}
if(newLines.length > 0) {
newLines[0] = concatArr(firstPre,newLines[0]);
newLines[newLines.length-1] = concatArr(newLines[newLines.length-1],lastPost);
spliceArr(this.lines,from.line, 0, newLines);
} else {
this.lines.splice(from.line,0,concatArr(firstPre,lastPost));
}
this.dump();
return mods;
}
// apply an insert/delete operation
Document.prototype.applyOp = function(op) {
function smaller(aid1, aid2) {
return aid1[0] < aid2[0] || (aid1[0] == aid2[0] && aid1[1] < aid2[1]);
}
function isFirst(aid) {
return aid[0] == 0 && aid[1] == 0;
}
if(op.action == 'insert') {
// console.log("inserting");
// console.log(op);
var atomAfter = this.atoms.get(op.after);
if(this.atoms.get(op.id) != null) {
return false; // already applied op
} else if(atomAfter == null) {
console.log("invalid position");
console.log(atomAfter);
console.log(op.id);
console.log(this.dumpStructure());
return false;
} else {
// first find insertion position
var atom = atomAfter;
while(true) {
if(atom.next == null || smaller(atom.next.id, op.id)) {
var newAtom = { id: op.id, ch: op.ch, removed: false, prev: atom, next: atom.next }
if(atom.next != null) {
atom.next.prev = newAtom;
}
atom.next = newAtom;
this.atoms.set(newAtom.id, newAtom);
break;
}
atom = atom.next;
}
// console.log("inserted after:");
// this.dumpAtom(aid);
// now search back to first visible, or [0,0]
// aid = op.after;
while(!isFirst(atom.id) && atom.removed) {
atom = atom.prev;
}
// find aid in lines
idx = this.findIndex(atom.id);
var l=idx[0];
var c=idx[1];
// console.log(this.dumpStructure());
// console.log("line index: " + l + ":" + c);
// and add our new id
if(op.ch == '\n') {
var remain = this.lines[l].slice(c+1);
this.lines[l] = this.lines[l].slice(0,c+1);
remain.unshift(op.id);
this.lines.splice(l+1,0,remain);
} else {
this.lines[l].splice(c+1,0,op.id);
}
// console.log(this.dumpStructure());
return true;
}
} else if(op.action == 'remove') {
var atom = this.atoms.get(op.id);
if(atom == null || atom.removed) {
return false;
} else {
atom.removed = true;
idx = this.findIndex(atom.id);
var l = idx[0];
var c = idx[1];
if(atom.ch == '\n') {
var remain = this.lines[l].slice(1);
this.lines[l-1] = concatArr(this.lines[l-1], remain);
this.lines.splice(l,1);
} else {
this.lines[l].splice(c,1);
}
return true;
}
} else {
return false;
}
}
Document.prototype.findIndex = function(aid) {
for(var l=0;l<this.lines.length;l++) {
var cl = this.lines[l];
for(var c=0;c<cl.length;c++) {
if(cl[c][0] == aid[0] && cl[c][1] == aid[1]) {
return [l,c];
}
}
}
return null;
}
// convert a line,ch position to an atom id
Document.prototype.pos2atomid = function(pos) {
return this.lines[pos.line][pos.ch+1];
}
// atom id before cursor { line: n, ch: m } (both start at 0)
Document.prototype.idbefore = function(pos) {
return this.lines[pos.line][pos.ch];
}
Document.prototype.dumpAtom = function(aid) {
console.log(this.atomStr(aid));
}
Document.prototype.dumpLine = function(aids) {
console.log(this.lineStr(aids));
}
Document.prototype.lineStr = function(aids) {
var str='';
for(var i=0;i<aids.length;i++) {
str += this.atomStr(aids[i]);
}
return str;
}
Document.prototype.atomStr = function(aid) {
if(aid == null) {
return "[null]";
}
var atom = this.atoms.get(aid);
var str = '';
function charify(c) { return (c=='\n') ? "\\n" : c; }
if(atom == null) {
str += "[" + [aid[0],aid[1]] + "](not found)";
aid = null;
} else {
str += "[" + [aid[0],aid[1],"'" + charify(atom.ch) + "'",atom.removed] + "]";
aid = atom.next;
}
return str;
}
// fixme remove usage of this
Document.prototype.previd = function(aid) {
var atom = this.atoms.get(aid);
if(atom == null || atom.prev == null) return null;
return atom.prev.id;
}
// fixme remove usage of this
Document.prototype.nextid = function(aid) {
var atom = this.atoms.get(aid);
if(atom == null || atom.next == null) return null;
return atom.next.id;
}
Document.prototype.dump = function() {
console.log(this.dumpStructure());
}
Document.prototype.dumpStructure = function() {
return ''; // comment out for dumps
str = "map:\n";
var aid = [0,0]; // this.first();
while(aid != null) {
str += this.atomStr(aid) + "\n";
aid = this.nextid(aid);
}
str += "lines:\n";
for(var i=0;i<this.lines.length;i++) {
str += this.lineStr(this.lines[i]) + "\n";
}
str += "current:\n" + this.currentVersion();
return str;
}