overleaf/services/track-changes/app/js/DiffGenerator.js

341 lines
9.1 KiB
JavaScript
Raw Normal View History

/* eslint-disable
camelcase,
no-proto,
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
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
let DiffGenerator
function ConsistencyError(message) {
const error = new Error(message)
error.name = 'ConsistencyError'
error.__proto__ = ConsistencyError.prototype
return error
}
ConsistencyError.prototype.__proto__ = Error.prototype
const logger = require('@overleaf/logger')
module.exports = DiffGenerator = {
ConsistencyError,
rewindUpdate(content, update) {
for (let j = update.op.length - 1, i = j; j >= 0; j--, i = j) {
const op = update.op[i]
if (op.broken !== true) {
try {
content = DiffGenerator.rewindOp(content, op)
} catch (e) {
if (e instanceof ConsistencyError && (i = update.op.length - 1)) {
// catch known case where the last op in an array has been
// merged into a later op
logger.warn(
{ err: e, update, op: JSON.stringify(op) },
'marking op as broken'
)
op.broken = true
} else {
throw e // rethrow the execption
}
}
}
}
return content
},
rewindOp(content, op) {
let p
if (op.i != null) {
// ShareJS will accept an op where p > content.length when applied,
// and it applies as though p == content.length. However, the op is
// passed to us with the original p > content.length. Detect if that
// is the case with this op, and shift p back appropriately to match
// ShareJS if so.
;({ p } = op)
const max_p = content.length - op.i.length
if (p > max_p) {
logger.warn({ max_p, p }, 'truncating position to content length')
p = max_p
}
const textToBeRemoved = content.slice(p, p + op.i.length)
if (op.i !== textToBeRemoved) {
throw new ConsistencyError(
`Inserted content, '${op.i}', does not match text to be removed, '${textToBeRemoved}'`
)
}
return content.slice(0, p) + content.slice(p + op.i.length)
} else if (op.d != null) {
return content.slice(0, op.p) + op.d + content.slice(op.p)
} else {
return content
}
},
rewindUpdates(content, updates) {
for (const update of Array.from(updates.reverse())) {
try {
content = DiffGenerator.rewindUpdate(content, update)
} catch (e) {
e.attempted_update = update // keep a record of the attempted update
throw e // rethrow the exception
}
}
return content
},
buildDiff(initialContent, updates) {
let diff = [{ u: initialContent }]
for (const update of Array.from(updates)) {
diff = DiffGenerator.applyUpdateToDiff(diff, update)
}
diff = DiffGenerator.compressDiff(diff)
return diff
},
compressDiff(diff) {
const newDiff = []
for (const part of Array.from(diff)) {
const lastPart = newDiff[newDiff.length - 1]
if (
lastPart != null &&
(lastPart.meta != null ? lastPart.meta.user : undefined) != null &&
(part.meta != null ? part.meta.user : undefined) != null
) {
if (
lastPart.i != null &&
part.i != null &&
lastPart.meta.user.id === part.meta.user.id
) {
lastPart.i += part.i
lastPart.meta.start_ts = Math.min(
lastPart.meta.start_ts,
part.meta.start_ts
)
lastPart.meta.end_ts = Math.max(
lastPart.meta.end_ts,
part.meta.end_ts
)
} else if (
lastPart.d != null &&
part.d != null &&
lastPart.meta.user.id === part.meta.user.id
) {
lastPart.d += part.d
lastPart.meta.start_ts = Math.min(
lastPart.meta.start_ts,
part.meta.start_ts
)
lastPart.meta.end_ts = Math.max(
lastPart.meta.end_ts,
part.meta.end_ts
)
} else {
newDiff.push(part)
}
} else {
newDiff.push(part)
}
}
return newDiff
},
applyOpToDiff(diff, op, meta) {
let consumedDiff
const position = 0
let remainingDiff = diff.slice()
;({ consumedDiff, remainingDiff } = DiffGenerator._consumeToOffset(
remainingDiff,
op.p
))
const newDiff = consumedDiff
if (op.i != null) {
newDiff.push({
i: op.i,
2021-07-13 07:04:43 -04:00
meta,
})
} else if (op.d != null) {
2021-07-13 07:04:43 -04:00
;({ consumedDiff, remainingDiff } =
DiffGenerator._consumeDiffAffectedByDeleteOp(remainingDiff, op, meta))
newDiff.push(...Array.from(consumedDiff || []))
}
newDiff.push(...Array.from(remainingDiff || []))
return newDiff
},
applyUpdateToDiff(diff, update) {
for (const op of Array.from(update.op)) {
if (op.broken !== true) {
diff = DiffGenerator.applyOpToDiff(diff, op, update.meta)
}
}
return diff
},
_consumeToOffset(remainingDiff, totalOffset) {
let part
const consumedDiff = []
let position = 0
while ((part = remainingDiff.shift())) {
const length = DiffGenerator._getLengthOfDiffPart(part)
if (part.d != null) {
consumedDiff.push(part)
} else if (position + length >= totalOffset) {
const partOffset = totalOffset - position
if (partOffset > 0) {
consumedDiff.push(DiffGenerator._slicePart(part, 0, partOffset))
}
if (partOffset < length) {
remainingDiff.unshift(DiffGenerator._slicePart(part, partOffset))
}
break
} else {
position += length
consumedDiff.push(part)
}
}
return {
consumedDiff,
2021-07-13 07:04:43 -04:00
remainingDiff,
}
},
_consumeDiffAffectedByDeleteOp(remainingDiff, deleteOp, meta) {
const consumedDiff = []
let remainingOp = deleteOp
while (remainingOp && remainingDiff.length > 0) {
let newPart
2021-07-13 07:04:43 -04:00
;({ newPart, remainingDiff, remainingOp } =
DiffGenerator._consumeDeletedPart(remainingDiff, remainingOp, meta))
if (newPart != null) {
consumedDiff.push(newPart)
}
}
return {
consumedDiff,
2021-07-13 07:04:43 -04:00
remainingDiff,
}
},
_consumeDeletedPart(remainingDiff, op, meta) {
let deletedContent, newPart, remainingOp
const part = remainingDiff.shift()
const partLength = DiffGenerator._getLengthOfDiffPart(part)
if (part.d != null) {
// Skip existing deletes
remainingOp = op
newPart = part
} else if (partLength > op.d.length) {
// Only the first bit of the part has been deleted
const remainingPart = DiffGenerator._slicePart(part, op.d.length)
remainingDiff.unshift(remainingPart)
deletedContent = DiffGenerator._getContentOfPart(part).slice(
0,
op.d.length
)
if (deletedContent !== op.d) {
throw new ConsistencyError(
`deleted content, '${deletedContent}', does not match delete op, '${op.d}'`
)
}
if (part.u != null) {
newPart = {
d: op.d,
2021-07-13 07:04:43 -04:00
meta,
}
} else if (part.i != null) {
newPart = null
}
remainingOp = null
} else if (partLength === op.d.length) {
// The entire part has been deleted, but it is the last part
deletedContent = DiffGenerator._getContentOfPart(part)
if (deletedContent !== op.d) {
throw new ConsistencyError(
`deleted content, '${deletedContent}', does not match delete op, '${op.d}'`
)
}
if (part.u != null) {
newPart = {
d: op.d,
2021-07-13 07:04:43 -04:00
meta,
}
} else if (part.i != null) {
newPart = null
}
remainingOp = null
} else if (partLength < op.d.length) {
// The entire part has been deleted and there is more
deletedContent = DiffGenerator._getContentOfPart(part)
const opContent = op.d.slice(0, deletedContent.length)
if (deletedContent !== opContent) {
throw new ConsistencyError(
`deleted content, '${deletedContent}', does not match delete op, '${opContent}'`
)
}
if (part.u) {
newPart = {
d: part.u,
2021-07-13 07:04:43 -04:00
meta,
}
} else if (part.i != null) {
newPart = null
}
remainingOp = {
p: op.p,
2021-07-13 07:04:43 -04:00
d: op.d.slice(DiffGenerator._getLengthOfDiffPart(part)),
}
}
return {
newPart,
remainingDiff,
2021-07-13 07:04:43 -04:00
remainingOp,
}
},
_slicePart(basePart, from, to) {
let part
if (basePart.u != null) {
part = { u: basePart.u.slice(from, to) }
} else if (basePart.i != null) {
part = { i: basePart.i.slice(from, to) }
}
if (basePart.meta != null) {
part.meta = basePart.meta
}
return part
},
_getLengthOfDiffPart(part) {
return (part.u || part.d || part.i || '').length
},
_getContentOfPart(part) {
return part.u || part.d || part.i || ''
2021-07-13 07:04:43 -04:00
},
}