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

534 lines
16 KiB
JavaScript

/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
// This is the implementation of the JSON OT type.
//
// Spec is here: https://github.com/josephg/ShareJS/wiki/JSON-Operations
let text;
if (typeof WEB !== 'undefined' && WEB !== null) {
({
text
} = exports.types);
} else {
text = require('./text');
}
const json = {};
json.name = 'json';
json.create = () => null;
json.invertComponent = function(c) {
const c_ = {p: c.p};
if (c.si !== undefined) { c_.sd = c.si; }
if (c.sd !== undefined) { c_.si = c.sd; }
if (c.oi !== undefined) { c_.od = c.oi; }
if (c.od !== undefined) { c_.oi = c.od; }
if (c.li !== undefined) { c_.ld = c.li; }
if (c.ld !== undefined) { c_.li = c.ld; }
if (c.na !== undefined) { c_.na = -c.na; }
if (c.lm !== undefined) {
c_.lm = c.p[c.p.length-1];
c_.p = c.p.slice(0, c.p.length - 1).concat([c.lm]);
}
return c_;
};
json.invert = op => Array.from(op.slice().reverse()).map((c) => json.invertComponent(c));
json.checkValidOp = function(op) {};
const isArray = o => Object.prototype.toString.call(o) === '[object Array]';
json.checkList = function(elem) {
if (!isArray(elem)) { throw new Error('Referenced element not a list'); }
};
json.checkObj = function(elem) {
if (elem.constructor !== Object) { throw new Error(`Referenced element not an object (it was ${JSON.stringify(elem)})`); }
};
json.apply = function(snapshot, op) {
json.checkValidOp(op);
op = clone(op);
const container = {data: clone(snapshot)};
try {
for (let i = 0; i < op.length; i++) {
const c = op[i];
let parent = null;
let parentkey = null;
let elem = container;
let key = 'data';
for (let p of Array.from(c.p)) {
parent = elem;
parentkey = key;
elem = elem[key];
key = p;
if (parent == null) { throw new Error('Path invalid'); }
}
if (c.na !== undefined) {
// Number add
if (typeof elem[key] !== 'number') { throw new Error('Referenced element not a number'); }
elem[key] += c.na;
} else if (c.si !== undefined) {
// String insert
if (typeof elem !== 'string') { throw new Error(`Referenced element not a string (it was ${JSON.stringify(elem)})`); }
parent[parentkey] = elem.slice(0, key) + c.si + elem.slice(key);
} else if (c.sd !== undefined) {
// String delete
if (typeof elem !== 'string') { throw new Error('Referenced element not a string'); }
if (elem.slice(key, key + c.sd.length) !== c.sd) { throw new Error('Deleted string does not match'); }
parent[parentkey] = elem.slice(0, key) + elem.slice(key + c.sd.length);
} else if ((c.li !== undefined) && (c.ld !== undefined)) {
// List replace
json.checkList(elem);
// Should check the list element matches c.ld
elem[key] = c.li;
} else if (c.li !== undefined) {
// List insert
json.checkList(elem);
elem.splice(key, 0, c.li);
} else if (c.ld !== undefined) {
// List delete
json.checkList(elem);
// Should check the list element matches c.ld here too.
elem.splice(key, 1);
} else if (c.lm !== undefined) {
// List move
json.checkList(elem);
if (c.lm !== key) {
const e = elem[key];
// Remove it...
elem.splice(key, 1);
// And insert it back.
elem.splice(c.lm, 0, e);
}
} else if (c.oi !== undefined) {
// Object insert / replace
json.checkObj(elem);
// Should check that elem[key] == c.od
elem[key] = c.oi;
} else if (c.od !== undefined) {
// Object delete
json.checkObj(elem);
// Should check that elem[key] == c.od
delete elem[key];
} else {
throw new Error('invalid / missing instruction in op');
}
}
} catch (error) {
// TODO: Roll back all already applied changes. Write tests before implementing this code.
throw error;
}
return container.data;
};
// Checks if two paths, p1 and p2 match.
json.pathMatches = function(p1, p2, ignoreLast) {
if (p1.length !== p2.length) { return false; }
for (let i = 0; i < p1.length; i++) {
const p = p1[i];
if ((p !== p2[i]) && (!ignoreLast || (i !== (p1.length - 1)))) { return false; }
}
return true;
};
json.append = function(dest, c) {
let last;
c = clone(c);
if ((dest.length !== 0) && json.pathMatches(c.p, (last = dest[dest.length - 1]).p)) {
if ((last.na !== undefined) && (c.na !== undefined)) {
return dest[dest.length - 1] = { p: last.p, na: last.na + c.na };
} else if ((last.li !== undefined) && (c.li === undefined) && (c.ld === last.li)) {
// insert immediately followed by delete becomes a noop.
if (last.ld !== undefined) {
// leave the delete part of the replace
return delete last.li;
} else {
return dest.pop();
}
} else if ((last.od !== undefined) && (last.oi === undefined) &&
(c.oi !== undefined) && (c.od === undefined)) {
return last.oi = c.oi;
} else if ((c.lm !== undefined) && (c.p[c.p.length-1] === c.lm)) {
return null; // don't do anything
} else {
return dest.push(c);
}
} else {
return dest.push(c);
}
};
json.compose = function(op1, op2) {
json.checkValidOp(op1);
json.checkValidOp(op2);
const newOp = clone(op1);
for (let c of Array.from(op2)) { json.append(newOp, c); }
return newOp;
};
json.normalize = function(op) {
const newOp = [];
if (!isArray(op)) { op = [op]; }
for (let c of Array.from(op)) {
if (c.p == null) { c.p = []; }
json.append(newOp, c);
}
return newOp;
};
// hax, copied from test/types/json. Apparently this is still the fastest way to deep clone an object, assuming
// we have browser support for JSON.
// http://jsperf.com/cloning-an-object/12
var clone = o => JSON.parse(JSON.stringify(o));
json.commonPath = function(p1, p2) {
p1 = p1.slice();
p2 = p2.slice();
p1.unshift('data');
p2.unshift('data');
p1 = p1.slice(0, p1.length-1);
p2 = p2.slice(0, p2.length-1);
if (p2.length === 0) { return -1; }
let i = 0;
while ((p1[i] === p2[i]) && (i < p1.length)) {
i++;
if (i === p2.length) {
return i-1;
}
}
};
// transform c so it applies to a document with otherC applied.
json.transformComponent = function(dest, c, otherC, type) {
let oc;
c = clone(c);
if (c.na !== undefined) { c.p.push(0); }
if (otherC.na !== undefined) { otherC.p.push(0); }
const common = json.commonPath(c.p, otherC.p);
const common2 = json.commonPath(otherC.p, c.p);
const cplength = c.p.length;
const otherCplength = otherC.p.length;
if (c.na !== undefined) { c.p.pop(); } // hax
if (otherC.na !== undefined) { otherC.p.pop(); }
if (otherC.na) {
if ((common2 != null) && (otherCplength >= cplength) && (otherC.p[common2] === c.p[common2])) {
if (c.ld !== undefined) {
oc = clone(otherC);
oc.p = oc.p.slice(cplength);
c.ld = json.apply(clone(c.ld), [oc]);
} else if (c.od !== undefined) {
oc = clone(otherC);
oc.p = oc.p.slice(cplength);
c.od = json.apply(clone(c.od), [oc]);
}
}
json.append(dest, c);
return dest;
}
if ((common2 != null) && (otherCplength > cplength) && (c.p[common2] === otherC.p[common2])) {
// transform based on c
if (c.ld !== undefined) {
oc = clone(otherC);
oc.p = oc.p.slice(cplength);
c.ld = json.apply(clone(c.ld), [oc]);
} else if (c.od !== undefined) {
oc = clone(otherC);
oc.p = oc.p.slice(cplength);
c.od = json.apply(clone(c.od), [oc]);
}
}
if (common != null) {
let from, p, to;
const commonOperand = cplength === otherCplength;
// transform based on otherC
if (otherC.na !== undefined) {
// this case is handled above due to icky path hax
} else if ((otherC.si !== undefined) || (otherC.sd !== undefined)) {
// String op vs string op - pass through to text type
if ((c.si !== undefined) || (c.sd !== undefined)) {
if (!commonOperand) { throw new Error("must be a string?"); }
// Convert an op component to a text op component
const convert = function(component) {
const newC = {p:component.p[component.p.length - 1]};
if (component.si) {
newC.i = component.si;
} else {
newC.d = component.sd;
}
return newC;
};
const tc1 = convert(c);
const tc2 = convert(otherC);
const res = [];
text._tc(res, tc1, tc2, type);
for (let tc of Array.from(res)) {
const jc = { p: c.p.slice(0, common) };
jc.p.push(tc.p);
if (tc.i != null) { jc.si = tc.i; }
if (tc.d != null) { jc.sd = tc.d; }
json.append(dest, jc);
}
return dest;
}
} else if ((otherC.li !== undefined) && (otherC.ld !== undefined)) {
if (otherC.p[common] === c.p[common]) {
// noop
if (!commonOperand) {
// we're below the deleted element, so -> noop
return dest;
} else if (c.ld !== undefined) {
// we're trying to delete the same element, -> noop
if ((c.li !== undefined) && (type === 'left')) {
// we're both replacing one element with another. only one can
// survive!
c.ld = clone(otherC.li);
} else {
return dest;
}
}
}
} else if (otherC.li !== undefined) {
if ((c.li !== undefined) && (c.ld === undefined) && commonOperand && (c.p[common] === otherC.p[common])) {
// in li vs. li, left wins.
if (type === 'right') {
c.p[common]++;
}
} else if (otherC.p[common] <= c.p[common]) {
c.p[common]++;
}
if (c.lm !== undefined) {
if (commonOperand) {
// otherC edits the same list we edit
if (otherC.p[common] <= c.lm) {
c.lm++;
}
}
}
// changing c.from is handled above.
} else if (otherC.ld !== undefined) {
if (c.lm !== undefined) {
if (commonOperand) {
if (otherC.p[common] === c.p[common]) {
// they deleted the thing we're trying to move
return dest;
}
// otherC edits the same list we edit
p = otherC.p[common];
from = c.p[common];
to = c.lm;
if ((p < to) || ((p === to) && (from < to))) {
c.lm--;
}
}
}
if (otherC.p[common] < c.p[common]) {
c.p[common]--;
} else if (otherC.p[common] === c.p[common]) {
if (otherCplength < cplength) {
// we're below the deleted element, so -> noop
return dest;
} else if (c.ld !== undefined) {
if (c.li !== undefined) {
// we're replacing, they're deleting. we become an insert.
delete c.ld;
} else {
// we're trying to delete the same element, -> noop
return dest;
}
}
}
} else if (otherC.lm !== undefined) {
if ((c.lm !== undefined) && (cplength === otherCplength)) {
// lm vs lm, here we go!
from = c.p[common];
to = c.lm;
const otherFrom = otherC.p[common];
const otherTo = otherC.lm;
if (otherFrom !== otherTo) {
// if otherFrom == otherTo, we don't need to change our op.
// where did my thing go?
if (from === otherFrom) {
// they moved it! tie break.
if (type === 'left') {
c.p[common] = otherTo;
if (from === to) { // ugh
c.lm = otherTo;
}
} else {
return dest;
}
} else {
// they moved around it
if (from > otherFrom) {
c.p[common]--;
}
if (from > otherTo) {
c.p[common]++;
} else if (from === otherTo) {
if (otherFrom > otherTo) {
c.p[common]++;
if (from === to) { // ugh, again
c.lm++;
}
}
}
// step 2: where am i going to put it?
if (to > otherFrom) {
c.lm--;
} else if (to === otherFrom) {
if (to > from) {
c.lm--;
}
}
if (to > otherTo) {
c.lm++;
} else if (to === otherTo) {
// if we're both moving in the same direction, tie break
if (((otherTo > otherFrom) && (to > from)) ||
((otherTo < otherFrom) && (to < from))) {
if (type === 'right') {
c.lm++;
}
} else {
if (to > from) {
c.lm++;
} else if (to === otherFrom) {
c.lm--;
}
}
}
}
}
} else if ((c.li !== undefined) && (c.ld === undefined) && commonOperand) {
// li
from = otherC.p[common];
to = otherC.lm;
p = c.p[common];
if (p > from) {
c.p[common]--;
}
if (p > to) {
c.p[common]++;
}
} else {
// ld, ld+li, si, sd, na, oi, od, oi+od, any li on an element beneath
// the lm
//
// i.e. things care about where their item is after the move.
from = otherC.p[common];
to = otherC.lm;
p = c.p[common];
if (p === from) {
c.p[common] = to;
} else {
if (p > from) {
c.p[common]--;
}
if (p > to) {
c.p[common]++;
} else if (p === to) {
if (from > to) {
c.p[common]++;
}
}
}
}
} else if ((otherC.oi !== undefined) && (otherC.od !== undefined)) {
if (c.p[common] === otherC.p[common]) {
if ((c.oi !== undefined) && commonOperand) {
// we inserted where someone else replaced
if (type === 'right') {
// left wins
return dest;
} else {
// we win, make our op replace what they inserted
c.od = otherC.oi;
}
} else {
// -> noop if the other component is deleting the same object (or any
// parent)
return dest;
}
}
} else if (otherC.oi !== undefined) {
if ((c.oi !== undefined) && (c.p[common] === otherC.p[common])) {
// left wins if we try to insert at the same place
if (type === 'left') {
json.append(dest, {p:c.p, od:otherC.oi});
} else {
return dest;
}
}
} else if (otherC.od !== undefined) {
if (c.p[common] === otherC.p[common]) {
if (!commonOperand) { return dest; }
if (c.oi !== undefined) {
delete c.od;
} else {
return dest;
}
}
}
}
json.append(dest, c);
return dest;
};
if (typeof WEB !== 'undefined' && WEB !== null) {
if (!exports.types) { exports.types = {}; }
// This is kind of awful - come up with a better way to hook this helper code up.
exports._bt(json, json.transformComponent, json.checkValidOp, json.append);
// [] is used to prevent closure from renaming types.text
exports.types.json = json;
} else {
module.exports = json;
require('./helpers').bootstrapTransform(json, json.transformComponent, json.checkValidOp, json.append);
}