├── .gitignore ├── .mocharc.yml ├── .travis.yml ├── README.md ├── lib ├── bootstrapTransform.js ├── index.js ├── json0.js └── text0.js ├── package.json └── test ├── json0-generator.coffee ├── json0.coffee ├── text0-generator.coffee └── text0.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.DS_Store 3 | node_modules 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | reporter: spec 2 | check-leaks: true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 8 5 | - 10 6 | - 12 7 | - 14 8 | - 16 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON0 OT Type 2 | 3 | The JSON OT type can be used to edit arbitrary JSON documents. 4 | 5 | ## Features 6 | 7 | The JSON OT type supports the following operations: 8 | 9 | - Insert/delete/move/replace items in a list, shuffling adjacent list items as needed 10 | - Object insert/delete/replace 11 | - Atomic numerical add operation 12 | - Embed arbitrary subtypes 13 | - Embedded string editing, using the old text0 OT type as a subtype 14 | 15 | JSON0 is an *invertable* type - which is to say, all operations have an inverse 16 | operation which will undo the original op. As such, all operations which delete 17 | content have the content to be deleted inline in the operation. 18 | 19 | But its not perfect - here's a list of things it *cannot* do: 20 | 21 | - Object-move 22 | - Set if null (object insert with first writer wins semantics) 23 | - Efficient list insert-of-many-items 24 | 25 | It also has O(a * b) complexity when transforming large operations by one 26 | another (as opposed to O(a + b) which better algorithms can manage). 27 | 28 | 29 | ## Operations 30 | 31 | JSON operations are lists of operation components. The operation is a grouping 32 | of these components, applied in order. 33 | 34 | Each operation component is an object with a `p:PATH` component. The path is a 35 | list of keys to reach the target element in the document. For example, given 36 | the following document: 37 | 38 | ``` 39 | {'a':[100, 200, 300], 'b': 'hi'} 40 | ``` 41 | 42 | An operation to delete the first array element (`100`) would be the following: 43 | 44 | ``` 45 | [{p:['a', 0], ld:100}] 46 | ``` 47 | 48 | The path (`['a', 0]`) describes how to reach the target element from the root. 49 | The first element is a key in the containing object and the second is an index 50 | into the array. 51 | 52 | ### Summary of operations 53 | 54 | op | Description 55 | ---------------------------------------|------------------------------------- 56 | `{p:[path], na:x}` | adds `x` to the number at `[path]`. 57 | `{p:[path,idx], li:obj}` | inserts the object `obj` before the item at `idx` in the list at `[path]`. 58 | `{p:[path,idx], ld:obj}` | deletes the object `obj` from the index `idx` in the list at `[path]`. 59 | `{p:[path,idx], ld:before, li:after}` | replaces the object `before` at the index `idx` in the list at `[path]` with the object `after`. 60 | `{p:[path,idx1], lm:idx2}` | moves the object at `idx1` such that the object will be at index `idx2` in the list at `[path]`. 61 | `{p:[path,key], oi:obj}` | inserts the object `obj` into the object at `[path]` with key `key`. 62 | `{p:[path,key], od:obj}` | deletes the object `obj` with key `key` from the object at `[path]`. 63 | `{p:[path,key], od:before, oi:after}` | replaces the object `before` with the object `after` at key `key` in the object at `[path]`. 64 | `{p:[path], t:subtype, o:subtypeOp}` | applies the subtype op `o` of type `t` to the object at `[path]` 65 | `{p:[path,offset], si:s}` | inserts the string `s` at offset `offset` into the string at `[path]` (uses subtypes internally). 66 | `{p:[path,offset], sd:s}` | deletes the string `s` at offset `offset` from the string at `[path]` (uses subtypes internally). 67 | 68 | --- 69 | 70 | ### Number operations 71 | 72 | The only operation you can perform on a number is to add to it. Remember, you 73 | can always replace the number with another number by operating on the number's 74 | container. 75 | 76 | > Are there any other ways the format should support modifying numbers? Ideas: 77 | > 78 | > - Linear multiple as well (Ie, `x = Bx + C`) 79 | > - MAX, MIN, etc? That would let you do timestamps... 80 | > 81 | > I can't think of any good use cases for those operations... 82 | 83 | #### Add 84 | 85 | Usage: 86 | 87 | {p:PATH, na:X} 88 | 89 | Adds X to the number at PATH. If you want to subtract, add a negative number. 90 | 91 | --- 92 | 93 | ### Lists and Objects 94 | 95 | Lists and objects have the same set of operations (*Insert*, *Delete*, 96 | *Replace*, *Move*) but their semantics are very different. List operations 97 | shuffle adjacent list items left or right to make space (or to remove space). 98 | Object operations do not. You should pick the data structure which will give 99 | you the behaviour you want when you design your data model. 100 | 101 | To make it clear what the semantics of operations will be, list operations and 102 | object operations are named differently. (`li`, `ld`, `lm` for lists and `oi`, 103 | `od` and `om` for objects). 104 | 105 | #### Inserting, Deleting and Replacing in a list 106 | 107 | Usage: 108 | 109 | - **Insert**: `{p:PATH, li:NEWVALUE}` 110 | - **Delete**: `{p:PATH, ld:OLDVALUE}` 111 | - **Replace**: `{p:PATH, ld:OLDVALUE, li:NEWVALUE}` 112 | 113 | Inserts, deletes, or replaces the element at `PATH`. 114 | 115 | The last element in the path specifies an index in the list where elements will 116 | be deleted, inserted or replaced. The index must be valid (0 <= *new index* <= 117 | *list length*). The indexes of existing list elements may change when new 118 | list elements are added or removed. 119 | 120 | The replace operation: 121 | 122 | {p:PATH, ld:OLDVALUE, li:NEWVALUE} 123 | 124 | is equivalent to a delete followed by an insert: 125 | 126 | {p:PATH, ld:OLDVALUE} 127 | {p:PATH, li:NEWVALUE} 128 | 129 | Given the following list: 130 | 131 | [100, 300, 400] 132 | 133 | applying the following operation: 134 | 135 | [{p:[1], li:{'yo':'hi there'}}, {p:[3], ld:400}] 136 | 137 | would result in the following new list: 138 | 139 | [100, {'yo':'hi there'}, 300] 140 | 141 | 142 | #### Moving list elements 143 | 144 | You can move list items by deleting them and & inserting them back elsewhere, 145 | but if you do that concurrent operations on the deleted element will be lost. 146 | To fix this, the JSON OT type has a special list move operation. 147 | 148 | Usage: 149 | 150 | {p:PATH, lm:NEWINDEX} 151 | 152 | Moves the list element specified by `PATH` to a different place in the list, 153 | with index `NEWINDEX`. Any elements between the old index and the new index 154 | will get new indicies, as appropriate. 155 | 156 | The new index must be 0 <= _index_ < _list length_. The new index will be 157 | interpreted __after__ the element has been removed from its current position. 158 | Given the following data: 159 | 160 | ['a', 'b', 'c'] 161 | 162 | the following operation: 163 | 164 | [{p:[1], lm:2}] 165 | 166 | will result in the following data: 167 | 168 | ['a', 'c', 'b'] 169 | 170 | 171 | #### Inserting, Deleting and Replacing in an object 172 | 173 | Usage: 174 | 175 | - **Insert**: `{p:PATH, oi:NEWVALUE}` 176 | - **Delete**: `{p:PATH, od:OLDVALUE}` 177 | - **Replace**: `{p:PATH, od:OLDVALUE, oi:NEWVALUE}` 178 | 179 | Set the element indicated by `PATH` from `OLDVALUE` to `NEWVALUE`. The last 180 | element of the path must be the key of the element to be inserted, deleted or 181 | replaced. 182 | 183 | When inserting, the key must not already be used. When deleting or replacing a 184 | value, `OLDVALUE` must be equal to the current value the object has at the 185 | specified key. 186 | 187 | As with lists, the replace operation: 188 | 189 | {p:PATH, od:OLDVALUE, oi:NEWVALUE} 190 | 191 | is equivalent to a delete followed by an insert: 192 | 193 | {p:PATH, od:OLDVALUE} 194 | {p:PATH, oi:NEWVALUE} 195 | 196 | There is (unfortunately) no equivalent for list move with objects. 197 | 198 | 199 | --- 200 | 201 | ### Subtype operations 202 | 203 | Usage: 204 | 205 | {p:PATH, t:SUBTYPE, o:OPERATION} 206 | 207 | `PATH` is the path to the object that will be modified by the subtype. 208 | `SUBTYPE` is the name of the subtype, e.g. `"text0"`. 209 | `OPERATION` is the subtype operation itself. 210 | 211 | To register a subtype, call `json0.registerSubtype` with another OT type. 212 | Specifically, a subtype is a JavaScript object with the following methods: 213 | 214 | * `apply` 215 | * `transform` 216 | * `compose` 217 | * `invert` 218 | 219 | See the [OT types documentation](https://github.com/ottypes/docs) for details on these methods. 220 | 221 | #### Text subtype 222 | 223 | The old string operations are still supported (see below) but are now implemented internally as a subtype 224 | using the `text0` type. You can either continue to use the original `si` and `sd` ops documented below, 225 | or use the `text0` type as a subtype yourself. 226 | 227 | To edit a string, create a `text0` subtype op. For example, given the 228 | following object: 229 | 230 | {'key':[100,'abcde']} 231 | 232 | If you wanted to delete the `'d'` from the string `'abcde'`, you would use the following operation: 233 | 234 | [{p:['key',1], t: 'text0', o:[{p:3, d:'d'}]} 235 | 236 | Note the path. The components, in order, are the key to the list, and the index to 237 | the `'abcde'` string. The offset to the `'d'` character in the string is given in 238 | the subtype operation. 239 | 240 | ##### Insert into a string 241 | 242 | Usage: 243 | 244 | {p:PATH, t:'text0', o:[{p:OFFSET, i:TEXT}]} 245 | 246 | Insert `TEXT` to the string specified by `PATH` at the position specified by `OFFSET`. 247 | 248 | ##### Delete from a string 249 | 250 | Usage: 251 | 252 | {p:PATH, t:'text0', o:[{p:OFFSET, d:TEXT}]} 253 | 254 | Delete `TEXT` in the string specified by `PATH` at the position specified by `OFFSET`. 255 | 256 | --- 257 | 258 | ### String operations 259 | 260 | These operations are now internally implemented as subtype operations using the `text0` type, but you can still use them if you like. See above. 261 | 262 | If the content at a path is a string, an operation can edit the string 263 | in-place, either deleting characters or inserting characters. 264 | 265 | To edit a string, add the string offset to the path. For example, given the 266 | following object: 267 | 268 | {'key':[100,'abcde']} 269 | 270 | If you wanted to delete the `'d'` from the string `'abcde'`, you would use the following operation: 271 | 272 | [{p:['key',1,3],sd:'d'}] 273 | 274 | Note the path. The components, in order, are the key to the list, the index to 275 | the `'abcde'` string, and then the offset to the `'d'` character in the string. 276 | 277 | #### Insert into a string 278 | 279 | Usage: 280 | 281 | {p:PATH, si:TEXT} 282 | 283 | Insert `TEXT` at the location specified by `PATH`. The path must specify an 284 | offset in a string. 285 | 286 | #### Delete from a string 287 | 288 | Usage: 289 | 290 | {p:PATH, sd:TEXT} 291 | 292 | Delete `TEXT` at the location specified by `PATH`. The path must specify an 293 | offset in a string. `TEXT` must be contained at the location specified. 294 | 295 | --- 296 | 297 | # Commentary 298 | 299 | This library was written a couple of years ago by [Jeremy Apthorp](https://github.com/nornagon). It was 300 | originally written in coffeescript as part of ShareJS, and then it got pulled 301 | out into the share/ottypes library and its finally landed here. 302 | 303 | The type uses the list-of-op-components model, where each operation makes a 304 | series of individual changes to a document. Joseph now thinks this is a 305 | terrible idea because it doesn't scale well to large operations - it has 306 | N2 instead of 2N complexity. 307 | 308 | Jeremy and Joseph have talked about rewriting this library to instead make each 309 | operation be a sparse traversal of the document. But it was obnoxiously 310 | difficult to implement JSON OT correctly in the first place - it'll probably 311 | take both of us thinking about nothing else for a few weeks to make that 312 | happen. 313 | 314 | When it was written, the embedded text0 type was sharejs's text type. Its since 315 | been rewritten to make each operation be a traversal, but the JSON OT type 316 | still embeds the old type. As such, that old text type is included in this 317 | repository. If you want to use text0 in your own project, I'd be very happy to 318 | pull it out of here and make it its own module. However, I recommend that you 319 | just use the new text type. Its simpler and faster. 320 | 321 | --- 322 | 323 | # License 324 | 325 | All code contributed to this repository is licensed under the standard MIT license: 326 | 327 | Copyright 2011 ottypes library contributors 328 | 329 | Permission is hereby granted, free of charge, to any person obtaining a copy 330 | of this software and associated documentation files (the "Software"), to deal 331 | in the Software without restriction, including without limitation the rights 332 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 333 | copies of the Software, and to permit persons to whom the Software is 334 | furnished to do so, subject to the following condition: 335 | 336 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 337 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 338 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 339 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 340 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 341 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 342 | THE SOFTWARE. 343 | 344 | 345 | -------------------------------------------------------------------------------- /lib/bootstrapTransform.js: -------------------------------------------------------------------------------- 1 | // These methods let you build a transform function from a transformComponent 2 | // function for OT types like JSON0 in which operations are lists of components 3 | // and transforming them requires N^2 work. I find it kind of nasty that I need 4 | // this, but I'm not really sure what a better solution is. Maybe I should do 5 | // this automatically to types that don't have a compose function defined. 6 | 7 | // Add transform and transformX functions for an OT type which has 8 | // transformComponent defined. transformComponent(destination array, 9 | // component, other component, side) 10 | module.exports = bootstrapTransform 11 | function bootstrapTransform(type, transformComponent, checkValidOp, append) { 12 | var transformComponentX = function(left, right, destLeft, destRight) { 13 | transformComponent(destLeft, left, right, 'left'); 14 | transformComponent(destRight, right, left, 'right'); 15 | }; 16 | 17 | var transformX = type.transformX = function(leftOp, rightOp) { 18 | checkValidOp(leftOp); 19 | checkValidOp(rightOp); 20 | var newRightOp = []; 21 | 22 | for (var i = 0; i < rightOp.length; i++) { 23 | var rightComponent = rightOp[i]; 24 | 25 | // Generate newLeftOp by composing leftOp by rightComponent 26 | var newLeftOp = []; 27 | var k = 0; 28 | while (k < leftOp.length) { 29 | var nextC = []; 30 | transformComponentX(leftOp[k], rightComponent, newLeftOp, nextC); 31 | k++; 32 | 33 | if (nextC.length === 1) { 34 | rightComponent = nextC[0]; 35 | } else if (nextC.length === 0) { 36 | for (var j = k; j < leftOp.length; j++) { 37 | append(newLeftOp, leftOp[j]); 38 | } 39 | rightComponent = null; 40 | break; 41 | } else { 42 | // Recurse. 43 | var pair = transformX(leftOp.slice(k), nextC); 44 | for (var l = 0; l < pair[0].length; l++) { 45 | append(newLeftOp, pair[0][l]); 46 | } 47 | for (var r = 0; r < pair[1].length; r++) { 48 | append(newRightOp, pair[1][r]); 49 | } 50 | rightComponent = null; 51 | break; 52 | } 53 | } 54 | 55 | if (rightComponent != null) { 56 | append(newRightOp, rightComponent); 57 | } 58 | leftOp = newLeftOp; 59 | } 60 | return [leftOp, newRightOp]; 61 | }; 62 | 63 | // Transforms op with specified type ('left' or 'right') by otherOp. 64 | type.transform = function(op, otherOp, type) { 65 | if (!(type === 'left' || type === 'right')) 66 | throw new Error("type must be 'left' or 'right'"); 67 | 68 | if (otherOp.length === 0) return op; 69 | 70 | if (op.length === 1 && otherOp.length === 1) 71 | return transformComponent([], op[0], otherOp[0], type); 72 | 73 | if (type === 'left') 74 | return transformX(op, otherOp)[0]; 75 | else 76 | return transformX(otherOp, op)[1]; 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Only the JSON type is exported, because the text type is deprecated 2 | // otherwise. (If you want to use it somewhere, you're welcome to pull it out 3 | // into a separate module that json0 can depend on). 4 | 5 | module.exports = { 6 | type: require('./json0') 7 | }; 8 | -------------------------------------------------------------------------------- /lib/json0.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is the implementation of the JSON OT type. 3 | 4 | Spec is here: https://github.com/josephg/ShareJS/wiki/JSON-Operations 5 | 6 | Note: This is being made obsolete. It will soon be replaced by the JSON2 type. 7 | */ 8 | 9 | /** 10 | * UTILITY FUNCTIONS 11 | */ 12 | 13 | /** 14 | * Checks if the passed object is an Array instance. Can't use Array.isArray 15 | * yet because its not supported on IE8. 16 | * 17 | * @param obj 18 | * @returns {boolean} 19 | */ 20 | var isArray = function(obj) { 21 | return Object.prototype.toString.call(obj) == '[object Array]'; 22 | }; 23 | 24 | /** 25 | * Checks if the passed object is an Object instance. 26 | * No function call (fast) version 27 | * 28 | * @param obj 29 | * @returns {boolean} 30 | */ 31 | var isObject = function(obj) { 32 | return (!!obj) && (obj.constructor === Object); 33 | }; 34 | 35 | /** 36 | * Clones the passed object using JSON serialization (which is slow). 37 | * 38 | * hax, copied from test/types/json. Apparently this is still the fastest way 39 | * to deep clone an object, assuming we have browser support for JSON. @see 40 | * http://jsperf.com/cloning-an-object/12 41 | */ 42 | var clone = function(o) { 43 | return JSON.parse(JSON.stringify(o)); 44 | }; 45 | 46 | /** 47 | * JSON OT Type 48 | * @type {*} 49 | */ 50 | var json = { 51 | name: 'json0', 52 | uri: 'http://sharejs.org/types/JSONv0' 53 | }; 54 | 55 | // You can register another OT type as a subtype in a JSON document using 56 | // the following function. This allows another type to handle certain 57 | // operations instead of the builtin JSON type. 58 | var subtypes = {}; 59 | json.registerSubtype = function(subtype) { 60 | subtypes[subtype.name] = subtype; 61 | }; 62 | 63 | json.create = function(data) { 64 | // Null instead of undefined if you don't pass an argument. 65 | return data === undefined ? null : clone(data); 66 | }; 67 | 68 | json.invertComponent = function(c) { 69 | var c_ = {p: c.p}; 70 | 71 | // handle subtype ops 72 | if (c.t && subtypes[c.t]) { 73 | c_.t = c.t; 74 | c_.o = subtypes[c.t].invert(c.o); 75 | } 76 | 77 | if (c.si !== void 0) c_.sd = c.si; 78 | if (c.sd !== void 0) c_.si = c.sd; 79 | if (c.oi !== void 0) c_.od = c.oi; 80 | if (c.od !== void 0) c_.oi = c.od; 81 | if (c.li !== void 0) c_.ld = c.li; 82 | if (c.ld !== void 0) c_.li = c.ld; 83 | if (c.na !== void 0) c_.na = -c.na; 84 | 85 | if (c.lm !== void 0) { 86 | c_.lm = c.p[c.p.length-1]; 87 | c_.p = c.p.slice(0,c.p.length-1).concat([c.lm]); 88 | } 89 | 90 | return c_; 91 | }; 92 | 93 | json.invert = function(op) { 94 | var op_ = op.slice().reverse(); 95 | var iop = []; 96 | for (var i = 0; i < op_.length; i++) { 97 | iop.push(json.invertComponent(op_[i])); 98 | } 99 | return iop; 100 | }; 101 | 102 | json.checkValidOp = function(op) { 103 | for (var i = 0; i < op.length; i++) { 104 | if (!isArray(op[i].p)) throw new Error('Missing path'); 105 | } 106 | }; 107 | 108 | json.checkList = function(elem) { 109 | if (!isArray(elem)) 110 | throw new Error('Referenced element not a list'); 111 | }; 112 | 113 | json.checkObj = function(elem) { 114 | if (!isObject(elem)) { 115 | throw new Error("Referenced element not an object (it was " + JSON.stringify(elem) + ")"); 116 | } 117 | }; 118 | 119 | // helper functions to convert old string ops to and from subtype ops 120 | function convertFromText(c) { 121 | c.t = 'text0'; 122 | var o = {p: c.p.pop()}; 123 | if (c.si != null) o.i = c.si; 124 | if (c.sd != null) o.d = c.sd; 125 | c.o = [o]; 126 | } 127 | 128 | function convertToText(c) { 129 | c.p.push(c.o[0].p); 130 | if (c.o[0].i != null) c.si = c.o[0].i; 131 | if (c.o[0].d != null) c.sd = c.o[0].d; 132 | delete c.t; 133 | delete c.o; 134 | } 135 | 136 | json.apply = function(snapshot, op) { 137 | json.checkValidOp(op); 138 | 139 | op = clone(op); 140 | 141 | var container = { 142 | data: snapshot 143 | }; 144 | 145 | for (var i = 0; i < op.length; i++) { 146 | var c = op[i]; 147 | 148 | // convert old string ops to use subtype for backwards compatibility 149 | if (c.si != null || c.sd != null) 150 | convertFromText(c); 151 | 152 | var parent = null; 153 | var parentKey = null; 154 | var elem = container; 155 | var key = 'data'; 156 | 157 | for (var j = 0; j < c.p.length; j++) { 158 | var p = c.p[j]; 159 | 160 | parent = elem; 161 | parentKey = key; 162 | elem = elem[key]; 163 | key = p; 164 | 165 | if (isArray(elem) && typeof key !== 'number') 166 | throw new Error('List index must be a number'); 167 | 168 | if (isObject(elem) && typeof key !== 'string') 169 | throw new Error('Object key must be a string'); 170 | 171 | if (parent == null) 172 | throw new Error('Path invalid'); 173 | } 174 | 175 | // handle subtype ops 176 | if (c.t && c.o !== void 0 && subtypes[c.t]) { 177 | elem[key] = subtypes[c.t].apply(elem[key], c.o); 178 | 179 | // Number add 180 | } else if (c.na !== void 0) { 181 | if (typeof elem[key] != 'number') 182 | throw new Error('Referenced element not a number'); 183 | 184 | if (typeof c.na !== 'number') 185 | throw new Error('Number addition is not a number'); 186 | 187 | elem[key] += c.na; 188 | } 189 | 190 | // List replace 191 | else if (c.li !== void 0 && c.ld !== void 0) { 192 | json.checkList(elem); 193 | // Should check the list element matches c.ld 194 | elem[key] = c.li; 195 | } 196 | 197 | // List insert 198 | else if (c.li !== void 0) { 199 | json.checkList(elem); 200 | elem.splice(key,0, c.li); 201 | } 202 | 203 | // List delete 204 | else if (c.ld !== void 0) { 205 | json.checkList(elem); 206 | // Should check the list element matches c.ld here too. 207 | elem.splice(key,1); 208 | } 209 | 210 | // List move 211 | else if (c.lm !== void 0) { 212 | if (typeof c.lm !== 'number') 213 | throw new Error('List move target index must be a number'); 214 | 215 | json.checkList(elem); 216 | if (c.lm != key) { 217 | var e = elem[key]; 218 | // Remove it... 219 | elem.splice(key,1); 220 | // And insert it back. 221 | elem.splice(c.lm,0,e); 222 | } 223 | } 224 | 225 | // Object insert / replace 226 | else if (c.oi !== void 0) { 227 | json.checkObj(elem); 228 | 229 | // Should check that elem[key] == c.od 230 | elem[key] = c.oi; 231 | } 232 | 233 | // Object delete 234 | else if (c.od !== void 0) { 235 | json.checkObj(elem); 236 | 237 | // Should check that elem[key] == c.od 238 | delete elem[key]; 239 | } 240 | 241 | else { 242 | throw new Error('invalid / missing instruction in op'); 243 | } 244 | } 245 | 246 | return container.data; 247 | }; 248 | 249 | // Helper to break an operation up into a bunch of small ops. 250 | json.shatter = function(op) { 251 | var results = []; 252 | for (var i = 0; i < op.length; i++) { 253 | results.push([op[i]]); 254 | } 255 | return results; 256 | }; 257 | 258 | // Helper for incrementally applying an operation to a snapshot. Calls yield 259 | // after each op component has been applied. 260 | json.incrementalApply = function(snapshot, op, _yield) { 261 | for (var i = 0; i < op.length; i++) { 262 | var smallOp = [op[i]]; 263 | snapshot = json.apply(snapshot, smallOp); 264 | // I'd just call this yield, but thats a reserved keyword. Bah! 265 | _yield(smallOp, snapshot); 266 | } 267 | 268 | return snapshot; 269 | }; 270 | 271 | // Checks if two paths, p1 and p2 match. 272 | var pathMatches = json.pathMatches = function(p1, p2, ignoreLast) { 273 | if (p1.length != p2.length) 274 | return false; 275 | 276 | for (var i = 0; i < p1.length; i++) { 277 | if (p1[i] !== p2[i] && (!ignoreLast || i !== p1.length - 1)) 278 | return false; 279 | } 280 | 281 | return true; 282 | }; 283 | 284 | json.append = function(dest,c) { 285 | c = clone(c); 286 | 287 | if (dest.length === 0) { 288 | dest.push(c); 289 | return; 290 | } 291 | 292 | var last = dest[dest.length - 1]; 293 | 294 | // convert old string ops to use subtype for backwards compatibility 295 | if ((c.si != null || c.sd != null) && (last.si != null || last.sd != null)) { 296 | convertFromText(c); 297 | convertFromText(last); 298 | } 299 | 300 | if (pathMatches(c.p, last.p)) { 301 | // handle subtype ops 302 | if (c.t && last.t && c.t === last.t && subtypes[c.t]) { 303 | last.o = subtypes[c.t].compose(last.o, c.o); 304 | 305 | // convert back to old string ops 306 | if (c.si != null || c.sd != null) { 307 | var p = c.p; 308 | for (var i = 0; i < last.o.length - 1; i++) { 309 | c.o = [last.o.pop()]; 310 | c.p = p.slice(); 311 | convertToText(c); 312 | dest.push(c); 313 | } 314 | 315 | convertToText(last); 316 | } 317 | } else if (last.na != null && c.na != null) { 318 | dest[dest.length - 1] = {p: last.p, na: last.na + c.na}; 319 | } else if (last.li !== undefined && c.li === undefined && c.ld === last.li) { 320 | // insert immediately followed by delete becomes a noop. 321 | if (last.ld !== undefined) { 322 | // leave the delete part of the replace 323 | delete last.li; 324 | } else { 325 | dest.pop(); 326 | } 327 | } else if (last.od !== undefined && last.oi === undefined && c.oi !== undefined && c.od === undefined) { 328 | last.oi = c.oi; 329 | } else if (last.oi !== undefined && c.od !== undefined) { 330 | // The last path component inserted something that the new component deletes (or replaces). 331 | // Just merge them. 332 | if (c.oi !== undefined) { 333 | last.oi = c.oi; 334 | } else if (last.od !== undefined) { 335 | delete last.oi; 336 | } else { 337 | // An insert directly followed by a delete turns into a no-op and can be removed. 338 | dest.pop(); 339 | } 340 | } else if (c.lm !== undefined && c.p[c.p.length - 1] === c.lm) { 341 | // don't do anything 342 | } else { 343 | dest.push(c); 344 | } 345 | } else { 346 | // convert string ops back 347 | if ((c.si != null || c.sd != null) && (last.si != null || last.sd != null)) { 348 | convertToText(c); 349 | convertToText(last); 350 | } 351 | 352 | dest.push(c); 353 | } 354 | }; 355 | 356 | json.compose = function(op1,op2) { 357 | json.checkValidOp(op1); 358 | json.checkValidOp(op2); 359 | 360 | var newOp = clone(op1); 361 | 362 | for (var i = 0; i < op2.length; i++) { 363 | json.append(newOp,op2[i]); 364 | } 365 | 366 | return newOp; 367 | }; 368 | 369 | json.normalize = function(op) { 370 | var newOp = []; 371 | 372 | op = isArray(op) ? op : [op]; 373 | 374 | for (var i = 0; i < op.length; i++) { 375 | var c = op[i]; 376 | if (c.p == null) c.p = []; 377 | 378 | json.append(newOp,c); 379 | } 380 | 381 | return newOp; 382 | }; 383 | 384 | // Returns the common length of the paths of ops a and b 385 | json.commonLengthForOps = function(a, b) { 386 | var alen = a.p.length; 387 | var blen = b.p.length; 388 | if (a.na != null || a.t) 389 | alen++; 390 | 391 | if (b.na != null || b.t) 392 | blen++; 393 | 394 | if (alen === 0) return -1; 395 | if (blen === 0) return null; 396 | 397 | alen--; 398 | blen--; 399 | 400 | for (var i = 0; i < alen; i++) { 401 | var p = a.p[i]; 402 | if (i >= blen || p !== b.p[i]) 403 | return null; 404 | } 405 | 406 | return alen; 407 | }; 408 | 409 | // Returns true if an op can affect the given path 410 | json.canOpAffectPath = function(op, path) { 411 | return json.commonLengthForOps({p:path}, op) != null; 412 | }; 413 | 414 | // transform c so it applies to a document with otherC applied. 415 | json.transformComponent = function(dest, c, otherC, type) { 416 | c = clone(c); 417 | 418 | var common = json.commonLengthForOps(otherC, c); 419 | var common2 = json.commonLengthForOps(c, otherC); 420 | var cplength = c.p.length; 421 | var otherCplength = otherC.p.length; 422 | 423 | if (c.na != null || c.t) 424 | cplength++; 425 | 426 | if (otherC.na != null || otherC.t) 427 | otherCplength++; 428 | 429 | // if c is deleting something, and that thing is changed by otherC, we need to 430 | // update c to reflect that change for invertibility. 431 | if (common2 != null && otherCplength > cplength && c.p[common2] == otherC.p[common2]) { 432 | if (c.ld !== void 0) { 433 | var oc = clone(otherC); 434 | oc.p = oc.p.slice(cplength); 435 | c.ld = json.apply(clone(c.ld),[oc]); 436 | } else if (c.od !== void 0) { 437 | var oc = clone(otherC); 438 | oc.p = oc.p.slice(cplength); 439 | c.od = json.apply(clone(c.od),[oc]); 440 | } 441 | } 442 | 443 | if (common != null) { 444 | var commonOperand = cplength == otherCplength; 445 | 446 | // backward compatibility for old string ops 447 | var oc = otherC; 448 | if ((c.si != null || c.sd != null) && (otherC.si != null || otherC.sd != null)) { 449 | convertFromText(c); 450 | oc = clone(otherC); 451 | convertFromText(oc); 452 | } 453 | 454 | // handle subtype ops 455 | if (oc.t && subtypes[oc.t]) { 456 | if (c.t && c.t === oc.t) { 457 | var res = subtypes[c.t].transform(c.o, oc.o, type); 458 | 459 | // convert back to old string ops 460 | if (c.si != null || c.sd != null) { 461 | var p = c.p; 462 | for (var i = 0; i < res.length; i++) { 463 | c.o = [res[i]]; 464 | c.p = p.slice(); 465 | convertToText(c); 466 | json.append(dest, c); 467 | } 468 | } else if (!isArray(res) || res.length > 0) { 469 | c.o = res; 470 | json.append(dest, c); 471 | } 472 | 473 | return dest; 474 | } 475 | } 476 | 477 | // transform based on otherC 478 | else if (otherC.na !== void 0) { 479 | // this case is handled below 480 | } else if (otherC.li !== void 0 && otherC.ld !== void 0) { 481 | if (otherC.p[common] === c.p[common]) { 482 | // noop 483 | 484 | if (!commonOperand) { 485 | return dest; 486 | } else if (c.ld !== void 0) { 487 | // we're trying to delete the same element, -> noop 488 | if (c.li !== void 0 && type === 'left') { 489 | // we're both replacing one element with another. only one can survive 490 | c.ld = clone(otherC.li); 491 | } else { 492 | return dest; 493 | } 494 | } 495 | } 496 | } else if (otherC.li !== void 0) { 497 | if (c.li !== void 0 && c.ld === undefined && commonOperand && c.p[common] === otherC.p[common]) { 498 | // in li vs. li, left wins. 499 | if (type === 'right') 500 | c.p[common]++; 501 | } else if (otherC.p[common] <= c.p[common]) { 502 | c.p[common]++; 503 | } 504 | 505 | if (c.lm !== void 0) { 506 | if (commonOperand) { 507 | // otherC edits the same list we edit 508 | if (otherC.p[common] <= c.lm) 509 | c.lm++; 510 | // changing c.from is handled above. 511 | } 512 | } 513 | } else if (otherC.ld !== void 0) { 514 | if (c.lm !== void 0) { 515 | if (commonOperand) { 516 | if (otherC.p[common] === c.p[common]) { 517 | // they deleted the thing we're trying to move 518 | return dest; 519 | } 520 | // otherC edits the same list we edit 521 | var p = otherC.p[common]; 522 | var from = c.p[common]; 523 | var to = c.lm; 524 | if (p < to || (p === to && from < to)) 525 | c.lm--; 526 | 527 | } 528 | } 529 | 530 | if (otherC.p[common] < c.p[common]) { 531 | c.p[common]--; 532 | } else if (otherC.p[common] === c.p[common]) { 533 | if (otherCplength < cplength) { 534 | // we're below the deleted element, so -> noop 535 | return dest; 536 | } else if (c.ld !== void 0) { 537 | if (c.li !== void 0) { 538 | // we're replacing, they're deleting. we become an insert. 539 | delete c.ld; 540 | } else { 541 | // we're trying to delete the same element, -> noop 542 | return dest; 543 | } 544 | } 545 | } 546 | 547 | } else if (otherC.lm !== void 0) { 548 | if (c.lm !== void 0 && cplength === otherCplength) { 549 | // lm vs lm, here we go! 550 | var from = c.p[common]; 551 | var to = c.lm; 552 | var otherFrom = otherC.p[common]; 553 | var otherTo = otherC.lm; 554 | if (otherFrom !== otherTo) { 555 | // if otherFrom == otherTo, we don't need to change our op. 556 | 557 | // where did my thing go? 558 | if (from === otherFrom) { 559 | // they moved it! tie break. 560 | if (type === 'left') { 561 | c.p[common] = otherTo; 562 | if (from === to) // ugh 563 | c.lm = otherTo; 564 | } else { 565 | return dest; 566 | } 567 | } else { 568 | // they moved around it 569 | if (from > otherFrom) c.p[common]--; 570 | if (from > otherTo) c.p[common]++; 571 | else if (from === otherTo) { 572 | if (otherFrom > otherTo) { 573 | c.p[common]++; 574 | if (from === to) // ugh, again 575 | c.lm++; 576 | } 577 | } 578 | 579 | // step 2: where am i going to put it? 580 | if (to > otherFrom) { 581 | c.lm--; 582 | } else if (to === otherFrom) { 583 | if (to > from) 584 | c.lm--; 585 | } 586 | if (to > otherTo) { 587 | c.lm++; 588 | } else if (to === otherTo) { 589 | // if we're both moving in the same direction, tie break 590 | if ((otherTo > otherFrom && to > from) || 591 | (otherTo < otherFrom && to < from)) { 592 | if (type === 'right') c.lm++; 593 | } else { 594 | if (to > from) c.lm++; 595 | else if (to === otherFrom) c.lm--; 596 | } 597 | } 598 | } 599 | } 600 | } else if (c.li !== void 0 && c.ld === undefined && commonOperand) { 601 | // li 602 | var from = otherC.p[common]; 603 | var to = otherC.lm; 604 | p = c.p[common]; 605 | if (p > from) c.p[common]--; 606 | if (p > to) c.p[common]++; 607 | } else { 608 | // ld, ld+li, si, sd, na, oi, od, oi+od, any li on an element beneath 609 | // the lm 610 | // 611 | // i.e. things care about where their item is after the move. 612 | var from = otherC.p[common]; 613 | var to = otherC.lm; 614 | p = c.p[common]; 615 | if (p === from) { 616 | c.p[common] = to; 617 | } else { 618 | if (p > from) c.p[common]--; 619 | if (p > to) c.p[common]++; 620 | else if (p === to && from > to) c.p[common]++; 621 | } 622 | } 623 | } 624 | else if (otherC.oi !== void 0 && otherC.od !== void 0) { 625 | if (c.p[common] === otherC.p[common]) { 626 | if (c.oi !== void 0 && commonOperand) { 627 | // we inserted where someone else replaced 628 | if (type === 'right') { 629 | // left wins 630 | return dest; 631 | } else { 632 | // we win, make our op replace what they inserted 633 | c.od = otherC.oi; 634 | } 635 | } else { 636 | // -> noop if the other component is deleting the same object (or any parent) 637 | return dest; 638 | } 639 | } 640 | } else if (otherC.oi !== void 0) { 641 | if (c.oi !== void 0 && c.p[common] === otherC.p[common]) { 642 | // left wins if we try to insert at the same place 643 | if (type === 'left') { 644 | json.append(dest,{p: c.p, od:otherC.oi}); 645 | } else { 646 | return dest; 647 | } 648 | } 649 | } else if (otherC.od !== void 0) { 650 | if (c.p[common] == otherC.p[common]) { 651 | if (!commonOperand) 652 | return dest; 653 | if (c.oi !== void 0) { 654 | delete c.od; 655 | } else { 656 | return dest; 657 | } 658 | } 659 | } 660 | } 661 | 662 | json.append(dest,c); 663 | return dest; 664 | }; 665 | 666 | require('./bootstrapTransform')(json, json.transformComponent, json.checkValidOp, json.append); 667 | 668 | /** 669 | * Register a subtype for string operations, using the text0 type. 670 | */ 671 | var text = require('./text0'); 672 | 673 | json.registerSubtype(text); 674 | module.exports = json; 675 | 676 | -------------------------------------------------------------------------------- /lib/text0.js: -------------------------------------------------------------------------------- 1 | // DEPRECATED! 2 | // 3 | // This type works, but is not exported. Its included here because the JSON0 4 | // embedded string operations use this library. 5 | 6 | 7 | // A simple text implementation 8 | // 9 | // Operations are lists of components. Each component either inserts or deletes 10 | // at a specified position in the document. 11 | // 12 | // Components are either: 13 | // {i:'str', p:100}: Insert 'str' at position 100 in the document 14 | // {d:'str', p:100}: Delete 'str' at position 100 in the document 15 | // 16 | // Components in an operation are executed sequentially, so the position of components 17 | // assumes previous components have already executed. 18 | // 19 | // Eg: This op: 20 | // [{i:'abc', p:0}] 21 | // is equivalent to this op: 22 | // [{i:'a', p:0}, {i:'b', p:1}, {i:'c', p:2}] 23 | 24 | var text = module.exports = { 25 | name: 'text0', 26 | uri: 'http://sharejs.org/types/textv0', 27 | create: function(initial) { 28 | if ((initial != null) && typeof initial !== 'string') { 29 | throw new Error('Initial data must be a string'); 30 | } 31 | return initial || ''; 32 | } 33 | }; 34 | 35 | /** Insert s2 into s1 at pos. */ 36 | var strInject = function(s1, pos, s2) { 37 | return s1.slice(0, pos) + s2 + s1.slice(pos); 38 | }; 39 | 40 | /** Check that an operation component is valid. Throws if its invalid. */ 41 | var checkValidComponent = function(c) { 42 | if (typeof c.p !== 'number') 43 | throw new Error('component missing position field'); 44 | 45 | if ((typeof c.i === 'string') === (typeof c.d === 'string')) 46 | throw new Error('component needs an i or d field'); 47 | 48 | if (c.p < 0) 49 | throw new Error('position cannot be negative'); 50 | }; 51 | 52 | /** Check that an operation is valid */ 53 | var checkValidOp = function(op) { 54 | for (var i = 0; i < op.length; i++) { 55 | checkValidComponent(op[i]); 56 | } 57 | }; 58 | 59 | /** Apply op to snapshot */ 60 | text.apply = function(snapshot, op) { 61 | var deleted; 62 | 63 | var type = typeof snapshot; 64 | if (type !== 'string') 65 | throw new Error('text0 operations cannot be applied to type: ' + type); 66 | 67 | checkValidOp(op); 68 | for (var i = 0; i < op.length; i++) { 69 | var component = op[i]; 70 | if (component.i != null) { 71 | snapshot = strInject(snapshot, component.p, component.i); 72 | } else { 73 | deleted = snapshot.slice(component.p, component.p + component.d.length); 74 | if (component.d !== deleted) 75 | throw new Error("Delete component '" + component.d + "' does not match deleted text '" + deleted + "'"); 76 | 77 | snapshot = snapshot.slice(0, component.p) + snapshot.slice(component.p + component.d.length); 78 | } 79 | } 80 | return snapshot; 81 | }; 82 | 83 | /** 84 | * Append a component to the end of newOp. Exported for use by the random op 85 | * generator and the JSON0 type. 86 | */ 87 | var append = text._append = function(newOp, c) { 88 | if (c.i === '' || c.d === '') return; 89 | 90 | if (newOp.length === 0) { 91 | newOp.push(c); 92 | } else { 93 | var last = newOp[newOp.length - 1]; 94 | 95 | if (last.i != null && c.i != null && last.p <= c.p && c.p <= last.p + last.i.length) { 96 | // Compose the insert into the previous insert 97 | newOp[newOp.length - 1] = {i:strInject(last.i, c.p - last.p, c.i), p:last.p}; 98 | 99 | } else if (last.d != null && c.d != null && c.p <= last.p && last.p <= c.p + c.d.length) { 100 | // Compose the deletes together 101 | newOp[newOp.length - 1] = {d:strInject(c.d, last.p - c.p, last.d), p:c.p}; 102 | 103 | } else { 104 | newOp.push(c); 105 | } 106 | } 107 | }; 108 | 109 | /** Compose op1 and op2 together */ 110 | text.compose = function(op1, op2) { 111 | checkValidOp(op1); 112 | checkValidOp(op2); 113 | var newOp = op1.slice(); 114 | for (var i = 0; i < op2.length; i++) { 115 | append(newOp, op2[i]); 116 | } 117 | return newOp; 118 | }; 119 | 120 | /** Clean up an op */ 121 | text.normalize = function(op) { 122 | var newOp = []; 123 | 124 | // Normalize should allow ops which are a single (unwrapped) component: 125 | // {i:'asdf', p:23}. 126 | // There's no good way to test if something is an array: 127 | // http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/ 128 | // so this is probably the least bad solution. 129 | if (op.i != null || op.p != null) op = [op]; 130 | 131 | for (var i = 0; i < op.length; i++) { 132 | var c = op[i]; 133 | if (c.p == null) c.p = 0; 134 | 135 | append(newOp, c); 136 | } 137 | 138 | return newOp; 139 | }; 140 | 141 | // This helper method transforms a position by an op component. 142 | // 143 | // If c is an insert, insertAfter specifies whether the transform 144 | // is pushed after the insert (true) or before it (false). 145 | // 146 | // insertAfter is optional for deletes. 147 | var transformPosition = function(pos, c, insertAfter) { 148 | // This will get collapsed into a giant ternary by uglify. 149 | if (c.i != null) { 150 | if (c.p < pos || (c.p === pos && insertAfter)) { 151 | return pos + c.i.length; 152 | } else { 153 | return pos; 154 | } 155 | } else { 156 | // I think this could also be written as: Math.min(c.p, Math.min(c.p - 157 | // otherC.p, otherC.d.length)) but I think its harder to read that way, and 158 | // it compiles using ternary operators anyway so its no slower written like 159 | // this. 160 | if (pos <= c.p) { 161 | return pos; 162 | } else if (pos <= c.p + c.d.length) { 163 | return c.p; 164 | } else { 165 | return pos - c.d.length; 166 | } 167 | } 168 | }; 169 | 170 | // Helper method to transform a cursor position as a result of an op. 171 | // 172 | // Like transformPosition above, if c is an insert, insertAfter specifies 173 | // whether the cursor position is pushed after an insert (true) or before it 174 | // (false). 175 | text.transformCursor = function(position, op, side) { 176 | var insertAfter = side === 'right'; 177 | for (var i = 0; i < op.length; i++) { 178 | position = transformPosition(position, op[i], insertAfter); 179 | } 180 | 181 | return position; 182 | }; 183 | 184 | // Transform an op component by another op component. Asymmetric. 185 | // The result will be appended to destination. 186 | // 187 | // exported for use in JSON type 188 | var transformComponent = text._tc = function(dest, c, otherC, side) { 189 | //var cIntersect, intersectEnd, intersectStart, newC, otherIntersect, s; 190 | 191 | checkValidComponent(c); 192 | checkValidComponent(otherC); 193 | 194 | if (c.i != null) { 195 | // Insert. 196 | append(dest, {i:c.i, p:transformPosition(c.p, otherC, side === 'right')}); 197 | } else { 198 | // Delete 199 | if (otherC.i != null) { 200 | // Delete vs insert 201 | var s = c.d; 202 | if (c.p < otherC.p) { 203 | append(dest, {d:s.slice(0, otherC.p - c.p), p:c.p}); 204 | s = s.slice(otherC.p - c.p); 205 | } 206 | if (s !== '') 207 | append(dest, {d: s, p: c.p + otherC.i.length}); 208 | 209 | } else { 210 | // Delete vs delete 211 | if (c.p >= otherC.p + otherC.d.length) 212 | append(dest, {d: c.d, p: c.p - otherC.d.length}); 213 | else if (c.p + c.d.length <= otherC.p) 214 | append(dest, c); 215 | else { 216 | // They overlap somewhere. 217 | var newC = {d: '', p: c.p}; 218 | 219 | if (c.p < otherC.p) 220 | newC.d = c.d.slice(0, otherC.p - c.p); 221 | 222 | if (c.p + c.d.length > otherC.p + otherC.d.length) 223 | newC.d += c.d.slice(otherC.p + otherC.d.length - c.p); 224 | 225 | // This is entirely optional - I'm just checking the deleted text in 226 | // the two ops matches 227 | var intersectStart = Math.max(c.p, otherC.p); 228 | var intersectEnd = Math.min(c.p + c.d.length, otherC.p + otherC.d.length); 229 | var cIntersect = c.d.slice(intersectStart - c.p, intersectEnd - c.p); 230 | var otherIntersect = otherC.d.slice(intersectStart - otherC.p, intersectEnd - otherC.p); 231 | if (cIntersect !== otherIntersect) 232 | throw new Error('Delete ops delete different text in the same region of the document'); 233 | 234 | if (newC.d !== '') { 235 | newC.p = transformPosition(newC.p, otherC); 236 | append(dest, newC); 237 | } 238 | } 239 | } 240 | } 241 | 242 | return dest; 243 | }; 244 | 245 | var invertComponent = function(c) { 246 | return (c.i != null) ? {d:c.i, p:c.p} : {i:c.d, p:c.p}; 247 | }; 248 | 249 | // No need to use append for invert, because the components won't be able to 250 | // cancel one another. 251 | text.invert = function(op) { 252 | // Shallow copy & reverse that sucka. 253 | op = op.slice().reverse(); 254 | for (var i = 0; i < op.length; i++) { 255 | op[i] = invertComponent(op[i]); 256 | } 257 | return op; 258 | }; 259 | 260 | require('./bootstrapTransform')(text, transformComponent, checkValidOp, append); 261 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ot-json0", 3 | "version": "1.1.0", 4 | "description": "JSON OT type", 5 | "main": "lib/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "files": [ 10 | "lib" 11 | ], 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "coffee-script": "^1.7.1", 15 | "mocha": "^9.0.2", 16 | "ot-fuzzer": "^1.0.0" 17 | }, 18 | "scripts": { 19 | "test": "mocha --require 'coffee-script/register' 'test/**'" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/ottypes/json0" 24 | }, 25 | "keywords": [ 26 | "ot", 27 | "json", 28 | "sharejs", 29 | "operational-transformation" 30 | ], 31 | "author": "Joseph Gentle ", 32 | "license": "ISC", 33 | "bugs": { 34 | "url": "https://github.com/ottypes/json0/issues" 35 | }, 36 | "homepage": "https://github.com/ottypes/json0" 37 | } 38 | -------------------------------------------------------------------------------- /test/json0-generator.coffee: -------------------------------------------------------------------------------- 1 | json0 = require '../lib/json0' 2 | {randomInt, randomReal, randomWord} = require 'ot-fuzzer' 3 | 4 | # This is an awful function to clone a document snapshot for use by the random 5 | # op generator. .. Since we don't want to corrupt the original object with 6 | # the changes the op generator will make. 7 | clone = (o) -> JSON.parse(JSON.stringify(o)) 8 | 9 | randomKey = (obj) -> 10 | if Array.isArray(obj) 11 | if obj.length == 0 12 | undefined 13 | else 14 | randomInt obj.length 15 | else 16 | count = 0 17 | 18 | for key of obj 19 | result = key if randomReal() < 1/++count 20 | result 21 | 22 | # Generate a random new key for a value in obj. 23 | # obj must be an Object. 24 | randomNewKey = (obj) -> 25 | # There's no do-while loop in coffeescript. 26 | key = randomWord() 27 | key = randomWord() while obj[key] != undefined 28 | key 29 | 30 | # Generate a random object 31 | randomThing = -> 32 | switch randomInt 6 33 | when 0 then null 34 | when 1 then '' 35 | when 2 then randomWord() 36 | when 3 37 | obj = {} 38 | obj[randomNewKey(obj)] = randomThing() for [1..randomInt(5)] 39 | obj 40 | when 4 then (randomThing() for [1..randomInt(5)]) 41 | when 5 then randomInt(50) 42 | 43 | # Pick a random path to something in the object. 44 | randomPath = (data) -> 45 | path = [] 46 | 47 | while randomReal() > 0.85 and typeof data == 'object' 48 | key = randomKey data 49 | break unless key? 50 | 51 | path.push key 52 | data = data[key] 53 | 54 | path 55 | 56 | 57 | module.exports = genRandomOp = (data) -> 58 | pct = 0.95 59 | 60 | container = data: clone data 61 | 62 | op = while randomReal() < pct 63 | pct *= 0.6 64 | 65 | # Pick a random object in the document operate on. 66 | path = randomPath(container['data']) 67 | 68 | # parent = the container for the operand. parent[key] contains the operand. 69 | parent = container 70 | key = 'data' 71 | for p in path 72 | parent = parent[key] 73 | key = p 74 | operand = parent[key] 75 | 76 | if randomReal() < 0.4 and parent != container and Array.isArray(parent) 77 | # List move 78 | newIndex = randomInt parent.length 79 | 80 | # Remove the element from its current position in the list 81 | parent.splice key, 1 82 | # Insert it in the new position. 83 | parent.splice newIndex, 0, operand 84 | 85 | {p:path, lm:newIndex} 86 | 87 | else if randomReal() < 0.3 or operand == null 88 | # Replace 89 | 90 | newValue = randomThing() 91 | parent[key] = newValue 92 | 93 | if Array.isArray(parent) 94 | {p:path, ld:operand, li:clone(newValue)} 95 | else 96 | {p:path, od:operand, oi:clone(newValue)} 97 | 98 | else if typeof operand == 'string' 99 | # String. This code is adapted from the text op generator. 100 | 101 | if randomReal() > 0.5 or operand.length == 0 102 | # Insert 103 | pos = randomInt(operand.length + 1) 104 | str = randomWord() + ' ' 105 | 106 | path.push pos 107 | parent[key] = operand[...pos] + str + operand[pos..] 108 | c = {p:path, si:str} 109 | else 110 | # Delete 111 | pos = randomInt(operand.length) 112 | length = Math.min(randomInt(4), operand.length - pos) 113 | str = operand[pos...(pos + length)] 114 | 115 | path.push pos 116 | parent[key] = operand[...pos] + operand[pos + length..] 117 | c = {p:path, sd:str} 118 | 119 | if json0._testStringSubtype 120 | # Subtype 121 | subOp = {p:path.pop()} 122 | if c.si? 123 | subOp.i = c.si 124 | else 125 | subOp.d = c.sd 126 | 127 | c = {p:path, t:'text0', o:[subOp]} 128 | 129 | c 130 | 131 | else if typeof operand == 'number' 132 | # Number 133 | inc = randomInt(10) - 3 134 | parent[key] += inc 135 | {p:path, na:inc} 136 | 137 | else if Array.isArray(operand) 138 | # Array. Replace is covered above, so we'll just randomly insert or delete. 139 | # This code looks remarkably similar to string insert, above. 140 | 141 | if randomReal() > 0.5 or operand.length == 0 142 | # Insert 143 | pos = randomInt(operand.length + 1) 144 | obj = randomThing() 145 | 146 | path.push pos 147 | operand.splice pos, 0, obj 148 | {p:path, li:clone(obj)} 149 | else 150 | # Delete 151 | pos = randomInt operand.length 152 | obj = operand[pos] 153 | 154 | path.push pos 155 | operand.splice pos, 1 156 | {p:path, ld:clone(obj)} 157 | else 158 | # Object 159 | k = randomKey(operand) 160 | 161 | if randomReal() > 0.5 or not k? 162 | # Insert 163 | k = randomNewKey(operand) 164 | obj = randomThing() 165 | 166 | path.push k 167 | operand[k] = obj 168 | {p:path, oi:clone(obj)} 169 | else 170 | obj = operand[k] 171 | 172 | path.push k 173 | delete operand[k] 174 | {p:path, od:clone(obj)} 175 | 176 | [op, container.data] 177 | -------------------------------------------------------------------------------- /test/json0.coffee: -------------------------------------------------------------------------------- 1 | # Tests for JSON OT type. 2 | 3 | assert = require 'assert' 4 | nativetype = require '../lib/json0' 5 | 6 | fuzzer = require 'ot-fuzzer' 7 | 8 | nativetype.registerSubtype 9 | name: 'mock' 10 | transform: (a, b, side) -> 11 | return { mock: true } 12 | 13 | # Cross-transform helper function. Transform server by client and client by 14 | # server. Returns [server, client]. 15 | transformX = (type, left, right) -> 16 | [type.transform(left, right, 'left'), type.transform(right, left, 'right')] 17 | 18 | genTests = (type) -> 19 | # The random op tester above will test that the OT functions are admissable, 20 | # but debugging problems it detects is a pain. 21 | # 22 | # These tests should pick up *most* problems with a normal JSON OT 23 | # implementation. 24 | 25 | describe 'sanity', -> 26 | describe '#create()', -> it 'returns null', -> 27 | assert.deepEqual type.create(), null 28 | 29 | describe '#compose()', -> 30 | it 'od,oi --> od+oi', -> 31 | assert.deepEqual [{p:['foo'], od:1, oi:2}], type.compose [{p:['foo'],od:1}],[{p:['foo'],oi:2}] 32 | assert.deepEqual [{p:['foo'], od:1},{p:['bar'], oi:2}], type.compose [{p:['foo'],od:1}],[{p:['bar'],oi:2}] 33 | it 'merges od+oi, od+oi -> od+oi', -> 34 | assert.deepEqual [{p:['foo'], od:1, oi:2}], type.compose [{p:['foo'],od:1,oi:3}],[{p:['foo'],od:3,oi:2}] 35 | 36 | 37 | describe '#transform()', -> it 'returns sane values', -> 38 | t = (op1, op2) -> 39 | assert.deepEqual op1, type.transform op1, op2, 'left' 40 | assert.deepEqual op1, type.transform op1, op2, 'right' 41 | 42 | t [], [] 43 | t [{p:['foo'], oi:1}], [] 44 | t [{p:['foo'], oi:1}], [{p:['bar'], oi:2}] 45 | 46 | describe 'number', -> 47 | it 'Adds a number', -> 48 | assert.deepEqual 3, type.apply 1, [{p:[], na:2}] 49 | assert.deepEqual [3], type.apply [1], [{p:[0], na:2}] 50 | 51 | it 'compresses two adds together in compose', -> 52 | assert.deepEqual [{p:['a', 'b'], na:3}], type.compose [{p:['a', 'b'], na:1}], [{p:['a', 'b'], na:2}] 53 | assert.deepEqual [{p:['a'], na:1}, {p:['b'], na:2}], type.compose [{p:['a'], na:1}], [{p:['b'], na:2}] 54 | 55 | it 'doesn\'t overwrite values when it merges na in append', -> 56 | rightHas = 21 57 | leftHas = 3 58 | 59 | rightOp = [{"p":[],"od":0,"oi":15},{"p":[],"na":4},{"p":[],"na":1},{"p":[],"na":1}] 60 | leftOp = [{"p":[],"na":4},{"p":[],"na":-1}] 61 | [right_, left_] = transformX type, rightOp, leftOp 62 | 63 | s_c = type.apply rightHas, left_ 64 | c_s = type.apply leftHas, right_ 65 | assert.deepEqual s_c, c_s 66 | 67 | it 'throws when adding a string to a number', -> 68 | assert.throws -> type.apply 1, [{p: [], na: 'a'}] 69 | 70 | it 'throws when adding a number to a string', -> 71 | assert.throws -> type.apply 'a', [{p: [], na: 1}] 72 | 73 | # Strings should be handled internally by the text type. We'll just do some basic sanity checks here. 74 | describe 'string', -> 75 | describe '#apply()', -> it 'works', -> 76 | assert.deepEqual 'abc', type.apply 'a', [{p:[1], si:'bc'}] 77 | assert.deepEqual 'bc', type.apply 'abc', [{p:[0], sd:'a'}] 78 | assert.deepEqual {x:'abc'}, type.apply {x:'a'}, [{p:['x', 1], si:'bc'}] 79 | 80 | it 'throws when the target is not a string', -> 81 | assert.throws -> type.apply [1], [{p: [0], si: 'a'}] 82 | 83 | it 'throws when the inserted content is not a string', -> 84 | assert.throws -> type.apply 'a', [{p: [0], si: 1}] 85 | 86 | describe '#transform()', -> 87 | it 'splits deletes', -> 88 | assert.deepEqual type.transform([{p:[0], sd:'ab'}], [{p:[1], si:'x'}], 'left'), [{p:[0], sd:'a'}, {p:[1], sd:'b'}] 89 | 90 | it 'cancels out other deletes', -> 91 | assert.deepEqual type.transform([{p:['k', 5], sd:'a'}], [{p:['k', 5], sd:'a'}], 'left'), [] 92 | 93 | it 'does not throw errors with blank inserts', -> 94 | assert.deepEqual type.transform([{p: ['k', 5], si:''}], [{p: ['k', 3], si: 'a'}], 'left'), [] 95 | 96 | describe 'string subtype', -> 97 | describe '#apply()', -> 98 | it 'works', -> 99 | assert.deepEqual 'abc', type.apply 'a', [{p:[], t:'text0', o:[{p:1, i:'bc'}]}] 100 | assert.deepEqual 'bc', type.apply 'abc', [{p:[], t:'text0', o:[{p:0, d:'a'}]}] 101 | assert.deepEqual {x:'abc'}, type.apply {x:'a'}, [{p:['x'], t:'text0', o:[{p:1, i:'bc'}]}] 102 | 103 | describe '#transform()', -> 104 | it 'splits deletes', -> 105 | a = [{p:[], t:'text0', o:[{p:0, d:'ab'}]}] 106 | b = [{p:[], t:'text0', o:[{p:1, i:'x'}]}] 107 | assert.deepEqual type.transform(a, b, 'left'), [{p:[], t:'text0', o:[{p:0, d:'a'}, {p:1, d:'b'}]}] 108 | 109 | it 'cancels out other deletes', -> 110 | assert.deepEqual type.transform([{p:['k'], t:'text0', o:[{p:5, d:'a'}]}], [{p:['k'], t:'text0', o:[{p:5, d:'a'}]}], 'left'), [] 111 | 112 | it 'does not throw errors with blank inserts', -> 113 | assert.deepEqual type.transform([{p:['k'], t:'text0', o:[{p:5, i:''}]}], [{p:['k'], t:'text0', o:[{p:3, i:'a'}]}], 'left'), [] 114 | 115 | describe 'subtype with non-array operation', -> 116 | describe '#transform()', -> 117 | it 'works', -> 118 | a = [{p:[], t:'mock', o:'foo'}] 119 | b = [{p:[], t:'mock', o:'bar'}] 120 | assert.deepEqual type.transform(a, b, 'left'), [{p:[], t:'mock', o:{mock:true}}] 121 | 122 | describe 'list', -> 123 | describe 'apply', -> 124 | it 'inserts', -> 125 | assert.deepEqual ['a', 'b', 'c'], type.apply ['b', 'c'], [{p:[0], li:'a'}] 126 | assert.deepEqual ['a', 'b', 'c'], type.apply ['a', 'c'], [{p:[1], li:'b'}] 127 | assert.deepEqual ['a', 'b', 'c'], type.apply ['a', 'b'], [{p:[2], li:'c'}] 128 | 129 | it 'deletes', -> 130 | assert.deepEqual ['b', 'c'], type.apply ['a', 'b', 'c'], [{p:[0], ld:'a'}] 131 | assert.deepEqual ['a', 'c'], type.apply ['a', 'b', 'c'], [{p:[1], ld:'b'}] 132 | assert.deepEqual ['a', 'b'], type.apply ['a', 'b', 'c'], [{p:[2], ld:'c'}] 133 | 134 | it 'replaces', -> 135 | assert.deepEqual ['a', 'y', 'b'], type.apply ['a', 'x', 'b'], [{p:[1], ld:'x', li:'y'}] 136 | 137 | it 'moves', -> 138 | assert.deepEqual ['a', 'b', 'c'], type.apply ['b', 'a', 'c'], [{p:[1], lm:0}] 139 | assert.deepEqual ['a', 'b', 'c'], type.apply ['b', 'a', 'c'], [{p:[0], lm:1}] 140 | 141 | it 'throws when keying a list replacement with a string', -> 142 | assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], li: 'x', ld: 'a'}] 143 | 144 | it 'throws when keying a list insertion with a string', -> 145 | assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], li: 'x'}] 146 | 147 | it 'throws when keying a list deletion with a string', -> 148 | assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], ld: 'a'}] 149 | 150 | it 'throws when keying a list move with a string', -> 151 | assert.throws -> type.apply ['a', 'b', 'c'], [{p: ['0'], lm: 0}] 152 | 153 | it 'throws when specifying a string as a list move target', -> 154 | assert.throws -> type.apply ['a', 'b', 'c'], [{p: [1], lm: '0'}] 155 | 156 | it 'throws when an array index part-way through the path is a string', -> 157 | assert.throws -> type.apply {arr:[{x:'a'}]}, [{p:['arr', '0', 'x'], od: 'a'}] 158 | 159 | ### 160 | 'null moves compose to nops', -> 161 | assert.deepEqual [], type.compose [], [{p:[3],lm:3}] 162 | assert.deepEqual [], type.compose [], [{p:[0,3],lm:3}] 163 | assert.deepEqual [], type.compose [], [{p:['x','y',0],lm:0}] 164 | ### 165 | 166 | describe '#transform()', -> 167 | it 'bumps paths when list elements are inserted or removed', -> 168 | assert.deepEqual [{p:[2, 200], si:'hi'}], type.transform [{p:[1, 200], si:'hi'}], [{p:[0], li:'x'}], 'left' 169 | assert.deepEqual [{p:[1, 201], si:'hi'}], type.transform [{p:[0, 201], si:'hi'}], [{p:[0], li:'x'}], 'right' 170 | assert.deepEqual [{p:[0, 202], si:'hi'}], type.transform [{p:[0, 202], si:'hi'}], [{p:[1], li:'x'}], 'left' 171 | assert.deepEqual [{p:[2], t:'text0', o:[{p:200, i:'hi'}]}], type.transform [{p:[1], t:'text0', o:[{p:200, i:'hi'}]}], [{p:[0], li:'x'}], 'left' 172 | assert.deepEqual [{p:[1], t:'text0', o:[{p:201, i:'hi'}]}], type.transform [{p:[0], t:'text0', o:[{p:201, i:'hi'}]}], [{p:[0], li:'x'}], 'right' 173 | assert.deepEqual [{p:[0], t:'text0', o:[{p:202, i:'hi'}]}], type.transform [{p:[0], t:'text0', o:[{p:202, i:'hi'}]}], [{p:[1], li:'x'}], 'left' 174 | 175 | assert.deepEqual [{p:[0, 203], si:'hi'}], type.transform [{p:[1, 203], si:'hi'}], [{p:[0], ld:'x'}], 'left' 176 | assert.deepEqual [{p:[0, 204], si:'hi'}], type.transform [{p:[0, 204], si:'hi'}], [{p:[1], ld:'x'}], 'left' 177 | assert.deepEqual [{p:['x',3], si: 'hi'}], type.transform [{p:['x',3], si:'hi'}], [{p:['x',0,'x'], li:0}], 'left' 178 | assert.deepEqual [{p:['x',3,'x'], si: 'hi'}], type.transform [{p:['x',3,'x'], si:'hi'}], [{p:['x',5], li:0}], 'left' 179 | assert.deepEqual [{p:['x',4,'x'], si: 'hi'}], type.transform [{p:['x',3,'x'], si:'hi'}], [{p:['x',0], li:0}], 'left' 180 | assert.deepEqual [{p:[0], t:'text0', o:[{p:203, i:'hi'}]}], type.transform [{p:[1], t:'text0', o:[{p:203, i:'hi'}]}], [{p:[0], ld:'x'}], 'left' 181 | assert.deepEqual [{p:[0], t:'text0', o:[{p:204, i:'hi'}]}], type.transform [{p:[0], t:'text0', o:[{p:204, i:'hi'}]}], [{p:[1], ld:'x'}], 'left' 182 | assert.deepEqual [{p:['x'], t:'text0', o:[{p:3,i: 'hi'}]}], type.transform [{p:['x'], t:'text0', o:[{p:3, i:'hi'}]}], [{p:['x',0,'x'], li:0}], 'left' 183 | 184 | assert.deepEqual [{p:[1],ld:2}], type.transform [{p:[0],ld:2}], [{p:[0],li:1}], 'left' 185 | assert.deepEqual [{p:[1],ld:2}], type.transform [{p:[0],ld:2}], [{p:[0],li:1}], 'right' 186 | 187 | it 'converts ops on deleted elements to noops', -> 188 | assert.deepEqual [], type.transform [{p:[1, 0], si:'hi'}], [{p:[1], ld:'x'}], 'left' 189 | assert.deepEqual [], type.transform [{p:[1], t:'text0', o:[{p:0, i:'hi'}]}], [{p:[1], ld:'x'}], 'left' 190 | assert.deepEqual [{p:[0],li:'x'}], type.transform [{p:[0],li:'x'}], [{p:[0],ld:'y'}], 'left' 191 | assert.deepEqual [], type.transform [{p:[0],na:-3}], [{p:[0],ld:48}], 'left' 192 | 193 | it 'converts ops on replaced elements to noops', -> 194 | assert.deepEqual [], type.transform [{p:[1, 0], si:'hi'}], [{p:[1], ld:'x', li:'y'}], 'left' 195 | assert.deepEqual [], type.transform [{p:[1], t:'text0', o:[{p:0, i:'hi'}]}], [{p:[1], ld:'x', li:'y'}], 'left' 196 | assert.deepEqual [{p:[0], li:'hi'}], type.transform [{p:[0], li:'hi'}], [{p:[0], ld:'x', li:'y'}], 'left' 197 | 198 | it 'changes deleted data to reflect edits', -> 199 | assert.deepEqual [{p:[1], ld:'abc'}], type.transform [{p:[1], ld:'a'}], [{p:[1, 1], si:'bc'}], 'left' 200 | assert.deepEqual [{p:[1], ld:'abc'}], type.transform [{p:[1], ld:'a'}], [{p:[1], t:'text0', o:[{p:1, i:'bc'}]}], 'left' 201 | 202 | it 'Puts the left op first if two inserts are simultaneous', -> 203 | assert.deepEqual [{p:[1], li:'a'}], type.transform [{p:[1], li:'a'}], [{p:[1], li:'b'}], 'left' 204 | assert.deepEqual [{p:[2], li:'b'}], type.transform [{p:[1], li:'b'}], [{p:[1], li:'a'}], 'right' 205 | 206 | it 'converts an attempt to re-delete a list element into a no-op', -> 207 | assert.deepEqual [], type.transform [{p:[1], ld:'x'}], [{p:[1], ld:'x'}], 'left' 208 | assert.deepEqual [], type.transform [{p:[1], ld:'x'}], [{p:[1], ld:'x'}], 'right' 209 | 210 | 211 | describe '#compose()', -> 212 | it 'composes insert then delete into a no-op', -> 213 | assert.deepEqual [], type.compose [{p:[1], li:'abc'}], [{p:[1], ld:'abc'}] 214 | assert.deepEqual [{p:[1],ld:null,li:'x'}], type.transform [{p:[0],ld:null,li:"x"}], [{p:[0],li:"The"}], 'right' 215 | 216 | it 'doesn\'t change the original object', -> 217 | a = [{p:[0],ld:'abc',li:null}] 218 | assert.deepEqual [{p:[0],ld:'abc'}], type.compose a, [{p:[0],ld:null}] 219 | assert.deepEqual [{p:[0],ld:'abc',li:null}], a 220 | 221 | it 'composes together adjacent string ops', -> 222 | assert.deepEqual [{p:[100], si:'hi'}], type.compose [{p:[100], si:'h'}], [{p:[101], si:'i'}] 223 | assert.deepEqual [{p:[], t:'text0', o:[{p:100, i:'hi'}]}], type.compose [{p:[], t:'text0', o:[{p:100, i:'h'}]}], [{p:[], t:'text0', o:[{p:101, i:'i'}]}] 224 | 225 | it 'moves ops on a moved element with the element', -> 226 | assert.deepEqual [{p:[10], ld:'x'}], type.transform [{p:[4], ld:'x'}], [{p:[4], lm:10}], 'left' 227 | assert.deepEqual [{p:[10, 1], si:'a'}], type.transform [{p:[4, 1], si:'a'}], [{p:[4], lm:10}], 'left' 228 | assert.deepEqual [{p:[10], t:'text0', o:[{p:1, i:'a'}]}], type.transform [{p:[4], t:'text0', o:[{p:1, i:'a'}]}], [{p:[4], lm:10}], 'left' 229 | assert.deepEqual [{p:[10, 1], li:'a'}], type.transform [{p:[4, 1], li:'a'}], [{p:[4], lm:10}], 'left' 230 | assert.deepEqual [{p:[10, 1], ld:'b', li:'a'}], type.transform [{p:[4, 1], ld:'b', li:'a'}], [{p:[4], lm:10}], 'left' 231 | 232 | assert.deepEqual [{p:[0],li:null}], type.transform [{p:[0],li:null}], [{p:[0],lm:1}], 'left' 233 | # [_,_,_,_,5,6,7,_] 234 | # c: [_,_,_,_,5,'x',6,7,_] p:5 li:'x' 235 | # s: [_,6,_,_,_,5,7,_] p:5 lm:1 236 | # correct: [_,6,_,_,_,5,'x',7,_] 237 | assert.deepEqual [{p:[6],li:'x'}], type.transform [{p:[5],li:'x'}], [{p:[5],lm:1}], 'left' 238 | # [_,_,_,_,5,6,7,_] 239 | # c: [_,_,_,_,5,6,7,_] p:5 ld:6 240 | # s: [_,6,_,_,_,5,7,_] p:5 lm:1 241 | # correct: [_,_,_,_,5,7,_] 242 | assert.deepEqual [{p:[1],ld:6}], type.transform [{p:[5],ld:6}], [{p:[5],lm:1}], 'left' 243 | #assert.deepEqual [{p:[0],li:{}}], type.transform [{p:[0],li:{}}], [{p:[0],lm:0}], 'right' 244 | assert.deepEqual [{p:[0],li:[]}], type.transform [{p:[0],li:[]}], [{p:[1],lm:0}], 'left' 245 | assert.deepEqual [{p:[2],li:'x'}], type.transform [{p:[2],li:'x'}], [{p:[0],lm:1}], 'left' 246 | 247 | it 'moves target index on ld/li', -> 248 | assert.deepEqual [{p:[0],lm:1}], type.transform [{p:[0], lm: 2}], [{p:[1], ld:'x'}], 'left' 249 | assert.deepEqual [{p:[1],lm:3}], type.transform [{p:[2], lm: 4}], [{p:[1], ld:'x'}], 'left' 250 | assert.deepEqual [{p:[0],lm:3}], type.transform [{p:[0], lm: 2}], [{p:[1], li:'x'}], 'left' 251 | assert.deepEqual [{p:[3],lm:5}], type.transform [{p:[2], lm: 4}], [{p:[1], li:'x'}], 'left' 252 | assert.deepEqual [{p:[1],lm:1}], type.transform [{p:[0], lm: 0}], [{p:[0], li:28}], 'left' 253 | 254 | it 'tiebreaks lm vs. ld/li', -> 255 | assert.deepEqual [], type.transform [{p:[0], lm: 2}], [{p:[0], ld:'x'}], 'left' 256 | assert.deepEqual [], type.transform [{p:[0], lm: 2}], [{p:[0], ld:'x'}], 'right' 257 | assert.deepEqual [{p:[1], lm:3}], type.transform [{p:[0], lm: 2}], [{p:[0], li:'x'}], 'left' 258 | assert.deepEqual [{p:[1], lm:3}], type.transform [{p:[0], lm: 2}], [{p:[0], li:'x'}], 'right' 259 | 260 | it 'replacement vs. deletion', -> 261 | assert.deepEqual [{p:[0],li:'y'}], type.transform [{p:[0],ld:'x',li:'y'}], [{p:[0],ld:'x'}], 'right' 262 | 263 | it 'replacement vs. insertion', -> 264 | assert.deepEqual [{p:[1],ld:{},li:"brillig"}], type.transform [{p:[0],ld:{},li:"brillig"}], [{p:[0],li:36}], 'left' 265 | 266 | it 'replacement vs. replacement', -> 267 | assert.deepEqual [], type.transform [{p:[0],ld:null,li:[]}], [{p:[0],ld:null,li:0}], 'right' 268 | assert.deepEqual [{p:[0],ld:[],li:0}], type.transform [{p:[0],ld:null,li:0}], [{p:[0],ld:null,li:[]}], 'left' 269 | 270 | it 'composes replace with delete of replaced element results in insert', -> 271 | assert.deepEqual [{p:[2],ld:[]}], type.compose [{p:[2],ld:[],li:null}], [{p:[2],ld:null}] 272 | 273 | it 'lm vs lm', -> 274 | assert.deepEqual [{p:[0],lm:2}], type.transform [{p:[0],lm:2}], [{p:[2],lm:1}], 'left' 275 | assert.deepEqual [{p:[4],lm:4}], type.transform [{p:[3],lm:3}], [{p:[5],lm:0}], 'left' 276 | assert.deepEqual [{p:[2],lm:0}], type.transform [{p:[2],lm:0}], [{p:[1],lm:0}], 'left' 277 | assert.deepEqual [{p:[2],lm:1}], type.transform [{p:[2],lm:0}], [{p:[1],lm:0}], 'right' 278 | assert.deepEqual [{p:[3],lm:1}], type.transform [{p:[2],lm:0}], [{p:[5],lm:0}], 'right' 279 | assert.deepEqual [{p:[3],lm:0}], type.transform [{p:[2],lm:0}], [{p:[5],lm:0}], 'left' 280 | assert.deepEqual [{p:[0],lm:5}], type.transform [{p:[2],lm:5}], [{p:[2],lm:0}], 'left' 281 | assert.deepEqual [{p:[0],lm:5}], type.transform [{p:[2],lm:5}], [{p:[2],lm:0}], 'left' 282 | assert.deepEqual [{p:[0],lm:0}], type.transform [{p:[1],lm:0}], [{p:[0],lm:5}], 'right' 283 | assert.deepEqual [{p:[0],lm:0}], type.transform [{p:[1],lm:0}], [{p:[0],lm:1}], 'right' 284 | assert.deepEqual [{p:[1],lm:1}], type.transform [{p:[0],lm:1}], [{p:[1],lm:0}], 'left' 285 | assert.deepEqual [{p:[1],lm:2}], type.transform [{p:[0],lm:1}], [{p:[5],lm:0}], 'right' 286 | assert.deepEqual [{p:[3],lm:2}], type.transform [{p:[2],lm:1}], [{p:[5],lm:0}], 'right' 287 | assert.deepEqual [{p:[2],lm:1}], type.transform [{p:[3],lm:1}], [{p:[1],lm:3}], 'left' 288 | assert.deepEqual [{p:[2],lm:3}], type.transform [{p:[1],lm:3}], [{p:[3],lm:1}], 'left' 289 | assert.deepEqual [{p:[2],lm:6}], type.transform [{p:[2],lm:6}], [{p:[0],lm:1}], 'left' 290 | assert.deepEqual [{p:[2],lm:6}], type.transform [{p:[2],lm:6}], [{p:[0],lm:1}], 'right' 291 | assert.deepEqual [{p:[2],lm:6}], type.transform [{p:[2],lm:6}], [{p:[1],lm:0}], 'left' 292 | assert.deepEqual [{p:[2],lm:6}], type.transform [{p:[2],lm:6}], [{p:[1],lm:0}], 'right' 293 | assert.deepEqual [{p:[0],lm:2}], type.transform [{p:[0],lm:1}], [{p:[2],lm:1}], 'left' 294 | assert.deepEqual [{p:[2],lm:0}], type.transform [{p:[2],lm:1}], [{p:[0],lm:1}], 'right' 295 | assert.deepEqual [{p:[1],lm:1}], type.transform [{p:[0],lm:0}], [{p:[1],lm:0}], 'left' 296 | assert.deepEqual [{p:[0],lm:0}], type.transform [{p:[0],lm:1}], [{p:[1],lm:3}], 'left' 297 | assert.deepEqual [{p:[3],lm:1}], type.transform [{p:[2],lm:1}], [{p:[3],lm:2}], 'left' 298 | assert.deepEqual [{p:[3],lm:3}], type.transform [{p:[3],lm:2}], [{p:[2],lm:1}], 'left' 299 | 300 | it 'changes indices correctly around a move', -> 301 | assert.deepEqual [{p:[1,0],li:{}}], type.transform [{p:[0,0],li:{}}], [{p:[1],lm:0}], 'left' 302 | assert.deepEqual [{p:[0],lm:0}], type.transform [{p:[1],lm:0}], [{p:[0],ld:{}}], 'left' 303 | assert.deepEqual [{p:[0],lm:0}], type.transform [{p:[0],lm:1}], [{p:[1],ld:{}}], 'left' 304 | assert.deepEqual [{p:[5],lm:0}], type.transform [{p:[6],lm:0}], [{p:[2],ld:{}}], 'left' 305 | assert.deepEqual [{p:[1],lm:0}], type.transform [{p:[1],lm:0}], [{p:[2],ld:{}}], 'left' 306 | assert.deepEqual [{p:[1],lm:1}], type.transform [{p:[2],lm:1}], [{p:[1],ld:3}], 'right' 307 | 308 | assert.deepEqual [{p:[1],ld:{}}], type.transform [{p:[2],ld:{}}], [{p:[1],lm:2}], 'right' 309 | assert.deepEqual [{p:[2],ld:{}}], type.transform [{p:[1],ld:{}}], [{p:[2],lm:1}], 'left' 310 | 311 | 312 | assert.deepEqual [{p:[0],ld:{}}], type.transform [{p:[1],ld:{}}], [{p:[0],lm:1}], 'right' 313 | 314 | assert.deepEqual [{p:[0],ld:1,li:2}], type.transform [{p:[1],ld:1,li:2}], [{p:[1],lm:0}], 'left' 315 | assert.deepEqual [{p:[0],ld:2,li:3}], type.transform [{p:[1],ld:2,li:3}], [{p:[0],lm:1}], 'left' 316 | assert.deepEqual [{p:[1],ld:3,li:4}], type.transform [{p:[0],ld:3,li:4}], [{p:[1],lm:0}], 'left' 317 | 318 | it 'li vs lm', -> 319 | li = (p) -> [{p:[p],li:[]}] 320 | lm = (f,t) -> [{p:[f],lm:t}] 321 | xf = type.transform 322 | 323 | assert.deepEqual (li 0), xf (li 0), (lm 1, 3), 'left' 324 | assert.deepEqual (li 1), xf (li 1), (lm 1, 3), 'left' 325 | assert.deepEqual (li 1), xf (li 2), (lm 1, 3), 'left' 326 | assert.deepEqual (li 2), xf (li 3), (lm 1, 3), 'left' 327 | assert.deepEqual (li 4), xf (li 4), (lm 1, 3), 'left' 328 | 329 | assert.deepEqual (lm 2, 4), xf (lm 1, 3), (li 0), 'right' 330 | assert.deepEqual (lm 2, 4), xf (lm 1, 3), (li 1), 'right' 331 | assert.deepEqual (lm 1, 4), xf (lm 1, 3), (li 2), 'right' 332 | assert.deepEqual (lm 1, 4), xf (lm 1, 3), (li 3), 'right' 333 | assert.deepEqual (lm 1, 3), xf (lm 1, 3), (li 4), 'right' 334 | 335 | assert.deepEqual (li 0), xf (li 0), (lm 1, 2), 'left' 336 | assert.deepEqual (li 1), xf (li 1), (lm 1, 2), 'left' 337 | assert.deepEqual (li 1), xf (li 2), (lm 1, 2), 'left' 338 | assert.deepEqual (li 3), xf (li 3), (lm 1, 2), 'left' 339 | 340 | assert.deepEqual (li 0), xf (li 0), (lm 3, 1), 'left' 341 | assert.deepEqual (li 1), xf (li 1), (lm 3, 1), 'left' 342 | assert.deepEqual (li 3), xf (li 2), (lm 3, 1), 'left' 343 | assert.deepEqual (li 4), xf (li 3), (lm 3, 1), 'left' 344 | assert.deepEqual (li 4), xf (li 4), (lm 3, 1), 'left' 345 | 346 | assert.deepEqual (lm 4, 2), xf (lm 3, 1), (li 0), 'right' 347 | assert.deepEqual (lm 4, 2), xf (lm 3, 1), (li 1), 'right' 348 | assert.deepEqual (lm 4, 1), xf (lm 3, 1), (li 2), 'right' 349 | assert.deepEqual (lm 4, 1), xf (lm 3, 1), (li 3), 'right' 350 | assert.deepEqual (lm 3, 1), xf (lm 3, 1), (li 4), 'right' 351 | 352 | assert.deepEqual (li 0), xf (li 0), (lm 2, 1), 'left' 353 | assert.deepEqual (li 1), xf (li 1), (lm 2, 1), 'left' 354 | assert.deepEqual (li 3), xf (li 2), (lm 2, 1), 'left' 355 | assert.deepEqual (li 3), xf (li 3), (lm 2, 1), 'left' 356 | 357 | 358 | describe 'object', -> 359 | it 'passes sanity checks', -> 360 | assert.deepEqual {x:'a', y:'b'}, type.apply {x:'a'}, [{p:['y'], oi:'b'}] 361 | assert.deepEqual {}, type.apply {x:'a'}, [{p:['x'], od:'a'}] 362 | assert.deepEqual {x:'b'}, type.apply {x:'a'}, [{p:['x'], od:'a', oi:'b'}] 363 | 364 | it 'Ops on deleted elements become noops', -> 365 | assert.deepEqual [], type.transform [{p:[1, 0], si:'hi'}], [{p:[1], od:'x'}], 'left' 366 | assert.deepEqual [], type.transform [{p:[1], t:'text0', o:[{p:0, i:'hi'}]}], [{p:[1], od:'x'}], 'left' 367 | assert.deepEqual [], type.transform [{p:[9],si:"bite "}], [{p:[],od:"agimble s",oi:null}], 'right' 368 | assert.deepEqual [], type.transform [{p:[], t:'text0', o:[{p:9, i:"bite "}]}], [{p:[],od:"agimble s",oi:null}], 'right' 369 | 370 | it 'Ops on replaced elements become noops', -> 371 | assert.deepEqual [], type.transform [{p:[1, 0], si:'hi'}], [{p:[1], od:'x', oi:'y'}], 'left' 372 | assert.deepEqual [], type.transform [{p:[1], t:'text0', o:[{p:0, i:'hi'}]}], [{p:[1], od:'x', oi:'y'}], 'left' 373 | 374 | it 'Deleted data is changed to reflect edits', -> 375 | assert.deepEqual [{p:[1], od:'abc'}], type.transform [{p:[1], od:'a'}], [{p:[1, 1], si:'bc'}], 'left' 376 | assert.deepEqual [{p:[1], od:'abc'}], type.transform [{p:[1], od:'a'}], [{p:[1], t:'text0', o:[{p:1, i:'bc'}]}], 'left' 377 | assert.deepEqual [{p:[],od:25,oi:[]}], type.transform [{p:[],od:22,oi:[]}], [{p:[],na:3}], 'left' 378 | assert.deepEqual [{p:[],od:{toves:""},oi:4}], type.transform [{p:[],od:{toves:0},oi:4}], [{p:["toves"],od:0,oi:""}], 'left' 379 | assert.deepEqual [{p:[],od:"thou an",oi:[]}], type.transform [{p:[],od:"thou and ",oi:[]}], [{p:[7],sd:"d "}], 'left' 380 | assert.deepEqual [{p:[],od:"thou an",oi:[]}], type.transform [{p:[],od:"thou and ",oi:[]}], [{p:[], t:'text0', o:[{p:7, d:"d "}]}], 'left' 381 | assert.deepEqual [], type.transform([{p:["bird"],na:2}], [{p:[],od:{bird:38},oi:20}], 'right') 382 | assert.deepEqual [{p:[],od:{bird:40},oi:20}], type.transform([{p:[],od:{bird:38},oi:20}], [{p:["bird"],na:2}], 'left') 383 | assert.deepEqual [{p:['He'],od:[]}], type.transform [{p:["He"],od:[]}], [{p:["The"],na:-3}], 'right' 384 | assert.deepEqual [], type.transform [{p:["He"],oi:{}}], [{p:[],od:{},oi:"the"}], 'left' 385 | 386 | it 'If two inserts are simultaneous, the lefts insert will win', -> 387 | assert.deepEqual [{p:[1], oi:'a', od:'b'}], type.transform [{p:[1], oi:'a'}], [{p:[1], oi:'b'}], 'left' 388 | assert.deepEqual [], type.transform [{p:[1], oi:'b'}], [{p:[1], oi:'a'}], 'right' 389 | 390 | it 'parallel ops on different keys miss each other', -> 391 | assert.deepEqual [{p:['a'], oi: 'x'}], type.transform [{p:['a'], oi:'x'}], [{p:['b'], oi:'z'}], 'left' 392 | assert.deepEqual [{p:['a'], oi: 'x'}], type.transform [{p:['a'], oi:'x'}], [{p:['b'], od:'z'}], 'left' 393 | assert.deepEqual [{p:["in","he"],oi:{}}], type.transform [{p:["in","he"],oi:{}}], [{p:["and"],od:{}}], 'right' 394 | assert.deepEqual [{p:['x',0],si:"his "}], type.transform [{p:['x',0],si:"his "}], [{p:['y'],od:0,oi:1}], 'right' 395 | assert.deepEqual [{p:['x'], t:'text0', o:[{p:0, i:"his "}]}], type.transform [{p:['x'],t:'text0', o:[{p:0, i:"his "}]}], [{p:['y'],od:0,oi:1}], 'right' 396 | 397 | it 'replacement vs. deletion', -> 398 | assert.deepEqual [{p:[],oi:{}}], type.transform [{p:[],od:[''],oi:{}}], [{p:[],od:['']}], 'right' 399 | 400 | it 'replacement vs. replacement', -> 401 | assert.deepEqual [], type.transform [{p:[],od:['']},{p:[],oi:{}}], [{p:[],od:['']},{p:[],oi:null}], 'right' 402 | assert.deepEqual [{p:[],od:null,oi:{}}], type.transform [{p:[],od:['']},{p:[],oi:{}}], [{p:[],od:['']},{p:[],oi:null}], 'left' 403 | assert.deepEqual [], type.transform [{p:[],od:[''],oi:{}}], [{p:[],od:[''],oi:null}], 'right' 404 | assert.deepEqual [{p:[],od:null,oi:{}}], type.transform [{p:[],od:[''],oi:{}}], [{p:[],od:[''],oi:null}], 'left' 405 | 406 | # test diamond property 407 | rightOps = [ {"p":[],"od":null,"oi":{}} ] 408 | leftOps = [ {"p":[],"od":null,"oi":""} ] 409 | rightHas = type.apply(null, rightOps) 410 | leftHas = type.apply(null, leftOps) 411 | 412 | [left_, right_] = transformX type, leftOps, rightOps 413 | assert.deepEqual leftHas, type.apply rightHas, left_ 414 | assert.deepEqual leftHas, type.apply leftHas, right_ 415 | 416 | 417 | it 'An attempt to re-delete a key becomes a no-op', -> 418 | assert.deepEqual [], type.transform [{p:['k'], od:'x'}], [{p:['k'], od:'x'}], 'left' 419 | assert.deepEqual [], type.transform [{p:['k'], od:'x'}], [{p:['k'], od:'x'}], 'right' 420 | 421 | it 'throws when the insertion key is a number', -> 422 | assert.throws -> type.apply {'1':'a'}, [{p:[2], oi: 'a'}] 423 | 424 | it 'throws when the deletion key is a number', -> 425 | assert.throws -> type.apply {'1':'a'}, [{p:[1], od: 'a'}] 426 | 427 | it 'throws when an object key part-way through the path is a number', -> 428 | assert.throws -> type.apply {'1': {x: 'a'}}, [{p:[1, 'x'], od: 'a'}] 429 | 430 | describe 'randomizer', -> 431 | @timeout 20000 432 | @slow 6000 433 | it 'passes', -> 434 | fuzzer type, require('./json0-generator'), 1000 435 | 436 | it 'passes with string subtype', -> 437 | type._testStringSubtype = true # hack 438 | fuzzer type, require('./json0-generator'), 1000 439 | delete type._testStringSubtype 440 | 441 | describe 'json', -> 442 | describe 'native type', -> genTests nativetype 443 | #exports.webclient = genTests require('../helpers/webclient').types.json 444 | -------------------------------------------------------------------------------- /test/text0-generator.coffee: -------------------------------------------------------------------------------- 1 | # Random op generator for the embedded text0 OT type. This is used by the fuzzer 2 | # test. 3 | 4 | {randomReal, randomWord} = require 'ot-fuzzer' 5 | text0 = require '../lib/text0' 6 | 7 | module.exports = genRandomOp = (docStr) -> 8 | pct = 0.9 9 | 10 | op = [] 11 | 12 | while randomReal() < pct 13 | # console.log "docStr = #{i docStr}" 14 | pct /= 2 15 | 16 | if randomReal() > 0.5 17 | # Append an insert 18 | pos = Math.floor(randomReal() * (docStr.length + 1)) 19 | str = randomWord() + ' ' 20 | text0._append op, {i:str, p:pos} 21 | docStr = docStr[...pos] + str + docStr[pos..] 22 | else 23 | # Append a delete 24 | pos = Math.floor(randomReal() * docStr.length) 25 | length = Math.min(Math.floor(randomReal() * 4), docStr.length - pos) 26 | text0._append op, {d:docStr[pos...(pos + length)], p:pos} 27 | docStr = docStr[...pos] + docStr[(pos + length)..] 28 | 29 | # console.log "generated op #{i op} -> #{i docStr}" 30 | [op, docStr] 31 | -------------------------------------------------------------------------------- /test/text0.coffee: -------------------------------------------------------------------------------- 1 | # Tests for the embedded non-composable text type text0. 2 | 3 | assert = require 'assert' 4 | fuzzer = require 'ot-fuzzer' 5 | text0 = require '../lib/text0' 6 | 7 | describe 'text0', -> 8 | describe 'compose', -> 9 | # Compose is actually pretty easy 10 | it 'is sane', -> 11 | assert.deepEqual text0.compose([], []), [] 12 | assert.deepEqual text0.compose([{i:'x', p:0}], []), [{i:'x', p:0}] 13 | assert.deepEqual text0.compose([], [{i:'x', p:0}]), [{i:'x', p:0}] 14 | assert.deepEqual text0.compose([{i:'y', p:100}], [{i:'x', p:0}]), [{i:'y', p:100}, {i:'x', p:0}] 15 | 16 | describe 'transform', -> 17 | it 'is sane', -> 18 | assert.deepEqual [], text0.transform [], [], 'left' 19 | assert.deepEqual [], text0.transform [], [], 'right' 20 | 21 | assert.deepEqual [{i:'y', p:100}, {i:'x', p:0}], text0.transform [{i:'y', p:100}, {i:'x', p:0}], [], 'left' 22 | assert.deepEqual [], text0.transform [], [{i:'y', p:100}, {i:'x', p:0}], 'right' 23 | 24 | it 'inserts', -> 25 | assert.deepEqual [[{i:'x', p:10}], [{i:'a', p:1}]], text0.transformX [{i:'x', p:9}], [{i:'a', p:1}] 26 | assert.deepEqual [[{i:'x', p:10}], [{i:'a', p:11}]], text0.transformX [{i:'x', p:10}], [{i:'a', p:10}] 27 | 28 | assert.deepEqual [[{i:'x', p:10}], [{d:'a', p:9}]], text0.transformX [{i:'x', p:11}], [{d:'a', p:9}] 29 | assert.deepEqual [[{i:'x', p:10}], [{d:'a', p:10}]], text0.transformX [{i:'x', p:11}], [{d:'a', p:10}] 30 | assert.deepEqual [[{i:'x', p:11}], [{d:'a', p:12}]], text0.transformX [{i:'x', p:11}], [{d:'a', p:11}] 31 | 32 | assert.deepEqual [{i:'x', p:10}], text0.transform [{i:'x', p:10}], [{d:'a', p:11}], 'left' 33 | assert.deepEqual [{i:'x', p:10}], text0.transform [{i:'x', p:10}], [{d:'a', p:10}], 'left' 34 | assert.deepEqual [{i:'x', p:10}], text0.transform [{i:'x', p:10}], [{d:'a', p:10}], 'right' 35 | 36 | it 'deletes', -> 37 | assert.deepEqual [[{d:'abc', p:8}], [{d:'xy', p:4}]], text0.transformX [{d:'abc', p:10}], [{d:'xy', p:4}] 38 | assert.deepEqual [[{d:'ac', p:10}], []], text0.transformX [{d:'abc', p:10}], [{d:'b', p:11}] 39 | assert.deepEqual [[], [{d:'ac', p:10}]], text0.transformX [{d:'b', p:11}], [{d:'abc', p:10}] 40 | assert.deepEqual [[{d:'a', p:10}], []], text0.transformX [{d:'abc', p:10}], [{d:'bc', p:11}] 41 | assert.deepEqual [[{d:'c', p:10}], []], text0.transformX [{d:'abc', p:10}], [{d:'ab', p:10}] 42 | assert.deepEqual [[{d:'a', p:10}], [{d:'d', p:10}]], text0.transformX [{d:'abc', p:10}], [{d:'bcd', p:11}] 43 | assert.deepEqual [[{d:'d', p:10}], [{d:'a', p:10}]], text0.transformX [{d:'bcd', p:11}], [{d:'abc', p:10}] 44 | assert.deepEqual [[{d:'abc', p:10}], [{d:'xy', p:10}]], text0.transformX [{d:'abc', p:10}], [{d:'xy', p:13}] 45 | 46 | describe 'transformCursor', -> 47 | it 'is sane', -> 48 | assert.strictEqual 0, text0.transformCursor 0, [], 'right' 49 | assert.strictEqual 0, text0.transformCursor 0, [], 'left' 50 | assert.strictEqual 100, text0.transformCursor 100, [] 51 | 52 | it 'works vs insert', -> 53 | assert.strictEqual 0, text0.transformCursor 0, [{i:'asdf', p:100}], 'right' 54 | assert.strictEqual 0, text0.transformCursor 0, [{i:'asdf', p:100}], 'left' 55 | 56 | assert.strictEqual 204, text0.transformCursor 200, [{i:'asdf', p:100}], 'right' 57 | assert.strictEqual 204, text0.transformCursor 200, [{i:'asdf', p:100}], 'left' 58 | 59 | assert.strictEqual 104, text0.transformCursor 100, [{i:'asdf', p:100}], 'right' 60 | assert.strictEqual 100, text0.transformCursor 100, [{i:'asdf', p:100}], 'left' 61 | 62 | it 'works vs delete', -> 63 | assert.strictEqual 0, text0.transformCursor 0, [{d:'asdf', p:100}], 'right' 64 | assert.strictEqual 0, text0.transformCursor 0, [{d:'asdf', p:100}], 'left' 65 | assert.strictEqual 0, text0.transformCursor 0, [{d:'asdf', p:100}] 66 | 67 | assert.strictEqual 196, text0.transformCursor 200, [{d:'asdf', p:100}] 68 | 69 | assert.strictEqual 100, text0.transformCursor 100, [{d:'asdf', p:100}] 70 | assert.strictEqual 100, text0.transformCursor 102, [{d:'asdf', p:100}] 71 | assert.strictEqual 100, text0.transformCursor 104, [{d:'asdf', p:100}] 72 | assert.strictEqual 101, text0.transformCursor 105, [{d:'asdf', p:100}] 73 | 74 | describe 'normalize', -> 75 | it 'is sane', -> 76 | testUnchanged = (op) -> assert.deepEqual op, text0.normalize op 77 | testUnchanged [] 78 | testUnchanged [{i:'asdf', p:100}] 79 | testUnchanged [{i:'asdf', p:100}, {d:'fdsa', p:123}] 80 | 81 | it 'adds missing p:0', -> 82 | assert.deepEqual [{i:'abc', p:0}], text0.normalize [{i:'abc'}] 83 | assert.deepEqual [{d:'abc', p:0}], text0.normalize [{d:'abc'}] 84 | assert.deepEqual [{i:'abc', p:0}, {d:'abc', p:0}], text0.normalize [{i:'abc'}, {d:'abc'}] 85 | 86 | it 'converts op to an array', -> 87 | assert.deepEqual [{i:'abc', p:0}], text0.normalize {i:'abc', p:0} 88 | assert.deepEqual [{d:'abc', p:0}], text0.normalize {d:'abc', p:0} 89 | 90 | it 'works with a really simple op', -> 91 | assert.deepEqual [{i:'abc', p:0}], text0.normalize {i:'abc'} 92 | 93 | it 'compress inserts', -> 94 | assert.deepEqual [{i:'xyzabc', p:10}], text0.normalize [{i:'abc', p:10}, {i:'xyz', p:10}] 95 | assert.deepEqual [{i:'axyzbc', p:10}], text0.normalize [{i:'abc', p:10}, {i:'xyz', p:11}] 96 | assert.deepEqual [{i:'abcxyz', p:10}], text0.normalize [{i:'abc', p:10}, {i:'xyz', p:13}] 97 | 98 | it 'doesnt compress separate inserts', -> 99 | t = (op) -> assert.deepEqual op, text0.normalize op 100 | 101 | t [{i:'abc', p:10}, {i:'xyz', p:9}] 102 | t [{i:'abc', p:10}, {i:'xyz', p:14}] 103 | 104 | it 'compress deletes', -> 105 | assert.deepEqual [{d:'xyabc', p:8}], text0.normalize [{d:'abc', p:10}, {d:'xy', p:8}] 106 | assert.deepEqual [{d:'xabcy', p:9}], text0.normalize [{d:'abc', p:10}, {d:'xy', p:9}] 107 | assert.deepEqual [{d:'abcxy', p:10}], text0.normalize [{d:'abc', p:10}, {d:'xy', p:10}] 108 | 109 | it 'doesnt compress separate deletes', -> 110 | t = (op) -> assert.deepEqual op, text0.normalize op 111 | 112 | t [{d:'abc', p:10}, {d:'xyz', p:6}] 113 | t [{d:'abc', p:10}, {d:'xyz', p:11}] 114 | 115 | 116 | describe 'randomizer', -> it 'passes', -> 117 | @timeout 4000 118 | @slow 4000 119 | fuzzer text0, require('./text0-generator') 120 | 121 | --------------------------------------------------------------------------------