var diff = require('fast-diff'); var is = require('./is'); var op = require('./op'); var NULL_CHARACTER = String.fromCharCode(0); // Placeholder char for embed in diff() var Delta = function (ops) { // Assume we are given a well formed ops if (is.array(ops)) { this.ops = ops; } else if (is.object(ops) && is.array(ops.ops)) { this.ops = ops.ops; } else { this.ops = []; } }; Delta.prototype.insert = function (text, attributes) { var newOp = {}; if (is.string(text)) { if (text.length === 0) return this; newOp.insert = text; } else if (is.number(text)) { newOp.insert = text; } if (is.object(attributes) && Object.keys(attributes).length > 0) newOp.attributes = attributes; return this.push(newOp); }; Delta.prototype['delete'] = function (length) { if (length <= 0) return this; return this.push({ 'delete': length }); }; Delta.prototype.retain = function (length, attributes) { if (length <= 0) return this; var newOp = { retain: length }; if (is.object(attributes) && Object.keys(attributes).length > 0) newOp.attributes = attributes; return this.push(newOp); }; Delta.prototype.push = function (newOp) { var index = this.ops.length; var lastOp = this.ops[index - 1]; newOp = op.clone(newOp); if (is.object(lastOp)) { if (is.number(newOp['delete']) && is.number(lastOp['delete'])) { this.ops[index - 1] = { 'delete': lastOp['delete'] + newOp['delete'] }; return this; } // Since it does not matter if we insert before or after deleting at the same index, // always prefer to insert first if (is.number(lastOp['delete']) && (is.string(newOp.insert) || is.number(newOp.insert))) { index -= 1; lastOp = this.ops[index - 1]; if (!is.object(lastOp)) { this.ops.unshift(newOp); return this; } } if (is.equal(newOp.attributes, lastOp.attributes)) { if (is.string(newOp.insert) && is.string(lastOp.insert)) { this.ops[index - 1] = { insert: lastOp.insert + newOp.insert }; if (is.object(newOp.attributes)) this.ops[index - 1].attributes = newOp.attributes return this; } else if (is.number(newOp.retain) && is.number(lastOp.retain)) { this.ops[index - 1] = { retain: lastOp.retain + newOp.retain }; if (is.object(newOp.attributes)) this.ops[index - 1].attributes = newOp.attributes return this; } } } this.ops.splice(index, 0, newOp); return this; }; Delta.prototype.chop = function () { var lastOp = this.ops[this.ops.length - 1]; if (lastOp && lastOp.retain && !lastOp.attributes) { this.ops.pop(); } return this; }; Delta.prototype.length = function () { return this.ops.reduce(function (length, elem) { return length + op.length(elem); }, 0); }; Delta.prototype.slice = function (start, end) { start = start || 0; if (!is.number(end)) end = Infinity; var delta = new Delta(); var iter = op.iterator(this.ops); var index = 0; while (index < end && iter.hasNext()) { var nextOp; if (index < start) { nextOp = iter.next(start - index); } else { nextOp = iter.next(end - index); delta.push(nextOp); } index += op.length(nextOp); } return delta; }; Delta.prototype.compose = function (other) { var thisIter = op.iterator(this.ops); var otherIter = op.iterator(other.ops); this.ops = []; while (thisIter.hasNext() || otherIter.hasNext()) { if (otherIter.peekType() === 'insert') { this.push(otherIter.next()); } else if (thisIter.peekType() === 'delete') { this.push(thisIter.next()); } else { var length = Math.min(thisIter.peekLength(), otherIter.peekLength()); var thisOp = thisIter.next(length); var otherOp = otherIter.next(length); if (is.number(otherOp.retain)) { var newOp = {}; if (is.number(thisOp.retain)) { newOp.retain = length; } else { newOp.insert = thisOp.insert; } // Preserve null when composing with a retain, otherwise remove it for inserts var attributes = op.attributes.compose(thisOp.attributes, otherOp.attributes, is.number(thisOp.retain)); if (attributes) newOp.attributes = attributes; this.push(newOp); // Other op should be delete, we could be an insert or retain // Insert + delete cancels out } else if (is.number(otherOp['delete']) && is.number(thisOp.retain)) { this.push(otherOp); } } } return this.chop(); }; Delta.prototype.diff = function (other) { var strings = [this.ops, other.ops].map(function (ops) { return ops.map(function (op) { if (is.string(op.insert)) return op.insert; if (is.number(op.insert)) return NULL_CHARACTER; var prep = ops === other.ops ? 'on' : 'with'; throw new Error('diff() called ' + prep + ' non-document'); }).join(''); }); var diffResult = diff(strings[0], strings[1]); var thisIter = op.iterator(this.ops); var otherIter = op.iterator(other.ops); var delta = new Delta(); diffResult.forEach(function (component) { var length = component[1].length; while (length > 0) { var opLength = 0; switch (component[0]) { case diff.INSERT: opLength = Math.min(otherIter.peekLength(), length); delta.push(otherIter.next(opLength)); break; case diff.DELETE: opLength = Math.min(length, thisIter.peekLength()); thisIter.next(opLength); delta['delete'](opLength); break; case diff.EQUAL: opLength = Math.min(thisIter.peekLength(), otherIter.peekLength(), length); var thisOp = thisIter.next(opLength); var otherOp = otherIter.next(opLength); if (thisOp.insert === otherOp.insert) { delta.retain(opLength, op.attributes.diff(thisOp.attributes, otherOp.attributes)); } else { delta.push(otherOp)['delete'](opLength); } break; } length -= opLength; } }); return delta.chop(); }; Delta.prototype.transform = function (other, priority) { priority = !!priority; if (is.number(other)) { return this.transformPosition(other, priority); } var thisIter = op.iterator(this.ops); var otherIter = op.iterator(other.ops); var delta = new Delta(); while (thisIter.hasNext() || otherIter.hasNext()) { if (thisIter.peekType() === 'insert' && (priority || otherIter.peekType() !== 'insert')) { delta.retain(op.length(thisIter.next())); } else if (otherIter.peekType() === 'insert') { delta.push(otherIter.next()); } else { var length = Math.min(thisIter.peekLength(), otherIter.peekLength()); var thisOp = thisIter.next(length); var otherOp = otherIter.next(length); if (thisOp['delete']) { // Our delete either makes their delete redundant or removes their retain continue; } else if (otherOp['delete']) { delta.push(otherOp); } else { // We retain either their retain or insert delta.retain(length, op.attributes.transform(thisOp.attributes, otherOp.attributes, priority)); } } } return delta.chop(); }; Delta.prototype.transformPosition = function (index, priority) { priority = !!priority; var thisIter = op.iterator(this.ops); var offset = 0; while (thisIter.hasNext() && offset <= index) { var length = thisIter.peekLength(); var nextType = thisIter.peekType(); thisIter.next(); if (nextType === 'delete') { index -= Math.min(length, index - offset); continue; } else if (nextType === 'insert' && (offset < index || !priority)) { index += length; } offset += length; } return index; }; module.exports = Delta;