2020-02-17 12:34:04 -05:00
|
|
|
/*
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
let DiffGenerator;
|
|
|
|
var ConsistencyError = function(message) {
|
|
|
|
const error = new Error(message);
|
|
|
|
error.name = "ConsistencyError";
|
|
|
|
error.__proto__ = ConsistencyError.prototype;
|
|
|
|
return error;
|
|
|
|
};
|
|
|
|
ConsistencyError.prototype.__proto__ = Error.prototype;
|
|
|
|
|
|
|
|
const logger = require("logger-sharelatex");
|
|
|
|
|
|
|
|
module.exports = (DiffGenerator = {
|
|
|
|
ConsistencyError,
|
|
|
|
|
|
|
|
rewindUpdate(content, update) {
|
|
|
|
for (let j = update.op.length - 1, i = j; j >= 0; j--, i = j) {
|
|
|
|
const op = update.op[i];
|
|
|
|
if (op.broken !== true) {
|
|
|
|
try {
|
|
|
|
content = DiffGenerator.rewindOp(content, op);
|
|
|
|
} catch (e) {
|
|
|
|
if (e instanceof ConsistencyError && (i = update.op.length - 1)) {
|
|
|
|
// catch known case where the last op in an array has been
|
|
|
|
// merged into a later op
|
|
|
|
logger.error({err: e, update, op: JSON.stringify(op)}, "marking op as broken");
|
|
|
|
op.broken = true;
|
|
|
|
} else {
|
|
|
|
throw e; // rethrow the execption
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return content;
|
|
|
|
},
|
|
|
|
|
|
|
|
rewindOp(content, op) {
|
|
|
|
let p;
|
|
|
|
if (op.i != null) {
|
|
|
|
// ShareJS will accept an op where p > content.length when applied,
|
|
|
|
// and it applies as though p == content.length. However, the op is
|
|
|
|
// passed to us with the original p > content.length. Detect if that
|
|
|
|
// is the case with this op, and shift p back appropriately to match
|
|
|
|
// ShareJS if so.
|
|
|
|
({ p } = op);
|
|
|
|
const max_p = content.length - op.i.length;
|
|
|
|
if (p > max_p) {
|
|
|
|
logger.warn({max_p, p}, "truncating position to content length");
|
|
|
|
p = max_p;
|
|
|
|
}
|
|
|
|
|
|
|
|
const textToBeRemoved = content.slice(p, p + op.i.length);
|
|
|
|
if (op.i !== textToBeRemoved) {
|
2014-03-01 08:31:34 -05:00
|
|
|
throw new ConsistencyError(
|
2020-02-17 12:34:04 -05:00
|
|
|
`Inserted content, '${op.i}', does not match text to be removed, '${textToBeRemoved}'`
|
|
|
|
);
|
|
|
|
}
|
2014-03-01 08:31:34 -05:00
|
|
|
|
2020-02-17 12:34:04 -05:00
|
|
|
return content.slice(0, p) + content.slice(p + op.i.length);
|
2014-03-01 08:31:34 -05:00
|
|
|
|
2020-02-17 12:34:04 -05:00
|
|
|
} else if (op.d != null) {
|
|
|
|
return content.slice(0, op.p) + op.d + content.slice(op.p);
|
2017-01-12 04:04:50 -05:00
|
|
|
|
2020-02-17 12:34:04 -05:00
|
|
|
} else {
|
|
|
|
return content;
|
2014-03-17 10:54:02 -04:00
|
|
|
}
|
2020-02-17 12:34:04 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
rewindUpdates(content, updates) {
|
|
|
|
for (let update of Array.from(updates.reverse())) {
|
|
|
|
try {
|
|
|
|
content = DiffGenerator.rewindUpdate(content, update);
|
|
|
|
} catch (e) {
|
|
|
|
e.attempted_update = update; // keep a record of the attempted update
|
|
|
|
throw e; // rethrow the exception
|
|
|
|
}
|
2014-03-01 08:31:34 -05:00
|
|
|
}
|
2020-02-17 12:34:04 -05:00
|
|
|
return content;
|
|
|
|
},
|
2014-03-01 08:31:34 -05:00
|
|
|
|
2020-02-17 12:34:04 -05:00
|
|
|
buildDiff(initialContent, updates) {
|
|
|
|
let diff = [ {u: initialContent} ];
|
|
|
|
for (let update of Array.from(updates)) {
|
|
|
|
diff = DiffGenerator.applyUpdateToDiff(diff, update);
|
|
|
|
}
|
|
|
|
diff = DiffGenerator.compressDiff(diff);
|
|
|
|
return diff;
|
|
|
|
},
|
|
|
|
|
|
|
|
compressDiff(diff) {
|
|
|
|
const newDiff = [];
|
|
|
|
for (let part of Array.from(diff)) {
|
|
|
|
const lastPart = newDiff[newDiff.length - 1];
|
|
|
|
if ((lastPart != null) && ((lastPart.meta != null ? lastPart.meta.user : undefined) != null) && ((part.meta != null ? part.meta.user : undefined) != null)) {
|
|
|
|
if ((lastPart.i != null) && (part.i != null) && (lastPart.meta.user.id === part.meta.user.id)) {
|
|
|
|
lastPart.i += part.i;
|
|
|
|
lastPart.meta.start_ts = Math.min(lastPart.meta.start_ts, part.meta.start_ts);
|
|
|
|
lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts);
|
|
|
|
} else if ((lastPart.d != null) && (part.d != null) && (lastPart.meta.user.id === part.meta.user.id)) {
|
|
|
|
lastPart.d += part.d;
|
|
|
|
lastPart.meta.start_ts = Math.min(lastPart.meta.start_ts, part.meta.start_ts);
|
|
|
|
lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts);
|
|
|
|
} else {
|
|
|
|
newDiff.push(part);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
newDiff.push(part);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return newDiff;
|
|
|
|
},
|
|
|
|
|
|
|
|
applyOpToDiff(diff, op, meta) {
|
|
|
|
let consumedDiff;
|
|
|
|
const position = 0;
|
|
|
|
|
|
|
|
let remainingDiff = diff.slice();
|
|
|
|
({consumedDiff, remainingDiff} = DiffGenerator._consumeToOffset(remainingDiff, op.p));
|
|
|
|
const newDiff = consumedDiff;
|
|
|
|
|
|
|
|
if (op.i != null) {
|
|
|
|
newDiff.push({
|
|
|
|
i: op.i,
|
|
|
|
meta
|
|
|
|
});
|
|
|
|
} else if (op.d != null) {
|
|
|
|
({consumedDiff, remainingDiff} = DiffGenerator._consumeDiffAffectedByDeleteOp(remainingDiff, op, meta));
|
|
|
|
newDiff.push(...Array.from(consumedDiff || []));
|
|
|
|
}
|
2014-03-01 08:31:34 -05:00
|
|
|
|
2020-02-17 12:34:04 -05:00
|
|
|
newDiff.push(...Array.from(remainingDiff || []));
|
2014-03-01 08:31:34 -05:00
|
|
|
|
2020-02-17 12:34:04 -05:00
|
|
|
return newDiff;
|
|
|
|
},
|
2014-03-01 08:31:34 -05:00
|
|
|
|
2020-02-17 12:34:04 -05:00
|
|
|
applyUpdateToDiff(diff, update) {
|
|
|
|
for (let op of Array.from(update.op)) {
|
|
|
|
if (op.broken !== true) {
|
|
|
|
diff = DiffGenerator.applyOpToDiff(diff, op, update.meta);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return diff;
|
|
|
|
},
|
|
|
|
|
|
|
|
_consumeToOffset(remainingDiff, totalOffset) {
|
|
|
|
let part;
|
|
|
|
const consumedDiff = [];
|
|
|
|
let position = 0;
|
|
|
|
while ((part = remainingDiff.shift())) {
|
|
|
|
const length = DiffGenerator._getLengthOfDiffPart(part);
|
|
|
|
if (part.d != null) {
|
|
|
|
consumedDiff.push(part);
|
|
|
|
} else if ((position + length) >= totalOffset) {
|
|
|
|
const partOffset = totalOffset - position;
|
|
|
|
if (partOffset > 0) {
|
|
|
|
consumedDiff.push(DiffGenerator._slicePart(part, 0, partOffset));
|
|
|
|
}
|
|
|
|
if (partOffset < length) {
|
|
|
|
remainingDiff.unshift(DiffGenerator._slicePart(part, partOffset));
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
position += length;
|
|
|
|
consumedDiff.push(part);
|
|
|
|
}
|
|
|
|
}
|
2014-03-01 08:31:34 -05:00
|
|
|
|
2020-02-17 12:34:04 -05:00
|
|
|
return {
|
|
|
|
consumedDiff,
|
|
|
|
remainingDiff
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
_consumeDiffAffectedByDeleteOp(remainingDiff, deleteOp, meta) {
|
|
|
|
const consumedDiff = [];
|
|
|
|
let remainingOp = deleteOp;
|
|
|
|
while (remainingOp && (remainingDiff.length > 0)) {
|
|
|
|
let newPart;
|
|
|
|
({newPart, remainingDiff, remainingOp} = DiffGenerator._consumeDeletedPart(remainingDiff, remainingOp, meta));
|
|
|
|
if (newPart != null) { consumedDiff.push(newPart); }
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
consumedDiff,
|
|
|
|
remainingDiff
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
_consumeDeletedPart(remainingDiff, op, meta) {
|
|
|
|
let deletedContent, newPart, remainingOp;
|
|
|
|
const part = remainingDiff.shift();
|
|
|
|
const partLength = DiffGenerator._getLengthOfDiffPart(part);
|
|
|
|
|
|
|
|
if (part.d != null) {
|
|
|
|
// Skip existing deletes
|
|
|
|
remainingOp = op;
|
|
|
|
newPart = part;
|
|
|
|
|
|
|
|
} else if (partLength > op.d.length) {
|
|
|
|
// Only the first bit of the part has been deleted
|
|
|
|
const remainingPart = DiffGenerator._slicePart(part, op.d.length);
|
|
|
|
remainingDiff.unshift(remainingPart);
|
|
|
|
|
|
|
|
deletedContent = DiffGenerator._getContentOfPart(part).slice(0, op.d.length);
|
|
|
|
if (deletedContent !== op.d) {
|
|
|
|
throw new ConsistencyError(`deleted content, '${deletedContent}', does not match delete op, '${op.d}'`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (part.u != null) {
|
|
|
|
newPart = {
|
|
|
|
d: op.d,
|
|
|
|
meta
|
|
|
|
};
|
|
|
|
} else if (part.i != null) {
|
|
|
|
newPart = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
remainingOp = null;
|
|
|
|
|
|
|
|
} else if (partLength === op.d.length) {
|
|
|
|
// The entire part has been deleted, but it is the last part
|
|
|
|
|
|
|
|
deletedContent = DiffGenerator._getContentOfPart(part);
|
|
|
|
if (deletedContent !== op.d) {
|
|
|
|
throw new ConsistencyError(`deleted content, '${deletedContent}', does not match delete op, '${op.d}'`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (part.u != null) {
|
|
|
|
newPart = {
|
|
|
|
d: op.d,
|
|
|
|
meta
|
|
|
|
};
|
|
|
|
} else if (part.i != null) {
|
|
|
|
newPart = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
remainingOp = null;
|
|
|
|
|
|
|
|
} else if (partLength < op.d.length) {
|
|
|
|
// The entire part has been deleted and there is more
|
|
|
|
|
|
|
|
deletedContent = DiffGenerator._getContentOfPart(part);
|
|
|
|
const opContent = op.d.slice(0, deletedContent.length);
|
|
|
|
if (deletedContent !== opContent) {
|
|
|
|
throw new ConsistencyError(`deleted content, '${deletedContent}', does not match delete op, '${opContent}'`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (part.u) {
|
|
|
|
newPart = {
|
|
|
|
d: part.u,
|
|
|
|
meta
|
|
|
|
};
|
|
|
|
} else if (part.i != null) {
|
|
|
|
newPart = null;
|
|
|
|
}
|
2014-03-01 08:31:34 -05:00
|
|
|
|
2014-03-11 11:24:38 -04:00
|
|
|
remainingOp =
|
2020-02-17 12:34:04 -05:00
|
|
|
{p: op.p, d: op.d.slice(DiffGenerator._getLengthOfDiffPart(part))};
|
|
|
|
}
|
2014-03-01 08:31:34 -05:00
|
|
|
|
|
|
|
return {
|
2020-02-17 12:34:04 -05:00
|
|
|
newPart,
|
|
|
|
remainingDiff,
|
|
|
|
remainingOp
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
_slicePart(basePart, from, to) {
|
|
|
|
let part;
|
|
|
|
if (basePart.u != null) {
|
|
|
|
part = { u: basePart.u.slice(from, to) };
|
|
|
|
} else if (basePart.i != null) {
|
|
|
|
part = { i: basePart.i.slice(from, to) };
|
2014-03-01 08:31:34 -05:00
|
|
|
}
|
2020-02-17 12:34:04 -05:00
|
|
|
if (basePart.meta != null) {
|
|
|
|
part.meta = basePart.meta;
|
|
|
|
}
|
|
|
|
return part;
|
|
|
|
},
|
2014-03-01 08:31:34 -05:00
|
|
|
|
2020-02-17 12:34:04 -05:00
|
|
|
_getLengthOfDiffPart(part) {
|
|
|
|
return (part.u || part.d || part.i || '').length;
|
|
|
|
},
|
2014-03-01 08:31:34 -05:00
|
|
|
|
2020-02-17 12:34:04 -05:00
|
|
|
_getContentOfPart(part) {
|
|
|
|
return part.u || part.d || part.i || '';
|
|
|
|
}
|
|
|
|
});
|