2020-05-06 06:09:15 -04:00
|
|
|
/* 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.
|
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
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
// An alternate composable implementation for text. This is much closer
|
|
|
|
// to the implementation used by google wave.
|
|
|
|
//
|
|
|
|
// Ops are lists of components which iterate over the whole document.
|
|
|
|
// Components are either:
|
|
|
|
// A number N: Skip N characters in the original document
|
|
|
|
// {i:'str'}: Insert 'str' at the current position in the document
|
|
|
|
// {d:'str'}: Delete 'str', which appears at the current position in the document
|
|
|
|
//
|
|
|
|
// Eg: [3, {i:'hi'}, 5, {d:'internet'}]
|
|
|
|
//
|
|
|
|
// Snapshots are strings.
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
let makeAppend
|
|
|
|
const p = function () {} // require('util').debug
|
|
|
|
const i = function () {} // require('util').inspect
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const exports = typeof WEB !== 'undefined' && WEB !== null ? {} : module.exports
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
exports.name = 'text-composable'
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
exports.create = () => ''
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// -------- Utility methods
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const checkOp = function (op) {
|
|
|
|
if (!Array.isArray(op)) {
|
|
|
|
throw new Error('Op must be an array of components')
|
|
|
|
}
|
|
|
|
let last = null
|
2020-05-06 06:08:21 -04:00
|
|
|
return (() => {
|
2020-05-06 06:09:33 -04:00
|
|
|
const result = []
|
2020-05-06 06:09:15 -04:00
|
|
|
for (const c of Array.from(op)) {
|
2020-05-06 06:09:33 -04:00
|
|
|
if (typeof c === 'object') {
|
|
|
|
if (
|
|
|
|
(c.i == null || !(c.i.length > 0)) &&
|
|
|
|
(c.d == null || !(c.d.length > 0))
|
|
|
|
) {
|
|
|
|
throw new Error(`Invalid op component: ${i(c)}`)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
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 added')
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
result.push((last = c))
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
return result
|
|
|
|
})()
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Makes a function for appending components to a given op.
|
|
|
|
// Exported for the randomOpGenerator.
|
2020-05-06 06:09:33 -04:00
|
|
|
exports._makeAppend = makeAppend = (op) =>
|
|
|
|
function (component) {
|
|
|
|
if (component === 0 || component.i === '' || component.d === '') {
|
|
|
|
} else if (op.length === 0) {
|
|
|
|
return op.push(component)
|
|
|
|
} else if (
|
|
|
|
typeof component === 'number' &&
|
|
|
|
typeof op[op.length - 1] === 'number'
|
|
|
|
) {
|
|
|
|
return (op[op.length - 1] += component)
|
|
|
|
} else if (component.i != null && op[op.length - 1].i != null) {
|
|
|
|
return (op[op.length - 1].i += component.i)
|
|
|
|
} else if (component.d != null && op[op.length - 1].d != null) {
|
|
|
|
return (op[op.length - 1].d += component.d)
|
|
|
|
} else {
|
|
|
|
return op.push(component)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
|
2020-05-06 06:08:21 -04:00
|
|
|
// checkOp op
|
|
|
|
|
|
|
|
// Makes 2 functions for taking components from the start of an op, and for peeking
|
|
|
|
// at the next op that could be taken.
|
2020-05-06 06:09:33 -04:00
|
|
|
const makeTake = function (op) {
|
2020-05-06 06:08:21 -04:00
|
|
|
// The index of the next component to take
|
2020-05-06 06:09:33 -04:00
|
|
|
let idx = 0
|
2020-05-06 06:08:21 -04:00
|
|
|
// The offset into the component
|
2020-05-06 06:09:33 -04:00
|
|
|
let offset = 0
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Take up to length n from the front of op. If n is null, take the next
|
|
|
|
// op component. If indivisableField == 'd', delete components won't be separated.
|
|
|
|
// If indivisableField == 'i', insert components won't be separated.
|
2020-05-06 06:09:33 -04:00
|
|
|
const take = function (n, indivisableField) {
|
|
|
|
let c
|
|
|
|
if (idx === op.length) {
|
|
|
|
return null
|
|
|
|
}
|
2020-05-06 06:09:15 -04:00
|
|
|
// assert.notStrictEqual op.length, i, 'The op is too short to traverse the document'
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
if (typeof op[idx] === 'number') {
|
|
|
|
if (n == null || op[idx] - offset <= n) {
|
|
|
|
c = op[idx] - offset
|
|
|
|
++idx
|
|
|
|
offset = 0
|
|
|
|
return c
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
offset += n
|
|
|
|
return n
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Take from the string
|
2020-05-06 06:09:33 -04:00
|
|
|
const field = op[idx].i ? 'i' : 'd'
|
|
|
|
c = {}
|
|
|
|
if (
|
|
|
|
n == null ||
|
|
|
|
op[idx][field].length - offset <= n ||
|
|
|
|
field === indivisableField
|
|
|
|
) {
|
|
|
|
c[field] = op[idx][field].slice(offset)
|
|
|
|
++idx
|
|
|
|
offset = 0
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
c[field] = op[idx][field].slice(offset, offset + n)
|
|
|
|
offset += n
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
return c
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const peekType = () => op[idx]
|
|
|
|
|
|
|
|
return [take, peekType]
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Find and return the length of an op component
|
2020-05-06 06:09:33 -04:00
|
|
|
const componentLength = function (component) {
|
|
|
|
if (typeof component === 'number') {
|
|
|
|
return component
|
2020-05-06 06:08:21 -04:00
|
|
|
} else if (component.i != null) {
|
2020-05-06 06:09:33 -04:00
|
|
|
return component.i.length
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
return component.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
|
|
|
|
|
|
|
// Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
|
|
|
|
// adjacent inserts and deletes.
|
2020-05-06 06:09:33 -04:00
|
|
|
exports.normalize = function (op) {
|
|
|
|
const newOp = []
|
|
|
|
const append = makeAppend(newOp)
|
|
|
|
for (const component of Array.from(op)) {
|
|
|
|
append(component)
|
|
|
|
}
|
|
|
|
return newOp
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Apply the op to the string. Returns the new string.
|
2020-05-06 06:09:33 -04:00
|
|
|
exports.apply = function (str, op) {
|
|
|
|
p(`Applying ${i(op)} to '${str}'`)
|
|
|
|
if (typeof str !== 'string') {
|
|
|
|
throw new Error('Snapshot should be a string')
|
|
|
|
}
|
|
|
|
checkOp(op)
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const pos = 0
|
|
|
|
const newDoc = []
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:15 -04:00
|
|
|
for (const component of Array.from(op)) {
|
2020-05-06 06:09:33 -04:00
|
|
|
if (typeof component === 'number') {
|
|
|
|
if (component > str.length) {
|
|
|
|
throw new Error('The op is too long for this document')
|
|
|
|
}
|
|
|
|
newDoc.push(str.slice(0, component))
|
|
|
|
str = str.slice(component)
|
2020-05-06 06:08:21 -04:00
|
|
|
} else if (component.i != null) {
|
2020-05-06 06:09:33 -04:00
|
|
|
newDoc.push(component.i)
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
if (component.d !== str.slice(0, component.d.length)) {
|
|
|
|
throw new Error(
|
|
|
|
`The deleted text '${
|
|
|
|
component.d
|
|
|
|
}' doesn't match the next characters in the document '${str.slice(
|
|
|
|
0,
|
|
|
|
component.d.length
|
|
|
|
)}'`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
str = str.slice(component.d.length)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
if (str !== '') {
|
|
|
|
throw new Error("The applied op doesn't traverse the entire document")
|
|
|
|
}
|
|
|
|
|
|
|
|
return newDoc.join('')
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// transform op1 by op2. Return transformed version of op1.
|
|
|
|
// op1 and op2 are unchanged by transform.
|
2020-05-06 06:09:33 -04:00
|
|
|
exports.transform = function (op, otherOp, side) {
|
|
|
|
let component
|
|
|
|
if (side !== 'left' && side !== 'right') {
|
|
|
|
throw new Error(`side (${side} must be 'left' or 'right'`)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
checkOp(op)
|
|
|
|
checkOp(otherOp)
|
|
|
|
const newOp = []
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const append = makeAppend(newOp)
|
|
|
|
const [take, peek] = Array.from(makeTake(op))
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
for (component of Array.from(otherOp)) {
|
2020-05-06 06:09:33 -04:00
|
|
|
var chunk, length
|
|
|
|
if (typeof component === 'number') {
|
|
|
|
// Skip
|
|
|
|
length = component
|
2020-05-06 06:08:21 -04:00
|
|
|
while (length > 0) {
|
2020-05-06 06:09:33 -04:00
|
|
|
chunk = take(length, 'i')
|
|
|
|
if (chunk === null) {
|
|
|
|
throw new Error(
|
|
|
|
'The op traverses more elements than the document has'
|
|
|
|
)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
append(chunk)
|
|
|
|
if (typeof chunk !== 'object' || chunk.i == null) {
|
|
|
|
length -= componentLength(chunk)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
} else if (component.i != null) {
|
|
|
|
// Insert
|
2020-05-06 06:08:21 -04:00
|
|
|
if (side === 'left') {
|
|
|
|
// The left insert should go first.
|
2020-05-06 06:09:33 -04:00
|
|
|
const o = peek()
|
|
|
|
if (o != null ? o.i : undefined) {
|
|
|
|
append(take())
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, skip the inserted text.
|
2020-05-06 06:09:33 -04:00
|
|
|
append(component.i.length)
|
|
|
|
} else {
|
|
|
|
// Delete.
|
2020-05-06 06:09:15 -04:00
|
|
|
// assert.ok component.d
|
2020-05-06 06:09:33 -04:00
|
|
|
;({ length } = component.d)
|
2020-05-06 06:08:21 -04:00
|
|
|
while (length > 0) {
|
2020-05-06 06:09:33 -04:00
|
|
|
chunk = take(length, 'i')
|
|
|
|
if (chunk === null) {
|
|
|
|
throw new Error(
|
|
|
|
'The op traverses more elements than the document has'
|
|
|
|
)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
if (typeof chunk === 'number') {
|
|
|
|
length -= chunk
|
2020-05-06 06:08:21 -04:00
|
|
|
} else if (chunk.i != null) {
|
2020-05-06 06:09:33 -04:00
|
|
|
append(chunk)
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:15 -04:00
|
|
|
// assert.ok chunk.d
|
2020-05-06 06:08:21 -04:00
|
|
|
// The delete is unnecessary now.
|
2020-05-06 06:09:33 -04:00
|
|
|
length -= chunk.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
|
|
|
// Append extras from op1
|
2020-05-06 06:09:33 -04:00
|
|
|
while ((component = take())) {
|
|
|
|
if ((component != null ? component.i : undefined) == null) {
|
|
|
|
throw new Error(`Remaining fragments in the op: ${i(component)}`)
|
|
|
|
}
|
|
|
|
append(component)
|
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
|
|
|
|
|
|
|
// Compose 2 ops into 1 op.
|
2020-05-06 06:09:33 -04:00
|
|
|
exports.compose = function (op1, op2) {
|
|
|
|
let component
|
|
|
|
p(`COMPOSE ${i(op1)} + ${i(op2)}`)
|
|
|
|
checkOp(op1)
|
|
|
|
checkOp(op2)
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const result = []
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const append = makeAppend(result)
|
|
|
|
const [take, _] = Array.from(makeTake(op1))
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
for (component of Array.from(op2)) {
|
2020-05-06 06:09:33 -04:00
|
|
|
var chunk, length
|
|
|
|
if (typeof component === 'number') {
|
|
|
|
// Skip
|
|
|
|
length = component
|
2020-05-06 06:08:21 -04:00
|
|
|
while (length > 0) {
|
2020-05-06 06:09:33 -04:00
|
|
|
chunk = take(length, 'd')
|
|
|
|
if (chunk === null) {
|
|
|
|
throw new Error(
|
|
|
|
'The op traverses more elements than the document has'
|
|
|
|
)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
append(chunk)
|
|
|
|
if (typeof chunk !== 'object' || chunk.d == null) {
|
|
|
|
length -= componentLength(chunk)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
} else if (component.i != null) {
|
|
|
|
// Insert
|
|
|
|
append({ i: component.i })
|
|
|
|
} else {
|
|
|
|
// Delete
|
|
|
|
let offset = 0
|
2020-05-06 06:08:21 -04:00
|
|
|
while (offset < component.d.length) {
|
2020-05-06 06:09:33 -04:00
|
|
|
chunk = take(component.d.length - offset, 'd')
|
|
|
|
if (chunk === null) {
|
|
|
|
throw new Error(
|
|
|
|
'The op traverses more elements than the document has'
|
|
|
|
)
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// If its delete, append it. If its skip, drop it and decrease length. If its insert, check the strings match, drop it and decrease length.
|
2020-05-06 06:09:33 -04:00
|
|
|
if (typeof chunk === 'number') {
|
|
|
|
append({ d: component.d.slice(offset, offset + chunk) })
|
|
|
|
offset += chunk
|
2020-05-06 06:08:21 -04:00
|
|
|
} else if (chunk.i != null) {
|
2020-05-06 06:09:33 -04:00
|
|
|
if (component.d.slice(offset, offset + chunk.i.length) !== chunk.i) {
|
|
|
|
throw new Error("The deleted text doesn't match the inserted text")
|
|
|
|
}
|
|
|
|
offset += chunk.i.length
|
2020-05-06 06:08:21 -04:00
|
|
|
// The ops cancel each other out.
|
|
|
|
} else {
|
|
|
|
// Delete
|
2020-05-06 06:09:33 -04:00
|
|
|
append(chunk)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
|
2020-05-06 06:08:21 -04:00
|
|
|
// Append extras from op1
|
2020-05-06 06:09:33 -04:00
|
|
|
while ((component = take())) {
|
|
|
|
if ((component != null ? component.d : undefined) == null) {
|
|
|
|
throw new Error(`Trailing stuff in op1 ${i(component)}`)
|
|
|
|
}
|
|
|
|
append(component)
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
return result
|
|
|
|
}
|
2014-02-12 05:40:42 -05:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
const invertComponent = function (c) {
|
|
|
|
if (typeof c === 'number') {
|
|
|
|
return c
|
2020-05-06 06:08:21 -04:00
|
|
|
} else if (c.i != null) {
|
2020-05-06 06:09:33 -04:00
|
|
|
return { d: c.i }
|
2020-05-06 06:08:21 -04:00
|
|
|
} else {
|
2020-05-06 06:09:33 -04:00
|
|
|
return { i: c.d }
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
|
|
|
// Invert an op
|
2020-05-06 06:09:33 -04:00
|
|
|
exports.invert = function (op) {
|
|
|
|
const result = []
|
|
|
|
const append = makeAppend(result)
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
for (const component of Array.from(op)) {
|
|
|
|
append(invertComponent(component))
|
|
|
|
}
|
2020-05-06 06:08:21 -04:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
return result
|
2020-05-06 06:08:21 -04:00
|
|
|
}
|
2014-02-12 05:40:42 -05:00
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
if (typeof window !== 'undefined' && window !== null) {
|
|
|
|
if (!window.ot) {
|
|
|
|
window.ot = {}
|
|
|
|
}
|
|
|
|
if (!window.ot.types) {
|
|
|
|
window.ot.types = {}
|
|
|
|
}
|
|
|
|
window.ot.types.text = exports
|
|
|
|
}
|