overleaf/services/document-updater/app/coffee/sharejs/text-tp2.coffee
2014-02-12 10:40:42 +00:00

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