You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
478 lines
18 KiB
478 lines
18 KiB
"use strict"; |
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
exports.AttributeMap = exports.OpIterator = exports.Op = void 0; |
|
const diff = require("fast-diff"); |
|
const cloneDeep = require("lodash.clonedeep"); |
|
const isEqual = require("lodash.isequal"); |
|
const AttributeMap_1 = require("./AttributeMap"); |
|
exports.AttributeMap = AttributeMap_1.default; |
|
const Op_1 = require("./Op"); |
|
exports.Op = Op_1.default; |
|
const OpIterator_1 = require("./OpIterator"); |
|
exports.OpIterator = OpIterator_1.default; |
|
const NULL_CHARACTER = String.fromCharCode(0); // Placeholder char for embed in diff() |
|
const getEmbedTypeAndData = (a, b) => { |
|
if (typeof a !== 'object' || a === null) { |
|
throw new Error(`cannot retain a ${typeof a}`); |
|
} |
|
if (typeof b !== 'object' || b === null) { |
|
throw new Error(`cannot retain a ${typeof b}`); |
|
} |
|
const embedType = Object.keys(a)[0]; |
|
if (!embedType || embedType !== Object.keys(b)[0]) { |
|
throw new Error(`embed types not matched: ${embedType} != ${Object.keys(b)[0]}`); |
|
} |
|
return [embedType, a[embedType], b[embedType]]; |
|
}; |
|
class Delta { |
|
constructor(ops) { |
|
// Assume we are given a well formed ops |
|
if (Array.isArray(ops)) { |
|
this.ops = ops; |
|
} |
|
else if (ops != null && Array.isArray(ops.ops)) { |
|
this.ops = ops.ops; |
|
} |
|
else { |
|
this.ops = []; |
|
} |
|
} |
|
static registerEmbed(embedType, handler) { |
|
this.handlers[embedType] = handler; |
|
} |
|
static unregisterEmbed(embedType) { |
|
delete this.handlers[embedType]; |
|
} |
|
static getHandler(embedType) { |
|
const handler = this.handlers[embedType]; |
|
if (!handler) { |
|
throw new Error(`no handlers for embed type "${embedType}"`); |
|
} |
|
return handler; |
|
} |
|
insert(arg, attributes) { |
|
const newOp = {}; |
|
if (typeof arg === 'string' && arg.length === 0) { |
|
return this; |
|
} |
|
newOp.insert = arg; |
|
if (attributes != null && |
|
typeof attributes === 'object' && |
|
Object.keys(attributes).length > 0) { |
|
newOp.attributes = attributes; |
|
} |
|
return this.push(newOp); |
|
} |
|
delete(length) { |
|
if (length <= 0) { |
|
return this; |
|
} |
|
return this.push({ delete: length }); |
|
} |
|
retain(length, attributes) { |
|
if (typeof length === 'number' && length <= 0) { |
|
return this; |
|
} |
|
const newOp = { retain: length }; |
|
if (attributes != null && |
|
typeof attributes === 'object' && |
|
Object.keys(attributes).length > 0) { |
|
newOp.attributes = attributes; |
|
} |
|
return this.push(newOp); |
|
} |
|
push(newOp) { |
|
let index = this.ops.length; |
|
let lastOp = this.ops[index - 1]; |
|
newOp = cloneDeep(newOp); |
|
if (typeof lastOp === 'object') { |
|
if (typeof newOp.delete === 'number' && |
|
typeof lastOp.delete === 'number') { |
|
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 (typeof lastOp.delete === 'number' && newOp.insert != null) { |
|
index -= 1; |
|
lastOp = this.ops[index - 1]; |
|
if (typeof lastOp !== 'object') { |
|
this.ops.unshift(newOp); |
|
return this; |
|
} |
|
} |
|
if (isEqual(newOp.attributes, lastOp.attributes)) { |
|
if (typeof newOp.insert === 'string' && |
|
typeof lastOp.insert === 'string') { |
|
this.ops[index - 1] = { insert: lastOp.insert + newOp.insert }; |
|
if (typeof newOp.attributes === 'object') { |
|
this.ops[index - 1].attributes = newOp.attributes; |
|
} |
|
return this; |
|
} |
|
else if (typeof newOp.retain === 'number' && |
|
typeof lastOp.retain === 'number') { |
|
this.ops[index - 1] = { retain: lastOp.retain + newOp.retain }; |
|
if (typeof newOp.attributes === 'object') { |
|
this.ops[index - 1].attributes = newOp.attributes; |
|
} |
|
return this; |
|
} |
|
} |
|
} |
|
if (index === this.ops.length) { |
|
this.ops.push(newOp); |
|
} |
|
else { |
|
this.ops.splice(index, 0, newOp); |
|
} |
|
return this; |
|
} |
|
chop() { |
|
const lastOp = this.ops[this.ops.length - 1]; |
|
if (lastOp && typeof lastOp.retain === 'number' && !lastOp.attributes) { |
|
this.ops.pop(); |
|
} |
|
return this; |
|
} |
|
filter(predicate) { |
|
return this.ops.filter(predicate); |
|
} |
|
forEach(predicate) { |
|
this.ops.forEach(predicate); |
|
} |
|
map(predicate) { |
|
return this.ops.map(predicate); |
|
} |
|
partition(predicate) { |
|
const passed = []; |
|
const failed = []; |
|
this.forEach((op) => { |
|
const target = predicate(op) ? passed : failed; |
|
target.push(op); |
|
}); |
|
return [passed, failed]; |
|
} |
|
reduce(predicate, initialValue) { |
|
return this.ops.reduce(predicate, initialValue); |
|
} |
|
changeLength() { |
|
return this.reduce((length, elem) => { |
|
if (elem.insert) { |
|
return length + Op_1.default.length(elem); |
|
} |
|
else if (elem.delete) { |
|
return length - elem.delete; |
|
} |
|
return length; |
|
}, 0); |
|
} |
|
length() { |
|
return this.reduce((length, elem) => { |
|
return length + Op_1.default.length(elem); |
|
}, 0); |
|
} |
|
slice(start = 0, end = Infinity) { |
|
const ops = []; |
|
const iter = new OpIterator_1.default(this.ops); |
|
let index = 0; |
|
while (index < end && iter.hasNext()) { |
|
let nextOp; |
|
if (index < start) { |
|
nextOp = iter.next(start - index); |
|
} |
|
else { |
|
nextOp = iter.next(end - index); |
|
ops.push(nextOp); |
|
} |
|
index += Op_1.default.length(nextOp); |
|
} |
|
return new Delta(ops); |
|
} |
|
compose(other) { |
|
const thisIter = new OpIterator_1.default(this.ops); |
|
const otherIter = new OpIterator_1.default(other.ops); |
|
const ops = []; |
|
const firstOther = otherIter.peek(); |
|
if (firstOther != null && |
|
typeof firstOther.retain === 'number' && |
|
firstOther.attributes == null) { |
|
let firstLeft = firstOther.retain; |
|
while (thisIter.peekType() === 'insert' && |
|
thisIter.peekLength() <= firstLeft) { |
|
firstLeft -= thisIter.peekLength(); |
|
ops.push(thisIter.next()); |
|
} |
|
if (firstOther.retain - firstLeft > 0) { |
|
otherIter.next(firstOther.retain - firstLeft); |
|
} |
|
} |
|
const delta = new Delta(ops); |
|
while (thisIter.hasNext() || otherIter.hasNext()) { |
|
if (otherIter.peekType() === 'insert') { |
|
delta.push(otherIter.next()); |
|
} |
|
else if (thisIter.peekType() === 'delete') { |
|
delta.push(thisIter.next()); |
|
} |
|
else { |
|
const length = Math.min(thisIter.peekLength(), otherIter.peekLength()); |
|
const thisOp = thisIter.next(length); |
|
const otherOp = otherIter.next(length); |
|
if (otherOp.retain) { |
|
const newOp = {}; |
|
if (typeof thisOp.retain === 'number') { |
|
newOp.retain = |
|
typeof otherOp.retain === 'number' ? length : otherOp.retain; |
|
} |
|
else { |
|
if (typeof otherOp.retain === 'number') { |
|
if (thisOp.retain == null) { |
|
newOp.insert = thisOp.insert; |
|
} |
|
else { |
|
newOp.retain = thisOp.retain; |
|
} |
|
} |
|
else { |
|
const action = thisOp.retain == null ? 'insert' : 'retain'; |
|
const [embedType, thisData, otherData] = getEmbedTypeAndData(thisOp[action], otherOp.retain); |
|
const handler = Delta.getHandler(embedType); |
|
newOp[action] = { |
|
[embedType]: handler.compose(thisData, otherData, action === 'retain'), |
|
}; |
|
} |
|
} |
|
// Preserve null when composing with a retain, otherwise remove it for inserts |
|
const attributes = AttributeMap_1.default.compose(thisOp.attributes, otherOp.attributes, typeof thisOp.retain === 'number'); |
|
if (attributes) { |
|
newOp.attributes = attributes; |
|
} |
|
delta.push(newOp); |
|
// Optimization if rest of other is just retain |
|
if (!otherIter.hasNext() && |
|
isEqual(delta.ops[delta.ops.length - 1], newOp)) { |
|
const rest = new Delta(thisIter.rest()); |
|
return delta.concat(rest).chop(); |
|
} |
|
// Other op should be delete, we could be an insert or retain |
|
// Insert + delete cancels out |
|
} |
|
else if (typeof otherOp.delete === 'number' && |
|
(typeof thisOp.retain === 'number' || |
|
(typeof thisOp.retain === 'object' && thisOp.retain !== null))) { |
|
delta.push(otherOp); |
|
} |
|
} |
|
} |
|
return delta.chop(); |
|
} |
|
concat(other) { |
|
const delta = new Delta(this.ops.slice()); |
|
if (other.ops.length > 0) { |
|
delta.push(other.ops[0]); |
|
delta.ops = delta.ops.concat(other.ops.slice(1)); |
|
} |
|
return delta; |
|
} |
|
diff(other, cursor) { |
|
if (this.ops === other.ops) { |
|
return new Delta(); |
|
} |
|
const strings = [this, other].map((delta) => { |
|
return delta |
|
.map((op) => { |
|
if (op.insert != null) { |
|
return typeof op.insert === 'string' ? op.insert : NULL_CHARACTER; |
|
} |
|
const prep = delta === other ? 'on' : 'with'; |
|
throw new Error('diff() called ' + prep + ' non-document'); |
|
}) |
|
.join(''); |
|
}); |
|
const retDelta = new Delta(); |
|
const diffResult = diff(strings[0], strings[1], cursor, true); |
|
const thisIter = new OpIterator_1.default(this.ops); |
|
const otherIter = new OpIterator_1.default(other.ops); |
|
diffResult.forEach((component) => { |
|
let length = component[1].length; |
|
while (length > 0) { |
|
let opLength = 0; |
|
switch (component[0]) { |
|
case diff.INSERT: |
|
opLength = Math.min(otherIter.peekLength(), length); |
|
retDelta.push(otherIter.next(opLength)); |
|
break; |
|
case diff.DELETE: |
|
opLength = Math.min(length, thisIter.peekLength()); |
|
thisIter.next(opLength); |
|
retDelta.delete(opLength); |
|
break; |
|
case diff.EQUAL: |
|
opLength = Math.min(thisIter.peekLength(), otherIter.peekLength(), length); |
|
const thisOp = thisIter.next(opLength); |
|
const otherOp = otherIter.next(opLength); |
|
if (isEqual(thisOp.insert, otherOp.insert)) { |
|
retDelta.retain(opLength, AttributeMap_1.default.diff(thisOp.attributes, otherOp.attributes)); |
|
} |
|
else { |
|
retDelta.push(otherOp).delete(opLength); |
|
} |
|
break; |
|
} |
|
length -= opLength; |
|
} |
|
}); |
|
return retDelta.chop(); |
|
} |
|
eachLine(predicate, newline = '\n') { |
|
const iter = new OpIterator_1.default(this.ops); |
|
let line = new Delta(); |
|
let i = 0; |
|
while (iter.hasNext()) { |
|
if (iter.peekType() !== 'insert') { |
|
return; |
|
} |
|
const thisOp = iter.peek(); |
|
const start = Op_1.default.length(thisOp) - iter.peekLength(); |
|
const index = typeof thisOp.insert === 'string' |
|
? thisOp.insert.indexOf(newline, start) - start |
|
: -1; |
|
if (index < 0) { |
|
line.push(iter.next()); |
|
} |
|
else if (index > 0) { |
|
line.push(iter.next(index)); |
|
} |
|
else { |
|
if (predicate(line, iter.next(1).attributes || {}, i) === false) { |
|
return; |
|
} |
|
i += 1; |
|
line = new Delta(); |
|
} |
|
} |
|
if (line.length() > 0) { |
|
predicate(line, {}, i); |
|
} |
|
} |
|
invert(base) { |
|
const inverted = new Delta(); |
|
this.reduce((baseIndex, op) => { |
|
if (op.insert) { |
|
inverted.delete(Op_1.default.length(op)); |
|
} |
|
else if (typeof op.retain === 'number' && op.attributes == null) { |
|
inverted.retain(op.retain); |
|
return baseIndex + op.retain; |
|
} |
|
else if (op.delete || typeof op.retain === 'number') { |
|
const length = (op.delete || op.retain); |
|
const slice = base.slice(baseIndex, baseIndex + length); |
|
slice.forEach((baseOp) => { |
|
if (op.delete) { |
|
inverted.push(baseOp); |
|
} |
|
else if (op.retain && op.attributes) { |
|
inverted.retain(Op_1.default.length(baseOp), AttributeMap_1.default.invert(op.attributes, baseOp.attributes)); |
|
} |
|
}); |
|
return baseIndex + length; |
|
} |
|
else if (typeof op.retain === 'object' && op.retain !== null) { |
|
const slice = base.slice(baseIndex, baseIndex + 1); |
|
const baseOp = new OpIterator_1.default(slice.ops).next(); |
|
const [embedType, opData, baseOpData] = getEmbedTypeAndData(op.retain, baseOp.insert); |
|
const handler = Delta.getHandler(embedType); |
|
inverted.retain({ [embedType]: handler.invert(opData, baseOpData) }, AttributeMap_1.default.invert(op.attributes, baseOp.attributes)); |
|
return baseIndex + 1; |
|
} |
|
return baseIndex; |
|
}, 0); |
|
return inverted.chop(); |
|
} |
|
transform(arg, priority = false) { |
|
priority = !!priority; |
|
if (typeof arg === 'number') { |
|
return this.transformPosition(arg, priority); |
|
} |
|
const other = arg; |
|
const thisIter = new OpIterator_1.default(this.ops); |
|
const otherIter = new OpIterator_1.default(other.ops); |
|
const delta = new Delta(); |
|
while (thisIter.hasNext() || otherIter.hasNext()) { |
|
if (thisIter.peekType() === 'insert' && |
|
(priority || otherIter.peekType() !== 'insert')) { |
|
delta.retain(Op_1.default.length(thisIter.next())); |
|
} |
|
else if (otherIter.peekType() === 'insert') { |
|
delta.push(otherIter.next()); |
|
} |
|
else { |
|
const length = Math.min(thisIter.peekLength(), otherIter.peekLength()); |
|
const thisOp = thisIter.next(length); |
|
const 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 { |
|
const thisData = thisOp.retain; |
|
const otherData = otherOp.retain; |
|
let transformedData = typeof otherData === 'object' && otherData !== null |
|
? otherData |
|
: length; |
|
if (typeof thisData === 'object' && |
|
thisData !== null && |
|
typeof otherData === 'object' && |
|
otherData !== null) { |
|
const embedType = Object.keys(thisData)[0]; |
|
if (embedType === Object.keys(otherData)[0]) { |
|
const handler = Delta.getHandler(embedType); |
|
if (handler) { |
|
transformedData = { |
|
[embedType]: handler.transform(thisData[embedType], otherData[embedType], priority), |
|
}; |
|
} |
|
} |
|
} |
|
// We retain either their retain or insert |
|
delta.retain(transformedData, AttributeMap_1.default.transform(thisOp.attributes, otherOp.attributes, priority)); |
|
} |
|
} |
|
} |
|
return delta.chop(); |
|
} |
|
transformPosition(index, priority = false) { |
|
priority = !!priority; |
|
const thisIter = new OpIterator_1.default(this.ops); |
|
let offset = 0; |
|
while (thisIter.hasNext() && offset <= index) { |
|
const length = thisIter.peekLength(); |
|
const 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; |
|
} |
|
} |
|
Delta.Op = Op_1.default; |
|
Delta.OpIterator = OpIterator_1.default; |
|
Delta.AttributeMap = AttributeMap_1.default; |
|
Delta.handlers = {}; |
|
exports.default = Delta; |
|
if (typeof module === 'object') { |
|
module.exports = Delta; |
|
module.exports.default = Delta; |
|
} |
|
//# sourceMappingURL=Delta.js.map
|