| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- # 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)
|