Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interesting Cursor Transform Work by Robert Lord #27

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# JSON0 OT Type
<p align="center">
<img src="https://raw.githubusercontent.com/lord/img/master/logo-json00.png" alt="JSON 00: Operational Transform Type" width="226">
<br>
<a href="https://travis-ci.org/wheatco/json00"><img src="https://travis-ci.org/wheatco/json00.svg?branch=master" alt="Build Status"></a>
</p>

The JSON OT type can be used to edit arbitrary JSON documents.

Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -242,15 +247,15 @@ 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

Usage:

{p:PATH, t:'text0', o:[{p:OFFSET, d:TEXT}]}

Delete `TEXT` in the string specified by `PATH` at the position specified by `OFFSET`.

---
Expand Down
75 changes: 75 additions & 0 deletions lib/json0.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down Expand Up @@ -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);

/**
Expand Down
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -25,10 +25,13 @@
"sharejs",
"operational-transformation"
],
"author": "Joseph Gentle <[email protected]>",
"contributors": [
"Joseph Gentle <[email protected]>",
"Robert Lord <[email protected]>"
],
"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"
}
61 changes: 61 additions & 0 deletions test/json0.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down