├── .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 [](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