diff --git a/README.md b/README.md index 678ec59..a44cc14 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# JSON0 OT Type +

+ JSON 00: Operational Transform Type +
+ Build Status +

The JSON OT type can be used to edit arbitrary JSON documents. @@ -64,6 +68,7 @@ into the array. `{p:[path], t:subtype, o:subtypeOp}` | applies the subtype op `o` of type `t` to the object at `[path]` `{p:[path,offset], si:s}` | inserts the string `s` at offset `offset` into the string at `[path]` (uses subtypes internally). `{p:[path,offset], sd:s}` | deletes the string `s` at offset `offset` from the string at `[path]` (uses subtypes internally). +`{p:[path], e:data}` | no-op to data, but can be used to trigger an event with data on the server. --- @@ -96,7 +101,7 @@ Lists and objects have the same set of operations (*Insert*, *Delete*, *Replace*, *Move*) but their semantics are very different. List operations shuffle adjacent list items left or right to make space (or to remove space). Object operations do not. You should pick the data structure which will give -you the behaviour you want when you design your data model. +you the behaviour you want when you design your data model. To make it clear what the semantics of operations will be, list operations and object operations are named differently. (`li`, `ld`, `lm` for lists and `oi`, @@ -201,9 +206,9 @@ There is (unfortunately) no equivalent for list move with objects. ### Subtype operations Usage: - + {p:PATH, t:SUBTYPE, o:OPERATION} - + `PATH` is the path to the object that will be modified by the subtype. `SUBTYPE` is the name of the subtype, e.g. `"text0"`. `OPERATION` is the subtype operation itself. @@ -242,7 +247,7 @@ the subtype operation. Usage: {p:PATH, t:'text0', o:[{p:OFFSET, i:TEXT}]} - + Insert `TEXT` to the string specified by `PATH` at the position specified by `OFFSET`. ##### Delete from a string @@ -250,7 +255,7 @@ Insert `TEXT` to the string specified by `PATH` at the position specified by `OF Usage: {p:PATH, t:'text0', o:[{p:OFFSET, d:TEXT}]} - + Delete `TEXT` in the string specified by `PATH` at the position specified by `OFFSET`. --- diff --git a/lib/json0.js b/lib/json0.js index dc3a405..db2858d 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -226,6 +226,10 @@ json.apply = function(snapshot, op) { delete elem[key]; } + else if (c.e !== void 0) { + // no-op change to data + } + else { throw new Error('invalid / missing instruction in op'); } @@ -651,6 +655,77 @@ json.transformComponent = function(dest, c, otherC, type) { return dest; }; +var transformPosition = function(cursor, op, isOwnOp) { + var cursor = clone(cursor) + + var opIsAncestor = (cursor.length >= op.p.length) // true also if op is self + var opIsSibling = (cursor.length === op.p.length) // true also if op is self + var opIsAncestorSibling = (cursor.length >= op.p.length) // true also if op is self or sibling of self + var equalUpTo = -1 + for (var i = 0; i < op.p.length; i++) { + if (op.p[i] !== cursor[i]) { + opIsAncestor = false + if (i < op.p.length-1) { + opIsSibling = false + opIsAncestorSibling = false + } + } + if (equalUpTo === i-1 && op.p[i] === cursor[i]) { + equalUpTo += 1 + } + } + + if (opIsSibling) { + if (op.sd) { + cursor[cursor.length-1] = text.transformCursor(cursor[cursor.length-1], [{p: op.p[op.p.length-1], d: op.sd}], isOwnOp ? 'right' : 'left') + } + if (op.si) { + cursor[cursor.length-1] = text.transformCursor(cursor[cursor.length-1], [{p: op.p[op.p.length-1], i: op.si}], isOwnOp ? 'right': 'left') + } + } + + if (opIsAncestor) { + if (op.lm !== undefined) { + cursor[equalUpTo] = op.lm + } + if (op.od && op.oi) { + cursor = op.p.slice(0, op.p.length) + } else if (op.od) { + cursor = op.p.slice(0,op.p.length-1) + } else if (op.ld && op.li) { + cursor = op.p.slice(0, op.p.length) + } else if (op.ld) { + cursor = op.p.slice(0, op.p.length-1) + } + } + + if (opIsAncestorSibling) { + var lastPathIdx = op.p.length-1 + if (!opIsAncestor && op.ld && !op.li && op.p[lastPathIdx] < cursor[lastPathIdx]) { + cursor[lastPathIdx] -= 1 + } else if (!op.ld && op.li && op.p[lastPathIdx] <= cursor[lastPathIdx]) { + cursor[lastPathIdx] += 1 + } + + // if move item in list from after to before + if (!opIsAncestor && op.lm !== undefined && op.p[lastPathIdx] > cursor[lastPathIdx] && op.lm <= cursor[lastPathIdx]) { + cursor[lastPathIdx] += 1 + // if move item in list from before to after + } else if (!opIsAncestor && op.lm !== undefined && op.p[lastPathIdx] < cursor[lastPathIdx] && op.lm >= cursor[lastPathIdx]) { + cursor[lastPathIdx] -= 1 + } + } + + return cursor +} + +json.transformCursor = function(cursor, op, isOwnOp) { + for (var i = 0; i < op.length; i++) { + cursor = transformPosition(cursor, op[i], isOwnOp); + } + return cursor; +} + require('./bootstrapTransform')(json, json.transformComponent, json.checkValidOp, json.append); /** diff --git a/package.json b/package.json index 0850266..882b8d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "ot-json0", - "version": "1.0.1", + "name": "json00", + "version": "2.0.0", "description": "JSON OT type", "main": "lib/index.js", "directories": { @@ -25,10 +25,13 @@ "sharejs", "operational-transformation" ], - "author": "Joseph Gentle ", + "contributors": [ + "Joseph Gentle ", + "Robert Lord " + ], "license": "ISC", "bugs": { - "url": "https://github.com/ottypes/json0/issues" + "url": "https://github.com/wheatco/json00/issues" }, - "homepage": "https://github.com/ottypes/json0" + "homepage": "https://github.com/wheatco/json00" } diff --git a/test/json0.coffee b/test/json0.coffee index 531f76e..319f913 100644 --- a/test/json0.coffee +++ b/test/json0.coffee @@ -389,6 +389,67 @@ genTests = (type) -> assert.deepEqual [], type.transform [{p:['k'], od:'x'}], [{p:['k'], od:'x'}], 'left' assert.deepEqual [], type.transform [{p:['k'], od:'x'}], [{p:['k'], od:'x'}], 'right' + describe 'transformCursor', -> + describe 'string operations', -> + it 'handles inserts before', -> + assert.deepEqual ['key', 10, 3+4], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 1], si: 'meow'}]) + it 'handles inserts after', -> + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 5], si: 'meow'}]) + it 'handles inserts at current point with isOwnOp', -> + assert.deepEqual ['key', 10, 3+4], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 3], si: 'meow'}], true) + it 'handles inserts at current point without isOwnOp', -> + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 3], si: 'meow'}]) + it 'handles deletes before', -> + assert.deepEqual ['key', 10, 3-2], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 0], sd: '12'}]) + it 'handles deletes after', -> + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 3], sd: '12'}]) + it 'handles deletes at current point', -> + assert.deepEqual ['key', 10, 1], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 1], sd: 'meow meow'}]) + it 'ignores irrelevant operations', -> + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 9, 1], si: 'meow'}]) + describe 'number operations', -> + it 'ignores', -> + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 3], na: 123}]) + describe 'list operations', -> + it 'handles inserts before', -> + assert.deepEqual ['key', 10, 3+1], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 3], li: 'meow'}]) + assert.deepEqual ['key', 10+1, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10], li: 'meow'}]) + it 'handles inserts after', -> + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 4], li: 'meow'}]) + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 11], li: 'meow'}]) + it 'handles replacements at current point', -> + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 3], ld: 'meow1', li: 'meow2'}]) + assert.deepEqual ['key', 10], type.transformCursor(['key', 10, 3], [{p: ['key', 10], ld: 'meow1', li: 'meow2'}]) # move cursor up tree when parent deleted + it 'handles deletes before', -> + assert.deepEqual ['key', 10, 3-1], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 2], ld: 'meow'}]) + assert.deepEqual ['key', 10-1, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 9], ld: 'meow'}]) + it 'handles deletes after', -> + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 4], ld: 'meow'}]) + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 11], ld: 'meow'}]) + it 'handles deletes at current point', -> + assert.deepEqual ['key', 10], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 3], ld: 'meow'}]) + assert.deepEqual ['key'], type.transformCursor(['key', 10, 3], [{p: ['key', 10], ld: 'meow'}]) + it 'handles movements of current point', -> + assert.deepEqual ['key', 10, 20], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 3], lm: 20}]) + assert.deepEqual ['key', 20, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10], lm: 20}]) + it 'handles movements of other points', -> + assert.deepEqual ['key', 10, 2], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 1], lm: 20}]) + assert.deepEqual ['key', 10, 4], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 5], lm: 3}]) + assert.deepEqual ['key', 10, 4], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 5], lm: 1}]) + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 10], lm: 20}]) + assert.deepEqual ['key', 10, 2], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 2], lm: 3}]) + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key', 10, 2], lm: 1}]) + describe 'dict operations', -> + it 'ignores irrelevant inserts and deletes', -> + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key2'], oi: 'meow'}]) + assert.deepEqual ['key', 10, 3], type.transformCursor(['key', 10, 3], [{p: ['key2'], od: 'meow'}]) + it 'handles deletes at current point', -> + assert.deepEqual [], type.transformCursor(['key', 0, 3], [{p: ['key'], od: ['meow123']}]) + assert.deepEqual ['key', 0], type.transformCursor(['key', 0, 'key2'], [{p: ['key', 0, 'key2'], od: ['meow123']}]) + it 'handles replacements at current point', -> + assert.deepEqual ['key'], type.transformCursor(['key', 0, 3], [{p: ['key'], od: ['meow123'], oi: 'newobj'}]) + assert.deepEqual ['key', 0, 'key2'], type.transformCursor(['key', 0, 'key2'], [{p: ['key', 0, 'key2'], od: ['meow123'], oi: 'newobj'}]) + describe 'randomizer', -> @timeout 20000 @slow 6000