mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 19:33:41 -05:00
322 lines
10 KiB
CoffeeScript
322 lines
10 KiB
CoffeeScript
# 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']
|
|
|
|
type =
|
|
name: 'text-tp2'
|
|
tp2: true
|
|
create: -> {charLength:0, totalLength:0, positionCache:[], data:[]}
|
|
serialize: (doc) ->
|
|
throw new Error 'invalid doc snapshot' unless doc.data
|
|
doc.data
|
|
deserialize: (data) ->
|
|
doc = type.create()
|
|
doc.data = data
|
|
|
|
for component in data
|
|
if typeof component is 'string'
|
|
doc.charLength += component.length
|
|
doc.totalLength += component.length
|
|
else
|
|
doc.totalLength += component
|
|
|
|
doc
|
|
|
|
|
|
checkOp = (op) ->
|
|
throw new Error('Op must be an array of components') unless Array.isArray(op)
|
|
last = null
|
|
for c in op
|
|
if typeof(c) == 'object'
|
|
if c.i != undefined
|
|
throw new Error('Inserts must insert a string or a +ive number') unless (typeof(c.i) == 'string' and c.i.length > 0) or (typeof(c.i) == 'number' and c.i > 0)
|
|
else if c.d != undefined
|
|
throw new Error('Deletes must be a +ive number') unless typeof(c.d) == 'number' and c.d > 0
|
|
else
|
|
throw new Error('Operation component must define .i or .d')
|
|
else
|
|
throw new Error('Op components must be objects or numbers') unless typeof(c) == 'number'
|
|
throw new Error('Skip components must be a positive number') unless c > 0
|
|
throw new Error('Adjacent skip components should be combined') if typeof(last) == 'number'
|
|
|
|
last = c
|
|
|
|
# Take the next part from the specified position in a document snapshot.
|
|
# position = {index, offset}. It will be updated.
|
|
type._takeDoc = takeDoc = (doc, position, maxlength, tombsIndivisible) ->
|
|
throw new Error 'Operation goes past the end of the document' if position.index >= doc.data.length
|
|
|
|
part = doc.data[position.index]
|
|
# peel off data[0]
|
|
result = if typeof(part) == 'string'
|
|
if maxlength != undefined
|
|
part[position.offset...(position.offset + maxlength)]
|
|
else
|
|
part[position.offset...]
|
|
else
|
|
if maxlength == undefined or tombsIndivisible
|
|
part - position.offset
|
|
else
|
|
Math.min(maxlength, part - position.offset)
|
|
|
|
resultLen = result.length || result
|
|
|
|
if (part.length || part) - position.offset > resultLen
|
|
position.offset += resultLen
|
|
else
|
|
position.index++
|
|
position.offset = 0
|
|
|
|
result
|
|
|
|
# Append a part to the end of a document
|
|
type._appendDoc = appendDoc = (doc, p) ->
|
|
return if p == 0 or p == ''
|
|
|
|
if typeof p is 'string'
|
|
doc.charLength += p.length
|
|
doc.totalLength += p.length
|
|
else
|
|
doc.totalLength += p
|
|
|
|
data = doc.data
|
|
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
|
|
return
|
|
|
|
# Apply the op to the document. The document is not modified in the process.
|
|
type.apply = (doc, op) ->
|
|
unless doc.totalLength != undefined and doc.charLength != undefined and doc.data.length != undefined
|
|
throw new Error('Snapshot is invalid')
|
|
|
|
checkOp op
|
|
|
|
newDoc = type.create()
|
|
position = {index:0, offset:0}
|
|
|
|
for component in op
|
|
if typeof(component) is '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
|
|
|
|
newDoc
|
|
|
|
# Append an op component to the end of the specified op.
|
|
# Exported for the randomOpGenerator.
|
|
type._append = append = (op, component) ->
|
|
if component == 0 || component.i == '' || component.i == 0 || component.d == 0
|
|
return
|
|
else if op.length == 0
|
|
op.push component
|
|
else
|
|
last = op[op.length - 1]
|
|
if typeof(component) == 'number' && typeof(last) == 'number'
|
|
op[op.length - 1] += component
|
|
else if component.i != undefined && last.i? && typeof(last.i) == typeof(component.i)
|
|
last.i += component.i
|
|
else if component.d != undefined && last.d?
|
|
last.d += component.d
|
|
else
|
|
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.
|
|
makeTake = (op) ->
|
|
# The index of the next component to take
|
|
index = 0
|
|
# The offset into the component
|
|
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.
|
|
take = (maxlength, insertsIndivisible) ->
|
|
return null if index == op.length
|
|
|
|
e = op[index]
|
|
if typeof((current = e)) == 'number' or typeof((current = e.i)) == 'number' or (current = e.d) != undefined
|
|
if !maxlength? or current - offset <= maxlength or (insertsIndivisible and 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 then {i:c} else if e.d != undefined then {d:c} else c
|
|
else
|
|
# Take from the inserted string
|
|
if !maxlength? or e.i.length - offset <= maxlength or insertsIndivisible
|
|
result = {i:e.i[offset..]}
|
|
++index; offset = 0
|
|
else
|
|
result = {i:e.i[offset...offset + maxlength]}
|
|
offset += maxlength
|
|
result
|
|
|
|
peekType = -> op[index]
|
|
|
|
[take, peekType]
|
|
|
|
# Find and return the length of an op component
|
|
componentLength = (component) ->
|
|
if typeof(component) == 'number'
|
|
component
|
|
else if typeof(component.i) == 'string'
|
|
component.i.length
|
|
else
|
|
# This should work because c.d and c.i must be +ive.
|
|
component.d or component.i
|
|
|
|
# Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
|
|
# adjacent inserts and deletes.
|
|
type.normalize = (op) ->
|
|
newOp = []
|
|
append newOp, component for component in op
|
|
newOp
|
|
|
|
# This is a helper method to transform and prune. goForwards is true for transform, false for prune.
|
|
transformer = (op, otherOp, goForwards, side) ->
|
|
checkOp op
|
|
checkOp otherOp
|
|
newOp = []
|
|
|
|
[take, peek] = makeTake op
|
|
|
|
for component in otherOp
|
|
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.
|
|
append newOp, take() while peek()?.i != undefined
|
|
|
|
# In any case, skip the inserted text.
|
|
append newOp, length
|
|
|
|
else # Prune. Remove skips for inserts.
|
|
while length > 0
|
|
chunk = take length, true
|
|
|
|
throw new Error 'The transformed op is invalid' unless chunk != null
|
|
throw new Error 'The transformed op deletes locally inserted characters - it cannot be purged of the insert.' if chunk.d != undefined
|
|
|
|
if typeof chunk is 'number'
|
|
length -= chunk
|
|
else
|
|
append newOp, chunk
|
|
|
|
else # Skip or delete
|
|
while length > 0
|
|
chunk = take length, true
|
|
throw new Error('The op traverses more elements than the document has') unless chunk != null
|
|
|
|
append newOp, chunk
|
|
length -= componentLength chunk unless chunk.i
|
|
|
|
# Append extras from op1
|
|
while (component = take())
|
|
throw new Error "Remaining fragments in the op: #{component}" unless component.i != undefined
|
|
append newOp, component
|
|
|
|
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 = (op, otherOp, side) ->
|
|
throw new Error "side (#{side}) should be 'left' or 'right'" unless side == 'left' or side == 'right'
|
|
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 = (op1, op2) ->
|
|
return op2 if op1 == null or op1 == undefined
|
|
|
|
checkOp op1
|
|
checkOp op2
|
|
|
|
result = []
|
|
|
|
[take, _] = makeTake op1
|
|
|
|
for component in op2
|
|
|
|
if typeof(component) == 'number' # Skip
|
|
# Just copy from op1.
|
|
length = component
|
|
while length > 0
|
|
chunk = take length
|
|
throw new Error('The op traverses more elements than the document has') unless chunk != null
|
|
|
|
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
|
|
throw new Error('The op traverses more elements than the document has') unless chunk != null
|
|
|
|
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())
|
|
throw new Error "Remaining fragments in op1: #{component}" unless component.i != undefined
|
|
append result, component
|
|
|
|
result
|
|
|
|
if WEB?
|
|
exports.types['text-tp2'] = type
|
|
else
|
|
module.exports = type
|
|
|