├── .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 |
--------------------------------------------------------------------------------