244 lines
7.6 KiB
JavaScript
244 lines
7.6 KiB
JavaScript
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;
|