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

253 lines
8.3 KiB
JavaScript
Raw Normal View History

/* eslint-disable
camelcase,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* 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
*/
// A simple text implementation
//
// Operations are lists of components.
// Each component either inserts or deletes at a specified position in the document.
//
// Components are either:
// {i:'str', p:100}: Insert 'str' at position 100 in the document
// {d:'str', p:100}: Delete 'str' at position 100 in the document
//
// Components in an operation are executed sequentially, so the position of components
// assumes previous components have already executed.
//
// Eg: This op:
// [{i:'abc', p:0}]
// is equivalent to this op:
// [{i:'a', p:0}, {i:'b', p:1}, {i:'c', p:2}]
// NOTE: The global scope here is shared with other sharejs files when built with closure.
// Be careful what ends up in your namespace.
let append, transformComponent;
const text = {};
text.name = 'text';
text.create = () => '';
const strInject = (s1, pos, s2) => s1.slice(0, pos) + s2 + s1.slice(pos);
const checkValidComponent = function(c) {
if (typeof c.p !== 'number') { throw new Error('component missing position field'); }
const i_type = typeof c.i;
const d_type = typeof c.d;
if (!((i_type === 'string') ^ (d_type === 'string'))) { throw new Error('component needs an i or d field'); }
if (!(c.p >= 0)) { throw new Error('position cannot be negative'); }
};
const checkValidOp = function(op) {
for (const c of Array.from(op)) { checkValidComponent(c); }
return true;
};
text.apply = function(snapshot, op) {
checkValidOp(op);
for (const component of Array.from(op)) {
if (component.i != null) {
snapshot = strInject(snapshot, component.p, component.i);
} else {
const deleted = snapshot.slice(component.p, (component.p + component.d.length));
if (component.d !== deleted) { throw new Error(`Delete component '${component.d}' does not match deleted text '${deleted}'`); }
snapshot = snapshot.slice(0, component.p) + snapshot.slice((component.p + component.d.length));
}
}
2014-02-12 05:40:42 -05:00
return snapshot;
};
// Exported for use by the random op generator.
//
// For simplicity, this version of append does not compress adjacent inserts and deletes of
// the same text. It would be nice to change that at some stage.
text._append = (append = function(newOp, c) {
if ((c.i === '') || (c.d === '')) { return; }
if (newOp.length === 0) {
return newOp.push(c);
} else {
const last = newOp[newOp.length - 1];
// Compose the insert into the previous insert if possible
if ((last.i != null) && (c.i != null) && (last.p <= c.p && c.p <= (last.p + last.i.length))) {
return newOp[newOp.length - 1] = {i:strInject(last.i, c.p - last.p, c.i), p:last.p};
} else if ((last.d != null) && (c.d != null) && (c.p <= last.p && last.p <= (c.p + c.d.length))) {
return newOp[newOp.length - 1] = {d:strInject(c.d, last.p - c.p, last.d), p:c.p};
} else {
return newOp.push(c);
}
}
});
text.compose = function(op1, op2) {
checkValidOp(op1);
checkValidOp(op2);
const newOp = op1.slice();
for (const c of Array.from(op2)) { append(newOp, c); }
return newOp;
};
// Attempt to compress the op components together 'as much as possible'.
// This implementation preserves order and preserves create/delete pairs.
text.compress = op => text.compose([], op);
text.normalize = function(op) {
const newOp = [];
2014-02-12 05:40:42 -05:00
// Normalize should allow ops which are a single (unwrapped) component:
// {i:'asdf', p:23}.
// There's no good way to test if something is an array:
// http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
// so this is probably the least bad solution.
if ((op.i != null) || (op.p != null)) { op = [op]; }
for (const c of Array.from(op)) {
if (c.p == null) { c.p = 0; }
append(newOp, c);
}
2014-02-12 05:40:42 -05:00
return newOp;
};
// This helper method transforms a position by an op component.
//
// If c is an insert, insertAfter specifies whether the transform
// is pushed after the insert (true) or before it (false).
//
// insertAfter is optional for deletes.
const transformPosition = function(pos, c, insertAfter) {
if (c.i != null) {
if ((c.p < pos) || ((c.p === pos) && insertAfter)) {
return pos + c.i.length;
} else {
return pos;
}
} else {
// I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length))
// but I think its harder to read that way, and it compiles using ternary operators anyway
// so its no slower written like this.
if (pos <= c.p) {
return pos;
} else if (pos <= (c.p + c.d.length)) {
return c.p;
} else {
return pos - c.d.length;
}
}
};
// Helper method to transform a cursor position as a result of an op.
//
// Like transformPosition above, if c is an insert, insertAfter specifies whether the cursor position
// is pushed after an insert (true) or before it (false).
text.transformCursor = function(position, op, side) {
const insertAfter = side === 'right';
for (const c of Array.from(op)) { position = transformPosition(position, c, insertAfter); }
return position;
};
// Transform an op component by another op component. Asymmetric.
// The result will be appended to destination.
//
// exported for use in JSON type
text._tc = (transformComponent = function(dest, c, otherC, side) {
checkValidOp([c]);
checkValidOp([otherC]);
if (c.i != null) {
append(dest, {i:c.i, p:transformPosition(c.p, otherC, side === 'right')});
} else { // Delete
if (otherC.i != null) { // delete vs insert
let s = c.d;
if (c.p < otherC.p) {
append(dest, {d:s.slice(0, otherC.p - c.p), p:c.p});
s = s.slice((otherC.p - c.p));
}
if (s !== '') {
append(dest, {d:s, p:c.p + otherC.i.length});
}
} else { // Delete vs delete
if (c.p >= (otherC.p + otherC.d.length)) {
append(dest, {d:c.d, p:c.p - otherC.d.length});
} else if ((c.p + c.d.length) <= otherC.p) {
append(dest, c);
} else {
// They overlap somewhere.
const newC = {d:'', p:c.p};
if (c.p < otherC.p) {
newC.d = c.d.slice(0, (otherC.p - c.p));
}
if ((c.p + c.d.length) > (otherC.p + otherC.d.length)) {
newC.d += c.d.slice(((otherC.p + otherC.d.length) - c.p));
}
// This is entirely optional - just for a check that the deleted
// text in the two ops matches
const intersectStart = Math.max(c.p, otherC.p);
const intersectEnd = Math.min(c.p + c.d.length, otherC.p + otherC.d.length);
const cIntersect = c.d.slice(intersectStart - c.p, intersectEnd - c.p);
const otherIntersect = otherC.d.slice(intersectStart - otherC.p, intersectEnd - otherC.p);
if (cIntersect !== otherIntersect) { throw new Error('Delete ops delete different text in the same region of the document'); }
if (newC.d !== '') {
// This could be rewritten similarly to insert v delete, above.
newC.p = transformPosition(newC.p, otherC);
append(dest, newC);
}
}
}
}
2014-02-12 05:40:42 -05:00
return dest;
});
const invertComponent = function(c) {
if (c.i != null) {
return {d:c.i, p:c.p};
} else {
return {i:c.d, p:c.p};
}
};
// No need to use append for invert, because the components won't be able to
// cancel with one another.
text.invert = op => Array.from(op.slice().reverse()).map((c) => invertComponent(c));
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.
bootstrapTransform(text, transformComponent, checkValidOp, append);
// [] is used to prevent closure from renaming types.text
exports.types.text = text;
} else {
module.exports = text;
// The text type really shouldn't need this - it should be possible to define
// an efficient transform function by making a sort of transform map and passing each
// op component through it.
require('./helpers').bootstrapTransform(text, transformComponent, checkValidOp, append);
}
2014-02-12 05:40:42 -05:00