/* eslint-disable no-cond-assign, no-return-assign, no-undef, no-unused-vars, */ // 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 * DS103: Rewrite code to no longer use __guard__ * 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 */ // A TP2 implementation of text, following this spec: // http://code.google.com/p/lightwave/source/browse/trunk/experimental/ot/README // // A document is made up of a string and a set of tombstones inserted throughout // the string. For example, 'some ', (2 tombstones), 'string'. // // This is encoded in a document as: {s:'some string', t:[5, -2, 6]} // // Ops are lists of components which iterate over the whole document. // Components are either: // N: Skip N characters in the original document // {i:'str'}: Insert 'str' at the current position in the document // {i:N}: Insert N tombstones at the current position in the document // {d:N}: Delete (tombstone) N characters at the current position in the document // // Eg: [3, {i:'hi'}, 5, {d:8}] // // Snapshots are lists with characters and tombstones. Characters are stored in strings // and adjacent tombstones are flattened into numbers. // // Eg, the document: 'Hello .....world' ('.' denotes tombstoned (deleted) characters) // would be represented by a document snapshot of ['Hello ', 5, 'world'] let append, appendDoc, takeDoc; var type = { name: 'text-tp2', tp2: true, create() { return {charLength:0, totalLength:0, positionCache:[], data:[]}; }, serialize(doc) { if (!doc.data) { throw new Error('invalid doc snapshot'); } return doc.data; }, deserialize(data) { const doc = type.create(); doc.data = data; for (const component of Array.from(data)) { if (typeof component === 'string') { doc.charLength += component.length; doc.totalLength += component.length; } else { doc.totalLength += component; } } return doc; } }; const checkOp = function(op) { if (!Array.isArray(op)) { throw new Error('Op must be an array of components'); } let last = null; return (() => { const result = []; for (const c of Array.from(op)) { if (typeof(c) === 'object') { if (c.i !== undefined) { if (((typeof(c.i) !== 'string') || !(c.i.length > 0)) && ((typeof(c.i) !== 'number') || !(c.i > 0))) { throw new Error('Inserts must insert a string or a +ive number'); } } else if (c.d !== undefined) { if ((typeof(c.d) !== 'number') || !(c.d > 0)) { throw new Error('Deletes must be a +ive number'); } } else { throw new Error('Operation component must define .i or .d'); } } else { if (typeof(c) !== 'number') { throw new Error('Op components must be objects or numbers'); } if (!(c > 0)) { throw new Error('Skip components must be a positive number'); } if (typeof(last) === 'number') { throw new Error('Adjacent skip components should be combined'); } } result.push(last = c); } return result; })(); }; // Take the next part from the specified position in a document snapshot. // position = {index, offset}. It will be updated. type._takeDoc = (takeDoc = function(doc, position, maxlength, tombsIndivisible) { if (position.index >= doc.data.length) { throw new Error('Operation goes past the end of the document'); } const part = doc.data[position.index]; // peel off data[0] const result = typeof(part) === 'string' ? maxlength !== undefined ? part.slice(position.offset, (position.offset + maxlength)) : part.slice(position.offset) : (maxlength === undefined) || tombsIndivisible ? part - position.offset : Math.min(maxlength, part - position.offset); const resultLen = result.length || result; if (((part.length || part) - position.offset) > resultLen) { position.offset += resultLen; } else { position.index++; position.offset = 0; } return result; }); // Append a part to the end of a document type._appendDoc = (appendDoc = function(doc, p) { if ((p === 0) || (p === '')) { return; } if (typeof p === 'string') { doc.charLength += p.length; doc.totalLength += p.length; } else { doc.totalLength += p; } const { data } = doc; if (data.length === 0) { data.push(p); } else if (typeof(data[data.length - 1]) === typeof(p)) { data[data.length - 1] += p; } else { data.push(p); } }); // Apply the op to the document. The document is not modified in the process. type.apply = function(doc, op) { if ((doc.totalLength === undefined) || (doc.charLength === undefined) || (doc.data.length === undefined)) { throw new Error('Snapshot is invalid'); } checkOp(op); const newDoc = type.create(); const position = {index:0, offset:0}; for (const component of Array.from(op)) { var part, remainder; if (typeof(component) === 'number') { remainder = component; while (remainder > 0) { part = takeDoc(doc, position, remainder); appendDoc(newDoc, part); remainder -= part.length || part; } } else if (component.i !== undefined) { appendDoc(newDoc, component.i); } else if (component.d !== undefined) { remainder = component.d; while (remainder > 0) { part = takeDoc(doc, position, remainder); remainder -= part.length || part; } appendDoc(newDoc, component.d); } } return newDoc; }; // Append an op component to the end of the specified op. // Exported for the randomOpGenerator. type._append = (append = function(op, component) { if ((component === 0) || (component.i === '') || (component.i === 0) || (component.d === 0)) { } else if (op.length === 0) { return op.push(component); } else { const last = op[op.length - 1]; if ((typeof(component) === 'number') && (typeof(last) === 'number')) { return op[op.length - 1] += component; } else if ((component.i !== undefined) && (last.i != null) && (typeof(last.i) === typeof(component.i))) { return last.i += component.i; } else if ((component.d !== undefined) && (last.d != null)) { return last.d += component.d; } else { return op.push(component); } } }); // Makes 2 functions for taking components from the start of an op, and for peeking // at the next op that could be taken. const makeTake = function(op) { // The index of the next component to take let index = 0; // The offset into the component let offset = 0; // Take up to length maxlength from the op. If maxlength is not defined, there is no max. // If insertsIndivisible is true, inserts (& insert tombstones) won't be separated. // // Returns null when op is fully consumed. const take = function(maxlength, insertsIndivisible) { let current; if (index === op.length) { return null; } const e = op[index]; if ((typeof((current = e)) === 'number') || (typeof((current = e.i)) === 'number') || ((current = e.d) !== undefined)) { let c; if ((maxlength == null) || ((current - offset) <= maxlength) || (insertsIndivisible && (e.i !== undefined))) { // Return the rest of the current element. c = current - offset; ++index; offset = 0; } else { offset += maxlength; c = maxlength; } if (e.i !== undefined) { return {i:c}; } else if (e.d !== undefined) { return {d:c}; } else { return c; } } else { // Take from the inserted string let result; if ((maxlength == null) || ((e.i.length - offset) <= maxlength) || insertsIndivisible) { result = {i:e.i.slice(offset)}; ++index; offset = 0; } else { result = {i:e.i.slice(offset, offset + maxlength)}; offset += maxlength; } return result; } }; const peekType = () => op[index]; return [take, peekType]; }; // Find and return the length of an op component const componentLength = function(component) { if (typeof(component) === 'number') { return component; } else if (typeof(component.i) === 'string') { return component.i.length; } else { // This should work because c.d and c.i must be +ive. return component.d || component.i; } }; // Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate // adjacent inserts and deletes. type.normalize = function(op) { const newOp = []; for (const component of Array.from(op)) { append(newOp, component); } return newOp; }; // This is a helper method to transform and prune. goForwards is true for transform, false for prune. const transformer = function(op, otherOp, goForwards, side) { let component; checkOp(op); checkOp(otherOp); const newOp = []; const [take, peek] = Array.from(makeTake(op)); for (component of Array.from(otherOp)) { var chunk; let length = componentLength(component); if (component.i !== undefined) { // Insert text or tombs if (goForwards) { // transform - insert skips over inserted parts if (side === 'left') { // The left insert should go first. while (__guard__(peek(), x => x.i) !== undefined) { append(newOp, take()); } } // In any case, skip the inserted text. append(newOp, length); } else { // Prune. Remove skips for inserts. while (length > 0) { chunk = take(length, true); if (chunk === null) { throw new Error('The transformed op is invalid'); } if (chunk.d !== undefined) { throw new Error('The transformed op deletes locally inserted characters - it cannot be purged of the insert.'); } if (typeof chunk === 'number') { length -= chunk; } else { append(newOp, chunk); } } } } else { // Skip or delete while (length > 0) { chunk = take(length, true); if (chunk === null) { throw new Error('The op traverses more elements than the document has'); } append(newOp, chunk); if (!chunk.i) { length -= componentLength(chunk); } } } } // Append extras from op1 while (component = take()) { if (component.i === undefined) { throw new Error(`Remaining fragments in the op: ${component}`); } append(newOp, component); } return newOp; }; // transform op1 by op2. Return transformed version of op1. // op1 and op2 are unchanged by transform. // side should be 'left' or 'right', depending on if op1.id <> op2.id. 'left' == client op. type.transform = function(op, otherOp, side) { if ((side !== 'left') && (side !== 'right')) { throw new Error(`side (${side}) should be 'left' or 'right'`); } return transformer(op, otherOp, true, side); }; // Prune is the inverse of transform. type.prune = (op, otherOp) => transformer(op, otherOp, false); // Compose 2 ops into 1 op. type.compose = function(op1, op2) { let component; if ((op1 === null) || (op1 === undefined)) { return op2; } checkOp(op1); checkOp(op2); const result = []; const [take, _] = Array.from(makeTake(op1)); for (component of Array.from(op2)) { var chunk, length; if (typeof(component) === 'number') { // Skip // Just copy from op1. length = component; while (length > 0) { chunk = take(length); if (chunk === null) { throw new Error('The op traverses more elements than the document has'); } append(result, chunk); length -= componentLength(chunk); } } else if (component.i !== undefined) { // Insert append(result, {i:component.i}); } else { // Delete length = component.d; while (length > 0) { chunk = take(length); if (chunk === null) { throw new Error('The op traverses more elements than the document has'); } const chunkLength = componentLength(chunk); if (chunk.i !== undefined) { append(result, {i:chunkLength}); } else { append(result, {d:chunkLength}); } length -= chunkLength; } } } // Append extras from op1 while (component = take()) { if (component.i === undefined) { throw new Error(`Remaining fragments in op1: ${component}`); } append(result, component); } return result; }; if (typeof WEB !== 'undefined' && WEB !== null) { exports.types['text-tp2'] = type; } else { module.exports = type; } function __guard__(value, transform) { return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; }