mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-30 02:45:26 -05:00
262 lines
7.7 KiB
CoffeeScript
262 lines
7.7 KiB
CoffeeScript
|
# 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.
|
||
|
|
||
|
p = -> #require('util').debug
|
||
|
i = -> #require('util').inspect
|
||
|
|
||
|
exports = if WEB? then {} else module.exports
|
||
|
|
||
|
exports.name = 'text-composable'
|
||
|
|
||
|
exports.create = -> ''
|
||
|
|
||
|
# -------- Utility methods
|
||
|
|
||
|
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'
|
||
|
throw new Error("Invalid op component: #{i c}") unless (c.i? && c.i.length > 0) or (c.d? && c.d.length > 0)
|
||
|
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 added') if typeof(last) == 'number'
|
||
|
|
||
|
last = c
|
||
|
|
||
|
# Makes a function for appending components to a given op.
|
||
|
# Exported for the randomOpGenerator.
|
||
|
exports._makeAppend = makeAppend = (op) -> (component) ->
|
||
|
if component == 0 || component.i == '' || component.d == ''
|
||
|
return
|
||
|
else if op.length == 0
|
||
|
op.push component
|
||
|
else if typeof(component) == 'number' && typeof(op[op.length - 1]) == 'number'
|
||
|
op[op.length - 1] += component
|
||
|
else if component.i? && op[op.length - 1].i?
|
||
|
op[op.length - 1].i += component.i
|
||
|
else if component.d? && op[op.length - 1].d?
|
||
|
op[op.length - 1].d += component.d
|
||
|
else
|
||
|
op.push component
|
||
|
|
||
|
# 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.
|
||
|
makeTake = (op) ->
|
||
|
# The index of the next component to take
|
||
|
idx = 0
|
||
|
# The offset into the component
|
||
|
offset = 0
|
||
|
|
||
|
# 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.
|
||
|
take = (n, indivisableField) ->
|
||
|
return null if idx == op.length
|
||
|
#assert.notStrictEqual op.length, i, 'The op is too short to traverse the document'
|
||
|
|
||
|
if typeof(op[idx]) == 'number'
|
||
|
if !n? or op[idx] - offset <= n
|
||
|
c = op[idx] - offset
|
||
|
++idx; offset = 0
|
||
|
c
|
||
|
else
|
||
|
offset += n
|
||
|
n
|
||
|
else
|
||
|
# Take from the string
|
||
|
field = if op[idx].i then 'i' else 'd'
|
||
|
c = {}
|
||
|
if !n? or op[idx][field].length - offset <= n or field == indivisableField
|
||
|
c[field] = op[idx][field][offset..]
|
||
|
++idx; offset = 0
|
||
|
else
|
||
|
c[field] = op[idx][field][offset...(offset + n)]
|
||
|
offset += n
|
||
|
c
|
||
|
|
||
|
peekType = () ->
|
||
|
op[idx]
|
||
|
|
||
|
[take, peekType]
|
||
|
|
||
|
# Find and return the length of an op component
|
||
|
componentLength = (component) ->
|
||
|
if typeof(component) == 'number'
|
||
|
component
|
||
|
else if component.i?
|
||
|
component.i.length
|
||
|
else
|
||
|
component.d.length
|
||
|
|
||
|
# Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
|
||
|
# adjacent inserts and deletes.
|
||
|
exports.normalize = (op) ->
|
||
|
newOp = []
|
||
|
append = makeAppend newOp
|
||
|
append component for component in op
|
||
|
newOp
|
||
|
|
||
|
# Apply the op to the string. Returns the new string.
|
||
|
exports.apply = (str, op) ->
|
||
|
p "Applying #{i op} to '#{str}'"
|
||
|
throw new Error('Snapshot should be a string') unless typeof(str) == 'string'
|
||
|
checkOp op
|
||
|
|
||
|
pos = 0
|
||
|
newDoc = []
|
||
|
|
||
|
for component in op
|
||
|
if typeof(component) == 'number'
|
||
|
throw new Error('The op is too long for this document') if component > str.length
|
||
|
newDoc.push str[...component]
|
||
|
str = str[component..]
|
||
|
else if component.i?
|
||
|
newDoc.push component.i
|
||
|
else
|
||
|
throw new Error("The deleted text '#{component.d}' doesn't match the next characters in the document '#{str[...component.d.length]}'") unless component.d == str[...component.d.length]
|
||
|
str = str[component.d.length..]
|
||
|
|
||
|
throw new Error("The applied op doesn't traverse the entire document") unless '' == str
|
||
|
|
||
|
newDoc.join ''
|
||
|
|
||
|
# transform op1 by op2. Return transformed version of op1.
|
||
|
# op1 and op2 are unchanged by transform.
|
||
|
exports.transform = (op, otherOp, side) ->
|
||
|
throw new Error "side (#{side} must be 'left' or 'right'" unless side == 'left' or side == 'right'
|
||
|
|
||
|
checkOp op
|
||
|
checkOp otherOp
|
||
|
newOp = []
|
||
|
|
||
|
append = makeAppend newOp
|
||
|
[take, peek] = makeTake op
|
||
|
|
||
|
for component in otherOp
|
||
|
if typeof(component) == 'number' # Skip
|
||
|
length = component
|
||
|
while length > 0
|
||
|
chunk = take(length, 'i')
|
||
|
throw new Error('The op traverses more elements than the document has') unless chunk != null
|
||
|
|
||
|
append chunk
|
||
|
length -= componentLength chunk unless typeof(chunk) == 'object' && chunk.i?
|
||
|
else if component.i? # Insert
|
||
|
if side == 'left'
|
||
|
# The left insert should go first.
|
||
|
o = peek()
|
||
|
append take() if o?.i
|
||
|
|
||
|
# Otherwise, skip the inserted text.
|
||
|
append(component.i.length)
|
||
|
else # Delete.
|
||
|
#assert.ok component.d
|
||
|
length = component.d.length
|
||
|
while length > 0
|
||
|
chunk = take(length, 'i')
|
||
|
throw new Error('The op traverses more elements than the document has') unless chunk != null
|
||
|
|
||
|
if typeof(chunk) == 'number'
|
||
|
length -= chunk
|
||
|
else if chunk.i?
|
||
|
append(chunk)
|
||
|
else
|
||
|
#assert.ok chunk.d
|
||
|
# The delete is unnecessary now.
|
||
|
length -= chunk.d.length
|
||
|
|
||
|
# Append extras from op1
|
||
|
while (component = take())
|
||
|
throw new Error "Remaining fragments in the op: #{i component}" unless component?.i?
|
||
|
append component
|
||
|
|
||
|
newOp
|
||
|
|
||
|
|
||
|
# Compose 2 ops into 1 op.
|
||
|
exports.compose = (op1, op2) ->
|
||
|
p "COMPOSE #{i op1} + #{i op2}"
|
||
|
checkOp op1
|
||
|
checkOp op2
|
||
|
|
||
|
result = []
|
||
|
|
||
|
append = makeAppend result
|
||
|
[take, _] = makeTake op1
|
||
|
|
||
|
for component in op2
|
||
|
if typeof(component) == 'number' # Skip
|
||
|
length = component
|
||
|
while length > 0
|
||
|
chunk = take(length, 'd')
|
||
|
throw new Error('The op traverses more elements than the document has') unless chunk != null
|
||
|
|
||
|
append chunk
|
||
|
length -= componentLength chunk unless typeof(chunk) == 'object' && chunk.d?
|
||
|
|
||
|
else if component.i? # Insert
|
||
|
append {i:component.i}
|
||
|
|
||
|
else # Delete
|
||
|
offset = 0
|
||
|
while offset < component.d.length
|
||
|
chunk = take(component.d.length - offset, 'd')
|
||
|
throw new Error('The op traverses more elements than the document has') unless chunk != null
|
||
|
|
||
|
# 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.
|
||
|
if typeof(chunk) == 'number'
|
||
|
append {d:component.d[offset...(offset + chunk)]}
|
||
|
offset += chunk
|
||
|
else if chunk.i?
|
||
|
throw new Error("The deleted text doesn't match the inserted text") unless component.d[offset...(offset + chunk.i.length)] == chunk.i
|
||
|
offset += chunk.i.length
|
||
|
# The ops cancel each other out.
|
||
|
else
|
||
|
# Delete
|
||
|
append chunk
|
||
|
|
||
|
# Append extras from op1
|
||
|
while (component = take())
|
||
|
throw new Error "Trailing stuff in op1 #{i component}" unless component?.d?
|
||
|
append component
|
||
|
|
||
|
result
|
||
|
|
||
|
|
||
|
invertComponent = (c) ->
|
||
|
if typeof(c) == 'number'
|
||
|
c
|
||
|
else if c.i?
|
||
|
{d:c.i}
|
||
|
else
|
||
|
{i:c.d}
|
||
|
|
||
|
# Invert an op
|
||
|
exports.invert = (op) ->
|
||
|
result = []
|
||
|
append = makeAppend result
|
||
|
|
||
|
append(invertComponent component) for component in op
|
||
|
|
||
|
result
|
||
|
|
||
|
if window?
|
||
|
window.ot ||= {}
|
||
|
window.ot.types ||= {}
|
||
|
window.ot.types.text = exports
|
||
|
|