2020-05-06 06:09:15 -04:00
|
|
|
/* eslint-disable
|
|
|
|
camelcase,
|
|
|
|
no-return-assign,
|
|
|
|
no-undef,
|
|
|
|
*/
|
|
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
|
|
// Fix any style issues and re-enable lint.
|
2020-05-06 06:08:21 -04:00
|
|
|
/*
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
// A simple text implementation
|
|
|
|
//
|
|
|
|
// Operations are lists of components.
|
|
|
|
// Each component either inserts or deletes at a specified position in the document.
|
|
|
|
//
|
|
|
|
// Components are either:
|
|
|
|
// {i:'str', p:100}: Insert 'str' at position 100 in the document
|
|
|
|
// {d:'str', p:100}: Delete 'str' at position 100 in the document
|
|
|
|
//
|
|
|
|
// Components in an operation are executed sequentially, so the position of components
|
|
|
|
// assumes previous components have already executed.
|
|
|
|
//
|
|
|
|
// Eg: This op:
|
|
|
|
// [{i:'abc', p:0}]
|
|
|
|
// is equivalent to this op:
|
|
|
|
// [{i:'a', p:0}, {i:'b', p:1}, {i:'c', p:2}]
|
|
|
|
|
|
|
|
// NOTE: The global scope here is shared with other sharejs files when built with closure.
|
|
|
|
// Be careful what ends up in your namespace.
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
let append, transformComponent
|
|
|
|
const text = {}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
text.name = 'text'
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
text.create = () => ''
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const strInject = (s1, pos, s2) => s1.slice(0, pos) + s2 + s1.slice(pos)
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const checkValidComponent = function (c) {
|
|
|
|
if (typeof c.p !== 'number') {
|
|
|
|
throw new Error('component missing position field')
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const i_type = typeof c.i
|
|
|
|
const d_type = typeof c.d
|
|
|
|
if (!((i_type === 'string') ^ (d_type === 'string'))) {
|
|
|
|
throw new Error('component needs an i or d field')
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
if (!(c.p >= 0)) {
|
|
|
|
throw new Error('position cannot be negative')
|
|
|
|
}
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const checkValidOp = function (op) {
|
|
|
|
for (const c of Array.from(op)) {
|
|
|
|
checkValidComponent(c)
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
text.apply = function (snapshot, op) {
|
|
|
|
checkValidOp(op)
|
2020-05-06 06:09:15 -04:00
|
|
|
for (const component of Array.from(op)) {
|
2020-05-06 06:08:21 -04:00
|
|
|
if (component.i != null) {
|
2020-05-06 06:09:33 -04:00
|
|
|
snapshot = strInject(snapshot, component.p, component.i)
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
const deleted = snapshot.slice(
|
|
|
|
component.p,
|
|
|
|
component.p + component.d.length
|
|
|
|
)
|
|
|
|
if (component.d !== deleted) {
|
|
|
|
throw new Error(
|
|
|
|
`Delete component '${component.d}' does not match deleted text '${deleted}'`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
snapshot =
|
|
|
|
snapshot.slice(0, component.p) +
|
|
|
|
snapshot.slice(component.p + component.d.length)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
return snapshot
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Exported for use by the random op generator.
|
|
|
|
//
|
|
|
|
// For simplicity, this version of append does not compress adjacent inserts and deletes of
|
|
|
|
// the same text. It would be nice to change that at some stage.
|
2020-05-06 06:09:33 -04:00
|
|
|
text._append = append = function (newOp, c) {
|
|
|
|
if (c.i === '' || c.d === '') {
|
|
|
|
return
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
if (newOp.length === 0) {
|
2020-05-06 06:09:33 -04:00
|
|
|
return newOp.push(c)
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
const last = newOp[newOp.length - 1]
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Compose the insert into the previous insert if possible
|
2020-05-06 06:09:33 -04:00
|
|
|
if (
|
|
|
|
last.i != null &&
|
|
|
|
c.i != null &&
|
|
|
|
last.p <= c.p &&
|
|
|
|
c.p <= last.p + last.i.length
|
|
|
|
) {
|
|
|
|
return (newOp[newOp.length - 1] = {
|
|
|
|
i: strInject(last.i, c.p - last.p, c.i),
|
2021-07-13 07:04:42 -04:00
|
|
|
p: last.p,
|
2020-05-06 06:09:33 -04:00
|
|
|
})
|
|
|
|
} else if (
|
|
|
|
last.d != null &&
|
|
|
|
c.d != null &&
|
|
|
|
c.p <= last.p &&
|
|
|
|
last.p <= c.p + c.d.length
|
|
|
|
) {
|
|
|
|
return (newOp[newOp.length - 1] = {
|
|
|
|
d: strInject(c.d, last.p - c.p, last.d),
|
2021-07-13 07:04:42 -04:00
|
|
|
p: c.p,
|
2020-05-06 06:09:33 -04:00
|
|
|
})
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
return newOp.push(c)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
text.compose = function (op1, op2) {
|
|
|
|
checkValidOp(op1)
|
|
|
|
checkValidOp(op2)
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const newOp = op1.slice()
|
|
|
|
for (const c of Array.from(op2)) {
|
|
|
|
append(newOp, c)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
return newOp
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Attempt to compress the op components together 'as much as possible'.
|
|
|
|
// This implementation preserves order and preserves create/delete pairs.
|
2021-07-13 07:04:42 -04:00
|
|
|
text.compress = op => text.compose([], op)
|
2020-05-06 06:09:33 -04:00
|
|
|
|
|
|
|
text.normalize = function (op) {
|
|
|
|
const newOp = []
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Normalize should allow ops which are a single (unwrapped) component:
|
|
|
|
// {i:'asdf', p:23}.
|
|
|
|
// There's no good way to test if something is an array:
|
|
|
|
// http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
|
|
|
|
// so this is probably the least bad solution.
|
2020-05-06 06:09:33 -04:00
|
|
|
if (op.i != null || op.p != null) {
|
|
|
|
op = [op]
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:15 -04:00
|
|
|
for (const c of Array.from(op)) {
|
2020-05-06 06:09:33 -04:00
|
|
|
if (c.p == null) {
|
|
|
|
c.p = 0
|
|
|
|
}
|
|
|
|
append(newOp, c)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
|
|
|
|
return newOp
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// This helper method transforms a position by an op component.
|
|
|
|
//
|
|
|
|
// If c is an insert, insertAfter specifies whether the transform
|
|
|
|
// is pushed after the insert (true) or before it (false).
|
|
|
|
//
|
|
|
|
// insertAfter is optional for deletes.
|
2020-05-06 06:09:33 -04:00
|
|
|
const transformPosition = function (pos, c, insertAfter) {
|
2020-05-06 06:08:21 -04:00
|
|
|
if (c.i != null) {
|
2020-05-06 06:09:33 -04:00
|
|
|
if (c.p < pos || (c.p === pos && insertAfter)) {
|
|
|
|
return pos + c.i.length
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
return pos
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length))
|
|
|
|
// but I think its harder to read that way, and it compiles using ternary operators anyway
|
|
|
|
// so its no slower written like this.
|
|
|
|
if (pos <= c.p) {
|
2020-05-06 06:09:33 -04:00
|
|
|
return pos
|
|
|
|
} else if (pos <= c.p + c.d.length) {
|
|
|
|
return c.p
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
return pos - c.d.length
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Helper method to transform a cursor position as a result of an op.
|
|
|
|
//
|
|
|
|
// Like transformPosition above, if c is an insert, insertAfter specifies whether the cursor position
|
|
|
|
// is pushed after an insert (true) or before it (false).
|
2020-05-06 06:09:33 -04:00
|
|
|
text.transformCursor = function (position, op, side) {
|
|
|
|
const insertAfter = side === 'right'
|
|
|
|
for (const c of Array.from(op)) {
|
|
|
|
position = transformPosition(position, c, insertAfter)
|
|
|
|
}
|
|
|
|
return position
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Transform an op component by another op component. Asymmetric.
|
|
|
|
// The result will be appended to destination.
|
|
|
|
//
|
|
|
|
// exported for use in JSON type
|
2020-05-06 06:09:33 -04:00
|
|
|
text._tc = transformComponent = function (dest, c, otherC, side) {
|
|
|
|
checkValidOp([c])
|
|
|
|
checkValidOp([otherC])
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
if (c.i != null) {
|
2020-05-06 06:09:33 -04:00
|
|
|
append(dest, {
|
|
|
|
i: c.i,
|
2021-07-13 07:04:42 -04:00
|
|
|
p: transformPosition(c.p, otherC, side === 'right'),
|
2020-05-06 06:09:33 -04:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
// Delete
|
|
|
|
if (otherC.i != null) {
|
|
|
|
// delete vs insert
|
|
|
|
let s = c.d
|
2020-05-06 06:08:21 -04:00
|
|
|
if (c.p < otherC.p) {
|
2020-05-06 06:09:33 -04:00
|
|
|
append(dest, { d: s.slice(0, otherC.p - c.p), p: c.p })
|
|
|
|
s = s.slice(otherC.p - c.p)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
if (s !== '') {
|
2020-05-06 06:09:33 -04:00
|
|
|
append(dest, { d: s, p: c.p + otherC.i.length })
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
} else {
|
|
|
|
// Delete vs delete
|
|
|
|
if (c.p >= otherC.p + otherC.d.length) {
|
|
|
|
append(dest, { d: c.d, p: c.p - otherC.d.length })
|
|
|
|
} else if (c.p + c.d.length <= otherC.p) {
|
|
|
|
append(dest, c)
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
|
|
|
// They overlap somewhere.
|
2020-05-06 06:09:33 -04:00
|
|
|
const newC = { d: '', p: c.p }
|
2020-05-06 06:08:21 -04:00
|
|
|
if (c.p < otherC.p) {
|
2020-05-06 06:09:33 -04:00
|
|
|
newC.d = c.d.slice(0, otherC.p - c.p)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
if (c.p + c.d.length > otherC.p + otherC.d.length) {
|
|
|
|
newC.d += c.d.slice(otherC.p + otherC.d.length - c.p)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// This is entirely optional - just for a check that the deleted
|
|
|
|
// text in the two ops matches
|
2020-05-06 06:09:33 -04:00
|
|
|
const intersectStart = Math.max(c.p, otherC.p)
|
|
|
|
const intersectEnd = Math.min(
|
|
|
|
c.p + c.d.length,
|
|
|
|
otherC.p + otherC.d.length
|
|
|
|
)
|
|
|
|
const cIntersect = c.d.slice(intersectStart - c.p, intersectEnd - c.p)
|
|
|
|
const otherIntersect = otherC.d.slice(
|
|
|
|
intersectStart - otherC.p,
|
|
|
|
intersectEnd - otherC.p
|
|
|
|
)
|
|
|
|
if (cIntersect !== otherIntersect) {
|
|
|
|
throw new Error(
|
|
|
|
'Delete ops delete different text in the same region of the document'
|
|
|
|
)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
if (newC.d !== '') {
|
|
|
|
// This could be rewritten similarly to insert v delete, above.
|
2020-05-06 06:09:33 -04:00
|
|
|
newC.p = transformPosition(newC.p, otherC)
|
|
|
|
append(dest, newC)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
return dest
|
|
|
|
}
|
|
|
|
|
|
|
|
const invertComponent = function (c) {
|
2020-05-06 06:08:21 -04:00
|
|
|
if (c.i != null) {
|
2020-05-06 06:09:33 -04:00
|
|
|
return { d: c.i, p: c.p }
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
return { i: c.d, p: c.p }
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// No need to use append for invert, because the components won't be able to
|
|
|
|
// cancel with one another.
|
2021-07-13 07:04:42 -04:00
|
|
|
text.invert = op =>
|
|
|
|
Array.from(op.slice().reverse()).map(c => invertComponent(c))
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
if (typeof WEB !== 'undefined' && WEB !== null) {
|
2020-05-06 06:09:33 -04:00
|
|
|
if (!exports.types) {
|
|
|
|
exports.types = {}
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// This is kind of awful - come up with a better way to hook this helper code up.
|
2020-05-06 06:09:33 -04:00
|
|
|
bootstrapTransform(text, transformComponent, checkValidOp, append)
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// [] is used to prevent closure from renaming types.text
|
2020-05-06 06:09:33 -04:00
|
|
|
exports.types.text = text
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
module.exports = text
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// The text type really shouldn't need this - it should be possible to define
|
|
|
|
// an efficient transform function by making a sort of transform map and passing each
|
|
|
|
// op component through it.
|
2020-05-06 06:09:33 -04:00
|
|
|
require('./helpers').bootstrapTransform(
|
|
|
|
text,
|
|
|
|
transformComponent,
|
|
|
|
checkValidOp,
|
|
|
|
append
|
|
|
|
)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|