mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-29 06:54:07 +00:00
441 lines
13 KiB
CoffeeScript
441 lines
13 KiB
CoffeeScript
# This is the implementation of the JSON OT type.
|
|
#
|
|
# Spec is here: https://github.com/josephg/ShareJS/wiki/JSON-Operations
|
|
|
|
if WEB?
|
|
text = exports.types.text
|
|
else
|
|
text = require './text'
|
|
|
|
json = {}
|
|
|
|
json.name = 'json'
|
|
|
|
json.create = -> null
|
|
|
|
json.invertComponent = (c) ->
|
|
c_ = {p: c.p}
|
|
c_.sd = c.si if c.si != undefined
|
|
c_.si = c.sd if c.sd != undefined
|
|
c_.od = c.oi if c.oi != undefined
|
|
c_.oi = c.od if c.od != undefined
|
|
c_.ld = c.li if c.li != undefined
|
|
c_.li = c.ld if c.ld != undefined
|
|
c_.na = -c.na if c.na != undefined
|
|
if c.lm != undefined
|
|
c_.lm = c.p[c.p.length-1]
|
|
c_.p = c.p[0...c.p.length - 1].concat([c.lm])
|
|
c_
|
|
|
|
json.invert = (op) -> json.invertComponent c for c in op.slice().reverse()
|
|
|
|
json.checkValidOp = (op) ->
|
|
|
|
isArray = (o) -> Object.prototype.toString.call(o) == '[object Array]'
|
|
json.checkList = (elem) ->
|
|
throw new Error 'Referenced element not a list' unless isArray(elem)
|
|
|
|
json.checkObj = (elem) ->
|
|
throw new Error "Referenced element not an object (it was #{JSON.stringify elem})" unless elem.constructor is Object
|
|
|
|
json.apply = (snapshot, op) ->
|
|
json.checkValidOp op
|
|
op = clone op
|
|
|
|
container = {data: clone snapshot}
|
|
|
|
try
|
|
for c, i in op
|
|
parent = null
|
|
parentkey = null
|
|
elem = container
|
|
key = 'data'
|
|
|
|
for p in c.p
|
|
parent = elem
|
|
parentkey = key
|
|
elem = elem[key]
|
|
key = p
|
|
|
|
throw new Error 'Path invalid' unless parent?
|
|
|
|
if c.na != undefined
|
|
# Number add
|
|
throw new Error 'Referenced element not a number' unless typeof elem[key] is 'number'
|
|
elem[key] += c.na
|
|
|
|
else if c.si != undefined
|
|
# String insert
|
|
throw new Error "Referenced element not a string (it was #{JSON.stringify elem})" unless typeof elem is 'string'
|
|
parent[parentkey] = elem[...key] + c.si + elem[key..]
|
|
else if c.sd != undefined
|
|
# String delete
|
|
throw new Error 'Referenced element not a string' unless typeof elem is 'string'
|
|
throw new Error 'Deleted string does not match' unless elem[key...key + c.sd.length] == c.sd
|
|
parent[parentkey] = elem[...key] + elem[key + c.sd.length..]
|
|
|
|
else if c.li != undefined && c.ld != undefined
|
|
# List replace
|
|
json.checkList elem
|
|
|
|
# Should check the list element matches c.ld
|
|
elem[key] = c.li
|
|
else if c.li != undefined
|
|
# List insert
|
|
json.checkList elem
|
|
|
|
elem.splice key, 0, c.li
|
|
else if c.ld != undefined
|
|
# List delete
|
|
json.checkList elem
|
|
|
|
# Should check the list element matches c.ld here too.
|
|
elem.splice key, 1
|
|
else if c.lm != undefined
|
|
# List move
|
|
json.checkList elem
|
|
if c.lm != key
|
|
e = elem[key]
|
|
# Remove it...
|
|
elem.splice key, 1
|
|
# And insert it back.
|
|
elem.splice c.lm, 0, e
|
|
|
|
else if c.oi != undefined
|
|
# Object insert / replace
|
|
json.checkObj elem
|
|
|
|
# Should check that elem[key] == c.od
|
|
elem[key] = c.oi
|
|
else if c.od != undefined
|
|
# Object delete
|
|
json.checkObj elem
|
|
|
|
# Should check that elem[key] == c.od
|
|
delete elem[key]
|
|
else
|
|
throw new Error 'invalid / missing instruction in op'
|
|
catch error
|
|
# TODO: Roll back all already applied changes. Write tests before implementing this code.
|
|
throw error
|
|
|
|
container.data
|
|
|
|
# Checks if two paths, p1 and p2 match.
|
|
json.pathMatches = (p1, p2, ignoreLast) ->
|
|
return false unless p1.length == p2.length
|
|
|
|
for p, i in p1
|
|
return false if p != p2[i] and (!ignoreLast or i != p1.length - 1)
|
|
|
|
true
|
|
|
|
json.append = (dest, c) ->
|
|
c = clone c
|
|
if dest.length != 0 and json.pathMatches c.p, (last = dest[dest.length - 1]).p
|
|
if last.na != undefined and c.na != undefined
|
|
dest[dest.length - 1] = { p: last.p, na: last.na + c.na }
|
|
else if last.li != undefined and c.li == undefined and c.ld == last.li
|
|
# insert immediately followed by delete becomes a noop.
|
|
if last.ld != undefined
|
|
# leave the delete part of the replace
|
|
delete last.li
|
|
else
|
|
dest.pop()
|
|
else if last.od != undefined and last.oi == undefined and
|
|
c.oi != undefined and c.od == undefined
|
|
last.oi = c.oi
|
|
else if c.lm != undefined and c.p[c.p.length-1] == c.lm
|
|
null # don't do anything
|
|
else
|
|
dest.push c
|
|
else
|
|
dest.push c
|
|
|
|
json.compose = (op1, op2) ->
|
|
json.checkValidOp op1
|
|
json.checkValidOp op2
|
|
|
|
newOp = clone op1
|
|
json.append newOp, c for c in op2
|
|
|
|
newOp
|
|
|
|
json.normalize = (op) ->
|
|
newOp = []
|
|
|
|
op = [op] unless isArray op
|
|
|
|
for c in op
|
|
c.p ?= []
|
|
json.append newOp, c
|
|
|
|
newOp
|
|
|
|
# hax, copied from test/types/json. Apparently this is still the fastest way to deep clone an object, assuming
|
|
# we have browser support for JSON.
|
|
# http://jsperf.com/cloning-an-object/12
|
|
clone = (o) -> JSON.parse(JSON.stringify o)
|
|
|
|
json.commonPath = (p1, p2) ->
|
|
p1 = p1.slice()
|
|
p2 = p2.slice()
|
|
p1.unshift('data')
|
|
p2.unshift('data')
|
|
p1 = p1[...p1.length-1]
|
|
p2 = p2[...p2.length-1]
|
|
return -1 if p2.length == 0
|
|
i = 0
|
|
while p1[i] == p2[i] && i < p1.length
|
|
i++
|
|
if i == p2.length
|
|
return i-1
|
|
return
|
|
|
|
# transform c so it applies to a document with otherC applied.
|
|
json.transformComponent = (dest, c, otherC, type) ->
|
|
c = clone c
|
|
c.p.push(0) if c.na != undefined
|
|
otherC.p.push(0) if otherC.na != undefined
|
|
|
|
common = json.commonPath c.p, otherC.p
|
|
common2 = json.commonPath otherC.p, c.p
|
|
|
|
cplength = c.p.length
|
|
otherCplength = otherC.p.length
|
|
|
|
c.p.pop() if c.na != undefined # hax
|
|
otherC.p.pop() if otherC.na != undefined
|
|
|
|
if otherC.na
|
|
if common2? && otherCplength >= cplength && otherC.p[common2] == c.p[common2]
|
|
if c.ld != undefined
|
|
oc = clone otherC
|
|
oc.p = oc.p[cplength..]
|
|
c.ld = json.apply clone(c.ld), [oc]
|
|
else if c.od != undefined
|
|
oc = clone otherC
|
|
oc.p = oc.p[cplength..]
|
|
c.od = json.apply clone(c.od), [oc]
|
|
json.append dest, c
|
|
return dest
|
|
|
|
if common2? && otherCplength > cplength && c.p[common2] == otherC.p[common2]
|
|
# transform based on c
|
|
if c.ld != undefined
|
|
oc = clone otherC
|
|
oc.p = oc.p[cplength..]
|
|
c.ld = json.apply clone(c.ld), [oc]
|
|
else if c.od != undefined
|
|
oc = clone otherC
|
|
oc.p = oc.p[cplength..]
|
|
c.od = json.apply clone(c.od), [oc]
|
|
|
|
|
|
if common?
|
|
commonOperand = cplength == otherCplength
|
|
# transform based on otherC
|
|
if otherC.na != undefined
|
|
# this case is handled above due to icky path hax
|
|
else if otherC.si != undefined || otherC.sd != undefined
|
|
# String op vs string op - pass through to text type
|
|
if c.si != undefined || c.sd != undefined
|
|
throw new Error("must be a string?") unless commonOperand
|
|
|
|
# Convert an op component to a text op component
|
|
convert = (component) ->
|
|
newC = p:component.p[component.p.length - 1]
|
|
if component.si
|
|
newC.i = component.si
|
|
else
|
|
newC.d = component.sd
|
|
newC
|
|
|
|
tc1 = convert c
|
|
tc2 = convert otherC
|
|
|
|
res = []
|
|
text._tc res, tc1, tc2, type
|
|
for tc in res
|
|
jc = { p: c.p[...common] }
|
|
jc.p.push(tc.p)
|
|
jc.si = tc.i if tc.i?
|
|
jc.sd = tc.d if tc.d?
|
|
json.append dest, jc
|
|
return dest
|
|
else if otherC.li != undefined && otherC.ld != undefined
|
|
if otherC.p[common] == c.p[common]
|
|
# noop
|
|
if !commonOperand
|
|
# we're below the deleted element, so -> noop
|
|
return dest
|
|
else if c.ld != undefined
|
|
# we're trying to delete the same element, -> noop
|
|
if c.li != undefined and type == 'left'
|
|
# we're both replacing one element with another. only one can
|
|
# survive!
|
|
c.ld = clone otherC.li
|
|
else
|
|
return dest
|
|
else if otherC.li != undefined
|
|
if c.li != undefined and c.ld == undefined and commonOperand and c.p[common] == otherC.p[common]
|
|
# in li vs. li, left wins.
|
|
if type == 'right'
|
|
c.p[common]++
|
|
else if otherC.p[common] <= c.p[common]
|
|
c.p[common]++
|
|
|
|
if c.lm != undefined
|
|
if commonOperand
|
|
# otherC edits the same list we edit
|
|
if otherC.p[common] <= c.lm
|
|
c.lm++
|
|
# changing c.from is handled above.
|
|
else if otherC.ld != undefined
|
|
if c.lm != undefined
|
|
if commonOperand
|
|
if otherC.p[common] == c.p[common]
|
|
# they deleted the thing we're trying to move
|
|
return dest
|
|
# otherC edits the same list we edit
|
|
p = otherC.p[common]
|
|
from = c.p[common]
|
|
to = c.lm
|
|
if p < to || (p == to && from < to)
|
|
c.lm--
|
|
|
|
if otherC.p[common] < c.p[common]
|
|
c.p[common]--
|
|
else if otherC.p[common] == c.p[common]
|
|
if otherCplength < cplength
|
|
# we're below the deleted element, so -> noop
|
|
return dest
|
|
else if c.ld != undefined
|
|
if c.li != undefined
|
|
# we're replacing, they're deleting. we become an insert.
|
|
delete c.ld
|
|
else
|
|
# we're trying to delete the same element, -> noop
|
|
return dest
|
|
else if otherC.lm != undefined
|
|
if c.lm != undefined and cplength == otherCplength
|
|
# lm vs lm, here we go!
|
|
from = c.p[common]
|
|
to = c.lm
|
|
otherFrom = otherC.p[common]
|
|
otherTo = otherC.lm
|
|
if otherFrom != otherTo
|
|
# if otherFrom == otherTo, we don't need to change our op.
|
|
|
|
# where did my thing go?
|
|
if from == otherFrom
|
|
# they moved it! tie break.
|
|
if type == 'left'
|
|
c.p[common] = otherTo
|
|
if from == to # ugh
|
|
c.lm = otherTo
|
|
else
|
|
return dest
|
|
else
|
|
# they moved around it
|
|
if from > otherFrom
|
|
c.p[common]--
|
|
if from > otherTo
|
|
c.p[common]++
|
|
else if from == otherTo
|
|
if otherFrom > otherTo
|
|
c.p[common]++
|
|
if from == to # ugh, again
|
|
c.lm++
|
|
|
|
# step 2: where am i going to put it?
|
|
if to > otherFrom
|
|
c.lm--
|
|
else if to == otherFrom
|
|
if to > from
|
|
c.lm--
|
|
if to > otherTo
|
|
c.lm++
|
|
else if to == otherTo
|
|
# if we're both moving in the same direction, tie break
|
|
if (otherTo > otherFrom and to > from) or
|
|
(otherTo < otherFrom and to < from)
|
|
if type == 'right'
|
|
c.lm++
|
|
else
|
|
if to > from
|
|
c.lm++
|
|
else if to == otherFrom
|
|
c.lm--
|
|
else if c.li != undefined and c.ld == undefined and commonOperand
|
|
# li
|
|
from = otherC.p[common]
|
|
to = otherC.lm
|
|
p = c.p[common]
|
|
if p > from
|
|
c.p[common]--
|
|
if p > to
|
|
c.p[common]++
|
|
else
|
|
# ld, ld+li, si, sd, na, oi, od, oi+od, any li on an element beneath
|
|
# the lm
|
|
#
|
|
# i.e. things care about where their item is after the move.
|
|
from = otherC.p[common]
|
|
to = otherC.lm
|
|
p = c.p[common]
|
|
if p == from
|
|
c.p[common] = to
|
|
else
|
|
if p > from
|
|
c.p[common]--
|
|
if p > to
|
|
c.p[common]++
|
|
else if p == to
|
|
if from > to
|
|
c.p[common]++
|
|
else if otherC.oi != undefined && otherC.od != undefined
|
|
if c.p[common] == otherC.p[common]
|
|
if c.oi != undefined and commonOperand
|
|
# we inserted where someone else replaced
|
|
if type == 'right'
|
|
# left wins
|
|
return dest
|
|
else
|
|
# we win, make our op replace what they inserted
|
|
c.od = otherC.oi
|
|
else
|
|
# -> noop if the other component is deleting the same object (or any
|
|
# parent)
|
|
return dest
|
|
else if otherC.oi != undefined
|
|
if c.oi != undefined and c.p[common] == otherC.p[common]
|
|
# left wins if we try to insert at the same place
|
|
if type == 'left'
|
|
json.append dest, {p:c.p, od:otherC.oi}
|
|
else
|
|
return dest
|
|
else if otherC.od != undefined
|
|
if c.p[common] == otherC.p[common]
|
|
return dest if !commonOperand
|
|
if c.oi != undefined
|
|
delete c.od
|
|
else
|
|
return dest
|
|
|
|
json.append dest, c
|
|
return dest
|
|
|
|
if WEB?
|
|
exports.types ||= {}
|
|
|
|
# This is kind of awful - come up with a better way to hook this helper code up.
|
|
exports._bt(json, json.transformComponent, json.checkValidOp, json.append)
|
|
|
|
# [] is used to prevent closure from renaming types.text
|
|
exports.types.json = json
|
|
else
|
|
module.exports = json
|
|
|
|
require('./helpers').bootstrapTransform(json, json.transformComponent, json.checkValidOp, json.append)
|
|
|