├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── client-side.js ├── component.json ├── lib ├── Builder.js ├── Changeset.js ├── ChangesetTransform.js ├── Operator.js ├── TextTransform.js ├── index.js └── operations │ ├── Insert.js │ ├── Mark.js │ ├── Retain.js │ └── Skip.js ├── ot.png ├── package.json ├── test.js └── test └── text.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | client-side.js : * 2 | browserify -e lib/index.js -o client-side.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # changesets [![Build Status](https://travis-ci.org/marcelklehr/changesets.png?branch=master)](https://travis-ci.org/marcelklehr/changesets) 2 | build text-based concurrent multi-user applications using operational transformation! 3 | 4 | Easily create and apply changesets at all sites of a distributed system, leveraging Operational Transformation with: 5 | 6 | * convergence (everybody sees the same state, eventually) 7 | * intention preservation (put the 's' into 'sock' and it'll stay a sock) 8 | * reversibility (undo any edit without problems) 9 | 10 | News: *changesets* now supports the ottypes API spec of shareJS. If you'd like a more unixy, transport agnostic tool, though, check out [gulf](https://github.com/marcelklehr/gulf). 11 | 12 | News: Changesets v1.0.0 corrects the semantics of Changeset#merge, which now requires you to pass a consecutive changeset, instead of one that was created concurrently to the first one. This is inline with shareJS's API spec. 13 | 14 | ## Install 15 | `npm install changesets` or `component install marcelklehr/changesets` 16 | 17 | In node and with component: 18 | 19 | ```js 20 | var Changeset = require('changesets').Changeset 21 | ``` 22 | 23 | In the bare browser: 24 | 25 | ```html 26 | 27 | 32 | ``` 33 | 34 | Support for adding more module systems is greatly appreaciated. 35 | 36 | 37 | The return value of `require('changesets')` or the global `changesets` has a shareJS ottype interface. 38 | 39 | 40 | # Usage 41 | A changeset is an ordered list of operations. There are 3 types of operations: Retain (retains a number of chars), Insert (inserts a number of chars), Skip (deletes them). 42 | 43 | Now, Suppose we have two texts 44 | ```js 45 | var text1 = 'Rockets fly higher than rocks' 46 | , text2 = 'Rockets can fly higher than rocks, usually' 47 | ``` 48 | 49 | To construct a changeset by hand, just do 50 | ```js 51 | var cs = Changeset.create() 52 | .retain(8) 53 | .insert('can ') 54 | .retain(21) 55 | .insert(', usually') 56 | .end() 57 | ``` 58 | 59 | You can also directly pass a diff created with [diff_match_patch](https://github.com/marcelklehr/diff_match_patch), so to construct a changeset between two texts: 60 | ```js 61 | var dmp = require('diff_match_patch') 62 | , engine = new dmp.diff_match_patch 63 | 64 | var diff = engine.diff_main(text1, text2) 65 | var changeset = Changeset.fromDiff(diff) 66 | ``` 67 | 68 | Changesets can be applied to a text as follows: 69 | ```js 70 | var applied = changeset.apply(text1) 71 | 72 | applied == text2 // true 73 | ``` 74 | 75 | In many cases you will find the need to serialize your changesets in order to efficiently transfer them through the network or store them on disk. 76 | ```js 77 | var serialized = changeset.pack() // '=5-1+2=2+5=6+b|habeen -ish thing.|i' 78 | ``` 79 | 80 | `Changeset.unpack()` takes the output of `Changeset#pack()` and returns a changeset object. 81 | ```js 82 | Changeset.unpack(serialized) // {"0":{"length":5,"symbol":"="},"1":{"length":1,"symbol":"-"},"2":{"length":2,"symbol":"+"},"3":{"length":2,"sym ... 83 | ``` 84 | 85 | If you'd like to display a changeset in a more humanly readable form, use `Changeset#inspect` (which is aliased to Changeset#toString): 86 | 87 | ```js 88 | changeset.inspect() // "=====-ha==been ======-ish thing." 89 | ``` 90 | 91 | Retained chars are displayed as `=` and removed chars as `-`. Insertions are displayed as the characters being inserted. 92 | 93 | ### Transforming them 94 | 95 | #### Inclusion Transformation 96 | Say, for instance, you give a text to two different people. Each of them makes some changes and hands them back to you. 97 | 98 | ```js 99 | var rev0 = "Hello adventurer!" 100 | , revA = "Hello treasured adventurer!" 101 | , revB = "Good day adventurers, y'all!" 102 | ``` 103 | 104 | As a human you're certainly able to make out the changes and tell what's been changed to combine both revisions, for your computer it's harder. 105 | Firstly, you'll need to extract the changes in each version. 106 | 107 | ```js 108 | var csA = computeChanges(rev0, revA) 109 | var csB = computeChanges(rev0, revB) 110 | ``` 111 | 112 | Now we can send the changes of `revA` from side A over the network to B and if we apply them on the original revision we get the full contents of revision A again. 113 | 114 | ```js 115 | csA.apply(rev0) == revA // true 116 | ``` 117 | 118 | But we don't want to apply them on the original revision, because we've already changed the text and created `revB`. We could of course try and apply it anyway: 119 | 120 | ```js 121 | csA.apply(revB) // apply csA on revision B -> "Good dtreasured ay adventurer!" 122 | ``` 123 | 124 | Ah, bad idea. 125 | 126 | Since changeset A still assumes the original context, we need to adapt it, based on the changes of changeset B that have happened in the meantime, In order to be able to apply it on `revB`. 127 | 128 | ```js 129 | var transformedCsA = csA.transformAgainst(csB) 130 | 131 | transformedCsA.apply(revB) // "Good day treasured adventurers, y'all!" 132 | ``` 133 | 134 | This transformation is called *Inclusion Transformation*, which adjusts a changeset in a way so that it assumes the changes of another changeset already happened. 135 | 136 | #### Exclusion Transformation 137 | Imagine a text editor, that allows users to undo any edit they've ever done to a document without undoing all edits that were done afterwards. 138 | 139 | We decide to store all edits in a list of changesets, where each applied on top of the other results in the currently visible document. 140 | 141 | Let's assume the following document with 4 revisions and 3 edits. 142 | 143 | ```js 144 | var versions = 145 | [ "" 146 | , "a" 147 | , "ab" 148 | , "abc" 149 | ] 150 | 151 | // For posterity we create the edits like this 152 | 153 | var edits = [] 154 | for (var i=1; i < versions.length; i++) { 155 | edits.push( computeChanges(text[i-1], text[i]) ) 156 | } 157 | ``` 158 | 159 | We can undo the last edit, by removing it from the stack of edits, inverting it and applying it on the current text. 160 | 161 | ```js 162 | var lastEdit = edits.pop() 163 | newEditorContent = lastEdit.invert().apply(currentEditorContent) 164 | ``` 165 | 166 | Now, if we want to undo *any* edit, let's say the second instead of the last, we need to construct the inverse changeset of that second edit. 167 | 168 | ```js 169 | 170 | ``` 171 | 172 | Then, we transform all following edits against this inverse changeset. But in order for this "undo changeset" to fit for the next changeset also, we in turn transform it against all previously iterated edits. 173 | 174 | ```js 175 | var undoIndex = 1 176 | 177 | var undoCs = edits[undoIndex].invert() 178 | 179 | var newEdits = [], edit 180 | for (var i=undoIndex+1; i < edits.length; i++) { 181 | edit = edits[i] 182 | newEdits[i] = edit.transformAgainst(undoCs) 183 | undoCs = undoCs.transformAgainst(edit) 184 | } 185 | ``` 186 | 187 | This way we can effectively exclude any given changes from all changes that follow it. This is called *Exclusion Transformation*. 188 | 189 | ### Attributes 190 | As you know, there are 3 types of operations (`Retain`, `Skip` and `Insert`) in a changeset, but actually, there are four. The forth is an operation type called `Mark`. 191 | 192 | Mark can be used to apply attributes to a text. Currently attributes are like binary flags: Either a char has an attribute or it doesn't. Attributes are integer numbers (you'll need to implement some mapping between attribute names and these ids). You can pass attributes to the `Mark` operation as follows: 193 | 194 | ```js 195 | var mark = new Mark(/*length:*/5, { 196 | 0: 1 197 | , 7: 1 198 | , 3: 1 199 | , 15: 1 200 | , -2: 1 201 | , 11: 1 202 | }) 203 | ``` 204 | 205 | Did you notice the negative number? While positive numbers result in the application of some attribute, negative numbers enforce the removal of an attribute that has already been applied on some range of the text. 206 | 207 | Now, how can you deal with those attributes? Currently, you'll have to keep changes to attributes in separate changesets. Storing attributes for a document can be done in a changeset with the length of the document into which you merge attribute changes. Applying them is as easy as iterating over the operations of that changeset (`changeset.forEach(fn..)`) and i.e. inserting HTML tags at respective positions in the corresponding document. 208 | 209 | *Warning:* Attributes are still experimental. There are no tests, yet, and the API may change in the future. 210 | 211 | ## Todo 212 | 213 | * Maybe support TP2? ([lightwave](https://code.google.com/p/lightwave/source/browse/trunk/experimental/ot/README) solves the FT puzzle by retaining deleted chars) 214 | * vows is super ugly. Switch to mocha! 215 | 216 | ## License 217 | MIT 218 | 219 | ## Changelog 220 | 221 | 1.0.0 222 | * Change semantics of Changeset#merge to adhere to logic as well as shareJS spec 223 | 224 | 0.4.0 225 | * Modularize operations 226 | * Attributes (Mark operation) 227 | * shareJS support as an ot type 228 | 229 | 0.3.1 230 | * fix Changeset#unpack() regex to allow for ops longer than 35 chars (thanks to @jonasp) 231 | 232 | 0.3.0 233 | * complete revamp of the algorithms and data structures 234 | * support for merging changesets 235 | -------------------------------------------------------------------------------- /client-side.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 47 | * 48 | * (MIT LICENSE) 49 | * Permission is hereby granted, free of charge, to any person obtaining a copy 50 | * of this software and associated documentation files (the "Software"), to deal 51 | * in the Software without restriction, including without limitation the rights 52 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 53 | * copies of the Software, and to permit persons to whom the Software is 54 | * furnished to do so, subject to the following conditions: 55 | * 56 | * The above copyright notice and this permission notice shall be included in 57 | * all copies or substantial portions of the Software. 58 | * 59 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 60 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 61 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 62 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 63 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 64 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 65 | * THE SOFTWARE. 66 | */ 67 | 68 | /** 69 | * A sequence of consecutive operations 70 | * 71 | * @param ops.. all passed operations will be added to the changeset 72 | */ 73 | function Changeset(ops/*or ops..*/) { 74 | this.addendum = "" 75 | this.removendum = "" 76 | this.inputLength = 0 77 | this.outputLength = 0 78 | 79 | if(!Array.isArray(ops)) ops = arguments 80 | for(var i=0; i start) { 118 | if(op.input) { 119 | if(op.length != Infinity) oplen = op.length -Math.max(0, start-pos) -Math.max(0, (op.length+pos)-(start+len)) 120 | else oplen = len 121 | range.push( op.derive(oplen) ) // (Don't copy over more than len param allows) 122 | } 123 | else { 124 | range.push( op.derive(op.length) ) 125 | oplen = 0 126 | } 127 | l += oplen 128 | } 129 | pos += op.input 130 | } 131 | return range 132 | } 133 | 134 | /** 135 | * Merge two changesets (that are based on the same state!) so that the resulting changseset 136 | * has the same effect as both orignal ones applied one after the other 137 | * 138 | * @param otherCs 139 | * @param left Which op to choose if there's an insert tie (If you use this function in a distributed, synchronous environment, be sure to invert this param on the other site, otherwise it can be omitted safely)) 140 | * @returns 141 | */ 142 | Changeset.prototype.merge = function(otherCs, left) { 143 | if(!(otherCs instanceof Changeset)) { 144 | throw new Error('Argument must be a #, but received '+otherCs.__proto__.constructor.name) 145 | } 146 | 147 | if(otherCs.inputLength !== this.outputLength) { 148 | throw new Error("Changeset lengths for merging don't match! Input length of younger cs: "+otherCs.inputLength+', output length of older cs:'+this.outputLength) 149 | } 150 | 151 | var newops = [] 152 | , addPtr1 = 0 153 | , remPtr1 = 0 154 | , addPtr2 = 0 155 | , remPtr2 = 0 156 | , newaddendum = '' 157 | , newremovendum = '' 158 | 159 | zip(this, otherCs, function(op1, op2) { 160 | // console.log(newops) 161 | // console.log(op1, op2) 162 | 163 | // I'm deleting something -- the other cs can't know that, so just overtake my op 164 | if(op1 && !op1.output) { 165 | newops.push(op1.merge().clone()) 166 | newremovendum += this.removendum.substr(remPtr1, op1.length) // overtake added chars 167 | remPtr1 += op1.length 168 | op1.length = 0 // don't gimme that one again. 169 | return 170 | } 171 | 172 | // op2 is an insert 173 | if(op2 && !op2.input) { 174 | newops.push(op2.merge().clone()) 175 | newaddendum += otherCs.addendum.substr(addPtr2, op2.length) // overtake added chars 176 | addPtr2 += op2.length 177 | op2.length = 0 // don't gimme that one again. 178 | return 179 | } 180 | 181 | // op2 is either a retain or a skip 182 | if(op2 && op2.input && op1) { 183 | // op2 retains whatever we do here (retain or insert), so just clone my op 184 | if(op2.output) { 185 | newops.push(op1.merge(op2).clone()) 186 | if(!op1.input) { // overtake addendum 187 | newaddendum += this.addendum.substr(addPtr1, op1.length) 188 | addPtr1 += op1.length 189 | } 190 | op1.length = 0 // don't gimme these again 191 | op2.length = 0 192 | }else 193 | 194 | // op2 deletes my retain here, so just clone the delete 195 | // (op1 can only be a retain and no skip here, cause we've handled skips above already) 196 | if(!op2.output && op1.input) { 197 | newops.push(op2.merge(op1).clone()) 198 | newremovendum += otherCs.removendum.substr(remPtr2, op2.length) // overtake added chars 199 | remPtr2 += op2.length 200 | op1.length = 0 // don't gimme these again 201 | op2.length = 0 202 | }else 203 | 204 | //otherCs deletes something I added (-1) +1 = 0 205 | { 206 | addPtr1 += op1.length 207 | op1.length = 0 // don't gimme these again 208 | op2.length = 0 209 | } 210 | return 211 | } 212 | 213 | console.log('oops', arguments) 214 | throw new Error('oops. This case hasn\'t been considered by the developer (error code: PBCAC)') 215 | }.bind(this)) 216 | 217 | var newCs = new Changeset(newops) 218 | newCs.addendum = newaddendum 219 | newCs.removendum = newremovendum 220 | 221 | return newCs 222 | } 223 | 224 | /** 225 | * A private and quite handy function that slices ops into equally long pieces and applies them on a mapping function 226 | * that can determine the iteration steps by setting op.length to 0 on an op (equals using .next() in a usual iterator) 227 | */ 228 | function zip(cs1, cs2, func) { 229 | var opstack1 = cs1.map(function(op) {return op.clone()}) // copy ops 230 | , opstack2 = cs2.map(function(op) {return op.clone()}) 231 | 232 | var op2, op1 233 | while(opstack1.length || opstack2.length) {// iterate through all outstanding ops of this cs 234 | op1 = opstack1[0]? opstack1[0].clone() : null 235 | op2 = opstack2[0]? opstack2[0].clone() : null 236 | 237 | if(op1) { 238 | if(op2) op1 = op1.derive(Math.min(op1.length, op2.length)) // slice 'em into equally long pieces 239 | if(opstack1[0].length > op1.length) opstack1[0] = opstack1[0].derive(opstack1[0].length-op1.length) 240 | else opstack1.shift() 241 | } 242 | 243 | if(op2) { 244 | if(op1) op2 = op2.derive(Math.min(op1.length, op2.length)) // slice 'em into equally long pieces 245 | if(opstack2[0].length > op2.length) opstack2[0] = opstack2[0].derive(opstack2[0].length-op2.length) 246 | else opstack2.shift() 247 | } 248 | 249 | func(op1, op2) 250 | 251 | if(op1 && op1.length) opstack1.unshift(op1) 252 | if(op2 && op2.length) opstack2.unshift(op2) 253 | } 254 | } 255 | 256 | /** 257 | * Inclusion Transformation (IT) or Forward Transformation 258 | * 259 | * transforms the operations of the current changeset against the 260 | * all operations in another changeset in such a way that the 261 | * effects of the latter are effectively included. 262 | * This is basically like a applying the other cs on this one. 263 | * 264 | * @param otherCs 265 | * @param left Which op to choose if there's an insert tie (If you use this function in a distributed, synchronous environment, be sure to invert this param on the other site, otherwise it can be omitted safely) 266 | * 267 | * @returns 268 | */ 269 | Changeset.prototype.transformAgainst = function(otherCs, left) { 270 | if(!(otherCs instanceof Changeset)) { 271 | throw new Error('Argument to Changeset#transformAgainst must be a #, but received '+otherCs.__proto__.constructor.name) 272 | } 273 | 274 | if(this.inputLength != otherCs.inputLength) { 275 | throw new Error('Can\'t transform changesets with differing inputLength: '+this.inputLength+' and '+otherCs.inputLength) 276 | } 277 | 278 | var transformation = new ChangesetTransform(this, [new Retain(Infinity)]) 279 | otherCs.forEach(function(op) { 280 | var nextOp = this.subrange(transformation.pos, Infinity)[0] // next op of this cs 281 | if(nextOp && !nextOp.input && !op.input && left) { // two inserts tied; left breaks it 282 | transformation.writeOutput(transformation.readInput(nextOp.length)) 283 | } 284 | op.apply(transformation) 285 | }.bind(this)) 286 | 287 | return transformation.result() 288 | } 289 | 290 | /** 291 | * Exclusion Transformation (ET) or Backwards Transformation 292 | * 293 | * transforms all operations in the current changeset against the operations 294 | * in another changeset in such a way that the impact of the latter are effectively excluded 295 | * 296 | * @param changeset the changeset to substract from this one 297 | * @param left Which op to choose if there's an insert tie (If you use this function in a distributed, synchronous environment, be sure to invert this param on the other site, otherwise it can be omitted safely) 298 | * @returns 299 | */ 300 | Changeset.prototype.substract = function(changeset, left) { 301 | // The current operations assume that the changes in 302 | // `changeset` happened before, so for each of those ops 303 | // we create an operation that undoes its effect and 304 | // transform all our operations on top of the inverse changes 305 | return this.transformAgainst(changeset.invert(), left) 306 | } 307 | 308 | /** 309 | * Returns the inverse Changeset of the current one 310 | * 311 | * Changeset.invert().apply(Changeset.apply(document)) == document 312 | */ 313 | Changeset.prototype.invert = function() { 314 | // invert all ops 315 | var newCs = new Changeset(this.map(function(op) { 316 | return op.invert() 317 | })) 318 | 319 | // removendum becomes addendum and vice versa 320 | newCs.addendum = this.removendum 321 | newCs.removendum = this.addendum 322 | 323 | return newCs 324 | } 325 | 326 | /** 327 | * Applies this changeset on a text 328 | */ 329 | Changeset.prototype.apply = function(input) { 330 | // pre-requisites 331 | if(input.length != this.inputLength) throw new Error('Input length doesn\'t match expected length. expected: '+this.inputLength+'; actual: '+input.length) 332 | 333 | var operation = new TextTransform(input, this.addendum, this.removendum) 334 | 335 | this.forEach(function(op) { 336 | // each Operation has access to all pointers as well as the input, addendum and removendum (the latter are immutable) 337 | op.apply(operation) 338 | }.bind(this)) 339 | 340 | return operation.result() 341 | } 342 | 343 | /** 344 | * Returns an array of strings describing this changeset's operations 345 | */ 346 | Changeset.prototype.inspect = function() { 347 | var j = 0 348 | return this.map(function(op) { 349 | var string = '' 350 | 351 | if(!op.input) { // if Insert 352 | string = this.addendum.substr(j,op.length) 353 | j += op.length 354 | return string 355 | } 356 | 357 | for(var i=0; i The changeset to be serialized 369 | * @returns The serialized changeset 370 | */ 371 | Changeset.prototype.pack = function() { 372 | var packed = this.map(function(op) { 373 | return op.pack() 374 | }).join('') 375 | 376 | var addendum = this.addendum.replace(/%/g, '%25').replace(/\|/g, '%7C') 377 | , removendum = this.removendum.replace(/%/g, '%25').replace(/\|/g, '%7C') 378 | return packed+'|'+addendum+'|'+removendum 379 | } 380 | Changeset.prototype.toString = function() { 381 | return this.pack() 382 | } 383 | 384 | /** 385 | * Unserializes the output of cs.text.Changeset#toString() 386 | * 387 | * @param packed The serialized changeset 388 | * @param 389 | */ 390 | Changeset.unpack = function(packed) { 391 | if(packed == '') throw new Error('Cannot unpack from empty string') 392 | var components = packed.split('|') 393 | , opstring = components[0] 394 | , addendum = components[1].replace(/%7c/gi, '|').replace(/%25/g, '%') 395 | , removendum = components[2].replace(/%7c/gi, '|').replace(/%25/g, '%') 396 | 397 | var matches = opstring.match(/[=+-]([^=+-])+/g) 398 | if(!matches) throw new Error('Cannot unpack invalidly serialized op string') 399 | 400 | var ops = [] 401 | matches.forEach(function(s) { 402 | var symbol = s.substr(0,1) 403 | , data = s.substr(1) 404 | if(Skip.prototype.symbol == symbol) return ops.push(Skip.unpack(data)) 405 | if(Insert.prototype.symbol == symbol) return ops.push(Insert.unpack(data)) 406 | if(Retain.prototype.symbol == symbol) return ops.push(Retain.unpack(data)) 407 | throw new Error('Invalid changeset representation passed to Changeset.unpack') 408 | }) 409 | 410 | var cs = new Changeset(ops) 411 | cs.addendum = addendum 412 | cs.removendum = removendum 413 | 414 | return cs 415 | } 416 | 417 | Changeset.create = function() { 418 | return new Builder 419 | } 420 | 421 | /** 422 | * Returns a Changeset containing the operations needed to transform text1 into text2 423 | * 424 | * @param text1 425 | * @param text2 426 | */ 427 | Changeset.fromDiff = function(diff) { 428 | /** 429 | * The data structure representing a diff is an array of tuples: 430 | * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] 431 | * which means: delete 'Hello', add 'Goodbye' and keep ' world.' 432 | */ 433 | var DIFF_DELETE = -1; 434 | var DIFF_INSERT = 1; 435 | var DIFF_EQUAL = 0; 436 | 437 | var ops = [] 438 | , removendum = '' 439 | , addendum = '' 440 | 441 | diff.forEach(function(d) { 442 | if (DIFF_DELETE == d[0]) { 443 | ops.push(new Skip(d[1].length)) 444 | removendum += d[1] 445 | } 446 | 447 | if (DIFF_INSERT == d[0]) { 448 | ops.push(new Insert(d[1].length)) 449 | addendum += d[1] 450 | } 451 | 452 | if(DIFF_EQUAL == d[0]) { 453 | ops.push(new Retain(d[1].length)) 454 | } 455 | }) 456 | 457 | var cs = new Changeset(ops) 458 | cs.addendum = addendum 459 | cs.removendum = removendum 460 | return cs 461 | } 462 | 463 | },{"./Builder":1,"./ChangesetTransform":3,"./TextTransform":5,"./operations/Insert":7,"./operations/Retain":8,"./operations/Skip":9}],3:[function(require,module,exports){ 464 | /*! 465 | * changesets 466 | * A Changeset library incorporating operational ChangesetTransform (OT) 467 | * Copyright 2012 by Marcel Klehr 468 | * 469 | * (MIT LICENSE) 470 | * Permission is hereby granted, free of charge, to any person obtaining a copy 471 | * of this software and associated documentation files (the "Software"), to deal 472 | * in the Software without restriction, including without limitation the rights 473 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 474 | * copies of the Software, and to permit persons to whom the Software is 475 | * furnished to do so, subject to the following conditions: 476 | * 477 | * The above copyright notice and this permission notice shall be included in 478 | * all copies or substantial portions of the Software. 479 | * 480 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 481 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 482 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 483 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 484 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 485 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 486 | * THE SOFTWARE. 487 | */ 488 | 489 | var Retain = require('./operations/Retain') 490 | , Skip = require('./operations/Skip') 491 | , Insert = require('./operations/Insert') 492 | , Changeset = require('./Changeset') 493 | 494 | 495 | function ChangesetTransform(inputCs, addendum) { 496 | this.output = [] 497 | this.addendum = addendum 498 | this.newRemovendum = '' 499 | this.newAddendum = '' 500 | 501 | this.cs = inputCs 502 | this.pos = 0 503 | this.addendumPointer = 0 504 | this.removendumPointer = 0 505 | } 506 | module.exports = ChangesetTransform 507 | 508 | ChangesetTransform.prototype.readInput = function (len) { 509 | var ret = this.cs.subrange(this.pos, len) 510 | this.pos += len 511 | return ret 512 | } 513 | 514 | ChangesetTransform.prototype.readAddendum = function (len) { 515 | //return [new Retain(len)] 516 | var ret = this.subrange(this.addendum, this.addendumPointer, len) 517 | this.addendumPointer += len 518 | return ret 519 | } 520 | 521 | ChangesetTransform.prototype.writeRemovendum = function (range) { 522 | range 523 | .filter(function(op) {return !op.output}) 524 | .forEach(function(op) { 525 | this.removendumPointer += op.length 526 | }.bind(this)) 527 | } 528 | 529 | ChangesetTransform.prototype.writeOutput = function (range) { 530 | this.output = this.output.concat(range) 531 | range 532 | .filter(function(op) {return !op.output}) 533 | .forEach(function(op) { 534 | this.newRemovendum += this.cs.removendum.substr(this.removendumPointer, op.length) 535 | this.removendumPointer += op.length 536 | }.bind(this)) 537 | } 538 | 539 | ChangesetTransform.prototype.subrange = function (range, start, len) { 540 | if(len) return this.cs.subrange.call(range, start, len) 541 | else return range.filter(function(op){ return !op.input}) 542 | } 543 | 544 | ChangesetTransform.prototype.result = function() { 545 | this.writeOutput(this.readInput(Infinity)) 546 | var newCs = new Changeset(this.output) 547 | newCs.addendum = this.cs.addendum 548 | newCs.removendum = this.newRemovendum 549 | return newCs 550 | } 551 | 552 | },{"./Changeset":2,"./operations/Insert":7,"./operations/Retain":8,"./operations/Skip":9}],4:[function(require,module,exports){ 553 | function Operator() { 554 | } 555 | 556 | module.exports = Operator 557 | 558 | Operator.prototype.clone = function() { 559 | return this.derive(this.length) 560 | } 561 | 562 | Operator.prototype.derive = function(len) { 563 | return new (this.constructor)(len) 564 | } 565 | 566 | Operator.prototype.pack = function() { 567 | return this.symbol + (this.length).toString(36) 568 | } 569 | 570 | },{}],5:[function(require,module,exports){ 571 | /*! 572 | * changesets 573 | * A Changeset library incorporating operational Apply (OT) 574 | * Copyright 2012 by Marcel Klehr 575 | * 576 | * (MIT LICENSE) 577 | * Permission is hereby granted, free of charge, to any person obtaining a copy 578 | * of this software and associated documentation files (the "Software"), to deal 579 | * in the Software without restriction, including without limitation the rights 580 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 581 | * copies of the Software, and to permit persons to whom the Software is 582 | * furnished to do so, subject to the following conditions: 583 | * 584 | * The above copyright notice and this permission notice shall be included in 585 | * all copies or substantial portions of the Software. 586 | * 587 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 588 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 589 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 590 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 591 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 592 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 593 | * THE SOFTWARE. 594 | */ 595 | 596 | var Retain = require('./operations/Retain') 597 | , Skip = require('./operations/Skip') 598 | , Insert = require('./operations/Insert') 599 | , Insert = require('./Changeset') 600 | 601 | 602 | function TextTransform(input, addendum, removendum) { 603 | this.output = '' 604 | 605 | this.input = input 606 | this.addendum = addendum 607 | this.removendum = removendum 608 | this.pos = 0 609 | this.addPos = 0 610 | this.remPos = 0 611 | } 612 | module.exports = TextTransform 613 | 614 | TextTransform.prototype.readInput = function (len) { 615 | var ret = this.input.substr(this.pos, len) 616 | this.pos += len 617 | return ret 618 | } 619 | 620 | TextTransform.prototype.readAddendum = function (len) { 621 | var ret = this.addendum.substr(this.addPos, len) 622 | this.addPos += len 623 | return ret 624 | } 625 | 626 | TextTransform.prototype.writeRemovendum = function (range) { 627 | //var expected = this.removendum.substr(this.remPos, range.length) 628 | //if(range != expected) throw new Error('Removed chars don\'t match removendum. expected: '+expected+'; actual: '+range) 629 | this.remPos += range.length 630 | } 631 | 632 | TextTransform.prototype.writeOutput = function (range) { 633 | this.output += range 634 | } 635 | 636 | TextTransform.prototype.subrange = function (range, start, len) { 637 | return range.substr(start, len) 638 | } 639 | 640 | TextTransform.prototype.result = function() { 641 | this.writeOutput(this.readInput(Infinity)) 642 | return this.output 643 | } 644 | 645 | },{"./Changeset":2,"./operations/Insert":7,"./operations/Retain":8,"./operations/Skip":9}],6:[function(require,module,exports){ 646 | /*! 647 | * changesets 648 | * A Changeset library incorporating operational transformation (OT) 649 | * Copyright 2012 by Marcel Klehr 650 | * 651 | * (MIT LICENSE) 652 | * Permission is hereby granted, free of charge, to any person obtaining a copy 653 | * of this software and associated documentation files (the "Software"), to deal 654 | * in the Software without restriction, including without limitation the rights 655 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 656 | * copies of the Software, and to permit persons to whom the Software is 657 | * furnished to do so, subject to the following conditions: 658 | * 659 | * The above copyright notice and this permission notice shall be included in 660 | * all copies or substantial portions of the Software. 661 | * 662 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 663 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 664 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 665 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 666 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 667 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 668 | * THE SOFTWARE. 669 | */ 670 | 671 | var Changeset = require('./Changeset') 672 | , Retain = require('./operations/Retain') 673 | , Skip = require('./operations/Skip') 674 | , Insert = require('./operations/Insert') 675 | 676 | exports.Operator = require('./Operator') 677 | exports.Changeset = Changeset 678 | exports.Insert = Insert 679 | exports.Retain = Retain 680 | exports.Skip = Skip 681 | 682 | if('undefined' != typeof window) window.changesets = exports 683 | 684 | /** 685 | * Serializes the given changeset in order to return a (hopefully) more compact representation 686 | * that can be sent through a network or stored in a database 687 | * @alias cs.text.Changeset#pack 688 | */ 689 | exports.pack = function(cs) { 690 | return cs.pack() 691 | } 692 | 693 | /** 694 | * Unserializes the output of cs.text.pack 695 | * @alias cs.text.Changeset.unpack 696 | */ 697 | exports.unpack = function(packed) { 698 | return Changeset.unpack(packed) 699 | } 700 | 701 | 702 | 703 | 704 | /** 705 | * shareJS ot type API sepc support 706 | */ 707 | 708 | exports.name = 'changesets' 709 | exports.url = 'https://github.com/marcelklehr/changesets' 710 | 711 | /** 712 | * create([initialText]) 713 | * 714 | * creates a snapshot (optionally with supplied intial text) 715 | */ 716 | exports.create = function(initText) { 717 | return initText || '' 718 | } 719 | 720 | /** 721 | * Apply a changeset on a snapshot creating a new one 722 | * 723 | * The old snapshot object mustn't be used after calling apply on it 724 | * returns the resulting 725 | */ 726 | exports.apply = function(snapshot, op) { 727 | op = exports.unpack(op) 728 | return op.apply(snapshot) 729 | } 730 | 731 | /** 732 | * Transform changeset1 against changeset2 733 | */ 734 | exports.transform = function (op1, op2, side) { 735 | op1 = exports.unpack(op1) 736 | op2 = exports.unpack(op2) 737 | return exports.pack(op1.transformAgainst(op2, ('left'==side))) 738 | } 739 | 740 | /** 741 | * Merge two changesets into one 742 | */ 743 | exports.compose = function (op1, op2) { 744 | op1 = exports.unpack(op1) 745 | op2 = exports.unpack(op2) 746 | return exports.pack(op1.merge(op2)) 747 | } 748 | 749 | /** 750 | * Invert a changeset 751 | */ 752 | exports.invert = function(op) { 753 | return op.invert() 754 | } 755 | 756 | },{"./Changeset":2,"./Operator":4,"./operations/Insert":7,"./operations/Retain":8,"./operations/Skip":9}],7:[function(require,module,exports){ 757 | /*! 758 | * changesets 759 | * A Changeset library incorporating operational transformation (OT) 760 | * Copyright 2012 by Marcel Klehr 761 | * 762 | * (MIT LICENSE) 763 | * Permission is hereby granted, free of charge, to any person obtaining a copy 764 | * of this software and associated documentation files (the "Software"), to deal 765 | * in the Software without restriction, including without limitation the rights 766 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 767 | * copies of the Software, and to permit persons to whom the Software is 768 | * furnished to do so, subject to the following conditions: 769 | * 770 | * The above copyright notice and this permission notice shall be included in 771 | * all copies or substantial portions of the Software. 772 | * 773 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 774 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 775 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 776 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 777 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 778 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 779 | * THE SOFTWARE. 780 | */ 781 | 782 | var Operator = require('../Operator') 783 | 784 | /** 785 | * Insert Operator 786 | * Defined by: 787 | * - length 788 | * - input=0 789 | * - output=length 790 | * 791 | * @param length How many chars to be inserted 792 | */ 793 | function Insert(length) { 794 | this.length = length 795 | this.input = 0 796 | this.output = length 797 | } 798 | 799 | // True inheritance 800 | Insert.prototype = Object.create(Operator.prototype, { 801 | constructor: { 802 | value: Insert, 803 | enumerable: false, 804 | writable: true, 805 | configurable: true 806 | } 807 | }); 808 | module.exports = Insert 809 | Insert.prototype.symbol = '+' 810 | 811 | var Skip = require('./Skip') 812 | , Retain = require('./Retain') 813 | 814 | Insert.prototype.apply = function(t) { 815 | t.writeOutput(t.readAddendum(this.output)) 816 | } 817 | 818 | Insert.prototype.merge = function() { 819 | return this 820 | } 821 | 822 | Insert.prototype.invert = function() { 823 | return new Skip(this.length) 824 | } 825 | 826 | Insert.unpack = function(data) { 827 | return new Insert(parseInt(data, 36)) 828 | } 829 | 830 | },{"../Operator":4,"./Retain":8,"./Skip":9}],8:[function(require,module,exports){ 831 | /*! 832 | * changesets 833 | * A Changeset library incorporating operational transformation (OT) 834 | * Copyright 2012 by Marcel Klehr 835 | * 836 | * (MIT LICENSE) 837 | * Permission is hereby granted, free of charge, to any person obtaining a copy 838 | * of this software and associated documentation files (the "Software"), to deal 839 | * in the Software without restriction, including without limitation the rights 840 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 841 | * copies of the Software, and to permit persons to whom the Software is 842 | * furnished to do so, subject to the following conditions: 843 | * 844 | * The above copyright notice and this permission notice shall be included in 845 | * all copies or substantial portions of the Software. 846 | * 847 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 848 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 849 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 850 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 851 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 852 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 853 | * THE SOFTWARE. 854 | */ 855 | 856 | var Operator = require('../Operator') 857 | 858 | /** 859 | * Retain Operator 860 | * Defined by: 861 | * - length 862 | * - input=output=length 863 | * 864 | * @param length How many chars to retain 865 | */ 866 | function Retain(length) { 867 | this.length = length 868 | this.input = length 869 | this.output = length 870 | } 871 | 872 | // True inheritance 873 | Retain.prototype = Object.create(Operator.prototype, { 874 | constructor: { 875 | value: Retain, 876 | enumerable: false, 877 | writable: true, 878 | configurable: true 879 | } 880 | }); 881 | module.exports = Retain 882 | Retain.prototype.symbol = '=' 883 | 884 | Retain.prototype.apply = function(t) { 885 | t.writeOutput(t.readInput(this.input)) 886 | } 887 | 888 | Retain.prototype.invert = function() { 889 | return this 890 | } 891 | 892 | Retain.prototype.merge = function(op2) { 893 | return this 894 | } 895 | 896 | Retain.unpack = function(data) { 897 | return new Retain(parseInt(data, 36)) 898 | } 899 | 900 | },{"../Operator":4}],9:[function(require,module,exports){ 901 | /*! 902 | * changesets 903 | * A Changeset library incorporating operational transformation (OT) 904 | * Copyright 2012 by Marcel Klehr 905 | * 906 | * (MIT LICENSE) 907 | * Permission is hereby granted, free of charge, to any person obtaining a copy 908 | * of this software and associated documentation files (the "Software"), to deal 909 | * in the Software without restriction, including without limitation the rights 910 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 911 | * copies of the Software, and to permit persons to whom the Software is 912 | * furnished to do so, subject to the following conditions: 913 | * 914 | * The above copyright notice and this permission notice shall be included in 915 | * all copies or substantial portions of the Software. 916 | * 917 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 918 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 919 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 920 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 921 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 922 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 923 | * THE SOFTWARE. 924 | */ 925 | 926 | var Operator = require('../Operator') 927 | 928 | /** 929 | * Skip Operator 930 | * Defined by: 931 | * - length 932 | * - input=length 933 | * - output=0 934 | * 935 | * @param length How many chars to be Skip 936 | */ 937 | function Skip(length) { 938 | this.length = length 939 | this.input = length 940 | this.output = 0 941 | } 942 | 943 | // True inheritance 944 | Skip.prototype = Object.create(Operator.prototype, { 945 | constructor: { 946 | value: Skip, 947 | enumerable: false, 948 | writable: true, 949 | configurable: true 950 | } 951 | }); 952 | module.exports = Skip 953 | Skip.prototype.symbol = '-' 954 | 955 | var Insert = require('./Insert') 956 | , Retain = require('./Retain') 957 | , Changeset = require('../Changeset') 958 | 959 | Skip.prototype.apply = function(t) { 960 | var input = t.readInput(this.input) 961 | t.writeRemovendum(input) 962 | t.writeOutput(t.subrange(input, 0, this.output)) // retain Inserts in my range 963 | } 964 | 965 | Skip.prototype.merge = function(op2) { 966 | return this 967 | } 968 | 969 | Skip.prototype.invert = function() { 970 | return new Insert(this.length) 971 | } 972 | 973 | Skip.unpack = function(data) { 974 | return new Skip(parseInt(data, 36)) 975 | } 976 | 977 | },{"../Changeset":2,"../Operator":4,"./Insert":7,"./Retain":8}]},{},[6]) -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "changesets", 3 | "repo": "marcelklehr/changesets", 4 | "description": "A Changeset library incorporating an operational transformation (OT) algorithm -- for node and the browser!", 5 | "version": "1.0.0", 6 | "keywords": [ 7 | "operational transformation", 8 | "ot", 9 | "changesets", 10 | "diff", 11 | "Forward Transformation", 12 | "Backward Transformation", 13 | "Inclusion Transformation", 14 | "Exclusion Transformation", 15 | "collaborative", 16 | "undo", 17 | "text" 18 | ], 19 | "dependencies": { 20 | 21 | }, 22 | "license": "MIT", 23 | "main": "lib/index.js", 24 | "scripts": [ 25 | "lib/index.js", 26 | "lib/Operator.js", 27 | "lib/Changeset.js", 28 | "lib/operations/Skip.js", 29 | "lib/operations/Insert.js", 30 | "lib/operations/Retain.js", 31 | "lib/Builder.js", 32 | "lib/TextTransform.js", 33 | "lib/ChangesetTransform.js" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /lib/Builder.js: -------------------------------------------------------------------------------- 1 | var Changeset = require('./Changeset') 2 | , Retain = require('./operations/Retain') 3 | , Skip = require('./operations/Skip') 4 | , Insert = require('./operations/Insert') 5 | 6 | function Builder() { 7 | this.ops = [] 8 | this.addendum = '' 9 | this.removendum = '' 10 | } 11 | 12 | module.exports = Builder 13 | 14 | Builder.prototype.keep = 15 | Builder.prototype.retain = function(len) { 16 | this.ops.push(new Retain(len)) 17 | return this 18 | } 19 | 20 | Builder.prototype.delete = 21 | Builder.prototype.skip = function(str) { 22 | this.removendum += str 23 | this.ops.push(new Skip(str.length)) 24 | return this 25 | } 26 | 27 | Builder.prototype.add = 28 | Builder.prototype.insert = function(str) { 29 | this.addendum += str 30 | this.ops.push(new Insert(str.length)) 31 | return this 32 | } 33 | 34 | Builder.prototype.end = function() { 35 | var cs = new Changeset(this.ops) 36 | cs.addendum = this.addendum 37 | cs.removendum = this.removendum 38 | return cs 39 | } 40 | -------------------------------------------------------------------------------- /lib/Changeset.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * changesets 3 | * A Changeset library incorporating operational transformation (OT) 4 | * Copyright 2012 by Marcel Klehr 5 | * 6 | * (MIT LICENSE) 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | /** 27 | * A sequence of consecutive operations 28 | * 29 | * @param ops.. all passed operations will be added to the changeset 30 | */ 31 | function Changeset(ops/*or ops..*/) { 32 | this.addendum = "" 33 | this.removendum = "" 34 | this.inputLength = 0 35 | this.outputLength = 0 36 | 37 | if(!Array.isArray(ops)) ops = arguments 38 | for(var i=0; i= start) { 76 | if(op.input) { 77 | if(op.length != Infinity) oplen = op.length -Math.max(0, start-pos) -Math.max(0, (op.length+pos)-(start+len)) 78 | else oplen = len 79 | if (oplen !== 0) range.push( op.derive(oplen) ) // (Don't copy over more than len param allows) 80 | } 81 | else { 82 | range.push( op.derive(op.length) ) 83 | oplen = 0 84 | } 85 | l += oplen 86 | } 87 | pos += op.input 88 | } 89 | return range 90 | } 91 | 92 | /** 93 | * Merge two changesets (that are based on the same state!) so that the resulting changseset 94 | * has the same effect as both orignal ones applied one after the other 95 | * 96 | * @param otherCs 97 | * @param left Which op to choose if there's an insert tie (If you use this function in a distributed, synchronous environment, be sure to invert this param on the other site, otherwise it can be omitted safely)) 98 | * @returns 99 | */ 100 | Changeset.prototype.merge = function(otherCs, left) { 101 | if(!(otherCs instanceof Changeset)) { 102 | throw new Error('Argument must be a #, but received '+otherCs.__proto__.constructor.name) 103 | } 104 | 105 | if(otherCs.inputLength !== this.outputLength) { 106 | throw new Error("Changeset lengths for merging don't match! Input length of younger cs: "+otherCs.inputLength+', output length of older cs:'+this.outputLength) 107 | } 108 | 109 | var newops = [] 110 | , addPtr1 = 0 111 | , remPtr1 = 0 112 | , addPtr2 = 0 113 | , remPtr2 = 0 114 | , newaddendum = '' 115 | , newremovendum = '' 116 | 117 | zip(this, otherCs, function(op1, op2) { 118 | // console.log(newops) 119 | // console.log(op1, op2) 120 | 121 | // I'm deleting something -- the other cs can't know that, so just overtake my op 122 | if(op1 && !op1.output) { 123 | newops.push(op1.merge().clone()) 124 | newremovendum += this.removendum.substr(remPtr1, op1.length) // overtake added chars 125 | remPtr1 += op1.length 126 | op1.length = 0 // don't gimme that one again. 127 | return 128 | } 129 | 130 | // op2 is an insert 131 | if(op2 && !op2.input) { 132 | newops.push(op2.merge().clone()) 133 | newaddendum += otherCs.addendum.substr(addPtr2, op2.length) // overtake added chars 134 | addPtr2 += op2.length 135 | op2.length = 0 // don't gimme that one again. 136 | return 137 | } 138 | 139 | // op2 is either a retain or a skip 140 | if(op2 && op2.input && op1) { 141 | // op2 retains whatever we do here (retain or insert), so just clone my op 142 | if(op2.output) { 143 | newops.push(op1.merge(op2).clone()) 144 | if(!op1.input) { // overtake addendum 145 | newaddendum += this.addendum.substr(addPtr1, op1.length) 146 | addPtr1 += op1.length 147 | } 148 | op1.length = 0 // don't gimme these again 149 | op2.length = 0 150 | }else 151 | 152 | // op2 deletes my retain here, so just clone the delete 153 | // (op1 can only be a retain and no skip here, cause we've handled skips above already) 154 | if(!op2.output && op1.input) { 155 | newops.push(op2.merge(op1).clone()) 156 | newremovendum += otherCs.removendum.substr(remPtr2, op2.length) // overtake added chars 157 | remPtr2 += op2.length 158 | op1.length = 0 // don't gimme these again 159 | op2.length = 0 160 | }else 161 | 162 | //otherCs deletes something I added (-1) +1 = 0 163 | { 164 | addPtr1 += op1.length 165 | op1.length = 0 // don't gimme these again 166 | op2.length = 0 167 | } 168 | return 169 | } 170 | 171 | console.log('oops', arguments) 172 | throw new Error('oops. This case hasn\'t been considered by the developer (error code: PBCAC)') 173 | }.bind(this)) 174 | 175 | var newCs = new Changeset(newops) 176 | newCs.addendum = newaddendum 177 | newCs.removendum = newremovendum 178 | 179 | return newCs 180 | } 181 | 182 | /** 183 | * A private and quite handy function that slices ops into equally long pieces and applies them on a mapping function 184 | * that can determine the iteration steps by setting op.length to 0 on an op (equals using .next() in a usual iterator) 185 | */ 186 | function zip(cs1, cs2, func) { 187 | var opstack1 = cs1.map(function(op) {return op.clone()}) // copy ops 188 | , opstack2 = cs2.map(function(op) {return op.clone()}) 189 | 190 | var op2, op1 191 | while(opstack1.length || opstack2.length) {// iterate through all outstanding ops of this cs 192 | op1 = opstack1[0]? opstack1[0].clone() : null 193 | op2 = opstack2[0]? opstack2[0].clone() : null 194 | 195 | if(op1) { 196 | if(op2) op1 = op1.derive(Math.min(op1.length, op2.length)) // slice 'em into equally long pieces 197 | if(opstack1[0].length > op1.length) opstack1[0] = opstack1[0].derive(opstack1[0].length-op1.length) 198 | else opstack1.shift() 199 | } 200 | 201 | if(op2) { 202 | if(op1) op2 = op2.derive(Math.min(op1.length, op2.length)) // slice 'em into equally long pieces 203 | if(opstack2[0].length > op2.length) opstack2[0] = opstack2[0].derive(opstack2[0].length-op2.length) 204 | else opstack2.shift() 205 | } 206 | 207 | func(op1, op2) 208 | 209 | if(op1 && op1.length) opstack1.unshift(op1) 210 | if(op2 && op2.length) opstack2.unshift(op2) 211 | } 212 | } 213 | 214 | /** 215 | * Inclusion Transformation (IT) or Forward Transformation 216 | * 217 | * transforms the operations of the current changeset against the 218 | * all operations in another changeset in such a way that the 219 | * effects of the latter are effectively included. 220 | * This is basically like a applying the other cs on this one. 221 | * 222 | * @param otherCs 223 | * @param left Which op to choose if there's an insert tie (If you use this function in a distributed, synchronous environment, be sure to invert this param on the other site, otherwise it can be omitted safely) 224 | * 225 | * @returns 226 | */ 227 | Changeset.prototype.transformAgainst = function(otherCs, left) { 228 | if(!(otherCs instanceof Changeset)) { 229 | throw new Error('Argument to Changeset#transformAgainst must be a #, but received '+otherCs.__proto__.constructor.name) 230 | } 231 | 232 | if(this.inputLength != otherCs.inputLength) { 233 | throw new Error('Can\'t transform changesets with differing inputLength: '+this.inputLength+' and '+otherCs.inputLength) 234 | } 235 | 236 | var transformation = new ChangesetTransform(this, [new Retain(Infinity)]) 237 | otherCs.forEach(function(op) { 238 | var nextOp = this.subrange(transformation.pos, Infinity)[0] // next op of this cs 239 | if(nextOp && !nextOp.input && !op.input) { // two inserts tied; left breaks it 240 | if (left) transformation.writeOutput(transformation.readInput(nextOp.length)) 241 | } 242 | op.apply(transformation) 243 | }.bind(this)) 244 | 245 | return transformation.result() 246 | } 247 | 248 | /** 249 | * Exclusion Transformation (ET) or Backwards Transformation 250 | * 251 | * transforms all operations in the current changeset against the operations 252 | * in another changeset in such a way that the impact of the latter are effectively excluded 253 | * 254 | * @param changeset the changeset to substract from this one 255 | * @param left Which op to choose if there's an insert tie (If you use this function in a distributed, synchronous environment, be sure to invert this param on the other site, otherwise it can be omitted safely) 256 | * @returns 257 | */ 258 | Changeset.prototype.substract = function(changeset, left) { 259 | // The current operations assume that the changes in 260 | // `changeset` happened before, so for each of those ops 261 | // we create an operation that undoes its effect and 262 | // transform all our operations on top of the inverse changes 263 | return this.transformAgainst(changeset.invert(), left) 264 | } 265 | 266 | /** 267 | * Returns the inverse Changeset of the current one 268 | * 269 | * Changeset.invert().apply(Changeset.apply(document)) == document 270 | */ 271 | Changeset.prototype.invert = function() { 272 | // invert all ops 273 | var newCs = new Changeset(this.map(function(op) { 274 | return op.invert() 275 | })) 276 | 277 | // removendum becomes addendum and vice versa 278 | newCs.addendum = this.removendum 279 | newCs.removendum = this.addendum 280 | 281 | return newCs 282 | } 283 | 284 | /** 285 | * Applies this changeset on a text 286 | */ 287 | Changeset.prototype.apply = function(input) { 288 | // pre-requisites 289 | if(input.length != this.inputLength) throw new Error('Input length doesn\'t match expected length. expected: '+this.inputLength+'; actual: '+input.length) 290 | 291 | var operation = new TextTransform(input, this.addendum, this.removendum) 292 | 293 | this.forEach(function(op) { 294 | // each Operation has access to all pointers as well as the input, addendum and removendum (the latter are immutable) 295 | op.apply(operation) 296 | }.bind(this)) 297 | 298 | return operation.result() 299 | } 300 | 301 | /** 302 | * Returns an array of strings describing this changeset's operations 303 | */ 304 | Changeset.prototype.inspect = function() { 305 | var j = 0 306 | return this.map(function(op) { 307 | var string = '' 308 | 309 | if(!op.input) { // if Insert 310 | string = this.addendum.substr(j,op.length) 311 | j += op.length 312 | return string 313 | } 314 | 315 | for(var i=0; i The changeset to be serialized 327 | * @returns The serialized changeset 328 | */ 329 | Changeset.prototype.pack = function() { 330 | var packed = this.map(function(op) { 331 | return op.pack() 332 | }).join('') 333 | 334 | var addendum = this.addendum.replace(/%/g, '%25').replace(/\|/g, '%7C') 335 | , removendum = this.removendum.replace(/%/g, '%25').replace(/\|/g, '%7C') 336 | return packed+'|'+addendum+'|'+removendum 337 | } 338 | Changeset.prototype.toString = function() { 339 | return this.pack() 340 | } 341 | 342 | /** 343 | * Unserializes the output of cs.text.Changeset#toString() 344 | * 345 | * @param packed The serialized changeset 346 | * @param 347 | */ 348 | Changeset.unpack = function(packed) { 349 | if(packed == '') throw new Error('Cannot unpack from empty string') 350 | var components = packed.split('|') 351 | , opstring = components[0] 352 | , addendum = components[1].replace(/%7c/gi, '|').replace(/%25/g, '%') 353 | , removendum = components[2].replace(/%7c/gi, '|').replace(/%25/g, '%') 354 | 355 | var matches = opstring.match(/[=+-]([^=+-])+/g) 356 | if(!matches) throw new Error('Cannot unpack invalidly serialized op string') 357 | 358 | var ops = [] 359 | matches.forEach(function(s) { 360 | var symbol = s.substr(0,1) 361 | , data = s.substr(1) 362 | if(Skip.prototype.symbol == symbol) return ops.push(Skip.unpack(data)) 363 | if(Insert.prototype.symbol == symbol) return ops.push(Insert.unpack(data)) 364 | if(Retain.prototype.symbol == symbol) return ops.push(Retain.unpack(data)) 365 | throw new Error('Invalid changeset representation passed to Changeset.unpack') 366 | }) 367 | 368 | var cs = new Changeset(ops) 369 | cs.addendum = addendum 370 | cs.removendum = removendum 371 | 372 | return cs 373 | } 374 | 375 | Changeset.create = function() { 376 | return new Builder 377 | } 378 | 379 | /** 380 | * Returns a Changeset containing the operations needed to transform text1 into text2 381 | * 382 | * @param text1 383 | * @param text2 384 | */ 385 | Changeset.fromDiff = function(diff) { 386 | /** 387 | * The data structure representing a diff is an array of tuples: 388 | * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] 389 | * which means: delete 'Hello', add 'Goodbye' and keep ' world.' 390 | */ 391 | var DIFF_DELETE = -1; 392 | var DIFF_INSERT = 1; 393 | var DIFF_EQUAL = 0; 394 | 395 | var ops = [] 396 | , removendum = '' 397 | , addendum = '' 398 | 399 | diff.forEach(function(d) { 400 | if (DIFF_DELETE == d[0]) { 401 | ops.push(new Skip(d[1].length)) 402 | removendum += d[1] 403 | } 404 | 405 | if (DIFF_INSERT == d[0]) { 406 | ops.push(new Insert(d[1].length)) 407 | addendum += d[1] 408 | } 409 | 410 | if(DIFF_EQUAL == d[0]) { 411 | ops.push(new Retain(d[1].length)) 412 | } 413 | }) 414 | 415 | var cs = new Changeset(ops) 416 | cs.addendum = addendum 417 | cs.removendum = removendum 418 | return cs 419 | } 420 | -------------------------------------------------------------------------------- /lib/ChangesetTransform.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * changesets 3 | * A Changeset library incorporating operational ChangesetTransform (OT) 4 | * Copyright 2012 by Marcel Klehr 5 | * 6 | * (MIT LICENSE) 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | var Retain = require('./operations/Retain') 27 | , Skip = require('./operations/Skip') 28 | , Insert = require('./operations/Insert') 29 | , Changeset = require('./Changeset') 30 | 31 | 32 | function ChangesetTransform(inputCs, addendum) { 33 | this.output = [] 34 | this.addendum = addendum 35 | this.newRemovendum = '' 36 | this.newAddendum = '' 37 | 38 | this.cs = inputCs 39 | this.pos = 0 40 | this.addendumPointer = 0 41 | this.removendumPointer = 0 42 | } 43 | module.exports = ChangesetTransform 44 | 45 | ChangesetTransform.prototype.readInput = function (len) { 46 | var ret = this.cs.subrange(this.pos, len) 47 | this.pos += len 48 | return ret 49 | } 50 | 51 | ChangesetTransform.prototype.readAddendum = function (len) { 52 | //return [new Retain(len)] 53 | var ret = this.subrange(this.addendum, this.addendumPointer, len) 54 | this.addendumPointer += len 55 | return ret 56 | } 57 | 58 | ChangesetTransform.prototype.writeRemovendum = function (range) { 59 | range 60 | .filter(function(op) {return !op.output}) 61 | .forEach(function(op) { 62 | this.removendumPointer += op.length 63 | }.bind(this)) 64 | } 65 | 66 | ChangesetTransform.prototype.writeOutput = function (range) { 67 | this.output = this.output.concat(range) 68 | range 69 | .filter(function(op) {return !op.output}) 70 | .forEach(function(op) { 71 | this.newRemovendum += this.cs.removendum.substr(this.removendumPointer, op.length) 72 | this.removendumPointer += op.length 73 | }.bind(this)) 74 | } 75 | 76 | ChangesetTransform.prototype.subrange = function (range, start, len) { 77 | if(len) return this.cs.subrange.call(range, start, len) 78 | else return range.filter(function(op){ return !op.input}) 79 | } 80 | 81 | ChangesetTransform.prototype.result = function() { 82 | this.writeOutput(this.readInput(Infinity)) 83 | var newCs = new Changeset(this.output) 84 | newCs.addendum = this.cs.addendum 85 | newCs.removendum = this.newRemovendum 86 | return newCs 87 | } 88 | -------------------------------------------------------------------------------- /lib/Operator.js: -------------------------------------------------------------------------------- 1 | function Operator() { 2 | } 3 | 4 | module.exports = Operator 5 | 6 | Operator.prototype.clone = function() { 7 | return this.derive(this.length) 8 | } 9 | 10 | Operator.prototype.derive = function(len) { 11 | return new (this.constructor)(len) 12 | } 13 | 14 | Operator.prototype.pack = function() { 15 | return this.symbol + (this.length).toString(36) 16 | } 17 | -------------------------------------------------------------------------------- /lib/TextTransform.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * changesets 3 | * A Changeset library incorporating operational Apply (OT) 4 | * Copyright 2012 by Marcel Klehr 5 | * 6 | * (MIT LICENSE) 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | var Retain = require('./operations/Retain') 27 | , Skip = require('./operations/Skip') 28 | , Insert = require('./operations/Insert') 29 | , Insert = require('./Changeset') 30 | 31 | 32 | function TextTransform(input, addendum, removendum) { 33 | this.output = '' 34 | 35 | this.input = input 36 | this.addendum = addendum 37 | this.removendum = removendum 38 | this.pos = 0 39 | this.addPos = 0 40 | this.remPos = 0 41 | } 42 | module.exports = TextTransform 43 | 44 | TextTransform.prototype.readInput = function (len) { 45 | var ret = this.input.substr(this.pos, len) 46 | this.pos += len 47 | return ret 48 | } 49 | 50 | TextTransform.prototype.readAddendum = function (len) { 51 | var ret = this.addendum.substr(this.addPos, len) 52 | this.addPos += len 53 | return ret 54 | } 55 | 56 | TextTransform.prototype.writeRemovendum = function (range) { 57 | //var expected = this.removendum.substr(this.remPos, range.length) 58 | //if(range != expected) throw new Error('Removed chars don\'t match removendum. expected: '+expected+'; actual: '+range) 59 | this.remPos += range.length 60 | } 61 | 62 | TextTransform.prototype.writeOutput = function (range) { 63 | this.output += range 64 | } 65 | 66 | TextTransform.prototype.subrange = function (range, start, len) { 67 | return range.substr(start, len) 68 | } 69 | 70 | TextTransform.prototype.result = function() { 71 | this.writeOutput(this.readInput(Infinity)) 72 | return this.output 73 | } 74 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * changesets 3 | * A Changeset library incorporating operational transformation (OT) 4 | * Copyright 2012 by Marcel Klehr 5 | * 6 | * (MIT LICENSE) 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | var Changeset = require('./Changeset') 27 | , Retain = require('./operations/Retain') 28 | , Skip = require('./operations/Skip') 29 | , Insert = require('./operations/Insert') 30 | 31 | exports.Operator = require('./Operator') 32 | exports.Changeset = Changeset 33 | exports.Insert = Insert 34 | exports.Retain = Retain 35 | exports.Skip = Skip 36 | 37 | if('undefined' != typeof window) window.changesets = exports 38 | 39 | /** 40 | * Serializes the given changeset in order to return a (hopefully) more compact representation 41 | * that can be sent through a network or stored in a database 42 | * @alias cs.text.Changeset#pack 43 | */ 44 | exports.pack = function(cs) { 45 | return cs.pack() 46 | } 47 | 48 | /** 49 | * Unserializes the output of cs.text.pack 50 | * @alias cs.text.Changeset.unpack 51 | */ 52 | exports.unpack = function(packed) { 53 | return Changeset.unpack(packed) 54 | } 55 | 56 | 57 | 58 | 59 | /** 60 | * shareJS ot type API sepc support 61 | */ 62 | 63 | exports.name = 'changesets' 64 | exports.url = 'https://github.com/marcelklehr/changesets' 65 | 66 | /** 67 | * create([initialText]) 68 | * 69 | * creates a snapshot (optionally with supplied intial text) 70 | */ 71 | exports.create = function(initText) { 72 | return initText || '' 73 | } 74 | 75 | /** 76 | * Apply a changeset on a snapshot creating a new one 77 | * 78 | * The old snapshot object mustn't be used after calling apply on it 79 | * returns the resulting 80 | */ 81 | exports.apply = function(snapshot, op) { 82 | op = exports.unpack(op) 83 | return op.apply(snapshot) 84 | } 85 | 86 | /** 87 | * Transform changeset1 against changeset2 88 | */ 89 | exports.transform = function (op1, op2, side) { 90 | op1 = exports.unpack(op1) 91 | op2 = exports.unpack(op2) 92 | return exports.pack(op1.transformAgainst(op2, ('left'==side))) 93 | } 94 | 95 | /** 96 | * Merge two changesets into one 97 | */ 98 | exports.compose = function (op1, op2) { 99 | op1 = exports.unpack(op1) 100 | op2 = exports.unpack(op2) 101 | return exports.pack(op1.merge(op2)) 102 | } 103 | 104 | /** 105 | * Invert a changeset 106 | */ 107 | exports.invert = function(op) { 108 | return exports.pack(exports.unpack(op).invert()) 109 | } 110 | -------------------------------------------------------------------------------- /lib/operations/Insert.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * changesets 3 | * A Changeset library incorporating operational transformation (OT) 4 | * Copyright 2012 by Marcel Klehr 5 | * 6 | * (MIT LICENSE) 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | var Operator = require('../Operator') 27 | 28 | /** 29 | * Insert Operator 30 | * Defined by: 31 | * - length 32 | * - input=0 33 | * - output=length 34 | * 35 | * @param length How many chars to be inserted 36 | */ 37 | function Insert(length) { 38 | this.length = length 39 | this.input = 0 40 | this.output = length 41 | } 42 | 43 | // True inheritance 44 | Insert.prototype = Object.create(Operator.prototype, { 45 | constructor: { 46 | value: Insert, 47 | enumerable: false, 48 | writable: true, 49 | configurable: true 50 | } 51 | }); 52 | module.exports = Insert 53 | Insert.prototype.symbol = '+' 54 | 55 | var Skip = require('./Skip') 56 | , Retain = require('./Retain') 57 | 58 | Insert.prototype.apply = function(t) { 59 | t.writeOutput(t.readAddendum(this.output)) 60 | } 61 | 62 | Insert.prototype.merge = function() { 63 | return this 64 | } 65 | 66 | Insert.prototype.invert = function() { 67 | return new Skip(this.length) 68 | } 69 | 70 | Insert.unpack = function(data) { 71 | return new Insert(parseInt(data, 36)) 72 | } 73 | -------------------------------------------------------------------------------- /lib/operations/Mark.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * changesets 3 | * A Changeset library incorporating operational transformation (OT) 4 | * Copyright 2012 by Marcel Klehr 5 | * 6 | * (MIT LICENSE) 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | var Operation = require('../Operation') 27 | 28 | /** 29 | * Mark Operation (a retain with attributes) 30 | * Defined by: 31 | * - length 32 | * - symbol 33 | * - input=output=length 34 | * 35 | * @param length How many chars to Mark 36 | * @param attribs A set of numbers that refer to attributes, if the number is positive the attribute is added, if negative, the attribute is removed. 37 | */ 38 | function Mark(length, attribs) { 39 | this.length = length 40 | this.input = length 41 | this.output = length 42 | this.attribs = attribs || {} 43 | this.symbol = '*' 44 | } 45 | 46 | var Retain = require('./Retain') 47 | 48 | // True inheritance 49 | Mark.prototype = Object.create(Retain.prototype, { 50 | constructor: { 51 | value: Mark, 52 | enumerable: false, 53 | writable: true, 54 | configurable: true 55 | } 56 | }); 57 | module.exports = Mark 58 | 59 | Mark.prototype.merge = function(op2) { 60 | var newop = new Mark(this.length) 61 | if(op2.attribs) Object.keys(op2.attribs).forEach(function(a) { 62 | newop[a] = 1 63 | }) 64 | Object.keys(this.attribs).forEach(function(a) { 65 | newop[a] = 1 66 | 67 | // add and remove annihilate each other 68 | if(newop[-a]) { 69 | delete newop[-a] 70 | delete newop[a] 71 | } 72 | }) 73 | return newop 74 | }else return Retain.prototype.merge.apply(this, arguments) 75 | } 76 | 77 | Mark.unpack = function(data) { 78 | data = data.split('*').map(function(i) {parseInt(i, 36)}) 79 | var length = data.shift() 80 | , attribs = {} 81 | 82 | data.forEach(function(a) { 83 | attribs[a] = 1 84 | }) 85 | 86 | return new Mark(length, attribs) 87 | } 88 | -------------------------------------------------------------------------------- /lib/operations/Retain.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * changesets 3 | * A Changeset library incorporating operational transformation (OT) 4 | * Copyright 2012 by Marcel Klehr 5 | * 6 | * (MIT LICENSE) 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | var Operator = require('../Operator') 27 | 28 | /** 29 | * Retain Operator 30 | * Defined by: 31 | * - length 32 | * - input=output=length 33 | * 34 | * @param length How many chars to retain 35 | */ 36 | function Retain(length) { 37 | this.length = length 38 | this.input = length 39 | this.output = length 40 | } 41 | 42 | // True inheritance 43 | Retain.prototype = Object.create(Operator.prototype, { 44 | constructor: { 45 | value: Retain, 46 | enumerable: false, 47 | writable: true, 48 | configurable: true 49 | } 50 | }); 51 | module.exports = Retain 52 | Retain.prototype.symbol = '=' 53 | 54 | Retain.prototype.apply = function(t) { 55 | t.writeOutput(t.readInput(this.input)) 56 | } 57 | 58 | Retain.prototype.invert = function() { 59 | return this 60 | } 61 | 62 | Retain.prototype.merge = function(op2) { 63 | return this 64 | } 65 | 66 | Retain.unpack = function(data) { 67 | return new Retain(parseInt(data, 36)) 68 | } 69 | -------------------------------------------------------------------------------- /lib/operations/Skip.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * changesets 3 | * A Changeset library incorporating operational transformation (OT) 4 | * Copyright 2012 by Marcel Klehr 5 | * 6 | * (MIT LICENSE) 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | var Operator = require('../Operator') 27 | 28 | /** 29 | * Skip Operator 30 | * Defined by: 31 | * - length 32 | * - input=length 33 | * - output=0 34 | * 35 | * @param length How many chars to be Skip 36 | */ 37 | function Skip(length) { 38 | this.length = length 39 | this.input = length 40 | this.output = 0 41 | } 42 | 43 | // True inheritance 44 | Skip.prototype = Object.create(Operator.prototype, { 45 | constructor: { 46 | value: Skip, 47 | enumerable: false, 48 | writable: true, 49 | configurable: true 50 | } 51 | }); 52 | module.exports = Skip 53 | Skip.prototype.symbol = '-' 54 | 55 | var Insert = require('./Insert') 56 | , Retain = require('./Retain') 57 | , Changeset = require('../Changeset') 58 | 59 | Skip.prototype.apply = function(t) { 60 | var input = t.readInput(this.input) 61 | t.writeRemovendum(input) 62 | t.writeOutput(t.subrange(input, 0, this.output)) // retain Inserts in my range 63 | } 64 | 65 | Skip.prototype.merge = function(op2) { 66 | return this 67 | } 68 | 69 | Skip.prototype.invert = function() { 70 | return new Insert(this.length) 71 | } 72 | 73 | Skip.unpack = function(data) { 74 | return new Skip(parseInt(data, 36)) 75 | } 76 | -------------------------------------------------------------------------------- /ot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelklehr/changesets/730f66de6f14a3c648aa29b2857a2a5babb8a97f/ot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "changesets", 3 | "version": "1.0.2", 4 | "description": "Changeset library incorporating an operational transformation (OT) algorithm - for node and the browser, with shareJS support", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/marcelklehr/changesets.git" 8 | }, 9 | "keywords": [ 10 | "operational transformation", 11 | "ot", 12 | "changesets", 13 | "diff", 14 | "Forward Transformation", 15 | "Backward Transformation", 16 | "Inclusion Transformation", 17 | "Exclusion Transformation", 18 | "collaborative", 19 | "undo", 20 | "text" 21 | ], 22 | "main": "./lib/index", 23 | "scripts": { 24 | "test": "vows ./test/*" 25 | }, 26 | "dependencies": {}, 27 | "devDependencies": { 28 | "diff_match_patch": "*", 29 | "vows": "0.7.x" 30 | }, 31 | "author": "Marcel Klehr ", 32 | "license": "MIT", 33 | "readmeFilename": "README.md" 34 | } 35 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | cs = require('./lib/Changeset') 2 | 3 | var a = 'a' 4 | , a2 = a+'0' 5 | , b = 'b' 6 | c = cs.fromDiff(a, b) 7 | console.log(a, '=>', b, ' = cs('+ c+')') 8 | 9 | console.log('cs.apply(a) =', c.apply(a)) 10 | 11 | c2 = cs.fromDiff(a, a2) 12 | 13 | console.log(a, '=>', a+'0', ' = cs2('+ c2+')') 14 | 15 | var n = c.transformAgainst(c2) 16 | 17 | console.log('transform(cs, cs2).apply(a2) =', n.apply(a2)) -------------------------------------------------------------------------------- /test/text.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * changesets 3 | * A Changeset library incorporating operational transformation (OT) 4 | * Copyright 2012 by Marcel Klehr 5 | * 6 | * (MIT LICENSE) 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | */ 25 | 26 | var vows = require('vows') 27 | , assert = require('assert') 28 | 29 | var changesets = require('../lib') 30 | , Changeset = changesets.Changeset 31 | 32 | var dmp = require('diff_match_patch') 33 | , engine = new dmp.diff_match_patch 34 | 35 | 36 | function constructChangeset(text1, text2) { 37 | var diff = engine.diff_main(text1, text2) 38 | engine.diff_cleanupEfficiency(diff) 39 | return Changeset.fromDiff(diff) 40 | } 41 | 42 | var suite = vows.describe('changesets: operational transformation of text') 43 | 44 | // IT 45 | 46 | ;// Insert onto Insert 47 | [ ["123", ["a123", "123b"], "a123b", "Insert onto Insert; o1.pos < o2.pos"] 48 | , ["123", ["1a23", "1b23"], "1ab23", "Insert onto Insert; o1.pos = o2.pos"] 49 | , ["123", ["12a3", "b123"], "b12a3", "Insert onto Insert; o2.pos < o1.pos"] 50 | // Insert onto Delete 51 | , ["123", ["1a23", "12"], "1a2", "Insert onto Delete; o1.pos < o2.pos"] 52 | , ["123", ["1a23", "13"], "1a3", "Insert onto Delete; o1.pos = o2.pos"] 53 | , ["123", ["12a3", "3"], "a3", "Insert onto Delete; o2.pos+len = o1.pos"] 54 | , ["123", ["12a3", "23"], "2a3", "Insert onto Delete; o2.pos < o1.pos"] 55 | , ["123", ["12a3", "1"], "1a", "Insert onto Delete; o1.pos < o2.pos+len"] 56 | , ["123", ["12a3", ""], "a", "Insert onto Delete; o2.pos < o1 < o2.pos+len"] 57 | // Delete onto Delete 58 | , ["1234", ["124", "234"], "24", "Delete onto Delete; o2.pos+len < o1.pos"] 59 | , ["1234", ["234", "124"], "24", "Delete onto Delete; o1.pos < o2.pos"] 60 | , ["123", ["3", "13"], "3", "Delete onto Delete; something at the end of my range has already been deleted"] 61 | , ["123", ["3", "23"], "3", "Delete onto Delete; something at the beginning of my range has already been deleted"] 62 | , ["1234", ["4", "134"], "4", "Delete onto Delete; something in the middle of my range has already been deleted"] 63 | , ["123", ["13", "1"], "1", "Delete onto Delete; my whole range has already been deleted ('twas at the beginning of the other change's range)"] 64 | , ["123", ["12", "1"], "1", "Delete onto Delete; my whole range has already been deleted ('twas at the end of the other change's range)"] 65 | , ["1234", ["134", "4"], "4", "Delete onto Delete; my whole range has already been deleted ('twas in the middle of the other change's range)"] 66 | // Delete onto Insert 67 | , ["123", ["23", "123b"], "23b", "Delete onto Insert; o1.pos+len < o2.pos"] 68 | , ["123", ["3", "1b23"], "b3", "Delete onto Insert; o1.pos < o2.pos < o2.pos+len < o1.pos+len"] 69 | , ["123", ["13", "1b23"], "1b3", "Delete onto Insert; o1.pos = o2.pos , o1.len = o2.len"] 70 | , ["123", ["1", "1b23"], "1b", "Delete onto Insert; o1.pos = o2.pos, o2.len < o1.len"] 71 | , ["123", ["1", "1bbb23"], "1bbb", "Delete onto Insert; o1.pos = o2.pos, o1.len < o2.len"] 72 | , ["123", ["12", "b123"], "b12", "Delete onto Insert; o2.pos+len < o1.pos"] 73 | // Insert onto nothing 74 | , ["123", ["1a2b3c", "123"], "1a2b3c", "Insert onto Nothing"] 75 | ] 76 | .forEach(function(test, i) { 77 | var batch = {} 78 | batch[test[3]] = { 79 | topic: function() { 80 | try{ 81 | var cs1 = constructChangeset(test[0],test[1][0]) 82 | , cs2 = constructChangeset(test[0],test[1][1]) 83 | 84 | console.log("\n\n", test[0]) 85 | console.dir(cs1.inspect()) 86 | console.dir(cs2.inspect()) 87 | 88 | cs1 = cs1.transformAgainst(cs2, /*left:*/true) 89 | console.log('=>', cs1.inspect()) 90 | 91 | return cs1.apply(cs2.apply(test[0])) 92 | }catch(e) { 93 | console.log(e.stack ||e) 94 | process.exit(1) 95 | } 96 | }, 97 | 'should be correctly transformed using inclusion transformation': function(err, text) { 98 | if(err) console.log(err.stack || err) 99 | assert.ifError(err) 100 | assert.equal(test[2], text) 101 | } 102 | } 103 | suite.addBatch(batch) 104 | }) 105 | 106 | // ET 107 | 108 | ; 109 | ETset = 110 | // Insert minus Insert 111 | [ [["123", "123b", "a123b"], "a123", "Insert minus Insert; o2.pos < o1.pos"] 112 | , [["123", "b123", "b12a3"], "12a3", "Insert minus Insert; o1.pos < o2.pos"] 113 | , [["123", "bb123", "bab123"], "a123", "Insert minus Insert; o1.pos < o2.pos < o1.pos+len"] 114 | // Insert minus Delete 115 | , [["1234", "124", "a124"], "a1234", "Insert minus Delete; o2.pos < o1.pos"] 116 | , [["1234", "134", "1a34"], "1a234", "Insert minus Delete; o2.pos = o1.pos"] 117 | , [["1234", "34", "3a4"], "123a4", "Insert minus Delete; o1.pos < o2.pos"] 118 | // Delete minus Insert 119 | , [["123", "a123", "a13"], "13", "Delete minus Insert; o1.pos < o2.pos"] 120 | , [["123", "123a", "13a"], "13", "Delete minus Insert; o2.pos < o1.pos"] 121 | , [["1234", "12a34", "14"], "14", "Delete minus Insert; o2.pos < o1.pos < o2.pos+len"] 122 | , [["123", "12abc3", "12ac3"], "123", "Delete minus Insert; o1.pos < o2.pos < o2.pos+len < o1.pos+len"] 123 | // Delete minus Delete 124 | , [["1234", "34", "4"], "124", "Delete minus Delete; o1.pos < o2.pos"] 125 | , [["1234", "123", "23"], "234", "Delete minus Delete; o2.pos < o1.pos"] 126 | , [["1234", "123", "12"], "124", "Delete minus Delete; o2.pos < o1.pos"] 127 | // Mixed ET 128 | , [["1234", "2bc3", "2abc3"], "12a34", "Mixed ET 1"] 129 | , [["1234", "d2bc", "da2abc"], "1234aa", "Mixed ET 2"]// yea. this is because of using cleanup_efficiency 130 | ] 131 | ETset.forEach(function(test, i) { 132 | var batch = {} 133 | batch[test[2]] = { 134 | topic: function() { 135 | var cs1 = constructChangeset(test[0][0],test[0][1]) 136 | , cs2 = constructChangeset(test[0][1],test[0][2]) 137 | 138 | console.log("\n\n "+test[0][0]+":", test[0][2], '-', test[0][1]) 139 | console.dir(cs1.inspect()) 140 | console.dir(cs1.invert()) 141 | console.dir(cs2.inspect()) 142 | 143 | cs2 = cs2.substract(cs1, /*left:*/true) 144 | console.log('=>', cs2.inspect()) 145 | 146 | return cs2.apply(test[0][0]) 147 | }, 148 | 'should be correctly transformed using exclusion transformation': function(err, text) { 149 | assert.ifError(err) 150 | assert.equal(test[1], text) 151 | } 152 | } 153 | suite.addBatch(batch) 154 | }) 155 | 156 | // MERGING 157 | 158 | ETset.forEach(function(test, i) { 159 | var batch = {}; 160 | batch[test[2]] = { 161 | topic: function() { 162 | try{ 163 | var cs1 = constructChangeset(test[0][0],test[0][1]) 164 | , cs2 = constructChangeset(test[0][1],test[0][2]) 165 | , merged 166 | 167 | console.log("\n\n", test[0]+': merging') 168 | console.dir(cs1.inspect()) 169 | console.dir(cs2.inspect()) 170 | 171 | merged = cs1.merge(cs2, /*left:*/true) 172 | console.log('=>', merged.inspect()) 173 | 174 | return merged.apply(test[0][0]) 175 | }catch(e){ 176 | console.log(e.stack||e) 177 | process.exit(1) 178 | } 179 | }, 180 | 'should be correctly merged': function(err, text) { 181 | assert.ifError(err) 182 | assert.equal(test[0][2], text) 183 | } 184 | } 185 | suite.addBatch(batch) 186 | }) 187 | 188 | 189 | var packUnpack1 = "012345678901234567890123456789" 190 | , packUnpack2 = "012345678aaaaaaaaaaaaaaaaaaaaaa8aaaaaaaaaaaaaaaaaaaa9" 191 | suite.addBatch({ 192 | 'pack/unpack': 193 | { topic: function() { 194 | return constructChangeset(packUnpack1, packUnpack2) 195 | } 196 | , 'should be packed and unpacked correctly': function(er, cs) { 197 | var packed = cs.pack() 198 | console.log() 199 | console.log(cs.inspect()) 200 | console.log(packed) 201 | var unpacked = Changeset.unpack(packed) 202 | assert.equal(unpacked.apply(packUnpack1), packUnpack2) 203 | } 204 | } 205 | }) 206 | 207 | // Invert 208 | 209 | ;// Inverting Insert 210 | [ ["123", "123b", "Insert at the end"] 211 | , ["123", "b123", "Insert at the beginning"] 212 | , ["123", "1b23", "Insert in the middle"] 213 | // Inverting Delete 214 | , ["123", "12", "Delete at the end"] 215 | , ["123", "23", "Delete at the beginning"] 216 | , ["123", "13", "Delete in the middle"] 217 | // Inverting Equal 218 | , ["123", "123", "Identity"] 219 | ] 220 | .forEach(function(test, i) { 221 | var batch = {} 222 | batch[test[2]] = { 223 | topic: function() { 224 | console.log("\n\n "+test[0]+": inverting ", test[1]) 225 | 226 | var cs1 = constructChangeset(test[0], test[1]) 227 | , cs2 = cs1.invert() 228 | 229 | console.dir(cs1.inspect()) 230 | console.dir(cs2.inspect()) 231 | 232 | var text = cs1.apply(test[0]) 233 | return cs2.apply(text) 234 | }, 235 | 'should be correctly inverted': function(err, text) { 236 | assert.ifError(err) 237 | assert.equal(test[0], text) 238 | } 239 | } 240 | suite.addBatch(batch) 241 | }) 242 | 243 | suite.addBatch({ 244 | 'mixed (insert, retain, delete)': 245 | { topic: function() { 246 | return ["Hello adventurer!", 247 | constructChangeset("Hello adventurer!", "Hello treasured adventurer!") 248 | , constructChangeset("Hello adventurer!", "Good day adventurers, y'all!") 249 | ] 250 | } 251 | , 'should cause the same outcome ragardless of the transformation order': function(er, cs) { 252 | var text1 = cs[1].transformAgainst(cs[2], /*left:*/true).apply( cs[2].apply(cs[0]) ) 253 | var text2 = cs[2].transformAgainst(cs[1], /*left:*/false).apply( cs[1].apply(cs[0]) ) 254 | console.log(text1, text2) 255 | assert.equal(text1, text2) 256 | } 257 | } 258 | }) 259 | 260 | suite.addBatch({ 261 | 'accessories': 262 | { topic: function() { 263 | return [constructChangeset("1234", "1234b"), constructChangeset("1234", "1234a")] 264 | } 265 | , 'should cause the same outcome ragardless of the transformation order': function(er, cs) { 266 | var text1 = cs[0].transformAgainst(cs[1], /*left:*/true).apply( cs[1].apply("1234") ) 267 | var text2 = cs[1].transformAgainst(cs[0], /*left:*/false).apply( cs[0].apply("1234") ) 268 | console.log(text1, text2) 269 | assert.equal(text1, text2) 270 | } 271 | } 272 | }) 273 | 274 | suite.addBatch({ 275 | 'validation': 276 | { topic: function() { 277 | var cs = constructChangeset("1234", "12a34b") 278 | cs.apply(cs.apply("1234")) 279 | 280 | return this.callback() 281 | } 282 | , 'should error if you apply the same cs twice, without transforming it': function(er) { 283 | if(!er) assert(false) 284 | console.log(er) 285 | } 286 | } 287 | }) 288 | 289 | /**/ 290 | suite.export(module) 291 | --------------------------------------------------------------------------------