mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 10:43:33 -05:00
497 lines
13 KiB
JavaScript
497 lines
13 KiB
JavaScript
/* 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
|
|
}
|