overleaf/services/document-updater/app/coffee/sharejs/json-api.js

273 lines
9.2 KiB
JavaScript

/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
// API for JSON OT
let json;
if (typeof WEB === 'undefined') { json = require('./json'); }
if (typeof WEB !== 'undefined' && WEB !== null) {
const {
extendDoc
} = exports;
exports.extendDoc = function(name, fn) {
SubDoc.prototype[name] = fn;
return extendDoc(name, fn);
};
}
const depath = function(path) {
if ((path.length === 1) && (path[0].constructor === Array)) {
return path[0];
} else { return path; }
};
class SubDoc {
constructor(doc, path) {
this.doc = doc;
this.path = path;
}
at(...path) { return this.doc.at(this.path.concat(depath(path))); }
get() { return this.doc.getAt(this.path); }
// for objects and lists
set(value, cb) { return this.doc.setAt(this.path, value, cb); }
// for strings and lists.
insert(pos, value, cb) { return this.doc.insertAt(this.path, pos, value, cb); }
// for strings
del(pos, length, cb) { return this.doc.deleteTextAt(this.path, length, pos, cb); }
// for objects and lists
remove(cb) { return this.doc.removeAt(this.path, cb); }
push(value, cb) { return this.insert(this.get().length, value, cb); }
move(from, to, cb) { return this.doc.moveAt(this.path, from, to, cb); }
add(amount, cb) { return this.doc.addAt(this.path, amount, cb); }
on(event, cb) { return this.doc.addListener(this.path, event, cb); }
removeListener(l) { return this.doc.removeListener(l); }
// text API compatibility
getLength() { return this.get().length; }
getText() { return this.get(); }
}
const traverse = function(snapshot, path) {
const container = {data:snapshot};
let key = 'data';
let elem = container;
for (let p of Array.from(path)) {
elem = elem[key];
key = p;
if (typeof elem === 'undefined') { throw new Error('bad path'); }
}
return {elem, key};
};
const pathEquals = function(p1, p2) {
if (p1.length !== p2.length) { return false; }
for (let i = 0; i < p1.length; i++) {
const e = p1[i];
if (e !== p2[i]) { return false; }
}
return true;
};
json.api = {
provides: {json:true},
at(...path) { return new SubDoc(this, depath(path)); },
get() { return this.snapshot; },
set(value, cb) { return this.setAt([], value, cb); },
getAt(path) {
const {elem, key} = traverse(this.snapshot, path);
return elem[key];
},
setAt(path, value, cb) {
const {elem, key} = traverse(this.snapshot, path);
const op = {p:path};
if (elem.constructor === Array) {
op.li = value;
if (typeof elem[key] !== 'undefined') { op.ld = elem[key]; }
} else if (typeof elem === 'object') {
op.oi = value;
if (typeof elem[key] !== 'undefined') { op.od = elem[key]; }
} else { throw new Error('bad path'); }
return this.submitOp([op], cb);
},
removeAt(path, cb) {
const {elem, key} = traverse(this.snapshot, path);
if (typeof elem[key] === 'undefined') { throw new Error('no element at that path'); }
const op = {p:path};
if (elem.constructor === Array) {
op.ld = elem[key];
} else if (typeof elem === 'object') {
op.od = elem[key];
} else { throw new Error('bad path'); }
return this.submitOp([op], cb);
},
insertAt(path, pos, value, cb) {
const {elem, key} = traverse(this.snapshot, path);
const op = {p:path.concat(pos)};
if (elem[key].constructor === Array) {
op.li = value;
} else if (typeof elem[key] === 'string') {
op.si = value;
}
return this.submitOp([op], cb);
},
moveAt(path, from, to, cb) {
const op = [{p:path.concat(from), lm:to}];
return this.submitOp(op, cb);
},
addAt(path, amount, cb) {
const op = [{p:path, na:amount}];
return this.submitOp(op, cb);
},
deleteTextAt(path, length, pos, cb) {
const {elem, key} = traverse(this.snapshot, path);
const op = [{p:path.concat(pos), sd:elem[key].slice(pos, (pos + length))}];
return this.submitOp(op, cb);
},
addListener(path, event, cb) {
const l = {path, event, cb};
this._listeners.push(l);
return l;
},
removeListener(l) {
const i = this._listeners.indexOf(l);
if (i < 0) { return false; }
this._listeners.splice(i, 1);
return true;
},
_register() {
this._listeners = [];
this.on('change', function(op) {
return (() => {
const result = [];
for (let c of Array.from(op)) {
var i;
if ((c.na !== undefined) || (c.si !== undefined) || (c.sd !== undefined)) {
// no change to structure
continue;
}
var to_remove = [];
for (i = 0; i < this._listeners.length; i++) {
// Transform a dummy op by the incoming op to work out what
// should happen to the listener.
const l = this._listeners[i];
const dummy = {p:l.path, na:0};
const xformed = this.type.transformComponent([], dummy, c, 'left');
if (xformed.length === 0) {
// The op was transformed to noop, so we should delete the listener.
to_remove.push(i);
} else if (xformed.length === 1) {
// The op remained, so grab its new path into the listener.
l.path = xformed[0].p;
} else {
throw new Error("Bad assumption in json-api: xforming an 'si' op will always result in 0 or 1 components.");
}
}
to_remove.sort((a, b) => b - a);
result.push((() => {
const result1 = [];
for (i of Array.from(to_remove)) {
result1.push(this._listeners.splice(i, 1));
}
return result1;
})());
}
return result;
})();
});
return this.on('remoteop', function(op) {
return (() => {
const result = [];
for (var c of Array.from(op)) {
var match_path = c.na === undefined ? c.p.slice(0, c.p.length-1) : c.p;
result.push((() => {
const result1 = [];
for (let {path, event, cb} of Array.from(this._listeners)) {
var common;
if (pathEquals(path, match_path)) {
switch (event) {
case 'insert':
if ((c.li !== undefined) && (c.ld === undefined)) {
result1.push(cb(c.p[c.p.length-1], c.li));
} else if ((c.oi !== undefined) && (c.od === undefined)) {
result1.push(cb(c.p[c.p.length-1], c.oi));
} else if (c.si !== undefined) {
result1.push(cb(c.p[c.p.length-1], c.si));
} else {
result1.push(undefined);
}
break;
case 'delete':
if ((c.li === undefined) && (c.ld !== undefined)) {
result1.push(cb(c.p[c.p.length-1], c.ld));
} else if ((c.oi === undefined) && (c.od !== undefined)) {
result1.push(cb(c.p[c.p.length-1], c.od));
} else if (c.sd !== undefined) {
result1.push(cb(c.p[c.p.length-1], c.sd));
} else {
result1.push(undefined);
}
break;
case 'replace':
if ((c.li !== undefined) && (c.ld !== undefined)) {
result1.push(cb(c.p[c.p.length-1], c.ld, c.li));
} else if ((c.oi !== undefined) && (c.od !== undefined)) {
result1.push(cb(c.p[c.p.length-1], c.od, c.oi));
} else {
result1.push(undefined);
}
break;
case 'move':
if (c.lm !== undefined) {
result1.push(cb(c.p[c.p.length-1], c.lm));
} else {
result1.push(undefined);
}
break;
case 'add':
if (c.na !== undefined) {
result1.push(cb(c.na));
} else {
result1.push(undefined);
}
break;
default:
result1.push(undefined);
}
} else if ((common = this.type.commonPath(match_path, path)) != null) {
if (event === 'child op') {
if (match_path.length === path.length && path.length === common) {
throw new Error("paths match length and have commonality, but aren't equal?");
}
const child_path = c.p.slice(common+1);
result1.push(cb(child_path, c));
} else {
result1.push(undefined);
}
} else {
result1.push(undefined);
}
}
return result1;
})());
}
return result;
})();
});
}
};